Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Campus Itabira
Capítulo 1
Introdução às Estruturas de Dados
1º Semestre de 2016
Sandro Carvalho Izidoro
1 Considerações Iniciais
Para o entendimento do que vem a ser Estrutura de Dados é preciso antes diferenciar
3 conceitos:
• Tipos de dados;
• Estruturas de dados;
• Tipos abstratos de dados.
Então, tipos de dados podem ser vistos como métodos para interpretar o conteúdo da
memória do computador. Mas este conceito também pode ser visto de uma outra
perspectiva: não em termos do que um computador pode fazer (interpretar os bits) mas em
termos do que os usuários desejam fazer (somar dois inteiros). Este conceito de tipo de
dados divorciado do hardware é chamado tipo abstrato de dados - TAD.
Antes de um programa ser escrito, o projetista deveria ter uma idéia ótima de como
realizar a tarefa que está sendo implementada por ele. Por isso, um delineamento do
programa contendo seus requisitos deveria preceder o processo de codificação. Quanto
maior e mais complexo o projeto, mais detalhada deveria ser a fase de delineamento. Os
detalhes de implementação deveriam ser adiados para estágios posteriores do projeto. Em
especial, os detalhes das estruturas de dados particulares a serem utilizadas na
implementação não deveriam ser especificadas no início.
3 Estruturas em C/C++
Nesta seção, será revisto dois dos principais mecanismos para a construção de novos
tipos da linguagem C/C++, denominados struct e union. Este destaque é importante, uma vez
que as estruturas de dados que serão estudadas neste curso serão descritas com o auxílio
das mesmas.
struct {
char Primeiro[10];
char Meio;
char Ultimo[20];
} sname, ename;
Esta declaração cria duas variáveis estrutura, sname e ename, cada uma das quais
contendo três membros: Primeiro, Meio e Ultimo. Dois dos membros são strings, e o terceiro
é um caractere isolado. Como alternativa, pode-se incluir um nome à estrutura e, em
Algoritmos e Estrutura de Dados I – Capítulo 1 – 4
struct NovoTipo {
char Primeiro[10];
char Meio;
char Ultimo[20];
};
NovoTipo sname, ename;
Como foi visto, quando uma variável é declarada como sendo de um determinado tipo,
está sendo informado que o identificador se refere a determinada parte da memória e que o
conteúdo dessa memória deve ser interpretado de acordo com o padrão definido pelo tipo.
Por exemplo, supondo a declaração a seguir:
struct NovoTipo {
int Campo1;
float Campo2;
char Campo3[10];
};
NovoTipo N;
de memória para cada membro [5]. Em outras palavras, uma union é o meio pelo qual um
espaço de memória ora é tratado como uma variável de um certo tipo, ora como outra
variável de outro tipo. Portanto, uniões são utilizadas para economizar memória. Quando é
declarado uma variável de um tipo union, automaticamente será alocado espaço de memória
suficiente para conter o seu maior membro. Eis um exemplo:
// Programa 000
// Exemplo de Union
#include<iostream>
union Novo {
char str[20];
int i;
float f;
} X;
int main( ) {
std::cout << sizeof(Novo) << "\n";
std::cout << sizeof(X);
return 0;
}
Uma variável de um tipo union tem o tamanho do maior membro. O exemplo acima
permite verificar esta característica através do operador sizeof. Este operador atua sobre o
nome de um tipo de dado ou sobre o nome de uma variável retornando o seu tamanho em
bytes.
4 Ponteiros
Um ponteiro é um endereço de memória. Seu valor indica onde uma variável está
armazenada, não o que está armazenado. Um ponteiro proporciona um modo de acesso a
uma variável sem referenciá-la diretamente.
A memória de um computador pode ser vista como uma enorme sequencia de bytes
contíguos. Cada byte está localizado em um endereço da memória. O primeiro byte ocupa o
endereço 0 da memória; o segundo byte fica no endereço 1 e assim sucessivamente. O
endereço é, portanto, um número que indica a posição de um determinado byte na memória.
A grandeza desse número varia de sistema para sistema.
Como foi discutido anteriormente, ao se declarar uma variável, dá-se a ela um nome e
um tipo. A partir dessas informações, o compilador é capaz de saber o número de bytes que
a variável necessita ocupar na memória, de modo a conter qualquer um dos possíveis
valores implicitamente especificados através do seu tipo. O compilador também sabe o
endereço exato onde a variável se encontra, pois é ele mesmo que cuida de reservar o
espaço necessário.
Na declaração acima, o identificador PtrInt fica declarado como o tipo ponteiro para
int. A variável P é, portanto, um ponteiro para um inteiro. Uma referência à variável P é uma
referência ao seu conteúdo, ou seja, a um endereço. Para referenciar a área apontada por P
Algoritmos e Estrutura de Dados I – Capítulo 1 – 7
deve-se usar *P. Mas para isto é necessário criar dinamicamente a área cujo endereço será
armazenado em P. Existem várias formas para atribuir um valor – um endereço – para uma
variável, sendo a utilização dos operadores new e delete os mais utilizados.
O operador new cuida de alocar dinamicamente uma área na memória (de acordo
com o tipo base para o qual a variável aponta), e retorna o endereço da região alocada. O
operador delete realiza a operação inversa, ou seja, delete libera a área alocada. A área
liberada pode ser novamente aproveitada numa operação de alocação futura. O exemplo a
seguir ilustra o que foi discutido.
// Programa 001
// Exemplo de ponteiro
#include<iostream>
int main(){
PtrInt P;
P = new int;
*P = 10;
std::cout << P << "\n" << *P << "\n";
delete P;
return 0;
}
P = new int;
Algoritmos e Estrutura de Dados I – Capítulo 1 – 8
P = &I;
está atribuindo ao ponteiro P o endereço da variável I. Isso significa que, após essa
atribuição, I e *P representam a mesma área da memória, ou seja, a mesma variável.
Na linguagem C++ ainda é possível declarar um ponteiro genérico void. Neste caso, o
ponteiro não referencia valores de um tipo específico. Um ponteiro void pode receber o
endereço de qualquer coisa. Porém, não é possível, através deste ponteiro, fazer referência
a esta área P. Em outras palavras, não é possível usar *P. A referência deve ser feita por
meio de typecast para algum outro tipo de ponteiro.
5 Funções
Cada função deve se limitar a realizar uma tarefa simples e bem-definida e o nome da
função deve expressar efetivamente aquela tarefa. Isto promove a reutilização do software. O
código a seguir apresenta, como exemplo, uma função para calcular o quadrado dos
números inteiros de 1 a 50.
// Programa 002
// Exemplo de função
#include <iostream>
int main(){
for (int x=1; x<=50; x++)
std::cout << quadrado( x ) << " ";
return 0;
}
// definicao da funcao
int quadrado ( int y ){
return y * y;
}
Em C++, um programa pode conter uma ou mais funções, das quais uma delas deve
ser main( ). No exemplo anterior, a função quadrado é chamada ou invocada na função main
através do comando:
quadrado( x )
Os ( ) em uma chamada de função são um operador de C++. Eles fazem com que a
função seja chamada. Esquecer os ( ) em uma chamada de função que não aceita
argumentos não é um erro de sintaxe, mas a função não será invocada.
Existem três maneiras de retornar o controle para o ponto no qual uma função foi
chamada. Se a função não fornecer um valor como resultado, o controle é retornado quando
a chave que indica o término da função é alcançada ou ao se executar o comando return. Se
a função fornecer um valor como resultado, o comando
return expressão;
6 Parâmetros da função
Como foi discutido, as informações transmitidas para uma função são chamadas
parâmetros. Os parâmetros podem ser utilizados livremente no corpo da função. Existem
basicamente dois tipos de parâmetros: parâmetros por valor e parâmetros por referência.
Entender a diferença destes parâmetros é fundamental para a boa estruturação de um
código.
receber o valor passado pela função chamadora. Receber parâmetros desta forma, é
conhecido como passagem por valor. Uma característica importante deste mecanismo é que,
se for utilizado uma variável como argumento, a variável que é utilizada como parâmetro não
pode alterar o conteúdo da variável argumento, mesmo que ela seja modificada dentro da
função. No exemplo a seguir, a função Somar incrementa a variável parâmetro X. Mas esta
alteração não prejudica a variável Y que continua valendo 0, ou seja, X apenas recebeu o
valor de Y durante a chamada da função.
// Programa 003
// Exemplo de passagem de parametros por valor
#include <iostream>
int main(){
int Y=0;
Somar(Y);
std::cout << Y << "\n";
return 0;
}
A linguagem C++ possui o operador unário de referência &. Este operador cria outro
nome para uma variável já existente. O código seguinte ilustra o seu uso:
// Programa 004
// Exemplo de referencia
#include <iostream>
Algoritmos e Estrutura de Dados I – Capítulo 1 – 12
int main(){
int n;
int& n1=n;
n=5;
std::cout << n1 << "\n";
n1=15;
std::cout << n << "\n";
return 0;
}
int n;
int& n1=n;
informam que n1 é outro nome para n. Toda operação em qualquer dos nomes tem o
mesmo resultado. Uma referência não é uma cópia da variável a quem se refere, é a mesma
variável sob nomes diferentes. O exemplo acima imprime o valor 5 e 15. O operador unário
&, quando usado na criação de referências, faz parte do tipo. Portanto, int& é um tipo de
dado. Além disso, toda referência deve ser obrigatoriamente inicializada.
O uso mais importante para referências é passar argumentos para funções. Desta
forma, a função pode acessar as variáveis da função chamadora. Além deste benefício, este
mecanismo possibilita que uma função retorne mais de um valor para a função que chama.
Os valores a serem retornados são colocados em referências de variáveis da função
chamadora conforme ilustra o código seguinte:
// Programa 005
// Exemplo de passagem de parametros por referencia
#include <iostream>
void Somar(int& X){
X++;
std::cout << X << "\n";
}
Algoritmos e Estrutura de Dados I – Capítulo 1 – 13
int main(){
int Y=0;
Somar(Y);
std::cout << Y << "\n";
return 0;
}
O próximo exemplo cria uma função que troca o conteúdo de duas variáveis e será
utilizada para ordenar uma lista de três números.
// Programa 006
// Exemplo de passagem de parametros por referencia
#include <iostream>
void Troca(int& X, int& Y){
int temp=X;
X = Y;
Y = temp;
}
int main(){
int A, B, C;
std::cout << "\nDigite 3 numeros: ";
std::cin >> A >> B >> C;
if (A > B) Troca(A, B);
if (A > C) Troca(A, C);
if (B > C) Troca(B, C);
std::cout << A << " " << B << " " << C << " \n";
return 0;
}
Algoritmos e Estrutura de Dados I – Capítulo 1 – 14
Para passar um vetor como argumento para uma função, é necessário especificar o
nome do vetor sem os colchetes. Por exemplo, se o vetor Alunos for declarado como:
int Idades[10];
As funções chamadas podem modificar os valores dos elementos nos vetores originais
do chamador. O nome do vetor é o endereço do primeiro elemento do vetor. Para uma
função receber um vetor por meio de uma chamada de função, sua lista de parâmetros deve
especificar que um vetor será recebido. Por exemplo, o cabeçalho da função modificaIdades
pode ser escrito como:
// Programa 007
// Passando vetores como parâmetros de função
#include <iostream>
int main(){
const int Tamanho = 5;
int Idades[Tamanho] = {0, 1, 2, 3, 4};
std::cout << "\nVetor Original: ";
for (int i=0; i<Tamanho; i++)
std::cout << Idades[i] << " ";
modificaIdades(Idades, Tamanho);
std::cout << "\nVetor Modificado: ";
for (int i=0; i<Tamanho; i++)
std::cout << Idades[i] << " ";
std::cout << "\n";
return 0;
}
void modificaIdades(int b[], int tam){
for (int i=0; i<tam; i++)
b[i]++;
}
Podem existir situações nas quais não se deve permitir que uma função modifique os
elementos de um vetor. Como os vetores são sempre passados por chamadas por
referência, as modificações nos valores de um vetor são difíceis de controlar. Entretanto,
pode-se utilizar o qualificador de tipo const com ponteiros.
Quando uma função especifica um parâmetro vetor que é precedido por const, os
elementos do vetor se tornam constantes no corpo da função e qualquer tentativa de
modificar um elemento resulta em um erro de compilação.
Um ponteiro para função é uma variável que armazena o endereço de uma função na
memória, permitindo a execução da função através deste ponteiro.
Algoritmos e Estrutura de Dados I – Capítulo 1 – 16
// Programa 008
// Ponteiro para funções
#include <iostream>
void Exemplo(){
std::cout << "Uma função para exemplificar ponteiros \n";
}
int main(){
void (*PtrFun)(void); // declaração do ponteiro
PtrFun = Exemplo; // Atribuição do endereço da função
(*PtrFun)(); // Chamada da função
return 0;
}
Neste exemplo, a função main( ) começa declarando uma variável ponteiro, PtrFun.
Os parênteses envolvendo a variável são necessários para que o compilador consiga
distinguir a declaração de um ponteiro com o cabeçalho de uma função. Outro detalhe
importante é que o nome da função sem os parênteses é o seu endereço. Caso contrário o
compilador entenderia como uma chamada da função. Além disso, a aritmética com
ponteiros para funções não é definida, ou seja, não é possível incrementar ou decrementar
ponteiros para funções. O próximo exemplo ilustra um ponteiro para uma função com
parâmetros.
// Programa 009
// Ponteiro para uma função com parâmetros
#include <iostream>
int SomaVetor( int *b, int tam ){
int Soma=0;
for (int i=0; i<tam; i++)
Soma += b[i];
return Soma;
}
Algoritmos e Estrutura de Dados I – Capítulo 1 – 17
int main( ) {
int (*PtrFun)(int *, int); // declaração do ponteiro
const int Tamanho = 5;
int Idades[Tamanho] = {0, 1, 2, 3, 4};
PtrFun = SomaVetor;
std::cout << "\nTotal do Vetor: " << PtrFun(Idades, Tamanho);
std::cout << "\n";
return 0;
}
7 Recursividade
Um tipo especial de função será utilizada, algumas vezes, ao longo deste curso. É
aquela que contém em sua descrição uma ou mais chamadas a si mesma. Uma função
desta natureza é denominada recursiva, e a chamada a si mesma é dita chamada recursiva.
De modo geral, a todo procedimento recursivo corresponde um outro não recursivo que
executa, exatamente, a mesma computação. Contudo, a recursividade pode apresentar
vantagens concretas. Frequentemente, os procedimentos recursivos são mais concisos do
que um não recursivo correspondente. Além disso, muitas vezes é aparente a relação direta
entre um procedimento recursivo e uma prova por indução matemática. Nesses casos, a
verificação da correção pode se tornar mais simples. Entretanto, muitas vezes há
desvantagens no emprego prático da recursividade. Um algoritmo não recursivo equivalente
pode ser mais eficaz [6].
// Programa 010
// Torre de Hanoi
#include<iostream>
void movimento (int n, char *Origem, char *Temp, char *Destino){
if (n > 0){
movimento (n-1, Origem, Destino, Temp);
std::cout << "Mova o disco " << n << " da haste " << Origem << " para
a haste " << Destino << "\n";
movimento (n-1, Temp, Origem, Destino);
}
}
Algoritmos e Estrutura de Dados I – Capítulo 1 – 19
int main() {
movimento (4, "ORIGEM", "TEMP", "DESTINO");
return 0;
}
8 Exercícios
1. Supondo que um número real seja representado por uma estrutura em C, como esta:
struct TipoReal {
int Esquerda;
int Direita;
};
(a) Escreva uma função para receber um número real e criar uma estrutura
representando esse número.
(b) Escreva uma função que aceite essa estrutura e retorne o número real
correspondente.
(c) Escreva rotinas Soma, Subtrai e Multiplica que aceitem duas dessas estruturas e
definam o valor de uma terceira estrutura para representar o número que seja a soma,
a diferença e o produto, respectivamente, dos dois registros de entrada.
4. Escreva uma estrutura para descrever um mês do ano. A estrutura deve ser capaz de
armazenar o nome do mês, a abreviação em três letras, o número de dias e o número do
mês.
Algoritmos e Estrutura de Dados I – Capítulo 1 – 20
6. Escreva uma função que recebe o número do mês como argumento e retorna o total de
dias do ano até aquele mês. Assuma que a matriz da questão anterior foi declara como
externa.
8. Explique cada uma das seguintes declarações e identifique quais são incorretas.
11. Elaborar uma função não recursiva para o problema da Torre de Hanoi.
#include <iostream>
int i;
void p1 (int x){
Algoritmos e Estrutura de Dados I – Capítulo 1 – 21
i++;
x += 2;
std::cout << x << "\n";
}
void p2 (int *x) {
i++;
*x += 2;
std::cout << *x << "\n";
}
int main( ) {
int a[2] = {10, 20};
std::cout << a[0] << " " << a[1] << "\n";
i=0;
p1(a[i]);
std::cout << a[0] << " " << a[1] << "\n";
i=0;
p2(&a[i]);
std::cout << a[0] << " " << a[1] << "\n";
}
void strSubstitui (char s[ ], const char s1[ ], const char s2[ ]);
gj = j − 1, 1 # j # k;
gj = gj−1 + gj−2, j > k.
9. Referências
[1] Maria da Graça Campos Pimentel and Maria Cristina Ferreira de Oliveira. Algoritmos e
estrutura de dados 1. http://www.icmc.usp.br/ sce182/, 2006.
[2] Adam Drozdek. Estrutura de Dados e Algoritmos em C++. Pioneira Thomson Learning,
São Paulo, 2002.
[3] Paulo Veloso, Clesio dos Santos, Paulo Azeredo, and Antonio Furtado. Estrutura de
Dados. Editora Campus, Rio de Janeiro, 2a edition, 1986.
[4] Aaron M. Tenenbaum, Yedidyah Langsam, and Moshe J. Augenstein. Estrutura de Dados
usando C. Makon Books, São Paulo, 1995.
[5] Victorine Viviane Mizrahi. Treinamento em Linguagem C++ – M´odulo 1. Makon Books,
São Paulo, 1994.
[6] Jayme Luiz Szwarcfiter and Lilian Markenzon. Estruturas de Dados e Seus Algoritmos.
LTC, Editora, Rio de Janeiro, 1994.
Observação
Material elaborado a partir das notas de aula do professor Edmilson Marmo Moreira (UNIFEI)