本文内容基于《数据结构与算法分析 Java语言描述》第三版,冯舜玺等译。
1. 预备知识
树可以用几种方式定义。定义树的一种自然的方式是递归。一棵树是一些节点的集合。这个集合可以是空集;若不是空集,则树由称作根的节点r以及0个或多个非空的子树组成,这些子树中每一颗的根都被来自根r的一条有向边所连接。
每一颗子树的根叫做根r的儿子,而r是每一颗子树的根的父亲。
一棵树是N个节点和N-1条边的集合,其中的一个节点叫做根。
没有儿子的节点称为树叶。
具有相同父亲的节点为兄弟。
对于图中的树,E的深度为1高为2;F的深度为1高为1;该树的高度为3。
1.1 树的实现
实现树的一种方法可以是在每一个节点除数据外还要有一些链,使得该节点的每一个儿子都有一个链指向它。
树节点的声明:
public class TreeNode {
private Object element;
private TreeNode firstChild;
private TreeNode nextSibling;
}
1.2 树的遍历及应用
流行的用法之一是包括UNIX和DOS在内的许多常用操作系统中的目录结构。
1.2.1 先序遍历
对节点的处理工作是在它的诸儿子节点被处理之前进行的。
public class Test {
public static void main(String[] args) {
TreeNode node1 = new TreeNode("F:\\Projects\\Java\\review\\files");
TreeNode node2 = new TreeNode("F:\\Projects\\Java\\review\\files\\csv1.csv");
TreeNode node3 = new TreeNode("F:\\Projects\\Java\\review\\files\\directory11");
TreeNode node4 = new TreeNode("F:\\Projects\\Java\\review\\files\\directory11\\file1.txt");
TreeNode node5 = new TreeNode("F:\\Projects\\Java\\review\\files\\file11.txt");
TreeNode node6 = new TreeNode("F:\\Projects\\Java\\review\\files\\file2.txt");
TreeNode node7 = new TreeNode("F:\\Projects\\Java\\review\\files\\html1.html");
node1.setFirstChild(node2);
node2.setNextSibling(node3);
node3.setFirstChild(node4);
node3.setNextSibling(node5);
node5.setNextSibling(node6);
node6.setNextSibling(node7);
preOrderTraversal(node1);
}
public static void preOrderTraversal(TreeNode node) {
System.out.println(node.getElement());
if (node.getFirstChild() != null) {
preOrderTraversal(node.getFirstChild());
}
if (node.getNextSibling() != null) {
preOrderTraversal(node.getNextSibling());
}
}
}
1.2.2 后序遍历
一个节点的工作是在它的诸儿子节点被计算后进行的。
public class Test {
public static void main(String[] args) {
TreeNode node1 = new TreeNode("F:\\Projects\\Java\\review\\files");
TreeNode node2 = new TreeNode("F:\\Projects\\Java\\review\\files\\csv1.csv");
TreeNode node3 = new TreeNode("F:\\Projects\\Java\\review\\files\\directory11");
TreeNode node4 = new TreeNode("F:\\Projects\\Java\\review\\files\\directory11\\file1.txt");
TreeNode node5 = new TreeNode("F:\\Projects\\Java\\review\\files\\file11.txt");
TreeNode node6 = new TreeNode("F:\\Projects\\Java\\review\\files\\file2.txt");
TreeNode node7 = new TreeNode("F:\\Projects\\Java\\review\\files\\html1.html");
node1.setFirstChild(node2);
node2.setNextSibling(node3);
node3.setFirstChild(node4);
node3.setNextSibling(node5);
node5.setNextSibling(node6);
node6.setNextSibling(node7);
postOrderTraversal(node1);
System.out.println(node1.getElement());
}
public static void postOrderTraversal(TreeNode node) {
for (node = node.getFirstChild(); node != null; node = node.getNextSibling()) {
System.out.println(node.getElement());
postOrderTraversal(node);
}
}
}
2. 二叉树
二叉树是一棵树,其中每个节点都不能有多于两个的儿子。
二叉树的一个性质是一颗平均二叉树的深度要比节点个数N小得多,其平均深度为,而对于特殊类型的二叉树,即二叉查找树,其深度的平均值是。
2.1 实现
因为一个二叉树节点最多有两个子节点,所以可以保存直接链接到他们的链。
二叉树节点类:
class BinaryNode{
Object element;
BinaryNode left;
BinaryNode right;
}
2.2 例子:表达式树
表达式树的树叶是操作数,如常量或变量名,其他节点为操作符。
上图中的树表示(a + (b * c)) + (((d * e) + f) * g) 。
可以通过递归地产生一个带括号的左表达式,然后打印出在根处的运算符,最后递归地产生一个带括号的右表达式,最终得到一个中缀表达式(左,节点,右)。称为中序遍历。
递归地打印出左子树、右子树、然后打印运算符。上图中的树将输出 abc*+de*f+g*+ 这个后缀表达式(左,右,节点)。称为后序遍历。
先打印出运算符,然后递归地打印出左子树和右子树。上图中的树将输出 ++a*bc*+*defg 这个前缀表达式(节点,左,右)。称为先序遍历。
2.2.1 将后缀表达式转变成表达式树
例如:ab+cde+**
public class BinaryTree {
public static void main(String[] args) {
String expression = "ab+cde+**";
Stack<BinaryNode> stack = new Stack<>();
for (int i = 0; i < expression.length(); i++) {
char x = expression.charAt(i);
if (!judgeOperand(x)) {
stack.push(new BinaryNode(x));
} else {
BinaryNode newNode = new BinaryNode(x);
BinaryNode rightNode = stack.pop();
newNode.right = rightNode;
BinaryNode leftNode = stack.pop();
newNode.left = leftNode;
stack.push(newNode);
}
}
BinaryNode t = stack.pop();
printTree(t);
}
public static boolean judgeOperand(char x) {
if ('+' == x || '-' == x || '*' == x || '/' == x) {
return true;
}
return false;
}
public static void printTree(BinaryNode t) {
if (t == null) {
return;
}
printTree(t.left);
printTree(t.right);
System.out.print(t.element);
}
}
class BinaryNode {
Object element;
BinaryNode left;
BinaryNode right;
public BinaryNode(Object element) {
this.element = element;
}
}
3. 查找树ADT-二叉查找树
使得二叉树称为二叉查找树的性质是,对于树中的每个节点X,它的左子树中所有项的值小于X中的项,而它的右子树中所有项的值大于X中的项。这意味着该树所有的元素可以用某种一直的方式排序。
/**
* 二叉查找树
* 树中的每个节点X,它的左子树中所有项的值都小于X中的值,它的右子树中所有项的值都大于X中的值
* @Author YETA
* @Date 2019-05-09 14:07
*/
public class BinarySearchTree<AnyType extends Comparable<? super AnyType>> {
/**
* 二叉树节点类
* @param <AnyType>
*/
private static class BinaryNode<AnyType> {
public BinaryNode(AnyType theElement) {
this(theElement, null, null);
}
public BinaryNode(AnyType element, BinaryNode<AnyType> left, BinaryNode<AnyType> right) {
this.element = element;
this.left = left;
this.right = right;
}
AnyType element;
BinaryNode<AnyType> left;
BinaryNode<AnyType> right;
}
private BinaryNode<AnyType> root;
public BinarySearchTree() {
this.root = null;
}
public void makeEmpty() {
root = null;
}
public boolean isEmpty() {
return root == null;
}
public boolean contains(AnyType x) {
return contains(x, root);
}
/**
* 判断树t中是否包含节点x
* @param x
* @param t
* @return
*/
private boolean contains(AnyType x, BinaryNode<AnyType> t) {
if (t == null) {
return false;
}
int compareResult = x.compareTo(t.element);
if (compareResult < 0) {
return contains(x, t.left);
} else if (compareResult > 0) {
return contains(x, t.right);
} else {
return true;
}
}
public AnyType findMin() {
if (isEmpty()) {
System.out.println("is empty!");
}
return findMin(root).element;
}
/**
* 返回树中包含最小元的节点的引用
* 递归实现
* @param t
* @return
*/
private BinaryNode<AnyType> findMin(BinaryNode<AnyType> t) {
if (t == null) {
return null;
} else if (t.left == null) {
return t;
}
return findMin(t.left);
}
public AnyType findMax() {
if (isEmpty()) {
System.out.println("is empty!");
}
return findMax(root).element;
}
/**
* 返回树中包含最大元的节点的引用
* 非递归实现
* @param t
* @return
*/
private BinaryNode<AnyType> findMax(BinaryNode<AnyType> t) {
if (t != null) {
while (t.right != null) {
t = t.right;
}
}
return t;
}
public void insert(AnyType x) {
root = insert(x, root);
}
/**
* 将节点x插入到树t中
* @param x
* @param t
* @return
*/
private BinaryNode<AnyType> insert(AnyType x, BinaryNode<AnyType> t) {
if (t == null) {
return new BinaryNode<>(x, null, null);
}
int compareResult = x.compareTo(t.element);
if (compareResult < 0) {
t.left = insert(x, t.left);
} else if (compareResult > 0) {
t.right = insert(x, t.right);
} else {
//duplicate, do nothing
}
return t;
}
public void remove(AnyType x) {
root = remove(x, root);
}
/**
* 将节点x从树t中移除
* @param x
* @param t
* @return
*/
private BinaryNode<AnyType> remove(AnyType x, BinaryNode<AnyType> t) {
if (t == null) {
return t;
}
int compareResult = x.compareTo(t.element);
if (compareResult < 0) {
//节点x的值比当前节点的值小,所以向左遍历
t.left = remove(x, t.left);
} else if (compareResult > 0) {
//节点x的值比当前节点的值大,所以向右遍历
t.right = remove(x, t.right);
} else if (t.left != null && t.right != null) {
//通过上面两个if的遍历,程序运行到这里说明当前节点就是要移除的节点
//当前节点有两个儿子的情况,用右儿子中最小的数据来替换当前节点,并删除那个最小数据的节点
t.element = findMin(t.right).element;
t.right = remove(t.element, t.right);
} else {
//通过上面两个if的遍历,程序运行到这里说明当前节点就是要移除的节点
//当前节点只有一个儿子的情况,直接用儿子节点替换当前节点
t = t.left != null ? t.left : t.right;
}
return t;
}
public void printTree() {
if (isEmpty()) {
System.out.println("is empty!");
} else {
printTree(root);
}
}
/**
* 中序遍历打印
* @param t
*/
private void printTree(BinaryNode<AnyType> t) {
if (t != null) {
printTree(t.left);
System.out.println(t.element);
printTree(t.right);
}
}
}
4. 标准库中的集合与映射
4.1 Set接口
Set接口代表不允许重复元的Collection。由接口SortedSet给出的一种特殊类型的Set保证其中的各项处于有序的状态,一个实现是TreeSet。
4.2 Map接口
Map是一个接口,代表由关键字以及它们的值组成的一些项的集合。关键字必须是唯一的,但是若干关键字可以映射到一些相同的值,因此值不是唯一的。在SortedMap接口中,映射中的关键字保持逻辑上有序状态,一种实现是TreeMap类。
Map不提供迭代器,而是提供3种方法:
4.3 TreeSet类和TreeMap类的实现
Java要求TreeSet和TreeMap支持基本的add、remove和contains操作以及以对数最坏情形时间完成。因此,基本的实现方法就是平衡二叉查找树,一般来说,并不适用AVL树,而是经常使用一些自顶向下的红黑树。