二分查找的變形應用3----legend050709

二分查找的應用總結

(1)n的平方根保留m位小數

(2)從小到大的有序數組循環右移n(n>=0)位,查找最小值;

(3)從小到大的有序數組中,查找絕對值最小的元素;

(4)從小到大的有序數組循環右移n(n>=0)位,查找某個特定值

(5) 二分查找變形之:
      變體一:查找第一個值等於給定值的元素;

      變體二:查找最後一個值等於給定值的元素;

      變體三:查找第一個大於等於給定值的元素;

      變體四:查找最後一個小於等於給定值的元素;

(6) 有序的無重疊區間數組,如何查找某個元素屬於哪個區間?

**************************

(0)概述:
1)二分查找的本質;

二分查找的本質在於判斷目標在哪個區間,然後擠壓法不斷縮小該區間即可;

 

2)數組的雙指針問題其實經常會用到:

1)二分查找;

2)將數組中負數放入到正數之前;

3)快速排序;

4)數組逆序;

------

(1)n的平方根保留m位小數

(1.1)二分查找源碼

public static int binarySearch(int[] arr, int target){
    int low = 0;
    int high = arr.length-1;
    while (low <= high){
        int mid = (low + high) / 2;
        if (target > arr[mid]){
            low = mid + 1;
        }else if (target < arr[mid]){
            high = mid - 1;
        }else {
            return mid;
        }
    }
    return -1;
}

(1.2)分析

求n的平方根x,則首先想到的是可不可以從1開始,分別求1、2、3等的平方,看是否等於n。這裏就有個問題我爲什麼首先從1開始,而且爲什麼各個數的步長爲1?也就是說我們應該從哪裏開始去試,每次去試時,各個數的步長應該怎麼選?

從上面的分析中得出求n的平方根其實就是從小於n的數中找到x,使x*x等於n

這就變成了一個查找的問題,而且是在有序數據集中查找,則最容易想到的就是二分查找;

則從哪裏開始去試,每次去試時,各個數的步長問題就都解決了。

對於一個數的平方根能夠找到該跟的範圍,即xx < n < yy,則n的平方根在x和y之間,且x+1=y
找到平方根所在的區域之後,把所要保留的精度的平方根做爲步長對x進行遞增,直到|n-x*x|<0.0001(精度)爲止。

 

(1.3)實現

例如求12的平方根,精度爲小數點後2位。代碼示例如下:

(1.3.1)實現一

//查找x的平方小於等於n的最後一個元素x;

public static double sqrtByInc(int n, double precision){
    int low = 0;
    int high = n;
    int mid = 0;
    // 二分查找找到平方根的區域
    while (low <= high){
        mid = (low + high) / 2;
        if (mid * mid > n){
            high = mid - 1;
        }else {
            if ((mid == n-1) || (mid * mid > n)) {
                break;
            }
            low = mid + 1;
        }
    }
    // 按照精度的平方根做爲步長
    double r = mid; //r爲double類型;
    while (Math.abs(n-r*r) > 0.0001){
        if (r * r < n){
            r += 0.01;
            r = BigDecimal.valueOf(r).setScale(2, RoundingMode.HALF_UP).doubleValue();
        }else {
            break;
        }
    }	
    return r;
}

說明:

上面的方法只是用二分查找找到平方根的範圍,然後遞增步長進行嘗試,雖然能夠快速找到根的範圍,但是遞增步長將是一個漫長的等待。

 

(1.3.2)實現二


double sqrt(double x)
{
    double low = 0;
    double up = x;
    double mid = (low + up) / 2;
    while(fabs(low - up) >= 1e-2)  //循環結束條件爲 low  和 up之間的差距小於 0.01
    {

       if(fabs(mid * mid - x) < 1e-4)
            return mid;
        if(mid * mid > x)
            up = mid; // 此中不設置 up= mid-1;
        else if(mid * mid < x)
            low = mid; //此中不設置 low = mid +1; 因爲求平方根,存在小數;
        mid = (up + low) / 2;
    }
  DecimalFormat df = new DecimalFormat("0.00");
return df.format(mid); // mid取2位小數;
}

ps: 感覺上面如果精度爲2位小數,其實是可以 up=mid-0.01; low=mid+0.01的;


注:mid的2位小數如何獲取的問題?

1》借住printf的精度打印;nprintf保持到字符串問題;

2》long  a = mid, 則a保持的是正數報文;

      double target = (double)a + (double)( mid - a)/0.01/100 

       //mid- a 保持的是小數部分;整體就得到了保留2位小數的情況;

------------------

(2)有序循環數組查找指定值;

(2.1)分析

比如[0,1,2,4,5,6,7]右移4位變爲[4,5,6,7,0,1,2],在[4,5,6,7,0,1,2]中查找某個元素,事先不知道移動了多少位;

對於有序序列,首先可以想到的是利用二分法查找,但是序列別移動後的起點不再是最左邊的位置。比如說上面的例子,起點0在下標爲4的位置,所以不一定滿足nums[0] < nums[n - 1],不能直接使用二分法。 
但是因爲是整體移動,局部仍然是有序的,所以如果確定了某個區間是有序的,那麼還是可以使用二分法的;

下面的*代表起點

1)情形1:

o   *   o   o   o   o   o
大          小          中
left       middle      right
如果起點在middle的左側,那麼nums[left],nums[middle],nums[right]的大小關係如上
可通過nums[middle] < nums[right]判斷起點在middle左邊;

2)情形2:

o   o   o   o   o   *   o
中          大          小
left       middle      right
如果起點在middle的右側,那麼nums[left],nums[middle],nums[right]的大小關係如上
可通過nums[middle] > nums[right]判斷起點在middle右邊;

3)說明:

以第一種情況爲例(起點在middle左側)即nums[middle] < nums[right] (nums[middle] < nums[right] 和 起點在middle左側是等價的)
如果目標元素在[middle, right]區間,那麼一定有nums[target] >= nums[middle] && nums[target] <= nums[right] 
否則,目標元素在[left, middle]區間內 
因爲[middle, right]一定是遞增的,所以可以判斷目標元素是否在這個區間內,而[left, middle]區間不是遞增的,故不能判斷。

第二種情況同理 ;

(2.2)注意點

該題和有序數組循環右移n位,然後查找最小值類似;

但是注意特殊情況:比如n=0的情況;比如,數組中存在重複元素的情況,如最小值重複,其他值重複等;

(2.3)實現

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        while(left < right)
        {
            int middle = (left + right) / 2;
            if(nums[middle] == target)
                return middle;

            /* 第一種情況,起點在middle左邊 */
            if(nums[middle] < nums[right])
            {
                /* 目標在[middle, right]區間,因爲nums[middle]已經比較過了,所以middle不需要等號 */
                if(target > nums[middle] && target <= nums[right])
                    left = middle + 1;  // 右邊不動,從左邊往右擠;
                else
                    right = middle - 1;

                     //目標值在mid的左邊 && mid左邊可能不是有序的;

                    //不過沒有關係;繼續劃分左邊;
            }
            /* 第二種情況,起點在middle右邊 */
            else
            {
                /* 目標在[left, middle]區間,middle不需要等號,原因同上 */
                if(target < nums[middle] && target >= nums[left])
                    right = middle - 1;
                else
                    left = middle + 1; // target在 mid右邊,但是右邊可能不是有序的;
            }
        }

        return left < nums.size() && nums[left] == target ? left : -1;

        // 最終看是否查找到;
    }

};

注:感覺這個是不是還要考慮一下特殊情況,

        比如整體本來就是有序的;

-------------------------

(3)有序循環數組中查找最小值

(3.1)分析

給出[4,4,5,6,7,0,1,2]  返回 0;

其實整體和上面類似;

此中給出另外一種類似的思路:
 

1.      如果arr[L]<arr[R],說明該數組是有序的,自然最小值在最左邊

2.      如果arr[L]>=arr[R],說明L到R範圍內包含循環部分,比如2 2 3 1 2,此時我們考察第一個數和中間的那個數的大小

(1)    如果arr[L]>arr[M],此時說明最小的那個數只能在L到M範圍內,比如 7 8 9 1 2 3 4 5 6。因爲只有當arr[M]是循環過的部分時,纔有arr[L]>arr[M]出現。

(2)    如果arr[M] >arr[R],此時說明最小的那個數只能在M到R範圍內,因爲只有當arr[M]不是循環部分的時候,纔會有arr[M] >arr[R],比如4 5 6 7 8 9 1 2 3
(3)    上述兩種情況不滿足時,說明arr[L]<=arr[M]並且arr[M] <=arr[R],此時又有條件arr[L]>=arr[R],說明arr[L]=arr[M] =arr[R],其實這種情況,無法再繼續用二分查找,比如數組2 2 …2 1 2… …2 2(只有一個1其實都是2),無論1出現在哪個位置都滿足有序循環數組的條件,此時找到1只能用遍歷的方式。當然,我們可以將左指針右移一位,略過一個相同數字,這對結果不會產生影響,因爲我們只是去掉了一個相同的,然後對剩餘的部分繼續用二分查找法,在最壞的情況下,比如數組所有元素都相同,時間複雜度會升到O(n),也就是遍歷。
 

(3.2)代碼實現

public class CircularArrayMinimumNum {
    
    //遍歷,複雜度爲O(n)
    public static int getMinNum_1(int[] num) {
        
        int min = num[0];
        for (int i = 1; i < num.length; i++) {
            if (num[i] < min)
                min = num[i];
        }
        return min;
    }
    
    //二分搜索,複雜度O(logN),最壞情況O(n)
    public static int getMinNum(int[] num) {
 
        if (num == null || num.length == 0)
            return -1;
        int left = 0;
        int right = num.length - 1;
        if (num[left] >= num[right]) {
            while (left<right-1) {
                int mid = left + (right - left) / 2;
                if (num[left] > num[mid])
                    right = mid;  

                    // 最小值在[left,mid]區間;不能用mid-1,因爲中間的數也有可能是最小的;從右邊往左擠;
                else if(num[mid] > num[right])  
                    left = mid;

                   //最小值在[mid,right]區間;該條件可替換爲 num[left] < num[mid];從左邊往右邊擠;
                else  // num[left] <= num[mid] <=num[right],此時就是有序的了。只能是
                    left++;  //這種情況,把左指針右移,略過相同數字
            }
            return Math.min(num[left],num[right]);
        }
        return num[0];
    }
 
}

上面是數組中出現重複數字的情況,那麼當數組沒有重複數字時,那麼就更簡單了,把上面的代碼刪掉相等的情況就可以,

如下:

public class CircularArrayMinimumNum_2 {
    
    public static int getMinNum(int[] nums) {
 
        if (nums == null || nums.length == 0)
            return -1;
        int left = 0;
        int right = nums.length - 1;
        if (nums[left] > nums[right]) {
            while (left< right -1 ) {
                int mid = left + (right - left) / 2;
                if (nums[left] > nums[mid])
                    right = mid;  // 最小值在left-mid區間;不能用mid-1,因爲中間的數也有可能是最小的
                else //if(num[mid] > num[right])   //該條件可替換爲 num[left] < num[mid]
                    left = mid;
//                else  //num[left]==num[right]==num[mid]
//                    left++;  //這種情況,把左指針右移,略過相同數字
            }
            return Math.min(nums[left],nums[right]);
        }
        return nums[0];
    }
 
}

--------------------------

(4)從小到大的有序數組中,查找絕對值最小的元素

(4.1)分析

數組是從小到大排序,數值可能爲負數、0、正數。

問題的本質是找到正數的最小值,以及負數的最大值:

分析以下集中情況

數組爲a[], 數組大小爲n.

1)n=1,沒有商量的餘地,直接返回
2)a[0] * a[n-1] >= 0,說明這些元素同爲非正或同爲非負。要是a[0]>=0,返回a[0];否則返回a[n-1]
3)a[0] * a[n-1] < 0,說明這些元素中既有正數,也有負數,分爲下面幾種情況:

此時需要計算中間位置爲 mid = (low + high)/2;

如果a[low] * a[mid] >=0 說明a[mid]也爲非正,縮小範圍low=mid;

如果a[mid]*a[high] >=0,說明a[mid]非負,縮小範圍high=mid。

在期間如果還有兩個元素,那麼就比較以下他倆,直接返回了;

(4.2)注意點

比如:

全是負數的情況;

全是正數的情況;

有負數,有正數,有0的情況;

有正數,有負數,存在正數的絕對值和負數的絕對值都是最小值;

(4.3)實現

#include <iostream>
#include <cmath>
using namespace std;

int absMin(int *a, int size)
{
    if(size == 1)
        return a[0];
    if(a[0] * a[size-1] >= 0)
        return (a[0] >= 0) ? a[0] : a[size-1];
    else
    { // 有正有負;
        int low = 0, high = size-1, mid;
        while(low < high)
        {
            if(low + 1 == high)
                return abs(a[low]) < abs(a[high]) ? a[low] : a[high];
            mid = low + (high - low) / 2;
            if(a[low] * a[mid] >= 0)
                low = mid;
            if(a[high] * a[mid] >= 0)
                high = mid;
        }
    }
}

int main()
{
    int arr1[] = {-8, -3, -1, 2, 5, 7, 10};
    size_t size1 = sizeof(arr1) / sizeof(int);
    int minabs1 = absMin(arr1, size1);
    cout << "Result:" << minabs1 << endl;

    int arr2[] = {-8, -3, 2, 5, 7, 10};
    size_t size2 = sizeof(arr2) / sizeof(int);
    int minabs2 = absMin(arr2, size2);
    cout << "Result:" << minabs2 << endl;
}

---------------------

(5) 二分查找變形:
      (5.1)查找第一個值等於給定值的元素;

分析:

查找第一個等於指定值的元素;當a[mid]==指定值時,需要不斷的往左邊擠,即high=mid-1;

但是需要考慮特殊情況:
比如左邊沒有元素了,或者左邊的元素不再等於該值;

 

實現:

// 二分查找:查找第一個值等於給定值的元素
public int bSearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while(low <= high) {
    int mid = low + ((high - low) >> 1); // 如果不明白這種寫法可以看看前面的一篇文章
    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;
      } else {
        high = mid - 1;
      }
    }
  }
  return -1;
}

 

(5.2)查找最後一個值等於給定值的元素;

分析:

查找最後等於指定值的元素;當a[mid]==指定值時,需要不斷的往右邊擠,即low=mid+1;

但是需要考慮特殊情況:
比如右邊沒有元素了,或者右邊的元素不再等於該值;

 

實現:

// 二分查找:查找最後一個值等於給定值的元素
public int bSearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while(low <= high) {
    int mid = low +((high - low) >> 1);
    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;
      } else {
        low = mid + 1;
      }
    }
  }
  return -1;
}

 (5.3)查找第一個大於等於給定值的元素;

分析:

查找第一個大於等於指定值的元素;當a[mid]>=指定值時,需要不斷的往左邊擠,即high=mid-1;

但是需要考慮特殊情況:
比如左邊沒有元素了,或者左邊的元素不再大於等於目標值;

 

實現:

// 二分查找:查找第一個大於等於給定值的元素
public int bSearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while(low <= high) {
    int mid = low + ((high - low) >> 1);
    if(a[mid] >= value) {
      if((mid == 0) || (a[mid - 1] < value)) {
        return mid;
      } else {
        high = mid - 1;
      }
    } else {
      low = mid + 1;
    }
  }
  return -1;
}

擴展:

 

(5.4)查找最後一個小於等於給定值的元素;

分析:

查找最後一個小於等於指定值的元素;當a[mid]<=指定值時,需要不斷的往右邊擠,即low=mid+1;

但是需要考慮特殊情況:
比如右邊沒有元素了,或者右邊的元素不再小於等於目標值;

 

實現:

//二分查找:查找最後一個小於等於給定值的元素
public int bSearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while(low <= high) {
    int mid = low + ((high - low) >> 1);
    if(a[mid] > value) {
      high = mid - 1;
    } else {
      if((mid == n - 1) || (a[mid + 1] > value)) {
        return mid;
      } else {
        low = mid + 1;
      }
    }
  }
  return -1;
}

擴展:

如果是二叉查找樹,其實也是可以實現的。

某個節點的值小於等於目標值時,得到該節點的左子樹的最右邊,以及該節點的右子樹的最左邊,和當前的節點進行比較;

------------

(6) 有序的區間數組,如何查找某個元素屬於哪個區間?(6.1)範例:

通過 IP 地址來查找 IP 歸屬地的功能;假設在內存中有 12 萬條這樣的 IP 區間與歸屬地的對應關係,如何快速定位出一個 IP 地址的歸屬地呢?

通過維護一個很大的 IP 地址庫來實現的。地址庫中包括 IP 地址範圍和歸屬地的對應關係。比如,當我們想要查詢 202.102.133.13 這個 IP 地址的歸屬地時,我們就在地址庫中搜索,發現這個 IP 地址落在 [202.102.133.0, 202.102.133.255] 這個地址範圍內,那我們就可以將這個 IP 地址範圍對應的歸屬地“山東東營市”顯示給用戶了。

(6.1)分析:

1)方法一:
首先ip地址範圍都是沒有交叉重疊的。所以IP地址範圍的排序可以認爲是地址範圍的起始ip的排序;

這個問題就可以轉化爲我剛講的第四種變形問題“在有序數組中,查找最後一個小於等於某個給定值的元素”了。當我們要查詢某個 IP 歸屬地時,我們可以先通過二分查找,找到最後一個起始 IP 小於等於這個 IP 的 IP 區間,然後,檢查這個 IP 是否在這個 IP 區間內,如果在,我們就取出對應的歸屬地顯示;如果不在,就返回未查找到。

 

總結:

凡是用二分查找能解決的,絕大部分我們更傾向於用散列表或者二叉查找樹。

即便是二分查找在內存使用上更節省,但是比內存如此緊缺的情況並不多。那二分查找真的沒什麼用處了嗎?

實際上,上一節講的求“值等於給定值”的二分查找確實不怎麼會被用到,二分查找更適合用在“近似”查找問題,在這類問題上,二分查找的優勢更加明顯。比如今天講的這幾種變體問題,用其他數據結構,比如散列表、二叉樹,就比較難實現了。

 

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