數(shù)據(jù)結(jié)構(gòu)與算法(二):排序算法

十大基礎(chǔ)排序算法。

Basic-Sorting-Algorithm

關(guān)于十大基本排序算法的整理。

十大排序算法分別為:冒泡排序,選擇排序,插入排序,希爾排序,堆排序,快速排序,歸并排序,計數(shù)排序,桶排序和基數(shù)排序。

排序算法根據(jù)相同的值在排序之前和排序之后的前后位置是否不變來表示該排序算法是否穩(wěn)定,如果不變則是穩(wěn)定的,否則是不穩(wěn)定的。

穩(wěn)定:冒泡排序,插入排序,歸并排序,計數(shù)排序,桶排序,基數(shù)排序

不穩(wěn)定:選擇排序,希爾排序,堆排序,快速排序

排序算法根據(jù)排序時所需數(shù)據(jù)是否一定要全部加載進(jìn)內(nèi)存來區(qū)分內(nèi)外排,不需要則是外排,需要則是內(nèi)排,注意,外排在數(shù)據(jù)少的時候也可以將需要排序的數(shù)據(jù)一次性全加載進(jìn)內(nèi)存,并不是外排就不可以處理數(shù)據(jù)少的情況,只是效率高與效率低的問題。

內(nèi)排:冒泡排序,選擇排序,插入排序,希爾排序,堆排序,快速排序

外排:歸并排序,計數(shù)排序,桶排序,基數(shù)排序

Sorting-Algorithm.png

計數(shù)排序中的k指的是最大數(shù)值和最小數(shù)值的差值。

桶排序中的k指的是分成多少個桶。

基數(shù)排序中的k指的是進(jìn)制中的基數(shù),比如:十進(jìn)制就是10。

基數(shù)排序中的d指的是最大數(shù)值的位數(shù),比如:max=1000,則d=4。

希爾排序的時間復(fù)雜度比較有爭議性,按照我的理解,希爾排序本質(zhì)上也是插入排序的一種,也就是當(dāng)增量=1時,希爾排序最好和最壞分別是:O(n)和O(n2)。

桶排序雖然排序需要遍歷k遍,但是由于每個桶可以采取不同的排序方法,比如:統(tǒng)一采取平均時間復(fù)雜度為O(nlogn)的排序方法,則平均時間復(fù)雜度為:O(O(n+n(logn-logk)))。不僅時間復(fù)雜度和桶里數(shù)據(jù)采取的排序算法有關(guān),連穩(wěn)定性也是,比如,采取不穩(wěn)定的算法,就有可能是不穩(wěn)定的排序算法。

基數(shù)排序雖然將排好的數(shù)據(jù)重新寫回去需要遍歷k遍,但是,其實(shí)還是需要訪問n個數(shù)據(jù),其實(shí)時間復(fù)雜度可以寫成:O(dn)。


1.冒泡排序

兩兩相鄰的數(shù)據(jù)比較,如果前面的數(shù)據(jù)比后面的數(shù)據(jù)大,則交換兩個數(shù)據(jù)的位置,直到所有的數(shù)據(jù)有序。

//Swift
func bubbleSort(sortedList: inout [Int]) {
    var i: Int = 1
    var flag = true  //優(yōu)化

    while i < sortedList.count && flag {
        flag = false
        for j in 0..<sortedList.count-i {
            if sortedList[j] > sortedList[j+1] {
                flag = true
                sortedList.swapAt(j, j+1)
            }
        }
        i++
    }
}

最好時間復(fù)雜度:最好的情況就是需要排序的數(shù)據(jù)完全有序,也就是只需要比較n-1次,移動0次,就可以得到一個完全有序的序列,所以時間復(fù)雜度為:O(n)。

最壞時間復(fù)雜度:需要排序的數(shù)據(jù)逆序,那么第一個數(shù)據(jù)需要比較n-1次,第二個數(shù)據(jù)需要比較n-2次,那么,總的比較時間為:n-1+n-2+n-3+...+1=(n^2 - n)/2,也就是時間復(fù)雜度為:O(n^2)。

平均時間復(fù)雜度:(O(n) + O(n^2))/2 = O(n^2)。

空間復(fù)雜度:因?yàn)榕判蚴窃谠瓟?shù)組上進(jìn)行交換和移動的,也就是不需要額外的輔助空間,嚴(yán)謹(jǐn)來說交換數(shù)據(jù)時需要一個臨時的空間,所以空間復(fù)雜度為:O(1)。

穩(wěn)定性:因?yàn)槭窍噜彽脑貎蓛杀容^,不存在跳躍比較,移動的情況,所以是穩(wěn)定的排序。

排序類型:因?yàn)槊看伪容^需要用到整個數(shù)組,換句話說需要把排序的數(shù)據(jù)一次性加載到內(nèi)存里進(jìn)行排序,所以是內(nèi)排類型。

2.選擇排序

選擇排序,每次都在無序的數(shù)據(jù)中選出最大的數(shù)據(jù),并排在后面,直到所有的數(shù)據(jù)有序。

//Swift
func simpleSelectSort(sortedList: inout [Int]) {
    for j in 0..<sortedList.count-1 {
        for i in j+1..<sortedList.count {
            if sortedList[j] > sortedList[i] {
                sortedList.swapAt(j, i)
            }
        }
    }
}

時間復(fù)雜度:選擇排序比較特殊,無論排序的數(shù)據(jù)是有序還是無序,時間復(fù)雜度都是一樣的。因?yàn)榫退阏麄€數(shù)據(jù)有序,但是你不將所有的數(shù)據(jù)比較一次,是不可能知道這個數(shù)據(jù)就是最大或者最小的,雖然人眼是能看出來,但是機(jī)器看不出,所以,時間復(fù)雜度為:O(n^2)。

空間復(fù)雜度:和冒泡排序一樣,最多使用一個數(shù)據(jù)空間,所以空間復(fù)雜度為:O(1)。

穩(wěn)定性:因?yàn)樾枰谑O碌乃袛?shù)據(jù)中尋找最大值,存在跳躍的情況,比如:5 4 5 3 2 => 4 3 5 2 5 很明顯前面的5跑到后面來了,所以是不穩(wěn)定的。

排序類型:同冒泡排序一樣,需要一次性把排序的數(shù)據(jù)加載到內(nèi)存,所以是內(nèi)排。

3.插入排序

插入排序是不斷的將數(shù)據(jù)插入前面有序的序列,形成新的有序序列。

//Swift
func insertSort(sortedList: inout [Int]) {
    for j in 1..<sortedList.count {
        if sortedList[j] < sortedList[j-1] {
            let temp = sortedList[j]
            var i: Int = j-1
            while i >= 0 && sortedList[i] > temp {
                sortedList[i+1] = sortedList[i]
                i--
            }
            sortedList[i+1] = temp
        }
    }
}

最好時間復(fù)雜度:如果排序的數(shù)據(jù)完全有序,則只需要比較n-1次,不需要移動數(shù)據(jù),則最好的時間復(fù)雜度為:O(n)。

最壞時間復(fù)雜度:如果排序的數(shù)據(jù)逆序,從第二數(shù)據(jù)開始,第一次在比較是否進(jìn)入循環(huán)時,比較了一次,然后在循環(huán)比較移動時有比較了一次,也就是兩次,總的時間復(fù)雜度為:2+3+4+...+n=(n+2)(n-1)/2,時間復(fù)雜度為:O(n^2)。

平均時間復(fù)雜度:O(n^2)。

穩(wěn)定性:因?yàn)椴迦肱判蚴且粋€一個數(shù)插入,也是相鄰兩個數(shù)據(jù)兩兩比較,不存在跳躍比較和移動的情況,所以是穩(wěn)定的。

排序類型:同冒泡排序一樣,需要一次性把排序的數(shù)據(jù)加載到內(nèi)存,所以是內(nèi)排。

4.希爾排序

希爾排序是插入排序的升級版,通過設(shè)置increment(增量),把數(shù)組分成increment組,分別進(jìn)行插入排序。然后,increment不斷的減少,最終一定是increment=1,也就是整個數(shù)組進(jìn)行插入排序,得出有序的序列。

//希爾排序
func shellSort(sortedList: inout [Int]) {
    let length = sortedList.count
    var increment = length

    repeat{
        increment = increment/3+1
        for i in 0..<length {
            if i >= increment && sortedList[i] < sortedList[i-increment] {
                let temp = sortedList[i]
                var j: Int = i-increment
                while j >= 0 && sortedList[j] > temp {
                    sortedList[j+increment] = sortedList[j]
                    j -= increment
                }
                sortedList[j+increment] = temp
            }
        }
    }while increment > 1
}

時間復(fù)雜度:因?yàn)橄柵判蚴峭ㄟ^不同的增量來進(jìn)行分組,然后每組進(jìn)行插入排序的,也就是說增量的取值直接影響到希爾排序的時間復(fù)雜度。但是,在我看來,希爾排序說到底屬于插入排,那么一開始increment=1,也就是希爾排序的最好時間復(fù)雜度和最壞時間復(fù)雜度都是和插入排序相同的,也就是O(n)~O(n^2) ,平均時間復(fù)雜度比較復(fù)雜,由于不同的增量取值,導(dǎo)致時間不一樣,有時間復(fù)雜度為O(n^1.3) 和O(n^1.5)的增量取值,大家可以了解一下。

空間復(fù)雜度:和插入排序一樣,空間復(fù)雜度為O(1)。

穩(wěn)定性:因?yàn)橄柵判蚴窃隽坎迦肱判颍嬖谔S比較和移動的情況,所以是不穩(wěn)定的排序。

排序類型:內(nèi)排。

5.堆排序

堆排序是選擇排序的升級版,通過一次次的構(gòu)建大頂堆,不斷獲取堆中最大的數(shù)據(jù),直到堆中沒有數(shù)據(jù),也就是所有數(shù)據(jù)都有序了。

//堆排序
 func heapSort(sortedList: inout [Int]) {
    sortedList.insert(sortedList.count, at: 0)
    let length = sortedList[0]

    for i in stride(from: length/2, through: 1, by: -1) {
        headAdjust(sortedList: &sortedList, index: i, length: length)
    }

    for i in stride(from: length, to: 1, by: -1) {
        sortedList.swapAt(1, i)
        headAdjust(sortedList: &sortedList, index: 1, length: i-1)
    }

    sortedList.removeFirst()
 }

 func headAdjust(sortedList: inout [Int], index: Int, length: Int) {
    let temp = sortedList[index]
    var s = index;  //根, index從1開始算
    var j = index*2  //左子樹
    while j <= length {
        if j < length && sortedList[j] < sortedList[j+1] {
            j++
        }
        if temp >= sortedList[j] {
            break
        }
        sortedList[s] = sortedList[j]
        s = j;
        j = s*2  //左子樹
    }
    sortedList[s] = temp
 }

最好時間復(fù)雜度:開始就是大頂堆,第一次構(gòu)建,只需要比較,不需要移動,所有的數(shù)據(jù)至少需要比較一次,時間復(fù)雜度為:O(n)。然后,從第二次開始,由于每次都是取葉結(jié)點(diǎn)的數(shù)據(jù)取代根結(jié)點(diǎn),所以,每次都需要比較和移動logi(i為當(dāng)前構(gòu)建大頂堆的結(jié)點(diǎn)數(shù)),也就是時間復(fù)雜度為:log(n-1)+log(n-2)+...+log(1)=log((n-1)!)=(n-1)log(n-1)(nlogn=logn!這個等式證明請自行百度),也就是時間復(fù)雜度為:O(nlogn)。

最壞時間復(fù)雜度:開始就是小頂堆,第一次構(gòu)建,每個數(shù)據(jù)都需要比較和移動,也是O(n)的復(fù)雜度。從第二次開始,其實(shí)和最好情況的大頂堆是一樣的,都需要比較和移動那么多的次數(shù),時間復(fù)雜度都是:O(nlogn)。

空間復(fù)雜度:因?yàn)闆]有額外的輔助空間,所以,時間復(fù)雜度為:O(1)。

穩(wěn)定性:因?yàn)榇嬖谔S的移動,所以是不穩(wěn)定的排序。

排序類型:內(nèi)排。

6.快速排序

快速排序是冒泡排序的升級版,歸根到底是比較排序的一種。通過關(guān)鍵數(shù),將數(shù)組分成左右兩個數(shù)組,左邊都小于關(guān)鍵數(shù),右邊都大于關(guān)鍵數(shù),然后左右兩個數(shù)組繼續(xù)分下去,直到所有數(shù)據(jù)都有序。

 //快速排序
 func fastSort(sortedList: inout [Int]) {
    sort(sortedList: &sortedList, start: 0, end: sortedList.count-1)
 }

 func sort(sortedList: inout [Int], start: Int, end: Int) {
    if start < end {
        let m = partion(sortedData: &sortedList, start: start, end: end)
        sort(sortedList: &sortedList, start: start, end: m-1)
        sort(sortedList: &sortedList, start: m+1, end: end)
    }
 }

 //pivot = sortedData[start]
 func partion(sortedData: inout [Int], start: Int, end: Int) -> Int {
    let pivot = sortedData[start]
    var left = start
    var right = end

    while left < right {
        while left < right && sortedData[right] >= pivot {
            right--
        }
        sortedData.swapAt(left, right)
        while left < right && sortedData[left] <= pivot {
            left++
        }
        sortedData.swapAt(left, right)
    }
    return left
 }

最好時間復(fù)雜度:快速排序不斷的把數(shù)組分成兩邊,相當(dāng)于一棵二叉樹,由二叉樹的知識可以知道,完全二叉樹的深度最小,為depth =?logn?+1,也就是說,當(dāng)數(shù)據(jù)比較均勻的分布在二叉樹的左右兩邊,則時間復(fù)雜度最小。假設(shè)快速排序的時間復(fù)雜度為:T(n),第一次需要遍歷整個數(shù)據(jù),然后把數(shù)據(jù)分成均勻的兩部分,則時間復(fù)雜度為:T(n)=2T(n/2)+n,同理,T(n/2)=2T(n/4)+n/2,T(n/4)=2T(n/8)+n/8,則T(n)=2T(n/2)+n=2(2T(n/4)+n/2)+n=4T(n/4)+2n=4(2T(n/8)+n/4)+2n=8T(n/8)+3n=...=nT(n/n)+nlogn=nT(1)+nlogn=nlogn。因?yàn)橥耆鏄涞纳疃葹閘ogn,所以遞歸調(diào)用了logn次,并且直到分到葉子結(jié)點(diǎn),也就是T(1),T(1)=0,所以,T(n)=nlogn。因此,快速排序的時間復(fù)雜度為:O(nlogn)。

最壞時間復(fù)雜度:由二叉樹的知識可以直到,斜樹的深度最大,為depth=n,也就是當(dāng)整個數(shù)組元素構(gòu)造成一棵斜樹,那么,該時間復(fù)雜度最高。由最好時間復(fù)雜度得出的公式,可以用在最壞時間復(fù)雜度的計算,也就是:T(n)=T(n-2)+n-1=T(n-3)+n-1+n-2=T(n-4)+n-1+n-2+n-3=...=n-1+n-2+n-3+...+1=((n-1)*n)/2,所以,最壞時間復(fù)雜度為:O(n2)。

空間復(fù)雜度:最好的情況,需要進(jìn)行l(wèi)ogn次遞歸,所以空間復(fù)雜度為:O(logn),最壞的情況,需要進(jìn)行n-1次遞歸,所以空間復(fù)雜度為:O(n),因此,空間復(fù)雜度為:O(logn)~O(n)。

穩(wěn)定性:存在數(shù)據(jù)元素跳躍的問題,是不穩(wěn)定的排序。

排序類型:內(nèi)排。

7.歸并排序

先沒兩個數(shù)據(jù)元素歸并成一個有序的整體,然后有序的整體再兩兩歸并成一個更大的有序整體,直到歸并所有的數(shù)據(jù)元素,形成一個有序的整體。

 //歸并排序
 func mergeSort(sortedList: inout [Int]) {
    var result: [Int] = Array.init(repeating: 0, count: sortedList.count)
    sort(sortedList: &sortedList, result: &result, start: 0, end: sortedList.count-1)
 }

 func sort(sortedList: inout [Int], result: inout [Int], start: Int, end: Int) {
    var result2: [Int] = Array.init(repeating: 0, count: MAXSIZE)
    if start == end {
        result[start] = sortedList[start]
    } else {
        let m = (end+start)/2
        sort(sortedList: &sortedList, result: &result2, start: start, end: m)
        sort(sortedList: &sortedList, result: &result2, start: m+1, end: end)
        merge(left: &result2, right: &result, start: start, middle: m, end: end)
    }
 }

 func merge(left: inout [Int], right: inout [Int], start: Int, middle: Int, end: Int) {
    var i = start, j = middle+1, k = start
    while i <= middle && j <= end {
        if left[i] < left[j] {
            right[k] = left[i]
            i++
        } else {
            right[k] = left[j]
            j++
        }
        k++
    }

    if i <= middle {
        for l in 0...middle-i {
            right[k+l] = left[i+l]
        }
    }
    if j <= end {
        for l in 0...end-j {
            right[k+l] = left[j+l]
        }
    }
 }

時間復(fù)雜度:因?yàn)閮蓛蓺w并,其實(shí)就是一棵完全二叉樹,所以,最好和最壞的時間復(fù)雜度都是一樣的,二叉樹的深度為:logn,并且需要比較n次,所以為:O(nlogn)。

空間復(fù)雜度:需要n個額外的輔助空間存結(jié)果,并且需要遞歸logn次,所以空間復(fù)雜度為:O(n+logn),但是如果不采用遞歸,則需要:O(n)個空間。

穩(wěn)定性:因?yàn)閮蓚€有序的整體merge的時候并不涉及到數(shù)據(jù)的跳躍比較和移動,所以是穩(wěn)定的。

排序類型:外排,因?yàn)椴恍枰獎傞_始就把所有的數(shù)據(jù)加載進(jìn)內(nèi)存進(jìn)行排序。

8.計數(shù)排序

適用于整數(shù),分布均勻的數(shù)據(jù)。先找到整個數(shù)組最小和最大的整數(shù),然后生(max-min+1)長度的數(shù)組,遍歷整個數(shù)組,最小的放在第一位,最大的放在最后一位,其他數(shù)據(jù)的位置根據(jù)和最小數(shù)據(jù)的差值放置相應(yīng)小標(biāo)的位置,只需遍歷一遍就可以把整個數(shù)組的數(shù)據(jù)變成有序。

//計數(shù)排序
 func countSort(sortedList: inout [Int]) {
    let min = sortedList.min()!
    let max = sortedList.max()!
    var result = Array.init(repeating: 0, count: max-min+1)
        //排序
   for num in sortedList {
        result[num-min] = result[num-min]+1;
    }
    //打印
    var result2: [Int] = []
    for (index, value) in result.enumerated() {
        for _ in 0..<value {
            result2.append(index+min)
        }
    }
    print(result2)
 }

時間復(fù)雜度:排序時間復(fù)雜度為O(n),結(jié)果遍歷時間復(fù)雜度為O(k)(k為最大和最小的差值+1),所以,時間復(fù)雜度為:O(n+k)。

空間復(fù)雜度:因?yàn)樾枰粋€長度為k的數(shù)組接收結(jié)果,所以,空間復(fù)雜度為:O(k)。

穩(wěn)定性:因?yàn)閿?shù)據(jù)是一個個放進(jìn)去的,相同數(shù)字的前后順序是不變的,所以,是穩(wěn)定的排序算法。

排序類型:外排,如果直到數(shù)據(jù)都是在哪一個數(shù)據(jù)段的,并不需要把所有的數(shù)據(jù)加載進(jìn)內(nèi)存,一個一個數(shù)據(jù)或者一部分一部分?jǐn)?shù)據(jù)加載就可以了。

9.桶排序

計數(shù)排序的升級版,計數(shù)排序可以看作分成max-min+1個桶的排序。桶排序在計數(shù)排序的基礎(chǔ)上,將max-min+1的數(shù)據(jù)段再分成k個桶,每個桶就是一個數(shù)據(jù)段,所有的桶數(shù)據(jù)段不會重疊,并且所有桶的數(shù)據(jù)段連起來就是max-min+1,先將所有數(shù)據(jù)加入桶里,然后桶里的數(shù)據(jù)再采用其他排序使桶里的數(shù)據(jù)有序,然后將所有桶的數(shù)據(jù)連接起來就是整個有序序列。

//桶排序
 func bucketSort(sortedList: [Int]) -> [Int] {
    let max = sortedList.max()!
    let min = sortedList.min()!
    let bucketSize = 20
    let bucketCount = (max-min)/bucketSize+1
    var buckets = Array.init(repeating: [Int](), count: bucketCount)
    for num in sortedList {
        let i = (num-min)/bucketSize
        var bucket = buckets[i]
        bucket.append(num)
        buckets[i] = bucket //因?yàn)镾wift是用時復(fù)制,所以需要把bucket重新賦值回去
    }
    var result = [Int]()
    for var bucket in buckets {
        insertSort(sortedList: &bucket) //桶里采用插入排序
        result.append(contentsOf: bucket)
    }
    return result
 }

最好時間復(fù)雜度:整個數(shù)據(jù)均勻分布在n個桶里,時間復(fù)雜度為:O(n)。

最壞時間復(fù)雜度:所有數(shù)據(jù)都在一個桶里,則時間復(fù)雜度為:O(n2)。

平均時間復(fù)雜度:遍歷需要n遍,排序需要k遍,即時間復(fù)雜度為:O(n+km)(m和桶采取的排序算法有關(guān))。假如,桶采取的排序算法平均的時間復(fù)雜度為O(nlogn),則O(n+k(n/k)log(n/k))=O(n+n(logn-logk))=O(n+m)(m=n(logn-logk))。

空間復(fù)雜度:需要額外k個桶作為輔助,并且排序結(jié)果也需要n個位置儲存數(shù)據(jù),所以為:O(n+k)。

穩(wěn)定性:因?yàn)橥袄锏呐判蛴玫降氖遣迦肱判颍惺欠€(wěn)定的。

排序類型:外排。

10.基數(shù)排序

將數(shù)字按不同的數(shù)位比較,從低位到高位,位數(shù)不足的補(bǔ)零。也就是先按照各位排序,然后再按照十位排序,再按照百位排序...直到最高位。

 func radixSort(sortedList: [Int]) -> [Int] {
    let max = sortedList.max()!
    var result = Array.init(sortedList)
    var buckets = Array.init(repeating: [Int](), count: 10)
    let maxDigit = "\(max)".count
    for i in 0..<maxDigit {  //d
        let mod = (pow(10, i+1) as NSDecimalNumber).intValue
        for num in result { //n
            let j = num%mod/(mod/10)
            var bucket = buckets[j]
            bucket.append(num)
            buckets[j] = bucket
        }
        var index = 0
        for j in 0..<buckets.count {  //k
            let bucket = buckets[j]
            for k in 0..<bucket.count {
                result[index+k] = bucket[k]
            }
            index = index+bucket.count
            buckets[j] = []
        }
    }
    return result
 }

時間復(fù)雜度:由上面的代碼上面可以知道,時間復(fù)雜度和maxDigit有關(guān),并且遍歷整個數(shù)組需要n遍,然后將所有桶的數(shù)組按順序加入結(jié)果數(shù)組,所以,還需要加上遍歷桶的數(shù)據(jù)的次數(shù),則和k有關(guān)。所以,時間復(fù)雜度為:O(d(n+k)),k為基數(shù),比如十進(jìn)制k就是10。

空間復(fù)雜度:因?yàn)橛玫搅私Y(jié)果數(shù)據(jù)和基數(shù)個桶,所以為:O(n+k)。

穩(wěn)定性:穩(wěn)定的,因?yàn)椴粫淖兿嗤瑑蓚€數(shù)據(jù)的前后關(guān)系。

排序類型:外排。

友情鏈接:

Basic-Sorting-Algorithm
JS-Sorting-Algorithm

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容