javascript數據結構與算法--鏈表


鏈表與數組的區別?

 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了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM