排序算法二:归并排序与快速排序

比较两种排序方法

排序方法 时间复杂度 额外空间复杂度 是否稳定
归并排序(Merge Sort) O ( n l o g n ) O(nlogn) T ( n ) T(n)
快速排序(Quick Sort) O ( n l o g n ) O(nlogn) T ( 1 ) T(1)

比较排序的时间复杂度下限

所有比较排序如:归并排序、快速排序、堆排序、插入排序等基于两两比较数组内元素的排序算法的时间复杂度下限都是 O ( l o g ( N ! ) ) O(log(N!)) ~ O ( N l o g N ) O(NlogN)
证明:使用二叉树表示所有的比较,树中的叶子节点表示排序完成,内部节点表示一个比较操作。假设该数组有N个元素,则该数组有N!种排列方式即二叉树有N!个叶子节点。
从一个根节点到一个叶子节点的路径长度是某种输入下算法进行比较的次数,也就是算法比较次数的上限
高度为h的二叉树最多拥有 2 h 2^h 个叶节点
所以,任意一棵基于比较的排序算法对应着一棵高度为h的比较树,其中 N ! 2 h N! \leq {叶子节点的数量} \leq 2^h
所以, h > l o g ( N ! ) h > log(N!) , 而 l o g ( N ! ) log(N!) ~ N l o g ( N ) Nlog(N)
所以所有基于比较的排序算法的时间复杂度下限为 N l o g ( N ) Nlog(N)

一、归并排序

基本方法: 归并排序使用分治方法(Divide and Conquer)主要基于归并操作,即将两个有序的数组合起来归并成一个有序的数列。定义完归并之后,我们可以自顶上下递归地将数组折成两半分别排序并最终归并。
归并的具体操作:
使用一个额外数组,用于存储原数组中的值,定义三个指针low、high、k分别指向额外数组中对应原先左边数组和右边数组的第一个元素和原数组的首位,并比较大小。
如果i指向的小,则拷贝其到k指向的位置,并且把low与k进一位。
如果j指向的小,则拷贝其到k指向的位置,并且把high进一位。
如果额外数组中的左边数组用尽,则将右边数组剩余的位置依次添加到原数组中
如果额外数组中的右边数组用尽,则将左边数组剩余的位置依次添加到原数组中

例如:merge([1, 4, 7, 2, 5, 10], 0, 2, 5)

原数组 额外数组 i j k
( 1, 4, 7, 2, 5, 10 ) ( 1, 4, 7, 2, 5, 10 ) 0 3 0
( 1, 4, 7, 2, 5, 10 ) ( 1, 4, 7, 2, 5, 10 ) 1 4 1
( 1, 2, 7, 2, 5, 10 ) ( 1, 4, 7, 2, 5, 10 ) 2 4 2
( 1, 2, 4, 2, 3, 6 ) ( 1, 4, 7, 2, 5, 10 ) 2 4 3
( 1, 2, 4, 5, 3, 6 ) ( 1, 4, 7, 2, 5, 10 ) 2 5 4
( 1, 2, 4, 5, 7, 6 ) ( 1, 4, 7, 2, 5, 10 ) 3 5 5
( 1, 2, 4, 5, 7, 10 ) (1, 4, 7, 2, 5, 10 )

注:原数组中粗体数字为k所指向的元素,额外数组中粗体的两个数字分别为i和j所指向的元素

以下为归并排序的Java代码实现:

    public static void merge(int[] arr, int low, int mid, int high){
        int[] temp = new int[arr.length];
        int i = low, j = mid+1;
        for (int k = low; k <= high; k++){
            temp[k] = arr[k];
        }
        int k = low;
        while (i <= mid && j<= high){
            if (temp[i] < temp[j]){
                arr[k++] = temp[i++];
            }
            else {
                arr[k++] = temp[j++];
            }
        }
        while (i <= mid){
            arr[k++] = temp[i++];
        }
        while (j <= high){
            arr[k++] = temp[j++];
        }
    }


    public static void mergeSort(int[] arr, int low, int high){
        if (low >= high){
            return;
        }
        int mid = low + (high - low)/2;
        mergeSort(arr, low, mid);
        mergeSort(arr, mid + 1, high);
        merge(arr, low, mid, high);
    }

二、快速排序

基本方法: 快速排序使用了分治方法(Divide and Conquer)主要基于分割方法(Partition),即将一个数组根据一个元素(pivot)分成两部分,左边小于等于这个元素,右边大于这个元素,并返回这个元素的位置。
Partition的具体操作:
选择一个元素作为pivot(这边取子数组中的最后一位)。定义两个个指针i和j,i初始为子数组首位-1,j初始为子数组首位
循环比较j指向的元素与pivot的大小直至j超出子数组范围
如果j指向的元素小于pivot,则i进一位,并交换i与j指向的元素
j进一位
交换pivot与i+1指向的元素,并返回i+1

以下为Partition的Java代码实现:

    public static void swap(int[] arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
    public static int partition(int[] arr, int low, int high){
        int pivot = arr[high];
        int i = low-1;
        for (int j = i+1; j < high-1; j++){
            if (arr[j] <= pivot){
                i++;
                swap(arr, i, j);
            }
        }
        swap(arr, i+1, high);
        return i+1;
    }

例如: 对子数组( 3, 2, 1, 5, 4 )进行partition

子数组 i j pivot
( 3, 2, 1, 5, 4 ) -1 0 4
( 3, 2, 1, 5, 4 ) 0 1 4
( 2, 3, 1, 5, 4 ) 1 2 4
( 2, 1, 3, 5, 4 ) 2 3 4
( 2, 1, 3, 4, 5 ) 2 3 4

快速排序的具体操作: 先对数组进行partition得出pivot,再递归的排序pivot所分割出的左右两个子数组即可

以下为快速排序的的Java代码实现:

    public static void quickSort(int[] arr, int low, int high){
        if (low < high){
            int p = partition(arr, low, high);
            quickSort(arr, low, p-1);
            quickSort(arr, p+1, high);
        }
    }

猜你喜欢

转载自blog.csdn.net/zealband/article/details/88636541