玩转二叉树
树是计算机编程当中很基础的一种数据结构,二叉树则是树中最基本的一种。虽然很多程序员在日常工作中未必会直接用到二叉树,但是对于这样一种基础的数据结构,熟练掌握其基本概念和操作,有助于关键场合不露怯。毕竟,所有的玩儿法都是由最基本最核心的理念衍生而成。
基本概念
二叉树,顾名思义,从根结点开始,每个结点可以分出最多两个子结点,没有子结点的结点叫叶子。
形态
满二叉树
“满”即是“饱满”:一个从根到叶子,每一个非叶子结点都分出两个子结点,看上去是那么的饱满。其总结点个数计算公式为:$2k-1$,$k$为深度;第$n$层结点个数计算公式为:$2$。
完全二叉树
不饱满也不要紧,只要缺的都是右叶子而不缺左叶子。其深度计算公式为:$\lfloor log_2k\rfloor+1$。
二叉搜索树/红黑树
左子结点值小于父结点值,父结点值小于右子结点值,且左子树中所有结点值均小于根结点值,根结点值小于右子树中任一结点值。
基本操作
定义
public class TreeNode {
private int value;
private TreeNode leftChildNode;
private TreeNode rightChildNode;
TreeNode() {
}
TreeNode(int value) {
this.value = value;
}
TreeNode(int value, TreeNode leftChildNode, TreeNode rightChildNode) {
this.value = value;
this.leftChildNode = leftChildNode;
this.rightChildNode = rightChildNode;
}
}
遍历
深度优先
深度遍历二叉树分为前序、中序和后序遍历,一般采用递归的实现形式比较简洁。无论哪种遍历,左结点一定是在右结点之前遍历,而所谓的前、中、后,则是中间结点在什么时机处理的区别。因此:
- 前序遍历 = 中 → 左 → 右
- 中序遍历 = 左 → 中 → 右
- 后序遍历 = 左 → 右 → 中
public void traverseDepthFirst(TreeNode node) {
if (node == null) {
return;
}
System.out.println(node.value); // Preorder.
traverseDepthFirst(node.leftChildNode);
// System.out.println(node.value); // Inorder.
traverseDepthFirst(node.rightChildNode);
// System.out.println(node.value); // Postorder.
}
宽度优先
层序遍历二叉树一般可以用LinkedList
或类似的数据类结合迭代来实现:
public void traverseBreadthFirst(TreeNode rootNode) {
LinkedList<TreeNode> linkedList = new LinkedList<>();
if (rootNode == null) {
return;
}
linkedList.offer(rootNode);
while (!linkedList.isEmpty()) {
int size = linkedList.size();
for (int index = 0; index < size; index++) {
TreeNode node = linkedList.pop();
if (node == null) {
continue;
}
System.out.println(node.value);
linkedList.offer(node.leftChildNode);
linkedList.offer(node.rightChildNode);
}
}
}
聊完遍历来看看构建吧。可以通过两个数组来构建一个二叉树,这两个数组分别对应的是二叉树的前序及中序遍历,或者中序与后序遍历。下面这段代码就是通过递归的方式,用前序和中序遍历数组构建出一个二叉树。而利用中序和后序遍历构建与这种方式雷同,区别在于:前序数组的第一个元素代表父结点,后续数组中对应的是最后一个元素。
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder == null || preorder.length == 0 || inorder == null || inorder.length == 0
|| preorder.length != inorder.length) {
return null;
}
int rootValue = preorder[0];
TreeNode root = new TreeNode(rootValue);
if (preorder.length == 1) {
return root;
}
int delimiterIndex = 0;
for (int number : inorder) {
if (number == rootValue) {
break;
}
delimiterIndex++;
}
int[] leftInorder = Arrays.copyOfRange(inorder, 0, delimiterIndex);
int[] rightInorder = Arrays.copyOfRange(inorder, delimiterIndex + 1, inorder.length);
int[] leftPreorder = Arrays.copyOfRange(preorder, 1, delimiterIndex + 1);
int[] rightPreorder = Arrays.copyOfRange(preorder, delimiterIndex + 1, preorder.length);
root.leftChildNode = buildTree(leftPreorder, leftInorder);
root.rightChildNode = buildTree(rightPreorder, rightInorder);
return root;
}
二叉树最核心的概念和操作基本就是这些,至于其它诸如合并、翻转、搜索、验证、最大……之类的问题,无非就是在遍历或构建的不同阶段实现不同的逻辑需求。所以,与其试图记住所有二叉树问题的答案,倒不如回过头来,找到二叉树概念的根,彻彻底底地吃透,从而任何问题都可以自己推导出解来。