Generar claves de clasificación al reordenar elementos

11

Tenemos una serie de elementos que el usuario final podrá organizar en el orden deseado. El conjunto de elementos no está ordenado, pero cada elemento contiene una clave de clasificación que se puede modificar.

Estamos buscando un algoritmo que permita generar una nueva clave de clasificación para un elemento que se agrega o mueve para ser el primer elemento, el último elemento o entre dos elementos. Esperamos tener que modificar la clave de clasificación del elemento que se está moviendo.

Un algoritmo de ejemplo sería hacer que cada clave de clasificación sea un número de coma flotante, y cuando coloque un elemento entre dos elementos, configure la clave de clasificación como su promedio. Colocar un elemento primero o último tomaría el valor más externo + - 1.

El problema aquí es que la precisión de coma flotante puede hacer que falle la clasificación. El uso de dos enteros para representar un número fraccionario podría hacer que los números se vuelvan tan grandes que no se puedan representar con precisión en tipos numéricos regulares (por ejemplo, cuando se transfieren como JSON). No nos gustaría usar BigInts.

¿Existe un algoritmo adecuado para esto que funcione, por ejemplo, usando cadenas, que no se verían afectadas por estas deficiencias?

No buscamos admitir grandes cantidades de movimientos, pero el algoritmo descrito anteriormente podría fallar en un número de coma flotante de doble precisión después de aproximadamente 50 movimientos.

Sampo
fuente
Las cadenas son una opción obvia, porque puedes seguir agregando caracteres al final de ellas para bifurcar. Dicho esto, siento que hay una mejor manera de abordar esto.
Robert Harvey
Desde lo alto de mi cabeza, no veo cómo hacerlo funcionar usando cadenas sin modificar las teclas de otros elementos.
Sampo
3
El problema que está describiendo se llama el Problema de mantenimiento de pedidos
Nathan Merrill
1
¿Por qué le preocupa no modificar los otros elementos de la lista?
GER
1
La forma en que lo hace funcionar con cadenas es así: A, B, C- A, AA, B, C- A, AA, AB, B, C- A, AA, AAA, AAB, AAC, AB, AC, B, C. Por supuesto, es probable que desee espaciar más sus letras para que las cadenas no crezcan tan rápido, pero se puede hacer.
Robert Harvey

Respuestas:

4

Como resumen de todos los comentarios y respuestas:

TL; DR : el uso de números de coma flotante de doble precisión con el algoritmo propuesto inicialmente debería ser suficiente para las necesidades más prácticas (al menos ordenadas manualmente). También se debe considerar mantener una lista ordenada separada de los elementos. Otras soluciones clave de clasificación son algo engorrosas.

Las dos operaciones problemáticas son insertar elementos al principio / final una y otra vez, e insertar o mover elementos repetidamente al mismo lugar (por ejemplo, con tres elementos moviendo repetidamente el tercer elemento entre los dos primeros, o agregando elementos nuevos repetidamente como el segundo elemento).

Desde un punto de vista teórico (es decir, permitiendo una reordenación infinita), la única solución que se me ocurre es usar dos enteros de tamaño ilimitado como a / b fraccional. Esto permite una precisión infinita para las inserciones medias, pero los números pueden ser cada vez más grandes.

Las cadenas pueden ser compatibles con una gran cantidad de actualizaciones (aunque todavía tengo problemas para descifrar el algoritmo para ambas operaciones), pero no infinito, porque no se pueden agregar infinitamente muchas en la primera posición (al menos usando una ordenación de cadena normal) comparación).

Los enteros requerirían elegir un espacio inicial para las teclas de clasificación, lo que limita la cantidad de inserciones medias que puede realizar. Si inicialmente separa las claves de clasificación 1024, solo puede realizar 10 inserciones medias en el peor de los casos antes de tener números adyacentes. Elegir un espacio inicial más grande limita la cantidad de inserciones primeras / últimas que puede realizar. Usando un número entero de 64 bits, está limitado a ~ 63 operaciones de cualquier manera, que necesita dividir entre las inserciones intermedias y las inserciones primera / última a priori.

El uso de valores de punto flotante elimina la necesidad de seleccionar el espaciado a priori. El algoritmo es simple:

  1. El primer elemento insertado tiene una clave de clasificación 0.0
  2. Un elemento insertado (o movido) primero o último tiene la clave de clasificación del primer elemento: 1.0 o último elemento + 1.0, respectivamente.
  3. Un elemento insertado (o movido) entre dos elementos tiene una clave de clasificación igual al promedio de los dos.

El uso de un flotador de doble precisión permite 52 insertos intermedios en el peor de los casos y, de manera efectiva, infinitos (aproximadamente 1e15) primeros / últimos insertos. En la práctica, al mover elementos alrededor del algoritmo debe autocorregirse, ya que cada vez que mueve un elemento primero o último, se amplía el rango que se puede utilizar.

Los flotadores de doble precisión también tienen la ventaja de que son compatibles con todas las plataformas y se almacenan y transportan fácilmente en prácticamente todos los formatos y bibliotecas de transporte. Esto fue lo que terminamos usando.

Sampo
fuente
1

Escribí una solución en TypeScript basada en el resumen de @ Sampo. El código se puede encontrar a continuación.

Un par de ideas adquiridas en el camino.

  • Solo la inserción en el medio entre dos claves de clasificación existentes debe generar una nueva clave de clasificación, el intercambio (es decir, la reorganización) no causa divisiones (es decir, nuevos puntos medios). Si mueve dos elementos y solo toca uno de ellos, está perdiendo información sobre qué dos elementos cambiaron de posición en la lista. Incluso si para empezar era un requisito, tenga en cuenta que es una buena idea.

  • Cada 1074: división del punto medio necesitamos normalizar el rango de coma flotante. Detectamos esto simplemente verificando si el nuevo punto medio satisface el invariante

    a.sortKey < m && m < b.sortKey

  • El escalado no importa, ya que las teclas de clasificación están normalizadas, la normalización todavía ocurre cada 1074división de punto medio. La situación no mejoraría si distribuimos los números más ampliamente para empezar.

  • La normalización de la clave de clasificación es increíblemente rara. Usted amortizará este costo hasta el punto en que la normalización no sea notable. Sin embargo, sería cuidadoso con este enfoque si tiene más de 1000 elementos.


export interface HasSortKey {
  sortKey: number;
}

function normalizeList<T extends HasSortKey>(list: Array<T>) {
  const normalized = new Array<T>(list.length);
  for (let i = 0; i < list.length; i++) {
    normalized[i] = { ...list[i], sortKey: i };
  }
  return normalized;
}

function insertItem<T extends HasSortKey>(
  list: Array<T>,
  index: number,
  item: Partial<T>
): Array<T> {
  if (list.length === 0) {
    list.push({ ...item, sortKey: 0 } as T);
  } else {
    // list is non-empty

    if (index === 0) {
      list.splice(0, 0, { ...item, sortKey: list[0].sortKey - 1 } as T);
    } else if (index < list.length) {
      // midpoint, index is non-zero and less than length

      const a = list[index - 1];
      const b = list[index];

      const m = (a.sortKey + b.sortKey) / 2;

      if (!(a.sortKey < m && m < b.sortKey)) {
        return insertItem(normalizeList(list), index, item);
      }

      list.splice(index, 0, { ...item, sortKey: m } as T);
    } else if (index === list.length) {
      list.push({ ...item, sortKey: list[list.length - 1].sortKey + 1 } as T);
    }
  }
  return list;
}

export function main() {
  const normalized: Array<number> = [];

  let list: Array<{ n: number } & HasSortKey> = [];

  list = insertItem(list, 0, { n: 0 });

  for (let n = 1; n < 10 * 1000; n++) {
    const list2 = insertItem(list, 1, { n });
    if (list2 !== list) {
      normalized.push(n);
    }
    list = list2;
  }

  let m = normalized[0];

  console.log(
    normalized.slice(1).map(n => {
      const k = n - m;
      m = n;
      return k;
    })
  );
}
John Leidegren
fuente
0

He estado allí, hecho eso, puede que tenga que hacer eso de nuevo. Use una cadena como clave de clasificación, siempre puede encontrar una clave que esté entre dos claves dadas. Si las cadenas se alargan demasiado para su gusto, debe modificar varias o todas las claves de clasificación.

gnasher729
fuente
1
Sin embargo, no siempre puede encontrar una clave que esté antes de otra clave de cadena.
Sampo
-1

Use números enteros y configure la clave de clasificación para la lista inicial en 500 * número de artículo. Al insertar entre elementos, puede usar el promedio. Esto permitirá muchas inserciones para comenzar

Rob Mulder
fuente
2
Esto es realmente peor que usar un flotador. Un espaciado inicial de 500 permite solo 8-9 inserciones de punto medio (2 ^ 9 = 512), mientras que un doble flotador permite aproximadamente 50, sin problema de seleccionar inicialmente un espaciado.
Sampo
¡Utiliza un hueco de 500 y flotadores!
Rob Mulder
Cuando se usa un flotador, el espacio no hace diferencia, ya que el factor limitante para las inserciones medias es el número de bits en el significado. Es por eso que propuse la brecha predeterminada de 1.0 al usar flotantes.
Sampo