15~16 | 二分查找

二分查找相關性質 :

二分查找針對一個有序的數據集合,每次通過和區間的中間元素對比,將待查區間縮小爲之前的一半,知道查到要查找的元素,或者區間縮小爲0。

查詢的時間複雜度:O(logn)

待查區間是:n,n/2,n/4,n/8,...,n/2^k。其中當n/2^k=1時,查詢了k = log2n次,並且每次查詢只涉及兩個數據的大小比較操作,所以時間複雜度是O(logn)

 

如何寫一個正確無誤的二分查找:

二分法的寫法有很多種,以爲你low和high的初始化會影響後面整個二分停止條件,以及mid的賦值,low和high的更新,所以我的寫法以下標爲準

int low = 0,high = n-1;

要注意三個容易出錯的地方:

  • 循環退出條件

注意是 low<=high,而不是 low

  • mid 的取值

mid = low + (high - low ) /2 防止當low和high都很大的時候,直接相加除以2的方法時的溢出。另外如果想繼續優化性能,mid = low+((high-low)>>1),注意後面要加個括號,因爲優先級的緣故。

  • low和high的更新
low = mid+1;
high = mid-1;

這裏很好理解,因爲二分的核心是拿中間的數取和low,high下標對應的數取比較,如果比較完成了之後,肯定要拋去中間這個數了。(對於二分的變形問題不適用,最好的方法就是解決不同變形問題時候畫圖想想)

 

二分法應用場景的侷限性

  • 二分法依賴的是順序表結構(數組),來完成它根據下標的O(1)快速查找。

如果你想利用鏈表來實現,鏈表的隨機訪問時間複雜度很高,這樣會導致二分查找的時間複雜度變高。

  • 二分查找針對有序的數據;靜態場景下少次插入刪除,多次查找的數據。

如果是一組無序的靜態數據,沒有頻繁的插入,刪除,我們進行一次排序多次查找,這樣排序的時間複雜度被均攤,二分查找效率就會很高。相反,如果數據集涉及到頻繁的插入和刪除,要想用二分查找,同時還要調用多次排序來維護有序性,這樣的時間複雜度就很高。

  • 數據量較小,直接使用順序查找即可,因爲體現不出來二分查找的優勢。

不過也有例外,當數據之間的比較非常耗時時,不管數據量大小,都推薦使用二分查找從而來降低比較的次數。

  • 數據量太大不適合二分查找

前面提到,二分法依賴順序表結構,就是要開闢一塊連續的內存。如果申請不到一塊很大的內存空間就沒法來存儲這些數據。

 

思考題:

如何在 1000 萬個整數中快速查找某個整數?內存限制是100MB。

每個數據大小是8B,直接將數據存在數組中,消耗內存80MB。對1000萬個數據從小到大排序,然後利用二分查找,可以快速查找到想要的數據。

至於爲什麼要排序,因爲如果要多次查找呢?如果不排序,每次都要O(n)的時間複雜度,但是排序了之後利用二分查找大大降低了需要的時間。並且,大部分情況下二分查找可以解決的問題,用哈希表和二叉樹都可以解決,但是會需要比較多的額外空間。所以100MB存儲不下。數組除了存儲數據本身之外,不需要額外存儲其他信息,是最省空間的存儲方式。

 

課後思考:

  • 如何編程實現“求一個數的平方根”?要求精確到小數點後 6 位。

https://blog.csdn.net/qq_34269988/article/details/97179601

  • 如果數據使用鏈表存儲,二分查找的時間複雜就會變得很高,那查找的時間複雜度究竟是多少呢?如果你自己推導一下,你就會深刻地認識到,爲何我們會選擇用數組而不是鏈表來實現二分查找了。

假設鏈表長度爲n,第一次查找的區域爲[0,n/2),指針需要移動n/2次;第二次需要移動n/4次;直到第k次需要移動1次。總共指針移動次數(查找次數) = n/2 + n/4 + n/8 + ...+ 1。sum = n -1。時間複雜度爲O(n)。

 

二分查找的四個變形問題:

  • 查找第一個值等於給定值的元素在數組中的位置
int search(int *a, int n, int value) {
	int low = 0, high = n - 1;
	while (low <= high) {
		int mid = low + (high - low) / 2;
		if (a[mid]>value) {
			high = mid - 1;
		}
		else if (a[mid]< value) {
			low = mid + 1;
		}
		else {
			if (mid == 0 || a[mid - 1] != value)	return mid;
			high = mid - 1;
		}
	}
	return -1;
}
  • 查找最後一個值等於給定值的元素在數組中的位置
int search(int *a, int n, int value) {
	int low = 0, high = n - 1;
	while (low <= high) {
		int mid = low + (high - low) / 2;
		if (a[mid]>value) {
			high = mid - 1;
		}
		else if (a[mid]< value) {
			low = mid + 1;
		}
		else {
			if (mid == n-1 || a[mid + 1] != value)	return mid;
			low = mid + 1;
		}
	}
	return -1;
}
  • 查找最後一個小於等於給定值的元素在數組中的位置
int search(int *a, int n, int value) {
	int low = 0, high = n - 1;
	while (low <= high) {
		int mid = low + (high - low) / 2;
		if (a[mid] >= value) {
			if (mid == 0 || a[mid - 1] < value)    return mid;
			high = mid - 1;
		}
		else {
			low = mid + 1;
		}
	}
	return -1;
}
  • 查找最後一個小於等於給定值的元素在數組中的位置 
int search(int *a, int n, int value) {
	int low = 0, high = n - 1;
	while (low <= high) {
		int mid = low + (high - low) / 2;
		if (a[mid] <= value) {
			if (mid == n - 1 || a[mid + 1] > value)    return mid;
			low = mid + 1;
		}
		else {
			high = mid - 1;
		}
	}
	return -1;
}

思考題: 如何快速定位IP對應的省份地址?

[202.102.133.0, 202.102.133.255]  山東東營市 
[202.102.135.0, 202.102.136.255]  山東煙臺 
[202.102.156.34, 202.102.157.255] 山東青島 
[202.102.48.0, 202.102.48.255] 江蘇宿遷 
[202.102.49.15, 202.102.51.251] 江蘇泰州 
[202.102.56.0, 202.102.56.255] 江蘇連雲港

將所有的ip區間的起始地址進行排序,然後根據二分查找,找到最後一個小於等於目標ip的起始ip,然後在這個區間內查找,如果在,我們就取出對應的歸屬地顯示;如果不在,就返回未查找到。

總結

        凡是用二分查找能解決的,絕大部分我們更傾向於用散列表或者二叉查找樹。即便是二分查找在內存使用上更節省,但是畢竟內存如此緊缺的情況並不多。那二分查找真的沒什麼用處了嗎?實際上,上一節講的求“值等於給定值”的二分查找確實不怎麼會被用到,二分查找更適合用在“近似”查找問題,在這類問題上,二分查找的優勢更加明顯。比如今天講的這幾種變體問題,用其他數據結構,比如散列表、二叉樹,就比較難實現了。

課後思考:如果有序數組是一個循環有序數組,比如 4,5,6,1,2,3。針對這種情況,如何實現一個求“值等於給定值”的二分查找算法呢?

先找到分界下標,然後在兩個區間做二分查找。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章