堆和排序

main
3wish 2023-12-11 18:17:16 +08:00
parent 4c1eb8c883
commit f1355f72af
10 changed files with 491 additions and 0 deletions

View File

@ -0,0 +1,82 @@
"""
hash map 是将数据项映射到列表中的对应位置由于有这样的映射关系
可以使用 hash map 实现哈希查找查找复杂度为 O(1)
计算映射的方式就是 hash 函数一般采用对列表长度取余的方式
hash 表重要指标负载表示已被占用的空间/总空间该比值也被称谓负载因子
当负载因子太大时需要扩容
hash 函数例如使用项值直接对一个数取余会存在几个项取余的结果相同这样就带来了冲突
也就是一个槽对应了多个值
冲突解决
1. 最简单的方法是从原哈希冲突处开始以顺序方式移动槽直到遇到第一个空槽
遇到末尾后可以循环从头开始查找这种冲突解决方法被称为开放寻址法线性查找的缺点是数据项聚集
2. 处理数据项聚集的一种方式是扩展开放寻址技术发生冲突时不是顺序查找下一个开放
而是跳过若干个槽从而更均匀地分散引起冲突的项比如每次隔三个槽来查看
在冲突后寻找另一个槽的过程叫重哈希重散列, rehash
其计算方法如下rehash(pos) = (pos + n)%size
要注意跳过的大小必须使得表中的所有槽最终都能被访问为确保这一点建议表大
小是素数这也为什么示例中要使用 11
3. 解决冲突的另一种方法是拉链法也就是说对每个冲突的位置我们设置一个链表来保
存数据项如图5.5
- 查找时发现冲突后就再到链上顺序查找复杂度为 O(n)当然
冲突链上的数据可以排序然后再借助二分查找这样哈希表复杂度为 O(log2(n))
- 拉链法是许多编程语言内置的哈希表数据结构解决冲突的默认实现
4. 如果采用扩容来解决冲突需要将原来表中的键值对重新计算
"""
class HashMap:
def __init__(self, size: int):
self.size = size
self.slot_used = 0
self.data: list[int | None] = [None for _ in range(self.size)]
self.slot: list[int | None] = [None for _ in range(self.size)]
def hash(self, key: int) -> int:
return key % self.size
def rehash(self, key: int) -> int:
return (key + 3) % self.size
def load(self):
return self.slot_used / self.size
def expand(self):
self.slot += [None for _ in range(self.size)]
self.data += [None for _ in range(self.size)]
self.size *= 2
def insert(self, key: int, value: int):
if self.load() >= 0.75:
self.expand()
pos = self.hash(key)
if not self.slot[pos]:
self.slot[pos] = key
self.data[pos] = value
else:
while self.slot[pos]:
pos = self.rehash(pos)
self.slot[pos] = key
self.data[pos] = value
self.slot_used += 1
def remove(self, key: int) -> int | None:
if self.slot_used == 0:
return None
pos = self.hash(key)

95
heap/heap.py 100644
View File

@ -0,0 +1,95 @@
"""
是一种满足特定条件的完全二叉树主要分为
- [大顶堆]任意节点的值 >= 其子节点
- [小顶堆]任意节点的值 <= 其子节点
堆作为完全二叉树的一个特例具有以下特性
- 最底层节点靠左填充其他层的节点都被填满
- 我们将二叉树的根节点称为堆顶将底层最靠右的节点称为堆底
- 对于大顶堆小顶堆堆顶元素根节点的值分别是最大最小
实际上堆通常用于实现优先队列大顶堆相当于元素按从大到小的顺序出队的优先队列
所以堆可以使用数组来保存
由于是完全二叉树除堆底外每个节点都有两个子节点所以在数组中很容易确定子节点和父节点的位置
假设父节点的下标 p, 则其左右子节点的下标为 2p 2p+1
注意下标0不能存数据因为因为 2p == 0
"""
# 小顶堆,小顶堆添加数据时,小数据要向上冒到正确的位置
class Heap:
def __init__(self):
self.size = 0
self.data = [0]
# 获取父节点下标
def parent(self, c: int):
return c >> 1
def left_child(self, c: int):
return c << 2
def right_child(self, c: int):
return c << 2 + 1
def push(self, value):
self.data.append(value)
self.size += 1
# 添加到堆底,需要向上冒泡
self.move_up(self.size)
# 小数据冒泡
def move_up(self, c: int):
while True:
p = self.parent(c)
if p <= 0:
break
if self.data[c] < self.data[p]:
self.data[c], self.data[p] = self.data[p], self.data[c]
c = p
# 删除堆底的最后一个元素
def pop(self):
return self.data.pop()
# 删除小顶堆中的最小值,也就是根节点
def pop_min(self):
if 0 == self.size:
return None
if 1 == self.size:
# 只有一个元素,直接弹出
self.size -= 1
return self.data.pop()
self.data[1], self.data[self.size] = self.data[self.size], self.data[1]
val = self.pop()
self.move_down(1)
return val
# 大数据下沉
def move_down(self, c: int):
while True:
lc = self.left_child(c)
if lc > self.size:
# 没有左子节点,故而当然没有右子节点
break
mc = self.min_child(c)
if self.data[c] > self.data[mc]:
self.data[c], self.data[mc] = self.data[mc], self.data[c]
c = mc
# 获取两个子节点中的较小节点的下标
def min_child(self, c):
lc, rc = self.left_child(c), self.right_child(c)
if rc > self.size:
return lc
if self.data[lc] > self.data[rc]:
return rc
else:
return lc

View File

@ -0,0 +1,34 @@
"""
桶排序通过设置一些具有大小顺序的桶每个桶对应一个数据范围将数据平均分配到各个桶中然后在每个桶内部分别执行排序
最终按照桶的顺序将所有数据合并
前述几种排序算法都属于基于比较的排序算法它们通过比较元素间的大小来实现排序
此类排序算法的时间复杂度无法超越 O(nlog2n)
考虑一个长度为 n 的数组其元素是范围 [0, 1) 内的浮点数:
1. 初始化 k个桶 n 个元素分配到 k 个桶中
2. 对每个桶分别执行排序这里采用编程语言的内置排序函数
3. 按照桶从小到大的顺序合并结果
"""
def bucket_sort(nums: list[int]):
# 初始化 k = n / 2 个桶预期向每个桶分配2个元素
k = len(nums) // 2
buckets = [[] for _ in range(k)]
# 1. 将数组元素分配到对应的桶中
for num in nums:
# 输入数据范围为 [0, 1),所以使用 num*k 可将 num*k 映射到范围[0, k-1]
i = int(num * k)
buckets[i].append(num)
for bucket in buckets:
# 对各个桶内的数排序
bucket.sort()
i = 0
for bucket in buckets:
for num in bucket:
nums[i] = num
i += 1

View File

@ -0,0 +1,28 @@
"""
计数排序
1. 遍历数组找出其中的最大数字记为 m然后创建一个长度为 m+1 的辅助数组 counter
2. 借助 counter 统计 nums 中各数字的出现次数其中 counter[num] 对应数字 num 的出现次数
3. 统计方法很简单只需遍历 nums设当前数字为 num每轮将 counter[num] 增加 1 即可
4. 由于 counter 的各个索引天然有序因此相当于所有数字已经排序好了接下来我们遍历 counter 根据各数字出现次数从小到大的顺序填入 nums 即可
"""
def counting_sort(nums: list[int]):
m = max(nums)
counter = [0] * (m + 1)
for num in nums:
counter[num] += 1
i = 0
for index, value in enumerate(counter):
if value > 0:
for j in range(value):
nums[i] = index
i += 1
nums = [54, 32, 99, 18, 75, 31, ]
counting_sort(nums)
print(nums)

46
sort/heap_sort.py 100644
View File

@ -0,0 +1,46 @@
"""
堆排序将待排序的序列构建成一个小顶堆此时整个序列的最小值就是
堆顶根节点将其与末尾元素进行交换此时末尾就为最小值这个最小值不再计算到堆内
那么再将剩余的 n - 1 个元素重新构造成一个堆这样会得到一个新的最小值此时将该最
小值再次交换到新堆的末尾这样就有了两个排序的值重复这个过程直到得到一个有序
序列当然小顶堆得到的是降序排序大顶堆得到的才是升序排序
"""
def sift_down(nums: list[int], n: int, i: int):
"""堆的长度为 n ,从节点 i 开始,从顶至底堆化"""
while True:
# 判断节点 i, l, r 中值最大的节点,记为 ma
l = 2 * i + 1 # i 的左子节点
r = 2 * i + 2 # i 的右子节点
ma = i
# 选择两个子节点中更大的那个
if l < n and nums[l] > nums[ma]:
ma = l
if r < n and nums[r] > nums[ma]:
ma = r
# 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
if ma == i:
break
# 交换两节点,将大节点向上移
nums[i], nums[ma] = nums[ma], nums[i]
# 循环向下堆化
i = ma
def heap_sort(nums: list[int]):
"""堆排序"""
# 建堆操作:堆化除叶节点以外的其他所有节点
for i in range(len(nums) // 2 - 1, -1, -1):
sift_down(nums, len(nums), i)
# 从堆中提取最大元素,循环 n-1 轮
for i in range(len(nums) - 1, 0, -1):
# 交换根节点与最右叶节点(交换首元素与尾元素)
nums[0], nums[i] = nums[i], nums[0]
# 以根节点为起点,从顶至底进行堆化
sift_down(nums, i, 0)
nums = [54, 32, 99, 18, 75, 31, ]
heap_sort(nums)
print(nums)

View File

@ -0,0 +1,54 @@
"""
插入排序插入数据项来实现排序始终在数据集的较低位置处维护一个有序的子序列然后
将新项插入子序列使得子序列扩大最终实现集合排序
1. 假设开始的子序列只有一项位置为 0
2. 对于项 1 n-1从后往前遍历前面的所有项
3. 比较过程中每个大于当前项的项将其值赋值到后一项相当于往后挪一位
直到找到小于等于当前项的位置并这个位置的值改为当前项
"""
def insert_sort(nums: list[int]):
for i in range(1, len(nums)):
pos = i
cur = nums[i]
while pos > 0 and cur < nums[pos - 1]:
# 将比当前值大的往后移一位
nums[pos] = nums[pos - 1]
pos -= 1
nums[pos] = cur
"""
插入排序优化由于子序列是已经排序好的序列所以在插入时可以使用二分查找
快速地在子序列中找到插入的位置
"""
def bin_insert_sort(nums: list[int]):
for i in range(1, len(nums)):
pos = i
cur = nums[i]
low = 0
high = pos - 1
while low < high:
mid = (low + high) >> 1
if cur > nums[mid]:
low = mid + 1
else:
high = mid - 1
for j in range(i, low, -1):
nums[j] = nums[j - 1]
if nums[low] > cur:
nums[low] = cur
else:
nums[low + 1] = cur
nums = [47, 29, 71, 99, 78, 19, 24, 47]
bin_insert_sort(nums)
print(nums)

51
sort/merge_sort.py 100644
View File

@ -0,0 +1,51 @@
"""
归并排序类似快排通过不断将列表折半来进行排序
如果集合为空或只有一个项则按基本情况排序
如果有多项则分割集合并递归调用两个区间的归并排序
一旦对这两个区间排序完成就执行合并操作
"""
def merge_sort(nums: list[int], left: int, right: int, ):
if left >= right:
return
mid = (left + right) >> 1
merge_sort(nums, left, mid)
merge_sort(nums, mid + 1, right)
merge(nums, left, mid, right)
def merge(nums: list[int], left: int, mid: int, right: int):
# 临时数组存放合并后的结果
temp = [0] * (right - left + 1)
# 初始化左、右字序列的起始索引左从left开始右从mid开始
i, j, k = left, mid + 1, 0
# 左边第一个肯定是左边最小的,右边第一个肯定是右边最小的
while i <= mid and j <= right:
if nums[i] <= nums[j]:
temp[k] = nums[i]
i += 1
else:
temp[k] = nums[j]
j += 1
k += 1
# 比较终结时,可能由于对折不均,仍有一边右数没有添加到临时列表中
while i <= mid:
temp[k] = nums[k]
i += 1
k += 1
while j <= right:
temp[k] = nums[j]
j += 1
k += 1
# 将临时数组中的元素复制回原数组对应的位置
for k in range(len(temp)):
nums[left + k] = temp[k]

46
sort/quick_sort.py 100644
View File

@ -0,0 +1,46 @@
"""
核心将小于基准的数移到左边将大于基准的数移到右边再对左右分区继续执行快速排序
1. 选择列表中的一个数作为基准一般是第一个元素
2. 设置左右两个指针分别指向第一个元素和最后一个元素
3. 向左移动右指针若发现比基准值小的值则将其与基准值交换
4. 接着向右移动左指针若发现比基准值大的值则将其与基准值交换
5. 重复 3, 4直到 左右指针相遇
6. 最后基准值所在的位置左边都比其小右边都比其大
7. 再对左右区域指向快排
复杂度为 O(n^2)
"""
def quick_sort(nums: list[int], low: int, high: int):
i = low
j = high
# 一开始的基准值为 nums[i]
if low >= high:
return
while i < j:
while i < j and nums[j] >= nums[i]:
j -= 1
if i < j:
# 交换后基准值移到右边
nums[i], nums[j] = nums[j], nums[i]
i += 1
while i < j and nums[i] < nums[j]:
i += 1
if i < j:
# 交换后基准值移到左边
nums[j], nums[i] = nums[i], nums[j]
j -= 1
# 基准值位置是i所以 low 到 i-1 是小区基准值的区域
quick_sort(nums, low, i - 1)
#
quick_sort(nums, j + 1, high)
nums = [47, 29, 71, 99, 78, 19, 24, 47]
quick_sort(nums, 0, len(nums)-1)
print(nums)

33
sort/shell_sort.py 100644
View File

@ -0,0 +1,33 @@
"""
希尔排序也称递减递增排序它将原始集合分为多个较小的子集合然后对每个集合
运用插入排序
1. 当增量为3时表示每相隔两个的元素索引差3为一个子序列将原序列分为了三组子序列
2. 对每个子序列使用插入排序此时每个子序列就是有序的
3. 逐渐减小增量重复第二2步
4. 当增量为1时再执行一次插入排序此时完整序列就是有序的
希尔排序的复杂度分析稍微复杂一些但其大致分布在 O(n) O(n^2) 之间
"""
def shell_sort(nums: list[int]):
step = len(nums) >> 1
while step >= 1:
# 分成了 step 个子序列,循环 step 次,对每个子序列使用插排
for i in range(step):
for j in range(i + step, len(nums), step):
pos = j
cur = nums[j]
# 无法对子序列使用二分查找,查找插入位置,因为中间位置无法确定
while pos >= step and cur < nums[pos - step]:
# 将比当前值大的往后移 step 位
nums[pos] = nums[pos - step]
pos -= step
nums[pos] = cur
step >>= 1
nums = [47, 29, 71, 99, 78, 19, 24, 47]
shell_sort(nums)
print(nums)

22
test/test_sort.py 100644
View File

@ -0,0 +1,22 @@
from sort.quick_sort import *
from sort.insert_sort import *
from sort.merge_sort import *
def test_quick_sort():
nums = [47, 29, 71, 99, 78, 19, 24, 47]
assert [19, 24, 29, 47, 47, 71, 78, 99] == quick_sort(nums, 0, len(nums) - 1)
def test_insert_sort1():
nums = [47, 29, 71, 99, 78, 19, 24, 47]
assert [19, 24, 29, 47, 47, 71, 78, 99] == insert_sort(nums)
def test_insert_sort2():
nums = [47, 29, 71, 99, 78, 19, 24, 47]
assert [19, 24, 29, 47, 47, 71, 78, 99] == bin_insert_sort(nums)
def test_merge_sort():
nums = [47, 29, 71, 99, 78, 19, 24, 47]
assert [19, 24, 29, 47, 47, 71, 78, 99] == merge_sort(nums)