文档详解:代码随想录(代码随想录)
视频详解:手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili状态:
二分查找:
时间复杂度O(logN):这是因为在每一次迭代中,二分法将搜索空间减半。当问题规模为 n
时,经过 k
次迭代,搜索空间将被减小为 n / 2^k
。最差情况下,经过log2N次迭代,当搜索空间缩小到1时,问题得到解决。
思路:
使用二分法的前提是数组为有序数组,同时数组中无重复元素。二分法的逻辑就是每次通过对比中间位置的值来判断目标值所在的区间,然后通过循环对比中间位置的值最后找到目标值的位置。
二分法的关键就是定义区间,一般为左闭右闭,左闭右开
在刚看到这道题时候,我知道二分法的逻辑然后根据自己的思路写了两种实现方法。分别为递归和while循环,看了卡哥的思路之后将一些细节优化了。例如mid的算法,避免加法溢出而使用减法计算中间位置。也让我记起来一些知识,加减法位的优先级比位运算高。
while循环:采用的左闭右闭
private static int searchLoop(int[] nums, int target) {
if (nums==null) throw new IllegalArgumentException();
if(nums.length==0) return -1;
int start=0;
int end=nums.length-1;
while(start<=end){
int mid=start+((end-start)>>1);
int num = nums[mid];
if(num ==target){
return mid;
}else if(num>target){
end=mid-1;
}else {
start=mid+1;
}
}
return -1;
}
递归:采用的左闭右闭
//递归
private static int searchRecursion(int[] nums, int target) {
if (nums==null) throw new IllegalArgumentException();
if(nums.length==0) return -1;
return binarySearch(nums,target,0,nums.length-1);
}
public static int binarySearch(int[] nums,int target,int start,int end) {
if (start>end){return -1;}
int mid=start+((end-start)>>1);
int num=nums[mid];
if (num==target){
return mid;
}else if(num>target){
return binarySearch(nums,target,0,mid-1);
}
return binarySearch(nums,target,mid+1,end);
}
对于leecode35搜索插入位置,采用了左闭右开方法
public static int searchInsert(int[] nums, int target) {
if (nums==null) throw new IllegalArgumentException();
int start=0;
int end=nums.length;
while (start<end){
int mid=start+((end-start)>>1);
int num=nums[mid];
if(num==target){
return mid;
}else if(num<target){
start=mid+1;
}else{
end=mid;
}
}
return start;
}
对于leecode69 x的平方根这一题,我是看题解才明白内部用二分法的逻辑的,这里也要主意中间位置的取值以及while的条件。采用左闭右闭。
思路:
如果这个整数的平方 恰好等于 输入整数,那么我们就找到了这个整数;
如果这个整数的平方 严格大于 输入整数,那么这个整数肯定不是我们要找的那个数;
如果这个整数的平方 严格小于 输入整数,那么这个整数 可能 是我们要找的那个数;
所以可以使用二分法去猜这个数,不断缩小区间。
猜的数平方以后大了就往小了猜;
猜的数平方以后恰恰好等于输入的数就找到了;
猜的数平方以后小了,可能猜的数就是,也可能不是。
一个整数的平方根肯定不会超过它自己的一半,但是 0 和 1 除外,因此我们可以在 1 到输入整数除以 2 这个范围里查找我们要找的平方根整数。0和1 单独判断一下就好。
细节:
因为题目要求向下取整,所以end可以减一但是start不能加一,因为加一会错过真正的目标数。例如:8是2.8多的平方,但是如果让2加一,就会错过真正的目标值导致程序错误。还有这里while的判断条件是不需要等于号的。因为当start和end相等的时候,while循环就应该结束了,然后这个目标数就是start或者end了,因为每个数都有它的平方根,和数组的情况不一样。然后这里的中间位置变量mid要向上取值,因为start没有加一,当只剩下两个数时,(end-start)/2会永远是0,那么start和mid就会永远相同,区间不会再缩小,会陷入死循环,所以mid要向上取整。
代码:
private static int mySqrt(int x) {
if(x==0){return 0;}
if(x==1){return 1;}
int start=1;
int end=x>>1;
while(start<end){
//中间数要向上取整, 因为当end-start为1时(只有两个数),mid会始终和start相同,然后这个时候平方仍然比x小,那么区间就不再会变进入死循环。
int mid=start+((end-start+1)>>1);
//中间数的平方大于x,证明数是在更小的区间
if(mid>x/mid){
end=mid-1;
}else if(mid<x/mid){
start=mid;
}else {
return mid;
}
}
return start;
}
移除元素
leecode27
暴力解法:
思路:
刚看到这道题时,我就想到了暴力解法,两层forloop,最外面一层遍历,最里面一层前后交换元素。我用的是类似冒泡排序的思想前后两两交换。i要减一是因为交换后,后面的数据会换到当前指针位置,所以i要继续从当前位置检查。
public static int removeELeBruteForce(int[] nums, int val) {
int count = 0;
for (int i = 0; i < nums.length - count; i++) {
int current = nums[i];
if (current == val) {
for (int j = i; j < nums.length - 1 - count; j++) {
nums[j] = nums[j] ^ nums[j + 1];
nums[j + 1] = nums[j] ^ nums[j + 1];
nums[j] = nums[j] ^ nums[j + 1];
}
count++;
i = i - 1;
}
}
return nums.length - count;
}
接下来就是双指针做法,
思路:
同过双指针,单循环做到双循环的事情。我的理解核心思想是让其中一个作为慢指针,在指向符合目标值的时候停下,然后快指针快速遍历数组,找到不为目标值的值进行替换。
单项:
private static int removeELeDoublePointerUnidirection(int[] nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
相向:
private static int removeELeDoublePointerBidirection(int[] nums, int val) {
int j=nums.length-1;
int count=0;
for (int i=0;i<=j;){
if (nums[i]==val){
count++;
nums[i]=nums[j--];
} else
i++;
}
return count;
}