小曾带你刷leetcode--双指针篇之左右指针(一)

小曾带你刷leetcode--双指针篇之左右指针(一)_第1张图片

文章目录

  • LeetCode简介
  • 怎么样刷LeetCode
    • 双指针
      • 左右指针
        • 3、无重复字符的最长子串
        • 11. 盛最多水的容器
        • 15、三数之和
        • 18 、四数之和
        • 26、删除有序数组中的重复项
        • 42.接雨水

LeetCode简介

当我们谈到leetcode,作为一个程序员来说,各位小伙伴应该也并不陌生把。这个网站成立于2011年,到如今也有10个年头了,在这个平台里面不仅包含各大互联网大厂的面试题目,还有一些针对各个算法的一些习题,供国内外的程序员来进行刷题,提升自己的能力。
现在基本上也是刷题必备,是一个准备面试非常有用工具。

怎么样刷LeetCode

其实对于很多小伙伴来说,都有这种疑惑。这也是千人千面,对于基础掌握比较好的同学来说,可以直接讲题库从头开始刷,也未尝不可。对于我这种基础比较薄弱,而且想不断的提升自己的能力的小伙伴来说,分章节来刷,是一个不错的选择。优势也很明显,能够反复练习这个内容,总结出属于自己的刷题套路,培养出自己的解题思维,这是对于每一个程序员是可望而不可即,是一个慢慢积累,打怪的过程。小曾陪大家一起开始刷LeetCode之旅!
小曾带你刷leetcode--双指针篇之左右指针(一)_第2张图片

双指针

双指针从广义上来说,是指用两个变量在线性结构上遍历而解决的问题。狭义上说,

  • 对于数组,指两个变量在数组上相向移动解决的问题,也称为「左右指针」问题;
  • 对于链表,指两个变量在链表上同向移动解决的问题,也称为「快慢指针」问题;

左右指针

首先判断是用左右指针还是快慢指针,这个可以根据数据结构来进行选择,如果给出的是一个数组,那么可以考虑用左右指针。左右指针在数组中实际是指两个索引值,⼀般初始化为 left = 0, right =nums.length - 1 。

滑动窗口算法框架:

int	left	=	0,	right	=	0;
while	(right	<	s.size())	{`
	#增大窗口
	window.add(s[right]);				
	right++;
	while	(window	needs	shrink)	{
		//	缩⼩窗⼝								
		window.remove(s[left]);								
		left++;
		}
}
3、无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例:输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3

题目分析:不含有重复字符
思路:用到了滑动窗口的算法,滑动窗⼝的右边界不断的右移,只要没有重复的字符,就持续向右扩大窗口边界。⼀旦出现了重复字符,就需要缩小左边界,直到重复的字符移出了左边界,然后继续移动滑动窗口的右边界。以此类推,每次移动需要计算当前长度,并判断是否需要更新最大长度度,最终最大的值就是题目中的所求。
如何判断重复字符呢?
可以用常用的数据结构–哈希集合(HashSet、set) ,在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。

算法代码实现:

python版本
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        #哈希集合,记录字符是否出现过
        occ = set()
        n = len(s)
        rk , ans = -1,0
        for i in range(n):
            #左指针向右移动一个,并删除一个字符
            if i !=0:
                occ.remove(s[i-1])
            while rk+1 < n and s[rk+1] not in occ:
                #不断移动右指针
                occ.add(s[rk+1])
                rk +=1
            #第i个到rk 极长的无重复字符子串
            ans = max(ans , rk - i + 1) 

        return ans
java版本
class Solution {
    public int lengthOfLongestSubstring(String s) {
        //hash 集合
        Set occ = new HashSet();
        int n = s.length();
        int rk = -1,ans =0;
        for (int i =0 ; i

分析时间复杂度:O(N) [左指针和右指针分别会遍历整个字符串一次]

11. 盛最多水的容器

给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

小曾带你刷leetcode--双指针篇之左右指针(一)_第3张图片

输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

题目分析:首先要清楚容纳水量的公式 ,水量 = 左右指针指向的数字的较小值 *左右指针的距离
题目思路:方案一、暴力法,两个for循环,然后选择最大容纳水量的即可。方案二、双指针法,设置双指针 i , j分别位于容器壁两端,左右指针对应的高度h[i],h[j],水槽的实际高度是由两个板子的短板决定的,每次选择两板高度h[i],h[j]中的短板,向中间修改一格,并且更新面积最大值result,直至i==j时返回最大值。

复杂度分析:
时间复杂度:O(N),双指针总计最多遍历整个数组一次
空间复杂度:O(1)

python版本
class Solution:
    def maxArea(self, height: List[int]) -> int:
        num = len(height)
        x,y = 0,num -1
        max_num =0
        while xheight[y]:
                y -=1
            else:
                x +=1
        ## 返回最大容纳水量
        return max_num
java代码
class Solution {
    public int maxArea(int[] height) {
        int i = 0 ,j = height.length-1 ;
        int maxA = 0;
        while(i
15、三数之和

给你一个包含 n 个整数的数组 nums判断 nums 中是否存在三个元素 a,b,c 使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]

分析:主要采用排序 + 双指针的方法

本题的难点在于如何去除重复解

算法流程:

1、特判,对于数组长度 n,如果数组为 null 或者数组长度小于 3,返回 []。
2、对数组进行排序。
3、遍历排序后数组:

  • 若 nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于 0,直接返回结果。
  • 对于重复元素:跳过,避免出现重复解
  • 令左指针 L=i+1,右指针 R=n-1,当 L (1)当 nums[i]+nums[L]+nums[R]==0,执行循环,判断左界和右界是否和下一位置重复,去除重复解。并同时将 L,R移到下一位置,寻找新的解
    (2)若和大于 0,说明 nums[R] 太大,R左移
    (3)若和小于 0,说明 nums[L] 太小,L 右移

复杂度分析
时间复杂度:O(n^2)
数组排序 O(NlogN),遍历数组 O(n),双指针遍历 O(n),总体 O(NlogN)+O(n)∗O(n),O(n^2)
空间复杂度:O(1)

Python3 代码

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
    	#对数组进行排序
        nums.sort()
        #对于数组长度 n,如果数组为 null 或者数组长度小于 3,返回 []
        if len(nums)<3 or nums[0]>0:
            return []
        i , n = 0,len(nums)
        ans = []
        #遍历排序
        for i in range(n-2):
        	#对于重复元素:跳过,避免出现重复解
            if i>0 and nums[i] == nums[i-1]:  # 去重
                continue
            l ,r = i+1 ,n-1
            #令左指针 L=i+1,右指针 R=n-1,当 L

java代码

class Solution {
    public List> threeSum(int[] nums) {
         List> lists = new ArrayList<>();
        //排序
        Arrays.sort(nums);
        //双指针
        int len = nums.length;
        for(int i =0;i0)  return lists;
            if(i>0 && nums[i]==nums[i-1]) continue;

            int curr =nums[i];
            int L=i+1 ,R =len -1;
            while(Llist = new ArrayList<>();
                    list.add(curr);
                    list.add(nums[L]);
                    list.add(nums[R]);
                    lists.add(list);
                    while(L
18 、四数之和

跟三数之和相比,就是多加了一个for循环,具体请参考下面代码

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        n = len(nums)
        nums.sort()
        ans = []
        if not nums or len(nums)<4:
            return []
        for i in range(n-3):
            if i>0 and nums[i]== nums[i-1]:
                continue
            for j in range(i+1,n-2):
                if j>i+1 and nums[j]== nums[j-1]:
                    continue
                l,r = j+1,n-1
                while l
26、删除有序数组中的重复项

题目描述:给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素

题目分析:去除重复项
思路:使用双指针的思想进行遍历
具体步骤:
1、一个指针 i 进行数组遍历,另外一个指针 j 指向有效数组的最后一个位置。
2、只有当 i 所指向的值和 j 不一致(不重复),才将 i 的值添加到 j 的下一位置。

python 3

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        n = len(nums)
        j = 0
        for i in range(n):
            if nums[i] !=nums[j]:
                j+=1
                nums[j]=nums[i]
            
        return j+1

java版本

class Solution {
    public int removeDuplicates(int[] nums) {
        int n =nums.length;
        int j = 0;
        for(int i=0;i

时间复杂度:O(N)
空间复杂度:O(1)

遇到相似题目有通用解法

为了让解法更具有一般性,我们将原问题的「最多保留 1 位」修改为「最多保留 k 位」。
对于此类问题,我们应该进行如下考虑:
由于是保留 k 个相同数字,对于前 k 个数字,我们可以直接保留。
对于后面的任意数字,能够保留的前提是:与当前写入的位置前面的第 k 个元素进行比较,不相同则保留。

举个,我们令 k=1,假设有样例:[3,3,3,3,4,4,4,5,5,5]

 1. 设定变量 idx,指向待插入位置。idx 初始值为 0,目标数组为 []
 2. 首先我们先让第 1 位直接保留(性质 1)。idx 变为 1,目标数组为 [3]
 3. 继续往后遍历,能够保留的前提是与 idx 的前面 1 位元素不同(性质 2),因此我们会跳过剩余的 3,将第一个 4 追加进去。idx
    变为 2,目标数组为 [3,4]
 4.继续这个过程,跳过剩余的 4,将第一个 5 追加进去。idx 变为 3,目标数组为 [3,4,5]
 5、当整个数组被扫描完,最终我们得到了目标数组 [3,4,5] 和 答案 idx 为 3。
 
注:这里借鉴了leetcode中宫水三叶的思路,感兴趣可以去看一看。

总结:「通用解法」是一种针对「数据有序,相同元素最多保留 k 位」问题更加本质的解法,该解法是从性质出发提炼的,利用了「数组有序 & 保留逻辑」两大主要性质

42.接雨水

题目描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下
可以接 6 个单位的雨水(蓝色部分表示雨水)

小曾带你刷leetcode--双指针篇之左右指针(一)_第4张图片
**题目分析:**可以直接暴力方法求解,对于数组中的每个元素,我们找出下雨后水能达到的最高位置,等于两边最大高度的较小值减去当前高度的值。
**关键:**找到左右两边最高的墙(max_left,max_right)
直观理解:计算存水量,主要分三种情况

  1. 较矮的墙的高度大于当前列的墙的高度
    小曾带你刷leetcode--双指针篇之左右指针(一)_第5张图片
    简化,去掉无关的墙
    小曾带你刷leetcode--双指针篇之左右指针(一)_第6张图片
    很明显,较矮的一边,也就是左边的墙的高度,减去当前列的高度就可以了,也就是 2 - 1 = 1,可以存一个单位的水。
  2. 较矮的墙的高度小于当前列的墙的高度
    小曾带你刷leetcode--双指针篇之左右指针(一)_第7张图片
    想象下,往两边最高的墙之间注水。正在求的列会有多少水?
    正在求的列不会有水,因为它大于了两边较矮的墙。
  3. 较矮的墙的高度等于当前列的墙的高度,也不会有水
    小曾带你刷leetcode--双指针篇之左右指针(一)_第8张图片
    具体流程:
  • 初始化 ans = 0 =0 =0
  • 从左向右扫描数组:
    (1)初始化 max_left = 0 =0 =0 和 max_right = 0 =0 =0
    (2) 从当前元素向左扫描并更新:max_left = max ⁡ ( max ⁡ − =\max \left(\max _{-}\right. =max(max left, height [ j ] ) \left.[j]\right) [j])
    (3) 从当前元素向右扫描并更新:max_right = max ⁡ ( =\max ( =max( max_right, height [ j ] ) [j]) [j])
    (4)将min(max_left, max_right) − - height [ i ] [i] [i] 累加到 ans
java版本:用两个for循环
public int trap(int[] height) {
    int ans = 0;
    int size = height.length;
    for (int i = 1; i < size - 1; i++) {
        int max_left = 0, max_right = 0;
        //找到左边最高
        for (int j = i; j >= 0; j--) {
            max_left = Math.max(max_left, height[j]);
        }
        // 找到右边最高
        for (int j = i; j < size; j++) { 
            max_right = Math.max(max_right, height[j]);
        }
        // 通过找到两端最小的减去当前高度计算存水量
        ans += Math.min(max_left, max_right) - height[i];
    }
    // 返回总的存水量
    return ans;
}

上述的方法是非常直观,也很容易想到,但是时间复杂度是比较高的O(N^2)

采用双指针的优点:将时间复杂度降低到O(N),只遍历一遍。
具体思路:维护两个指针 left 和 right, 以及两个变量 leftMax 和 rightMax,
初始时 left = 0 =0 =0, right = n − 1 =n-1 =n1, leftMax = 0 =0 =0, rightMax = 0 =0 =0 。 指针
left 只会向右移动, 指针 right 只会向左移动,在移动指针的过
程中维护两个变量 left ⁡ M a x \operatorname{left} M a x leftMax rightMax ⁡ \operatorname{rightMax} rightMax 的值。

python3 代码
class Solution:
    def trap(self, height: List[int]) -> int:
        ans = 0
        left, right = 0, len(height) - 1
        leftMax = rightMax = 0
		# 开始从左向右开始遍历
        while left < right:
            # 思路与上面一致,分别计算左、右边最大值
            leftMax = max(leftMax, height[left])
            rightMax = max(rightMax, height[right])
            if height[left] < height[right]:
                ans += leftMax - height[left]
                left += 1
            else:
                ans += rightMax - height[right]
                right -= 1
        
        return ans
java版本
class Solution {
    public int trap(int[] height) {
        int ans = 0;
        int left = 0, right = height.length - 1;
        int leftMax = 0, rightMax = 0;
        while (left < right) {
            leftMax = Math.max(leftMax, height[left]);
            rightMax = Math.max(rightMax, height[right]);
            if (height[left] < height[right]) {
                ans += leftMax - height[left];
                ++left;
            } else {
                ans += rightMax - height[right];
                --right;
            }
        }
        return ans;
    }
}

后续题目会继续添加,同时我目前也在看labuladong 的算法小抄和halfrost的LeetCode刷题手册,对我受益匪浅,现在推荐给大家,大家一起刷题,共勉!
在这里插入图片描述
在这里插入图片描述
同时需要这些资料的小伙伴们,可以关注一下“研行笔录”微信公众号,回复leetcode即可,还不快来领取leetcode刷题大礼包。
小曾带你刷leetcode--双指针篇之左右指针(一)_第9张图片

你可能感兴趣的:(小曾带你刷力扣,leetcode,指针,算法,数据结构,python)