从这篇文章开始,我将开启算法专栏,记录在刀砍leetcode算法过程中的理论总结与实战代码,我认为二分算法是算法问题中细节最多的部分,所以我先拿它开刀!二分题目实战请看我的二分查找专栏:二分查找实战专栏

1.Overview-三种问题类型、两种算法形式

⼆分搜索的原型就是在「有序数组」中搜索⼀个元素 target,返回该元素对应的索引。 如果该元素不存在,那可以返回⼀个什么特殊值,这种细节问题只要微调算法实现就可实现。

还有⼀个重要的问题,如果「有序数组」中存在多个 target 元素,那么这些元素肯定挨在⼀起,这⾥就涉 及到算法应该返回最左侧的那个 target 元素的索引还是最右侧的那个 target 元素的索引,也就是所谓的 「搜索左侧边界」「搜索右侧边界」,这个也可以通过微调算法的代码来实现。但是一般算法题没有这么无脑,但是都可以转化为「搜索左侧边界」和「搜索右侧边界」问题。

另外,众所周知,我们二分查找实在一个区间范围内进行的,所以这就引申出了两种区间表示法这也对应了两种不同形式的二分查找代码框架(本质上是一样的,是可以相互转化的),且这两种形式的算法框架都可应用于我们上述的三种问题:搜索⼀个元素、「搜索左侧边界」、「搜索右侧边界」

这两种形式分别是左闭右闭区间的搜索、左闭右开区间的搜索

下面我们针对每种问题类型分别进行两种形式的算法框架详述

2.搜索一个元素target

2.1左闭右闭区间搜索 - [ ]

int left_bound(int[] nums, int target) {if (nums.length == 0) return -1;int left = 0, right = nums.length - 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 - 1;}}return -1 ;
}

1.因为我们设定好是左闭右闭区间形式,所以一开始区间左端点应该是对应于待查找序列第一个元素,区间右端点应该是对应于待查找序列队最后一个元素故有:

int left = 0, right = nums.length - 1;

2.为什么是left <= right 不是left < right ,因为若是left < right的话,算法进行到left=right时,我们不会进入while循环了,即没有对left和right同时指向的元素进行判断,就直接pass返回-1了,那万一这个元素就是我们的target怎么办。所以问题本质就是我们while括号里的东西要使得搜索范围包含了区间里的所有元素,不能遗漏,所以我们左闭右闭区间搜索对应的写法就是:

left <= right

那我们稍加思考不难发现,在算法没有找到target而结束while循环时left和right分别指向相邻的元素,其中left指向相邻后一位置,right指向相邻前一位置,他们发生交错。但凡能找到target,left和right是不会交错的,最极端也是相等的情况。

3.二分查找是折半查找,所以每次在区间内先判断的是中间元素是否是target,固有:

int mid = left + (right - left) / 2;

为什么不写mid = (left + right)/ 2 的形式,当然可以,但是这样会在left和right都非常大的情况下发生内存泄漏,而我们的写法就会避免这个问题,这点大家应该都知道,不多说

4.如果区间中间元素等于target,直接返回,不等于,我们将区间缩小一半继续查找:

if (nums[mid] == target) { return mid; }

5.如果target大于区间中间元素,则我们在中间元素右边区间寻找,所以这时候新区间左端点等于mid+1:

else if (nums[mid] < target) { left = mid + 1; }

6.同理,如果target小于区间中间元素,则我们在中间元素左边区间寻找,所以这时候新区间右端点等于mid-1:

else if (nums[mid] > target) { right = mid - 1; }

这里直接写成else right = mid - 1;也是可以的

7.最后如果跳出while,就说明没找到,返回 -1

2.2左闭右开区间搜索 - [ )

int left_bound(int[] nums, int target) {if (nums.length == 0) return -1;int left = 0, right = nums.length;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 ;
}

1.因为我们设定好是左闭右开区间形式,所以一开始区间左端点应该是对应于待查找序列第一个元素,区间右端点应该是对应于待查找序列队最后一个元素的后一个位置故有:

int left = 0, right = nums.length ;

2.为什么是left < right 不是left <= right ,因为若是left <= right,算法进行到left=right时( 对应区间假设为[ k , k) ),还会再进入一次while,重复了,为什么重复了呢?我们给right赋值为k的那一刻起,就说明k位置对应元素不是target(因为右开!!!)所以我们while括号里的写法是left < right,并不会造成遗漏

left < right

3.因为左闭右闭区间和左闭右开区间的左闭合的,我们对区间左端点的操作都是相同的 

if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        }

4. 为什么右端点处理不同?为什么不是right = mid - 1 ,因为我们发现target小于区间mid位置元素所以这时候新区间应该是[ left,mid ),所以我们right赋值为mid:

else if (nums[mid] > target) { right = mid ; }

3.搜索左侧边界

3.1左闭右闭区间搜索 - [ ]

int left_bound(int[] nums, int target) {if (nums.length == 0) return -1;int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {right = mid - 1;} else if (nums[mid] > target) {right = mid - 1;} else if (nums[mid] < target) {left = mid + 1;}}if (left == nums.size()) return -1;//left换成right+1也可以return nums[left] == target ? left : -1;
}

1.左闭右闭同前面一样一开始left对应为待查找区间的第一个元素,right对应为待查找区间的最后一个元素:

int left = 0, right = nums.length - 1;

2.为什么时 left <= right ,不多做解释,和前面一样

left <= right

3.当我们判断区间中间元素是否等于target时,若等于,不再是直接返回其位置,因为我们要找到我们序列中第一个等于target的元素位置(即左边界的含义),故我们要在right=mid-1,left不变的新区间内继续寻找:

if (nums[mid] == target) { right = mid - 1;} 

4.如果target小于区间中间元素,那我们target肯定在mid左边的区间里,target的左边界肯定也在这个区间里,故我们将right赋值为mid-1

else if (nums[mid] > target) {right = mid - 1;}

5.如果target大于区间中间元素,那我们target肯定在mid右边的区间里,target的左边界肯定也在这个区间里,故我们将left赋值为mid+1:

else if (nums[mid] < target) { left = mid + 1;}

6.上面都很好理解,不好理解的是while结束时我们找没找到左边界呢?以及找到的话它在哪?

首先,我们要明确一定,在我们左闭右闭区间搜索中,我们结束while循环时,left和right是交错的,right指向相邻前一位置,left指向相邻后一位置。

其次,若我们在序列中存在target(潜台词:存在target就一定有左边界),那么算法结束时,left指向的就是这个左边界,没有为什么,算法决定的,你可以手动验证,那么若有target,则最后left的取值范围一定为 [ 0 , nums.length -1 ],right始终指向left的前一位(注意left=0的时候,right就等于-1哦~),这是找到的情况

那如果不存在target,left,right是什么情况呢?没找到无非有三种情况,每种情况下left都指向大于target的第一个元素,没有为什么,算法决定的,同样你可以手动验证,三种情况如下:

· target小于序列所有元素,算法执行过程中会一直减小right,left保持不变,那么最后终止时left=0,right=-1(0对应于大于target的第一个元素的位置)

· target大于序列所有元素,算法执行过程中会一直增大left,right保持不变,那么最后终止时right=nums.length-1,left=nums.length(nums.length是大于target的第一个元素的位置 )

· target介于序列所有元素之间,算法执行过程中left,right都会变化,终止时left指向第一个大于target的元素right指向他前一个元素

所以我们最后判断有没有找到就是以left的位置为和对应位置元素值来判断,具体如下:

当算法结束时left=nums.length对应于target大于序列所有元素,没找到,返回-1,

if (left == nums.size()) return -1;

left不等于nums.length时,没法仅根据其位置判断是否有target,则需要用到 nums[left] == target 判断:

return nums[left] == target ? left : -1

3.2左闭右开区间搜索 - [ )

int left_bound(int[] nums, int target) {if (nums.length == 0) return -1;int left = 0, right = nums.length;while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {right = mid;} else if (nums[mid] > target) {right = mid;} else if (nums[mid] < target) {left = mid + 1;}}if(left==nums.length) return -1; //left换成right也可以return nums[left]==target ? left : -1;
}

1.为什么right=nums.length?前面应该讲懂了

2.为什么while括号里是left<right?前面应该讲懂了

3.当区间中间元素等于target时,这时候有两种情况:

· mid左边还有别的target,我们令right=mid,新区间为[ left,mid ),我们在新区间里继续寻找target左边界,这是没问题的,算法继续执行一定会进入下面一种情况

· 若mid本身就是最左边的target了,我们令right=mid,这时候新区间[ left,mid)不含我们的target,算法还在继续但找了寂寞,那为什么还要这么写?我们稍加思索会发现,算法继续执行,最后left会等于right,跳出while循环,最后left,right都指向我们的左边界

所以当中间元素等于target时:

if (nums[mid] == target) { right = mid;}

4.当target小于区间中间元素时,target在mid左边区间,所以:

else if (nums[mid] > target) { right = mid;}

5.当target大于区间中间元素时,target在mid右边区间,所以:

else if (nums[mid] < target) { left = mid + 1;}

7.不好理解的地方是,while结束时,找没找到左边界?找到的话它在哪?

首先,我们明确一点,我们while循环结束是left和right是相等的,

其次,若我们序列中存在target,我们算法结束时left和right一定是指向左边界的,算法决定的,没有为什么,那么最后我们left和right指向的范围为[ 0 , nums.length-1 ]

那如果不存在target,即没找到target左边界,无非对应三种情况,每种情况下left和right都同时指向大于target的第一个元素 ,三种情况如下:

· target小于序列所有元素,算法执行过程中会一直减小right,left保持不变,那么最后终止时left=right=0(0对应于大于target的第一个元素的位置)

· target大于序列所有元素,算法执行过程中会一直增大left,right保持不变,那么最后终止时left=right=nums.length(nums.length是大于target的第一个元素的位置 )

· target介于序列所有元素之间,算法执行过程中left,right都会变化,终止时left和right指向第一个大于target的元素

所以和上面一样方式返回:

if(left==nums.length) return -1;
return nums[left]==target ? left : -1;

4.搜索右侧边界

4.1左闭右闭区间搜索 - [ ]

int right_bound(int[] nums, int target) {if (nums.length == 0) return -1;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) {left = mid + 1;} else if (nums[mid] > target) {right = mid -1;}}if(left - 1 < 0) return -1; // left-1换成right也可以return nums[left-1] == target ? (left-1) : -1;
}

直接拎出不同点分析:left-1

如果序列中存在target,则算法结束时,right指向该target的右边界(和搜索左边界中左闭右闭区间搜法不一样),left指向相邻下一个元素

如果序列中不存在target,则算法结束时,也是对应的三种情况,每种情况下left指向大于target的第一个元素(和搜索左边界中左闭右闭区间搜法一样),right指向相邻上一个元素

4.2左闭右开区间搜索 - [ )

int right_bound(int[] nums, int target) {if (nums.length == 0) return -1;int left = 0, right = nums.length;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;}}if(left - 1 == nums.length) return -1; // left-1换成right-1也可以return nums[left-1] == target ? (left-1) : -1;
}

如果序列中存在target,则算法结束时,left和right指向该target的右边界的下一位(和搜索左边界中左闭右开区间搜法不一样),left指向相邻下一个元素

如果序列中不存在target,则算法结束时,也是对应的三种情况,每种情况下left和right都指向大于target的第一个元素(和搜索左边界中左闭右开区间搜法一样),right指向相邻上一个元素

二分查找理论(三种问题类型、两种算法形式)相关推荐

  1. 【数据结构与算法】一篇文章彻底搞懂二分查找(思路图解+代码优化)两种实现方式,递归与非递归

    1.二分查找是什么? 二分查找也称折半查找,是一种效率较高的查找方式.但是,二分查找要求线性表为顺序存储结构且表按关键字有序排列. 时间复杂度:O(log2n) 2.二分查找的思路分析 便于叙述,笔者 ...

  2. 二分查找 —— 从三分支到二分支

    二分查找,三分支向二分支的转变,降低的是时间复杂度的常系数. 1. 三分支版本 template <typename T> Rank binSearch(T *A, T const& ...

  3. 一元三次多项式因式分解的两种方法

    参考文献: 张育波. 一元三次多项式因式分解的两种方法[J]. 初中数学教与学, 2007, No.160(04):42.

  4. 常用十大算法 非递归二分查找、分治法、动态规划、贪心算法、回溯算法(骑士周游为例)、KMP、最小生成树算法:Prim、Kruskal、最短路径算法:Dijkstra、Floyd。

    十大算法 学完数据结构该学什么?当然是来巩固算法,下面介绍了十中比较常用的算法,希望能帮到大家. 包括:非递归二分查找.分治法.动态规划.贪心算法.回溯算法(骑士周游为例).KMP.最小生成树算法:P ...

  5. 水到底是一种液体还是两种液体

    来源:科技日报 水很寻常,它在生活中司空见惯,我们洗衣.做饭.饮用都离不开它.它可以变成水蒸气,也可以结成冰......我们似乎对水最了解不过了,但是,这看似普通的水却仍然有很多待解的谜题,科学家甚至 ...

  6. ML之kNN(两种):基于两种kNN(平均回归、加权回归)对Boston(波士顿房价)数据集(506,13+1)进行价格回归预测并对比各自性能

    ML之kNN(两种):基于两种kNN(平均回归.加权回归)对Boston(波士顿房价)数据集(506,13+1)进行价格回归预测并对比各自性能 目录 输出结果 设计思路 核心代码 输出结果 Bosto ...

  7. [转] 两种老公,两种人生。。(女人该看,男生更该看)

    A:她:"老公.帮我接杯水呗." 他:"石头剪子布.谁输了谁去." 她:"算了.我自己去吧." B:他们坐在一起看韩剧.她起身.他问&quo ...

  8. unity Canvas,Rect Transform,自动烘焙和手动烘焙,四种光源和两种发光系统

    UGUI Canvas 画布,是用来把UI元素组合在一起的组件,所有UI元素必须是Canvas的子节点:场景中可以有多个Canvas,若没有则会在创建UI元素的时候自动创建Canvas: Sortin ...

  9. Python二分查找的三种思路

    二分查找的条件: 1.列表是有序的 2.掐头去尾去中间 第一种(最普通的方式): lst = [1, 4, 5, 7, 12, 15, 16, 23, 35, 56] n = 5 left = 0 r ...

最新文章

  1. 删除机器人 异星工厂_10个视频,它们是国内智能工厂的标杆
  2. python中5个json库的速度对比
  3. 一个SAP开发人员的2018年终总结
  4. React中后台管理系统添加广告分类显示不出来
  5. 微课|中学生可以这样学Python(1.3节):Python代码编写规范
  6. 03系统服务器安装iis,服务器Win2003系统IIS 安装方法图文教程
  7. 计量经济学第六版伍德里奇计算机答案,求伍德里奇计量经济学答案第六版
  8. Unity3D AssetStore下载文件/项目保存位置
  9. itextPDF生成表格的pdf
  10. CMSIS 记录与下载
  11. 广东省计算机一级技巧,广东省计算机一级
  12. 汽车暖风系统操作步骤
  13. 终于有人把元数据讲明白了
  14. python实现xmind_Python 使用Python操作xmind文件
  15. Word2Vec词向量模型代码
  16. Ubuntu磁盘分区和内存查看
  17. 【电力电子技术】 THE FLYBACK 电路
  18. sgu290:Defend the Milky Way(三维凸包)
  19. qq音信点亮最全说明
  20. 申请高新技术企业有什么好处?

热门文章

  1. 【历史上的今天】1 月 25 日:电子游戏起源;《吃豆人》作者出生;“蠕虫王”问世
  2. 电脑无法识别U盘的解决方式集锦_艾孜尔江撰稿
  3. MySQL学习记录(导入Excel表到数据库,并筛选条件输出)
  4. Natural Sea Beauty以色列护肤品NSB外星人面膜,为肌肤赋予能量
  5. 每天过得很焦虑怎么办?尤其是职场焦虑。
  6. 图片收集/收集图片/图片征集/征集图片的工具/小程序
  7. 数据产品新人的三大有毒问题,你犯了吗?
  8. Scratch3.0----函数(2)
  9. 盖世神器PowerPro使用视频教程-1. 程序的安装概述
  10. 深度学习平台的搭建(anaconda-pytorch-pycharm)