二分查找

二分查找算法基本思想
二分查找算法的前置條件是,一個已經排序好的序列(在本篇文章中爲了說明問題的方便,假設這個序列是升序排列的),這樣在查找所要查找的元素時,首先與序列中間的元素進行比較,如果大於這個元素,就在當前序列的後半部分繼續查找,如果小於這個元素,就在當前序列的前半部分繼續查找,直到找到相同的元素,或者所查找的序列範圍爲空爲止.

用僞代碼來表示, 二分查找算法大致是這個樣子的:

left = 0, right = n -1
while (left <= right)
    mid = (left + right) / 2
    case
        x[mid] < t:    left = mid + 1;
        x[mid] = t:    p = mid; break;
        x[mid] > t:    right = mid -1;

return -1;


第一個正確的程序
根據前面給出的算法思想和僞代碼, 我們給出第一個正確的程序,但是,它還有一些小的問題,後面會講到
int search(int array[], int n, int v)
{
    int left, right, middle;

    left = 0, right = n - 1;

    while (left <= right)
    {
        middle = (left + right) / 2;
        if (array[middle] > v)
        {
            right = middle;
        }
        else if (array[middle] < v)
        {
            left = middle;
        }
        else
        {
            return middle;
        }
    }

    return -1;
}


下面,講講在編寫二分查找算法時可能出現的一些問題.

邊界錯誤造成的問題
二分查找算法的邊界,一般來說分兩種情況,一種是左閉右開區間,類似於[left, right),一種是左閉右閉區間,類似於[left, right].需要注意的是, 循環體外的初始化條件,與循環體內的迭代步驟, 都必須遵守一致的區間規則,也就是說,如果循環體初始化時,是以左閉右開區間爲邊界的,那麼循環體內部的迭代也應該如此.如果兩者不一致,會造成程序的錯誤.比如下面就是錯誤的二分查找算法:
int search_bad(int array[], int n, int v)
{
    int left, right, middle;

    left = 0, right = n;

    while (left < right)
    {
        middle = (left + right) / 2;
        if (array[middle] > v)
        {
            right = middle - 1;
        }
        else if (array[middle] < v)
        {
            left = middle + 1;
        }
        else
        {
            return middle;
        }
    }

    return -1;
}

這個算法的錯誤在於, 在循環初始化的時候,初始化right=n,也就是採用的是左閉右開區間,而當滿足array[middle] > v的條件是, v如果存在的話應該在[left, middle)區間中,但是這裏卻把right賦值爲middle - 1了,這樣,如果恰巧middle-1就是查找的元素,那麼就會找不到這個元素.

下面給出兩個算法, 分別是正確的左閉右閉和左閉右開區間算法,可以與上面的進行比較:

下面這兩個算法是正確的

int search2(int array[], int n, int v)
{
    int left, right, middle;

    left = 0, right = n - 1;

    while (left <= right)
    {
        middle = (left + right) / 2;
        if (array[middle] > v)
        {
            right = middle - 1;
        }
        else if (array[middle] < v)
        {
            left = middle + 1;
        }
        else
        {
            return middle;
        }
    }

    return -1;
}

int search3(int array[], int n, int v)
{
    int left, right, middle;

    left = 0, right = n;

    while (left < right)
    {
        middle = (left + right) / 2;

        if (array[middle] > v)
        {
            right = middle;
        }
        else if (array[middle] < v)
        {
            left = middle + 1;
        }
        else
        {
            return middle;
        }
    }

    return -1;
}


死循環
上面的情況還只是把邊界的其中一個寫錯, 也就是右邊的邊界值寫錯, 如果兩者同時都寫錯的話,可能會造成死循環,比如下面的這個程序:
int search_bad2(int array[], int n, int v)
{
    int left, right, middle;

    left = 0, right = n - 1;

    while (left <= right)
    {
        middle = (left + right) / 2;
        if (array[middle] > v)
        {
            right = middle;
        }
        else if (array[middle] < v)
        {
            left = middle;
        }
        else
        {
            return middle;
        }
    }

    return -1;
}

這個程序採用的是左閉右閉的區間.但是,當array[middle] > v的時候,那麼下一次查找的區間應該爲[middle + 1, right], 而這裏變成了[middle, right];當array[middle] < v的時候,那麼下一次查找的區間應該爲[left, middle - 1], 而這裏變成了[left, middle].兩個邊界的選擇都出現了問題, 因此,有可能出現某次查找時始終在這兩個範圍中輪換,造成了程序的死循環.

溢出
前面解決了邊界選擇時可能出現的問題, 下面來解決另一個問題,其實這個問題嚴格的說不屬於算法問題,不過我注意到很多地方都沒有提到,我覺得還是提一下比較好.
在循環體內,計算中間位置的時候,使用的是這個表達式:
middle = (left + right) / 2;

假如,left與right之和超過了所在類型的表示範圍的話,那麼middle就不會得到正確的值.
所以,更穩妥的做法應該是這樣的:
middle = left + (right - left) / 2;

更完善的算法
前面我們說了,給出的第一個算法是一個"正確"的程序, 但是還有一些小的問題.
首先, 如果序列中有多個相同的元素時,查找的時候不見得每次都會返回第一個元素的位置, 比如考慮一種極端情況:序列中都只有一個相同的元素,那麼去查找這個元素時,顯然返回的是中間元素的位置.
其次, 前面給出的算法中,每次循環體中都有三次情況,兩次比較,有沒有辦法減少比較的數量進一步的優化程序?
<<編程珠璣>>中給出瞭解決這兩個問題的算法,結合前面提到溢出問題我對middle的計算也做了修改:
int search4(int array[], int n, int v)
{
    int left, right, middle;

    left = -1, right = n;

    while (left + 1 != right)//這個循環維持的條件是left<right && array[left]<v<=array[right],所以到最後的時候,
    {//如果可以找到目標,則只剩下兩個數,並且滿足 array[left]<v<=array[right],是要查找的數是right
        middle = left + (right - left) / 2;

        if (array[middle] < v)//必須保證array[left]<v<=array[right],所以left = middle;
        {//如果left =middle+1,則有可能出現 array[left]<=v的情況
            left = middle;
        }
        else
        {
            right = middle;
        }
    }

    if (right >= n || array[right] != v)
    {
        right = -1;
    }

    return right;
}

這個算法是所有這裏給出的算法中最完善的一個,正確,精確且效率高.

但是這個算法的還是不能很好的理解

可以用下面的算法,可以找出滿足條件的數


int Bi_Search(int a[],int n,int b)//
{//返回等於b的第一個
	if(n==0)
		return -1;
	int low = 0;
	int high = n-1;
	int last = -1;//用last記錄上一次滿足條件的下標
	while (low<=high)
	{
		int mid = low +(high-low)/2;
		if (a[mid]==b)
		{
			last = mid;
			high = mid -1;
		}
		else if(a[mid]>b)
			high = mid -1;
		else
			low = mid +1;
	}

	return last;

}
int Bi_Search1(int a[],int n,int b)//大於b的第一個數
{
	if(n<=0)
		return -1;
	int last = -1;
	int low = 0;
	int high = n-1;
	while (low<=high)
	{
		int mid = low +(high - low)/2;
		if(a[mid]>b)
		{
			last = mid;
			high = mid -1;
		}
		else if (a[mid]<=b)
		{
			low =mid +1;
		}
	}

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