本文参考《算法4》
分治法思想
分–将问题分解为规模更小的子问题;
治–将这些规模更小的子问题逐个击破;
合–将已解决的子问题合并,最终得出“母”问题的解;
归并排序
如有一个数组A[0…n]
先将数组分解,分到只剩两个元素为止
合并小数组的同时进行排序(称为归并)
一路合并直到原数组。
关键在于两点:
- 如何分解
- 如何归并
分解:
方法1:构建一棵二叉树。构建之后,按照后序遍历(即左子树-右子树-根)
。用递归实现。
方法2:完全不用构建树,也不用递归,直接分解到只剩两个元素。
我们采用方法1
归并:
采用类似打擂台的方式。我们将待合并的两个小数组称为左数组和右数组。
左边和右边一一对比,谁小,就把谁排上去,一个一个排。
归并排序实践
例题:
A = [3, 7, 6, 7, 8, 6, 2, 9]
用归并排序A
步骤1:分解
如图,不断二分,直到只剩两个元素。
假如我们使用第一种方式来分解。那么上图就对应一棵树。
利用后序遍历构建一棵树:构建树的时候,假如没到底,就不断向左分叉。当左分叉到底的时候,右分叉。当左右分叉都到底的时候,返回到根。
后序遍历的流程如下:
- 左分叉都到底了吗?(即当前节点还可以继续向左分叉吗?),如果没有,继续左分叉
- 右分叉都到底了吗?(即当前节点还可以继续向右分叉吗?),如果没有,继续右分叉
- 如果都没有,那么返回到根(即回到上一层)
我们用递归的方式构建树。
写递归有两个要义:
- 终止条件是什么?(什么时候退出?)
- 递归条件是什么? (什么时候分叉?)
递归函数总是可以归结为一个模板,形如以下伪代码:
def 递归函数(a,b,c):
if(满足了终止条件)
return (xxx)
递归函数(a1,b1,c1)
递归函数(a1,b1,c1)
自底向上的重复操作
其中分几个叉,递归函数就写几个,这里构建二叉树所以写两个。
其中return xxx也可以单纯return空
其中“if(满足了终止条件)return xxx",也可以写成
def 递归函数(a,b,c):
if(不满足终止条件)
递归函数(a1,b1,c1)
递归函数(a1,b1,c1)
自底向上的重复操作
那么,我们套用模板,得到用递归写的分解的伪代码
def sort(A, lo, hi):
if(lo>=hi):#终止条件
return
mid = int((lo+hi)/2)
sort(A, lo, mid) #左分叉
sort(A, mid+1, hi) #右分叉
merge(A, lo, hi) #归并操作,下节讲解
其中,终止条件是lo>=hi,表示的是只剩下一个元素的时候返回,但是这时候什么都不做,只是返回了。所以实际上的最底层的操作,是在返回后的倒数第二层完成的。为了防止混淆,我们就认为返回后的倒数第二层是最底层。这时候,最底层只有两个元素。
其中lo 和 hi代表了未分解的组的第一个位置和最后一个位置,如图,
在中间把它分成两半,得到的组为A[lo…mid]和A[mid+1…hi]
至此,分解已经完成,我们最后得到的最底层数组为:
这四个长度为2的小数组。
步骤2:归并
我们要像打擂台一样进行归并,谁小谁被选入排好的数组。
在真正进行排序之前,我们必须先定义一个临时的数组。
定义临时数组
因此,我们先定义一个临时的数组,用来放排好的数组。
比如定义
ordered = []
用来存放排好的数组
进行某些操作,然后把某个元素a,被放入ordered
ordered.append(a)
最终排完之后,返回ordered数组就行了。
但是这样有个问题,就是函数要不断传递ordered数组。另一种更好的做法是,只传递数组A,把排好的元素放到A里。但是这样原本的A就会被覆盖。所以为了不丢失原本的A,申请一个临时的数组tmp用来存放原本的A。tmp不需要被传递,因为当排序完成之后,我们只需要排好的数组A就行了,tmp可以被舍弃掉。
tmp:长度为n的数组,用来存放原本的A
A:排好序的数组,过程中不断被更新。
在merge函数的一开始,先把A拷贝到tmp
for i in range(n):
tmp[i] = A[i]
打擂法排序
然后就可以开始排序了。
现在开始打擂台!
首先介绍规则:
- 从头开始,两边各出一个元素,进行1v1 PK
- PK方式为:谁小谁就被划掉,并被选入数组A;谁大谁就留下继续PK
- 假如一方队伍为空了,那么就直接把另一方元素选入数组A
所以胜利的标志是:谁小谁赢。被选入A是一项殊荣。
为了标记谁被划掉了,谁还剩下,我们定义两个下标:i和j
- i表示左边队伍(蓝方)当前在擂台上的元素是谁(其下标),i之前的,就是被划掉并被选入A了的,i之后的,就是还没上过擂台的。
- 同理,j表示右边队伍当前在擂台上的元素是谁。
其次介绍两个队伍:
左方(蓝方):[3, 6, 7, 7]
右方(绿方):[2, 6, 8, 9]
开赛前是这样的阵容:
万事俱备,
开始比赛!
首先是3和2比。2小,被选入A。
我们挪动下标j
3仍然需要留在擂台上比赛。
开始下一轮:
此时蓝方派出了3,扳回一局,被选入A!
挪动下标i
开始第三轮:
蓝方是6,绿方也是6,平局!这时候可以随便选一个,我们就选蓝方吧。
挪动下标i
开始第四轮:
蓝方是7,绿方是6. 绿方被选入A
开始第五轮:
绿方派出了8,不如7小
开始第六轮:
蓝方又派出了一个7,再次胜利
这时候,蓝队所有的元素都已经被选入A了!
比赛已经不用比了。其余的蓝队的两个选手,直接依次放入A就好了。
最后的结果是:
如何保证子组内部的有序?
我们看到,在打擂台的同时,两个数组就被合成到了一个数组A,数组A是有序的。
但是有一点我故意没有提到:两个子组内部本身是有序的!
这是打擂台的前提条件!
这一点如何保证呢?
只要从下往上走,保证每次向上走之前都是有序的,自然两个子组就是有序的!
从最底层的时候,仍然可以用打擂台的方式比较,只不过这时候是只有两个元素1v1了。打完擂台,自然就合并成立一个有序的子组,再往上走是2v2。然后这四个打完擂台也合并成了一个有序的数组。再往上走是4v4。如此往复,直到合并到原数组。
所以答案是,只要自底向上地归并,那么子组内部一定是有序的。也就是说,我们什么都不用做,它自然就是有序的。
归并的代码实现
伪代码如下
def merge(A, lo, hi):
mid = int((lo + hi)/2) #左边子组的最后一个位置
for k in range(lo, hi): #拷贝到tmp
tmp[k] = A[k]
for k in range(lo, hi):
if(i>mid): A[k] = tmp[j++] #假如左边空了,选右边的
elif(j>hi): A[k] = tmp[i++] #假如右边空了,选左边的
elif(A[i]<=A[j]): A[k] = tmp[i++] #假如左边的小,选左边的
else: A[k] = tmp[j++] #假如右边的小,选右边的
代码参考自《算法4》
拓展:只加两行求逆序数
cnt = 0
def merge(A, lo, hi):
mid = int((lo + hi)/2) #左边子组的最后一个位置
for k in range(lo, hi): #拷贝到tmp
tmp[k] = A[k]
for k in range(lo, hi):
if(i>mid): A[k] = tmp[j++] #假如左边空了,选右边的
elif(j>hi): A[k] = tmp[i++] #假如右边空了,选左边的
elif(A[i]<=A[j]): A[k] = tmp[i++] #假如左边的小,选左边的
else:
A[k] = tmp[j++] #假如右边的小,选右边的
cnt += mid - i +1 #cnt是逆序数,mid-i+1是i与mid之间的距离
最终代码:归并排序C++
#include <vector>
#include <iostream>
using std::vector;
/**
* @brief
* 对A[lo..mid]和A[mid+1..hi](左右均闭区间)两个数组进行归并
* 归并后的新数组应该有序(从小到大)
*
* @param A 被排序数组
* @param lo 左子组的起点下标
* @param mid 两个数组的分割点
* @param hi 右子组的终点下标
*/
static void merge(vector<int> &A, int lo, int mid, int hi, vector<int>& tmp)
{
int i = lo, j = mid + 1;
//拷贝A[lo..hi]到临时数组
for (int k = 0; k < tmp.size(); k++)
tmp[k] = -1;
for (int k = lo; k <= hi; k++)
tmp[k] = A[k];
for (int k = lo; k <= hi; k++)
{
if (i > mid)
A[k] = tmp[j++]; //如果左子组空了,就直接取右边的
else if (j > hi)
A[k] = tmp[i++]; //如果右子组空了,就直接取左边的
else if (tmp[j] < tmp[i])
A[k] = tmp[j++]; //右边的小,取右边的(从小到大排序)
else
A[k] = tmp[i++]; //左边的小,取左边的
}
vector<int> sorted(A.size());
for (int k = 0; k <A.size(); k++)
sorted[k] = -2;
for (int k = lo; k <= hi; k++)
sorted[k] = A[k];
std::cout<<"\nsorted:\t";
for(auto &&x:sorted)
std::cout<<x<<' ';
std::cout<<"end sort";
}
/**
* @brief 将子组A[lo..hi]进行归并排序(从小到大)
* 自顶向下地递归
*
* @param A 被排序的数组
* @param lo 当前子组的起点下标
* @param hi 当前子组的终点下标
*/
static void sort(vector<int> &A, int lo, int hi, vector<int>& tmp)
{
if (hi <= lo)
return; //递归终止条件
int mid = lo + (hi - lo) / 2;
sort(A, lo, mid, tmp); //左分叉
sort(A, mid + 1, hi, tmp); //右分叉
merge(A, lo, mid, hi, tmp); //自底向上地归并两个子组
}
/**
* @brief 归并排序
*
* @param A 被排序数组
*/
void mergeSort(vector<int> &A)
{
vector<int> tmp;
tmp.resize(A.size());
sort(A, 0, A.size()-1, tmp);
}
int main()
{
vector<int> A{3,7,6,7,8,6,2,9};
for(auto &&x:A)
std::cout<<x<<' ';
std::cout << A.size() - 1 << std::endl;
mergeSort(A);
std::cout<<"\n\nsorted A: ";
for(auto &&x:A)
std::cout<<x<<' ';
}
输出
3 7 6 7 8 6 2 9 7
sorted: 3 7 -2 -2 -2 -2 -2 -2 end sort
sorted: -2 -2 6 7 -2 -2 -2 -2 end sort
sorted: 3 6 7 7 -2 -2 -2 -2 end sort
sorted: -2 -2 -2 -2 6 8 -2 -2 end sort
sorted: -2 -2 -2 -2 -2 -2 2 9 end sort
sorted: -2 -2 -2 -2 2 6 8 9 end sort
sorted: 2 3 6 6 7 7 8 9 end sort
sorted A: 2 3 6 6 7 7 8 9
C++ 归并排序(类版本)
#include <vector>
#include <iostream>
using std::vector;
class myMergeSort
{
public:
vector<int> A;
vector<int> tmp;
myMergeSort(vector<int> &Arr);
myMergeSort() = default;
private:
void merge(int lo, int mid, int hi);
void sort(int lo, int hi);
};
/**
* @brief
* 对A[lo..mid]和A[mid+1..hi](左右均闭区间)两个数组进行归并
* 归并后的新数组应该有序(从小到大)
*
* @param A 被排序数组
* @param lo 左子组的起点下标
* @param mid 两个数组的分割点
* @param hi 右子组的终点下标
*/
void myMergeSort::merge(int lo, int mid, int hi)
{
int i = lo, j = mid + 1;
//拷贝A[lo..hi]到临时数组
for (int k = lo; k <= hi; k++)
tmp[k] = A[k];
for (int k = lo; k <= hi; k++)
{
if (i > mid)
A[k] = tmp[j++]; //如果左子组空了,就直接取右边的
else if (j > hi)
A[k] = tmp[i++]; //如果右子组空了,就直接取左边的
else if (tmp[j] < tmp[i])
A[k] = tmp[j++]; //右边的小,取右边的(从小到大排序)
else
A[k] = tmp[i++]; //左边的小,取左边的
}
}
/**
* @brief 将子组A[lo..hi]进行归并排序(从小到大)
* 自顶向下地递归
*
* @param A 被排序的数组
* @param lo 当前子组的起点下标
* @param hi 当前子组的终点下标
*/
void myMergeSort::sort(int lo, int hi)
{
if (hi <= lo)
return; //递归终止条件
int mid = lo + (hi - lo) / 2;
sort(lo, mid); //左分叉
sort(mid + 1, hi); //右分叉
merge(lo, mid, hi); //自底向上地归并两个子组
}
/**
* @brief 归并排序
*
* @param A 被排序数组
*/
myMergeSort::myMergeSort(vector<int> &arr) : A(arr)
{
tmp.resize(A.size());
sort(0, A.size() - 1);
arr = A;
}
// example
int main()
{
vector<int> Arr{3, 7, 6, 7, 8, 6, 2, 9};
for (auto &&x : Arr)
std::cout << x << ' ';
myMergeSort sort(Arr);
std::cout << "\n\nsorted Arr: ";
for (auto &&x : Arr)
std::cout << x << ' ';
}