查找
1.普通查找
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3限制:
2 <= n <= 100000
class Solution {
public int findRepeatNumber1(int[] nums) {
// 暴力搜索
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] == nums[j]) {
return nums[i];
}
}
}
return -1;
}
public int findRepeatNumber(int[] nums) {
// hashSet:存储无序的,不重复的元素
Set<Integer> set = new HashSet<>();
for (int num : nums) {
if (set.contains(num)) {
return num;
}
set.add(num);
}
return -1;
}
public int findRepeatNumber(int[] nums) {
// 长度为n的数组中元素取值是0~n-1,则索引与值的对应关系是一对多的关系
// 如果题目要求空间复杂度是O(1),则需要使用原地交换
int i = 0;
while (i < nums.length) {
if (nums[i] == i) {// 只有当nums[i] == i时我们才i++,这样可以避免漏掉的元素
i++;
continue;
}
if (nums[i] == nums[nums[i]]) { // 找到相同的元素
return nums[i];
}
// 否则交换nums[i]与nums[nums[i]]
int tmp = nums[i];
nums[i] = nums[tmp];
nums[tmp] = tmp;
}
return -1;
}
}
2.二分查找
2.1 什么时候用
排序数组的搜索问题,首先想到二分查找
- 当题目数组是单调数组时,考虑用二分查找;
- 当题目要求时间复杂度是logN时,考虑用二分查找;
- 当题目求最小最大值时
2.2 模板
// 模板一:二分查找某个数,返回index
public int binarySearch(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) {
return mid;
}
}
return -1;
}
// 模板二:二分查找左边界
public int searchLeft(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 {
right = mid - 1;
}
}
return left;
}
// 结果分析,对于nums = {5,7,7,8,8,10},target = 8,此时left = 3;如果target = 9,此时left = 5
// 即:如果nums包含target,则返回第一个target的下标,如果不包含target,则返回第一个大于target的下标
// 如果题目要求如果不包含target则返回-1,只需要添加如下判断就行
public int searchLeft(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 {
right = mid - 1;
}
}
if (left >= nums.length || nums[left] != target) return -1;
return left;
}
// 模板三:二分查找右边界
public int searchRight(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return right;
}
// 结果分析,对于nums = {5,7,7,8,8,10},target = 8,此时right = 4;如果target = 9,此时left = 4
// 即:如果nums包含target,则返回最后一个target的下标,如果不包含target,则返回第一个小于target的下标
// 如果题目要求如果不包含target则返回-1,只需要添加如下判断就行
public int searchRight(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
if (right < 0 || nums[right] != target) return -1;
return right;
}
口诀:移右确左,移左确右
通用模板:
1.初始化:左边界i = 0,右边界j = nums.length - 1。
2.循环二分:当闭区间[i, j]无元素时跳出;
- 计算中点m = (i + j) / 2(向下取整);
- 若nums[m] < target,则target在闭区间[m+1, j]中,因此执行i = m + 1;
- 若nums[m] > target,则target在闭区间[i, m - 1]中,因此执行j = m -1;
- 若nums[m] = target,则右边界right在闭区间[m + 1, j],左边界在区间[i, m - 1]中,因此分为一下两种情况:
- 若查找右边界,则执行i = m + 1;(跳出时i指向右边界)
- 若查找左边界,则执行j = m - 1;(跳出时j指向左边界)
2.3 题目一
统计一个数字在排序数组中出现的次数。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:输入: nums = [5,7,7,8,8,10], target = 6
输出: 0提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109
class Solution {
public int search(int[] nums, int target) {
// 解法一:暴力搜索
if (nums.length == 0 || target < nums[0] || target > nums[nums.length - 1]) return 0;
int res = 0;
for (int num : nums) {
if (num == target) {
res++;
}
}
return res;
}
public int search(int[] nums, int target) {
// 解法一:二分查找,重复出现的次数 = right - left -1
if (nums.length == 0 || target < nums[0] || target > nums[nums.length - 1]) return 0;
// 1.查找右边界
int i = 0;
int j = nums.length - 1;
while (i <= j) {
int m = (i + j) / 2;
if (nums[m] <= target) {
i = m + 1;
} else {
j = m - 1;
}
}
int right = i;
if (j > 0 && nums[j] != target) return 0;
// 2.查找左边界
i = 0;
j = nums.length - 1;
while (i <= j) {
int m = (i + j) / 2;
if (nums[m] < target) {
i = m + 1;
} else {
j = m - 1;
}
}
int left = j;
return right - left - 1;
}
public int search(int[] nums, int target) {
// 解法三:查找左边界可以变换为查找target - 1的右边界
return searchLeft(nums, target) - searchLeft(nums, target - 1);
}
private int searchLeft(int[] nums, int target) {
int i = 0;
int j = nums.length - 1;
while (i <= j) {
int m = (i + j) / 2;
if (nums[m] <= target) {
i = m + 1;
} else {
j = m - 1;
}
}
return i;
}
}
步骤:
- 确定x,f(x)和target分别是什么,并写出函数f的代码;
- 找到x的取值范围作为二分搜索的搜索区间,并初始化left和right变量;
- 根据题目的要求,确定是搜索左右区间。
2.4 题目二
珂珂喜欢吃香蕉。这里有 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提示:
1 <= piles.length <= 10^4
piles.length <= H <= 10^9
1 <= piles[i] <= 10^9
class Solution {
public int minEatingSpeed(int[] piles, int h) {
// x是吃香蕉的速度,f(x)是吃掉香蕉的时间,f(x)是单调递减的,target是时间H
int left = 1, right = 1000000000;
while (left <= right) {
int mid = left + (right - left) / 2;
if (hours(piles, mid) <= h) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
private int hours(int[] piles, int speed) {
int hour = 0;
for (int i = 0; i < piles.length; i++) {
hour += (piles[i] % speed) > 0 ? ((piles[i] / speed) + 1) : (piles[i] / speed);
}
return hour;
}
}
2.5 题目三
传送带上的包裹必须在 days 天内从一个港口运送到另一个港口。
传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量(weights)的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。
返回能在 days 天内将传送带上的所有包裹送达的船的最低运载能力。
示例 1:
输入:weights = [1,2,3,4,5,6,7,8,9,10], days = 5
输出:15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天:10
请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。
示例 2:
输入:weights = [3,2,2,4,1,4], days = 3
输出:6
解释:
船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
第 1 天:3, 2
第 2 天:2, 4
第 3 天:1, 4
示例 3:
输入:weights = [1,2,3,1,1], D = 4
输出:3
解释:
第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1
提示:
1 <= days <= weights.length <= 5 * 104
1 <= weights[i] <= 500
class Solution {
public int shipWithinDays(int[] weights, int days) {
int left = Arrays.stream(weights).max().getAsInt();
int right = Arrays.stream(weights).sum();
while (left <= right) {
int mid = left + (right - left) / 2;
if (days(weights, mid) <= days) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
private int days(int[] weights, int capacity) {
int day = 0;
int curWeight = 0;
for (int i = 0; i < weights.length; i++) {
curWeight += weights[i];
if (curWeight > capacity) {
day += 1;
curWeight = weights[i];
}
}
return day + 1;// 遍历到最后curWeight <= capacity要单独算一天
}
}