《数据结构与算法之美》笔记
二叉树基础
学习数据结构和算法,我们需要学习它的由来、特性、适用的场景以及能够解决的问题即可。对于一些比较复杂的数据结构和算法,其实很多时候并不是很需要我们去实现。
提到树的时候,先考虑树的基本概念,然后再分析树的存储,最后再考虑树的应用。
在各种各样的树里面,比较重要的为二叉树;在二叉树里面,比较重要的为完全二叉树和满二叉树。
完全二叉树:叶子节点都在最后两层,最后一层的节点全部靠左排列,同时除了最后一层,其他所有层的节点数全部要达到最大数量。满二叉树为一种特殊的完全二叉树。
堆就是一个完全二叉树
- 哈希表相对于二叉查找树的劣势
- 哈希表中的数据是无序的
- 哈希扩容耗时,存在散列冲突,性能不稳定
- 哈希表比二叉树构造更负责,哈希表需要考虑哈希函数、冲突解决策略、扩容等问题,但是二叉树只需要考虑平衡性一个点
- 由于哈希表的的装载因子不能过大,所以哈希表会消耗一部分多余的内存空间。
堆的应用
- 合并有序小文件
- 高性能定时器
- 利用堆求TopK
- 利用堆求中位数
- 大文件的关键字统计
题目实战
基本知识之树的遍历
树的存储类型有线性存储和链式存储。线性存储一般用于完全二叉树。对于一般的二叉树而言,最经常使用的存储类型就是链式存储方式。
typedef struct BitNode { // 二叉树的链式存储结构体
struct BitNode *lchild;
struct BitNode *rchild;
int Value;
} BitNode, *BiTree;
树的遍历是树的最基本的知识。分为三种遍历方式:前序遍历、中序遍历、后序遍历。
其中,每种遍历方式都有递归形式和非递归形式。
- 前序遍历的递归形式(另外两种更换递归函数与visit函数的顺序即可)
void PreOrderTraverse(BiTree root) { // 前序遍历 根 -> 左孩子 -> 右孩子
if(!root){
visit(root);
PreOrderTraverse(root->lchild);
PreOrderTraverse(root->rchild);
}
}
递归形式的遍历比较好写并且很容易理解。但是递归会调用大量递归栈,会增加系统内存的消耗。所以也需要掌握三种遍历方式的相应迭代书写方式。
- 中序遍历迭代写法
//非递归的二叉树中序遍历示例一
status InOrderTraverse(BiTree T,void (*visit)(BiTree))
//中序遍历二叉树T
{
// 请在这里补充代码,完成本关任务
/********** Begin *********/
BiTree stack[100];
int i = 0;
BiTree p = T;
while(p || i){
if(p) {stack[i] = p; i++; p = p->lchild;}
else{
i--;
p = stack[i]; visit(p);
p = p->rchild;
}
}
return OK;
/********** End **********/
}
//非递归的二叉树中序遍历示例二(伪代码)
status InOrderTraverse(BiTree T, void(*visit)(BiTree)){
InitStack(S); Push(S, T);
while(!StackEmpty(S)){
while(Getop(S, p) && p) { Push(S, p->lchild); }
Pop(S, p);//空指针退栈!
if(!StackEmpty(S)){
Pop(S, p); if(!visit(p)) return ERROR;
Push(S, p->rchild);
}
}
return true;
}
这两种迭代的中序遍历写法是最常见的,但是容易混淆。需要记忆并多加练习。但是,对于前序和后序遍历来说,这种写法确实比较难以实现的。可以考虑下面这种用迭代模拟递归的写法,可以很方便实现三种遍历方式的非递归实现。
- 三种遍历的容易理解迭代写法——涂色方法
我们可以给每个节点标记颜色来简单处理这个问题。
首先将所有的节点全部染为白色。(白色表示为访问过)然后从root节点开始访问
如果遇到一个白色节点,就将其染为灰色,然后将其左右节点与其本身加入栈中。
如果遇到一个灰色节点,就对其节点中值进行处理即可。
对于三种不同的遍历方式,只需要适当更改当遇到节点颜色为白色时的push顺序即可。(具体见注释)
enum color_set {white, gray};
typedef struct BitNode {
struct BitNode *lchild;
struct BitNode *rchild;
int Value;
} BitNode, *BiTree;
stack<pair<BiTree ,color_set> > st;
void PreOrderTraverse(BiTree root) { // 前序遍历 根 -> 左孩子 -> 右孩子
st.push(make_pair(root, white));
while(!st.empty()) {
pair<BiTree, color_set> p = st.top(); st.pop();
if(p.second == white) { // 如果为白色的点,第一次访问
//由于是向栈中push,所以这里的顺序与实际的访问顺序是颠倒的。
st.push(make_pair(p.first->rchild, white));
st.push(make_pair(p.first->lchild, white));
st.push(make_pair(p.first, gray));
} else if(p.second == gray) {
visit(p.first->Value);
}
}
}
洛谷SP14932 LCA问题(最小共同祖先问题)
1)LCA(Lowest Common Ancester)问题有很多解决方案。例如树链、倍增、Tarjan等等。本题采用倍增算法。
- 链式前向星数据结构存储树!(数组模拟邻接表)
2)基本思想:
用一个二维数组f[a][b]记录 a 结点的 2 的 i 次方级祖先。因此有以下迭代关系。利用这个迭代关系就可以结合动态规划思想求出所有的 f 值
f[a][b] = f[f[a][b-1]][b-1];
首先要利用 dfs ,先得到每一个节点的深度以及初始化 f[i][0]的值。
void dfs(int u, int f){
for(int i = Head[u]; i; i = Edge[i].next){
if(Edge[i].to != f){//访问到空结点时推出递归
d[Edge[i].to] = d[u] + 1;
f[Edge[i].to][0] = u;
dfs(Edge[i].to, u);
}
}
}
然后再倍增出其他的 f 值
然后利用f数组进行爬树操作。将 a 和 b 结点爬到同样高度后,如果父节点一样就表示可以进行输出
int LCA(int a, int b){
if(d[a] < d[b]) {a = a^b; b = a^b; a = a^b;}
int L = 0;
while((1<<L) <= d[a]) L++;//L是最大的爬树次数,爬L次就到根了
L--;
for(int i = L; i >= 0; i--)//尽量爬到相同的高度
if(d[a]-(1<<i) >= d[b])
a = f[a][i];
if(a == b) return a;
for(int i = L; i >= 0; i--)//同时向上爬树
if(f[a][i] != f[b][i])
a = f[a][i], b = f[b][i];
return f[a][0];
}
leetcode236 二叉树的最近公共祖先问题
通过学习灵神的思想,可以得到以下分类讨论的思想:
我们通过递归进行处理,左子树返回left,右子树返回right,其中left不为nullptr,说明左子树中至少有p或者q;right同理。
首先假定left 和 right 都已经返回了, 要么是 none 要么是返回了节点
如果两边都有 那么当前root一定是lca
如果两边有一边没有,那么另一边一定是lca,因为是从上到下遍历,所以找到第一个p 或者 q后,另一个没有找的节点一定在 已经返回的节点的下面。那么第一个被返回的节点就是另一个节点的lca
代码如下:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(!root || root == p || root == q) return root;
auto left = lowestCommonAncestor(root->left, p, q);
auto right = lowestCommonAncestor(root->right, p, q);
if(left && right) return root;
return left? left: right;
}