用最少的重新编号来对项目进行排序

4

我需要快速将重新排序的序列保存回我的项目的整数sortOrder列。

简单的逐一加一的方法可能很慢 - 如果最后一个项目移动到第一个位置,那么所有N行都会被修改。使用多行更新语句可以让数据库完成这项工作,但我想探索更智能的方法,比如将sortOrder设置为浮点数,但我没有这个选项:(

我想象中的解决方案将接受类似这样重新编号的列表:(100,200,1700,300,400...1600,1800,...) 并产生 (100,200,250,300,400...1600,1800,...) (在此示例中只修改了一行)。 这似乎很简单,但我很难用代码表达出来...

有人能帮我理清思路吗?可能会有需要移动相邻项目序列才能适应新项目 - 我希望有人已经写好了这部分内容?它必须比我现在拥有的更快,但仍然易于阅读和维护。

好的,在得到答案后,我将返回我想到的代码(欢迎评论):

/**
 * Renumber list with minimal changes
 * 
 * Given a list of SortOrderNumbers in the 'new' sequence they are to be saved in, determine the
 * minimal set of changes (described by Change(i, newSortOrderNumber)) that can be saved that
 * results in a properly ordered sequence.
 * 
 * A simple answer would always be List(change<1,1>, change<2,2>, ...change<n,n>) which is of length N.
 * This method returns a set of changes no larger than N (often much smaller for simple moves).
 * 
 * @author Jim Pinkham
 * @param s
 * @return Set<Change>
 */
private Set<Change> renumber(int[] s) {
    Set<Change> ret = new HashSet<Change>();
    // pass1 goes from start forwards looking for decrease in numbering
    for (int i=1; i<s.length; i++) {
        // if predecessor of item is larger and it's large enough to renumber from start of list 
        if (s[i-1]>s[i] && s[i]>i) {                
            int beforeStart=0;
            int beforeIndex=-1;
            // go back towards start looking for anchor
            for (int j=i-2; j>=0; --j) {
                int diff = s[i]-s[j];
                if (diff>(i-j)) {
                    beforeIndex=j;
                    beforeStart=s[beforeIndex];
                    break;
                }
            }
        int diff = s[i]-beforeStart;
        int stepsToDiff=i-beforeIndex;
            int delta = diff/stepsToDiff;
            // renumber from start of list or anchor thru decrease
            int fixCnt=0;
            for (int j=beforeIndex+1; j<i; ++j) {
                s[j] = beforeStart + (delta*++fixCnt); 
                System.out.println("s["+j+"]="+s[j]);
                ret.add(new Change(j, s[j]));
            }
        }
    }
    // pass1 could leave some decreases in sequence  
    // pass2 goes from end back to start
    for (int i=s.length-1; i>0; i--) {
        // if predecessor of item is larger 
        if (s[i-1] > s[i]) {
            int afterIndex=s.length;
            int delta=DEFAULT_RENUMBER_GAP;
            // go back towards end looking for anchor               
            for (int j=i; j<s.length; ++j) {
                int diff = s[j]-s[i-1];
                if (diff>(j-(i-1))) {
                    afterIndex=j;
                    int afterEnd=s[afterIndex];
                    int stepsToDiff=afterIndex-(i-1);
                    int gap = afterEnd-s[i-1];
                    delta = gap/stepsToDiff;
                    break;
                }
            }
            // renumber from decrease thru anchor or end of list
            int fixCnt=0;
            for (int j=i; j<afterIndex; ++j) {
                s[j] = s[i-1] + (delta*++fixCnt); 
                System.out.println("s["+j+"]="+s[j]);
                ret.add(new Change(j, s[j]));                   
            }
        }
    }
    return ret;
}
class Change { 
    int i; 
    int sortOrder; 
    Change(int i, int sortOrder) { 
        this.i=i; this.sortOrder=sortOrder; 
    }
    public boolean equals(Change other) {
        return Integer.valueOf(i).equals(Integer.valueOf(other.i));
    }
    public int hashCode() {
        return Integer.valueOf(i).hashCode();
    }
}

抱歉,显然我是新手 - 如果有人可以让它易读,请随意修改。 - Jim P
您可以编辑自己的问题并在那里添加源代码,然后标记为“edit:”或类似内容。 - DaClown
3个回答

2
如果你认为浮点数更容易理解,为什么不将数字想象成定点数呢?
例如,针对你的算法目的,将1000000解释为100.0000。你需要选择点的位置,以便在整数大小与(数组中最大条目数+2)给定的情况下,有尽可能多的小数(或二进制)位数。因此,假设最大条目数为998,则需要3个小数点前的数字,其余部分可用于“间隙”。
移动操作可以简单地设置其新排序号为相邻项排序号之和的一半,即将移动的项目插入到其新邻居之间。使用0和size(array)+1作为结束情况。我再次假设您的UI可以记录用户执行的移动-无论如何,我认为这应该是相当简单的,标准的排序算法可能可以使用,只需重新定义“交换”。
因此,例如,在此数组中将最后一个移动到第一个(带有虚拟小数点):
1.0000 2.0000 3.0000 4.0000 5.0000
变成
1.0000 2.0000 3.0000 4.0000 0.5000 =(0.0000 + 1.0000)/ 2
得到的排序顺序为
0.5000 1.0000 2.0000 3.0000 4.0000
这只改变了一个记录,即数组中的最后一个记录。
将最后一个移动到第二个会做到这一点:
1.0000 2.0000 3.0000 4.0000 5.0000
变成
1.0000 2.0000 3.0000 4.0000 1.5000 =(1.0000 + 2.0000)/ 2
导致排序顺序为
1.0000 1.5000 2.0000 3.0000 4.0000
同样,只有一个记录发生了变化。
你仍然需要考虑在两个数字之间运行出空间的情况,这是不可避免的。我认为无论使用哪种算法都是如此。这将需要“交换”重新编号更多条目以腾出更多空间。同样,无论使用哪种算法,我认为你不能排除所有东西都必须重新编号的情况,只是这种情况很少见。我还怀疑随着时间的推移,大规模重新编号变得更加可能,无论你做什么-可用空间将会分裂。但是通过选择点的位置以尽可能地获得更多空间,它应该是最优的,即您尽可能地延迟它。
为了避免在不方便的时候进行更广泛的重新编号,建议在空闲期间定期进行某种批量重新编号 - 基本上再次拉伸间隔以为用户驱动排序腾出更多空间。再次强调,无论使用哪种方法,这都是必要的。
这只是一个草图,我认为它可能等同于任何其他方式,尽管可能更直观/易于维护,并且最大限度地扩展空间。另外,如果您真的担心退化情况的性能差-从您的描述中听起来应该是的-我建议使用很多随机数据(没有数据库)在长时间内在测试工具中运行您选择的任何算法,以查看实际执行了多少次重新编号,特别是在长时间使用后是否会降级。我怀疑任何此类算法都将这样做。

感谢您抽出时间提出这个建议。我几乎考虑过完全相同的想法 - 正如您所提到的,最终需要重新编号。我想我通过使用大型RENUMBER_GAP提出了一种混合方法,这样我就可以在大多数情况下得到“浮点近似”,但它也可以在需要时处理重新编号。 - Jim P

1

根据您的示例,您可以这样做:

遍历您的数字数组。如果一个项目x的后继比x本身小,则向后遍历数组,直到找到项目y,其与x+1之间的最小差异。计算您向后走的步数,取最小距离,从y开始向前走,并将项目设置为y+((x+1)-y)/count。


谢谢,这正是我在寻找的那种想法。 - Jim P

0

可以考虑增加一层间接性,例如用可重定位句柄代替行索引来实现。因此,不是处理row[x],而是处理row[handle[x]]

编辑:好的,那么在您的情况下这是不可能的...您能否澄清您期望重新排序多少?

从问题的措辞中我了解到,您只期望N个项目中的M个移动,其中M远小于N。因此,您想避免N次更新 - 您宁愿有类似M次更新的东西。

如果M小于N/2,则以交换操作为基础定义重新排序应该会更快。您没有说您的UI是什么,但用户在那里实际上可能正在执行交换操作。因此,通过记录这些操作,或使用标准排序算法从原始状态到所需状态,您应该能够生成重新排序元素所需的M个交换操作集。这只需要M*2行更新 - 也就是说,如果只有两个项目交换位置,您只需要更新2行。

虽然可能存在一些退化情况,这实际上比重新编写所有内容要慢 - 但如果像问题所暗示的那样,只是用户重新排序东西,这似乎不太可能发生。


换句话说,使用外部索引可能会对其他人有所帮助,但在我的情况下并不可行。我的主要问题是,我只能实现大约5个记录/秒的更新速度,因此在某些情况下保存需要6分钟以上。希望能提供一些重新编号算法的参考。 - Jim P
我很欣赏这个想法,但我认为“只有两个项目交换位置时,您只需要更新2行”这个想法并不适用,除非我正在使用链表。 - Jim P
其实这是真的,无论使用哪种数据结构。然而,当我在纸上运行它时,我发现它仍然会受到退化情况的影响,比如您提到的情况,例如将最后一个元素移动到第一个位置需要足够多的交换以触碰每一行,所以它不适用于您想要的情况。我现在认为我理解了您试图通过间隔做什么 - 我有一个不同的想法,可能适合您。我会将其作为一个不同的答案发布。 - frankodwyer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接