Как улучшить проверку, является ли строка (массив) подмножеством другой строки(массива), при помощи Stream API?

Я решал задачу leetcode 383. Ransom Note, в которой нужно было определить, является ли строка (массив символов) подмножеством другой строки (массива).

Задачу я решил при помощи Stream API, вот код:

public static boolean canConstruct(String ransomNote, String magazine) {
    boolean result = false;
    
    Map<Character, Long> letters = magazine.chars()
            .mapToObj(c -> (char) c)
            .collect(Collectors.toMap(c -> c, c -> 1L, Long::sum));
    
    ransomNote.chars()
            .mapToObj(c -> (char) c)
            .forEach(c -> {
                long count = letters.getOrDefault(c, 0L) - 1L;
                letters.put(c, count);
            });
    
    if (letters.entrySet()
            .stream()
            .filter(entry -> entry.getValue() < 0L)
            .count() == 0L) {
        result = true;
    }
    
    return result;
}

вопросы:

  1. Хочется узнать существует ли способ решить эту проблему при единожды открытом стриме (сейчас стрим "открывается" трижды), возможно ли это вообще?
  2. Если есть эксперты в Stream API, подскажите пожалуйста топ 5 или топ 10 методов в стримах, чтобы особое внимание им уделить))

Ответы (1 шт):

Автор решения: Nowhere Man

1. Хочется узнать существует ли способ решить эту проблему при единожды открытом стриме (сейчас стрим "открывается" трижды), возможно ли это вообще?

В представленном решении открываются два разных стрима для каждой из двух входных строк, чтобы построить карту частот символов. Затем в третьем стриме проверяется, не содержит ли карта частот хотя бы одно отрицательное значение частоты для какого-то символа (не слишком оптимальным образом).

Следует отметить, что для некоторых входных данных не понадобится открывать ни один стрим -- если строка magazine заведомо короче, чем строка ransomNote.

Затем, не нужно проверять всю строку ransomNote целиком, достаточно будет завершить проверку сразу же, как только в ней будет обнаружен символ, отсутствующий в карте частот, используя терминальную операцию с коротким замыканием noneMatch.

Поскольку в условии задачи явно сказано, что обе строки имеют длину до 105 и содержат только строчные английские буквы, вместо Map<Character, Long> можно использовать целочисленный массив на 26 элементов.

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

public boolean canConstruct(String ransomNote, String magazine) {
    int n  = magazine.length();
    int m = ransomNote.length();
    if (m > n) {
        return false;
    }

    int[] freq = new int[26]; // заполнен 0-ми по умолчанию

    // Заполняем карту частот по строке magazine
    magazine
        .chars()
        .forEach(c -> freq[c - 'a']++);

    // При проверке символов в ransomNote сразу же выходим, если найден отсутствующий символ
    return ransomNote
        .chars()
        .map(c -> --freq[c - 'a']) // вернёт -1, если символ не найден в мапе
        .noneMatch(q -> q == -1); // возвращаем false из метода
}

Если очень хочется использовать только один стрим, то отдельные стримы для каждой строки можно заменить одним целочисленным стримом по индексам, используя при этом метод String::charAt для доступа к элементам каждой строки.

Получится что-то вроде:

public boolean canConstruct(String ransomNote, String magazine) {
    int n  = magazine.length();
    int m = ransomNote.length();
    if (m > n) {
        return false;
    }
    int[] freq = new int[26];

    return IntStream.range(0, m + n)
        .map(i -> i < n 
            ? ++freq[magazine.charAt(i) - 'a'] 
            : --freq[ransomNote.charAt(i - n) - 'a']
        )
        .noneMatch(f -> f == -1);
}

Более общее решение с использованием карты частот символов, а не массива, может выглядеть так:

public boolean canConstruct(String ransomNote, String magazine) {
    int n  = magazine.length();
    int m = ransomNote.length();
    if (m > n) {
        return false;
    }
    Map<Character, Integer> freq = new HashMap<>(32);
    return IntStream.range(0, m + n)
        .map(i -> i < n 
            ? freq.merge(magazine.charAt(i), 1, Integer::sum) 
            : freq.merge(ransomNote.charAt(i - n), -1, Integer::sum)
        )
        .noneMatch(f -> f == -1);
}

2. Если есть эксперты в Stream API, подскажите пожалуйста топ 5 или топ 10 методов в стримах, чтобы особое внимание им уделить))

Ответы на подобного рода вопросы про хит-парады не приветствуются на Stackoverflow, так как они провоцируют обмен мнениями, а не фактами (см. Справка: Какие вопросы лучше не задавать?).

Наиболее популярными скорее всего являются базовые методы Stream API:

  • промежуточные операции map / flatMap, filter, sorted / distinct, limit / skip,
  • терминальные операции collect / reduce, findFirst / findAny, allMatch / noneMatch / anyMatch
  • фабричные методы для разнообразных коллекторов Collectors.toList, Collectors.groupingBy, Collectors.toMap, Collectors.toSet, Collectors.joining и др.
  • различные способы получения/объединения стримов типа Stream.of, Stream.concat.
→ Ссылка