鏈表與數組的區別?
1. 定義:
數組又叫做順序表,順序表是在內存中開辟一段連續的空間來存儲數據,數組可以處理一組數據類型相同的數據,但不允許動態定義數組的大小,即在使用數組之前必須確定數組的大小。而在實際應用中,用戶使用數組之前有時無法准確確定數組的大小,只能將數組定義成足夠大小,這樣數組中有些空間可能不被使用,從而造成內存空間的浪費。
鏈表是一種常見的數據組織形式,它采用動態分配內存的形式實現。鏈表是靠指針來連接多塊不連續的的空間,在邏輯上形成一片連續的空間來存儲數據。需要時可以用new分配內存空間,不需要時用delete將已分配的空間釋放,不會造成內存空間的浪費。
2. 二者區分;
A 從邏輯結構來看
- 數組必須事先定義固定的長度,不能適應數據動態地增減情況。當數據增加時,可能超出數組原先定義的數組的長度;當數據減少時,浪費內存。
- 鏈表可以動態地進行存儲分配;可以適應數據動態增減情況;
B 從內存存儲來看
- 數組是從棧中分配空間,對於程序員方便快速,自由度小。
- 鏈表是從堆中分配空間,自由度大但是申請管理比較麻煩。
C 從訪問順序來看
數組中的數據是按順序來存儲的,而鏈表是隨機存儲的。
- 要訪問數組的元素需要按照索引來訪問,速度比較快,如果對他進行插入刪除操作的話,就得移動很多元素,所以對數組進行插入操作效率低。
- 由於鏈表是隨機存儲的,鏈表在插入,刪除操作上有很高的效率(相對數組),如果要訪問鏈表中的某個元素的話,那就得從鏈表的頭逐個遍歷,直到找到所需要的元素為止,所以鏈表的隨機訪問的效率比數組要低。
注意: 以上的區分在其他的編程語言 “數組和鏈表”的區別或許確實是這樣的,但是在javascript的數組中並不存在上面的問題,因為 javascript有push,pop,shift,unshift,split等方法,所以不需要再訪問數組中其他的元素了。
Javascript中數組的主要問題是:它們被實現成了對象,與其他語言(比如c++和java)的數組相比,效率很低。如果發現使用數組很慢的話,可以使用鏈表來替代它。至於在javascript中,一般情況下還是使用數組比較方便,我個人建議使用數組,但是現在我們還是要介紹下鏈表的基本概念,至少我們有一個理念,什么是鏈表,這個我們應該要知道的。所以下面我們來慢慢來分析鏈表的基本原理了。
一:定義鏈表
鏈表是由一組節點組成的集合。每個節點都使用一個對象的引用指向它的后繼。指向另一個節點的引用叫做鏈。如下圖一:
數組元素靠他們位置的索引進行引用,而鏈表元素則是靠相互之間的關系進行引用。如上圖一:我們說B跟在A的后面,而不是和數組一樣說B是鏈表中的第二個元素。遍歷鏈表,就是跟着鏈接,從鏈表的首元素一直遍歷到尾元素(不包含鏈表的頭節點,頭節點一般用來作為鏈表的接入點)。而鏈表的尾元素指向null 如上圖1.
二:單向鏈表插入新節點和刪除一個節點的原理;
1. 單向鏈表插入新節點,如上圖2所示;鏈表中插入一個節點效率很高。向鏈表中插入一個節點,需要修改它前面的節點(前驅),使其指向新加入的節點,而新加入的節點則指向原來前驅指向的節點。
2. 單向鏈表刪除一個節點;如上圖3所示;從鏈表中刪除一個元素也很簡單,將待刪除元素的前驅節點指向待刪除元素的后繼節點,同時將待刪除元素指向null,元素就刪除成功了。
三:設計一個基於對象的鏈表。
1 先設計一個創建節點Node類;如下:
function Node(element) { this.element = element; this.next = null; }
Node類包含2個屬性,element用來保存節點上的數據,next用來保存指向下一個節點的鏈接(指針)。
2. 再設計一個對鏈表進行操作的方法,包括插入刪除節點,在列表中查找給定的值等。
function LinkTable () { this.head = new Node(“head”); }
上面的鏈表類只有一個屬性,那就是使用一個Node對象來保存該鏈表的頭節點。
一:插入新節點insert方法步驟如下;
- 需要明確知道新節點要在那個節點前面或者后面插入。
- 在一個已知節點后面插入元素時,先要找到后面的節點。
因此在創建新節點之前,先要創建查找節點的方法 find,如下:
function find(item){ var curNode = this.head; while(curNode.element != item) { curNode = curNode.next; } return curNode; }
如上代碼的意思:首先創建一個新節點,並將鏈表的頭節點賦給這個新創建的節點curNode,然后再鏈表上進行循環,如果當前節點的element屬性和我們要找的信息不符合,就從當前的節點移動到下一個節點,如果查找成功,該方法返回包含該數據的節點,否則的話 返回null。
一旦找到 “后面”的節點了,就可以將新節點插入到鏈表中了。首先將新節點的next屬性設置為 “后面”節點的next屬性對應的值。然后設置 “后面”節點的next屬性指向新節點。Insert方法定義如下:
function insert(newElement,item) { var newNode = new Node(newElement); var current = this.find(item); newNode.next = current.next; newNode.previous = current; current.next = newNode; }
二:定義一個顯示鏈表中的元素。
function display (){ var curNode = this.head; while(!(curNode.next == null)) { console.log(curNode.next.element); curNode = curNode.next; } }
該方法先將列表的頭節點賦給一個變量curNode,然后循環遍歷列表,如果當前節點的next屬性為null時,則循環結束。
下面是添加節點的所有JS代碼;如下:
function Node(element) { this.element = element; this.next = null; } function LinkTable() { this.head = new Node("head"); } LinkTable.prototype = { find: function(item){ var curNode = this.head; while(curNode.element != item) { curNode = curNode.next; } return curNode; }, insert: function(newElement,item) { var newNode = new Node(newElement); var current = this.find(item); newNode.next = current.next; current.next = newNode; }, display: function(){ var curNode = this.head; while(!(curNode.next == null)) { console.log(curNode.next.element); curNode = curNode.next; } } }
我們可以先來測試如上面的代碼;
如下初始化;
var test = new LinkTable();
test.insert("a","head");
test.insert("b","a");
test.insert("c","b");
test.display();
1 執行test.insert("a","head"); 意思是說把a節點插入到頭節點 head的后面去,執行到上面的insert方法內中的代碼 var current = this.find(item); item就是頭節點head傳進來的;那么變量current值是 截圖如下:
繼續走到下面 newNode.next = current.next; 給新節點newNode的next屬性指向null,繼續走,current.next = newNode; 設置后面的節點next屬性指向新節點a;如下:
2. 同上面原理一樣,test.insert("b","a"); 我們接着走 var current = this.find(item);
那么現在的變量current值是如下:
繼續走 newNode.next = current.next; 給新節點newNode的next屬性指向null,繼續走,current.next = newNode; 設置后面的節點next屬性指向新節點b;如下:
test.insert("c","b"); 在插入一個c 原理也和上面執行一樣,所以不再一步一步講了,所以最后執行 test.display();方法后,將會打印出a,b,c
三:從鏈表中刪除一個節點;
原理是:從鏈表中刪除節點時,需要先找到待刪除節點前面的節點。找到這個節點后,修改它的next屬性使其不再指向待刪除的節點,而是指向待刪除節點的下一個節點。如上面的圖三所示:
現在我們可以定義一個方法 findPrevious()。該方法遍歷鏈表中的元素,檢查每一個節點的下一個節點中是否存儲着待刪除數據,如果找到的話,返回該節點,這樣就可以修改它的next屬性了。如下代碼:
function findPrevious (item) { var curNode = this.head; while(!(curNode.next == null) && (curNode.next.element != item)) { curNode = curNode.next; } return curNode; }
現在我們可以編寫singleRemove方法了,如下代碼:
function singleRemove(item) { var prevNode = this.findPrevious(item); if(!(prevNode.next == null)) { prevNode.next = prevNode.next.next; } }
下面所有的JS代碼如下:
function Node(element) { this.element = element; this.next = null; } function LinkTable() { this.head = new Node("head"); } LinkTable.prototype = { find: function(item){ var curNode = this.head; while(curNode.element != item) { curNode = curNode.next; } return curNode; }, insert: function(newElement,item) { var newNode = new Node(newElement); var current = this.find(item); newNode.next = current.next; current.next = newNode; }, display: function(){ var curNode = this.head; while(!(curNode.next == null)) { console.log(curNode.next.element); curNode = curNode.next; } }, findPrevious: function(item) { var curNode = this.head; while(!(curNode.next == null) && (curNode.next.element != item)) { curNode = curNode.next; } return curNode; }, singleRemove: function(item) { var prevNode = this.findPrevious(item); if(!(prevNode.next == null)) { prevNode.next = prevNode.next.next; } } }
下面我們再來測試下代碼,如下測試;
var test = new LinkTable(); test.insert("a","head"); test.insert("b","a"); test.insert("c","b"); test.display(); test.singleRemove("a"); test.display();
當執行 test.singleRemove("a"); 刪除鏈表a時,執行到singleRemove方法內的var prevNode = this.findPrevious(item); 先找到前面的節點head,如下:
然后在singleRemove方法內判斷上一個節點head是否有下一個節點,如上所示,很明顯有下一個節點,那么就把當前節點的下一個節點 指向 當前的下一個下一個節點,那么當前的下一個節點就被刪除了,如下所示:
二:雙向鏈表
雙向鏈表圖,如下圖一所示:
前面我們介紹了是單向鏈表,在Node類里面定義了2個屬性,一個是element是保存新節點的數據,還有一個是next屬性,該屬性指向后驅節點的鏈接,那么現在我們需要反過來,所以我們需要一個指向前驅節點的鏈接,我們現在把他叫做previous。Node類代碼現在改成如下:
function Node(element) { this.element = element; this.next = null; this.previous = null; }
1. 那么雙向鏈表中的insert()方法和單向鏈表的方法類似,但是需要設置新節點previous屬性,使其指向該節點的前驅。代碼如下:
function insert(newElement,item) { var newNode = new Node(newElement); var current = this.find(item); newNode.next = current.next; newNode.previous = current; current.next = newNode; }
2. 雙向鏈表的doubleRemove() 刪除節點方法比單向鏈表的效率更高,因為不需要再查找前驅節點了。那么雙向鏈表的刪除原理如下:
1. 首先需要在鏈表中找出存儲待刪除數據的節點,然后設置該節點前驅的next屬性,使其指向待刪除節點的后繼。
2. 設置該節點后繼的previous屬性,使其指向待刪除節點的前驅。
如上圖2所示;首先在鏈表中找到刪除節點C,然后設置該C節點前驅的B的next屬性,那么B指向尾節點Null了;設置該C節點后繼的(也就是尾部節點Null)的previous屬性,使尾部Null節點指向待刪除C節點的前驅,也就是指向B節點,即可把C節點刪除掉。
代碼可以如下:
function doubleRemove(item) { var curNode = this.find(item); if(!(curNode.next == null)) { curNode.previous.next = curNode.next; curNode.next.previous = curNode.previous; curNode.next = null; curNode.previous = null; } }
比如測試代碼如下:
var test = new LinkTable(); test.insert("a","head"); test.insert("b","a"); test.insert("c","b"); test.display(); // 打印出a,b,c console.log("------------------"); test.doubleRemove("b"); // 刪除b節點 test.display(); // 打印出a,c console.log("------------------");
進入doubleRemove方法,先找到待刪除的節點,如下:
然后設置該節點前驅的next屬性 ,使其指向待刪除節點的后繼,如上代碼
curNode.previous.next = curNode.next;
該節點的后繼的previous屬性,使其指向待刪除節點的前驅。如上代碼
curNode.next.previous = curNode.previous;
再設置 curNode.next = null; 再查看curNode值如下圖所示:
下一個節點為null,同理當設置完 curNode.previous = null 的時候,會打印出如下:
我們可以再看看如上圖2 刪除節點的圖 就可以看到,C節點與它的前驅節點B,與它的尾節點都斷開了。即都指向null。
注意:雙向鏈表中刪除節點貌似不能刪除最后一個節點,比如上面的C節點,為什么呢?因為當執行到如下代碼時,就不執行了,如下圖所示;
上面的是刪除節點的分析,現在我們再來分析下 雙向鏈表中的 添加節點的方法insert(); 我們再來看下;
當執行到代碼 test.insert("a","head"); 把a節點插入到頭部節點后面去,我們來看看insert方法內的這一句代碼;
newNode.previous = current; 如下所示;
當執行到如下這句代碼時候;
current.next = newNode;
如下所示;
同理插入b節點,c節點也類似的原理。
雙向鏈表反序操作;現在我們也可以對雙向鏈表進行反序操作,現在需要給雙向鏈表增加一個方法,用來查找最后的節點。如下代碼;
function findLast(){ var curNode = this.head; while(!(curNode.next == null)) { curNode = curNode.next; } return curNode; }
如上findLast()方法就可以找到最后一個鏈表中最后一個元素了。現在我們可以寫一個反序操作的方法了,如下:
function dispReverse(){ var curNode = this.head; curNode = this.findLast(); while(!(curNode.previous == null)) { console.log(curNode.element); curNode = curNode.previous; } }
如上代碼先找到最后一個元素,比如C,然后判斷當前節點的previous屬性是否為空,截圖如下;
可以看到當前節點的previous不為null,那么執行到console.log(curNode.element); 先打印出c,然后把當前的curNode.previous 指向與curNode了(也就是現在的curNode是b節點),如下所示;
同理可知;所以分別打印出c,b,a了。