Recentemente, recebi esta pergunta da entrevista e estou curioso para saber qual seria uma boa solução para isso.
Digamos que eu receba uma matriz 2d em que todos os números na matriz estão em ordem crescente da esquerda para a direita e de cima para baixo.
Qual é a melhor maneira de pesquisar e determinar se um número de destino está na matriz?
Agora, minha primeira inclinação é utilizar uma pesquisa binária, já que meus dados são classificados. Posso determinar se um número está em uma única linha no tempo O (log N). No entanto, são as 2 direções que me confundem.
Outra solução que acho que pode funcionar é começar em algum lugar no meio. Se o valor do meio for menor que meu objetivo, posso ter certeza de que ele está na parte quadrada esquerda da matriz a partir do meio. Eu então me movo diagonalmente e verifico novamente, reduzindo o tamanho do quadrado em que o alvo poderia estar, até que eu tenha focado no número alvo.
Alguém tem boas idéias para resolver esse problema?
Matriz de exemplo:
Ordenado da esquerda para a direita, de cima para baixo.
1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11
[[1 1][1 1]]
:?Respostas:
Esta é uma abordagem simples:
Para uma
NxM
matriz, isso é executado emO(N+M)
. Acho que seria difícil fazer melhor. :)Edit: Muita boa discussão. Eu estava falando sobre o caso geral acima; claramente, se
N
ouM
forem pequenos, você pode usar uma abordagem de pesquisa binária para fazer isso em algo próximo ao tempo logarítmico.Aqui estão alguns detalhes, para quem tem curiosidade:
História
Esse algoritmo simples é chamado de Saddleback Search . Já existe há algum tempo e é o ideal quando
N == M
. Algumas referências:No entanto, quando
N < M
, a intuição sugere que a pesquisa binária deve ser capaz de fazer melhor do queO(N+M)
: Por exemplo, quandoN == 1
, uma pesquisa binária pura será executada em tempo logarítmico em vez de linear.Limite de pior caso
Richard Bird examinou essa intuição de que a busca binária poderia melhorar o algoritmo de Saddleback em um artigo de 2006:
Usando uma técnica de conversação bastante incomum, Bird nos mostra que para
N <= M
, esse problema tem um limite inferior deΩ(N * log(M/N))
. Este limite faz sentido, pois nos dá desempenho linear quandoN == M
e desempenho logarítmico quandoN == 1
.Algoritmos para matrizes retangulares
Uma abordagem que usa uma pesquisa binária linha por linha é a seguinte:
N < M
. Digamos queN
são linhas eM
colunas.value
. Se encontrarmos, estamos prontos.s
eg
, ondes < value < g
.s
é menor quevalue
, portanto, podemos eliminá-lo.g
é maior quevalue
, para que possamos eliminá-lo.Em termos de complexidade de pior caso, esse algoritmo
log(M)
funciona para eliminar metade das soluções possíveis e, em seguida, chama a si mesmo recursivamente duas vezes em dois problemas menores. Precisamos repetir uma versão menor desselog(M)
trabalho para cada linha, mas se o número de linhas for pequeno em comparação com o número de colunas, então, poder eliminar todas essas colunas em tempo logarítmico começa a valer a pena .Isso dá ao algoritmo uma complexidade de
T(N,M) = log(M) + 2 * T(M/2, N/2)
, que Bird mostra serO(N * log(M/N))
.Outra abordagem postada por Craig Gidney descreve um algoritmo semelhante à abordagem acima: ele examina uma linha por vez usando um tamanho de passo de
M/N
. Sua análise mostra que isso também resulta emO(N * log(M/N))
desempenho.Comparação de Desempenho
A análise Big-O é muito boa, mas quão bem essas abordagens funcionam na prática? O gráfico abaixo examina quatro algoritmos para matrizes cada vez mais "quadradas":
(O algoritmo "ingênuo" simplesmente pesquisa todos os elementos da matriz. O algoritmo "recursivo" é descrito acima. O algoritmo "híbrido" é uma implementação do algoritmo de Gidney . Para cada tamanho de matriz, o desempenho foi medido cronometrando cada algoritmo em um conjunto fixo de 1.000.000 matrizes geradas aleatoriamente.)
Alguns pontos notáveis:
Resumo
O uso inteligente da pesquisa binária pode fornecer
O(N * log(M/N)
desempenho para matrizes retangulares e quadradas. OO(N + M)
algoritmo "saddleback" é muito mais simples, mas sofre de degradação de desempenho à medida que os arrays se tornam cada vez mais retangulares.fonte
M==N
queremosO(N)
complexidade, nãoO(N*log(N/N))
porque a última é zero. Um limite agudo "unificado" correto éO(N*(log(M/N)+1))
quandoN<=M
.Este problema leva
Θ(b lg(t))
tempo, ondeb = min(w,h)
et=b/max(w,h)
. Discuto a solução nesta postagem do blog .Limite inferior
Um adversário pode forçar um algoritmo a fazer
Ω(b lg(t))
consultas, restringindo-se à diagonal principal:Legenda: células brancas são itens menores, células cinzas são itens maiores, células amarelas são itens menores ou iguais e células laranja são itens maiores ou iguais. O adversário força a solução a ser a célula amarela ou laranja que o algoritmo consultar por último.
Observe que existem
b
listas classificadas independentes de tamanhot
, exigindoΩ(b lg(t))
consultas para eliminar completamente.Algoritmo
w >= h
)t
à esquerda do canto superior direito da área válidat
células na linha com uma pesquisa binária. Se um item correspondente for encontrado ao fazer isso, retorne com sua posição.t
colunas curtas.Encontrar um item:
Determinar que um item não existe:
Legenda: células brancas são itens menores, células cinzas são itens maiores e a célula verde é um item igual.
Análise
Existem
b*t
colunas curtas para eliminar. Existemb
longas filas para eliminar. Eliminar uma longa fila custaO(lg(t))
tempo. A eliminação det
colunas curtas custaO(1)
tempo.No pior caso, teremos que eliminar cada coluna e cada linha, demorando
O(lg(t)*b + b*t*1/t) = O(b lg(t))
.Observe que estou assumindo
lg
grampos para um resultado acima de 1 (ou sejalg(x) = log_2(max(2,x))
). É por isso que quandow=h
, ou sejat=1
, obtemos o limite esperado deO(b lg(1)) = O(b) = O(w+h)
.Código
fonte
O(b*(lg(t)+1))
vez deO(b*lg(t))
. Bom artigo, esp. por chamar a atenção para a "técnica do adversário" ao mostrar o limite do "pior caso".Eu usaria a estratégia de dividir e conquistar para esse problema, semelhante ao que você sugeriu, mas os detalhes são um pouco diferentes.
Esta será uma pesquisa recursiva nos subintervalos da matriz.
Em cada etapa, escolha um elemento no meio do intervalo. Se o valor encontrado for o que você está procurando, está feito.
Caso contrário, se o valor encontrado for menor do que o valor que você está procurando, então você sabe que ele não está no quadrante acima e à esquerda de sua posição atual. Portanto, pesquise recursivamente os dois subintervalos: tudo (exclusivamente) abaixo da posição atual e tudo (exclusivamente) à direita que está na posição atual ou acima.
Caso contrário, (o valor encontrado é maior do que o valor que você está procurando) você sabe que não está no quadrante abaixo e à direita de sua posição atual. Portanto, pesquise recursivamente os dois subintervalos: tudo (exclusivamente) à esquerda da posição atual e tudo (exclusivamente) acima da posição atual que está na coluna atual ou em uma coluna à direita.
E ba-da-bing, você encontrou.
Observe que cada chamada recursiva lida apenas com o subintervalo atual, não (por exemplo) TODAS as linhas acima da posição atual. Apenas aqueles no subintervalo atual.
Aqui estão alguns pseudocódigos para você:
fonte
As duas respostas principais fornecidas até agora parecem ser o
O(log N)
método indiscutivelmente "ZigZag" e oO(N+M)
método de pesquisa binária. Pensei em fazer alguns testes comparando os dois métodos com várias configurações. Aqui estão os detalhes:A matriz é N x N quadrados em todos os testes, com N variando de 125 a 8000 (o maior que meu heap JVM poderia suportar). Para cada tamanho de array, escolhi um local aleatório no array para colocar um único
2
. Em seguida, coloquei um em3
todos os lugares possíveis (à direita e abaixo do 2) e, em seguida, preenchi o resto da matriz com1
. Alguns dos comentaristas anteriores pareciam pensar que esse tipo de configuração resultaria no pior caso de tempo de execução para ambos os algoritmos. Para cada tamanho de array, escolhi 100 locais aleatórios diferentes para os 2 (alvo de pesquisa) e executei o teste. Registrei o tempo médio de execução e o tempo de execução do pior caso para cada algoritmo. Como estava acontecendo muito rápido para obter boas leituras de ms em Java e porque não confio no nanoTime () do Java, repeti cada teste 1000 vezes apenas para adicionar um fator de polarização uniforme a todas as vezes. Aqui estão os resultados:ZigZag beat binário em cada teste para tempos médios e de pior caso, no entanto, eles estão todos dentro de uma ordem de magnitude um do outro mais ou menos.
Aqui está o código Java:
fonte
Esta é uma pequena prova do limite inferior do problema.
Você não pode fazer isso melhor do que o tempo linear (em termos de dimensões da matriz, não o número de elementos). Na matriz abaixo, cada um dos elementos marcados como
*
pode ser 5 ou 6 (independentemente dos outros). Portanto, se seu valor-alvo for 6 (ou 5), o algoritmo precisa examinar todos eles.É claro que isso se expande para arrays maiores também. Isso significa que essa resposta é ótima.
Atualização: Como apontado por Jeffrey L Whitledge, é apenas ideal como o limite inferior assintótico no tempo de execução vs tamanho dos dados de entrada (tratado como uma única variável). O tempo de execução tratado como função de duas variáveis em ambas as dimensões da matriz pode ser melhorado.
fonte
Acho que aqui está a resposta e funciona para qualquer tipo de matriz classificada
fonte
Pergunta interessante. Considere esta ideia - crie um limite onde todos os números sejam maiores que o seu alvo e outro onde todos os números sejam menores que o seu alvo. Se sobrar alguma coisa entre os dois, esse é o seu alvo.
Se estou procurando 3 em seu exemplo, leio na primeira linha até chegar a 4 e, a seguir, procuro o menor número adjacente (incluindo diagonais) maior que 3:
1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11
Agora faço o mesmo para os números menores que 3:
1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11
Agora eu pergunto, há alguma coisa dentro dos dois limites? Se sim, deve ser 3. Se não, então não há 3. Meio indireto, já que não encontro o número, apenas deduzo que deve estar lá. Isso tem o bônus adicional de contar TODOS os 3.
Eu tentei isso em alguns exemplos e parece funcionar bem.
fonte
A pesquisa binária na diagonal do array é a melhor opção. Podemos descobrir se o elemento é menor ou igual aos elementos da diagonal.
fonte
A. Faça uma pesquisa binária nas linhas onde o número de destino pode estar.
B. Faça um gráfico: procure o número pegando sempre o menor nó vizinho não visitado e retrocedendo quando um número muito grande for encontrado
fonte
A pesquisa binária seria a melhor abordagem, imo. Começando em 1/2 x, 1/2 y irá cortá-lo pela metade. Ou seja, um quadrado 5x5 seria algo como x == 2 / y == 3. Arredondei um valor para baixo e um valor para cima para uma zona melhor na direção do valor desejado.
Para maior clareza, a próxima iteração forneceria algo como x == 1 / y == 2 OU x == 3 / y == 5
fonte
Bem, para começar, vamos supor que estamos usando um quadrado.
1. Pesquisando um quadrado
Eu usaria uma pesquisa binária na diagonal. O objetivo é localizar o número menor que não seja estritamente inferior ao número de destino.
Digamos que estou procurando por
4
exemplo, então acabaria localizando5
em(2,2)
.Então, tenho certeza de que se
4
estiver na mesa, está em uma posição(x,2)
ou(2,x)
comx
dentro[0,2]
. Bem, são apenas 2 buscas binárias.A complexidade não é assustadora:
O(log(N))
(3 pesquisas binárias em intervalos de comprimentoN
)2. Procurando um retângulo, abordagem ingênua
Claro, fica um pouco mais complicado quando
N
eM
difere (com um retângulo), considere este caso degenerado:E digamos que estou procurando
9
... A abordagem diagonal ainda é boa, mas a definição de diagonais muda. Aqui está minha diagonal[1, (5 or 6), 17]
. Digamos que eu peguei[1,5,17]
, então sei que se9
está na tabela, também está na subparte:Isso nos dá 2 retângulos:
Então, podemos recurse! provavelmente começando por aquele com menos elementos (embora neste caso nos mate).
Devo apontar que, se uma das dimensões for menor que
3
, não podemos aplicar os métodos diagonais e devemos usar uma busca binária. Aqui, isso significaria:10 11 12 13 14 15 16
, não encontrado5 6 7 8
, não encontrado6 7 8 9
, não encontradoÉ complicado porque para obter um bom desempenho, você pode querer diferenciar entre vários casos, dependendo da forma geral ....
3. Procurando um retângulo, abordagem brutal
Seria muito mais fácil se lidássemos com um quadrado ... então, vamos resolver as coisas.
Agora temos um quadrado.
Claro, provavelmente NÃO criaremos essas linhas, poderíamos simplesmente emulá-las.
então ele se comporta como um quadrado sem ocupar mais memória (ao custo da velocidade, provavelmente, dependendo do cache ... bem: p)
fonte
EDITAR:
Eu entendi mal a pergunta. Como os comentários apontam, isso só funciona no caso mais restrito.
Em uma linguagem como C, que armazena dados em ordem de linha maior, simplesmente trate-os como um array 1D de tamanho n * me use uma pesquisa binária.
fonte
Eu tenho uma solução recursiva Divide & Conquer. A ideia básica para uma etapa é: sabemos que o Left-Upper (LU) é o menor e o right-bottom (RB) é o maior número, então o No (N) dado deve: N> = LU e N <= RB
IF N == LU e N == RB :::: Elemento encontrado e abortado retornando a posição / Índice Se N> = LU e N <= RB = FALSE, Não não existe e aborta. Se N> = LU e N <= RB = TRUE, divida a matriz 2D em 4 partes iguais da matriz 2D, cada uma de maneira lógica .. E, em seguida, aplique a mesma etapa de algo a todas as quatro submatrizes.
Meu Algo está correto Eu implementei no PC do meu amigo. Complexidade: cada 4 comparações podem ser usadas para deduzir o número total de elementos a um quarto em seu pior caso. Então, minha complexidade chega a ser 1 + 4 x lg (n) + 4 Mas realmente esperava que funcionasse em O (n)
Acho que algo está errado em algum lugar no meu cálculo de Complexidade, corrija se sim ..
fonte
A solução ideal é começar no canto superior esquerdo, que tem valor mínimo. Mova diagonalmente para baixo para a direita até atingir um elemento cujo valor> = valor do elemento fornecido. Se o valor do elemento for igual ao do elemento fornecido, retorna encontrado como verdadeiro.
Caso contrário, a partir daqui podemos proceder de duas maneiras.
Estratégia 1:
Estratégia 2: Deixe i denotar o índice da linha ej denotar o índice da coluna do elemento diagonal em que paramos. (Aqui, temos i = j, BTW). Seja k = 1.
1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11
fonte
fonte
Eu sugiro, armazene todos os personagens em a
2D list
. em seguida, localize o índice do elemento necessário se ele existir na lista.Se não estiver presente, imprima a mensagem apropriada, caso contrário, imprima a linha e a coluna como:
row = (index/total_columns)
ecolumn = (index%total_columns -1)
Isso incorrerá apenas no tempo de pesquisa binária em uma lista.
Por favor, sugira quaisquer correções. :)
fonte
Se a solução O (M log (N)) estiver ok para uma matriz MxN -
Demonstração de trabalho em C ++.
Por favor, deixe-me saber se isso não funcionaria ou se há um bug.
fonte
Venho fazendo essa pergunta em entrevistas há quase uma década e acho que só uma pessoa foi capaz de criar um algoritmo ideal.
Minha solução sempre foi:
Pesquisa binária na diagonal do meio, que é a diagonal descendo e à direita, contendo o item em
(rows.count/2, columns.count/2)
.Se o número de destino for encontrado, retorna verdadeiro.
Caso contrário, dois números (
u
ev
) terão sido encontrados de forma queu
seja menor do que o destino,v
maior do que o destino ev
um à direita e outro abaixou
.Pesquise recursivamente a submatriz à direita
u
e no topov
e aquela na parte inferioru
e à esquerda dev
.Eu acredito que esta é uma melhoria estrita em relação ao algoritmo fornecido por Nate aqui , uma vez que pesquisar na diagonal geralmente permite uma redução de mais da metade do espaço de pesquisa (se a matriz estiver próxima do quadrado), enquanto pesquisar uma linha ou coluna sempre resulta em uma eliminação exatamente da metade.
Aqui está o código em (provavelmente não terrivelmente Swifty) Swift:
fonte
Dada uma matriz quadrada da seguinte forma:
Sabemos que a <c, d <f, i <k. O que não sabemos é se d <c ou d> c, etc. Temos garantias apenas em 1 dimensão.
Olhando para os elementos finais (c, f, k), podemos fazer uma espécie de filtro: é N <c? search (): next (). Assim, temos n iterações ao longo das linhas, com cada linha tomando O (log (n)) para pesquisa binária ou O (1) se filtrada.
Deixe-me dar um EXEMPLO onde N = j,
Tente novamente com N = q,
Provavelmente existe uma solução melhor, mas isso é fácil de explicar .. :)
fonte
Como esta é uma pergunta de entrevista, parece levar a uma discussão sobre programação paralela e algoritmos de redução de mapa .
Consulte http://code.google.com/intl/de/edu/parallel/mapreduce-tutorial.html
fonte