你真的會用二分查詢嗎?

DK_BurNIng發表於2019-03-03

二分查詢寫起來真的簡單嗎?未必!

嗯,其實最簡單的二分查詢寫起來還是挺簡單的,稍微注意下可能出錯的地方即可。用迴圈還是遞迴都可以,我這裡先寫一個 你們也可以搜一下有沒有更好的寫法

 /**
     * 二分查詢
     * @param a  源陣列 注意這個陣列的值一定是經過排序的有序陣列
     * @param n  陣列大小
     * @param key 想要查詢位置的值
     * @return
     */
    public int search(int[] a, int n, int key) {
        int low = 0;
        int high = n - 1;

        while (low <= high) {
            //這個地方 計算mid的值 有可能會發生 越界異常的,low+high 非常有可能超過int的最大值限制
            //所以這邊可以優化成low+(high-low)/2  甚至用到位運算low+((high-low)>>1)
            int mid = (low + high) / 2;
            if (a[mid] == key) {
                return mid;
            } else if (a[mid] < key) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }
        return -1;
    }
複製程式碼

但是注意了,這裡只是最簡單的二分查詢,實際生產中,我們可能面對的是:

找出第一個值等於key的元素,或者找出最後一個值等於key的元素,甚至還有找出第一個<= 或者>=的元素位。

這種二分查詢寫起來就比較麻煩而且很容易出bug了,有興趣的同學可以去leetcode上寫一下玩一玩。

真正生產環境中二分查詢還是用在 這種近似查詢的地方比較多,真正意義上的 準確查詢,我們還是用hashmap,二叉樹來

做得比較多,因為這2種做準確查詢有額外優勢(雖然這2個需要額外記憶體空間)

二分查詢啥時候用比較合適?

首先要注意的是,二分查詢速度還是挺快的,千萬不要以為他寫起來簡單就認為他不快。。這和寫起來簡單跑起來十分緩慢的 氣泡排序是兩碼事。畢竟是O(logn)的速度,就算你有2的32次方大概40多億的資料量要查一個資料,最多也就32次就可以查完。

但是二分查詢仍舊有自己的侷限性:

  1. 二分查詢需要資料來源是陣列 假設我們二分查詢用在連結串列為資料結構的地方,可以想一下,連結串列的隨機訪問長度時間 複雜度是O(n)。 我們可以還原一下這個場景:假設用二分查詢在連結串列上,那麼第一次找到這個mid的位置 要移動n/2次,第二次找到mid的位置要移動n/4,以此類推n/8,n/16,可以看出來這個尋找 mid的操作時間複雜度就是O(n),再想想其他操作,這個實際的執行時間肯定比順序查詢要慢了所以二分查詢不可以用在連結串列上,只能用在陣列上

  2. 二分查詢必須要求資料來源是有序的,且資料來源最好是靜態的,動態的資料來源多數場景不使用二分查詢。 因為二分查詢必須要求資料來源是有序的,所以如果我們資料來源不是有序資料,就必須先進行排序才能查詢。排序當然也 是耗時的,最快也要O(nlogn),所以如果你要查詢的資料來源頻繁的插入刪除,需要頻繁的重新排序的話,用二分查詢 就很慢了。

  3. 資料量太多就別用二分查詢了,資料量太小也別用,特殊情況最好用。 前面說到二分查詢要用陣列來做 資料結構,如果你這裡資料量太大,那麼你需要的記憶體空間就太大了,比方說你剩餘記憶體是4gb,你的資料有3.8gb, 那麼大概率這裡你用二分查詢要失敗,因為陣列要求的是連續記憶體空間,我們剩餘記憶體有4gb,可不代表著4個gb的剩餘空間是連續的。

資料量太小為啥也不用呢,其實你要用也不是不可以,主要是資料量太少,二分查詢優勢不明顯,寫起來還麻煩,省事的話 資料量小就用順序查詢吧。有一種情況特殊,比如我們的二分查詢的比較操作比較耗時的話就最好用二分查詢了,也就是 說單次比較操作耗時的查詢(比方說長度很長超過500左右的大字串比較),最好用二分查詢,因為二分查詢可以大幅減少比較的次數。

本寶寶就是頭硬,非要在連結串列上實現二分查詢怎麼辦,而且還不能慢?

計算機界有兩句名言。1.電腦科學領域的任何問題都可以通過增加一個間接的中間層來解決。2.如果時間不夠就用空間來換, 如果空間不夠,就用時間來換。

所以,如果想在連結串列上實現二分查詢有序集合還不能慢的話,用空間換時間就是一個好方法。“跳錶” 就是一個很有效的解決方案。 簡單來說所謂跳錶就是對一個連結串列 建立n個索引層,然後查詢的時候 通過索引層來找即可。我畫了個草圖大家可以體會下:

你真的會用二分查詢嗎?

很好理解吧,其實redis的有序集合也用的這個。只不過跳錶這個解決方案出現的比較晚,很多sdk都沒有跳錶的實現,但是 紅黑樹的實現很多,所以跳錶知道的人不多。因為索引的存在,在區間查詢的時候跳錶甚至比紅黑樹還要快一些。

跳錶的具體實現大家可以自行百度學習,比如這個連結就不錯

這裡只做開拓視野,讓大家知道有這麼回事即可。有興趣的同學可以自己寫寫看,跳錶的插入刪除和索引更新的策略都是蠻有意思 的東西。

相關文章