На видео более подробное объяснение каждого решения

Постановка задачи

Ссылка на задачу: https://leetcode.com/problems/middle-of-the-linked-list

Дан указатель head на начало односвязного списка, нужно вернуть средний узел списка.

Если средних узлов два, нужно вернуть второй средний узел.

1. Решение с использованием массива

Подход

Преобразование списка в массив упрощает доступ к элементам по индексу. Мы проходим по списку и сохраняем все узлы в массив, так как доступ по индексу в массиве осуществляется за постоянное время O(1). После этого мы легко можем найти средний элемент, просто взяв элемент по индексу len(items) // 2

Алгоритм

  1. Пройти по списку и сохранить все его элементы в массив.

  2. Определить индекс среднего элемента массива.

  3. Вернуть элемент, находящийся по этому индексу.

Код решения

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
        items = []

        while head: #None
            items.append(head)
            head = head.next
        
        return items[len(items) // 2]

Сложность алгоритма

По времени: O(n), где n — количество элементов в списке.

По памяти: O(n), так как мы сохраняем все элементы списка в массив.

Визуализация

  1. Исходный список: 1 -> 2 -> 3 -> 4 -> 5

  2. Массив после прохождения по списку: [1, 2, 3, 4, 5]

  3. Индекс среднего элемента: 5 // 2 = 2

  4. Средний элемент в массиве: 3
    0 1 2 3 4
    [1, 2, {3}, 4, 5]

2. Решение с подсчетом длины списка

Подход

В этом подходе мы сначала считаем количество элементов в списке, чтобы точно знать, сколько узлов в нем содержится. Считая количество узлов, мы можем определить индекс среднего элемента как length // 2. Затем, проходя список во второй раз, мы останавливаемся на этом индексе и возвращаем соответствующий узел.

Алгоритм

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

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

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
        count = self.count(head)

        for _ in range(count // 2):
            head = head.next
        
        return head
    
    def count(self, head):
        res = 0
        while head:
            res += 1
            head = head.next
        return res

Сложность алгоритма

По времени: O(n), где n — количество элементов в списке.

По памяти: O(1), так как мы не используем дополнительную память.

Визуализация

  1. Исходный список: 1 -> 2 -> 3 -> 4 -> 5

  2. Проход 1: Подсчет длины списка

    • Длина списка: 5

  3. Средний индекс: 5 // 2 = 2

  4. Проход 2: Получение среднего элемента

    • Проходим через узлы до индекса, который мы вычислили на 3 шаге:

    1 (0), 2 (1), 3 (2)

  5. Средний элемент: 3

3. Решение с использованием метода быстрого и медленного указателя

Подход

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

Алгоритм

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

  2. Перемещать медленный указатель на один шаг, а быстрый на два шага, пока быстрый указатель не достигнет конца списка.

  3. Когда быстрый указатель достигнет конца списка или выходит за его пределы, медленный указатель будет находиться на среднем элементе.

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow = head
        fast = head

        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        
        return slow

Сложность алгоритма

По времени: O(n), где n — количество элементов в списке.

По памяти: O(1), так как используем только два указателя.

Визуализация

  1. Исходный список: 1 -> 2 -> 3 -> 4 -> 5

  2. Начальное положение:

    • Медленный указатель (S) на 1

    • Быстрый указатель (F) на 1

  3. Шаг 1:

    S: 2 (двигается на 1 узел вперед)

    F: 3 (двигается на 2 узла вперед)

    • Список: 1 -> [2 (S)] -> 3 -> [4 (F)] -> 5

  4. Шаг 2:

    S: 3 (двигается на 1 узел вперед)

    F: 5 (двигается на 2 узла вперед)

    • Список: 1 -> 2 -> [3 (S)] -> 4 -> [5 (F)]

  5. Конец:

    • Быстрый указатель (F) достигает конца списка или выходит за его пределы.

    • Медленный указатель (S) находится на 3, который и является средним элементом.

Итоговый результат: Средний элемент списка 3.

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


  1. SpiderEkb
    04.08.2024 10:48

    Если мне приходится делать реализации списков, я обычно сразу закладываю туда счетчик элементов. Если реализован двусвязный список, то любой элемент по номеру можно получить не более чем за length / 2 шагов - если n <= length / 2 - считаем с начала списка, иначе - с конца.

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

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

    Сам объект "список" - это ссылка на HEAD.


    1. idsulik Автор
      04.08.2024 10:48
      +1

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


      1. SpiderEkb
        04.08.2024 10:48

        Да, конечно от задачи. Иногда вообще ничего не нужно - можно использовать готовые библиотечные реализации.


      1. monpa
        04.08.2024 10:48
        +1

        Тут структура данных неподходящая выбрана) Искать середину полной итерацией - ну такое себе развлечение.


      1. YMA
        04.08.2024 10:48
        +1

        Вариант с 2 указателями хорош для олимпиады - оптимально, изящно. Он приходит в голову сразу, когда читаешь заголовок.

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


        1. idsulik Автор
          04.08.2024 10:48

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


          1. acsent1
            04.08.2024 10:48
            +3

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


            1. idsulik Автор
              04.08.2024 10:48

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


    1. avost
      04.08.2024 10:48
      +3

      Если мне приходится делать реализации списков

      Самое интересное в этом комментарии не озвучено :). Расскажите, пожалуйста, что это были за задачи (во множестве числе!), для решения которых вам приходилось не только использовать связные списки, но и писать свои реализации?

      Я спрашиваю без подколок - иногда на тех же собеседованиях спрашивают когда linked list лучше, чем array list - обычно ничего вменяемого придумать не удаётся.

      В практических, конечно, задачах, не в литкодных/олимпиадных.


      1. idsulik Автор
        04.08.2024 10:48

        на тех же собеседованиях спрашивают когда linked list лучше, чем array list - обычно ничего вменяемого придумать не удаётся

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

        а вот реально применять связный список мне пока не приходилось)


        1. avost
          04.08.2024 10:48
          +1

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

          Обычно, это не очень большая проблема и если уж стало припекать по объёму занимаемой памяти, то стоит задуматься либо о том, так ли хорошо всё делается или о ручном управлении памятью (насколько это в яве возможно). LinkedList в этом случае о-о-очень сомнительное решение, тк у него офигенный оверхед по памяти, особенно, на небольших целевых объектах, который может сожрать всю "выгоду" от переаллокации массивов.

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

          Fifo-Lifo прекрасно делаются на массивах.
          Частые дополнения большого количества - я бы тоже подумал о кастомных ArrayList-ах
          Про объекты больших размеров - ну, такое,.. смотреть по месту, конечно, надо...

          А, вот список поверх другой структуры данных (типа LRU структур) - это да-а! Как раз и список и его реализация. И в яве уже даже есть LinkedHash[Map|Set] - весьма полезная штуковина. В том числе, чтобы на собеседовании сказать если вопрос возникнет ))

          а вот реально применять связный список мне пока не приходилось)

          Вот, в том-то и дело ))


      1. SpiderEkb
        04.08.2024 10:48

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

        Список или массив... Нам часто приходится иметь дело с очень большими объемами данных. Память в целом не проблема, можно считать что она не ограничена. Но есть ограничение - 16Мб одним куском (ну можно 2Тб, но там уже приходится "танцевать вприсядку" - это другая модель памяти и, в общем случае, даже другой размер указателя). И вот если нет уверенности что получится выделить массив (даже динамический массив все равно в памяти одним куском будет), то тогда используется список.

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

        А если это статика - один раз заполнил, отсортировал и потом 100500 раз ищешь - там сортированный массив по производительности лучше.


      1. Sap_ru
        04.08.2024 10:48
        +3

        1. Нужен FIFO или LIFO доступ к элементам.

        2. Нужны частые операции добавляения большого количества элементов или объединения списков.

        3. Список состоит из элементов большого размера, может содержать большое колличество элеметнов и при этом все время растет. Использование массивов тут неоптимально по причине часто реаллокации больших объёмов памяти.

        4. Массив состоит из структур и по причине экономии ресурсов или необходимости обеспечения эффекивного многопоточного атомарного доступа нужно, чтобы элементы структур были сразу элементами списка (т.е. отказ от работы с указателями даёт какие-то преимущества). Крайне редкая ситуация.


        1. idsulik Автор
          04.08.2024 10:48

          Лучше не опишешь, добавлю это в комментарии к видео и оставлю ссылку на источник)


      1. aamonster
        04.08.2024 10:48

        В практических задачах обычно linked list делается не отдельно, а поверх уже существующей структуры данных, чтобы выстроить в цепочку объекты, которые существуют без него. Как контейнер – не припомню...


        1. idsulik Автор
          04.08.2024 10:48

          linked list делается не отдельно, а поверх уже существующей структуры данных

          не понял этот момент, что имеется в виду?


          1. aamonster
            04.08.2024 10:48
            +1

            В смысле у вас уже есть какое-то хранилище объектов (массив, дерево, ещё что – что вам диктует задача) и вам понадобилось для каких-то нужд объединить эти объекты в цепочки. Можно хранить отдельно списки указателей на ваши объекты, а можно просто добавить в каждый объект поле next.


            1. idsulik Автор
              04.08.2024 10:48

              ну это как-то не очень, лучше создать LinkedList, чтобы код был чище)


              1. aamonster
                04.08.2024 10:48
                +2

                Ага, и фигакнуть на ровном месте по выделению памяти в куче на каждый элемент.


                1. idsulik Автор
                  04.08.2024 10:48

                  ну значение ноды будет ССЫЛКА на нужный объект, поэтому не вижу в этом проблем) ну или плохо понял идею


                  1. aamonster
                    04.08.2024 10:48
                    +1

                    Ага, только на N объектов вам придётся выделить N нод. Каждая будет хранить указатель на объект, указатель на следующую ноду + накладные расходы кучи. И затраты времени на выделение/освобождение.

                    А если вы прямо внутри вашего объекта сделаете поле указателя на следующий – это куда выгоднее.

                    Если же вам надо очередь делать снаружи, не модифицируя объекты – обычно выгоднее (и по памяти, и по быстродействию) кольцевой буфер.


                    1. idsulik Автор
                      04.08.2024 10:48

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


                      1. aamonster
                        04.08.2024 10:48
                        +3

                        Я за 29 лет практики не припомню задачи, где мне понадобился бы именно отдельный Linked list :-)


                      1. idsulik Автор
                        04.08.2024 10:48

                        тоже не припоминаю) ни в том, ни в другмо виде


                      1. SpiderEkb
                        04.08.2024 10:48

                        Вопрос - а что были за задачи в практике?


                      1. slonopotamus
                        04.08.2024 10:48

                        В C такое делают постоянно.


                      1. idsulik Автор
                        04.08.2024 10:48

                        а можно конкретный пример?)


          1. wataru
            04.08.2024 10:48
            +1

            Например, при реализации LRU кеша. Там при доступе у ключу, он перемещается в конец списка. При переполнении кеша (хеш таблица значений), удаляется элемент из начала списка.


        1. SpiderEkb
          04.08.2024 10:48

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


          1. aamonster
            04.08.2024 10:48

            Учитывая, что чтобы вставить или удалить элемент, нужно вначале найти его место за O(n) – редко когда linked list в качестве контейнера оправдан.


            1. SpiderEkb
              04.08.2024 10:48

              Простейший случай - идете по цепочке от начала к концу и для каждого элемента принимаете решение - оставить его или удалить. Такой вот play-off алгоритм.

              Есть еще очереди. Например, на отправку сообщений. Сообщение для отправки берется из начала очереди. Новые сообщения добавляются в конец. Отправленное (точнее, отправленное на которое получено подтверждение о поиеме) сообщение удаляется из начала очереди.


              1. aamonster
                04.08.2024 10:48
                +2

                Очередь обычно делается кольцевым буфером, не linked list. Исключение – когда объекты уже лежат где-то и вы их просто связываете прямо на месте.

                Play-off – тот же один проход по массиву (двумя указателями) удаляет всё лишнее за O(n), как и в случае linked list.

                Я не говорю, что linked list нигде не пригодится, но его использование в качестве контейнера – лютая экзотика.


                1. AlexSky
                  04.08.2024 10:48

                  В ядре Линукса нередко встречается.


                  1. aamonster
                    04.08.2024 10:48

                    Он там обычно владеет объектами или связывает хранящиеся не в нём?


                  1. morijndael
                    04.08.2024 10:48
                    +1

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


                  1. idsulik Автор
                    04.08.2024 10:48

                    как используется?


                1. SpiderEkb
                  04.08.2024 10:48

                  Очередь обычно делается кольцевым буфером, не linked list. Исключение – когда объекты уже лежат где-то и вы их просто связываете прямо на месте.

                  Кольцевые буфера хороши там, где поток сообщений относительно невелик, а ресурсы сильно ограничены. Например, на промконтроллере.

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

                  Помножьте на то, что размер сообщения может быть разным - от десятков байт до килобайт. В статическом буфере вам придется выделять память под максимальный размер сообщения.

                  Помножьте на то, что могут быть "приоритетные" сообщения, которые помещаются не в конец, а в начало очереди.

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

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

                  Сейчас задачи другие, но вот есть параллельная обработка больших объемов данных (десятки и сотни элементов). И там есть такая штука как "конвейер". Тоже на FIFO очередях строится.

                  Play-off – тот же один проход по массиву (двумя указателями) удаляет всё лишнее за O(n), как и в случае linked list.

                  Play-Off, как следует из названия, подразумевает многократный проход по списку с удалением элементов на каждом проходе.

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

                  В результате по скорости (подтверждено PEX статистиками) разницы нет - что связанный список, что два массива, что один. А вот простоте и понятности кода со списком намного проще.


            1. wataru
              04.08.2024 10:48

              Ничего не мешает держать хеш-таблицу из идентификаторов элементов в указатели/итераторы в list. Популярное применение - LRU cache. Там список навешан сверху на хеш-таблицу и задает порядок элементов, чтобы выбирать, какой элемент самый старый и его надо удалить. При доступе элемент из центра списка будет перемещен в конец.


              1. aamonster
                04.08.2024 10:48

                Это то, про что я говорил: linked list не как контейнер, а связываем хранящиеся отдельно данные.


  1. IvanZaycev0717
    04.08.2024 10:48

    Хочу поинтеросоваться у автора - каким образом была выбрана задача? Когда я проходил собеседование, мне сказали написать random.randrange (3000) и что выведится в консоль, то и решай.


    1. idsulik Автор
      04.08.2024 10:48

      В первую очередь подбираю задачи, так чтобы объяснить какую-то тему, которая будет полезна на интервью, на примере этой задачи или саму задачу, которая часто встречается на интервью.
      У меня на канале идут подряд задачи связанные с какой-то темой(two pointers, sliding window), сейчас перешел на тему fast and slow pointer и на примере этой задачи объяснил, что это такое + объяснил что такое связный список и какие они бывают(на видео).

      Мне вот давали Reverse Linked List(фаанг в РФ) и десятки других задач(easy/medium/hard).


      1. AlexSky
        04.08.2024 10:48

        Три года назад была эта задача, когда собеседовался на нынешнюю работу. На нервах от собеседования (очень мало на них ходил) решил её каким-то безумным способом с двумя указателями. Нормальное решение пришло минут через 5 после окончания этапа собеседования. Было стыдно, но в итоге работу и так получил.


        1. idsulik Автор
          04.08.2024 10:48

          речь про Reverse задачу?) я тоже с горем пополам решил, с помощью интервьюера, потому что трудно в голове держать связи между нодами


          1. AlexSky
            04.08.2024 10:48

            Да, реверс.

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


            1. idsulik Автор
              04.08.2024 10:48

              поэтому перед интервью всегда просят приступать к коду в самую последнюю очередь) сначала вопросы, потом объяснение своей идеи и только после приступать к решению


  1. NevoWin
    04.08.2024 10:48
    +6

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

    Единственное, что в 3 варианте более красивая и элегантная идея.

    Но 2 вариант проще в понимании и чтении кода в будущем.

    Я бы взял на работу того, кто не усложняет код, где это не требуется, а пишет более просто.


    1. wataru
      04.08.2024 10:48
      +1

      Второй и третий варинаты имеют одинаковое количество сравнений и обращений к памяти.

      Но из-за архитектурных ньюансов, сложно сказать, какой будет быстрее.

      Третий вариант имеет меньше условных переходов (n/2 в случае одного цикла и n+n/2 в случае двух). Но это хорошо предсказываемые условные переходы на следующую итерацию цикла, поэтому они относительно дешевые.

      С другой стороны, во втором варианте циклы короче.

      С точки зрения кода второй вариант лучше, я с вами согласен.


    1. idsulik Автор
      04.08.2024 10:48

      Согласен, что третий вариант довольно хитрий и да, второй вариант проще в понимании


    1. SquareRootOfZero
      04.08.2024 10:48
      +1

      Идея 3 варианта довольно проста, только "у кого-то это щёлкает, а у кого-то не щёлкает". Предварить реализацию коротеньким комментарием о том, что происходит и почему это работает, и по лёгкости понимания и чтения оно, на мой взгляд, не только не уступит второму варианту, а даже его превзойдёт.


      1. idsulik Автор
        04.08.2024 10:48

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


        1. SquareRootOfZero
          04.08.2024 10:48
          +1

          Начнём с того, что сама постановка задачи, возникни она не на интервью, а в реальных условиях, ненадолго остановит человека, чтобы действительно понять: а) зачем нужно находить середину связного списка? б) зачем выбирать именно связный список как упорядоченный контейнер для данных, у которого надо находить серединный элемент. Что-то тут, наверное, придумать можно, как-то обосновать - но ненадолго остановит.

          С другой стороны, а зачем человеку действительно понимать, что происходит? Есть функция, возвращает срединный элемент, вот аргумент(ы), вот возвращаемое значение. Работает? Работает. Ну и не трожь, пускай работает. Можно, опять-таки, представить ситуацию, когда понадобилось действительно понять - прошёл, допустим, год, два, три, и почему-то понадобилось переделать. Но не по нескольку же раз на дню человек будет читать именно эту часть кода, каждый раз ненадолго останавливаясь, чтобы понять.


          1. idsulik Автор
            04.08.2024 10:48

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


  1. x89377
    04.08.2024 10:48

    Хорошо было бы добавить голосование, что бы каждый мог выбрать правильный вариант № 3.


    1. idsulik Автор
      04.08.2024 10:48

      Самый оптимальный вариант третий) а две другие лишь подводят к третьему


      1. wataru
        04.08.2024 10:48
        +2

        Я выше это расписал: не факт. Второй может выполняться даже быстрее.


      1. aamonster
        04.08.2024 10:48
        +3

        Вы проверили? Асимптотика у второго и третьего одинаковая, стоило сделать бенчмарк.

        В боевом коде не задумываясь писал бы второй ("пиши код так, будто поддерживать его будет склонный к насилию психопат, знающий, где ты живёшь").


        1. idsulik Автор
          04.08.2024 10:48

          стоило сделать бенчмарк

          это уж точно, как будет свободное время надо будет сделать


  1. kibergus
    04.08.2024 10:48

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


    1. idsulik Автор
      04.08.2024 10:48

      согласен, в случае с быстрым и медленным указателем, проходим каждый элемент 1.5 раз, но в рамках одного цикла


    1. aamonster
      04.08.2024 10:48

      Быстрый и медленный указатель, вероятно, тиснуты из более сложной задачки про кольцевой список :-)


      1. idsulik Автор
        04.08.2024 10:48

        не понял про кольцевой список


        1. aamonster
          04.08.2024 10:48

          Google "поиск цикла в односвязном списке" или "алгоритм черепахи и зайца".

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


          1. idsulik Автор
            04.08.2024 10:48

            а, решал эту задачу и планирую тоже сделать видео по нему)


        1. SpiderEkb
          04.08.2024 10:48
          +1

          Проверка списка на "зацикливание". Классически решается через "быстрый" и "медленный" указатели.


    1. SquareRootOfZero
      04.08.2024 10:48

      По Big-O такую же, одинаковое количество переходов к следующему элементу, но в третьем варианте отсутствуют операции сравнения текущего индекса указателя с вычисленной длиной списка, делённой пополам. Впрочем, выше @wataru, по-моему, уже написал про то же самое.


  1. kibergus
    04.08.2024 10:48
    +2

    Если хочется извращений, можно съэкономить половину итераций на втором итераторе: заводим два допонительных итератора i1, i2. Когда основной прохтдит через степень двойки (2, 4, 8, 16,...) сохраняем:

    i1 = i2

    i2 = i

    Теперь когда дойдем до конца списка у нас есть закешированный итератор, который между 1/4 и 1/2 длины.

    Но общая сложность все равно линия.


    1. idsulik Автор
      04.08.2024 10:48

      что-то звучит сложно и извращено)


  1. Alexandroppolus
    04.08.2024 10:48

    По сабжу вспоминается забавная, но специфичная для С/С++ задачка в жанре "ненормальное программирование". Надо сначала ввести с клавиатуры N чисел (N произвольное, но не слишком большое), потом вывести их в том же порядке. По условию, нельзя использовать массивы и динамически выделяемую память (кучу).


    1. idsulik Автор
      04.08.2024 10:48

      получилось решить?)


      1. Alexandroppolus
        04.08.2024 10:48

        Да, там ничего сложного


        1. idsulik Автор
          04.08.2024 10:48

          раз уж есть такая задача, то значит кто-то находим в ней сложность)


    1. wataru
      04.08.2024 10:48

      Обязательно вывести потом, а не во время ввода? N задается, или читаем до условного перевода строки?

      Можно ли исползовать N переменных, раз оно небольшое? Можно ли использовать препроцессор?


      1. idsulik Автор
        04.08.2024 10:48

        Можно ли исползовать N переменных, раз оно небольшое? Можно ли использовать препроцессор?

        first_input
        second_input
        third_input
        n_input


        это будет довольно интересный подход)


      1. Alexandroppolus
        04.08.2024 10:48

        Обязательно вывести потом, а не во время ввода?

        Разумеется, в этом и суть ) Сначала читаем N, потом читаем числа N раз, потом выводим тоже N штук.

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


        1. wataru
          04.08.2024 10:48

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

          Еще можно эти локальные переменные организовать в односвязный список. Тогда локальная переменная будет пара число+указатель на предыдущую. В рекурсивный вызов передаем указатель на локальную переменную (которая последняя нода в списке) + указатель на первую локальную переменную. в локальную переменную перед этим читаем и дописываем ее в список. В самом нижнем рекурсивном вызове выводим список.

          Еще, кажется, можно было бы написать вариадическую рекурсивную функцию, но пока не понятно, как можно передать дальше все параметры и еще один int в конце.


          1. Alexandroppolus
            04.08.2024 10:48

            можно эти локальные переменные организовать в односвязный список.

            Я так решал. Это честный вариант без хаков с адресами. Потому и вспомнил задачу в теме про списки.


            1. wataru
              04.08.2024 10:48

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


              1. Alexandroppolus
                04.08.2024 10:48
                +1

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


                1. wataru
                  04.08.2024 10:48

                  Не, это же совсем искусственные ограничения. Если и брать металлические головоломки, то можно их собирать только в варежках. Или только смотря в зеркало.

                  Вот в этой задаче, например, массив фактически есть. Только он размазан по стеку.


  1. Foror
    04.08.2024 10:48

    А зачем вы решаете подобные задачи на собеседовании? Вам там доступ к гугл и чатгпт запрещают? А на работе потом доступ в гугл и чатгпт тоже только по спец пропускам?

    P.S. Как в LinkedList найти средний элемент начиная перебирать с head?

    Чтобы найти средний элемент в связном списке (LinkedList), начиная с головы (head), можно использовать два указателя: один будет двигаться на один элемент за раз (slow), а другой — на два элемента за раз (fast). Когда указатель fast достигнет конца списка, указатель slow будет находиться на среднем элементе. Вот пример алгоритма на Python:

    class Node:
    def init(self, data):
    self.data = data
    self.next = None

    class LinkedList:
    def init(self):
    self.head = None

    def find_middle(self):
        slow = self.head
        fast = self.head
    
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
    
        return slow.data if slow else None
    

    В этом коде метод find_middle возвращает значение среднего элемента. Если список пуст, он вернет None.


    1. wataru
      04.08.2024 10:48

      Да, на интервью не дают доступа к интернету, ибо проверяют ваши знания и навыки, а не chatgpt.

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


      1. Foror
        04.08.2024 10:48

        >Иногда можно чуть-чуть слова в условии поменять и он выдает очень правдоподобный и очень неправильный бред

        Вы или крестик снимите или трусы наденьте. Так чего тогда не даёте людям доступ к чатгпт? Поменяйте слова в условии, если конечно хватит ваших мясных мозгов обойти чатгпт.

        >Да, на интервью не дают доступа к интернету, ибо проверяют ваши знания и навыки, а не chatgpt.

        Так и на работе запретите гугл и чатгпт. Вы работника наняли или халявщика? Он же в гугл полезет, в чатгпт зайдет и всё скопирует! А вы ему деньги заплатите!


        1. idsulik Автор
          04.08.2024 10:48
          +1

          а как тогда проверить того, кого хотят нанимать?) ведь нужен какой-то фильтр


          1. Foror
            04.08.2024 10:48

            В вузе, когда преподаватель видел, что мне было лень делать лабу и я её позаимствовал у местного ботана, просто просил мне объяснить, что здесь происходит. Я ему объяснял и ко мне претензий не было.


            1. idsulik Автор
              04.08.2024 10:48

              молодец!
              но к чему это?)


              1. Foror
                04.08.2024 10:48

                Спросите у чатгпт, правда я не уверен, что сможете ли вы правильно сформулировать вопрос. Поэтому я сделаю это за вас:

                В вузе, когда преподаватель видел, что мне было лень делать лабу и я её позаимствовал у местного ботана, просто просил мне объяснить, что здесь происходит. Я ему объяснял и ко мне претензий не было.

                к чему это?)


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


                1. idsulik Автор
                  04.08.2024 10:48

                  так ты можешь у местного ботана спросить, что там написано, выучить это и рассказать учителю)


              1. Foror
                04.08.2024 10:48

                >Это было сказано при споре о выполнении алгоритмических заданий при приёме на работу


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


                1. idsulik Автор
                  04.08.2024 10:48

                  никто же этого и не отрицает) если написать правильное решение, но молча, есть больше вероятность НЕ попасть на работу, нежели если решить неправильно, НО объясняя ход своих мыслей


    1. idsulik Автор
      04.08.2024 10:48
      +1

      ну разные виды разработок бывают) раньше были Google Driven Development, Stackoverflow Driven Development, а теперь Chatgpt Driven Development. но это все тебе не поможет на собеседовании, там только ты и твой опыт, если ты раньше скопипастил решение откуда-то и не вникал, то это не поможет на интервью)


      1. Foror
        04.08.2024 10:48

        Я вас не пойму, так доступ с чатгпт даёте или нет?


        1. idsulik Автор
          04.08.2024 10:48
          +1

          Чтобы проверить что? ваше умение печатать?) задавать вопрос ИИ?)


          1. Foror
            04.08.2024 10:48

            Умение задавать вопрос ИИ и гуглить в частности, получая верный ответ очень ценный навык. Если вы этого не понимаете, то вам ещё предстоит долгий путь к этому.


            1. idsulik Автор
              04.08.2024 10:48

              с этим навыком вы далеко не пойдете, если нет других навыков)) наймете себе человека, который только и будет делать, что гуглить и спрашивать у чатгпт, завершит проект через пару лет


          1. Foror
            04.08.2024 10:48

            В частности, можете начать развиваться с чтение научной фантастики. Например, Лема Сумма технологий.


            1. idsulik Автор
              04.08.2024 10:48

              удивительно, что у вас за каша в голове)