知识汇总
树相关的知识点比较多,个人感觉重点会是二叉树的遍历,搜索二叉树、堆,并查集可能会涉及,其他的还有平衡二叉树,这个应该不会让手撕。
二叉树
二叉树相关问题最常考的就是二叉树遍历的模板:
递归模板
// Traverse
void traverse(TreeNode* root) {
// 先序遍历
traverse(root->left);
// 中序遍历
traverse(root->right);
// 后序遍历
}
迭代模板(先序遍历和中序遍历)
// 先序遍历和中序遍历
while (!stack.empty() || node) {
while (node) {
stack.push_back(node);
node = node->left;
// 先序遍历
}
node = stack[stack.size()-1];
// 中序遍历
stack.pop_back();
node = node->right;
}
迭代模板(后序遍历)
如果使用模拟递归的迭代方法进行后序遍历的时候,需要考虑向右遍历时何时弹栈
while (!stack.empty() || node) {
while (node) {
stack.push_back(node);
node = node->left;
}
node = stack[stack.size()-1];
stack.pop_back();
if (!node->right || node->right == pre) {
returnArray.push_back(node->val);
pre = node;
node = nullptr;
} else {
stack.push_back(node);
node = node->right;
}
}
还有一种方法则是利用了遍历顺序的特征,因为后序遍历是“左右根”,逆序则为“根右左”,而根右左实现起来要简单很多,即将先序遍历访问左右子树的顺序反过来。代码在此则不再赘述。
堆
关于堆的问题我认为只要把握好三个堆的方法和两种操作,手撕起来也不复杂。其中三个方法分别是建堆、入堆、出堆;两种操作是SiftUp和SiftDown;
void swap(int& a, int& b) {
int c = a;
a = b;
b = c;
}
void SiftDown(vector<int>& heap, int start) {
int n = heap.size();
while (start*2+1 < n) { // 为非叶子结点时
if (start*2+2 < n) { //有右儿子的时候
if (heap[start*2+1] < heap[start] && heap[start*2+1] < heap[start*2+2]) {
swap(heap[start], heap[start*2+1]);
start = start * 2 + 1;
}
else if (heap[start*2+2] < heap[start] && heap[start*2+2] < heap[start*2+2]) {
swap(heap[start, heap[start*2+2]);
start = start * 2 + 2;
} else break;
} else {
if (heap[start*2+1] < heap[start]) {
swap(heap[start*2+1], heap[start]);
start = start * 2 + 1;
} else break;
}
}
void SiftUp(vector<int>& heap, int start) {
int n = heap.size();
while (start > 0) {
if (heap[start] < heap[(start - 1) / 2]) {
swap(heap[start], heap[(start - 1) / 2]);
start = (start - 1) / 2;
}
}
}
建堆
建堆的过程是从最后一个非叶子结点开始做SiftDown操作,直到根节点结束
n = heap.size();
int lastNode = (n / 2) - 1;
for (int i = lastNode; i >= 0; --i) SiftDown(heap, i);
入堆
入堆是将结点作为完全二叉树的最后一个结点插入之后,对其做SiftUp操作
heap.push_back(k);
SiftUp(heap, heap.size()-1);
出堆
出堆是将根节点弹出后,把堆的最后一个结点放在根节点的位置,然后对其做一次SiftDown操作
int topVal = heap[0]; // 保留堆顶的值
heap[0] = heap[heap.size() - 1]; // 将最后一个结点的值放到堆顶
heap.pop_back(); // 弹出最后一个结点
SiftDown(heap, 0); //对新堆顶进行一个SiftDown操作
并查集
并查集的关键在于三点:并(Union)、查(Find)和路径压缩
并(Union)
查询两个结点的祖先,如果相同则说明两个结点已经在同一颗树中,如果不同则合并它们对应的两颗树
int a_ancestor = find(a); // 找到a的祖先
int b_ancestor = find(b); // 找到b的祖先
if (a_ancestor != b_ancestor) { // 如果两者祖先不同则合并
father[b_ancestor] = a_ancestor;
}
查(Find)
递归查找祖先结点
int find(int a) { // 递归查找祖先结点
if (father[a] != a) {
return find(father[a]);
} else return a;
}
路径压缩
路径压缩有几个方面,这里介绍最方便写的一个方面,就是尽量减小树的深度,实现方式是在find的递归过程中,将返回值赋给非根结点
int find(int a) { // 递归查找祖先结点
if (father[a] != a) {
father[a] = find(father[a]); // 这一步即为路径压缩,沿途的父亲都变成祖先
return father[a];
} else return a;
}
总结
个人感觉这几个点本身难度不大,主要在于编程实现的时候留意一些细节。以上的代码均未调试,如果有什么问题也请指正。