概念
二叉搜索树(Binary Search Tree,BST),是指一棵空树或者具有下列性质的二叉树:
- 某节点的左子树节点值仅包含小于该节点值
- 某节点的右子树节点值仅包含大于该节点值
- 左右子树每个也必须是二叉查找树
二叉查找树有一个根节点,且每个节点下最多有只能有两个子节点,左子节点的值小于其父节点,右子节点的值大于其父节点。
可以自己创建二叉树感受一下:在线演示BST网址
参考:
Java数据结构和算法(十)——二叉树 、数据结构之红黑树-动图演示(上)
创建
节点类:
public class Node {
int data; //节点数据
Node leftChild; //左子节点的引用
Node rightChild; //右子节点的引用
boolean isDelete;//表示节点是否被删除
public Node(int data){
this.data = data;
}
//打印节点内容
public void display(){
System.out.println(data);
}
}
具体方法:
public interface Tree {
//查找节点
public Node find(int key);
//插入新节点
public boolean insert(int data);
//中序遍历
public void infixOrder(Node current);
//前序遍历
public void preOrder(Node current);
//后序遍历
public void postOrder(Node current);
//查找最大值
public Node findMax();
//查找最小值
public Node findMin();
//删除节点
public boolean delete(int key);
}
查找
查找某个节点,必须从根节点开始遍历:
-
查找值比当前节点值大,则搜索右子树;
-
查找值等于当前节点值,停止搜索(终止条件);
-
查找值小于当前节点值,则搜索左子树;
@Override
public Node find(int key) {
Node current = root;
while(current != null){
if(current.data > key) { //当前值比查找值大
current = current.leftChild; //搜索左子树
}else if(current.data < key){ //当前值比查找值小
current = current.rightChild; //搜索右子树
}else {
return current;
}
}
return null; //遍历完整个树没找到,返回null
}
树的效率:查找节点的时间取决于这个节点所在的层数,每一层最多有 2 n − 1 2^{n-1} 2n−1个节点,总共l层,共有 2 n − 1 2^n-1 2n−1个节点,那么时间复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)
插入
@Override
public boolean insert(int data) {
Node newNode = new Node(data);
if(root == null) { //当前树为空时
root = newNode;
return true;
}else {
Node current = root;
Node parentNode = null;
while(current != null) {
parentNode = current;
if(current.data > data) { //当前值比插入值大,搜索左子节点
current = current.leftChild;
if(current == null) { //子节点为空,直接将新值插入到该节点
parentNode.leftChild = newNode;
return true;
}
}else {
current = current.rightChild;
if(current == null) { //子节点为空,直接将新值插入到该节点
parentNode.rightChild = newNode;
return true;
}
}
}
}
return false;
}
删除
删除节点首先要查找要删除的节点,找到后执行删除操作。
删除节点的节点有如下几种情况:
- 删除的节点没有子节点
- 删除的节点有一个子节点
- 删除的节点有两个子节点
1 删除的节点没有子节点
在没有子节点的情况,其父节点指向空,要删除的节点依然存在,但是它已经不是树的一部分了,由于Java语言的垃圾回收机制,我们不需要非得把节点本身删掉,一旦Java意识到程序不在与该节点有关联,就会自动把它清理出存储器。
如图:删除节点[70]
public boolean delete(int key) {
Node current = root;
Node parent = root;
boolean isLeftChild = false;
while(current.data != key){ //查找删除值,找不到直接返回false
parent = current;
if(current.data > key){
isLeftChild = true;
current = current.leftChild;
}else{
isLeftChild = false;
current = current.rightChild;
}
if(current == null){
return false;
}
}
//如果当前节点没有子节点
if(current.leftChild == null && current.rightChild == null){
if(current == root){
root = null;
}else if(isLeftChild){
parent.leftChild = null;
}else{
parent.rightChild = null;
}
return true;
}
return false;
}
//当前节点有一个子节点,左子节点
else if(current.leftChild != null && current.rightChild == null){
if(current == root){
root = current.leftChild;
}else if(isLeftChild){
parent.leftChild = current.leftChild;
}else{
parent.rightChild = current.leftChild;
}
return true;
2 删除的节点有一个子节点
有一个子节点的情况下,将其父节点指向其子节点,然后删除该节点。
如图:删除节点[200]
//当前节点有一个子节点,左子节点
else if(current.leftChild != null && current.rightChild == null){
if(current == root){
root = current.leftChild;
}else if(isLeftChild){
parent.leftChild = current.leftChild;
}else{
parent.rightChild = current.leftChild;
}
return true;
3 删除的节点有两个子节点
该种情况下,涉及到节点的“位置变换”,用右子树中的最小节点替换当前节点。从右子树一直 left 到 NULL。最后会被转换为前两种情况。
所以对于删除有两个孩子的节点,删除的是其右子树的最小节点,最小节点的内容会替换要删除节点的内容。
如图:删除节点[50]
else{
//当前节点存在两个子节点
Node successor = getSuccessor(current);
if(current == root){
root= successor;
}else if(isLeftChild){
parent.leftChild = successor;
}else{
parent.rightChild = successor;
}
successor.leftChild = current.leftChild;
}
public Node getSuccessor(Node delNode){
Node successorParent = delNode;
Node successor = delNode;
Node current = delNode.rightChild;
while(current != null){
successorParent = successor;
successor = current;
current = current.leftChild;
}
//后继节点不是删除节点的右子节点,将后继节点替换删除节点
if(successor != delNode.rightChild){
successorParent.leftChild = successor.rightChild;
successor.rightChild = delNode.rightChild;
}
return successor;
}
遍历
遍历树是根据一种特定的顺序访问树的每一个节点。比较常用的有前序遍历,中序遍历和后序遍历。而二叉搜索树最常用的是中序遍历。
-
中序遍历: 左子树——》根节点——》右子树
-
前序遍历: 根节点——》左子树——》右子树
-
后序遍历: 左子树——》右子树——》根节点
//中序遍历
public void infixOrder(Node current){
if(current != null){
infixOrder(current.leftChild);
System.out.print(current.data+" ");
infixOrder(current.rightChild);
}
}
//前序遍历
public void preOrder(Node current){
if(current != null){
System.out.print(current.data+" ");
infixOrder(current.leftChild);
infixOrder(current.rightChild);
}
}
//后序遍历
public void postOrder(Node current){
if(current != null){
infixOrder(current.leftChild);
infixOrder(current.rightChild);
System.out.print(current.data+" ");
}
}
测试:
public static void main(String[] args) {
BinaryTree bt = new BinaryTree();
bt.insert(50);
bt.insert(20);
bt.insert(80);
bt.insert(10);
bt.insert(30);
bt.insert(60);
bt.insert(90);
bt.insert(25);
bt.insert(85);
bt.insert(100);
bt.delete(10);//删除没有子节点的节点
bt.delete(30);//删除有一个子节点的节点
bt.delete(80);//删除有两个子节点的节点
System.out.println(bt.findMax().data);
System.out.println(bt.findMin().data);
System.out.println(bt.find(100));
System.out.println(bt.find(200));
}
结果:
该树的优缺点
优:
有序数组删除或插入数据较慢(向数组中插入数据时,涉及到插入位置前后数据移动的操作),但根据索引查找数据很快,可以快速定位到数据,适合查询。而链表正好相反,查找数据比较慢,插入或删除数据较快,只需要引用移动下就可以,适合增删。
而二叉树就是同时具有以上优势的数据结构。
缺:
上面的树是非平衡树,由于插入数据顺序原因,多个节点可能会倾向根的一侧。极限情况下所有元素都在一侧,此时就变成了一个相当于链表的结构。
比如:依次插入节点[100,150,170,300,450,520 …]
这种不平衡将会使树的层级增多(树的高度增加),查找或插入元素效率变低。
那么只要当插入元素或删除元素时还能维持树的平衡,使元素不至于向一端严重倾斜,就可以避免这个问题。
到此,红黑树闪亮登场, 红黑树就是一种平衡二叉树。
后续:
红黑树 —— 算法学习笔记(三)