二分搜索 Binary Search

时间复杂度(后续更新)

  • 推导公式:T(n)=T(n/2)+O(1)
  • 根据时间复杂度倒退算法是面试中常用的策略
  • 带名字的某算法在面试中大概率不会考察,因为这类算法一般都是针对解决某一种问题,适用性低
复杂度可能对应的算法备注
O(1)位运算常数级复杂度,一般面试中不会有
O(logn)二分法,倍增法,快速幂算法,辗转相除法
O(n)枚举法,双指针算法,单调栈算法,KMP算法,Rabin Karp,Manacher’s Algorithm又称作线性时间复杂度
O(nlogn)快速排序,归并排序,堆排序
O(n^2)枚举法,动态规划,Dijkstra
O(n^3)枚举法,动态规划,Floyd
O(2^n)与组合有关的搜索问题
O(n!)与排列有关的搜索问题

二分法

  • 面试时能不递归就不要递归,因为递归是一个不好的coding pattern。可以和面试官讨论用不用recursion。

  • 二分法的时间复杂度为Ο(log n)

模板1

while的目的是缩小一半的区间

二分法基础概念–LeetCode704

class Solution{
	//left ≤ target ≤ right (left和right相邻)
    public int binarySearch(int[] nums, int target) {
        if (nums == null || nums.length == 0){
            return -1;
        }
        int left = 0, right = nums.length - 1;
        // 要点1: left + 1 < right
        while (left + 1 < right) {
            // 要点2:left + (right - left) / 2
            int mid = left + (right - left) / 2;
            //要点3:=, <, > 三种情况分开讨论,mid 不+1也不-1
            if (nums[mid] == target) {
                right = mid;//要点4
            } else if (nums[mid] < target) {
                left = mid;
            } else if (nums[mid] > target) {
                right = mid;
            }
        }

        //要点5:出循环的时候,判断谁是题目所要的答案
        if (nums[left] == target) {
            return left;
        }
        if (nums[right] == target) {
            return right;
        }
        return -1;
    }
}

常见问题

Q: 要点1处为什么要用 left + 1 < right?而不是 left < right 或者 left <= right?

A: 为了避免死循环。二分法的模板中,整个程序架构分为两个部分:

  1. 通过 while 循环,将区间范围从 n 缩小到 2 (只有 left 和 right 两个点)。
  2. 在 left 和 right 中判断是否有解。

left < right 或者 left <= right 在寻找目标最后一次出现的位置的时候,会出现死循环。

Q: 要点2处为什么要用 left + (right - left) / 2?而不是 (left + right) / 2?

A: 为了避免可能出现加法溢出,也就是说加法的结果大于整型能够表示的范围,因为这里的/号的结果多向左取数。但是left 和 right都为正数,因此 right - left 不会出现加法溢出问题。关于为什么left + (right - left) / 2 不会Overflow,可以参考:why-leftright-left-2-will-not-overflow

Q: 要点3处为什么明明可以 left = mid + 1 偏偏要写成 left = mid?

A: 大部分时候,mid 是可以 +1 和 -1 的。在一些特殊情况下,比如寻找目标的最后一次出现的位置时,当 target 与 nums[mid] 相等的时候,是不能够使用 mid + 1 或者 mid - 1 的。因为会导致漏掉解。那么为了节省脑力,统一写成 left = mid 或 right = mid 并不会造成任何解的丢失,并且也不会损失效率——log(n) 和 log(n+1) 没有区别。

Q: 要点4处为什么不直接return mid?

A: 分成三种情况。

  • 当寻找左侧边界的二分搜索,此处需要设置为right = mid;。找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left+1, right) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
  • 当查找目标值在排序数组中的末尾位置时候(寻找右侧边界的二分搜索),此处需要设置为left = mid;
  • 当只是单纯的查找一个在排序数组中的目标值时,则可以设置为return mid。

Q: 要点5处为什么要设置判断?

A:该模板中,当left和right相邻时候,就不会走while循环,此时target就是left和right两个值的其中一个,故此处做判断。

变形

  • Binary Search on Index - OO==X==X

先用ArrayList倍增找到≥target的一个区间范围,再去用二分法找到题目所要求的target。

  • Binary Search on Index - Half half:上上下下,多个谷峰

分成四种情况讨论mid所在的位置,即波谷,波峰,上升趋势的某点,下降趋势的某点

参考:算法班笔记 第二章 二分与 LogN 算法

模板2

  • 寻找一个数
  • 寻找左侧边界的一个数
  • 寻找右侧边界的一个数
//迭代写法 <= 
//right < target < left
int binary_search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1; //[left,right]
    while(left <= right) {	//只有right + 1 = left时才跳出此循环
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1; 
        } else if(nums[mid] == target) {
            // 直接返回
            return mid;
        }
    }
    // 直接返回
    return -1;
}

int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定左侧边界
            right = mid - 1;
        }
    }
    // 最后要检查 left 越界的情况
    if (left >= nums.length || nums[left] != target)
        return -1;
    return left;
}


int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定右侧边界
            left = mid + 1;
        }
    }
    // 最后要检查 right 越界的情况
    if (right < 0 || nums[right] != target)
        return -1;
    return right;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值