欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

数据结构 树与二叉树

程序员文章站 2022-06-17 19:14:05
...

概述

树是非常重要的非线性结构,在该结构中,一个数据元素可以有两个或以上的直接后继元素,树广泛用于描述客观世界中大量存在的层次结构关系。

其中,又以二叉树最为常用。下面先介绍一下树中常用的术语,然后给出树和二叉树的定义,最后简单实现二叉树。

术语

  • 结点(Node):包含一个数据元素及指向其子树的分支。
  • 结点的度(Degree):结点所拥有的子树数目。
  • 树的度:整棵树中各结点的度的最大值
  • 叶子(Leaf):结点的度为0的节点,即没有任何后继的结点。也称为终端结点
  • 孩子(Child)/子结点:结点的子树称作该结点的孩子。
  • 双亲(Parent)/父结点:与子结点对应,子结点的前驱结点即子结点的父结点。
  • 兄弟(Sibling):拥有同一个父结点的结点称为兄弟。
  • 祖先,子孙:与上述类似推断即可。
  • 层次(Level):将根定义为**第一层**,随后逐渐往下递增。(有些书籍为了方便编程的描述会将根定义为第0层,其实质无二)
  • 树的深度(Depth)/高度:树中结点的最大层次称为树的深度。
  • 有序树:树的结点之间是有序的,不可随意互换的
  • 无序树:树的结点之间是无序的,可随意互换的。
  • 森林(Forest)m(m0)m(m \geq 0)互不相交的树的集合。

树(Tree)的定义

树是n(n0)n(n \geq 0)个结点的有限集,在任意一棵非空树中:

  1. 有且仅有一个**根(Root)**结点
  2. n>1n>1时,其余结点可分为m(m>0)m(m>0)个互不相交的有限集,其中每个集合本身又是一棵树,称为子树(Sub Tree)

数据结构 树与二叉树

二叉树(Binary Tree)

二叉树是一种特殊的树,也是最常用的树结构。特点是每个结点最多只有两棵子树,并且有次序之分

特殊形态

二叉树树有两种特殊形态,完全二叉树满二叉树,满二叉树也是完全二叉树,反之则不一定。如下图:

数据结构 树与二叉树

完全二叉树(Complete Binary Tree)的性质

一颗只有最下面两层上的结点度小于2,并且最下一层的结点都在左侧组成的树称为完全二叉树,其具有如下性质:

  1. 叶子结点只出现在最下面两层。
  2. 对于任意结点,如果右子树的高度为hh,那么左子树的高度只能是hhh+1h+1
  3. 具有nn个结点的完全二叉树深度为k=log2n+1k = \lfloor \log_2n \rfloor +1

满二叉树(Full Binary Tree)的性质

一棵深度为k且有2k12^{k}-1个结点的二叉树称为满二叉树,其具有如下性质:

  1. 每一层上的结点都是满的。
  2. 显而易见,满二叉树不存在度为1的结点,要么度为2,要么都是叶子结点。

二叉树的性质

  1. 在二叉树的第ii层上最多有2i1(i1)2^{i-1}(i \geq 1) 个结点。

  2. 深度为kk的二叉树最多有2k1(k1)2^{k-1}(k \geq 1)个结点。

  3. 对任一二叉树TT,如果其终端结点数为n0n_0,度为2的结点数为n2n_2,则有:n0=n2+1n_0 = n_2+1

    其证明如下:

    n1n_1TT中度为1的结点数,因为二叉树所有结点的度均小于等于2。因此结点总数为n=n0+n1+n2n = n_0 + n_1 + n_2

    又有除根结点外其余结点都有一个分支进入,设BB为分支总数,则n=B+1n = B+1,由于分支总是度为1或2的结点射出,所以又有B=n1+2n2B = n_1 + 2n_2,因此又有:n=n1+2n2+1n = n_1 + 2n_2 +1

    将上两式合并得:
    n0=n2+1{n=n0+n1+n2n=n1+2n2+1 n_0 = n_2 + 1 \begin {cases} n = n_0 + n_1 + n_2\\ n = n_1 + 2n_2 +1 \end {cases}

  4. 如果对一棵nn个结点的完全二叉树的结点按层编号(从第1层到log2n+1\lfloor \log_2n \rfloor+1层,每层自左向右),则任一结点i(1in)i(1 \leq i \leq n)有:

    • 如果i=1i=1,则结点ii是二叉树的根。如果i>1i>1,则其父结点是i/2\lfloor i / 2\rfloor
    • 如果2i>n2i > n,则结点ii无左子结点(ii为叶子结点);否则,其左子结点为2i2i
    • 如果2i+1>n2i+1 > n,则结点ii无右子结点;否则其右子结点为2i+12i +1

    如下图所示:

    数据结构 树与二叉树

二叉树的存储结构

顺序存储

这种方式实现比较简单,原理是利用完全二叉树按层编号后每个子树所在编号都可以计算得出,但缺点是在存储非完全二叉树的情况下非常浪费空间,尤其是倾斜的二叉树(即只有一边子树的情况)时,但优点是寻址快、实现简单。如要定位结点B的左子树只需要计算2×(B)=42\times (B的编号) = 4即可得出。其结构见下图:

数据结构 树与二叉树

链式存储

为了避免存储时造成空间的浪费,也可以使用链式结构存储,见下图:

数据结构 树与二叉树

这样一来,就不会产生空间的浪费,这种最普通的方式称为二叉链表。有时候为了能快速的定位一个结点的父结点,也可以为每个结点中增加一个指针指向父结点。

C实现

顺序存储很直观,就不写了,链式存储的实现如下,这里实现的是二叉搜索树,删除比较特殊,另外写了一篇文 二叉搜索树的删除

//二叉树的链式存储
//假定存储数据类型为int 其结点定义如下
struct BTreeNode;

typedef struct BTree {
	int cnt;
	struct BTreeNode *root;
}BTree; //头结点

typedef struct BTreeNode
{
	int data;
	struct BTreeNode *left;
	struct BTreeNode *right;
}BTreeNode;

//辅助函数,负责申请内存
static BTreeNode *newNode(int dat, BTreeNode *left, BTreeNode *right)
{
	BTreeNode *p = (BTreeNode *)malloc(sizeof(BTreeNode));
	if (p == NULL)
		return NULL;

	p->data = dat;
	p->left = left;
	p->right = right;
	return p;
}

//初始化
void BT_initialize(BTree * t)
{
	t->cnt = 0;
	t->root = NULL;
}

//插入一个新的元素
int BT_insert(BTree * t, int dat)
{
	BTreeNode *p = newNode(dat, NULL, NULL);
	BTreeNode *node = t->root;
    
	if (t->cnt == 0)
	{
		t->root = p;
		t->cnt++;
		return 1;
	}

	while (1) //一直按规则寻找,直到找到合适的位置
	{
		if (dat <= node->data) //如果待插入的数比现结点小,则往左走
		{
			if (node->left == NULL) //如果已经没有左子结点了,那就代表找到了待插入的位置
			{
				node->left = p;
				break;
			}
			else
				node = node->left; //否则递进一层
		}
		else if (dat > node->data) //右也是同理
		{
			if (node->right == NULL)
			{
				node->right = p;
				break;
			}
			else
				node = node->right;
		}
	}
	t->cnt++;
	return 1;
}

二叉树的遍历

遍历是二叉树的核心,又有前序、中序、后序、层序四种遍历方式。

前中后的顺序指的是数据什么时候被访问,如前序则是访问一个结点的时候就访问数据,随后再处理左右子树;中序则是先处理左子树,然后再访问数据,最后再处理右子树;后序则是先处理左右子树,最后再访问数据;

它们的遍历路线看起来是这样的,后序层序就不画了,类似的,红色的代表访问数据:

数据结构 树与二叉树

C实现

其中前三者最简单的方式就是使用递归实现,但也可以用栈代替,递归本质就是栈。

//前序遍历,先访问数据,随后处理左子树、右子树
void BT_preOrderTravese(BTreeNode *t, void(*pFun)(int *item))
{
	if (t == NULL)
		return;
	pFun(&(t->data)); //采用递归的写法一目了然,十分易懂。
	BT_preOrderTravese(t->left, pFun);
	BT_preOrderTravese(t->right, pFun);
}

//中序遍历,先处理左子树,然后访问数据,然后处理右子树
void BT_inOrderTravese(BTreeNode *t, void(*pFun)(int *item))
{
	if (t == NULL)
		return;
	BTreeNode **pStack = (BTreeNode **)malloc(sizeof(BTreeNode**) * 128);
	int top = -1;
	//递归的程序总能转化为使用栈的程序,这样效率会更高
	while (top >= 0 || t != NULL) //当栈为空且当前结点为空则退出
	{
		if (t != NULL)
		{
			pStack[++top] = t; //当前结点非空则入栈,且继续往左子树递进
			t = t->left;
		}
		else
		{//如果当前结点是空的,出栈,访问数据随后递进至右子树
			t = pStack[top--];
			pFun(&(t->data));
			t = t->right;
		}
	}
	free(pStack);
}

//层序遍历,一层一层往下访问
void BT_levelOrderTravese(BTreeNode *t, void(*pFun)(int *item))
{
	if (t == NULL)
		return;
	BTreeNode **pQueue = (BTreeNode**)malloc(sizeof(BTreeNode**) * 128);
	int front = 0, rear = 0;
	pQueue[front] = t;
	while (rear >= front)
	{//层序可以通过队列实现,每到一个结点,先访问数据,然后将左右子树入队。
		t = pQueue[front++];
		pFun(&(t->data));
		if( t->left != NULL) pQueue[++rear] = t->left;
		if( t->right != NULL) pQueue[++rear] = t->right;
	}
	free(pQueue);
}

参考资料:

《数据结构》 严蔚敏、吴伟民著 清华大学出版社 2007

《算法之美》 左飞著 电子工业出版社 2016