Generazione di chiavi di ordinamento quando si riordinano gli oggetti


11

Abbiamo un numero di articoli che l'utente finale sarà in grado di organizzare in un ordine desiderato. L'insieme di elementi non è ordinato, ma ogni elemento contiene una chiave di ordinamento che può essere modificata.

Stiamo cercando un algoritmo che consenta di generare una nuova chiave di ordinamento per un elemento che viene aggiunto o spostato in modo che sia il primo, l'ultimo o tra due elementi. Speriamo di dover solo modificare la chiave di ordinamento dell'articolo da spostare.

Un algoritmo di esempio sarebbe avere ogni chiave di ordinamento come un numero in virgola mobile e quando si posiziona un elemento tra due elementi, impostare la chiave di ordinamento come media. Posizionare un oggetto per primo o per ultimo richiederebbe il valore più esterno + - 1.

Il problema qui è che la precisione in virgola mobile potrebbe causare errori nell'ordinamento. L'uso di due numeri interi per rappresentare un numero frazionario potrebbe similmente far sì che i numeri diventino così grandi da non poter essere rappresentati con precisione in normali tipi numerici (ad esempio quando si trasferisce come JSON). Non vorremmo usare BigInts.

Esiste un algoritmo adatto a questo che funzionerebbe, ad esempio, usando le stringhe, che non sarebbero interessate da queste carenze?

Non stiamo cercando di supportare un numero enorme di mosse, ma l'algoritmo sopra descritto potrebbe fallire su un numero in virgola mobile a precisione doppia dopo circa 50 mosse.


Le stringhe sono una scelta ovvia, perché puoi semplicemente continuare ad aggiungere caratteri alla fine per biforcarti. Detto questo, mi sento come se ci fosse un modo migliore per affrontarlo.
Robert Harvey,

Dalla cima della mia testa non vedo come farlo funzionare usando le stringhe senza modificare le chiavi degli altri elementi.
Sampo,

3
Il problema che stai descrivendo si chiama Problema di manutenzione dell'ordine
Nathan Merrill

1
Perché ti preoccupi di non modificare gli altri elementi nell'elenco?
GER,

1
Il modo in cui lo fai funzionare con le stringhe è come questo: A, B, C- A, AA, B, C- A, AA, AB, B, C- A, AA, AAA, AAB, AAC, AB, AC, B, C. Naturalmente, probabilmente vorrai spaziare di più le tue lettere in modo che le stringhe non crescano così rapidamente, ma può essere fatto.
Robert Harvey,

Risposte:


4

Come riepilogo di tutti i commenti e le risposte:

TL; DR - L'uso di numeri in virgola mobile a precisione doppia con l'algoritmo inizialmente proposto dovrebbe essere sufficiente per le esigenze più pratiche (almeno ordinate manualmente). Anche il mantenimento di un elenco ordinato separato degli elementi deve essere considerato. Altre soluzioni chiave di ordinamento sono alquanto ingombranti.

Le due operazioni problematiche sono l'inserimento e la ripetizione ripetuta di elementi all'inizio / fine e l'inserimento o lo spostamento ripetuto di elementi nello stesso punto (ad es. Con tre elementi che spostano ripetutamente il terzo elemento tra i primi due o aggiungono ripetutamente nuovi elementi come secondo elemento).

Da un punto di vista teorico (ovvero consentire un riordino infinito), l'unica soluzione a cui riesco a pensare è l'uso di due numeri interi di dimensioni illimitate come a / b frazionario. Ciò consente una precisione infinita per gli inserti medi, ma i numeri possono diventare sempre più grandi.

Le stringhe potrebbero essere in grado di supportare un gran numero di aggiornamenti (anche se ho ancora problemi a capire l'algoritmo per entrambe le operazioni), ma non infinito, perché non è possibile aggiungere infinitamente molti nella prima posizione (almeno usando un ordinamento di stringhe regolare confronto).

I numeri interi richiedono la scelta di una spaziatura iniziale per le chiavi di ordinamento, che limita il numero di inserti intermedi che è possibile eseguire. Se inizialmente si distanziano le chiavi di ordinamento 1024, è possibile eseguire solo 10 inserti intermedi nel caso peggiore prima di avere numeri adiacenti. La scelta di una spaziatura iniziale maggiore limita il numero di inserimenti primo / ultimo che è possibile eseguire. Utilizzando un numero intero a 64 bit, si è limitati a ~ 63 operazioni in entrambi i modi, che è necessario dividere a metà tra inserti medi e inserti primo / ultimo.

L'uso di valori in virgola mobile elimina la necessità di selezionare la spaziatura a priori. L'algoritmo è semplice:

  1. Il primo elemento inserito ha una chiave di ordinamento 0.0
  2. Un elemento inserito (o spostato) per primo o per ultimo ha la chiave di ordinamento del primo elemento - 1,0 o ultimo elemento + 1,0, rispettivamente.
  3. Un elemento inserito (o spostato) tra due elementi ha una chiave di ordinamento uguale alla media dei due.

L'uso di un galleggiante a doppia precisione consente 52 inserti medi nel caso peggiore ed effettivamente infiniti (circa 1e15) primi / ultimi inserti. In pratica, quando si spostano oggetti all'interno dell'algoritmo, si dovrebbe auto-correggere, poiché ogni volta che si sposta un oggetto per primo o per ultimo si espande l'intervallo che può essere utilizzato.

I galleggianti a doppia precisione hanno anche il vantaggio di essere supportati da tutte le piattaforme e di essere facilmente archiviati e trasportati praticamente da tutti i formati e librerie di trasporto. Questo è quello che abbiamo finito per usare.


1

Ho scritto una soluzione in TypeScript basata sul riassunto di @ Sampo. Il codice può essere trovato sotto.

Un paio di approfondimenti acquisiti lungo la strada.

  • Solo l' inserimento nel mezzo tra due chiavi di ordinamento esistenti deve generare una nuova chiave di ordinamento, lo scambio (ovvero la riorganizzazione) non provoca suddivisioni (ovvero nuovi punti intermedi). Se sposti due elementi e ne tocchi solo uno, perdi informazioni su quali due elementi hanno cambiato posizione nell'elenco. Anche se era un requisito per cominciare, nota che è una buona idea

  • Ogni 1074: divisione del punto medio è necessario normalizzare l'intervallo in virgola mobile. Lo rileviamo semplicemente controllando se il nuovo punto medio soddisfa l'invariante

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

  • Il ridimensionamento non ha importanza, poiché i tasti di ordinamento sono normalizzati, la normalizzazione avviene comunque ogni 1074divisione del punto medio. La situazione non migliorerebbe se inizialmente distribuissimo i numeri in modo più ampio.

  • La normalizzazione della chiave di ordinamento è incredibilmente rara. Ammortizzerai questo costo al punto in cui la normalizzazione non è evidente. Tuttavia, starei attento con questo approccio se hai più di mille elementi.


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;
    })
  );
}

0

Ci sono stato, fatto ciò, potrebbe essere necessario farlo di nuovo. Utilizzare una stringa come chiave di ordinamento, quindi è sempre possibile trovare una chiave compresa tra due chiavi specificate. Se le stringhe diventano troppo lunghe per i tuoi gusti, devi modificare più o tutti i tasti di ordinamento.


1
Tuttavia, non è sempre possibile trovare una chiave prima di un'altra chiave di stringa.
Sampo,

-1

Utilizzare numeri interi e impostare la chiave di ordinamento per l'elenco iniziale su 500 * numero articolo. Quando si inserisce tra gli elementi è possibile utilizzare la media. Ciò consentirà di iniziare molti inserimenti


2
Questo è in realtà peggio che usare un galleggiante. Una spaziatura iniziale di 500 consente solo 8-9 inserimenti di punti medi (2 ^ 9 = 512), mentre un doppio float consente circa 50, senza problemi di selezione iniziale di una spaziatura.
Sampo,

Usa uno spazio di 500 e galleggia!
Rob Mulder,

Quando si utilizza un float, il gap non fa alcuna differenza, poiché il fattore limitante per gli inserimenti intermedi è il numero di bit nel significato. Ecco perché ho proposto il gap predefinito di 1.0 quando si usano i float.
Sampo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.