力扣--分治(归并排序)算法题I:排序数组,交易逆序对的总数

相关文章推荐:

力扣--分治(快速排序)算法题I:颜色分类,排序数组

力扣--分治(快速排序)算法题II:数组中的第K个最大元素(Top K问题),LCR159.库存管理III


目录

1.排序数组

理解题意

归并与快排区别

算法原理

[1. 分(Divide):盲目拆分,直到不能再拆](#1. 分(Divide):盲目拆分,直到不能再拆)

[2. 治(Conquer):子问题天然解决](#2. 治(Conquer):子问题天然解决)

[3. 合(Combine/Merge):归并排序的灵魂](#3. 合(Combine/Merge):归并排序的灵魂)

优化代码

2.交易逆序对的总数

理解题意

算法原理

拓展:策略二


1.排序数组

https://leetcode.cn/problems/sort-an-array/description/

理解题意

本题在相关文章中,按照快速排序做过,现在回顾一下之前的代码:

复制代码
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];
    }
};

归并与快排区别

比较维度 归并排序 (Merge Sort) 快速排序 (Quick Sort)
处理顺序 自底向上(后序遍历) :先递归拆分到最小单元(单元素),再在回溯时进行合并(干重活)。 自顶向下(前序遍历) :先选定基准值进行划分(干重活),再对左右子区间递归。
空间复杂度 O(N)(需要辅助数组暂存数据) O(log N)(原地排序,仅消耗递归栈空间)
时间复杂度 始终为 O(N log N) 平均 O(N log N),最坏退化为 O(N^2)
稳定性 稳定(相对顺序不改变) 不稳定(交换元素时会破坏相对顺序)
缓存友好性 较差(频繁操作非连续的辅助数组空间) 极好(在原数组上双指针滑动,命中 CPU 缓存率高)
适用场景 链表排序、外部排序(数据量大到内存放不下时) 绝大多数内存中的数组常规排序(工业界首选)

接下来,我们用归并排序完成

算法原理

这段代码背后的核心算法原理,用四个字概括就是:分而治之(Divide and Conquer)

归并排序(Merge Sort)是分治法最经典的成功应用之一。它的基本逻辑是:如果要排序一个很大的数组,直接排太难了;那我们就把它拆成小数组,小数组排好序之后,再把它们"缝合"起来。

我们可以将这个原理拆解为三个阶段来理解:

1. 分(Divide):盲目拆分,直到不能再拆

面对一个长度为 N 的无序数组,归并排序的策略是"不看数据,直接从中间一刀切"。

  • 一个大数组被劈成两个长度为 N/2 的子数组。

  • 这两个子数组再被劈成四个长度为 N/4 的子数组。

  • 劈砍一直进行(对应代码中的不断递归 mergeSort),直到每个子数组里只剩下 1 个元素

  • 原理重点 :在算法世界里,一个只有一个元素的数组,天然就是已经排好序的。这就是分治法的"基线条件"(Base Case)。

2. 治(Conquer):子问题天然解决

因为拆分到了极致(单元素),我们不需要做任何特殊的排序操作,单个元素的数组本身就是有序的。这时候,递归开始触底反弹(回溯)。

3. 合(Combine/Merge):归并排序的灵魂

这是归并排序真正"干活"的地方。现在我们手里有了一堆极小的、已经排好序的数组,我们需要把它们两两合并。

  • 合并原理 :将两个已经有序的数组合并成一个大的有序数组,是非常高效的。我们只需要两个指针分别指向两个数组的开头,每次比较这两个指针指向的元素,谁小就把谁拿出来放到新数组里,然后对应指针后移。

  • 这就像两支已经按身高从矮到高排好队的队伍,要合并成一支大队伍。你只需要每次比较两支队伍站在最前面的人,谁更矮,谁就先出列站到新队伍里。

  • 这样一层一层地向上合并,最终把两个长度为 N/2 的有序数组合并成一个长度为 N 的完整有序数组。

    class Solution {
    public:
    vector<int> sortArray(vector<int>& nums) {
    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) >> 1;
    
          // 左右区间排序
          mergeSort(nums, left, mid);
          mergeSort(nums, mid + 1, right);
    
          // 合并两个有序数组
          vector<int> tmp(right - left + 1);
          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++];
    
          // 还原
          for(int i = left; i <= right; i++)
          {
              nums[i] = tmp[i - left];
          }
      }

    };

优化代码

上述代码中,在合并两个有序数组时开辟空间,实际上是有一定的消耗的,所以这次我们将其放在全局辅助数组,这样对效率会有极大的提升。

复制代码
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) >> 1;

        // 左右区间排序
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);

        // 合并两个有序数组
        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++];

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

2.交易逆序对的总数

https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/description/

理解题意

按照所给数组,依次寻找前一数比后一数大的组合并返回。

算法原理

本题最基础的思路是,将一数组拆分为一左一右,再从两边取值进行匹配。

更进一步:在排序时,将一左一右的数组进行排序,之后,再从两边取值进行匹配。

因此,我们使用归并排序,并且对本题排序的要求为 升序排序。

策略:升序排序(找出在此数前有多少个比我大的数)

情况一:nums[cur1] <= nums[cur2] -> cur1++

情况二:nums[cur1] > nums[cur2] -> ret += mid - cur1 + 1; cur2++;

代码:

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

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

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

        int i = 0, cur1 = left, cur2 = mid + 1;
        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++];

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

        return ret;
    }
};

拓展:策略二

降序排序(找出在此数之后,有多少个比我小的数)

本题降序排列代码:

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

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

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

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

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

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

        return ret;
    }
};

本章完。

相关推荐
阳光永恒7362 小时前
C++编程全套学习资料免费分享 | 从零基础到进阶(含视频课/PPT课件/源码/项目实战)
c++·学习·编程学习·免费资料·零基础学c++·c++资料
楼田莉子2 小时前
C++高性能并发内存池:三种Cache的设计及其内存申请释放
c++·后端·链表·哈希算法·visual studio
sprite_雪碧2 小时前
排版类问题(机试高频)
c语言·数据结构·算法
暮冬-  Gentle°2 小时前
设计模式在C++中的实现
开发语言·c++·算法
2501_908329852 小时前
实时音频处理C++实现
开发语言·c++·算法
dapeng28702 小时前
移动语义与完美转发详解
开发语言·c++·算法
bbbb3652 小时前
算法工程中的可扩展性与分布式实现方案的技术7
算法
Shining05962 小时前
AI 编译器系列(六)《Stable Diffusion 在 InfiniTensor 推理框架中的适配与工程实践》
人工智能·算法·stable diffusion·大模型·图像生成·ai编译器·infinitensor
佩奇大王2 小时前
P159 摆动序列
java·开发语言·算法