Gegevensstructuren met JavaScript een eenmalig gekoppelde lijst en een dubbel gelinkte lijst

Wat je gaat creëren

Twee van de meest onderwezen datastructuren in de informatica zijn de enkelvoudig gekoppelde lijst en dubbel gelinkte lijst. 

Toen ik deze datastructuren leerde, vroeg ik mijn collega's naar analogieën om ze te conceptualiseren. Wat ik hoorde waren verschillende voorbeelden, zoals een lijst met boodschappen en een trein. Deze analogieën, zoals ik uiteindelijk leerde, waren onnauwkeurig. Een boodschappenlijstje was meer analoog een wachtrij; een trein was meer analoog aan een array. 

Naarmate er meer tijd verstreek, ontdekte ik uiteindelijk een analogie die een enkelvoudig gelinkte lijst en een dubbel gelinkte lijst nauwkeurig beschreef: een speurtocht. Als je nieuwsgierig bent naar de relatie tussen een speurtocht en een gekoppelde lijst, lees dan hieronder voor het antwoord!

Een singly-gelinkte lijst

In de informatica is een enkelvoudig gekoppelde lijst een datastructuur die een reeks gekoppelde knooppunten bevat. Elk knooppunt bevat op zijn beurt gegevens en een aanwijzer, die naar een ander knooppunt kunnen wijzen.

Knopen van een enkelvoudig gelinkte lijst lijken erg op stappen in een speurtocht. Elke stap bevat een bericht (bijvoorbeeld "U bent in Frankrijk aangekomen") en wijst naar de volgende stap (bijvoorbeeld "Deze breedtegraad- en lengtegraadcoördinaten bezoeken"). Wanneer we deze afzonderlijke stappen gaan sequencen om een ​​reeks stappen te vormen, creëren we een speurtocht.  

Nu we een conceptueel model hebben van een enkelvoudig gekoppelde lijst, laten we de werking van een enkelvoudig gekoppelde lijst verkennen.

Bewerkingen van een enkelvoudig gekoppelde lijst

Omdat een enkelvoudig gekoppelde lijst knooppunten bevat, die een afzonderlijke constructor kunnen zijn van een enkelvoudig gekoppelde lijst, schetsen we de werking van beide constructeurs: Knooppunt en SinglyList.

Knooppunt

  • gegevens slaat een waarde op.
  • volgende wijst naar het volgende knooppunt in de lijst.

SinglyList

  • _lengte haalt het aantal knooppunten in een lijst op.
  • hoofd wijst een knooppunt toe als de kop van een lijst.
  • waarde toevoegen) voegt een knooppunt toe aan een lijst.
  • searchNodeAt (positie) zoekt naar een knoop op de n-positie in onze lijst.
  • Verwijder (positie) verwijdert een knooppunt uit een lijst.

Implementatie van een singly-linked lijst 

Voor onze implementatie zullen we eerst een constructor met de naam definiëren Knooppunt en dan een constructor genaamd SinglyList

Elke instantie van Knooppunt heeft de mogelijkheid nodig om gegevens op te slaan en de mogelijkheid om naar een ander knooppunt te wijzen. Om deze functionaliteit toe te voegen, zullen we twee eigenschappen creëren: gegevens en volgende, respectievelijk. 

function Node (data) this.data = data; this.next = null; 

Vervolgens moeten we definiëren SinglyList:

function SinglyList () this._length = 0; this.head = null; 

Elke instantie van SinglyList zal twee eigenschappen hebben: _lengte en hoofd. De eerste krijgt het aantal knooppunten in een lijst toegewezen; de laatste wijst naar de kop van de lijst - het knooppunt vooraan in de lijst. Sinds elke nieuwe instantie van SinglyList bevat geen knooppunt, de standaardwaarde van hoofd is nul en de standaardwaarde van _lengte is 0.

Methoden van een enkelvoudig gekoppelde lijst

We moeten methoden definiëren die een knooppunt uit een lijst kunnen toevoegen, doorzoeken en verwijderen. Laten we beginnen met het toevoegen van een knooppunt. 

1 van 3: waarde toevoegen)

Geweldig, laten we nu de functionaliteit implementeren om knooppunten aan een lijst toe te voegen. 

SinglyList.prototype.add = function (value) var node = new Node (value), currentNode = this.head; // 1e use-case: een lege lijst als (! CurrentNode) this.head = node; this._length ++; return node;  // 2e use-case: een niet-lege lijst while (currentNode.next) currentNode = currentNode.next;  currentNode.next = knooppunt; this._length ++; return node; ;

Het toevoegen van een knoop aan onze lijst omvat vele stappen. Laten we beginnen vanaf het begin van onze methode. We gebruiken het argument van waarde toevoegen) om een ​​nieuw exemplaar van een te maken Knooppunt, die is toegewezen aan een variabele met de naam knooppunt. We verklaren ook een variabele met de naam currentNode en initialiseer het voor de _hoofd van onze lijst. Als er geen knooppunten in de lijst staan, is de waarde van hoofd is nul

Na dit punt in onze code behandelen we twee use-cases. 

De eerste use case is het toevoegen van een knooppunt aan een lege lijst. Als hoofd wijst niet naar een knooppunt en wijst het toe knooppunt als het hoofd van onze lijst, verhoog de lengte van onze lijst met één, en keer terug knooppunt

De tweede use case is het toevoegen van een knooppunt aan een niet-lege lijst. We gaan de terwijl loop en tijdens elke iteratie evalueren we of currentNode.next wijst naar een ander knooppunt. (Tijdens de eerste iteratie, currentNode wijst altijd naar de kop van een lijst.) 

Als het antwoord nee is, wijzen we het toe knooppunt naar currentNode.next En terugkomen knooppunt

Als het antwoord ja is, komen we in het lichaam van de terwijl lus. In het lichaam, we opnieuw toewijzen currentNode naar currentNode.next. Dit proces wordt herhaald tot currentNode.next wijst niet langer naar een ander knooppunt. Met andere woorden, currentNode wijst naar het laatste knooppunt van onze lijst.

De terwijl lus breekt. Eindelijk, we wijzen knooppunt naar currentNode.next, we verhogen _lengte door één, en dan keren we terug knooppunt

2 van 3: searchNodeAt (positie)

We kunnen nu knooppunten aan onze lijst toevoegen, maar we kunnen niet naar knooppunten op specifieke posities in onze lijst zoeken. Laten we deze functionaliteit toevoegen en een methode met de naam maken searchNodeAt (positie), welke een argument genaamd accepteert positie. Het argument is naar verwachting een geheel getal dat een node op de n-positie in onze lijst aangeeft.  

SinglyList.prototype.searchNodeAt = function (positie) var currentNode = this.head, length = this._length, count = 1, message = failure: 'Failure: non-existent node in this list.'; // 1e use-case: een ongeldige positie als (lengte === 0 || positie < 1 || position > lengte) gooi nieuwe fout (message.failure);  // 2e use-case: een geldige positie tijdens (tel < position)  currentNode = currentNode.next; count++;  return currentNode; ;

De als statementcontroles voor de eerste use-case: een ongeldige positie wordt doorgegeven als een argument.

Als de index is doorgegeven aan searchNodeAt (positie) is geldig, dan bereiken we de tweede use case-the terwijl lus. Tijdens elke iteratie van de terwijl lus, currentNode-waar eerst naar wijst hoofd-wordt opnieuw toegewezen aan het volgende knooppunt in de lijst. Deze iteratie gaat door totdat de telling gelijk is aan de positie. 

Wanneer de lus eindelijk breekt, currentNode wordt teruggestuurd. 

3 van 3: Verwijder (positie)

De uiteindelijke methode die we zullen maken, wordt genoemd Verwijder (positie)

SinglyList.prototype.remove = function (positie) var currentNode = this.head, length = this._length, count = 0, message = failure: 'Failure: non-existent node in this list.', BeforeNodeToDelete = null , nodeToDelete = null, deletedNode = null; // 1e use-case: een ongeldige positie als (positie < 0 || position > lengte) gooi nieuwe fout (message.failure);  // 2e use-case: het eerste knooppunt wordt verwijderd als (positie === 1) this.head = currentNode.next; deletedNode = currentNode; currentNode = null; this._length--; return deletedNode;  // 3e use-case: elk ander knooppunt is verwijderd terwijl (tel < position)  beforeNodeToDelete = currentNode; nodeToDelete = currentNode.next; count++;  beforeNodeToDelete.next = nodeToDelete.next; deletedNode = nodeToDelete; nodeToDelete = null; this._length--; return deletedNode; ;

Onze implementatie van Verwijder (positie) omvat drie use-cases: 

  1. Een ongeldige positie wordt doorgegeven als een argument.
  2. Een positie van één (hoofd van een lijst) wordt doorgegeven als een argument.
  3. Een bestaande positie (niet de eerste positie) wordt als argument doorgegeven. 

De eerste en tweede use-cases zijn de eenvoudigste om te hanteren. Met betrekking tot het eerste scenario wordt een fout gegenereerd als de lijst leeg is of als een niet-bestaande positie wordt doorgegeven. 

De tweede use case behandelt de verwijdering van het eerste knooppunt in de lijst, wat ook het geval is hoofd. Als dit het geval is, gebeurt de volgende logica:

  1. hoofd is opnieuw toegewezen aan currentNode.next.
  2. deletedNode wijst naar currentNode
  3. currentNode is opnieuw toegewezen aan nul
  4. decrement _lengte van onze lijst met één.
  5. deletedNode wordt teruggestuurd. 

Het derde scenario is het moeilijkst te begrijpen. De complexiteit komt voort uit de noodzaak om twee knooppunten te volgen tijdens elke iteratie van a terwijl lus. Tijdens elke iteratie van onze lus volgen we zowel het knooppunt vóór het knooppunt dat moet worden verwijderd en het knooppunt dat moet worden verwijderd. Wanneer onze lus uiteindelijk het knooppunt bereikt op de positie die we willen verwijderen, eindigt de lus. 

Op dit punt houden we verwijzingen naar drie knooppunten: beforeNodeToDeletenodeToDelete, en deletedNode. Voor het verwijderen nodeToDelete, we moeten de waarde ervan toewijzen volgende naar de volgende waarde van beforeNodeToDelete. Als het doel van deze stap onduidelijk lijkt, herinner jezelf eraan dat we een lijst met gekoppelde knooppunten hebben; alleen het verwijderen van een knoop breekt de link van het eerste knooppunt van de lijst naar het laatste knooppunt van de lijst. 

Vervolgens wijzen we toe deletedNode naar nodeToDelete. Vervolgens stellen we de waarde in van nodeToDelete naar nul, verlaag de lengte van de lijst met één, en keer terug deletedNode

Volledige implementatie van een singly-linked lijst

De volledige implementatie van onze lijst is hier: 

function Node (data) this.data = data; this.next = null;  function SinglyList () this._length = 0; this.head = null;  SinglyList.prototype.add = function (value) var node = new Node (value), currentNode = this.head; // 1e use-case: een lege lijst als (! CurrentNode) this.head = node; this._length ++; return node;  // 2e use-case: een niet-lege lijst while (currentNode.next) currentNode = currentNode.next;  currentNode.next = knooppunt; this._length ++; return node; ; SinglyList.prototype.searchNodeAt = function (positie) var currentNode = this.head, length = this._length, count = 1, message = failure: 'Failure: non-existent node in this list.'; // 1e use-case: een ongeldige positie als (lengte === 0 || positie < 1 || position > lengte) gooi nieuwe fout (message.failure);  // 2e use-case: een geldige positie tijdens (tel < position)  currentNode = currentNode.next; count++;  return currentNode; ; SinglyList.prototype.remove = function(position)  var currentNode = this.head, length = this._length, count = 0, message = failure: 'Failure: non-existent node in this list.', beforeNodeToDelete = null, nodeToDelete = null, deletedNode = null; // 1st use-case: an invalid position if (position < 0 || position > lengte) gooi nieuwe fout (message.failure);  // 2e use-case: het eerste knooppunt wordt verwijderd als (positie === 1) this.head = currentNode.next; deletedNode = currentNode; currentNode = null; this._length--; return deletedNode;  // 3e use-case: elk ander knooppunt is verwijderd terwijl (tel < position)  beforeNodeToDelete = currentNode; nodeToDelete = currentNode.next; count++;  beforeNodeToDelete.next = nodeToDelete.next; deletedNode = nodeToDelete; nodeToDelete = null; this._length--; return deletedNode; ;

Van alleenstaand tot dubbel

Geweldig, onze implementatie van een enkelvoudig gekoppelde lijst is voltooid. We kunnen nu een gegevensstructuur gebruiken die knooppunten in een lijst toevoegt, verwijdert en doorzoekt die niet-aaneengesloten ruimte in het geheugen innemen. 

Op dit moment beginnen al onze bewerkingen vanaf het begin van een lijst en lopen ze naar het einde van een lijst. Met andere woorden, ze zijn unidirectioneel. 

Er kunnen gevallen zijn waarin we willen dat onze activiteiten bidirectioneel zijn. Als u deze gebruikssituatie hebt overwogen, heeft u zojuist een dubbel gelinkte lijst beschreven.

Een dubbel gelinkte lijst

Een dubbel gelinkte lijst neemt alle functionaliteit van een enkelvoudig gekoppelde lijst en breidt deze uit voor bidirectionele verplaatsing in een lijst. We kunnen met andere woorden een gekoppelde lijst doorlopen van het eerste knooppunt in de lijst naar het laatste knooppunt in de lijst; en we kunnen van het laatste knooppunt in de lijst doorlopen naar het eerste knooppunt in de lijst. 

In deze sectie zullen we onze aandacht primair blijven richten op de verschillen tussen een dubbel gelinkte lijst en een enkelvoudig gekoppelde lijst. 

Bewerkingen van een dubbel gelinkte lijst 

Onze lijst bevat twee constructeurs: Knooppunt en DoublyList. Laten we hun activiteiten schetsen.

Knooppunt 

  • gegevens slaat een waarde op.
  • volgende wijst naar het volgende knooppunt in de lijst.
  • voorgaand wijst naar het vorige knooppunt in de lijst.

DoublyList

  • _lengte haalt het aantal knooppunten in een lijst op.
  • hoofd wijst een knooppunt toe als de kop van een lijst.
  • staart wijst een knoop toe als de staart van een lijst.
  • waarde toevoegen) voegt een knooppunt toe aan een lijst.
  • searchNodeAt (positie) zoekt naar een knoop op de n-positie in onze lijst.
  • Verwijder (positie) verwijdert een knooppunt uit een lijst.

Implementatie van een dubbel gelinkte lijst 

Laat een code schrijven! 

Voor onze implementatie maken we een constructor genaamd Knooppunt:

function Node (waarde) this.data = value; this.previous = null; this.next = null; 

Om de tweerichtingsbeweging van een dubbel gelinkte lijst te maken, hebben we eigenschappen nodig die in beide richtingen van een lijst wijzen. Deze eigenschappen zijn genoemd voorgaand en volgende

Vervolgens moeten we een DoublyList en voeg drie eigenschappen toe: _lengte, hoofd, en staart. In tegenstelling tot een enkelvoudig gekoppelde lijst, bevat een dubbel gelinkte lijst een verwijzing naar zowel het begin van een lijst als het einde van een lijst. Sinds elke instantie van een DoublyList wordt geïnstantieerd zonder knooppunten, de standaardwaarden van hoofd en staart zijn ingesteld op nul.

function DoublyList () this._length = 0; this.head = null; this.tail = null; 

Methoden van een dubbel gelinkte lijst

We gaan nu de volgende methoden verkennen: waarde toevoegen), Verwijder (positie), en searchNodeAt (positie). Al deze methoden werden gebruikt voor een enkelvoudig gekoppelde lijst; ze moeten echter wel herschreven worden voor beweging in twee richtingen. 

1 van 3: waarde toevoegen)

DoublyList.prototype.add = function (value) var node = new Node (value); if (this._length) this.tail.next = node; node.previous = this.tail; this.tail = knooppunt;  else this.head = node; this.tail = knooppunt;  this._length ++; return node; ;

In deze methode hebben we twee scenario's. Ten eerste, als een lijst leeg is, wijs dan toe aan zijn hoofd en staart het knooppunt dat wordt toegevoegd. Ten tweede, als de lijst knooppunten bevat, zoek dan de staart van de lijst en wijs deze toe tail.next het knooppunt dat wordt toegevoegd; evenzo moeten we de nieuwe staart configureren voor bidirectionele beweging. We moeten met andere woorden instellen, tail.previous naar de originele staart. 

2 van 3: searchNodeAt (positie)

De implementatie van searchNodeAt (positie) is identiek aan een enkelvoudig gekoppelde lijst. Als je vergeten bent hoe je het moet implementeren, heb ik het hieronder toegevoegd: 

DoublyList.prototype.searchNodeAt = function (positie) var currentNode = this.head, length = this._length, count = 1, message = failure: 'Failure: non-existent node in this list.'; // 1e use-case: een ongeldige positie als (lengte === 0 || positie < 1 || position > lengte) gooi nieuwe fout (message.failure);  // 2e use-case: een geldige positie tijdens (tel < position)  currentNode = currentNode.next; count++;  return currentNode; ;

3 van 3: Verwijder (positie)

Deze methode zal het meest uitdagend zijn om te begrijpen. Ik zal de code weergeven en het dan uitleggen. 

DoublyList.prototype.remove = function (positie) var currentNode = this.head, length = this._length, count = 1, message = failure: 'Failure: non-existent node in this list.', BeforeNodeToDelete = null , nodeToDelete = null, deletedNode = null; // 1e use-case: een ongeldige positie als (lengte === 0 || positie < 1 || position > lengte) gooi nieuwe fout (message.failure);  // 2e use-case: het eerste knooppunt wordt verwijderd als (positie === 1) this.head = currentNode.next; // 2e use-case: er is een tweede knooppunt als (! This.head) this.head.previous = null; // 2e use-case: er is geen tweede node else this.tail = null;  // 3e use-case: het laatste knooppunt is verwijderd else if (position === this._length) this.tail = this.tail.previous; this.tail.next = null; // 4e use-case: een middelste knooppunt is verwijderd else while (count < position)  currentNode = currentNode.next; count++;  beforeNodeToDelete = currentNode.previous; nodeToDelete = currentNode; afterNodeToDelete = currentNode.next; beforeNodeToDelete.next = afterNodeToDelete; afterNodeToDelete.previous = beforeNodeToDelete; deletedNode = nodeToDelete; nodeToDelete = null;  this._length--; return message.success; ;

Verwijder (positie) behandelt vier use-cases: 

  1. De positie die wordt doorgegeven als een argument van Verwijder (positie) bestaat niet. In dit geval gooien we een fout. 
  2. De positie die wordt doorgegeven als een argument van Verwijder (positie) is het eerste knooppunt (hoofd) van een lijst. Als dit het geval is, zullen we toewijzen deletedNode naar hoofd en vervolgens opnieuw toewijzen hoofd naar het volgende knooppunt in de lijst. Op dit moment moeten we overwegen of onze lijst meer dan één knoop heeft. Als het antwoord nee is, hoofd wordt toegewezen aan nul en we zullen de als onderdeel van onze if-else uitspraak. In het lichaam van als, we moeten ook gaan staart naar nul-met andere woorden, we keren terug naar de oorspronkelijke staat van een lege dubbel gelinkte lijst. Als we het eerste knooppunt in een lijst verwijderen en we meer dan één knooppunt in onze lijst hebben, voeren we het anders sectie van onze if-else uitspraak. In dit geval moeten we het juiste instellen voorgaand eigendom van hoofd naar nul-er zijn geen knooppunten voor het hoofd van een lijst.  
  3. De positie die wordt doorgegeven als een argument van Verwijder (positie) is de staart van een lijst. Eerste, deletedNode is toegewezen aan staart. Tweede, staart wordt opnieuw toegewezen aan het knooppunt voorafgaand aan de staart. Ten derde heeft de nieuwe staart er geen knoop achter en heeft hij de waarde ervan nodig volgende worden ingesteld op nul
  4. Er gebeurt hier veel, dus ik zal me meer concentreren op de logica dan op elke regel van de code. We breken onze terwijl eenmaal lus currentNode wijst naar het knooppunt op de positie die als argument wordt doorgegeven aan Verwijder (positie). Op dit moment wordt de waarde van opnieuw toegewezen beforeNodeToDelete.next naar het knooppunt erna nodeToDelete en omgekeerd, we herwaarderen de waarde van afterNodeToDelete.previous naar het knooppunt ervoor nodeToDelete. Met andere woorden, we verwijderen verwijzingen naar het verwijderde knooppunt en wijzen deze opnieuw toe aan de juiste knooppunten. Vervolgens wijzen we toe deletedNode naar nodeToDelete. Eindelijk, we wijzen nodeToDelete naar nul.   

Ten slotte verlagen we de lengte van onze lijst en keren we terug deletedNode

Volledige implementatie van een dubbel gelinkte lijst

Dit is de volledige implementatie: 

function Node (waarde) this.data = value; this.previous = null; this.next = null;  function DoublyList () this._length = 0; this.head = null; this.tail = null;  DoublyList.prototype.add = function (value) var node = new Node (value); if (this._length) this.tail.next = node; node.previous = this.tail; this.tail = knooppunt;  else this.head = node; this.tail = knooppunt;  this._length ++; return node; ; DoublyList.prototype.searchNodeAt = function (positie) var currentNode = this.head, length = this._length, count = 1, message = failure: 'Failure: non-existent node in this list.'; // 1e use-case: een ongeldige positie als (lengte === 0 || positie < 1 || position > lengte) gooi nieuwe fout (message.failure);  // 2e use-case: een geldige positie tijdens (tel < position)  currentNode = currentNode.next; count++;  return currentNode; ; DoublyList.prototype.remove = function(position)  var currentNode = this.head, length = this._length, count = 1, message = failure: 'Failure: non-existent node in this list.', beforeNodeToDelete = null, nodeToDelete = null, deletedNode = null; // 1st use-case: an invalid position if (length === 0 || position < 1 || position > lengte) gooi nieuwe fout (message.failure);  // 2e use-case: het eerste knooppunt wordt verwijderd als (positie === 1) this.head = currentNode.next; // 2e use-case: er is een tweede knooppunt als (! This.head) this.head.previous = null; // 2e use-case: er is geen tweede node else this.tail = null;  // 3e use-case: het laatste knooppunt is verwijderd else if (position === this._length) this.tail = this.tail.previous; this.tail.next = null; // 4e use-case: een middelste knooppunt is verwijderd else while (count < position)  currentNode = currentNode.next; count++;  beforeNodeToDelete = currentNode.previous; nodeToDelete = currentNode; afterNodeToDelete = currentNode.next; beforeNodeToDelete.next = afterNodeToDelete; afterNodeToDelete.previous = beforeNodeToDelete; deletedNode = nodeToDelete; nodeToDelete = null;  this._length--; return message.success; ;

Conclusie

We hebben veel informatie in dit artikel behandeld. Als iets daarvan verwarrend lijkt, lees het dan opnieuw en experimenteer met de code. Als het uiteindelijk logisch voor je is, voel je je trots. U hebt zojuist de mysteries blootgelegd van zowel een afzonderlijk gelinkte lijst als een dubbel gelinkte lijst. U kunt deze gegevensstructuren toevoegen aan uw coderingstoolgordel!