1. 程式人生 > >高階排序-快速排序詳解

高階排序-快速排序詳解

輔助工具

概述

快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。

簡單來說, 就是設定一個標準V, 每次遍歷就將整個陣列分成小於V和大於V的兩部分, 這時,V就處於它在排好序的陣列中應該在的位置, 然後再對左右兩部分分別進行遞迴排序, 這個將陣列分割成兩部分的操作, 稱之為partition, 下面看下程式碼實現 :

程式碼實現

package sort.quick;
import sort.Sort ; import utils.ArrayUtil ; public class QuickSort implements Sort { @Override public void sort(int [] arr) { quickSort(arr, 0, arr.length - 1); } /** * 對陣列arr的[l, r]前閉後閉的區間進行快速排序 * 遞迴方法 * @param arr * @param l * @param r */ private void quickSort(int [] arr, int
l, int r){ // 遞迴終止條件 if (l >= r) { return; } int p = partition(arr, l, r); quickSort(arr, l, p - 1); quickSort(arr, p + 1, r); } /** * 對arr陣列的前閉後閉區間[l, r]進行partition操作 * @param arr * @param l * @param r * @return 返回分割後作為標記元素的下標位置 */ private int partition(int [] arr,
int l, int r){ // 設定用於對比的元素為arr[l] int v = arr[l]; // arr[l + 1, j] <v; arr[j+1, i)>v int j = l; for (int i = l; i <= r; i++) { if (arr[i] < v) { ArrayUtil.swap(arr, i, j + 1); j++; } } ArrayUtil.swap(arr, l, j); return j; } }

下面我們就可以測試一下快速排序和歸併排序的效能之間的差異, 測試程式碼如下 :

	/**
	 * 測試插入排序和歸併排序效能
	 */
	@Test
	public void testSort(){
		// 生成一個隨機陣列
		int[] arr = ArrayUtil.generateArray(1000000, 0, 1000000);
		int[] arr1 = ArrayUtil.copyArray(arr);
		int[] arr2 = ArrayUtil.copyArray(arr);
		int[] arr3 = ArrayUtil.copyArray(arr);
		int[] arr4 = ArrayUtil.copyArray(arr);
		System.out.println("----------------------------------隨機陣列----------------------------------") ;
		System.out.println("歸併排序1 : " + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
		System.out.println("歸併排序2 : " + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
		System.out.println("歸併排序3 : " + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
		System.out.println("自底向上的歸併排序 : " + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
		System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;
		/*
		 * 生成一個近乎有序的陣列
		 * 100000 : 陣列元素個數
		 * 10 : 在一個完全有序的陣列上進行多少次元素交換
		 */
        arr = ArrayUtil.generateNearlyOrderedArray(1000000, 10);
		arr1 = ArrayUtil.copyArray(arr);
		arr2 = ArrayUtil.copyArray(arr);
		arr3 = ArrayUtil.copyArray(arr);
		arr4 = ArrayUtil.copyArray(arr);
		System.out.println("------------------------------近乎有序的陣列------------------------------") ;
		System.out.println("歸併排序1:" + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
		System.out.println("歸併排序2:" + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
		System.out.println("歸併排序3:" + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
		System.out.println("自底向上的歸併排序:" + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
		System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;


	      /*
         * 生成一個存在大量重複元素的陣列
         * 100000 : 陣列元素個數
         */
        arr = ArrayUtil.generateArray(1000000, 0,  100);
        arr1 = ArrayUtil.copyArray(arr);
        arr2 = ArrayUtil.copyArray(arr);
        arr3 = ArrayUtil.copyArray(arr);
        arr4 = ArrayUtil.copyArray(arr);
        System.out.println("------------------------------存在大量重複元素的陣列------------------------------") ;
        System.out.println("歸併排序1:" + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
        System.out.println("歸併排序2:" + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
        System.out.println("歸併排序3:" + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
        System.out.println("自底向上的歸併排序:" + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
        System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;

	}

測試結果如下 :

----------------------------------隨機陣列---------------------------------- 歸併排序1 : 0.205s 歸併排序2 : 0.148s 歸併排序3 : 0.154s 自底向上的歸併排序 : 0.162s 快速排序 : 0.109s ------------------------------近乎有序的陣列------------------------------ 歸併排序1:0.082s 歸併排序2:0.047s 歸併排序3:0.016s 自底向上的歸併排序:0.02s 快速排序 : 60.363s ------------------------------存在大量重複元素的陣列------------------------------ 歸併排序1:0.154s 歸併排序2:0.114s 歸併排序3:0.09s 自底向上的歸併排序:0.09s 快速排序 : 3.401s

對於100萬的資料量, 從上面的測試結果來說, 可以得到下面的結論 :

  • 對於隨機陣列來說, 快速排序的效能已經高於了歸併排序了
  • 對於近乎有序的陣列, 由於這個版本的在partition的過程中每次都取第一個元素作為標準, 這會導致partition分割的兩部分嚴重不均衡, 在極端情況下即陣列完全有序的情況下, 會退化成O(n²)級別的演算法, 所以這個排序花了60多秒, 這個時間顯然是不能接受的, 下面會進行優化
  • 對於存在大量重複元素的陣列, 上面的程式碼其實是分成小於等於V和大於V的兩部分, 一樣是會造成partition的不平衡, 下面是對這些問題的優化方案

需要注意的一點是, 在對近乎有序的陣列使用這一版的歸併排序時, 會因為遞迴深度較深而佔用大量棧空間, 如果報StackOVerflow的異常, 可以使用引數-Xss將棧空間設定大一點, 我這裡設定了-Xss128m是沒有問題的

排序優化

優化1

在上一篇歸併排序的文章中就說過, 對於資料量較小的陣列, 使用插入排序的效能反而更快, 所以可以在partition到底層資料量較小的時候, 使用插入排序替換快速排序, 進行第一次優化 :

package sort.quick;

import sort.Sort ;
import utils.ArrayUtil ;


/**
 * 快速排序優化
 * @author xuxiumeng
 *
 */
public class QuickSort2 implements Sort {
    
    @Override
    public void sort(int [] arr) {
        quickSort(arr, 0, arr.length - 1);
    }
    
    /**
     * 對陣列arr的[l, r]前閉後閉的區間進行快速排序
     * 遞迴方法
     * @param arr
     * @param l
     * @param r
     */
    private void quickSort(int [] arr, int l, int r){
        // 遞迴終止條件
        if ( r - l < 16) {
            ArrayUtil.insertSort(arr, l, r);
            return;
        }
        
        int p = partition(arr, l, r);
        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);
    }
    
    /**
     * 對arr陣列的前閉後閉區間[l, r]進行partition操作
     * @param arr
     * @param l
     * @param r
     * @return 返回分割後作為標記元素的下標位置
     */
    private int partition(int [] arr, int l, int r){
        // 設定用於對比的元素為arr[l]
                int v = arr[l];
                // arr[l + 1, j] <v; arr[j+1, i)>v
                int j = l;
                for (int i = l; i <= r; i++) {
                    if (arr[i] < v) {
                        ArrayUtil.swap(arr, i, j + 1);
                        j++;
                    }
                }
                ArrayUtil.swap(arr, l, j);
        return j;
    }
    
}

測試程式碼 :

	/**
	 * 測試插入排序和歸併排序效能
	 */
	@Test
	public void testSort(){
		// 生成一個隨機陣列
		int[] arr = ArrayUtil.generateArray(1000000, 0, 1000000);
		int[] arr1 = ArrayUtil.copyArray(arr);
		int[] arr2 = ArrayUtil.copyArray(arr);
		int[] arr3 = ArrayUtil.copyArray(arr);
		int[] arr4 = ArrayUtil.copyArray(arr);
		int[] arr5 = ArrayUtil.copyArray(arr);
		System.out.println("----------------------------------隨機陣列----------------------------------") ;
		System.out.println("歸併排序1 : " + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
		System.out.println("歸併排序2 : " + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
		System.out.println("歸併排序3 : " + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
		System.out.println("自底向上的歸併排序 : " + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
		System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;
        System.out.println("快速排序2 : " + ArrayUtil.testSort(arr5, new QuickSort2()) + "s") ;

		/*
		 * 生成一個近乎有序的陣列
		 * 100000 : 陣列元素個數
		 * 10 : 在一個完全有序的陣列上進行多少次元素交換
		 */
        arr = ArrayUtil.generateNearlyOrderedArray(1000000, 10);
		arr1 = ArrayUtil.copyArray(arr);
		arr2 = ArrayUtil.copyArray(arr);
		arr3 = ArrayUtil.copyArray(arr);
		arr4 = ArrayUtil.copyArray(arr);
	    arr5 = ArrayUtil.copyArray(arr);
		System.out.println("------------------------------近乎有序的陣列------------------------------") ;
		System.out.println("歸併排序1:" + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
		System.out.println("歸併排序2:" + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
		System.out.println("歸併排序3:" + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
		System.out.println("自底向上的歸併排序:" + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
		System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;
        System.out.println("快速排序2 : " + ArrayUtil.testSort(arr5, new QuickSort2()) + "s") ;


	      /*
         * 生成一個存在大量重複元素的陣列
         * 100000 : 陣列元素個數
         */
        arr = ArrayUtil.generateArray(1000000, 0,  100);
        arr1 = ArrayUtil.copyArray(arr);
        arr2 = ArrayUtil.copyArray(arr);
        arr3 = ArrayUtil.copyArray(arr);
        arr4 = ArrayUtil.copyArray(arr);
        arr5 = ArrayUtil.copyArray(arr);
        System.out.println("------------------------------存在大量重複元素的陣列------------------------------") ;
        System.out.println("歸併排序1:" + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
        System.out.println("歸併排序2:" + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
        System.out.println("歸併排序3:" + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
        System.out.println("自底向上的歸併排序:" + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
        System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;
        System.out.println("快速排序2 : " + ArrayUtil.testSort(arr5, new QuickSort2()) + "s") ;


	}

測試結果 :

----------------------------------隨機陣列---------------------------------- 歸併排序1 : 0.191s 歸併排序2 : 0.143s 歸併排序3 : 0.156s 自底向上的歸併排序 : 0.16s 快速排序 : 0.109s 快速排序2 : 0.102s ------------------------------近乎有序的陣列------------------------------ 歸併排序1:0.082s 歸併排序2:0.047s 歸併排序3:0.014s 自底向上的歸併排序:0.021s 快速排序 : 134.718s 快速排序2 : 133.616s ------------------------------存在大量重複元素的陣列------------------------------ 歸併排序1:0.183s 歸併排序2:0.109s 歸併排序3:0.086s 自底向上的歸併排序:0.09s 快速排序 : 3.495s 快速排序2 : 3.481s

從測試結果看出, 這一版的快速排序比上一版的快速排序效能上要稍微好一點, 但是並沒有借據近乎有序的陣列, 和存在大量重複元素的陣列效能較差的問題, 所以還需要繼續優化

優化2

因為陣列可能是近乎有序的陣列, 所以我們可以不每次取隊首的元素, 一種方案是每次隨機取一個元素, 一種方案是取隊首, 中間, 隊尾三個元素中取大小排在中間的那個, 這裡我們就使用隨機元素的方案, 程式碼如下:

package sort.quick;

import java.util.Random;

import sort.Sort ;
import utils.ArrayUtil ;


/**
 * 快速排序優化
 * 上面兩個版本的快速排序, 在處理近乎有序的陣列的時候,效能很差, 
 * 因為每次partition時都取隊首元素, 在陣列近乎有序的時候, 會導致partition的兩部分很不均勻
 * 在陣列完全有序的情況下, 快速排序甚至會退化成O(n²)演算法
 * 優化方案 : 每次隨機取一個元素, 而不是每次取第一個元素
 * @author xuxiumeng
 *
 */
public class QuickSort3 implements Sort {
  private static Random random = new Random();
  
    @Override
    public void sort(int [] arr) {
        quickSort(arr, 0, arr.length - 1);
    }
    
    /**
     * 對陣列arr的[l, r]前閉後閉的區間進行快速排序
     * 遞迴方法
     * @param arr
     * @param l
     * @param r
     */
    private void quickSort(int [] arr, int l, int r){
        // 遞迴終止條件
        if ( r - l < 16) {
            ArrayUtil.insertSort(arr, l, r);
            return;
        }
        
        int p = partition(arr, l, r);
        quickSort(arr, l, p - 1);
        quickSort(arr, p +