Recientemente me dieron esta pregunta de la entrevista y tengo curiosidad por saber cuál sería una buena solución.
Digamos que me dan una matriz 2d donde todos los números de la matriz están en orden creciente de izquierda a derecha y de arriba a abajo.
¿Cuál es la mejor manera de buscar y determinar si un número objetivo está en la matriz?
Ahora, mi primera inclinación es utilizar una búsqueda binaria ya que mis datos están ordenados. Puedo determinar si un número está en una sola fila en el tiempo O (log N). Sin embargo, son las 2 direcciones las que me desvían.
Otra solución que pensé que podría funcionar es comenzar en algún punto intermedio. Si el valor medio es menor que mi objetivo, entonces puedo estar seguro de que está en el cuadrado izquierdo de la matriz desde el medio. Luego me muevo en diagonal y vuelvo a comprobar, reduciendo el tamaño del cuadrado en el que podría estar el objetivo hasta que haya afinado el número objetivo.
¿Alguien tiene buenas ideas para resolver este problema?
Matriz de ejemplo:
Ordenado de izquierda a derecha, de arriba a abajo.
1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11
[[1 1][1 1]]
:?Respuestas:
He aquí un enfoque simple:
Para una
NxM
matriz, esto se ejecuta enO(N+M)
. Creo que sería difícil hacerlo mejor. :)Editar: Mucha buena discusión. Estaba hablando del caso general anterior; Claramente, si
N
oM
son pequeños, podría usar un enfoque de búsqueda binaria para hacer esto en algo cercano al tiempo logarítmico.Aquí hay algunos detalles, para aquellos que tengan curiosidad:
Historia
Este simple algoritmo se llama búsqueda de Saddleback . Ha existido por un tiempo, y es óptimo cuando
N == M
. Algunas referencias:Sin embargo, cuando
N < M
, la intuición sugiere que la búsqueda binaria debería ser mejor queO(N+M)
: Por ejemplo, cuandoN == 1
, una búsqueda binaria pura se ejecutará en tiempo logarítmico en lugar de lineal.En el peor de los casos
Richard Bird examinó esta intuición de que la búsqueda binaria podría mejorar el algoritmo Saddleback en un artículo de 2006:
Usando una técnica de conversación bastante inusual, Bird nos muestra que
N <= M
este problema tiene un límite inferior deΩ(N * log(M/N))
. Este límite tiene sentido, ya que nos da un rendimiento lineal cuandoN == M
y un rendimiento logarítmico cuandoN == 1
.Algoritmos para matrices rectangulares
Un enfoque que utiliza una búsqueda binaria fila por fila se ve así:
N < M
. Digamos queN
son filas yM
columnas.value
. Si lo encontramos, habremos terminado.s
yg
, dóndes < value < g
.s
es menor quevalue
, por lo que podemos eliminarlo.g
es mayor quevalue
, por lo que podemos eliminarlo.En términos de complejidad en el peor de los casos, este algoritmo
log(M)
funciona para eliminar la mitad de las posibles soluciones y luego se llama a sí mismo de forma recursiva dos veces en dos problemas más pequeños. Tenemos que repetir una versión más pequeña de eselog(M)
trabajo para cada fila, pero si la cantidad de filas es pequeña en comparación con la cantidad de columnas, entonces comenzar a ser útil eliminar todas esas columnas en tiempo logarítmico .Esto le da al algoritmo una complejidad de la
T(N,M) = log(M) + 2 * T(M/2, N/2)
que Bird muestraO(N * log(M/N))
.Otro enfoque publicado por Craig Gidney describe un algoritmo similar al enfoque anterior: examina una fila a la vez utilizando un tamaño de paso de
M/N
. Su análisis muestra que esto también se traduce enO(N * log(M/N))
rendimiento.Comparación de rendimiento
El análisis Big-O está muy bien, pero ¿qué tan bien funcionan estos enfoques en la práctica? El siguiente cuadro examina cuatro algoritmos para matrices cada vez más "cuadradas":
(El algoritmo "ingenuo" simplemente busca en todos los elementos de la matriz. El algoritmo "recursivo" se describe anteriormente. El algoritmo "híbrido" es una implementación del algoritmo de Gidney . Para cada tamaño de matriz, el rendimiento se midió cronometrando cada algoritmo sobre un conjunto fijo de 1.000.000 de matrices generadas aleatoriamente).
Algunos puntos notables:
Resumen
El uso inteligente de la búsqueda binaria puede proporcionar
O(N * log(M/N)
rendimiento tanto para matrices rectangulares como cuadradas. ElO(N + M)
algoritmo "saddleback" es mucho más simple, pero sufre una degradación del rendimiento a medida que las matrices se vuelven cada vez más rectangulares.fuente
M==N
queremosO(N)
complejidad, noO(N*log(N/N))
ya que esta última es cero. Un límite agudo "unificado" correcto esO(N*(log(M/N)+1))
cuandoN<=M
.Este problema lleva
Θ(b lg(t))
tiempo, dóndeb = min(w,h)
yt=b/max(w,h)
. Discuto la solución en esta publicación de blog .Límite inferior
Un adversario puede obligar a un algoritmo a realizar
Ω(b lg(t))
consultas restringiéndose a la diagonal principal:Leyenda: las celdas blancas son elementos más pequeños, las celdas grises son elementos más grandes, las celdas amarillas son elementos más pequeños o iguales y las celdas naranjas son elementos más grandes o iguales. El adversario obliga a que la solución sea la celda amarilla o naranja que dure el algoritmo.
Tenga en cuenta que hay
b
listas de tamaño ordenadas independientest
, que requieren que lasΩ(b lg(t))
consultas se eliminen por completo.Algoritmo
w >= h
)t
a la izquierda de la esquina superior derecha del área válidat
celdas en la fila con una búsqueda binaria. Si se encuentra un elemento coincidente al hacer esto, regrese con su posición.t
columnas cortas.Encontrar un artículo:
Determinar que un artículo no existe:
Leyenda: las celdas blancas son elementos más pequeños, las celdas grises son elementos más grandes y la celda verde es un elemento igual.
Análisis
Hay
b*t
columnas cortas para eliminar. Hayb
largas filas para eliminar. Eliminar una fila larga cuestaO(lg(t))
tiempo. Eliminandot
los costos de columnas cortasO(1)
tiempo.En el peor de los casos, tendremos que eliminar cada columna y cada fila, tomando tiempo
O(lg(t)*b + b*t*1/t) = O(b lg(t))
.Tenga en cuenta que supongo que se
lg
sujeta a un resultado superior a 1 (es decirlg(x) = log_2(max(2,x))
). Es por eso que cuandow=h
, es decirt=1
, obtenemos el límite esperado deO(b lg(1)) = O(b) = O(w+h)
.Código
fuente
O(b*(lg(t)+1))
lugar deO(b*lg(t))
. Buen artículo, especialmente. por llamar la atención sobre la "técnica del adversario" al mostrar un límite en el "peor caso".Usaría la estrategia de divide y vencerás para este problema, similar a lo que sugieres, pero los detalles son un poco diferentes.
Esta será una búsqueda recursiva en subrangos de la matriz.
En cada paso, elija un elemento en el medio del rango. Si el valor encontrado es el que busca, entonces ha terminado.
De lo contrario, si el valor encontrado es menor que el valor que está buscando, entonces sabrá que no está en el cuadrante superior ni a la izquierda de su posición actual. Por lo tanto, busque de forma recursiva los dos subrangos: todo (exclusivamente) debajo de la posición actual, y todo (exclusivamente) a la derecha que esté en la posición actual o por encima de ella.
De lo contrario (el valor encontrado es mayor que el valor que está buscando) sabrá que no está en el cuadrante de abajo y a la derecha de su posición actual. Así que busque recursivamente los dos subrangos: todo (exclusivamente) a la izquierda de la posición actual, y todo (exclusivamente) arriba de la posición actual que está en la columna actual o una columna a la derecha.
Y ba-da-bing, lo encontraste.
Tenga en cuenta que cada llamada recursiva solo se ocupa del subrango actual, no (por ejemplo) TODAS las filas por encima de la posición actual. Solo aquellos en el subrango actual.
Aquí hay un pseudocódigo para ti:
fuente
Las dos respuestas principales dadas hasta ahora parecen ser el
O(log N)
"método ZigZag" y elO(N+M)
método de búsqueda binaria. Pensé en hacer algunas pruebas comparando los dos métodos con varias configuraciones. Aquí están los detalles:La matriz es N x N cuadrados en cada prueba, con N que varía de 125 a 8000 (el mayor montón que mi JVM podría manejar). Para cada tamaño de matriz, elegí un lugar aleatorio en la matriz para poner un archivo
2
. Luego puse un en3
todas partes posibles (a la derecha y debajo del 2) y luego llené el resto de la matriz con1
. Algunos de los comentaristas anteriores parecían pensar que este tipo de configuración produciría el peor tiempo de ejecución para ambos algoritmos. Para cada tamaño de matriz, elegí 100 ubicaciones aleatorias diferentes para el 2 (objetivo de búsqueda) y ejecuté la prueba. Registré el tiempo de ejecución promedio y el tiempo de ejecución en el peor de los casos para cada algoritmo. Debido a que estaba sucediendo demasiado rápido para obtener buenas lecturas de ms en Java, y porque no confío en nanoTime () de Java, repetí cada prueba 1000 veces solo para agregar un factor de sesgo uniforme a todas las veces. Aquí están los resultados:ZigZag venció al binario en cada prueba tanto para el tiempo promedio como para el peor de los casos, sin embargo, todos están dentro de un orden de magnitud entre sí más o menos.
Aquí está el código de Java:
fuente
Esta es una prueba breve del límite inferior del problema.
No puede hacerlo mejor que el tiempo lineal (en términos de dimensiones de la matriz, no del número de elementos). En la siguiente matriz, cada uno de los elementos marcados como
*
puede ser 5 o 6 (independientemente de los demás). Entonces, si su valor objetivo es 6 (o 5), el algoritmo debe examinarlos todos.Por supuesto, esto también se expande a arreglos más grandes. Esto significa que esta respuesta es óptima.
Actualización: como señaló Jeffrey L Whitledge, solo es óptimo como límite inferior asintótico en el tiempo de ejecución frente al tamaño de los datos de entrada (tratado como una sola variable). Se puede mejorar el tiempo de ejecución tratado como una función de dos variables en ambas dimensiones de la matriz.
fuente
Creo que aquí está la respuesta y funciona para cualquier tipo de matriz ordenada.
fuente
Interesante pregunta. Considere esta idea: cree un límite donde todos los números sean mayores que su objetivo y otro donde todos los números sean menores que su objetivo. Si queda algo entre los dos, ese es tu objetivo.
Si estoy buscando 3 en su ejemplo, leo en la primera fila hasta que llego a 4, luego busco el número adyacente más pequeño (incluidas las diagonales) mayor que 3:
1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11
Ahora hago lo mismo para esos números menores a 3:
1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11
Ahora pregunto, ¿hay algo dentro de los dos límites? Si es así, debe ser 3. Si no, entonces no hay 3. Algo indirecto ya que no encuentro el número, simplemente deduzco que debe estar allí. Esto tiene la ventaja adicional de contar TODOS los 3.
Probé esto en algunos ejemplos y parece funcionar bien.
fuente
La búsqueda binaria a través de la diagonal de la matriz es la mejor opción. Podemos averiguar si el elemento es menor o igual que los elementos de la diagonal.
fuente
A. Haga una búsqueda binaria en aquellas líneas donde podría estar el número objetivo.
B. Hágalo un gráfico: busque el número tomando siempre el nodo vecino no visitado más pequeño y retrocediendo cuando se encuentre un número demasiado grande
fuente
La búsqueda binaria sería el mejor enfoque, en mi opinión. A partir de 1/2 x, 1/2 y lo cortará por la mitad. Es decir, un cuadrado de 5x5 sería algo así como x == 2 / y == 3. Redondeé un valor hacia abajo y un valor hacia arriba para mejorar la zona en la dirección del valor objetivo.
Para mayor claridad, la siguiente iteración le daría algo como x == 1 / y == 2 O x == 3 / y == 5
fuente
Bueno, para empezar, supongamos que estamos usando un cuadrado.
1. Buscando un cuadrado
Usaría una búsqueda binaria en diagonal. El objetivo es localizar el número más pequeño que no sea estrictamente más bajo que el número objetivo.
Digamos que estoy buscando, por
4
ejemplo, luego terminaría localizando5
en(2,2)
.Entonces, tengo la seguridad de que si
4
está en la mesa, está en una posición(x,2)
o(2,x)
conx
adentro[0,2]
. Bueno, son solo 2 búsquedas binarias.La complejidad no es abrumadora:
O(log(N))
(3 búsquedas binarias en rangos de longitudN
)2. Buscando un rectángulo, enfoque ingenuo
Por supuesto, se vuelve un poco más complicado cuando
N
yM
difieren (con un rectángulo), considere este caso degenerado:Y digamos que estoy buscando
9
... El enfoque diagonal sigue siendo bueno, pero la definición de diagonal cambia. Aquí está mi diagonal[1, (5 or 6), 17]
. Digamos que recogí[1,5,17]
, entonces sé que si9
está en la tabla, está en la subparte:Esto nos da 2 rectángulos:
¡Entonces podemos recurrir! probablemente comenzando por el que tiene menos elementos (aunque en este caso nos mata).
Debo señalar que si una de las dimensiones es menor que
3
, no podemos aplicar los métodos diagonales y debemos usar una búsqueda binaria. Aquí significaría:10 11 12 13 14 15 16
, no encontrada5 6 7 8
, no encontrada6 7 8 9
, no encontradaEs complicado porque para obtener un buen rendimiento, es posible que desee diferenciar entre varios casos, según la forma general ...
3. Buscando un rectángulo, enfoque brutal
Sería mucho más fácil si nos ocupamos de un cuadrado ... así que vamos a arreglar las cosas.
Ahora tenemos un cuadrado.
Por supuesto, probablemente NO crearemos esas filas, simplemente podríamos emularlas.
por lo que se comporta como un cuadrado sin ocupar más memoria (a costa de la velocidad, probablemente, dependiendo de la caché ... bueno: p)
fuente
EDITAR:
Entendí mal la pregunta. Como señalan los comentarios, esto solo funciona en el caso más restringido.
En un lenguaje como C que almacena datos en orden de fila principal, simplemente trátelo como una matriz 1D de tamaño n * my use una búsqueda binaria.
fuente
Tengo una solución recursiva Divide & Conquer. La idea básica para un paso es: sabemos que la parte superior izquierda (LU) es la más pequeña y la parte inferior derecha (RB) es el número más grande, por lo que el No (N) dado debe: N> = LU y N <= RB
IF N == LU y N == RB :::: Elemento encontrado y Abortar devolviendo la posición / Índice Si N> = LU y N <= RB = FALSE, No no está allí y aborta. Si N> = LU y N <= RB = TRUE, divida la matriz 2D en 4 partes iguales de la matriz 2D cada una de manera lógica. Y luego aplique el mismo paso de algoritmo a las cuatro submatriz.
Mi algoritmo es correcto lo he implementado en la PC de mis amigos. Complejidad: cada 4 comparaciones se pueden usar para deducir el número total de elementos a un cuarto en el peor de los casos. Entonces, mi complejidad llega a ser 1 + 4 x lg (n) + 4 Pero realmente esperaba que esto funcionara en O (norte)
Creo que algo está mal en algún lugar de mi cálculo de Complejidad, corríjalo si es así.
fuente
La solución óptima es comenzar en la esquina superior izquierda, que tiene un valor mínimo. Muévase en diagonal hacia abajo a la derecha hasta que golpee un elemento cuyo valor> = valor del elemento dado. Si el valor del elemento es igual al del elemento dado, devuelve encontrado como verdadero.
De lo contrario, desde aquí podemos proceder de dos formas.
Estrategia 1:
Estrategia 2: Sea i el índice de fila y j el índice de columna del elemento diagonal en el que nos hemos detenido. (Aquí tenemos i = j, por cierto). Sea k = 1.
1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11
fuente
fuente
Sugiero que almacene todos los caracteres en un archivo
2D list
. luego busque el índice del elemento requerido si existe en la lista.Si no está presente, imprima el mensaje apropiado; de lo contrario, imprima la fila y la columna como:
row = (index/total_columns)
ycolumn = (index%total_columns -1)
Esto incurrirá solo en el tiempo de búsqueda binaria en una lista.
Sugiera correcciones. :)
fuente
Si la solución O (M log (N)) está bien para una matriz MxN -
Demostración funcional de C ++.
Por favor, avíseme si esto no funcionaría o si hay un error.
fuente
He estado haciendo esta pregunta en entrevistas durante la mayor parte de una década y creo que solo una persona ha podido encontrar un algoritmo óptimo.
Mi solución siempre ha sido:
Búsqueda binaria en la diagonal media, que es la diagonal que corre hacia abajo y hacia la derecha, que contiene el elemento en
(rows.count/2, columns.count/2)
.Si se encuentra el número objetivo, devuelve verdadero.
De lo contrario, se habrán encontrado dos números (
u
yv
) de manera queu
sea más pequeño que el objetivo,v
sea más grande que el objetivo yv
esté uno a la derecha y otro hacia abajou
.Busque de forma recursiva la submatriz a la derecha
u
y la parte superior dev
y la que está al finalu
y a la izquierda dev
.Creo que esta es una mejora estricta sobre el algoritmo dado por Nate aquí , ya que buscar en la diagonal a menudo permite una reducción de más de la mitad del espacio de búsqueda (si la matriz está cerca del cuadrado), mientras que buscar una fila o columna siempre resulta en una eliminación de exactamente la mitad.
Aquí está el código en (probablemente no terriblemente Swifty) Swift:
fuente
Dada una matriz cuadrada de la siguiente manera:
Sabemos que a <c, d <f, i <k. Lo que no sabemos es si d <c o d> c, etc. Tenemos garantías solo en 1 dimensión.
Mirando los elementos finales (c, f, k), podemos hacer una especie de filtro: ¿N <c? buscar (): siguiente (). Por lo tanto, tenemos n iteraciones sobre las filas, y cada fila toma O (log (n)) para la búsqueda binaria u O (1) si se filtra.
Déjame dar un EJEMPLO donde N = j,
Inténtelo de nuevo con N = q,
Probablemente haya una solución mejor, pero es fácil de explicar ... :)
fuente
Como se trata de una pregunta de entrevista, parece conducir a una discusión sobre la programación paralela y Map-reduce algoritmos de .
Ver http://code.google.com/intl/de/edu/parallel/mapreduce-tutorial.html
fuente