参考:1·《算法(第四版)》 2· MoreWindows Blog海纳百川的博客——白话经典算法系列
排序算法小结
1·选择排序
(1)核心思想
选择排序的核心思想是选择剩余元素中最小的一个放在已经有序的部分的下一个位置。例如,2,3,4,5,9,23,12,8,在这一组数中,2345的部分已经有序,接下来选择排序需要完成的工作就是在9,1,23,12,8中把8找到并放在5的后边,之后把9找到放在8的后边,再然后把12找到放在9的后边,直到完成排序。
(2)代码实现
public staticvoidselection_sort(Comparable[] a)
{
int n=a.length;
for(int i=0;i<n;i++)
{
int min=i;
for(int j=i+1;j<n;j++)
{
if(less(a[j],a[min]))
min=j;
exch(a,i,min);
}
//exch(a,i,min);
show(a);
}
}
内循环的任务:找到最小元素的标记j存入到min中去
外循环的任务:完成当前元素与min标记的最小元素的交换
(3)性能分析
由于需要找到剩余元素中最小的那一个,所以必须对数组进行遍历。于是,对于一个长度为n的数组而言,在第一次遍历数组寻找最小的元素时,需要对数组进行(n-1)次访问,在第二次遍历数组寻找最小的元素时,需要对数组进行(n-2)次访问,在第三次遍历数组寻找最小的元素时,需要对数组进行(n-3)次访问,……,在第(n-1)次遍历数组寻找最小的元素时,需要对数组进行1次访问,然后结束。由此可见,对数组的访问总共要(n-1)+(n-2)+(n-3)+……+2+1=n*(n-1)/2~n2/2。同时,每当进行一次访问时就需要进行一次比较,所以,比较的次数总计也是n*(n-1)/2~n2/2。因此,随着数据规模的增大,对于数组的访问和数据的比较次数会迅速增长(平方级别)。
其优点是,不论数据的多少,交换次数与数据的多少呈线性关系——每一次遍历最多进行一次交换,总共最多n-1次交换;另外,运行时间不会因数组内容的改变而改变,只要还是那个长度,运行时间就一定还是那么多。
2·插入排序
(1)核心思想
插入排序的核心思想是将待处理元素插入到已经有序的部分中的合适的位置上,为了完成这一步就需要将数组元素进行移动。例如,2,3,4,5,9,23,12,8,为了把8插入到9前边去,就需要将8和12换位,再与23换位,再与9换位即可。当所有元素完成换位之后,数组就完成了排序。(换句话说,在循环的时候,只要找到比目标元素小的元素就进行交换。)
(2)代码实现
public staticvoidinsert_sort(Comparable[] a)
{
int n=a.length;
for(int i=0;i<n;i++)
{
for(int j=i;j>0&&less(a[j],a[j-1]);j--)
{
exch(a,j,j-1);
}
show(a);
}
}
内循环的任务:完成新添元素的插入(比较和交换)
(3)性能分析
对于随机序列长度为n且主键不重复的数组,平均情况下插入排序需要~ n2/4次比较和~ n2/4次交换。最坏情况下为~ n2/2次比较和~ n2/4次交换;最好情况下n-1次比较和0次交换。
在最坏情况下,所要寻找的元素总是出现在另外一侧,也即是说需要遍历剩余元素之后才能找到该元素。于是,就有(n-1)+(n-2)+(n-3)+……+2+1=n*(n-1)/2~n2/2;因为每比较一次就交换一次,所以比较次数和交换次数是相同的。
最好的情况时,就是数组已经有序,这时再插入排序完全没有意义。(其实,最坏情况时,数组也是有序的,只不过这个顺序与你想要的顺序刚好相反。)
3·希尔排序
(1)核心思想
希尔排序的实质是步长为h的插入排序。在普通的插入排序中,比较和交换只发生在相邻的两个元素之间,这就意味着,当你所寻找的元素在另一端的时候,需要进行更多的比较和交换。当设置步长为h时,就可以减少比较和交换的次数。
(2)代码实现
public staticvoidShell_sort(Comparable[] a)
{
int n=a.length;
int h=1;
while(h<n/3)
h=3*h+1;
while(h>=1)
{
for(int i=h;i<n;i++)
{
for(int j=i;j>=h&&less(a[j],a[j-h]);j-=h)
{
exch(a,j,j-h);
}
}
h=h/3;
}
}
希尔排序的关键:步长h的控制
(3)性能分析
(无)
4·归并排序
(1)核心思想
归并,就是指把两个数组合并为一个数组。那么,当合并的两个数组都是有序数组的时候,最终得到的合并之后的数组也是有序的。当然,在合并时还需要简单的控制。于是,为了得到这样两个有序的数组,就需要把这样两个子数组进行排序,进而形成递归。所以,归并排序的核心是递归。
(2)代码实现
private staticvoidmerge(Comparable[] a,int lo,intmid, inthi)
{
Comparable[]aux;
aux=new Comparable[a.length];
int i=lo;
int j=mid+1;
for(int k=lo;k<=hi;k++)
{
aux[k]=a[k];
}
for(int k=lo;k<=hi;k++)
{
if(i>mid)//左半边用尽取右半边
a[k]=aux[j++];
else if(j>hi)//右半边用尽取左半边
a[k]=aux[i++];
else if(less(aux[j],aux[i]))//右半边当前元素小于左半边当前元素
a[k]=aux[j++];
else//左半边当前元素小于右半边当前元素
a[k]=aux[i++];
}
}
/*归并排序*/
public static void merge_sort(Comparable[] a,int lo, int hi)
{
if(hi<=lo)
return;
int mid=lo+(hi-lo)/2;
merge_sort(a,lo,mid);
merge_sort(a,mid+1,hi);
merge(a,lo,mid,hi);
}
归并排序的关键在于:1·归并(原地归并和两个数组归并为第三个数组)2·归并排序的递归
原地归并:数组a[lo,……,mid,mid+1,……,hi],分为两组a[lo……mid]和a[mid+1,……hi](也即是两数组归并时的数组a和b),然后归并为数组aux,最后将归并完成的数组aux的值全部复制回数组a即可。
(3)性能分析
所以,对于长度为n的数组而言,归并排序最多需要nlgn次比较和6*nlgn次对数组的访问。
5·快速排序
(1)核心思想
快速排序的基础也是递归。与归并不同的是,归并要先递归后合并,而快排则是先划分后递归。(另外,划分方式也不同:归并划分时尽可能等分,而快排的划分则是取决于内容。)快速排序,其思路是当由原数组划分得到的两个数组有序时,整个数组就有序了。因此,快排需要先划分后递归。
(2)代码实现
private static int partition(Comparable[]a,intlo,inthi)
{
int i=lo,j=hi+1;
Comparablev=a[lo];
while(true)
{
while(less(a[++i],v))
if(i==hi)break;
while(less(v,a[--j]))
if(j==lo)break;
if(i>=j)break;
exch(a,i,j);
}
exch(a,lo,j);
return j;
}
public static void quick_sort(Comparable[]a,intlo,inthi)
{
//StdRandom.shuffle(a);
if(hi<=lo)return;
int j=partition(a,lo,hi);
quick_sort(a,lo,j-1);
quick_sort(a,j+1,hi);
}
快排的关键在于切分——从开头找比切分元素大的元素,从末尾找比切分元素小的元素,然后完成这两个元素的交换,直到结束。当结束时,最中间的元素就是切分元素。
(3)性能分析
对于长度为n的无重复数组而言,快排的平均比较次数与nlnn呈线性关系。最坏情况下,需要n2/2次比较。