排序算法大总结

来源:互联网 发布:js a.length 编辑:程序博客网 时间:2024/04/29 19:14

前面在看STL源码剖析时,发现自己对排序算法掌握得不好,遂花时间彻底的学习了一番,并做个全面的总结如下。

一.直接插入排序

插入排序的基本思想是:每步将一个待排序的纪录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。

算法的伪代码:

for j ←2 to n 
          do key ← A[ j] 
          i ← j – 1 
          while i > 0 and A[i] > key 
               do A[i+1] ← A[i] 
                    i ← i – 1 
          A[i+1] = key

分析:每次选择一个元素A[j]插入到之前已排好序的部分A[1…j-1]中,先将A[j]赋值到key变量,在插入过程中,key依次由后向前与A[1…j-1]中的元素进行比较,即从i=j-1、j-2,直到第一个元素0,若比较中发现key<A[i],则将A[i]往后移动,当key<A[i]不成立时,将key值放到i+1位置,即key应该插入的位置。

注意:此处是从后往前比较然后进行插入的,这样在数据基本有序的情况下算法的执行时间会很快。此算法是稳定的。

具体的复杂度分析如下:

最好情况:数据为完全正序,比较次数为n次,不需要移动元素,复杂度为O(n)。

最坏情况:数据为完全逆序,比较次数为1+2+..+n-1,复杂度为O(n­2)。

平均情况:O(n­2)。

下面是C++实现代码:

void insertsort(int a[],int n){int key;int i,j;for(i=1;i<n;++i){key=a[i];j=i-1;while(j>=0&&key<a[j]){a[j+1]=a[j];j--;}a[j+1]=key;}}

二、希尔排序(插入排序)

希尔排序思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量 d=1,即所有记录放在同一组中进行直接插入排序为止。
该方法实质上是一种分组插入方法。是直接插入排序的改进。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

最好情况:由于希尔排序的好坏和步长d的选择有很多关系,不能确切求出最好的情况下的算法时间复杂度。  
       最坏情况:O(N*logN)  
       平均情况:O(N*logN)

下面是希尔排序的c++代码:

void shellsort(int *a,int len){int i,j;int d=len/2;while(d>0){for(i=d;i<len;++i){j=i-d;while(j>=0&&a[j+d]<a[j]){swap(a[j+d],a[j]);j=j-d;}  }d=d/2;}}

三、冒泡排序(交换排序)

基本思想:通过无序区中相邻记录关键字间的比较和位置的交换,使关键字最小的记录如气泡一般逐渐往上“漂浮”直至“水面”。 

算法实现步骤:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。结束后,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
最好情况:复杂度为O(n)
最坏情况和平均情况:复杂度为O(n­2)
此时需要注意的是,最好情况下的复杂度和代码实现有一定关系,下面对此作出了分析。
实现代码一:
void bubblesort(int *a,int len){int i,j;for(i=0;i<len-1;++i){for(j=1;j<len-i;++j){if(a[j]<a[j-1])  swap(a[j-1],a[j]);}}}
此代码实现时,复杂度总为O(n­2),原因是此实现算法没有优化,当正序有序时,外循环指向n次,内循环也要执行n-i次。此实现中,也可先选出最小的放在最前面,接着次小的放在第二个位置。

实现代码二:

void bubblesort(int *a,int len){int i,j;bool didswap=false;for(i=0;i<len-1;++i){didswap=false;for(j=1;j<len-i;++j){if(a[j]<a[j-1]){swap(a[j-1],a[j]);didswap=true;}}if(didswap==false)  return;}}
代码二可以实现最佳情况下复杂度为O(n),代码中加入了一个判断,当内层循环中没有进行交换操作,则说明序列已经有序了,因此,算法结束,返回结果。

四、快速排序(交换排序)

基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

算法实现步骤:设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。

一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

一般情况:复杂度为 O(N*logN)

最坏情况:基本有序时,退化为冒泡排序,几乎要比较N*N次,故为O(N*N)。

快速排序算法实现有很多种,下面对各种实现一一进行分析。

实现一:递归实现,key为数组第一个元素

void quicksort(int *a,int first,int last){if(first>=last)return;int low=first,high=last;int key=a[first];while(low<high){while(low<high&&key<=a[high])  --high;a[low]=a[high];while(low<high&&key>=a[low])  ++low;a[high]=a[low];}a[low]=key;quicksort(a,first,low-1);quicksort(a,high+1,last);}
实现一是用第一个元素作为关键元素(枢纽),当第一个数据为极值时,会出现最坏的情况,算法的复杂度没有得到改进。在算法导论上,看到快速排序的实现和此方式有点不同,虽然也是递归算法,但二分区间的方式不同,于是也实现了一下。有点不太直观,借用了这个博客中的图方便理解:http://blog.sina.com.cn/s/blog_73428e9a01017f9x.html

快速排序-算法导论版本

实现代码为:

int partition(int *a,int first,int last){int key=a[last];int i=first-1;int j;for(j=first;j<last;j++){if(a[j]<=key){++i;swap(a[i],a[j]);}}swap(a[i+1],a[j]);return i+1;}void quicksort(int *a,int first,int last){if(first>=last)return;int mid=partition(a,first,last);quicksort(a,first,mid-1);quicksort(a,mid+1,last);}
上面都是递归实现的快速排序,但是基于非递归算法比对应的递归算法速度快的思想,因此尝试实现了非递归的快速排序。

实现二.非递归:

void quicksort(int *a,int size){stack<int> st;st.push(0);st.push(size-1);while(!st.empty()){int end=st.top;st.pop();int start=st.top;st.pop();int key=a[end];int i=start-1;int j;for(j=start;j<end;j++){if(a[j]<x){++i;swap(a[i],a[j];}}swap(a[i+1],a[end]);if(start<1){st.push(start);st.push(i);}if(i+2<end){st.push(i+2);st.push(end);}}}

但测试结果却是此非递归算法更慢,原因是STL容器是stack效率太低,因此,使用时最好用递归算法实现的代码。


实现三:随机化版本

快速排序大多时选择第一个元素或者最后一个作为主元,此处使用随机化选取主元的方式,这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度

int rand(int low,int high){int size=high-low+1;return low+rand()%size;}void quicksort(int *a,int first,int last){if(first>=last)return;swap(a[rand(first,last)],a[first]);int low=first,high=last;int key=a[first];while(low<high){while(low<high&&key<=a[high])  --high;a[low]=a[high];while(low<high&&key>=a[low])  ++low;a[high]=a[low];}a[low]=key;quicksort(a,first,low-1);quicksort(a,high+1,last);}

实现四:三平均分区法

是选择待排数组的第一个数作为中轴,而是选用待排数组最左边、最右边和最中间的三个元素的中间值作为中轴。这一改进对于原来的快速排序算法来说,主要有两点优势:
  (1) 首先,它使得最坏情况发生的几率减小了。
  (2) 其次,未改进的快速排序算法为了防止比较时数组越界,在最后要设置一个哨点。


五、直接选择排序

对于每一趟,选择未排序序列中的最小的数放到排序序列的最后面。即第一趟,选择数组所有元素中的最小值放到第一个位置,第二趟,选择第二个到数组尾中最小值(即次小值)放到第二个位置...直到最后一个。算法实现如下:

void selectsort(int a[],int first,int last){for(int i=0;i<last;++i){int min=i;for(int j=i+1;j<=last;++j){if(a[j]<a[min])min=j;}if(min!=i)swap(a[i],a[min]);}}


六、堆排序(选择排序)

直接看算法实现,分为三个子程序.

1、调整堆,对于一个存在子节点的节点,调整此节点的位置,使此节点所在的树满足大顶堆或者小顶堆的定义。下面程序时按升序排列,因此建立大顶堆。

void heapadjust(int a[],int first,int last){if(first>=last)return;int child=2*first+1;while(child<=last){if(child+1<=last&&a[child+1]>a[child])//选择子节点中较大的一个++child;if(a[first]<a[child])//父节点小于子节点{swap(a[first],a[child]);//交换first=child;child=2*child+1;}elsebreak;}for(int i=0;i<8;++i)cout<<a[i]<<" "; //方便查看结果cout<<endl;}
对于上述程序,可以有一点改进,如果父节点值较小,需要多次和子节点、子节点的子节点...进行交换,此处将父节点的值保存下来,等到最后其插入位置确定后才进行赋值操作,减少赋值操作次数。改进的调整堆程序如下:

void heapadjust(int a[],int first,int last){if(first>=last)return;int child=2*first+1;int temp=a[first];while(child<=last){if(child+1<=last&&a[child+1]>a[child])++child;if(temp<a[child])  //此处和上述程序不同,需注意{a[first]=a[child];first=child;child=2*child+1;}elsebreak;}if(a[first]!=temp)a[first]=temp;  //最后才进行赋值。for(int i=0;i<8;++i)cout<<a[i]<<" ";cout<<endl;}
2、建立堆

在堆排序中,建堆是第一步,但是建堆也需要使用调整堆程序,原因是建堆过程是从最后一个非叶子节点开始进行调整的,直到根节点。

void buildheap(int a[],int first,int last){for(int i=(last-1)/2;i>=0;--i)heapadjust(a,i,last);}
3、排序过程

先建立堆,然后将堆顶元素与最后一个未排序元素交换,然后对堆顶元素进行调整。如下:

void heapsort(int a[],int first,int last){buildheap(a,first,last);for(int i=last;i>0;--i){swap(a[i],a[0]);heapadjust(a,0,i-1);}}












0 0