归并排序

归并排序也是属于效率较高的排序,时间复杂度O(N logn),而且无论最好最坏情况都是O(N logn)
但是需要额外的O(N)的临时空间存放排序后的数组,这么说来是外部排序为不是内部排序
采用了分治的思想,

  1. 先分将数组元素分为最小只有一个元素的单位

分成两部分,称为二路归并,此外还有三路归并

  1. 再并:将每两组中的元素合并为一个有序数组
    结构上很类似于一颗完全二叉树,也可以采用迭代和递归两种写法

应用题目

代码实现

伪代码

递归实现

递归实现在于每一步合并都要copy一遍两个待合并,需要额外的空间
有一个问题在于,为什么空间复杂度是O(n)而不是O(n logn)?

我是这样分析的
每一次合并都要将两个待合并的数组复制一遍,那么相当于,每一层都将n个元素(整个数组)复制了一遍
那么一共多少层?按照二路归并的话log_2 n
也就是说空间复杂度应该是O(n logn)才对

解释是这样的:

实际上,递归代码的空间复杂度并不能像时间复杂度那样累加。刚刚我们忘记了最重要的一点,那就是,尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。
在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是O(n)

/*
* 归并排序
* 递归实现 来自GeeksForGeeks
* 把静态数据改成了动态数组的实现
*/

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
/*
* 并的每一步都copy了左右两个临时数组,然后遍历两个临时数组,把较小的放到原数组的响应位置中去
* 如果其中一个临时数组遍历完了,就把另一个临时数组中剩下的全部copy到原数组中去
*/
void merge(vector<int>& arr, int left, int mid, int right) {

	vector<int> tempLeft(arr.begin()+left, arr.begin() + mid+1);// 不包括mid+1
	vector<int> tempRight(arr.begin() + mid+1, arr.begin()+right+1);

	//for (int i : tempLeft)cout << i << " ";
	//cout << endl;
	//for (int j : tempRight)cout << j << " ";

	int i = 0, j = 0, k = left;
	while (i < tempLeft.size() && j < tempRight.size()) {
		if (tempLeft[i] < tempRight[j]) arr[k++] = tempLeft[i++];
		else arr[k++] = tempRight[j++];
	}

	while (i < tempLeft.size()) {
		arr[k] = tempLeft[i];
		k++;
		i++;
	}
	while (j < tempRight.size()) {
		arr[k] = tempRight[j];
		k++;
		j++;
	}
}

void mergeSort(vector<int> &arr,int left,int right) {
	if (left < right) {
		// same as (left+right)/2 but avoid overflow for INT_MAX
		int mid = left + (right - left) / 2;
		// 递归地去分左半边和右半边,并合并
		mergeSort(arr, left, mid);
		mergeSort(arr, mid + 1, right);
		// 合并左半边和右半边
		merge(arr, left, mid, right);
	}
}

int main() {

	vector<int> nums = { 4,6,5,1,7,9,3 };
	mergeSort(nums, 0, nums.size() - 1);
	// merge(nums, 0, 3, 6);
	for (int i : nums) cout << i << " ";

	return 0;
}

迭代实现

采用自底向上的迭代写法可以优化额外的空间复杂度,使之达到O(1)
迭代和归并的不同主要体现在merge_sort()排序主函数中分的过程

来自菜鸟教程

递归版本

这个版本的写法很不一样,

  1. 首先,它每次都copy构造了两个子数组,然后再从这两个子数组中挑元素往原数组放
  2. 构造的两个子数组容量都+1,并且设置末尾值为max值,为了比较大小的时候方便

感觉不是很常规,同时创建了更多的数组,需要更多的空间与时间
感觉效率很差
但是非常好理解

 // 合并操作
 void Merge(vector<int>& arr, int front, int mid, int end) {
	 // 这是个什么构造方式,是类似于传入一个vector,然后copy一份对吧
	 vector<int> LeftSubArr(arr.begin() + front, arr.begin() + mid + 1);
         // 这个很诡异,其实是从第一个参数开始,到第二个参数-1的复制
	 vector<int> RightSubArr(arr.begin() + mid + 1, arr.begin() + end + 1);
	 // 左半部分的指针和右半部分的指针
	 int idxLeft = 0, idxRight = 0;
	 // 后面的意思是返回编译器允许的int最大值
	 // 在数组末尾插入int最大值?方便后面比较,相当于是个辅助元素
	 LeftSubArr.insert(LeftSubArr.end(), numeric_limits<int>::max());
	 RightSubArr.insert(RightSubArr.end(), numeric_limits<int>::max());
	 // Pick min of LeftSubArray[idxLeft] and RightSubArray[idxRight], and put into Array[i]
	 for (int i = front; i <= end; i++) {
		 if (LeftSubArr[idxLeft] < RightSubArr[idxRight]) {
			 arr[i] = LeftSubArr[idxLeft];
			 idxLeft++;
		 }
		 else {
			 arr[i] = RightSubArr[idxRight];
			 idxRight++;
		 }
	 }
 }

 // 手写归并排序
 // 待排序的数组,待排序序列的起始下标、结束下标
 void MergeSort(vector<int>& arr, int front, int end) {
	 if (front >= end) return;// 当数组长度不合法或者数组长度为1时不理会
	 int mid = (front + end) / 2;
	 MergeSort(arr, front, mid);// 处理左半边
	 MergeSort(arr, front, mid);// 处理右半边
     Merge(arr, front, mid, end);
 }
posted @ 2022-09-03 23:14  YaosGHC  阅读(47)  评论(0)    收藏  举报