二、二分查找(简)
题目
给定一个 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]之间。
题解
在升序数组nums中寻找目标值target,对于特定下标i,比较nums[i]和target的大小:
- 如果nums[i] = target,则下标i即为要寻找的下标;
- 如果nums[i] > target,则target在下标i左侧;
- 如果nums[i] < target,则target在下标i右侧;
所以,二分查找的做法是,
1、定义查找的范围[left,right],初始查找范围是整个数组;
2、每次取查找范围的中点mid,比较nums[mid]和target的大小;
3、如果相等则mid即为要寻找的下标,如果不相等则根据nums[mid]和target的大小关系将查找范围缩小一半。
1 | class Solution { |
二、数组中的第K个最大元素(困)
题目
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
提示:
1 <= k <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
题解1
最简单的,先排序,再取第k个出来,排序的话就用快排,快排也有很多种,下面是双指针法
1 | class Solution { |
题解2
既然都想到排序了,何不用大根堆来实现。而且C++自带的优先队列就能实现大根堆,自动帮我们排序
首先回顾下优先队列priority_queue
普通队列是先进先出;而优先队列中具有最高优先级的元素将被首先删除
如下所示,升序队列从大到小排列,top()返回的是最小值,即小根堆;降序队列从小到大排列,top()返回的是最大值,即大根堆
1 | // 升序队列 |
这道题就可以直接用降序队列,把nums存进队列中,让它自个排序,再取第k个就行
1 | class Solution { |
题解3
面试官肯定不希望你调库,所以自己实现大根堆才是加分项
首先要回顾下堆排序,忘记了就回去看这里
大根堆就是根节点一定大过左右节点的值。
这样我们就可以根据给定的数组,创建一个大根堆,让最大的值在栈顶,然后要找第k个最大值,就把堆顶的值和最末尾的子节点互换,并忽略该子节点,重新生成个大根堆,这样堆顶元素就是要求的值。
1 | class Solution { |
三、搜索旋转排序数组(中)
题目
整数数组 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
-10^4 <= nums[i] <= 10^4
nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-10^4 <= target <= 10^4
题解
首先要明白这道题问的是啥,很简单就是从给定的数组中找出target
的下标。
那为啥题目描述的花里胡哨的,还有什么旋转呢,主要是怕你想不到,因为要设计O(log n)
的算法,单纯暴力搜索肯定是不行的。
那是不是可以用二分法呢?严格来说,标准的二分法只适用于有序数组,但题目给我们的是旋转后的数组,它不是有序的,所以需要对二分法进行修改变形。
这个旋转后的数组有什么特点呢?可以发现的是,我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。
所以我们在常规二分查找的时候,可以查看当前mid
为分割位置分割出来的两个部分[left, mid]
和[mid+1, right]
哪个部分是有序的,根据有序的那部分判断出target
在不在这个部分。
所以我们在用二分法时有两种情况:
1、若nums[0] ≤ nums[mid]
,说明[left, mid]
是有序的;
- 则当
nums[0] ≤ target < nums[mid]
时target
在[left, mid-1]
中, - 否则在
[mid+1, right]
中;
2、若nums[0] > nums[mid]
,说明[mid+1, right]
是有序的;
- 则当
nums[mid] < target ≤ nums[size-1]
时target
在[mid+1, right]
中, - 否则在
[left, mid-1]
中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40class Solution {
public:
int search(vector<int>& nums, int target) {
if (nums.empty()) {
return -1;
}
int n_size = nums.size();
int left = 0;
int right = n_size - 1;
int res = -1;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
res = mid;
break;
}
if (nums[0] <= nums[mid]) {
if (nums[0] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[n_size - 1]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return res;
}
};
int main() {
Solution s;
vector<int> nums = {4, 5, 6, 7, 0, 1, 2};
cout << s.search(nums, 0);
}
四、搜索旋转排序数组 II(中)
题目
已知存在一个按非降序排列的整数数组 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,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
你必须尽可能减少整个操作步骤。
示例 1:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
示例 2:
输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false
提示:
题解
和上一题一样,只不过因为数组中有重复元素,二分查找时可能会有num[left] = num[mid] = num[right]
。
比如数组[3,1,2,3,3,3,3]
,target = 2
,首次二分时无法判断区间[0, 3]
和[4, 6]
哪个是有序的。
对于这种情况,我们只能将当前二分区间的左边界加一,右边界减一,然后在新区间上继续二分查找。其余的照抄上一题。
1 | class Solution { |
五、寻找旋转排序数组中的最小值(中)
题目
已知一个长度为 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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
提示:
题解
首先对于这个旋转数组,它的特点是:
1、以最小值为分界线的左子序列是单调递增的,右子序列也是单调递增的;
2、对于数组中最后一个元素x
,左子序列全大于x
,右子序列全小于x
。
通过nums[mid]
和nums[right]
比较,判断nums[mid]
在左子序列还是右子序列。
有两种情况:
1、如果nums[mid] < nums[right]
,则mid
在右子序列,即在最小值右边,所以right
左移;
2、如果nums[mid] > nums[right]
,则mid
在左子序列,即在最小值左边,所以left
右移。
1 | class Solution { |
六、寻找旋转排序数组中的最小值 II(困)
题目
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须尽可能减少整个过程的操作步骤。
示例 1:
输入:nums = [1,3,5]
输出:1
示例 2:
输入:nums = [2,2,2,0,1]
输出:0
提示:
题解
这道题和上一题区别就在于数组元素是可以重复的,所以对于有重复元素的旋转数组,它的特点是:
1、以最小值为分界线的左子序列是单调递增的,右子序列也是单调递增的(注意区分严格单调递增);
2、对于数组中最后一个元素x
,左子序列全大于或等于x
,右子序列全小于或等于x
。
同样通过nums[mid]
和nums[right]
比较,判断nums[mid]
在左子序列还是右子序列。只不过因为有重复值,所以多了一种情况。
有三种情况:
- 如果
nums[mid] < nums[right]
,则mid
在右子序列,即在最小值右边,所以right
左移; - 如果
nums[mid] > nums[right]
,则mid
在左子序列,即在最小值右左边,所以left
右移; - 如果
nums[mid] = nums[right]
,则不能确定mid在左子序列还是右子序列;由于它们的值相同,所以无论nums[right]
是不是最小值,都有一个最小值nums[mid]
,因此我们可以忽略二分查找区间的右端点。
解释下第3点,比如数组[1,0,1,1,1]
,left = 0
,right = 4
,mid = 2
,无法判断mid在在左子序列还是右子序列,我们采用right--
解决此问题
那这个操作会不会影响取最小值呢,事实证明不会:
- 此操作不会使数组越界:因为迭代条件保证了
right>left>=0
; - 此操作不会使最小值丢失:假设
nums[right]
是最小值,有两种情况:- 若
nums[right]
是唯一最小值:那就不可能满足判断条件nums[mid]==nums[right]
,因为mid<right
(left!=right && mid=(left + right)/2
); - 若
nums[right]
不是唯一最小值,由于mid<right
而nums[mid]==nums[right]
,即还有最小值存在于[left,right−1]
区间,因此不会丢失最小值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] < nums[right]) {
right = mid; // right左移
} else if (nums[mid] > nums[right]) {
left = mid + 1; // left右移
} else {
right--;
}
}
return nums[left];
}
};
- 若