Você está na página 1de 22

Universidade Federal de Itajubá – UNIFEI

Campus Itabira

Algoritmos e Estrutura de Dados I


ECO i04

Capítulo 1
Introdução às Estruturas de Dados
1º Semestre de 2016
Sandro Carvalho Izidoro

1 Considerações Iniciais

A disciplina tem como objetivo introduzir estruturas de dados e seus algoritmos,


familiarizando o estudante com as várias estruturas de dados e habilitando-o a utilizar
adequadamente esses recursos no desenvolvimento de outras atividades da computação.

2 O Papel das Estruturas de Dados

Em várias situações em programação é necessário trabalhar com um conjunto de


dados que possuem algo em comum. Estes dados ficam melhor organizados se derivam de
uma mesma estrutura.

A estrutura implementada é manipulada por uma série de operações que são


primitivas a ela. Por exemplo, uma pilha de pratos é uma estrutura composta por um local
onde ficam armazenados os pratos, com destaque para o prato que sempre está disponível
para ser utilizado – o topo da pilha de pratos. As operações primitivas que podem ser
utilizadas para manipular esta estrutura são:

• A inserção de um prato na pilha, que passará a ser o topo da pilha;

• A remoção de um prato da pilha. Neste caso, para não comprometer a estrutura, o


prato que será retirado deve estar no topo da pilha.
Algoritmos e Estrutura de Dados I – Capítulo 1 – 2

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.

Embora estes termos sejam parecidos, eles tem significados diferentes. Em


linguagens de programação o tipo de dados de uma variável define o conjunto de valores
que a variável poderá assumir. Por exemplo, uma variável do tipo boolean pode assumir o
valor true ou false.

Uma declaração de variável em uma linguagem como C ou Pascal especifica duas


coisas [1]:

1. A quantidade de bytes que deve ser reservada para a variável.


2. Como o dado representado por esses bytes deve ser interpretado.

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.

Desta forma, a declaração de tipos de uma variável determina:

1. O conjunto de valores que esta variável poderá assumir.


2. As operações aplicáveis a esta variável.

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.

Desde o início, é importante especificar cada tarefa de entrada e de saída. O


comportamento do programa é mais importante do que as engrenagens do mecanismo que o
executa. Por exemplo, se um item é necessário para realizar algumas tarefas, o item é
especificado em termos das operações nele realizadas, em vez de em termos de sua
estrutura interna. Somente depois que essas operações forem especificadas precisamente, a
implementação do programa poderá começar. A implementação decide que estrutura de
dados deverá ser usada para tornar a execução mais eficiente em relação a tempo e espaço.
Algoritmos e Estrutura de Dados I – Capítulo 1 – 3

Assim, um item especificado em termos de operações é chamado de tipo abstrato de


dados. Um TAD não é parte de um programa, já que um programa escrito em linguagem de
programação exige a definição de uma estrutura de dados, não apenas das operações nessa
estrutura. No entanto, uma linguagem orientada a objetos, tal como C++, tem vínculo direto
com os tipos abstratos de dados, implementando-os como uma classe [2].

Neste contexto, Estrutura de Dados é um método particular de se implementar um


TAD. A implementação de um TAD escolhe uma estrutura de dados para representá-lo. Cada
estrutura de dados é construída dos tipos básicos (int, real, char, etc.) ou dos tipos
estruturados (vetores, registros, etc.) de uma linguagem de programação.

Finalmente, é importante destacar que a forma (ESTRUTURA) como os dados ficam


armazenados condicionam a eficiência de um sistema de informação. Por exemplo, uma lista
telefônica possui uma estrutura de dados que permite a localização de suas informações
rapidamente, devido à ordenação dos seus elementos [3]. Esta forma organizada de
estruturar os dados e programas é o que se costuma denominar programação estruturada.
Nos dias atuais, a programação estruturada está totalmente inserida no contexto da
programação orientada a objetos que será motivo de estudo em disciplina própria.

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.

3.1 Estruturas – struct

Uma estrutura é um grupo de itens no qual cada item é identificado por um


identificador próprio, sendo cada um deles conhecidos como membros da estrutura [4]. Por
exemplo:

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

seguida, declarar as variáveis por meio desse nome.

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;

A quantidade de memória especificada pela estrutura é a soma do armazenamento


especificado por cada um dos tipos de seus membros. Consequentemente, o espaço
necessário para a variável N é a soma do espaço necessário para um inteiro (4 bytes), um
número de ponto flutuante (8 bytes) e um vetor de 10 caracteres (10 bytes). Para cada
referência a um membro de uma estrutura, deve ser calculado um endereço. Associado a
cada identificador de membro de uma estrutura existe um deslocamento que especifica a
distância a partir do início da estrutura em que se encontra a posição desse campo. No
exemplo anterior, o deslocamento de Campo1 é 0, o de Campo2 é 4, e o deslocamento de
Campo3 é 12.

3.2 Uniões – union

As estruturas examinadas até aqui apresentaram membros fixos e um único formato.


Uma union permite que uma variável seja interpretada de várias maneiras diferentes. Esta
estrutura é usada de forma semelhante a struct, para agrupar um número de diferentes
variáveis sob um único nome. Entretanto, uma union utiliza um mesmo espaço de memória a
ser compartilhado com diferentes membros, enquanto uma struct aloca um espaço diferente
Algoritmos e Estrutura de Dados I – Capítulo 1 – 5

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.

Os ponteiros são usados em situações em que o uso de uma variável é difícil ou


indesejável. Algumas razões para o uso de ponteiros são:
Algoritmos e Estrutura de Dados I – Capítulo 1 – 6

• Manipular elementos de matrizes;


• Receber argumentos em procedimentos e funções que necessitem modificar o
argumento original (passagem de parâmetros por referência);
• Criar estruturas de dados complexas, como listas encadeadas e arvores binárias,
onde um item deve conter referências a outro;
• Alocar e desalocar memória do sistema.

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.

O endereço de uma variável que ocupa mais de um byte na memória é o endereço do


seu primeiro byte. Quando o nome da variável aparece posteriormente em algum comando, o
compilador sabe gerar o código necessário para localizar a variável, seja para guardar nela
algum valor, seja para ler o seu valor corrente. Dessa forma, um ponteiro é uma variável cujo
conteúdo é um endereço de memória. Ao invés de conter o próprio dado, um ponteiro contém
o endereço do dado.

4.1 Declaração de um Ponteiro

A declaração de um tipo ponteiro requer o uso do símbolo * seguido de um


identificador. Esse identificador é o chamado tipo base, cuja área será alocada
dinamicamente. Por exemplo:

typedef int * PtrInt;


PtrInt P;

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>

typedef int * PtrInt;

int main(){
PtrInt P;
P = new int;
*P = 10;
std::cout << P << "\n" << *P << "\n";
delete P;
return 0;
}

Neste exemplo, a variável P é declarada como um ponteiro para um int. Antes da


chamada ao operador new, o conteúdo de P é indeterminado. Quando o comando:

P = new int;
Algoritmos e Estrutura de Dados I – Capítulo 1 – 8

é executado, uma área na memória é alocada dinamicamente e o endereço dessa


área é atribuído a P. Qual o tamanho da área alocada? Naturalmente, isso depende do tipo
base indicado na declaração do ponteiro. Neste exemplo, é alocada uma área suficiente para
armazenar valores do tipo int. Tipicamente, porém, o tipo base de um ponteiro é algum tipo
estruturado, bem mais complexo que um simples int. A área alocada não tem um nome. Seu
endereço, porém, fica armazenado no ponteiro utilizado durante a chamada ao operador
new. Quando não é mais necessária a área para onde P aponta, utiliza-se o operador delete
para liberar o respectivo espaço de memória.

Pode-se atribuir NULL a qualquer ponteiro, e também pode-se testar se um ponteiro é


igual ou diferente de NULL.

Os operadores new e delete trabalham com alocação dinâmica de memória. A parte


da memória onde os diversos blocos são alocados e liberados é chamada heap. Quando um
bloco é alocado dinamicamente, a aplicação recebe o endereço do bloco e, armazenando-o
em um ponteiro, pode usar o bloco, guardando nele valores de variáveis que lhe interessam.
As variáveis estáticas - ou seja, as que não são alocadas dinamicamente - também ocupam,
logicamente, um espaço na memória. Têm, portanto, um endereço. O endereço de uma
variável, seja ela estática ou dinâmica, pode ser obtido através do operador &. Por exemplo,
o comando:

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

Uma função é um conjunto de instruções projetadas para cumprir uma tarefa


específica e agrupadas numa unidade com um identificador próprio. A principal razão para se
usar funções é dividir a tarefa original em módulos que simplifiquem e organizem o programa
como um todo. Outra razão é reduzir o tamanho do programa. Qualquer sequência de
instruções que se repita no programa é candidata a ser uma função [5].

As funções permitem ao programador modularizar um programa. Todas as variáveis


declaradas em definições de função são variáveis locais, ou seja, elas só são conhecidas na
função na qual são definidas. A maioria das funções tem uma lista de parâmetros que
proveem os meios de transferir informações entre funções. Os parâmetros de uma função
também são variáveis locais.
Algoritmos e Estrutura de Dados I – Capítulo 1 – 9

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 quadrado( int ); // prototipo

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 )

A função quadrado recebe uma cópia do valor da variável x através do parâmetro y.


Então, quadrado calcula y * y. O resultado é passado de volta para o ponto em main onde
quadrado foi chamada. Esse processo é repetido 50 vezes, através da estrutura de
repetição. A definição de quadrado indica que ela espera receber um parâmetro inteiro y. A
palavra-chave int antes do nome da função indica que quadrado retorna um resultado inteiro.
O comando return passa o resultado do cálculo de volta para a função chamadora. Em
adição, a linha:
Algoritmos e Estrutura de Dados I – Capítulo 1 – 10

int quadrado( int ); // protótipo

é um protótipo de função. O tipo de dados int entre parêenteses informa ao compilador


que quadrado espera receber um valor inteiro da função que faz a chamada. O protótipo da
função não é necessário se a definição da função aparece antes do primeiro uso da função
no programa. Neste caso, a própria definição da função funciona como protótipo.

O nome da função é qualquer identificador válido. O tipo de retorno é o tipo de dado


do resultado devolvido. Um tipo de valor de retorno void indica que a função não retorna um
valor. A lista de parâmetros é uma lista separada por vírgulas, contendo as declarações dos
parâmetros recebidos pela função quando ela é executada. Se uma função não recebe
nenhum valor, a lista de parâmetros é void ou simplesmente vazia. É um erro de sintaxe não
declarar o tipo para cada parâmetro da função, mesmo que sejam do mesmo tipo. Por
exemplo, declarar int A, B em vez de int A, int B geraria um erro, porque os tipos são
obrigatórios para cada parâmetro da lista de parâmetros.

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;

retorna o valor da expressão para a função que realizou a chamada.

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.

6.1 Passagem de parâmetros por valor

No exemplo apresentado anteriormente, a função criou uma nova variável para


Algoritmos e Estrutura de Dados I – Capítulo 1 – 11

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>

void Somar(int X){


X++;
std::cout << X << "\n";
}

int main(){
int Y=0;
Somar(Y);
std::cout << Y << "\n";
return 0;
}

6.2 Passagem de parâmetros por referência

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;
}

Neste exemplo, as instruções:

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;
}

Neste exemplo, a função Somar incrementa a variável parâmetro X. Como X é uma


referência da variável Y, o valor de Y é alterado, prevalecendo mesmo depois que a função
foi encerrada, passando a valer 1.

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

6.3 Passando vetores como parâmetros de função

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];

o comando de chamada de função:

modificaIdades (Idades, 10);

passa o vetor Idades e seu tamanho para a função modificaIdades. Frequentemente,


ao passar um vetor para uma função, o tamanho do vetor também é passado, para que a
função possa processar o número total de elementos do vetor.

A linguagem C++ automaticamente passa os vetores às funções usando chamadas


por referência simulada. Isso ocorre por motivos de desempenho. Se os vetores fossem
passados por valor, seria passada uma cópia de cada elemento. Para vetores grandes isso
seria demorado e consumiria um espaço considerável de memória.

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:

void modificaIdades (int b[], int tam);

indicando que modificaIdades espera receber o endereço de um vetor de inteiros no


parâmetro b e o número de elementos do vetor no parâmetro tam. A seguir um exemplo
completo:

// Programa 007
// Passando vetores como parâmetros de função

#include <iostream>

void modificaIdades( int [], int );


Algoritmos e Estrutura de Dados I – Capítulo 1 – 15

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.

6.4 Ponteiros para funções

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].

O exemplo clássico mais simples de recursividade é o cálculo do fatorial de um inteiro


n diferente de 0. Uma função recursiva para efetuar esse cálculo encontra-se em seguida:

long fatorial (int i) {


if (i <= 1)
return 1;
else
return i * fatorial (i-1);
}

Um exemplo conhecido, onde a solução recursiva é natural e intuitiva, é o do


Problema da Torre de Hanoi. Este consiste em três pinos, A, B e C, denominados origem,
Algoritmos e Estrutura de Dados I – Capítulo 1 – 18

destino e trabalho, respectivamente, e n discos de diâmetros diferentes. Inicialmente, todos


os discos se encontram empilhados no pino-origem, em ordem decrescente de tamanho, de
baixo para cima. O objetivo é empilhar todos os discos no pino-destino, atendendo às
seguintes restrições:

1. Apenas um disco deve ser deslocado por vez;


2. Em todo instante, os discos devem estar em uma das 3 hastes;
3. Em momento algum um disco de raio maior deve ser colocado sobre outro disco de
raio menor que o dele;
4. A terceira haste pode ser usada temporariamente.

A solução do problema é descrita a seguir. Naturalmente, para n > 1, o pino-trabalho


deverá ser utilizado como área de armazenamento temporário. O raciocínio utilizado para
resolver o problema é semelhante ao de uma prova matemática por indução. Suponha que
se saiba como resolver o problema até n − 1 discos, n > 1, de forma recursiva. A extensão
para n discos pode ser obtida pela realização dos seguintes passos:

1. Resolver o problema da Torre de Hanoi para os n − 1 discos do topo do


pino-origem A, supondo que o pino-destino seja C e o trabalho seja B;
2. Mover o n-ésimo pino (maior de todos) de A para B;
3. Resolver o problema da Torre de Hanoi para os n−1 discos localizados no pino C,
suposto origem, considerando os pinos A e B como trabalho e destino,
respectivamente.

Ao final desses passos, todos os discos se encontram empilhados no pino B e as


restrições foram satisfeitas. O código a seguir implementa o processo:

// 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;
};

onde Esquerda e Direita representam os dígitos posicionados à esquerda e à direita


do ponto decimal, respectivamente. Se Esquerda for um inteiro negativo, o número real
representado será negativo.

(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.

2. Suponha dois vetores, um de registros de estudantes e outro de registros de funcionários.


Cada registro de estudante contém membros para um último nome, um primeiro nome e um
índice de pontos de graduação. Cada registro de funcionário contém membros para um
último nome, um primeiro nome e um salário. Ambos os vetores são classificados em ordem
alfabética pelo último e pelo primeiro nome. Dois registros com o último e o primeiro nome
iguais não aparecem no mesmo vetor. Escreva uma função em C/C++ para conceder um
aumento de 10% a todo funcionário que tenha um registro de estudante cujo índice de
pontos de graduação seja maior que 3.0.

3. Qual é a diferença entre operador de referência e o de endereço?

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

5. Declare uma matriz de 12 estruturas descritas na questão anterior e inicialize-a com os


dados de um ano não-bissexto.

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.

7. Assuma as declarações abaixo e indique qual é o valor das seguintes expressões:

int i=3, j=5;


int *p=&i, *q=&j;
(a) p == &i;
(b) *p - *q;
(c) **&p;
(d) 3*-*p/*q+7;

8. Explique cada uma das seguintes declarações e identifique quais são incorretas.

(a) int *const x=&y;


(b) const int &x=*y;
(c) int &const x=*y;
(d) int const *x=&y;

9. O que declara cada uma destas instruções?

(a) int (*ptr)[10];


(b) int *ptr[10];
(c) int (*ptr)();
(d) int *ptr();
(e) int (*ptr[10])( );

10. Identifique, em função de n, o número de movimentos de disco utilizados no problema da


Torre de Hanoi.

11. Elaborar uma função não recursiva para o problema da Torre de Hanoi.

12. Simule a execução do seguinte programa, destacando a sua saída:

#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";
}

13. Escreva uma função de protótipo:

void strSubstitui (char s[ ], const char s1[ ], const char s2[ ]);

que substitui a string s1 pela string s2 na string s.

14. Considere a seguinte sequencia de elementos g1, . . . , gn para um dado valor de k.

gj = j − 1, 1 # j # k;
gj = gj−1 + gj−2, j > k.

Elaborar uma função para determinar o elemento gn da sequência.


Algoritmos e Estrutura de Dados I – Capítulo 1 – 22

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)