第5章 链表 (LinkedList)

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

1. 链表数据结构

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


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

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


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

你可能感兴趣的:(第5章 链表 (LinkedList))