数据结构之二分搜索树(Binary Search Tree)
Posted 敲代码的人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之二分搜索树(Binary Search Tree)相关的知识,希望对你有一定的参考价值。
先了解二叉树
和链表一样,动态的数组结构
具有唯一的根节点
二叉树每个节点最多有两个孩子
每个节点最多有一个父亲
特性
二叉树不一定是满的
一个节点,甚至一个null,也是一个二叉树
具有天然的递归性,每一个孩子的左子树,和又子树也是一个二叉树
二分搜索树
二分搜索树也是一颗二叉树
二分搜索树每个节点的值,大于其左子树所有的值,小于其又子树所有的值
每一棵子树也是二分搜索树
这样存储的元素就具有可比较性
设计:
内部类节点的设计
private class Node {
private E e;
// 定义两个节点,左右孩子
private Node left, rigth;
public Node(E e) {
this.e = e;
left = null;
rigth = null;
}
}
基础框架及基础方法的搭建
//元素的E需要实现Compareable接口,
//因为需要可比较性
public class BST<E extends Comparable<E>> {
//内部节点类
//.................
private Node root;//定义二分搜索树的根节点
private int size;//定义元素的个数
public BST(){
root = null;
size = 0;
}
//获取元素的个数
public int getSize(){
return size;
}
//判断元素是否为空
public boolean isEmpty(){
return size == 0;
}
}
向二分搜索树中添加元素
思路分析:
如果二分搜索树为空,插入进来的节点作为根节点
如果不为空,如果小于根节点,和根节点的左子树比较,此时左子树可以看成一个新的二分搜索树,,也有根,和左右孩子,然后一层一层往下,知道找一个一个空的节点挂上
如果大于,往又子树去比较
遍历寻找空节点,就是自己的位置
注:这里忽略相同情况,如果是相同元素,不插入(可以设计插入做节点或者又节点中)
简单图示
//向二分搜索树中添加元素
public void add(E e){
//判断是否为空
if(isEmpty()){
//创建根节点
root = new Node(e);
}else{
//递归查找需要插入的位置
//传入一个子树的根节点,和要插入的元素
add(root,e);
}
}
//设计向以node为根的二分搜索树中插入元素E,递归算法
private void add(Node node,E e){
//递归的终止条件,
//1,小于正在比较的此二分搜索树根节点,且此根节点的左孩子为null
//2.大于正在比较的此二分搜索树根节点,且此根节点的又孩子为null
//先处理相同的情况
if(e.equals(node.e)){
return;
}else if(e.compareTo(node.e) < 0 && node.left == null){//1.
//创建节点,元素为e,挂上
node.left = new Node(e);
size++;
return;
}else if(e.compareTo(node.e) > 0 && node.rigth == null){
node.rigth = new Node(e);
size++;
return;
}
//一步一步处理递归逻辑,变成更小的逻辑
//如果此节点小于根节点,就去左孩子去查找
if(e.compareTo(node.e) < 0){
add(node.left,e);
}else {
add(node.rigth, e);
}
}
实现思路二
对于添加一个节点,不管左还是右,最终都是添加到null的位置
所以我们对于处理最小问题(递归终止条件),让这个二分搜索树node节点为空,就是插入位置
所以每递归一次,我们的左子树,或者右子树就有可能发生变化,所以需要接住发生变化的孩子
// 设计向以node为根的二分搜索树中插入元素E,递归算法
private void add(Node node, E e) {
// 递归的终止条件,
// 1,小于正在比较的此二分搜索树根节点,且此根节点的左孩子为null
// 2.大于正在比较的此二分搜索树根节点,且此根节点的又孩子为null
// 先处理相同的情况
if (e.equals(node.e)) {
return;
} else if (e.compareTo(node.e) < 0 && node.left == null) {// 1.
// 创建节点,元素为e,挂上
node.left = new Node(e);
size++;
return;
} else if (e.compareTo(node.e) > 0 && node.rigth == null) {
node.rigth = new Node(e);
size++;
return;
}
// 一步一步处理递归逻辑,变成更小的逻辑
// 如果此节点小于根节点,就去左孩子去查找
if (e.compareTo(node.e) < 0) {
add(node.left, e);
} else {
add(node.rigth, e);
}
}
public void add2(E e) {
root = add2(root, e);
}
private Node add2(Node node, E e) {
//递归终止条件传入节点为null
if(node == null){
//创建节点并返回
size++;
return new Node(e);
}
//处理递归函数
//去左子树,此时左子树会发生变化,需要接住
//但是根节点还是node
if(e.compareTo(node.e) < 0){
node.left = add2(node.left,e);
}else if(e.compareTo(node.e) > 0){
node.rigth = add2(node.rigth,e);
}
//最终返回的就是已node为根节点的二分搜索树
return node;
}
判断是否包含元素的节点
思路:
查看以node为节点的树,
终止条件,当递归后到最后node为空是,返回false
如果元素小于当前的节点,去左孩子找
大于,去又孩子找
等于,说明包含,直接返回true
private boolean contains(Node node, E e) {
// 如果传入的节点为空,说明递归到底了,
// 还是没有查到,所以返回false
if (node == null) {
return false;
}
if (e.compareTo(node.e) < 0) {
return contains(node.left, e);
} else if (e.compareTo(node.e) > 0) {
return contains(node.rigth, e);
} else {
return true;
}
}
前序遍历
先遍历根节点,再遍历左孩子,最后又孩子
//遍历已node为节点的二分搜索树
private void perOrder(Node node){
//递归终止
/* if(node == null){
return;
}
//为了方便直接打印
System.out.println(node.e);
perOrder(node.left);
perOrder(node.rigth);*/
//可以写成
if(node != null){
System.out.println(node.e);
perOrder(node.left);
perOrder(node.rigth);
}
}
图解,每个节点返回三次,蓝色点访问的时候取值
中序遍历
先访问左孩子,在访问节点,最后访问又子树
private void inOrder(Node node){
if(node == null){
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.rigth);
}
图解,三次返回的顺序和前序一样,蓝色点取值
后序遍历
先左后又,最后节点
public void postOrder(){
postOrder();
}
private void postOrder(Node node){
if(node == null){
return;
}
postOrder(node.left);
postOrder(node.rigth);
System.out.println(node.e);
}
图解,同理,蓝色点取值
前序遍历的非递归写法
思路:需要借助栈这种数据结构,
把要操作的当前节点压入栈
弹栈(就是遍历),就是刚才压入栈的节点,操作此节点
如果右节点不为空,压栈,左节点不为空,压栈(先进后出),总体来说弹一次,压入两次
再弹栈(遍历),操作弹出栈的节点
循环步骤,最后弹出栈中所有元素,遍历完成
图解:
public void perOrderNR(){
Stack<Node> stack = new Stack<>();
//先压入根节点
stack.push(root);
//如果栈不为空,弹出
if(!stack.isEmpty()){
Node cur = stack.pop();
System.out.println(cur.e);
//压栈,压入弹出节点的左右子树
//先压入左子树
if(cur.left != null){
stack.push(cur.left);
}
if(cur.rigth != null){
stack.push(cur.rigth);
}
}
}
层序遍历,一层一层,从左向又遍历
思路:借助队列
把根节点入队
把之前的入队的节点出队(遍历)
操作刚才出队的,如果左孩子不为空,入队,又孩子不为空入队(先进先出)
出队,循环出队与入队
当队列为空时,所有出队(遍历)完成
图解:
public void levelOrder(){
Queue<Node> queue = new LinkedList<>();
//根节点入队
queue.add(root);
//如果队列不为空,循环出队,与入队
if(!queue.isEmpty()){
//出队
Node cur = queue.poll();
System.out.println(cur.e);
//入队
if(cur.left != null){
queue.add(cur.left);
}
if(cur.rigth != null){
queue.add(cur.rigth);
}
}
}
删除最小值
思路分析
根据二分搜索树的定义,最小节点在左子树上
一路往左子树递归,当这个节点的左子树为空时,说明当前节点就是最小的
找到最小节点之后删除,并返回一个删除之后的新的节点
如果删除节点没有又孩子,返回节点就是空(也是一个节点),如果有又孩子,返回节点就是又孩子
把返回节点(新的二叉树),挂接到删除节点上一个节点的左子树上
图解
//寻找最小位置
public E minimun(){
//判断元素的个数
if(size == 0){
throw new IllegalArgumentException("BST is empty!");
}
return minimun(root).e;
}
//返回以node的为根的二分搜索树最小值所在的节点
private Node minimun(Node node){
//递归终止条件.节点的左孩子为空
if(node.left == null){
return node;
}
//处理递归函数
return minimun(node.left);
}
//删除最小值,并返回
public E removeMin(){
E ret = (E) minimun(root);
removeMin(root);
return ret;
}
//删除以node为根的二分搜索树中的最小节点
//返回删除节点后新的二分搜索树的根
private Node removeMin(Node node){
//递归终止条件,节点左孩子为空
if(node.left == null){
//先暂存一下这个删除节点的右孩子
Node rightNode = node.rigth;
size--;
return rightNode;
}
//处理递归函数
//没有到递归结束时,说明还有左孩子,需要继续递归
//递归一次,左孩子就会发生变化,需要重新接住左孩子
node.left = removeMin(node.left);
return node;
}
删除最大值.同理
public E maximun(){
if(size == 0){
throw new IllegalArgumentException("BST is empty!");
}
return maximun(root).e;
}
private Node maximun(Node node){
if(node.rigth == null){
return node;
}
return maximun(node.rigth);
}
public E removeMax(){
E ret = maximun();
removeMax(root);
return ret;
}
private Node removeMax(Node node){
if(node.rigth == null){
Node leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
node.rigth = removeMax(node.rigth);
return node;
}
删除任意位置
思路:如果节点只有右子树,或者只有左子树,逻辑和删除最大最小差不多
找到删除节点的后继(就是离删除节点最近,比这个节点所有左孩子都大,右孩子都小)
这个后继,就是右孩子的最小值
这个后继脱离,替换待删除节点,删除节点脱离整个二叉树
//从二分搜索树中删除元素为e的节点
public void remove(E e){
root = remove(root, e);
}
//删除以node为根二分搜索树中值为e的节点,递归算法
//返回删除节点后新的二分搜索树的根
private Node remove(Node node, E e){
//如果node为空,直接返回null
if(node == null){
return null;
}
//需要向左子树方向找
if(e.compareTo(node.e) < 0){
//一次函数调用之后,左子树会发生变化,需要接住新的左子树
node.left = remove(node.left, e);
return node;//返回根节点
}else if(e.compareTo(node.e) > 0){
node.rigth = remove(node.rigth, e);
return node;
} else{ //e == node.e
//删除位置的左孩子为空
if(node.left == null){
//逻辑通删除最小节点差不多
Node rightNode = node.rigth;
node.rigth = null;
size--;
return rightNode;
}
//删除位置的又孩子为空
if(node.rigth == null){
Node leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
//待删除节点左右都不为空
//找到比待删除节点最小的节点,及待删除节点又子树的最小节点
//使用minimun();返回就是上述所需要的节点
Node succeror = minimun(node.rigth);//所谓的后继S
//后继的又孩子,接上removeMin(node.right)这个返回新的二分搜索树的根
//顺带着node.right及succeror也脱离了
succeror.rigth = removeMin(node.rigth);
size++;//实际succeror指向着,所以需要++
//后继左子树指向待删除元素的左子树
succeror.left= node.left;
//把待删除节点脱离
node.left = node.rigth = null;
size--;
//返回删除后的节点
return succeror;
}
}
以上是关于数据结构之二分搜索树(Binary Search Tree)的主要内容,如果未能解决你的问题,请参考以下文章
[LeetCode] Closest Binary Search Tree Value 最近的二分搜索树的值
132.Find Mode in Binary Search Tree(二分搜索树的众数)
[LeetCode] Closest Binary Search Tree Value II 最近的二分搜索树的值之二
数据结构05红-黑树基础----二叉搜索树(Binary Search Tree)
Algorithms - Data Structure - Binary Search Tree - 数据结构之二叉搜索树