Единично свързан списък в структури от данни

⚡ Умно обобщение

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

  • 🧩 Структура на възела: Всеки възел съдържа едно поле за данни и едно до указател към следващия възел; опашния възел до показалецът е NULL.
  • 📦 Списък срещу масив: Едносвързаните списъци са предпочитани, когато броят на елементите е неизвестен, не се изисква произволен достъп и вмъкването в средата на списъка е често срещано.
  • ➕ Вмъквания: Възлите могат да се добавят в началото, в края, след съвпадащ възел или преди съвпадащ възел, използвайки пренаписвания на следващия указател.
  • ➖ Изтривания: Премахването на главата, опашката или търсения възел актуализира указателите към съседи и освобождава освободената памет, за да се избегнат течове.
  • 🔁 Преминаване: Поддържа се само обхождане напред, тъй като няма предишен указател, така че обхождането назад в едносвързан списък не е възможно.
  • 💻 C++ намлява Python Code: Пълните имплементации показват процедури за вмъкване, изтриване, търсене и преминаване с изпълним изход.
  • 📊 Сложност: Вмъкването или изтриването на заглавие е O(1); търсенето и други вмъквания и изтривания са O(n); пространствената сложност е O(n).

Единично свързан списък

Какво е единично свързан списък?

Едносвързаният списък е линейна и еднопосочна структура от данни, където данните се съхраняват във възлите и всеки възел е свързан чрез връзка със следващия си възел. Всеки възел съдържа поле за данни и връзка към следващия възел. Едносвързаните списъци могат да се обхождат само в една посока, докато Двойно свързан списък може да се преминава в двете посоки.

Ето структурата на възлите на едносвързан списък:

Структура на възел в свързан списък

Структура на възел в свързан списък

Защо да използваме свързан списък върху масив?

Няколко сценария предпочитат свързан списък пред Array:

  • Неизвестен брой елементи: Когато необходимият брой елементи не е известен по време на компилация, свързан списък разпределя памет динамично, когато елементите се добавят.
  • Произволен достъп: Когато не е необходим произволен индексиран достъп, свързан списък е подходящ избор.
  • Вмъкване в средата: Вмъкването в средата на масив изисква изместване на елементи. Свързаният списък позволява вмъкване на всяка позиция чрез пренаписване само на няколко указателя.

Operaции на единично свързан списък

Едносвързаният списък е подходящ за динамично разпределяне на памет. Той поддържа стандартните операции на свързания списък, т.е. вмъкване, изтриване, търсене, актуализиране, сливане на два списъка и обхождане.

В тази статия са разгледани следните операции:

  • Вмъкване в главата
  • Вмъкване на опашката
  • Вмъкване след възел
  • Вмъкване преди възел
  • Изтрийте главния възел
  • Изтрийте опашния възел
  • Търсене и изтриване на възел
  • Обхождане на свързания списък

Ето пример за свързан списък с четири възела.

Пример за единично свързан списък

Пример за единично свързан списък

Вмъкване в началото на едносвързан списък

Това е проста операция. Обикновено е известна като добавяне към едносвързан списък. Създава се нов възел и се поставя начело на списъка.

За да извършите тази операция, следвайте две важни условия:

  1. Ако списъкът е празен, новосъздаденият възел става главен възел и неговият до показалецът е NULL.
  2. Ако списъкът не е празен, новият възел става главен възел и неговият до показалецът сочи към предишния главен възел.

Ето псевдокодът за вмъкване на възел в началото на свързан списък:

function insertAtHead(head, value):
  newNode = Node(value)
  if head is NULL:
    head = newNode
    return head
  else:
    newNode.next = head
    return newNode

Вмъкване в главата

Вмъкване в главата

Вмъкване в края на едносвързан списък

Вмъкването на възел в края на свързан списък е подобно на вмъкването в началото. Преминете към опашния възел, след което насочете неговия до указател към новия възел. Ако началният възел е NULL, новият възел става начален възел.

Стъпка 1) Преминавайте, докато до Указателят на текущия възел става NULL.

Стъпка 2) Създайте нов възел с указаната стойност.

Стъпка 3) Задайте новия възел като следващ възел на опашния възел.

Псевдокодът за вмъкване в края на единичен списък:

function insertAtEnd(head, value):
  newNode = Node(value)
  if head is NULL:
    head = newNode
    return head
  while head.next is not NULL:
    head = head.next
  head.next = newNode
  newNode.next = NULL

Вмъкване в опашката

Вмъкване на опашката

Вмъкване след възел в едносвързан списък

Вмъкването след възел се състои от две части: търсене на целевия възел и добавяне на нов възел след него. Обходете списъка, докато се намери съвпадение, след което вмъкнете новия възел.

Стъпка 1) Преминавайте, докато стойността на текущия възел стане равна на търсения елемент.

Стъпка 2) Задайте новия възел до указател към текущия възел до указател.

Стъпка 3) Насочете текущия възел до указател към новия възел.

Псевдокод:

function insertAfter(head, value, searchItem):
  newNode = Node(value)
  while head.value != searchItem:
    head = head.next
  newNode.next = head.next
  head.next = newNode

Вмъкване на възел след възел в единично свързан списък

Вмъкване на възел след възел в единично свързан списък

Вмъкване преди възел в едносвързан списък

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

Стъпка 1) Преминете, докато стойността на следващия възел се изравни с търсения елемент.

Стъпка 2) Създайте нов възел и го задайте до указател към текущия възел до.

Стъпка 3) Насочете текущия възел до към новия възел.

function insertBefore(head, value, searchItem):
  newNode = Node(value)
  while head.next.value != searchItem:
    head = head.next
  newNode.next = head.next
  head.next = newNode

Вмъкване на възел преди възел в единично свързан списък

Вмъкване на възел преди възел в единично свързан списък

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

Указателят на „head“ възел се предоставя като параметър. Главният възел се премахва и следващият възел става новият „head“. Паметта на изтрития възел трябва да бъде освободена, за да се избегнат изтичания на памет.

Стъпка 1) Задайте следващия възел на главата като нова глава.

Стъпка 2) Освободете разпределената памет на предишния главен възел.

Стъпка 3) Върнете новия главен възел.

function deleteHead(head):
  temp = head
  head = head.next
  free(temp)
  return head

Изтриване на главата на свързан списък

Изтриване на главата на свързан списък

Изтриване на опашката на едносвързания списък

Изтриването на опашния възел е подобно на изтриването на главния възел. Разликата е, че е необходимо преминаване до края на списъка. В едносвързан списък, възелът, чийто до показалецът е NULL, това е опашният възел.

Стъпка 1) Преминете точно до опашния възел. Запазете текущия възел.

Стъпка 2) Освободете паметта на следващия възел (опашката).

Стъпка 3) Задайте следващия възел на текущия възел на NULL.

function deleteTail(head):
  while head.next.next is not NULL:
    head = head.next
  free(head.next)
  head.next = NULL

Изтриване на опашката на единично свързан списък

Изтриване на опашката на единично свързан списък

Търсене и изтриване на възел от едносвързан списък

Тази функция изпълнява две задачи: търсене и изтриване. Обхожда до края на списъка. Ако се намери съответстващ възел, той се премахва и се свързва отново с предишния възел. до указател.

Стъпка 1) Преминете до края на списъка. Проверете дали текущият възел е равен на търсения възел.

Стъпка 2) Ако се намери съвпадение, се запазва указател към текущия възел.

Стъпка 3) - до на предишния възел става следващият възел на текущия възел.

Стъпка 4) Изтрийте текущия възел и освободете паметта му.

function searchAndDelete(head, searchItem):
  while head.next.next is not NULL and head.next.value != searchItem:
    head = head.next
  temp = head.next
  head.next = head.next.next
  free(temp)

Търсене и изтриване на възел от единично свързан списък

Търсене и изтриване на възел от единично свързан списък

Преминаване през едносвързан списък

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

Стъпка 1) Преминава се през всеки възел, докато се достигне NULL.

Стъпка 2) Отпечатайте стойността на текущия възел.

function traverse(head):
  while head is not NULL:
    print head.value
    head = head.next

Пример за единично свързан списък в C++

#include<iostream>
using namespace std;
struct Node{
  int data;
  struct Node *next;
};
void insertAtHead(Node* &head, int value){
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  if(head != NULL){
    newNode->next = head;
  }
  head = newNode;
  cout<<"Added "<<newNode->data<<" at the front"<<endl;
}
void insertEnd(Node* &head, int value){
  if(head == NULL){
    insertAtHead(head, value);
    return;
  }
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  Node *temp = head;
  while(temp->next != NULL){
    temp = temp->next;
  }
  temp->next = newNode;
  cout<<"Added "<<newNode->data<<" at the end"<<endl;
}
void searchAndDelete(Node **headPtr, int searchItem){
  Node *temp = NULL;
  if((*headPtr)->data == searchItem){
    temp = *headPtr;
    *headPtr = (*headPtr)->next;
    free(temp);
  } else {
    Node *currentNode = *headPtr;
    while(currentNode->next != NULL){
      if(currentNode->next->data == searchItem){
        temp = currentNode->next;
        currentNode->next = currentNode->next->next;
        free(temp);
        break;
      } else {
        currentNode = currentNode->next;
      }
    }
  }
  cout<<"Deleted Node\t"<<searchItem<<endl;
}
void insertAfter(Node* &headPtr, int searchItem, int value){
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  Node *head = headPtr;
  while(head->next != NULL && head->data != searchItem){
    head = head->next;
  }
  newNode->next = head->next;
  head->next = newNode;
  cout<<"Inserted "<<value<<" after node\t"<<searchItem<<endl;
}
void insertBefore(Node* &headPtr, int searchItem, int value){
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  Node *head = headPtr;
  while(head->next != NULL && head->next->data != searchItem){
    head = head->next;
  }
  newNode->next = head->next;
  head->next = newNode;
  cout<<"Inserted "<<value<<" before node\t"<<searchItem<<endl;
}
void traverse(Node *headPointer){
  Node* tempNode = headPointer;
  cout<<"Traversal from head:\t";
  while(tempNode != NULL){
    cout<<tempNode->data;
    if(tempNode->next)
      cout<<" --> ";
    tempNode = tempNode->next;
  }
  cout<<endl;
}
int main(){
  Node *head = NULL;
  insertAtHead(head, 5);
  insertAtHead(head, 6);
  insertAtHead(head, 7);
  insertEnd(head, 9);
  traverse(head);
  searchAndDelete(&head, 6);
  traverse(head);
  insertAfter(head, 7, 10);
  insertBefore(head, 9, 11);
  traverse(head);
}

Продукция

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

Пример за единично свързан списък в Python

class Node:
  def __init__(self, data=None, next=None):
    self.data = data
    self.next = next
class SinglyLinkedList:
  def __init__(self):
    self.head = None
  def insertAtHead(self, value):
    newNode = Node(data=value)
    if self.head is not None:
      newNode.next = self.head
    self.head = newNode
    print(f'Added {newNode.data} at the front.')
  def insertAtEnd(self, value):
    if self.head is None:
      self.insertAtHead(value)
      return
    newNode = Node(value)
    temp = self.head
    while temp.next is not None:
      temp = temp.next
    temp.next = newNode
    print(f'Added {newNode.data} at the end.')
  def searchAndDelete(self, searchItem):
    if self.head is None:
      return
    if self.head.data == searchItem:
      self.head = self.head.next
      print(f'Deleted node\t{searchItem}')
      return
    currentNode = self.head
    while currentNode.next is not None:
      if currentNode.next.data == searchItem:
        currentNode.next = currentNode.next.next
        print(f'Deleted node\t{searchItem}')
        return
      currentNode = currentNode.next
  def insertAfter(self, searchItem, value):
    newNode = Node(data=value)
    temp = self.head
    while temp.next is not None and temp.data != searchItem:
      temp = temp.next
    newNode.next = temp.next
    temp.next = newNode
    print(f'Inserted {value} after node\t{searchItem}')
  def insertBefore(self, searchItem, value):
    newNode = Node(data=value)
    temp = self.head
    while temp.next is not None and temp.next.data != searchItem:
      temp = temp.next
    newNode.next = temp.next
    temp.next = newNode
    print(f'Inserted {value} before node\t{searchItem}')
  def traverse(self):
    temp = self.head
    print("Traversing from head:\t", end="")
    while temp:
      print("{}\t".format(temp.data), end="")
      temp = temp.next
    print()
singlyLinkedList = SinglyLinkedList()
singlyLinkedList.insertAtHead(5)
singlyLinkedList.insertAtHead(6)
singlyLinkedList.insertAtHead(7)
singlyLinkedList.insertAtEnd(9)
singlyLinkedList.traverse()
singlyLinkedList.searchAndDelete(6)
singlyLinkedList.traverse()
singlyLinkedList.insertAfter(7, 10)
singlyLinkedList.insertBefore(9, 11)
singlyLinkedList.traverse()

Продукция

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

Сложност на единично свързан списък

Съществуват два вида сложност: времева сложност и пространствена сложност. Времевата сложност в най-лошия и средния случай са еднакви за едносвързан списък.

Времева сложност в най-добрия случай:

  • Вмъкването в началото може да се извърши за O(1). Не се изисква обхождане в списъка.
  • Търсенето и изтриването могат да се извършат за O(1), ако целевият елемент е в главния възел.

Средна времева сложност на случая:

  • Вмъкването в свързан списък отнема O(n), където n е общият брой елементи.
  • Търсенето и изтриването също могат да отнемат O(n), защото целевият елемент може да се намира навсякъде до опашния възел.

Пространствена сложност на едносвързан списък

Едносвързаният списък динамично разпределя памет. За да се съхранява n елементи, той разпределя n паметни единици. Така пространствената сложност е O(n).

Приложения на едносвързан списък

Едносвързаните списъци се появяват на много места, където обхождането само напред и динамичната памет са полезни:

  • Стекове и опашки: Базово хранилище за LIFO стекове и FIFO опашки, изградени от възли.
  • Верижно свързване на хеш таблици: Колизиите се разрешават чрез верижно свързване на записи в едносвързан списък за всяка кофа.
  • Списъци със съседни обекти: Разредените графи използват едносвързан списък от съседи за всеки връх.
  • Таблици със символи: Компилаторите и интерпретаторите свързват идентификаторите в едносвързан списък за всяка област.
  • Разпределители на памет: Разпределители от свободен списък track свободни блока като еднократно свързан списък.

Въпроси и Отговори

Едносвързаните списъци свързват верижни обучителни проби, мини-партиди и свободни блокове памет в рамките на AI рамки, което позволява динамични опашки за стрийминг на входни данни и канали за данни без заключване, които се мащабират според търсенето на модела.

Да. GitHub Copilot и GPT могат да създадат пълен едносвързан списък на C, C++, Java, Python или JavaСкрипт, включително вмъкване, изтриване, обръщане, откриване на цикли и модулни тестове.

Еднократно свързан списък има един следващ указател и се движи само напред. Двойно свързан списък има както следващ, така и предишен указател и се движи в двете посоки, но използва повече памет на възел.

Често срещаните приложения включват имплементации на стекове и опашки, вериги от хеш таблици, списъци със съседство за разредени графове, таблици със символи в компилатори, разпределители на свободни списъци и отмяна на историята в леки редактори.

Вмъкването или изтриването в началото е O(1). Вмъкването в края, търсенето, вмъкването в позиция и изтриването на специфичен възел струват O(n), защото се изисква обхождане от началото.

Свързаните списъци се увеличават и свиват по време на изпълнение, вмъкват или изтриват за O(1), след като позицията е известна, и никога не се нуждаят от непрекъсната памет. Масивите предлагат O(1) произволен достъп и по-добра локалност на кеша.

Разходете се по списъка с три указателя: prev, curr и next. На всяка стъпка запазете curr.next, насочете curr.next към prev и преместете prev и curr напред. Върнете prev като нов начален елемент.

Алгоритъмът на Флойд за костенурка и заек използва два указателя, движещи се с различна скорост. Ако някога се срещнат, списъкът съдържа цикъл. В противен случай бързият указател достига NULL и цикъл не съществува.

Обобщете тази публикация с: