堆和排序
parent
4c1eb8c883
commit
f1355f72af
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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]
|
||||
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
Loading…
Reference in New Issue