【啊哈!算法】第一章:一大波数正在靠近-排序

桶排序,冒泡排序,快速排序

Posted by x-jeff on October 2, 2022

博客为参考《啊哈!算法》一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.最快最简单的排序-桶排序

👉相关博文:【算法基础】【排序】桶排序

假设有5个学生,分数分别为5分、3分、5分、2分和8分。现在希望编写一段程序让计算机随机读入5个数然后将这5个数从小到大输出。

首先,我们申请一个大小为11的数组int a[11]

数组下标表示对应的分数。我们将每个分数对应的人数存入数组中:

接下来,我们只需要将出现过的分数打印出来就可以了,出现几次就打印几次,具体如下:

  • a[0]为0,表示“0”没有出现过,不打印。
  • a[1]为0,表示“1”没有出现过,不打印。
  • a[2]为1,表示“2”出现过1次,打印2。
  • a[3]为1,表示“3”出现过1次,打印3。
  • a[4]为0,表示“4”没有出现过,不打印。
  • a[5]为2,表示“5”出现过2次,打印5 5。
  • a[6]为0,表示“6”没有出现过,不打印。
  • a[7]为0,表示“7”没有出现过,不打印。
  • a[8]为1,表示“8”出现过1次,打印8。
  • a[9]为0,表示“9”没有出现过,不打印。
  • a[10]为0,表示“10”没有出现过,不打印。

最终屏幕输出“2 3 5 5 8”,完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
int main()
{
    int a[11],i,j,t;
    for(i=0;i<=10;i++)
        a[i]=0; //初始化为0
    for(i=1;i<=5;i++)
    {
        scanf("%d",&t); //把每一个数读到变量t中
        a[t]++; //进行计数
    }
    
    for(i=0;i<=10;i++) //依次判断a[0]~a[10]
        for(j=1;j<=a[i];j++) //出现了几次就打印几次
            printf("%d ",i);
    
    getchar();getchar();
    //这里的getchar();用来暂停程序,以便查看程序输出的内容
    //也可以用system("pause");等来代替
    return 0;
}

输入数据为:“5 3 5 2 8”。这个算法就好比有11个桶,编号从0~10。每出现一个数,就在对应编号的桶中放一个小旗子,最后只要数数每个桶中有几个小旗子就OK了:

桶排序是一个非常快的排序算法。但这个例子其实并不是真正的桶排序算法,真正的桶排序算法要比这个更加复杂。这个例子本质上还不能算是一个真正意义上的排序算法。为什么呢?例如遇到下面这个例子就没辙了。

现在分别有5个人的名字和分数:huhu 5分、haha 3分、xixi 5分、hengheng 2分和gaoshou 8分。请按照分数从高到低,输出他们的名字。即应该输出gaoshou、huhu、xixi、haha、hengheng。如果使用我们刚才简化版的桶排序算法仅仅是把分数进行了排序。最终输出的也仅仅是分数,但没有对人本身进行排序。

2.邻居好说话-冒泡排序

👉相关博文:【算法基础】【排序】冒泡排序

简化版的桶排序不仅仅有上一节所遗留的问题,更要命的是:它非常浪费空间!例如需要排序数的范围是0~2100000000之间,那我们需要申请2100000001个变量,也就是说要写成int a[2100000001]。即便只给5个数进行排序,我们也仍然需要2100000001个“桶”,这真是太浪费空间了!还有,如果现在需要排序的不再是整数而是一些小数,那又该怎么办呢?因此,我们引入冒泡排序来解决这两个问题。

冒泡排序的原理介绍详见:【算法基础】【排序】冒泡排序。代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<stdio.h>
int main()
{
    int a[100],i,j,t,n;
    scanf("%d",&n); //输入一个数n,表示接下来有n个数
    for(i=1;i<=n;i++) //循环读入n个数到数组a中
        scanf("%d",&a[i]);
    //冒泡排序的核心部分
    for(i=1;i<=n-1;i++) //n个数排序,只用进行n-1趟
    {
        for(j=1;j<=n-i;j++) //从第1位开始比较直到最后一个尚未归位的数
        {
            if(a[j]<a[j+1]) //比较大小并交换
            {
                t=a[j];
                a[j]=a[j+1];
                a[j+1]=t;
            }
        }
    }
    for(i=1;i<=n;i++) //输出结果
        printf("%d ",a[i]);
    
    getchar();getchar();
    return 0;
}

假设输入为:

1
2
10
8 100 50 22 15 6 1 1000 999 0

运行结果是:

1
1000 999 100 50 22 15 8 6 1 0 

将上面代码稍加修改,就可以解决第1节遗留的问题,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<stdio.h>
struct student
{
    char name[21];
    int score;
}; //这里创建了一个结构体用来存储姓名和分数
int main()
{
    struct student a[100],t;
    int i,j,n;
    scanf("%d",&n); //输入一个数n
    for(i=1;i<=n;i++) //循环读入n个人名和分数
        scanf("%s %d",a[i].name,&a[i].score);
    //按分数从高到低进行排序
    for(i=1;i<=n-1;i++)
    {
        for(j=1;j<=n-i;j++)
        {
            if(a[j].score<a[j+1].score)//对分数进行比较
            {
                t=a[j];
                a[j]=a[j+1];
                a[j+1]=t;
            }
        }
    }
    for(i=1;i<=n;i++)//输出人名
        printf("%s\n",a[i].name);
    
    getchar();getchar();
    return 0;
}

假设输入为:

1
2
3
4
5
6
5
huhu 5
haha 3
xixi 5
hengheng 2
gaoshou 8

运行结果是:

1
2
3
4
5
gaoshou
huhu
xixi
haha
hengheng

个人理解:其实桶排序按照这个策略也完全能解决这个问题。

3.最常用的排序-快速排序

👉相关博文:【算法基础】【排序】快速排序

冒泡排序解决了桶排序浪费空间的问题,但在算法的执行效率上却牺牲了很多,它的时间复杂度达到了$O(N^2)$。假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序只需要0.1秒,而冒泡排序则需要1千万秒,达到115天之久。因此引入快速排序,既不浪费空间又可以速度很快。

假设待排序的数为:

1
6 1 2 7 9 3 4 5 10 8

设第一个数6为基准数。

先向左移动j(j--),直到找到一个小于6的数停下来。接下来向右移动i(i++),直到找到一个大于6的数停下来,然后交换i和j所指的数:

到此,第一次交换结束。接下来j继续向左挪动(每次必须是j先出发)。和上一次移动的规则一样:

直至i和j相遇:

这里介绍的快速排序流程和【算法基础】【排序】快速排序稍有不同,但原理都是一样的。

此时,第一轮排序结束,6左边的数都小于6,6右边的数都大于6。后续再分别对左右两个子序列进行同样规则的排序即可,迭代多次,直至所有排序完成。

快速排序的最差时间复杂度和冒泡排序是一样的,都是$O(N^2)$,它的平均时间复杂度为$O(N \log N)$。快速排序的实现代码见下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<stdio.h>
int a[101],n; //定义全局变量,这两个变量需要在子函数中使用

void quicksort(int left, int right)
{
    int i,j,t,temp;
    if(left>right)
        return;
    
    temp=a[left]; //temp中存的就是基准数
    i=left;
    j=right;
    while(i!=j)
    {
        //顺序很重要,要先从右往左找
        while(a[j]>=temp && i<j)
            j--;
        //再从左往右找
        while(a[i]<=temp && i<j)
            i++;
        
        //交换两个数在数组中的位置
        if(i<j)//当i和j没有相遇时
        {
            t=a[i];
            a[i]=a[j];
            a[j]=t;
        }
    }
    //最终将基准数归位
    a[left]=a[i];
    a[i]=temp;
    
    quicksort(left,i-1);//继续处理左边的,这里是一个递归的过程
    quicksort(i+1,right);//继续处理右边的,这里是一个递归的过程
}

int main()
{
    int i,j,t;
    //读入数据
    scanf("%d",&n);
    for(i=1;i<=n;i++)
        scanf("%d",&a[i]);
    
    quicksort(1,n); //快速排序调用
    
    //输出排序后的结果
    for(i=1;i<=n;i++)
        printf("%d ",a[i]);
    
    getchar();getchar();
    return 0;
}

输入

1
2
10 
6 1 2 7 9 3 4 5 10 8

得到的输出为:

1
1 2 3 4 5 6 7 8 9 10 

4.小哼买书

一个实战例子,这里不再赘述。此处附上之前写的10种经典排序算法的博文链接(内附c++和python代码实现):

  1. 选择排序
  2. 冒泡排序
  3. 插入排序
  4. 希尔排序
  5. 归并排序
  6. 快速排序
  7. 堆排序
  8. 计数排序
  9. 桶排序
  10. 基数排序