基础闯关记-快速排序

快速排序

快速排序基本思想

快速排序的基本思想就是分治法,就是将一个大的数组按照某一中间值分成两个子集,一组是每个元素都大于中间值,另一组是每个元素都小于中间值,然后递归调用该过程,最后可完成排序。步骤:

1、先从数列中取出一个数作为基准数,可以是第一个元素或最后一个元素。

2、分两个子集,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。

3、再对左右子集间重复第二步,直到各子集只有一个数,排序结束

整个过程可以用下图的竹竿按从低到高的顺序排序,首先我们找出第一根杆子为标准,剩下的高杆移到右边,低杆移到左边(不用管两遍的顺序,只要符合低左高右即可)。然后针对左边再次进行同样的动作,最后就可以保证左边是从低到高排列的,再针对右边也进行同样的动作(图中没有画),最终所有的竹竿都是有序的。

image

这个图可以看出,只要不断递归左右两个子集,最终递归结果就是整个数组有序。所以快速排序的核心是找到一个快速的左右子集划分方法,以及找到一个合适的基准元素,这两个因素影响了你写的快速排序到底快不快。

实现快速排序

前面说到,找到划分方法和基准元素,快速排序基本上就实现了。

如果要实现以某一个元素为基准,将数组划分成左右两个子集。方法其实有很多种,有的可能时间复杂度高,有的可能耗费空间多。

左右子集划分思路

《啊哈,算法》里介绍的一种方法,这种方法的效率比较高。这种方法就是假设有两个探针,分别是左探针和右探针。一开始分别指向数组的首地址和末地址。然后右探针开始向中间靠拢,直到发现一个小于基准值的元素,然后左探针也向中间靠拢,直到发现一个大于基准值的元素,这是交换两个探针的数组,直到两个探针相遇结束。整个过程可以用下图表示,仔细观察下基准元素4是如何插入到中间的

image

这是可以思考一下为什么不能让i先走,如果i先走,可能就把一个比基准元素大的放在了第一位,因为i总是碰到比基准元素大的才停止。

左右子集划分代码

对照上图,很容易就可以写出相应的算法,实现上面过程的函数是partition函数,需要注意的是,看上面的图,最终和基准元素交换的是i和j相遇的地方。

#include<stdio.h>
void partition(int arr[], int left, int right);
void swap(int *a, int *b);

void main(){
    int arr[] = {4, 3, 6, 7, 1, 5, 2};
    partition(arr, 0, 6);
    int i = 0;
    while( i <= 7 ){
        printf("%d ", arr[i]);
        i++;
    }
}

/**
 * 以数组第一个元素为基准数,重排数组使得基准数左边都比它小,基准数右边都比它大
 * @param arr 数组地址
 * @param left 数组第一个元素地址 
 * @param right 数组最后一个元素地址
 */
void partition(int arr[], int left, int right){
    if(left >= right){
        return;
    }
    int mid = arr[left];
    int i = left;
    int j = right;
    while( i < j ){
        while(arr[j] >= mid && i < j){
            j--;
        }

        while(arr[i] <= mid && i < j){
            i++;
        }
        if(i<j){
            swap(&arr[i], &arr[j]);
        }
    }
    swap(&arr[i], &arr[left]);
}

//工具函数,交换两个数组元素
void swap(int *a, int *b){
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

之所以把左右子集划分的算法单独提取出来,因为这个算法是可替代的,你可以写出自己的更高效的划分算法。

快速排序实现

有了上面的左右子集划分算法的实现,我们很快就能编写出一个快速排序算法,只需要递归调用左右自己划分算法就可以了,然后在这个基础上,再封装一个快速排序的方法

void quickSort(int arr[], int length){
    partition(arr, 0, length-1);
}

最后,附上快速排序的整个源代码

#include<stdio.h>
void partition(int arr[], int left, int right);
void swap(int *a, int *b);
void quickSort(int arr[], int length);

void main(){
    int arr[] = {4, 3, 6, 7, 1, 5, 2};
    quickSort(arr, 7);
    int i;
    for(i=0; i<7; i++){
        printf("%d ", arr[i]);
    }
}

void quickSort(int arr[], int length){
    partition(arr, 0, length-1);
}

/**
 * 以数组第一个元素为基准数,重排数组使得基准数左边都比它小,基准数右边都比它大
 * @param arr 数组地址
 * @param left 数组第一个元素地址 
 * @param right 数组最后一个元素地址
 */
void partition(int arr[], int left, int right){
    if(left >= right){
        return;
    }
    int mid = arr[left];
    int i = left;
    int j = right;
    while( i < j ){
        while(arr[j] >= mid && i < j){
            j--;
        }

        while(arr[i] <= mid && i < j){
            i++;
        }
        if(i<j){
            swap(&arr[i], &arr[j]);
        }
    }
    swap(&arr[j], &arr[left]);
    partition(arr, left, j-1);
    partition(arr, j+1, right);
}

//工具函数,交换两个数组元素
void swap(int *a, int *b){
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

基准元素

我们前面随便选第一个元素为基准元素太草率了。选基准元素很重要,要不然快速排序一点也不快。前面我们直接使用了数组的第一个元素为基准。但是这种情况往往并不理想,因为快速排序的思想是分治法,如果分的不均匀,那快速排序的时间复杂度一样为O(n^2),试想一下最糟糕的情况,输入的数组已经有序了,在这个有序的基础上再次排序会怎么样。

image

计算一下这种情况的复杂度

T(n) = O(n) + T(n-1)
     = O(n) + O(n-1) + T(n-2)
     = O(n) + O(n-1) + ... + O(1)
     = O(n^2)

快速排序最理想的情况找到一个基准元素将数组对半分开

这也是分治法最理想的情况,因为每次对半开的话,子集的个数将按指数规模减少。所以针对快速排序,需要处理好基准元素的选取。

这里介绍一种取数组头,中,尾的中位数作为基准元素,有三种情况

1. 第一个元素已经是中位数,不用处理
2. 中间元素为中位数,交换第一个元素和中间元素
3. 最后一个元素为中位数,交换第一个元素和最后一个元素

代码实现

int getMid(int arr[], int left, int right){
    int leftVal = arr[left];
    int rightVal = arr[right];
    int mid = (left + right) / 2;
    int midVal = arr[mid];

    if(leftVal < midVal && leftVal < rightVal){
        return leftVal;
    }else if(midVal < leftVal && midVal < rightVal){
        swap(&arr[left], &arr[mid]);
        return midVal;
    }else{
        swap(&arr[left], &arr[right]);
        return rightVal;
    }
}
posted @ 2017-06-22 17:00  一路既往  阅读(314)  评论(0)    收藏  举报