数据结构和算法——用C语言实现所有树形结构及相关算法

news2024/11/19 15:23:32

文章目录

  • 前言
  • 树和森林基础概念
  • 二叉树
    • 二叉树的遍历
    • 二叉树的构造
    • 树和森林与二叉树之间的转化
    • 树和森林的遍历
  • 满二叉树
  • 完全二叉树
  • 线索二叉树
    • 线索二叉树的构造
    • 寻找前驱和后继
    • 线索二叉树的遍历
  • 最优二叉树(哈夫曼树)
    • 哈夫曼树的构造
    • 哈夫曼编码
  • 二叉排序树(BST)
    • 二叉排序树的插入
    • 二叉排序树的构造
    • 二叉排序树的查找
    • 二叉排序树的删除
  • 平衡二叉树(AVL)
    • 平衡二叉树失衡调整
    • 平衡二叉树的插入
    • 平衡二叉树的删除
  • 红黑树
    • 红黑树的插入
    • 红黑树的删除
  • B树
  • B+树

前言

本文所有代码均在仓库中,这是一个完整的由纯C语言实现的可以存储任意类型元素的数据结构的工程项目。

在这里插入图片描述

  • 首先是极好的工程意识,该项目是一个中大型的CMake项目,结构目录清晰,通过这个项目可以遇见许多工程问题并且可以培养自己的工程意识。
  • 其次是优秀的封装性(每个数据结构的头文件中只暴漏少量的信息),以及优秀的代码风格和全面的注释,通过这个项目可以提升自己的封装技巧:

在这里插入图片描述

  • 异常处理功能:在使用C语言编写代码的时候不能使用类似Java的异常处理机制是非常难受的,所以我也简单实现了一下。详情可看在C语言中实现类似面向对象语言的异常处理机制

在这里插入图片描述

最后也是最重要的一点,数据结构的通用性和舒适的体验感,下面以平衡二叉树为例:

  • 第一步:要想使用平衡二叉树,只需要引入其的头文件:
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"
  • 第二步:定义自己任意类型的数据,并构造插入数据(以一个自定义的结构体为例):
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"

int dataCompare(void *, void *);

typedef struct People {
    char *name;
    int age;
} *People;

int main(int argc, char **argv) {
    struct People dataList[] = {
            {"张三", 15},
            {"李四", 3},
            {"王五", 7},
            {"赵六", 10},
            {"田七", 9},
            {"周八", 8},
    };
    BalancedBinaryTree tree = balancedBinaryTreeConstructor(NULL, 0, dataCompare);
    for (int i = 0; i < 6; ++i) {
        balancedBinaryTreeInsert(&tree, dataList + i, dataCompare);
    }
    return 0;
}

/**
 * 根据人的年龄比较
 */
int dataCompare(void *data1, void *data2) {
    int sub = ((People) data1)->age - ((People) data2)->age;
    if (sub > 0) {
        return 1;
    } else if (sub < 0) {
        return -1;
    } else {
        return 0;
    }
}
  • 第三步:打印一下平衡二叉树:
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"

int dataCompare(void *, void *);

void dataPrint(void *);

typedef struct People {
    char *name;
    int age;
} *People;

int main(int argc, char **argv) {
    struct People dataList[] = {
            {"张三", 15},
            {"李四", 3},
            {"王五", 7},
            {"赵六", 10},
            {"田七", 9},
            {"周八", 8},
    };
    BalancedBinaryTree tree = balancedBinaryTreeConstructor(NULL, 0, dataCompare);
    for (int i = 0; i < 6; ++i) {
        balancedBinaryTreeInsert(&tree, dataList + i, dataCompare);
        balancedBinaryTreePrint(tree, dataPrint);
        printf("-------------\n");
    }
    return 0;
}

/**
 * 根据人的年龄比较
 */
int dataCompare(void *data1, void *data2) {
    int sub = ((People) data1)->age - ((People) data2)->age;
    if (sub > 0) {
        return 1;
    } else if (sub < 0) {
        return -1;
    } else {
        return 0;
    }
}

/**
 * 打印人的年龄
 * @param data
 */
void dataPrint(void *data) {
    People people = (People) data;
    printf("%d", people->age);
}

打印的结果如下:

在这里插入图片描述
最后期待大佬们的点赞。

树和森林基础概念

树是由 n n n个结点组成的有限集,其中每个结点有零个或多个子结点以及零个或一个父结点,没有父结点的结点称为根结点,每一个非根结点有且只有一个父结点。除根结点外的其余结点可以分为 m m m个互不相交的有限集,其中每个有限集本身就是一棵树,这些树又称为根的子树森林就是 m m m棵不相交的树的集合,树一定是森林,但森林不一定是树。

  • 结点间的关系:A是E的祖先,E是A的子孙,A是B的双亲,B是A的孩子,有相同双亲的结点称为兄弟
  • 度:一个结点的孩子数称为该结点的度,树中结点最大的度称为树的度。度数为零的结点称为叶子结点,度不为零的结点称为分支结点
    • 树中的结点数就等于所有结点的度数之和加一。
    • 度为 m m m的树第 i i i层最多有 m i − 1 m^{i-1} mi1个结点。
  • m m m叉树:任意结点的度小于 m m m的树
  • 层次:结点的层次从根结点开始定义,在同一层的非兄弟结点互为堂兄弟。
  • 深度:结点的深度从根结点开始向下逐层累加,树的深度就是结点的最大层数。
  • 高度:结点的高度从叶子节点开始向上逐层累加,树的高度就是结点的最大层数。
    • 高度为 h h h m m m叉树至多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m1mh1个结点,最少有 h h h个结点。
    • 高度为 h h h、度为 m m m的树至少有 h + m − 1 h+m-1 h+m1个结点。
    • 具有 n n n个结点的 m m m叉树的最小高度为 ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1)\rceil logm(n(m1)+1)⌉
  • 结点的间的路径和路径长度:结点间的路径由这两个结点之间所经过的结点序列构成;路径长度是路径上所经过的边的条数。树的路径长度为根到每个结点的路径长度之和。
  • 有序树和无序树:结点的每个子树是有序的不能互换的树称为有序树,否则就是无序树。

在这里插入图片描述

树一般有以下三种存储方式:

  • 顺序存储:双亲表示法
  • 顺序+链式存储:孩子表示法
  • 链式存储:孩子兄弟表示法

双亲表示法采用一组连续的空间存储每一个结点,同时在每个结点中增设一个伪指针,这个伪指针指向其双亲结点在连续空间中的位置:

在这里插入图片描述

struct ParentTreeNode {
    void *data;
    int parent;
};

struct ParentTree {
    struct ParentTreeNode *nodeList;
    int size;
    int nodeCount;
};

在孩子表示法中,每个结点不再增设一个伪指针,而是增设一个存储当前结点孩子下标的链表:

在这里插入图片描述

struct ChildTreeNode {
    void *data;
    int index; //表示当前接结点在数组中的位置,仅在链表中使用
    struct ChildTreeNode *child;
};

struct ChildTree {
    struct ChildTreeNode *nodeList;
    int size;
    int nodeCount;
};

孩子兄弟表示法又称为二叉树表示法,它采用二叉链表作为树的存储结构,其中每个结点包含三部分内容:结点值、指向第一个孩子结点的指针以及指向下一个兄弟结点的指针:

在这里插入图片描述

struct ChildSiblingTreeNode {
    void *data;
    struct ChildSiblingTreeNode *firstChild;
    struct ChildSiblingTreeNode *nextSibling;
};

二叉树

二叉树是指每个结点最多只有两棵子树且子树之间顺序不能颠倒的树,这两棵子树分别称为左子树和右子树。二叉树与度为 2 2 2的有序树的区别如下:

  • 度为 2 2 2的有序树至少有三个结点,而二叉树可以为空树。
  • 度为 2 2 2的有序树的孩子的左右次序是相对于另一个孩子而言的,如果只有一个孩子那么无需区分左右,而二叉树无论几个孩子必须区分左右。

二叉树的性质如下:

  • 二叉树在第 i i i层至多有 2 i − 1 2^{i-1} 2i1个结点,最少有 1 1 1个结点。

  • 高度为 h h h的二叉树最多有 2 h − 1 2^h-1 2h1个结点。

  • 设非空二叉树中度为 0 、 1 、 2 0、1、2 012的结点的个数分别为 n 1 、 n 2 、 n 3 n_1、n_2、n_3 n1n2n3,设树中结点的总个数为 n n n,则

    • n = n 0 + n 1 + n 2 n=n_0+n_1+n_2 n=n0+n1+n2
    • n = n 1 + 2 n 2 + 1 n=n_1+2n_2+1 n=n1+2n2+1
    • n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
  • 对于任意一颗二叉树,如果对其进行编号,那么结点编号所对应的二进制可以反应从根节点到该结点的路径,其中最高位是 1 1 1,其余各位从高到低表示从根节点到第 k k k个节点的移动方向, 0 0 0表示移动到左子节点, 1 1 1表示移动到右子节点。

在这里插入图片描述

二叉树的顺序存储是指用一组地址连续的存储单元依次自上而下、自左向右存储完全二叉树上的结点元素,这样树中结点的序号就可以唯一反应结点之间的关系。对于不是完全二叉树的二叉树,需要填充空结点让其每个结点与完全二叉树上的结点对应。因此非完全二叉树的二叉树通常采用链式存储,链表结点至少包含三个域:数据域、左孩子指针域和右孩子指针域。还可以增加一个指针域指向其双亲。

struct BinaryTreeNode {
    void *data;
    struct BinaryTreeNode *lNode;
    struct BinaryTreeNode *rNode;
};

二叉树的遍历

遍历一棵二叉树要决定对根节点N、、左子树L和右子树R的访问顺序,常见的访问顺序如下:

  • 先序遍历(NLR):首先访问根结点,然后遍历左子树,最后遍历右子树。
void preOrder(BinaryTree tree, LinkedQueue preOrderList) {
    if (tree != NULL) {
        linkedQueueEnQueue(preOrderList, tree->data);
        preOrder(tree->lNode, preOrderList);
        preOrder(tree->rNode, preOrderList);
    }
}
  • 中序遍历(LNR):先遍历左子树,然后访问根结点,然后遍历右子树。
void inOrder(BinaryTree tree, LinkedQueue inOrderList) {
    if (tree != NULL) {
        inOrder(tree->lNode, inOrderList);
        linkedQueueEnQueue(inOrderList, tree->data);
        inOrder(tree->rNode, inOrderList);
    }
}
  • 后序遍历(LRN):是先遍历左子树,然后遍历右子树,最后访问根结点。
void postOrder(BinaryTree tree, LinkedQueue postOrderList) {
    if (tree != NULL) {
        postOrder(tree->lNode, postOrderList);
        postOrder(tree->rNode, postOrderList);
        linkedQueueEnQueue(postOrderList, tree->data);
    }
}
  • 层序遍历:根据二叉树的层次层次结构,从第一层开始自上而下、自左向右访问每一个结点。
void levelOrder(BinaryTree tree, LinkedQueue levelOrderList) {
    LinkedQueue queue = linkedQueueConstructor();
    linkedQueueEnQueue(queue, tree);
    while (!linkedQueueIsEmpty(queue)) {
        BinaryTreeNode *node = linkedQueueDeQueue(queue);
        linkedQueueEnQueue(levelOrderList, node->data);
        if (node->lNode != NULL) {
            linkedQueueEnQueue(queue, node->lNode);
        }
        if (node->rNode != NULL) {
            linkedQueueEnQueue(queue, node->rNode);
        }
    }
}

二叉树的构造

由二叉树的以下遍历序列组合可以唯一确认一棵二叉树:

  • 先序+中序
  • 后序+中序
  • 层序+中序
BinaryTree binaryTreePreInOrderConstructor(void **preOrderList, void **inOrderList, int (*compare)(void *, void *), int ps, int pe, int is, int ie) {
    BinaryTreeNode *root = malloc(sizeof(BinaryTreeNode));
    root->data = *(preOrderList + ps);
    int lLen, rLen;
    for (int i = is; i <= ie; ++i) {
        if (compare(*(inOrderList + i), *(preOrderList + ps))) {
            lLen = i - is;
            rLen = ie - i;
            break;
        }
    }
    if (lLen) {
        root->lNode = binaryTreePreInOrderConstructor(preOrderList, inOrderList, compare, ps + 1, ps + lLen, is, is + lLen - 1);
    } else {
        root->lNode = NULL;
    }
    if (rLen) {
        root->rNode = binaryTreePreInOrderConstructor(preOrderList, inOrderList, compare, pe - rLen + 1, pe, ie - rLen + 1, ie);
    } else {
        root->rNode = NULL;
    }
    return root;
}

如果将二叉树的某一遍历序列使用填充元素填充为满二叉树的相同遍历序列,那么也可以唯一确定一棵二叉树:

BiTree  completeLevelOrderConstruct(BiTreeElemType completeLevelOrderList[],int len,BiTreeElemType filler){
    LinkQueue queue= linkQueueConstruct();
    BiTNode * root= malloc(sizeof (BiTNode));
    root->data=completeLevelOrderList[0];
    enQueue(queue,root);
    for (int i = 1; !isEmpty(queue); ++i) {
        BiTNode * node=deQueue(queue);
        if(node!=NULL){
            if((2*1-1)>=len||(2*i+1-1)>=len){
                node->lChild=NULL;
                node->rChild=NULL;
                continue;
            }
            if(completeLevelOrderList[2*i-1]!=filler){
                node->lChild= malloc(sizeof (BiTNode));
                node->lChild->data=completeLevelOrderList[2*i-1];
                enQueue(queue,node->lChild);
            } else{
                node->lChild=NULL;
                enQueue(queue,NULL);
            }
            if(completeLevelOrderList[2*i+1-1]!=filler){
                node->rChild= malloc(sizeof (BiTNode));
                node->rChild->data=completeLevelOrderList[2*i+1-1];
                enQueue(queue,node->rChild);
            } else{
                node->rChild=NULL;
                enQueue(queue,NULL);
            }
        } else{
            continue;
        }
    }
    linkQueueFinalize(queue);
    return root;
}

树和森林与二叉树之间的转化

由于二叉树的结构简单、规律性最强,因此在处理树和森林时一般将它们转换成一个唯一的二叉树进行。由于树和二叉树都可以使用二叉链表表示,那么就可以以二叉链表做媒介来进行转化。

  • 树转换为二叉树:每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟,由于根节点没有兄弟,所以对应的二叉树没有右子树。

  • 树转换为二叉树的画法:在兄弟结点之间加一条线;对每一个结点,只保留它与第一个孩子的连线,与其它孩子的连线全部抹掉;以树根为中心顺时针旋转四十五度。

  • 森林转换为二叉树:先将森林中的每一棵树转换为二叉树,然后将第二棵二叉树作为第一棵二叉树的右子树,再将第三棵二叉树作为第二棵二叉树的右子树···依次类推。

  • 森林转换为二叉树的画法:将森林中的每一棵树转换为二叉树;每棵树的根视为兄弟关系,在每棵树的根之间加一条连线;以第一棵树的根为中心顺时针旋转四十五度。

  • 二叉树转换为森林:将二叉树的根及左子树视为第一棵树的二叉树形式,并将右链断开;将右子树视为一个由除第一棵树外的森林转换后的二叉树,应用相同的方法,直到最后只剩一棵没有右子树的二叉树为止;最后将每棵二叉树依次转换为树就得到了森林。

树和森林的遍历

树的遍历方式如下:

  • 先根遍历:先访问根结点,再依次访问根结点的每棵子树,依次类推。其遍历序列对应二叉树的先序序列。
  • 后根遍历:先访问根结点的每棵子树,再访问根结点,依次类推。其遍历序列对应二叉树的中序序列。
  • 层序遍历:按层次访问。

森林的遍历方式如下:

  • 先根遍历:访问森林中第一棵树的根节点;先根遍历第一棵树中根结点的子树森林;先根遍历除去第一棵树之后剩余的树构成的森林;其遍历序列对应二叉树的先序序列。
  • 后根遍历:后根遍历森林中第一棵树的根结点的子树森林;访问第一棵树根结点;后根遍历除第一棵树之后剩余的树构成的森林;其遍历序列对应二叉树的中序序列。
  • 层序遍历:按层次遍历。

满二叉树

每一层的结点数都达到最大值的二叉树称为满二叉树,满二叉树的性质如下:

  • 所有叶子节点都在最后一层。
  • 一棵高为 h h h的满二叉树的结点数为 2 h − 1 2^h-1 2h1
  • 不存在度为 1 1 1的结点。
  • 若对满二叉树自上而下,自左到右从 1 1 1开始编号,那么对于编号为 i i i的结点,若有双亲则其双亲编号为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor i/2,若有左孩子则左孩子编号为 2 i 2i 2i,若有右孩子则其编号为 2 i + 1 2i+1 2i+1

在这里插入图片描述

完全二叉树

一棵深度为 k k k的有 n n n个结点的二叉树,对树中的结点自上而下,自左到右从 1 1 1开始编号,如果编号为 i i i的结点与满二叉树中编号为 i i i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。完全二叉树的性质如下:

  • i < = ⌊ n / 2 ⌋ i<=⌊n/2⌋ i<=n/2,那么结点 i i i为分支结点,否则为叶子结点。
  • 叶子节点只可能在层次最大的两层上出现,对于最大层次中的叶子结点都依次排列在该层的最左边的位置。
  • 若有度为 1 1 1的结点,则只可能有一个,且该结点只有左孩子而没有右孩子。
  • 一但某结点 i i i为叶子结点或只有左孩子,则编号大于 i i i的结点均为叶子结点。
  • n n n为奇数,则每个分支结点都有左孩子和右孩子;若 n n n为偶数,则编号最大的分支结点只有左孩子没有右孩子,其余分支结点左孩子右孩子都有。
  • i i i为偶数时,其双亲的编号为 i / 2 i/2 i/2,它是双亲的左孩子;当 i i i为奇数时,其双亲的编号为 ( i − 1 ) / 2 (i-1)/2 (i1)/2,它是双亲的右孩子。
  • 2 i < = n 2i<=n 2i<=n时,结点i的左孩子编号为 2 i 2i 2i,否则无左孩子。
  • 2 i + 1 < = n 2i+1<=n 2i+1<=n时,结点i的右孩子编号为 2 i + 1 2i+1 2i+1,否则无右孩子。
  • 结点 i i i所在的层次为 ⌊ l o g 2 i ⌋ + 1 ⌊log₂i⌋+1 log2i+1
  • 具有 n n n个结点的完全二叉树的高度为 ⌈ l o g 2 ( n + 1 ) ⌉ ⌈log₂(n+1)⌉ log2(n+1)⌉ ⌊ l o g 2 n ⌋ + 1 ⌊log₂n⌋+1 log2n+1

在这里插入图片描述

线索二叉树

在含有 n n n个结点的二叉树中,含有 n + 1 n+1 n+1个空指针域。线索化是指利用这些空指针域存放指向结点的前驱和后继的指针,它的规则如下:

  • 如果某个结点的左孩子为空,则将空的左孩子引用指向其前驱;
  • 如果某结点的右孩子为空,则将空的右孩子引用指向其后驱。

此外还需要增加两个标志位标识指针域是指向左右孩子还是指向前驱后继,最好再添加一个指针域指向其父结点。

struct ThreadedBinaryTreeNode {
    void *data;
    ThreadedBinaryTreeNode *lNode;
    ThreadedBinaryTreeNode *rNode;
    ThreadedBinaryTreeNode *pNode;
    bool lIsPredecessor;
    bool rIsSuccessor;
};

在这里插入图片描述

线索二叉树的构造

构造一棵线索二叉树分为两步:

  • 首先使用普通二叉树的构造方法构造一棵ThreadTree
  • 然后使用线索化方法将ThreadTree转换成一个真正的线索二叉树

以前序线索化为例,它的算法思想为:

  • 本质是一次前序遍历
  • tree指针指向当前访问的结点,predecessor指针指向tree的前驱
  • 在遍历时
    • 检查tree的左指针是否为空,若为空则将其指向predecessor
    • 检查predecessor 的右指针是否为空,若为空则将其指向tree
/**
 * 先序线索化
 * @param tree
 * @param predecessor
 * @return
 */
ThreadedBinaryTree preOrderThreaded(ThreadedBinaryTree tree, ThreadedBinaryTreeNode *predecessor) {
    if (tree != NULL) {
        if (tree->lNode == NULL) {
            tree->lNode = predecessor;
            tree->lIsPredecessor = true;
        }
        if (predecessor != NULL && predecessor->rNode == NULL) {
            predecessor->rNode = tree;
            predecessor->rIsSuccessor = true;
        }
        predecessor = tree;
        if (tree->lIsPredecessor == false) {
            preOrderThreaded(tree->lNode, predecessor);
        }
        if (tree->rIsSuccessor == false) {
            preOrderThreaded(tree->rNode, predecessor);
        }
    }
}

寻找前驱和后继

以前序线序哦二叉树为例,寻找tree结点前驱的算法思想如下:

  • 如果tree->lIsPredecessor == true,则直接返回其左孩子
  • tree的左孩子不指向其前驱:
    • tree是其父结点的左孩子,则直接返回其父结点
    • tree是其父结点的右孩子
      • 若父结点没有左孩子,则直接返回父结点
      • 若父结点有左孩子,则返回父结点左子树中按照前序序列访问到的最后一个结点
/**
 * 寻找前序序列的前驱
 * @param tree
 * @return
 */
ThreadedBinaryTreeNode *preOrderFirstNode(ThreadedBinaryTree tree) {
    if (tree->lIsPredecessor == true) {
        return tree->lNode;
    } else {
        ThreadedBinaryTreeNode *parent = tree->pNode;
        if (parent->lNode == tree) {
            return parent;
        } else {
            if (parent->lIsPredecessor == true || parent->lNode == NULL) {
                return parent;
            } else {
                ThreadedBinaryTreeNode *node = parent->lNode;
                while (node != NULL) {
                    if (node->rNode == tree) {
                        break;
                    } else if (node->rNode != NULL) {
                        node = node->rNode;
                    } else if (node->lIsPredecessor == false && node->lNode != NULL) {
                        node = node->lNode;
                    }
                }
                return node;
            }
        }
    }
}

寻找后继的算法思想如下:

  • 如果tree->rIsSuccessor == true,则直接返回其右孩子
  • tree的右孩子不指向其后继:
    • 如果 tree->lIsPredecessor == false && tree->lNode != NULL,返回其左孩子
    • 否则返回其右孩子
/**
 * 寻找前序序列的后继
 * @param tree
 * @return
 */
ThreadedBinaryTreeNode *preOrderNextNode(ThreadedBinaryTree tree) {
    if (tree->rIsSuccessor == true) {
        return tree->rNode;
    } else {
        if (tree->lIsPredecessor == false && tree->lNode != NULL) {
            return tree->lNode;
        } else {
            return tree->rNode;
        }
    }

线索二叉树的遍历

以前序线序哦二叉树为例,前序线索二叉树中包含了二叉树的前驱和后继的信息,在对其进行遍历时,只需要找到前序序列中的第一个结点,然后依次找结点的后继,直至其后继为空。

/**
 * 线索二叉树前序遍历
 * @param tree
 * @param queue
 */
void threadedBinaryTreePreOrder(ThreadedBinaryTree tree, LinkedQueue queue) {
    for (ThreadedBinaryTreeNode *node = tree; node != NULL; node = preOrderNextNode(node)) {
        linkedQueueEnQueue(queue, node->data);
    }
}

最优二叉树(哈夫曼树)

如果给树中结点赋一个有某种含义的数值,则这个数值称为该结点的。结点的权和根结点到该结点的路径长度的乘积称为结点的带权路径长度,树的带权路径长度是树中所有叶子节点带权路径长度之和。带权路径长度最小的二叉树称为最优二叉树。最优二叉树的特点如下:

  • 包含 n n n个叶子节点的最优二叉树共有 2 n − 1 个 2n-1个 2n1结点。
  • 最优二叉树结点的度要么是 0 0 0要么是 2 2 2
struct HuffmanTreeNode {
    void *data;
    void *weight;
    struct HuffmanTreeNode *lNode;
    struct HuffmanTreeNode *rNode;
};

在这里插入图片描述

哈夫曼树的构造

给定 n n n个结点以及它们的权值,那么构造过程如下:

  • 构造森林全是根:将这 n n n个结点分别作为 n n n棵仅包含一个结点的二叉树,构成森林 F F F
  • 选用两小造新树:构造一个新结点,在 F F F中选择两棵根结点权值最小的树作为新结点的左右子树新结点权值即为两个孩子节点的权值之和。
  • 删除两小添新人:从 F F F中删除刚才选出的两棵树,并将新构造的二叉树加入 F F F
  • 重复2、3剩单根:重复2和3步直至F中只剩一棵树。
/**
 * 构造哈夫曼树
 * @param dataList 数据列表
 * @param weightList 权值列表
 * @param size 大小
 * @param reCompare 权值逆序比较
 * @param weightSum 权值相加
 * @return
 */
HuffmanTree huffmanTreeNodeConstructor(void *dataList[], void *weightList[], int size, int (*reCompare)(void *, void *), void *(*weightSum)(void *, void *)) {
    HuffmanTreeNode *forest[size];
    int treeCount = 0;
    for (int i = 0; i < size; ++i) {
        HuffmanTreeNode *node = malloc(sizeof(HuffmanTreeNode));
        node->data = dataList[i];
        node->weight = weightList[i];
        node->lNode = NULL;
        node->rNode = NULL;
        forest[i] = node;
        treeCount++;
    }
    sorted(forest, size, reCompare);
    for (; treeCount != 1;) {
        HuffmanTreeNode *node = malloc(sizeof(HuffmanTreeNode));
        node->lNode = forest[0];
        forest[0] = NULL;
        treeCount--;
        node->rNode = forest[1];
        forest[1] = NULL;
        treeCount--;
        node->data = NULL;
        node->weight = weightSum(node->lNode->weight, node->rNode->weight);
        forest[0] = node;
        sorted(forest, size, reCompare);
    }
    return forest[0];
}

哈夫曼编码

由哈夫曼树可以得到哈夫曼编码:

  • 首先将一段文本中每个出现的字符当作一个独立的结点构造成哈夫曼树,每个字符都会出现在叶子结点,其权值为它出现的频度;
  • 然后在每条路径上标记 0 0 0 1 1 1,标记为 0 0 0表示转向左孩子,标记为 1 1 1表示转向右孩子。
  • 那么字符的哈夫曼编码就为从根结点到该结点的路径上边标记的 01 01 01序列。
  • 哈夫曼树的带权路径长度就是该文本最终编码得到的二进制编码长度。
/**
 * 是否是叶子结点
 * @param node
 * @return
 */
static bool isLeafNode(HuffmanTreeNode *node) {
    return node->lNode == NULL && node->rNode == NULL;
}

/**
 * 哈夫曼编码
 * @param tree
 * @param target
 * @param compare
 * @return
 */
char *huffmanCoding(HuffmanTree tree, void *target, int (*compare)(void *, void *)) {
    if (tree != NULL) {
        char *lr = huffmanCoding(tree->lNode, target, compare);
        char *rr = huffmanCoding(tree->rNode, target, compare);
        if (lr != NULL) {
            char *code = calloc(strlen(lr) + 2, sizeof(char));
            strcat(code, "0");
            strcat(code, lr);
            return code;
        } else if (rr != NULL) {
            char *code = calloc(strlen(rr) + 2, sizeof(char));
            strcat(code, "1");
            strcat(code, rr);
            return code;
        } else if (tree->lNode != NULL && isLeafNode(tree->lNode) && compare(tree->lNode->data, target) == 0) {
            return "0";
        } else if (tree->rNode != NULL && isLeafNode(tree->rNode) && compare(tree->rNode->data, target) == 0) {
            return "1";
        } else {
            return NULL;
        }
    } else {
        return NULL;
    }
}

二叉排序树(BST)

二叉排序树是满足以下性质的二叉树:

  • 若任意结点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若任意结点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 任意结点的左、右子树也分别为二叉排序树;

对二叉排序树进行中序遍历可以得到一个递增有序的有序序列。

struct SortedBinaryTreeNode {
    void *data;
    SortedBinaryTreeNode *lNode;
    SortedBinaryTreeNode *rNode;
};

二叉排序树的插入

算法思想:

  • 若树为空,则新建根结点
  • 若树非空,则插入值与根结点值比较
    • 若相等则插入失败
    • 若小于根结点值,则插入到左子树
    • 若大于根结点值,则插入到右子树
/**
 * 二叉排序树的插入
 * @param tree
 * @param element
 * @param compare
 * @return
 */
bool sortedBinaryTreeInsert(SortedBinaryTree *tree, void *element, int (*compare)(void *, void *)) {
    if ((*tree) == NULL) {
        *tree = malloc(sizeof(SortedBinaryTreeNode));
        (*tree)->data = element;
        (*tree)->lNode = NULL;
        (*tree)->rNode = NULL;
        return true;
    } else {
        int cmpResult = compare(element, (*tree)->data);
        if (cmpResult == 0) {
            return false;
        } else if (cmpResult > 0) {
            if ((*tree)->rNode == NULL) {
                SortedBinaryTreeNode *node = malloc(sizeof(SortedBinaryTreeNode));
                node->data = element;
                node->rNode = NULL;
                node->rNode = NULL;
                (*tree)->rNode = node;
                return true;
            } else {
                return sortedBinaryTreeInsert(&(*tree)->rNode, element, compare);
            }
        } else {
            if ((*tree)->lNode == NULL) {
                SortedBinaryTreeNode *node = malloc(sizeof(SortedBinaryTreeNode));
                node->data = element;
                node->rNode = NULL;
                node->rNode = NULL;
                (*tree)->lNode = node;
                return true;
            } else {
                return sortedBinaryTreeInsert(&(*tree)->lNode, element, compare);
            }
        }
    }
}

二叉排序树的构造

/**
 * 二叉排序树构造函数
 * @param elementList
 * @param size
 * @param compare
 * @return
 */
SortedBinaryTree sortedBinaryTreeConstructor(void **elementList, int size, int (*compare)(void *, void *)) {
    static SortedBinaryTree tree = NULL;
    for (int i = 0; i < size; ++i) {
        sortedBinaryTreeInsert(&tree, *(elementList + i), compare);
    }
    return tree;
}

二叉排序树的查找

算法思想:

  • 若树非空,则目标值与根结点值比较
  • 若相等则查找成功
  • 若小于根结点值,则在左子树上查找
  • 若大于根结点值,则在右子树上查找
/**
 * 二叉排序树的查找
 * @param tree
 * @param element
 * @param compare
 * @return
 */
SortedBinaryTreeNode *sortedBinaryTreeSearch(SortedBinaryTree tree, void *element, int (*compare)(void *, void *)) {
    if (tree == NULL) {
        return NULL;
    } else {
        int cmpResult = compare(element, tree->data);
        if (cmpResult == 0) {
            return tree;
        } else if (cmpResult > 0) {
            return sortedBinaryTreeSearch(tree->rNode, element, compare);
        } else {
            return sortedBinaryTreeSearch(tree->lNode, element, compare);
        }
    }
}

二叉排序树的删除

算法思想:

  • 首先找到要删除的结点。
  • 若删除结点是叶子结点,则直接删除。
  • 若删除结点只有左子树或者右子树,则让左子树或右子树替代删除结点。
  • 若若删除结点左右子树都存在:
    • 方法一:找到右子树中最小的值替代删除结点的值,然后再删除这个最小值所在的结点。
    • 方法二:找到左子树中最大的值替代删除结点的值,然后再删除这个最大值所在的结点。
/**
 * 删除元素
 * @param tree
 * @param element
 * @param compare
 * @return
 */
bool sortedBinaryTreeDelete(SortedBinaryTree *tree, void *element, int (*compare)(void *, void *)) {
    if (*tree == NULL) {
        return false;
    } else {
        int cmpResult = compare(element, (*tree)->data);
        if (cmpResult == 0) {
            if ((*tree)->lNode == NULL && (*tree)->rNode == NULL) {
                free(*tree);
                *tree = NULL;
                return true;
            } else if ((*tree)->lNode == NULL) {
                SortedBinaryTreeNode *node = (*tree);
                *tree = (*tree)->rNode;
                free(node);
                return true;
            } else if ((*tree)->rNode == NULL) {
                SortedBinaryTreeNode *node = (*tree);
                *tree = (*tree)->lNode;
                free(node);
                return true;
            } else {
                SortedBinaryTreeNode *node = (*tree)->rNode;
                while (node->lNode != NULL) {
                    node = node->lNode;
                }
                (*tree)->data = node->data;
                return sortedBinaryTreeDelete(&(*tree)->rNode, node->data, compare);
            }
        } else if (cmpResult > 0) {
            return sortedBinaryTreeDelete(&(*tree)->rNode, element, compare);
        } else {
            return sortedBinaryTreeDelete(&(*tree)->lNode, element, compare);
        }
    }
}

平衡二叉树(AVL)

结点的平衡因子定义如下:
平衡因子 = 左子树高 − 右子树高 平衡因子=左子树高-右子树高 平衡因子=左子树高右子树高
树中任一结点的平衡因子的绝对值不超过 1 1 1的二叉树称为平衡二叉树,平衡二叉树避免了树高的过度增长,如果可以保证一棵二叉排序树为平衡二叉树,那么就可以减少搜索元素时比较的次数,从而提高二叉排序树的性能。

struct BalancedBinaryTreeNode {
    void *data;
    BalancedBinaryTreeNode *lNode;
    BalancedBinaryTreeNode *rNode;
};

平衡二叉树失衡调整

当对一棵平衡二叉树进行插入或删除操作时,有可能会导致平衡二叉树失衡,此时应该从插入或删除结点往回找到第一个不平衡结点,调整以该结点为根的子树,这棵被调整的树称为最小不平衡子树,只要最小不平衡子树恢复了平衡,那么整棵树就恢复了平衡。平衡二叉树的失衡类型有以下四种,针对不同的类型有不同的调整策略。

  • LL平衡旋转:B结点带左子树代替A成为根结点;A结点带右子树成为B的右子树;B结点原来的右子树作为A的左子树。
void ll(BalancedBinaryTree tree) {
    BalancedBinaryTreeNode *lNode = tree->lNode;
    void *lNodeData = lNode->data;
    BalancedBinaryTreeNode *rNode = tree->rNode;
    lNode->data = tree->data;
    tree->data = lNodeData;
    tree->lNode = lNode->lNode;
    tree->rNode = lNode;
    lNode->lNode = rNode;
}

在这里插入图片描述

  • RR平衡旋转:B结点带右子树代替A成为根结点;A结点带左子树成为B的左子树;B结点原来的左子树作为A的右子树。
void rr(BalancedBinaryTree tree) {
    BalancedBinaryTreeNode *lNode = tree->lNode;
    BalancedBinaryTreeNode *rNode = tree->rNode;
    void *rNodeData = rNode->data;
    rNode->data = tree->data;
    tree->data = rNodeData;
    tree->rNode = rNode->rNode;
    tree->lNode = rNode;
    rNode->rNode = rNode->lNode;
    rNode->lNode = lNode;
}

在这里插入图片描述

  • LR先左后右双旋转:C结点代替A结点成为根结点;B结点带左子树成为C的左孩子,A结点带右子树成为C的右孩子;原来C结点的左子树作为B的右子树,原来C的右子树作为A的左子树。
void lr(BalancedBinaryTree tree) {
    BalancedBinaryTreeNode *rNode = tree->rNode;
    BalancedBinaryTreeNode *lrNode = tree->lNode->rNode;
    void *lrNodeData = lrNode->data;
    lrNode->data = tree->data;
    tree->data = lrNodeData;
    tree->lNode->rNode = lrNode->lNode;
    tree->rNode = lrNode;
    lrNode->lNode = lrNode->rNode;
    lrNode->rNode = rNode;
}

在这里插入图片描述

  • RL先右后左双旋转:C结点代替A作为根结点;A结点带左子树成为C的左孩子,B结点带右子树成为C的右孩子;原来C结点的左子树作为A的右子树,原来C的右子树作为B的左子树。
void rl(BalancedBinaryTree tree) {
    BalancedBinaryTreeNode *lNode = tree->lNode;
    BalancedBinaryTreeNode *rlNode = tree->rNode->lNode;
    void *rlData = rlNode->data;
    rlNode->data = tree->data;
    tree->data = rlData;
    tree->rNode->lNode = rlNode->rNode;
    tree->lNode = rlNode;
    rlNode->rNode = rlNode->lNode;
    rlNode->lNode = lNode;
}

在这里插入图片描述

最后是失衡调整算法,在这个算法中只需要判断失衡类型是哪种并调用相应的算法即可:

void imbalanceAdjust(BalancedBinaryTree tree, void *element, int (*compare)(void *, void *)) {
    int lDeep = balancedBinaryTreeDeep(tree->lNode);
    int rDeep = balancedBinaryTreeDeep(tree->rNode);
    int balance = lDeep - rDeep;
    if (balance > 1) {
        //左左和左右
        if (compare(element, tree->lNode->data) < 0) {
            ll(tree);
        } else {
            lr(tree);
        }
    } else if (balance < -1) {
        //右右和右左
        if (compare(element, tree->rNode->data) > 0) {
            rr(tree);
        } else {
            rl(tree);
        }
    } else {
        return;
    }
}

平衡二叉树的插入

/**
 * 二叉排序树的插入
 * @param tree
 * @param element
 * @param compare
 * @return
 */
bool balancedBinaryTreeInsert(BalancedBinaryTree *tree, void *element, int (*compare)(void *, void *)) {
    if ((*tree) == NULL) {
        *tree = malloc(sizeof(BalancedBinaryTreeNode));
        (*tree)->data = element;
        (*tree)->lNode = NULL;
        (*tree)->rNode = NULL;
        return true;
    } else {
        int cmpResult = compare(element, (*tree)->data);
        if (cmpResult == 0) {
            return false;
        } else if (cmpResult > 0) {
            if ((*tree)->rNode == NULL) {
                BalancedBinaryTreeNode *node = malloc(sizeof(BalancedBinaryTreeNode));
                node->data = element;
                node->rNode = NULL;
                node->rNode = NULL;
                (*tree)->rNode = node;
                return true;
            } else {
                bool result = balancedBinaryTreeInsert(&(*tree)->rNode, element, compare);
                imbalanceAdjust(*tree, element, compare);
                return result;
            }
        } else {
            if ((*tree)->lNode == NULL) {
                BalancedBinaryTreeNode *node = malloc(sizeof(BalancedBinaryTreeNode));
                node->data = element;
                node->rNode = NULL;
                node->rNode = NULL;
                (*tree)->lNode = node;
                return true;
            } else {
                bool result = balancedBinaryTreeInsert(&(*tree)->lNode, element, compare);
                imbalanceAdjust(*tree, element, compare);
                return result;
            }
        }
    }
}

平衡二叉树的删除

红黑树

虽然平衡二叉树可以保证二叉排序树的性能,但是频繁的平衡调整代价较大,为此在平衡二叉树上放宽了条件,引入了红黑树,红黑树是具有以下性质的二叉排序树:

  • 每个结点是红色或是黑色。
  • 根结点是黑色。
  • 所有叶子结点(NULL结点)都是黑色。
  • 红色结点的父结点和孩子结点都是黑色。
  • 对于每个结点而言,其到任意叶子结点的路径都包含相同数目的黑结点,这些黑结点的数量(不含自身)称为该结点的黑高,根结点的黑高称为红黑树的黑高。

常用的结论如下:

  • 从根结点到叶子结点的最长路径不大于最短路径的两倍。因为最短路径全由黑结点组成,最长路径由黑结点和红结点交替组成,而红黑节点的数量相同。
  • 有n个内部结点的红黑树的高度h<=log₂(n+1)。因为从根结点到叶子结点的任意一条路径都至少有一半是黑结点。。所以根的黑高是少是h/2,
  • 新插入红黑树的结点着为红色。

红黑树的插入

红黑树的删除

B树

B树又称为多路平衡查找树,B数中所有结点的孩子个数的最大值称为B树的阶,通常用m表示,B树要么是空树,要么是满足以下条件的m叉树:

  • 树中每个结点至少有m棵子树。
  • 若根结点不是终端结点,则至少有两棵子树。
  • 除根结点之外的非叶子结点至少有⌈m/2⌉棵子树
  • 所有叶子节点都出现在同一层次上,且不带任何信息。

B+树

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1116837.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

16.2 ARP 主机探测技术

ARP &#xff08;Address Resolution Protocol&#xff0c;地址解析协议&#xff09;&#xff0c;是一种用于将 IP 地址转换为物理地址&#xff08;MAC地址&#xff09;的协议。它在 TCP/IP 协议栈中处于链路层&#xff0c;为了在局域网中能够正确传输数据包而设计&#xff0c;…

C嘎嘎之类和对象上

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大二&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;掌握类的引用和定义&#xff0c;熟悉类成员函数的…

代码随想录算法训练营第二十九天丨 回溯算法part06

回溯总结 对于回溯算法&#xff0c;我们需要知道的是 回溯是递归的副产品&#xff0c;只要有递归就会有回溯&#xff0c;所有回溯法常与二叉树遍历【前中后序遍历】&#xff0c;深搜混在一起&#xff0c;原因是都涉及到的递归。 回溯法 暴力搜索&#xff0c;它的效率并不高&…

FreeRTOS入门教程(事件组概念和函数使用)

文章目录 前言一、事件组概念二、事件组和信号量&#xff0c;队列的区别三、事件组相关函数三、事件组应用示例1.等待多个事件2.任务同步 总结 前言 本篇文章将带大家学习什么是事件组以及如何使用事件组。 一、事件组概念 事件组通常是由一组位&#xff08;bits&#xff09…

CSS页面基本布局

前提回顾 1. 超文本标记语言&#xff08;HTML&#xff09;是一种标记语言&#xff0c;用来结构化我们的网页内容并赋予内容含义&#xff1b; &#xff08;超文本标记语言&#xff08;英语&#xff1a;HyperText Markup Language /ˈhaɪpətekst ˈmɑːkʌp ˈlŋɡwɪdʒ /…

Linux小技巧之awk必知必会

Linux Shell 三剑客之一&#xff0c;废话不多说直接上干货。 目录 1. 指定分隔符2.指定打印行数3.打印最后一列4.打印倒数第二列5.匹配输出6.if 判断7.统计列总数值8.时间格式转换 示例数据&#xff1a; 1. 指定分隔符 awk -F | {print $1} log.txt # awk -F | {print $1}…

STM32MP135和STM32MP157的区别

本文介绍了STMicroelectronics公司推出的两款多核处理器STM32MP135和STM32MP157之间的区别&#xff0c;包括主频、集成硬件模块数量、内存大小和电压调节模块等方面。 STMicroelectronics是一家领先的半导体解决方案提供商&#xff0c;在嵌入式系统领域有着丰富的经验。他们…

序列合并

题目描述 有两个长度为 N 的单调不降序列 A,B&#xff0c;在 A,B 中各取一个数相加可以得到 个和&#xff0c;求这 个和中最小的 个。 输入格式 第一行一个正整数 N&#xff1b; 第二行 N 个整数 1…N​。 第三行 N 个整数 1…N​。 输出格式 一行 N 个整数&#xff…

从头开始使用 KNN 进行 KNN 和 MNIST 手写数字识别的初学者指南

坦维佩努穆迪 Kaggle参考&#xff1a; MNIST Perfect 100% using kNN | Kaggle 一、说明 MNIST &#xff08;“修改后的国家标准与技术研究所”&#xff09;是事实上的计算机视觉“hello world”数据集。自 1999 年发布以来&#xff0c;这个经典的手写图像数据集一直作为分类算…

编写内联函数求解 2x²+4x+5的值,并用主函数调用该函数

动态内存分配可以根据实际需要在程序运行过程中动态地申请内存空间,这种内存空间的分配和释放是由程序员自己管理的,因此也被称为手动内存分配。 C++ 中,动态内存的分配和释放是通过 new 和 delete 操作符进行的。new 操作符用于在堆内存上为对象动态分配空间,dele…

Python之哈希表-哈希表原理

Python之哈希表-哈希表原理 集合Set 集合&#xff0c;简称集。由任意个元素构成的集体。高级语言都实现了这个非常重要的数据结构类型。Python中&#xff0c;它是可变的、无序的、不重复的元素的集合 初始化 set() -> new empty set objectset(iterable) -> new set …

Docker——如何自定义镜像【将自己的项目制作成镜像】?

目录 前言&#xff1a;我们以前是如何部署项目的&#xff1f; 1、镜像由哪几部分构成的 2、如何手动自定义一个镜像 2.1、Dockerfile 2.2、dockerfile文本文件中&#xff0c;最终要写什么&#xff1f; 2.3、构建镜像 3、案例&#xff1a;部署java项目 4、如何与其他容器…

Python 机器学习入门之ID3决策树算法

系列文章目录 第一章 Python 机器学习入门之线性回归 第一章 Python 机器学习入门之梯度下降法 第一章 Python 机器学习入门之牛顿法 第二章 Python 机器学习入门之逻辑回归 番外 Python 机器学习入门之K近邻算法 番外 Python 机器学习入门之K-Means聚类算法 第三章 Python 机…

华为云HECS云服务器docker环境下安装nacos

华为云HECS云服务器&#xff0c;安装docker环境&#xff0c;查看如下文章。 华为云HECS安装docker-CSDN博客 一、拉取镜像 docker pull nacos/nacos-server二、宿主机创建挂载目录 执行如下命令&#xff1a; mkdir -p /usr/local/nacos/logs mkdir -p /usr/local/nacos/con…

服务端监控要怎么做?

目录 前言 一、Google的四类黄金指标 二、RED方法 三、USE方法 RED方法 vs USE方法 四、监控指标 WEB服务监控 MySQL数据库监控 QPS TPS 最大连接数 缓存监控 总结 前言 众所周知&#xff0c;业界各种大中型软件系统在生产运行时&#xff0c;总会有一些手段来…

ESP32C3 LuatOS TM1650②动态显示累加整数

--注意:因使用了sys.wait()所有api需要在协程中使用 -- 用法实例 PROJECT "ESP32C3_TM1650" VERSION "1.0.0" _G.sys require("sys") local tm1650 require "tm1650"-- 拆分整数&#xff0c;并把最低位数存放在数组最大索引处 loc…

车载网关通信能力解析——SV900-5G车载网关推荐

随着车联网的发展,各类车载设备对车载网关的需求日益增长。车载网关作为车与车、车与路、车与云之间连接的关键设备,其通信能力直接影响整个系统的性能。本文将详细解析车载网关的通信能力,并推荐性价比高的SV900-5G车载网关。 链接直达&#xff1a;https://www.key-iot.com/i…

类与对象-对象特性-构造函数与析构函数

#include<iostream> using namespace std; class Person { public: //构造函数 : //没有返回值,不写void //函数名与类名相同 //构造函数可以有参数,能发生重载 //创建对象时,构造函数会自动调用,而且只调用一次 Person() { cout << &quo…

哈里斯鹰算法优化BP神经网络(HHO-BP)回归预测研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Python爬虫:ad广告引擎的模拟登录

⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ &#x1f434;作者&#xff1a;秋无之地 &#x1f434;简介&#xff1a;CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作&#xff0c;主要擅长领域有&#xff1a;爬虫、后端、大数据…