Рубрики
Без рубрики

Связанные списки Python

Автор оригинала: Frank Hofmann.

Связанные списки Python

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

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

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

Односвязный список

Рисунок 1: Односвязный список

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

Двусвязный список

Рисунок 2: Двусвязный список

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

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

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

Шаг 1: Узел как структура данных

Чтобы иметь структуру данных, с которой мы можем работать, мы определяем узел. Узел реализуется как класс с именем ListNode . Класс содержит определение для создания экземпляра объекта, в данном случае с двумя переменными – data для сохранения значения узла и next для хранения ссылки на следующий узел в списке. Кроме того, узел имеет следующие методы и свойства:

  • __init_() : инициализировать узел с данными
  • self.data : значение, хранящееся в узле
  • self.next : опорный указатель на следующий узел
  • has_value() : сравнение значения со значением узла

Эти методы гарантируют, что мы можем правильно инициализировать узел с нашими данными ( __init__ () ) и покрывать как извлечение данных, так и их хранение (через свойство self.data ), а также получение ссылки на подключенный узел (через свойство self.next ). Метод has_value() позволяет сравнить значение узла со значением другого узла.

Листинг 1: Класс ListNode

class ListNode:
    def __init__(self, data):
        "constructor to initiate this object"
        
        # store data
        self.data = data
        
        # store reference (next item)
        self.next = None
        return
    
    def has_value(self, value):
        "method to compare the value with the node data"
        if self.data == value:
            return True
        else:
            return False

Создание узла так же просто, как и создание экземпляра объекта класса ListNode :

Листинг 2: Создание экземпляров узлов

node1 = ListNode(15)
node2 = ListNode(8.2)
node3 = ListNode("Berlin")

Сделав это, мы имеем в наличии три экземпляра класса ListNode . Эти экземпляры представляют собой три независимых узла, которые содержат значения 15 (целое число), 8.2 (поплавок) и “Берлин” (строка).

Шаг 2: Создание класса для односвязного списка

В качестве второго шага мы определяем класс с именем Single Linked List , который охватывает методы, необходимые для управления узлами списка. Он содержит эти методы:

  • __init__() : инициировать объект
  • list_length() : возвращает количество узлов
  • output_list() : выводит значения узлов
  • add_list_item() : добавить узел в конец списка
  • unordered_search() : поиск в списке узлов с заданным значением
  • remove_list_item_by_id() : удалить узел в соответствии с его идентификатором

Мы рассмотрим каждый из этих методов шаг за шагом.

Метод __init__() определяет две внутренние переменные класса с именами head и tail . Они представляют собой начальный и конечный узлы списка. Изначально и head , и tail имеют значение None до тех пор, пока список пуст.

Листинг 3: Класс Single Linked List (часть первая)

class SingleLinkedList:
    def __init__(self):
        "constructor to initiate this object"
        
        self.head = None
        self.tail = None
        return

Шаг 3: Добавление узлов

Добавление элементов в список осуществляется через add_list_item() . Этот метод требует узла в качестве дополнительного параметра. Чтобы убедиться, что это правильный узел (экземпляр класса ListNode ), параметр сначала проверяется с помощью встроенной функции Python isinstance() . В случае успеха узел будет добавлен в конец списка. Если item не является ListNode , то он создается.

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

Листинг 4: Класс Single Linked List (часть вторая)

    def add_list_item(self, item):
        "add an item at the end of the list"
        
        if not isinstance(item, ListNode):
            item = ListNode(item)

        if self.head is None:
            self.head = item
        else:
            self.tail.next = item

        self.tail = item
            
        return

Метод list_length() подсчитывает узлы и возвращает длину списка. Чтобы перейти от одного узла к следующему в списке, в игру вступает свойство узла self.next и возвращает ссылку на следующий узел. Подсчет узлов выполняется в цикле while до тех пор, пока мы не достигнем конца списка, который представлен ссылкой None на следующий узел.

Листинг 5: Класс Единого связанного списка (часть третья)

    def list_length(self):
        "returns the number of list items"
        
        count = 0
        current_node = self.head
        
        while current_node is not None:
            # increase counter by one
            count = count + 1
            
            # jump to the linked node
            current_node = current_node.next
            
        return count

Метод output_list() выводит значения узлов с помощью свойства node data . Опять же, для перехода от одного узла к другому используется ссылка, предоставляемая через свойство next .

Листинг 6: Класс Single Linked List (часть четвертая)

    def output_list(self):
        "outputs the list (the value of the node, actually)"
        
         current_node = self.head
        
        while current_node is not None:
            print(current_node.data)
            
            # jump to the linked node
            current_node = current_node.next
            
        return

На основе класса SingleLinkedList мы можем создать правильный список с именем track и играть с его методами , как уже описано выше в Listings 3-6 . Поэтому мы создаем четыре узла списка, оцениваем их в цикле for и выводим содержимое списка. Листинг 7 показывает, как это запрограммировать, а Листинг 8 показывает выходные данные.

Листинг 7: Создание узлов и вывод списка

# create four single nodes
node1 = ListNode(15)
node2 = ListNode(8.2)
item3 = "Berlin"
node4 = ListNode(15)

track = SingleLinkedList()
print("track length: %i" % track.list_length())

for current_item in [node1, node2, item3, node4]:
    track.add_list_item(current_item)
    print("track length: %i" % track.list_length())
    track.output_list()

Вывод выглядит следующим образом и показывает, как растет список:

Листинг 8: Добавление узлов в список

$ python3 simple-list.py
track length: 0
track length: 1
15
track length: 2
15
8.2
track length: 3
15
8.2
Berlin
track length: 4
15
8.2
Berlin
15

Шаг 4: Поиск по списку

Поиск по всему списку осуществляется с помощью метода unordered_search() . Для поиска значения требуется дополнительный параметр. Глава списка-это отправная точка.

Во время поиска мы подсчитываем узлы. Чтобы указать совпадение, мы используем соответствующий номер узла. Метод unordered_search() возвращает список номеров узлов, представляющих совпадения. Например, и первый, и четвертый узел содержат значение 15. Поиск 15 результатов в списке с двумя элементами: [1, 4] .

Листинг 9: Метод поиска unordered_search()

    def unordered_search (self, value):
        "search the linked list for the node that has this value"
        
        # define current_node
        current_node = self.head
        
        # define position
        node_id = 1
        
        # define list of results
        results = []
        
        while current_node is not None:
            if current_node.has_value(value):
                results.append(node_id)
                
            # jump to the linked node
            current_node = current_node.next
            node_id = node_id + 1
        
        return results

Шаг 5: Удаление элемента из списка

Удаление узла из списка требует корректировки только одной ссылки – та, которая указывает на удаляемый узел, теперь должна указывать на следующий. Эта ссылка хранится удаляемым узлом и должна быть заменена. В фоновом режиме сборщик мусора Python заботится о несвязанных объектах и приводит их в порядок.

Следующий метод называется remove_list_item_by_id() . В качестве параметра он ссылается на номер узла, аналогичный значению, возвращаемому функцией unordered_search() .

Листинг 10: Удаление узла по номеру узла

    def remove_list_item_by_id(self, item_id):
        "remove the list item with the item id"
        
        current_id = 1
        current_node = self.head
        previous_node = None
        
        while current_node is not None:
            if current_id == item_id:
                # if this is the first node (head)
                if previous_node is not None:
                    previous_node.next = current_node.next
                else:
                    self.head = current_node.next
                    # we don't have to look any further
                    return
            
            # needed for the next iteration
            previous_node = current_node
            current_node = current_node.next
            current_id = current_id + 1
        
        return

Шаг 6: Создание Двусвязного списка

Чтобы создать двусвязный список, естественно просто расширить класс ListNode , создав дополнительную ссылку на предыдущий узел. Это влияет на методы добавления, удаления и сортировки узлов. Как показано в Листинге 11 , для хранения ссылочного указателя на предыдущий узел в списке было добавлено новое свойство с именем previous . Мы изменим наши методы, чтобы использовать это свойство также для отслеживания и обхода узлов.

Листинг 11: Класс узлов расширенного списка

class ListNode:
    def __init__(self, data):
        "constructor class to initiate this object"

        # store data
        self.data = data
        
        # store reference (next item)
        self.next = None

        # store reference (previous item)
        self.previous = None
        return

    def has_value(self, value):
        "method to compare the value with the node data"
        if self.data == value:
            return True
        else:
            return False

Теперь мы можем определить двусвязный список следующим образом:

Листинг 12: Класс Двойного Связанного списка

class DoubleLinkedList:
    def __init__(self):
        "constructor to initiate this object"

        self.head = None
        self.tail = None
        return

    def list_length(self):
        "returns the number of list items"
        
        count = 0
        current_node = self.head

        while current_node is not None:
            # increase counter by one
            count = count + 1
            
            # jump to the linked node
            current_node = current_node.next
        
        return count

    def output_list(self):
        "outputs the list (the value of the node, actually)"
        current_node = self.head

        while current_node is not None:
            print(current_node.data)

            # jump to the linked node
            current_node = current_node.next
        
        return

    def unordered_search (self, value):
        "search the linked list for the node that has this value"

        # define current_node
        current_node = self.head

        # define position
        node_id = 1

        # define list of results
        results = []

        while current_node is not None:
            if current_node.has_value(value):
                results.append(node_id)
            
            # jump to the linked node
            current_node = current_node.next
            node_id = node_id + 1
        
        return results

Как было описано ранее, добавление узлов требует немного больше действий. Листинг 13 показывает, как это реализовать:

Листинг 13: Добавление узлов в двусвязный список

    def add_list_item(self, item):
        "add an item at the end of the list"

        if isinstance(item, ListNode):
            if self.head is None:
                self.head = item
                item.previous = None
                item.next = None
                self.tail = item
            else:
                self.tail.next = item
                item.previous = self.tail
                self.tail = item
        
        return

При удалении элемента из списка необходимо учитывать аналогичные затраты. Листинг 14 показывает, как это сделать:

Листинг 14: Удаление элемента из двусвязного списка

    def remove_list_item_by_id(self, item_id):
        "remove the list item with the item id"
        
        current_id = 1
        current_node = self.head

        while current_node is not None:
            previous_node = current_node.previous
            next_node = current_node.next

            if current_id == item_id:
                # if this is the first node (head)
                if previous_node is not None:
                    previous_node.next = next_node
                    if next_node is not None:
                        next_node.previous = previous_node
                else:
                    self.head = next_node
                    if next_node is not None:
                        next_node.previous = None
                # we don't have to look any further
                return
 
            # needed for the next iteration
            current_node = next_node
            current_id = current_id + 1
                
        return

В листинге 15 показано, как использовать класс в программе Python.

Листинг 15: Построение двусвязного списка

# create three single nodes
node1 = ListNode(15)
node2 = ListNode(8.2)
node3 = ListNode("Berlin")
node4 = ListNode(15)

track = DoubleLinkedList()
print("track length: %i" % track.list_length())

for current_node in [node1, node2, node3, node4]:
    track.add_list_item(current_node)
    print("track length: %i" % track.list_length())
    track.output_list()

results = track.unordered_search(15)
print(results)

track.remove_list_item_by_id(4)
track.output_list()

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

Шаг 7: Создание Двусвязных списков с помощью deque

Поскольку другие инженеры столкнулись с той же проблемой, мы можем упростить вещи для себя и использовать одну из немногих существующих реализаций. В Python мы можем использовать объект deque из модуля collections . Согласно документации модуля:

Деки-это обобщение стеков и очередей (название произносится как “колода” и является сокращением от “двусторонняя очередь”). Deques поддерживают потокобезопасные, эффективные с точки зрения памяти приложения и всплывающие окна с обеих сторон deque с примерно одинаковой производительностью O(1) в любом направлении.

Например, этот объект содержит следующие методы:

  • append() : добавить элемент в правую часть списка (конец)
  • append_left() : добавить элемент в левую часть списка (head)
  • clear() : удалить все элементы из списка
  • count() : подсчет количества элементов с определенным значением
  • count() : подсчет количества элементов с определенным значением
  • count() : подсчет количества элементов с определенным значением
  • pop() : удаление элемента из правой части списка (конец)
  • pop left() : удаление элемента из левой части списка (head)
  • remove() : удаление элемента из списка
  • reverse() : перевернуть список

Базовая структура данных deque представляет собой список Python, который является двусвязным. Первый узел списка имеет индекс 0. Использование deque приводит к значительному упрощению класса ListNode . Единственное, что мы сохраняем, – это переменная класса data для хранения значения узла. Листинг 16 выглядит следующим образом:

Листинг 16: Класс ListNode с deque (упрощенный)

from collections import deque

class ListNode:
    def __init__(self, data):
        "constructor class to initiate this object"
        
        # store data
        self.data = data
        
        return

Определение узлов не меняется и аналогично Листингу 2 . С учетом этих знаний мы создаем список узлов следующим образом:

Листинг 17: Создание списка с помощью deque

track = deque([node1, node2, node3])
print("three items (initial list):")
for item in track:
    print(item.data)

Добавление элемента в начало списка работает с помощью метода append_left() как показано в листинге 18 :

Листинг 18: Добавление элемента в начало списка

# add an item at the beginning
node4 = ListNode(15)
track.append_left(node4)
print("four items (added as the head):")
for item in track:
    print(item.data)

Аналогично, append() добавляет узел в конец списка, как показано в Листинге 19 :

Листинг 19: Добавление элемента в конец списка

# add an item at the end
node5 = ListNode("Moscow")
print("five items (added at the end):")
track.append(node5)
for item in track:
    print(item.data)

Вывод

Связанные списки как структуры данных просты в реализации и обеспечивают большую гибкость использования. Это делается с помощью нескольких строк кода. В качестве улучшения вы можете добавить счетчик узлов – переменную класса, которая просто содержит количество узлов в списке. Это уменьшает определение длины списка до одной операции с O(1), и вам не нужно проходить весь список.

Для дальнейшего чтения и альтернативных реализаций вы можете посмотреть здесь:

Признание

Автор хотел бы поблагодарить Герольда Рупрехта и Мэнди Ноймайер за их поддержку и комментарии при подготовке этой статьи.