双指针数组解法

Catalog
    目录

      之前博客介绍了采用双指针法来解决链表类题目Link。 本博文主要介绍用双指针法来解决数组类题目,并且把数组类相关的解题思路也放在此博客中。
      数组与链表是最基本的两种数据结构,其他均可以由这两种构成。 链表是用离散的内存块存储数据,而数组是用连续的内存块存储数据。 因此,对于数组,只需要直知道内存空间首地址(也就是数组名),通过索引即可访问任意元素。
      在处理数组和链表相关问题时,双指针技巧是经常用到的,双指针技巧主要分为两类:左右指针快慢指针

      • 左右指针: 两个指针相向而行或者相背而行。
      • 快慢指针:两个指针同向而行,一快一慢。
      虽然在数组中并没有真正意义上的指针,但可以把索引当做数组中的指针,这样也可以在数组中施展双指针技巧,

      
      
      


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

      Image description

      第一次遇到这个题目的时候,还是比较困惑的,主要是返回什么。题目要求是“原地修改”。如果不是原地修改的话,我们直接 new 一个 int[] 数组,把去重之后的元素放进这个新数组中,然后返回这个新数组即可。 而所谓的“原地修改”,其实就是原地删除,不允许 new 新数组,只能在原数组上操作,然后返回一个长度,这样就可以通过返回的长度在原始数组中截取去重后的元素了。
      对于此代码,原本有两个解法:

      解法1:采用unique函数。时间复杂度O(Nlogn),空间复杂度为O(1)
      
        
      
      解法2:采用键值对,时间复杂度为O(n),由于需要一个键值对,空间复杂度估计也是O(n)
      
        
      

      而快慢指针法的思路就是:慢指针 slow 走在后面,快指针 fast 走在前面探路,找到一个不重复的元素就赋值给 slow 并让 slow 前进一步。
      这样,就保证了 nums[初始点..slow] 都是无重复的元素,当 fast 指针遍历完整个数组 nums 后,nums[初始点..slow] 就是整个数组去重之后的结果。

      Click to expand the code. 时间复杂度为O(n),空间复杂度为O(1)
      Image description

      
        class Solution {
          public:
              int removeDuplicates(vector<int>& nums) {
          
                  //采用双指针法(快慢指针)时间复杂度为O(n),空间复杂度为O(1)
                  int slow=0, fast=0;
                  while(fast!=nums.size())//遍历整个数组
                  {
                      if(nums[slow]!=nums[fast])//找到不等的
                      {
                          slow++;//slow自增(必须先自增,因为一开始fast=slow,下一刻再判断时必须要先把slow自增)
                          nums[slow]=nums[fast];//进行赋值
                      }
                      fast++;
                  }
                  //代码将不重复的元素放在数组前面slow索引中~
          
                  return slow+1;//这个就是排在前面的不重复的size(注意要+1)
              }
          };
          
      


      对于前面的解法1和解法2,特别是解法1,除非比较熟悉cpp的函数,不然一般不懂使用,所以更建议实际解题的时候用算法去解题而不是用库函数取巧🤭 而类似的题目链表类博客中有记录了,给出了两个解法(见Leetcode中83题)


      2.移除元素(原地删除而非上面的换位置)

      Image description

      解法1:数组函数的使用,时间复杂度O(Nlogn+N);空间复杂度O(1)
      
        class Solution {
          public:
              int removeElement(vector<int>& nums, int val) {
          
                  // 先进行排序,排序后只需要对找到对应值的位置进行删除即可
                  sort(nums.begin(),nums.end());//时间复杂度为O(nlogn)
                  auto index=find(nums.begin(),nums.end(),val);//返回的是指针(时间复杂度为O(n))
          
                  if(index!=nums.end())//代表找到了
                  {
                      //从找到的位置开始,到结尾
                      for(auto it=index;it!=nums.end();)
                      {
                          if(*it==val)
                          {
                              it = nums.erase(it);//直接删掉
                          }
                          else
                              break;
                      }
                  }
          
                  return nums.size();//返回的是nums 中与 val 不同的元素的数量
              }
          };
      
      解法2:快慢指针,其实跟上面题1的思路是一样的。时间复杂度为O(n),空间复杂度为O(1)
      
        class Solution {
          public:
              int removeElement(vector<int>& nums, int val) {
          
                 int slow=0, fast=0;
                 while(fast!=nums.size())
                 {
                      if(nums[fast]!=val)//不等
                      {
                          nums[slow]=nums[fast];
                          slow++;
                      }
                      fast++;
                 }
                 return slow;
                  //此处与有序数组去重的解法有一个细节差异:
                  // 先给 nums[slow] 赋值然后再slow++
                  // nums[0..slow-1] 是不包含值为 val 的元素的,最后的结果数组长度就是 slow
              }
          };
      


      3.移动零

      Image description

      类似上面的解法,只是改为vector以及没有返回值。时间复杂度为O(n),空间复杂度为O(1)
      
        class Solution {
          public:
              void moveZeroes(vector<int>& nums) {
          
                  // 快慢指针
                  int slow=0,fast=0;
          
                  while(fast!=nums.size())
                  {
                      if(nums[fast]!=0)//若不为零
                      {
                          nums[slow]=nums[fast];
                          slow++;
                      }
                      fast++;
                  }
          
                  while(slow!=nums.size())
                  {//由于没有返回值,把后面的也赋值为0(因为可能是原本位置的其他值,上面并不是交换操作)
                      nums[slow]=0;
                      slow++;
                  }
              }
          };
      


      4. 两数之和

      Image description

      左右指针,时间复杂度应该为O(N),空间复杂度应该为O(1)
      
        class Solution {
          public:
              vector<int> twoSum(vector<int>& numbers, int target) {
          
                   // 由于数组已经按照非递减顺序排序,故此可以用左右指针法
                   int left=0, right=numbers.size()-1;
                   while(left<right)
                   {
                      int sum=numbers[left]+numbers[right];
                      if(sum==target)//找到了
                      {
                          return vector<int>{left+1, right+1};//索引从1开始
                      }
                      else if(sum<target)
                      {
                          left++;//让值大一些
                      }
                      else if(sum>target)
                      {
                          right--;//让值小一些
                      }
          
                   }
                   return vector<int>{ -1, -1};//找不到
              }
          };
      
      用hash table (时间复杂度为O(N))
      
        class Solution {
          public:
              vector<int> twoSum(vector<int>& numbers, int target) {
                  
                  int value_a,value_b;
                  vector<int> result;
                  // 用hash table (时间复杂度可以降为O(N))
                  unordered_map<int, int> hash_able;//(具体的值、坐标)
                  for(int i=0;i<numbers.size();i++)
                  {
                      auto it=hash_able.find(target-numbers[i]);//寻找这个值的另一半
                      // 如果找到了,
                      if(it!=hash_able.end())
                      {
                          int index_1=it->second;
                          int index_2=i;
                          // 注意返回的为坐标值(且下标开始为1)
                          result.push_back(index_1+1);
                          result.push_back(index_2+1);
                          break;
                      }
                      hash_able[numbers[i]]=i;//若没找到,就继续放入hash table
                  }
                  
                  return result;
          
              }
          };
      

      相类似的题目还有下题(几乎一样,只是下标索引从1开始):

      Image description

      解法1:用hash table (时间复杂度为O(N))
      
        class Solution {
          public:
              vector<int> twoSum(vector<int>& nums, int target) {
                  
                  int value_a,value_b;
                  vector<int> result;
                  // 用hash table (时间复杂度可以降为O(N))
                  unordered_map<int, int> hash_able;//(具体的值、坐标)
                  for(int i=0;i<nums.size();i++)
                  {
                      auto it=hash_able.find(target-nums[i]);//寻找这个值的另一半
                      // 如果找到了,
                      if(it!=hash_able.end())
                      {
                          int index_1=it->second;
                          int index_2=i;
                          // 注意返回的为坐标值
                          result.push_back(index_1);
                          result.push_back(index_2);
                          break;
                      }
                      hash_able[nums[i]]=i;//若没找到,就继续放入hash table
                  }
                  
                  return result;
              }
          };
      
      解法2: 暴力匹配的解法(时间复杂度为O(N^2))这必然非最优解了🤭
      
        class Solution {
          public:
              vector<int> twoSum(vector<int>& nums, int target) {
                  
                  int value_a,value_b;
                  vector<int> result;
                  // 暴力匹配的解法(时间复杂度为O(N^2))
                  for(int i=0;i<nums.size();i++)
                  {
                      value_a=nums[i];
                      for(int j=i+1;j<nums.size();j++)
                      {
                          value_b=nums[j];
                          if(value_a+value_b==target)
                          {
                              result.push_back(i);
                              result.push_back(j);
                              break;
                          }
                      }
                  }
                  
                  return result;
              }
          };
          
      


      5.反转数组

      Image description

      最简单的就是采用reverse函数了~时间复杂度O(n)
      
        class Solution {
          public:
              void reverseString(vector<char>& s) {
                  //采用reverse函数
                  reverse(s.begin(),s.end());
              }
          };
      
      但是还是采用双指针法来看看,时间复杂度O(n),空间复杂度为O(1)跟上面一样
      
        class Solution {
          public:
              void reverseString(vector<char>& s) {
          
                  int left=0, right=s.size()-1;
                  while(left<right)//等于的时候不处理了
                  {
                      char temp=s[left];//用个中间变量来赋值~
                      s[left]=s[right];
                      s[right]=temp;
                      left++;
                      right--;
                  }       
              }
          };
      


      6.最长回文子串

      Image description

      回文子串的判断应该是经典的动态规划的题目,但是复杂度是O(N^2)。空间复杂度是O(N^2)(存储动态规划状态需要的空间。)
      
        class Solution {
          public:
          
              string longestPalindrome(string s) {
                  int max_length=1;//最长的长度
                  int begin_index=0;//起始的index
                  // 动态规划
                  // 对于坐标为i~j的子串,是否回文子串,状态为dp[i][j]
                  bool dp[s.size()][s.size()];  //定义动态规划的状态矩阵大小
                  
                  //确定初始状态,所有长度为1的都是回文子串
                  for(int i=0;i<s.size();i++)
                  {
                      int j=i;
                      dp[i][j]=true;
                  }     
          
                  //进行递推状态转移
                  // for(int i=0;i<s.size();i++)
                  for(int i=s.size()-1;i>=0;i--)//要从小到大!!!
                  {
                      for(int j=i+1;j<s.size();j++)//j=i的情况以及是初始化了
                      {
                          if(s[i]==s[j])//如果相等,那么就检查下一个状态
                          {
                              if(j<=i+2)//如果此时到达边界(aba)或(aa)的情况
                                  dp[i][j]=true;
                              else//继续递推
                                  dp[i][j]=dp[i+1][j-1];
                          }
                          else//如果不等,那么当前就是false
                              dp[i][j]=false;
          
                          //如果是回文子串且长度大于记录值
                          if(dp[i][j] && j-i+1>max_length)
                          {
                              begin_index=i;
                              max_length=j-i+1;
                          }
                      }
                  }
          
                  return s.substr(begin_index,max_length);//字符串的截取
              }
          };
      
      采用双指针解法,时间复杂度是O(N^2)更上面一样的。但空间复杂度更小,为O(1)
      
        class Solution {
          public:
          
              // 对于以left与right为中心的最长回文子串
              // 通过左右指针向两边扩散
              string function(string s, int left, int right)
              {
                  //确保不越界以及相等
                  while(left>=0 && right<s.size() &&s[left]==s[right])
                  {
                      left--;
                      right++;
                  }
                  //若不等的话,会跳出来。所以应该是当前的left+1到right为目标结果
                  return s.substr(left+1,right-(left+1));
              }
          
              string longestPalindrome(string s) {
                  
                  string result="";
                  for(int i=0;i<s.size();i++)
                  {
                      //以i为中心的,奇数字符串
                      string s1=function(s,i,i);
                      //以i为中心的,偶数字符串
                      string s2=function(s,i,i+1);
          
                      // 保存最长的
                      result= result.size()>s1.size()? result:s1;
                      result= result.size()>s2.size()? result:s2;
                  }
                  return result;
              }
          };
      

      下面是几乎一样的题目

      Image description

      Click to expand the code。先给出双指针解法,跟上面几乎一样~
      
      
      
      下面是动态规划的解法,还是双指针法比较简单,建议实际用双指针法解决~
      
      
      
      下面的动态规划解法更容易理解些~
      
        
        


      采用二分法来解数组类题目

      二分法其实就是左右双指针法的一种特殊情况,即两个指针分别指向数组的两端,然后根据题目的要求,每次将指针移动到中间,然后根据中间的值来判断下一步应该移动哪个指针。 更多关于二分法的介绍请见博客Link


      采用滑动窗口算法来解数组类题目

      调滑动窗口算法的快慢指针特性:left 指针在后,right 指针在前,两个指针中间的部分就是「窗口」,算法通过扩大和缩小「窗口」来解决某些问题。 更多关于滑动窗口算法的介绍请见博客Link


      三数之和

      Image description

      先确定一个值,另外两个通过左右指针的方式获取
      
      
      
      Click to expand the code。解法2也是双指针
      
      
      


      最接近的三数之和

      Image description

      Click to expand the code。有时做最大最小值记录的时候,可以选择初始化为INT_MIN和INT_MAX
      
      
      


      四数之和

      Image description

      Click to expand the code。最直接的方法就是用回溯算法。但是运行会超时。230 / 294 个通过的测试用例
      
      
      
      Click to expand the code。要避免超时应该采用的是双指针法,类似三数之和。在三数和的基础上上再套一层for循环.注意测试样例中存在long数据。时间复杂度为O(N3
      
      
      


      高效解决接雨水问题

      接雨水

      Image description

      Click to expand the code。首先给出暴力的解法。这个思路是最直接的,把问题进行分解然后解题,但是最终只有320 / 323 个通过的测试用例 时间复杂度应该是O(N2
      
      
      
      Click to expand the code.通过备忘录,先预先计算每个i的两个数组,避免每次循环都重复遍历,可以将时间复杂度降低为O(N),空间复杂度为O(1)
      
      
      
      Click to expand the code.下面通过双指针法解题,将时间复杂度一样为O(N),如下图所示
      Image description

      
      
      


      盛最多水的容器

      Image description

      Click to expand the code。上一题给出的类似一幅直方图,每个横坐标都有宽度,而本题给出的每个横坐标是一条竖线,没有宽度。因此就不存在height[i]存放多少水的问题。 因此本题只需要知道了两个指针的位置,就可以计算出最大的面积:min(height[left], height[right]) * (right - left)
      
      
      
      Click to expand the code下面写法是一样的。注意更新的索引不要写错了~
      
      
      


      参考资料