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

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

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

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

1. Связанный список

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

Рассмотрим простой связанный список, показанный ниже.

Каждое значение или данные связаны с указателями.

Список ссылок состоит из узлов и указателей, как показано ниже.

У узла есть значение (данные) и указатель, указывающий на следующий узел, к которому он подключается. Указатели — это просто ссылка на ячейку памяти значения или данных.

Существует два основных типа связанных списков

а) Односвязные списки

б) двусвязный список

а) Односвязные списки

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

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

class Node(object):

    def __init__(self, data, next_node=None):
        self.data = data
        self.next_node = next_node

    def get_data(self):
        return self.data

    def get_next_node(self):
        return self.get_next_node

    def set_data(self, data):
        self.data = data

    def set_next_node(self, next_node):
        self.next_node = next_node

Теперь давайте создадим класс односвязного списка, используя объект Node, как показано в шаблоне ниже.

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

class LinkedList(object):

    def __init__(self, next_node=None):
        self.next_node = next_node
        self.size = 0

    def get_size(self):
        return self.size

    def get_points_to(self):
        return self.get_points_to

    def add_node(self, data):
        new_node = Node(data, self.next_node)
        self.next_node = new_node
        self.size += 1
        return str(data) + " added to the linked list"

    def print_node(self):
        current_node = self.next_node
        while current_node:
            print (current_node.data)
            current_node = current_node.next_node

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

Код

my_linked_list = LinkedList()
print ("Inserting data")
print (my_linked_list.add_node(15))
print (my_linked_list.add_node(4))
print (my_linked_list.add_node(26))
print ("Printing Linked List Data")
my_linked_list.print_node()
print ("Size is")
print(my_linked_list.get_size())
print ("Head currently is at")
print(my_linked_list.next_node.data)

Выход будет таким

# Output
Inserting data
15 added to the linked list
4 added to the linked list
26 added to the linked list
Printing Linked List Data
26
4
15
Size is
3
Head currently is at
26

б) двусвязный список

Двусвязный список — это связанная структура данных, состоящая из набора последовательно связанных узлов. Каждый узел содержит три поля: два поля ссылок (ссылки на предыдущий и следующий узел в последовательности узлов) и одно поле данных.

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

class Node(object):
    
    def __init__(self, data=None, previous_node=None, next_node=None):
        self.data = data
        self.previous_node = previous_node
        self.next = next_node

    def __repr__(self):
        return repr(self.data)

    def get_data(self):
        return self.data

    def set_data(self, data):
        self.data = data

    def get_next_node(self):
        return self.get_next_node
    
    def get_previous_node(self):
        return self.get_next_node

    def set_next_node(self, previous_node):
        self.previous_node = previous_node

Теперь давайте создадим класс двусвязного списка, используя объект Node.

class DoublyLinkedList(object):

    def __init__(self):
        self.head = None

    def __repr__(self):
        # Return a string representation of the list.
        nodes = []
        curr = self.head
        while curr:
            nodes.append(repr(curr))
            curr = curr.next
        return '[' + ', '.join(nodes) + ']'

    def prepend(self, data):
        #Insert a new element at the beginning of the list.
        new_head = Node(data=data, next_node=self.head)
        if self.head:
            self.head.prev = new_head
        self.head = new_head

    def append(self, data):
        # Insert a new element at the end of the list.
        if not self.head:
            self.head = Node(data=data)
            return
        curr = self.head
        while curr.next:
            curr = curr.next
        curr.next = Node(data=data, previous_node=curr)

    def find(self, key):
        """
        Search for the first element with `data` matching
        `key`. Return the element or `None` if not found.
        """
        curr = self.head
        while curr and curr.data != key:
            curr = curr.next
        return curr  # Will be None if not found

    def remove_elem(self, node):
        # Unlink an element from the list.
        if node.prev:
            node.prev.next = node.next
        if node.next:
            node.next.prev = node.prev
        if node is self.head:
            self.head = node.next
        node.prev = None
        node.next = None

    def remove(self, key):
        # Remove the first occurrence of `key` in the list.
        elem = self.find(key)
        if not elem:
            return
        self.remove_elem(elem)

    def reverse(self):
        # Reverse the list in-place.
        curr = self.head
        prev_node = None
        while curr:
            prev_node = curr.prev
            curr.prev = curr.next
            curr.next = prev_node
            curr = curr.prev
        self.head = prev_node.prev

Давайте проверим приведенный выше двусвязный список.

doubly_linked_list = DoublyLinkedList()

print(doubly_linked_list)
print("Insert a new element 10 at the beginning of the Doubly Linked List")
doubly_linked_list.prepend(10)
print(doubly_linked_list)
print("Insert a new element 23 at the beginning of the Doubly Linked List")
doubly_linked_list.prepend(23)
print(doubly_linked_list)
print("Insert a new element 10 at the end of the Doubly Linked List")
doubly_linked_list.append(7)
print(doubly_linked_list)
print("Unlink (remove) element 10 from the Doubly Linked List")
doubly_linked_list.remove(10)
print(doubly_linked_list)

Вывод вышеуказанной программы:

# Output


[]
Insert a new element 10 at the beginning of the Doubly Linked List
[10]
Insert a new element 23 at the beginning of the Doubly Linked List
[23, 10]
Insert a new element 10 at the end of the Doubly Linked List
[23, 10, 7]
Unlink (remove) element 10 from the Doubly Linked List
[23, 7]

Производительность связанного списка

Ниже приведены показатели производительности нотации Big O для связанного списка.

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

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

2. Стек

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

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

я) толкать

ii) поп

я) толкнуть

push добавляет элемент в стек

ii) поп

pop удаляет самый последний элемент из стека.

Код для создания шаблона стека

class Stack (object):

    def __init__(self):
        self.items = []

    def is_empty(self):
        return self.items == []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def peek(self):
        return self.items[len(self.items) - 1]

    def size(self):
        return len(self.items)

    def print_items(self):
        print(self.items)

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

Стек вначале пуст

# Create a stack Object
my_stack = Stack()
# Stack is empty at the beginning
print(s.is_empty())

Теперь давайте поместим элемент (пример 26) в стек.

# Push 26 to the empty stack
my_stack.push(26)

Теперь нажмите больше элементов

# Push more elements to the stack
my_stack.push(4)
my_stack.push(36)
my_stack.push(81)
my_stack.push(15)

Просмотр элементов в стеке

s.print_items()

Полный код, как показано ниже

s.push(4)
s.push(36)
s.push(81)
s.push(15)
print(s.peek())
s.print_items()
print(s.size())
print("Is the stack empty? %s" % s.is_empty())
s.push(8.4)
s.print_items()
print(s.pop())
print(s.pop())
print(s.size())
s.print_items()
print("Is the stack empty? %s" % s.is_empty())
print(s.pop())
print(s.pop())
print(s.pop())
print(s.pop())
s.print_items()
print("Is the stack empty? %s" % s.is_empty())

Выход

Is the stack empty? True
15
[26, 4, 36, 81, 15]
5
Is the stack empty? False
[26, 4, 36, 81, 15, 8.4]
8.4
15
4
[26, 4, 36, 81]
Is the stack empty? False
81
36
4
26
[]
Is the stack empty? True

Производительность стека

Ниже приведены показатели производительности нотации Big O для стека.

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

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

4. Очередь

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

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

Операция добавления элемента в конец очереди называется постановкой в ​​очередь.

Операция удаления элемента с фронта называется удалением из очереди.

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

Операции очереди делают ее структурой данных «первым пришел — первым обслужен» (FIFO). В структуре данных FIFO первый элемент, добавленный в очередь, будет первым удаленным.

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

Код для создания очереди

Создайте класс с инициализированной пустой очередью (массив за сценой).

class SimpleQueue:
    def __init__(self):
        self.queue = []

Проверьте, пуста ли очередь или нет

def is_empty(self):
    return self.queue == []

Теперь поставьте в очередь (добавьте элемент) в очередь

def enqueue(self, item):
    self.queue.insert(0, item) 
    # 0 is the index of the element to insert

Найдите размер очереди

def size(self):
    return len(self.queue)

Теперь из очереди (удалить элемент) в очередь

def dequeue(self):
    return self.queue.pop()

Найдите размер очереди

def size(self):
    return len(self.queue)

Все вместе с get_queue для просмотра содержимого очереди

class SimpleQueue:
    def __init__(self):
        self.items = []

    def is_empty(self):
        return self.items == []

    def enqueue(self, item):
        self.items.insert(0, item)
        
    def dequeue(self):
        return self.items.pop()

    def size(self):
        return len(self.items)

    def get_queue(self):
        return self.items

Теперь давайте проверим очередь

q = SimpleQueue()

q.enqueue(34)
q.enqueue("hi")
q.enqueue(4)
q.enqueue('dog')
q.enqueue(True)
q.enqueue(25)
print(q.size())
print(q.get_queue())
q.dequeue()
print(q.size())
print(q.get_queue())
q.dequeue()
print(q.get_queue())

Вывод приведенного выше кода

# Output
6
[25, True, 'dog', 4, 'hi', 34]
5
[25, True, 'dog', 4, 'hi']
[25, True, 'dog', 4]

Производительность очереди

Ниже приведены показатели производительности очереди в нотации Big O.

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

Доступ и поиск элементов в Queue будут масштабироваться линейно. Вставка и удаление элементов в очереди является постоянной с точки зрения временной сложности, даже если количество элементов в связанном списке значительно увеличивается.

Дерево

Древовидные структуры данных выглядят как древовидные структуры. Деревья иерархичны по своей природе.

Каждый узел в дереве может иметь одно из трех состояний.

  1. Это может быть родительский узел.
  2. Это может быть дочерний узел
  3. Он может быть и родителем, и ребенком одновременно.

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

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

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

Операции с деревом

Теперь давайте посмотрим, как элементы добавляются в бинарное дерево.

Добавление первого элемента в бинарное дерево

Добавление элемента в существующее бинарное дерево

Поиск подходящего места для добавления нового элемента

Добавление еще одного элемента 100

Добавить еще один элемент 30

и это продолжается…

Однако за хорошую производительность приходится платить, поскольку кодирование двоичного дерева — одна из наиболее сложных структур данных.

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

Создайте объект Node, как показано ниже.

class Node(object):

    def __init__(self, item):
        self.item = item
        self.left = None
        self.right = None
        self.parent = None

    def get_item(self):
        return self.item

    def get_left(self):
        return self.left

    def set_left(self, item):
        self.left = item

    def get_right(self):
        return self.right

    def set_right(self, item):
        self.right = item

    def get_parent(self):
        return self.parent

    def set_parent(self, item):
        self.parent = item

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

from DataStructures.Tree.node import Node

class BinaryTree(object):

    def __init__(self):
        self.root = None
        self.size = 0

Добавьте функцию для определения размера дерева в любое время

def get_size(self):
    return self.size

Добавить элемент

def add_item(self, item):
    node = Node(item)
    # If this is first item in the tree , set it as root
    if self.root is None:
        self.root = node
        print('Root is empty. Setting root to : %s' % node.get_item())
        self.size += 1
    else:  # Otherwise, we need to insert the item into the tree using the binary tree insert algorithm
        self.insert(self.root, node)

def insert(self, parent, child):

    # If child is less than parent, it goes here
    if child.get_item() < parent.get_item():
        if parent.get_left() is None:  # If left node is None (null) we have found our spot!
            parent.set_left(child)
            child.set_parent(parent)
            self.size += 1
        # Otherwise we need to call insert again and test for left or right (recursion)
        else:
            self.insert(parent.get_left(), child)

    # If child is greater than parent, it goes to the right
    elif child.get_item() > parent.get_item():
        # If right node is null, we have found our spot!
        if parent.get_right() is None:
            parent.set_right(child)
            child.set_parent(parent)
            self.size += 1
        else:
            # Otherwise we need to call insert again and test for left and right using recursion
            self.insert(parent.get_right(), child)
    # If the parent and child happen to be equal, we don't do anything assuming that the data in the binary tree is unique

Поиск элемента

def does_tree_contains(self, item):
    current_node = self.get_node(item)
    if current_node is None:
        return False
    else:
        return True

def get_node(self, item):
    current_node = self.root
    while current_node is not None:
        if item == current_node.get_item():
            return current_node
        elif item < current_node.get_item():
            current_node = current_node.get_left()
        else:
            current_node = current_node.get_right()
    return None

Удалить элемент

def delete_item(self, item):
    deleted = False

    # Make sure the root isn't null meaning the tree is empty
    if self.root is None:
        return False

    if self.get_node(item) is None:
        return False

    # Find the node to delete
    node_to_be_deleted = self.get_node(item)
    print("Deleting node %s" % node_to_be_deleted.get_item())

    if node_to_be_deleted is not None:
        # If the node to delete doesn't have any children, just delete it
        if node_to_be_deleted.get_left() is None and node_to_be_deleted.get_right() is None:
            self.unlink_item(node_to_be_deleted, None)
            deleted = True
        # If the node to delete only has a right child, remove it in the h  hierarchy
        elif node_to_be_deleted.get_left() is None and node_to_be_deleted.get_right() is not None:
            self.unlink_item(node_to_be_deleted, node_to_be_deleted.get_right())
            deleted = True
        # If the node to delete only has a left child, remove it in the h  hierarchy
        elif node_to_be_deleted.get_left() is not None and node_to_be_deleted.get_right() is None:
            self.unlink_item(node_to_be_deleted, node_to_be_deleted.get_left())
            deleted = True
        # The node has both children, do a node swap to delete
        else:
            child = node_to_be_deleted.get_left()
            while child.get_right() is not None and child.get_left() is not None:
                child = child.get_right()
            # We have the right most leaf node. We can replace the current node with this node
            child.get_parent().set_right(None)  # Remove the leaf node from it's current position

            child.set_left(node_to_be_deleted.get_left())
            child.set_right(node_to_be_deleted.get_right().get_right())

            self.unlink_item(node_to_be_deleted, child)
            deleted = True
        if deleted:
            self.size -= 1

    return deleted

def unlink_item(self, node_to_be_deleted, new_node):

    if node_to_be_deleted.get_item() == self.root.get_item():
        new_node.set_left(node_to_be_deleted.get_left())
        new_node.set_right(node_to_be_deleted.get_right())
        self.root = new_node
    elif node_to_be_deleted.get_parent().get_right().get_item() == node_to_be_deleted.get_item():
        node_to_be_deleted.get_parent().set_right(new_node)
    else:
        node_to_be_deleted.get_parent().set_left(new_node)

Простая вспомогательная функция для отображения данных дерева

def display_tree_data(self):

    print("Tree Root is: %s" % self.root.get_item())
    child_node = self.root.get_left()
    print("<< Left side Nodes from tree Root >>")
    while child_node:
        print("Left child_node is %s" % child_node.get_item())
        if child_node.get_right() is not None:
            print("Right child_node is %s" % child_node.get_right().get_item())
        child_node = child_node.get_left()
    print("<< Right side Nodes from tree Root >>")
    child_node = self.root.get_right()
    while child_node:
        print("Right child_node is %s" % child_node.get_item())
        if child_node.get_left() is not None:
            print("Left child_node is %s" % child_node.get_left().get_item())
        child_node = child_node.get_right()

Тестируем все вместе

bt = BinaryTree()

print(bt.get_size())


bt.add_item(50)
bt.add_item(25)
bt.add_item(100)
bt.add_item(150)
bt.add_item(200)
bt.add_item(10)
bt.add_item(30)
bt.add_item(15)
bt.add_item(110)
print("Does the Node Contains 400? : %s" % bt.does_tree_contains(400))
print("Does the Node Contains 25? : %s" % bt.does_tree_contains(25))
print("Does the Node Contains 0? : %s" % bt.does_tree_contains(0))
print("Does the Node Contains 10? : %s" % bt.does_tree_contains(10))
print("Tree size is: %s" % bt.get_size())
bt.display_tree_data()
print("Deleting 50 from the tree: %s" %  bt.delete_item(50))
print("Tree size is: %s" % bt.get_size())
print("New Root is: " + str(bt.root.get_item()))
bt.display_tree_data()
print("Deleting 50 from the tree: " + str(bt.delete_item(50)))
bt.display_tree_data()
print("Deleting 200 from the tree: " + str(bt.delete_item(200)))
bt.display_tree_data()
print("Deleting 15 from the tree: " + str(bt.delete_item(15)))
bt.display_tree_data()
bt.add_item(40)
bt.add_item(113)
bt.display_tree_data()
print("Tree size is: %s" % bt.get_size())

Вывод приведенного выше кода

# Output
0
Root is empty. Setting root to : 50
Does the Node Contains 400? : False
Does the Node Contains 25? : True
Does the Node Contains 0? : False
Does the Node Contains 10? : True
Tree size is: 9
Tree Root is: 50
 << Left side Nodes from tree Root >>
Left child_node is 25
Right child_node is 30
Left child_node is 10
Right child_node is 15
<< Right side Nodes from tree Root >>
Right child_node is 100
Right child_node is 150
Left child_node is 110
Right child_node is 200
Deleting 50 from the tree: True
Tree size is: 8
New Root is: 30
Tree Root is: 30
 << Left side Nodes from tree Root >>
Left child_node is 25
Left child_node is 10
Right child_node is 15
<< Right side Nodes from tree Root >>
Right child_node is 100
Right child_node is 150
Left child_node is 110
Right child_node is 200
Deleting 50 from the tree: False
Tree Root is: 30
 << Left side Nodes from tree Root >>
Left child_node is 25
Left child_node is 10
Right child_node is 15
<< Right side Nodes from tree Root >>
Right child_node is 100
Right child_node is 150
Left child_node is 110
Right child_node is 200
Deleting 200 from the tree: True
Tree Root is: 30
 << Left side Nodes from tree Root >>
Left child_node is 25
Left child_node is 10
Right child_node is 15
<< Right side Nodes from tree Root >>
Right child_node is 100
Right child_node is 150
Left child_node is 110
Deleting 15 from the tree: True
Tree Root is: 30
 << Left side Nodes from tree Root >>
Left child_node is 25
Left child_node is 10
<< Right side Nodes from tree Root >>
Right child_node is 100
Right child_node is 150
Left child_node is 110
Tree Root is: 30
 << Left side Nodes from tree Root >>
Left child_node is 25
Left child_node is 10
<< Right side Nodes from tree Root >>
Right child_node is 100
Left child_node is 40
Right child_node is 150
Left child_node is 110
Tree size is: 8

Производительность бинарного дерева

Ниже приведены показатели производительности нотации Big O для двоичного дерева.

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

Как видите, все четыре операции в двоичном дереве имеют одинаковую производительность O(log n), что делает их отличным выбором для эффективного хранения, поиска, доступа и удаления данных.

Вывод

Мы увидели 5 основных структур данных, их реализацию и производительность с точки зрения нотации Bog O. Каждая из этих структур данных имеет свои сильные и слабые стороны. Как программисты, мы должны выбирать эти структуры данных в зависимости от проблемы, которую нам нужно решить. Во многих случаях достижение масштаба и производительности в информатике требует компромиссов. Поэтому выбирайте тот люкс, который лучше всего подходит для ваших условий.

Понимание этих основных концепций поможет вам писать лучшие программы. Освоение этих алгоритмов и структур данных также повысит вашу уверенность в технических дискуссиях и интервью.

Исходный код

Если вам интересно изучить исходные коды этой статьи, вы можете найти их в моем Git Hub: https://github.com/sachinsadasivan/algorithms-and-data-structures.