【优选算法】(实战掌握分治思想的使用方法)


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法的世界里,"高效求解"是永恒的追求,而分治思想便是实现这一追求的核心利器之一.分治算法(Divide and Conquer)的核心逻辑简洁而强大,即通过"分解、解决、合并"三个步骤,将一个庞大、复杂的原问题,拆解为若干个规模更小、结构与原问题一致的独立子问题,递归求解每个子问题后,再将子问题的解整合为原问题的最终答案.这种思想的优势在于,它能将原本复杂度极高的问题(如时间复杂度O(n²)、O(n³)),大幅简化为可高效处理的形式,许多经典优选算法都离不开它的支撑:归并排序、快速排序凭借分治实现高效排序,二分查找依托分治实现O(log n)的极致查找效率,大整数乘法、最近点对问题等也通过分治突破传统算法的性能瓶颈.本文立足"实战"核心,跳出纯理论讲解的框架,聚焦分治思想在优选算法中的实际应用方法.我们将从分治思想的核心本质出发,拆解其适用场景与判断标准,通过经典例题拆解实战步骤,梳理常见误区与优化技巧,帮助学习者真正理解"如何拆分问题、如何求解子问题、如何合并结果",实现从"理解理论"到"实战运用"的跨越,让分治思想成为你破解优选算法难题的"必备工具".废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.分治算法思想背景介绍

分治算法的核心思想分而治之 ,并非计算机领域独有,而是源自人类解决复杂问题的朴素思维方式:面对规模庞大、结构复杂、直接处理难度极高的问题时,先将其拆解为若干个规模更小、结构相同/ 相似、相互独立的子问题,逐个解决子问题后,再将子问题的结果合并,最终得到原问题的解.

分治不是具体算法,而是算法设计策略,完整流程分为三步:

  • 分解(Divide):将原问题划分为 k 个规模较小、相互独立、与原问题结构一致的子问题.
  • 解决(Conquer):若子问题规模足够小,直接求解;否则递归执行分治,继续拆分.
  • 合并(Combine):将所有子问题的解整合,得到原问题的最终解.

其本质是用递归缩小问题规模,把难解的大问题转化为易解的小问题.


2.颜色分类(OJ题)


算法思路:解法(快排思想 - 三指针法使数组分三块)

类比数组分两块的算法思想,这里是将数组分成三块,那么我们可以再添加一个指针,实现数组分三块.

设数组大小为 n,定义三个指针 left, i, right:

  • left:用来标记 0 序列的末尾,因此初始化为 -1;
  • i:用来扫描数组,初始化为 0;
  • right:用来标记 2 序列的起始位置,因此初始化为 n.

i 往后扫描的过程中,保证:

  • [0, left] 内的元素都是 0
  • [left + 1, i - 1] 内的元素都是 1
  • [i, right - 1] 内的元素是待定元素;
  • [right, n] 内的元素都是 2.

算法流程:

①初始化 i = 0, left = -1, right = numsSize;

②当 i < right 的时候(因为 right 表示的是 2 序列的左边界,因此当 i 碰到 right 的时候,说明已经将所有数据扫描完毕了),一直进行下面循环:

根据 nums[i]的值,可以分为下面三种情况:

(1)nums[i] == 0:说明此时这个位置的元素需要在 left + 1 的位置上,因此交换 left + 1i 位置的元素,并且让 left++(指向 0 序列的右边界),i++(为什么可以 ++ 呢,是因为 left + 1 位置要么是 0,要么是 i,交换完毕之后,这个位置的值已经符合我们的要求,因此 i++);

(2)nums[i] == 1:说明这个位置应该在 lefti 之间,此时无需交换,直接让 i++,判断下一个元素即可;

(3)nums[i] == 2:说明这个位置的元素应该在 right - 1 的位置,因此交换 right - 1i 位置的元素,并且让 right--(指向 2 序列的左边界),i 不变(因为交换过来的数是没有被判断过的,因此需要在下轮循环中判断)

③当循环结束之后:

  • [0, left] 表示 0 序列;
  • [left + 1, right - 1] 表示 1 序列;
  • [right, numsSize - 1] 表示 2 序列.

核心代码

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

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>  
using namespace std;

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

void printVector(const vector<int>& nums) {
    for (int num : nums) {
        cout << num << " ";
    }
    cout << endl;
}

int main() {
    Solution sol;
    
    vector<int> nums1 = {2, 0, 2, 1, 1, 0};
    cout << "测试用例1 排序前:";
    printVector(nums1);
    sol.sortColors(nums1);
    cout << "测试用例1 排序后:";
    printVector(nums1);
    cout << "-------------------------" << endl;
    
    vector<int> nums2 = {2, 0, 1};
    cout << "测试用例2 排序前:";
    printVector(nums2);
    sol.sortColors(nums2);
    cout << "测试用例2 排序后:";
    printVector(nums2);
    cout << "-------------------------" << endl;
    
    vector<int> nums3 = {0};
    cout << "测试用例3 排序前:";
    printVector(nums3);
    sol.sortColors(nums3);
    cout << "测试用例3 排序后:";
    printVector(nums3);
    cout << "-------------------------" << endl;
    
    vector<int> nums4 = {1, 1, 1};
    cout << "测试用例4 排序前:";
    printVector(nums4);
    sol.sortColors(nums4);
    cout << "测试用例4 排序后:";
    printVector(nums4);
    cout << "-------------------------" << endl;
    
    vector<int> nums5 = {1, 2, 0, 2, 1, 0, 0};
    cout << "测试用例5 排序前:";
    printVector(nums5);
    sol.sortColors(nums5);
    cout << "测试用例5 排序后:";
    printVector(nums5);

    return 0;
}

3.排序数组(OJ题)--用快速排序方法


算法思路:解法(数组分三块思想 + 随机选择基准元素的快速排序)

我们在数据结构阶段学习的快速排序的思想可以知道,快排最核心的一步就是 Partition (分割数据) :将数据按照一个标准,分成左右两部分.

如果我们使用荷兰国旗问题的思想(核心是将数组按特定规则划分为三个独立区域,就像荷兰国旗的红、白、蓝三色条纹一样整齐排列.),将数组划分为左中右 三部分:左边是比基准元素小的数据,中间是与基准元素相同的数据,右边是比基准元素大的数据.然后再去递归的排序左边部分和右边部分即可(可以舍去大量的中间部分).在处理数据量有很多重复的情况下,效率会大大提升.
算法流程:

随机选择基准算法流程:

函数设计:int randomKey(vector<int>& nums, int left, int 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 = getRandom(nums, l, r);
        int i = l, left = l - 1, right = r + 1;
        while (i < right) 
        {
            if (nums[i] < key)
                swap(nums[++left], nums[i++]);
            else if (nums[i] == key)
                i++;
            else
                swap(nums[--right], nums[i]);
        }
        //[l, left] [left + 1, right - 1] [right, r]
        qsort(nums, l, left);
        qsort(nums, right, r);
    }
    int getRandom(vector<int>& nums, int left, int right) 
    {
        int r = rand();
        return nums[r % (right - left + 1) + left];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

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 = getRandom(nums, l, r);
        int i = l, left = l - 1, right = r + 1;
        while (i < right)
        {
            if (nums[i] < key)
                swap(nums[++left], nums[i++]);
            else if (nums[i] == key)
                i++;
            else
                swap(nums[--right], nums[i]);
        }
        //递归排序左区间和右区间,中间相等区间无需处理
        qsort(nums, l, left);
        qsort(nums, right, r);
    }

    //随机获取区间 [left, right] 内的元素作为基准值
    int getRandom(vector<int>& nums, int left, int right)
    {
        int r = rand();
        return nums[r % (right - left + 1) + left];
    }
};

void printVector(const vector<int>& nums) {
    for (int num : nums) {
        cout << num << " ";
    }
    cout << endl;
}

int main() {
    Solution sol;

    vector<int> nums1 = {5, 2, 3, 1, 4};
    cout << "测试用例1 排序前:";
    printVector(nums1);
    cout << "测试用例1 排序后:";
    printVector(sol.sortArray(nums1));
    cout << "-------------------------" << endl;

    vector<int> nums2 = {2, 0, 2, 1, 1, 0, 2, 1, 0};
    cout << "测试用例2 排序前:";
    printVector(nums2);
    cout << "测试用例2 排序后:";
    printVector(sol.sortArray(nums2));
    cout << "-------------------------" << endl;

    vector<int> nums3 = {1, 2, 3, 4, 5};
    cout << "测试用例3 排序前:";
    printVector(nums3);
    cout << "测试用例3 排序后:";
    printVector(sol.sortArray(nums3));
    cout << "-------------------------" << endl;

    vector<int> nums4 = {9, 7, 5, 3, 1};
    cout << "测试用例4 排序前:";
    printVector(nums4);
    cout << "测试用例4 排序后:";
    printVector(sol.sortArray(nums4));
    cout << "-------------------------" << endl;

    vector<int> nums5 = {-3, 5, -1, 0, -5, 2};
    cout << "测试用例5 排序前:";
    printVector(nums5);
    cout << "测试用例5 排序后:";
    printVector(sol.sortArray(nums5));
    cout << "-------------------------" << endl;

    vector<int> nums6 = {10};
    cout << "测试用例6 排序前:";
    printVector(nums6);
    cout << "测试用例6 排序后:";
    printVector(sol.sortArray(nums6));
    cout << "-------------------------" << endl;

    vector<int> nums7;
    cout << "测试用例7 排序前:空数组";
    printVector(sol.sortArray(nums7));
    cout << "测试用例7 排序后:空数组" << endl;

    return 0;
}

4.数组中的第k个最大元素(OJ题)


算法思路:解法(快速选择算法):

在快排中,当我们把数组分成三块之后:[l, left] [left + 1, right - 1] [right, r] ,我们可以通过计算每⼀个区间内元素的个数,进⽽推断出我们要找的元素是在哪⼀个区间⾥⾯.那么我们可以直接去相应的区间去寻找最终结果就好了.

荷兰国旗三路分区:把数组分成三块

左区:< 基准值(key)

中区:== 基准值(key)

右区:> 基准值(key)

第k大:我们要找的数,一定在右区/中区/左区中的一个,只需要递归目标区间即可.

🔥最核心:分情况判断逻辑

先明确三个区间:

右区 [right, r]:所有数 > key(最大的一块)

中区 [left+1, right-1]:所有数 = key

左区 [l, left]:所有数 < key(最小的一块)

变量定义:

c:右区元素数量

b:中区元素数量

c >= k:第 k 大的数就在右区(右区的数都比其他数大),直接递归右区,k不变.

b+c>= k:第 k 大的数在中区(所有数都等于基准),直接返回 key 即可.

都不满足第 k 大的数在左区,递归左区,且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];
        //1. 随机选择基准元素
        int key = getRandom(nums, l, r);
        //2. 根据基准元素将数组分三块
        int left = l - 1, right = r + 1, i = l;
        while (i < right) 
        {
            if (nums[i] < key)
                swap(nums[++left], nums[i++]);
            else if (nums[i] == key)
                i++;
            else
                swap(nums[--right], nums[i]);
        }
        //3. 分情况讨论
        int c = r - right + 1, 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);
    }
    int getRandom(vector<int>& nums, int left, int right) 
    {
        return nums[rand() % (right - left + 1) + left];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

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];
        //1.随机选择基准元素
        int key = getRandom(nums, l, r);
        //2.根据基准元素将数组分三块
        int left = l - 1, right = r + 1, i = l;
        while (i < right)
        {
            if (nums[i] < key)
                swap(nums[++left], nums[i++]);
            else if (nums[i] == key)
                i++;
            else
                swap(nums[--right], nums[i]);
        }
        //3.分情况讨论
        int c = r - right + 1, 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);
    }

    int getRandom(vector<int>& nums, int left, int right)
    {
        return nums[rand() % (right - left + 1) + left];
    }
};

void printVector(const vector<int>& nums) {
    for (int num : nums) {
        cout << num << " ";
    }
}

int main() {
    Solution sol;

    vector<int> nums1 = {3,2,1,5,6,4};
    int k1 = 2;
    cout << "测试用例1 数组:"; printVector(nums1);
    cout << "第 " << k1 << " 大元素:" << sol.findKthLargest(nums1, k1) << endl;
    cout << "-------------------------" << endl;

    vector<int> nums2 = {3,2,3,1,2,4,5,5,6};
    int k2 = 4;
    cout << "测试用例2 数组:"; printVector(nums2);
    cout << "第 " << k2 << " 大元素:" << sol.findKthLargest(nums2, k2) << endl;
    cout << "-------------------------" << endl;

    vector<int> nums3 = {1};
    int k3 = 1;
    cout << "测试用例3 数组:"; printVector(nums3);
    cout << "第 " << k3 << " 大元素:" << sol.findKthLargest(nums3, k3) << endl;
    cout << "-------------------------" << endl;

    vector<int> nums4 = {7,7,7,7};
    int k4 = 2;
    cout << "测试用例4 数组:"; printVector(nums4);
    cout << "第 " << k4 << " 大元素:" << sol.findKthLargest(nums4, k4) << endl;
    cout << "-------------------------" << endl;

    vector<int> nums5 = {9,8,7,6,5};
    int k5 = 3;
    cout << "测试用例5 数组:"; printVector(nums5);
    cout << "第 " << k5 << " 大元素:" << sol.findKthLargest(nums5, k5) << endl;
    cout << "-------------------------" << endl;

    vector<int> nums6 = {-1,-2,-3,-4,-5};
    int k6 = 2;
    cout << "测试用例6 数组:"; printVector(nums6);
    cout << "第 " << k6 << " 大元素:" << sol.findKthLargest(nums6, k6) << endl;

    return 0;
}

5.库存管理|||(OJ题)


算法思路:解法(快速选择算法):

在快排中,当我们把数组分成三块之后:[l, left] [left + 1, right - 1] [right, r] ,我们可以通过计算每⼀个区间内元素的个数,进⽽推断出最⼩的cnt个库存余量在哪些区间⾥⾯.

那么我们可以直接去相应的区间继续划分数组即可.

把数组分成三部分

  • 左区 [l, left]:小于 基准值 key
  • 中区 [left+1, right-1]:等于 基准值 key
  • 右区 [right, r]:大于 基准值 key

快速选择思想

我们只需要最小的 cnt 个数 ,不需要排序整个数组,只递归处理目标所在的区间,大幅减少计算量.
🔥最核心:分情况判断逻辑

我们的目标:找最小的 cnt 个数 ,优先选左区(最小) ,再选中区 ,最后选右区:

  1. a > cnt
    左区的数已经够 cnt 个了,最小的 cnt 个数全在左区,递归处理左区;
  2. a + b >= cnt
    左区+中区的数刚好够 cnt 个,无需继续递归,直接返回;
  3. a + b < cnt
    左区+中区不够,需要从右区 补充,递归右区,同时 cnt 减去左区+中区的总数量.

核心代码

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>& stock, int l, int r, int cnt) 
    {
        if (l >= r)
            return;
        //1.随机选择⼀个基准元素 + 数组分三块
        int key = getRandom(stock, l, r);
        int left = l - 1, right = r + 1, i = l;
        while (i < right) 
        {
            if (stock[i] < key)
                swap(stock[++left], stock[i++]);
            else if (stock[i] == key)
                i++;
            else
                swap(stock[--right], stock[i]);
        }
        //[l, left][left + 1, right - 1] [right, r]
        //2.分情况讨论
        int a = left - l + 1, b = right - left - 1;
        if (a > cnt)
            qsort(stock, l, left, cnt);
        else if (a + b >= cnt)
            return;
        else
            qsort(stock, right, r, cnt - a - b);
    }
    int getRandom(vector<int>& stock, int l, int r) 
    {
        return stock[rand() % (r - l + 1) + l];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

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>& stock, int l, int r, int cnt)
    {
        if (l >= r)
            return;
        //1.随机选择⼀个基准元素 + 数组分三块
        int key = getRandom(stock, l, r);
        int left = l - 1, right = r + 1, i = l;
        while (i < right)
        {
            if (stock[i] < key)
                swap(stock[++left], stock[i++]);
            else if (stock[i] == key)
                i++;
            else
                swap(stock[--right], stock[i]);
        }
        //[l, left][left + 1, right - 1] [right, r]
        //2.分情况讨论
        int a = left - l + 1, b = right - left - 1;
        if (a > cnt)
            qsort(stock, l, left, cnt);
        else if (a + b >= cnt)
            return;
        else
            qsort(stock, right, r, cnt - a - b);
    }

    int getRandom(vector<int>& stock, int l, int r)
    {
        return stock[rand() % (r - l + 1) + l];
    }
};

void printVector(const vector<int>& nums) {
    for (int num : nums) {
        cout << num << " ";
    }
    cout << endl;
}

int main() {
    Solution sol;

    vector<int> stock1 = {3,2,1,5,6,4};
    int cnt1 = 3;
    cout << "测试用例1 原数组:"; printVector(stock1);
    cout << "最小的 " << cnt1 << " 个数:";
    printVector(sol.inventoryManagement(stock1, cnt1));
    cout << "-------------------------" << endl;

    vector<int> stock2 = {2,1,2,1,3,2};
    int cnt2 = 3;
    cout << "测试用例2 原数组:"; printVector(stock2);
    cout << "最小的 " << cnt2 << " 个数:";
    printVector(sol.inventoryManagement(stock2, cnt2));
    cout << "-------------------------" << endl;

    vector<int> stock3 = {10};
    int cnt3 = 1;
    cout << "测试用例3 原数组:"; printVector(stock3);
    cout << "最小的 " << cnt3 << " 个数:";
    printVector(sol.inventoryManagement(stock3, cnt3));
    cout << "-------------------------" << endl;

    vector<int> stock4 = {5,3,9};
    int cnt4 = 0;
    cout << "测试用例4 原数组:"; printVector(stock4);
    cout << "最小的 " << cnt4 << " 个数:";
    printVector(sol.inventoryManagement(stock4, cnt4));
    cout << "-------------------------" << endl;

    vector<int> stock5 = {7,7,7,7};
    int cnt5 = 2;
    cout << "测试用例5 原数组:"; printVector(stock5);
    cout << "最小的 " << cnt5 << " 个数:";
    printVector(sol.inventoryManagement(stock5, cnt5));
    cout << "-------------------------" << endl;

    vector<int> stock6 = {-5, 2, -3, 0, 1};
    int cnt6 = 2;
    cout << "测试用例6 原数组:"; printVector(stock6);
    cout << "最小的 " << cnt6 << " 个数:";
    printVector(sol.inventoryManagement(stock6, cnt6));
    cout << "-------------------------" << endl;

    vector<int> stock7 = {9,7,5,3,1};
    int cnt7 = 4;
    cout << "测试用例7 原数组:"; printVector(stock7);
    cout << "最小的 " << cnt7 << " 个数:";
    printVector(sol.inventoryManagement(stock7, cnt7));

    return 0;
}

6.排序数组(OJ题)--用归并排序方法


算法思路:解法(归并排序):

归并排序的流程充分的体现了分⽽治之的思想,⼤体过程分为两步:

◦ 分:将数组⼀分为⼆为两部分,⼀直分解到数组的⻓度为1,使整个数组的排序过程被分为左半部分排序+右半部分排序;

◦ 治:将两个较短的有序数组合并成⼀个⻓的有序数组,⼀直合并到最初的⻓度.
🔥关键步骤说明

  • 递归终止:当区间长度为1(left == right)时,元素天然有序,无需排序.
  • 划分区间:mid = (left + right) >> 1:用位运算计算中点,效率高于除法,将数组一分为二.
  • 递归排序:先递归把左、右子区间都变成有序数组,这是分治的治.
  • 双指针合并:cur1:指向左有序区间的起点;cur2:指向右有序区间的起点;比较两个指针指向的元素,小的优先放入辅助数组,指针后移.
  • 补齐剩余元素:其中一个子区间遍历完成后,另一个区间的剩余元素直接追加到辅助数组(本身已有序).
  • 数据还原:将辅助数组中排好序的数据,拷贝回原数组的对应位置,完成合并.

核心代码

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;
        //1.选择中间点划分区间
        int mid = (left + right) >> 1;
        //[left, mid] [mid + 1, right]
        //2.把左右区间排序
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);
        //3.合并两个有序数组
        int cur1 = left, cur2 = mid + 1, 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++];
        //4.还原
        for (int i = left; i <= right; i++)
            nums[i] = tmp[i - left];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

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;
        //1.选择中间点划分区间(等价于 (left+right)/2,避免溢出)
        int mid = (left + right) >> 1;
        //2.递归排序左右两个子区间
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);
        //3.合并两个有序数组
        int cur1 = left, cur2 = mid + 1, 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++];
        //4.将辅助数组的结果拷贝回原数组
        for (int i = left; i <= right; i++)
            nums[i] = tmp[i - left];
    }
};

void printVector(const vector<int>& nums) {
    for (int num : nums) {
        cout << num << " ";
    }
    cout << endl;
}

int main() {
    Solution sol;
    
    vector<int> nums1 = {5, 2, 3, 1, 4};
    cout << "测试用例1 排序前:"; printVector(nums1);
    cout << "测试用例1 排序后:"; printVector(sol.sortArray(nums1));
    cout << "-------------------------" << endl;
    
    vector<int> nums2 = {2, 0, 2, 1, 1, 0};
    cout << "测试用例2 排序前:"; printVector(nums2);
    cout << "测试用例2 排序后:"; printVector(sol.sortArray(nums2));
    cout << "-------------------------" << endl;
    
    vector<int> nums3 = {1, 2, 3, 4, 5};
    cout << "测试用例3 排序前:"; printVector(nums3);
    cout << "测试用例3 排序后:"; printVector(sol.sortArray(nums3));
    cout << "-------------------------" << endl;
    
    vector<int> nums4 = {9, 7, 5, 3, 1};
    cout << "测试用例4 排序前:"; printVector(nums4);
    cout << "测试用例4 排序后:"; printVector(sol.sortArray(nums4));
    cout << "-------------------------" << endl;
    
    vector<int> nums5 = {-3, 5, -1, 0, -5, 2};
    cout << "测试用例5 排序前:"; printVector(nums5);
    cout << "测试用例5 排序后:"; printVector(sol.sortArray(nums5));
    cout << "-------------------------" << endl;
    
    vector<int> nums6 = {10};
    cout << "测试用例6 排序前:"; printVector(nums6);
    cout << "测试用例6 排序后:"; printVector(sol.sortArray(nums6));
    cout << "-------------------------" << endl;
    
    vector<int> nums7;
    cout << "测试用例7 排序前:空数组" << endl;
    cout << "测试用例7 排序后:"; printVector(sol.sortArray(nums7));

    return 0;
}

7.交易逆序对的总数(OJ题)


算法思路:解法(利用归并排序的过程---分治):

用归并排序求逆序数是很经典的方法,主要就是在归并排序的合并过程中统计出逆序对的数量,也就是在合并两个有序序列的过程中,能够快速求出逆序对的数量.我们将这个问题分解成几个小问题,逐一破解这道题.注意:默认都是升序,如果掌握升序的话,降序的归并过程也是可以解决问题的.
先解决第一个问题,为什么可以利用归并排序?

如果我们将数组从中间划分成两个部分,那么我们可以将逆序对产生的方式划分成三组:

  • 逆序对中两个元素:全部从左数组中选择
  • 逆序对中两个元素:全部从右数组中选择
  • 逆序对中两个元素:一个选左数组另一个选右数组

根据排列组合的分类相加原理,三种情况下产生的逆序对的总和,正好等于总的逆序对数量.

而这个思路正好匹配归并排序的过程:

  • 先排序左数组;
  • 再排序右数组;
  • 左数组和右数组合二为一.

因此,我们可以利用归并排序的过程,先求出左半数组中逆序对的数量,再求出右半数组中逆序对的数量,最后求出一个选择左边,另一个选择右边情况下逆序对的数量,三者相加即可.

解决第二个问题,为什么要这么做?

在归并排序合并的过程中,我们得到的是两个有序的数组.我们是可以利用数组的有序性,快速统计出逆序对的数量,而不是将所有情况都枚举出来.最核心的问题,如何在合并两个有序数组的过程中,统计出逆序对的数量?

合并两个有序序列时求逆序对的方法有两种:

1.快速统计出某个数前面有多少个数比它大;

2.快速统计出某个数后面有多少个数比它小;

方法一:快速统计出某个数前面有多少个数比它大

通过一个示例来演示方法一:

假定已经有两个已经有序的序列以及辅助数组 left = [5, 7, 9] right = [4, 5, 8] help = [],通过合并两个有序数组的过程,来求得逆序对的数量:

规定如下定义来叙述过程:

  • cur1 遍历 left 数组,cur2 遍历 right 数组
  • ret 记录逆序对的数量

第一轮循环:
left[cur1] > right[cur2],由于两个数组都是升序的,那么我们可以断定,此刻 left 数组中[cur1, 2] 区间内的 3个元素均可与 right[cur2] 的元素构成逆序对,因此可以累加逆序对的数量 ret += 3,并且将 right[cur2] 加入到辅助数组中,cur2++ 遍历下一个元素.

第一轮循环结束后: left = [5, 7, 9] right = [x, 5, 8] help = [4] ret = 3 cur1 = 0 cur2 = 1

第二轮循环:
left[cur1] == right[cur2],因为 right[cur2] 可能与 left 数组中往后的元素构成逆序对,因此我们需要将 left[cur1] 加入到辅助数组中去,此时没有产生逆序对,不更新 ret.

第二轮循环结束后: left = [x, 7, 9] right = [x, 5, 8] help = [4, 5] ret = 3 cur1 = 1 cur2 = 1

第三轮循环:
left[cur1] > right[cur2],与第一轮循环相同,此刻 left 数组中 [cur1, 2] 区间内的 2 个元素均可与 right[cur2] 的元素构成逆序对,更新 ret 的值为 ret += 2,并且将 right[cur2] 加入到辅助数组中去,cur2++ 遍历下一个元素.

第三轮循环结束后: left = [x, 7, 9] right = [x, x, 8] help = [4, 5, 5] ret = 5 cur1 = 1 cur2 = 2

第四轮循环:
left[cur1] < right[cur2],由于两个数组都是升序的,因此我们可以确定 left[cur1]right 数组中的所有元素都要小.left[cur1] 这个元素是不可能与 right 数组中的元素构成逆序对.因此,大胆的将 left[cur1] 这个元素加入到辅助数组中去,不更新 ret 的值.

第四轮循环结束后: left = [x, x, 9] right = [x, x, 8] help = [4, 5, 5, 7] ret = 5 cur1 = 2 cur2 = 2

第五轮循环:
left[cur1] > right[cur2],与第一、第三轮循环相同.此时 left 数组内的 1 个元素能与 right[cur2] 构成逆序对,更新 ret 的值,并且将 right[cur2] 加入到辅助数组中去.

第五轮循环结束后: left = [x, x, 9] right = [x, x, x] help = [4, 5, 5, 7, 8] ret = 6 cur1 = 2 cur2 = 2

处理剩余元素:

  • 如果是左边出现剩余,说明左边剩下的所有元素都是比右边元素大的,但是它们都是已经被计算过的(我们以右边的元素为基准的),因此不会产生逆序对,仅需归并排序即可.
  • 如果是右边出现剩余,说明右边剩下的元素都是比左边大的,不符合逆序对的定义,因此也不需要处理,仅需归并排序即可.

整个过程只需将两个数组遍历一遍即可,时间复杂度为 O(N).

由上述过程我们可以得出方法一统计逆序对的关键点:在合并有序数组的时候,遇到左数组当前元素 > 右数组当前元素时,我们可以通过计算左数组中剩余元素的长度,就可快速求出右数组当前元素前面有多少个数比它大,对比解法一中一个一个枚举逆序对效率快了许多.

方法二:快速统计出某个数后面有多少个数比它小

依旧通过一个示例来演示方法二:

假定已经有两个已经有序的序列以及辅助数组 left = [5, 7, 9] right = [4, 5, 8] help = [],通过合并两个有序数组的过程,来求得逆序对的数量:

规定如下定义来叙述过程:

  • cur1 遍历 left 数组,cur2 遍历 right 数组
  • ret 记录逆序对的数量

第一轮循环:
left[cur1] > right[cur2],先不要着急统计,因为我们要找的是当前元素后面有多少比它小的,这里虽然出现了一个,但是 right 数组中依旧还可能有其余比它小的.因此此时仅将 right[cur2] 加入到辅助数组中去,并且将 cur2++.

第一轮循环结束后: left = [5, 7, 9] right = [x, 5, 8] help = [4] ret = 0 cur1 = 0 cur2 = 1

第二轮循环:
left[cur1] == right[cur2],由于两个数组都是升序,这个时候对于元素 left[cur1] 来说,我们已经可以断定 right 数组中 [0, cur2) 左闭右开区间上的元素都是比它小的.因此此时可以统计逆序对的数量 ret += cur2 - 0,并且将 left[cur1] 放入到辅助数组中去,cur1++ 遍历下一个元素.

第二轮循环结束后: left = [x, 7, 9] right = [x, 5, 8] help = [4, 5] ret = 1 cur1 = 1 cur2 = 1

第三轮循环:
left[cur1] > right[cur2],与第一轮循环相同,直接将 right[cur2] 加入到辅助数组中去,cur2++ 遍历下一个元素.

第三轮循环结束后: left = [x, 7, 9] right = [x, x, 8] help = [4, 5, 5] ret = 1 cur1 = 1 cur2 = 2

第四轮循环:
left[cur1] < right[cur2],由于两个数组都是升序的,这个时候对于元素 left[cur1] 来说,我们依旧已经可以断定 right 数组中 [0, cur2) 左闭右开区间上的元素都是比它小的.因此此时可以统计逆序对的数量 ret += cur2 - 0,并且将 left[cur1] 放入到辅助数组中去,cur1++ 遍历下一个元素.

第四轮循环结束后: left = [9] right = [8] help = [4, 5, 5, 7] ret = 3 cur1 = 2 cur2 = 2

第五轮循环:
left[cur1] > right[cur2],与第一、第三轮循环相同.直接将 right[cur2] 加入到辅助数组中去,cur2++ 遍历下一个元素.

第五轮循环结束后: left = [x, x, 9] right = [x, x, x] help = [4, 5, 5, 7, 8] ret = 3 cur1 = 2 cur2 = 2

处理剩余元素:

  • 如果是左边出现剩余,说明左边剩下的所有元素都是比右边元素大的,但是相比较于方法一,逆序对的数量是没有统计过的.因此,我们需要统计 ret 的值:
    • 设左边数组剩余元素的个数为 leave
    • ret += leave * (cur2 - 0)

对于本题来说,处理剩余元素的时候,left 数组剩余 1 个元素,cur2 - 0 = 3,因此 ret 需要类加上3,结果为6.与方法一求得的结果相同.

  • 如果是右边出现剩余,说明右边剩下的元素都是比左边大的,不符合逆序对的定义,因此也不需要处理,仅需归并排序即可.

整个过程只需将两个数组遍历一遍即可,时间复杂度依旧为 O(N).

由上述过程我们可以得出方法二统计逆序对的关键点:在合并有序数组的时候,遇到左数组当前元素 <= 右数组当前元素时,我们可以通过计算右数组已经遍历过的元素的长度,快速求出左数组当前元素后面有多少个数比它大.但是需要注意的是,在处理剩余元素的时候,方法二还需要统计逆序对的数量.

核心代码

cpp 复制代码
//升序的版本
class Solution 
{
    int tmp[50010];
public:
    int reversePairs(vector<int>& nums) 
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }
    int mergeSort(vector<int>& nums, int left, int right) 
    {
        if (left >= right)
            return 0;
        int ret = 0;
        //1.找中间点,将数组分成两部分
        int mid = (left + right) >> 1;
        //[left, mid][mid + 1, right]
        //2.左边的个数 + 排序 + 右边的个数 + 排序
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        //3.⼀左⼀右的个数
        int cur1 = left, cur2 = mid + 1, 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++];
            }
        }
        //4.处理⼀下排序
        while (cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while (cur2 <= right)
            tmp[i++] = nums[cur2++];
        for (int j = left; j <= right; j++)
            nums[j] = tmp[j - left];
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution
{
    int tmp[50010]; 
public:
    int reversePairs(vector<int>& nums)
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }

    int mergeSort(vector<int>& nums, int left, int right)
    {
        if (left >= right)
            return 0;
        int ret = 0;
        //1.找中间点,将数组分成两部分
        int mid = (left + right) >> 1;
        //2.递归统计左、右子数组的逆序对数量并排序
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        //3.统计跨左右区间的逆序对数量 + 合并有序数组
        int cur1 = left, cur2 = mid + 1, 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++];
            }
        }
        //4.处理剩余元素,完成归并排序
        while (cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while (cur2 <= right)
            tmp[i++] = nums[cur2++];
        //将辅助数组的结果拷贝回原数组
        for (int j = left; j <= right; j++)
            nums[j] = tmp[j - left];
        return ret;
    }
};

int main() {
    Solution sol;
    
    vector<int> nums1 = {7, 5, 6, 4};
    cout << "测试用例1 数组:7 5 6 4" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums1) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums2;
    cout << "测试用例2 数组:空数组" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums2) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums3 = {1};
    cout << "测试用例3 数组:1" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums3) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums4 = {1, 2, 3, 4};
    cout << "测试用例4 数组:1 2 3 4" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums4) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums5 = {4, 3, 2, 1};
    cout << "测试用例5 数组:4 3 2 1" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums5) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums6 = {2, 2, 1};
    cout << "测试用例6 数组:2 2 1" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums6) << endl;

    return 0;
}


核心代码

cpp 复制代码
//降序的版本
class Solution 
{
    int tmp[50010];
public:
    int reversePairs(vector<int>& nums) 
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }
    int mergeSort(vector<int>& nums, int left, int right) 
    {
        if (left >= right)
            return 0;
        int ret = 0;
        //1.找中间点,将数组分成两部分
        int mid = (left + right) >> 1;
        //[left, mid][mid + 1, right]
        //2.左边的个数 + 排序 + 右边的个数 + 排序
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        //3.⼀左⼀右的个数
        int cur1 = left, cur2 = mid + 1, i = 0;
        while (cur1 <= mid && cur2 <= right) //降序的版本
        {
            if (nums[cur1] <= nums[cur2]) 
            {
                tmp[i++] = nums[cur2++];
            } else {
                ret += right - cur2 + 1;
                tmp[i++] = nums[cur1++];
            }
        }
        //4.处理⼀下排序
        while (cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while (cur2 <= right)
            tmp[i++] = nums[cur2++];
        for (int j = left; j <= right; j++)
            nums[j] = tmp[j - left];
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution
{
    int tmp[50010]; 
public:
    int reversePairs(vector<int>& nums)
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }
    int mergeSort(vector<int>& nums, int left, int right)
    {
        if (left >= right)
            return 0;
        int ret = 0;
        //1.找中间点,将数组分成两部分
        int mid = (left + right) >> 1;
        //[left, mid][mid + 1, right]
        //2.递归统计左右区间逆序对并排序
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        //3.统计跨区间逆序对(降序归并)
        int cur1 = left, cur2 = mid + 1, i = 0;
        while (cur1 <= mid && cur2 <= right) //降序版本
        {
            if (nums[cur1] <= nums[cur2])
            {
                tmp[i++] = nums[cur2++];
            } else {
                ret += right - cur2 + 1;
                tmp[i++] = nums[cur1++];
            }
        }
        //4.处理剩余元素,完成归并排序
        while (cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while (cur2 <= right)
            tmp[i++] = nums[cur2++];
        //拷贝回原数组
        for (int j = left; j <= right; j++)
            nums[j] = tmp[j - left];
        return ret;
    }
};

int main() {
    Solution sol;
    
    vector<int> nums1 = {7, 5, 6, 4};
    cout << "测试用例1 数组:7 5 6 4" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums1) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums2;
    cout << "测试用例2 数组:空数组" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums2) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums3 = {1};
    cout << "测试用例3 数组:1" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums3) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums4 = {1, 2, 3, 4};
    cout << "测试用例4 数组:1 2 3 4" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums4) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums5 = {4, 3, 2, 1};
    cout << "测试用例5 数组:4 3 2 1" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums5) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums6 = {2, 2, 1};
    cout << "测试用例6 数组:2 2 1" << endl;
    cout << "逆序对数量:" << sol.reversePairs(nums6) << endl;

    return 0;
}

8.计算右侧小于当前元素的个数(OJ题)


算法思路:解法(归并排序):

这一道题的解法与求数组中的逆序对 的解法是类似的,但是这一道题要求的不是求总的个数,而是要返回一个数组,记录每一个元素的右边有多少个元素比自己小 .但是在我们归并排序的过程中,元素的下标是会跟着变化的,因此我们需要一个辅助数组,来将数组元素和对应的下标绑定在一起归并,也就是在归并元素的时候,顺势将下标也转移到对应的位置上.由于我们要快速统计出某一个元素后面有多少个比它小的,因此我们可以利用求逆序对的第二种方法 .
算法流程:

• 创建两个全局的数组:

  • vector<int> index:记录下标
  • vector<int> ret:记录结果
    index 用来与原数组中对应位置的元素绑定,ret 用来记录每个位置统计出来的逆序对的个数.

countSmaller() 主函数:

①计算 nums 数组的大小为 n;

②初始化定义的两个全局的数组:

(1)为两个数组开辟大小为 n 的空间

(2)index 初始化为数组下标;

(3)ret 初始化为 0

③调用 mergeSort() 函数,并且返回 ret 结果数组.

void mergeSort( vector<int>& nums, int left, int right ) 函数:

函数设计:通过修改全局的数组 ret,统计出每一个位置对应的逆序对的数量,并且排序;

无需返回值,因为直接对全局变量修改,当函数结束的时候,全局变量已经被修改成最后的结果.

mergeSort() 函数流程:

①定义递归出口: left >= right 时,直接返回;

②划分区间: 根据中点 mid,将区间划分为 [left, mid][mid + 1, right];

③统计左右两个区间逆序对的数量:

(1)统计左边区间 [left, mid] 中每个元素对应的逆序对的数量到 ret 数组中,并排序;

(2)统计右边区间 [mid + 1, right] 中每个元素对应的逆序对的数量到 ret 数组中,并排序.

④合并左右两个有序区间,并且统计出逆序对的数量:

(1)创建两个大小为 right - left + 1 大小的辅助数组:
numsTmp: 排序用的辅助数组;
indexTmp: 处理下标用的辅助数组。

(2)初始化遍历数组的指针: cur1 = left (遍历左半部分数组)、cur2 = mid + 1 (遍历右半边数组)、dest = 0 (遍历辅助数组)、curRet (记录合并时产生的逆序对的数量);

(3)循环合并区间:

nums[cur1] <= nums[cur2] 时:

◦ 说明此时 [mid + 1, cur2) 之间的元素都是小于 nums[cur1] 的,需要累加到 ret 数组的 index[cur1] 位置上(因为 index 存储的是元素对应位置在原数组中的下标)

◦ 归并排序:不仅要将数据放在对应的位置上,也要将数据对应的坐标也放在对应的位置上,使数据与原始的下标绑定在一起移动.

nums[cur1] > nums[cur2] 时,无需统计,直接归并,注意 index 也要跟着归并.

(4)处理归并排序中剩余的元素;

当左边有剩余的时候,还需要统计逆序对的数量 ;

当右边还有剩余的时候,无需统计,直接归并.

(5)将辅助数组的内容替换到原数组中去;

核心代码

cpp 复制代码
class Solution 
{
    vector<int> ret;
    vector<int> index; //记录 nums 中当前元素的原始下标
    int tmpNums[500010];
    int tmpIndex[500010];
public:
    vector<int> countSmaller(vector<int>& nums) 
    {
        int n = nums.size();
        ret.resize(n);
        index.resize(n);
        //初始化⼀下 index 数组
        for (int i = 0; i < n; i++)
            index[i] = i;
        mergeSort(nums, 0, n - 1);
        return ret;
    }
    void mergeSort(vector<int>& nums, int left, int right) 
    {
        if (left >= right)
            return;
        //1.根据中间元素,划分区间
        int mid = (left + right) >> 1;
        //[left, mid] [mid + 1, right]
        //2.先处理左右两部分
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);
        //3.处理⼀左⼀右的情况
        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++];
            }
        }
        //4.处理剩下的排序过程
        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];
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution
{
    vector<int> ret;
    vector<int> index; //记录 nums 中当前元素的原始下标
    int tmpNums[500010];
    int tmpIndex[500010];
public:
    vector<int> countSmaller(vector<int>& nums)
    {
        int n = nums.size();
        ret.resize(n);
        index.resize(n);
        //初始化 index 数组,绑定元素与原始下标
        for (int i = 0; i < n; i++)
            index[i] = i;
        mergeSort(nums, 0, n - 1);
        return ret;
    }
    void mergeSort(vector<int>& nums, int left, int right)
    {
        if (left >= right)
            return;
        //1.根据中间元素,划分区间
        int mid = (left + right) >> 1;
        //[left, mid] [mid + 1, right]
        //2.先处理左右两部分
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);
        //3.处理一左一右的情况
        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++];
            }
        }
        //4.处理剩下的排序过程
        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];
        }
    }
};

void printVector(const vector<int>& nums) {
    for (int x : nums) {
        cout << x << " ";
    }
    cout << endl;
}

int main() {
    Solution sol;
    
    vector<int> nums1 = {5, 2, 6, 1};
    cout << "测试用例1 原数组:"; printVector(nums1);
    cout << "右侧小于当前元素的个数:"; printVector(sol.countSmaller(nums1));
    cout << "-------------------------" << endl;
    
    vector<int> nums2;
    cout << "测试用例2 原数组:空数组" << endl;
    cout << "结果:"; printVector(sol.countSmaller(nums2));
    cout << "-------------------------" << endl;
    
    vector<int> nums3 = {10};
    cout << "测试用例3 原数组:"; printVector(nums3);
    cout << "结果:"; printVector(sol.countSmaller(nums3));
    cout << "-------------------------" << endl;
    
    vector<int> nums4 = {1, 2, 3, 4};
    cout << "测试用例4 原数组:"; printVector(nums4);
    cout << "结果:"; printVector(sol.countSmaller(nums4));
    cout << "-------------------------" << endl;
    
    vector<int> nums5 = {4, 3, 2, 1};
    cout << "测试用例5 原数组:"; printVector(nums5);
    cout << "结果:"; printVector(sol.countSmaller(nums5));
    cout << "-------------------------" << endl;
    
    vector<int> nums6 = {2, 2, 1};
    cout << "测试用例6 原数组:"; printVector(nums6);
    cout << "结果:"; printVector(sol.countSmaller(nums6));

    return 0;
}

9.翻转对(OJ题)


算法思路:解法(归并排序):

大思路与求逆序对的思路一样,就是利用归并排序的思想,将求整个数组的翻转对的数量,转换成三部分:左半区间翻转对的数量 ,右半区间翻转对的数量 ,一左一右选择时翻转对的数量 .重点就是在合并区间过程中,如何计算出翻转对的数量.与上个问题不同的是,上一道题我们可以一边合并一边计算,但是这道题要求的是左边元素大于右边元素的两倍 ,如果我们直接合并的话,是无法快速计算出翻转对的数量的.例如 left = [4, 5, 6] right = [3, 4, 5] 时,如果是归并排序的话,我们需要计算 left 数组中有多少个能与 3 组成翻转对.但是我们要遍历到最后一个元素 6 才能确定,时间复杂度较高.因此我们需要在归并排序之前完成翻转对的统计 .

下面依旧以一个示例来模仿两个有序序列如何快速求出翻转对的过程:

假定已经有两个已经有序的序列 left = [4, 5, 6] right = [1, 2, 3].

用两个指针 cur1cur2 遍历两个数组.

  • 对于任意给定的 left[cur1] 而言,我们不断地向右移动 cur2,直到 left[cur1] <= 2 * right[cur2].此时对于 right 数组而言,cur2 之前的元素全部都可以与 left[cur1] 构成翻转对.
  • 随后,我们再将 cur1 向右移动一个单位,此时 cur2 指针并不需要回退(因为 left 数组是升序的),依旧往右移动直到 left[cur1] <= 2 * right[cur2].不断重复这样的过程,就能够求出所有左右端点分别位于两个子数组的翻转对数目.由于两个指针最后都是不回退 地扫描到数组的结尾,因此两个有序序列求出翻转对的时间复杂度是 O(N).

综上所述,我们可以利用归并排序的过程,将求一个数组的翻转对转换成求:左数组的翻转对数量 + 右数组中翻转对的数量 + 左右数组合并时翻转对的数量.

核心代码

cpp 复制代码
//降序版本
class Solution 
{
    int tmp[50010];

public:
    int reversePairs(vector<int>& nums) 
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }
    int mergeSort(vector<int>& nums, int left, int right) 
    {
        if (left >= right)
            return 0;
        int ret = 0;
        //1.先根据中间元素划分区间
        int mid = (left + right) >> 1;
        //[left, mid] [mid + 1, right]
        //2.先计算左右两侧的翻转对
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        //3.先计算翻转对的数量
        int cur1 = left, cur2 = mid + 1, i = left;
        while (cur1 <= mid) //降序的情况
        {
            while (cur2 <= right && nums[cur2] >= nums[cur1] / 2.0)
                cur2++;
            if (cur2 > right)
                break;
            ret += right - cur2 + 1;
            cur1++;
        }
        //4.合并两个有序数组
        cur1 = left, cur2 = mid + 1;
        while (cur1 <= mid && cur2 <= right)
            tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];
        while (cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while (cur2 <= right)
            tmp[i++] = nums[cur2++];
        for (int j = left; j <= right; j++)
            nums[j] = tmp[j];
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution
{
    int tmp[50010];

public:
    int reversePairs(vector<int>& nums)
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }
    int mergeSort(vector<int>& nums, int left, int right)
    {
        if (left >= right)
            return 0;
        int ret = 0;
        //1.先根据中间元素划分区间
        int mid = (left + right) >> 1;
        //[left, mid] [mid + 1, right]
        //2.先计算左右两侧的翻转对
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        //3.先计算翻转对的数量
        int cur1 = left, cur2 = mid + 1, i = left;
        while (cur1 <= mid) //降序的情况
        {
            while (cur2 <= right && nums[cur2] >= nums[cur1] / 2.0)
                cur2++;
            if (cur2 > right)
                break;
            ret += right - cur2 + 1;
            cur1++;
        }
        //4.合并两个有序数组
        cur1 = left, cur2 = mid + 1;
        while (cur1 <= mid && cur2 <= right)
            tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];
        while (cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while (cur2 <= right)
            tmp[i++] = nums[cur2++];
        for (int j = left; j <= right; j++)
            nums[j] = tmp[j];
        return ret;
    }
};

int main() {
    Solution sol;
    
    vector<int> nums1 = {1,3,2,3,1};
    cout << "测试用例1 数组:1 3 2 3 1" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums1) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums2 = {2,4,3,5,1};
    cout << "测试用例2 数组:2 4 3 5 1" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums2) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums3;
    cout << "测试用例3 数组:空数组" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums3) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums4 = {5};
    cout << "测试用例4 数组:5" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums4) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums5 = {1,2,3,4};
    cout << "测试用例5 数组:1 2 3 4" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums5) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums6 = {5,4,3,2,1};
    cout << "测试用例6 数组:5 4 3 2 1" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums6) << endl;

    return 0;
}

核心代码

cpp 复制代码
//升序版本
class Solution 
{
    int tmp[50010];
public:
    int reversePairs(vector<int>& nums) 
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }
    int mergeSort(vector<int>& nums, int left, int right) 
    {
        if (left >= right)
            return 0;
        int ret = 0;
        //1.先根据中间元素划分区间
        int mid = (left + right) >> 1;
        //[left, mid] [mid + 1, right]
        //2.先计算左右两侧的翻转对
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        //3.先计算翻转对的数量
        int cur1 = left, cur2 = mid + 1, i = left;
        while (cur2 <= right) //升序的情况
        {
            while (cur1 <= mid && nums[cur2] >= nums[cur1] / 2.0)
                cur1++;
            if (cur1 > mid)
                break;
            ret += mid - cur1 + 1;
            cur2++;
        }
        //4.合并两个有序数组
        cur1 = left, cur2 = mid + 1;
        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 j = left; j <= right; j++)
            nums[j] = tmp[j];
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution
{
    int tmp[50010];
public:
    int reversePairs(vector<int>& nums)
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }
    int mergeSort(vector<int>& nums, int left, int right)
    {
        if (left >= right)
            return 0;
        int ret = 0;
        //1.先根据中间元素划分区间
        int mid = (left + right) >> 1;
        //[left, mid] [mid + 1, right]
        //2.先计算左右两侧的翻转对
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        //3.先计算翻转对的数量
        int cur1 = left, cur2 = mid + 1, i = left;
        while (cur2 <= right) //升序的情况
        {
            while (cur1 <= mid && nums[cur2] >= nums[cur1] / 2.0)
                cur1++;
            if (cur1 > mid)
                break;
            ret += mid - cur1 + 1;
            cur2++;
        }
        //4.合并两个有序数组
        cur1 = left, cur2 = mid + 1;
        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 j = left; j <= right; j++)
            nums[j] = tmp[j];
        return ret;
    }
};

int main() {
    Solution sol;
    
    vector<int> nums1 = {1,3,2,3,1};
    cout << "测试用例1 数组:1 3 2 3 1" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums1) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums2 = {2,4,3,5,1};
    cout << "测试用例2 数组:2 4 3 5 1" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums2) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums3;
    cout << "测试用例3 数组:空数组" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums3) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums4 = {10};
    cout << "测试用例4 数组:10" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums4) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums5 = {1,2,3,4,5};
    cout << "测试用例5 数组:1 2 3 4 5" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums5) << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums6 = {5,4,3,2,1};
    cout << "测试用例6 数组:5 4 3 2 1" << endl;
    cout << "翻转对数量:" << sol.reversePairs(nums6) << endl;

    return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容:【优选算法】(实战剖析链表核心操作技巧)


每日心灵鸡汤:昨日之深渊,今日之浅谈!
能将你从谷底拖出来的,从来不是时间,而是你心里的格局和你发自内心的释怀.这个世间从来没有真正的感同身受,你可以消沉,也可以抱怨,甚至可以崩溃,但你一定要懂得自愈.无人问津也好,技不如人也罢,你都要试着沉下心来去做自己的事,而不是让内心的烦躁,焦虑,毁掉你本就不多的热情和定力.你要明白,真正给你撑腰的,是丰富的知识储备,足够的经济基础,持续的情绪稳定,可控的生活节奏,和那个打不败的自己.昨日之深渊,今日之浅谈.路虽远,行则将至;事虽难,做则可成!

相关推荐
shughui2 小时前
Cursor下载安装以及和VSCode的区别(附安装包)
ide·vscode·ai·编辑器·cursor
adam_life2 小时前
A*算法——# P1379 八数码难题
算法·优先队列·a星算法·最优启发式搜索·哈希标记·启发式函数·已走步数+预估距离
南境十里·墨染春水2 小时前
C++传记 this指针 及区分静态非静态成员(面向对象)
开发语言·jvm·c++·笔记
揽月凡尘2 小时前
基于 SWIG 的 C++ Embind 绑定自动化技术说明
开发语言·c++·自动化
Yungoal2 小时前
C++基础项目结构
数据结构·c++·算法
扶摇接北海1762 小时前
洛谷:B4477 [语言月赛 202601] 考场安排
数据结构·c++·算法
qq_571099352 小时前
学习周报三十八
学习
IAUTOMOBILE2 小时前
Qt 入门级开发实践:浅析基于 QTtest 项目的 C++ GUI 编程基础
开发语言·c++·qt
爱丽_2 小时前
AQS 的 `state`:volatile + CAS 如何撑起原子性与可见性
java·前端·算法