算法学习之二分查找、位运算、数学系列小结

二分查找

注意事项:

  1. 在计算 mid 时不能使用 mid = (l + h) / 2 这种方式,因为 l + h 可能会导致加法溢出,应该使用 mid = l + (h - l) / 2
  2. 对 h 的赋值和循环条件有关,当循环条件为 while(l <= h) 时,h = mid - 1;当循环条件为while(l < h)时,h = mid解释如下: 在循环条件为 l <= h 时,如果 h = mid,会出现循环无法退出的情况,例如 l = 1,h = 1,此时 mid 也等于 1,如果此时继续执行 h = mid,那么就会无限循环;在循环条件为 l < h,如果 h = mid - 1,会错误跳过查找的数,例如对于数组 [1,2,3],要查找 1,最开始 l = 0,h = 2,mid = 1,判断 key < arr[mid] 执行 h = mid - 1 = 0,此时循环退出,直接把查找的数跳过了。
  3. l 的赋值一般都为 l = mid + 1,你可以想象一下其实左边永远都是闭区间的。
  4. 当循环条件为 l <= h 时,h=nums.length-1,这样可以取到范围为[l,h]。当循环条件为 l < h 时,h=nums.length,这样可以取到范围为[l,h)

寻找最左侧的二分查找:

//代码1:
int left_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0;
    int right = nums.length; // 注意

    while (left < right) { //[left,right)
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            right = mid;	//分为了[left,mid),[mid+1,right),缩小区间
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) { 
            right = mid; //因为while()是开区间,所以right可以取到mid即[left,mid),其实也是取到mid-1,如果是闭区间,就直接为[left,mid-1],也是到mid-1。
        }
    }
    // target 比所有数都大
	if (left == nums.length) return -1;
	// 类似之前算法的处理方式
	return nums[left] == target ? left : -1;
}

//代码2:
int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    // 搜索区间为 [left, right]
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            // 搜索区间变为 [mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 搜索区间变为 [left, mid-1]
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 收缩右侧边界
            right = mid - 1;
        }
    }
    // 检查出界情况 这个时候只有left = right + 1才退出
    if (left >= nums.length || nums[left] != target)
        return -1;
    return left;
}

//只要理解了区间距离,其实上面两种就没有什么区别了。
  • 左侧边界含义:如果我们返回的是leftleft的值就表示了比target小的数有几个。所以上面需要判断left是否超过数组长度。
  • while()是闭区间和开区间left,right的退出条件不同,所以要考虑一下出界情况。

寻找最右侧的二分查找:

int right_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length;

    while (left < right) {	//区间[left,right)
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            left = mid + 1; // 注意	缩小空间,left始终是左边闭区间,所以肯定是mid + 1
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    if (left == 0) return -1;
	return nums[left-1] == target ? (left-1) : -1;
   // return left - 1; 
    //为什么返回left - 1;  
    //终止条件是left = right
    //left 的更新必须是 left = mid + 1,while 循环结束时,nums[left] 一定不等于 target 了,而 nums[left-1] 可能是 target
}

//代码2:
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;
}
69. x 的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:

输入: 4
输出: 2

示例 2:

输入: 8
输出: 2
说明: 8 的平方根是 2.82842..., 
     由于返回类型是整数,小数部分将被舍去。

解题思路:

  • 一个数 x 的开方 sqrt 一定在 0 ~ x 之间,并且满足 sqrt == x / sqrt 。可以利用二分查找在 0 ~ x 之间查找 sqrt。
  • 当 x>2 时,它的整数平方根一定小于等于 x/2 。即有 0 < 整数平方根 <= x/2。
class Solution {
    public int mySqrt(int x) {
        if(x <= 1) return x;
        int l = 1, r = x/2;
        while(l <= r){	//闭区间[l , r]
            int mid = l + (r - l)/2;
            if(mid == x/mid){
                return mid;
            }else if(mid > x/mid){
                r = mid - 1;
            }else{
                l = mid + 1;
            }
        }
        return r;
    }
}

//代码2
public class Solution { 
    public int mySqrt(int x) { 
        if (x == 0) return 0; 
        long left = 1; 
        long right = x / 2; 
        while (left < right) { 	[left, right)
            //注意这一行代码 
            long mid = (right + left) / 2 + 1;	//这里为什么加1,mid更靠近右侧
            if (mid > x / mid) {
                right = mid - 1;
            } else {
                left = mid;
            }
        }
        return (int) left;
    }
}
153. 寻找旋转排序数组中的最小值

假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

请找出其中最小的元素。

你可以假设数组中不存在重复元素。

示例 1:

输入: [3,4,5,1,2]
输出: 1

示例 2:

输入: [4,5,6,7,0,1,2]
输出: 0

解题思路:
很精彩的解答:leetcode解答

class Solution {
    public int findMin(int[] nums) {
        int left = 0;
        int right = nums.length - 1;                /* 左闭右闭区间,如果用右开区间则不方便判断右值 */ 
        while (left < right) {                      /* 循环不变式,如果left == right,则循环结束 */
            int mid = left + (right - left) / 2;    /* 地板除,mid更靠近left */
            if (nums[mid] > nums[right]) {          /* 中值 > 右值,最小值在右半边,收缩左边界 */ 
                left = mid + 1;                     /* 因为中值 > 右值,中值肯定不是最小值,左边界可以跨过mid */ 
            } else if (nums[mid] < nums[right]) {   /* 明确中值 < 右值,最小值在左半边,收缩右边界 */ 
                right = mid;                        /* 因为中值 < 右值,中值也可能是最小值,右边界只能取到mid处 */ 
            }
        }
        return nums[left];    /* 循环结束,left == right,最小值输出nums[left]或nums[right]均可 */     
    }
} 

附加: 154. 寻找旋转排序数组中的最小值 II

class Solution {
    public int findMin(int[] nums) {
        int left = 0;
        int right = nums.length - 1; 
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[right]) {
                left = mid + 1;
            } else if (nums[mid] < nums[right]) {
                right = mid;
            } else {
                right--;	//只需要在nums[mid] == nums[right]时挪动右边界就行
            }
        }
        return nums[left];
    }
}
540. 有序数组中的单一元素

给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。

示例 1:

输入: [1,1,2,3,3,4,4,8,8]
输出: 2

示例 2:

输入: [3,3,7,7,10,11,11]
输出: 10          

注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。
解题思路:
因为是有序数组,所以自然会想到二分查找来解决问题。这道题只需要对偶数索引进行二分搜索,对所有偶数索引进行搜索,直到遇到第一个其后元素不相同的索引。

class Solution {
    public int singleNonDuplicate(int[] nums) {
	    int l = 0, h = nums.length - 1;
	    while(l < h) {	//退出条件 l = h
	        int m = l + (h - l) / 2;
	        if(m % 2 == 1) m--; // 保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数
	        if(nums[m] == nums[m + 1]) l = m + 2;
	        else h = m;
	    }
	    return nums[l];
	}
}

位运算

  1. n&(n-1)该位运算可以把n中最低位(不是最后一位)的1变为0。例如对于二进制表示 10110 100 ,减去 1 得到 10110011,这两个数相与得到 10110000。
  2. 任意一个数和自己^为0==>x^x=0,任意一个数和0 ^ 为自己==>x^0=x
  3. >> n 为算术右移,相当于除以 2n; >>> n 为无符号右移,左边会补上 0
191. 位1的个数

编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。
示例:

输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。

解题思路:
因为求1的个数,上面的结论1中就有可以求的方法,遍历带入这个公式

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int count = 0;
        while(n != 0){
            n &= n-1;
            ++count;
        }
        return count;
    }
}
136. 只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:

输入: [2,2,1]
输出: 1

解题思路:
因为只有一个不一样,根据结论2公式依次遍历

class Solution {
    public int singleNumber(int[] nums) {
        int single = 0;
        for (int num : nums) {
            single ^= num;
        }
        return single;
    }
}

数学系列

素数

204. 计数质数

统计所有小于非负整数 n 的质数的数量。

示例:

输入: 10
输出: 4
解释: 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。

解题思路:

  1. 直接求解
class Solution {
    public int countPrimes(int n) {
	    int count = 0;
	    for (int i = 2; i < n; i++)
	        if (isPrime(i)) count++;
	    return count;
	}
	
	// 判断整数 n 是否是素数
	public boolean isPrime(int n) {
	    for (int i = 2; i*i <= n; i++)	//如果是i的话就会超时
	        if (n % i == 0)
	            // 有其他整除因子
	            return false;
	    return true;
	}
}
  1. 如果在 [2,sqrt(n)] 这个区间之内没有发现可整除因子,就可以直接断定 n 是素数了。
class Solution {
    public int countPrimes(int n) {
        boolean[] isPrim = new boolean[n];
        Arrays.fill(isPrim,true);

        for(int i = 2; i*i < n; i++){
            if(isPrim[i]){	//从这里开始排除多种可能
                // 从 i * i 开始,因为如果 k < i,那么 k * i 在之前就已经被去除过了
                for(int j = i*i; j < n; j += i){
                    isPrim[j] = false;
                }
            }
        }
        int count = 0;
        for(int i = 2; i < n; i++){
            if(isPrim[i]) count++;
        }
        return count;
    }
}

最大公约数

int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}
int lcm(int a, int b){
    return a * b / gcd(a, b);
}

阶乘

172. 阶乘后的零

给定一个整数 n,返回 n! 结果尾数中零的数量。

示例 1:

输入: 3
输出: 0
解释: 3! = 6, 尾数中没有零。

解题思路:
尾部的 0 由 2 * 5 得来,2 的数量明显多于 5 的数量,因此只要统计有多少个 5 即可。
对于一个数 N,它所包含 5 的个数为:N/5 + N/52 + N/53 + ...,其中 N/5 表示不大于 N 的数中 5 的倍数贡献一个 5,N/52 表示不大于 N 的数中 52 的倍数再贡献一个 5 …。

//递归
class Solution {
    public int trailingZeroes(int n) {
        return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
    }
}

//迭代
public int trailingZeroes(int n) {
    int count = 0;
    while (n > 0) {
        count += n / 5;
        n = n / 5;
    }
    return count;
}

字符串加法减法

67. 二进制求和

给你两个二进制字符串,返回它们的和(用二进制表示)。输入为 非空 字符串且只包含数字 1 和 0。

示例 1:

输入: a = "11", b = "1"
输出: "100"

示例 2:

输入: a = "1010", b = "1011"
输出: "10101"

解题思路:
将尾巴对齐,用一个变量来记录是否需要进位,用心的str来接收这个结果。

class Solution {
    public String addBinary(String a, String b) {
        int i = a.length() - 1, j = b.length() - 1, carry = 0;
        String str = "";
        while(i >= 0 || j >= 0){
            if(i >= 0 && a.charAt(i--) == '1') carry++;
            if(j >= 0 && b.charAt(j--) == '1') carry++;
            str = carry % 2 + str;
            carry /= 2;
        }
        if(carry == 1) str = "1" + str;
        return str;
    }
}
415. 字符串相加

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。

class Solution {
    public String addStrings(String num1, String num2) {
        StringBuilder sb = new StringBuilder();
        int i = num1.length() - 1, j = num2.length() - 1, carry = 0;
        while(i >= 0 || j >= 0 || carry != 0){
            int x = i < 0 ? 0 : num1.charAt(i--) - '0'; //对位数较短的数字进行补零操作
            int y = j < 0 ? 0 : num2.charAt(j--) - '0';
            sb.append((x+y+carry) % 10);
            carry = (x+y+carry) / 10;
        }
        return sb.reverse().toString();
    }
}

其他

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值