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”和 “侠客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 2 1
继续: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的任务就是要找大于基准数的数,直到i和j碰头为止。
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++,让大家变得轻松,希望这种松弛的风格能让大家感到放松,也感谢大家对我的鼎力支持,谢谢!