数据结构与算法总结4_排序算法

来源:互联网 发布:彩票号码组合软件 编辑:程序博客网 时间:2024/06/05 02:12

0.

前面已经介绍了基本的数据结构,接下来会正式进入算法部分。
算法部分将分成排序,查找,图和字符串四个部分进行介绍。
这篇博客将会介绍10种不同的排序算法。选择排序,冒泡排序,插入排序,希尔排序,归并排序,快速排序,堆排序,计数排序,基数排序,桶排序。

受这篇博客的启发
http://blog.csdn.net/whuslei/article/details/6442755
我的这篇博客会对每种算法进行四个方面的介绍:
1、思想是什么?
2、具体代码?
3、算法特点什么?稳定性怎样?时间复杂度是多少?
4、在什么情况下,算法出现最好情况 or 最坏情况?

在最开始先介绍一下稳定性的概念。
如果一个排序算法能够保留数组中重复元素的相对位置则可以被称为稳定的。
一般认为冒泡、插入和归并排序是稳定的,选择、希尔、快速、堆排序是不稳定的。
这里需要强调一点:如果想判断一个排序算法到底稳定与否,你需要去看这个排序算法的具体实现。因为有很多办法能够将任意排序算法变成稳定的。

说明一点:
N(N1)/2=1+2+...+N
便N(N1)/2N2/2

1.选择排序

(1).思想

选择排序:首先,找到数组中最小的那个元素,然后将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到整个数组有序。这种方法叫做选择排序,因为它在不断地选择剩余元素之中的最小者。
下面这张图是对一个具体的数组进行选择排序的轨迹图:
(灰色字母代表最终位置已确认)
这里写图片描述

(2).代码实现:

void exch(int a[],int i,int j){    int temp;    temp=a[i];    a[i]=a[j];    a[j]=temp;}void SelectSort(int a[],int n)//a为待排序数组,n为待排序数组长度{    for(int i=0;i<n;i++)    {        int min=i;        for(int j=i+1;j<n;j++)            if(a[j]<a[min])                min=j;.        exch(a,i,min);    //将a数组的第i个元素与第min个元素进行交换    }}

(3).算法特点什么?稳定性怎样?时间复杂度是多少?

选择排序是一种很容易理解和实现的简单排序算法,它有两个很鲜明的特点。
第一个特点是选择排序的运行时间和输入无关。一个已经有序的数组和一个元素随机排列的数组所用的排序时间是一样长的。
第二个特点是数据移动是最少的。选择排序所用的交换次数为N次(和数组的大小是线性关系)。

稳定性:
通常说选择排序是不稳定的。
不过仔细读一下上面的代码,你会发现在整个过程中,我们的算法不会改变数组中重复元素的相对位置。这也就是说我们实现的选择排序是稳定的。

这也映证了在最开始强调的一点:
如果想判断一个排序算法到底稳定与否,你需要去看这个排序算法的具体实现。因为有很多办法能够将任意排序算法变成稳定的。

时间复杂度:
对于长度为N的数组,选择排序需要大约N2/2次比较和N次交换,所以时间复杂度是O(N2)。

(4).在什么情况下,算法出现最好情况 or 最坏情况?

对于选择排序不存在最好或者最坏情况。

2.冒泡排序

(1).思想

通过对数组相邻元素的比较和位置的交换,使数组中最大的元素如气泡一般逐渐“上浮”直至“水面”。

冒泡排序算法的流程(摘自百度百科):
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一步之后,数组中最后的元素应该会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

冒泡排序大家都已经很熟悉了,就不说具体的例子了。
(下面这个链接介绍了一个详细的例子,下面那张炫酷的图片同样出自这个链接
http://www.cnblogs.com/kkun/archive/2011/11/23/2260280.html)
这里写图片描述

(2).代码实现:

void exch(int a[],int i,int j){    int temp;    temp=a[i];    a[i]=a[j];    a[j]=temp;}void BubbleSort1(int a[],int n)//反向冒泡{    for(int i=0;i<n;i++)        for(int j=n-1;j>i;j--)            if(a[j]<a[j-1])                exch(a,j-1,j);}void BubbleSort2(int a[],int n)//正向冒泡{    for (int i = 0; i < n; i++)        for (int j = 0; j < n - 1 - i; j++)            if(a[j] > a[j + 1])                exch(a,j,j+1);}//这是两种实现方式,我更喜欢第一个。

(3).算法特点什么?稳定性怎样?时间复杂度是多少?

冒泡排序的特点,嗯,让我想一想,慢是不是算是一个特点呢?

稳定性:
一般来说冒泡排序是稳定的,对上面的代码来说,它也是稳定的。

时间复杂度:
对长度为N的数组来说,冒泡排序需要完成N2/2次比较,和(数组的逆序对个数)次交换,时间复杂度为O(N2)。

(4).在什么情况下,算法出现最好情况 or 最坏情况?

最好情况:当待排序数组是完全正序的时候,这时只需要完成N2/2次比较和0次交换。
最坏情况:当待排序数组是完全倒序的时候,这时需要完成N2/2次比较和N2/2次交换。

(5).冒泡算法的改进

改进是转自http://blog.csdn.net/hguisu/article/details/7776068

对冒泡排序常见的改进方法是加入一标志性变量exchange,用于标志某一趟排序过程中是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。
这里再提供一种改进算法:
设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。

改进后算法如下:

void Bubble_1 ( int r[], int n) {      int i= n -1;  //初始时,最后位置保持不变      while ( i> 0) {           int pos= 0; //每趟开始时,无记录交换          for (int j= 0; j< i; j++)              if (r[j]> r[j+1]) {                  pos= j; //记录交换的位置                   int tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;              }           i= pos; //为下一趟排序作准备       }   }   

3.插入排序

(1).思想

思想:人们整理桥牌的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。插入排序就是这么做的,将每一个元素插入到其他已经有序的元素序列中的合适位置。
下面是插入排序的轨迹图:
这里写图片描述

(2).代码实现

void exch(int a[],int i,int j){    int temp;    temp=a[i];    a[i]=a[j];    a[j]=temp;}void InsertionSort(int a[],int n)//a为待排序数组,n为待排序数组长度{    for(int i=0;i<n;i++)        for(int j=i;(j>0)&&(a[j]<a[j-1]);j--)                exch(a,j-1,j);        //当(a[j]>=a[j-1])时,说明第i个元素已经在合适位置了,内层循环就可以结束了。}//可以发现选择冒泡插入,这三种排序算法的代码非常的相似。不过一般情况下,插入排序在内层循环的执行次数要上比选择和冒泡的次数要少(插入排序的内层循环的条件更严格)。

(3).算法特点什么?稳定性怎样?时间复杂度是多少?

特点:
插入排序所需的时间取决于输入中元素的初始顺序。
插入排序对于部分有序的数组十分高效,也很适合小规模数组。

稳定性:
一般认为插入排序是稳定的。上面我们的代码也是稳定的。

时间复杂度:
对于随机排列的长度为N切主键不重复的数组,平均情况下插入排序需要N2/4次比较和N2/4次交换。(平均情况很容易理解,最好和最坏情况之和除以2)。

(4).在什么情况下,算法出现最好情况 or 最坏情况?

当待排序数组已经是排好序的时候,这是最好情况,只需要N-1次比较和0次交换。
当待排序数组是完全倒序的时候,这是最坏情况,需要N2/2次比较和N2/2次交换。

4.希尔排序

(1).思想

对于大规模乱序数组,插入排序的执行速度很慢,因为它只会交换相邻的元素,因此元素只能一点一点从数组的一端移动到另一端。例如,如果数组中的最小元素在最右边,要将它挪到正确的位置需要移动N-1次。这是非常低效的。

希尔排序是一种基于插入排序的快速排序算法。
思想:
使数组中任意间隔为h的元素都是有序的(当h很大时,我们就可以将元素移动到很远的地方)。h逐渐递减,当h递减至1时,我们就可以把数组排好序了。

举个例子:
这里写图片描述

最开始的时候,h为13,先让间隔为13的元素有序。
第1个元素S和第14个元素P,因为S>P所以需要交换。
第2个元素H和第15个元素L。
第3个元素E和第16个元素E。
数组到最后了。
h递减为4
第1个元素P,第5个元素L,第9个元素,第13个元素
这四个元素进行排序。(在下面的代码中,使用的是插入排序对这四个元素进行排序)
第2个元素,第6个元素,第10个元素,第14个元素
这四个元素进行排序
…. ….
h递减为1
最后执行一遍插入排序。

如果还是看不明白,结合代码再仔细看一下。

(2).代码实现

void exch(int a[],int i,int j){    int temp;    temp=a[i];    a[i]=a[j];    a[j]=temp;}void ShellSort(int a[],int N){    int h=1;    while(h<N/3)        h=3*h+1;        //h:1,4,13,40,121..确定h的起始大小        //这只是h的一种取法,还有很多其他不同的取法    while(h>=1)    {        for(int i=h;i<N;i++)        {            for(int j=i;(j>=h)&&(a[j]<a[j-h]);j=j-h)                exch(a,j-h,j);        }        h=h/3;    }}

(3).算法特点什么?稳定性怎样?时间复杂度是多少?

特点:
我们无法确切描述其对于乱序数组的性能特征。

稳定性:
希尔排序是不稳定的。

时间复杂度:
关于这个,目前最重要的结论是它的运行时间达不到平方级别。它和h序列的选取是有关的。

关于h序列,还需要强调一点,没有办法证明某个序列是最好的,所以可以有很多选择。

5.归并排序

(1).思想

将一个数组排序,可以先(递归地)将它分成两半分别排序,然后将结果归并起来。

基于这种思想,归并排序有两种实现方式:递归和非递归。

(2).代码实现

void min(int a,int b){    if(a<b)        return a;    return b;}void merge(int a[],int lo,int mid,int hi)//将子数组a[lo...mid]和a[mid+1...hi]归并成一个有序数组并将结果存放在a[lo..hi]中{    int i=lo;    int j=mid+1;    int axu[hi-lo+1];    for(int k=lo;k<=hi;k++)          axu[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(a[i]<a[j])            a[k]=aux[i++];        else            a[k]=aux[j++];    }       }void MergeSort_digui(int a[],int lo,int hi)//自顶向下,递归{    if(hi<=lo)        return;    int mid=lo+(hi-lo)/2;    MergeSort_digui(a,lo,mid);    MergeSort_digui(a,mid+1,hi);    merge(a,lo,mid,hi);}void MergeSort_feidigui(int a[],int N)//自底向上,非递归{    for(int sz=1;sz<N;sz=sz+sz)        for(int lo=0;lo<N-sz;lo=lo+sz+sz)            merge(a,lo,lo+sz-1,min(lo+sz+sz-1,N-1));}

(3).算法特点什么?稳定性怎样?时间复杂度是多少?

特点:
它能保证将任意长度为N的数组排序所需时间和NlogN成正比。主要缺点是在实现归并操作时需要额外的与N成正比的空间。

稳定性:
是一种稳定排序算法,我们的实现代码也是稳定的。

时间复杂度:
O(NlogN)
大概说一下NlogN怎么算的(不是特别精确)。
完成一个归并排序
大概需要执行归并操作(调用merge函数) logN次 (这个通过二叉树的树高可以算出来)。
每一次归并操作大概会访问数组N的正比次。
所以复杂度与Nlog(N)成正比。

(4).在什么情况下,算法出现最好情况 or 最坏情况?

对归并排序来说,最坏也是NlogN的倍数,所以就不讨论这个问题了。

6.快速排序

(1).思想

快速排序:当两个子数组都有序时整个数组也就自然有序了。
你可能会对这个提出质疑,当两个子数组都有序时整个数组也有可能是无序的啊,这不是扯着玩么。是的,但这里是需要强调一点:快速排序中所说的两个子数组是经过精心设计的,而不是随意的子数组。

细心会发现,这和归并排序非常的像。
归并排序是将数组分成两个子数组分别进行排序,并将有序的子数组归并以实现整个数组排序。(递归调用发生在处理整个数组之前)
而快速排序是先对整个数组进行特定的处理,得到两个子数组,再对子数组进行排序。只要子数组有序,那么整个数组就有序了。(递归调用发生在处理整个数组之后)

下面是快速排序示意图。
这里写图片描述

快速排序中,重点是:如何对整个数组进行处理,使“只要子数组有序,那么整个数组就自然有序”。这个处理通常叫做切分(partition)。
这里的切分包含了两个内容:先对数组进行处理,然后再进行切分。就是从数组中选择一个切分元素,比这个元素小的放在左边,比这个元素大的放在右边。这样就有了两个子数组,这两个子数组有序了,整个数组就自然有序了。

这里对上面的示意图做一点说明,shuffle操作时将数组元素的顺序打乱,为了保证切分元素选取的随机性。

(2).代码实现:

void exch(int a[],int i,int j){    int temp;    temp=a[i];    a[i]=a[j];    a[j]=temp;}int partition(int a[],int lo,int hi){    int i=lo;    int j=hi+1;    int v=a[lo];   //作为切分元素    while(true)    {        while(a[++i]<v)            if(i==hi)                break;        while(a[--j]>v)            if(j==lo)                break;        if(i>=j)            break;        exch(a,i,j);    }    exch(a,lo,j);    return j;   }void QuickSort(int a[],int lo,int hi){    if(hi<=lo)        return;    int j=partition(a,lo,hi);    QuickSort(a,lo,j-1);    QuickSort(a,j+1,hi);}

(3).算法特点什么?稳定性怎样?时间复杂度是多少?

特点:
将长度为N的数组排序所需时间与NlgN成正比,而且只需要很少的额外空间。
它的缺点是比较脆弱,一不小心,就可能会使算法的性能达到平方级别。

稳定性:
很容易就可以看出了,它是不稳定的。

时间复杂度:
O(NlgN)
将长度为N的无重复数组排序,快速排序平均需要~2NlnN次比较。

(4).在什么情况下,算法出现最好情况 or 最坏情况?

最好情况:每次切分的时候都能把数组切分成完全相等的两个子数组。O(NlgN)
最坏情况:每次切分后两个子数组之一总是空。这样最多需要N2/2次比较。快速排序的性能将会达到 O(N2)。 通过随机打乱数组能够预防这种情况。
补充一点:对于大数组,运行时间是平方级别的概率小的可以忽略不计。

(5).快速排序的一些改进

1.当子数组小于一定程度时,可以切换成插入排序。
2.通过选取一部分元素的中位数作为切分元素,避免最坏情况的发生。

3.当数组中存在大量重复元素时,采用三向切分的快速排序
三向切分示意图如下:
这里写图片描述

//三向切分,代码不是特别容易理解。void Quick3way(int a[], int lo, int hi){    if (hi<=lo)        return;    int lt=lo;    int i=lo+1;    int gt=hi;    int v=a[lo];    while(i<=gt)    {        if(a[i]<v)            exch(a,lt++,i++);        else if(a[i]>v)            exch(a,i,gt--);        else            i++;    }    Quick3way(a,lo,lt-1);    Quick3way(a,gt+1,hi);}

7.堆排序

(1).思想

堆排序分成两个阶段:堆的构造和下沉排序。
在堆的构造阶段中,我们将原始数组重新组织安排进一个堆中;然后在下沉排序阶段,我们从堆中按递减顺序取出所有元素并得到排序结果。

这里所说的堆是一棵堆有序的二叉树。什么又叫堆有序呢?当一棵二叉树的每个节点都大于等于它的两个子节点时,它被称为堆有序。

再换种说法描述堆排序:第一步先建一个堆有序的二叉树(下图左边);第二步按从大到小的顺序依次从这个二叉树中取出元素,完成排序(下图右边两栏)。
请对照代码,看下面的堆排序示意图。
这里写图片描述

在堆的有序化的过程中,需要这么两个操作。
1.当某个节点的值变大了的时候,为了维持堆有序,我们需要由下至上恢复堆的顺序。上浮(swim)操作。
这里写图片描述

2.当某个节点的值变小了的时候,为了维持堆有序,我们需要由上至下恢复堆的顺序。下沉(sink)操作。
这里写图片描述

(2)代码实现:

//基于数组实现堆排序//用长度为N+1的数组a[]表示一个大小为N的堆int less(int a[],int i,int j){    if(a[i]<a[j])        return 1;    return 0;}void exch(int a[],int i,int j){    int temp;    temp=a[i];    a[i]=a[j];    a[j]=temp;}void swim(int a[],int k,int N)//上浮{    while((k>1)&& less(a,k/2,k))// k/2是其父节点所在位置,判断是否大于其父节点值    {        exch(a,k/2,k);        k=k/2;    }}void sink(int a[],int k,int N){    while(2*k<=N)    {        int j=2*k;        if((j<N)&& less(a,j,j+1))            j++;        if(!less(a,k,j))//父节点的值大于等于子节点的值(已经有序)            break;        exch(a,k,j);        k=j;    }}void HeapSort(int a[],int N)//结合前面的图去理解代码{    for(int k=N/2;k>=1;k--)  //先将无序数组变成堆有序的二叉树        sink(a,k,N);    while(N>1)    {        exch(a,1,N--);//根节点为最大元素,和最后一个元素进行交换        sink(a,1,N);  //保持堆有序    }}

(3).算法特点什么?稳定性怎样?时间复杂度是多少?

特点:
通过构建堆有序的二叉树完成排序。
它是我们所知的唯一能够同时最优地利用空间和时间的方法(在最坏的情况下,它也可以保证 时间复杂度是O(NlgN) 和 只使用恒定的额外空间)

稳定性:
不稳定

时间复杂度:
O(NlgN)

(4).在什么情况下,算法出现最好情况 or 最坏情况?

因为堆排序会将原始数组转换成堆有序的二叉树,所以感觉讨论最好情况与最坏情况没什么意义。只需要知道的是:在最坏的情况下,它也可以保证 时间复杂度是O(NlgN) 。

8.几种线性时间排序

前面所说的七种排序算法,在其排序结果中,各元素的次序都是基于输入元素间的比较。我们把这类排序算法称为比较排序。
通过二叉树的模型可以证明比较排序的下界是O(NlgN)的。(算法导论,p98)
除了比较排序外,这里再介绍三种基于非比较操作来确定排序顺序的算法。

计数排序
http://www.cnblogs.com/wuyudong/p/counting-sort-algorithm.html

基数排序
http://blog.csdn.net/qunxingvip/article/details/51823347
需要注意,其中按位排序算法的稳定性对基数排序非常重要。

桶排序
http://www.cnblogs.com/hxsyl/p/3214379.html

这几个算法理解起来都很简单,所以就不具体写了。

说到这三种算法的稳定性的时候,好像只会提起基数排序是稳定的。而计数和桶排序我搜了一圈,都没有找到关于它们稳定性的答案。
我只知道有具体的实现可以使计数排序和桶排序成为稳定的排序算法,但我不知道“一般情况下,它们是否稳定”。

9.总结

这篇长博客终于要结束了。
关于总结,其实也没什么要写的。
只要记住,插入,归并,冒泡,基数是稳定的排序算法。(因为一般认为和具体实现的稳定性可能不一致,考试还经常考排序算法稳定性,所以强记吧!!!)
其余的复杂度,最好最坏等等,只要理解了都是很容易分析出来的。

~!!!发现错误或者不认同的地方,请告诉我一声。

0 0