文章目录
- 前言
- 树和森林基础概念
- 二叉树
- 二叉树的遍历
- 二叉树的构造
- 树和森林与二叉树之间的转化
- 树和森林的遍历
- 满二叉树
- 完全二叉树
- 线索二叉树
- 线索二叉树的构造
- 寻找前驱和后继
- 线索二叉树的遍历
- 最优二叉树(哈夫曼树)
- 哈夫曼树的构造
- 哈夫曼编码
- 二叉排序树(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} mi−1个结点。
- m m m叉树:任意结点的度小于 m m m的树
- 层次:结点的层次从根结点开始定义,在同一层的非兄弟结点互为堂兄弟。
- 深度:结点的深度从根结点开始向下逐层累加,树的深度就是结点的最大层数。
- 高度:结点的高度从叶子节点开始向上逐层累加,树的高度就是结点的最大层数。
- 高度为 h h h的 m m m叉树至多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m−1mh−1个结点,最少有 h h h个结点。
- 高度为 h h h、度为 m m m的树至少有 h + m − 1 h+m-1 h+m−1个结点。
- 具有 n n n个结点的 m m m叉树的最小高度为 ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1)\rceil ⌈logm(n(m−1)+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} 2i−1个结点,最少有 1 1 1个结点。
-
高度为 h h h的二叉树最多有 2 h − 1 2^h-1 2h−1个结点。
-
设非空二叉树中度为 0 、 1 、 2 0、1、2 0、1、2的结点的个数分别为 n 1 、 n 2 、 n 3 n_1、n_2、n_3 n1、n2、n3,设树中结点的总个数为 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 2h−1。
- 不存在度为 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 (i−1)/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个 2n−1个结点。
- 最优二叉树结点的度要么是 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⌉棵子树
- 所有叶子节点都出现在同一层次上,且不带任何信息。