重识快排(一)

Posted by 令德湖周杰伦 on 11-10,2020

背景

常见的排序算法由:插入排序,冒泡排序,shell排序,快速排序,堆排序,归并排序,基数排序等等。
其中,快速排序使用的最为广泛,因为它实现简单、时间复杂度和空间复杂度较低。

一、经典快排

Classic Qucik Sort

1. 描述

1.1 分解divide

将数组A[ p, …, r]划分为两个子数组A[ p, …, q-1]和 A[q+1, …, r], 使得A[ p, …, q-1]中的每个元素都小于等于A[q],且A[q+1, …, r]中的每个元素都大于A[q]

1.2 求解conquer

通过递归调用快速排序,对子数组A[ p, …, q-1]和 A[q+1, …, r]分别排序。

1.3 合并combine

两个子数组都是有序的,因此直接合并即可

2. 实现

2.1. pivot的选择

上面的q就是pivot,通常的选择方式有:

  • 选取中间的元素
  • 选取第一个元素
  • 选取最后一个元素

2.2 代码的实现

以下代码都是递归实现的,非递归实现可以看文章末尾

 /**
 * pivot选取中间元素,实现quickSort
 * 
 * @param arr = {6,3,4,1,5,8,7}
 * @param l 初始 0
 * @param r 初始 arr.length -1
 */
public static void quickSort(int[] arr,int l,int r){
    int i = l;
    int j = r;
    int piovt = arr[(l+r)/2];

    while (i<=j) {
        while (arr[i] < piovt) {
            i++;
        }
        while (arr[j] > piovt) {
            j--;
        }
        if(i<=j){
            int tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
            i++;
            j--;
        }
    }
    if(l<j){
        quickSort(arr,l,i);
    }
    if(i<r){
        quickSort(arr,i,r);
    }
}
/**
 * pivot选取第一个元素,实现quickSort
 * 
 * @param arr = {6,3,4,1,5,8,7}
 * @param l 初始 0
 * @param r 初始 arr.length -1
 */
public static void quickSort1(int arr[], int l, int r){
    int i;
    int j;
    //递归终止条件
    if(l<r){
        i = l;
        j = r + 1;
        while(true){
            do{
                i++;
            }while(arr[i] < arr[l] && i < r);

            do{
                j--;
            }while(arr[j] > arr[l] && j > l);

            if(i<j){
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }else{
                break;
            }
        }
        //交换哨兵位置
        int temp = arr[j];
        arr[j] = arr[l];
        arr[l] = temp;
        quickSort(arr,l,j-1);
        quickSort(arr,j+1,r);
    }
}
/**
 * pivot最后一个元素,实现quickSort
 * 
 * @param arr = {6,3,4,1,5,8,7}
 * @param l 初始 0
 * @param r 初始 arr.length -1
 */
public static void quickSort2(int a[], int p, int r){
    //递归终止条件
    if(p < r){
        int q = partition(a, p ,r);
        System.out.println(q);
        quickSort2(a, p, q-1);
        quickSort2(a, q+1, r);
    }
}
/**
 * 排序并返回pivot的位置
 *
 * @param arr 待排序数组
 * @param p 数组头下标
 * @param r 数组尾下标
 * @return
 */
public static int partition(int arr[], int p, int r){
    //选择尾为pivot元素
    int x = arr[r];
    int i = p-1;
    for (int j = p; j <= r-1; j++) {
        if(arr[j] <= x){
            i++;
            //交换边界位置
            int temp = arr[j];
            arr[j] = arr[i];
            arr[i] = temp;
        }
    }
    //交换r和pivot的位置
    int temp = arr[r];
    arr[r] = arr[i+1];
    arr[i+1] = temp;
    return i+1;
}

3. 性能分析

3.1 Worst-case of quicksort

  • 输入数组已经排好序(正向/反向)
  • 在最小/大值附近划分(主元的选择)
  • 划分的一个子集为空 1 + (n - 1)+ 0

时间复杂度 T(n) = n^2

3.2 Best-case of quicksort

  • 每次都是平均分成两个子序列

时间复杂度: T(n) = 2T(n/2) + O(n) = O(nlogn)

3.3 更多的分法

如果是1:9的分法呢?
T(n) = T(n/10) + T(9n/10) + O(n) = O(nlogn)
quicksort1.jpg

3.4 更有意思的情况

假设每次划分的情况是lucky和unlucky不断交替的,即:
L(n) = 2U(n/2) + O(n)
U(n) = L(n-1) + O(n)

L(n) 
= 2(L((n-1) / 2) + 1/2 O(n)) + O(n)
= 2L((n-1) / 2) + 2O(n)
= 2nlogn
= O(nlogn)

二、随机版本

Randomed-QuickSort
由经典快排,我们可知时间复杂可能为O(n^2), 那为什么快排还能在实际的算法中应用

1. 描述

从子数组A[ p, …, r]中随机选择一个元素作为主元
将随机选择出来的主元与A[r]交换
主元元素x = A[r]是等概率地从 r-p+1 个元素中选出来

2. 实现

/**
 * 随机版本的快排
 * @param arr
 * @param p
 * @param r
 */
public static void quickSortWithRandom(int arr[], int p, int r){
    if(p<r){
        int q = randomPartition(arr, p ,r);
        quickSortWithRandom(arr, p , q-1);
        quickSortWithRandom(arr, q+1, r);
    }
}

public static int randomPartition(int arr[], int p, int r){
    int i = random(p, r);
    //交换i和r的位置
    int temp = arr[r];
    arr[r] = arr[i];
    arr[i] = temp;
    return partition(arr, p ,r);
}

3. 性能分析

  • 运行时间不受输入数组顺序的影响
  • 不需要对输入的分布做任何假设
  • 任何预先设置都不一定会导致最差情况
  • lucky / unlucky主要依赖随机数的生产过程

T(n) 是randomized-quicksort的运行时间,因此是一个随机变量。
定义X(k):将数组分区成(k :n-k-1)的事件,其中k=0, 1, 2 ... n-1
那么X(k)的取值分为2中情况:

X(k) = 1 能; 
     = 0 否;

假设每次划分都是等概率的,且没有重复元素,那么有

二点分布:
X   1    0
P  1/n   0
则X的期望E(X(k)) = 1 * 1/n + 0 = 1/n

T(n)可以分为以下情况:
image.png
两边求期望:
image.png
其中:k=0,1两项可以归入O(n),以下证明E[T(n)] <= anlogn
image.png

由以上可知
时间复杂度:T(n) = O(nlogn)

快速排序的非递归实现,用到了栈

public static void quickSortByStack(int arr[]){
    int length = arr.length;
    int buf[][] = new int[length][2];
    int i,j;
    int pos = -1;
    int l = 0;
    int r = length-1;
    while(true){
        i = l;
        j = r + 1;
        while(true){
            do{
                i++;
            }while(arr[i] < arr[l] && i < r);
            do{
                j--;
            }while(arr[j] > arr[l] && j > l);
            if(i<j){
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }else{
                break;
            }
        }
        //交换哨兵位置
        int temp = arr[j];
        arr[j] = arr[l];
        arr[l] = temp;
        //被分成的前后两部分都不足两个元素
        if(j-1 <= l && j+1 >= r){
            if(pos == -1){
                break;
            }else{
                //获取新的排序范围的起始位置
                l = buf[pos][0];
                //获取新的排序范围的末尾位置
                r = buf[pos][1];
                pos--;
            }
        }else{
            if(j-1>l && j+1< r){
                //分割的部分都超过一个元素
                pos++;
                buf[pos][0] = j+1;
                buf[pos][1] = r;
                r = j-1;
            }else if(j+1>=r){
                //只有前半部分超过一个元素
                r = j-1;
            }else{
                //只有后半部分超过一个元素
                l = j+1;
            }
        }
    }

}