定义
二叉查找树是以二叉树组织的,可以采用一个链表数据结构表示,其每一个节点为一个对象,包含left指针指向左孩子,right指针指向右孩子,parent指针指向父节点。
在二叉搜索树中:
- 若任意节点的左子树不为空,则左子树上所有节点的值均小于根节点。
- 若任意节点的右子树不为空,则右子树上所有节点的值均大于根节点。
- 任意节点的左,右子树也分别为二查搜索树。
- 没有键值相等的节点
代码实现
1. 二叉查找树的节点(这里是采用内部类实现)
private class Node{
private int key;
private Node left;
private Node right;
private Node father;
public Node(int key){
this.key = key;
}
public Node(){}
}
2. 二叉查找树的基本操作
对于二叉搜索树来说,它能够支持许多动态集合操作,包括遍历,查找,最小关键字,最大关键字,后继,前驱,插入,删除等。
2.1 插入 INSERT
在插入一个新的节点后,新的节点总是作为一个叶子节点而存在的,这是二叉查找树的重要特性。
由于是根节点root开始向下遍历,且新的节点作为叶子节点存在,所以其插入时间复杂度为O(h),h是二叉查找树的高度。
2.1.1 代码实现
在insert时,主要通过parent,current记录当前插入节点的位置,从root迭代向下寻找插入位置。
public void insert(int key){
Node node = new Node();//创建一个node节点用于存储插入数据key
node.key = key;
if(root == null){
root = node;//若根节点为空,那么该节点为根节点
return;
}
Node parent = new Node();
Node current = root;
while (true){
parent = current;
if(key>current.key){
current = current.right;//右子树
if(current == null){
parent.right = node;
node.father = parent;
return;
}
}else {
current = current.left;
if(current == null){
parent.left = node;
node.father = parent;
return;
}
}
}
}
2.2 遍历 WALK
2.2.1 中序遍历 INORDER-TREE-WALK
遍历顺序:左节点--->父节点--->右节点
对下图作中序遍历,结果为1,3,6,7,8,9
可以发现中序遍历是一个有序的结果,这也是二叉查找树的重要特性。
代码实现
public void inOrder(Node root){
if(root != null){
inOrder(root.left);
System.out.print(root.data+" ");
inOrder(root.right);
}
}
2.2.2 前序遍历 PREORDER-TREE-WALK
遍历顺序:父节点--->左节点--->右节点
对下图作前序遍历,结果为6,3,1,8,7,9
代码实现
public void preOrder(Node root){
if (root != null) {
System.out.print(root.data + " ");
preOrder(root.left);
preOrder(root.right);
}
}
2.2.3 后序遍历 POSTORDER-TREE-WALK
遍历顺序:左节点--->右节点--->父节点
对下图作后序遍历,结果为1,3,7,9,8,6
代码实现
public void postOrder(Node root){
if (root != null) {
postOrder(root.left);
postOrder(root.right);
System.out.print(root.data + " ");
}
}
2.3 查找 search
按照关键值查找,从根节点root开始向下迭代。在最坏情况下,key从root开始直到叶子节点都没有匹配到或是在叶子节点才匹配到,该情况的比较节点个数等于二叉查找树的高度H,因此二叉查找树的时间复杂度为O(h)。
- 如果二叉查找树是平衡的,则n个节点的二叉查找树的高度为log2(n+1),其查找效率为O(log2n),近似于折半查找。
- 如果二叉查找树完全不平衡,则n个节点的二叉查找树的高度为n,查找效率为O(n),退化为顺序查找。
一般的,二叉查找树的查找性能在O(log2n)到O(n)之间。因此,为了获得较好的查找性能,就要构造一棵平衡的二叉查找树。
代码实现
public Node search(int key){
Node current = root;
while (current.data != key){
if(key > current.data){//在右子树查找
current = current.right;
}else {
current = current.left;
}
if(current == null){
return null;
}
}
return current;
}
2.4 最大值和最小值 MAX&MIN
最大值最小值的实现思想差不太多,按照二叉查找树的特性,最大值返回最右边的叶子节点,最小值返回最左边的叶子节点。
2.4.1 最大值
public Node maxNode(Node root){
Node current = root;
while (current.right != null){
current = current.right;
}
return current;
}
2.4.2 最小值
public Node minNode(Node root){
Node current = root;
while (current.left != null){
current = current.left;
}
return current;
}
2.5 前驱和后继 Predecessor&Successor
以一个Node节点为例
1. 前驱节点PreNode就是:所有key值小于Node.key的节点中,key最大的那个。
2. 后继节点SucNode就是:所有key值大于Node.key的节点中,key最小的那个。
例如:下图中6的前驱节点为3,6的后继节点为7
2.5.1 前驱结点 Predecessor
根据二叉查找树 左节点<父节点<右节点的特性,可以得出求前驱结点的过程:
①若节点Node的左子树非空,那么前驱就是其左子树中的最大key
②若节点Node的左子树为空,那么有两种可能
a. 节点Node是其父节点的右孩子,则其前驱为父节点
b. 节点Node是其父节点的左孩子 ,则其前驱为父节点的最底层祖先,同时满足这个最底层祖先的右孩子是节点Node的祖先
(例如7是8的左孩子,前驱是以8作为右孩子的先祖节点6。)
代码实现
public Node getPredecessor(int key){
Node node = search(key);
if(root == null){
return null;
}
//如果node的左子树不为空:直接在其中寻找最大值返回即可
if (node.left != null){
return maxNode(node.left);
}
//若node没有左子树,则有两种可能:
//1.node为右子树,则前驱就是父节点
//2.node为左子树,则去node的父节点继续寻找,
else {
Node parent = node.father;
while (parent != null && node == parent.left){//若为左子树且parent!=NULL
node = parent;//将parent赋给node
parent = node.father;//parent节点的parent节点
}
return parent;
}
}
2.5.2 后继节点 Successor
后继节点实现同理前驱节点,其过程如下:
①若节点Node的右子树非空,那么后继就是其右子树中的最小key
②若节点Node的左子树为空,那么有两种可能:
a. 节点Node是父节点的左孩子,那么后继就是它的父节点
b. 节点Node是父节点的右孩子,那么后继就是Node的最底层祖先,同时满足这个最底层祖先的左孩子是节点Node的祖先
代码实现:
public Node getSuccessor(int key){
Node node = search(key);
if(root == null){
return null;
}
if(node.right != null){
return minNode(node.right);
}
//若node没有右子树,则有两种可能:
//1. node为左子树,则后继就是父节点
//2. node为右子树,则去node的父节点继续寻找
else{
Node parent = node.father;
while (parent!= null && node == parent.right){
node = parent;
parent = node.father;
}
return parent;
}
}
2.4 删除节点 Delete
删除节点是二叉查找树中最复杂的操作,分以下几种情况:
2.4.1 要删除的current节点为叶子节点
只需要将parent.left或parent.right设置为null,Java垃圾回收机制会自动删除current节点。
2.4.2 要删除的current节点有一个子节点
只需要将parent.left或parent.right设置为current.left或current.right
2.4.3 要删除的节点有两个子节点
该情况比较复杂,如果将一颗二叉树按照中序遍历的方式输出,则任意节点的下一个节点就是该节点的后继节点。此时情况分两种:
a. 后继节点为待删除节点的右孩子:
只需要将current用后继节点替换就好,注意处理好current.left和successor.right。
b. 后继节点为待删除节点的右孩子的左子树
那么步骤为:
1. successorParent.left = successor.right
2. successor.right = current.right
3. parent.left = successor
代码实现:
private Node getDelSucNode(Node delNode){
Node successor = getSuccessor(delNode.key);//后继节点
Node successorParent = successor.father;//后继节点的parent
//如果后继节点为删除节点的右子树的左孩子 需要预先调整删除节点的右子树
if(successor != delNode.right){
successorParent.left = successor.right;
successor.right = delNode.right;
}
return successor;
}
public boolean delete(int key){
Node current = root;
Node parent = new Node();
boolean isRightChild = true;
//从root往下寻找值为key的node
while (current.key != key){
parent = current;
if(key > current.key){
current = current.right;
isRightChild = true;
}else {
current = current.left;
isRightChild = false;
}
//没找到 直接返回
if (current == null) return false;
}
//情况1:只需要将parent.left或parent.right设置为null
if(current.right == null && current.left == null){
if (current == root){//如果删除节点为根节点,整棵树清空
root = null;
}
else {
if(isRightChild){
parent.right = null;
}else {
parent.left = null;
}
}
return true;
}
//情况2:要删除结点有一个右孩子
else if(current.left==null) {
if(current==root) {//若删除的根节点有左孩子,直接将左孩子置为root
root = current.right;
}
else if(isRightChild)//如果current是右孩子,需要将parent的右指针指向其右孩子
parent.right=current.right;
else//...
parent.left=current.right;
return true;
}//要删除结点有一个左孩子
else if(current.right==null){
if(current==root) {//若删除的根节点有右孩子,直接将右孩子置为root
root = current.left;
}
else if(isRightChild)
parent.right=current.left;
else
parent.left=current.left;
return true;
}
//要删除结点有两个子结点
else{
Node successor=getDelSucNode(current); //找到要删除结点的后继结点
if(current==root) {
root = successor;
}
else if(isRightChild) {
parent.right = successor;
}
else {
parent.left = successor;
}
successor.father = parent;
successor.left=current.left;
current.left.father = successor;
return true;
}
}
本文参考自:https://www.cnblogs.com/yahuian/p/10813614.html#commentform
所有完整代码均放置在Github https://github.com/whl-1998/DataStructAndAlgorithm