BST
一、什么是二叉查找树?
二叉查找树(BST)是一种数据结构同时具有二分查找和插入元素时不需要扩容的功能
BST的定义:
BST的特点:
1.有序性:BST的中序遍历是有序的
2.具有高效的查找和插入性能:
在查找和插入时类似二分查找高效
平均时间复杂度:O(logn)
最坏情况下时间复杂度是O(n):
下图表示BST被退化成链表,这也是为什么我们要对二叉查找树进行平衡操作的原因
二、手写二叉查找树
本篇文章使用java语言手写实现BST数据结构的增删改查操作。如果代码有误望大家指正。
1.BST类和TreeNode结点
定义BST类和TreeNode类:后面的功能实现基于BST类
public class BST<E extends Comparable<E>> {//支持泛型并且结点类型要具有可比较性所以extends Comparable类
//结点类型
private class TreeNode{
E data;
TreeNode left;
TreeNode right;
public TreeNode(E data) {
this.data = data;
}
}
//树的根节点
private TreeNode root;
//size表示节点个数
private int size;
public BST() {
this.root = null;
this.size = 0;
}
public int getSize() {
return size;
}
//判空
public boolean isEmpty(){
return size == 0;
}
}
2.插入
实现步骤:
1.如果root==null , root 就是新结点
2. 如果root != null , 定义辅助指针curr帮助我们查找新结点应该插入到哪个位置
2.1插入结点(版本1)
//版本1:容易理解但是if-else循环有点多,看着不舒服
public void add(E e){
if(root == null){
root = new TreeNode(e);
}else{
TreeNode curr = root;
//curr一直找要插入的位置,curr == null 时找到了,跳出循环
while(curr != null){
if(e.compareTo(curr.data) == 0){
return;//直接返回,不做任何事情
}else if (e.compareTo(curr.data) < 0){//新节点要插入到curr的左边部分
if(curr.left == null){//curr左子树为null
curr.left = new TreeNode(e);
size++;
return;
}else{//curr左子树不为null
curr = curr.left;//curr指向curr.left,接着进行查找
}
}else{//e.compareTo(curr.data) > 0
//新节点要插入到curr的右边部分
if(curr.right == null){//curr右子树为null
curr.right = new TreeNode(e);
size++;
return;
}else{//curr右子树不为null
curr = curr.right;//curr指向curr.right,接着进行查找
}
}
}
}
}
2.2插入结点(版本2)
//版本2:代码稍微优雅一点,少了一层if-else循环
public void add(E e){
if(root == null){
root = new TreeNode(e);
}else{
//curr用于查找插入到哪个位置
TreeNode curr = root;
while(curr != null){
if(e.compareTo(curr.data) == 0){
return;
}else if(e.compareTo(curr.data) < 0 && curr.left == null){
curr.left = new TreeNode(e);
size++;
return;
}else if(e.compareTo(curr.data) > 0 && curr.right == null){
curr.right = new TreeNode(e);
size++;
return;
}
if(e.compareTo(curr.data) < 0) curr = curr.left;//e.compareTo(curr.data) < 0 && curr.left != null
else curr = curr.right;//e.compareTo(curr.data) > 0 && curr.right != null
}
}
}
选择代码1或者代码2都可以。
3.查找
类似二分查找,比较简单
3.1查找任意结点
public boolean contains(E target){
return find(target) == null ? false : true;
}
public TreeNode find(E target){
//空树直接返回null
if(root == null) return null;
//辅助指针curr用于查找结点
TreeNode curr = root;
while (curr != null){
if(target.compareTo(curr.data) == 0){
return curr;//找到了
}else if(target.compareTo(curr.data) < 0){
curr = curr.left;
}else{
curr = curr.right;
}
}
//不存在该结点
return null;
}
//修改结点值
public void set(E src , E target){
if(contains(target)) return;
TreeNode srcNode = find(src);
srcNode.data = target;
}
3.2查找最小值结点
//查找最小结点
public E minValue(){
if(root == null) throw new RuntimeException("tree is null");
TreeNode min = root;
while(min.left != null){
min = min.left;
}
return min.data;
}
3.2查找最大值结点
//查找最大结点
public E maxValue(){
if(root == null) throw new RuntimeException("tree is null");
TreeNode max = root;
while(max.right != null){
max = max.right;
}
return max.data;
}
4.遍历
遍历操作直接套用二叉树的遍历方式
下面给出非递归版本(递归读者可以自己实现)
注:二叉树的遍历非递归写法是面试的重点,大家一定要理解和记住代码
4.1先序遍历
//前序遍历二叉树
public List<E> preOrder() {
List<E> res = new ArrayList<>();
if (root == null) return res;
// 1. 使用一个栈
Stack<TreeNode> stack = new Stack<>();
// 2. 将根节点压入栈中
stack.push(root);
// 3. 当栈不为空的时候,while循环
while (!stack.isEmpty()) {
// 3.1 取出栈顶结点
TreeNode curr = stack.pop();
// 3.2 处理弹出的结点
res.add(curr.data);
// 3.3 先将栈顶节点的右子节点压入栈中,再将左子节点压入栈中
// 压入栈的目的是为了下一次循环的处理
// 先压入右子节点的目的是先处理左子节点(栈有后进先出的特点)
if (curr.right != null) stack.push(curr.right);
if (curr.left != null) stack.push(curr.left);
}
return res;
}
4.2中序遍历
//中序遍历二叉树
public List<E> inOrder() {
ArrayList<E> res = new ArrayList<>();
if (root ==null) return res;
Stack<TreeNode> stack = new Stack<>();
TreeNode curr = root;
while (curr != null || !stack.isEmpty()) {
while (curr != null) {
stack.push(curr);
curr = curr.left;
}
TreeNode node = stack.pop();
res.add(node.data);
curr = node.right;
}
return res;
}
4.3后序遍历
//后序遍历
public List<E> postOrder() {
LinkedList res = new LinkedList<>();
if (root == null) return res;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode curr = stack.pop();
res.addFirst(curr.data);
if (curr.left != null) stack.push(curr.left);
if (curr.right != null) stack.push(curr.right);
}
return res;
}
4.4层序遍历
//层序遍历
public List<List<E>> levelOrder() {
List<List<E>> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
// 每轮循环遍历处理一层的节点
int size = queue.size();
List<E> levelNodes = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode curr = queue.poll();
levelNodes.add(curr.data);
// 将遍历后的节点的左右子节点入队,等到下一轮 while 循环的时候遍历处理
if (curr.left != null) queue.add(curr.left);
if (curr.right != null) queue.add(curr.right);
}
res.add(levelNodes);
}
return res;
}
5.删除
删除节点比较复杂:
要定义两个指针:
min指针用于找到最小值结点
parent指针用于找到最小值结点的父亲结点
5.1删除最小值结点
我们可能会认为只要找到最小的结点(min.left == null)
然后执行代码1即可
//代码1(错误!)
parent.left = null;
情况1:
情况2:不可以
如果执行parent.left = null,会删掉整个左子树
我们应该执行代码2
//代码2
parent.left = min.right;
min.right = null;
成功删除
注意:如果最小结点是根节点
我们执行 root = root.right 删除根节点。
public E removeMin(){
//空树没有最小值抛出异常
if(root == null){
throw new RuntimeException("tree is null");
}
//min指针用于指向找到最小值
TreeNode min = root;
//parent记录min指针的父结点
TreeNode parent = null;
//通过min.left 是否为null去找最小值节点
// min.left != null表示还没找到,接着找
while(min.left != null){
parent = min;
min = min.left;
}
//min指向的是最小值节点
if(parent == null){//表示min同时也是根节点,直接root=root.right
root = root.right;
}else{
//表示min不是根节点进行代码2操作
parent.left = min.right;
min.right = null;
}
size--;
return min.data;
}
5.2删除最大值结点
类似删除最小的结点,代码也是对称的
//类似
parent.right = max.left;
max.left = null;
同时如果要删除的最大结点是根节点
执行root = root.left
//代码和删除最小值结点思路一样,对称写就好了
public E removeMax(){
if(root == null){
throw new RuntimeException("tree is null");
}
TreeNode max = root;
TreeNode parent = null;
while(max.right != null){
parent = max;
max = max.right;
}
if(parent == null){
root = root.left;
}else{
parent.right = max.left;
max.left = null;
}
size--;
return max.data;
}
5.3删除任意结点
删除任意结点的前提是找到结点本身和结点的父亲结点
我们定义两个指针:curr指针和parent指针
curr用于找到该结点,parent为该结点的父亲结点
例如我们删除值为35的结点的前提如下图:
//代码1:找到要删除的结点
public void remove(E e){
if(root == null) return;
TreeNode curr = root;
TreeNode parent = null;
//找到要删除的结点
while(curr != null && e.compareTo(curr.data) != 0){
parent = curr;
if(e.compareTo(curr.data) < 0){
curr = curr.left;
}else{
curr = curr.right;
}
}
//跳出while循环表示:curr==null 或者找到了该结点
//如果没有找到要删除的结点,直接返回
if(curr == null) return;
}
此时我们找到了要删除的结点
但是待删除的结点有四种情况:
5.3.1要删除的结点是叶子结点
叶子结点(没有左右子树):
例如下图要删除值为15的结点(parent.left = curr)
我们执行:
parent.left = null;
图1
再例如我们要删除值为25的结点(parent.right = curr)
图2
parent.right = null;
代码补充:
//代码1:找到要删除的结点
public void remove(E e){
if(root == null) return;
TreeNode curr = root;
TreeNode parent = null;
//找到要删除的结点
while(curr != null && e.compareTo(curr.data) != 0){
parent = curr;
if(e.compareTo(curr.data) < 0){
curr = curr.left;
}else{
curr = curr.right;
}
}
//跳出while循环表示:curr==null 或者找到了该结点
//如果没有找到要删除的结点,直接返回
if(curr == null) return;
//下面是四种情况讨论:
if(curr.left == null && curr.right == null){//叶子结点
//同时要注意如果删除的是根节点,根节点也是叶子结点,我们直接让root 指向 null。否则下面else部分的代码会出现空指针异常。
if(parent == null){//删除根节点
root = null;
}else{
if(curr == parent.left){//图1
parent.left = null;
}else if(curr == parent.right){//图2
parent.right = null;
}
}
}else if(curr.left != null && curr.right == null){//只有左子树
}else if(curr.left == null && curr.right != null){//只有右子树
}else{//左右子树都有
}
}
5.3.2要删除的结点只有一个左子树
图1:删除值为22的结点(parent.left = curr)
我们执行
parent.left = curr.left;
curr.left = null;
图2:删除结点22的结点成功!
图3:删除值为66的结点(parent.right = curr)
我们执行
parent.right = curr.left;
curr.left = null;
图4:成功删除值为66的结点!
图5:要删除的是根节点(parent == null)
执行
root = root.left;
就好了!
所以删除只有左子树的结点代码如下
//只有左子树的else-if部分
else if(curr.left != null && curr.right == null){
if(parent == null){//删除根节点
root = root.left;
}else{
if(curr == parent.left){
parent.left = curr.left;
curr.left = null;
}else if(curr == parent.right){
parent.right = curr.left;
curr.left = null;
}
}
}
5.3.3要删除的结点只有一个右子树
图1:删除值为22的结点(parent.left = curr)
执行代码
parent.left = curr.right;
curr.right = null;
删除成功!
图2:如果要删除值为66的结点(parent.right = curr)
执行
parent.right = curr.right;
curr.right = null;
删除成功!
如果要删除的是根节点
执行
root = root.right;
代码汇总:
else if(curr.left == null && curr.right != null){//只有右子树
if(parent == null){
root = root.right;
}else if(curr == parent.left){
parent.left = curr.right;
curr.right = null;
}else if(curr == parent.right){
parent.right = curr.right;
curr.right = null;
}
}
5.3.4要删除的结点同时有左子树和右子树
例如要删除结点66,但是要保证删除后保持BST的性质
所以很麻烦!
我们要怎么做呢?
应该先寻找结点66的右子树上的最小结点(68)
因为68存在一个性质:68大于 结点66的左子树 的任意一个结点值,68同时小于等于结点66的右子树 的任意一个结点值。
我们也可以这么理解:我们使用中序遍历6。8结点是不是应该在66结点的后面一个。
即以中序遍历的方式,要删除的结点的右子树部分上的最小结点值在待删除结点的后一个!
下面给出图示:
步骤1:找到要删除的结点的右子树部分上的最小结点
同时要使用指针minParent指向min指针的父亲结点
步骤2:覆盖操作(66 变 68)
步骤3:删除多余的(68)
代码如下:
else if(curr.left != null && curr.right != null){//左右子树都有
//1.找到curr右子树的最小值结点
TreeNode min = curr.right;//min用于寻找右子树的最小值
TreeNode minParent = curr;//min的父亲结点
while(min.left != null){
minParent = min;
min = min.left;
}
//2.覆盖操作
curr.data = min.data;
//3.删除多余结点
minParent.left = null;
}
5.3.5删除结点代码汇总
是不是看着还挺头疼的!
//优化前:
public void remove(E e){
if(root == null) return;
TreeNode curr = root;
TreeNode parent = null;
//找到要删除的结点
while(curr != null && e.compareTo(curr.data) != 0){
parent = curr;
if(e.compareTo(curr.data) < 0){
curr = curr.left;
}else{
curr = curr.right;
}
}
//跳出while循环表示:curr==null 或者找到了该结点
//如果没有找到要删除的结点,直接返回
if(curr == null) return;
if(curr.left == null && curr.right == null){//叶子结点
if(parent == null){//删除根节点
root = null;
}else{
if(curr == parent.left){
parent.left = null;
}else if(curr == parent.right){
parent.right = null;
}
}
}else if(curr.left != null && curr.right == null){//只有左子树
if(parent == null){//删除根节点
root = root.left;
}else{
if(curr == parent.left){
parent.left = curr.left;
curr.left = null;
}else if(curr == parent.right){
parent.right = curr.left;
curr.left = null;
}
}
}else if(curr.left == null && curr.right != null){//只有右子树
if(parent == null){
root = root.right;
}else if(curr == parent.left){
parent.left = curr.right;
curr.right = null;
}else if(curr == parent.right){
parent.right = curr.right;
curr.right = null;
}
}else if(curr.left != null && curr.right != null){//左右子树都有
//1.找到curr右子树的最小值结点
TreeNode min = curr.right;//min用于寻找右子树的最小值
TreeNode minParent = curr;//min的父亲结点
while(min.left != null){
minParent = min;
min = min.left;
}
//2.覆盖操作
curr.data = min.data;
//3.删除多余结点
minParent.left = null;
}
}
//版本2:不好理解
public void remove(E e){
if(root == null) return;
TreeNode curr = root;
TreeNode parent = null;
//找到要删除的结点
while(curr != null && e.compareTo(curr.data) != 0){
parent = curr;
if(e.compareTo(curr.data) < 0){
curr = curr.left;
}else{
curr = curr.right;
}
}
//跳出while循环表示:curr==null 或者找到了该结点
//如果没有找到要删除的结点,直接返回
if(curr == null) return;
//我们把情况4(删除既有左子树又有左子树的结点)这个问题转化成情况1(删除叶子结点)
if(curr.left != null && curr.right != null){
//1.找到curr右子树的最小值结点
TreeNode min = curr.right;//min用于寻找右子树的最小值
TreeNode minParent = curr;//min的父亲结点
while(min.left != null){
minParent = min;
min = min.left;
}
//2.覆盖操作
curr.data = min.data;
//3.curr此时指向叶子结点,待删除中。。
curr = min;
parent = minParent;
}
TreeNode child;//用于存储要删除结点的子节点
if (curr.left != null){
child = curr.left;
if(parent != null) curr.left = null;
}else if(curr.right != null){
child = curr.right;
if(parent != null) curr.right = null;
}else{
child = null;
}
//现在我们的问题转化成了:
// 要删除的结点是 叶子结点 或者 只有一个子树
if(parent == null){
root = child;
}else if(curr == parent.left){
parent.left = child;
}else if(curr == parent.right){
parent.right = child;
}
}
6.修改(禁用)
我们直接调用查找方法:
//修改结点值
public void set(E src , E target){
if(contains(target)) return;
TreeNode srcNode = find(src);
srcNode.data = target;
}
public boolean contains(E target){
return find(target) == null ? false : true;
}
public TreeNode find(E target){
//空树直接返回null
if(root == null) return null;
//辅助指针curr用于查找结点
TreeNode curr = root;
while (curr != null){
if(target.compareTo(curr.data) == 0){
return curr;//找到了
}else if(target.compareTo(curr.data) < 0){
curr = curr.left;
}else{
curr = curr.right;
}
}
//不存在该结点
return null;
}
但是这个方法其实是有问题:它会破坏BST的结构
所以我们一般对BST数据结构不提供修改操作。
要修改可以把原来的删了然后添加新的结点。
7.递归写法
7.1插入元素
//递归版本
public void add(E e){
root = add(root , e);
}
//递归写法
//将结点e插入到以node为根节点的子树中
//要求插入结点后返回二叉查找树的根节点
private TreeNode add(TreeNode node , E e){
if(node == null){
size++;
return new TreeNode(e);
}
if(e.compareTo(node.data) < 0){
TreeNode leftRoot = add(node.left, e);
node.left = leftRoot;
}else{
TreeNode rightRoot = add(node.right , e);
node.right = rightRoot;
}
return node;
}
7.2查找元素
//递归版本
private TreeNode find(TreeNode node , E target){
if(node == null) return null;
if(target.compareTo(node.data) == 0) return node;
else if(target.compareTo(node.data) < 0) return find(node.left , target);
else return find(node.right, target);
}
7.3查找元素
//找任意值
private TreeNode find(TreeNode node , E target){
if(node == null) return null;
if(target.compareTo(node.data) == 0) return node;
else if(target.compareTo(node.data) < 0) return find(node.left , target);
else return find(node.right, target);
}
//找最小值
public E minValue(){
if(root == null) throw new RuntimeException("tree is null");
return minValue(root).data;
}
private TreeNode minValue(TreeNode node){
if(node.left == null) return node;
return minValue(node.left);
}
//找最大值
public E maxValue(){
if(root == null) throw new RuntimeException("tree is null");
return maxValue(root).data;
}
private TreeNode maxValue(TreeNode node){
if(node.left == null) return node;
return maxValue(node.right);
}
7.4删除元素
//删除最小值结点
public E removeMin(){
E res = minValue();
root = removeMin(root);
return res;
}
//删除以node为根节点的子树的最小结点
//返回删除完最小结点的子树的根节点
private TreeNode removeMin(TreeNode node){
if(node.left == null){
TreeNode rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
TreeNode leftRoot = removeMin(node.left);
node.left = leftRoot;
return node;
}
//删除最大值结点
public E removeMax(){
E res = maxValue();
root = removeMax(root);
return res;
}
public TreeNode removeMax(TreeNode node){
if(node.right == null){
TreeNode leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
TreeNode rightRoot = removeMax(node.right);
node.right = rightRoot;
return node;
}
删除任意节点递归版本
// 时间复杂度:O(logn)
public void remove(E e) {
root = remove(root, e);
}
// 在以 node 为根节点的子树中删除节点 e
// 并且返回删除后的子树的根节点
private TreeNode remove(TreeNode node, E e) {
if (node == null) return null;
if (e.compareTo(node.data) < 0) {
node.left = remove(node.left, e);
return node;
} else if (e.compareTo(node.data) > 0) {
node.right = remove(node.right, e);
return node;
} else {
// 要删除的节点就是 node
if (node.left == null) {
TreeNode rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
if (node.right == null) {
TreeNode leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
// node 的 left 和 right 都不为空
//successor是node的后继结点
TreeNode successor = minValue(node.right);
//1,2不能调换位置
successor.right = removeMin(node.right);//1
successor.left = node.left;//2
node.left = null;
node.right = null;
size--;
return successor;
}
}