Как вернуть k-й элемент в TreeSet в Java - PullRequest
20 голосов
/ 14 января 2012

Возможно, я не использую правильную структуру данных.Мне нужно использовать набор, но я также хочу эффективно вернуть k-й наименьший элемент.Может ли TreeSet в Java сделать это?Кажется, нет встроенного метода TreeSet для этого.

Пожалуйста, помогите мне.

Ответы [ 7 ]

20 голосов
/ 14 января 2012

Я не верю, что TreeSet имеет метод, который делает это напрямую.Существуют двоичные деревья поиска, которые поддерживают O (log n) произвольного доступа (их иногда называют деревья статистики порядка ), и существует Java-реализации этой структуры данных .Эти структуры обычно реализуются в виде двоичных деревьев поиска, которые хранят информацию в каждом узле, считая, сколько элементов находится слева или справа от узла, поэтому можно выполнить поиск по дереву, чтобы найти соответствующий элемент, спустившись в соответствующее поддерево вкаждый шаг.Классическая книга «Введение в алгоритмы, третье издание» Кормена, Ривеста, Лейссерсона и Стейна исследует эту структуру данных в их главе «Дополнение структур данных», если вам интересно, как реализовать ее самостоятельно.

В качестве альтернативы,Вы можете (в некоторых случаях) использовать метод TreeSet tailSet и модифицированный двоичный поиск, чтобы попытаться найти k-й элемент.В частности, посмотрите на первый и последний элементы TreeSet, затем (если возможно, учитывая содержимое) выберите некоторый элемент, который находится на полпути между ними, и передайте его в качестве аргумента tailSet, чтобы получить представление об элементахнабор после средней точки.Используя количество элементов в tailSet, вы можете решить, нашли ли вы элемент или исследовать левую или правую половинки дерева.Это слегка измененный интерполяционный поиск по дереву, который потенциально может быть быстрым.Однако я не знаю внутренней сложности методов tailSet, так что это может быть на самом деле хуже, чем дерево статистики заказов.Также может произойти сбой, если вы не можете вычислить «среднюю точку» двух элементов, например, если вы храните String s в вашем TreeSet.

Надеюсь, это поможет!

18 голосов
/ 14 января 2012

Вам просто нужно перейти к элементу k .Один из способов сделать это - использовать один из методов Guava * Iterables.get :

T element = Iterables.get(set, k);

Нет встроенного метода для этого, потому чтоSet не является List, и подобные операции на основе индекса обычно зарезервированы для List s.TreeSet больше подходит для таких вещей, как поиск ближайшего содержащегося элемента, который> = какое-то значение.

Одна вещь, которую вы могли бы сделать, если бы самый быстрый доступ к k-му наименьшему элементу былдействительно важно было бы использовать ArrayList вместо TreeSet и обрабатывать вставки путем двоичного поиска точки вставки и либо вставки элемента с этим индексом, либо замены существующего элемента с этим индексом, в зависимости от результата поиска,Тогда вы можете получить k-й наименьший элемент в O (1), просто вызвав get(k).

. Вы даже можете создать реализацию SortedSet, которая обрабатывает все это и добавляет метод get(index), если вы действительнохотел.

7 голосов
/ 14 января 2012

Используйте TreeSet.iterator () , чтобы получить итератор в порядке возрастания и вызвать next() K раз:

// Example for Integers
Iterator<Integer> it = treeSet.iterator();
int i = 0;
Integer current = null;
while(it.hasNext() && i < k) {
   current = it.next();
   i++;
}
6 голосов
/ 11 февраля 2013

У меня была такая же проблема.Поэтому я взял исходный код 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;
    if (e.left != null) {
        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;
}

Результат этой работы можно найти на http://code.google.com/p/indexed-tree-map/. Перемещено в https://github.com/geniot/indexed-tree-map

1 голос
/ 14 января 2012

[Ниже я сокращаю "kth операция поиска наименьшего элемента" как " Kth op."]

Вам необходимо предоставить более подробную информацию.Какие операции предоставит ваша структура данных? K в Kth операция очень мала по сравнению с N , или это может быть что-нибудь?Как часто у вас будут вставки и удаления по сравнению с поиском?Как часто у вас будет Kth поиск самых маленьких элементов по сравнению с поиском?Вы ищете быстрое решение из нескольких строк в библиотеке Java или готовы потратить некоторые усилия на создание собственной структуры данных?

Операции для предоставления могут быть любым подмножеством:

  • LookUp (найти элемент по его ключу; где ключ сопоставим и может бытьчто угодно)

  • Вставить

  • Удалить

  • Kth

Вот несколько возможностей:

  • Если вставок и удалений не будет / будет очень мало, вы можетепросто отсортируйте элементы и используйте массив с O (Log (N)) временем поиска и O (1) для Kth .

  • Если O (Log (N)) для LookUp , Вставить , Удалить и O (k) для Kth op.достаточно хорош, вероятно, самая простая реализация будет Skip Lists.(Статья в Википедии очень хороша, если вам нужно больше подробностей)

  • Если K достаточно мал или Kth операции будут выполняться только после "Фаза вставок и удалений "вы можете хранить самые маленькие элементы K в куче, сортируя после вставок и удалений в течение O (N + k Log k) времени.(Вам также понадобится отдельный хэш для LookUp )

  • Если K является произвольным и O (N) достаточно для операции Kth , вы можете использовать Hash для O (1) поиска времени и использовать алгоритм "односторонней быстрой сортировки" для Kth операции (основная идея - сделать быструю сортировку, но на каждом рекурсивном двоичном делении только на той стороне, которая вам действительно нужна; это дало бы (это грубое упрощение) N (1/2 + 1/4 + 1 /8 + ...) = O (N) ожидаемое время)

  • Вы можете построить расширенную "простую" структуру дерева интервалов, в которой каждый узел хранит количество своих дочерних элементов,так что LookUp , Вставить , Удалить , Kth все вычисления в O (Log N) время как долгопоскольку дерево сбалансировано, но, возможно, это не составит труда реализовать, если вы новичок.

и т. д.и т. д. Множество альтернатив бесконечно в качестве возможных интерпретаций вашего вопроса.

0 голосов
/ 30 мая 2013

Я знаю, что этот вопрос довольно старый, но поскольку TreeSet реализует NavigableSet, у вас есть доступ к методу subSet, который выполняется в постоянное время.

subSet(k, k + 1).first();

Первый вызов () занимает log (n) время, где n - размер исходного набора. Это создает некоторые ненужные объекты, которых можно избежать с помощью более надежной реализации TreeSet, но избегает использования сторонней библиотеки.

0 голосов
/ 14 января 2012

Не могли бы вы использовать ConcurrentSkipListSet и использовать метод toArray ()? ConcurrentSkipListSet сортируется по естественному порядку элементов. Единственное, в чем я не уверен, так это в том, что toArray () - это O (n) или поскольку он поддерживается списком (поддерживаемым массивом, как ArrayList) - это O (1).

Если toArray () = O (1), вы должны быть в состоянии skipList.toArray () [k], чтобы получить k-й наименьший элемент.

...