在上一篇文章中我们分析了插入排序等排序方式的原理与代码实现,今天我们来继续学习两个稍复杂但非常实用的排序算法:堆排序(Heap Sort) 和 桶排序(Bucket Sort)。它们在处理大数据时尤其高效,了解其底层逻辑对掌握数据结构与算法非常有帮助。
1.堆排序
概念:推排序基于完全二叉树的结构,利用了大顶堆(父节点>左右孩子结点)的性质。
对于堆排序可以分为两大流程:
-
建堆阶段:将无序数组构建为一个大顶堆。
-
排序阶段:将堆顶(最大值)取出放到数组末尾,剩下部分继续维护大顶堆结构。
那接下来,我们来详细的经历一遍创建堆排序的流程!
首先明确各节点之间的关系:(也就是上图各结点下标间的关系)
假设当前节点为 i :
左节点下标 = 父节点下标 X 2 + 1
右节点下标 = 父节点下标 X 2 + 2
第一步:将整个数组进行堆化,也就是从最后一个非叶子节点形成的子堆进行调序并逐步形成一个大顶堆。
所以在这一步我们先构造一个堆化函数(head_ify),它的作用是调用后使数组形成的堆满足完全二叉树的性质。
def head_ify(l_list, n, i): # l_list是数字列表,n是列表元素的个数,i是当前的下标
largest = i # 这里相当于是默认将父节点的下标视为最大,然后赋值给largest
left = (i * 2) + 1
right = (i * 2) + 2 # 左右两个孩子的下标
if left > n and right > n: # 当左右下标大于数组长度 就结束返回
return
# 调整每一个非叶子节点使得其成为大顶堆
if l_list[largest] < l_list[left] and left < n: # 这是当左孩子的值大于父节点所对应的值时,将largest 更新为left的下标
largest = left
if l_list[largest] < l_list[right] and right < n:
largest = right
# 发现最大值的下标不是父节点的下标,所以将最大值和父节点处的值交换,就实现了父节点时整个子堆的最大值
if largest != i:
l_list[i], l_list[largest] = l_list[largest], l_list[i] # 这一步骤虽然使得当前子堆符合大顶堆的性质,有可能破坏了其他子堆。
head_ify(l_list, n, largest)
# 递归调用:函数在自己的定义里面调用自己。
# 避免重复上面用于调整为大顶堆的if语句,“调用自己”使得交换下去的子节点所在的子树也符合大顶堆的性质。
解析:
1.这里的三个 if 语句是并列关系,顺序判断。目的是依次比较当前节点,左子节点、右子节点的大小、找到三者中最大的索引。
- 第一个 if :
如果左子节点存在(l < n
),而且左子节点比当前最大的节点还大,那么就更新最大值的位置为左子节点。
- 第二个 if :
同样判断右子节点,如果右子节点比当前已知的最大值还大,就更新为右子节点。
- 第三个 if :
如果发现最大值的位置(largest)不是当前节点,就把它和父节点的值进行交换,并递归地对被换下去的子树继续堆化。
每次堆化的结果是保证父节点的值是最大的。
2. head_ify( l_list, n, largest )
递归调用:函数在自己的定义里面调用自己。可以有效避免重复语句的撰写,效率更高。
第二步: 在上面构建的堆化函数基础上,我们开始构建堆排序函数,这里我们分两小步走:
首先,将每个非叶子节点的下标通过遍历的方式拿出,再对其子树进行堆化操作。
之后,将堆顶元素放到最后元素位置拿出排序,再重新堆化。经过几轮操作之后,按着从大到小的顺序将所有的元素都拿出排好顺序了。
def heap_sort(l_list):
n = len(l_list)
# 第一步:建立最大堆(从最后一个非叶子节点开始向上调整)
#(任意节点i与其父节点的关系为(i-2)//2)
for i in range(n // 2 - 1, -1, -1): #将每个结点都构造成大顶堆的形式
#第一个-1:根节点是0,所以end=-1
#第二个-1:从后往前遍历
h_if(l_list, n, i)# i 表示建堆时调整部分的根下标,n是当前堆的长度
# 第二步:不断将堆顶元素交换到最后一个叶子节点,然后堆大小减1,重新调整(挨个将列表中最大的数字拿出来)
for i in range(n - 1, 0, -1): # range(start,stop,step),n=len(l_list),是下标为0~n-1的数
l_list[0], l_list[i] = l_list[i], l_list[0] # 最大值l_list[0]与最末元素l_list[i]换位置,最大值就能到达正确位置
h_if(l_list, i, 0)
# i是当前剩下有效数组长度(不包括已经排好序的部分)0代表的是每次都从堆顶开始堆化
return l_list
解析:
两个for 循环是并列关系,
第一个for 循环(用于建堆):把整个数组通过第一步构建的堆化函数head_ify()调整成一个”大顶堆“结构,形成根节点最大>父节点 >子节点的堆结构。
第二个for 循环(用于堆中最大值取出排序):通过将堆顶元素与末尾元素换位取出,实现排序。
- 对于第二个循环里的参数:最后一次迭代是将剩下两个元素(下标为0、1)进行交换,所以遍历停在索引1 。而它从堆的末尾开始进行堆排序,所以每次下标都向前(也就是步长-1)
2.桶排序
概念:将原始数据划分到若干个桶中,每个桶内排序后再合并,最终得到有序结果。
桶排序的三步走:
-
分桶:确定桶宽度,将元素按范围分配到各个桶中。
-
桶内排序:通常使用插入排序或其他高效排序算法对每个桶内部排序。
-
合并输出:将各个桶中的元素按顺序合并,形成有序序列。
需要将桶中的元素进行排序,那么在这里就构造一个插入排序函数,便于之后的调用。(对于插入排序函数在上一篇文章中有详细的解析,需要的话可以在我的主页中进行查看)
桶排序函数实现
def bucket_sort(arr,size=10): #size是桶款
if len(arr) ==0:
return arr #空列表直接返回
#找出最大值最小值,确定桶的个数
# (最大值 - 最小值) // 桶宽度 + 1 = 桶数
min_num,ma_num=min(arr),max(arr)
bucket_count=(ma_num-min_num)//size+1 #桶的数量
buckets=[[] for _ in range(bucket_count)] #创建每个桶
for num in arr:
index=(num-min_num)//size #算出的是存在第几个桶里(从0开始编号)
buckets[index].append(num) #将每个元素添加到相应的桶中
sorted_arr = []
for bucket in buckets:
sorted_arr.extend(insert_sort(bucket)) # 排序每个桶,再合并
return sorted_arr #返回排序好的数组
l=[22,21,35,33,31,44,51,41,65,78,98,97,64]
print(bucket_sort(l))
插入排序(桶内排序算法)
def insert_sort(nums):
li=len(nums)
for i in range(1,li): #遍历除第一个以外的每一个元素
key = nums[i] #当前要插入的元素
j=i-1
while j >=0 and nums[j]>key: #向前遍历已经排好序的部分,找到合适的位置插入key
nums[j+1]=nums[j] #将较大值往后移
j-=1
nums[j+1]=key
return nums
解析:
1.计算桶数:
桶的数量 = 数据跨度 / 桶宽度 + 1(防止整除遗漏最大值)
比如最大81,最小20,size=10,那么桶数量是:(81-20)//10 + 1 = 6 + 1 = 7
2.创建一个 “桶” 的集合:
创建一个“桶的集合”,每个桶就是一个空列表 []
,这些空列表要放在一个总的列表里,比如:
[[],[],[],[],[],[],[]]
你不知道事先每个桶里会放哪些数据,只知道你大概需要 bucket_count
个桶。这种情况最适合使用列表推导式!解决
buckets = [[] for _ in range(bucket_count)]
- range(bucket_count) 这个表示你要按照bucket_count数量生成多少个桶。循环几次就创建几个桶。
- [ ] for _ in ... 是python 列表推导式的写法,指的是:
-
每次循环都生成一个新的
[]
(空列表)作为桶。 -
for i in range(...)
是标准写法。 -
for _ in range(...)
表示:不关心循环变量,只关心循环次数。
-
所以,这一句可以逐词翻译为:
"对
range(bucket_count)
中的每个元素,我都生成一个空列表[]
,然后把这些列表组合成一个大的列表。"
3. 桶内排序:
sorted_arr = []
for bucket in buckets:
sorted_arr.extend(insert_sort(bucket)) # 排序每个桶,再合并
- 这里的insert_sort(bucket)是对这个桶进行插入排序,返回排序好的列表。
- extend() 用法:把“列表中的每一个元素”单独加入到目标列表中,主要目的合并所有桶的列表成一个有序列表。
知识总览:
总结:
本文通过代码演示与逐步解析了两种高效的排序算法 —— 堆排序(Heap Sort) 和 桶排序(Bucket Sort)。堆排序适用于需要原地排序、稳定高效的场景,而桶排序在处理数据分布较均匀的大规模数据时尤其高效。掌握这两种排序方法,不仅能提升算法能力,也为后续更复杂的数据结构与算法学习打下坚实基础。
本文对堆排序和桶排序进行了详细的讲解, 相信你一定收获满满的! 后续我会持续更新关于排序等相关知识的内容,敬请期待!