vlambda博客
学习文章列表

二分查找算法合集-1

 二分查找作为一种高效率的查找方法,在计算机算法中被广泛使用。本系列归纳了基本的二分查找算法,以及二分查找算法的几种变形,包括:lower bound,upper bound等。同时每种算法在leetcode上给出了对应的习题,希望能够帮助到学习算法的同学。


01

二分查找

问:查找是做什么?

答:这里的查找可以理解为从一组数字中,找出某一个数字是否存在,存在的话在第几个位置。例如:从【1,3,2,4,7,6】组数字中,查找4是否存在,那么可以得到在第4个位置。


问:二分查找是干什么用的呢?

答:在计算机科学中,二分查找算法也叫折半查找算法或者对数搜索算法。是一种在有序的数组中查找特征元素的算法。这里是一种特殊的情况,要求了数组要有序。在【0,2,3,4,15,16】的这种有序的数组中可以用二分查找,在1,3,2,4,7,6】这种无序的数组中就不能直接使用二分查找。


问:二分查找有什么好处呢?

答:二分查找可以在对数时间复杂度的得到结果,即时间复杂度为O(log n)。例如:在【0,2,3,4,15,16中查找4是否存在,不用二分查找,可以把每一个数字判断一次,这样共需要判断4次,如果4在最后一个位置,需要查找6次。序列有多长,最差就需要查找多次。使用二分查找,最多只需要查找3次。这样看还不明显,如果序列长度为1024,那么线性查找最多需要查找1024次,而二分查找最多查找10次,差距就很明显了。


上面三个问题解释清楚了二分查找是什么,和为什么使用二分查找的问题。下面我们具体讲解二分查找算法。


给定一个升序数组A=,并且二分查找算法合集-1,给定一个目标值T,下面的算法返回A数组中T的位置。

1. 设置L=0,R=n-12. 如果L > R 那么搜索结束,反回失败。3. 令 m = (L+R) / 24. 如果Am T, 则L = m + 1,返回步骤25. 若果Am > T, 则R = m - 1  返回步骤26. 如果Am = T, 则返回m

说明一下,在计算机中序列是从第0个数字开始的,所以一个序列的序号是0到n-1。另外,在计算机中对整数的除法是向下取整的,例如5/2=2。


【0,2,3,4,15,16】,查找目标值4为例,演示一遍二分查找算法。设置L=0表示查找范围的最左边位置,设置R=n-1=6-1=5,表示查找范围的最右边,其中n为序列的长度为6。

二分查找算法合集-1

取m=(L+R)/ 2 = (0+5)/ 2 = 2,计算机中对整数的除法是向下取整的,因此这里m取2,那么A2=3,即第2个元素的值为3。

二分查找算法合集-1

此时,A2 = 3 < 4,那么L = m + 1 = 2 + 1 = 3.

二分查找算法合集-1

重复上述步骤,m=(L+R)/ 2 = (3+5)/ 2 = 4,A4=15 > 4,那么

R = m - 1 = 5 - 1 = 4。

二分查找算法合集-1

继续重复上述步骤,m=(L+R)/ 2 = (3+4)/ 2 = 3,A3=4=4,查找成功,返回3,表示4在序列中的第三个位置。

二分查找算法合集-1

看到这里二分查找的原理已经讲完了,后面的部分是二分查找在leetcode上相关的习题,读者可以选择感兴趣的部分阅读。


02

二分查找习题

本节给出二分查找的8道典型leetcode练习题,不仅仅要理解第一节的二分查找形式,更要理解二分查找的思想。后面的部分:题目一是典型的二分查找算法实现;题目二是二分思想在树中的应用;题目三和题目八是二分查找在计算除法和pow中的应用;题目四、五、六、七是二分查找的简单变形,可以套用题目一的范式,只不是简单的条件修改,请仔细对比体会4-7题,后续的系列介绍upper bound和lower bound也会频繁出现4-7题的范式。

另外一点小技巧,在二分查找中,m的计算方法有两种。

1)m=(l+r)/2 

2)m=l+(r-l)/2

下面的题目中两种方式都可以使用,但是在熟悉之后,推荐使用第二种方式。原因是l+r容易导致越界,而第二种写法则避免了越界的问题,更加安全。


如果下列题目有没写清楚的地方,或者还有什么想了解的地方,请私信留言,我会在下一期进行解答。


题目1: leetcode 704题

二分查找算法合集-1

解题思路就是实现一遍上述算法,这里使用c++实现。

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


题目2:leetcode 222题

二分查找算法合集-1


  • 解法一:深度优先搜索dfs,每个node cnt+1,这里不做展示。时间复杂度为O(n)。

  • 解法二:首先,一个满二叉树节点的数量为2^h-1,其中h为树的高度。那么可以通过求出左子树和右子树的高度,如果高度相等,那么左子树是满二叉树,直接返回左子树的节点数量,递归求右子树的节点数量。如果左边比右边多,那么右边是满二叉树,递归求左子树的节点数量,右子树的节点数量可以直接返回。

/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */class Solution {public: int countNodes(TreeNode* root) { if(!root) return 0; int l = getHeight(root->left); int r = getHeight(root->right); if (l > r) return 1+countNodes(root->left) + (1 << r) - 1; return 1+(1<<l)-1 + countNodes(root->right); }  int getHeight(TreeNode* root) { if(!root) return 0; return 1 + getHeight(root->left); }};


题目3:leetcode 29题

二分查找算法合集-1

  • 解法一:本题首先可以忽略符号,得到结果之后再给定符号。暴力的解法是直接用被除数不断的减掉除数,当不能减的时候,减了多少次就是最终的答案。

  • 解法二:符号判断同解法一。但是减的时候可以不断判断,是否除数是否大于被除数。如果除数比被除数小,那么可以给除数*2,这样的话,这样的话,减一次,相当于原来减两次。按照这个方式递归下去,直到除数大于被除数。然后依次返回,使用减法完成。

class Solution {public: int divide(int dividend, int divisor) { if(divisor == 0 || (dividend == INT_MIN && divisor==-1)) { return INT_MAX; } if (divisor == 1) return dividend; int sign = 1; if((dividend > 0 && divisor < 0) || (dividend < 0 && divisor > 0)) { sign = -1; } long long int dividend_new = dividend, divisor_new = divisor; if(dividend < 0) dividend_new = -dividend_new; if(divisor < 0) divisor_new = -divisor_new; int result = divide(dividend_new, divisor_new, 1); return sign == 1 ? result : -result; }  int divide(long long int &dividend, long long int divisor, int x) { if (divisor > dividend) { return 0; } int result = 0; result = divide(dividend, divisor << 1, x << 1); if (divisor > dividend) { return result; } dividend -= divisor; return result + x; }};


题目4:leetcode 157题

二分查找算法合集-1

  • 解法一:暴力求解,直接按顺序扫一遍数组,得到最小值。

  • 解法二:按照二分查找的思路,如果nums[l] < nums[r],那么数组为递增的,nums[l]为最小值。如果nums[l] < nums[m],那么左边为递增序列,从右边查找,l = m + 1即可。如果nums[l] > nums[m],那么最小值肯定不在右边,从左边查找,r = m即可。

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


题目5:leetcode 162题

二分查找算法合集-1

解法:如果没有复杂度要求,直接顺序查找一次就行,找到一个大于左右两边的就返回。要求复杂度为O(logn),肯定要使用二分查找。当前数字为mid,那么比较mid-1和mid+1的数字,只向大的一遍查找即可。因为大的一边至少会存在一个peak。

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


题目6:leetcode278题

二分查找算法合集-1

解法:二分查找,如果version(m)是badversion,那么第一个badversion在l到m之间,如果version(m)不是badversion,那么第一个badversion在m+1到n之间。

// The API isBadVersion is defined for you.// bool isBadVersion(int version);
class Solution {public: int firstBadVersion(int n) { long long int l = 1, r = n; while(l < r) { int m = (l + r) / 2; if(isBadVersion(m)) r = m; else l = m+1; } return l; }};


题目7:leetcode 275题

解法:还是二分查找的套路,只不过判断条件需要改为n-m,即文章的数量。

class Solution {public: int hIndex(vector<int>& citations) { int n = citations.size(); int l = 0, r = n - 1; while (l <= r) { int m = l + (r-l) / 2; if (citations[m] == n-m) return n-m; if (citations[m] > (n-m)) {r = m - 1;} else {l = m+1;} } return n - l; }};


题目8:leetcode 50题

解法:使用二分法的思想递归,例如:2的15次方就等于2^7*2*7*2,递归可得2^7=2^3*2^3*2,继续递归2^3=2^1*2^1*2,最后2^0=1。然后不断递归返回。这样2^7, 2^3, 2^1都只计算了一次,也不需要2*2*2...*2计算15次。当n为负数这样写也成立,half就是每次折半之后的结果,只不过单独的乘法要改成除法。

class Solution {public: double myPow(double x, int n) { if (n == 0) return 1; double half = myPow(x, n / 2); if (n % 2 == 0) return half * half; if (n > 0) return half * half * x; return half * half / x; }};