2. 链表
203. 移除链表元素
- 203. 移除链表元素
- 0505,easy,answer
设置一个虚拟头指针,用来解决如果 head 结点要删除时,需要移动 head 的特殊情况。
var removeElements = function (head, val) {
// 虚拟头节点
const res = new ListNode(0, head);
let cur = res;
while(cur.next) {
if (cur.next.val === val) cur.next = cur.next.next;
else cur = cur.next;
}
return res.next; // 返回虚拟头节点指向的下一个指针。
};
707. 设计链表
- 707. 设计链表
- 0505,mid,answer
- 链表的定义,链表的处理
错误:找了很久的错误:
// ❗️拼写错误要注意
this._tail // 写成了 this.tail
this._head // 写成了 this_head
// 要注意边界问题,每一个函数在开始是,都要考虑到现处理边界情况。
需要先自定义三个方法(对象):
- 定义公共方法:
// 创建一个单链表的结点
class LinkNode {
constructor(val, next) {
this.val = val;
this.next = next || null;
}
}
// 链表存储:长度、头指针、尾指针
var MyLinkedList = function () {
this._size = 0;
this._head = null;
this._tail = null;
};
// 根据 index 获取链表中的某个节点
MyLinkedList.prototype.getNode = function (index) {
if (index < 0 || index >= this._size) return null;
// 如果要最后一个,可以快速定位一下,省去 for 循环
if (index === this._size - 1) return this._tail;
let cur = this._head;
for (let i = 0; i < index; i++) {
cur = cur.next;
}
return cur;
}
答案 (需要加上上面的三个方法):
// 根据 index 获取链表中的某个节点
MyLinkedList.prototype.getNode = function (index) {
if (index < 0 || index >= this._size) return null;
// 如果要最后一个,可以快速定位一下,省去 for 循环
if (index === this._size - 1) return this._tail;
let cur = this._head;
for (let i = 0; i < index; i++) {
cur = cur.next;
}
return cur;
}
/**
* @param {number} index
* @return {number}
*/
MyLinkedList.prototype.get = function (index) {
if (index < 0 || index >= this._size) return -1;
// 获取节点值
return this.getNode(index).val;
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtHead = function (val) {
const node = new LinkNode(val, this._head);
this._size += 1;
this._head = node;
// 如果链表中还没结点
if (this._size === 1) this._tail = node;
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function (val) {
const node = new LinkNode(val, null);
this._size += 1;
// 如果链表中还没结点
if (this._size === 1) {
this._head = node;
this._tail = node;
return
}
this._tail.next = node;
this._tail = node;
};
/**
* @param {number} index
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtIndex = function (index, val) {
if (index > this._size) return;
// 头插入
if (index <= 0) this.addAtHead(val);
// 尾插入
else if (index === this._size) this.addAtTail(val);
// 正常插入
else {
const pre = this.getNode(index - 1); // 找到要插入的index的前一个结点
pre.next = new LinkNode(val, pre.next);
this._size += 1;
}
};
/**
* @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;
this._size -= 1;
// 如果链表中没有结点了,额外处理尾结点
if (this._size === 0) this._tail = null;
return;
}
// 按照 index 查找前一个结点
const pre = this.getNode(index - 1);
pre.next = pre.next.next;
// 处理尾结点
if (index === this._size - 1) this._tail = pre;
this._size -= 1;
};
206. 反转链表
- 206. 反转链表
- 0505,easy,quick
方法一:双指针
- pre 指向前一个结点,cur 指向当前要修改的结点,temp 作为临时结点,指向 cur.next。
var reverseList = function (head) {
let [pre, cur] = [null, head];
while (cur) {
const temp = cur.next;
cur.next = pre;
// pre 、cur 往前移动
[pre, cur] = [cur, temp];
}
return pre;
};
方法二:迭代
逻辑和双指针是一样的。
var reverseList = function (head) {
return reverse(null, head);
function reverse(pre, cur) {
// 结束递归
if (!cur) return pre;
const temp = cur.next;
cur.next = pre;
return reverse(cur, temp);
}
};
方法三:栈
如果要求不原地修改,而是重新构建一个 nodeList 则使用栈结构来复制:
var reverseList = function (head) {
const stack = [];
const resList = new ListNode();
// 原链表的 val 全部入栈:
for (let cur = head; cur !== null; cur = cur.next) {
stack.push(cur.val);
}
// 将栈中的 val 写入新链表中:
for (let cur = resList; stack.length; cur = cur.next) {
cur.next = new ListNode(stack.pop());
}
return resList.next;
};
24. 两两交换链表中的节点
- 24. 两两交换链表中的节点
- 0505,mid,quick
方法一:三指针
交换链表,需要改动 3 处结点的 .next
关系,也就是说需要三个指针。
-
需要定义一个虚拟头指针,以解决 head 结点也需要移动的问题;
-
可以自己画个图,就明白逻辑了。
-
时间复杂度:O(n),空间复杂度:O(1)。
一共有 三步,但顺序可以反过来:
var swapPairs = function (head) {
if (!head || !head.next) return head;
// 虚拟头节点
const res = new ListNode('', head);
// cur 指向两个被交换节点前的一个结点
// 交换两个结点:change 和 change.next (也就是temp).
let [cur, change] = [res, head];
while (change && change.next) {
const temp = change.next;
change.next = temp.next; // 步骤三
temp.next = change; // 步骤二
cur.next = temp; // 步骤一
// 指针往前移动
cur = change;
change = change.next;
}
return res.next;
};
方法二:迭代
// 返回,head 和 head.next 完成交换的链表
var swapPairs = function (head) {
if (!head || !head.next) return head;
// newNode 是后面交换好的链表
const newNode = swapPairs(head.next.next);
// head 和 head.next(temp) 交换位置,交换后链表头就是tamp了,所以return temp。
const temp = head.next;
temp.next = head;
head.next = newNode;
return temp;
};
19. 删除链表的倒数第 N 个结点
- 19. 删除链表的倒数第 N 个结点
- 0505,mid,quick
双指针 / 快慢指针:
- left 和 right。right 在 left 的右边,与 left 一直保持 n 的距离。
- right 从 index = n 开始向后遍历,直到遍历到链表的结尾为止,
- 此时 right 则指向了待删除节点的前一个结点,也就是说
left.next
结点即将被删除;
- 此时 right 则指向了待删除节点的前一个结点,也就是说
- ⚠️ 栈结构也可以,先把所有节点放入栈中,然后向外取 n 个节点,则正好取出带删除节点的前一个节点。
var removeNthFromEnd = function (head, n) {
res = new ListNode('', head); // 虚拟头
let [left, right] = [res, res];
// right 指针先走 n 个距离
for (let i = 0; i < n; i++) right = right.next;
// left 和 right 一起走,直到 right 指向链表尾
while (right.next) {
[left, right] = [left.next, right.next];
}
// 此时 left 指向带删除节点的前一个结点
left.next = left.next.next;
return res.next;
};
面试题 02.07. 链表相交
- 面试题 02.07. 链表相交
- 0505,easy,answer
- 双指针
错误原因:
- 这里涉及到当变量有点多:有两个链表、两个指针、两个长度。所以需要对每一个操作步骤仔细核对。我就是在其中有很多小错误:
- 变量没有定义直接使用;
- while 循环时,只移动了变量 a,没有移动变量 b;
- 已经定义了一个变量 curL,再后面把 curL 改名时,没有对所有的 curL 全部更换名字。
方法一:双指针|求长度
求两个链表交点节点的指针。 交点不是数值相等,而是指针相等。所以在一个链表上,可能存在多个相同值的节点。
- 先求出两个链表各自的 长度,并求出两个链表长度的 差值
gap
。 - 代码中, curA 永远指向更长的一个链表,此时让 curA 移动到,和 curB 末尾对齐的位置,也就是向后移动
gap
个距离。 - 然后就可以用 while 循环,同时遍历 curA 和 curB 了,当他们两个相等时,则表明指向了同一个节点,返回即可。
时间复杂度:两次获得链表的长度:O(m+n),然后从头遍历一次链表:O(m) ==> O(m + 2n) 假设 n 为 更短的一边
var getIntersectionNode = function (headA, headB) {
// 拿到更短的一个, lenA is longer
let [lenA, lenB] = [getLen(headA), getLen(headB)];
if (lenA < lenB) {
[lenA, lenB] = [lenB, lenA];
[headA, headB] = [headB, headA];
}
let gap = lenA - lenB;
// 对长链表进行移动
let [curA, curB] = [headA, headB];
while (gap-- > 0) curA = curA.next;
while (curA) {
if (curA === curB) return curA;
curA = curA.next;
curB = curB.next;
}
return null;
// 获取链表的长度:时间复杂度 O(n)
function getLen(list) {
let len = 0, cur = list;
while (cur) {
cur = cur.next;
len++;
}
return len;
}
};
方法二:双指针|数学思维
- 总结数学规律:🔗.
- 时间复杂度:O(a+b) 空间复杂度:O(1)。 节点指针 A , B 使用常数大小的额外空间。
var getIntersectionNode = function(headA, headB) {
if (headA === null || headB === null) {
return null;
}
let pA = headA, pB = headB;
while (pA !== pB) {
pA = pA === null ? headB : pA.next;
pB = pB === null ? headA : pB.next;
console.log(pA, pB);
}
return pA;
};
方法三:Set|哈希集合
-
利用
Set
只能保存 唯一value
,且 有序 来确定是否指向同一节点。 -
流程: