是否有可能合理地模拟yield-syntax,也许是在Java 8的帮助下?

时间:2014-02-23 18:29:19

标签: java iterator yield java-8

我今天正在尝试这个问题,来自欧拉问题:

  

回文数字两种方式相同。由两个2位数字的乘积制成的最大回文是9009 = 91×99。

     

找出由两个3位数字的乘积制成的最大回文。

我考虑过它,它当然可以用for循环完成,但是我想使用Java 8,因为它打开了新选项。

然而,首先,我不知道如何生成产生这些元素的IntStream,所以我仍然使用正常的for循环:

public class Problem4 extends Problem<Integer> {
    private final int digitsCount;

    private int min;
    private int max;

    public Problem4(final int digitsCount) {
        this.digitsCount = digitsCount;
    }

    @Override
    public void run() {
        List<Integer> list = new ArrayList<>();
        min = (int)Math.pow(10, digitsCount - 1);
        max = min * 10;

        for (int i = min; i < max; i++) {
            for (int j = min; j < max; j++) {
                int sum = i * j;
                if (isPalindrome(sum)) {
                    list.add(sum);
                }
            }
        }

        result = list.stream().mapToInt(i -> i).max().getAsInt();
    }

    private boolean isPalindrome(final int number) {
        String numberString = String.valueOf(number);
        String reversed = new StringBuilder(numberString).reverse().toString();
        return (numberString.equals(reversed));
    }

    @Override
    public String getName() {
        return "Problem 4";
    }
}

正如你所看到的,我可能有点懒,真的IntStream::max是一个非常好的方法,我觉得最好用它来自己编写。

虽然问题出现了,我需要list现在能够以这种方式获得最大值,这意味着我需要存储数据,我应该在哪里不这样做。

那么,现在的问题是,是否可以在Java 8中实现它?

for (int i = min; i < max; i++) {
    for (int j = min; j < max; j++) {
        yield i * j;
    }
}

然后从该方法中创建PrimitiveIterator.OfIntIterator<Integer>的取消装箱版本,或直接创建IntStream? 然后使用streamFromYield.filter(this::isPalindrome).max().getAsInt()获得答案将非常容易实现。

最后,我知道之前已经提出过这个问题,但是最后一次已经很久了,现在Java 8将很快发生,他们已经添加了大概念Stream<T>和新的语言结构,称为lambdas 因此,现在制作这样的代码可能与人们为Java 6或7制作代码时的情况大不相同。

4 个答案:

答案 0 :(得分:6)

好吧,我认为我们已经使用来自“外部”的Streams API,使用flatMap,优化回文查找算法,等等。请参阅Boris the Spider和{{的答案3}}。但是,我们回避了如何使用Python的yield语句编写生成器函数的原始问题。 (我认为OP的嵌套 - 例如yield使用的是Python。)

使用flatMap的一个问题是并行拆分只能发生在最外层的流上。内部流(从flatMap返回)按顺序处理。我们可以尝试使内部流也平行,但它们可能与外部流竞争。我认为嵌套拆分可以工作,但我不太自信。

一种方法是使用Stream.generate或(如assylias'答案)Stream.iterate函数。但是,这些会创建无限的流,因此必须提供外部limit来终止流。

如果我们能够创建一个有限但“扁平”的流,那么整个值的流可能会分裂,这将是很好的。不幸的是,创建一个流并不像Python的生成器函数那么方便。不过,它可以毫不费力地完成。以下是使用StreamSupportAbstractSpliterator类的示例:

class Generator extends Spliterators.AbstractIntSpliterator {
    final int min;
    final int max;
    int i;
    int j;

    public Generator(int min, int max) {
        super((max - min) * (max - min), 0);
        this.min = min;
        this.max = max;
        i = min;
        j = min;
    }

    public boolean tryAdvance(IntConsumer ic) {
        if (i == max) {
            return false;
        }
        ic.accept(i * j);
        j++;
        if (j == max) {
            i++;
            j = min;
        }
        return true;
    }
}

public static void main(String[] args) {
    Generator gen = new Generator(100, 1000);
    System.out.println(
        StreamSupport.intStream(gen, false)
            .filter(i -> isPalindrome(i))
            .max()
            .getAsInt());
}

不是让迭代变量在堆栈上(如在带有yield方法的嵌套for中),我们必须使它们成为对象的字段并使tryAdvance递增它们直到迭代完成。现在,这是分离器的最简单形式,并不一定能很好地并行化。通过额外的工作,可以实现trySplit方法以更好地进行拆分,从而实现更好的并行性。

可以覆盖forEachRemaining方法,它看起来几乎就像嵌套for-loop-with-yield示例,调用IntConsumer而不是yield。遗憾的是tryAdvance是抽象的,因此必须实现,所以仍然需要让迭代变量成为对象的字段。

答案 1 :(得分:4)

如何从另一个方向看它:

您需要Stream [100,1000),并且对于Stream的每个元素,您希望该元素的另一个Stream乘以[100, 1000)。这是flatMap的用途:

public static void main(final String[] args) throws Exception {
    OptionalInt max = IntStream.range(100, 1000).
            flatMap((i) -> IntStream.range(i, 1000).map((j) -> i * j)).
            unordered().
            parallel().
            filter((i) -> {
                String forward = Integer.toString(i);
                String backward = new StringBuilder(forward).reverse().toString();
                return forward.equals(backward);
            }).
            max();
    System.out.println(max);
}

不确定是否获得String然后相反是检测回文的最有效方法,从我的头顶看这似乎更快:

final String asString = Integer.toString(i);
for (int j = 0, k = asString.length() - 1; j < k; j++, k--) {
    if (asString.charAt(j) != asString.charAt(k)) {
        return false;
    }
}
return true;

它给出了相同的答案,但我没有对它进行严格的测试......似乎在我的机器上快了大约100ms。

同样不确定这个问题对于unordered().parallel()来说是否足够大 - 删除它会对速度产生一点提升。

只是试图展示Stream API的功能。

修改

正如@Stuart在评论中指出的那样,乘法是可交换的,我们只需要在子流中IntStream.range(i, 1000)。这是因为一旦我们检查a x b,我们就不需要检查b x a。我已经更新了答案。

答案 2 :(得分:2)

我会试一试。带循环的版本,然后是流。虽然我从另一端开始,所以它更容易,因为我可以limit(1)

public class Problem0004 {

    public static void main(String[] args) {
        int maxNumber = 999 * 999;
        //with a loop
        for (int i = maxNumber; i > 0; i--) {
            if (isPalindrome(i) && has3DigitsFactors(i)) {
                System.out.println(i);
                break;
            }
        }
        //with a stream
        IntStream.iterate(maxNumber, i -> i - 1)
                .parallel()
                .filter(i -> isPalindrome(i) && has3DigitsFactors(i))
                .limit(1)
                .forEach(System.out::println);
    }

    private static boolean isPalindrome(int n) {
        StringBuilder numbers = new StringBuilder(String.valueOf(n));
        return numbers.toString().equals(numbers.reverse().toString());
    }

    private static boolean has3DigitsFactors(int n) {
        for (int i = 999; i > 0; i--) {
            if (n % i == 0 && n / i < 1000) {
                return true;
            }
        }
        return false;
    }
}

答案 3 :(得分:2)

总有一些方法可以模拟高估的yield功能,即使没有Java 8.基本上它是关于存储执行的状态,即堆栈帧,这可以通过一个线程来完成。一个非常简单的实现可能如下所示:

import java.util.Iterator;
import java.util.NoSuchElementException;

public abstract class Yield<E> implements Iterable<E> {
  protected interface Flow<T> { void yield(T item); }
  private final class State implements Runnable, Iterator<E>, Flow<E> {
    private E nextValue;
    private boolean finished, value;

    public synchronized boolean hasNext() {
      while(!(value|finished)) try { wait(); } catch(InterruptedException ex){}
      return value;
    }
    public synchronized E next() {
      while(!(value|finished)) try { wait(); } catch(InterruptedException ex){}
      if(!value) throw new NoSuchElementException();
      final E next = nextValue;
      value=false;
      notify();
      return next;
    }
    public void remove() { throw new UnsupportedOperationException(); }
    public void run() {
      try { produce(this); }
      finally {
        synchronized(this) {
          finished=true;
          notify();
        }
      }
    }
    public synchronized void yield(E next) {
      while(value) try { wait(); } catch(InterruptedException ex){}
      nextValue=next;
      value=true;
      notify();
    }
  }

  protected abstract void produce(Flow<E> f);

  public Iterator<E> iterator() {
    final State state = new State();
    new Thread(state).start();
    return state;
  }
}

一旦有了这样的帮助类,用例就会直截了当:

// implement a logic the yield-style
Iterable<Integer> y=new Yield<Integer>() {
  protected void produce(Flow<Integer> f) {

    for (int i = min; i < max; i++) {
      for (int j = min; j < max; j++) {
          f.yield(i * j);
      }
    }

  }
};

// use the Iterable, e.g. in a for-each loop
int maxPalindrome=0;
for(int i:y) if(isPalindrome(i) && i>maxPalindrome) maxPalindrome=i;
System.out.println(maxPalindrome);

以前的代码没有使用任何Java 8功能。但它允许使用它们而无需任何改变:

// the Java 8 way
StreamSupport.stream(y.spliterator(), false).filter(i->isPalindrome(i))
  .max(Integer::compare).ifPresent(System.out::println);

请注意,上面的Yield支持类不是最有效的实现,如果迭代未完成但放弃了Iterator,它就无法处理。但它表明这种逻辑确实可以在Java中实现(而其他答案令人信服地表明,这样的yield逻辑不是解决这种问题所必需的。)