快慢指针&双指针

[leetcode]灵魂画师图解🎨快慢指针在算法中的应用

天下武功, 无坚不破, 唯快不破 ——— 某功夫大佬

本文为我,leetcode easy player,algorithm小xuo生在刷题过程中对快慢指针的应用的总结

ps: 向leetcode里耐心写解题, 特别是画图解题的各位算法大佬们致敬, 给大佬们递茶🍵

什么是快慢指针

  1. 快慢说的是移动的速度, 即每次移动的步长的大小
  2. 指针为记录变量内存地址的变量, 用它能间接访问变量的值

为了更直观的了解快慢指针, 请看如下c++demo

在内存中开辟容量为11个整型元素的数组存储空间

初始化整型快慢指针变量都记录数组第一个元素的内存地址

 

167 两数之和

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};

    }
}

26. 删除有序数组中的重复项

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;
    }
}

无环的链表是长这样的

有环的链表是长这样的

  1. 染色标记法, 缺点: 改变了链表的结构, 若链表为只可读的则不可取, 而且此方法需开辟额外的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 };

  2. 哈希表记录法, 缺点: 哈希表需开辟额外的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 }

  3. 快慢指针法, 兔子与乌龟同时在头节点出发, 兔子每次跑两个节点, 乌龟每次跑一个节点, 如果兔子能够追赶到乌龟, 则链表是有环的

因为不管有没有环, 以及进环前和进换后耗时都与数据规模成正比, 所以时间复杂度为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
};
复制代码

回文链表

  1. 把链表变成双向链表, 并从两端向中间比较

时间复杂度为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
  }
};
复制代码
  1. 快慢指针
  • 慢指针🐢依次访问下一个节点, 并翻转链表
  • 快指针🐰依次访问下下一个节点, 直到快指针🐰没有下一个节点(奇数链表)或者快指针指向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!

leetcode11  盛水最多的容器

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;
    // }
}

42. 接雨水

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…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值