第4章 树
对于大量数据链表访问太慢,而树支持以O(logN)平均时间支持各种操作。
树的概念:父亲、祖先、儿子、后裔、兄弟、根、路径、深度。
树节点写为泛型的嵌套类,多叉树用左孩子右兄弟表示法,二叉树左右孩子表示法。
private static classTreeNode{
AnyType element;
TreeNode leftChild;
TreeNode rightSibling;
}
private static classTreeNode{
AnyType element;
TreeNode leftChild;
TreeNode rightChild;
}
对于遍历,链表可以用索引号、增强for、迭代器。二叉树则使用递归。
先序遍历(preorder travesal)就是1判空、2输出、3归子。注意到改进后判空也是递归的。遍历的要点就是首先处理null情形。遍历的递归没有传递任何变量,减少出错。
public void printTree(){
if(isEmpty())
System.out.println("Emptytree");
else
printTree(root);
}
private voidprintTree(BinaryNode t){
if(t!=null){
System.out.println(t.element);
printTree(t.left);
printTree(t.right);
}
}
后序遍历计算树高的例子:
public intheight(BinaryNode t){
if(t!=null)
return -1;
else
return1+Math.max(height(t.left),height(t.right));
}
Linux中目录本身也是文件,需要占用空间。
列出文件系统目录(先序遍历、递归)的伪码(了解):
private void listAll(intdepth){//ll命令被写为节点的类方法
printName(depth);
if(isDirectory()){
for each file c in thisdirectory(for each child)
c.listAll(depth+1);
}
}
public void listAll(){
listAll(0);
}
层序遍历中所有深度为d的节点要在深度d+1之前处理。使用非递归的队列实现,而不是递归默认的栈。
二叉树平均深度O(sqrtN),二叉查找树平均深度O(logN)。
用二叉树作表达式转换方法:构造表达式树,其节点为操作符、树叶为操作数。中序遍历得中缀表达式,后序遍历得后缀表达式。如何由中缀表达式构造表达式树??
至此已经学习了中缀表达式转为后缀表达式的”栈实现”和”二叉树实现”。
4.3二叉查找树
二叉查找树的写法:
[if !supportLists]l [endif]BST定义为最多2孩子,不允许重复元素
[if !supportLists]l [endif]数据只有根节点root,节点写为嵌套类。
[if !supportLists]l [endif]BinarySearchTree的API为: 判空置空、元素式添删容遍、最值查找。没有建树而用插入建树。
内部实现函数为:含树根形参和”根/节”返回的”元素式添删容遍、找最值”(用于递归)
(链表判长空置空、添删容迭、索引式增删改进。BST没有判长、没有迭代器、没有索引相关的方法。)(树更容易判空,不需要theSize)
public classBinarySearchTree> {
private BinaryNode root;
private static BinaryNode{
private AnyType element;
private BinaryNodeleft;
private BinaryNoderight;
public BinaryNode(AnyType e){
element=e;leftChild=null;rightChild=null;
}
public BinaryNode(AnyTypee,BinaryNode lt,BinaryNode rt){
element=e;leftChild=lt;rightChild=rt;
}
}
public BinarySearchTree(){root=null;}
public void makeEmpty(){root=null;}
public boolean isEmpty(){return root==null;}
public void insert(AnyType x){root=insert(x,root);}
public void remove(AnyType x){root=remove(x,root);}
public boolean contains(AnyType x){returncontains(root);}
public void printTree(){printTree(root);}
public AnyType findMin(){
if(isEmpty())
throw newUnderflowException();
return findMin(root).element;
}
public AnyType findMax(){
if(isEmpty())
throw newUnderflowException();
return findMax(root).element;
}
private BinarySearchNodeinsert(AnyType x,BinaryNode t){}
private BinarySearchNoderemove(AnyType x,BinaryNode t){}
private boolean contains(AnyTypex,BinaryNode t){}
private void printTree(BinaryNodet){}
private BinarySearchNodefindMin(BinaryNode t){}
private BinarySearchNodefindMax(BinaryNode t){}
}
返回值递归:二叉查找树的添删容的内部函数均使用了返回值递归。此处的返回值递归意味着更新树。
BST的内部函数实现:
[if !supportLists]l [endif]insert先递归查找,再处理递归底为空位插入和重复元素。
insert递归底并没有直接赋值给空位,而只是在空位处创建节点并返回,在上一个递归完成赋值。
[if !supportLists]l [endif]remove先递归查找再分情况处理:叶子直接删;单儿子补儿子;双儿子补右子树最小点,并递归删除替补。
二叉查找树的找最值即是外部方法,又协助完成remove操作。
补右子树最小点用改元素的方法而非改链。
删除替补为"返回值递归"。
[if !supportLists]l [endif]contains递归解决,找到与否均为递归底。
[if !supportLists]l [endif]printTree递归解决,先判空后遍历。
[if !supportLists]l [endif]findMin/findMax先判空再循还,而不要用尾递归解决。
(注意空树的退化情形,在递归中作为递归底其处理尤为重要)
二叉查找树的insert/remove/contains/printTree/findMin/findMax平均运行时间O(logN),而非最坏除非树得到平衡。这也是二叉查找树需要:平衡附加条件,使任何节点深度不得过深的原因。
删除次数不多时,通常使用"惰性删除"(仅将待删除元素标记为已删除)。即便惰性删除的节点数与实际节点数相同,而树仅是上升很小的常数,不影响其insert/remove/contains性能,并且删除的效率和重新插入该节点的效率将大大提升。
private booleancontains(AnyType x,BinaryNode t){
if(t==null)
return false;
int compareResult=x.compareTo(t);//节约一次比较次数
if(compareResult<0)
return contains(x,t.left);
else if(compareResult>0)
return contains(x,t.right);
else
return true;
}
private AnyTypefindMin(BinaryNode t){
if(t==null)
return null;
else{
while(t.left!=null)
t=t.left;
return t.element;
}
}
private AnyTypefindMax(BinaryNode t){
if(t==null)
return null;
else{
while(t.right!=null)
t=t.right;
return t.element;
}
}
如果AnyType不是Comparable的,则需要使用比对象作比较。体现在成员、构造、比较器(有比较对象用比较对象,无比较器将参数强制转换为Comparable)、contains实现。
import java.util.Comparator;
public classBinarySearchTree{
private BinaryNode root;
private Comparator cmp;
public BinarySearchTree(){
this(null);
}
public BinarySearchTree(Comparator c){
root=null;cmp=c;
}
private int myCompare(AnyType lhs,AnyType rhs){
if(cmp!=null)
returncmp.compare(lhs,rhs);
else
return((Comparable)lhs).compareTo(rhs);
}
private boolean contains(AnyType x,BinaryNodet){
if(t==null)
returnfalse;//not found
int compareResult=cmp.compare(x,t);
if(compareResult<0)
returncontains(x, t.left);
else if(compareResult>0)
returncontains(x, t.right);
else
returntrue;//found
}
}
4.4 AVL平衡树
AVL树是左右子树的高度最多差1的二叉查找树(空树高度定义为-1)。插入/删除节点需要通过旋转恢复平衡,惰性删除不用维护平衡性。
[if !vml]
[endif]
AVL平衡树进化BST:节点记录树高、单双旋维护平衡、
[if !supportLists]l [endif]节点树高定义为其子树最大树高加1,因而节点树高可以递归求解。(区别:离根节点为深度,离叶节点为高度)
[if !supportLists]l [endif]单旋转为内逆子归重平,重平成逆子,自下而上地大子高加1地刷树高。实现为重平为形参逆点为返回的持左子/持右子两种情况。
双旋转为自下而上对两级作单旋转。
[if !supportLists]l [endif]insert的平衡维护:以insert的树根形参为参考。左右子树超过1开启维护,”一字型插入”用单旋转,”之字型插入”用双旋转。
4.5伸展树
伸展树的展开实现:父为根做单旋转,父祖俱在做双旋转。
展开操作不仅将被访问节点移到根处,而且将访问路径上大部分节点的深度大致减少一半。当访问路径过长导致过长查找时间时,树的伸展对未来操作有益。当访问消耗很少时,树伸展益处较少甚至有害。
伸展树查询contains将被访问节点旋转到树根上。
伸展树删除:访问被删除节点将其推到根处,访问左子树最大节点将其推到左子树根处,此时左子树没有右儿子可以挂载右子树成为其右儿子。
伸展树拥有:操作的O(logN)的摊还时间界、快速重访、无需保存更新树高的优点。伸展树的编程较AVL树简单。
4.7B树
M阶B树的有以下特性:B树就是非页节点作索引,叶节点存数据。
[if !supportLists]l [endif]叶节点存数据,叶节点深度相同存(L/2的下取整)至L个数据项
[if !supportLists]l [endif]非叶节点存关键字,M-1个关键字存2-M个叶节点中的最小关键字。
[if !supportLists]l [endif]树根儿子数2至M之间,或仅有一片树叶
[if !supportLists]l [endif]树根以外的非页节点的儿子数(M/2的下取整)至M之间
节点容量为块大小,块大小8K,为磁盘最小I/O单位。7200转磁盘的1转8.3毫秒,访问时间约为寻道时间+磁盘转动。B树将叶节点(叶节点和非叶节点)提高为块大小,提高访问效率。
M和L的取值:L=块大小/行数据大小。如块在小8192字节,行数据256字节,则L=32。每个非叶节点厚M-1个关键字和M个分支(地址)。如块大小8192字节,关键字大小32字节,分支大小4字节,则M=228。
B树先查找后插入:插入后L+1项则分裂树叶并在节点中添加查找关键字,父节点关键字个数超过M则递归解决。根节分裂为2个则建立新根,这就是根节点允许有2个儿子的原因。
另一种方法:交给数据项不超过L的邻节点领养,并修改父节点。
B树先查找后删除:删除后数据项低于最小值,从数据项没有达到最小值的邻节点领养。如果邻节点已达最小值则合并邻节点,并递归地修改父节点。
4.8标准库中的集合
Set接口不允许重复元素,Map接口关键字唯一而允许值重复。TreeSet、TreeMap是有序状态,是SortedSet、SortedMap的实现类。TreeSet、TreeMap支持以O(logN)时间支持add/remove/contains,实现方法为平衡二叉查找树中的红黑树。
词典的1字之差相似单词算法
词典的1字之差相似单词算法:
[if !supportLists]l [endif]读入List words。
[if !supportLists]l [endif]单词按长度分组Map>wordsByLength。
[if !supportLists]l [endif]遍历各组、遍历各位置,按缺字母词根分组。
[if !supportLists]l [endif]词根分组以单词对加入结果Map>adjWords。
[if !supportLists]l [endif]补写图链表update(图中取表、空表建表、表中加值)和图链表遍历方法。
低效的另一种方法:
[if !supportLists]l [endif]先写单词对相似检测函数:单词长度比较、逐个字母比较。
[if !supportLists]l [endif]单词分组,单循还地取出组内单词对进行相似验证。
代码参考教材。