双指针技巧与链表的经典问题
虚心接受批评和指正,互相成就,共勉!
从这篇文章你会收获什么?
- 双指针技巧;
- 双指针的常见用法;
- 经典问题;
- 总结
目录
- 双指针技巧;
- 环形链表
- 环形链表II
- 相交链表
- 删除链表的倒数第N个节点
- 注意
指针:c语言中指针变量是用来存放内存地址的变量
两种常用双指针技巧的情景
- 两个指针
从不同位置出发
:一个从起点开始,一个从终点开始; - 两个指针
以不同速度出发
:一个指针快一些,一个指针慢一些;
在单链表中,不同位置出发毫无意义,但是不同速度出发,却可以让我们找到一些规律,这种技巧也叫快慢指针
话不多说,我们上LeetCode上链表的经典题去理解快慢指针
的作用
环形链表
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。
思路
- 一个有环的链表,比作一个环形跑道,跑的快的运动员迟早会再次和跑得慢的运动员相遇
- 慢指针一次一定1位,快指针一次移动2位,快指针如果跑道了终点,证明无环,否则一定会相遇
代码
var hasCycle = function(head) {
if(!head || !head.next) return false;
var slow = head
var fast = head.next
while( slow != fast ){
if(fast == null || fast.next == null){
return false
}
slow = slow.next
fast = fast.next.next
}
return true
};
环形链表II
思路
- 第一阶段,我们先确定当前链表是否存在环;
- 第二阶段,如果存在环,我们要确定入口的位置:
- 入口前节点个数为a,环节点个数为b;
- 快节点走过的节点数 = 2 * 慢节点走过的节点数,即 f = 2s;
- 快节点走过的节点数 = s + nb(多揍了n倍的环)(相遇时);
- 由此得出:s = nb(相遇时);
- 如果说慢指针走到入口的距离 k = a + nb,两点相遇时s = nb,即再走a步即可;
- 我们重新确定双指针(速度要相同),新指针走 a 步应该到达入口,慢指针走a步也会到达入口;
- 新指针推断为 头节点,两者相遇的节点为入口;
代码
var hasCycle = function(head) {
if(!head || !head.next) return false;
var slow = head
var fast = head.next
while( slow != fast ){
if(fast == null || fast.next == null){
return false
}
slow = slow.next
fast = fast.next.next
}
return true
};
相交链表
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。如果有环,则确定它的入口位置。
思路
- 定义A、B节点对应headA、headB链表
- 如果两个链表相交,那么他们的共同点就是相交节点及以后的节点相等;
- 在未知长度的链表中,为了保证两个节点走相同的路程。
- A节点走完后,走B节点,假设A链表交点前的节点数尾a
- B节点走完后,走A节点,假设A链表交点前的节点数尾b
- 如果存在交点:A和B在相遇前都走了
a+共同节点+b
,此时A === B,并指向入口位置
代码
var getIntersectionNode = function(headA, headB) {
var A = headA
var B = headB
while(A || B){
if(A === B) return A
A = A ? A.next : headB
B = B ? B.next : headA
}
return null
};
删除链表的倒数第N个节点
思路
这里利用的思想是,不同位置,相同速度的双指针思想
- 倒数N个节点,可以理解成正数第
链表长度 - N
节点。 - 所以我们要找到头节点之后的第
链表长度 - N - 1
个节点 - 让它跳过下一节点即可
- 让快节点先走N+1步
- 之后,让慢节点从头节点与快节点一起移动,
- 快节点走到尾端时,慢节点走了
链表长度 - N - 1
- 做删除操作即可
代码
var removeNthFromEnd = function(head, n) {
var current = { val: '', next: head }
var fast = current
var slow = current
for (let i = 1; i <= n + 1; i++) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return current.next;
}
注意
我们要注意一些事情:
- 在调用 next 字段之前,始终检查节点是否为空。
获取空节点的下一个节点将导致空指针错误。例如,在我们运行 fast = fast.next.next 之前,需要检查 fast 和 fast.next 不为空。
- 仔细定义循环的结束条件。
运行几个示例,以确保你的结束条件不会导致无限循环。在定义结束条件时,你必须考虑我们的第一点提示。
复杂度分析
-
空间复杂度分析容易。如果只使用指针,而不使用任何其他额外的空间,那么空间复杂度将是 O(1)。但是,时间复杂度的分析比较困难。为了得到答案,我们需要分析运行循环的次数。
-
在前面的查找循环示例中
- 假设我们每次移动较快的指针 2 步,每次移动较慢的指针 1 步。
- 如果没有循环,快指针需要 N/2 次才能到达链表的末尾,其中 N 是链表的长度。
- 如果存在循环,则快指针需要 M 次才能赶上慢指针,其中 M 是列表中循环的长度。
- 显然,M <= N 。所以我们将循环运行 N 次。对于每次循环,我们只需要常量级的时间。
- 因此,该算法的时间复杂度总共为 O(N)。
下一篇,我们来介绍一下双链表的原理,及Js的基本实现,共勉!