首先需要了解链表的概念
无论是插入,删除,还是翻转等等操作,先把 next 指针用临时变量保存起来,这可以解决 90% 重组链表中指向出错的问题,
类型守卫 emptyNode 是创建的一个空的节点,并将它连接到 head 节点之前,无论链表进行任何操作, emptyNode 都指向最后的头节点,是一个很实用的小方法,如果不知道什么时候用,什么时候不用,那就先都用起来吧;
其实在大部分时候,emptyNode 都是能用上,即便只是遍历查找值,用上作为第 0 个值,当要找第 k 个值的时候,也不需要再判空处理啊
如果懒或者经常忘记看题目的给定条件,头节点判空都做起来,对于一些翻转题,还得将 head.next 也判断起来;
到熟练之后,其实可以不做,但是用上最多就浪费一段代码,也还好
遇事不决的时候,还是要画图,一步一步的连起来,总能够捋清楚的,画图是链表的关键所在
链表是一个特定的数据结构,在 JS 中可以表现为一个拥有 val 和 next 属性的对象,所以遇到形如交换两个链表节点的时候,千万不能交换两个链表的 val 值,虽然 LC 有一些题是可以过,但是实际上是不合理的,而且一旦出现这种思想,对于一些经典题 160. 相交链表 就会理解不了;
记住,链表是一个数据结构,不是一个值,可以类比成一个对象,交换链表比如不是简单交换值;
这里选的都是按照 LC 火热排序,中等难度的题,感觉纯链表学习做特别难没太大必要,毕竟我只是一个菜鸟,大佬们可以自由选择,一起 💪,进大厂;
分析
var reverseList = function (head) {
let prev = null;
while (head) {
const next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
};
分析
/** * @分析 * 1. 这里的返回值是按照十进制计算后的 `链表` */
var addTwoNumbers = function (l1, l2) {
const emptyNode = new ListNode();
let current = emptyNode;
let isUpper = 0; // 是否满10,为后面的位+1
while (l1 && l2) {
let sum = l1.val + l2.val + isUpper;
if (sum >= 10) {
isUpper = 1;
sum = sum % 10;
} else {
isUpper = 0;
}
current.next = new ListNode(sum);
current = current.next;
l1 = l1.next;
l2 = l2.next;
}
let l3 = l1 || l2; //剩余的那个链表
while (l3) {
let sum = l3.val + isUpper;
if (sum >= 10) {
isUpper = 1;
sum = sum % 10;
} else {
isUpper = 0;
}
current.next = new ListNode(sum);
current = current.next;
l3 = l3.next;
}
if (isUpper) {
// 遍历完了,如果还有进位
current.next = new ListNode(isUpper);
current = current.next;
}
return emptyNode.next;
};
参考视频:传送门
分析
var addTwoNumbers = function (l1, l2) {
const emptyNode = (current = new ListNode());
// 翻转量个链表,让他们头节点对齐
let temp1 = reverseList(l1);
let temp2 = reverseList(l2);
let isUpper = 0; // 是否满10,为后面的位+1
while (temp1 && temp2) {
let sum = temp1.val + temp2.val + isUpper;
if (sum >= 10) {
isUpper = 1;
sum = sum % 10;
} else {
isUpper = 0;
}
current.next = new ListNode(sum);
current = current.next;
temp1 = temp1.next;
temp2 = temp2.next;
}
let l3 = temp1 || temp2; //剩余的那个链表
while (l3) {
let sum = l3.val + isUpper;
if (sum >= 10) {
isUpper = 1;
sum = sum % 10;
} else {
isUpper = 0;
}
current.next = new ListNode(sum);
current = current.next;
l3 = l3.next;
}
if (isUpper) {
// 遍历完了,如果还有进位
current.next = new ListNode(isUpper);
current = current.next;
}
return reverseList(emptyNode.next);
};
// 反转链表
var reverseList = function (head) {
let prev = null;
while (head) {
const next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
};
分析:
var rotateRight = function (head, k) {
// 先求链表的长度
let len = 0,
tempNode = head;
while (tempNode) {
len++;
tempNode = tempNode.next;
}
// 需要位移 size 到头节点
let size = len - (k % len);
let prev = new ListNode();
prev.next = head;
let cur = head;
while (size--) {
cur = cur.next;
prev = prev.next;
}
//保存移动之后的尾部节点
let tail = prev;
// 继续往前走
while (cur) {
cur = cur.next;
prev = prev.next;
}
// 原来的尾结点和头节点相连
prev.next = head;
// 获取移动后的头节点
const next = tail.next;
// 尾结点的 next 指针指向的是 null
tail.next = null;
return next;
};
分析
var deleteDuplicates = function (head) {
let emptyNode = (prev = new ListNode());
emptyNode.next = head;
while (head) {
while (head.next && head.val === head.next.val) {
head = head.next;
}
if (prev.next !== head && head.val === prev.next.val) {
// 这是遇到重复值时,删除节点后进行整理, prev.next 重新指向新的 head 节点
head = head.next;
// 重新连接起来
prev.next = head;
} else {
// 这是正常没有重复值走
prev = prev.next;
head = head.next;
}
}
return emptyNode.next;
};
分析
注意:面试题 02.04. 分割链表 这道题和本题类似,但是不保留每个分区的初始相对位置
var partition = function (head, x) {
let emptyNode = (prev = new ListNode());
emptyNode.next = head;
while (head && head.val < x) {
head = head.next;
prev = prev.next;
}
// 走完了,或者遇到了 K,保存一下这个前置节点
let tail = prev;
// 这个时候找到小于 x 的就要处理了
while (head) {
if (head.val < x) {
const next = head.next;
// 插入到 tail 和 tail.next 之间
head.next = tail.next;
tail.next = head;
tail = tail.next;
// 删除 head 节点,重新设置新的 head
prev.next = next;
head = next;
} else {
head = head.next;
prev = prev.next;
}
}
return emptyNode.next;
};
分析
var sortedListToBST = function (head) {
const recursion = (head) => {
if (!head) return null; // 空节点
// 使用双指针找出中间节点 -- 这是向上取整
let emptyNode = (prev = new ListNode());
prev.next = head;
let slow = (fast = head);
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
prev = prev.next;
}
// 以 slow 为根节点,左侧一段离岸边要截断
prev.next = null;
const node = new TreeNode(slow.val);
node.left = recursion(emptyNode.next);
node.right = recursion(slow.next);
return node;
};
return recursion(head);
};
分析
var detectCycle = function (head) {
if (!head) return null; //一个节点都没得,还有啥环
const emptyNode = new ListNode();
emptyNode.next = head;
let slow = (fast = emptyNode); // 相当于走了走了一次了
while (fast && fast.next) {
// 一开始都是从边界守卫开始,这样可以防止在第一个节点就有环了
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
// 在环中的某一个点相交了
let newSlow = emptyNode;
while (slow !== newSlow) {
newSlow = newSlow.next;
slow = slow.next;
}
return newSlow;
}
}
return null; //如果会跳出来,证明无环
};
分析
var insertionSortList = function (head) {
if (!head || !head.next) return head;
let emptyNode = new ListNode();
emptyNode.next = head;
let write = head,
read = head.next;
while (read) {
if (read.val >= write.val) {
// 读指针比写指针更大的时候,一起走
read = read.next;
write = write.next;
} else {
// 删除 read 指针,然后从 emptyNode 到 write 中找个位置插入
// 先删除 -- 这个时候 read 指针先当做一个普通节点使用,注意: write 指针其实一直都在 read 之后,和 prev 指针的作用差不多
write.next = read.next;
let em = emptyNode;
while (em.next.val < read.val) {
em = em.next;
}
// 插入 em.next >= read.val , 所有插入到 em - read - em.next
read.next = em.next;
em.next = read;
// 把 read 指针放回到 write 之后,再继续走 -- 这里当然可以用临时遍历处理,但是
read = write.next;
}
}
return emptyNode.next;
};
分析
var oddEvenList = function (head) {
if (!head || !head.next) return head; // 两个节点都没得,直接回家吧
let emptyNode = new ListNode();
emptyNode.next = head;
let fast = (slow = head);
while (fast && fast.next) {
// 这是fast的前一个节点,用来删除 fast 节点 -- 同时作为在前面插入删除节点后,重新锚点的位置
const prev = fast.next;
fast = fast.next.next;
if (fast) {
// 删除 fast 节点
prev.next = fast.next;
fast.next = slow.next;
slow.next = fast;
slow = slow.next;
// 恢复 fast
fast = prev;
}
}
return emptyNode.next;
};
特别注意
var getIntersectionNode = function (headA, headB) {
let tempA = headA,
tempB = headB;
while (tempA && tempB) {
// 一起走
tempA = tempA.next;
tempB = tempB.next;
}
// tempC 是剩下的, long 是更长的链表
if (tempA) {
tempC = tempA;
long = headA;
short = headB;
} else {
tempC = headB;
long = headB;
short = headA;
}
// 将 long 多出来的节点先走完,得到和 short 相同长度的链表
while (long) {
while (tempC) {
tempC = tempC.next;
long = long.next;
}
}
while (long) {
if (long === short) return long;
long = long.next;
short = short.next;
}
return null;
};
var getIntersectionNode = function (headA, headB) {
let tempA = headA,
tempB = headB;
while (tempA !== tempB) {
tempA = tempA ? tempA.next : headB;
tempB = tempB ? tempB.next : headA;
}
return tempA;
};
分析
var swapNodes = function (head, k) {
if (!head) return head;
let emptyNode = new ListNode();
emptyNode.next = head;
let pFirst = emptyNode;
let first = head;
while (--k) {
first = first.next;
pFirst = pFirst.next;
}
// 现在 first 就是正向第 k 个节点,只需要保存
let temp = first.next;
let pSecond = emptyNode;
let second = head;
while (temp) {
temp = temp.next;
pSecond = pSecond.next;
second = second.next;
}
// 这个时候 second 就是反向第 K 个节点
if (first.next === second) {
// 相邻节点交换
pFirst.next = second;
first.next = second.next;
second.next = first;
} else if (second.next === first) {
// 相邻节点交换
pSecond.next = first;
second.next = first.next;
first.next = second;
} else {
// 交换 first 和 second
const fNext = first.next;
const sNext = second.next;
pFirst.next = second;
pSecond.next = first;
second.next = fNext;
first.next = sNext;
}
return emptyNode.next;
};
分析
var splitListToParts = function (head, k) {
if (!head) return new Array(k).fill(null); // 没有节点也要切,只是切成 k 份的 null
let emptyNode = new ListNode();
emptyNode.next = head;
let temp = head;
let len = 0;
while (temp) {
len++;
temp = temp.next;
}
const n = Math.floor(len / k);
let m = len % k; // 前 m 个链表取 n+1 个值
let write = (read = head);
let ret = [];
let other = k - m;
// 插入 m 个 n+1 的链表
while (m--) {
let count = 0;
//前 m 个值,最少都还有一个值
while (count < n) {
read = read.next;
count++;
}
// 此时 read 指针在切割指针的位置
const next = read.next;
read.next = null; //切割
ret.push(write);
write = next;
read = next;
}
// 再插入 k-m 个 n 长度的链表
while (other--) {
if (n === 0) {
ret.push(null);
} else {
let count = 0;
while (count < n - 1) {
read = read.next;
count++;
}
// 此时 read 指针在切割指针的位置
const next = read.next;
read.next = null; //切割
ret.push(write);
write = next;
read = next;
}
}
return ret;
};
分析
var numComponents = function (head, nums) {
const arr = [];
for (let num of nums) {
arr[num] = 1;
}
let len = nums.length;
let ret = 0;
let count = 0; //每一个组件的长度 -- 必须大于 1 才能组成一个组件
while (head && len) {
if (arr[head.val]) {
// nums 的值在减少,一旦为 0 了,就结束遍历了
count++; // 万一需要求最大组件,就可以用这个 count 了
len--;
}
if (count && !arr[head.val]) {
// 处于匹配状态,但是这一次却没有匹配值
ret++;
count = 0; // 恢复到 0, 继续下一次的匹配
}
head = head.next;
}
return count ? ret + 1 : ret; //弹出遍历时如果还存在有匹配的组件没计算,则再加1
};
分析
/** * @分析 * 1. 这里是用数组来模拟链表 */
var MyLinkedList = function () {
this.data = [];
};
/** * @分析 -- 获取第 index 个节点的值 * 1. 这里的 index 类比数组的下标值,是从 0 开始的,也就是 index 为 0 代表头节点 * 2. 这里是获取第 index 个节点的值,如果没有这个 index,即 index 超出链表长度 len-1,返回 -1 */
MyLinkedList.prototype.get = function (index) {
const size = this.data.length;
return index < size ? this.data[index] : -1;
};
/** * @分析 -- 从头部插入一个链表值 */
MyLinkedList.prototype.addAtHead = function (val) {
this.data.unshift(val);
};
/** * @分析 -- 从尾部插入一个链表值 */
MyLinkedList.prototype.addAtTail = function (val) {
this.data.push(val);
};
/** * @分析 -- 从 index 插入一个值 */
MyLinkedList.prototype.addAtIndex = function (index, val) {
const len = this.data.length;
if (index <= len) {
if (index <= 0) {
this.data.unshift(val);
} else if (index === len) {
this.data.push(val);
} else {
this.data.splice(index, 0, val); //在 index 节点删除 0 个值,并加入 val
}
}
};
/** * @分析 -- 删除第 index 个值 */
MyLinkedList.prototype.deleteAtIndex = function (index) {
const len = this.data.length;
if (index >= 0 && index < len) {
this.data.splice(index, 1);
}
};
分析 -- 暴力解法
var removeZeroSumSublists = function (head) {
let emptyNode = new ListNode();
emptyNode.next = head;
let sum = 0;
let outer = emptyNode;
while (outer) {
let inner = outer.next;
while (inner) {
// 每次都由 inner 来判断是否要删除相应的链表
// outer 相当于是外围的一个 prev 指针,一旦某一个链表需要删除,直接 outer.next = 删除节点的下一个节点 即可
sum += inner.val;
inner = inner.next;
if (sum === 0) {
// outer -> inner 的节点都要删除
outer.next = inner;
sum = 0; //返回
}
}
// outer 也需要不断遍历到 tail
outer = outer.next;
// 每一次遍历时,临时总和要重置
sum = 0;
}
return emptyNode.next;
};
分析 -- 双指针
var nextLargerNodes = function (head) {
if (!head) return [];
let ret = [];
while (head) {
let r = head.next;
let temp = 0;
while (r) {
if (r.val > head.val) {
temp = r.val;
break;
}
r = r.next;
}
ret.push(temp);
head = head.next;
}
return ret;
};
分析
var mergeInBetween = function (list1, a, b, list2) {
const emptyNode = new ListNode();
emptyNode.next = list1;
let prev = (next = emptyNode);
//这个时候 prev 和 next 都是空节点,而 list1 的 head 节点对应的 index 是0,所以初始化为 -1
let index = -1;
// 不取 = 的时候,得到的就是 下标为 b 的节点,
while (index <= b) {
if (index < a - 1) {
// 这里是为了取下标为 a 节点的前一个节点 prev
prev = prev.next;
}
next = next.next;
index++;
}
// 这个时候 index 是b+1, 所以 next 是 b 的下一个节点
// 插入 list2
prev.next = list2;
while (list2 && list2.next) {
list2 = list2.next;
}
// 这个时候的 list2 已经到了 tail
list2.next = next;
return emptyNode.next;
};
分析
var copyRandomList = function (head) {
if (!head) return head;
const map = new WeakMap();
let temp = head;
while (temp) {
// key 是旧节点,value 保存一个新的节点
map.set(temp, new Node(temp.val));
temp = temp.next;
}
// 开始复制
temp = head;
while (temp) {
const node = map.get(temp); //这个是一个新的节点,它的 next 和 random 也要是新的,存在 map 中
node.next = map.get(temp.next) || null;
node.random = map.get(temp.random) || null;
temp = temp.next;
}
return map.get(head);
};
var reverseKGroup = function (head, k) {
if (!head.next || k < 2) return head;
const emptyNode = new ListNode();
emptyNode.next = head;
let len = 0;
let cur = head;
while (cur) {
len++;
cur = cur.next;
}
let count = Math.floor(len / k); //需要翻转的次数
cur = head;
let outerPrev = emptyNode; //每次翻转链表的前一个节点
while (count--) {
let tempHead = cur; // 翻转链表的临时链表头
let prev = outerPrev;
let step = 0; //每一次翻转走的步数
while (step < k) {
const next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
step++;
}
// 翻转好了,外部prev 和翻转后的头节点相连
outerPrev.next = prev;
tempHead.next = cur;
// 更新外部prev 为临时头节点
outerPrev = tempHead;
}
return emptyNode.next;
};
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。