什么是二叉树
在理解二分搜索树之前,我们先来看看二叉树是什么。二叉树是一种动态的数据结构,每个节点最多只有两个叉,也就是两个孩子节点,分别叫做左孩子,右孩子,而没有一个孩子的节点叫做叶子节点。每个节点最多有一个父亲节点,二叉树的起始节点,是唯一没有父亲节点的节点,叫做根节点。
二分搜索树
二叉搜索树,又称二叉查找树,二叉排序树,亦称二叉搜索树。
特点:
- 二分搜索树是一颗二叉树,满足二叉树的所有定义。
- 二分搜索树的每个节点的值,大于其左子树的所有节点的值;小于其右子树的所有节点的值。
- 每一颗子树也是二分搜索树。
- 存储的元素必须要有可比较性, Java中的话就要求二分搜索树保存的数据类型要实现Comparable接口或者使用额外的比较器实现
- 一般二分搜索树不包含重复元素, 当然也可以定义包含重复元素的二分搜索树。如果想要包含重复元素的话, 只需要定义二分搜索树的左节点的值小于等于当前节点的值或右节点的值大于等于当前节点即可。
- 二分搜索树天然的具有递归特性。
意义: 当我们看到二分搜索树的定义时,是否会去联系这样定义的意义何在呢?其实,二分搜索树是在给数据做整理,因为左右子树的值和根节点存在大小关系,所以在查找元素时,我们与根节点进行对比后,就能每次近乎一半的去除掉查找范围,这就大大的加快了我们的查询速度,插入元素时也是一样。
代码展示
在进行相关操作之前, 先定义一个支持泛型的节点类, 用于存储二分搜索树每个节点的信息, 这个类作为二分搜索树的一个内部类, 二分搜索树的类声明以及Node节点类声明如下:
package com.example.tree;
/**
* 递归实现二分搜索树
* 这里设计的树是不存储重复元素的, 重复添加元素只保存一个
* @param <E>
* @author hxl
*/
public class BSTree<E extends Comparable<E>> {
private class Node {
public E e;
public Node left,right;
public Node(E e) {
this.e = e;
this.left = null;
this.right = null;
}
}
private Node root;
private int size;
public BSTree(){
root = null;
size = 0;
}
public int size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
}
添加元素
由于二分搜索树本身的递归特性, 所以可以很方便的使用递归实现向二分搜索树中添加元素, 步骤如下:
- 定义一个公共的add方法, 用于添加元素
- 定义一个递归的add方法用于实际向二分搜索树中添加元素
/**
* 向二分搜索树中添加新的元素e
* @param e
*/
public void add(E e){
root = add(root,e);
}
/**
* 向以node为根的二分搜索树中插入元素e,递归算法
* @param node
* @param e
* @return 返回插入新节点后二分搜索树的根
*/
private Node add(Node node,E e){
if(null == node){
size++;
return new Node(e);
}
if(e.compareTo(node.e) < 0){
node.left = add(node.left,e);
}else if(e.compareTo(node.e) > 0){
node.right = add(node.right,e);
}
return node;
}
查询元素
由于二分搜索树没有下标, 所以针对二分搜索树的查找操作, 这里定义一个contains方法, 查看二分搜索树是否包含某个元素, 返回一个布尔型变量, 这个查找的操作一样是一个递归的过程, 具体代码实现如下:
/**
* 判断树中是否包含元素e
* @param e
* @return
*/
public boolean contains(E e) {
return contains(root, e) ;
}
/**
* 判断以node为根节点的二分搜索树中是否包含元素e,递归算法
* @param node
* @param e
* @return
*/
private boolean contains(Node node, E e) {
if (null == node) {
return false ;
}
// 递归终结条件
if (node.e.compareTo(e) == 0) {
return true;
} else if (node.e.compareTo(e) > 0) {
return contains(node.left, e) ;
} else {
return contains(node.right, e) ;
}
}
遍历元素
遍历分类:
深度优先遍历
- 前序遍历 : 对当前节点的遍历在对左右孩子节点的遍历之前, 遍历顺序 : 当前节点->左孩子->右孩子
- 中序遍历 : 对当前节点的遍历在对左右孩子节点的遍历中间, 遍历顺序 : 左孩子->当前节点->右孩子
- 后序遍历 : 对当前节点的遍历在对左右孩子节点的遍历之后, 遍历顺序 : 左孩子->右孩子->当前节点
广度优先遍历
- 层序遍历 : 按层从左到右进行遍历
前序遍历
递归写法
/**
* 前序遍历
*/
public void preOrder() {
preOrder(root);
}
private void preOrder(Node node) {
if (null == node) {
return ;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
非递归写法
/**
* 这里使用栈进行辅助实现
*/
public void preOrderNr() {
// 使用栈辅助实现前序遍历
Stack<Node> stack = new Stack<>();
/*
* 前序遍历的过程就是先访问当前节点, 然后再访问左子树, 然后右子树
* 所以使用栈实现时, 可以先将当前节点入栈, 当前节点出栈时,
* 分别将当前节点的右孩子, 左孩子压入栈
*/
stack.push(root);
while (!stack.isEmpty()) {
Node cur = stack.pop();
// 前序遍历当前节点
System.out.println(cur.e) ;
// 由于栈是后入先出, 前序遍历是先左孩子, 再右孩子, 所以这里需要先将右孩子压入栈
if (cur.right != null) {
stack.push(cur.right);
}
if (cur.left != null) {
stack.push(cur.left);
}
}
中序遍历
二分搜索树的中序遍历的结果是顺序的
/**
* 中序遍历
*/
public void inOrder() {
inOrder(root) ;
}
private void inOrder(Node node) {
if (null == node) {
return ;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
后序遍历
/**
* 后序遍历
*/
public void postOrder(){
postOrder(root);
}
private void postOrder(Node node) {
if (null == node) {
return ;
}
postOrder(node.left) ;
postOrder(node.right) ;
System.out.println(node.e) ;
}
层序遍历
借助队列进行实现
/**
* 层序遍历, 从左到右一层一层遍历
* 借助队列实现
*/
public void levelOrder(){
Queue<Node> queue = new LinkedList<Node>();
queue.add(root);
while(!queue.isEmpty()){
Node cur = queue.poll();
System.out.println(cur.e);
if (cur.left != null) {
queue.add(cur.left);
}
if (cur.right != null) {
queue.add(cur.right);
}
}
}
删除元素
删除最小值
/**
* 查找树中最小元素
* @return
*/
public E minimum(){
if (size == 0) {
throw new IllegalArgumentException("BSTree is empty");
}
return minimum(root).e;
}
/**
* 查找以node为根节点的最小元素, 递归方法
* @param node
* @return
*/
private Node minimum(Node node){
if (node.left == null) {
return node;
}
return minimum(node.left);
}
/**
* 删除二分搜索树中的最小值
* @return
*/
public E removeMin(){
E ret = minimum();
root = removeMin(root);
return ret;
}
/**
* 删除以node为根节点的树的最小值
* @param node
* @return 返回删除后的新的二分搜索树的根
*/
private Node removeMin(Node node){
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
删除最大值
/**
* 查找树中最大元素
* @return
*/
public E maximum(){
if (size == 0) {
throw new IllegalArgumentException("BSTree is empty");
}
return maximum(root).e;
}
/**
* 查找以node为根节点的最大元素, 递归方法
* @param node
* @return
*/
private Node maximum(Node node){
if (node.right == null) {
return node;
}
return maximum(node.right);
}
/**
* 删除二分搜索树中的最大值
* @return
*/
public E removeMax(){
E ret = maximum();
root = removeMax(root);
return ret;
}
/**
* 删除以node为根节点的树的最大值
* @param node
* @return 返回删除后的新的二分搜索树的根
*/
private Node removeMax(Node node){
if(node.right == null){
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
node.right = removeMin(node.right);
return node;
}
删除任意节点
删除任意节点可以分为以下几种情况 :
- 删除叶子节点, 直接删除即可。
- 删除只有右子树的节点, 逻辑同删除最小值, 虽然这个节点不一定是最小值, 但是删除逻辑是一样的。
- 删除只有左子树的节点, 逻辑同删除最大值。
- 删除同时具有左右子树的节点, 这个时候删除节点的步骤稍微复杂一些
- 首先找到要删除的节点
- 然后找到对应节点的前驱或者后继节点, 前驱就是指当前节点的左子树中最大的元素节点, 后继就是指当前节点右子树中最小的元素节点, 下图就是基于后继节点的删除演示
- 使用后继节点替换当前节点, 然后再删除要删除的节点
/**
* 删除指定元素e所在的节点
* @param e
*/
public void remove(E e){
root = remove(root, e);
}
/**
* 删除以node为根节点中的二分搜索树中
* @param node
* @param e
* @return 返回删除后的新二分搜索树的根节点
*/
private Node remove(Node node, E e){
if (node == null) {
return null;
}
if (node.e.compareTo(e) > 0) {
node.left =remove(node.left, e);
return node;
} else if (node.e.compareTo(e) < 0) {
node.right = remove(node.right, e);
return node;
} else {
// 待删除节点左子树为空的情况
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
// 待删除节点右子树为空的情况
if (node.right == null) {
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
// 待删除节点左右子树均不为空的情况
// 查找待删除节点的后继节点
// 用后继节点替换当前待删除节点
// 查找后继节点, 从待删除节点的右子树,查找最小值
Node successor = minimum(node.right);
/*
* 需要注意的是, 这里removeMin中进行了size--操作,
* 但是实际上最小的元素变成了successor, 并没有删除
* 所以按照逻辑的话, 这里应该有一个size++
* 但是后面删除了元素之后,需要再进行一次size--, 所以这里就不对size进行操作了
*/
successor.right = removeMin(node.right);
successor.left = node.left;
// 后继节点完成替换, 删除当前节点
node.left = node.right = null;
return successor;
}
}