想系统学习算法的可以三连关注,后续更新大厂所有类型的算法
10.二分查找
二分查找 总结,
已解答
中等
相关标签
相关企业
提示
已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7]
在变化后可能得到:
-
若旋转
4
次,则可以得到[4,5,6,7,0,1,2]
-
若旋转
7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2] 输出:1 解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
public static int findMin(int[] nums) { int l = 0; int n = nums.length; int r = n-1; int min = Integer.MAX_VALUE; while(l<=r){ int m = l+((r-l)>>>1); if(nums[m]>=nums[r]){ l = m+1; }else{ r = m-1; } } return nums[r]; }
已解答
中等
相关标签
相关企业
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]
public static int[] searchRange(int[] nums, int target) { int l = 0; int r = nums.length-1; int left = -1; int right = -1; while(l<=r){ int mid = (l+r)>>>1; if(nums[mid]<target){ l=mid+1; }else { left = mid; r = mid-1; } } l = 0; r = nums.length-1; while(l<=r){ int mid = (l+r)>>>1; if(nums[mid]<=target){ right = mid; l=mid+1; }else { r = mid-1; } }; if (left>right||left ==-1||nums[left]!=target){ return new int[]{-1,-1}; } return new int[]{left,right}; }
11.二分答案法
前置知识:讲解005-对数器、讲解006-基本二分搜索、讲解042-进一步了解对数器
二分答案法
1)估计 最终答案可能的范围 是什么
2)分析 问题的答案 和 给定条件 之间的 单调性,大部分时候只需要用到 自然智慧
3)建立一个f函数,当答案固定的情况下,判断 给定的条件是否达标
4)在 最终答案可能的范围上不断二分搜索,每次用f函数判断,直到二分结束,找到最合适的答案
核心点:分析单调性、建立f函数
注意: 这个技巧常用且重要,一定要引起重视,非常的美、精妙! 后续还会使用到
875. 爱吃香蕉的珂珂
中等
珂珂喜欢吃香蕉。这里有 n
堆香蕉,第 i
堆中有 piles[i]
根香蕉。警卫已经离开了,将在 h
小时后回来。
珂珂可以决定她吃香蕉的速度 k
(单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k
根。如果这堆香蕉少于 k
根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h
小时内吃掉所有香蕉的最小速度 k
(k
为整数)。
示例 1:
输入:piles = [3,6,7,11], h = 8 输出:4
public static int minEatingSpeed(int[] piles, int h) { int r = 0; int l = 1; for(int i:piles){ r = Math.max(r,i); } int ans = r; while(l<=r){ int m = l+((r-l)>>1); if(process(piles,m,h)){ ans = m; r = m-1; }else{ l = m+1; } } return ans; } public static boolean process(int []piles,int speed,int h){ int ans = 0; for(int i = 0;i<piles.length;i++){ ans += (piles[i]+speed-1)/speed; if (ans>h){ return false; } } if(ans<=h){ return true; }else{ return false; } }
机器人跳跃问题牛客题霸牛客网 (nowcoder.com)
描述
机器人正在玩一个古老的基于DOS的游戏。游戏中有N+1座建筑——从0到N编号,从左到右排列。编号为0的建筑高度为0个单位,编号为i的建筑的高度为H(i)个单位。
起初, 机器人在编号为0的建筑处。每一步,它跳到下一个(右边)建筑。假设机器人在第k个建筑,且它现在的能量值是E, 下一步它将跳到第个k+1建筑。它将会得到或者失去正比于与H(k+1)与E之差的能量。如果 H(k+1) > E 那么机器人就失去 H(k+1) - E 的能量值,否则它将得到 E - H(k+1) 的能量值。
游戏目标是到达第个N建筑,在这个过程中,能量值不能为负数个单位。现在的问题是机器人以多少能量值开始游戏,才可以保证成功完成游戏?
输入描述:
第一行输入,表示一共有 N 组数据.
第二个是 N 个空格分隔的整数,H1, H2, H3, ..., Hn 代表建筑物的高度
输出描述:
输出一个单独的数表示完成游戏所需的最少单位的初始能量
示例1
输入:
5 3 4 3 2 4
输出:
4
import java.util.Scanner; import java.io.*; // 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main { public static void main(String[] args) throws IOException { BufferedReader bf = new BufferedReader((new InputStreamReader(System.in))); // 注意 hasNext 和 hasNextLine 的区别 String [] s = bf.readLine().split(" "); int n = Integer.parseInt(s[0]); int [] arr = new int[n]; s = bf.readLine().split(" "); int min = Integer.MAX_VALUE; int max = Integer.MIN_VALUE; for (int i = 0; i < n; i++) { int num = Integer.parseInt(s[i]); arr[i] = num; min = Math.min(num, min); max = Math.max(max, num); } int ans = -1; int r = max; int l = min; while (l <= r) { int mid = l + ((r - l) >> 1); if (process(arr, mid, max)) { ans = mid; r = r - 1; } else { l = l + 1; } } PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); out.print(ans); out.close(); bf.close(); } public static boolean process(int [] arr, int pouwer, int max) { for (int i = 0; i < arr.length; i++) { if (arr[i] > pouwer) { pouwer -= arr[i] - pouwer; } else { pouwer += pouwer - arr[i]; } if (pouwer > max) { return true; } if (pouwer < 0) { return false; } } return true; } }
分割数组的最大值(画匠问题) 给定一个非负整数数组 nums 和一个整数 m 你需要将这个数组分成 m 个非空的连续子数组。 设计一个算法使得这 m 个子数组各自和的最大值最小。
410. 分割数组的最大值
已解答
困难
给定一个非负整数数组 nums
和一个整数 k
,你需要将这个数组分成 k
个非空的连续子数组。
设计一个算法使得这 k
个子数组各自和的最大值最小。
这题也是分析很容易找到答案的给定条件k的单调性,k越大,答案就越小,那我们找到可能出现的最大答案max,以及最小的min,使用二分查找再合适不过了,关键就是找到答案和给定条件的单调性。
示例 1:
输入:nums = [7,2,5,10,8], k = 2 输出:18 解释: 一共有四种方法将 nums 分割为 2 个子数组。 其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。 因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
public static int splitArray(int[] nums, int k) { int max = 0; int min = 0; for(int i = 0;i<nums.length;i++){ max+=nums[i]; min = Math.max(nums[i],min); } int l = min;//取min是为了缩小范围,其次防止一个数都装不下 int r = max; int ans = max; while(l<=r){ int mid = l + ((r-l)>>>1); if(f(mid,nums)<=k){ ans = mid; r = mid - 1; }else{ l = mid + 1; } } return ans; } public static int f(int mid,int [] nums){ int ans = 0; for(int l = 0,r = 0;r<nums.length;){ int sum = 0; sum = nums[r++]; while (r<nums.length&&(sum+nums[r])<=mid){ sum +=nums[r++]; } l = r; ans++; } return ans; }
719. 找出第 K 小的数对距离
已解答
困难
提示
数对 (a,b)
由整数 a
和 b
组成,其数对距离定义为 a
和 b
的绝对差值。
给你一个整数数组 nums
和一个整数 k
,数对由 nums[i]
和 nums[j]
组成且满足 0 <= i < j < nums.length
。返回 所有数对距离中 第 k
小的数对距离。
分析题目很轻易可以得到答案的范围介于0 和 nums[n-1]-nums[0];之间,
题目又要求第k小的距离,发现 答案范围越大,对数越多,那我们就找到答案的对数之间的单调性了,就很合适使用二分查找发,关键是找到单调性
示例 1:
输入:nums = [1,3,1], k = 1 输出:0 解释:数对和对应的距离如下: (1,3) -> 2 (1,1) -> 0 (3,1) -> 2 距离第 1 小的数对是 (1,1) ,距离为 0 。
public static int smallestDistancePair(int[] nums, int k) { int n = nums.length; Arrays.sort(nums); int max = nums[n-1]-nums[0]; int l = 0; int r = max; int ans = 0; while (l<=r){ int mid = l + ((r-l)>>1); int result = f(mid,nums); if (result>=k){ ans = mid; r = mid-1; }else { l = mid+1; } } return ans; } //使用滑动窗口计算满足条件的数对个数更快 private static int f(int mid, int[] nums) { int k = 0; for (int l = 0 ,r = 0;l<nums.length;l++){ while (r+1<nums.length&&nums[r+1]-nums[l]<=mid) { r++; } k+=r-l; } return k; }
2141. 同时运行 N 台电脑的最长时间
已解答
困难
相关标签
相关企业
提示
你有 n
台电脑。给你整数 n
和一个下标从 0 开始的整数数组 batteries
,其中第 i
个电池可以让一台电脑 运行 batteries[i]
分钟。你想使用这些电池让 全部 n
台电脑 同时 运行。
一开始,你可以给每台电脑连接 至多一个电池 。然后在任意整数时刻,你都可以将一台电脑与它的电池断开连接,并连接另一个电池,你可以进行这个操作 任意次 。新连接的电池可以是一个全新的电池,也可以是别的电脑用过的电池。断开连接和连接新的电池不会花费任何时间。
注意,你不能给电池充电。
请你返回你可以让 n
台电脑同时运行的 最长 分钟数。
示例 1:
输入:n = 2, batteries = [3,3,3] 输出:4 解释: 一开始,将第一台电脑与电池 0 连接,第二台电脑与电池 1 连接。 2 分钟后,将第二台电脑与电池 1 断开连接,并连接电池 2 。注意,电池 0 还可以供电 1 分钟。 在第 3 分钟结尾,你需要将第一台电脑与电池 0 断开连接,然后连接电池 1 。 在第 4 分钟结尾,电池 1 也被耗尽,第一台电脑无法继续运行。 我们最多能同时让两台电脑同时运行 4 分钟,所以我们返回 4 。
public static void main(String[] args) { System.out.println(maxRunTime(3, new int[]{10, 10, 3, 5})); } public static long maxRunTime(int n, int[] batteries) { long sum = 0; long min = Integer.MAX_VALUE; for(int i = 0 ;i<batteries.length;i++){ sum +=batteries[i]; min = Math.min(min,batteries[i]); } long l = min; long r = sum; long ans = min; while(l<=r){ long mid = l+((r-l)>>1); if(f(n,mid,batteries,sum)){ ans = mid; l = mid+1; }else{ r = mid-1; } } return ans; } public static boolean f(int n,long mid ,int [] batteries,long sum){ int length = batteries.length; for (int i:batteries){ if (i>=mid){ n--; sum-=i; length--; } } if(sum>=mid*n&&length>n){ return true; }else{ return false; } }
谷歌真题
// 计算等位时间 // 给定一个数组arr长度为n,表示n个服务员,每服务一个人的时间 // 给定一个正数m,表示有m个人等位,如果你是刚来的人,请问你需要等多久? // 假设m远远大于n,比如n <= 10^3, m <= 10^9,该怎么做是最优解? // 谷歌的面试,这个题连考了2个月 // 找不到测试链接,所以用对数器验证 public static int waitingTime2(int[] arr, int m) { int min = Integer.MAX_VALUE; for (int i = 0;i<arr.length;i++){ min = Math.min(min,arr[i]); } int r = min*m; int l = 0; int ans = 0; while (l<=r){ int mid = l+((r-l)>>1); if (f(mid,arr,m)){ ans = mid; r = mid-1; }else { l = mid+1; } } return ans; } public static boolean f(int mid,int [] arr,int m){ int ans = 0; for (int num : arr) { ans += (mid / num) + 1; } return ans>m; } // 堆模拟 // 验证方法,不是重点 // 如果m很大,该方法会超时 // 时间复杂度O(m * log(n)),额外空间复杂度O(n) public static int waitingTime1(int[] arr, int m) { // 一个一个对象int[] // [醒来时间,服务一个客人要多久] PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> (a[0] - b[0])); int n = arr.length; for (int i = 0; i < n; i++) { heap.add(new int[] { 0, arr[i] }); } for (int i = 0; i < m; i++) { int[] cur = heap.poll(); cur[0] += cur[1]; heap.add(cur); } return heap.peek()[0]; } // 对数器测试 public static void main(String[] args) { System.out.println("测试开始"); int N = 50; int V = 30; int M = 3000; int testTime = 20000; for (int i = 0; i < testTime; i++) { int n = (int) (Math.random() * N) + 1; int[] arr = randomArray(n, V); int m = (int) (Math.random() * M); int ans1 = waitingTime1(arr, m); int ans2 = waitingTime2(arr, m); if (ans1 != ans2) { System.out.println("出错了!"); } } System.out.println("测试结束"); } // 对数器测试 public static int[] randomArray(int n, int v) { int[] arr = new int[n]; for (int i = 0; i < n; i++) { arr[i] = (int) (Math.random() * v) + 1; } return arr; }
大厂真题
// 刀砍毒杀怪兽问题 // 怪兽的初始血量是一个整数hp,给出每一回合刀砍和毒杀的数值cuts和poisons // 第i回合如果用刀砍,怪兽在这回合会直接损失cuts[i]的血,不再有后续效果 // 第i回合如果用毒杀,怪兽在这回合不会损失血量,但是之后每回合都损失poisons[i]的血量 // 并且你选择的所有毒杀效果,在之后的回合都会叠加 // 两个数组cuts、poisons,长度都是n,代表你一共可以进行n回合 // 每一回合你只能选择刀砍或者毒杀中的一个动作 // 如果你在n个回合内没有直接杀死怪兽,意味着你已经无法有新的行动了 // 但是怪兽如果有中毒效果的话,那么怪兽依然会在血量耗尽的那回合死掉 // 返回至少多少回合,怪兽会死掉 // 数据范围 : // 1 <= n <= 10^5 // 1 <= hp <= 10^9 // 1 <= cuts[i]、poisons[i] <= 10^9 // 本题来自真实大厂笔试,找不到测试链接,所以用对数器验证 // 动态规划方法(只是为了验证) // 目前没有讲动态规划,所以不需要理解这个函数 // 这个函数只是为了验证二分答案的方法是否正确的 // 纯粹为了写对数器验证才设计的方法,血量比较大的时候会超时 // 这个方法不做要求,此时并不需要理解,可以在学习完动态规划章节之后来看看这个函数 public static int fast1(int[] cuts, int[] poisons, int hp) { int sum = 0; for (int num : poisons) { sum += num; } int[][][] dp = new int[cuts.length][hp + 1][sum + 1]; return f1(cuts, poisons, 0, hp, 0, dp); } // 不做要求 public static int f1(int[] cuts, int[] poisons, int i, int r, int p, int[][][] dp) { r -= p; if (r <= 0) { return i + 1; } if (i == cuts.length) { if (p == 0) { return Integer.MAX_VALUE; } else { return cuts.length + 1 + (r + p - 1) / p; } } if (dp[i][r][p] != 0) { return dp[i][r][p]; } int p1 = r <= cuts[i] ? (i + 1) : f1(cuts, poisons, i + 1, r - cuts[i], p, dp); int p2 = f1(cuts, poisons, i + 1, r, p + poisons[i], dp); int ans = Math.min(p1, p2); dp[i][r][p] = ans; return ans; } // 二分答案法 // 最优解 // 时间复杂度O(n * log(hp)),额外空间复杂度O(1) public static int fast2(int[] cuts, int[] poisons, int hp) { int ans = Integer.MAX_VALUE; int r = hp+1; int l = 0; while (l<=r){ int mid = l+((r-l)>>1); if(f(cuts,poisons,hp,mid)){ ans = mid; r = mid-1; }else { l = mid+1; } } return ans; } // cuts、posions,每一回合刀砍、毒杀的效果 // hp:怪兽血量 // limit:回合的限制 public static boolean f(int []cuts,int []poisons,int hp,int limit) { int n = Math.min(cuts.length, limit); for (int i = 0, j = 1; i < n; i++, j++) { int poison = (limit - i-1) * poisons[i]; if (cuts[i]>poison){ hp-=cuts[i]; }else{ hp -=poison; } if (hp <= 0) { return true; } } return false; } // 对数器测试 public static void main(String[] args) { // 随机测试的数据量不大 // 因为数据量大了,fast1方法会超时 // 所以在数据量不大的情况下,验证fast2方法功能正确即可 // fast2方法在大数据量的情况下一定也能通过 // 因为时间复杂度就是最优的 System.out.println("测试开始"); int N = 30; int V = 20; int H = 300; int testTimes = 10000; for (int i = 0; i < testTimes; i++) { int n = (int) (Math.random() * N) + 1; int[] cuts = randomArray(n, V); int[] posions = randomArray(n, V); int hp = (int) (Math.random() * H) + 1; int ans1 = fast1(cuts, posions, hp); int ans2 = fast2(cuts, posions, hp); if (ans1 != ans2) { System.out.println("出错了!"); } } System.out.println("测试结束"); } // 对数器测试 public static int[] randomArray(int n, int v) { int[] ans = new int[n]; for (int i = 0; i < n; i++) { ans[i] = (int) (Math.random() * v) + 1; } return ans; }