02 双指针


对应题目类型

  • 需要双层循环遍历数组的,可以考虑使用双指针方法
  • 双指针有些是针对一个列表的问题定义两个指针;有些是对两个列表的问题,每个列表分别定义一个指针
  • 双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。
  • 双指针法可以将时间复杂度降低一个数量级

无序序列:

  1. 删除列表中的某些元素——快慢双指针:slow指向当前待插入位置、fast指向当前元素【原地删除】
  2. 求解和为目标值的n个数目元组——(三数之和、四数之和问题)使用相对双指针将复杂度降低一个等级 首先需要对数组进行排序!!!【例:三数之和:固定a对bc进行相对双指针选取,时间复杂度O(n^2)】

有序序列:

  1. 两个有序序列的合并——快慢双指针分别指向两个列表的头
  2. 有序序列寻找满足某条件的元素对——相对双指针,根据两个指针与目标的大小关系,判断移动哪个指针

链表题目:(大部分都用双指针

  1. 删除链表的倒数第N个节点——slow,fast两个指针之间的间隔定义为N
  2. 判断链表是否相交,求交点——slow,fast两个指针,分别从两个链表的头结点开始移动,移动的范围是两个链表的和
  3. 判断链表是否有环,求环的起始位置——slow,fast两个指针,fast每次走两个节点;slow每次走一个节点

解题思想

使用两个指针对数组进行遍历:

  • 快慢双指针:
    • 快指针:寻找要保留的元素
    • 慢指针:用于指向要更改的元素的位置
  • 相对双指针
    • 左指针:
    • 右指针:

快慢指针不会更改元素的原相对顺序;相对指针具有更少的元素移动。

快慢双指针

双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

  • 暴力解法时间复杂度:O(n^2)
  • 双指针时间复杂度:O(n)

▶例:移除数组中的某个元素。
其实就是将数组中要保留的元素全部移动到数组的前面。

定义:slow=0,fast=0
slow指向的是当前可以更改的元素位置,fast寻找当前需要保留的元素
如果,nums[fast]==target,fast++
如果,nums[fast]!=target,即要保留当前元素,nums[slow++]=nums[fast++]
循环结束时,当前slow就是要保留的元素序列的下一个位置,也就是保留的元素的个数。

注意使用快慢指针不会改变原来元素的相对位置!

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        fast,slow=0,0
        while fast<len(nums):
            if(nums[fast]!=val):
                nums[slow]=nums[fast]
                slow+=1
            
            fast+=1
        return slow

相对双指针

▶例:移除数组中的某个元素。
其实就是将数组前面的要移除的元素与数组后面要保留的元素交换位置,这样数组就变成前面是要保留的元素后面是要删除的元素。

定义,left=0,right=len(nums)-1
首先,要对left向后移动找要移除的元素,
找到之后,要对right向前移动寻找要保留的元素,
找到之后,交换left与right的元素。
重复上面的步骤。

注意两点:
1、整体的循环条件是,left<=right
2、对left以及right移动时要注意循环条件应该有两个:left<=right and nums[left]!=target

相对指针的方法相比于快慢指针方法具有更少的元素移动,但是他们的时间复杂度都是O(n)

class Solution(object):
    def removeElement(self, nums, val):
        """
        :type nums: List[int]
        :type val: int
        :rtype: int
        """
        # 相对双指针实现
        
        left,right=0,len(nums)-1

        while left<=right:
            while left<=right and nums[left]!=val:
                left+=1
            while right>=left and nums[right]==val:
                right-=1
            
            if left<right:
                temp=nums[right]
                nums[right]=nums[left]
                nums[left]=temp

        return  left

题目汇总

删除数组中的某些元素

27. 移除元素

https://leetcode.cn/problems/remove-element/

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

https://leetcode.cn/problems/remove-duplicates-from-sorted-array/

给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。
将最终结果插入 nums 的前 k 个位置后返回 k 。
不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

思想:其实就是将不删除的元素移动到数组的前面部分

class Solution(object):
    def removeDuplicates(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        # 使用快慢指针
        if len(nums)==0:
            return 0

        slow,fast=1,1
        while fast<len(nums):
            if nums[fast]!=nums[fast-1]:
                nums[slow]=nums[fast]
                slow+=1
            fast+=1
        return slow

283.移动零

https://leetcode.cn/problems/move-zeroes/

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。

class Solution(object):
    def moveZeroes(self, nums):
        """
        :type nums: List[int]
        :rtype: None Do not return anything, modify nums in-place instead.
        """
        slow,fast=0,0

        while fast<len(nums):
            if nums[fast]!=0:
                nums[slow]=nums[fast]
                slow+=1
            
            fast+=1
        
        while slow<len(nums):
            nums[slow]=0
            slow+=1

844.比较含退格的字符串

https://leetcode.cn/problems/backspace-string-compare/

给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。
注意:如果对空文本输入退格字符,文本继续为空。

该题目最直接的方法是使用栈实现。
但是如果非要使用二分法实现,也可以,如下:
快慢双指针从后往前处理

//使用双指针实现
        int sp=s.length()-1;
        int tp=t.length()-1;
        int scount=0;
        int tcount=0;
        while(sp>=0 || tp>=0){
            while(sp>=0){
                if(s.charAt(sp)=='#'){
                    scount++;
                    sp--;
                }else if(scount!=0){
                    sp--;
                    scount--;
                }else{
                    break;
                }
            }
            while(tp>=0){
                if(t.charAt(tp)=='#'){
                    tcount++;
                    tp--;
                }else if(tcount!=0){
                    tp--;
                    tcount--;
                }else{
                    break;
                }
            }
            if(sp>=0 && tp>=0){
                if(s.charAt(sp)!=t.charAt(tp)){
                    return false;
                }
            }else if(sp>=0 || tp>=0){
                return false;
            }else{
                return true;
            }
            sp--;
            tp--;
        }
        return true;
    }

977.有序数组的平方

https://leetcode.cn/problems/squares-of-a-sorted-array/submissions/

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

思想:
对于一个有正有负的数组,其元素平方之后,结果大的元素一定出现在原数组的左右两端,
因此使用相对指针解决更好。

class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        # 使用双指针方法-相对指针:因为平方的最大值一定出现在数组的两端

        left,right=0,len(nums)-1
        pownums=[-1]*len(nums)
        k=right

        while(left<=right):
            left2=nums[left]**2
            right2=nums[right]**2
            if left2>=right2:
                pownums[k]=left2
                left+=1
            else:
                pownums[k]=right2
                right-=1
            k-=1
        return pownums

例1:有序序列寻找元素对(a,b)使得a+b=m

求解思想:

给定一个递增序列。查找序列中的两个元素a,b,使其元素和为m。查找所有的满足条件的元素对。

暴力算法:双层循环,遍历。 时间复杂度为 O(n^2)
简化:two pointers。如果暴力遍历历会存在很多无效枚举,因此想办法避免这种无效枚举。 时间复杂度 O(n)

使用相对在双指针

递增序列中,i,j两个pointers,i<j
如果:
(1)datai+dataj==m:
那么后续如果还有元素对的值可以等于m,元素对的取值区间一定在[i+1,j-1]之中
(2)datai+dataj<m:
如果存在元素对的值等于m,其取值区间在[i+1,j]之中,也就是要令i++
(3)datai+dataj>m:
如果存在元素对的值等于m,其取值区间在[i,j-1]之中,也就是要令j–

#include<cstdio>

void search(int A[],int n,int m){
    int i=0,j=n-1;
    while(i<j){
        if(A[i]+A[j]==m){
            printf("%d+%d=%d\n",A[i],A[j],m);
            i++;
            j--;
        }
        else if(A[i]+A[j]<m){
            i++;
        }
        else{
            j--;
        }
    }
}

int main(){
    const int n=10;
    int A[n]={1,2,3,4,5,6,7,8,9,10};
    int m=5;
    search(A,n,m);
    return 0;
}

例2:序列合并问题,两个递增序列合并为一个递增序列

#include<cstdio>

void solve(int A[],int lenA,int B[],int lenB,int C[]){
    int i=0,j=0,k=0;
    while(i<lenA&&i<lenB){
        if(A[i]<=B[j]){
            C[k]=A[i];
            k++;
            i++;
        }
        else{
            C[k]=B[j];
            j++;
            k++;
        }
    }
    while(i<lenA){
        C[k++]=A[i++];
    }
    while(j<lenB){
        C[k++]=B[j++];
    }
}

int main(){
    const int n=5;
    const int nn=10;
    int A[n]={1,3,5,7,9};
    int B[n]={2,4,6,8,10};
    int C[nn]={};
    solve(A,n,B,n,C);
    for(int i=0;i<nn;i++){
        printf("%d ",C[i]);
    }
    return 0;
}

数组中找出所有和为目标值的n个数的所有结果

15. 三数之和

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

解题思想:

区分于hash table中相关题目:

  • 454. 四数相加 II 计算四个数组中满足和为目标值的四元组的个数;
  • 1. 两数之和 计算数组中唯一一个和为目标值的二元组的元素的下标

该题目要求,返回所有的不重复三元组,返回的是元素值
再使用hash table对于重复元素的处理不好做。

如果使用hash table解决,思考方式:
1、首先对nums先求解所有的两数之和,使用两重循环即可,利用dict存储:

  • key:a+b
  • value:[[index(a),index(b)]]
  • 可能存在一个key对应多个a+b,因此value是一个列表,其中的元素时下标二元组

2、然后对nums进行遍历,看每个元素是否是target-key

  • 如果==:那么,将该元素作为c与对应value进行组合,其中还要判断c是否存在于value中(即c是否与a,b重复

上述解法,会导致三倍重复。比如,三个元素0,1,2的和为目标值,上述解决方式会对这三个元素产生三种结果:

(a,b,c)—>(0,1,2)(0,2,1)(1,2,0),造成三倍重复。

因此,考虑对数组进行排序后使用双指针法降低时间复杂度:

  • 首先对原数组进行排序
  • 然后,对于排序后的数组,以固定a寻找b,c解决步骤处理:
    • a采用对数组进行遍历的方式,for a in range(0,len(nums)-2)
    • b\c采用二分查找的方式:
      • b=a+1
      • c=len(nums)-1
      • 如果b+c>target-a:c–
      • 如果b+c<target-a:b++
      • 如果b+c==target-a:记录a,b,c作为一个结果
  • 去重:a+b+c三个元素的组合,
    • 首先,要保重a不可以重复,
    • 其次,a确定时,要保证b也不可以重复
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        # 双指针法
        # 首先对数组进行排序
        if len(nums)<3:
            return []

        nums.sort()

        res=[]
        a=0
        while a <= len(nums) - 3 and nums[a] <= 0:
            if a>0 and nums[a]==nums[a-1]:    # 对a进行去重
                a+=1
                continue

            b = a + 1
            c = len(nums) - 1

            while b < c:
                if b>a+1 and nums[b]==nums[b-1]:  # 对b进行去重
                    b+=1
                    continue
                if nums[b] + nums[c] < -nums[a]:
                    b += 1
                elif nums[b] + nums[c] > -nums[a]:
                    c -= 1
                else:
                    temp=[nums[a], nums[b], nums[c]]
                    res.append(temp)
                    b += 1
                    c -= 1

            a += 1
        return res


18. 四数之和

题目介绍:
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • a、b、c 和 d 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target
    你可以按 任意顺序 返回答案 。

解题思想:
类比于三数之和的解法,其实就是对三数之和的解题步骤增加一个循环。
相比暴力解法O(n4)的时间复杂度,采用**排序+二分**的方法会降低一个单位的时间复杂度,变为O(n4)

  • 第一层循环:对a进行遍历,for a in range(0,len(nums)-3)
  • 第二层循环:对b进行遍历,for b in range(a+1,len(nums)-2)
  • 然后采用二分查找的方式对c,d进行寻找
  • 去重:类比三数之和中的去重操作,要保证
    • a不可重复
    • a确定的情况下,b不可重复
    • a,b确定的情况下,c不可重复
class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        //寻找四数之和的元素,采用双指针法相比暴力法时间复杂度降低一个等级
        List<List<Integer>> res=new ArrayList<List<Integer>>();
        Arrays.sort(nums);
        int len=nums.length;
        for(int a=0;a<=len-4;a++){
            if(a>0 && nums[a]==nums[a-1]){
                continue;
            }
            if((long)nums[a]+nums[a+1]+nums[a+2]+nums[a+3]>target){
                break;
            }
            if((long)nums[a]+nums[len-1]+nums[len-2]+nums[len-3]<target){
                continue;
            }
            for(int b=a+1;b<=len-3;b++){
                if(b>a+1 && nums[b]==nums[b-1]){
                    continue;
                }
                if((long)nums[a]+nums[b]+nums[b+1]+nums[b+2]>target){
                    break;
                }
                if((long)nums[a]+nums[b]+nums[len-2]+nums[len-1]<target){
                    continue;
                }
                int c=b+1;
                int d=len-1;
                //System.out.printf(a+" "+b+" "+c+" "+d);
                while(c<d){
                    if(c>b+1 && nums[c]==nums[c-1]){
                        c++;
                        continue;
                    }
                    long abcd=(long)nums[a]+nums[b]+nums[c]+nums[d];
                    if(abcd==target){
                        List<Integer> temp=new ArrayList<Integer>();
                        temp.add(nums[a]);
                        temp.add(nums[b]);
                        temp.add(nums[c]);
                        temp.add(nums[d]);
                        res.add(temp);
                        c++;
                        d--;
                    }else if(abcd<target){
                        c++;
                    }else{
                        d--;
                    }
                }
            }
        }
        return res;
    }
}

易错点1:

  • 数据取值范围-10^9 <= nums[i] <= 10^9
  • 10^9 =1,000,000,000
  • 2^31=2,147,483,648 int类型的表示范围:-231~231-1
  • 因此如果直接进行nums[a]+nums[b]+nums[c]+nums[d]==target的比较可能会出现数据越界的情况
  • 因此要进行类型强转转成long型:(long)nums[a]+nums[b]+nums[c]+nums[d]==target

易错点2:

  • 该问题是三重循环,因此时间复杂度为O(n)
  • 为了尽可能减小代码的运行时间可以进行 剪枝
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值