前端涨薪功法:深入探讨双向链表的高级功能

319 阅读5分钟

当然,我可以为您提供有关双向链表数据结构在 JavaScript 中的重要性的技术文章。下面是第一部分。

初识双向链表

在计算机科学中,链表是一种数据结构,其中每个节点都包含一个指向下一个节点的指针。单向链表只能向前遍历,而双向链表则可以向前或向后遍历。每个节点还包含一个指向前一个节点的指针,这使得在双向链表中查找、插入和删除元素更加高效。

在 JavaScript 中,我们可以使用类来实现双向链表。以下是一个简单的 Node 类的示例,它包含一个值属性和前向和后向指针属性:

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

在这个类中,我们可以使用 next 和 prev 属性来表示节点的前向和后向指针。如果节点没有前向或后向节点,则这些属性将为 null

接下来,我们可以创建 DoublyLinkedList 类来管理双向链表。以下是一个简单的示例:

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }
}

在这个类中,我们使用 head 和 tail 属性来表示双向链表的头部和尾部节点。我们还使用 length 属性来表示链表中节点的数量。

接下来,我们可以实现一些基本方法来操作双向链表。这些方法包括添加节点,删除节点和遍历链表。以下是一个示例:

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  // 添加节点
  push(value) {
    const node = new Node(value);

    if (this.length === 0) {
      this.head = node;
      this.tail = node;
    } else {
      this.tail.next = node;
      node.prev = this.tail;
      this.tail = node;
    }

    this.length++;
    return this;
  }

  // 删除节点
  pop() {
    if (!this.head) return undefined;

    const removedNode = this.tail;

    if (this.length === 1) {
      this.head = null;
      this.tail = null;
    } else {
      this.tail = removedNode.prev;
      this.tail.next = null;
      removedNode.prev = null;
    }

    this.length--;
    return removedNode;
  }

  // 遍历链表
  traverse() {
    let current = this.head;

    while (current) {
      console.log(current.value);
      current = current.next;
    }
  }
}

在这个类中,我们实现了 push 和 pop 方法来添加和删除节点。我们还实现了 traverse 方法来遍历整个链表并打印每个节点的值。

到此为止,我们已经了解了双向链表的基本结构和实现方式。在下一部分中,我们将深入探讨双向链表的高级功能。

部分2:双向链表的高级功能

在第一部分中,我们已经了解了如何创建和使用双向链表。在本部分中,我们将介绍一些更高级的功能,包括在链表中插入和删除节点以及反转链表。

  1. 在链表中插入节点

在双向链表中,我们可以在任何位置插入节点。以下是一个示例:

class DoublyLinkedList {
  // ...

  // 在指定位置插入节点
  insert(index, value) {
    // 如果 index 无效,则返回 undefined
    if (index < 0 || index > this.length) {
      return undefined;
    }

    const node = new Node(value);
    let current = this.head;

    // 如果 index 为 0,则将节点插入头部
    if (index === 0) {
      if (!this.head) {
        this.head = node;
        this.tail = node;
      } else {
        node.next = current;
        current.prev = node;
        this.head = node;
      }
    }
    // 如果 index 为 length,则将节点插入尾部
    else if (index === this.length) {
      this.tail.next = node;
      node.prev = this.tail;
      this.tail = node;
    }
    // 否则,在指定位置插入节点
    else {
      for (let i = 0; i < index - 1; i++) {
        current = current.next;
      }
      node.prev = current;
      node.next = current.next;
      current.next.prev = node;
      current.next = node;
    }

    this.length++;
    return this;
  }
}

在这个类中,我们实现了 insert 方法来在链表中插入节点。如果指定的 index 无效,则该方法将返回 undefined。否则,它将创建一个新节点,并将其插入到指定位置。

  1. 删除节点

在双向链表中,我们也可以删除任何位置的节点。以下是一个示例:

class DoublyLinkedList {
  // ...

  // 删除指定位置的节点
  remove(index) {
    // 如果 index 无效,则返回 undefined
    if (index < 0 || index >= this.length) {
      return undefined;
    }

    let removedNode = null;
    let current = this.head;

    // 如果 index 为 0,则从头部删除节点
    if (index === 0) {
      removedNode = this.head;
      this.head = removedNode.next;
      if (this.length === 1) {
        this.tail = null;
      } else {
        this.head.prev = null;
      }
    }
    // 如果 index 为 length - 1,则从尾部删除节点
    else if (index === this.length - 1) {
      removedNode = this.tail;
      this.tail = removedNode.prev;
      this.tail.next = null;
    }
    // 否则,从指定位置删除节点
    else {
      for (let i = 0; i < index; i++) {
        current = current.next;
      }
      removedNode = current;
      current.prev.next = current.next;
      current.next.prev = current.prev;
    }

    removedNode.next = null;
    removedNode.prev = null;
    this.length--;
    return removedNode;
  }
}

在这个类中,我们实现了 remove 方法来删除链表中指定位置的节点。如果指定的 index 无效,则该方法将返回 undefined。否则,它将从链表中删除指定位置的节点。

  1. 反转链表

在双向链表中,我们也可以反转链表。以下是一个示例:

class DoublyLinkedList {
  // ...

  // 反转链表
  reverse() {
    let current = this.head;
    let prev = null;

    while (current) {
      const next = current.next;
      current.next = prev;
      current.prev = next;
      prev = current;
      current = next;
    }

    this.tail = this.head;
    this.head = prev;
    return this;
  }
}

在这个类中,我们实现了 reverse 方法来反转整个链表。该方法遍历链表,并将每个节点的前向和后继指针交换,最后交换链表的头和尾以完成反转。

实战Leetcod题目

  1. 707. Design Linked List

设计自己的链表

class ListNode {
  constructor(val, prev, next) {
    this.val = val;
    this.prev = prev;
    this.next = next;
  }
}

class MyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }

  get(index) {
    if (index < 0 || index >= this.size) {
      return -1;
    }

    let cur = this.head;

    for (let i = 0; i < index; i++) {
      cur = cur.next;
    }

    return cur.val;
  }

  addAtHead(val) {
    const newNode = new ListNode(val, null, this.head);

    if (this.head) {
      this.head.prev = newNode;
    }

    this.head = newNode;

    if (!this.tail) {
      this.tail = newNode;
    }

    this.size++;
  }

  addAtTail(val) {
    const newNode = new ListNode(val, this.tail, null);

    if (this.tail) {
      this.tail.next = newNode;
    }

    this.tail = newNode;

    if (!this.head) {
      this.head = newNode;
    }

    this.size++;
  }

  addAtIndex(index, val) {
    if (index < 0 || index > this.size) {
      return;
    }

    if (index === 0) {
      this.addAtHead(val);
      return;
    }

    if (index === this.size) {
      this.addAtTail(val);
      return;
    }

    let cur = this.head;

    for (let i = 0; i < index - 1; i++) {
      cur = cur.next;
    }

    const newNode = new ListNode(val, cur, cur.next);
    cur.next.prev = newNode;
    cur.next = newNode;

    this.size++;
  }

  deleteAtIndex(index) {
    if (index < 0 || index >= this.size) {
      return;
    }

    let cur = this.head;

    if (index === 0) {
      this.head = cur.next;

      if (this.head) {
        this.head.prev = null;
      }

      if (!this.head) {
        this.tail = null;
      }

      this.size--;
      return;
    }

    if (index === this.size - 1) {
      cur = this.tail;
      this.tail = cur.prev;
      this.tail.next = null;

      if (!this.tail) {
        this.head = null;
      }

      this.size--;
      return;
    }

    for (let i = 0; i < index - 1; i++) {
      cur = cur.next;
    }

    cur.next = cur.next.next;
    cur.next.prev = cur;

    this.size--;
  }
}
  1. 146. LRU Cache 利用了Map的key是有序的这一最大特点
class LRUCache {
  constructor(capacity) {
    this.map = new Map();
    this.capacity = capacity;
  }

  get(key) {
    if (!this.map.has(key)) {
      return -1;
    }

    const val = this.map.get(key);
    this.map.delete(key);
    this.map.set(key, val);

    return val;
  }

  put(key, value) {
    if (this.map.has(key)) {
      this.map.delete(key);
    }

    this.map.set(key, value);

    if (this.map.size > this.capacity) {
      const firstKey = this.map.keys().next().value;
      this.map.delete(firstKey);
    }
  }
}

这个解答利用了 JavaScript 中的 Map 数据结构,它是一个键值对的集合,其中每个键唯一。我们可以通过 Map 来实现 LRU Cache,其中 Map 中的键表示缓存中的键,Map 中的值表示缓存中的值。

在 get 方法中,我们首先使用 map.get(key) 从 Map 中获取对应的值。如果值不存在,我们返回 -1。如果值存在,我们将其从 Map 中删除,然后再将其添加回 Map 中。这样做的目的是为了将它放到 Map 的末尾,表示它是最近访问的元素。

在 put 方法中,我们首先检查 Map 中是否已经存在该键。如果存在,我们将其从 Map 中删除,然后再将其添加回 Map 中。这样做的目的是为了将它放到 Map 的末尾,表示它是最近访问的元素。如果 Map 的大小超过了 capacity,我们需要删除 Map 中最久未使用的元素,即 Map 中的第一个键。

  1. 148. Sort List
class ListNode {
  constructor(val, next) {
    this.val = val;
    this.next = next;
  }
}

const merge = (left, right) => {
  const dummy = new ListNode(0);
  let cur = dummy;

  while (left && right) {
    if (left.val < right.val) {
      cur.next = left;
      left = left.next;
    } else {
      cur.next = right;
      right = right.next;
    }

    cur = cur.next;
  }

  if (left) {
    cur.next = left;
  }

  if (right) {
    cur.next = right;
  }

  return dummy.next;
};

const getMiddle = (head) => {
  if (!head) {
    return null;
  }

  let slow = head;
  let fast = head.next;

  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;
  }

  return slow;
};

const sortList = (head) => {
  if (!head || !head.next) {
    return head;
  }

  const middle = getMiddle(head);
  const right = sortList(middle.next);
  middle.next = null;
  const left = sortList(head);

  return merge(left, right);
};
  1. 430. Flatten a Multilevel Doubly Linked List
class Node {
  constructor(val, prev, next, child) {
    this.val = val;
    this.prev = prev;
    this.next = next;
    this.child = child;
  }
}

const flatten = (head) => {
  if (!head) {
    return null;
  }

  const dummy = new Node(0, null, head, null);
  let prev = dummy;
  const stack = [head];

  while (stack.length) {
    const node = stack.pop();

    node.prev = prev;
    prev.next = node;

    if (node.next) {
      stack.push(node.next);
    }

    if (node.child) {
      stack.push(node.child);
      node.child = null;
    }

    prev = node;
  }

  dummy.next.prev = null;

  return dummy.next;
};

这个解答使用了双向链表的特性来实现了 Flatten a Multilevel Doubly Linked List 题目。

在 flatten(head) 方法中,我们首先检查 head 是否为空,如果为空,则直接返回 null。接下来,我们使用一个 cur 变量来遍历双向链表。

在遍历的过程中,如果 cur 的 child 不为空,则说明 cur 后面有一个子链表。我们需要将子链表插入到 cur 和 cur 的 next 之间。

具体的做法是,我们先将 cur 的 next 和 child 分别赋值给 next 和 child 变量。然后,我们将 cur 的 next 指向 child,将 child 的 prev 指向 cur,将 cur 的 child 设为 null。

接下来,我们遍历子链表,找到最后一个节点 lastChild,然后将 lastChild 的 next 指向 next,将 next 的 prev 指向 lastChild。这样就将子链表插入到 cur 和 cur 的 next 之间了。

最后,我们将 cur 设为 cur 的 next,继续遍历双向链表,直到 cur 为 null。最后返回 head,表示已经将双向链表扁平化了。

这些代码示例应该能够帮助你更好地理解双向链表相关的 LeetCode 题目。当然,这些并不是最优解,只是提供一些可行的实现方式。