【问题标题】:Doubts on classes when implementing binary search trees实现二叉搜索树时对类的怀疑
【发布时间】:2020-09-09 16:35:25
【问题描述】:

我正在研究一些包含实现伪代码的笔记上的通用二叉搜索树 (BST) 和 AVL 树 (AVL)。我对它们的一些实现细节有些疑惑。

BST 基于下面的struct Node

struct Node{
  int key;
  Node* parent;
  Node* left;
  Node* right;

  //constructors
}

//methods

AVL 版本基本相同,增加了几个字段用于平衡树(为了清楚起见,我将其称为 AVLNode,但注释上没有这种区别):

struct AVLNode{
  int key;
  int height;
  int size;
  AVLNode* parent;
  AVLNode* leftchild;
  AVLNode* rightchild;

  //constructors
}

//methods

两棵树之间的很多操作是相同的,我可以轻松地使用模板以便在两棵树上重用它们。但是,请考虑插入新节点的操作insert。 BST 的代码类似于

//Insert node with key k in tree with root R
void insert(const int& k, Node* root){
  Node* N=find(k, root);         //finds where to insert the node
  if (N->key>k)
    N->leftchild=new Node(k,N);  //inserts as a left child
  else
    N->rightchild=new Node(k,N); //inserts as a right child
}

现在,重点是AVL树的insert操作基本相同。笔记中呈现的伪代码如下:

void avlInsert(int k, AVLNode* R){
  insert(k,R);          //same operations as for Nodes, shown above
  AVLNode* N=find(x,R); //find node inserted (generic operation for BST)
  rebalance(N);         //perform balancing operations specific to AVL trees 
}

此时我有点困惑,我知道上面只是一个伪代码,但我想知道是否有办法重用已经为Node 提供的操作insert。使用模板特化只是意味着为 AVLNode 写一个不同的特化 insert<AVLNode>,所以这不是我所指的。

我认为一种方法是将AVLNode 定义为Node 的子类,然后使用类似

struct AVLNode : Node {
  //implementation
}

void avlInsert(int k, AVLNode* R){
  Node *root=R;
  insert(k,root);
  AVLNode* N=find(x,R);
  rebalance(N);
}

但我不太确定这会起作用,而且我不知道如何管理指向 parent 和孩子的指针(即它们必须是指向 NodeNodeAVLNode 的指针在AVLNode).

有没有办法避免重写相同的代码?

【问题讨论】:

  • 你考虑过oop吗?看起来 AVL 节点是一个特殊的节点。您可以让 AVL 节点扩展无,然后尽可能使用通用节点
  • 是的,请阅读最后一段。
  • 重点是Node中有3个指针Node*,必须是AVLNode中的AVLNode*。如果我只是在AVLNode 中重新定义它们,那么子类将在AVLNode 中拥有三个无用的Node* 指针。
  • 您能否显示您不想重复的insert 代码?我不清楚有多少重复。如果代码基本相同,或许可以使用模板。
  • 不要为继承或简化而烦恼。你会付出比你得到的好处更多的努力。由于结构是独立的,请保持这种方式。

标签: c++ class templates binary-search-tree avl-tree


【解决方案1】:

您可以在此处使用 CRTP。这将允许您在基类中创建左右和父节点。例如考虑这样的事情:

template<typename T>
struct BaseNode{
  int key;
  T* parent;
  T* left;
  T* right;
};

struct AVLNode : public BaseNode<AVLNode>{
  int height;
  int size;
  AVLNode(const int&k, AVLNode*root){};
  AVLNode(){};
};
struct Node : public BaseNode<Node>{
    Node(const int&k, Node*root){};
    Node(){};
};

template<typename T>
T* find(const int& k, T* root){return root;};

template<typename T>
void insert(const int& k, T* root){
  T* N=find(k, root);         //finds where to insert the node
  if (N->key>k)
    N->left=new T(k,N);  //inserts as a left child
  else
    N->right=new T(k,N); //inserts as a right child
}

void test(){
    AVLNode avl_root;
    Node node_root;
    insert(42, &avl_root);
    insert(42, &node_root);
}

缺点是编译器会生成不必要的代码。因为它为每种类型创建了一个新的插入函数。这对您来说可能不是问题,但值得考虑。生成代码见godbolt

顺便说一句。 拜托拜托不要使用原始指针和新建和删除。您将遇到如此多的内存泄漏,特别是如果指针因为其父级被删除而“丢失”。考虑使用像unique_ptrshared_ptr 这样的智能指针

【讨论】:

    猜你喜欢
    • 2015-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-24
    • 2010-11-22
    • 2013-05-03
    • 2019-02-20
    • 1970-01-01
    相关资源
    最近更新 更多