我們可以看到在javascript概念中的佇列與棧都是一種特殊的線性表的結構,也是一種比較簡單的基於陣列的順序儲存結構。由於javascript的直譯器針對陣列都做了直接的優化,不會存在在很多程式語言中陣列固定長度的問題(當陣列填滿後再新增就比較困難了,包括新增刪除,都是需要把陣列中所有的元素全部都變換位置的,javascript的的陣列確實直接給優化好了,如push,pop,shift,unshift,split方法等等…)
線性表的順序儲存結構,最大的缺點就是改變其中一個元素的排列時都會引起整個合集的變化,其原因就是在記憶體中的儲存本來就是連貫沒有間隙的,刪除一個自然就要補上。針對這種結構的優化之後就出現了鏈式儲存結構,換個思路,我們完全不關心資料的排列,我們只需要在每一個元素的內部把下一個的資料的位置給記錄就可以了,所以用連結方式儲存的線性表簡稱為連結串列,在鏈式結構中,資料=(資訊+地址)
鏈式結構中,我們把地址也可以稱為“鏈”,一個資料單元就是一個節點,那麼可以說連結串列就是一組節點組成的合集。每一個節點都有一個資料塊引用指向它的下一個節點
1 |
陣列元素是靠位置關係做邏輯引用,連結串列則是靠每一個資料元儲存引用指標關係進行引用 |
這種結構上的優勢就非常明顯的,插入一個資料完全不需要關心其排列情況,只要把“鏈”的指向銜接上
這樣做連結串列的思路就不會侷限在陣列上了,我們可以用物件了,只要物件之間存在引用關係即可
連結串列一般有,單連結串列、靜態連結串列、迴圈連結串列、雙向連結串列
單連結串列:就是很單一的向下傳遞,每一個節點只記錄下一個節點的資訊,就跟無間道中的梁朝偉一樣做臥底都是通過中間人上線與下線聯絡,一旦中間人斷了,那麼就無法證明自己的身份了,所以片尾有一句話:”我是好人,誰知道呢?”
靜態連結串列:就是用陣列描述的連結串列。也就是陣列中每一個下表都是一個“節”包含了資料與指向
迴圈連結串列:由於單連結串列的只會往後方傳遞,所以到達尾部的時候,要回溯到首部會非常麻煩,所以把尾部節的鏈與頭連線起來形成迴圈
雙向連結串列:針對單連結串列的優化,讓每一個節都能知道前後是誰,所以除了後指標域還會存在一個前指標域,這樣提高了查詢的效率,不過帶來了一些在設計上的複雜度,總體來說就是空間換時間了
綜合下,其實連結串列就是線性表中針對順序儲存結構的一種優化手段,但是在javascript語言中由於陣列的特殊性(自動更新引用位置),所以我們可以採用物件的方式做連結串列儲存的結構
單連結串列
我們實現一個最簡單的連結串列關係
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function createLinkList() { var _this = {}, prev = null; return { add: function(val) { //儲存當前的引用 prev = { data: val, next: prev || null } } } } var linksList = createLinkList(); linksList.add("arron1"); linksList.add("arron2"); linksList.add("arron3"); //node節的next鏈就是 -arron3-arron2-arron1 |
通過node物件的next去直引用下一個node物件,初步是實現了通過連結串列的引用,這種鏈式思路jQuery的非同步deferred中的then方法,還有日本的cho45的jsderferre中都有用到。這個實現上還有一個最關鍵的問題,我們怎麼動態插入資料到執行的節之後?
所以我們必須 要設計一個遍歷的方法,用來搜尋這個node鏈上的資料,然後找出這個對應的資料把新的節插入到當前的鏈中,並改寫位置記錄
1 2 3 4 5 6 7 8 9 10 |
//在連結串列中找到對應的節 var findNode = function createFindNode(currNode) { return function(key){ //迴圈找到執行的節,如果沒有返回本身 while (currNode.data != key) { currNode = currNode.next; } return currNode; } }(headNode); |
這就是一個查詢當前節的一個方法,通過傳遞原始的頭部headNode節去一直往下查詢next,直到找到對應的節資訊
這裡是用curry方法實現的
那麼插入節的的時候,針對連結串列地址的換算關係這是這樣
a-b-c-d的連結串列中,如果要在c(c.next->d)後面插入一個f
a-b-c-f-d ,那麼c,next->f , f.next-d
通過insert方法增加節
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
//建立節 function createNode(data) { this.data = data; this.next = null; } //初始化頭部節 //從headNode開始形成一條鏈條 //通過next銜接 var headNode = new createNode("head"); //在連結串列中找到對應的節 var findNode = function createFindNode(currNode) { return function(key){ //迴圈找到執行的節,如果沒有返回本身 while (currNode.data != key) { currNode = currNode.next; } return currNode; } }(headNode); //插入一個新節 this.insert = function(data, key) { //建立一個新節 var newNode = new createNode(data); //在鏈條中找到對應的資料節 //然後把新加入的掛進去 var current = findNode(key); //插入新的接,更改引用關係 //1:a-b-c-d //2:a-b-n-c-d newNode.next = current.next; current.next = newNode; }; |
首先分離出createNode節的構建,在初始化的時候先建立一個頭部節物件用來做節開頭的初始化物件
在insert增加節方法中,通過對headNode鏈的一個查詢,找到對應的節,把新的節給加後之後,最後就是修改一下鏈的關係
如何從連結串列中刪除一個節點?
由於連結串列的特殊性,我們a->b->c->d ,如果要刪除c那麼就必須修改b.next->c為 b.next->d,所以找到前一個節修改其連結串列next地址,這個有點像dom操作中removeChild找到其父節點呼叫移除子節點
同樣的我們也在remove方法的設計中,需要設計一個遍歷往上回溯一個父節點即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//找到前一個節 var findPrevious = function(currNode) { return function(key){ while (!(currNode.next == null) && (currNode.next.data != key)) { currNode = currNode.next; } return currNode; } }(headNode); //插入方法 this.remove = function(key) { var prevNode = findPrevious(key); if (!(prevNode.next == null)) { //修改連結串列關係 prevNode.next = prevNode.next.next; } }; |
測試程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
<!doctype html><button id="test1">插入多條資料</button> <button id="test2">刪除Russellville資料</button><div id="listshow"><br /></div><script type="text/javascript"> ////////// //建立連結串列 // ////////// function createLinkList() { //建立節 function createNode(data) { this.data = data; this.next = null; } //初始化頭部節 //從headNode開始形成一條鏈條 //通過next銜接 var headNode = new createNode("head"); //在連結串列中找到對應的節 var findNode = function createFindNode(currNode) { return function(key) { //迴圈找到執行的節,如果沒有返回本身 while (currNode.data != key) { currNode = currNode.next; } return currNode; } }(headNode); //找到前一個節 var findPrevious = function(currNode) { return function(key) { while (!(currNode.next == null) && (currNode.next.data != key)) { currNode = currNode.next; } return currNode; } }(headNode); //插入一個新節 this.insert = function(data, key) { //建立一個新節 var newNode = new createNode(data); //在鏈條中找到對應的資料節 //然後把新加入的掛進去 var current = findNode(key); //插入新的接,更改引用關係 //1:a-b-c-d //2:a-b-n-c-d newNode.next = current.next; current.next = newNode; }; //插入方法 this.remove = function(key) { var prevNode = findPrevious(key); if (!(prevNode.next == null)) { //修改連結串列關係 prevNode.next = prevNode.next.next; } }; this.display = function(fn) { var currNode = headNode; while (!(currNode.next == null)) { currNode = currNode.next; fn(currNode.data) } }; } var cities = new createLinkList(); function create() { var text = ''; cities.display(function(data) { text += '-' + data; }); var div = document.createElement('div') div.innerHTML = text; document.getElementById("listshow").appendChild(div) } document.getElementById("test1").onclick = function() { cities.insert("Conway", "head"); cities.insert("Russellville", "Conway"); cities.insert("Carlisle", "Russellville"); cities.insert("Alma", "Carlisle"); create(); } document.getElementById("test2").onclick = function() { cities.remove("Russellville"); create() } </script> |
雙連結串列
原理跟單連結串列是一樣的,無非就是給每一個節增加一個前連結串列的指標
增加節
1 2 3 4 5 6 7 8 9 10 11 12 |
//插入一個新節 this.insert = function(data, key) { //建立一個新節 var newNode = new createNode(data); //在鏈條中找到對應的資料節 //然後把新加入的掛進去 var current = findNode(headNode,key); //插入新的接,更改引用關係 newNode.next = current.next; newNode.previous = current current.next = newNode; }; |
刪除節
1 2 3 4 5 6 7 8 9 |
this.remove = function(key) { var currNode = findNode(headNode,key); if (!(currNode.next == null)) { currNode.previous.next = currNode.next; currNode.next.previous = currNode.previous; currNode.next = null; currNode.previous = null; } }; |
在刪除操作中有一個明顯的優化:不需要找到父節了,因為雙連結串列的雙向引用所以效率比單鏈要高
測試程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
<!doctype html><button id="test1">插入多條資料</button> <button id="test2">刪除Russellville資料</button><div id="listshow"><br /></div><script type="text/javascript"> ////////// //建立連結串列 // ////////// function createLinkList() { //建立節 function createNode(data) { this.data = data; this.next = null; this.previous = null } //初始化頭部節 //從headNode開始形成一條鏈條 //通過next銜接 var headNode = new createNode("head"); //在連結串列中找到對應的資料 var findNode = function(currNode, key) { //迴圈找到執行的節,如果沒有返回本身 while (currNode.data != key) { currNode = currNode.next; } return currNode; } //插入一個新節 this.insert = function(data, key) { //建立一個新節 var newNode = new createNode(data); //在鏈條中找到對應的資料節 //然後把新加入的掛進去 var current = findNode(headNode,key); //插入新的接,更改引用關係 newNode.next = current.next; newNode.previous = current current.next = newNode; }; this.remove = function(key) { var currNode = findNode(headNode,key); if (!(currNode.next == null)) { currNode.previous.next = currNode.next; currNode.next.previous = currNode.previous; currNode.next = null; currNode.previous = null; } }; this.display = function(fn) { var currNode = headNode; while (!(currNode.next == null)) { currNode = currNode.next; fn(currNode.data) } }; } var cities = new createLinkList(); function create() { var text = ''; cities.display(function(data) { text += '-' + data; }); var div = document.createElement('div') div.innerHTML = text; document.getElementById("listshow").appendChild(div) } document.getElementById("test1").onclick = function() { cities.insert("Conway", "head"); cities.insert("Russellville", "Conway"); cities.insert("Carlisle", "Russellville"); cities.insert("Alma", "Carlisle"); create(); } document.getElementById("test2").onclick = function() { cities.remove("Russellville"); create() } </script> |