【算法题类型总结】

文章目录

一、旋转数组问题

旋转数组,都用二分法的思路解决。

1、轮转数组

在这里插入图片描述
思路:这是408里面经典的一道题目,主要考虑用3次翻转操作,实现空间复杂度O(1)。

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        int len = nums.size();
        k = k%len;       // 这里取模,因为有可能k会大于数组长度
        int res = len - k;
        reverse(nums.begin(), nums.begin()+res);    // 翻转前面部分
        reverse(nums.begin()+res, nums.end());     // 翻转后面k个数
        reverse(nums.begin(), nums.end());         // 整体翻转

    }
};

2、寻找旋转数组的最小值

在这里插入图片描述
思路:因为是有序数组进行旋转,所以旋转数组会变成两边有序,而中间断开,可以用二分查找来进行。

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0;
        int right = nums.size()-1;

        while(left<right)  //这里控制条件没取等号,取等号大多是为了在while中直接return mid,不取等号就跳出while返回l的值。
        {
            int mid = left + (right - left) / 2;
            if(nums[mid]>nums[right])    // 说明左侧是递增的,最小值在右边
            {
                left = mid + 1;
            }else{                        // 说明右侧是递增的,最小值在左边
                right = mid;
            }

        }
        return nums[left];

    }
};

3、寻找旋转数组中最小值(2)

在这里插入图片描述
思路:这一题和上一题的做法是大致相同的,只不过这里有重复值出现,因此要考虑到左、中、右边界会相等的情况,而不仅仅是比大小。

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0;
        int right = nums.size()-1;

        int ans = nums[0];
        while(left<=right)
        {
            int mid  = left + (right - left) / 2;

            ans = min(ans, nums[mid]);
            if(nums[mid]>nums[right])     // 左侧递增
            {
                left = mid+1;
            }else if(nums[mid]<nums[right])    // 右侧递增,最小值在左边
            {
                right = mid - 1;
            }else{        // nums[mid]==nums[right],此时最小值在中间,不能轻易判断,只能让右边界递减
                right--;
            }

        }
        return ans;

    }
};

4、搜索旋转排序数组

在这里插入图片描述
思路:同样用二分查找,这里只进行一次旋转,所以会存在两段有序,因此每次二分查找要判断左侧有序还是右侧有序。

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size()-1;
        while(left<=right)
        {
            int mid = left + (right - left) / 2;
            if(nums[mid]==target)
            {
                return mid;
            }else if(nums[mid]<nums[right])   // 右侧递增。
            {
                if(target>nums[mid]&&target<=nums[right])
                {
                    left = mid+1;
                }else{
                    right = mid-1;
                }

            }else{     // 左侧递增
                if(nums[left]<=target&&target<nums[mid])
                {
                    right = mid-1;
                }else{
                    left = mid+1;
                }
                
            }

        }
        return -1;
    }
};

5、搜索旋转排序数组2

在这里插入图片描述
思路:就是在上一题的基础上,出现了重复的可能性,因此这里讨论的情况更多。

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size()-1;
        while(left<=right)
        {
            int mid = left + (right - left) / 2;
            if(nums[mid]==target)
            {
                return true;
            }else if(nums[mid]<nums[right])
            {
                if(nums[mid]<target&&target<=nums[right])
                {
                    left = mid+1;
                }else{
                    right = mid-1;
                }

            }else if(nums[mid]==nums[right])    // 有可能想等,这时候让右边界递减
            {
                right--;
            }else if(nums[left]<nums[mid])
            {
                if(nums[mid]>target&&target>=nums[left])
                {
                    right = mid-1;
                }else{
                    left = mid+1;
                }

            }else if(nums[left]==nums[mid])      // 可能和左边界想等,不能判断递增,因此左边界➕1
            {
                left++;
            }



        }
        return false;

    }
};

6、搜索旋转数组

在这里插入图片描述
思路:这里也是旋转多次,有重复,同时还要求返回索引值最小的一个,因此这里从左侧开始寻找。

//旋转多次和旋转一次没有区别,最终只有一个旋转点
class Solution {
public:
    int search(vector<int>& arr, int target) {
        int left=0;
        int right=arr.size()-1;
        while(left<=right)
        {
            int mid = left+(right-left)/2;
            if(arr[left]==target)  // 先和左边界比,因为找最小索引
            {
                return left;
            }
            if(arr[mid]==target)   // 肯定往左边找,因为最小索引
            {
                right=mid;
            }else if(arr[left]<arr[mid])   // 左侧递增
            {
                if(target>=arr[left] && target<arr[mid])
                {
                    right=mid-1;
                }else{
                    left=mid+1;
                }
            }else if(arr[left]>arr[mid])
            {
                if(target>arr[mid] && target<=arr[right])
                {
                    left=mid+1;
                }else{
                    right=mid-1;
                }
            }else if (arr[left]==arr[mid])
            {
                left++;
            }
        }
        return -1;

    }
};

二、排列、组合问题

统一是回溯的思想,只是处理好条件。

1、组合

在这里插入图片描述
思路:组合中(1,2)和(2,1)会视为同一个,为了避免重复,在回溯的过程中以从小到大的顺序进行遍历,并且进入下一轮时是cur+1。

class Solution {
public:
    vector<vector<int> >  ans;
    vector<int> tmp;
    vector<int> nums;
    void dfs(int cur, vector<int>& nums, int k)
    {
        if(tmp.size() + (nums.size() - cur)<k)   // 剪枝
        {
            return;
        }
        if(tmp.size()==k)
        {
            ans.push_back(tmp);
            return;
        }
        for(int i=cur;i<nums.size();i++)   // 索引不断递增
        {
            tmp.push_back(nums[i]);
            dfs(i+1, nums, k);   // 进入下一个索引
            tmp.pop_back();
        }
    }

    vector<vector<int>> combine(int n, int k) {
        for(int i=1;i<=n;i++)
        {
            nums.push_back(i);
        }
        dfs(0, nums, k);
        return ans;

    }
};

但如果,在进入下一个递归中,写成了dfs(cur, nums, k),那就会出现(i,i)的情况,因为下个递归还是从 i 开始遍历。
输入:3 3
输出:[[1,1,1],[1,1,2],[1,1,3],[1,2,2],[1,2,3],[1,3,3],[2,2,2],[2,2,3],[2,3,3]]
不会出现[2,1,1]这样的组合,因为索引顺序在递增。

2、全排列

在这里插入图片描述
思路:这里和组合不一样,全排列允许出现(1,0)和(0,1)这样的情况。因此遍历索引需要从头遍历,每个数字都可能再次出现。那么为了避免重复选取数字,需要另外一个vis数组来标记。

class Solution {
public:
    vector<vector<int> > ans;
    vector<int> tmp;
    void dfs(vector<int>& nums, vector<int>& vis)
    {
        if(tmp.size()==nums.size())
        {
            ans.push_back(tmp);
            return;
        }
        for(int i=0;i<nums.size();i++)
        {
            if(vis[i]==0)
            {
                vis[i]=1;
                tmp.push_back(nums[i]);
                dfs(nums, vis);
                tmp.pop_back();
                vis[i]=0;
            }
        }   

    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<int> vis(nums.size(), 0);
        dfs(nums, vis);
        return ans;
    }
};

3、全排列2

在这里插入图片描述
思路:这里是有重复数字,和上一题没有重复数字的全排列有些不同。因为如果直接使用上一题的代码,对于[1,1,2]:
输出:[[1,1,2],[1,2,1],[1,1,2],[1,2,1],[2,1,1],[2,1,1]]。为什么会有两个[1,1,2]呢?其实是第一个[1,1,2]中,第1个1是来自nums[0],第2个1来自nums[1];第二个[1,1,2]是相反的情况。表现在vector中就是两个相同的[1,1,2]。
1、粗暴的方法可以直接上set,来暴力去重。
2、让重复数字只用一次

class Solution {
public:
    vector<vector<int> > ans;
    vector<int> tmp;
    void dfs(vector<int>& nums, vector<int>& vis)
    {
        if(tmp.size()==nums.size())
        {
            ans.push_back(tmp);
            return;
        }
        for(int i=0;i<nums.size();i++)
        {
            if(i>0 && nums[i]==nums[i-1] && vis[i-1]==1)   // 去重,相邻两个数字相同,并且前一个数字已经用过了,那么当前 i 不再使用
            {
                continue;
            }
            if(vis[i]==0)
            {
                vis[i]=1;
                tmp.push_back(nums[i]);
                dfs(nums, vis);
                tmp.pop_back();
                vis[i]=0;
            }
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());   // 先排序,让重复的数字相邻
        vector<int> vis(nums.size(), 0);
        dfs(nums, vis);

        return ans;

    }
};

4、允许重复选择元素的组合

在这里插入图片描述
思路:乍一看是组合题,但是这里组合中元素个数是不限制的,限制条件改成了和为target。
但组合的性质(1,2)和(2,1)相同,因此for中遍历要以当前索引递增(和之前的组合一样);另外,数组中的选择能够重复选择,因此递归中索引不能递增dfs(i,…)。

class Solution {
public:
    vector<vector<int> > ans;
    vector<int> tmp;
    void dfs(vector<int>& candidates, int target, int sum, int idx)
    {
        if(sum>target)
        {
            return;
        }
        if(sum==target)   // 返回条件不再是 数量=k,而是和=target
        {
            ans.push_back(tmp);
            return;
        }
        for(int i=idx;i<candidates.size();i++)   // 和组合问题一样,要从当前索引遍历,不能走回头路
        {
            tmp.push_back(candidates[i]);
            dfs(candidates, target, sum+candidates[i], i);    // 这里索引不能增加,因为可以重复选择数字
            tmp.pop_back();
        }

    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        dfs(candidates, target, 0, 0);
        if(ans.size()==0)
        {
            return {};
        }
        return ans;

    }
};

5、含有重复元素集合的组合

在这里插入图片描述
思路:这里数组中会有重复元素,而每个数字只能用一次,和上面的重复选取不同,因此这里在递归的索引中应该是递增索引dfs(i+1,…)。

class Solution {
public:
    vector<vector<int> > ans;
    vector<int> tmp;
    void dfs(vector<int>& candidates, int target, int sum, int idx)
    {
        if(sum>target)
        {
            return ;
        }
        if(sum==target)
        {
            ans.push_back(tmp);
            return;
        }
        for(int i=idx;i<candidates.size();i++)
        {
            if(i>idx && candidates[i]==candidates[i-1])   // 上一层已经使用过candidatescandidates[i-1],因此要跳过相同值
            {
                continue;
            }
            tmp.push_back(candidates[i]);
            dfs(candidates, target, sum+candidates[i], i+1);  // 索引递增
            tmp.pop_back();
        }

    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        dfs(candidates, target, 0, 0);
        return ans;

    }
};

6、组合总和

在这里插入图片描述

class Solution {
   
public:
    vector<vector<int> > ans;
    vector<int> tmp;
    void backtrace(vector<int>& candidates, int cur, int sum, int target)
    {
   
        if(sum>target)
        {
   
            return;
        }
        if(sum==target)
        {
   
            ans.push_back(tmp);
        }
        for(int i=cur; i<candidates.size(); i++)
        {
   
            tmp.push_back(candidates[i]);
            backtrace(candidates, i, sum+candidates[i], target);
            tmp.pop_back();
        }

    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
   
        backtrace(candidates, 0, 0, target);
        return ans;

    }
};

7、组合总和2

在这里插入图片描述

class Solution {
   
public:
    vector<vector<int> > ans;
    void backtrace(vector<int>& candidates, int cur, vector<int> tmp, int sum, int target)
    {
   
        if(sum>target)
        {
   
            return;
        }
        if(sum==target)
        {
   
            ans.push_back(tmp);
            return;
        }
        for(int i=cur;i<candidates.size();i++)
        {
   
            if(i>cur && candidates[i]==candidates[i-1])
            {
   
                continue;
            }
            sum+=candidates[i];
            tmp.push_back(candidates[i]);
            backtrace(candidates, i+1, tmp, sum, target);
            tmp.pop_back();
            sum-=candidates[i];
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
   
        sort(candidates.begin(), candidates.end());
        backtrace(candidates, 0, {
   }, 0, target);
        return ans;

    }
};

8、组合总和3

在这里插入图片描述

class Solution {
   
public:
    vector<vector<int> > ans;
    vector<int> tmp;
    void backtrace(int cur, int sum, int k, int n)
    {
   
        if(sum>n || tmp.size()>k)
        {
   
            return;
        }
        if(tmp.size()==k && sum==n)
        {
   
            ans.push_back(tmp);
        }
        for(int i=cur;i<=9;i++)
        {
   
            tmp.push_back(i);
            backtrace(i+1, sum+i, k, n);
            tmp.pop_back();
        }

    }
    vector<vector<int>> combinationSum3(int k, int n) {
   
        backtrace(1, 0, k, n);
        return ans;

    }
};

四、二分查找

二分查找的前置条件:升序,时间复杂度O(logn)

1、在排序数组中查找元素的第一个和最后一个位置

在这里插入图片描述
思路:题目条件升序 + 时间复杂度O(logn),明显是要用二分来做。但是数组中会出现重复元素,因此和普通二分在判断条件上有所不同。
要求查找元素的第一个位置:即大于等于target的第一个位置;
最后一个位置:即大于target的第一个位置,再-1。
需要两次二分来分别获得两个位置索引。

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> ans(2, -1);
        if(nums.size()==0 || (nums.size()==1 && nums[0]!=target))
        {
            return ans;
        }
        int left_idx=-1, right_idx=nums.size();  // 右边界初始化为数组最后,因为如果数组中存在target,那么左边界必然!=-1;而又可能target是数组的最大值,因此不一定存在大于target的位置,此时右边界就是数组的最后
        // 找第一个大于等于target的位置
        int left=0;
        int right=nums.size()-1;
        while(left<=right)
        {
            int mid = left + (right - left) / 2;
            if(nums[mid]>=target)   // 此时mid也满足大于等于target的条件,暂记左边界
            {
                right=mid-1;
                left_idx=mid;
            }else{
                left=mid+1;
            }
        }

        // 找第一个大于target的位置
        left=0;
        right=nums.size()-1;
        while(left<=right)
        {
            int mid = left + (right - left) / 2;
            if(nums[mid]>target)  // 此时mid已经满足条件,再将右边界左移,试着还有没有满足条件的mid
            {
                right=mid-1;
                right_idx=mid;   // 先暂记mid是满足条件的右边界
            }else{
                left=mid+1;
            }
        }
        
        if(left_idx!=-1 && nums[left_idx]==target && nums[right_idx-1]==target)
        {
            ans[0]=left_idx;
            ans[1]=right_idx-1;
        }
        return ans;

    }
};

2、x的平方根 (md文档中有详细解法)

在这里插入图片描述
思路:x的范围是0~ 2 31 − 1 2^{31}-1 2311,又要求算术平方根,可以利用二分来加速查找。值得注意的是,查找过程中需要判断mid*mid,可能会超出int范围。题目的要求中,如果开根号得到小数,需要强制向下取整,翻译过来其实就是找一个整数K,满足 K ∗ K < = x K*K<=x KK<=x且K能取到最大。

class Solution {
public:
    int mySqrt(int x) {
        int left=0, right=x;
        int ans=-1;
        while(left<=right)
        {
            int mid = left + (right - left) / 2;
            if((long long)mid*mid<=x)    // 和上一题一样,此时mid是满足条件的,暂记下来
            {
                ans=mid;
                left=mid+1;
            }else{
                right=mid-1;
            }
        }
        return ans;

    }
};

3、完成旅途的最少时间

在这里插入图片描述
思路:这里随着趟数的增加,所需的最少时间也必然增加,符合单调性,可用二分法来查找。

class Solution {
public:
    long long minimumTime(vector<int>& time, int totalTrips) {
        sort(time.begin(), time.end());
        int min_time = time[0];
        long long left = min_time;   // 最少时间的下界:只跑一趟,那就用最快的车跑
        long long right = (long long)min_time * totalTrips;   // 最少时间的上界:只用最快的车,跑完所有趟需要的时间
        while(left<=right)
        {
            long long mid = left + (right - left) / 2;
            long long total=0;     // 记录在当前时间下,最多能跑几趟
            for(auto t: time)
            {
                total+= mid / t;
            }
            if(total>=totalTrips)
            {
                right = mid - 1;
            }else{
                left = mid + 1;
            }
        }
        return left;
    }
};

五、双指针

常见于原地操作数组

1、移动0

在这里插入图片描述
思路:由于要保持非0元素的相对顺序,因此不能直接把0往后扔,要找到0和第一个非0交换位置。

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int i=0;
        while(i<nums.size())    // 找到第一个0的位置
        {
            if(nums[i]==0)
            {
                break;
            }else{
                i++;
            }
        }
        for(int j=i+1;j<nums.size();j++)   // 从0后开始找第一个不为0的数,然后和i交换位置,i始终指向0的位置
        {
            if(nums[j]!=0)
            {
                swap(nums[i++], nums[j]);
            }
        }
    }
};

2、有序数组的平方

在这里插入图片描述
思路:用双指针实现比较插入法。

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> ans(nums.size(), 0);
        int k=nums.size()-1;
        for(int i=0,j=nums.size()-1;i<=j;)
        {
            if(nums[i]*nums[i]>nums[j]*nums[j])
            {
                ans[k--]=nums[i]*nums[i];
                i++;
            }else
            {
                ans[k--]=nums[j]*nums[j];
                j--;
            }
        }
        return ans;

    }
};

六、排序

熟练掌握各种排序方法。

1、基本排序算法总结

(1)选择排序 平均、最好、最坏复杂度O(n2) 不稳定

void selectSort(vector<int>& nums)
{
  for(int i=0;i<nums.size()-1;i++)
  {
    int min = i;
    for(int j=i+1;j<nums.size();j++)
    {
      if(nums[min]>nums[j])
      {
        min=j;
      }
    }
    swap(nums[min], nums[i]);
  }
   
}

(2)插入排序 平均O(n2),最坏O(n2),最好O(n),稳定

通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

void insertsort(vector<int>& nums)
{
  for(int i=1;i<nums.size();i++)
  {
    for(int j=i-1;j>=0 && nums[j]>nums[j+1];j--)
    {
      swap(nums[j], nums[j+1]);
    }
  }
}

(3)冒泡排序 平均O(n2),最坏O(n2),最好O(n),稳定

越小的元素会经由交换慢慢“浮”到数列的顶端。

void sort(vector<int>& nums)
{
  /*
        如果用一个flag来判断一下,当前数组是否已经有序,
      有序就退出循环,可以提高冒泡排序的性能。
  */
    int n = nums.size();
    for (int i = 1; i<n; i++) {
        bool flag = true;
        for (int j = 0; j < n - i; j++) {
            if (nums[j]>nums[j+1]) {
                swap(nums[j], nums[j+1]);
                flag=false;
            }
        }
        if (flag) {
            break;
        }
    }
}

(4)归并排序 平均、最好、最坏复杂度O(nlogn),稳定

void merge(vector<int>& nums, int left, int mid, int right)
{
  vector<int> cnt(right-left+1, 0);

  int i = 0;
  int pFirst = left;
  int pSecond = mid+1;
  while (pFirst <= mid && pSecond <= right) {
      cnt[i++] = nums[pFirst] < nums[pSecond] ? nums[pFirst++]:nums[pSecond++];
  }
  while (pFirst <= mid) {
      cnt[i++] = nums[pFirst++];
  }
  while (pSecond <= right) {
      cnt[i++] = nums[pSecond++];
  }
  for (int j = 0; j < (right-left+1); j++) {
      nums[left+j] = cnt[j];
  }
}

void mergeSort(vector<int>& nums, int left, int right)
{
  if(left==right)
  {
    return;
  }
  int mid = (left+right)/2;
  mergeSort(nums, left, mid);
  mergeSort(nums, mid+1, right);
  merge(nums,left,mid,right);
   
}

(5)快排(随机)平均、最好复杂度O(nlogn),最坏O(n2),空间O(logn),不稳定

通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

int partition(vector<int>& nums, int left, int right) {
  int pivot = nums[right];
  int i = left;

  for (int j = left ; j <= right - 1; ++j)  //注意这里的<=边界条件!
  {
      if (nums[j] < pivot) {
          
          swap(nums[i], nums[j]);
          ++i;
      }
  }
  swap(nums[i], nums[right]);
  return i;
}

void quicksort(vector<int>& nums, int left, int right)
{
  if(left<right)
  {
    int picked = rand() % (right - left + 1) + left;   // 取一个随机数,将数组打乱
    swap(nums[picked], nums[left]);

    int p = partition(nums, left, right);
    quicksort(nums, left, p - 1);
    quicksort(nums, p + 1, right);
  }
   
}

非递归版本:
用栈来存放每次的left和right

class Solution {
   
public:
    int partion(vector<int>& nums, int left, int right)
    {
   
        int pivot = nums[right];
        int i=left;
        for(int j=left;j<=right-1;j++)
        {
   
            if(nums[j]<pivot)
            {
   
                swap(nums[i], nums[j]);
                i++;
            }
        }
        swap(nums[i], nums[right]);
        return i;
    }
    vector<int> sortArray(vector<int>& nums) {
   
        stack<int> st;
        int k;
        int low = 0;
        int high = nums.size()-1;
        if(low<high)
        {
   
            st.push(low);
            st.push(high);
            while(!st.empty())
            {
   
                int j=st.top();
                st.pop();
                int i=st.top();
                st.pop();
                
                int rand_p = rand()%(j-i+1)+i;
                swap(nums[i], nums[rand_p]);
                k=partion(nums,i,j);
        
                if(i<k-1)
                {
   
                    st.push(i);
                    st.push(k-1);
                }
                if(k+1<j)
                {
   
                    st.push(k+1);
                    st.push(j);
                }
            }
        }
        return nums;
    }
};

(6)堆排序 平均、最好、最坏复杂度O(nlogn),不稳定

//向上走
void heapInsert(vector<int>& nums,int index){
    while (nums[index] > nums[(index-1)/2]) {
        swap(nums[index], nums[(index-1)/2]);
        index = (index -1)/2;
    }
}
//向下走
//size为最右的边界,size是取不到的.
void heapify(vector<int>& nums,int index ,int size){
  int leftChild = index*2 + 1;
  while (leftChild < size) {
      int maxChild = leftChild + 1 < size && nums[leftChild+1] >nums[leftChild] ? leftChild+1 : leftChild;
      int maxAll = nums[maxChild] > nums[index] ? maxChild: index;
      if (maxAll  == index) {
          break;
      }
      swap(nums[maxAll], nums[index]);
      index = maxAll;
      leftChild = index*2 +1;
  }
}  


int main(){
  vector<int> nums;
  nums.push_back(2);
  nums.push_back(11);
  nums.push_back(9);
  nums.push_back(6);
  nums.push_back(5);

  for(int i = 0;i < nums.size();i++)
  {
        heapInsert(nums, i);     // 大顶堆
  }
  int size = nums.size();
  swap(nums[0], nums[--size]);   // 此时在0位置的是最大值,把它放到最后去,就固定在最后的位置
  while (size > 0){
      //heapify时间复杂度为O(logN)
      heapify(nums, 0, size);   // 调整堆,从头开始向下调整
      swap(nums[0], nums[--size]);    // 每次都把当前的最大值放到后面去
  }
  return 0;
}

(7)计数排序 平均、最好、最坏复杂度O(n+k)

int max = arr[0];
int lastIndex=  0;
 for (int i = 1; i<length; i++) {
     max = arr[i]>max ? arr[i]:max;
 }
 int* sortArr  = new int[max+1]();   // 设置哈希表
 for (int j = 0; j< length; j++) {
     sortArr[arr[j]]++;   // 记录每个元素出现的个数
 }
 for (int k = 0; k<max+1; k++) {
     while (sortArr[k]>0) {    // 根据索引大小,然后把这个索引出现了几次都放到数组中
         arr[lastIndex++] = k;
         sortArr[k]--;
     }
 }

2、前K个高频元素

在这里插入图片描述
思路1:堆排序。这里用小顶堆,将出现次数少的元素放在堆顶,这样便于pop。

class Solution {
public:
    static bool cmp(pair<int, int>& m, pair<int, int>& n) {
        return m.second > n.second;   // 出现次数多的元素放在后面,少的放在堆顶
    }

    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> occurrences;
        for (auto& v : nums) {
            occurrences[v]++;
        }

        // pair 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
        priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> q(cmp);
        for (auto& [num, count] : occurrences) {
            if (q.size() == k) {    // 当已经有k个元素,那就要把出现次数少的给移除
                if (q.top().second < count) {
                    q.pop();
                    q.emplace(num, count);
                }
            } else {   // 当堆中数量少于k,说明还能加入
                q.emplace(num, count);
            }
        }
        vector<int> ret;
        while (!q.empty()) {
            ret.emplace_back(q.top().first);
            q.pop();
        }
        return ret;
    }
};

思路2:快排
在这里插入图片描述

class Solution {
public:
    void qsort(vector<pair<int, int>>& v, int start, int end, vector<int>& ret, int k) {
        int picked = rand() % (end - start + 1) + start;   // 取一个随机数,将数组打乱
        swap(v[picked], v[start]);

        int pivot = v[start].second;
        int index = start;
        for (int i = start + 1; i <= end; i++) {
            if (v[i].second >= pivot) {    // 高频的都往前扔
                swap(v[index + 1], v[i]);
                index++;
            }
        }
        swap(v[start], v[index]);

        if (k <= index - start) {       //此时只要在【start, index-1】的范围内找k个数即可
            qsort(v, start, index - 1, ret, k);
        } else {
            for (int i = start; i <= index; i++) {     // 说明左侧全是前k个高频中的元素
                ret.push_back(v[i].first);
            }
            if (k > index - start + 1) {   // 还有剩余的就在右边,然后递归右侧找剩余的元素
                qsort(v, index + 1, end, ret, k - (index - start + 1));
            }
        }
    }

    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> occurrences;
        for (auto& v: nums) {
            occurrences[v]++;
        }

        vector<pair<int, int>> values;
        for (auto& kv: occurrences) {
            values.push_back(kv);
        }
        vector<int> ret;
        qsort(values, 0, values.size() - 1, ret, k);
        return ret;
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/top-k-frequent-elements/solution/qian-k-ge-gao-pin-yuan-su-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

3、根据字符出现频率排序

在这里插入图片描述
思路:和上一题一样,用哈希记录出现频率,再构造堆。

class Solution {
public:
    static bool cmp(pair<char, int>& a, pair<char, int>& b)
    {
        return a.second < b.second;
    }
    string frequencySort(string s) {
        unordered_map<char, int> mp;
        for(int i=0;i<s.size();i++)
        {
            mp[s[i]]++;
        }
        // 构建大顶堆,出现频率高的字母在堆顶
        priority_queue<pair<char, int>, vector<pair<char, int> >, decltype(&cmp) > q(cmp);
        for(auto iter=mp.begin();iter!=mp.end();iter++)
        {
            q.push(make_pair(iter->first, iter->second));
        }
        string ans="";
        while(!q.empty())
        {
            int num = q.top().second;
            for(int i=0;i<num;i++)
            {
                ans+=q.top().first;
            }
            q.pop();
        }
        return ans;

    }
};

4、颜色分类

在这里插入图片描述
思路:一开始的思路是两次排序,第一次将0全扔到最前面,然后记录非0的第一个位置;第二次将1扔到最前面,就不用管2了。后来看了评论,发现可以将这两次合成一次遍历(利用双指针)。每次遍历到0,就将0扔到前面,遍历到2将2扔到后面,而需要注意的是,扔完2后,如果当前值不是1(是0或2),那需要将遍历指针回退一下,不然就会漏掉一个0或2。

class Solution {
public:
    void sortColors(vector<int>& nums) {
        int ptr_0=0, ptr_2=nums.size()-1;
        for(int k=ptr_0;k<=ptr_2;k++)   // 注意遍历到2指针的位置即可,不用遍历到数组末尾
        {
            if(nums[k]==0)
            {
                swap(nums[k], nums[ptr_0++]);
            }else if(nums[k]==2)
            {
                swap(nums[k], nums[ptr_2--]);
                if(nums[k]!=1)   // 当前指针不是1,要回退,下次进入循环再判断是0还是2
                {
                    k--;
                }
            }
        }
    }
};

七、贪心

要保证局部操作是最优的,那么最后的结果是全局最优的。

1、无重叠区间

在这里插入图片描述
思路:题目问移除区间的最小数量,反向思维,求不重叠区间的最多数量,那么答案=总数-不重叠区间数量。在每次选择中,区间的结尾最为重要,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。此外贪心体现在排序,让局部情况尽早出现。

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.size()==0)
        {
            return 0;
        }
        sort(intervals.begin(), intervals.end(),
        [](const vector<int>& a, const vector<int>& b)
        {
            return a[1] < b[1];    // 让右端点小的排在前面
        });
        int ans=1;   // 记录不重叠区间个数,第一个区间自然没人重叠
        int right = intervals[0][1];   // 第一个区间的右端点
        for(int i=1;i<intervals.size();i++)
        {
            if(intervals[i][0]>=right)   // 下一个区间的左端点只要不小于右端点,那么这两个区间不会重叠
            {
                ans++;
                right = intervals[i][1];
            }
        }
        return intervals.size() - ans;   // 总数-不重叠区间 = 重叠区间数量
    }
};

2、用最少数量的箭引爆气球

在这里插入图片描述
思路:这和上一题是一样的思路,但在条件处理上相反。这里要求最少的箭射爆所有的气球,意味着求最少的不重叠区间数量(因为区间若重叠,那么只需要一支箭就能射爆),上一题要求最多的不重叠区间数量。因此在排序时,要让左端点尽可能大,这样留给左边的空间更小。

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if(points.size()==1)
        {
            return 1;
        }
        sort(points.begin(), points.end(),
        [](const vector<int>& a, const vector<int>& b)
        {
            return a[0] > b[0];    // 左边大
        });
        int left=points[0][0];
        int ans=1;     // 最少的不重叠区间数量
        for(int i=1;i<points.size();i++)
        {
            if(points[i][1]<left)   // 让前面区间的右端点小于当前区间的左端点,不能取等号,因为取等号的话箭可以同时射爆这两个区间
            {
                ans++;
                left = points[i][0];
            }
        }
        int n = points.size();
        return ans;
    
    }
};

3、买卖股票的最佳时机2

在这里插入图片描述
思路:这里能够允许多次买卖,因此要尽可能抓住每次能获得利润的机会(局部最优),才能使最终最大利润(全局最优),满足贪心。根据 121. 买卖股票的最佳时机,可以通过一个最低价格变量,来保存 i 位置之前的最低价格,这样如果之后有高于该价格,那么就可卖出,获得当前的最大利润。又鉴于题目允许同一天买卖,因此当天如果卖出了股票可以立即买,这样仍能保持现在情况的最低价格变量,等待后续更大的价格来卖出。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size()==1)
        {
            return 0;
        }
        int min_value = prices[0];
        int ans=0;
        for(int i=1;i<prices.size();i++)
        {
            if(min_value>prices[i])
            {
                min_value = prices[i];
            }else if(prices[i] > min_value){
                ans += prices[i] - min_value;
                min_value = prices[i];
            }
        }
        return ans;

    }
};

4、根据身高重建队列

在这里插入图片描述
思路:二维数组,贪心体现在对每一维都要利用排序,构成最符合题意的序列,在遍历重建队列。这里先让身高降序排,若身高想等,则让k升序排。此时排完序后,之前人的身高必然大于等于当前的人,且k也符合要求,那么只要根据k值来将每个人插入队列即可(让其前面恰好有k个人)。

class Solution {
public:
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort(people.begin(), people.end(),
        [](const vector<int>& a, const vector<int>& b)
        {
            if(a[0]!=b[0])
            {
                return a[0]>b[0];   // 让身高从大到小排序
            }else{
                return a[1]<b[1];   // 身高一样,那么让k小的在前面。
            }
        });    // 此时排完序后,身高是降序,并且如果身高相同,那么先排k小的,再排k大的,满足题目条件。这样的话,无论哪个人的身高都小于等于他前面人的身高。所以接下来只要按照K值将他插入相应的位置就可以了。
        int n = people.size();
        vector<vector<int> > ans;
        for (const vector<int>& person: people) {
            int pos = person[1];
            ans.insert(ans.begin()+pos, person);
        }
        return ans;

    }
};

5、种花问题

在这里插入图片描述
思路:在数组的两边界加上0,这样就不用考虑边界是1开始还是0开始的。另外,如果出现连续3个位置是0,那么位置中间可种下一朵花。

class Solution {
public:
    bool canPlaceFlowers(vector<int>& flowerbed, int n) {
        flowerbed.insert(flowerbed.begin(), 0);
        flowerbed.push_back(0);

        for(int i=1;i<flowerbed.size()-1;i++)
        {
            if(flowerbed[i-1]==0&&flowerbed[i]==0&&flowerbed[i+1]==0)   // 连续3个位置为0
            {
                flowerbed[i]=1;
                n--;
            }
        }
        if(n<=0)
        {
            return true;
        }else{
            return false;
        }

    }
};

补:视频拼接

在这里插入图片描述
思路一:将区间排序,维护一个最小的覆盖最长的数组。

class Solution {
   
public:
    int videoStitching(vector<vector<int>>& clips, int time) {
   
        sort(clips.begin(), clips.end(),
        [](const vector<int>& a, const vector<int>& b)
        {
   
            if(a[0]!=b[0])
            {
   
                return a[0]<b[0];
            }else{
   
                return a[1]<b[1];
            }
        });
        if(clips[0][0]!=0)    // 开头不是0,左端肯定不满足
        {
   
            return -1;
        }
        vector<vector<int> > vec;
        vec.push_back({
   clips[0][0], clips[0][1]});
        if(clips[0][1]>=time)    // 第一个区间直接覆盖time了,那就返回
        {
    
            return 1;
        }
        for(int i=1;i<clips.size();i++)
        {
   
            int right = vec.back()[1];
            if(clips[i][0]<=right && clips[i][1]>right)   // 新来的区间,一定要左端小于等于数组末端的右端,说明能重叠;并且右端比数组末端的右端大,不然没加进去的必要
            {
   
             	// 这里有两种情况让数组末端退出
             	// 1.新来的左端小于等于末端的左端,说明覆盖范围更大
             	// 2.新来的左端小于等于数组末端前一个的右端,说明能过和前一个重叠,那就不需要数组末端了
                if(clips[i][0]<=vec.back()[0])
                {
   
                    vec.pop_back();
                }else if(vec.size()>=2 && clips[i][0]<=vec[vec.size()-2][1])
                {
   
                    vec.pop_back();
                }
                vec.push_back(clips[i]);
                if(vec.back()[1]>=time)
                {
   
                    break;
                }
            }
        }
        if(vec.back()[1]>=time)
            return vec.size();
        else
            return -1;

    }
};

思路二:dp
设dp[i]表示覆盖[0,i)的最少区间数量。

class Solution {
   
public:
    int videoStitching(vector<vector<int>>& clips, int time) {
   
        vector<int> dp(time + 1, INT_MAX - 1);
        dp[0] = 0;
        for (int i = 1; i <= time; i++) {
   
            for (auto& it : clips) {
   
                if (it[0] < i && i <= it[1]) {
      // 说明当前区间能包含i,那么dp[it[0]]表示覆盖0~it[0]的最少数量,可以进行当前状态的更新
                    dp[i] = min(dp[i], dp[it[0]] + 1);
                }
            }
        }
        return dp[time] == INT_MAX - 1 ? -1 : dp[time];
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/video-stitching/solution/shi-pin-pin-jie-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

思路三:贪心
可以理解为跳格子的问题。需要知道在当前位置能够跳到的位置,然后用最少的次数跳到目的地。

class Solution {
   
public:
    int videoStitching(vector<vector<int>>& clips, int time) {
   
        vector<int> maxn(time);
        for(auto t:clips)
        {
   
            if(t[0]<time)
            {
   
                maxn[t[0]]=max(maxn[t[0]], t[1]);   // 确定每个区间左端点,能够碰到的最右端点
            }
        }
        int ans=0;
        int last=0;
        int pre=0;
        for(int i=0;i<time;i++)
        {
   
            last = max(last, maxn[i]);   // 从该位置能碰到的最右边
            if(i==last)    // 说明该位置能碰到的最右边还是该位置,不能继续往后
            {
   
                return -1;
            }
            if(i==pre)    // 要走到上一步能到的最后位置,才能更新所跳的步数
            {
   
                ans++;
                pre=last;
            }
        }
        return ans;

    }
};

补:跳跃游戏

在这里插入图片描述
思路:在每一个格子时,就能知道自己能跳的最远距离,不断更新最远距离,如果能到达最后的索引,说明跳到了。

class Solution {
   
public:
    bool canJump(vector<int>& nums) {
   
        int n = nums.size();
        int rightmost=0;
        for(int i=0;i<n;i++)
        {
   
            if(i<=rightmost)  // 说明在能跳到的范围内
            {
   
                rightmost = max(rightmost, i+nums[i]);
                if(rightmost>=n-1)
                {
   
                    return true;
                }
            }
        }
        return false;
    }
};

反向dp(通过169/166,竟然超时)

class Solution {
   
public:
    bool canJump(vector<int>& nums) {
   
        int n = nums.size();
        if(n==1)
        {
   
            return true;
        }
        vector<int> dp(n, 0);
        dp[n-1]=1;
        for(int i=n-2;i>=0;i--)
        {
   
            for(int j=nums[i];j>=1;j--)  // 只要在当前格子的跳跃范围内,有一个格子是1,那么当前格子肯定也是1.
            {
   
                if(i+j<nums.size())
                {
   
                    dp[i]|=dp[i+j];
                    if(dp[i]==1)
                    {
   
                        break;
                    }
                }
            }
        }
        return dp[0];
    }
};

补:跳跃游戏2

在这里插入图片描述
思路:贪心的想法,每一步要尽可能远,但是对于当前位置 i ,不能直接跳到nums[i]+i,因为有可能在 i ~ nums[i]+i的区间中,有更远的距离。因此分为当前到达的最远位置,下一步到达的最远位置。每一次走到当前的最远位置时,步长再++,而在走的过程中不断更新下一步的最远位置。

class Solution {
   
public:
    int jump(vector<int>& nums) {
   
        int ans=0;
        int end=0;  // 当前能走到的终点位置
        int maxpos=0;
        for(int i=0;i<nums.size()-1;i++)
        {
   
            maxpos = max(maxpos, nums[i]+i);  // 更新能跳到的最远位置。其实这里在算下一步能跳到的最远位置
            if(i==end)   // 什么时候跳跃数才++,因为还没跳到最后,还有下一步的跳跃空间
            {
   
                end=maxpos;  // 将当前能走到的最远位置更新
                ans++;
            }
        }
        return ans;
    }
};

补:加油站

在这里插入图片描述
思路:贪心。根据题目,可以得出两个推论:
1)将每个加油站的剩余油量累加给left,即left+=gas[i]-cost[i],如果left<0,那么从出发站到i都不是起点。
2)总油量要>=总消耗量。

class Solution {
   
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
   
        int curSum=0, totalSum=0;   // 一开始油量状态是0
        int start=0;   // 假设从0位置出发
        for(int i=0;i<gas.size();i++)
        {
   
            curSum+=gas[i]-cost[i];  // 记录当前剩余油量
            totalSum+=gas[i]-cost[i];  // 记录总共剩余油量
            if(curSum<0)    // 说明油量不足,因此0~i都不能作为起始点
            {
   
                curSum=0;
                start=i+1;
            }
        }
        if(totalSum<0)   // 总共剩余油量都不够,那就肯定走不完一圈
        {
   
            return -1;
        }
        return start;
    }
};

补:会议室2

在这里插入图片描述
思路:这样看,最少的会议室数量,其实就是同一时间使用的最多会议室数量,因为必须满足这一个时间,那么多会议都有会议室用。那我们可以思考在这个区间内,有会议就表示区间都+1(需要一个会议室),那么只要看看那个时间点对应的值,就表示这个时间点所需的会议室数量。这种区间增量的问题,可以用差分数组+前缀和。

/**
 * Definition of Interval:
 * classs Interval {
 *     int start, end;
 *     Interval(int start, int end) {
 *         this->start = start;
 *         this->end = end;
 *     }
 * }
 */

class Solution {
   
public:
    /**
     * @param intervals: an array of meeting time intervals
     * @return: the minimum number of conference rooms required
     */
    int minMeetingRooms(vector<Interval> &intervals) {
   
        // Write your code here
        int last = INT_MIN;
        for(int i=0;i<intervals.size();i++)
        {
   
            last = max(last, intervals[i].end);
        }
        vector<int> f(last+1, 0);
        for(int i=0;i<intervals.size();i++)
        {
   
            int left = intervals[i].start;
            int right = intervals[i].end;
            f[left]++;   // 差分
            f[right]--;
        }
        int ans=f[0];
        for(int i=1;i<=last;i++)
        {
   
            f[i]+=f[i-1];   // 前缀和
            ans = max(ans, f[i]);
        }
        return ans;

    }
};

八、分治

分治的本质就是拆分成最小的部分进行操作。

1、为运算表达式设计优先级

在这里插入图片描述
思路:分治的思想将表达式向下不断切成更小的表达式(最小的表达式就是只有数字,不包含运算符),切完之后回溯的过程中进行表达式的计算,逐渐向上传递结果。

class Solution {
public:
    vector<int> diffWaysToCompute(string expression) {
        vector<int> count;
        for(int i=0;i<expression.size();i++)
        {
            char c = expression[i];
            if(c=='+' || c=='-' || c=='*')
            {
                vector<int> left = diffWaysToCompute(expression.substr(0, i));   // 计算左侧表达式结果
                vector<int> right = diffWaysToCompute(expression.substr(i+1));   // 计算右侧表达式结果
                for(auto & l : left)
                {
                    for(auto &r : right)
                    {
                        if(c=='+')
                        {
                            count.push_back(l+r);
                        }else if(c=='-')
                        {
                            count.push_back(l-r);
                        }else if(c=='*')
                        {
                            count.push_back(l*r);
                        }
                    }
                }
            }
        }
        if(count.size()==0)   // 没有运算符,已经切成最小的表达式,只包含数字
        {
            count.push_back(stoi(expression));
        }
        return count;

    }
};

2、不同的二叉搜索树2

在这里插入图片描述
思路:一开始的想法是,将1~n全排列,然后对每一种排列情况进行建树,但是会出现重复的情况,剪完树再去重的话成本太高。想到其实建树的问题可以很好的用递归来解决(和上一题非常像)。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<TreeNode*> create(int left, int right)    // 表示left~right的范围内能建的树
    {
        vector<TreeNode*> ans;
        if(left>right)       // 下标不符,只能返回空
        {
            ans.push_back(nullptr);
            return ans;
        }
        for(int i=left;i<=right;i++)
        {
            vector<TreeNode*> left_vec = create(left, i-1);    // 左子树
            vector<TreeNode*> right_vec = create(i+1, right);  // 右子树
            for(auto l : left_vec)     // 分别遍历,因为可以有不同的组合方式
            {
                for(auto r : right_vec)
                {
                    TreeNode* root = new TreeNode(i);
                    root->left = l;
                    root->right = r;
                    ans.push_back(root);
                }
            }
        }
        return ans;
    }
    vector<TreeNode*> generateTrees(int n) {
        return create(1, n);

    }
};

九、树状数组

动态维护前缀和,就利用树状数组。

1、计算右侧小于当前元素的个数

在这里插入图片描述
思路:为什么这道题能用树状数组?例如[5,5,2,3,6],我们可以列出其对应的哈希表:

而要考虑右侧小于当前元素的个数,应该从右往左遍历。一开始哈希表全为0。
(1)从6遍历开始,此时6索引下+1,而6的前缀和(1~5)是0,因此表示没有小于6的元素出现过;
此时index:12345678
此时value:00000100
(2)再遍历到3,前缀和1~2也是0;
此时index:12345678
此时value:00100100
(3)再遍历到2,前缀和1~1也是0;
此时index:12345678
此时value:01100100
(4)再遍历到5,前缀和1~4是2,表示在5的右侧出现过2个小于5的数;
此时index:12345678
此时value:01101100
(5)最后遍历到5时,同样前缀和也是1~4是2,因为要严格小于5,因此重复的5不算。
此时index:12345678
此时value:01102100
现在可以发现前缀和正好有利于求解题目,而问题是如果直接列哈希表,范围太大,因为可能出现很多0值,导致成本过高;此外,题目要求严格小于,因此重复出现并不会影响计数,可以去重。因此最终的解决方法是:排序+去重,得到一个新的数列,用来初始化树状数组。
这里只能用树状数组来求前缀和,不然求1~i-1用遍历来求会超时。树状数组是变成了,如果有小于v的元素出现,直接让c[v]+1,这样直接就能获得有多少小于v的元素了。

class Solution {
private:
    //原数组为nums,
    //将nums离散化,此处是排序+去重,转化为数组a
    vector<int> a;
    //将nums对应a的元素update到树状数组c
    vector<int> c;

    //resize树状数组大小
    void init(int len) {
        c.resize(len);
    }

    //lowbit为二进制中最低位的1的值
    int lowbit(int x) {
        return x & (-x);
    }

    //单点更新,从子节点更新到所有父节点(祖父节点等一直往上到上限c.size())
    void update(int pos) {
        while (pos < c.size()) {
            c[pos] += 1;   // 后面位置上每个元素都要+1,表示有小元素出现了
            pos += lowbit(pos);   // 往后遍历
        }
    }

    //查询,实际是求和[0,...,pos],即求1~pos的元素数量
    //如c[8],在update时,a[1],a[2],a[3],...,a[8]都会使c[8]增加一个value(该题中我们设置为1)
    //res += c[8],然后8减去lowbit为0。
    //也可以拿c[6]举例,c[6] =a[5]+a[6],lowbit后,c[4] = a[1]+a[2]+a[3]+a[4]
    int query(int pos) {
        int res = 0;
        while (pos) {
            res += c[pos];   // 求前缀和
            pos -= lowbit(pos);   // 往前遍历
        }
        return res;
    }

    //离散化处理
    void Discretization(vector<int>& nums) {
        //拷贝数组 [5,4,5,3,2,1,1,1,1,1]
        a.assign(nums.begin(), nums.end());
        //排序[1,1,1,1,1,2,3,4,5,5]
        sort(a.begin(), a.end());
        //去重[1,2,3,4,5]
        a.erase(unique(a.begin(), a.end()), a.end());
    }

    int getId(int x) {
        //lower_bound返回第一个不小于x的迭代器
        //[1,2,3,4,5]中1,减去begin()再加1,得到id(1-5)
        return lower_bound(a.begin(), a.end(), x) - a.begin() + 1;  //+1的目的是不要让id出现0,从1开始
    }

public:
    vector<int> countSmaller(vector<int>& nums) {
        vector<int> res;

        //将nums转化为a
        Discretization(nums);

        //题解是+5,其实+1就够了,树状数组中我们不使用0下标,所以需扩展1位空间
        init(a.size()+1);   // 对离散化去重后得到的a,构建树状数组

        int n = nums.size();  // 从右往左遍历nums
        for (int i=n-1; i>=0; --i) {
            //倒序处理
            int id = getId(nums[i]);
            //查询严格小于id的元素数量,所以使用id-1
            res.push_back(query(id-1));
            //更新id,其实更新也可以提前,因为查询是id-1,所以更新操作不影响当前结果
            update(id);
        }
        //倒序处理再倒序回来。如果不是用push_back,直接用下标可以不用在这里再倒序
        reverse(res.begin(), res.end());
        return res;
    }
};

十、搜索

BFS:广度优先搜索,可以求解无权图的最短路径。

使用BFS的注意事项:

  • 队列:用来存储每一轮遍历得到的节点;
  • 标记:对于遍历过的节点,应该将它标记,防止重复遍历。

1、二进制矩阵中的最短路径

在这里插入图片描述
思路:用bfs来求最短路径。由于bfs每次按层遍历,不像dfs会走回头路,因此当设完访问标记后,不用恢复状态。
BFS过程:

  • 1、创建队列,将起点入队,并设置访问状态
  • 2、遍历当前队列中的元素,出队一个元素,然后for循环遍历该元素可能走的下一步,将下一步再入队
  • 3、每出队一个元素,根据终止条件进行判断

不过这个流程有点像dfs。。。

class Solution {
public:

    int x[8] = {-1, 1, 0, 0, -1, -1, 1, 1};
    int y[8] = {0, 0, -1, 1, -1, 1, -1, 1};
    int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
        if(grid[0][0]==1)   // 开头第一个就是1,说明不能走
        {
            return -1;
        }
        int n = grid.size();
        queue<vector<int> > q;
        vector<int> tmp = {0, 0, 1};    // 保存i, j, 当前路径长度
        q.push(tmp);
        grid[0][0]=1;  // 表示已经通过了

        int ans=INT_MAX;
        while(!q.empty())
        {
            vector<int> node = q.front();
            q.pop();
            
            if(node[0]==n-1&&node[1]==n-1)   // 到达终点时,比较路径长度,取最短的
            {
                ans = min(ans, node[2]);
            }
            for(int k=0;k<8;k++)    // 可以走8个方向
            {
                int newX = node[0] + x[k];
                int newY = node[1] + y[k];
                if(newX>=0&&newX<n&&newY>=0&&newY<n && grid[newX][newY]==0)  // 能走的格子,再入队
                {
                    grid[newX][newY]=1;  // 说明这个格子已经走过了
                    vector<int> new_node = {newX, newY, node[2]+1};
                    q.push(new_node);
                }
            }

        }
        return ans==INT_MAX ? -1 : ans;


    }
};

另一种写法

class Solution {
public:
    int X[8] = {-1, 1, 0, 0, -1, -1, 1, 1};
    int Y[8] = {0, 0, -1, 1, -1, 1, -1, 1};
    int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
        if(grid[0][0]==1)
            return -1;
        int n=grid.size(),ans=1;
        
        queue<pair<int,int> > q;
        q.emplace(0,0);         //从0,0开始
        grid[0][0]=1;           //标记为1代表走过
        while(!q.empty()){      //bfs
            int size=q.size();
            while(size--){
                auto [x,y]=q.front();
                q.pop();
                if(x==n-1&&y==n-1)
                    return ans;
                for(int i=0;i<8;i++)    //遍历八个方向的
                {                       
                    int nx=x+X[i];
                    int ny=y+Y[i];
                    if(nx<0||ny<0||nx>=n||ny>=n)
                        continue;   //判断是否越界
                    if(grid[nx][ny]==0) //判断是否能走
                    {        
                        q.emplace(nx,ny);
                        grid[nx][ny]=1;         //标记
                    }
                }
            }
            ans++;          //记录循环次数
        }
        return -1;

    }
};

2、完全平方数

在这里插入图片描述
思路:这里用bfs来记录当前和,而不是记录当前入队的完全平方数。用vis来标记所经过的当前和,用来剪枝。
不同于上一题的BFS步骤:

  • 建队,记步骤=1
  • 遍历当前队列中的所有节点,这样避免和下一层节点混在一起
  • 判断当前节点是否满足终止条件,如果不是且没访问过,那就再次入队
class Solution {
public:
    int numSquares(int n) {
        unordered_set<int> visited;
        queue<int> q{
  {0}};    // 一开始和为0
        int steps = 1;
        while (!q.empty()) {
            auto size = q.size();
            while (size--) {    // 遍历当前层的节点,不要让当前层和下一层的节点混在一起
                auto cur = q.front(); q.pop();
                for (int i = 1; i * i + cur <= n; i++) {     // 为当前层中每一个节点,再找一个能满足条件的完全平方数
                    auto next = i * i + cur;    // 更新当前的和
                    if (next == n) {
                        return steps;
                    }
                    if (!visited.count(next)) {       // 记忆化,如果当前和已经出现过,说明已经尝试使用当前和,但失败了。因此做标记来避免重复搜索。
                        visited.insert(next);
                        q.push(next);
                    }
                }
            }
            steps++;
        }

        return -1; // should never reach here.
    }
};


3、单词接龙

在这里插入图片描述
思路:其实想法比较直接,可以上BFS。
给定的条件也比较明显:
(1)开头、结尾都给出了
(2)下一次遍历该选谁入队也给出了
但简单实用BFS造成了超时。这里一个剪枝的方法,因为下一个单词和当前单词只有一个字符的差别(更改这个字符后,两者完全一样),那么可以尝试用26个字母分别替换当前单词的每个位置,然后判断该单词是否在wordList中,可以利用哈希查找,加快遍历速度。

class Solution {
public:
    bool judge(string tmp, string str)
    {
        int num=0;
        for(int i=0;i<tmp.size();i++)
        {
            if(tmp[i]!=str[i])
            {
                num++;
            }
        }
        if(num==1)
            return true;
        else{
            return false;
        }
    }
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        queue<string> q;
        q.push(beginWord);
        int step=1;
        unordered_set<string> wordSet(wordList.begin(), wordList.end());   // 变成哈希表,因为每个单词各不相同
        unordered_map<string, int> visted;  // 访问情况
        visted[beginWord]=1;

        while(!q.empty())
        {
            int size = q.size();
            while(size--)
            {
                auto str = q.front();
                q.pop();
                if(str==endWord)
                {
                    return step;
                }
                for (int j = 0; j < str.size(); j++)   //遍历单词的每个位置
                { 
                    string lWord = str;
                    for (int k = 0; k < 26; k++)   //26个字母替换第j个位置
                    { 
                        lWord[j] = k + 'a';
                        // 字典中有这个字符串,并且没有被访问过
                        if (wordSet.count(lWord) && !visted.count(lWord)) 
                        {   
                            q.push(lWord);
                            visted[lWord]=1;
                        }
                    }
                }
            }
            step++;
        }
        return 0;

    }
};

DFS

BFS是一层一层遍历,将每层的新节点遍历完后,才遍历下一层节点。DFS是在得到一个新节点时立即对新节点进行遍历,直到没有新节点才返回。
从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。
DFS需要注意的问题:

  • 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
  • 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。

4、岛屿的最大面积

在这里插入图片描述
思路:就是计算连通1的最大数量,体现了dfs的可达性(究竟能到达多少个1),因此是典型的dfs。

class Solution {
public:
    int X[4] = {-1, 1, 0, 0};
    int Y[4] = {0, 0, -1, 1};
    int ans=0;
    int num=0;
    void dfs(vector<vector<int>>& grid, int i, int j)
    {
        int m = grid.size();
        int n = grid[0].size();

        ans = max(num, ans);
        
        for(int k=0;k<4;k++)
        {
            int newX = i + X[k];
            int newY = j + Y[k];
            if(newX>=0&&newX<m&&newY>=0&&newY<n && grid[newX][newY]==1)
            {
                grid[newX][newY]=0;
                num++;
                dfs(grid, newX, newY);
            }
        }
    }
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        for(int i=0;i<m;i++)
        {
            for(int j=0;j<n;j++)
            {
                if(grid[i][j]!=0)
                {
                    num=1;
                    grid[i][j]=0;
                    dfs(grid, i, j);
                }
            }
        }
        return ans;
    }
};

补:统计封闭岛屿的数目

在这里插入图片描述
思路:就是加一个标记位,如果这个水域有碰到边界,那就不计数。

class Solution {
   
public:
    int ans=0;
    int X[4] = {
   -1, 1, 0, 0};
    int Y[4] = {
   0, 0, -1, 1};
    void dfs(vector<vector<int>>& grid, int x, int y, int& f_mark)
    {
   
        int m = grid.size();
        int n = grid[0].size();
        if(x==0||x==m-1||y==0||y==n-1)
        {
   
            f_mark=0;
            return;
        }
        if(grid[x][y]==0)
        {
   
            grid[x][y]=1;
        }
        for(int i=0;i<4;i++)
        {
   
            int newX = x+X[i];
            int newY = y+Y[i];
            if(newX>=0&&newX<m && newY>=0&&newY<n && grid[newX][newY]==0)
            {
   
                dfs(grid, newX, newY, f_mark);
            }
        }
    }
    int closedIsland(vector<vector<int>>& grid) {
   
        int m = grid.size();
        int n = grid[0].size();
        for(int i=0;i<m;i++)
        {
   
            for(int j=0;j<n;j++)
            {
   
                if(grid[i][j]==0)
                {
   
                    int f_mark=1;
                    dfs(grid, i, j, f_mark);
                    if(f_mark==1)
                    {
   
                        ans++;
                    }
                }
            }
        }
        return ans;
    }
};

补:飞地的数量

在这里插入图片描述
思路:从边界开始,把1变成0,那么剩下的1都在0的包围里。

class Solution {
   
public:
    int X[4] = {
   -1, 1, 0, 0};
    int Y[4] = {
   0, 0, -1, 1};
    void dfs(vector<vector<int> >& grid, int x, int y)
    {
   
        int m = grid.size();
        int n = grid[0].size();
        grid[x][y]=0;
        for(int i=0;i<4;i++)
        {
   
            int newX = x+X[i];
            int newY = y+Y[i];
            if(newX>=0&&newX<m &&newY>=0&&newY<n && grid[newX][newY]==1)
            {
   
                dfs(grid, newX, newY);
            }
        }
    }
    int numEnclaves(vector<vector<int>>& grid) {
   
        int m = grid.size();
        int n = grid[0].size();
        for(int i=0;i<m;i++)
        {
   
            if(grid[i][0]==1)
            {
   
                dfs(grid, i, 0);
            }
            if(grid[i][n-1]==1)
            {
   
                dfs(grid, i, n-1);
            }
        }
        for(int j=0;j<n;j++)
        {
   
            if(grid[0][j]==1)
            {
   
                dfs(grid, 0, j);
            }
            if(grid[m-1][j]==1)
            {
   
                dfs(grid, m-1, j);
            }
        }
        int ans=0;
        for(int i=0;i<m;i++)
        {
   
            for(int j=0;j<n;j++)
            {
   
                if(grid[i][j]==1)
                {
   
                    ans++;
                }
            }
        }
        return ans;

    }
};

补:统计子岛屿

在这里插入图片描述
思路:只要遍历grid2,如果该位置有1,且grid1对应位置也是1,那就ok,继续递归;如果grid1不是1,那就直接返回。

class Solution {
   
public:
    int X[4] = {
   -1, 1, 0, 0};
    int Y[4] = {
   0, 0, -1, 1};
    void dfs(vector<vector<int>>& grid1, vector<vector<int>>& grid2, int x, int y, int& f_mark)
    {
   
        int m = grid2.size();
        int n = grid2[0].size();
        if(grid2[x][y]==1 && grid1[x][y]==1)
        {
   
            grid2[x][y]=0;
        }else{
   
            f_mark=0;
            return;
        }
        for(int i=0;i<4;i++)
        {
   
            int newX = x+X[i];
            int newY = y+Y[i];
            if(newX>=0&&newX<m&&newY>=0&&newY<n&&grid2[newX][newY]==1)
            {
   
                dfs(grid1, grid2, newX, newY, f_mark);
            }
        }
    }
    int countSubIslands(vector<vector<int>>& grid1, vector<vector<int>>& grid2) {
   
        int m = grid1.size();
        int n = grid1[0].size();
        int ans=0;
        for(int i=0;i<m;i++)
        {
   
            for(int j=0;j<n;j++)
            {
   
                if(grid2[i][j]==1)
                {
   
                    int f_mark=1;
                    dfs(grid1, grid2, i, j, f_mark);
                    if(f_mark)
                    {
   
                        ans++;
                    }
                }
            }
        }

        return ans;
    }
};

补:不同的岛屿数量2

在这里插入图片描述
思路:先用dfs,找到所有的岛屿坐标,然后将坐标的不同方向都转化成字符串,进行去重。

class Solution {
   
public:
    /**
     * @param grid: the 2D grid
     * @return: the number of distinct islands
     */
    int X[4] = {
   -1, 1, 0, 0};
    int Y[4] = {
   0, 0, -1, 1};
    void dfs(vector<vector<int>> &grid, int x, int y, vector<pair<int,int> > & land)
    {
   
        int m = grid.size();
        int n = grid[0].size();
        if(grid[x][y]==1)
        {
   
            grid[x][y]=0;
            land.push_back(make_pair(x, y));
        }   
        for(int i=0;i<4;i++)
        {
   
            int newX = x+X[i];
            int newY = y+Y[i];
            if(newX>=0&&newX<m&&newY>=0&&newY<n && grid[newX][newY]==1)
            {
   
                dfs(grid, newX, newY, land);
            }
        }
    }

    string getstring(vector<pair<int, int>>& island)
    {
   
        sort(island.begin(), island.end(), 
        [](pair<int, int>& a, pair<int, int>& b)
        {
   
            if (a.first != b.first)
                return a.first < b.first;
            else if (a.first == b.first)
            {
   
                return a.second < b.second;
            }
        });

        // 算出左上角的点,然后进行标准化
        int ox = island[0].first;
        int oy = island[0].second;
        string res = "";
        for (auto p : island)
        {
      
            res += to_string(p.first - ox) + " " + to_string(p.second - oy);
        }
        return res;
    }
    string getpatten(vector<pair<int, int>>& islandset)
    {
   
        int dx[4] = {
   1, -1, 1
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值