Excluir um nó da árvore binária de busca em C++

Jinku Hu 12 outubro 2023
Excluir um nó da árvore binária de busca em C++

Este artigo irá explicar como implementar uma função para excluir um nó na estrutura de dados da árvore binária de busca C++.

Excluir nó na árvore binária de busca em C++

Uma árvore binária de busca é um tipo de árvore binária que armazena um valor-chave em cada nó. Essa chave é usada para construir uma árvore ordenada de forma que a chave de cada nó seja maior do que todas as chaves em sua subárvore esquerda e menor que as chaves em sua subárvore direita.

Cada nó geralmente inclui dois ponteiros para os nós left e right, mas também adicionamos outro ponteiro para denotar o nó pai, pois é mais fácil implementar a função de membro remove.

Observe que a implementação da árvore binária de busca a seguir inclui apenas o mínimo das funções de membro para demonstrar uma operação de remoção de nó.

A classe BinSearchTree é capaz de armazenar apenas tipos int como valores-chave. A maioria das funções, exceto remove, utiliza recursão, portanto, fornecemos funções-membro private correspondentes que são chamadas internamente. Geralmente, remover um nó da árvore é uma operação mais complexa do que inserir e pesquisar, pois envolve vários cenários.

O primeiro e mais simples cenário é quando precisamos remover um nó sem filhos (conseqüentemente referido como nó folha). Os nós folha podem ser desalocados e nullptr atribuído ao ponteiro correspondente de seu pai.

O segundo caso é remover o nó com apenas um filho. O último pode ser resolvido conectando o pai do alvo ao seu filho, e então podemos desalocar a memória associada.

#include <iostream>

using std::cerr;
using std::cout;
using std::endl;
using std::string;

struct BSTreeNode {
  int key{};
  BSTreeNode *parent{};
  BSTreeNode *left{};
  BSTreeNode *right{};

} typedef BSTreeNode;

class BinSearchTree {
 public:
  BinSearchTree() {
    root = nullptr;
    size = 0;
  };
  BinSearchTree(std::initializer_list<int> list);

  void insert(int k);
  BSTreeNode *find(int k);
  int remove(int k);
  void print();
  size_t getSize() const;

  ~BinSearchTree();

 private:
  BSTreeNode *root;
  size_t size;

  void freeNodes(BSTreeNode *&rnode);
  void printTree(BSTreeNode *node);
  void insertNode(BSTreeNode *&rnode, int k, BSTreeNode *pnode);
  BSTreeNode **findNode(BSTreeNode *&rnode, int k);
};

BinSearchTree::BinSearchTree(std::initializer_list<int> list) {
  root = nullptr;
  size = 0;

  for (const auto &item : list) {
    insertNode(root, item, nullptr);
  }
}

BinSearchTree::~BinSearchTree() { freeNodes(root); }

void BinSearchTree::freeNodes(BSTreeNode *&rnode) {
  if (rnode != nullptr) {
    freeNodes(rnode->left);
    freeNodes(rnode->right);
    delete rnode;
  }
}

BSTreeNode *BinSearchTree::find(const int k) { return *findNode(root, k); }

BSTreeNode **BinSearchTree::findNode(BSTreeNode *&rnode, const int k) {
  if (rnode == nullptr) return nullptr;

  if (k == rnode->key) return &rnode;

  if (k < rnode->key)
    return findNode(rnode->left, k);
  else
    return findNode(rnode->right, k);
}

void BinSearchTree::print() {
  if (size > 0)
    printTree(root);
  else
    cout << "tree is empty!" << endl;
}

void BinSearchTree::printTree(BSTreeNode *rnode) {
  if (rnode != nullptr) {
    printTree(rnode->left);
    cout << rnode->key << "; ";
    printTree(rnode->right);
  }
}

void BinSearchTree::insert(const int k) { insertNode(root, k, nullptr); }

void BinSearchTree::insertNode(BSTreeNode *&rnode, const int k,
                               BSTreeNode *pnode) {
  if (rnode == nullptr) {
    rnode = new BSTreeNode;
    rnode->key = k;
    rnode->parent = pnode;
    rnode->left = nullptr;
    rnode->right = nullptr;
    size++;
  } else {
    if (k < rnode->key)
      insertNode(rnode->left, k, rnode);
    else if (k == rnode->key)
      return;
    else
      insertNode(rnode->right, k, rnode);
  }
}

size_t BinSearchTree::getSize() const { return size; }

int BinSearchTree::remove(const int k) {
  auto ret = findNode(root, k);
  if (ret == nullptr) return -1;

  if (size == 1) {
    auto tmp = root;
    root = nullptr;
    delete tmp;
    size--;
    return 0;
  }

  if ((*ret)->left == nullptr && (*ret)->right == nullptr) {
    auto tmp = *ret;
    if ((*ret)->key < (*ret)->parent->key)
      (*ret)->parent->left = nullptr;
    else
      (*ret)->parent->right = nullptr;
    delete tmp;
    size--;
    return 0;
  }

  if ((*ret)->left != nullptr && (*ret)->right != nullptr) {
    auto leftmost = (*ret)->right;

    while (leftmost && leftmost->left != nullptr) leftmost = leftmost->left;

    (*ret)->key = leftmost->key;

    if (leftmost->right != nullptr) {
      leftmost->right->parent = leftmost->parent;
      auto tmp = leftmost->right;
      *leftmost = *leftmost->right;
      leftmost->parent->left = leftmost;
      delete tmp;
    } else {
      leftmost->parent->right = nullptr;
      delete leftmost;
    }

    size--;
    return 0;
  } else {
    if ((*ret)->left != nullptr) {
      auto tmp = *ret;
      *ret = (*ret)->left;
      (*ret)->parent = tmp->parent;
      delete tmp;
    } else {
      auto tmp = *ret;
      *ret = (*ret)->right;
      (*ret)->parent = tmp->parent;
      delete tmp;
    }

    size--;
    return 0;
  }
}

int main() {
  BinSearchTree bst = {6, 5, 11, 3, 2, 10, 12, 4, 9};

  cout << "size of bst = " << bst.getSize() << endl;
  bst.print();
  cout << endl;

  bst.insert(7);
  bst.insert(8);
  cout << "size of bst = " << bst.getSize() << endl;
  bst.print();
  cout << endl;

  bst.remove(6);
  bst.remove(2);
  bst.remove(12);
  cout << "size of bst = " << bst.getSize() << endl;
  bst.print();
  cout << endl;

  return EXIT_SUCCESS;
}
size of bst = 9
2; 3; 4; 5; 6; 9; 10; 11; 12;
size of bst = 11
2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12;
size of bst = 8
3; 4; 5; 7; 8; 9; 10; 11;

O cenário mais complicado é quando o nó de destino tem dois filhos. Nesse caso, precisamos conectar corretamente os nós, bem como reter a ordem dos elementos conforme especificado para a estrutura da árvore binária de busca. Precisamos substituir o nó de destino pela menor chave e parte da subárvore direita do destino.

O nó com a menor chave é encontrado no lugar mais à esquerda. Portanto, devemos percorrer a subárvore direita até chegarmos a este nó. Assim que o nó for encontrado, podemos atribuir sua chave ao nó de destino e, em seguida, tentar remover a anterior como se fosse um nó com um único filho. O último está implícito no fato de que este nó é o mais à esquerda na subárvore fornecida. Assim, só pode ter um filho right ou nenhum filho.

Esses três cenários são implementados em bloco if...else separado na função de membro remove, mas também incluímos código adicional para verificar alguns casos esquivos, como quando o elemento não é encontrado na árvore ou o último nó é removido. Observe que a função remove também pode ser implementada de maneira recursiva.

Autor: Jinku Hu
Jinku Hu avatar Jinku Hu avatar

Founder of DelftStack.com. Jinku has worked in the robotics and automotive industries for over 8 years. He sharpened his coding skills when he needed to do the automatic testing, data collection from remote servers and report creation from the endurance test. He is from an electrical/electronics engineering background but has expanded his interest to embedded electronics, embedded programming and front-/back-end programming.

LinkedIn Facebook

Artigo relacionado - C++ Data Structure