数据结构——树

《数据结构与算法之美》笔记

二叉树基础

学习数据结构和算法,我们需要学习它的由来、特性、适用的场景以及能够解决的问题即可。对于一些比较复杂的数据结构和算法,其实很多时候并不是很需要我们去实现。

提到树的时候,先考虑树的基本概念,然后再分析树的存储,最后再考虑树的应用。

在各种各样的树里面,比较重要的为二叉树;在二叉树里面,比较重要的为完全二叉树和满二叉树。

完全二叉树:叶子节点都在最后两层,最后一层的节点全部靠左排列,同时除了最后一层,其他所有层的节点数全部要达到最大数量。满二叉树为一种特殊的完全二叉树。

堆就是一个完全二叉树

  • 哈希表相对于二叉查找树的劣势
  1. 哈希表中的数据是无序的
  2. 哈希扩容耗时,存在散列冲突,性能不稳定
  3. 哈希表比二叉树构造更负责,哈希表需要考虑哈希函数、冲突解决策略、扩容等问题,但是二叉树只需要考虑平衡性一个点
  4. 由于哈希表的的装载因子不能过大,所以哈希表会消耗一部分多余的内存空间。

堆的应用

  1. 合并有序小文件
  2. 高性能定时器
  3. 利用堆求TopK
  4. 利用堆求中位数
  5. 大文件的关键字统计

题目实战

基本知识之树的遍历

树的存储类型有线性存储和链式存储。线性存储一般用于完全二叉树。对于一般的二叉树而言,最经常使用的存储类型就是链式存储方式。

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;
}

P1131时态同步

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lingwu_hb

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值