首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【算法一周目】分而治之,归并如风:算法中的美学与哲理

【算法一周目】分而治之,归并如风:算法中的美学与哲理

作者头像
HZzzzzLu
发布2025-07-21 08:36:40
发布2025-07-21 08:36:40
1490
举报
文章被收录于专栏:codingcoding

1. 颜色分类

题目链接: 75. 颜色分类

题目描述:

给定一个包含红色、白色和蓝色的数组 nums,共 n 个元素,要求对它们进行原地排序,使得相同颜色的元素相邻,并按红色、白色、蓝色的顺序排列。这里使用整数 012 分别表示红色、白色和蓝色,且不能使用库的排序函数

示例 1:

  • 输入:nums = [2,0,2,1,1,0]
  • 输出:[0,0,1,1,2,2]

示例 2:

  • 输入:nums = [2,0,1]
  • 输出:[0,1,2]

提示:

  • n == nums.length
  • 1 <= n <= 300
  • nums[i] 为 0、1 或 2

解题思路

对于颜色分类问题,我们可以采取三路划分的思想来解决,利用三个指针

left、cur、right

将数组划分成

3

块,分别是

0、1、2

,这样可以实现一次遍历就将颜色快速分类。

具体如下:

  1. 各个区间的含义:
    [0, left]

    :区间内的元素全是0

    [left+1, cur-1]

    :区间内的元素全是1

    [cur, right-1]

    :待处理的区间

    [right, n-1]

    :区间内的元素全是2

  2. 遍历数组:
    cur

    从左往右遍历数组。

    • 若 nums[cur] == 0 ,则
    swap(nums[left+1, cur])

    ,然后

    left++,cur++

    • 若 nums[cur] == 1,直接
    left++

    即可,无需做额外操作。

    • 若 nums[cur] == 2,则
    swap(nums[right-1, cur])

    ,然后

    right++

    ,此时

    cur

    需要保持不变,因为

    right - 1

    位置的元素是待处理区间的元素。

cur == right

时,结束循环,数组被成功划分成

0、1、2

区间。

代码实现

代码语言:javascript
复制
class Solution {
public:
    void sortColors(vector<int>& nums) {
        int n = nums.size();
        int left = -1, right = n, cur = 0;
        while(cur < right)
        {
            if(nums[cur] == 0) swap(nums[++left], nums[cur++]);	//将0交换到0区间的末尾
            else if(nums[cur] == 1) cur++;	//元素为1,直接让cur++
            else if(nums[cur] == 2) swap(nums[--right], nums[cur]);	//将2交换到2区间的起始的前一个位置
        }
    }
};
  • 时间复杂度:
O(n)
  • 空间复杂度:
O(1)

2. 排序数组(快排)

题目链接: 912. 排序数组

题目描述:

给定一个整数数组 nums,请将该数组按升序排列。

你必须在 不使用任何内置函数 的情况下解决问题,时间复杂度为 O(nlog(n)),并且空间复杂度尽可能小。

示例 1:

  • 输入:nums = [5,2,3,1]
  • 输出:[1,2,3,5]

示例 2:

  • 输入:nums = [5,1,1,2,0,0]
  • 输出:[0,0,1,1,2,5]

提示:

  • 1 <= nums.length <= 5 * 104
  • -5 * 104 <= nums[i] <= 5 * 104

解题思路

题目要求实现 O(nlog(n)) 的算法来排序数组,我们这里选择实现快排算法。

这里快排的实现主要的要点是 随机选择基准元素+三路划分

  1. 随机选择基准元素保证了快排在递归深度比较深的情况下时间复杂度依然逼近 O(nlog(n))
  2. 三路划分在处理重复元素较多时效率较高,时间复杂度不会退化。

具体的三路划分思路与75. 颜色分类类似,使用三个指针

left、i、right

来划分区间:

[l, left]

:小于

key

的区间

[left+1, i-1]

等于

key

的区间

[i, right-1]

待处理的区间

[right, r]

大于

key

的区间

代码实现

代码语言:javascript
复制
class Solution {
public:
    //随机选择基准元素
    int getRandom(vector<int>& nums, int l, int r)
    {
        int randomNum = rand();
        return nums[randomNum % (r - l + 1) +l];
    }

    //对区间[l, r]的数组进行快排
    void qsort(vector<int>& nums, int l, int r)
    {
        //当区间无效或者区间元素个数为1时,退出递归
        if(l >= r) return;

        //生成随机选择的key
        int key = getRandom(nums, l, r);
        //初始化三个指针
        int left = l - 1, right = r + 1, cur = l;
        while(cur < right)
        {
            if(nums[cur] < key) swap(nums[++left], nums[cur++]);
            else if(nums[cur] == key) cur++;
            else swap(nums[--right], nums[cur]);
        }

        //分别对左区间和右区间进行快排
        qsort(nums, l, left);
        qsort(nums, right, r);
    }


    vector<int> sortArray(vector<int>& nums) {
        //生成随机数的种子
        srand(time(NULL));
        qsort(nums, 0, nums.size() - 1);
        return nums;
    }
};
  • 时间复杂度: 平均的时间复杂度为
O(nlog(n))
  • 空间复杂度:
O(log(n))

,递归的空间开销,某些极端情况下会退化成

O(n)

3. 数组中的第K个最大元素

题目链接:215. 数组中的第K个最大元素

题目描述:

给定一个整数数组 nums 和一个整数 k,返回数组中第 k个最大的元素。 请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例1:

  • 输入:[3,2,1,5,6,4], k = 2
  • 输出:5

示例2:

  • 输入:[3,2,3,1,2,4,5,5,6], k = 4
  • 输出:4

提示:

  • 1 <= k <= nums.length <= 105
  • -104 <= nums[i] <= 104

解题思路

题目要求的第

K

大元素就是位置为

N-K

元素,我们可以利用快排的思想来构造快速选择算法。

  1. 快排的一次操作(随机选择基准元素+三路划分)让数组分为了三块区间:小于
key

、等于

key

、大于

key

,根据各区间的长度确定第

K

大的元素处在哪个区间,从而再去目标区间寻找第

K

大元素。

  1. 小于
key

、等于

key

、大于

key

的区间长度分别为

a、b、c

c \ge K

,说明第

K

大元素在大于

key

的区间,再去该区间递归寻找。

b + c \ge K

,说明第

K

大元素在等于

key

的区间,直接返回该区间的任意一个元素即可。

b + c > K

,说明第

K

大元素在小于

key

的区间,再去该区间递归寻找第

K-b-c

大的元素。

代码实现

代码语言:javascript
复制
class Solution {
public:

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

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

        //随机选择+三路划分
        int key = GetRandomNum(nums, l, r);
        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]);
        }

        //[l, left] [left + 1, right - 1] [right, r]
        //将各个区间长度与k比较从而确定目标元素在哪个区间
        int b = right - left - 1, c = r - right + 1;
        if(c >= k) return quickSelect(nums, right, r, k);
        else if(b + c >= k) return nums[left + 1];
        else return quickSelect(nums, l, left, k - b -c);

    }


    int findKthLargest(vector<int>& nums, int k) {
        srand(time(NULL));
        return quickSelect(nums, 0, nums.size() - 1, k);
    }
};
  • 时间复杂度: 平均为
O(n)
  • 空间复杂度:
O(log(n))

,递归的额外空间开销


4. 最小的 k 个数

题目链接: 剑指 Offer 40. 最小的k个数

题目描述:

设计一个算法,找出数组中最小的 k 个数。以任意顺序返回这k个数均可。

示例:

  • 输入: arr = [1,3,5,7,2,4,6,8], k = 4
  • 输出: [1,2,3,4]

提示:

  • 0 <= len(arr) <= 100000
  • 0 <= k <= min(100000, len(arr))

解题思路

这道题与上一道题类似,使用快速选择算法。

进行一次快排操作后(随机选择基准元素+三路划分),根据各个区间的长度确定前

K

小的元素在哪个区间,再去该区间递归寻找。

小于

key

、等于

key

、大于

key

的区间长度分别为

a、b、c

a > k

,说明前

K

小元素在小于

key

的区间,再去该区间递归寻找。

a + b \ge K

,说明前

K

小元素刚好在小于

key

的区间或者横跨小于

key

和等于

key

的区间,我们直接返回即可。

a + b < K

,说明前

K

小元素在大于

key

的区间,再去该区间递归寻找前

K - a -b

小的元素。

代码实现

代码语言:javascript
复制
class Solution {
public:

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

    //将数组arr的[l, r]范围的前k个小的元素排列到数组的[l, l + k - 1]位置,不考虑顺序
    void quickSelect(vector<int>& arr, int l, int r, int k)
    {
        if(l >= r) return;

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

        //[l, left] [left + 1, right - 1] [right, r]
        int a = left - l + 1, b = right - left - 1;
        if(a > k) quickSelect(arr, l, left, k);
        else if(a + b >= k) return;
        else quickSelect(arr, right, r, k - a - b);
    }

    vector<int> smallestK(vector<int>& arr, int k) {
        srand(time(NULL));
        //将数组arr前k个小的元素排列到数组的[0, k - 1]位置,不考虑顺序
        quickSelect(arr, 0, arr.size() - 1, k);
        return {arr.begin(), arr.begin() + k};
    }
};
  • 时间复杂度: 平均
O(n)

,随机选择基准值大大降低了效率退化的可能性

  • 空间复杂度:
O(log(n))

,递归的额外空间开销。


5. 排序数组(归并)

题目链接: 912. 排序数组

题目描述:

给定一个整数数组 nums,请将该数组按升序排列。

你必须在 不使用任何内置函数 的情况下解决问题,时间复杂度为 O(nlog(n)),并且空间复杂度尽可能小。

示例 1:

  • 输入:nums = [5,2,3,1]
  • 输出:[1,2,3,5]

示例 2:

  • 输入:nums = [5,1,1,2,0,0]
  • 输出:[0,0,1,1,2,5]

提示:

  • 1 <= nums.length <= 5 * 104
  • -5 * 104 <= nums[i] <= 5 * 104

解题思路

我们直接使用归并排序来排序数组即可。

归并排序的过程:

  1. 计算中点,划分区间。
  2. 递归调用归并排序的函数
mergeSort

对划分的左右区间进行排序。

  1. 将排序好的两个区间合并在提前开好的
tmp

数组,由于是合并成升序,因此小的元素先转移到

tmp

数组。

tmp

数组赋值回原来的数组

nums

的对应位置。

因为

mergeSort

要递归调用,我们需要确定递归的出口,当传入的区间元素个数为

1

个时或者区间无效时,递归结束。

代码实现

代码语言:javascript
复制
class Solution {
public:
    //合并两个有序数组需要一个临时数组
    vector<int> tmp;

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

        //1.求中点划分区间
        int mid = (right - left) / 2 + left;
        //[left, mid] [mid + 1, right]

        //2.递归调用mergeSort对两个区间进行排序
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);

        //3.利用tmp合并两个数组,合并成升序
        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.将tmp数组赋值回nums的对应位置
        for(int j = left; j <= right; j++)
            nums[j] = tmp[j - left];
    }

    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        tmp.resize(n);
        mergeSort(nums, 0, n - 1);
        return nums;
    }
};
  • 时间复杂度:
O(n \times log(n))
  • 空间复杂度: 实际上为
O(n + log(n))

,但是一般忽略掉

log(n)

,所以为

O(n )

6. 数组中的逆序对

题目链接: 剑指 Offer 51. 数组中的逆序对

题目描述:

在一个数组中的两个数字,如果前面的一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

  • 输入:record = [9, 7, 5, 4, 6]
  • 输出:8

限制:

  • 0 <= record.length <= 50000

解题思路

计算数组中的逆序对,可以将数组划分两个区间,分别计算出两个区间对应的逆序对的数量,再计算两个区间缺失的逆序对,最后将得到的数量全部加起来就能得到整个数组中的逆序对的数量,而缺失的逆序对数量主要由于前一个区间的某个元素可能大于后一个区间的某个元素,也能构成逆序对。

因此,这道题我们基于归并排序来解决。

Merge

函数的作用是返回数组

record

[left, right]

范围内的逆序对的个数,并且将数组排序好。

  1. 计算中点对数组
record

进行划分区间,并对得到的区间递归调用

Merge

函数,获取了两个区间的逆序对的数量且对两个区间进行排序(假设为升序)。

  1. 合并两个区间并且统计缺失的逆序对:
    record[cur1] \le record[cur2]

    ,不更新逆序对的数量,将

    record[cur1]

    赋值给

    tmp

    数组,然后

    cur1++

    record[cur1] > record[cur2]

    ,由于两个区间已经是升序,区间

    [cur1, mid]

    内的元素都大于

    record[cur2]

    ,更新逆序对的数量,并且将

    record[cur2]

    赋值给

    tmp

    数组,然后

    cur2++

tmp

赋值回原数组对应的位置,最后返回逆序对的数量。

代码实现

代码语言:javascript
复制
class Solution {
public:

    //用于合并两个数组所用到的临时数组
    int tmp[50005];

    //返回数组record的[left, right]范围内的逆序对的个数,并且将数组排序好
    int Merge(vector<int>& record, int left, int right)
    {
        if(left >= right) return 0;

        //记录逆序对的数量
        int ret = 0;
        //1.计算中点划分区间
        int mid = (right - left) / 2 + left;

        //[left, mid] [mid + 1, right]
        //2.对两个区间排序并计算其逆序对的数量
        ret += Merge(record, left, mid);
        ret += Merge(record, mid + 1, right);

        //3.合并两个有序区间的元素并统计缺失逆序对的数量
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right)
        {
            if(record[cur1] <= record[cur2])
            {
                tmp[i++] = record[cur1++];
            }
            else
            {
                //更新逆序对的数量
                ret += mid - cur1 + 1;
                tmp[i++] = record[cur2++];
            }
        }
        while(cur1 <= mid) tmp[i++] = record[cur1++];
        while(cur2 <= right) tmp[i++] = record[cur2++];

        //4.将tmp数组赋值回原来的数组
        for(int j = left; j <= right; j++)
            record[j] = tmp[j - left];
        return ret; 
    }

    int reversePairs(vector<int>& record) {
        return Merge(record, 0, record.size() - 1);
    }
};
  • 时间复杂度:
O(n \times log(n))
  • 空间复杂度:
O(n )

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

题目链接: 315. 计算右侧小于当前元素的个数

题目描述:

给你一个整数数组 nums ,按要求返回一个新数组 counts。 数组 counts 有如下性质:counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1:

  • 输入:nums = [5,2,6,1]
  • 输出:[2,1,1,0]
  • 解释:
    • 5 的右侧有 2 个更小的元素 (21)
    • 2 的右侧仅有 1 个更小的元素 (1)
    • 6 的右侧有 1 个更小的元素 (1)
    • 1 的右侧有 0 个更小的元素

示例 2:

  • 输入:nums = [-1]
  • 输出:[0]

示例 3:

  • 输入:nums = [-1,-1]
  • 输出:[0,0]

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

解题思路

这道题与上一道题类似,一样基于归并排序去解决。

MergeCount

函数是对数组进行排序,计算每个元素右侧小于当前元素的个数并且记录在返回数组

ret

。注意,这里排的是降序,便于快速统计右侧小于当前元素的个数

  1. 计算中点划分区间,并且对两个区间递归调用
MergeCount

函数

  1. 合并两个有序区间,并且统计第一个区间的元素对应缺失的右侧小于当前元素的个数并且修改对应的 返回数组
ret

问题来了,如何正确去修改

ret

中的值?

因为递归调用 MergeCount 函数,两个区间的元素已经是有序了,相较于原来未排序的数组,每个元素已经已经不是对应原来的下标了,无法得知当前元素在未排序前的下标,从而导致无法修改

ret

数组中右侧小于当前元素的个数

解决方法是创建一个与

nums

数组同等大小的

index

数组,用于记录未排序之前每个元素对应的下标,让

index

数组与

nums

数组同时参与归并排序,实现

nums

数组元素与其原来的下标的绑定效果。

具体如何实现同时参与归并排序请参考代码实现。

代码实现

代码语言:javascript
复制
class Solution {
public:

    //记录结果的数组
    vector<int> ret;
    //记录数组未排序之后每个元素的下标
    vector<int> index;
    //用于合并两个有序数组的辅助数组
    int tmpNums[100005];
    //用于合并两个"排序"后index数组的辅助数组
    int tmpIndex[100005];

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

        //1.计算中点划分区间
        int mid = (right - left) / 2 + left;
        //[left, mid] [mid + 1, right]

        //2.左右区间调用Merge
        MergeCount(nums, left, mid);
        MergeCount(nums, mid + 1, right);

        //3.合并+修改左区间元素对应ret数组的元素值
        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];
                i++; cur2++;
            }
            else
            {
                //通过index数组找到当前元素的未排序之前的下标
                ret[index[cur1]] += right - cur2 + 1;
                tmpNums[i] = nums[cur1];
                tmpIndex[i] = index[cur1];
                i++; cur1++;
            }
        }

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

        //4.将tmpNums和tmpIndex数组恢复到nums数组和index数组
        for(int i = left; i <= right; ++i)
        {
            nums[i] = tmpNums[i - left];
            index[i] = tmpIndex[i - left];
        }
    }

    vector<int> countSmaller(vector<int>& nums) {
        int n = nums.size();
        ret.resize(n);
        index.resize(n);

        for(int i = 0; i < n; ++i) index[i] = i;
        MergeCount(nums, 0, n - 1);
        return ret;
    }
};
  • 时间复杂度:
O(n \times log(n))
  • 空间复杂度:
O(n )

8. 翻转对

题目链接: 493. 翻转对

题目描述:

给定一个数组 nums,如果 i < jnums[i] > 2 * nums[j],我们就将 (i, j) 称作一个重要翻转对。 你需要返回给定数组中的重要翻转对的数量。

示例 1:

  • 输入: [1,3,2,3,1]
  • 输出: 2

示例 2:

  • 输入: [2,4,3,5,1]
  • 输出: 3

注意:

  • 给定数组的长度不会超过50000
  • 输入数组中的所有数字都在32位整数的表示范围内。

解题思路

与上两道题的思路基本一致,还是基于归并排序去解决。

与上两道题不同的是,本道题不能与归并排序完美适配,不能在合并数组时候统计翻转对的数量,因为翻转对的要求是前一个数大于后一个数的两倍。

所以我们在对划分出来的两个区间递归调用完 Merge 函数后,就直接统计翻转对的数量,统计完后再合并数组。

这里我们采用将数组合并成降序,当然,合并成升序同样能解决。

代码实现

代码语言:javascript
复制
class Solution {
public:
    //用于合并
    int tmp[50005];

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

        int ret = 0;

        //1.求中点划分区间
        int mid = (right - left) / 2 + left;
        //[left mid] [mid + 1, right]

        //2.对划分出来的两个数组递归调用Merge
        ret += Merge(nums, left, mid);
        ret += Merge(nums, mid + 1, right);

        //3.统计翻转对的数量
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid)
        {
            while(cur2 <= right && nums[cur1] / 2.0 <= nums[cur2]) 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++];

        //5.将tmp数组的每个元素恢复到原来数组的对应地方
        for(int j = left; j <= right; ++j)
            nums[j] = tmp[j - left];

        return ret;
    }

    int reversePairs(vector<int>& nums) {
        return Merge(nums, 0, nums.size() - 1);
    }
};
  • 时间复杂度:
O(n \times log(n))
  • 空间复杂度:
O(n )

Have a good day😏

See you next time, guys!😁✨🎞

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-06-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 颜色分类
  • 2. 排序数组(快排)
  • 3. 数组中的第K个最大元素
  • 4. 最小的 k 个数
  • 5. 排序数组(归并)
  • 6. 数组中的逆序对
  • 7. 计算右侧小于当前元素的个数
  • 8. 翻转对
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档