Створення ключів сортування під час упорядкування елементів


11

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

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

Прикладним алгоритмом може бути, щоб кожен ключ сортування був номером з плаваючою точкою, а при розміщенні елемента між двома елементами встановити ключ сортування як їх середнє значення. Якщо розмістити елемент першим або останнім, знадобиться найбільш зовнішнє значення + - 1.

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

Чи є для цього відповідний алгоритм, який би працював, наприклад, використовуючи рядки, на які ці недоліки не впливали б?

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


Рядки - це очевидний вибір, оскільки ви можете просто продовжувати додавати символи до кінця, щоб роздвоєти. Однак, я відчуваю, що є кращий спосіб підійти до цього.
Роберт Харві

Зверху я не бачу, як змусити його працювати за допомогою рядків, не змінюючи клавіш інших елементів.
Сампо

3
Проблема, яку ви описуєте, називається Проблема обслуговування замовлень
Натан Меррілл

1
Чому ви переймаєтесь тим, що не змінюєте інші елементи у списку?
НЕР

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) перших / останніх вставок. На практиці при переміщенні елементів навколо алгоритму слід самокорегуватися, оскільки кожен раз, коли ви переміщуєте елемент перший чи останній, він розширює діапазон, який можна використовувати.

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


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

Побувши там, зробивши це, можливо, доведеться це зробити ще раз. Використовуйте рядок як ключ сортування, тоді ви завжди можете знайти ключ, який знаходиться між двома заданими клавішами. Якщо рядки виходять занадто довгими на ваш смак, вам доведеться змінити декілька клавіш або всі сортування.


1
Не завжди можна знайти ключ, який знаходиться перед іншим рядковим ключем.
Сампо

-1

Використовуйте цілі числа та встановіть для сортування для початкового списку номер 500 * номер елемента. При вставці між елементами можна використовувати середню. Це дозволить почати багато вставок


2
Це насправді гірше, ніж використання поплавця. Початковий пробіл у 500 дозволяє лише 8-9 середніх точок вставки (2 ^ 9 = 512), тоді як подвійний поплавок дозволяє приблизно 50, без жодного питання про вихідний пробіл.
Сампо

Використовуйте розрив 500 і плаває!
Роб Малдер

При використанні поплавця розрив не має різниці, оскільки обмежуючим коефіцієнтом для середини вставки є кількість бітів у значенніі. Ось чому я запропонував розрив за замовчуванням 1,0 при використанні плавців.
Сампо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.