連結串列與陣列的區別?
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了。