一、原理
- 從區間中取一個數據作爲基準值,按照基準值將區間劃分爲左右兩部分,其中左半部分的數據 < 基準值,右半部分的數據>基準值;
- 按照快排的思想排左半部分;
- 按快排的思想排右半部分;
類似於二叉樹前序遍歷的框架:
public static void quickSort(int[] arr,int left, int right){
if(right-left > 1){
//按基準值對[left,right)區間進行分割
int key = partion2(arr,left,right);
//遞歸基準值左半側和右半側
quickSort(arr,left,key);
quickSort(arr,key+1,right);
}
}
二、如何進行劃分?
下面我會講三種方法來進行劃分。
- 交換的方法(每個劃分都會用到,所以寫在最前)
public static void swap(int[]arr,int left, int right){
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
- 方法一:進行數據劃分
標註:key=arr[length-1]
1、設置兩個索引 begin 和 end;
2、 begin 從前往後找,找比 key 大的數,找到後停止;
3、end 從後往前找,找比 key 小的數,找到後停止;
4、begin 位置上的元素與 end 位置上的元素進行交換;
5、最後再將 key 與 begin 位置數據進行交換(如果所指元素位置就是 key , 就不需要交換了)
public static int partion(int[]arr,int left, int right){
int begin = left;
int end = right-1;
int mid = getIndexOfMiddle(arr,left,right);//優化(後面會講到)
swap(arr,mid,right);
int key = arr[end];
while (begin<end){
//begin
while (begin<end && arr[begin]<=key){
begin++;
}
//end
while (begin<end && arr[end]>=key){
end--;
}
if(begin<end){
swap(arr,begin,end);
}
}
if(begin!=right-1){
swap(arr,begin,key);
}
return begin;
}
- 方法二:“挖坑法”
標註:key=arr[length-1]
1、設置兩個索引 begin 和 end;
2、 begin 從前往後找,找比 key 大的數,找到後停止;【8的位置】
3、begin 去填坑 【8 → 5】,end向前走一步;
4、begin的位置則爲新的坑【8】;
5、end 從後往前走,找到比 key 小的元素,找到後填坑;
6、找到的位置又爲新的坑,再從 begin 開始向後找,以此類推;
7、用key填最後一個坑。
public static int partion2(int[]arr,int left, int right){
int begin = left;
int end = right-1;
int mid = getIndexOfMiddle(arr,left,right);//優化(後面會講到)
swap(arr,mid,right);
int key = arr[end];
while (begin<end){
//begin從前往後找,找大於Key的
while (begin<end && arr[begin] <= key){
begin++;
}
//找到大於key的,用該元素填end位置的坑
if(begin<end){
arr[end] = arr[begin];
end--;
}
//end從後往前找,找比Key小的
while (begin<end && arr[end] >= key){
end--;
}
//找到了,用該元素去填begin位置的坑
if(begin<end){
arr[begin] = arr[end];
begin++;
}
}
//用key填最後一個坑
arr[begin] = key;
return begin;
}
- 方法三:前後索引
標註:key=arr[length-1]
此方法結合代碼來分析,首先先看代碼,如下所示
public static int partion3(int[]arr,int left, int right){
int cur = left;
int pre = cur-1;
int mid = getIndexOfMiddle(arr,left,right);//優化(後面會講到)
swap(arr,mid,right);
int key = arr[right-1];
while (cur<right){
if(arr[cur] < key && pre++!=cur){
swap(arr,cur,pre);
}
cur++;
}
if(pre++!=cur){
swap(arr,pre,right-1);
}
return pre;
}
1、定義兩個索引:cur 和 pre ;
2、從3開始,arr[cur] < key (3 < 5滿足),但是pre++!=cur(不滿足) ,pre(pre++)、 cur都在3的位置上;不進入if語句。
3、cur++: cur 到8的位置上,再次進行while循環;
4、arr[cur] < key (8 不小於 5 ----不滿足),直接cur++(cur到2的位置上);
5、此時 arr[cur] < key (2 < 5滿足),pre++!=cur(pre在3的位置上)此條件也滿足,進行交換後cur++(此時cur在6的位置上);
6、一直循環,直到跳出循環位置;
7、循環結束後,pre+1與key值進行交換即可。
注意: pre 與 cur 一直是一前一後的關係,一段時間後,二者之間有距離,且二者之間的元素都大於key值。
四、最優情況與最差情況
- 最優:
如果每次獲取到的基準值都能夠將區間劃分成左右兩半部分,類似於一棵平衡二叉樹:O(N1ogN);
- 最差:
每次劃分之後,數據都在基準值的一側(每次拿到的基準值剛好是區間中的極值),類似於一棵單支樹:O(N²);
我們要儘量避免最差情況情況出現,因此我們可以採取三數取中的方式進行優化,使得平均時間複雜度爲O(N1ogN)
public static int getIndexOfMiddle(int[] arr,int left,int right){
int mid = left+(right-left)>>1;
if(arr[left] < arr[right-1]){ //a<c
if(arr[mid] < arr[left]){ // b<a
return left;
}else if(arr[mid] > arr[right-1]){//b>c
return right-1;
}else{
return mid;
}
}else{ //a>c
if(arr[mid]> arr[left]){ //b>a
return left;
}else if(arr[mid]< arr[left]){
return right-1;
}else {
return mid;
}
}
}
採用三數取中優化之後,每次拿到極值的概率就降低,認爲快排最終看的是平均複雜度:O(NlogN)
五、應用場景及優化
【應用場景】:數據量大此較隨機(數據雜亂)
數據量大,將來遞歸深度可能比較深,每次遞歸都是一次函數調用,每次都需要再棧中壓入一個棧幀;
(棧幀:函數在運行期間要保存的中間結果-比如:函數中的局部變量參數返回值信息)
棧是有大小的,所以可能會導致棧溢出,優化遞歸過深可能會導致棧溢出的問題,所以我們採取插入排序優化(插入排序在這裏就不寫了):
public static void quickSort(int[] arr,int left, int right){
if(right-left < 16){
insertSort(arr,left,right);
}else{
//按基準值對[left,right)區間進行分割
int key = partion2(arr,left,right);
//遞歸基準值左半側和右半側
quickSort(arr,left,key);
quickSort(arr,key+1,right);
}
}
right-left < 16:沒有讓遞歸到區間只剩一個數據時退出,是因爲遞歸到一定程度,區間中的數據實際慢慢的變少;
採取插入排序優化的這種方式只能將遞歸導致棧溢出的概率降低,不能杜絕;如果想要杜絕此問題,可採取循環的方式(可藉助棧完成):
①、棧的特性:後進先出;
②、遞歸:先調用的後退出,後調用的先退出;
public static void quickSort2(int[] arr) {
Stack<Integer> stack = new Stack<>();
//相當於right left
stack.push(arr.length);
stack.push(0);
while (!stack.empty()){
int left = stack.pop();
int right = stack.pop();
if(right- left > 1){
int key =partion(arr,left,right);
//[key+1,right)
stack.push(right);
stack.push(key+1);
//[left,key)
stack.push(key);
stack.push(left);
}
}
}