双指针训练

1.原理

双指针是一种解题常用方法,常用于将数组按照某种要求进行分块/划分,这里的指针对于数组来说,可以替换成下标(毕竟使用下标实际上就是用了指针)。

1.1.划分区间

通常将这两个指针命名位dest/cur(或者begin/end),利用这两个指针将区间划分为三个不同性质的区间[0, dest-1][dest, cur-1][cur, n-1],在cur移动的时候,保持三个区间的性质不变。

实际上这也是快速排序中的前后指针、三路划分中使用的原理。仔细回想一下,快速排序的算法逻辑中,实际上也是将下标[left, right]之间的数据划分为[left, key-1][key, key][key+1, right],并且让这三个区间保持某种性质不变(在快排中是第一个区间比key小/大,最后一个区间比key大/小)。

具体可以看一下这一道:[2.1.[283. 移动零]](##2.1.[283. 移动零])。

1.2.覆盖重写

利用双指针可以达到覆盖数组的目的,这里可以看看:[2.2.[1089. 复写零]](##2.2.[1089. 复写零])。

1.3.快慢指针

经典的应用就是判断链表是否带环,可以看看:[2.3.[202. 快乐数]](##2.3.[202. 快乐数])。

1.4.单调性

有些时候将序列排序后使用双指针会有很高效的解法,可以看看:[2.5.[611. 有效三角形的个数]](##2.5.[611. 有效三角形的个数])、[2.6.[LCR 179. 查找总价格为目标值的两个商品]](##2.6.[LCR 179. 查找总价格为目标值的两个商品])

2.题目

2.1.283. 移动零

这道题目的解法有很多,这里我写一个常见的解法:使用cur扫描/遍历数组,使用dest来作为分界线,在划分成的[0,dest-1][dest, cur-1][cur, n-1]中,三个区间的性质是:[0,dest-1]是已经处理过确认非0的区间,[dest, cur-1]是确认为0的区间,[cur, n-1]是待处理的部分,然后接下来一直移动cur直至[cur, n - 1]为空,区间一直保持以上的性质。

class Solution
{
public:
    void moveZeroes(vector<int>& nums)
    {
        size_t dest = 0;   
        size_t cur = 0;     
        while(cur < nums.size())
        {
            if(nums[cur] != 0)
            {
                std::swap(nums[cur], nums[dest]);
                dest++;
            }
            cur++;
        }
    }
};

时间复杂度为 O ( n ) O(n) O(n),因为只需要遍历一次即可解决问题
空间复杂度为 O ( 1 ) O(1) O(1),因为不计输入数据,只用到了两个有限的变量

2.2.1089. 复写零

首先最容易想到的就是使用另外一个vector ret顺序表来存储复写后的数组,并且拷贝回vector arr即可。

class Solution
{
public:
    void duplicateZeros(vector<int>& arr)
    {
        std::vector<int> ret;
        for(auto it : arr)
        {
            ret.push_back(it);
            if(it == 0)
            {
                ret.push_back(0);
            }
        }
        int i = 0;
        for(auto& it : arr)
        {
            it = ret[i]; 
            i++;
        }
    }
};

虽然可以过,但是忽略了“就地修改”这个规则,这又该怎么办呢?

我们可以定义一个size指向“复写操作后数组”的最后一个元素,cur指向“复写操作前数组”的最后一个元素,也就是说:对于arr=[1,0,2,3,0,4,5,0]经过复写操作后为ret=[1,0,0,2,3,0,0,4]。因此cur下标指向arr[5]=4的位置,假设size下标指向arr[7]=0的位置,然后从右往左判断覆盖(非0arr[7]=arr[5]0则复写两个0),重复操作即可。

但是怎么找到“复写操作后数组”的最后一个元素的下标呢?依旧可以使用双指针寻找,你么看错,就是双指针里面套双指针。

  1. 我们假设size=0, cur=0然后让cur遍历整个数组,只要遇到0就让size+=2, cur++,如果是遇到非0就让size++, cur++
  2. 也就是说size实时记录了“复写操作后数组”的大小
  3. 最后遍历完整个数组,此时的cur一定指向复写操作后数组的最后一个元素的后面
  4. 然后执行之前说的“从右向左覆盖”操作即可
class Solution
{
public:
    void duplicateZeros(vector<int>& arr)
    {
        //1.寻找复写操作后,数组的最后一个元素
        int cur = 0;//寻找“复写操作后数组”的最后元素的下标
        int size = 0;//记录“复写操作后数组”的大小

        //[例子1]
        //复写前:[1,5,2,0,6,8,0,6,0]
        //复写后:[1,5,2,0,0,6,8,0,0]
        //[例子2]
        //复写前:[5,2,1,3,0]
        //复写后:[5,2,1,3,0]0 会溢出一个0
        while (size < (int)arr.size())
        {
            if (arr[cur] == 0)
            {
                size += 2;
            }
            else
            {
                size += 1;
            }
            cur++;
        }
        cur--;
        size--;

        //2.从右往左做开始复写
        //[例子1]
        //复写前:[1,5,2,0,6,8,0,6,0],cur->arr[5]=4, size->arr[7]=0
        //复写后:[1,5,2,0,0,6,8,0,0]
        //[例子2]
        //复写前:[5,2,1,3,0],cur->arr[4]=0, size->arr[5] 越界了 
        //复写后:[5,2,1,3,0]0 会溢出一个0

        if (arr[cur] == 0 && size >= arr.size())
        //当出现最后一个元素为0的时候很特殊,有两种可能:
        //1.复写操作后溢出了一个0,这种情况只复写一次即可
        //2.复写操作后刚好最后两个元素为0,这种情况正常复写即可
        {
            size--;
            arr[size] = arr[cur];
            size--;
            cur--;
        }
        while (cur != -1)
        {
            if (arr[cur] != 0)
            {
                arr[size--] = arr[cur--];
            }
            else
            {
                arr[size--] = arr[cur];
                arr[size--] = arr[cur];
                cur--;
            }
        }
    }
};

时间复杂度为 O ( n ) O(n) O(n),两次循环就是 O ( n + n ) = O ( 2 n ) O(n+n)=O(2n) O(n+n)=O(2n),也就是 O ( n ) O(n) O(n)
空间复杂度为 O ( 1 ) O(1) O(1),只用到了常数个有限的变量

2.3.202. 快乐数

实际上这是一道双链表判断是否回环的问题,可以使用快慢指针(双指针的一种使用方法)来判断链表是否存在回环点。本题最麻烦的问题就是如何知道是否会出现无限循环的情况。

using namespace std;
class Solution
{
private:
    int _function(int number)//计算平方值
    {
        int add = 0;
        while (number)
        {
            add += (number % 10) * (number % 10);
            number /= 10;
        }
        return add;
    }
public:
    bool isHappy(int number)
    {
        //循环判断是否happy
        int p = number;//走一步的指针(慢指针)
        int curp = number;//走两步的指针(快指针)
        while (true)
        {
            p = _function(p);//得4,走一步...
            curp = _function(_function(curp));//得16,走两步...
            if (p == curp)//如果找到循环点,也就是快慢指针相遇,就进入if语句
            {
                if (p == 1)//如果相遇点处的值为1就返回true
                {
                    return true;
                }
                return false;//如果不是1就返回false
            }
        }
    }
};

但是这道题目实际上是出简单的了,题目已经提前告诉我们取平方后只有两种情况:

  1. 最后得到1(实际上也是循环)
  2. 最后是无限循环,一直在一个回环里循环

但是实际上,从简单的思考上,是否存在:无限计算下去得不到1,但是不存在环的可能?我们可以使用”鸽笼原理“:在n个鸽笼中放入n+1只鸽子,则至少有一个笼子,里面的鸽子数量大于1

我们设int的范围中最大的正数为(0111 1111 1111 1111 1111 1111 1111 1111)2=(21 4748 3647)10≈(2.1*10^9)2

假设有一个比int更大的数(99 9999 9999)10,则经过平方后得到9^2*10=810。因此[1, 99 9999 9999]范围内的数,经过平方后得到的数一定在[0, 810]的范围内,因此我们就有了810个鸽笼,假设最坏情况下一个属于[1, 99 9999 9999]数经过平方后得到1填充了一个笼,再次平方得到2填充第二个笼…一直到810个笼子全部填完(这里只是假设最坏情况,实际不太可能达到),则第811次平方后又得到了一个[0, 810]内的数,因此造成了循环。

那对于int范围内的数就更加会出现循环的情况了,毕竟int范围内的数平方得到的新数一定在[0, 810]的范围内,如果不循环,那么每一个数才放进小于810的笼子里根本不够用!

2.4.11. 盛最多水的容器

本题难度相较于前面会上升一些,首先最容易的解法就是:暴力穷举,直接上两个循环,枚举出所有的情况,找到最大的容积,但是使用这种方法一定会超时。

但是本题可以在遍历的时候作出节省,这是一种重要的思路:有的时候不得不使用枚举时,可以考虑是否剔除一些情况达到提高枚举效率的目的。

  1. 首先使用两个指针beginend指向数组heights[]的首尾处
  2. 计算高和宽height = min{heights[begin], heights[end]}width = end - begin,然后尾插到volumes
  3. 然后从heights[begin]heights[end]选择中最小的那一个数的下标begin++或者end--
  4. 只要满足begin < end就一直循环1~3即可
  5. 最后找到volumes中最大的体积返回即可
class Solution 
{
public:
    int maxArea(vector<int>& heights) 
    {
        int begin = 0, end = (int)heights.size() - 1;

        vector<int> volumes;
        
        int width = 0;
        int height = 0;
        while (begin < end)
        {
            //1.取得“最大的宽”
            width = end - begin;

            //2.取得“最小的高”
            height = heights[begin];
            if (height > heights[end])
            {
                height = heights[end--];
            }
            else
            {
                begin++;
            }
            //3.计算区间内最大面积
            volumes.push_back(width * height);
        }
        return *std::max_element(volumes.begin(), volumes.end());;
    }
};

时间复杂度是 O ( n ) O(n) O(n),由于是两个指针在遍历数组,因此就相当于遍历整个数组,因此数据复杂度就是 O ( n ) O(n) O(n)

空间复杂度是 O ( 1 ) O(1) O(1),这点没什么好解释的

2.5.611. 有效三角形的个数

一般而言,判断三个数abc能否构成三角形的依据是:
{ a + b > c a + c > b b + c > a \begin{cases} a+b>c\\ a+c>b\\ b+c>a \end{cases} a+b>ca+c>bb+c>a
但是,如果将abc三个数有序为a <= b <= c,就只需要判断一次a + b > c即可,因此就可以先把整个数组排序好后再来判断是否构成三角形。

因此暴力一点的话可以像下面这么写:

class Solution
{
    bool check(vector<int>& nums, int i, int j, int k)
    {
        return (nums[i] + nums[j] > nums[k]);
    }
public:
    int triangleNumber(vector<int>& nums)
    {
        sort(nums.begin(), nums.end());

        int count = 0;
        for (int i = 0; i < nums.size(); i++)
        {
            for (int j = i + 1; j < nums.size(); j++)
            {
                for (int k = j + 1; k < nums.size(); k++)
                {
                    if (check(nums, i, j, k))
                    {
                        count++;
                    }
                }
            }
        }
        return count;
    }
};

但是这只能过大部分的例子,还有一些情况没有办法过(数据量太大),因此还需要做优化。

首先数组已经是有序(具有单调性)的了,只需要判断a + b > c即可。

则从最后一个元素 x n x_{n} xn开始证明有 x 0 + x n − 1 > x n x_{0}+x_{n-1}>x_{n} x0+xn1>xn即可直接认为 x n − 2 x_{n-2} xn2后的数都可以和 x n − 1 x_{n-1} xn1以及 x n x_{n} xn构成三角形,无需判断…

class Solution
{
public:
    int triangleNumber(vector<int>& nums)
    {
        //1.排序数组
        sort(nums.begin(), nums.end());

        //2.通过三角形的性质结合单调性判断有多少种符合的情况
        int count = 0;//存储有多少种组合
        int key = nums.size() - 1, begin = 0, end = key - 1;//分为区间[begin, end]key
        while (key >= 2)
        {
            while (begin < end)
            {
                if (nums[begin] + nums[end] > nums[key])
                    //如果nums[begin] + nums[end] > key,
                    //那么nums[end]加上“nums[begin]...nums[end-1]”的任意一个数都大于key
                    //然后变成[begin, end--]key,也就是说:明确了和nums[end]结合有end - begin种情况
                {
                    count += (end - begin);
                    end--;
                }
                else//如果不是这样,那么就变成[begin++, end]key
                {
                    begin++;
                }
            }
            begin = 0;//最后重置一下左区间索引
            end = --key - 1;//最后重置一下右区间索引(并且修改key)
        }
        return count;
    }
};

时间复杂度: O ( n 2 ) O(n^{2}) O(n2)

2.6.LCR 179. 两数之和

这道题目也是利用单调性达到节省枚举次数的目的。

  1. 首先选定最后一个数price[end],和数组其他的数price[begin]相加
  2. 如果发现相加值等于目标值,则返回price[begin]price[end]
  3. 如果发现相加值小于目标值,则begin++
  4. 如果发现相加值大于于目标值,则说明不用继续移动begin了。因为此时的price[begin] + price[end] > target,那么根据单调性,price[begin~end-1] + price[end]都大于target。此时begin重置为0,然后end--,重复1~4步骤即可
class Solution
{
public:
    vector<int> twoSum(vector<int>& price, int target)
    {
        //1.创建要返回的顺序表
        vector<int> ret;

        //2.创建双指针begin和end
        int end = price.size() - 1;
        int begin = 0;

        //3.从最后一个指针开始循环,每次循环向前一步
        while (end != 0)
        {
            while (begin < end)
            {
                if (price[begin] + price[end] == target)//如果相等就找到了
                {
                    ret.push_back(price[begin]);
                    ret.push_back(price[end]);
                    return ret;
                }
                else if (price[begin] + price[end] < target)//如果小于就找比price[begin]更大的数
                {
                    begin++;
                }
                else//price[begin] + price[end] > target,这个时候已经大于目标值了,没有必要再循环让begin++了,直接跳出即可
                {
                    break;
                }
            }
            begin = 0;//重置begin
            end--;
        }
        return ret;
    }
};

或者简化为以下代码(上述代码还有些冗余):

class Solution
{
public:
    vector<int> twoSum(vector<int>& price, int target)
    {
        //1.创建要返回的顺序表
        vector<int> ret;

        //2.创建双指针begin和end
        int end = price.size() - 1;
        int begin = 0;

        //3.从最后一个指针开始循环,每次循环向前一步
        while (begin < end)
        {
            if (price[begin] + price[end] > target)//如果相等就找到了
            {
                end--;
            }
            else if (price[begin] + price[end] < target)//如果小于就找比price[begin]更大的数
            {
                begin++;
            }
            else
            {
                ret.push_back(price[begin]);
                ret.push_back(price[end]);
                break;
            }
        }

        return ret;
    }
};

时间复杂度:O(n)

2.7.LCR 007. 三数之和

class Solution
{
public:
    vector<vector<int>> threeSum(vector<int> nums)
    {
        //1.设置要返回的顺序表
        vector<vector<int>> ret;
        //2.排序原数据数组
        sort(nums.begin(), nums.end());
        //3.查找复合条件的三元组
        int n = nums.size();
        for (int i = 0; i < n; )
        {
            if (nums[i] > 0) break;

            int left = i + 1, right = n - 1, target = -nums[i];
            while (left < right)
            {
                int sum = nums[left] + nums[right];
                if (sum > target)
                {
                    right--;
                }
                else if (sum < target)
                {
                    left++;
                }
                else
                {
                    ret.push_back({ nums[i], nums[left], nums[right] });
                    left++, right--;

                    //去重
                    while (left < right && nums[left] == nums[left - 1]) left++;
                    while (left < right && nums[right] == nums[right + 1]) right--;
                }
            }
            i++;
            while (i < n && nums[i] == nums[i - 1]) i++;
        }
        return ret;
    }
};

这题比较难,以后再做然后补充。

时间复杂度: O ( n 2 ) O(n^{2}) O(n2)

2.8.18. 四数之和

class Solution
{
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target)
    {
        vector<vector<int>> ret;
        sort(nums.begin(), nums.end());

        int n = nums.size();
        for (int i = 0; i < n; )
        {
            for (int j = i + 1; j < n; )
            {
                int left = j + 1, right = n - 1;
                long long aim = (long long)target - nums[i] - nums[j];//防止溢出
                while (left < right)
                {
                    int sum = nums[left] + nums[right];
                    
                    if (sum < aim)
                    {
                        left++;
                    }
                    else if (sum > aim)
                    {
                        right--;
                    }
                    else
                    {
                        ret.push_back({ nums[i], nums[j], nums[left++], nums[right--] });
                        //去重
                        while (left < right && nums[left] == nums[left - 1]) left++;
                        while (left < right && nums[right] == nums[right + 1]) right--;
                    }
                }
                //去重
                j++;
                while (j < n && nums[j] == nums[j - 1]) j++;
            }
            //去重
            i++;
            while (i < n && nums[i] == nums[i - 1]) i++;
        }
        return ret;
    }
};

时间复杂度: O ( n ) = n 3 O(n) = n^{3} O(n)=n3

空间复杂度:主要消耗在排序那里

你可能感兴趣的:(常见算法训练,c++,开发语言,算法,数据结构)