目录
桶排序
假设我们要从大到小排序5个同学的分数,分别是2分,3分,5分,5分和8分,满分10分,因此我们需要10个桶,不难得到桶排序中,桶的大小就是MaxSize+1,如果MaxSize很大,桶排序其实是很浪费空间的。把分数放入桶中的代码如下:
#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++) //循环读入5个数
{
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;
}
这种排序方法我们暂且叫它“桶排序”。因为其实真正的桶排序要比这个复杂一些,,目前此算法已经能够满足我们的需求了。
这个算法就好比有 11 个桶,编号从 0~10。每出现一个数,就在对应编号的桶中放一个 小旗子,最后只要数数每个桶中有几个小旗子就 OK 了。
例如 2 号桶中有 1 个小旗子,表示 2 出现了一次;3 号桶中有 1 个小旗子,表示 3 出现了一次;5 号桶中有 2 个小旗子,表示 5 出现了两次;8 号桶中有 1 个小旗子,表示 8 出现了一次。 现在你可以尝试一下输入 n 个 0~1000 之间的整数,将它们从大到小排序。
如果需要对数据范围在 0~1000 之间的整数进行排序,我们需要 1001 个桶,来表示 0~1000 之间每一个数出现的次数,这一点一定要注意。另外,此处的每一个桶的作用其实就是“标 记”每个数出现的次数,因此我喜欢将之前的数组 a 换个更贴切的名字 book(book 这个单 词有记录、标记的意思),代码实现如下:
#include <stdio.h>
int main()
{
int book[1001],i,j,t,n;
for (i = 0; i <= 1000; i++) {
book[i]=0;
}
scanf("%d",&n);//输入一个数n,表示接下来有n个数
for (i = 1; i <= n; i++) {//循环读入n个数,并进行桶排序
scanf("%d",&t); //把每一个数读到变量t中
book[t]++; //进行计数,对编号为t的桶放一个小旗子
}
for (i=1000; i >= 0; i--) {//依次判断编号1000~0的桶
for (j = 1; j <= book[i]; j++) {//出现了几次就将桶的编号打印几次
printf("%d ", i);
}
}
getchar();getchar();
return 0;
}
最后来说下时间复杂度的问题。代码中第 6 行的循环一共循环了 m 次(m 为桶的个数), 第 9 行的代码循环了 n 次(n 为待排序数的个数),第 14 行和第 15 行一共循环了 m+n 次。 所以整个排序算法一共执行了 m+n+m+n 次。我们用大写字母 O 来表示时间复杂度,因此该 第 1 章 一大波数正在靠近——排序 7 算法的时间复杂度是 O(m+n+m+n)即 O(2*(m+n))。我们在说时间复杂度的时候可以忽略较小 的常数,最终桶排序的时间复杂度为 O(m+n)。还有一点,在表示时间复杂度的时候,n 和 m 通常用大写字母即 O(M+N)。
冒泡排序
冒泡排序的基本思想是:每次比较两个相邻的元素,如果它们的顺序错误就把它们交换 过来。
#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位开始比较直到最后一个尚未归位的数,想一想为什么到n-i就可以了。
{
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;
}
冒泡排序同时可以进行结构体的排序,实现同学们的人名和分数一起排序:
#include <stdio.h>
struct student {
char name[21];
char score;
}; // 这里创建了一个结构体用来存储姓名和分数
int main() {
struct student a[100], t;
int i, j, n;
scanf("%d", &n); // 输入一个数n
// 循环读入n个人名和分数
for (i = 0; i < n; i++) {
scanf("%s %d", a[i].name, &a[i].score);
}
// 按分数从高到低进行排序
for (i = 1; i <= n - 1; i++) {
for (j = 1; j <= n - i - 1; j++) {
if (a[j].score < a[j + 1].score) { // 对分数进行比较
t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
}
}
}
// 输出人名
for (i = 0; i < n; i++) {
printf("%s\n", a[i].name);
}
getchar();
getchar();
return 0;
}
冒泡排序的核心部分是双重嵌套循环。不难看出冒泡排序的时间复杂度是 O(N 2 )。这是 一个非常高的时间复杂度。冒泡排序早在 1956 年就有人开始研究,之后有很多人都尝试过 对冒泡排序进行改进,但结果却令人失望。如 Donald E. Knuth(中文名为高德纳,1974 年 图灵奖获得者)所说:“冒泡排序除了它迷人的名字和导致了某些有趣的理论问题这一事实 之外,似乎没有什么值得推荐的。
快速排序
快速排序之所以比较快,是因为相比冒泡排序,每次交换是跳跃式的。
每次排序的时候 设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全 部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样只能在相邻的数之间进 行交换,交换的距离就大得多了。因此总的比较和交换次数就少了,速度自然就提高了。当 然在最坏的情况下,仍可能是相邻的两个数进行了交换。
因此快速排序的最差时间复杂度和 冒泡排序是一样的,都是 O(n*2 ),它的平均时间复杂度为 O (nlogn)。其实快速排序是基于一 种叫做“二分”的思想。
#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;
// 读入数据
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;
}
快速排序由 C. A. R. Hoare(东尼·霍尔,Charles Antony Richard Hoare)在 1960 年提出, 之后又有许多人做了进一步的优化。如果对快速排序感兴趣,可以去看看东尼·霍尔 1962 年在 Computer Journal 发表的论文“Quicksort”以及《算法导论》的第七章。快速排序 算法仅仅是东尼·霍尔在计算机领域才能的第一次显露,后来他受到了老板的赏识和重用, 公司希望他为新机器设计一种新的高级语言。你要知道当时还没有 PASCAL 或者 C 语言这 些高级的东东。后来东尼·霍尔参加了由 Edsger Wybe Dijkstra(1972 年图灵奖得主)举办的 ALGOL 60 培训班,他觉得自己与其没有 把握地去设计一种新的语言,还不如对现有的 ALGOL 60 进行改进,使之能在公司的新机器 上使用。于是他便设计了 ALGOL 60 的一个子集版本。这个版本在执行效率和可靠性上都在 当时 ALGOL 60 的各种版本中首屈一指,因此东尼·霍尔受到了国际学术界的重视。后来他 在 ALGOL X 的设计中还发明了大家熟知的 case 语句,也被各种高级语言广泛采用,比如PASCAL、C、Java 语言等等。当然,东尼·霍尔在计算机领域的贡献还有很多很多,他在 1980 年获得了图灵奖。
小哼买书
小哼的学校要建立一个图书角,老师派小哼去找一些同学做调查,看看同学们都喜欢读 哪些书。小哼让每个同学写出一个自己最想读的书的 ISBN 号。当然有一些好书会有很多同学都喜欢, 这样就会收集到很多重复的 ISBN 号。小哼需要去掉其中重复的 ISBN 号,即每个 ISBN 号只 保留一个,也就说同样的书只买一本。然后再把这些 ISBN 号从小到大排序,小哼将按照排序好的 ISBN 号去书店买书。请你协助小哼完成“去重”与“排序” 的工作。
输入有 2 行,第 1 行为一个正整数,表示有 n 个同学参与调查(n≤100)。第 2 行有 n 个用空格隔开的正整数,为每本图书的 ISBN 号(假设图书的 ISBN 号在 1~1000 之间)。 输出也是 2 行,第 1 行为一个正整数 k,表示需要买多少本书。第 2 行为 k 个用空格隔 开的正整数,为从小到大已排好序的需要购买的图书的 ISBN 号。
例如输入:
10
20 40 32 67 40 20 89 300 400 15
则输出:
8
15 20 32 40 67 89 300 400
解决这个问题的方法大致有两种。第一种方法:先将这 n 个图书的 ISBN 号去重,再进 行从小到大排序并输出;第二种方法:先从小到大排序,输出的时候再去重。这两种方法都 可以。
先来看第一种方法。通过第一节的学习我们发现,桶排序稍加改动正好可以起到去重的 效果,因此我们可以使用桶排序的方法来解决此问题。
#include <stdio.h>
int main()
{
int a[1001], n, i, t;
for (i = 1; i <= 1000; i++) {
a[i] = 0; // 初始化
}
scanf("%d", &n); // 读入n
for (i = 1; i <= n; i++) { // 循环读入n个图书的ISBN号
scanf("%d", &t); // 把每一个ISBN号读到变量t中
a[t] = 1; // 标记出现过的ISBN号
}
for (i = 1; i <= 1000; i++) { // 依次判断1~1000这个1000个桶
if (a[i] == 1) { // 如果这个ISBN号出现过则打印出来
printf("%d ", i);
}
}
getchar();
getchar();
return 0;
}
这种方法的时间复杂度就是桶排序的时间复杂度,为 O(N+M)。
第二种方法我们需要先排序再去重。排序我们可以用冒泡排序或者快速排序。
20 40 32 67 40 20 89 300 400 15
将这 10 个数从小到大排序之后为 15 20 20 32 40 40 67 89 300 400。
接下来,要在输出的时候去掉重复的。因为我们已经排好序,所以相同的数都会紧挨在一起。只要在输出的时候,预先判断一下当前这个数 a[i]与前面一个数 a[i-1]是否相同。如 果相同则表示这个数之前已经输出过了,不用再次输出;不同则表示这个数是第一次出现, 需要输出这个数。
#include <stdio.h>
int main()
{
int a[101], n, i, j, t;
scanf("%d", &n); // 读入n
for (i = 1; i <= n; i++) { // 循环读入n个图书ISBN号
scanf("%d", &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;
}
}
}
printf("%d ", a[1]); // 输出第1个数
for (i = 2; i <= n; i++) { // 从2循环到n
if (a[i] != a[i - 1]) { // 如果当前这个数是第一次出现则输出
printf("%d ", a[i]);
}
}
getchar();
getchar();
return 0;
}
这种方法的时间复杂度由两部分组成,一部分是冒泡排序的时间复杂度,是 N (N2 ),另 一部分是读入和输出,都是 O(N),因此整个算法的时间复杂度是 O(2*N+N 2)。相对于 N2 来 说,2*N 可以忽略(我们通常忽略低阶),最终该方法的时间复杂度是 O(N2 )。