分治法思想-归并排序-求逆序数

本文参考《算法4》

分治法思想

分–将问题分解为规模更小的子问题;
治–将这些规模更小的子问题逐个击破;
合–将已解决的子问题合并,最终得出“母”问题的解;

归并排序

如有一个数组A[0…n]
先将数组分解,分到只剩两个元素为止
合并小数组的同时进行排序(称为归并)
一路合并直到原数组。

关键在于两点:

  1. 如何分解
  2. 如何归并

分解:
方法1:构建一棵二叉树。构建之后,按照后序遍历(即左子树-右子树-根)
。用递归实现。
方法2:完全不用构建树,也不用递归,直接分解到只剩两个元素。

我们采用方法1

归并:
采用类似打擂台的方式。我们将待合并的两个小数组称为左数组和右数组。
左边和右边一一对比,谁小,就把谁排上去,一个一个排。

归并排序实践

例题:
A = [3, 7, 6, 7, 8, 6, 2, 9]
用归并排序A

步骤1:分解

如图,不断二分,直到只剩两个元素。

在这里插入图片描述

假如我们使用第一种方式来分解。那么上图就对应一棵树。
利用后序遍历构建一棵树:构建树的时候,假如没到底,就不断向左分叉。当左分叉到底的时候,右分叉。当左右分叉都到底的时候,返回到根。

后序遍历的流程如下:

  1. 左分叉都到底了吗?(即当前节点还可以继续向左分叉吗?),如果没有,继续左分叉
  2. 右分叉都到底了吗?(即当前节点还可以继续向右分叉吗?),如果没有,继续右分叉
  3. 如果都没有,那么返回到根(即回到上一层)

在这里插入图片描述
我们用递归的方式构建树。

写递归有两个要义:

  1. 终止条件是什么?(什么时候退出?)
  2. 递归条件是什么? (什么时候分叉?)

递归函数总是可以归结为一个模板,形如以下伪代码:

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]
打擂法排序

然后就可以开始排序了。

现在开始打擂台!

首先介绍规则:

  1. 从头开始,两边各出一个元素,进行1v1 PK
  2. PK方式为:谁小谁就被划掉,并被选入数组A;谁大谁就留下继续PK
  3. 假如一方队伍为空了,那么就直接把另一方元素选入数组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 << ' ';
}
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值