第5章 链表 (LinkedList)

每种编程语言都实现了数组,但在大多数语言中,数组大小是固定的(创建时指定),从数组起点或中间插入或移除元素的成本很高,因为后面的元素都需要挨个挪位置。

1. 链表数据结构

链表是一种动态的数据结构,用于存储有序的元素集合,可以从中任意添加或移除元素,不需要移动其他元素,可按需扩容(如众人手拉手,加个人和减个人都很简单),链表中的元素并不是连续放置的,每个元素由一个存储元素本身的节点,和一个指向下一个元素的引用(也称指针或链接)组成:


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

数组可以直接访问任何位置的元素(可以理解为是物理电路,访问任意位置的元素都一样快),而想要访问链表中间的元素,就需要从表头开始迭代链表中的节点,挨个寻找,就像藏宝游戏,不断地根据线索寻找。
各自优势:

  • 链表:增、删
  • 数组:改、查

1.1 创建链表

  • append(element):向列表尾部添加一个新的项。
  • insert(position, element):向列表的特定位置插入一个新的项。
  • remove(element):从列表中移除一项。
  • indexOf(element):返回元素在列表中的索引。如果列表中没有该元素则返回-1。
  • removeAt(position):从列表的特定位置移除一项。
  • isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false。
  • size():返回链表包含的元素个数。与数组的length属性类似。
  • toString():由于列表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值。
function LinkedList() {

  // 单个节点类,表示要加入到链表中的项
  let Node = function (element) {
    this.element = element;
    this.next = null;
  };

  let length = 0; // 内部私有变量,记录链表长度
  let head = null; // 头指针

  /**
   * 向链表尾部添加一个新的项
   * 两种场景:链表为空,添加的是第一个元素;链表不为空,向其追加元素。
   *
   * @param element
   */
  this.append = function (element) {

    let node = new Node(element), current;

    if (head === null) {
      head = node;
    } else {
      current = head;
      // 从表头开始循环找到最后一项
      while (current.next !== null) {
        current = current.next;
      }
      // 把节点链接到最后一项的 next 上
      current.next = node;
    }

    length++; // 更新链表长度
  };

  /**
   * 从任意位置移除元素并返回被移除的元素
   * 两种场景:移除第一个元素;移除第一个以外的任一元素。
   * 被移除的元素被丢弃在计算机内存中,等待被垃圾回收器清理。
   *
   * @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 的前一个位置时终止循环
        while (index++ < position) {
          previous = current;
          current = current.next;
        }
        // 上下节点进行链接,跳过中间将被移除的 current 节点
        previous.next = current.next;
      }

      length--;

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

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

    // 检查是否越界,注意这里包含了空链表时的情形
    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; // 即新节点插入到目标位置的前面,这里current可能是null
        previous.next = node;
      }

      length++;

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

  /**
   * 把 LinkedList 对象转换成字符串
   *
   * @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;
  };

  /**
   * 获取表头
   * 方便实例外部访问和迭代链表
   *
   * @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) { // 第一项
        head = current.next;
        // 如果只有一项,需要更新tail
        if (length === 1) {
          tail = null;
        } else {
          head.prev = null;
        }
      } else if (position === length - 1) { // 最后一项
        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;
    }
  };

  /**
   * 从任意位置添加节点
   *
   * @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) { // 在第一个位置添加
        if (!head) { // 当前链表为空
          head = node;
          tail = node;
        } else {
          node.next = current;
          current.prev = node;
          head = node;
        }
      } else if (position === length) { // 最后一项
        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 循环链表

循环链表 CircularLinkedList 可以只有单向引用,也可以像双向链表一样有双向引用。
单向循环链表中:循环链表中 tail.next 指向 head。


image.png
image.png

双向循环链表中:tail.next 指向 head,head.prev 指向 tail。


image.png
image.png

2. 链表相关问题

2.1 判断链表是否存在环形

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

// 1. 暴力解法(brute force): 迭代节点, 存储或进行标记, 如果遇见已经存在的记录, 则说明存在环形

// 2. 快慢双指针(龟兔赛跑):
// slow速度为1, fast为2, 若fast遇见了slow(地球是圆的), 则说明存在环形

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

/**
 * 判断链表是否存在环形
 *
 * @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 求两个链表交集时的节点

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

// 1. 暴力解法(brute force): 使用数组或者set存储一条链表所有节点,然后迭代第二条链表进行查找是否存在(略)

// 2. 双指针 two pointers
// 两个链表指针同时从各自表头移动, 速度一致, 当较短的链表(A)跑到头后, 较长的链表(B)就开始顺便重定向头部指针,
// 此后B的前后两个指针之差始终等于A的总长度,
// 然后等到B的后指针也跑到头后, 再同时从A和B的左侧指针开始迭代比较是否相等,找出交点.

// 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;
 * }
 */

/**
 * 求两个链表交集时的节点
 *
 * @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;
};
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260