持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情
二叉树的概念和性质
二叉树的定义
二叉树(Binary Tree)是含有n(n≥0)个结点(node)的有限集合。当n = 0时称为空二叉树。在非空二叉树中:
- 有且仅有一个称为根(root)的结点
- 其余结点划分为两个互不相交的子集L和R,其中L和R也是一棵二叉树,分别称为左子树(left subtree)和右子树(right subtree),且其次序不能颠倒。(其余结点0个的话,划分为两个空集——两棵空子树)
二叉树的基本术语
- 二叉树的结点包含一个数据元素及指向其左右子树的两个分支,分别称为左分支和右分支。
- 结点的左、右子树的根称为该结点的左、右孩子,统称为孩子(children),相应地,该结点称为孩子的双亲(parent)
- 同一个双亲的孩子之间可互称为兄弟(sibling)
- 结点的孩子个数称为结点的度(degree)
- 度为0的结点称为叶子结点(leaf)
- 非叶子结点称为内部结点或分支结点(internal node)
- 结点的层次(level): 从根结点开始定义,根为第1层,根的孩子为第2层,如此计数,直到该结点为止
- 二叉树的深度(depth)或高度(height): 二叉树中结点的最大层次。
二叉树上的性质
- 性质1 :在二叉树的第 i 层上至多有2^(i-1) 个结点。(i≥1)
- 性质 2 :深度为 k 的二叉树上最多含 2^k-1 个结点(k≥1)
-
性质 3:对于任意一棵二叉树,如果度为0的结点个数为n0,度为2的结点个数为n2,则n0 = n2+1
-
两类特殊的二叉树
-
满二叉树:深度为k且含有2k-1个结点的二叉树。(除最后一层外,每一层上的所有结点都有两个子结点)
-
完全二叉树:二叉树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应
-
-
- 性质 4:具有 n 个结点的完全二叉树的深度为 [log2n] +1。
-
性质5: 对于含n个结点的完全二叉树中编号为i(1≤i≤n)的结点:
-
(1) 如果i = 1,则i结点是这棵完全二叉树的根,没有双亲;否则其双亲的编号为 [i/2] 。
-
(2)如果2i>n,则i结点没有左孩子;否则其左孩子的编号为2i。
-
(3)如果2i+1>n,则i结点没有右孩子;否则其右孩子的编号为2i+1。
-
二叉树的存储结构
顺序结构储存完全二叉树
可用一维数组存储完全二叉树(0号单元不用),结点的编号对应于结点的下标,且每个结点的双亲、左孩子和右孩子在数组中的下标均可根据性质5计算而得。
//完全二叉树的顺序存储结构的类型定义
typedef char TElemType;
// 假设二叉树结点的元素类型为字符
typedef struct {
TElemType *elem; // 0号单元闲置
int lastIndex; // 二叉树最后一个结点的编号
} SqBiTree; // 顺序存储的二叉树
链式存储结构
typedef struct BiTNode {
TElemType data; // 数据域
struct BiTNode *lchild,*rchild; // 左、右孩子指针
} BiTNode,*BiTree; // 二叉链表
创建二叉树
- 分为两步:
-
- 生成一个新结点t,其数据域的值为e
- 创建一棵二叉树T,其中根结点为t,L和R分别作为左子树和右子树
BiTree MakeBiTree(TElemType e, BiTree L, BiTree R) {
BiTree t;
t = (BiTree)malloc(sizeof(BiTNode));
if(NULL==t) return NULL;
t->data = e; // 根结点的值为e
t->lchild = L; // L作为t的左子树
t->rchild = R; // R作为t的右子树
return t;
}
三叉链表:
typedef struct TriTNode {
TElemType data; // 数据域
TriTNode *parent, *lchild, *rchild;
// 双亲、左、右孩子指针
} TriTNode, *TriTree; //三叉链表
遍历二叉树
对“二叉树”而言,可以有三条搜索路径:
- 先左(子树)后右(子树)的遍历;
- 先右(子树)后左(子树)的遍历;
- 先上后下的层次遍历。
限定先左后右,则有三种实现方案:先序遍历(根左右)、中序遍历(左根右)、后序遍历(左右根)
递归遍历:以中序为例
若二叉树不为空,则依次进行以下操作:
- 遍历根结点的左子树
- 访问根结点
- 遍历根结点的右子树
Status InOrderTraverse(BiTree T, Status (*visit)(TElemType e)) {
if(NULL==T) return OK;
if(ERROR==InOrderTraverse(T->lchild, visit))
return ERROR; // 递归遍历T的左子树
if(ERROR==visit(T->data)) return ERROR; // 访问结点的数据域
return InOrderTraverse(T->rchild, visit); // 递归遍历T的右子树
}
从三种遍历算法可以知道:如果将visit语句抹去,从递归的角度看,这三种算法是完全相同的,或者说这三种遍历算法的访问路径是相同的,只是访问结点的时机不同。
非递归遍历
可利用栈实现
-
前序:根结点T先进栈,随后栈顶出栈,之后先后将T的右子树左子树压栈,周而复始重复操作,直到出栈结点没有左右子树
-
void PreOrder(BiTree T, Status (*visit)(TElemType)) { Stack s; InitStack(s); if (T) Push(s, T); while (StackEmpty(s)!=TRUE) { BiTree t; Pop(s, t); visit(t->data); if (t->rchild) Push(s, t->rchild); if (t->lchild) Push(s, t->lchild); } }
-
-
中序:从根节点T开始一直往左下走,沿途结点入栈,返回最左下的结点p,若p结点的右孩子存在,则令p指向右孩子,然后向左走到底,并依次将指向沿途结点的指针入栈。否则判断栈是否为空?若非空则将保留在栈顶的指针退栈并赋给p,周而复始。
-
void InorderTraverse_I(BiTree T, Status(*visit) (TElemType e)) { LStack S; InitStack_LS(S); BiTree p; p = GoFarLeft(T, S); // 找到最左下的结点 while(p!=NULL) { visit(p->data); // 访问结点 if(p->rchild!=NULL) // 令p指向其右孩子为根的子树的最左下结点 p = GoFarLeft(p->rchild, S); else if(StackEmpty_LS(S)!=TRUE) Pop_LS(S,p); // 栈不空时退栈 else p = NULL; // 栈空表明遍历结束 } }
-
-
后序:需要用到两个栈,一个操作栈一个收集栈,根结点T先进操作栈,随后操作栈栈顶出栈到收集栈,之后先后将T的左子树右子树压操作栈,周而复始重复操作,直到出栈结点没有左右子树,之后收集栈出栈到空
-
void PostOrder(BiTree T, Status (*visit)(TElemType)) { // Add your code here SElemType t,l,r; if(T==NULL) return; Stack s1,s2;//两个栈,分别是操作栈和收集栈 InitStack(s1); InitStack(s2); t.ptr=T; Push(s1,t); while(!StackEmpty(s1)){ Pop(s1,t); Push(s2,t); if(t.ptr->lchild!=NULL){ l.ptr=t.ptr->lchild; Push(s1,l); } if(t.ptr->rchild!=NULL){ r.ptr=t.ptr->rchild; Push(s1,r); } } while(!StackEmpty(s2)){ //遍历收集栈 Pop(s2,t); visit(t.ptr->data); } }
-