Пара слов о том, как программисты разных конфессий справляются с самой очевидной задачей в Computer Science.

А вы уверены, что хорошо понимаете все тонкости разворачивания списков?
А вы уверены, что хорошо понимаете все тонкости разворачивания списков?

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


Связный список. Связный, блин, список. Эта штука преследует программистов, как тени под глазами после ночного деплоя в пятницу. Каждый собеседующий на позицию «мы ищем рок-звезду» задает этот вопрос с видом человека, открывающего тебе тайны мироздания. Переверни список. Cправишься — будешь писать круды за еду. Не справишься — иди торговать шавермой, там интервью попроще.

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

Erlang: телефонная станция решает школьную олимпиаду

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

reverse(List) -> reverse(List, []).
reverse([], Acc) -> Acc;
reverse([H|T], Acc) -> reverse(T, [H|Acc]).

Тут все честно. Два аргумента: список и аккумулятор. Берешь голову, кладешь в аккумулятор, хвост рекурсивно обрабатываешь. Когда список кончится — аккумулятор и есть результат. Хвостовая рекурсия, никаких фокусов. Ошибиться буквально негде: в эрланге нет циклов

Код выглядит так, будто его писал бухгалтер. Скучный, предсказуемый, работает уже тридцать лет без единого сбоя. Как шведская мебель. В стандартной библиотеке есть :lists.reverse/1.

Elixir: когда Erlang надел очки в роговой оправе

Эликсир — это Эрланг для людей, которые считают, что fun вместо fn — это излишняя многословность. Тот же BEAM, та же семантика, но теперь с пайпами и сахаром.

def reverse(list), do: reverse(list, [])
defp reverse([], acc), do: acc
defp reverse([h | t], acc), do: reverse(t, [h | acc])

Идентичная логика, немного иной синтаксис. В стандартной библиотеке, естественно, есть Enum.reverse/1.

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

Как не надо:

# std library
Enum.reduce(list, [], &[&1|&2])

# comprehension
for x <- list, reduce: [], do: (acc -> [x | acc])

А что у нас в языках с богатыми возможностями?

Haskell: функциональное высокомерие

Хаскель — это язык, на котором люди пишут статьи про языки, на которых можно писать. Монады, функторы, аппликативные функторы, моноиды. Если ты не понимаешь, что такое Monad, значит, ты недостаточно умен. Если понимаешь — все равно недостаточно умен, просто притворяешься лучше.

Как надо:

reverse :: [a] -> [a]
reverse = foldl (flip (:)) []

Одна строка. Используем foldl, переворачиваем оператор cons через flip, и вуаля. Элегантно, как смокинг.

Как не надо:

reverse :: [a] -> [a]
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]

Тут квадратичная сложность из-за конкатенации списков (++), и любой хаскеллист посмотрит на вас как на человека, который режет пиццу ножницами.

Idris: ФП для тех, кому Haskell показался слишком практичным

Идрис — это зависимые типы. Это когда ты можешь доказать на уровне системы типов, что твой список действительно перевернут. Не просто «я написал тесты», а математически доказал. Компилятор — твой теорема-прувер.

Как надо:

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

Как не надо:

reverse : Vect n a -> Vect n a
reverse [] = []
reverse (x :: xs) = reverse xs ++ [x]

Тут компилятор хотя бы проверит, что длина вектора сохраняется, и если реализация случайно потеряет элемент — код просто не скомпилируется.

Как совсем не надо:

reverse : List a -> List a
reverse [] = []
reverse (x :: xs) = reverse xs ++ [x]

Ruby: язык, в котором все уже сделано за тебя

Без стандартной библиотеки имеет смысл сворачивать список с аккумулятором.

Как надо:

def reverse(list)
  list.reduce([]) { |result, item| result.unshift(item) }
end

Как не надо:

def reverse(list)
  result = []
  list.each { |item| result.unshift(item) }
  result
end

Мне лично приходилось заворачивать людей, которые расплескивают локальные переменные наружу скоупа и используют итерации вместо свёрток. Код выше настолько невыразителен, что любой профессионал на нём будет спотыкаться. Но это бы еще и ладно.

Как совсем не надо:

def reverse(list, memo = [])
  return memo if list == []
  reverse(list[1..-1], [list[0]] + memo)
end

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

pry> reverse((1..42_000).to_a)
#⇒ SystemStackError: stack level too deep

Но:

RubyVM::InstructionSequence.compile_option = {
  tailcall_optimization: true,
  trace_instruction: false
}
# необходимо переопределить метод, иначе VM не станет применять TCO 
def reverse(list, memo = [])
  return memo if list == []
  reverse(list[1..-1], [list[0]] + memo)
end
reverse((1..42_000).to_a)
#⇒ [42000, 41999, …]

JavaScript: хаос, возведенный в систему

JavaScript был написан за десять дней. И каждый из этих дней ощущается в любой строке кода.

Как надо:

const reverse = (list) => [...list].reverse();

Или, без стандартной библиотеки:

const reverse = (list) => list.reduce((acc, current) => [current, ...acc], []); ;

Как не надо:

const reverse = (list) => list.reverse();

Array.prototype.reverse() мутирует исходный массив, а в 2026 году это не самое ожидаемое поведение какой-то сторонней функции.

А spread-оператор создает копию. Теперь мы функциональные программисты. Почти. Если не смотреть слишком пристально.

Как совсем не надо:

const reverse = ([head, ...tail]) =>
  head === undefined ? [] : [...reverse(tail), head];

Деструктуризация, spread, рекурсия. Три модных слова в одной функции. HR будет в восторге. Вот только такой код отыквится еще быстрее, чем руби выше. И способа починить уже нет. В JS хвостовой оптимизации не бывает, а без нее любой рекурсивный вызов — переход пропасти по канату.

Как можно:

Если нельзя пользоваться стандартной библиотекой, у нас все еще есть циклы.

function reverse(arr) {
  let reversed = [];
  for (let i = arr.length - 1; i >= 0; i--) {
    reversed.push(arr[i]);
  }
  return reversed;
}

Python: читаемость прежде всего (и после всего тоже)

Как надо:

def reverse(lst):
    return lst[::-1]

Слайсы. [::-1] — это «от начала до конца с шагом минус один». Криптография? Нет, просто читаемость кода важнее всего остального.

Как не надо:

def reverse(lst):
    result = []
    for item in lst:
        result.insert(0, item)
    return result

Императивно, понятно, квадратичная сложность из-за insert(0, ...). Зато любой джун прочитает. А это важнее всего.

Как совсем не надо:

def reverse(lst):
    if not lst:
        return []
    return reverse(lst[1:]) + [lst[0]]

На больших списках упадет с RecursionError. Python не оптимизирует хвостовую рекурсию принципиально. Гвидо так решил, и мы живем с этим.

Как можно:

from functools import reduce

def reverse(lst):
    return reduce(lambda acc, x: [x] + acc, lst, [])

Как можно:

def reverse(lst):
    return [lst[i] for i in range(len(lst) - 1, -1, -1)]

Как можно, но не нужно:

def reverse(lst):
    result = []
    for i in range(len(lst) - 1, -1, -1):
        result.append(lst[i])
    return result

Go: простота как форма агрессии

Go — это язык, созданный в Google. Циклы и структуры, чтобы не перегреть голову гуглерам.

Как надо:

С дженериками (теперь они есть):

func reverse[T any](list []T) []T {
    result := make([]T, len(list))
    for i, v := range list {
        result[len(list)-1-i] = v
    }
    return result
}

Никакой магии. Создаем слайс нужного размера, заполняем в обратном порядке. Как в учебнике по программированию 1985 года. Но быстро.

Как не надо:

func reverse[T any](list []T) {
    for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 {
        list[i], list[j] = list[j], list[i]
    }
}   

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

Как хотелось бы:

  1. reduce — не предусмотрен в стандартной библиотеке;

  2. comprehensions — не предусмотрены в стандартной библиотеке;

  3. рекурсия — TCO не предусмотрен в стандартной библиотеке, поэтому нет.

Rust: параноидальная безопасность

Rust — это язык для людей, которые просыпаются в холодном поту от мысли о dangling pointer. Компилятор Rust — строгий учитель, который бьет по рукам за каждую потенциальную ошибку. Но потом твой код работает. Без segfault. Без утечек памяти. Без надежды на лучшую жизнь, потому что ты потратил её на борьбу с borrow checker.

Как надо:

fn reverse<T>(list: Vec<T>) -> Vec<T> {
    list.into_iter().rev().collect()
}

into_iter() забирает владение, rev() переворачивает итератор, collect() собирает обратно в вектор. Функционально, элегантно, zero-cost abstraction.

Как можно:

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

fn reverse<T>(list: &mut Vec<T>) {
    list.reverse();
}

Мутабельная ссылка. &mut. Компилятор проверит, что никто больше не держит ссылку на этот вектор. Если держит — не скомпилируется.

Как надо для связного списка (настоящего, не вектора):

fn reverse<T>(mut list: LinkedList<T>) -> LinkedList<T> {
    let mut result = LinkedList::new();
    while let Some(item) = list.pop_front() {
        result.push_front(item);
    }
    result
}

LinkedList в стандартной библиотеке Rust — редкий зверь. Все используют Vec, потому что кэш процессора важнее алгоритмической чистоты.

C, C++

Как не надо:

Писать свою функцию.

Как надо:

Возьмите готовую реализацию из сторонней библиотеки, интернета, LLM-ки.

Java, C#, и т. п.

В энтерпрайзах списки не разворачивают.


Эпилог: к чему это всё?

В принципе, ничего нового. Функциональные языки рекурсивно складывают в аккумулятор. Императивные — итерируют и мутируют. Мультипарадигменные — делают и то, и другое, и еще что-нибудь странное. И, тем не менее, я надеюсь, что этот текст немного расширил горизонты вашей эрудиции.

Суть в том, что переворачивание списка — это не про списки. Это про мировоззрение. Хаскеллист видит fold. Гофер видит цикл. Рубист видит один вызов метода и идет пить чай.

А интервьюер видит, как ты потеешь у доски, и тихо радуется, что сегодня не его очередь.


P.S. Если вы дочитали до сюда и всё ещё не знаете, как перевернуть связный список — используйте стандартную библиотеку. Для того она и существует.

Комментарии (44)


  1. mayorovp
    24.02.2026 06:04

    Какое-то изложение непоследовательное. Почему подход с аккумулятором и хвостовой рекурсией, вроде как идеальный для Erlang, даже не был упомянут как вариант в Хаскеле?


    1. cupraer Автор
      24.02.2026 06:04

      Ну вы уж простите великодушно, как смог.

      Первый вариант в хаскеле — это хвостовая рекурсия.


      1. mayorovp
        24.02.2026 06:04

        Канонический вариант:

        reverse :: [a] -> [a]
        reverse x = reverse' x []
          where
            reverse' [] acc = acc
            reverse' (x:xs) acc = reverse' xs (x:acc)
        


    1. vkni
      24.02.2026 06:04

      Вы же читали известное эссе про эволюцию программиста на Хаскеле? :-) Автор подходит с точки зрения point-less программиста, что более, чем естественно! :-)

      https://people.willamette.edu/~fruehr/haskell/evolution.html


      1. mayorovp
        24.02.2026 06:04

        Читал я его, читал. Однако там полно стадий, из которых автор почему-то выбрал разные для разных языков.


        1. vkni
          24.02.2026 06:04

          "Художник так видит" - ну, блин, сама по-себе идеология программиста-полиглота, горячо поддерживаемая Алексеем, прямо запрещает "писать на любом языке, как на ФОРТРАНЕ" (а я видел, как по-английски пишут "на ФОРТРАНЕ"). То есть, для разных не родственных языков ОБЯЗАТЕЛЬНО должны быть разные стили.

          Короче, придираетесь.


          1. mayorovp
            24.02.2026 06:04

            Но ведь Erlang и Haskell - родственные же...


  1. Seasle
    24.02.2026 06:04

    JavaScript – ожидаю toReversed... в 2026 то году. Но его почему-то нет.


  1. MadridianFox
    24.02.2026 06:04

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


    1. Kotofay
      24.02.2026 06:04

      struct Node {
          Node * next;
          int data;
      };
      
      void foo( Node ** node ) {
          if( !node || !*node || !( *node )->next )
              return;
      
          auto & cur = *node;
          auto next = cur->next;
      
          cur->next = nullptr;
      
          while( next ) { 
              auto tmp = next->next;
              next->next = cur;
              cur = next;
              next = tmp;
          }
      }

      Собеседовал как то претендентов на синьора С++.
      Что это и как работает ответило 2 из 10.
      Так что да, пользуйтесь стандартными алгоритмами.


      1. kmatveev
        24.02.2026 06:04

        Я ни разу не сеньор C++, такой вопрос: зачем в foo передаётся указатель на указатель? Просто указателя вроде как должно быть достаточно.


        1. mayorovp
          24.02.2026 06:04

          Потому что после разворота указатель на голову списка должен изменить свой значение.


        1. vkni
          24.02.2026 06:04

          Это, кмк, реально древний код из непонятных времён, скорее всего переписанный с чего-то вроде Pascal или FORTRAN с указателями - то есть, с языков, где есть различие между функциями и процедурами. В С я бы использовал возвращаемое значение типа node *. Соответственно, сигнатура выглядела бы

          node * foo( const node *)

          В случае ошибки возвращается NULL, в случае успеха - указатель на новую голову.


          1. Kotofay
            24.02.2026 06:04

            И в каком состоянии останется список после ошибки?
            Голову то вы уже потеряли, список разорван, память утекла.
            И кстати, какие ошибки могут быть?
            Список уже существует, никакая память не выделяется и не освобождается.
            Это учебный код для выявления !С/С++ программистов и
            !программистов прикидывающихся синьорами С++.


            1. vkni
              24.02.2026 06:04

              И в каком состоянии останется список после ошибки?

              В отличном. Там состояния ошибки — это только

              !node || !*node

              Как вы, собственно, и пишете.


  1. serp2002
    24.02.2026 06:04

    >>В энтерпрайзах списки не разворачивают.

    ну блин, я только ради этого статью и читал! )


  1. qrdl
    24.02.2026 06:04

    Жаль, что мы так и не увидели ни одного связанного списка. Развернуть слайс/массив/вектор - это совсем другая задача.


    1. cupraer Автор
      24.02.2026 06:04

      В языках, в которых они есть из коробки, вы их увидели.


      1. qrdl
        24.02.2026 06:04

        Вектор в Rust:

        A contiguous growable array type, written as Vec<T>

        Слайс в Go:

        They build on arrays to provide great power and convenience

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


        1. kmatveev
          24.02.2026 06:04

          подозреваю, с остальными языками такая же ситуация

          В Haskell, Erlang и Elixir не так, там язык предоставляет тип данных "ячейка с двумя ссылками", первая ссылка указывает на элемент, вторая - на следующую ячейку, так что там настоящие односвязные списки. Исторически это идёт от Lisp-ов, называется cons-ячейки и продолжает жить во многих современных диалектах, например Scheme (кстати, где он?), но там оно мутабельное, а в Haskell/Erland/Elixir - нет.


          1. cupraer Автор
            24.02.2026 06:04

            Да, пардон, про Scheme я впопыхах непростительно забыл.


            1. vkni
              24.02.2026 06:04

              Вообще, можно совместить со статьёй про парадигмы и их влияние.


          1. qrdl
            24.02.2026 06:04

            Ок, значит все же какие-то списки в статье есть. Но по тем языкам, которые я знаю, написано неправильно. И в Python'е слайсы тоже на базе массивов, насколько я помню.


  1. Taraflex
    24.02.2026 06:04

    Как надо:

    const reverse = (list) => [...list].reverse();

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

    const reverse = (list) => Array.from(/*вот тут проглотит любой объект-итератор*/ list).reverse();

    Остальные примеры не особо лучше.


    1. mayorovp
      24.02.2026 06:04

      А где тут переполнение стека может случиться?


      1. cupraer Автор
        24.02.2026 06:04

        Нигде, конечно.


      1. Taraflex
        24.02.2026 06:04

        Каюсь. В данном случае переполнения нет. Приучил себя не использовать ... на итераторах неизвестной длинны. Ибо в конструкции

        Math.max(...Array(1000000))

        уже получим RangeError: Maximum call stack size exceeded


        1. cupraer Автор
          24.02.2026 06:04

          Это совершенно нерелевантная проблема, связанная с внутренним разворотом интервала в массив.


          1. mayorovp
            24.02.2026 06:04

            Нет, эта проблема связана с попыткой передать 1000000 аргументов через стек. Но да, она нерелевантна.


  1. tmhba
    24.02.2026 06:04

    Это ведь про массивы, а не про связные списки.


  1. gravicappa
    24.02.2026 06:04

    const reverse = (list) => list.reduce((acc, current) => [current, ...acc], []);

    Тут, если что, тоже O(n²).


  1. Alexandroppolus
    24.02.2026 06:04

    Раз уж в статье про разворот списка совершенно неожиданно (судя по всему, не только для меня) разворачивают массив, то вот знаменитый js-кодогольф на тему сабжа:

    const reverse = a => a.map(a.pop,[...a])

    Иммутабельно, за O(n)


  1. domix32
    24.02.2026 06:04

    Мне кажется более интересная задача про отражение дерева вокруг корня. Вот где всякое мясо начинает всплывать наружу.


    1. Alexandroppolus
      24.02.2026 06:04

      Это задача, на которую однажды намотали автора homebrew?


      1. vkni
        24.02.2026 06:04

        Отразили! :-)


      1. domix32
        24.02.2026 06:04

        Ещё бы понимать о какой истории речь. Не могу ни подтвердить ни опровергнуть


        1. Alexandroppolus
          24.02.2026 06:04

          История, нашумевшая в своё время - https://habr.com/ru/articles/345756/


  1. vkni
    24.02.2026 06:04

    Вот интересный момент, почему вы рядом с Эрлангом не поставили код на Прологе?

    Очень многое по-поводу «инопланетного» синтаксиса Эрланга стало бы сразу очевидно...

    С возвращением!


    1. oeditus
      24.02.2026 06:04

      На прологе же буквально ничем не отличается от на эрланге:

      reverse(List, Reversed) :-
          reverse(List, [], Reversed).
      
      reverse([], Buffer, Buffer).
      reverse([Head|Tail], Buffer, Reversed) :-
          reverse(Tail, [Head|Buffer], Reversed). 
      
      ?- reverse([1, 2, 3], Result).

      https://swish.swi-prolog.org/p/list-reverse-am.swinb


      1. mayorovp
        24.02.2026 06:04

        Настоящий код на Прологе должен работать в обе стороны, а тут reverse(Result, [1, 2, 3]) вылетает с переполнением стека на попытке выдать второй результат.


        1. oeditus
          24.02.2026 06:04

          Покажите настоящий код, пожалуйста.


          1. mayorovp
            24.02.2026 06:04

            Ну, квадратичный код работает с любой стороны:

            reverse([], []).
            reverse([Head|Tail], Reversed) :- 
                reverse(Tail, ReversedTail),
                append(ReversedTail, [Head], Reversed).
            

            А если оптимизировать - то без free/bound (var/unvar) и ! не обойтись.


            1. oeditus
              24.02.2026 06:04

              Спасибо!


    1. cupraer Автор
      24.02.2026 06:04

      Спасибо! Накопил материал зато, пока меня не было.