【问题标题】:Delete function binary search tree BST with parent passing the tree pointer as argument删除函数二叉搜索树 BST,其父传递树指针作为参数
【发布时间】:2021-03-08 20:01:12
【问题描述】:

我很难理解我的书要我学习什么。我目前正在重新审视我多年前研究的一些算法和数据结构材料。其中一章是关于二叉搜索树及其应用等。这本书讨论了 BST(二叉搜索树)作为具有以下特性的节点的树:int data, node left, node right, node parent。我明白这一点,我已经实现了搜索功能。

现在的问题是:当我的参数是N* delete(Tree* tree, N* delete_node); 时,如何创建删除函数?

有很多递归函数,但它们依赖于我们递归传递节点指针和键值这一事实。这不是本书中的练习所要寻找的。我尝试过并且有效的方法示例是:source code for C Program for Insertion and Deletion in Binary Search Tree。不符合这些标准的另外两个重要来源是codezclubgeeksforgeeks。他们解决了“删除”功能,但他们的功能在他们的功能中有其他参数。我的书指定

一个修改操作,给定一个指针 delete_node 指向一个 BST 树中的元素从 BST 树中删除 delete_node 并返回其指针。

函数声明:N* delete(Tree* tree, N* delete_node);

有人有什么想法吗?上面的源代码中还给出了最小可重复的示例,其中结构缺少“父”节点。以下是我对树和节点的定义:

typedef struct N {
    int data;
    struct N* left;
    struct N* right;
    struct N* parent;
} N;

typedef struct Tree {
    N* root;
} Tree;

【问题讨论】:

    标签: c binary-search-tree


    【解决方案1】:

    您可以参考CLRS 来了解Binary search tree 的数据结构和对它的操作。让我们先实现delete 程序,然后解释必要的部分。
    我们需要2 过程、SUCCESSORtransplant 而不是delete 本身来完成它。在delete之前执行所需的程序。

    为了在二叉搜索树中移动子树,我们定义了一个 子程序移植,将一个子树替换为其父树的子树 另一个子树。当移植替换以节点 u 为根的子树时 以节点 v 为根的子树,节点 u 的父节点成为节点 v 的父节点,并且 u 的 parent 最终将 v 作为其适当的孩子。

    void transplant(Tree** root, Tree* u, Tree* v) {
        if(u == NULL)
            return;
        else if(u->parent == NULL)
            *root = v;
        else if(u->parent->left == u)
            u->parent->left = v;
        else
            u->parent->right = v;
        if(v != NULL)
            v->parent = u->parent;
    }
    

    我们需要实现Tree* SUCCESSOR(Tree* T) 过程,它返回node,其中node->key 是大于T->key 的下一个更大的元素(如果存在)。否则,返回NULL。当delete_node 有两个孩子时,我们需要SUCCESSOR。它将被 SUCCESSOR(delete_node) 替换。

    Tree* SUCCESSOR(Tree* T) {
        if(T == NULL)
            return T; // Since T==NULL, it's equivalent to return NULL
        else if(T->left != NULL) {
            while(T->left != NULL)
                T=T->left;
            return T;
        }
        else {
            while(T->parent != NULL && T->parent->right == T)
                T = T->parent;
            return T->parent;
        }
    }
    

    我们都可以实现delete过程。

    Tree* delete(Tree* root, Tree* delete_node) {
        if(delete_node == NULL)
            return root;
        
        if(delete_node->left== NULL)
            transplant(&root, delete_node, delete_node->right);
    
        else if(delete_node->right == NULL)
            transplant(&root, delete_node, delete_node->left);
    
        else {
            Tree* succ = SUCCESSOR(delete_node);
            if(delete_node->right != succ) {
                transplant(&root, succ , succ ->right);
                succ->right = delete_node->right;
                succ->right->parent = succ;
            }
            transplant(&root, delete_node, succ);
            succ->left = delete_node->left;
            succ->left->parent = succ;
        }
        return root;
    }
    

    delete 程序的工作原理如下:

    从二叉搜索树root 中删除给定节点delete_node 的过程将指向rootdelete_node 的指针作为参数。它的案例组织方式与上述三个略有不同。

    • 如果delete_node 没有左孩子(第一个if 语句做这部分),那么我们用它的右孩子替换delete_node,它可能是也可能不是NULL。当delete_node的右孩子为NULL时,本案例处理delete_node没有孩子的情况。当delete_node的右孩子为非NULL时,本案例处理delete_node只有一个孩子,即其右孩子的情况。

    • 如果delete_node 只有一个孩子,也就是它的左孩子(else if 部分处理这种情况),那么我们将delete_node 替换为其左孩子。

    • 否则,delete_node 既有左孩子又有右孩子。我们找到delete_node的继任者succ,它位于delete_node的右子树中并且没有左孩子(如果它有左孩子,那么它不是大于delete_node->key的最小数。我们得到矛盾)。我们想将succ 拼接出它的当前位置,并让它替换树中的delete_node

      • 如果succdelete_node 的右孩子,那么我们将delete_node 替换为succ

      • 否则,succ 位于delete_node 的右子树中,但不是delete_node 的右子树。在这种情况下,我们首先将succ 替换为它自己的右孩子,然后我们将delete_node 替换为succ

    由于这两个引用均来自CLRS,因此我鼓励您查看它,以防在理解它的工作原理时遇到问题。

    【讨论】:

      【解决方案2】:

      当然你可以不使用递归函数进行删除。

      typedef struct N {
          int data;
          struct N* left;
          struct N* right;
          struct N* parent;
      } N;
      
      typedef struct Tree {
          N* root;
      } Tree;
      
      N* delete(Tree* tree, N* delete_node);
      

      让我们从这个定义开始。好吧,不得不说删除功能并不常见,因为在大多数情况下我们都在使用:

      void delete(Tree* tree, int element);

      因为我们不知道元素是否存在(执行删除时需要通过 contains() 或 find() 函数来判断),而且我们很可能不需要任何返回值。 无论如何,让我们按照你的方式。

      从 BST 中删除很复杂,您应该从三种基本情况着手:

      1. 要删除的节点是叶子节点,即没有子节点;
      2. 节点有一个子节点,左或右;
      3. 节点同时具有左右子节点。

      这些情况在处理上是不同的,为简单起见,我们将省略一些实现细节和安全检查,但不要忘记在生产代码中。

      在情况 1 中,您只需释放内存,并将其父级中的指针更改为 NULL:

      if (delete_node->parent->data < delete_node->data)
          delete_node->parent->right = NULL; // WARNING: corner case
      else
          delete_node->parent->left= NULL; // WARNING: corner case
      
      free(delete_node);
      

      情况 2 很像情况 1;我们需要将父节点中的指针更改为其唯一的子节点,而不是 NULL,然后释放该节点。逻辑类似,代码略过。

      情况 3 稍微复杂一些。通常我们有两种方法,它们都有效;要么我们用其左子树的最大(在 BST 中我们可以说“最右”)数据替换节点的数据,或者我们用其右子树的最小/最左数据替换节点的数据。替换后,我们改为删除该节点。让我们研究第二种方法,即将节点的数据替换为其右子树的最小数据。第一种方法的逻辑非常相似。

      对于这种方法,我们需要实现这个函数:

      N* findMin(N* root_of_subtree);
      

      它将root_of_subtree作为输入(搜索将从该节点开始),并返回指向所需的最小节点的指针。

      假设我们在delete_node的右子树中找到了想要的最小节点:

      N* smallestNodeOfRightSubtree = findMin(delete_node->right);
      
      delete_node->data = smallestNodeOfRightSubtree->data;
      smallestNodeOfRightSubtree->parent->left = smallestNodeOfRightSubtree->right; // WARNING, corner case
      free(smallestNodeOfRightSubtree);
      

      我们直接改变了parent->left here,因为在general情况下(角落案例2解释了什么不是一般情况),该节点有两个特征:

      1. 它没有左孩子。
      2. 它必须在其父节点的左子树上。

      但是,它并不总是通用的,我们仍然需要涵盖一些极端情况:

      1. 如果delete_node 是根节点怎么办?它没有父级,情况 1 中的代码将是未定义的行为。
      2. 如果delete_node的右子树的最小节点是delete_node->right,即右子树只有1个节点怎么办。在这种情况下,我们不能改变 minimumNodeOfRightSubtree->parent->left;相反,我们应该改变 parent->right。

      就是这样。现在您知道了逻辑和一些极端情况。 GL 和 HF。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多