Estrutura de dados JavaScript e notas de estudo de algoritmo (in)

Sete, JavaScript implementa estrutura de árvore (1)

1. Introdução à estrutura em árvore

1.1 Compreensão simples da estrutura da árvore

Vantagens da estrutura de árvore sobre array/lista encadeada/tabela de hash:

variedade:

  • Vantagens: pode ser acessado por valor subscrito , alta eficiência;
  • Desvantagens: Ao pesquisar dados, você precisa classificar os dados primeiro para gerar uma matriz ordenada para melhorar a eficiência da pesquisa; e ao inserir e excluir elementos, é necessário um grande número de operações de deslocamento ;

lista encadeada:

  • Vantagens: as operações de inserção e exclusão de dados são muito eficientes;
  • Desvantagens: A eficiência da pesquisa é baixa e precisa ser pesquisada desde o início até que os dados de destino sejam encontrados; quando é necessário inserir ou excluir dados no meio da lista vinculada, a eficiência de inserir ou excluir não é alta .

Tabela de hash:

  • Vantagens: a eficiência de inserção/consulta/exclusão da tabela hash é muito alta;
  • Desvantagens: A taxa de utilização do espaço não é alta e muitas unidades na matriz subjacente não são usadas; e os elementos na tabela de hash estão fora de ordem e os elementos na tabela de hash não podem ser percorridos em uma ordem fixa; e o hash não pode ser encontrado rapidamente Esses valores especiais são os valores máximos ou mínimos na tabela.

estrutura da árvore:

  • Vantagens: A estrutura de árvore combina as vantagens das três estruturas acima e também compensa suas deficiências (embora a eficiência não seja necessariamente maior do que elas), como os dados na estrutura de árvore são ordenados, a eficiência de pesquisa é alta; a taxa de utilização do espaço alta; e pode obter rapidamente os valores máximos e mínimos, etc.

Em geral: cada estrutura de dados tem seus próprios cenários de aplicação específicos

estrutura da árvore:

  • Árvore (Tree) : Uma coleção finita de n (n ≥ 0) nós . Quando n = 0, é chamada de árvore vazia .

Para qualquer árvore não vazia (n > 0), tem as seguintes propriedades:

  • Existe um nó especial chamado Raiz no número, representado por **r**;
  • O restante dos nós pode ser dividido em m (m > 0) conjuntos finitos T1, T2, ..., Tm que não se cruzam, e cada conjunto em si é uma árvore, chamada de subárvore da árvore original (SubTree ) .

Termos comuns para árvores:

b692456e722440f98996cba99e56d23d.png

  • Grau do nó (Degree) : o número de subárvores do nó , por exemplo, o grau do nó B é 2;
  • Grau da árvore : o grau máximo de todos os nós da árvore , conforme a figura acima, o grau da árvore é 2;
  • Nó folha (Leaf) : um nó com grau 0 (também chamado de nó folha), como H, I, etc. na figura acima;
  • Nó pai (Parent) : Um nó cujo grau não é 0 é chamado de nó pai, conforme mostrado na figura acima, o nó B é o nó pai dos nós D e E;
  • Nó filho (Filho) : Se B é o nó pai de D, então D é o nó filho de B;
  • Irmãos : nós com o mesmo nó pai são nós irmãos entre si, como B e C na figura acima, D e E são nós irmãos entre si;
  • Caminho e comprimento do caminho : Um caminho refere-se à passagem de um nó para outro nó.O número de arestas contidas no caminho é chamado de comprimento do caminho.Por exemplo, o comprimento do caminho de A->H é 3;
  • Nível do nó (Level) : É estipulado que o nó raiz está no nível 1 , e o nível de qualquer outro nó é o nível de seu nó pai mais 1 . Por exemplo, os níveis dos nós B e C são 2;
  • Profundidade da árvore (Depth) : O nível máximo de todos os nós da árvore é a profundidade da árvore, conforme mostrado na figura acima, a profundidade da árvore é 4;

1.2. Representação da estrutura da árvore

  • A representação mais comum:

cf3b5235f97746d894e14e8da754df11.png

Conforme mostrado na figura, a composição da estrutura em árvore é semelhante à de uma lista encadeada, que é composta por nós conectados um a um. No entanto, dependendo do número de nós filhos de cada nó pai, o número de referências necessárias para cada nó pai também é diferente.

A desvantagem desse método é que não podemos determinar o número de referências a um nó.

  • Notação filho-irmão:

6619e6dc8fe44de9ae31e4131f65c7ab.png

Este método de representação pode registrar completamente os dados de cada nó, como:

// Nó A

Nó{

  //armazenando dados

  this.data = dados

  // Registra uniformemente apenas os nós filhos à esquerda

  this.leftChild = B

  // Registra uniformemente apenas o primeiro nó irmão à direita

  this.rightSibling = null

}

// Nó B

Nó{

  this.data = dados

  this.leftChild = E

  this.rightIrmão = C

}

// Nó F

Nó{

  this.data = dados

  this.leftChild = null

  this.rightSibling = null

}

A vantagem dessa notação é que o número de referências em cada nó é determinístico.

  • rotação de notação filho-irmão

O seguinte é uma estrutura de árvore composta de notação filho-irmão:

5251e20441d546b4904ddc5c5d4c2065.png

 Depois de girá-lo 45° no sentido horário:

be5381ebe278417cafc6e4e4300df21e.png

Isso se torna uma árvore binária, da qual podemos concluir que qualquer árvore pode ser simulada por uma árvore binária .

Em segundo lugar, a árvore binária

2.1. Introdução à árvore binária

O conceito de uma árvore binária : Se cada nó na árvore só puder ter no máximo dois nós filhos , tal árvore é chamada de árvore binária ;

As árvores binárias são muito importantes não apenas por sua simplicidade, mas também porque quase todas as árvores podem ser representadas como árvores binárias.

A composição da árvore binária:

  • A árvore binária pode estar vazia, ou seja, não há nós;
  • Se a árvore binária não estiver vazia, ela consiste em um nó raiz e duas árvores binárias disjuntas chamadas de subárvore esquerda TL e subárvore direita TR;

Cinco formas de árvore binária:

dd04e6487e614172a2d76a2f0d4771c1.png

Características de uma árvore binária:

  • O nó máximo da árvore da i-ésima camada de uma árvore binária é: 2(i-1), i >= 1;
  • O número total máximo de nós em uma árvore binária com profundidade k é: 2k - 1, k >= 1;
  • Para qualquer árvore binária não vazia, se n0 representa o número de nós folha e n2 representa o número de nós não folha com grau 2, então os dois satisfazem a relação: n0 = n2 + 1; como mostrado na figura abaixo: H, E, I, J, G são nós folha, o número total é 5; A, B, C, F são nós não folha com grau 2, o número total é 4; satisfaz a lei de n0 = n2 + 1.

bdf12cb3c9bc485e9bee7ad6bd37468d.png

 2.2. Árvores binárias especiais

árvore binária perfeita

A Árvore Binária Perfeita (Perfect Binary Tree) também é chamada de Full Binary Tree (Full Binary Tree). .

a2d86417e05741aca53ac565c957c2c7.png

Árvore binária completa :

  • Com exceção da última camada da árvore binária, o número de nós em cada camada atingiu o valor máximo;
  • Além disso, os nós folha da última camada existem continuamente da esquerda para a direita, e apenas alguns nós folha à direita estão faltando;
  • Uma árvore binária perfeita é uma árvore binária especial completa;

c78ab933278e41c08faf0352a294cece.png

Na imagem acima, como H não tem seu filho direito, não é uma árvore binária completa.

2.3. Armazenamento de dados de árvore binária

Métodos comuns de armazenamento de árvore binária são arrays e listas encadeadas :

Use uma matriz:

  • Árvore binária completa : armazene dados de cima para baixo e da esquerda para a direita.

601c8ead7af542dca946615e9b2495dc.png

Ao usar armazenamento de matriz, também é muito conveniente buscar dados: o número de série do nó filho esquerdo é igual ao número de série do nó pai * 2 , e o número de série do nó filho direito é igual ao número de série do nó pai * 2 + 1 .

  • Árvore binária incompleta: A árvore binária incompleta precisa ser convertida em uma árvore binária completa para ser armazenada de acordo com o esquema acima, o que desperdiçará muito espaço de armazenamento.

b9f1fff6e84c4cce9426d570328b40bd.png

usar lista encadeada

O método de armazenamento mais comum de uma árvore binária é uma lista vinculada : cada nó é encapsulado em um nó e o nó contém dados armazenados, referências ao nó esquerdo e referências ao nó direito.

daba40c8da6b4c819a59d71b8c5ce25f.png

 3. Árvore de busca binária

3.1. Entendendo a árvore de busca binária

Árvore de pesquisa binária ( BST , Binary Search Tree), também conhecida como árvore de classificação binária e árvore de pesquisa binária .

Uma árvore de pesquisa binária é uma árvore binária que pode estar vazia;

Se não estiver vazio, as seguintes propriedades serão atendidas :

  • Condição 1: Todos os valores-chave da subárvore esquerda não vazia são menores que os valores-chave de seu nó raiz.
  • Condição 2: Todos os valores-chave da subárvore direita não vazia são maiores que os valores-chave de seu nó raiz;
  • Condição 3: As próprias subárvores esquerda e direita também são árvores binárias de busca;

41c33542419b41d1b7394bd4bc4b8d6d.png

Oito, JavaScript implementa estrutura de árvore (2)

1. Encapsulamento da árvore de busca binária

Propriedades básicas de uma árvore de busca binária:

Conforme mostrado na figura: a árvore de busca binária tem quatro atributos mais básicos: a raiz apontando para o nó (root), a chave (chave) no nó , o ponteiro esquerdo (direita) e o ponteiro direito (direita).

89ed012121f440e9a60fc42acd9171c4.png

Portanto, além de definir o atributo raiz na árvore binária de busca, também deve ser definida uma classe interna do nó, que contém três atributos de esquerda, direita e chave em cada nó:

// Encapsula a árvore de busca binária

    função BinarySearchTree(){

      // classe interna do nó

      Nó de função(chave){

        this.key = chave

        this.left = null

        this.right = null

      }

      //Atributos

      this.root = null

  }

Operações comuns em árvores de pesquisa binária :

  • insert(key): Insere uma nova chave na árvore;
  • search(key): Encontre uma chave na árvore e retorne true se o nó existir; false se não existir;
  • inOrderTraverse: percorre todos os nós através da travessia inorder;
  • preOrderTraverse: atravessa todos os nós através da travessia de pré-ordem;
  • postOrderTraverse: percorre todos os nós por meio de travessia de pós-ordem;
  • min: retorna o menor valor/chave da árvore;
  • max: retorna o maior valor/chave na árvore;
  • remove(chave): remove uma chave da árvore;

1. Insira os dados

Ideias de implementação:

  • Primeiro crie um objeto de nó de acordo com a chave recebida;
  • Em seguida, julgue se o nó raiz existe, caso contrário, passe: this.root = newNode, use diretamente o novo nó como o nó raiz da árvore de pesquisa binária.
  • Se houver um nó raiz, redefina um método interno insertNode() para localizar o ponto de inserção.

//inserir método: método exposto a usuários externos

  BinarySearchTree.prototype.insert = function(key){

        //1. Cria um nó de acordo com a chave

        let newNode = new Node(chave)

        //2. Determina se o nó raiz existe

        if (this.root == nulo) {

          this.root = newNode

          // quando o nó raiz existe

        }outro {

          this.insert(this.root, newNode)

        }

      }

A ideia de implementação do método interno insert() :

De acordo com a comparação dos dois nós de entrada, ele continua procurando a posição de inserção adequada do novo nó até que o novo nó seja inserido com sucesso.

Quando newNode.key < node.key, olhe para a esquerda:

  • Caso 1: Quando o nó não tem nó filho esquerdo, insira diretamente:
  • Caso 2: quando o nó tiver um nó filho esquerdo, chame insert() recursivamente até que new seja inserido com sucesso sem um nó filho esquerdo e essa situação não seja mais atendida, então insert() não será mais chamado e a recursão será interrompida.

b5ef2e56e08c489baca0c3d11a46803c.png

Quando newNode.key >= node.key pesquisa à direita, semelhante à pesquisa à esquerda:

  • Caso 1: Quando o nó não possui um nó filho correto, insira diretamente:
  • Caso 2: Quando o nó tem um nó filho correto, insert() ainda é chamado recursivamente até que o nó passado para o método insert não tenha nenhum nó filho correto e seja inserido com sucesso em newNode:

cc158f20575943ecbcdffe0666d1f13a.png

 implementação do código insert():

 //O método de inserção usado internamente: usado para comparar se o nó é inserido da esquerda ou da direita


  BinarySearchTree.prototype.insert= function(node, newNode){         //Quando newNode.key < node.key procura à esquerda         if(newNode.key < node.key){ //Caso 1: o nó não possui nenhum nó filho esquerdo, insira diretamente           if (node.left == null) {             node.left = newNode //case 2: node has a left child node, chame insert() recursivamente           }else{             this.insert(node.left, newNode)           }








 //Quando newNode.key >= node.key olha para a direita
        }else{           //Caso 1: o nó não tem nenhum nó filho direito, insira diretamente           if(node.right == null){             node.right == newNode           //Caso 2 : o nó tem um nó filho certo, ainda chama insert() recursivamente           }else{             this.insert(node.right, newNode)           }         }       }








2. Percorra os dados

Esta travessia funciona para todas as árvores binárias. Os três métodos comuns de passagem de árvore binária são:

  • travessia de pré-ordem;
  • Travessia inorder;
  • travessia pós-ordem;

Há também a travessia da ordem da camada, que é menos usada.

2.1. Passagem de pré-encomenda

O processo de travessia de pré-ordem é:

  • Primeiro, percorra o nó raiz;
  • Em seguida, percorra sua subárvore esquerda;
  • Finalmente, percorra sua subárvore direita;

ce44a3a2687945eab53508f3725639ee.png

Conforme mostrado na figura acima, a ordem de passagem dos nós da árvore binária é: A -> B -> D -> H -> I -> E -> C -> F -> G.

Código:

      //Traversal de pré-ordem
      //Incorpore uma função de manipulador para facilitar o processamento da chave obtida
      BinarySearchTree.prototype.preOrderTraversal = function(handler){         this.preOrderTraversalNode(this.root, handler)       }

      //Encapsula o método interno para percorrer um nó

 BinarySearchTree.prototype.preOrderTraversalNode = function(node,handler){         if (node ​​!= null) {           //1. Processe o nó passado           handler(node.key)           //2. Atravesse os nós na subárvore esquerda this. preOrderTraversalNode( node.left, handler)           //3. Atravesse os nós na subárvore direita           this.preOrderTraversalNode(node.right, handler)         }       }








2.2. Travessia em ordem

Ideias de implementação:

  • Primeiro, percorra sua subárvore esquerda;
  • Em seguida, percorra o nó raiz (pai);
  • Finalmente, percorra sua subárvore direita;

06883e8ff0c8499fab86c4fd1eaa1659.png

A ordem dos nós de saída deve ser: 3 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> 12 -> 13 -> 14 -> 15 -> 18 -> 20 -> 25 .

Código:

// Travessia em ordem

 BinarySearchTree.prototype.midOrderTraversal = function(manipulador){

 this.midOrderTraversalNode(this.root, manipulador)

      }

 BinarySearchTree.prototype.midOrderTraversalNode = function(node, handler){

        if (nó != nulo) {

          //1. Percorrer os nós na subárvore esquerda

 this.midOrderTraversalNode(node.left, manipulador)

          //2. Nó de processamento

          handler(node.key)

          //3. Percorrer os nós na subárvore direita

 this.midOrderTraversalNode(node.right, manipulador)

        }

      }

2.3. Percurso pós-ordem

Ideias de implementação:

  • Primeiro, percorra sua subárvore esquerda;
  • Em seguida, percorra sua subárvore direita;
  • Finalmente, o nó raiz (pai) é percorrido;

d874a7d90a1940b4a15a9d56a251d928.png

A ordem dos nós de saída deve ser: 3 -> 6 -> 5 -> 8 -> 10 -> 9 -> 7 -> 12 -> 14 -> 13 -> 18 -> 25 -> 20 -> 15 -> 11 .

Código:

      //后序遍历
 BinarySearchTree.prototype.postOrderTraversal = function(handler){         this.postOrderTraversalNode(this.root, handler)       }

 BinarySearchTree.prototype.postOrderTraversalNode = function(node, handler){         if (node ​​!= null) {           //1. Atravesse os nós na subárvore esquerda           this.postOrderTraversalNode(node.left, handler)           //2. Atravesse o Nó da subárvore direita           neste.postOrderTraversalNode(node.right, handler)



          

          //3. Processando nó
          handler(node.key)
        }
      }

3. Encontre dados

3.1. Encontre o valor máximo e mínimo

Encontrar o maior valor na árvore de pesquisa binária é muito simples, o valor mínimo está na extremidade esquerda da árvore de pesquisa binária e o valor máximo está na extremidade direita da árvore de pesquisa binária . Você só precisa pesquisar esquerda/direita o tempo todo para obter o máximo valor, conforme mostrado na figura abaixo:

18b8c3bc62824a399d5946f3f8e53348.png

Código:

      //Encontre o valor máximo
      BinarySearchTree.prototype.max = function () {         //1. Obtenha o nó raiz         let node = this.root         //2. Defina a chave para salvar o valor do nó         let key = null         //3. Pesquise continuamente para a direita, até que o nó seja nulo         while (node!= null) {           key = node.key           node = node.right         }         return key       }










      //Encontre o valor mínimo
      BinarySearchTree.prototype.min = function(){          //1. Obtenha o nó raiz          let node = this.root         //2. Defina a chave para salvar o valor do nó         let key = null         //3. Continue procurando à esquerda, por sua vez, até que o nó seja nulo         while (nó! = nulo) {           chave = nó.chave           nó = nó.esquerda         }         tecla de retorno       }










3.2. Encontre um valor específico

A partir do nó raiz, compare o valor da chave do nó a ser pesquisado. Se node.key < root , procure à esquerda. Se node.key > root , pesquise à direita até encontrar ou encontrar nulo. Implementado com recursão/loop.

Código de implementação:

//Encontrar uma chave específica

  BinarySearchTree.prototype.search = function(key){

        //1. Obtenha o nó raiz

        let node = this.root

        //2. Chave de pesquisa de loop

        while(nó != nulo){

          if (chave < nó.chave) {

            //Se for menor que o nó raiz (pai), olhe para a esquerda

            node = node.left

            //Se for maior que o nó raiz (pai), procure-o à direita

          }else if(chave > nó.chave){

            node = node.right

          }outro{

            retornar verdadeiro

          }

        } 

        retorna falso

      }

4. Excluir dados

Ideias de implementação:

Passo 1 : Encontre o nó que precisa ser excluído primeiro, se não for encontrado, não é necessário excluí-lo;

Primeiro defina a variável current para salvar o nó a ser excluído, a variável pai para salvar seu nó pai e a variável isLeftChild para salvar se a corrente é o nó esquerdo do pai, para que seja conveniente alterar a direção do nó relacionado ao excluir o nó posteriormente.

Código de implementação:

//1.1. Definir variáveis

        deixe atual = this.root

        deixe pai = nulo

        deixe éLeftChild = verdadeiro

        //1.2. Comece a procurar nós excluídos

        while (chave.atual != chave) {

          pai = atual

          // Se menor que, procura à esquerda

          if (chave <chave.atual) {

            isLeftChild = verdadeiro

            atual = atual.esquerda

          } outro{

            isLeftChild = false

            atual = atual.rigth

          }

          //Encontra o último nodo que ainda não foi encontrado

          if (atual == nulo) {

            retorna falso

          }

        }

        //Após o fim do loop while: current.key = key

Etapa 2 : Exclua o nó especificado encontrado e há três casos:

  • Excluir nós folha;
  • Exclua um nó com apenas um nó filho;
  • Excluir um nó com dois filhos;

4.1. Caso 1: Sem nós filhos

Há também dois casos em que não há nós filhos:

  • Quando o nó folha for o nó raiz, passe diretamente: this.root = null para deletar o nó raiz.
  • Existem também duas situações em que o nó folha não é o nó raiz, conforme mostrado na figura a seguir

0ff1f2cb4a3244df8081f561cee33358.png

 Código:

        //Caso 1: Um nó folha é excluído (sem nós filhos)
        if (current.left == null && current.right ==null) {           if (current == this.root) {             this.root = null           }else if (isLeftChild){             pai. esquerda = nulo           } else {             pai. direita = nulo           }         }







4.2. Caso 2: Há um nó filho

Existem seis situações:

Quando current tem um nó filho esquerdo (current.right == null):

  • Caso 1: atual é o nó raiz;
  • Caso 2: atual é o nó filho esquerdo do nó pai pai (isLeftChild == true);
  • Caso 3: atual é o nó filho direito do nó pai pai (isLeftChild == false);

d280084db4624702b3a2c9fee1b49a0e.png

Quando current tem um nó filho direito (current.left = null):

  • Caso 4: current é o nó raiz (current == this.root);
  • Caso 5: atual é o nó filho esquerdo do nó pai pai (isLeftChild == true);
  • Caso 6: current é o nó filho direito do nó pai pai (isLeftChild == false);

db81090a69f241e9b7f40b65cfb3f0f5.png

4.3. Caso 3: Existem dois nós filhos

Esta situação é muito complicada. Primeiro, discutimos tais problemas com base na seguinte árvore de busca binária:

3f859a9a153449b0a993681ab6105bfa.png

excluir nó 9

Sob a premissa de garantir que a árvore binária original ainda seja uma árvore binária de pesquisa após a exclusão do nó 9, existem duas maneiras:

  • Método 1: Selecione um nó adequado da subárvore esquerda do nó 9 para substituir o nó 9, e pode-se ver que o nó 8 atende aos requisitos;
  • Método 2: Selecione um nó adequado da subárvore direita do nó 9 para substituir o nó 9, e pode-se ver que o nó 10 atende aos requisitos;

98f25aa6a0574c078fd389fd7583287d.png

excluir nó 7

Sob a premissa de garantir que a árvore binária original ainda seja uma árvore binária de pesquisa após a exclusão do nó 7, existem duas maneiras:

  • Método 1: Selecione um nó adequado da subárvore esquerda do nó 7 para substituir o nó 7, pode-se ver que o nó 5 atende aos requisitos;
  • Método 2: Selecione um nó adequado da subárvore direita do nó 7 para substituir o nó 7, e pode-se ver que o nó 8 atende aos requisitos;

42035c636c0d4e3bbe43ed2e920b29f1.png

excluir nó 15

Sob a premissa de garantir que a árvore binária da árvore original ainda seja uma árvore binária de pesquisa após a exclusão do nó 15, também existem duas maneiras:

  • Modo 1: selecione um nó adequado da subárvore esquerda do nó 15 para substituir o nó 15, pode-se saber que o nó 14 atende aos requisitos;
  • Modo 2: selecione um nó adequado da subárvore direita do nó 15 para substituir o nó 15, pode-se ver que o nó 18 atende aos requisitos;

23307d3dbe3e44959f0096831f35df45.png

Resumo da regra: Se o nó a ser excluído tiver dois nós filhos, ou mesmo o nó filho tiver nós filhos, nesse caso, é necessário encontrar um nó adequado entre os nós filhos abaixo do nó a ser excluído para substituir o nó atual .

Se current for usado para representar o nó a ser excluído, o nó apropriado se refere a:

  • O nó na subárvore esquerda atual que é um pouco menor que o atual é o valor máximo na subárvore esquerda atual;
  • O nó na subárvore direita atual que é ligeiramente maior que o atual é o valor mínimo na subárvore direita atual;

antecessor e sucessor

Em uma árvore de pesquisa binária, esses dois nós especiais têm nomes especiais:

  • Um nó que é um pouco menor que o atual é chamado de predecessor do nó atual . Por exemplo, o nó 5 na figura abaixo é o predecessor do nó 7;
  • Um nó que é um pouco maior que o atual é chamado de sucessor do nó atual . Por exemplo, o nó 8 na figura abaixo é o sucessor do nó 7;

98310e077ed645ad934683bf146ac931.png

Código:

  • Para encontrar o sucessor de current, você precisa encontrar o valor mínimo na subárvore direita de current ;
  • Ao procurar o predecessor , você precisa encontrar o valor máximo na subárvore esquerda de current ;

4.4. Realize a busca sucessora

      //Excluir nó
  BinarySearchTree.prototype.remove = function(key){ /*-----1. Encontre o nó a ser excluído ------*/         let current = this.root         let parent = null         let isLeftChild = verdadeiro




        while (current.key != key) {           parent = current           // Se for menor que, procura à esquerda           if (key < current.key) {             isLeftChild = true             current = current.left           } else{             isLeftChild = false             current = current.right           }           //Encontra o último nó que ainda não foi encontrado           if (current == null) {             return false           }         }         //Após o fim do loop while: current.key = key














   /*--2. Excluir o nó de acordo com a situação correspondente -----*/
        //Caso 1: O nó folha é excluído (sem nó filho)
        if (current.left == null && current.right == null) {           if (current == this.root) {             this.root = null           }else if(isLeftChild){             parent.left = null           }else {             parent.right =null           }         }         //Caso 2: O nó excluído tem um nó filho         / /Quando atual tem um nó filho esquerdo         else if(current.right == null){             if (current == this.root) {               this.root = current.left             } else if(isLeftChild) {                 parent.left = atual.esquerda















            } else{                 parent.right = current.left             }         //Quando current tem um nó filho direito       } else if(current.left == null){             if (current == this.root) {               this.root = current.right             } else if(isLeftChild) {                 parent.left = current.right             } else{                 parent.right = current.right             }        }         //Caso 3: O nó excluído tem dois nós filhos         else{           //1. Obtenha o nó sucessor           let success = este .getSuccessor(atual)















          //2. Julgamento ponto de raiz certo ou errado
          if (atual == this.root) {             this.root = sucessor           }else if (isLeftChild){             pai.esquerda = sucessor           }else{             pai.direita = sucessor           }





          //3. Altere o nó filho esquerdo sucessor para o nó filho esquerdo do nó excluído
          success.left = current.left
        }
      }

      //Encapsule o método de encontrar o sucessor
      BinarySearchTree.prototype.getSuccessor = function(delNode){         //1. Defina variáveis ​​para salvar o sucessor encontrado         let success = delNode         let current = delNode.right         let successParent = delNode



        //2. Loop para encontrar o nó da subárvore direita da corrente
        while(atual != nulo){           sucessorParent = sucessor           sucessor =           atual atual = atual.esquerdo         }



        //3. Determinar se o nó sucessor encontrado é diretamente o nó certo do nó excluído
        if(successor != delNode.right){           successParent.left = success.right           success.right = delNode.right          }         return success       }




2. Árvore de equilíbrio

Desvantagens da árvore de busca binária:

Quando os dados inseridos são dados ordenados , a profundidade da árvore de busca binária será muito grande, afetando seriamente o desempenho da árvore de busca binária.

29ebd351b5374672aa8555afcd989183.png

árvore desequilibrada

  • Depois de inserir dados contínuos , a distribuição de dados na árvore de busca binária torna-se desigual , e chamamos essa árvore de árvore desbalanceada ;
  • Para uma árvore binária balanceada , a eficiência de operações como inserção/localização é O(logN) ;
  • Para uma árvore binária desbalanceada, é equivalente a escrever uma lista encadeada, e a eficiência de busca torna-se O(N) ;

equilíbrio da árvore

Para poder operar uma árvore em tempo mais rápido O(logN) , precisamos garantir que a árvore esteja sempre balanceada :

  • Pelo menos a maioria deles está balanceada, e a complexidade de tempo neste momento também está próxima de O(logN);
  • Isso requer que o número de nós descendentes à esquerda de cada nó na árvore seja o mais igual possível ao número de nós descendentes à direita ;

árvore balanceada comum

  • Árvore AVL: É o tipo mais antigo de árvore balanceada, que mantém a árvore balanceada armazenando dados extras em cada nó. Como a árvore AVL é uma árvore balanceada, sua complexidade de tempo também é O(logN). Mas sua eficiência geral não é tão boa quanto a árvore rubro-negra e é menos usada no desenvolvimento.
  • Árvore rubro-negra: também mantém o equilíbrio da árvore através de algumas características , e a complexidade de tempo também é O(logN). Ao realizar operações como inserção/exclusão, o desempenho é melhor que o da árvore AVL , portanto a aplicação da árvore balanceada é basicamente uma árvore rubro-negra.

Nove, árvore rubro-negra gráfica

1. Cinco regras da árvore rubro-negra

Além de cumprir as regras básicas da árvore binária de busca, a árvore rubro-negra também agrega as seguintes funcionalidades:

  • Regra 1: Os nós são vermelhos ou pretos;
  • Regra 2: O nó raiz é preto;
  • Regra 3: Cada nó folha é um nó preto vazio (nó NIL);
  • Regra 4: Ambos os filhos de cada nó vermelho são pretos (é impossível ter dois nós vermelhos consecutivos em todos os caminhos de cada folha até a raiz);
  • Regra 5: Todos os caminhos de qualquer nó para cada um de seus nós folha contêm o mesmo número de nós pretos;

18384f84ff0243b0a57569a6f2f8a90f.png

Equilíbrio relativo da árvore rubro-negra

As restrições das cinco regras anteriores garantem as seguintes propriedades-chave das árvores rubro-negras:

  • O caminho mais longo da raiz ao nó folha não excederá o dobro do caminho mais curto ;
  • O resultado é que a árvore está basicamente equilibrada;
  • Embora não haja equilíbrio absoluto, pode-se garantir que a árvore ainda é eficiente no pior caso;

Por que o caminho mais longo não pode exceder o dobro do caminho mais curto?

  • A propriedade 4 determina que não pode haver dois nós vermelhos conectados no caminho;
  • Portanto, o caminho mais longo deve ser formado alternadamente por nós vermelhos e nós pretos;
  • Como o nó raiz e os nós folha são pretos, o caminho mais curto pode ser nós pretos e deve haver mais nós pretos do que nós vermelhos no caminho mais longo;
  • A propriedade 5 determina que haja o mesmo número de nós pretos em todos os caminhos;
  • Isso significa que nenhum caminho pode ter mais do que o dobro do comprimento de qualquer outro caminho.

2. Três transformações da árvore rubro-negra

Ao inserir um novo nó, é possível que a árvore não esteja mais balanceada, podendo a árvore ser mantida balanceada por três formas de transformação:

  • Descoloração;
  • vire à esquerda;
  • vire à direita;

2.1. Descoloração

Para se adequar novamente às regras da árvore rubro-negra, é necessário transformar o vermelho em preto , ou transformar o preto em vermelho ;

Os novos nós inseridos são geralmente nós vermelhos :

  • Quando o nó inserido é vermelho , a maioria dos casos não viola nenhuma regra da árvore rubro-negra;

  • Inserir um nó preto inevitavelmente levará a um nó preto extra em um caminho , que é difícil de ajustar;

  • Embora nós vermelhos possam levar a conexões vermelho-vermelho , esta situação pode ser ajustada por troca de cores e rotação ;

2.2. Girar para a esquerda

Gire a árvore de pesquisa binária no sentido anti-horário com o nó X como raiz , de modo que a posição original do nó pai seja substituída por seu próprio nó filho direito e a posição do nó filho esquerdo seja substituída pelo nó pai;

Explicação detalhada:

Como mostrado acima, após a rotação à esquerda:

  • O nó X substitui a posição original do nó a;
  • O nó Y substitui a posição original do nó X;
  • A subárvore esquerda a do nó X ainda é a subárvore esquerda  do nó X (aqui, a subárvore esquerda de X tem apenas um nó, e o mesmo se aplica quando há vários nós, o mesmo se aplica abaixo);
  • A subárvore direita c do nó Y ainda é a subárvore direita  do nó Y ;
  • A subárvore b  esquerda do nó Y é transladada para a esquerda para se tornar a subárvore direita do nó X ;

Além disso, a árvore de pesquisa binária ainda é uma árvore de pesquisa binária após a rotação à esquerda:

2.3. Girar para a direita

Gire a árvore de pesquisa binária no sentido horário com o nó X como raiz , de modo que a posição original do nó pai seja substituída por seu próprio nó filho esquerdo e a posição do nó filho direito seja substituída pelo nó pai;

 

Explicação detalhada:

Conforme mostrado na figura acima, após a rotação à direita:

  • O nó X substitui a posição original do nó a;
  • O nó Y substitui a posição original do nó X;
  • A subárvore direita a do nó X ainda é a subárvore direita  do nó X (aqui, embora a subárvore direita de X tenha apenas um nó, ela também se aplica a vários nós, o mesmo se aplica abaixo);
  • A subárvore esquerda b do nó Y ainda é a subárvore esquerda  do nó Y ;
  • A subárvore direita  c do nó Y é transladada para a direita para se tornar a subárvore esquerda do nó X ;

Além disso, a árvore de pesquisa binária ainda é uma árvore de pesquisa binária após a rotação à direita

Em terceiro lugar, a operação de inserção da árvore rubro-negra

Antes de mais nada, é preciso ficar claro que o nodo recém-inserido deve ser um nodo vermelho quando as cinco regras da árvore rubro-negra forem garantidas .

Para facilitar a explicação, os quatro nós a seguir são estipulados: o nó recém-inserido é N (nó), o nó pai de N é P (pai), o nó irmão de P é U (tio) e o nó pai de U é G (Vovô), da seguinte forma Como mostrado na figura:

3.1. Situação 1

Quando um novo nó N é inserido na raiz da árvore, ele não tem pai.

Neste caso, basta alterar o nó vermelho para o nó preto para satisfazer a regra 2.

3.2. Situação 2

O nó pai P do novo nó N é um nó preto e nenhuma alteração é necessária neste momento.

Neste momento, tanto a regra 4 quanto a regra 5 são satisfeitas. Embora o novo nó seja vermelho, o novo nó N tem dois nós pretos NIL, então o número de nós pretos no caminho que leva a ele ainda é igual, então a regra 5 é satisfeita.

3.3. Situação 3

O nó P é vermelho e também é vermelho o nó U. Nesse momento, o nó G deve ser preto, ou seja, o pai é vermelho, o tio é vermelho e o ancestral é preto .

Neste caso você precisa:

  • Primeiro mude o nó pai P para preto;
  • Em seguida, altere o nó do tio U para preto;
  • Por fim, transforme o nó avô G em vermelho;

Ou seja, pai negro tio negro ancestral vermelho , conforme figura abaixo:

Problemas que podem surgir:

  • O nodo pai do nodo avô G de N também pode ser vermelho, o que viola a regra 4. Nesse momento, a cor do nodo pode ser ajustada recursivamente;
  • Quando o ajuste recursivo atinge o nó raiz, ele precisa ser rotacionado, conforme mostrado no nó A e no nó B na figura abaixo, e a situação específica será apresentada posteriormente;

3.4. Situação 4

O nó P é um nó vermelho, o nó U é um nó preto e o nó N é o nó filho esquerdo do nó P. Neste momento, o nó G deve ser um nó preto, ou seja, pai vermelho tio preto ancestral preto .

Neste caso você precisa:

  • Mude a cor primeiro: mude o nó pai P para preto e o nó avô G para vermelho;
  • Rotação para trás: rotação à direita com o nó avô G como raiz;

3.5. Situação 5

O nó P é um nó vermelho, o nó U é um nó preto e o nó N é o nó filho certo do nó P. Neste momento, o nó G deve ser um nó preto, ou seja, pai vermelho tio preto ancestral preto .

Neste caso você precisa:

  • Primeiro gire para a esquerda com o nó P como raiz, conforme mostrado na Figura b após a rotação;
  • Então, o nó vermelho P e o preto B são considerados como um vermelho inteiro N1 , e o vermelho recém-inserido N é considerado como um vermelho P1  , conforme mostrado na figura c. Neste momento, o todo é transformado no caso 4.

Em seguida, pode ser processado de acordo com o caso 4:

  • Mude a cor primeiro: mude o nó pai P1 do nó N1 para preto e mude o nó avô G para vermelho;

  • Pós-rotação: rotação à direita com o nó avô G como raiz, conforme mostrado na Figura e após a rotação;

  • Por fim, transforme os nós N1 e P1 de volta para completar a inserção do nó N, conforme Figura f;

Acho que você gosta

Origin blog.csdn.net/m0_65835778/article/details/126482262
Recomendado
Clasificación