Генерация ключей сортировки при изменении порядка элементов

11

У нас есть ряд предметов, которые конечный пользователь сможет организовать в желаемом порядке. Набор элементов неупорядочен, но каждый элемент содержит ключ сортировки, который можно изменить.

Мы ищем алгоритм, который позволил бы генерировать новый ключ сортировки для элемента, который добавляется или перемещается в качестве первого элемента, последнего элемента или между любыми двумя элементами. Мы надеемся только изменить ключ сортировки перемещаемого элемента.

В качестве примера алгоритма каждый ключ сортировки должен иметь число с плавающей запятой, а при размещении элемента между двумя элементами установите ключ сортировки равным их среднему значению. Размещение элемента первым или последним будет иметь крайнее значение + - 1.

Проблема здесь в том, что точность с плавающей точкой может привести к сбою сортировки. Использование двух целых чисел для представления дробного числа может также привести к тому, что числа станут настолько большими, что их невозможно будет точно представить в обычных числовых типах (например, при передаче в формате JSON). Мы бы не хотели использовать BigInts.

Существует ли подходящий для этого алгоритм, который бы работал, например, с использованием строк, на которые эти недостатки не влияли бы?

Мы не стремимся поддерживать огромное количество ходов, но описанный выше алгоритм может дать сбой для числа с плавающей запятой двойной точности примерно через 50 ходов.

Sampo
источник
Строки являются очевидным выбором, потому что вы можете просто добавлять символы в конце, чтобы разделить их. Тем не менее, я чувствую, что есть лучший способ приблизиться к этому.
Роберт Харви
Из головы не видно, как заставить его работать, используя строки, не изменяя ключи других элементов.
Сампо
3
Проблема, которую вы описываете, называется проблемой обслуживания заказов
Натан Меррил
1
Почему вы не хотите изменять другие элементы в списке?
GER
1
Как заставить его работать со строками, это так: A, B, C- A, AA, B, C- A, AA, AB, B, C- A, AA, AAA, AAB, AAC, AB, AC, B, C. Конечно, вы, вероятно, захотите разместить буквы больше, чтобы строки не росли так быстро, но это можно сделать.
Роберт Харви

Ответы:

4

Как резюме всех комментариев и ответов:

TL; DR - Использование чисел с плавающей запятой двойной точности с первоначально предложенным алгоритмом должно быть достаточно для большинства практических (по крайней мере, вручную упорядоченных) нужд. Ведение отдельного упорядоченного списка элементов следует также рассмотреть. Другие ключевые решения несколько громоздки.

Двумя проблемными операциями являются вставка элементов в начале / конце снова и снова и многократная вставка или перемещение элементов в одну и ту же точку (например, с помощью трех элементов, многократно перемещающих третий элемент между первыми двумя, или повторное добавление новых элементов в качестве второй. элемент).

С теоретической точки зрения (т. Е. Допускается бесконечное переупорядочение), единственное решение, о котором я могу подумать, это использование двух целых чисел неограниченного размера в виде дробного числа a / b. Это допускает бесконечную точность для средних вставок, но числа могут становиться все более большими.

Строки могут поддерживать большое количество обновлений (хотя у меня все еще возникают проблемы с выяснением алгоритма для обеих операций), но не бесконечно, потому что вы не можете добавлять бесконечно много в первой позиции (по крайней мере, используя обычную сортировку строк). сравнение).

Для целых чисел потребуется выбрать начальный интервал для ключей сортировки, который ограничивает количество промежуточных вставок, которые вы можете выполнить. Если вы изначально разнесете ключи сортировки на 1024, вы можете выполнить только 10 промежуточных вставок в худшем случае до того, как у вас появятся соседние числа. Выбор большего начального интервала ограничивает количество первых / последних вставок, которые вы можете выполнить. Используя 64-битное целое число, вы ограничиваетесь ~ 63 операциями в любом случае, которые вам нужно разделить между промежуточными вставками и первыми / последними вставками априори.

Использование значений с плавающей точкой устраняет необходимость выбора пробелов априори. Алгоритм прост:

  1. Первый вставленный элемент имеет ключ сортировки 0.0
  2. Элемент, вставленный (или перемещенный) первым или последним, имеет ключ сортировки первого элемента - 1,0 или последний элемент + 1,0, соответственно.
  3. Элемент, вставленный (или перемещенный) между двумя элементами, имеет ключ сортировки, равный среднему из двух.

Использование плавающего типа с двойной точностью позволяет 52 промежуточных вставки в наихудшем случае и практически бесконечные (около 1e15) первые / последние вставки. На практике при перемещении элементов по всему алгоритму он должен самокорректироваться, так как каждый раз, когда вы перемещаете элемент первым или последним, он расширяет диапазон, который можно использовать.

Преимущество поплавков двойной точности также заключается в том, что они поддерживаются всеми платформами и легко сохраняются и транспортируются практически всеми транспортными форматами и библиотеками. Это было то, что мы в конечном итоге использовали.

Sampo
источник
1

Я написал решение в TypeScript на основе резюме @ Sampo. Код можно найти ниже.

Пара идей, полученных по пути.

  • Только вставка в середину между двумя существующими ключами сортировки должна генерировать новый ключ сортировки, замена (то есть перестановка) не вызывает расщепления (то есть новые средние точки). Если вы перемещаете два элемента и касаетесь только одного из них, вы теряете информацию о том, какие два элемента изменили положение в списке. Даже если это было требование для начала, обратите внимание, что это хорошая идея

  • Каждое 1074-е деление средней точки нам нужно нормализовать диапазон с плавающей точкой. Мы обнаруживаем это, просто проверяя, удовлетворяет ли новая средняя точка инварианту

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

  • Масштабирование не имеет значения, поскольку ключи сортировки нормализованы, нормализация по-прежнему происходит при каждом 1074разделении средней точки. Ситуация не улучшится, если мы начнем распределять цифры более широко.

  • Нормализация сортировки ключей невероятно редка. Вы амортизируете эту стоимость до такой степени, что нормализация не будет заметна. Тем не менее, я был бы осторожен с этим подходом, если у вас есть более 1000 элементов.


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

Там, сделал это, возможно, придется сделать это снова. Используйте строку в качестве ключа сортировки, тогда вы всегда можете найти ключ, который находится между двумя заданными ключами. Если строки становятся слишком длинными на ваш вкус, вы должны изменить несколько или все ключи сортировки.

gnasher729
источник
1
Вы не всегда можете найти ключ перед другим ключом строки, хотя.
Сампо
-1

Используйте целые числа и установите ключ сортировки для начального списка равным 500 * номеру элемента. При вставке между предметами вы можете использовать среднее. Это позволит много вставок для начала

Роб Малдер
источник
2
Это на самом деле хуже, чем использование поплавка. Первоначальный интервал 500 допускает только 8-9 вставок средней точки (2 ^ 9 = 512), в то время как двойной плавающий интервал допускает около 50, без необходимости первоначального выбора интервала.
Сампо
Используйте разрыв 500 и плавает!
Роб Малдер
При использовании числа с плавающей запятой разница не имеет значения, так как ограничивающим фактором для вставок в середине является количество бит в значении. Вот почему я предложил разрыв по умолчанию 1,0 при использовании чисел с плавающей запятой.
Сампо