算法—分治

快速排序

颜色分类

**思路:**定义三个指针 i,left,right。(三个指针在遍历过程中会将数组分为四块,但是遍历结束时会将数组分为三块,题目中三种颜色的划分正好需要将数组分为三块)

三个指针的作用:

  • i:用来遍历数组
  • left:标记 0 区域的最右侧
  • right:标记 2 区域的最左侧

遍历过程的四个区间:

  • 0,left :全都是 0
  • left + 1,i - 1 :全都是 1
  • i,right - 1 :待遍历区域
  • right,n - 1 :全都是 2(n 是数组元素个数)

当前遍历的元素(numsi)的三种情况:

  • numsi = 0:swap( nums ++left , nums i++ )
  • numsi = 1:i++
  • numsi = 2:swap( nums --right , numsi )

遍历如图所示:

代码:

cpp 复制代码
class Solution {
public:
    void sortColors(vector<int>& nums) {
        int i = 0;
        int left = -1;
        int right = nums.size();
        while(i < right){
            if(nums[i] == 0){
                swap(nums[++left], nums[i++]);
            }
            else if(nums[i] == 1){
                i++;
            }
            else{
                swap(nums[--right], nums[i]);
            }
        }
    }
};

排序数组

**思路:**使用快排来解决这道题,核心排序步骤和上一道题(颜色分类)一样,使用数组分三块的思想来排序。选择一个基准值key,定义三个指针 i,left,right,然后根据基准值进行数组分三块的过程,这个过程结束,left + 1, right - 1 这个区间其实就排好了(区间内元素都等于 key),然后递归排序还没有排好的左区间和右区间。

遍历过程的四个区间:

  • 0,left :小于 key 的值
  • left + 1,i - 1 :等于 key
  • i,right - 1 :待遍历区域
  • right,n - 1 :全都是大于 key 的值(n 是数组元素个数)

优化:用随机的方式选择基准元素,这样可以让时间复杂度更加接近 O(nlogn),即 r = rand() % (right - left + 1) + left。(r 是随机的基准元素的下标, 这里的 left,right 是要排序区间的左右端点)

代码:

cpp 复制代码
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        srand(time(NULL));
        qsort(nums, 0, nums.size() - 1);
        return nums;
    }

    void qsort(vector<int>& nums, int l, int r){
        if(l >= r)
            return;

        int key = nums[rand() % (r - l + 1) + l];
        int i = l;
        int left = l - 1;
        int right = r + 1;
        while(i < right){
            if(nums[i] < key){
                swap(nums[++left], nums[i++]);
            }
            else if(nums[i] > key){
                swap(nums[--right], nums[i]);
            }
            else{
                i++;
            }
        }

        qsort(nums, l, left);
        qsort(nums, right, r);
    }
};

数组中的第K个最大元素

**思路:**我们使用快排(数组分三块 + 随机选择基准元素)的方式来解决这道题,首先使用本文第二道题快排的方式对区间数组进行排序,但是并不是将数组直接彻底排好,快排彻底排好序需要进行若干次递归,这道题中每次递归排序结束后我们进行如下判断:

  • c >= k:不需要管 right 左边的区间,直接去 right, r 区间继续排序找第 k 大即可。
  • b + c >= k:直接返回 key(随机选择的基准元素)。
  • 上面两个不成立时:去 l , left 区间继续排序找第 k 大元素。

代码:

cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        srand(time(NULL));
        return qsort(nums, 0, nums.size() - 1, k);
    }

    int qsort(vector<int>& nums, int l, int r, int k){
        if(l == r)
            return nums[l];

        int key = nums[rand() % (r - l + 1) + l];
        int left = l - 1;
        int right = r + 1;
        int i = l;
        while(i < right){
            if(nums[i] < key){
                swap(nums[++left], nums[i++]);
            }
            else if(nums[i] > key){
                swap(nums[--right], nums[i]);
            }
            else {
                i++;
            }
        }

        int c = r - right + 1;
        int b = right - left - 1;
        if(c >= k){
            return qsort(nums, right, r, k);
        }
        else if(b + c >= k){
            return key;
        }
        else{
            return qsort(nums, l, left, k - b - c);
        }
    }
};

库存管理 III

**思路:**我们使用快排(数组分三块 + 随机选择基准元素)的方式来解决这道题,首先使用本文第二道题快排的方式对区间数组进行排序,但是并不是将数组直接彻底排好,快排彻底排好序需要进行若干次递归,这道题中每次递归排序结束后我们进行如下判断:

  • a > k:不需要管 left 右边的区间,直接去 l, left 区间继续找最小的 k 个元素。
  • a + b >= k:排序结束,直接返回前 k 个元素即可(建立在第一个判断为 false 的前提下)。
  • 上面两个不成立时:去 right , r 区间继续排序找最小的 k - a - b 个元素。

这些判断的作用其实就是快速将前 k 个小的元素放到数组的前 k 个位置,并且这前 k 个小的元素之间没有必要排序。

代码:

cpp 复制代码
class Solution {
public:
    vector<int> inventoryManagement(vector<int>& stock, int cnt) {
        srand(time(NULL));
        qsort(stock, 0, stock.size() - 1, cnt);
        return {stock.begin(), stock.begin() + cnt};
    }

    void qsort(vector<int>& nums, int l, int r, int k){
        if(l >= r)
            return;

        int key = nums[rand() % (r - l + 1) + l];
        int left = l - 1;
        int right = r + 1;
        int i = l;
        while(i < right){
            if(nums[i] < key){
                swap(nums[++left], nums[i++]);
            }
            else if(nums[i] > key){
                swap(nums[--right], nums[i]);
            }
            else{
                i++;
            }
        }

        int a = left - l + 1;
        int b = right - left - 1;
        if(a > k){
            qsort(nums, l, left, k);
        }
        else if(a + b > k){
            return;
        }
        else{
            qsort(nums, right, r, k - a - b);
        }
    }
};

归并排序

排序数组

**思路:**这里使用归并排序的方法,计算数组中间下标,根据中间下标将数组分为左右两部分,分别将左右两部分排好序,然后左右部分进行合并,左右部分的排序只需要继续递归,不断将待排序区间分为左右两部分,直到待排序区间只有一个元素,此时无需排序了,递归结束,开始向上返回,返回过程中将排好序的左右两部分进行合并。

代码:

cpp 复制代码
class Solution {
    vector<int> tmp;
public:
    vector<int> sortArray(vector<int>& nums) {
        tmp.resize(nums.size());
        mergeSort(nums, 0, nums.size() - 1);
        return nums;
    }

    void mergeSort(vector<int>& nums, int left, int right){
        if(left >= right) return;

        int mid = left + (right - left) / 2;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);

        int cur1 = left;
        int cur2 = mid + 1;
        int i = 0;
        while(cur1 <= mid && cur2 <= right)
            tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];
        while(cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while(cur2 <= right)
            tmp[i++] = nums[cur2++];

        for(int i = left; i <= right; i++){
            nums[i] = tmp[i - left];
        }
    }
};

交易逆序对的总数

**思路:**这里使用归并排序的思想解决这道问题,找一个数组中的所有逆序对,我们可以根据数组的中间下标将数组分为左右两部分,分别找左边部分的逆序对,右边部分的逆序对,一左一右(即左右部分各出一个数)的逆序对,三种情况的逆序对数加起来就是最终结果。其中只在左边部分找逆序对的情况可以继续向下递归,分成左右部分,然后继续找左边部分的逆序对,右边部分的逆序对,一左一右的逆序对。右边部分同理,向下递归即可。所以最终其实就是在处理一左一右的情况。

找一左一右情况逆序对的两种策略:

1. 找出该数之前,有多少个数比我大

这种情况必须排升序,否则计算逆序对数时会重复计算。计算过程如下图:(横线表示数组)

因为两边区间已经排好序了,所以当 numscur1 > numscur2 时,numscur1 后面的元素都大于 numscur2,所以cur1 , mid 这个区间(包含 cur1 和 mid)的数都能和 numscur2 构成逆序对。

2. 找出该数之前,有多少个数比我小

这种情况必须排降序,否则计算逆序对数时会重复计算。计算过程和上面同理。

代码:

cpp 复制代码
class Solution {
    vector<int> tmp;
public:
    int reversePairs(vector<int>& record) {
        tmp.resize(record.size());
        return mergeSort(record, 0, record.size() - 1);
    }

    int mergeSort(vector<int>& nums, int left, int right){
        if(left >= right)
            return 0;

        int ret = 0;
        int mid = left + (right - left) / 2;
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);

        int cur1 = left;
        int cur2 = mid + 1;
        int i = 0;
        while(cur1 <= mid && cur2 <= right){
            if(nums[cur1] <= nums[cur2]){
                tmp[i++] = nums[cur1++];
            }
            else{
                ret += mid - cur1 + 1;
                tmp[i++] = nums[cur2++];
            }
        }

        while(cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while(cur2 <= right)
            tmp[i++] = nums[cur2++];

        for(int i = left; i <= right; i++)
            nums[i] = tmp[i - left];

        return ret;
    }
};

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

**思路:**这道题可以借助归并排序来解决,对数组进行归并排序,在左右区间排好序进行合并的时候进行如下判断:(注意:此题中归并排序排的是降序)

但是由于排序会导致数组元素位置变化,导致算出来的数据如果直接填入结果数组位置会不正确,所以需要一个辅助数组来记录当前元素原始的数组下标是多少,这个用来记录下标的数组只需要将正确的下标先记录下来,然后题目给出的数组中的元素一同移动位置即可。

代码:

cpp 复制代码
class Solution 
{
    vector<int> ret;
    vector<int> index;//记录nums数组中当前元素的原始下标

    int tmpNums[500010];
    int tmpIndex[500010];
public:
    vector<int> countSmaller(vector<int>& nums) 
    {
        ret.resize(nums.size());
        index.resize(nums.size());
        for(int i = 0; i < nums.size() - 1; i++)
        {
            index[i] = i;
        }
        mergeSort(nums, 0, nums.size() - 1);
        return ret;
    }

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

        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);

        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right)
        {
            if(nums[cur1] <= nums[cur2])
            {
                tmpNums[i] = nums[cur2];
                tmpIndex[i++] = index[cur2++];
            }
            else
            {
                ret[index[cur1]] += right - cur2 + 1;
                tmpNums[i] = nums[cur1];
                tmpIndex[i++] = index[cur1++];
            }
        }

        while(cur1 <= mid)
        {
            tmpNums[i] = nums[cur1];
            tmpIndex[i++] = index[cur1++];
        }
        while(cur2 <= right)
        {
            tmpNums[i] = nums[cur2];
            tmpIndex[i++] = index[cur2++];
        }
        for(int j = left; j <= right; j++)
        {
            nums[j] = tmpNums[j - left];
            index[j] = tmpIndex[j - left];
        }
    }
};

翻转对

**思路:**这道题的思路和上面两题一样,就不在赘述了,不同的是这道题给出的比较关系是二倍的关系,而归并排序中合并有序数组时是直接比较的,不需要乘 2,所以此题计算有多少符合条件的翻转对是在左右区间排好序后,合并前单独进行一次循环来统计。具体参考如下代码(下面代码采用的是降序,升序也可以解决这道题)。

代码:

cpp 复制代码
class Solution 
{
    vector<int> temp;
public:
    int reversePairs(vector<int>& nums) 
    {
        temp.resize(nums.size());
        int ret = mergeSort(nums, 0, nums.size() - 1);
        return ret;
    }

    int mergeSort(vector<int>& nums, int left, int right)
    {
        if(left >= right)
            return 0;

        int ret = 0;
        int mid = (left + right) >> 1;
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);

        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid)
        {
            while(cur2 <= right && nums[cur2] >= nums[cur1] / 2.0)
                cur2++;
            if(cur2 > right)
                break;

            ret += right - cur2 + 1;
            cur1++;
        }

        cur1 = left;
        cur2 = mid + 1;
        while(cur1 <= mid && cur2 <= right)
        {
            if(nums[cur1] > nums[cur2])
                temp[i++] = nums[cur1++];
            else
                temp[i++] = nums[cur2++];
        }

        while(cur1 <= mid)
            temp[i++] = nums[cur1++];
        while(cur2 <= right)
            temp[i++] = nums[cur2++];
        for(int i = left; i <= right; i++)
            nums[i] = temp[i - left];

        return ret;
    }
};
相关推荐
8Qi81 小时前
LeetCode 516:最长回文子序列
算法·leetcode·职场和发展·动态规划
秋91 小时前
Go语言(Golang)开发工程师全景解析:岗位职责·语言优势与使用场景·各城市薪资·发展前景·高考志愿填报(2026版)
开发语言·golang·高考
和平宇宙1 小时前
AI笔记005. hermes-DeepSeek V4 Pro, 128K上下文引发的探索
前端·人工智能·笔记
十月的皮皮1 小时前
C语言学习笔记20260606- 求月份天数三种写法
c语言·笔记·学习
cmes_love2 小时前
Level 2逐笔成交历史数据下载方法笔记
数据库·笔记·oracle
huangdong_2 小时前
1688商品图片采集技术解析:登录态处理与SKU图自动分类
开发语言
搬砖魁首2 小时前
基础能力系列 - 多线程2 - 条件变量
c++·rust·条件变量·原子类型·线程同步互斥
youngerwang2 小时前
【从搬运工到协处理器:网卡芯片架构、算法、验证与边缘演进深度剖析】
网络·算法·架构·芯片
chase_my_dream2 小时前
C++ + SLAM 高频面试问题整理
开发语言·c++·面试
牛油果子哥q2 小时前
【C++ STL string 】C++ STL string 终极精讲:底层原理、内存机制、全套API、深浅拷贝、易错坑点与工程实战规范
数据库·c++