概述:
和链表一样,是一种动态数据结构,也是使用指针来实现,在Java中使用类来实现其数据结构。
二叉树包含了根节点,孩子节点,叶节点,每一个二叉树只有一个根节点,每一个结点最多只有两个节点,左子树的键值小于根的键值,右子树的键值大于根的键值;二叉搜索树也是一种二叉树,每一个颗二叉树的子树也是一颗二叉搜索树。
也可以定义重复元素的二叉搜索树,只要让左节点值小于等于其根节点或右节点大于等于其根节点即可。
如何实现:
首先,由于二叉搜索树的元素之间需要比较,所以需要实现Comparable接口,而且当比较元素不是基本类型时也可以定义自己的比较规则;可以和链表的实现学习,采用内部类的方式,内部类为节点类,存放存储的元素,和左右子树的根节点。
以下程序测试样例:
节点定义代码如下:
//内部类的方式实现,起到很好的封装
private class Node{
public E e;
public Node left;
public Node right;
public Node(E e){
this.e = e;
left = null;
right = null;
}
}
成员变量如下:
private Node root;
private int size;
//初始化二叉树,定义根节点为空,元素个数为零
public BST(){
root = null;
size = 0;
}
向二叉树中添加元素:
//向二分搜索树中添加元素
public void add(E e){
//对头结点进行判断,不为空就调用私有函数进行递归
if (root == null){
root = new Node(e);
size++;
}else
add1(root,e); //调用私有函数
}
//以node为根的二分搜索树中插入元素e,递归实现
private void add1(Node node, E e){
//元素相等的话,就直接返回,不做添加操作
if (node.e .equals(e))
return;
else if (e.compareTo(node.e) < 0 && node.left == null){ //对左子树为空,添加元素
node.left = new Node(e);
size++;
return;
}else if (e.compareTo(node.e) > 0 && node.right == null){ //对右子树为空,添加元素
node.right = new Node(e);
size++;
return;
}
//左右子树都不为空,递归调用函数
if (e.compareTo(node.e) < 0)
add1(node.left,e);
if (e.compareTo(node.e) > 0)
add1(node.right,e);
}
添加元素代码改进:
//向二分搜索树中添加元素
public void add(E e){
/*if (root == null){
root = new Node(e);
size++;
}else
add1(root,e);*/
add2(root,e);
}
//以node为根的二分搜索树中插入元素e,递归实现
//此处优化了add1()中的判断,首先不管左右孩子是否为空,都可以作为一个二叉树
//只不过空的就是空的二叉树,这时就可以一直判断,知道下一节点为空,就创建一个节点返回,
private Node add2(Node node, E e){
if (node == null){
size++;
return new Node(e);
}
//递归调用,已经有的元素不做操作
if (e.compareTo(node.e) < 0)
node.left = add2(node.left,e);
if (e.compareTo(node.e) > 0)
node.right = add2(node.right,e);
//返回最终以node为根节点的二叉树
return node;
}
检查二叉树中是否包含指定元素:
//递归查询
private boolean contains(Node node, E e){
//递归的终止条件,查询到叶子节点也没有该元素
if (node == null)
return false;
if (e.compareTo(node.e) == 0)
return true;
else if (e.compareTo(node.e) < 0){ //父节点不是要找的元素,继续向左子树查询
return contains(node.left,e);
}else //e.compareTo(node.e) > 0 //父节点不是要找的元素,继续向右子树查询
return contains(node.right,e);
}
前序,中序,后序遍历:
按照遍历顺序的不同把二叉树中的所有元素遍历一遍。
前序遍历是指根节点先遍历,然后在遍历左右子树;中序遍历是指根节点放中间,先遍历左子树,然后左子树,然后右子树;后序遍历是指先遍历左右子树,然后在遍历根节点。
前序遍历代码:
//前序遍历
public void preOrder(){
preOrder(root);
}
//递归遍历
private void preOrder(Node node){
if (node == null)
return;
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
中序遍历代码:
//中序遍历
public void inOrder(){
inOrder(root);
}
private void inOrder(Node node){
if (node == null)
return;
inOrder(node.left);
System.out.print(node.e + " ");
inOrder(node.right);
}
前序遍历非递归实现:
//非递归实现前序遍历
public void preOrderNR(){
Stack<Node> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
Node curr = stack.pop();
System.out.print(curr.e + " ");
//由于栈是后入先出,所以先入栈右节点,然后左节点,出栈时先左后右符合前序遍历
if (curr.right != null)
stack.push(curr.right);
if (curr.left != null)
stack.push(curr.left);
}
}
层序遍历(广度遍历):
//层序遍历
public void levelOrder(){
Queue<Node> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()){
//5出队,然后5的左右节点入队6->3,打印5
//3出队,然后入队3的左右节点2->4->6,打印3
//6出队,然后入队6的左右节点8->4->2,打印6
//队中元素是叶子节点,直接都出队:打印2 4 8
//最终打印结果为:5 3 6 2 4 8,
//其中一点遍历到叶子节点就不在遍历了
Node curr = queue.remove();
System.out.print(curr.e + " ");
if (curr.left != null)
queue.add(curr.left);
if (curr.right != null) {
queue.add(curr.right);
}
}
}
查询二分搜索树中最小元素:
//递归查询
public E minimum(){
if (size == 0)
throw new IllegalArgumentException("BST is Empty.");
return minimum(root).e;
}
//定义公共函数,方便其他函数调用
private Node minimum(Node node){
if (node.left == null)
return node;
return minimum(node.left);
}
查询二分搜索树中最大元素:
public E minimax(){
if (size == 0)
throw new IllegalArgumentException("BST is Empty.");
return minimax(root).e;
}
//递归查找最大元素
private Node minimax(Node node){
if (node.right == null)
return node;
return minimax(node.right);
}
删除最小元素:
由于在二分搜索树中,最小元素就在左子树中,一共有两种情况,如果左子树有左叶子节点那么最小元素就是它,如果没有那就是它的当前的父节点是最小元素。针对这两种情况,其实编码就是一种情况,不管哪种情况,都是不断遍历左子树一直遍历到其左子树不再有节点,然后将右子树的根节点替换其父节点,无论右子树是否为空或者有节点,都不影响,因为它都替换了最小元素,此时就是删除了最小元素。
public E removeMin(){
E min = minimum();
root = removeMin(root);
return min;
}
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;
}
删除最大元素:
同上述删除最小元素相似。
public E removeMax(){
E max = minimax();
root = removeMax(root);
return max;
}
private Node removeMax(Node node){
if (node.right == null){
Node leftNode= node.left; //将左子树保存下来
node.left = null;
size--;
return leftNode; //返回左子树,作为替换其父节点
}
node.right = removeMax(node.right);
return node;
}
删除任意元素:
对于任意元素,如果最小元素和最大元素不再有左节点和右节点,那么就直接删除即可,就是删除最小和最大元素操作。如果删除的元素还有左右子树,那么根据二叉树的规则,如果想删除此元素,那么代替该元素的只能是左子树中的最大元素或右子树中的最小元素,只有这样才能保证符合二叉搜索树的性质。我选择寻找右子树中的最小元素代替删除的元素,具体操作就是,先找出删除元素的右子树的最小元素保存下来,然后在删除这个最小元素,然后将保存的元素其左右子树指向被删除元素的左右子树即可,然后返回该元素,同时需要将被删除元素的左右子树置空。
public void removeEle(E e){
root = removeEle(root,e);
}
private Node removeEle(Node node, E e){
if (node == null)
return null;
if (e.compareTo(node.e) < 0){
node.left = removeEle(node.left,e);
return node;
}else if (e.compareTo(node.e) > 0){
node.right = removeEle(node.right,e);
return node;
}else {
//如果左子树为空,就把右子树返回,并删除原来指向的右子树
if (node.left == null){
Node delNode = node.right;
size--;
node.right = null;
return delNode;
}
if (node.right == null){
Node delNode = node.left;
size--;
node.left = null;
return delNode;
}
//左右子树都不为空时,采用Hibbard Deletion,通过找到右子树中最小元素来替代删除元素
//也可以找出左子树中的最大元素,替代要删除的元素
// 下面不需要显式写出size--,因为在removeMin(node.right)中已经减过了
Node tempNode = minimum(node.right);
tempNode.right = removeMin(node.right);
tempNode.left = node.left;
node.left = node.right = null;
return tempNode;
}
}