经典排序算法分析

从学第一门计算机语言开始就了解了选择、冒泡排序等算法,后来又学习了高效的归并、快速排序,排序算法种类很多,一直都想把它们总结一下,但却信心不足。这几天终于下定决心尝试写一写,于是看了算法导论,在网上也查了很多资料,最终将这些排序算法一个一个用C语言实现,最后进行了测试。看着它们一个个运行起来心里还是很有成就感的。接着用了两天时间写了下面这篇小文章,虽然写下了,但对于很多问题比如一些算法复杂度的证明还是不太明白,估计代码写得也不够好,待有了新的领悟再来补充。

文章中的排序算法有:1、选择排序;2、插入排序;3、希尔排序;4、冒泡排序;5、快速排序;6、归并排序;7,堆排序;8、计数排序;9、基数排序;10、桶排序。

一、选择排序(Selection Sort)


1、算法思想
选择排序是一种简单直观的排序算法,首先从数组中先找到最小的元素,放到第一个位置。然后从剩余元素中找到最小的,放到第二个位置……以此类推,就可以完成整个的排序工作了。

2、算法实现
数组A中存放length个整数。

void SelectSort(int A[], int length)   //选择排序
{
    int i, j, min,temp;
    for (i = 0; i < length - 1; ++i)
    {
        min = i;            //假设第i个元素是最小的元素
        for (j = i + 1; j < length; ++j)
            if (A[j] < A[min])  //如果有比A[i]还小的元素,则记录
                min = j;

        if (min != i)          //如果最小元素不是A[i],则交换
        {
            temp = A[min];
            A[min] = A[i];
            A[i] = temp;
        }
    }
}

3、算法分析
算法中含有双重循环,容易得出时间复杂度也是O(n*n)。在n比较小时,算法可以保证一定的速度,当n足够大时,算法的效率会降低,并且随着n的增大,算法的时间增长很快。最差时间复杂度О(n²),最优时间复杂度О(n²),平均时间复杂度О(n²),最差空间复杂度О(n) 。

二、插入排序(Insertion Sort)


1、算法思想
插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

2、算法实现

void InsertSort(int A[], int length)     //插入排序
{
    if (length < 2)  //只有一个元素,不用排序
        return;
    int i, j, key;
    for ( j = 2; j < length; j++) // 从第二个元素开始遍历
    {
        i = j - 1;     //j之前的元素都已经排好,i从j-1遍历
        key = A[j];
        while (i >= 0  &&  A[i] > key) //如果A[i]比key大,则将A[i]后移一个元素
        {
            A[i + 1] = A[i];
            i--;
        }
        A[i + 1] = key;  //找到位置,复制key
    }
}

3、 算法分析
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(n*n)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。

三、希尔排序(Shell Sort)

1、算法思想
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序是基于插入排序的以下两点性质而提出改进方法的:
(1)插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
(2)但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

希尔排序基本思想:先取一个小于length的整数gap作为第一个步长把全部数据分成gap个组。所有距离为gap的倍数的数据放在同一个组中。首先在各组内进行插入排序;然后,取得第二个步长重复上述的分组和排序,直至所取的步长gap=1,即所有数据放在同一组中进行插入排序为止。一般步长选择为\frac{n}{2}并且对步长取半直到步长达到1。

2、算法实现

void ShellSort(int A[], int length)   //希尔排序
{
    int i, j, gap,key;
    for ( gap = length/2; gap > 0; gap /= 2)
        for ( j = gap; j < length; j++)
        {
            i = j - gap;
            key = A[j];
            while (A[i] > key && i>=0)
            {
                A[i + gap] = A[i];
                i = i - gap;
            }
            A[i + gap] = key;
        }
}

3、算法分析
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高,所以,希尔排序的时间复杂度会比o(n^2)好一些。希尔排序的时间复杂度与步长序列的选取有关,例如取希尔步长的时间复杂度为O(n^2),而Hibbard步长的时间复杂度为O( n^1.5 ),下界是n*log2n,平均时间复杂度为O(n^1.3),在最坏的情况下和平均情况下执行效率相差不是很多。

四、冒泡排序(Bubble Sort)

1、算法思想
冒泡排序需要遍历要排序的数组,一次比较两个元素,如果顺序错误就把它们交换过来。重复地遍历直到没有再需要交换,排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数组的顶端。

2、算法实现

void BubbleSort(int A[], int length)    //冒泡排序
{
    int i, j,t;
    for (j = 0;j <length-1;j++)
        for (i = 0;i < length - j - 1;i++)
            if (A[i + 1] < A[i])
            {
                t = A[i + 1];
                A[i + 1] = A[i];
                A[i] = t;
            }
}

3、算法分析
冒泡排序与插入排序拥有相等的运行时间,但是需要的交换次数却不同。在最坏的情况下,冒泡排序需要O(n^2)次交换,而插入排序最多需要O(n)次交换,所以冒泡排序效率很低。

五、快速排序(Quick Sort)

快速排序算法由C. A. R. Hoare在1962年提出。

1、算法思想
快速排序采用分治策略把一个数组分为两个子数组。
步骤为:
(1)从数组中挑出一个元素,称为”基准”(pivot),所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数组的中某一位置。这个称为分区(partition)操作。
(2)通过递归调用快速排序,对子数组排序。
(3)因为子数组都是原址排序的,不需要合并,数组已经有序。

2、算法实现
(1)快速排序算法

void QuickSort(int A[], int left, int right)  //快速排序
{
    int mid;
    if (left < right)
    {
        mid = Partition(A, left, right);
        QuickSort(A, left, mid - 1);
        QuickSort(A, mid + 1, right);
    }
}

(2)划分算法

int  Partition(int A[], int p, int r)  //对数组A[left..right]原址重排
{
    int key = A[r];
    int i = p - 1;
    int j,temp;
    for (j = p;j < r;j++)
    {
        if (A[j] <= key)
        {
            i++;
            temp = A[i];A[i] = A[j];A[j] = temp;
        }
    }
    temp = A[i+1];A[i+1] = A[r];A[r] = temp;
    return i + 1;
}

划分算法图示:
这里写图片描述
令key = A[r],浅阴影部分数组元素划在第一部分,其值都不大于key,深阴影部分数组元素划在第二部分,其值都大于key,无阴影则不属于任何部分。
(a)初始化;
(b)2与它自身交换;
(c)~(d)8和7被添加到较大部分;
(e)1和8交换,数值较小部分规模增加;
(g)~(h)5和6被包含进较大部分,循环结束;
(i)交换key使其位于中间。

3、算法分析
快速排序的时间主要耗费在划分操作上,对长度为 n的区间进行划分,共需 n-1 次关键字的比较。

最坏时间复杂度
最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的数据,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。 因此,快速排序必须做 n-1 次划分,第i次划分开始时区间长度为 n-i+1,所需的比较次数为 n-i(1≤i≤n-1),故总的比较次数达到最大值:Cmax = n(n-1)/2=O(n^2)

最好时间复杂度
在最好情况下,每次划分所取的基准都是当前无序区的”中值”数据,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:O(nlgn)。尽管快速排序的最坏时间为 O(n^2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。它的平均时间复杂度为 O(nlgn)。

空间复杂度
快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为 O(lgn),故递归后需栈空间为 O(lgn)。最坏情况下,递归树的高度为 O(n),所需的栈空间为 O(n)。

六、归并排序(Merge Sort)

归并排序是创建在归并操作上的一种有效的排序算法,效率为O(n log n)。1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。

1、算法思想
分治模式:
分解:分解待排序的n个元素组成的序列成各具有n/2个元素的两个子序列;
解决:使用归并排序递归地排序两个子序列;
合并:合并两个已经排序好的子序列,产生已排序的序列。

2、算法实现

合并子数组不难,有很多种算法,可以使用数组或者指针。下面给出使用数组的方法:首先定义一个与待排序数组A同样大小的数组temp,从前往后依次比较两个子数组中的数据,将值小的存入temp数组中,然后再进行比较,如果其中一个子数组为空,那么直接将另一个子数组的数据依次取出放到temp后即可。

void Merge(int A[], int first, int mid, int last)  //合并子数组
{
    int temp[MAXSIZE];    //存储排序后的数组
    int i = first, j = mid + 1; 
    int k = 0;            //temp数组下标
    while (i <= mid && j <= last)   //分别从第一个元素比较两个子数组,
    {                                //将小的存储到temp数组中
        if (A[i] <= A[j])
            temp[k++] = A[i++];
        else
            temp[k++] = A[j++];

    }
    while(i <= mid)          //如果第一个数组中还有元素,将其复制到temp数组后面
        temp[k++] = A[i++];
    while(j <= last)         //如果第一个数组中还有元素,将其复制到temp数组后面
        temp[k++] = A[j++];
    for (i = 0;i < k;i++)    //最后将排序好的数组temp中的元素复制到A中
        A[first + i] = temp[i];
}

归并排序算法

void MergeSort(int A[], int first, int last)  //归并排序
{
    if (first < last)
    {
        int mid = (first + last) / 2;
        MergeSort(A, first, mid);
        MergeSort(A, mid + 1, last);
        Merge(A, first, mid, last);
    }
}

3、算法分析
比较操作的次数介于(nlgn)/2和nlgn - n + 1。 赋值操作的次数是(2nlgn)。归并算法的空间复杂度为:O(n)
最差时间复杂度 :O(nlgn)
最优时间复杂度 :O(n)
平均时间复杂度 :O(nlgn)

七、堆排序(Heapsort)

1991年的计算机先驱奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德(Robert W.Floyd)和威廉姆斯(J.Williams)在1964年共同发明了著名的堆排序算法

1、算法思想

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆可以被看成是一个近似的完全二叉树,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。 若A[parent[i]] >= A[i],则为最大堆;若A[parent[i]] <= A[i],则为最小堆。

以二叉树和数组形式展现的一个最大堆:

堆节点的访问
通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:
父节点i的左子节点在位置(2*i+1);
父节点i的右子节点在位置(2*i+2);
子节点i的父节点在位置floor((i-1)/2);

堆的操作
在最大堆中,堆中的最大值总是位于根节点。定义以下几种操作:
最大堆调整(MaxHeapify):将堆的节点作调整,使得子节点永远小于父节点,时间复杂度O(lgn)。
创建最大堆(BuildMaxHeap):从无序的输入数组中创建一个最大堆,具有线性时间复杂度。
堆排序(HeapSort):对一个数组进行原址排序。

2、算法实现
(1)最大堆调整(MaxHeapify)
程序输入为一个数组A和下标 i,假定根结点为LEFT( i )和RIGHT( i )的二叉树都是最大堆,但这时A[ i ]可能小于其孩子,这就违背了最大堆得性质,MaxHeapify通过让A[ i ]的值在最大堆中“逐级下降”,从而使得以下标为根结点的子树重新遵循最大堆的性质。

void MaxHeapify(int A[],int length, int i)        //维护堆的性质
{
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    int largest,temp;
    if (left < length && A[left]>A[i])
        largest = left;
    else
        largest = i;
    if (right < length && A[right] > A[largest])
        largest = right;
    if (largest != i)
    {
        temp = A[largest];
        A[largest] = A[i];
        A[i] = temp;
        MaxHeapify(A, length, largest);
    }
}

(2)创建最大堆(BuildMaxHeap)
通过自底向上方法的方法利用MaxHeapify可以把一个大小为length的数组A转换为最大堆。表面上看,每次调用MaxHeapify的时间复杂度是O(lgn),BuildMaxHeap需要O(n)次这样的调用,因此总的时间复杂度是O(nlgn),但此结果不是渐进紧确的,可以证明,MaxHeapify的时间复杂度是O(n),即线性时间。

void BuildMaxHeap(int A[], int length)     //建堆;
{
    int i;
    for (i = (length - 1) / 2;i >= 0;--i)
        MaxHeapify(A, length, i);
}

(3)堆排序(HeapSort)

void HeapSort(int A[], int length)   //堆排序算法
{
    BuildMaxHeap(A, length);
    int i, temp;
    for (i = length - 1;i > 0;--i)
    {
        temp = A[0];A[0] = A[i];A[i] = temp;
        length--;
        MaxHeapify(A, length,0);
    }
}

首先利用BuildMaxHeap创建最大堆,A[0]就成了数组中最大的元素,然后将A[0]和最后一个元素A[length-1]交换,这样,最大的元素就放到了正确的位置,但是新的堆(去掉最后一个元素后的)可能会违背最大堆得性质,于是调用MaxHeapify维护最大堆性质,接着重复操作直到堆中剩余一个元素,排序完成。图示如下:

3、算法分析
每次调用BuildMaxHeap的时间复杂度是O(n),而n-1次调用MaxHeapify,每次时间复杂度是O(lgn),因此堆排序过程HeapSort时间复杂度是O(nlgn)。
最差时间复杂度 O(nlgn)
最优时间复杂度 O(nlgn)
平均时间复杂度 O(nlgn)
最差空间复杂度 O(n) 。

八、计数排序(Counting Sort)

1、算法思想

计数排序是一种稳定的线性时间排序算法。计数排序假设n个输入元素中的每一个都是在0~k区间内的一个整数,对每一个元素x,确定小于x的元素的个数,利用这一信息,将x放到输出数组的位置上。例如:如果有15个元素小于x,则x就应该放在第16个位置上。

2、算法实现

void CountingSort(int A[], int length)      //计数排序
{
    int i,j,k;
    k=Max(A);
    int C[k];   //在第x位置上存放小于x的元素的个数,k=Max(A);
    int B[length];  //存放排序好的数列
    for (i = 0;i < k ;i++)
        C[i] = 0;       //初始化,将C[]中元素全部置零
    for (j = 0;j < length;j++)   
        C[A[j]] = C[A[j]] + 1; //C中第j个位置存A中元素j的个数
    for (i = 1;i < k;i++)
        C[i] = C[i] + C[i - 1]; //C中第j个位置存小于或等于元素j的个数
    for (j = length - 1;j >= 0;j--)  //排序,在B中,元素被放到正确的位置上
    {
        B[C[A[j]]-1] = A[j];
        C[A[j]] = C[A[j]] - 1;
    }
    for (i = 0;i < length - 1;i++)  //最后,将排序好的B中元素复制到A中
        A[i] = B[i];
}

通俗地理解,例如有10个年龄不同的人,统计出有8个人的年龄比A小,那A的年龄就排在第9位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要逆向填充目标数组,以及将每个元素的统计减去1的原因。算法的步骤图示:

3、算法分析
当输入的元素是n个0到k之间的整数时,它的运行时间是Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。

* 九、基数排序(Radix Sort) *

1、算法思想
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献。

2、算法实现
将所有待比较数值(正整数)统一为同样的数位长度d,数位较短的数前面补零。然后,从最低位1开始,依次进行一次排序。这样从最低位排序一直到最高位d排序完成以后,数列就变成一个有序序列。

RadixSort(A,d)
{
    for(i=1 to d)
        用一个稳定的算法对第i位排序;
}

3、算法分析
给定n个d位数,其中每一个数位有k个可能的取值。如果Radix使用的稳定排序算法耗时O(n+k),那么它就可以在O(d(n+k))时间内将这些数排好。

十、桶排序(Bucket Sort)

1、算法思想
桶排序将元素区间(a,b)划分为若干区间(或称为桶),然后对每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
算法步骤:
(1)设置一个数组当作空桶。
(2)遍历数组,并且把元素放到对应的桶(可用链表表示)中去。
(3)对每个不是空的桶进行排序。
(4)从不是空的桶里把元素再放回原来的序列中。
举例如下:假设A中共n个元素,数值在(0,1)区间内,数组B存放桶(链表),遍历A,将A中元素A[i]放到B中第n*A[i]个桶中,接着将每个桶排序,最后将B中元素有序存到A中,排序完成。

2、算法实现

不用链表的一种实现方式如下:

void BucketSort(int A[], int length)   //桶排序
{
    int i, k=Max(A);
    int B[k];  //定义桶,大小等于元素最大值
    for (i = 0;i < k - 1;++i)
        B[i] = 0;           //初始化桶
    for (i = 0;i < length;i++)
        B[A[i]]++;   //遍历A中元素,在桶中相应位置做标记(元素个数)
    int j = 0;
    for (i = 0;i < k - 1;i++)  //顺序读入到A中
        while (B[i]-- > 0)
            A[j++] = i;

}

3、算法分析

桶排序假设输入数据服从均匀分布,它不是比较排序,不受O(nlgn)下限的影响,平均情况下时间代价为O(n)。

十一、算法性能比较

1、性能分析
(1).O(n^2)性能分析
平均性能为O(n^2)的有:插入排序,选择排序,冒泡排序
在数据规模较小时(9W内),差别不大。当数据规模较大时,冒泡排序算法的时间代价最高。

(2).O(nlogn)性能分析
平均性能为O(nlogn)的有:快速排序,归并排序,希尔排序,堆排序。其中,快排是最好的, 其次是归并和希尔,堆排序在数据量很大时效果明显。这四种排序可看作为“先进算法”,其中,快速排序效率最高,但在待排序列基本有序的情况下,会变成冒泡排序,接近O(n^2)。

在排序的最终结果中,如果各元素的次序依赖它们之间的比较,这类算法就称为比较算法,插入排序、冒泡排序、快速排序、堆排序等都是比较排序算法,由决策树模型可得比较排序算法的下界是O(nlgn)。而计数排序、基数排序和桶排序不是比较排序,不受下界的约束

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值