一個快速排序寫了快 10000 字?

2021-03-06 編程如畫

   快速排序

今天我們來說一下快速排序,這個排序算法也是面試的高頻考點,原理很簡單,我們一起來扒一下他吧。

我們先來說一下快速排序的基本思想,很容易理解。

1.先從數組中找一個基準數

2.讓其他比它大的元素移動到數列一邊,比他小的元素移動到數列另一邊,從而把數組拆解成兩個部分。

3.再對左右區間重複第二步,直到各區間只有一個數。

見下圖

快速排序

上圖則為一次快排示意圖,下面我們再利用遞歸,分別對左半邊區間也就是 [3,1,2] 和右半區間 [7,6,5,8] 執行上訴過程,直至區間縮小為 1 也就是第三條,則此時所有的數據都有序。

簡單來說就是我們利用基準數通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比基準數小,另一部分記錄的關鍵字均比基準數大,然後分別對這兩部分記錄繼續進行分割,進而達到有序。

我們現在應該了解了快速排序的思想,那麼大家還記不記得我們之前說過的歸併排序,他們兩個用到的都是分治思想,那他們兩個有什麼不同呢?見下圖

註:這裡我們以區間的第一個元素作為基準數

對比

雖然歸併排序和快速排序都用到了分治思想,但是歸併排序是自下而上的,先處理子問題,然後再合併,將小集合合成大集合,最後實現排序

快速排序是由上到下的,先分區,然後再處理子問題。歸併排序雖然是穩定的、時間複雜度為 O(nlogn) 的排序算法,但是它是非原地排序算法。我們前面講過,歸併之所以是非原地排序算法,主要原因是合併函數需要利用輔助數組保存元素。

快速排序通過設計巧妙的原地分區函數,可以實現原地排序,解決了歸併排序佔用太多內存的問題。

我們根據思想可知,快速排序算法的核心就是如何利用基準數將記錄分區,這裡我們主要介紹兩種容易理解的方法,一種是挖坑填數,另一種是利用雙指針思想進行元素交換。

下面我們先來介紹下挖坑填數的分區方法

基本思想是我們首先以序列的第一個元素為基準數,然後將該位置挖坑,下面判斷 nums[hight] 是否大於基準數,如果大於,則左移 hight 指針,直至找到一個小於基準數的元素,將其填入之前的坑中,則 hight 位置會出現一個新的坑,此時移動 low 指針,找到大於基準數的元素,填入新的坑中。不斷迭代直至完成分區。

大家直接看我們的視頻模擬吧,一目了然。

註:為了便於理解所以採取了挖坑的形式展示

是不是很容易就理解啦,下面我們直接看代碼吧。

class Solution {
    public int[] sortArray(int[] nums) { 

        quickSort(nums,0,nums.length-1);
        return nums;

    }
    public void quickSort (int[] nums, int low, int hight) {

        if (low < hight) {
            int index = partition(nums,low,hight);
            quickSort(nums,low,index-1);
            quickSort(nums,index+1,hight);
        }    

    }
    public int partition (int[] nums, int low, int hight) {

            int pivot = nums[low];
            while (low < hight) {
                //移動hight指針
                while (low < hight && nums[hight] >= pivot) {
                    hight--;
                }
                //填坑
                if (low < hight) nums[low] = nums[hight];
                while (low < hight && nums[low] <= pivot) {
                    low++;
                }
                //填坑
                if (low < hight) nums[hight] = nums[low];
            }
            //基準數放到合適的位置
            nums[low] = pivot;
            return low;
    }   
}

下面我們來看一下第二種交換方法的思路,原理一致,實現也比較簡單。

見下圖。

快速排序

其實這種方法,算是對上面方法的挖坑填坑步驟進行合併,low 指針找到大於 pivot 的元素,hight 指針找到小於 pivot 的元素,然後兩個元素交換位置,最後再將基準數歸位。

兩種方法都很容易理解和實現,即使是完全沒有學習過快速排序的同學,理解了思想之後也能自己動手實現,下面我們繼續用視頻模擬下這種方法的執行過程吧。

兩種方法都很容易實現,對我們非常友好,大家可以自己去 AC 一下啊。

class Solution {
    public int[] sortArray (int[] nums) {   

        quickSort(nums,0,nums.length-1);
        return nums;

    }

    public void quickSort (int[] nums, int low, int hight) {

        if (low < hight) {
            int index = partition(nums,low,hight);
            quickSort(nums,low,index-1);
            quickSort(nums,index+1,hight);
        } 

    }

    public int partition (int[] nums, int low, int hight) {

            int pivot = nums[low];
            int start = low;

            while (low < hight) {
                while (low < hight && nums[hight] >= pivot) hight--;           
                while (low < hight && nums[low] <= pivot) low++;
                if (low >= hight) break;
                swap(nums, low, hight);  
            }
            //基準值歸位
            swap(nums,start,low);
            return low;
    }  
    public void swap (int[] nums, int i, int j) {      
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
     } 
}

快速排序的時間複雜度分析

快排也是用遞歸來實現的。所以快速排序的時間性能取決於快速排序的遞歸樹的深度。

如果每次分區操作,都能正好把數組分成大小接近相等的兩個小區間,那麼此時的遞歸樹是平衡的,性能也較好,遞歸樹的深度也就和之前歸併排序求解方法一致。

我們每一次分區需要對數組掃描一遍,做 n 次比較,所以最優情況下,快排的時間複雜度是 O(nlogn)。

但是大多數情況下我們不能劃分的很均勻,比如數組為正序或者逆序時,即 [1,2,3,4] 或 [4,3,2,1] 時,此時為最壞情況,那麼此時我們則需要遞歸調用 n-1 次,此時的時間複雜度則退化到了 O(n^2)。

快速排序的空間複雜度分析

快速排序主要時遞歸造成的棧空間的使用,最好情況時其空間複雜度為O (logn),對應遞歸樹的深度。最壞情況時則需要 n-1 次遞歸調用,此時空間複雜度為O(n)。

快速排序的穩定性分析

快速排序是一種不穩定的排序算法,因為其關鍵字的比較和交換是跳躍進行的,見下圖。

穩定性

此時無論使用哪一種方法,第一次排序後,黃色的 1 都會跑到紅色  1 的前面,所以快速排序是不穩定的排序算法。

性能分析

好啦,快速排序我們掌握的差不多了,下面我們一起來看看如何對其優化吧。

快速排序的迭代寫法

該方法實現也是比較簡單的,藉助了棧來實現,很容易實現。主要利用了先進先出的特性,這裡需要注意的就是入棧的順序,這裡算是一個小細節,需要注意一下,其中分區函數和上面一致。大家只需看入棧出棧情況即可。

class Solution {

     public int[] sortArray(int[] nums) {
        Stack<Integer> stack = new Stack<>();
        stack.push(nums.length - 1);
        stack.push(0);
        while (!stack.isEmpty()) {
            int low = stack.pop();
            int hight = stack.pop();

            if (low < hight) {
                int index = partition(nums, low, hight);
                stack.push(index - 1);
                stack.push(low);
                stack.push(hight);
                stack.push(index + 1);
            }
        }
        return nums;
    }

    public int partition (int[] nums, int low, int hight) {

            int pivot = nums[low];
            int start = low;
            while (low < hight) {

                while (low < hight && nums[hight] >= pivot) hight--;           
                while (low < hight && nums[low] <= pivot) low++;
                if (low >= hight) break;
                swap(nums, low, hight); 
            }
            swap(nums,start,low);
            return low;

    } 
    public void swap (int[] nums, int i, int j) {    
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }  
}

快速排序優化

三數取中法

我們在上面的例子中選取的都是 nums[low] 做為我們的基準值,那麼當我們遇到特殊情況時呢?見下圖

特殊舉例

我們按上面的方法,選取第一個元素做為基準元素,那麼我們執行一輪排序後,發現僅僅是交換了 2 和 7 的位置,這是因為 7 為序列的最大值。

所以我們的 pivot 的選取尤為重要,選取時應儘量避免選取序列的最大或最小值做為基準值。則我們可以利用三數取中法來選取基準值。

也就是選取三個元素中的中間值放到 nums[low] 的位置,做為基準值。這樣就避免了使用最大值或最小值做為基準值。

所以我們可以加上這幾行代碼實現三數取中法。

     int mid = low + ((hight-low) >> 1);
     if (nums[low] > nums[hight]) swap(nums,low,hight);
     if (nums[mid] > nums[hight]) swap(nums,mid,hight);
     if (nums[mid] > nums[low]) swap(nums,mid,low); 

其含義就是讓我們將中間元素放到 nums[low] 位置做為基準值,最大值放到 nums[hight],最小值放到 nums[mid],即 [4,2,3] 經過上面代碼處理後,則變成了 [3,2,4]。

此時我們選取 3 做為基準值,這樣也就避免掉了選取最大或最小值做為基準值的情況。

三數取中法

class Solution {
    public int[] sortArray(int[] nums) {       
        quickSort(nums,0,nums.length-1);
        return nums;
    }
    public void quickSort (int[] nums, int low, int hight) {
        if (low < hight) {
            int index = partition(nums,low,hight);
            quickSort(nums,low,index-1);
            quickSort(nums,index+1,hight);
        }       
    }

    public int partition (int[] nums, int low, int hight) {
            //三數取中,大家也可以使用其他方法
            int mid = low + ((hight-low) >> 1);
            if (nums[low] > nums[hight]) swap(nums,low,hight);
            if (nums[mid] > nums[hight]) swap(nums,mid,hight);
            if (nums[mid] > nums[low]) swap(nums,mid,low); 
            //下面和之前一樣,僅僅是多了上面幾行代碼
            int pivot = nums[low];
            int start = low;
            while (low < hight) {
                while (low < hight && nums[hight] >= pivot) hight--;           
                while (low < hight && nums[low] <= pivot) low++;
                if (low >= hight) break;
                swap(nums, low, hight); 
            }
            swap(nums,start,low);
            return low;
    }  
    public void swap (int[] nums, int i, int j) {     
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }  
}

和插入排序搭配使用

我們之前說過,插入排序在元素個數較少時效率是最高的,沒有看過的同學可以去看一下之前的文章,所以當元素數量較少時,快速排序反而不如插入排序好用。

所以我們可以設定一個閾值,當元素個數大於閾值時使用快速排序,小於等於該閾值時則使用插入排序。我們設定閾值為 7 。

三數取中+插入排序

class Solution {
    private static final int INSERTION_SORT_MAX_LENGTH = 7;

    public int[] sortArray(int[] nums) {      
        quickSort(nums,0,nums.length-1);
        return nums;
    }

    public void quickSort (int[] nums, int low, int hight) {

            if (hight - low <= INSERTION_SORT_MAX_LENGTH) {
                insertSort(nums,low,hight);
                return;
            }               
            int index = partition(nums,low,hight);
            quickSort(nums,low,index-1);
            quickSort(nums,index+1,hight);         
    }

    public int partition (int[] nums, int low, int hight) {
            //三數取中,大家也可以使用其他方法
            int mid = low + ((hight-low) >> 1);
            if (nums[low] > nums[hight]) swap(nums,low,hight);
            if (nums[mid] > nums[hight]) swap(nums,mid,hight);
            if (nums[mid] > nums[low]) swap(nums,mid,low);   
            int pivot = nums[low];
            int start = low;
            while (low < hight) {
                while (low < hight && nums[hight] >= pivot) hight--;           
                while (low < hight && nums[low] <= pivot) low++;
                if (low >= hight) break;
                swap(nums, low, hight); 
            }
            swap(nums,start,low);
            return low;
    } 

    public void insertSort (int[] nums, int low, int hight) {

        for (int i = low+1; i <= hight; ++i) {
            int temp = nums[i];
            int j;
            for (j = i-1; j >= 0; --j) {
                if (temp < nums[j]) {
                    nums[j+1] = nums[j];
                    continue;
                } 
                break;
            }
            nums[j+1] = temp;
        }
    } 
    public void swap (int[] nums, int i, int j) {

        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    } 
}

好啦,我們的插入排序和快速排序的搭配使用就搞定啦。

我們繼續看下面這種情況

我們對其執行一次排序後,則會變成上面這種情況,然後我們繼續對藍色基準值的左區間和右區間執行相同操作。也就是 [2,3,6,3,1,6] 和 [8,6] 。我們注意觀察一次排序的結果數組中是含有很多重複元素的,我們之前的優化方式並不能很好的解決這種情況。

那麼我們為什麼不能將相同元素放到一起呢?這樣就能大大減小遞歸調用時的區間大小,見下圖。

三向切分

這樣就減小了我們的左右區間大小,只需對區間 [3,1,2,4] 和 [8] 執行相同操作即可,我們將數組切分成了三部分,小於基準數的左區間,大於基準數的右區間,等於基準數的中間區間。

下面我們來看一下如何達到上面的情況,我們先來進行視頻模擬,模擬之後應該就能明白啦。

我們來剖析一下視頻,其實原理很簡單,我們利用探路指針也就是  i,遇到比 pivot 大的元素,則和 right 指針進行交換,交換後 right 指向的元素肯定比 pivot 大,則 right--,但是,此時我們的 nums[i] 指向的元素並不知道情況,所以我們的 i 指針先不動,繼續判斷。

如果此時 nums[i] < pivot 則與 left 指針交換,注意此時我們的 left 指向的值肯定是等於 povit的,所以交換後我們要 left++,i++, nums[i] == pivot 時,僅需要 i++ 即可,繼續判斷下一個元素。我們也可以藉助這個思想來解決經典的荷蘭國旗問題。

好啦,我們下面直接看代碼吧。

三數取中+三向切分+插入排序

class Solution {
    private static final int INSERTION_SORT_MAX_LENGTH = 7;
    public int[] sortArray(int[] nums) {
        quickSort(nums,0,nums.length-1);
        return nums;

    }
    public void quickSort(int nums[], int low, int hight) {
        //插入排序
        if (hight - low <= INSERTION_SORT_MAX_LENGTH) {
            insertSort(nums,low,hight);
            return;
        }
        //三數取中
        int mid = low + ((hight-low) >> 1);
        if (nums[low] > nums[hight]) swap(nums,low,hight);
        if (nums[mid] > nums[hight]) swap(nums,mid,hight);
        if (nums[mid] > nums[low]) swap(nums,mid,low);
        //三向切分
        int left = low,  i = low + 1, right = hight;
        int pvoit = nums[low];
        while (i <= right) {
            if (pvoit < nums[i]) {
                swap(nums,i,right);
                right--;
            } else if (pvoit == nums[i]) {
                i++;
            } else {
                swap(nums,left,i);
                left++;
                i++;
            }
        }
        quickSort(nums,low,left-1);
        quickSort(nums,right+1,hight);
    }
     public void insertSort (int[] nums, int low, int hight) {

        for (int i = low+1; i <= hight; ++i) {
            int temp = nums[i];
            int j;
            for (j = i-1; j >= 0; --j) {
                if (temp < nums[j]) {
                    nums[j+1] = nums[j];
                    continue;
                } 
                break;
            }
            nums[j+1] = temp;
        }
    } 
    public  void swap (int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

好啦,一些常用的優化方法都整理出來啦,還有一些其他的優化算法九數取中,優化遞歸操作等因為不常用就不在這裡進行描述啦,

感興趣的可以自己看一下。好啦,這期的文章就到這裡啦,我們下期見,拜了個拜。

相關焦點

  • QuickSort 快速排序到底快在哪裡?
    創作目的最近想系統整理一下資料庫的索引系列,然後就牽扯到了二分查找,二分查找的前提需要排序。排序工作中我們用的很多,不過很少去關心實現;面試中,排序的出場率非常高,以此來驗證大家是否懂得「算法」。無論如何,排序算法都值得每一位極客去學習和掌握。快速排序 Quicksort快速排序(有時稱為分區交換排序)是一種有效的排序算法。
  • 分而治之和快速排序
    二、快速排序2.1 快速排序原理第一個重要的D&C算法-快速排序,快速排序是一種排序算法。速度比選擇排序快得多。使用快速排序對數組進行排序,對排序算法來說,最簡單的數組是什麼樣子的呢?就是根本不需要排序的數組。例如:空數組和只包含一個元素的數組。所以基線條件為數組為空數組或者數組只包含一個元素,在這種情況下,只需要原樣返回數組,根本不需要排序。
  • 我們如何用Python寫個快速排序的小程序
    今天學習完算法後,下面的快速排序Python小程序還是不錯的,如果正在學習這方面知識的,可以參考如下程序:# 用Python寫的快速排序的小程序def pai(value):# 遞歸退出條件# 判斷只有一個數據時,無需排序if
  • 快速排序算法介紹
    算法過程設要排序的數組是 A[0] …… A[N-1],首先任意選取一個數據(通常選用第一個數據)作為關鍵數據,然後將所有比它小的數都放到它前面,所有比它大的數都放到它後面,這個過程稱為一趟快速排序。值得注意的是,快速排序不是一種穩定的排序算法,也就是說,多個相同的值的相對位置也許會在算法結束時產生變動。
  • Python中的快速排序算法,快速排序的優缺點,中級python技術點
    Python中的快速排序算法就像合併排序一樣,快速排序算法採用分治法的原理將輸入數組分為兩個列表,第一個包含小項目,第二個包含大項目。然後,該算法將對兩個列表進行遞歸排序,直到對結果列表進行完全排序為止。劃分輸入列表稱為對列表進行分區。
  • 谷歌大腦重磅研究:快速可微分排序算法,速度快出一個數量級
    魚羊 十三 發自 凹非寺量子位 報導 | 公眾號 QbitAI快排堆排冒泡排。排序,在計算機中是再常見不過的算法。在機器學習中,排序也經常用於統計數據、信息檢索等領域。那麼問題來了,排序算法在函數角度上是分段線性的,也就是說,在幾個分段的「節點」處是不可微的。這樣,就給反向傳播造成了困難。現在,谷歌大腦針對這一問題,提出了一種快速可微分排序算法,並且,時間複雜度達到了O(nlogn),空間複雜度達為O(n)。
  • 快速排序的JavaScript實現詳解
    // 每日前端夜話 第414篇// 正文共:3300 字// 預計閱讀時間:10 分鐘介紹排序是指以特定順序(數字或字母)排列線性表的元素。排序通常與搜索一起配合使用。有許多排序算法,而迄今為止最快的算法之一是快速排序(Quicksort)。快速排序用分治策略對給定的列表元素進行排序。這意味著算法將問題分解為子問題,直到子問題變得足夠簡單可以直接解決為止。
  • 深入理解快速排序和 STL 的 sort 算法
    很多人提起快排和二分都覺得很容易的樣子,但是讓現場Code很多就翻車了,就算可以寫出個遞歸版本的代碼,但是對其中的複雜度分析、邊界條件的考慮、非遞歸改造、代碼優化等就無從下手,填鴨背誦基本上分分鐘就被面試官擺平了。快速排序Quicksort又稱劃分交換排序partition-exchange sort,簡稱快排,一種排序算法。最早由C.
  • 關於 JavaScript 的數組隨機排序
    (點擊上方藍字,快速關注我們)作者:oldjblog.oldj.net/2017/01/23/shuffle-an-array-in-javascript
  • 快速排序 Quicksort
    快速排序(quick sort)的採用了分而治之(divide and conquer)的策略:將原問題分解為若干個規模更小但結構與原問題相似的子問題。遞歸地解這些子問題,然後將這些子問題的解組合為原問題的解。    1. 算法思想        快速排序的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分:分割點左邊都是比它小的數,右邊都是比它大的數。
  • 排序算法:快速排序
    在日常開發中經常會遇到一類問題,就是對一個集合的數據進行排序掌握一些排序算法,對於日常開發是非常有幫助今天介紹一下快速排序法01算法邏輯02時間複雜度由上圖邏輯可以得出,參照遞歸表達式漸進複雜度分析快速排序的時間複雜度遞歸表達式為,其中a=2,b=2,d=1由算法導論的主定理可以推導出,快速排序的時間複雜度為03空間複雜度由上圖邏輯可以得出
  • 重溫7種排序算法 第一篇,交換排序算法:冒泡排序、快速排序
    此為第一篇,交換排序算法:冒泡排序、快速排序。簡介所謂交換指根據序列中兩個元素關鍵字的比較結果來對換這兩個記錄在序列中的位置。基於交換的排序算法主要介紹冒泡排序和快速排序。( n^2 ); 2.3穩定性:若在一個待排序的序列中,存在2個相等的數,在排序後這2個數的相對位置保持不變,那麼該排序算法是穩定的;否則是不穩定的由於 i>j且A[i]= [j]時,不會發生交換,因此冒泡排序是種穩定排序方法。
  • 八十四、堆排序解決TopK問題
    「@Author:Runsen」上次介紹了堆排序,這次介紹堆排序常見的應用場景TopK問題。利用堆求TopK問題TopK問題是一個堆排序典型的應用場景。至於為什麼TopK問題最佳的答案是堆排序?其實在空間和時間的複雜度來考量,雖說快排是最好的排序算法,但是對於100億個元素從大到小排序,然後輸出前 K 個元素值。可是,無論我們掌握的是快速排序算法還是堆排序算法,在排序的時候,都需要將全部的元素讀入到內存中。
  • 遞歸與快速排序
    今天我們要說的是快速排序,快速排序也是使用了分治法的一個典型的算法,通過每次選取一個元素作為中間元素,將比這個元素小的放在元素的一側
  • Sort an Array 數組排序
    這裡需要自己實現排序功能,常見排序方法有很多,插入排序,選擇排序,堆排序,快速排序,冒泡排序,歸併排序,桶排序等等。它們的時間複雜度不盡相同,這道題貌似對於平方級複雜度的排序方法會超時,所以只能使用那些速度比較快的排序方法啦。
  • 快速排序(QSort,快排)算法及C語言實現
    上節介紹了如何使用起泡排序的思想對無序表中的記錄按照一定的規則進行排序,本節再介紹一種排序算法——快速排序算法(Quick Sort)。
  • 快速排序算法
    快速排序的時間複雜度與空間複雜度:  最優的情況:當數據隨機分布時,以第一個關鍵字為基準分為兩個子序列,兩個子序列的元素個數接近相等,此時執行效率最好。  最壞情況:當數據有序時,以第一個關鍵字為基準分為兩個子序列,前一個子序列為空,此時執行效率最差。
  • ...選擇,插入,希爾,歸併,快排,堆排序,計數排序,桶排序,基數排序)
    沒用javascript實現過或是沒仔細看過相關算法的原理,導致寫起來浪費很多時間。對於一個前端來說,尤其是筆試面試的時候,算法方面考的其實不難(十大排序算法或是和十大排序算法同等難度的)。算法由來:9世紀波斯數學家提出的算法「al-Khowarizmi」,阿拉伯人對數學史的貢獻很大,讓人欽佩。
  • 快速排序和堆排序
    龜哥今天和大家分享下快速排序,堆排序算法。首先快速排序的思想,給定一個無序的數組,首先用兩個指針left和right分別指向數組的頭部和尾部,將數組的第一個元素array[0]作為和left,right指針比較的元素。
  • 算法圖解 | 分而治之與快速排序算法
    快速排序例如快速排序問題,一個列表進行排序,如下圖首先選擇列表中的一個元素作為基準元素,其他的元素都與這個元素做比較,找出小於這個基準值的值、大於基準值的值。(O表示法表示)快速排序的性能高度依賴於你選擇的基準值。