一、二叉搜索树
1.O(logn)
的计算规模有什么特点?
- 经典的一句话:像计算规模不断除以 2 的,那么它的复杂度就是 O(log2n)
2. 在 n 个动态的整数中搜索某个整数(从这里理解为什么我们需要学习二叉搜索树)?
3. 二叉搜索树的英文名是什么?基本定义是什么?
- 二叉搜索树:Binary Search Tree
4. 观察如下二叉搜索树的接口设计,思考对比普通数组的接口,少了什么概念?为什么会少掉?
- 少了索引的概念
- 因为二叉搜索树的索引概念比较难以定义
二、对二叉树的遍历
1、线性数据结构的遍历比较简单,通常有哪两种方法?
- 正序遍历
- 逆序遍历
2、根据节点访问顺序的不同,二叉树的场景遍历方式有 4 种,分别是什么?
- 前序遍历(Preorder Traversal)
- 中序遍历(Inorder Traversal)
- 后续遍历(Postorder Traversal)
- 层序遍历 (Level Order Traversal)
3、什么是前序遍历(Preorder Traversal)?代码如何实现?
- 访问顺序:根节点、前序遍历左子树、前序遍历右子树
public void preorderTraversal() {
preorderTraversal(root);
}
private void preorderTraversal(Node node) {
if (node == null) return;
System.out.print(node.element + ",");
preorderTraversal(node.left);
preorderTraversal(node.right);
}
4、什么是中序遍历(Inorder Traversal)?代码如何实现?
- 访问顺序:中序遍历左子树、根节点、中序遍历右子树
public void inorderTraversal() {
inorderTraversal(root);
}
private void inorderTraversal(Node node) {
if (node == null) return;
inorderTraversal(node.left);
System.out.print(node.element + ",");
inorderTraversal(node.right);
}
5、什么是后序遍历(Postorder Traversal)?代码如何实现?
- 访问顺序:后续遍历左子树、后续遍历右子树、根节点
public void postorderTraversal() {
postorderTraversal(root);
}
private void postorderTraversal(Node node) {
if (node == null) return;
postorderTraversal(node.left);
postorderTraversal(node.right);
System.out.print(node.element + ",");
}
6、什么是层序遍历(Level Order Traversal)?代码如何实现?
- 访问顺序:从上到下,从左到右依次访问每一个节点
public void levelOrderTraversal() {
if (root == null) return;
Queue> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
Node node = queue.poll();
System.out.print(node.element + ",");
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
7、上面四种遍其实还存在问题,比如外部想拿到二叉树每个节点做点事情,是无法办到的,怎么办?
- 主要思路:让外部代码逻辑传递给搜索二叉树,让内部在合适的实际进行调用。
public static interface Visitor {
public boolean visit(E element);
}
public void levelOrder(Visitor visitor) {
if (root == null) return;
Queue> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node node = queue.poll();
if (visitor.visit(node.element)) return;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
8、如果还想对上面的遍历方法做一次增加,客户端可以自由控制遍历停止时机,要怎么做?
- 对于层级遍历非常容易,仅需提供一个返回值就可以达到要求。
- 对于其他的递归遍历,需要将 Visitor 接口变成抽象类,存储一个 stop 值。(因为 visitor 是跟随整个过程的)
//抽象类
public static abstract class Visitor {
boolean stop = false;
public abstract boolean visit(E element);
}
//后续遍历
public void postorder(Visitor visitor) {
postorder(root, visitor);
}
private void postorder(Node node, Visitor visitor) {
if (node == null || visitor.stop) return;
postorder(node.left, visitor);
postorder(node.right, visitor);
if (visitor.stop) return;
visitor.stop = visitor.visit(node.element);
}
三、打印二叉树、练习获取二叉树高度、是否为完全二叉树,翻转二叉树
1、当你在网络上看到一棵二叉搜索树图片,如何将二叉搜索树在你的代码里面还原?
- 使用层序遍历的顺序,将节点填入数组
2、如何有层次结构的打印一棵二叉树?
- 通常使用前序遍历的逻辑进行打印,优先打印根节点。
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
toString(root, sb, "");
return sb.toString();
}
private void toString(Node node, StringBuilder sb, String prefix) {
if (node == null) return;
sb.append(prefix).append(node.element).append("\n");
toString(node.left, sb, prefix + "L__");
toString(node.right, sb, prefix + "R__");
}
3、获取二叉树的高度?
- 递归实现:①第一层节点高度 = max(第二层左子树高度+1, 第二层右子树高度+1) ②如果节点为空,则返回 0
public int height1() {
return height1(root);
}
//递归实现:获取二叉树高度
private int height1(Node node) {
if (node == null) return 0;
return 1 + Math.max(height1(node.left), height1(node.right));
}
- 层级遍历实现:每遍历完一层 height++(在即将遍历下一层时,可以拿到下一层的节点总数)
public int height() {
Queue> queue = new LinkedList<>();
queue.offer(root);
//树的高度
int height = 0;
//存储着每一层的元素数量
int levelSize = queue.size();
while (!queue.isEmpty()) {
Node node = queue.poll();
levelSize--;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
if (levelSize == 0) { //意味着即将要访问下一层
height ++;
levelSize = queue.size();
}
}
return height;
}
4、判断是否为完全二叉树?
public boolean isComplete() {
if (root == null) return false;
Queue> queue = new LinkedList<>();
queue.offer(root);
boolean leaf = false;
while (!queue.isEmpty()) {
Node node = queue.poll();
if (!node.isLeaf() && leaf) {
return false;
}
if (node.left != null) {
queue.offer(node.left);
} else if (node.right != null) {//①如果node.left == null && node.right != null 返回 false
return false;
}
if (node.right != null) {
queue.offer(node.right);
}
if (!node.hasTwoChildren()) {
leaf = true;
}
}
return true;
}
5、如何翻转一棵二叉树?
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
Queue queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
TreeNode tempNode = node.left;
node.left = node.right;
node.right = tempNode;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return root;
}
四、前驱节点、后继节点、删除节点
1、根据遍历结果如何重构二叉树?
以下结果可以保证重构出唯一的一棵二叉树
- ① 前序遍历 + 中序遍历
- ② 后序遍历 + 中序遍历
- ③ 前序遍历 + 后序遍历 + 它是一棵真二叉树(Proper Binary Tree)
2、前驱节点(predecessor)的定义?如何查找前驱节点?
- 前驱节点:中序遍历时的前一个节点;如果是二叉搜索树,前驱节点就是前一个比它小的节点。
private Node predecessor(Node node) {
//前驱节点在左子树当中:predecessor = node.left.right.right.right...
//终止条件:right 为 null
Node p = node.left;
if (p != null) {
while (p.right != null) {
p = p.right;
}
return p;
}
//前驱节点在父节点当中predecessor = node.parent.parent.parent...
//终止条件:node 在 parent 的右子树
while (node.parent != null && node == node.parent.left) {
node = node.parent;
}
return node.parent;
}
3、后继节点(successor)的定义?如何查找后继节点?
后继节点:中序遍历时的后一个节点;如果是二叉搜索树,后继节点就是后一个比它大的节点。
private Node successor(Node node) {
//后继节点在右子树当中:successor = node.right.left.left.left...
//终止条件 left 为 null
Node p = node.right;
if (p != null) {
while (p.left != null) {
p = p.left;
}
return p;
}
//当后继节点在父节点中 successor = node.parent.parent.parent...
//终止条件:node 在 parent 的左子树
while (node.parent != null && node == node.parent.right) {
node = node.parent;
}
return node.parent;
}
4、删除分段思考--如何删除度为 0 的节点?
- 直接删除
if (node == node.parent.left) node.parent.left = null;
if (node == node.parent.right) node.parent.right = null;
if (node.parent == null) root = null;
5、删除分段思考--如何删除度为 1 的节点?
- 用子节点代替原节点的位置
- child 是 node.left 或者 child 是 node.right
if(child == node.left) {
child.parent = node.parent;
child.parent.left = child;
}
if(child == node.right) {
child.parent = node.parent;
child.parent.right = child;
}
if(child == root) {
root = child;
child.parent = null;
}
6、删除分段思考--如何删除度为 2 的节点?
- 先用前驱或后继节点的值
覆盖
原节点的值 - 然后删除相应前驱或后继节点
- 如果
度为 2 的节点
的前驱或后继节点,它们度要么为 0 要么为 1(因为前驱节点的查找特点,度为 2 的节点
不会向父节点找前驱或后继)
7、代码删除节点
private void remove(Node node) {
if (node == null) return;
size--;
//先处理度为 2 的节点:①用前驱节点的值覆盖要删除的节点
if (node.hasTwoChildren()) {
Node predecessor = predecessor(node);
node.element = predecessor.element;
node = predecessor;
}
if (node.isLeaf()) { //②删除度为 0 的节点
if (node.parent == null) {
root = null;
} else if (node == node.parent.left) {
node.parent.left = null;
} else {
node.parent.right = null;
}
} else { //②删除度为 1 的节点
Node replacement = node.left != null ? node.left : node.right;
replacement.parent = node.parent;
if (node.parent == null) {
root = replacement;
} else if (node == node.parent.left) {
node.parent.left = replacement;
} else {
node.parent.right = replacement;
}
}
}