目录
排序算法介绍
《Hello算法》是GitHub上一个开源书籍,对新手友好,有大量的动态图,很适合算法初学者自主学习入门。而我则是正式学习算法,以这本书为参考,写写笔记,有错误的地方还请指正,下面我会用python和C++实现其中的实例
排序介绍:排序简介 - Hello 算法 (hello-algo.com)
这里有更详细的介绍。
归并算法
Merge Sort是算法中“分治思想”的典型体现,共有“划分”、“合并”两个阶段:
- 划分阶段,通过递归不断地将数组从中间划分开,将长数组地排序问题转化为短数组地排序问题;
- 合并阶段,当划分地子数组长度为1时,开始向上合并,不断地将左右两个短排序数组合并为一个长排序数组,直至合并至原数组时完成排序;
算法流程
划分是从上到下将数组从中点切为两个子数组,直至长度为1;
- 计算数组中点mid,划分左子数组([left,mid])以及右子数组([mid+1,right]);
- 递归执行1.步骤,直至子数组区间长度为1时,终止递归划分;
合并从底至顶地将左子数组和右子数组合并为一个有序数组;
需要注意的是,由于从长度为 1 的子数组开始合并,所以 每个子数组都是有序的 。因此,合并任务本质是要 将两个有序子数组合并为一个有序数组 。
观察发现,归并排序的递归顺序就是二叉树的后序遍历。
- 后序遍历: 先递归左子树、再递归右子树、最后处理根结点。
- 归并排序: 先递归左子树、再递归右子树、最后处理合并
算法实现
下面将以python与C++为例
python
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)
C++
void merge(vector<int>& nums, int left, int mid, int right) {
// 初始化辅助数组
vector<int> tmp(nums.begin() + left, nums.begin() + right + 1);
// 左子数组的起始索引和结束索引
int leftStart = left - left, leftEnd = mid - left;
// 右子数组的起始索引和结束索引
int rightStart = mid + 1 - left, rightEnd = right - left;
// i, j 分别指向左子数组、右子数组的首元素
int i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组
for (int k = left; k <= right; k++) {
// 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd)
nums[k] = tmp[j++];
// 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++
else if (j > rightEnd || tmp[i] <= tmp[j])
nums[k] = tmp[i++];
// 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
else
nums[k] = tmp[j++];
}
}
/* 归并排序 */
void mergeSort(vector<int>& nums, int left, int right) {
// 终止条件
if (left >= right) return; // 当子数组长度为 1 时终止递归
// 划分阶段
int mid = (left + right) / 2; // 计算中点
mergeSort(nums, left, mid); // 递归左子数组
mergeSort(nums, mid + 1, right); // 递归右子数组
// 合并阶段
merge(nums, left, mid, right);
}
重点解释一下合并方法 merge() 的流程:
- 初始化一个辅助数组 tmp 暂存待合并区间 [left,right] 内的元素,后续通过覆盖原数组 nums 的元素来实现合并;
- 初始化指针 i,j,k 分别指向左子数组、右子数组、原数组的首元素;
- 循环判断 tmp[ i ] 和 tmp[ j ] 的大小,将较小的先覆盖至 nums[ k ] ,指针 i,j 根据判断结果交替前进(指针 k 也前进),直至两个子数组都遍历完,即可完成合并。
合并方法 merge() 代码中的主要难点:
- nums 的待合并区间为 [left,right] ,而因为 tmp 只复制了 nums 该区间元素,所以 tmp 对应区间为 [0,right - left] ,需要特别注意代码中各个变量的含义。
- 判断 tmp[ i ] 和 tmp[ j ] 的大小的操作中,还 需考虑当子数组遍历完成后的索引越界问题,即 i >leftEnd 和 j>rightEnd 的情况,索引越界的优先级是最高的,例如如果左子数组已经被合并完了,那么不用继续判断,直接合并右子数组元素即可。