文章目录
二分查找
注意事项:
- 在计算 mid 时不能使用 mid = (l + h) / 2 这种方式,因为 l + h 可能会导致加法溢出,应该使用
mid = l + (h - l) / 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,此时循环退出,直接把查找的数跳过了。 - l 的赋值一般都为
l = mid + 1
,你可以想象一下其实左边永远都是闭区间的。 - 当循环条件为
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;
}
//只要理解了区间距离,其实上面两种就没有什么区别了。
- 左侧边界含义:如果我们返回的是
left
,left
的值就表示了比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]均可 */
}
}
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];
}
}
位运算
n&(n-1)
该位运算可以把n中最低位(不是最后一位)的1变为0。例如对于二进制表示 101101
00 ,减去 1 得到 10110011,这两个数相与得到 101100
00。- 任意一个数和自己^为0==>
x^x=0
,任意一个数和0 ^ 为自己==>x^0=x
- >> 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 。
解题思路:
- 直接求解
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;
}
}
- 如果在 [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();
}
}