算法与数据结构(归并排序)

归并排序 Merge Sort

利用分治策略来解决排序问题

分治法 Divide and Conquer:

分治法的基本思想如下:

  • 分解:将原问题分解为若干子问题,这些子问题是原问题的规模较小的实例。
  • 解决:递归地求解这些子问题。当子问题规模足够小时,可直接求解。
  • 合并:合并子问题的解成原问题的解。

归并排序算法完全遵循分治模式。其操作步骤如下:

  • 分解:将原序列分解为两个各占 n / 2 n/2 n/2 个元素的子序列。
  • 解决:使用归并排序递归地排序两个子序列。这里的策略是不断分解直到子序列只包含一个元素,此时子序列必然有序。
  • 合并:合并两个已排序的子序列以产生有序序列。

归并排序的思路:

如上所述,归并排序的基本思路是先将序列分为两部分,让左右两部分分别有序,然后合并。但是左右两部分的排序又重复上述过程,不断地分裂成两部分,直到最终都只有一个元素,此时所有的数组都是有序的。然后不断合并两个有序数组,直到最终成为一个整体。

其中的重点是合并步骤。假设数组为 A A A p p p q q q r r r 是数组下标。现在子数组 A [ p . . q ] A[p..q] A[p..q]
A [ q + 1.. r ] A[q+1..r] A[q+1..r] 都已排好序,我们要合并这两个有序子数组以形成单一的有序数组并替代当前的 A [ p . . r ] A[p..r] A[p..r]

还是以扑克牌为例,假设现在有两堆牌面朝上的牌,每堆都已从小到大排好序,最小的牌位于顶端。我们每一次都从两堆牌的顶上选择更小的那一张放入输出堆中,这样不断循环直到一个牌堆为空,然后将剩余的牌堆全部放入输出堆即可。合并过程用伪代码表示如下:

MERGE(A, p, q, r)
n1 = q-p+1
n2 = r-q
let L[1..n1=1] and R[1..n2+1] be new arrays
for i = 1 to n1
    L[i] = A[p+i-1]
for j = 1 to n2
    R[j] = A[q+j]
L[n1+1] = inf
R[n2+1] = inf
i = 1
j = 1
for k = p to r
    if L[i] <= R[i]
        A[k] = L[i]
        i = i+1
    else A[k] = R[j]
        j = j+1

值得注意的是可以使用哨兵来避免判断数组溢出。在第 9-10 行,我们将两个临时列表的最后一位元素设置为无穷大,当一个牌堆已经到底的时候,其哨兵牌将大于另一牌堆的所有牌(哨兵牌除外)。

之后我们就可以把上述的合并过程当作归并排序的一个子程序来使用。我们使用 M E R G E − S O R T ( A , p , r ) \mathrm{MERGE-SORT}(A, p, r) MERGESORT(A,p,r) 来对子数组 A [ p . . r ] A[p..r] A[p..r] 排序:

MERGE_SORT(A, p, r)
if p < r
    q = floor((p+r)/2) // 向下取整
    MERGE_SORT(A, p, q)
    MERGE_SORT(A, q+1, r)
    MERGE(A, p, q, r)

第 4-5 行对原序列不断分解,当全部子序列都只包含一个元素时,就已经完成了对最小子序列的排序工作,然后就开始合并。

图示如下:
分解:
在这里插入图片描述
合并:
在这里插入图片描述

实现:

以 Java 为例,C++代码见文末。

首先实现并的部分,当我们有两个有序数组需要合并时(这里采取的策略是用一个额外的数组 t e m p temp temp 来保存排好序的数组,然后将这个 t e m p temp temp 内的值返回到原数组),我们需要三个指针 l e f t , m i d left, mid left,mid r i g h t right right , 分别比较两个数组中较小的那个,保存到 t e m p temp temp 中。排序完毕后,将 t e m p temp temp 中的数据返回到原数组中。

这里用最后一次的排序举例:
在这里插入图片描述

// java
	/**
	 * 合并数组
	 * 
	 * @param arr   排序的原始数组
	 * @param left  左边有序序列的初始索引
	 * @param mid   中间索引
	 * @param right 右边索引
	 * @param temp  临时数组
	 */
	public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
		int i = left; // 初始化 i ,左边有序序列的初始索引
		int j = mid + 1; // 初始化 j ,右边有序序列的初始索引
		int t = 0; // 指向temp数组的第一位

		// 将左右两边的数据填充到temp数组
		while (i <= mid && j <= right) {
			if (arr[i] <=  arr[j]) {
				temp[t] = arr[i];
				i++;
			}else {
				temp[t] = arr[j];
				j++;
			}
			t++;
		}
		
		// 当一边填充完毕时,另一边数组一般还有剩余
		// 将剩余元素全部填充到temp数组
		// 如果左边还有剩余
		while (i<=mid) {
			temp[t] = arr[i];
			i++;
			t++;
		}
		// 如果右边还有剩余
		while (j <= right) {
			temp[t] = arr[j];
			j++;
			t++;
		}
		
		//将temp数组元素拷贝到arr数组,并不是将temp全部拷贝到arr,而是截取部分
		t = 0;
		int tl = left;
		while (tl <= right) {
			arr[tl] = temp[t];
			tl++;
			t++;
		}
	}

然后是拆分部分,主要是递归

代码如下:

// java
    public static void mergeSort(int[] arr, int left, int right, int[] temp) {
      if (left < right) {
        int mid = (left + right)/2;
        // 向左分解
        mergeSort(arr, left, mid, temp);
        // 向右分解
        mergeSort(arr, mid+1, right, temp);
        // 合并
        merge(arr, left, mid, right, temp);
      }
    }

下图是逻辑顺序:
在这里插入图片描述

时间复杂度:

归并排序的时间复杂度主要考虑两个函数需要的时间,即 mergeSort 和 merge

merge 合并的时间复杂度为 O ( n ) O(n) O(n),因为全部都是长度为 n n n 的一次循环

利用之前说过的递归的时间复杂度公式,归并排序的时间复杂度为 T ( n ) = 2 ( T ( n / 2 ) ) + O ( n ) T(n) = 2(T(n/2))+O(n) T(n)=2(T(n/2))+O(n)

这个递归式可以用递归树来解(假设解决最后一步子问题的时间是常数 c c c,则 n n n 个子问题花费的时间为 c n cn cn):
在这里插入图片描述
我们可以看到递归的每一层需要的时间都是 c n cn cn,总共有 l o g n + 1 logn+1 logn+1 层,总的时间为 c n ( l o g n + 1 ) cn(logn+1) cn(logn+1) ,时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)

空间复杂度:

归并排序在每一次合并时需要临时数组来储存排好序的序列,最后一次排序,要储存的数字最多,为 n n n,所以空间复杂度为 O ( n ) O(n) O(n)

稳定性:

归并排序可以保证等值元素之间的顺序,因此是稳定的


相关章节
第一节 简述
第二节 稀疏数组 Sparse Array
第三节 队列 Queue
第四节 单链表 Single Linked List
第五节 双向链表 Double Linked List
第六节 单向环形链表 Circular Linked List
第七节 栈 Stack
第八节 递归 Recursion
第九节 时间复杂度 Time Complexity
第十节 排序算法 Sort Algorithm
第十一节 冒泡排序 Bubble Sort
第十二节 选择排序 Select Sort
第十三节 插入排序 Insertion Sort
第十四节 冒泡排序,选择排序和插入排序的总结
第十五节 希尔排序 Shell’s Sort
第十六节 快速排序 Quick Sort
第十七节 归并排序 Merge Sort


C++ 实现:

#include <iostream>
#include <vector>
using namespace std;

/**
 * @brief Merge two ordered lists(nums[low..mid], nums[mid+1..high]).
 * 
 * @param nums 
 * @param low 
 * @param mid 
 * @param high 
 */
void merge(vector<int>& nums, int low, int mid, int high) {
    int len1 = mid-low+1;
    int len2 = high-mid;
    vector<int> left(len1+1);
    vector<int> right(len2+1);
    for (int i=0;i<len1;i++) {
        left[i] = nums[i+low];
    }
    for (int j=0;j<len2;j++) {
        right[j] = nums[j+mid+1];
    }
    // 设置哨兵
    left[len1] = INT_MAX;
    right[len2] = INT_MAX;

    // 合并 
    int i = 0;
    int j = 0;
    for (int k=low;k<=high;k++) {
        if (left[i] <= right[j]) {
            nums[k] = left[i++];
        } else {
            nums[k] = right[j++];
        }
    }

}


void mergeSort(vector<int>& nums, int low, int high) {
    if (low < high) {
        int mid = (high-low)/2+low;
        mergeSort(nums, low, mid);
        mergeSort(nums, mid+1, high);
        merge(nums, low, mid, high);
    }
}

// test
int main(int argc, char const *argv[])
{
    vector<int> nums = {4,7,2,9};
    mergeSort(nums, 0, nums.size()-1);
    for (auto num : nums) {
        cout << num << endl;
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值