本文主要介绍下二叉树常用的一部分操作
数据结构
提到二叉树,离不开数据结构,数据结构中常用的数据结构就是链表和数组了。
数据结构的基本操作
对于任何数据结构,其基本操作无非遍历 + 访问,再具体一点就是:增删查改。
数据结构存在的目的都是在不同的应用场景,尽可能高效地增删查改
如何遍历 + 访问?我们仍然从最高层来看,各种数据结构的遍历 + 访问无非两种形式:线性的和非线性的。
线性就是 for/while 迭代为代表,非线性就是递归为代表。再具体一步,无非以下几种框架:
遍历
数组遍历框架,典型的线性迭代结构:
void traverse(int[] arr) {
for (int i = 0; i < arr.length; i++) {
// 迭代访问 arr[i]
}
}
链表遍历框架,兼具迭代和递归结构:
/* 基本的单链表节点 */
class ListNode {
int val;
ListNode next;
}
void traverse(ListNode head) {
for (ListNode p = head; p != null; p = p.next) {
// 迭代访问 p.val
}
}
void traverse(ListNode head) {
// 递归访问 head.val
traverse(head.next)
}
二叉树遍历框架
二叉树遍历框架,典型的非线性递归遍历结构
/* 基本的二叉树节点 */
class TreeNode {
int val;
TreeNode left, right;
}
void traverse(TreeNode root) {
traverse(root.left)
traverse(root.right)
}
二叉树框架可以扩展为 N 叉树的遍历框架:
/* 基本的 N 叉树节点 */
class TreeNode {
int val;
TreeNode[] children;
}
void traverse(TreeNode root) {
for (TreeNode child : root.children)
traverse(child)
}
N 叉树的遍历又可以扩展为图的遍历,因为图就是好几 N 叉棵树的结合体。你说图是可能出现环的?这个很好办,用个布尔数组 visited 做标记就行了。
所谓框架,就是套路。不管增删查改,这些代码都是永远无法脱离的结构,可以把这个结构作为大纲,根据具体问题在框架上添加代码就行了,下面会具体举例。
二叉树操作集锦
二叉树算法的设计的总路线:明确一个节点要做的事情,然后剩下的事抛给框架。
void traverse(TreeNode root) {
// root 需要做什么?在这做。
// 其他的不用 root 操心,抛给框架
traverse(root.left);
traverse(root.right);
}
例如:
1. 如何把二叉树所有的节点中的值加一?
void plusOne(TreeNode root) {
if (root == null) return;
root.val += 1;
plusOne(root.left);
plusOne(root.right);
}
2. 如何判断两棵二叉树是否完全相同?
boolean isSameTree(TreeNode root1, TreeNode root2) {
// 都为空的话,显然相同
if (root1 == null && root2 == null) return true;
// 一个为空,一个非空,显然不同
if (root1 == null || root2 == null) return false;
// 两个都非空,但 val 不一样也不行
if (root1.val != root2.val) return false;
// root1 和 root2 该比的都比完了
return isSameTree(root1.left, root2.left)
&& isSameTree(root1.right, root2.right);
}
二叉搜索树
二叉搜索树(Binary Search Tree,简称 BST)是一种很常用的的二叉树。它的定义是:一个二叉树中,任意节点的值要大于等于左子树所有节点的值,且要小于等于右边子树的所有节点的值。
即 二叉搜索树中序遍历是一个有序的升序序列
如下就是一个符合定义的 BST:
中序遍历结果为:1、2、3、4、5、6、7
下面实现 BST 的基础操作:判断 BST 的合法性、增、删、查。其中“删”和“判断合法性”略微复杂。
零、判断 BST 的合法性
这里是有坑的哦,我们按照刚才的思路,每个节点自己要做的事不就是比较自己和左右孩子吗?看起来应该这样写代码:
boolean isValidBST(TreeNode root) {
if (root == null) return true;
if (root.left != null && root.val <= root.left.val) return false;
if (root.right != null && root.val >= root.right.val) return false;
return isValidBST(root.left)
&& isValidBST(root.right);
}
上述算法其实是有错误的,BST 的每个节点应该要小于右边子树的所有节点,下面这个二叉树显然不是 BST,但是我们的算法会把它判定为 BST。
出现错误,不要慌张,框架没有错,一定是某个细节问题没注意到。我们重新看一下 BST 的定义,root 需要做的不只是和左右子节点比较,而是要整个左子树和右子树所有节点比较。
找到问题所在,就开始进行修改。我们可以使用辅助函数,增加函数参数列表,在参数中携带额外信息,正确代码如下:
boolean isValidBST(TreeNode root) {
return isValidBST(root, null, null);
}
boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) {
if (root == null) return true;
if (min != null && root.val <= min.val) return false;
if (max != null && root.val >= max.val) return false;
return isValidBST(root.left, min, root)
&& isValidBST(root.right, root, max);
}
在 BST 中查找一个数是否存在
根据上述思想,代码如下:
boolean isInBST(TreeNode root, int target) {
if (root == null) return false;
if (root.val == target) return true;
return isInBST(root.left, target)
|| isInBST(root.right, target);
}
上述代码已经使用了框架性思维,但是上述写法没有利用BST的特性:即左小右大的特性
我们可以将上述代码稍微改动,类似二叉查找思想,根据根节点与目标值的关系,可以排除一颗子树,改动后的代码如下:
boolean isInBST(TreeNode root, int target) {
if (root == null) return false;
if (root.val == target)
return true;
if (root.val < target)
return isInBST(root.right, target);
if (root.val > target)
return isInBST(root.left, target);
// root 该做的事做完了,顺带把框架也完成了,妙
}
再通过对上述代码进行改造,抽象出一套针对BST的遍历框架
二叉搜索树遍历框架
void BST(TreeNode root, int target) {
if (root.val == target)
// 找到目标,做点什么
if (root.val < target)
BST(root.right, target);
if (root.val > target)
BST(root.left, target);
}