当然,我可以为您提供有关双向链表数据结构在 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:双向链表的高级功能
在第一部分中,我们已经了解了如何创建和使用双向链表。在本部分中,我们将介绍一些更高级的功能,包括在链表中插入和删除节点以及反转链表。
- 在链表中插入节点
在双向链表中,我们可以在任何位置插入节点。以下是一个示例:
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。否则,它将创建一个新节点,并将其插入到指定位置。
- 删除节点
在双向链表中,我们也可以删除任何位置的节点。以下是一个示例:
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。否则,它将从链表中删除指定位置的节点。
- 反转链表
在双向链表中,我们也可以反转链表。以下是一个示例:
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题目
设计自己的链表
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--;
}
}
- 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 中的第一个键。
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);
};
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 题目。当然,这些并不是最优解,只是提供一些可行的实现方式。