❤️ 比「 快速排序 」更高效的排序,但是也有其局限性 ❤️(推荐收藏)
Posted 英雄哪里出来
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤️ 比「 快速排序 」更高效的排序,但是也有其局限性 ❤️(推荐收藏)相关的知识,希望对你有一定的参考价值。
- 本文已收录于专栏《数据结构入门》
零、📃前言
这个系列的文章中,我会着重讲解一些常见的 「算法和数据结构」 的设计思想,并且配上动图。希望读者能够带上自己的 「思考」,主要针对面试中常见的问题和新手朋友们比较难理解的点进行解析。当然,这个系列的文章,后面也会给出面向算法竞赛的提纲,如果有兴趣深入学习的欢迎在「评论区留言,一起成长交流」 。
零基础学算法的最好方法,莫过于刷题了。当然,刷题是不够的,刷的过程中也要多多总结,多多思考,养成 「经常思考」 的习惯,这就是所谓的 「 流水不腐,户枢不蠹 」,任何事情都是需要坚持的,刷题也一样,没有刷够足够的题,就很难做出系统性的总结。所以上大学的时候,我花了三年的时间来刷题, 工作以后还是会抽点时间出来刷题。
千万不要用工作忙来找借口,时间挤一挤总是有的。当然,每天不需要花太多时间在这个上面,把这个事情做成一个规划,按照长期去推进。反正也没有 KPI 压力,就当成是工作之余的一种消遣,还能够提升思维能力。
所以,无论你是 小学生,中学生,高中OIer,大学ACMer,职场人士,只要想开始,一切都不会太晚!
今天要讲的主要内容是:「 计数排序 」,希望读者朋友给个面子,帮我完成阅读,不能完读,我可就完犊子了 🤣,当然,如果你觉得还可以,给我个「 赞 」 和 「 收藏 」,这个对我很重要,谢谢了!
「 计数排序 」 是比较好理解且编码相对简单的排序算法,可以说是效率最高的排序算法之一,但是也有「 局限性 」,这个后面我会讲。在讲排序这块内容时,我会尽量做到「 深入浅出 」,让 90% 的 「 零基础小白 」 也都能理解,真正做到 「让天下没有难学的算法」 。我知道这很难,但是我愿意尝试!我会尽量把文章写得有趣,一气呵成,不浪费读者的宝贵时间。毕竟,「 时间就是金钱 」。
🙉饭不食,水不饮,题必须刷🙉
C语言免费动漫教程,和我一起打卡! 🌞《光天化日学C语言》🌞
LeetCode 太难?先看简单题! 🧡《C语言入门100例》🧡
数据结构难?不存在的! 🌳《数据结构入门》🌳
LeetCode 太简单?算法学起来! 🌌《夜深人静写算法》🌌
究极算法奥义!深度学习! 🟣《深度学习100例》🟣
那么,我的教程和别人的教程有什么不同的地方呢?
「第一步」简单释义: 我会简单解释一下这个算法的目的、思想、以及为什么叫这个名字以帮助记忆。
「第二步」核心思想: 我会大致介绍一下这个算法的核心思想。
「第三步」动图演示: 我会引入一个动图,并且用一个切实的例子展示一下算法执行的全过程。
「第四步」算法前置: 在学习这个算法之前,我们需要学习的前置内容有哪些?这些内容是需要事先去攻克的。
「第五步」算法描述: 细致的讲解整个算法的执行流程。
「第六步」算法分析: 对算法的时间复杂度和空间复杂度进行一个详细的分析。
「第七步」优化方案: 介绍一些可以优化的点。
「第八步」代码实践: 用 C/C++ 来实现上述算法。
「第九步」代码验证: 最后,我会推荐一些比较好用的在线评测系统来验证我们实现的算法的正确性。
一、🎯简单释义
1、算法目的
将原本乱序的数组变成有序,可以是 「升序」 或者 「降序」 (为了描述统一,本文一律只讨论 「 升序」 的情况)。
2、算法思想
首先,准备一个 「 计数器数组 」,通过一次 「 枚举 」,对所有「 原数组 」元素进行计数。
然后,「 从小到大 」枚举所有数,按照 「 计数器数组 」 内的个数,将枚举到的数放回 「 原数组 」。执行完毕以后,所有元素必定按照 「升序」 排列。
3、命名由来
整个过程的核心,就是在 「计算某个数的数量」,故此命名 「 计数排序 」 。
二、🧡核心思想
- 「枚举」:穷举所有情况。
- 「哈希」:将一个数字映射到一个数组中。
- 「计数」:一次计数就是一次自增操作。
三、🔆动图演示
1、样例
2 | 3 | 1 | 3 | 2 | 1 | 4 | 2 | 4 | 6 | 2 |
- 初始情况下的数据如 图二-1-1 所示,基本属于乱序,纯随机出来的数据。
2、算法演示
- 接下来,我们来看下排序过程的动画演示。如 图二-2-1 所示:
3、样例说明
图示 | 含义 |
---|---|
■ 的柱形 | 计数为 0 的数 |
■ 的柱形 | 计数为 1 的数 |
■ 的柱形 | 计数为 2 的数 |
■ 的柱形 | 计数为 3 的数 |
■ 的柱形 | 计数为 4 的数 |
我们看到,首先程序生成了一个区间范围为 [ 1 , 9 ] [1, 9] [1,9] 的 「 计数器数组 」,并且一开始所有值的计数都为 0。
然后,遍历枚举「 原数组 」的所有元素,在 元素值 对应的计数器上执行 「 计数 」 操作。
最后,遍历枚举「 计数器数组 」,按照数组中元素个数放回到 「 原数组 」 中。这样,一定可以保证所有元素都是 「升序」 排列的。
四、🌳算法前置
1、循环的实现
- 这个算法本身需要做一些「 循环 」进行枚举计算,所以你至少需要知道「 循环 」 的含义,这里以 「 c++ 」 为例,来看下一个简单的「 循环 」是怎么写的。代码如下:
int n = 111;
for(int i = 0; i < n; ++i) {
// TODO : 。。。
}
- 这个语句就是一个最简单的循环语句,它会将循环体内的语句执行 n n n 次,而这里的 n n n 等于 111 111 111,也就是会执行 111 111 111 次。
- 「 循环 」 是计算机完成 「 枚举 」 和 「 迭代 」 的基础操作。
2、哈希的实现
- 「 哈希 」就是将一个数字「 映射 」到一个「 数组 」中,然后通过数组的 「 取下标 」 这一步来完成 O ( 1 ) O(1) O(1) 的 「 查询 」 操作 。
- 所有 「 计数排序 」 对待排序数组中的元素,是有范围要求的,它的值不能超过数组本身的大小,这就是上文提到的 「 局限性 」。
- 如下代码所示,代表的是把 数字5 「 哈希 」到
cnt
数组的第 6(C语言中下标从 0 开始) 个槽位中,并且将值置为 1。
cnt[5] = 1;
- 有关「 哈希 」的更多内容,可以参考:夜深人静写算法(九)- 哈希表。
3、计数的实现
- 「 计数 」 就比较简单了,直接对「 哈希 」的位置执行自增操作即可,如下:
++cnt[5];
五、🥦算法描述
1、问题描述
给定一个 n n n 个元素的整型数组,数组下标从 0 0 0 开始,且数组元素范围为 [ 1 , 1 0 5 ] [1, 10^5] [1,105],采用「 计数排序 」将数组按照 「升序」排列。
2、算法过程
整个算法的执行过程分以下几步:
1) 初始化计数器数组cnt[i] = 0
,其中 i ∈ [ 1 , 1 0 5 ] i \\in [1, 10^5] i∈[1,105];
2) 令 i = 0 → n − 1 i = 0 \\to n-1 i=0→n−1,循环执行计数器数组的自增操作++cnt[a[i]]
;
3) 令 i = 1 → 100000 i = 1 \\to 100000 i=1→100000,检测cnt[i]
的值,如果非零,则将cnt[i]
个i
的值依次放入原数组a[]
中。
六、🧶算法分析
1、时间复杂度
- 我们假设一次 「 哈希 」 和 「 计数 」 的时间复杂度均为 O ( 1 ) O(1) O(1)。并且总共 n n n 个数,数字范围为 1 → k 1 \\to k 1→k。
除了输入输出以外,「 计数排序 」 中总共有四个循环。
第一个循环,用于初始化 「 计数器数组 」,时间复杂度 O ( k ) O(k) O(k);
第二个循环,枚举所有数字,执行「 哈希 」 和 「 计数 」 操作,时间复杂度 O ( n ) O(n) O(n);
第三个循环,枚举所有范围内的数字,时间复杂度 O ( k ) O(k) O(k);
第四个循环,是嵌套在第三个循环内的,最多走 O ( n ) O(n) O(n),虽然是嵌套,但是它可第三个循环是相加的关系,而并非相乘的关系。
- 所以,总的时间复杂度为: O ( n + k ) O(n + k) O(n+k)
2、空间复杂度
- 假设最大的数字为 k k k,则空间复杂度为 O ( k ) O(k) O(k)。
七、🧢优化方案
「 计数排序 」在众多排序算法中效率最高,时间复杂度为 O ( n + k ) O(n + k) O(n+k) 。
但是,它的缺陷就是非常依赖它的数据范围。必须为整数,且限定在 [ 1 , k ] [1, k] [1,k] 范围内,所以由于内存限制, k k k 就不能过大,优化点都是常数优化了,主要有两个:
(1) 初始化 「 计数器数组 」 可以采用系统函数memset
,纯内存操作,由于循环;
(2) 上文提到的第三个循环,当排序元素达到 n n n 个时,可以提前结束,跳出循环。
八、💙源码详解
#include <stdio.h>
#include <string.h>
#define maxn 1000001
#define maxk 100001
int a[maxn];
int cnt[maxk];
void Input(int n, int *a) {
for(int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
}
void Output(int n, int *a) {
for(int i = 0; i < n; ++i) {
if(i)
printf(" ");
printf("%d", a[i]);
}
puts("");
}
void CountingSort(int n, int *a) { // (1)
int i, top;
memset(cnt, 0, sizeof(cnt)); // (2)
for(i = 0; i < n; ++i) { // (3)
++cnt[ a[i] ]; // (4)
}
top = 0; // (5)
for(i = 0; i < maxk; ++i) {
while(cnt[i]) { // (6)
a[top++] = i; // (7)
--cnt[i]; // (8)
}
if(top == n) { // (9)
break;
}
}
}
int main() {
int n;
while(scanf("%d", &n) != EOF) {
Input(n, a);
CountingSort(n, a);
Output(n, a);
}
return 0;
}
-
(
1
)
(1)
(1)
void CountingSort(int n, int *a)
为 计数排序 的实现,代表对a[]
数组进行升序排序。 -
(
2
)
(2)
(2) 利用
memset
初始化 计数器数组cnt
; - ( 3 ) (3) (3) 遍历原数组中的每个元素;
- ( 4 ) (4) (4) 相应数 的 计数器 增加1;
- ( 5 ) (5) (5) 栈顶指针指向空栈;
- ( 6 ) (6) (6) 如果 i i i 这个数的计数 c n t [ i ] cnt[i] cnt[i] 为零,则结束循环,否则进入 ( 7 ) (7) (7);
- ( 7 ) (7) (7) 将 i i i 放入原数组;
- ( 8 ) (8) (8) 计数器减一;
- ( 9 ) (9) (9) 当原数组个数 等于 n n n 跳出循环。
九、💗代码验证
- 比如,你可以在百度上搜索 代码在线提交、OnlineJudge、LeetCode、洛谷、HDOJ、POJ 等等的关键词,然后去找对应的题目提交验证你的代码的正确性。
🙉饭不食,水不饮,题必须刷🙉
C语言免费动漫教程,和我一起打卡! 🌞《光天化日学C语言》🌞
LeetCode 太难?先看简单题! 🧡《C语言入门100例》🧡
数据结构难?不存在的! 🌳《数据结构入门》🌳
LeetCode 太简单?算法学起来! 🌌《夜深人静写算法》🌌
以上是关于❤️ 比「 快速排序 」更高效的排序,但是也有其局限性 ❤️(推荐收藏)的主要内容,如果未能解决你的问题,请参考以下文章