From f1355f72afb25dab57e49c8fe02ff1de3a5d24c1 Mon Sep 17 00:00:00 2001 From: 3wish Date: Mon, 11 Dec 2023 18:17:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A0=86=E5=92=8C=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hash_map/hash_map.py | 82 +++++++++++++++++++++++++++++++++++++ heap/heap.py | 95 +++++++++++++++++++++++++++++++++++++++++++ sort/bucket_sort.py | 34 ++++++++++++++++ sort/counting_sort.py | 28 +++++++++++++ sort/heap_sort.py | 46 +++++++++++++++++++++ sort/insert_sort.py | 54 ++++++++++++++++++++++++ sort/merge_sort.py | 51 +++++++++++++++++++++++ sort/quick_sort.py | 46 +++++++++++++++++++++ sort/shell_sort.py | 33 +++++++++++++++ test/test_sort.py | 22 ++++++++++ 10 files changed, 491 insertions(+) create mode 100644 hash_map/hash_map.py create mode 100644 heap/heap.py create mode 100644 sort/bucket_sort.py create mode 100644 sort/counting_sort.py create mode 100644 sort/heap_sort.py create mode 100644 sort/insert_sort.py create mode 100644 sort/merge_sort.py create mode 100644 sort/quick_sort.py create mode 100644 sort/shell_sort.py create mode 100644 test/test_sort.py diff --git a/hash_map/hash_map.py b/hash_map/hash_map.py new file mode 100644 index 0000000..df8fdff --- /dev/null +++ b/hash_map/hash_map.py @@ -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) diff --git a/heap/heap.py b/heap/heap.py new file mode 100644 index 0000000..f71e587 --- /dev/null +++ b/heap/heap.py @@ -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 diff --git a/sort/bucket_sort.py b/sort/bucket_sort.py new file mode 100644 index 0000000..fa9a7a6 --- /dev/null +++ b/sort/bucket_sort.py @@ -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 diff --git a/sort/counting_sort.py b/sort/counting_sort.py new file mode 100644 index 0000000..f9d7d36 --- /dev/null +++ b/sort/counting_sort.py @@ -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) \ No newline at end of file diff --git a/sort/heap_sort.py b/sort/heap_sort.py new file mode 100644 index 0000000..e127f91 --- /dev/null +++ b/sort/heap_sort.py @@ -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) diff --git a/sort/insert_sort.py b/sort/insert_sort.py new file mode 100644 index 0000000..cf30ae0 --- /dev/null +++ b/sort/insert_sort.py @@ -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) \ No newline at end of file diff --git a/sort/merge_sort.py b/sort/merge_sort.py new file mode 100644 index 0000000..28a7278 --- /dev/null +++ b/sort/merge_sort.py @@ -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] + diff --git a/sort/quick_sort.py b/sort/quick_sort.py new file mode 100644 index 0000000..4ded36a --- /dev/null +++ b/sort/quick_sort.py @@ -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) \ No newline at end of file diff --git a/sort/shell_sort.py b/sort/shell_sort.py new file mode 100644 index 0000000..ff849f6 --- /dev/null +++ b/sort/shell_sort.py @@ -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) \ No newline at end of file diff --git a/test/test_sort.py b/test/test_sort.py new file mode 100644 index 0000000..a56ff84 --- /dev/null +++ b/test/test_sort.py @@ -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) \ No newline at end of file