「数组」归并排序 / if语句优化|小区间插入优化(C++)

目录

概述

思路

算法过程

merge_sort()

recursion()

merge() 

Code

谈谈奇数

优化方案

1.if语句优化

2.小区间插入优化

Code

复杂度

总结

DLC


概述

在上一篇文章中,我们介绍了快速排序以及随机快速排序:

「数组」快速排序 / 随机值优化|小区间插入优化(C++)

今天,我们来介绍归并排序。

相比于快速排序是冒泡排序融合了分治思想后形成的究极promax进化版,归并排序不来自任何一种使用分治的基础排序:它本人就是纯粹的分治。


思路

冯·诺依曼想过这么一个事:两个有序数组二路合并成一个有序数组的时间复杂度是线性的:

     i  0 1 2 3 4
arr1[i] 1 5 6 8 9
arr2[i] 0 2 3 4 7
 ans[i] 1 2 3 4 5 6 7 8 9

很明显, 你只需要双指针i和j分别指向两个数组,k指针指向答案数组,

每次都进行ans[k++]=min(arr1[i],arr2[j]),之后i++或j++(这要看min函数取到了谁)

就能在线性时间内得到合并好的数组。

那如果能把一个无序数组在中间一分为二成两个有序的子数组然后合并就好了,但是子数组怎么能有序呢?

那如果能把这两个子数组分成四个有序子数组然后合并就好了

那如果能把这四个子数组分成八个有序子数组然后合并就好了

.....................

那如果能把这一个数分成,诶,等等,一个数本来就有序啊

那从这里再一层一层合并上去不就好了?冯·诺依曼恍然大悟。


算法过程

来把我们的思路实现一遍。

merge_sort()

我们给出函数merge_sort,它接收一个范围,它只干两件事: 

①申请一个辅助数组。

②启动这个层层划分的过程。

让我们谈谈辅助数组:

在二路归并时,我们需要一个地方承载我们的结果。那不如就让原数组承载我们的结果,辅助数组[l,r)位置上拷贝一份原数组[l,r)位置上的值,两个指针i和j都指向辅助数组的对应位置,然后向原数组填入对应的结果。这样我们的排序结果就还放在原来的内存地址上。

void merge_sort(int arr[], int len) {
	int* assist= new int[len];
	recursion(arr, 0, len, assist);
	delete[] assist;
}

recursion()

我们给出递归函数recursion,它只干两件事: 

①一刀劈开接收到的数组,然后传给两个子recursion。

②在劈开的两个数组有序后启动二路合并。

*注意*:根据代码语言传统,l和r往往是一个左闭右开区间,即[l,r),表示范围取到l,但取不到r。两个子数组的范围分别是[l,m),[m,r)。

归并排序,就是递归与合并排序。想一想我们的逻辑,我们是在recursion递出去再归回来后,才进行二路合并,因为这个时候这一层的两个子数组才是有序的。

*注意*:一个数先天有序,所以r-l<=1时直接返回。

void recursion(int arr[], int l, int r, int assist[]) {
	if (r - l <= 1)return;
	int m = (l + r) / 2;
	recursion(arr, l, m, assist);
	recursion(arr, m, r, assist);
	merge(arr, l, m, r, assist);
}

merge() 

我们给出递归函数recursion,它只干两件事: 

①把原数组对应位置的数据拷贝给assist辅助数组的对应位置,然后用原数组承接结果。

②进行一个极其简单的二路合并。

k指向arr数组,i和j指向assist辅助数组上的对应位置。arr[k]依次获得max(arr[i],arr[j])。

*注意*:i或j触底后将另一个数组的剩余部分通通接入合并后数组的末尾。

void merge(int arr[], int l, int m, int r, int assist[]) {
	memcpy(&assist[l], &arr[l], sizeof(int) * (r - l));
	int i, j, k;
	for (i = l, j = m, k = l;; k++) {
		if (i == m || j == r)break;
		if (assist[i] <= assist[j])arr[k] = assist[i++];
		else arr[k] = assist[j++];
	}
	while (i != m)arr[k++] = assist[i++];
	while (j != r)arr[k++] = assist[j++];
}

Code

void merge(int arr[], int l, int m, int r, int assist[]) {
	memcpy(&assist[l], &arr[l], sizeof(int) * (r - l));
	int i, j, k;
	for (i = l, j = m, k = l;; k++) {
		if (i == m || j == r)break;
		if (assist[i] <= assist[j])arr[k] = assist[i++];
		else arr[k] = assist[j++];
	}
	while (i != m)arr[k++] = assist[i++];
	while (j != r)arr[k++] = assist[j++];
}
void recursion(int arr[], int l, int r, int assist[]) {
	if (r - l <= 1)return;
	int m = (l + r) / 2;
	recursion(arr, l, m, assist);
	recursion(arr, m, r, assist);
	merge(arr, l, m, r, assist);
}
void merge_sort(int arr[], int len) {
	int* assist= new int[len];
	recursion(arr, 0, len, assist);
	delete[] assist;
}

谈谈奇数

常有人对于归并排序对于奇数长度数组的可行性产生质疑。我们来思考一下这个问题。

先想想二路归并:

     i  0 1 2 3 4
arr1[i] 1 5 6 8
arr2[i] 0 2 3 4 7
 ans[i] 1 2 3 4 5 6 7 8

结合上面的merge()实现代码,你能发现:两个长度不同的子数组丝毫不影响二路归并的进行。

那么奇数数组导致的中间位置不均匀丝毫不会影响整个排序。


优化方案

1.if语句优化

在一些时候,二路合并根本没必要发生:第一个有序子数组的最后一个数比第二个有序子数组的第一个数小,那么他们天然就接在了一起。

void merge_if(int arr[], int l, int m, int r, int assist[]) {
	if (arr[m - 1] <= arr[m])return;
	memcpy(&assist[l], &arr[l], sizeof(int) * (r - l));
	int i, j, k;
	for (i = l, j = m, k = l;; k++) {
		if (i == m || j == r)break;
		if (assist[i] <= assist[j])arr[k] = assist[i++];
		else arr[k] = assist[j++];
	}
	while (i != m)arr[k++] = assist[i++];
	while (j != r)arr[k++] = assist[j++];
}

2.小区间插入优化

最底层会产生大量recursion递归来对小区间二分,而他们分区的范围却极小,这是不必要的,而且相当被动。

插入排序在小区间的表现要好于分区合并,当分区小到某个程度时,我们直接转发给插入排序。

void recursion_with_insertion(int arr[], int l, int r, int assist[]) {
	if (r - l <= 20){
		insertion_sort(&arr[l], r - l); 
		return;
    }
	int m = (l + r) / 2;
	recursion_with_insertion(arr, l, m, assist);
	recursion_with_insertion(arr, m, r, assist);
	merge_if(arr, l, m, r, assist);
}

Code

void merge_if(int arr[], int l, int m, int r, int assist[]) {
	if (arr[m - 1] <= arr[m])return;
	memcpy(&assist[l], &arr[l], sizeof(int) * (r - l));
	int i, j, k;
	for (i = l, j = m, k = l;; k++) {
		if (i == m || j == r)break;
		if (assist[i] <= assist[j])arr[k] = assist[i++];
		else arr[k] = assist[j++];
	}
	while (i != m)arr[k++] = assist[i++];
	while (j != r)arr[k++] = assist[j++];
}
void recursion_with_insertion(int arr[], int l, int r, int assist[]) {
	if (r - l <= 20){
		insertion_sort(&arr[l], r - l); 
		return;
    }
	int m = (l + r) / 2;
	recursion_with_insertion(arr, l, m, assist);
	recursion_with_insertion(arr, m, r, assist);
	merge_if(arr, l, m, r, assist);
}
void MGsort(int arr[], int len) {
	int* assist = new int[len];
	recursion_with_insertion(arr, 0, len, assist);
	delete[] assist;
}

复杂度

时间复杂度:O(nlogn)

空间复杂度:O(n)

复杂度分析

时间分析:{

把一个长度为n的数组一直二分成长度为1的数组的次数是logn次,也就是有logn层

--------------------------------------
----------------- --------------------
--------- ------- ------ -------------
--- ----- --- --  -- --- --- ---------
- - - --- - - --  -- - - - - ---- ----
- - - - - - - - - - - - - - - - - - --
——————————————————n———————————————————
—m——m——m——m——m——m——m——m——m——m——m——m——m

每一层都要对子数组两两二路合并,复杂度为O(m),m为子数组的长度 ,而每一层的全体m求和为n,故每一层的复杂度都为O(n),有logn层,故得到O(nlogn)

}

空间分析:{

使用了长度为n的辅助数组,故为O(n)

*注意*:同一层的每次合并函数和递归函数结束后空间都会被释放,下次函数都会再次占据这块空间,空间复用使得每一层的复杂度都是O(1),函数的空间占用整体为O(logn) 。O(n+logn)=O(n)。

}


总结

分而治之,是为分治。

分治使得问题的数据规模缩小了,因此在枚举遍历时的效率大大提高。

归并排序作为高级排序的开篇一章,是纯粹的分治思想的应用,此后,我们将讲解三种基本排序(冒泡,选择,插入)与分治思想结合形成的高级排序算法:快速排序、堆排序、希尔排序。

DLC

逆序对问题是归并排序的延伸问题,详见:

「数组」逆序对 / LeetCode LCR 170(C++)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值