面试_算法_归并排序

71 阅读3分钟

基本思想

归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略。


假设原始数据有n个,可以将n个数据看成n个独立的有序子序列,每个子序列的长度为1。这就将大问题分成了n个子问题。这是”分“的思想

相邻子序列两两比较并排序,得到[n/2]个长度为2或1的有序子序列;再两两比较并排序,一种重复下去,直到得到一个长度为n的有序序列为止。这就是”治“的思想




按照递归的写法。将整个序列分成n个独立的有序子序列。



治(合并)

再来看看阶段,需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。



代码实现

import java.util.Arrays;

public class A {
    static int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
    static int len = arr.length;

    public static void merge(int[] arr, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        int i = left;// 左指针
        int j = mid + 1;// 右指针
        int k = 0;
        // 下面的三个while:总的来说就是合并两个有序数组为一个有序数组(新建)
        // 第四个while:把新的有序数组覆盖到旧的无序数组空间
        // 把较小的数先移到新数组中
        while (i <= mid && j <= right) {
            if (arr[i] < arr[j]) {
                temp[k++] = arr[i++];
            } else {
                temp[k++] = arr[j++];
            }
        }
        // 把左边剩余的数移入数组
        while (i <= mid) {
            temp[k++] = arr[i++];
        }
        // 把右边边剩余的数移入数组
        while (j <= right) {
            temp[k++] = arr[j++];
        }
        // 将temp中的元素全部覆盖到原数组中
        // 这个步骤是是需要重复多次的。先覆盖最小的串,越来越大,最后覆盖整个长串
        int t = 0;
        while (left <= right) {
            arr[left++] = temp[t++];
        }
    }

    public static void mergeSort(int[] a, int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            // 左边
            mergeSort(a, left, mid);
            // 右边
            mergeSort(a, mid + 1, right);
            // 左右归并
            merge(a, left, mid, right);
            //System.out.println(Arrays.toString(a));// 输出每一趟的排序结果
        }
    }

    public static void main(String[] args) {
        mergeSort(arr, 0, len - 1);
        System.out.println("最终排序结果:" + Arrays.toString(arr));
    }
}


复杂度分析

  • 时间复杂度

    每次合并操作的平均时间复杂度为O(n)O(n),而完全二叉树的深度为log2nlog_2n。总的平均时间复杂度为O(nlogn)O(nlogn)。 而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)O(nlogn)。与初始序列排列状态无关。

    至于拆分其实是抽象上的,可以用下标来表示,一次拆分只需要O(1)O(1)时间。拆分的次数时可以计算的:第一层拆分1次,第二层拆分2次,第三层拆分4次。。。一直到倒数第二层,最后一层只有一个数据不会拆分。所以拆分操作的用时为:

    i=0logn12i=Θ(n)\sum_{i=0}^{\log n-1}2^i= \Theta(n)
  • 空间复杂度

    用顺序表实现归并排序时,需要一个数组来暂存整个待排序数组,故空间复杂度为O(n)O(n)



算法特性

  • 是稳定的排序
  • 可用于顺序存储结构,也可用于链式存储结构。并且使用链式存储结构使不需要附加存储空间,但递归实现时仍需要开辟相应的递归工作栈。
  • java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。


重要的应用:求逆序数的个数

归并排序的基本思想是分治,在治的过程中有前后数字的大小对比,此时就是统计逆序对的最佳时机。具体看下面的代码段(只加入了一个ans变量记录个数,其他部分完全不用动):

int ans=0;

...

while (i <= mid && j <= right) {
    if (arr[i] < arr[j]) {
        temp[k++] = arr[i++];
    } 
    else {
        // 如果使用ans+=(mid-i+1); 的写法,在牛客的编译器上,当遇到大规模输入时,会溢出
 		ans=ans+mid-i+1;
        temp[k++] = arr[j++];
    }
}
  • 因为当前左半段和右半段都是有序的,所以,如果arr[i]>arr[j]了,那么arr[i]到arr[mid]这几个数,都肯定比arr[j]大,所以这mid-i+1个数,都与arr[j]构成逆序数
  • 因为检测到这个现象后会立马把arr[j]放在暂存数组的尾部,构成有序的数列,所以代码后续不会再运行到这个else片段里,也就不会出现重复计数的情况



www.cnblogs.com/chengxiao/p…

www.nowcoder.com/practice/96…