データ構造とアルゴリズム(Golang実装)(25)ソートアルゴリズム-迅速なソート

クイックソート

クイックソートは、分割統治戦略のためのソートアルゴリズムであり、イギリスのコンピューターサイエンティストによって発明されましたTony Hoare。このアルゴリズムは19612010年にリリースされましたCommunications of the ACM 国际计算机学会月刊

注:ACM = Association for Computing Machinery1947年に設立された、コンピュータ実践者のための世界的な専門組織であるInternational Computer Societyは、世界で最初の科学的および教育的コンピュータ社会です。

クイックソートはバブルソートの改良版であり、エクスチェンジクラスのソートアルゴリズムにも属しています。

1.アルゴリズムの紹介

クイックソートでは、ソートするデータを1つのソートで2つの独立した部分に分割し、1つの部分のすべてのデータが他の部分のすべてのデータよりも小さい場合、このメソッドを使用して、データ全体の2つの部分をすばやくソートします。このプロセスは再帰的に実行できるため、データ全体が順序付けられたシーケンスになります。

手順は次のとおりです。

  1. 最初に、シーケンスから番号を参照番号として取得します。通常、最初の番号を取得します。
  2. パーティショニングプロセスでは、この数値より大きいすべての数値が右側に配置され、それ以下の数値が左側に配置されます。
  3. 各間隔に数値が1つだけになるまで、左と右の間隔について2番目の手順を繰り返します。

例:5 9 1 6 8 14 6 49 25 4 6 3

一般取第一个数 5 作为基准,从它左边和最后一个数使用[]进行标志,

如果左边的数比基准数大,那么该数要往右边扔,也就是两个[]数交换,这样大于它的数就在右边了,然后右边[]数左移,否则左边[]数右移。

5 [9] 1 6 8 14 6 49 25 4 6 [3]  因为 9 > 5,两个[]交换位置后,右边[]左移
5 [3] 1 6 8 14 6 49 25 4 [6] 9  因为 3 !> 5,两个[]不需要交换,左边[]右移
5 3 [1] 6 8 14 6 49 25 4 [6] 9  因为 1 !> 5,两个[]不需要交换,左边[]右移
5 3 1 [6] 8 14 6 49 25 4 [6] 9  因为 6 > 5,两个[]交换位置后,右边[]左移
5 3 1 [6] 8 14 6 49 25 [4] 6 9  因为 6 > 5,两个[]交换位置后,右边[]左移
5 3 1 [4] 8 14 6 49 [25] 6 6 9  因为 4 !> 5,两个[]不需要交换,左边[]右移
5 3 1 4 [8] 14 6 49 [25] 6 6 9  因为 8 > 5,两个[]交换位置后,右边[]左移
5 3 1 4 [25] 14 6 [49] 8 6 6 9  因为 25 > 5,两个[]交换位置后,右边[]左移
5 3 1 4 [49] 14 [6] 25 8 6 6 9  因为 49 > 5,两个[]交换位置后,右边[]左移
5 3 1 4 [6] [14] 49 25 8 6 6 9  因为 6 > 5,两个[]交换位置后,右边[]左移
5 3 1 4 [14] 6 49 25 8 6 6 9  两个[]已经汇总,因为 14 > 5,所以 5 和[]之前的数 4 交换位置
第一轮切分结果:4 3 1 5 14 6 49 25 8 6 6 9  

现在第一轮快速排序已经将数列分成两个部分:

4 3 1 和 14 6 49 25 8 6 6 9

左边的数列都小于 5,右边的数列都大于 5。

使用递归分别对两个数列进行快速排序。

クイックソートは、主に参照番号に基づいてシーケンスを2つの部分に分割します。1つの部分は参照番号よりも小さく、もう1つの部分は参照番号よりも大きくなります。

最良のケースでは、一方が各セグメントを平均化することができるので、長い要素が横切るようにn/2列の数は、2つの部分に分割することができる時間を、時間複雑さの各々について1つです:O(n)問題は、各バイナリのサイズであるため、列の数を半分にすると、次のように複雑さが計算される時間の合計である、再帰的分割を継続します:T(n) = 2*T(n/2) + O(n)主定理式の計算によれば、時間の複雑さは次のとおりであることがわかります。O(nlogn)もちろん、具体的に計算できます。

我们来分析最好情况,每次切分遍历元素的次数为 n/2

T(n) = 2*T(n/2) + n/2
T(n/2) = 2*T(n/4) + n/4
T(n/4) = 2*T(n/8) + n/8
T(n/8) = 2*T(n/16) + n/16
...
T(4) = 2*T(2) + 4
T(2) = 2*T(1) + 2
T(1) = 1

进行合并也就是:

T(n) = 2*T(n/2) + n/2
     = 2^2*T(n/4)+ n/2 + n/2
     = 2^3*T(n/8) + n/2 + n/2 + n/2
     = 2^4*T(n/16) + n/2 + n/2 + n/2 + n/2
     = ...
     = 2^logn*T(1) + logn * n/2
     = 2^logn + 1/2*nlogn
     = n + 1/2*nlogn

因为当问题规模 n 趋于无穷大时 nlogn 比 n 大,所以 T(n) = O(nlogn)。

最好时间复杂度为:O(nlogn)。

最悪の場合、毎回均等に分割することはできません。分割が最大または最小になるたびに、2つの系列に分割できないため、時間の複雑度はとなりT(n) = T(n-1) + O(n)、主定理計算により時間を知ることができます。複雑さはO(n^2)次のとおりです。実際に計算できます。

我们来分析最差情况,每次切分遍历元素的次数为 n

T(n) = T(n-1) + n
     = T(n-2) + n-1 + n
     = T(n-3) + n-2 + n-1 + n
     = ...
     = T(1) + 2 +3 + ... + n-2 + n-1 + n
     = O(n^2)

最差时间复杂度为:O(n^2)。

エントロピーの概念によれば、数値が大きいほどランダム性が高くなり、自然に無秩序になるため、ソートするデータのサイズが非常に大きい場合、最悪のケースは少なくなります。総合的なケースでは、クイックソートの平均時間計算量は次のとおりですO(nlogn)以前に紹介した並べ替えアルゴリズムと比較すると、正方形の基本的な並べ替えアルゴリズムよりもクイック並べ替えの方が優れています。

セグメンテーションの結果は、クイックソートのパフォーマンスに大きく影響します。不均一なセグメンテーションの発生を回避するには、いくつかの改善方法があります。

  1. クイックソートを実行するたびに、番号のシーケンスがランダムにシャッフルされてからセグメント化されます。これにより、ランダムな衝撃が加わり、不均一性が減少します。もちろん、最初の番号の代わりにランダムに参照番号を選択することができます。
  2. 毎回、シーケンスの先頭、中央、末尾に3つの数値を取り、3つの数値の中央値をセグメンテーションの参照番号として使用します。

方法1は比較的優れており、方法2は追加の比較演算を導入します。一般的に、参照番号をランダムに選択できます。

ソートクイックソート場所、収納スペースの複雑さ:O(1)再帰的スタックの影響により、再帰的プログラムスタックにはさまざまなレイヤーlogn~nがあるため、再帰的スタックのスペースの複雑さは次のとおりO(logn)~log(n)です。最悪の場合は、log(n)要素が多いと、プログラムスタックがオーバーフローする可能性があります。アルゴリズムを改善し、最適化に疑似テール再帰を使用することにより、再帰スタックのスペースの複雑さをO(logn)次のアルゴリズム最適化に減らすことができます

要素はセグメンテーション中に交換され、同じ値の要素が位置を変更する可能性があるため、クイックソートは不安定です。

2.アルゴリズムの実装

package main

import "fmt"

// 普通快速排序
func QuickSort(array []int, begin, end int) {
    if begin < end {
        // 进行切分
        loc := partition(array, begin, end)
        // 对左部分进行快排
        QuickSort(array, begin, loc-1)
        // 对右部分进行快排
        QuickSort(array, loc+1, end)
    }
}

// 切分函数,并返回切分元素的下标
func partition(array []int, begin, end int) int {
    i := begin + 1 // 将array[begin]作为基准数,因此从array[begin+1]开始与基准数比较!
    j := end       // array[end]是数组的最后一位

    // 没重合之前
    for i < j {
        if array[i] > array[begin] {
            array[i], array[j] = array[j], array[i] // 交换
            j--
        } else {
            i++
        }
    }

    /* 跳出while循环后,i = j。
     * 此时数组被分割成两个部分  -->  array[begin+1] ~ array[i-1] < array[begin]
     *                        -->  array[i+1] ~ array[end] > array[begin]
     * 这个时候将数组array分成两个部分,再将array[i]与array[begin]进行比较,决定array[i]的位置。
     * 最后将array[i]与array[begin]交换,进行两个分割部分的排序!以此类推,直到最后i = j不满足条件就退出!
     */
    if array[i] >= array[begin] { // 这里必须要取等“>=”,否则数组元素由相同的值组成时,会出现错误!
        i--
    }

    array[begin], array[i] = array[i], array[begin]
    return i
}

func main() {
    list := []int{5}
    QuickSort(list, 0, len(list)-1)
    fmt.Println(list)

    list1 := []int{5, 9}
    QuickSort(list1, 0, len(list1)-1)
    fmt.Println(list1)

    list2 := []int{5, 9, 1}
    QuickSort(list2, 0, len(list2)-1)
    fmt.Println(list2)

    list3 := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}
    QuickSort(list3, 0, len(list3)-1)
    fmt.Println(list3)
}

出力:

[5]
[5 9]
[1 5 9]
[1 3 4 5 6 6 6 8 9 14 25 49]

画像の例:

クイックソート、セグメンテーションごとに2つの添え字を維持、前進、そして最後にシーケンスを2つの部分に分割。

3、アルゴリズムの改善

クイックソートはアルゴリズムを改善し続けることができます。

  1. 小規模配列の場合、直接挿入ソートの効率が最も良く、クイックソートの再帰部分が小さい配列範囲に入ったら直接挿入ソートに切り替えることができます。
  2. 並べ替え順序に重複する値が多数含まれている可能性があります。3方向スライスを使用して、配列を3つの部分にすばやく並べ替えます。3つの部分は、参照番号より大きく、参照番号と等しく、参照番号より小さくなります。現時点では、3つの添え字を維持する必要があります。
  3. 疑似テール再帰を使用してプログラムスタックスペースの占有を減らし、スタックスペースの複雑さをO(logn)~log(n):からに変更ますO(logn)

3.1改善:小規模配列は直接挿入ソートを使用します

func QuickSort1(array []int, begin, end int) {
    if begin < end {
        // 当数组小于 4 时使用直接插入排序
        if end-begin <= 4 {
            InsertSort(array[begin : end+1])
            return
        }

        // 进行切分
        loc := partition(array, begin, end)
        // 对左部分进行快排
        QuickSort1(array, begin, loc-1)
        // 对右部分进行快排
        QuickSort1(array, loc+1, end)
    }
}

直接挿入による並べ替えは小規模な配列で非常に効率的です。end-begin <= 4再帰部分を直接挿入による並べ替え、つまり小さな配列の並べ替えに置き換えるだけで済みます

3.2改善:3方向セグメンテーション

package main

import "fmt"

// 三切分的快速排序
func QuickSort2(array []int, begin, end int) {
    if begin < end {
        // 三向切分函数,返回左边和右边下标
        lt, gt := partition3(array, begin, end)
        // 从lt到gt的部分是三切分的中间数列
        // 左边三向快排
        QuickSort2(array, begin, lt-1)
        // 右边三向快排
        QuickSort2(array, gt+1, end)
    }
}

// 切分函数,并返回切分元素的下标
func partition3(array []int, begin, end int) (int, int) {
    lt := begin       // 左下标从第一位开始
    gt := end         // 右下标是数组的最后一位
    i := begin + 1    // 中间下标,从第二位开始
    v := array[begin] // 基准数

    // 以中间坐标为准
    for i <= gt {
        if array[i] > v { // 大于基准数,那么交换,右指针左移
            array[i], array[gt] = array[gt], array[i]
            gt--
        } else if array[i] < v { // 小于基准数,那么交换,左指针右移
            array[i], array[lt] = array[lt], array[i]
            lt++
            i++
        } else {
            i++
        }
    }

    return lt, gt
}

デモ:

数列:4 8 2 4 4 4 7 9,基准数为 4

[4] [8] 2 4 4 4 7 [9]  从中间[]开始:8 > 4,中右[]进行交换,右边[]左移
[4] [9] 2 4 4 4 [7] 8  从中间[]开始:9 > 4,中右[]进行交换,右边[]左移
[4] [7] 2 4 4 [4] 9 8  从中间[]开始:7 > 4,中右[]进行交换,右边[]左移
[4] [4] 2 4 [4] 7 9 8  从中间[]开始:4 == 4,不需要交换,中间[]右移
[4] 4 [2] 4 [4] 7 9 8  从中间[]开始:2 < 4,中左[]需要交换,中间和左边[]右移
2 [4] 4 [4] [4] 7 9 8  从中间[]开始:4 == 4,不需要交换,中间[]右移
2 [4] 4 4 [[4]] 7 9 8  从中间[]开始:4 == 4,不需要交换,中间[]右移,因为已经重叠了
第一轮结果:2 4 4 4 4 7 9 8

分成三个数列:

2
4 4 4 4 (元素相同的会聚集在中间数列)
7 9 8

接着对第一个和最后一个数列进行递归即可。

画像の例:

3つのカットは、左側にある参照番号よりも小さい、右側にある参照番号よりも大きい、同じ要素が集約されます。

繰り返し要素が多数ある場合、同じ要素が途中で収集され、これらの要素が次の再帰的な反復に入ることがなくなるため、並べ替え速度が大幅に向上し、線形時間になります。

3ウェイセグメンテーションは、主にオランダの旗の3色の問題に由来しDijkstraます。

赤、白、青のフラグが付いたロープがあるとします。最初は、ロープのフラグの色が適切ではありません。それらを分類し、青、白、赤の順に並べたいとします。どのように移動しますか?ロープに対してのみこのアクションを実行でき、一度に2つのフラグのみを交換できることに注意してください。

あなたが見ることができる、上記の溶液は、カットの3分の1を使用することと等価である限り、我々は、フラグの値を設定するように白である100、ブルーフラッグの値がセットされ0、赤のフラグ値が設定される200100、参照番号、第1の三元分割後、3色のフラグが配置され蓝(0)白(100)红(200)ます。

注:Edsger Wybe Dijkstraコンピュータ科学者のオランダ人、Izger W. Dickescher(1930年5月11日〜2002年8月6日)は、チューリング賞を受賞しました。

3.3改善:疑似テール再帰最適化

// 伪尾递归快速排序
func QuickSort3(array []int, begin, end int) {
    for begin < end {
        // 进行切分
        loc := partition(array, begin, end)

        // 那边元素少先排哪边
        if loc-begin < end-loc {
            // 先排左边
            QuickSort3(array, begin, loc-1)
            begin = loc + 1
        } else {
            // 先排右边
            QuickSort3(array, loc+1, end)
            end = loc - 1
        }
    }
}

これは末尾再帰であると多くの人が考えています。実際、この高速ランク付け方法はfor、直接return QuickSortではなくループがあり、再帰がスタックを継続的に押し続けており、スタックレベルがまだ増加しているため、実際の末尾再帰ではなく、偽装テール再帰です。

ただし、小規模パーツが最初にソートされるため、スタックの深さが大幅に減少し、プログラムスタックの深さがlognレイヤーを超えないため、スタックの最悪のスペースの複雑さがにO(n)減少しO(logn)ます。

:この最適化は、層のスタックが長いなどとして、10億個の整数を注文するために、低減されるためだけでなく、最適化もありlog(100 0000 0000)=29.897、最高層のスタックによって占め30層、より最適化されていない、表示されることがO(n)はるかに優れた一定の層を。

4、補足:非再帰的書き込み

非再帰的書き込み方法は、以前の再帰的スタックをそれ自体が保持する手動スタックに変換することだけです。

// 非递归快速排序
func QuickSort5(array []int) {

    // 人工栈
    helpStack := new(LinkStack)

    // 第一次初始化栈,推入下标0,len(array)-1,表示第一次对全数组范围切分
    helpStack.Push(len(array) - 1)
    helpStack.Push(0)

    // 栈非空证明存在未排序的部分
    for !helpStack.IsEmpty() {
        // 出栈,对begin-end范围进行切分排序
        begin := helpStack.Pop() // 范围区间左边
        end := helpStack.Pop()   // 范围

        // 进行切分
        loc := partition(array, begin, end)

        // 右边范围入栈
        if loc+1 < end {
            helpStack.Push(end)
            helpStack.Push(loc + 1)
        }

        // 左边返回入栈
        if begin < loc-1 {
            helpStack.Push(loc - 1)
            helpStack.Push(begin)
        }
    }
}

再帰begin,endなしで最初に再帰的である必要がある配列の範囲は、独自の人工スタックに順番にプッシュされ、人工スタックはループで処理されます。

再帰なしでは、プログラムスタックスペースの複雑さは次のようになりますO(1)が、追加のストレージスペースが生成されます。

補助的な人工スタック構造helpStackは追加のスペースを占有し、保管スペースはその場でのソートO(1)から変更されO(logn)~log(n)ます。

私たちは、上記の擬似末尾再帰バージョンを参照することができ、メモリの複雑さを変更することができるように、スタックの短い範囲を聞かせて、非再帰的なバージョンを最適化するために引き続き:O(logn)など:

// 非递归快速排序优化
func QuickSort6(array []int) {

    // 人工栈
    helpStack := new(LinkStack)

    // 第一次初始化栈,推入下标0,len(array)-1,表示第一次对全数组范围切分
    helpStack.Push(len(array) - 1)
    helpStack.Push(0)

    // 栈非空证明存在未排序的部分
    for !helpStack.IsEmpty() {
        // 出栈,对begin-end范围进行切分排序
        begin := helpStack.Pop() // 范围区间左边
        end := helpStack.Pop()   // 范围

        // 进行切分
        loc := partition(array, begin, end)

        // 切分后右边范围大小
        rSize := -1
        // 切分后左边范围大小
        lSize := -1

        // 右边范围入栈
        if loc+1 < end {
            rSize = end - (loc + 1)
        }

        // 左边返回入栈
        if begin < loc-1 {
            lSize = loc - 1 - begin
        }

        // 两个范围,让范围小的先入栈,减少人工栈空间
        if rSize != -1 && lSize != -1 {
            if lSize > rSize {
                helpStack.Push(end)
                helpStack.Push(loc + 1)
                helpStack.Push(loc - 1)
                helpStack.Push(begin)
            } else {
                helpStack.Push(loc - 1)
                helpStack.Push(begin)
                helpStack.Push(end)
                helpStack.Push(loc + 1)
            }
        } else {
            if rSize != -1 {
                helpStack.Push(end)
                helpStack.Push(loc + 1)
            }

            if lSize != -1 {
                helpStack.Push(loc - 1)
                helpStack.Push(begin)
            }
        }
    }
}

完全な手順は次のとおりです。

package main

import (
    "fmt"
    "sync"
)

// 链表栈,后进先出
type LinkStack struct {
    root *LinkNode  // 链表起点
    size int        // 栈的元素数量
    lock sync.Mutex // 为了并发安全使用的锁
}

// 链表节点
type LinkNode struct {
    Next  *LinkNode
    Value int
}

// 入栈
func (stack *LinkStack) Push(v int) {
    stack.lock.Lock()
    defer stack.lock.Unlock()

    // 如果栈顶为空,那么增加节点
    if stack.root == nil {
        stack.root = new(LinkNode)
        stack.root.Value = v
    } else {
        // 否则新元素插入链表的头部
        // 原来的链表
        preNode := stack.root

        // 新节点
        newNode := new(LinkNode)
        newNode.Value = v

        // 原来的链表链接到新元素后面
        newNode.Next = preNode

        // 将新节点放在头部
        stack.root = newNode
    }

    // 栈中元素数量+1
    stack.size = stack.size + 1
}

// 出栈
func (stack *LinkStack) Pop() int {
    stack.lock.Lock()
    defer stack.lock.Unlock()

    // 栈中元素已空
    if stack.size == 0 {
        panic("empty")
    }

    // 顶部元素要出栈
    topNode := stack.root
    v := topNode.Value

    // 将顶部元素的后继链接链上
    stack.root = topNode.Next

    // 栈中元素数量-1
    stack.size = stack.size - 1

    return v
}

// 栈是否为空
func (stack *LinkStack) IsEmpty() bool {
    return stack.size == 0
}

// 非递归快速排序
func QuickSort5(array []int) {

    // 人工栈
    helpStack := new(LinkStack)

    // 第一次初始化栈,推入下标0,len(array)-1,表示第一次对全数组范围切分
    helpStack.Push(len(array) - 1)
    helpStack.Push(0)

    // 栈非空证明存在未排序的部分
    for !helpStack.IsEmpty() {
        // 出栈,对begin-end范围进行切分排序
        begin := helpStack.Pop() // 范围区间左边
        end := helpStack.Pop()   // 范围

        // 进行切分
        loc := partition(array, begin, end)

        // 右边范围入栈
        if loc+1 < end {
            helpStack.Push(end)
            helpStack.Push(loc + 1)
        }

        // 左边返回入栈
        if begin < loc-1 {
            helpStack.Push(loc - 1)
            helpStack.Push(begin)
        }
    }
}

// 非递归快速排序优化
func QuickSort6(array []int) {

    // 人工栈
    helpStack := new(LinkStack)

    // 第一次初始化栈,推入下标0,len(array)-1,表示第一次对全数组范围切分
    helpStack.Push(len(array) - 1)
    helpStack.Push(0)

    // 栈非空证明存在未排序的部分
    for !helpStack.IsEmpty() {
        // 出栈,对begin-end范围进行切分排序
        begin := helpStack.Pop() // 范围区间左边
        end := helpStack.Pop()   // 范围

        // 进行切分
        loc := partition(array, begin, end)

        // 切分后右边范围大小
        rSize := -1
        // 切分后左边范围大小
        lSize := -1

        // 右边范围入栈
        if loc+1 < end {
            rSize = end - (loc + 1)
        }

        // 左边返回入栈
        if begin < loc-1 {
            lSize = loc - 1 - begin
        }

        // 两个范围,让范围小的先入栈,减少人工栈空间
        if rSize != -1 && lSize != -1 {
            if lSize > rSize {
                helpStack.Push(end)
                helpStack.Push(loc + 1)
                helpStack.Push(loc - 1)
                helpStack.Push(begin)
            } else {
                helpStack.Push(loc - 1)
                helpStack.Push(begin)
                helpStack.Push(end)
                helpStack.Push(loc + 1)
            }
        } else {
            if rSize != -1 {
                helpStack.Push(end)
                helpStack.Push(loc + 1)
            }

            if lSize != -1 {
                helpStack.Push(loc - 1)
                helpStack.Push(begin)
            }
        }
    }
}

// 切分函数,并返回切分元素的下标
func partition(array []int, begin, end int) int {
    i := begin + 1 // 将array[begin]作为基准数,因此从array[begin+1]开始与基准数比较!
    j := end       // array[end]是数组的最后一位

    // 没重合之前
    for i < j {
        if array[i] > array[begin] {
            array[i], array[j] = array[j], array[i] // 交换
            j--
        } else {
            i++
        }
    }

    /* 跳出while循环后,i = j。
     * 此时数组被分割成两个部分  -->  array[begin+1] ~ array[i-1] < array[begin]
     *                        -->  array[i+1] ~ array[end] > array[begin]
     * 这个时候将数组array分成两个部分,再将array[i]与array[begin]进行比较,决定array[i]的位置。
     * 最后将array[i]与array[begin]交换,进行两个分割部分的排序!以此类推,直到最后i = j不满足条件就退出!
     */
    if array[i] >= array[begin] { // 这里必须要取等“>=”,否则数组元素由相同的值组成时,会出现错误!
        i--
    }

    array[begin], array[i] = array[i], array[begin]
    return i
}

func main() {
    list3 := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}
    QuickSort5(list3)
    fmt.Println(list3)

    list4 := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}
    QuickSort6(list4)
    fmt.Println(list4)
}

出力:

[1 3 4 5 6 6 6 8 9 14 25 49]
[1 3 4 5 6 6 6 8 9 14 25 49]

再帰的プログラムスタックの代わりに人工スタックが使用され、速度に変化はありませんが、コードの可読性が低下します。

5.補足:組み込みライブラリがクイックソートを使用する理由

まず、ヒープソート、マージソート、最悪および最悪の時間の複雑さは次のとおりです。O(nlogn)クイックソート、最悪の時間の複雑さは次のとおりです。O(n^2)しかし、多くのプログラミング言語の組み込みのソートアルゴリズムは、まだクイックソートを使用しています。なぜですか?

  1. この問題には偏りがあります。並べ替えアルゴリズムの選択は、特定のシナリオによって異なります。Linuxカーネルで使用される並べ替えアルゴリズムは、ヒープの並べ替えです。Java多数の複雑なオブジェクトを並べ替える場合、組み込みの並べ替えはマージ並べ替えを使用しますが、通常、クイック並べ替えの方が高速です。 。
  2. マージソートには2つの安定性があります。最初の安定性はソートの前後で同じ要素の位置です。2番目の安定性はソートが非常に平均的であるたびに、読み取りデータも順次読み取られ、メモリキャッシュを使用できることです。ディスクからデータを読み取ってソートするなどの機能。並べ替え処理には追加の補助配列スペースが必要になるため、この部分にはコストがかかりますが、インプレース手動マージ並べ替えはこの欠陥を克服します。
  3. 複雑、大規模なO各ヒープソート後に撮影された最大値は、定数項が省略されている、スタックを復元するために逆にするノードの必要性は、無駄な努力の多くを備えて、定数項は、ほとんどの場合、クイックソートよりも大きく、ダウンはクイックソートよりもはるかに低速です。ただし、ヒープのソート時間は比較的安定しており、最悪のO(n^2)場合の高速ソートは発生せず、スペースを節約し、追加のストレージスペースとスタックスペースを必要としません。
  4. ソートする数値が16000要素を超える場合、ボトムアップヒープソートを使用すると、クイックソートよりも高速になります。https//core.ac.uk/download/pdf/82350265.pdfを参照してください
  5. 最悪の場合のクイックソートの複雑さは、主にセグメンテーションがマージソートのように平均化されないために高くなりますが、ベース数に大きく依存します。これで、乱数、3つのカットなど、この最悪のケースの確率が改善されました大幅に削減されました。ほとんどの場合、それほど悪くはありません。それらのほとんどは実際のブロックです。
  6. マージソートとクイックソートはどちらも分割統治法であり、ソートされたデータは隣接しており、ヒープソート比較の数が広い範囲に及ぶ可能性があり、その結果ローカルヒット率が低下し、最新のメモリキャッシュの特性を使用してデータをロードできませんプロセスはパフォーマンスを失います。

安定性が必要な場合は、並べ替えの前後で同じ要素の位置を変更せず、マージされた並べ替えを使用できますJava。複雑なオブジェクトタイプでは、並べ替えの前後の位置を変更できないようにする必要があります。マージソートを使用します。

スタックと記憶域の要件については、ヒープの並べ替えを使用できます。たとえば、Linuxカーネルスタックは小さく、クイック並べ替えはプログラムスタックの多くを占めます。クイック並べ替えを使用すると、スタックオーバーフローが発生する可能性があるため、ヒープ並べ替えが使用されます。

ではGolangsortスライスは標準ライブラリで安定してソートされます

func SliceStable(slice interface{}, less func(i, j int) bool) {
    rv := reflectValueOf(slice)
    swap := reflectSwapper(slice)
    stable_func(lessSwap{less, swap}, rv.Len())
}

func stable_func(data lessSwap, n int) {
    blockSize := 20
    a, b := 0, blockSize
    for b <= n {
        insertionSort_func(data, a, b)
        a = b
        b += blockSize
    }
    insertionSort_func(data, a, n)
    for blockSize < n {
        a, b = 0, 2*blockSize
        for b <= n {
            symMerge_func(data, a, a+blockSize, b)
            a = b
            b += 2 * blockSize
        }
        if m := a + blockSize; m < n {
            symMerge_func(data, a, m, n)
        }
        blockSize *= 2
    }
}

最初に20、小さな配列の挿入と並べ替えの効率が高いため、スライスセグメント全体が要素の範囲に従って挿入および並べ替えられます。次に、これらの並べ替えられた小さな配列がマージされて並べ替えられます。マージソートは、その場でのソートも使用し、補助スペースを節約します。

そして一般的な種類:

func Slice(slice interface{}, less func(i, j int) bool) {
    rv := reflectValueOf(slice)
    swap := reflectSwapper(slice)
    length := rv.Len()
    quickSort_func(lessSwap{less, swap}, 0, length, maxDepth(length))
}

func quickSort_func(data lessSwap, a, b, maxDepth int) {
    for b-a > 12 {
        if maxDepth == 0 {
            heapSort_func(data, a, b)
            return
        }
        maxDepth--
        mlo, mhi := doPivot_func(data, a, b)
        if mlo-a < b-mhi {
            quickSort_func(data, a, mlo, maxDepth)
            a = mhi
        } else {
            quickSort_func(data, mhi, b, maxDepth)
            b = mlo
        }
    }
    if b-a > 1 {
        for i := a + 6; i < b; i++ {
            if data.Less(i, i-6) {
                data.Swap(i, i-6)
            }
        }
        insertionSort_func(data, a, b)
    }
}

func doPivot_func(data lessSwap, lo, hi int) (midlo, midhi int) {
    m := int(uint(lo+hi) >> 1)
    if hi-lo > 40 {
        s := (hi - lo) / 8
        medianOfThree_func(data, lo, lo+s, lo+2*s)
        medianOfThree_func(data, m, m-s, m+s)
        medianOfThree_func(data, hi-1, hi-1-s, hi-1-2*s)
    }
    medianOfThree_func(data, lo, m, hi-1)
    pivot := lo
    a, c := lo+1, hi-1
    for ; a < c && data.Less(a, pivot); a++ {
    }
    b := a
    for {
        for ; b < c && !data.Less(pivot, b); b++ {
        }
        for ; b < c && data.Less(pivot, c-1); c-- {
        }
        if b >= c {
            break
        }
        data.Swap(b, c-1)
        b++
        c--
    }
    protect := hi-c < 5
    if !protect && hi-c < (hi-lo)/4 {
        dups := 0
        if !data.Less(pivot, hi-1) {
            data.Swap(c, hi-1)
            c++
            dups++
        }
        if !data.Less(b-1, pivot) {
            b--
            dups++
        }
        if !data.Less(m, pivot) {
            data.Swap(m, b-1)
            b--
            dups++
        }
        protect = dups > 1
    }
    if protect {
        for {
            for ; a < b && !data.Less(b-1, pivot); b-- {
            }
            for ; a < b && data.Less(a, pivot); a++ {
            }
            if a >= b {
                break
            }
            data.Swap(a, b-1)
            a++
            b--
        }
    }
    data.Swap(pivot, b-1)
    return b - 1, c
}

クイックソートは、プログラムスタックのレイヤー数を制限します。2*ceil(log(n+1))再帰がこのレイヤーを超える場合、プログラムスタックが深すぎることを意味し、ヒープソートに切り替えます。

上記のクイックソートでは、3つの最適化も使用しています。1つ目は、再帰時に小さな配列を挿入ソートに変換すること、2つ目は中央参照番号を使用すること、3つ目は3分割を使用することです。

シリーズ記事エントリー

私は、スター陳思い、私が個人的に書かれているようこそ(Golangが達成)のデータ構造とアルゴリズムの記事で始まる、より親しみやすいGitBookを読むために

おすすめ

転載: www.cnblogs.com/nima/p/12724868.html