Files
hsa/Html/apps/static/68bacdfadf5aeae0912f7f18-第三章:排序-非比较型排序.html
2025-11-17 14:04:52 +08:00

219 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>非比较型排序</h1>
<h2>非比较型排序</h2>
<h3>基数排序原理</h3>
<h4>排序算法复杂度背景</h4>
<p>基于比较的排序算法,理论上无法突破(Ω(n \log n))时间复杂度下界
特殊场景下,不依赖元素间比较、利用元素结构特征,可实现线性时间排序</p>
<h4>基数排序核心思想</h4>
<p>将元素视为由多个 “位”digit组成
按位进行多轮排序,每轮针对一个关键位
依赖稳定的子排序算法,保证先前排好的顺序不被打乱
关键保障:<strong>排序的稳定性(相同元素相对顺序不变)</strong></p>
<h4>经典示例(十进制整数的基数排序):</h4>
<p>按最低有效位LSB→ 最高有效位MSB 逐轮排序
第一轮按个位数排序0-9 分组)
后续轮次:依次按十位数、百位数等排序</p>
<h4>实操流程:</h4>
<p>初始序列:[329, 457, 657, 839, 436]
第一轮(按个位排序):
按个位 0-9 分组 → [436(6), 457(7), 657(7), 329(9), 839(9)]
457 与 657 个位相同,保持原顺序)
第二轮(按十位排序):
按十位 0-9 分组 → [329(2), 436(3), 839(3), 457(5), 657(5)]
436 与 839 十位相同,保持第一轮后的顺序)
第三轮(按百位排序):
按百位 0-9 分组 → [329(3), 436(4), 457(4), 657(6), 839(8)]
436 与 457 百位相同,保持第二轮后的顺序)</p>
<p>每轮排序通过稳定性保留前序结果,最终实现 “高位决定整体顺序”,完成正确排序。</p>
<h4>时间复杂度:</h4>
<p>每轮扫一遍序列O(n)总共轮数就是基数位数设为k。
时间复杂度为O(kn)其中k一般不太大。</p>
<h3>基数排序的实现</h3>
<p>为了加深对基数排序的理解我们通过一个编程任务来实践其实现。我们将以整数排序为例并采用从最低有效位开始的方法对数字进行排序LSD 基数排序)。</p>
<p>在实现过程中,我们需要编写一个按某个位数进行计数排序的辅助函数,然后在主函数中对每一位循环调用该辅助函数。</p>
<h4>用计数排序稳定地对基数排序</h4>
<p>每选定一个基数后需要用O(n)的时间复杂度将当前序列按照这个基数进行排序;或说<strong>找到按照当前基数下,当前序列的每一个数要前往的位置</strong>
这里用“计数排序”这个算法。
具体做法如下:</p>
<ol>
<li>建一个新数组count记录当前基数的每个不同的数有多少个计数</li>
<li>从前往后遍历序列,取出序列每一个元素在当前基数位置上的值,对应计数值+1count[number]+=1</li>
<li>将count转变为前缀和这样就得到了每一个基数对应的原始数的最后一个在新序列中的位置</li>
<li>从后往前遍历原序列将每一个数基数上的数为number放到对应新序列count[number]-1的位置并让count[number]-=1。</li>
</ol>
<h5>练习:按位计数排序</h5>
<p>作为练习,[170, 45, 75, 90, 802, 24, 2, 66]按个位排序exp=1进行计数排序请写下count数组的计数结果2.步后和前缀和结果。并表述在第4步的利用方式从而得到序列结果。</p>
<h4>任务:实现 LSD 基数排序</h4>
<p>请实现 radix_sort(arr) 函数,对传入的整数列表进行从小到大的排序。你可以假设所有整数为非负且位数相对固定。为了简化问题,我们以十进制为基数(基数=10实现并假定输入整数的最大位数为<eq>d</eq>(例如<eq>10^d</eq>数量级)。算法思路如下:</p>
<p>从最低位个位exp=1开始对数组进行计数排序Counting Sort按该位的数值对元素排序。</p>
<p>然后依次向更高一位exp=10exp=100...)进行排序,直到处理完最高位。</p>
<h4>代码框架</h4>
<p>请在下方代码编辑区完成 counting_sort_by_digit 和 radix_sort 函数的实现。</p>
<pre><code class="language-python">import random
import time
def counting_sort_by_digit(arr, exp):
&quot;&quot;&quot;
对数组按某个数位进行计数排序(稳定的)。
参数:
arr: 要排序的数组
exp: 位标志,表示当前要按照 (exp位) 上的数值进行排序,
例如 exp=1 表示按个位排序exp=10 表示按十位排序。
返回:
按指定位排序后的数组副本
&quot;&quot;&quot;
n = len(arr)
output = [0] * n # 存放排序结果
count = [0] * 10 # 计数数组十进制位范围0-9
# TODO: 计算每个数组元素在该位上的值,并计数
def radix_sort(arr):
&quot;&quot;&quot;
基数排序(按从低位到高位逐位排序)
参数:
arr: 待排序的整数列表
返回:
按非递减顺序排序的列表
&quot;&quot;&quot;
#TODO: 利用 counting_sort_by_digit 按位排序数组
#测试与性能分析
def measure(sort_func, data):
start = time.perf_counter_ns()
sort_func(data)
end = time.perf_counter_ns()
return (end - start) / 10**6 # 转换为毫秒
#生成随机测试数据
sizes = [10000, 50000, 100000]
print(&quot;基数排序性能测试:&quot;)
for n in sizes:
data = [random.randrange(0, 1000000) for _ in range(n)]
t = measure(radix_sort, data)
print(f&quot;数据规模 n={n}: 排序耗时 {t:.2f} ms&quot;)
</code></pre>
<p>性能讨论</p>
<p>在完成代码并运行测试后,可以看到基数排序在不同规模输入下运行时间增长相对温和。理想情况下,对于固定位数<eq>d</eq>的整数,基数排序的时间复杂度为<eq>O(d \times (n + k))</eq>,其中<eq>k</eq>为每位可能的取值数量(对十进制整数而言<eq>k=10</eq>)。
<br>
<eq>d</eq>视作常数时,复杂度近似<eq>O(n)</eq>,因此输入规模扩大倍数时,运行时间应近似线性增长。
<br>
然而需注意基数排序的实际常数因子不小多次稳定排序和额外空间对中等规模数据Python实现未必比C语言内建排序更快。
<br>
更重要的是,基数排序并非万能:如果元素位数<eq>d</eq><eq>n</eq>增长(例如排序<eq>1</eq><eq>N</eq>范围的数,<eq>N</eq>约为<eq>n</eq>量级),则<eq>d = O(\log n)</eq>,此时基数排序复杂度<eq>≈O(n \log n)</eq>,并没有打破比较排序下界。因此,基数排序的线性性能依赖于位数相对较小这一前提。</p>
<h3>桶排序的原理与实现</h3>
<h4>桶排序核心定位</h4>
<p>典型的线性时间排序算法
利用输入数据的<strong>分布特性(假设大致均匀分布在某范围)</strong></p>
<h4>桶排序原理与类比</h4>
<h5>核心逻辑(三步):</h5>
<p>第一步:创建若干 “桶”,将元素按值映射到对应桶中(实现粗分类)
第二步:对每个桶内元素单独排序(可选用任意排序算法)
第三步:按桶的顺序合并所有元素,得到有序序列
<br>
映射:将小数按值投入对应桶,每个桶仅覆盖原始区间 1/10数据量少
桶内排序:因元素少,插入排序等简单算法足够快
合并:按桶序号收集,因 “i&lt;j 时,第 i 号桶元素<第 j 号桶元素”,天然有序
<br></p>
<h5>案例练习</h5>
<p>使用桶排序算法对以下 8 个数字进行排序
[35, 12, 48, 27, 5, 39, 18, 42]
以48为分母将其他所有数划分到0-0.2-0.4-0.6-0.8-1这5个桶中输出每个桶里面的数字是什么。
<br><br></p>
<h4>代码实现</h4>
<p>请实现 bucket_sort(arr) 函数,将传入的[0,1)区间的小数数组排序。思路如下:
<br>
创建若干个空桶列表桶的数量可以根据数据规模n选择这里取<eq>n/10</eq>(为整数)个桶。
<br>
将每个元素按照其值乘以桶数量后的整数部分映射到对应的桶中。例如值为0.23、桶数为10时<eq>0.23 \times 10 = 2.3</eq>放入索引2号桶。
<br>
对每个非空桶内部进行排序。你可以直接使用 Python 内置排序sorted或实现简单排序算法。</p>
<p>按桶的顺序依次合并桶内元素,得到排序后的结果。</p>
<h5>代码框架</h5>
<pre><code class="language-python">import random
import time
def bucket_sort(arr):
&quot;&quot;&quot;
桶排序适用于0到1区间的小数
参数:
arr: 介于[0,1)的浮点数列表
返回:
升序排序后的列表
&quot;&quot;&quot;
# TODO: 按原理实现桶排序
n = len(arr)
if n == 0:
return arr
# 1. 创建桶
# 2. 分配元素到各桶
# 3. 桶内排序
#简单测试与性能评估
sizes = [1000, 5000, 10000]
print(&quot;桶排序性能测试:&quot;)
for n in sizes:
#生成n个[0,1)均匀分布的小数
data = [random.random() for _ in range(n)]
start = time.perf_counter_ns()
sorted_data = bucket_sort(data)
end = time.perf_counter_ns()
elapsed = (end - start) / 10**6
#验证排序正确性
is_correct = &quot;&quot; if sorted_data == sorted(data) else &quot;×&quot;
print(f&quot;数据规模 n={n}: 排序耗时 {elapsed:.2f} ms, 排序正确性 {is_correct}&quot;)
</code></pre>
<h4>桶排序时间复杂度分析</h4>
<h5>平均情况(理想分布):</h5>
<p>前提n 个元素、m 个桶,数据均匀分布 → 每个桶平均含 n/m 个元素
优化:选 m 接近 n 的量级 → 桶内排序成本视为常数
结果整体期望运行时间为O(n)(线性时间)</p>
<h5>最坏情况(分布不均):</h5>
<p>极端场景:所有元素落入同一个桶
复杂度取决于桶内排序算法O (n log n) 或 O (n²)+ 分配桶的 O (n)
结果退化至O (n log n) 甚至更差,失去线性时间优势
结论:理想分布下性能出众,数据分布不均时无优势</p>
<h3>桶排序与哈希</h3>
<h4>核心相通点:映射分散思想</h4>
<p>通过函数映射将元素分散到不同 “容器”(桶排序的 “桶”/ 哈希算法的 “哈希桶”),把原问题拆解为小的子问题
<br>
桶排序:用映射函数(如 index = int(num * bucket_count))划分元素所属桶,依据元素值分配
哈希算法:用哈希函数计算键的哈希桶索引,确定元素存储位置
这种映射也意味着都对<strong>分布均匀性的依赖</strong></p>
<h4>关键区别:映射函数的目标差异</h4>
<p>对比维度
桶排序的映射函数:一般直接做除法:保留元素大小顺序(按值范围划分)
哈希算法的哈希函数:随机映射函数,实现元素均匀分布,不能用于排序(除非特殊设计有序哈希)</p>
<h4>总结</h4>
<p>桶排序可看作哈希思想在排序问题中的应用:均通过 “先分类、后处理” 的范式优化性能,但因目标不同,映射函数设计存在本质差异。</p>
</body>
</html>