Вибір випадкового елемента з набору


180

Як вибрати випадковий елемент із множини? Мені особливо цікаво вибрати випадковий елемент з HashSet або LinkedHashSet на Java. Рішення для інших мов також вітаються.


5
Ви повинні вказати деякі умови, щоб побачити, чи дійсно це те, що ви хочете. - Яким чином ви можете вибирати випадковий елемент? - Чи потрібно зберігати дані в HashSet або LinkedHashSet, вони також не мають довільного доступу. - Хеш набір великий? Клавіші невеликі?
Девід Неме

Відповіді:


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

94
Якщо myHashSet великий, то це буде досить повільним рішенням, оскільки в середньому потрібні (n / 2) ітерації для пошуку випадкового об'єкта.
Даніель

6
якщо ваші дані в хеш-наборі, вам потрібен O ​​(n) час. Нічого не обійти, якщо ви просто вибираєте один елемент, а дані зберігаються в HashSet.
Девід Неме

8
@David Nehme: Це недолік у специфікації HashSet на Java. Для C ++ типово мати можливість безпосередньо отримувати доступ до відер, що складають хешсет, що дозволяє нам більш ефективно вибирати випадкові елементи. Якщо в Java потрібні випадкові елементи, можливо, варто визначити спеціальний хеш-набір, який дозволяє користувачеві зазирнути під капот. Докладніше про це див. У [boost's docs] [1]. [1] boost.org/doc/libs/1_43_0/doc/html/unordered/buckets.html
Аарон Макдейд

11
Якщо набір не змінюється через декілька доступів, ви можете скопіювати його у масив та отримати доступ до O (1). Просто використовуйте myHashSet.toArray ()
ykaganovich

2
@ykaganovich не погіршив би це, оскільки набір повинен бути скопійований у новий масив? docs.oracle.com/javase/7/docs/api/java/util/… "цей метод повинен виділити новий масив, навіть якщо ця колекція підтримується масивом"
anton1980

73

Ви дещо пов’язані Чи знаєте ви:

Існують корисні методи java.util.Collectionsдля переміщення цілих колекцій: Collections.shuffle(List<?>)і Collections.shuffle(List<?> list, Random rnd).


Дивовижно! Це не перекреслено ніде в java doc! Як і Python's random.shuffle ()
smci

25
Але це працює лише зі списками, тобто структурами, які мають функцію .get ().
bourbaki4481472

4
@ bourbaki4481472 абсолютно правильно. Це працює лише для тих колекцій, що розширюють Listінтерфейс, а не Setінтерфейс, обговорений ОП.
Томас

31

Швидке рішення для Java, використовуючи a ArrayListі a HashMap: [елемент -> індекс].

Мотивація: мені знадобився набір елементів із 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()є напівбезпечними. Також ОП нічого не сказала про одночасність, тому це правдива і хороша відповідь.
TWiStErRob

Повернений сюди ітератор не повинен мати змогу видаляти елементи з dtaцього (наприклад, це можна досягти, наприклад, через гуаву Iterators.unmodifiableIterator). В іншому випадку реалізація за замовчуванням, наприклад, RemoveAll і retainAll в AbstractSet та його батьки, які працюють з цим ітератором, зіпсують вашу справу RandomSet!
вівторок

Приємне рішення. Насправді ви можете використовувати дерево, якщо кожен вузол містить кількість вузлів у піддерев’ї, котрі його корені. Потім обчисліть випадкову реальність у 0..1 та прийміть зважене тристоронне рішення (виберіть поточний вузол або спустіться у підлівець ліворуч або праворуч) на кожному вузлі на основі підрахунку вузлів. Але imo ваше рішення набагато приємніше.
Гена

30

Це швидше, ніж для кожного циклу у прийнятій відповіді:

int index = rand.nextInt(set.size());
Iterator<Object> iter = set.iterator();
for (int i = 0; i < index; i++) {
    iter.next();
}
return iter.next();

Конструкція for- Iterator.hasNext()every запускає кожен цикл, але, оскільки 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();
    }
}

15

Якщо ви хочете зробити це на Java, вам слід розглянути можливість копіювання елементів у якусь колекцію випадкового доступу (наприклад, ArrayList). Тому що, якщо ваш набір невеликий, доступ до обраного елемента буде дорогим (O (n) замість O (1)). [ed: список копій також O (n)]

Крім того, ви можете шукати іншу програму Set, яка б більш відповідала вашим вимогам. ListOrderedSet з Commons Колекції виглядає багатообіцяючим.


8
Копіювання до списку коштуватиме O (n) у часі, а також використовувати O (n) пам'ять, то чому б це було кращим вибором, ніж отримання з карти безпосередньо?
mdma

12
Це залежить від того, скільки разів ви хочете вибрати з набору. Копія - це одноразова операція, і тоді ви можете вибрати з набору стільки разів, скільки вам потрібно. Якщо ви вибираєте лише один елемент, то так, копія не покращує справи.
Ден Дайер

Це лише разова операція, якщо ви хочете мати можливість вибирати з повторенням. Якщо ви хочете, щоб вибраний елемент був видалений із набору, ви повернетесь до O (n).
TurnipEntropy


9

На 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())]);
}

11
Ваша відповідь працює, але це не дуже ефективно через частину set.toArray ().
Підказки менше

12
вам слід перемістити toArray на зовнішню петлю.
Девід Неме

8
List asList = new ArrayList(mySet);
Collections.shuffle(asList);
return asList.get(0);

21
Це неефективно. Ваш конструктор ArrayList викликає .toArray () на наданому наборі. ToArray (у більшості, якщо не у всіх стандартних реалізаціях колекції) ітералізує всю колекцію, заповнюючи масив, як йде. Потім ви перетасуєте список, який замінює кожен елемент випадковим елементом. Вам буде набагато краще просто перебирати набір до випадкового елемента.
Кріс Боде

4

Це ідентично прийнятій відповіді (Кхот), але з непотрібними sizeта iзмінними видалено.

    int random = new Random().nextInt(myhashSet.size());
    for(Object obj : myhashSet) {
        if (random-- == 0) {
            return obj;
        }
    }

Хоча усуваючи дві вищезгадані змінні, вищевказане рішення все ще залишається випадковим, оскільки ми покладаємось на випадкові (починаючи з випадково вибраного індексу), щоб зменшити себе 0над кожною ітерацією.


1
Третя лінія також може бути if (--random < 0) {, куди randomдоходить -1.
Сальвадор

3

Рішення Clojure:

(defn pick-random [set] (let [sq (seq set)] (nth sq (rand-int (count sq)))))

1
Це рішення також є лінійним, оскільки, щоб отримати nthелемент, ви також повинні пройти його seq.
Бруно Кім

1
Він також лінійний, оскільки добре вписується в один рядок: D
Krzysztof Wolny

2

Perl 5

@hash_keys = (keys %hash);
$rand = int(rand(@hash_keys));
print $hash{$hash_keys[$rand]};

Ось один із способів зробити це.


2

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

2

Наведене вище рішення говорить про затримку, але не гарантує однакової ймовірності вибору кожного індексу.
Якщо це потрібно врахувати, спробуйте відібрати пробу водойми. http://en.wikipedia.org/wiki/Reservoir_sampling .
Collections.shuffle () (як пропонують мало хто) використовує один такий алгоритм.


1

Оскільки ви сказали, що "Рішення для інших мов також вітаються", ось версія для Python:

>>> import random
>>> random.choice([1,2,3,4,5,6])
3
>>> random.choice([1,2,3,4,5,6])
4

3
Тільки, [1,2,3,4,5,6] - це не набір, а список, оскільки він не підтримує такі речі, як швидкі пошуки.
Томас Ейл

Ви все ще можете зробити: >>> random.choice (список (набір (діапазон (5))))) >>> 4
SapphireSun

1

Ви не можете просто отримати розмір / довжину набору / масиву, генерувати випадкове число між 0 та розміром / довжиною, а потім викликати елемент, індекс якого відповідає цьому числу? У HashSet є метод .size (), я впевнений.

У psuedocode -

function randFromSet(target){
 var targetLength:uint = target.length()
 var randomIndex:uint = random(0,targetLength);
 return target[randomIndex];
}

Це працює лише в тому випадку, якщо відповідний контейнер підтримує випадковий пошук індексу. У багатьох реалізаціях контейнерів немає (наприклад, хеш-таблиці, двійкові дерева, пов'язані списки).
Девід Хейлі

1

PHP, якщо "set" - це масив:

$foo = array("alpha", "bravo", "charlie");
$index = array_rand($foo);
$val = $foo[$index];

Функції Mersenne Twister є кращими, але в PHP немає еквівалента MT-масиву array_rand.


Більшість реалізацій набору не мають оператора get (i) або індексації, тому я припускаю, що саме тому OP вказав свій набір
DownloadPizza

1

Значок має тип набору та оператор випадкових елементів, одинаковий "?", Так що вираз

? set( [1, 2, 3, 4, 5] )

виведе випадкове число між 1 і 5.

Випадкове насіння ініціалізується до 0 при запуску програми, щоб отримати різні результати при кожному використанні запуску randomize()


1

В 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"]);

схоже, що вони зволікають через те, що шалений словник Java (або так званий LinkedHashSet, як би там не було) не може бути "випадковим доступом" (до якого, напевно, доступний ключ). Лайно на Java змушує мене так сміятися
Федеріко Берасатегуї

1

Рішення 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();

Я віддаю перевагу другу альтернативу. :-)
marcospereira

ooh, мені подобається розширити додавання нового методу масиву!
матовий lohkamp

1

У ліпс

(defun pick-random (set)
       (nth (random (length set)) set))

Це працює лише для списків, правда? З ELTним може працювати будь-яка послідовність.
Кен

1

Математика:

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


1

Для задоволення я написав 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;
        }
    }
}

1
Саме те, що я думав! Найкраща відповідь!
ммм

Насправді, повертаючись до цього, я думаю, це не зовсім рівномірно, якщо в хешмапі багато зіткнень, і ми робимо багато запитів. Це тому, що в хешмапі Java використовується відра / ланцюжок, і цей код завжди поверне перший елемент у конкретному відрі. Ми все ще єдині щодо випадковості хеш-функції.
Thomas Ahle

1

Найпростіше з Java 8:

outbound.stream().skip(n % outbound.size()).findFirst().get()

де nвипадкове ціле число. Звичайно, це менше продуктивності, ніж уfor(elem: Col)


1

З Гуавою ми можемо зробити трохи краще, ніж відповідь Хота:

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

0

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

0

Ви також можете передати набір для масиву використовувати масив, він, ймовірно, буде працювати в малих масштабах, я бачу, що цикл for найбільш проголосований відповідь - це O (n) у будь-якому випадку

Object[] arr = set.toArray();

int v = (int) arr[rnd.nextInt(arr.length)];

0

Якщо ви просто хочете вибрати "будь-який" об'єкт із Set, без жодних гарантій на випадковість, найпростіше - це взяти перший, повернутий ітератором.

    Set<Integer> s = ...
    Iterator<Integer> it = s.iterator();
    if(it.hasNext()){
        Integer i = it.next();
        // i is a "random" object from set
    }

1
Це не буде випадковим вибором. Уявіть виконання однієї і тієї ж операції над одним і тим же набором кілька разів. Я думаю, що порядок буде таким самим.
Menezes Sousa

0

Загальне рішення, що використовує відповідь Хота як вихідний пункт.

/**
 * @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;
}

0

На жаль, це неможливо зробити ефективно (краще, ніж 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] як вихід, тому шви розподілу хороші.

Я боровся з тією самою проблемою і для себе, і я ще не вирішив, що приріст продуктивності цього більш ефективного вибору вартий витрат на використання колекції на основі пітона. Я, звичайно, міг би вдосконалити та перекласти це на C, але це для мене сьогодні занадто багато роботи :)


1
Причина, на яку я думаю, що це не реалізовано у двійковому дереві, полягає в тому, що такий метод не збирає елементи рівномірно. Оскільки це вузли без дітей ліворуч / праворуч, може статися ситуація, коли на лівій дитині міститься більше предметів, ніж у правої дитини (або навпаки), це зробить вибір предмета у правої (або лівої) дитини більш імовірним.
Віллем Ван Онсем

1
@CommuSoft: Ось чому я зберігаю розмір кожного піддерева, тому я можу вибрати свої ймовірності на основі цих.
Томас Ейл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.