每週一演算法之二分查詢(Kotlin描述)

極客熊貓發表於2019-04-04

簡述: 從這篇文章起就會開啟另一個系列就是上篇文章中提到的每週學習一個基本演算法,會結合LeetCode上題目來做分析。解題的語言一般是Kotlin或Java. 如果涉及到一些有關Kotlin的知識點也會做一些介紹。如果平時就養成學習資料結構演算法以及刷題的習慣,不管今後你是面試(願從此再也不是面試造火箭平時擰螺絲了)或在實際上工作中都會對你有很大幫助。這也是這個系列文章的目的。

一、時間複雜度

最壞時間複雜度 O(log n)

最優時間複雜度 O(1)

平均時間複雜度 O(log n)

二、基本思想

在一個有序的列表中,要查詢的數與列表中間的位置相比,若相等說明找到了,若要查詢的數大於列表中間的數,說明要查詢的數可能在有序列表的後半部分;若要查詢的數小於列表中間的數,說明要查詢的數可能在有序列表的前半部分;然後類似上述操作縮短查詢範圍,最後直到查詢到最後一個數,如果不等於要查詢的數,那麼查詢範圍就為空。

三、演算法步驟

給定一個包含n個帶值的元素陣列或序列A[0], ... A[n-1],使A[0] <= ... <= A[n-1],以及目標值Target.

  • 1、令 low = 0, high = n - 1
  • 2、若low > high, 則表示查詢失敗結束
  • 3、令mid(中間值元素)為 (low + high) / 2的值向下取整 (注意: 在具體實現中為了防止溢位,一般會採用 low + (high - low) / 2的值向下取整 或者直接採用位運算的移位運算來實現除2的操作。這個後面會有具體題目來說明)
  • 4、若Target > A[mid], 令 low = mid + 1 (說明要查詢的值在序列或陣列後半部分(除去自身),移動low遊標,縮小查詢範圍),並回到步驟2
  • 5、如果Target < A[mid], 令 high = mid - 1 (說明要查詢的值在序列或陣列前半部分(除去自身),移動high遊標,縮小查詢範圍),並回到步驟2
  • 6、如果Target == A[mid], 查詢成功並結束,返回mid下標值。

四、演算法過程演示

每週一演算法之二分查詢(Kotlin描述)

五、程式碼實現(Kotlin語言描述)

二分查詢演算法主要有兩種實現方式,一種是迴圈遞推的方式,另一種則是遞迴的方式

  • 1、 迴圈遞推實現方式
    每週一演算法之二分查詢(Kotlin描述)
  • 2、遞迴實現方式
    每週一演算法之二分查詢(Kotlin描述)

六、為什麼Java中mid = (low + high) / 2方式會溢位

相信刷過LeetCode題目的小夥伴們可能會在刷二分查詢的題目過程會遇到超過時間限制的錯誤

每週一演算法之二分查詢(Kotlin描述)
不知道大家有沒有去分析過為什麼會得到超時啊,我都用了二分查詢了時間複雜度都變成 O(log n) 呢,為啥還會超時呢。實際上是int資料型別溢位導致出現負數,使得程式碼進入了死迴圈,然後導致超時,最後OJ給你個超出時間錯誤。

  • 1、出現問題的原因

我們可以確定 lowhigh 都是非負數,那麼也就是二進位制表示的最高位符號位是0,並且lowhigh 都是31位二進位制的整數

假設下面這種場景:

high = 0100 0000 0000 0000 0000 0000 0000 0000 = 1073741824 (Integer.MAX_VALUE的一半)
low  = 0100 0000 0000 0000 0000 0000 0000 0000 = 1073741824 (Integer.MAX_VALUE的一半)
複製程式碼

當執行 low + high 操作時,進行二進位制運算,如下

high + low = 1000 0000 0000 0000 0000 0000 0000 0000
複製程式碼

針對上述high + low 運算的結果,如果是無符號的32位(4個位元組)Integer來說就表示 2147483648 (Integer.MAX_VALUE的大小);如果是有符號的32位(4個位元組)Integer來說就表示 -2147483648。 需要特別注意的是Java或Kotlin中是不支援無符號的Integer型別,只存在有符號的Integer型別

所以問題就來了,如果是在Java或Kotlin中 (low + high) / 2的值就變成了負數 -1073741824low = mid + 1, low就變成負數了。然後target的值會一直比mid要大 low就不斷累加,直到low又累加到1073741824mid 又變成 -1073741824,不斷往復進入了死迴圈導致超時。可以看下面這個例子:

每週一演算法之二分查詢(Kotlin描述)

運算結果:

每週一演算法之二分查詢(Kotlin描述)

  • 2、解決該問題的方式

針對上述問題,你可能看到兩種解決問題的辦法,一種是採用 low + (high - low) / 2,另一種就是 (low + high) >>> 1 或 Kotlin中的 (low + high) ushr 1.

(high + low) >>> 1 = 0100 0000 0000 0000 0000 0000 0000 0000 = 1073741824
複製程式碼

一起來看下例子:

每週一演算法之二分查詢(Kotlin描述)
執行結果:
每週一演算法之二分查詢(Kotlin描述)

七、補充一下Kotlin和Java中的位運算的知識點

  • 1、Java中的 >>>>> (或Kotlin中的 ushrshr ) 的區別

實際上無符號右移運算子>>>(或kotlin中的ushr)和右移運算子>>(或kotlin中的shr)是一樣的,只不過右移時左邊是補上符號位,而無符號右移運算子是補上0

  • 2、Kotlin中的位運算

在Kotlin中拋棄了Java那種直接使用 >>>、>>、<<、&、~、|、^這些非語義化的符號來實現位運算,說真的這樣符號對程式碼可讀性確實降低了很多,看過原始碼小夥伴就知道,很多原始碼中為了追求程式碼的執行效率,往往會採用位運算,但是程式碼理解和讀起來就有點費力了。然而很高興的是Kotlin卻採用一種更加語義化的中綴呼叫函式(infix)來實現位運算,能夠做到真正的簡明識義, 並且用起來就像是在使用運算子一樣,但是它更加具有含義。

每週一演算法之二分查詢(Kotlin描述)

八、LeetCode上二分查詢相關的題目(練一練)

注意: 在做二分查詢題目之前,給幾點建議。

  • 1、真正在做題過程很少會有直接寫標準的二分查詢的題目,一般都是需要變型,轉化成二分查詢的問題。所以掌握二分查詢思想比掌握實現方式更重要。
  • 2、一般是二分查詢去解題有個很明顯的特徵那就是 升序陣列或有序陣列,以及在一些查詢數中對時間複雜度要求比較高,比如時間複雜度必須低於O(n), 很明顯你不能直接用迴圈去做,二分查詢的平均時間複雜度是O(log n) 明顯低於 O(n), 可能就需要你考慮是否能用二分查詢。
  • 3、還有一個典型使用二分查詢的題目,就是求平方根或者求完全平方數,有個通用結論是: 一個非負數n的平方根小於n/2 + 1。所以就轉化了從[0,n/2 + 1]查詢符合的平方根或完全平方數。
  • 1、兩數之和 II - 輸入有序陣列

    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)

  • 2、有效的完全平方數

    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)

  • 3、x的平方根

    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)

  • 4、山脈陣列的峰頂索引

    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)

  • 5、標準的二分查詢

    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)

  • 6、尋找比目標字母大的最小字母

    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)

  • 7、猜數字大小

    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)

  • 8、第一個錯誤的版本

    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)

  • 9、求兩個陣列的交集

    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)

  • 10、兩個陣列的交集 II

    每週一演算法之二分查詢(Kotlin描述)
    每週一演算法之二分查詢(Kotlin描述)

每週一演算法之二分查詢(Kotlin描述)

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~

Kotlin系列文章,歡迎檢視:

原創系列:

Effective Kotlin翻譯系列

翻譯系列:

實戰系列:

相關文章