Как выбрать случайный элемент из набора? Меня особенно интересует выбор случайного элемента из HashSet или LinkedHashSet в Java. Также приветствуются решения для других языков.




Поскольку вы сказали: «Также приветствуются решения для других языков», вот версия для Python:
>>> import random
>>> random.choice([1,2,3,4,5,6])
3
>>> random.choice([1,2,3,4,5,6])
4
Только [1,2,3,4,5,6] - это не набор, а список, поскольку он не поддерживает такие вещи, как быстрый поиск.
Вы все еще можете: >>> random.choice (list (set (range (5)))) >>> 4 Не идеально, но подойдет, если вам это абсолютно необходимо.
В Java:
Set<Integer> set = new LinkedHashSet<Integer>(3);
set.add(1);
set.add(2);
set.add(3);
Random rand = new Random(System.currentTimeMillis());
int[] setArray = (int[]) set.toArray();
for (int i = 0; i < 10; ++i) {
System.out.println(setArray[rand.nextInt(set.size())]);
}
Ваш ответ работает, но он не очень эффективен из-за части set.toArray ().
вы должны переместить toArray за пределы цикла.
int size = myHashSet.size();
int item = new Random().nextInt(size); // In real life, the Random object should be rather more shared than this
int i = 0;
for(Object obj : myhashSet)
{
if (i == item)
return obj;
i++;
}
Если myHashSet большой, то это будет довольно медленное решение, поскольку в среднем для поиска случайного объекта потребуется (n / 2) итераций.
если ваши данные находятся в наборе хешей, вам понадобится время O (n). Нет никакого способа обойти это, если вы просто выбираете один элемент, а данные хранятся в HashSet.
@David Nehme: Это недостаток спецификации HashSet в Java. В C++ типично иметь возможность напрямую обращаться к сегментам, составляющим хэш-набор, что позволяет нам более эффективно выбирать случайные элементы. Если в Java необходимы случайные элементы, возможно, стоит определить собственный набор хэшей, который позволит пользователю заглянуть под капот. См. [Boost's docs] [1] для получения дополнительной информации. [1] boost.org/doc/libs/1_43_0/doc/html/unordered/buckets.html
Если набор не изменяется при многократном доступе, вы можете скопировать его в массив, а затем получить доступ к O (1). Просто используйте myHashSet.toArray ()
@AaronMcDaid Даже тогда вам придется учитывать пустые ведра.
@ykaganovich, не усугубит ли это положение, ведь набор нужно было бы скопировать в новый массив? docs.oracle.com/javase/7/docs/api/java/util/… "этот метод должен выделить новый массив, даже если эта коллекция поддерживается массивом"
@ anton1980 См. обсуждение под ответом Дэна Дайера stackoverflow.com/a/129386/10026
Разве вы не можете просто получить размер / длину набора / массива, сгенерировать случайное число от 0 до размера / длины, а затем вызвать элемент, индекс которого соответствует этому числу? Я уверен, что в HashSet есть метод .size ().
В псевдокоде -
function randFromSet(target){
var targetLength:uint = target.length()
var randomIndex:uint = random(0,targetLength);
return target[randomIndex];
}
Это работает только в том случае, если рассматриваемый контейнер поддерживает случайный поиск по индексу. Многие реализации контейнеров этого не делают (например, хеш-таблицы, двоичные деревья, связанные списки).
PHP, предполагая, что "set" - это массив:
$foo = array("alpha", "bravo", "charlie");
$index = array_rand($foo);
$val = $foo[$index];
Функции Mersenne Twister лучше, но в PHP нет эквивалента array_rand для MT.
Большинство реализаций набора не имеют оператора get (i) или индексации, поэтому id предполагает, что именно поэтому OP указал свой набор
PHP, используя MT:
$items_array = array("alpha", "bravo", "charlie");
$last_pos = count($items_array) - 1;
$random_pos = mt_rand(0, $last_pos);
$random_item = $items_array[$random_pos];
Решение Javascript;)
function choose (set) {
return set[Math.floor(Math.random() * set.length)];
}
var set = [1, 2, 3, 4], rand = choose (set);
Или альтернативно:
Array.prototype.choose = function () {
return this[Math.floor(Math.random() * this.length)];
};
[1, 2, 3, 4].choose();
Я предпочитаю вторую альтернативу. :-)
ох, мне нравится расширять добавление нового метода массива!
Несколько связанное с этим Знаете ли вы:
В java.util.Collections есть полезные методы для перетасовки целых коллекций: Collections.shuffle(List<?>) и Collections.shuffle(List<?> list, Random rnd).
Потрясающий! На это нигде в java-документе нет перекрестных ссылок! Нравится Python random.shuffle ()
Но это работает только со списками, то есть структурами, имеющими функцию .get ().
@ bourbaki4481472 абсолютно правильно. Это работает только для тех коллекций, которые расширяют интерфейс List, но не для интерфейса Set, обсуждаемого OP.
Однако OP желает выбрать элемент а (как я полагаю). Перемешивание всего списка (а также перенос всех элементов в наборе в список) очень дорого обходится для этого одного элемента ... Если у вас есть список, вы используете List.get(new Random().nextInt(size)).
Perl 5
@hash_keys = (keys %hash);
$rand = int(rand(@hash_keys));
print $hash{$hash_keys[$rand]};
Вот один из способов сделать это.
Значок имеет тип набора и оператор случайного элемента, унарный "?", Поэтому выражение
? set( [1, 2, 3, 4, 5] )
выдаст случайное число от 1 до 5.
Случайное начальное число инициализируется 0 при запуске программы, поэтому для получения разных результатов при каждом запуске используйте randomize().
В C#
Random random = new Random((int)DateTime.Now.Ticks);
OrderedDictionary od = new OrderedDictionary();
od.Add("abc", 1);
od.Add("def", 2);
od.Add("ghi", 3);
od.Add("jkl", 4);
int randomIndex = random.Next(od.Count);
Console.WriteLine(od[randomIndex]);
// Can access via index or key value:
Console.WriteLine(od[1]);
Console.WriteLine(od["def"]);
В шепелявке
(defun pick-random (set)
(nth (random (length set)) set))
Это работает только для списков, верно? С ELT он может работать для любой последовательности.
Если вы хотите сделать это на Java, вам следует подумать о копировании элементов в какую-то коллекцию с произвольным доступом (например, ArrayList). Потому что, если ваш набор не маленький, доступ к выбранному элементу будет дорогостоящим (O (n) вместо O (1)). [ed: список копий тоже O (n)]
В качестве альтернативы вы можете поискать другую реализацию Set, которая более точно соответствует вашим требованиям. ListOrderedSet от Commons Collections выглядит многообещающим.
Копирование в список будет стоить O (n) по времени, а также использовать O (n) памяти, так почему это лучший выбор, чем выборка с карты напрямую?
Это зависит от того, сколько раз вы хотите выбрать из набора. Копирование - это одноразовая операция, и затем вы можете выбирать из набора столько раз, сколько вам нужно. Если вы выбираете только один элемент, то да, копия не ускоряет работу.
Это только одноразовая операция, если вы хотите иметь возможность выбирать с повторением. Если вы хотите, чтобы выбранный элемент был удален из набора, вы вернетесь к O (n).
Решение Clojure:
(defn pick-random [set] (let [sq (seq set)] (nth sq (rand-int (count sq)))))
Это решение также является линейным, потому что для получения элемента nth вы также должны пройти через seq.
Это также линейно, так как прекрасно вписывается в одну строку: D
К сожалению, это невозможно сделать эффективно (лучше, чем O (n)) ни в одном из контейнеров набора стандартной библиотеки.
Это странно, поскольку очень легко добавить функцию рандомизированного выбора как к хэш-наборам, так и к двоичным наборам. В не разреженном наборе хешей вы можете пробовать случайные записи, пока не получите результат. Для двоичного дерева вы можете произвольно выбирать между левым или правым поддеревом с максимумом O (log2) шагов. Я реализовал демонстрацию следующего ниже:
import random
class Node:
def __init__(self, object):
self.object = object
self.value = hash(object)
self.size = 1
self.a = self.b = None
class RandomSet:
def __init__(self):
self.top = None
def add(self, object):
""" Add any hashable object to the set.
Notice: In this simple implementation you shouldn't add two
identical items. """
new = Node(object)
if not self.top: self.top = new
else: self._recursiveAdd(self.top, new)
def _recursiveAdd(self, top, new):
top.size += 1
if new.value < top.value:
if not top.a: top.a = new
else: self._recursiveAdd(top.a, new)
else:
if not top.b: top.b = new
else: self._recursiveAdd(top.b, new)
def pickRandom(self):
""" Pick a random item in O(log2) time.
Does a maximum of O(log2) calls to random as well. """
return self._recursivePickRandom(self.top)
def _recursivePickRandom(self, top):
r = random.randrange(top.size)
if r == 0: return top.object
elif top.a and r <= top.a.size: return self._recursivePickRandom(top.a)
return self._recursivePickRandom(top.b)
if __name__ == '__main__':
s = RandomSet()
for i in [5,3,7,1,4,6,9,2,8,0]:
s.add(i)
dists = [0]*10
for i in xrange(10000):
dists[s.pickRandom()] += 1
print dists
На выходе я получил [995, 975, 971, 995, 1057, 1004, 966, 1052, 984, 1001], так что распределение швов хорошее.
Я боролся с той же проблемой для себя, и я еще не решил, стоит ли повышение производительности этого более эффективного выбора на накладные расходы, связанные с использованием коллекции на основе Python. Я, конечно, мог бы доработать его и перевести на C, но сегодня для меня это слишком много работы :)
Причина, по которой я думаю, что это не реализовано в двоичном дереве, заключается в том, что такой метод не будет выбирать элементы равномерно. Поскольку это узлы без левых / правых дочерних элементов, может возникнуть ситуация, когда левый дочерний элемент содержит больше элементов, чем правый дочерний элемент (или наоборот), это сделает выбор элемента в правом (или левом) дочернем элементе более вероятным.
@CommuSoft: Вот почему я храню размер каждого поддерева, поэтому я могу выбирать свои вероятности на основе них.
C++. Это должно быть достаточно быстрым, поскольку не требует повторения по всему набору или его сортировки. Это должно работать из коробки с большинством современных компиляторов, если они поддерживают tr1. Если нет, возможно, вам придется использовать Boost.
Документы Boost могут помочь здесь объяснить это, даже если вы не используете Boost.
Уловка состоит в том, чтобы использовать тот факт, что данные были разделены на сегменты, и быстро идентифицировать случайно выбранный сегмент (с соответствующей вероятностью).
//#include <boost/unordered_set.hpp>
//using namespace boost;
#include <tr1/unordered_set>
using namespace std::tr1;
#include <iostream>
#include <stdlib.h>
#include <assert.h>
using namespace std;
int main() {
unordered_set<int> u;
u.max_load_factor(40);
for (int i=0; i<40; i++) {
u.insert(i);
cout << ' ' << i;
}
cout << endl;
cout << "Number of buckets: " << u.bucket_count() << endl;
for(size_t b=0; b<u.bucket_count(); b++)
cout << "Bucket " << b << " has " << u.bucket_size(b) << " elements. " << endl;
for(size_t i=0; i<20; i++) {
size_t x = rand() % u.size();
cout << "we'll quickly get the " << x << "th item in the unordered set. ";
size_t b;
for(b=0; b<u.bucket_count(); b++) {
if (x < u.bucket_size(b)) {
break;
} else
x -= u.bucket_size(b);
}
cout << "it'll be in the " << b << "th bucket at offset " << x << ". ";
unordered_set<int>::const_local_iterator l = u.begin(b);
while(x>0) {
l++;
assert(l!=u.end(b));
x--;
}
cout << "random item is " << *l << ". ";
cout << endl;
}
}
прочитав эту ветку, лучшее, что я мог написать, это:
static Random random = new Random(System.currentTimeMillis());
public static <T> T randomChoice(T[] choices)
{
int index = random.nextInt(choices.length);
return choices[index];
}
Вопрос касается наборов, а не массивов. Также нет необходимости заполнять Random текущим временем; new Random() из коробки возвращает правильно засеянный экземпляр.
Быстрое решение для Java с использованием ArrayList и HashMap: [element -> index].
Мотивация: мне нужен был набор предметов со свойствами RandomAccess, особенно чтобы выбрать случайный предмет из набора (см. Метод pollRandom). Случайная навигация в двоичном дереве не точна: деревья не сбалансированы идеально, что не приведет к равномерному распределению.
public class RandomSet<E> extends AbstractSet<E> {
List<E> dta = new ArrayList<E>();
Map<E, Integer> idx = new HashMap<E, Integer>();
public RandomSet() {
}
public RandomSet(Collection<E> items) {
for (E item : items) {
idx.put(item, dta.size());
dta.add(item);
}
}
@Override
public boolean add(E item) {
if (idx.containsKey(item)) {
return false;
}
idx.put(item, dta.size());
dta.add(item);
return true;
}
/**
* Override element at position <code>id</code> with last element.
* @param id
*/
public E removeAt(int id) {
if (id >= dta.size()) {
return null;
}
E res = dta.get(id);
idx.remove(res);
E last = dta.remove(dta.size() - 1);
// skip filling the hole if last is removed
if (id < dta.size()) {
idx.put(last, id);
dta.set(id, last);
}
return res;
}
@Override
public boolean remove(Object item) {
@SuppressWarnings(value = "element-type-mismatch")
Integer id = idx.get(item);
if (id == null) {
return false;
}
removeAt(id);
return true;
}
public E get(int i) {
return dta.get(i);
}
public E pollRandom(Random rnd) {
if (dta.isEmpty()) {
return null;
}
int id = rnd.nextInt(dta.size());
return removeAt(id);
}
@Override
public int size() {
return dta.size();
}
@Override
public Iterator<E> iterator() {
return dta.iterator();
}
}
Что ж, это сработает, но вопрос был в интерфейсе Set. Это решение заставляет пользователей иметь конкретные ссылки на типы RandomSet.
Мне очень нравится это решение, но оно не является потокобезопасным, могут возникать неточности между картой и списком, поэтому я бы добавил несколько синхронизированных блоков.
@KonstantinosChalkias, встроенные коллекции также не являются потокобезопасными. Только те, у которых есть название Concurrent, действительно безопасны, те, которые завернуты в Collections.synchronized(), являются полубезопасными. Кроме того, OP ничего не сказал о параллелизме, так что это правильный и хороший ответ.
Итератор, возвращенный здесь, не должен иметь возможность удалять элементы из dta (это может быть достигнуто, например, с помощью Iterators.unmodifiableIterator guava). В противном случае реализации по умолчанию, например, removeAll и keepAll в AbstractSet и его родителях, работающих с этим итератором, испортят ваш RandomSet!
Хорошее решение. Фактически вы можете использовать дерево, если каждый узел содержит количество узлов в поддереве, корнем которого он является. Затем вычислите случайное вещественное число в 0..1 и примите взвешенное трехстороннее решение (выберите текущий узел или спуститесь в левое или правое поддерево) на каждом узле на основе количества узлов. Но, как мне кажется, ваше решение намного лучше.
В системе Mathematica:
a = {1, 2, 3, 4, 5}
a[[ ⌈ Length[a] Random[] ⌉ ]]
Или, в последних версиях, просто:
RandomChoice[a]
Это получило отрицательное голосование, возможно, из-за отсутствия объяснения, поэтому вот одно из них:
Random[] генерирует псевдослучайное число с плавающей запятой между 0 и 1. Оно умножается на длину списка, а затем используется функция потолка для округления до следующего целого числа. Затем этот индекс извлекается из a.
Поскольку функции хеш-таблицы часто выполняются с помощью правил в Mathematica, а правила хранятся в списках, можно использовать:
a = {"Badger" -> 5, "Bird" -> 1, "Fox" -> 3, "Frog" -> 2, "Wolf" -> 4};
List asList = new ArrayList(mySet);
Collections.shuffle(asList);
return asList.get(0);
Это ужасно неэффективно. Ваш конструктор ArrayList вызывает .toArray () для предоставленного набора. ToArray (в большинстве, если не во всех стандартных реализациях коллекций) выполняет итерацию по всей коллекции, заполняя массив по мере его поступления. Затем вы перемешиваете список, в котором каждый элемент меняет местами случайный элемент. Было бы намного лучше просто перебирать набор до случайного элемента.
Как насчет просто
public static <A> A getRandomElement(Collection<A> c, Random r) {
return new ArrayList<A>(c).get(r.nextInt(c.size()));
}
Ради интереса я написал RandomHashSet, основанный на выборке отклонения. Это немного взломано, поскольку HashMap не позволяет нам напрямую обращаться к своей таблице, но он должен работать нормально.
Он не использует дополнительную память, а время поиска амортизируется за O (1). (Потому что java HashTable плотный).
class RandomHashSet<V> extends AbstractSet<V> {
private Map<Object,V> map = new HashMap<>();
public boolean add(V v) {
return map.put(new WrapKey<V>(v),v) == null;
}
@Override
public Iterator<V> iterator() {
return new Iterator<V>() {
RandKey key = new RandKey();
@Override public boolean hasNext() {
return true;
}
@Override public V next() {
while (true) {
key.next();
V v = map.get(key);
if (v != null)
return v;
}
}
@Override public void remove() {
throw new NotImplementedException();
}
};
}
@Override
public int size() {
return map.size();
}
static class WrapKey<V> {
private V v;
WrapKey(V v) {
this.v = v;
}
@Override public int hashCode() {
return v.hashCode();
}
@Override public boolean equals(Object o) {
if (o instanceof RandKey)
return true;
return v.equals(o);
}
}
static class RandKey {
private Random rand = new Random();
int key = rand.nextInt();
public void next() {
key = rand.nextInt();
}
@Override public int hashCode() {
return key;
}
@Override public boolean equals(Object o) {
return true;
}
}
}
Именно то, о чем я думал! Лучший ответ!
На самом деле, возвращаясь к этому, я предполагаю, что это не совсем единообразно, если хэш-карта имеет много коллизий и мы выполняем много запросов. Это связано с тем, что хэш-карта java использует сегменты / цепочки, и этот код всегда будет возвращать первый элемент в конкретном сегменте. Однако мы по-прежнему едины в случайности хэш-функции.
Это быстрее, чем цикл for-each в принятом ответе:
int index = rand.nextInt(set.size());
Iterator<Object> iter = set.iterator();
for (int i = 0; i < index; i++) {
iter.next();
}
return iter.next();
Конструкция for-each вызывает Iterator.hasNext() в каждом цикле, но, начиная с index < set.size(), эта проверка не требует дополнительных затрат. Я видел прирост скорости на 10-20%, но YMMV. (Кроме того, это компилируется без добавления дополнительного оператора возврата.)
Обратите внимание, что этот код (и большинство других ответов) можно применить к любой коллекции, а не только к Set. В форме универсального метода:
public static <E> E choice(Collection<? extends E> coll, Random rand) {
if (coll.size() == 0) {
return null; // or throw IAE, if you prefer
}
int index = rand.nextInt(coll.size());
if (coll instanceof List) { // optimization
return ((List<? extends E>) coll).get(index);
} else {
Iterator<? extends E> iter = coll.iterator();
for (int i = 0; i < index; i++) {
iter.next();
}
return iter.next();
}
}
вы также можете передать набор в массив использовать массив он, вероятно, будет работать в небольшом масштабе, я вижу, что цикл for в ответе с наибольшим количеством голосов - O (n) в любом случае
Object[] arr = set.toArray();
int v = (int) arr[rnd.nextInt(arr.length)];
Это идентично принятому ответу (Хот), но с удаленными ненужными переменными size и i.
int random = new Random().nextInt(myhashSet.size());
for(Object obj : myhashSet) {
if (random-- == 0) {
return obj;
}
}
Несмотря на устранение двух вышеупомянутых переменных, вышеупомянутое решение по-прежнему остается случайным, потому что мы полагаемся на случайное (начиная со случайно выбранного индекса), чтобы уменьшаться в сторону 0 на каждой итерации.
Третьей строкой также может быть if (--random < 0) {, где random достигает -1.
Приведенное выше решение говорит о задержке, но не гарантирует равную вероятность выбора каждого индекса.
Если это необходимо учитывать, попробуйте отбор проб из резервуара. http://en.wikipedia.org/wiki/Reservoir_sampling.
Collections.shuffle () (как было предложено немногими) использует один такой алгоритм.
Если вы действительно хотите выбрать «любой» объект из Set, без каких-либо гарантий случайности, проще всего взять первый, возвращенный итератором.
Set<Integer> s = ...
Iterator<Integer> it = s.iterator();
if (it.hasNext()){
Integer i = it.next();
// i is a "random" object from set
}
Однако это не будет случайный выбор. Представьте, что вы выполняете одну и ту же операцию над одним и тем же набором несколько раз. Думаю, порядок будет такой же.
Самый простой вариант с Java 8:
outbound.stream().skip(n % outbound.size()).findFirst().get()
где n - случайное целое число. Конечно, производительность ниже, чем у for(elem: Col).
Общее решение, использующее ответ Хота в качестве отправной точки.
/**
* @param set a Set in which to look for a random element
* @param <T> generic type of the Set elements
* @return a random element in the Set or null if the set is empty
*/
public <T> T randomElement(Set<T> set) {
int size = set.size();
int item = random.nextInt(size);
int i = 0;
for (T obj : set) {
if (i == item) {
return obj;
}
i++;
}
return null;
}
Если размер набора невелик, это можно сделать с помощью массивов.
int random;
HashSet someSet;
<Type>[] randData;
random = new Random(System.currentTimeMillis).nextInt(someSet.size());
randData = someSet.toArray();
<Type> sResult = randData[random];
С Гуава мы можем сделать немного лучше, чем ответ Хота:
public static E random(Set<E> set) {
int index = random.nextInt(set.size();
if (set instanceof ImmutableSet) {
// ImmutableSet.asList() is O(1), as is .get() on the returned list
return set.asList().get(index);
}
return Iterables.get(set, index);
}
Если вы не возражаете против сторонней библиотеки, в библиотеке Утилиты есть IterableUtils, у которого есть метод randomFrom (Iterable iterable), который будет принимать Set и возвращать из него случайный элемент.
Set<Object> set = new HashSet<>();
set.add(...);
...
Object random = IterableUtils.randomFrom(set);
Он находится в центральном репозитории Maven по адресу:
<dependency>
<groupId>com.github.rkumsher</groupId>
<artifactId>utils</artifactId>
<version>1.3</version>
</dependency>
В Java 8:
static <E> E getRandomSetElement(Set<E> set) {
return set.stream().skip(new Random().nextInt(set.size())).findFirst().orElse(null);
}
Вы должны указать некоторые условия, чтобы увидеть, действительно ли это то, что вы хотите. - Какое время вы собираетесь выбирать случайный элемент? - Нужно ли хранить данные в HashSet или LinkedHashSet, ни один из них не может быть доступен случайным образом. - Хеш-набор большой? Ключи маленькие?