Двусвязный список: C++, Python (Пример кода)

Что такое двусвязный список?

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

Вот базовая структура двусвязного списка.

Структура двусвязного списка
Структура двусвязного списка

Каждый связанный список имеет головной и хвостовой узел. Головной узел не имеет узла «prev» (предыдущий указатель), а хвостовой узел не имеет узла «следующий».

Вот некоторые важные термины для двусвязного списка:

  • Предыдущая: Каждый узел связан со своим предыдущим узлом. Он используется как указатель или ссылка.
  • Далее: Каждый узел связан со своим следующим узлом. Он используется как указатель или ссылка.
  • Данные: Это используется для хранения данных в узле. «Данные» могут содержать другие Структуры данных внутри него. Например, в разделе «Данные» можно хранить строку, словарь, набор, хэш-карту и т. д.

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

Структура узла в двусвязном списке

Структура узла в двусвязном списке

Operaции двусвязного списка

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

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

  • Вставка спереди
  • Вставка в хвост или последний узел
  • Вставка после узла
  • Вставка перед узлом
  • Удаление спереди
  • Удаление из хвоста
  • Найти и удалить узел
  • Траверс голова к хвосту
  • Траверс хвостом к голове

Мы увидим реализацию и псевдокод для всех вышеописанных операций.

Вставка перед двусвязным списком

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

Например, есть заданный узел «15». Нам нужно добавить это в головной узел.

Вот два важных условия при выполнении этой операции:

  1. Новый узел будет головным узлом, если в двусвязном списке нет узла.
  2. Если головной узел уже существует, предыдущий головной узел будет заменен новым узлом.

Вот псевдокод этой операции:

function insertAtFront (ListHead, value):
  newNode = Node()
  newNode.value = value
  ListHead.prev = NewNode
  NewNode.next = ListHead
  newNode.prev = NULL
  return ListHead
Вставка в передний узел
Вставка в передний узел

Вставка в конец двусвязного списка

«Вставка в конец» означает, что мы создадим узел в связанном списке и поместим его в конец.

Для этого мы можем использовать два метода:

  • Метод 1: Начните перемещение с начала двусвязного списка, пока «следующий» не станет нулевым. Затем свяжите новый узел со «следующим».
  • Метод 2: Возьмите последний узел двусвязного списка. Затем «следующий» узел последнего узла будет связан с новым узлом. Теперь новый узел будет хвостовым узлом.

Вот псевдокод для вставки в хвостовой узел:

function insertAtTail(ListHead, value):
  newNode = Node()
  newNode.value = value
  newNode.next = NULL
  while ListHead.next is not NULL:
	then ListHead = ListHead.next
  newNode.prev = ListHead
  ListHead.next = newNode
  return ListHead
Вставка в конец связанного списка

Вставка в конец связанного списка

Вставка после узла

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

Вставка после узла

Мы хотим вставить данный узел, который будет связан после узла, имеющего значение «12».

Вот шаги для этого:

Шаг 1) Траверс от головы до последнего узла. Проверьте, какой узел имеет значение «12».

Шаг 2) Создайте новый узел и назначьте его следующим указателем узла «12». «Следующим» узлом нового узла будет номер 15.

Вот псевдокод для вставки узла после узла в двусвязном списке:

function insertAfter(ListHead, searchItem, value):
  List = ListHead
  NewNode = Node()
  NewNode.value = value
  while List.value is not equal searchItem
	then List = ListHead.next
  List = List.next
  NewNode.next = List.next
  NewNode.prev = List
  List.next = NewNode
Вставка после узла

Вставка после узла

Вставка перед узлом

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

Допустим, мы хотим вставить данный узел «15» перед узлом «12». Тогда вот шаги для этого:

Шаг 1) Перейдите по связанному списку от головного узла к хвостовому узлу.

Шаг 2) Проверьте, имеет ли следующий указатель текущего узла значение «12» или нет.

Шаг 3) Вставьте новый узел как «следующий» узел текущего узла.

Вот псевдокод для вставки узла перед узлом в двусвязном списке:

function insertBefore(ListHead, searchItem, value):
  List = ListHead
  NewNode = Node()
  NewNode.value = value
  while List.next.value is not equal searchItem
	then List = ListHead.next
  NewNode.next = List.next
  NewNode.prev = List
  List.next = NewNode
Вставка узла перед узлом

Вставка узла перед узлом

Удалить заголовок двусвязного списка

Головной узел в двусвязном списке, не имеющий предыдущего узла. Итак, следующим указателем будет новый головной узел, если мы хотим удалить головной узел. Нам также необходимо освободить память, занимаемую узлом, при удалении узла.

Вот шаги для удаления головного узла:

Шаг 1) Назначьте переменную текущему головному узлу.

Шаг 2) Посетите «следующий» узел текущего головного узла и установите для узла «prev» (предыдущий указатель) значение «NULL». Это означает, что мы отключаем второй узел от первого узла.

Шаг 3) Освободите память, занятую предыдущим головным узлом.

Вот псевдокод для удаления заголовка из двусвязного списка:

function deleteHead(ListHead):
  PrevHead = ListHead
  ListHead = ListHead.next
  ListHead.prev = NULL
  PrevHead.next = NULL
  free memory(PrevHead)
  return ListHead
Удаление головного узла

Удаление головного узла

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

Удалите хвост двусвязного списка.

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

Эта операция также известна как «удаление со спины».

Вот шаги, которые нужно сделать:

Шаг 1) Пройдите до хвостового узла двусвязного списка.

Шаг 2) Назначьте переменную или указатель хвостовому узлу.

Шаг 3) Сделайте «следующий» узел равным NULL и освободите память хвостового узла.

Вот псевдокод для удаления хвостового узла:

function DeleteTail( ListHead ):
  head = ListHead
  while ListHead.next is not NULL:
	ListHead = ListHead.next
  Tail = ListHead.next
  ListHead.next = NULL
  free memory( Tail )
  return head

Удалить хвост двусвязного

Найдите и удалите узел из двусвязного списка.

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

Вот шаги для поиска и удаления узла в двусвязном списке:

Шаг 1) Перемещайтесь по связанному списку от головы до тех пор, пока значение узла не станет равным искомому элементу.

Шаг 2) Назначьте переменную «deleteNode» соответствующему узлу.

Шаг 3) Назначьте предыдущий узел «deleteNode» следующему узлу.

Шаг 4) Освободите память «deleteNode»

Вот псевдокод для поиска и удаления узла из связанного списка:

function SearchAndDelete(ListHead, searchItem):
  head = ListHead
  while ListHead.next.value not equals searchItem:
	head = head.next
  deleteNode = head.next
  head.next = head.next.next
  head.next.prev = head
  deleteNode.next, deleteNode.next = NULL
  free memory(deleteNode)
  return ListHead
Найти и удалить Operaпроизводство

Операция поиска и удаления

Обход двусвязного списка вперед

Чтобы пройти от головного узла и перебирать следующий узел, пока не найдем «NULL». Проходя каждый узел, мы можем напечатать значение узла. Вот шаги для перемещения спереди (вперед):

Шаг 1) Назначьте указатель или переменную текущему головному узлу.

Шаг 2) Перейдите к следующему узлу головы, пока не получите «NULL».

Шаг 3) Распечатывайте данные узла на каждой итерации.

Шаг 4) Верните головной узел.

Вот псевдокод для обхода двусвязного списка спереди:

function traverseFromFront(ListHead):
  head = ListHead
  while head not equals to NULL:
	print head.data
	head = head.next
  return ListHead

Здесь возврат не является обязательным. Но после выполнения операций рекомендуется возвращать головной узел.

Обход двусвязного списка сзади

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

Вот шаги для обхода двусвязного списка сзади:

Шаг 1) Траверс, пока не достигнем хвостового узла.

Шаг 2) От хвостового узла мы будем перемещаться до тех пор, пока предыдущий узел не будет иметь значение «NULL». «prev» (предыдущий указатель) будет нулевым для головного узла.

Шаг 3) На каждой итерации мы будем печатать данные узла.

Вот псевдокод для перемещения сзади:

function traverseFromBack(ListHead):
  head = ListHead
  while head not equals NULL:
	head = head.next
  tail = head
  while tail not equal to NULL:
	print tail.value
	tail = tail.prev
  return ListHead

Разница между односвязным и двусвязным списком

Основное различие между одиночным списком и двусвязным списком заключается в количестве ссылок.

Разница между односвязным и двусвязным списком

Вот разница между узлами односвязного списка и структурой узлов двусвязного списка:

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

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

#include<iostream>
using namespace std;
  struct node{
  int data;
  struct node *next;
  struct node *prev;
  };
void insertFront(node* &listHead, int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;
  newNode->next = NULL;
  if(listHead!=NULL)
  {
	listHead->prev = newNode;
  	newNode->next = listHead;
  }
  
  listHead = newNode;
  cout<<"Added "<<value<<" at the front"<<endl;
  }
void insertEnd(node * &listHead, int value){
  if(listHead==NULL)
  {
  	insertFront(listHead,value);
  }
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;
  newNode->next = NULL;
  node *head = listHead;
  while(head->next!=NULL){
  head = head->next;
  }
  head->next = newNode;
  newNode->prev = head;
  cout<<"Added "<<value<<" at the end"<<endl;
  }
void insertAfter(node * &listHead, int searchValue,int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;	
  newNode->next = NULL;
  node *head = listHead;	
  while(head->next!=NULL &&  head->data!=searchValue){	
  head = head->next;	
  }	
  newNode->next = head->next;	
  head->next = newNode;	
  newNode->prev = head;	
  if(newNode->next !=NULL){	
  newNode->next->prev = newNode;	
  }	
  cout<<"Inserted "<<value<<" after node "<<searchValue<<endl;
  }
void insertBefore(node * &listHead, int searchValue,int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;	
  newNode->next = NULL;	
  node *head = listHead;	
  while(head->next!=NULL &&  head->next->data!=searchValue){	
  head = head->next;	
  }	
  newNode->next = head->next;	
  head->next = newNode;	
  newNode->prev = head;	
  if(newNode->next !=NULL){	
  newNode->next->prev = newNode;	
  }	
  cout<<"Inserted "<<value<<" before node "<<searchValue<<endl;	
  }
void traverseFromFront(node *listHead){
  node* head = new node();
  head = listHead;
  cout<<"Traversal from head:\t";	
  while(head!=NULL){	
  cout<<head->data<<"\t ";	
  head = head->next;	
  }	
  cout<<endl;
  }	
void traverseFromEnd(node *listHead){
  node* head = new node();
  head = listHead;	
  cout<<"Traversal from head:\t";	
  while(head->next!=NULL){	
  head = head->next;	
  }	
  node *tail = head;	
  while(tail!=NULL){	
  cout<<tail->data<<"\t";	
  tail = tail->prev;	
  }	
  cout<<endl;	
  }
void searchAndDelete(node **listHead, int searchItem){
  node* head = new node();
  head = (*listHead);
  while(head->next!=NULL &&  head->data!=searchItem){
  head = head->next;
  }
  if(*listHead == NULL || head == NULL) return;
  if((*listHead)->data == head->data){
  	*listHead = head->next;
  }
  if(head->next != NULL){
  	head->next->prev = head->prev;
  }
  
  if(head->prev != NULL){
  	head->prev->next = head->next;
  }
  free(head);
  cout<<"Deleted Node\t"<<searchItem<<endl;
  return;
  }
int main(){
  node *head = NULL;
  insertFront(head,5);
  insertFront(head,6);
  insertFront(head,7);
  insertEnd(head,9);
  insertEnd(head,10);
  insertAfter(head,5,11);
  insertBefore(head,5,20);
  traverseFromFront(head);
  traverseFromEnd(head);
  searchAndDelete(&head,7);
  traverseFromFront(head);
  traverseFromEnd(head);
}

Результат

Added 5 at the front
Added 6 at the front
Added 7 at the front
Aded 9 at the end
Added 10 at the end
Inserted 11 after node 5
Inserted 20 before node 5
Traversal from head:    7        6       20      5       11      9       10
Traversal from head:    10      9       11      5       20      6       7
Deleted Node    7
Traversal from head:    6        20      5       11      9       10
Traversal from head:    10      9       11      5       20      6

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

class node:
  def __init__(self,data = None, prev=None, next = None):
    self.data = data
    self.next = next
    self.prev = prev
class DoublyLinkedList:
  def __init__(self):
    self.head = None

  def insertFront(self, val):
    newNode = node(data=val)
    newNode.next = self.head
    if self.head is not None:
      self.head.prev = newNode
    self.head = newNode
    print("Added {} at the front".format(val))

  def insertEnd(self,val):
    newNode = node(data=val)
    if self.head is None:
      self.insertFront(val)
    
    temp = self.head
    while temp.next is not None:
      temp = temp.next
    temp.next = newNode
    newNode.prev = temp
    print("Added {} at the end".format(val))

  def traverseFromFront(self):
    temp = self.head
    print("Traversing from head:\t",end="")
    while temp:
      print("{}\t".format(temp.data),end="")
      temp = temp.next
    print()

  def traverseFromEnd(self):
    temp = self.head
    print("Traversing from Tail:\t",end="")
    while temp.next is not None:
      temp = temp.next
    tail = temp
    while tail is not None:
      print("{}\t".format(tail.data),end="")
      tail = tail.prev
    print()
  
  def insertAfter(self,searchItem, value):
    newNode = node(data=value)

    temp = self.head
    while temp.next is not None and temp.data is not searchItem:
      temp = temp.next
    
    newNode.next = temp.next
    temp.next = newNode
    newNode.prev = temp

    if newNode.next is not None:
      newNode.next.prev = newNode
    print("Inserted {} after node {}".format(value,searchItem))

  def insertBefore(self,searchItem, value):
    newNode = node(data=value)

    temp = self.head
    while temp.next is not None and temp.next.data is not searchItem:
      temp = temp.next
    
    newNode.next = temp.next
    temp.next = newNode
    newNode.prev = temp

    if newNode.next is not None:
      newNode.next.prev = newNode
    print("Inserted {} before node {}".format(value,searchItem))

  def searchAndDelete(self,searchItem):
    temp = self.head

    while temp.next is not None and temp.data is not searchItem:
      temp = temp.next
    
    if self.head is None or temp is None:
      return

    if self.head.data is temp.data:
      self.head = temp.next

    if temp.next is not None:
      temp.next.prev = temp.prev
    
    if temp.prev is not None:
      temp.prev.next = temp.next
    
    print("Deleted Node\t{}".format(searchItem))
    return

doublyLinkedList = DoublyLinkedList()
doublyLinkedList.insertFront(5)
doublyLinkedList.insertFront(6)
doublyLinkedList.insertFront(7)
doublyLinkedList.insertEnd(9)
doublyLinkedList.insertEnd(10)
doublyLinkedList.insertAfter(5, 11)
doublyLinkedList.insertBefore(5, 20)
doublyLinkedList.traverseFromFront()
doublyLinkedList.traverseFromEnd()
doublyLinkedList.searchAndDelete(7)
doublyLinkedList.traverseFromFront()
doublyLinkedList.traverseFromEnd()

Результат

Added 5 at the front
Added 6 at the front
Added 7 at the front
Added 9 at the end
Added 10 at the end
Inserted 11 after node 5
Inserted 20 before node 5
Traversing from head:   7       6       20      5       11      9       10
Traversing from Tail:   10      9       11      5       20      6       7
Deleted Node    7
Traversing from head:   6       20      5       11      9       10
Traversing from Tail:   10      9       11      5       20      6

Сложность двусвязного списка

Обычно временная сложность делится на три типа.

К ним относятся:

  1. лучший случай
  2. Средний случай
  3. Худший случай

Временная сложность в лучшем случае для двусвязного списка:

  1. Вставка в заголовок или хвост будет стоить O(1). Потому что нам не нужно перемещаться внутри связанного списка. Указатель головы и хвоста может дать нам доступ к узлу головы и хвоста с временной сложностью O (1).
  2. Удаление в начале или в хвосте будет стоить O(1).
  3. Поиск узла будет иметь временную сложность O(1). Потому что целевой узел может быть головным узлом.

Временная сложность в среднем случае для двусвязного списка:

  1. Вставка в начале или в конце будет иметь временную сложность стоимости O(1).
  2. Удаление в начале или в хвосте будет иметь временную сложность O(1).
  3. Поиск узла будет иметь временную сложность O(n). Потому что элемент поиска может находиться где угодно в связанном списке. Здесь «n» — это общее количество узлов, присутствующих в связанном списке.

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

Сложность памяти двусвязного списка

Сложность памяти равна O(n), где «n» — общее количество узлов. При реализации связанного списка мы должны освободить использованную память. В противном случае для большего связанного списка это приведет к утечкам памяти.

Резюме

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