七大排序的个人总结(二) 归并排序(Merge
七大排序的个人总结(二)
归并排序(Merge
归并排序(Merge Sort):
归并排序是一个相当“稳定”的算法对于其它排序算法,比如希尔排序,快速排序和堆排序而言,这些算法有所谓的最好与最坏情况。而归并排序的时间复杂度是固定的,它是怎么做到的?
两个有序数组的合并:
首先来看归并排序要解决的第一个问题:两个有序的数组怎样合成一个新的有序数组:
比如数组1{ 3,5,7,8 }数组2为{ 1,4,9,10 }:
首先那肯定是创建一个长度为8的新数组咯,然后就是分别从左到右比较两个数组中哪一个值比较小,然后复制进新的数组中:比如我们这个例子:
{ 3,5,7,8 } { 1,4,9,10 } { }一开始新数组是空的。
然后两个指针分别指向第一个元素,进行比较,显然,1比3小,所以把1复制进新数组中:
{ 3,5,7,8 } { 1,4,9,10 } { 1, }
第二个数组的指针后移,再进行比较,这次是3比较小:
{ 3,5,7,8 } { 1,4,9,10 } { 1,3, }
同理,我们一直比较到两个数组中有某一个先到末尾为止,在我们的例子中,第一个数组先用完。{ 3,5,7,8 } { 1,4,9,10 } { 1,3,4,5,7,8 }
最后把第二个数组中的元素复制进新数组即可。
{ 1,3,4,5,7,8,9,10 }
由于前提是这个两个数组都是有序的,所以这整个过程是很快的,我们可以看出,对于一对长度为N的数组,进行合并所需要的比较次数最多为2 * N -1(这里多谢园友@icyjiang的提醒)。
这其实就是归并排序的最主要想法和实现,归并排序的做法是:
将一个数组一直对半分,问题的规模就减小了,再重复进行这个过程,直到元素的个数为一个时,一个元素就相当于是排好顺序的。
接下来就是合并的过程了,合并的过程如同前面的描述。一开始合成两个元素,然后合并4个,8个这样进行。
所以可以看到,归并排序是“分治”算法的一个经典运用。
我们可以通过代码来看看归并算法的实现:

public static int[] sort(int[] array, int left, int right) { if (left == right) { return new int[] { array[left] }; } int mid = (right + left) / 2; int[] l = sort(array, left, mid); int[] r = sort(array, mid + 1, right); return merge(l, r); } // 将两个数组合并成一个 public static int[] merge(int[] l, int[] r) { int[] result = new int[l.length + r.length]; int p = 0; int lp = 0; int rp = 0; while (lp < l.length && rp < r.length) { result[p++] = l[lp] < r[rp] ? l[lp++] : r[rp++]; } while (lp < l.length) { result[p++] = l[lp++]; } while (rp < r.length) { result[p++] = r[rp++]; } return result; }

代码量其实也并不多,主要的工作都在合并两个数组上。从代码上看,
if (left == right) { return new int[] { array[left] }; }
这个是递归的基准(base case),也就是结束的条件是当元素的个数只有一个时。
int mid = (right + left) / 2; int[] l = sort(array, left, mid); int[] r = sort(array, mid + 1, right);
这一部分显然就是分(divide),将一个大问题分成小的问题。
最后也就是治(conquer)了,将两个子问题的解合并可以得到较大问题的解。
所以可以说,归并排序是说明递归和分治算法的经典例子。
然后就又要回到比较原始的问题了,归并排序它为什么会快呢?
想回答这个问题可以先想一下之前说过的提高排序速度的两个重要的途径:一个是减少比较次数,一个是减少交换次数。
对于归并排序而言,我们来从之前的例子应该可以看到,两个数组的合并过程是线性时间的,也就是说我们每一次比较都可以确定出一个元素的位置。这是一个重要的性质。
我们来看一个可以用一个例子来体会一下假如有这样一个数组{ 3,7,2,5,1,0,4,6 },
冒泡和选择排序的比较次数是25次。
直接插入排序用了15次。
而归并排序的次数是相对稳定的,由我们上面提到的比较次数的计算方法,我们的例子要合并4对长度为1的,2对长度为2的,和1对长度为4的。
归并排序的最多的比较次数为4 * 1 + 2 * 3 + 7 = 17次。(感谢@icyjiang的提醒)
再次说明一下,这个例子依然只是为了好理解,不能作为典型例子来看。
因为元素的随机性,直接插入排序也可能是相当悲剧的。但我们应该从中看到的是归并排序在比较次数上的优势。
至于在种优势是怎么来的,我个人不成熟的总结一下,就是尽量的让上一次操作的结果为下一次操作服务。
我们每一次合并出来的数组,是不是就是为下一次合并做准备的。因为两个要合并的数组是有序的,我们才可能高效地进行合并。
快速排序(Quick Sort):
这个算法的霸气程度从它的名字就可以看出来了。快速排序的应用也是非常广的的,各种类库都可以看到他的身影。这当然与它的“快”是有联系的,正所谓天下武功唯快不破。
快速排序的一个特点是,对数组的一次遍历,可以找到一个枢纽元(pivot)确定位置,还可以把这个数组以这个枢纽元分成两个部分,左边的元素值都比枢纽元小,右边的都比枢纽元大。我们递归地解决这两个子数组即可。
我们还是通过一个特殊的例子来看一下快速排序的原理:
我们假设有这样一个数组{ 4,7,3,2,8,1,5 }
对于快速排序来说,第一步就是找出一个枢纽元,而对于枢纽元的寻找是对整个算法的时间性能影响很大的,因为搞不好快速排序会退化成选择排序那样。
对于这个不具有代表性的例子,我们选择的是第一个元素做为枢纽元。
pivot 4
{ 4,7,3,2,8,1,5 }
其中,红色为左指针,蓝色为右指针。一开始我们从右边开始,找到第一个比pivot小的数。停止,然后将该值赋给左指针,同样,左指针向右移动。
也就是说我们第一次得到的的结果是这样的:
{ 1,7,3,2,8,1,5 }
同样的道理,我们在左边找到一个比pivot大的值,赋值给右指针,同时右指针左移一步。
得到的结果应该是这样的:
{ 1,7,3,2,8,7,5 }
请注意,我们的这个移动过程的前提都是左指针不能超过右指针的前提下进行的。
这两个过程交替进行,其实就是在对元素进行筛选。这一次得到的结果是:
{ 1,2,3,2,8,7,5 }
黄色高亮表示两个指针重叠了,这时候我们也就找到了枢纽元的位置了,将我们的枢纽元的值插入。
也就是说,我们接下来的工作就是以这个枢纽元为分割,对左右两个数组进行同样的排序工作。
来看看具体的代码是怎么实现的:

public static void sort(int[] array, int start, int end) { if (start >= end) { return; } int left = start; int right = end; int temp = array[left]; while (left < right) { while (left < right && temp < array[right]) { right--; } if (left < right) { array[left] = array[right]; left++; } while (left < right && temp > array[left]) { left++; } if (left < right) { array[right] = array[left]; right--; } } array[left] = temp; sort(array, start, left - 1); sort(array, left + 1, end); }

接下来还是同样的问题,快速排序为什么会快呢?如果没有足够的强大,那不是“浪得虚名”吗?
首先还是看看前面的例子。
首先可以比较容易感受到的就是元素的移动效率高了。比如说例子中的1,一下子就移动到了前面去。
这也是我个人的一点感受,只是觉得可以这样理解比较高效的排序算法的特性:
高效的排序算法对元素的移动效率都是比较高的。
它不像冒泡,直接插入那样,每次可能都是步进一步,而是比较快速的移动到“感觉是正确”的位置。
想想,希尔排序不就是这么做的吗?后面的堆排序也是这个原理。
其次,快速排序也符合我们前面说的,“让上一个操作的结果为下一次操作服务”。
很明显,在枢纽元左边的元素都比枢纽元要小,右边的都比枢纽元大。显然,数据的范围小了,数据的移动的准确性就高了。
但是,快速排序的一个隐患就是枢纽元的选择,我提供的代码中是选第一个元素做枢纽元,这是一种很冒险的做法。
比如我们对一个数组{ 9,8,7,6,5 }想通过快速排序来变成从小到大的排序。如果还是选择以第一个元素为枢纽元的话,快速排序就变成选择排序了。
所以,在实际应用中如果数据都是是随机数据,那么选择第一个做枢纽元并没有什么不妥。因为这个本来就是看“人品”的。
但是,如果是对于一些比较有规律的数据,我们的“人品”可能就不会太好的。所以常见的有两种选择策略:
一种是使用随机数来做选择。呵呵,听天由命。
另一种是取数组中的第一个,最后一个和中间一个,选择数值介于最大和最小之间的。
这一种又叫做“三数中值分割法”。理论上,这两种选择策略还是可能很悲剧的。但概率要小太多了。
堆排序用文字太难看懂了,想画一些图来帮助理解,求各位大大推荐可以比较方便画二叉树的工具。
七大排序的个人总结(二) 归并排序(Merge的更多相关文章
- 排序算法二:归并排序(Merge sort)
归并排序(Merge sort)用到了分治思想,即分-治-合三步,算法平均时间复杂度是O(nlgn). (一)算法实现 private void merge_sort(int[] array, int ...
- 经典排序算法 - 归并排序Merge sort
经典排序算法 - 归并排序Merge sort 原理,把原始数组分成若干子数组,对每个子数组进行排序, 继续把子数组与子数组合并,合并后仍然有序,直到所有合并完,形成有序的数组 举例 无序数组[6 2 ...
- 连续线性空间排序 起泡排序(bubble sort),归并排序(merge sort)
连续线性空间排序 起泡排序(bubble sort),归并排序(merge sort) 1,起泡排序(bubble sort),大致有三种算法 基本版,全扫描. 提前终止版,如果发现前区里没有发生交换 ...
- (转)白话经典算法系列之八 MoreWindows白话经典算法之七大排序总结篇
在我的博客对冒泡排序,直接插入排序,直接选择排序,希尔排序,归并排序,快速排序和堆排序这七种常用的排序方法进行了详细的讲解,并做成了电子书以供大家下载.下载地址为:http://download.cs ...
- 排序算法:七大排序算法的PHP实现
由于最近在找工作,面试中难免会遇到一些算法题,所以就用PHP把七大排序算法都实现了一遍,也当做是一种复习于沉淀. 冒泡排序 2. 选择排序 3. 插入排序 4. 快速排序 5. 希尔排序 6. 归并排 ...
- Python排序搜索基本算法之归并排序实例分析
Python排序搜索基本算法之归并排序实例分析 本文实例讲述了Python排序搜索基本算法之归并排序.分享给大家供大家参考,具体如下: 归并排序最令人兴奋的特点是:不论输入是什么样的,它对N个元素的序 ...
- 排序算法总结(二)归并排序【Merge Sort】
一.归并排序原理(Wikipedia) 归并排序本质是分治思想的应用,并且各层分治递归可以同时进行 1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列 2.设定两个指针,最初位置 ...
- 【高级排序算法】2、归并排序法的实现-Merge Sort
简单记录 - bobo老师的玩转算法系列–玩转算法 -高级排序算法 Merge Sort 归并排序 Java实现归并排序 SortTestHelper 排序测试辅助类 package algo; im ...
- 【高级排序算法】1、归并排序法 - Merge Sort
归并排序法 - Merge Sort 文章目录 归并排序法 - Merge Sort nlogn 比 n^2 快多少? 归并排序设计思想 时间.空间复杂度 归并排序图解 归并排序描述 归并排序小结 参 ...
随机推荐
- 经典算法 KMP算法详解
内容: 1.问题引入 2.暴力求解方法 3.优化方法 4.KMP算法 1.问题引入 原始问题: 对于一个字符串 str (长度为N)和另一个字符串 match (长度为M),如果 match 是 st ...
- vb 使用StreamWriter书写流写出数据并生成文件
sql = "Select case when date ='' then '0'else CONVERT(varchar(100), date, 101) end as date,case ...
- openlayers3教材详解及demo(完整)
openlayers3教材详解及demo(完整) OpenLayers 3对OpenLayers网络地图库进行了根本的重新设计.版本2虽然被广泛使用,但从JavaScri ...
- spring boot 整合案例
github : https://github.com/nbfujx/springBoot-learn-demo
- SqlServer——触发器
一:触发器基本知识 1.首先必须明确以下几点: 触发器是一种特殊的存储过程,但没有接口(输入输出参数),在用户执行Inserted.Update.Deleted 等操作时被自动触发: 当触发的SQL ...
- Laravel基础
一.Laravel核心目录文件介绍 app:程序的核心代码和业务逻辑代码,其中的Http目录是我们业务逻辑的存放点 bootstrap:包含框架启动的和自动加载文件 config:包含所有程序中的配置 ...
- redis详解(一)-- 概述
首先,分布式缓存框架 可以 看成是nosql的一种 (1)什么是redis? redis 是一个基于内存的高性能key-value数据库. (有空再补充,有理解错误或不足欢迎指正) (2)Reids的 ...
- PHP依赖注入(DI)和控制反转(IoC)详解
这篇文章主要介绍了PHP依赖注入(DI)和控制反转(IoC)的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 首先依赖注入和控制反转说的是同一个东西,是一种设计模式,这种设计模式用来减少程 ...
- 机器学习入门-数值特征-数字映射和one-hot编码 1.LabelEncoder(进行数据自编码) 2.map(进行字典的数字编码映射) 3.OnehotEncoder(进行one-hot编码) 4.pd.get_dummies(直接对特征进行one-hot编码)
1.LabelEncoder() # 用于构建数字编码 2 .map(dict_map) 根据dict_map字典进行数字编码的映射 3.OnehotEncoder() # 进行one-hot编码 ...
- cv::circle《转》
void circle(CV_IN_OUT Mat& img, Point center, int radius, const Scalar& color, int thickness ...