一篇文章,让你彻底搞懂初阶排序

UPDATE 2024.12.01 :修正了文章中的一些错误

大家好,我是Zac,今天我突然突发奇想(脑子抽了),想做一套关于排序的课程,前方高能预警

1.来个情景

期末考试考完了,老师要将同学们的分数按照从大到小的顺序进行排序。班上只有5个同学,分数分别是(10分满分哈~):6分,7分,5分,3分,和2分。惨不忍睹呀!排序后是“7 6 5 3 2”。请你设计一段代码,实现排序。

2.来点思路

Ⅰ. 一维数组

首先,我们申请一个大小为11的数组a[11]。OK,11个变量已到账,编号为a[0]到a[10],分别表示0到10分。

接着,我们开始处理每个人的分数,第一个人的分数是6分,我们就在对应的a[6]加一,变成一,表示6出现了一次。后面的以此类推,最终是这个样子的:

现在,我们只需要将这些数字打印出来就行了,出现几次就打几次:a[0]、a[1]、a[4]、a[8]~a[10]都为0,不用打,其他的 都是1,打一次就够了。最终屏幕打出2 3 5 6 7。代码如下:

#include <iostream>
using namespace std;
int main() {
    int a[11], t;
    for (int i = 0; i < 11; i++) {
        a[i] = 0;
    }
    for (int i = 1; i <= 5; i++) {
        cin >> t;
        a[t]++;
    }
    for (int i = 0; i < 11; i++) {
        for (int j = 1; j <= a[i]; j++) {
            cout << i << " ";
        }
    }
    return 0;
}

输入数据为:

6 7 5 3 2

但是问题来了,刚才是从小到大排序,但我们要实现从大到小排序呀,怎么办呢? 其实很简单,把输出数据时的 "for (int i = 0; i < 11; i++)"改成"for (int i = 11 - 1; i >= 0; i--)"就行了。这种方法我们暂且叫它“桶排序”。现在有5个人的名字和分数:nancy score 5, bob score 3, master score 10, tom score 5, henry score 2。现在我们要对他们进行从大到小排序,并输出他们的名字。怎么做呢?桶只能呈现分数,不能呈现名字,别着急,咱们接着看下一节——冒泡排序。

Ⅱ. 冒泡排序

简化版的桶排序,有很多优点:简单、易懂。但是也有很多缺点,更要命的是,它超——级浪费空间!!!例如,需要排序数的范围是0~2100000000,那么你要申请一个a[2100000001]那么大的数组(二十一亿零一!好吧,如果真有这么一座由这么多楼层组成的摩天大楼,不说地基稳不稳,我估计它会把火星给捅爆!这可不好玩呀,毕竟火星是我们人类唯一一个除地球以外的“说得过去”的家园,把他给捅了……)咳,咳!Sorry哈,跑题了,咱们接着往下讲,这2100000001个桶也不是全都用呀!你最少也会剩2100000001 - 5 = 2099999996个不用的桶(说的我都“细思极恐了”,咱们不说桶了,)咱们说冒泡!冒泡排序的基本思想是:每次比较两个相邻元素,如果他们的顺序错误就把他们交换过来。举个栗子:5 4 3 2 1这几个数,如何排序,详询店内 ↓

 话不多说,上……呃,等等,你应该明白了什么吧?不明白就使劲盯着看,看明白了再看下面的内容。话不多说,上代码(冒泡思路):

#include <bits/stdc++.h>
using namespace std;
int main() {
    int a[100], n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    for (int i = 1; i <= n - 1; i++) {
        for (int j = 1; j <= n - i; j++) {
            if (a[j] < a[j + 1]) {
                swap(a[j], a[j + 1]);
            }
        }
    }
    for (int i = 1; i <= n; i++) {
        cout << a[i] << " ";
    }
    return 0;
}

可以输入以下数据:

5
2 3 6 7 5

运行结果是:

7 6 5 3 2

将上面代码稍作修改,就可以解决第一节留下的问题:

#include <bits/stdc++.h>
using namespace std;
struct stu {
    string nm;
    int sc;
}a[100];
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i].nm >> a[i].sc;
    }
    for (int i = 1; i <= n - 1; i++) {
        for (int j = 1; j <= n - i; j++) {
            if (a[j].sc < a[j + 1].sc) {
                swap(a[j], a[j + 1]);
            }
        }
    }
    for (int i = 1; i <= n; i++) {
        cout << a[i].nm << endl;
    }
    return 0;
}

输入以下数据进行验证:

5
nancy 5
bob 3
master 10
tom 5
henry 2

运行结果是:

master
nancy
tom
bob
henry

咱们梳理一下第一段代码的逻辑:

首先,先开始找第一个小的数,用第一个数和第二个数作比较,小的放后。再用第二个数和第三个数进行比较,小的放后,以此类推。我们叫他第一趟。第一趟完了以后是这样的:“3 6 7 5 2”。接着,我们开始第二趟,按照第一趟的逻辑,完成后是这样的:“6 7 5 3 2”。然后开始第三趟,完成后是“7 6 5 3 2”。最后我们要开始第四趟。你们是不是感到很奇怪?这不是已经排好了吗?怎么还要继续?拿“5 4 3 2 1”来说,他就需要4次,你还能找出其他样例吗?在评论区打下来吧!

 Ⅲ. 快速排序

上一节我们学习了第一个真正的排序算法——冒泡排序,并且解决了桶排序浪费空间的问题。但冒泡也有一个最致命的缺点,那就是太慢了!桶虽说费了点空间,但时间复杂度只有O(n + m)。而冒泡是O(n^2)!!!假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序只需要0.1秒,而冒泡排序则需要1千万秒,达到115天之久.!(如果你在9月份考GESP,等你得出这道题的结果时,人家12月的GESP都考完了!)细不细很吓人?那有没有集少量时间和少量空间于一体的排序算法呢?那就是“快速排序”啦!光听这个名字是不是就觉得很快呢?

假设我们现在对 “6 1 2 7 9 3 4 5 10 8” 这10个数进行降序排序。首先在这个序列中随便找一个数作为基准数(听起来挺吓人,这就是一个用来参照的数,待会儿你就知道它是干嘛的了)。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中 所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列:

                                3   1   2   5   4   6   9   7   10   8

快排方法其实很简单:分别从初始序列 “6 1 2 7 9 3 4 5 10 8” 两端开始“搜查”。先从右往左找一个大于6的数,再从左往右找一个小6的数,然后交换它们。这里可以用两个变量 和 分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“侠客i“侠客j”。刚开始的时候让侠客i指向序列的最左边,即数字6。让侠客j指向序列的最右边,即数字8。(电脑绘图,请谅解)

 首先,侠客j(右边的)开始移动,因为我们设置6为基准数。侠客j先动,她一步一步往前挪(j--),寻找第一个比6大的数。接下来侠客i开始移动(i++),寻找第一个比6小的数。最后他们停在了1和8上。

现在交换侠客i和侠客j的值:

 

变成:6  8  2  7  9  3  4  5  10  1 

继续:6  8  10  7  9  3  4  5 

继续:9  8  10  7  6  3  4  5  2  1

(注意:这时候没有比9小的在左边的数了(除了基准数6),所以由 “6  8  10  7  9  3  4  5  2  1”变成“9  8  10  7  6  3  4  5  2  1”。)

现在我们已经完成第一次“搜查”了。此时以6为分界点,6左边的都大于6,右边的都小于6。回顾一下刚才的过程,其实侠客j的任务就是要找小于基准数的数, 而侠客i的任务就是要找大于基准数的数,直到ij碰头为止。

OK,解释完毕,自己动手试一下吧!

左边:10  9  8  7  6  3  4  5  2  1。

右边:10  9  8  7  6  5  4  3  2  1。

至此,快排就完成啦!现在动手做一下开头的情景吧!(先自己再看我的):

#include <iostream>
using namespace std;
int a[101], n;
void quicksort(int left, int right) {
    int i, j, t, temp;
    if (left > right) {
        return;
    }
    temp = a[left];
    i = left;
    j = right;
    while (i != j) {
        while (a[j] <= temp && i < j) j--;
        while (a[i] >= temp && i < j) i++;
        if (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;
    cin >> n;
    for (i = 1; i <= n; i++) cin >> a[i];
    quicksort(1, n);
    for (i = 1; i <= n; i++) cout << a[i] << " ";
    return 0;
}

做完的小朋友我们继续往下看。

Ⅳ. 更多思路

其实我们在的生活中,还有插入、选择、计数、基数、归并、和堆排序等等。我们还能用sort、greater<int>、和cmp函数来实现随心所欲的排序。有兴趣的可以自行了解!这里是初阶排序,后面关于sort函数我还会出课程进行分享哈!

3. 来道练习

说了这么多,该你练习练习了!现在来看一个具体的例子“小Zac买书”(根据全NOIP2006普及组T1改编),来实践一下本章所学的三种排序算法。

Zac的学校要建立一个图书角,老师派Zac去找一些同学做调查,看看同学们都喜欢读 哪些书。Zac让每个同学写出一个自己最想读的书的ISBN号(你知道吗?每本书都有唯一 的ISBN号,不信的话你去找本书翻到背面看看)当然有一些好书会有很多同学都喜欢, 这样就会收集到很多重复的ISBN号。Zac需要去掉其中重复的ISBN号,即每个ISBN号只 保留一个,也就说同样的书只买一本(学校真是够抠门的)然后再把这些ISBN号从小到 大排序,Zac将按照排序好的ISBN号去书店买书。请你协助Zac完成“去重”与“排序” 的工作。

输入有2行,第1行为一个正整数,表示有n个同学参与调查nW100)。2行有n 个用空格隔开的正整数,为每本图书的ISBN号(假设图书的ISBN号在1~1000之间)

输出也是2行,第1行为一个正整数n,表示需要买多少本书。第2行为k个用空格隔 开的正整数,为从小到大已排好序的需要购买的图书的ISBN号。

例如输入:

10
20 40 32 67 40 20 89 300 400 15

则输出:

8
15 20 32 40 67 89 300 400

最后,程序运行的时间限制为1秒。

解题思路有两种。第一种是桶排序,第二种是冒泡(或快排),这里还有2种选择:先去重,再排序,再去重。下面是桶排序AC代码:

#include <iostream>
using namespace std;
int main() {
    int a[1001], n, i, t, cnt = 0;
    for (i = 1; i <= 1000; i++) {
        a[i] = 0;
    }
    cin >> n;
    for (i = 1; i <= n; i++) {
        cin >> t;
        a[t] = 1;
    }
    for (i = 1; i <= 1000; i++) {
        if (a[i] == 1) {
            cnt++;
        }
    }
    cout << cnt << endl;
    for (i = 1; i <= 1000; i++) {
        if (a[i] == 1) {
            cout << i << " ";
        }
    }
    return 0;
}

桶排序的时间复杂度是O(n + m)。接下来是冒泡排序AC代码:

#include <iostream>
using namespace std;
int main() {
    int a[101] = {0}, n, i, j, t, cnt = 0;
    cin >> n;
    for (i = 1; i <= n; i++) {
        cin >> a[i];
    }
    for (i = 1; i <= n - 1; i++) {
        for (j = 1; j <= n - i; j++) {
            if (a[j] > a[j + 1]) {
                t = a[j];
                a[j] = a[j + 1];
                a[j + 1] = t;
            }
        }
    }
    for (i = 2; i <= n + 1; i++) {
        if (a[i] != a[i - 1]) {
            cnt++;
        }
    }
    cout << cnt << endl;
    cout << a[1] << " ";
    for (i = 2; i <= n; i++) {
        if (a[i] != a[i - 1]) {
            cout << a[i] << " ";
        }
    }
    return 0;
}

4. 尾声(必看!)

希望本篇文章能给大家带来帮助。本教程制作不易,望各位点赞、关注、收藏!我们下次不见不散!对了,我写的手稿有5页A4纸呢(5000多字呢!!!),求求了~

THE END, BYE!

对了,我发这篇博客不是为了刷流量,而是为了让大家能更加容易地学好C++,让大家变得轻松,希望这种松弛的风格能让大家感到放松,也感谢大家对我的鼎力支持,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值