树
对于大量的输入数据,链表的线性访问时间太慢,不宜使用。本章讨论一种简单的数据结构,其大部分操作的运行时间平均为O(log N)。我们要简述这种数据结构上在概念上的简单修改,它保证了在最坏清醒下上述的时间界。此外,还讨论了第二种修改,对于长的指令序列它基本上给出每种操作O(log N)运行时间。
这种数据结构叫做二叉查找树(binary search tree)。二叉查找树是两种库集合类TreeSet和TreeMap实现的基础。它们用于许多应用之中。我们将:
- 看到树是如何用于实现几个流行的操作系统中的文件系统
- 看到树如何能够用来计算算术表达式的值
- 指出如何利用树支持以O(log N)平均时间进行的各种搜索操作,以及如何细化以得到最坏情况时间界O(log N)。讨论当数据被存放在磁盘上时如何实现这些操作
- 讨论并使用TreeSet类和TreeMap类
预备知识
树(tree)是一个或一个以上的节点(node)组成,存在一个特殊的节点,称为树的根(root)。每个节点是一些数据和指针组合而成的记录。除了树根,其余节点可以分为n>=0个互斥的集合,每个子集和本身也是一种树状结构,即此根节点的子树。此外,一棵合法的书,节点间可以相互连接,但不能形成无出口的回路。下图就是一棵不合法的树。
树还可以组成森铃,也就是n个互斥树的集合(n>=0),移去树根即为森林,下图就为包含三棵树的森林。
专有名词介绍
以下图为例:
-
度数(degree):每个节点所有子树的个数。例如图中节点B的度数是2,D的度数为3,FKIJ等的度数为0
-
层数(level):树的层数,假设树根A为第一层,BCD节点的层数为2,EFGHIJ的层数为3。
-
高度(height):树的最大层数,图所示的树的高度为4
-
树叶或称终端节点(terminal nodes):度数为零的节点就是树叶,如图KLFGMIJ就是树叶。
-
父节点(parent):每个节点有链接的上一层节点(即父节点),如图所示,F的父节点为B,而B的父节点为A。
树的实现
实现树的一种方法可以是在每一个节点除数据外还要有一些链,使得该节点的每一个儿子都有一个链指向它。然而儿子的数目不确定,因此可以将每个节点的所有儿子都放在树节点的链表。
声明如下:
class TreeNode{
Object element;
TreeNode firstChild;
TreeNode nextSibling;
}
下图指出了一棵树是如何用这种实现方法表示出来的。图中向下的箭头是指向firstChild(第一儿子)的链,而水平箭头是指向nextSibling(下一兄弟)的链,因为null链太多了所以省略。
树的遍历及应用
前序遍历
树有很多应用,流行的用法之一是包括UNIX和DOS在内的许多常用操作系统中的目录结构。图4-5是UNIX文件系统中一个典型的目录。
这个目录的根是/usr。/usr有三个子目录:mark,alex,bill,它们自己也是目录。
设我们想要列出目录中所有文件的名字。输出格式将是:深度di的文件将被di次tab缩进后打印名字。以下为伪码:
算法的核心为递归方法listAll,算法逻辑简单易懂,文件对象的名字和适当的跳格次数一起打印出来。如果是一个目录,那么以递归方式一个一个处理它所有的儿子。这些儿子均处在下一层的深度上。
这种遍历策略叫做先序遍历(preorder traversal)。在先序遍历中,对节点的处理工作是在它的诸儿子节点被处理之前进行的。
前序遍历首先访问根节点然后遍历偏左子树,最后遍历偏右子树,在遍历偏左子树偏右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树。
若二叉树为空则结束返回,否则:
- 访问根节点
- 前序遍历左子树
- 前序遍历右子树
如图所示二叉树,前序遍历结果:ABDECF
已知后序遍历和中序遍历,就能确定前序遍历
class TreeNode{
int value;
TreeNode left;
TreeNode right;
TreeNode(int value){
this.value = value;
}
}
import java.util.Stack;
public class Test {
public static void main(String[] args) {
TreeNode[] node = new TreeNode[10];//以数组的形式生成一棵完全二叉树
for(int i = 0; i < 10; i++) {
node[i] = new TreeNode(i);
}
for(int i = 0; i < 10; i++) {
if(i*2+1 < 10) {
node[i].left = node[i*2+1];
}
if(i*2+2 < 10) {
node[i].right = node[i*2+2];
}
}
preOrder(node[0]);
}
//递归实现
public static void preOrderRe(TreeNode bitTree) {
System.out.println(bitTree.value);
TreeNode leftTree = bitTree.left;
if(leftTree != null) {
preOrderRe(leftTree);
}
TreeNode rightTree = bitTree.right;
if(rightTree != null) {
preOrderRe(rightTree);
}
}
//非递归实现
public static void preOrder(TreeNode bitTree) {
Stack<TreeNode> stack = new Stack<>();
while(bitTree != null || !stack.isEmpty()) {
while(bitTree != null) {
System.out.println(bitTree.value);
stack.push(bitTree);
bitTree = bitTree.left;
}
if(!stack.isEmpty()) {
bitTree = stack.pop();
bitTree = bitTree.right;
}
}
}
}
中序遍历
在二叉树中,中序遍历首先遍历左子树,然后访问根结点,最后遍历右子树。若二叉树为空则结束返回,否则:
- 中序遍历左子树
- 访问根节点
- 中序遍历右子树
中序遍历结果:DBEAFC
import java.util.Stack;
public class Test1 {
public static void main(String[] args) {
TreeNode[] node = new TreeNode[10];//以数组的形式生成一棵完全二叉树
for(int i = 0; i < 10; i++) {
node[i] = new TreeNode(i);
}
for(int i = 0; i < 10; i++) {
if(i*2+1 < 10) {
node[i].left = node[i*2+1];
}
if(i*2+2 < 10) {
node[i].right = node[i*2+2];
}
}
midOrder(node[0]);
}
//递归实现
public static void midOrderRe(TreeNode bitTree) {
if(bitTree == null) {
return ;
}else {
midOrderRe(bitTree.left);
System.out.println(bitTree.value);
midOrderRe(bitTree.right);
}
}
//非递归实现
public static void midOrder(TreeNode bitTree) {
Stack<TreeNode> stack = new Stack<>();
while(bitTree != null || !stack.isEmpty()) {
while(bitTree != null) {
stack.push(bitTree);
bitTree = bitTree.left;
}
if(!stack.isEmpty()) {
bitTree = stack.pop();
System.out.println(bitTree.value);
bitTree = bitTree.right;
}
}
}
}
后序遍历
后序遍历右递归算法和非递归算法两种。
后续遍历首先遍历左子树,再遍历右子树,最后访问根节点,在遍历左,右子树时,仍然先遍历左子树,然后遍历右子树,最后遍历根节点,即:
若二叉树为空则结束返回,
否则:
- 后序遍历左子树
- 后序遍历右子树
- 访问根节点
后序遍历结果:DEBFCA
在遍历二叉树时有三次遍历,如前序遍历:A->B->D->D(D左子节点并返回到D)->D(D右子节点并返回到D)->B->E->E(左)->E(右)->->B->A->C->F->F(左)->F(右)->C->C(右),所以可以用栈结构,把遍历到的节点压进栈,没子节点时再出栈。也可以用递归的方式,递归的输出当前节点。
算法核心思想:
首先要搞清楚先序,中序,后序和非递归算法共同之处:用栈来保存先前走过的路径,以便可以在访问完子树后,可以利用栈中的信息,回退到当前节点的双亲节点,进行下一步操作。
后序遍历的非递归算法是三种顺序中最复杂的,原因在于,后序遍历是先访问左,右子树,再访问根节点,而在非递归算法中,利用栈回退时,并不知道是从左子树回退到根节点,还是从右子树回退到根节点:如果从左子树回退到根节点,此时就应该去访问右子树,而如果从右子树回退到根节点,此时就应该访问根节点。所以相比前序和后序,必须得在压栈时添加信息,以便在退栈时可以知道是从左子树返回,还是从右子树返回进而决定下一步的操作。
public class Test2 {
public static void main(String[] args) {
TreeNode[] node = new TreeNode[10];//以数组的形式生成一棵完全二叉树
for(int i = 0; i < 10; i++) {
node[i] = new TreeNode(i);
}
for(int i = 0; i < 10; i++) {
if(i*2+1 < 10) {
node[i].left = node[i*2+1];
}
if(i*2+2 < 10) {
node[i].right = node[i*2+2];
}
}
postOrder(node[0]);
}
//递归实现后序遍历
public static void postOrderRe(TreeNode bitTree) {
if(bitTree == null) {
return ;
}else {
postOrderRe(bitTree.left);
postOrderRe(bitTree.right);
System.out.println(bitTree.value);
}
}
//非递归实现后序遍历
public static void postOrder(TreeNode bitTree) {
int left = 1;//在辅助栈里表示左节点
int right = 2;//在辅助栈里表示右节点
Stack <TreeNode> stack = new Stack<>();
Stack <Integer> stack2 = new Stack<>();
//辅助栈,用来判断节点返回父节点时处于左节点还是右节点
while(bitTree != null || !stack.empty()) {
while(bitTree != null) {
stack.push(bitTree);
stack2.push(left);
bitTree = bitTree.left;
}
while(!stack.empty() && stack2.peek() == right) {
//如果是右节点,则下一步退回根节点,应该右节点出栈
stack2.pop();
System.out.println(stack.pop().value);
}
while(!stack.empty() && stack2.peek() == left) {
//如果是左节点,则下一步应该访问右节点,右节点进栈,左节点出栈
stack2.pop();
stack2.push(right);
bitTree = stack.peek().right;
}
}
}
}
二叉树
二叉树每个节点都不能多于两个儿子。
二叉树的一个性质是一棵平均二叉树的深度要比节点个数N小很多,其平均深度为(O√N),而对于特殊类型的二叉树,即二叉查找树,其深度的平均值是O(log N)
实现
例子:表达式树
上图显示一个表达式树的例子。表达式树的树叶是操作数,如常数或变量名,而其他节点为操作符。
我们可以通过递归产生一个带括号的左表达式,然后打印出在根处的运算符,最后再递归地产生一个带括号的右表达式而得到一个(对两个括号整体进行运算的)中缀表达式类型。
查找树ADT——二叉查找树
二叉树的一个重要的应用是它们在查找中的使用。使二叉树称为二叉查找树的性质是,对于树中的每个节点X,它的左子树中所有项的值小于X中的值,而它右子树中所有项的值大于X中的项。
图上右边不符合的原因是6的左子树中有一个节点是7
现在给出通常对二叉查找树进行的操作的简要描述。注意,由于树的递归定义,通常是递归地编写这些操作例程。因为二叉查找树的平均深度是O(log N),所以一般不必担心栈空间被用尽。
二叉查找树要求所有项都能够被排序。要写出一个一般的类,我们需要提供一个 interface 来表示这个性质。这个 interface 就是Comparable,该接口告诉我们,树中的两项总可以用compareTo方法进行比较。特别是我们不使用equals方法,而是根据两项相等当且仅当compareTo方法返回0来判断相等。
二叉搜索树是具有下列性质的二叉树:
- 若任何节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值
- 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
- 任意节点的左,右子树也分别为二叉搜索树;
- 没有键值相等的节点。
用Java来表示二叉树
public class BinarySearchTree {
//二叉搜索树类
public class Node{//节点类
int data;//数据域
Node right;//右子树
Node left;//左子树
}
private Node root;//树根节点
}
首先需要一个节点对象的类,这个对象包含数据域和指向节点的两个子节点的引用。
其次,需要一个树对象的类,这个对象包含一个根节点root。