二叉树作为面试中高频出现的数据结构,本文将介绍二叉树基础。
对于二叉树,最常见的面试问题就是:请描述一棵二叉树,二叉树的XX遍历的递归算法与非递归算法。这些都是基础,也就是说是必须要掌握的内容。
本篇博文就致力于梳理清楚,这些基础问题。希望有所收获。
很多人会问,二叉树在我们平时开发中用的很少啊,为什么面试官那么爱问,以及算法题中出现的那么平凡,很多人甚至没有办法立刻想到二叉树的应用有哪些?那么就从这个问题开始本文吧。
1. 实际开发中哪里用到二叉树了呢?
- 用的最多的应该是平衡二叉树,有种特殊的平衡二叉树红黑树,查找、插入、删除的时间复杂度最坏为
O(logn)
。 堆排序
,堆排序是利用堆
这种数据结构而设计的一种排序算法。- 堆是具有以下性质的
完全二叉树
:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
- 堆是具有以下性质的
- Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。
- 还有哈夫曼树编码方面的应用,他是一种压缩算法。
- B-Tree,B±Tree在文件系统中的应用。
2.二叉树
花花酱说过,树是用递归定义的数据结构,凡是树的题目都可以用递归做。当然递归本质上还是利用了程序执行时候的入栈与出栈操作。我们也可以在代码中模拟这种操作,因此树的题目同样可以使用非递归的方式进行解答
用语言描述一棵二叉树?
二叉树是递归定义的数据结构,其结点最多只能有2个子节点,他们被称为左右子节点。
写到这里,我发现了一篇写的非常好的遍历二叉树文章(见参考文献二),既然前人已经写了那么好的文章了,那我就直接参考。下面记录二叉树的三种遍历方式。
定义二叉树
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
前序遍历(递归)
public void preOrder(TreeNode root) {
if (root == null) {
return;
}
System.out.println(root.val);
preOrder(root.left);
preOrder(root.right);
}
中序遍历(递归)
public void inOrder(TreeNode root) {
if (root==null) {
return;
}
inOrder(root.left);
System.out.println(root.val);
inOrder(root.right);
}
后序遍历(递归)
public void postOrder(TreeNode root) {
if (root==null) {
return;
}
postOrder(root.left);
postOrder(root.right);
System.out.println(root.val);
}
前序遍历(非递归)
public void preOrder(TreeNode root) {
if(root==null) {
return;
}
Stack<TreeNode> stack = new Stack();
while(root!=null) {
System.out.println(root.val);
stack.push(root);
root = root.left;
}
while(!stack.isEmpty()) {
TreeNode top = stack.peek();
TreeNode node = top.right;
stack.pop();
while(node!=null) {
System.out.println(node.val);
stack.push(node);
node = node.left;
}
}
}
中序遍历(非递归)
public void inOrder(TreeNode root) {
if(root==null) {
return;
}
Stack<TreeNode> stack = new Stack();
while(root!=null) {
stack.push(root);
root = root.left;
}
while(!stack.isEmpty()) {
TreeNode top = stack.peek();
TreeNode node = top.right;
System.out.println(top.val);
stack.pop();
while(node!=null) {
stack.push(node);
node = node.left;
}
}
}
后序遍历(非递归)
后续遍历相对复杂,原因就在于出栈的情况不一样了。
在先序和中序遍历过程中,只要左子树处理完毕实际上栈顶元素就可以出栈了,但是后续遍历情况不同,什么是后续遍历?只有左子树和右子树都遍历完毕才可以处理当前节点,这是后续遍历,那么我们该如何知道当前节点的左子树和右子树都处理完了呢?
显然我们需要某种方法记录下遍历的过程,实际上我们只需要记录下遍历的前一个节点就足够了。
如果我们知道了遍历过程中的前一个节点,那么我们就可以做如下判断了:
- 如果前一个节点是当前节点的右子树,那么说明右子树遍历完毕可以pop了
- 如果前一个节点是当前节点的左子树而且当前节点右子树为空,那么说明可以pop了
- 如果当前节点的左子树和右子树都为空,也就是叶子节点那么说明可以pop了
public void postOrder(TreeNode root) {
if(root!=null) {
return;
}
Stack<TreeNode> stack = new Stack();
TreeNode before = null;
while(root!=null) {
stack.push(root);
root = root.left;
}
while(!stack.isEmpty()) {
TreeNode top = stack.peek();
if(top.left==null&&top.right==null ||
before==top.right ||
before==top.left&&top.right==null) {
System.out.println(top.val);
before = top;
stack.pop();
} else {
TreeNode node = top.right;
while(node!=null) {
stack.push(node);
node = node.left;
}
}
}
}
下一篇文章,主要讨论的是堆排序。