树形数据结构是非常重要的非线性数据结构.这里我们将要重点来谈一谈的是二叉树.
树的概念:
树是N个结点的有限集.在任意一棵非空的树:有且仅有一个特定的称为根的结点,当结点数大于1时,其余结点可分为m个互不相交的有限集,其中每个结点可以向下组成一棵新的树,叫做子树.
树的度:一棵树中,每个结点的度的最大值为树的度
树的深度:树中所有结点的最大层次数称为树的深度.
叶子:没有子结点的结点称为树叶
森林:m棵互不相交的树的集合
关于树的重要一点!
递归!从定义中任意一个结点都可以向下成为一棵子树就可以知道,这是树的固有特性!
二叉树
是一种特殊的树型结构,他的特点是每个结点至多只有两棵子树(即二叉树中不存在度数大于2的结点),并且二叉树的左子树和右子树有先后顺序,不能颠倒.
二叉树可以有五种状态,空树,只有根节结点,右子树为空,左子树为空,左右子树均非空
对于创建二叉树,就是根据其递归这条重要的性质来的.
我们用代码简单实现一下,根据需要和输入的不同,以下几种方法均可:(均以链式存储)
#define END '#' using namespace std; typedef char ElemType; typedef struct BNode{ ElemType data; struct BNode *leftchild; struct BNode *rightchild; }*Tree; //创建一个及结点 BNode * Buy_node(){ BNode * s = (BNode *)malloc(sizeof(BNode)); if(s == NULL) exit(1); s -> leftchild = s -> rightchild = NULL; return s; } BNode *CreateTree(){ ElemType item; cin >> item; if(item == END) return NULL; else{ BNode *s = Buy_node(); s -> data = item; s -> leftchild = CreateTree(); s -> rightchild = CreateTree(); return s; } } //通过二级指针来创建树 void *CreateTree2(BNode **s){ ElemType item; cin >> item; if(item == END) *s = NULL; else{ *s = Buy_node(); (*s) -> data = item; CreateTree2(&(*s) -> leftchild); CreateTree2(&(*s) -> rightchild); } } //通过指针和引用来创建树 void CreateTree3(BNode *&s){ ElemType item; cin >> item; if(item == END) s = NULL; else{ s = Buy_node(); s -> data = item; CreateTree3(s -> leftchild); CreateTree3(s -> rightchild); } } //一次性读入参数字符串str BNode *CreateTree4(ElemType *str){ BNode *s = NULL; if(*str != END){ s = Buy_node(); s -> data = *str; s -> leftchild = CreateTree4(++ str); s -> rightchild = CreateTree4(++ str); } return s; }
比较好用递归的思想理解.
满二叉树:一棵每一层上的结点数数都是最大结点数的二叉树.
完全二叉树:有n个结点的二叉树,与满二叉树中编号对应.
二叉树的性质:
二叉树第i层桑至多有2的i-1次幂个结点.
对于上面这个的证明,因为满二叉树的总结点数为2的i次幂-1,然后第i层相当于总的结点数加1后除以2.得到答案.
深度为k的二叉树的最多有2的k次幂减1个结点.
满二叉树的情况.
具有n个结点的完全二叉树一个双亲结点i的左孩子结点编号是2i,右孩子编号是2i + 1
对二叉树的遍历:
遍历分为三种,先序,中序,后序遍历.三种遍历的区别是对根结点的访问顺序(前提是根节点非空)
先序是先访问根结点,再访问左子树,再访问右子树.
中序是先访问左子树,再访问根结点,最后访问右子树
后序是先访问左子树,再访问右子树,最后访问根结点.
对于三种遍历的实现:
//前序遍历 void Preorder(BNode *p){ if(p != NULL){ cout << p -> data << " "; Preorder(p -> leftchild); Preorder(p -> rightchild); } } //中序遍历 void Inorder(BNode *p){ if(p != NULL){ Inorder(p -> leftchild); cout << p -> data << " "; Inorder(p -> rightchild); } } //后序遍历 void Pastorder(BNode *p){ if(p != NULL){ Pastorder(p -> leftchild); Pastorder(p -> rightchild); cout << p -> data << " "; } }
当然我们也可以不用递归的方式来进行遍历.利用栈,相似的原理,这里不多描述.
写一段伪代码吧:
当栈S不为空的时候
向左不停入栈,直到访问到的结点为空
出栈访问结点.
向右一步,入栈.
我们一般在用一个字符串表示一棵树的时候用一个标志,如我创建的时候宏定义的#号,作为空的标志.
举个例子:ABC##DE##F##G#H##
它的树状图是这样的:
A
B G
C D H
E F
其中E和F是D的左右结点.
我们可以得到它的前序:ABCDEFGH
中序:CBEDFAGH
后序:CEFDBHGA
当时如果仔细观察,我们是可以通过前序中序,或者中序后序画出一棵树的,因为通过前序后序的一个结点或者最后一个结点是根结点,在中序中找到就可以将树分成两课子树.递归就可以实现:代码如下:
//在一个长度为n的子符串中找到一个元素,返回其位置 int findis(char *is, char x, int n){ for(int i = 0; i < n; i ++){ if(is[i] == x) return i; } return -1; } //前序中序成立一棵树 BNode *CreateTreePI(char *ps ,char *is ,int n){ BNode *s = NULL; if(n > 0){ s = Buy_node(); s -> data = *ps; int pos = findis(is, *ps, n); if(pos == -1) exit(1); s -> leftchild = CreateTreePI(ps + 1, is, pos); s -> rightchild = CreateTreePI(ps + 1 + pos, is + 1 + pos, n - pos - 1); } }
线索二叉树
我们在以二叉链表作为存储结构的时候,只能找到结点左右孩子的信息,直接前驱和后继无法找到也就是无法找到根据遍历法则的下一个遍历点和上一个点.
所以有了线索二叉树,它通过它的两个孩子的来存储信息,如果它的左孩子为空,则使左孩子指向其前驱,否则指向它的左孩子,它的右孩子若为空则指向其直接后继.否则指向其右孩子.
树的存储结构:
双亲表示法:数组,里面有data和parent,parent存储双亲结点坐标
这种表示方法利用了除了根节点外,都只有唯一双亲的性质.这种表示方法找双亲结点十分快捷,但是求结点的孩子则需要遍历整个数据结构.
孩子表示法;数组加单链表,单链表写入当前结点的孩子
树转化为二叉树:
加线:兄弟之间加线
删除:第一个孩子和双亲结点删去
转化后的二叉树的先序遍历与转化前的中序遍历相同,中序遍历与先序遍历相同.