现有一颗如下图所示的二叉树
一、基本概念
(1)先序遍历(深度优先遍历):
前、中、后这三个词是针对根节点的访问顺序而言的
先访问根结点,再访问左子结点,最后访问右子结点。
图中的二叉树的先序遍历的顺序是1 2 4 8 9 5 3 6 7
(2)中序遍历:
先访问左子结点,再访问根结点,最后访问右子结点。
图中的二叉树的中序遍历的顺序是8 4 9 2 5 1 6 3 7
(3)后序遍历:
先访问左子结点,再访问右子结点,最后访问根结点。
图中的二叉树的后序遍历的顺序是8 9 4 5 2 6 7 3 1
(4)层序遍历(广度优先遍历):
先访问树的第一层结点,再访问树的第二层结点……一直访问到最下面一层的结点。在同一层结点中,以从左到右的顺序依次访问。
图中的二叉树的层序遍历的顺序是1 2 3 4 5 6 7 8 9
二、构造二叉树
为了后面测试方便,我们先把它构造出来
构造二叉树的代码如下
/**
* 根据层序遍历数组构造二叉树
* 返回二叉树的根结点
* @param array
*/
public static Node createBinTree(int[] array) {
List<Node> nodeList = new LinkedList<Node>();
// 将一个数组的值依次转换为Node节点
for (int nodeIndex = 0; nodeIndex < array.length; nodeIndex++) {
nodeList.add(new Node(array[nodeIndex]));
}
// 对前lastParentIndex-1个父节点按照父节点与孩子节点的数字关系建立二叉树
for (int parentIndex = 0; parentIndex < array.length / 2 - 1; parentIndex++) {
// 左孩子
nodeList.get(parentIndex).leftChild = nodeList
.get(parentIndex * 2 + 1);
// 右孩子
nodeList.get(parentIndex).rightChild = nodeList
.get(parentIndex * 2 + 2);
}
// 最后一个父节点:因为最后一个父节点可能没有右孩子,所以单独拿出来处理
int lastParentIndex = array.length / 2 - 1;
// 左孩子
nodeList.get(lastParentIndex).leftChild = nodeList
.get(lastParentIndex * 2 + 1);
// 右孩子,如果数组的长度为奇数才建立右孩子
if (array.length % 2 == 1) {
nodeList.get(lastParentIndex).rightChild = nodeList
.get(lastParentIndex * 2 + 2);
}
return nodeList.get(0);
}
/**
* 内部类:节点
*/
private static class Node {
Node leftChild;
Node rightChild;
int data;
Node(int newData) {
leftChild = null;
rightChild = null;
data = newData;
}
}
三、递归的方式实现前序、中序和后序遍历
前序、中序和后序遍历三种遍历方式可以使用递归和非递归来实现,递归的自然更简单一些。这三种不同的遍历结构都是一样的,只是先后顺序不一样而已。代码如下
/**
* 先序遍历
*/
public static void preOrderTraverse(Node node) {
if (node == null)
return;
System.out.print(node.data + " ");
preOrderTraverse3(node.leftChild);
preOrderTraverse3(node.rightChild);
}
/**
* 中序遍历
*/
public static void inOrderTraverse(Node node) {
if (node == null)
return;
inOrderTraverse2(node.leftChild);
System.out.print(node.data + " ");
inOrderTraverse2(node.rightChild);
}
/**
* 后序遍历
*/
public static void postOrderTraverse(Node node) {
if (node == null)
return;
postOrderTraverse(node.leftChild);
postOrderTraverse(node.rightChild);
System.out.print(node.data + " ");
}
四、深度优先遍历和广度优先遍历
深度优先遍历
实现思路:使用栈来实现。由于出栈顺序和入栈顺序相反,所以每次添加节点的时候先添加右节点,再添加左节点。这样在下一轮访问子树的时候,就会先访问左子树,再访问右子树
public static void preOrderTraverse1(Node root) {
if (root == null)
return;
LinkedList<Node> stack = new LinkedList<Node>();
stack.push(root);
while (!stack.isEmpty()) {
Node node = stack.pop();
System.out.print(node.data + " ");
if (node.rightChild != null) {
stack.push(node.rightChild);
}
if (node.leftChild != null) {
stack.push(node.leftChild);
}
}
}
广度优先遍历
实现思路:使用队列,出队列的同时左右孩子依次进队列
public static void levelOrderTraverse(Node root) {
if (root == null) {
return;
}
LinkedList<Node> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node node = queue.poll();
System.out.print(node.data + " ");
if (node.leftChild != null) {
queue.offer(node.leftChild);
}
if (node.rightChild != null) {
queue.offer(node.rightChild);
}
}
}
五、非递归的方式实现中序遍历和先序遍历
非递归的方式实现中序遍历
中序遍历稍微复杂一点,因为最先遇到的根节点不是最先访问的,需要先访问左子树,再回退到根节点,再访问根节点的右子树,这里的一个难点是从左子树回退到根节点的操作,虽然可以用栈来实现回退,但是要注意在出栈时保存根节点的引用,因为我们还需要通过根节点来访问右子树。
实现思路:
(1)如果当前节点存在,则当前节点入栈,指向leftChild,并leftChild(此时的当前节点)进行相同处理。重复1
(2)如果当前节点不存在,当前指向栈顶元素,栈顶元素出栈,处理当前节点值(因为左子节点不存在或者已经处理完了),指向rightChild,并对rightChild(此时的当前节点)进行相同处理。重复1
public static void inOrderTraverse(Node root) {
LinkedList<Node> stack = new LinkedList<>();
Node pNode = root;
while (pNode != null || !stack.isEmpty()) {
if (pNode != null) {
stack.push(pNode);
pNode = pNode.leftChild;
} else { //pNode == null && !stack.isEmpty()
Node node = stack.pop();
System.out.print(node.data + " ");
pNode = node.rightChild;
}
}
}
非递归的方式实现先序遍历
实现思路:
(1)如果当前节点存在,则处理当前节点的value(先处理根节点的值),然后将当前节点入栈,当前节点指向leftChild,并对leftChild(此时的当前节点)进行相同处理。重复1
(2)如果当前节点不存在,当前节点指向栈顶元素,栈顶元素出栈,当前节点指向rightChild,并对rightChild(此时的当前节点)进行相同处理。重复1
/**
* 与上面的中序遍历类似,存储入栈的元素是先序遍历,存储出栈的元素是中序遍历
*/
public static void preOrderTraverse(Node root) {
LinkedList<Node> stack = new LinkedList<>();
Node pNode = root;
while (pNode != null || !stack.isEmpty()) {
if (pNode != null) {
System.out.print(pNode.data + " ");
stack.push(pNode);
pNode = pNode.leftChild;
} else { //pNode == null && !stack.isEmpty()
Node node = stack.pop();
pNode = node.rightChild;
}
}
}
六、后序遍历
后序遍历在访问完左子树向上回退到根节点的时候不是立马访问根节点的,而是得先去访问右子树,访问完右子树后在回退到根节点
public static void postOrderTraverse(Node root) {
Stack<Node> stack = new Stack<>();
Node cur = root;
Node pre = null;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur); // 添加根节点
cur = cur.leftChild; // 递归添加左节点
}
cur = stack.peek(); // 已经访问到最左的节点了
//在不存在右节点或者右节点已经访问过的情况下,访问根节点
if (cur.rightChild == null || cur.rightChild == pre) {
stack.pop();
System.out.print(cur.data + " ");
pre = cur;
cur = null;
} else {
cur = cur.rightChild; // 右节点还没有访问过就先访问右节点
}
}
}
七、总结
可以先记忆三种先序遍历的写法,然后推出其他几种遍历方式的写法