Danh sách mảng được sắp xếp trong Java


85

Tôi bối rối rằng tôi không thể tìm ra câu trả lời nhanh chóng cho điều này. Về cơ bản, tôi đang tìm kiếm một cấu trúc dữ liệu trong Java để triển khai java.util.Listgiao diện, nhưng nó lưu trữ các thành viên của nó theo thứ tự được sắp xếp. Tôi biết rằng bạn có thể sử dụng bình thường ArrayListvà sử dụngCollections.sort() trên nó, nhưng tôi có một tình huống là thỉnh thoảng tôi thêm và thường truy xuất các thành viên từ danh sách của mình và tôi không muốn phải sắp xếp nó mỗi khi tôi truy xuất một thành viên trong trường hợp cái mới đã được thêm vào. Bất cứ ai có thể chỉ cho tôi về một thứ tồn tại trong JDK hoặc thậm chí các thư viện của bên thứ ba?

BIÊN TẬP : Cơ cấu dữ liệu sẽ cần giữ lại các bản sao.

TRẢ LỜI TÓM TẮT : Tôi thấy tất cả những điều này rất thú vị và đã học được rất nhiều. Aioobe nói riêng đáng được nhắc đến vì sự kiên trì của anh ấy trong việc cố gắng đạt được các yêu cầu của tôi ở trên (chủ yếu là triển khai java.util.List được sắp xếp hỗ trợ các bản sao). Tôi đã chấp nhận câu trả lời của anh ấy là chính xác nhất cho những gì tôi đã hỏi và hầu hết đều cho rằng gợi lên ý nghĩa của những gì tôi đang tìm kiếm ngay cả khi những gì tôi hỏi không phải là những gì tôi cần.

Vấn đề với những gì tôi yêu cầu nằm ở chính giao diện Danh sách và khái niệm về các phương thức tùy chọn trong một giao diện. Để trích dẫn javadoc:

Người dùng giao diện này có quyền kiểm soát chính xác vị trí mà mỗi phần tử được chèn trong danh sách.

Việc chèn vào danh sách đã sắp xếp không có quyền kiểm soát chính xác điểm chèn. Sau đó, bạn phải nghĩ cách bạn sẽ xử lý một số phương pháp. Lấy addví dụ:

public boolean add (Object o)

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

Bây giờ bạn đang ở trong tình huống không thoải mái của một trong hai) Phá vỡ hợp đồng và thực hiện một phiên bản được sắp xếp của phần bổ sung 2) Để addthêm một phần tử vào cuối danh sách, phá vỡ thứ tự đã sắp xếp của bạn 3) Bỏ addđi (tùy chọn) bằng cách ném một UnsupportedOperationExceptionvà thực hiện một phương pháp mà thêm mặt hàng theo một thứ tự sắp xếp.

Tùy chọn 3 có lẽ là tốt nhất, nhưng tôi thấy không có lợi khi có một phương thức thêm mà bạn không thể sử dụng và một phương thức Đã phân loại khác không có trong giao diện.

Các giải pháp liên quan khác (không theo thứ tự cụ thể):

  • java.util.PutorQueue có lẽ gần với những gì tôi cần hơn những gì tôi yêu cầu. Hàng đợi không phải là định nghĩa chính xác nhất về tập hợp các đối tượng trong trường hợp của tôi, nhưng về mặt chức năng, nó thực hiện mọi thứ tôi cần.
  • net.sourceforge.nite.util.SortedList . Tuy nhiên, việc triển khai này phá vỡ hợp đồng của giao diện Danh sách bằng cách thực hiện sắp xếp trong add(Object obj)phương thức và kỳ lạ là không có phương pháp nào có hiệu lực add(int index, Object obj). Sự đồng thuận chung cho thấy throw new UnsupportedOperationException()có thể là một lựa chọn tốt hơn trong kịch bản này.
  • Guava's TreeMultiSet Một triển khai bộ hỗ trợ các bản sao
  • ca.odell.glazedlists.SortedList Lớp này có thông báo trước trong javadoc của nó:Warning: This class breaks the contract required by List

4
Nếu bạn thỉnh thoảng chèn và đọc thường xuyên, tại sao không chỉ sắp xếp nó trong khi chèn?
serg

Câu trả lời:


62

Giải pháp tối giản

Đây là một giải pháp "tối thiểu".

class SortedArrayList<T> extends ArrayList<T> {

    @SuppressWarnings("unchecked")
    public void insertSorted(T value) {
        add(value);
        Comparable<T> cmp = (Comparable<T>) value;
        for (int i = size()-1; i > 0 && cmp.compareTo(get(i-1)) < 0; i--)
            Collections.swap(this, i, i-1);
    }
}

Chèn chạy theo thời gian tuyến tính, nhưng đó sẽ là những gì bạn sẽ nhận được khi sử dụng ArrayList (tất cả các phần tử ở bên phải của phần tử được chèn sẽ phải được dịch chuyển theo cách này hay cách khác).

Chèn một thứ gì đó không thể so sánh được trong ClassCastException. (Đây cũng là cách tiếp cận được thực hiện bởi PriorityQueue: Hàng đợi ưu tiên dựa trên thứ tự tự nhiên cũng không cho phép chèn các đối tượng không so sánh được (làm như vậy có thể dẫn đến ClassCastException). )

Ghi đè List.add

Lưu ý rằng việc ghi đè List.add(hoặc List.addAllđối với vấn đề đó) để chèn các phần tử theo kiểu đã được sắp xếp sẽ vi phạm trực tiếp đặc tả giao diện . Những gì bạn có thể làm là ghi đè phương thức này để ném mộtUnsupportedOperationException .

Từ tài liệu của List.add:

boolean add(E e)
    Thêm phần tử được chỉ định vào cuối danh sách này (thao tác tùy chọn).

Lập luận tương tự áp dụng cho cả hai phiên bản của add, cả hai phiên bản của addAllset. (Tất cả đều là thao tác tùy chọn theo giao diện danh sách.)


Một số bài kiểm tra

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);

.... bản in:

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

Một khởi đầu tốt, nhưng việc gọi add hoặc addall sẽ thêm thành viên theo kiểu không được sắp xếp.
Chris Knight

Đúng. Bất cứ điều gì ngoại trừ việc thêm chúng vào danh sách sẽ là một vi phạm trực tiếp của giao diện Danh sách. Xem câu trả lời cập nhật của tôi.
aioobe

@aioobe Điểm tốt. Nhưng một hoạt động Không được hỗ trợ của một phương thức giao diện có phải là một mùi mã không? Cách thích hợp có thể là không mở rộng ArrayList mà thực hiện Danh sách nhưng thậm chí sau đó có thể Danh sách không được dành cho mục đích này. Từ Javadoc cho Danh sách: The user of this interface has precise control over where in the list each element is insertedđây không phải là mô tả tốt nhất để chèn các phần tử theo kiểu được sắp xếp và bạn vẫn phải xử lý add(int index, Object obj)phương pháp giao diện. Những vấn đề này có thể giải thích tại sao Danh sách không được triển khai theo kiểu sắp xếp.
Chris Knight

Vâng, hoạt động là tùy chọn vì một lý do. Tôi sẽ không ngạc nhiên nếu tôi nhận được UnsupportedExceptionOperation khi thực hiện .addtrên SortedArrayList. Có, cùng một lý do áp dụng cho cả hai phiên bản add, cả hai phiên bản addAll và set. (Tất cả trong số đó là hoạt động bắt buộc theo quy định của giao diện danh sách.)
aioobe

Ah, tôi không nhận ra chúng là hoạt động tùy chọn. Cốt truyện dày lên ...;)
Chris Knight

10

7
đó không phải là Danh sách, tức là không có quyền truy cập ngẫu nhiên.
Thilo

1
Đó là một đống ưu tiên dựa trên hàng đợi a không triển khai Danh sách.
zengr

3
Tất nhiên, với một danh sách duy trì thứ tự sắp xếp, các chỉ mục thay đổi liên tục, vì vậy có lẽ không cần truy cập ngẫu nhiên.
Thilo 27/10/10

5
@Qwerky, lưu ý rằng câu trả lời chính xác không phải lúc nào cũng là câu trả lời tốt nhất hoặc câu trả lời mà OP thực sự là sau.
aioobe

3
hàng đợi ưu tiên không cấp thứ tự đã sắp xếp khi lặp lại.
marcorossi

6

Hãy xem SortedList

Lớp này thực hiện một danh sách được sắp xếp. Nó được xây dựng với một bộ so sánh có thể so sánh hai đối tượng và sắp xếp các đối tượng cho phù hợp. Khi bạn thêm một đối tượng vào danh sách, nó sẽ được chèn vào đúng vị trí. Đối tượng bằng theo bộ so sánh, sẽ nằm trong danh sách theo thứ tự mà chúng được thêm vào danh sách này. Chỉ thêm các đối tượng mà trình so sánh có thể so sánh.


Khi danh sách đã chứa các đối tượng bằng nhau theo bộ so sánh, đối tượng mới sẽ được chèn ngay sau các đối tượng khác này.


5
Điều đó có vẻ tốt, nhưng nó cũng có lỗi: không có ghi đè của một trong hai phiên bản addAll, vì vậy danh sách sẽ không được sắp xếp sau khi gọi chúng.
Tom Anderson

3
Và phương thức add "không có hiệu lực". Nó nên ném một UnsupportedOperationException nếu nó không thể được sử dụng.
Thilo 27/10/10

@Tom Anderson @Thilo, đồng ý với cả hai bạn.
Jigar Joshi

1
Thật thú vị, nhưng tôi khá cảnh giác với một người nào đó trong tương lai sử dụng addAll()và nghĩ rằng nó sẽ tất cả các yếu tố theo kiểu được sắp xếp. Đồng ý với UnsupportedOperationException.
Chris Knight

1
Sự phức tạp về thời gian của việc thêm vào danh sách này là gì?
shni1000

6

Bạn có thể thử TreeMultiSet của Guava .

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

+1. Đây là một thư viện tuyệt vời. MultiSet isA collection that supports order-independent equality, like Set, but may have duplicate elements
Shervin Asgari 28/10/10

5

Cách tiếp cận của Aioobe là con đường để đi. Mặc dù vậy, tôi muốn đề xuất cải tiến sau đối với giải pháp của anh ấy.

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
    }
}

Giải pháp của aioobe rất chậm khi sử dụng danh sách lớn. Sử dụng thực tế là danh sách được sắp xếp cho phép chúng ta tìm điểm chèn cho các giá trị mới bằng cách sử dụng tìm kiếm nhị phân.

Tôi cũng sẽ sử dụng bố cục thay vì kế thừa, một cái gì đó dọc theo dòng

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

4

Các danh sách thường bảo toàn thứ tự các mục được thêm vào. Bạn có chắc chắn cần một danh sách , hoặc một tập hợp được sắp xếp (ví dụ TreeSet<E>) sẽ ổn cho bạn? Về cơ bản, bạn có cần phải bảo quản các bản sao không?


2
Cảm ơn Jon, nhưng tôi cần giữ lại các bản sao
Chris Knight


1

Bạn có thể phân lớp ArrayList và gọi Collections.sort (this) sau khi bất kỳ phần tử nào được thêm vào - bạn sẽ cần ghi đè hai phiên bản của add và hai của addAll, để thực hiện việc này.

Hiệu suất sẽ không tốt bằng một triển khai thông minh hơn đã chèn các phần tử vào đúng vị trí, nhưng nó sẽ thực hiện công việc. Nếu hiếm khi bổ sung vào danh sách, chi phí khấu hao cho tất cả các hoạt động trong danh sách phải thấp.


1

Chỉ cần tạo một lớp mới như thế này:

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");
}

}

Bạn có thể kiểm tra nó như thế này:

    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

Tôi nghĩ rằng lựa chọn giữa SortedSets / Lists và các bộ sưu tập có thể sắp xếp 'bình thường' tùy thuộc, cho dù bạn chỉ cần sắp xếp cho mục đích trình bày hay ở hầu hết mọi thời điểm trong thời gian chạy. Sử dụng một tập hợp đã sắp xếp có thể tốn kém hơn nhiều vì việc sắp xếp được thực hiện mỗi khi bạn chèn một phần tử.

Nếu bạn không thể chọn một bộ sưu tập trong JDK, bạn có thể xem qua Bộ sưu tập Apache Commons


0

Vì các triển khai được đề xuất hiện tại thực hiện một danh sách được sắp xếp bằng cách phá vỡ API Bộ sưu tập, có một triển khai riêng của một cây hoặc một cái gì đó tương tự, tôi rất tò mò cách triển khai dựa trên Bản đồ cây sẽ hoạt động như thế nào. (Đặc biệt vì TreeSet cũng dựa trên TreeMap)

Nếu ai đó cũng quan tâm đến điều đó, họ có thể thoải mái xem xét nó:

TreeList

Một phần của thư viện cốt lõi , tất nhiên bạn có thể thêm nó thông qua phụ thuộc Maven. (Giấy phép Apache)

Hiện tại việc triển khai có vẻ như so sánh khá tốt ở cùng cấp độ với ổi SortedMultiSet và TreeList của thư viện Apache Commons.

Nhưng tôi sẽ rất vui nếu hơn chỉ mình tôi kiểm tra việc triển khai để chắc chắn rằng tôi không bỏ lỡ điều gì đó quan trọng.

Trân trọng!


0

Tôi đã từng gặp vấn đề tương tự. Vì vậy, tôi đã lấy mã nguồn của java.util.TreeMap và viết IndexedTreeMap . Nó thực hiện IndexedNavigableMap của riêng tôi :

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

Việc triển khai dựa trên việc cập nhật trọng số nút trong cây đỏ-đen khi nó được thay đổi. Trọng số là số lượng nút con bên dưới một nút nhất định, cộng với một nút tự. Ví dụ: khi xoay cây sang trái:

    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 chỉ cần cập nhật trọng số lên đến thư mục gốc:

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

Và khi chúng ta cần tìm phần tử theo chỉ mục ở đây là việc triển khai sử dụng các trọng số:

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);
}

Cũng rất hữu ích khi tìm chỉ mục của một khóa:

    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;
}

Bạn có thể tìm thấy kết quả của công việc này tại http://code.google.com/p/indexed-tree-map/

TreeSet / TreeMap (cũng như các đối tác được lập chỉ mục của chúng từ dự án bản đồ cây được lập chỉ mục) không cho phép các khóa trùng lặp, bạn có thể sử dụng 1 khóa cho một mảng giá trị. Nếu bạn cần một SortedSet có trùng lặp, hãy sử dụng TreeMap với các giá trị dưới dạng mảng. Tôi sẽ làm điều đó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.