一、二分法
704. 二分查找
细节注意: m i d = b e g + e n d − b e g 2 mid=beg+\frac{end-beg}{2} mid=beg+2end−beg,如此计算等价于 m i d = b e g + e n d 2 mid=\frac{beg+end}{2} mid=2beg+end,但后者可能导致溢出。
C++ std::upper_bound
函数实现
/* Returns an iterator pointing to the first element
* in the range [first, last) which compares greater than val.
*/
template <class ForwardIterator, class T>
ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last, const T& val) {
ForwardIterator it;
iterator_traits<ForwardIterator>::difference_type count, step;
count = std::distance(first, last); // count is always be (last - first)
while (count > 0) {
it = first;
step = count / 2; // step = (last - first) / 2
std::advance(it, step); // it = first + (last - first) / 2
if (!(val < *it)) {
first = ++it; // choose the right range (it, last)
count -= step + 1;
}
else
count = step; // choose the left range [first, it)
}
return first;
}
C++ lower_bound
函数实现
/* Returns an iterator pointing to the first element
* in the range [first, last) which does not compare less than val.
*/
template <class ForwardIterator, class T>
ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T& val) {
ForwardIterator it;
iterator_traits<ForwardIterator>::difference_type count, step;
count = std::distance(first, last);
while (count > 0) {
it = first;
step = count / 2;
std::advance(it, step);
if (*it < val) {
first = ++it;
count -= step + 1;
}
else
count = step;
}
return first;
}
二分法中闭区间和左闭右开区间之间的映射关系
m i d = b e g + e n d 2 mid=\frac{beg + end}{2} mid=2beg+end 这一运算,当区间长度为奇数时,两种区间都是对应中间位置,当区间长度为偶数时,左闭右开区间对应中间偏右的位置,闭区间对应中间偏左的位置
左闭右开区间 | 闭区间 | 注 | |
---|---|---|---|
偶数长度区间偏左中值 | b e g + e n d − b e g − 1 2 beg+\frac{end-beg-1}{2} beg+2end−beg−1 | b e g + e n d − b e g 2 beg+\frac{end-beg}{2} beg+2end−beg | 前者当区间长度为奇数时会偏左 |
偶数长度区间偏右中值 | b e g + e n d − b e g 2 beg+\frac{end-beg}{2} beg+2end−beg | b e g + e n d − b e g + 1 2 beg+\frac{end-beg+1}{2} beg+2end−beg+1 | 后者当区间长度为奇数时会偏右 |
奇数长度区间中值 | b e g + e n d − b e g 2 beg+\frac{end-beg}{2} beg+2end−beg | b e g + e n d − b e g 2 beg+\frac{end-beg}{2} beg+2end−beg | - |
区间长度不为 0 | b e g < e n d beg<end beg<end | b e g ≤ e n d beg\le end beg≤end | 前者若条件允许建议使用 ≠ \ne = |
区间长度不为 1 | b e g < e n d − 1 beg<end-1 beg<end−1 | b e g < e n d beg<end beg<end | - |
选取不含 mid 的左区间 | e n d = m i d end=mid end=mid | e n d = m i d − 1 end=mid-1 end=mid−1 | 后者请注意 m i d = 0 mid=0 mid=0 的情况 |
选取不含 mid 的右区间 | b e g = m i d + 1 beg=mid+1 beg=mid+1 | b e g = m i d + 1 beg=mid+1 beg=mid+1 | - |
35. 二分查找,若找不到返回其插入后的位置
细节注意:检查最后区间长度收敛到 1 时该元素与被查找元素的相对大小。
34. 实现 std::equal_range
闭区间版
细节注意:先求的边界值可以用来初始化后求的边界值的搜索范围,C++ 的 std::equal_range
的实现也用了同种方法
69. 求 x x x 的平方根(保留整数)
本题可直接借鉴 std::upper_bound
的思想,搜索第一个平方值大于
x
x
x 的数,最后返回时将该数减 1 即可
本题亦有利用指数和对数的解法以及牛顿迭代法
367. 判断一个整数是否恰好是某个整数的平方(完全平方数)
二分查找即可,注意整数溢出
本题亦可使用库函数或使用牛顿迭代法辅助判断
二、双指针
27. 移除数组中等于某个值的元素(原地重排数组)
- 双指针法:模拟人类思维的方法(可保证删除后得到的数组相对顺序不变)
- 相向双指针法:类似快排中的 partialize 方法,将数组靠后部分的不等于给定值的元素与数组靠前部分的等于给定值的元素进行交换,直到前后指针相遇即可,如此两个指针总共加一起只遍历一遍数组,理论上比前面的双指针法更快,但可能改变相对顺序
26. 实现 std::unique
双指针法,可参考 C++ STL 源码
283. 把 0 元素全部移动到数组后面,非零元素相对顺序不变(原地重排数组)
参照 27 双指针法
844. 比较两个含退格的字符串是否显示一致(空间复杂度 O ( 1 ) O(1) O(1))
- 参照 27 双指针法,原地删除对应的字符再进行比较
- 逆序遍历并进行比较。遇到退格符时,继续进行逆序遍历但要对跳过的字符进行计数,初始值为 1,再次遇到退格符时计数加 1,否则计数减 1,直到计数回到 0 再进行比较。须注意如此计数可能导致下标减到负值,对负值下标需要进行特别处理。
977. 对非递减数组的每个元素计算其平方值得到一个新数组,要求新数组为非递减(时间复杂度 O ( n ) O(n) O(n))
将原数组逐个计算其平方后可以得到一个 V 形数组,设置从两边开始的双指针,使用类似归并排序的 merge 方法对两个递减数组进行反向合并即可,注意每次取较大值放入结果数组,循环结束条件为两个指针相遇。
344. 反转字符串(实现 std::reverse
)
使用双指针进行元素交换即可。
541. 每隔 k 个元素反转字符串的 k 个元素
每隔 2k 个元素对当前块中的字符作对应的处理即可,注意边界条件。
151. 颠倒字符串中的单词顺序(字符串中只有字母数字字符和空格)
- Python 可以直接使用
" ".join(reverse(s.split()))
- 对于 C++,可以先反转整个字符串,令 idx(s 的插入位置)为 0,start 表示待插入单词的开始位置,初始化为 0 之后执行如下的循环
- 右移 start 直至其到达字符串末尾或不为空格,若 start 到达末尾则结束循环。否则,若 idx 不为 0,则说明 idx 前面有单词,令 s[idx] 为空格,再令 idx 自增 1,准备向 idx 插入新的单词。
- 设 start 标记的单词的尾后位置为 end,将子串 [start, end) 移动到 [idx, idx + (end - start)) 这个区间,并令 idx 移动到子串移动后的尾后位置 idx + (end - start)。
- 此时移动后的子串所在区间为 [idx - (end - start), idx),将这一部分反转。
- 令 start 为 end,返回 1。
循环执行结束后,idx 若还未达到 s.length(),则说明后面的字符已经被添加完毕了,直接删除即可。
- 使用栈或双端队列保存每个单词,再将单词反向输出即可,注意添加空格和边界条件。
28. 实现 std::string::find
-
暴力破解,对 h a y s t a c k haystack haystack 中每一个属于 [ 0 , h a y s t a c k . l e n g t h − n e e d l e . l e n g t h ) [0,haystack.length-needle.length) [0,haystack.length−needle.length) 的下标进行检查
-
KMP 算法
前缀 p r e f i x prefix prefix:包含首字符的连续子串
后缀 s u f f i x suffix suffix:包含尾字符的连续子串
最长相等前后缀长度 n n n:对于字符串 s s s,求最大的 n n n,使得 s [ 0 , n ) = s [ s . l e n g t h − n , s . l e n g t h ) s[0,n) = s[s.length-n,s.length) s[0,n)=s[s.length−n,s.length),注意不是对称而是相等- 计算 n e e d l e needle needle 的每个形如 n e e d l e [ 0 , i ] needle[0,i] needle[0,i] 的子串的最长相等前后缀长度,记为 n e x t [ i ] next[i] next[i],可见 n e x t next next 为长度为 n e e d l e . l e n g t h needle.length needle.length 的数组。特别地,令 n e x t [ 0 ] : = − 1 next[0]:=-1 next[0]:=−1.
- 令 h a y s t a c k haystack haystack 的初始下标 s t r I d x strIdx strIdx 为 0, n e e d l e needle needle 的初始下标 m o d I d x modIdx modIdx 为 − 1 -1 −1.
- 检查 s t r I d x strIdx strIdx 是否越界,若越界则返回 − 1 -1 −1.
- 若 h a y s t a c k [ s t r I d x ] n e e d l e [ m o d I d x + 1 ] haystack[strIdx]needle[modIdx + 1] haystack[strIdx]needle[modIdx+1],则说明这两项匹配,将 s t r I d x strIdx strIdx 和 m o d I d x modIdx modIdx 均自增 1 1 1,否则跳转到 6.
- 若此时 m o d I d x modIdx modIdx 已经到达 n e e d l e needle needle 的末尾,则返回匹配结果 s t r I d x − n e e d l e . l e n g t h + 1 strIdx-needle.length+1 strIdx−needle.length+1,否则令 s t r I d x strIdx strIdx 自增 1 1 1 并跳转到 3.
- 不断地令 m o d I d x : = n e x t [ m o d I d x ] modIdx:=next[modIdx] modIdx:=next[modIdx],直到 m o d I d x modIdx modIdx 为 − 1 -1 −1 或满足 h a y s t a c k [ s t r I d x ] = n e e d l e [ m o d I d x + 1 ] haystack[strIdx]=needle[modIdx + 1] haystack[strIdx]=needle[modIdx+1],跳转到 4.
n e x t next next 数组的计算方法:
- 首先令 n e x t [ 0 ] : = − 1 next[0]:=-1 next[0]:=−1, p r e f i x I d x : = − 1 prefixIdx:=-1 prefixIdx:=−1, s u f f i x I d x : = 1 suffixIdx:=1 suffixIdx:=1,其中 s u f f i x I d x suffixIdx suffixIdx 表示当前处理的后缀末尾元素下标, p r e f i x I d x prefixIdx prefixIdx 表示当前计算的 n e x t [ s u f f i x I d x ] next[suffixIdx] next[suffixIdx] 对应的最长相等前后缀的前缀的末尾元素下标,可见当最终计算出的 p r e f i x I d x prefixIdx prefixIdx 为 − 1 -1 −1 时,说明 s u f f i x I d x suffixIdx suffixIdx 对应的最长相等前后缀均为空。
- 检查 s u f f i x I d x suffixIdx suffixIdx 是否越界,若越界则 n e x t next next 数组已经计算完毕,退出循环。
- 若 n e e d l e [ s u f f i x I d x ] = n e e d l e [ p r e f i x I d x + 1 ] needle[suffixIdx]=needle[prefixIdx + 1] needle[suffixIdx]=needle[prefixIdx+1] 则说明最长相等前后缀可以扩展,令 p r e f i x I d x prefixIdx prefixIdx 自增 1,之后令 n e x t [ s u f f i x I d x ] : = p r e f i x I d x next[suffixIdx]:=prefixIdx next[suffixIdx]:=prefixIdx,跳转到 2.
- 不断地令 p r e f i x I d x : = n e x t [ p r e f i x I d x ] prefixIdx:=next[prefixIdx] prefixIdx:=next[prefixIdx],直到 p r e f i x I d x prefixIdx prefixIdx 为 − 1 -1 −1 或满足 n e e d l e [ s u f f i x I d x ] = n e e d l e [ p r e f i x I d x + 1 ] needle[suffixIdx]=needle[prefixIdx + 1] needle[suffixIdx]=needle[prefixIdx+1],跳转到 3.
class Solution {
public:
/**
* e.g. haystack = "abeababeabf"; needle = "abeabf";
* next = {-1, 0, 0, 1, 0, 0};
* The values for each time the "match" for-loop begin:
* strIdx: 0 1 2 3 4 5 6 7 8 9 10
* modIdx: -1 0 1 2 3 4 0 1 2 3 4
* when strIdx = 5 and modIdx = 4:
* 1. modIdx rollback to -1
* 2. ++modIdx
*/
int strStr(string haystack, string needle) {
int modLen = needle.size();
int strLen = haystack.size();
vector<int> next(modLen, 0);
next[0] = -1;
int prefixIdx = -1; // stand for the empty prefix
// calculate next[1:modLen]
for (int suffixIdx = 1; suffixIdx < modLen; ++suffixIdx) {
// rollback to the longest prefix for prefix + needle[prefixIdx + 1] == suffix
// prefixIdx may become -1
while (prefixIdx >= 0 && needle[suffixIdx] != needle[prefixIdx + 1]) {
prefixIdx = next[prefixIdx];
}
// the length of prefix is able to be appended
if (needle[suffixIdx] == needle[prefixIdx + 1]) {
++prefixIdx;
}
next[suffixIdx] = prefixIdx; // next[suffixIdx] maybe -1
}
// match
int modIdx = -1; // stand for the empty mod prefix
for (int strIdx = 0; strIdx < strLen; ++strIdx) {
// rollback to the longest modIdx for needle[0:modIdx + 2] == haystack[0:strIdx + 1]
// modIdx maybe -1
while (modIdx >= 0 && haystack[strIdx] != needle[modIdx + 1]) {
modIdx = next[modIdx];
}
// modIdx is able to be increased
if (haystack[strIdx] == needle[modIdx + 1]) {
++modIdx;
}
// success
if (modIdx == modLen - 1) {
return strIdx - modLen + 1;
}
}
return -1;
}
};
459. 检查字符串 s 是否由多个相同的连续子串相加而成
- 令 t = ( s + s ) [ 1 : 2 ∗ s . l e n g t h − 1 ] t=(s+s)[1:2*s.length-1] t=(s+s)[1:2∗s.length−1],若能在 t 中找到 s,则说明 s 由多个子串 s’ 组成,即 s = k ∗ s ′ s=k*s' s=k∗s′
class Solution {
public:
bool repeatedSubstringPattern(string s) {
return (s + s).find(s, 1) != s.size();
}
};
- KMP 算法 1:使用 KMP 算法检查模式 s 和上个解法中的待测字符串 t 是否匹配。
- KMP 算法 2:先计算 next 数组,之后检查字符串长度是否可以被(字符串长度 - 最长相等前后缀长度)整除即可。
- 统计每个字符的出现次数,计算出现次数非零的各个字符的出现次数的最大公约数 gcd,若 gcd 不为 1,则将字符串分为 gcd 块,检查每个块是否相等即可。
三、滑动窗口
209. 寻找和大于等于正整数 x x x 的最短连续子数组(数组中均为正整数)
维护一个滑动窗口,若其中所有数的和小于 x x x,则滑动右边界,否则滑动左边界,滑动时要刷新窗口中的数字和,记录滑动过程中的最小的窗口长度即可。须注意的细节是,若右边界已达到数组尾端而总和仍然小于 x x x 时可以直接停止计算
904. 寻找元素种类数小于等于 2 的最长连续子数组
维护一个滑动窗口,用一个哈希表维护当前窗口中的每个元素的个数,视情况对数组进行滑动,记录最大的符合条件的窗口,须注意的细节是,若右边界已达到数组尾端可以直接停止计算
76. 给定字符串 s s s 和 t t t,寻找 s s s 的包含 t t t 中所有字符的最小连续子串
维护一个滑动窗口
[
b
,
e
)
[b, e)
[b,e)。建立两个哈希表
h
s
hs
hs 和
h
t
ht
ht,其中
h
s
hs
hs 用于存储当前滑动窗口中每个字符出现的次数,
h
t
ht
ht 用于存储
t
t
t 中字符出现的次数。建立一个变量
c
n
t
cnt
cnt 表示当前滑动窗口对应的连续子串中已经匹配了多少个
t
t
t 中的字符,对于滑动窗口中的任意字符
c
c
c,最多匹配
h
t
(
c
)
ht(c)
ht(c) 次。
于是,我们在循环中每次将
e
e
e 右移,每次根据并入的字符更新
c
n
t
cnt
cnt,之后不断右移
b
b
b,直到当前右移操作会导致
h
s
(
s
(
b
)
)
<
h
t
(
s
(
b
)
)
hs(s(b)) < ht(s(b))
hs(s(b))<ht(s(b)) 为止,若此时
c
n
t
=
s
.
l
e
n
g
t
h
cnt = s.length
cnt=s.length,计算当前窗口长度,若当前窗口比原来的窗口更短(假定窗口长度初始为无穷大),则记录当前窗口。
四、模拟
54. 剑指 Offer 29. 将矩阵按顺时针螺旋的顺序打印到一个数组中
转圈遍历,每转向一次便缩小范围,比如到达右上角准备向下遍历之前,把遍历上界自增 1 即可,注意细节
59. 将给定的数组转化成顺时针螺旋的正方形矩阵
与上一题类似
剑指 Offer 05. 将字符串中的空格替换为 “%20”
可以先统计空格数量预先分配空间,也可以直接把 string
当作动态数组。
剑指 Offer 58 - II. 将字符串循环左移 k 次
- 新建一个字符串然后分片拷贝即可
- 先反转前 k 个字符,再反转剩余的字符,最后反转整个字符串