O tempo constante e o tempo constante amortizado são efetivamente considerados equivalentes?

16

Preciso escrever um RandomQueue que permita anexos e remoção aleatória em Constant Time (O (1)).

Meu primeiro pensamento foi apoiá-lo com algum tipo de Array (eu escolhi um ArrayList), pois os arrays têm acesso constante por meio de um índice.

Examinando a documentação, porém, percebi que as adições de ArrayLists são consideradas Tempo constante amortizado, pois uma adição pode exigir uma realocação da matriz subjacente, que é O (n).

O Tempo Constante Amortizado e o Tempo Constante são efetivamente os mesmos ou preciso examinar uma estrutura que não exija uma realocação completa de todas as adições?

Estou perguntando isso porque estruturas baseadas em array de lado (que, até onde eu sei, sempre terão adições de tempo constante amortizado), não consigo pensar em nada que atenda aos requisitos:

  • Qualquer coisa baseada em árvore terá, na melhor das hipóteses, acesso O (log n)
  • Uma lista vinculada pode potencialmente ter adições de O (1) (se uma referência à cauda for mantida), mas uma remoção aleatória deve ser, na melhor das hipóteses, O (n).

Aqui está a pergunta completa; no caso de ver alguns detalhes importantes:

Projete e implemente um RandomQueue. Esta é uma implementação da interface da Fila, na qual a operação remove () remove um elemento escolhido uniformemente aleatoriamente entre todos os elementos atualmente na fila. (Pense em um RandomQueue como uma bolsa na qual podemos adicionar elementos ou acessar e remover cegamente algum elemento aleatório.) As operações add (x) e remove () em um RandomQueue devem ser executadas em tempo constante por operação.

Carcinigenicado
fonte
A tarefa especifica como as remoções aleatórias são realizadas? Você recebeu um índice para remover ou uma referência a um elemento da fila?
Não fornece nenhum detalhe. Os requisitos são apenas uma estrutura que implementa a interface da Fila e possui O (1) adições e remoções.
Carcigenicate
Como um aparte - uma matriz redimensionável com O (n) crescente não necessariamente tem adição de O (1): isso depende de como aumentamos a matriz. Crescer por uma quantidade constante a ainda é O (n) para adição (temos uma 1/achance para uma operação O (n)), mas crescer por um fator constante a > 1é O (1) amortizado para adição: temos uma (1/a)^nchance para um O (n) operação, mas essa probabilidade se aproxima de zero para grandes n.
amon
ArrayLists usam o último correto?
Carcigenicate
1
O autor da pergunta (eu) estava pensando na solução amortizada de tempo constante. Vou esclarecer isso na próxima edição. (Apesar de pior caso constante de tempo pode ser alcançado aqui, usando a técnica de de-amortização .)
Pat Morin

Respostas:

10

O tempo constante amortizado quase sempre pode ser considerado equivalente ao tempo constante e, sem conhecer as especificidades do seu aplicativo e o tipo de uso que você planeja fazer nessa fila, é mais provável que você esteja coberto.

Uma lista de matrizes tem o conceito de capacidade , que é basicamente igual ao maior tamanho / comprimento / contagem de itens que já foram exigidos até agora. Portanto, o que acontecerá é que, no início, a lista de matrizes continuará se realocando para aumentar sua capacidade à medida que você continua adicionando itens, mas em algum momento o número médio de itens adicionados por unidade de tempo inevitavelmente corresponderá ao número médio de itens removido por unidade de tempo (caso contrário, você acabaria ficando sem memória de qualquer maneira). Nesse ponto, a matriz deixará de se realocar e todos os anexos serão atendidos no tempo constante de O (1).

No entanto, lembre-se de que, por padrão, a remoção aleatória de uma lista de matrizes não é O (1), é O (N), porque as listas de matrizes movem todos os itens após o item removido uma posição para baixo e substituem os itens removidos. item. Para alcançar O (1), você precisará substituir o comportamento padrão para substituir o item removido por uma cópia do último item da lista de matrizes e, em seguida, remover o último item, para que nenhum item seja movido. Mas então, se você fizer isso, não terá mais exatamente uma fila.

Mike Nakis
fonte
1
Porra, bom argumento sobre remoções; Eu não considerei isso. E como estamos removendo elementos aleatoriamente, isso tecnicamente não significa que não seja mais uma fila nesse sentido?
Carcigenicate
Sim, isso significa que você não está realmente tratando-o como uma fila. Mas não sei como você planeja encontrar os itens a serem removidos. Se o seu mecanismo de encontrá-los espera que eles estejam presentes na fila na ordem em que foram adicionados, você estará sem sorte. Se você não se importa se a ordem dos itens fica distorcida, você está bem.
Mike Nakis
2
A expectativa é que eu RandomQueueimplemente a Queueinterface e que o removemétodo fornecido remova aleatoriamente em vez de estourar a cabeça, portanto, não deve haver nenhuma maneira de confiar em um pedido específico. Acho que, dada a natureza aleatória, o usuário não deve esperar manter nenhuma ordem específica. Eu citei a tarefa na minha pergunta para esclarecimentos. Obrigado.
Carcigenicate
2
Sim, parece que você ficará bem se apenas garantir que a remoção do item seja feita da maneira que sugeri.
precisa
Uma última coisa, se você não se importa. Eu pensei muito sobre isso, e não parece possível ter adições "verdadeiras" em O (1) e remoção "verdadeira" em O (1); será uma troca entre os 2. Você tem uma estrutura alocada individualmente (como uma matriz) que fornece remoção, mas não adições, ou uma estrutura alocada em blocos como uma Lista Vinculada que fornece adições, mas não remoção. Isso é verdade? Mais uma vez obrigado.
Carcigenicate
14

A questão parece pedir especificamente tempo constante, e não um tempo constante amortizado . Portanto, com relação à pergunta citada, não, eles não são efetivamente os mesmos *. No entanto, eles estão em aplicações do mundo real?

O problema típico com constante amortizada é que, ocasionalmente, você precisa pagar a dívida acumulada. Portanto, enquanto as inserções geralmente são constantes, às vezes você precisa sofrer a sobrecarga de reinserir tudo novamente quando um novo bloco é alocado.

Onde a diferença entre tempo constante e tempo constante amortizado é relevante para uma aplicação, depende se essa velocidade muito lenta ocasional é aceitável. Para um número muito grande de domínios, isso geralmente é bom. Especialmente se o contêiner tiver um tamanho máximo efetivo (como caches, buffers temporários, contêineres em funcionamento), você poderá efetivamente pagar os custos apenas uma vez durante a execução.

Em aplicações críticas de resposta, esses horários podem ser inaceitáveis. Se você precisar de uma garantia de retorno em pouco tempo, não poderá confiar em um algoritmo que ocasionalmente excederá isso. Já trabalhei nesses projetos antes, mas eles são extremamente raros.

Também depende de quão alto é esse custo. Os vetores tendem a ter um bom desempenho, pois seu custo de realocação é relativamente baixo. Se você for para o mapa de hash, no entanto, a realocação pode ser muito maior. No entanto, para a maioria dos aplicativos, provavelmente, tudo bem, especialmente servidores de vida mais longa com um limite superior nos itens do contêiner.

* Há um pouco de um problema aqui. Para fazer com que qualquer contêiner de uso geral tenha tempo constante para inserção, uma das duas coisas deve ser mantida:

  • O contêiner deve ter um tamanho máximo fixo; ou
  • você pode assumir que a alocação de memória de elementos individuais é tempo constante.
edA-qa mort-ora-y
fonte
"servidor de fígado" parece uma frase estranha para usar aqui. Você quer dizer "servidor ativo", talvez?
precisa saber é o seguinte
6

Depende - se você está otimizando a taxa de transferência ou a latência:

  • Os sistemas sensíveis à latência precisam de desempenho consistente. Para esse cenário, precisamos enfatizar o pior comportamento do sistema. Exemplos são sistemas flexíveis em tempo real, como jogos que desejam obter uma taxa de quadros consistente ou servidores da Web que precisam enviar uma resposta dentro de um determinado prazo: desperdiçar ciclos de CPU é melhor do que chegar atrasado.
  • Os sistemas com taxa de transferência otimizada não se importam com paradas ocasionais, desde que a quantidade máxima de dados possa ser processada a longo prazo. Aqui, estamos principalmente interessados ​​no desempenho amortizado. Geralmente é o caso de trituração de números ou outros trabalhos de processamento em lote.

Observe que um sistema pode ter componentes diferentes que devem ser categorizados de maneira diferente. Por exemplo, um processador de texto moderno teria um thread de interface do usuário sensível à latência, mas threads otimizados para taxa de transferência para outras tarefas, como verificação ortográfica ou exportação de PDF.

Além disso, a complexidade algorítmica geralmente não importa tanto quanto poderíamos pensar: quando um problema é limitado a um determinado número, as características de desempenho reais e medidas são mais importantes que o comportamento "para n muito grande ".

amon
fonte
Infelizmente, tenho muito pouco histórico. A pergunta termina com: "As operações add (x) e remove () em um RandomQueue devem ser executadas em tempo constante por operação".
Carcigenicate
2
@Carcigenicate, a menos que você saiba que o sistema é sensível à latência, o uso da complexidade amortizada para selecionar uma estrutura de dados deve ser absolutamente suficiente.
amon
Tenho a impressão de que isso pode ser um exercício de programação ou um teste. E certamente não é fácil. Absolutamente verdade que muito raramente importa.
gnasher729
1

Se você for solicitado para um algoritmo "amortizado constante de tempo", o algoritmo pode às vezes levar um longo tempo. Por exemplo, se você usar std :: vector em C ++, esse vetor poderá ter espaço alocado para 10 objetos e, quando você alocar o 11º objeto, será alocado espaço para 20 objetos, 10 objetos serão copiados e o 11º será adicionado. leva um tempo considerável. Mas se você adicionar um milhão de objetos, poderá ter 999.980 operações rápidas e 20 lentas, com o tempo médio sendo rápido.

Se você for solicitado a usar um algoritmo de "tempo constante", ele sempre deve ser rápido, para cada operação. Isso seria importante para sistemas em tempo real, onde você pode precisar de uma garantia de que cada operação seja sempre rápida. Frequentemente, o "tempo constante" não é necessário, mas definitivamente não é o mesmo que o "tempo constante amortizado".

gnasher729
fonte