二分查找算法详解
1.什么是二分查找
二分查找針對的是一個有序的數據集合,查找思想有點類似分治思想。每次都通過跟區間的中間元素對比,將待查找的區間縮小為之前的一半,直到找到要查找的元素,或者區間被縮小為 0。
二分查找是一種非常高效的查找算法,高效到什么程度呢?我們來分析一下它的時間復雜度。
我們假設數據大小是 n,每次查找后數據都會縮小為原來的一半,也就是會除以 2。最壞情況下,直到查找區間被縮小為空,才停止。
可以看出來,這是一個等比數列。其中 n/2k=1 時,k 的值就是總共縮小的次數。而每一次縮小操作只涉及兩個數據的大小比較,所以,經過了 k 次區間縮小操作,時間復雜度就是 O(k)。通過 n/2k=1,我們可以求得 k=log2n,所以時間復雜度就是 O(logn)。
2.二分查找應用場景的局限性
二分查找的時間復雜度是 O(logn),查找數據的效率非常高。不過,并不是什么情況下都可以用二分查找,它的應用場景是有很大局限性的。那什么情況下適合用二分查找,什么情況下不適合呢?
首先,二分查找依賴的是順序表結構,簡單點說就是數組。
那二分查找能否依賴其他數據結構呢?比如鏈表。答案是不可以的,主要原因是二分查找算法需要按照下標隨機訪問元素。數組按照下標隨機訪問數據的時間復雜度是 O(1),而鏈表隨機訪問的時間復雜度是 O(n)。所以,如果數據使用鏈表存儲,二分查找的時間復雜就會變得很高。
二分查找只能用在數據是通過順序表來存儲的數據結構上。如果你的數據是通過其他數據結構存儲的,則無法應用二分查找。
其次,二分查找針對的是有序數據。
二分查找對這一點的要求比較苛刻,數據必須是有序的。如果數據沒有序,我們需要先排序。前面章節里我們講到,排序的時間復雜度最低是 O(nlogn)。所以,如果我們針對的是一組靜態的數據,沒有頻繁地插入、刪除,我們可以進行一次排序,多次二分查找。這樣排序的成本可被均攤,二分查找的邊際成本就會比較低。
但是,如果我們的數據集合有頻繁的插入和刪除操作,要想用二分查找,要么每次插入、刪除操作之后保證數據仍然有序,要么在每次二分查找之前都先進行排序。針對這種動態數據集合,無論哪種方法,維護有序的成本都是很高的。
所以,二分查找只能用在插入、刪除操作不頻繁,一次排序多次查找的場景中。針對動態變化的數據集合,二分查找將不再適用。那針對動態數據集合,如何在其中快速查找某個數據呢?別急,等到二叉樹那一節我會詳細講。
再次,數據量太小不適合二分查找。
如果要處理的數據量很小,完全沒有必要用二分查找,順序遍歷就足夠了。比如我們在一個大小為 10 的數組中查找一個元素,不管用二分查找還是順序遍歷,查找速度都差不多。只有數據量比較大的時候,二分查找的優勢才會比較明顯。
不過,這里有一個例外。如果數據之間的比較操作非常耗時,不管數據量大小,我都推薦使用二分查找。比如,數組中存儲的都是長度超過 300 的字符串,如此長的兩個字符串之間比對大小,就會非常耗時。我們需要盡可能地減少比較次數,而比較次數的減少會大大提高性能,這個時候二分查找就比順序遍歷更有優勢。
最后,數據量太大也不適合二分查找。
二分查找的底層需要依賴數組這種數據結構,而數組為了支持隨機訪問的特性,要求內存空間連續,對內存的要求比較苛刻。比如,我們有 1GB 大小的數據,如果希望用數組來存儲,那就需要 1GB 的連續內存空間。
注意這里的“連續”二字,也就是說,即便有 2GB 的內存空間剩余,但是如果這剩余的 2GB 內存空間都是零散的,沒有連續的 1GB 大小的內存空間,那照樣無法申請一個 1GB 大小的數組。而我們的二分查找是作用在數組這種數據結構之上的,所以太大的數據用數組存儲就比較吃力了,也就不能用二分查找了。
3.二分查找的幾個經典案例的代碼實現
尋找第一個值等于給定值的元素
? ?public int bSearchV1(int[] arr, int n, int value) {int low = 0;int high = n - 1;while (low <= high) {//這里沒有用mid=(low+high)/2 是為了防止low+high過大造成數據溢出//踩坑點:位運算的優先級低于四則運算,如果要用,需要加上大括號int mid = low + ((high - low) >> 1);if (arr[mid] > value) {high = mid - 1;} else if (arr[mid] < value) {low = mid + 1;} else {if (mid == 0 || arr[mid - 1] != value) {return mid;} else {high = mid - 1;}}}我們分析一下第一個案例的代碼:
arr[mid]跟要查找的 value 的大小關系有三種情況:大于、小于、等于。對于 arr[mid]>value 的情況,我們需要更新 high= mid-1;對于 a[mid]<value 的情況,我們需要更新 low=mid+1。這兩點都很好理解。那當 arr[mid]=value 的時候應該如何處理呢?
如果我們查找的是任意一個值等于給定值的元素,當 a[mid]等于要查找的值時,arr[mid]就是我們要找的元素。但是,如果我們求解的是第一個值等于給定值的元素,當 a[mid]等于要查找的值時,我們就需要確認一下這個 a[mid]是不是第一個值等于給定值的元素。
如果 mid 等于 0,那這個元素已經是數組的第一個元素,那它肯定是我們要找的;如果 mid 不等于 0,但 arr[mid]的前一個元素 arr[mid-1]不等于 value,那也說明 a[mid]就是我們要找的第一個值等于給定值的元素。
如果經過檢查之后發現 arr[mid]前面的一個元素 arr[mid-1]也等于 value,那說明此時的 arr[mid]肯定不是我們要查找的第一個值等于給定值的元素。那我們就更新 high=mid-1,因為要找的元素肯定出現在[low, mid-1]之間。
尋找最后一個值等于給定值的元素
? ?public int bSearchV2(int[] arr, int n, int value) {int low = 0;int high = n - 1;while (low <= high) {int mid = low + ((high - low) >> 1);if (arr[mid] > value) {high = mid - 1;} else if (arr[mid] < value) {low = mid + 1;} else {if (mid == n - 1 || arr[mid + 1] != value) {return mid;} else {low = mid + 1;}}}return -1;}尋找第一個大于等于給定值的元素
? ?//尋找第一個大于等于給定值的元素public int bSearchV3(int[] arr, int n, int value) {int low = 0;int high = n - 1;while (low <= high) {int mid = low + ((high - low) >> 1);if (arr[mid] < value) {low = mid + 1;} else {if (mid == 0 || arr[mid - 1] <= value) {return mid;} else {high = mid - 1;}}}return -1;}尋找最后一個小于等于給定值的元素
? ?public int bSearchV4(int[] arr, int n, int value) {int low = 0;int high = n - 1;while (low <= high) {int mid = low + ((high - low) >> 1);if (arr[mid] >= value) {high = mid - 1;} else {if (mid == n - 1 || arr[mid + 1] > value) {return mid;} else {low = mid + 1;}}}return -1;}?
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
- 上一篇: Spring如何实现统一的基于请求头he
- 下一篇: 白话解析:一致性哈希算法 consist