Üksiklingitud loend andmestruktuurides

⚡ Nutikas kokkuvõte

Üksikult lingitud loend (Singly Linked List) on lineaarne, ühesuunaline andmestruktuur, kus iga sõlm salvestab andmeid ja ühe pointeri järgmisele sõlmele, seega liigub läbimine ainult otsast sabani ja mälu eraldatakse dünaamiliselt uute sõlmede lisamisel.

  • 🧩 Sõlme struktuur: Iga sõlm sisaldab ühte andmevälja ja ühte järgmine osuti järgmisele sõlmele; sabasõlme järgmine pointer on NULL.
  • 📦 Loend vs massiiv: Üksikult lingitud loendeid eelistatakse siis, kui elementide arv pole teada, juhuslikku juurdepääsu pole vaja ja loendi keskele lisamine on tavaline.
  • Lisamised: Sõlme saab lisada otsa, sabasse, sobiva sõlme järele või enne sobivat sõlme, kasutades järgmise pointeri ümberkirjutamisi.
  • Kustutused: Pea, saba või otsitud sõlme eemaldamine uuendab naaberpointereid ja vabastab vabanenud mälu, et vältida lekkeid.
  • 🔁 Läbiminek: Toetatud on ainult edasiliikumine, kuna eelmist pointerit pole, seega pole üksikult lingitud loendi tagasiliikumine võimalik.
  • 💻 C++ ja Python Code: Täielikud implementatsioonid näitavad sisestamise, kustutamise, otsimise ja läbimise rutiine käivitatava väljundiga.
  • 📊 Keerukus: Pea sisestamine või kustutamine on O(1); otsing ja muud sisestamised ja kustutamised on O(n); ruumi keerukus on O(n).

Üksiklingitud loend

Mis on üksikult lingitud loend?

Üksikult lingitud loend (Singly Linked List) on lineaarne ja ühesuunaline andmestruktuur, kus andmed salvestatakse sõlmedesse ja iga sõlm on lingi kaudu ühendatud järgmise sõlmega. Iga sõlm sisaldab andmevälja ja linki järgmise sõlmega. Üksikult lingitud loendeid saab läbida ainult ühes suunas, samas kui Topeltlingitud loend läbida saab mõlemas suunas.

Siin on üksikult lingitud loendi sõlme struktuur:

Lingitud loendi sõlme struktuur

Lingitud loendi sõlme struktuur

Miks kasutada lingitud loendit massiivi kohal?

Mitmed stsenaariumid eelistavad seotud loendit loendile Array:

  • Tundmatu arv elemente: Kui nõutav elementide arv pole kompileerimise ajal teada, jaotab lingitud loend mälu dünaamiliselt elementide lisamisel.
  • Juhuslik juurdepääs: Kui juhuslikku indekseeritud juurdepääsu pole vaja, on sobiv valik lingitud loend.
  • Sisestamine keskele: Massiivi keskele lisamine nõuab elementide nihutamist. Lingitud loend (linked list) võimaldab elementide lisamist mis tahes positsioonile, kirjutades ümber vaid mõned pointerid.

Operaüksikult lingitud nimekirja

Üksikult lingitud loend sobib hästi mälu dünaamiliseks eraldamiseks. See toetab lingitud loendi standardtoiminguid, st lisamist, kustutamist, otsimist, uuendamist, kahe loendi ühendamist ja läbimist.

Selles artiklis käsitletakse järgmisi toiminguid:

  • Sisestamine peas
  • Sisestamine sabas
  • Sisestamine pärast sõlme
  • Sisestamine enne sõlme
  • Kustutage peasõlm
  • Kustutage saba sõlm
  • Otsige ja kustutage sõlm
  • Lingitud loendi läbimine

Siin on näide nelja sõlmega lingitud loendist.

Näide üksikult lingitud loendist

Näide üksikult lingitud loendist

Lisamine üksikult lingitud loendi algusesse

See on lihtne toiming. Üldiselt tuntakse seda üksikult lingitud loendile (Singly Linked List) lisamisena. Luuakse uus sõlm ja asetatakse loendi algusesse.

Selle toimingu sooritamiseks järgige kahte olulist tingimust:

  1. Kui loend on tühi, saab äsja loodud sõlmest peasõlm ja selle järgmine pointer on NULL.
  2. Kui loend pole tühi, saab uuest sõlmest peasõlm ja selle järgmine pointer osutab eelmisele peasõlmele.

Siin on pseudokood sõlme lisamiseks lingitud loendi algusesse:

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

Sisestamine peas

Sisestamine peas

Lisamine üksikult lingitud loendi lõppu

Lingitud loendi lõppu sõlme lisamine sarnaneb loendi algusesse lisamisega. Liikuge sabasõlmeni ja seejärel suunake selle järgmine kursor uuele sõlmele. Kui pea on NULL, saab uuest sõlmest pea.

Step 1) Liigu läbi kuni järgmine Praeguse sõlme pointer muutub NULL-iks.

Step 2) Looge määratud väärtusega uus sõlm.

Step 3) Määrake uus sõlm sabasõlme järgmiseks sõlmeks.

Pseudokood üksikute loendite lõppu lisamiseks:

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

Sisestamine saba juures

Sisestamine saba juures

Lisamine pärast sõlme üksikult lingitud loendis

Sõlme järele lisamine koosneb kahest osast: sihtsõlme otsimine ja uue sõlme lisamine selle järele. Loendi läbimine kuni vaste leidmiseni ja uue sõlme lisamine.

Step 1) Liigutakse seni, kuni praeguse sõlme väärtus võrdub otsinguüksusega.

Step 2) Määrake uue sõlme järgmine osuti praeguse sõlme juurde järgmine osuti.

Step 3) Suuna praeguse sõlme järgmine pointer uuele sõlmele.

Pseudokood:

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

Sõlme lisamine üksikult lingitud loendisse sõlme järele

Sõlme lisamine sõlme järel üksikult lingitud loendisse

Lisamine enne sõlme üksikult lingitud loendis

See sarnaneb lisamisega pärast sõlme. Liigutakse seni, kuni järgmine sõlm vastab otsinguväärtusele, seejärel lisatakse uus sõlm enne seda.

Step 1) Liikuge seni, kuni järgmise sõlme väärtus võrdub otsinguüksusega.

Step 2) Loo uus sõlm ja määra selle järgmine osuti praeguse sõlme juurde järgmine.

Step 3) Suuna praeguse sõlme järgmine uue sõlme juurde.

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

Sõlme sisestamine üksikult lingitud loendi sõlme ette

Sõlme sisestamine üksikult lingitud loendis sõlme ette

Kustuta üksikult lingitud loendi pea

Parameetrina antakse päisviide. Peasõlm eemaldatakse ja järgmisest sõlmest saab uus peasõlm. Mälulekete vältimiseks tuleb kustutatud sõlme mälu vabastada.

Step 1) Määrake pea järgmine sõlm uueks peaks.

Step 2) Vabastage eelmise peasõlme eraldatud mälu.

Step 3) Tagasta uus peasõlm.

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

Lingitud loendi pea kustutamine

Lingitud loendi pea kustutamine

Kustuta üksikult lingitud loendi saba

Sabasõlme kustutamine sarnaneb peasõlme kustutamisega. Erinevus seisneb selles, et on vaja läbida loendi lõppu. Üksikult lingitud loendis on sõlm, mille järgmine pointer on NULL on sabasõlm.

Step 1) Liigu läbi kuni vahetult enne sabasõlme. Salvesta praegune sõlm.

Step 2) Vabastage järgmise sõlme (saba) mälu.

Step 3) Määra praeguse sõlme järgmise sõlme väärtuseks NULL.

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

Üksiklingitud loendi saba kustutamine

Üksiklingitud loendi saba kustutamine

Sõlme otsimine ja kustutamine üksikult lingitud loendist

See funktsioon täidab kahte ülesannet: otsimine ja kustutamine. Liigub loendis lõpuni. Kui leitakse sobiv sõlm, eemaldab see ja lingib eelmise sõlme uuesti. järgmine osuti.

Step 1) Liigu nimekirja lõpuni. Kontrolli, kas praegune sõlm võrdub otsingusõlmega.

Step 2) Kui vaste leitakse, salvestatakse pointer praegusele sõlmele.

Step 3) . järgmine Eelmise sõlme sõlmest saab praeguse sõlme järgmine sõlm.

Step 4) Kustuta praegune sõlm ja vabasta selle mälu.

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)

Otsige ja kustutage sõlm üksikult lingitud loendist

Otsige ja kustutage sõlm üksikult lingitud loendist

Läbida üksikult lingitud loend

Üksikult lingitud loend toetab läbimist ainult algusest lõpuni. Eelmisele sõlmele puudub pointer, seega pole tagasiliikumine võimalik. Iga sõlme külastatakse kordamööda, prindides selle väärtuse kuni NULL-ini.

Step 1) Läbi iga sõlme, kuni jõutakse NULL-ini.

Step 2) Printige praeguse sõlme väärtus.

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

Üksiklingitud loendi näide 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);
}

Väljund

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

Üksiklingitud loendi näide 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()

Väljund

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

Üksiklingitud loendi keerukus

Keerukust on kahte tüüpi: ajaline keerukus ja ruumiline keerukus. Halvima ja keskmise juhtumi ajaline keerukus on üksikult lingitud loendi puhul sama.

Parima stsenaariumi ajaline keerukus:

  • Loendi pähe lisamine on võimalik funktsioonis O(1). Loendi sees pole vaja läbida.
  • Otsingut ja kustutamist saab teha O(1)-s, kui sihtelement asub peasõlmes.

Juhtumi keskmine ajaline keerukus:

  • Lisamine lingitud loendisse võtab väärtuse O(n), kus n on elementide koguarv.
  • Otsing ja kustutamine võivad samuti võtta O(n), sest sihtelement võib asuda ükskõik kus kuni sabasõlmeni.

Üksikult seotud loendi ruumi keerukus

Üksikult lingitud loend (Singly Linked List) eraldab mälu dünaamiliselt. Salvestamiseks n elemente, see eraldab n mäluühikuid. Seega on ruumi keerukus O(n).

Üksikult seotud loendi rakendused

Üksikult lingitud loendid esinevad paljudes kohtades, kus ainult edasiliikumine ja dünaamiline mälu on kasulikud:

  • Pinud ja järjekorrad: Sõlmedest ehitatud LIFO-pinude ja FIFO-järjekordade alussalvestus.
  • Räsitabeli aheldamine: Kokkupõrked lahendatakse kirjete aheldamise teel iga ämbri kohta üksikult lingitud loendisse.
  • Kõrvalkohtade loendid: Hõredates graafides kasutatakse iga tipu jaoks üksikult lingitud naabrite loendit.
  • Sümbolite tabelid: Kompilaatorid ja interpreteerijad aheldavad identifikaatorid ühekordselt lingitud loendiks iga ulatuse kohta.
  • Mälu jaoturid: Vaba nimekirjaga jaoturid track vaba plokki üheselt lingitud loendina.

KKK

Üksikult lingitud loendid aheldavad treeningnäidiseid, minipartiisid ja vabu mäluplokke tehisintellekti raamistikes, võimaldades dünaamilisi järjekordi voogedastussisendite jaoks ja lukuvabasid andmekanaleid, mis skaleeruvad vastavalt mudeli nõudlusele.

Jah. GitHub Copilot ja GPT suudavad C-s luua täieliku üksikult lingitud loendi. C++, Java, Pythonvõi JavaSkript, sealhulgas sisestamine, kustutamine, tagasipööramine, tsükli tuvastamine ja ühiktestid.

Ühekordselt lingitud loendil on üks järgmise pointer ja see liigub ainult edasi. Kahekordselt lingitud loendil on nii järgmise kui ka eelmise pointer ning see liigub mõlemas suunas, kuid kasutab sõlme kohta rohkem mälu.

Levinumad kasutusalad hõlmavad pinu ja järjekorra implementatsiooni, räsitabelite aheldamist, hõredate graafikute külgnevusloendeid, sümbolitabeleid kompilaatorites, vabade loendi eraldajaid ja tagasivõtmise ajalugu kergetes redaktorites.

Lisamine või kustutamine tippu on O(1). Lisamine sabasse, otsing, lisamine positsiooni ja kindla sõlme kustutamine maksavad kõik O(n), kuna läbimine on vajalik tippu.

Lingitud loendid kasvavad ja kahanevad käitusajal, lisavad või kustutavad O(1)-sse, kui positsioon on teada, ning ei vaja kunagi külgnevat mälu. Massiivid pakuvad O(1) suvapöördust ja paremat vahemälu lokaalsust.

Käi läbi loend kolme kursoriga – prev, curr ja next. Igal sammul salvesta curr.next, osuta curr.next kursorile prev ning nihuta prev ja curr ettepoole. Tagasta prev uue loendipeana.

Floydi kilpkonna ja jänese algoritm kasutab kahte erineva kiirusega liikuvat pointerit. Kui need kunagi kohtuvad, sisaldab loend tsüklit. Vastasel juhul jõuab kiire pointer väärtuseni NULL ja tsüklit ei eksisteeri.

Võta see postitus kokku järgmiselt: