开篇介绍:
hello 大家,很高兴又能和大家在这个关于排序算法的知识分享空间里相遇。
在上一篇博客中,我们一同深入剖析了直接插入排序和希尔排序这两种经典的排序算法。为了让大家更好地理解,我们先从直接插入排序的基本思想入手,它就像是在整理一手扑克牌,每次从无序区间取出一张牌,插入到有序区间的合适位置,逐步构建有序序列。我们详细推导了它的执行过程,通过多个示例演示,大家能清晰看到每一个元素是如何一步步找到自己的 “归宿”。接着,我们分析了它的时间复杂度,在最好情况下,数据已经是有序的,时间复杂度为O(n);而在最坏情况下,数据是逆序的,时间复杂度为O(n2)。空间复杂度方面,它只需要常数级的额外空间,是一种原地排序算法。同时,直接插入排序是稳定的排序算法,因为相等元素的相对顺序在排序过程中不会被改变。
对于希尔排序,我们知道它是直接插入排序的改进版。它通过将数组按照一定的间隔划分为多个子数组,分别进行直接插入排序,然后逐步缩小间隔,直到间隔为 1,此时整个数组基本有序,再进行一次直接插入排序。我们探讨了希尔排序中间隔序列的选择对排序效率的影响,不同的间隔序列会带来不同的时间复杂度表现。虽然希尔排序的时间复杂度分析较为复杂,没有一个精确的数学表达式,但它在平均情况下的效率要优于直接插入排序。空间复杂度同样是O(1),不过希尔排序是不稳定的排序算法,因为在子数组的排序过程中,相等元素的相对位置可能会发生变化。通过大量的实例和对比,大家也切实感受到了这两种算法在不同数据规模和数据分布情况下的性能差异,从而对它们有了较为扎实的掌握。
然而,正如我在上一篇博客里所说的那样,排序算法的世界可谓是精彩纷呈、百花齐放。在计算机科学的浩瀚领域中,排序算法远远不止我们之前所了解的那几种。
我们都知道,排序在数据处理、信息检索、数据库管理等众多领域都有着至关重要的作用。比如在电商平台中,商品需要按照价格、销量等进行排序,方便用户查找;在学生成绩管理系统里,成绩要按从高到低或从低到高排序,以便进行统计和分析;在搜索引擎中,网页的排序更是直接影响用户获取信息的效率和准确性。不同的场景、不同的数据规模,往往需要不同的排序算法来应对,才能达到最优的效率。仅仅掌握冒泡排序、希尔排序这些常见的算法,对于我们在实际应用中灵活地选择和运用排序算法是远远不够的。
就好比在处理小规模数据且对稳定性要求较高时,直接插入排序可能是个不错的选择,它简单且能保证相等元素的相对顺序;而当面对大规模数据,想要提高排序效率时,希尔排序通过分组排序的方式,减少了元素的移动次数,优势就会显现出来。但这只是冰山一角,还有更多不同特点、不同适用场景的排序算法等待我们去探索和学习。
有些算法在数据规模较小时表现出色,但随着数据量的增大,效率会急剧下降;而有些算法则更适合处理特定范围的数据,或者在并行计算环境下能发挥更大的作用。比如选择排序,它有着独特的简洁性和直观性,虽然在时间复杂度上可能不是最优的,但在某些对代码简洁性要求较高,且数据规模不大的特定场景下,它的表现依然值得我们关注;而计数排序,则是一种基于非比较的排序算法,在处理一定范围整数数据时,能够展现出令人惊叹的高效性,突破了比较排序算法时间复杂度的下限,为我们在处理这类数据时提供了一种全新的、高效的思路。
所以,为了让大家能够更全面、更深入地理解排序算法,在各种复杂的实际情况中都能游刃有余地选择合适的排序方法,今天,我将带领大家走进选择排序和计数排序这两种同样非常重要的排序算法的世界。
接下来,我们首先来学习选择排序。我们会从它的基本思想出发,详细讲解它是如何在每一轮中选择出最小(或最大)的元素,并将其放置到正确的位置上。然后通过具体的例子,一步步展示选择排序的执行过程,让大家直观地看到数组是如何被逐步排序的。之后,我们会分析它的时间复杂度、空间复杂度以及稳定性等特性,探讨它在不同情况下的优缺点和适用场景。
在掌握了选择排序之后,我们再来看计数排序。计数排序的思想与之前的比较排序算法有很大的不同,它不通过元素之间的比较来确定顺序,而是利用元素的数值范围,通过计数的方式来确定每个元素的位置。我们会详细介绍计数排序的具体步骤,包括如何确定计数数组的大小,如何进行计数,以及如何根据计数结果重新排列元素。
希望通过今天的学习,大家能够对选择排序和计数排序有全面且深入的理解,进一步丰富自己在排序算法方面的知识储备,为今后在实际项目中更好地应用排序算法打下坚实的基础。现在,就让我们一起开启这段新的学习旅程,去揭开选择排序和计数排序的神秘面纱,探寻它们各自的奥秘与魅力吧!
选择排序:
其实呢,选择排序这一个排序算法,效率确实是不高的,但是呢,在某些特定环境下,它也有着它的得天独厚的优势。
那么,接下来就让我们看看实现选择排序的思路吧:
实现思路:

大家可以根据上图知道个大概,不过在这里我讲的是改进版,而动图中演示的则是只找小的
选择排序的总体实现思路是一种基于 “逐步筛选最值并固定位置” 的原地排序方法,核心逻辑是通过双指针动态划分无序区间,在每一轮排序中同时定位区间内的最大值和最小值,将其分别交换至区间两端以形成有序部分,再通过收缩缩无序区间范围实现整体排序。具体可拆解为以下层层递进的逻辑:
区间划分的动态管理以双指针
start和end作为无序区间的边界标识,初始时start指向数组首位(0),end指向数组末位(size-1),此时整个数组均为无序区间。随着排序推进,start持续右移、end持续左移,意味着无序区间从两侧向中间逐步收缩,而start左侧与end右侧的元素已完成排序并固定位置,形成 “有序部分”。这种设计避免了额外存储空间的使用,通过指针移动实现对排序范围的精准控制。双最值同步定位机制在每一轮无序区间(
[start, end])中,通过两个下标变量maxi和mini同步追踪最大值与最小值的位置。初始时二者均指向start(默认区间首个元素为当前最大 / 最小值),随后从start+1到end遍历整个区间:若当前元素大于maxi指向的值,则更新maxi为当前下标;若当前元素小于mini指向的值,则更新mini为当前下标。采用两个独立if判断而非if-else,确保即使某元素同时大于当前最大值且小于当前最小值,也能被正确记录,避免遗漏最值。最值交换的有序化策略定位到最值后,分两步完成有序化:
- 先将最大值交换至区间末尾(
end位置),使最大值成为右侧有序部分的新成员; - 处理特殊情况:若最小值原本位于
end位置(mini == end),则交换后最小值已被移至maxi位置,需将mini更新为maxi以保证后续操作准确性; - 再将最小值交换至区间开头(
start位置),使最小值成为左侧有序部分的新成员。这种 “先右后左” 的交换顺序,既确保了每次操作固定两个元素的位置,又通过特殊情况处理避免了最值位置被干扰导致的错误。
- 先将最大值交换至区间末尾(
迭代收敛的终止条件每轮排序结束后,通过
start++和end--缩小无序区间,重复上述 “定位 - 交换” 过程,直至end <= start(区间内仅剩 0 或 1 个元素)。此时数组从0到size-1已完全有序,排序终止。这种迭代方式保证了每个元素都能被放置到正确位置,且每轮操作的时间成本集中在区间遍历,整体逻辑具有极强的连贯性和可追溯性。
完整代码:
那么其实选择排序的实现思路就是如上,还是比较简单的,我下面先给出完整代码共大家参考:
//选择排序
void selectsort(int* arr, size_t size)
{
//选择排序,一个比冒泡排序还要low的排序算法
//唯一的价值就是让我们知道,这个排序算法没用
//选择排序的基本思想:
//每一次从待排序的数据元素中选出最小(或最大)的一个元素,
//存放在序列的起始位置,直到全部待排序的数据元素排完 。
//1. 在元素集合array[i]--array[n-1] 中选择关键码最大(小)的数据元素
//2. 若它不是这组元素中的最后一个(第一个)元素,
//则将它与这组元素中的最后一个(第一个)元素交换
//3. 在剩余的array[i]--array[n - 2](array[i + 1]--array[n - 1]) 集合中,
//重复上述步骤,直到集合剩余1 个元素
//那么选择排序的思路其实也是很简单
//就是去挑选序列中最大的一个值和最小的一个值
//然后把最小的值放在前面,最大的值放在后面
//经过一轮交换之后,再去把挑选剩下的序列中最大的一个值和最小的一个值
//并且重复上面的操作
//其实就是使用双指针去逐渐缩小无序序列的范围
int start = 0;//定位到序列的第一个数据
int end = size - 1;//定位到序列的最后一个数据
//在start和end没相遇之前,就代表序列还没有达到有序
while (end > start)//等于就有序了
{
int maxi = start;//先认为序列中最大的数据就是下标为start的数据
int mini = start;//先认为序列中最小的数据就是下标为start的数据
//接下来就要去遍历数组去找到最大的数据和最小的数据
for (int i = start + 1; i <= end; i++)
{
//上面的循环条件还是挺有说法的
//因为我们要找到最大和最小的,所以我们肯定就要遍历数组
//那么,我们肯定就要从数组的第一个数据开始,找到数组的最后一个数据
//那么我们为什么不直接从0开始,然后再到size-1结束呢
//那是因为,每次我们找到了最大与最小数据之后,
//数组中无序的序列的数据个数就会少两个,
//比如 2 6 4 5 经过第一次交换之后,就会变为2 5 4 6
//那么此时start由原本的0变为1(下标),end由原本的3变为2(下标)
//此时数组中无序的序列就从原本的4个为2个,我们的循环遍历范围也就可以随之减少
//那我们之所以要从start+1开始而不从start开始
//是因为我们一开始就默认了最大和最小的值是下标为start的数据
//所以我们再去比较start没意义,应该从它的后面一个开始
if (arr[i] > arr[maxi])
{
maxi = i;//哪个下标的数据比maxi下标对应的数据大,就将maxi更新为这个下标
}
//要用两个if语句,避免遗漏
//用if else的话,达到一个满足条件之后,就会不看另一个语句
if (arr[i] < arr[mini])
{
mini = i;//哪个下标的数据比mini下标对应的数据小,就将mini更新为这个下标
}
}
//找到了无序序列中的最大值和最小值,就要把它们放在start和end对应的位置
//其实就是交换数据
swap(&arr[maxi], &arr[end]);
//这里还是有个小细节需要注意的
//万一在这个交换数据中,mini指向的数据和end指向的数据一样呢
//那么这个时候交换了数据之后,mini还是指向原本end的位置
//可是此时的那个位置,可不是原本最小的数据了,而是变成了原本maxi指向的数据
//原本的end指向的数据,到了maxi指向的位置
//所以,我们就得通过判断语句去处理这个情况
if (mini == end)//如果mini指向的数据和end指向的数据一样
{
mini = maxi;//就将mini更新为maxi,在经过交换之后,maxi指向的数据已经变成了原本end指向的数据
}
swap(&arr[mini], &arr[start]);
//要记得更新start和end的值
//将start向后挪,end向前挪
//因为原本start的位置已经是原本序列的最小的数据
//而原本end的位置则已经是原本序列的最大的数据
start++;
end--;
}
}
详细解释:
光看代码大家可能还是有点蒙,我再给出对代码的详细解释:
一、核心思想与整体框架
选择排序的核心思想是:从待排序的序列中,每一轮同时找出最大和最小的元素,将最小元素放在序列的起始位置,将最大元素放在序列的结束位置,然后缩小待排序序列的范围,重复上述操作,直到整个序列变得有序。这种方式通过双指针控制待排序区间,实现了 “一次遍历处理两个最值” 的高效逻辑,属于原地排序算法,无需额外的存储空间。
二、关键变量的定义与作用
start指针:初始值为 0,用于标记当前待排序(无序)序列的起始位置。所有位于start左侧的元素(即arr[0]到arr[start-1])都是已经排好序的,不再参与后续的排序过程。随着排序的进行,start会不断向后移动(start++),意味着左侧的有序部分在持续扩大。end指针:初始值为数组长度减一(size - 1),用于标记当前待排序(无序)序列的结束位置。所有位于end右侧的元素(即arr[end+1]到arr[size-1])都是已经排好序的,不再参与后续的排序过程。随着排序的进行,end会不断向前移动(end--),意味着右侧的有序部分在持续扩大。maxi下标:每一轮排序开始时初始化为start,用于记录当前待排序序列中最大值所在的位置。通过遍历序列,不断更新maxi的值,最终精准定位最大值的下标,避免了直接交换元素带来的频繁赋值操作,提高效率。mini下标:每一轮排序开始时同样初始化为start,用于记录当前待排序序列中最小值所在的位置。与maxi类似,通过遍历序列不断更新,最终定位最小值的下标,实现与maxi的配合,一次遍历即可同时找到最大和最小值。
三、详细执行步骤
初始化双指针,确定初始待排序区间首先设定
start = 0,end = size - 1,此时整个数组(从start到end)都是待排序的无序序列。例如,对于数组[5, 2, 7, 1, 3],初始的待排序区间就是整个数组,start指向 5,end指向 3。循环处理待排序区间,直至区间有序排序过程在
while (end > start)的循环中进行,这个条件的意义是:只有当待排序区间内至少有两个元素时,才有排序的必要。如果end <= start,说明区间内只剩一个元素(或没有元素),而单个元素本身就是有序的,排序即可终止。遍历待排序区间,定位最大和最小值每一轮循环开始时,先将
maxi和mini都初始化为start,即默认当前待排序区间的第一个元素既是最大值也是最小值。随后,从i = start + 1开始遍历到i = end(覆盖整个待排序区间):- 当遍历到的元素
arr[i]大于arr[maxi]时,说明找到了更大的元素,此时将maxi更新为i,确保maxi始终指向当前区间内的最大值。 - 当遍历到的元素
arr[i]小于arr[mini]时,说明找到了更小的元素,此时将mini更新为i,确保mini始终指向当前区间内的最小值。 - 这里必须使用两个独立的
if语句,而不能用if-else结构。因为如果用if-else,当一个条件满足时会直接跳过另一个条件,可能导致漏判(例如,某元素同时比当前最大值大且比当前最小值小,if-else只会更新其中一个下标,造成最值定位错误)。 - 遍历从
start + 1开始而非start,是因为maxi和mini已经初始化为start,无需再与自身进行比较,减少无意义的操作。
- 当遍历到的元素
交换最大值到待排序区间的末尾找到最大值的位置
maxi后,将arr[maxi](最大值)与arr[end](当前待排序区间的最后一个元素)交换。这一步的目的是将当前区间内的最大值 “固定” 在区间的末尾,使其成为右侧有序部分的新成员。例如,若待排序区间为[5, 2, 7, 1, 3],maxi指向 7(下标 2),end指向 3(下标 4),交换后数组变为[5, 2, 3, 1, 7],最大值 7 被固定在末尾。处理 “最小值位置被最大值交换干扰” 的特殊情况交换最大值后,可能出现一种特殊场景:原本的最小值恰好位于
end位置(即mini == end)。此时,交换maxi和end后,最小值会被移动到maxi的位置,而mini仍然指向原来的end位置(此时该位置已不是最小值)。因此,必须添加判断:如果mini == end,则将mini更新为maxi,确保后续交换最小值时能正确定位。例如,若待排序区间为[4, 1, 3, 2],mini指向 1(下标 1),maxi指向 4(下标 0),end指向 2(下标 3),交换后数组变为[2, 1, 3, 4],此时mini仍指向 1(正确);但如果mini原本指向end位置,交换后就必须更新mini。交换最小值到待排序区间的开头处理完特殊情况后,将
arr[mini](最小值)与arr[start](当前待排序区间的第一个元素)交换。这一步的目的是将当前区间内的最小值 “固定” 在区间的开头,使其成为左侧有序部分的新成员。例如,上述例子中,mini指向 1(下标 1),start指向 2(下标 0),交换后数组变为[1, 2, 3, 4],最小值 1 被固定在开头。缩小待排序区间,进入下一轮循环完成最大值和最小值的交换后,执行
start++和end--,使待排序区间的起始位置后移、结束位置前移,意味着下一轮排序的区间范围缩小(排除已固定的最小值和最大值)。例如,原本start=0、end=4,更新后变为start=1、end=3,待排序区间从[0,4]缩小为[1,3]。重复上述 3 到 7 的步骤,直到end <= start,整个数组排序完成。
四、核心细节与设计巧思
- 双指针的动态收缩:通过
start和end的移动,精准控制待排序区间的范围,每轮排序后区间长度减少 2,避免重复处理已排序的元素,提高效率。 - 双最值的同步定位:一次遍历同时找到最大和最小值,相比 “单次找一个最值” 的方式,减少了一半的遍历次数,优化了时间开销。
- 特殊情况的容错处理:针对 “最小值位置被最大值交换干扰” 的场景,通过
if (mini == end)的判断及时修正mini的位置,确保排序逻辑的正确性,体现了算法设计的严谨性。
例子:
一、核心思想与变量说明
选择排序通过双指针 start(起始边界)和 end(结束边界)划定待排序区间,每轮同时找出区间内的最大值和最小值,分别交换至区间两端,再收缩区间重复操作,直至整体有序。关键变量:
start:初始为0,标记待排序区间起点,左侧为已排序部分。end:初始为size-1(数组长度为 5,故end=4),标记待排序区间终点,右侧为已排序部分。maxi:记录区间内最大值下标,每轮初始为start。mini:记录区间内最小值下标,每轮初始为start。
二、完整排序步骤(含特殊场景)
初始数组:[4, 1, 3, 2, 5],start=0,end=4(待排序区间 [0,4])
第一轮排序:处理区间 [0,4]
定位最大、最小值:
- 初始化
maxi=0(暂认4最大),mini=0(暂认4最小)。 - 遍历
i=1到i=4(元素1,3,2,5):i=1(1):1 < 4→mini=1;1 < 4→maxi不变。i=2(3):3 > 1→mini不变;3 < 4→maxi不变。i=3(2):2 > 1→mini不变;2 < 4→maxi不变。i=4(5):5 > 4→maxi=4;5 > 1→mini不变。
- 结果:
maxi=4(最大值5),mini=1(最小值1)。
- 初始化
交换最大值到区间末尾:
- 交换
arr[4](5)与arr[4](5)→ 数组不变(自身交换)。
- 交换
处理特殊情况:
- 检查
mini == end?mini=1,end=4→ 不相等,无需处理。
- 检查
交换最小值到区间开头:
- 交换
arr[1](1)与arr[0](4)→ 数组变为[1, 4, 3, 2, 5]。
- 交换
收缩区间:
start=1,end=3(新区间[1,3],元素[4,3,2])。
第二轮排序:处理区间 [1,3]
定位最大、最小值:
- 初始化
maxi=1(暂认4最大),mini=1(暂认4最小)。 - 遍历
i=2到i=3(元素3,2):i=2(3):3 < 4→mini不变;3 < 4→maxi不变。i=3(2):2 < 4→mini=3;2 < 4→maxi不变。
- 结果:
maxi=1(最大值4),mini=3(最小值2)。
- 初始化
交换最大值到区间末尾:
- 交换
arr[1](4)与arr[3](2)→ 数组变为[1, 2, 3, 4, 5]。
- 交换
处理特殊情况:
- 检查
mini == end?mini=3,end=3→ 相等!需将mini更新为maxi=1(因交换后,原end位置的2已移至maxi=1位置)。
- 检查
交换最小值到区间开头:
- 交换
arr[mini=1](2)与arr[start=1](2)→ 数组不变(自身交换)。
- 交换
收缩区间:
start=2,end=2(新区间[2,2],仅剩元素3)。
终止条件:end <= start(2 <= 2)
循环终止,数组已完全有序:[1, 2, 3, 4, 5]。
三、特殊场景的关键处理(第二轮中的典型案例)
在第二轮排序中,mini 初始指向 3(元素 2),而 end 也为 3,形成 mini == end 的特殊情况:
- 交换最大值(
maxi=1的4)与end=3的2后,原最小值2被移至maxi=1位置,此时mini仍指向3(但该位置已变为4)。 - 若不更新
mini = maxi,后续交换会错误地将arr[3](4)与arr[start=1]交换,导致数组变为[1,4,3,2,5],排序失败。 - 正确更新后,
mini=1指向真实最小值2,确保交换逻辑正确。
四、多轮对比与总结
| 轮次 | 待排序区间 | 最大值位置 | 最小值位置 | 交换后数组 | 收缩后区间 |
|---|---|---|---|---|---|
| 初始 | [0,4] | - | - | [4,1,3,2,5] | - |
| 1 | [0,4] | 4(5) | 1(1) | [1,4,3,2,5] | [1,3] |
| 2 | [1,3] | 1(4) | 3(2) | [1,2,3,4,5] | [2,2] |
如何大家,看完了相信大家对选择排序的理解,肯定是蹭蹭蹭的往上涨。
计数排序:
计数排序呢,这个排序,一个比较典型的用空间换时间的排序算法,怎么说呢,这个排序算法的效率,其实有时候是真的高的可怕,高的让人怀疑人生,但是肯定的,它也有局限性,比如只能排整数等等。
实现思路:

大家可以先看上面的这个动图理解理解,然后再结合下面的解析去思考,想必理解计数排序就不难
计数排序是一种非比较类排序算法,在 “待排序元素为整数且元素范围相对集中” 的特定场景下,能突破比较类排序算法的时间复杂度下限(O(nlogn)),实现极高效率。其核心逻辑是借助 “哈希直接定址法” 的变形思路,通过统计元素出现频次、基于频次重新排布元素,最终完成数组有序化,具体实现思路可拆解为以下三部分:
一、核心原理与操作步骤
计数排序的本质是 “鸽巢原理” 的工程化应用 —— 每个元素对应一个 “鸽巢”(计数数组的下标),通过统计每个 “鸽巢” 中元素的数量(频次),再按 “鸽巢” 顺序将元素取出并重新排列,核心操作分为两步:
- 统计相同元素出现次数:创建一个 “计数数组”,该数组的下标与待排序元素存在映射关系,数组元素的值表示对应待排序元素的出现频次,通过遍历待排序数组,完成所有元素的频次统计;

- 根据统计结果回收元素:按计数数组的下标升序(或降序)遍历,根据每个下标对应的频次,将 “下标映射回的原始元素” 重复填入原待排序数组,直到所有频次消耗完毕,此时原数组即变为有序数组。
二、关键问题与解决方案:内存优化与映射设计
若直接以元素值作为计数数组下标,当元素范围较大时会产生严重内存浪费。例如数组[1000, 1001, 1002],直接开辟 0-1002 的数组需 1003 个空间,但仅 3 个空间被使用。为解决此问题,需通过 "相对映射" 机制压缩空间,具体设计如下:
界定元素实际范围:
- 找出数组中的最大值(
max)和最小值(min),确定元素的分布区间[min, max]; - 例如数组
[5, 2, 8, 2, 9],min=2,max=9,元素分布在[2,9]区间。
- 找出数组中的最大值(
计算计数数组的精确大小(
range):- 计算公式:
range = max - min + 1; - 加 1 的数学依据:区间
[min, max]包含的整数个数为max - min + 1。例如[2,9]包含 8 个整数(2-9),9-2+1=8,因此计数数组需 8 个空间才能完全覆盖所有元素; - 若不加 1,
range = max - min会导致少一个空间。例如[2,9]的max - min=7,计数数组仅 7 个空间(0-6),无法容纳9-2=7的映射下标,导致越界。
- 计算公式:
建立元素与计数数组下标的映射规则:
- 任意元素
arr[i]在计数数组中的对应下标为:index = arr[i] - min; - 该映射将
[min, max]区间的元素压缩到[0, range-1]区间,完全避免内存浪费; - 例如
arr[i]=9,min=2,则index=9-2=7,对应计数数组的第 7 个空间(0-based)。
- 任意元素

三、具体实现步骤(含底层逻辑)
步骤 1:遍历数组获取最大值(max)和最小值(min)
操作目的:为后续计算range和建立映射提供基础数据。
初始化规则:
- 必须将
max和min初始化为数组第一个元素(arr[0]),而非固定值 0; - 若初始化为 0,当数组元素全为正数(如
[3,5,7])时,min会错误地保持 0,导致后续映射偏差(如元素 3 会被映射为 3-0=3,而非正确的 3-3=0)。
- 必须将
遍历与更新逻辑:
- 从
i=1开始遍历(arr[0]已用于初始化),直到i < size(size为数组长度); - 每次遍历执行两个独立判断:
- 若
arr[i] > max,则max = arr[i](更新最大值); - 若
arr[i] < min,则min = arr[i](更新最小值);
- 若
- 示例:数组
[5, 2, 8, 2, 9],遍历后max=9,min=2。
- 从
步骤 2:创建并初始化计数数组
操作目的:开辟用于存储元素频次的空间,并确保初始频次为 0。
内存开辟方式:
- 使用
calloc(range, sizeof(int))动态创建计数数组(int* count); - 选择
calloc而非malloc的原因:calloc会自动将所有元素初始化为 0,而计数数组需要从 0 开始统计频次,省去手动初始化步骤; - 例如
range=8时,calloc会创建 8 个 int 类型空间,每个值均为 0。
- 使用
内存容错处理:
- 若
count == NULL,说明内存开辟失败(如内存不足); - 必须通过
perror("calloc count false:")打印错误信息,并通过exit(1)终止程序,避免后续对空指针的操作导致崩溃。
- 若
步骤 3:统计元素出现频次
操作目的:记录每个元素在待排序数组中出现的次数,为后续重构数组提供依据。
- 遍历与统计逻辑:
- 从
i=0遍历至i < size,覆盖数组所有元素; - 对每个元素
arr[i],计算映射下标index = arr[i] - min; - 执行
count[index]++,表示该元素的出现次数加 1; - 示例:数组
[5, 2, 8, 2, 9],min=2:arr[0]=5→index=5-2=3→count[3] = 1;arr[1]=2→index=0→count[0] = 1;arr[2]=8→index=6→count[6] = 1;arr[3]=2→index=0→count[0] = 2;arr[4]=9→index=7→count[7] = 1;- 统计后
count = [2,0,0,1,0,0,1,1],分别对应元素 2(2 次)、5(1 次)、8(1 次)、9(1 次)。
- 从
步骤 4:基于计数数组重构原数组
操作目的:将统计好的频次转化为有序数组,直接覆盖原数组实现原地排序。
索引变量设计:
- 定义
sign = 0,用于标记原数组中当前待填充的位置,确保元素连续存储; - 每次填充后
sign++,移动到下一个位置。
- 定义
重构逻辑:
- 从
i=0遍历计数数组至i < range(按下标升序,保证结果为升序); - 当
count[i] > 0时,执行循环填充:- 将
i + min(映射回原始元素值)填入arr[sign]; sign++(移动填充位置);count[i]--(减少剩余频次);- 重复直到
count[i] = 0,再处理下一个下标;
- 将
- 示例:基于
count = [2,0,0,1,0,0,1,1],min=2:i=0,count[0]=2:填充0+2=2→arr[0]=2,sign=1,count[0]=1;再填充2→arr[1]=2,sign=2,count[0]=0;i=1,count[1]=0:跳过;i=3,count[3]=1:填充3+2=5→arr[2]=5,sign=3;i=6,count[6]=1:填充6+2=8→arr[3]=8,sign=4;i=7,count[7]=1:填充7+2=9→arr[4]=9,sign=5;- 重构后原数组变为
[2,2,5,8,9],完成排序。
- 从
空间复用原理:
- 直接在原数组上填充,无需额外开辟空间;
- 计数数组已记录所有元素的 "值"(通过
i + min还原)和 "次数"(count[i]),原数组的原始数据已无保留必要,完全不影响排序结果。
步骤 5:释放动态内存
操作目的:避免内存泄漏,确保程序内存管理规范。
- 排序完成后,必须通过
free(count)释放步骤 2 中calloc开辟的内存; - 若不释放,该内存块会一直被占用(直到程序结束),多次调用排序函数时会导致内存耗尽。
四、核心细节与易错点总结
max和min的初始化:必须用arr[0]而非 0,否则会在数组元素全为正数 / 负数时出错;range的计算:max - min + 1是数学上的精确解,少加 1 会导致下标越界;- 映射规则的可逆性:
index = arr[i] - min和原始值 = i + min是互逆操作,确保元素值正确还原; - 计数数组的遍历顺序:按
i=0到i < range遍历得到升序,按i=range-1到i >=0遍历得到降序; - 内存释放:
free(count)不可遗漏,否则会造成内存泄漏。
关键:
其实大家对于计数排序,一定一定一定要知道的点就是,我们是再创建一个数组,然后用这个数组的下标加上待排序数组的最小值去表示待排序数组中的数据,然后用这个数组的下标对应的数据表示这个下标加上待排序数组的最小值的结果在待排序数组中出现了几次,统计完了之后,再按下标从0开始,将下标对应数据非0的下标加上min去表达,数据有多少,就表达几次。
大家一定要深知这一点。
完整代码:
那么大家知道了之后,也就能够写出代码了,下面我就给出计数排序的完整代码:
//计数排序
void countsort(int* arr, size_t size)
{
//计数排序,一个在某些时刻拥有难以估量的牛波一的算法
//计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
//(1)统计相同元素出现次数
//(2)根据统计的结果将序列回收到原来的序列中
//那么它的实现思路,其实是用到了计数数组的思路
//我们将要排序的数组的每个数据出现了几次都统计一下
//接着我们再创建一个数组,然后将这个数组的下标(为要排序的数组的每个数据)
//所指向数据是这个下标在要排序数组中出现的次数
//比如待排序数组:5 4 6 2 2
//那么这个时候我们就要创建一个计数数组count
//然后呢,count[5]==1,count[2]==2,count[4]==1,count[6]==1
//其实就是让计数数组的下标为待排序数组中的数据的时候,记录这个下标在要排序数组中出现的次数
//但是要是待排序数组中是101 102 105 100等的数据
//我们总不可能去开辟一个下标从0到105的数组
//那么根据上面的说法,只会有100 101 102 105这几个下标对应的空间会记录次数
//其他的空间就全部浪费掉了
//所以,为了避免这种情况,
//我们就得去统计待排序数组中最大的数减去最小的数再加上1,为什么加1会在后面讲
//我们用变量range去接收结果
//所以,我们就得先遍历待排序数组,去找出其中的最大值和最小值,这个就和选择排序有点像了
//这个range不是指待排序数组中有几个数据,这一点要注意,有几个数据是在size中
//这个range是后面开辟计数数组所需的数据个数
//换句话来说就是,range为多少,那么计数数组count中就有几个数据
//为什么要这样呢?其实本质上还是为了避免上面所说的内存浪费的现象
//因为我们不知道数据的范围是什么,但是我们数组又都是从0开始往后开辟的
//所以,我们就要用相对映射,即将待排序数组中的数据减去其中的最小值
//然后这个结果去对应体计数数组的下标
//比如待排序数组105 102 100 101
//那么这个时候range=105-100+1=6,所以,我们只开辟5个空间的计数数组
//又因为数组肯定都是从0开始的,不能说是从100开始的
//所以开辟的计数数组的下标就是0 1 2 3 4 5(没有6哦,因为数组从0开始)
//所以,我们就用105-100==5,102-100==2,100-100=0,101-100=1
//这几个结果去对应计数数组的下标,102对应的是下标2,100对应的是下标0
//101对应的是下标1,而105对应的是下标5
//那么,要注意105,这个就是关键了,可以看到,105要去存储在计数数组下标为5的位置
//那要是前面的range只是简单的max-min,那么结果就会为5
//开辟的计数数组的下标范围也只会是从0到4,
//就不会有5,那么这个时候我们的105就无处存储
//所以,我们的range要是加一的,就是为了能将待排序数组中最大的值能够有地方存储
//即使是对应105 10这种相隔较大两个数据进行排列
//计数数组的下标也会覆盖到
//此时range==105-10+1=96,所以计数数组开辟的下标就是从0到95
//正好可以到105-10==95,至于中间的浪费的,也只能让它浪费了
//当我们的计数数组统计完毕之后
//我们就可以利用计数数组去让原本无序的数组变得有序了
//那么是如何实现的呢?
//其实也挺简单,就是让计数数组从下标0开始,
//下标所对于的数据大小为多少,就表达多少次这个下标+min
//之所以加min,是因为我们前面计数的时候,
//是将待排序数组减去min后去对应下标并统计数据出现次数的
//然后呢,为了避免再开辟一个数组空间
//所以我们直接在原本的待排序数组进行计数数组的表达
//因为正好计数数组中所有次数加起来就是待排序数组的数据个数
//不用担心原本待排序的数组的数据会不见
//我们早已把它保存到计数数组的下标中
//计数数组中那些不为0的数据所对应的下标加上min,就是待排序数组的数据了
//而且我们将计数数组从0开始表达,正好能够符合升序的要求,其实也是必然的
//所以,我们还得定义一个变量
//每当计数数组中下标+min存放在原本待排序数组一次,这个变量就得++
//使得数据能够连续存储
//要将max和min都初始化为待排序数组的第一个数据
//这样子才能用于下面的比较,因为我们的max和min本质上就是待排序数组内的
//而要是把max和min初始化为0的话
//如果数组中全部为正数的话,那么就永远找不到比min小的数据
//这一点还是需要注意的
int max = arr[0];
int min = arr[0];
for (int i = 1; i < size; i++)//因为arr[0]已经用了,所以i从1开始
{
if (arr[i] > max)
{
max = arr[i];
}
if (arr[i] < min)
{
min = arr[i];
}
}
//此时出来之后就找到了最大值和最小值
//计算range,即为计数数组的大小
int range = max - min + 1;
//创建计数数组
int* count = (int*)calloc(range, sizeof(int));//使用calloc,直接全部初始化为0
if (count == NULL)
{
perror("calloc count false:");
exit(1);
}
for (int i = 0; i < size; i++)
{
//i依旧是小于size,因为我们是要遍历待排序数组
//统计每个数据出现的次数,并将这个次数
//存进计数数组的数据-min的下标所对应的数据
//直接如下面++就行
//有出现一次,就会对count的arr[i] - min下标所对应的数据加一
//要是没出现,那也不可能,因为我们是将待排序数组的所有数据都遍历了一遍
count[arr[i] - min]++;
}
int sign = 0;//用于表达计数数组到待排序数组时的连续储存
for (int i = 0; i < range; i++)
{
//此时i的范围就要小于range了,因为我们是遍历计数数组
//将计数数组中数据不为0的下标加上min后
//丢到待排序数组中的
while (count[i])
{
arr[sign] = i + min;//时存放下标加min哦
sign++;
count[i]--;//丢完一次后,要记得对数据减一
}
}
free(count);//要记得销毁我们申请的count数组的空间
}
例子:
以待排序数组 [105, 102, 100, 101, 102] 为例,从变量初始化到内存释放,每一步均拆解至代码逻辑与数据变化细节,完整呈现计数排序的执行过程:
一、初始状态定义
- 待排序数组:
int arr[] = {105, 102, 100, 101, 102} - 数组长度:
size_t size = 5(共 5 个元素,下标 0~4) - 核心目标:将数组按升序排列为
[100, 101, 102, 102, 105]
二、步骤 1:遍历数组,获取最大值(max)与最小值(min)
1.1 变量初始化逻辑
- 必须将
max和min初始化为数组第一个元素(arr[0]),而非固定值 0。- 若初始化为 0,当数组元素均大于 0(如本例中最小元素 100)时,
min会错误保持 0,导致后续映射偏差。
- 若初始化为 0,当数组元素均大于 0(如本例中最小元素 100)时,
- 初始值:
int max = arr[0] = 105,int min = arr[0] = 105。
1.2 遍历对比过程(i从1到4,覆盖剩余元素)
| 遍历次数 | i 值 | 当前元素 arr[i] | 与 max 对比(max=105) | 与 min 对比(min=105) | max 更新后 | min 更新后 |
|---|---|---|---|---|---|---|
| 1 | 1 | 102 | 102 < 105 → 不更新 | 102 < 105 → 更新 | 105 | 102 |
| 2 | 2 | 100 | 100 < 105 → 不更新 | 100 < 102 → 更新 | 105 | 100 |
| 3 | 3 | 101 | 101 < 105 → 不更新 | 101 > 100 → 不更新 | 105 | 100 |
| 4 | 4 | 102 | 102 < 105 → 不更新 | 102 > 100 → 不更新 | 105 | 100 |
1.3 步骤 1 结果
- 最大值
max = 105,最小值min = 100,明确元素分布区间为[100, 105]。
三、步骤 2:计算计数数组大小(range)并创建计数数组
2.1 计算 range 的逻辑
- 公式推导:区间
[min, max]包含的整数个数 =max - min + 1(如[100, 105]含 6 个整数:100、101、102、103、104、105)。 - 本例计算:
int range = max - min + 1 = 105 - 100 + 1 = 6。 - 关键细节:若少加 1(
range=5),计数数组仅 5 个空间(下标 0~4),无法容纳105 - 100 = 5的映射下标,导致 105 无法统计,引发下标越界错误。
2.2 创建计数数组(count)
- 内存开辟:使用
calloc(range, sizeof(int)),即calloc(6, sizeof(int))。- 选择
calloc而非malloc的原因:calloc会自动将所有元素初始化为 0,无需手动赋值,确保频次统计从 0 开始。
- 选择
- 初始计数数组:
int* count = [0, 0, 0, 0, 0, 0](下标 0~5,共 6 个元素)。 - 容错处理:若
count == NULL(内存开辟失败),执行perror("calloc count false:")打印错误信息,并exit(1)终止程序,避免后续空指针操作崩溃。
四、步骤 3:统计待排序数组元素的出现频次
3.1 统计逻辑
- 遍历待排序数组(
i从0到4),对每个元素arr[i],计算其在计数数组中的 映射下标 =arr[i] - min,再执行count[映射下标]++(频次加 1)。 - 映射目的:将
[100, 105]的元素压缩到[0, 5]的连续下标,避免内存浪费(无需开辟 0~105 的数组)。
3.2 逐元素统计过程
| 遍历次数 | i 值 | 当前元素 arr[i] | 映射下标(arr[i]-min) | 计数数组 count 变化(count[映射下标]++) | 变化后 count 数组 |
|---|---|---|---|---|---|
| 1 | 0 | 105 | 105 - 100 = 5 | count[5] 从 0→1 | [0,0,0,0,0,1] |
| 2 | 1 | 102 | 102 - 100 = 2 | count[2] 从 0→1 | [0,0,1,0,0,1] |
| 3 | 2 | 100 | 100 - 100 = 0 | count[0] 从 0→1 | [1,0,1,0,0,1] |
| 4 | 3 | 101 | 101 - 100 = 1 | count[1] 从 0→1 | [1,1,1,0,0,1] |
| 5 | 4 | 102 | 102 - 100 = 2 | count[2] 从 1→2 | [1,1,2,0,0,1] |
3.3 步骤 3 结果
- 计数数组
count = [1, 1, 2, 0, 0, 1],每个下标含义:- 下标 0 → 对应元素
100(0+100),出现 1 次; - 下标 1 → 对应元素
101(1+100),出现 1 次; - 下标 2 → 对应元素
102(2+100),出现 2 次; - 下标 5 → 对应元素
105(5+100),出现 1 次; - 下标 3、4 → 对应元素
103、104,出现 0 次。
- 下标 0 → 对应元素
五、步骤 4:基于计数数组重构原数组(实现排序)
4.1 关键变量定义
- 定义
int sign = 0:标记原数组arr中当前待填充的位置,确保元素连续存储(初始为 0,填充后逐步递增)。
4.2 重构逻辑
- 按计数数组下标升序遍历(
i从0到5),对每个i,若count[i] > 0(该元素有出现),则:- 将
i + min(还原为原元素值)填入arr[sign]; sign++(移动到下一个待填充位置);count[i]--(该元素剩余频次减 1);- 重复上述操作,直到
count[i] = 0,再处理下一个下标。
- 将
- 升序保证:按下标 0→5 遍历,对应原元素 100→105,自然形成升序。
4.3 逐下标重构过程
(1)处理 i=0(count[0] = 1,对应元素 100)
| 操作步骤 | count[0] 值 | 原数组 arr 填充 | sign 值变化 | 操作后 count[0] | 操作后 arr 数组 |
|---|---|---|---|---|---|
| 填充 1 次 | 1 | arr[0] = 0+100=100 | 0→1 | 0 | [100, ?, ?, ?, ?] |
(2)处理 i=1(count[1] = 1,对应元素 101)
| 操作步骤 | count[1] 值 | 原数组 arr 填充 | sign 值变化 | 操作后 count[1] | 操作后 arr 数组 |
|---|---|---|---|---|---|
| 填充 1 次 | 1 | arr[1] = 1+100=101 | 1→2 | 0 | [100, 101, ?, ?, ?] |
(3)处理 i=2(count[2] = 2,对应元素 102)
| 操作步骤 | count[2] 值 | 原数组 arr 填充 | sign 值变化 | 操作后 count[2] | 操作后 arr 数组 |
|---|---|---|---|---|---|
| 填充 1 次 | 2 | arr[2] = 2+100=102 | 2→3 | 1 | [100, 101, 102, ?, ?] |
| 填充 2 次 | 1 | arr[3] = 2+100=102 | 3→4 | 0 | [100, 101, 102, 102, ?] |
(4)处理 i=3(count[3] = 0,对应元素 103)
- 无填充操作,直接跳过。
(5)处理 i=4(count[4] = 0,对应元素 104)
- 无填充操作,直接跳过。
(6)处理 i=5(count[5] = 1,对应元素 105)
| 操作步骤 | count[5] 值 | 原数组 arr 填充 | sign 值变化 | 操作后 count[5] | 操作后 arr 数组 |
|---|---|---|---|---|---|
| 填充 1 次 | 1 | arr[4] = 5+100=105 | 4→5 | 0 | [100, 101, 102, 102, 105] |
4.4 步骤 4 结果
- 原数组重构完成,排序后数组:
arr = [100, 101, 102, 102, 105],完全升序。
六、步骤 5:释放计数数组内存
5.1 内存释放逻辑
- 计数数组
count是通过calloc动态开辟的内存,排序完成后需手动释放,避免内存泄漏(内存被占用但无法回收,多次调用会耗尽系统内存)。 - 执行代码:
free(count),释放count指向的 6 个 int 类型空间。 - 注意:释放后不可再访问
count指针,否则会引发野指针错误。
七、最终总结
通过以上 6 个步骤,计数排序完成了从 “无序数组” 到 “有序数组” 的转换,核心优势在于:
- 线性时间复杂度:遍历数组(
O(n))+ 遍历计数数组(O(range)),总复杂度O(n + range),当range较小时效率极高; - 原地排序优化:直接在原数组重构,无需额外开辟数组空间(仅计数数组需少量空间);
- 映射逻辑严谨:通过
arr[i]-min压缩元素范围,避免内存浪费,同时通过i+min准确还原原元素值。
perfect!!!
结语:在排序的星河中,寻找技术进阶的航标
当我们的指尖划过选择排序的双指针逻辑,看着无序区间在 start 与 end 的收缩中一点点变得规整;当我们跟随计数排序的映射规则,见证整数数组从混乱到有序的线性飞跃 —— 此刻,我们所触摸的已不仅是两段代码的肌理,更是计算机科学大厦中一块坚实的基石。从开篇回望的直接插入排序、希尔排序,到今日深研的选择排序、计数排序,这趟旅程从未止步于 “学会一种排序方法”,而是在算法的更迭中,触摸 “问题解决” 的本质逻辑,在代码的细节里,沉淀 “技术思考” 的底层能力。
很多初学者在接触选择排序时,常会被它 O (n²) 的时间复杂度劝退,甚至因 “效率不高” 而轻视它的价值。可当我们真正拆解它的执行步骤 —— 用 start 标记无序区间的起点,用 end 锁定终点,在每一轮遍历中同步追踪 maxi 与 mini,先将最大值交换至 end 位置,再通过 “mini == end” 的判断修正可能被干扰的最小值下标,最后将最小值交换至 start 位置 —— 才会发现,这看似简单的逻辑里藏着对 “资源最优利用” 的深刻思考。选择排序的每一步操作都带着明确的目标:一次遍历解决两个最值的定位,用最少的交换次数完成元素归位,在有限的内存空间里实现原地排序。这种 “以最小代价构建秩序” 的思路,在实际开发中有着惊人的适配场景:当我们需要在嵌入式设备等内存受限的环境中处理小规模数据时,选择排序的 “零额外空间” 特性远比复杂算法更具优势;当我们需要对实时数据流进行 “增量排序” 时,它 “逐步收缩区间” 的逻辑能帮助我们快速定位新元素的插入位置。更重要的是,选择排序教会我们的 “边界控制” 思维 —— 如何用双指针划定处理范围,如何在操作中避免边界干扰,如何通过条件判断修正异常 —— 这些能力是解决所有算法问题的通用钥匙,远比 “记住排序代码” 更有长远价值。
而计数排序,则像一把钥匙,为我们打开了 “非比较排序” 的新大门。在此之前,我们接触的排序算法都遵循一个潜规则:通过元素间的比较确定位置。直接插入排序比较 “待插入元素与有序区元素”,希尔排序比较 “分组内的元素”,选择排序比较 “当前元素与最值”—— 这些比较类算法的时间复杂度始终绕不开 O (nlogn) 的理论下限。但计数排序跳出了这个框架,它抓住 “待排序元素为整数” 的特性,用 “哈希直接定址法” 建立元素与计数数组下标的映射,通过 “统计频次 — 重构数组” 的两步操作,实现了 O (n+range) 的线性复杂度。这种 “跳出比较,另辟蹊径” 的创新思维,在计算机科学史上留下了浓墨重彩的一笔:在早期的数据库排序中,计数排序因对整数型字段的高效处理被广泛应用;在今日的大数据处理中,它的 “频次统计” 思想衍生出了分布式计数排序,成为海量整数数据排序的核心方案。但计数排序也有着清晰的局限:只能处理整数、依赖元素范围的集中度、需要额外的计数数组空间。这些局限恰恰是算法设计的 “平衡艺术”—— 没有任何一种算法能在所有场景下最优,选择算法的本质是在 “效率、空间、通用性” 之间找到平衡点。就像处理学生成绩排序时,分数范围固定在 0-100,计数排序的优势无可替代;但处理浮点数的温度数据时,我们又不得不回到比较类算法的阵营。这种 “根据场景动态权衡” 的思维,是技术决策的核心素养。
回顾整个学习过程,我们会发现排序算法的演进史,就是一部 “问题驱动创新” 的历史。直接插入排序应对 “近乎有序数据” 的场景,希尔排序优化 “大规模无序数据” 的效率,选择排序适配 “内存受限环境”,计数排序突破 “整数数据排序” 的瓶颈 —— 每一种算法的诞生,都源于对特定问题的深度思考。在这个过程中,我们学到的不仅是算法本身,更是一套完整的 “问题解决方法论”:面对排序需求,首先要分析数据特性 —— 是整数还是浮点数?数据规模有多大?是否近乎有序?元素范围是否集中?然后根据分析结果匹配算法特性 —— 是否需要稳定排序?内存空间是否受限?对时间效率的要求有多高?最后在 “特性匹配” 中找到最优解。这种方法论不仅适用于排序问题,更适用于所有技术决策:开发一个缓存系统时,要权衡 “缓存命中率” 与 “内存占用”;设计一个数据库索引时,要平衡 “查询效率” 与 “插入开销”;选择一个分布式架构时,要协调 “一致性” 与 “可用性”。技术成长的本质,就是从具体算法中提炼出这种 “分析 — 匹配 — 权衡” 的思维模式,并将其应用到更广阔的问题领域。
或许有同学会说,如今编程语言的内置排序函数早已足够强大,无需我们手动实现这些基础算法。但正是这些被封装的基础,构成了我们理解高级技术的前提。就像计数排序的 “映射与统计” 思想,是理解哈希表、布隆过滤器等数据结构的基础;选择排序的 “双指针筛选” 逻辑,为滑动窗口、二分查找等算法提供了核心思路。如果跳过对基础算法的深入理解,我们面对的技术世界将是碎片化的:看到哈希表时,只会用它存储键值对,却不理解 “哈希函数如何减少冲突”;使用滑动窗口时,只会套用模板,却不清楚 “窗口收缩的边界条件”。只有深入理解基础算法的设计逻辑,我们才能看透复杂技术的本质,在面对未知问题时,有能力构建自己的解决方案。
学习算法的过程,注定伴随着困惑与调试的阵痛。或许你曾为选择排序中 “先交换最大值还是最小值” 的顺序纠结许久,或许你曾因计数排序中 “range 少加 1” 导致下标越界而百思不解,或许你曾在跟踪数组变化时因一个细节疏漏而前功尽弃 —— 但这些经历恰恰是成长的印记。每一次调试都是对逻辑的复盘,每一次困惑都是对理解的深化,每一次成功都是对思维的强化。这种在 “试错 — 修正 — 领悟” 中螺旋上升的过程,正是技术能力形成的必经之路。就像选择排序需要 “逐步收缩区间” 才能完成排序,我们的技术成长也需要在一次次 “拆解问题 — 解决问题” 中逐步精进,没有捷径,却有坦途。
排序算法的世界远未到尽头。桶排序如何通过 “分桶 + 桶内排序” 解决计数排序的空间局限?基数排序如何利用 “多关键字排序” 实现对字符串等非整数类型的高效排序?快速排序如何用 “分治 + pivot” 策略实现平均 O (nlogn) 的效率?归并排序如何在保证稳定性的同时实现高效排序?这些问题等待着我们去探索,每一种算法都是一扇窗,窗外是不同的设计哲学与解题思路。我们今天走过的选择排序与计数排序,只是这扇窗的一角,前方还有更广阔的风景。
最后,我想对每一位在算法学习路上跋涉的同学说:不要因 “基础” 而轻视,不要因 “复杂” 而退缩。选择排序的朴素告诉我们,“简单逻辑做到极致就是力量”;计数排序的创新启示我们,“跳出框架才能看见新可能”。技术的进阶从来不是一蹴而就的,而是在对每一个基础算法的深入理解中,在对每一个逻辑细节的反复打磨中,逐渐积累出的穿透力。这种穿透力,能让你在面对复杂问题时一眼看到本质,在设计方案时精准权衡利弊,在技术迭代中始终把握核心。
排序的星河浩瀚无垠,技术的航船永不停歇。愿你在未来的探索中,既能像选择排序一样,在细节处保持严谨;能像计数排序一样,在创新中突破局限;更能在算法的更迭中,找到属于自己的技术成长之路。下一次,当我们再谈起排序算法时,或许不只是代码与逻辑,更是那段在拆解与领悟中,逐渐清晰的技术初心。
最后,诸君,共勉!!!
浙公网安备 33010602011771号