一、基本概念
与顺序表、栈和队列这种一对一的线性结构不同,树是一种一对多的线性结构。这里的一对多是指一个元素最多有一个前驱节点,并且可以有多个后继节点。
树是n个节点的有穷集,n=0时称为空树,n>0称为非空树;
非空树中的每个元素称为节点(node);其中最顶端没有前驱节点的节点称为根节点或树根(root);
当n>1时,其余节点可分为m个互不相交的集合,每个集合本身也是一棵树,被称为子树(subtree);
接下来介绍树中的几个名词:
- 节点拥有的子树的数量称之为度(Degree)。度为0的节点称为终端节点或叶节点(Leaf),度不为零的节点称为分支节点。除了叶节点以外,根节点和分支节点统称为内部节点。节点的子树的根称为该节点的孩子(Child),该节点称为孩子的双亲或父节点。同一个双亲的孩子之间互相称为兄弟节点。树的度是各个节点度的最大值。
- 节点的层次(Level)从根开始定义,根为第一层根的孩子为第二层。双亲在同一层的结点互为堂兄弟。树中结点的最大层次称为树的深度(Depth)或高度。如果将树中结点的各个子树看成从左到右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。森林是 m(m>=0)棵互不相交的树的集合。
二、树的存储结构
由于树中每个结点的孩子可以有多个,所以简单的顺序存储结构无法满足树的实现要求。下面介绍两种常用的物理存储结构来表示树的结构:
- 链式存储结构;
- 数组;
1. 链式存储结构
链式存储结构最为直观,声明一个节点Node,然后节点内定义指针指向孩子节点,如下图:
public class Tree<E> {
private class Node {
E data;
Node left;
Node right;
Node(E data, Node left, Node right) {
this.data = data;
this.left = left;
this.right = right;
}
}
// ...
}
2. 数组存储
使用数组结构存储时,会将树中的节点按一定的层次结构存储到数组中。假如某个节点为空,那么相应数组中的位置也将空出来:
之所以这样在相应的位置空出来,是为了可以更快的查找到二叉树的孩子节点和双亲节点,假如一个双亲节点的下标为parent,那么它的两个孩子节点就是:
2 * parent + 1 和 2 * parent + 2。放过来也可以从孩子节点的下标推出双亲节点的下标。
三、二叉树
1. 基本概念
二叉树基本都不陌生,像上面存储结构中所展示的图就都是二叉树。二叉树(Binary Tree)是每个节点最多有两个子树,通常被称为“左子树(left subtree)”和“右子树(right subtree)”。除此之外,二叉树的子树是拥有左右子树次序之分的,像下面的两棵树,是同一棵树,但是又是不同的二叉树:
2. 几种常见的二叉树
接下来介绍二叉树中几种常见的树,它们在二叉树的应用中(比如说查找)都很有价值:
2.1 斜树
所有的结点都只有左子树的二叉树叫左斜树。所有的结点都只有右子树的二叉树叫右斜树。这两者统称斜树。
斜树每一层只有一个结点,结点个数与二叉树的深度相同。其实斜树就是线性表结构,基本上就退化到与链表相识的结构了。
2.2 满二叉树
一个二叉树的所有非叶子节点都存在左右孩子节点,并且所有的叶子节点都在同一层级,这样的二叉树称为满二叉树。
2.3 完全二叉树
若设二叉树的高度为 h,除第 h 层外,其他各层(1 到 h-1)的结点数都达到最大个数,第 h 层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
从上可以看出,完全二叉树具有以下特点:
- 叶子只能出现在最下层;
- 拥有同样节点数的二叉树,完全二叉树的深度最小。
2.4 平衡二叉树
平衡二叉树又称为 AVL树(区别于AVL算法),它是一颗二叉树,且具有以下性质:它是一颗空树或它的左右两个子树的高度差的绝对值不超过1,并且每个节点两个子树(如果有节点的话)都是一颗平衡二叉树。
四、二叉树的实现与遍历
1. 数组存储实现
public class ArrayBinaryTree<E> {
private static final int DEFAULT_DEPTH = 5; // 定义二叉树的深度
private int size = 0;
private E[] data;
ArrayBinaryTree() {
this(DEFAULT_DEPTH);
}
@SuppressWarnings("unchecked")
ArrayBinaryTree(int depth) {
data = (E[]) new Object[(int) Math.pow(2, depth)];
}
public boolean isEmpty() {
return size == 0;
}
public int getSize() {
return size;
}
// 获取指定结点的父结点
public E getParent(int index) {
checkIndex(index);
if (index == 1) {
throw new RuntimeException("根节点不存在父节点!");
}
// 判断节点是左孩子节点还是右孩子节点
if (index % 2 == 1) { // 左孩子节点
return data[(index - 1) / 2];
} else { // 右孩子节点
return data[(index - 2) / 2];
}
}
// 获取左子结点
public E getLeft(int index) {
checkIndex(index * 2 + 1);
return data[index * 2 + 1];
}
// 获取右子结点
public E getRight(int index) {
checkIndex(index * 2 + 2);
return data[index * 2 + 2];
}
// 返回指定数据的位置
public int indexOf(E value) {
if (value == null) {
throw new NullPointerException();
} else {
for (int i = 0; i < data.length; i++) {
if (value.equals(data[i])) {
return i;
}
}
}
return -1;
}
// 顺序添加元素
public void add(E element) {
checkIndex(size);
data[size] = element;
size++;
}
// 在指定位置添加元素
public void add(E element, int parent, boolean isLeft) {
if (data[parent] == null) {
throw new RuntimeException("index[" + parent + "] is not Exist!");
}
if (element == null) {
throw new NullPointerException();
}
if (isLeft) { // 判断是否为左孩子节点
checkIndex(2 * parent + 1);
if (data[parent * 2 + 1] != null) {
throw new RuntimeException("index[" + parent * 2 + 1 + "] is Exist!");
}
data[2 * parent + 1] = element;
} else {
checkIndex(2 * parent + 2);
if (data[(parent + 1) * 2] != null) {
throw new RuntimeException("index[" + parent * 2 + 2 + "] is Exist!");
}
data[2 * parent + 2] = element;
}
size++;
}
// 检查下标是否越界
private void checkIndex(int index) {
if (index < 0 || index >= data.length) {
throw new IndexOutOfBoundsException();
}
}
public static void main(String[] args) {
char[] data = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'};
ArrayBinaryTree<Character> abt = new ArrayBinaryTree();
for (int i = 0; i < data.length; i++) {
abt.add(data[i]);
}
System.out.print(abt.getParent(abt.indexOf('J')));
}
}
2. 链式存储实现
public class LinkedBinaryTree<E> {
private List<Node> nodes = null;
private class Node {
Node leftChild;
Node rightChild;
E data;
Node(E data) {
this.data = data;
}
}
// 获取根节点
public Node getRoot() {
return nodes.get(0);
}
public void createBinTree(E[] array) {
nodes = new LinkedList<Node>();
for (int i = 0; i < array.length; i++) {
nodes.add(new Node(array[i]));
}
// 对前 last-1 个父节点按照父节点与孩子结点的数字关系建立二叉树
for (int i = 0; i < array.length / 2 - 1; i++) {
nodes.get(i).leftChild = nodes.get(i * 2 + 1);
nodes.get(i).rightChild = nodes.get(i * 2 + 2);
}
// 最后一个父节点:因为最后一个父节点可能没有右孩子,所以单独拿出来处理
int lastParent = array.length / 2 - 1;
nodes.get(lastParent).leftChild = nodes.get(lastParent * 2 + 1);
// 右孩子,如果数组的长度为奇数才建立右孩子
if(array.length % 2 == 1) {
nodes.get(lastParent).rightChild = nodes.get(lastParent * 2 + 2);
}
}
public static void main(String[] args) {
Character[] data = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'};
LinkedBinaryTree<Character> ldt = new LinkedBinaryTree();
ldt.createBinTree(data);
}
}
3. 二叉树的遍历
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种顺序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树的遍历主要有四种:前序遍历、中序遍历、后序遍历和层序遍历。
3.1 前序遍历
// 数组存储
public void preOrderTraverse(int index) {
if(datas[index] == null)
return;
System.out.print(datas[index] + " ");
preOrderTraverse(index * 2);
preOrderTraverse(index * 2 + 1);
}
// 链式存储
public void preOrderTraverse(Node node) {
if(node == null)
return;
System.out.print(node.data + " ");
preOrderTraverse(node.leftChild);
preOrderTraverse(node.RightChild);
}
3.2 中序遍历
// 链式存储
public void inOrderTraverse(Node node) {
if(node == null)
return;
inOrderTraverse(node.leftChild);
System.out.print(node.data + " ");
inOrderTraverse(node.rightChild);
}
3.2 后序遍历
// 链式存储
public void postOrderTraverse(Node node) {
if(node == null)
return;
postOrderTraverse(node.leftChild);
postOrderTraverse(node.rightChild);
System.out.print(node.data + " ");
}