• [数据结构] 树、森林及二叉树的应用


    树、森林

    树的存储结构

    双亲表示法

    image

    双亲表示法的存储结构

    #define MAX_TREE_SIZE 100
    typedef struct {
    int data;
    int parent;
    }PTNode;
    typedef struct {
    PTNode nodes[MAX_TREE_SIZE];
    int n;
    }PTree;

    【注】 区别树的顺序存储结构与二叉树的顺序存储结构。在树的顺序存储结构中,数组下标代表节点的编号,下标中所存的内容指示了节点之间的关系。而在二叉树的顺序存储结构中, 数组下标既表达了节点的编号,又指示了二叉树中节点之间的关系。当然,二叉树属于树,因此二叉树也可以用树的存储结构来存储,但树却不能都用都用二叉树的存储结构来存储。

    孩子表示法

    孩子表示法是将每个节点的孩子节点视为一个线性表,且以单链表作为存储结构,则n个节点就有n个孩子链表(叶节点的孩子链表为空表)。而n个头指针又组成一个线性表。

    与双亲表示法相反,孩子表示法寻找孩子的操作非常方便,而寻找双亲的操作则需要遍历n个节点中孩子链表指针域所指向的n个孩子链表。

    孩子兄弟表示法

    又称二叉树表示法,即以二叉链表作为树的存储结构。孩子兄弟表示法使每个节点包括三个部分的内容:节点值、指向节点第一个孩子节点的指针、指向节点下一个兄弟节点的指针

    结构体如下:

    typedef struct CSNode {
    inr data;
    struct CSNode *firstchild, *nextsibling;
    }CSNode, *CSTree;

    树、森林、二叉树的转换

    树转换为二叉树

    (1)在兄弟节点之间画一条线;

    (2)对每个节点,只保留它与第一个孩子的连线,而与其他孩子的连线全部删除;

    (3)以树根为轴心,顺时针旋转45°。

    image
    image

    以下代码引用我大佬同学:作者:Amαdeus,出处:https://www.cnblogs.com/MAKISE004/p/17089756.html
    //树 转化为 二叉树
    BinaryTree CSTree_Transform_to_BinaryTree(CSTree ct){
    if(!ct) return NULL;
    BinaryTree T = (BinaryTree)malloc(sizeof(BiNode));
    T->data = ct->data;
    //相当于将left变为firstchild, 将right变为nextsibling 本质的形态没有改变
    T->leftchild = CSTree_Transform_to_BinaryTree(ct->firstchild);
    T->rightchild = CSTree_Transform_to_BinaryTree(ct->nextsibling);
    return T;
    }

    森林转换为二叉树

    (1)将森林中的每棵树转换成相应的二叉树;

    (2)每棵树的根视为兄弟关系,加上连线;

    (3)以第一棵树的根为轴心顺时针旋转45°。

    以下代码引用我大佬同学:作者:Amαdeus,出处:https://www.cnblogs.com/MAKISE004/p/17089756.html
    //森林 转化为 二叉树
    BinaryTree Forest_Transform_to_BinaryTree(CSTree ct[], int low, int high){
    if(low > high) return NULL;
    //每个树变成二叉树
    BinaryTree T = CSTree_Transform_to_BinaryTree(ct[low]);
    //通过rightchild连接每一个二叉树的根节点
    T->rightchild = Forest_Transform_to_BinaryTree(ct, low + 1, high);
    return T;
    }

    树、森林的遍历

    树的遍历

    • 先根遍历。若树非空,则按如下规则遍历:

      • 先访问根节点
      • 再依次遍历根节点的每棵子树,遍历子树时仍遵循先根后子树的规则
    • 后根遍历。若树非空,则按如下规则遍历:

      • 先依次遍历根节点的每棵子树,遍历子树时仍遵循先子树后根的规则
      • 再访问根节点

    树的先根遍历与对应二叉树的先序序列相同,树的后根遍历与对应二叉树的中序序列相同。

    森林的遍历

    • 先序遍历森林。若森林非空,则按如下规则遍历:

      • 访问森林中第一棵树的根节点
      • 先序遍历第一棵树中根节点的子树森林
      • 先序遍历除去第一棵树之后剩余的树构成的森林
    • 中序遍历森林。若森林非空,则按如下规则遍历:

      • 中序遍历森林中第一棵树的根节点的子树森林
      • 访问第一棵树的根节点
      • 中序遍历粗去第一棵树之后剩余的树构成的森林

    树与二叉树的应用

    哈夫曼树和哈夫曼编码

    几个概念

    • 路径:树中一个节点到另一个节点之间的分支构成

    • 路径长度:路径上的分支数目

    • 权:树中节点被赋予的一个表示某种意义的数值

    • 带权路径长度:从树的根到一个节点的路径长度与该节点上权值的乘积

      WPL=i=1nwili

      其中,wi是第i个叶节点所带的权值,li是该叶节点到根节点的路径长度

    在含有n个带权叶节的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树

    下面看个算法:递归求WPL

    int getWPL(struct TreeNode *root, int depth) {
    if (root == NULL) { // 如果节点为空,返回0
    return 0;
    }
    if (root->left == NULL && root->right == NULL) { // 如果节点是叶子节点,返回带权路径长度
    return depth * root->val;
    }
    // 如果节点不是叶子节点,递归计算左子树和右子树的WPL,并相加返回
    return getWPL(root->left, depth + 1) + getWPL(root->right, depth + 1);
    }

    接下来咱们举个栗子,来看一下哈夫曼编码

    image

    看题:来自北邮考研机试

    3531. 哈夫曼树 - AcWing题库

    题解:

    // 优先队列求哈夫曼树最短带权路径长度
    #include
    using namespace std;
    int main() {
    int n;
    cin >> n;
    priority_queue<int, vector<int>, greater<int> > q;
    for(int i = 0; i < n; i ++) {
    int x;
    cin >> x;
    q.push(x);
    }
    int ans = 0;
    while(q.size() > 1) {
    int t1 = q.top();
    q.pop();
    int t2 = q.top();
    q.pop();
    q.push(t1 + t2);
    ans += t1 + t2;
    }
    cout << ans << endl;
    return 0;
    }

    并查集

    这里不想做太多解释,我们看一下y总的模版(写的时候已经快要零点了,第二天还要早起)(PS:第二天果然多睡了半个小时)

    (1)朴素并查集:
    int p[N]; //存储每个点的祖宗节点
    // 返回x的祖宗节点
    int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
    }
    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;
    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    (2)维护size的并查集:
    int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
    // 返回x的祖宗节点
    int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
    }
    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) {
    p[i] = i;
    size[i] = 1;
    }
    // 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);
    (3)维护到祖宗节点距离的并查集:
    int p[N], d[N];
    //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
    // 返回x的祖宗节点
    int find(int x) {
    if (p[x] != x) {
    int u = find(p[x]);
    d[x] += d[p[x]];
    p[x] = u;
    }
    return p[x];
    }
    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) {
    p[i] = i;
    d[i] = 0;
    }
    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
    作者:yxc
    链接:https://www.acwing.com/blog/content/404/
    来源:AcWing
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    836. 合并集合 - AcWing题库

    #include
    using namespace std;
    const int N = 100010;
    int p[N];//定义多个集合
    int find(int x) {
    if(p[x] != x) p[x] = find(p[x]);
    /*
    经上述可以发现,每个集合中只有祖宗节点的p[x]值等于他自己,即:
    p[x]=x;
    */
    return p[x];
    //找到了便返回祖宗节点的值
    }C
    int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i ++) p[i]=i;
    while(m --) {
    char op[2];
    int a, b;
    scanf("%s%d%d", op, &a, &b);
    if(*op == 'M') p[find(a)] = find(b);//集合合并操作
    else
    if(find(a)==find(b))
    //如果祖宗节点一样,就输出yes
    printf("Yes\n");
    else
    printf("No\n");
    }
    return 0;
    }
  • 相关阅读:
    超级加速器链接促进会(UALink)能否打破英伟达的垄断?
    SpringAOP前备知识
    360杀毒卸载办法
    【Django 05】Django-DRF(ModelViewSet)、路由组件、自定义函数
    uniapp 动态切换应用图标、名称插件(如新年、国庆等) Ba-ChangeIcon
    rust错误处理
    网络安全(黑客)小白学习笔记
    星辰天合联合星环科技完成互认证 共同打造更有生命力的大数据存算解决方案
    假冒 Skype 应用程序网络钓鱼分析
    ArmV8常用汇编指令
  • 原文地址:https://www.cnblogs.com/wanyy-home/p/18053227