Merge Sort(归并排序)

归并排序是一种采用分治法的排序算法,包括自顶向下的递归实现和自底向上的非递归实现。文章详细介绍了这两种方法的步骤和源码,还探讨了如何通过改进提升性能,如对小规模子序列使用插入排序、测试序列是否已有序以及角色互调。归并排序的时间复杂度为O(nlog2n),且是稳定的排序算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

       归并排序是建立在归并操作(将若干(通常为两个)有序的序列合成为一个更大的有序序列)上的一种排序算法,是分治法的典型应用。

算法思想

       用分治法的思想,将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。常见的归并排序将两个有序的子序列合成一个更大的序列,称为二路归并排序。

实现步骤

 

       以自顶向下的二路归并排序为例(从小到大):
1.申请辅助空间,存放子序列合并后的序列
2.设置两个指针,分别指向两个有序的子序列的起始位置
3.比较两个指针指向的元素值大小,将较小的元素放入辅助空间,并移动指针到下一个位置
4.重复步骤3,直到其中一个子序列的所有元素都访问完
5.将另一个元素未被访问完的子序列中的剩余所有未访问元素直接复制到辅助空间队列尾

 

源码

自顶向下的归并排序(递归实现)

void Merge (std::vector<int> &vec, std::vector<int> &tmp, int first, int mid, int last) {
    int i = first, j = mid+1, k = first;
    std::copy_n(vec.begin(), last-first+1, tmp);

    for (k = first; k <= last; ++k) {
        if (i > mid) {
            vec[k] = tmp[j];
            ++j;
        }
        else if (j > last) {
            vec[k] = tmp[i];
            ++i;
        }
        else if (tmp[i] < tmp[j]) {
            vec[k] = tmp[i];
            ++i;
        }
        else {
            vec[k] = tmp[j];
            ++j;
        }
    } 
}

void MergeSort (std::vector<int> &vec, std::vector<int> &tmp, int first, int last) {
    int mid;
    if (first < last) {
        mid = first + (last - first >> 1);
        MergeSort(vec, tmp, first, mid);
        MergeSort(vec, tmp, mid+1, last);
        Merge(vec, tmp, first, mid, last);
    }
}


       递归实现的归并排序是分治法的典型应用,分治法将一个大问题分割成小问题来分别解决,然后用所有小问题的答案来解决整个大问题。

自底向上的归并排序(非递归实现)

       实现归并的另一种方法是先归并小序列,然后再成对归并得到的子序列,如此这般,直到将整个序列归并到一起(听起来像是自顶向下归并的逆过程)。自底向上的归并首先进行的是两两归并(把每个元素都看做是一个大小为1的子序列),然后将归并后得到的大小为2的子序列再两两归并,直到归并完整个序列。最后一次归并的第二个子序列可能会比第一个小,否则所有归并过程中两个子序列大小都会是一样的。

void Merge (std::vector<int> &vec, std::vector<int> &tmp, int first, int mid, int last) {
    int i = first, j = mid+1, k = first;
    std::copy_n(vec.begin(), last-first+1, tmp);

    for (k = first; k <= last; ++k) {
        if (i > mid) {
            vec[k] = tmp[j];
            ++j;
        }
        else if (j > last) {
            vec[k] = tmp[i];
            ++i;
        }
        else if (tmp[i] < tmp[j]) {
            vec[k] = tmp[i];
            ++i;
        }
        else {
            vec[k] = tmp[j];
            ++j;
        }
    } 
}

void MergeSort (std::vector<int> &vec) {
    std::vector<int>::size_type n = vec.size();
    for (int gap = 1; gap < n; gap <<= 1) {
        for (int i = 0; i < n-gap; i += (gap << 1)) {
            Merge(a, tmp, i, i+gap-1, min(i+(gap<<1)-1, n-1));
        }
    }
}

 

       自底向上的归并排序比较适合用链表组织的数据。想象一下先按大小为1的子链表进行排序,然后是大小为2的子链表,然后是大小为4的子链表,依此类推。这种方法只需要重新组织链表链接就能将链表原地排序(不需要创建任何新的链表结点)。

改进

1.对小规模子序列使用插入排序

       用不同的排序算法配合处理小规模问题能改进大多数递归算法的性能,因为递归会使小规模问题中方法的调用过于频繁,所以改进对它们的处理方法就能改进整个算法。插入排序非常简单,相较而言,插入排序的原地、迭代实现的特性使得它在小规模数据上更有优势,因此很可能在小序列上比归并排序更快(这种速度从算法的时间复杂度上不能体现出来)。因此可以使用插入排序或选择排序处理小规模的子序列,一般可以将归并排序的运行时间缩短10%~15%。通过对插入排序和归并排序两者运行时间的分析可以得出数据规模的阙值在10~20之间。

       这种改进只针对规模较大的数据。

void InsertionSort (int *a, int first, int last) {
	int i, j, temp;
	for (i = first+1; i < last-first+1; ++i) {
		if (a[i] < a[i-1]) {
			temp = a[i];
			for (j = i-1; j >= first && a[j] > temp; --j)
				a[j+1] = a[j];
			a[i+1] = temp;
		}
	}
}


void Merge (int *a, int *tmp, int first, int mid, int last) {
    int i = first, j = mid+1, k = first;
    for (k = first; k <= last; ++k)
        tmp[k] = a[k];
    for (k = first; k <= last; ++k) {
        if (i > mid) {
            a[k] = tmp[j];
            ++j;
        }
        else if (j > last) {
            a[k] = tmp[i];
            ++i;
        }
        else if (tmp[i] < tmp[j]) {
            a[k] = tmp[i];
            ++i;
        }
        else {
            a[k] = tmp[j];
            ++j;
        }
    } 
}

void MergeSort (int *a, int *tmp, int first, int last) {
	int mid;
	if (first < last) {
		if (last-first+1 < 15)
			InsertionSort(a, first, last);
		else {
            mid = first+((last-first)>>1);
			MergeSort(a, tmp, first, mid);
			MergeSort(a, tmp, mid+1, last);
		}
		Merge(a, tmp, first, mid, last);
	}
}

 

2.测试序列是否已经有序

       添加一个判断条件,当子序列已经有序的时候就可以跳过Merge函数。这个改动不影响排序的递归调用,但任意有序的子序列算法的运行时间就变为线性的了。这样优化会使归并排序失去稳定性。

void Merge (int *a, int *tmp, int first, int mid, int last) {
    int i = first, j = mid+1, k = first;
    for (k = first; k <= last; ++k)
        tmp[k] = a[k];
    for (k = first; k <= last; ++k) {
        if (i > mid) {
            a[k] = tmp[j];
            ++j;
        }
        else if (j > last) {
            a[k] = tmp[i];
            ++i;
        }
        else if (tmp[i] < tmp[j]) {
            a[k] = tmp[i];
            ++i;
        }
        else {
            a[k] = tmp[j];
            ++j;
        }
    } 
}

void MergeSort (int *a, int *tmp, int first, int last) {
	int mid;
	if (first < last) {
		mid = first+((last-first)>>1);
		MergeSort(a, tmp, first, mid);
		MergeSort(a, tmp, mid+1, last);
		if (a[mid] > a[mid+1]) //大序列已经有序就不需要再调用merge来合并结果了
			Merge(a, tmp, first, mid, last);
	}
}

 

3.输入序列与辅助空间角色互调

       传统归并排序需要将原序列中的元素反复与辅助空间交换,我们可以考虑节省将元素复制到辅助空间所用的时间(但空间不行)。简单来说,就是让辅助空间与原序列的角色互调,这样从理论上就减少了一半的数据交换次数,更充分利用了辅助空间。这种方法需要在递归调用的每个层次交换输入序列和辅助序列的角色。

bool exch = false;
void Merge (int *a, int *tmp, int first, int mid, int last) {
    int i = first, j = mid+1, k = first;
    for (k = first; k <= last; ++k)
        tmp[k] = a[k];
    for (k = first; k <= last; ++k) {
        if (i > mid) {
            a[k] = tmp[j];
            ++j;
        }
        else if (j > last) {
            a[k] = tmp[i];
            ++i;
        }
        else if (tmp[i] < tmp[j]) {
            a[k] = tmp[i];
            ++i;
        }
        else {
            a[k] = tmp[j];
            ++j;
        }
    } 
}

void MergeSort (int *a, int *tmp, int first, int last) {
	if (first < last) {
        int mid;
		mid = first+((last-first)>>1);
        if (exch) {
        exch = false;
        MergeSort(tmp, a, first, mid);
		MergeSort(tmp, a, mid+1, last);
        Merge(tmp, a, first, mid, last);
        }
		else {
            exch = true;
            MergeSort(a, tmp, first, mid);
            MergeSort(a, tmp, mid+1, last);
            Merge(a, tmp, first, mid, last);
        }
	}
}

 

时间复杂度

       以自顶向下的归并排序为例,通过前面的示例图可以看到,一个拥有N个元素的序列需要划分log2N次(层),最下面的第k层有2k个子序列,每个子序列的长度为2n-k,归并最多需要2n-k次比较。因此每层的比较次数为2k×2n-k=2n,n层总共有n2n=Nlog2N。故归并排序所需的时间和Nlog2N成正比,也即归并排序的时间复杂度为O(nlog2n)。

       当序列长度为2的幂时,自顶向下和自底向上的归并排序所用的比较次数和数组访问次数正好相同,只是顺序不同。其他时候,两种方法的比较和数组访问的次序会有所不同。

       此外,没有任何基于比较的排序算法能够保证使用少于log2(N!)~Nlog2N次比较将长度为N的序列排序。因此,归并排序已经是基于比较的排序算法中时间复杂度最优的排序算法了。

稳定性

       对于相等元素,归并排序在分解和归并过程中并不会改变相等元素的相对顺序,因此归并排序是稳定的排序算法。

### 归并排序的实现原理 归并排序是一种基于分治法(Divide and Conquer)的有效排序算法。其核心思想是通过将待排序数组分割为更小的部分,分别对这些部分进行排序后再将其合并成为一个整体有序的结果[^1]。 具体来说,归并排序的过程可以分为以下几个方面: #### 1. **分解** 整个数据集被递归地划分为较小的子集合,直到每个子集合仅包含单个元素为止。因为单一元素本身已经是有序的,所以这一步骤完成了基础单元的创建[^2]。 #### 2. **合并** 当所有的子集合都已经被拆解到最小单位之后,开始逐步地把它们两两合并起来,在每次合并的过程中都会确保新形成的组合也是按照顺序排列好的。这种“归并”的过程会一直持续下去,直至最终形成一个完整的、完全有序的数据列表[^3]。 以下是归并排序的核心伪代码表示: ```python def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left_half = merge_sort(arr[:mid]) right_half = merge_sort(arr[mid:]) return merge(left_half, right_half) def merge(left, right): sorted_array = [] i = j = 0 while i < len(left) and j < len(right): if left[i] < right[j]: sorted_array.append(left[i]) i += 1 else: sorted_array.append(right[j]) j += 1 sorted_array.extend(left[i:]) sorted_array.extend(right[j:]) return sorted_array ``` 这段代码展示了如何使用 Python 来实现归并排序的功能。其中 `merge` 函数负责执行实际的合并操作,而 `merge_sort` 则控制着递归调用以及何时停止进一步划分输入数组。 ### 时间复杂度分析 由于每一次都将当前序列分成两半处理,并且每一层都需要遍历全部 n 项来进行比较和移动,因此总的运行时间为 O(n log n)。 ### 稳定性特点 值得注意的是,归并排序属于稳定性的排序方式之一,这意味着即使存在相等的关键字记录也不会改变彼此原有的次序关系。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值