算法—分治

快速排序

颜色分类

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

三个指针的作用:

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

遍历过程的四个区间:

  • 0,left \]:全都是 0

  • i,right - 1 \]:待遍历区域

当前遍历的元素(nums[i])的三种情况:

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

遍历如图所示:

代码:

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 的值

  • i,right - 1 \]:待遍历区域

优化:用随机的方式选择基准元素,这样可以让时间复杂度更加接近 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. 找出该数之前,有多少个数比我大

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

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

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;
    }
};
相关推荐
2501_944525543 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 预算详情页面
android·开发语言·前端·javascript·flutter·ecmascript
zhuqiyua3 小时前
第一次课程家庭作业
c++
3 小时前
java关于内部类
java·开发语言
好好沉淀3 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
只是懒得想了3 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
lsx2024063 小时前
FastAPI 交互式 API 文档
开发语言
VCR__3 小时前
python第三次作业
开发语言·python
ruxshui3 小时前
个人笔记: 星环Inceptor/hive普通分区表与范围分区表核心技术总结
hive·hadoop·笔记
码农水水3 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
wkd_0073 小时前
【Qt | QTableWidget】QTableWidget 类的详细解析与代码实践
开发语言·qt·qtablewidget·qt5.12.12·qt表格