[leetcode]灵魂画师图解🎨快慢指针在算法中的应用
天下武功, 无坚不破, 唯快不破 ——— 某功夫大佬
本文为我,leetcode easy player
,algorithm小xuo生
在刷题过程中对快慢指针的应用的总结
ps: 向leetcode
里耐心写解题, 特别是画图解题的各位算法大佬们致敬, 给大佬们递茶🍵
什么是快慢指针
快慢
说的是移动的速度, 即每次移动的步长的大小指针
为记录变量内存地址的变量, 用它能间接访问变量的值
为了更直观的了解快慢指针, 请看如下c++
demo
在内存中开辟容量为11个整型元素的数组存储空间
初始化整型快慢指针变量都记录数组第一个元素的内存地址
class Solution {
public int[] twoSum(int[] numbers, int target) {
int left=0,right=numbers.length-1;
while(left<right){
int sum=numbers[left]+numbers[right];
if(sum==target){
return new int[]{left+1,right+1};
}else if(sum>target){
right--;
}else{
left++;
}
}
return new int[]{-1,-1};
}
}
class Solution {
public int removeDuplicates(int[] nums) {
int slower=0,faster=0;
while(faster<nums.length){
if(nums[slower]!=nums[faster]){
slower++;
nums[slower]=nums[faster];
}
faster++;
}
return slower+1;
}
}
使用双指针实现字符串反转
public static String reverseString(String str) {
if (str == null || str.length() == 0 || str.length() == 1) {
return str;
}
char[] chars = str.toCharArray();
//指向数组首元素
int start = 0;
//指向数组尾元素
int end = chars.length - 1;
char temp;
while (start < end) {
//字符互换
temp = chars[start];
chars[start] = chars[end];
chars[end] = temp;
//移动索引位置
start++;
end--;
}
return String.valueOf(chars);
}
public static void main(String[] args) {
String str = "gfedcba";
System.out.println("字符串:" + str);
System.out.println("反转后:" + reverseString(str));
}
字符串:gfedcba
反转后:abcdefg
判断链表是否有环
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slower=head,faster=head;
while(faster!=null&&faster.next!=null){
faster=faster.next.next;
slower=slower.next;
if(faster==slower){
return true;
}
}
return false;
}
}
无环的链表是长这样的
有环的链表是长这样的
-
染色标记法, 缺点: 改变了链表的结构, 若链表为只可读的则不可取, 而且此方法需开辟额外的
O(n)
存储空间记录标记信息var hasCycle = function(head) { let res = false while (head && head.next) { if (head.flag) { res = true break } else { head.flag = 1 head = head.next } } return res };
-
哈希表记录法, 缺点: 哈希表需开辟额外的
O(n)
空间var hasCycle = function(head) { const map = new Map() while (head) { if (map.get(head)) { return true } else { map.set(head, head) head = head.next } } return false }
-
快慢指针法, 兔子与乌龟同时在头节点出发, 兔子每次跑两个节点, 乌龟每次跑一个节点, 如果兔子能够追赶到乌龟, 则链表是有环的
因为不管有没有环, 以及进环前和进换后耗时都与数据规模成正比, 所以时间复杂度为O(n)
, 因为只需额外的常数级存储空间记录两个指针, 所以空间复杂度为O(1)
var hasCycle = function(head) {
let slowPointer = head
let fastPointer = head
while (fastPointer && fastPointer.next) {
slowPointer = slowPointer.next
fastPointer = fastPointer.next.next
if (fastPointer === slowPointer) {
return true
}
}
return false
}
复制代码
寻找链表的入环节点
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode faster=head,slower=head;
while(faster!=null&&faster.next!=null){
faster=faster.next.next;
slower=slower.next;
if(faster==slower){
slower=head;
while(slower!=faster){
slower=slower.next;
faster=faster.next;
}
return faster;
}
}
return null;
}
}
此题也可用标记法和哈希表法解决, 用快慢指针法解决如下
还是前面的龟兔赛跑, 当兔子追到乌龟的时候, 假设有另外一只乌龟从头节点开始往前爬, 每次也只爬一个节点, 那么两只乌龟会在入环的节点相遇
这只是一个巧合吗, 我们来分析一下
- 假设入环之前的长度为
L
, 入环之后快慢指针第一相遇时快指针比慢指针🐢多跑了N
圈, 每一圈的长度为C
, 此时快指针🐰在环内离入环节点的距离为C'
- 此时慢指针🐢走过的距离为:
L + C'
- 此时快指针🐰走过的距离为:
L + C' + N * C
- 因为快指针🐰的速度是慢指针🐢的两倍, 所以有:
2 * (L + C') = L + C' + N * C
- 整理后得到:
(N - 1) * C + (C - C') = L
- 由此可知, 若此时有两个慢指针🐢同时分别从链表头结点和快慢指针第一次相遇的节点出发, 两者必然会在入环节点相遇
var detectCycle = function(head) {
let slowPointer = head
let fastPointer = head
while (fastPointer && fastPointer.next) {
slowPointer = slowPointer.next
fastPointer = fastPointer.next.next
if (slowPointer === fastPointer) {
slowPointer = head
while (slowPointer !== fastPointer) {
slowPointer = slowPointer.next
fastPointer = fastPointer.next
}
return slowPointer
}
}
return null
};
复制代码
寻找重复数
class Solution {
public int findDuplicate(int[] nums) {
int left=1,right=nums.length;
while(left<right){
int middle=(left+right)/2;
int sum=0;
for(int num :nums){
if(num<=middle){
sum++;
}
}
if(sum>middle){
right=middle;
}else{
left=middle+1;
}
}
return left;
}
}
此题暴力解法为先排序再寻找重复的数字, 注意不同JavaScript
引擎对sort
的实现原理不一样, V8
引擎 sort
函数对数组长度小于等于 10 的用插入排序(时间复杂度O(n^2)
, 空间复杂度O(1)
),其它的用快速排序(时间复杂度O(nlogn)
, 递归栈空间复杂度O(logn)
), 参考github.com/v8/v8/blob/…
这一题可以利用寻找链表的入环节点的思想, 把数组当成对链表的一种描述, 数组里的每一个元素的值表示链表的下一个节点的索引
如示例1中的[1, 3, 4, 2, 2]
- 把数组索引为0的元素当成链表的头节点
- 索引为0的元素的值为1, 表示头节点的下一个节点的索引为1, 即数组中的3
- 再下一个节点的索引为3, 即为第一个2
- 再下一个节点的索引为2, 即为4
- 再下一个节点的索引为4, 即为第二个2
- 再下一个节点的索引为2, 即为4
- 此时形成了一个环
- 而形成环的原因是下一节点的索引一致, 即为重复的数字
var findDuplicate = function(nums) {
let slowPointer = 0
let fastPointer = 0
while (true) {
slowPointer = nums[slowPointer]
fastPointer = nums[nums[fastPointer]]
if (slowPointer == fastPointer) {
let _slowPointer = 0
while (nums[_slowPointer] !== nums[slowPointer]) {
slowPointer = nums[slowPointer]
_slowPointer = nums[_slowPointer]
}
return nums[_slowPointer]
}
}
};
复制代码
删除链表的倒数第N个元素
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode fast=head,slow=head;
ListNode result=new ListNode(0,head);
ListNode dumpy=result;
if(head.next==null&&n>0){
return null;
}
while(n>0&&fast!=null){
fast=fast.next;
n--;
}
while(fast!=null){
fast=fast.next;
dumpy=dumpy.next;
}
dumpy.next=dumpy.next.next;
return result.next;
}
}
要删除链表的倒数第N个元素, 需要找到其倒数第N + 1个元素, 让这个元素的next指向它的下下一个节点
此题可利用两次正向遍历链表, 或者一次正向遍历的同时记录前节点, 然后再反向遍历
题目的进阶要求只使用一趟扫描, 利用快慢指针可实现, 我们最终想要的乌龟和兔子的位置是这样的, 它们之间相距N + 1
个节点, 这样乌龟所在的位置即为我们想要找的那个节点--被删除的节点前面的一个节点
为方便处理头节点, 我们创建dummy
虚拟头节点
让快指针🐰和慢指针🐰最开始都指向dummy
节点
让快指针🐰向前移动N + 1
个节点, 慢指针保持原地不动
然后两个指针以同样的速度直至快指针🐰移动至null
此时慢指针🐢移动到的位置即为被删除的指针前面的一个指针
var removeNthFromEnd = function(head, n) {
const dummy = new ListNode(null)
dummy.next = head
let slowPointer = dummy
let fastPointer = dummy
while (n-- > -1) {
fastPointer = fastPointer.next
}
while (fastPointer !== null) {
slowPointer = slowPointer.next
fastPointer = fastPointer.next
}
slowPointer.next = slowPointer.next.next
return slowPointer === dummy ? slowPointer.next : head
};
复制代码
链表的中间节点
快慢指针法, 快慢指针开始时都指向头节点, 快指针每次移动一个节点, 慢指针每次移动两个节点
对于奇数链表, 当快指针下一节点为null
时, 慢指针指向的节点即为所求
对于偶数链表, 当快指针指向null
时, 慢指针指向的节点即为所求
var middleNode = function(head) {
let slowPointer = head
let fastPointer = head
while (fastPointer !== null && fastPointer.next !== null) {
slowPointer = slowPointer.next
fastPointer = fastPointer.next.next
}
return slowPointer
};
复制代码
回文链表
- 把链表变成双向链表, 并从两端向中间比较
时间复杂度为O(n)
, 因为要存储prev
指针, 所以空间复杂度为O(n)
var isPalindrome = function(head) {
if (head === null) {
return true
} else {
let headPointer = head
let tailPointer = head
while (tailPointer.next) {
tailPointer.next.prev = tailPointer
tailPointer = tailPointer.next
}
while(headPointer !== tailPointer) {
if (headPointer.next !== tailPointer) {
if (headPointer.val === tailPointer.val) {
headPointer = headPointer.next
tailPointer = tailPointer.prev
} else {
return false
}
// 考虑偶数链表
} else {
return headPointer.val === tailPointer.val
}
}
return true
}
};
复制代码
- 快慢指针
- 慢指针🐢依次访问下一个节点, 并翻转链表
- 快指针🐰依次访问下下一个节点, 直到快指针🐰没有下一个节点(奇数链表)或者快指针指向
null
(偶数链表), 此时已完成了前半截链表的翻转 - 依次比较前半截链表和后半截链表节点的值
对于奇数链表:
对于偶数链表:
时间复杂度O(n)
, 空间复杂度O(1)
var isPalindrome = function(head) {
if (head === null) {
return true
} else if (head.next === null) {
return true
} else {
let slowPointer = head
let fastPointer = head
let _head = null
let temp = null
// 找到中间节点, 并翻转前半截链表,
// 让_head指向翻转后的前半截链表的头部,
// 让slow指向后半截链表的头节点
while (fastPointer && fastPointer.next) {
_head = slowPointer
slowPointer = slowPointer.next
fastPointer = fastPointer.next.next
_head.next = temp
temp = _head
}
// 奇数链表跳过最中间的节点
if (fastPointer) {
slowPointer = slowPointer.next
}
while (_head) {
if (_head.val !== slowPointer.val) {
return false
}
_head = _head.next
slowPointer = slowPointer.next
}
return true
}
};
复制代码
在一些场景, 如链表数据结构和判断循环, 利用快慢指针创造的差值, 可节省内存空间, 减少计算次数
快慢指针, 一对快乐的指针, just be happy!
class Solution {
public int maxArea(int[] height) {
int left=0,right=height.length-1,maxArea=0;
if(height==null||height.length==0){
return maxArea;
}
while(left<right){
maxArea=Math.max(maxArea,Math.min(height[left],height[right])*(right-left));
if(height[left]>height[right]){
right--;
}else{
left++;
}
}
return maxArea;
}
// public int maxArea(int[] height) {
// int maxArea=0;
// if(height==null||height.length==0){
// return maxArea;
// }
// for(int i=0;i<height.length-1;i++){
// int curHeight=height[i];
// for(int j=height.length-1;j>i;j--){
// if(height[j]>=height[i]){
// maxArea=Math.max(maxArea,height[i]*(j-i));
// break;
// }else{
// maxArea=Math.max(maxArea,height[j]*(j-i));
// }
// }
// }
// return maxArea;
// }
}
class Solution {
// 1.首先我们需要搞清楚,下标为i的雨水量是由什么决定的.
// 是由i左右两边最大值中较小的那个减去height[i]决定的.例 [0,1,0,2,1,0,1,3,2,1,2,1]中,下标为2的位置 值为0,而它的用水量是由左边的最大值1,右边最大值3 中较小的那个 也就是1减去0得到的。
// 2.本题解的双指针先找到当前维护的左、右最大值中较小的那个,例 当前 i 处左边的最大值如果比右边的小,那么就可以不用考虑 i 处右边最大值的影响了,因为 i 处 右边真正的最大值绝对比左边的最大值要大,在不断遍历时,更新max_l和max_r以及返回值即可。例 [0,1,0,2,1,0,1,3,2,1,2,1]中i=2时,值为0,此时max_l一定为1,当前max_r如果为2,即便max_r不是真正的i右边的最大值,也可忽略右边最大值的影响,因为右边真正的最大值一定比左边真正的最大值大。
public int trap(int[] height) {
if(height==null||height.length==0){
return 0;
}
int result=0,left=0,right=height.length-1,leftMax=0,rightMax=0;
while(left<right){
int heightLeft=height[left];
int heightRight=height[right];
leftMax=Math.max(leftMax, heightLeft);
rightMax=Math.max(rightMax, heightRight);
if(leftMax<rightMax){
left++;
result+=(leftMax-heightLeft);
}else{
right--;
result+=(rightMax-heightRight);
}
}
return result;
}
// public int trap(int[] height) {
// if(height==null||height.length==0){
// return 0;
// }
// int result=0;
// Deque<Integer> stack=new LinkedList<Integer>();
// for(int i=0;i<height.length;i++){
// int curHeight=height[i];
// if(stack.isEmpty()){
// stack.push(i);
// }else{
// while(!stack.isEmpty()&&curHeight>height[stack.peek()]){
// //当出现升高的柱子之后,低位柱子最右边的索引
// int rightIndex=stack.pop();
// if(stack.isEmpty()){
// break;
// }
// //最右边柱子的左边柱子可能之前已经被计算掉了一些,需要用左边未计算的第一个柱子计算
// //例如[4,2,0,3,2,5] 3时已经把之前2 0 就算,5时先把2计算,之后应该从4开始计算高度3的量
// int leftIndex=stack.peek();
// result+=(Math.min(curHeight,height[leftIndex])-height[rightIndex])*(i-leftIndex-1);
// }
// stack.push(i);
// }
// }
// return result;
// }
}
原文在掘金: juejin.im/post/684490…