Exercícios MT1
Nenhum exercício corresponde a esta pesquisa.
Classes e polimorfismo em C++
Despacho dinâmico apenas em membros-função virtuaisEscolha múltipla
Teste 1 (CI1), 2016/17
class Function {
public:
virtual float evaluate(float x)=0;
string info();
};
class Double: public Function {
public:
float evaluate(float x);
string info();
};
Function *f = new Double();
float value = f->evaluate(4.0);
string info = f->info();Considere o código acima (as classes Function e Double estão parcialmente definidas). Indique o que acontece na execução das duas últimas instruções.
Solução
value é o resultado de Double::evaluate() e info é o resultado de Function::info().
A tabela de funções virtuais só inclui membros-função declarados virtual. Assim, f->evaluate(4.0) faz despacho dinâmico para Double::evaluate(), mas f->info() resolve estaticamente para Function::info().
- O código está incorreto, devido à atribuição inválida (=0) na declaração de Function::evaluate. A sintaxe =0 declara um membro-função virtual puro, que é válida e torna a classe abstrata.
- value = 0 e info é o resultado da invocação de Function::info(). value não fica 0; como evaluate é virtual, é invocada Double::evaluate().
- value é o resultado de Double::evaluate() e info é o resultado de Double::info(). info não é declarada virtual em Function, logo não há despacho dinâmico para Double::info().
- value é o resultado de Double::evaluate() e info é o resultado de Function::info(). evaluate é virtual, logo a chamada através de Function* invoca Double::evaluate(); info não é virtual, logo usa a implementação da classe estática do apontador.
- Nenhuma das possibilidades anteriores. A quarta opção descreve corretamente o comportamento.
Objetos de uma classe abstrataEscolha múltipla
Teste 1 (CI1), 2016/17
Shape é uma classe abstrata. É correto afirmar que:
Solução
Não é possível criar objetos da classe Shape.
A instanciação de uma classe abstrata é impedida pelo compilador devido à presença de membros-função virtuais puros. A sua utilidade está em definir uma interface comum para as classes derivadas.
- Todos os membros-função da classe Shape devem ser abstratos. Basta um membro-função virtual puro para a classe ser abstrata; os restantes podem ter implementação.
- Só é possível criar objetos da classe Shape se estes forem constantes. A constante não altera o facto de a classe ser abstrata.
- Shape pode ser uma classe base, mas não uma classe derivada. Uma classe abstrata pode ser base e também derivar de outra classe.
- Não é possível criar objetos da classe Shape. Uma classe abstrata tem pelo menos um membro-função virtual puro e não pode ser instanciada; serve apenas como base de classes concretas.
- Nenhuma das possibilidades anteriores. A quarta opção está correta.
Invocação polimórfica através de apontador da classe baseEscolha múltipla
Teste 1 (CI1), 2017/18
class Pessoa {
public:
virtual void info() { /* ... */ }
};
class Estudante: public Pessoa {
public:
void info() { /* ... */ }
};
class Mestrando: public Estudante {
public:
void info() { /* ... */ }
};
int main() {
Pessoa *p = new Mestrando();
p->info();
}Considere o código acima (incompleto). A instrução p->info() resulta em:
Solução
É invocada Mestrando::info().
Como info() é declarada virtual em Pessoa e redefinida em Estudante e Mestrando, a chamada através de Pessoa* usa a tabela virtual do objeto concreto (Mestrando).
- Invocação de Pessoa::info(). O tipo estático do apontador não decide; o despacho é dinâmico.
- Invocação de Estudante::info(). O objeto criado é Mestrando, que redefine info().
- Invocação de Mestrando::info(). info() é virtual e o objeto apontado é um Mestrando, logo a versão mais derivada é invocada.
- O programa está errado, pois o método virtual void info() não pode ter implementação. Um método virtual pode ter implementação na classe base.
- Nenhuma das possibilidades anteriores. A terceira opção está correta.
Atribuições entre classes genéricas base e derivadaEscolha múltipla
Teste 1 (CI1), 2018/19
template <class T> class Auto { };
template <class T> class AutoOne: public Auto<T> { };
I. Auto<string> a = AutoOne<string>();
II. AutoOne<string> b = Auto<string>();
III. Auto<string> c = AutoOne<int>();Considere as classes genéricas acima. Que atribuições estão corretas?
Solução
Apenas a atribuição I está correta.
Existe conversão implícita de AutoOne
- I, II e III. II e III são inválidas.
- II apenas. II é inválida: não se pode converter base em derivada implicitamente.
- I apenas. A conversão derivada-para-base é válida (I); base-para-derived (II) e entre instanciações com tipos diferentes (III) não compilam.
- I e II apenas. II não compila.
- Nenhuma das possibilidades anteriores. A opção I é válida.
Classe abstrata: definição e utilidadeAutoavaliação
Teste 1 (CI1), 2017/18
Defina o conceito de classe abstrata. Qual a utilidade de uma classe abstrata?
Solução
Uma classe abstrata é uma classe com pelo menos um membro-função virtual puro, que não pode ser instanciada. Serve de base/interface para hierarquias de classes, permitindo despacho polimórfico através de apontadores ou referências da classe base.
O virtual puro (=0) obriga as classes derivadas a fornecerem a sua implementação, garantindo o contrato da interface definida na classe base.
Confere a tua resposta:
- A resposta indica que uma classe abstrata contém pelo menos um membro-função virtual puro (declarado com =0).
- A resposta afirma que não é possível criar objetos dessa classe.
- A resposta indica a utilidade: definir uma interface comum que as classes derivadas concretizam, permitindo polimorfismo.
Complexidade temporal
Complexidade de ciclo aninhado com passo geométricoEscolha múltipla
Teste 1 (CI1), 2016/17
void function1(vector<int> &v1) {
for (int i=0; i<v1.size(); i++)
for (int j=1; j<v1.size(); j*=2)
cout << i << " - " << j << endl;
}Indique a complexidade temporal da função acima.
Solução
O(n log n).
Para cada um dos n valores de i, o ciclo interior executa cerca de log₂(n) iterações (j = 1, 2, 4, ...). O número total de impressões é, portanto, Θ(n log n).
- O(log n). O ciclo exterior alone já executa n iterações.
- O(n). Falta contar o ciclo interior, que também itera.
- O(n log n). O ciclo exterior dá n iterações e o interior, com j*=2, dá O(log n); o total é O(n log n).
- O(n²). O ciclo interior não é linear, pois j cresce geometricamente.
- Nenhuma das anteriores. A terceira opção está correta.
Tempo de um algoritmo O(n²) ao duplicar a entradaEscolha múltipla
Teste 1 (CI1), 2016/17
A complexidade temporal de um algoritmo é . Este algoritmo apresentou um tempo de execução de 10 segundos para um conjunto de dados de entrada de tamanho . Se o tamanho dos dados de entrada for duplicado, quanto tempo demorará (aproximadamente) a execução deste algoritmo?
Solução
40 segundos (aproximadamente).
Num algoritmo Θ(n²), duplicar o tamanho da entrada quadruplica o número de operações básicas: 10 s × 4 = 40 s.
- 10 segundos. O tempo não fica igual.
- 20 segundos. Duplicar n não duplica o tempo num algoritmo quadrático.
- 40 segundos. Se n passa para 2n, o custo passa de c·n² para c·(2n)² = 4·c·n², ou seja, 4 vezes mais: 40 segundos.
- 100 segundos. Seria 10× o tempo original, não 4×.
- Nenhuma das anteriores. A terceira opção está correta.
Complexidade com ciclo exterior de tamanho fixoEscolha múltipla
Teste 1 (CI1), 2017/18
int funcaoQQ(vector<int> &v) {
int ct=0;
for (int i=0; i<10; i++) {
for (int j=v.size()-1; j>i; j/=2)
if (v[i]<v[j]) ct++;
}
return ct;
}Indique a complexidade temporal da função acima.
Solução
O(log n).
O ciclo exterior repete-se um número fixo de vezes (10), pelo que é uma constante. Em cada iteração, o ciclo interior divide j por 2, executando O(log n) passos. O total é 10·O(log n) = O(log n).
- O(log n). O ciclo exterior tem 10 iterações (constante) e o interior O(log n), logo o total é O(log n).
- O(n). O ciclo exterior não depende de n.
- O(n log n). Seria se o ciclo exterior iterasse n vezes; aqui itera 10.
- O(n²). Nenhum ciclo é quadrático.
- Nenhuma das anteriores. A primeira opção está correta.
Complexidade de ciclo aninhado com divisão por 2Escolha múltipla
Teste 1 (CI1), 2018/19
int funcaoQQ2(vector<int> &vec) {
int ct=0;
for (int i=0; i<vec.size(); i++) {
cout << vec[i];
for (int j=vec.size(); j>i; j/=2)
if (vec[i]<vec[j]) ct++;
}
return ct;
}Indique a complexidade temporal do fragmento de código acima (no enunciado original faltava o int na declaração de i, aqui normalizado).
Solução
O(N log N).
Para cada i, o ciclo interior começa em N e divide j por 2 até j ≤ i, num total de O(log N) iterações. Ao fim de N iterações do ciclo exterior, o custo total é O(N log N).
- O(N). O ciclo interior também contribui.
- O(log N). O ciclo exterior já é O(N).
- O(N log N). O ciclo exterior itera N vezes e o interior O(log N) vezes; o total é O(N log N).
- O(N²). O ciclo interior não é linear.
- Nenhuma das anteriores. A terceira opção está correta.
Comparação assintótica de função exponencial com polinomialEscolha múltipla
Teste 1 (CI1 teórica), 2022/23
Considere as funções e . Qual das afirmações é verdadeira?
Solução
, mas não nem .
Toda função exponencial com base > 1 domina qualquer polinomial. Assim, , o que mostra que f é Ω de g mas não O de g.
- . Para ser Θ, f teria de ser simultaneamente O e Ω de g; não é O de g.
- não é , ou de . f é Ω de g.
- . A exponencial cresce mais depressa que o polinomial, logo f não é limitada superiormente por g.
- . 2^(n/2) cresce mais depressa que n², logo existe c tal que f(n) ≥ c·g(n) para n suficientemente grande.
Complexidade da soma recursiva por divisão em duas metadesEscolha múltipla
Teste 1 (CI1 teórica), 2022/23
int sum(int a, int b) {
if (a==b) return a;
int middle = (b-a)/2 + a;
return sum(a, middle) + sum(middle+1, b);
}A função recursiva acima calcula a soma dos inteiros entre a e b. Qual é a sua complexidade temporal?
Solução
.
Cada chamada divide o intervalo em duas metades e faz trabalho constante, pelo que T(n) = 2T(n/2) + O(1). Pelo teorema mestre (caso 2), ou somando os níveis da árvore de recorrência (n folhas e custo total proporcional a n), obtém-se Θ(n).
- . A árvore de recorrência não tem 2^n folhas.
- . Cada nível trabalha O(1) por nó e há n folhas, não n log n de trabalho total.
- . Há n chamadas no último nível.
- . A recorrência T(n)=2T(n/2)+O(1) resolve-se para Θ(n): a árvore tem n folhas e o trabalho por nível decresce geometricamente.
Pesquisa e ordenação
Custo de inserir num vetor ordenadoEscolha múltipla
Teste 1 (CI1), 2017/18
A inserção de um elemento num vetor ordenado (mantendo a ordenação) é realizada pelo seguinte algoritmo: uso de pesquisa binária para localizar a posição onde deve ser inserido o novo elemento; inserir o novo elemento nessa posição. Qual a complexidade temporal deste algoritmo?
Solução
O(N).
Localizar a posição com pesquisa binária custa O(log N), mas a posição de inserção obriga a deslocar todos os elementos à direita, uma operação O(N). O custo total é dominado por O(N).
- O(log N). A pesquisa binária é O(log N), mas a inserção exige deslocar elementos.
- O(N log N). O deslocamento é linear, não logarítmico.
- O(N²). Não há dois ciclos aninhados.
- O(N). A pesquisa binária custa O(log N), mas inserir na posição correta desloca até N elementos, com custo O(N) dominante.
- Nenhuma das anteriores. A quarta opção está correta.
Trocas inúteis da ordenação por seleção num vetor decrescenteEscolha múltipla
Teste 1 (CI1), 2018/19
O algoritmo de ordenação por seleção é usado na ordenação, por ordem crescente, de um vetor de 100 elementos. Considerando que o vetor está inicialmente ordenado por ordem decrescente {100, 99, 98, ..., 1}, quantas trocas inúteis são realizadas, aproximadamente? (uma troca inútil ocorre quando um elemento é trocado com ele próprio, logo o vetor não se altera).
Solução
Aproximadamente 50 trocas inúteis.
Numa ordenação por seleção, a cada passo i procura-se o mínimo do sufixo v[i..99] e troca-se com v[i]. Num vetor decrescente, a segunda metade do vetor já fica ordenada por ordem crescente à medida que os menores vão sendo colocados à frente; a partir de cerca de i = 50, o mínimo já está na posição i e a troca é inútil. Total: ≈ n/2 = 50.
- 100. Nem todas as iterações fazem troca inútil.
- 50. Na primeira metade o elemento mínimo ainda não está no sítio; a partir de i ≈ 50 o mínimo do sufixo já está na posição i e a troca é consigo próprio.
- 0. Há várias trocas consigo próprio na segunda metade.
- 6. O valor não depende de logaritmos.
- Nenhuma das anteriores. A segunda opção está correta.
Invariante útil do bubblesort decrescenteEscolha múltipla
Teste 1 (CI1 teórica), 2022/23
void bubblesort(vector<int> &v) {
for (unsigned i=0; i<v.size()-1; i++)
for (unsigned j=0; j<v.size()-i-1; j++)
if (v[j] < v[j+1])
swap(v[j], v[j+1]);
}Está a ordenar-se um vetor de inteiros por ordem decrescente com o bubblesort acima. Qual das seguintes condições é um invariante útil no início de cada iteração do ciclo com a variável i, para provar a correção do algoritmo?
Solução
Os maiores i números de todo o vetor estão nas últimas i posições e estão ordenados.
Como a ordenação é decrescente e a comparação troca quando v[j] < v[j+1], o maior elemento remanescente acaba na última posição ainda não fixada a cada passagem. Assim, no início da iteração i, as posições v[n-i..n-1] contêm os i maiores valores, por ordem decrescente, e nunca mais saem de lá.
- Os mais pequenos i números de todo o vetor estão nas primeiras i posições e estão ordenados. Isso seria o invariante da ordenação crescente, não da decrescente.
- Os maiores i números de todo o vetor estão nas primeiras i posições e estão ordenados. Os maiores elementos vão para o fim do vetor nesta implementação decrescente.
- Os mais pequenos i números de todo o vetor estão nas últimas i posições e estão ordenados. Numa ordenação decrescente, os menores é que ficam no fim, mas o invariante do ciclo exterior refere-se aos maiores i números.
- Os maiores i números de todo o vetor estão nas últimas i posições e estão ordenados. A cada passagem do ciclo interior, o maior elemento da parte não processada sobe para a última posição livre; no início da iteração i do ciclo exterior, as últimas i posições contêm os maiores i elementos, ordenados.
Pior caso do QuicksortAutoavaliação
Teste 1 (CI1), 2017/18
O algoritmo de ordenação Quicksort possui complexidade temporal no caso médio, mas pode, no pior caso, apresentar complexidade temporal . Explique porquê.
Solução
O pior caso acontece quando, em cada partição, um dos lados fica vazio: o pivô é o menor ou o maior elemento. A recorrência fica T(n) = T(n-1) + Θ(n), que resolve para Θ(n²). Exemplo: vetor já ordenado com o pivô escolhido no primeiro elemento.
Cada partição custa Θ(n) para colocar o pivô no sítio, mas se um lado tem tamanho 0 e o outro n-1, a árvore de recursão degenera numa lista com n níveis, dando um total de n + (n-1) + ... + 1 = Θ(n²).
Confere a tua resposta:
- A resposta indica que o pior caso ocorre quando a partição é sempre maximamente desbalanceada (o pivô é o menor ou o maior elemento do intervalo).
- A resposta apresenta a recorrência T(n) = T(n-1) + Θ(n) (ou equivalente) e conclui Θ(n²).
- A resposta dá um exemplo de entrada que provoca esse caso, como um vetor já ordenado com o pivô escolhido no primeiro elemento.
Estruturas de dados lineares
Estrutura adequada ao botão "voltar" de um browserEscolha múltipla
Teste 1 (CI1 teórica), 2022/23
Qualquer browser possui a funcionalidade de retorno à página visitada anteriormente. Na implementação desta (e apenas desta) funcionalidade, qual a estrutura de dados que considera mais adequada (eficiente em tempo e espaço)?
Solução
Pilha (stack).
A cada nova página faz-se push; o botão "voltar" faz pop, devolvendo a página visitada mais recentemente. É a disciplina LIFO, implementada de forma eficiente e económica com uma pilha.
- Fila. A fila dá o primeiro a entrar, não o último.
- Lista duplamente ligada. Gasta mais memória do que o necessário para esta função.
- Lista simplesmente ligada. Também funciona, mas a pilha é a estrutura natural e mais simples.
- Pilha. A página "anterior" é sempre a última visitada: comportamento LIFO, com push/pop em O(1).
Operações em O(1) numa lista simplesmente ligada com cabeça e caudaEscolha múltipla
Teste 1 (CI1 teórica), 2022/23
Considere a estrutura lista implementada por uma lista simplesmente ligada com referências para o início e o fim (primeiro e último nó). Quais das seguintes operações podem ser realizadas em tempo O(1)?
- I. inserir um elemento no início da lista
- II. inserir um elemento no fim da lista
- III. remover o primeiro elemento (início) da lista
- IV. remover o último elemento (fim) da lista
Solução
I, II e III apenas.
Inserir no início (I), inserir no fim graças à cauda (II) e remover o primeiro (III) são operações locais, em O(1). Remover o último (IV) exige o nó anterior ao último, que numa lista simplesmente ligada obriga a percorrer a lista inteira: O(n).
- I, II e IV, apenas. IV não é O(1) numa lista simplesmente ligada.
- I e II, apenas. Falta a III, que também é O(1).
- I, II e III, apenas. Com apontador para a cauda, inserir no fim é O(1); remover o último exigiria percorrer a lista para atualizar a cauda, logo é O(n).
- I e III, apenas. II também é O(1) graças à referência para o fim.
Árvores binárias de pesquisa
Árvore binária de pesquisa resultante de uma sequência de inserçõesEscolha múltipla
Teste 1 (CI1 teórica), 2022/23
Se introduzir a sequência 10, 15, 7, 9, 17, 12 numa árvore binária de pesquisa, a árvore resultante:
Solução
Não é completa, e é equilibrada.
Inserindo por ordem: 10 é a raiz; 15 fica à direita; 7 à esquerda; 9 à direita de 7; 17 à direita de 15; 12 à esquerda de 15. A subárvore de 7 tem apenas o filho direito 9, pelo que o nível 2 não está preenchido da esquerda para a direita e a árvore não é completa. Em qualquer nó, as alturas das duas subárvores diferem no máximo de 1 (raiz: 1 e 2), logo a árvore é equilibrada.
- Não é completa, e é equilibrada. O nó 7 só tem filho direito (9), logo a árvore não é completa; mas as alturas das subárvores de cada nó diferem no máximo de 1, logo é equilibrada.
- É completa, e não é equilibrada. Não é completa nem está desiquilibrada.
- Não é completa e não é equilibrada. A árvore é equilibrada.
- É completa e equilibrada. Não é completa.