题目列表
复习
4. 寻找两个正序数组的中位数
33. 搜索旋转排序数组
34. 在排序数组中查找元素的第一个和最后一个位置
35. 搜索插入位置
69. x 的平方根
167. 两数之和 II - 输入有序数组
209. 长度最小的子数组
222. 完全二叉树的节点个数
287. 寻找重复数
300. 最长递增子序列
350. 两个数组的交集 II
704. 二分查找
718. 最长重复子数组
887. 鸡蛋掉落
练习
2023.04.14
4. 寻找两个正序数组的中位数
题目
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
提示:
- nums1.length == m
- nums2.length == n
- 0 <= m <= 1000
- 0 <= n <= 1000
- 1 <= m + n <= 2000
- -106 <= nums1[i], nums2[i] <= 106
思路
- 总元素个数如果是奇数
- 每一个数组都找中间的指针
- 如果对于这俩指针, 左右恰巧数量一样,就返回这俩指针对应的元素(奇数返回小的,偶数返回两个的平均)
- a[0], …, a[i],…a[m-1],b[0],…,b[j],…b[n-1],左右一样指a[0]…a[i]元素个数加上b[0]…b[j]元素个数和剩余部分一致,并且a[i-1]<b[j]<a[i+1] && b[j-1] < a[i] < b[j+1],即a[i]的坐边小于b[j]、b[j]的左边小于a[i]、a[i]的右边大于b[j]、b[j]的右边大于a[i]
- 否则使用二分查找其中一个,同时调整另一个元素的个数,始终保持,左右元素相等,直到满足上述第二个条件
答案
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
if (m > n) {
// 从短的做遍历。为了避免处理nums2的边界
return findMedianSortedArrays(nums2, nums1);
}
int halfNum = (m + n + 1) / 2;
int left = 0, right = m;
int leftMax = 0, rightMin = 0;
while (left <= right) {
// 防溢出
int middle1 = left + (right - left) / 2;
int middle2 = halfNum - middle1;
// i_1要处理左溢出即==0的判断,i本身要处理右溢出即==m
// MAX_VALUE还是MIN_VALUE是从max还是min赋值的,反正就是取合法的那边
int ai_1 = middle1 == 0 ? Integer.MIN_VALUE : nums1[middle1 - 1];
int ai = middle1 == m ? Integer.MAX_VALUE : nums1[middle1];
int bj_1 = middle2 == 0 ? Integer.MIN_VALUE : nums2[middle2 - 1];
int bj = middle2 == n ? Integer.MAX_VALUE : nums2[middle2];
// 满足条件就记录
if (ai_1 <= bj) {
leftMax = Math.max(ai_1, bj_1);
rightMin = Math.min(ai, bj);
left = middle1 + 1;
} else {
right = middle1 - 1;
}
}
// 满足条件才更新,所以直接返回
return ((m + n) % 2 == 0) ? (leftMax + rightMin) / 2.0 : leftMax;
}
}
33. 搜索旋转排序数组
题目
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
提示:
- 1 <= nums.length <= 5000
- -104 <= nums[i] <= 104
- nums 中的每个值都 独一无二
- 题目数据保证 nums 在预先未知的某个下标上进行了旋转
- -104 <= target <= 104
思路
核心是找到在哪边搜索
- 先判断哪边是有序的
- 如果目标值在有序的这边,就在这边,否则在另一边
答案
class Solution {
public int search(int[] nums, int target) {
return search(nums, target, 0, nums.length - 1);
}
public int search(int[] nums, int target, int left, int right) {
// 递归的边界
if (left > right) {
return -1;
}
int middle = left + (right - left) / 2;
int value = nums[middle];
// 递归的终止条件
if (value == target) {
return middle;
}
// 左边有序
if (nums[left] <= value) {
// 因为左边有序,所以判断在不在左边范围内,在的话就在左边
if (target < value && nums[left] <= target) {
return search(nums, target, left, middle - 1);
} else {
return search(nums, target, middle + 1, right);
}
} else {
// 右边同理
if (target > value && target <= nums[right]) {
return search(nums, target, middle + 1, right);
} else {
return search(nums, target, left, middle - 1);
}
}
}
}
34. 在排序数组中查找元素的第一个和最后一个位置
题目
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
提示:
- 0 <= nums.length <= 105
- -109 <= nums[i] <= 109
- nums 是一个非递减数组
- -109 <= target <= 109
思路
和普通二分的区别是搜索到之后的动作
- 搜索最左边的话,就一直把右边界往左边移动。
- 右边同理,详见注释
答案
class Solution {
public int[] searchRange(int[] nums, int target) {
if (nums.length == 0) {
return new int[] { -1, -1 };
}
return new int[] { lBsearch(nums, target, 0, nums.length - 1), rBsearch(nums, target, 0, nums.length - 1) };
}
public int lBsearch(int[] nums, int target, int left, int right) {
// 因为左右都是闭区间,所以相等
while (left <= right) {
int middle = left + (right - left) / 2;
int value = nums[middle];
if (value < target) {
left = middle + 1;
} else {
// 相等就移动右边界
right = middle - 1;
}
}
// 考虑剩两个元素的时候,[m, target],这时候middle==left, m < target,最后右移一步。所以最后,如果有left一定指向元素
// 考虑剩两个元素的时候,[target, target],这时候middle==left, 最后左移一步。所以最后,如果有left一定指向元素
// 一些边界判断
return left >= 0 && left < nums.length && nums[left] == target ? left : -1;
}
// 同搜索左边界
public int rBsearch(int[] nums, int target, int left, int right) {
while (left <= right) {
int middle = left + (right - left) / 2;
int value = nums[middle];
if (value <= target) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return right >= 0 && right < nums.length && nums[right] == target ? right : -1;
}
}
2024.04.19
35. 搜索插入位置
题目
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
提示:
- 1 <= nums.length <= 104
- -104 <= nums[i] <= 104
- nums 为 无重复元素 的 升序 排列数组
- -104 <= target <= 104
思路
比二分查找,要多考虑下,最后left和right的位置关系
考虑两个元素的时候[m, n], m < target < n, nums[middle] = m < target,移动right,left和right重合在m,最后一次,right-1,left不动
答案
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 闭区间,所以=
while (left <= right) {
int middle = left + (right - left) / 2;
int value = nums[middle];
if (value == target) {
return middle;
} else if (value < target) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return left;
}
}
69. x 的平方根
题目
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842…, 由于返回类型是整数,小数部分将被舍去。
提示:
- 0 <= x <= 2^31 - 1
思路
0-x一半一半逼近,因为1 = 1 x 1, 所以x是闭区间
答案
class Solution {
public int mySqrt(int x) {
// 边界0,1
if (x < 2) {
return x;
}
int left = 0, right = x;
while (left <= right) {
int middle = left + (right - left) / 2;
if (x / middle == middle) {
return middle;
} else if (x / middle < middle) {
right = middle - 1;
} else {
left = middle + 1;
}
}
// 2.8取2,所以-1
return left - 1;
}
}
167. 两数之和 II - 输入有序数组
题目
给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。
以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。
示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]
解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
提示:
- 2 <= numbers.length <= 3 * 104
- -1000 <= numbers[i] <= 1000
- numbers 按 非递减顺序 排列
- -1000 <= target <= 1000
- 仅存在一个有效答案
思路
因为有序,固定一个,另一个用二分
答案
// 二分,O(n log(n))
class Solution {
public int[] twoSum(int[] numbers, int target) {
for (int i = 0; i < numbers.length; i++) {
int current = numbers[i];
int left = i + 1;
int right = numbers.length - 1;
while (left <= right) {
int middle = left + (right - left) / 2;
if (current + numbers[middle] == target) {
return new int[] { i + 1, middle + 1 };
} else if (current + numbers[middle] < target) {
left = middle + 1;
} else {
right = middle - 1;
}
}
}
return new int[] { -1, -1 };
}
}
// 双指针 O(n)
class Solution {
public int[] twoSum(int[] numbers, int target) {
int left = 0, right = numbers.length - 1;
while (left < right) {
int leftValue = numbers[left];
int rightValue = numbers[right];
if (leftValue + rightValue == target) {
return new int[] { left + 1, right + 1 };
} else if (leftValue + rightValue < target) {
left++;
} else {
right--;
}
}
return new int[] { -1, -1 };
}
}
209. 长度最小的子数组
题目
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 连续
子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
- 1 <= target <= 109
- 1 <= nums.length <= 105
- 1 <= nums[i] <= 105
进阶:
- 如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。
思路
- 滑动窗口
- 前缀和+二分搜索。二分一定要有序,前缀和可以做到
答案
// 滑动窗口
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int result = Integer.MAX_VALUE;
int left = 0, right = 0;
int sum = 0;
while (right < nums.length) {
sum += nums[right];
while (sum >= target && left <= right) {
result = Math.min(result, right - left + 1);
sum -= nums[left++];
}
right++;
}
return result == Integer.MAX_VALUE ? 0 : result;
}
}
// 前缀和+二分
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int[] sums = new int[nums.length + 1];
int sum = 0;
for (int i = 1; i < sums.length; i++) {
sum += nums[i - 1];
sums[i] = sum;
}
int result = Integer.MAX_VALUE;
for (int i = 0; i < sums.length; i++) {
int current = sums[i];
int left = i + 1, right = sums.length - 1;
while (left <= right) {
int middle = left + (right - left) / 2;
int value = sums[middle];
if (value - current >= target) {
result = Math.min(result, middle - i);
right = middle - 1;
} else {
left = middle + 1;
}
}
}
return result == Integer.MAX_VALUE ? 0 : result;
}
}
222. 完全二叉树的节点个数
题目
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。
示例 1:
输入:root = [1,2,3,4,5,6]
输出:6
示例 2:
输入:root = []
输出:0
示例 3:
输入:root = [1]
输出:1
提示:
- 树中节点的数目范围是[0, 5 * 104]
- 0 <= Node.val <= 5 * 104
- 题目数据保证输入的树是 完全二叉树
进阶: 遍历树来统计节点是一种时间复杂度为 O(n) 的简单解决方案。你可以设计一个更快的算法吗?
思路
- 因为是完全二叉树
- 求左右树的高度,如果相等,则说明左子树是完全二叉树,左子树元素个数是2左子树的高度-1,再加上根节点,就是2左子树的高度
- 如果不相等,说明右子树是完全二叉树,算法同2
答案
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
int left = high(root.left);
int right = high(root.right);
if (left == right) {
// 左子树是完全二叉树,左子树加右边的数量
return countNodes(root.right) + (1 << left);
} else {
// 右子树是完全二叉树,右子树加左边的数量
return countNodes(root.left) + (1 << right);
}
}
public int high(TreeNode root) {
int result = 0;
while (root != null) {
result++;
root = root.left;
}
return result;
}
}
287. 寻找重复数
题目
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
示例 1:
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
输入:nums = [3,1,3,4,2]
输出:3
示例 3 :
输入:nums = [3,3,3,3,3]
输出:3
提示:
- 1 <= n <= 105
- nums.length == n + 1
- 1 <= nums[i] <= n
- nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次
进阶:
- 如何证明 nums 中至少存在一个重复的数字?
- 你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?
思路
之前双指针有过,这次换个思路
要二分,必须有单调数组,有点像定义DP数组了、本次定义cnt[i]表示小于等于i的元素数量,有点像前缀和的逻辑,记住吧。数组构造单调数组有两个办法:前缀和、cnt[i]
如果数组中只有一个重复元素,根据题意 nums.length == n + 1 、1 <= nums[i] <= n,其实必然重复(n+1个位置,只有n个元素)。则重复元素之前,cnt[i] == i,[1,2,3,4,5,6,5,7,8],这种cnt[0-4] = [0-4],因为可能重复多个,所以cnt[i] <= i
用二分找到这个就行了,就是哪个位置开始出现不小于等于
答案
class Solution {
public int findDuplicate(int[] nums) {
int left = 0, right = nums.length - 1;
int result = 0;
// 找数,数在0-n之间(根据题意nums.length==n+1),搜索的是小于等于xxx
while (left <= right) {
// 二分找数
int middle = left + (right - left) / 2;
// 计算cnt
int cnt = 0;
for (int i = 0; i < nums.length; i++) {
// 遍历数组,小于等于middle的数个数
if (nums[i] <= middle) {
cnt++;
}
}
// 是不是满足条件,满足说明数小了
if (cnt <= middle) {
left = middle + 1;
} else {
// 记录下,因为可能是大于target的,所以继续搜索
// [1,2,3,4,5,6,5,7,8],cnt[6] = 7, cnt[5] = 6, cnt[8] = 8,都满足条件,所以继续搜索
result = middle;
right = middle - 1;
}
}
return result;
}
}
2024.04.21
300. 最长递增子序列
题目
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的
子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
- 1 <= nums.length <= 2500
- -104 <= nums[i] <= 104
进阶:
- 你能将算法的时间复杂度降低到 O(n log(n)) 吗?
思路
- 动态规划
- 二分
答案
class Solution {
public int lengthOfLIS(int[] nums) {
// 以i结尾的长度
int[] dp = new int[nums.length];
dp[0] = 1;
// 因为最长的不一定以最后一个结尾,所以记录下,最小长度是1(题目条件,数组一定有元素)
int result = 1;
for (int i = 1; i < dp.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
// 如果当前元素比之前的大,就满足,就上一个+1,但是不一定是最大的,所以要遍历
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 更新结果
result = Math.max(result, dp[i]);
}
return result;
}
}
class Solution {
// 二分 + 贪心
// 1. 升的越慢升的越久(贪心),为什么可以替换,比如到了3,前面的5已经遍历过了,后面如果有数字,大于3的概率高于大于5(多了个4)
// 2. 二分之后,左右都落到最新节点位置
// 3. result 取最大
public int lengthOfLIS(int[] nums) {
// 维护这个【严格】递增的数组
int[] tail = new int[nums.length];
int result = 0;
for (int num : nums) {
int left = 0, right = result;
// 退出条件是重合,这个是因为最后要left落到第一个大于num的位置上,而且可能有重复
while (left < right) {
int middle = (right + left) / 2;
// 因为有重复
if (tail[middle] >= num) {
right = middle;
} else {
left = middle + 1;
}
}
// 要让最后left落到第一个大于num的数上
tail[left] = num;
// 下标和长度的转化,nums.length是长度,但是最大下标是nums.length - 1
result = Math.max(result, right + 1);
}
return result;
}
}
350. 两个数组的交集 II
题目
给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]
提示:
- 1 <= nums1.length, nums2.length <= 1000
- 0 <= nums1[i], nums2[i] <= 1000
进阶:
- 如果给定的数组已经排好序呢?你将如何优化你的算法?
- 如果 nums1 的大小比 nums2 小,哪种方法更优?
- 如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
思路
- 排序+双指针
- 排序后,拿少的,去多的里面二分(m logn)
答案
// 双指针
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
sorted(nums1, 0, nums1.length - 1);
sorted(nums2, 0, nums2.length - 1);
int current1 = 0, current2 = 0, resultIndex = 0;
int[] result = new int[nums2.length];
while (current1 < nums1.length && current2 < nums2.length) {
// 相同,就赋值,谁小谁去追
if (nums1[current1] == nums2[current2]) {
result[resultIndex++] = nums1[current1];
current1++;
current2++;
} else if (nums1[current1] < nums2[current2]) {
current1++;
} else {
current2++;
}
}
return Arrays.copyOf(result, resultIndex);
}
// 老规矩,练一下快排
public void sorted(int[] nums, int start, int end) {
if (start >= end) {
return;
}
int min = start, mid = min + 1, max = end;
int target = nums[min];
while (mid <= max) {
int element = nums[mid];
if (element < target) {
swap(nums, min++, mid++);
} else if (element > target) {
swap(nums, mid, max--);
} else {
mid++;
}
}
sorted(nums, start, min - 1);
sorted(nums, max + 1, end);
}
public void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
}
// 二分查找
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
// m小于n,否则互换
if (m > n) {
return intersect(nums2, nums1);
}
sorted(nums1, 0, nums1.length - 1);
sorted(nums2, 0, nums2.length - 1);
int[] result = new int[n];
int index = 0;
int left = 0;
for (int i = 0; i < m; i++) {
int right = n - 1;
int target = nums1[i];
// 因为两个都排序了,left可以只往后挪一下(避免重复)
while (left <= right) {
int middle = (left + right) / 2;
int element = nums2[middle];
// 找到最左边
if (element < target) {
left = middle + 1;
} else if (element >= target) {
right = middle - 1;
}
}
// 看看找没找到,left < n 边界
if (left < n && nums2[left] == target) {
result[index++] = target;
// 往后串一下,避免[1,1,2], [1]出现[1, 1]。应该是[1]
left++;
}
}
return Arrays.copyOf(result, index);
}
public void sorted(int[] nums, int start, int end) {
if (start >= end) {
return;
}
int min = start, mid = min + 1, max = end;
int target = nums[min];
while (mid <= max) {
int element = nums[mid];
if (element < target) {
swap(nums, min++, mid++);
} else if (element > target) {
swap(nums, mid, max--);
} else {
mid++;
}
}
sorted(nums, start, min - 1);
sorted(nums, max + 1, end);
}
public void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
}
704. 二分查找
题目
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
- 你可以假设 nums 中的所有元素是不重复的。
- n 将在 [1, 10000]之间。
- nums 的每个元素都将在 [-9999, 9999]之间。
思路
标准二分查找
答案
// 标准二分
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int middle = (left + right) / 2;
int element = nums[middle];
if (target == element) {
return middle;
} else if (target > element) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return -1;
}
}
// 二分重复左边界
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int middle = left + (right - left) / 2;
int element = nums[middle];
if (element >= target) {
right = middle - 1;
} else {
left = middle + 1;
}
}
return left >= 0 && left < nums.length && nums[left] == target ? left : -1;
}
}
// 二分重复右边界
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int middle = left + (right - left) / 2;
int element = nums[middle];
if (element > target) {
right = middle - 1;
} else {
left = middle + 1;
}
}
return right >= 0 && right < nums.length && nums[right] == target ? right : -1;
}
}
718. 最长重复子数组
题目
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5
提示:
- 1 <= nums1.length, nums2.length <= 1000
- 0 <= nums1[i], nums2[i] <= 100
思路
- 动态规划
- 滑窗
- 二分查找
答案
// 动态规划
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int[][] dp = new int[m][n];
int result = 0;
for (int i = 0; i < m; i++) {
dp[i][0] = nums1[i] == nums2[0] ? 1 : 0;
result = Math.max(result, dp[i][0]);
}
for (int i = 0; i < n; i++) {
dp[0][i] = nums1[0] == nums2[i] ? 1 : 0;
result = Math.max(result, dp[0][i]);
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (nums1[i] == nums2[j]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
result = Math.max(result, dp[i][j]);
}
}
return result;
}
}
// 滑动窗口
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
// 确保上面大,下面小
if (m < n) {
return findLength(nums2, nums1);
}
int max = 0;
// 以nums1开头的时候
for (int l = 1; l < n; l++) {
max = Math.max(max, maxLength(nums1, 0, nums2, n - l, l));
}
// 完全重叠的时候
for (int i = 0; i < m - n; i++) {
max = Math.max(max, maxLength(nums1, i, nums2, 0, n));
}
// 错开的时候,以nums2开头的时候
for (int l = n; l > 0; l--) {
max = Math.max(max, maxLength(nums1, m - l, nums2, 0, l));
}
return max;
}
public int maxLength(int[] nums1, int nums1StartIndex, int[] nums2, int nums2StartIndex, int length) {
int count = 0, max = 0;
for (int i = 1; i <= length; i++) {
if (nums1[nums1StartIndex++] == nums2[nums2StartIndex++]) {
count++;
} else {
max = Math.max(max, count);
count = 0;
}
}
max = Math.max(max, count);
return max;
}
}
// 二分,这里复制下官方答案,hash过程比较难理解,感觉必要性也不是很大。主要说下二分过程
class Solution {
int mod = 1000000009;
int base = 113;
public int findLength(int[] A, int[] B) {
// 为什么可以二分?前提是如果存在长度为k的公共子数组,就一定存在长度小于k的公共子数组,[0, k]具有相同的性质
int left = 1, right = Math.min(A.length, B.length) + 1;
while (left <= right) {
int mid = (left + right) >> 1;
// 如果找到了,就继续向右找,相当于找右边界
if (check(A, B, mid)) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left - 1;
}
public boolean check(int[] A, int[] B, int len) {
long hashA = 0;
for (int i = 0; i < len; i++) {
hashA = (hashA * base + A[i]) % mod;
}
Set<Long> bucketA = new HashSet<Long>();
bucketA.add(hashA);
long mult = qPow(base, len - 1);
for (int i = len; i < A.length; i++) {
hashA = ((hashA - A[i - len] * mult % mod + mod) % mod * base + A[i]) % mod;
bucketA.add(hashA);
}
long hashB = 0;
for (int i = 0; i < len; i++) {
hashB = (hashB * base + B[i]) % mod;
}
if (bucketA.contains(hashB)) {
return true;
}
for (int i = len; i < B.length; i++) {
hashB = ((hashB - B[i - len] * mult % mod + mod) % mod * base + B[i]) % mod;
if (bucketA.contains(hashB)) {
return true;
}
}
return false;
}
// 使用快速幂计算 x^n % mod 的值
public long qPow(long x, long n) {
long ret = 1;
while (n != 0) {
if ((n & 1) != 0) {
ret = ret * x % mod;
}
x = x * x % mod;
n >>= 1;
}
return ret;
}
}
887. 鸡蛋掉落
题目
给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
示例 1:
输入:k = 1, n = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1 。
如果它没碎,那么肯定能得出 f = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 f 是多少。
示例 2:
输入:k = 2, n = 6
输出:3
示例 3:
输入:k = 3, n = 14
输出:4
提示:
- 1 <= k <= 100
- 1 <= n <= 104
答案
class Solution {
public int superEggDrop(int k, int n) {
int[][] dp = new int[k + 1][n + 1];
for (int i = 0; i <= n; i++) {
dp[1][i] = i;
}
for (int i = 0; i <= k; i++) {
dp[i][0] = 0;
dp[i][1] = 1;
}
for (int i = 2; i <= k; i++) {
for (int j = 2; j <= n; j++) {
int left = 0;
int right = j;
// 代等号的二分,最终left落在要找点左边
while (left <= right) {
int middle = (left + right + 1) / 2;
int broken = dp[i - 1][middle - 1];
int unBroken = dp[i][j - middle];
if (broken > unBroken) {
right = middle - 1;
} else {
left = middle + 1;
}
}
//
dp[i][j] = Math.max(dp[i - 1][left - 2], dp[i][j - left + 1]) + 1;
}
}
return dp[k][n];
}
}