二叉查找树又叫 二叉排序树、二叉搜索树
文章中树的概念和二叉树的定义转自二叉查找树(一)之 图文解析 和 C语言的实现
前驱节点和后继节点 参考:二叉搜索树的前驱节点和后继节点
删除节点参考:二叉查找树 - 删除节点 详解(Java实现)
本文对前驱节点、后继节点、删除操作进行着重讲解
完整代码是C#实现
0X01 节点的定义
- 节点的定义
public class Node
{
public int Key;
public Node Parent;//parent
public Node L; //left
public Node R; //right
}
- 树的定义
public class BSTree
{
//树的根节点
public Node Root;
}
0X02 遍历
这里列举 前序遍历、中序遍历、后序遍历、层次遍历、Z形(蛇形)遍历。
二叉搜索树的中序遍历是单调递增的
2.1 前序遍历
public void Preorder(Node n)
{
if (n == null)
return;
Print(n);
Preorder(n.L);
Preorder(n.R);
}
2.2 中序遍历
/// <summary>
/// 中序遍历
/// </summary>
/// <param name="n"></param>
public void Inorder(Node n)
{
if (n == null)
return;
Inorder(n.L);
Print(n);
Inorder(n.R);
}
二叉搜索树的中序遍历是单调递增的
2.3 后序遍历
/// <summary>
/// 后序遍历
/// </summary>
/// <param name="n"></param>
public void Postorder(Node n)
{
if (n == null)
return;
Postorder(n.L);
Postorder(n.R);
Print(n);
}
2.4 层次遍历
/// <summary>
/// 层次遍历
/// </summary>
/// <param name="n"></param>
public void Levelorder(Node n)
{
if (n == null)
return;
Queue<Node> queue = new Queue<Node>();
queue.Enqueue(n);
while(queue.Count>0)
{
var item = queue.Dequeue();
if (item == null)
continue;
Print(item);
queue.Enqueue(item.L);
queue.Enqueue(item.R);
}
}
2.5 Z形(蛇形)遍历
/// <summary>
/// Z形(蛇形)遍历
/// </summary>
/// <param name="n"></param>
public void ZLevelorder(Node n)
{
Stack<Node> stackl2r = new Stack<Node>();
Stack<Node> stackr2l = new Stack<Node>();
stackl2r.Push(n);
while(stackl2r.Count>0|| stackr2l.Count > 0)
{
while (stackl2r.Count>0)
{
n = stackl2r.Pop();
if (n == null)
continue;
Print(n);
stackr2l.Push(n.L);
stackr2l.Push(n.R);
}
while (stackr2l.Count > 0)
{
n = stackr2l.Pop();
if (n == null)
continue;
Print(n);
stackl2r.Push(n.R);
stackl2r.Push(n.L);
}
}
}
0X03 前驱节点和后继节点详解
前驱节点:对一棵二叉树进行中序遍历,按照遍历后的顺序,当前节点的前一个节点为该节点的前驱节点。
后继节点:对一棵二叉树进行中序遍历,按照遍历后的顺序,当前节点的后一个节点为该节点的后继节点
3.1 前驱节点
- 若一个节点有左子树,那么该节点的前驱节点是其左子树中键值最大的节点(按照中序遍历,左子树中最大的节点后继就是父节点即当前节点)(果有左子树,那么前驱节点是左子树中最大值)
- 若一个节点没有左子树,那么判断该节点和其父节点的关系 :
2.1 若该节点是其父节点的右边孩子,那么该节点的前驱结点即为其父节点。(若该节点是右孩子,则前驱节点为它的父节点)(按照中序遍历该节点是右孩子且没有该节点没有左孩子,则该节点的父节点的右子树中最小的节点即是后续节点,反过来父节点为该节点的前驱节点)
例:上图7的前驱是6
2.2 若该节点是其父节点的左边孩子,那么需要沿着其父亲节点一直向树的顶端寻找,直到找到一个节点P,P节点是其父节点Q的右边孩子,那么Q就是该节点的后继节点。(若该节点是左孩子则找到最近的父节点且父节点的右孩子是该节点所在的子树,找到的那个父节点即为前驱节点)
例:上图2的前驱是1,8的前驱是7
/// <summary>
/// 前驱节点
/// 对一棵二叉树进行中序遍历,按照遍历后的顺序,当前节点的前一个节点为该节点的前驱节点
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public Node Predecessor(Node node)
{
Node n = null;
if (node == null)
return null;
//1.如果有左子树,那么前驱节点是左子树中最大值
else if (HasLeft(node))
return Maximum(node.L);
//2.1如果是右孩子,node的前驱节点为它的父节点
else if (IsRight(node))
return node.Parent;
//2.2如果是左孩子则找到最低的父节点且父节点的右孩子是node所在的子树
//另一种说法:如果是左孩子则找到node所在子树被称为右子树的父节点
else if (IsLeft(node))
{
n = node.Parent;
while (n != null)
{
if (IsLeft(n))
n = n.Parent;
else
return n.Parent;
}
}
return null;
}
3.2 后继节点
- 若一个节点有右子树,那么该节点的后继节点是其右子树中val值最小的节点(如果有右子树,那么后继节点是左子树中最大值)
- 若一个节点没有右子树,那么判断该节点和其父节点的关系 :
2.1 若该节点是其父节点的左边孩子,那么该节点的后继结点即为其父节点 (如果是左孩子,该节点的后继节点为它的父节点)
例:上图8的后继是9
2.2 若该节点是其父节点的右边孩子,那么需要沿着其父亲节点一直向树的顶端寻找,直到找到一个节点P,P节点是其父节点Q的左边孩子,那么Q就是该节点的后继节点(该节点如果是右孩子则找到最近的父节点且父节点的左孩子是node所在的子树,找到的那个父节点即为后继节点)
例:上图4的后继是5
/// <summary>
/// 后继节点
/// 对一棵二叉树进行中序遍历,按照遍历后的顺序,当前节点的后一个节点为该节点的后继节点
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public Node Successor(Node node)
{
Node n = null;
//1.如果有右子树,那么后继节点是左子树中最大值
if (HasRight(node))
return Minimum(node.R);
//2.1 如果是左孩子,node的后继节点为它的父节点
else if (IsLeft(node))
return node.Parent;
//2.2 如果是右孩子则找到最低的父节点且父节点的左孩子是node所在的子树
//另一种说法:如果是右孩子则找到node所在子树被称为左子树的父节点
else if (IsRight(node))
{
n = node.Parent;
while (n != null)
{
if (IsRight(n))
n = n.Parent;
else
return n.Parent;
}
}
return null;
}
0X04 查找
/// <summary>
/// 查找
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public Node Search(int key)
{
return Search(Root, key);
}
public Node Search(Node node,int key)
{
if(node == null|| node.Key==key)
{
return node;
}
if (node.Key > key)
return Search(node.L,key);
else
return Search(node.R, key);
}
0X05 插入
/// <summary>
/// 插入
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public Node Insert(int key)
{
return Insert(CreateNode(key));
}
public Node Insert(Node node)
{
if (Root == null)
{
Root = node;
return Root;
}
Node n = Root;
Node x =null;
while(n!=null)
{
x = n;
if (node > n)
n = n.R;
else if (node < n)
n = n.L;
else
{
n = n.L; //允许插入相同键值,如果不允许注释该行,将return注释取消
//return n;
}
}
node.Parent = x;
if (node > x)
x.R = node;
else
x.L = node;
return node;
}
0X06 删除详解
删除节点存在 3 种情况,几乎所有类似博客都提到了这点。这 3 种情况分别如下:
- 没有左右子节点,可以直接删除
- 存在左节点或者右节点,删除后需要对子节点移动
- 同时存在左右子节点,不能简单的删除,但是可以通过和后继节点交换后转换为前两种情况(后继节点不可能存在左孩子,有可能有右孩子;)(按照后继节点定义在有右孩子情况下后继节点只能是该节点的右子树最小的节点(所以这个节点没有左孩子,因为它是最小的))
/// <summary>
/// 删除
/// </summary>
/// <param name="n"></param>
/// <returns></returns>
public Node Remove(Node n)
{
Node x=null;
//1. 没有左右子节点
if (!HasChild(n))
{
if (IsLeft(n))
n.Parent.L = null;
else
n.Parent.R = null;
}
//2. 存在左节点或者右节点,删除后需要对子节点移动
else if (HasOneChild(n))
{
x = n.L == null ? n.R : n.L;
if (IsLeft(n))
n.Parent.L = x;
else
n.Parent.R = x;
}
//3. 同时存在左右子节点,通过和后继节点交换后转换为前两种情况(后继节点不可能存在左孩子,有可能有右孩子;)
else
{
//找到后继节点,将后继节点填到删除节点位置,(继节点不可能有左孩子,可能右孩子,也就是变为删除后继节点问题,且后继节点最多有一个孩子节点(右孩子))
Node successorNode = Successor(n); //找到删除节点n的后继节点,后继节点不可能存在左孩子,有可能有右孩子
Node rightNode = successorNode.R;
if(rightNode!=null)
{
rightNode.Parent = successorNode.Parent;
}
if (successorNode.Parent != null)
if (IsLeft(successorNode))
successorNode.Parent.L = rightNode;
else
successorNode.Parent.R = rightNode;
successorNode.Parent = n.Parent;
if (IsLeft(n))
n.Parent.L = successorNode;
else if (IsRight(n))
n.Parent.R = successorNode;
else
Root = successorNode;
successorNode.L = n.L;
if (n.L != null)
n.L.Parent = successorNode;
successorNode.R = n.R;
if (n.R != null)
n.R.Parent = successorNode;
}
n.L = null;
n.R = null;
n.Parent = null;
return n;
}
0X07 完整代码(C#)
0X08 可视化工具
二叉搜索树可视化工具(旧金山大学 (usfca)|数据结构可视化工具)