ffmpeg 二叉树代码测试及分析 - 详解


author: hjjdebug
date : 2025年 11月 20日 星期四 08:57:46 CST
description: ffmpeg 二叉树代码测试及分析


1 二叉树的概念.

二叉树每一个结点有2个孩子结点, 直到叶子结点.
叶子结点有2个指针为空的孩子结点.
左支的孩子结点,其key值小于父结点,右值的孩子结点,其key值大于父结点.

2 平衡的二叉树.

对所有结点,左右子树高度差不超过1 的二叉树叫平衡的二叉树.

3 考察3个结点的二叉树

考察3个结点构成的二叉树. 是的,树的结点数可以很多, 但3个结点是它的基本结构,
搞清楚了3个结点, 就基本搞清楚了树.

3.1 平衡的二叉树

3个结点, 平衡的2叉树结构只有一种. 它们每个结点高度差为0,如图示:
在这里插入图片描述

3.2 不平衡的二叉树

不平衡的2叉数有4种, 如图示. 它们高度差为2,所以是不平衡的.
根据路径,可称之为LL,LR,RL,RR, 其中LL与RR 对称, LR 与 RL 对称
在这里插入图片描述

它们的共同特点是, 首个结点不是中结点,二是大结点或者小结点,不符合平衡树第一个结点是中结点的特点.

3.3 不平衡2叉数的调整

由于LL与RR 对称, LR 与 RL 对称, 所以4种只要搞懂了2种就够了.
LL 型的调整. 只要把中结点向上提一级,让大结点做它的右孩子就可以了, 看起来就像把顶部的大结点向右旋转了一下.
LR 型的调整. 此时的中结点是新加入的结点,它需要提高2级到第1结点位置,然后把小节点作为左支,把大节点作为右支.
看起来已不是简单的旋转了, 不过也仍然只是简单的调整.

4 ffmpeg 二叉树测试实例

ffmpeg 中实现了一种平衡的2叉树(AVL树), 我们看看它的使用方法
它的调用参数有点奇特,是为了减少接口所特意设计的.
测试用例如下. 测试代码把av_tree_insert()函数挪到了本地方便注释,调试

$ cat main.c
#include "libavutil/mem.h"
#include "libavutil/tree.h"
#include <stdint.h>
  #include <stdio.h>
    //技巧高能够使代码简洁, 可是阅读起来就比较吃力了, 写代码还是应该以易读为主要原则!!
    //不过技巧好的也应该阅读
    typedef struct AVTreeNode
    {
    struct AVTreeNode* child[2]; //child[2] 是二叉树节点的标准形式
    void* elem; // 这里的elem 指针保留的是传入的 data,10,20,30之类的数据
    int state; // state 保存的是右子树减左子树的差值,层高差值
    } AVTreeNode;
    static void print(AVTreeNode* t, int depth)
    {
    int i;
    // depth 负责打印缩进,可以分清父节点和子节点
    for (i = 0; i < depth * 4; i++)
    printf(" ");
    if (t)
    {
    printf("Node %p %2d %ld\n", t, t->state, (long int)t->elem);
    print(t->child[0], depth + 1);
    print(t->child[1], depth + 1);
    }
    else //节点指针为空时,打印NULL
    printf("NULL\n");
    }
    // 这里的指针a, 指针b, 就是传入的数据. b是待插入的节点即key 值
    static int cmp(const void* a, const void* b)
    {
    return (const uint8_t*)a - (const uint8_t*)b;
    }
    void* av_tree_insert(AVTreeNode** tp, void* key,
    int (*cmp)(const void* key, const void* b), AVTreeNode** next)
    {
    // *tp, t 是顶部节点指针
    AVTreeNode* t = *tp;
    if (t) // top节点不为空的情况,要继续查找位置
    { // 例如考虑v 比较为负值,向右child插入的情景
    unsigned int v = cmp(t->elem, key);
    void* ret;
    if (!v) //比较结果为相等
    {
    if (*next) // 如果*next 有值,就不用插入了,直接返回t->elem值
    return t->elem;
    else if (t->child[0] || t->child[1])
    {  //如果该节点下有子结点,
    int i = !t->child[0];//child[0]不为空,则i=0,从左支查找,否则,i=1从右支查找
    void* next_elem[2];
    av_tree_find(t->child[i], key, cmp, next_elem);
    //这里修改了key 和 t->elem,把自己伪装成下一个元素, 后续执行insert从而把该节点删除,并调整了树平衡
    key = t->elem = next_elem[i];
    v = -i;
    }
    else
    { // *next 为空,是要删除包含key的节点,那就把节点返回去
    *next = t;
    *tp = NULL; //把节点指针变成NULL,这是节点没有左右子节点的情况
    return NULL;
    }
    }
    // v>>31 v=正数,v>>31=0, v=负数, v>>31=1, v是比较的返回值
    // 先考察v为负值,向右边找的情景,递归会找到空子节点
    ret = av_tree_insert(&t->child[v >> 31], key, cmp, next); // 这里递归调用
    // 下面代码时递归返回的代码
    if (!ret)
    { // 返回NULL 是新节点已插入
    int i = (v >> 31) ^ !!*next; // 比较为负值且next为空,右子树在增高i=1
    AVTreeNode** child = &t->child[i]; // child 是我们关注的那个child
    t->state += 2 * i - 1; // 更新top节点高度差,可能加1或者减1
    // 判定top 节点是否需要调整
    if (!(t->state & 1)) // 当t->state 为偶数时
    {
    if (t->state)// 当t->state为真时, 例如t->state==2, 就需要调整节点了
    {
    //调整节点,我宁愿分四种情况写, 可是它按2种情况混合着写,代码短了点,但很难读
    //因为首先要确定这个i
    //问题2: (*child)->state是什么时候改变的?也是在71行t->state处改变的
    // child 也会在递归中做顶部节点,递归函数好难调试,算法混在一块也不清晰,只是为了简洁
    if ((*child)->state * 2 == -t->state) //例如RL类型, t->state==2,(*child)->state==-1
    {  //2叉树是对指针操作的考验!
    *tp = (*child)->child[i ^ 1]; //child 的左支做顶层,提升2级
    (*child)->child[i ^ 1] = (*tp)->child[i];
    (*tp)->child[i] = *child;  //把child 做顶层右支
    *child = (*tp)->child[i ^ 1]; //顶层左支送child
    (*tp)->child[i ^ 1] = t; //旧顶层做新顶层左支
    //调整新顶层及其2个child 的层高差. 写法有点难读!
    (*tp)->child[0]->state = -((*tp)->state > 0);
    (*tp)->child[1]->state = (*tp)->state < 0;
    (*tp)->state = 0;
    }
    else
    { // 例如单调递增代码,i为1, RR调整
    //						例如10,20,30中,10为t,20为child,30为child[1], *tp为新top,指向20
    *tp = *child; // child 提高1层, 送新top
    *child = (*child)->child[i ^ 1]; // child 左支送child
    (*tp)->child[i ^ 1] = t; // 原来的top送新top(*tp) 的左支
    //更新state
    if ((*tp)->state) // 根据新tp, 调整原始top的state
    t->state = 0;
    else
    t->state >>= 1;
    (*tp)->state = -t->state; // 设置新top的state
    }
    }
    }
    //比较的是(*tp)->state 布尔值与 *next 布尔值相同返回1,例如state==0,*next==0
    if (!(*tp)->state ^ !!*next)
    return key; // 从这里返回,代表什么? 代表不用调整节点
    }
    return ret; // 这个ret 是递归 av_tree_insert 的 ret; 如果为空需要计算层差并判断是否需要调整平衡
    }
    else // 递归后总能找到空子节点 *tp==0
    {
    *tp = *next; // 新节点接到它的空节点处
    *next = NULL; //*next 指针赋空表示已使用,并返回NULL
    if (*tp)
    {
    (*tp)->elem = key; // 并把key值赋值给top节点, 如果不是一招2用,本来上层应该直接赋值的.
    return NULL;
    }
    else
    return key; // 返回是树中节点保留的数值
    }
    }
    // 传入整数数据, 把数据挂到树上, 保持树的平衡.
    void testTree(int* data, int length)
    {
    AVTreeNode *root = NULL, *node = NULL;
    int i;
    for (i = 0; i < length; i++)
    {
    printf("inserting  %4d\n", data[i]);
    // 分配一个结点
    if (!node)
    node = av_tree_node_alloc();
    if (!node)
    {
    printf("Memory allocation failure.\n");
    return;
    }
    // 把数据插入到结点上
    // 这个接口有点怪! 它没有直接把data[i] 赋值给node->element, 而是分开传.反人类.
    // 为什么这样呢? 因为这个insert 也是delete接口, 当node=NULL时, 要delete 数值是data[i]的节点
    av_tree_insert(&root, (void*)(long)data[i], cmp, &node);
    // 第一次root为空, 插完第一个节点, root 指针改变为第一个节点地址
    print(root, 0); // 打印整个树
    }
    av_free(node);
    av_tree_destroy(root);
    }
    void testRemove()
    {
    AVTreeNode *root = NULL;
    AVTreeNode *node = av_tree_node_alloc();
    int i=50;
    av_tree_insert(&root, (void*)(long)i, cmp, &node);
    if(node==NULL)
    node=av_tree_node_alloc();
    int j=100;
    printf("adding %4d\n", j);
    av_tree_insert(&root, (void *)(long)(j), cmp, &node);
    printf("removing %4d\n", i);
    AVTreeNode *node2 = NULL;
    av_tree_insert(&root, (void *)(long)(i), cmp, &node2);
    void *  k = av_tree_find(root, (void *)(long)i, cmp, NULL);
    if (k)
    printf("removal failure %d\n", j);
    av_free(node2);
    }
    int RR_data[] = { 10, 20, 30, 40, 50 }; // 数据是精心准备的,这是单调递增,必需要调整树,左旋使平衡
    int RR_data_size = sizeof(RR_data) / sizeof(RR_data[0]);
    int RL_data[] = { 10, 100, 20, 150, 110 }; // 数据是精心准备的,这是波浪递增,必需要调整树使平衡
    int RL_data_size = sizeof(RL_data) / sizeof(RL_data[0]);
    int main()
    {
    testTree(RR_data, RR_data_size); // 测试单调递增
    //    testTree(RL_data, RL_data_size); //测试波浪递增
    //	testRemove();
    return 0;
    }

4.1 执行结果:

单调递增测试:

$ ./t2
inserting    10
Node 0x5da73a56e040  0 10
NULL
NULL
inserting    20
Node 0x5da73a56e040  1 10
NULL
Node 0x5da73a56e0c0  0 20
NULL
NULL
inserting    30
Node 0x5da73a56e0c0  0 20
Node 0x5da73a56e040  0 10
NULL
NULL
Node 0x5da73a56e140  0 30
NULL
NULL
inserting    40
Node 0x5da73a56e0c0  1 20
Node 0x5da73a56e040  0 10
NULL
NULL
Node 0x5da73a56e140  1 30
NULL
Node 0x5da73a56e1c0  0 40
NULL
NULL
inserting    50
Node 0x5da73a56e0c0  1 20
Node 0x5da73a56e040  0 10
NULL
NULL
Node 0x5da73a56e1c0  0 40
Node 0x5da73a56e140  0 30
NULL
NULL
Node 0x5da73a56e240  0 50
NULL
NULL

波浪递增测试:

./t2
inserting    10
Node 0x57ec61470040  0 10
NULL
NULL
inserting   100
Node 0x57ec61470040  1 10
NULL
Node 0x57ec614700c0  0 100
NULL
NULL
inserting    20
Node 0x57ec61470140  0 20
Node 0x57ec61470040  0 10
NULL
NULL
Node 0x57ec614700c0  0 100
NULL
NULL
inserting   150
Node 0x57ec61470140  1 20
Node 0x57ec61470040  0 10
NULL
NULL
Node 0x57ec614700c0  1 100
NULL
Node 0x57ec614701c0  0 150
NULL
NULL
inserting   110
Node 0x57ec61470140  1 20
Node 0x57ec61470040  0 10
NULL
NULL
Node 0x57ec61470240  0 110
Node 0x57ec614700c0  0 100
NULL
NULL
Node 0x57ec614701c0  0 150
NULL
NULL

5 ffmpeg 二叉树算法代码.

ffmpeg 中的代码简洁干练,所以我特别标注了它的insert算法. 其它函数比较简单
把av_tree_insert() 挪到了用户的代码空间, 方便调试和注释.

5.1 av_tree_insert() 函数调用参数

void* av_tree_insert(AVTreeNode** tp, void* key,
int (cmp)(const void key, const void* b), AVTreeNode** next)

第一参数: top顶层node指针的地址, 意味着执行完插入,这个地址可能会改变.
第二参数: 插入的数据,
第三参数: 用户自定义的比较算法,传参a,是内部节点的数据, 传参b,就是用户待插入的数据key了.
第四参数: 这是个容易误解的东西. 如果next 为真, 是把key 插入到树,
如果
next为NULL,是把包含key值的节点从树中删除.

5.2 程序技巧

  1. 删除节点时, 如果该节点有子节点, 它把自己的值先改成叶子结点的值,把key值也改成叶子节点的值,
    继续执行av_tree_insert, 引起比较返回0值,从而删除该节点并调整了树平衡.
  2. 维持树平衡代码它把4种情况按2种情况来写,用变量i来区分左右,代码简洁但逻辑有点难读.
    除了看代码注释还是要调试才能理解代码. 标注信息都写在测试代码中了,就不再这里赘述了.
posted @ 2026-01-22 22:54  gccbuaa  阅读(0)  评论(0)    收藏  举报