Недавно я прошел испытание кода для стажировки в крупной технологической компании. Мне понравились вопросы, я делал по ним заметки, а ограничение по времени и набор тестов на их платформе позволили мне уточнить свой ответ, пока он мне не понравился. Вот первый вопрос, мой процесс, включая первоначальные попытки, и мой окончательный ответ. Я надеюсь, что они не возражают против того, что я это пишу - я не смог найти другого языка, который бы противоречил этому правилу.
Вопрос
Они написали небольшую предысторию, похожую на словесную проблему: «О, у нас есть билетное агентство yadda yadda и множество цен yadda yadda», - я точно не помню. Дело было в следующем: Напишите функцию, которая принимает массив целых чисел и дает длину самого длинного ряда последовательных целых чисел, включая повторения. Так, например, для ввода [5,1,6,8 , 5,2,2,1,6,4] он должен вернуть 5 для [4,5,5,6,6].
Я решил решать на Ruby.
Первая попытка
Мой первоначальный мыслительный процесс был следующим: мы можем подобрать это, проверив размер прогонов, которые вы можете получить, начиная с каждого элемента. Итак, в нашем примере, отметьте 1, вы получите 1,1,2,2 и остановитесь на этом. Сделайте это для каждого элемента, выберите самый длинный. Это наиболее наивный метод, но лучше всего начинать с получения результатов на бумаге.
def someTicketsFunction(ticketsArray) sortedTix = ticketsArray.sort m = 0 sortedTix.each{|price| currentprice = price innerm = 0 while sortedTix.include?(currentprice) innerm += sortedTix.count(currentprice) currentprice += 1 end m = [innerm, m].max } p m end
Это прекрасно работает, но очень неэффективно по времени. Вы можете видеть, что у него есть вложенная итерация (while внутри each), которую вы хотите исключить.
Что здесь происходит, так это то, что мы сначала сортируем входной массив с помощью удобного .sort рубина, превращая наш предыдущий пример [5,1,6,8,5,2,2,1,6,4] в [1,1,2,2,4,5,5,6,6,8]. Затем мы начинаем просматривать каждый элемент и проверять, является ли он началом цикла, и если да, мы находим размер цикла, сравниваем его с ранее сохраненным размером (m - почему Я называю его m?), А если он больше, замените на него m.
Считаем пробежку с помощью этого:
innerm = 0 while sortedTix.include?(currentprice) innerm += sortedTix.count(currentprice) currentprice += 1 end
Мы проверяем, содержит ли массив следующее целое число, затем следующее, а затем следующее. Это работает, но программе нужно утомиться.
Вторая попытка
При разработке этого метода первого прохода я понял, что нам не нужно проверять длину прогона для каждого элемента, а только тех, которые являются его началом. Другими словами, если во входном массиве есть 1 и 2, нет причин проверять запуск, начинающийся с 2 - мы точно знаем, что тот, который начинается с 1, будет длиннее. Итак, с деталями проработаны:
def someTicketsFunction(ticketsArray) sortedTix = ticketsArray.sort m = 0 sortedTix.each{|price| if sortedTix.include?(price - 1) nil else currentprice = price innerm = 0 while sortedTix.include?(currentprice) innerm += sortedTix.count(currentprice) currentprice += 1 end m = [innerm, m].max end } p m end
Мы проверяем, является ли это началом пробега, с помощью этого:
sortedTix.include?(price - 1)
Это проверяет, существует ли значение, на единицу меньшее, чем значение, которое мы сейчас проверяем, как запись в массиве. Если это так, значит, запись, которую мы сейчас проверяем, не является первой из прогона, поэтому нас это не волнует и мы можем перейти к следующему.
Это улучшение, поскольку теперь мы проверяем намного меньше значений, но это не решает нашу основную проблему итерации внутри итератора. Мы все еще слишком медлительны, и нам нужно найти способ обойти это.
Третья попытка (лучшее решение)
def someTicketsFunction(ticketsArray) tixFrequencyHash = ticketsArray.sort.reverse.each_with_object(Hash.new(0)){|key,hash| hash[key] += 1} m = 0 tixFrequencyHash.each{|price, frequency| tixFrequencyHash[price] = frequency + tixFrequencyHash[price + 1] m = [tixFrequencyHash, m].max } p m end
Впечатленный? Я, по крайней мере. Medium не любит эти длинные строки, по крайней мере, на моем экране, так что меня это беспокоит.
Идея здесь - частотный хеш. На самом деле, это не первый раз, когда я сталкиваюсь с подобным улучшением времени в тесте кода, поэтому, если вы проводите собеседование, я бы порекомендовал посмотреть и подумать о частотных хэшах. Частотный хеш для нашего предыдущего примера массива будет выглядеть так:
# the array [5,1,6,8,5,2,2,1,6,4] # frequency hash {5=>2, 1=>2, 6=>2, 8=>1, 2=>2, 4=>1}
Вот первый фрагмент неразберихи в нашей функции:
tixFrequencyHash = ticketsArray.sort.reverse.each_with_object(Hash.new(0)){|key,hash| hash[key] += 1}
Он состоит из двух частей. Во-первых, шаблонный (но модный!) Ruby-способ создания частотного хэша из массива:
tixFrequencyHash = ticketsArray.each_with_object(Hash.new(0)){|key,hash| hash[key] += 1}
А затем наше небольшое изменение: сначала мы сортируем и меняем местами массив, чтобы хэш частоты шел в порядке от наибольшего к наименьшему значению (цена билета или что-то еще. должно быть). Итак, для нашего примера:
{8=>1, 6=>2, 5=>2, 4=>1, 2=>2, 1=>2}
Это главный трюк в нашем рукаве. Теперь мы можем читать это слева направо, чтобы найти прогоны, вложенная итерация не требуется. Вы видите это?
tixFrequencyHash.each{|price, frequency| tixFrequencyHash[price] = frequency + tixFrequencyHash[price + 1] }
К каждому значению в хэше мы добавляем значение, назначенное ключу, который на единицу больше текущего ключа. Посмотрим, как это выглядит на нашем примере:
#initial state {8=>1, 6=>2, 5=>2, 4=>1, 2=>2, 1=>2} # tixFrequencyHash[8] = tixFrequencyHash[8] + tixFrequencyHash[9] {8=>1, 6=>2, 5=>2, 4=>1, 2=>2, 1=>2} # tixFrequencyHash[6] = tixFrequencyHash[6] + tixFrequencyHash[7] {8=>1, 6=>2, 5=>2, 4=>1, 2=>2, 1=>2} # tixFrequencyHash[5] = tixFrequencyHash[5] + tixFrequencyHash[6] {8=>1, 6=>2, 5=>4, 4=>1, 2=>2, 1=>2} # tixFrequencyHash[4] = tixFrequencyHash[4] + tixFrequencyHash[5] {8=>1, 6=>2, 5=>4, 4=>5, 2=>2, 1=>2} # tixFrequencyHash[2] = tixFrequencyHash[2] + tixFrequencyHash[3] {8=>1, 6=>2, 5=>4, 4=>5, 2=>2, 1=>2} # tixFrequencyHash[1] = tixFrequencyHash[1] + tixFrequencyHash[2] {8=>1, 6=>2, 5=>4, 4=>5, 2=>2, 1=>3}
По мере прохождения он собирает длину каждого прогона в значении, присвоенном ключу начала прогона! Шестерки, пятерки и четверки складываются в 4 = ›5.
Нет вложенной итерации - намного быстрее.
Прокомментируйте свои улучшения, и на следующей неделе я расскажу о второй проблеме из того же теста кода.
edit 13 января 2021 г. - (1) абзац продублирован не в том месте, не имеет смысла; (2) демонстрация частотного хеширования с ошибками