【图解数据结构】二叉查找树

[TOC]

二叉查找树定义

每棵子树头节点的值都比各自左子树上所有节点值要大,也都比各自右子树上所有节点值要小。

二叉查找树的中序遍历序列一定是从小到大排列的。

二叉查找树节点定义

/// <summary>
/// 二叉查找树节点
/// </summary>
public class Node
{
    /// <summary>
    /// 节点值
    /// </summary>
    public int Data { get; set; }
    /// <summary>
    /// 左子节点
    /// </summary>
    public Node Left { get; set; }
    /// <summary>
    /// 右子节点
    /// </summary>
    public Node Right { get; set; }

    /// <summary>
    /// 打印节点值
    /// </summary>
    public void DisplayNode()
    {
        Console.Write(Data + " ");
    }
}

插入节点

二叉查找树的插入节点操作相对比较简单,只需要找到要插入节点的位置放置即可。

插入节点的整体流程:

  1. 把父节点设置为当前节点,即根节点。
  2. 如果新节点内的数据值小于当前节点内的数据值,那么把当前节点设置为当前节点的左子节点。如果新节点内的数据值大于当前节点内的数据值,那么就跳到步骤 4。
  3. 如果当前节点的左子节点的数值为空(null),就把新节点插入在这里并且退出循环。否则,跳到 while 循环的下一次循环操作中。
  4. 把当前节点设置为当前节点的右子节点。
  5. 如果当前节点的右子节点的数值为空(null),就把新节点插入在这里并且退出循环。否则,跳到 while 循环的下一次循环操作中。

代码实现:

public class BinarySearchTree
{
    public Node root;
    public BinarySearchTree()
    {
        root = null;
    }
    /// <summary>
    /// 二叉查找树插入结点
    /// </summary>
    /// <param name="i"></param>
    public void Insert(int i)
    {
        Node newNode = new Node
        {
            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;
                    }
                }
            }
        }
    }
}

因为二叉查找树的中序遍历序列一定是由小到大排列的,所以我们可以通过中序遍历测试二叉查找树的插入操作。关于二叉树遍历操作可以移步我的上一篇博客【图解数据结构】 二叉树遍历

中序遍历代码实现:

/// <summary>
/// 二叉查找树中序遍历
/// </summary>
/// <param name="node"></param>
public void InOrder(Node node)
{
    if (node != null)
    {
        InOrder(node.Left);
        node.DisplayNode();
        InOrder(node.Right);
    }
}

测试代码:

class BinarySearchTreeTest
{
    static void Main(string[] args)
    {
        BinarySearchTree bst = new BinarySearchTree();
        bst.Insert(23);
        bst.Insert(45);
        bst.Insert(16);
        bst.Insert(37);
        bst.Insert(3);
        bst.Insert(99);
        bst.Insert(22);         

        Console.WriteLine("中序遍历: ");
        bst.InOrder(bst.root);

        Console.ReadKey();
    }
}

测试结果:

上面的测试代码形成了一棵这样的二叉查找树:

查找节点

对于 二叉查找树(BST) 有三件最容易做的事情:查找一个特殊数值,找到最小值,以及找到最大值。

查找最小值

根据二叉查找树的性质,二叉查找树的最小值一定是在左子树的最左侧子节点。

所以实现很简单,就是从根结点出发找出二叉查找树左子树的最左侧子节点。

代码实现:

/// <summary>
/// 查找二叉查找树最小值
/// </summary>
/// <returns></returns>
public int FindMin()
{
    Node current = root;
    while (current.Left != null)
    {
        current = current.Left;
    }
    return current.Data;
}

查找最大值

根据二叉查找树的性质,二叉查找树的最大值一定是在右子树的最右侧子节点。

所以实现很简单,就是从根结点出发找出二叉查找树右子树的最右侧子节点。

代码实现:

/// <summary>
/// 查找二叉查找树最大值
/// </summary>
/// <returns></returns>
public int FindMax()
{
    Node current = root;
    while (current.Right != null)
    {
        current = current.Right;
    }
    return current.Data;
}

查找特定值

根据二叉查找树的性质,从根结点开始,比较特定值和根结点值的大小。如果比根结点值大,则说明特定值在根结点右子树上,继续在右子节点执行此操作;如果比根结点值小,则说明特定值在根结点左子树上,继续在左子节点执行此操作。如果到执行完成都没有找到和特定值相等的节点值,那么二叉查找树中没有包含此特定值的节点。

代码实现:

/// <summary>
/// 查找二叉查找树特定值节点
/// </summary>
/// <param name="key">特定值</param>
/// <returns></returns>
public Node Find(int key)
{
    Node current = root;
    while (current.Data != key)
    {
        if (key < current.Data)
        {
            current = current.Left;
        }
        if (key > current.Data)
        {
            current = current.Right;
        }
        // 如果已到达 BST 的末尾
        if (current == null)
        {
            return null;
        }
    }
    return current;
}

删除节点

相对于前面的操作,二叉查找树的删除节点操作就显得要复杂一些了,因为删除节点会有破坏 BST 正确
层次顺序的风险。

我们都知道在二叉查找树中的结点可分为:没有子节点的节点,带有一个子节点的节点 ,带有两个子节点的节点 。那么可以将二叉查找树的删除节点操作简单拆分一下,以便于我们的理解。如下图:

删除叶子节点

删除叶子节点是最简单的事情。 唯一要做的就是把目标节点的父节点的一个子节点设置为空(null)。

查看这个节点的左子节点和右子节点是否为空(null),都为空(null)说明为叶子节点。

然后检测这个节点是否是根节点。如果是,就把它设置为空(null)。

否则,如果isLeftChild 为true,把父节点的左子节点设置为空(null);如果isLeftChild 为false,把父节点的右子节点设置为空(null)。

代码实现:

//要删除的结点是叶子结点的处理
if (current.Left == null && current.Right == null)
{
    if (current == root)
        root = null;
    else if (isLeftChild)
        parent.Left = null;
    else
    {
        parent.Right = null;
    }
}

删除带有一个子节点的节点

当要删除的节点有一个子节点的时候,需要检查四个条件:

  1. 这个节点的子节点可能是左子节点;
  2. 这个节点的子节点可能是右子节点;
  3. 要删除的这个节点可能是左子节点;
  4. 要删除的这个节点可能是右子节点。

代码实现:

//要删除的结点是带有一个子节点的节点的处理
//首先判断子结点是左子节点还是右子节点,然后再判断当前节点是左子节点还是右子节点
else if (current.Right == null)
    if (current == root)
        root = current.Left;
    else if (isLeftChild)
        parent.Left = current.Left;
    else
        parent.Right = current.Left;
else if (current.Left == null)
    if (current == root)
        root = current.Right;
    else if (isLeftChild)
        parent.Left = current.Right;
    else
        parent.Right = current.Right;

删除带有两个子节点的节点

如果要删除标记为 52 的节点,需要重构这棵树。这里不能用起始节点为 54 的子树来替换它,因为 54 已经有一个左子节点了。这个问题的答案是把中序后继节点移动到要删除节点的位置上。 当然还要区分后继节点本身是否有子节点。

这里我们需要了解一下后继节点的定义。

一个节点的后继节点是指,这个节点在中序遍历序列中的下一个节点。相应的,前驱节点是指这个节点在中序遍历序列中的上一个节点。

举个例子,下图中的二叉树中序遍历序列为: DBEAFCG,则A的后继节点为F,A的前驱节点为E。

了解了这些,删除带有两个子节点的节点的操作就可以转化为寻找要删除节点的后继节点并且把要删除节点的右子树赋给后继结点的右子节点,这里需要注意的是如果后继节点本身有子节点,则需要将后继节点的子结点赋给后继节点父节点的左子节点。

先上获取后继结点的代码,然后举个例子说明:

/// <summary>
/// 获取后继结点
/// </summary>
/// <param name="delNode">要删除的结点</param>
/// <returns></returns>
public Node GetSuccessor(Node delNode)
{
    //后继节点的父节点
    Node successorParent = delNode;
    //后继节点
    Node successor = delNode.Right;
    Node current = delNode.Right.Left;
    while (current != null)
    {
        successorParent = successor;
        successor = current;
        current = current.Left;
    }
    //如果后继结点不是要删除结点的右子结点,
    //则要将后继节点的子结点赋给后继节点父节点的左节点
    //删除结点的右子结点赋给后继结点作为 后继结点的后继结点
    if (successor != delNode.Right)
    {
        successorParent.Left = successor.Right;
        successor.Right = delNode.Right;
    }
    return successor;
}

删除带有两个子节点的节点的代码实现:

//要删除的结点是带有两个子节点的节点的处理
else
{
    Node successor = GetSuccessor(current);
    if (current == root)
        root = successor;
    else if (isLeftChild)
        parent.Left = successor;
    else
        parent.Right = successor;
    //因为后继结点是要删除结点右子树的最左侧结点
    //所以后继结点的左子树肯定是要删除结点左子树
    successor.Left = current.Left;
}

我们观察到删除节点的后继节点一定是删除节点右子树的最左侧节点。这里有3种情况:

后继节点是删除节点的子节点

删除节点37,后继节点40是删除节点37的子节点。delNode是结点37,successor是节点40,delNode.Right是节点40,successor == delNode.Right,后继节点为删除节点的子节点,这种情况是最简单的。

后继节点不是删除节点的子节点

后继节点38是删除节点37右子树的最左侧节点。delNode是节点37,successor是节点38,successorParent 是节点40,delNode.Right 是节点40。successor != delNode.Right,所以要将 successorParent.Left = successor.Right;successor.Right = delNode.Right;。因为successor.Right==null,所以successorParent.Left = nullsuccessor.Right = delNode.Right,节点40成为了节点38的右子节点。因为删除节点的后继节点一定是删除节点右子树的最左侧节点,所以后继节点肯定没有左子节点。删除节点被删除后,后继结点会补到删除节点的位置。successor.Left = current.Left;,也就是删除节点的左子节点变成了后继节点的左子节点。

完成删除节点后的搜索二叉树变为:

后继节点不是删除节点的子节点且有子节点

这种情况和上一种情况相似,唯一的区别是后继节点有子节点(注意肯定是右子节点)。也就是successorParent.Left = successor.Right;,后继节点的右子节点变成后继结点父节点的左子节点。因为successor.Right是节点39,所以节点40的左子节点变成了节点39。其它操作和上一种情况完全相同。

完成删除节点后的搜索二叉树变为:

删除节点操作的整体流程:

  1. 把后继节点的右子节点赋值为后继节点的父节点的左子节点。
  2. 把要删除节点的右子节点赋值为后继节点的右子节点。
  3. 从父节点的右子节点中移除当前节点,并且把它指向后继节点。
  4. 从当前节点中移除当前节点的左子节点,并且把它指向后继节点的左子节点。

综合以上删除节点的三种情况,删除节点操作的完整代码如下:

/// <summary>
/// 二叉查找树删除节点
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
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.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;
        }
    }

    //要删除的结点是带有一个子节点的节点的处理
    else if (current.Right == null)
        if (current == root)
            root = current.Left;
        else if (isLeftChild)
            parent.Left = current.Left;
        else
            parent.Right = current.Left;
    else if (current.Left == null)
        if (current == root)
            root = current.Right;
        else if (isLeftChild)
            parent.Left = current.Right;
        else
            parent.Right = current.Right;

    //要删除的结点是带有两个子节点的节点的处理
    else
    {
        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;
}

/// <summary>
/// 获取后继结点
/// </summary>
/// <param name="delNode">要删除的结点</param>
/// <returns></returns>
public Node GetSuccessor(Node delNode)
{
    //后继节点的父节点
    Node successorParent = delNode;
    //后继节点
    Node successor = delNode.Right;
    Node current = delNode.Right.Left;
    while (current != null)
    {
        successorParent = successor;
        successor = current;
        current = current.Left;
    }
    //如果后继结点不是要删除结点的右子结点,
    //则要将后继节点的子结点赋给后继节点父节点的左节点
    //删除结点的右子结点赋给后继结点作为 后继结点的后继结点
    if (successor != delNode.Right)
    {
        successorParent.Left = successor.Right;
        successor.Right = delNode.Right;
    }
    return successor;
}

删除节点测试

我们还是使用中序遍历进行测试,首先构造二叉查找树:

static void Main(string[] args)
{
    BinarySearchTree bst = new BinarySearchTree();
    bst.Insert(23);
    bst.Insert(45);
    bst.Insert(16);
    bst.Insert(37);
    bst.Insert(3);
    bst.Insert(99);
    bst.Insert(22);
    bst.Insert(40);
    bst.Insert(35);
    bst.Insert(38);
    bst.Insert(44);
    bst.Insert(39);
}          

构造出的二叉查找树:

测试分三种情况:

测试删除叶子节点

删除叶子节点39

Console.Write("删除节点前: ");
bst.InOrder(bst.root);

bst.Delete(39);

Console.Write("删除节点后: ");
bst.InOrder(bst.root);

测试结果:

测试删除带有一个子节点的节点

删除带有一个子节点的节点38

Console.Write("删除节点前: ");
bst.InOrder(bst.root);

bst.Delete(38);

Console.Write("删除节点后: ");
bst.InOrder(bst.root);

测试结果:

测试删除带有两个子节点的节点

删除带有两个子节点的节点37

Console.Write("删除节点前: ");
bst.InOrder(bst.root);

bst.Delete(37);

Console.Write("删除节点后: ");
bst.InOrder(bst.root);

测试结果:

参考:

《数据结构与算法 C#语言描述》

《大话数据结构》

《数据结构与算法分析 C语言描述》

五一大家都出去happy了,为什么我还要自己在家撸代码,是因为爱吗?是因为责任吗?都不是。是因为我的心里只有学习(其实是因为)。哈哈,提前祝大家五一快乐,吃好玩好!

作者:喜欢天黑却怕鬼

来源:http://songwenjie.cnblogs.com/

声明:本文为博主学习感悟总结,水平有限,如果不当,欢迎指正。如果您认为还不错,不妨点击一下下方的【推荐】按钮,谢谢支持。转载与引用请注明出处。

原文地址:https://www.cnblogs.com/songwenjie/p/8973217.html

时间: 2024-10-03 21:56:47

【图解数据结构】二叉查找树的相关文章

数据结构—二叉查找树

查找树是一种数据结构,二叉查找树是按二叉树结构来组织的.可以用链表结构表示,其中每一个结点就是一个对象.结点中包括:key.数据.left.right.p.其中left.right和p分别指向左儿子,右儿子和父结点. 二叉查找树中的关键字总是满足二叉查找树的性质:y是x左子树上的结点,则key[y]≤key[x],如果y是x右子树上的结点,则key[y]≥key[x]. 遍历: 根据二叉查找树的性质,可以用一个递归算法按排列顺序输出树中的所有关键字.这种是中序遍历算法:因为一子树根的关键字在输出

Python编程电子资料《流畅的Python》+《图解数据结构使用Python》+《Python可以这样学》

现在无人不谈“大数据技术”和“人工智能技术”,而商业智能和机器学习等应用的具体开发中又大量使用Python程序设计语言,用Python语言来描述算法和讲述数据结构就成为顺其自然的事情了. “数据结构”毫无疑问是计算机科学既经典又核心的课程之一,只要从事计算机相关的开发工作,系统地学习数据结构是进入这个行业的“开山斧”. <图解数据结构使用Python>电子资料包含电子书和源代码,采用丰富的图例来阐述基本概念,并以简洁清晰的语言来诠释重要的理论和算法,同时配合完整的范例程序代码,可以通过“实例+

图解数据结构(7)——二叉查找树及平衡二叉查找树(一共14篇)

这篇将是最有难度和挑战性的一篇,做好心理准备!十.二叉查找树(BST)前一篇介绍了树,却未介绍树有什么用.但就算我不说,你也能想得到,看我们Windows的目录结构,其实就是树形的,一个典型的分类应用.当然除了分类,树还有别的作用,我们可以利用树建立一个非常便于查找取值又非常便于插入删除的数据结构,这就是马上要提到的二叉查找树(Binary Search Tree),这种二叉树有个特点:对任意节点而言,左子(当然了,存在的话)的值总是小于本身,而右子(存在的话)的值总是大于本身. 这种特性使得我

数据结构——二叉查找树

使二叉树成为二叉查找树的性质是,对于树中的每个节点X,它的左子树中所有关键字值小于X的关键字值,而它的右子树中所有关键字值大于X的关键字值.这意味着,该树所有的元素以某种统一的方式排序. 二叉查找树的声明 二叉查找树是一棵特殊的二叉树,二叉查找树中节点的结构与二叉树种节点的结构相同,关键在于可以在二叉查找树上可以执行的操作以及对其进行操作的算法.二叉查找树的声明如下: #ifndef  _Tree_H struct TreeNode; typedef struct TreeNode *Posit

数据结构-二叉查找树

前序遍历: 后序遍历: 二叉查找树按照二叉树进行组织.二叉查找树关键字的存储方式总是瞒住二叉查找树性质: 设x为二查查找树种一个节点.如果y是x的左子树中的一个节点,那么key[x] >= key[y].如果y是x的右子树的一个节点,那么key[x] <= key[y]. 这样对二叉查找树进行中序遍历就可得到书中所有元素的一个非降序排列. 查找某一个存在节点的前驱和后继.某一个节点x的后继就是大于key[x]的关键字中最小的那个节点,前驱就是小于key[x]的关键字中最大的那个节点.查找二叉前

JAVA数据结构--二叉查找树

二叉查找树定义 二叉查找树(英语:Binary Search Tree),也称二叉搜索树.有序二叉树(英语:ordered binary tree),排序二叉树(英语:sorted binary tree),是指一棵空树或者具有下列性质的二叉树: 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值: 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值: 任意节点的左.右子树也分别为二叉查找树: 没有键值相等的节点. 二叉查找树相比于其他数据结构的优势在于查找.插入

图解数据结构树之AVL树

AVL树(平衡二叉树): AVL树本质上是一颗二叉查找树,但是它又具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树.在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为平衡二叉树.下面是平衡二叉树和非平衡二叉树对比的例图: 平衡因子(bf):结点的左子树的深度减去右子树的深度,那么显然-1<=bf<=1; AVL树的作用: 我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为

Java数据结构-二叉查找树续以及平衡二叉查找树

??前面一篇文章讲到了二叉查找树的实现,其中的插入操作是使用非递归方法实现的,这里再增加一种递归实现插入的操作,Java代码如下,建议增加到前一篇文章对应的FOBinarySearchTree.java中: /** * @TODO 二叉排序树插入元素(递归方法) * @param e 需要插入的元素 * @return true or false */ public boolean insert(E e){ insert(root,e); return true; } /** * @TODO 二

数据结构--二叉查找树的java实现

上代码: package com.itany.erchachazhaoshu; public class BinarySearchTree<T extends Comparable<? super T>> { //定义二叉查找树的根节点 每一个查找二叉树都有一个自己的root 节点 root外界看不到 private BinaryNode<T> root; public BinarySearchTree() { root=null; } //节点类 private st