java - TimSort 什么时候提示损坏的比较器?

标签 java java-7 timsort

Java 7 changed the sorting algorithm这样它就会抛出一个

java.lang.IllegalArgumentException: "Comparison method violates its general contract!"

在某些情况下,当使用的比较器有问题时。是否可以判断比较器中的哪种错误导致了这种情况?在我的实验中,如果 x != x 并不重要,如果 x < y 和 y < z 但 z < x 也无关紧要,但如果 x = y 和 y = z 但对于某些值 x < z 确实很重要x, y, z。通常是这样吗?

(如果对此有一般规则,则可能更容易在比较器中查找错误。但是当然最好修复所有错误。:-))

特别是,以下两个比较器没有让 TimSort 提示:

    final Random rnd = new Random(52);

    Comparator<Integer> brokenButNoProblem1 = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            if (o1 < o2) {
                return Compare.LESSER;
            } else if (o1 > o2) {
                return Compare.GREATER;
            }
            return rnd.nextBoolean() ? Compare.LESSER : Compare.GREATER;
        }
    };

    Comparator<Integer> brokenButNoProblem2 = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            if (o1 == o2) {
                return Compare.EQUAL;
            }
            return rnd.nextBoolean() ? Compare.LESSER : Compare.GREATER;
        }
    };

但是下面的比较器确实让它吐了:

    Comparator<Integer> brokenAndThrowsUp = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            if (Math.abs(o1 - o2) < 10) {
                return Compare.EQUAL; // WRONG and does matter
            }
            return Ordering.natural().compare(o1, o2);
        }
    };

更新:在一些现实生活中的数据中,我们遇到了一个失败,其中没有 x,y,z 且 x = y 和 y = z 但 x < z 。所以看来我的猜测是错误的,而且似乎不仅仅是这种特定的失败。有更好的想法吗?

最佳答案

看了 ComparableTimSort 的代码后,我不太确定。我们来分析一下。这是唯一抛出它的方法(有一个类似的方法只对交换的角色做同样的事情,所以分析其中一个就足够了)。

private void mergeLo(int base1, int len1, int base2, int len2) {
        assert len1 > 0 && len2 > 0 && base1 + len1 == base2;

        // Copy first run into temp array
        Object[] a = this.a; // For performance
        Object[] tmp = ensureCapacity(len1);

        int cursor1 = tmpBase; // Indexes into tmp array
        int cursor2 = base2;   // Indexes int a
        int dest = base1;      // Indexes int a
        System.arraycopy(a, base1, tmp, cursor1, len1);

        // Move first element of second run and deal with degenerate cases
        a[dest++] = a[cursor2++];
        if (--len2 == 0) {
            System.arraycopy(tmp, cursor1, a, dest, len1);
            return;
        }
        if (len1 == 1) {
            System.arraycopy(a, cursor2, a, dest, len2);
            a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
            return;
        }

        int minGallop = this.minGallop;  // Use local variable for performance
    outer:
        while (true) {
            int count1 = 0; // Number of times in a row that first run won
            int count2 = 0; // Number of times in a row that second run won

            /*
             * Do the straightforward thing until (if ever) one run starts
             * winning consistently.
             */
// ------------------ USUAL MERGE
            do {
                assert len1 > 1 && len2 > 0;
                if (((Comparable) a[cursor2]).compareTo(tmp[cursor1]) < 0) {
                    a[dest++] = a[cursor2++];
                    count2++;
                    count1 = 0;
                    if (--len2 == 0)
                        break outer;
                } else {
                    a[dest++] = tmp[cursor1++];
                    count1++;
                    count2 = 0;
                    if (--len1 == 1)
                        break outer;
                }
            } while ((count1 | count2) < minGallop);

// ------------------ GALLOP
            /*
             * One run is winning so consistently that galloping may be a
             * huge win. So try that, and continue galloping until (if ever)
             * neither run appears to be winning consistently anymore.
             */
            do {
                assert len1 > 1 && len2 > 0;
                count1 = gallopRight((Comparable) a[cursor2], tmp, cursor1, len1, 0);
                if (count1 != 0) {
                    System.arraycopy(tmp, cursor1, a, dest, count1);
                    dest += count1;
                    cursor1 += count1;
                    len1 -= count1;
// -->>>>>>>> HERE IS WHERE GALLOPPING TOO FAR WILL TRIGGER THE EXCEPTION
                    if (len1 <= 1)  // len1 == 1 || len1 == 0
                        break outer;
                }
                a[dest++] = a[cursor2++];
                if (--len2 == 0)
                    break outer;

                count2 = gallopLeft((Comparable) tmp[cursor1], a, cursor2, len2, 0);
                if (count2 != 0) {
                    System.arraycopy(a, cursor2, a, dest, count2);
                    dest += count2;
                    cursor2 += count2;
                    len2 -= count2;
                    if (len2 == 0)
                        break outer;
                }
                a[dest++] = tmp[cursor1++];
                if (--len1 == 1)
                    break outer;
                minGallop--;
            } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
            if (minGallop < 0)
                minGallop = 0;
            minGallop += 2;  // Penalize for leaving gallop mode
        }  // End of "outer" loop
        this.minGallop = minGallop < 1 ? 1 : minGallop;  // Write back to field

        if (len1 == 1) {
            assert len2 > 0;
            System.arraycopy(a, cursor2, a, dest, len2);
            a[dest + len2] = tmp[cursor1]; //  Last elt of run 1 to end of merge
        } else if (len1 == 0) {
            throw new IllegalArgumentException(
                "Comparison method violates its general contract!");
        } else {
            assert len2 == 0;
            assert len1 > 1;
            System.arraycopy(tmp, cursor1, a, dest, len1);
        }
    }

该方法执行两个已排序运行的合并。它会进行通常的合并,但一旦遇到一方开始“获胜”(即总是比另一方少),它就会开始“疾驰”。 Gallopping 试图通过向前看更多元素而不是一次比较一个元素来使事情变得更快。由于运行应该排序,向前看是好的。

您会看到只有当 len1 最后为 0 时才会抛出异常。 第一个观察结果如下:在通常的合并过程中,永远不会 抛出异常,因为一旦 len this 1 循环就会直接中止。 因此,异常只能作为奔跑的结果抛出

这已经强烈暗示异常行为是不可靠的:只要你有小数据集(小到生成的运行可能永远不会疾驰,因为 MIN_GALLOP7) 或生成的运行总是巧合生成一个从不疾驰的合并,你将永远不会收到异常。因此,无需进一步审查 gallopRight 方法,我们就可以得出您不能依赖该异常的结论:它可能永远不会被抛出无论您的比较器有多么错误

关于java - TimSort 什么时候提示损坏的比较器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24951257/

相关文章:

java - 找不到 Artifact com.sun :tools:jar:1. 7

c++ - 在 C++ 中使用 OpenMP 和 Timsort 算法

algorithm - 如何使用 TimSort 按多个字段排序?

java - "Comparison method violates its general contract!"- TimSort 和 GridLayout

java - 最有效的 Artifact 复制方式?

java - 了解 writeDataToPipe

java - 从链表末尾删除第 K 个节点的内存高效方法

java - 为什么 Java 7 中需要 Diamond 运算符?

java - 带字符串的 Switch 语句

java - 为什么人们在事件队列上运行 Java GUI