目录
1、二叉树的抽象数据类型定义
对于二叉树,定义其结点则可轻松获得整颗二叉树(只需要知道根结点即可)
给出二叉树结点的ADT
public interface BinNode{
Object getElement();
void setElement(Object v);
BinNode getLeft();
void setLeft(BinNode p);
BinNode getRight();
void setRight(BinNode p);
boolean isLeaf();
}
2、二叉树的实现
(1)顺序存储
顺序二叉树用数组来实现,n个结点的二叉树可以使用大小为n+1的数组(为表示的便捷,0位置可以省略)来实现,因此不存在结构性开销,这一点比链式存储要好。
对于完全二叉树,我们可以发现在下面所示的顺序下,其数组下标与逻辑关系的联系:
- 非根结点(序号 i > 1)的父结点的序号是
- 结点(序号为 i )的左孩子结点的序号是 2i,(若2 i <= n,否则没有左孩子);
- 结点(序号为 i )的右孩子结点的序号是 2i+1,(若2 i +1<= n,否则没有右孩子);
此种存储方式只适用于完全二叉树,若是非完全二叉树采用此种形式存储,则会导致较大的空间资源浪费。
(2)链式存储
每个结点至少需要维护三个数据:数据区(该结点存储的元素)、两个指向子结点的指针
可见,树更适合于链式存储下实现
public class BinNode {
//链式二叉树的结点类
private Object element;//存储数据的元素
private BinNode left;//左子结点
private BinNode right;//右子结点
//constructor部分
public BinNode() {
}
public BinNode(Object element) {
this.element = element;
}
//getter与setter部分
Object getElement() {
return element;
}
void setElement(Object v) {
this.element = element;
}
BinNode getLeft() {
return left;
}
void setLeft(BinNode p) {
this.left = left;
}
BinNode getRight() {
return right;
}
void setRight(BinNode p) {
this.right = right;
}
boolean isLeaf() {
return (left == null) && (right == null);
}
//遍历、搜索、删除等操作见后文
}
(待整理)
数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低
链式存储方式的分析
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
三、树的周游
1、层次遍历
采用先上后下的遍历顺序:
- 若二叉树不为空。则访问其根结点
- 从左到右访问根结点的孩子节点
- 从左到右访问根结点的孙子结点
- ........
遍历的核心问题:二维结构的线性化,即从结点访问其左、右儿子结点,访问左儿子后,右儿子结点怎么办?
解决方法:需要一个存储结构保存暂时不访问的结点→队列
队列实现:遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队、访问该结点、其左右儿子入队
算法思想:
- 先将根结点入队,
- 从队列中取出一个元素;
- 访问该元素所指结点;
- 若该元素所指结点的左、右孩子结点非空,则将其左、右孩子的指针顺序入队。
- 依次重复2-4,直至队列为空。
void LevelOrderTraversal(BinNode root) {
Queue q = new LQueue();
BinNode node = root;
if (node == null) return; // 若是空树则直接返回
q.enqueue(node);
while (!q.isEmpty()) {
node = (BinNode) q.dequeue();
System.out.println(node); //(访问)打印结点
if (node.getLeft() != null) q.enqueue(node.getLeft());
if (node.getRight() != null) q.enqueue(node.getRight());
}
}
2、周游(先序周游、中序周游和后序周游)
下文以先左后右的遍历形式为例
(1)先序周游
即先(根)序遍历,遍历过程为:访问根结点→先序遍历其左子树→先序遍历其右子树。
采用递归实现
void preOrder(BinNode root) {
if (root == null) return;//空树
visit(root);
preOrder(root.getLeft());
preOrder(root.getRight());
}
非递归算法实现的基本思路:使用堆栈
算法思想:
- 遇到一个结点,先访问它;
- 再把它压栈,并去遍历它的左子树;
- 当左子树遍历结束后,从栈顶弹出这个结点;
- 然后按其右指针再去中序遍历该结点的右子树。
void preOrder(BinNode root) {
BinNode node=root;
Stack s = new Stack(); //创建并初始化堆栈s
while ((node != null) || !s.isEmpty()) {
while (node != null) {
System.out.println(node); //(访问)打印结点
s.push(node); //一直向左并将沿途结点压入堆栈
node=node.getLeft();
}
if (!s.isEmpty()) {
node= (BinNode) s.pop(); //结点弹出堆栈
node=node.getRight(); //转向右子树
}
}
}
(2)中序周游
即中(根)序遍历,遍历过程为:中序遍历其左子树→访问根结点→中序遍历其右子树。
采用递归实现
void infixOrder(BinNode root) {
if (root == null) return;//空树
infixOrder(root.getLeft());
visit(root);
infixOrder(root.getRight());
}
非递归算法实现的基本思路:使用堆栈
算法思想:
- 遇到一个结点,就把它压栈,并去遍历它的左子树;
- 当左子树遍历结束后,从栈顶弹出这个结点并访问它;
- 然后按其右指针再去中序遍历该结点的右子树。
void infixOrder(BinNode root) {
BinNode node=root;
Stack s = new Stack(); //创建并初始化堆栈s
while ((node != null) || !s.isEmpty()) {
while (node != null) { //一直向左并将沿途结点压入堆栈
s.push(node);
node=node.getLeft();
}
if (!s.isEmpty()) {
node= (BinNode) s.pop(); //结点弹出堆栈
System.out.println(node); //(访问)打印结点
node=node.getRight(); //转向右子树
}
}
}
(3)后序周游
即后(根)序遍历,遍历过程为:后序遍历其左子树→ 后序遍历其右子树→访问根结点。
采用递归实现
void postOrder(BinNode root) {
if (root == null) return;//空树
postOrder(root.getLeft());
postOrder(root.getRight());
visit(root);
}
非递归算法实现的基本思路:使用堆栈
算法思想:
对于父节点的访问输出,需要在其右子树遍历完成的前提下进行。故不能像前中序遍历一样,在遍历完左子树后,就直接出栈,而是需要利用这个未出栈的栈顶元素去获取右子树,在遍历完右子树后,就可以出栈,并对此节点进行访问输出。
因此,需要使用一个标记,以区分是从左子树取栈还是从右子树出栈:
算法步骤:
从当前结点开始遍历:
1. 若当前结点存在,就存入栈中,并且置结点访问次数visit为1(第一次访问),然后访问其左子树;
2. 直到当前结点不存在,需要回退,这里有两种情况:
1)当栈顶结点访问次数visit为1时,表明是从左子树回退,这时需置栈顶结点访问次数visit为2(第二次访问),然后通过栈顶结点访问其右子树(取栈结点用,但不出栈)
2)当栈顶结点访问次数visit为2时,则表明是从右子树回退,这时需出栈,并取出栈结点做访问输出。(需要注意的是,输出完毕需要置当前结点为空,以便继续回退。)
3. 不断重复1、2,直到当前结点不存在且栈空。
int visit=0;
void postOrder(BinNode root) {
//需在BinNote类中给结点增加访问次数的属性visit,初始化为0
BinNode node=root;
Stack s = new Stack(); //创建并初始化堆栈s
while ((node != null) || !s.isEmpty()) {
while (node != null) {
if (node.visit == 0) {//虽然没必要判断,为便于理解
node.visit++;
s.push(node); //第一次入栈,不访问
}
node=node.getLeft(); //转向左子树
}
if (!s.isEmpty()) {
node= (BinNode) s.pop(); //结点弹出堆栈
if (node.visit == 2) {
System.out.println(node); //第三次碰到它,访问节点
node = null;//左右子数均已经访问过,可以彻底从堆栈弹出了
}
else {
node.visit++;
s.push(node); //第二次入栈,不访问,(相当于node没有出栈)
node=node.getRight(); //转向右子树
}
}
}
}
在均采用先左后右(或先右后左)的遍历方式时,先序、中序和后序遍历过程中经过结点的路线一样,只是访问各结点的时机不同。在遍历的整个过程中,每个结点都有三次被访问的机会。
图中在从入口到出口的曲线上用、
和
三种符号分别标记出了先序、中序和后序访问各结点的时刻。
检索和删除待整理学习
四、遍历二叉树的应用与相关计算
1、输出二叉树中的叶子结点
思想:在二叉树的遍历算法中增加检测结点的“左右子树是否都为空”。
void OrderPrintLeaves(BinNode root) {
if (root!=null) {
if (root.isLeaf())
System.out.println(root);
OrderPrintLeaves(root.getLeft());
OrderPrintLeaves(root.getRight());
}
}
2、求二叉树的高度
思想:采用递归思想,求左右子树高度,知道了左右子树的高度后取最大+1,即为该树的高度。
因此,不难看出本题采用的是后序遍历方式。
int getHeight(BinNode root) {
//采用 postOrder
int height = 0, leftHeight = 0, rightHeight = 0;
if (root==null) return height;
leftHeight=getHeight(root.getLeft()); //求左子树的深度
rightHeight=getHeight(root.getRight()); //求右子树的深度
height=Math.max(leftHeight,rightHeight)+1; //取左右子树较大的深度
return height;
}
3、二元运算表达式树及其遍历
三种遍历可以得到三种不同的访问结果:
- 先序遍历得到前缀表达式:+ + a * b c * + * d e f g
- 中序遍历得到中缀表达式:a + b * c + d * e + f * g //中缀表达式会受到运算符优先级的影响
- 后序遍历得到后缀表达式:a b c * + d e * f + g * +
4、二叉树的计数
(1)由两种遍历序列确定二叉树
二叉树遍历的结果是将一个非线性结构中的数据通过访问排列到一个线性序列中。
Q:已知三种遍历中的任意两种遍历序列,能否唯一确定一棵二叉树?
A:二叉树的前序序列和中序序列可以唯一的确定一棵二叉树
二叉树的中序序列和后序序列可以唯一的确定一棵二叉树
Q:只知道前序序列和后序序列为何不可以唯一确定一棵二叉树?
A:在没有中序序列时,给出先序遍历序列:A,B;后序遍历序列:B A。其非线性结构可以有以下两种:
e.g. 先序和中序遍历序列来确定一棵二叉树,步骤:
- 根据先序遍历序列第一个结点确定根结点;
- 根据根结点在中序遍历序列中分割出左右两个子序列;
- 对左子树和右子树分别递归使用相同的方法继续分解。
(2)具有n个结点的不同二叉树棵数
证明:(待整理)
(1)先考虑只有一个节点的情形,设此时的形态有f(1)种,那么很明显f(1)=1
(2)如果有两个节点呢?我们很自然想到,应该在f(1)的基础上考虑递推关系。那么,如果固定一个节点后,左右子树的分布情况为1=1+0=0+1,故有f(2) = f(1) + f(1)
(3)如果有三个节点,(我们需要考虑固定两个节点的情况么?当然不,因为当节点数量大于等于2时,无论你如何固定,其形态必然有多种)我们考虑固定一个节点,即根节点。好的,按照这个思路,还剩2个节点,那么左右子树的分布情况为2=2+0=1+1=0+2。
所以有3个节点时,递归形式为f(3)=f(2) + f(1)*f(1) + f(2)。(注意这里的乘法,因为左右子树一起组成整棵树,根据排列组合里面的乘法原理即可得出)(4)那么有n个节点呢?我们固定一个节点,那么左右子树的分布情况为n-1=n-1 + 0 = n-2 + 1 = … = 1 + n-2 = 0 + n-1。此时递归表达式为f(n) = f(n-1) + f(n-2)f(1) + f(n-3)f(2) + … + f(1)f(n-2) + f(n-1)
【其他使用Catalan数解决的问题】
(1)矩阵链乘: P=a1×a2×a3×……×an,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?
(2)一个栈(无穷大)的进栈序列为1,2,3,..n,有多少个不同的出栈序列?
(3)有2n个人排成一行进入剧场。入场费5元。其中只有n个人有一张5元钞票,另外n人只有10元钞票,剧院无其它钞票,问有多少中方法使得只要有10元的人买票,售票处就有5元的钞票找零?(将持5元者到达视作将5元入栈,持10元者到达视作使栈中某5元出栈)
(4)将一个凸多边形区域分成三角形区域的方法数?
(5)在圆上选择2n个点,将这些点成对连接起来,使得所得到的n条线段不相交的方法数。
(6)一位大城市的律师在她住所以北n个街区和以东n个街区处工作。每天她走2n个街区去上班。如果她从不穿越(但可以碰到)从家到办公室的对角线,那么有多少条可能的道路?
拓展:树与栈的关系:
以三个结点为例,树有以下五种形态:
对于此五种形态,先序遍历的序列均为abc;
中序遍历则分别为:cba、bca、bac、acb、abc
故不难发现,先序遍历即元素入栈的顺序(中途可出栈),中序遍历即元素出栈的五种可能的顺序