BinarySearch_0_闭区间搜索和左闭右开搜索

naive 的二分搜索: 闭区间搜素 v.s. 左闭右开搜索

二分搜索通常由两种写法: [闭区间搜索] 和 [左闭右开搜索], 对于 [闭区间搜索的写法]
1.对于一个有序无重复元素的数组, 按照二分搜索的思想去模拟写, 刚开始的时候搜索的下界元素索引是0, 搜索的上界元素索引是 nums.size()-1
2.当前搜索目标索引记录为 mid, 判断 nums[mid] == target 如果搜到则返回; 如果没搜到执行变更区间操作: 上下界为当前搜索 mid + 1 或者 mid - 1
3.到最后没搜到的情况, 默认直接返回 -1
4.只适用于数组无重复元素下, 因此无法处理找左侧边界或者右侧边界的问题

// naive 二分搜索: 闭区间搜索: 搜索的是 [l, h] 区间
int binarySearch(vector<int>& nums, int target) {
  int l = 0;
  int h = nums.size() - 1;
  while (l <= h) {
    int mid = l + (h - l) / 2;
    int midNum = nums[mid];
    if (midNum == target) {
      return mid;
    } else if (midNum < target) {
      // 更新搜索区间到 [mid+1, h]
      l = mid + 1;
    } else if (midNum > target) {
      // 更新搜索区间到 [l, mid-1]
      h = mid - 1;
    }
  }
  return -1;
}

然而二分搜索还有另一种习惯的写法, 和上面的区别在于 while () 括号里面的边界更新操作为 while (l < h) , 核心思想是更改了搜索区间为左闭右开的方式, 搜索区间为 [l, h)

// naive 二分搜索: 左闭右开区间搜索: 永远搜索的是 [l, h) 区间
class Solution {
 public:
  int search(vector<int>& nums, int target) {
    int l = 0;
    int h = nums.size();
    while (l < h) {
      int mid = l + (h - l) / 2;
      int midNum = nums[mid];
      if (midNum == target) {
        return mid;
      } else if (midNum < target) {
        // 更新搜索区间到 [mid+1, h)
        l = mid + 1;
      } else if (midNum > target) {
        // 更新搜索区间到 [l, mid)
        h = mid;
      }
    }
    return -1;
  }
};

二分搜索的边界处理问题分析

二分搜索的问题在于很多边界条件的处理上是容易出错的, 处理细节问题如下:
1.while (l <= h) 用的是 while (l <= h) 还是 while (l < h) ? 有什么区别?
2.更新搜索区间是用 l = mid+1, h = mid-1 还是用 h = mid, l = mid?
3.为什么 naive 的二分搜索不能找到左侧边界或者右侧边界?
4.能否将 naive 的二分搜索和 [找左侧边界的二分搜索] 和 [找右侧边界的二分搜索] 的统一形式?

逐个分析上面的几个问题
1.while (l <= h) 用的是 while (l <= h) 还是 while (l < h) ? 有什么区别?
这里有个 [搜索区间] 的问题, 其实就是确认 [我们的搜索区间有没有完全覆盖应该被搜索的所有的区间], 在用 while (l <= h) 的设定下, 搜索区间是 [0, nums.size()-1], 在 while (l < l) 的设定下, 搜索区间是 [0, nums.size()) 我们不妨细看下里面的区别:
(i). while (l <= h) 的终止退出条件是 l == h+1, 写成区间形式就是 [h+1, h], 写个例子带入进去 [3,2], 这时候搜索区间为空的, 也不应该搜索, 这是没问题的
(ii). while (l < h) 的终止退出条件是 l == h, 写成区间形式就是 [h, h], 写个例子带入进去 [2,2] , 这时候区间是应该搜索一次的, 但是 while 循环终止推出了, 没有搜这最后一次, 因此区间 [2,2] 这一次搜索过程被遗漏掉了, 这不正确, 或者说搜索不够充分 (增加一次额外的判断就变完整的搜索了)

如果非要用 while (l < h) 这种模式搜索, 需要增加对这种情况单独处理

while (l < h) {
  // naive 的二分搜索
}
return nums[l] == target ? l : -1;

2.是用 l = mid+1, h = mid-1 还是用 h = mid, l = mid?
基于我们要搜索的 [搜索区间] 是 [l, h] 还是 [l, h), 在 mid 已经完成搜索的情况下,
(i). 闭区间搜索 [l, h] 应该去搜索左边的 [l, mid-1] 和 右边的 [mid+1, h] 这两个区间
(ii). 左闭右开区间搜索 [l, h) 应该去搜索左边的 [l, mid) 和 右边的 [mid+1, h) 这两个区间

3.基础的二分搜索算法为什么不能搜索左侧边界?
比如给出数组 nums = [1,2,2,2,3], target = 2, 用 naive 的算法返回的是索引2 (也就是第2个2), 但是如果我想得到 target 的左侧边界, 也就是索引1的这个位置, 这种问题通常出现在 [搜索有序数组中第1个出现的位置]; 或者想得到target的右侧边界, 也就是索引3, 这种问题通常出现在 [搜索有序数组中最后一个出现的位置], naive 的二分搜索算法是无法得到的

搜索左侧边界的二分搜索: 搜索第1个 >= target 的元素

搜索左侧边界的二分搜索实现原理 (基础版本), 如何做到搜索左侧边界呢? 我们想一下 naive 二分搜索做的事情, 当找到第一个 mid == target 的时候, 直接就返回了, 左边如果还有 mid == target 的值, 我们应该继续搜索而不是停下来, 如何继续搜索? 更新右侧的搜索边界为 mid
1.当找到 nums[mid] == target 时候, 右侧边界要继续缩小, 而不是直接返回索引; 缩小右侧边界的操作是 h = mid 还是 h = mid-1 呢?
(i). 如果 while 里面是 while (l <= h) 那么搜索的是 [l, h], 左右都闭的区间, 那么 h = mid-1
(ii). 如果 while 里面是 while (l < h) , 那么搜索区间是 [l, h), h = mid
2.返回的值是什么, 返回的索引是 l
3.在没有找到的情况下, 需要特殊处理返回-1的条件, 没有找到条件是什么?
(i). l >= nums.size() , 一个大于最大值的数字
(ii). 搜索目标是一个不存在在数组里面的元素, nums[l] != target

我们先看第一种的方式: 采用搜索闭区间的方式二分搜索左边界, 按照收缩右边界的思想进行边界更新

int binarySearchLowerBound(vector<int>& nums, int target) {
  int l = 0;
  int h = nums.size() - 1; 
  while (l <= h) {
    int mid = l + (h - l) / 2;
    int midNum = nums[mid];
    if (midNum == target) {
      // 持续收缩右侧边界
      h = mid - 1;
    } else if (midNum < target) {
      // 更新搜索区间 [mid+1, h]
      l = mid + 1;
    } else if (midNum > target) {
      // 更新搜索区间 [l, mid-1]
      h = mid - 1;
    }
  }
  // 没有找到的情况的特殊处理
  if (l >= nums.size() || nums[l] != target) {
    return -1;
  }
  return l;
}

再看利用搜索左闭右开的方式二分搜索左侧边界, 和搜索闭区间的方式相比, 因为搜索的是左闭右开区间 [l, h), 所以在更新有边界的时候或者在收缩有边界的时候采用的是 h = mid, 而不是 h = mid + 1

// 这里采用搜索左闭右开的方式来处理左侧边界的二分搜索
int binarySearchLowerBound(vector<int>& nums, int target) {
  int l = 0;
  int h = nums.size();  // 搜索的是左闭右开区间的方式 h = nums.size()
  while (l < h) {       // 搜索的是左闭右开区间的方式 while 里面符号用 while (l < h)
    int mid = l + (h - l) / 2;
    int midNum = nums[mid];
    if (midNum == target) {
      // 收缩右边界
      h = mid;
    } else if (midNum < target) {
      // 搜索区间是[mid+1, h)
      l = mid + 1;
    } else if (midNum > target) {
      // 搜索区间是[l, mid)
      h = mid;
    }
  }
  // 没有找到的情况的特殊处理
  if (l >= nums.size() || nums[l] != target) {
    return -1;
  }
  return l;
}

intuitively,
1.采用 while (l < h) 实质上将搜索区间转换成了 [l, h), 终止条件是 l == h, 也就是 [l, l) 为空
2.这里找到 target 的时候没有马上返回, 而是 [缩小区间的右边界] h, 在 [l, mid) 中继续搜索, 也就是说不断的向着左边收缩; 我们现在的 [搜索区间] 改成了搜索某个 [l, h) 分开二分区间也就是在搜索 [l, mid) 和 [mid+1, h)

搜索右侧边界的二分搜索: 搜索最后1个 <= target 的元素

同理, 搜索右侧边界的二分搜索, 本质上是在搜索到 target 元素的时候进行左侧的边界收缩:
1.我们先看闭区间搜索下的右侧边界二分搜索, 也就是循环写成 while (l <= h) 的这种形式, 搜索的区间始终是 [l, h], 在搜到目标 target 并收缩左侧边界时, 收缩边界的方式为 l = mid + 1
2.因为搜索的是右边界, 最终返回的结果是右边界 h
3.处理没有搜索到的情况: h 向左搜索到 < 0 或者没搜到对应的值 nums[h] != target

int binarySearchHigherBound(vector<int>& nums, int target) {
  int l = 0;
  int h = nums.size() - 1; 
  while (l <= h) {
    int mid = l + (h - l) / 2;
    int midNum = nums[mid];
    if (midNum == target) {
      // 收缩左侧边界, 向右继续搜索
      l = mid + 1;
    } else if (midNum < target) {
      // 更新搜索区间 [mid+1, h]
      l = mid + 1;
    } else if (midNum > target) {
      // 更新搜索区间 [l, mid-1]
      h = mid - 1;
    }
  }
  // 没有找到的情况的特殊处理
  if (h < 0 || nums[h] != target) {
    return -1;
  }
  return h;
}

我们再看下如何用搜索左闭右开的形式下的右侧边界二分搜索, 持续搜索的边界是 [l, h):
1.如何收缩左侧的边界? 我们的搜索区间是 [l, h), 因此收缩左侧边界的操作为 mid 直接+1, 也就是l = mid + 1
2.这时候我们二分的搜索区间 [l, h) 分别是 [mid+1, h) 和 [l, mid)
3.在判断没有找到的情况的特殊处理, 需要判断的是l-1的是否<0 或者 nums[l-1] != target, 且返回的是 l-1, 这是为什么?
4.我们在判定 nums[mid] == target 的时收缩左侧边界用的是 l = mid+1 进行左侧边界收缩, 也就是说 mid = l-1 因为我们对l的更新必须是 l = mid+1, 在 while 结束时, nums[l] 不一定等于 target 了, 而 nums[l-1] 可能等于 target
5.while终止的条件是 (l == h), 所以找到最终的结果返回 h-1, 或者判断没有找到的情况将 l-1 换成 h-1 是一样的

int binarySearchHigherBound(vector<int>& nums, int target) {
  int l = 0;
  int h = nums.size(); 
  while (l < h) {
    int mid = l + (h - l) / 2;
    int midNum = nums[mid];
    if (midNum == target) {
      // 收缩左侧边界, 向右继续搜索
      l = mid + 1;
    } else if (midNum < target) {
      // 搜索区间变为[mid+1, h)
      l = mid + 1;
    } else if (midNum > target) {
      // 搜索区间变为[l, mid)
      h = mid;
    }
  }
  // 没有找到的情况的特殊处理
  if (l-1 < 0 || nums[l-1] != target) {
    return -1;
  }
  return l - 1;
}

测试代码, 在找不到的情况先不采用返回 -1 这种设定, 打出来最后的值

#include <iostream>
#include <vector>
#include <algorithm>
#include <ctime>
#include <cstdlib>
using namespace std;

int binarySearchLowerBound(vector<int>& nums, int target) {
  int l = 0;
  int h = nums.size() - 1; 
  while (l <= h) {
    int mid = l + (h - l) / 2;
    int midNum = nums[mid];
    if (midNum == target) {
      // 持续收缩右侧边界
      h = mid - 1;
    } else if (midNum < target) {
      // 更新搜索区间 [mid+1, h]
      l = mid + 1;
    } else if (midNum > target) {
      // 更新搜索区间 [l, mid-1]
      h = mid - 1;
    }
  }
  return l;
}

int binarySearchHigherBound(vector<int>& nums, int target) {
  int l = 0;
  int h = nums.size() - 1; 
  while (l <= h) {
    int mid = l + (h - l) / 2;
    int midNum = nums[mid];
    if (midNum == target) {
      // 收缩左侧边界, 向右继续搜索
      l = mid + 1;
    } else if (midNum < target) {
      // 搜索区间变为[mid+1, h]
      l = mid + 1;
    } else if (midNum > target) {
      // 搜索区间变为[l, mid-1]
      h = mid - 1;
    }
  }
  return h;
}

int main() {
  vector<int> c{0,1,2,3,4,5,5,5,6,7,8,9,10};
    std::cout << "target=8, lower_pos=" << binarySearchLowerBound(c, 8) << std::endl;
    std::cout << "target=5, lower_pos=" << binarySearchLowerBound(c, 5) << std::endl;
    std::cout << "target=11, lower_pos=" << binarySearchLowerBound(c, 11) << std::endl;
    std::cout << "target=0, lower_pos=" << binarySearchLowerBound(c, 0) << std::endl;

    std::cout << "target=8, higher_pos=" << binarySearchHigherBound(c, 8) << std::endl;
    std::cout << "target=5, higher_pos=" << binarySearchHigherBound(c, 5) << std::endl;
    std::cout << "target=11, higher_pos=" << binarySearchHigherBound(c, 11) << std::endl;
    std::cout << "target=0, higher_pos=" << binarySearchHigherBound(c, 0) << std::endl;

  std::cout << std::endl;
  c = vector<int>{0,1,2,3,4,6,7,8,9,10};
    std::cout << "target=5, lower_pos=" << binarySearchLowerBound(c, 5) << std::endl;
    std::cout << "target=11, lower_pos=" << binarySearchLowerBound(c, 11) << std::endl;

    std::cout << "target=5, higher_pos=" << binarySearchHigherBound(c, 5) << std::endl;
    std::cout << "target=11, higher_pos=" << binarySearchHigherBound(c, 11) << std::endl;
  return 0;
}

输出结果

target=8, lower_pos=10
target=5, lower_pos=5
target=11, lower_pos=13
target=0, lower_pos=0
target=8, higher_pos=10
target=5, higher_pos=7
target=11, higher_pos=12
target=0, higher_pos=0

target=5, lower_pos=5
target=11, lower_pos=10
target=5, higher_pos=4
target=11, higher_pos=9

二分搜索的在 CPP 中的 api

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
  vector<int> nums = {4,10,11,30,30,30,69,70,96,100};
  int t0 = 70;
  int t1 = 101;
  // binary_search 返回的是有没有找到, 找到了返回1, 否则返回0
  int b0 = binary_search(nums.begin(), nums.end(), t0);
  int b1 = binary_search(nums.begin(), nums.end(), t1);
  cout << "b0:" << b0 << endl;
  cout << "b1:" << b1 << endl;

  int t2 = 30;
  int t3 = 100;
  // lower_bound 二分搜索的是左边边界, 否则返回 nums.end()
  // lower_bound 二分搜索的是左边边界, 否则返回 nums.end()
  auto l = lower_bound(nums.begin(), nums.end(), t2);
  // upper_bound 二分搜索的第一个严格大于 target 的元素, 否则返回 nums.end()
  auto u = upper_bound(nums.begin(), nums.end(), t2);

  if (l != nums.end() && *l == t2)  {
    cout << "find t2 lower_bound, t2.index = " << l - nums.begin() << endl;
  } else {
    cout << "no find" << endl;
  }
  if (u != nums.end())  {
    cout << "find t2 upper_bound, first greater than t2 = " << *u << endl;
  } else {
    cout << "no find" << endl;
  }

  auto u3 = upper_bound(nums.begin(), nums.end(), t3);
  if (u3 != nums.end())  {
    cout << "find t3 upper_bound, first greater than t3 = " << *u3 << endl;
  } else {
    cout << "no element greater than t3" << endl;
  }
  return 0;
}

标准数组中的二分搜索

34.在排序数组中搜索元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]

示例 2:输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]

示例 3:输入:nums = [], target = 0 输出:[-1,-1]

分析:
1.题意为搜索左侧边界和右侧边界的二分搜索

class Solution {
 public:
  int binarySearchLowerBound(vector<int>& nums, int target) {
    int l = 0;
    int h = nums.size() - 1; 
    while (l <= h) {
      int mid = l + (h - l) / 2;
      int midNum = nums[mid];
      if (midNum == target) {
        // 收缩右侧边界, 向左继续搜索
        h = mid - 1;
      } else if (midNum < target) {
        // 搜索区间变为[mid+1, h]
        l = mid + 1;
      } else if (midNum > target) {
        // 搜索区间变为[l, mid-1]
        h = mid - 1;
      }
    }
    // 没有找到的情况的特殊处理
    if (l >= nums.size() || nums[l] != target) {
      return -1;
    }
    return l;
  }
  int binarySearchHigherBound(vector<int>& nums, int target) {
    int l = 0;
    int h = nums.size() - 1; 
    while (l <= h) {
      int mid = l + (h - l) / 2;
      int midNum = nums[mid];
      if (midNum == target) {
        // 收缩左侧边界, 向右继续搜索
        l = mid + 1;
      } else if (midNum < target) {
        // 搜索区间变为[mid+1, h]
        l = mid + 1;
      } else if (midNum > target) {
        // 搜索区间变为[l, mid-1]
        h = mid - 1;
      }
    }
    // 没有找到的情况的特殊处理
    if (h < 0 || nums[h] != target) {
      return -1;
    }
    return h;
  }
  vector<int> searchRange(vector<int>& nums, int target) {
    vector<int> ans = {binarySearchLowerBound(nums, target), binarySearchHigherBound(nums, target)};
    return ans;
  }
};

704.二分搜索

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1: 输入: nums = [-1,0,3,5,9,12], target = 9 输出: 4 解释: 9 出现在 nums 中并且下标为 4

示例 2: 输入: nums = [-1,0,3,5,9,12], target = 2 输出: -1 解释: 2 不存在 nums 中因此返回 -1

分析:
1.基础的二分搜索实现, 可以用闭区间搜索和左闭右开搜索两种方法

class Solution {
 public:
  int search(vector<int>& nums, int target) {
    int l = 0;
    int h = nums.size() - 1;
    while (l <= h) {
      int mid = l + (h - l) / 2;
      int midNum = nums[mid];
      if (midNum == target) {
        return mid;
      } else if (midNum < target) {
        l = mid + 1;
      } else if (midNum > target) {
        h = mid - 1;
      }
    }
    return -1;
  }
};

35.搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。

示例 1: 输入: nums = [1,3,5,6], target = 5 输出: 2

示例 2: 输入: nums = [1,3,5,6], target = 2 输出: 1

示例 3: 输入: nums = [1,3,5,6], target = 7 输出: 4

分析:
1.插入位置相当于找第 1 个 >= target 的位置

class Solution {
 public:
  int searchInsert(vector<int>& nums, int target) {
    int l = 0;
    int h = nums.size() - 1;
    // target <= nums[pos] || nums[pos-1] < target
    // 两种情况找到的都是左侧边界
    while (l <= h) {
      int mid = l + (h - l) / 2;
      if (nums[mid] == target) {
        return mid;
      } else if (nums[mid] < target) {
        l = mid + 1;
      } else if (nums[mid] > target) {
        h = mid - 1;
      }
    }
    return l;
  }
};

转载请注明来源, from goldandrabbit.github.io

💰

×

Help us with donation