一、归并排序的基本概念

咱先来说说归并排序是个啥。归并排序是一种很经典的排序算法,它的核心思想就是分治和合并。啥是分治呢?简单来说,就是把一个大问题拆分成一个个小问题,等把这些小问题都解决了,大问题也就解决了。合并呢,就是把解决好的小问题的结果整合起来。

比如说,你有一堆杂乱无章的扑克牌,要把它们按顺序排好。归并排序会先把这堆牌分成两堆,然后再把每堆继续分,一直分到每堆只有一张牌为止。这就是分治的过程。然后呢,再把这些单张的牌两两合并,合并的时候按顺序排好,接着再把合并后的小堆继续合并,直到最后又变成一堆排好序的牌。这就是合并的过程。

二、归并排序的具体实现步骤

1. 分治阶段

分治阶段就是不断地把数组或链表分成两半,直到每一部分只有一个元素或者为空。下面是用 Java 实现的分治阶段的代码示例:

// Java 技术栈
// 分治函数,对数组进行分割
public static 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); 
    }
}

2. 合并阶段

合并阶段就是把两个已经排好序的子数组合并成一个有序的数组。下面是 Java 实现的合并阶段的代码示例:

// Java 技术栈
// 合并函数,将两个有序数组合并成一个有序数组
public static void merge(int[] arr, int left, int mid, int right) {
    // 计算左半部分数组的长度
    int n1 = mid - left + 1; 
    // 计算右半部分数组的长度
    int n2 = right - mid; 

    // 创建临时数组来存储左右两部分的元素
    int[] L = new int[n1];
    int[] R = new int[n2];

    // 将左半部分元素复制到临时数组 L 中
    for (int i = 0; i < n1; ++i) {
        L[i] = arr[left + i];
    }
    // 将右半部分元素复制到临时数组 R 中
    for (int j = 0; j < n2; ++j) {
        R[j] = arr[mid + 1 + j];
    }

    // 初始化索引变量
    int i = 0, j = 0;
    int k = left;

    // 比较左右两部分元素,将较小的元素放入原数组
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }

    // 将左半部分剩余元素复制到原数组
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }

    // 将右半部分剩余元素复制到原数组
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

3. 完整的归并排序代码

把分治和合并阶段的代码结合起来,就得到了完整的归并排序代码:

// Java 技术栈
public class MergeSort {
    // 分治函数,对数组进行分割
    public static 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); 
        }
    }

    // 合并函数,将两个有序数组合并成一个有序数组
    public static void merge(int[] arr, int left, int mid, int right) {
        // 计算左半部分数组的长度
        int n1 = mid - left + 1; 
        // 计算右半部分数组的长度
        int n2 = right - mid; 

        // 创建临时数组来存储左右两部分的元素
        int[] L = new int[n1];
        int[] R = new int[n2];

        // 将左半部分元素复制到临时数组 L 中
        for (int i = 0; i < n1; ++i) {
            L[i] = arr[left + i];
        }
        // 将右半部分元素复制到临时数组 R 中
        for (int j = 0; j < n2; ++j) {
            R[j] = arr[mid + 1 + j];
        }

        // 初始化索引变量
        int i = 0, j = 0;
        int k = left;

        // 比较左右两部分元素,将较小的元素放入原数组
        while (i < n1 && j < n2) {
            if (L[i] <= R[j]) {
                arr[k] = L[i];
                i++;
            } else {
                arr[k] = R[j];
                j++;
            }
            k++;
        }

        // 将左半部分剩余元素复制到原数组
        while (i < n1) {
            arr[k] = L[i];
            i++;
            k++;
        }

        // 将右半部分剩余元素复制到原数组
        while (j < n2) {
            arr[k] = R[j];
            j++;
            k++;
        }
    }

    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6, 7};
        System.out.println("排序前的数组: ");
        for (int num : arr) {
            System.out.print(num + " ");
        }
        mergeSort(arr, 0, arr.length - 1);
        System.out.println("\n排序后的数组: ");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

在这个代码中,mergeSort 函数负责分治,merge 函数负责合并。在 main 函数中,我们创建了一个数组,然后调用 mergeSort 函数对其进行排序,并输出排序前后的数组。

三、归并排序在链表排序中的应用

链表排序和数组排序有点不一样,因为链表不能像数组那样直接通过索引访问元素。但是归并排序同样适用于链表。下面是用 Java 实现的链表归并排序的代码示例:

// Java 技术栈
// 定义链表节点类
class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

public class LinkedListMergeSort {
    // 对链表进行归并排序
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        // 找到链表的中间节点
        ListNode mid = getMiddle(head);
        ListNode right = mid.next;
        mid.next = null;

        // 递归排序左右两部分
        ListNode leftSorted = sortList(head);
        ListNode rightSorted = sortList(right);

        // 合并左右两部分
        return mergeLists(leftSorted, rightSorted);
    }

    // 找到链表的中间节点
    private ListNode getMiddle(ListNode head) {
        if (head == null) {
            return null;
        }
        ListNode slow = head;
        ListNode fast = head.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // 合并两个有序链表
    private ListNode mergeLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                tail.next = l1;
                l1 = l1.next;
            } else {
                tail.next = l2;
                l2 = l2.next;
            }
            tail = tail.next;
        }
        if (l1 != null) {
            tail.next = l1;
        }
        if (l2 != null) {
            tail.next = l2;
        }
        return dummy.next;
    }

    public static void main(String[] args) {
        // 创建链表 4 -> 2 -> 1 -> 3
        ListNode head = new ListNode(4);
        head.next = new ListNode(2);
        head.next.next = new ListNode(1);
        head.next.next.next = new ListNode(3);

        LinkedListMergeSort sorter = new LinkedListMergeSort();
        ListNode sortedHead = sorter.sortList(head);

        // 输出排序后的链表
        ListNode current = sortedHead;
        while (current != null) {
            System.out.print(current.val + " ");
            current = current.next;
        }
    }
}

在这个代码中,sortList 函数负责对链表进行归并排序,getMiddle 函数用于找到链表的中间节点,mergeLists 函数用于合并两个有序链表。在 main 函数中,我们创建了一个链表,然后调用 sortList 函数对其进行排序,并输出排序后的链表。

四、归并排序的应用场景

1. 大规模数据排序

当需要对大规模数据进行排序时,归并排序是一个不错的选择。因为它的时间复杂度是 $O(n log n)$,在处理大规模数据时效率比较高。比如说,在处理电商平台的商品价格排序、数据库中的数据排序等场景中,归并排序都能发挥很好的作用。

2. 外部排序

外部排序是指数据量太大,无法一次性全部加载到内存中进行排序的情况。归并排序可以通过分块和合并的方式,将数据分成多个小块进行排序,然后再将这些小块合并成一个有序的整体。这在处理大数据集时非常有用,比如处理日志文件、大型数据库备份等。

3. 链表排序

就像前面我们看到的,归并排序在链表排序中也有很好的应用。因为链表的特性,不能像数组那样随机访问元素,而归并排序只需要顺序访问链表节点,非常适合链表排序。

五、归并排序的技术优缺点

1. 优点

  • 稳定性:归并排序是一种稳定的排序算法,即相等元素的相对顺序在排序前后不会改变。这在一些对元素相对顺序有要求的场景中非常重要,比如在对学生成绩进行排序时,如果有多个学生成绩相同,我们希望他们的顺序在排序后保持不变。
  • 时间复杂度:归并排序的时间复杂度始终是 $O(n log n)$,无论数据的初始状态如何。这使得它在处理大规模数据时具有较好的性能。
  • 适用性:归并排序不仅适用于数组,还适用于链表等数据结构,具有很强的适用性。

2. 缺点

  • 空间复杂度:归并排序需要额外的空间来存储临时数组或链表,空间复杂度为 $O(n)$。这在处理大规模数据时可能会占用较多的内存。
  • 常数因子:虽然归并排序的时间复杂度是 $O(n log n)$,但它的常数因子相对较大,在处理小规模数据时,可能不如一些简单的排序算法(如插入排序)效率高。

六、归并排序的注意事项

1. 空间使用

由于归并排序需要额外的空间来存储临时数据,所以在处理大规模数据时,要注意内存的使用情况。可以考虑使用一些优化方法,如原地归并排序,来减少空间的使用。

2. 递归深度

归并排序是通过递归实现的,递归深度为 $O(log n)$。在处理大规模数据时,递归深度可能会比较大,这可能会导致栈溢出的问题。可以考虑使用迭代的方式来实现归并排序,避免递归带来的问题。

3. 数据类型

归并排序适用于各种数据类型,只要这些数据类型可以进行比较。在使用时,要确保数据类型支持比较操作,否则需要自定义比较函数。

七、文章总结

归并排序是一种非常实用的排序算法,它的核心思想是分治和合并。通过不断地将数据分成小问题,然后解决这些小问题,最后将结果合并起来,实现对数据的排序。归并排序具有稳定性、时间复杂度低等优点,适用于大规模数据排序、外部排序和链表排序等场景。但它也有空间复杂度高、常数因子大等缺点。在使用归并排序时,要注意空间使用、递归深度和数据类型等问题。