正常实现
Input : [1,2,3,4,5];key : 3
return the index : 2
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length - 1;
while (l <= h) {
int m = l + (h - l) / 2;
if (nums[m] == key) {
return m;
} else if (nums[m] > key) {
h = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
时间复杂度
二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(logN)。
m 的计算
有两种计算中值 m 的方式:
l + h 可能出现加法溢出,也就是说加法的结果大于整型能够表示的范围。但是 l 和 h 都为正数,因此 h - l 不会出现加法溢出问题。所以,最好使用第二种计算法方法。
未成功查找的返回值
循环退出时如果仍然没有查找到 key,那么表示查找失败。
变种实现
二分查找可以有很多变种,变种实现要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下:
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] >= key) {
h = m;
} else {
l = m + 1;
}
}
return l;
}
该实现和正常实现有以下不同:
在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。
在 h 的赋值表达式为 h = m 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况:
nums = {0, 1, 2}, key = 1
l m h
0 1 2 nums[m] >= key
0 0 1 nums[m] < key
1 1 1 nums[m] >= key
1 1 1 nums[m] >= key
…
查找的返回值的理解
变种的二分查找最终返回的是 l,该返回值有以下解释:
当循环体退出时,为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。
leetcode 35 正确插入的位置(简单)
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
题解:
class Solution {
public int searchInsert(int[] nums, int target) {
if(nums.length == 0){
return 0;
}
int l = 0, r = nums.length-1;
if(target>nums[r]){
return r+1;
}
while(l
leetcode 69 x的平方根(简单)
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。
题解:
注意:乘法运算很容易造成溢出
class Solution {
public int mySqrt(int x) {
int l = 1, r = x;
while(l=x/mid){ // 乘法可能会溢出, 改为除法
r = mid;
}else{
l = mid+1;
}
}
return l>x/l? (l-1):l; // 乘法可能会溢出, 改为除法
}
}
leetcode 744 寻找比目标字母大的最小字母(简单)
给定一个只包含小写字母的有序数组letters 和一个目标字母 target,寻找有序数组里面比目标字母大的最小字母。
数组里字母的顺序是循环的。举个例子,如果目标字母target = ‘z’ 并且有序数组为 letters = [‘a’, ‘b’],则答案返回 ‘a’。
示例:
输入:
letters = [“c”, “f”, “j”]
target = “a”
输出: “c”
输入:
letters = [“c”, “f”, “j”]
target = “c”
输出: “f”
输入:
letters = [“c”, “f”, “j”]
target = “d”
输出: “f”
输入:
letters = [“c”, “f”, “j”]
target = “g”
输出: “j”
输入:
letters = [“c”, “f”, “j”]
target = “j”
输出: “c”
输入:
letters = [“c”, “f”, “j”]
target = “k”
输出: “c”
注:
题解:
class Solution {
public char nextGreatestLetter(char[] letters, char target) {
int n = letters.length;
int l = 0, r = n-1;
while(l=target){
r = mid;
}else{
l = mid+1;
}
}
char res = letters[l];
if(l == n-1 && letters[l] < target){
res = letters[0];
}
if(letters[l] == target){
int i = 1;
while(res == target){ // 防止[e,e,e,e,n,n] 的情况
res = letters[(l+i)%n];
i++;
}
}
return res;
}
}
leetcode 278 第一个错误的版本(简单)
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例:
给定 n = 5,并且 version = 4 是第一个错误的版本。
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
题解:
/* The isBadVersion API is defined in the parent class VersionControl.
boolean isBadVersion(int version); */
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int l=1, r=n;
while(l
leetcode 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
题解:
class Solution {
public int findMin(int[] nums) {
int n = nums.length;
int l=0, r=n-1;
while(lnums[r]){
l = mid+1;
}else{
r = mid;
}
}
return nums[l];
}
}
leetcode 34 在排序数组中查找元素的第一个和最后一个位置(中等)
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
题解:
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] res = new int[2];
int s = sub(nums, target);
int e = sub(nums, target+1)-1;
if( s == nums.length || nums[s] != target){ // ||运算具有短路性质,顺序不能变
res[0] = -1;
res[1] = -1;
}else{
res[0] = s;
res[1] = e;
}
return res;
}
// 二分查找
private int sub(int[] nums, int target){
int l=0, r=nums.length; // 注意,这里的右边界取nums.length, 当目标值大于数组中最大值时,返回nums.length
while(l=target){
r = mid;
}else{
l = mid+1;
}
}
return l;
}
}
leetcode 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)空间复杂度中运行。
题解:
插入单一元素后,成对元素在数组中的奇偶位会发生改变。
偶位 | 奇位 | 偶位 | 奇位 | 偶位 | 奇位 | 偶位 | 奇位 | 偶位 |
---|---|---|---|---|---|---|---|---|
1 | 1 | 2 | 2 | 9 | 4 | 4 | 5 | 5 |
插入单一元素9后,原先先偶数位后奇数位的成对元素,变成了先奇数位后偶数位
由此,我们先保证mid为偶数位,若nums[mid]==nums[mid+1], 则单一元素在mid+1之后, 令l=mid+2;
若nums[mid] != mid[mid+1], 则说明mid可能是单一元素所在位置,也可能是单一元素后边的位置,令r=mid.
class Solution {
public int singleNonDuplicate(int[] nums) {
int n = nums.length;
int l = 0, r = n-1;
while(l
leetcode 668 乘法表中第k小的数(困难)
几乎每一个人都用 乘法表。但是你能在乘法表中快速找到第k小的数字吗?
给定高度m 、宽度n 的一张 m * n的乘法表,以及正整数k,你需要返回表中第k 小的数字。
例 1:
输入: m = 3, n = 3, k = 5
输出: 3
解释:
乘法表:
1 2 3
2 4 6
3 6 9
第5小的数字是 3 (1, 2, 2, 3, 3).
例 2:
输入: m = 2, n = 3, k = 6
输出: 6
解释:
乘法表:
1 2 3
2 4 6
第6小的数字是 6 (1, 2, 2, 3, 4, 6).
注意:
题解:
解这道题可能会想着先构造出这个乘法表,然后再去搜索,但这样是行不通的,因为m、n的取值可能非常大,非常耗内存。
首先我们知道在m、n的乘法表中取值范围为[1, m * n],那么我们可不可以使用使用二分搜索呢?
观察乘法表我们会发现,由于构造关系,决定了他每一行都是递增的。
如果我们需要在第i行中寻找小于等于num的个数,我们只要min(num / i, n),其中(i是这一行的行号,n是矩阵的列数)num / i代表的是如果num也在第i行,n为列数,所以只要取最小值就是第i行中不大于num的个数。(比如例题1中,我们需要知道第2行,不大于4的个数,min(4 / 2, 3) == 2个(就是2, 4))
这样,我们确定这个乘法表中不大于num的数的总个数就非常简单了,我们只要将每一行不大于num的个数累加即可。(比如例题1中,我们需要知道乘法表中不大于4的个数,第一行3个、第二行2个,第三行1个)
现在,我们就可以使用二分搜索了,初始化left = 1, right = n * m ,mid = (left + right) / 2,在m,n的乘法表中寻找不超过mid的个数。
class Solution {
public int findKthNumber(int m, int n, int k) {
int l = 1, r = m*n;
while(l=k){
r = mid;
}else{
l = mid+1;
}
}
return l;
}
}
leetcode 658 找到 K 个最接近的元素(中等)
给定一个排序好的数组,两个整数 k 和 x,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。如果有两个数与 x 的差值一样,优先选择数值较小的那个数。
示例 1:
输入: [1,2,3,4,5], k=4, x=3
输出: [1,2,3,4]
示例 2:
输入: [1,2,3,4,5], k=4, x=-1
输出: [1,2,3,4]
说明:
题解:
假设 mid 是左边界,则当前区间覆盖的范围是 [mid, mid + k -1]. 如果发现 a[mid] 与 x 距离比 a[mid + k] 与 x 的距离要大,说明解一定在右侧。
class Solution {
public List findClosestElements(int[] arr, int k, int x) {
List res = new ArrayList();
int n = arr.length;
if(n==0 || k>n){
return res;
}
int l=0, r=n-k; // 注意这里 r 的初值时n-k,而不是n-1
while(l arr[m+k]-x){ // 区间需要右移
l=m+1;
}else{ // 区间左移或者不移,当arr[m]恰好是最优区间的左边界时,区间不移
r=m;
}
}
for(int i=l; i
lettcode 410 分割数组的最大值(困难)
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意:
数组长度 n 满足以下条件:
示例:
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
题解:
1 dfs暴力求解, leetcode会超时
时间复杂度O(n^m), 空间复杂度O(n)
class Solution {
private int res = Integer.MAX_VALUE;
public int splitArray(int[] nums, int m) {
dfs(nums, m, 0, 0, 0, 0);
return res;
}
private void dfs(int[] nums, int m, int index, int cntOfArr, int curSum, int maxSum){
if(index == nums.length){
if(cntOfArr == m){
res = Math.min(res, maxSum);
}
return;
}
// 由于连续,只能向前一个分割区添加索引为index的元素
// 当index==0时,只能新开一个分组
if(index > 0){
dfs(nums, m, index+1, cntOfArr, curSum+nums[index], Math.max(maxSum, curSum+nums[index]));
}
// 分割数没有达到上限m时, 可以将当前元素作为一个新分割
if(cntOfArr
2 动态规划
这个问题满足无后向性的特点。
无后向性的特点意味着,一旦当前状态确定了,它就不会被之后的状态影响。在这个问题里面,如果我们在将 nums[0…j] 分成 i 份时得到了当前最小的分割数组的最大值,不论后面的部分怎么分割这个值不会受到影响。
首先我们把 dp[i][j] 定义为将 nums[0…j] 分成 i 份时能得到的最小的分割子数组最大值。
对于第 i个子数组,它为数组中下标 k + 1 到 j 的这一段。因此,dp[i][j] 可以从 max(f[i-1][k], nums[k + 1] + … + nums[j]) 这个公式中得到。遍历所有可能的 k,会得到 dp[i][j] 的最小值。
整个算法那的最终答案为 dp[m-1][n-1],其中 n 为数组大小。
复杂度分析
时间复杂度: O(n^2 * m)O(n^2∗m)
总状态数为 O(n * m)O(n∗m)。为了计算每个状态 f[i][j],需要遍历整个数组去找到那个最优的 k,这里会产生 O(n)O(n) 次循环。所以总时间复杂度为 O(n ^ 2 * m)O(n^2 ∗m).
空间复杂度: O(n * m)O(n∗m)
空间复杂度为状态总数,也就是 O(n * m)O(n∗m)。
class Solution {
public int splitArray(int[] nums, int m) {
int n = nums.length;
// 用第[0,i]号节点完成第[0,j]件工作,所需的最大处理时间
int[][] dp = new int[m][n];
// sum[k] 表示nums[0]~nums[k]的和
int[] sum = new int[n];
sum[0] = nums[0];
for(int i=1; ij
dp[i][j] = Integer.MAX_VALUE;
for(int k=0; k
3.二分查找法
子数组的最大值是有范围的,即在区间[max(nums), sum(nums)]之中。
令l=max(nums),h=sum(nums),mid=(l+h)/2,计算数组和最大值不大于mid对应的子数组个数cnt (这个是关键!)
如果cnt>m,说明划分的子数组多了,即我们找到的mid偏小,故l=mid+1;
否则,说明划分的子数组少了,即mid偏大(或者正好就是目标值),故h=mid。
class Solution {
public int splitArray(int[] nums, int m) {
int n = nums.length;
int maxNum = nums[0];
int sumArr = 0;
for(int i=0; i maxNum){
maxNum=nums[i];
}
sumArr += nums[i];
}
// 子数组的最大和一定介于[maxNum, sumOfArr]之间,通过猜测最大和,对此区间进行二分查找
// 当猜测的子数组最大和偏小时,子数组个数会偏多,猜测的子数组最大和偏大时,子数组个数会偏小
int l = maxNum, r = sumArr;
while(l maxSum){
cnt++;
sum = 0;
}
sum = sum + nums[i];
}
return cnt+1; // 注意,最后一组可能不满足sum + nums[i] > maxSum,这里要加一
}
}
leetcode 875 爱吃香蕉的珂珂(中等)
珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。
珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。
示例 1:
输入: piles = [3,6,7,11], H = 8
输出: 4
示例 2:
输入: piles = [30,11,23,4,20], H = 5
输出: 30
示例 3:
输入: piles = [30,11,23,4,20], H = 6
输出: 23
提示:
题解:
二分法, 找出能吃完所有香蕉的最小速度的可能取值范围,通过二分定位最终答案
class Solution {
public int minEatingSpeed(int[] piles, int H) {
int n = piles.length;
int maxNum = piles[0];
for(int i=0; imaxNum){
maxNum = piles[i];
}
}
// 能吃完所有香蕉的最小速度介于[1, maxNum]之间,进行二分查找
int l=1, r=maxNum;
while(lH){
l = mid+1;
}else{
r = mid;
}
}
return l;
}
// 在速度为k的情况下,吃完所有香蕉所需的最少小时数
private int getH(int[] piles, int k){
int h = 0;
for(int i=0; i
参考:Cyc2018 算法