數據結構淺析:鏈表

數據結構淺析:鏈表

你有修建過戈德堡機械嗎承璃?如果沒有,也許應該修建過精心構建的多米諾骨牌線蚌本?

好的盔粹,也許你的童年應該不像我一樣書呆子。就這樣吧程癌,對于玩過上述兩者中任意一樣的人玻佩,你已經掌握了今天數據結構的本質:鏈表!

鏈表是怎么工作的

鏈表的最簡單實現形式—單鏈表--是一系列的節(jié)點席楚,每一個單獨的節(jié)點都包含一個值和一個指向鏈表中下一個節(jié)點的指針咬崔。

添加(Add)通過在表的尾部添加一個項目增加列表的大小。

移除(Remove)通常移除鏈表中指定位置的元素烦秩。

搜索(Contains)將在列表中搜索一個指定的值垮斯。

使用例子:

  • 哈希表中為了防止出現沖突,將 values 存儲在一個列表中

  • 重拍極速前進(The Amazing Race

    ?

為了讓這篇文章保持輕松友好一些只祠,我們一起來創(chuàng)建一個 CBS 可用于計劃其下一季“極速前進”真人秀拍攝的工具兜蠕。

在閱讀本文時,我希望你能夠一直問自己:“鏈表和數組有什么區(qū)別抛寝?它們有什么相似之處熊杨?”

我們開始吧。

首先盗舰,你需要創(chuàng)建一個鏈表:

class LinkedList{
  constructor(){
    this._head = null;
    this._tail = null;
    this._length = 0;
  }
  size(){
    return this._length;
  }
}

為了跟蹤比賽的起點和終點晶府,需要創(chuàng)建 head 和 tail 屬性。

然后钻趋,為了保證賽程不要太長或者太短川陆,需要創(chuàng)建 length 屬性和 size 方法。這樣你就總是可以精確的掌握賽程的長度蛮位。

現在你有了一種存儲數據的方法较沪,你應該創(chuàng)建一個往列表添加數據的方法。問題是失仁,你要添加什么數據呢尸曼?

記住,鏈表是一系列的節(jié)點萄焦,每一個節(jié)點都有一個值和指向列表中下一個節(jié)點的指針控轿。了解了這一點,你會意識到節(jié)點就是一個存儲有 value 和 next 兩個屬性的對象。

由于每一次往列表添加數據都需要創(chuàng)建一個新的節(jié)點解幽,你決定創(chuàng)建一個構造函數,這樣每次往列表添加數據時創(chuàng)建節(jié)點會更簡單一些烘苹。

class Node{
  constructor(value){
    this.value = value;
    this.next = null;
  }
}

有了構造函數躲株,就可以創(chuàng)建 add 方法。

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class LinkedList {
 
  constructor() {
    this._head = null;
    this._tail = null;
    this._length = 0;
  }
  
  add(value) {
    let node = new Node(value);         //we create our node
    if(!this._head && !this._tail){     //If it's the first node
      this._head = node;                //1st node is head & tail
      this._tail = node;
    }else{
    this._tail.next = node;             //add node to the back
    this._tail = this._tail.next;       //reset tail to last node
    }
    this._length++;
  }
  
  size() {
    return this._length;
  }
}

const AmazingRace = new LinkedList();
AmazingRace.add("Colombo, Sri Lanka");
AmazingRace.add("Lagos, Nigeria");
AmazingRace.add("Surat, India");
AmazingRace.add("Suzhou, China");

現在你已經添加了這個方法镣衡,你可以往 “AmazingRace” 列表中添加一堆比賽地點霜定。看起來像這樣廊鸥。注意為了更容易理解我添加了一些額外的空格望浩。

{ _head: 
   { value: 'Colombo, Sri Lanka',
     next: { value: 'Lagos, Nigeria', 
             next: { value: 'Surat, India',
                     next: { value: 'Suzhou, China',
                             next: null 
                           }
                   }
           } 
   },
  _tail: { value: 'Suzhou, China', next: null },
  _length: 4 
}

好啦,現在已經創(chuàng)建了列表以及添加內容的方法惰说,你意識到往列表中添加地點還需要一些方法磨德。

你決定將這個鏈表共享給合作者,Kent吆视,要求他往里面添加更多的比賽地點典挑。唯一的問題是,當你將列表給他時啦吧,沒有告訴他你已經添加了哪些地點您觉。不幸的是你也忘記了。

當然他可以通過運行 console.log(AmazingRace) 然后逐一檢查其輸出授滓。但是 Kent 是一個懶人琳水,他需要一種方式來確認某些值是否已經存在列表中,從而避免重復添加般堆≡谛ⅲ考慮到這一點,你創(chuàng)建了 contains 方法來檢查是否包含某些值淮摔。

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}
class LinkedList {
 
  constructor() {
    this._head = null;
    this._tail = null;
    this._length = 0;
  }
  
  add(value) {
    let node = new Node(value);         
    if(!this._head && !this._tail){     
      this._head = node;                
      this._tail = this._head;
    }else{
    this._tail.next = node;             
    this._tail = this._tail.next;       
    }
    this._length++;
  }
  
  contains(value){
    let node = this._head;
    while(node){
      if(node.value === value){
        return true;
      }
      node = node.next;
    }
    return false;
  }
  
  size() {
    return this._length;
  }
  
}

const AmazingRace = new LinkedList();
AmazingRace.add("Colombo, Sri Lanka");
AmazingRace.add("Lagos, Nigeria");
AmazingRace.add("Surat, India");
AmazingRace.add("Suzhou, China");
//Kent's check
AmazingRace.contains('Suzhou, China'); //true
AmazingRace.contains('Hanoi, Vietnam'); //false
AmazingRace.add('Hanoi, Vietnam');
AmazingRace.contains('Seattle, Washington'); //false
AmazingRace.add('Seattle, Washington');
AmazingRace.contains('North Pole'); // false
AmazingRace.add('North Pole');

很棒浑玛,現在 Kent 為了避免重復,他可以在添加之前檢查一下是否已經存在噩咪。

另一方面顾彰,你可能會想為什么不在添加之前進行檢查從而避免重復呢?當你在實現一個鏈表—或者任意的數據結構時— 理論上你可以添加任何額外你想要的功能胃碾。

你甚至可以改變已有數據結構中原來的方法涨享。在 REPL 中試一下以下代碼:

Array.prototype.push = () => {
 return 'cat';
}
let arr = [];
arr.push('eggs'); // returns 'cat';

我們沒有做這些事情的原因是因為約定的標準∑桶伲基本上厕隧,開發(fā)者對特定的方法應該如何工作是有預期的。

盡管我們的鏈表不是 JavaScript 的原生庫,我們在實現它時擁有更多的自由吁讨,但是這些數據結構如何運作我們仍有基本的期望髓迎。鏈表本身并不保證存儲的值唯一。但是它們確實可以提供 contains 這樣的方法允許我們進行預檢建丧,從而維護我們列表中的值不重復排龄。

Kent 將他修改后的列表再發(fā)給你,但是其中一些可能有問題翎朱。比如北極可能不是“極速前進”最好的終點橄维。

所以你決定創(chuàng)建一個可以移除某些節(jié)點的方法。一定要記住的是拴曲,一旦你刪除了某一個節(jié)點争舞,你就打斷了列表,你需要將被刪除節(jié)點的前后兩個節(jié)點重新連接起來澈灼。

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}
class LinkedList {
 
  constructor() {
    this._head = null;
    this._tail = null;
    this._length = 0;
  }
  
  add(value) {
    let node = new Node(value);         
    if(!this._head && !this._tail){     
      this._head = node;                
      this._tail = this._head;
    }else{
    this._tail.next = node;             
    this._tail = this._tail.next;       
    }
    this._length++;
  }
  
  remove(value) {
    if(this.contains(value)){          // see if our value exists
      let current = this._head;           // begin at start of list
      let previous = this._head;
        while(current){                   // check each node
          if(current.value === value){
            if(this._head === current){   // if it's the head
              this._head = this._head.next;  // reset the head
              this._length--;              // update the length
              return;                      // break out of the loop
            }
            if(this._tail === current){   // if it's the tail node
              this._tail = previous;       // make sure to reset it
            }
            previous.next = current.next;  // unlink (see img below)
            this._length--;            // update the length
            return;                    // break out of 
          }
          previous = current;          // look at the next node
          current = current.next;      // ^^
        }
     }  
  }  
  
  contains(value){
    let node = this._head;
    while(node){
      if(node.value === value){
        return true;
      }
      node = node.next;
    }
    return false;
  }
  
  size() {
    return this._length;
  }  
}
const AmazingRace = new LinkedList();
AmazingRace.add("Colombo, Sri Lanka");
AmazingRace.add("Lagos, Nigeria");
AmazingRace.add("Surat, India");
AmazingRace.add("Suzhou, China");
AmazingRace.add('Hanoi, Vietnam');
AmazingRace.add('Seattle, Washington');
AmazingRace.add('North Pole');
//Kent's check
AmazingRace.remove('North Pole');

remove 方法的代碼比較長竞川。本質上它按照如下步驟運行:

1、檢查待刪除的值是否存在叁熔。流译。。

2者疤、遍歷列表福澡,并且需要跟蹤當前節(jié)點和上一個節(jié)點

3、然后驹马,如果當前節(jié)點就是要刪除的點—>

4A革砸、當前節(jié)點是鏈表的頭

  • 將鏈表的頭重置為列表中的下一個節(jié)點
  • 更新鏈表長度
  • 跳出循環(huán)
    4B、當前節(jié)點是鏈表的尾
  • 將鏈表的尾設置為鏈表中的上一個節(jié)點
  • 按照下圖的方式重新連接節(jié)點

  • 4C糯累、如果不是要刪除的節(jié)點—>繼續(xù)迭代

  • 將下一個節(jié)點設置為當前節(jié)點
  • 將當前節(jié)點設置為上一個節(jié)點

最后要說說明的一件事:你可能已經注意到你并沒有真正的刪除那個節(jié)點算利。你只是移除了對它的引用。對泳姐,這樣就可以了效拭,因為一旦對一個對象的所有引用都被移除以后,垃圾回收器會幫助我們將它從內存中移除的胖秒。更多關于垃圾回收的信息參考這里

有了你剛才實現的 remove 方法缎患,你只需下面這一行就可以保證參賽者不會凍死、或者打擾在正在準備新年慶典的圣誕老人阎肝。

AmazingRace.remove('North Pole');

好了挤渔,你已經完成了一個單鏈表的簡單實現。你可通過添加元素來擴充列表风题,也可以通過移除元素來壓縮列表判导。

如果你想在鏈表的首部嫉父、尾部或者任意節(jié)點后插入元素。你需要自己實現這些方法眼刃。這些方法的名稱和參數應該類似這樣:

addHead(value) {
}
insertAfter(target, value){
}

時間復雜度分析

再來看一下完整的代碼

class LinkedList {
 
  constructor() {
    this._head = null;
    this._tail = null;
    this._length = 0;
  }
  
  add(value) {
    let node = new Node(value);         
    if(!this._head && !this._tail){     
      this._head = node;                
      this._tail = this._head;
    }else{
    this._tail.next = node;             
    this._tail = this._tail.next;       
    }
    this._length++;
  }
  
  remove(value) {
    if(this.contains(value)){          
      let current = this._head;        
      let previous = this._head;
        while(current){         
          if(current.value === value){
            if(this._head === current){ 
              this._head = this._head.next;
              this._length--;              
              return;                      
            }
            if(this._tail === current){ 
              this._tail = previous;    
            }
            previous.next = current.next;
            this._length--;            
            return;                    
          }
          previous = current;          
          current = current.next;      
        }
     }  
  }  
 
  contains(value){
    let node = this._head;
    while(node){
      if(node.value === value){
        return true;
      }
      node = node.next;
    }
    return false;
  }
  
  size() {
    return this._length;
  }

// To Be Implemented
addHead(value) {
}
insertAfter(target, value){
}

Add 的復雜度是O(1):因為有 tail 屬性跟蹤隊列的尾部绕辖,所以你不必遍歷整個列表。

Remove 的復雜度是 O(n): 最壞的情況下擂红,你需要遍歷完整個列表才能找到你需要移除的節(jié)點仪际。盡管真正的移除節(jié)點的操作是 O(1)(因為你只需要重置一下指針就可以)。

Contains 的復雜度是 O(n):為了確認列表中是否包含指定的值篮条,你必須遍歷整個列表。

addHead 的復雜度是 O(1):和我們的 add 方法相似吩抓,我們總是知道列表的頭部涉茧,所以不需要遍歷。

insertAfter 的復雜度是 O(n):和上面討論的 Remove 方法一樣疹娶,你需要遍歷整個列表找到你需要插入的位置伴栓。同樣的,實際的插入操作復雜度為 O(1) 雨饺。

鏈表 VS 數組

為什么要使用鏈表而不是數組钳垮?技術上數組可以運行所有鏈表操作,如 添加额港、插入饺窿、刪除。此外移斩, JavaScript 中數組已經具備這些方法肚医。

最大的差別來自插入和刪除。因為數組是基于索引向瓷,當你在數組的中間插入或者刪除元素時肠套,你需要將其后面所有的元素重置到新的索引位置。

想象一下猖任,在一個有 100,000 個元素的頭部或者中間插入一個元素你稚!像這樣的插入和刪除操作是非常耗費資源的。因為這一點朱躺,鏈表通常用于常常被移動的大型數據集刁赖。

另一方面,數組在查找方面非常出色长搀,因為它是基于索引的痒给。如果你知道元素的位置,你可以通過 array[position] 在 O(1) 時間內查找到該元素戚篙。

鏈表通常需要順序遍歷列表撒汉〗文疲基于這一原因,數組常常用于小一些的數據集或者不常移動的數據集病苗。

小節(jié)

鏈表:

  • 1疗垛、有 tail 和 head 屬性用于跟蹤鏈表的兩端
  • 2、有 add硫朦, addHead贷腕,insertAfter 和 remove 方法用于管理列表內容
  • 3、有 length 屬性用過跟蹤列表長度

本文譯自 :A Gentle Introduction to Data Structures: How Linked Lists Work

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末咬展,一起剝皮案震驚了整個濱河市泽裳,隨后出現的幾起案子,更是在濱河造成了極大的恐慌破婆,老刑警劉巖涮总,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異祷舀,居然都是意外死亡瀑梗,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門裳扯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抛丽,“玉大人,你說我怎么就攤上這事饰豺∫谙剩” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵冤吨,是天一觀的道長狡门。 經常有香客問我,道長锅很,這世上最難降的妖魔是什么其馏? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮爆安,結果婚禮上叛复,老公的妹妹穿的比我還像新娘。我一直安慰自己扔仓,他們只是感情好褐奥,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著翘簇,像睡著了一般撬码。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上版保,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天呜笑,我揣著相機與錄音夫否,去河邊找鬼。 笑死叫胁,一個胖子當著我的面吹牛凰慈,可吹牛的內容都是我干的。 我是一名探鬼主播驼鹅,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼微谓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了输钩?” 一聲冷哼從身側響起豺型,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎买乃,沒想到半個月后姻氨,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡为牍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年哼绑,在試婚紗的時候發(fā)現自己被綠了岩馍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碉咆。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蛀恩,靈堂內的尸體忽然破棺而出疫铜,到底是詐尸還是另有隱情,我是刑警寧澤双谆,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布壳咕,位于F島的核電站,受9級特大地震影響顽馋,放射性物質發(fā)生泄漏谓厘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一寸谜、第九天 我趴在偏房一處隱蔽的房頂上張望竟稳。 院中可真熱鬧,春花似錦熊痴、人聲如沸他爸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诊笤。三九已至,卻和暖如春巾陕,著一層夾襖步出監(jiān)牢的瞬間讨跟,已是汗流浹背纪他。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留许赃,地道東北人止喷。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像混聊,于是被迫代替她去往敵國和親弹谁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理句喜,服務發(fā)現预愤,斷路器,智...
    卡卡羅2017閱讀 134,693評論 18 139
  • 鏈表(Linked-list) 前面我們討論了如何使用棧咳胃、隊列進行存數數據植康,他們其實都是列表的一種,底層存儲的數據...
    Cryptic閱讀 38,826評論 7 57
  • 1 序 2016年6月25日夜展懈,帝都销睁,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照存崖,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,107評論 0 12
  • 有時候人的眼睛冻记,看世間、看萬物来惧、看他人冗栗,就是看不到自己; 能看到別人過失供搀,卻看不到自己的缺點隅居; 能看到別人的貪婪,...
    醫(yī)成道人閱讀 220評論 0 0
  • 券商:三年不開張涕蚤,開張吃三年 券商最突出的特征就是與市場行情高度相關,熊市表現一般摄悯,牛市則一鳴驚人赞季,正所謂三年不開...
    鹿西西的仙人閱讀 22,076評論 1 7