快速排序的詳細分析、代碼實現以及如何優化(Java)

一、原理
  1. 從區間中取一個數據作爲基準值,按照基準值將區間劃分爲左右兩部分,其中左半部分的數據 < 基準值,右半部分的數據>基準值;
  2. 按照快排的思想排左半部分;
  3. 按快排的思想排右半部分;

在這裏插入圖片描述
類似於二叉樹前序遍歷的框架:

 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;
    }
  1. 方法一:進行數據劃分
    在這裏插入圖片描述
    標註: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;
    }
  1. 方法二:“挖坑法”
    在這裏插入圖片描述
    標註: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;
    }
  1. 方法三:前後索引
    在這裏插入圖片描述
    標註: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);
         }
        }
    }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章