Java 中的排序数组列表

Sorted array list in Java

提问人:Chris Knight 提问时间:10/27/2010 最后编辑:Chris Knight 更新时间:12/19/2022 访问量:119636

问:

我很困惑,我找不到一个快速的答案。我本质上是在 Java 中寻找一个实现接口的数据结构,但它按排序顺序存储其成员。我知道您可以使用普通并使用它,但是我有一个场景,我偶尔会添加并经常从我的列表中检索成员,并且我不想每次检索成员时都必须对其进行排序,以防添加新成员。谁能指出JDK甚至第三方库中存在的这样的东西?java.util.ListArrayListCollections.sort()

编辑:数据结构需要保留重复项。

答案摘要:我发现所有这些都非常有趣,也学到了很多东西。特别值得一提的是 Aioobe 坚持不懈地尝试实现我的上述要求(主要是支持重复项的排序 java.util.List 实现)。我已经接受了他的回答,认为这是我所问的最准确的,也是最发人深省的,即使我问的并不完全是我所需要的。

我要求的问题在于 List 接口本身和接口中可选方法的概念。引用 javadoc:

此界面的用户可以精确控制每个元素在列表中的插入位置。

插入到排序列表中无法精确控制插入点。然后,您必须考虑如何处理某些方法。举个例子:add

public boolean add(对象 o)

 Appends the specified element to the end of this list (optional operation).

你现在处于不舒服的境地 1)打破合约并实现添加的排序版本 2)允许在列表末尾添加一个元素,破坏您的排序顺序 3)通过抛出并实现另一个按排序顺序添加项目的方法省略(作为其可选)。addaddUnsupportedOperationException

选项 3 可能是最好的,但我发现有一个你不能使用的 add 方法和另一个不在界面中的 sortedAdd 方法,这很令人讨厌。

其他相关解决方案(排名不分先后):

  • java.util.PriorityQueue,这可能比我要求的更接近我需要的。就我而言,队列并不是对象集合的最精确定义,但从功能上讲,它可以完成我需要它的所有工作。
  • net.sourceforge.nite.util.SortedList。 但是,此实现通过在方法中实现排序来破坏 List 接口的约定,并且奇怪的是,该方法对 没有效果。普遍共识表明,在这种情况下可能是更好的选择。add(Object obj)add(int index, Object obj)throw new UnsupportedOperationException()
  • 番石榴的 TreeMultiSet支持重复项的集合实现
  • ca.odell.glazedlists.SortedList 这个类在其 javadoc 中带有警告:Warning: This class breaks the contract required by List
Java 数据结构 排序

评论

4赞 serg 11/2/2010
如果您偶尔插入并经常阅读,为什么不在插入过程中对其进行排序呢?

答:

5赞 Jon Skeet 10/27/2010 #1

列表通常保留添加项的顺序。你肯定需要一个列表,还是一个排序的集合(例如 TreeSet<E>)对你来说没问题?基本上,您需要保留重复项吗?

评论

2赞 Chris Knight 10/27/2010
谢谢乔恩,但我需要保留重复项
5赞 jmj 10/27/2010 #2

看一看 SortedList

此类实现排序列表。它由一个比较器构成,可以比较两个对象并相应地对对象进行排序。将对象添加到列表中时,该对象将插入到正确的位置。根据比较器相等的对象将按照它们添加到此列表中的顺序在列表中。仅添加比较器可以比较的对象。


当列表已经包含根据比较器相等的对象时,新对象将立即插入到这些其他对象之后。

评论

5赞 Tom Anderson 10/27/2010
这看起来不错,但它看起来也有问题:addAll 的任何一个版本都没有覆盖,因此在调用这些版本后列表将被取消排序。
3赞 Thilo 10/27/2010
而add方法“没有效果”。如果无法使用,它应该抛出 UnsupportedOperationException。
0赞 jmj 10/27/2010
@Tom安德森@Thilo,同意你们俩的看法。
1赞 Chris Knight 10/27/2010
有趣的是,但我对将来有人使用并认为它会以排序的方式处理所有元素相当警惕。也同意 UnsupportedOperationException。addAll()
1赞 shrini1000 6/7/2012
添加到此列表的时间复杂度是多少?
11赞 Gadolin 10/27/2010 #3

使用 java.util.PriorityQueue

评论

7赞 Thilo 10/27/2010
这不是列表,即没有随机访问。
1赞 zengr 10/27/2010
它是一个基于队列的优先级堆,不实现 List。
3赞 Thilo 10/27/2010
当然,对于保持排序顺序的列表,索引会一直变化,因此可能不需要随机访问。
5赞 aioobe 10/27/2010
@Qwerky,请注意,确切的答案并不总是最佳答案,或者 OP 实际追求的答案。
3赞 marcorossi 11/3/2011
优先级队列在迭代时不授予排序顺序。
0赞 codeporn 10/27/2010 #4

我认为 SortedSets/Lists 和“普通”可排序集合之间的选择取决于,您是仅出于演示目的而需要排序,还是在运行时的几乎每个点都需要排序。使用排序集合的成本可能要高得多,因为每次插入元素时都会进行排序。

如果您无法在 JDK 中选择集合,可以查看 Apache Commons 集合

1赞 Tom Anderson 10/27/2010 #5

您可以对 ArrayList 进行子类化,并在添加任何元素后调用 Collections.sort(this) - 您需要覆盖两个版本的 add 和两个版本的 addAll 才能做到这一点。

性能不如在正确位置插入元素的更智能的实现,但它可以完成这项工作。如果很少添加到列表中,则清单上所有操作的摊销成本应该很低。

64赞 aioobe 10/27/2010 #6

简约解决方案

这是一个快速而肮脏的解决方案。

class SortedArrayList<T> extends ArrayList<T> {
    @SuppressWarnings("unchecked")
    public void insertSorted(T value) {
        int i = Collections.binarySearch((List<Comparable<T>>) this, value);
        add(i < 0 ? -i - 1 : i, value);
    }
}

请注意,尽管 ,将以线性时间运行,因为 .binarySearchinsertSortedadd(index, value)ArrayList

插入不可比较的内容会导致 ClassCastException。(这也是 PriorityQueue 采用的方法:依赖于自然排序的优先级队列也不允许插入不可比较的对象(这样做可能会导致 ClassCastException)。)

一个更完整的实现,就像 一样,还包括一个构造函数,允许用户传入 .PriorityQueueComparator

演示

SortedArrayList<String> test = new SortedArrayList<String>();

test.insertSorted("ddd");    System.out.println(test);
test.insertSorted("aaa");    System.out.println(test);
test.insertSorted("ccc");    System.out.println(test);
test.insertSorted("bbb");    System.out.println(test);
test.insertSorted("eee");    System.out.println(test);

....指纹:

[ddd]
[aaa, ddd]
[aaa, ccc, ddd]
[aaa, bbb, ccc, ddd]
[aaa, bbb, ccc, ddd, eee]

重写List.add

请注意,重写 List.add(或 List.addAll)以排序方式插入元素将直接违反接口规范

来自以下文档:List.add

boolean add(E e)
    将指定的元素追加到此列表的末尾(可选操作)。

保持排序不变性

除非这是一些一次性代码,否则您可能希望保证所有元素都保持排序。这将包括抛出 和 等方法,以及重写以返回 who 方法抛出。UnsupportedOperationExceptionaddaddAllsetlistIteratorListIteratorset

评论

0赞 Chris Knight 10/27/2010
一个好的开始,但调用 add 或 addall 将以未排序的方式添加成员。
0赞 aioobe 10/27/2010
是的。除了将它们附加到列表中之外,任何事情都将直接违反 List 接口。查看我更新的答案。
0赞 Chris Knight 10/27/2010
@aioobe 好点子。但是,接口方法的不受支持的操作不是代码味吗?正确的方法可能是不扩展 ArrayList,而是实现 List,但即便如此,List 可能也不是为了这个目的。从 Javadoc for List: 这不是以排序方式插入元素的最佳描述,您仍然需要处理接口方法。这些问题可能解释了为什么 List 没有以排序方式实现。The user of this interface has precise control over where in the list each element is insertedadd(int index, Object obj)
0赞 aioobe 10/27/2010
好吧,出于某种原因,该操作是可选的。如果我在对 SortedArrayList 执行操作时得到 UnsupportedExceptionOperation,我不会感到惊讶。是的,相同的推理适用于两个版本的 add、addAll 和 set 的两个版本。(根据列表接口,所有这些都是可选操作。.add
0赞 Chris Knight 10/27/2010
啊,我没有意识到它们是可选操作。剧情变厚了...... ;)
2赞 I82Much 10/27/2010 #7

它对你来说可能有点太重量级了,但 GlazedLists 有一个 SortedList,非常适合用作表或 JList 的模型

6赞 Emil 10/27/2010 #8

您可以尝试 Guava 的 TreeMultiSet

 Multiset<Integer> ms=TreeMultiset.create(Arrays.asList(1,2,3,1,1,-1,2,4,5,100));
 System.out.println(ms);

评论

0赞 Shervin Asgari 10/28/2010
+1.这是一个很棒的图书馆。MultiSet 是A collection that supports order-independent equality, like Set, but may have duplicate elements
0赞 Omnaest 3/12/2012 #9

由于目前提议的实现确实通过破坏集合 API 来实现排序列表,并且有自己的树或类似的东西的实现,因此我很好奇基于 TreeMap 的实现将如何执行。(特别是因为 TreeSet 也基于 TreeMap)

如果有人也对此感兴趣,他或她可以随时研究一下:

树列表

它是核心库的一部分,当然你可以通过 Maven 依赖来添加它。(Apache 许可证)

目前,该实现似乎与番石榴 SortedMultiSet 和 Apache Commons 库的 TreeList 在同一级别上相当不错。

但是,如果不仅仅是我一个人测试实现以确保我没有错过重要的东西,我会很高兴。

此致敬意!

0赞 Vitaly Sazanovich 2/11/2013 #10

https://github.com/geniot/indexed-tree-map

我也有同样的问题。于是我拿了java.util.TreeMap的源码,写了IndexedTreeMap。它实现了我自己的 IndexedNavigableMap

public interface IndexedNavigableMap<K, V> extends NavigableMap<K, V> {
   K exactKey(int index);
   Entry<K, V> exactEntry(int index);
   int keyIndex(K k);
}

该实现基于在红黑树发生更改时更新节点权重。权重是给定节点下的子节点数,加上一个 - self。例如,当一棵树向左旋转时:

    private void rotateLeft(Entry<K, V> p) {
    if (p != null) {
        Entry<K, V> r = p.right;

        int delta = getWeight(r.left) - getWeight(p.right);
        p.right = r.left;
        p.updateWeight(delta);

        if (r.left != null) {
            r.left.parent = p;
        }

        r.parent = p.parent;


        if (p.parent == null) {
            root = r;
        } else if (p.parent.left == p) {
            delta = getWeight(r) - getWeight(p.parent.left);
            p.parent.left = r;
            p.parent.updateWeight(delta);
        } else {
            delta = getWeight(r) - getWeight(p.parent.right);
            p.parent.right = r;
            p.parent.updateWeight(delta);
        }

        delta = getWeight(p) - getWeight(r.left);
        r.left = p;
        r.updateWeight(delta);

        p.parent = r;
    }
  }

updateWeight 只是将权重更新到根目录:

   void updateWeight(int delta) {
        weight += delta;
        Entry<K, V> p = parent;
        while (p != null) {
            p.weight += delta;
            p = p.parent;
        }
    }

当我们需要按索引查找元素时,这里是使用权重的实现:

public K exactKey(int index) {
    if (index < 0 || index > size() - 1) {
        throw new ArrayIndexOutOfBoundsException();
    }
    return getExactKey(root, index);
}

private K getExactKey(Entry<K, V> e, int index) {
    if (e.left == null && index == 0) {
        return e.key;
    }
    if (e.left == null && e.right == null) {
        return e.key;
    }
    if (e.left != null && e.left.weight > index) {
        return getExactKey(e.left, index);
    }
    if (e.left != null && e.left.weight == index) {
        return e.key;
    }
    return getExactKey(e.right, index - (e.left == null ? 0 : e.left.weight) - 1);
}

查找键的索引也非常方便:

    public int keyIndex(K key) {
    if (key == null) {
        throw new NullPointerException();
    }
    Entry<K, V> e = getEntry(key);
    if (e == null) {
        throw new NullPointerException();
    }
    if (e == root) {
        return getWeight(e) - getWeight(e.right) - 1;//index to return
    }
    int index = 0;
    int cmp;
    index += getWeight(e.left);
    
    Entry<K, V> p = e.parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        while (p != null) {
            cmp = cpr.compare(key, p.key);
            if (cmp > 0) {
                index += getWeight(p.left) + 1;
            }
            p = p.parent;
        }
    } else {
        Comparable<? super K> k = (Comparable<? super K>) key;
        while (p != null) {
            if (k.compareTo(p.key) > 0) {
                index += getWeight(p.left) + 1;
            }
            p = p.parent;
        }
    }
    return index;
}

您可以在以下位置找到这项工作的结果 https://github.com/geniot/indexed-tree-map

TreeSet/TreeMap(以及它们在 indexed-tree-map 项目中的索引对应项)不允许重复键,您可以将 1 个键用于值数组。如果您需要具有重复项的 SortedSet,请使用将值作为数组的 TreeMap。我会这样做的。

5赞 Emanuel Moecklin 7/22/2016 #11

Aioobe的方法是要走的路。不过,我想对他的解决方案提出以下改进建议。

class SortedList<T> extends ArrayList<T> {

    public void insertSorted(T value) {
        int insertPoint = insertPoint(value);
        add(insertPoint, value);
    }

    /**
     * @return The insert point for a new value. If the value is found the insert point can be any
     * of the possible positions that keeps the collection sorted (.33 or 3.3 or 33.).
     */
    private int insertPoint(T key) {
        int low = 0;
        int high = size() - 1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            Comparable<? super T> midVal = (Comparable<T>) get(mid);
            int cmp = midVal.compareTo(key);

            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else {
                return mid; // key found
            }
        }

        return low;  // key not found
    }
}

Aioobe 的解决方案在使用大型列表时会变得非常慢。利用列表排序的事实,我们可以使用二进制搜索找到新值的插入点。

我也会使用组合而不是继承,类似于

SortedList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

评论

0赞 aioobe 9/25/2022
这个解决方案可能优于我提出的解决方案,但只有一个恒定的因子。复杂性是一样的:在我们的两个解决方案中都以 O(n) 运行。insertSorted
0赞 aioobe 9/25/2022
实际上已经更新了我的答案。如果我错了,请纠正我,但我认为你所做的就是吗?Collections.binarySearchinsertPoint
1赞 user13750620 6/15/2020 #12

只需创建一个新类,如下所示:

public class SortedList<T> extends ArrayList<T> {

private final Comparator<? super T> comparator;

public SortedList() {
    super();
    this.comparator = null;
}

public SortedList(Comparator<T> comparator) {
    super();
    this.comparator = comparator;
}

@Override
public boolean add(T item) {
    int index = comparator == null ? Collections.binarySearch((List<? extends Comparable<? super T>>)this, item) :
            Collections.binarySearch(this, item, comparator);
    if (index < 0) {
        index = index * -1 - 2;
    }
    super.add(index+1, item);
    return true;
}

@Override
public void add(int index, T item) {
    throw new UnsupportedOperationException("'add' with an index is not supported in SortedArrayList");
}

@Override
public boolean addAll(Collection<? extends T> items) {
    boolean allAdded = true;
    for (T item : items) {
        allAdded = allAdded && add(item);
    }
    return allAdded;
}

@Override
public boolean addAll(int index, Collection<? extends T> items) {
    throw new UnsupportedOperationException("'addAll' with an index is not supported in SortedArrayList");
}

}

你可以像这样测试它:

    List<Integer> list = new SortedArrayList<>((Integer i1, Integer i2) -> i1.compareTo(i2));
    for (Integer i : Arrays.asList(4, 7, 3, 8, 9, 25, 20, 23, 52, 3)) {
        list.add(i);
    }
    System.out.println(list);
0赞 pilus 12/16/2022 #13

以下方法可用于打印 String 对象的 LinkedList 的元素。

该方法接受 LinkedList 对象作为输入,并将列表的每个元素(用空格分隔)打印到控制台。 为了使输出更具可读性,该方法还在列表前后添加换行符。

public static void printList(LinkedList<String> list) {
    System.out.println("\nList is: ");
    for (String element : list) {
        System.out.print(element + " ");
    }
    System.out.println();
}