二分的本质是找到一个临界点,临界点满足一个条件,临界点右边不满足这个条件。
二分搜索一般有枚举下标和枚举值两种,mid表示下标或者某一个值。
这样的二分搜所通常涉及两个函数定义,一个叫upper_bound, 一个叫lower_bound,这里会产生边界问题。
从笔试/面试的角度来说, 笔试中,常常涉及到二分搜索的题目,这样的题目,由于时间充足,允许不断调试,有部分分,另外需要注意,笔试中往往不会给出出错数据,需要自己想办法调试,所以这类题目往往是必须要拿下的。
在国内面试中,往往不会涉及过于难的二分题目,因为除非背下模板,否则需要手动调试处理边界问题。这在有限的面试时间中是不现实的,特别是手写代码。所以一般只要实现比较简单的二分搜索即可。
Leetcode 69 sqrt(x)
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
这道题目一般使用二分搜索或者牛顿迭代,因为牛顿迭代涉及到公式的推导,面试中一般不会出现,所以一般就是用二分的方法做。
根据这里的思路,就是找临界点,临界点右边的数n*n>x, 临界点左边<=x,整数二分通常会涉及到临界点的处理问题,这里一种方法是背标准的模板,另一种方法是用一个标准的左闭右闭模板,然后自己调试。这里注意处理溢出问题。
class Solution {
public int mySqrt(int x) {
int left = 1, right = x;
while(left<=right){
int mid = left + (right-left)/2;
if(mid==x/mid) return mid;
if(mid<x/mid) left = mid+1;
else right = mid-1;
}
return right;
}
}
Leetcode 74 搜索二维矩阵
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。
示例 1:
输入:
matrix = [
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 50]
]
target = 3
输出: true
这一题目就是最简单的二分搜索问题
func searchMatrix(matrix [][]int, target int) bool {
if len(matrix) == 0{
return false
}
left, right := 0, len(matrix)*len(matrix[0])-1
for left<=right{
mid := left + (right-left)/2
row := mid / len(matrix[0])
col := mid % len(matrix[0])
val := matrix[row][col]
if target == val{
return true
}
if val > target {
right = mid-1
}else{
left = mid+1
}
}
return false
}
Leetcode 278. 第一个错误的版本
是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例:
给定 n = 5,并且 version = 4 是第一个错误的版本。
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
这道题目是比较简单的,找一个临界点,左边是好的,右边是坏的。
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int left=1, right = n;
while(left<=right){
int mid = left + (right-left)/2;
if(isBadVersion(mid))
right = mid-1;
else
left = mid+1;
}
return left;
}
}
Leetcode 153. 寻找旋转排序数组中的最小值
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
示例 1:
输入: [3,4,5,1,2]
输出: 1
示例 2:
输入: [4,5,6,7,0,1,2]
输出: 0
找到一个临界点,临界点右边的数小于第一个数,临界点左边的数大于第一个数
func findMin(nums []int) int {
left, right := 0, len(nums)-1
if nums[left]<=nums[right]{
return nums[left]
}
for left<=right{
mid := left + (right-left)/2
if(nums[mid]<nums[0]){
right = mid-1
}else{
left = mid+1
}
}
return nums[left]
}
Leetcode 33. 搜索旋转排序数组
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2:
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
这道题目直接做比较容易出错,可以分成两步做,首先用第153的做法,找到分解点,然后再做一次二分查找。
func search(nums []int, target int) int {
end := len(nums)-1
if end==-1{
return -1
}
var left int
var right int
if target==nums[end]{
return end
}else if target<nums[end]{
left, right = findMin(nums), end
}else{
left, right = 0, findMin(nums)
}
for left<=right{
mid := left + (right-left)/2
if nums[mid]== target{
return mid
}
if nums[mid] > target{
right = mid - 1
}else{
left = mid + 1
}
}
return -1
}
func findMin(nums []int) int {
left, right := 0, len(nums)-1
if nums[left]<=nums[right]{
return left
}
for left<=right{
mid := left + (right-left)/2
if(nums[mid]<nums[0]){
right = mid-1
}else{
left = mid+1
}
}
return left
}
Leetcode 35 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
这个函数就是实现upper_bound 的函数,这里涉及到边界问题的解决。如果找到数就返回数,否则就返回后面的数。
func searchInsert(nums []int, target int) int {
nLen := len(nums)
left, right := 0, nLen-1
for right>left{
mid := left + (right-left)/2;
if target < nums[mid]{
right = mid-1;
}else if target>nums[mid]{
left = mid+1;
}else{
return mid;
}
}
if target<=nums[left]{
return left;
}
return left+1;
}
Leetcode 34. 在排序数组中查找元素的第一个和最后一个位置
这道题目就是upper_bound 和lower_bound的函数,涉及到很多边界处理问题
func searchRange(nums []int, target int) []int {
if len(nums) == 0 || nums[0] > target || nums[len(nums)-1] < target {
return []int{-1, -1}
}
nLen := len(nums)
left, right := 0, nLen-1
var start, end int; // 答案
for right>=left{
mid := left + (right-left)/2;
if target > nums[mid]{
left = mid+1
}else{
right = mid-1
}
}
if nums[left]!=target{
return []int{-1,-1};
}
start = left;
left, right = 0, nLen-1
for right>=left{
mid := left + (right-left)/2
if(target<nums[mid]){
right = mid-1
}else{
left = mid+1
}
}
end = right
return []int{start, end}
}
Leetcode 287 寻找重复的数
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:
输入: [1,3,4,2,2]
输出: 2
示例 2:
输入: [3,1,3,4,2]
输出: 3
说明:
不能更改原数组(假设数组是只读的)。
只能使用额外的 O(1) 的空间。
时间复杂度小于 O(n2) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。
这个题目有一点难度,最直接的O(1)空间复杂度的方法是枚举1到n的数,扫一遍看看,是否出现两次,时间复杂度为O(n^2)
这里优化的方法是每次看一个区间[left,mid] [ mid+1,right], 遍历一般数组,如果在[left,mid]中的数大个数大于mid-left+1, 那么说明重复的数在[left,mid]区间内,否则在[mid+1,right]区间内,这道题目的时间复杂是O(NlogN)
func findDuplicate(nums []int) int {
left, right := 1, len(nums)-1
for left<=right{
mid := left + (right-left)/2
if(check(nums,left,mid)){
right = mid-1
}else{
left = mid+1
}
}
return left
}
func check(nums []int, left int, mid int) bool{
count := 0
for i:=0;i<len(nums);i++{
if nums[i]>=left && nums[i]<=mid{
count++
}
}
return count>mid-left+1
}
Leetcode 275. H指数 II
给定一位研究者论文被引用次数的数组(被引用次数是非负整数),数组已经按照升序排列。编写一个方法,计算出研究者的 h 指数。
这道题是有一定难度的,首先如果直接枚举h指数,那么就要遍历一遍数组,时间复杂度至少是O(nlogn),事实上,这到题目有O(logn) 的做法,因为h的含义和论文篇数/下标有关,所以枚举的是下标,这里更新的条件有些难想。同时,不难按照通常那样返回left或right,而要用一个变量记录满足要求的res
func hIndex(citations []int) int {
n := len(citations)
var res int
if n==0{
return 0
}
left, right :=0, n-1
for left<=right{
mid := left + (right-left)/2
if(citations[mid]>=n-mid){
res = n-mid
right = mid-1
}else{
left = mid+1
}
}
return res
}
Leetcode 162. 寻找峰值
峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。
数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞。
示例 1:
输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5
解释: 你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
说明:
你的解法应该是 O(logN) 时间复杂度的。
这道题目是比较难得, 优化结论是,如果nums[mid]<nums[mid-1], 那么左边一定有峰值,可以用反证法证明
nums[mid]<nums[mid+1], 那么右边一定有峰值
如果 nums[mid]>nums[mid-1] && nums[mid]>nums[mid+1] 那么mid是峰值。
由于这里涉及到mid-1和mid+1,会出现大量得边界问题需要解决,注意看程序得代码
所以处理方法是下面这样
func findPeakElement(nums []int) int {
left, right :=0, len(nums)
n := len(nums)
if n==1{
return 0
}
for left<=right{
mid := left + (right-left)/2
if(mid-1>=0){
if (nums[mid-1]>nums[mid]){
right = mid-1
}else if mid ==n-1|| nums[mid]>nums[mid+1]{
return mid
}else{
left = mid+1
}
}else if mid+1<n{
if(nums[mid]<nums[mid+1]){
left = mid+1
}else if mid == 0 || nums[mid]>nums[mid-1]{
return mid
}else{
right = mid-1
}
}
}
return left
}