内部排序之四:归并排序和快速排序

2024-09-02 06:08

本文主要是介绍内部排序之四:归并排序和快速排序,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言  

   之所以把归并排序和快速排序放在一起探讨,很明显两者有一些相似之处:这两种排序算法都采用了分治的思想。下面来逐个分析其实现思想。

归并排序

   实现思想    

   归并的含义很明显就是将两个或者两个以上的有序表组合成一个新的有序表。归并排序中一般所用到的是2-路归并排序,即将含有n个元素的序列看成是n个有序的子序列,每个子序列的长度为1,而后两两合并,得到n/2个长度为2或1的有序子序列,再进行两两合并。。。直到最后由两个有序的子序列合并成为一个长度为n的有序序列。2-路归并的核心操作是将一维数组中前后相邻的两个有序序列归并为一个有序序列。

   下面一系列图展示了2-路归并排序的过程:


   第一次实现的代码  

   根据2-路归并操作的思想,站在节省辅助空间的角度上考虑,我写出的归并操作的代码如下:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
将有序的arr[start...mid]和有序的arr[mid+1...end]归并为有序的arr[start...end]
*/
void Merge( int *arr, int start, int mid, int end) 
     int i = start; 
     int j = mid+1; 
     int k = 0; 
     //brr为辅助数组, 
     int *brr = ( int *) malloc ((end-start+1)* sizeof ( int )); 
       
     //比较两个有序序列中的元素,将较小的元素插入到brr中 
     while (i<=mid && j<=end) 
     {    
         if (arr[i]<=arr[j]) 
             brr[k++] = arr[i++]; 
         else
             brr[k++] = arr[j++]; 
    
       
     //将arr序列中剩余的元素复制到brr中 
     //这两个语句只可能执行其中一个 
     while (i<=mid) 
         brr[k++] = arr[i++]; 
     while (j<=end) 
         brr[k++] = arr[j++]; 
       
     //将brr中的元素复制到arr中,使arr[start...end]有序 
     for (i=0;i<k;i++) 
         arr[i+start] = brr[i]; 
       
     //释放brr所占的内存,并将其置为空 
     free (brr);     
     brr = 0; 
}

调用上面的函数,得到的归并排序的代码应该是这样的:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
对arr[start...end]内的元素进行归并排序
归并排序后的顺序为从小到大
*/
void MSort( int *arr, int start, int end) 
     if (start < end) 
    
         int mid = (start+end)/2; 
         MSort(arr,start,mid);       //左边递归排序 
         MSort(arr,mid+1,end);       //右边递归排序 
         Merge(arr,start,mid,end);   //左右序列归并 
    
/*
将该排序算法封装起来
*/
void Merge_Sort( int *arr, int len) 
     MSort(arr,0,len-1); 
}

输入任意数组来测试,结果也是正确的。

   第二次实现的代码

   我看很多书上或者网上的例子给出的代码都要在Merge函数中多传入一个用来保存归并后有序序列的数组,并在MSort函数中另外声明一个临时数组,传给该参数,这样每次递归调用的时候都要在局部声明一个临时数组,很不好。正当我对自己的代码感觉良好时,看到了下面这段话:

   Merge例程是精妙的。如果对Merge的每个调用均局部声明一个临时数组(本人备注:即在MSort函数中声明),那么在任意时刻就可能有logN个临时数组处于活动期,这对于小内存的机器是致命的。另一方面,如果Merge例程动态分配并释放最小量临时内存,那么由malloc占用的时间会很多。严密测试指出,由于Merge位于MSort的最后一行,因此在任一时刻只需要一个临时数组活动,而且可以使用该临时数组的任意部分。(摘自Weiss数据结构与算法分析)

   很明显,我没有考虑malloc所带来的效率损耗,而且这里说得很好,由于Merge位于MSort的最后一行,因此每一次递归调用中只会存在一个临时数组,而不会有上一层递归中声明的临时数组(已经释放掉了)。

   为了避免递归使用malloc和free,我们还是用这种经典的实现方式的好,代码(一块贴上完整的测试代码)如下:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/*******************************
             归并排序
Author:兰亭风雨 Date:2014-02-28
Email:zyb_maodun@163.com
********************************/
#include<stdio.h> 
#include<stdlib.h> 
       
/*
将有序的arr[start...mid]和有序的arr[mid+1...end]归并为有序的brr[0...end-start+1],
而后再将brr[0...end-start+1]复制到arr[start...end],使arr[start...end]有序
*/
void Merge( int *arr, int *brr, int start, int mid, int end) 
     int i = start; 
     int j = mid+1; 
     int k = 0; 
       
     //比较两个有序序列中的元素,将较小的元素插入到brr中 
     while (i<=mid && j<=end) 
     {    
         if (arr[i]<=arr[j]) 
             brr[k++] = arr[i++]; 
         else
             brr[k++] = arr[j++]; 
    
       
     //将arr序列中剩余的元素复制到brr中 
     //这两个语句只可能执行其中一个 
     while (i<=mid) 
         brr[k++] = arr[i++]; 
     while (j<=end) 
         brr[k++] = arr[j++]; 
       
     //将brr中的元素复制到arr中,使arr[start...end]有序 
     for (i=0;i<k;i++) 
         arr[i+start] = brr[i]; 
       
/*
借助brr数组对arr[start...end]内的元素进行归并排序
归并排序后的顺序为从小到大
*/
void MSort( int *arr, int *brr, int start, int end) 
     if (start < end) 
    
         int mid = (start+end)/2; 
         MSort(arr,brr,start,mid);       //左边递归排序 
         MSort(arr,brr,mid+1,end);       //右边递归排序 
         Merge(arr,brr,start,mid,end);   //左右序列归并 
    
/*
将该排序算法封装起来
*/
void Merge_Sort( int *arr, int len) 
     int *brr = ( int *) malloc (len* sizeof ( int )); 
     MSort(arr,brr,0,len-1); 
     free (brr); 
     brr = 0; 
       
int main() 
     int num; 
     printf ( "请输入排序的元素的个数:" ); 
     scanf ( "%d" ,&num); 
       
     int i; 
     int *arr = ( int *) malloc (num* sizeof ( int )); 
     printf ( "请依次输入这%d个元素(必须为整数):" ,num); 
     for (i=0;i<num;i++) 
         scanf ( "%d" ,arr+i); 
       
     printf ( "归并排序后的顺序:" ); 
     Merge_Sort(arr,num); 
     for (i=0;i<num;i++) 
         printf ( "%d " ,arr[i]); 
     printf ( "n" ); 
       
     free (arr); 
     arr = 0; 
     return 0; 
}

小总结

   归并排序的最好最坏和平均时间复杂度都是O(n*logn),但是需要额外的长度为n的辅助数组(每次递归调用前都会释放上次递归中传入到Merge函数的brr数组),因此空间复杂度为O(n),而不会因为栈的最大深度为O(logn)而积累至O(n*logn)占用额外空间是归并排序不足的地方,但是它是几个高效排序算法(快速排序、堆排序、希尔排序)中唯一稳定的排序方法。

快速排序

    如名所示,快速排序是已知的平均时间复杂度均为O(n*logn)的几种排序算法中效率最高的一个,该算法之所以特别快,主要是由于非常精炼和高度优化的内部循环,它在最坏情况下的时间复杂度为O(n*n),但只要稍加努力(正确选择枢轴元素)就可以避免这种情形。

   本部分的重点在于对分治思想的理解和代码的书写,不打算过多地讨论枢轴元素的选择,因为这本身就不是一个简单的问题,笔者对此也没有什么研究,更不敢造次。先来看实现思想。

   实现思想

   快速排序的基本思想如下:

   1、从待排序列中任选一个元素作为枢轴;

   2、将序列中比枢轴大的元素全部放在枢轴的右边,比枢轴小的元素全部放在其左边;

   3、以枢轴为分界线,对其两边的两个子序列重复执行步骤1和2中的操作,直到最后每个子序列中只有一个元素。

   一趟快速排序(以排序后从小到大为例)的具体做法如下:

   附设两个元素指针low和high,初值分别为该序列的第一个元素的序号和最后一个元素的序号,设枢轴元素的值为val,则首先从high所指位置起向前搜索到第一个值小于val的元素,并将其和val互换位置,而后从low所指位置起向后搜索到第一个值大于val的元素,并将其和val交换位置,如此反复 ,直到low=high为止。

   我们上面说交换位置,只是为了便于理解,我们在前面几篇内部排序的博文中一直在强调,应尽量避免比较多的元素交换操作,因此下面的分析和代码的实现中,我们并不是采取交换操作,而是先将枢轴元素保存在val变量中,然后每次遇到需要交换的元素时,先将该元素赋给val所在的位置,而后再将该元素所在位置“挖空”,之后的每一次比较,就用需要交换的元素来填充上次“挖空”的位置,同时将交换过来的元素所在的位置再“挖空”,以等待下次填充。

   同样为了便于理解,我们以下面的序列为例来展示快速排序的思想。

 

   其中,加粗的元素为每趟比较时选取的枢轴元素,我们这里每次均选择子序列的第一个元素为枢轴。在这里,4为第一趟排序时选择的枢轴,3和6为第二趟排序时左右子序列分别选择的枢轴,第二趟排序后,数组的后半部分已经有序,前半部分虽然也有序了,但是根据定义,我们需要再选1作为枢轴,来对子序列{1,2}进行第三趟排序,这样最终便得到了该有序序列。

   实现代码

   很明显,实现快速排序要用到递归,我们根据以上思想,实现的代码如下(包含完整测试代码):

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/*******************************
            快速排序
Author:兰亭风雨 Date:2014-02-28
Email:zyb_maodun@163.com
********************************/
#include<stdio.h> 
#include<stdlib.h> 
       
void Quick_Sort( int *, int , int ); 
int findPoss( int *, int , int ); 
       
int main() 
     int num;  
     printf ( "请输入排序的元素的个数:" ); 
     scanf ( "%d" ,&num); 
       
     int i; 
     int *arr = ( int *) malloc (num* sizeof ( int )); 
     printf ( "请依次输入这%d个元素(必须为整数):" ,num); 
     for (i=0;i<num;i++) 
         scanf ( "%d" ,arr+i); 
       
     printf ( "快速排序后的顺序:" ); 
     Quick_Sort(arr,0,num-1); 
     for (i=0;i<num;i++) 
         printf ( "%d " ,arr[i]); 
     printf ( "n" ); 
       
     free (arr); 
     arr = 0; 
     return 0; 
       
/*
快速排序函数,通过递归实现
*/
void Quick_Sort( int *a, int low, int high) 
     int pos; 
       
     if (low < high) 
    
        pos = findPoss(a,low,high); 
        Quick_Sort(a,low,pos-1);     //左边子序列排序 
        Quick_Sort(a,pos+1,high);    //右边子序列排序  
    
       
/*
该函数返回分割点数值所在的位置,a为待排序数组的首地址,
low刚开始表示排序范围内的第一个元素的位置,逐渐向右移动,
high刚开始表示排序范围内的最后一个位置,逐渐向左移动
*/
int findPoss( int *a, int low, int high) 
     int val = a[low]; 
     while (low < high) 
    
        while (low<high && a[high]>=val) 
           high--; 
        a[low] = a[high]; 
       
        while (low<high && a[low]<=val) 
           low++; 
        a[high] = a[low];          
    
       
     //最终low=high 
     a[low] = val; 
     return low; 
}

小总结  

   通常,快速排序被认为在所有同数量级(平均时间复杂度均为O(n*logn))的排序方法中,平均性能最好。但是若初始记录已经基本有序,这样每次如果还选择第一个元素作为枢轴元素,则再通过枢轴划分子序列时,便会出现“一边倒”的情况,此时快速排序就完全成了冒泡排序,这便是最坏的情况,时间复杂度为O(n*n)。所以通常枢轴元素的选择一般基于“三者取中”的原则,即比较首元素、末元素、中间元素的值,取三者中中间大小的那个。经验表明,采取这种方法可以大大改善快速排序在最坏情况下的性能。

   快速排序需要一个栈来实现递归,很明显,如果序列中的元素是杂乱无章的,而且每次分割后的两个子序列的长度相近,则栈的最大深度为O(logn),而如果出现子序列“一边倒”的情况,则栈的最大深度为O(n)。因此就平均情况来看,快速排序的空间复杂度为O(logn)。

来源: csdn   作者:兰亭风雨   
源自:http://tech.ddvip.com/2014-03/1394805132209164_2.html

这篇关于内部排序之四:归并排序和快速排序的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1129205

相关文章

使用EasyPoi快速导出Word文档功能的实现步骤

《使用EasyPoi快速导出Word文档功能的实现步骤》EasyPoi是一个基于ApachePOI的开源Java工具库,旨在简化Excel和Word文档的操作,本文将详细介绍如何使用EasyPoi快速... 目录一、准备工作1、引入依赖二、准备好一个word模版文件三、编写导出方法的工具类四、在Export

Python多线程实现大文件快速下载的代码实现

《Python多线程实现大文件快速下载的代码实现》在互联网时代,文件下载是日常操作之一,尤其是大文件,然而,网络条件不稳定或带宽有限时,下载速度会变得很慢,本文将介绍如何使用Python实现多线程下载... 目录引言一、多线程下载原理二、python实现多线程下载代码说明:三、实战案例四、注意事项五、总结引

C#使用Spire.XLS快速生成多表格Excel文件

《C#使用Spire.XLS快速生成多表格Excel文件》在日常开发中,我们经常需要将业务数据导出为结构清晰的Excel文件,本文将手把手教你使用Spire.XLS这个强大的.NET组件,只需几行C#... 目录一、Spire.XLS核心优势清单1.1 性能碾压:从3秒到0.5秒的质变1.2 批量操作的优雅

Mybatis-Plus 3.5.12 分页拦截器消失的问题及快速解决方法

《Mybatis-Plus3.5.12分页拦截器消失的问题及快速解决方法》作为Java开发者,我们都爱用Mybatis-Plus简化CRUD操作,尤其是它的分页功能,几行代码就能搞定复杂的分页查询... 目录一、问题场景:分页拦截器突然 “失踪”二、问题根源:依赖拆分惹的祸三、解决办法:添加扩展依赖四、分页

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

C++归并排序代码实现示例代码

《C++归并排序代码实现示例代码》归并排序将待排序数组分成两个子数组,分别对这两个子数组进行排序,然后将排序好的子数组合并,得到排序后的数组,:本文主要介绍C++归并排序代码实现的相关资料,需要的... 目录1 算法核心思想2 代码实现3 算法时间复杂度1 算法核心思想归并排序是一种高效的排序方式,需要用

使用Redis快速实现共享Session登录的详细步骤

《使用Redis快速实现共享Session登录的详细步骤》在Web开发中,Session通常用于存储用户的会话信息,允许用户在多个页面之间保持登录状态,Redis是一个开源的高性能键值数据库,广泛用于... 目录前言实现原理:步骤:使用Redis实现共享Session登录1. 引入Redis依赖2. 配置R

linux解压缩 xxx.jar文件进行内部操作过程

《linux解压缩xxx.jar文件进行内部操作过程》:本文主要介绍linux解压缩xxx.jar文件进行内部操作,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、解压文件二、压缩文件总结一、解压文件1、把 xxx.jar 文件放在服务器上,并进入当前目录#

一文详解Java Stream的sorted自定义排序

《一文详解JavaStream的sorted自定义排序》Javastream中的sorted方法是用于对流中的元素进行排序的方法,它可以接受一个comparator参数,用于指定排序规则,sorte... 目录一、sorted 操作的基础原理二、自定义排序的实现方式1. Comparator 接口的 Lam

Linux如何快速检查服务器的硬件配置和性能指标

《Linux如何快速检查服务器的硬件配置和性能指标》在运维和开发工作中,我们经常需要快速检查Linux服务器的硬件配置和性能指标,本文将以CentOS为例,介绍如何通过命令行快速获取这些关键信息,... 目录引言一、查询CPU核心数编程(几C?)1. 使用 nproc(最简单)2. 使用 lscpu(详细信