二叉树应该是数据结构中最基本的数据类型了,由二叉树又延伸出二叉搜索树(排序二叉树,BInary Search Tree)、平衡二叉树(AVL-Tree)、红黑树(Red-Black Tree)等。本篇文章主要讲一下二叉树的构建,以及二叉树的遍历。后续文章会详细讲一下排序二叉树、平衡二叉树和红黑树。
1、二叉树构建
二叉树的构建一般是根据给定的二叉树的前序遍历串和中序遍历串,或者后序遍历串和中序遍历串还原一棵二叉树,注意如果只有前序遍历串和后序遍历串是不能唯一确定一棵二叉树的,所以这里我们只讨论一下如何根据前面两种情况如何还原构建二叉树。
在讲之前先讲一下一些设定。另外本文演示代码采用的是java。构成树的关键Node
public class Node<K, V> {
public Node<K, V> left = null;
public Node<K, V> right = null;
public Node<K, V> parent;
public int height = 1;
public K key;
public V value;
}
Node中包含了左、右孩子节点以及父亲节点(java中没有指针,所以有指向父亲的节点,对树的插入删除等操作会方便很多,事实上java TreeMap源码也是这样设定的),height代表节点的高度,key代表节点的索引Key,value代表节点存储的值。
/**
* 树的一些基本操作,增、删、查、改、打印树
*/
@SuppressWarnings("unused")
public interface ITreeAction<K, V> {
boolean insert(K key, V value);
boolean remove(K key);
Node<K, V> find(K key);
boolean amend(K key, V value);
void print(IPrintStrategy strategy);
}
然后定义一个接口,里面有一些树的基本操作:增删查改以及打印(遍历)。二叉树的遍历下一节会讲,其他操作对于普通二叉树都基本不会用到,会在后续将排序二叉树、平衡二叉树的文章讲到,本篇文章先忽略。
public class BTree<K extends Comparable<K>, V> implements ITreeAction<K, V> {
Node<K, V> root = null;
@Override
public boolean insert(K key, V value) {
return false;
}
@Override
public boolean remove(K key) {
return false;
}
@Override
public Node<K, V> find(K key) {
return null;
}
@Override
public boolean amend(K key, V value) {
return false;
}
@Override
public void print(IPrintStrategy strategy) {
strategy.print(root);
}
}
OK,现在一棵树已经整装待发了,我们可以开始考虑构建树了。根据给定的二叉树的前序遍历串和中序遍历串,或者后序遍历串和中序遍历串还原一棵二叉树。两种情况的思路其实都是一样的,以根据前序遍历串和中序遍历串来构建树来说,来举个例子讲解吧。
public static String preOrderStr = "ABDEHICFJG";
public static String inOrderStr = "DBHEIAFJCG";
public static String postOrderStr = "DHIEBJFGCA";
前序遍历串"ABDEHICFJG",中序遍历串"DBHEIAFJCG"。首先我们要明白前序遍历串是怎么来的,它是按照“根–左--右”的顺序输出的,所以对于一棵树的前序遍历串,串的第一位一定是它的根节点!!!明白这一点非常重要,所以我们首先确定了树的根节点就是A。其次我们还要明白A后面紧跟着的,一定是它的左子树的前序遍历串(除非A没有左子树),至于它的左子树的前序遍历串多长,这个就要根据下面的中序遍历串决定了。
然后我们再看一下中序遍历串,它是树按照“左–根--右”的顺序输出的,也就意味着,根的左边的串就是根的左子树的中序遍历串,根的右边串就是根的右子树的中序遍历串。而且我们还找到了根A在中序遍历串的位置index = 5,再回到前序遍历串,我们就可以根据这个index找到根A的左子树的前序遍历串[1,6)和左子树的前序遍历串[6,length)。
OK,到现在为止,我们已经找到了根、根的左子树、根的右子树,所以下面就可以用递归来构建树了。
来张图一看就明白了。
/**
* 根据前序遍历串和中序遍历串重建二叉树
*/
public void buildTreeWithPreAndInSequence(List<KeyValue<K, V>> preOrder, List<KeyValue<K, V>> inOrder) {
root = buildTreeWithPreAndInSequenceInternal(preOrder, inOrder);
}
private Node<K, V> buildTreeWithPreAndInSequenceInternal(List<KeyValue<K, V>> preOrder, List<KeyValue<K, V>> inOrder) {
if (preOrder.isEmpty() || inOrder.isEmpty() || preOrder.size() != inOrder.size()) {
return null;
}
int size = preOrder.size();
int firstIndex = 0;
if (size == 1) {
return new Node<>(inOrder.get(0).key, inOrder.get(0).value);
}
Node<K, V> parent = new Node<>(preOrder.get(firstIndex).key, preOrder.get(firstIndex).value);
int position = inOrder.indexOf(preOrder.get(firstIndex));
parent.left = buildTreeWithPreAndInSequenceInternal(preOrder.subList(1, position + 1), inOrder.subList(0, position));
parent.right = buildTreeWithPreAndInSequenceInternal(preOrder.subList(position + 1, size), inOrder.subList(position + 1, size));
return parent;
}
/**
* 根据后序遍历串和中序遍历串重建二叉树
*/
public void buildTreeWithPostAndInSequence(List<KeyValue<K, V>> postOrder, List<KeyValue<K, V>> inOrder) {
root = buildTreeWithPostAndInSequenceInternal(postOrder, inOrder);
}
private Node<K, V> buildTreeWithPostAndInSequenceInternal(List<KeyValue<K, V>> postOrder, List<KeyValue<K, V>> inOrder) {
if (postOrder.isEmpty() || inOrder.isEmpty() || postOrder.size() != inOrder.size()) {
return null;
}
int size = inOrder.size();
int lastIndex = inOrder.size() - 1;
if (size == 1) {
return new Node<>(inOrder.get(0).key, inOrder.get(0).value);
}
Node<K, V> parent = new Node<>(postOrder.get(lastIndex).key, postOrder.get(lastIndex).value);
int position = inOrder.indexOf(postOrder.get(lastIndex));
parent.left = buildTreeWithPostAndInSequenceInternal(postOrder.subList(0, position), inOrder.subList(0, position));
parent.right = buildTreeWithPostAndInSequenceInternal(postOrder.subList(position, lastIndex), inOrder.subList(position + 1, size));
return parent;
}
KeyValue就是一个自定义类,方便存储Key-Value用的,为了方便大家理解这里也贴一下代码。
public class KeyValue<K, V> {
public final K key;
public final V value;
public KeyValue(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof KeyValue)) {
return false;
}
KeyValue<?, ?> p = (KeyValue<?, ?>) o;
return p.key.equals(key);
}
@Override
public int hashCode() {
return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
}
@Override
public String toString() {
return "<" + key + "," + value + ">";
}
}
2、二叉树遍历
二叉树的常见遍历有前序遍历、中序遍历和后序遍历,一般我们都记得这三种遍历的递归算法,一个是因为递归比较符合我们大脑思考问题的习惯,还有一点就是这三种递归算法保持了代码形式上的高度统一。而教科书上给出的这三种算法的非递归遍历又长又难记忆,而且每个算法的代码形式也不一样,这就更加加深了记忆难度。下面我就给大家推荐一种前、中、后序遍历的非递归算法的简单算法,而且代码形式高度统一,方便记忆。至于递归形式大家应该都知道就没必要讲了;非递归形式的教科书代码,自从我知道了前面的简单方法之后早就忘光了,所以让我讲也讲不了。扯远了扯远了,下面开始正题,先上代码。
public interface IPrintStrategy {
void print(Node node);
}
2.1 前序遍历
import java.util.Stack;
public class PreOrderTraversalPrint implements IPrintStrategy {
@Override
public void print(Node node) {
Stack<KeyValue<Boolean, Node>> stack = new Stack<>();
stack.push(new KeyValue<>(false, node));
while (!stack.isEmpty()) {
KeyValue<Boolean, Node> pair = stack.pop();
boolean visited = pair.key;
Node parent = pair.value;
if (visited) {
System.out.println("key =" + parent.key + ", value = " + parent.value);
} else {
Node right = parent.getRight();
if (right != null) {
stack.push(new KeyValue<>(false, right));
}
Node left = parent.getLeft();
if (left != null) {
stack.push(new KeyValue<>(false, left));
}
stack.push(new KeyValue<>(true, parent));
}
}
}
}
2.2 中序遍历
import java.util.Stack;
public class InOrderTraversalPrint implements IPrintStrategy {
@Override
public void print(Node node) {
Stack<KeyValue<Boolean, Node>> stack = new Stack<>();
stack.push(new KeyValue<>(false, node));
while (!stack.isEmpty()) {
KeyValue<Boolean, Node> pair = stack.pop();
boolean visited = pair.key;
Node parent = pair.value;
if (visited) {
System.out.println("key =" + parent.key + ", value = " + parent.value);
} else {
Node right = parent.getRight();
if (right != null) {
stack.push(new KeyValue<>(false, right));
}
stack.push(new KeyValue<>(true, parent));
Node left = parent.getLeft();
if (left != null) {
stack.push(new KeyValue<>(false, left));
}
}
}
}
}
2.3 后序遍历
import java.util.Stack;
public class PostOrderTraversalPrint implements IPrintStrategy {
@Override
public void print(Node node) {
Stack<KeyValue<Boolean, Node>> stack = new Stack<>();
stack.push(new KeyValue<>(false, node));
while (!stack.isEmpty()) {
KeyValue<Boolean, Node> pair = stack.pop();
boolean visited = pair.key;
Node parent = pair.value;
if (visited) {
System.out.println("key =" + parent.key + ", value = " + parent.value);
} else {
stack.push(new KeyValue<>(true, parent));
Node right = parent.getRight();
if (right != null) {
stack.push(new KeyValue<>(false, right));
}
Node left = parent.getLeft();
if (left != null) {
stack.push(new KeyValue<>(false, left));
}
}
}
}
}
OK,代码贴完了。大家对比一下三个算法的代码就会发现,除了根的压栈顺序不一样,其余代码一模一样!这不合递归算法一样了吗?没错,它就是和递归算法一样了!至于为什么会这样,相信你稍微思考一下就能知道,递归其实也是一种栈(Stack)的调用,而这里也是巧妙的应用了栈后进先出的特性,巧妙的将三种遍历算法统一了。注意下,这里根的压栈顺序是相反的,比如前序遍历(根–左--右)的压栈顺序是右–左--根,因为栈是后进先出的嘛。
还有一点,算法巧妙的利用了一个布尔型变量来记录节点是不是第一次压栈,第一次从栈弹出时,并不会直接输出,而是将其重新压栈(为什么要重新压栈,因为第一次是为了向更深的子节点遍历,比中序遍历要先遍历左孩子结点,以深度优先的的方式,这个时候我们需要记录它的路径,所以需要重新将父节点压栈,这样在左孩子结点遍历到头的时候,能依次弹出它的祖先节点,然后去遍历右孩子节点),并重置布尔值,在第二次弹出时去检查这个布尔值,发现是第二次出栈,才会输出,这样保证了输出的顺序。
吃水不忘挖井人,这里贴一下原算法链接。更简单的非递归遍历二叉树的方法,上面的只是我自己对这种算法的理解,原文中作者还从局部有序以及整体有序的角度论证了算法,推荐去看一下。
2.4 层序遍历
二叉树还一种层序遍历,这里就简单贴一下代码吧,这种遍历好像也不常见。
import java.util.ArrayDeque;
import java.util.Queue;
public class LevelOrderTraversalPrint implements IPrintStrategy {
@Override
public void print(Node node) {
Queue<Node> queue = new ArrayDeque<>();
queue.add(node);
while (!queue.isEmpty()) {
Node parent = queue.poll();
System.out.println("key =" + parent.key + ", value = " + parent.value);
Node left = parent.getLeft();
if (left != null) {
queue.add(left);
}
Node right = parent.getRight();
if (right != null) {
queue.add(right);
}
}
}
}
3 源码
对代码有兴趣的小伙伴,可以直接去github上查看,Algorithm,欢迎fork,star,讨论!