# Резюме ### Ключевые моменты - Массивы и связные списки — это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: непрерывное и распределенное хранение. Их характеристики взаимодополняющие. - Массивы поддерживают произвольный доступ, занимают меньше памяти; но операции вставки и удаления элементов неэффективны, а длина после инициализации не может быть изменена. - Связные списки обеспечивают эффективную вставку и удаление узлов путем изменения ссылок (указателей), а также позволяют гибко регулировать длину; но доступ к узлам неэффективен, занимают больше памяти. Распространенные типы связных списков включают односвязные списки, кольцевые списки и двусвязные списки. - Список — это упорядоченная коллекция элементов с поддержкой добавления, удаления, поиска и изменения, обычно реализуемая на основе динамического массива. Он сохраняет преимущества массива и при этом позволяет гибко регулировать длину. - Появление списков значительно повысило практичность массивов, но может привести к частичной потере памяти. - Во время выполнения программы данные в основном хранятся в памяти. Массивы обеспечивают более высокую эффективность использования памяти, в то время как связные списки более гибки в использовании памяти. - Кеш обеспечивает быстрый доступ к данным для процессора через механизмы загрузки данных, такие как строки кеша, предварительная выборка, пространственная и временная локальность, значительно повышая эффективность выполнения программы. - Благодаря более высокому коэффициенту попадания в кеш массивы обычно более эффективны, чем связные списки. При выборе структуры данных следует принимать соответствующее решение в зависимости от конкретных требований и сценария. ### Вопросы и ответы **В:** Влияет ли хранение массива в стеке или в куче на временную и пространственную эффективность? Массивы, хранящиеся как в стеке, так и в куче, размещаются в непрерывном пространстве памяти, и эффективность операций с данными практически одинакова. Однако стек и куча имеют свои особенности, что приводит к следующим различиям. 1. Эффективность выделения и освобождения: стек — это небольшая область памяти, выделение выполняется компилятором автоматически; в то время как память кучи относительно больше, может выделяться динамически в коде и более подвержена фрагментации. Поэтому операции выделения и освобождения в куче обычно медленнее, чем в стеке. 2. Ограничение размера: память стека относительно мала, размер кучи обычно ограничен доступной памятью. Поэтому куча более подходит для хранения больших массивов. 3. Гибкость: размер массива в стеке должен быть определен во время компиляции, в то время как размер массива в куче может быть определен динамически во время выполнения. **В:** Почему массив требует элементов одного типа, а в связном списке это не подчеркивается? Связный список состоит из узлов, узлы соединены ссылками (указателями), каждый узел может хранить данные разных типов, например `int`, `double`, `string`, `object` и т.д. В отличие от этого, элементы массива должны быть одного типа, чтобы можно было получить позицию соответствующего элемента путем вычисления смещения. Например, если массив одновременно содержит типы `int` и `long`, отдельные элементы занимают 4 байта и 8 байт соответственно, в этом случае нельзя использовать следующую формулу для вычисления смещения, поскольку массив содержит две разные «длины элемента». ```shell # Адрес элемента в памяти = Адрес массива в памяти (адрес первого элемента) + Длина элемента * Индекс элемента ``` **В:** После удаления узла `P` нужно ли устанавливать `P.next` в `None`? Можно не изменять `P.next`. С точки зрения данного связного списка, при обходе от головного узла до хвостового узел `P` уже не встретится. Это означает, что узел `P` уже удален из связного списка, и в этот момент не имеет значения, куда указывает узел `P`, это не повлияет на данный связный список. С точки зрения структур данных и алгоритмов (решения задач) отсутствие разрыва связи не имеет значения, главное, чтобы логика программы была правильной. С точки зрения стандартной библиотеки разрыв связи более безопасен, логика более ясна. Если не разорвать связь и предположить, что удаленный узел не был корректно освобожден, это может повлиять на освобождение памяти последующих узлов. **В:** Временная сложность операций вставки и удаления в связном списке составляет $O(1)$. Но перед добавлением или удалением требуется $O(n)$ времени для поиска элемента, так почему временная сложность не $O(n)$? Если сначала искать элемент, а затем удалять его, временная сложность действительно $O(n)$. Однако преимущество связного списка в виде вставки и удаления за $O(1)$ может проявиться в других применениях. Например, двусторонняя очередь подходит для реализации на основе связного списка, мы поддерживаем переменную-указатель, всегда указывающую на головной и хвостовой узлы, каждая операция вставки и удаления выполняется за $O(1)$. **В:** На рисунке «Определение и способ хранения связного списка» занимает ли светло-синий узел хранения указателя один адрес памяти? Или он и значение узла занимают по половине? Эта схема является только качественным представлением, количественное представление требует анализа в зависимости от конкретной ситуации. - Разные типы значений узлов занимают разное пространство, например `int`, `long`, `double` и объекты экземпляров. - Размер памяти, занимаемой переменной-указателем, зависит от используемой операционной системы и среды компиляции, чаще всего это 8 байт или 4 байта. **В:** Всегда ли добавление элемента в конец списка выполняется за $O(1)$? Если при добавлении элемента превышается длина списка, необходимо сначала расширить список, а затем добавить элемент. Система выделит новый блок памяти и переместит туда все элементы исходного списка, в этом случае временная сложность будет $O(n)$. **В:** «Появление списков значительно повысило практичность массивов, но может привести к частичной потере памяти» — означает ли потеря пространства здесь память, занимаемую дополнительными переменными, такими как емкость, длина, коэффициент расширения? Потеря пространства здесь имеет два основных значения: с одной стороны, для списков устанавливается начальная длина, которая может нам не полностью понадобиться; с другой стороны, чтобы предотвратить частое расширение, расширение обычно выполняется с коэффициентом, например $\times 1.5$. Таким образом, также появляется много свободных мест, которые мы обычно не можем полностью заполнить. **В:** В Python после инициализации `n = [1, 2, 3]` адреса этих 3 элементов последовательны, но при инициализации `m = [2, 1, 3]` обнаруживается, что id каждого элемента не последовательны, а соответствуют элементам из `n`. Адреса этих элементов не последовательны, является ли `m` массивом? Если заменить элементы списка на узлы связного списка `n = [n1, n2, n3, n4, n5]`, обычно эти 5 объектов узлов также распределены по разным местам памяти. Однако, имея индекс списка, мы все равно можем за время $O(1)$ получить адрес памяти узла и таким образом получить доступ к соответствующему узлу. Это потому, что массив хранит ссылки на узлы, а не сами узлы. В отличие от многих языков, в Python числа также упакованы как объекты, список хранит не сами числа, а ссылки на числа. Поэтому мы обнаруживаем, что одинаковые числа в двух массивах имеют один и тот же id, и адреса памяти этих чисел не обязаны быть последовательными. **В:** В C++ STL уже реализован двусвязный список `std::list`, но в некоторых книгах по алгоритмам его не часто используют напрямую, не связано ли это с какими-то ограничениями? С одной стороны, мы часто предпочитаем использовать массивы для реализации алгоритмов, а связные списки используем только при необходимости, главным образом по двум причинам. - Накладные расходы на пространство: поскольку каждый элемент требует двух дополнительных указателей (один на предыдущий элемент, один на следующий элемент), `std::list` обычно занимает больше места, чем `std::vector`. - Неэффективность кеша: поскольку данные не хранятся последовательно, `std::list` имеет низкую эффективность использования кеша. В общем случае производительность `std::vector` будет лучше. С другой стороны, случаи, когда необходимо использовать связные списки, — это в основном бинарные деревья и графы. Для стека и очереди обычно используются предоставляемые языком программирования `stack` и `queue`, а не связные списки. **В:** Операция `res = [[0]] * n` создает двумерный список, является ли каждый `[0]` в нем независимым? Нет, не является независимым. В этом двумерном списке все `[0]` фактически являются ссылками на один и тот же объект. Если мы изменим один элемент, обнаружим, что все соответствующие элементы изменятся вместе с ним. Если нужно, чтобы каждый `[0]` в двумерном списке был независимым, можно использовать `res = [[0] for _ in range(n)]`. Принцип этого способа заключается в инициализации $n$ независимых объектов списка `[0]`. **В:** Операция `res = [0] * n` создает список, является ли каждое целое число 0 в нем независимым? В этом списке все целые числа 0 являются ссылками на один и тот же объект.