motivation
Top “Hard” Interview Problems from Google, Facebook, FANG (for software engineers) - YouTube
看了这个视频, 视频中解决了三个问题, 刚好这三个问题都比较经典, 所以记录一下.
1 trie的应用:
问题:
- “You are given a prefix and a list of words, you need to return all the potential words that would match this prefix”
- eg: prefix
"do"
, word-list:["dog", "dark", "cat", "door", "dodge"]
, the potential words would be["dog", "door", "dodge"]
;
分析:
假设prefix长度为k, word-list长度为n
brutal force:
- 遍历word-list中每一个单词, 检查每个单词的前k为是否和prefix相同, 是则加入res
- 时间复杂度O(nk)
代码:
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
31class Solution {
public:
vector<string> brutal(vector<string> &words, string &prefix) {
vector<string> res;
int k = prefix.size();
for (string word : words) {
if (word.size() < k) continue;
bool flag = true;
for (int i = 0; i < k; ++i) {
if (word[i] != prefix[i]) {
flag = false;
break;
}
}
if (flag) res.push_back(word);
}
return res;
}
};
int main() {
vector<string> words{"dog", "dark", "cat", "door", "dodge"};
string prefix {"do"};
Solution solution;
auto res = solution.brutal(words, prefix);
for (auto &x : res) {
cout << x << " ";
}
return 0;
}
使用Trie (时间复杂度为O(k) + O(n), 所有单词可能都在这个子树下)
代码
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using namespace std;
struct Node { // trie 结点定义
Node *children[26];
bool isWord;
Node() {
for (auto & i : children) {
i = nullptr;
}
isWord = false;
}
};
class Solution {
public:
Node *trie;
Solution() { // 构造一个空结点
trie = new Node;
}
void build(vector<string> &words) {
for (string &word : words) {
Node *cur = trie;
for (char c : word) {
if (!cur->children[c - 'a']) { // 对应孩子没有路径
Node *node = new Node;
node->isWord = false;
cur->children[c - 'a'] = node;
}
cur = cur->children[c - 'a'];
}
cur->isWord = true;
}
}
vector<string> ansWithTrie(vector<string> &words, string &prefix) {
build(words);
vector<string> res;
Node *cur = trie;
for (char c : prefix) { // 走到对应子树的根
if (!cur->children[c - 'a']) return res;
cur = cur->children[c - 'a'];
}
dfs(cur, res, prefix);
return res;
}
void dfs(Node *cur, vector<string> &res, string tmp) {
if (!cur) return;
if (cur->isWord) res.push_back(tmp);
for (int i = 0; i < 26; ++i) {
if (cur->children[i]) {
char c = 'a' + i;
dfs(cur->children[i], res, tmp + c);
}
}
}
};
int main() {
vector<string> words{"dog", "dark", "cat", "door", "dodge"};
string prefix {"do"};
Solution solution;
auto res = solution.ansWithTrie(words, prefix);
for (auto &x : res) {
cout << x << " ";
}
return 0;
}
- 以上代码针对26个小写字母, 如果全部要支持, 扩展到256ascii码即可.
2 TopK
问题
- 给一个未排序的数组, 返回第k大的元素
分析:
- 方法1, 每次拿最大的, 并且vis标记该结点已经访问过, 拿K次, 时间复杂度O(nk), 空间O(n);
- 方法2, sort, 返回下标为size - k的位置的值, 时间复杂度O(nlogn), 空间快排空间, 可认为是O(1);
- 方法3, 使用heap, 拿k次最大值. 建堆O(n), extract-max O(logn), 所以总时间复杂度为O(n + klogn);
- 最佳方法:基于partition (算法导论版本最好, 不要改了!!!), 递推式近似为
T(n) = T(n/2) + n
, 时间复杂度显然为n + n/2 + n/4 + n/8 +… = 2n = O(n);
代码:
1 |
|
3 单词组合
- 问题
- 给定一些words, 判断哪些word 可以由别的word组合(concatenate)而成, 返回所有这些可以被别人组合而成的word
- 例如: words:{“cat”, “cats”, “dog”, “catsdog”}, 我们应该返回{“catdogs”}, 因为只有”catsdog”可以由别的单词(“cats” + “dog”) 组合而成.
- 分析
- 对于每一个单词, 例如”catsdog” , 可以拆分为两部分, 第一部分直接查询
unordered_set
观察是否存在, 如果存在, 则递归第二部分. 例如:- 拆分为”c” , “atsdog”, “c”在words set里面不存, 返回
- 拆分为”ca” , “tsdog”, “ca”不存在, 返回
- 拆分为”cat”, “sdog”, “cat” 存在, 递归”sdog”, 返回false, 故总体不存在
- 拆分为”cats”, “dog”, “cats”存在, 递归”dog”, 下面展示递归”dog”的过程:
- 拆为 “d”, “og”, “d”不存在, 返回
- 拆为”do”, “g”, “do”不存在, 返回
- 拆为”dog”, “”, “dog”存在, 递归””, 对于空字符串, 递归返回true, 故整体返回true.
- 故整体”catsdog”可以是别的word的concatenation, 加入res中.
- 对于每一个单词, 例如”catsdog” , 可以拆分为两部分, 第一部分直接查询
- 暴力复杂度分析
- 假设words.size 为n, 每个word平均长度为m, O(n * 2 ^m), 具体怎么分析没弄懂
- 优化
- 可以使用cache来保存哪些满足条件的word(不管是不是一个subword), 使得时间复杂度降低到(n * m^2);
- 代码:
1 | class Solution1 { |
总结
- 测试驱动编程, 可以先把如何调用这个函数(类)写好, 再写具体代码
- 每个函数可以先把函数签名和返回值准备好
- 记忆化搜索, 大大降低时间复杂度
- trie C++实现一般需要写构造函数, 明确地将children指针都置为
nullptr
- trie不需要显示得存储
char
, 因为char
的信息已经被隐式得存储在下标之中