重新排序项目时生成排序键


11

我们有许多项目,最终用户将可以将它们组织成所需的顺序。项目集是无序的,但是每个项目都包含一个可以修改的排序键。

我们正在寻找一种算法,该算法将允许为添加或移动为第一项,最后一项或任何两个项目之间的项目生成新的排序键。我们希望只需要修改要移动的项目的排序键。

一个示例算法是使每个排序键为浮点数,并且在将一项放置在两个项之间时,将排序键设置为其平均值。第一个或最后一个放置项将采用最外层的值+/- 1。

这里的问题是浮点精度可能会导致排序失败。类似地,使用两个整数表示小数可能会使数字变得很大,以致于无法以常规数值类型(例如,当以JSON格式传输时)准确地表示它们。我们不想使用BigInts。

是否有合适的算法对此起作用,例如使用字符串,而不会受到这些缺点的影响?

我们不希望支持大量的移动,但是上述算法可能会在大约50个移动后出现在双精度浮点数上而失败。


字符串是一个显而易见的选择,因为您可以一直在字符串的末尾添加字符以进行分叉。就是说,我觉得有更好的方法来解决这个问题。
罗伯特·哈维

从我的头顶上,我没有看到如何在不修改其他项的键的情况下使用字符串使其工作。
Sampo

3
您所描述的问题称为订单维护问题
内森·梅里尔

1
您为什么不修改列表中的其他项目?
德国公开赛

1
你如何使其与字符串的工作是这样的: A, B, C- - A, AA, B, C- 。A, AA, AB, B, C A, AA, AAA, AAB, AAC, AB, AC, B, C当然,您可能希望更多地分隔字母,以使字符串不会增长得那么快,但是可以做到。
罗伯特·哈维

Answers:


4

作为所有评论和答案的摘要:

TL; DR-在最初提出的算法中使用双精度浮点数应足以满足大多数实际(至少是人工排序)的需求。还应考虑维护元素的单独的有序列表。其他排序密钥解决方案比较麻烦。

这两个有问题的操作是一遍又一遍地在开始/结束处插入项目,并重复将项目插入或移动到同一位置(例如,三个元素在前两个之间重复移动第三个元素,或重复添加新元素作为第二个元素)元件)。

从理论上讲(即允许无限重排序),我能想到的唯一解决方案是使用两个无限大小的整数作为分数a / b。这使得中间插入物具有无限的精度,但是数目可能会越来越大。

字符串可能能够支持大量更新(尽管我仍然难以确定两个操作的算法),但不是无限的,因为您不能在第一个位置无限次添加(至少使用常规的字符串排序)比较)。

整数将需要为排序键选择一个初始间距,这限制了您可以执行多少个中间插入。如果最初将排序键间隔1024个,则在具有相邻编号之前只能执行10次最坏情况的中间插入。选择较大的初始间距会限制您可以执行的第一个/最后一个插入次数。使用64位整数,无论​​哪种方式,您最多只能进行约63个操作,您需要在中间插入和先后插入之间进行拆分。

使用浮点值无需选择先验间距。该算法很简单:

  1. 插入的第一个元素具有排序键0.0
  2. 最先插入(或移动)的元素具有第一个元素的排序键-1.0或最后一个元素+ 1.0。
  3. 在两个元素之间插入(或移动)的元素的排序键等于两个元素的平均值。

使用双精度浮点数允许52个最坏情况的中间插入,并有效地无限次(大约1e15)个第一个/最后一个插入。在实践中,当围绕算法移动项目时,它应该自我校正,因为每次您第一次或最后一次移动项目时,它都会扩大可使用的范围。

双精度浮标还具有以下优点:所有平台都支持它们,并且几乎所有运输格式和库都易于存储和运输。这就是我们最终使用的。


1

我根据@Sampo的摘要在TypeScript中编写了一个解决方案。该代码可以在下面找到。

在此过程中获得了一些见解。

  • 只有在中间插入两个现有的排序键之间需要生成一个新的排序关键字,交换(即清理)不会进行拆分(即新的中点)。如果您移动了两个项目,而您仅触摸了其中一项,则将丢失有关哪些两个元素更改了列表位置的信息。即使是一开始的要求,也请注意这是一个好主意

  • 每隔1074:th中点分割一次,我们就需要标准化浮点范围。我们可以通过简单地检查新的中点是否满足不变量来检测到这一点。

    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个,没有初始选择间隔的问题。
Sampo'7

使用500中的间隙彩车!
罗伯·穆尔德

当使用浮点数时,间隙没有区别,因为中间插入的限制因素是有效位数。这就是为什么我建议使用浮点数时的默认差距1.0。
Sampo'7
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.