详解各版本快速排序算法及其时间和空间复杂度

快速排序算法思路

快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

前提:以下所有的算法版本都假设数组中并无重复元素。

原始版本Java实现及其性能分析

代码如下:

public static void quickSort(int[] a, int lo, int hi) {
        if (lo < hi) {
            int pivot = partition(a, lo, hi);
            quickSort(a, lo, pivot - 1);
            quickSort(a, pivot + 1, hi);
        }
    }

public static int partition(int[] a, int lo, int hi) {
        int x = a[lo];
        int j = hi + 1;

        for (int i = hi; i > lo; i--) {
            if (a[i] >= x) {
                j--;
                swap(a, i, j);
            }
        }
        swap(a, lo, j - 1);
        return j - 1;
    }

算法时间复杂度:上面算法如果在最坏情况(数组中的元素已经有序)下,时间复杂度为Θ(n^2);期望运行时间为Θ(nlogn)。

算法空间复杂度:由于快速排序为原址排序,所以其主要的空间复杂度来自于递归调用,每一次递归调用将在调用栈上创建一个栈帧。假设每一次传递数组的信息是采用指针的方式,那么每一次递归调用可以认为其空间复杂度为O(1)。那么算法在最坏情况下,有Θ(n)次递归调用,所以此时其空间复杂度为Θ(n);如果算法在平均情况下,其空间复杂度为O(logn)。

随机化版本Java实现及其性能分析

代码如下:

public static int randomized_partition(int[] a, int lo, int hi) {
        int i = new Random().nextInt(hi - lo + 1) + lo;
        swap(a, lo, i);
        return partition(a, lo, hi);
    }

这个版本的实现只是对原始版本的代码稍作改动,只不过将随机在数组中选择一个主元,并与数组中的第一个元素进行交换。上述代码中的partition方法与原始版本相同。

算法时间复杂度:虽然我们对程序在最坏情况下的运行时间感兴趣,但是在随机化版本中,它并没有改变最坏情况下的运行时间,它只是减少了出现最坏情况的可能性。因此,我们只分析算法的期望运行时间,而不是其最坏运行时间。它的期望运行时间是Θ(nlogn)。

算法空间复杂度:由于这个版本的算法只是减少最坏情况出现的可能性,所以其空间复杂度与原始版本的分析一致。

Hoare版本Java实现及其性能分析

代码如下:

public static void quickSort(int[] a, int lo, int hi) {
        if (lo < hi) {
            int pivot = hoare_partition(a, lo, hi);
            quickSort(a, lo, pivot);
            quickSort(a, pivot + 1, hi);
        }
}

public static int hoare_partition(int[] a, int lo, int hi) {
        int x = a[lo];
        int i = lo - 1;
        int j = hi + 1;

        while (true) {
            do
                j--;
            while (a[j] > x);

            do
                i++;
            while (a[i] < x);

            if (i < j)
                swap(a, i, j);
            else
                return j;
        }
}

这个版本中的quickSort方法,第一行递归不是pivot - 1而是pivot。这是因为当hoare_partition结束时,只能保证a[p…j]中的每一个元素都小于或等于a[j+1…r]中的元素,其所选取的主元并没有就位。

这个版本的算法在含有许多重复元素的情况下,可以避免其出现最坏情况的划分。

算法时间复杂度:由于这个版本的算法并没有杜绝最坏情况的出现,所以分析同上面两个版本。

算法空间复杂度:由于这个版本的算法并没有杜绝最坏情况的出现,所以分析同上面两个版本。

通过尾递归改变最坏情况下的空间复杂度

我们知道,对于任何一种算法改进的版本来说,都不可能完全避免最坏情况的出现,它们只是减小其出现的机率。但是改变最坏情况下的空间复杂度是可能做到的。我们通过递归调用元素少的那部分,对于元素多的那部分,我们改写尾递归。只要元素少的那部分总是小于或等于输入规模的一半,那么递归调用至多为O(logn)。

代码如下:

public static void tailRecursiveQuickSort(int[] a, int lo, int hi) {
        while (lo < hi) {
            int pivot = partition(a, lo, hi);//partition方法与上述版本相同
            if ( pivot - lo < hi - pivot ) {
                quickSort(a, lo, pivot - 1);
                lo = pivot + 1;
            } else {
                quickSort(a, pivot + 1, hi);
                hi = pivot - 1;
            }
        }
}

算法时间复杂度:由于这个版本的算法并没有杜绝最坏情况的出现,所以分析同上面两个版本。

算法空间复杂度:这个版本的算法其递归深度至多为logn,所以其空间复杂度为O(logn);

你可能感兴趣的:(算法,快速排序)