Fork me on GitHub

个人总结之排序算法

前言

回顾之前学习的各种排序算法,从初级到高级,包括选择排序,冒泡排序,插入排序,希尔排序,快速排序,归并排序,堆排序等等,持续更新中…

注:这里实现的算法都是递增排序,也就是从小到大排序。

初级排序算法

1.选择排序

思想:首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static int [] sort(int a[],int length){ //选择排序
for(int i=0;i<length;i++){
int minIndex = i; //初始化最小元素的索引
for(int j=i+1;j<length;j++){
if(a[minIndex]>a[j]){
minIndex = j; //找到最小元素的索引
}
}
int tem = a[i];
a[i] = a[minIndex];
a[minIndex] = tem;
}
return a;
}

2.直接插入排序

思想: 每一步将一个待排序的记录,插入到前面应排好序的有序序列中去,直到查完所有元素为止。

代码:

第一种:从后往前依次比较前面排好序的有序序列,如果插入元素较小时,交换,j- -,继续比较。

1
2
3
4
5
6
7
8
9
10
public static int [] sort(int a[],int length){
for(int i=1;i<a.length;i++){
for(int j=i;j>0&&a[j]<a[j-1];j--){
int tem = a[j-1];
a[j-1] = a[j];
a[j] = tem;
}
}
return a;
}

第二种:不需要交换的直接插入排序,将内循环中较大的元素都向右移动而不总是交换两个元素,从而提高效率。

1
2
3
4
5
6
7
8
9
10
11
public static int [] sort(int a[],int length){ //不需要交换的插入排序
for(int i=1;i<a.length;i++){
int tem = a[i]; //待插入的元素
int j;
for(j=i-1;j>=0&&tem<a[j];j--){
a[j+1] = a[j]; //元素后移,直到找到待插入的元素的位置
}
a[j+1] = tem; //将带插入元素插入到查找到的位置
}
return a;
}

第三种:此外还可以通过增加哨兵的形式,在插入排序的实现中先找出最小的元素并将其置于数组的最左边,这样就能去掉内循环的判断条件j>0。这是一种常见的规避边界测试的方法,能够省略判断条件的元素通常被称为哨兵。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static int [] sort(int a[],int length){
int minIndex =0;
for(int i=1;i<length;i++){
if(a[minIndex]>a[i]){
minIndex = i;
}
}
int tem = a[0];
a[0] = a[minIndex];
a[minIndex] = tem;
for(int i=2;i<length;i++){
tem = a[i];
int j;
for(j=i-1;tem<a[j];j--){
a[j+1] = a[j];
}
a[j+1] = tem;
}
return a;
}

3.希尔排序

思想:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。

我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int[] sort(int a[],int length){
int h = length/2; //初始增量
while(h>=1){
//将数组变为h有序
for(int i=h;i<length;i++) {
//将a[i]插入到a[i-h],a[i-2*h],a[i-3*h]...之中
for(int j=i;j>=h&&a[j]<a[j-h];j-=h){
int tem = a[j-h];
a[j-h] = a[j];
a[j] = tem;
}
}
h = h/2; //每次排完序后,增量减少
}
return a;
}

参考:dreamcatcher-cx

归并排序

简介

归并排序,即将两个有序的数组归并成一个更大的有序数组,要将一个数组排序,可以(递归的)先将它分成两半分别排序,然后将结果归并起来。归并排序最吸引人的性质是它能够保证将任意长度为N的数组排序所需的时间和NlogN成正比;它的主要缺点则是它所需的额外空间和N成正比。

2-路归并排序

2-路归并排序:假设初始序列含有n个记录,则可看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2(整数值)个长度为2或1的有序子序列;在两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2-路归并排序。 如下为一个典型的例子。

该图显示的就是循环2-路归并排序算法的过程:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Mergesort { //2-路归并排序
public static int [] aux; //辅助数组
public static void merge(int a[],int lo,int mid,int hi){ //核心算法
//将a[lo..mid]和a[mid+1,hi](已有序)归并
int i = lo, j = mid+1;
for(int k = lo;k<=hi;k++){ //将a[lo..hi]复制到aux[lo..hi]
aux[k] = a[k];
}
for(int k=lo;k<=hi;k++){
if(i>mid) a[k] = aux[j++]; //左半边用尽,取右半边的元素复制到a中
else if(j>hi) a[k] = aux[i++]; //右半边用尽,取左半边的元素复制到a中
else if(aux[i]<aux[j]) a[k] = aux[i++]; //左半边元素小于右半边元素,取左半边元素复制到a中
else a[k] = aux[j++]; //右半边元素小于左半边元素,取右半边元素复制到a中
}
}
public static void Mergesort(int a[]){ //二路归并递归算法
aux = new int [a.length]; //一次性分配空间
sort(a,0,a.length-1);
}
private static void sort(int[] a, int lo, int hi) {
// 将数组a[lo..hi]排序
if(lo>=hi) return ;
int mid = lo + (hi-lo)/2;
sort(a,lo,mid); //递归将左半边排序
sort(a,mid+1,hi); //递归将右半边排序
merge(a, lo, mid, hi); //归并结果
}
public static void Mergesort1(int [] a) { //二路归并非递归算法
//进行lgN次两两归并
int N = a.length;
aux = new int [N];
for(int sz = 1; sz<N;sz = 2*sz){ //sz的子数组大小
for(int lo =0;lo<N-sz; lo+=2*sz){ //子数组的索引
merge(a, lo, lo+sz-1, Math.max(lo+2*sz-1, N-1));
}
}
}
public static void main(String[] args) {
int a[] = {2,3,5,1,4,0,7,6};
//Mergesort(a); //调用2-路归并递归排序函数
Mergesort1(a); //调用2-路归并非递归排序函数
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
}

一些改进

1.对小规模子数组使用插入排序,因为递归会使小规模问题中方法的调用过于频繁,所以改进对他们的处理方法就能改进整个算法。使用插入排序处理小规模子数组(比如长度小于15)。

2.测试数组是否已经有序,我们可以添加一个判断条件,如果a[mid]小于等于a[mid+1],此时前半部分有序数组最后一个数小于后半部分有序数组的第一个数,我们就认为数组已经是有序并跳过merge()方法。这个改动不影响排序的递归调用。

3.不将元素复制到辅助数组(暂时不太明白)。

快速排序

简介

快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立地排序。快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况中,递归调用发生在处理整个数组之后。在归并排序中,一个数组被等分成两半;在快速排序中,切分的位置取决于数组的内容。

关键算法

该方法的关键在于切分,这个过程使得数组满足下面的三个条件:

1.对于某个j,a[j]已经排定;

2.a[lo]到a[j-1]中的所有元素都不大于a[j];

3.a[j+1]到a[hi]中的所有元素都不小于a[j]。

代码

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
public class QuickSort {
public static int[] quicksort(int a[]){
sort(a,0,a.length-1);
return a;
}
private static void sort(int[] a, int lo, int hi) {
if(hi<=lo) return ;
int j=partition(a,lo,hi); //切分
//第j个位置已经在它所在的排好序的位置
sort(a,lo,j-1);
sort(a,j+1,hi);
}
private static int partition(int[] a, int lo, int hi) {
int part = a[lo]; //切分元素
int i = lo,j=hi+1; //左右扫描指针
int tem;
while(true){
//扫描左右,检查扫描是否结束并交换元素
while(a[++i]<part) { //从左到右(第一个元素除外)找到大于等于part的元素
if(i==hi) break;
}
while(a[--j]>part){ //从右到左找到小于等于part的元素
if(j==lo) break;
}
if(i>=j) break;
tem = a[i];
a[i] = a[j];
a[j] = tem;
}
tem = a[lo]; //将part=a[j]放入正确的位置
a[lo] = a[j];
a[j] = tem;
return j;
}
public static void main(String[] args) {
int []a = {4,5,4,6,1,3};
a = quicksort(a);
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
}
-------------本文结束感谢您的阅读-------------

本文地址:http://www.wangxinri.cn/2017/08/21/个人总结之排序算法/
转载请注明出处,谢谢!

梦想夹带眼泪,咸咸的汗水!