二分查找
二分查找的思想
二分查找(Binary Search)算法,也叫折半查找算法屿岂。
二分查找針對(duì)的是一個(gè)有序的數(shù)據(jù)集合缸兔,查找思想有點(diǎn)類似分治思想诗充。每次都通過(guò)跟區(qū)間的中間元素對(duì)比,將待查找的區(qū)間縮小為之前的一半逛腿,直到找到要查找的元素稀并,或者區(qū)間被縮小為0。
假設(shè)有1000條訂單數(shù)據(jù)鳄逾,已經(jīng)按照訂單金額從小到大排序稻轨,每個(gè)訂單都不同,并且最小單位是元〉癜迹現(xiàn)在想知道是否存在金額等于19元的訂單殴俱。如果存在,則返回訂單數(shù)據(jù)枚抵,如果不存在則返回null线欲。
利用二分思想,每次都與區(qū)間的中間數(shù)據(jù)比對(duì)大小汽摹,縮小查找區(qū)間的范圍李丰。下圖中,low和high表示待查找區(qū)間的下標(biāo)逼泣,mid表示待查找區(qū)間的中間元素下標(biāo)趴泌。
二分查找的時(shí)間復(fù)雜度為O(logn),被查找區(qū)間的大小變化:
n, n / 2, n / 4, n / 8, ..., n / (2^k), ...
二分查找的實(shí)現(xiàn)
最簡(jiǎn)單的情況就是有序數(shù)組中不存在重復(fù)元素拉庶,java代碼循環(huán)實(shí)現(xiàn):
public int bsearch(int[] arr, int n, int value){
int low = 0;
int high = n - 1;
while(low <= high){
int mid = (low + high) / 2;
if (arr[mid] == value){
return mid;
} else if (arr[mid] < value){
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
low嗜憔、high、mid都是指數(shù)組下標(biāo)氏仗,其中l(wèi)ow和high表示當(dāng)前查找的區(qū)間范圍吉捶,初始low = 0,high = n - 1皆尔。mid表示[low, high]的中間位置呐舔。我們通過(guò)對(duì)比arr[mid]與value的大小,來(lái)更新接下來(lái)要查找的區(qū)間范圍慷蠕,知道找到或者區(qū)間縮小為0珊拼,就退出。
需要注意的點(diǎn):
循環(huán)退出條件是low <= high砌们,而不是low < high杆麸。
mid = (low + high) / 2這種寫法搁进,在low和high比較大時(shí)浪感,兩者之和可能會(huì)溢出昔头。
改進(jìn)的方法是將mid的計(jì)算方式寫成low + (high - low) / 2,轉(zhuǎn)化成位運(yùn)算(low + (high - low) >> 1)更佳影兽。
3.寫成low = mid或者h(yuǎn)igh = mid揭斧,可能會(huì)發(fā)生死循環(huán)。應(yīng)當(dāng)寫成low = mid + 1峻堰,high = mid - 1讹开。
二分查找的java遞歸實(shí)現(xiàn):
public int bsearch(int[] arr, int n, int val){
return bsearchInternally(arr, 0, n - 1, val);
}
private int bsearchInternally(int[] arr, int low, int high, int value){
if (low > high){
return -1;
}
int mid = low + ((high - low) >> 1);
if (arr[mid] == value){
return mid;
} else if (arr[mid] < value){
return bsearchInternally(arr, mid + 1, high, value);
} else {
return bsearchInternally(arr, low, mid - 1, value);
}
}
二分查找應(yīng)用場(chǎng)景的局限性
1.二分查找依賴的是順序表結(jié)構(gòu),即數(shù)組捐名。
二分查找算法需要按照下標(biāo)隨機(jī)訪問(wèn)元素旦万,所以不能用鏈表隨機(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)用二分查找。
2.二分查找要求數(shù)據(jù)必須是有序的拂酣,或者無(wú)序但沒(mé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)景中格郁。
3.數(shù)據(jù)量太小查找性能提升不大
但如果數(shù)據(jù)之間的比較操作非常耗時(shí)腹殿,比較次數(shù)的減少會(huì)大大提高性能独悴,這個(gè)時(shí)候二分查找就比較順序遍歷更有優(yōu)勢(shì)。
4.數(shù)據(jù)量太大不適合二分查找
只有數(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ù)組這種數(shù)據(jù)結(jié)構(gòu),而數(shù)組為了支持隨機(jī)訪問(wèn)的特性雨效,要求內(nèi)存空間連續(xù)迅涮,對(duì)內(nèi)存的要求比較苛刻。比如有1G大小的數(shù)據(jù)徽龟,用數(shù)組來(lái)存儲(chǔ)叮姑,就有需要1G的連續(xù)內(nèi)存空間。
“連續(xù)”意味著即便有2G的內(nèi)存空間剩余据悔,但是如果這剩余的2G內(nèi)存空間都是零散的传透,沒(méi)有連續(xù)的1G大小的內(nèi)存空間,那就無(wú)法申請(qǐng)一個(gè)1G大小的數(shù)組极颓。
二分查找的三種變體
變體一:查找第一個(gè)值等于給定值的元素
如果有序數(shù)據(jù)集合中存在重復(fù)的數(shù)據(jù)朱盐,要找到第一個(gè)值等于給定值的數(shù)據(jù)。
比如下面這樣一個(gè)有序數(shù)組菠隆,其中兵琳,a[5], a[6], a[7]的值都等于8,是重復(fù)的數(shù)據(jù)骇径。我們希望查找第一個(gè)等于8的數(shù)據(jù)躯肌,也就是下標(biāo)是5的元素。
簡(jiǎn)潔實(shí)現(xiàn):
public int bsearch(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 {
low = mid + 1;
}
}
if (low < n && arr[low] == value){
return low;
} else {
return -1;
}
}
容易理解的java實(shí)現(xiàn):
public int bsearch(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 == 0) || (arr[mid - 1] != value)){
return mid;
} else {
high = mid - 1;
}
}
}
return -1;
}
求解的是第一個(gè)值等于給定值的元素破衔,當(dāng)arr[mid]等于要查找的值清女,就需要確認(rèn)一下這個(gè)arr[mid]是不是第一個(gè)值等于給定值的元素。
如果mid等于0晰筛,那這個(gè)元素已經(jīng)是數(shù)組的第一個(gè)元素嫡丙,那它肯定是我們要找的拴袭;
如果mid不等于0,但arr[mid]的前一個(gè)元素arr[mid - 1]不等于value曙博,那也說(shuō)明arr[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è)值等于給定值的元素
java實(shí)現(xiàn):
public int bsearch(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;
}
}
}
}
如果arr[mid]這個(gè)元素已經(jīng)是數(shù)組中的最后一個(gè)元素了尘吗,那它肯定是我們要找的;
如果arr[mid]的后一個(gè)元素arr[mid + 1]不等于value浇坐,那也說(shuō)明arr[mid]就是我們要找的最后一個(gè)值等于給定值的元素睬捶。
如果arr[mid]后面的一個(gè)元素arr[mid + 1]也等于value,那說(shuō)明當(dāng)前的這個(gè)arr[mid]并不是最后一個(gè)值等于給定值的元素近刘。我們就更新low = mid + 1擒贸,因?yàn)橐业脑乜隙ǔ霈F(xiàn)在[mid + 1, high]之間。
變體三:查找第一個(gè)大于等于給定值的元素
java代碼實(shí)現(xiàn):
public int bsearch(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){
if ((mid == 0) || (arr[mid - 1] < value)) {
return mid;
} else {
high = mid - 1;
}
} else {
low = mid + 1;
}
}
return -1;
}
如果arr[mid]小于要查找的值value觉渴,那要查找的值肯定在[mid + 1, high]之間介劫,更新low = high + 1。
對(duì)于arr[mid]大于等于給定值value的情況案淋,如果arr[mid]前面已經(jīng)沒(méi)有元素座韵,或者前面一個(gè)元素arr[mid - 1]小于要查找的值value,那arr[mid]就是目標(biāo)元素踢京。
如果arr[mid - 1]也大于等于要查找的值value誉碴,那說(shuō)明要查找的元素在[low, mid - 1]之間,所以瓣距,將high更新為mid - 1黔帕。
變體四:查找最后一個(gè)小于等于給定值的元素
java代碼實(shí)現(xiàn):
public int bsearch(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;
}
二分查找相關(guān)問(wèn)題
如何快速判斷某個(gè)整數(shù)是否出現(xiàn)在這1000萬(wàn)數(shù)據(jù)中?
假設(shè)有1000萬(wàn)個(gè)整數(shù)蹈丸,每個(gè)數(shù)據(jù)占8個(gè)字節(jié)成黄,內(nèi)存限制是100MB,如何設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)和算法逻杖,快速判斷某個(gè)整數(shù)是否出現(xiàn)在者1000萬(wàn)數(shù)據(jù)中奋岁?
每個(gè)數(shù)據(jù)大小是8字節(jié),將數(shù)據(jù)存儲(chǔ)在數(shù)組中弧腥,內(nèi)存占用差不多是80MB厦取,符合內(nèi)存的限制。對(duì)這1000萬(wàn)數(shù)據(jù)利用原地排序算法排序管搪,然后再利用二分查找算法虾攻,就可以快速地查找想要的數(shù)據(jù)了铡买。
用二分查找求一個(gè)數(shù)的平方根
如何編程實(shí)現(xiàn)“求一個(gè)數(shù)據(jù)的平方根”?要求精確到小數(shù)點(diǎn)后6位霎箍。
python代碼實(shí)現(xiàn):
def bsearch_sqrt(num: float) -> float:
if num < 0
raise ValueError
if num ** 2 == num:
return num
if num < 1:
low, high = num, 1
else:
low, high = 0, num
while high - low > 0.0000001:
mid = (high + low) / 2
mid_v2 = mid ** 2
if mid_v2 > num:
high = mid
elif mid_v2 < num:
low = mid
elif mid_v2 == num:
return mid
return round((high + low) / 2, 6)
二分查找快速定位IP對(duì)應(yīng)的省份地址
IP地址查找IP歸屬地是通過(guò)維護(hù)一個(gè)很大的IP地址庫(kù)來(lái)實(shí)現(xiàn)的奇钞。地址庫(kù)中包括IP地址范圍和歸屬地的對(duì)應(yīng)關(guān)系。
當(dāng)查詢202.102.133.13這個(gè)IP地址的歸屬地時(shí)漂坏,就在地址庫(kù)中搜索景埃,發(fā)現(xiàn)這個(gè)IP地址落在[202.102.133.0,202.102.133.255]這個(gè)地址范圍內(nèi)顶别,那我們就可以將這個(gè)IP地址范圍對(duì)應(yīng)的歸屬地“山東東營(yíng)市”顯示給用戶了谷徙。
202.102.133.0|202.102.133.63|山東東營(yíng)聯(lián)通
202.102.133.64|202.102.133.255|山東濟(jì)南聯(lián)通
202.102.134.0|202.102.134.255|山東青島聯(lián)通
202.102.135.0|202.102.135.231|山東煙臺(tái)聯(lián)通
202.102.48.0|202.102.48.255|江蘇宿遷電信
202.102.49.0|202.102.49.255|江蘇泰州電信
202.102.50.0|202.102.50.255|江蘇蘇州電信
202.102.51.0|202.102.101.255|江蘇南京電信
假設(shè)有12萬(wàn)條這樣的IP區(qū)間與歸屬地的對(duì)應(yīng)關(guān)系,如何快速定位出一個(gè)IP地址的歸屬地呢驯绎?
思路:
可以先預(yù)處理這12萬(wàn)條數(shù)據(jù)完慧,存儲(chǔ)在數(shù)組中,讓其按照起始IP(按照地址對(duì)應(yīng)的整型值)從小到大排序剩失。
查詢某個(gè)IP歸屬地時(shí)屈尼,先通過(guò)二分查找找到最后一個(gè)起始IP小于等于這個(gè)IP的IP區(qū)間,然后檢查這個(gè)IP是否在這個(gè)IP區(qū)間內(nèi)拴孤,如果在就取出對(duì)應(yīng)的歸屬地顯示脾歧;如果不在就返回未查找到。
代碼實(shí)現(xiàn):
ip_rule_list = []
with open("ip.txt", encoding = "utf-8") as f:
for line in f:
start_ip, end_ip, location = line.rstrp().split("|", 2)
ip_rule_list.append((start_ip, end_ip, location))
def ip2int(ip_str: str) -> int:
result = 0
for i in ip_str.split(",")
result = (result << 8) + int(i)
return result
ip_rule_list.sort(key = lambda x: ip2int(x[0]))
def ip_to_location(ip: str) -> str:
low, high = 0, len(ip_rule_list) - 1
ip_int = ip2int(ip)
while low <= high:
mid = (low + high) >> 1
#查找最后一個(gè)起始IP小于等于這個(gè)IP的區(qū)間
if ip2int(ip_rule_list[mid][0]) < ip_int:
low = mid + 1
else:
high = mid - 1
if high >= 0 and ip2int(ip_rule_list[high][0] <= ip_int <= ip2int(ip_rule_list[high][1])):
return ip_rule_list[high][2]
else:
return ""
循環(huán)有序數(shù)組的二分查找問(wèn)題
如果有序數(shù)組是一個(gè)循環(huán)有序數(shù)組演熟,比如4鞭执、5、6绽媒、1蚕冬、2、3是辕。針對(duì)這種情況囤热,如何實(shí)現(xiàn)一個(gè)求“值等于給定值”的二分查找算法呢?
思路:
對(duì)于數(shù)組[1获三,2旁蔼,3,4疙教,5棺聊,6]共有下列6中旋轉(zhuǎn)方法:
1 2 3 4 5 6
6 1 2 3 4 5
5 6 1 2 3 4
4 5 6 1 2 3
3 4 5 6 1 2
2 3 4 5 6 1
以數(shù)組中間點(diǎn)為分區(qū),會(huì)將數(shù)組分成升序和非升序兩部分贞谓。
由此可得:
- 若中間數(shù)小于最右邊數(shù)限佩,則右半段是升序區(qū)間
- 若中間數(shù)大于最右邊數(shù),則左半段是升序區(qū)間
然后根據(jù)目標(biāo)值是否存在與升序區(qū)間,確定保留哪一區(qū)間祟同,進(jìn)行二分查找即可
python代碼實(shí)現(xiàn):
def binary_search_circle_array(arr: list, value) -> int:
low, high = 0, len(arr) - 1
while low <= high:
mid = (low + high) >> 1
if arr[mid] < arr[high]: return mid
# 上面沒(méi)有返回作喘,說(shuō)明中間值不是目標(biāo)值
if arr[mid] < arr[high]: # 若中間數(shù)小于最右邊數(shù),則右半段是升序區(qū)間
if arr[mid] < value and value <= arr[high]: # 在右半段有序區(qū)間
low = mid + 1
else:
high = mid - 1
elif arr[mid] > arr[high]: # 若中間數(shù)大于最右邊數(shù)晕城,則左半段是升序區(qū)間
if arr[low] <= value and value < arr[mid]: # 在左半段有序區(qū)間
high = mid - 1
else:
low = mid + 1
else: # 若中間數(shù)等于最右邊數(shù)泞坦,則目標(biāo)不在右半段
low = mid + 1
return -1