在数据结构中,对于有序数组来说查找很快,但是插入和删除慢,因为插入和删除需要先找到指定的位置,后面所有的元素都要移动一个位置,为插入腾出一个位置或填入删除的那个位置; 而对于链表来说,插入和删除快,但是查找很慢,插入和删除只要更改一下元素的引用值即可,而查找每次都要从头开始遍历直到找到目标元素为止。那么有没有一种数据结构能够同时具备查找、插入、删除都快的呢?所以有了树的诞生!
树结构有很多种,我们这里主要讲讲常见的二叉树。二叉树 :树的每个节点最多有两个字节点。如下图所示:
在上图中,A是根节点,B是A的左节点,C是A的右节点,BDE是A的左子树,CF是A的右子树。在二叉树中,有一种树叫二叉搜索树,本文主要讲解二叉搜索树的相关知识。二叉搜索树的要求是:如果一个节点存在左子树,那么左子树中的所有节点的值都比此节点的值小;同样,如果一个节点存在右子树,右子树中的所有节点的值都比此节点的值大。简单地说,就是二叉搜索树的左边的值由上到下不断变小,树的右边的值由上到下不断变大。二叉搜索树的结构图示例如下:
二叉搜索树作为一种数据结构,那么它的增删改查又是如何实现的?下面我们就来看看
首先我们定义二叉树节点的类如下:
public class Node {
private Object data; //节点数据
private Node leftChild; //左子节点的引用
private Node rightChild; //右子节点的引用
//打印节点内容
public void display() {
System.out.println(data);
}
public Node(Object data) {
this.data = 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);
//......
}
下面我们就基于上面的接口定义的方法来讲解一下,二叉搜索树是如何实现的?
1.查找节点
查找节点的算法:我们首先从根节点开始遍历,如果查找值比当前节点值小,便去左子树搜索,如果查找值比当前节点值大,便去右子树搜索,如果查找值等于当前节点值,则停止搜索。代码示例:
//查找节点
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.插入新节点
要插入节点,首先得找到待插入的位置。同样,我们还是从根节点开始比较,如果节点值大于插入值,则搜索左子树,如果节点值小于插入值,则搜索右子树,这样一直搜索,直到找到比插入值小的所有节点中最大的那个或者比插入值大的所有节点中的最小的那个节点,如果插入值比这个节点小,则新节点插入到它的左边,反之插入右边。代码示例如下:
//插入节点
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;
}
可以这样理解插入的算法,当我们拿插入值跟节点值比较的时候,只有两种情况,插入值要么比节点值大,要么就比节点值小,当然排除相等的情况。当这个节点左右子节点都不为空的时候,还不是插入的时候,只有当至少有一个子节点为null的时候,才可以插入。
3.遍历树
遍历树分为三种情况,前序遍历(根节点 --> 左节点 --> 右节点)、中序遍历(左节点 --> 根节点 --> 右节点)、后序遍历(左节点 --> 右节点 --> 根节点),我们主要看根节点的位置,左右节点的顺序都是由左到右。比如下面的二叉树:
结果:前序遍历(ABDGCFK)、中序遍历(DGBAFCK)、后序遍历(GDBKFCA)。总结一下,前序遍历是从根节点开始遍历,然后是左子树,然后是右子树;中序遍历是从左子树开始遍历,然后是根节点,然后是右子树;后序遍历是从左子树开始遍历,然后是右子树,然后是根节点。
代码示例:
//中序遍历
public void infixOrder(Node current){
if(current != null){
//这里利用了递归的算法,遍历左子树,直到左子树为null的时候跳出
infixOrder(current.leftChild);
System.out.print(current.data+" ");
//左子树跳出的时候,开始遍历右子树,右子树的遍历规则和上面一样,还是先遍历右子树中的左子树
//直到左子树为null的时候跳出
infixOrder(current.rightChild);
}
}
//前序遍历
public void preOrder(Node current){
if(current != null){
System.out.print(current.data+" ");
preOrder(current.leftChild);
preOrder(current.rightChild);
}
}
//后序遍历
public void postOrder(Node current){
if(current != null){
postOrder(current.leftChild);
postOrder(current.rightChild);
System.out.print(current.data+" ");
}
}
4.查找最大值和最小值
找最小值,先找根的左节点,再找左节点的左节点,直到找到一个节点没有左节点了,那么这个节点就是最小节点了。同理,找最大值是先找根节点的右节点,再找右节点的右节点,直到有一个节点没有右节点了,那么这个节点就是最大右节点了。代码示例:
//找到最大值
public Node findMax(){
//从根节点开始
Node current = root;
//记录最大节点
Node maxNode = current;
while(current != null){
maxNode = current;
current = current.rightChild;
}
return maxNode;
}
//找到最小值
public Node findMin(){
Node current = root;
Node minNode = current;
while(current != null){
minNode = current;
current = current.leftChild;
}
return minNode;
}
5.删除节点
删除节点的操作比较复杂一点,要分三种情况:
- 节点是一个叶节点(没有子节点)
- 节点只有一个子节点
- 节点有两个子节点,最复杂
下面我们分别来解析一下
5.1 删除节点是一个叶节点,这个简单,我们只要将父节点对该节点的引用置为null即可
public boolean delete(int key) {
//从根节点开始查找
Node current = root;
Node parent = root;
boolean isLeftChild = false;
//查找删除值,找不到直接返回false
while(current.data != key){
parent = current;
//如果要删的key小于当前节点值,那么就去左节点寻找,否则去右节点寻找
if(key < current.data){
//记录删除节点是父节点的左节点还是右节点,在后面要删除的时候才知道,是置父节点的左节点为null还是右节点为null
isLeftChild = true;
current = current.leftChild;
}else{
isLeftChild = false;
current = current.rightChild;
}
//查找到叶节点了,还是没有扎到匹配的节点,那么返回false
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;
}
5.2 删除有一个子节点的节点,我们只需要将父节点对删除节点的引用指向删除节点的子节点即可。代码示例如下
public boolean delete(int key) {
//从根节点开始查找
Node current = root;
Node parent = root;
boolean isLeftChild = false;
//查找删除值,找不到直接返回false
while(current.data != key){
parent = current;
//如果要删的key小于当前节点值,那么就去左节点寻找,否则去右节点寻找
if(key < current.data){
//记录删除节点是父节点的左节点还是右节点,在后面要删除的时候才知道,是置父节点的左节点为null还是右节点为null
isLeftChild = true;
current = current.leftChild;
}else{
isLeftChild = false;
current = current.rightChild;
}
//查找到叶节点了,还是没有扎到匹配的节点,那么返回false
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;
}
//删除节点有子节点
else{
//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;
}
return false;
}
5.3 删除节点有两个子节点,这个就比较复杂一点,图示如下
我们需要找个节点来替代被删除的节点, 根据二叉搜索树的排序规则,我们只要找到所有比6大的节点中最小的那个节点就是继承节点了。比如上图中的9就是继承节点,我们只需要将9替代6,那么此时也是符合搜索二叉树规则的。后继节点就是比删除节点大的最小节点。
算法:首先找到删除节点的右节点,如果该右节点还有左节点,顺着左节点往下找,直到找到那个没有左节点的节点就是继承节点了。
下面我们分两种情况讨论,一种是后继节点没有右节点,一种是后继节点还有右节点
① 后继节点是删除节点的右节点并且后继节点没有右节点,如上示例图职工,9是继承节点并且没有右节点,那么此时直接将9节点替代6节点即可。图示如下
② 后继节点不是删除节点的直接右节点,此时我们需要将后继节点父节点的对它的引用指向后继节点的右节点
//删除节点有两个子节点
public Node delNodeWithTwoNodes(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;
}
上面我们可以看到,对二叉树的操作都是需要从根节点开始往下一层一层查找的。从效率来说,不管查找还是删除都是比较高的。本文完整代码链接:https://github.com/jiusetian/DataStructureDemo/tree/master/app/src/main/java/binarytree