二分查找模板
查找递增数组num中第一个大于等于target的元素下标。
写法采用闭区间写法,即
[0, left - 1]
始终小于target,即下图中红色区域[right + 1, nums.size() - 1]
始终大于等于target,即下图中蓝色区域- 遍历结束的条件是
left > right
- 最后
left
指向的元素(也是right + 1
指向的元素)就是第一个大于等于target的元素
int lower_bound(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
// 闭区间
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] < target)
left = mid + 1;
else right = mid - 1;
}
return left;
}
上述模板函数返回第一个大于等于target的元素下标。对于其他三种情况(前提是整数数组):
- 大于target:等价于大于等于target + 1
- 小于target:等价于大于等于target的前一个元素
- 小于等于target:等价于大于等于target + 1的前一个元素
34. 在排序数组中查找元素的第一个和最后一个位置 二分查找 2023/2/7
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
使用上述代码模板轻松求解,start为大于等于target的第一个元素,end为小于等于target的第一个元素。
class Solution {
public:
// 二分模板
int lower_bound(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
// 闭区间
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] < target)
left = mid + 1;
else right = mid - 1;
}
return left;
}
vector<int> searchRange(vector<int>& nums, int target) {
int start = lower_bound(nums, target);
if (start == nums.size() || nums[start] != target) {
return {-1, -1};
}
int end = lower_bound(nums, target + 1) - 1;
return {start, end};
}
};
162. 寻找峰值 类二分查找 2023/2/7
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
对于所有有效的 i 都有 nums[i] != nums[i + 1]
示例:
输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。
峰值元素可能不止一个,只需找出一个即可。
每次选取中间的一个元素mid
,并与其相邻元素mid + 1
比较。
- 如果元素
mid
较大,一定有一个峰顶在[left, mid]
之间,将右区间置为mid
- 如果元素
mid + 1
较大,一定有一个峰顶在[mid + 1, right]
之间,将左区间置为mid + 1
- 当只剩一个元素时,必定为峰顶,跳出循环
class Solution {
public:
int findPeakElement(vector<int>& nums) {
// 二分查找写法,峰值一定在闭区间[left, right]内
int left = 0;
int right = nums.size() - 1;
// 当左右区间相等时跳出
while (left < right) {
// 类似二分每次消除一半元素
int mid = (left + right) / 2;
// 一定有一个峰顶在[mid + 1, right]之间,区间改写
if (nums[mid] < nums[mid + 1])
left = mid + 1;
// 一定有一个峰顶在[left, mid]之间,区间改写
else right = mid;
}
return left;
}
};
153. 寻找旋转排序数组中的最小值 类二分查找 2023/2/7
已知一个长度为 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) 的算法解决此问题。
示例:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
每次选取中间的一个元素mid
,并与其结尾元素right
比较。
- 逐步缩减
[left, right]
范围,直到找到最小元素 - 比较前看看
left
元素是否小于right
元素,如果是,则序列已经为升序序列了,左端元素一定为最小元素 - 如果元素
right
较大,[mid, right]
为升序数组,最小值一定在[left, mid]
之间,舍弃右半边元素 - 如果元素
mid
较大,[left, mid]
为升序数组,最小值一定在[mid + 1, right]
之间,舍弃左半边元素 - 当只剩一个元素时,必定最小元素,跳出循环
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
// 此时整个数组一定为升序数组,最小值即为left
if (nums[left] < nums[right]) return nums[left];
int mid = (left + right) / 2;
// [mid, right]为升序数组,最小值一定在[left, mid]之间,舍弃右半边元素
if (nums[mid] < nums[right]) {
right = mid;
}
// [left, mid]为升序数组,最小值一定在[mid + 1, right]之间,舍弃左半边元素
else left = mid + 1;
}
return nums[left];
}
};
33. 搜索旋转排序数组 类二分查找 2023/2/7
整数数组 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) 的算法解决此问题。
示例:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
本题与上题非常类似,一种方法是先使用上题类二分法找到最小值对应下标,将原数组分解成两个有序数组,接着对两个有序数组进行二分查找即可。
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int mid = (left + right) / 2;
// [mid, right]有序
if (nums[mid] < nums[right])
right = mid;
// [left, mid]有序
else left = mid + 1;
}
// 在[0, left - 1]和[left, nums.size() - 1]区间分别使用二分
auto it1 = lower_bound(nums.begin(), nums.begin() + left, target);
if (it1 != nums.end() && *it1 == target) return it1 - nums.begin();
auto it2 = lower_bound(nums.begin() + left, nums.end(), target);
if (it2 != nums.end() && *it2 == target) return it2 - nums.begin();
return -1;
}
};
还有一种方法,取完mid后可以得到一个有序区间和一个无序区间,若该有序区间左右端点包括target,直接在该有序区间进行二分查找,否则缩小查找范围。
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int mid = (left + right) / 2;
// [mid, right]有序
if (nums[mid] < nums[right]) {
// 看看该有序区间左右端点是否包括target
if (nums[mid] <= target && target <= nums[right]) {
auto it = lower_bound(nums.begin() + mid, nums.begin() + right, target);
return *it == target ? it - nums.begin() : -1;
}
else right = mid - 1;
}
// [left, mid]有序
else {
// 看看该有序区间左右端点是否包括target
if (nums[left] <= target && target <= nums[mid]) {
auto it = lower_bound(nums.begin() + left, nums.begin() + mid, target);
return *it == target ? it - nums.begin() : -1;
}
else left = mid + 1;
}
}
return nums[left] != target ? -1 : left;
}
};
436. 寻找右区间 Medium lower_bound
方法 2021/11/10
给你一个区间数组 intervals ,其中 intervals[i] = [starti, endi] ,且每个 starti 都 不同 。
区间 i 的 右侧区间 可以记作区间 j ,并满足 startj >= endi ,且 startj 最小化 。
返回一个由每个区间 i 的 右侧区间 在 intervals 中对应下标组成的数组。如果某个区间 i 不存在对应的 右侧区间 ,则下标 i 处的值设为 -1 。
示例:
输入:intervals = [[3,4],[2,3],[1,2]]
输出:[-1,0,1]
解释:对于 [3,4] ,没有满足条件的“右侧”区间。
对于 [2,3] ,区间[3,4]具有最小的“右”起点;
对于 [1,2] ,区间[2,3]具有最小的“右”起点。
利用map容器插入时自动按key排序的性质,使用map容器存储intervals中的左区间值和对应下标,遍历各区间时,使用map容器中的lower_bound
方法找到key中首个>=其右区间值的迭代器,其second值即为其下标。
class Solution {
public:
vector<int> findRightInterval(vector<vector<int>>& intervals) {
map<int, int> mp_start;
// 将各个区间的左区间值和其对应下标放到map里
for (int i = 0; i < intervals.size(); i++)
mp_start[intervals[i][0]] = i;
vector <int> res;
for (int i = 0; i < intervals.size(); i++)
{
// 二分查找mp_start中最近的key>=其右区间值,返回该迭代器
auto p = mp_start.lower_bound(intervals[i][1]);
if (p == mp_start.end()) res.emplace_back(-1);
// 其second就是对应下标
else res.emplace_back(p->second);
}
return res;
}
};
函数补充 map<key, value>::lower_bound
前提必须有序
// 返回一个迭代器,指向键值 >= _Keyval 的第一个元素;
iterator lower_bound(const key_type& _Keyval)
// 返回一个迭代器,指向键值 > _Keyval 的第一个元素;
iterator upper_bound(const key_type& _Keyval)
本题也可以使用vector容器存放pair类型数据,再使用sort函数对其pair的第一个元素排序(自定义排序规则参考“C++提高编程”文件),最后使用lower_bound
函数找到最近>=其右区间值的pair。
class Solution {
public:
vector<int> findRightInterval(vector<vector<int>>& intervals) {
vector<pair<int, int>> v_start;
// 将各个区间的左区间值和其对应下标放到vector里
for (int i = 0; i < intervals.size(); i++)
v_start.emplace_back(intervals[i][0], i);
// 将v_start排序(默认按照前一个元素从大到小排序)
sort(v_start.begin(), v_start.end());
vector <int> res;
for (int i = 0; i < intervals.size(); i++)
{
// 二分查找v_start中最近的>=其右区间值,返回该迭代器
auto p = lower_bound(v_start.begin(), v_start.end(), make_pair(intervals[i][1], 0));
if (p == v_start.end()) res.emplace_back(-1);
// 其second就是对应下标
else res.emplace_back(p->second);
}
return res;
}
};
函数补充 lower_bound
前提必须有序
区别于上文中map类中的方法,这里的lower_bound
是algorithm头文件中的函数。
//在 [first, last) 区域内查找不小于 val 的元素
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last,
const T& val);
6356.子字符串异或查询 位运算、多次查询、查询预处理 2023/2/12
给你一个 二进制字符串 s 和一个整数数组 queries ,其中 queries[i] = [firsti, secondi] 。
对于第 i 个查询,找到 s 的 最短子字符串 ,它对应的 十进制值 val 与 firsti 按位异或 得到 secondi ,换言之,val ^ firsti == secondi 。
第 i 个查询的答案是子字符串 [lefti, righti] 的两个端点(下标从 0 开始),如果不存在这样的子字符串,则答案为 [-1, -1] 。如果有多个答案,请你选择 lefti 最小的一个。
请你返回一个数组 ans ,其中 ans[i] = [lefti, righti] 是第 i 个查询的答案。
子字符串 是一个字符串中一段连续非空的字符序列。
示例:
输入:s = “101101”, queries = [[0,5],[1,2]]
输出:[[0,2],[2,3]]
解释:第一个查询,端点为 [0,2] 的子字符串为 “101” ,对应十进制数字 5 ,且 5 ^ 0 = 5 ,所以第一个查询的答案为 [0,2]。第二个查询中,端点为 [2,3] 的子字符串为 “11” ,对应十进制数字 3 ,且 3 ^ 1 = 2 。所以第二个查询的答案为 [2,3] 。
本题首先需要熟悉异或运算的性质:A⊕B=C ⇒ A=B⊕C
。
最容易想到的是首先对first和second进行异或运算,然后将其转化为字符串,再使用find方法找出当前字符串在二进制字符串中的位置即可。
可使用map进行优化,避免重复元素查找,但这样的做法往往对于多次查询很难通过,即queries比较长时,每次调用一次find时间复杂度较高。
class Solution {
public:
vector<vector<int>> substringXorQueries(string s, vector<vector<int>>& queries) {
vector<vector<int>> res(queries.size(), vector<int>());
unordered_map<int, vector<int>> mm;
for (int i = 0; i < queries.size(); i++) {
int num = queries[i][0] ^ queries[i][1];
if (mm.find(num) != mm.end()) {
res[i] = mm[num];
continue;
}
string str = convert(num);
int index = s.find(str);
if (index != -1) {
res[i] = {index, int(index + str.size() - 1)};
mm[num] = res[i];
}
else res[i] = {-1, -1};
}
return res;
}
// 将十进制转化为二进制字符串
string convert(int num) {
if (num == 0) return "0";
string str;
int i = 0;
while (num) {
str = to_string(num % 2) + str;
num /= 2;
i++;
}
return str;
}
};
对于这种需要多次查询的问题,一种通用的解决方法是进行预处理,本题中计算以s[i]开始的1位数,2位数… 30位数的值(超过30位的数据其值将大于10^9),并使用unordered_map<int, pair<int, int>>
进行记录,每次窗口右滑时,将当前val左移一位并与s[i + len] - '0'
进行或运算即可,非常巧妙,如果当前窗口对应的十进制值没有被记录,则存储。
这样每次查询都只有O(1)的时间复杂度。
class Solution {
public:
vector<vector<int>> substringXorQueries(string s, vector<vector<int>>& queries) {
int n = s.size();
vector<vector<int>> ans;
unordered_map<int, pair<int, int>> mp; // <值,<开始位置,结束位置>>
// 预处理: 计算以s[i]开始的1位数,2位数... 30位数的值
for (int i = 0; i < n; ++i) {
if (s[i] == '0') { // 当前位为0,跳过
if (!mp.count(0)) mp[0] = pair<int, int>(i, i);
continue;
}
for (int len = 0, val = 0; len < 30 && i + len < n; ++len) {
val = val << 1 | (s[i + len] - '0'); // 窗口右滑,计算当前窗口二进制值
if (!mp.count(val)) // 之前没有出现过才进行保存
mp[val] = pair<int, int>(i, i + len);
}
}
for (auto& q : queries) {
int x = q[0] ^ q[1];
if (!mp.count(x)) ans.push_back({-1, -1});
else ans.push_back({mp[x].first, mp[x].second});
}
return ans;
}
};
剑指 Offer II 057.值和下标之差都在给定的范围内 滑动窗口 set 2023/3/11
给你一个整数数组 nums 和两个整数 k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k 。
如果存在则返回 true,不存在返回 false。
示例:
输入:nums = [1,2,3,1], k = 3, t = 0
输出:true
维持一个长度为k的滑动窗口,并使用数组存储,每次使用std::upper_bound
方法找到不小于该元素的下一个元素,并与之比较,并找到前一个元素与之比较。这里使用数组较慢的一点是删除时仍需用std::upper_bound
方法或std::equal_range
方法找到删除的位置。
class Solution {
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
vector<int> v;
for (int i = 0; i < nums.size(); i++) {
auto p = lower_bound(v.begin(), v.end(), nums[i]);
if (p != v.end())
if ((long long)*p - nums[i] <= t) return true;
if (p != v.begin())
if ((long long)nums[i] - *(p - 1) <= t) return true;
v.insert(p, nums[i]);
if (v.size() > k)
v.erase(lower_bound(v.begin(), v.end(), nums[i - k]));
}
return false;
}
};
使用set有序存储即可解决上述问题,在删除时直接删除该元素即可。但在索引时找到前一个元素不能使用*(p-1)
因为set的迭代器不支持,只能使用*next
方法。
set自带lower_bound
方法,无需指定开始和结束迭代器。但其实也有std::upper_bound
方法,两种择一使用即可。
class Solution {
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
set<int> st;
for (int i = 0; i < nums.size(); i++) {
auto p = st.lower_bound(nums[i]);
if (p != st.end())
if ((long long)*p - nums[i] <= t) return true;
if (p != st.begin())
if ((long long)nums[i] - *next(p, -1) <= t) return true;
st.insert(p, nums[i]);
if (st.size() > k)
st.erase(nums[i - k]);
}
return false;
}
};
剑指 Offer II 058.日程表 自定义数据类型 运算符重载 2023/3/11
请实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内没有其他安排,则可以存储这个新的日程安排。
MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数 x 的范围为, start <= x < end。
当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生重复预订。
每次调用 MyCalendar.book方法时,如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true。否则,返回 false 并且不要将该日程安排添加到日历中。
示例:
输入:
[“MyCalendar”,“book”,“book”,“book”]
[[],[10,20],[15,25],[20,30]]
输出: [null,true,false,true]
解释:
MyCalendar myCalendar = new MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(15, 25); // returns false ,第二个日程安排不能添加到日历中,因为时间 15 已经被第一个日程安排预定了
MyCalendar.book(20, 30); // returns true ,第三个日程安排可以添加到日历中,因为第一个日程安排并不包含时间 20
本题说白了就是有多个左闭右开的区间,设计一种算法来判断加入下一个区间是否满足要求。
使用 set并自定义新数据类型+重载<运算符 可巧妙解决。
两个区间的重叠方式一共就上述三种,观察到若区间存在重叠,则必定有两个区间的左区间小于另一个区间的右区间,因此重载运算符时使用
e
n
d
<
=
n
.
s
t
a
r
t
end <= n.start
end<=n.start 。
补充:set的元素插入时,将调用重载<运算符来比较元素是否相同,若有
A
<
B
A<B
A<B 不成立,且有
B
<
A
B<A
B<A 不成立,则判定为
A
=
B
A=B
A=B ,只要有一个满足,则判定两个元素不为相同元素。
插入方法的返回值为一个二元素,第一个元素为插入位置的迭代器,第二个元素为是否插入成功的布尔变量,本题恰好使用第二个元素。
class Node {
public:
int start, end;
Node(int _start, int _end): start(_start), end(_end) {}
bool operator< (const Node& n) const {
return end <= n.start;
}
};
class MyCalendar {
public:
set<Node> st;
bool book(int start, int end) {
return st.insert(Node(start, end)).second;
}
};
剑指 Offer II 070.排序数组中只出现一次的数字 二分查找 位运算 2023/3/11
给定一个只包含整数的有序数组 nums ,每个元素都会出现两次,唯有一个数只会出现一次,请找出这个唯一的数字。
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
要满足 O ( l o g n ) O(logn) O(logn) 的时间复杂度只能使用二分查找。数组中每次都是 2 n + 1 2n+1 2n+1 个元素,要判断中间的元素和左/右的元素是否相等来判断出现一次的元素在哪一堆,而且要分奇偶讨论,非常容易错。
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] == nums[mid + 1]) {
if ((right - mid) % 2 == 0)
left = mid + 2;
else right = mid - 1;
}
else if (nums[mid] == nums[mid - 1]) {
if ((right - mid) % 2 == 0)
right = mid - 2;
else left = mid + 1;
}
else return nums[mid];
}
return nums[left];
}
};
巧用异或运算符
∧
\wedge
∧ 可以完美解决。与1进行异或,如果为
2
k
−
1
2k-1
2k−1 则会得到
2
k
2k
2k ,如果为
2
k
2k
2k 则会得到
2
k
−
1
2k-1
2k−1。
n
u
m
s
[
m
i
d
]
=
=
n
u
m
s
[
m
i
d
∧
1
]
nums[mid] == nums[mid \wedge 1]
nums[mid]==nums[mid∧1]说明问题出在右边
l
e
f
t
=
m
i
d
+
1
left=mid+1
left=mid+1,否则在左边
r
i
g
h
t
=
m
i
n
(
m
i
d
,
m
i
d
∧
1
)
right=min(mid,mid\wedge1)
right=min(mid,mid∧1)。
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while (left < right) {
int mid = (right - left) / 2 + left;
if (nums[mid] == nums[mid ^ 1]) {
left = mid + 1;
} else {
right = min(mid, mid ^ 1);
}
}
return nums[left];
}
};
剑指 Offer II 071.按权重生成随机数 二分查找 前缀和 2023/3/23
给定一个正整数数组 w ,其中 w[i] 代表下标 i 的权重(下标从 0 开始),请写一个函数 pickIndex ,它可以随机地获取下标 i,选取下标 i 的概率与 w[i] 成正比。
例如,对于 w = [1, 3],挑选下标 0 的概率为 1 / (1 + 3) = 0.25 (即,25%),而选取下标 1 的概率为 3 / (1 + 3) = 0.75(即,75%)。
也就是说,选取下标 i 的概率为 w[i] / sum(w) 。
首先构建前缀和数组,再从数组里随机挑选一个值,例如
[
1
,
3
]
[1,3]
[1,3] ,则前缀和数组为
[
0
,
1
,
4
]
[0,1,4]
[0,1,4] ,用rand
方法比如返回的是2.5,向下取整得到2,再在前缀和数组中找到第一个大于2的元素,其前一个元素就是要找的下标。
class Solution {
public:
vector<int> preSum;
Solution(vector<int>& w) {
preSum = vector<int>(w.size() + 1, 0);
for (int i = 0; i < w.size(); i++) preSum[i + 1] = w[i] + preSum[i];
srand(time(NULL));
}
int pickIndex() {
int num = rand() % preSum.back();
return lower_bound(preSum.begin(), preSum.end(), num + 1) - preSum.begin() - 1;
}
};