Datastrukturer med JavaScript Singly-Linked List og Doubly-Linked List

Hva du skal skape

To av de mest vanlige datastrukturene i datavitenskap er den enkeltstående listen og dobbeltkoblede listen. 

Da jeg ble lært disse datastrukturene, spurte jeg mine jevnaldrende for analogier å konseptualisere dem. Det jeg hørte var flere eksempler, for eksempel en liste over dagligvarer og et tog. Disse analogiene, som jeg til slutt lærte, var unøyaktige. En dagligvareliste var mer analog en kø; et tog var mer analogt med en matrise. 

Etter hvert som det var mer tid, oppdaget jeg til slutt en analogi som nøyaktig beskrev en enkeltkoblet liste og en dobbeltkoblet liste: en scavenger jakt. Hvis du er nysgjerrig på forholdet mellom en scavengerjakt og en koblet liste, les så nedenfor for svaret!

En enkeltstående liste

I datavitenskap er en enkeltlinket liste en datastruktur som har en sekvens av knutte noder. Hver knutepunkt inneholder i sin tur data og en peker, som kan peke til en annen knutepunkt.

Noder av en enkeltkoblet liste ligner veldig på trinn i en scavenger jakt. Hvert trinn inneholder en melding (for eksempel "Du har nådd Frankrike") og pekere til neste trinn (for eksempel "Besøk disse breddegrad og lengdegradskoordinater"). Når vi starter sekvensering disse individuelle trinnene for å danne en sekvens av trinn, oppretter vi en scavenger jakt.  

Nå som vi har en konseptuell modell av en enkeltkoblet liste, la oss utforske operasjonene i en enkeltkoblet liste.

Operasjoner av en enkeltstående liste

Siden en enkeltkoblet liste inneholder noder, som kan være en separat konstruktør fra en enkeltkoblet liste, skisserer vi operasjonene til begge konstruktørene: node og SinglyList.

node

  • data lagrer en verdi.
  • neste peker til neste knutepunkt i listen.

SinglyList

  • _lengde henter antall noder i en liste.
  • hode tilordner en node som leder av en liste.
  • Legg til verdi) legger til en node i en liste.
  • searchNodeAt (stilling) søker etter en node på n-posisjon i vår liste.
  • fjerne (stilling) fjerner en node fra en liste.

Implementering av en enkeltstående liste 

For implementeringen skal vi først definere en konstruktør som heter node og deretter en konstruktør oppkalt SinglyList

Hver forekomst av node trenger evnen til å lagre data og evnen til å peke til en annen knutepunkt. For å legge til denne funksjonaliteten, oppretter vi to egenskaper: data og neste, henholdsvis. 

funksjonsnode (data) this.data = data; this.next = null; 

Deretter må vi definere SinglyList:

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

Hver forekomst av SinglyList vil ha to egenskaper: _lengde og hode. Den tidligere er tilordnet antall noder i en liste; sistnevnte peker til lederen av listen - noden på forsiden av listen. Siden hver ny forekomst av SinglyList inneholder ikke en node, standardverdien for hode er null og standardverdien for _lengde er 0.

Metoder for en enkeltstående liste

Vi må definere metoder som kan legge til, søke og fjerne en knutepunkt fra en liste. La oss begynne med å legge til en node. 

1 av 3: Legg til verdi)

Awesome, la oss nå implementere funksjonaliteten for å legge til noder på en liste. 

SinglyList.prototype.add = funksjon (verdi) var node = ny Node (verdi), currentNode = this.head; // Første brukstilstand: En tom liste hvis (! CurrentNode) this.head = node; this._length ++; returknutepunkt;  // 2. brukskasse: en ikke-tom liste mens (currentNode.next) currentNode = currentNode.next;  currentNode.next = node; this._length ++; returknutepunkt; ;

Å legge til en node i vår liste innebærer mange trinn. La oss starte fra begynnelsen av vår metode. Vi bruker argumentet til Legg til verdi) å opprette en ny forekomst av a node, som er tildelt en navngitt variabel node. Vi erklærer også en variabel som heter currentNode og initialiser den til _hode av vår liste. Hvis det ikke er noen noder i listen, så verdien av hode er null

Etter dette punktet i koden håndterer vi to brukstilfeller. 

Første brukstilfelle vurderer å legge til en node i en tom liste. Hvis hode peker ikke på en node, og tildeler deretter node som leder av listen vår, øke lengden på listen vår, og returner node

Den andre brukssaken vurderer å legge til en node i en ikke-tom liste. Vi går inn i samtidig som loop, og under hver iterasjon vurderer vi om currentNode.next peker til en annen knutepunkt. (Under den første iterasjonen, currentNode peker alltid på hodet på en liste.) 

Hvis svaret er nei, tildeler vi node til currentNode.next og returnere node

Hvis svaret er ja, går vi inn i legemet samtidig som sløyfe. Innenfor kroppen overfører vi igjen currentNode til currentNode.next. Denne prosessen gjentas til currentNode.next peker ikke lenger på en annen knutepunkt. Med andre ord, currentNode peker til siste knutepunkt i vår liste.

De samtidig som sløyfe Til slutt tildeler vi node til currentNode.next, vi øker _lengde av en, og så kommer vi tilbake node

2 av 3: searchNodeAt (stilling)

Vi kan nå legge til noder på vår liste, men vi kan ikke søke etter noder på bestemte stillinger i vår liste. La oss legge til denne funksjonaliteten og opprette en metode som heter searchNodeAt (stilling), som aksepterer et argument som heter stilling. Argumentet forventes å være et heltall som indikerer en node på n-posisjon i vår liste.  

SinglyList.prototype.searchNodeAt = funksjon (posisjon) var currentNode = this.head, lengde = this.length, count = 1, message = failure: 'Feil: ikke-eksisterende node i denne listen.'; // Første brukstilstand: En ugyldig stilling hvis (lengde === 0 || posisjon < 1 || position > lengde) kaste ny feil (message.failure);  // 2. brukskasse: en gyldig posisjon mens (telle < position)  currentNode = currentNode.next; count++;  return currentNode; ;

De hvis setningskontroll for første brukstilfelle: En ugyldig stilling er bestått som et argument.

Hvis indeksen passerte til searchNodeAt (stilling) er gyldig, da kommer vi til den andre brukstilfeller-den samtidig som sløyfe. Under hver iterasjon av samtidig som sløyfe, currentNode-som først peker på hode-er tilordnet til neste knutepunkt i listen. Denne iterasjonen fortsetter til tellingen er lik posisjonen. 

Når sløyfen endelig bryter, currentNode returneres. 

3 av 3: fjerne (stilling)

Den endelige metoden vi skal opprette, heter fjerne (stilling)

SinglyList.prototype.remove = funksjon (posisjon) var currentNode = this.head, lengde = this._length, count = 0, message = feil: 'Feil: ikke-eksisterende node i denne listen.', BeforeNodeToDelete = null , nodeToDelete = null, deletedNode = null; // Første brukstilstand: En ugyldig stilling hvis (posisjon < 0 || position > lengde) kaste ny feil (message.failure);  // 2. brukskasse: den første noden er fjernet hvis (posisjon === 1) this.head = currentNode.next; deletedNode = currentNode; currentNode = null; this._length--; returner deletedNode;  // 3. brukskode: en hvilken som helst annen knutepunkt blir fjernet mens (telle < position)  beforeNodeToDelete = currentNode; nodeToDelete = currentNode.next; count++;  beforeNodeToDelete.next = nodeToDelete.next; deletedNode = nodeToDelete; nodeToDelete = null; this._length--; return deletedNode; ;

Vår implementering av fjerne (stilling) innebærer tre brukstilfeller: 

  1. En ugyldig posisjon er bestått som et argument.
  2. En posisjon av en (hode av en liste) er gått som et argument.
  3. En eksisterende posisjon (ikke den første posisjonen) er bestått som et argument. 

Det første og andre brukssaker er det enkleste å håndtere. Når det gjelder det første scenariet, hvis en liste er tom eller en ikke-eksisterende posisjon er passert, blir en feil kastet. 

Den andre brukssaken håndterer fjerningen av den første noden i listen, som også er hode. Hvis dette er tilfelle, skjer følgende logikk:

  1. hode er tilordnet til currentNode.next.
  2. deletedNode poeng til currentNode
  3. currentNode er tilordnet til null
  4. minsk _lengde av vår liste av en.
  5. deletedNode returneres. 

Det tredje scenariet er det vanskeligste å forstå. Kompleksiteten stammer fra nødvendigheten av å spore to noder under hver iterasjon av a samtidig som sløyfe. Under hver iterasjon av vår løkke sporer vi både noden før noden skal slettes og noden som skal slettes. Når sløyfen til slutt når noden på stillingen vi vil fjerne, slår sløyfen seg. 

På dette punktet holder vi referanser til tre noder: beforeNodeToDeletenodeToDelete, og deletedNode. Før du sletter nodeToDelete, Vi må tildele verdien av neste til neste verdi av beforeNodeToDelete. Hvis formålet med dette trinnet virker uklart, må du minne deg på at vi har en liste over koblede noder; bare å fjerne en node bryter lenken fra den første noden til listen til den siste noden i listen. 

Deretter tildeler vi deletedNode til nodeToDelete. Da setter vi verdien av nodeToDelete til null, redusere lengden på listen med en, og returner deletedNode

Fullstendig implementering av en enkeltstående liste

Den komplette implementeringen av listen vår er her: 

funksjonsnode (data) this.data = data; this.next = null;  funksjon SinglyList () this._length = 0; this.head = null;  SinglyList.prototype.add = funksjon (verdi) var node = ny Node (verdi), currentNode = this.head; // Første brukstilstand: En tom liste hvis (! CurrentNode) this.head = node; this._length ++; returknutepunkt;  // 2. brukskasse: en ikke-tom liste mens (currentNode.next) currentNode = currentNode.next;  currentNode.next = node; this._length ++; returknutepunkt; ; SinglyList.prototype.searchNodeAt = funksjon (posisjon) var currentNode = this.head, lengde = this.length, count = 1, message = failure: 'Feil: ikke-eksisterende node i denne listen.'; // Første brukstilstand: En ugyldig stilling hvis (lengde === 0 || posisjon < 1 || position > lengde) kaste ny feil (message.failure);  // 2. brukskasse: en gyldig posisjon mens (telle < 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 > lengde) kaste ny feil (message.failure);  // 2. brukskasse: den første noden er fjernet hvis (posisjon === 1) this.head = currentNode.next; deletedNode = currentNode; currentNode = null; this._length--; returner deletedNode;  // 3. brukskode: en hvilken som helst annen knutepunkt blir fjernet mens (telle < position)  beforeNodeToDelete = currentNode; nodeToDelete = currentNode.next; count++;  beforeNodeToDelete.next = nodeToDelete.next; deletedNode = nodeToDelete; nodeToDelete = null; this._length--; return deletedNode; ;

Fra Singly til Doubly

Fantastisk, vår implementering av en enkeltlinket liste er fullført. Vi kan nå bruke en datastruktur som legger til, fjerner og søker noder i en liste som opptar ikke-sammenhengende plass i minnet. 

Men i øyeblikket begynner alle våre operasjoner fra begynnelsen av en liste og går til slutten av en liste. Med andre ord, de er uni-directional. 

Det kan være tilfeller hvor vi vil at vår virksomhet skal være toveis. Hvis du vurderte dette brukstilfellet, har du nettopp beskrevet en dobbeltkoblet liste.

En dobbeltkoblet liste

En dobbeltkoblet liste tar all funksjonaliteten til en enkeltkoblet liste og strekker den for toveisbevegelse i en liste. Vi kan krysse, med andre ord en koblet liste fra den første noden i listen til siste node i listen; og vi kan krysse fra den siste noden i listen til den første noden i listen. 

I denne delen vil vi opprettholde vårt fokus først og fremst på forskjellene mellom en dobbeltkoblet liste og en enkeltkoblet liste. 

Operasjoner av en dobbeltkoblet liste 

Vår liste vil inneholde to konstruktører: node og DoublyList. La oss skissere deres operasjoner.

node 

  • data lagrer en verdi.
  • neste peker til neste knutepunkt i listen.
  • tidligere peker til forrige node i listen.

DoublyList

  • _lengde henter antall noder i en liste.
  • hode tilordner en node som leder av en liste.
  • hale tilordner en node som halen på en liste.
  • Legg til verdi) legger til en node i en liste.
  • searchNodeAt (stilling) søker etter en node på n-posisjon i vår liste.
  • fjerne (stilling) fjerner en node fra en liste.

Implementering av en dobbeltkoblet liste 

La oss skrive noen kode! 

For implementeringen skal vi opprette en konstruktør som heter node:

funksjonsknute (verdi) this.data = value; this.previous = null; this.next = null; 

For å opprette toveisbevegelsen til en dobbeltkoblet liste, trenger vi egenskaper som peker i begge retninger av en liste. Disse egenskapene er oppkalt tidligere og neste

Deretter må vi implementere en DoublyList og legg til tre egenskaper: _lengde, hode, og hale. I motsetning til en enkeltlinket liste, har en dobbeltkoblet liste en referanse til både begynnelsen på en liste og enden av en liste. Siden hver forekomst av a DoublyList er instantiated uten noder, standardverdiene for hode og hale er satt til null.

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

Metoder for en dobbeltkoblet liste

Vi vil nå utforske følgende metoder: Legg til verdi), fjerne (stilling), og searchNodeAt (stilling). Alle disse metodene ble brukt til en enkeltkoblet liste; De må imidlertid omskrives for toveisbevegelser. 

1 av 3: Legg til verdi)

DoublyList.prototype.add = funksjon (verdi) var node = ny Node (verdi); hvis (this.length) this.tail.next = node; node.previous = this.tail; this.tail = node;  ellers this.head = node; this.tail = node;  this.length ++; returknutepunkt; ;

I denne metoden har vi to scenarier. Først, hvis en liste er tom, så tilordnes den til hode og hale noden blir lagt til. For det andre, hvis listen inneholder noder, så finn halen på listen og tilordne til tail.next noden blir lagt til; På samme måte må vi konfigurere den nye halen for toveisbevegelse. Vi må sette med andre ord, tail.previous til den opprinnelige halen. 

2 av 3: searchNodeAt (stilling)

Gjennomføringen av searchNodeAt (stilling) er identisk med en enkeltkoblet liste. Hvis du har glemt hvordan du implementerer den, har jeg tatt med den nedenfor: 

DoublyList.prototype.searchNodeAt = funksjon (posisjon) var currentNode = this.head, lengde = this.length, count = 1, message = failure: 'Feil: ikke-eksisterende node i denne listen.'; // Første brukstilstand: En ugyldig stilling hvis (lengde === 0 || posisjon < 1 || position > lengde) kaste ny feil (message.failure);  // 2. brukskasse: en gyldig posisjon mens (telle < position)  currentNode = currentNode.next; count++;  return currentNode; ;

3 av 3: fjerne (stilling)

Denne metoden vil være den mest utfordrende å forstå. Jeg vil vise koden og deretter forklare den. 

DoublyList.prototype.remove = funksjon (posisjon) var currentNode = this.head, lengde = this.length, count = 1, message = failure: 'Feil: ikke-eksisterende node i denne listen.', BeforeNodeToDelete = null , nodeToDelete = null, deletedNode = null; // Første brukstilstand: En ugyldig stilling hvis (lengde === 0 || posisjon < 1 || position > lengde) kaste ny feil (message.failure);  // 2. brukskasse: den første noden er fjernet hvis (posisjon === 1) this.head = currentNode.next; // 2. brukskasse: det er en andre node hvis (! This.head) this.head.previous = null; // 2. brukskasse: det er ingen andre node annet this.tail = null;  // 3. brukskasse: den siste noden er fjernet annet hvis (posisjon === this.length) this.tail = this.tail.previous; this.tail.next = null; // Fjerde brukstilstand: En mellomnode er fjernet ellers mens (telle < 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; ;

fjerne (stilling) håndterer fire brukstilfeller: 

  1. Stillingen blir passert som et argument av fjerne (stilling) er ikke-eksisterende. I dette tilfellet kaster vi en feil. 
  2. Stillingen blir passert som et argument av fjerne (stilling) er den første noden (hode) av en liste. Hvis dette er tilfelle, vil vi tildele deletedNode til hode og deretter tilordne hode til neste knutepunkt i listen. For øyeblikket må vi vurdere om listen vår har mer enn én node. Hvis svaret er nei, hode vil bli tildelt til null og vi kommer inn i hvis en del av vår if-else uttalelse. I kroppen av hvis, vi må også sette hale til null-med andre ord, vi går tilbake til den opprinnelige tilstanden til en tom dobbeltkoblet liste. Hvis vi fjerner den første noden i en liste og vi har mer enn én node i vår liste, skriver vi inn ellers delen av vår if-else uttalelse. I dette tilfellet må vi riktig sette inn tidligere tilhører hode til null-det er ingen noder før hodet på en liste.  
  3. Stillingen blir passert som et argument av fjerne (stilling) er halen av en liste. Først, deletedNode er tildelt til hale. Sekund, hale er tilordnet til noden tidligere til halen. For det tredje har den nye halen ingen node etter den og trenger verdien av neste å bli satt til null
  4. Mye skjer her, så jeg vil fokusere på logikken mer enn hver linje i koden. Vi knuser våre samtidig som loop en gang currentNode peker til noden på stillingen som blir sendt som argument til fjerne (stilling). I øyeblikket overfører vi verdien av beforeNodeToDelete.next til noden etter nodeToDelete og omvendt overfører vi verdien av afterNodeToDelete.previous til noden før nodeToDelete. Med andre ord fjerner vi pekere til den fjernede noden og overfører dem til de riktige noderne. Deretter tildeler vi deletedNode til nodeToDelete. Til slutt tildeler vi nodeToDelete til null.   

Endelig reduserer vi lengden på vår liste og returnerer deletedNode

Fullstendig implementering av en dobbeltkoblet liste

Her er hele implementeringen: 

funksjonsknute (verdi) this.data = value; this.previous = null; this.next = null;  funksjonen DoublyList () this._length = 0; this.head = null; this.tail = null;  DoublyList.prototype.add = funksjon (verdi) var node = ny Node (verdi); hvis (this.length) this.tail.next = node; node.previous = this.tail; this.tail = node;  ellers this.head = node; this.tail = node;  this.length ++; returknutepunkt; ; DoublyList.prototype.searchNodeAt = funksjon (posisjon) var currentNode = this.head, lengde = this.length, count = 1, message = failure: 'Feil: ikke-eksisterende node i denne listen.'; // Første brukstilstand: En ugyldig stilling hvis (lengde === 0 || posisjon < 1 || position > lengde) kaste ny feil (message.failure);  // 2. brukskasse: en gyldig posisjon mens (telle < 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 > lengde) kaste ny feil (message.failure);  // 2. brukskasse: den første noden er fjernet hvis (posisjon === 1) this.head = currentNode.next; // 2. brukskasse: det er en andre node hvis (! This.head) this.head.previous = null; // 2. brukskasse: det er ingen andre node annet this.tail = null;  // 3. brukskasse: den siste noden er fjernet annet hvis (posisjon === this.length) this.tail = this.tail.previous; this.tail.next = null; // Fjerde brukstilstand: En mellomnode er fjernet ellers mens (telle < 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; ;

Konklusjon

Vi har dekket mye informasjon i denne artikkelen. Hvis noe av det virker forvirrende, les det igjen, og eksperimenter med koden. Når det til slutt gir mening, føler du deg stolt. Du har nettopp avdekket mysteriene til både en enkeltlinket liste og en dobbeltkoblet liste. Du kan legge til disse datastrukturene til kodingsverktøyet ditt!