确定一个列表是不是由 Java 8 中的字谜元素组成

Posted

技术标签:

【中文标题】确定一个列表是不是由 Java 8 中的字谜元素组成【英文标题】:Determine if a list composed of anagram elements in Java 8确定一个列表是否由 Java 8 中的字谜元素组成 【发布时间】:2019-04-02 12:51:33 【问题描述】:

我想使用 Java 8 确定一个列表是否是字谜。

示例输入:

"cat", "cta", "act", "atc", "tac", "tca"

我已经编写了以下函数来完成这项工作,但我想知道是否有更好、更优雅的方法来做到这一点。

boolean isAnagram(String[] list) 
    long count = Stream.of(list)
            .map(String::toCharArray)
            .map(arr -> 
                Arrays.sort(arr);
                return arr;
            )
            .map(String::valueOf)
            .distinct()
            .count();
    return count == 1;


似乎我无法使用Stream.sorted() 方法对字符数组进行排序,所以这就是我使用第二个地图运算符的原因。如果有某种方法可以直接对 char 流而不是 char 数组的 Stream 进行操作,那也会有所帮助。

【问题讨论】:

【参考方案1】:

您可以在字符串中获得charsStream,而不是创建和排序char[]int[],这不能内联并因此“中断”流将它们转换为数组。请注意,这是一个IntSteam,但String.valueOf(int[]) 将包含数组的内存地址,这在这里不是很有用,因此最好在这种情况下使用Arrays.toString

boolean anagrams = Stream.of(words)
        .map(String::chars).map(IntStream::sorted)
        .map(IntStream::toArray).map(Arrays::toString)
        .distinct().count() == 1;

当然,你也可以用map(s -> Arrays.toString(s.chars().sorted().toArray()))代替四个maps的系列。不确定速度是否存在(显着)差异,这可能主要是口味问题。

此外,您可以使用IntBuffer.wrap 使数组具有可比性,这应该比Arrays.toString 快得多(感谢cmets 中的Holger)。

boolean anagrams = Stream.of(words)
        .map(s -> IntBuffer.wrap(s.chars().sorted().toArray()))
        .distinct().count() == 1;

【讨论】:

不错的答案,来自我的 +1。虽然我个人会拆分单词的映射并列出本身,而不是使用四个映射。 It still does exactly the same and outputs the same results, though. @Michael 你在包装数组时免费获得它,CharBuffer.wrap(array)char[] 的情况下,IntBuffer.wrap(array)int[] 的情况下。无需昂贵的转换为String... 一般来说,这是一个典型的方法引用过度使用的例子。单个.map(s -> Arrays.toString(s.chars().sorted().toArray()).map(s -> IntBuffer.wrap(s.chars().sorted().toArray())(请参阅我之前的评论),阅读效果要好得多…… @Holger 是的,你提到了。我在最后一段中添加了这一点,但我想这有点模棱两可。 “不确定是否更快”部分只是关于地图与 lambda 位。改写了最后一部分。 是的,这是一个更好的措辞。我还假设带有方法引用的多个map 步骤和单个map 步骤之间没有显着的速度差异(与this Q&A 相比)。我只是认为这里的单个 lambda 表达式更具可读性。【参考方案2】:

我不会处理不同值的计数,因为这不是您感兴趣的。您想知道的是,根据特殊的相等规则,所有元素是否相等。

因此,当我们创建一个将String 转换为规范键(即所有字符排序)的方法时

private CharBuffer canonical(String s) 
    char[] array = s.toCharArray();
    Arrays.sort(array);
    return CharBuffer.wrap(array);

我们可以简单地检查所有后续元素是否等于第一个:

boolean isAnagram(String[] list) 
    if(list.length == 0) return false;
    return Arrays.stream(list, 1, list.length)
        .map(this::canonical)
        .allMatch(canonical(list[0])::equals);

请注意,对于 expression::name 形式的方法引用,表达式只计算一次并捕获结果,因此对于整个流操作,canonical(list[0]) 只计算一次,并且只为每个元素调用 equals

当然,您也可以使用 Stream API 创建规范密钥:

private IntBuffer canonical(String s) 
    return IntBuffer.wrap(s.chars().sorted().toArray());

isAnagram方法不需要做任何改动)

请注意,CharBufferIntBuffer 可以用作数组周围的轻量级包装器,就像在这个答案中一样,并根据实际数组内容适当地实现 equalshashCode

【讨论】:

看起来不错。但是,我确实认为如果列表为空则返回 false 很奇怪,但如果列表有一个元素则返回 true。 @Michael 这是一个选择,作者必须做出。您可以考虑返回 true 或故意抛出异常,因为空集合是否由字谜组成的问题毫无意义,但是,我决定模仿 OP 原始代码的行为,因为检查不同计数是否为一个将零元素导致false【参考方案3】:

我不会对 char 数组进行排序,因为排序是 O(NlogN),这里没有必要。

对于列表中的每个单词,我们只需要计算每个字符的出现次数。为此,我们将每个单词的字符收集到 Map<Integer, Long>,其中键是每个字符,值是它的计数。

然后,我们检查,对于数组参数中的所有单词,我们有相同的字符数,即相同的映射:

return Arrays.stream(list)
    .map(word -> word.chars()
            .boxed().collect(Collectors.grouping(c -> c, Collectors.counting()))
    .distinct()
    .count() == 1;

【讨论】:

虽然这是真的,但我想知道这是否更快,甚至更慢,前提是这种方法可能会带来更大的开销(?)并且我认为所有字符串都相对较小(比如说最多不超过几十个字符)。 如果我不使用 Streams,我会使用 HashMaps 实现类似的东西。 @tobias_k 在许多语言中,大多数单词由少量字母组成,这会将 O(NlogN) 减少到您提到的几乎可以忽略不计。现在我想知道这两种方法的记忆印记是什么,以及一种在速度方面是否比另一种更有优势。 @tobias_k 对于短字符串,你是对的,但对于这些,它可能无论如何都无关紧要。 @Limonkufu 我认为 Tobias 和 Peter 的 cmets 是有效的。也许Map 的开销比就地排序小数组要多。无论如何,如果你追求性能,你根本不应该使用流,即传统的迭代方法会更快,我相信...... @tobias_k 同意,你有一个有效的观点。我认为最终,这一切都取决于输入。也许我的解决方案更适合较长的字符串,而您的解决方案更适合一般情况......【参考方案4】:

或者,可以工作的实施的更新版本是:

boolean isAnagram(String[] list) 
    return Stream.of(list) // Stream<String>
            .map(String::toCharArray) // Stream<char[]>
            .peek(Arrays::sort) // sort 
            .map(String::valueOf) // Stream<String>
            .distinct() //distinct
            .count() == 1;

【讨论】:

【参考方案5】:

或者可能是BitSet:

  System.out.println(stream.map(String::chars)
        .map(x -> 
            BitSet bitSet = new BitSet();
            x.forEach(bitSet::set);
            return bitSet;
        )
        .collect(Collector.of(
            BitSet::new,
            BitSet::xor,
            (left, right) -> 
                left.xor(right);
                return left;
            
        ))
        .cardinality() == 0);

【讨论】:

以上是关于确定一个列表是不是由 Java 8 中的字谜元素组成的主要内容,如果未能解决你的问题,请参考以下文章

关于字谜的几个问题

Java 8 Stream 函数将字谜列表分组为列表映射

一种可能的算法来确定两个字符串是不是是彼此的字谜? [关闭]

给定一些文本输入,找到最大的字谜组

字谜索引计算[重复]

使用python中的递归解决方案在字符串列表中查找字谜