16 KiB
Резюме
Ключевые моменты
- Массивы и связные списки — это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: непрерывное и распределенное хранение. Их характеристики взаимодополняющие.
- Массивы поддерживают произвольный доступ, занимают меньше памяти; но операции вставки и удаления элементов неэффективны, а длина после инициализации не может быть изменена.
- Связные списки обеспечивают эффективную вставку и удаление узлов путем изменения ссылок (указателей), а также позволяют гибко регулировать длину; но доступ к узлам неэффективен, занимают больше памяти. Распространенные типы связных списков включают односвязные списки, кольцевые списки и двусвязные списки.
- Список — это упорядоченная коллекция элементов с поддержкой добавления, удаления, поиска и изменения, обычно реализуемая на основе динамического массива. Он сохраняет преимущества массива и при этом позволяет гибко регулировать длину.
- Появление списков значительно повысило практичность массивов, но может привести к частичной потере памяти.
- Во время выполнения программы данные в основном хранятся в памяти. Массивы обеспечивают более высокую эффективность использования памяти, в то время как связные списки более гибки в использовании памяти.
- Кеш обеспечивает быстрый доступ к данным для процессора через механизмы загрузки данных, такие как строки кеша, предварительная выборка, пространственная и временная локальность, значительно повышая эффективность выполнения программы.
- Благодаря более высокому коэффициенту попадания в кеш массивы обычно более эффективны, чем связные списки. При выборе структуры данных следует принимать соответствующее решение в зависимости от конкретных требований и сценария.
Вопросы и ответы
В: Влияет ли хранение массива в стеке или в куче на временную и пространственную эффективность?
Массивы, хранящиеся как в стеке, так и в куче, размещаются в непрерывном пространстве памяти, и эффективность операций с данными практически одинакова. Однако стек и куча имеют свои особенности, что приводит к следующим различиям.
- Эффективность выделения и освобождения: стек — это небольшая область памяти, выделение выполняется компилятором автоматически; в то время как память кучи относительно больше, может выделяться динамически в коде и более подвержена фрагментации. Поэтому операции выделения и освобождения в куче обычно медленнее, чем в стеке.
- Ограничение размера: память стека относительно мала, размер кучи обычно ограничен доступной памятью. Поэтому куча более подходит для хранения больших массивов.
- Гибкость: размер массива в стеке должен быть определен во время компиляции, в то время как размер массива в куче может быть определен динамически во время выполнения.
В: Почему массив требует элементов одного типа, а в связном списке это не подчеркивается?
Связный список состоит из узлов, узлы соединены ссылками (указателями), каждый узел может хранить данные разных типов, например int, double, string, object и т.д.
В отличие от этого, элементы массива должны быть одного типа, чтобы можно было получить позицию соответствующего элемента путем вычисления смещения. Например, если массив одновременно содержит типы int и long, отдельные элементы занимают 4 байта и 8 байт соответственно, в этом случае нельзя использовать следующую формулу для вычисления смещения, поскольку массив содержит две разные «длины элемента».
# Адрес элемента в памяти = Адрес массива в памяти (адрес первого элемента) + Длина элемента * Индекс элемента
В: После удаления узла 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 являются ссылками на один и тот же объект.