简体   繁体   English

为什么我的二叉搜索树 void 删除功能不能正常工作?

[英]Why is my Binary Search Tree void remove function not working properly?

I am having trouble getting my binary search tree remove function to work properly.我无法让我的二叉搜索树删除功能正常工作。 It never actually wants to work properly no matter what I do.无论我做什么,它实际上都不想正常工作。 It always seems to do something the strangest things when I try to get it to function properly.当我试图让它正常运行时,它似乎总是做一些最奇怪的事情。

The node* struct is located in another header file as is the root_ as well (they are setup in the normal right, left, and data stored configuration) node* 结构体和 root_ 一样位于另一个头文件中(它们设置在正常的右侧、左侧和数据存储配置中)

void remove(int value){
   node* n = root_;
   node* nClone = nullptr; 

    while (n != nullptr) {//constant checker to ensure that a 
        if (value > n->value_) { // if value is larger than value stored within node n it will descend further down right
        
            nClone = n; //stores n before continueing
            n = n->rhs_;
        
        } else if (value < n->value_) { // if value is less than value stored within node n it will descend further down left
        
            nClone = n; //stores n before continueing
            n = n->lhs_;
        
        } else { //if it is equal to the value (there are no other possible outcomes so an else would work) check if there are any subsiquent leaves attached

            if (n->lhs_ == nullptr && n->rhs_ == nullptr) { //if both left and right are empty (I.E. no leaves attached to node) set n to nullptr and then delete using free();
            
                nClone->lhs_ = nullptr; //stores both left
                nClone->rhs_ = nullptr; // and right leaves as nullptr
                
                free(n); //frees n

                n = nullptr;

                count_--;//decreases count_/size counter by 1
                return; //exits from function as there is nothing more to do
            
            } else if (n->lhs_ == nullptr || n->rhs_ == nullptr) { //if n has one connection whether it be on the left or right it stores itself in nClone and then deletes n

                if (n->lhs_ != nullptr) { //if statement to check if left leaf of n exists
                
                    nClone->lhs_ = n->lhs_; //if it does it stores n's left leaf in nClone 
                                    
                } else { //if it doesnt have anything stored in the left then there garuntteed is one on the right

                    nClone->rhs_ = n->rhs_; //stores n's right leaf in nClone

                }
                
                free(n);
                count_--; //decreases count_/size counter by 1
                return; //exits from function as there is nothing more to do

            } else {
                //for preorder succession
                node* nSuc = n->rhs_; //stores right leaf of n in nSuc

                while (nSuc->lhs_ != nullptr) { //look for successor
                    nSuc = nSuc->lhs_;
                
                }

                n->value_ = nSuc->value_;
                free(n);
                count_--;
                return;

            
            }
                    
        }    

    }

}

Your code is written as if it was C. C++ is a different language, and you can and should leverage it to your advantage.您的代码就像是 C 一样编写。C++ 是一种不同的语言,您可以而且应该利用它来发挥自己的优势。

The following text is interspersed with a complete, compileable example that you can try online :) It is of course not the only way to implement a tree, and I had to gloss over various details that would be needed to make it at least a full-featured solution.下面的文本中穿插了一个完整的、可编译的示例,您可以在线尝试 :) 这当然不是实现树的唯一方法,我不得不掩盖需要使其至少完整的各种细节- 特色解决方案。 A "real life" implementation may be much more complex, since the "textbook" approach usually doesn't mix well with cache hierarchies on modern CPUs, and a tree like this would be rather slow compared to state-of-the-art implementations. “现实生活”的实现可能要复杂得多,因为“教科书”方法通常不能很好地与现代 CPU 上的缓存层次结构混合,并且与最先进的实现相比,像这样的树会相当慢. But it does help, I think, to bridge the gap between the pervasive "C-like" way of thinking about trees, and what the modern C++ brings.但我认为,它确实有助于弥合普遍存在的“类 C”的树思考方式与现代 C++ 带来的东西之间的差距。

Again: This example is minimal, it doesn't do many of the things that would be needed in common practice, but it at least points in the direction away from C, and towards C++, and that's what I intended.再次:这个例子是最小的,它没有做很多通常实践中需要的事情,但它至少指向了远离 C 的方向,朝向 C++,这就是我的意图。

First, let's have a Node type that uses owning pointers for the child nodes.首先,让我们有一个Node类型,它使用子节点的拥有指针。 These pointers automatically manage memory and essentially prevent you from making mistakes that would leak memory or allow use of dangling pointers.这些指针会自动管理内存,从根本上防止您犯可能导致内存泄漏或允许使用悬空指针的错误。 The term "owning pointer" means that there's always a well defined owner: it is the pointer itself.术语“拥有指针”意味着总是有一个明确定义的所有者:它是指针本身。 Those pointers cannot be, for example, copied - since then you'd have two owners for the same object, and for that you need shared ownership.例如,这些指针不能被复制——从那时起,同一个对象将有两个所有者,为此您需要共享所有权。 But shared ownership is hard to get right, since there must be some protocols in place to ensure that you don't get cyclic references.但是共享所有权很难做到正确,因为必须有一些协议来确保您不会获得循环引用。 When building a tree, the "parent" node is the natural owner of a "child" node, and thus the unique ownership is precisely what's needed.在构建树时,“父”节点是“子”节点的自然所有者,因此唯一的所有权正是所需要的。

// complete example begins
#include <cassert>
#include <memory>

using Value = int;

struct Node {
  std::unique_ptr<Node> lhs_;
  std::unique_ptr<Node> rhs_;
  Value value_;
  explicit Node(int value) : value_(value) {}
};
// cont'd.

We also should have a Tree that owns the root node, and keeps the node count:我们还应该有一个拥有根节点的Tree ,并保持节点数:

// cont'd.
struct Tree {
  std::unique_ptr<Node> root_;
  int count_ = 0;
};
// cont'd.

When operating on such data structures, you frequently want to have access not only to the value of the node pointer, but also to the pointer itself so that you can modify it.在对此类数据结构进行操作时,您经常希望不仅能够访问节点指针的,而且还希望能够访问指针本身,以便您可以对其进行修改。 So, we need some sort of a "node reference" that mostly behaves like Node* would, but which, internally, also carries the address of the pointer to the node, so that eg the node could be replace d:因此,我们需要某种“节点引用”,它的行为主要类似于Node* ,但在内部,它也携带指向节点的指针的地址,以便例如可以replace节点 d:

// cont'd.
class NodeRef {
  std::unique_ptr<Node> *owner;
public:
  NodeRef() = delete;
  NodeRef(std::unique_ptr<Node> &o) : owner(&o) {}
  Node *get() const { return owner->get(); }
  // Use -> or * to access the underlying node
  Node *operator->() const { return get(); }
  Node &operator*() const { return *get(); }
  // In boolean contexts, it's true if the Node exists
  explicit operator bool() const { return bool(*owner); }
  // Replace the Node (if any) with some other one
  void replace(std::unique_ptr<Node> &&oldNode) {
    *owner = std::move(oldNode);
  }
  NodeRef &operator=(std::unique_ptr<Node> &val) {
    owner = &val;
    return *this;
  }
};
// cont'd.

NodeRef holds a pointer to the owner of the node (the owner is the owning pointer type std::unique_ptr ). NodeRef持有一个指向节点所有者的指针(所有者是拥有指针类型std::unique_ptr )。

The following are the ways that you can use NodeRef as-if it was Node* :以下是您可以像使用Node*一样使用NodeRef的方法:

NodeRef node = ...;
node->value_       // access to pointed-to Node using ->
(*node).value      // access to pointed-to Node using *
if (node) ...      // null check
node = otherNode;  // assignment from another node (whether owner or NodeRef)

And the following would be the way that NodeRef behaves similar to std::unique_ptr<Node> & , ie like a reference to the node owner, allowing you to alter the ownership:以下是NodeRef行为方式类似于std::unique_ptr<Node> & ,即类似于对节点所有者的引用,允许您更改所有权:

Tree tree;
NodeRef root = tree.root_; // reference the root of the tree
root.replace(std::make_unique<Node>(2)); // replace the root with a new node

Note that this code performs all the necessary memory allocation and deallocation thanks to the power of std::unique_ptr and move semantics.请注意,由于std::unique_ptr和移动语义的强大功能,此代码执行所有必要的内存分配和释放。 There are no new , delete , malloc nor free statements anywhere.任何地方都没有newdeletemallocfree语句。 And, also, the performance is on par with manual allocations - this code does not use any sort of garbage collection or reference counting.而且,性能与手动分配相当 - 此代码不使用任何类型的垃圾收集或引用计数。 std::unique_ptr is a tool that lets you leverage the compiler to write memory allocation and deallocation code for you, in a way that's guaranteed to be correct. std::unique_ptr是一种工具,可让您利用编译器以保证正确的方式为您编写内存分配和释放代码。

But, NodeRef is not an "observing" pointer, ie if the owner of the node it points to suddely disappears, then NodeRef becomes dangling.但是, NodeRef不是一个“观察”指针,即如果它指向的节点的所有者突然消失了,那么NodeRef就会变成悬空。 To do otherwise would have more overhead, and would require the use of some tracking pointers, eg shared_ptr and weak_ptr , or a bespoke solution - certainly out of scope here.否则会产生更多的开销,并且需要使用一些跟踪指针,例如shared_ptrweak_ptr ,或者定制的解决方案——当然超出了这里的范围。

And thus NodeRef fulfills the typical requirements that make the actual tree management code much easier to write, understand, and maintain with reduced potential for errors.因此, NodeRef满足了典型要求,这些要求使实际的树管理代码更易于编写、理解和维护,并减少了出错的可能性。 This approach facilitates code that is correct by design, ie where mistakes that would cause undefined behavior are mostly caught by the compiler, or impossible to write.这种方法有助于设计正确的代码,即会导致未定义行为的错误大多被编译器捕获,或者无法编写。

Let's see how would a binary node search look, using the types we introduced above:让我们看看使用我们上面介绍的类型进行二分节点搜索的样子:

// cont'd
// Finds the owner of a node that contains a given value,
// or the insertion point where the value would be
NodeRef find(Tree &tree, const Value &value)
{
  NodeRef node = tree.root_;
  while (node) {
    if (value < node->value_)
      node = node->lhs_;
    else if (node->value_ < value)
      node = node->rhs_;
    else
      break; // we found the value we need
  }
  return node;
}
// cont'd

First, let's note that while the returned node reference can be null, it doesn't mean that it's "useless".首先,让我们注意,虽然返回的节点引用可以为空,但这并不意味着它“无用”。 A NodeRef is never "completely" null, and must always refer to some node owner - that's why the default constructor is deleted, so you can't create an invalid NodeRef by mistake. NodeRef永远不会“完全”为空,并且必须始终引用某个节点所有者 - 这就是删除默认构造函数的原因,因此您不能错误地创建无效的NodeRef It is the node that can be null, not the underlying reference to the owning pointer to the node.它是可以为空的节点,而不是对指向该节点的拥有指针的底层引用。

Notice how similar the code is to a version that would use Node * , yet it is more powerful.请注意代码与使用Node *的版本有多么相似,但它更强大。 Since this version of find returns a NodeRef , we can use this reference to replace the node (or set it for the first time if it was null), whereas the signature Node *find(Node *root, const Value &value) would only give us access to the node itself, but not to its owner.由于这个版本的find返回一个NodeRef ,我们可以使用这个引用来替换节点(或者如果它为空则第一次设置它),而签名Node *find(Node *root, const Value &value)只会给出我们可以访问节点本身,但不能访问其所有者。 And, in case the node wasn't found, it would return a null pointer - nor bringing us any closer to knowing where to insert the new node, and discarding the work done to find such insertion point (!).并且,如果没有找到节点,它会返回一个空指针——也不会让我们更接近于知道在哪里插入新节点,放弃为找到这样的插入点所做的工作(!)。

NodeRef gives us a circumspect access to the parent node: it doesn't expose the entire parent node, but just the owning pointer which owns given node - and it's also more general than a "parent" node would be, since the owning pointer does not need to be even held by a Node type. NodeRef为我们提供了对父节点的谨慎访问:它不公开整个父节点,而仅公开拥有给定节点的拥有指针 - 而且它也比“父”节点更通用,因为拥有指针确实如此甚至不需要由Node类型持有。 And indeed, NodeRef works just fine when a node's owner is in the Tree class, or it could refer to a stand-alone pointer as well:事实上,当节点的所有者在Tree类中时, NodeRef工作得很好,或者它也可以引用一个独立的指针:

std::unique_ptr<Node> myNode;
NodeRef node = myNode;

// The two lines below are equivalent - both change the `myNode` owning pointer
node.replace(std::make_unique<Node>(42));
myNode = std::make_unique<Node>(42);

In principle, there could be a NodeRef &NodeRef::operator=(std::unique_ptr<Node> &&) , ie a way to move-assign the node itself, but this would hide the important fact that NodeRef doesn't really own the node, but only refers to some owner, and the replace method makes this more explicit: we are replacing the node held by the owner.原则上,可能有一个NodeRef &NodeRef::operator=(std::unique_ptr<Node> &&) ,即一种移动分配节点本身的方法,但这会隐藏一个重要的事实,即NodeRef并不真正拥有节点,但仅指某个所有者, replace方法使这一点更加明确:我们正在替换所有者持有的节点。

Now we can implement the function you sought: node removal.现在我们可以实现您寻求的功能:删除节点。 This function takes a NodeRef , and modifies the subtree at the root of that node, so that the original node is removed:此函数采用NodeRef ,并修改该节点根处的子树,以便删除原始节点:

// cont'd
// Removes the given node. Returns true if the node was removed, or false if
// there was nothing to remove
bool remove(NodeRef node)
{
  for (;;) {
    if (!node) return false; // the node is empty, nothing to do
  
    if (!node->lhs_) {
      // replace the node with its sole right child, if any
      node.replace(std::move(node->rhs_));
      return true;
    }
    else if (!node->rhs_) {
      // replace the node with its sole left child, if any
      node.replace(std::move(node->lhs_));
      return true;
    }
    else {
      // node has two children
      // 1. take on the largest value in the left subtree
      // oldValue is a *reference* to the value of the node being replaced
      Value &oldValue = node->value_;
      node = node->lhs_;
      while (node->rhs_) node = node->rhs_;
      // we found the node with a replacement value - substitute it for
      // the old value
      oldValue = std::move(node->value_);

      // 2. remove that child - continue the removal loop
      continue;
      // instead of continue, we could also do
      // remove(node);
      // return;
      // but by continuing we don't have recursion, and we levarage
      // the fact that the `node` references the correct node to remove
    }
  }
}
// cont'd

We std::move the values - this is not important at all when dealing with "simple" value types like integers, but would be important if, for example, the Value was a type that can only be moved but not copied, eg using Value = std::unique_ptr<SomeType>;我们std::move值 - 在处理像整数这样的“简单”值类型时,这根本不重要,但如果,例如, Value是一种只能移动但不能复制的类型,例如using Value = std::unique_ptr<SomeType>; . .

And now the helper that manages node removal in the Tree :现在是管理Tree节点删除的助手:

// cont'd
void remove(Tree &tree, const Value& value)
{
  auto node = find(tree, value);
  if (remove(node))
    -- tree.count_;
}
// cont'd

Instead of const Value &value we could have had int value , but this way it's a more generic approach that would work with other Value types.我们可以使用int value代替const Value &value ,但这样它是一种更通用的方法,可以与其他Value类型一起使用。

Node insertion is also fairly easy, since find already provides the insertion point where the value would be, were it to exist:节点插入也相当容易,因为find已经提供了值所在的插入点,如果它存在的话:

// cont'd
bool insert(Tree &tree, const Value& value)
{
  auto node = find(tree, value);
  if (node) {
    // Such a value already exists
    assert(node->value_ == value);
    return false;
  } else {
    // Insert new value
    node.replace(std::make_unique<Node>(value));
    ++ tree.count_;
    return true;
  }
}
// cont'd

If Value was a non-copyable type, then we'd need an insert signature that takes rvalue reference, ie bool insert(Tree &tree, Value &&value) .如果Value是不可复制的类型,那么我们需要一个接受右值引用的insert签名,即bool insert(Tree &tree, Value &&value)

Now you may ask: how would we "walk" the tree?现在你可能会问:我们将如何“走”树? In C++, the idiomatic way to deal with collections of items is via iterators, and then one can use so-called range-for .在 C++ 中,处理项目集合的惯用方法是通过迭代器,然后可以使用所谓的range-for The following example prints out the elements of a vector:以下示例打印出向量的元素:

std::vector<int> values{1,2,3,4,5};
for (int val : values)
  std::cout << val << "\n";

When iterating, or "walking" the tree, we need some "breadcrumbs" to leave behind us, so that we can find our way back up the tree.在迭代或“行走”树时,我们需要一些“面包屑”留在我们身后,以便我们可以找到返回树的路。 Those need to reference the node, as well as whether the node was visited or traversed:那些需要引用节点,以及节点是否被访问或遍历:

// cont'd
#include <functional>
#include <stack>
#include <vector>

// An entry in the node stack used to iterate ("walk") the tree
struct BreadCrumb {
  NodeRef node;
  bool visited = false; // was this node visited?
  bool traversedLeft = false; // was the left child descended into?
  bool traversedRight = false; // was the right child descended into?
  BreadCrumb(std::unique_ptr<Node> &owner) : node(owner) {}
  BreadCrumb(NodeRef node) : node(node) {}
  Node *operator->() const { return node.get(); }
  explicit operator bool() const { return bool(node); }
};
// cont'd

The "path" that we walk down the tree is kept on a stack dedicated for this purpose:我们沿着树走的“路径”保存在一个专门用于此目的的堆栈中:

// cont'd
// A stack holds the path to the current node
class NodeStack {
  // Top of stack is the current node
  std::stack<BreadCrumb, std::vector<BreadCrumb>> m_stack;
public:
  NodeStack() = default;
  NodeStack(NodeRef n) { if (n) m_stack.push(n); }
  
  bool empty() const { return m_stack.empty(); }
  // The breadcrumb that represents the top of stack, and thus the current node
  BreadCrumb &crumb() { return m_stack.top(); }
  const BreadCrumb &crumb() const { return m_stack.top(); }
  NodeRef node() { return crumb().node; }
  Node *node() const { return empty() ? nullptr : crumb().node.get(); }
  
  void push(NodeRef n) { m_stack.push(n); }

  // Visit and mark the node if not visited yet
  bool visit() {
    if (crumb().visited) return false;
    crumb().visited = true;
    return true;
  }
  // Descend one level via the left edge if not traversed left yet
  bool descendLeft() {
    if (crumb().traversedLeft) return false;
    crumb().traversedLeft = true;
    auto &n = crumb()->lhs_;
    if (n) m_stack.push(n);
    return bool(n);
  }
  // Descends one level via right edge if not traversed right yet
  bool descendRight() {
    if (crumb().traversedRight) return false;
    crumb().traversedRight = true;
    auto &n = crumb()->rhs_;
    if (n) m_stack.push(n);
    return bool(n);
  }
  // Ascends one level
  bool ascend() {
    m_stack.pop();
    return !empty();
  }
};
// cont'd

The tree traversal operations are abstracted away in the stack, so that the remaining code is higher level and devoid of such details.树遍历操作在堆栈中被抽象掉了,所以剩下的代码是更高级别的并且没有这些细节。

Now we can implement a node iterator that uses the stack to keep its trail of breadcrumbs:现在我们可以实现一个节点迭代器,它使用堆栈来保持面包屑的踪迹:

// cont'd
// Node Forward Iterator - iterates the nodes in given order
class NodeIterator {
  using Advancer = void (NodeIterator::*)();
  
  NodeStack m_stack; // Breadcrumb path to the current node
  Advancer m_advancer; // Method that advances to next node in chosen order
  Order m_order = Order::In;

public:
  NodeIterator() = default;
  // Dereferencing operators
  Node& operator*() { return *m_stack.node(); }
  Node* operator->() { return m_stack.node().get(); }
  // Do the iterators both point to the same node (or no node)?
  bool operator==(const NodeIterator &other) const {
    return m_stack.node() == other.m_stack.node();
  }
  bool operator==(decltype(nullptr)) const { return !bool(m_stack.node()); }
  bool operator!=(const NodeIterator &other) const { return m_stack.node(); }
  bool operator!=(decltype(nullptr)) const { return bool(m_stack.node()); }
  
  NodeIterator(NodeRef n, Order order = Order::In) : m_stack(n) {
    setOrder(order);
    if (n) operator++(); // Start the traversal
  }
  
  void setOrder(Order order) {
    if (order == Order::In)
      m_advancer = &NodeIterator::advanceInorder;
    else if (order == Order::Pre)
      m_advancer = &NodeIterator::advancePreorder;
    else if (order == Order::Post)
      m_advancer = &NodeIterator::advancePostorder;
    m_order = order;
  }

  NodeIterator &operator++() { // Preincrement operator
    assert(!m_stack.empty());
    std::invoke(m_advancer, this);
    return *this;
  }
  // No postincrement operator since it'd need to copy the stack and thus
  // be way too expensive to casually expose via postincrement.

  void advanceInorder();
  void advancePreorder();
  void advancePostorder();
  
  bool goLeft() { return m_stack.descendLeft(); }
  bool goRight() { return m_stack.descendRight(); }
};
// cont'd

Remember the stack?还记得堆栈吗? It lets us describe the in-, pre- and post-order traversal rather succinctly:它让我们可以相当简洁地描述 in-, pre- and post-order 遍历:

// cont'd
void NodeIterator::advanceInorder() {
  for (;;) {
    if (m_stack.descendLeft())
      continue;
    if (m_stack.visit())
      break;
    if (m_stack.descendRight())
      continue;
    if (m_stack.ascend())
      continue;
    assert(m_stack.empty());
    break;
  }
}

void NodeIterator::advancePreorder() {
  for (;;) {
    if (m_stack.visit())
      break;
    if (m_stack.descendLeft())
      continue;
    if (m_stack.descendRight())
      continue;
    if (m_stack.ascend())
      continue;
    assert(m_stack.empty());
    break;
  }
}

void NodeIterator::advancePostorder() {
  for (;;) {
    if (m_stack.descendLeft())
      continue;
    if (m_stack.descendRight())
      continue;
    if (m_stack.visit())
      break;
    if (m_stack.ascend())
      continue;
    assert(m_stack.empty());
    break;
  }
}
// cont'd

And now we'd want some easy way to use this iterator when we'd wish to iterate a tree rooted in some node:现在,当我们希望迭代以某个节点为根的树时,我们需要一些简单的方法来使用这个迭代器:

// cont'd
class TreeRangeAdapter {
  NodeRef m_root;
  Order m_order;
public:
  TreeRangeAdapter(NodeRef root, Order order) :
    m_root(root), m_order(order) {}
  NodeIterator begin() const { return {m_root, m_order}; }
  constexpr auto end() const { return nullptr; }
};

auto inOrder(NodeRef node) { return TreeRangeAdapter(node, Order::In); }
auto preOrder(NodeRef node) { return TreeRangeAdapter(node, Order::Pre); }
auto postOrder(NodeRef node) { return TreeRangeAdapter(node, Order::Post); }
// cont'd

And how would all that work?这一切将如何运作? This is but a simple example of filling up a tree, and in-order traversal:这只是一个简单的填充树和中序遍历的例子:

// cont'd
#include <iostream>
#include <cstdlib>

int main() {
  Tree tree;
  for (int i = 0; i < 10; ++i) insert(tree, rand() / (RAND_MAX/100));
  
  for (auto &node : inOrder(tree.root_)) {
    std::cout << node.value_ << " ";
  }
  std::cout << "\n";
}
// complete example ends

Output:输出:

19 27 33 39 55 76 78 79 84 91 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM