二叉树(binary tree)是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有2个孩子节点。注意,这里是最多有2个,也可能只有1个,或者没有孩子节点。

代码定义:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 二叉树
*/
private static class TreeNode {
int data;
TreeNode leftChild;
TreeNode rightChild;

public TreeNode(int data) {
this.data = data;
}
}

深度优先遍历

递归遍历

前序遍历

二叉树的前序遍历,输出顺序是根节点、左子树、右子树。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 前序遍历
*
* @param root
*/
public static void preOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
System.out.print(root.data + " ");
preOrderTraversal(root.leftChild);
preOrderTraversal(root.rightChild);
}

中序遍历

二叉树的中序遍历,输出顺序是左子树、根节点、右子树。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 中序遍历
*
* @param root
*/
public static void inOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
inOrderTraversal(root.leftChild);
System.out.print(root.data + " ");
inOrderTraversal(root.rightChild);
}

后序遍历

二叉树的后序遍历,输出顺序是左子树、右子树、根节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 后序遍历
*
* @param root
*/
public static void postOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
postOrderTraversal(root.leftChild);
postOrderTraversal(root.rightChild);
System.out.print(root.data + " ");
}

辅助栈遍历

其实二叉树遍历这里,辅助栈遍历才是重点!!!

前序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 辅助栈前序遍历
*
* @param root
*/
public static void preOrderTraversalWithStack(TreeNode root) {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode p = root;
while (p != null || !stack.isEmpty()) {
while (p != null) {
System.out.print(p.data + " ");
stack.push(p);
p = p.leftChild;
}
if (!stack.isEmpty()) {
p = stack.pop();
p = p.rightChild;
}
}
}

中序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 辅助栈中序遍历
*
* @param root
*/
public static void inOrderTraversalWithStack(TreeNode root) {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode p = root;
while (p != null || !stack.isEmpty()) {
while (p != null) {
stack.push(p);
p = p.leftChild;
}
if (!stack.isEmpty()) {
p = stack.pop();
System.out.print(p.data + " ");
p = p.rightChild;
}
}
}

后序遍历

辅助栈后序遍历难点在于遍历时需要判断当前节点是否属于右子树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 辅助栈后序遍历
*
* @param root
*/
public static void postOrderTraversalWithStack(TreeNode root) {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode p = root;
TreeNode pre = null;
while (p != null || !stack.isEmpty()) {
while (p != null) {
stack.push(p);
p = p.leftChild;
}
p = stack.peek();
if (p.rightChild == null || p.rightChild == pre) {
stack.pop();
System.out.print(p.data);
pre = p;
p = null;
} else {
p = p.rightChild;
}
}
}

Morris算法

算法介绍

Morris算法充分利用了二叉树叶子节点下的空间,从而可以在时间复杂度为O(N),空间复杂度为O(1) 的条件下,前中后序遍历二叉树(不是完全二叉树也可以使用)。

而常见的遍历二叉树的方法为递归和栈迭代,这两种方法的时间复杂度虽然也为O(N),但是空间复杂度需要O(N),因此Morris算法可以极大节省空间。

前序遍历

算法步骤

  1. 如果当前节点的左子节点为空时,输出当前节点,并将当前节点置为该节点的右子节点;
  2. 如果当前节点的左子节点不为空,找到当前节点左子树的最右节点(该节点为当前节点中序遍历的前驱节点);
  3. 如果最右节点的右指针为空(right=null),将最右节点的右指针指向当前节点,并输出当前节点(在此处输出),当前节点置为其左子节点;
  4. 如果最右节点的右指针不为空,将最右节点右指针重新置为空(恢复树的原状),并将当前节点置为其右节点;
  5. 重复1~2,直到当前节点为空。

图片演示

img

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Morris算法前序遍历
*
* @param root
*/
public static void preOrderTraversalWithMorris(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
while (cur != null) {
if (cur.leftChild == null) {
System.out.print(cur.data + " ");
cur = cur.rightChild;
} else {
TreeNode t = cur.leftChild;
while (t.rightChild != null && t.rightChild != cur) {
t = t.rightChild;
}
if (t.rightChild == null) {
System.out.print(cur.data + " ");
t.rightChild = cur;
cur = cur.leftChild;
} else {
t.rightChild = null;
cur = cur.rightChild;
}
}
}

}

中序遍历

算法步骤

  1. 如果当前节点的左子节点为空时,输出当前节点,并将当前节点置为该节点的右子节点;
  2. 如果当前节点的左子节点不为空,找到当前节点左子树的最右节点(该节点为当前节点中序遍历的前驱节点);
  3. 如果最右节点的右指针为空(right=null),将最右节点的右指针指向当前节点,当前节点置为其左子节点;
  4. 如果最右节点的右指针不为空,将最右节点右指针重新置为空(恢复树的原状),输出当前节点,并将当前节点置为其右节点;
  5. 重复1~2,直到当前节点为空。

图片演示

img

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Morris算法中序遍历
*
* @param root
*/
public static void inOrderTraversalWithMorris(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
while (cur != null) {
if (cur.leftChild == null) {
System.out.print(cur.data + " ");
cur = cur.rightChild;
} else {
TreeNode t = cur.leftChild;
while (t.rightChild != null && t.rightChild != cur) {
t = t.rightChild;
}
if (t.rightChild == null) {
t.rightChild = cur;
cur = cur.leftChild;
} else {
t.rightChild = null;
System.out.print(cur.data + " ");
cur = cur.rightChild;
}
}
}

}

后序遍历

后序遍历较前两者比较麻烦,需要建立一个临时节点,并令该节点的左子节点为root,并且需要一个子过程,倒序输出某两个节点之间路径上的各个节点。

morris后序遍历严格意义上来说空间复杂度不为O(1)。

算法步骤

  1. 如果当前节点的左子节点为空时,则将其右子节点作为当前节点;
  2. 如果当前节点的左子节点不为空,找到当前节点左子树的最右节点(该节点为当前节点中序遍历的前驱节点);
  3. 如果最右节点的右指针为空(right=null),将最右节点的右指针指向当前节点,当前节点置为其左子节点;
  4. 如果最右节点的右指针不为空,将最右节点右指针重新置为空(恢复树的原状),倒序输出从当前节点的左子节点到该最右节点路径上的所有节点,并将当前节点置为其右节点;
  5. 重复1~2,直到当前节点为空。

图片演示

img

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Morris算法后序遍历
*
* @param root
*/
public static void postOrderTraversalWithMorris(TreeNode root) {
if (root == null) {
return;
}
TreeNode virNode = new TreeNode(-1);
virNode.leftChild = root;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = virNode;
while (cur != null) {
if (cur.leftChild == null) {
cur = cur.rightChild;
} else {
TreeNode tmp = cur.leftChild;
while (tmp.rightChild != null && tmp.rightChild != cur) {
tmp = tmp.rightChild;
}
if (tmp.rightChild == null) {
tmp.rightChild = cur;
cur = cur.leftChild;
} else {
tmp.rightChild = null;
TreeNode t = cur.leftChild;
while (t != null) {
stack.push(t);
t = t.rightChild;
}
while (!stack.isEmpty()) {
System.out.print(stack.pop().data + " ");
}
cur = cur.rightChild;
}
}
}
}

广度优先遍历

广度优先遍历使用队列将其子节点加入队列即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/***
* 广度优先遍历
* @param root
*/
public static void bfs(TreeNode root) {
if (root == null) {
return;
}
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode r = queue.poll();
System.out.println(r.data);
if (r.leftChild != null) {
queue.offer(r.leftChild);
}
if (r.rightChild != null) {
queue.offer(r.rightChild);
}
}
}

参考文章: