公司理念网站,10G网站空间,地方门户网站如何推广,代理商加盟项目网站二分查找 文章目录 二分查找基本的查找第一种 左闭右闭区间第二种写法 左闭右开区间 循环不变量接下来。。。 二分查找算法思路之简单#xff0c;实现之复杂#xff0c;是典型的现实世界问题#xff0c;映射为计算机计算问题时#xff0c;语义模拟不到位而导致错误的最典型…二分查找文章目录二分查找基本的查找第一种 左闭右闭区间第二种写法 左闭右开区间循环不变量接下来。。。二分查找算法思路之简单实现之复杂是典型的现实世界问题映射为计算机计算问题时语义模拟不到位而导致错误的最典型代表。理解二分查找是编程路上的必经之路也是最重要的一课之一。基本的查找二分查找的第一个应用就是在一个有序的数组中查找一个target值是否出现。力扣的704题就是一个原汁原味的二分查找。题目描述如下给定一个 n 个元素有序的升序整型数组 nums 和一个目标值 target 写一个函数搜索 nums 中的 target如果目标值存在返回下标否则返回 -1。再次复述一下二分查找的思路两个指针分别在有序数组的左右两侧作为查找的区间的起始和终点。然后再查找的过程中缩小区间的大小最后直到区间中没有元素就说明查找完毕。那么怎么缩小区间呢因为是有序数组所以我们可以使用区间中点的值和target值比较这样不论target大于还是小于区间中点值我们都能移动left或者right直接让区间缩小一半效率很高。自然如果target等于区间中点值则直接返回。如果你能像上面的描述那样定义二分查找那么你也一定会写了。但是大概率是在初学的时候你是无法把二分查找定义得那么仔细的所以你才会在循环判断条件发愁——到底是left right 还是left right呢到底怎么更新left和right呢其实这些是不需要死记硬背的。只要你定义好left和right的语义循环判断条件和更新区间是很自然的。下面介绍两种常见的二分查找写法你看一看他们是怎么定义left和right的以及怎么根据这个定义来更新left和right指针的。只要你懂了这两种写法你也可以写出其他的写法。第一种 左闭右闭区间首先定义查找的区间。再左闭右闭的写法中我们查找的区间为[left,right]中的所有元素同时包括left和right指向的值——因为是闭区间嘛这也是很自然的。因为我们的查找范围是整个数组所以left初始化为0right初始化为nums.size()-1.这样我们的查找区间为[0,nums.size()-1]这个区间。根据之前的思路只要区间没有元素了就停止搜索。那么在这样的定义之下在计算机中我们要这么表示while(leftright){....}这也很自然在闭区间中[a,b]中只要ab这个区间就至少有一个值只有当ab的时候[a,b]区间才没有值这时搜索才会停止。在targetnums[mid]的时候我们直接返回即可while(leftright){intmid(leftright)/2;if(targetnums[mid]){returnmid;}elseif(targetnums[mid]){...}elseif(targetnums[mid]){...}}在target nums[mid]时索命target在 [left,mid-1] 这个区间中。这也时很自然的因为nums[mid]这个元素已经确定不是target了所以target只能在[left,mid-1]这个闭区间内所以right很自然地更新为midwhile(leftright){intmid(leftright)/2;if(targetnums[mid]){returnmid;}elseif(targetnums[mid]){rightmid-1;}elseif(targetnums[mid]){...}}同理在target nums[mid]时nums[mid]这个元素也已经确定了不是我们要查找的元素所以新的查找区间不应该包含mid这个位置。left更新为mid1。完整代码为intbinary_search(vectorintnums,inttarget){intleft0,rightnums.size()-1;while(leftright){intmid(leftright)/2;if(targetnums[mid]){returnmid;}elseif(targetnums[mid]){// 将区间变为[left,mid-1]// 因为nums[mid]已经确定了不是targetrightmid-1;}elseif(targetnums[mid]){// 将区间变为[mid1,right]// 因为nums[mid]已经确定了不是targetleftmid1;}}// 没有找到return-1;}第二种写法 左闭右开区间还是一样先定义区间。既然时左闭右开写法那么我们的搜索范围是[left,right)区间内的元素。所以如果想搜索整个数组区间中就要包含整个数组。初始化区间为left0rightnums.size()也就是[0,nums.size())。那么什么时候结束搜索呢还是当区间没有元素时结束搜索。对于一个半开半闭的区间[a,b)只要ab那么区间内就至少有一个元素。当ab时区间内没有元素。举个例子说也就是说[0,1)这个区间有一个元素[0,0)这个区间没有元素。循环条件如下// ab说明区间内有元素继续搜索while(leftright){....}这里的left更新和写法一一样相信聪明的你已经明白了。现在说明一下right的更新。当targetnums[mid]时需要更新right指针但是由于他是区间的开部分也就是right指向的值不是区间的搜索范围right左部的值才是搜索范围。按照之前的说法nums[mid]已经确定了不是target所以right应该指向mid让right指向的值的左部才是要搜索的范围。while(leftright){intmid(leftright)/2;if(targetnums[mid]){returnmid;}elseif(targetnums[mid]){rightmid;}elseif(targetnums[mid]){leftmid1;}}完整代码如下intbinary_search(vectorintnums,inttarget){intleft0,rightnums.size();while(leftright){intmid(leftright)/2;if(targetnums[mid]){returnmid;}elseif(targetnums[mid]){// 将区间变为[left,mid)// 因为nums[mid]已经确定了不是targetrightmid;}elseif(targetnums[mid]){// 将区间变为[mid1,right)// 因为nums[mid]已经确定了不是targetleftmid1;}}// 没有找到return-1;}需要说明的是以上两种写法是一样的具体写哪一种纯看个人喜好。你甚至可以自己写出左开右闭的写法。循环不变量恭喜你掌握了二分查找。但是对于二分查找算法我们能学到的也远不止二分查找。我们再回过头看一下曾经困扰我们的那些细节问题是这么被解决的。left和right的初始化到底是什么循环结束的条件是什么left和right到底怎么更新这三个问题我们都是通过明确的定义算法的过程和定义语义明确的变量来实现的。定义好了left和right的语义之后我们后面的所有操作都只是在维护left和right的语义。循环结束条件就是使用了left和right定义好的语义。所以定义好left和right是很关键的。left和right又有一个高大上的名字循环不变量。下面是循环不变量的含义。我们使用循环不变量帮助我们理解一个算法为什么是对的。对于一个给定的循环不变量我们必须遵循以下三个属性初始化 在循环的第一次迭代之前循环不变量为真。保持 如果在循环的一次迭代之前循环不变量为真那么在下一次迭代之前循环不变量同样为真。终止 当循环结束时不变量能够提供我们有用的属性用于帮助我们证实算法是正确的。不变量中的不变不是说它的值不变而是说它的语义不变。简单的说就是将循环中的变量的语义定义好然后在循环的过程的每一步都维护这个变量的语义这样直到循环结束我们在循环外部也能根据循环不变量的语义获取重要的信息。所以首先是定义。left和right到底是什么left是搜索区间的左侧right是搜索区间的右侧他们一起定义了整个搜索区间。然后根据区间的定义方法可以是开区间也可以是闭区间也可以是半开半闭区间根据这些区间的定义就衍生出了不同的二分查找写法。每一个算法都是在维护自己对于区间的定义而已。其实不止是二分查找算法所有算法和工程都是如此。你要做的就是准确定义自己的变量和函数然后在算法的执行过程中不停的维护变量的语义正确的根据函数的语义实现函数和使用函数然后算法自然就正确了。掌握了变量的语义对于理解算法的细节很关键掌握函数的语义对于理解各种递归的算法有很大的帮助。例如你一定了解二叉树的中序遍历的过程其定义就是以中序遍历左子树访问根节点以中序遍历遍历右子树。结合下面的递归算法你再看看这个算法描述看看这个算法中函数的名字并根据这个函数的名字明白它的语义。那么遍历过程过程就是对中序遍历算法定义的完整计算机语言描述罢了。当然由于空树也是树这也是树的定义所以你同样可以遍历一个空树只是空树需要不同的处理。classSolution{public:voidinorder(TreeNode*root,vectorintres){if(!root){return;}inorder(root-left,res);res.push_back(root-val);inorder(root-right,res);}vectorintinorderTraversal(TreeNode*root){vectorintres;inorder(root,res);returnres;}};而一个符号最好的语义提供源就是符号的名字。所以说名字是最好的注释是维护语义的重要根据之一这也是为什么老师在编程的入门课都会告诉学生——不要起没有意义的名字。接下来。。。恭喜你现在你已经懂得了二分搜索的实现和循环不变量的含义和重要性这是编程中的重要一课。现在我们来到力扣的34题来看看这么使用变种的二分查找以及怎么通过定义正确的left和right来完成这个新的二分查找应用题。