Back
Featured image of post Collections of quickSort

Collections of quickSort

Nothing is true , Everything is permitted

如何理解quickSort?

最近在复习快排,便重新整理了一下,除了classic quickSort 还有变种quickSelectionSort(随机快排),如2151738,还有Dual-Pivot PartitionThree-Way Partition。 不同的partition复用,使其在面对不同情况下有不同的效果

最常见的快排,最坏的情况是

1.已经/几乎有序 → 退化成冒泡排序

2.pivot刚好是最大值/最小值

Hoare quickSort,

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

public int partition(int[] a , int lo , int hi){
    int p = a[lo];
    int i = lo - 1 , j = hi + 1;
    
   while(true){
       while(a[--j] > p);
       while(a[++i] < p);
       
       if(i < j)
           swap(a , i , j);
       else
           return j ;
   }
}

这是改进后的,也是我们现在最常用的版本

    public void quickSort(int[] arr){
        quickSort(arr , 0 , arr.length - 1);
    }
    
    public void quickSort(int[]arr , int first , int last){
        if (last > first){
            int pivotIndex = partition(arr , first , last);
            quickSort(arr , first , pivotIndex - 1);
            quickSort(arr , pivotIndex + 1, last);
        }
    }

    public int partition(int[]arr , int first , int last){
        int pivot = arr[first];
        int low = first + 1;
        int high = last;

        while (high > low){
            //Search left to right
            while (high >= low && arr[low] <= pivot){
                low++;
            }

            //Search right to left
            while (high >= low && arr[high] > pivot){
                high--;
            }

            if (high > low){  //swap the element in the array
                swap(arr , low , high);
            }
        }

        while (high > first && arr[high] >= pivot){
            high--;
        }

        //swap the pivot
        if (pivot > arr[high]){
            swap(arr , first , high)
            return high;
        }
        else {
            return first;
        }
    }
	
	private void swap(int[] arr , int i , int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

随机化

先洗牌后partition , 随机生成pivot , 然而并没有改变最坏情况下的运行时间,只是减少了出现最坏情况的可能,即无法保证每次pivot都是正中间的索引的。

且quickSelectionSort不关心a[ lo…p - 1]a[p + 1 … hi] 是否 有序 ,此方法一般应用于找Kth Element

private int partition(int[] nums, int lo, int hi) {
        if (lo == hi) {
            return lo;
        }
        int pivot = nums[lo];
        // j = hi + 1 因为 while 中会先执行--
        int i = lo, j = hi + 1;

        while (true) {
            // 保证 nums[lo..i] 都小于 pivot
            while (nums[++i] < pivot) {
                if (i == hi) {
                    break;
                }
            }
            // 保证 nums[j..hi] 都大于 pivot
            while (nums[--j] > pivot) {
                if (j == lo) {
                    break;
                }
            }
            if (i >= j) {
                break;
            }

            swap(nums, i, j);
        }

       //交换pivot
        swap(nums, j, lo);
       // 如果走到这里,一定有:
       // nums[i] > pivot && nums[j] < pivot
       // 所以需要交换 nums[i] 和 nums[j],
       // 保证 nums[low..i] < pivot < nums[j..high]
    
        return j;
}

private void shuffle(int[] nums) {//打乱数组
        int n = nums.length;
        Random ran = new Random();
        for (int i = 0; i < n; i++) {
            int r = i + ran.nextInt(n - i);
            swap(nums, i, r);
        }
}

//another ver.
public int randomPartition(int[] a, int l, int r) {
        int i = random.nextInt(r - l + 1) + l;
        swap(a, i, r);
        return partition(a, l, r);
}

Lomuto划分

一个数组被分成三段 ,设最左为pivot , 一段是<a[pivot] , 一段>a[pivot] , 还有一段未处理的数据

从左往右扫描数组,将第三段未处理的数据和p比较 , 其中 s 指向 第一段末尾i 指向 第二段末尾

若val >= a[p] 则 i++ / 若val <= a[p] , 则需要s++ , 并和a[i]互换 ,然后 i++ ,继续指向尾端

直到第三段为空,a[s] 与 a[pivot] 交换

Lomuto
Lomuto

public void quickSort(int[] a , int l , int r){
    if(l < r){
        int p = partition(a , l , r);
        quickSort(a , l , p - 1);
        quickSort(a , p + 1 , r);
    }
}

private int partition(int[] a ,int l, int r) {
        int p = a[l];
        int s = l;
        for (int i = l + 1; i <= r; i++) {
            if (a[i] < p) {
                s = s + 1;
                int temp = a[s];
                a[s] = a[i];
                a[i] = temp;
            }
        }
        int temp = a[l];
        a[l] = a[s];
        a[s] = temp;
        return s;
}

Dual-Pivot Partition

Arrays.sort()就是用该方法实现的

  1. 对于长度小于17的数组使用插入排序(常见优化步骤,传统快排也有应用)。
  2. 选择两个枢纽元p1,p2,一般选择起始元素a[left]和末尾元素a[right](有其他选取方式)。
  3. 假设p1<p2,如果不是就交换。
  4. 将整个数组分成三部分,分别为 <p1的 , p1<X<p2 , >p2 的。
  5. 递归排序这三个部分。

Dual-Pivot
Dual-Pivot

这样进行一次后递归的进行下一次双轴快排,一直到结束,但是在这个执行过程应该去如何处理分析呢?需要几个参数呢?

  • 假设知道排序区间[start,end]。数组为arr, pivot1=arr[start],pivot2=arr[end]
  • 还需要三个参数left,right和k。 l
  • left初始为start,[start,left]区域即为小于等于pivot1小的区域(第一个等于)。
  • right与left对应,初始为end,[right,end]为大于等于pivot2的区域(最后一个等于)。
  • k初始为start+1,是一个从左往右遍历的指针,遍历的数值与pivot1,pivot2比较进行适当交换,当k>=right即可停止。

pre1
pre1

k交换过程然后你可能会问k遍历时候究竟怎么去交换?left和right该如何处理呢?不急我带你慢慢分析,首先K是在left和right中间的,遍历k的位置和pivot1,pivot2进行比较:如果arr[k]<pivot1,那么先++left,然后swap(arr,k,left),**因为初始在start在这个过程不结束start先不动。**然后k++;继续进行

pre2
pre2

而如果arr[k]>pivot2.(区间自行安排即可) 有点区别的就是right可能连续的大于arr[k],比如9 3 3 9 7如果我们需要跳过7前面9到3才能正常交换,这和快排的交换思想一致,当然再具体的实现上就是right–到一个合适比arr[k]小的位置。然后swap(arr,k,right)**切记此时k不能自加。**因为带交换的那个有可能比pivot1还小要和left交换。如果是介于两者之间,k++即可

pre3
pre3

最后则是在执行完这一趟即k=right之后,即开始需要将pivot1和pivot2的数值进行交换

swap(arr, start, left);
swap(arr, end, right);

然后三个区间根据编号递归执行排序函数即可。

pre4
pre4

public void dualPivotQuickSort(int[] arr, int start, int end) {
        if(start > end){
            return;
        }
        if(arr[start] > arr[end])
            swap(arr, start, end);
        int pivot1 = arr[start] , pivot2 =a rr[end];
    	//取最左侧和最右侧作为pivot
        //(start,left]:左侧小于等于pivot1 [right,end)大于pivot2
        int left = start , right = end, k = left + 1;
        while (k < right) {
            //和左侧交换
            if(arr[k] <= pivot1)
            {
                //需要交换
                swap(arr, ++left, k++);
            }
            else if (arr[k]<=pivot2) {//在中间的情况
                k++;
            }
            else {
                while (arr[right]>=pivot2) {//如果全部小于直接跳出外层循环
                    if(right--==k)
                        break ;
                }
                if(k>=right)break ;
                swap(arr, k, right);
            }
        }
        swap(arr, start, left);
        swap(arr, end, right);
    	//分段递归,继续排序
        dualPivotQuickSort(arr, start, left-1);
        dualPivotQuickSort(arr, left+1, right-1);
        dualPivotQuickSort(arr, right+1, end);
    }

private void swap(int arr[],int i,int j){
        int team=arr[i];
        arr[i]=arr[j];
        arr[j]=team;
}

Three-Way Partition , 三路快排 , Dijkstra / Bentley-McIlroy

以最左元素、最右元素、最中间元素为pivot ,在双轴快排的基础上,在分一个等于p的区段

private void sort(Comparable[] arr, int l, int r){
        // 对于小规模数组, 使用插入排序
        if(r - l <= 15){
            InsertionSort.sort(arr, l, r);
            return;
        }
        // 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
        swap(arr, l, (int)(Math.random()*(r-l+1)) + l );
 
        Comparable v = arr[l];
 
        int lt = l;     // arr[l+1...lt] < v
        int gt = r + 1; // arr[gt...r] > v
        int i = l + 1;    // arr[lt+1...i) == v
        while(i < gt){
            if(arr[i].compareTo(v) < 0){
                swap( arr, i, lt + 1);
                i++;
                lt++;
            } else if(arr[i].compareTo(v) > 0){
                swap(arr, i, gt - 1);
                gt--;
            } else{ // arr[i] == v
                i++;
            }
        }
 
        swap(arr, l, lt);
        sort(arr, l, lt-1);
        sort(arr, gt, r);
}
 
private void swap(Object[] arr, int i, int j) {
        Object t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
}

Dijkstra / Bentley-McIlroy的源码 , 在无重复值的情况下,Bentley-McIlroy方法相较于双轴快排, 由于头尾只有少量的等值元素需要交换,所以额外的开销很小。

// Dijkstra 3-way partitioning
private static void sort(Comparable[] a, int lo, int hi) {
    if (hi <= lo) return;
    int lt = lo, gt = hi;
    Comparable v = a[lo];
    int i = lo+1;
    while (i <= gt)
    {
        int cmp = a[i].compareTo(v);
        if      (cmp < 0) exch(a, lt++, i++);
        else if (cmp > 0) exch(a, i, gt--);
        else              i++;
    }
    sort(a, lo, lt - 1);
    sort(a, gt + 1, hi);
}

// Bentley-McIlroy 3-way partitioning
private static void sort(Comparable[] a, int lo, int hi) { 
    int i = lo, j = hi+1;
    int p = lo, q = hi+1;
    Comparable v = a[lo];
    while (true) {
        while (less(a[++i], v))
            if (i == hi) break;
        while (less(v, a[--j]))
            if (j == lo) break;

        // pointers cross
        if (i == j && eq(a[i], v))
            exch(a, ++p, i);
        if (i >= j) break;

        exch(a, i, j);
        if (eq(a[i], v)) exch(a, ++p, i);
        if (eq(a[j], v)) exch(a, --q, j);
    }

    i = j + 1;
    //多了交换的步骤
    for (int k = lo; k <= p; k++)
        exch(a, k, j--);
    for (int k = hi; k >= q; k--)
        exch(a, k, i++);
}
Welcome to the world of Minezeratul