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

[[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]]

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

С текущей матрицей результат должен быть 1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10.

Как подойти?

Лучший способ подойти к этой проблеме - распознать ряд закономерностей:

  1. Сначала нам нужно получить горизонтальный ряд целиком.
  2. Затем нам нужно получить последний элемент каждой оставшейся строки.
  3. Затем нам нужно получить остальные элементы нижнего ряда в обратном порядке.
  4. Затем нам нужно получить первый элемент из оставшихся строк снизу вверх.
  5. Наконец, мы используем рекурсию, чтобы повторить все, ЕСЛИ есть какие-либо оставшиеся элементы, пока мы не получим их все.

Следующее изображение может помочь визуализировать шаблоны. Мы начинаем с 1 и проходим матрицу:

Хорошо, приступим к работе

Шаг 0: давайте построим скелет функции

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

// Wel'll use this variable to fill with the numbers we're going to take from our matrix:
let endResult = [];
unrollMatrix = (array) => {
// This is our condition to check for any remaining elements and break out of recursion and give us the end result
if(array.length === 0) {
        console.log(endResult.join(', '));
        return;
    }
// 1.- Get the top row
// 2.- Get the last element of all remaining rows
// 3.- Get what's left of the bottom row in reverse
// 4.- Get the first element of any remaining rows
// 5.- Make the function call itself over whatever's left of our original matrix
}

Довольно простые вещи. Флажок вверху означает: «Продолжайте работать, пока ничего не будет, а когда это произойдет, распечатайте все числа в виде простого списка строк».

Шаг 1. Получите верхний ряд:

Это очень просто. Все, что нам нужно сделать, это сдвинуть, а затем поместить нашу матрицу в массив endResult. Используя оператор распространения, мы обязательно «экранируем» массив и получаем голые числа. Помните, что мы работаем с массивом массивов, и если бы мы опускали оператор распространения, мы бы получили первую строку в виде массива:

endResult.push(...array.shift());

Важно следить за тем, что происходит, поэтому имейте в виду, что и endResult, и наша исходная матрица содержат значения, отличные от тех, с которыми мы начали, поэтому на данном этапе endResult представляет собой массив:

endResult: Array(4) [ 1, 2, 3, 4 ]

В то время как наша исходная матрица «потеряла» первую строку, когда мы ее сдвинули:

matrix: Array(3)  [[5,   6,  7,   8],
                   [9,  10, 11,  12],
                   [13, 14, 15,  16]];

Шаг 2. Получите последний элемент всех оставшихся строк

Пока нам удалось получить первую строку, и теперь нам нужна «правая часть» нашей матрицы. Это означает 8, 12 и 16. Для этого нам нужно написать быстрый цикл, чтобы перебирать все оставшиеся массивы, получать их последний элемент и передавать его в нашу переменную endResult:

for(piece of array){
        endResult.push(piece.pop());
}

КТО, что здесь происходит? Не бойтесь, это просто более лаконичный способ написания цикла for. Он был введен в ES2015 и отбрасывает любые счетчики и условия для итерации по объекту. Проверьте эту статью MDN для более подробного изучения.

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

Проверка хода выполнения: наша переменная endResult должна выглядеть так:

endResult: Array(7) [ 1, 2, 3, 4, 8, 12, 16 ]

И наша исходная матрица должна выглядеть так:

matrix: Array(3)  [[5, 6, 7],
                   [9, 10, 11],
                   [13, 14, 15,]];

Шаг 3. Получите то, что осталось от нижнего ряда, в обратном порядке

Мы на полпути! Теперь нам нужно получить нижнюю строку в обратном порядке, что означает 15, 14 и 13. Все, что нам нужно сделать, это вызвать нашу матрицу с помощью оператора распространения, вытащить последний элемент и вставить его в наш endResult в обратном порядке:

endResult.push(...array.pop().reverse());

Довольно просто. Это похоже на первый шаг, только на этот раз мы используем pop (), чтобы получить последний элемент вместо первого, а затем нажимаем его с помощью reverse ().

Проверка выполнения: endResult теперь должен выглядеть так:

endResult: Array(10) [ 1, 2, 3, 4, 8, 12, 16, 15, 14, 13 ]

И наша матрица должна выглядеть так:

matrix: Array(2)  [[5, 6, 7],
                   [9, 10, 11]];

Шаг 4. Получите первый элемент из оставшихся строк.

Мы почти там! На этом последнем шаге нам нужно получить первый элемент каждого оставшегося массива снизу вверх, что в нашем случае будет 9, 5. Для этого нам просто нужно отобразить наши массивы, вызвать для них shift () и вставьте их в endResult в обратном порядке:

endResult.push(...array.map(arr => arr.shift()).reverse());

Наша функция почти готова, и теперь endResult должен выглядеть так:

endResult: Array(12) [ 1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5 ]

И наша матрица должна выглядеть так:

matrix: Array(2)  [[6, 7],
                   [10, 11]];

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

unrollMatrix(array);

И готово! Если ваше условие выхода работает нормально, вы должны получить ожидаемый результат:

1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10

Есть куда расти

То, что наша функция работает, не означает, что мы закончили. Нам нужно контролировать нашу временную сложность, и пока наша функция состоит из четырех основных операций. Давайте посмотрим на них и посмотрим, что можно улучшить:

endResult.push(...array.shift());

Это О (1), у нас все хорошо.

for(piece of array){
        endResult.push(piece.pop());
}

Это О (п), мы хороши, но не совсем.

endResult.push(...array.pop().reverse());

Это O (1), так что у нас все хорошо.

endResult.push(...array.map(arr => arr.shift()).reverse());

И, наконец, этот тоже O (1), так что у нас все еще хорошо.

Так что по большей части наша функция хороша. Если бы у нас не было альтернативы, временная сложность O (n) вполне подойдет, но если есть место для перехода от O (n) к O (1), то мы должны попытаться сделать это во что бы то ни стало:

endResult.push(...array.map(arr => arr.pop()))

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

Окончательный код

const matrix = [[1,   2,  3,   4],
                [5,   6,  7,   8],
                [9,  10, 11,  12],
                [13, 14, 15,  16]];
let endResult = [];
unrollMatrix = (array) => {
// This is our condition to check for any remaining elements and break out of recursion and give us the end result
if(array.length === 0) {
        console.log(endResult.join(', '));
        return;
    }
    // 1.- Get the top row
    endResult.push(...array.shift());
    // 2.- Get the last element of all remaining rows
    endResult.push(...array.map(arr => arr.pop()));
    // 3.- Get what's left of the bottom row in reverse
    endResult.push(...array.pop().reverse());
    // 4.- Get the first element of any remaining rows
    endResult.push(...array.map(arr => arr.shift()).reverse());
    // 5.- Make the function call itself over whatever's left of our original matrix
    unrollMatrix(array);
}
unrollMatrix(matrix); // 1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10