文章目录
二叉树的遍历算法分为三种
1. 递归
2. 迭代
3. Mirrors遍历
本文主要讲述Mirror遍历与迭代算法,递归算法很简单,不再专门重述。
本文所使用的创建树的代码如下,思想采用的是层序遍历的逆向:
创建二叉树
public static TreeNode createTree(Integer[] root){
if (root == null || root.length == 0)
return null;
TreeNode[] nodes = new TreeNode[root.length];
nodes[0] = new TreeNode(root[0]);
int floor = 1;
int levels = (int) Math.ceil(Math.log(root.length+1)) + 1;
while (floor < levels){
int start = (int) (Math.pow(2,floor)-1);
int end = (int) (Math.pow(2,floor+1)-1);
if (end > root.length)
end = root.length;
for (int i = start; i < end; i++) {
if (root[i] != null) {
nodes[i] = new TreeNode(root[i]);
}
int parent = (i-1)/2;
if (parent >= 0){
if (nodes[parent] != null){
if (i % 2 == 0){
nodes[parent].right = nodes[i];
}else {
nodes[parent].left = nodes[i];
}
}
}
}
floor++;
}
return nodes[0];
}
本文所使用的二叉树如下:
递归
先序遍历
// 先序
public void preTravel(TreeNode root){
if (root == null)
return;
System.out.println(root.val);
preTravel(root.left);
preTravel(root.right);
}
中序遍历
// 中序
public void midTravel(TreeNode root){
if (root == null)
return;
midTravel(root.left);
System.out.println(root.val);
midTravel(root.right);
}
后序遍历
// 后序
public void postTravel(TreeNode root){
if (root == null)
return;
postTravel(root.left);
postTravel(root.left);
System.out.println(root.val);
}
迭代
先序遍历
1. 申请一个新的栈,记为stack。将头节点root压入stack中。
2.从stack中弹出栈顶节点,记为cur,然后打印cur的值,再将cur的右孩子(不为空的话)先压入stack中,最后将cur的左孩子(不为空的话)压入stack中。
3. 不断重复步骤2,直到stack为空,全部过程结束。
先序走流程
节点1先入栈,然后弹出来并打印,将节点3入栈,节点2入栈,从栈顶到栈底依次为2,3。
节点2弹出来并打印,节点5入栈,节点4入栈,此时栈顶到栈底依次是4,5,3。
节点4弹出来并打印,节点4没有孩子压入栈,stack从栈顶到栈底依次是5,3。
节点5弹出来并打印,节点5只有左孩子8入栈,stack从栈顶到栈底依次是8,3。
节点8弹出来并打印,节点8没有孩子压入栈,stack从栈顶到栈底依次是3。
节点3弹出来并打印,节点7先入栈,节点6再入栈,stack从栈顶到栈底依次是6,7。
节点6弹出来并打印,节点6没有孩子压入栈,stack从栈顶到栈底依次是7。
节点7弹出来并打印,节点6没有孩子压入栈,stack已经为空,过程停止。
打印顺序: 1,2,4,5,8,3,6,7。
代码实现
public void preTravelUnRecur(TreeNode root){
System.out.println("pre-order");
if (root != null){
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.add(root);
while (!stack.isEmpty()){
root = stack.pop();
System.out.print(root.val+",");
if (root.right != null)
stack.push(root.right);
if (root.left != null)
stack.push(root.left);
}
}
System.out.println("pre-order is end!");
}
中序遍历
- 申请一个新的栈,记为stack。初始时,令变量cur=root。
- 先把cur节点压入栈中,以对cur节点为头整颗子树来说,依次把左边界压入栈中,即不停地令cur=cur.left,然后重复步骤2。
- 不断重复步骤2,直到发现cur为空,此时从stack中弹出一个节点,记为node。打印node的值,并且让cur=node.right,然后继续重复步骤2。
- 当stack为空且cur为空时,整个过程结束。
举例说明流程
初始时cur节点为1,将节点1压入stack中,令cur=cur.left,即cur变为节点2。
cur为节点2,将节点2压入stack,令cur=cur.left,即cur变为节点4。
cur为节点4,将节点4压入stack,令cur=cur.left,即cur变为节点null,此时stack从栈顶到栈底为4,2,1。
cur为null,从stack弹出节点4(node)并打印,令cur=node.right,即cur变为节点null,此时stack从栈顶到栈底为2,1。
cur为null,从stack弹出节点2(node)并打印,令cur=node.right,即cur变为节点5,此时stack中栈顶到栈底为1。
cur为5,将节点5压入stack,令cur = cur.left,即cur=8,此时stack从栈顶到栈底为5,1。
cur为8,将节点8压入stack,令cur = cur.left,即cur=null,此时stack从栈顶到栈底为8,5,1。
cur为null,从stack弹出节点8(node)并打印,令cur = node.right,此时stack从栈顶到栈底为5,1。
cur为null,从stack弹出节点5(node)并打印,令cur = node.right,此时stack从栈顶到栈底为1。
cur为null,从stack弹出节点1(node)并打印,令cur = node.right,此时stack为空。但是cur并不为空!!
cur为3,将节点3压入stack,cur=cur.left,即cur变为节点6,此时stack从栈顶到栈底为3.
cur为6,将节点6压入stack,cur=cur.left,即cur=null,此时stack从栈顶到栈底为6,3.
cur为null,从stack弹出节点6(node)并打印,cur=node.right,即cur=null,此时stack从栈顶到栈底为3.
cur为null,从stack弹出节点3(node)并打印,cur=node.right,即cur=7,此时stack为空。
cur为7,将节点7压入stack,cur=cur.left,即cur=null,此时stack从栈顶到栈底为7。
cur为null,从stack弹出节7(node)并打印,cur=node.right,即cur=null,此时stack为空.
&emsp;cur=null 并且 stack为空,整个过程停止。
代码实现
public void midTravelUnRecur(TreeNode root){
System.out.println("in-order:");
if (root != null){
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || root != null){
if (root != null){
stack.push(root);
root = root.left;
}else {
root = stack.pop();
System.out.println(root.val+",");
root = root.right;
}
}
}
System.out.println("in order is end!");
}
后序遍历
后序遍历非递归实现稍微有一点麻烦,本文提供两种实现思路。
思路一:两个栈搞定版
- 申请一个栈,记为s1,然后将头节点root压入s1中。
- 从s1中弹出的节点记为cur,然后依次将cur的左孩子和右孩子压入s1中。
- 在整个过程中,每一个从s1中弹出的节点都放进s2中。
- 不断重复步骤2和步骤3,直到s1为空,过程停止。
- 从s2中依次弹出节点并打印,打印的顺序就是后续遍历的顺序。
举例说明
节点1放入s1中。
从s1中弹出节点1,节点1放入s2,然后将节点2和节点3依次放入s1,此时s1从栈顶到栈底为3,2; s2从栈顶到栈底为1。
从s1中弹出节点3,节点3放入s2, 节点6,7放入s1,此时s1从栈顶到栈底依次为7,6,2; s2依次从栈顶到栈底为3,1。
从s1中弹出节点7,节点7放入s2, 无节点放入s1,此时s1从栈顶到栈底依次为6,2; s2依次从栈顶到栈底为7,3,1。
从s1中弹出节点6,节点6放入s2, 无节点放入s1,此时s1从栈顶到栈底依次为2; s2依次从栈顶到栈底为6,7,3,1。
从s1中弹出节点2,节点2放入s2, 节点4,5放入s1,此时s1从栈顶到栈底依次为4,5; s2依次从栈顶到栈底为2,6,7,3,1。
从s1中弹出节点5,节点5放入s2, 节点8放入s1,此时s1从栈顶到栈底依次为4,8; s2依次从栈顶到栈底为5,2,6,7,3,1。
从s1中弹出节点8,节点8放入s2, 无节点放入s1,此时s1从栈顶到栈底依次为4; s2依次从栈顶到栈底为8,5,2,6,7,3,1。
从s1中弹出节点4,节点4放入s2, 无节点放入s1,此时s1为空; s2依次从栈顶到栈底为4,8,5,2,6,7,3,1。
s1为空,此时只需要打印s2中的节点即可: 4,8,5,2,6,7,3,1。
过程可以总结为:每颗子树的头结点都最先从s1中弹出,然后把该节点的孩子节点按照先左再右的顺序压入s1,这时从s1弹出的顺序就是先右再左,所以从s1中弹出的顺序就是中,右,左。 对于s2来说,就是把s1斤进行逆序!!所以s2从栈顶到栈底为左、右、中。
代码实现
// 后序
// 两个栈版本
public void postTravel1(TreeNode root){
System.out.println("post-order: ");
if (root != null){
Stack<TreeNode> s1 = new Stack<>();
Stack<TreeNode> s2 = new Stack<>();
s1.push(root);
while (!s1.isEmpty()){
root = s1.pop();
s2.push(root);
if (root.left != null)
s1.push(root.left);
if (root.right != null)
s1.push(root.right);
}
while (!s2.isEmpty()){
System.out.println(s2.pop().val + " ");
}
}
System.out.println("post order is end!");
}
思路二:一个栈搞定版
- 申请一个栈,记为stack,将头节点压入stack,同时设置两个变量h和c。在整个流程中,h代表最近一次弹出并打印的节点,c代表stack的栈顶节点,初始时h为头节点,c为null。
- 每次令c等于当前stack的栈顶节点,但是不从stack中弹出,这时分三种情况:
- 如果c的左孩子不为null,并且h不等于c的左孩子,也不等于c的右孩子,则把c的左孩子压入stack中。原因:h的含义是最近一次弹出并打印的节点,所以如果h等于c的左孩子或者右孩子,说明c的左子树与右子树已经打印完毕,此时不应该再将c的左孩子放入stack中。否则,说明左子树还没有处理过,此时将c的左孩子压入stack中。
- 如果条件1不成立,并且c的右孩子不为null,h不等于c的右孩子,则把c的右孩子压入stack中。原因:如果h等于c的右孩子,说明c的右子树已经打印完毕,此时不应该再将c的右孩子放入stack中。否则,说明右子树还没有处理过,此时将c的右孩子压入stack中。
- 如果条件1和条件2都不成立,说明c的左子树与右子树都已经打印完毕,那么从stack中弹出c并打印,然后令h=c。
- 一直重复步骤2,直到stack为空。
举例说明:
节点1压入stack中,初始时h为节点1,c为null,stack 从栈顶到栈底为1。
令c等于stack的栈顶节点——节点1,此时步骤2的条件1命中,将节点2压入stack中,h为节点1,stack从栈顶到栈底依次为2,1。
令c等于stack的栈顶节点——节点2,此时步骤2的条件1命中,将节点4压入stack中,h为节点1,stack从栈顶到栈底依次为4,2,1。
令c等于stack的栈顶节点——节点4,此时步骤2的条件3命中,将节点4从stack中弹出并打印,h变为节点4,stack从栈顶到栈底为2,1。
令c等于stack的栈顶节点——节点2,此时步骤2的条件2命中,将节点5压入stack中,h为节点4,stack从栈顶到栈底依次为5,2,1。
令c等于stack的栈顶节点——节点5,此时步骤2的条件1命中,将节点8压入stack中,h为节点4,stack从栈顶到栈底依次为8,5,2,1。
令c等于stack的栈顶节点——节点8,此时步骤2的条件3命中,弹出栈顶节点8,h为节点8,stack从栈顶到栈底依次为8,5,2,1。
令c等于stack的栈顶节点——节点5,此时步骤2的条件3命中,弹出栈顶节点5,h为节点5,stack从栈顶到栈底依次2,1。
令c等于stack的栈顶节点——节点2,此时步骤2的条件3命中,弹出栈顶节点2,h为节点2,stack从栈顶到栈底依次1。
令c等于stack的栈顶节点——节点1,此时步骤2的条件2命中,节点3入stack,h为节点1,stack从栈顶到栈底依次3,1。
令c等于stack的栈顶节点——节点3,此时步骤2的条件1命中,节点6入stack,h为节点1,stack从栈顶到栈底依次6,3,1。
令c等于stack的栈顶节点——节点6,此时步骤2的条件3命中,弹出节点6,h为节点6,stack从栈顶到栈底依次3,1。
令c等于stack的栈顶节点——节点3,此时步骤2的条件2命中,节点7入栈,h为节点6,stack从栈顶到栈底依次7,3,1。
令c等于stack的栈顶节点——节点7,此时步骤2的条件3命中,节点7弹出,h为节点7,stack从栈顶到栈底依次3,1。
令c等于stack的栈顶节点——节点3,此时步骤2的条件3命中,节点3弹出,h为节点3,stack从栈顶到栈底依次1。
令c等于stack的栈顶节点——节点1,此时步骤2的条件3命中,节点1弹出,h为节点1,stack为空。
代码实现
// 一个栈版本
public void postOrderUnRec2(TreeNode root){
System.out.println("post-order : ");
if (root != null){
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
TreeNode c = null;
while (!stack.isEmpty()){
c = stack.peek();
if (c.left != null && root != c.left && root != c.right)
stack.push(c.left);
else if (c.right != null && root != c.right)
stack.push(c.right);
else{
System.out.println(stack.pop().val + " ");
root = c;
}
}
}
System.out.println("post order is end");
}
到此,二叉树的三种遍历的迭代版本已经描述完毕。但是迭代方法的空间复杂度为O(n),因此有了下面的Mirrors版本遍历,其空间复杂度为O(1)。
Mirrors遍历
Mirrors核心思想
假设当前来到节点cur,开始时cur来到头结点的位置
-
如果cur没有左孩子,cur向右移动(cur = cur.right )
-
如果cur有左孩子,找到左子树上最右的节点mostRight:
a. 如果mostRight的右指针指向空,让其指向cur,然后cur向左移动(cur = cur.left)
b. 如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur=cur.right) -
cur为空遍历停止
Mirrors遍历的模板代码
//模板
public void Mirrors(TreeNode root){
if (root == null)
return;
TreeNode cur = root;
TreeNode mostRight = null;
while (cur != null){
mostRight = cur.left; // mostRight 是cur的左孩子
if (mostRight != null){ // 有左子树
while (mostRight.right != null && mostRight.right != cur)
mostRight = mostRight.right;
// mostRight变成了cur左子树上的最右节点
if (mostRight.right == null){
mostRight.right = cur;
cur = cur.left;
continue;
}
else
mostRight.right = null;
}
cur = cur.right;
}
}
先序遍历
按照例子走流程
这里使用一个list用来表示cur所走过的路径(实际代码实现中不需要,只是为了方便举例)。
初始时cur = 1, mostRight = 8,节点8的右孩子为空,所以mostRight.right= 1,此时 cur = cur.left;list中的元素为(1);
cur = 2,mostRight=4,节点4的右孩子为空,所以mostRight.right=2,此时cur = cur.left,list中的元素为(1,2);
cur=4, cur没有左孩子,因此cur=cur.right。list中的元素为(1,2,4);
cur=2,mostRight=4,但是mostRight.right不为空,所以执行most.Right=null。cur=cur.right,list中的元素为(1,2,4,2);
cur=5,mostRight=8, mostRight.right=5,cur=cur.left,list中的元素为(1,2,4,2,5)
cur=8,cur没有左孩子,cur=cur.right, list中的元素为(1,2,4,2,5,8)
cur=5, cur有左孩子,左孩子的mostRight.right=cur,所以mostRight.right=null,让cur=cur.right,list中的元素为(1,2,4,2,5,8,5);
上述中cur此时应该=1,mostRight.right=cur,所以置mostRight.right=null,cur=cur.right,list中的元素为(1,2,4,2,5,8,5,1);
cur=3,此时mostRight=6,mostRight.right=3,cur=cur.left,list中元素为(1,2,4,2,5,8,5,1,3)
cur=6,此时cur的左孩子为空,cur=cur.right,list中的元素为(1,2,4,2,5,8,5,1,3,6)
cur=3,此时mostRight.right=3,所以置,mostRight.right=null,cur=cur.right,list中的元素为(1,2,4,2,5,8,5,1,3,6,3)
cur=7,此时cur没有左孩子,cur=cur.right,list中的元素为(1,2,4,2,5,8,5,1,3,6,3,7)
cur=null,结束遍历。
list中的元素代表cur所走过的路径,与递归得到的先序遍历结果对比:
cur走过的路径: (1,2,4,2,5,8,5,1,3,6,3,7)
先序遍历递归得到的结果: (1,2,4,5,8,3,6,7)
对比可以看出只要是cur有左孩子的地方,其遍历的结果都会显示两遍(上述的1,2,3,5),但是在最终的先序遍历中,只需要打印其第一次遍历到的该节点即可。右孩子只会打印一次不用关心。因此先序遍历的Mirrors代码根据模板可以修改为:
代码实现
// 先序
public void preOrderOnMirrors(TreeNode root){
if (root == null)
return;
TreeNode cur = root;
TreeNode mostRight = null;
while (cur != null){
mostRight = cur.left; // mostRight 是cur的左孩子
if (mostRight != null){ // 有左子树
while (mostRight.right != null && mostRight.right != cur)
mostRight = mostRight.right;
// mostRight变成了cur左子树上的最右节点
if (mostRight.right == null){
mostRight.right = cur;
System.out.print(cur.val + " "); // 这里添加打印第一次出现的左孩子
cur = cur.left;
continue;
}
else
mostRight.right = null;
}else
System.out.print(cur.val + " "); // 添加打印右孩子
cur = cur.right;
}
}
中序遍历
按照例子走流程
流程与先序的一样,根据打印的路径流程与递归中序遍历的结果对比可以看出:
cur走过的路径: (1,2,4,2,5,8,5,1,3,6,3,7)
中序遍历递归得到的结果: (4,2,8,5,1,6,3,7)
对比可以看出只要是cur有左孩子的地方,其遍历的结果都会显示两遍(上述的1,2,3,5),但是在最终的中序遍历中,只需要打印其第二次遍历到的该节点即可。右孩子只会打印一次不用关心。因此中序遍历的Mirrors代码根据模板可以修改为:
代码实现
// 中序
public void InOrderOnMirrors(TreeNode root){
if (root == null)
return;
TreeNode cur = root;
TreeNode mostRight = null;
while (cur != null){
mostRight = cur.left; // mostRight 是cur的左孩子
if (mostRight != null){ // 有左子树
while (mostRight.right != null && mostRight.right != cur)
mostRight = mostRight.right;
// mostRight变成了cur左子树上的最右节点
if (mostRight.right == null){
mostRight.right = cur;
cur = cur.left;
continue;
}
else {
mostRight.right = null;
System.out.print(cur.val + " "); // 这里代表第二次走到左孩子需要打印
}
}else
System.out.print(cur.val + " "); // 右孩子需要打印
cur = cur.right;
}
}
后序遍历
按照例子走流程
流程与先序的一样,根据打印的路径流程与递归后序遍历的结果对比可以看出:
cur走过的路径: (1,2,4,2,5,8,5,1,3,6,3,7)
中序遍历递归得到的结果: (4,8,5,2,6,7,3,1)
后序遍历与先序、中序均有一些不一样。但是仍然可以发现,规律其实是,当cur第二次走到某节点时,就要该cur的左孩子的所有右分支逆序打印出来。例如:
当cur第二次来到节点2时,其左孩子只有4,因此打印4。
当cur第二次来到节点5时,其左孩子只有8,因此打印8。
当cur第二次来到节点1时,其左孩子的右分支逆序打印应该为5,2。
当cur第二次来到节点3时,其左孩子只有6,因此打印6。
最后再添加一次从根节点到右分支的逆序打印,为7,3,1。
代码实现:
// 后序
public List<Integer> PostOrderOnMirrors(TreeNode root){
ArrayList<Integer> result = new ArrayList<>();
if (root == null)
return result;
TreeNode cur = root;
TreeNode mostRight = null;
while (cur != null){
mostRight = cur.left; // mostRight 是cur的左孩子
if (mostRight != null){ // 有左子树
while (mostRight.right != null && mostRight.right != cur)
mostRight = mostRight.right;
// mostRight变成了cur左子树上的最右节点
if (mostRight.right == null){
mostRight.right = cur;
cur = cur.left;
continue;
}
else {
mostRight.right = null;
addPath(result, cur.left); // 第二次来到左孩子时逆序打印左孩子的右分支
}
}
cur = cur.right;
}
addPath(result, root); // 逆序打印从根节点的右分支
result.forEach(System.out::println);
return result;
}
private static void addPath(ArrayList<Integer> result, TreeNode root) {
int count = 0 ;
while (root != null){
count++;
result.add(root.val);
root = root.right;
}
int left = result.size() - count;
int right = result.size() - 1;
while (left < right){
int temp = result.get(left);
result.set(left, result.get(right));
result.set(right, temp);
left++;
right--;
}
}
Mirrors遍历其实与递归的思路相似,只是利用了节点的右指针(如果为空)。这个指针就可以将空间复杂度降低为O(1)。