快速排序及優化

原文地址java

快速排序

原理

快速排序是C.R.A.Hoare提出的一種交換排序。它採用分治的策略,因此也稱其爲分治排序。算法

實現快速排序算法的關鍵在於,先在數組中選一個數做爲基數,接着以基數爲中心將數組中的數字分爲兩部分,比基數小的放在數組的左邊,比基數大的放到數組的右邊。接下來咱們能夠用遞歸的思想分別對基數的左右兩邊進行排序。windows

整個快速排序能夠總結爲如下三步:數組

  1. 從數組中取出一個數做爲基數
  2. 分區,將數組分爲兩部分,比基數小的放在數組的左邊,比基數大的放到數組的右邊。
  3. 遞歸使左右區間重複第二步,直至各個區間只有一個數。

樣例分析

下面我用博客專家MoreWindows挖坑填數+分治思想來分析一個實例(使用快速排序將如下數組排序)性能

圖一.png

首先,取數組中的第一個數字52爲第一次分區的基數ui

圖二.png

初始時,start=0、end=八、base=52(start區間的頭、end區間的尾、base基數)spa

因爲將array[0]中的數據賦給了基數base,咱們能夠理解爲在array[0]處挖了一個坑,固然這個坑是能夠用其它數據來填充的。.net

接着咱們從end開始 向前找比base小或者等於base的數據,很快咱們就找了19,咱們就19這個數據填充到array[0]位置,這樣array[8]位置就又造成了一個新的坑。不怕,咱們接着找符合條件的數據來填充這個新坑。 start=0、end=八、base=523d

圖三.png

而後咱們再從start開始向後找大於或等於base的數據,找到了88,咱們用88來填充array[8],這樣array[1]位置也造成了一個新坑。 Don't Worry 咱們接着從end開始 向前找符合條件的數據來填充這個坑。 start=一、end=八、base=52code

圖四.png

須要注意的是start和end隨着查找的過程是不斷變化的。

以後一直重複上面的步驟,先從前向後找,再從後向前找。直至全部小於基數的數據都出如今左邊,大於基數的數據都出如今右邊。

圖五.png

最後將base中的數據填充到數組中便可完成此次分區的過程。

圖六.png

而後遞歸使左右區間重複分區過程,直至各個區間只有一個數,便可完成整個排序過程。

快速排序的核心是以基數爲中心,將數組分爲兩個區間,小於基數的放到基數的左邊,大於基數的放到基數的右邊。

在第一次分區的時候咱們以數組中的第一個數據爲基數,在array[0]處挖了一個坑,這時咱們只能從區間的後面往前找小於基數的數據來填充array[0]這個坑,若是直接從前日後找符合條件的數據,就會形成大於基數的數據留在了數組的左邊。

因此咱們只能經過從後往前找小於基數的數據來填充前面的坑,從前日後找大於基數的數據來填充後面的坑。這樣能夠保證在只遍歷一遍數組就能將數組分爲兩個區間

最後一步咱們用基數自己來填充數組的最後一個坑

代碼實現:

// 分區
	public static int partition(int[] array, int start, int end) {
		int base = array[start];
		while (start < end) {
			while (start < end && array[end] >= base)
				end --;
			array[start] = array[end];
			while (start < end && array[start] <= base)
				start ++;
			array[end] = array[start];
		}
		array[end] = base;
		return start;
	}
	
	// 快速排序
	public static void quickSort(int[] array,int start, int end) {
		if(start < end) {
			int index = partition(array, start, end);
			quickSort(array, start, index-1);
			quickSort(array, index+1, end);
		}
	}
複製代碼
class QuickSort {
public:

	//快速排序
    int* quickSort(int* A, int n) {
		QSort(A,0,n-1);
		return A;
    }
	//對數組A[low,...,high]
	void QSort(int *A,int low,int high) {
		if(low<high) {
			int n;
			n=Partition(A,low,high);
			QSort(A,low,n-1);
			QSort(A,n+1,high);
		}
	}
	
	//交換數組A[low,...,high]的記錄,支點記錄到位,並返回其所在位置,此時
	//在它以前(後)的記錄均不大(小)於它
	int Partition(int *A,int low,int high) {
		int key=A[low];
		while(low<high) {
			while(low<high&&A[high]>=key)
				high--;
			A[low]=A[high];
			while(low<high&&A[low]<=key)
				low++;
			A[high]=A[low];
		}
		A[low]=key;
		return low;
	}
};
複製代碼

算法分析:

當數據有序時,以第一個關鍵字爲基準分爲兩個子序列,前一個子序列爲空,此時執行效率最差。時間複雜度爲O(n^2) 而當數據隨機分佈時,以第一個關鍵字爲基準分爲兩個子序列,兩個子序列的元素個數接近相等,此時執行效率最好。時間複雜度爲O(nlogn) 因此,數據越隨機分佈時,快速排序性能越好;數據越接近有序,快速排序性能越差。

快速排序在每次「挖坑」的過程當中,須要 1 個空間存儲基數。而快速排序的大概須要 NlogN次的處理,因此佔用空間也是 NlogN 個。

下面咱們在來看幾種改進的快排算法

快速排序中元素切分的方式

快速排序中最重要的步驟就是將小於等於中軸元素的整數放到中軸元素的左邊,將大於中軸元素的數據放到中軸元素的右邊,這裏咱們把該步驟定義爲'切分'。以首元素做爲中軸元素,下面介紹幾種常見的'切分方式'。

兩端掃描,一端挖坑,另外一端填補

這種方法就是咱們上面用到的'挖坑填數',在這再作個簡單的總結。

**這種方法的基本思想是:**使用兩個變量i和j,i指向最左邊的元素,j指向最右邊的元素,咱們將首元素做爲中軸,將首元素複製到變量pivot中,這時咱們能夠將首元素i所在的位置當作一個坑,咱們從j的位置從右向左掃描,找一個小於等於中軸的元素A[j],來填補A[i]這個坑,填補完成後,拿去填坑的元素所在的位置j又能夠看作一個坑,這時咱們在以i的位置從前日後找一個大於中軸的元素來填補A[j]這個新的坑,如此往復,直到i和j相遇(i == j,此時i和j指向同一個坑)。最後咱們將中軸元素放到這個坑中。最後對左半數組和右半數組重複上述操做。

這種方法的代碼實現請參考上面的完整代碼。

從兩端掃描交換

**這種方法的基本思想是:**使用兩個變量i和j,i指向首元素的元素下一個元素(最左邊的首元素爲中軸元素),j指向最後一個元素,咱們從前日後找,直到找到一個比中軸元素大的,而後從後往前找,直到找到一個比中軸元素小的,而後交換這兩個元素,直到這兩個變量交錯(i > j)(注意不是相遇 i == j,由於相遇的元素還未和中軸元素比較)。最後對左半數組和右半數組重複上述操做。

public static void QuickSort1(int[] A, int L, int R){
    if(L < R){//遞歸的邊界條件,當 L == R時數組的元素個數爲1個
        int pivot = A[L];//最左邊的元素做爲中軸,L表示left, R表示right
        int i = L+1, j = R;
        //當i == j時,i和j同時指向的元素尚未與中軸元素判斷,
        //小於等於中軸元素,i++,大於中軸元素j--,
        //當循環結束時,必定有i = j+1, 且i指向的元素大於中軸,j指向的元素小於等於中軸
        while(i <= j){
            while(i <= j && A[i] <= pivot){
                i++;
            }
            while(i <= j && A[j] > pivot){
                j--;
            }
            //當 i > j 時整個切分過程就應該中止了,不能進行交換操做
            //這個能夠改爲 i < j, 這裏 i 永遠不會等於j, 由於有上述兩個循環的做用
            if(i <= j){
                Swap(A, i, j);
                i++;
                j--;
            }
        }
        //當循環結束時,j指向的元素是最後一個(從左邊算起)小於等於中軸的元素
        Swap(A, L, j);//將中軸元素和j所指的元素互換
        QuickSort1(A, L, j-1);//遞歸左半部分
        QuickSort1(A, j+1, R);//遞歸右半部分
    }
}
複製代碼

從一端掃描

和前面兩種方法同樣,咱們選取最左邊的元素做爲中軸元素,A[1,i]表示小於等於pivot的部分,i指向中軸元素(i < 1),表示小於等於pivot的元素個數爲0,j之後的都是未知元素(即不知道比pivot大,仍是比中軸元素小),j初始化指向第一個未知元素。

1.png

當A[j]大於pivot時,j繼續向前,此時大於pivot的部分就增長一個元素

2.png

當A[j]小於等於pivot時,咱們注意i的位置,i的下一個就是大於pivot的元素,咱們將i增長1而後交換A[i]和A[j],交換後小於等於pivot的部分增長1,j增長1,繼續掃描下一個。而i的下一個元素仍然大於pivot,又回到了先前的狀態。

3.png

public static void QuickSort3(int[] A, int L, int R){
    if(L < R){
        int pivot = A[L];//最左邊的元素做爲中軸元素
        //初始化時小於等於pivot的部分,元素個數爲0
        //大於pivot的部分,元素個數也爲0
        int i = L, j = L+1;
        while(j <= R){
            if(A[j] <= pivot){
                i++;
                Swap(A, i, j);
                j++;//j繼續向前,掃描下一個
            }else{
                j++;//大於pivot的元素增長一個
            }
        }
        //A[i]及A[i]之前的都小於等於pivot
        //循環結束後A[i+1]及它之後的都大於pivot
        //因此交換A[L]和A[i],這樣咱們就將中軸元素放到了適當的位置
        Swap(A, L, i);
        QuickSort3(A, L, i-1);
        QuickSort3(A, i+1, R);
    }
}
複製代碼

快速排序的改進方法

三向切分的快速排序

三向切分快速排序的基本思想,用i,j,k三個將數組切分紅四部分,a[0, i-1]表示小於pivot的部分,a[i, k-1]表示等於pivot的部分,a[j+1]以後的表示大於pivot的部分,而a[k, j]表示未進行比較的元素(即不知道比pivot大,仍是比pivot元素小)。咱們要注意a[i]始終位於等於pivot部分的第一個元素,a[i]的左邊是小於pivot的部分。

4.png

咱們依舊選取最左邊的元素做爲中軸元素,初始化時,i = L,k = L+1,j=R(L表示最左邊元素的索引,R表示最右邊元素的索引)

5.png

經過上一段的表述可知,初始化時<pivot部分的元素個數爲0,等於pivot部分元素的個數爲1,大於pivot部分的元素個數爲0,這顯然符合目前咱們對所掌握的狀況。k自左向右掃描直到k與j錯過爲止(k > j)。

這裏咱們掃描的目的就是爲了逐個減小未知元素,並將每一個元素按照和pivot的大小關係放到不一樣的區間上去。

在k的掃描過程當中咱們能夠對a[k]分爲三種狀況討論

  1. a[k] < pivot 交換a[i]和a[k],而後i和k都自增1,k繼續掃描

  2. a[k] = pivot k自增1,k接着繼續掃描

  3. a[k] > pivot 這個時候顯然a[k]應該放到最右端,大於pivot的部分。可是咱們不能直接將a[k]與a[j]交換,由於目前a[j]和pivot的關係未知,因此咱們這個時候應該從j的位置自右向左掃描。而a[j]與pivot的關係能夠繼續分爲三種狀況討論

    • a[j] > pivot --- j自減1,j接着繼續掃描
    • a[j] = pivot --- 交換a[k]和a[j],k自增1,j自減1,k繼續掃描(注意此時j的掃描就結束了)
    • a[j] < pivot --- 此時咱們注意到a[j] < pivot, a[k] > pivot, a[i] == pivot,那麼咱們只須要將a[j]放到a[i]上,a[k]放到a[j]上,而a[i]放到a[k]上。而後i和k自增1,j自減1,k繼續掃描(注意此時j的掃描就結束了)

注意,當掃描結束時,i和j的表示了=等於pivot部分的起始位置和結束位置。咱們只須要對小於pivot的部分以及大於pivot的部分重複上述操做便可。

6.png

public static void QuickSort3Way(int[] A, int L, int R){
    if(L >= R){//遞歸終止條件,少於等於一個元素的數組已有序
        return;
    }
     
    int i,j,k,pivot;
    pivot = A[L]; //首元素做爲中軸
    i = L;
    k = L+1;
    j = R;
     
    OUT_LOOP:
    while(k <= j){
        if(A[k] < pivot){
            Swap(A, i, k);
            i++;
            k++;
        }else
        if(A[k] == pivot){
            k++;
        }else{// 遇到A[k]>pivot的狀況,j從右向左掃描
            while(A[j] > pivot){//A[j]>pivot的狀況,j繼續向左掃描
                j--;
                if(j < k){
                    break OUT_LOOP;
                }
            }
            if(A[j] == pivot){//A[j]==pivot的狀況
                Swap(A, k, j);
                k++;
                j--;
            }else{//A[j]<pivot的狀況
                Swap(A, i, j);
                Swap(A, j, k);
                i++;
                k++;
                j--;
            }
        }
    }
    //A[i, j] 等於 pivot 且位置固定,不須要參與排序
    QuickSort3Way(A, L, i-1); // 對小於pivot的部分進行遞歸
    QuickSort3Way(A, j+1, R); // 對大於pivot的部分進行遞歸
}
複製代碼

雙軸快速排序

雙軸快速排序算法的思路和上面的三向切分快速排序算法的思路基本一致,雙軸快速排序算法使用兩個軸元素,一般選取最左邊的元素做爲pivot1和最右邊的元素做pivot2。首先要比較這兩個軸的大小,若是pivot1 > pivot2,則交換最左邊的元素和最右邊的元素,已保證pivot1 <= pivot2。雙軸快速排序一樣使用i,j,k三個變量將數組分紅四部分

7.png

A[L+1, i]是小於pivot1的部分,A[i+1, k-1]是大於等於pivot1且小於等於pivot2的部分,A[j, R]是大於pivot2的部分,而A[k, j-1]是未知部分。和三向切分的快速排序算法同樣,初始化i = L,k = L+1,j=R,k自左向右掃描直到k與j相交爲止(k == j)。咱們掃描的目的就是逐個減小未知元素,並將每一個元素按照和pivot1和pivot2的大小關係放到不一樣的區間上去。

在k的掃描過程當中咱們能夠對a[k]分爲三種狀況討論(注意咱們始終保持最左邊和最右邊的元素,即雙軸,不發生交換)

  1. a[k] < pivot1 i先自增,交換a[i]和a[k],k自增1,k接着繼續掃描

  2. a[k] >= pivot1 && a[k] <= pivot2 k自增1,k接着繼續掃描

  3. a[k] > pivot2: 這個時候顯然a[k]應該放到最右端大於pivot2的部分。但此時,咱們不能直接將a[k]與j的下一個位置a[--j]交換(能夠認爲A[j]與pivot1和pivot2的大小關係在上一次j自右向左的掃描過程當中就已經肯定了,這樣作主要是j首次掃描時避免pivot2參與其中),由於目前a[--j]和pivot1以及pivot2的關係未知,因此咱們這個時候應該從j的下一個位置(--j)自右向左掃描。而a[--j]與pivot1和pivot2的關係能夠繼續分爲三種狀況討論

    • a[--j] > pivot2 j接着繼續掃描
    • a[--j] >= pivot1且a[j] <= pivot2 交換a[k]和a[j],k自增1,k繼續掃描(注意此時j的掃描就結束了)
    • a[--j] < pivot1 先將i自增1,此時咱們注意到a[j] < pivot1, a[k] > pivot2, pivot1 <= a[i] <=pivot2,那麼咱們只須要將a[j]放到a[i]上,a[k]放到a[j]上,而a[i]放到a[k]上。k自增1,而後k繼續掃描(此時j的掃描就結束了)

8.png

注意

  1. pivot1和pivot2在始終不參與k,j掃描過程。
  2. 掃描結束時,A[i]表示了小於pivot1部分的最後一個元素,A[j]表示了大於pivot2的第一個元素,這時咱們只須要交換pivot1(即A[L])和A[i],交換pivot2(即A[R])與A[j],同時咱們能夠肯定A[i]和A[j]所在的位置在後續的排序過程當中不會發生變化(這一步很是重要,不然可能引發無限遞歸致使的棧溢出),最後咱們只須要對A[L, i-1],A[i+1, j-1],A[j+1, R]這三個部分繼續遞歸上述操做便可。

9.png

public static void QuickSortDualPivot(int[] A, int L, int R){
    if(L >= R){
        return;
    }
     
    if(A[L] > A[R]){
        Swap(A, L, R); //保證pivot1 <= pivot2
    }
     
    int pivot1 = A[L];
    int pivot2 = A[R];
     
    //若是這樣初始化 i = L+1, k = L+1, j = R-1,也能夠
    //但代碼中邊界條件, i,j先增減,循環截止條件,遞歸區間的邊界都要發生相應的改變
    int i = L;
    int k = L+1;
    int j = R;
 
    OUT_LOOP:
    while(k < j){
        if(A[k] < pivot1){
            i++;//i先增長,首次運行pivot1就不會發生改變
            Swap(A, i, k);
            k++;
        }else
        if(A[k] >= pivot1 && A[k] <= pivot2){
            k++;
        }else{
            while(A[--j] > pivot2){//j先增減,首次運行pivot2就不會發生改變
                if(j <= k){//當k和j相遇
                    break OUT_LOOP;
                }
            }
            if(A[j] >= pivot1 && A[j] <= pivot2){
                Swap(A, k, j);
                k++;
            }else{
                i++;
                Swap(A, j, k);
                Swap(A, i, k);
                k++;
            }
        }
    }
    Swap(A, L, i);//將pivot1交換到適當位置
    Swap(A, R, j);//將pivot2交換到適當位置
     
    //一次雙軸切分至少肯定兩個元素的位置,這兩個元素將整個數組區間分紅三份
    QuickSortDualPivot(A, L, i-1);
    QuickSortDualPivot(A, i+1, j-1);
    QuickSortDualPivot(A, j+1, R);
}
複製代碼

下面代碼初始化方式使得邊界條件更加容易肯定,在下面代碼的方式中A[L+1]~A[i-1]表示小於pivot1的部分,A[i]~A[j]表示兩軸之間的部分,A[j]~A[R-1]表示大於pivot2的部分。

當排序數組中不存在pivot1~pivot2中的部分時,一趟交換完成後i剛好比j大1;當排序數組中僅僅存在一個元素x使得pivot1 <=x <=pivot2時,一趟交換完成,i和j剛好相等。

public static void QuickSortDualPivot(int[] A, int L, int R){
     
    if(L >= R){
        return;
    }
     
    if(A[L] > A[R]){
        Swap(A, L, R); //保證pivot1 <= pivot2
    }
     
    int pivot1 = A[L];
    int pivot2 = A[R];
     
    int i = L+1;
    int k = L+1;
    int j = R-1;
 
    OUT_LOOP:
    while(k <= j){
        if(A[k] < pivot1){
            Swap(A, i, k);
            k++;
            i++;
        }else
        if(A[k] >= pivot1 && A[k] <= pivot2){
            k++;
        }else{
            while(A[j] > pivot2){
                j--;
                if(j < k){//當k和j錯過
                    break OUT_LOOP;
                }
            }
            if(A[j] >= pivot1 && A[j] <= pivot2){
                Swap(A, k, j);
                k++;
                j--;
            }else{//A[j] < pivot1
                Swap(A, j, k);//注意k不動
                j--;
            }
        }
    }
    i--;
    j++;
    Swap(A, L, i);//將pivot1交換到適當位置
    Swap(A, R, j);//將pivot2交換到適當位置
     
    //一次雙軸切分至少肯定兩個元素的位置,這兩個元素將整個數組區間分紅三份
    QuickSortDualPivot(A, L, i-1);
    QuickSortDualPivot(A, i+1, j-1);
    QuickSortDualPivot(A, j+1, R);
}
複製代碼
相關文章
相關標籤/搜索