1. 程式人生 > >資料結構與演算法學習--二分查詢

資料結構與演算法學習--二分查詢

二分查詢(Binary Search)演算法,也叫折半查詢演算法。二分查詢針對的時一個有序的資料集合,查詢思想有點類似於分治。每次都是通過和區間的中間元素進行比較,將待查區縮小為原來的一半,直到將元素找到或者區間縮小為0。
我們可以通過2種方式實現:遞迴和非遞迴。

/*************************************************************************
 > File Name: bsearch.c
 > Author:  jinshaohui
 > Mail:    [email protected]
> Time: 18-10-21 > Desc: ************************************************************************/ #include<stdio.h> #include<stdlib.h> #include<assert.h> int mybsearch(int a[],int size,int value) { int mid = 0; int left = 0; int right = size - 1; while(left <= right) { /*防止size數量太大是,(left + right)資料翻轉,導致問題*/ mid = left + ((right - left)>>1); if (a[mid] == value) { return mid; } else if (a[mid] < value) { left = mid + 1; } else { right = mid - 1; } } return -1; } int helper(int a[], int left,int right,int value) { int mid = 0; if (left > right) { return -1; } /*防止size數量太大是,(left + right)資料翻轉,導致問題*/ mid = left + ((right - left)>>1); if (a[mid] == value) { return mid; } else if (a[mid] < value) { return helper(a,mid + 1,right,value); } else { return helper(a,left,mid - 1,value); } return -1; } /*遞迴實現*/ int mybsearch_2(int a[],int size,int value) { return helper(a,0,size-1,value); } int main() { int a[10] = {5,6,8,9,10,11,23,42,53,123}; int data = 0; int res = 0; printf("\r\n輸入一個整數"); scanf("%d",&data); res = mybsearch(a,10,data); printf("data[%d] %s 在資料中,下標是%d",data,(res != -1)?"":"不",res); printf("\r\n輸入一個整數"); scanf("%d",&data); res = mybsearch_2(a,10,data); printf("data[%d] %s 在資料中,下標是%d",data,(res != -1)?"":"不",res); return; }

看了上面的程式碼,我們覺得很容易吧。但是還有幾個地方需要注意:
1、迴圈退出條件
這裡必須是left <= right,而不是left < right
2、mid的求值
還有一個是為了程式的更好的健壯性:求中間值的時候,我們多用mid=(left+right)/2; 但是這樣的可能會越界,比如 left+right超int型的範圍,所以我們用更好更安全的寫法 :mid=left+(right-left)/2; 起初還鑽牛角尖不理解為什麼等式相同。。。 為了進一步將效能優化到極致的話,我們採用位移操作。>>1 位移優先順序比較低,我們必須考慮到加括號。mid=left+((right-left) >>1).
3、left和right的更新
left = mid + 1;right = mid - 1;這裡必須是+1 和-1,不能直接等於mid,否則可能會導致死迴圈。

二分查詢的變形問題
我們一直以為二分查詢比較簡單,但是寫出完全正確的二分查詢演算法確很難。首先引用一下《程式設計珠璣》中的兩句話:
1、儘管給了那麼充裕的時間,只有大約10%的專業程式設計師能夠寫出正確的二分查詢。
2、儘管第一個二分查詢程式於1946年就公佈了,但是第一個沒有bug的二分查詢程式在1962年才出現。
二分查詢的變形問題很多,我們重點來看下面四種
1、找出第一個等於給定數值的元素
2、找出最後一個等於給定數值的元素
3、找出第一個大於等於給定數值的元素
4、找出第一個小於等於給定數值的元素。

/*************************************************************************
 > File Name: bsearch.c
 > Author:  jinshaohui
 > Mail:    [email protected]
 > Time:    18-10-21
 > Desc:    
 ************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

/*二分查詢演算法的變形問題
 *1、查詢第一個等於給定數值的元素
 *2、查詢最後一個等於給定數值的元素
 *3、查詢第一個大於等於給定數值的元素
 *4、查詢第一個小於等於給定數值的元素
 * */


 /*1、查詢第一個等於給定數值的元素*/
int mybsearch_1(int a[],int size,int value)
{
	int mid = 0;
	int left = 0;
	int right = size - 1;

	while(left <= right)
	{
		/*防止size數量太大是,(left + right)資料翻轉,導致問題*/
		mid = left + ((right - left)>>1);

		if (a[mid] < value)
		{
			left = mid + 1;
		}
		else if (a[mid] > value)
		{
			right = mid - 1;
		}
		else
		{
			if ((mid == 0) || (a[mid - 1] != value))
			{
                return mid;
			}
			else
			{
				right = mid - 1;
			}
		}
	}

	return -1;
}

 /*2、查詢最後一個等於給定數值的元素*/
int mybsearch_2(int a[],int size,int value)
{
	int mid = 0;
	int left = 0;
	int right = size - 1;

	while(left <= right)
	{
		/*防止size數量太大是,(left + right)資料翻轉,導致問題*/
		mid = left + ((right - left)>>1);

		if (a[mid] < value)
		{
			left = mid + 1;
		}
		else if (a[mid] > value)
		{
			right = mid - 1;
		}
		else
		{
			if ((mid == (size - 1)) || (a[mid + 1] != value))
			{
                return mid;
			}
			else
			{
				left = mid + 1;
			}
		}
	}

	return -1;
}
 /*3、查詢第一個大於等於給定數值的元素*/
int mybsearch_3(int a[],int size,int value)
{
	int mid = 0;
	int left = 0;
	int right = size - 1;

	while(left <= right)
	{
		/*防止size數量太大是,(left + right)資料翻轉,導致問題*/
		mid = left + ((right - left)>>1);

		if (a[mid] < value)
		{
			left = mid + 1;
		}
		else
		{
			/*a[mid] >= value 當mid==0 或者a[mid-1] > value 說明是第一個大於等於value*/
			if ((mid == 0) || (a[mid - 1] < value))
			{
                return mid;
			}
			else
			{
				right = mid - 1;
			}
		}
	}

	return -1;
}

 /*4、查詢第一個小於等於給定數值的元素*/
int mybsearch_4(int a[],int size,int value)
{
	int mid = 0;
	int left = 0;
	int right = size - 1;

	while(left <= right)
	{
		/*防止size數量太大是,(left + right)資料翻轉,導致問題*/
		mid = left + ((right - left)>>1);

		if (a[mid] > value)
		{
			right = mid - 1;
		}
		else
		{
			/*a[mid] <= value 時,當前mid == size -1 陣列中最大的數值;
			 *                    或者a[mid + 1] 大於vlaue,就是mid就第一個小於等於value*/
			if ((mid == (size - 1)) || (a[mid + 1] > value))
			{
                return mid;
			}
			else
			{
				left = mid + 1;
			}
		}
	}

	return -1;
}
int main()
{
	int a[10] = {5,6,6,9,10,11,11,22,33,33};
    int data = 0;
	int i = 0;
	int res =0;

	printf("\r\n");
    for(i = 0; i < 10 ; i++)
	{
		printf("%d ",a[i]);
	}
	printf("\r\n");
	printf("\r\n輸入一個整數");
	scanf("%d",&data);
    res = mybsearch_1(a,10,data);
	printf("第一個等於data[%d],下標是%d",data,res);
	
	printf("\r\n輸入一個整數");
	scanf("%d",&data);
    res = mybsearch_2(a,10,data);
	printf("最後一個等於data[%d],下標是%d",data,res);

	printf("\r\n輸入一個整數");
	scanf("%d",&data);
    res = mybsearch_2(a,10,data);
	printf("第一個大於等於data[%d],下標是%d",data,res);

	printf("\r\n輸入一個整數");
	scanf("%d",&data);
    res = mybsearch_2(a,10,data);
	printf("第一個小等於data[%d],下標是%d",data,res);
	return;
}

課後思考:
1、如何求一個數的平方根

/*************************************************************************
 > File Name: sqrt.c
 > Author:  jinshaohui
 > Mail:    [email protected]
 > Time:    18-10-31
 > Desc:    
 ************************************************************************/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
/*求解精度設定*/
#define E 0.000001 
double mybsearch(double num)
{
	double start = 1.0;
	double end = num;
    double mid = 0.0;
	while(1)
	{
	    mid = (start + end)/2;
        if(((mid*mid - num) <= E) && ((mid*mid - num) >= -E))
		{
			return mid;
		}

		if ((mid*mid - num) > E)
		{
			end = mid;
		}
		else
		{
			start = mid;
		}
 	}
    return 0;
}
int main()
{
	double num = 0.0;

	/*這裡需要注意:double的輸入方式*/
	scanf("%lf",&num);
	printf("\r\n num %lf的平方根是%lf",num,mybsearch(num));

	return 0;
}

2、如果使用連結串列儲存資料,二分查詢的時間複雜度怎麼計算?
假設連結串列長度為n,二分查詢每次都要找到中間點(計算中忽略奇偶數差異):
第一次查詢中間點,需要移動指標n/2次;
第二次,需要移動指標n/4次;
第三次需要移動指標n/8次;

以此類推,一直到1次為值
總共指標移動次數(查詢次數) = n/2 + n/4 + n/8 + …+ 1,這顯然是個等比數列,根據等比數列求和公式:Sum = 2n - 1.
最後演算法時間複雜度是:O(2n-1),忽略常數,記為O(n),
3、快速定位IP地址對應的省份
當我們要查詢202.102.133.13對應的IP地址的省份的時候,發現IP地址在[202.102.133.0, 202.102.133.255] 這個範圍就返回山東東營市。那我們如何在龐大的地址庫中找到指定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] 江蘇連雲港

我們可以運用上面的第3或第四來通過二分查詢來在有序集合中找到。
我們可以將ip地址轉換成一個int型:202.102.133.13 = 202256+102256+133256+13256,然後在地址庫中找到最後一個小於等於這個地址的數值,他對應的就是我們要查詢的省份。