树的基本概念
树的定义
树是
N
N
N(
N
≥
0
N\geq0
N≥0)个结点的有限集合,N=0时,称为空树。
对于任意一个非空树,都有:
- 有且仅有一个特定的称为根的结点。
- 当N>1时,其余结点可分为m(m>0)个互不相交的有限集合 T 1 T_1 T1, T 2 T_2 T2, …, T m T_m Tm,其中每一个集合本身又是一棵树,并且称为根结点的子树。
树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
- 树的根节点没有前驱结点,除了根节点之外的所有结点有且仅有一个前驱节点。
- 树中所有结点可以有零个或多个后继节点。
基本术语
结点: 包含一个数据元素及若干指向其子树的分支。
结点的度数: 结点的非空字数个数。
叶子: 度为0的结点。
树的度: 树中各结点的度的最大值。
孩子: 结点的子树的根。
双亲: 结点的直接前驱。
兄弟: 同一个双亲的孩子之间互为兄弟。
祖先结点: 结点的祖先节点是指从根节点到该节点的路径上的所有结点。
子孙结点: 结点的子孙结点是指该结点的子树中的所有结点。
结点层次: 结点的层次从树根开始定义,根为第一层,根的孩子为第二层,如此。
树的深度: 树中所有结点层次的最大值。
森林: m(m
≥
\geq
≥ 0)棵互不相交的树的集合称为森林。
有序树和无序树: 树中结点的各棵子树从左到右是有特定次序的树称为有序树,否则称为无序树。
二叉树
定义
二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
特点
- 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某节点只有一颗子树,也要区分它是左子树还是右子树。
性质
- 在二叉树的第i层至多有 2 i − 1 2^{i-1} 2i−1个结点( i ≥ 1 i\geq1 i≥1)。
- 深度为k的二叉树至多有 2 k 2^k 2k-1个结点( k ≥ 1 k\geq1 k≥1)。
- 对任意一棵二叉树T,若终端结点数为 n 0 n_0 n0,度为2的结点数为 n 2 n_2 n2,则 n 0 n_0 n0 = n 2 n_2 n2 + 1。
特殊二叉树
满二叉树: 深度为k且含有
2
k
−
1
2^k-1
2k−1个结点的二叉树称为满二叉树。对其编号,从根开始,按层从上到下,层内从左到右,逐个对每一个结点进行编号1,2,…, n。
完全二叉树: 深度为k,结点数为n(
n
≤
2
k
−
1
n{\leq}2^k-1
n≤2k−1)的二叉树,当且仅当其n个结点与满二叉树中连续编号为1至n的结点位置一一对应时,称为完全二叉树。
完全二叉树的重要特征
- 所有叶子节点只可能出现在层号最大的两层上。
- 对任意结点,若其右子树的层高为k,则其左子树的层高只可为k或k+1。
- 满二叉树必为完全二叉树,而完全二叉树不一定是满二叉树。
性质
- 具有n个结点的完全二叉树的深度为⌊ l o g 2 n log_2n log2n⌋+1。
- 对于具有n个结点的完全二叉树,如果按照对满二叉树结点进行连续编号的方式,对所有的结点从1开始顺序编号,则对于任意序号为
i
i
i的结点有:
(1) 如果 i i i=1,则结点 i i i为根,其无双亲结点;如果 i i i>1,则结点 i i i的双亲结点序号为⌊ i / 2 i/2 i/2⌋。
(2) 如果2 i ≤ n i{\leq}n i≤n,则结点 i i i的左孩子节点序号为 2 i 2i 2i,否则,结点 i i i无左孩子。
(3)如果 2 i + 1 ≤ n 2i+1{\leq}n 2i+1≤n,则节点 i i i的右孩子序号为 2 i + 1 2i+1 2i+1,否则,结点 i i i无右孩子。
二叉树的存储
1. 顺序存储结构
对于满二叉树和完全二叉树来说,可以根据结点的编号次序,将其结点数据存储到一组连续的存储单元中,即用一维数组作存储结构。
顺序存储结构描述如下:
2.链式存储结构
二叉链表的结点结构包括:数据域(存放结点的值)、左孩子域(存放该节点的左孩子结点的存储地址)、右孩子域(存放该节点的右孩子结点的存储地址)。
三叉链表的结点结构在二叉链表上再增加一个父结点域(存放该节点的父节点的存储地址)。
二叉链表存储结构的结点类描述如下:
public class BiTreeNode {
public Object data;
public BiTreeNode lchild, rchild;
// 构造一个空结点
public BiTreeNode() {
this(null);
}
// 构造一棵左右孩子为空的二叉树
public BiTreeNode(Object data) {
this(data, null, null);
}
// 构造一棵数据域和左右孩子域都不为空的二叉树
public BiTreeNode(Object data, BiTreeNode lchild, BiTreeNode rchild) {
this.data = data;
this.lchild = lchild;
this.rchild = rchild;
}
}
一个二叉树含有 n n n个结点,则它的二叉链表中比含有 2 n 2n 2n个指针域,而仅有 n − 1 n-1 n−1个指针域指向孩子,其余的 n + 1 n+1 n+1个指针域为空的链域。
二叉树的遍历
1. 层次遍历
若二叉树为空,则为空操作。否则,先访问第0层的根结点,然后从左至右一次访问第1层的每一个结点,依此类推,当第
i
i
i层的所有结点访问完后,再从左到右一次访问第
i
+
1
i+1
i+1层的每一个结点,直到最后一层的所有结点都访问完为止。
2. 先序遍历(DLR)
若二叉树为空,则为空操作。否则,
(1)访问根节点
(2)先根遍历左子树
(3)先根遍历后子树
3. 中序遍历(LDR)
若二叉树为空,则为空操作。否则,
(1)中根遍历左子树
(2)访问根节点
(3)中根遍历后子树
4. 后序遍历(LRD)
若二叉树为空,则为空操作。否则,
(1)后根遍历左子树
(2)先根遍历后子树
(3)访问根节点
如上图:
层次遍历序列:ABCDEFGH
先序遍历序列:ABDEGCFH
中序遍历序列:DBGEAFHC
后序遍历序列:DGEBHFCA
递归和非递归方法遍历二叉树
//递归方法
//先序遍历
public void preRootTraverse(BiTreeNode T) {
if (T != null) {
System.out.println(T.data);
preRootTraverse(T.lchild);
preRootTraverse(T.rchild);
}
}
//中序遍历
public void inRootTraverse(BiTreeNode T) {
if (T != null) {
inRootTraverse(T.lchild);
System.out.println(T.data);
inRootTraverse(T.rchild);
}
}
//后序遍历
public void postRootTraverse(BiTreeNode T) {
if (T != null) {
postRootTraverse(T.lchild);
postRootTraverse(T.rchild);
System.out.println(T.data);
}
}
/**
* 1.先序非递归实现二叉树遍历
* 算法描述
* 从根开始,当前结点存在或栈不为空,
* (1)访问根结点,当前结点进栈,进入其左子树,重复知道当前节点为空
* (2)若栈非空,则退栈顶元素,进入右子树。
*/
public void preRootTraverse1(BiTreeNode root) {
Stack s = new Stack();
BiTreeNode p = root;
while (p != null || !s.isEmpty()) { //当前结点指针及栈均为空,结束
while (p != null) {
System.out.println(p.data);
s.push(p);
p = p.lchild;
}
if (!s.isEmpty()) {
p = (BiTreeNode)s.pop();
p = p.rchild;
}
}
}
/**
* 2.先序非递归实现二叉树遍历
* 算法描述
* (1)创建一个战对象,根节点入栈
* (2)当栈为非空时,将栈顶元素退栈并访问该节点。
* (3)对当前访问结点的非空左孩子结点相继依次访问,并将当前访问结点的非空右孩子结点压入栈内。
*/
public void preRootTraverse2(BiTreeNode root) {
BiTreeNode p = root;
if (p != null) {
Stack s = new Stack();
s.push(p);
while (!s.isEmpty()) {
p = (BiTreeNode)s.pop();
System.out.println(p.data);
while (p != null) {
if (p.lchild != null)
System.out.println(p.lchild.data);
if (p.rchild != null)
s.push(p.rchild);
p = p.lchild;
}
}
}
}
/**
* 3.中序非递归实现二叉树遍历
* 算法描述
* 从根开始,当前结点存在或者栈不为空
* (1)当前结点进栈,进入其左子树,重复直到当前结点为空。
* (2)若栈非空,则退栈,访问出栈结点,并进入其右子树。
*/
public void inRootTraverse1(BiTreeNode root) {
Stack s = new Stack();
BiTreeNode p = root;
while (p != null || !s.isEmpty()) {
while (p != null) {
s.push(p);
p = p.lchild;
}
if (!s.isEmpty()) {
p = (BiTreeNode) s.pop();
System.out.println(p.data);
p = p.rchild;
}
}
}
/**
* 4. 后序非递归实现二叉树遍历
* q记录刚刚访问过的结点
* 算法描述:
* 从根开始,当前结点存在或者栈不为空
* (1)当前结点进栈,并进入其左子树,重复直至当前节点为空。
* (2)若栈非空,判栈顶结点p的右子树是否为空、右子树是否刚访问过,
* 是,则退栈,访问p结点,p赋值给q,p置为空;不是,则进入p的右子树。
*/
public void postRootTraverse1(BiTreeNode root) {
Stack s = new Stack();
BiTreeNode p = root, q = null;
while (p != null || !s.isEmpty()) {
while (p != null) {
s.push(p);
p = p.lchild;
}
if (!s.isEmpty()) {
p = (BiTreeNode)s.peek();
if (p.rchild == null || p.rchild == q) {
s.pop();
System.out.println(p.data);
q = p;
p = null;
} else
p = p.rchild;
}
}
}
/**
* 5. 层次遍历二叉树
* 算法描述
* (1)创建一个队列对象,根节点入队
* (2)若队列非空,则将队首结点出队并访问该节点,再将该节点的非空左右孩子结点依次入队。
*/
public void levelTraverse(BiTreeNode root) {
BiTreeNode p = root;
if (p != null) {
LinkedBlockingQueue q = new LinkedBlockingQueue();
q.offer(p);
while (q != null) {
p = (BiTreeNode) q.poll();
System.out.println(p.data);
if (p.lchild != null)
q.offer(p.lchild);
if (p.rchild != null)
q.offer(p.rchild);
}
}
}
线索二叉树
利用二叉链表中的空的指针域,将遍历过程中结点的前驱、后继信息保存下来,这既充分的利用了空间,也节省了动态遍历二叉树求取节点在遍历序列中的前驱和后继所需的时间。由此,便产生了线索二叉树。
线索二叉树的结点结构
- 若结点有左子树,则LChild域扔指向其左孩子;否则,LChild域指向其某种遍历序列中的直接前驱结点。
- 若结点有右子树,则RChild域仍指向其右孩子;否则,RChild域指向其某种遍历序列中的直接后继结点。
- 为避免混淆,结点结构增设两个布尔型的标志域:Ltag和Rtag,含义如下:
Ltag = 0,LChild域指向结点的左孩子;Ltag = 1,LChild域指向结点的遍历前驱。
Rtag = 0,RChild域指向结点的右孩子;Rtag = 1,RChild域指向结点的遍历后继。
存储结构
public class ThreadBitreeNode {
public Object data;
public ThreadBitreeNode lchild, rchild;
int ltag, rtag;
}