链表
其实我们开始做题之前需要对链表的基础部分做一些复习攻略
1. 链表的结构是怎样的
2. 链表的遍历方式和我们熟悉的数组有没有区别
3. 链表常见的操作都有哪些?
4. 链表的 指针域 和 数据域 都是用来干嘛的?
5. 你能不能去实现一个简单的链表呢?
如果你对上述的部分都了然于胸,那我们就可以开始我们的刷题之旅了
2. 两数相加
我们还是基于双指针的思路,现在目的是去计算两个链表的和, 那么我们可以不可以这么想,对于数组来说,我们去计算它的两个数组和的时候,我们是不是可以按照对应下标去实现两数的相加,对于我们的链表来说, 是不是可以采用类似的思路去尝试一下呢:
现在我们梳理一下思路:
首先呢, 我们需要去定义两个指针, 目的是在遍历链表的过程中,去维护一个和的链表结构
然后呢, 我们需要依次遍历两个链表, 当然会包含没有值情况,需要做一些处理
根据题目的要求: 逆序存储, 可能情况就是需要去记录进位; 结合示例3,我们需要对最后遍历结束后,看进位的结果是不是进行后续的处理
最后,我们去返回链表的头节点, 结束🔚
我对自己之前书写有问题的地方做了编注,希望对你有所帮助
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function(l1, l2) {
let head = null, tail = null;
// 进位
let temp = 0;
while (l1 || l2) {
let l1Num = l1 ? l1.val : 0;
let l2Num = l2 ? l2.val : 0;
let sum = l1Num + l2Num + temp;
if (!head && !tail) {
head = tail = new ListNode(sum % 10)
} else {
tail.next = new ListNode(sum % 10)
tail = tail.next;
}
// 计算进位
temp = Math.floor(sum / 10);
// if (temp > 1) {
// tail.next = tail.val + 1
// }
// 问题点
if (l1) {
l1 = l1.next;
}
// 问题点
if (l2) {
l2 = l2.next;
}
}
// 需要注意的是最后的 temp值是不是需要进位 //问题点
if (temp > 0) {
tail.next = new ListNode(temp)
}
return head;
};
下面我们来看一道很类似的题目:
445. 两数相加 II
解题思路:
其实刚开始我会按照上面题目的思路快速抄完,发现是错的,这是一个令人尴尬的结果;
那么我是错在了哪里,它们之间到底是哪里不同呢?
首先最开始去遍历的时候就不一样,一个正序进位,一个逆序进位;
我们需要一种新的思路的结合去做:
既然我们从头开始是有问题的,那么按照正常的加法运算的逻辑我们是需要从个位开始累加结果的;
我们是不是可以把链表用一种结构存储, 然后每次取最后一个链表节点的值;
你是不是有一种恍然大悟的感觉呢?
这不就是栈嘛,哎呀妈呀,我突然就懂了。
代码实现:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function(l1, l2) {
let stact_l1 = [], stact_l2 = [];
while(l1) {
stact_l1.push(l1.val);
l1 = l1.next;
}
while(l2) {
stact_l2.push(l2.val)
l2 = l2.next;
}
// 定义一个进位 和 前置节点
let temp = 0, pre = null;
while(!stact_l1.length || !stact_l2.length || temp !== 0) {
let l1_num = stact_l1.length === 0 ? 0 : stact_l1.pop();
let l2_num = stact_l2.length === 0 ? 0 : stact_l2.pop();
let sum = l1_num + l2_num + temp;
temp = Math.floor(sum / 10);
let newNode = new ListNode(sum % 10);
newNode.next = pre;
pre = newNode;
}
return pre;
};
回归正题
21. 合并两个有序链表
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
var mergeTwoLists = function(list1, list2) {
// 虚拟头节点
let dummy = new ListNode(0), pre = dummy;
while(list1 !== null && list2 !== null) {
if (list1.val > list2.val) {
pre.next = list2;
list2 = list2.next;
} else {
pre.next = list1;
list1 = list1.next;
}
pre = pre.next;
}
if (list1) {
pre.next = list1;
}
if (list2) {
pre.next = list2;
}
return dummy.next;
};
19. 删除链表的倒数第N个节点
让我们来看一下这道题, 如果正着来的话就是删除第k个节点, 遍历到第k个节点执行删除节点的操作p.next = p.next.next;但是如果是逆序的话就有点不一样了, 逆序的第n个节点是不是正序的第 n - k个节点呢?
但是我们需要一个值n, 需要我们去遍历一次链表的长度n;然后再去遍历找到第n-k个节点。
但是我们想一想有没有优化的思路呢?
我们能不能只遍历一次链表就能得出倒数第k个节点。
梳理一下思路:
首先, 我们定义一个 p1节点, 然后呢当我们遍历到k个节点的时候此时到链表结尾的空指针的距离为 n - k;
那么 如果此时再走 n - k 步是不是到链表的结尾了呢
假设 我们定义两个指针, fast = head, slow = head;
fast 先跑 k 步, 然后 slow 从头开始跑 当 fast.next === null
slow 就跑到了倒数第k个节点
代码实现
/**
* 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} n
* @return {ListNode}
*/
var removeNthFromEnd = function(head, n) {
// 结合虚拟头节点 避免出现空指针的问题
let dummy = new ListNode(-1);
dummy.next = head;
// 找到 倒数第k个节点
let x = findFromEnd(dummy, n); // error
// 删除第 k 个节点
x.next = x.next.next;
return dummy.next;
};
function findFromEnd(head, n) {
if (!head) return null;
let fast = head;
while (n--) fast = fast.next;
// 遍历到第k个节点
let slow = head;
// slow ,fast 同时走 n - k 步
while(fast.next !== null) {
fast = fast.next;
slow = slow.next;
}
// slow 此时正指向 n-k 个节点
return slow;
}
876. 链表的中间结点
其实延续上面题目的思路,结合快慢指针的套路可以很快的解题。
首先,我们定义了两个指针 fast, slow;
然后, 按照fast每次走两步, slow 每次走一步的策略;
最后, fast走到链表尾端的时候, slow刚好走到链表的中间。
* 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 middleNode = function(head) {
let slow = head, fast = head;
// 需要注意一下这个条件
while(fast !== null && fast.next !== null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
};
141.环形链表
解题思路:
现在我们到了判断链表有没有环的题目, 假设链表没有环, 那么链表就会跑到链表的尾部;
如果 链表指针一直没有等于null,那么此时确定是有环的.
让我们先定义快慢指针 fast,slow,每次fast走两步, slow走一步,如果fast===slow说明两者相遇。
代码实现:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
// 现在我们到了判断链表有没有环的题目
// 假设 链表没有环, 那么链表就会跑到链表的尾部
//
// 那么此时确定是有环的
// 先定义两个快慢指针
let fast = head, slow = head;
while(fast !== null && fast.next !== null) {
fast = fast.next.next;
slow = slow.next;
// 快慢节点相遇
if (slow === fast) {
return true;
}
}
return false;
};
142. 环形链表II
解题思路:
寻找入环的第一个节点,那么是不是就在判断有环的基础上, 根据此时的状态,fast 指针的位置和 从head开始到环的入口的距离是一致的详细解析.
代码实现:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function(head) {
let slow = head, fast = head;
while(fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (fast === slow) {
// 相遇了
let temp = head;
// 当 temp === fast 在入口点相遇了
while (temp !== fast) {
temp = temp.next;
fast = fast.next;
}
return temp;
}
}
return null;
};
160. 相交链表
解题思路:
其实刚开始有一个比较稳妥的思路,就是说需要我们提供一个额外的存储空间;
借助于set结构,我们先存一条链表,然后看另一条链表中是不是有重复出现的元素,就判断两者就是相交的状态
代码实现:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
// 存一条 取一条
let set = new Set();
let temp = headA;
while(temp !== null) { // error
set.add(temp)
temp = temp.next;
}
temp = headB;
while(temp !== null) { // error
if (set.has(temp)) {
return temp;
}
temp = temp.next;
}
return null;
};
如果不使用额外的空间,使用两个指针能做吗?
这种思路的难点在于: 当两条链表的长度不一致的时候, 如何保证两个链表的节点可以对应上
如果, p1和p2两个节点同时前进, 怎么能保证同时到达相交的节点呢?
这个问题确实是一个棘手的问题, 我们想想如何去解决呢?
如上图所示, 我们可以让p1先遍历完A链表,然后在遍历B链表; 让p2先遍历完B链表,然后在遍历A链表;这样在逻辑上实现了两条链表连接到一起, 然后可以找到相交的节点
你可能还会问一句, 如果没有相交节点呢, 也就是说最后会返回null, 其实我们已经包括这种判断。
代码实现:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
// 如果不使用额外的空间
// 使用两个指针能做吗?
let p1 = headA, p2 = headB;
// p1!== p2 还没相交
while(p1 !== p2) {
if (p1 === null) p1 = headB;
else {
p1 = p1.next;
}
if (p2 === null) p2 = headA;
else {
p2 = p2.next;
}
}
return p1;
};