Рубрики
Без рубрики

Получение набора питания набора в Java

Изучите процесс генерации набора питания данного набора на Java.

Автор оригинала: baeldung.

1. Введение

В этом учебнике мы изумим процесс создания набор питания данного набора в Java.

Как быстрое напоминание, для каждого набора размера n , есть набор мощности размера 2 n . Мы узнаем, как получить его с помощью различных методов.

2. Определение набора мощности

Набор мощности данного набора S является набором всех подмножеов S , в том числе S себя и пустой набор.

Например, для данного набора:

{"APPLE", "ORANGE", "MANGO"}

набор питания:

{
    {},
    {"APPLE"},
    {"ORANGE"},
    {"APPLE", "ORANGE"},
    {"MANGO"},
    {"APPLE", "MANGO"},
    {"ORANGE", "MANGO"},
    {"APPLE", "ORANGE", "MANGO"}
}

Поскольку это также набор подмножего, порядок его внутренних подмножего не имеет важного значение, и они могут появиться в любом порядке:

{
    {},
    {"MANGO"},
    {"ORANGE"},
    {"ORANGE", "MANGO"},
    {"APPLE"},
    {"APPLE", "MANGO"},
    {"APPLE", "ORANGE"},
    {"APPLE", "ORANGE", "MANGO"}
}

3. Библиотека Гуавы

Библиотека Google Guava имеет некоторые полезные утилиты Set, такие как набор питания. Таким образом, мы можем легко использовать его, чтобы получить набор питания данного набора, тоже:

@Test
public void givenSet_WhenGuavaLibraryGeneratePowerSet_ThenItContainsAllSubsets() {
    ImmutableSet set = ImmutableSet.of("APPLE", "ORANGE", "MANGO");
    Set> powerSet = Sets.powerSet(set);
    Assertions.assertEquals((1 << set.size()), powerSet.size());
    MatcherAssert.assertThat(powerSet, Matchers.containsInAnyOrder(
      ImmutableSet.of(),
      ImmutableSet.of("APPLE"),
      ImmutableSet.of("ORANGE"),
      ImmutableSet.of("APPLE", "ORANGE"),
      ImmutableSet.of("MANGO"),
      ImmutableSet.of("APPLE", "MANGO"),
      ImmutableSet.of("ORANGE", "MANGO"),
      ImmutableSet.of("APPLE", "ORANGE", "MANGO")
   ));
}

гуава powerSet внутренне работает над итератор интерфейс в пути, когда следующий подмножество запрашивается, подмножество рассчитывается и возвращается. Таким образом, сложность пространства сводится к O(n) Вместо O(2 n ) .

Но как Гуава достигает этого?

4. Подход к генерации силовых наборов

4.1. Алгоритм

Теперь рассмотрим возможные шаги по созданию алгоритма для этой операции.

Набор питания пустого набора {{}} в котором он содержит только один пустой набор, так что это наш самый простой случай.

Для каждого набора S кроме пустого набора, мы сначала извлекаем один элемент и назовем его элемент . Затем, для остальных элементов набора подмножествоБезуберега , мы вычисляем их набор питания повторяется – и назвать его что-то вроде PowerSet S ubsetСубезЭлемент . Затем, добавив извлеченные элемент для всех наборов в PowerSet S ubsetСубезЭлемент , мы получаем PowerSet S ubsetWithElement.

Теперь, набор питания S является объединением powerSetSubsetСубетоном и powerSetSubsetСЭлемент :

Рассмотрим пример стека рекурсивного набора питания для данного набора “ЯБЛОКО”, “ОРАНЖЕВЫЙ”, “МАНГО” .

Для улучшения читаемости изображения мы используем короткие формы имен: P означает функцию набора питания и “А”, “О”, “М” являются короткими формами “ЯБЛОКО”, “ОРАНЖЕВЫЙ”, и “МАНГО” соответственно:

4.2. Осуществление

Итак, во-первых, давайте напишем Java-код для извлечения одного элемента и получим оставшиеся подмножества:

T element = set.iterator().next();
Set subsetWithoutElement = new HashSet<>();
for (T s : set) {
    if (!s.equals(element)) {
        subsetWithoutElement.add(s);
    }
}

Тогда мы хотим, чтобы получить власть подмножествоБезуберега :

Set> powersetSubSetWithoutElement = recursivePowerSet(subsetWithoutElement);

Далее, мы должны добавить, что powerset обратно в оригинал:

Set> powersetSubSetWithElement = new HashSet<>();
for (Set subsetWithoutElement : powerSetSubSetWithoutElement) {
    Set subsetWithElement = new HashSet<>(subsetWithoutElement);
    subsetWithElement.add(element);
    powerSetSubSetWithElement.add(subsetWithElement);
}

Наконец союз powerSetSubSetСубликование и powerSetSubSetСЭлемент является набором питания данного набора входных данных:

Set> powerSet = new HashSet<>();
powerSet.addAll(powerSetSubSetWithoutElement);
powerSet.addAll(powerSetSubSetWithElement);

Если собрать все фрагменты кода вместе, мы сможем увидеть наш конечный продукт:

public Set> recursivePowerSet(Set set) {
    if (set.isEmpty()) {
        Set> ret = new HashSet<>();
        ret.add(set);
        return ret;
    }

    T element = set.iterator().next();
    Set subSetWithoutElement = getSubSetWithoutElement(set, element);
    Set> powerSetSubSetWithoutElement = recursivePowerSet(subSetWithoutElement);
    Set> powerSetSubSetWithElement = addElementToAll(powerSetSubSetWithoutElement, element);

    Set> powerSet = new HashSet<>();
    powerSet.addAll(powerSetSubSetWithoutElement);
    powerSet.addAll(powerSetSubSetWithElement);
    return powerSet;
}

4.3. Примечания к унитарным испытаниям

Теперь давайте тестируем. У нас есть немного критериев здесь, чтобы подтвердить:

  • Во-первых, мы проверяем размер набора питания, и он должен быть 2 n для набора размеров n .
  • Затем каждый элемент будет возникать только один раз в подмножестве и 2 n-1 различные подмножества.
  • Наконец, каждый подмножество должен появиться один раз.

Если все эти условия пройдены, мы можем быть уверены, что наша функция работает. Теперь, так как мы использовали Набор , мы уже знаем, что нет повторения. В этом случае нам нужно только проверить размер набора питания и количество возникновений каждого элемента в подмножествах.

Чтобы проверить размер набора питания, мы можем использовать:

MatcherAssert.assertThat(powerSet, IsCollectionWithSize.hasSize((1 << set.size())));

И проверить количество происшествий каждого элемента:

Map counter = new HashMap<>();
for (Set subset : powerSet) { 
    for (String name : subset) {
        int num = counter.getOrDefault(name, 0);
        counter.put(name, num + 1);
    }
}
counter.forEach((k, v) -> Assertions.assertEquals((1 << (set.size() - 1)), v.intValue()));

Наконец, если мы можем собрать все вместе в одну единицу теста:

@Test
public void givenSet_WhenPowerSetIsCalculated_ThenItContainsAllSubsets() {
    Set set = RandomSetOfStringGenerator.generateRandomSet();
    Set> powerSet = new PowerSet().recursivePowerSet(set);
    MatcherAssert.assertThat(powerSet, IsCollectionWithSize.hasSize((1 << set.size())));
   
    Map counter = new HashMap<>();
    for (Set subset : powerSet) {
        for (String name : subset) {
            int num = counter.getOrDefault(name, 0);
            counter.put(name, num + 1);
        }
    }
    counter.forEach((k, v) -> Assertions.assertEquals((1 << (set.size() - 1)), v.intValue()));
}

5. Оптимизация

В этом разделе мы постараемся свести к минимуму пространство и уменьшить количество внутренних операций для оптимального расчета набора мощности.

5.1. Структура данных

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

Вместо этого мы можем сопоставить каждый набор или подмножество с некоторыми другими понятиями, чтобы уменьшить количество операций.

Во-первых, мы должны назначить все большее число, начиная от 0 до каждого объекта в данном наборе S что означает, что мы работаем с упорядоченным списком чисел.

Например, для данного набора “ЯБЛОКО”, “ОРАНЖЕВЫЙ”, “МАНГО” мы получаем:

“ЯБЛОКО” -> 0

“ОРАНЖЕВЫЙ” -> 1

“МАНГО” -> 2

Таким образом, отныне вместо того, чтобы создавать подмножества S , мы генерируем их для заказанного списка «0, 1, 2», и по заказу мы можем имитировать вычитания по стартовому индексу.

Например, если стартовый индекс 1, это означает, что мы генерируем набор питания в размере 1,2.

Чтобы получить отображенный идентификатор объекта и наоборот, мы храним обе стороны отображения. Используя наш пример, мы храним оба (“МАНГО” -> 2) и (2 -> “МАНГО”) . Поскольку отображение чисел началось с нуля, так что для обратной карты мы можем использовать простой массив для извлечения соответствующего объекта.

Одним из возможных реализаций этой функции было бы:

private Map map = new HashMap<>();
private List reverseMap = new ArrayList<>();

private void initializeMap(Collection collection) {
    int mapId = 0;
    for (T c : collection) {
        map.put(c, mapId++);
        reverseMap.add(c);
    }
}

Теперь, чтобы представлять подмножества Есть две известные идеи:

  1. Представление индекса
  2. Двоичное представление

5.2. Представительство индекса

Каждое подмножество представлено индексом его значений. Например, отображение индекса данного набора “ЯБЛОКО”, “ОРАНЖЕВЫЙ”, “МАНГО” Было бы:

{
   {} -> {}
   [0] -> {"APPLE"}
   [1] -> {"ORANGE"}
   [0,1] -> {"APPLE", "ORANGE"}
   [2] -> {"MANGO"}
   [0,2] -> {"APPLE", "MANGO"}
   [1,2] -> {"ORANGE", "MANGO"}
   [0,1,2] -> {"APPLE", "ORANGE", "MANGO"}
}

Таким образом, мы можем получить соответствующий набор из подмножества индексов с заданным отображением:

private Set> unMapIndex(Set> sets) {
    Set> ret = new HashSet<>();
    for (Set s : sets) {
        HashSet subset = new HashSet<>();
        for (Integer i : s) {
            subset.add(reverseMap.get(i));
        }
        ret.add(subset);
    }
    return ret;
}

5.3. Двоичное представительство

Или мы можем представлять каждый подмножество с помощью двоичного файла. Если элемент фактического набора существует в этом подмножестве, его соответствующее значение 1 ; в противном случае это 0 .

Для нашего фруктового примера, набор питания будет:

{
    [0,0,0] -> {}
    [1,0,0] -> {"APPLE"}
    [0,1,0] -> {"ORANGE"}
    [1,1,0] -> {"APPLE", "ORANGE"}
    [0,0,1] -> {"MANGO"}
    [1,0,1] -> {"APPLE", "MANGO"}
    [0,1,1] -> {"ORANGE", "MANGO"}
    [1,1,1] -> {"APPLE", "ORANGE", "MANGO"}
}

Таким образом, мы можем получить соответствующий набор из двоичного подмножества с заданным отображением:

private Set> unMapBinary(Collection> sets) {
    Set> ret = new HashSet<>();
    for (List s : sets) {
        HashSet subset = new HashSet<>();
        for (int i = 0; i < s.size(); i++) {
            if (s.get(i)) {
                subset.add(reverseMap.get(i));
            }
        }
        ret.add(subset);
    }
    return ret;
}

5.4. Рекурсивное внедрение алгоритма

На этом этапе мы постараемся реализовать предыдущий код, используя обе структуры данных.

Прежде чем вызвать одну из этих функций, мы должны позвонить инициализироватьМап метод получения заказанного списка. Кроме того, после создания нашей структуры данных, мы должны назвать соответствующие unMap функция для извлечения фактических объектов:

public Set> recursivePowerSetIndexRepresentation(Collection set) {
    initializeMap(set);
    Set> powerSetIndices = recursivePowerSetIndexRepresentation(0, set.size());
    return unMapIndex(powerSetIndices);
}

Итак, давайте попробуем свои силы в представлении индекса:

private Set> recursivePowerSetIndexRepresentation(int idx, int n) {
    if (idx == n) {
        Set> empty = new HashSet<>();
        empty.add(new HashSet<>());
        return empty;
    }
    Set> powerSetSubset = recursivePowerSetIndexRepresentation(idx + 1, n);
    Set> powerSet = new HashSet<>(powerSetSubset);
    for (Set s : powerSetSubset) {
        HashSet subSetIdxInclusive = new HashSet<>(s);
        subSetIdxInclusive.add(idx);
        powerSet.add(subSetIdxInclusive);
    }
    return powerSet;
}

Теперь давайте посмотрим двоичный подход:

private Set> recursivePowerSetBinaryRepresentation(int idx, int n) {
    if (idx == n) {
        Set> powerSetOfEmptySet = new HashSet<>();
        powerSetOfEmptySet.add(Arrays.asList(new Boolean[n]));
        return powerSetOfEmptySet;
    }
    Set> powerSetSubset = recursivePowerSetBinaryRepresentation(idx + 1, n);
    Set> powerSet = new HashSet<>();
    for (List s : powerSetSubset) {
        List subSetIdxExclusive = new ArrayList<>(s);
        subSetIdxExclusive.set(idx, false);
        powerSet.add(subSetIdxExclusive);
        List subSetIdxInclusive = new ArrayList<>(s);
        subSetIdxInclusive.set(idx, true);
        powerSet.add(subSetIdxInclusive);
    }
    return powerSet;
}

5.5. Итерировать через No 0, 2n)

Теперь, есть хорошая оптимизация, которую мы можем сделать с двоичным представлением. Если мы посмотрим на него, мы увидим, что каждая строка эквивалентна двоичному формату числа в No 0, 2 n ).

Так что, если мы итерировать через номера из 0 2 n , мы можем преобразовать этот индекс в двоичный, и использовать его для создания boolean представление каждого подмножества:

private List> iterativePowerSetByLoopOverNumbers(int n) {
    List> powerSet = new ArrayList<>();
    for (int i = 0; i < (1 << n); i++) {
        List subset = new ArrayList<>(n);
        for (int j = 0; j < n; j++)
            subset.add(((1 << j) & i) > 0);
        powerSet.add(subset);
    }
    return powerSet;
}

5.6. Минимальные подмножества изменений по серому коду

Теперь, если мы определим какие-либо двухдомовой функция от двоичного представления длины n на номер в No 0, 2 n ) , мы можем создавать подмножества в любом порядке, что мы хотим.

Серый код является хорошо известной функцией, которая используется для создания двоичных представлений чисел, так что двоичное представление последовательных чисел отличается только на один бит (даже разница последних и первых чисел одна).

Таким образом, мы можем оптимизировать это немного дальше:

private List> iterativePowerSetByLoopOverNumbersWithGrayCodeOrder(int n) {
    List> powerSet = new ArrayList<>();
    for (int i = 0; i < (1 << n); i++) {
        List subset = new ArrayList<>(n);
        for (int j = 0; j < n; j++) {
            int grayEquivalent = i ^ (i >> 1);
            subset.add(((1 << j) & grayEquivalent) > 0);
        }
        powerSet.add(subset);
    }
    return powerSet;
}

6. Ленивая загрузка

Чтобы свести к минимуму использование пространства набора питания, который O (2 n ) , мы можем использовать Итератор интерфейс для получения каждого подмножества, а также каждый элемент в каждом подмножестве лениво.

6.1. Листитератор

Во-первых, чтобы иметь возможность итерировать от 0 2 n , мы должны иметь особую Итератор что петли над этим диапазоном, но не потребляют весь диапазон заранее.

Для решения этой проблемы мы будем использовать две переменные; один для размера, который 2 n , а другой для текущего подмножества индекса. Наша hasNext () функция проверит, что положение меньше, чем размер :

abstract class ListIterator implements Iterator {
    protected int position = 0;
    private int size;
    public ListIterator(int size) {
        this.size = size;
    }
    @Override
    public boolean hasNext() {
        return position < size;
    }
}

И наши Далее () функция возвращает подмножество для текущего положение и повышает ценность положение по одному:

@Override
public Set next() {
    return new Subset<>(map, reverseMap, position++);
}

6.2. Подмножество

Чтобы иметь ленивый груз Подмножество , мы определяем класс, который расширяет АбстрактСет , и мы переопределить некоторые из его функций.

Зациклив все биты, которые 1 в принимающем маска (или положение) из Подмножество , мы можем реализовать Итератор и другие методы в АбстрактСет .

Например, размер () это число 1 s в принимающем маска :

@Override
public int size() { 
    return Integer.bitCount(mask);
}

И содержит () функция просто ли соответствующий бит в маска это 1 или нет:

@Override
public boolean contains(@Nullable Object o) {
    Integer index = map.get(o);
    return index != null && (mask & (1 << index)) != 0;
}

Мы используем другую переменную – remainingSetBits – чтобы изменить его всякий раз, когда мы извлекаем его соответствующий элемент в подмножестве, мы меняем этот бит 0 . Затем, hasNext () проверяет, если remainingSetBits не является нулевым (то есть, он имеет по крайней мере один бит со значением 1 ):

@Override
public boolean hasNext() {
    return remainingSetBits != 0;
}

И Далее () функция использует право-самый 1 в remainingSetBits , затем преобразует его в 0 , а также возвращает соответствующий элемент:

@Override
public E next() {
    int index = Integer.numberOfTrailingZeros(remainingSetBits);
    if (index == 32) {
        throw new NoSuchElementException();
    }
    remainingSetBits &= ~(1 << index);
    return reverseMap.get(index);
}

6.3. PowerSet

Чтобы иметь ленивую нагрузку PowerSet класс, нам нужен класс, который расширяет АннотацияСет<Сет>.

размер () функция просто 2 к власти размера набора:

@Override
public int size() {
    return (1 << this.set.size());
}

Поскольку набор питания будет содержать все возможные подмножества входного набора, так содержит (Объект o) функция проверяет, проверяют ли все элементы объект o существующи в обратныйМап (или в наборе входных данных):

@Override
public boolean contains(@Nullable Object obj) {
    if (obj instanceof Set) {
        Set set = (Set) obj;
        return reverseMap.containsAll(set);
    }
    return false;
}

Проверить равенство данного Объект с помощью этого класса мы можем проверить только если входные установить равен данному Объект :

@Override
public boolean equals(@Nullable Object obj) {
    if (obj instanceof PowerSet) {
        PowerSet that = (PowerSet) obj;
        return set.equals(that.set);
    }
    return super.equals(obj);
}

итератор () функция возвращает экземпляр Листитератор которые мы уже определили:

@Override
public Iterator> iterator() {
    return new ListIterator>(this.size()) {
        @Override
        public Set next() {
            return new Subset<>(map, reverseMap, position++);
        }
    };
}

Библиотека Гуавы использует эту ленивую идею нагрузки, и эти Силовой набор и подмножество эквивалентные реализации библиотеки Гуавы.

Для получения дополнительной информации, проверьте их исходный код и документация .

Кроме того, если мы хотим сделать параллельную операцию над подмножествами в PowerSet , мы можем позвонить Подмножество для различных значений в ThreadPool .

7. Резюме

Подводя итог, во-первых, мы изучили, что такое набор питания. Затем мы создали его с помощью библиотеки Гуавы. После этого мы изучили подход и как его реализовать, а также как написать для него удельный тест.

Наконец, мы использовали Итератор интерфейс для оптимизации пространства генерации подмножеов, а также их внутренних элементов.

Как всегда, исходный код доступен более на GitHub .