estrutura de dados AJSG.pdf

69
Estrutura de Dados Prof. airton josé sachetim garcia Departamento de Engenharia Prof. Airton José Sachetim Garcia Estrutura de Dados Londrina 2015.2

Transcript of estrutura de dados AJSG.pdf

Page 1: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Departamento de Engenharia

Prof. Airton José Sachetim Garcia

Estrutura de Dados

Londrina 2015.2

Page 2: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Faculdade PROGRAMA DE GRADUAÇÃO EM ENGENHARIA

DEPARTAMENTO DE ENGENHARIA

Airton José Sachetim Garcia

C++

LONDRINA-PR

2015.2

Page 3: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

SACHETIM GARCIA, Airton José.

Software Excel 2010 / SACHETIM GARCIA, Airton José.

Londrina:

Coordenador (a):

Matéria (Algoritmo e Estrutura de Dados, C++) – Faculdade

Referências: f. 110

Page 4: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

INTRODUÇÃO

Este material apresenta uma coleção de algoritmos sobre uma variedade de estruturas de

dados de memória principal, estudadas na disciplina de Estrutura de Dados – Algoritmos e

Estruturas de Dados.

Page 5: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1 Aula. 1.1 Uma breve história do C++

O C++ foi inicialmente desenvolvido por Bjarne Stroustrup dos Bell Labs, durante a década de

1980 com o objetivo de implementar uma versão distribuída do núcleo do Unix. Como o Unix

era escrito em C, dever-se-ia manter a compatibilidade, ainda que adicionando novos recursos.

O C foi escolhido como base de desenvolvimento da nova linguagem, pois possuía uma

proposta de uso genérico, era rápido e também portável para diversas plataformas. Algumas

outras linguagens que também serviram de inspiração para o cientista da computação foram

ALGOL 68, Ada, CLU e ML.

Ainda em 1983 o nome da linguagem foi alterado de C with Classes para C++. Antes

implementada usando um pré-processador, a linguagem passou a exigir um compilador

próprio, escrito pelo próprio Stroustrup.

Novas características foram adicionadas, como funções virtuais, sobrecarga de operadores e

funções, melhorias na verificação de tipo de dado e estilo de comentário de código de uma

linha (//).

Em 1985 foi lançada a primeira edição do livro The C++ Programming Language, contendo

referências para a utilização da linguagem, já que ainda não era uma norma oficial.

A primeira versão comercial foi lançada em outubro do mesmo ano.

Em 1989 a segunda versão foi lançada, contendo novas características como herança múltipla,

classes abstratas, métodos estáticos, métodos constantes e membros protegidos,

incrementando o suporte a orientação a objeto.

Assim como a linguagem, sua biblioteca padrão também sofreu melhorias ao longo do tempo.

Sua primeira adição foi a biblioteca de E/S, e posteriormente a Standard Template Library

(STL); ambas tornaram-se algumas das principais funcionalidades que distanciaram a

linguagem em relação a C.

Criada primordialmente na HP por Alexander Stepanov no início da década de 1990 para

explorar os potenciais da programação genérica, a STL foi apresentada a um comitê unificado

ANSI e ISO em 1993 à convite de Andrew Koenig.

Após uma proposta formal na reunião do ano seguinte, a biblioteca recebe o aval do comitê.

Pode-se dizer que C++ foi a única linguagem entre tantas outras que obteve sucesso como uma

sucessora à linguagem C, inclusive servindo de inspiração para outras linguagens como Java, a

IDL de CORBA e C#.

Page 6: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.2 Compiladores

Um compilador é um programa de sistema que traduz um programa descrito em uma

linguagem de alto nível para um programa equivalente em código de máquina para um

processador. Em geral, um compilador não produz diretamente o código de máquina, mas sim

um programa em linguagem simbólica (assembly) semanticamente equivalente ao programa

em linguagem de alto nível. O programa em linguagem simbólica é então traduzido para o

programa em linguagem de máquina através de montadores.

Para desempenhar suas tarefas, um compilador deve executar dois tipos de atividade. A

primeira atividade é a análise do código fonte, onde a estrutura e significado do programa de

alto nível são reconhecidos. A segunda atividade é a síntese do programa equivalente em

linguagem simbólica. Embora conceitualmente seja possível executar toda a análise e apenas

então iniciar a síntese, em geral estas duas atividades ocorrem praticamente em paralelo. Para

apresentar um exemplo das atividades que um compilador deve desempenhar, considere o

seguinte trecho de um programa em C:

Para o compilador, este segmento nada mais é do que uma seqüência de caracteres em um

arquivo texto. O primeiro passo da análise é reconhecer que agrupamentos de caracteres têm

significado para o programa, por exemplo, saber que int é uma palavra-chave da linguagem e

que a e b serão elementos individuais neste programa. Posteriormente, o compilador deve

reconhecer que a seqüência int a ,corresponde a uma declaração de uma variável inteira cujo

identificador recebeu o nome a.

As regras de formação de elementos e frases válidas de uma linguagem são expressas na

gramática da linguagem. O processo de reconhecer os comandos de uma gramática é

conhecido como reconhecimento de sentenças.

Page 7: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.3 Estrutura Básica de um programa C++.

Temos abaixo a estrutura de um programa escrito na linguagem C++:

As duas primeiras linhas são o cabeçalho do programa. Todo programa deve ter um cabeçalho

desse tipo para definir quais as bibliotecas ele utilizará. “Bibliotecas” são arquivos que

normalmente são instalados juntos com o compilador e que possuem os comandos e funções

pertencentes à linguagem.

#include<>

Serve para indicar ao compilador todas as bibliotecas que este programa utilizará. Na maioria

dos programas que escreveremos durante esta apostila, só utilizaremos o

#include <iostream>

,que serve para incluir a biblioteca iostream em nossos programas.

Esta biblioteca contém as principais funções, comandos e classes de entrada e saída de C++,

necessárias para realizar programas que, por exemplo, recebam dados via teclado e enviem

dados via monitor.

A segunda linha do cabeçalho,

using namespace std;

, é um aviso ao compilador que estaremos utilizando os comandos e funções padrão de C++.

Ele é necessário porque em C++ podemos criar várias bibliotecas para serem utilizáveis em

vários programas.

Cada uma dessas bibliotecas contém comandos, classes e funções próprias, e para evitar

confusões e problemas com os nomes destes comandos, utilizamos o cabeçalho “using

namespace ...;” para definir qual o campo de nomes que estamos utilizando.

Num programa normal, que não utiliza outras bibliotecas além do padrão de C++, utilizamos o

namespace std como nosso campo de nomes de comandos e funções. Assim, sempre que

utilizamos um comando próprio de C++, o compilador reconhecerá automaticamente este

comando como sendo pertencente à biblioteca padrão de C++.

Assim como em C++, tudo o que acontece durante a execução do programa está

contido dentro de uma função principal, chamada main. Declaramos a função main com:

Page 8: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

int main ( )

Todos os comandos executados pelo programa estão contidos entre as chaves “{ }”

da função main.

O encerramento de um programa geralmente é feito da mesma maneira para todos eles. As

duas últimas linhas antes do fecha-chaves são dois comandos normalmente utilizados ao

fim de um programa.

system(“PAUSE > null”)

é uma chamada de função própria de C++.

A função system( ) recebe argumentos como o PAUSE que na verdade são comandos para o

sistema operacional. Neste caso, ela recebe o comando “PAUSE > null” para pausar a

execução do programa até que o usuário aperte uma tecla qualquer. Utilizamos este recurso

para que a tela do programa não seja terminada automaticamente pelo sistema, impedindo

que vejamos os resultados do programa.

Finalmente, o comando

return 0

é a “resposta” da função main para o sistema.

1.4 Tipos de Variáveis

Quando definimos uma variável em C++, precisamos informar ao compilador o tipo da variável:

um número inteiro, um número de ponto flutuante, um caractere, e assim por diante. Essa

informação diz ao compilador quanto espaço deve ser reservado na memória para a variável, e

o tipo de valor que será armazenado nela. As variáveis mais utilizadas, em um curso de C++

básico são: A tabela abaixo apresenta os tipos para valores numéricos inteiros.

A linguagem oferece ainda dois tipos básicos para a representação de números reais (ponto

flutuante): float e double. A tabela abaixo compara estes dois tipos.

Page 9: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.5 Declaração de variáveis.

1.5.1 Variável.

É a maneira pela qual se faz a alocação de memória para armazenarmos um dado (valor) na

memória do computador, deve-se reservar o espaço correspondente ao tipo do dado a ser

armazenado.

1.5.2 Declarar variável.

É a associação de um nome (da variável) a este espaço de memória que foi utilizado para

armazenar um dado.

1.5.3 Constantes.

Quando temos que usar valores constantes, que não se alteram durante o processo.

A diferença básica em relação às variáveis, como os nomes dizem (variáveis e constantes), é

que o valor armazenado numa área de constante não pode ser alterado.

1.5.4 Restrições para as atribuições de variáveis.

As constantes são valores que serão mantidos fixos pelo compilador;

O nome das variáveis deve começar com uma letra ou um sublinhado “_”;

Os demais caracteres podem ser letras, números ou sublinhado;

O nome da variável não pode ser igual a uma palavra reservada e aos nomes das

funções;

Tamanho máximo para o nome de uma variável é 32 caracteres.

1.5.5 Modificador const.

A linguagem C++ introduz um novo modificador chamado const, que tem comportamento

variado dependendo do local onde está sendo declarado. Sua função, basicamente, é

estabelecer um vínculo entre declaração e obrigatoriedade da coerência no uso do símbolo

declarado. A princípio, quando declaramos uma constante com este modificador fazemos com

que seja obrigatório o uso do símbolo de forma que o mesmo não possa ter seu valor alterado.

Assim, se fizermos:

Page 10: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

A constante x não poderá deixar de ter valor igual a 4. Qualquer tentativa de modificar o valor

da constante ao longo do programa será reportada como erro pelo compilador.

1.6 O comando Include e as Bibliotecas.

C++ tem a capacidade de importar bibliotecas.

A importância da biblioteca em C é imensa, pois ela nos poupa de muita programação. Uma

vez que a função já está pronta dentro da biblioteca, basta importar tal biblioteca e utilizar a

função que queremos.

Caso quiséssemos mostrar uma mensagem na tela, você não tem que produzir uma função

inteira ou criar um comando novo, basta importar a biblioteca <iostream.h>, que possui a

finalidade de I/O (entrada e saída) de dandos. Quando o programa for compilado, o

compilador irá buscar nas bibliotecas exigidas pelo usuário tais funções para saber como

utilizá-las no programa.

O papel do pré-processamento é indicar, antes mesmo de compilar, os parâmetros necessários

para ser criado o arquivo executável.

A importação de uma biblioteca é dada pelo comando #include (incluir) seguido da biblioteca

entre os sinais de menor (<) e maior (>).

Como importar a biblioteca padrão de entrada e saída de C++.

As bibliotecas de C são diferentes das bibliotecas de C++. Apesar de muitos compiladores de

C++ suportarem as bibliotecas de C, nenhum compilador exclusivamente de C suporta

bibliotecas de C++.

1.6.1 Algumas bibliotecas do C++.

<algorithm>

<fstream>

<functional>

<iostream>

<locale>

<map>

<set>

<sstream>

<string>

<vector>

<Math.h>

Page 11: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.7 Biblioteca <Math.h>.

A biblioteca <math.h> nos ajuda nos cálculos matemáticos, onde podemos encontrar

facilmente funções para calcular potências, raíz quadrada, funções trigonométricas para

cálculos que envolvem seno, co-seno e tangente, além de constantes para números irracionais

como, por exemplo, PI (Π) e √2.

1.7.1 Trigonométricas.

sin (): Retorna o valor do seno. Recebe como argumento o valor dos graus em double.

cos (): Retorna o valor do co-seno. Recebe como argumento o valor dos graus em

double.

tan (): Retorna o valor da tangente. Recebe como argumento o valor dos graus em

double.

1.7.2 Logarítmicas.

log (): Retorna o valor do logaritmo na base 2. Exige um argumento do tipo double.

log10 (): Retorna o valor do logaritmo na base 10. Exige um argumento do tipo double.

1.7.3 Potência.

sqrt (): Retorna o valor da raiz quadrada. Recebe como argumento um double do qual

ele deve extrair a raiz.

pow (): Retorna o valor da base elevada ao expoente. Recebe dois argumentos do tipo

double, o primeiro é a base e o segundo o expoente. Por exemplo: se quisermos saber

o resultado da operação 210,faríamos pow (2, 10).

1.8 Operadores.

A linguagem C++ oferece uma gama variada de operadores, entre binários e unários. Os

operadores básicos são apresentados a seguir.

1.8.1 Operadores Aritméticos.

1.8.1.1 Operadores aritméticos binários.

adição (+);

subtração (-);

multiplicação (*);

divisão (/);

operador módulo (%).

Page 12: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

A operação é feita na precisão dos operandos. Assim, a expressão 5/2 resulta no valor 2, pois a

operação de divisão é feita em precisão inteira, já que os dois operandos (5 e 2) são constantes

inteiras. A divisão de inteiros trunca a parte fracionária, pois o valor resultante é sempre do

mesmo tipo da expressão. Conseqüentemente, a expressão 5.0/2.0 resulta no valor real 2.5,

pois a operação é feita na precisão real (double, no caso).

O operador módulo, %, não se aplica a valores reais, seus operandos devem ser do tipo

inteiro. Este operador produz o resto da divisão do primeiro pelo segundo operando. Como

exemplo de aplicação deste operador, podemos citar o caso em que desejamos saber se o

valor armazenado numa determinada variável inteira x é par ou ímpar. Para tanto, basta

analisar o resultado da aplicação do operador %, aplicado à variável e ao valor dois.

1.8.2 Precedência de operadores.

Os operadores *, / e % têm precedência maior que os operadores + e -. O operador (-)

unário tem precedência maior que ( *, / e %). Operadores com mesma precedência são

avaliados da esquerda para a direita. Assim, na expressão:

a + b * c /d

executa-se primeiro a multiplicação, seguida da divisão, seguida da soma. Podemos utilizar

parênteses para alterar a ordem de avaliação de uma expressão. Assim, se quisermos avaliar

a soma em primeiro, podemos escrever:

(a + b) * c /d

1.8.3 Precedência e ordem de avaliação dos operadores.

A tabela abaixo mostra a precedência, em ordem decrescente, dos principais operadores da

linguagem C++.

1.8.4 Operadores de atribuição (=).

A expressão a = 7, armazena na variável a o valor sete.

Page 13: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

A linguagem também permite utilizar os chamados operadores de atribuição compostos.

Comandos do tipo:

i = i + 2; em que a variável à esquerda do sinal de atribuição também aparece à direita, podem ser

escritas de forma mais compacta:

i += 2.

Usando o operador de atribuição composto +=. Analogamente, existem, entre outros, os

operadores de atribuição: -=, *=, /=, %=. De forma geral, comandos do tipo:

var op= expr;

são equivalentes a:

var = var op (expr);

Salientamos a presença dos parênteses em torno de expr. Assim:

x *= y + 1;

equivale a

x = x * (y + 1)

e não a

x = x * y + 1;

1.8.5 Operadores de incremento e decremento (++, --).

Os dois operadores não convencionais. São os operadores de incremento e decremento, que

possuem precedência comparada ao - unário e servem para incrementar e decrementar uma

unidade nos valores armazenados nas variáveis. Assim, se n é uma variável que armazena um

valor, o comando:

n++;

incrementa de uma unidade o valor de n (análogo para o decremento em n--). O aspecto não usual é que ++ e -- podem ser usados tanto como operadores pré-fixados (antes da variável, como em ++n) ou pós-fixados (após a variável, como em n++). Em ambos os casos, a variável n é incrementada. Porém, a expressão ++n incrementa n antes de usar seu valor, enquanto n++ incrementa n após seu valor ser usado. Isto significa que, num contexto onde o valor de n é usado, ++n e n++ são diferentes. Se n armazena o valor cinco, então:

x = n++; atribui 5 a x, mas:

x = ++n;

atribuiria 6 a x. Em ambos os casos, n passa a valer 6, pois seu valor foi incrementado em

uma unidade. Os operadores de incremento e decremento podem ser aplicados somente em

variáveis; uma expressão do tipo x = (i + 1)++ é ilegal.

Page 14: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Mesmo para programadores experientes, o uso das formas compactas deve ser feito com

critério. Por exemplo, os comandos:

a = a + 1;

a += 1;

a++;

++a;

são todos equivalentes e o programador deve escolher o que achar mais adequado e simples.

Em termos de desempenho, qualquer compilador razoável é capaz de otimizar todos estes

comandos da mesma forma.

1.8.6 Operadores relacionais e lógicos.

1.8.6.1 Relacionais.

Estes operadores comparam dois valores. O resultado produzido por um operador relacional

é zero ou um. Em C++, não existe o tipo booleano (true ou false). O valor zero é interpretado

como falso e qualquer valor diferente de zero é considerado verdadeiro. Assim, se o resultado

de uma comparação for falso, produz-se o valor 0, caso contrário, produz-se o valor 1.

1.8.6.2 Lógicos.

Expressões conectadas por && ou || são avaliadas da esquerda para a direita, e a avaliação

pára assim que a veracidade ou falsidade do resultado for conhecida. Recomendamos o uso

de parênteses em expressões que combinam operadores lógicos e relacionais.

Os operadores relacionais e lógicos são normalmente utilizados para tomada de decisões.

No entanto, podemos utilizá-los para atribuir valores a variáveis. Por exemplo, o trecho de

código abaixo é válido e armazena o valor 1 em a e 0 em b.

Page 15: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Na avaliação da expressão atribuída à variável b, a operação (d>c) não chega a ser avaliada,

pois independente do seu resultado a expressão como um todo terá como resultado 0 (falso),

uma vez que a operação (c<20) tem valor falso.

1.8.7 Operador sizeof.

Outro operador fornecido por C, sizeof, resulta no número de bytes de um determinado

tipo. Por exemplo:

Armazena o valor 4 na variável a, pois um float ocupa 4 bytes de memória. Este operador

pode também ser aplicado a uma variável, retornando o número de bytes do tipo associado

à variável.

1.9 Conversão de tipo.

Na maioria das linguagens, existem conversões automáticas de valores na avaliação de uma

expressão. Assim, na expressão 3/1.5, o valor da constante 3 (tipo int) é promovido

(convertido) para double antes de a expressão ser avaliada, pois o segundo operando é do tipo

double (1.5) e a operação é feita na precisão do tipo mais representativo.

Quando, numa atribuição, o tipo do valor atribuído é diferente do tipo da variável, também

há uma conversão automática de tipo. Por exemplo, se escrevermos:

int a = 3.5; o valor 3.5 é convertido para inteiro (isto é, passa a valer 3) antes de a atribuição

ser efetuada. Como resultado, como era de se esperar, o valor atribuído à variável é 3 (inteiro).

Alguns compiladores exibem advertências quando a conversão de tipo pode significar uma

perda de precisão (é o caso da conversão real para inteiro, por exemplo).

O programador pode explicitamente requisitar uma conversão de tipo através do uso do

operador de molde de tipo (operador cast). Por exemplo, são válidos (e isentos de qualquer

advertência por parte dos compiladores) os comandos abaixo.

int a, b;

a = (int) 3.5;

b = (int) 3.5 % 2;

Precedência e ordem de avaliação dos operadores. Em ordem decrescente, dos principais operadores da linguagem C++.

Page 16: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.10 Entrada e saída básicas.

Em C++ tudo é feito através de funções, inclusive as operações de entrada e saída. Por isso, já

existe em C uma biblioteca padrão que possui as funções básicas normalmente necessárias. Na

biblioteca padrão de C++, podemos, por exemplo, encontrar funções matemáticas do tipo raiz

quadrada, seno, cosseno, etc., funções para a manipulação de cadeias de caracteres e funções de

entrada e saída. Nesta seção, serão apresentadas as duas funções básicas de entrada e saída

disponibilizadas pela biblioteca padrão. Para utilizá-las, é necessário incluir o protótipo destas

funções no código. Este assunto foi abordado na seção 1.6.

Por ora, basta saber que é preciso escrever:

#include <iostream.h> C++.

#include <stdio.h> C;

1.10.1 Saída.

cout << “Exemplo”<<endl; C++.

cout é o dispositivo de saída padrão no C++ (geralmente o monitor), e a frase completa

insere uma seqüência de caracteres no dispositivo de saída. O cout é declarado no

arquivo de cabeçalho <iostream.h>, então pra que seja possível utiliza-lo, esse arquivo

precisa ser incluso. Note que a frase termina com um caractere ponto-e-vírgula (Esse

caractere significa o fim da instrução e precisa ser incluído após toda instrução em

qualquer programa em C++ )Um dos erros mais comuns dos programadores de C++ é

devido ao fato de esquecerem de incluir um ponto-e-vírgula; no final de cada

instrução.

printf (formato, lista de constantes/variáveis/expressões...); C;

O primeiro parâmetro é uma cadeia de caracteres, em geral delimitada com aspas, que

especifica o formato de saída das constantes, variáveis e expressões listadas em seguida.

Para cada valor que se deseja imprimir, deve existir um especificador de formato

correspondente na cadeia de caracteres formato. Os especificadores de formato variam

com o tipo do valor e a precisão em que queremos que eles sejam impressos. Estes

especificadores são precedidos pelo caractere % e podem ser, entre outros:

Alguns exemplos em C: printf ("%d %g\n", 33, 5.3);

tem como resultado a impressão da linha:

33 5.3

Ou:

printf ("Inteiro = %d Real = %g\n", 33, 5.3);

com saída:

Inteiro = 33 Real = 5.3

Page 17: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

isto é, além dos especificadores de formato, podemos incluir textos no formato, que

são mapeados diretamente para a saída. Assim, a saída é formada pela cadeia de

caracteres do formato onde os especificadores são substituídos pelos valores

correspondentes.

Existem alguns caracteres de escape que são freqüentemente utilizados nos formatos

de saída. São eles:

Ainda, se desejarmos ter como saída um caractere %, devemos, dentro do formato,

escrever %%.

É possível também especificarmos o tamanho dos campos:

A função printf retorna o número de campos impressos. Salientamos que para cada

constante, variável ou expressão listada devemos ter um especificador de formato

apropriado.

1.10.2 Entrada.

cin>>variável; C++

A entrada padrão no C++ é feita aplicando-se o operador sobrecarregado de extração

(>>) no comando cin. Isso precisa ser seguido pela variável que irá guardar o dado que

será lido.

Declarando a variável como desejada, então espera por uma entrada do cin (teclado)

para que possa guardá-la em um espaço reservado da memória ROM. O comando cin

só pode processar a entrada do teclado depois que a tecla ENTER for pressionada.

Sendo assim, mesmo que você peça um único caractere, o cin não irá processar a

entrada até que o usuário pressione ENTER depois que o caractere tenha sido digitado.

Você precisa sempre considerar o tipo da variável que você está usando para guardar

o valor extraído pelo cin. Se você pedir um inteiro, você receberá um inteiro, se você

Page 18: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

pedir um caractere, você receberá um caractere, e se você pedir uma string de

caracteres, você receberá uma string de caracteres.

scanf (formato, lista de endereços das variáveis...); C.

O formato deve possuir especificadores de tipos similares aos mostrados para a

função printf. Para a função scanf, no entanto, existem especificadores diferentes para

o tipo float e o tipo double:

A principal diferença é que o formato deve ser seguido por uma lista de endereços de

variáveis (na função printf passamos os valores de constantes, variáveis e expressões).

Na seção sobre ponteiros, este assunto será tratado em detalhes. Por ora, basta saber

que, para ler um valor e atribuí-lo a uma variável, devemos passar o endereço da

variável para a função scanf. O operador & retorna o endereço de uma variável. Assim,

para ler um inteiro, devemos ter:

int n;

scanf ("%d", &n);

Para a função scanf, os especificadores %f, %e e %g são equivalentes. Aqui, caracteres

diferentes dos especificadores no formato servem para cercar a entrada. Por exemplo:

scanf ("%d:%d", &h, &m);

obriga que os valores (inteiros) fornecidos sejam separados pelo caractere dois pontos

(:).

Um espaço em branco dentro do formato faz com que sejam "pulados" eventuais

brancos da entrada. Os especificadores %d, %f, %e e %g automaticamente pulam os

brancos que precederem os valores numéricos a serem capturados. A função scanf

retorna o número de campos lidos com sucesso.

1.11 Controle de fluxo.

C++ provê as construções fundamentais de controle de fluxo necessárias para

programas bem estruturados: agrupamentos de comandos, tomadas de decisão (if-else),

laços com teste de encerramento no início (while, for) ou no fim (do-while), e seleção

de um dentre um conjunto de possíveis casos (switch).

Page 19: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.11.1 Decisão.

Os métodos de tomada de decisão no C++ estão presentes para as tarefas mais corriqueiras

que o programa deve executar.

1.11.1.1 Estrutura IF/ELSE.

Uma ação muito importante que o processador de qualquer computador executa, e que o

torna diferente de qualquer outra máquina, é a tomada de decisão definindo o que é

verdadeiro e o que é falso.

Page 20: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

É possível observar que podemos criar um programa com quantas condições queremos,

restringindo a cada condição, uma ação ou um conjunto de ações.

Alguns exemplos para serem refeitos.

Elabore um programa que diga qual ação, ou ações foram escolhidas:

a) Ação 1: caso o número seja maior ou igual a 2.

b) Ação 2: caso o número seja maior que 1.

c) Ação 3: caso não seja satisfeita nenhuma condição.

Observe que o erro encontra-se no uso do “else if”, com ele, excluímos possibilidades possíveis

de respostas. Por exemplo, se digitarmos o número 3 no programa acima, o compilador nos

dará como saída apenas a primeira condição ("Ação 1 escolhida”), onde na verdade temos

duas respostas, pois satisfaz a ação 1 e 2 simultaneamente. Se substituirmos o “if” no lugar do

“else if”, o compilador nos dará as 2 respostas possíveis ("Ação 1 escolhida” e "Ação 2

escolhida”), com isso, corrigiríamos o problema da redundância do nosso exemplo. Com o uso

apenas do “if” e do “else” é possível o compilador executar várias condições que ocorram

simultaneamente.

Exemplos de Aplicação.

1. Dados dois números reais quaisquer, desenvolva um programa que diga se eles são

iguais ou diferentes.

Solução:

Page 21: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

2. Dado um número qualquer, determinar se este é neutro, positivo ou negativo.

Solução:

3. Elabore um programa que identifique se um número inteiro digitado é par, impar ou

nulo

Solução:

#include<iostream>

#include<conio.h>

#include <math.h>

using namespace std;

main()

{int n;

cout<<"digite o numero"<<endl;

cin>>n;

if (n%2==0)

{

if (n==0)

{cout<<" nulo "<<endl;}

else

{cout<<" par "<<endl;}

}

else

{cout<<"impar"<<endl;}

getch();}

Page 22: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

4. Dado um número real qualquer, calcule sua raiz quadrada.

Solução:

Observe que o comando pow serve para realizar operações com exponenciais. No nosso caso:

a = pow(n,0.5), estamos atribuindo à variável a, a seguinte expressão exponencial n elevado a

0.5. De forma genérica, no comando pow (A,B), teremos a função exponencial, onde A é a base

e B o expoente.

1.11.2 Repetição.

As estruturas de repetições são muito importantes para a solução de problemas na

programação, pois muitas vezes os mesmo procedimentos têm que ser executados mais de

uma vez, ou um número de vezes variável.

Em C/C++, basicamente existem três tipos de estrutura de repetição: for, while e do while.

1.11.2.1 Estrutura FOR.

Para o for, como qualquer iteração (repetição), precisa de uma variável para controlar os loops

(voltas). No for, essa variável deverá ser iniciada, indicando pelo seu critério de execução, e

forma de incremento ou decremento. Ou seja, o for precisa de três condições. Vale salientar

que essas condições são separadas por ponto-e-vírgula. O comando deve ser inserido no

compilador da seguinte forma:

Um dos exemplos mais utilizados é o cálculo da potência de um número, ou o fatorial de um

número, para a potência o usuário informa ao programa a base e o expoente. O programa

deve fazer o número de interações iguais o número x do expoente. Considerando o número do

expoente natural, positivo e maior que zero, temos o seguinte programa:

Page 23: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Observe que em “for (int i=0; i<x; i++ )”, temos as três condições dentro do parênteses.

Exemplos de Aplicação.

1. Elabore um programa que calcule o fatorial de um número dado.

Solução:

2. Um número é dito perfeito quando a soma de seus divisores (exceto ele mesmo), é ele

próprio. Exemplo 28, divisores = 14+7+4+2+1 =28. Elabore um programa que diga se o

número digitado é perfeito ou não.

Solução:

Page 24: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.11.2.2 Estrutura WHILE.

Outra forma de iteração (repetição) em C/C++ é o WHILE. O while executa uma comparação

com a variável. Se a comparação for verdadeira, ele executa o bloco de instruções, quantas

vezes forem necessárias, até a comparação se tornar falsas.

Exemplos de Aplicação.

1. Elabore um programa que imprima os termos de uma progressão aritmética cujo

primeiro termo é 3 e a razão 5. Parar o processamento quando for impresso um termo

maior que 100.

Solução:

Page 25: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

2. Elabore um algoritmo que imprima os termos da serie abaixo. Parar o processamento

quando for impresso um termo negativo.

15, 30, 60, 12, 24, 48, 9, 18, 36, 6, 12, 24, ...

Solução:

Exemplo de Aplicação em C:

É muito comum, em programas computacionais, termos procedimentos iterativos, isto

é, procedimentos que devem ser executados em vários passos. Como exemplo, vamos

considerar o cálculo do valor do fatorial de um número inteiro não negativo. Por

definição:

Enquanto expr for avaliada em verdadeiro, o bloco de comandos é executado

repetidamente. Se expr for avaliada em falso, o bloco de comando não é executado e a

Page 26: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

execução do programa prossegue. Uma possível implementação do cálculo do fatorial

usando while é mostrada a seguir.

Mais compacta e amplamente utilizada, é através de laços for. Faça este mesmo exemplo em

FOR.

1.11.2.3 Estrutura DO/WHILE.

A estrutura de repetição DO/WHILE parte do princípio de que se deve fazer algo primeiro e só

depois comparar uma variável para saber se o loop será executado mais uma vez. A estrutura

do/while é parecida com a do while, no aspecto de possuir apenas uma condição, e ambas são

estruturas de repetição, porém o do/while, diferentemente do while, informa a condição ao

compilador apensa no final da estrutura.

Page 27: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.11.2.4 Estrutura Switch / Case

Outra forma de estrutura seletiva é o switch. Dentro do switch há o case (que significa caso).

Ou seja, é quase que um if com várias possibilidades, mas com algumas diferenças

importantes.

Primeira diferença: Os cases não aceitam operadores lógicos. Portanto, não é possível fazer

uma comparação. Isso limita o case a apenas valores definidos.

Segunda diferença: O switch executa seu bloco em cascata. Ou seja, se a variável indicar para

o primeiro case e dentro do switch tiver 5 cases, o switch executará todos os outros 4 cases a

não ser que utilizemos o comando para sair do switch. (Nos referimos ao BREAK).

Agora, que conhecemos diferenças importantes, vamos ver como proceder com o switch /

case.

O primeiro comando switch deve-se colocar entre parênteses a variável na qual está guardado

o valor que será avaliado pelo case. Então, abre-se o bloco de dados.

Dentro do bloco de dados colocamos o comando case e logo após um valor terminando a

linha com dois pontos (:). Preste atenção no tipo de variável que será colocado, pois há

diferenças entre um dado e outro. Por exemplo: 1 não é a mesma coisa que '1' e 'a' não é a

mesma coisa que 'A'. Então, é estruturado o comando que será executado pelo case.

Estruturalmente, seria isso:

Page 28: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Exemplo de Aplicação:

1. Elabore uma simples calculadora que realize as operações de adição, subtração,

multiplicação e divisão, utilizando a estrutura switch/case.

Solução:

Perceba que no final de cada case há um break. Porque se não houvesse, o switch continuaria

executando até o final. Por exemplo, no exercício anterior, havia 4 casos (case), e escolhemos

a operação 2, caso não houvesse o break, o programa executaria os casos seguintes, no nosso

caso, a operação 3 e 4.

Page 29: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Default

Default, do inglês padrão, é o case que é ativado caso não tenha achado nenhum case

definido. Ou seja, é o que aconteceria em último caso. Vamos imaginar o seguinte

cenário: Seu programa pede para que o usuário digite apenas duas opções (S ou N)

para reiniciar o programa. Mas, propositalmente ou por engano, o usuário digita uma

opção totalmente diferente. E agora? O que seu programa deve fazer? É aqui que o

default entra. Geralmente o default é quando é previsto um erro, uma entrada de

dado incorreta ou não de acordo com o contexto. O default tem seu papel parecido

com o else, da estrutura if/else, caso nenhuma condição for feita, fará os comandos

definidos no default.

Como podemos ver, há dois casos: S para reiniciar ou N para sair. Se por acaso alguém digitar

algo diferente disso, executa-se o default, que informa que a opção escolhida é invalida e

repete a pergunta se o usuário deseja sair do programa, até que seja digitada uma opção

valida (S ou N). Como a linguagem C/C++ é case sensitive (diferencia maiúsculas de minúsculas)

usamos uma função para deixar a letra maiúscula (toupper da biblioteca ctype). Agora, não

importa o que o usuário digitar, pois o programa está preparado para reagir à qualquer

entrada de dado.

Lista de exercícios de fixação lista03

Page 30: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.11.3 Interrupções com break e continue.

A linguagem C/C++ oferece duas formas para a interrupção antecipada de um determinado laço.

1.11.3.1 Break.

Quando utilizado dentro de um laço, interrompe e termina a execução do mesmo. A execução

prossegue com os comandos subseqüentes ao bloco. O código abaixo, em C, ilustra o efeito de

sua utilização.

Faça em C++.

A saída deste programa, se executado, será:

0 1 2 3 4 fim, pois, quando i tiver o valor 5, o laço será interrompido e finalizado pelo

comando break, passando o controle para o próximo comando após o laço, no caso

uma chamada final de printf.

1.11.3.2 Continue.

Interrompe a execução dos comandos de um laço. A diferença básica em relação ao comando

break é que o laço não é automaticamente finalizado. O comando continue interrompe a

execução de um laço passando para a próxima iteração. Assim, o código, em C++:

Faça em C.

gera a saída:

0 1 2 3 4 6 7 8 9 fim

Page 31: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.12 Funções.

As funções dividem grandes tarefas de computação em tarefas menores. Os programas em

C/C++ geralmente consistem de várias pequenas funções em vez de poucas funções de maior

tamanho. A criação de funções evita a repetição de código, de modo que um procedimento que é

repetido deve ser transformado numa função que, será chamada diversas vezes. Um

programa bem estruturado deve ser pensado em termos de funções, e estas, por sua vez,

podem (e devem, se possível) esconder do corpo principal do programa detalhes ou

particularidades de implementação. Em C/C++, tudo é feito através de funções. Os exemplos

anteriores utilizam as funções da biblioteca padrão para realizar entrada e saída. Nesta seção,

discutiremos a codificação de nossas próprias funções.A forma geral para definir uma função é:

Para melhor ilustrar a criação de funções, consideraremos o cálculo do fatorial de um número,

exemplo que já é de nosso conhecimento, em seção anteriores.

Podemos escrever uma função que, dado um determinado número inteiro não negativo n,

imprime o valor de seu fatorial. Um programa, em C, que utiliza esta função seria:

Faça em C++.

Neste exemplo, que a função fat recebe como parâmetro o número cujo fatorial deve ser

impresso. Os parâmetros de uma função devem ser listados, com seus respectivos tipos, entre

os parênteses que seguem o nome da função. Quando uma função não tem parâmetros,

colocamos a palavra reservada void entre os parênteses. Devemos notar que main também é

uma função; sua única particularidade consiste em ser a função automaticamente executada

após o programa ser carregado. Como as funções main que temos apresentado não recebem

parâmetros, temos usado a palavra void na lista de parâmetros.

Page 32: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Além de receber parâmetros, uma função pode ter um valor de retorno associado. No

exemplo do cálculo do fatorial, a função fat não tem nenhum valor de retorno, portanto

colocamos a palavra void antes do nome da função, indicando a ausência de um valor de

retorno.

A função main obrigatoriamente deve ter um valor inteiro como retorno. Esse valor pode

ser usado pelo sistema operacional para testar a execução do programa. A convenção

geralmente utilizada faz com que a função main retorne zero no caso da execução ser bem

sucedida ou diferente de zero no caso de problemas durante a execução.

Por fim, salientamos que C exige que se coloque o protótipo da função antes desta ser

chamada. O protótipo de uma função consiste na repetição da linha de sua definição

seguida do caractere (;). Temos então:

A rigor, no protótipo não há necessidade de indicarmos os nomes dos parâmetros, apenas os

seus tipos, portanto seria válido escrever: void fat (int);. Porém, geralmente

mantemos os nomes dos parâmetros, pois servem como documentação do significado de

cada parâmetro, desde que utilizemos nomes coerentes. O protótipo da função é necessário

para que o compilador verifique os tipos dos parâmetros na chamada da função. Por

exemplo, se tentássemos chamar a função com fat(4.5); o compilador provavelmente

indicaria o erro, pois estaríamos passando um valor real enquanto a função espera um valor

inteiro. É devido a esta necessidade que se exige a inclusão do arquivo stdio.h para a

utilização das funções de entrada e saída da biblioteca padrão. Neste arquivo, encontram-se,

entre outras coisas, os protótipos das funções printf e scanf.

Uma função pode ter um valor de retorno associado. Para ilustrar a discussão, vamos

reescrever o código acima, fazendo com que a função fat retorne o valor do fatorial. A

função main fica então responsável pela impressão do valor.

Page 33: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.13 Pilha de execução.

Apresentada a forma básica para a definição de funções, discutiremos agora, em detalhe,

como funciona a comunicação entre a função que chama e a função que é chamada. Já

mencionamos na introdução deste curso que as funções são independentes entre si. As

variáveis locais definidas dentro do corpo de uma função (e isto inclui os parâmetros das

funções) não existem fora da função. Cada vez que a função é executada, as variáveis locais

são criadas, e, quando a execução da função termina, estas variáveis deixam de existir.

A transferência de dados entre funções é feita através dos parâmetros e do valor de retorno

da função chamada. Conforme mencionado, uma função pode retornar um valor para a função

que a chamou e isto é feito através do comando return. Quando uma função tem um valor de

retorno, a chamada da função é uma expressão cujo valor resultante é o valor retornado pela

função. Por isso, foi válido escrevermos na função main acima a expressão r = fat(n); que

chama a função fat armazenando seu valor de retorno na variável r.

Page 34: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Saída do programa Fatorial de 5 = 120

Vamos considerar um esquema representativo da memória do computador. Salientamos que

este esquema é apenas uma maneira didática de explicar o que ocorre na memória do

computador. Suponhamos que as variáveis são armazenadas na memória como ilustrado

abaixo. Os números à direita representam endereços (posições) fictícios de memória e os

nomes à esquerda indicam os nomes das variáveis. A figura abaixo ilustra este esquema

representativo da memória que adotaremos.

Então, podemos analisar passo a passo a evolução do programa mostrado acima, ilustrando

o funcionamento da pilha de execução.

Page 35: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Isto ilustra por que o valor da variável passada nunca será alterado dentro da função.

A seguir, discutiremos uma forma para podermos alterar valores por passagem de

parâmetros, o que será realizado passando o endereço de memória onde a variável está

armazenada.

1.14 Ponteiros de variáveis.

A linguagem C permite o armazenamento e a manipulação de valores de endereços de

memória. Para cada tipo existente, há um tipo ponteiro que pode armazenar endereços de

memória onde existem valores do tipo correspondente armazenados. Por exemplo, quando

escrevemos:

int a

Declaramos uma variável com nome a que pode armazenar valores inteiros.

Automaticamente, reserva-se um espaço na memória suficiente para armazenar valores

inteiros (geralmente 4 bytes).

Da mesma forma que declaramos variáveis para armazenar inteiros, podemos declarar

variáveis que, em vez de servirem para armazenar valores inteiros, servem para armazenar

valores de endereços de memória onde há variáveis inteiras armazenadas. C/C++ não reserva

uma palavra especial para a declaração de ponteiros; usamos a mesma palavra do tipo com

os nomes das variáveis precedidas pelo caractere *. Assim, podemos escrever:

int *p

Neste caso, declaramos uma variável com nome p que pode armazenar endereços de

memória onde existe um inteiro armazenado. Para atribuir e acessar endereços de memória,

a linguagem oferece dois operadores unários ainda não discutidos. O operador unário &

(“endereço de”), aplicado a variáveis, resulta no endereço da posição da memória reservada

para a variável. O operador unário * (“conteúdo de”), aplicado as variáveis do tipo ponteiro,

acessa o conteúdo do endereço de memória armazenado pela variável ponteiro. Para

Page 36: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

exemplificar, vamos ilustrar esquematicamente, através de um exemplo simples, o que

ocorre na pilha de execução. Consideremos o trecho de código mostrado na figura abaixo.

Após as declarações, ambas as variáveis, a e p, tem armazenadas como valores "lixo", pois

não foram inicializadas. Podemos fazer atribuições como exemplificado nos fragmentos de

código da figura a seguir:

Nas atribuições ilustradas na figura acima, a variável a recebe, indiretamente, ou seja, o

endereço da variável a recebe o valor o valor 6, pois estamos atribuindo para *p que aponta

para o endereço de a.

Dizemos que p aponta para a, daí o nome ponteiro. Em vez de criarmos valores fictícios para

os endereços de memória no nosso esquema ilustrativo da memória, podemos desenhar setas

graficamente, sinalizando que um ponteiro aponta para uma determinada variável.

Page 37: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Alguns exemplos.

imprime o valor 2.

1.14.1 Passando ponteiros para funções.

Os ponteiros oferecem meios de alterarmos valores de variáveis acessando-as indiretamente.

Já discutimos que as funções não podem alterar diretamente valores de variáveis da função

que fez a chamada.

No entanto, se passarmos para uma função os valores dos endereços de memória onde suas

variáveis estão armazenadas, a função pode alterar, indiretamente, os valores das variáveis da

função que a chamou.

Uma função projetada para trocar os valores entre duas variáveis. O código abaixo:

Não funciona como esperado (serão impressos 5 e 7), pois os valores de a e b da função

main não são alterados. Alterados são os valores de x e y dentro da função troca, mas

eles não representam as variáveis da função main, apenas são inicializados com os valores

de a e b. A alternativa é fazer com que a função receba os endereços das variáveis e, assim,

alterar seus valores indiretamente. Reescrevendo:

Page 38: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Mostrando graficamente.

1.15 Recursividade.

As funções podem ser chamadas recursivamente, isto é, dentro do corpo de uma função

podemos chamar novamente a própria função. Se uma função A chama a própria função A,

dizemos que ocorre uma recursão direta. Se uma função A chama uma função B que, por

sua vez, chama A, temos uma recursão indireta. Diversas implementações ficam muito mais

fáceis usando recursividade. Por outro lado, implementações não recursivas tendem a ser

mais eficientes em relação ao tempo.

Para cada chamada de uma função, recursiva ou não, os parâmetros e as variáveis locais são

empilhados na pilha de execução. Assim, mesmo quando uma função é chamada

recursivamente, cria-se um ambiente local para cada chamada. As variáveis locais de

chamadas recursivas são independentes entre si, como se estivéssemos chamando funções

diferentes.

Page 39: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

As implementações recursivas devem ser pensadas considerando-se a definição recursiva

do problema que desejamos resolver. Por exemplo, o valor do fatorial de um número pode

ser definido de forma recursiva:

Considerando a definição acima, fica muito simples pensar na implementação recursiva de

uma função que calcula e retorna o fatorial de um número.

1.16 Pré-processador e macros.

Um código C/C++, antes de ser compilado, passa por um pré-processador. O pré-processador

de C/C++ reconhece determinadas diretivas e altera o código para, então, enviá-lo ao

compilador.

1.16.1 #include.

Uma das diretivas reconhecidas pelo pré-processador, e já utilizada nos nossos exemplos, é

#include. Ela é seguida por um nome de arquivo e o pré-processador a substitui pelo

corpo do arquivo especificado. É como se o texto do arquivo incluído fizesse parte do

código fonte.

Uma observação: quando o nome do arquivo a ser incluído é envolto por aspas

("arquivo"), o pré-processador procura primeiro o arquivo no diretório atual e, caso não o

encontre, o procura nos diretórios de include especificados para compilação. Se o arquivo é

colocado entre os sinais de menor e maior (<arquivo>), o pré-processador procura somente no

no diretório de include.

Page 40: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.16.2 #define.

É muito utilizada e que será agora discutida é a diretiva de definição. Por exemplo, uma função

para calcular a área de um círculo pode ser escrita da seguinte forma:

Neste caso, antes da compilação, toda ocorrência da palavra PI (desde que não envolvida

por aspas) será trocada pelo número 3.14159.

C permite ainda a utilização da diretiva de definição com parâmetros. É válido escrever, por

exemplo:

após esta definição existir uma linha de código com o trecho:

será entendido pelo compilador verá:

Estas definições com parâmetros recebem o nome de macros. Devemos ter muito cuidado

na definição de macros. Mesmo um erro de sintaxe pode ser difícil de ser detectado, pois

ocompilador indicará um erro na linha em que se utiliza a macro e não na linha de definição

da macro (onde efetivamente encontra-se o erro).

Outros efeitos colaterais de macros mal definidas podem ser ainda piores. Por exemplo, no

código abaixo:

o resultado impresso é 17 e não 8, como poderia ser esperado. Por quê?

Page 41: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Neste outro exemplo que envolve a macro com parênteses:

o resultado é 11 e não 14. A macro corretamente definida seria:

Portanto, concluímos que, na regra básica para a definição de macros, devemos envolver cada

parâmetro, e a macro como um todo, com parênteses.

1.17 Vetores.

A forma mais simples de estruturarmos um conjunto de dados é por meio de vetores. Como

a maioria das linguagens de programação, C/C++ permite a definição de vetores. Definimos um

vetor em C/C++ da seguinte forma:

int v[10]

A declaração acima diz que v é um vetor de inteiros dimensionado com 10 elementos, isto

é, reservamos um espaço de memória contínuo para armazenar 10 valores inteiros. Assim,

se cada int ocupa 4 bytes, a declaração acima reserva um espaço de memória de 40 bytes,

como ilustra a figura abaixo.

Page 42: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

O acesso a cada elemento do vetor é feito através de uma indexação da variável v.

Observamos que, em C, a indexação de um vetor varia de zero a n-1, onde n representa a

dimensão do vetor. Assim:

Para exemplificar o uso de vetores, vamos considerar um programa que lê 10 números

reais, fornecidos via teclado, e calcula a destes números. A média é dada por:

A implementação, uma delas, é apresentada a seguir.

Atenção todo programa aqui apresentado será escrito em C, para que o aluno

refaça o mesmo em C++. Pois as quetões em prova srão em C e/ou C++.

Devemos observar que passamos para a função scanf o endereço de cada elemento do

vetor (&v[i]), pois desejamos que os valores capturados sejam armazenados nos

elementos do vetor. Se v[i] representa o (i+1)-ésimo elemento do vetor, &v[i] representa

o endereço de memória onde esse elemento está armazenado.

Page 43: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Os vetores também podem ser inicializados na declaração:

Neste caso, a linguagem dimensiona o vetor pelo número de elementos inicializados.

1.17.1 Passagem de vetores para funções.

Passar um vetor para uma função consiste em passar o endereço da primeira posição do vetor.

Quando passado um valor de endereço, a função chamada deve ter um parâmetro do tipo

ponteiro para armazenar este valor.

Assim, se passarmos para uma função um vetor de int, devemos ter um parâmetro do tipo

int*, capaz de armazenar endereços de inteiros.

Quando utilizamos a expressão “passar um vetor para uma função” deve ser interpretada

como “passar o endereço inicial do vetor”. Os elementos do vetor não são copiados para a

função, o argumento copiado é apenas o endereço do primeiro elemento.

Modificando o código do exemplo acima, para usar funções separadas para o cálculo da média.

(Usando os operadores de atribuição += para acumular as somas.).

Page 44: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Note que, ao ser passado para a função o endereço do primeiro elemento do vetor (e não os

elementos propriamente ditos), podem alterar os valores dos elementos do vetor dentro da

função. O exemplo abaixo ilustra:

A saída sera 2 4 6, pois os elementos do vetor serão incrementados dentro da função.

1.17.2 Alocação dinâmica.

Até aqui, na declaração de um vetor, foi preciso dimensioná-lo. Isto nos obrigava, a saber,

de antemão, quanto espaço seria necessário.

É necessário saber o número máximo de elementos no vetor na sua declaração.

Este pré-dimensionamento do vetor torna-se um fator limitante.

C/C++ oferece meios de requisitarmos espaços de memória em tempo de execução. Dizemos

que podemos alocar memória dinamicamente. Com este recurso, nosso programa para o

cálculo da média e agora com variância discutido acima pode, em tempo de execução,

consultar o número de alunos da turma e então fazer a alocação do vetor dinamicamente, sem

desperdício de memória.

Page 45: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.17.3 Uso da memória.

Informalmente, podemos dizer que existem três maneiras de reservarmos espaço de memória

para o armazenamento de informações.

A primeira delas é através do uso de variáveis globais (e estáticas). O espaço reservado

para uma variável global existe enquanto o programa estiver sendo executado.

A segunda maneira é através do uso de variáveis locais. Neste caso, como já

discutimos, o espaço existe apenas enquanto a função que declarou a variável está

sendo executada, sendo liberado para outros usos quando a execução da função

termina. Por este motivo, a função que chama não pode fazer referência ao espaço

local da função chamada.

As variáveis globais ou locais podem ser simples ou vetores. Para os vetores,

precisamos informar o número máximo de elementos, caso contrário o compilador

não saberia o tamanho do espaço a ser reservado.

A terceira maneira de reservarmos memória é requisitando ao sistema, em tempo de

execução, um espaço de um determinado tamanho. Este espaço alocado

dinamicamente permanece reservado até que explicitamente seja liberado pelo

programa. Por isso, podemos alocar dinamicamente um espaço de memória numa

função e acessá-lo em outra.

A partir do momento que liberarmos o espaço, ele estará disponibilizado para outros

usos e não podemos mais acessá-lo. Se o programa não liberar um espaço alocado,

este será automaticamente liberado quando a execução do programa terminar.

Apresentamos abaixo um esquema didático que ilustra de maneira fictícia a distribuição do

uso da memória pelo sistema operacional.

Page 46: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Quando requisitamos ao sistema operacional para executar um determinado programa, o

código em linguagem de máquina do programa deve ser carregado na memória. O sistema

operacional reserva também os espaços necessários para armazenarmos as variáveis globais (e

estáticas) existentes no programa. O restante da memória livre é utilizado pelas variáveis

locais e pelas variáveis alocadas dinamicamente. Cada vez que uma determinada função é

chamada, o sistema reserva o espaço necessário para as variáveis locais da função. Este espaço

pertence à pilha de execução e, quando a função termina, é desempilhado. A parte da

memória não ocupada pela pilha de execução pode ser requisitada dinamicamente. Se a pilha

tentar crescer mais do que o espaço disponível existente, dizemos que ela “estourou” e o

programa é abortado com erro. Similarmente, se o espaço de memória livre for menor que o

espaço requisitado dinamicamente, a alocação não é feita e o programa pode prever um

tratamento de erro adequado (por exemplo, podemos imprimir a mensagem “Memória

insuficiente” e interromper a execução do programa).

1.17.4 Funções da biblioteca padrão.

Na biblioteca padrão stdlib, temos funções que nos permitem alocar e liberar memória

dinamicamente.

1.17.4.1 malloc.

É a função básica para alocar memória, recebe como parâmetro o número de bytes que se

deseja alocar e retorna o endereço inicial da área de memória alocada.

Exemplificando, consideremos a alocação dinâmica do vetor de inteiros com 10

elementos. Como a função malloc retorna o endereço da área alocada e, desejamos

armazenar valores inteiros nessa área, temos que declarar um ponteiro de inteiro para receber

o endereço inicial do espaço alocado. O trecho de código então seria:

int *v;

v = malloc(10*4);

Feito isso, v armazenará o endereço inicial de uma área contínua de memória suficiente para

armazenar 10 valores inteiros. Deve-se tratar v como tratamos um vetor declarado

estaticamente, pois, se v aponta para o inicio de uma área alocada, pode-se dizer que v[0]

acessa o endereço do primeiro elemento que armazenaremos, e v[1] aponta para o

segundo, até v[9].

Na exemplificação acima, trabalhamos com um inteiro, que ocupa 4 bytes. Devemos ficar

independentes de compiladores e máquinas, usando o operador sizeof( ).

v = malloc(10*sizeof(int));

Segue ilustração de maneira esquemática o que ocorre na memória:

Page 47: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Caso não houver espaço livre suficiente para realização da alocação, será retornado um

endereço nulo (representado pelo símbolo NULL, definido em stdlib.h). Devemos prevenir

o erro na alocação do programa verificando o valor de retorno da função malloc. Deve-se

imprimir uma mensagem e abortar o programa usando a função exit, também definida na

stdlib.

v = (int*) malloc(10*sizeof(int));

if (v==NULL)

{

printf("Memoria insuficiente.\n");

exit(1); /* aborta o programa e retorna 1 para o sist. operacional */

}

Para liberação da memória alocada dinamicamente, usa-se a função free. Esta função recebe

como parâmetro o ponteiro da memória a ser liberada. Assim, para liberar o vetor v, fazemos:

free (v);

Devemos passar para a função free um endereço de memória que tenha sido alocado

dinamicamente. Deve-se lembrar de que não podemos acessar o espaço na memória

depois que o liberamos.

Como exemplo do uso da alocação dinâmica, usaremos o programa para o cálculo da

média e da variância. Agora, o programa lê o número de valores que serão fornecidos, aloca

um vetor dinamicamente e faz os cálculos. Somente a função principal precisa ser alterada,

pois as funções para calcular a média e a variância anteriormente apresentadas independem

do fato de o vetor ter sido alocado estática ou dinamicamente.

Page 48: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.18 Cadeia de caracteres.

1.18.1 Caracteres

A linguagem C/C++ permite a escrita de constantes caracteres. Uma constante caractere é

escrita envolvendo o caractere com aspas simples. Assim, a expressão 'a' representa uma

constante caracter.

Page 49: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Exemplo: Suponhamos que queremos escrever uma função para testar se uma

variável caractere c é um dígito (um dos caracteres entre '0' e '9'). Esta função pode ter

o protótipo:

int digito(char c);

e ter como resultado 1 (verdadeiro) se c for um dígito, e 0 (falso) se não for.

A implementação desta função pode ser dada por:

1.18.2 String.

Cadeias de caracteres (strings), em C/C++, são representadas por vetores do tipo char

terminadas, obrigatoriamente, pelo caractere nulo ('\0'). Portanto, para armazenarmos uma

cadeia de caracteres, devemos reservar uma posição adicional para o caractere de fim da

cadeia. Todas as funções que manipulam cadeias de caracteres (e a biblioteca padrão de C/C++

oferece várias delas) recebem como parâmetro um vetor de char, isto é, um ponteiro para o

primeiro elemento do vetor que representa a cadeia, e processam caractere por caractere, até

encontrarem o caractere nulo, que sinaliza o final da cadeia.

O próximo passo é entender as strings. Strings em C/C++ são tratados como vetores de

tamanho determinado que podem armazenar qualquer caracter. Diferentemente de declarar

apenas uma variável do tipo char (que armazena apenas um caracter) a string é uma cadeia de

caracteres, ou seja, pode guardar quantos caracteres nós determinarmos.

Portanto, para declararmos uma string, basta nós criarmos um vetor de caracteres dessa

forma:

char minhaString [50];

O único problema das strings são o seu consumo de recursos. Por exemplo, se levarmos em

conta o vetor de caracteres que acabamos de criar, apesar dele conter 50 posições, nós só

poderemos digitar até 49 letras. Isso ocorre porque toda string deve ter um caracter terminal,

que geralmente é indicado pelo NULL (nulo). Isso quer dizer que um vetor de caracteres

(string) de 50 posições terá 49 caracteres efetivos e um NULL indicando seu final.

1.18.2.1 Entrada de String.

Para entrarmos com uma String no sistema usamos a mesma função de entrada padrão - cin.

Ou seja, se quisermos que o usuário digite seu nome faríamos da seguinte forma:

Page 50: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Agora, outro problema ao tratarmos de strings em C/C++. Embora a função cin consiga obter a

string, ela sempre termina assim que pressionarmos o espaço a primeira vez, ou seja, ele só

consegue pegar uma palavra por vez.

Então, como vamos obter uma linha inteira?

Bem, para obtermos uma linha inteira nós devemos fazer uso de um dos métodos encontrados

dentro de cin - o método getline.

O método getline obtém uma linha de acordo com o tamanho definido no método. Então, o

método getline utiliza dois parâmetros: 1°. O nome da string; 2°. O tamanho máximo que será

preenchido.

Então, usando o mesmo exemplo, apenas mudaríamos a 6ª linha. Vejamos:

1.18.3 Funções de String.

Podemos fazer muitas coisas com Strings, como por exemplo, ver ser tamanho, juntar mais de

uma palavra, comparar duas palavras diferentes, etc. Para isso, basta incluirmos uma

biblioteca para tratamento de strings em C/C++ chamado - string.h.

1.18.3.1 Obter o tamanho de uma String.

Para obter o tamanho de uma string usamos a função strlen (que é a junção do

inglês String Length, que quer dizer, largura de string). Essa função retorna o número de

caracteres utilizados (incluindo os espaços se houver). Ela recebe como argumento apenas a

string que deve ser verificada e retorna o número de caracteres encontrados.

Page 51: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.18.3.2 Comparar duas strings.

Há também uma forma de compararmos duas strings para ver ser ambas são iguais. A função

que determina isso é strcmp. Embora C/C++ é case sensitive, ou seja, diferencia maiúsculas de

minúsculas, isso não irá influenciar nessa função. Essa função retorna 0 se há igualdade entre

as strings ou um número diferente de zero se não houver igualdade. Portanto, se quisermos

fazer uma comparação de duas strings, procedemos da seguinte forma: strcmp (string1,

string2). Vejamos o exemplo:

1.18.3.3 Copiar uma String.

Para copiar uma string para outra string usamos strcpy (que vem de String copy). Essa função

recebe dois argumentos: 1°. a string para onde será armazenada a cópia; 2°. a string que será

copiada. Resumidamente, ele copia a segunda string para a primeira. Exemplo:

Page 52: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.18.3.4 Concatenar uma String.

Concatenar uma String que dizer juntar. De uma forma mais simplória, é como se disséssemos

que a junção da palavra passa mais a palavra tempo é igual a passatempo. Ou seja, essa função

- strcat - pega duas strings e junta o que tiver na primeira com o que tiver na segunda.

Tome cuidado: Se concatenarmos duas strings e uma delas ou ambas forem vazias ocorrerá

um erro.

No exemplo abaixo, faremos o seguinte: vamos obter o valor de duas strings e concatená-las

formando uma nova string.

Lidar com strings é extremamente importante para a programação, pois a string é a base de

qualquer arquivo e principalmente controles de rotina (por exemplo: rotinas e procedimentos

de banco de dados, o SQL; endereçamento de arquivos; modificação de configurações...).

Abaixo esta um exemplo com todas as funções básicas de manipulação de string vistas até

aqui.

Page 53: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.19 Estrutura de Dados – STRUCT.

As estruturas de dados consistem em criar apenas um dado que contém vários membros, que

nada mais são do que outras variáveis. De uma forma mais simples, é como se uma variável

tivesse outras variáveis dentro dela. A vantagem em se usar estruturas de dados é que

podemos agrupar de forma organizada vários tipos de dados diferentes, por exemplo, dentro

de uma estrutura de dados podemos ter juntos tanto um tipo float, um inteiro, um char ou

um double.

As variáveis que ficam dentro da estrutura de dados são chamadas de membros.

1.19.1 Criando uma estrutura de dados com STRUCT.

Para criar uma estrutura de dados usamos a palavra reservada struct. Toda estrutura deve ser

criada antes de qualquer função ou mesmo da função principal main. Toda estrutura tem

nome e seus membros são declarados dentro de um bloco de dados. Após a definição de seus

membros no bloco de dados, terminamos a linha com um ponto-e-vírgula (;). Portanto:

struct nome_da_estrutura { tipo_de_dado nome_do_membro; };

Por exemplo, se fossemos criar uma estrutura de dados para uma data faríamos:

Page 54: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

struct data { int dia; int mes; int ano; };

1.19.2 Declarando um struct e acessando seus membros.

Ainda utilizando o exemplo acima, vamos declarar uma variável do tipo estrututa de dados

data e acessar seus membros.

Após criarmos uma estrutura de dados com struct, poderemos utilizá-la como um tipo de dado

comum (ex.: float, int, char). E para acessar seus membros utilizamos a variável declarada mais

um ponto (.) e o nome do membro. Veja este exemplo abaixo:

Portanto, a variável hoje é declarada como sendo um tipo de dado data. Data é uma estrutura

de dados que tem três características (ou três membros) inteiras: dia, mes e ano. Como hoje é

um tipo de dado data, ele obtém os mesmos três membros. Para acessar cada membro,

usamos a variável e depois o nome do membro que queremos acessar separados por ponto (.).

1.19.3 Ponteiro de Struct.

Vimos a pouco como criar uma estrutura de dados agrupado (struct), como definir um nome à

essa estrutura com typedef e como criar um ponteiro para indicar um endereço de memória.

Agora, vamos nos aprofundar um pouco mais nesse assunto vendo como procedemos com um

ponteiro de struct.

Como vimos anteriormente, um struct consiste em vários dados agrupados em apenas um.

Para acessarmos cada um desses dados, usamos um ponto (.) para indicar que o nome

seguinte é o nome do membro.

Um ponteiro guarda o endereço de memória que pode ser acessado diretamente.

Page 55: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

O problema aqui está no seguinte, como acessaremos um membro de uma estrutura de dados

usando um ponteiro? Pois é simples. Para isso, basta usarmos o que chamamos de "seta". A

"seta" consiste de um sinal de menos e um maior (->).

Portanto, podemos criar nosso struct do mesmo jeito de sempre e nosso ponteiro também.

Mas, quando formos acessar um membro dessa estrutura usando um ponteiro nós não

usaremos um ponto, mas uma seta.

Vejamos:

Agora, há mais uma maneira de acessarmos um membro da estrutura usando um ponteiro.

Esta outra forma consiste em indicar de qual ponteiro nos referimos colocando o diferenciador

entre parênteses, assim (*hoje). Dessa forma podemos acessar diretamente usando um ponto

(.).

No exemplo abaixo usamos apenas ponteiros com diferenciador para escrever no struct data.

Page 56: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Concluindo, podemos acessar um membro de um tipo de dado estrutura de dado usando

ponteiro de duas formas:

1. Usando um diferenciador entre parênteses e um ponto (.) para indicar o membro.

2. Usando o próprio ponteiro e uma seta (->) para indicar o membro.

1.20 Vetores bidimensionais – Matrizes.

A linguagem C permite a criação de vetores bidimensionais, declarados estaticamente. Por

exemplo, para declararmos uma matriz de valores reais com 4 linhas e 3 colunas, fazemos:

float mat[4][3];

Page 57: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Esta declaração reserva um espaço de memória necessário para armazenar os 12 elementos

da matriz, que são armazenados de maneira contínua, organizados linha a linha.

Os elementos da matriz são acessados com indexação dupla: mat[i][j]. O primeiro

índice, i, acessa a linha e o segundo, j, acessa a coluna. Como em C/C++ a indexação começa

em zero, o elemento da primeira linha e primeira coluna é acessado por mat[0][0]. Após

a declaração estática de uma matriz, a variável que representa a matriz, mat no exemplo

acima, representa um ponteiro para o primeiro “vetor-linha”, composto por 3 elementos.

Com isto, mat[1] aponta para o primeiro elemento do segundo “vetor-linha”, e assim por

diante.

As matrizes também podem ser inicializadas na declaração:

Ou podemos inicializar seqüencialmente:

O número de elementos da linha pode ser omitido numa inicialização, mas o número de

colunas deve, obrigatoriamente, ser fornecido:

1.20.1 Passagem de matrizes para funções.

A matriz criada estaticamente é representada por um ponteiro para um “vetor-linha” com o

número de elementos da linha. Quando passamos uma matriz para uma função, o parâmetro

da função deve ser deste tipo. O protótipo de uma função que recebe a matriz declarada

acima seria:

Uma segunda opção é declarar o parâmetro como matriz, podendo omitir o número de linhas.

Em diversas aplicações, as matrizes têm dimensões

fixas e não justificam a criação de estratégias para trabalhar com alocação dinâmica. Em

aplicações da área de Computação Gráfica, por exemplo, é comum trabalharmos com

matrizes de 4 por 4 para representar transformações geométricas e projeções. Nestes casos,

é muito mais simples definirmos as matrizes estaticamente (float mat[4][4];), uma vez que

sabemos de antemão as dimensões a serem usadas. Nestes casos, vale a pena

definirmos um tipo próprio, pois nos livramos das construções sintáticas confusas

explicitadas acima. Por exemplo, podemos definir o tipo Matrix4.

Com esta definição podemos declarar variáveis e parâmetros deste tipo:

Page 58: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.21 Listas Encadeadas.

Para representarmos um grupo de dados, já vimos que podemos usar um vetor em C. O vetor

é a forma mais primitiva de representar diversos elementos agrupados. Para simplificar a

discussão dos conceitos que serão apresentados agora, vamos supor que temos que

desenvolver uma aplicação que deve representar um grupo de valores inteiros. Para tanto,

podemos declarar um vetor escolhendo um número máximo de elementos.

Ao declararmos um vetor, reservamos um espaço contíguo de memória para armazenar

seus elementos, conforme ilustra a figura abaixo.

No entanto o vetor não é uma estrutura de dados muito flexível, pois precisamos dimensioná-

lo com um número máximo de elementos. Se o número de elementos que precisarmos

armazenar exceder a dimensão do vetor, teremos um problema, pois não existe uma maneira

simples e barata (computacionalmente) para alterarmos a dimensão do vetor em tempo de

execução. Por outro lado, se o número de elementos que precisarmos armazenar no vetor for

muito inferior à sua dimensão, estará subutilizando o espaço de memória reservado.

A solução para esses problemas é utilizar estruturas de dados que cresçam à medida que

precisarmos armazenar novos elementos (e diminuam à medida que precisarmos retirar

elementos armazenados anteriormente). Tais estruturas são chamadas dinâmicas e

armazenam cada um dos seus elementos usando alocação dinâmica.

Então, discutiremos a estrutura de dados conhecida como lista encadeada.

As listas encadeadas são amplamente usadas para implementar diversas outras estruturas de

dados com semânticas próprias, que serão tratadas nos capítulos seguintes.

Numa lista encadeada, para cada novo elemento inserido na estrutura, alocamos um

espaço de memória para armazená-lo. Desta forma, o espaço total de memória gasto

pela estrutura é proporcional ao número de elementos nela armazenado. No entanto, não

podemos garantir que os elementos armazenados na lista ocuparão um espaço de

memória contíguo, portanto não temos acesso direto aos elementos da lista. Para que

seja possível percorrer todos os elementos da lista, devemos explicitamente guardar o

encadeamento dos elementos, o que é feito armazenando-se, junto com a informação de

cada elemento, um ponteiro para o próximo elemento da lista. A Figura abaixo ilustra o

arranjo da memória de uma lista encadeada.

Page 59: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

A estrutura consiste numa seqüência encadeada de elementos, em geral chamados de nós da

lista. A lista é representada por um ponteiro para o primeiro elemento (ou nó).

Do primeiro elemento, podemos alcançar o segundo seguindo o encadeamento, e assim por

diante. O último elemento da lista aponta para NULL, sinalizando que não existe um próximo

elemento.

Para exemplificar a implementação de listas encadeadas em C/C++, vamos considerar um

exemplo simples em que queremos armazenar valores inteiros numa lista encadeada. O

nó da lista pode ser representado pela estrutura abaixo:

Nota-se que se trata de uma estrutura de auto-referencia, pois, além do campo que armazena

a informação (no caso, um número inteiro, info), há um campo que é um ponteiro,(prox) para

uma próxima estrutura do mesmo tipo. Também é uma boa estratégia definirmos o tipo Lista

como sinônimo de struct lista. O tipo Lista representa um nó da lista e a estrutura da lista

encadeada é representada pelo ponteiro para seu primeiro elemento (tipo Lista*).

1.21.1 Função de inicialização.

A função que inicializa uma lista deve criar uma lista vazia, sem nenhum elemento.

Como a lista é representada pelo ponteiro para o primeiro elemento, uma lista vazia é

representada pelo ponteiro NULL, pois não existem elementos na lista. A função tem como

valor de retorno a lista vazia inicializada, isto é, o valor de retorno é NULL. Uma possível

implementação da função de inicialização é mostrada a seguir:

1.21.2 Função de inserção.

Uma vez criada à lista vazia, podemos inserir novos elementos nela. Para cada elemento

inserido na lista, devemos alocar dinamicamente a memória necessária para armazenar

o elemento e encadeá-lo na lista existente. A função de inserção mais simples insere o

novo elemento no início da lista.

Page 60: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Uma das implementações dessa função é mostrada a seguir. Devemos notar que o ponteiro que

representa a lista deve ter seu valor atualizado, pois a lista será representada pelo ponteiro para o

novo primeiro elemento. Por esta razão, a função de inserção recebe como parâmetros de

entrada a lista onde será inserido o novo elemento e a informação do novo elemento, e tem

como valor de retorno a nova lista, representada pelo ponteiro para o novo elemento.

Esta função aloca dinamicamente o espaço para armazenar o novo nó da lista, guarda a

informação no novo nó e faz este nó apontar para (isto é, ter como próximo elemento) o

elemento que era o primeiro da lista. A função então retorna o novo valor que representa a

lista, que é o ponteiro para o novo primeiro elemento. A Figura abaixo ilustra a operação de

inserção de um novo elemento no início da lista.

Um trecho de código que cria uma lista inicialmente vazia e insere nela dois novos elementos.

Observe que não podemos deixar de atualizar a variável que representa a lista a cada

inserção de um novo elemento.

1.21.3 Função que percorre os elementos da lista.

Para mostrarmos a implementação de uma função que percorre todos os elementos da lista,

vamos considerar a criação de uma função que imprima os valores dos elementos

armazenados numa lista.

Page 61: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

1.21.4 Função que verifica se lista está vazia.

É útil implementarmos uma função que verifique se uma lista está vazia.

A função recebe a lista e retorna 1 se estiver vazia ou 0 se não estiver vazia. Como

sabemos, uma lista está vazia se seu valor é NULL.

Essa função pode ser reescrita de forma mais compacta, conforme mostrado abaixo:

1.21.5 Função de busca.

Outra função útil, verificar se um determinado elemento está presente na lista. A função

recebe a informação referente ao elemento que queremos buscar e fornece como valor de

retorno o ponteiro do nó da lista que representa o elemento. Caso o elemento não seja

encontrado na lista, o valor retornado é NULL.

1.21.6 Função que exclui um elemento da lista.

A função para retirar um elemento da lista é mais complexa. Se descobrirmos que o elemento

a ser retirado é o primeiro da lista, devemos fazer com que o novo valor da lista passe a ser o

ponteiro para o segundo elemento, e então podemos liberar o espaço alocado para o

elemento que queremos retirar. Se o elemento a ser removido estiver no meio da lista,

devemos fazer com que o elemento anterior a ele passe a apontar para o

Page 62: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

elemento seguinte, e então podemos liberar o elemento que queremos retirar. Devemos

notar que, no segundo caso, precisamos do ponteiro para o elemento anterior para

podermos acertar o encadeamento da lista. As Figuras a seguir ilustram as operações de

remoção.

Uma implementação da função para retirar um elemento da lista é mostrada a seguir.

Inicialmente, busca-se o elemento que se deseja retirar, guardando uma referência para o

elemento anterior.

O caso de retirar o último elemento da lista recai no caso de retirar um elemento no

meio da lista, conforme pode ser observado na implementação acima. Mais adiante,

estudaremos a implementação de filas com listas encadeadas. Numa fila, devemos

armazenar, além do ponteiro para o primeiro elemento, um ponteiro para o último

elemento. Nesse caso, se for removido o último elemento, veremos que será necessário

atualizar a fila.

1.21.7 Função para liberar a lista.

Uma outra função útil que devemos considerar destrói a lista, liberando todos os elementos

alocados. Uma implementação dessa função é mostrada abaixo. A função percorre elemento a

elemento, liberando-os. É importante observar que devemos guardar a referência para o

próximo elemento antes de liberar o elemento corrente (se liberássemos o elemento e depois

tentássemos acessar o encadeamento, estaríamos acessando um espaço de memória que não

estaria mais reservado para nosso uso).

1.21.8 Um programa completo.

Um programa que ilustra a utilização dessas funções é mostrado a seguir.

Page 63: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Mais uma vez, observe que não podemos deixar de atualizar a variável que representa a

lista a cada inserção e a cada remoção de um elemento. Esquecer-se de atribuir o valor de

retorno à variável que representa a lista pode gerar erros graves. Se, por exemplo, a

função retirar o primeiro elemento da lista, e variável que representa a lista, não fosse

atualizada, estaria apontando para um nó já liberado. Como alternativa, poderíamos

fazer com que as funções insere e retira recebessem o endereço da variável que

representa a lista. Nesse caso, os parâmetros das funções seriam do tipo ponteiro para

lista (Lista** l) e seu conteúdo poderia ser acessado/atualizado de dentro da função

usando o operador conteúdo (*lst).

1.22 Pilha.

Uma das estruturas de dados mais simples é a pilha. Possivelmente por essa razão, é a

estrutura de dados mais utilizada em programação, sendo inclusive implementada

diretamente pelo hardware da maioria das máquinas modernas. A idéia fundamental da pilha

é que todo o acesso a seus elementos é feito através do seu topo. Assim, quando um elemento

novo é introduzido na pilha, passa a ser o elemento do topo, e o único elemento que pode ser

removido da pilha é o do topo. Isto faz com que os elementos da pilha sejam retirados na

ordem inversa à ordem em que foram introduzidos: o primeiro que sai é o último que entrou

(a sigla LIFO – last in, first out – é usada para descrever esta estratégia).

Existem duas operações básicas que devem ser implementadas numa estrutura de pilha: a

operação para empilhar um novo elemento, inserindo-o no topo, e a operação para

desempilhar um elemento, removendo-o do topo. É comum nos referirmos a essas duas

operações pelos termos em inglês push (empilhar) e pop (desempilhar).

Page 64: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

O exemplo de utilização de pilha mais próximo é a própria pilha de execução da linguagem

C/C++. As variáveis locais das funções são dispostas numa pilha e uma função só tem acesso às

variáveis que estão no topo (não é possível acessar as variáveis da função locais às outras

funções).

Consideraremos duas implementações de pilha: usando vetor e usando lista encadeada. Para

simplificar a exemplo, consideraremos uma pilha que armazena valores reais. Independente da

estratégia de implementação, podemos definir a interface do tipo abstrato que representa uma

estrutura de pilha. A interface é composta pelas operações que estarão disponibilizadas para

manipular e acessar as informações da pilha. Neste exemplo, vamos considerar a implementação

de cinco operações:

1. criar uma estrutura de pilha; 2. inserir um elemento no topo (push); 3. remover o elemento do topo (pop); 4. verificar se a pilha está vazia; 5. liberar a estrutura de pilha.

A função cria aloca dinamicamente a estrutura da pilha, inicializa seus campos e

retorna seu ponteiro;

as funções push e pop inserem e retiram, respectivamente, um valor real na pilha;

a função vazia informa se a pilha está ou não vazia; e

a função libera destrói a pilha, liberando toda a memória usada pela estrutura.

1.22.1 Implementação de pilha com vetor.

Em aplicações computacionais que precisam de uma estrutura de pilha, é comum sabermos de

antemão o número máximo de elementos que podem estar armazenados simultaneamente na

pilha, isto é, a estrutura da pilha tem um limite conhecido. Nestes casos, a implementação da

pilha pode ser feita usando um vetor. A implementação com vetor é bastante simples.

Devemos ter um vetor (vet) para armazenar os elementos da pilha. Os elementos inseridos

ocupam as primeiras posições do vetor. Desta forma, se temos n elementos armazenados na

pilha, o elemento vet[n-1] representa o elemento do topo.

A estrutura que representa o tipo pilha deve, portanto, ser composta pelo vetor e pelo

número de elementos armazenados.

Page 65: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

A função para criar a pilha aloca dinamicamente essa estrutura e inicializa a pilha como

sendo vazia, isto é, com o número de elementos igual a zero.

Para inserir um elemento na pilha, usamos a próxima posição livre do vetor. Devemos

ainda assegurar que exista espaço para a inserção do novo elemento, tendo em vista que trata-se de um vetor com dimensão fixa.

A função pop retira o elemento do topo da pilha, fornecendo seu valor como retorno.

Podemos também verificar se a pilha está ou não vazia.

A função que verifica se a pilha está vazia pode ser dada por:

Page 66: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Finalmente, a função para liberar a memória alocada pela pilha pode ser:

1.22.2 Implementação de pilha com Lista.

Quando o número máximo de elementos que serão armazenados na pilha não é conhecido,

devemos implementar a pilha usando uma estrutura de dados dinâmica, no caso, empregando

uma lista encadeada. Os elementos são armazenados na lista e a pilha pode ser representada

simplesmente por um ponteiro para o primeiro nó da lista.

O nó da lista para armazenar valores reais pode ser dado por:

A estrutura da pilha é então simplesmente:

A função cria aloca a estrutura da pilha e inicializa a lista como sendo vazia.

O primeiro elemento da lista representa o topo da pilha. Cada novo elemento é inserido no

início da lista e, conseqüentemente, sempre que solicitado, retiramos o elemento também do

início da lista. Desta forma, precisamos de duas funções auxiliares da lista:

para inserir no início e

para remover do início.

Ambas as funções retornam o novo primeiro nó da lista.

Page 67: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

As funções que manipulam a pilha, (Inserir e excluir) fazem uso dessas funções de lista:

A pilha estará vazia se a lista estiver vazia:

Por fim, a função que libera a pilha deve antes liberar todos os elementos da lista.

Page 68: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

A rigor, pela definição da estrutura de pilha, só temos acesso ao elemento do topo. No

entanto, para testar o código, pode ser útil implementarmos uma função que imprima os

valores armazenados na pilha. Os códigos abaixo ilustram a implementação dessa função nas

duas versões de pilha (vetor e lista). A ordem de impressão adotada é do topo para a base.

1.23 Filas.

Page 69: estrutura de dados AJSG.pdf

Estrutura de Dados Prof. airton josé sachetim garcia

Referências Bibliográficas.

1. http://www.gsmfans.com.br/index.php?topic=67407.0;

2. http://pt.wikipedia.org/wiki/C%2B%2B;

3. Lista de exercício Prof. Alexandre Ribeiro – FEIS;

4. Lista de exercício Prof. Anirio Salles Filho – FEIS;

5. Lista de exercício Profa. Erica Regina Marani Daruichi Machado – FEIS;

6. Apostila: “Introdução à Ciência da Computação e Teoria e Desenvolvimento de

Algoritmos”, Profa. Erica M. Daruichi Machado – FEIS;

7. Curso Básico de Lógica de Programação, Unicamp - Centro de Computação – DSC,

Autor: Paulo Sérgio de Moraes;

8. http://www.dca.fee.unicamp.br/cursos/EA876/apostila/HTML/node37.html;

9. http://www.linhadecodigo.com.br/Artigo.aspx?id=1114;

10. http://www.vivaolinux.com.br/artigo/Substituindo-a-biblioteca-conio.h-no-Linux-

usando-ncursescurses.h/;

11. http://allanlima.wordpress.com/;

12. http://pt.wikipedia.org/wiki/C_(linguagem_de_programação);

13. http://www.apostilando.com/download.php?cod=3149&categoria=C%20e%20C++;

14. http://pt.wikibooks.org/wiki/Programar_em_C%2B%2B;

15. http://www.tiexpert.net/programacao;

16. http://pt.wikibooks.org/wiki/Programar_em_C/Vetores;

17. http://pt.wikipedia.org/wiki/C%2B%2B;

18.