На техническом собеседовании проблемы с белой доской могут застать вас врасплох. Иногда бывает сложно последовать решению, которое вы придумываете в уме, и достичь финиша, особенно если кто-то пытается направить вас сделать это по-другому.

Находясь под давлением, если мы не можем придумать лучший ответ, по крайней мере, мы можем придумать рабочий Решение.

Возьмем эту давнюю проблему слияния наборов диапазонов:

Комбинируйте [1..5, 10..15, 35..40] с [5..20, 38..48]

Первая загадка здесь заключается в том, являются ли концы ваших диапазонов включающими или исключающими. Простой способ решить эту проблему - определить диапазоны в соответствии с началом и длиной, а не началом и концом. Конец диапазона - это расплывчатое понятие (потенциально включающее или исключающее), но длина очевидна.

Теперь, когда у нас есть способ точно выразить наши данные, приступим к работе.

Таким образом, входные данные представляют собой массив объектов диапазона, где объект диапазона может обозначаться буквально:

{
  start: 35,
  length: 5
}

Мы можем описать нашу функцию следующим образом:

function mergeRanges(arr, arr2) {
  let result = [];
  // compute something
  return result;
}

Мы знаем как данность, что в каждом из исходных массивов ничего не перекрывается. Но мы знаем, что объединенный результат может иметь размер от 1 элемента до суммы всех исходных элементов.

Поскольку любой элемент может перекрываться с любым другим элементом, я думаю, будет справедливо просмотреть каждый элемент один за другим, чтобы проверить, не перекрываются ли они с чем-то другим, и пройтись по каждому массиву, чтобы наши индексы были выровнены.

1..5   10..15        35...40
   5..........20       38.......48

Но увы, я забегаю вперед. Идея обхода обоих массивов в теории кажется простой, но реализация может оказаться сложной очень быстрой.

Итак, давайте перейдем к решению грубой силы. Просто попробуйте смешать каждый элемент с каждым другим элементом и повторяйте, пока ничего не столкнется. Мы будем использовать вложенные циклы, очень похожие на реализацию пузырьковой сортировки.

function mergeRanges(arr1, arr2) {
  let result = arr1.concat(arr2);
  let goAgain = true;
  while (goAgain) {
    goAgain = false;
    for (let i = 0; i < result.length — 1; i++) {
      for (let j = i + 1; j < result.length; j++) {
        if (overlaps(result[i], result[j])) {
          result[i] = merge(result[i], result[j]);
          result.splice(j, 1);
          j--; // since the element at j got trashed, walk backwards
          goAgain = true;
        }
      }
    }
  }
  return result;
}

Заметили, что я сделал несколько новых функций "на лету": перекрытия и слияние? Давайте заполним их. Теперь, что такое перекрытие? Перекрытие означает, что крайний левый ИЛИ крайний правый элемент одного диапазона присутствует в другом диапазоне.

[....]        [....]       [....]      [..........]
   [....]  [....]       [..........]      [....]

Сначала я создам вспомогательную функцию, чтобы получить эксклюзивный результат. Я знаю, что сказал, что длина была более интуитивной, и это так, но это избавляет нас от работы.

const getEnd = a => {
  return a.start + a.length;
}

Теперь функция перекрытия:

const overlaps = (a, b) => {
  let leftAinB = a.start >= b.start && a.start < getEnd(b);
  let rightAinB = getEnd(a) > b.start && getEnd(a) <= getEnd(b);
  return leftAinB || rightAinB;
}

И, наконец, функция слияния:

const merge = (a, b) => {
  // assume a and b overlap
  let start = Math.min(a.start, b.start);
  let length = Math.max(getEnd(a), getEnd(b)) — start;
  return { start, length };
}

Теперь предположим, что это решение работает и в нем нет ошибок. Насколько это быстро? Поскольку у нас есть концентрические циклы, и алгоритм может запускаться повторно каждый раз, когда элементы объединяются, время выполнения будет O (n³), где n представляет средний размер каждого из массивов. Это достойное решение для небольшого набора данных, но я думаю, что мы можем добиться большего. Можем ли мы создать решение O (m + n)?

function mergeRanges(arr1, arr2) {
  let index1 = 0;
  let index2 = 0;
  let result = [];
  while (index1 < arr1.length && index2 < arr2.length) {
    // case 1: range 1 comes before range 2 and does not overlap
    if (getEnd(arr1[index1]) < arr2[index2]) {
      result.push(arr1[index1]);
      index1++;
    }
    // case 2: range 2 comes before range 1 and does not overlap
    if (getEnd(arr2[index2]) < arr1[index1]) {
      result.push(arr2[index2]);
      index2++;
    }
    // case 3: they overlap
    result.push(merge(arr1[index1], arr2[index2]));
    index1++;
    index2++;
    // Wait a minute, I’m going to have to run through the list
    // of results again to see if they merge.
    // Ugh, I think it’s time to pull my hair out. All of it.
  }
  return result;
}

К сожалению, мой мозг превратился в тушеное мясо. Кайф от кофеина, который помог мне перебором, больше не работает. Я нервничаю. Мне стыдно. Я думаю про себя: Я просто тупой?

Если бы у меня было больше практики. Если бы я только лучше справлялся с такими проблемами. Я уже могу представить, что инженер, интервьюирующий меня, смотрит на меня сверху вниз, потому что знает ответ. Он умнее меня? Запомнил ли он решение? Я полный неудачник? Должен ли я просто продираться, как идиот, полностью осознавая, что я идиот без шансов на улучшение? Когда я попрошу о помощи, они поймут, что я волшебный единорог, и побалуют меня волшебной пылью? Думаю, займусь другой профессией.

Серьезно, эти мысли приходят мне в голову. Давление здесь реально, ребята. Что делать? Думаю, я собираюсь обсудить это, готовясь к худшему, но надеясь на лучшее.

Потому что реальность такова: в благоприятной среде окружающие нас люди будут нам помогать. В неблагоприятной среде они этого не сделают. Если кто-то не желает помочь вам в наиболее уязвимом состоянии, то ваше сияние им не светит. Это не потому, что у вас нет свечения, а потому, что их темнота его отталкивает.

Да, пройди через звонок, но нет, не чувствуй себя менее человеком из-за того, что тебя избили и избили. Спросите любого эксперта, и он скажет, что настойчивость окупается. Возможно, вы станете самым скромным, но в самом скромном человеке проявятся ваши истинные сильные стороны.

А пока используйте решение грубой силы. Как видите, это был довольно окольный путь решения этой проблемы, но для этого потребовалась только одна развилка, а не 10. Оптимизированное решение, безусловно, в моих силах, но для этого потребуется третья чашка кофе, и спасибо, но я действительно мог бы воспользоваться своим прекрасным отдыхом сегодня вечером.

Столкнувшись с невозможным компромиссом, выберите легкий выход. Когда вы сами себе босс, будьте амбициозны, но пока вы работаете на кого-то другого, просто делайте дела. Вы естественным образом поплывете в место, где вас ценят за волшебного единорога, внутри которого вы действительно находитесь, и там будет множество умных людей, которые окатят вас ведрами с волшебной пылью.

Так что идите туда, будь собой, и приберегите совершенство для повседневного ухода за собой.