鏈表用來存儲有序的元素集合,與數組不一樣,鏈表中的元素並不是保存在連續的存儲空間內,每一個元素由一個存儲元素自己的節點和一個指向下一個元素的指針構成。當要移動或刪除元素時,只須要修改相應元素上的指針就能夠了。對鏈表元素的操做要比對數組元素的操做效率更高。下面是鏈表數據結構的示意圖:node
要實現鏈表數據結構,關鍵在於保存head元素(即鏈表的頭元素)以及每個元素的next指針,有這兩部分咱們就能夠很方便地遍歷鏈表從而操做全部的元素。能夠把鏈表想象成一條鎖鏈,鎖鏈中的每個節點都是相互鏈接的,咱們只要找到鎖鏈的頭,整條鎖鏈就均可以找到了。讓咱們來看一下具體的實現方式。數組
首先咱們須要一個輔助類,用來描述鏈表中的節點。這個類很簡單,只須要兩個屬性,一個用來保存節點的值,一個用來保存指向下一個節點的指針。數據結構
let Node = function (element) { this.element = element; this.next = null; };
下面是咱們鏈表類的基本骨架:app
class LinkedList { constructor() { this.length = 0; this.head = null; } append (element) {} // 向鏈表中添加節點 insert (position, element) {} // 在鏈表的指定位置插入節點 removeAt (position) {} // 刪除鏈表中指定位置的元素,並返回這個元素的值 remove (element) {} // 刪除鏈表中對應的元素 indexOf (element) {} // 在鏈表中查找給定元素的索引 getElementAt (position) {} // 返回鏈表中索引所對應的元素 isEmpty () {} // 判斷鏈表是否爲空 size () {} // 返回鏈表的長度 getHead () {} // 返回鏈表的頭元素 clear () {} // 清空鏈表 toString () {} // 輔助方法,按指定格式輸出鏈表中的全部元素,方便測試驗證結果 }
讓咱們從查找鏈表元素的方法getElementAt()開始,由於後面咱們會屢次用到它。ide
getElementAt (position) { if (position < 0 || position >= this.length) return null; let current = this.head; for (let i = 0; i < position; i++) { current = current.next; } return current; }
首先判斷參數position的邊界值,若是值超出了索引的範圍(小於0或者大於length - 1),則返回null。咱們從鏈表的head開始,遍歷整個鏈表直到找到對應索引位置的節點,而後返回這個節點。是否是很簡單?和全部有序數據集合同樣,鏈表的索引默認從0開始,只要找到了鏈表的頭(因此咱們必須在LinkedList類中保存head值),而後就能夠遍歷找到索引所在位置的元素。測試
有了getElementAt()方法,接下來咱們就能夠很方便地實現append()方法,用來在鏈表的尾部添加新節點。this
append (element) { let node = new Node(element); // 若是當前鏈表爲空,則將head指向node if (this.head === null) this.head = node; else { // 不然,找到鏈表尾部的元素,而後添加新元素 let current = this.getElementAt(this.length - 1); current.next = node; } this.length++; }
若是鏈表的head爲null(這種狀況表示鏈表爲空),則直接將head指向新添加的元素。不然,經過getElementAt()方法找到鏈表的最後一個節點,將該節點的next指針指向新添加的元素。新添加的元素的next指針默認爲null,鏈表最後一個元素的next值爲null。將節點掛到鏈表上以後,不要忘記將鏈表的長度加1,咱們須要經過length屬性來記錄鏈表的長度。spa
接下來咱們要實現insert()方法,能夠在鏈表的任意位置添加節點。指針
insert (position, element) { // position不能超出邊界值 if (position < 0 || position > this.length) return false; let node = new Node(element); if (position === 0) { node.next = this.head; this.head = node; } else { let previous = this.getElementAt(position - 1); node.next = previous.next; previous.next = node; } this.length++; return true; }
首先也是要判斷參數position的邊界值,不能越界。當position的值爲0時,表示要在鏈表的頭部插入新節點,對應的操做以下圖所示。將新插入節點的next指針指向如今的head,而後更新head的值爲新插入的節點。code
若是要插入的節點在鏈表的中間或者尾部,對應的操做以下圖。假設鏈表長度爲3,要在位置2插入新節點,咱們首先找到位置2的前一個節點previous node,將新節點new node的next指針指向previous node的next所對應的節點,而後再將previous node的next指針指向new node,這樣就把新節點掛到鏈表中了。考慮一下,當插入的節點在鏈表的尾部,這種狀況也是適用的。而若是鏈表爲空,即鏈表的head爲null,則參數position會超出邊界條件,從而insert()方法會直接返回false。
最後,別忘了更新length屬性的值,將鏈表的長度加1。
按照相同的方式,咱們能夠很容易地寫出removeAt()方法,用來刪除鏈表中指定位置的節點。
removeAt (position) { // position不能超出邊界值 if (position < 0 || position >= this.length) return null; let current = this.head; if (position === 0) this.head = current.next; else { let previous = this.getElementAt(position - 1); current = previous.next; previous.next = current.next; } this.length--; return current.element; }
下面兩張示意圖說明了從鏈表頭部和其它位置刪除節點的狀況。
若是要刪除的節點爲鏈表的頭部,只須要將head移到下一個節點便可。若是當前鏈表只有一個節點,那麼下一個節點爲null,此時將head指向下一個節點等同於將head設置成null,刪除以後鏈表爲空。若是要刪除的節點在鏈表的中間部分,咱們須要找出position所在位置的前一個節點,將它的next指針指向position所在位置的下一個節點。總之,刪除節點只須要修改相應節點的指針,使斷開位置左右相鄰的節點從新鏈接上。被刪除的節點因爲再也沒有其它部分的引用而被丟棄在內存中,等待垃圾回收器來清除。有關JavaScript垃圾回收器的工做原理,能夠查看這裏。
最後,別忘了將鏈表的長度減1。
下面咱們來看看indexOf()方法,該方法返回給定元素在鏈表中的索引位置。
indexOf (element) { let current = this.head; for (let i = 0; i < this.length; i++) { if (current.element === element) return i; current = current.next; } return -1; }
咱們從鏈表的頭部開始遍歷,直到找到和給定元素相同的元素,而後返回對應的索引號。若是沒有找到對應的元素,則返回-1。
鏈表類中的其它方法都比較簡單,就再也不分部講解了,下面是完整的鏈表類的代碼:
1 class LinkedList { 2 constructor() { 3 this.length = 0; 4 this.head = null; 5 } 6 7 append (element) { 8 let node = new Node(element); 9 10 // 若是當前鏈表爲空,則將head指向node 11 if (this.head === null) this.head = node; 12 else { 13 // 不然,找到鏈表尾部的元素,而後添加新元素 14 let current = this.getElementAt(this.length - 1); 15 current.next = node; 16 } 17 18 this.length++; 19 } 20 21 insert (position, element) { 22 // position不能超出邊界值 23 if (position < 0 || position > this.length) return false; 24 25 let node = new Node(element); 26 27 if (position === 0) { 28 node.next = this.head; 29 this.head = node; 30 } 31 else { 32 let previous = this.getElementAt(position - 1); 33 node.next = previous.next; 34 previous.next = node; 35 } 36 37 this.length++; 38 return true; 39 } 40 41 removeAt (position) { 42 // position不能超出邊界值 43 if (position < 0 || position >= this.length) return null; 44 45 let current = this.head; 46 47 if (position === 0) this.head = current.next; 48 else { 49 let previous = this.getElementAt(position - 1); 50 current = previous.next; 51 previous.next = current.next; 52 } 53 54 this.length--; 55 return current.element; 56 } 57 58 remove (element) { 59 let index = this.indexOf(element); 60 return this.removeAt(index); 61 } 62 63 indexOf (element) { 64 let current = this.head; 65 66 for (let i = 0; i < this.length; i++) { 67 if (current.element === element) return i; 68 current = current.next; 69 } 70 71 return -1; 72 } 73 74 getElementAt (position) { 75 if (position < 0 || position >= this.length) return null; 76 77 let current = this.head; 78 for (let i = 0; i < position; i++) { 79 current = current.next; 80 } 81 return current; 82 } 83 84 isEmpty () { 85 // return this.head === null; 86 return this.length === 0; 87 } 88 89 size () { 90 return this.length; 91 } 92 93 getHead () { 94 return this.head; 95 } 96 97 clear () { 98 this.head = null; 99 this.length = 0; 100 } 101 102 toString () { 103 let current = this.head; 104 let s = ''; 105 106 while (current) { 107 let next = current.next; 108 next = next ? next.element : 'null'; 109 s += `[element: ${current.element}, next: ${next}] `; 110 current = current.next; 111 } 112 113 return s; 114 } 115 }
在isEmpty()方法中,咱們能夠根據length是否爲0來判斷鏈表是否爲空,固然也能夠根據head是否爲null來進行判斷,前提是全部涉及到鏈表節點添加和移除的方法都要正確地更新length和head。toString()方法只是爲了方便測試而編寫的,咱們來看看幾個測試用例:
let linkedList = new LinkedList(); linkedList.append(10); linkedList.append(15); linkedList.append(20); console.log(linkedList.toString()); linkedList.insert(0, 9); linkedList.insert(2, 11); linkedList.insert(5, 25); console.log(linkedList.toString()); console.log(linkedList.removeAt(0)); console.log(linkedList.removeAt(1)); console.log(linkedList.removeAt(3)); console.log(linkedList.toString()); console.log(linkedList.indexOf(20)); linkedList.remove(20); console.log(linkedList.toString()); linkedList.clear(); console.log(linkedList.size());
下面是執行結果:
上面鏈表中每個元素只有一個next指針,用來指向下一個節點,這樣的鏈表稱之爲單向鏈表,咱們只能從鏈表的頭部開始遍歷整個鏈表,任何一個節點只能找到它的下一個節點,而不能找到它的上一個節點。雙向鏈表中的每個元素擁有兩個指針,一個用來指向下一個節點,一個用來指向上一個節點。在雙向鏈表中,除了能夠像單向鏈表同樣從頭部開始遍歷以外,還能夠從尾部進行遍歷。下面是雙向鏈表的數據結構示意圖:
因爲雙向鏈表具備單向鏈表的全部特性,所以咱們的雙向鏈表類能夠繼承自前面的單向鏈表類,不過輔助類Node須要添加一個prev屬性,用來指向前一個節點。
let Node = function (element) { this.element = element; this.next = null; this.prev = null; };
下面是繼承自LinkedList類的雙向鏈表類的基本骨架:
class DoubleLinkedList extends LinkedList { constructor() { super(); this.tail = null; } }
先來看看append()方法的實現。當鏈表爲空時,除了要將head指向當前添加的節點外,還要將tail也指向當前要添加的節點。當鏈表不爲空時,直接將tail的next指向當前要添加的節點node,而後修改node的prev指向舊的tail,最後修改tail爲新添加的節點。咱們不須要從頭開始遍歷整個鏈表,而經過tail能夠直接找到鏈表的尾部,這一點比單向鏈表的操做要更方便。最後將length的值加1,修改鏈表的長度。
append (element) { let node = new Node(element); // 若是鏈表爲空,則將head和tail都指向當前添加的節點 if (this.head === null) { this.head = node; this.tail = node; } else { // 不然,將當前節點添加到鏈表的尾部 this.tail.next = node; node.prev = this.tail; this.tail = node; } this.length++; }
因爲雙向鏈表能夠從鏈表的尾部往前遍歷,因此咱們修改了getElementAt()方法,對基類中單向鏈表的方法進行了改寫。當要查找的元素的索引號大於鏈表長度的一半時,從鏈表的尾部開始遍歷。
getElementAt (position) { if (position < 0 || position >= this.length) return null; // 從後往前遍歷 if (position > Math.floor(this.length / 2)) { let current = this.tail; for (let i = this.length - 1; i > position; i--) { current = current.prev; } return current; } // 從前日後遍歷 else { return super.getElementAt(position); } }
有兩種遍歷方式,從前日後遍歷調用的是基類單向鏈表裏的方法,從後往前遍歷須要用到節點的prev指針,用來查找前一個節點。
咱們同時還須要修改insert()和removeAt()這兩個方法。記住,與單向鏈表惟一的區別就是要同時維護head和tail,以及每個節點上的next和prev指針。
insert (position, element) { if (position < 0 || position > this.length) return false; // 插入到尾部 if (position === this.length) this.append(element); else { let node = new Node(element); // 插入到頭部 if (position === 0) { if (this.head === null) { this.head = node; this.tail = node; } else { node.next = this.head; this.head.prev = node; this.head = node; } } // 插入到中間位置 else { let current = this.getElementAt(position); let previous = current.prev; node.next = current; node.prev = previous; previous.next = node; current.prev = node; } } this.length++; return true; } removeAt (position) { // position不能超出邊界值 if (position < 0 || position >= this.length) return null; let current = this.head; let previous; // 移除頭部元素 if (position === 0) { this.head = current.next; this.head.prev = null; if (this.length === 1) this.tail = null; } // 移除尾部元素 else if (position === this.length - 1) { current = this.tail; this.tail = current.prev; this.tail.next = null; } // 移除中間元素 else { current = this.getElementAt(position); previous = current.prev; previous.next = current.next; current.next.prev = previous; } this.length--; return current.element; }
操做過程當中須要判斷一些特殊狀況,例如鏈表的頭和尾,以及當前鏈表是否爲空等等,不然程序可能會在某些特殊狀況下致使越界和報錯。下面是一個完整的雙向鏈表類的代碼:
1 class DoubleLinkedList extends LinkedList.LinkedList { 2 constructor() { 3 super(); 4 this.tail = null; 5 } 6 7 append (element) { 8 let node = new Node(element); 9 10 // 若是鏈表爲空,則將head和tail都指向當前添加的節點 11 if (this.head === null) { 12 this.head = node; 13 this.tail = node; 14 } 15 else { 16 // 不然,將當前節點添加到鏈表的尾部 17 this.tail.next = node; 18 node.prev = this.tail; 19 this.tail = node; 20 } 21 22 this.length++; 23 } 24 25 getElementAt (position) { 26 if (position < 0 || position >= this.length) return null; 27 28 // 從後往前遍歷 29 if (position > Math.floor(this.length / 2)) { 30 let current = this.tail; 31 for (let i = this.length - 1; i > position; i--) { 32 current = current.prev; 33 } 34 return current; 35 } 36 // 從前日後遍歷 37 else { 38 return super.getElementAt(position); 39 } 40 } 41 42 insert (position, element) { 43 if (position < 0 || position > this.length) return false; 44 45 // 插入到尾部 46 if (position === this.length) this.append(element); 47 else { 48 let node = new Node(element); 49 50 // 插入到頭部 51 if (position === 0) { 52 if (this.head === null) { 53 this.head = node; 54 this.tail = node; 55 } 56 else { 57 node.next = this.head; 58 this.head.prev = node; 59 this.head = node; 60 } 61 } 62 // 插入到中間位置 63 else { 64 let current = this.getElementAt(position); 65 let previous = current.prev; 66 node.next = current; 67 node.prev = previous; 68 previous.next = node; 69 current.prev = node; 70 } 71 } 72 73 this.length++; 74 return true; 75 } 76 77 removeAt (position) { 78 // position不能超出邊界值 79 if (position < 0 || position >= this.length) return null; 80 81 let current = this.head; 82 let previous; 83 84 // 移除頭部元素 85 if (position === 0) { 86 this.head = current.next; 87 this.head.prev = null; 88 if (this.length === 1) this.tail = null; 89 } 90 // 移除尾部元素 91 else if (position === this.length - 1) { 92 current = this.tail; 93 this.tail = current.prev; 94 this.tail.next = null; 95 } 96 // 移除中間元素 97 else { 98 current = this.getElementAt(position); 99 previous = current.prev; 100 previous.next = current.next; 101 current.next.prev = previous; 102 } 103 104 this.length--; 105 return current.element; 106 } 107 108 getTail () { 109 return this.tail; 110 } 111 112 clear () { 113 super.clear(); 114 this.tail = null; 115 } 116 117 toString () { 118 let current = this.head; 119 let s = ''; 120 121 while (current) { 122 let next = current.next; 123 let previous = current.prev; 124 next = next ? next.element : 'null'; 125 previous = previous ? previous.element : 'null'; 126 s += `[element: ${current.element}, prev: ${previous}, next: ${next}] `; 127 current = current.next; 128 } 129 130 return s; 131 } 132 }
咱們重寫了toString()方法以方便更加清楚地查看測試結果。下面是一些測試用例:
let doubleLinkedList = new DoubleLinkedList(); doubleLinkedList.append(10); doubleLinkedList.append(15); doubleLinkedList.append(20); doubleLinkedList.append(25); doubleLinkedList.append(30); console.log(doubleLinkedList.toString()); console.log(doubleLinkedList.getElementAt(1).element); console.log(doubleLinkedList.getElementAt(2).element); console.log(doubleLinkedList.getElementAt(3).element); doubleLinkedList.insert(0, 9); doubleLinkedList.insert(4, 24); doubleLinkedList.insert(7, 35); console.log(doubleLinkedList.toString()); console.log(doubleLinkedList.removeAt(0)); console.log(doubleLinkedList.removeAt(1)); console.log(doubleLinkedList.removeAt(5)); console.log(doubleLinkedList.toString());
對應的結果以下:
[element: 10, prev: null, next: 15] [element: 15, prev: 10, next: 20] [element: 20, prev: 15, next: 25] [element: 25, prev: 20, next: 30] [element: 30, prev: 25, next: null] 15 20 25 [element: 9, prev: null, next: 10] [element: 10, prev: 9, next: 15] [element: 15, prev: 10, next: 20] [element: 20, prev: 15, next: 24] [element: 24, prev: 20, next: 25] [element: 25, prev: 24, next: 30] [element: 30, prev: 25, next: 35] [element: 35, prev: 30, next: null] 9 15 30 [element: 10, prev: null, next: 20] [element: 20, prev: 10, next: 24] [element: 24, prev: 20, next: 25] [element: 25, prev: 24, next: 35] [element: 35, prev: 25, next: null]
顧名思義,循環鏈表的尾部指向它本身的頭部。循環鏈表能夠有單向循環鏈表,也能夠有雙向循環鏈表。下面是單向循環鏈表和雙向循環鏈表的數據結構示意圖:
在實現循環鏈表時,須要確保最後一個元素的next指針指向head。下面是單向循環鏈表的完整代碼:
1 class CircularLinkedList extends LinkedList.LinkedList { 2 constructor () { 3 super(); 4 } 5 6 append (element) { 7 let node = new LinkedList.Node(element); 8 9 if (this.head === null) this.head = node; 10 else { 11 let current = this.getElementAt(this.length - 1); 12 current.next = node; 13 } 14 15 node.next = this.head; // 將新添加的元素的next指向head 16 this.length++; 17 } 18 19 insert (position, element) { 20 // position不能超出邊界值 21 if (position < 0 || position > this.length) return false; 22 23 let node = new LinkedList.Node(element); 24 25 if (position === 0) { 26 node.next = this.head; 27 let current = this.getElementAt(this.length - 1); 28 current.next = node; 29 this.head = node; 30 } 31 else { 32 let previous = this.getElementAt(position - 1); 33 node.next = previous.next; 34 previous.next = node; 35 } 36 37 this.length++; 38 return true; 39 } 40 41 removeAt (position) { 42 if (position < 0 || position >= this.length) return null; 43 44 let current = this.head; 45 46 if (position === 0) this.head = current.next; 47 else { 48 let previous = this.getElementAt(position - 1); 49 current = previous.next; 50 previous.next = current.next; 51 } 52 this.length--; 53 54 if (this.length > 1) { 55 let last = this.getElementAt(this.length - 1); 56 last.next = this.head; 57 } 58 59 60 return current.element; 61 } 62 63 toString () { 64 let current = this.head; 65 let s = ''; 66 67 for (let i = 0; i < this.length; i++) { 68 let next = current.next; 69 next = next ? next.element : 'null'; 70 s += `[element: ${current.element}, next: ${next}] `; 71 current = current.next; 72 } 73 74 return s; 75 } 76 }
單向循環鏈表的測試用例:
let circularLinkedList = new CircularLinkedList(); circularLinkedList.append(10); circularLinkedList.append(15); circularLinkedList.append(20); console.log(circularLinkedList.toString()); circularLinkedList.insert(0, 9); circularLinkedList.insert(3, 25); console.log(circularLinkedList.toString()); console.log(circularLinkedList.removeAt(0)); console.log(circularLinkedList.toString());
對應的測試結果:
下一章咱們將介紹如何用JavaScript來實現集合這種數據結構。