JDK閱讀筆記 -- TimSort

引子

TimSort是一種穩(wěn)定的、自適應(yīng)的變種歸并排序。當(dāng)被應(yīng)用在部分有序的數(shù)組排序問題時,TimSort有遠(yuǎn)好于O(NlgN)的時間性能;而在最差情況下,TimSort也能保持與傳統(tǒng)歸并排序相近的表現(xiàn)。
TimSort最早由Tim Peters實現(xiàn)在Python的list sort中,從JDK 1.7開始被引入并成為Java中Arrays的默認(rèn)排序算法。

原理

歸并排序的核心思路是先將原數(shù)組分割成長度為1的子序列組,再遞歸地將兩個相鄰的子序列合并成一個序列。傳統(tǒng)歸并排序完全忽視了原數(shù)組中的順序關(guān)系,因而在最好和最壞情況下都擁有O(NlgN)的時間復(fù)雜度。反過來,利用原數(shù)組中的序列就成了優(yōu)化歸并排序的一個很好的切入點。
下面這幾幅圖展示了一個較為簡單直觀的思路:依次尋找所給數(shù)組中盡量長的序列(升序列,或降序列經(jīng)反轉(zhuǎn)形成的升序列),將這些序列做歸并操作。











然而,稍加思考后不難發(fā)現(xiàn),如果每次都將新發(fā)現(xiàn)的升序列直接與已排序好的部分進(jìn)行歸并,只能帶來O(N^2)的時間性能(別問,畫圖欠考慮又不想改了)。即使我們選擇了好的歸并策略,其性能提升能否抵消以及超過花費在尋找升序列上的開銷也仍存疑,尤其是當(dāng)找到的升序列普遍比較短的時候。所以,讓我們觀察下JDK中的TimSort實現(xiàn)是如何解決這些問題的。

源碼分析

二分排序:

此函數(shù)是在TimSort主循環(huán)中被調(diào)用到的一個方法,由于其作用和邏輯都相對獨立,因此可以放在最前面單獨進(jìn)行分析

private static <T> void binarySort(T[] a, int lo, int hi, int start,
                                   Comparator<? super T> c) {
    // lo和hi分別是當(dāng)前所操作數(shù)組的低索引和高索引,同名參數(shù)在此文件很多函數(shù)中都有用到
    // 此函數(shù)中保證:lo <= start <= hi
    // 此外根據(jù)主循環(huán)對此函數(shù)的調(diào)用可以知道,數(shù)組a中[lo, start)內(nèi)的元素是保證升序的,[start, hi)內(nèi)的元素是默認(rèn)無序的
    assert lo <= start && start <= hi;
    if (start == lo)
        start++;
    // 大致思路就是遍歷[start, hi)中的元素,分別插入[lo, start)中
    for ( ; start < hi; start++) {
        T pivot = a[start];

        int left = lo;
        int right = start;
        assert left <= right;
        
        // 二分查找的循環(huán)實現(xiàn)
        while (left < right) {
            int mid = (left + right) >>> 1;
            if (c.compare(pivot, a[mid]) < 0)
                right = mid;
            else
                left = mid + 1;
        }
        assert left == right;

        // 根據(jù)當(dāng)前元素索引到目標(biāo)索引的距離,選擇移動策略
        int n = start - left;  
        switch (n) {
            // 減少對arraycopy()的調(diào)用,挺巧妙的
            case 2:  a[left + 2] = a[left + 1];
            case 1:  a[left + 1] = a[left];
                     break;
            default: System.arraycopy(a, left, a, left + 1, n);
        }
        a[left] = pivot;
    }
}
私有變量:
private static final int MIN_MERGE = 32;  //使用TimSort的閥值,長度小于閥值的數(shù)組使用傳統(tǒng)歸并排序

private final T[] a;  //待排序數(shù)組

private final Comparator<? super T> c;  //比較器

private static final int  MIN_GALLOP = 7;   //進(jìn)入Gallop模式的閥值
private int minGallop = MIN_GALLOP;

//用于歸并操作的臨時數(shù)組的相關(guān)變量
private static final int INITIAL_TMP_STORAGE_LENGTH = 256;
private T[] tmp;
private int tmpBase; 
private int tmpLen;  

//用于保存升序列的棧
//在算法運行時保證:runBase[i] + runLen[i] == runBase[i+1]
private int stackSize = 0; 
private final int[] runBase;   //每個升序列頭在原數(shù)組中的索引
private final int[] runLen;    //每個升序列的長度
工廠方法:
static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
                     T[] work, int workBase, int workLen) {
    assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;

    int nRemaining  = hi - lo;
    if (nRemaining < 2)
        return;

    // 若數(shù)組長度小于閥值,將不適用TimSort
    if (nRemaining < MIN_MERGE) {
        // 嘗試一次尋找升序列
        // 這里binarySort的時間復(fù)雜度是O((hi - initRunLen) * lg(hi - lo))
        // 因此所找到起始升序列的長度對總體運行時間的影響明顯
        int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
        binarySort(a, lo, hi, lo + initRunLen, c);
        return;
    }

    // 正式進(jìn)入TimSort的邏輯,首先調(diào)用私有的構(gòu)造器
    TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);
    int minRun = minRunLength(nRemaining);
    do {
        // 尋找下一個序列
        int runLen = countRunAndMakeAscending(a, lo, hi, c);

        // 如果這個序列長度小于閥值minRun,會將后續(xù)minRun個元素一起做二分排序
        // minRun的取值策略很有趣,后面會詳細(xì)討論
        if (runLen < minRun) {
            // 在主循環(huán)的最后一趟,剩余元素可能不夠minRun個了,就直接將所有元素做二分排序
            int force = nRemaining <= minRun ? nRemaining : minRun;
            binarySort(a, lo, lo + force, lo + runLen, c);
            runLen = force;
        }

        ts.pushRun(lo, runLen); //將此序列壓入棧
        ts.mergeCollapse();     //根據(jù)棧頂若干序列的長度關(guān)系,嘗試清棧

        // 更新索引
        lo += runLen;
        nRemaining -= runLen;
    } while (nRemaining != 0);

    // 主循環(huán)結(jié)束時,完成強制清棧等收尾工作
    assert lo == hi;
    ts.mergeForceCollapse();
    assert ts.stackSize == 1;
}
構(gòu)造器:
private TimSort(T[] a, Comparator<? super T> c, T[] work, int workBase, int workLen) {
    this.a = a;
    this.c = c;

    // 為歸并操作分配空間,大致為原數(shù)組的一半
    // 可能根據(jù)需要,在其他函數(shù)中被修改
    int len = a.length;
    int tlen = (len < 2 * INITIAL_TMP_STORAGE_LENGTH) ?
        len >>> 1 : INITIAL_TMP_STORAGE_LENGTH;

    // 如果從IDE里查看usage的話, 會發(fā)現(xiàn)絕大部分傳入的work參數(shù)都是null, 從而進(jìn)入這個分支
    if (work == null || workLen < tlen || workBase + tlen > work.length) {
        @SuppressWarnings({"unchecked", "UnnecessaryLocalVariable"})
        T[] newArray = (T[])java.lang.reflect.Array.newInstance
            (a.getClass().getComponentType(), tlen);
        tmp = newArray;
        tmpBase = 0;
        tmpLen = tlen;
    }
    else {
        tmp = work;
        tmpBase = workBase;
        tmpLen = workLen;
    }
    
    // 根據(jù)原數(shù)組長度,為序列棧分配初始空間
    // 真·魔數(shù),難以揣測具體數(shù)值來源
    // 值得一提的是,用這種方法寫長串if-else邏輯個人還是第一次見,學(xué)到了
    int stackLen = (len <    120  ?  5 :
                    len <   1542  ? 10 :
                    len < 119151  ? 24 : 40);
    runBase = new int[stackLen];
    runLen = new int[stackLen];
}
尋找升序列:
// 此函數(shù)嘗試在給定的數(shù)組段中,從最低位開始,尋找一個升序列
// 或?qū)ふ乙粋€降序列,并原地反轉(zhuǎn)成升序列
// 返回值是所找到升序列的長度
private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,
                                                Comparator<? super T> c) {
    assert lo < hi;
    int runHi = lo + 1;
    if (runHi == hi)
        return 1;

    // 根據(jù)前兩個元素的大小關(guān)系來判斷下個序列是升序或降序
    if (c.compare(a[runHi++], a[lo]) < 0) { // 降序
        while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
            runHi++;
        reverseRange(a, lo, runHi);  // 原地反轉(zhuǎn)
    } else {                              // 升序
        while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
            runHi++;
    }

    return runHi - lo;
}
原地反轉(zhuǎn):
// 沒有什么好說的
private static void reverseRange(Object[] a, int lo, int hi) {
    hi--;
    while (lo < hi) {
        Object t = a[lo];
        a[lo++] = a[hi];
        a[hi--] = t;
    }
}
最短序列長度閥值生成函數(shù)(姑且這么描述 吧。。):
// 注意,這個函數(shù)在一次TimSort生命周期中只被調(diào)用一次
// 入?yún)⑹窃瓟?shù)組總長度
private static int minRunLength(int n) {
    assert n >= 0;
    int r = 0; 
    // 首先,當(dāng) n < MIN_MERGE 時,返回 n
    // (實際上這種情況下,這個函數(shù)根本調(diào)用不到)
    // 若 n 為 2 的冪,那么 r 最終會為 0,返回結(jié)果是 MIN_MERGE / 2
    // 在剩余情況下,返回值 k 落在 [MIN_MERGE/2, MIN_MERGE] 間
    // 同時保證 n / k 接近且嚴(yán)格小于一個 2 的冪
    while (n >= MIN_MERGE) {
        r |= (n & 1);
        n >>= 1;
    }
    return n + r;
}
序列壓棧操作:
// 見私有變量中的注釋描述
private void pushRun(int runBase, int runLen) {
    this.runBase[stackSize] = runBase;
    this.runLen[stackSize] = runLen;
    stackSize++;
}
棧內(nèi)歸并:
// 此方法的作用是將序列棧中的第 i 和 第 i + 1 個序列歸并成一個序列,
// 并放在第 i 個位置上
private void mergeAt(int i) {
    assert stackSize >= 2;
    assert i >= 0;
    // 注意 i 必須是棧內(nèi)倒數(shù)第二個或倒數(shù)第三個序列
    assert i == stackSize - 2 || i == stackSize - 3;

    int base1 = runBase[i];
    int len1 = runLen[i];
    int base2 = runBase[i + 1];
    int len2 = runLen[i + 1];
    assert len1 > 0 && len2 > 0;
    assert base1 + len1 == base2;

    // 更新棧的狀態(tài)
    runLen[i] = len1 + len2;
    // 如果準(zhǔn)備進(jìn)行歸并的是倒數(shù)第二和倒數(shù)第三的序列,那么還需要維護(hù)倒數(shù)第一序列的狀態(tài)
    if (i == stackSize - 3) {
        runBase[i + 1] = runBase[i + 2];
        runLen[i + 1] = runLen[i + 2];
    }
    stackSize--;

    // 下面就是正式的歸并操作邏輯
    // 目前我們有序列 1: [base1, base1 + len1) 和序列 2 : [base2, base2 + len2)
    // 其中 base1 + len1 == base2
    // 首先嘗試在序列 1 中尋找一個索引 base1 + k
    // 使得 [base1 + k, base1 + len1) 中的每一個元素都比序列 2 中的元素小
    // 從而減少實際需進(jìn)行歸并操作的序列長度
    int k = gallopRight(a[base2], a, base1, len1, 0, c);
    assert k >= 0;
    base1 += k;
    len1 -= k;
    if (len1 == 0)
        return;
    // 與上一段類似,在序列 2 中尋找索引 base2 + k
    // 使得 [base2 + k, base2 + len2) 中每一個元素都比序列 1 中的元素大
    len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
    assert len2 >= 0;
    if (len2 == 0)
        return;

    // 將剪枝后的兩個序列進(jìn)行歸并,以長度較短的序列為基底
    if (len1 <= len2)
        mergeLo(base1, len1, base2, len2);
    else
        mergeHi(base1, len1, base2, len2);
}
序列內(nèi)查找索引:
// 在序列內(nèi)查找一個與給定元素值最接近的索引,直接想法應(yīng)該是二分
// 這個函數(shù)實現(xiàn)也是基于二分搜索的,同時又增加了一些啟發(fā)式的邏輯
// 比如 hint 這個參數(shù),根據(jù)原注釋,如果傳入時越接近目標(biāo)索引,此函數(shù)運行越快
private static <T> int gallopLeft(T key, T[] a, int base, int len, int hint,
                                  Comparator<? super T> c) {
    assert len > 0 && hint >= 0 && hint < len;
    // 在經(jīng)典二分查找開始前,先嘗試縮小查找范圍
    int lastOfs = 0;
    int ofs = 1;
    // 大致思路是依次嘗試 [base, base+1), [base+1, base+3), [base+3, base+7)... 等區(qū)間
    // 同時根據(jù) hint 的值,對區(qū)間上下界進(jìn)行偏移 
    if (c.compare(key, a[base + hint]) > 0) {
        int maxOfs = len - hint;
        while (ofs < maxOfs && c.compare(key, a[base + hint + ofs]) > 0) {
            lastOfs = ofs;
            ofs = (ofs << 1) + 1;
            if (ofs <= 0)   
                ofs = maxOfs;
        }
        if (ofs > maxOfs)
            ofs = maxOfs;

        lastOfs += hint;
        ofs += hint;
    } else { 
        final int maxOfs = hint + 1;
        while (ofs < maxOfs && c.compare(key, a[base + hint - ofs]) <= 0) {
            lastOfs = ofs;
            ofs = (ofs << 1) + 1;
            if (ofs <= 0)   
                ofs = maxOfs;
        }
        if (ofs > maxOfs)
            ofs = maxOfs;

        int tmp = lastOfs;
        lastOfs = hint - ofs;
        ofs = hint - tmp;
    }
    assert -1 <= lastOfs && lastOfs < ofs && ofs <= len;

    // 在區(qū)間 [base + lastOfs, base + ofs) 上進(jìn)行查找,經(jīng)典的二分循環(huán)實現(xiàn)
    // 個人認(rèn)為讀此方法的代碼時,可以從這里開始,然后帶著對 lastOfs 和 ofs 的問題回頭讀前半部分
    lastOfs++;
    while (lastOfs < ofs) {
        int m = lastOfs + ((ofs - lastOfs) >>> 1);

        if (c.compare(key, a[base + m]) > 0)
            lastOfs = m + 1;  
        else
            ofs = m;          
    }
    assert lastOfs == ofs;   
    return ofs;
}

// 與 gallopLeft 相對稱
private static <T> int gallopRight(T key, T[] a, int base, int len,
                                   int hint, Comparator<? super T> c); 
歸并:
private void mergeLo(int base1, int len1, int base2, int len2) {
    assert len1 > 0 && len2 > 0 && base1 + len1 == base2;

    // 出于性能考慮,將類中的對象用本地變量形式聲明出來,這種用法出現(xiàn)多次
    T[] a = this.a; 
    T[] tmp = ensureCapacity(len1);
    // 聲明游標(biāo) cursor1 和 cursor2 分別指向兩個序列頭部
    int cursor1 = tmpBase; 
    int cursor2 = base2;   
    int dest = base1;      
    System.arraycopy(a, base1, tmp, cursor1, len1);

    // 對一些極端情況的處理
    a[dest++] = a[cursor2++];
    if (--len2 == 0) {
        System.arraycopy(tmp, cursor1, a, dest, len1);
        return;
    }
    if (len1 == 1) {
        System.arraycopy(a, cursor2, a, dest, len2);
        a[dest + len2] = tmp[cursor1]; 
        return;
    }

    Comparator<? super T> c = this.c;  
    int minGallop = this.minGallop;
    // 以下是個人認(rèn)為整個TimSort中最能體現(xiàn)“啟發(fā)性”的一段邏輯   
outer:
    while (true) {
        // 一次元素比較中,某個序列派出的元素較小,定義為這個序列“贏”了一次
        int count1 = 0; // 序列 1 贏的次數(shù)
        int count2 = 0; // 序列 2 贏的次數(shù)
      
        // 在下面的循環(huán)中,如果先不管 count1 和 count2, 那就是經(jīng)典的歸并操作邏輯
        do {
            assert len1 > 1 && len2 > 0;
            if (c.compare(a[cursor2], tmp[cursor1]) < 0) {
                a[dest++] = a[cursor2++];
                // 注意到若一個序列開始贏了,就將另一個序列贏的計數(shù)清零
                // 因此 count1 和 count2 運行時實際代表了對應(yīng)序列連續(xù)贏的次數(shù)
                count2++;
                count1 = 0;
                if (--len2 == 0)
                    break outer;
            } else {
                a[dest++] = tmp[cursor1++];
                count1++;
                count2 = 0;
                if (--len1 == 1)
                    break outer;
            }
        } while ((count1 | count2) < minGallop);

       // 因此當(dāng)某一序列連續(xù)贏的次數(shù)超過了 minGallop 這個閥值,就開始進(jìn)入 gallop 模式
       // 在此模式下,不再逐元素地比較大小后移動游標(biāo)
       // 而是基于序列1中的某個元素可能比序列2中的一大段元素都要小的預(yù)測(或者反過來,類似)
       // 調(diào)用 gallopRight 方法,用 O(lgN) 的速度找到序列 1 中這個元素應(yīng)當(dāng)出現(xiàn)在序列 2 中的位置
       // 其目的是加速歸并過程
        do {
            assert len1 > 1 && len2 > 0;
            count1 = gallopRight(a[cursor2], tmp, cursor1, len1, 0, c);
            // 我們已經(jīng)知道 gallopRight 返回的是索引的偏移量
            // 在此循環(huán)的邏輯下,此返回值也恰好是序列 1 連續(xù)贏的次數(shù)
            if (count1 != 0) {
                System.arraycopy(tmp, cursor1, a, dest, count1);
                dest += count1;
                cursor1 += count1;
                len1 -= count1;
                if (len1 <= 1) 
                    break outer;
            }
            a[dest++] = a[cursor2++];
            if (--len2 == 0)
                break outer;

            count2 = gallopLeft(tmp[cursor1], a, cursor2, len2, 0, c);
            if (count2 != 0) {
                System.arraycopy(a, cursor2, a, dest, count2);
                dest += count2;
                cursor2 += count2;
                len2 -= count2;
                if (len2 == 0)
                    break outer;
            }
            a[dest++] = tmp[cursor1++];
            if (--len1 == 1)
                break outer;
            // 下面這一段對 minGallop 的調(diào)整很有趣
            // 注意到 minGallop 決定了能否進(jìn)入 gallop 模式, 而 MIN_GALLOP 決定了能否保持在 gallop 模式
            // 個人認(rèn)為可以跟人的記憶過程做個類比,比如記單詞
            // 設(shè)想一名同學(xué)正在備考GRE,他將每天的任務(wù)分成組
            // 其中第一組任務(wù)是復(fù)習(xí),其他組是學(xué)習(xí)新單詞
            // 學(xué)習(xí)一遍所需的時間固定為 MIN_GALLOP, 復(fù)習(xí)所需時間不固定
            // 一般而言,基于遺忘規(guī)律,復(fù)習(xí)所需時間會隨天數(shù)逐漸變長(minGallop += 2;)
            // 同時,今天這名同學(xué)在這些單詞上每多學(xué)習(xí)一遍,第二天復(fù)習(xí)所需要的時間就減少一小時
            // (這樣類比其實有硬傷,今天多花了7小時才為明天省了1小時,有點跑步長壽的意思。。。)
            minGallop--;
        } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
        if (minGallop < 0)
            minGallop = 0;
        minGallop += 2;  
    } 
    this.minGallop = minGallop < 1 ? 1 : minGallop;  

    if (len1 == 1) {
        assert len2 > 0;
        System.arraycopy(a, cursor2, a, dest, len2);
        a[dest + len2] = tmp[cursor1];
    } else if (len1 == 0) {
        throw new IllegalArgumentException(
            "Comparison method violates its general contract!");
    } else {
        assert len2 == 0;
        assert len1 > 1;
        System.arraycopy(tmp, cursor1, a, dest, len1);
    }
}

// 與 mergeLo 相對稱
private void mergeHi(int base1, int len1, int base2, int len2) ;
為歸并操作分配空間:
private T[] ensureCapacity(int minCapacity) {
    if (tmpLen < minCapacity) {
        // 尋找一個盡量小的但大于 minCapacity 的 2 的冪 
        // 經(jīng)典的位運算 (Hacker's Delight: figure 3-3, java.lang.Integer 中也用到了很多書中算法)
        int newSize = minCapacity;
        newSize |= newSize >> 1;
        newSize |= newSize >> 2;
        newSize |= newSize >> 4;
        newSize |= newSize >> 8;
        newSize |= newSize >> 16;
        newSize++;

        if (newSize < 0) 
            newSize = minCapacity;
        else
            newSize = Math.min(newSize, a.length >>> 1);

        @SuppressWarnings({"unchecked", "UnnecessaryLocalVariable"})
        T[] newArray = (T[])java.lang.reflect.Array.newInstance
            (a.getClass().getComponentType(), newSize);
        tmp = newArray;
        tmpLen = newSize;
        tmpBase = 0;
    }
    return tmp;
}
清棧:
// 會根據(jù)棧內(nèi)倒數(shù)第一,倒數(shù)第二和倒數(shù)第三(如果有)序列的長度關(guān)系控制是否歸并
// 大致思路是優(yōu)先將兩個長度較短的序列歸并(當(dāng)然,指的是倒一&倒二,或倒二&倒三)
// 在時機不當(dāng)時,還可以等待TimSort主循環(huán)向棧內(nèi)壓入了更多序列后,再嘗試歸并
// (腦補下2048游戲合并方格的過程,還挺好理解的)
private void mergeCollapse() {
    while (stackSize > 1) {
        int n = stackSize - 2;
        if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) {
            if (runLen[n - 1] < runLen[n + 1])
                n--;
            mergeAt(n);
        } else if (runLen[n] <= runLen[n + 1]) {
            mergeAt(n);
        } else {
            break; 
        }
    }
}

// 主循環(huán)結(jié)束時調(diào)用,強制清棧
private void mergeForceCollapse() {
    while (stackSize > 1) {
        int n = stackSize - 2;
        if (n > 0 && runLen[n - 1] < runLen[n + 1])
            n--;
        mergeAt(n);
    }
}

回顧總結(jié)

不難發(fā)現(xiàn) mergeCollapse --> mergeAt --> mergeLo & mergeHi --> gallopLeft & gallopRight 這個調(diào)用鏈內(nèi)的幾個函數(shù),其實可以看作是一些“常規(guī)操作”的常數(shù)級優(yōu)化,也可以用較為簡單的經(jīng)典實現(xiàn)代替。
整個TimSort的核心思路還是體現(xiàn)在主循環(huán)里面,如果用一句話去概括,個人認(rèn)為是將整個排序任務(wù)分割成若干二分插入排序和歸并排序。一般來說,短而無序的數(shù)組先用二分插排轉(zhuǎn)換成長序列,再將幾個長序列歸并起來。這其中策略切換以及閥值的計算過程很值得細(xì)細(xì)體會。比如考慮一個極端差的情況,原數(shù)組中沒有連續(xù)三個及以上的子序列,再去看主循環(huán)的執(zhí)行過程:

// 返回值 k 落在 [MIN_MERGE/2, MIN_MERGE] 間
// 同時保證 n / k 接近且嚴(yán)格小于一個 2 的冪
// 我們設(shè) t = Round(n / k) + 1
int minRun = minRunLength(nRemaining);
do {
    int runLen = countRunAndMakeAscending(a, lo, hi, c);
    // 此時都要進(jìn)入這個分支,每次從數(shù)組中取一個 minRun 長的數(shù)組進(jìn)行 binarySort
    if (runLen < minRun) {
        int force = nRemaining <= minRun ? nRemaining : minRun;
        binarySort(a, lo, lo + force, lo + runLen, c);
        runLen = force;
    }
    
    // 此時,每次(除最后一次)壓棧的序列長度也都是 minRun,共壓棧 t 次
    ts.pushRun(lo, runLen); 
    // 在這種情況下,mergeCollapse 的行為就很好預(yù)測了
    // 總運行時間大致正比于 n * Round(log(2, t) + 1)
    // 這樣就不難看出為什么 minRunLength 要保證  n / k 接近并小于一個 2 的冪了
    ts.mergeCollapse();    

    lo += runLen;
    nRemaining -= runLen;
} while (nRemaining != 0);

有一些仍沒能想明白的點,比如 MIN_GALLOP 和 MIN_MERGE 的取值,應(yīng)該對整體性能也有關(guān)鍵性的影響。
(突然想到了一個挺好玩的問題,一個長度為32的實數(shù)數(shù)組中,沒有長度為7及以上的序列的概率是多大呢?)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,990評論 2 374

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