Encontre um "buraco" em uma lista de números

14

Qual é a maneira mais rápida de encontrar o primeiro inteiro (menor) que não existe em uma determinada lista de números inteiros não classificados (e que é maior que o menor valor da lista)?

Minha abordagem primitiva é classificá-los e percorrer a lista, existe uma maneira melhor?

Fabian Zeindl
fonte
6
@Jodrell Acho classificando uma progressão infinita seria difícil ;-)
maple_shaft
3
@maple_shaft concordou, pode demorar um pouco.
Jodrell
4
Como você define primeiro uma lista não classificada?
Jodrell
1
Acabei de perceber que isso provavelmente pertence ao StackOverflow, já que não é realmente um problema conceitual.
JasonTrue
2
@JasonTrue A partir do FAQ, If you have a question about… •algorithm and data structure conceptsestá no tópico IMHO.
maple_shaft

Respostas:

29

Supondo que você queira dizer "número inteiro" quando diz "número", você pode usar um vetor de bit de tamanho 2 ^ n, em que n é o número de elementos (digamos que seu intervalo inclua números inteiros entre 1 e 256, use um número 256- bit ou 32 bytes, vetor de bits). Quando você encontrar um número inteiro na posição n do seu intervalo, defina o enésimo bit.

Quando você termina de enumerar a coleção de números inteiros, itera sobre os bits no seu vetor de bits, procurando a posição de qualquer conjunto de bits 0. Eles agora correspondem à posição n dos números inteiros ausentes.

Isso é O (2 * N), portanto O (N) e provavelmente é mais eficiente em termos de memória do que classificar a lista inteira.

JasonTrue
fonte
6
Bem, como comparação direta, se você tivesse todos os números inteiros positivos não assinados de 32 bits, com exceção de 1, seria possível resolver o problema do número inteiro ausente em cerca de meio gigabyte de memória. Se você ordenou, teria que usar mais de 8 gigabytes de memória. E a classificação, exceto em casos especiais como este (sua lista é classificada assim que você tiver um vetor de bits) é quase sempre n log n ou pior, portanto, exceto nos casos em que a constante supera a complexidade do custo, a abordagem linear vence.
JasonTrue
1
E se você não souber o alcance a priori?
Blrfl
2
Se você tem um tipo de dados inteiro, Blrfl, certamente conhece as extensões máximas do intervalo, mesmo se não tiver informações suficientes para restringir ainda mais. Se você sabe que é uma lista pequena, mas não sabe o tamanho exato, a classificação pode ser uma solução mais simples.
JasonTrue
1
Ou faça outro loop primeiro na lista para encontrar o menor e o maior elemento. Em seguida, você pode alocar uma matriz do tamanho exato com o menor valor como deslocamento básico. Ainda O (N).
Secure
1
@JPatrick: Não trabalho de casa, negócios, me formei em CS anos atrás :).
Fabian Zeindl
4

Se você classificar a lista inteira primeiro, garante o pior tempo de execução. Além disso, sua escolha do algoritmo de classificação é crítica.

Aqui está como eu abordaria esse problema:

  1. Use uma classificação de pilha , concentrando-se nos menores elementos da lista.
  2. Após cada troca, veja se você tem uma lacuna.
  3. Se você encontrar uma lacuna, então return: Você encontrou sua resposta.
  4. Se você não encontrar uma lacuna, continue trocando.

Aqui está uma visualização de uma classificação de heap .

Jim G.
fonte
Uma pergunta, como você identifica os "menores" elementos da lista?
Jodrell
4

Para ser esotérico e "inteligente", no caso especial da matriz com apenas um "furo", você pode tentar uma solução baseada em XOR:

  • Determine o alcance da sua matriz; isso é feito configurando uma variável "max" e "min" para o primeiro elemento da matriz e, para cada elemento depois disso, se esse elemento for menor que o mínimo ou maior que o máximo, defina o mínimo ou o máximo como novo valor.
  • Se o intervalo for um a menos que a cardinalidade do conjunto, haverá apenas um "furo" para que você possa usar o XOR.
  • Inicialize uma variável inteira X para zero.
  • Para cada número inteiro de min a max, inclusive, XOR esse valor com X e armazene o resultado em X.
  • Agora, XOR cada número inteiro na matriz com X, armazenando cada resultado sucessivo em X como antes.
  • Quando terminar, X será o valor do seu "buraco".

Isso será executado em aproximadamente 2N tempo semelhante à solução de vetor de bits, mas requer menos espaço de memória para qualquer tamanho N> de (int). No entanto, se a matriz tiver vários "orifícios", X será a "soma" XOR de todos os orifícios, o que será difícil ou impossível de separar nos valores reais do orifício. Nesse caso, você volta a algum outro método, como as abordagens "pivot" ou "bitvector" de outras respostas.

Você pode recursar isso também usando algo semelhante ao método de pivô para reduzir ainda mais a complexidade. Reorganize a matriz com base em um ponto de articulação (que será o máximo do lado esquerdo e o mínimo da direita; será trivial encontrar o máximo e o mínimo de toda a matriz durante a rotação). Se o lado esquerdo do pivô tiver um ou mais orifícios, recue somente nesse lado; caso contrário, recuar para o outro lado. Em qualquer ponto em que você possa determinar que há apenas um furo, use o método XOR para encontrá-lo (que deve ser mais barato no geral do que continuar girando até uma coleção de dois elementos com um furo conhecido, que é o caso base para o algoritmo de pivô puro).

KeithS
fonte
Isso é ridiculamente inteligente e incrível! Agora você pode encontrar uma maneira de fazer isso com um número variável de furos? :-D
2

Qual é o intervalo de números que você encontrará? Se esse intervalo não for muito grande, você pode resolver isso com duas varreduras (tempo linear O (n)) usando uma matriz com tantos elementos quanto números, trocando espaço por tempo. Você pode encontrar o intervalo dinamicamente com mais uma varredura. Para reduzir o espaço, você pode atribuir 1 bit a cada número, fornecendo 8 números de armazenamento por byte.

Sua outra opção, que pode ser melhor para cenários iniciais e seria insituitária, em vez de copiar memória, é modificar a classificação para sair mais cedo, se o mínimo encontrado em um passe de digitalização não for 1 a mais que o último min encontrado.

Peter Smith
fonte
1

Não, na verdade não. Como qualquer número ainda não escaneado pode sempre ser um número que preenche um determinado "buraco", não é possível evitar escanear cada número pelo menos uma vez e depois compará-lo com seus possíveis vizinhos. Você provavelmente poderia acelerar as coisas construindo uma árvore binária mais ou menos e percorrendo-a da esquerda para a direita até encontrar um buraco, mas isso é essencialmente da mesma complexidade de tempo da classificação, uma vez que está sendo classificada. E você provavelmente não terá nada mais rápido que o Timsort .

pillmuncher
fonte
1
Você está dizendo que percorrer uma lista é a mesma complexidade de tempo que a classificação?
Maple_shaft
@ maple_shaft: Não, estou dizendo que construir uma árvore binária a partir de dados aleatórios e percorrê-la da esquerda para a direita equivale a classificar e percorrer de pequeno a grande.
pillmuncher
1

A maioria das idéias aqui não passa de uma triagem. A versão do bitvector é Bucketsort simples. O tipo de pilha também foi mencionado. Basicamente, tudo se resume a escolher o algoritmo de classificação correto, que depende dos requisitos de tempo / espaço e também do intervalo e do número de elementos.

Na minha opinião, o uso de uma estrutura de heap é provavelmente a solução mais geral (um heap basicamente fornece os menores elementos de maneira eficiente, sem uma classificação completa).

Você também pode analisar abordagens que encontrem os menores números primeiro e depois procurar por um número inteiro maior que isso. Ou você encontra os 5 menores números esperando que o espaço fique vazio.

Todos esses algoritmos têm sua força, dependendo das características de entrada e dos requisitos do programa.

Gerenuk
fonte
0

Uma solução que não usa armazenamento adicional ou assume a largura (32 bits) de números inteiros.

  1. Em uma passagem linear, encontre o menor número. Vamos chamar isso de "min". O (n) complexidade do tempo.

  2. Escolha um elemento dinâmico aleatório e faça uma partição no estilo quicksort.

  3. Se o pivô acabar na posição = ("pivô" - "min"), recue no lado direito da partição, caso contrário, recue no lado esquerdo da partição. A idéia aqui é que, se não houver furos desde o início, o pivô estará na ("pivô" - "min") na posição, portanto, o primeiro furo deve ficar à direita da partição e vice-versa.

  4. A caixa base é uma matriz de 1 elemento e o orifício fica entre esse elemento e o próximo.

A complexidade total esperada do tempo de execução é O (n) (8 * n com as constantes) e o pior caso é O (n ^ 2). A análise da complexidade do tempo para um problema semelhante pode ser encontrada aqui .

aufather
fonte
0

Acredito que criei algo que deve funcionar de maneira geral e eficiente se você não tiver duplicatas * (no entanto, deve ser extensível a qualquer número de furos e a qualquer intervalo de números inteiros).

A idéia por trás desse método é como quicksort, pois encontramos um pivô e o particionamos em volta dele, depois recuamos no (s) lado (s) com um furo. Para ver quais lados têm o furo, encontramos os números mais baixo e mais alto e os comparamos com o pivô e o número de valores desse lado. Digamos que o pivô seja 17 e o número mínimo seja 11. Se não houver furos, deve haver 6 números (11, 12, 13, 14, 15, 16, 17). Se houver 5, sabemos que existe um buraco nesse lado e podemos recuar desse lado para encontrá-lo. Estou tendo problemas para explicar mais claramente do que isso, então vamos dar um exemplo.

15 21 10 13 18 16 22 23 24 20 17 11 25 12 14

Pivô:

10 13 11 12 14 |15| 21 18 16 22 23 24 20 17 25

15 é o pivô, indicado pelos tubos ( ||). Existem 5 números no lado esquerdo do pivô, como deve haver (15 - 10) e 9 à direita, onde deve haver 10 (25 - 15). Então, nós recursamos no lado direito; notaremos que o limite anterior era 15, caso o buraco esteja adjacente a ele (16).

[15] 18 16 17 20 |21| 22 23 24 25

Agora, existem 4 números no lado esquerdo, mas deve haver 5 (21 - 16). Então, recuamos lá e, novamente, notamos o limite anterior (entre colchetes).

[15] 16 17 |18| 20 [21]

O lado esquerdo tem os 2 números corretos (18 - 16), mas o direito tem 1 em vez de 2 (20 - 18). Dependendo de nossas condições finais, poderíamos comparar o número 1 com os dois lados (18, 20) e ver que 19 está faltando ou se repetir mais uma vez:

[18] |20| [21]

O lado esquerdo tem um tamanho de zero, com um espaço entre o pivô (20) e o limite anterior (18), então 19 é o furo.

*: Se houver duplicatas, você provavelmente poderá usar um conjunto de hash para removê-las em O (N), mantendo o método geral O (N), mas isso pode levar mais tempo do que usar outro método.

Kevin
fonte
1
Não acredito que o OP tenha dito nada sobre haver apenas um buraco. A entrada é uma lista não classificada de números - eles podem ser qualquer coisa. Não está claro em sua descrição como você determinaria quantos números "deveriam existir".
Caleb
@caleb Não importa quantos furos existem, apenas duplicatas (que podem ser removidas em O (N) com um conjunto de hash, embora na prática isso possa ter mais sobrecarga do que outros métodos). Eu tentei melhorar a descrição, veja se é melhor.
25412 Kevin
Isso não é linear, IMO. É mais como (logN) ^ 2. Em cada etapa, você gira o subconjunto da coleção de sua preferência (a metade do subarray anterior que você identificou como tendo o primeiro "buraco") e depois recorre para o lado esquerdo, se houver um "buraco", ou o lado direito, se o lado esquerdo não. (logN) ^ 2 ainda é melhor que linear; se N aumentar dez vezes, você executa apenas a ordem de 2 (log (N) -1) + mais 1 etapa.
Keiths
@Keith - infelizmente, você precisa olhar para todos os números em cada nível para girá-los, então isso levará cerca de n + n / 2 + n / 4 + ... = 2n (tecnicamente, 2 (nm)) comparações .
Kevin