二叉树遍历四种算法(递归遍历、非递归遍历、既不用递归也不用栈遍历(三种深度优先遍历),层序遍历(广度优先遍历))
递归遍历
采用递归遍历就很简单了,想好前序(左根右)、中序(根左右)、后序(左右根)三种遍历的顺序与终止递归逻辑,直接写递归代码就行,递归用到的其实也是java虚拟机的方法栈
// 中序递归
public static void inorderRecursiveTraversal(TreeNode root) {
if (root == null) {
return;
}
// 前序后序换换位置,但方法名别忘了换哦
System.out.println(root.value);
inorderRecursiveTraversal(root.left);
inorderRecursiveTraversal(root.right);
}
public static class TreeNode {
public int value;
public TreeNode left;
public TreeNode right;
}
非递归遍历
非递归遍历说到底就是用自己构建的栈代替递归底层用到的java方法栈,这样就没有递归调用自己方法的代码了。但入栈的顺序需要注意,自己实现的栈与java的方法栈概念不同,入栈和出栈的时机不同,java的方法栈是可以重新回到代码中的,自己实现的栈需要依靠循环。但别忘了后打印的得先入栈哦!
// 中序栈
public static void inorderStackTraversal(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode cur = stack.pop();
// 先打印后入栈两个子节点,所以打印完即可抛弃
System.out.println(cur.value);
if (cur.right != null) {
stack.push(cur.right);
}
if (cur.left != null) {
stack.push(cur.left);
}
}
}
以上,中序栈实现比较简单,因为是中左右的顺序,从栈取出即可先打印值再处理左右子节点。
// 前序栈
public static void preorderStackTraversal(TreeNode root) {
Deque<Object> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
Object cur = stack.pop();
if (cur instanceof TreeNode) {
TreeNode curNode = (TreeNode) cur;
// 后序遍历调换位置即可
if (curNode.right != null) {
stack.push(curNode.right);
}
// 因该节点第一次遍历到了,第二次回溯时只用打印值,若再次入节点会造成混淆
stack.push(Integer.valueOf(curNode.value));
if (curNode.left != null) {
stack.push(curNode.left);
}
} else {
System.out.println(cur);
}
}
}
用栈实现前序和后序遍历与中序较为不同,取出栈后不能立即打印值,因为得先处理它的左节点,故需将其左右节点与值重新入栈。
即不用递归也不用栈遍历(利用线索二叉树的思想)
如果即不用递归也不用栈来遍历,这里就无法利用自己构建的栈或java的方法栈来保存打印顺序了,因为终归到底是为了保证遍历的顺序,那必须得寻找其它的方法来记录这种遍历顺序。利用线索二叉树的思想,即不再闲置叶子节点的左右指针,比如中序利用叶子节点右指针指向前序遍历情况时的后继节点。
// 中序线索
public static void inorderThreadedTraversal(TreeNode root) {
TreeNode cur = root;
TreeNode pre;
while (cur != null) {
// 查找当前节点的在前序遍历情况时的前驱节点,规避线索,有点绕哈
pre = cur.left;
while (pre != null && pre.right != null && pre.right != cur) {
pre = pre.right;
}
if (pre != null) {
// 如果已保存线索则为回速找祖先节点的情况,跳过继续往后
if (pre.right == cur) {
pre.right = null;
cur = cur.right;
continue;
// 保存线索
} else {
pre.right = cur;
}
}
System.out.println(cur.value);
// right为线索后继节点
cur = cur.left != null ? cur.left : cur.right;
}
}
以上,需要叶子节点的右指针保存祖先节点以便回溯,自己写完代码debug下看问题在哪。
前序遍历利用线索二叉树和中序的差不多,但需要注意打印时机,一个是回溯祖先节点时需打印,一个是没有左节点的时候需打印。
// 前序线索
public static void preorderThreadedTraversal(TreeNode root) {
TreeNode cur = root;
TreeNode pre;
while (cur != null) {
// 查找当前节点的前驱节点,规避线索
pre = cur.left;
while (pre != null && pre.right != null && pre.right != cur) {
pre = pre.right;
}
if (pre != null) {
// 如果已保存线索则为回溯找祖先节点的情况,打印并跳过继续往后
if (pre.right == cur) {
pre.right = null;
System.out.println(cur.value);
cur = cur.right;
continue;
// 保存线索
} else {
pre.right = cur;
}
}
// 后继节点
if (cur.left != null) {
cur = cur.left;
} else {
System.out.println(cur.value);
cur = cur.right;
}
}
}
以上,前序遍历和中序遍历一样都是将叶子节点的右指针保存为前序遍历情况时的后继节点,然而找当前节点的后继节点无法向上找,故按找当前节点的前驱节点来解决。
后序遍历相比前序和中序难,但叶子节点的线索与前序中序一样都是保存当前节点在前序遍历情况下的前驱节点的右指针指向当前节点,主要体现在什么时候该打印后序遍历左右根的右到根这条路径,具体为在通过叶子节点线索回溯到前序遍历情况下的后继节点后开始倒序打印它的左节点的右臂路径即可。
// 后续线索
public static void postorderThreadedTraversal(TreeNode root) {
TreeNode cur = root;
TreeNode pre;
while (cur != null) {
// 查找当前节点在前序遍历情况下的前驱节点,并规避线索
pre = cur.left;
while (pre != null && pre.right != null && pre.right != cur) {
pre = pre.right;
}
if (pre != null) {
// 如果已保存线索则为回溯找祖先节点的情况
if (pre.right == cur) {
pre.right = null;
// 打印左右根顺序的右到根这条路径
printPre2Left(cur.left);
cur = cur.right;
continue;
} else {
pre.right = cur;
}
}
if (cur.left != null) {
cur = cur.left;
} else {
cur = cur.right;
}
}
// 由于根节点没有父节点,故没有线索指向根节点的父节点,故最后需打印根节点的右臂路径
printPre2Left(root);
}
// 倒序打印 当前节点的左节点 或 根节点 至 前序遍历情况下的前驱节点的路径
private static void printPre2Left(TreeNode node) {
TreeNode cur = node;
if (cur != null) {
printPre2Left(cur.right);
System.out.println(cur.value);
}
}
后序遍历相对较难,可以debug看下其行走逻辑后再自己实现。
利用线索二叉树的思想来实现深度优先遍历涉及复杂的指针改变以及打印时机,可以继续深入考虑下其时间复杂度O(n)和空间复杂度O(1),虽然其适用场景很狭窄但可以锻炼逻辑思维能力。
层序遍历
层序遍历二叉树又可称为广度优先遍历,即二叉树一层一层的打印,如何保存每一层的顺序是关键,可以利用队列来保存每一层的顺序。
// 层序或广度优先遍历(书写顺序)
public static void levelOrderTraversal(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
System.out.println(cur.value);
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
}
以上,每层书写顺序的层序遍历相对简单,将出队列的值取出打印后再将左右节点放入队列尾即可实现;取出的值先打印,其儿子节点相对他们的那一层也是先打印。
之字形层序遍历因为每层打印方向交替相反,故比较简单的想法是用两个队列来实现,每个队列存相反的顺序;也可以用一个双端队列来实现,需要提前存值保存每层的节点个数。
// 层序或广度优先遍历(之字形顺序)
public static void ZigzagLevelOrderTraversal(TreeNode root) {
Deque<TreeNode> deque = new LinkedList<>();
deque.offerFirst(root);
// 保存当前层的节点个数,打印完后刷新为下一层的个数
int nodeNumOfCurLayer = deque.size();
// 每打印完一层换队列头来以相反顺序打印
boolean isLeftPoll = true;
while (!deque.isEmpty()) {
// 双端队列左边出
if (isLeftPoll) {
while (nodeNumOfCurLayer != 0) {
TreeNode cur = deque.pollFirst();
System.out.println(cur.value);
// 保持顺序入队列的右
if (cur.left != null) {
deque.offerLast(cur.left);
}
if (cur.right != null) {
deque.offerLast(cur.right);
}
--nodeNumOfCurLayer;
}
// 双端队列右边出
} else {
while (nodeNumOfCurLayer != 0) {
TreeNode cur = deque.pollLast();
System.out.println(cur.value);
// 保持顺序入队列的左
if (cur.right != null) {
deque.offerFirst(cur.right);
}
if (cur.left != null) {
deque.offerFirst(cur.left);
}
--nodeNumOfCurLayer;
}
}
isLeftPoll = !isLeftPoll;
nodeNumOfCurLayer = deque.size();
}
}
之字形打印就是保证每层的输出顺序如何存储的问题,以两个队列来实现和以双端队列来实现异曲同工,就是将两个队列合并成一个双端队列。