javascript資料結構與演算法--連結串列

龍恩0707發表於2015-03-14

連結串列與陣列的區別?

 1. 定義:

     陣列又叫做順序表,順序表是在記憶體中開闢一段連續的空間來儲存資料,陣列可以處理一組資料型別相同的資料,但不允許動態定義陣列的大小,即在使用陣列之前必須確定陣列的大小。而在實際應用中,使用者使用陣列之前有時無法準確確定陣列的大小,只能將陣列定義成足夠大小,這樣陣列中有些空間可能不被使用,從而造成記憶體空間的浪費。

     連結串列是一種常見的資料組織形式,它採用動態分配記憶體的形式實現。連結串列是靠指標來連線多塊不連續的的空間,在邏輯上形成一片連續的空間來儲存資料。需要時可以用new分配記憶體空間,不需要時用delete將已分配的空間釋放,不會造成記憶體空間的浪費。

2. 二者區分

     A 從邏輯結構來看

  1. 陣列必須事先定義固定的長度,不能適應資料動態地增減情況。當資料增加時,可能超出陣列原先定義的陣列的長度;當資料減少時,浪費記憶體。
  2. 連結串列可以動態地進行儲存分配;可以適應資料動態增減情況;

     B 從記憶體儲存來看

  1. 陣列是從棧中分配空間,對於程式設計師方便快速,自由度小。
  2. 連結串列是從堆中分配空間,自由度大但是申請管理比較麻煩。

     C 從訪問順序來看

     陣列中的資料是按順序來儲存的,而連結串列是隨機儲存的。

  1. 要訪問陣列的元素需要按照索引來訪問,速度比較快,如果對他進行插入刪除操作的話,就得移動很多元素,所以對陣列進行插入操作效率低。
  2. 由於連結串列是隨機儲存的,連結串列在插入,刪除操作上有很高的效率(相對陣列),如果要訪問連結串列中的某個元素的話,那就得從連結串列的頭逐個遍歷,直到找到所需要的元素為止,所以連結串列的隨機訪問的效率比陣列要低。

 注意: 以上的區分在其他的程式語言 “陣列和連結串列”的區別或許確實是這樣的,但是在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方法步驟如下;

  1. 需要明確知道新節點要在那個節點前面或者後面插入。
  2. 在一個已知節點後面插入元素時,先要找到後面的節點。

 因此在建立新節點之前,先要建立查詢節點的方法 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了。

相關文章