diff --git a/codes/python/chapter_sorting/bubble_sort.py b/codes/python/chapter_sorting/bubble_sort.py index 2027047..54260dd 100644 --- a/codes/python/chapter_sorting/bubble_sort.py +++ b/codes/python/chapter_sorting/bubble_sort.py @@ -1,10 +1,46 @@ ''' File: bubble_sort.py Created Time: 2022-11-25 -Author: Krahets (krahets@163.com) +Author: timi (xisunyy@163.com) ''' import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) from include import * +""" 冒泡排序 """ +def bubble_sort(nums): + n = len(nums) + # 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in range(n - 1, -1, -1): + # 内循环:冒泡操作 + for j in range(i): + if nums[j] > nums[j + 1]: + # 交换 nums[j] 与 nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + +""" 冒泡排序(标志优化) """ +def bubble_sort_with_flag(nums): + n = len(nums) + # 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in range(n - 1, -1, -1): + flag = False # 初始化标志位 + # 内循环:冒泡操作 + for j in range(i): + if nums[j] > nums[j + 1]: + # 交换 nums[j] 与 nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + flag = True # 记录交换元素 + if not flag: + break # 此轮冒泡未交换任何元素,直接跳出 + + +""" Driver Code """ +if __name__ == '__main__': + nums = [4, 1, 3, 1, 5, 2] + bubble_sort(nums) + print("排序后数组 nums =", nums) + + nums1 = [4, 1, 3, 1, 5, 2] + bubble_sort_with_flag(nums1) + print("排序后数组 nums =", nums1) diff --git a/codes/python/chapter_sorting/insertion_sort.py b/codes/python/chapter_sorting/insertion_sort.py index 28e0331..9f32673 100644 --- a/codes/python/chapter_sorting/insertion_sort.py +++ b/codes/python/chapter_sorting/insertion_sort.py @@ -1,10 +1,28 @@ ''' File: insertion_sort.py Created Time: 2022-11-25 -Author: Krahets (krahets@163.com) +Author: timi (xisunyy@163.com) ''' import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) from include import * +""" 插入排序 """ +def insertion_sort(nums): + # 外循环:base = nums[1], nums[2], ..., nums[n-1] + for i in range(1, len(nums)): + base = nums[i] + j = i - 1 + # 内循环:将 base 插入到左边的正确位置 + while j >= 0 and nums[j] > base: + nums[j + 1] = nums[j] # 1. 将 nums[j] 向右移动一位 + j -= 1 + nums[j + 1] = base # 2. 将 base 赋值到正确位置 + + +""" Driver Code """ +if __name__ == '__main__': + nums = [4, 1, 3, 1, 5, 2] + insertion_sort(nums) + print("排序后数组 nums = ", nums) diff --git a/codes/python/chapter_sorting/merge_sort.py b/codes/python/chapter_sorting/merge_sort.py index 6f9b4a6..e4a1b10 100644 --- a/codes/python/chapter_sorting/merge_sort.py +++ b/codes/python/chapter_sorting/merge_sort.py @@ -1,10 +1,57 @@ ''' File: merge_sort.py Created Time: 2022-11-25 -Author: Krahets (krahets@163.com) +Author: timi (xisunyy@163.com) ''' import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) from include import * +""" +合并左子数组和右子数组 +左子数组区间 [left, mid] +右子数组区间 [mid + 1, right] +""" +def merge(nums, left, mid, right): + # 初始化辅助数组 借助 copy模块 + tmp = nums[left:right + 1] + # 左子数组的起始索引和结束索引 + left_start, left_end = left - left, mid - left + # 右子数组的起始索引和结束索引 + right_start, right_end = mid + 1 - left, right - left + # i, j 分别指向左子数组、右子数组的首元素 + i, j = left_start, right_start + # 通过覆盖原数组 nums 来合并左子数组和右子数组 + for k in range(left, right + 1): + # 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if i > left_end: + nums[k] = tmp[j] + j += 1 + # 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + elif j > right_end or tmp[i] <= tmp[j]: + nums[k] = tmp[i] + i += 1 + # 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else: + nums[k] = tmp[j] + j += 1 + +""" 归并排序 """ +def merge_sort(nums, left, right): + # 终止条件 + if left >= right: + return # 当子数组长度为 1 时终止递归 + # 划分阶段 + mid = (left + right) // 2 # 计算中点 + merge_sort(nums, left, mid) # 递归左子数组 + merge_sort(nums, mid + 1, right) # 递归右子数组 + # 合并阶段 + merge(nums, left, mid, right) + + +""" Driver Code """ +if __name__ == '__main__': + nums = [4, 1, 3, 1, 5, 2] + merge_sort(nums, 0, len(nums) - 1) + print("归并排序完成后 nums =", nums) diff --git a/codes/python/chapter_sorting/quick_sort.py b/codes/python/chapter_sorting/quick_sort.py index a668096..5ba826b 100644 --- a/codes/python/chapter_sorting/quick_sort.py +++ b/codes/python/chapter_sorting/quick_sort.py @@ -1,10 +1,127 @@ ''' File: quick_sort.py Created Time: 2022-11-25 -Author: Krahets (krahets@163.com) +Author: timi (xisunyy@163.com) ''' import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) from include import * +""" 快速排序类 """ +class QuickSort: + """ 哨兵划分 """ + def partition(self, nums, left, right): + # 以 nums[left] 作为基准数 + i, j = left, right + while i < j: + while i < j and nums[j] >= nums[left]: + j -= 1 # 从右向左找首个小于基准数的元素 + while i < j and nums[i] <= nums[left]: + i += 1 # 从左向右找首个大于基准数的元素 + # 元素交换 + nums[i], nums[j] = nums[j], nums[i] + # 将基准数交换至两子数组的分界线 + nums[i], nums[left] = nums[left], nums[i] + return i # 返回基准数的索引 + + """ 快速排序 """ + def quick_sort(self, nums, left, right): + # 子数组长度为 1 时终止递归 + if left >= right: + return + # 哨兵划分 + pivot = self.partition(nums, left, right) + # 递归左子数组、右子数组 + self.quick_sort(nums, left, pivot - 1) + self.quick_sort(nums, pivot + 1, right) + +""" 快速排序类(中位基准数优化)""" +class QuickSortMedian: + """ 选取三个元素的中位数 """ + def median_three(self, nums, left, mid, right): + # 使用了异或操作来简化代码 + # 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if (nums[left] > nums[mid]) ^ (nums[left] > nums[right]): + return left + elif (nums[mid] < nums[left]) ^ (nums[mid] > nums[right]): + return mid + return right + + """ 哨兵划分(三数取中值) """ + def partition(self, nums, left, right): + # 以 nums[left] 作为基准数 + med = self.median_three(nums, left, (left + right) // 2, right) + # 将中位数交换至数组最左端 + nums[left], nums[med] = nums[med], nums[left] + # 以 nums[left] 作为基准数 + i, j = left, right + while i < j: + while i < j and nums[j] >= nums[left]: + j -= 1 # 从右向左找首个小于基准数的元素 + while i < j and nums[i] <= nums[left]: + i += 1 # 从左向右找首个大于基准数的元素 + # 元素交换 + nums[i], nums[j] = nums[j], nums[i] + # 将基准数交换至两子数组的分界线 + nums[i], nums[left] = nums[left], nums[i] + return i # 返回基准数的索引 + + """ 快速排序 """ + def quick_sort(self, nums, left, right): + # 子数组长度为 1 时终止递归 + if left >= right: return + # 哨兵划分 + pivot = self.partition(nums, left, right) + # 递归左子数组、右子数组 + self.quick_sort(nums, left, pivot - 1) + self.quick_sort(nums, pivot + 1, right) + +""" 快速排序类(尾递归优化) """ +class QuickSortTailCall: + """ 哨兵划分 """ + def partition(self, nums, left, right): + # 以 nums[left] 作为基准数 + i, j = left, right + while i < j: + while i < j and nums[j] >= nums[left]: + j -= 1 # 从右向左找首个小于基准数的元素 + while i < j and nums[i] <= nums[left]: + i += 1 # 从左向右找首个大于基准数的元素 + # 元素交换 + nums[i], nums[j] = nums[j], nums[i] + # 将基准数交换至两子数组的分界线 + nums[i], nums[left] = nums[left], nums[i] + return i # 返回基准数的索引 + + """ 快速排序(尾递归优化) """ + def quick_sort(self, nums, left, right): + # 子数组长度为 1 时终止 + while left < right: + # 哨兵划分操作 + pivot = self.partition(nums, left, right) + # 对两个子数组中较短的那个执行快排 + if pivot - left < right - pivot: + self.quick_sort(nums, left, pivot - 1) # 递归排序左子数组 + left = pivot + 1 # 剩余待排序区间为 [pivot + 1, right] + else: + self.quick_sort(nums, pivot + 1, right) # 递归排序右子数组 + right = pivot - 1 # 剩余待排序区间为 [left, pivot - 1] + + +""" Driver Code """ +if __name__ == '__main__': + # 快速排序 + nums = [4, 1, 3, 1, 5, 2] + QuickSort().quick_sort(nums, 0, len(nums) - 1) + print("快速排序完成后 nums =", nums) + + # 快速排序(中位基准数优化) + nums1 = [4, 1, 3, 1, 5, 2] + QuickSortMedian().quick_sort(nums1, 0, len(nums1) - 1) + print("快速排序(中位基准数优化)完成后 nums =", nums) + + # 快速排序(尾递归优化) + nums2 = [4, 1, 3, 1, 5, 2] + QuickSortTailCall().quick_sort(nums, 0, len(nums2) - 1) + print("快速排序(尾递归优化)完成后 nums =", nums) diff --git a/docs/chapter_computational_complexity/space_time_tradeoff.md b/docs/chapter_computational_complexity/space_time_tradeoff.md index 07975a5..7bc827c 100644 --- a/docs/chapter_computational_complexity/space_time_tradeoff.md +++ b/docs/chapter_computational_complexity/space_time_tradeoff.md @@ -1,3 +1,7 @@ +--- +comments: true +--- + # 权衡时间与空间 理想情况下,我们希望算法的时间复杂度和空间复杂度都能够达到最优,而实际上,同时优化时间复杂度和空间复杂度是非常困难的。 diff --git a/docs/chapter_sorting/bubble_sort.md b/docs/chapter_sorting/bubble_sort.md index 9e16895..c3ac3ba 100644 --- a/docs/chapter_sorting/bubble_sort.md +++ b/docs/chapter_sorting/bubble_sort.md @@ -47,9 +47,7 @@ comments: true ## 算法流程 1. 设数组长度为 $n$ ,完成第一轮「冒泡」后,数组最大元素已在正确位置,接下来只需排序剩余 $n - 1$ 个元素。 - 2. 同理,对剩余 $n - 1$ 个元素执行「冒泡」,可将第二大元素交换至正确位置,因而待排序元素只剩 $n - 2$ 个。 - 3. 以此类推…… **循环 $n - 1$ 轮「冒泡」,即可完成整个数组的排序**。 ![bubble_sort](bubble_sort.assets/bubble_sort.png) @@ -76,6 +74,21 @@ comments: true } ``` +=== "Python" + + ```python + """ 冒泡排序 """ + def bubble_sort(nums): + n = len(nums) + # 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in range(n - 1, -1, -1): + # 内循环:冒泡操作 + for j in range(i): + if nums[j] > nums[j + 1]: + # 交换 nums[j] 与 nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + ``` + ## 算法特性 **时间复杂度 $O(n^2)$ :** 各轮「冒泡」遍历的数组长度为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,因此使用 $O(n^2)$ 时间。 @@ -116,3 +129,22 @@ comments: true } } ``` + +=== "Python" + + ```python + """ 冒泡排序(标志优化) """ + def bubble_sort_with_flag(nums): + n = len(nums) + # 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in range(n - 1, -1, -1): + flag = False # 初始化标志位 + # 内循环:冒泡操作 + for j in range(i): + if nums[j] > nums[j + 1]: + # 交换 nums[j] 与 nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + flag = True # 记录交换元素 + if not flag: + break # 此轮冒泡未交换任何元素,直接跳出 + ``` diff --git a/docs/chapter_sorting/insertion_sort.md b/docs/chapter_sorting/insertion_sort.md index 9b987c9..06efb08 100644 --- a/docs/chapter_sorting/insertion_sort.md +++ b/docs/chapter_sorting/insertion_sort.md @@ -44,6 +44,22 @@ comments: true } ``` +=== "Python" + + ```python + """ 插入排序 """ + def insertion_sort(nums): + # 外循环:base = nums[1], nums[2], ..., nums[n-1] + for i in range(1, len(nums)): + base = nums[i] + j = i - 1 + # 内循环:将 base 插入到左边的正确位置 + while j >= 0 and nums[j] > base: + nums[j + 1] = nums[j] # 1. 将 nums[j] 向右移动一位 + j -= 1 + nums[j + 1] = base # 2. 将 base 赋值到正确位置 + ``` + ## 算法特性 **时间复杂度 $O(n^2)$ :** 最差情况下,各轮插入操作循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,使用 $O(n^2)$ 时间。 diff --git a/docs/chapter_sorting/merge_sort.md b/docs/chapter_sorting/merge_sort.md index 4c2d966..a8b31c9 100644 --- a/docs/chapter_sorting/merge_sort.md +++ b/docs/chapter_sorting/merge_sort.md @@ -7,7 +7,6 @@ comments: true 「归并排序 Merge Sort」是算法中 “分治思想” 的典型体现,其有「划分」和「合并」两个阶段: 1. **划分阶段:** 通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题; - 2. **合并阶段:** 划分到子数组长度为 1 时,开始向上合并,不断将 **左、右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序; ![merge_sort_preview](merge_sort.assets/merge_sort_preview.png) @@ -104,6 +103,51 @@ comments: true } ``` +=== "Python" + + ```python title="merge_sort.py" + """ + 合并左子数组和右子数组 + 左子数组区间 [left, mid] + 右子数组区间 [mid + 1, right] + """ + def merge(nums, left, mid, right): + # 初始化辅助数组 借助 copy模块 + tmp = nums[left:right + 1] + # 左子数组的起始索引和结束索引 + left_start, left_end = left - left, mid - left + # 右子数组的起始索引和结束索引 + right_start, right_end = mid + 1 - left, right - left + # i, j 分别指向左子数组、右子数组的首元素 + i, j = left_start, right_start + # 通过覆盖原数组 nums 来合并左子数组和右子数组 + for k in range(left, right + 1): + # 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if i > left_end: + nums[k] = tmp[j] + j += 1 + # 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + elif j > right_end or tmp[i] <= tmp[j]: + nums[k] = tmp[i] + i += 1 + # 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else: + nums[k] = tmp[j] + j += 1 + + """ 归并排序 """ + def merge_sort(nums, left, right): + # 终止条件 + if left >= right: + return # 当子数组长度为 1 时终止递归 + # 划分阶段 + mid = (left + right) // 2 # 计算中点 + merge_sort(nums, left, mid) # 递归左子数组 + merge_sort(nums, mid + 1, right) # 递归右子数组 + # 合并阶段 + merge(nums, left, mid, right) + ``` + 下面重点解释一下合并方法 `merge()` 的流程: 1. 初始化一个辅助数组 `tmp` 暂存待合并区间 `[left, right]` 内的元素,后续通过覆盖原数组 `nums` 的元素来实现合并; diff --git a/docs/chapter_sorting/quick_sort.md b/docs/chapter_sorting/quick_sort.md index 8918971..d29d80d 100644 --- a/docs/chapter_sorting/quick_sort.md +++ b/docs/chapter_sorting/quick_sort.md @@ -61,6 +61,25 @@ comments: true } ``` +=== "Python" + + ```python title="quick_sort.py" + """ 哨兵划分 """ + def partition(self, nums, left, right): + # 以 nums[left] 作为基准数 + i, j = left, right + while i < j: + while i < j and nums[j] >= nums[left]: + j -= 1 # 从右向左找首个小于基准数的元素 + while i < j and nums[i] <= nums[left]: + i += 1 # 从左向右找首个大于基准数的元素 + # 元素交换 + nums[i], nums[j] = nums[j], nums[i] + # 将基准数交换至两子数组的分界线 + nums[i], nums[left] = nums[left], nums[i] + return i # 返回基准数的索引 + ``` + !!! note "快速排序的分治思想" 哨兵划分的实质是将 **一个长数组的排序问题** 简化为 **两个短数组的排序问题**。 @@ -93,6 +112,21 @@ comments: true } ``` +=== "Python" + + ```python title="quick_sort.py" + """ 快速排序 """ + def quick_sort(self, nums, left, right): + # 子数组长度为 1 时终止递归 + if left >= right: + return + # 哨兵划分 + pivot = self.partition(nums, left, right) + # 递归左子数组、右子数组 + self.quick_sort(nums, left, pivot - 1) + self.quick_sort(nums, pivot + 1, right) + ``` + ## 算法特性 **平均时间复杂度 $O(n \log n)$ :** 平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。 @@ -149,6 +183,29 @@ comments: true } ``` +=== "Python" + + ```python title="quick_sort.py" + """ 选取三个元素的中位数 """ + def median_three(self, nums, left, mid, right): + # 使用了异或操作来简化代码 + # 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if (nums[left] > nums[mid]) ^ (nums[left] > nums[right]): + return left + elif (nums[mid] < nums[left]) ^ (nums[mid] > nums[right]): + return mid + return right + + """ 哨兵划分(三数取中值) """ + def partition(self, nums, left, right): + # 以 nums[left] 作为基准数 + med = self.median_three(nums, left, (left + right) // 2, right) + # 将中位数交换至数组最左端 + nums[left], nums[med] = nums[med], nums[left] + # 以 nums[left] 作为基准数 + # 下同省略... + ``` + ## 尾递归优化 **普通快速排序在某些输入下的空间效率变差。** 仍然以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 0 ,那么将形成一个高度为 $n - 1$ 的递归树,此时使用的栈帧空间大小劣化至 $O(n)$ 。 @@ -175,3 +232,21 @@ comments: true } } ``` + +=== "Python" + + ```python title="quick_sort.py" + """ 快速排序(尾递归优化) """ + def quick_sort(self, nums, left, right): + # 子数组长度为 1 时终止 + while left < right: + # 哨兵划分操作 + pivot = self.partition(nums, left, right) + # 对两个子数组中较短的那个执行快排 + if pivot - left < right - pivot: + self.quick_sort(nums, left, pivot - 1) # 递归排序左子数组 + left = pivot + 1 # 剩余待排序区间为 [pivot + 1, right] + else: + self.quick_sort(nums, pivot + 1, right) # 递归排序右子数组 + right = pivot - 1 # 剩余待排序区间为 [left, pivot - 1] + ```