提出问题
在n个动态的整数中搜索某个整数,你会选择什么数据结构?
- 假设使用动态数组,则从第
0个位置进行遍历搜索,平均时间复杂度为:O(n)
- 假如维护一个有序的动态数组,使用二分搜索,最坏时间复杂度为:
O(logn),但是添加和删除操作的平均时间复杂度为O(n)
如果使用二叉搜索树,无论添加、删除、搜索的最坏时间复杂度可以为:O(logn)。
二叉搜索树(Binary Search Tree)
二叉搜索树是二叉树的一种,又可称为:二叉查找树或二叉排序树
特点:
-
任一节点的值都大于其左子树所有节点的值
-
任一节点的值都小于其右子树所有节点的值
-
它的左右子树也都是二叉搜索树
-
二叉搜索树可以大大提高搜索数据的效率
-
二叉搜索树存储的元素必须具备可比较性
a.例如
int、double等b.如果是自定义类型,需要指定比较方式
c.不允许为
null
实现二叉树的接口设计
// 元素的数量
public int size()
// 是否为空
public boolean isEmpty()
// 清空所有元素
public void clear()
// 添加元素
public void add(E element)
// 移除元素
public void remove(E element)
// 判断是否包含某元素
public boolean contains(E element)
创建节点类
节点中应该包含:
- 存放的元素
- 左节点
- 右节点
- 父节点
private static class Node<E> {
E element;
Node<E> left;
Node<E> right;
Node<E> parent;
public Node(E element, Node<E> parent) {
this.element = element;
this.parent = parent;
}
}
添加节点
例如对这棵树进行添加12和1的过程
步骤:
- 找到根节点
7,发现12比7大,找到右节点9 12比9大,找到右节点1112比11大,发现右节点为空- 创建新的节点,元素为
12,父节点是11,成为11的右子树
实现:
之前说过二叉搜索树中的元素必须具有可比较性,可以看到在添加时会有一次比较的操作compare(E e1, E e2),例子中Integer类型本身具有可比较性,如果是自定义类型呢?
元素比较方案的设计
假如现在加入二叉搜索树的元素类型为Person,比较规则为比较年龄,年龄大的元素为大
通过Comparable接口
创建一个接口,其中定义比较方法
public interface Comparable<E> {
int compareTo(E e);
}
那么只需让二叉搜索树的泛型来实现这个接口的方法即可,即:
public class BinarySearchTree<E extends Comparable<E>>
那么在compare(E e1, E e2)方法中让元素类来比较即可
private int compare(E e1, E e2) {
return e1.compareTo(e2);
}
但是这个方案的弊端在于比较的方法写死在元素类中,并不灵活。
外界传入比较器Comparator
public interface Comparator<E> {
int compare(E e1, E e2);
}
二叉搜索树中添加Comparator 属性,由外界传入比较器
public class BinarySearchTree<E> {
private int size;
private Node<E> root;
private Comparator<E> comparator;
public BinarySearchTree(Comparator<E> comparator) {
this.comparator = comparator;
}
}
那么在外部便可以自己定义比较规则
private static class PersonComparator1 implements Comparator<Person> {
*@Override*
public int compare(Person e1, Person e2) {
// **TODO** Auto-generated method stub
return e1.getAge() - e2.getAge();
}
}
总结
第二种方案看似灵活了,但是会强制外部一定要传入一个比较器,这样也不是很合理,那么比较好得方案便是对这两种方案进行结合,不用强制外部一定传入比较器,那么内部判断,如果外部有比较器就使用比较器,如果没有就强制元素实现Comparable接口
private int compare(E e1, E e2) {
if (comparator != null) {
return comparator.compare(e1, e2);
} else {
// 不在类的地方去强制,在这里强制
return ((Comparable<E>)e1).compareTo(e2);
}
}
二叉树的遍历
-
遍历是数据结构中常见的操作,即把所有的元素都访问一遍
-
线性数据结构的遍历分为:
正序遍历和逆序遍历 -
二叉树的常见遍历方式有四种:
1.
前序遍历(Preorder Traversal)2.
中序遍历(Inorder Traversal)3.
后序遍历(Postorder Traversal)4.
层序遍历(Level Order Traversal)
前序遍历(Preorder Traversal)
访问顺序:
-
根节点、前序遍历左子树、前序遍历右子树
-
7、4、2、1、3、5、9、8、11、10、12
中序遍历(Inorder Traversal)
访问顺序可以有两种:
a.
- 中序遍历左子树、根节点、中序遍历右子树
- 1、2、3、4、5、6、7、8、9、10、11、12
b.
- 中序遍历右子树、根节点、中序遍历左子树
- 12、11、10、9、8、7、6、5、4、3、2、1
结论:
二叉搜索树的中序遍历结果是升序或者降序的。
后序遍历(Postorder Traversal)
访问顺序:
- 后序遍历左子树、后序遍历右子树、根节点
- 1、3、2、5、4、8、10、12、11、9、7
层序遍历(Level Order Traversal)
层序遍历顾名思义就是从上至下一层一层的访问,与之前三种实现方式不同,并不方便使用递归
访问顺序:
- 从上至下,从左到右依次访问每一个节点
- 7、4、9、2、5、8、11、1、3、10、12
实现思路:
-
使用队列,首先将根节点入队
-
循环以下操作,直到队列为空
a. 将队头节点
A出队进行访问b.如果
A有左子节点,进行入队c.如果
A有右子节点,进行入队
设计对外开放的遍历接口
上面实现了四种遍历方式,但是可以发现我们对于访问节点的方式均为打印,这样就很不灵活,如果接口对外开放的话,考虑遍历到的元素应该暴露给使用者。
设计一个访问接口
计算二叉树的高度
对于计算二叉树的高度方法大致有两种:递归和迭代
递归
很容易想到树的高度可以看做是取其左右子树高度的最大值进行加1操作,从根节点一路向下,直到节点为空
迭代
使用层序遍历,在遍历完每一层后高度进行+1操作,现在问题在于如何判定对于一层的节点全部遍历完成?当每一层遍历完毕的时候,此时队列中存在的节点就是下一层所有的节点,这个时机可作为每层节点遍历完成的时机。
判断是否为完全二叉树
完全二叉树的特点:
- 对节点从上至下,从左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应
- 叶子节点只会出现在后两层,最后一层的叶子节点都靠左对齐
- 完全二叉树从根节点到倒数第二层是一棵满二叉树
- 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
思路:
-
如果树为空,返回
false -
如果树不为空,利用层序遍历
1.如果左子树不为空,将左节点入队
2.如果左子树为空,右子树不为空,返回
false3.如果右子树不为空,将右节点入队
4.如果右子树为空,不论左子树为不为空,之后遍历的节点都应该为叶子节点,否则返回
false
代码实现:
翻转二叉树
递归
这里前序遍历,访问节点时对该节点进行翻转,中序遍历和后序遍历同理,此处不再赘述。
迭代
可以想到使用层序遍历来操作
根据遍历结果重构二叉树
以下结果可以保证重构出唯一的一棵二叉树:
前序遍历+中序遍历后序遍历+中序遍历
若一棵树为
真二叉树则前序遍历+后序遍历重构的二叉树结果是唯一的,否则不唯一,因为无法分辨出左右子树
例如:
已知前序遍历:4、2、1、3、5、6,中序遍历:1、2、3、4、5、6
即可重构出唯一的二叉树:
删除节点
在操作删除之前需要了解前驱节点和后继节点
前驱节点
前驱节点:中序遍历时的前一个节点,若是二叉搜索树,前驱节点就是前一个比它小的节点
既然是找前一个比它小的节点,看看是不是有左子树
- 如果有
左子树,就从左子树中找最大的 - 如果没有左子树,那么前一个比它小的一定是
父节点、祖父节点...往上找,一直找到是左子树为止 - 如果既没有
左子树也没有父节点,那么这个节点没有前驱节点
后继节点
后继节点:中序遍历时的后一个节点,若是二叉搜索树,后继节点就是后一个比它大的节点
既然是找后一个比它大的节点,看看是不是有
右子树
- 如果有
右子树,就从右子树中找到最小的,例如图中:1、8、4 - 如果没有
右子树,如果父节点、祖父节点一路向上找一旦发现处于左子树的分支,就在这条线上找到比它大的,例如:7、6、3、11 - 如果没有
右子树,也没有父节点,那就没有后继节点实现思路基本等同于寻找前驱节点
根据被删除节点的情况可分为:叶子节点、度为1的节点、度为2的节点
删除节点为叶子节点
删除叶子节点很简单,直接删除即可,但要注意如果删除的是根节点的情况
删除节点是度为1的节点
用被删除节点的父节点指向被删除节点的子节点,注意左右子树,如果删除根节点记得分开处理
删除节点是度为2的节点
例如:删除5再删除4
- 先用
前驱节点或者后继节点的值覆盖原节点的值 - 删除相应的
前驱或者后继节点 - 如果一个节点的度为
2,则它的前驱或者后继节点度只能为0或者1
实现: