近日刷了很多二分法相關的題目,想以一文簡單總結一下
文章目錄
1.二分查找經典框架
- 1.相鄰就退出 start + 1<end
- 2.求中點要防溢出
- 3.三種情況分開寫
- 4.雙重檢查更保險
public static int binarySearch(int[] nums, int target) {
// write your code here
if (nums == null || nums.length == 0){
return -1;
}
int start = 0;
int end = nums.length - 1;
//要素1.相鄰就退出 start + 1<end
while (start + 1<end){
// mid = (start + end) /2 這種寫法在大多數情況下是對的
// 但是如果 start = 2^31 -1 end = 2^32 - 1 明顯這會發生一個整數溢出
// 要素2.求中點要防溢出
int mid = (end - start)/2 + start;
// 要素3.三種情況分開寫
if (nums[mid] == target){
return mid; //這裏的寫法要根據題目來 如果是找到並返回, 則此處可以直接返回
// end = mid; //如果是找滿足target的第一個位置 則這裏繼續往前面找
//start = mid; //如果是找滿足target的最後一個位置 則此處繼續往後找
}else if(target < nums[mid]){
//在左區間
end = mid;
}else {//在右區間
start = mid;
}
}
// 要素4.雙重檢查更保險
if (nums[start] == target){
return start;
}
if (nums[end] == target){
return end;
}
return -1;
}
2.First/Last Position
2.1First Position(lintcode14)
給定一排序數組,其中元素可能重複,給定一目標數,尋找該目標數在數組中首次出現的位置First Position
- 非常清晰的二分法的應用,尋找最開始的,所以往最前面找
- 關鍵點
if (nums[mid] == target){ //尋找最開始的位置 所以往前找 end = mid; ........... //因爲找最開始的位置,所以先檢查前面的位置 if (nums[start] == target){ return start; } if (nums[end] == target){ return end; }
- 完整代碼
*/
public static int binarySearch02(int[] nums, int target) {
if (nums == null || nums.length == 0){
return -1;
}
int start = 0;
int end = nums.length - 1;
//1.相鄰就退出
while (start+1<end){
//2.求中點 放溢出
int mid = (end - start)/2 + start;
//3.三種情況分別判斷
if (nums[mid] == target){
//尋找最開始的位置 所以往前找
end = mid;
}else if (target<nums[mid]){
end = mid;
}else {
start = mid;
}
}
//4.二次判斷更保險
if (nums[start] == target){
return start;
}
if (nums[end] == target){
return end;
}
return -1;
}
2.2First Bad Version(lintcode74)
尋找第一個壞的版本,思路和2.1幾乎完全一樣,,都是尋找第一個滿足條件的。需要注意的點也都一樣第一個壞的版本
- 完整代碼
public int findFirstBadVersion(int n) {
// write your code here
//可利用二分查找來解決
//如果每次查找 中點是壞的 則往前找
//如果每次查找 中點是好的 則往後找
//這裏要注意這裏不是數組下標
int start = 1;
int end = n;
while (start + 1 < end){
int mid = (end - start)/2 + start;
//如果中點對應版本是壞的 返回true 則繼續往前半部分找
if (SVNRepo.isBadVersion(mid)){
end = mid;
}else {
//如果中點對應版本是好的 返回false 則繼續往後半部分找
start = mid;
}
}
//二次判斷
//從while中退出時 start 和 end一定是相鄰的
//那麼顯然這兩個之中 一定有一個是壞的 但是題目求最近的那一個
//所以先驗證start
//如果不行 則一定是end
if (SVNRepo.isBadVersion(start)){
return start;
}
return end;
}
2.3 Last Position(lintcode458)
和First Position剛好相反,尋找滿足添加的最後一個數。那關鍵的地方也正好相反。Last Position
- 首先要注意,找到滿足添加的點後繼續往後半部分找
int mid = (end - start)/2 + start; if (target == nums[mid]){ //由於這裏是尋找 最後一個等於target的位置, 顯然應該儘可能的往後面找(這裏和First position的區別) //所以如果當前的mid = target的話 則繼續往後尋找 start = mid; }
- 其次就是最後判斷的時候先判斷end
//因爲是找最後一個 所以先判斷後者 if (nums[end] == target){ return end; } if (nums[start] == target){ //start 和 end一定相鄰,如果其中一個不等於目標 則最後的一個一定是start return start; } return -1;
- 完整代碼
public static int lastPosition(int[] nums, int target) { // write your code here if (nums == null || nums.length == 0){ return -1; } int start = 0; int end = nums.length -1; while (start +1 < end){ int mid = (end - start)/2 + start; if (target == nums[mid]){ //由於這裏是尋找 最後一個等於target的位置, 顯然應該儘可能的往後面找(這裏和First position的區別) //所以如果當前的mid = target的話 則繼續往後尋找 start = mid; }else if (target < nums[mid]){ end = mid; }else { start = mid; } } //因爲是找最後一個 所以先判斷後者 if (nums[end] == target){ return end; } if (nums[start] == target){ //start 和 end一定相鄰,如果其中一個不等於目標 則最後的一個一定是start return start; } return -1; }
3.尋找插入位置(leetcode35)
二分法的簡單應用,在給定數組中尋找給定的target,如果找到則返回位置,如果沒找到則返回該目標數應該插入的位置,插入後數組仍然有序。尋找插入位置
-
1.首先兩種特殊情況的判斷,如果給定數小於數組的最小值和大於數組的最大值,則插入位置是固定的。
//首先判斷兩種特殊的情況 if(target < nums[0]){ return 0; } if(target > nums[nums.length - 1]){ return nums.length; }
-
2.如果找到,則顯然可以直接返回位置
int mid = (end - start)/2 + start; // 要素3.三種情況分開寫 循環內部不返回 if (nums[mid] == target){ return mid; }
-
3.跳出while繼續判斷,此時start 和 end一定相鄰,先判斷start是否與之相等,如果不等,則end也不可能與之相等,則返回應該插入的位置,顯然應該是end的所在位置。
if (target == nums[start] ){ return start; } //這裏就是處理 在給定數組的中間尋找 找不到的情況 那麼target 一定是處於 start 和end之間 //之間的(因爲相鄰即退出)那麼 要返回插入的位置 一定是end return end;
-
4.完整代碼
if (nums == null || nums.length == 0){ return -1; } //首先判斷兩種特殊的情況 if(target < nums[0]){ return 0; } if(target > nums[nums.length - 1]){ return nums.length; } int start = 0; int end = nums.length - 1; //要素1.相鄰就退出 start + 1<end while (start + 1<end){ // mid = (start + end) /2 這種寫法在大多數情況下是對的 // 但是如果 start = 2^31 -1 end = 2^32 - 1 明顯這會發生一個整數溢出 // 要素2.求中點要防溢出 int mid = (end - start)/2 + start; // 要素3.三種情況分開寫 循環內部不返回 if (nums[mid] == target){ return mid; }else if(target < nums[mid]){ //在左區間 end = mid; }else {//在右區間 start = mid; } } // 要素4.雙重檢查更保險 //下面兩種if是能找到的情況 則正常返回 if (target == nums[start] ){ return start; } //這裏就是處理 在給定數組的中間尋找 找不到的情況 那麼target 一定是處於 start 和end之間 //之間的(因爲相鄰即退出)那麼 要返回插入的位置 一定是end return end; }
4.區間查找(leetcode34)
給的數組和目標數,查找目標數在數組中的起始位置。
區間查找
- 完全就是
2.1
和2.3
的一個結合 - 完整代碼
public int[] searchRange02(int[] nums, int target) { int[] rs = {-1,-1}; if (nums == null || nums.length == 0){ return rs; } rs[0] = findFirst(nums, target); rs[1] = findLast(nums, target); return rs; } /** * 20190802 * 找到滿足條件的第一個位置 * @param nums * @param target * @return */ public static int findFirst(int[] nums, int target){ int start = 0; int end = nums.length - 1; while (start +1 <end){ int mid = (end - start)/2 + start; if (target == nums[mid]){ //往前找 end = mid; }else if (target > nums[mid]){ //往後找 start = mid; }else { end = mid; } } if (target == nums[start]){ return start; } if (target == nums[end]){ return end; } return -1; } /** * 20190804 * 找最後一個位置 * @param nums * @param target * @return */ public static int findLast(int[] nums, int target){ int start = 0; int end = nums.length - 1; while (start +1 <end){ int mid = (end - start)/2 + start; if (target == nums[mid]){ //往後找 start = mid; }else if (target > nums[mid]){ //往後找 start = mid; }else { end = mid; } } if (target == nums[end]){ return end; } if (target == nums[start]){ return start; } return -1; }
5.Search in a Big Sorted Array(lintcode458)
給定一個很大的升序正數數組,不能求長度,但是可以通過一個函數返回第k個數的值。要求在log(k)的時間內找到指定target的值
- 不能求長度,二分法的end的初始值如何確定?
- end的值可以不等於整個數組的長度,只要大於target即可
- 那麼,只要我能找到一個index,它在數組中對應的值大於target,也就可以確定end的初始值
- 所以,關鍵就在於
如何在log(k)的時間內找到大於target的index
倍增法
:一次尋找 1 2 4 8 …- 只要確定了end的初始值,後面的則是一個普通的二分查找問題了
- 完整代碼
/** * 20190804 * 顯然,這道題目需要二分法來解決 * 但是 因爲不能求長度,而無法直接爲 end賦值 * 其實 end的值 不一定是需要整個數組長度的 * nums[end] 只要 > target即可 * 所以關鍵點 如何在log(k)的時間內 能找到一個nums[end] 則成爲了關鍵 * 這裏介紹一種“倍增”的思想 就是找1,找不到就找2 然後找4!! * @param reader: An instance of ArrayReader. * @param target: An integer * @return : An integer which is the index of the target number */ public int searchBigSortedArray(ArrayReader reader, int target){ //1利用倍增法 尋找end的最大值 //即找到第一個 大於target 的 index值即可 int index = 1; while (reader.get(index - 1) < target ){ index = index * 2; } //只要找到了index 下面就是一個普通的二分查找了 int start = 0; int end = index - 1; while (start + 1< end){ int mid = (end - start)/2 + start; if (reader.get(mid) == target){ //因爲這裏要找 First,所以繼續往前找 不能返回 end = mid; }else if (target > reader.get(mid)){ start = mid; }else { end = mid; } } if (reader.get(start) == target){ return start; } if (reader.get(end) == target){ return end; } return -1; }
6. Find Minimum in Rotated Sorted Array尋找旋轉排序數組中的最小值(leetcode153,lintcode159)
給定一旋轉的排序數組,要求找到其中的最小值。例如:Input: [3,4,5,1,2] Output: 1
- 首先一個很直白的思路,就是一個for循環嘛,遍歷整個數組,只要找到第一個
nums[i]>nums[i+1]
,則就是最小值,時間複雜度爲 - 如果想把一個的繼續做優化的話,只能到,而且幾乎只能用二分法
- 這道題還非常明顯的告知了
有序數組
,所以幾乎應該用二分來做了
- 這道題的結構,可以用這個圖很清晰的表達出來,顯然A點和B點的值都是可以獲取的
A = nums[0],B=nums[nums.length-1]
C
點的值明顯就是最小值,那如何求得C點的值呢?- 二分法中一定會涉及到一個
目標數
,這裏也一樣,只是這裏要求的是小於等於目標數的第一個位置上的數
,取等號是因爲可能原數組只旋轉一位,則原來最小的第一位跑到了最後一位 - 所以這裏選B作爲目標數,
尋找<=B的First position
即可 - 後面用二分法解即可
- 完整代碼
public static int findMin(int[] nums) {
if(nums == null || nums.length == 0){
return -1;
}
//定義 target
int target = nums[nums.length-1];
//下面的工作就變成尋找 小於target的第一個數
//當然也是用二分法尋找
int start = 0;
int end = nums.length - 1;
while (start + 1 < end){
int mid = (end - start)/2 + start;
//如果當前終點位置 的值 小於等於 target
// 則當前的mid 一定在旋轉後數組 的後一段了
// 因爲數組是旋轉過的 target一定是 後一段的最大值
// 當前的mid位置 小於 target則一定是在後半段了(前半段的值一定都大於target)
// 爲了找到小於target的第一個位置 則繼續往前找
if (nums[mid] <= target ){
end = mid;
}else {
//如果 當前中點的值 大於target 則顯然小於target的值肯定還在後面
// 這是因爲數組是旋轉過的 target一定是 後一段的最大值
// 當前還有比target大的 則說明當前的位置一定在前一段 所以繼續往後找
start = mid;
}
}
if (nums[start] <= target){
return nums[start];
}
//給定排序數組中的最小值是一定存在的
return nums[end];
}
7.
8.搜索二維矩陣(leetcode74)
給定一二維數組,其中每一行從左到右是升序,而且下一行的第一個數大於上一行的最後一個數,也就是按每一行排列,整個二維數組的值都是升序
- 本質還是二分法的應用,關鍵在於把二維的數組轉換爲一維數組,然後進行二分查找
8.1解法一:先確定目標數所在行,然後在該行進行二分查找
/**
* 20190805(自解AC)
* 關鍵在於如何把2D轉換爲1D 然後進行二分查找
* @param matrix 給定矩陣
* @param target 給定搜索的數
* @return
*/
public static boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
return false;
}
int row = matrix.length;
int col = matrix[0].length;
int target_row = 0;
for (int i=0;i<row; i++){
//按行 遍歷 找到target所在的行
if (target>=matrix[i][0] && target<=matrix[i][col-1]){
target_row = i;
}
}
//這裏已經把2D轉換爲了1D直接 利用二分查找即可
//對target_row 行 進行二分查找
int start = 0;
int end = col - 1;
while (start+1<end){
int mid = (end - start)/2 + start;
if (matrix[target_row][mid] == target ){
return true;
}else if (matrix[target_row][mid] < target){
start = mid;
}else {
end = mid;
}
}
if (target == matrix[target_row][start]){
return true;
}
if (target == matrix[target_row][end]){
return true;
}
return false;
}
8.2解法2
- 和解法1沒有本質區別
- 關鍵在於一個知識點:
在一個m*n的數組中,第k個數的位置是[k/n][k%n]
- 又因爲整個數組的數按行排列起來都升序的,因此可以直接在整個數組中進行二分查找
/**
* 20190805
* 參考:https://yisuang1186.gitbooks.io/-shuatibiji/search_a_2d_matrix.html
* 其實和我的解法 沒有本質上的差別,仍然是把2D轉換爲1D
* 但是關鍵的地方在於如和轉換 這裏的轉換方法感覺比我的優雅
* 給定 m*n的數組 第k個數 在其中的位置爲 [k/n][k%n]
* 知道了這個 就可以查找了
* @param matrix
* @param target
* @return
*/
public static boolean searchMatrix02(int[][] matrix, int target){
if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
return false;
}
int row = matrix.length;
int col = matrix[0].length;
int start = 0;
int end = row * col - 1;
while (start + 1 < end){
int mid = (start) + (end - start)/2;
if (matrix[mid/col][mid%col] == target){
return true;
}else if (matrix[mid/col][mid%col] < target){
start = mid;
}else {
end = mid;
}
}
if (matrix[start/col][start%col] == target){
return true;
}
if (matrix[end/col][end%col] == target){
return true;
}
return false;
}