程序猿修仙之路--算法之快速排序到底有多快



 

快排

天下武功,唯快不破!!外功如此,內功亦是如此。今日我們來修煉一門比較快速的排序算法-快速排序。快速排序流行的原因是它實現簡單,並且在多數應用中比其他排序算法快的多。


習練快速排序,先要了解如下兩個概念:

分治思想

關於排序,江湖盛傳有一種分治思想,能大幅度提高排序心法的性能。所謂分治,即:化大為小,分而治之。達到治小而治大的成效。多年來基於分治思想衍生出多種排序心法,然萬變不離其宗!

遞歸思想

關於遞歸,其實更像是一種解決問題的手段。我們把具有相同

解決思路的部分提取出來,循環調用。在code的表現形式上我們更傾向於說:自己調用自己。

       
               

雖然江湖上算法內功繁多,但是好的算法小編認為必須符合以下幾個條件,方能真正提高習練者實力:

1            

時間復雜度(運行時間)        

在算法時間復雜度維度,我們主要對比較和交換的次數做對比,其他不交換元素的算法,主要會以訪問數組的次數的維度做對比。。            

        其實有很多修煉者對於算法的時間復雜度有點模糊,分不清什么所謂的 O(n),O(nlogn),O(logn)...等,也許下圖對一些人有一些更直觀的認識。 

 

                   
                       

2            

空間復雜度(額外的內存使用)        

排序算法的額外內存開銷和運行時間同等重要。 就算一個算法時間復雜度比較優秀,空間復雜度非常差,使用的額外內存非常大,菜菜認為它也算不上一個優秀的算法。
           

3            

結果的正確性        

這個指標是菜菜自己加上的,我始終認為一個優秀的算法最終得到的結果必須是正確的。就算一個算法擁有非常優秀的時間和空間復雜度,但是結果不正確,導致修煉者經脈逆轉,走火入魔,又有什么意義呢?

     

氣運丹田,開啟修煉之路
原理
基本思想:選取一個元素作為分割點,通過遍歷把小於分割點的元素放到分割點左邊,把大於分割點的元素放到分割點元素右邊。然后再按此方法對兩部分數據分別排序,以此類推,直到分割的數組大小為1。 整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。

實現快速排序的方式有很多,其中以類似指針移動方式最為常見,為什么最常見呢?因為它的空間復雜度為O(1),也就是說是原地排序

1.   我們從待排序的記錄序列中選取一個記錄(通常第一個)作為基准元素(稱為key)key=arr[left],然后設置兩個變量,left指向數列的最左部,right指向數據的最右部。

2.   key首先與arr[right]進行比較,如果arr[right]<key,則arr[left]=arr[right]將這個比key小的數放到左邊去,如果arr[right]>key則我們只需要將right--,right--之后,再拿arr[right]與key進行比較,直到arr[right]<key交換元素為止。


3.   如果右邊存在arr[right]<key的情況,將arr[left]=arr[right],接下來,將轉向left端,拿arr[left ]與key進行比較,如果arr[left]>key,則將arr[right]=arr[left],如果arr[left]<key,則只需要將left++,然后再進行arr[left]與key的比較。

4.   然后再移動right重復上述步驟

5.   最后得到 {23 58 13 10 57 62} 65 {106 78 95 85},再對左子數列與右子數列進行同樣的操作。最終得到一個有序的數列。


{23 58 13 10 57 62} 65 {106 78   95 85}

{10 13} 23 {58 57 62} 65 {85 78 95} 106

10 13 23 57 58 62 65 78 85 95 106


性能特點
關於復雜度相關O(n)等公式,我這里需要強調一點,公式代表的是算法的復雜度增長的趨勢,而不是具體計算復雜度的公式。比如:O(n²)和O(n)相比較,只是說明 O(n²)增長的趨勢要比o(n)快,並不是說明O(n²)的算法比O(n)的算法所用時間一定就要多。


1. 時間復雜度:

        快速排序平均時間復雜度為O(nlogn),最好情況下為O(nlogn),最壞情況下O(n²)

2. 空間復雜度:

        基於以上例子來實現的快排,空間復雜度為O(1),也就是原地排序。

 3. 穩定性

        舉個例子:待排序數組:int a[] ={1, 2, 2, 3, 4, 5, 6};在快速排序的隨機選擇比較子(即pivot)階段:若選擇a[2](即數組中的第二個2)為比較子,,而把大於等於比較子的數均放置在大數數組中,則a[1](即數組中的第一個2)會到pivot的右邊, 那么數組中的兩個2非原序(這就是“不穩定”)。

若選擇a[1]為比較子,而把小於等於比較子的數均放置在小數數組中,則數組中的兩個2順序也非原序。可見快速排序不是穩定的排序。


改進
通過以上分析各位俠士是否能夠分析出來快速排序有哪些地方存在瑕疵呢?


1. 切分不平衡:

        也就是說我們選取的切分元素距離數組中間值的元素位置很遠,極端情況下會是數組最大或最小的元素,這就導致了划分出來的大數組會被划分為很多次。針對此情況,我們可以取數組多個元素來平衡這種情況,例如:我們可以隨機選取三個或者五個元素,取其中間值的元素作為分割元素。

2. 小數組:

        當快速排序切分為比較小的數組時候,也會利用遞歸調用自己。在這種小數組的情況下,其實一些基礎排序算法反而比快速排序要快。當數組比較小的時候不妨嘗試一下切換到插入排序。具體多小是小呢?一般5-15吧,僅供參考。

3. 重復元素:

        在我們實際應用中經常會遇到重復元素比較多的情況,按照快排的思想,相同元素是會被頻繁移動和划分的,其實這完全沒有必要。我們該怎么辦呢?我們可以把數組切換為三部分:大於-等於-小於 三部分數組,這樣等於的那部分數組就可以避免移動了,不過落地的代碼復雜度要高很多,有興趣的同學可以實現一下。

使用場景

1. 當一個數組大小為中型以上的數量級時,菜菜認為可以使用快速排序,並且伴隨着數組的持續增大,快速排序的性能趨於平均運行時間。至於多大的數組為中型,一般認為50+ 吧,僅供參考。

2. 當一個數組為無序並且重復元素不多時候,也適合快速排序。為什么提出重復元素這個點呢?因為如果重復元素過多,本來重復元素是無需排序的,但是快速排序還是要划分為更多的子數組來比較,這個時候也許插入排序更適合



試煉一發吧
1

c#武器版本


        static void Main(string[] args)
       {

           List<int> data = new List<int>();

            for (int i = 0; i < 11; i++)

           {                data.Add(new Random(Guid.NewGuid().GetHashCode()).Next(1, 100));            }            //打印原始數組值            Console.WriteLine($"原始數據: {string.Join(",", data)}");            quickSort(data, 0, data.Count - 1);            //打印排序后的數組            Console.WriteLine($"排序數據: {string.Join(",", data)}");            Console.Read();

       }

    public static void quickSort(List <int> source, int left, int right)

       {  

            int pivot = 0;

            if (left < right)

           {                pivot = partition(source, left, right);                quickSort(source, left, pivot - 1);                quickSort(source, pivot + 1, right);            }        }        //對一個數組/列表按照第一個元素 分組排序,返回排序之后key所在的位置索引        private static int partition(List<int> source, int left, int right)

       {  

            int key = source[left];  

            while (left < right)

           {                //從右邊篩選 大於選取的值的不動,小於key的交換位置                while (left < right && source[right] >= key)                {                    right--;

               }

                source[left] = source[right];

                while (left < right && source[left] <= key)

               {                    left++;

               }  

                source[right] = source[left];

           }

            source[left] = key;  

            return left;

       }
2
golang 武器版
    
    
    
            
package main

import (

"fmt"

"math/rand"

)

func main() {

var data []int for i := 0; i < 10; i++ {

data = append(data, rand.Intn(100))

}

fmt.Println(data)

quickSort(data[:], 0, len(data)-1)

fmt.Println(data)

}

func quickSort(source []int, left int, right int) {

var pivot = 0

if left < right {

pivot = partition(source, left, right)

quickSort(source, left, pivot-1)

quickSort(source, pivot+1, right)

}

}

func partition(source []int, left int, right int) int {

var key = source[left] for left < right {

for left < right && source[right] >= key {

right--

}

source[left] = source[right]

for left < right && source[left] <= key {

left++

}

source[right] = source[left]

}

source[left] = key

return left

}


運行結果:

[81 87 47 59 81 18 25 40 56 0]
[0 18 25 40 47 56 59 81 81 87]

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM