18 篇文章 0 订阅

对于大量的输入数据,链表的线性访问时间太慢,不宜使用。本章讨论一种简单的数据结构,其大部分操作的运行时间平均为O(log N)。我们要简述这种数据结构上在概念上的简单修改,它保证了在最坏清醒下上述的时间界。此外,还讨论了第二种修改,对于长的指令序列它基本上给出每种操作O(log N)运行时间。

这种数据结构叫做二叉查找树(binary search tree)。二叉查找树是两种库集合类TreeSet和TreeMap实现的基础。它们用于许多应用之中。我们将:

  • 看到树是如何用于实现几个流行的操作系统中的文件系统
  • 看到树如何能够用来计算算术表达式的值
  • 指出如何利用树支持以O(log N)平均时间进行的各种搜索操作,以及如何细化以得到最坏情况时间界O(log N)。讨论当数据被存放在磁盘上时如何实现这些操作
  • 讨论并使用TreeSet类和TreeMap类

预备知识

树(tree)是一个或一个以上的节点(node)组成,存在一个特殊的节点,称为树的根(root)。每个节点是一些数据和指针组合而成的记录。除了树根,其余节点可以分为n>=0个互斥的集合,每个子集和本身也是一种树状结构,即此根节点的子树。此外,一棵合法的书,节点间可以相互连接,但不能形成无出口的回路。下图就是一棵不合法的树。

image-20200223150707621

树还可以组成森铃,也就是n个互斥树的集合(n>=0),移去树根即为森林,下图就为包含三棵树的森林。

image-20200223151452261

专有名词介绍

以下图为例:
image-20200223151530868

  • 度数(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链太多了所以省略。

image-20200223161653514

树的遍历及应用

前序遍历

树有很多应用,流行的用法之一是包括UNIX和DOS在内的许多常用操作系统中的目录结构。图4-5是UNIX文件系统中一个典型的目录。

image-20200223162349238

这个目录的根是/usr。/usr有三个子目录:mark,alex,bill,它们自己也是目录。

设我们想要列出目录中所有文件的名字。输出格式将是:深度di的文件将被di次tab缩进后打印名字。以下为伪码:

image-20200223170501674

算法的核心为递归方法listAll,算法逻辑简单易懂,文件对象的名字和适当的跳格次数一起打印出来。如果是一个目录,那么以递归方式一个一个处理它所有的儿子。这些儿子均处在下一层的深度上。

这种遍历策略叫做先序遍历(preorder traversal)。在先序遍历中,对节点的处理工作是在它的诸儿子节点被处理之前进行的。

前序遍历首先访问根节点然后遍历偏左子树,最后遍历偏右子树,在遍历偏左子树偏右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树。

若二叉树为空则结束返回,否则:

  1. 访问根节点
  2. 前序遍历左子树
  3. 前序遍历右子树
image-20200223183229556

如图所示二叉树,前序遍历结果: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;
			}
		}
		
	}

}

中序遍历

在二叉树中,中序遍历首先遍历左子树,然后访问根结点,最后遍历右子树。若二叉树为空则结束返回,否则:

  1. 中序遍历左子树
  2. 访问根节点
  3. 中序遍历右子树
image-20200223195212395

中序遍历结果: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;
			}
			
		}
	}

}

后序遍历

后序遍历右递归算法和非递归算法两种。

后续遍历首先遍历左子树,再遍历右子树,最后访问根节点,在遍历左,右子树时,仍然先遍历左子树,然后遍历右子树,最后遍历根节点,即:

若二叉树为空则结束返回,

否则:

  1. 后序遍历左子树
  2. 后序遍历右子树
  3. 访问根节点
image-20200227120524197

后序遍历结果: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)

image-20200228165414752

实现

image-20200228165514558

例子:表达式树

image-20200228170018902

上图显示一个表达式树的例子。表达式树的树叶是操作数,如常数或变量名,而其他节点为操作符。

我们可以通过递归产生一个带括号的左表达式,然后打印出在根处的运算符,最后再递归地产生一个带括号的右表达式而得到一个(对两个括号整体进行运算的)中缀表达式类型。

查找树ADT——二叉查找树

二叉树的一个重要的应用是它们在查找中的使用。使二叉树称为二叉查找树的性质是,对于树中的每个节点X,它的左子树中所有项的值小于X中的值,而它右子树中所有项的值大于X中的项。

image-20200228172041925

图上右边不符合的原因是6的左子树中有一个节点是7

现在给出通常对二叉查找树进行的操作的简要描述。注意,由于树的递归定义,通常是递归地编写这些操作例程。因为二叉查找树的平均深度是O(log N),所以一般不必担心栈空间被用尽。

二叉查找树要求所有项都能够被排序。要写出一个一般的类,我们需要提供一个 interface 来表示这个性质。这个 interface 就是Comparable,该接口告诉我们,树中的两项总可以用compareTo方法进行比较。特别是我们不使用equals方法,而是根据两项相等当且仅当compareTo方法返回0来判断相等。

二叉搜索树是具有下列性质的二叉树:

  1. 若任何节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  3. 任意节点的左,右子树也分别为二叉搜索树;
  4. 没有键值相等的节点。

用Java来表示二叉树

public class BinarySearchTree {
	//二叉搜索树类
	public class Node{//节点类
		
		int data;//数据域
		Node right;//右子树
		Node left;//左子树
		
	}
	
	private Node root;//树根节点

}

首先需要一个节点对象的类,这个对象包含数据域和指向节点的两个子节点的引用。

其次,需要一个树对象的类,这个对象包含一个根节点root。

创建树(insert)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值