Neo's Blog

不抽象就无法深入思考
不还原就看不到本来面目!

0%

heap

red-black

更加平衡,没有极端;

avl

过于平衡,查询性能最好,但是维护成本过高;

treap

splay-tree

区间树

区间树是在平衡树基础上进行扩展得到的支持以区间为元素的动态集合的操作,其中每个节点的关键值是区间的左端点。

区间树

线段树

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 [1]
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

对编号连续的一些点进行修改或者统计操作,修改和统计的复杂度都是$O(log2(n))$

参考:https://blog.csdn.net/zearot/article/details/48299459

用线段树解题,关键是要想清楚每个节点要存哪些信息(当然区间起终点,以及左右子节点指针是必须的),

以及这些信息如何高效更新,维护,查询。不要一更新就更新到叶子节点,那样更新效率最坏就可能变成O(n)的了。

先建树,然后插入数据,然后更新,查询

树状数组(binary indexed tree)

树状数组所能解决的典型问题就是存在一个长度为n的数组,我们如何高效进行如下操作:

  • update(idx, delta):将num加到位置idx的数字上。

  • prefixSum(idx):求从数组第一个位置到第idx(含idx)个位置所有数字的和。

  • rangeSum(from_idx, to_idx):求从数组第from_idx个位置到第to_idx个位置的所有数字的和

m叉树中的m具体取决于一个Page的大小,例如4K

如果一个Node的子节点数量超过m,则分裂;如果小于m,会考虑合并

有一根双向链表来连接所有的叶节点

B+优势:

  • 查询效率更加稳定,所有数据的查找均是从根节点到叶子节点。

MongoDB采用B树,聚合文档,没有范围查找需要。

字典树(Trie),顾名思义是以树结构来模拟字典。

回想我们查字典的过程,比如查找”man”,先翻到字典m部分,再翻第二个字母a和第三个字母n,一共查找3次。

查找次数最多是等于个单词的长度。插入查找单词的时间复杂度时$O(m)$,此外有公共前缀的单词只需存一次公共前缀,节省了空间,也可理解为前缀树

字典树应用

  • 字符串检索
    1. 查询检索字符串
  • 词频统计
    1. 统计一个单词出现了多少次
  • 字符串排序
    1. 字典树建好后,先序遍历就得到了排序。
  • 前缀匹配
    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
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量

// 插入一个字符串
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
cnt[p] ++ ;
}

// 查询字符串出现的次数
int query(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}

跟字符串关联很大的数据结构包括

  1. Trie
  2. 后缀树
  3. 后缀数组(Suffix Array)

后缀树建树的时间和空间成本都很高。后缀数组和后缀自动机可以看作是对后缀树时间和空间上的优化,通过映射关系避免建树和提高树节点重复利用率。

跟字符串相关的hash算法

  • P进制hash(滚动哈希)
    1. 例如:构造哈希比较前缀和后缀,快速判断回文串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
字符串哈希 —— 模板题 AcWing 841. 字符串哈希
核心思想:将字符串看成P进制数,P的经验值是13113331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i];
p[i] = p[i - 1] * P;
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}

跟字符串有关系的技巧

  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
class Solution {
public:
void reverse(string& s, int i, int j) {
while (i < j) {
swap(s[i++], s[j--]);
}
}

string trans(string s, int n) {
int begin = 0;
for (int i = 0; i < s.length();) {
if (s[i] == ' ') {
reverse(s, begin, i - 1);
i++;
begin = i;
}
else {
i++;
}
}
reverse(s, begin, s.length() - 1);

reverse(s, 0, s.length() - 1);

for (int i = 0; i < s.length(); ++i) {
if (s[i] >= 'a' && s[i] <= 'z') {
s[i] = 'A' + (s[i] - 'a');
} else if (s[i] >= 'A' && s[i] <= 'Z') {
s[i] = 'a' + (s[i] - 'A');
}
}
return s;
}
};

字符串匹配算法

分类
单调递增栈,栈顶元素最大
单调递减栈,栈顶元素最小

一般套路

如果找右边更大的元素,则从前到后构造从底到顶的递减栈;
如果找右边更小的元素,则从前到后构造从底到顶的递增栈;
如果找左边更大的元素,则从后到前构造从底到顶的递减栈;
如果找左边更小的元素,则从后到前构造从底到顶的递增栈;

举一个例子: n个整数的无序数组,找到每个元素后面比它大的第一个数,要求时间复杂度为O(N)。右边比我更大的问题

栈里面存的是啥?
存的是还没有算出来的元素,为啥没有算出来呢,因为还没有遇到比栈顶元素更大的。并且栈顶元素是最小的,所以当前元素也不会大于栈里面的其他元素。

什么时候可以算出来?
后面遇到比栈顶更大的元素,这时候就出栈,并更新数据。

遍历到最后,栈里面还有啥内容?
整个列表中没有比这些元素更大的元素。

1
2
3
4
5
6
7
8
9
10

//单调栈 —— 模板题 AcWing 830. 单调栈
//常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;
stk[ ++ tt] = i;
}


Given a list of daily temperatures, produce a list that, for each day in the input, tells you how many days you would have to wait until a warmer temperature. If there is no future day for which this is possible, put 0 instead.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vector<int> dailyTemperatures(vector<int>& t) {
int n = t.size();
stack<int> s;
int top = 0;
for (int i = 0; i < n; ++i) {
while (!s.empty() && t[i] > t[top = s.top()]){
t[top] = i - top;
s.pop();
}

s.push(i); //压入了一个更小的,递减栈
}


while (!s.empty()) {
t[s.top()] = 0;
s.pop();
}
return t;
}

求解一个区间的和乘以这个区间最小值的最大值

解题思路:单调栈 + 前缀数组

i到j的区间和,通过前缀数组,比较容易在O(1)的复杂度内取得。

对每一个元素,取得左边第一个比他小的元素,作为j;从后往前遍历,递增栈;

对每一个元素,取得右边第一个比他小的元素,作为i;从前往后遍历,递增栈;

整体时间复杂度$O(n)$

关于队列一共有两类问题

  1. 在一定条件下,实现某种具有某种特性的队列
  2. 借助队列的某些特性去巧妙的解决某一类问题。

队列的特点:先进先出

队列的用途:

  1. 单调队列解决滑动窗口最值问题
  2. 树或者图的BFS
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

//队列 —— 模板题 AcWing 829. 模拟队列
1. 普通队列:
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh <= tt) //如果hh <= tt, 则队列不空。
{

}

//2. 循环队列
// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh != tt) //只要两者不相等,队列就不空。
{

}

实现队列

借助两个栈来实现一个队列

滑动窗口最值问题

利用一种特殊的队列(单调队列)来巧妙的解决滑动窗口最值问题。

请用栈实现一个队列,支持如下四种操作:

push(x) – 将元素x插到队尾;
pop() – 将队首的元素弹出,并返回该元素;
peek() – 返回队首元素;
empty() – 返回队列是否为空;
注意:

你只能使用栈的标准操作:push to top,peek/pop from top, size 和 is empty;
如果你选择的编程语言没有栈的标准库,你可以使用list或者deque等模拟栈的操作;
输入数据保证合法,例如,在队列为空时,不会进行pop或者peek等操作;
样例
MyQueue queue = new MyQueue();

queue.push(1);
queue.push(2);
queue.peek(); // returns 1
queue.pop(); // returns 1
queue.empty(); // returns false

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
class MyQueue {
public:
stack<int> is;
stack<int> os;

/** Initialize your data structure here. */
MyQueue() {

}

/** Push element x to the back of queue. */
void push(int x) {
is.push(x);
}

/** Removes the element from in front of queue and returns that element. */
int pop() {
if (os.empty()) {
while (!is.empty()) {
os.push(is.top());
is.pop();
}
}
int r = os.top();
os.pop();
return r;
}

/** Get the front element. */
int peek() {
if (os.empty()) {
while (!is.empty()) {
os.push(is.top());
is.pop();
}
}
return os.top();
}

/** Returns whether the queue is empty. */
bool empty() {
return is.empty() && os.empty();
}
};

/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* bool param_4 = obj.empty();
*/

1
2
3
4
5
6
7
8
9
10
//单调队列 —— 模板题 AcWing 154. 滑动窗口
//常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ;
//新的元素入队
q[ ++ tt] = i;
}

什么是LRU缓存,怎么设计的LRU缓存。
时间复杂度:$O(1)$

  1. Hash + 双向链表
  2. 哨兵节点来简化判断
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
75
76
77
78

struct ListNode {
int val;
ListNode* next;
ListNode* pre;
}


class LRUCache {
private:
unordered_map<int, ListNode*> hash;
ListNode* head;
ListNode* tail;
int n;

LRUCache(int nn) {
n = nn;
head = new ListNode();
tail = new ListNode();
head->next = tail;
tail->pre = head;
}

void put(int k, int v) {
ListNode* r = get2(k);
if (r != NULL) return;

ListNode* node = NULL;
if (hash.size() > n) {
//链表上移除
auto node = tail->pre;
node->next->pre = node->pre;
node->pre->next = node->next;
//hash移除
hash.erase(node->k);
}
else {
node = new ListNode();
}

node.val = v;
//将node放在头部
node->next = head->next;
node->pre = head;

head->next->pre = node;
head->next = node;

hash[k] = node;

}

ListNode* get2(int k) {
auto it = hash.find(k);
if it == hash.end() {
return NULL;
}

auto node = *it;

//移除
node->next->pre = node->pre;
node->pre->next = node->next;

node->next = head->next;
node->pre = head;

head->next->pre = node;
head->next = node;
}

int get(int k) {
ListNode* r = get2(k);
if (r == NULL) return -1;
else r->val;
}
};