第5章 鏈表 (LinkedList)

每種編程語(yǔ)言都實(shí)現(xiàn)了數(shù)組武鲁,但在大多數(shù)語(yǔ)言中灭翔,數(shù)組大小是固定的(創(chuàng)建時(shí)指定)嵌言,從數(shù)組起點(diǎn)或中間插入或移除元素的成本很高嗅回,因?yàn)楹竺娴脑囟夹枰€(gè)挪位置。

1. 鏈表數(shù)據(jù)結(jié)構(gòu)

鏈表是一種動(dòng)態(tài)的數(shù)據(jù)結(jié)構(gòu)摧茴,用于存儲(chǔ)有序的元素集合绵载,可以從中任意添加或移除元素,不需要移動(dòng)其他元素苛白,可按需擴(kuò)容(如眾人手拉手娃豹,加個(gè)人和減個(gè)人都很簡(jiǎn)單),鏈表中的元素并不是連續(xù)放置的购裙,每個(gè)元素由一個(gè)存儲(chǔ)元素本身的節(jié)點(diǎn)懂版,和一個(gè)指向下一個(gè)元素的引用(也稱指針或鏈接)組成:


image.png
image.png
image.png
image.png

數(shù)組可以直接訪問(wèn)任何位置的元素(可以理解為是物理電路,訪問(wèn)任意位置的元素都一樣快)躏率,而想要訪問(wèn)鏈表中間的元素躯畴,就需要從表頭開(kāi)始迭代鏈表中的節(jié)點(diǎn)民鼓,挨個(gè)尋找,就像藏寶游戲蓬抄,不斷地根據(jù)線索尋找摹察。
各自優(yōu)勢(shì):

  • 鏈表:增、刪
  • 數(shù)組:改倡鲸、查

1.1 創(chuàng)建鏈表

  • append(element):向列表尾部添加一個(gè)新的項(xiàng)供嚎。
  • insert(position, element):向列表的特定位置插入一個(gè)新的項(xiàng)。
  • remove(element):從列表中移除一項(xiàng)峭状。
  • indexOf(element):返回元素在列表中的索引克滴。如果列表中沒(méi)有該元素則返回-1。
  • removeAt(position):從列表的特定位置移除一項(xiàng)优床。
  • isEmpty():如果鏈表中不包含任何元素劝赔,返回true,如果鏈表長(zhǎng)度大于0則返回false胆敞。
  • size():返回鏈表包含的元素個(gè)數(shù)着帽。與數(shù)組的length屬性類似。
  • toString():由于列表項(xiàng)使用了Node類移层,就需要重寫繼承自JavaScript對(duì)象默認(rèn)的toString方法仍翰,讓其只輸出元素的值。
function LinkedList() {

  // 單個(gè)節(jié)點(diǎn)類观话,表示要加入到鏈表中的項(xiàng)
  let Node = function (element) {
    this.element = element;
    this.next = null;
  };

  let length = 0; // 內(nèi)部私有變量予借,記錄鏈表長(zhǎng)度
  let head = null; // 頭指針

  /**
   * 向鏈表尾部添加一個(gè)新的項(xiàng)
   * 兩種場(chǎng)景:鏈表為空,添加的是第一個(gè)元素频蛔;鏈表不為空灵迫,向其追加元素。
   *
   * @param element
   */
  this.append = function (element) {

    let node = new Node(element), current;

    if (head === null) {
      head = node;
    } else {
      current = head;
      // 從表頭開(kāi)始循環(huán)找到最后一項(xiàng)
      while (current.next !== null) {
        current = current.next;
      }
      // 把節(jié)點(diǎn)鏈接到最后一項(xiàng)的 next 上
      current.next = node;
    }

    length++; // 更新鏈表長(zhǎng)度
  };

  /**
   * 從任意位置移除元素并返回被移除的元素
   * 兩種場(chǎng)景:移除第一個(gè)元素晦溪;移除第一個(gè)以外的任一元素瀑粥。
   * 被移除的元素被丟棄在計(jì)算機(jī)內(nèi)存中,等待被垃圾回收器清理三圆。
   *
   * @param {number} position
   * @returns {element|null}
   */
  this.removeAt = function (position) {

    // 檢查是否越界
    if (position >= 0 && position < length) {

      let current = head,
        previous,
        index = 0;

      // 分兩種情況:表頭和非表頭
      if (position === 0) {
        head = current.next;
      } else {
        // position 的前一個(gè)位置時(shí)終止循環(huán)
        while (index++ < position) {
          previous = current;
          current = current.next;
        }
        // 上下節(jié)點(diǎn)進(jìn)行鏈接狞换,跳過(guò)中間將被移除的 current 節(jié)點(diǎn)
        previous.next = current.next;
      }

      length--;

      return current.element;
    } else {
      return null;
    }
  };

  /**
   * 在任意位置插入元素
   *
   * @param {number} position
   * @param element
   * @returns {boolean}
   */
  this.insert = function (position = 0, element) {

    // 檢查是否越界,注意這里包含了空鏈表時(shí)的情形
    if (position >= 0 && position <= length) {

      let node = new Node(element),
        current = head,
        previous,
        index = 0;

      if (position === 0) {
        node.next = current;
        head = node;
      } else {
        while (index++ < position) {
          previous = current;
          current = current.next;
        }
        node.next = current; // 即新節(jié)點(diǎn)插入到目標(biāo)位置的前面,這里current可能是null
        previous.next = node;
      }

      length++;

      return true;
    } else {
      return false;
    }
  };

  /**
   * 把 LinkedList 對(duì)象轉(zhuǎn)換成字符串
   *
   * @returns {string}
   */
  this.toString = function () {
    let current = head,
      string = '',
      index = 0;

    while (current) {
      string += index++ + ': ' + current.element + (current.next ? '\n' : '');
      current = current.next;
    }
    return string;
  };

  /**
   * 查找給定元素的索引,找不到則返回 -1
   *
   * @param element
   * @returns {number}
   */
  this.indexOf = function (element) {

    let current = head,
      index = -1;

    while (current) {
      index++;
      if (element === current.element) {
        return index;
      }
      current = current.next;
    }

    return -1;
  };

  /**
   * 移除給定的元素
   *
   * @param element
   * @returns {element|null}
   */
  this.remove = function (element) {
    const index = this.indexOf(element);
    return this.removeAt(index);
  };

  /**
   * 鏈表是否為空
   * @returns {boolean}
   */
  this.isEmpty = function () {
    return length === 0;
  };

  /**
   * 鏈表大小
   * @returns {number}
   */
  this.size = function () {
    return length;
  };

  /**
   * 獲取表頭
   * 方便實(shí)例外部訪問(wèn)和迭代鏈表
   *
   * @returns {element|null}
   */
  this.getHead = function () {
    return head;
  };
}

1.2 雙向鏈表

image.png
image.png
function DoublyLinkedList() {

  let Node = function (element) {
    this.element = element;
    this.next = null;
    this.prev = null;
  };

  let length = 0;
  let head = null;
  let tail = null;

  /**
   * 從任意位置移除元素
   *
   * @param position
   * @returns {element|null}
   */
  this.removeAt = function (position) {

    if (position >= 0 && position < length) {

      let current = head,
        previous,
        index = 0;

      if (position === 0) { // 第一項(xiàng)
        head = current.next;
        // 如果只有一項(xiàng)嫌术,需要更新tail
        if (length === 1) {
          tail = null;
        } else {
          head.prev = null;
        }
      } else if (position === length - 1) { // 最后一項(xiàng)
        current = tail;
        tail = current.prev;
        tail.next = null;
      } else {
        while (index++ < position) {
          previous = current;
          current = current.next;
        }
        previous.next = current.next;
        current.next.prev = previous;
      }

      length--;

      return current.element;
    } else {
      return null;
    }
  };

  /**
   * 從任意位置添加節(jié)點(diǎn)
   *
   * @param {number} position
   * @param element
   * @returns {boolean}
   */
  this.insert = function (position, element) {

    if (position >= 0 && position <= length) {

      let node = new Node(element),
        current = head,
        previous,
        index = 0;

      if (position === 0) { // 在第一個(gè)位置添加
        if (!head) { // 當(dāng)前鏈表為空
          head = node;
          tail = node;
        } else {
          node.next = current;
          current.prev = node;
          head = node;
        }
      } else if (position === length) { // 最后一項(xiàng)
        current = tail;
        current.next = node;
        node.prev = current;
        tail = node;
      } else {
        while (index++ < position) {
          previous = current;
          current = current.next;
        }
        previous.next = node;
        node.next = current;
        current.prev = node;
        node.prev = previous;
      }

      length++;

      return true;
    } else {
      return false;
    }
  };
  
  // 其他方法和單向鏈表類似
}

1.3 循環(huán)鏈表

循環(huán)鏈表 CircularLinkedList 可以只有單向引用哀澈,也可以像雙向鏈表一樣有雙向引用。
單向循環(huán)鏈表中:循環(huán)鏈表中 tail.next 指向 head度气。


image.png
image.png

雙向循環(huán)鏈表中:tail.next 指向 head割按,head.prev 指向 tail。


image.png
image.png

2. 鏈表相關(guān)問(wèn)題

2.1 判斷鏈表是否存在環(huán)形

// https://leetcode.com/problems/linked-list-cycle/

// 1. 暴力解法(brute force): 迭代節(jié)點(diǎn), 存儲(chǔ)或進(jìn)行標(biāo)記, 如果遇見(jiàn)已經(jīng)存在的記錄, 則說(shuō)明存在環(huán)形

// 2. 快慢雙指針(龜兔賽跑):
// slow速度為1, fast為2, 若fast遇見(jiàn)了slow(地球是圓的), 則說(shuō)明存在環(huán)形

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * 判斷鏈表是否存在環(huán)形
 *
 * @param {ListNode} head
 * @return {boolean}
 */
const hasCycle = function (head) {
  let slow = head,
    fast = head;

  while (fast !== null && fast.next !== null) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow === fast) {
      return true;
    }
  }
  return false;
};

2.2 求兩個(gè)鏈表交集時(shí)的節(jié)點(diǎn)

// https://leetcode.com/problems/intersection-of-two-linked-lists/

// 1. 暴力解法(brute force): 使用數(shù)組或者set存儲(chǔ)一條鏈表所有節(jié)點(diǎn),然后迭代第二條鏈表進(jìn)行查找是否存在(略)

// 2. 雙指針 two pointers
// 兩個(gè)鏈表指針同時(shí)從各自表頭移動(dòng), 速度一致, 當(dāng)較短的鏈表(A)跑到頭后, 較長(zhǎng)的鏈表(B)就開(kāi)始順便重定向頭部指針,
// 此后B的前后兩個(gè)指針之差始終等于A的總長(zhǎng)度,
// 然后等到B的后指針也跑到頭后, 再同時(shí)從A和B的左側(cè)指針開(kāi)始迭代比較是否相等,找出交點(diǎn).

// Time complexity : O(m+n)O(m+n).
// Space complexity : O(1)O(1).

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * 求兩個(gè)鏈表交集時(shí)的節(jié)點(diǎn)
 *
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode|null}
 */
const getIntersectionNode = function (headA, headB) {
  let a = headA,
    b = headB;

  while (a !== null || b !== null) {
    if (a !== null) {
      a = a.next;
    } else {
      headB = headB.next;
    }

    if (b !== null) {
      b = b.next;
    } else {
      headA = headA.next;
    }
  }

  while (headA !== headB) {
    headA = headA.next;
    headB = headB.next;
  }

  return headA;
};
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末磷籍,一起剝皮案震驚了整個(gè)濱河市适荣,隨后出現(xiàn)的幾起案子现柠,更是在濱河造成了極大的恐慌,老刑警劉巖弛矛,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件够吩,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡丈氓,警方通過(guò)查閱死者的電腦和手機(jī)周循,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)万俗,“玉大人湾笛,你說(shuō)我怎么就攤上這事∪蛲幔” “怎么了嚎研?”我有些...
    開(kāi)封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)库倘。 經(jīng)常有香客問(wèn)我临扮,道長(zhǎng),這世上最難降的妖魔是什么教翩? 我笑而不...
    開(kāi)封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任杆勇,我火速辦了婚禮,結(jié)果婚禮上迂曲,老公的妹妹穿的比我還像新娘靶橱。我一直安慰自己,他們只是感情好路捧,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著传黄,像睡著了一般杰扫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上膘掰,一...
    開(kāi)封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天章姓,我揣著相機(jī)與錄音,去河邊找鬼识埋。 笑死凡伊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的窒舟。 我是一名探鬼主播系忙,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼惠豺!你這毒婦竟也來(lái)了银还?” 一聲冷哼從身側(cè)響起风宁,我...
    開(kāi)封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蛹疯,沒(méi)想到半個(gè)月后戒财,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捺弦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年饮寞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片列吼。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡骂际,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出冈欢,到底是詐尸還是另有隱情歉铝,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布凑耻,位于F島的核電站太示,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏香浩。R本人自食惡果不足惜类缤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望邻吭。 院中可真熱鬧餐弱,春花似錦、人聲如沸囱晴。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)畸写。三九已至驮瞧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間枯芬,已是汗流浹背论笔。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留千所,地道東北人狂魔。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像淫痰,于是被迫代替她去往敵國(guó)和親最楷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348