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:
- 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:
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:
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:
Depois de girá-lo 45° no sentido horário:
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:
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.
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). .
Á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;
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.
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.
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.
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;
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).
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.
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:
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;
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;
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;
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:
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
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);
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);
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:
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;
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;
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;
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;
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.
á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;
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 nó vermelho em preto , ou transformar o nó 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 nó preto B são considerados como um nó vermelho inteiro N1 , e o nó vermelho recém-inserido N é considerado como um nó 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;