数据结构:二叉树的链式存储
数据结构:二叉树的链式存储(C语言版)
1.写在前面
数组表示的优势和弊端
二叉树同样有两种存储方式,数组和链式存储,对于数组来说,我们利用二叉树的性质然后利用下标可以方便的找到一个节点的子节点和父节点。
二叉树的性质:
1.二叉树的第i层上至多有2i-1个节点
2.深度为K的二叉树至多有2k-1个节点
3.任何一个二叉树中度数为2的节点的个数必度数为0的节点数目少1.
说明:度数为0,为叶子节点。
4.具有n个节点的完全二叉树的深度为|_Log2N_|+1
5.若完全二叉树中的某节点编号为i,则若有左孩子编号为2i,若有右孩子编号为2i+1,母亲节点为i/2。
结合第5条性质:
若完全二叉树中的某节点编号为i,则若有左孩子编号为2i,若有右孩子编号为2i+1,母亲节点为i/2。
可以在这种完全二叉树中十分方便的找到任何相关联(父子、兄弟等)的元素。
但是由于顺序存储天生适配于完全二叉树,对于下面这种非完全二叉树并不合适,主要体现在空间上的浪费,所以我们需要用到另一种存储方式——链式存储。
引入链表存储
在链式存储中,每个节点的结构如下
结构描述:
一个存储数据的变量与两个指向孩子的指针域。
利用指针域我们便可以完美的存储非完全二叉树,如下:
2.代码分解
►首先根据上述图示,我们不难给出节点的描述
typedef struct BiTNode { TElemType data; struct BiTNode *lchild,*rchild; }BiTNode,*BiTree;
►创建链式二叉树
创建二叉树不是很容易的,并且它的创建方式也对应三种顺序。
其实二叉树的遍历和二叉树的创建可以说是几乎一样的过程。无非是创建的过程是给data赋值,遍历是取出data。
我们先不给出创建的代码,而是要明白:
说明:
1.叶子节点不是没有左右孩子,只不过左右孩子都是空。
2.我们的二叉树是手动输入数据进行创建的 我在实际开发中不可能是这样创建的,所以我们要处理空节点的问题。
3.创建和遍历的执行顺序(先序、中序、后序)是一样的,那么输入和取出的值才会是一样的。
比如我们要先序创建一个上述图片中的二叉树,我们应该怎样输入呢?
AB##CD##EF##G##
# 在这里是表示叶子节点的左或右节点为空。
代码如下:
void createBitree(Bitree &T) { char ch; if((ch=getchar())=='#') T=NULL; else { T=(Bitnode*)malloc(sizeof(Bitnode)); T->data=ch; createBitree(T->Lchild); createBitree(T->Rchild); } }
►遍历代码和创建代码基本一样:
/*先序遍历*/ void preTraverse(Bitree T) { if(T!=NULL) { printf("%c",T->data); preTraverse(T->Lchild); preTraverse(T->Rchild); } }
/*中序遍历*/ void inorder(Bitree T) { if(T!=NULL) { inorder(T->Lchild); printf("%c",T->data); inorder(T->Rchild); } }
/*后序遍历*/ void postorder(Bitree T) { if(T!=NULL) { postorder(T->lchild); postorder(T->rchild); printf("%c",T->data); } }
►我们输入字符后三种遍历情况如下:
AB##CD##EF##G##
先序:ABCDEFG
中序:BADCFEG
后序:BDFGECA
►求二叉树的深度
int Depth(Bitree T) {//返回深度 int d,dl,dr; if(!T) d=0; else { dl=Depth(T->Lchild); dr=Depth(T->Rchild); d=1+(dl>dr?dl:dr) ; } return d; }
►二叉树的层序遍历
queue<Bitree> TreeQueue; //使用队列 TreeQueue.push(tree); //先将队头元素加入队列 Bitree p = tree; while (!TreeQueue.empty()) //循环判断队列是否未空,若不空则 { p = TreeQueue.front(); //获取队列头节点,并出队列 TreeQueue.pop(); printf(" %c ", p->data); //打印队列元素 if (p->Lchild) //如果该节点有左节点 { TreeQueue.push(p->Lchild); //加入队列 } if (p->Rchild) //如果该节点有右节点 { TreeQueue.push(p->Rchild); //加入队列 } }
|说明:
1.算法轨迹:
依旧使用下图:
演示:
我们首先将A加入队列, 此时对列 中只有 ⇐[A]
我们将A出弹出队列,并将它的左右子树 BC加入队列,此时队列中有 ⇐[BC] ,打印出 A
我们将B出弹出队列,它没有子树,我们不做任何操作,此时队列中有 ⇐[C] ,打印出 ABC
我们将C出弹出队列,并将它的左右子树 DE加入队列,此时队列中有 ⇐[DE] ,打印出 ABC
我们将D出弹出队列,它没有子树,我们不做任何操作,此时队列中有 ⇐[E] ,打印出 ABCD
我们将E出弹出队列,并将它的左右子树 FG加入队列,此时队列中有 ⇐[FG] ,打印出 ABCDE
我们将F出弹出队列,它没有子树,我们不做任何操作,此时队列中有 ⇐[G] ,打印出 ABCDEF
我们将G出弹出队列,它没有子树,我们不做任何操作,此时队列中有 ⇐[null] ,打印出 ABCDEFG
结论:
根据轨迹我们不难发现,队列是保证层序遍历的基础,因为它保证了先加入队列的上层元素会必后加入队列的下层元素优先出队列。
2.这段代码直接使用会出现问题,因为我们使用了C++标准模板库中的queue。所以,我们需要加入头文件
#include<queue>,并且要使用命名空间 using namespace std;。
3.我们对待数据结构最重要的是理解和使用,实现它只是帮助我们理解,我们无须重复造轮子,在以后的算法中,我们将尽可能的引用标准模板库。