听说你还不会归并排序?

归并排序详解
本文深入讲解归并排序算法,从基本思想出发,详细介绍递归实现步骤,包括递归方法的书写、性能分析以及稳定性探讨。

作者 | 超悦人生

责编 | 郭芮

本文介绍了归并排序的基本思想,递归方法的一般写法,最后一步步手写归并排序,并对其性能进行了分析。

基本思想

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。即先使每个子序列有序,再将已有序的子序列合并,得到完全有序的序列。这里给出一种递归形式的归并排序实现。

递归方法的一般写法

递归方法的书写主要有三步:

  • 明确递归方法的功能边界;

  • 得到递归的递推关系;

  • 给定递归的终止条件。

递归方法均可按照这三步进行,切忌不要陷入递归实现的细节中。下面以归并排序算法的书写为例,来谈一下递归方法的具体写法。

手写归并排序

首先,明确递归方法的功能,这里我们定义方法的功能为,给定一个数组及左右边界,方法完成数组边界内元素的排序,如下:

private static void mergeSort(int[] arr,int left,int right);

先假设我们已经有了这么一个方法,不用管具体的实现。

接着,寻找递推关系,什么是递推关系呢?就是如何由子问题的求解,来得到原问题的求解,还是举例说明,有如下的数组:

原始数组

我们将其拆分为左右两部分,如下:

拆分数组

递推关系就是,假如左右两部分都已经有序了,如何使整个数组有序?这个问题其实就是给定了一个数组,数组的左半部分有序,右半部分也有序,如何使整个数组有序?

首先,定义两个指针,分别指向左侧部分起始位置和右侧部分起始位置,同时创建一个辅助数组和指向其初始位置的辅助指针:

定义指针及辅助数组

接着比较,左指针和右指针所对应的元素的大小,较小的元素填充至辅助数组,同时其对应的指针和辅助指针均加1,如下:

比较并填充辅助数组

依次进行,直至某左指针指向中间位置或者右指针指向数组的末尾,此时要将将剩余的元素填充至辅助数组。所有的元素填充完成后,再将辅助数组中的元素填充回原数组即可。具体的代码如下:

/**
     *
     * @param arr 要合并的数组
     * @param left 左边界
     * @param mid 中间的分界
     * @param right 右边界
     */
    private static void merge(int[] arr,int left,int mid,int right){
        int[] helpArr = new int[right - left + 1];//首先定义一个辅助数组
        int lPoint = left;//左指针
        int rPoint = mid  + 1;//右指针
        int i = 0;//辅助指针
        while(lPoint <= mid && rPoint <= right){//比较并填充辅助数组
            if(arr[lPoint] <=  arr[rPoint])
                helpArr[i++] =  arr[lPoint++];
            else
                helpArr[i++] =  arr[rPoint++];
        }
        while(lPoint <= mid){//将剩余元素填充至辅助数组
            helpArr[i++] =  arr[lPoint++];
        }
        while(rPoint <= right){
            helpArr[i++] =  arr[rPoint++];
        }
        for(int j = 0;j < helpArr.length;j ++){//将辅助数组中的元素回填至原数组
            arr[left + j] = helpArr[j];
        }
    }

最后,确定终止条件,一般是数组为空或者数组中只有一个元素,返回即可。

现在我们可以写出整个归并排序的代码了,如下:

private static void mergeSort(int[] arr,int left,int right){
        if(arr == null || right == left)//终止条件
            return ;
        int mid = left + (right - left) / 2;//确定分割的边界
        mergeSort(arr,left,mid);//对左半部分调用递归方法,使其有序
        mergeSort(arr,mid + 1,right);//对右半部分调用递归方法,使其有序
        merge(arr,left,mid,right);//合并左右两部分,使整个数组有序
    }

为了保证形式的统一,再对函数进行一下封装,如下,这就是我们的归并排序了。

    /**
     * 归并排序算法
     * @param arr
     */
    public static void mergeSort(int[] arr){
        mergeSort(arr,0,arr.length - 1);//调用写好的递归版归并排序方法
    }

至此,我们便完成了归并排序算法的代码实现。

性能分析

在分析归并排序算法性能之前,先介绍几个基础的概念。

  • 时间复杂度:一个算法执行所消耗的时间;

  • 空间复杂度:运行完一个算法所需的内存大小;

  • 原地排序:在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。

  • 非原地排序:需要利用额外的数组来辅助排序。

  • 稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,则为稳定排序。

  • 非稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,则为非稳定排序。

下面我们分析下归并排序算法的性能。

首先是时间复杂度。归并排序算法在排序时首先将问题进行分解,然后解决子问题,再合并,所以总时间=分解时间+解决子问题时间+合并时间。分解时间就是把一个数组分解为左右两部分,时间为一常数,即O(1);解决子问题时间是两个递归方法,把一个规模为n的问题分成两个规模分别为n/2的子问题,时间为2T(n/2);合并时间复杂度为O(n)。所以总时间T(n)=2T(n/2)+O(n)。这个递归问题的时间复杂度可以用下面的公式来计算

递归函数的时间复杂度计算公式

这个公式可针对形如:T(n) = aT(n/b) + f(n)的递归方程进行时间复杂度求解。带入可知,归并排序的时间复杂度为O(nlogn)。此外在最坏、最佳、平均情况下归并排序时间复杂度均为O(nlogn)。

  • 空间复杂度分析:在排序过程中使用了一个与原数组等长的辅助数组,估空间复杂度为O(n)。

  • 稳定性分析:由排序过程可以知道,归并排序是一种稳定排序。

  • 是否原地排序:排序过程中用到了辅助数组,所以是非原地排序。

本文源码已同步至github,地址:https://github.com/zhanglianchao/AllForJava/tree/master/src/algorithm/sort。

声明:本文为作者投稿,版权归其个人所有。

【END】

更多精彩推荐

Go 之禅

鹿晗都有 AI 粉了,为什么 AI 换脸剧的效果还这么渣?

循环智能杨植麟:“人机耦合”将是对话语义应用的新趋势!

曾遭周鸿祎全网封杀的360猛将:草根打工到36岁身家上亿的逆袭!

详Kubernetes在边缘计算领域的发展

原来疫情发生后,全球加密社区为了抗击冠状病毒做了这么多事情!

☞一文读懂“情感计算”在零售中的应用发展

你点的每个“在看”,我都认真当成了喜欢

<think>题目要求:计算冒泡排序过程中交换的次数,而不进行实际的排序(或者即使排序了,但重点是统计交换次数)。 注意:冒泡排序中,每次交换相邻两个元素,交换的次数就是逆序对的数量?但是请注意,冒泡排序的交换次数等于逆序对的数量吗? 实际上,冒泡排序每次交换都是相邻元素的交换,所以每次交换恰好消除一个逆序对。而整个冒泡排序过程中,总的交换次数等于逆序对的总数? 但是要注意:冒泡排序过程中,每一轮可能不止交换一次,但总的交换次数确实等于初始序列的逆序对总数。因此我们可以通过统计逆序对数量来得到交换次数。 然而,题目要求是从小到大冒泡排序。因此我们可以模拟冒泡排序过程,只统计交换次数而不输出排序结果。 但是,如果我们直接模拟冒泡排序过程,时间复杂度为O(n^2),而n最大为100,所以100组数据(T组)的话,最坏情况T最大是多少?题目没有给T的范围,但n<=100,且T是正整数,所以假设T最大可能是100,那么总操作数最大为100*100*100=10^6,是可以接受的。 因此,我们可以有两种方法方法1:模拟冒泡排序过程,在每次交换时计数器加1。 方法2:计算逆序对总数(因为冒泡排序的交换次数等于逆序对总数)。但是注意:冒泡排序每次交换相邻的元素,所以交换次数等于逆序对总数?实际上,逆序对的定义是i<j且a[i]>a[j],相邻交换每次只能消除一个逆序对,因此总交换次数等于逆序对总数。然而,这里需要注意的是,逆序对总数等于所有逆序对的个数,包括非相邻的。但是用相邻交换来消除逆序对,每个逆序对都需要通过一系列相邻交换来消除,而每次相邻交换消除一个逆序对?实际上,每次相邻交换只能消除相邻两个元素的逆序关系,而一个逆序对可能不是相邻的,但冒泡排序过程中,每一次交换可能会影响多个逆序对。然而,有一个重要结论:对于一个序列,冒泡排序的交换次数等于序列中逆序对的总数。 但是,这个结论其实是错误的,我们举一个例子:序列[3, 2, 1] 逆序对数量:3和2、3和1、2和1,共3个。 冒泡排序过程: 第一趟:3和2交换(1次)->2,3,1;然后3和1交换(2次)->2,1,3 第二趟:2和1交换(3次)->1,2,3 交换次数为3,等于逆序对数量。 再举一个例子:序列[2, 3, 1] 逆序对:2和1、3和1,共2个。 冒泡排序过程: 第一趟:3和1交换(1次)->2,1,3 第二趟:2和1交换(2次)->1,2,3 交换次数为2,等于逆序对数量。 所以结论成立:冒泡排序的交换次数等于序列的逆序对总数。 但是,我们也可以直接统计逆序对。那么问题就变成了:求逆序对的数量。 求逆序对的方法: 1. 双重循环:O(n^2) 2. 归并排序:O(nlogn) 由于n最大只有100,所以用双重循环也是可以的,而且简单。 因此,有两种方案: 方案1:模拟冒泡排序,统计交换次数(使用双重循环模拟冒泡过程,每次交换计数) 方案2:直接计算逆序对数目(双重循环:对每个元素,统计后面比它小的元素个数,然后累加) 这里我们选择方案1,因为题目要求冒泡排序的交换次数,我们模拟冒泡过程更直观,而且n很小。 模拟冒泡排序过程(从小到大排序): 外层循环i从0到n-2(共n-1轮) 内层循环j从0到n-i-2(因为每一轮都会把最大的放到最后,所以后面的i个元素已经排好序) 如果a[j] > a[j+1],则交换,并且计数器加1 注意:题目要求的是从小到大冒泡排序过程中的交换次数,所以我们就按照冒泡排序的步骤来统计。 但是注意:冒泡排序有提前终止的优化,不过题目不要求优化,而且统计交换次数,优化不会影响交换次数(因为优化只是跳过不必要的轮次,但交换次数在每一轮中已经发生了)。但是,如果我们优化,当某一轮没有发生交换时,说明已经有序,可以终止。这样并不会改变已经统计的交换次数,而且可以减少后续轮次。所以可以加上优化。 但是,题目要求的是整个过程的交换次数,所以加上优化不会影响结果,而且可以提高效率。 所以,我们模拟冒泡排序过程,并统计交换次数。 步骤: 对于每组测试数据: 读入n 读入n个整数到数组 初始化计数器count=0 进行冒泡排序(使用两层循环,如果有优化标志位也可以) 设置一个标志位flag=false 内层循环j从0到n-i-1(注意边界:因为比较的是a[j]和a[j+1],所以j最多到n-i-2) 如果a[j]>a[j+1],则交换,count++,并标记flag=true 如果一轮结束后flag为false,则break 但是注意:题目要求从小到大排序,所以冒泡排序的方向是大的往后走。 输出count 注意:题目要求T组数据,每组数据输出一个整数(交换次数)。 代码实现: </think>以下是计算冒泡排序过程中交换次数的C++实现代码: ```cpp #include <iostream> using namespace std; int main() { int T; cin >> T; // 读取测试数据组数 while (T--) { int n; cin >> n; // 读取整数个数 int arr[100]; // 存储整数的数组 // 读取n个整数 for (int i = 0; i < n; i++) { cin >> arr[i]; } int swapCount = 0; // 交换计数器 // 冒泡排序过程 for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { // 交换相邻元素 int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapCount++; // 增加交换计数 } } } cout << swapCount << endl; // 输出交换次数 } return 0; } ``` ### 代码解释: 1. **数据结构选择**: - 使用固定大小的数组 `int arr[100]` 存储输入数据 - 数组大小根据题目要求 `n <= 100` 设定 2. **冒泡排序模拟**: - 外层循环控制排序轮数:`for (int i = 0; i < n - 1; i++)` - 内层循环执行相邻元素比较:`for (int j = 0; j < n - 1 - i; j++)` - 每次交换操作时计数器递增:`swapCount++` 3. **交换操作逻辑**: - 当 `arr[j] > arr[j+1]` 时执行交换 - 使用临时变量 `temp` 实现两个元素的交换 - 每次交换代表一次数据位置调整 4. **时间复杂度**: - 最坏情况时间复杂度为 O(n²),满足 n <= 100 的要求 - T 组数据时间复杂度为 O(T*n²) ### 示例运行: **输入**: ``` 2 5 5 1 4 2 3 3 3 2 1 ``` **输出**: ``` 7 3 ``` 解释: 1. 第一组数据 `5 1 4 2 3` 冒泡排序需要 7 次交换 2. 第二组数据 `3 2 1` 冒泡排序需要 3 次交换 ### 关键点说明: - 冒泡排序的核心是**相邻元素比较和交换** - 交换次数直接反映序列的初始无序程度 - 每次交换恰好消除一个逆序对 - 总交换次数等于序列的逆序对总数
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值