这一篇主要介绍了数据结构中树的知识点
是一种递归的数据结构 根节点没有前驱,其他节点只有一个前驱;所有节点有零个或多个后继。
节点的度:树中一个节点的子节点个数; 树的度:树中节点的最大度数; 节点的深度:从根节点自顶向下逐层累加; 节点的高度:从叶子节点自底向上逐层累加;
(1)树中节点数等于所有节点的度数加1; (2)度为m的树第 i 层至多有 m^(i-1) 个节点; (3)高度为h的m叉树 至多有 (m^h-1)/(m-1) 个节点;
二叉树是有序树,二叉树可以是空树
特点如下:
叶子节点只可能在层次最大的两层,且最大层次的叶子节点都在左边。如果有度为1的节点,只可能有一个,且该节点只有左孩子没有右孩子。节点个数为n,若n为奇数,则每个分支节点都有左孩子和右孩子;若n为偶数,则编号最大的节点只有左孩子没有右孩子,其余节点左右孩子都有。左子树上所有节点的关键字 < 根节点的关键字 < 右子树上所有节点的关键字
任一节点的左右子树深度之差不超过1.
时间复杂度O(n),空间复杂度O(n)
void PreOrder(BiTree T){ if(T != NULL){ visit(T); PreOrder(T->lchild); PreOrder(T->rchild); } }时间复杂度O(n),空间复杂度O(n)
void InOrder(BiTree T){ if(T != NULL){ InOrder(T->lchild); visit(T); InOrder(T->rchild); } }时间复杂度O(n),空间复杂度O(n)
void PostOrder(BiTree T){ if(T != NULL){ PostOrder(T->lchild); PostOrder(T->rchild); visit(T); } }可以借助栈,将二叉树的递归遍历转化为非递归遍历。
非递归先序遍历 算法步骤如下: 从根开始,当前节点存在或栈不为空,重复下面两个操作。 (1)访问当前节点,当前节点进栈,进入其左子树,重复至当前节点为空; (2)若栈非空,则栈顶节点出栈,并进入其右子树。 void PreOrder(BiTree root){ SeqStack *S; BiTree p; InitStack(S); p = root; while(p != NULL || !IsEmpty(S)){ //当前节点指针和栈均为空,则结束 while(p != NULL){ visit(p->data); //访问根节点 Push(S, p); //根指针进栈 p = p->lchild; //进入左子树 } if(!IsEmpty(S)){ //如果栈非空 Pop(S, &p); //根指针出栈 p = p->rchild; //进入右子树 } } } 非递归中序遍历 算法步骤如下: 从根开始,当前节点存在或栈不为空,重复下面两个操作。 (1)当前节点进栈,进入其左子树,重复至当前节点为空; (2)若栈非空,则栈顶节点出栈,访问该出栈节点,并进入其右子树。 void InOrder(BiTree root){ SeqStack *S; BiTree p; InitStack(S); p = root; while(p != NULL || !IsEmpty(S)){ //当前节点指针和栈均为空,则结束 while(p != NULL){ Push(S, p); //根指针进栈 p = p->lchild; //进入左子树 } if(!IsEmpty(S)){ //如果栈非空 Pop(S, &p); //根指针出栈 visit(p->data); //访问根节点(和先序遍历的不同之处) p = p->rchild; //进入右子树 } } } 非递归后序遍历 算法步骤如下: 从根开始,当前节点存在或栈不为空,重复下面两个操作。 (1)当前节点进栈,进入其左子树,重复至当前节点为空; (2)若栈非空,判断栈顶 p 的右子树是否为空、右子树是否刚访问过。 若是,则出栈、访问节点 p ,p 赋给 q,p置为空; 若不是,则进入 p 的右子树。 void PostOrder(BiTree root){ SeqStack *S; BiTree p,q; InitStack(S); p = root; q = NULL; while(p != NULL || !IsEmpty(S)){ //当前节点指针和栈均为空,则结束 while(p != NULL){ Push(S, p); //根指针进栈 p = p->lchild; //进入左子树 } if(!IsEmpty(S)){ //如果栈非空 Top(S, &p); //判断栈顶的右子树是否为空,右子树是否访问过 if((p->rchild==NULL) || (p->rchild==q)){ Pop(S, &p); //根指针出栈 visit(p->data); //访问根节点 q = p; p = NULL; } else{ p = p->rchild; } } } }层次遍历需要借助一个队列。 算法步骤如下: 首先根节点入队,当队列非空时,重复如下操作。 (1)队头节点出队,并访问出队节点; (2)出队节点的非空 左、右孩子依次入队。
void LevelOrder(BiTree T){ InitQueue(Q); BiTree p; EnQueue(Q, T); //根节点入队 while(!IsEmpty(Q)){ //队列不为空进循环 DeQueue(Q, p); //队头元素出队 visit(p); //访问当前p所指向的节点 if(p->lchild != NULL){ EnQueue(Q, p->lchild); //左子树不为空,则左子树入队 } if(p->rchild != NULL){ EnQueue(Q, p->rchild); //右子树不为空,则右子树入队 } } }通过中序遍历对二叉树线索化:
//中序遍历对二叉树线索化的递归算法 void InThread(ThreadTree &p, ThreadTree &pre){ if(p != NULL){ InThread(p->lchild, pre); //递归,线索化左子树 if(p->lchild==NULL){ //左子树为空,建立前驱线索 p->lchild = pre; p->ltag = 1; } if(pre != NULL && pre->rchild == NULL){ pre->rchild = p; //建立前驱节点的后继线索 pre->rtag = 1; } pre = p; //标记当前节点 成为刚访问过的节点 InThread(p->rchild, pre); //递归,线索化右子树 } }中序遍历建立中序二叉树的主过程算法:
void CreateInThread(ThreadTree T){ ThreadTree pre = NULL; if(T != NULL){ //树非空 InThread(T, pre); //线索化二叉树(调用函数) pre->rchild = NULL; //处理便利的最后一个节点 pre->rtag = 1; } }中序线索二叉树的遍历不需要栈,可以实现非递归遍历。
求中序线索二叉树中 中序序列的第一个节点 ThreadNode *Firstnode(ThreadNode *p){ while(p->ltag == 0){ p = p->lchild; //最左下节点(不一定是叶子节点) } return p; } 求中序线索二叉树中 节点 p 在中序序列下的后继节点 ThreadNode *Nextnode(ThreadNode *p){ if(p->rtag == 0){ return Fiestnode(p->rchild); } else{ return p->rchild; //rtag==1,直接返回后继线索 } } 二叉树的线索链表也可以带头结点。树转换为二叉树:每个节点左指针指向第一个孩子节点,右指针指向它在树中相邻的兄弟节点。(左孩子右兄弟) 注意:由树转换得到的二叉树没有右子树。
森林转换为二叉树:与上述转换类似,先将森林中的每棵树转换为二叉树。然后将第一棵树的根作为二叉树的根,第二棵树的根作为第一个右孩子,以此类推。
二叉树转换为森林:上述过程的逆推即可。
二叉排序树(BST),也称二叉查找树,中序遍历是一个有序序列。 左子树节点值 < 根节点值 < 右子树节点值
二叉排序树的非递归查找:从根节点开始,沿某一个分支逐层向下进行比较
//查找函数返回指向关键字值为key的节点指针,若不存在,返回 NULL BSTNode *BST_Search(BiTree T, ElemType key, BSTNode *&p){ p = NULL; while(T != NULL && key != T->data){ p = T; // p 指向被查找节点的双亲,用于插入和删除操作中 if(key < T->data){ T = T->lchild; } else{ T = T->rchild; } } return T; }二叉排序树是一种动态集合,树的结构通常不是一次生成的,而是在查找过程中,逐渐插入的。(插入的新节点一定是某个叶子节点)
//在二叉排序树中插入一个关键字为 k 的节点 int BST_Insert(BiTree &T, KeyType k){ if(T == NULL){ //原树为空,新插入的记录为根节点 T = (BiTree)malloc(sizeof(BSTNode)); T->key = k; T->lchild = T->rchild = NULL; return 1; //返回1,表示成功 } else if(k == T->key){ //树中存在相同的关键字节点 return 0; } else if(k < T->key){ //小于关键字,插入到T的左子树 return BST_Insert(T->lchild, k); } else{ //大于关键字,插入到T的右子树 return BST_Insert(T->rchild, k); } }构造一棵二叉排序树就是一次输入数据元素,并将它们插入到二叉排序树中的适当位置上的过程。
//用关键字数组 str[] 建立一个二叉排序树 void Creat_BST(BiTree &T, KeyType str[], int n){ T = NULL; //初始时为空树 int i = 0; while(i < n){ //依次将每个元素插入 BST_Insert(T, str[i]); i++; } }删除操作的过程按三种情况来处理: (1)若被删节点 k 为叶子节点,则直接删除。 (2)若被删节点 k 只有一棵左子树或右子树,则令 k 的孩子替代 k 的位置。 (3)若被删节点 k 有左、右两棵子树,则令 k 的直接后继(或直接前驱)替代 k ,然后将其删除。
对于高度为 H 的二叉排序树,其插入和删除操作的运行时间都是 O(H) 平均查找长度 ASL 主要取决于树的高度。
如果是一棵单支树,平均查找长度为 O(n) 如果是一棵平衡二叉树,平均查找长度为 O(log2 n)
优点:无需移动节点,只需修改指针即可完成插入和删除操作。 当有序表是静态查找时,宜用顺序表作为其存储结构,用二分查找; 当有序表是动态查找时,宜用二叉排序树作为其逻辑结构。
(1)LL平衡旋转(右单旋转)
(2)RR平衡旋转(左单旋转)
(3)LR平衡旋转(先左后右双旋转)
(4)RL平衡旋转(先右后左双旋转)
在平衡二叉树上的查找过程和二叉排序树相同。 平衡二叉树的平均查找长度为 O(log2 n)
节点的带权路径长度:从树的根节点到任意节点的路径长度与该节点上权值的乘积。 树的带权路径长度:树中所有叶子节点的带权路径长度之和。
哈夫曼树的构造过程:
哈夫曼树的特点: (1)每个初始节点都成为叶子节点,并且权值越小到根节点路径越长。 (2)构造过程中共新建了 N-1 个节点,因此哈夫曼树中节点总数为 2N-1。 (3)每次构造都选择2棵树作为新节点的孩子,因此哈夫曼树中不存在度为1的节点。
