[算法竞赛]四、树

之前提到的都是线性数据结构,树是非线性数据结构,可以很好描述层序关系

二叉树

二叉树是最最常用的树形数据结构

二叉树每个节点最多有两个子节点,称为左孩子和右孩子,他们的子树分别称为左子树和右子树。

二叉树第i层最多🈶️2^(i-1)个节点。如果树的所有节点都是满的那么为满二叉树,如果只有最后一层有节点缺失则为完全二叉树。

二叉树从根到某个节点的路径长度为该节点的深度,某个节点到它叶子节点的最大路径为该节点的高度,根节点高度最大

二叉树的访问效率极高

二叉树的存储

动态二叉树:

复制代码
struct node{
int data;
node *l,*r;
};

每次使用申请节点,用后删除

静态数组存储(常用)

复制代码
struct node{
char value;
int l,r
}tree[N];

二叉树与树

树转二叉树

1 先把来自同一个父节点的子节点从左到右连上

2 所有节点的子节点只连左边第一个节点 别的删掉

3 然后整理

二叉树转树

1 若某结点的左孩子结点存在,将左孩子结点的右孩子结点、右孩子结点的右孩子结点......都作为该结点的孩子结点,将该结点与这些右孩子结点用线连接起来;

2 删除原二叉树中所有结点与其右孩子结点的连线;

3 整理

二叉树的遍历

宽度优先遍历(BFS)

从第一层到最后一层从上到下从左到右逐个遍历

深度优先遍历(DFS)

包括前序遍历,中序遍历,后序遍历

前序遍历

根➡️左子树➡️右子树

复制代码
void pre(node *root){
cout<<root->data;
pre(root->l);递归左子树
pre(root->r);递归右子树
}

中序遍历

复制代码
void in(node *root){
in(root->l);递归左子树
cout<<root->data;
in(root->r);递归右子树
}

后序遍历

从左到右从下到上先子节点再父节点,最后根节点

复制代码
void in(node *root){
in(root->l);递归左子树
in(root->r);递归右子树
cout<<root->data;
}

已知中序遍历+前序遍历或者后序遍历 都可以构造出一个树

只有前序遍历和后序遍历无法构造出一棵树

哈夫曼树与哈夫曼编码

哈夫曼树是带权路径最短的树,是贪心思想与二叉树的结合,

哈夫曼编码则是哈夫曼树的经典应用,可以理解为对于比较频繁出现的信息,用尽量少的位数去表示;不常出现的信息,用大的空间来表示

比如我们现在想存储以下这串数据

AAABBBBBBBBBCCCCCCDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEEE

整理后我们得到这串数据有3个A 9个B 6个C 15个D 19个E,共6个字母,我们想用二进制编码表示这些数,我们马上可以想到用相同长度的二进制01来表示

即000为A 001为B 010为C 011为D 100为E

那么这种情况下假设每个二进制数占用一个存储单位,那么现在我们总占用内存为3*(3+9+6+5+19)=156

这种编码虽然很好想也很简单但实际上可能会占用大量空间,聪明的你肯定想到,让个数多的字母用少的空间,让个数少的字母用多的空间,比如E用0表示,D用1表示,A用100来表示

但这样会出现另一个新的问题,如果给你一个二进制数100,这个100表示的是什么?是A?还是DEE。因此我们能发现编码不能随便编,不然会出现混淆。那我们应该怎么解决

我们用二叉树来构造编码:即用哈夫曼树来构建哈夫曼编码

我们让出现频次高的字符靠近根节点,出现频次少的远离根节点,用0表示左节点,1表示右节点

我们得到了A(1100),B(111),C(1101),D(10),E(0)

接下来计算一下他们的空间大小:3*4+3*9+4*6+15*2+19*1=112 此时答案为最优

题目练习

洛谷P1087(橙题)

首先我们仔细读题,根据题目中给出的递归构造方法与给出的输入输出样例,我们自己进行模拟一下(画图)

进行后序遍历后发现我们得到的输出与题目所给一致,那么我们只需用二叉树模拟题目过程即可,

复制代码
#include<bits/stdc++.h>
using namespace std;

char ver(string s) {
    bool F=0,B=0,I=0;

    for (int i=0;i<s.length();i++) {
        if (s[i]=='0')B=1;//判断B
        if (s[i]=='1')I=1;//判断I
        if (B&&I)F=1;//判断F
    }
    if (F==1)return 'F';//return F的判断放在第一个!
    if (B==1)return 'B';
    if (I==1)return 'I';
}

void tree(string s) {//递归树解决

    if (s.size()==1) {//递归停止条件为字符串大小为1
        cout<<ver(s);
        return;
    }
    int l=s.size()/2;//每次字符串大小减半

    tree(s.substr(0,l));//截取前半字符串
    tree(s.substr(l));//拿剩下的字符串 即截取后半字符串
    cout<<ver(s);//后序遍历所以cout放在最后
}

int main() {
    int n;
    string s;
    cin>>n>>s;
    tree(s);
    return 0;
}

洛谷P1030(橙题)

需要注意到

后序遍历的最后一个元素是树的根

中序遍历中左右子树被根给分隔开

左 / 右子树的中序序列长度 = 左 / 右子树的后序序列长度

知道这三条性质后我们再去画树就有思路了

我们先用后序序列找到该子树的根,并寻找它在中序序列中的位置。

接下来我们分别截取左右子树的中序序列和后序序列,分别对它们进行建树操作。

直到建树操作完成后,才停止递归

复制代码
#include<bits/stdc++.h>
using namespace std;

struct node {
    char data;
    int l,r;
}tree[1001]; // 足够存储常规二叉树节点

int pos = 0; // 显式初始化,更规范

// 递归构建二叉树:中序in + 后序post → 返回根节点编号
int planttree(string in,string post) {
    int len = in.length();
    if(len == 0) return 0; // 空树返回0
    
    char c = post[len-1]; // 后序最后一位是当前根
    int root = ++pos;     // 分配当前根的唯一编号
    tree[root].data = c;  // 存储根节点值
    
    int k = in.find(c);   // 找到根在中序中的位置
    // 递归构建左子树:中序[0,k) + 后序[0,k)
    tree[root].l = planttree(in.substr(0,k), post.substr(0,k));
    // 递归构建右子树:中序[k+1,end) + 后序[k, len-k-1)
    tree[root].r = planttree(in.substr(k+1), post.substr(k, len-k-1));
    
    return root; // 返回当前根节点编号
}

// 递归输出前序遍历结果(根→左→右)
void out(int root) {
    if (root == 0) return; // 空节点直接返回,避免越界
    cout << tree[root].data; // 输出根
    out(tree[root].l);       // 递归输出左子树
    out(tree[root].r);       // 递归输出右子树
}

int main() {
    string in, post;
    cin >> in >> post;
    // 1. 构建树,接收根节点编号
    int root = planttree(in, post);
    // 2. 调用输出函数,输出前序遍历结果(核心修复点)
    out(root);
    return 0;
}

洛谷P2168(蓝题)哈夫曼树

复制代码
#include<bits/stdc++.h>
using namespace std;

using ll = long long;

struct Node {
    ll weight;
    ll height;
    
    bool operator<(const Node& other) const {
        if (weight != other.weight) {
            return weight > other.weight;
        }
        return height > other.height;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    ll n, k;
    cin >> n >> k;
    
    priority_queue<Node> pq;
    for (ll i = 0; i < n; ++i) {
        ll w;
        cin >> w;
        pq.push({w, 1});
    }
    
    if ((n - 1) % (k - 1) != 0) {
        ll add_count = k - 1 - (n - 1) % (k - 1);
        for (ll i = 0; i < add_count; ++i) {
            pq.push({0, 1});
        }
    }
    
    ll total_weight = 0;
    ll max_height = 0;
    
    while (pq.size() > 1) {
        ll sum_w = 0;
        ll current_max_h = LLONG_MIN;
        
        for (ll i = 0; i < k; ++i) {
            Node top = pq.top();
            pq.pop();
            
            sum_w += top.weight;
            if (top.height > current_max_h) {
                current_max_h = top.height;
            }
        }
        
        pq.push({sum_w, current_max_h + 1});
        total_weight += sum_w;
        
        if (current_max_h > max_height) {
            max_height = current_max_h;
        }
    }
    
    cout << total_weight << '\n' << max_height << endl;
    
    return 0;
}
相关推荐
bill_man1 小时前
性能优化学习笔记(2)-更好地使用字符串
笔记·学习·性能优化
白云偷星子2 小时前
云原生笔记1
笔记·云原生
桂花很香,旭很美2 小时前
Anthropic Agent 工程实战笔记(五)评测与 Eval
笔记·架构·agent
liliangcsdn3 小时前
探索和学习信任区域策略优化算法-TRPO
学习·算法
无限进步_4 小时前
面试题 02.04. 分割链表 - 题解与详细分析
c语言·开发语言·数据结构·git·链表·github·visual studio
Mr YiRan8 小时前
C++面向对象继承与操作符重载
开发语言·c++·算法
宇木灵11 小时前
考研数学-高中数学-反三角函数与特殊函数day3
笔记·考研·数学·函数
蚊子码农12 小时前
算法题解记录--239滑动窗口最大值
数据结构·算法
liliangcsdn12 小时前
A3C算法从目标函数到梯度策略的探索
算法