一、归并排序的基本概念
归并排序(Merge Sort)是一种基于分治法思想的高效稳定排序算法。其基本原理是将一个待排序的数组不断地分割成较小的子数组,直到每个子数组只包含一个元素,此时每个子数组都被认为是有序的。然后,再将这些有序的子数组合并成一个更大的有序数组。
归并排序的特点主要包括以下几个方面:
首先,它是一种稳定的排序算法,这意味着在排序过程中,相同元素的相对顺序不会发生改变。这对于某些需要保持原始顺序的场景非常重要。
其次,归并排序在处理大规模数据时表现出色,具有较好的时间复杂度。其平均时间复杂度、最坏时间复杂度和最好时间复杂度均为 O (n log n),其中 n 是待排序数组的长度。
归并排序的空间复杂度为 O (n),因为在合并过程中需要额外的辅助空间来存储临时数组。
在实际应用中,归并排序常用于对数据规模较大且对稳定性有要求的情况。例如,在处理包含多个属性的对象数组时,如果需要按照某个属性排序,同时保持其他属性的相对顺序不变,归并排序是一个不错的选择。
总的来说,归并排序以其稳定、高效的特点,在排序算法中占据着重要的地位。
二、归并排序的实现步骤
(一)递归分割
归并排序通过递归的方式将数组不断地分割成两半。具体过程如下:
首先,选择一个中间位置将数组分成左右两部分。然后,对左右两部分分别再次进行分割,重复这个过程,直到每个子数组只包含一个元素。
在实现中,通常通过计算中间索引来确定分割位置。例如,对于数组arr,长度为n,中间索引mid可以通过mid = (left + right) / 2计算得到。
接着,分别对左半部分arr[left, mid]和右半部分arr[mid + 1, right]进行递归分割,直到子数组的长度为 1 。
(二)合并操作
合并操作是归并排序的关键步骤。首先,创建一个与合并后数组大小相同的临时数组,用于存储合并过程中的元素。
然后,设置两个指针分别指向两个待合并子数组的起始位置。通过不断比较两个指针所指向的元素,将较小的元素放入临时数组,并相应地移动指针。
当其中一个子数组的元素全部放入临时数组后,将另一个子数组剩余的元素直接复制到临时数组的末尾。
最后,将临时数组中的元素复制回原数组,完成两个子数组合并为一个有序数组的操作。
在合并过程中,要确保相同元素的相对顺序不发生改变,以保证归并排序的稳定性。
三、归并排序的代码实现
(一)函数定义
// 归并函数
void merge(int arr[], int left, int mid, int right)
{
int i;// i 用于遍历左子数组 L
int j;// j 用于遍历右子数组 R
int k;// k 用于遍历原数组 arr
int n1 = mid - left + 1;// n1 为左子数组的长度
int n2 = right - mid;// n2 为右子数组的长度
int L[n1]; // 创建左子数组 L,长度为 n1
int R[n2]; // 创建右子数组 R,长度为 n2
for (i = 0; i < n1; i++) // 将原数组中左子数组的元素复制到 L
L[i] = arr[left + i];
for (j = 0; j < n2; j++) // 将原数组中右子数组的元素复制到 R
R[j] = arr[mid + 1 + j];
// 初始化指针
i = 0;
j = 0;
k = left;
// 当左子数组和右子数组都还有元素未处理时
while (i < n1 && j < n2)
{
// 如果左子数组当前元素小于等于右子数组当前元素
if (L[i] <= R[j])
{
arr[k++] = L[i++];// 将左子数组当前元素放入原数组,并移动指针
}
else
{
arr[k++] = R[j++];// 将右子数组当前元素放入原数组,并移动指针
}
}
// 如果左子数组还有剩余元素
while (i < n1)
{
arr[k++] = L[i++];
}
// 如果右子数组还有剩余元素
while (j < n2)
{
arr[k++] = R[j++];
}
}
// 归并排序函数
void mergeSort(int arr[], int left, int right)
{
// 如果左边界小于右边界,说明子数组至少有两个元素
if (left < right)
{
// 计算中间索引
int mid = left + (right - left) / 2;
// 对左子数组进行归并排序
mergeSort(arr, left, mid);
// 对右子数组进行归并排序
mergeSort(arr, mid + 1, right);
// 合并左右两个已排序的子数组
merge(arr, left, mid, right);
}
}
(二)示例应用
#include <stdio.h>
// 打印数组函数
void printArray(int arr[], int size)
{
// 循环遍历数组
for (int i = 0; i < size; i++)
// 打印数组中的每个元素,元素之间用空格分隔
printf("%d ", arr[i]);
// 打印一个换行符,使输出更加清晰
printf("\n");
}
int main()
{
// 定义一个整数数组并初始化
int arr[] = {12, 11, 13, 5, 6, 7};
// 计算数组的大小
int arrSize = sizeof(arr) / sizeof(arr[0]);
// 打印提示信息和原始数组
printf("原始数组: ");
printArray(arr, arrSize);
// 调用归并排序函数对数组进行排序
mergeSort(arr, 0, arrSize - 1);
// 打印提示信息和排序后的数组
printf("排序后的数组: ");
printArray(arr, arrSize);
return 0;
}
四、归并排序的性能分析
(一)时间复杂度
归并排序的时间复杂度为 O (n log n),这是因为其基本操作是将待排序数组不断地分成两半,然后再合并。在分解阶段,每次将数组一分为二,这部分的时间复杂度为 O (log n)。在合并阶段,对于长度为 n 的数组,合并操作的时间复杂度为 O (n)。综合起来,总的时间复杂度为 O (n log n)。
假设对长度为 n 的数组进行归并排序。每次将数组一分为二,直到每个子数组只有一个元素,共进行了 log n 次分解。每次合并两个子数组合成一个长度为 n 的有序数组,需要 n 次操作。所以总的操作次数为 n log n 。
(二)空间复杂度
归并排序通常需要额外的 O (n) 空间来存储临时数组。在合并操作中,为了将两个有序的子数组合并成一个有序的数组,需要创建一个与原数组大小相同的临时数组来存储中间结果。这意味着在整个排序过程中,需要额外的 O (n) 空间来辅助排序。
(三)稳定性
归并排序是稳定的排序算法。在合并两个有序子数组的过程中,如果遇到相等的元素,会按照它们在原始数组中的顺序将其放入临时数组,从而保证了相同元素的相对顺序不变。例如,当合并两个子数组时,如果两个当前元素相等,会将处于前面子数组的元素先放入临时数组,这样就确保了稳定性。这使得归并排序在处理需要保持元素原始相对顺序的场景时非常有用。