心中有“树”:数据结构之树详解

前言

树型结构是一类重要的非线性数据结构。其中以树和二叉树最为常用。树状结构在客观世界中广泛存在,如人类社会的族谱和社会组织机构等,都可以用树来形象表示。同时在计算机领域中,树可以用来表示源程序的语法结构,并用于加密信息。本文主要讨论二叉树极其各种操作与应用,并拓展树的结构与简单操作。

(一)树的基础定义与表示

1 树的定义

(Tree)是n(n>=0)个节点的有限集。在任意一颗非空树中:
(1)有且仅有一个特定的(Root)的节点;
(2)n>1时其余节点可分为m(m>0)个互不相交的有限集合T1,T2,…Tm,其中每一个集合本身又是一颗树,并且称为根的子树
显然,这是一个递归的定义。故对于树的操作等也需要使用递归操作。

2 树的图示

图源自网络
设有树a,b,c,如上图所示,其中a为空树,b只有一个根节点为A,c的根节点为A,它又有三颗子树,假设为T1,T2,T3,这三颗子树又分别有自己的根B,C,D和相应的更小的子树,依此类推。

3 树的逻辑结构表示法

(1)层次表示法
前面的树的图示所用的表示法即层次表示法,它可以直观表示树中个节点的层次关系。同时可以更好地理解关于树的各种操作
(2)嵌套表示法
嵌套表示法采用集合形式表示逻辑结构,类似于集合中的韦恩图,应用较少,此不过多介绍。
(3)广义表表示法
广义表利用根节点元素和括号的组合表示。括号外边是根节点,括号里边是每个子树。如(2)中的图示中 c 树可以表示为 A(B(E(K,L),F),C(G),D(H(M),I,J)))。

(二)二叉树

1 二叉树定义

二叉树是最常用的一种树型结构,其特点是每个节点最多只有两颗子树(即二叉树中不存在度大于2的节点),二叉树的子树有左右之分,次序不能任意颠倒。

2 二叉树示意图

二叉树的顺序表表示法较为繁琐,且浪费空间,我们以二叉链表表示法为例:
图源自网络
将左图层次表示与节点的存储结构关联即右图所示。每个节点有一个数据域,与两个指针域,分别指向左孩子右孩子。同时每个左孩子又是一颗子树。在这里我们以 ^ 符号表示指针域为空(NULL)表示其没有左(右)子树。

3 程序实现

前面说了二叉树的定义,接下来看一下关键的程序代码。

(1)节点定义

二叉树的二叉链表存储结构中,节点拥有一个数据域存储元素,此处元素类型我们以字符型为例,两个指针域存储左右孩子节点的地址。

二叉链表表示法

typedef struct BiTNode
{
	char data;  //数据元素
	BiTNode* lc, * rc;//分别指向左孩子 与 右

}*BiTree;

lc,rc分别为指向左右孩子节点的地址的指针。BiTNode为节点结构体类型,为了代码编写方便,同时我们将指向BiTNode的指针的类型定义为BiTree。BiTree相当于BiTNode*。同时二叉树还有三叉链表表示法,其节点定义多了一个指向双亲的指针域。即BiTNode* parent,这样可以更方便的寻找某个节点的双亲节点。

(2)二叉树的先序遍历

二叉树的遍历分为先序遍历中序遍历后序遍历层序遍历
先序遍历:
(1)访问根节点——D;
(2)遍历左子树——L;
(3)遍历右子树——R;
先序遍历简称为DLR遍历,同理中序为LDR遍历,后序为LRD遍历。下面我们以先序遍历程序为例,中序和后序仅需要调整程序顺序即可。

void Preorder(BiTree T,void visit(TElemType))
{
	if(!T)return;visit(T->data);//访问根节点
	Preorder(T->lc,visit);//遍历左子树
	Preorder(T->rc,visit);//遍历右子树
}

其中visit为函数作为形参,为了使得代码具有通用性。具体visit实现可以是最简单的输出,或者对于节点进行的一系列操作。函数作为形参参考下列代码。

#include<iostream>
int num[] = { 1,2,3,4 };
void Print(int m) { std::cout << m << std::endl; }
void ArrayPrint(int num[], void visit(int))
{
	for (int i = 0; i < 4; i++)
		visit(num[i]);
}
int main()
{
	ArrayPrint(num, Print);
}

(3)二叉树非递归先序遍历

由于递归实际是栈的压栈出栈操作,所以我们可以将递归的先序遍历改为非递归的栈操作。
算法思路:从根节点开始,每访问一个节点,按照前序遍历规则走左子树,如果系欸点的右子树存在,那么将右指针入栈,以便正确遍历相应子树。非递归操作不常用,对二叉树的操作基本采用递归形式容易理解。

void PreorderStack(BiTree T)
{
	BiTree p;
	StackInit(S);//栈初始化为空
	push(S,T);//根节点入栈
	while(!empty(S))
	{	
		p=pop(S);//出栈 同时p为栈顶元素
		while(p)
		{
			visit(p);//访问p节点
			if(p->rc)
				push(S,p->rc);//右子树存在 则进栈
			p=p->lc;//向左走一步
		}
	}
}

(4)二叉树层序遍历

二叉树层序遍历需要用到队列。即访问根节点的同时,将其左右孩子一次入队列,较为简单。利用STL封装的queue实现。代码程序如下:

void QueuePreorder(BiTree T) 
{
    queue <BiTree> q;
    if(!T)
    	q.push(T);
    while (!q.empty())  //队列不为空判断
    {
        visit(q.front()->data);
       	if(!q.front()->lc)
       		q.push(q.front()->lc);
		if(!q.front()->rc)
			q.push(q.front()->rc);
        q.pop();  //已遍历节点出队
    }
}

(5)计算二叉树的深度

//求二叉树的深度
int depth(BiTree T)
{
	if (!T)return 0;
	int dl, dr;//分别表示左子树深度与右子树深度
	dl = depth(T->lc); dr = depth(T->rc);
	return dl > dr ? dl + 1 : dr + 1;
	//加一为 加上根节点所在深度
}

4 二叉树算法设计题目

1 查找元素为X的节点

题目:再二叉树中查找是否有元素为X的节点,找到则返回节点地址,否则返回空指针
分析:题目比较简单。可以直接在二叉树中递归查找,如果根是空,返回NULL。如果元素为X,返回地址。否则新建节点M接收左子树返回的值,如果非空返回M。否则返回右子树返回的值。同时递归时,我们只需要考虑当前一层的情况,具体内部递归回代实现可以不必详细了解,否则很容易被绕进去。程序实现为

BiTree TreeSearch(BiTree T,char  e)//BiTree类型
{
	//递归考虑当前一层 
	BiTree M;
	if (!T)return NULL;
        if (T->data == e)return T;
	else
	{
		M = TreeSearch(T->lc, e);
		if (M)return M;//左子树存在 返回地址
		return TreeSearch(T->rc, e);//无论右子树是否存在,均返回
	}
}

在这里我们需要用M来保存递归左子树返回的值,否则若只有两个TreeSearch的顺序结构,则只有第二个TreeSearch起作用,左子树是否右X不得而知。局部错误代码为:

else
{//错误
	TreeSearch(T->lc,e);
	TreeSearch(T->rc,e);
}

2 判断二叉树是否为完全二叉树

完全二叉树:深度为k的,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树的1至n的节点一一对应时,称为完全二叉树。
简单来说,就是只可能最后一层节点是不满的,同时节点应尽可能向左补充。
算法思路:可以利用层序遍历,首先将根节点入队。当可以从队列取出元素且不空,则将左右孩子入队。当出现空指针结束循环。最后再用一个while循环如果遍历到了非空元素则为非完全二叉树,否则最终为完全二叉树。(因为完全二叉树只有最后一排可以空节点,也就是遇到空指针后,后面的层序遍历只能都是空指针)。代码实现为:

bool ComTreeJudge(BiTree T)
{
	CSQueue q; QueueInit(q); BiTree p;
	if (!T)return false; Enqueue(q, T);
	while (Dequeue(q, p))//有元素可以出队
	{
		//遍历到空指针结束遍历
		if (!p)break;
		Enqueue(q, p->lc);
		Enqueue(q, p->rc);
		
	}
	//访问后续队列元素,若出现非空,那么非完全二叉树
	while (Dequeue(q, p))
	{
		if (p)return false;
	}
	return true;
}

(三)树

1 树的存储结构

此处推广至更为普遍的树结构,一个节点可以拥有多个孩子节点。常见表示法为双亲表示法孩子表示法孩子-兄弟表示法(二叉树表示法),此处以更为常见的孩子兄弟表示法为例。

typedef struct CSNode
{
	int  data;
	CSNode* firstchild,*nextsibling;
	//
}*Tree;

2 存储结构图示

图源自网络
孩子-兄弟表示法又称为二叉树表示法或二叉链表表示法。链表中节点的两个指针域分别指向该节点的第一个孩子节点和下一个兄弟节点(即与第一个孩子节点在同一层)。

3 树的先序遍历

如果根节点为空,返回;否则访问根节点。然后依次遍历第一个孩子节点和它的兄弟节点(以其为根的子树),此处可以用一个for循环。下面以fc代表第一个孩子节点指针域,ns代表兄弟节点指针域。程序实现为:

void TreePreorder(Tree T,void visit(TElemType))
{
	Tree p;
	if(!T)return;
	visit(T->data);
	for(p=T->fc;p;p=p->ns)
	TreePreorder(p,visit);
}

读者可依据二叉树的某些操作实现树的一些操作,算法思路没有太大变化。

4 求树的深度

树的深度为各个子树的最大深度加一,可一次遍历递归所有子树,最后返回深度加一。程序实现:

int TreeDepth(Tree T)
{
	int m1;Tree p;
	int m2;//记录最大深度
	if(!T)return 0;
	for(m2=0,p=T->fc;p;p=p->ns)//循环求出子树深度最大值,最后加上根节点层
	{
		m1=TreeDepth(p);//以p为根节点的子树深度
		if(m1>m2)m2=m1;
	}
	return m2+1;
}

(四)总结

1 本篇文章总结了以二叉树为主的树的一些常用算法,包括对其的遍历与一些算法设计题目。大部分通过递归实现,可在理解递归基础上编写相应算法。
2 有关于树的数据结构还有很多,例如Huffman树,二叉排序树等主要数据结构和算法,读者可以自行学习,此处没有详细介绍。
3 本章讨论的树是数据结构中非线性数据结构中很重要的一类,是一种层次模型的数据结构。它可以较好地描述数据间地层次关系,能对数据机构随机再组织,故树一般成为动态数据结构。

(五)参考文献

【1】严蔚敏.吴伟民. 数据结构(C语言版). 北京:清华大学出版社,1997.
【2】齐悦.夏克俭.姚琳. 数据结构、算法与应用. 北京:清华大学出版社.2015.
【3】聂立新.桑兆阳.张华清. 数据结构与算法. 中国石油大学出版社.2017.

小白第二篇,受限于知识水平与写作能力,文中不足之处,敬请大家批评指正!
  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值