一. 快速排序的基本思想算法
快速排序使用分治的思想,經過一趟排序將待排序列分割成兩部分,其中一部分記錄的關鍵字均比另外一部分記錄的關鍵字小。以後分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。數組
二. 快速排序的三個步驟安全
1) 選擇基準:在待排序列中,按照某種方式挑出一個元素,做爲 「基準」(pivot);數據結構
2) 分割操做:以該基準在序列中的實際位置,把序列分紅兩個子序列。此時,在基準左邊的元素都比該基準小,在基準右邊的元素都比基準大;dom
3) 遞歸地對兩個序列進行快速排序,直到序列爲空或者只有一個元素;測試
三. 選擇基準元的方式優化
對於分治算法,當每次劃分時,算法若都能分紅兩個等長的子序列時,那麼分治算法效率會達到最大。也就是說,基準的選擇是很重要的。選擇基準的方式決定了兩個分割後兩個子序列的長度,進而對整個算法的效率產生決定性影響。spa
最理想的方法是,選擇的基準剛好能把待排序序列分紅兩個等長的子序列。pwa
方法一:固定基準元(基本的快速排序)線程
思想:取序列的第一個或最後一個元素做爲基準元。
/// <summary> /// 1.0 固定基準元(基本的快速排序) /// </summary> public static void QsortCommon(int[] arr, int low, int high) { if (low >= high) return; //遞歸出口 int partition = Partition(arr, low, high); //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域 QsortCommon(arr, low, partition - 1); QsortCommon(arr, partition + 1, high); } /// <summary> /// 固定基準元,默認數組第一個數爲基準元,左右分組,返回基準元的下標 /// </summary> public static int Partition(int[] arr, int low, int high) { int first = low; int last = high; int key = arr[low]; //取第一個元素做爲基準元 while (first < last) { while (first < last && arr[last] >= key) last--; arr[first] = arr[last]; while (first < last && arr[first] <= key) first++; arr[last] = arr[first]; } arr[first] = key; //基準元居中 return first; }
注意:基本的快速排序選取第一個或最後一個元素做爲基準。可是,這是一直很很差的處理方法。
測試數據:
測試數據分析:若是輸入序列是隨機的,處理時間能夠接受的。若是數組已經有序時,此時的分割就是一個很是很差的分割。由於每次劃分只能使待排序序列減一,此時爲最壞狀況,快速排序淪爲冒泡排序,時間複雜度爲Θ(n^2)。並且,輸入的數據是有序或部分有序的狀況是至關常見的。所以,使用第一個元素做爲基準元是很是糟糕的,爲了不這個狀況,就引入了下面兩個獲取基準的方法。
方法二:隨機基準元
思想:取待排序列中任意一個元素做爲基準元。
引入的緣由:在待排序列是部分有序時,固定選取基準元使快排效率底下,要緩解這種狀況,就引入了隨機選取基準元。
/// <summary> /// 2.0 隨機基準元 /// </summary> public static void QsortRandom(int[] arr, int low, int high) { if (low >= high) return; //遞歸出口 PartitionRandom(arr, low, high); //隨機基準元 int partition = Partition(arr, low, high); //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域 QsortRandom(arr, low, partition - 1); QsortRandom(arr, partition + 1, high); } /// <summary> /// 隨機基準元,將肯定好的基準元與第一個數交換,無返回值 /// </summary> public static void PartitionRandom(int[] arr, int low, int high) { Random rd = new Random(); int randomIndex = rd.Next() % (high - low) + low;//取數組中隨機下標 Swap(arr, randomIndex, low); //與第一個數交換 }
測試數據:
測試數據分析::這是一種相對安全的策略。因爲基準元的位置是隨機的,那麼產生的分割也不會老是會出現劣質的分割。在整個數組數字全相等時,仍然是最壞狀況,時間複雜度是O(n^2)。實際上,隨機化快速排序獲得理論最壞狀況的可能性僅爲1/(2^n)。因此隨機化快速排序能夠對於絕大多數輸入數據達到O(nlogn)的指望時間複雜度。一位前輩作出了一個精闢的總結:「隨機化快速排序能夠知足一我的一生的人品需求。」
方法三:三數取中
引入的緣由:雖然隨機選取基準時,減小出現很差分割的概率,可是仍是最壞狀況下仍是O(n^2),要緩解這種狀況,就引入了三數取中選取基準。
分析:最佳的劃分是將待排序的序列分紅等長的子序列,最佳的狀態咱們可使用序列的中間的值,也就是第N/2個數。但是,這很難算出來,而且會明顯減慢快速排序的速度。這樣的中值的估計能夠經過隨機選取三個元素並用它們的中值做爲基準元而獲得。事實上,隨機性並無多大的幫助,所以通常的作法是使用左端、右端和中心位置上的三個元素的中值做爲基準元。顯然使用三數中值分割法消除了預排序輸入的很差情形,而且減小快排大約14%的比較次數。
舉例:待排序序列爲:8 1 4 9 6 3 5 2 7 0
左邊爲:8,右邊爲0,中間爲6
咱們這裏取三個數排序後,中間那個數做爲樞軸,則樞軸爲6
注意:在選取中軸值時,能夠從由左中右三個中選取擴大到五個元素中或者更多元素中選取,通常的,會有(2t+1)平均分區法(median-of-(2t+1),三平均分區法英文爲median-of-three。
具體思想:對待排序序列中low、mid、high三個位置上數據進行排序,取他們中間的那個數據做爲基準,並用0下標元素存儲基準。
即:採用三數取中,並用0下標元素存儲基準。
/// <summary> /// 3.0 三數取中 /// </summary> public static void QsortMedianOfThree(int[] arr, int low, int high) { if (low >= high) return; //遞歸出口 PartitionMedianOfThree(arr, low, high); //三數取中 int partition = Partition(arr, low, high); //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域 QsortMedianOfThree(arr, low, partition - 1); QsortMedianOfThree(arr, partition + 1, high); } /// <summary> /// 三數取中肯定基準元,將肯定好的基準元與第一個數交換,無返回值 /// </summary> public static void PartitionMedianOfThree(int[] arr, int low, int high) { int mid = low + (high + -low) / 2; if (arr[mid] > arr[high]) { Swap(arr, mid, high); } if (arr[low] > arr[high]) { Swap(arr, low, high); } if (arr[mid] > arr[low]) { Swap(arr, mid, low); } //將中間大小的數與第一個數交換 }
測試數據:
測試數據分析:使用三數取中優點仍是很明顯的,可是仍是處理不了重複數組。
四. 兩種優化的方法
優化一:當待排序序列的長度分割到必定大小後,使用插入排序
緣由:對於很小和部分有序的數組,快排不如插排好。當待排序序列的長度分割到必定大小後,繼續分割的效率比插入排序要差,此時可使用插排而不是快排。
截止範圍:待排序序列長度N = 10,雖然在5~20之間任一截止範圍都有可能產生相似的結果,這種作法也避免了一些有害的退化情形。
—-摘自《數據結構與算法分析》Mark Allen Weiness 著
/// <summary> /// 4.0 三數取中+插排 /// </summary> public static void QsortThreeInsert(int[] arr, int low, int high) { if (high - low + 1 < 10) { InsertSort(arr, low, high); return; } //插排,遞歸出口 PartitionMedianOfThree(arr, low, high); //三數取中 int partition = Partition(arr, low, high); //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域 QsortMedianOfThree(arr, low, partition - 1); QsortMedianOfThree(arr, partition + 1, high); }
測試數據:
測試數據分析:針對隨機數組,使用三數取中選擇基準+插排,效率仍是能夠提升一點,真是針對已排序的數組,是沒有任何用處的。由於待排序序列是已經有序的,那麼每次劃分只能使待排序序列減一。此時,插排是發揮不了做用的。因此這裏看不到時間的減小。另外,三數取中選擇基準+插排仍是不能處理重複數組。
優化二:在一次分割結束後,能夠把與Key相等的元素聚在一塊兒,繼續下次分割時,不用再對與key相等元素分割
舉例:
待排序序列 1 4 6 7 6 6 7 6 8 6
三數取中選取基準:下標爲4的數6
轉換後,待分割序列:6 4 6 7 1 6 7 6 8 6
基準key:6
本次劃分後,未對與key元素相等處理的結果:1 4 6 6 7 6 7 6 8 6
下次的兩個子序列爲:1 4 6 和 7 6 7 6 8 6
本次劃分後,對與key元素相等處理的結果:1 4 6 6 6 6 6 7 8 7
下次的兩個子序列爲:1 4 和 7 8 7
通過對比,咱們能夠看出,在一次劃分後,把與key相等的元素聚在一塊兒,能減小迭代次數,效率會提升很多
具體過程:在處理過程當中,會有兩個步驟
第一步,在劃分過程當中,把與key相等元素放入數組的兩端
第二步,劃分結束後,把與key相等的元素移到樞軸周圍
/// <summary> /// 5.0 三數取中+插排+彙集相同元素 /// </summary> public static void QsortThreeInsertGather(int[] arr, int low, int high) { if (high - low + 1 < 10) { InsertSort(arr, low, high); return; } //插排,遞歸出口 PartitionMedianOfThree(arr, low, high); //三數取中 //進行左右分組(處理相等元素) int first = low; int last = high; int left = low; int right = high; int leftLength = 0; int rightLength = 0; int key = arr[first]; while (first < last) { while (first < last && arr[last] >= key) { if (arr[last] == key) //處理相等元素,將相等的元素放置數組兩端 { Swap(arr, last, right); right--; rightLength++; } last--; } arr[first] = arr[last]; while (first < last && arr[first] <= key) { if (arr[first] == key) { Swap(arr, first, left); left++; leftLength++; } first++; } arr[last] = arr[first]; } arr[first] = key; //一次快排結束 //把與基準元key相同的元素移到最終位置周圍 int i = first - 1; int j = low; while (j < left && arr[i] != key) { Swap(arr, i, j); i--; j++; } i = last + 1; j = high; while (j > right && arr[i] != key) { Swap(arr, i, j); i++; j--; } QsortThreeInsertGather(arr, low, first - leftLength - 1); QsortThreeInsertGather(arr, first + rightLength + 1, high); }
測試數據:
測試數據分析:三數取中+插排+彙集相等元素的組合,效果居然好的出奇。
緣由:在數組中,若是有相等的元素,那麼就能夠減小很多冗餘的劃分。這點在重複數組中體現特別明顯啊。
其實這裏,插排的做用仍是不怎麼大的。
如下是所有的測試程序源碼:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Diagnostics; using System.Threading; namespace Sort { class Program { static void Main(string[] args) { //開啓10M的堆棧空間的線程 ThreadStart ts = new ThreadStart(Sort.DoQsort); Thread thread = new Thread(ts, 10000000); thread.Start(); } } class Sort { public static void DoQsort() { int[] arr = new int[100000]; //10W個空間大小的數組 //Random rd = new Random(); //for (int i = 0; i < arr.Length; i++) //隨機數組 //{ // arr[i] = rd.Next(); //} //for (int i = 0; i < arr.Length; i++) //升序數組 //{ // arr[i] = i; //} //for (int i = 0; i < arr.Length; i++) //降序數組 //{ // arr[i] = arr.Length - 1 - i; //} for (int i = 0; i < arr.Length; i++) //重複數組 { arr[i] = 5768461; } Stopwatch watch = new Stopwatch(); watch.Start(); //開始計時 //QsortCommon(arr, 0, arr.Length - 1); //固定基準元 //QsortRandom(arr, 0, arr.Length - 1); //隨機基準元 //QsortMedianOfThree(arr, 0, arr.Length - 1); //三數取中 //QsortThreeInsert(arr, 0, arr.Length - 1); //三數取中+插排 QsortThreeInsertGather(arr, 0, arr.Length - 1); //三數取中+插排+彙集相同元素 watch.Stop(); //計時結束 Console.WriteLine(watch.ElapsedMilliseconds.ToString()); } /// <summary> /// 1.0 固定基準元(基本的快速排序) /// </summary> public static void QsortCommon(int[] arr, int low, int high) { if (low >= high) return; //遞歸出口 int partition = Partition(arr, low, high); //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域 QsortCommon(arr, low, partition - 1); QsortCommon(arr, partition + 1, high); } /// <summary> /// 2.0 隨機基準元 /// </summary> public static void QsortRandom(int[] arr, int low, int high) { if (low >= high) return; //遞歸出口 PartitionRandom(arr, low, high); //隨機基準元 int partition = Partition(arr, low, high); //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域 QsortRandom(arr, low, partition - 1); QsortRandom(arr, partition + 1, high); } /// <summary> /// 3.0 三數取中 /// </summary> public static void QsortMedianOfThree(int[] arr, int low, int high) { if (low >= high) return; //遞歸出口 PartitionMedianOfThree(arr, low, high); //三數取中 int partition = Partition(arr, low, high); //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域 QsortMedianOfThree(arr, low, partition - 1); QsortMedianOfThree(arr, partition + 1, high); } /// <summary> /// 4.0 三數取中+插排 /// </summary> public static void QsortThreeInsert(int[] arr, int low, int high) { if (high - low + 1 < 10) { InsertSort(arr, low, high); return; } //插排,遞歸出口 PartitionMedianOfThree(arr, low, high); //三數取中 int partition = Partition(arr, low, high); //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域 QsortMedianOfThree(arr, low, partition - 1); QsortMedianOfThree(arr, partition + 1, high); } /// <summary> /// 5.0 三數取中+插排+彙集相同元素 /// </summary> public static void QsortThreeInsertGather(int[] arr, int low, int high) { if (high - low + 1 < 10) { InsertSort(arr, low, high); return; } //插排,遞歸出口 PartitionMedianOfThree(arr, low, high); //三數取中 //進行左右分組(處理相等元素) int first = low; int last = high; int left = low; int right = high; int leftLength = 0; int rightLength = 0; int key = arr[first]; while (first < last) { while (first < last && arr[last] >= key) { if (arr[last] == key) //處理相等元素 { Swap(arr, last, right); right--; rightLength++; } last--; } arr[first] = arr[last]; while (first < last && arr[first] <= key) { if (arr[first] == key) { Swap(arr, first, left); left++; leftLength++; } first++; } arr[last] = arr[first]; } arr[first] = key; //一次快排結束 //把與基準元key相同的元素移到最終位置周圍 int i = first - 1; int j = low; while (j < left && arr[i] != key) { Swap(arr, i, j); i--; j++; } i = last + 1; j = high; while (j > right && arr[i] != key) { Swap(arr, i, j); i++; j--; } QsortThreeInsertGather(arr, low, first - leftLength - 1); QsortThreeInsertGather(arr, first + rightLength + 1, high); } /// <summary> /// 固定基準元,默認數組第一個數爲基準元,左右分組,返回基準元的下標 /// </summary> public static int Partition(int[] arr, int low, int high) { int first = low; int last = high; int key = arr[low]; //取第一個元素做爲基準元 while (first < last) { while (first < last && arr[last] >= key) last--; arr[first] = arr[last]; while (first < last && arr[first] <= key) first++; arr[last] = arr[first]; } arr[first] = key; //基準元居中 return first; } /// <summary> /// 隨機基準元,將肯定好的基準元與第一個數交換,無返回值 /// </summary> public static void PartitionRandom(int[] arr, int low, int high) { Random rd = new Random(); int randomIndex = rd.Next() % (high - low) + low;//取數組中隨機下標 Swap(arr, randomIndex, low); //與第一個數交換 } /// <summary> /// 三數取中肯定基準元,將肯定好的基準元與第一個數交換,無返回值 /// </summary> public static void PartitionMedianOfThree(int[] arr, int low, int high) { int mid = low + (high + -low) / 2; if (arr[mid] > arr[high]) { Swap(arr, mid, high); } if (arr[low] > arr[high]) { Swap(arr, low, high); } if (arr[mid] > arr[low]) { Swap(arr, mid, low); } //將中間大小的數與第一個數交換 } /// <summary> /// 插入排序 /// </summary> public static void InsertSort(int[] arr, int low, int high) { for (int i = low + 1; i <= high; i++) { if (arr[i] < arr[i - 1]) { for (int j = low; j < i; j++) { if (arr[j] > arr[i]) { Swap(arr, i, j); } } } } } /// <summary> /// 數組交換 /// </summary> public static void Swap(int[] arr, int index1, int index2) { int temp = arr[index1]; arr[index1] = arr[index2]; arr[index2] = temp; } } }