算法 - 快速排序 - 经典快排 | 随机快排

经典快排

经典快排的思路是选取数组的最后一个数 x,按照问题一的思路把整个数组划分成 小于等于 x | 大于 x 两个部分,将 x 和 大于 x 部分数组的第一个元素交换位置。此时整个数组划分成 小于等于 x | x | 大于 x 三个部分,也就是这一次排序将 x 值排好位置。

再分别对 小于等于 x大于 x 中的数组递归划分,直到划分成一个数,此时所有元素也完成排序。

按照问题二的思路可以对经典快排做改进,使得每次划分数组成为小于 x | 等于 x | 大于 x 三个部分,通过这种排序方式可以一次划分多个 x 值的位置,排序效率得到提高。

但是,经典快排出现问题与数据状况有关。每次选择 x 值都是数组的最后一个数,如果遇到 [1,2,3,4,5] 或者 [5,4,3,2,1] 这种数组,算法时间复杂度将变成 O(n^2)。

随机快排

随机快排是经典快排的一种改进,通过生成随机下标 i,选择 a[i] 和最后一个数 x 进行交换,再使用经典快排。此时的事件就是一个概率事件,需要使用期望来估计算法的时间复杂度。

仍以 [1,2,3,4,5] 为例,经过随机快排初始变换,可以形成下列五种情况,数据状况的影响有效降低。在长期期望下,随机快排算法的时间复杂度为 O(N*logN)。由于每次划分数据都需要记录 =x 数组的下标范围,因此额外的空间复杂度为 O(logN)。

5,2,3,4,1;
1,5,3,4,2;
1,2,5,4,3;
1,2,3,5,4;
1,2,3,4,5.

思想

随机快排和经典快排的差别就在于添加了一行代码,使比较的数 x 具有随机性。

通过这种随机的方法处理特殊的数据,使得算法具有更好的鲁棒性。

单边循环法和双边循环法

上述快排的实现都是从一个方向上遍历元素,然后分成两个数组,成为单边循环法。还有另一种实现的方法是双边循环,在《大话数据结构》书中可以看到实现。

虽然实现方法多种多样,但是其核心本质仍然是选取数组中的值与数组其他元素比较大小,经过一轮循环之后将数组分成两个部分。单边循环和双边循环本质上是一样的,只是实现方式上不同。

快排代码

思路:使用 quickSort() 函数处理数组,先进行随机处理,使用核心方法 partition() 将数据分成 小于 x | 等于 x | 大于 x 三个部分,返回等于区域的左右下标值 [a, b],递归调用 小于 x 区域和 大于 x 区域。

quickSort(arr, left, right) {
	if (left < right) 
 		swap(); //随机快排的改动处
	    int[] p = partition(arr, left, right); //通过方法返回的是分组后确定的 = num 区域范围
		quickSort(arr, left, p[0] - 1); //递归 < num 区域
		quickSort(arr, p[1] + 1, right); //递归 > num 区域
}

partition(); //就是荷兰国旗问题的过程
public static void quickSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    quickSort(arr, 0, arr.length - 1);
}


public static void quickSort(int[] arr, int left, int right) {
    //多余判断
    //if (left == right) {
    //    return;
    //}
    if (left < right) {
        //随机快排核心
        //Math.random() 取值范围 [0, 1)
        //(Math.random() * (right - left + 1)) 此处数值 <= (right - left),因此可以保证参数 first 在区间内
        swap(arr, left + (int) (Math.random() * (right - left + 1)), right);
        int[] p = partition(arr, left, right);
        quickSort(arr, left, p[0] - 1);
        quickSort(arr, p[1] + 1, right);
    }
}


public static int[] partition(int[] arr, int left, int right) {
    int less = left - 1;
    int more = right + 1;
    //int more = right;
    //left 成为数组遍历的 cur 指针,当触碰到 more 边界时终止循环
    while (left < more) {
        if (arr[left] < arr[right]) {
            swap(arr, less + 1, left);
            less++;
            left++;
            //左神代码
            //swap(arr, ++less, left++);
        } else if (arr[left] == arr[right]) {
            left++;
        } else {
            swap(arr, left, more - 1);
            more--;
            //左神代码
            //swap(arr, left, --more);
        }
    }
    return new int[]{less + 1, more - 1};
    //return new int[]{less + 1, more};
}

手写代码过程中发现有些判断多余,因此做了注释。在整体代码中,有些语句能够精简成更简洁的代码,但是为了思路的连贯性,仅将简洁的代码做注释保留。从这些精简的代码中也可以看到一些优雅的代码真令人惊叹。

posted @ 2019-11-19 12:22  学习趁早  阅读(1113)  评论(0编辑  收藏  举报