Сомнения в классах при реализации бинарных деревьев поиска

Я изучаю общие двоичные деревья поиска (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
}

Теперь дело в том, что операция insert дерева AVL в основном такая же. псевдокод, представленный в примечаниях, выглядит следующим образом:

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 
}

Я немного озадачен в этот момент, я знаю, что это всего лишь псевдокод, но мне было интересно, есть ли способ повторно использовать операцию insert, уже предоставленную для Node. Использование шаблонной специализации означало бы просто написание другой специализации insert<AVLNode> для 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 и потомков (т. е. они должны быть указателями на Node внутри Node и на AVLNode внутри AVLNode).

Есть ли способ избежать переписывания одного и того же кода?


person luigi    schedule 09.09.2020    source источник
comment
Вы думали об оп? Похоже, что узел AVL — это особый узел. Вы можете иметь узел AVL, расширяющий None, а затем использовать общий узел везде, где это возможно.   -  person Mitch    schedule 09.09.2020
comment
Да, прочитайте последний абзац.   -  person luigi    schedule 09.09.2020
comment
Дело в том, что есть 3 указателя Node* в Node, который должен быть AVLNode* в AVLNode. Если я просто переопределю их в AVLNode, дочерний класс будет иметь три бесполезных указателя Node* в AVLNode.   -  person luigi    schedule 09.09.2020
comment
Можете ли вы показать код insert, который вы не хотите повторять? Я не понимаю, насколько много дублирования. Если код в основном такой же, вы можете использовать шаблоны.   -  person cigien    schedule 09.09.2020
comment
Не беспокойтесь о наследовании или упрощении. Вы потратите больше усилий, чем получите выгоды. Поскольку структуры независимы, оставьте их такими.   -  person Thomas Matthews    schedule 09.09.2020
comment
@cigien Я добавил код для insert. Повторение кода не имеет большого значения, просто AVLNode может быть определено как дочернее для Node, и мне было интересно, можно ли использовать этот факт для повторного использования кода для insert. Это также отвечает на @ThomasMatthews, я уже реализовал это с помощью шаблонов, мне просто интересно.   -  person luigi    schedule 09.09.2020
comment
Другая структура, но та же идея: отвечает ли это на ваш вопрос? Если "A" имеет переменную-член "x" типа "A*", а "B" наследуется от "A", как переопределить "x" как "B*" внутри "B"?   -  person HTNW    schedule 09.09.2020
comment
Спасибо @HTNW, это частично отвечает на мои сомнения по поводу указателя. Я проверю это и в конечном итоге обновлю   -  person luigi    schedule 09.09.2020


Ответы (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_ptr или shared_ptr.

person sqrtroot    schedule 10.09.2020