我想要一种简单的方法来表示对象列表的顺序。当一个对象改变该列表中的位置时,我想更新只有一个记录。我不知道是否可以这样做,但我有兴趣问这个问题......
为什么我只关心一次更新一件物品......
[更新澄清问题]
此算法的用例是具有CRUDy,资源丰富的服务器设置和干净(Angular)客户端的Web应用程序。
在可能的情况下保持纯CRUD操作并在全面实现更清晰的代码是一种很好的做法。如果我可以在单个resource#update
请求中执行此操作,那么我不需要任何其他服务器端代码来处理重新排序,并且可以使用CRUD完成所有操作而无需进行任何更改。
如果每次移动需要更新列表中的多个项目,那么我需要在我的控制器上执行一个新操作来处理它。它不是一个showstopper,但它开始蔓延到Angular,一切都变得不那么理想应该是干净的。
我们说我们有一本杂志,杂志上有很多页面:
Original magazine
- double page advert for Ford (page=1)
- article about Jeremy Clarkson (page=2)
- double page advert for Audi (page=3)
- article by James May (page=4)
- article by Richard Hammond (page=5)
- advert for Volkswagen (page=6)
...我们每次最多更新N条记录
如果我想将Richard Hammond的页面从第5页拉到第2页,我可以通过更改页码来实现。但是,我还必须更改所有它取代的页面:
Updated magazine
- double page advert for Ford (page=1)
- article by Richard Hammond (page=2)(old_value=5)*
- article about Jeremy Clarkson (page=3)(old_value=2)*
- double page advert for Audi (page=4)(old_value=3)*
- article by James May (page=5)(old_value=4)*
- advert for Volkswagen (page=6)
*属性已更新
- 它不适合我的架构
让我们说这是通过Angular.js使用javascript drag-n-drop重新排序来完成的。理想情况下,我只想更新已移动的页面上的值,并保留其他页面。我想向Richard Hammond的页面发送一个http请求到CRUD资源,说它现在已被移动到第二页。
- 并且它没有缩放
对我来说这不是问题,但在某些时候我可能有10,000页。当我将新页面移到首页时,我宁愿不更新9,999个。
...我们每次更新3条记录
如果不是存储页面的位置,而是存储之前的页面,然后我将操作次数从最多N减少到3。
Original magazine
- double page advert for Ford (id = ford, page_before = nil)
- article about Jeremy Clarkson (id = clarkson, page_before = ford)
- article by James May (id = captain_slow, page_before = clarkson)
- double page advert for Audi (id = audi, page_before = captain_slow)
- article by Richard Hammond (id = hamster, page_before = audi)
- advert for Volkswagen (id = vw, page_before = hamster)
我们再次移动厚脸皮的仓鼠......
Updated magazine
- double page advert for Ford (id = ford, page_before = nil)
- article by Richard Hammond (id = hamster, page_before = ford)*
- article about Jeremy Clarkson (id = clarkson, page_before = hamster)*
- article by James May (id = captain_slow, page_before = clarkson)
- double page advert for Audi (id = audi, page_before = captain_slow)
- advert for volkswagen (id = vw, page_before = audi)*
*属性已更新
这需要更新数据库中的三行:我们移动的页面,旧页面下方的页面以及新位置下方的页面。
它更好但它仍然涉及更新三个记录,并没有给我我正在寻找的足智多谋的CRUD行为。
...我们每次只更新1条记录(但需要保养)
请记住,我仍然希望每次重新定位只更新一条记录。在我的努力中,我采取了不同的方法。不是将页面位置存储为整数,而是将其存储为浮点数。这允许我通过在两个其他人之间移动来移动项目:
Original magazine
- double page advert for Ford (page=1.0)
- article about Jeremy Clarkson (page=2.0)
- double page advert for Audi (page=3.0)
- article by James May (page=4.0)
- article by Richard Hammond (page=5.0)
- advert for Volkswagen (page=6.0)
然后我们再次移动Hamster:
Updated magazine
- double page advert for Ford (page=1.0)
- article by Richard Hammond (page=1.5)*
- article about Jeremy Clarkson (page=2.0)
- double page advert for Audi (page=3.0)
- article by James May (page=4.0)
- advert for Volkswagen (page=6.0)
*属性已更新
每次我们移动一个项目时,我们在其上方和下方的项目之间选择一个值(比如我们在两个项目的平均值之间滑动)。
最终虽然你需要重置......
无论您使用哪种算法将页面插入彼此,最终都会耗尽小数,因为您必须继续使用较小的数字。随着您移动物品的次数越来越多,您逐渐向下移动浮点链并最终需要一个小于任何可用物品的新位置。
因此,您不得不进行重置以重新索引列表并将其全部带回范围内。这没关系,但我很想知道是否有办法对不需要这种内务管理的订单进行编码。
此问题是否存在算法(或者更确切地说,数据编码),只需要一次更新而不需要内务处理?如果是这样,你可以用简单的英语解释它是如何工作的(例如,没有引用有向图或顶点......)? Muchos gracias。
我已经将这个问题的奖励给了我认为最有趣的答案。没有人能够提供解决方案(因为从事物的外观来看,没有一个)所以我没有将任何特定问题标记为正确。
调整无管家准则
在花了更多时间思考这个问题之后,我发现实际上应该调整内务管理标准。家务管理的真正危险并不在于它是一件很麻烦的事情,但理想情况下,对于拥有一套出色套装的客户来说,它应该是健全的。
让我们说Joe加载一个包含列表的页面(使用Angular),然后去喝一杯茶。在他下载之后,家务管理就会发生并重新索引所有物品(1000,2000,3000等)。在他从他的茶杯中回来后,他从1010 1011移动物品。此时存在风险重新编制索引会将他的项目置于一个不打算去的位置。
作为未来的注释 - 理想情况下,任何内务处理算法对于在列表的不同housekept版本中提交的项目也应该是健壮的。或者你应该对内务管理进行版本控制并在有人试图创建错误时跨版本更新。
链接列表的问题
虽然链表只需要一些更新,但它也有一些缺点:
我选择的方法
我认为已经看过所有的讨论,我想我会选择非整数(或字符串)定位:
但它确实需要内务管理,如上所述,如果您要完成,您还需要对每个内务处理进行编辑,如果有人尝试根据以前的列表版本进行更新,则会引发错误。
答案 0 :(得分:7)
@tmyklebu有答案,但他从来没有完全触及过:你的问题的答案是“不”,除非你愿意接受最坏情况下的n-1位密钥长度来存储n个项目。 / p>
这意味着n个项目的总密钥存储量为O(n ^ 2)。
有一个“对手”的信息理论论证说无论你为n个项目的数据库选择分配键的方案,我总能拿出一系列n项重新定位(“移动项目k”定位p。“)将强制你使用n-1位的密钥。或者通过扩展,如果我们从一个空数据库开始,并且你给我插入项目,我可以选择一系列插入位置,这将要求你至少使用第一个零位,一个用于第二个,等等。
修改强>
我之前有一个关于使用有理数字作为键的想法。但它比仅仅添加一位长度来分割相差一对的键之间的差距更为昂贵。所以我删除了它。
答案 1 :(得分:6)
您应该在愿望清单中添加一个明智的约束:
O(log N)
空间(N为商品总数)例如,链表解决方案坚持这一点 - 您需要至少N个可能的指针值,因此指针占用log N空间。如果你没有这个限制,Lasse Karlsen和tmyklebu已经提到的简单解决方案(增长字符串)是解决你的问题的方法,但是每个操作的内存增加了一个字符(在最坏的情况下)。您需要某些限制,这是明智之举。
然后,听听答案:
嗯,这是一个强有力的陈述,并且不容易听到,所以我想证明是必需的:)我试图找出一般证明,posted a question on Computer Science Theory,但一般的证据真的很难做到。假设我们更容易,我们将明确假设有两类解决方案:
反驳绝对寻址算法的存在很容易。只取3个项目,A,B,C,然后继续移动前两个项目。您将很快用完移动元素的地址的可能组合,并将需要更多位。你将打破有限空间的约束。
反驳相对寻址的存在也很容易。对于非平凡的安排,当然存在一些其他项目所指的两个不同的位置。然后,如果您在这两个位置之间移动某个项目,则必须至少更改两个项目 - 引用旧位置的项目和引用新位置的项目。这违反了仅更改一个项目的约束。
<强> Q.E.D。强>
现在我们(和您)可以承认您所需的解决方案不存在,为什么您会使用不起作用的复杂解决方案使您的生活复杂化?正如我们在上面所证明的那样,他们无法工作。我想我们迷路了。这里的伙伴们花费了巨大的努力来结束过于复杂的解决方案,这些解决方案甚至比最简单的解决方案更糟糕:
基因的有理数 - 在他的例子中它们增长4-6位,而不是最简单的算法(如下所述)所需的1位。 9/14有4 + 4 = 8位,19/21有5 + 5 = 10位,得到的数字65/84有7 + 7 = 14位!!如果我们只看这些数字,我们会发现10/14或2/3是更好的解决方案。 可以很容易地证明,不断增长的弦乐解决方案是无与伦比的,见下文。
mhelvens&#39;解决方案 - 在最坏的情况下,他将在每次操作后添加一个新的修正项目。这肯定会占用更多。
这些家伙非常聪明,但显然不能带来明智的东西。有人必须告诉他们 - 停止,没有解决方案,而你所做的事情并不比你害怕提供的最简单的解决方案更好: - )
现在,返回限制列表。 其中一个必须被打破,你知道吗。通过列表并询问,哪一个最不痛苦?
这很难被无限侵犯,因为你的空间有限......所以要准备好不时地违反管家限制。
对此的解决方案是tmyklebu已经提出并由Lasse Karlsen提及的解决方案 - 不断增长的字符串。只考虑0和1的二进制字符串。你有A,B和C项,并在A和B之间移动C.如果A和B之间没有空格,即它们看起来
A xxx0
B xxx1
然后再为C添加一位:
A xxx0
C xxx01
B xxx1
在最坏的情况下,每次操作后都需要1位。您还可以处理字节,而不是位。然后在最坏的情况下,您将不得不为每8个操作添加一个字节。它都是一样的。 而且,很容易看出这个解决方案无法打败。您必须添加至少一位,并且不能添加更少。 换句话说,无论解决方案如何复杂,它都不会比这更好。
优点:
缺点:
这会导致原始的链接列表解决方案。此外,还有很多平衡的树数据结构,如果你需要查找或比较项目(你没有提到),它们会更好。
这些可以修改3个项目,平衡树有时需要更多(当需要进行平衡操作时),但由于它是分摊的O(1)
,在长行操作中,每个操作的修改数量是不变的。在您的情况下,我只会在您需要查找或比较项目时使用树解决方案。否则,链表解决方案就会出现问题。抛出它只是因为它们需要3次操作而不是1次操作? C&#39; mon:)
<强>优点:强>
<强>缺点:强>
O(N)
列表和O(log N)
平衡树。这些是整数和浮点数的解决方案,最好由Lasse Karlsen在这里描述。此外,点1)的解决方案将落在这里:)。 Lasse已经提到了关键问题:
如果您将使用k
- 位整数,那么从最佳状态开始,当项目在整数空间中均匀分布时,每次k - log N
次操作都必须进行内务处理,在最坏的情况下。然后,您可以使用更多不那么复杂的算法来限制您和#34; housekeep&#34;。
<强>优点:强>
<强>缺点:强>
我认为最好的方法,以及这里的答案证明,决定哪些约束是最不痛苦的,只是采取以前不赞成的简单解决方案之一。
但是,希望永远不会消亡。写这篇文章的时候,我意识到如果我们能够询问服务器的话,会有你想要的解决方案!当然取决于服务器的类型,但是经典的SQL服务器已经实现了树/链接列表 - 用于索引。服务器已经在执行像&#34这样的操作;在树之前移动此项目&#34; !!但服务器正在根据数据进行操作,而不是基于我们的请求。如果我们能够以某种方式要求服务器执行此操作,而无需创建不正当,无休止增长的数据,那将是您理想的解决方案!正如我所说,服务器已经做到了 - 解决方案非常接近,但到目前为止。如果你可以编写自己的服务器,你可以这样做: - )
答案 2 :(得分:5)
您还可以将选项3解释为将位置存储为无限长字符串。这样你就不会“用完小数点”或任何那种性质。给出第一项,说'foo',位置1
。递归地将您的Universe划分为“少于foo的东西”,它获得0
前缀,以及“比foo更大的东西”,它获得1
前缀。
这在很多方面很糟糕,特别是对象的位置可能需要尽可能多的位来代表你完成对象移动。
答案 3 :(得分:5)
我被这个问题着迷,所以我开始研究一个想法。不幸的是,它很复杂(你可能知道它会)并且我没有时间全力以赴。我以为我会分享我的进步。
它基于双向链接列表,但在每个移动的项目中都有额外的簿记信息。有了一些聪明的技巧,我怀疑该集合中的每个n项都需要少于O(n)的额外空间,即使在最坏的情况下,但我没有证明这一点。它还需要额外的时间来确定视图顺序。
例如,采取以下初始配置:
A (-,B|0)
B (A,C|0)
C (B,D|0)
D (C,E|0)
E (D,-|0)
从上到下排序纯粹来自元数据,元数据由每个项目的状态序列(predecessor,successor|timestamp)
组成。
在D
和A
之间移动B
时,您会使用新的时间戳将新状态(A,B|1)
推送到其序列的前面,您可以通过递增共享柜台:
A (-,B|0)
D (A,B|1) (C,E|0)
B (A,C|0)
C (B,D|0)
E (D,-|0)
如您所见,我们会保留旧信息,以便将C
与E
联系起来。
以下是大致如何从元数据中获取正确的顺序:
A
。A
同意它没有前任。所以插入A
。它会引导您B
。B
同意它希望成为A
的继承者。因此,在B
之后插入A
。它会引导您C
。C
同意它希望成为B
的继承者。因此,在C
之后插入B
。它会引导您D
。D
不同意。它希望成为A
的继承者。开始递归以插入它并找到真正的后继者:
D
从B
获胜,因为它有更新的时间戳。在D
之后插入A
。它会引导您B
。B
已经是D
的继任者。回顾D
的历史记录,它会引导您E
。E
同意它希望成为D
的后继者,时间戳为0.所以请返回E
。E
。在E
之后插入C
。它告诉你它没有继承者。你已经完成了。这还不是一个算法,因为它并未涵盖所有情况。例如,当您向前移动项目而不是向后移动项目时。在B
和D
之间移动E
时:
A (-,B|0)
C (B,D|0)
D (C,E|0)
B (D,E|1)(A,C|0)
E (D,-|0)
'move'操作是一样的。但是导出正确顺序的算法有点不同。从A
开始,它会遇到B
,能够从中获得真正的继承者C
,但却无法插入B
。您可以将其保留为D
之后插入的候选对象,在该位置最终将时间戳与E
的位置匹配以获得该位置的权限。
我写了一些Angular.js code on Plunker,可以作为实现和测试该算法的起点。相关功能称为findNext
。它没有做任何聪明的事情。
有优化可以减少元数据量。例如,当一个项目远离它最近放置的位置时,它的邻居仍然是自己联系的,你不必保留它的最新状态,但可以只替换它。并且可能存在这样的情况:您可以丢弃所有项目的足够旧状态(当您移动它时)。
遗憾的是,我没有时间完全解决这个问题。这是一个有趣的问题。
祝你好运! 编辑:我觉得我需要澄清上述优化提示。首先,如果原始链接仍然存在,则无需推送新的历史记录配置。例如,可以从这里开始(在D
和A
之间移动B
):
A (-,B|0)
D (A,B|1) (C,E|0)
B (A,C|0)
C (B,D|0)
E (D,-|0)
到此处(然后在D
和B
之间移动C
):
A (-,B|0)
B (A,C|0)
D (B,C|2) (C,E|0)
C (B,D|0)
E (D,-|0)
我们可以放弃(A,B|1)
配置,因为A
和B
仍然是自己连接的。任何数量的“不相关”运动都可以在不改变它的情况下进行。
其次,假设最终C
和E
彼此远离,因此(C,E|0)
配置可以在下次D
移动时删除。不过,这很难证明。
考虑到所有这些,我相信列表很可能需要少于而不是O(n+k)
空间(n
是列表中的项目数,{在最坏的情况下,{1}}是操作的数量;特别是在一般情况下。
证明这一点的方法是为这种数据结构提出一个更简单的模型,很可能是基于图论。我再次感到遗憾的是,我没有时间研究这个问题。
答案 4 :(得分:4)
你最好的选择是“选项3”,虽然不一定要涉及“非整数”。
“非整数”可以表示具有某种精确度定义的任何内容,这意味着:
在每种情况下,您都会遇到准确性问题。对于浮点类型,数据库引擎中可能存在硬限制,但对于字符串,限制将是您允许的空间量。请注意,您的问题可以理解为“没有限制”,这意味着对于这样的解决方案而言,您确实需要无限的精确度/空间用于键。
但是,我认为你不需要那样。
假设您最初将每个第1000个索引分配给每一行,这意味着您将拥有:
1000 A
2000 B
3000 C
4000 D
... and so on
然后按如下方式移动:
此时,列表如下所示:
1000 A
1001 D
1002 B
1004 C
现在,您想要在A和D之间向上移动C.
目前这是不可能的,因此您将不得不重新编号某些项目。
您可以通过将B更新为编号1003来尝试更新最小行数,从而得到:
1000 A
1001 C
1002 D
1003 B
但是现在,如果你想在A和C之间移动B,你将重新编号除A之外的所有东西。
问题是:你有多少可能发生这种病态的事件?
如果答案非常可能那么无论你做什么,都会遇到问题。
如果答案可能很少,那么您可能会认为上述方法的“问题”是可管理的。请注意,重新编号和排序多行可能是这里的例外情况,您可能会得到类似“每次移动已分摊的1行”的内容。摊销意味着您分摊了在您不需要更新多行的情况下的成本。
答案 5 :(得分:1)
如果您保存原始订单并在保存一次后不更改它然后将增量数存储在列表中或列表中,该怎么办?
然后通过向上移动3个级别,您只会存储此操作。
在数据库中,您可以按数学计算列进行排序。
首次插入:
ord1 | ord2 | value
-----+------+--------
1 | 0 | A
2 | 0 | B
3 | 0 | C
4 | 0 | D
5 | 0 | E
6 | 0 | F
更新订单,将D向上移动2级
ord1 | ord2 | value | ord1 + ord2
-----+------+-------+-------------
1 | 0 | A | 1
2 | 0 | B | 2
3 | 0 | C | 3
4 | -2 | D | 2
5 | 0 | E | 5
6 | 0 | F | 6
按ord1 + ord2排序
ord1 | ord2 | value | ord1 + ord2
-----+------+-------+-------------
1 | 0 | A | 1
2 | 0 | B | 2
4 | -2 | D | 2
3 | 0 | C | 3
5 | 0 | E | 5
6 | 0 | F | 6
按ord1 + ord2 ASC,ord2 ASC
排序ord1 | ord2 | value | ord1 + ord2
-----+------+-------+-------------
1 | 0 | A | 1
4 | -2 | D | 2
2 | 0 | B | 2
3 | 0 | C | 3
5 | 0 | E | 5
6 | 0 | F | 6
将E向上移动4级
ord1 | ord2 | value | ord1 + ord2
-----+------+-------+-------------
5 | -4 | E | 1
1 | 0 | A | 1
4 | -2 | D | 2
2 | 0 | B | 2
3 | 0 | C | 3
6 | 0 | F | 6
类似于相对排序的东西,其中ord1是绝对顺序,而ord2是相对顺序。
同样的想法就是只存储运动历史和基于此的分类。
没有经过测试,没有尝试过,只是记下了我此刻的想法,也许它可以指向你某个方向:)
答案 6 :(得分:0)
我不确定您是否会将此作弊称为“欺骗”,但为什么不创建引用页面资源的单独页面列表资源? 如果您更改页面的顺序,则无需更新任何页面,只需更新存储订单的列表。
原始页面列表
[ford, clarkson, captain_slow, audi, hamster, vw]
更新到
[ford, hamster, clarkson, captain_slow, audi, vw]
保持页面资源不变。
答案 7 :(得分:0)
你总是可以将排序排列分别存储为ln(num_records!)/ ln(2)位位串,并弄清楚如何自己转换/ CRUD,这样你只需更新一位简单的操作,如果更新2/3记录对你来说不够好。
答案 8 :(得分:0)
以下非常简单的算法怎么样:
(让我们用书中的页码来比喻)
如果您将页面移动到“新”页面3,您现在“至少”有一页3,可能是两页,甚至更多。那么,哪一个是“正确的”第3页?
解决方案:“最新”。因此,我们利用记录也具有“更新日期/时间”的事实来确定真实页面3是谁。
如果您需要按正确的顺序表示整个列表,则必须使用两个键进行排序,一个用于页码,另一个用于“更新日期/时间”字段。