簡述: 從這篇文章起就會開啟另一個系列就是上篇文章中提到的每週學習一個基本演算法,會結合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語言描述)
二分查詢演算法主要有兩種實現方式,一種是迴圈遞推的方式,另一種則是遞迴的方式
- 1、 迴圈遞推實現方式
- 2、遞迴實現方式
六、為什麼Java中mid = (low + high) / 2方式會溢位
相信刷過LeetCode題目的小夥伴們可能會在刷二分查詢的題目過程會遇到超過時間限制的錯誤
不知道大家有沒有去分析過為什麼會得到超時啊,我都用了二分查詢了時間複雜度都變成 O(log n) 呢,為啥還會超時呢。實際上是int資料型別溢位導致出現負數,使得程式碼進入了死迴圈,然後導致超時,最後OJ給你個超出時間錯誤。- 1、出現問題的原因
我們可以確定 low 和 high 都是非負數,那麼也就是二進位制表示的最高位符號位是0,並且low 和 high 都是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的值就變成了負數 -1073741824,low = mid + 1, low就變成負數了。然後target的值會一直比mid要大 low就不斷累加,直到low又累加到1073741824,mid 又變成 -1073741824,不斷往復進入了死迴圈導致超時。可以看下面這個例子:
運算結果:
- 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和Java中的位運算的知識點
- 1、Java中的 >>> 與 >> (或Kotlin中的 ushr 與 shr ) 的區別
實際上無符號右移運算子>>>(或kotlin中的ushr)和右移運算子>>(或kotlin中的shr)是一樣的,只不過右移時左邊是補上符號位,而無符號右移運算子是補上0
- 2、Kotlin中的位運算
在Kotlin中拋棄了Java那種直接使用 >>>、>>、<<、&、~、|、^
這些非語義化的符號來實現位運算,說真的這樣符號對程式碼可讀性確實降低了很多,看過原始碼小夥伴就知道,很多原始碼中為了追求程式碼的執行效率,往往會採用位運算,但是程式碼理解和讀起來就有點費力了。然而很高興的是Kotlin卻採用一種更加語義化的中綴呼叫函式(infix)來實現位運算,能夠做到真正的簡明識義, 並且用起來就像是在使用運算子一樣,但是它更加具有含義。
八、LeetCode上二分查詢相關的題目(練一練)
注意: 在做二分查詢題目之前,給幾點建議。
- 1、真正在做題過程很少會有直接寫標準的二分查詢的題目,一般都是需要變型,轉化成二分查詢的問題。所以掌握二分查詢思想比掌握實現方式更重要。
- 2、一般是二分查詢去解題有個很明顯的特徵那就是 升序陣列或有序陣列,以及在一些查詢數中對時間複雜度要求比較高,比如時間複雜度必須低於O(n), 很明顯你不能直接用迴圈去做,二分查詢的平均時間複雜度是O(log n) 明顯低於 O(n), 可能就需要你考慮是否能用二分查詢。
- 3、還有一個典型使用二分查詢的題目,就是求平方根或者求完全平方數,有個通用結論是: 一個非負數n的平方根小於n/2 + 1。所以就轉化了從[0,n/2 + 1]查詢符合的平方根或完全平方數。
-
1、兩數之和 II - 輸入有序陣列
-
2、有效的完全平方數
-
3、x的平方根
-
4、山脈陣列的峰頂索引
-
5、標準的二分查詢
-
6、尋找比目標字母大的最小字母
-
7、猜數字大小
-
8、第一個錯誤的版本
-
9、求兩個陣列的交集
-
10、兩個陣列的交集 II
歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~
Kotlin系列文章,歡迎檢視:
原創系列:
- 教你如何完全解析Kotlin中的型別系統
- 如何讓你的回撥更具Kotlin風味
- Jetbrains開發者日見聞(三)之Kotlin1.3新特性(inline class篇)
- JetBrains開發者日見聞(二)之Kotlin1.3的新特性(Contract契約與協程篇)
- JetBrains開發者日見聞(一)之Kotlin/Native 嚐鮮篇
- 教你如何攻克Kotlin中泛型型變的難點(實踐篇)
- 教你如何攻克Kotlin中泛型型變的難點(下篇)
- 教你如何攻克Kotlin中泛型型變的難點(上篇)
- Kotlin的獨門祕籍Reified實化型別引數(下篇)
- 有關Kotlin屬性代理你需要知道的一切
- 淺談Kotlin中的Sequences原始碼解析
- 淺談Kotlin中集合和函式式API完全解析-上篇
- 淺談Kotlin語法篇之lambda編譯成位元組碼過程完全解析
- 淺談Kotlin語法篇之Lambda表示式完全解析
- 淺談Kotlin語法篇之擴充套件函式
- 淺談Kotlin語法篇之頂層函式、中綴呼叫、解構宣告
- 淺談Kotlin語法篇之如何讓函式更好地呼叫
- 淺談Kotlin語法篇之變數和常量
- 淺談Kotlin語法篇之基礎語法
Effective Kotlin翻譯系列
- [譯]Effective Kotlin系列之考慮使用原始型別的陣列優化效能(五)
- [譯]Effective Kotlin系列之使用Sequence來優化集合的操作(四)
- [譯]Effective Kotlin系列之探索高階函式中inline修飾符(三)
- [譯]Effective Kotlin系列之遇到多個構造器引數要考慮使用構建器(二)
- [譯]Effective Kotlin系列之考慮使用靜態工廠方法替代構造器(一)
翻譯系列:
- [譯]記一次Kotlin官方文件翻譯的PR(內聯類)
- [譯]Kotlin中內聯類的自動裝箱和高效能探索(二)
- [譯]Kotlin中內聯類(inline class)完全解析(一)
- [譯]Kotlin的獨門祕籍Reified實化型別引數(上篇)
- [譯]Kotlin泛型中何時該用型別形參約束?
- [譯] 一個簡單方式教你記住Kotlin的形參和實參
- [譯]Kotlin中是應該定義函式還是定義屬性?
- [譯]如何在你的Kotlin程式碼中移除所有的!!(非空斷言)
- [譯]掌握Kotlin中的標準庫函式: run、with、let、also和apply
- [譯]有關Kotlin型別別名(typealias)你需要知道的一切
- [譯]Kotlin中是應該使用序列(Sequences)還是集合(Lists)?
- [譯]Kotlin中的龜(List)兔(Sequence)賽跑
實戰系列: