树的四种遍历案例和AVL树的旋转的四种情况

树和二叉树

Posted by chensong on 2019-11-25 21::07::01

前言

现在感觉自己基础太差了, 需要要补一下基础了。于是学习王道数据结构分析非常详细。

正文

树与二叉树

一, 树的基本术语

  1. 树中一个结点的子结点个数称为该结点的度, 树中结点的最大度数称为树的度。如结点B的度为2,结点D的度为3,树的度为3.
  2. 度大于0的结点称为分支结点(又称非终端结点),度为0(没有子女结点)的结点称为叶子结点(又称终端结点)。在分支结点中,每个结点的分支数就是该结点的度。
  3. 结点的深度,高度和层次。

二, 树的性质

  1. 树中的结点数等于所有结点的度数加1。
  2. 度为m的树中第i层上至多有m^(i-1)个结点(i>=0)。
  3. 高度为h的m叉树至多有(m^h-1)/(m-1)个结点。
  4. 具有n个结点的m叉树的最小高度为[log m(n(m-1)+1)]。

三, 二叉树的概念

1, 几种特殊的二叉树

  1. 满二叉树。一颗高度为h。且含有2^h - 1个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点,上图所示。满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点之外的每个结点度数均为2。

可以对满二叉树按层续编号:约定编号从跟结点(跟结点编号为1)起,自上而下,自左而右。这样,每个结点对应一个编号,对于编号为i的结点,若有双亲,则其双亲为[i/2],若有左孩子,则左孩子为2i; 若有右孩子,则右孩子为2i+1。

  1. 完全二叉树。设一个高度为h,有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中编号为1到n的结点一一对应时,称为完全二叉树,如上图。这种树的特点如下:

①. 如i<[i/2],则结点i为分支结点,否则为叶子结点。

②. 叶子节点只可能在层次最大的两层上出现。对于最大层次中的叶子结点,都依赖排列在该层最左边的位置上。

③. 若有度为1的结点,则只可能有一个,且该结点只有左孩子而无右孩子。

④. 按层序编号后,一旦出现某个结点(编号为i)为叶子结点或只有左孩子, 则编号大于i的结点均为叶子结点。

⑤. 若n为奇数,则每个分支结点都有左子女和右子女; 若n为二数,则编号最大的分支结点(编号为n/2)只有左子女,没有右子女,其中分支结点左,右子女都有。

2, 二叉树的性质

  1. 非空二叉树上的叶子结点数等于度为2的结点数加1,即n0 = n2 + 1。
  2. 非空二叉树上第k层上至多有2^k-1结点(k>=1)。
  3. 高度为h的二叉树至多有2^h-1个结点(h>=1)。
  4. 对于完全二叉树按从上到下
  5. 具有n个(n>0)结点的完全二叉树的高度为[log2(n+1)]或者[ log2n +1]。

四, 二叉树的四种遍历和线索二叉树

1, 二叉树的四种遍历

所谓二叉树的遍历,是指按某条搜索路径访问树中的每个结点,使得每个结点均被服务一次,而且仅被访问一次。

由二叉树的递归定义可知,遍历一颗二叉树便要决定根结点N,左子树L和右子树R的访问顺序。按照先遍历左子树再遍历右子树的原则,常见的遍历次序有先序(NLR),中序(LNR),和后序(LRN)三种遍历算法,其中”序”指的是根结点再何时被访问。

① 先序遍历

先序遍历(preorder)的操作过程如下:

若二叉树为空,则什么也不做,否则,

1) 访问根结点; 2) 先序遍历左子树; 3) 先序遍历右子树;

对应的递归算法如下:

void preorder(BiTree T)
{
	if (T)
	{
		//访问根结点
		show(T); 
		//递归遍历左子树
		preorder(T->lchild);
		//递归遍历右子树
		preorder(T->rchild);
	}
}

如上图所示, 先序遍历所得的结点序列为 1, 2, 4, 8, 9, 5, 10, 11, 3, 6, 12, 13, 7, 14, 15

② 中序遍历

中序遍历(inorder)的操作过程如下: 若二叉树为空,则什么也不做,否则:

1) 中序遍历左子树, 2) 访问根结点 3) 中序遍历右子树

对应的递归算法如下:

void inorder(BiTree T)
{
	if (T)
	{
		// 递归遍历左子树
		inorder(T->lchild);
		//访问根结点
		show(T);
		// 递归遍历右子树
		inorder(T->rchild);
	}
}

对应上图二叉树, 中序遍历所得到的结点序列为 8, 4, 9, 2, 10, 5, 11, 1, 12, 6, 13, 3, 14, 15, 7

③ 后序遍历

后序遍历(postorder)的操作过程如下 若二叉树为空,则什么也不做;否则

1) 后序遍历左子树 2) 后序遍历右子树 3) 访问根结点

对应的递归算法如下

void postorder(BiTree T)
{
	if (T)
	{
		//递归遍历左子树
		postorder(T->lchild);
		// 递归遍历右子树
		postorder(T->rchild);
		//访问根结点
		show(T);
	}
}

对应上图二叉树 :后序遍历得到的结点序列为 8, 9, 4, 10, 11, 5, 2, 12, 13, 6, 14, 15, 7, 3, 1

总结三种遍历算法中,递归遍历左,右子树的顺序都是固定的, 只是访问根结点的顺序不同。不管采用哪种遍历算法,每个结点都访问一次且仅访问一次,故时间复杂度都是O(n)。在递归遍历中,递归工作栈的栈深恰好为树的深度,所以在再坏情况下,二叉树是有n个结点且深度为n的单支树,遍历算法的空间复杂度为O(n)。

④ 递归算法和非递归算法的转换

借助栈,可以将二叉树的递归遍历算法转换为非递归算法。下面以中序遍历为列子给出中序遍历的非递归算法。先扫描(并非访问)根结点的所有左结点并将它们一一进栈,然后出栈一个结点p(显然结点p没有左孩子结点或者左孩子结点已经被访问过),访问它。然后扫描该结点的右孩子结点,将其进栈,再扫描该右孩子结点的所有左结点并一一进栈,如此继续,直到栈空为止。

中序遍历的非递归算法如下:

void inorder2(BiTree T)
{
	//初始化栈
	initstatck(s);
	//p是遍历指针
	TiTree p = T;
	// 栈不空或者p不空时循环
	while (p || !s.emptr())
	{
		
		if (p)
		{
			//根指针进栈,遍历左子树
			s.push(p);
			//每遇到非空二叉树先向左走
			p = p->lchild;
		}
		else 
		{
			// 根指针退栈, 访问根结点,遍历右子树
			//退栈, 访问根结点
			s.pop(p);
			show(p);
			// 再向右子树走
			p = p->rchild;
		}
	}
}

后序遍历算法如下

void postorder2(BiTree T)
{
	if (T)
	{
		s.push(T);
		BiTree p = NULL;
		while (!s.empty())
		{
			p = s.top();
			// 判断当前lcheild和rchild是否已经再栈中 
			if (p->lchild && p->lchild != T && p->rchild != T)
			{
				s.push(p);
			}
			else if (p->rchild && p->rchild != T)
			{
				s.push(p);
			}
			else 
			{
				s.pop(p);
				show(p);
				T = p;
			}
		}
	}
}

再写非递归遍历时要有递归压栈的模型 来使用栈的数据结构

⑤ 层次遍历

如上图所示为二叉树的层次遍历,即按照箭头所指方向,按照1, 2, 3, 4 层次顺序,对二叉树中的各个结点进行访问。

要进行层次遍历,需要借助一个队列。先将二叉树根结点入队,然后出队,访问该结点,若它有左子树,则将左子树根结点入队,如它有右子树,则将右子树根结点入队。然后出队,对出队结点访问,如此反复,直到队列为空。

二叉树的层次遍历算法如下

void level_order(BiTree T)
{
	initqueue(q);
	BiTree p;
	//将根结点入队列中
	q.endqueue(T);
	//队列不空循环
	while (!q.empty())
	{
		//队头元素出队
		q.dequeue(p);
		show(p);
		
		if (p->lchild)
		{
			//左子树不空,则左子树入队列
			q.endqueue(p->lchild);
		}
		if (p->rchild)
		{
			//右子树不空, 则右子树入队列
			q.endqueue(p->rchild);
		}
	}
}

五, 树与二叉树的应用

1, 平衡二叉树

① 平衡二叉树的定义

为避免树的高度增长过快,降低二叉排序树的性能, 我们规定在插入和删除二叉树的结点的时, 要保证任意结点的左, 右子树高度差的绝对不超过1, 将这样的二叉树称为平衡二叉树(Balanced Binary Tree), 简称平衡树(AVL)。定义结点左子树和右子树的高度差为该结点的平衡因子,则平衡二叉树结点的平衡因子的值只可能是-1, 0或1。 因此,平衡二叉树可定义的为或者是一颗空树, 或者是具有下列性质的二叉树; 它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度差的绝对值不超过1。如上图a所示 ,是平衡二叉树,如上图b所示是不平衡二叉树。结点种的值为该结点的平衡因子。

② 平衡二叉树的插入

二叉排序树保证平衡的基本思想如下:每当在二叉排序树种插入(或删除)一个结点时,首先检查其插入路径上的结点因为此次操作而导致了不平衡。若导致了不平衡,在保持二叉排序树特性的前提下,调整各结点的位置关系,使之重新达到平衡。

注意:每次调整的对象都是最小的不平衡子树,即以插入路径上离插入结点最近的平衡因子的绝对值大于1的结点作为根的子树。如上图所示框的为最小不平衡子树。

平衡二叉树的插入过程的前半部分与二叉排序树相同,但在新结点插入后,若造成查找路径上的某个结点不再平衡,则需要做出相应的调整。一般可将失去平衡后进行调整的规律归纳为下列4种情况:

1, LL平衡旋转(右单旋转)

由于在结点A的左孩子(L)的左子树(L)上插入了新的结点,A的平衡因子由1曾至2, 导致以A为根的子树失去平衡, 需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树。

2, RR平衡旋转(左单旋转)

由于在结点A的右孩子(R)的右子树(R)上插入了新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。将A的右孩子B向左上旋转替代A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原右子树则作为A结点的右子树,

3, LR平衡旋转(先左后右双旋转)

由于在A的左孩子(L)的右子树(R)上插入新的结点,A的平衡因子由1曾至2,导致以A为根的子树失去平衡,需要进行二次旋转操作,先左旋转后右旋转。先将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后再把该C结点向右上旋转提升到A结点的位置。

4, RL平衡旋转(先右后左双旋转)

由于在A的右孩子(R)的左子树(L)上插入新的结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要进行二次旋转操作,先右旋转后左旋转。先将A结点的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置,然后再把该结点向左上旋转提升到A结点的位置。

结语