203.移除链表元素(虚拟头节点)
套路 1
要想删除节点 node,必须在 node 的前一个节点执行删除操作。
例如链表 1→2→3,要想删除 2,必须在节点 1 处操作,也就是把节点 1 的 next 更新为节点 3。
套路 2
如果头节点可能被删除,那么要在头节点之前添加一个哨兵节点,这样我们无需特判头节点被删除的情况,从而简化代码逻辑。
或者说,根据套路 1,要想删除头节点,必须在头节点的前一个节点操作,所以要添加一个哨兵节点。
算法
初始化哨兵节点 dummy,其 next 为 head。
遍历链表,初始化 cur=dummy。
循环直到 cur 的下一个节点为空。
如果 cur 的下一个节点的值等于 val,那么删除下一个节点,把 cur.next 更新为 cur.next.next。
如果 cur 的下一个节点的值不等于val ,那么不删除下一个节点,继续看下下一个节点是否要删除,即更新 cur 为 cur.next。
循环结束,返回 dummy.next,即删除节点后的新链表的头节点。
答疑
问:为什么没有修改 dummy,但 dummy.next 却是新链表的头节点?如果删除了 head,那么最后返回的是不是原链表的头节点?
答:注意初始化时,cur 和 dummy 都指向同一个节点,cur 和dummy只是同一个节点的引用,所以修改 cur.next 也会同时修改 dummy.next。
问:为什么删除下一个节点后,不需要更新 cur 为 cur.next?
答:删除下一个节点后,cur.next 的节点值也可能等于 val,也需要删除,如果直接更新 cur 为 cur.next,就漏删了节点。
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function(head, val) {
const dommy = new ListNode(0, head);
let cur = dommy;
while (cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return dommy.next;
};
707.设计链表
题意:
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
思路
删除链表节点:
添加链表节点:
这道题目设计链表的五个接口:
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点
可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目
链表操作的两种方式:
- 直接使用原来的链表来进行操作。
- 设置一个虚拟头结点在进行操作。
class LinkNode {
constructor(val, next) {
this.val = val;
this.next = next;
}
}
/**
* Initialize your data structure here.
* 单链表 储存头尾节点 和 节点数量
*/
var MyLinkedList = function () {
this._size = 0;
this._tail = null;
this._head = null;
};
/**
* Get the value of the index-th node in the linked list. If the index is invalid, return -1.
* @param {number} index
* @return {number}
*/
MyLinkedList.prototype.getNode = function (index) {
if (index < 0 || index >= this._size) return null;
// 创建虚拟头节点
let cur = new LinkNode(0, this._head);
// 0 -> head
while (index-- >= 0) {
cur = cur.next;
}
return cur;
};
MyLinkedList.prototype.get = function (index) {
if (index < 0 || index >= this._size) return -1;
// 获取当前节点
return this.getNode(index).val;
};
/**
* Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list.
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtHead = function (val) {
const node = new LinkNode(val, this._head);
this._head = node;
this._size++;
if (!this._tail) {
this._tail = node;
}
};
/**
* Append a node of value val to the last element of the linked list.
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function (val) {
const node = new LinkNode(val, null);
this._size++;
if (this._tail) {
this._tail.next = node;
this._tail = node;
return;
}
this._tail = node;
this._head = node;
};
/**
* Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted.
* @param {number} index
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtIndex = function (index, val) {
if (index > this._size || index < 0) return;
if (index === 0) {
this.addAtHead(val);
return;
}
if (index === this._size) {
this.addAtTail(val);
return;
}
// 获取目标节点的上一个的节点
const node = this.getNode(index - 1);
node.next = new LinkNode(val, node.next);
this._size++;
};
/**
* Delete the index-th node in the linked list, if the index is valid.
* @param {number} index
* @return {void}
*/
MyLinkedList.prototype.deleteAtIndex = function (index) {
if (index < 0 || index >= this._size) return;
if (index === 0) {
this._head = this._head.next;
// 如果删除的这个节点同时是尾节点,要处理尾节点
if (index === this._size - 1) {
this._tail = this._head
}
this._size--;
return;
}
// 获取目标节点的上一个的节点
const node = this.getNode(index - 1);
node.next = node.next.next;
// 处理尾节点
if (index === this._size - 1) {
this._tail = node;
}
this._size--;
};
// MyLinkedList.prototype.out = function () {
// let cur = this._head;
// const res = [];
// while (cur) {
// res.push(cur.val);
// cur = cur.next;
// }
// };
/**
* Your MyLinkedList object will be instantiated and called as such:
* var obj = new MyLinkedList()
* var param_1 = obj.get(index)
* obj.addAtHead(val)
* obj.addAtTail(val)
* obj.addAtIndex(index,val)
* obj.deleteAtIndex(index)
*/
206.反转链表(双指针)
题意:反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
思路
如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:
之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。
那么接下来看一看是如何反转的呢?
我们拿有示例中的链表来举例,如动画所示:(纠正:动画应该是先移动pre,在移动cur)
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function (head) {
if (!head || !head.next) return head;
let pre = null;
let cur = head;
let temp;
while (cur) {
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
};
- 代码开头,是一个链表节点的定义。
ListNode类有两个属性:val存储节点的值,next存储指向下一个节点的指针。 reverseList函数接受一个单链表的头节点head作为参数。如果head为null或者head的下一个节点为null,表示该链表为空或者只有一个节点,直接返回head。- 如果链表有多个节点,使用三个指针来遍历和修改链表。
pre指针初始化为null,cur指针初始化为head。进入一个while循环,循环条件是cur指针不为null。 - 在循环体中,
temp指针用于临时存储cur的下一个节点,这样做是为了确保在修改cur的指向后,可以继续访问原来的下一个节点。然后修改cur的next指向pre,这实际上是将当前节点的指向前一个节点。接着把pre指针指向前进到当前节点,cur指针也前进一步,指向下一个节点。 - 当
cur为null时,循环结束。此时,pre指针指向的是原来链表的最后一个节点,也就是反转后的链表的头节点。 - 返回
pre,即反转后的链表的头节点。
整体流程是通过遍历链表的同时修改每个节点的 next 指针,实现对整个链表的反转。这种算法的时间复杂度为 O(n),其中 n 是链表的长度,因为需要遍历整个链表。空间复杂度为 O(1),因为只使用了常数级别的额外存储空间,即三个指针。