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