二分查找

在不重復(fù)升序數(shù)組查找目標(biāo)值
解法1
public static int binarySearchV1(int[] nums, int target) {

  if (nums.length == 0) {
    return -1;
  }

  // 遍歷區(qū)間: [left,right] 
  int left = 0;
  int right = nums.length - 1; // 注意

  // 終止條件 left == right + 1
  while (left <= right) {
    // 相比(left+right)/2可以避免整型溢出
    int mid = left + (right - left) / 2;  
    if (nums[mid] == target) {
      return mid;
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid - 1;
    }
  }
  return -1;
}

說明:
① right的初始值為nums.length-1,表示此次二分查找的區(qū)間為[left,right]瞳秽,為左閉右閉區(qū)間
②根據(jù)①雙閉區(qū)間的特性,while(left <= right)模软,滿足進入循環(huán)體條件 left <= right ,此時終止循環(huán)的條件為 left = right + 1疗我,區(qū)間為[right+1,right]為空區(qū)間,不存在未遍歷元素
③循環(huán)體內(nèi)對left和right的重新賦值南捂,left=mid+1表達式對應(yīng)新的區(qū)間為[mid+1,right]; right = mid-1表達式對應(yīng)新的區(qū)間[left,mid-1]吴裤,兩個區(qū)間都不包含nums[mid]元素,因為這個元素已經(jīng)在if (nums[mid] == target)判斷分支處理過

解法2
public static int binarySearchV2(int[] nums, int target) {

  if (nums.length == 0) {
    return -1;
  }

    // 二分查找遍歷區(qū)間: [left,right]  
    int left = 0;
    int right = nums.length - 1;         // 注意

    // 終止條件為left == right
    while (left < right) {                  // 注意和解法1的區(qū)別 
      int mid = left + (right - left) / 2;
      if (nums[mid] == target) {
        return mid;
      } else if (nums[mid] < target) {
        left = mid + 1; 
      } else if (nums[mid] > target) {
        right = mid - 1;
      }
    }
    // 注意和解法1的區(qū)別 
    return nums[left] == target ? left : -1;
}

說明:
本題和解法1區(qū)間同為[left,right]溺健,但是進入循環(huán)的條件卻是while(left < right)麦牺,那么終止條件為left == right,這將導(dǎo)致到最后存在區(qū)間[left,left]鞭缭,剩余一個元素未被遍歷
①對于剩下最后一個元素未被二分查找遍歷的情況剖膳,此時left=right,因此最后打補丁多判斷一次nums[left]==target

解法3
public static int binarySearchV3(int[] nums, int target) {

  if (nums.length == 0) {
    return -1;
  }

  // 二分查找遍歷區(qū)間: [left,right)  
  int left = 0;
  int right = nums.length;        // 注意

  // 終止條件為left == right+1
  while (left < right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] == target) {
      return mid;
    } else if (nums[mid] < target) {
      left = mid + 1;                
    } else if (nums[mid] > target) {
      right = mid;                  // 注意
    }
  }
  return -1;
}

說明:
本解法采取左閉右開區(qū)間岭辣,二分查找區(qū)間 [left,right)吱晒,同時搭配進入循環(huán)條件while(left < right),此時終止循環(huán)的條件為 left == right易结,區(qū)間為[left,left)為一個空區(qū)間
①不同于解法1和解法2枕荞,循環(huán)體內(nèi)right=mid,而不是right=mid-1搞动,這是因為查找區(qū)間左邊為開區(qū)間躏精,此時新的查找區(qū)間為[left,mid),同樣nums[mid]不再參與下一輪二分查找

在可能重復(fù)升序數(shù)組查找目標(biāo)值左邊界

以下不再對上文已經(jīng)解釋過的內(nèi)容重新說明鹦肿,關(guān)于開閉區(qū)間與left矗烛、right的重新取值以上文為準(zhǔn)

先來看兩個初步解法

解法1
public static int leftBoundV1(int[] nums, int target) {

  if (nums.length == 0) {
    return -1;
  }

  // 二分查找遍歷區(qū)間: [left,right]  
  int left = 0;
  int right = nums.length - 1; 

  // 終止條件left == right+1
  while (left <= right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] == target) {
      right = mid - 1;
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid - 1;
    }
  }
  return left;
}

說明:
①不同于之前查找目標(biāo)值,查找左邊界在 if (nums[mid] == target) 符合條件時箩溃,采取進一步壓縮右邊界 right = mid-1
②為什么最后返回left瞭吃,由于循環(huán)終止條件left = right+1, 當(dāng)查找到左邊界時,一定滿足 if (nums[mid] == target)條件涣旨,此時會執(zhí)行 right = mid -1歪架,由于mid為符合條件的答案,此時mid=right+1=left

解法2
public static int leftBoundV2(int[] nums, int target) {

  if (nums.length == 0) {
    return -1;
  }

  // 二分查找遍歷區(qū)間: [left,right) 
  int left = 0;
  int right = nums.length;

  // 終止條件left == right
  while (left < right) { 
    int mid = left + (right - left) / 2;
    if (nums[mid] == target) {
      right = mid;
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid; 
    }
  }
  return left;
}

說明:
本解法就是對解法1進行區(qū)間[left,right)的改造霹陡,因此在 if (nums[mid] == target) 符合條件時和蚪,壓縮右邊界 right = mid
①解釋為什么返回left,由于左邊界一定符合條件 if (nums[mid] == target) 烹棉,此時會執(zhí)行語句 right = mid攒霹,又因為終止循環(huán)條件為 left == right , 此時存在表達式left == mid == right,所以返回left也可以

解法1和解法2看似完美浆洗,但是沒有解決以下問題:
在數(shù)組nums={1,2,3,4,5} 里查找6催束,將返回5,很明顯nums[5]并不是6

理解這個問題前伏社,先討論下解法1和解法2left和right的取值范圍
解法1:
由于二分查找mid區(qū)間為[left,right]即[0,nums.length-1]
left初始值0抠刺,循環(huán)體內(nèi)存在表達式left=mid+1區(qū)間為[1塔淤,nums.length],合并之后left的區(qū)間為[0,nums.length]
right初始值nums.length-1,循環(huán)體內(nèi)存在表達式right=mid-1區(qū)間為[-1矫付,nums.length-2]凯沪,合并之后right的區(qū)間為[-1,nums.length-1]

解法2:
由于二分查找mid區(qū)間為[left,right)即[0,nums.length)
left初始值0,循環(huán)體內(nèi)存在表達式left=mid+1區(qū)間為[0买优,nums.length+1)妨马,合并之后left的區(qū)間為[0,nums.length+1) 即 [0,nums.length]
right初始值nums.length,循環(huán)體內(nèi)存在表達式right=mid區(qū)間為[0杀赢,nums.length)烘跺,合并之后right的區(qū)間為[0,nums.length) 即 [0,nums.length]

通過上述總結(jié),發(fā)現(xiàn)left的區(qū)間總為[0,nums.length]脂崔,其實可以將left值理解為數(shù)組nums中有多少個元素小于target滤淳,那么在數(shù)組nums={1,2,3,4,5} 里查找6土浸,返回5的問題就很好理解了睛琳,如下圖最后返回left=1,表示有一個元素小于目標(biāo)2


那么可以對解法1進行修改(解法2修改類同默终,不贅述)

public static int leftBoundV3(int[] nums, int target) {

  if (nums.length == 0) {
    return -1;
  }

  // 二分查找遍歷區(qū)間: [left,right]  
  int left = 0, right = nums.length - 1;

  // 終止條件: left = right + 1
  while (left <= right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] == target) {  // 收縮右側(cè)邊界
      right = mid - 1;
    } else if (nums[mid] < target) {
      // 搜索區(qū)間變?yōu)?[mid+1, right], nums[mid]不參與下一輪
      left = mid + 1;
    } else if (nums[mid] > target) {
      // 搜索區(qū)間變?yōu)?[left, mid-1]汇歹,nums[mid]不參與下一輪
      right = mid - 1;
    }
  }

  if (left >= nums.length || nums[left] != target) {
    return -1;
  }
  return left;
}

說明:
既然left取值區(qū)間為[0,nums.length]屁擅,那么新增判斷l(xiāng)eft是否超過nums.length-1
同時,由于left表示數(shù)組nums內(nèi)有多少元素小于target产弹,不代表target一定在數(shù)組內(nèi)派歌,新增判斷nums[left]!=target

在可能重復(fù)升序數(shù)組查找目標(biāo)值右邊界

和查找左邊界一樣,先來看初步解法

解法1
public static int rightBoundV1(int[] nums, int target) {

  if (nums.length == 0) {
    return -1;
  }

  // 二分查找遍歷區(qū)間: [left,right]
  int left = 0, right = nums.length - 1;

  // 終止條件left == right+1
  while (left <= right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] == target) {
      left = mid + 1;             // 注意
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid - 1;
    }
  }
  return left - 1;     // 注意
}

說明:
①為什么左邊界返回left痰哨,右邊界返回left-1?
由于最后一個滿足target的右邊界滿足條件 if (nums[mid] == target) 胶果,此時執(zhí)行l(wèi)eft = mid +1,nums[target]==target斤斧,那么這個時候存在表達式left-1=mid=right早抠,因此最后返回left-1
②left、right區(qū)間撬讽?
由于mid 取值區(qū)間 [0,nums.length-1]
left初始值0蕊连,mid+1取值區(qū)間為[1,nums.length],因此合并后left區(qū)間為[0,nums.length]
right 初始值nums.length-1锐秦,mid-1取值區(qū)間為[-1,nums.length-2]咪奖,因此合并后right區(qū)間 [-1,nums.length-1]

解法2
public static int rightBoundV2(int[] nums, int target) {

  if (nums.length == 0) {
    return -1;
  }

  // 二分查找遍歷區(qū)間: [left,right)  
  int left = 0, right = nums.length;

  // 終止條件left == right
  while (left < right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] == target) {
      left = mid + 1;                   // 注意
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid;
    }
  }
  return left - 1;     // 注意
}

說明:
①為什么左邊界返回left盗忱,右邊界返回left-1?
同解法1
②left酱床、right區(qū)間?
由于mid 取值區(qū)間 [0,nums.length)
left初始值0趟佃,mid+1取值區(qū)間為[1,nums.length+1)扇谣,因此合并后left區(qū)間為[0,nums.length+1)昧捷,即[0,nums.length]
right 初始值nums.length,mid取值區(qū)間為[0,nums.length)罐寨,因此合并后right區(qū)間 [0,nums.length]
③可以看到無論是雙閉區(qū)間還是左閉右開區(qū)間靡挥,求右邊界統(tǒng)一解答都是left-1(right則由于循環(huán)結(jié)束條件不同而不同)

最后同樣需要處理一下target不在數(shù)組內(nèi)部的情況

解法3
public static int rightBoundV3(int[] nums, int target) {

  if (nums.length == 0) {
    return -1;
  }

  // 搜索區(qū)間 [left,right]
  int left = 0, right = nums.length - 1;

  // 終止條件  left = right+1
  while (left <= right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] == target) {
      left = mid + 1;
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid - 1;
    }
  }

  if (right < 0 || nums[right] != target) {    // 注意
    return -1;
  }

  return right;
}

說明:
①和解法1一樣,本題left區(qū)間為[0,nums.length]鸯绿,right區(qū)間[-1,nums.length-1]跋破,循環(huán)終止條件left = right+1,此時解為 mid = left-1=right瓶蝴,因此需要判斷right是否小于0
②同時需要判斷nums[right] != target毒返,排除target不在數(shù)組的情況,此時的left-1或者right可以理解為有多少個元素小于等于target

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末舷手,一起剝皮案震驚了整個濱河市拧簸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌男窟,老刑警劉巖盆赤,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異歉眷,居然都是意外死亡牺六,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門姥芥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來兔乞,“玉大人,你說我怎么就攤上這事凉唐∮棺罚” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵台囱,是天一觀的道長淡溯。 經(jīng)常有香客問我,道長簿训,這世上最難降的妖魔是什么咱娶? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮强品,結(jié)果婚禮上膘侮,老公的妹妹穿的比我還像新娘。我一直安慰自己的榛,他們只是感情好琼了,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般雕薪。 火紅的嫁衣襯著肌膚如雪昧诱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天所袁,我揣著相機與錄音盏档,去河邊找鬼。 笑死燥爷,一個胖子當(dāng)著我的面吹牛蜈亩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播前翎,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼勺拣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鱼填?” 一聲冷哼從身側(cè)響起药有,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎苹丸,沒想到半個月后愤惰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡赘理,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年宦言,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片商模。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡奠旺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出施流,到底是詐尸還是另有隱情响疚,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布瞪醋,位于F島的核電站忿晕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏银受。R本人自食惡果不足惜践盼,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宾巍。 院中可真熱鬧咕幻,春花似錦、人聲如沸顶霞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至绷耍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鲜侥,已是汗流浹背褂始。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留描函,地道東北人崎苗。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像舀寓,于是被迫代替她去往敵國和親胆数。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容