ESTRUTURA DE DADOS Professor Victor Sotero Estrutura de Dados 1.
estrutura de dados AJSG.pdf
-
Upload
lucas-franca -
Category
Documents
-
view
225 -
download
0
Transcript of 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
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
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
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.
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#.
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.
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:
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.
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:
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>
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 (%).
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.
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.
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.
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++.
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
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ê
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).
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.
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:
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();}
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:
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:
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:
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
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.
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:
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.
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
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
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.
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.
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.
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.
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
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.
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:
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.
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.
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ê?
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.
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.
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.).
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.
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.
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:
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.
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.
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:
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.
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:
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.
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:
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.
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.
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];
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:
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.
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.
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.
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
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.
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).
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.
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:
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.
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.
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.
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.