双指针法-

双指针法

双指针法应该只是一种思想,而非算法,一开始我没有理解透其中的妙处,特别是对于三数之和的问题,先前特别困惑为何双指针法能将时间复杂度降低为 O ( N 2 ) O(N^2) O(N2),在写题,debug的过程中结合力扣的官方题解,总算是有些理解。

三数之和问题(力扣第15题)

三数之和
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
常规暴力思路

这里多提一嘴,在刚开始准备一些机考写题的过程中,我非常不重视题目的暴力解法,很多时候直接跳过暴力解法的思考过程而直接去想更高效的算法,例如回文字符的动态规划等等,但是最近也有在社区的一些分享中学习到说暴力解法的重要性,它是我们理解得出更高效算法的基础,这点被我忽视了很久导致对一些算法的思考不够深入。在对本题的思考过程中,我充分理解到了暴力解法对思考过程的重要性。

事实上,要直接对本题使用三层循环的方法解题也不算简单,因为问题中提到了不可以包含重复的三元组,对于这个问题最高效的解法很容易想到的是先排序,然后在遍历的过程中直接对重复的元素进行跳过。由于暴力解法代码比较简单,大致写一下伪代码。

sort(nums);
for(i;i<n;i++)//i即为第一个数字
{
    if(nums[i]==nums[i-1])//判断是否是重复的数字,若是,则直接跳过,我们只需要对重复数字中第一个出现的进行判断即可
        continuefor(j=i+1;j<n;j++)//j为第二个数字
    {
        if(nums[j]==nums[j-1])//判断是否是重复的数字
            continue;
        for(k=j+1;k<n;k++)
        {
            if(nums[k]==nums[k-1])//判断是否是重复的数字
                continue;
            if(nums[i]+nums[k]+nums[k]==0)
                若符合要求则加入结果
        }
    }
}

很明显暴力解法的时间复杂度为 O ( N 3 ) O(N^3) O(N3),是非常糟糕的,因此考虑对代码进行优化。

排序+双指针法

1、首先根据前面的分析,首先需对数组进行排序,利用 s o r t ( ) sort() sort()函数对数组进行默认的升序排序后,可以得到一个新的升序数组,如[-2,1,0,1,1,2,3],则我们枚举出的结果 ( n u m s [ i ] , n u m s [ j ] , n u m s [ k ] ) (nums[i],nums[j],nums[k]) (nums[i],nums[j],nums[k]),一定满足 n u m s [ i ] ≤ n u m s [ j ] ≤ n u m s [ k ] nums[i]\le nums[j]\le nums[k] nums[i]nums[j]nums[k]
2、此外容易发现,当前两个数 n u m s [ i ] , n u m s [ j ] nums[i],nums[j] nums[i],nums[j]确定之后,nums[k]的值也是唯一确定的,并且其中有一个重要的关系,由于升序排序数组的缘故,假设遍历到了 j j j的下一个合法位置 j ′ j' j j ′ > j j'>j j>j,即 j ′ j' j j j j的右侧,有 n u m s [ j ′ ] > n u m s [ j ] nums[j']>nums[j] nums[j]>nums[j],因此 k k k的下一个遍历位置 k ′ k' k必须满足 n u m s [ k ′ ] < n u m s [ k ] nums[k']< nums[k] nums[k]<nums[k],才能够保证有 n u m s [ i ] + n u m s [ j ] + n u m s [ k ] = 0 nums[i]+ nums[j]+ nums[k]=0 nums[i]+nums[j]+nums[k]=0的可能性出现,由于在升序数组中,这意味着 k ′ k' k必须在 k k k的左侧 .

根据上述两点性质分析,我们发现可以遍历的过程可以从小到大枚举 j j j,同时从大到小枚举 k k k,因此第二重与第三重循环本质上可以是一种并列的关系。
这便是双指针的想法,我们保持第二重对于j的循环不变,其意义为从数组左边开始往右移动的指针;将第三重循环变成从数组最右端开始往左移动的指针。理解了这个性质,我们来看下面的代码。

主要核心代码如下

for (int i = 0; i < n; i++) //第一重循环
{
    if (i != 0 && nums[i] == nums[i - 1])    //若重复,跳过
    {
        continue;
    }
    else
    {
        int k = n - 1; //重点在这里!!!!!!
        for (int j = i + 1; j < n; j++) //第二重循环
        {
            
            if (j != i+1 && nums[j] == nums[j - 1]) //若重复,跳过
            {
                continue;
            }
            while (j < k && nums[j] + nums[k]>=-nums[i])
            {
                if (k != n - 1 && nums[k] == nums[k + 1]) //若重复,跳过
                {
                    k = k - 1;      //k指针左移
                    continue;
                }
                if (nums[i] + nums[j] + nums[k] == 0)  //找到了,定下j时,由分析的性质知每一次k循环最多出现一个符合的结果,则遍历出后可以跳出k循环
                {
                    result.push_back({nums[i],nums[j],nums[k]});
                    break;
                }
                k = k - 1;  //k指针左移
            }

            if(j==k) //若两指针相遇,则跳出第二重循环
                break;
        }
    }
}
代码分析

个人认为,最核心的代码在于int k = n-1;所在的位置,它出现在第二层循环前面,我最开始自己写双指针的时候,虽然分析到了上述的性质,但是在代码编写的时候,习惯性地将该定义语句放到了第二重循环内部:

for (int i = 0; i < n; i++) //第一重循环
{
    if (i != 0 && nums[i] == nums[i - 1])    //若重复,跳过
    {
        continue;
    }
    else
    {
        for (int j = i + 1; j < n; j++) //第二重循环
        {
            int k = n - 1; 
            ......
            while(...)

这导致代码本身仍旧是三重循环 O ( N 3 ) O(N^3) O(N3),那么为什么将int k=n-1;放在第二重循环前面就能简化为 O ( N 2 ) O(N^2) O(N2)了呢?

核心

主要是与后面的if(j==k) break;语句配合,在第二重循环中,j左指针向右移动一个位置,k指针就会向左移动若干位置,若找到三数和为0的位置,则记录三数,并且k指针停留在原地,一直移动到两个指针相遇,就会触发if语句跳出第二层循环,此时双指针一共移动的位置数为N(j,k指针合计走的路程),操作步数为N,时间复杂度为 O ( N ) O(N) O(N)。因此加上第一层循环,总的时间复杂度为 O ( N 2 ) O(N^2) O(N2).

最后贴上完整代码,未对输入作处理,直接初始化了一个vector。

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main()
{
	vector<int> nums = { -1,0,1,2,-1,-4};
	int n = nums.size();
	sort(nums.begin(), nums.end());
	vector<vector<int>>result;
	for (int i = 0; i < n; i++)
	{
		if (i != 0 && nums[i] == nums[i - 1])    //若重复,跳过
		{
			continue;
		}
		else
		{
			int k = n - 1;
			for (int j = i + 1; j < n; j++)
			{
				
				if (j != i+1 && nums[j] == nums[j - 1])
				{
					continue;
				}
				while (j < k&&nums[j] + nums[k]>=(-nums[i]))
				{
					if (k != n - 1 && nums[k] == nums[k + 1])
					{
						k = k - 1;
						continue;
					}
					if (nums[i] + nums[j] + nums[k] == 0)  //找到了,定下j时,由特性知每一次k循环最多出现一个,则可以跳出k循环
					{
						result.push_back({nums[i],nums[j],nums[k]});
						break;
					}
					k = k - 1;
				}
				if(k==j)
					break;
			}
		}
	}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值