什么是排序?
排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。
1.排序的分类
排序分为两类:
- 内部排序:若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。
- 外部排序:若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
一般来说,外部排序只有数据量极大时会使用,一般情况下排序指的都是内部排序。
2.空间复杂度
1.类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
2.空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况。
3.在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间。
3.排序的稳定
待排序的记录序列中可能存在两个或两个以上关键字相等的记录。排序前的序列中arr[i]领先于arr[j](即i<j)。若在排序后的序列中arr[i]仍然领先于arr[j],则称所用的方法是稳定的。
比如int数组[1,1,1,6,4]中arr[0],arr[1],arr[2]的值相等,在排序时不改变其序列,则称所用的方法是稳定的。
- 稳定的排序:冒泡排序,插入排序,归并排序,基数排序,计数排序
- 不稳定的排序:快速排序,希尔排序,选择排序,堆排序
稳定性设计到排序的现实意义,举个例子:
例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。
更多关于稳定性的理解可以参考这个
4.各排序时间复杂度概览
排序法 | 平均时间 | 最差情形 | 是否稳定 | 优先选择条件 |
---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | 稳定 | n小时较好 |
交换排序 | O(n^2) | O(n^2) | 不稳定 | n小时较好 |
选择排序 | O(n^2) | O(n^2) | 不稳定 | n小时较好 |
插入排序 | O(n^2) | O(n^2) | 稳定 | 大部分已排序时较好 |
基数排序 | O(logRB) | O(logRB) | 稳定 | B是真数(0-9),R是基数(个十百) |
希尔排序 | O(nlogn) | O(ns)1<s<2 | 不稳定 | s是所选分组 |
快速排序 | O(nlogn) | O(n2) | 不稳定 | n大时较好 |
归并排序 | O(nlogn) | O(nlogn) | 稳定 | n大时较好 |
堆排序 | O(nlogn) | O(nlogn) | 不稳定 | n大时较好 |
一、冒泡排序
冒泡排序是一种简单的排序算法,它也是一种稳定排序算法。其实现原理是重复扫描待排序序列,并比较每一对相邻的元素,当该对元素顺序不正确时进行交换。一直重复这个过程,直到没有任何两个相邻元素可以交换,就表明完成了排序。
1.举个例子
要对10,-1,8,3这四个数进行排序:
第一次排序:
-1,10,8,3 //比较10和-1,逆序则交换
-1,8,10,3 //比较10和8
-1,8,3,10 //比较10和3
第一次排序结束,确定了四个数里最大的数的位置
第二次排序:
-1,8,3,10 //比较-1和8,不逆序所以不需要移动
-1,3,8,10 //比较8和3
由于已经确定了10为最大数,所以只需要比较到倒数第二位。
第二次排序结束,确定了第二大的数的位置
第三次排序:
-1,3,8,10 //比较-1和3
由于已经确定了第三和第四大的数,所以只需要比较到倒数第三位。
第三次排序结束,确定了第三大的数,故第一大的数也随之确定
2.思路
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
3.实现代码
1 | /** |
二、选择排序
选择排序,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
1.举个例子
选择排序(select sorting)也是一种简单的排序方法。 它的基本思想是: 第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换; 第二次从arr[1]~arr[n-1]中选取最小值,与arr[1]交换; 第三次从arr[2]~arr[n-1]中选取最小值,与arr[2]交换; … 第i次从arr[i-1]~arr[n-1]中选取最小值,与arr[i-1]交换; … 第n-1次从arr[n-2]~arr[n-1]中选取最小值,与arr[n-2]交换, 总共通过n-1次,得到一个按排序码从小到大排列的有序序列。
2.思路
- 需要进行n-1轮排序
- 若要为第i个数排序,就先默认第i个元素为最小数,记录其大小和下标
- 接着从第i+1到第n个数开始依次比较,如果有数小于最小数,则用该数替换原最小数和其下标
- 第i轮比较结束后,让找出的最小数与第i个数交换位置
3.代码实现
1 | /** |
4.与冒泡排序比较
同样对长度80000的数字进行排序,选择排序比冒泡排序快不少,原因在于选择排序每次排序只移动指针,找到位置后才进行一次元素交换,而冒泡需要多次交换。
换句话说,要排序的数组越长,冒泡每次排序元素要移动的次数就越多,与选择排序的差距就越明显。
这个同样能解释希尔排序的两种实现方式的速度差距。
1 | //冒泡排序交换值,交换n次值 |
三、插入排序
插入排序,是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表
1.举个例子
有数组{5,2,4,6,1,3}要进行排序,
- 从第二位往前看,5比2大,且5为第一个数,于是5后移,把2插入5前。现在是{2,5,4,6,1,3}
- 从第三位往前看,5比4大,于是5后移,继续往前看,2比4小,所以把4插入原先5的位置。现在是{2,4,5,6,1,3}
- 从第四位往前看,4比6小,于是6就不动了。现在是{2,4,5,6,1,3}
- 从第五位往前看,6比1小,于是6后移,继续往前看,5比1小,5后移,继续往前看.....2比1小,2后移,又2为第一个数,于是把1插入原本2的位置。现在是{1,2,4,5,6,3}
- 从第六位往前看,6比3小,于是6后移,继续往前看,.....3比2大,于是把3插入原先4的位置。排序完成。
2.思路分析
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
3.代码实现
1 | /** |
四、希尔排序
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
希尔排序是把记录按下标的一定增量分组, 对每组使用直接插入排序算法排序; 随着增量逐渐减少,每组包含的关键词越来越多, 当增量减至1时,整个文件恰被分成一组,算法便终止。
1.举个例子
2.思路
- 将数组除于2进行分组,得到gap组数字
- 对gap组数字进行插入排序,由于数据共分为gap组,所以同一组相邻的数字在数组中的位置总是相隔gap
- 遍历gap组数字,表现在数组上就是从gap遍历到arr.length
3.代码实现
有两种实现思路,一种是交换法,一种是移位法,
先说结论:移位法比交换法快,原因在于:
交换法思路有点类似于冒泡排序,需要不断的比较并交换数值,
而移位法即选择排序,仅仅遍历赋值后移动指针,找到插入位置后,再把元素插入到有序表,而不是多次交换加入有序表。
1 | //交换法交换n次值 |
3.1交换法实现
1 | /** |
3.2 移位法实现
下面是通过移位法实现的排序,比交换法更快更稳定:
1 | /** |
五、快速排序
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
1.举个例子
- 先选择一个中间位置数11,分别将数组中比11大和比11小的数放到左右两个数组中
- 对两个数组分别选一个中间位置数,也就是5和21,各自再根据比中间数小或者比中间数大再次分为两个数组。以此类推
2.思路
- 把长度为n的输入序列分成两个长度为n/2的子序列
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列
- 对左支和右支可通过递归实现排序
3.代码实现
1 | public static int[] sort(int[] arr) { |
六、归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
1.举个例子
我们以上图最后一次合并为例:
以上多个有序数组间的合并需要进行多次,通过递归完成
2.思路
- 把长度为n的数组分成两个长度为n/2的子数组;
- 如果子数组仍然长度大于1,就重复步骤1直到所有数组都被拆分完毕
- 将拆分后的元素两两合并为一个有序数组,然后相邻两个数组A,B进行合并:
- 创建一个新数组,然后遍历B数组并与A数组第一位进行比较,如果该数字比A数组第一小则放入新数组第一位
- 否则将A数组第一位放入新数组
- 重复上面步骤3直到A,B数组所有元素都有序放入新数组,即合并完成
- 重复步骤3,直到所有数组都最终都被合并为一个有序数组
3.代码实现
1 | public static int[] sort(int[] arr) { |
七、基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
1.举个例子
2.思路
- 取得数组中的最大数,并取得位数
- 准备一个长度为10,内部一位数组长度为arr.length的二维数组,可以理解为10个高度为arr.length的桶
- 遍历数组,根据数组中个位数决定要放在哪个桶,即如果个位数为1就放入1号桶,为2就放入2号桶,直到数组所有元素分配完毕
- 遍历桶将桶中数字放回原数组,然后清空桶。即完成了个位数的排序
- 重复步骤3和步骤4,但是排序依据从个位数换成十位数,然后百位数.....以此类推,直到数组最大位数
3.代码实现
1 | /** |
4.基数排序注意事项:
基数排序是典型的空间换时间,当排序的数字过多的时候可能会发生
OutOfMemoryError
(实测八千万时报错)基数排序要排负数的时候需要加以改进:
将数组中的负数单独摘出并取绝对值后进行排序,然后倒序插入排序完的整数数组,并且在插入过程加上负号
八、堆排序
参照二叉树部分