第五讲. 经典算法之排序算法

第五讲. 经典算法之排序算法

1. 简介

顾名思义,排序算法就是将数组中杂乱无章的数按照从小到大或者从大到小之类的顺序进行排列的代码程序。一般常见的排序算法有冒泡排序、选择排序、快速排序、归并排序、堆排序、计数排序、桶排序、希尔排序等。
例如:

54 12 32 55 4 10 1 3 89 12

按从小到大(升序)的顺序进行排序后将变成:

1 3 4 10 12 12 32 54 55 89

那么如何编写这样的一个程序便是本次内容的重点。

2. 几种常见排序算法的实现

2.1 简单排序之冒泡排序与选择排序

冒泡排序与选择排序在思路上其实差不多,都是通过一个双重循环语句,每次将从数组的当前排序部分中挑出最大或者最小的那个元素,移动至当前待排序部分的最边位置,代码如下:

//冒泡排序
void bubbleSort(int a[], int n)
{
	for(int i=0;i<n-1;i++)
		for(int j=0;j<n-i-1;j++)
			if(a[j]>a[j+1])swap(a[j],a[j+1]);
}
//选择排序
void selectionSort(int a[], int n)
{
	for(int i=0;i<n-1;i++)
		for(int j=i+1;j<n;j++)
			if(a[i]>a[j])swap(a[i],a[j]);
}

不难分析得到,这两种排序算法的时间复杂度都是 O(n2) ,都是很容易理解且最基础的排序算法。

2.2 最好排序之快速排序

快速排序算是最好用且最常用的排序算法了,它的思想有点像二分思想,首先随便挑选一个数作为参考数,然后遍历一边数组将比参考数小的所有数排在一边,比参考数大的所有数排在另一边,这样就分成了两个未排序的数组,又递归进行这样的操作。举例而言,对于未排序的数组 A

54 12 32 55 4 10 1 3 89 12

记参考数为 A[k]=54 ,算法初始情况下 k=0 。记搜索变量 i=0 (指向首元素下标), j=9 (指向末元素下标)。
首先利用搜索变量 j 从后往前搜索,找到第一个比参考数小的元素,然后交换它们的位置。不难发现 j=9, A[j]=12<54 ,满足条件,故交换 A[9]A[k] (k=0) ,同时 j = j-1 = 8 ,数组变为如下情形:

12 12 32 55 4 10 1 3 89 54

此刻 i=0, j=8, k=9
接着利用搜索变量 i 从前往后搜索,找到第一个比参考数大的元素,然后同样交换它们的位置。不难发现 i=3, A[i]=55>54 ,满足条件,故交换 A[3]A[k] (k=9) ,同时 i = i+1 = 4,数组变为如下情形:

12 12 32 54 4 10 1 3 89 55

此刻 i=4, j=8, k=3
接着同理利用搜索变量 jj=8 位置处接着继续往前搜索,找到第一个比参考数小的元素,并交换。不难发现,j=7, A[j]=3<54 ,满足条件,故交换 A[7]A[k] (k=3) ,同时 j = j-1 = 6,数组变为如下情形:

12 12 32 3 4 10 1 54 89 55
此刻 i=4, j=6, k=7

接着同理利用搜索变量 ij=4 位置处接着继续往后搜索,找到第一个比参考数大的元素,并交换。不难发现,直到 i=6 ,即 ij 碰头了,也没有找到比参考数大的元素,而且此时两个搜索变量碰头,说明已经将整个数组遍历了一遍,换句话说,经过这样的一个操作之后,不难发现,所有比 54 大的数都到了它的右边,所有比 54 小的数都到了它的左边。
这样,整个数组就以54为界限,分为了左边待排序部分和右边待排序部分,且不难证明 54 已经在它排序后应在的下标位置上了。然后我们只需要,分别对这两个待排序部分进行递归地进行和之前一样的操作即可,代码如下:

void quickSort(int a[], int n, int s, int e)
{
    if(s>=e)
        return;
    int i=s, j=e-1, k=s;
    while(i<j)
    {
        for(; i<j&&k<=j; j--)
        {
            if(a[j]<a[k])
            {
                swap(a[j],a[k]);
                k=j;
                j--;
                break;
            }
        }
        for(; i<j&&i<=k; i++)
        {
            if(a[i]>a[k])
            {
                swap(a[i],a[k]);
                k=i;
                i++;
                break;
            }
        }
    }
    quickSort(a,n,s,k),quickSort(a,n,k+1,e);
}

不难分析得到,在每一层的所有递归调用所花费的时间复杂度为 O(n) ,总共有 O(logn) 层,故时间复杂度为 O(nlogn)
快速排序无疑是一种很巧妙的排序算法,也是实际运用最为广泛的排序算法,如果有兴趣深究的话,会发现快速排序其实是一种不稳定的排序算法,极端情况上,如果待排序数组本就有序,那么时间复杂度将会达到 O(n2) ,因此在实际运用中,很多封装好的函数在算法的实现上都运用了一点随机化的小技巧,就是随机化选择参考数,即随机初始化 k ,这样的好处是,不会存在总是最坏时间复杂度的情况。

2.3 最美排序之计数排序

设想一种情况,如果待排序数组全是一定范围内的非负整数,那么我们便可以通过计数数组来统计每个数出现的次数,然后对计数数组做一个累加求和,这个累加和便是相应元素在排序后数组中的下标,这里不详细说思路了,有兴趣的可以上网自行了解,下面是代码:

#include <stdio.h>
define MAXN = 100000;
int k = 1000; 
int a[k+1], c[MAXN]={0}, ranked[MAXN];
 
int main() {
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) {
        scanf("%d", &a[i]);
        ++c[a[i]];
    }
    for (int i = 1; i < k; ++i)
        c[i] += c[i-1];
    for (int i = n-1; i >= 0; --i)
        ranked[--c[a[i]]] = a[i];
    for (int i = 0; i < n; ++i)
        printf(" %d", ranked[i]);
    printf("\n");
    return 0;
}

这种排序算法其实比较好理解,可以简单理解成遍历了一遍数组,然后记录了每个数出现了多少个,然后从小到大或者从大到小把这些数再放回去,这样得到的结果数组便是有序的。而且不难发现,这个算法是时间复杂度是线性的,没有循环的嵌套, T(n) = O(n + k) ,其中 k 表示数组中元素的范围大小。
这是一种典型的牺牲空间换时间的算法,所以缺点也很明显,一方面, k 很可能会很大很大,以至于比 nlogn 都大,亦或者使得无法申请数组a[k];另一方面,算法需要以下标与不同元素对应起来然后计数,而下标只能是非负整数,所以该算法对于元素有复数或者浮点数的情况,是无法应付的,故计数排序适用范围较小,尽管它在理论上十分优美。
事实上,为了应对 k 过大,或者浮点负数等情况,之后又有人提出了桶排序以及基数排序,有兴趣的可以自行了解。

3. 最后说几句

除了以上提到的排序之外,还有希尔排序、堆排序、归并排序等排序算法。事实上,排序算法可以分为比较排序和非比较排序,除了计数排序、基数排序属于非比较排序算法这一类外,其余的都属于比较排序算法(桶排序根据桶内排序算法的实现既可以属于比较排序也可以属于非比较排序)。比较排序算法的时间复杂度下限理论上是 O(nlogn) ,其中堆排序、归并排序的时间复杂度和快速排序一样,都是 O(nlogn) 级别的。
排序算法是程序设计中一个极其基础且常用的算法,无论是程序设计竞赛还是面试中,排序算法的出现率都很高,而快速排序作为排序算法中最常用的一个更是十分重要。虽然在比赛中直接调用头文件中的库函数sort(…)便可直接使用封装好的快速排序,但作为一名学者,了解其实现原理与思想是十分重要且必要的,也是程序员的内功之所在。

    原文作者:五大常用算法
    原文地址: https://blog.csdn.net/qq_38609781/article/details/84843249
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞