I 二叉搜索树特点
- 每个结点有唯一的值,且每个结点的值均不相同
- 若它的左子树不为空,则它的左子树的所有结点均小于根节点的值
- 若它的右子树不为空,则它的右子树的所有结点均大于根结点的值
- 它的左右子树均为二叉搜索树。
II 二叉搜索树的操作
二叉查找树由节点组成,所以需要一个 Node 类
public class Node
{
public int Data;
public Node left;
public Node right;
public void DisplayNode()
{
Console.Write(iData);
}
}
1 插入
插入的步骤
1.创建一个 Node 对象,并且把 Node 存储的数据赋值给 iData 变量
2.查看 BST 是否有根节点。如果没有,那么说明这是一个新的 BST,并且要插入的节点就是根节
点。如果是这种情况,那么就结束这个方法。
3.如果要添加的节点不是根节点,那么为了找到合适的插入点需要遍历 BST(BinarySearchTree),确定节点正确位置的算法如下:
1)把父节点设置为当前节点,即根节点。
2)如果新节点内的数据值小于当前节点内的数据值,那么把当前节点设置为当前节点的左子节点。如果新节
点内的数据值大于当前节点内的数据值,那么就跳到步骤 4。
3)如果当前节点的左子节点的数值为空(null),就把新节点插入在这里并且退出循环。否则,跳到 while 循
环的下一次循环操作中。
4)把当前节点设置为当前节点的右子节点。
5)如果当前节点的右子节点的数值为空(null),就把新节点插入在这里并且退出循环。否则,跳到 while 循
环的下一次循环操作中。
代码如下:
public class BinarySearchTree
{
public Node root;
public BinarySearchTree()
{
root = null;
}
public void Insert(int i)
{
Node newNode = new Node();
newNode.Data = i;
if (root == null)
root = newNode;
else
{
Node current = root;
Node parent;
while (true)
{
parent = current;
if (i < current.Data)
{
current = current.Left;
if (current == null)
{
parent.Left = newNode;
break;
}
}
else
{
current = current.Right;
if (current == null)
{
parent.Right = newNode;
break;
}
}
}
}
}
}
2 查找
这个方法首先创建一个Node 节点,并且把它设置为 BST 的根节点。接下来方法会查看关键字(要查找的数据)是否在这个节点内。如果在,那么这个方法就简单地返回当前节点并且退出。如果在根节点内没有找到该数据,就把要查找的数据与存储在
当前节点内的数据进行比较。如果关键字小于当前数据值,就把当前节点设置为左子节点。如果关键字大于当前数据值,就把当前节点设置为右子节点。如果当前节点为空(null),那么这个方法的最后一段就会返回空(null)作为方法的返回值,这表明到达 BST 的末尾也没有找到关键值。当 while 循环结束的时候,在 current 中存储的数值就是要查找的数值。
public Node Find(int key)
{
Node current = root;
while (current.iData != key)
{
if (key < current.iData)
current = current.Left;
else
current = current.Right;
if (current == null) return null;
}
return current;
}
3 删除
1)删除叶子节点
因为不需要考虑子节点的问题,所以移除叶子是最简单的事情。唯一要做的就是把目标节点的父节点的每一个子节点设置为null。当然,节点始终存在,只是与该节点没有任何连接了。
public boolNode Delete(int key)
{
Node current = root;
Node parent = root;
bool isLeftChild = true;
while (current.Data != key)
{
parent = current;
if (key < current.Data)
{
isLeftChild = true;
current = current.Left;
}
else
{
isLeftChild = false;
current = current.Right;
}
if (current == null)
return false;
}
if ((current.Left == null) & (current.Right == null))
{
if (current == root)
root == null;
else if (isLeftChild)
parent.Left = null;
else
parent.Right = null;
}
return true;
}
2)删除带有一个子节点的节点
当要删除的节点有一个子节点的时候,需要检查四个条件:1.这个节点的子节点可能是左子节点;2.这个节点的子节点可能是右子节点;3.要删除的这个节点可能是左子节点;4.要删除的这个节点可能是右子节点。
代码:
public boolNode Delete(int key)
{
Node current = root;
Node parent = root;
bool isLeftChild = true;
while (current.Data != key)
{
parent = current;
if (key < current.Data)
{
isLeftChild = true;
current = current.Right;
}
else
{
isLeftChild = false;
current = current.Right;
}
if (current == null)
return false;
}
if ((current.Left == null) & (current.Right == null))
{
if (current == root)
root == null;
else if (isLeftChild)
parent.Left = null;
else
parent.Right = null;
}
else if (current.Right == null)
{
if (current == root)
root = current.Left;
else if (isLeftChild)
parent.Left = current.Left;
else
parent.Right = current.Right;
}
else if (current.Left == null)
{
if (current == root)
root = current.Right;
else if (isLeftChild)
parent.Left = parent.Right;
else
parent.Right = current.Right;
}
return true;
}
3)删除带有两个子节点的节点
要删除拥有2个子节点的节点时,需要重构以子节点为根的子树。做法是找到这个子节点在中序遍历的后一个节点,成为后继节点,把后继节点移动到要删除的位置上,来保证二叉搜索树的结构规则。
(还有另外一种方法删除带有2个子节点的节点,也是我后来才看到的,思路就是把后继节点的值赋值给要删除的节点,然后问题就转变为删除后继节点了,而此时后继节点肯定是没有左树的,因为找后继节点就是要删除节点右子树中最小的一个。所以最终就转变为了删除带有右子节点的问题。以下代码理解起来没有这个简单,但也是一个正确的方法)
这里不再单独写2个子节点的代码,直接贴出综合以上几种情况的完整delete方法的代码:
public Node GetSuccessor(Node delNode)
{
Node successorParent = delNode;
Node successor = delNode;
Node current = delNode.Right;
while (!(current == null))
{
successorParent = current;
successor = current;
current = current.Left;
}
if (!(successor == delNode.Right))
{
successorParent.Left = successor.Right;
successor.Right = delNode.Right;
}
return successor;
}
public bool Delete(int key)
{
Node current = root;
Node parent = root;
bool isLeftChild = true;
while (current.Data != key)
{
parent = current;
if (key < current.Data)
{
isLeftChild = true;
current = current.Right;
}
else
{
isLeftChild = false;
current = current.Right;
}
if (current == null) return false;
}
if ((current.Left == null) & (current.Right == null))//当删除叶子节点
{
if (current == root)
root = null;
else if (isLeftChild)
parent.Left = null;
else if (isRightChild)
parent.Right = null;
}
else if (current.Right == null)//当删除带有左节点的节点
{
if (current == root)
root = current.Left;
else if (isLeftChild)
parent.Left = current.Left;
else
parent.Right = current.Right;
}
else if (current.Left == null)//当删除带有右节点的节点
{
if (current == root)
root = current.Right;
else if (isLeftChild)
parent.Left = current.Right;
else
parent.Right = current.Right;
}
else//删除2个子节点的节点
{
Node successor = GetSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.Left = successor;
else
parent.Right = successor;
successor.Left = current.Left;
}
return true;
}
III 二叉搜索树的性能分析
二叉搜索树的高度计算:假设是一颗完美二叉树,n为节点的总个数,那么(n+1)/2为最后一层节点数,除以h-1次的2就是第一层的节点数即为1.
例如,如果是一颗高度为5的满二叉树,每层节点数如下:
1
2
4
8
16
那么整棵树的节点数为2^h-1=31, 最后一层节点数为(31+1)/2=16,因为有5层,所以要向上除以4次2,得到第一层的节点数目为1.即计算公式为
(n+1)/2 / 2^(n-1) = 1
h = log2(n+1)
1.时间复杂度
查找效率:循环体内一共2次判断操作 一次赋值操作
如果是执行n次就是 3n + 1 这个1是 while (current.iData != key) 这句
但是二叉搜索树是一共log2(n+1)层,所以深度是log2n,循环是log2n次
最后=3log2n =O(log2n)
也就是说以上述假定完美二叉树的情况下,时间复杂度是 O(log2n) 如果二叉搜索树完全不平衡则其深度可达到n,查找效率为O(n),退化为顺序查找。如下图:
2.空间复杂度
要构建二叉搜索树,是需要通过一个个插入排序的,所以空间复杂度为O(n)
3 平均查找长度
公式:ASL = [(n+1)/n] * log2(n+1) - 1
总结
二叉搜索树的出现是为了优化查找效率,但当一个二叉树不平衡时,甚至为单支树,它的查找效率会退化到与顺序查找效率相近,为了解决这个问题,有了AVL树(二叉平衡树)