一、基本概念
二叉查找树(Binary Search Tree),它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
二、结构
如基本概念所述,二叉查找树有一个左子树,一个右子树,大于该节点的值则处于右边,小于该节点的值则处于左边。
如图:
二叉查找树只有两个节点,一个左一个右,且二叉查找树的的平均运行时间都是:O(lg n)。
三、代码(java实现)
3.1 结构定义
/**
* 二叉搜索树定义
*/
public class BinarySearchTree {
//根节点
private Node root;
/**
* 树节点定义,静态内部类,每个节点都是一颗子树
*/
private static class Node {
//数据
private int data;
//右节点
private Node right;
//左节点
private Node left;
//父节点,用于辅助作用
private Node parent;
public Node(int data, Node right, Node left, Node parent) {
this.data = data;
this.right = right;
this.left = left;
this.parent = parent;
}
}
}
节点也就是每一棵树,它有左节点和右节点,以及数据域,和一个父节点,父节点也就是用于辅助,因为增删改查都需要用到父亲。
二叉搜索树只有一个根节点,类似单链表,只需要一个头结点即可,因为头结点连接着所有的节点。
3.1 插入操作
insert方法:
public void insert(int data) {
//1.构造节点
Node node = new Node(data,null,null,null);
//2.判断根节点是否为空
if(root == null) {
//为空直接设置为根节点
root = node;
}else {
//设置当前节点为根节点
Node currentNode = root;
while (true) {
//如果值为当前节点值,则结束,不进行操作,因为已经存在该值。
if(data == currentNode.data) {
return;
}else if(data < currentNode.data) { //如果值小于当前节点值
//左节点是否为空
if(currentNode.left == null) {
//设置为左节点
currentNode.left = node;
break;
}else {
//当前节点设置为左节点,继续循环
currentNode = currentNode.left;
}
}else{ //如果值大于当前节点值
//右节点是否为空
if(currentNode.right == null) {
//设置为右节点
currentNode.right = node;
break;
}else {
//当前节点设置为右子节点,继续循环
currentNode = currentNode.right;
}
}
}
//设置父节点
node.parent = currentNode;
}
}
插入操作的逻辑也很简单,如图,当需要插入一个35时,从根节点开始判断,如果35大于根节点,即大于25,则判断25的右节点是否为空,如果为空,则设25的右节点为35,而25的右节点不为空,又从30开始判断,35大于30,则应该在右边,而30的右节点为空,所以35的位置则为30的右节点。
插入前:
插入后:
3.2 删除操作
二叉查找树的删除操作是比较复杂的,共分为三种情况,每一种情况的处理方式都不一样。
第一种:当删除的节点没有任何子节点(即叶子结点)。
如:要删除35时,35没有任何的左右节点,直接把其删除即可,把30的右节点设为null即可。
删除前:
删除后:
第二种:当删除的节点有一个子节点。
如:要删除30时,30存在一个右节点35,用节点35替代节点30既可。
删除前:
删除后:
第三种:当删除节点有两个子节点。
如:当要删除30时,30存在一个左节点28和一个右节点35,我们应该寻找节点30的左节点中最大的或者右节点中最小的来替代它,该节点也叫后继节点。则应该用29来替代30或用32来替代30,因为对于30节点的这棵树,30其实就是一个中间值,而左子树中最大的节点,它肯定比30的左节点28大,肯定比30的右节点35小;右子树中最小的节点,肯定也比30的右节点35小,30的左节点的28大,所以可以用来替换30。
我们转换思路,删除有2个子节点的节点时,我们的步骤应该是这样的,先找出后继节点存储值,然后删除后继节点,把后继节点的值设为原来要删除节点的值,即先找出32(本文使用右子树中的最小节点)存储起来,然后删除节点32,最后把节点30的值修改为32即可。
删除前:
删除后:
remove方法:
public boolean remove(int data) {
//1.找到要删除的节点
Node node = find(data);
//2.如果当前节点是null,则返回false,表示删除失败
if(node == null) {
return false;
}
//子节点数量
int nodeNum = hasNodeNum(node);
//3.判断子节点数量进行对应的删除
if(0 == nodeNum) {
deleteNoneNode(node);
}else if(1 == nodeNum) {
deleteHasOneNode(node);
}else {
deleteTwoNode(node);
}
return true;
}
find方法:
public Node find(int data) {
//从根节点开始找
Node currentNode = root;
while (currentNode != null) {
//1.如果值为当前节点,则表示找到
if(data == currentNode.data) {
return currentNode;
}else if(data < currentNode.data) { //2.如果值小于当前节点,则把左节点设为当前节点
currentNode = currentNode.left;
}else {//3.如果值大于当前节点,则把右节点设为当前节点
currentNode = currentNode.right;
}
}
return currentNode;
}
find方法则为从头开始向下找,找到要删除的节点则返回。
hasNodeNum方法:
private int hasNodeNum(Node node) {
if(node.left == null && node.right == null) {
return 0;
}else if(node.left == null || node.right == null) {
return 1;
}else {
return 2;
}
}
hasNodeNum方法判断该节点存在几个子节点。
deleteNoneNode方法:
private void deleteNoneNode(Node node) {
//如果是根节点,这把根节点设为null即可。
if(node == root) {
root = null;
}else { //不为根节点
//找到父节点
Node parent = node.parent;
//判断要删除的节点是左节点还是右节点
if(isRight(node)) {
parent.right = null;
}else {
parent.left = null;
}
}
}
deleteNoneNode方法即删除的节点不存在子节点的删除操作,逻辑如上,很简单。
isRight方法:
private boolean isRight(Node node) {
if (node == null || node.parent == null) {
return false;
}
return node == node.parent.right ? true : false;
}
isRight方法为判断一个节点是否是右节点。
deleteHasOneNode方法:
private void deleteHasOneNode(Node node) {
//如果为根节点
if(node == root) {
//使用不为null的那个节点替代根节点
root = (root.right == null) ? root.left : root.right;
}else { //不是根节点
Node parent = node.parent;
//如果是右节点
if(isRight(node)) {
//判断要删除的节点存在的是左节点还是右节点,用不为null的那个节点替代要删除的节点
parent.right = (node.right == null) ? node.left : node.right;
parent.right.parent = parent;
}else {
parent.left = (node.right == null) ? node.left : node.right;
parent.left.parent = parent;
}
}
}
deleteHasOneNode方法即删除的节点存在一个子节点的删除操作,逻辑如上,直接使用要删除节点下存在的那个节点替代要删除的节点即可。
deleteTwoNode方法:
private void deleteTwoNode(Node node) {
//找到右子树中最小的
Node min = findMin(node.right);
if(1 == hasNodeNum(min)) {
deleteHasOneNode(min);
}else {
deleteNoneNode(min);
}
//把替代节点的值设为原来要被删除节点的值
node.data = min.data;
}
deleteTwoNode方法即删除的节点存在两个子节点的删除操作,逻辑如上,找到后继节点(本文使用右节点中的最小节点),然后删除后继节点,最后再使用后继节点的值替代要删除节点的值即可。
注意:因为右节点中最小节点只保证找到的该节点没有左节点,并不保证不存在右节点,所以需要判断该后继节点存在几个子节点,然后进行对应的删除操作。
findMin方法:
private Node findMin(Node node) {
Node current = node;
while (current.left != null) {
current = current.left;
}
return current;
}
findMin方法寻找某个节点中最小的值,即找到左节点,如果左节点不存在,则当前节点值为最小。
到此为止,删除操作代码已经写完。
3.3 遍历操作
遍历主要分为三种,前序遍历、中序遍历、后序遍历。
第一种:前序遍历
前序遍历是对于每一棵树来说,先打印根节点,再打印左节点,最后打印右节点。
如图:
这是一个前序遍历的打印顺序,代码如下。
public void preOrder() {
preOrder(root);
}
private void preOrder(Node node) {
if(node != null) {
System.out.println(node.data);
preOrder(node.left);
preOrder(node.right);
}
}
第二种:中序遍历
中序遍历是对于每一棵树来说,先打印左节点,再打印根节点,最后打印右节点。
如图:
这是一个中序遍历的打印顺序,代码如下。
public void midOrder() {
midOrder(root);
}
private void midOrder(Node node) {
if(node != null) {
midOrder(node.left);
System.out.println(node.data);
midOrder(node.right);
}
}
第三种:后续遍历
后序遍历是对于每一棵树来说,先打印左节点,再打印右节点,最后打印根节点。
如图:
这是一个后序遍历的打印顺序,代码如下。
public void proOrder() {
proOrder(root);
}
private void proOrder(Node node) {
if(node != null) {
proOrder(node.left);
proOrder(node.right);
System.out.println(node.data);
}
}
对于以上三种遍历方式,通过看代码只需要记忆前序遍历即可,中序遍历是把根节点的打印放到中间,后续遍历是把根节点的打印放到最后。
总结:
以上内容就是整个二叉查找树的内容,经过测试代码都是OK的,如果发现问题,请留言,下一篇内容为二叉平衡树(也是java实现,其中包括插入、删除内容的详解讲解)