Structuri de date cu JavaScript Arbore

Ce veți crea

Copacii sunt una dintre cele mai utilizate structuri de date în dezvoltarea web. Această afirmație este valabilă atât pentru dezvoltatori, cât și pentru utilizatori. Fiecare dezvoltator de web care a scris HTML și a încărcat-o într-un browser web a creat un copac, denumit Document Object Model (DOM). Fiecare utilizator al Internetului, care, la rândul său, a consumat informații pe internet, la primit sub forma unui copac - DOM. 

Acum, iată punctul culminant: articolul pe care îl citiți în acest moment este redat în browserul dvs. ca un copac! Paragraful pe care îl citiți este reprezentat ca text în a

element;

elementul este imbricat în interior element; si elementul este imbricat în interior element. 

Cuibul de date este similar cu un arbore genealogic. element este un părinte, element este un copil, și

element este un copil al element. Dacă această analogie a unui copac ți se pare utilă, atunci vei găsi confortul în a ști că mai multe analogii vor fi folosite în timpul implementării unui copac. 

În acest articol, vom crea un copac utilizând două metode diferite de traversare a copacilor: Căutarea în profunzime (DFS) și Căutarea în Bretă (BFS). (În cazul în care cuvântul traversal nu vă este cunoscut, considerați că înseamnă vizitarea fiecărui nod al copacului.) Ambele tipuri de traversări evidențiază modalități diferite de a interacționa cu un copac; ambele călătorii includ, în plus, utilizarea structurilor de date pe care le-am acoperit în această serie. DFS utilizează o stivă și BFS utilizează o coadă pentru a vizita nodurile. Asta e tare!

Arbore (căutare în profunzime și căutare primă)

În domeniul informaticii, un arbore este o structură de date care simulează datele ierarhice cu noduri. Fiecare nod al unui arbore își păstrează propriile date și indicii către alte noduri.

Terminologia nodurilor și pointerilor poate fi nouă pentru unii cititori, așadar să le descriem mai departe cu o analogie. Să comparăm un copac cu o diagramă organizațională. Graficul are o poziție de nivel superior (nod rădăcină), cum ar fi CEO. Direct sub această poziție sunt alte poziții, cum ar fi vicepreședintele (VP). 

Pentru a reprezenta această relație, folosim săgeți care indică de la CEO la un VP. O poziție, cum ar fi CEO-ul, este un nod; relația pe care o creăm de la un CEO la un VP este un indicator. Pentru a crea mai multe relații în schema organizațională, repetăm ​​acest proces - avem un punct de nod în alt nod. 

La nivel conceptual, sper că nodurile și indicii au sens. La nivel practic, putem beneficia de un exemplu mai tehnic. Să luăm în considerare DOM. Un DOM are un element ca poziție de nivel superior (nod rădăcină). Acest nod indică a element și a element. Acest proces este repetat pentru toate nodurile din DOM. 

Una dintre frumusețile acestui design este capacitatea de a cuibui noduri: a

    element, de exemplu, poate avea multe 
  • elemente imbricate în interiorul acesteia; mai mult, fiecare
  • element poate avea frate
  • noduri. Este ciudat, dar amuzant și adevărat!

    Operațiunile unui copac

    Deoarece fiecare arbore conține noduri, care pot fi un constructor separat dintr-un arbore, vom sublinia operațiile ambelor constructori: Nodul și Copac.

    Nodul

    • date stochează o valoare.
    • mamă indică la părintele unui nod.
    • copii puncte către următorul nod din listă.

    Copac

    • _rădăcină indică nodul rădăcină al unui copac.
    • traverseDF (callback) traversează nodurile unui copac cu DFS.
    • traverseBF (callback) traversează nodurile unui copac cu BFS.
    • conține (date, traversal) caută un nod dintr-un copac.
    • adăugați (date, date, traversează) adaugă un nod unui copac.
    • elimina (copil, părinte) elimină un nod într-un copac. 

    Implementarea unui copac

    Acum, să scriem codul pentru un copac!

    Proprietățile unui nod 

    Pentru implementarea noastră, vom defini mai întâi o funcție numită Nodul și apoi un constructor numit Copac.

    funcția Nod (date) this.data = date; this.parent = null; this.children = []; 

    Fiecare instanță a nodului conține trei proprietăți: date, mamă, și copii. Prima proprietate deține datele asociate unui nod. A doua proprietate indică un nod. A treia proprietate indică mai multe noduri pentru copii.

    Proprietățile unui copac

    Acum, să definim constructorul nostru Copac, care include Nodul constructor în definiția sa: 

    funcția Arbore (date) var node = nou Nod (date); this._root = nod; 

    Copac conține două linii de cod. Prima linie creează o nouă instanță de Nodul; a doua linie atribuie nodul ca rădăcină a unui copac. 

    Definițiile Copac și Nodul cere doar câteva linii de cod. Aceste linii, totuși, sunt suficiente pentru a ne ajuta să simulăm datele ierarhice. Pentru a dovedi acest punct, hai să folosim câteva exemple de date pentru a crea o instanță de Copac (și, indirect, Nodul).

    var tree = copac nou (CEO); // date: "CEO", părinte: null, copii: [] tree._root;

    Datorită existenței mamă și copii, putem adăuga noduri ca și copii ai lui _rădăcină și, de asemenea, alocați _rădăcină ca părinți ai acestor noduri. Cu alte cuvinte, putem simula crearea de date ierarhice. 

    Metodele unui copac

    Apoi, vom crea următoarele cinci metode: 

    Copac

    1. traverseDF (callback)
    2. traverseBF (callback)
    3. conține (date, traversal)
    4. adăugați (copil, părinte)
    5. elimina (nod, părinte)

    Deoarece fiecare metodă a unui copac ne obligă să traversăm un copac, vom implementa mai întâi metode care definesc diferite tipuri de traversare a copacilor. (Traversarea unui copac este o modalitate formală de a spune vizitarea fiecărui nod al unui copac.) 

    1 din 5: traverseDF (callback)

    Această metodă traversează un arbore cu căutare în profunzime.  

    Tree.prototype.traverseDF = funcția (callback) // aceasta este o funcție recurse și imediat invocată (funcția recurse (currentNode) // pasul 2 pentru (var i = 0, length = currentNode.children.length; i < length; i++)  // step 3 recurse(currentNode.children[i]);  // step 4 callback(currentNode); // step 1 )(this._root); ;

    traverseDF (callback) are un parametru numit suna inapoi. Dacă nu este clar din nume, suna inapoi se presupune a fi o funcție, care va fi numită mai târziu traverseDF (callback)

    Corpul lui traverseDF (callback) include o altă funcție numită recurse. Această funcție este o funcție recursivă! Cu alte cuvinte, ea se invocă și se auto-învine. Utilizând pașii menționați în comentariile de la recurse, Voi descrie procesul general recurse utilizează pentru a traversa întregul copac. 

    Iată pașii: 

    1. Invocați imediat recurse cu nodul rădăcină al unui arbore ca argument. În acest moment, currentNode indică spre nodul curent. 
    2. Introduceți a pentru buclă și iterați o dată pentru fiecare copil din currentNode, începând cu primul copil. 
    3. În interiorul corpului pentru buclă, invoca recurs cu un copil de currentNode. Copilul exact depinde de actuala repetare a lui pentru buclă. 
    4. Cand currentNode nu mai are copii, ieșim pentru buclă și invocați suna inapoi am trecut în timpul invocării traverseDF (callback)

    Etapele 2 (auto-terminare), 3 (auto-invocare) și 4 (apel invers) repetați până când traversăm fiecare nod al unui copac. 

    Recurgerea este un subiect foarte greu de învățat și necesită un întreg articol care să-i explice în mod adecvat. Deoarece explicația recursivității nu este punctul central al acestui articol - accentul este punerea în aplicare a unui copac - voi sugera că orice cititor care nu are o bună înțelegere a recursivității face următoarele două lucruri.

    Mai întâi, experimentați cu implementarea noastră actuală de traverseDF (callback) și încercați să înțelegeți într-o anumită măsură cum funcționează. În al doilea rând, dacă vreți să scriu un articol despre recursivitate, vă rugăm să îl solicitați în comentariile acestui articol.

    Următorul exemplu demonstrează modul în care ar fi traversat un copac traverseDF (callback). Pentru a traversa un copac, o voi crea în exemplul de mai jos. Abordarea pe care o voi folosi în acest moment nu este ideală, dar funcționează. O abordare mai bună ar fi utilizarea adaugă valoare), pe care o vom implementa la pasul 4 din 5. 

    var tree = copac nou ("unul"); tree._root.children.push (nou Nod ("două")); tree._root.children [0] .parent = copac; tree._root.children.push (nou Nod ("trei")); tree._root.children [1] .parent = copac; tree._root.children.push (nou Nod ("patru")); tree._root.children [2] .parent = copac; tree._root.children [0] .children.push (nou Nod ("cinci")); copac_root.children [0] .children [0] .parent = tree._root.children [0]; tree._root.children [0] .children.push (nou Nod ("șase")); tree._root.children [0] .children [1] .parent = tree._root.children [0]; tree._root.children [2] .children.push (nou Nod ("șapte")); tree._root.children [2] .children [0] .parent = tree._root.children [2]; / * creează acest arbore unu ─────────────────────────────────────────────────────────────────────────────────────────────────────────

    Acum, să invocăm traverseDF (callback).

    tree.traverseDF (funcție (nod) console.log (node.data)); / * înregistrează următoarele șiruri de caractere la consola "cinci" șase "două" trei "șapte" patru "unul" * /

    2 din 5: traverseBF (callback)

    Această metodă utilizează o căutare la prima lățime pentru a traversa un copac. 

    Diferența dintre căutarea de adâncime și prima căutare de lățime-primă implică secvența pe care nodurile unui copac o vizitează. Pentru a ilustra acest punct, să folosim arborele din care am creat traverseDF (callback).

    / * un copac unul (adâncime: 0) ├───────────────────────────────────────────────────────────────────────────────────────────────────────────── patru (adâncime: 1) └──apte (adâncime: 2) * /

    Acum, hai să trecem traverseBF (callback) același callback pe care l-am folosit traverseDF (callback)

    tree.traverseBF (funcție (nod) console.log (node.data)); / * înregistrează următoarele șiruri de caractere la consola "unul" două "trei" patru "cinci" șase "șapte" * /

    Buștenii din consola și diagrama arborelui nostru dezvăluie un model despre căutarea pe lățime. Începeți cu nodul rădăcină; apoi deplasați o adâncime și vizitați fiecare nod în acea adâncime de la stânga la dreapta. Repetați acest proces până când nu mai aveți adâncimi de parcurs. 

    De vreme ce avem un model conceptual de căutare la prima lățime, să punem acum în aplicare codul care ar face ca exemplul nostru să funcționeze. 

    Tree.prototype.traverseBF = funcția (callback) var queue = noua coadă (); queue.enqueue (this._root); curentTree = queue.dequeue (); în timp ce (currentTree) pentru (var i = 0, length = currentTree.children.length; i < length; i++)  queue.enqueue(currentTree.children[i]);  callback(currentTree); currentTree = queue.dequeue();  ;

    Definiția noastră de traverseBF (callback) conține o mulțime de logică. Din acest motiv, voi explica logica în pași maniabili: 

    1. Creați o instanță de Coadă.
    2. Adăugați nodul invocat traverseBF (callback) la instanța lui Coadă
    3. Declarați o variabilă numită currentNode și inițializați-l la nodul tocmai am adăugat la coada noastră. 
    4. In timp ce currentNode indică un nod, execută codul în interiorul lui in timp ce buclă. 
    5. Folosește o pentru buclă pentru a itera asupra copiilor din currentNode.
    6. În interiorul corpului pentru bucla, adăugați fiecare copil la coadă. 
    7. Lua currentNode și treci ca un argument al lui suna inapoi
    8. realocaţi currentNode la nodul care este eliminat din coadă. 
    9. Pana cand currentNode nu indică un nod - fiecare nod din copac a fost vizitat - repetați pașii 4 până la 8.

    3 din 5: conține (apel invers, traversal)

    Să definim o metodă care ne va permite să căutăm o anumită valoare în arborele nostru. Pentru a utiliza oricare dintre metodele noastre de traversare a copacilor, am definit conține (apel invers, traversal) să accepte două argumente: datele de căutare și tipul traversalului. 

    Tree.prototype.contains = funcție (apel invers, traversal) traversal.call (acest lucru, apel invers); ;

    În corpul lui conține (apel invers, traversal), folosim o metodă numită apel a trece acest și suna inapoi. Primul argument se leagă traversarea la pomul care a invocat conține (apel invers, traversal); al doilea argument este o funcție invocată pe fiecare nod din arborele nostru. 

    Imaginați-vă că vrem să conectăm la consola orice nod care conține date cu un număr impar și să traverseze fiecare nod din arborele nostru cu BFS. Acesta este codul pe care îl scriem:

    // arborele este un exemplu de nod rădăcină tree.contains (funcția (node) if (node.data === 'două') console.log (node);, tree.traverseBF);

    4 din 5: adăugați (date, date, traversal)

    Acum avem o metodă de căutare a unui nod specific în arborele nostru. Să definim acum o metodă care ne va permite să adăugăm un nod unui anumit nod.  

    Tree.prototype.add = funcție (date, toData, traversal) var copil = nou Nod (date), parent = null, callback = funcția (node) if (node.data === toData) parent = node; ; acest lucru conține (apel invers, traversal); dacă (părinte) parent.children.push (copil); child.parent = părinte;  altceva arunca o eroare nouă ("Nu se poate adăuga un nod la un părinte inexistent"); ;

    adăugați (date, date, traversal) definește trei parametri. Primul parametru, date, este folosit pentru a crea o nouă instanță de Nodul. Al doilea parametru, toData, este folosit pentru a compara cu fiecare nod dintr-un copac. Al treilea parametru, traversarea, este tipul de traversal arbore utilizat în această metodă. 

    În corpul lui adăugați (date, date, traversal), noi declarăm trei variabile. Prima variabilă, copil, este inițializată ca o nouă instanță de Nodul. A doua variabilă, mamă, este inițializată ca nul; dar va indica mai târziu la orice nod dintr-un arbore care se potrivește cu valoarea lui toData. Repartizarea mamă se întâmplă în a treia variabilă pe care o declarăm, adică suna inapoi

    suna inapoi este o funcție care se compară toData la fiecare nod date proprietate. În cazul în care dacă declarația se evaluează la Adevărat, atunci mamă este atribuită nodului care a corespuns comparației din dacă afirmație. 

    Comparația efectivă a fiecărui nod cu toData se întâmplă în conține (apel invers, traversal). Tipul de traverse și suna inapoi trebuie să fie transmisă ca argumente ale lui conține (apel invers, traversal)

    În cele din urmă, dacă mamă există în copac, împingem copil în parent.children; noi atribuim și noi mamă la părintele lui copil. Altfel, aruncăm o eroare. 

    Să folosim adăugați (date, date, traversal) într-un exemplu: 

    var tree = copac nou (CEO); tree.add ("VP de fericire", "CEO", tree.traverseBF); / * copacul nostru "CEO" └─────────

    Iată un exemplu mai complex adăugați (addData, toData, traversal)

    var tree = copac nou (CEO); tree.add ("VP de fericire", "CEO", tree.traverseBF); tree.add ("VP de finanțe", "CEO", tree.traverseBF); tree.add ("VP de tristețe", "CEO", tree.traverseBF); tree.add ("Directorul Puppies", "VP of Finance", tree.traverseBF); tree.add ("Managerul Puppies", "Directorul Puppies", tree.traverseBF); / * Copac 'CEO' ├── 'VP of Happiness' ├── 'VP de Finanțe' │ ├── 'Director al Cățeluși' │ └── 'Managerul Cățeluși' └── 'VP tristeții' * /

    5 din 5: eliminați (date, de la Data, traversal)

    Pentru a finaliza implementarea noastră de Copac, vom adăuga o metodă numită eliminați (date, de la Data, traversal). Similar cu eliminarea unui nod din DOM, această metodă va elimina un nod și toți copiii acestuia.

    Tree.prototype.remove = funcția (date, dinData, traversal) var tree = aceasta, parent = null, childToRemove = null, index; var callback = funcția (nodul) if (node.data === fromData) parent = node; ; acest lucru conține (apel invers, traversal); dacă (părinte) index = findIndex (parent.children, data); if (index === undefined) arunca o eroare nouă ("Nodul de eliminat nu există");  altfel childToRemove = parent.children.splice (index, 1);  altfel arunca o eroare nouă ("Parent nu există.");  return childToRemove; ;

    Similar cu adăugați (date, date, traversal), elimina traverses un copac pentru a găsi un nod care conține al doilea argument, care este acum fromData. Dacă se găsește acest nod, atunci mamă arată spre el. 

    În acest moment, ajungem la primul nostru dacă afirmație. Dacă mamă nu există, aruncăm o eroare. Dacă mamă există, invocăm findIndex () cu parent.children și datele pe care dorim să le eliminăm din nodurile copiilor din mamă. (findIndex () este o metodă de ajutor pe care am definit-o mai jos.)

    funcția findIndex (arr, date) var index; pentru (var i = 0; i < arr.length; i++)  if (arr[i].data === data)  index = i;   return index; 

    Interior findIndex (), apare următoarea logică. Dacă unul din noduri din parent.children conține date care se potrivesc date, variabila index i se atribuie un număr întreg. Dacă nu se potrivește niciunul dintre proprietățile de date ale copiilor date, atunci index își păstrează valoarea implicită nedefinit. Pe ultima linie din findIndex (), ne intoarcem index

    Acum ne întoarcem eliminați (date, de la Data, traversal). Dacă index este nedefinit, o eroare este aruncată. Dacă index este definit, îl folosim pentru a uni nodul pe care dorim să îl eliminăm de la copiii lui mamă; îi atribuim și copilul eliminat childToRemove

    În cele din urmă, ne întoarcem childToRemove

    Implementarea completă a unui copac

    Implementarea noastră de Copac este complet. Aruncă o privire - am realizat foarte mult: 

    funcția Nod (date) this.data = date; this.parent = null; this.children = [];  funcția Tree (date) var node = nou Nod (date); this._root = nod;  Tree.prototype.traverseDF = functie (callback) // aceasta este o functie recurse si imediat invocatoare (function recurse (currentNode) // pasul 2 pentru (var i = 0, length = currentNode.children.length; i < length; i++)  // step 3 recurse(currentNode.children[i]);  // step 4 callback(currentNode); // step 1 )(this._root); ; Tree.prototype.traverseBF = function(callback)  var queue = new Queue(); queue.enqueue(this._root); currentTree = queue.dequeue(); while(currentTree) for (var i = 0, length = currentTree.children.length; i < length; i++)  queue.enqueue(currentTree.children[i]);  callback(currentTree); currentTree = queue.dequeue();  ; Tree.prototype.contains = function(callback, traversal)  traversal.call(this, callback); ; Tree.prototype.add = function(data, toData, traversal)  var child = new Node(data), parent = null, callback = function(node)  if (node.data === toData)  parent = node;  ; this.contains(callback, traversal); if (parent)  parent.children.push(child); child.parent = parent;  else  throw new Error('Cannot add node to a non-existent parent.');  ; Tree.prototype.remove = function(data, fromData, traversal)  var tree = this, parent = null, childToRemove = null, index; var callback = function(node)  if (node.data === fromData)  parent = node;  ; this.contains(callback, traversal); if (parent)  index = findIndex(parent.children, data); if (index === undefined)  throw new Error('Node to remove does not exist.');  else  childToRemove = parent.children.splice(index, 1);   else  throw new Error('Parent does not exist.');  return childToRemove; ; function findIndex(arr, data)  var index; for (var i = 0; i < arr.length; i++)  if (arr[i].data === data)  index = i;   return index; 

    Concluzie 

    Copacii simulează date ierarhice. O mare parte din lumea din jurul nostru se aseamănă cu acest tip de ierarhie, cum ar fi paginile web și familiile noastre. Ori de câte ori vă aflați în nevoie de structurare a datelor cu ierarhie, luați în considerare utilizarea unui copac. 

    Cod