Linguagem C - Árvores Binárias

Neste artigo, falarei sobre o que é e como implementar uma estrutura de dados chamada Árvore Binária. Com tempos de pesquisa, inserção e remoção expressivamente melhores que de listas encadeadas, esta estrutura é usada principalmente em bancos de dados e sistemas de arquivos.

[ Hits: 51.356 ]

Por: Enzo de Brito Ferber em 07/05/2015 | Blog: http://www.maximasonorizacao.com.br


Inserção



Como dito na introdução, as rotinas de inserção e pesquisa são bastante simples. O algoritmo para inserir um novo elemento na árvore é o seguinte:

	Inserir(elemento) {
		Se (elemento <= raiz) Inserir (arvore esquerda)
		Senão Inserir (arquivo direita)
	}

Como a estrutura de uma árvore binária é recursiva, também construiremos funções recursivas, e como toda função recursiva, a função para inserir um elemento também precisa de uma condição de pausa. Uma boa condição de pausa é quando a raiz fornecida como argumento for NULL. Assim, além de uma condição de pausa, teremos também uma deixa para inserir o elemento.

Primeiro, as definições básicas da estrutura da árvore:

typedef struct folha* Folha;

struct folha {
	int	info;

	Folha	esquerda;
	Folha	direita;
};

Agora, a função para inserir um novo elemento na árvore (completa, depois comentada passo a passo):

/* @ Folha inserir (Folha raiz, int info)
 *
 * Argumentos
 * ----------
 *	raiz	ponteiro para 'struct folha', estrutura de dado básica para
 *		construção da árvore
 *	info	nova informação a ser inserida
 *
 * Retorno
 * -------
 *	NULL	em caso de erro
 *	Folha	em caso de sucesso (nó 'raiz')
 */
Folha inserir (Folha raiz, int info)
{
	if (!raiz) {
		/* significa que o ponteiro é nulo, e que está é a posição
		 * para inserção.
		 *
		 * 1. Alocar e checar memória
		 */
		if (!(raiz = FOLHA)) {
			perror("inserir:malloc()");
			return NULL;
		}

		/* 2. Cópia da Informação
		 * 3. Ponteiros de referência
		 * 4. Retorno da nova folha
		 */
		raiz->info = info;
		raiz->esquerda = raiz->direita = NULL;

		return raiz;
	} else if (info > raiz->info)
		raiz->direita = inserir (raiz->direita, info);
	else
		raiz->esquerda = inserir (raiz->esquerda, info);

	/* retorna o ponteiro raiz
	 *
	 * Isso é necessário pois a função inserir é recursiva, e seu retorno
	 * é sempre atribuido ao mesmo ponteiro passado como argumento 'raiz',
	 *
	 * raiz->esquerda = inserir(raiz->esquerda,info)
	 * raiz->direita  = inserir(raiz->direta,  info)
	 */
	return raiz;
}

Para utilizar a função, inserir:

	Folha arvore = NULL;

	arvore = inserir(arvore, 4);
	...
	// as chamadas subsequentes a inserir serão:
	inserir (arvore, 4);

Dissecando a função:

Folha inserir (Folha raiz, int info)

A linha acima é a declaração da função e seus argumentos. A função "inserir" é declarada como do tipo "Folha" - tipo definido no início do código como um ponteiro para o tipo "struct folha" (typedef struct folha *Folha).

A seguir, a lista de parâmetros define os dois argumentos necessários para chamarmos a função: raiz e info. raiz é do tipo "Folha" (ponteiro para "struct folha"), e info é do tipo "int" e é nosso dado a ser inserido. Na chamada original, "raiz" será a raiz principal da árvore - nas subsequentes será uma subárvore esquerda ou direita da raiz original.

	if (!raiz) {
		/* significa que o ponteiro é nulo, e que está é a posição
		 * para inserção.
		 *
		 * 1. Alocar e checar memória
		 */
		if (!(raiz = (Folha) malloc (sizeof(struct folha)))) {
			perror("inserir:malloc()");
			return NULL;
		}

		/* 2. Cópia da Informação
		 * 3. Ponteiros de referência
		 * 4. Retorno da nova folha
		 */
		raiz->info = info;
		raiz->esquerda = raiz->direita = NULL;

		return raiz;
	}

A primeira parte do escopo da função é um bloco "if". Esse bloco é nossa condição de parada da recursão. Assim que "raiz" for NULL, (!raiz) será avaliado como VERDADEIRO e nosso bloco será executado (Também funciona para detectar uma nova árvore). A primeira ação a ser tomada é alocar memória para o novo nó e verificar se a chamada a malloc() foi bem sucedida.

Quando nos asseguramos que a memória foi alocada corretamente, começamos as atribuições. "raiz->info = info" atribui ao campo info de raiz a informação "info" de nossa lista de argumentos. A próxima linha inicializa ambos os ponteiros de referência "esquerda" e "direita" como NULL (isso é importante, uma vez que se deixarmos lixo nos ponteiros, as rotinas de operação na árvore não funcionarão corretamente, já que são baseadas em um teste condicional de negação).

É importante notar que esse algoritmo não trabalha com reestruturação de nós, então, por definição, todos os novos nós inseridos na árvore são "folhas" de acordo com a definição formal (ambas as subárvores nulas). A seguir retornamos a nova raiz que criamos.

Agora passamos para a parte da recursão, e é onde o algoritmo de árvore é implementado:

	else if (info > raiz->info)
		raiz->direita = inserir (raiz->direita, info);
	else
		raiz->esquerda = inserir (raiz->esquerda, info);

	return raiz;

Começamos com um "else if" em nosso primeiro "if(!raiz)". Nessa linha testamos se o valor do parâmetro "info" é MAIOR QUE "info" da "raiz" atual - lembre-se que "raiz" foi passado como argumento para a função. Caso a condição seja avaliada como verdadeira, significa que "info" é maior que a informação contida na raiz atual, então ela deve ser inserida à direita da raiz. Para isso, utilizamos:

raiz->direita = inserir (raiz->direita, info);

O que estamos fazendo aqui é chamar a função inserir() e fornecendo à ela a mesma "info" que nos foi passada a primeiro momento. Entretanto, passamos um novo argumento como raiz: "raiz->direita". "raiz->direita" refere-se à subárvore direita da "raiz" atual (argumento da função).

Também atribuímos o retorno dessa função ao ponteiro "raiz->direita". Lembre-se que a função "inserir" vai retornar uma nova raiz caso o parâmetro "raiz" seja NULL, então, caso a "raiz->direita" seja NULL, nos será retornado um novo ponteiro que atribuiremos à "raiz->direita". Reserve um tempo para entender o que está acontecendo aqui.

A seguir, completamos as condições de inserção com um "else". Esta condicional representa todo o resto não tratado pelos "if" e "else if" acima. Ou seja, quando uma "info" é *menor ou igual* à "raiz->info", realizaremos uma chamada à "inserir" exatamente como a acima, exceto que dessa vez, usaremos "raiz->esquerda".

raiz->esquerda = inserir (raiz->esquerda, info);

Após a sequência condicional, retornamos a raiz. E fazemos isso para dar consistência às definições condicionais acima mencionadas. Pense um pouco em como é o processo de procura do local para inserir um nó. A função "inserir" entra em modo recursivo e sempre retorna a raiz que foi passada como argumento.

Para percorrer a árvore, chamamos a função "inserir" repetidamente com os ponteiros "esquerda" e "direita" de raiz e atribuímos o retorno da função à eles. Isso significa que a caso não retornemos a "raiz", iremos criar inconsistências na árvore (buracos!).
Linux: Linguagem C - Árvores Binárias
Página anterior     Próxima página

Páginas do artigo
   1. Introdução
   2. Recursividade
   3. Inserção
   4. Pesquisa
   5. Remoção
   6. Transversalização
   7. Conclusão
   8. Código Completo e Comentado
Outros artigos deste autor

Linguagem C - Listas Duplamente Encadeadas

Linguagem C - Funções Variádicas

Leitura recomendada

Tutorial SDL

Dicas para aprender programação

Guia de Programação em C/GTK 2 - Construindo uma Calculadora Completa

Análise dos Métodos de Ordenação usados em Algoritmos Computacionais

Otimização de algoritmos

  
Comentários
[1] Comentário enviado por fabio em 07/05/2015 - 12:10h

Parabéns pelo trabalho, dissertação completa sobre o assunto.

[2] Comentário enviado por SamL em 09/05/2015 - 12:49h

Completissimo, muito bom, parabéns.

[3] Comentário enviado por EnzoFerber em 09/05/2015 - 13:34h


Muito obrigado, pessoal.

[4] Comentário enviado por pmargreff em 09/05/2015 - 21:09h


Primeiramente, parabéns pelo artigo.
Segundo, gostaria de saber que programa utilizou para fazer as imagens. Abraço.

[5] Comentário enviado por EnzoFerber em 11/05/2015 - 08:44h


[4] Comentário enviado por pablomrg em 09/05/2015 - 21:09h


Primeiramente, parabéns pelo artigo.
Segundo, gostaria de saber que programa utilizou para fazer as imagens. Abraço.


Bom dia.

Para as imagens usei o primo rico do GIMP! Filho da Adobe!

[]'s

[6] Comentário enviado por dianaluz92 em 12/05/2015 - 15:08h


Parabéns pelo trabalho, a parte conceitual está muito clara da para a estrutura da arvore, mas eu queria tirar um duvida se for possível, como inserir um conjunto de elementos na árvore no caso quando o campo info é um ponteiro que deverá receber elemento tipo inteiro,float e um ponteiro char para nome.
Desde já grata,

[7] Comentário enviado por EnzoFerber em 12/05/2015 - 16:19h


[6] Comentário enviado por dianaluz92 em 12/05/2015 - 15:08h


Parabéns pelo trabalho, a parte conceitual está muito clara da para a estrutura da arvore, mas eu queria tirar um duvida se for possível, como inserir um conjunto de elementos na árvore no caso quando o campo info é um ponteiro que deverá receber elemento tipo inteiro,float e um ponteiro char para nome.
Desde já grata,

Boa tarde.
Pelo que entendi, você quer usar árvores para armazenar uma estrutura sua, certo?

typedef struct data {
int i;
float f;
char *nome;
} data_t;

Coloque os ponteiros direita e esquerda na estrutura que está usando.

typedef struct data* data_t;
struct data {
int i;
float f;
char *nome;
data_t esquerda, direita;
};

Agora é só modificar as funções inserir(), deletar(), procurar(), imprimir(), etc.

if (!raiz) {
...
} else if (strcmp(raiz->nome, nome_ptr) > 0) raiz->direita = inserir(raiz->direita, nome_ptr);
else if (strcmp(raiz->nome, nome_ptr) < 0 ) raiz->equerda = inserir(raiz->esquerda, nome_ptr);
else {
// strcmp() == 0
// dado existente
printf("dado existente!");
return raiz;
}
return raiz;

Espero ter ajudado,
[]'s
Enzo Ferber

[8] Comentário enviado por Ragen em 13/05/2015 - 08:41h

Bom dia Enzo,

Parabéns pelo texto. Vejo que hoje muitos do meio acadêmico não tem nem de longe a sua didática - até porque para ser didático, um professor precisa ser profundo conhecedor do tema.

Eu fiz faculdade em 2003 na PUC-Goiás e me recordo das aulas de ICC2 onde fizemos este caso de B-tree. Logo depois a grade mudou, e o curso afundou em total decadência.

Hoje vejo que muitos alunos se formam bacharéis sem ao menos entender o conceito das árvores binárias. Sem entender o que é um ponteiro, endereço de memória - afinal as faculdades estão pondo Java no lugar do C. Moral da história? Os alunos se formam e não sabem programar - então terminam seus cursos e ficam desempregados e não entendem porque nenhum empresa os querem contratar.

Obrigado por compartilhar conosco este material e seu conhecimento!

[9] Comentário enviado por EnzoFerber em 13/05/2015 - 09:04h


[8] Comentário enviado por Ragen em 13/05/2015 - 08:41h

Bom dia Enzo,

Parabéns pelo texto. Vejo que hoje muitos do meio acadêmico não tem nem de longe a sua didática - até porque para ser didático, um professor precisa ser profundo conhecedor do tema.

Eu fiz faculdade em 2003 na PUC-Goiás e me recordo das aulas de ICC2 onde fizemos este caso de B-tree. Logo depois a grade mudou, e o curso afundou em total decadência.

Hoje vejo que muitos alunos se formam bacharéis sem ao menos entender o conceito das árvores binárias. Sem entender o que é um ponteiro, endereço de memória - afinal as faculdades estão pondo Java no lugar do C. Moral da história? Os alunos se formam e não sabem programar - então terminam seus cursos e ficam desempregados e não entendem porque nenhum empresa os querem contratar.

Obrigado por compartilhar conosco este material e seu conhecimento!


Bom dia, Ragen.
Em primeiro lugar, muito obrigado pelos elogios.

Segundo, concordo plenamente com você (Muita gente e eu, na verdade). Uma dessas pessoas é o CEO do StackExchange, que possui um blog muito bom. O nome dele é Joel Spolsky e o blog é o http://www.joelonsoftware.com. Ele escreveu um artigo precisamente sobre a troca de linguagens "mais baixas" pelo Java nas faculdades, e como isto está arruinando o mercado de trabalho e a qualidade dos profissionais. O artigo chama-se "The Perils of JavaSchool". Excelente texto, vale a pena ser lido. (O blog todo, na verdade)
http://www.joelonsoftware.com/articles/ThePerilsofJavaSchools.html

Pessoalmente, acredito que todos deveriam ter um semestre em Assembly para entender como a CPU/Memória/HD funcionam e interagem - como tudo realmente acontece "embaixo do capô". Faz uma falta danada!

Sobre Joel: http://www.joelonsoftware.com/AboutMe.html
StackExchange: http://www.stackexchange.com

*

http://blog.codinghorror.com/why-cant-programmers-program/
Artigo muito bom também, falando sobre bacharéis que não conseguem programar programas *simples*! Leitura recomendada!


[]'s
Enzo Ferber

[10] Comentário enviado por Ragen em 13/05/2015 - 18:22h


[9] Comentário enviado por EnzoFerber em 13/05/2015 - 09:04h


[8] Comentário enviado por Ragen em 13/05/2015 - 08:41h

Bom dia Enzo,

Parabéns pelo texto. Vejo que hoje muitos do meio acadêmico não tem nem de longe a sua didática - até porque para ser didático, um professor precisa ser profundo conhecedor do tema.

Eu fiz faculdade em 2003 na PUC-Goiás e me recordo das aulas de ICC2 onde fizemos este caso de B-tree. Logo depois a grade mudou, e o curso afundou em total decadência.

Hoje vejo que muitos alunos se formam bacharéis sem ao menos entender o conceito das árvores binárias. Sem entender o que é um ponteiro, endereço de memória - afinal as faculdades estão pondo Java no lugar do C. Moral da história? Os alunos se formam e não sabem programar - então terminam seus cursos e ficam desempregados e não entendem porque nenhum empresa os querem contratar.

Obrigado por compartilhar conosco este material e seu conhecimento!

Bom dia, Ragen.
Em primeiro lugar, muito obrigado pelos elogios.

Segundo, concordo plenamente com você (Muita gente e eu, na verdade). Uma dessas pessoas é o CEO do StackExchange, que possui um blog muito bom. O nome dele é Joel Spolsky e o blog é o http://www.joelonsoftware.com. Ele escreveu um artigo precisamente sobre a troca de linguagens "mais baixas" pelo Java nas faculdades, e como isto está arruinando o mercado de trabalho e a qualidade dos profissionais. O artigo chama-se "The Perils of JavaSchool". Excelente texto, vale a pena ser lido. (O blog todo, na verdade)
http://www.joelonsoftware.com/articles/ThePerilsofJavaSchools.html

Pessoalmente, acredito que todos deveriam ter um semestre em Assembly para entender como a CPU/Memória/HD funcionam e interagem - como tudo realmente acontece "embaixo do capô". Faz uma falta danada!

Sobre Joel: http://www.joelonsoftware.com/AboutMe.html
StackExchange: http://www.stackexchange.com

*

http://blog.codinghorror.com/why-cant-programmers-program/
Artigo muito bom também, falando sobre bacharéis que não conseguem programar programas *simples*! Leitura recomendada!


[]'s
Enzo Ferber


Opa Enzo,

Valeu pela dica. Eu já conhecia o StackExchange, mas não conhecia o blog do fundador - estou lendo os links que recomendou, realmente ótima leitura!

Se tiver Linkedin, me adiciona lá: https://www.linkedin.com/profile/view?id=80923831

[11] Comentário enviado por EnzoFerber em 18/07/2015 - 12:15h

Pessoal,

Na página 4 há um erro. Na função pesquisa esqueci de fazer justamente a comparação que identifica o elemento procurado.
Para utilizar a função corretamente, favor fazer a seguinte modificação:

Após a linha "if (!raiz) return NULL;"
Colocar:
else if (raiz->info == info) return raiz;

É isso.

[]'s


Contribuir com comentário




Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts