資料結構與演算法學習--二分查詢
二分查詢(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,然後在地址庫中找到最後一個小於等於這個地址的數值,他對應的就是我們要查詢的省份。