❤️「 哈希表 」+ 「 队列 」将会擦出怎样的火花?❤️
Posted 英雄哪里出来
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤️「 哈希表 」+ 「 队列 」将会擦出怎样的火花?❤️相关的知识,希望对你有一定的参考价值。
- 本文已收录于专栏《画解数据结构》
零、📃前言
这个系列的文章中,我会着重讲解一些常见的 「 算法和数据结构 」 的设计思想,并且配上动图。希望读者能够带上自己的 「思考」,主要针对面试中常见的问题和新手朋友们比较难理解的点进行解析。当然,这个系列的文章,后面也会给出面向算法竞赛的提纲,如果有兴趣深入学习的欢迎在「评论区留言,一起成长交流」 。
零基础学算法的最好方法,莫过于刷题了。当然,刷题是不够的,刷的过程中也要多多总结,多多思考,养成 「经常思考」 的习惯,这就是所谓的 「 流水不腐,户枢不蠹 」,任何事情都是需要坚持的,刷题也一样,没有刷够足够的题,就很难做出系统性的总结。所以上大学的时候,我花了三年的时间来刷题, 工作以后还是会抽点时间出来刷题。
千万不要用工作忙来找借口,时间挤一挤总是有的。当然,每天不需要花太多时间在这个上面,把这个事情做成一个规划,按照长期去推进。反正也没有 KPI 压力,就当成是工作之余的一种消遣,还能够提升思维能力。
所以,无论你是 小学生,中学生,高中OIer,大学ACMer,职场人士,只要想开始,一切都不会太晚!
今天要讲的主要内容是:「 基数排序 」,希望读者朋友给个面子,帮我完成阅读,不能完读,我可就完犊子了 🤣,当然,如果你觉得还可以,给我个「 赞 」 和 「 收藏 」,这个对我很重要,谢谢了!
「 基数排序 」 很好的弥补了 「 计数排序 」 中待排序的数据范围过大的问题,它适合 「 范围大 」 且 「 数位少 」 的整数的排序。它将所有的整数认为是一个字符串,从最低有效位(最右边的)到 最高有效位(最左边的)开始迭代,我会尽量做到「 深入浅出 」,让 90% 的 「 零基础小白 」 也都能理解,真正做到 「让天下没有难学的算法」 。我知道这很难,但是我愿意尝试!我会尽量把文章写得有趣,一气呵成,不浪费读者的宝贵时间。毕竟,「 时间就是金钱 」。那么,接下来,让我们开始今天的主题:「 基数排序 」
🙉饭不食,水不饮,题必须刷🙉
C语言免费动漫教程,和我一起打卡! 🌞《光天化日学C语言》🌞
LeetCode 太难?先看简单题! 🧡《C语言入门100例》🧡
数据结构难?不存在的! 🌳《画解数据结构》🌳
LeetCode 太简单?算法学起来! 🌌《夜深人静写算法》🌌
那么,我的教程和别人的教程有什么不同的地方呢?
「第一步」简单释义: 我会简单解释一下这个算法的目的、思想、以及为什么叫这个名字以帮助记忆。
「第二步」核心思想: 我会大致介绍一下这个算法的核心思想。
「第三步」动图演示: 我会引入一个动图,并且用一个切实的例子展示一下算法执行的全过程。
「第四步」算法前置: 在学习这个算法之前,我们需要学习的前置内容有哪些?这些内容是需要事先去攻克的。
「第五步」算法描述: 细致的讲解整个算法的执行流程。
「第六步」算法分析: 对算法的时间复杂度和空间复杂度进行一个详细的分析。
「第七步」优化方案: 介绍一些可以优化的点。
「第八步」代码实践: 用 C/C++ 来实现上述算法。
「第九步」代码验证: 最后,我会推荐一些比较好用的在线评测系统来验证我们实现的算法的正确性。
一、🎯简单释义
1、算法目的
将原本乱序的数组变成有序,可以是 「升序」 或者 「降序」 (为了描述统一,本文一律只讨论 「 升序」 的情况)。
2、算法思想
首先,准备 10 个队列,进行若干次「 迭代 」。每次「 迭代 」,先清空队列,然后取每个待排序数的对应十进制位,通过「 哈希 」,映射到它「 对应的队列 」中,然后将所有数字「 按照队列顺序 」塞回「 原数组 」完成一次「 迭代 」。
可以认为类似「 关键字排序 」,先对「 第一关键字 」进行排序,再对「 第二关键字 」排序,以此类推,直到所有关键字都有序为止。
二、🧡核心思想
- 「迭代」:类似的事情,不停地做。
- 「哈希」:将一个数字映射到一个数组中。
- 「队列」:一种「 先进先出 」的数据结构。
三、🔆动图演示
1、样例
3121 | 897 | 3122 | 3 | 23 | 5 | 55 | 7 | 97 | 123 | 456 | 1327 |
- 初始情况下的数据如 图二-1-1 所示,基本属于乱序,纯随机出来的数据。
2、算法演示
- 接下来,我们来看下排序过程的动画演示。如 图二-2-1 所示:
3、样例说明
- 上图中 「 红色的数字位 」 代表需要进行 「 哈希 」 映射到给定 「 队列 」 中的数字位。
我们看到,首先程序生成了一个区间范围为 [ 0 , 9 ] [0, 9] [0,9] 的 「 基数队列 」。
然后,总共进行了 4 轮「 迭代 」(因为最大的数总共 4 个数位)。
每次迭代,遍历枚举 「 原数组 」 中的所有数,并且取得本次迭代对应位的数字,通过「 哈希 」,映射到它「 对应的队列 」中 。然后将 「 队列 」 中的数据按顺序塞回 「 原数组 」 完成一次「 迭代 」,4 次「 迭代 」后,一定可以保证所有元素都是 「升序」 排列的。
四、🌳算法前置
1、循环的实现
- 这个算法本身需要做一些「 循环 」进行枚举计算,所以你至少需要知道「 循环 」 的含义,这里以 「 c++ 」 为例,来看下一个简单的「 循环 」是怎么写的。代码如下:
int n = 128;
for(int i = 0; i < n; ++i) {
// TODO : 。。。
}
- 这个语句就是一个最简单的循环语句,它会将循环体内的语句执行 n n n 次,而这里的 n n n 等于 128 128 128,也就是会执行 128 128 128 次。
- 「 循环 」 是计算机完成 「 枚举 」 和 「 迭代 」 的基础操作。
2、哈希的实现
- 「 哈希 」就是将一个数字「 映射 」到一个「 数组 」中,然后通过数组的 「 取下标 」 这一步来完成 O ( 1 ) O(1) O(1) 的 「 查询 」 操作 。
- 所有 「 计数排序 」 对待排序数组中的元素,是有范围要求的,它的值不能超过数组本身的大小,这就是上文提到的 「 局限性 」。
- 如下代码所示,代表的是把 数字5 「 哈希 」到
cnt
数组的第 6(C语言中下标从 0 开始) 个槽位中,并且将值置为 1。
cnt[5] = 1;
- 有关「 哈希 」的更多内容,可以参考:夜深人静写算法(九)- 哈希表。
3、队列的实现
- 队列是一种 「 先进先出 」 的数据结构。本文会采用数字来实现。下文会有讲到,如果有兴趣了解更多内容,可以参考这篇文章:详解队列。
4、十进制位数计算
- 在进行排序过程中,我们需要取得一个数字 v v v 的十进制的第 k k k 位的值。如下
- v = a p 1 0 p + a p − 1 1 0 p − 1 . . . + a k 1 0 k + . . . + a 1 1 0 1 + a 0 1 0 0 v = a_p10^p + a_{p-1}10^{p-1}... + a_k10^k + ... + a_110^1 + a_010^0 v=ap10p+ap−110p−1...+ak10k+...+a1101+a0100
- 我们要得到的就是 a k a_k ak。
- 可以将 v v v 直接除上 1 0 k 10^k 10k 再模上 10,即 v 1 0 k m o d 10 \\frac v {10^k} \\ mod \\ 10 10kv mod 10。
- 正确性显而易见,比 1 0 k 10^k 10k 高的位,除完 1 0 k 10^k 10k 以后必然是 10 10 10 的倍数,所以模 10 10 10 以后答案为 0 0 0,不会产生贡献;比 1 0 k 10^k 10k 低的位,除完 1 0 k 10^k 10k 以后本身就已经变成了 0 0 0,更加不会产生贡献,所以剩下的只有 1 0 k 10^k 10k 的系数,即 a k a_k ak。
五、🥦算法描述
1、问题描述
给定一个 n n n 个元素的整型数组,数组下标从 0 0 0 开始,且数组元素范围为 [ 1 , 1 0 8 ) [1, 10^8) [1,108),采用「 基数排序 」将数组按照 「升序」排列。
2、算法过程
整个算法的执行过程分以下几步:
1) 定好进制,一般为 10 10 10,然后预处理 10 10 10 的幂,存储在数组中,PowOfBase[i]
代表 1 0 i 10^i 10i;
2) 由于数据范围为 [ 1 , 99999999 ] [1, 99999999] [1,99999999],即 8 8 8 个 9 9 9,最多 8 8 8 位,所以可以令 p o s = 0 → 7 pos = 0 \\to 7 pos=0→7,执行下一步;
3) 初始化 [ 0 , 9 ] [0,9] [0,9] 队列,对 n n n 个数字取 p o s pos pos 位,放入对应的队列中;
4) 从第 0 0 0 个队列到 第 9 9 9 个队列,将所有数字按照顺序取出来,放回原数组,然后 p o s = p o s + 1 pos = pos + 1 pos=pos+1,回到 3) 继续迭代;
六、🧶算法分析
1、时间复杂度
- 我们假设一次 「 哈希 」 以及 「 入队 」、「 出队 」 的时间复杂度均为 O ( 1 ) O(1) O(1)。并且总共 n n n 个数,数字位数为 1 → k 1 \\to k 1→k。
除了输入输出以外,「 基数排序 」 中总共有 k k k 轮 「 迭代 」。
每一轮「 迭代 」,都需要遍历数组中的所有数,进行 「 入队 」 操作,时间复杂度 O ( n ) O(n) O(n)。所有数都 「 入队 」 完毕以后,需要将所有队列中的数塞回 「 原数组 」 ,时间复杂度也是 O ( n ) O(n) O(n)。
- 所以,总的时间复杂度为: O ( n k ) O(nk) O(nk)
2、空间复杂度
- 空间复杂度需要看 「 队列 」 的实现方式,如果采用静态数组,那么总共的队列个数为 b b b 个(这里 b = 10 b = 10 b=10),每个队列最多元素为 n n n,则空间复杂度为 O ( n b ) O(nb) O(nb),牛逼啊!
- 如果利用 S T L STL STL 的队列动态分配内存,则可以达到 O ( n ) O(n) O(n) 。
七、🧢优化方案
「 基数排序 」 的时间复杂度为 O ( n k ) O(nk) O(nk) 。
其中 k k k 代表数字位的个数,所以比较依赖数据,如果最大的那个数的位数小,某次迭代下来,所有的数都已经放在一个队列中了,那就没必要继续迭代了。这里可以进行一波常数优化。
八、💙源码详解
#include <stdio.h>
#include <string.h>
const int MAXN = 100005; // (1)
const int MAXT = 8; // (2)
const int BASE = 10; // (3)
int PowOfBase[MAXT]; // (4)
int RadixBucket[BASE][MAXN]; // (5)
int RadixBucketTop[BASE]; // (6)
void InitPowOfBase() {
int i;
PowOfBase[0] = 1;
for(i = 1; i < MAXT当 Thrift 遇到 JDK Epoll Bug,将会擦出怎样的火花?