【问题标题】:How to get the indexes of the elements in a Stream?如何获取Stream中元素的索引?
【发布时间】:2019-11-28 21:18:48
【问题描述】:

作为学习 Java 8+ Streams 的练习,我想将一些简单的 Codility 实现转换为 Stream 解决方案。

例如,BinaryGap 问题.. 使用 Streams 的简单单线解决方案可能类似于:

public static int solution(int N) {
    return Integer.toBinaryString(N).chars().
                     filter(x -> x == '1').whichIndexes().diff().max();
}

唯一的问题是,whichIndexesdiff 不存在。我需要一种方法来获取过滤元素的索引,然后计算它们的成对差异,这对于基于 Streams 的单线解决方案来说是一个很好的起点。

更新:这是我在 C++ 中的 BinaryGap 解决方案,但 Java 非 Stream-ed 版本会非常相似:

#include <bitset>
#include <iostream>
#include <math.h>

using namespace std;

int solution(int N) {
    bitset<32> bs(N);
    int maxGap = 0;
    std::size_t i = 0;
    while (bs[i] == 0) {
        i++;
    }
    int startPos = i;
    for (; i < bs.size(); ++i) {
        if (bs[i] == 1) {
            int gap = i - startPos - 1;
            if (gap > maxGap) {
                maxGap = gap;
            }
            startPos = i;
        }
    }
    return maxGap;
}

int main() {
    cout << solution(9) << " must be 2" << endl;
    cout << solution(529) << " must be 4" << endl;
    cout << solution(20) << " must be 1" << endl;
    cout << solution(32) << " must be 0" << endl;
    cout << solution(1041) << " must be 5" << endl;
    return 0;
}

【问题讨论】:

  • 关于已经有答案:嗯,但与this post 一起,它或多或少地被覆盖了。

标签: java algorithm java-stream


【解决方案1】:

获取索引很简单;流式传输一系列整数而不是字符串中的字符。

String s = Integer.toBinaryString(N);
IntStream.range(0, s.length())
    .filter(i -> s.charAt(i) == '1')
    . // more stream operations

计算最大差异更难,因为这取决于流中的连续对,而不是单个元素。任何依赖于流中多个元素(而不是一次一个)的事情都必须在collect 阶段完成。这有点棘手:IntStream.collect 方法需要三个参数:

  • 一个Supplier&lt;R&gt;,它提供了一个R 类型的新对象来收集结果,
  • 一个ObjIntConsumer&lt;R&gt;,它累积了一个R类型的对象和一个int,
  • BiConsumer&lt;R, R&gt; 在非并行流中不执行任何操作。

R 类型需要跟踪当前的最大差值,以及最后看到的数字,以便计算与下一个数字的差值。一种方法是使用两个数字组成的List,其中索引 0 保存迄今为止的最大差值,索引 1 保存之前看到的数字(如果有)。

String s = Integer.toBinaryString(N);
int maxDiff = IntStream.range(0, s.length())
    .filter(i -> s.charAt(i) == '1')
    .collect(
        // supply a new list to hold intermediate results
        () -> {
            List<Integer> acc = new ArrayList<Integer>();
            acc.add(0); // initial max diff; and result if no "gap" exists
            return acc;
        },
        // accumulate one more number into the result
        (List<Integer> list, int num) -> {
            if(list.size() == 1) {
                // this is the first number, no diffs yet
                list.add(num);
            } else {
                int max = list.get(0);
                int lastNum = list.get(1);
                int diff = num - lastNum;
                list.set(0, diff > max ? diff : max);
                list.set(1, num);
            }
        },
        // combine two accummulators; shouldn't be called
        (List<Integer> list1, List<Integer> list2) -> {
            throw new RuntimeException("combiner shouldn't be called");
        }
    )
    .get(0); // max diff is at index 0 in the list

这几乎肯定是比您尝试使用流之前的解决方案更糟糕的解决方案,但是......好吧,您想要一个流解决方案,所以就在这里。

【讨论】:

  • 我的解决方案计算索引之间的最大差异,这是问题中所要求的。二进制序列10001 在索引 0 和 4 处有 1,因此差为 4。
  • 问题本身是不同的,尽管作为该链接中的状态
  • 我正在回答有关 Stack Overflow 的问题,而不是解决有关 Codility 的问题。您的反对似乎是问题是错误的。
  • 然后计算成对差异,这对我来说意味着你展示的不同的东西。
  • 成对差异是指每对之间的差异。它还可能意味着什么?
【解决方案2】:

除非像IntStream 技巧那样从头开始提供索引,否则您无法获取单个流中元素的索引。如果你只喂它字符串,你就迷路了。

Zipping 流式字符与另一个整数流最终在Pair&lt;String, Integer&gt; 上运行将是引入索引的另一种方式,但流式框架中没有内置的 zip 方法(目前)。有库和解决方法(见链接)。

你也不能对连续的元素进行操作,你所看到的(除了收集阶段或reduce 方法,这对目的没有帮助)就是手头的元素(除非你想出脏技巧如下图和)。

顺便说一句。这是一个潜在的单线,在这个过程中突然出现,可能有一些关于二进制间隙的边缘情况缺陷(我想问问@Holger(或Eugene),他们仍在讨论......)。我实际上并不熟悉它的细则,它与 OP (IMO) 无关。

Arrays.stream(Integer.toBinaryString(n).replaceAll("0*$|^0*", "").split("1"))
      .map(String::length)
      .reduce(0, Math::max)

我认为这个最接近你最初的想法(没有zipping),从@Kaya3 和this 中清除元素,有趣的黑客从连续元素({1,2,3,4} -&gt; {{1,2},{2,3},{3,4}})中获取对。您也可以在 map-to-pair 步骤中进行 diff,可能会省略另一行,但它很奇怪,如下所示:

        String st = Integer.toBinaryString(N);
        Integer max = IntStream.range(0, st.length() - 1)
                .filter(i -> st.charAt(i) == '1')
                .boxed()
                .map(new Function<Integer, Pair>() {
                    Integer previous;

                    @Override
                    public Pair apply(Integer i) {
                        Pair p = new Pair(previous, i);
                        previous = i;
                        return p;
                    }
                }).skip(1) //first element is {null, smthg}
                .map(i -> (i.right - i.left) - 1)
                .max(Integer::compareTo)
                .orElse(0);

但是mind you: “这个 [map hack] 完全违背了流框架的设计,直接违反了地图 API 的约定,因为匿名函数不是无状态的。尝试使用并行流和......你会看到......不常见的随机“错误”几乎不可能重现......这可能是灾难性的。”

我认为,与parallelStream() 一起使用它会给你的结果甚至可以保证是错误的,因为这样会丢失与流的分区一样多的对。

【讨论】:

  • 好主意,你的一个班轮。效率不高,但安静紧凑。但这里有几点。 toBinaryString 不会产生前导零,但可能有多个尾随零,因此您需要 *。换句话说,它应该是.replaceAll("0*$", "")。此外,此解决方案无法区分最大间隙大小为零和根本没有任何间隙的数字。 (例如,00b0100 应被视为没有间隙,而 0b001100b001100 均具有零大小间隙)。
  • @Holger 但是如果我们将没有间隙视为零间隙或反之亦然,这真的很重要吗?该函数应返回int,而不是OptionalInt,例如可以区分“空”和“零间隙”。还有为什么 * 而不是 +replaceAll 部分?你不想替换“最后全为零”吗? Gurioso 1+ 用于您已删除的解决方案,恕我直言,您不应该这样做。
  • 我真的不想干涉你和霍尔格的家族企业。不过既然版本引人注意,我会恢复它。
  • @gurioso 那个版本还不错;我从不希望你删除它。值得指出解决方案的缺点(我还提到了我的可读性问题)。所以我很高兴你决定重新包含它。
  • @Eugene 这是任务定义的问题。 OP 将决定一个解决方案,但是,对于未来的读者,我会提到“无间隙”和“零间隙”之间的语义差异。这取决于此操作的实际用例,区别是否重要。
【解决方案3】:

请注意,计算机确实已经在二进制系统中进行计算,而与处理 int 值相比,将值转换为 String 和处理 String 都是昂贵的操作。

要对int 的位进行流式传输,您可以使用以下解决方案:

给定

int value = 1001;
IntStream.range(0, 32)
    .filter(i -> (value & (1 << i)) != 0)
    .forEach(System.out::println);

这利用了我们知道最多可以有 32 位的事实,它只是检查每个是否在 int value 中设置。打印动作仅供演示。

或者,我们可以使用内置工具进行位操作:

BitSet.valueOf(new long[] { Integer.toUnsignedLong(value) }).stream()
    .forEach(System.out::println);

为了完整起见,使用循环之类的迭代逻辑

IntStream.iterate(value, i -> i != 0, i -> i - Integer.highestOneBit(i))
    .map(i -> Integer.numberOfTrailingZeros(Integer.highestOneBit(i)))
    .forEach(System.out::println);

但是这个带有结束条件的 iterate 方法需要 Java 9。对于 Java 8,我们需要一种解决方法:

IntStream.iterate(value, i -> i - Integer.highestOneBit(i))
    .limit(32).filter(i -> i != 0)
    .map(i -> Integer.numberOfTrailingZeros(Integer.highestOneBit(i)))
    .forEach(System.out::println);

这再次利用了我们知道最大位数的事实,进一步我们知道遇到零后不会出现非零值,所以filter在语义上相当于这里的停止条件。

不过,对于处理两个相邻元素的任务,流并不是一个好的工具。正如其他答案所示,Stream 解决方案很复杂,但这些解决方案并未完全遵守 Stream API 的合同,具有一定的局限性,您必须牢记。

相比之下,基于BitSet 的循环解决方案如下所示:

BitSet bs = BitSet.valueOf(new long[] { Integer.toUnsignedLong(value) });
int maxDiff = 0;
for(int s = bs.nextSetBit(0), e; (e = bs.nextSetBit(s + 1)) >= 0; s = e) {
    maxDiff = Math.max(maxDiff, e - s);
}
System.out.println(maxDiff == 0? "No range at all":
    "max diff: "+maxDiff+", resp, max gap "+(maxDiff-1));

请注意,间隙,即两个 1 位之间的零个数,比位号之间的差小一。

我们可以完全使用固有的int 操作来执行操作,这将是最有效的:

int maxDiff = 0;
for(int i = value, bit1 = Integer.lowestOneBit(i), bit2,
        s = Integer.numberOfTrailingZeros(bit1), e;
        (bit2 = Integer.lowestOneBit(i -= bit1)) != 0; bit1 = bit2, s = e) {
    e = Integer.numberOfTrailingZeros(bit2);
    maxDiff = Math.max(maxDiff, e - s);
}
System.out.println(maxDiff == 0? "No range at all":
    "max diff: "+maxDiff+", resp, max gap "+(maxDiff-1));

这有点难以阅读,但仍然比 Stream 解决方案简单得多……

【讨论】:

    【解决方案4】:

    我的第一个直觉是使用正则表达式,如下所示:

    static int regex(int n) {
        String s = Integer.toBinaryString(n);
        Pattern p = Pattern.compile("1(0+)(?=1)");
        Matcher m = p.matcher(s);
        int max = 0;
        while (m.find()) {
            max = Math.max(max, m.end(1) - m.start(1));
        }
        return max;
    }
    

    如果你有 java-9,这也可以写成“单行”:

    static int regexMineJava9(int n) {
        return Pattern.compile("1(0+)(?=1)")
                      .matcher(Integer.toBinaryString(n))
                      .results()
                      .mapToInt(mr -> mr.end(1) - mr.start(1))
                      .max()
                      .orElse(0);
    }
    

    对我来说,这比这更具可读性,例如:

    static int loop(int n) {
        int max = 0;
    
        while (n != 0) {
            int left = Integer.numberOfTrailingZeros(Integer.highestOneBit(n));
            n = n & ~(1 << left);
            int right = Integer.numberOfTrailingZeros(Integer.highestOneBit(n));
    
            if (right != 32) {
                max = left - right - 1;
            }
        }
    
        return max;
    }
    

    虽然最后一个循环应该是性能最高的循环。

    为了好玩,这里还有一个BitSet,这与 Holger 所做的差不多,但有点松散:

    static int bitSet(int n) {
        BitSet bitSet = BitSet.valueOf(new long[]{Integer.toUnsignedLong(n)});
        int left = bitSet.nextSetBit(0);
        int max = 0;
        while (left != -1) {
            int right = bitSet.nextSetBit(left + 1);
            if (right == -1) {
                break;
            }
            max = Math.max(right - left - 1, max);
            left = right;
        }
    
        return max;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-16
      • 1970-01-01
      • 2019-05-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多