归并排序(Merge Sort)

什么是归并排序

归并排序(Merge Sort)是一种经典的排序算法,它采用分治法(Divide and Conquer)策略,将一个大数组分为两个小数组,分别进行排序,然后将这两个已排序的小数组合并成一个有序的大数组。归并排序在最坏情况下的时间复杂度为 O(nlog⁡n)O(nlogn),并且是一种稳定的排序算法。

核心思想

归并排序的核心思想可以分为以下几个步骤:

  1. 分解(Divide):将待排序的数组分为两个子数组,直到每个子数组只包含一个元素。因为一个元素的数组是有序的。
  2. 解决(Conquer):递归地对这两个子数组进行归并排序。
  3. 合并(Merge):将两个已排序的子数组合并成一个有序的数组。

快速排序与归并排序的比较

快速排序和归并排序都是经典的排序算法,均采用分治法(Divide and Conquer)的思想。尽管它们在基本原理上相似,但在实现细节、性能特征及适用场景上存在显著差异。

1. 基本思路

  • 快速排序
    • 选择一个基准元素(Pivot),通常是数组的第一个元素或随机选择。
    • 将数组分为两个部分:左侧部分包含小于基准的元素,右侧部分包含大于基准的元素。
    • 递归地对这两个部分进行快速排序,直到整个数组有序。
  • 归并排序
    • 将数组递归分为两个子数组,直到每个子数组只包含一个元素。
    • 然后将这两个已排序的子数组合并成一个有序的数组。
    • 合并过程是关键,它需要额外的空间来存放临时数组。

2. 稳定性

  • 快速排序:不稳定排序。在排序过程中,相等元素的相对顺序可能会被改变。
  • 归并排序:稳定排序。相等元素的相对顺序在排序后保持不变,这在某些应用中非常重要。

示例一

题目描述

代码实现

java 复制代码
import java.util.Scanner;

public class Main {
    final static int N = 1000000;
    private static int[] temp = new int[N];
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int arr[] = new int[n];

        for (int i = 0; i < n; i++) {
            arr[i] = scanner.nextInt();
        }
        mergeSort(arr, 0, n - 1);
        for (int i = 0; i < n; i++) {
            System.out.print(arr[i] + " ");
        }
    }
    public static void mergeSort(int[] arr, int l, int r) {
        if (l >= r) return;
        int mid = (l + r) >> 1;

        mergeSort(arr, l, mid);
        mergeSort(arr, mid + 1, r);

        int k = 0, i = l, j = mid + 1;
        while (i <= mid && j <= r) {
            if (arr[i] <= arr[j])
                temp[k++] = arr[i++];
            else
                temp[k++] = arr[j++];
        }
        //将剩余元素复制到temp数组中
        while (i <= mid) temp[k++] = arr[i++];
        while (j <= r) temp[k++] = arr[j++];

        //将temp数组中的元素复制回arr数组
        for (int t = l; t <= r; t++)
            arr[t] = temp[t - l];
    }
}

示例二

题目描述

求逆序对的数量

代码实现

java 复制代码
import java.util.Scanner;

public class Main {
    final static int N = 1000000;
    private static int[] temp = new int[N];
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int arr[] = new int[n];

        for (int i = 0; i < n; i++) {
            arr[i] = scanner.nextInt();
        }
        scanner.close();
        System.out.println(mergeSort(arr, 0, n - 1));

    }
    public static long mergeSort(int[] arr, int l, int r) {
        if (l >= r) return 0;
        int mid = (l + r) >> 1;

        long res =  mergeSort(arr, l, mid)+mergeSort(arr, mid + 1, r);

        int k = 0, i = l, j = mid + 1;
        while (i <= mid && j <= r) {
            if (arr[i] <= arr[j])
                temp[k++] = arr[i++];
            else{
                res += (mid - i + 1);
                temp[k++] = arr[j++];
            }

        }
        while (i <= mid)
            temp[k++] = arr[i++];
        while (j <= r)
            temp[k++] = arr[j++];
        for(i=l,j=0;i<=r;i++,j++)
            arr[i]=temp[j];
        return res;
    }
}

逆序对数量算法思路与实现

求逆序对的算法是利用了归并排序的思想,在归并排序的过程中会将序列分为两部分,此时逆序对可以分为三种情况:两个数都在左边的(设为s1)。两个数都在右边的(s2),一个数在左边一个数在右边的(s3)。现在假设我们在归并排序的时候写的函数mergeSort(int[] arr, int l, int r)可以返回l到r区间中逆序对数量。那么s1=mergeSort(a, l, mid),s2=mergeSort(a, mid + 1, r);,s3很显然没有直观的答案。

那么核心问题就在于怎么求s3,以及怎么使我们的mergeSort(int[] arr, int l, int r)可以返回l到r区间中逆序对数量。

在归并排序中,左右两边都是排序好的数列。因此,对于右图中的B我们只需要找到左边第一个大于B的数A,那么A后面的数都是大于B的。假设A的下标为i,不难得到我们要统计的数目为:**mid-i+1,**else中的语句即代表左边数列中的数大于右边中的数了,我们可以在此时将mid-i+1加到总答案中去。那么s3的问题就解决了。经过递归之后,res即我们所求。

时间复杂度

  • 最佳情况:O(nlog⁡n)O(nlogn)
  • 平均情况:O(nlog⁡n)O(nlogn)
  • 最坏情况:O(nlog⁡n)O(nlogn)

归并排序在所有情况下的时间复杂度都是 O(nlog⁡n)O(nlogn),这使得它在处理大规模数据时非常高效。

空间复杂度

归并排序的空间复杂度为 O(n)O(n),因为在合并过程中需要额外的存储空间来存放临时数组。

优点

  • 稳定排序,能够保持相等元素的相对顺序。
  • 在处理大规模数据时,性能稳定。

缺点

  • 需要额外的存储空间,不适合内存受限的环境。

应用场景

归并排序广泛用于需要稳定排序的场合,如外部排序(处理无法完全放入内存的数据集)和链表排序。由于其稳定性和性能,归并排序在许多编程语言的标准库中被实现为排序算法。

总结

归并排序是一种高效且稳定的排序算法,适用于大规模数据的排序。通过分治法的思想,归并排序能够在最坏情况下保持 O(nlog⁡n)O(nlogn) 的时间复杂度。虽然它需要额外的空间,但在许多实际应用中,归并排序仍然是一种非常有用的算法。

相关推荐
m0_6312704028 分钟前
标准C++(二)
开发语言·c++·算法
沫刃起31 分钟前
Codeforces Round 972 (Div. 2) C. Lazy Narek
数据结构·c++·算法
爱coding的橙子37 分钟前
CCF-CSP认证考试准备第十五天 202303-3 LDAP
算法
QXH2000002 小时前
Leetcode—环形链表||
c语言·数据结构·算法·leetcode·链表
小灰灰爱代码3 小时前
C++——判断year是不是闰年。
数据结构·c++·算法
小灰灰爱代码3 小时前
C++——求3个数中最大的数(分别考虑整数、双精度数、长整数数的情况),用函数重载方法。
数据结构·c++·算法
Kerwin要坚持日更3 小时前
Java小白一文讲清Java中集合相关的知识点(九)
java·开发语言
爱coding的橙子4 小时前
CCF-CSP认证考试准备第十七天
数据结构·c++·算法
常某某的好奇心4 小时前
56 - I. 数组中数字出现的次数
算法
_Power_Y5 小时前
JavaSE:11、内部类
java·开发语言