b站左老师课程对应链接:3.详解桶排序以及排序内容大总结_哔哩哔哩_bilibili
堆
在完全二叉树中,i位置的左子位置为2i+1,右子位置为2i+2,父位置为(i-1)/2。
heapinsert
用户依次输入值可以保证组成的数组为大根堆。也可以认为是给较大值找位置。
过程描述:初始化heapsize=0,用户依次输入一个值,heapsize+1,输入后与自己的父位置值比较,如果大于父位置值则交换位置。
def heapinsert(arr, i):
while arr[i] > arr[int((i-1)/2)]:
arr[i], arr[int((i-1)/2)] = arr[int((i-1)/2)], arr[i]
i = int((i-1)/2)
heapify
用于当把最大值从数组中抽离后,将数组重新排列成大根堆。也可以认为是给较小值找位置。
过程描述:最大值从数组中删除后,将数组的最后一个值复制到最大值位置(0位),heapsize-1。i初始化为0位,从0位(i)开始与子数中的最大值比较,当子数中的最大值大于父值,则交换位置,并使i=子数最大值本来的位置。继续重复上步,直到当子数中的最大值不大于父值或i下方没有子数。
def heapify(arr, i, heapsize):
left = int(i * 2 + 1) #左子数位置
while left < heapsize: #用于判断下方是否有子数,当左子数位置>=heapsize时,越界,即没有左子数,也就没有右子数。
#筛选子数最大值位置,只有当右子数位置在界内,即有右子数,且值大于左子数,子数最大值位置才为右子数
largest = left + 1 if (left + 1 < heapsize and arr[left+1] > arr[left]) else left
#比较父与最大子数的大小,把大的下标给largest
largest = largest if arr[largest] > arr[i] else i
#当i为最大值时,就不用继续往下比较了,已经满足大根堆要求
if largest == i:
break
#其余情况需要互换位置,并将i = largest
arr[largest], arr[i] = arr[i], arr[largest]
i = largest
#新左子数,即原来i位置的子数的左子数
left = int(i * 2 + 1)
变换任意位置的值,将这个值与原值对比,小于原值使用heapify,大于原值使用heapinsert。
时间复杂度:O(logN):更换的数只在二叉树层之间变换,因此最坏的结果是logN
使用堆来排序数组
过程描述:从数组中逐一加载数,每次加一个数都使用heapinsert处理,将所有数按大根堆排序后,再将最大值与后一个值位置互换,使用heapify操作处理。如此循环直到heapsize=0。
class heapsort:
def __init__(self, arr):
self.arr = arr
def compare_num(self):
for i in range(len(self.arr)):
self.heapinsert(i)
heapsize = len(self.arr)
while heapsize > 0:
self.arr[0], self.arr[heapsize - 1] = self.arr[heapsize - 1], self.arr[0]
heapsize -= 1
self.heapify(0, heapsize)
return self.arr
def heapinsert(self, i):
while self.arr[i] > self.arr[int((i-1)/2)]:
self.arr[i], self.arr[int((i-1)/2)] = self.arr[int((i-1)/2)], self.arr[i]
i = int((i-1)/2)
def heapify(self, i, heapsize):
left = int(i * 2 + 1) # 左子数位置
while left < heapsize: # 用于判断下方是否有子数,当左子数位置>=heapsize时,越界,即没有左子数,也就没有右子数。
# 筛选子数最大值位置,只有当右子数位置在界内,即有右子数,且值大于左子数,子数最大值位置才为右子数
largest = left + 1 if (left + 1 < heapsize and self.arr[left + 1] > self.arr[left]) else left
# 比较父与最大子数的大小,把大的下标给largest
largest = largest if self.arr[largest] > self.arr[i] else i
# 当i为最大值时,就不用继续往下比较了,已经满足大根堆要求
if largest == i:
break
# 其余情况需要互换位置,并将i = largest
self.arr[largest], self.arr[i] = self.arr[i], self.arr[largest]
i = largest
# 新左子数,即原来i位置的子数的左子数
left = int(i * 2 + 1)
if __name__ == '__main__':
arr = [1,3,9,7,2,5,8,0]
print(heapsort(arr).compare_num())
时间复杂度:O(N*LogN)
空间复杂度:O(1)
将循环加载数步骤简化,直接导入整个数组进行操作。
过程描述:从数组最后一个数反向遍历数组,每个数heapify后,向上一个数继续迭代,直到i=0
class heapsort:
def __init__(self, arr):
self.arr = arr
def compare_num(self):
for i in range(len(self.arr)-1, -1, -1):
self.heapify(i, len(self.arr))
heapsize = len(self.arr)
while heapsize > 0:
self.arr[0], self.arr[heapsize - 1] = self.arr[heapsize - 1], self.arr[0]
heapsize -= 1
self.heapify(0, heapsize)
return self.arr
def heapinsert(self, i):
while self.arr[i] > self.arr[int((i-1)/2)]:
self.arr[i], self.arr[int((i-1)/2)] = self.arr[int((i-1)/2)], self.arr[i]
i = int((i-1)/2)
def heapify(self, i, heapsize):
left = int(i * 2 + 1) # 左子数位置
while left < heapsize: # 用于判断下方是否有子数,当左子数位置>=heapsize时,越界,即没有左子数,也就没有右子数。
# 筛选子数最大值位置,只有当右子数位置在界内,即有右子数,且值大于左子数,子数最大值位置才为右子数
largest = left + 1 if (left + 1 < heapsize and self.arr[left + 1] > self.arr[left]) else left
# 比较父与最大子数的大小,把大的下标给largest
largest = largest if self.arr[largest] > self.arr[i] else i
# 当i为最大值时,就不用继续往下比较了,已经满足大根堆要求
if largest == i:
break
# 其余情况需要互换位置,并将i = largest
self.arr[largest], self.arr[i] = self.arr[i], self.arr[largest]
i = largest
# 新左子数,即原来i位置的子数的左子数
left = int(i * 2 + 1)
if __name__ == '__main__':
arr = [1,3,9,7,2,5,8,0]
print(heapsort(arr).compare_num())
数组变为大根堆步骤的时间复杂度:O(N):二叉树最底层子节点有N/2个,这些节点没有子节点,不用heapify操作,时间复杂度为N/2*1,倒数第二层子节点需要遍历和heapify操作,时间复杂度为N/4*2,以此类推。加和后为O(N)。
整体时间复杂度仍为O(N*LogN)
一个几乎有序数组的排序:
几乎有序数组:把数组中每个数排好序,每个元素移动距离不超过k,k相对于整个数组长度较小。
过程描述:可以使用小根堆处理,遍历数组前k+1个值,将这k+1个数转化为小根堆,之后再向后一位,也就是1~k+2范围的值再转换为小根堆,以此类推,到最后7个值只要逐步减小变为小根堆的范围,就可以获得有序数组。
时间复杂度:O(N*Logk)
补充材料:python中可以直接调用的堆算法
heapq.heappush(heap, item)
将 item 的值加入 heap 中,保持heap为小根堆。
import heapq
heap = []
arr = [1, 3, 6, 2, 5, 8, 3, 0, 4, 7]
for i in arr:
heapq.heappush(heap, i)
print(heap)
#结果:[0, 1, 3, 2, 5, 8, 6, 3, 4, 7]
heapq.heappop(heap)
弹出并返回 heap 的最小的元素,保持heap为小根堆。如果堆为空,抛出IndexError 。使用heap[0] ,可以只访问最小的元素而不弹出它。
import heapq
heap = []
h = []
arr = [1, 3, 6, 2, 5, 8, 3, 0, 4, 7]
for i in arr:
heapq.heappush(heap, i)
for _ in range(len(heap)):
h.append(heapq.heappop(heap))
print(h)
#结果:[0, 1, 2, 3, 3, 4, 5, 6, 7, 8]
heapq.heappushpop(heap, item)
将 item 放入堆中,然后弹出并返回 heap 的最小元素。该组合操作比先调用heappush()再调用 heappop()运行起来更有效率。如果堆为空,行为类似于调用heappush(),并且返回None。
import heapq
heap = []
arr = [1, 3, 6, 2, 5, 8, 3, 5, 4, 7]
for i in arr:
heapq.heappush(heap, i)
print(heapq.heappushpop(heap, 0))
#结果:0
heapq.heapify(x)
将list x 转换成小根堆,就地操作,也就是直接在list内操作,线性时间内。
import heapq
arr = [1, 3, 6, 2, 5, 8, 3, 5, 4, 7]
heapq.heapify(arr)
print(arr)
#结果:[1, 2, 3, 3, 5, 8, 6, 5, 4, 7]
heapq.heapreplace(heap, item)
弹出并返回 heap 中最小的一项,同时推入新的 item。 堆的大小不变。 如果堆为空则引发IndexError。
利用可调用函数直接运行堆的方法称为黑盒方法。黑盒方法就是用户输入一个值,黑盒返回一个值。但如果要从堆内部改变一个值就不推荐这种方法了,相对于我们手写的方法,黑盒方法处理这种问题,效率较差。
利用黑盒方法实现完成几乎有序数组的排序:
import heapq
def sort_almost_sorted_array(arr, k):
# 创建一个最小堆
min_heap = []
result = []
# 将前 k+1 个元素加入堆中
for i in range(min(k + 1, len(arr))):
heapq.heappush(min_heap, arr[i])
# 从 k+1 到数组结束
for i in range(k + 1, len(arr)):
# 弹出堆顶元素(最小元素)并加入结果
result.append(heapq.heappop(min_heap))
# 将当前元素加入堆
heapq.heappush(min_heap, arr[i])
# 将堆中剩余的元素全部弹出并加入结果
while min_heap:
result.append(heapq.heappop(min_heap))
return result
# 示例数组
arr = [3, 2, 1, 6, 5, 4, 9, 8, 7]
k = 3
sorted_arr = sort_almost_sorted_array(arr, k)
print(sorted_arr)
?按课中方法做?
比较器
用于单个参数有多个变量的比较场景,对其中一个变量进行排序。
用于学生信息排序
sorted()实现
class Student:
def __init__(self, name, id, age):
self.name = name
self.id = id
self.age = age
if __name__ == '__main__':
# 创建学生对象
student1 = Student('A', 1, 25)
student2 = Student('B', 3, 30)
student3 = Student('C', 2, 18)
# 将学生对象放入列表
students = [student1, student2, student3]
# 使用 sorted() 函数按学生ID排序
sorted_students = sorted(students, key=lambda student: student.id)
print('ID are sorted in ascending order.')
# 输出排序结果
for student in sorted_students:
print(f"Name: {student.name}, ID: {student.id}, Age: {student.age}")
#################################################################################
sorted_students = sorted(students, key=lambda student: student.age)
print('Ages are sorted in ascending order.')
for student in sorted_students:
print(f"Name: {student.name}, ID: {student.id}, Age: {student.age}")
###################################################################################
sorted_students = sorted(students, key=lambda student: student.age, reverse=True)
print('Ages are sorted in descending order.')
for student in sorted_students:
print(f"Name: {student.name}, ID: {student.id}, Age: {student.age}")
比较器方法 functools.cmp_to_key()实现
from functools import cmp_to_key
class Student:
def __init__(self, name, student_id, age):
self.name = name
self.id = student_id
self.age = age
class IdAscendingSorter:
@staticmethod
def compare(a, b):
return a.id - b.id
class IdDescendingSorter:
@staticmethod
def compare(a, b):
return b.id - a.id
class AgeAscendingSorter:
@staticmethod
def compare(a, b):
return a.age - b.age
class AgeDescendingSorter:
@staticmethod
def compare(a, b):
return b.age - a.age
if __name__ == '__main__':
# 创建学生对象
student1 = Student('A', 1, 25)
student2 = Student('B', 3, 30)
student3 = Student('C', 2, 18)
# 将学生对象放入列表
students = [student1, student2, student3]
# 使用 sorted() 函数按学生ID排序(升序)
sorted_students = sorted(students, key=cmp_to_key(IdAscendingSorter.compare))
print('ID are sorted in ascending order:')
for student in sorted_students:
print(f"Name: {student.name}, ID: {student.id}, Age: {student.age}")
# 使用 sorted() 函数按学生ID排序(降序)
sorted_students_desc = sorted(students, key=cmp_to_key(IdDescendingSorter.compare))
print('\nID are sorted in descending order:')
for student in sorted_students_desc:
print(f"Name: {student.name}, ID: {student.id}, Age: {student.age}")
# 使用 sorted() 函数按学生年龄排序(升序)
sorted_students_name_asc = sorted(students, key=cmp_to_key(AgeAscendingSorter.compare))
print('\nNames are sorted in ascending order:')
for student in sorted_students_name_asc:
print(f"Name: {student.name}, ID: {student.id}, Age: {student.age}")
# 使用 sorted() 函数按学生年龄排序(降序)
sorted_students_name_desc = sorted(students, key=cmp_to_key(AgeDescendingSorter.compare))
print('\nNames are sorted in descending order:')
for student in sorted_students_name_desc:
print(f"Name: {student.name}, ID: {student.id}, Age: {student.age}")
桶排序
与之前的方法有区别,之前的方法都是基于比较的排序,该方法不基于比较。
计数排序
假设目标数组有N种值,生成一个N个值的额外数组,key代表目标数组中的值,value代表目标数组中该值出现的次数。遍历目标数组后,统计并体现再额外数组中。
如一组年龄数组年龄分布从0~150。0岁有5人,就在辅助数组0位置输入值5,1岁有8人,就在辅助数组1位置输入值8,依次类推,再按照0000011111111的对应人名进行排序。
该方法时间复杂度为O(N)
但当数组中值范围分布极大时,额外数字内存占用也会极大。所以需要分析数据状况来使用。
基数排序
设置0~9的容器,将目标数组按十进制最高位数补零,按顺序遍历数组,将个位数对应的值将目标数组内值放入容器,遍历完并放入容器后,再从0容器开始按顺序取出到目标数组中,之后按十位百位千位...一直到最高位重复步骤。
如有数组[12,22,3,74,100]
目标数组按十进制最高位数补零[012,026,003,074,100]
0容器:100
1容器:无
2容器:012,022
3容器:003
4容器:074
回传目标数组:[100,012,022,003,074]
......
[003,012,022,074,100]
实质上是从最高位开始到最低位优先级。
这种方法只能用于有限进制的数字,对没有进制或无限进制的对象无法排序。
这里可以推导出一个规律:按照基数排序方法,每位排序时,先进容器的会先出容器。
因为python中没有容器(桶)这一概念,因此,我们这里使用另一种方式来还原桶进出的过程。
过程描述:设置十进制位中每位(个十百千...)中0~9每个数在该位上出现的频率表count,设置一个辅助数组help用于存放十进制中每位的排序情况,这个辅助数组可以视作容器。从左往右依次遍历数组,先将个位出现的数用count统计频率,遍历统计完后将从左往右累加,这时count中显示的数c就是个位为x(0~9)的数最后应该处于第几个位置,换句话说,就是个位x的数最后出容器的位置为c-1位,所以这里可以倒序遍历数组来更方便的完成出桶步骤。具体流程是,从右往左遍历数组,看个位数字,将count中对应位的数-1,得到的数就是该数在个位出容器步骤的位置。后面的十位百位...也按这个步骤来。
举例
[013,021,011,052,062]
常规个位进出容器
0容器:无
1容器:021,011
2容器:052,062
3容器:013
出容器数组[021,011,052,062,013]
按过程描述方法
count[0,2,2,1,0...]
累加count[0,2,4,5,5,5...]代表个位每个数字最后出容器是第几个位置.。
从左往右遍历数组013个位是3,3在count对应值是5,也就在第5个位置,将count-1对应数组第4位,放到辅助数组变为[0位,1位,2位,3位,013]。
继续遍历062个位是2,2在count对应值是4,也就在第4个位置,将count-1对应数组第3位,放到辅助数组变为[0位,1位,2位,062,013]。
继续遍历052个位是2,2在count对应值是3,也就在第3个位置,将count-1对应数组第2位,放到辅助数组变为[0位,1位,052,062,013]。
....
可以看出最后结果与常规思路一致,只是换了流程顺序。
class RadixSort:
def __init__(self, arr, L, R, digit):
self.arr = arr # 输入的数组
self.L = L # 数组的起始索引
self.R = R # 数组的结束索引
self.digit = digit # 总的位数
def get_digit(self, number, d):
"""获取数字 number 在第 d 位上的值"""
return (number // 10 ** (d - 1)) % 10 # 以0作为最低位
def counting_sort(self, d):
"""基于第 d 位的计数排序"""
# 计数器
count = [0] * 10
output = [0] * (self.R - self.L + 1)
# 统计当前位数的出现频率
for i in range(self.L, self.R + 1):
digit_value = self.get_digit(self.arr[i], d)
count[digit_value] += 1
# 改变 count 用于位置计算
for i in range(1, 10):
count[i] += count[i - 1]
# 构建输出数组
for i in range(self.R, self.L - 1, -1): # 倒序遍历以保持稳定性
digit_value = self.get_digit(self.arr[i], d)
output[count[digit_value] - 1] = self.arr[i]
count[digit_value] -= 1
# 将输出数组复制回原数组中
for i in range(self.L, self.R + 1):
self.arr[i] = output[i - self.L]
def sort(self):
# 对每一位进行排序
for d in range(1, self.digit + 1):
self.counting_sort(d)
if __name__ == '__main__':
# 测试基数排序
arr = [170, 45, 75, 90, 802, 24, 2, 66]
L = 0
R = len(arr) - 1
max_digit = 3 # 以 802 为例,最多 3 位
sorter = RadixSort(arr, L, R, max_digit)
sorter.sort()
print("Sorted array:", sorter.arr)