归并排序与快速排序------时间复杂度为O(nlogn)的排序算法

      归并排序与快速排序时间复杂度均为O(nlogn),适合大规模的数据排序且很常用,它们都用到了分治思想,将大的问题分解成小问题来解决。接下来我们分别对其进行分析。

归并排序(Merge Sort)

思路:归并排序的核心思想是先分后合。假如要排序一个数组,先将其从数组中间分解为两部分,分别进行排序,之后再将排好序的两部分按序合并在一起

解决方法:归并排序用到的是分治思想,分治(处理思想)常常也用递归(编程技巧)来实现。下面给出代码。

public class MergeSort {
    @Test
    public  void test() {
        int[] a = {5,9,4,3,2,1,4,5,6,8,7,5,4,3,9,8};
        mergeSort(a, 0, a.length-1);
        for (int i = 0; i < a.length; i++) {
            System.out.println(a[i]);
        }
    }

    public void mergeSort(int[] a, int start, int end) {
        if(start < end){
            int mid = (start + end)/2;//划分子数组
            mergeSort(a, start, mid);//排序左侧数组
            mergeSort(a, mid + 1, end);//排序右侧数组
            merge(a, start, mid, end);//将两部分数组排序合并
        }
    }

    public void merge(int[] a, int start, int mid, int end){
        int[] temp = new int[a.length];//辅助数组
        int p = start,q = mid + 1, k = start;
        //合并
        while(p <= mid&& q <= end){
            if(a[p] <= a[q]){
                temp[k++] = a[p++];
            }else{
                temp[k++] = a[q++];
            }
        }
        while(p<=mid){temp[k++] = a[p++];}
        while(q<=end) {temp[k++] = a[q++];}
        for (int i = start; i <= end; i++) {
            //将排序好的数组部分复制回原数组
            a[i] = temp[i];
        }
    }
}

其中,merge()函数用于将两个排序好的子数组合并。


性能分析

稳定性:判断归并函数的稳定性要观察merge()函数。你会发现,如果前半数组与后半数组有相同元素时,只要我们控制先把前半部分的元素放入数组(if(a[p] <= a[q])),就可以保证算法的稳定性。

时间复杂度

分析归并排序的时间复杂度涉及到递归,所以分析归并排序的方法我们也可以应用到分析递归代码中。

T(a) = T(b)+T(c)+k

其中T(n)表示解决n问题需要的方法,上面这条式子就可以理解为解决a问题的时间等于解决b、c问题的和加上k,k表示将bc两个子问题合并成a所用的时间。所以,在归并排序中。

T(n) = 2*T(n/2)+n

       =2*(2*T(n/4)+n/2)+n=4*T(n/4)+2n

       =4*(2*T(n/8)+n/4)+2n=8*T(n/8)+3n

       =8*(2*T(n/16)+n/8)+3n=16*T(n/16)+4n

       ......

       =2^k*T(n/2^k)+k*n

因为排序的终止条件是T(n/2^k)=T(1),此时n/2^k=1,解得k=log(2)n。所以原式子等于T(n) =Cn+nlog(2)n。用大O表示法表示就为O(nlogn)

我们从归并排序的代码可以看出,归并排序的执行效率与数据的有序程度无关,所以最差、最好、平均复杂度均为O(nlogn)。

空间复杂度:

归并排序的空间消耗来自于合并时的temp数组,每次合并时都需要申请一份额外空间,那是否空间复杂度就为这些额外空间的累加呢?

答案是否定的,我们要注意一点,递归代码的空间复杂度不能和时间复杂度一样累加,因为尽管每次合并都需要申请额外数组,但是当合并完成后就释放了,到下次合并时才会再次申请,所以每个时间点只有一份不超过n个数据的额外空间,所以空间复杂度就为O(n)

快速排序

思路:当我们要排序一个数组时,先任意选出一个数据(一般为第一个或最后一个)作为分区点(pivot),将比它小的放在左边,大的放在右边,再通过递归的思想,分别递归左右两部分,当区间缩小到1时,此时所有数据就全部有序了。下面给出代码。

public class QuickSort {
    @Test
    public void test() {
        int[] a = {8,4,5,6,4,8,3,1,9,5,4,2,3,7,8,4,1,7,6,6};
        quickSort(a, 0, a.length - 1);
        for (int i = 0; i < a.length; i++) {
            System.out.println(a[i]);
        }
    }

    public void quickSort(int[] a, int start, int end) {
        if (start < end) {
            //分区并找出分区点
            int i = partition(a, start, end);
            //递归左右两部分
            quickSort(a, start, i - 1);
            quickSort(a, i + 1, end);
        }
    }

    public int partition(int[] a, int start, int end) {
        //指定最后一个为分区点
        int pivot = a[end];
        //快速排序在分区时运用了巧妙的方法因此不用申请额外空间
        //将数据分为已排序(小于分区点)和未排序两部分
        //使用双指针,当遍历到比分区点小的元素时将其与未排序的第一个数据进行交换
        int i = start;
        for (int j = start; j < end; j++) {
            if(a[j] < pivot){
                int temp = a[i];
                a[i] = a[j];
                a[j] = temp;
                i++;
            }
        }
        //将分区点放在未排序的第一位,完成了左右划分
        a[end] = a[i];
        a[i] = pivot;
        return i;
    }
}

性能分析

稳定性:快速排序无法保证数组稳定性,可以用一个实例结合快排原理验证。

时间复杂度

快排的时间复杂度分析方法与归并排序类似,如果每次都把数组分解为两个大小相近的子数组,则时间复杂度为O(nlogn)。

但是,当最坏情况时,假如数组刚好倒序,则我们需要n次分区操作才能将数组排好序,这个过程类似冒泡排序,我们每次只能将最后一个数往前冒泡(可以用一个倒序数组进行验证),时间复杂度将退化为O(n²)

但是,大多数情况下时间复杂度接近O(nlogn),我们也有很多方法避免坏情况的发生。

空间复杂度:

快排排序过程中无需借助额外空间,所以空间复杂度为O(1)。


在实际运用中,快排要比归并排序更加广泛,原因在于快速排序无需额外空间的支持。有的人会认为这两者的原理相似,其实是大有不同,我们可以理解归并排序是先分后合(先处理子问题再合并),由下到上的,而快速排序是由上到下的(先分区再处理子问题)。

本文是学习王争老师的《数据结构与算法之美》之后做的学习笔记,如有错误请评论指正,谢谢!

 

你可能感兴趣的:(数据结构与算法)