【问题标题】:Prime Factorization of a Positive Integer with Streams带流的正整数的素数分解
【发布时间】:2016-03-13 05:00:53
【问题描述】:

我目前正在尝试将 Java 8 的 Stream API 合并到我的日常 Java 工具箱中。我正在尝试使用 Streams 来查找正整数的质因子,然后将每个因子存储在一个数组(或ArrayList)中,并将它们的多重性存储在一个并行数组中。或者,我正在尝试创建一个说... FactorWithMultiplicity 对象的流,甚至是 Map ,其中因子作为键,多重性作为值。如果因子按升序排序,并且它甚至可以处理非常大的数字(例如,我敢说,Long.MAX_VALUE),那就太好了。

目前,我的代码看起来像这样,但是,由于我是 Streams 的初学者,我确信有一种更快或更适合的方式来完成这项任务。请使用 Streams 创建您的解决方案,但如果您知道某些非 Stream 解决方案更快,请随时向我指出该代码。

int num = getPositiveInt();
ArrayList<Integer> factors = new ArrayList<>();
ArrayList<Integer> multiplicities = new ArrayList<>();
boolean isPrime = IntStream.rangeClosed(2, num / 2)
    .reduce(num, (int temp, int factor) -> {
        int count = 0;
        while (temp % factor == 0) {
            temp /= factor;
            count++;
        }
        if (count > 0) {
            factors.add(factor);
            multiplicities.add(count);
        }
        return temp;
    }) > 1;

【问题讨论】:

  • 我觉得你使用流的方式很奇怪。这是因为您使用它来执行 lambda 以操作外部变量,而通常流的目的是映射或过滤流本身中的数字。
  • @Nayuki 你是对的。我认为如果 lambda 映射到包含主要因素及其多重性的 Stream 会更好。
  • 我认为真正的解决方案是根本不使用流来解决这个问题。
  • @StephenC 请解释一下。我认为我的解决方案做得很好,不是吗?至少我在这里只是将 Streams 用作 foreach 循环,但 reduce 的作用远不止于此。
  • 素因子的 OO 表示可能是组合因子和多重性的小对象。

标签: java java-8 java-stream prime-factoring


【解决方案1】:

如果您特别想要一个基于流的解决方案,您可以使用一种递归分解数字的方法:

IntStream factors(int num) {
    return IntStream.range(2, num)
        .filter(x -> num % x == 0)
        .mapToObj(x -> IntStream.concat(IntStream.of(x), factors(num / x)))
        .findFirst()
        .orElse(IntStream.of(num));
}

然后你可以使用下面的代码来制作你的两个列表:

Map<Integer, Integer> f2m = factors(2, num).boxed()
        .collect(toMap(f -> f, f -> 1, Integer::sum));  // or groupingBy with summingInt(f->1), whichever you prefer

List<Integer> factors = new ArrayList<>(f2m.keySet());
List<Integer> multiplicities = factors.stream().map(f2m::get).collect(toList());

如果您想从中获得更多性能,您可以将 last found factor 传递给 factors 方法并使用它来代替 2

如果您想考虑多头,这里有一个具有一些性能改进的版本:

static LongStream factors(long lastFactor, long num) {
    return LongStream.rangeClosed(lastFactor, (long) Math.sqrt(num))
            .filter(x -> num % x == 0)
            .mapToObj(x -> LongStream.concat(LongStream.of(x), factors(x, num / x)))
            .findFirst()
            .orElse(LongStream.of(num));
}

如果您希望结果按排序顺序排列,您可以使用

SortedMap<Long, Integer> f2m = factors(2, num).boxed()
         .collect(toMap(f -> f, f -> 1, Integer::sum, TreeMap::new));

或者,或者,保持 Map 原样并使用

List<Long> factors = f2m.keySet().stream().sorted().collect(toList());

【讨论】:

  • 如果您对此代码进行 3 次更改,那将是最好的! (对于 Stream,否则 this solution 基本上是无与伦比的)首先,将所有内容切换到 Long 以便可以使用更大的数字(因为这个解决方案可以在早餐时吃掉它们)。其次,将factors 方法更改为使用.rangeClosed(2, (long) Math.sqrt(num))(巨大的速度提升)。第三,在.orElse 之后添加.sorted()(因为这会非常有帮助,甚至不会影响速度)。
  • 我使用了int,因为这就是问题所使用的。出于同样的原因跳过了各种性能优化——即使是大的ints 也会很快被分解。我将在答案中添加long 版本。在boxed() 之前添加sorted() 毫无意义,因为toMap 无论如何都不会保留订单。如果要对结果进行排序,可以将toMapTreeMap::new 一起使用,或者对keySet() 的输出进行排序。
  • 听起来不错,在这种情况下,只需将性能优化添加到 long 版本即可。另外,我发现用new TreeMap&lt;&gt;() 包裹f2m 也是一种简单的排序解决方案。
  • 不必创建Map,然后用TreeMap 包裹。您可以直接使用toMap 收集器制作TreeMap。查看更新的代码。
【解决方案2】:

如果您想重复调用factorsOf,这将是有用的另一种变体。 (我在某处偷了筛子的基本概念,修好了。)

这里的想法是使用素数作为流,过滤因子并确定它们的多重性以创建确定结果的 FactorTimes 对象。

public class PrimeFactors  {
  private final int limit = 1_000_000;
  private BitSet sieve = new BitSet( limit+1 );
  public PrimeFactors(){
    sieve.set( 2, limit );
    long count = sieve.stream()
    .peek( x -> { if( (long)x*x < limit )
                    for( int i = x*x; i <= limit; i += x )
                      sieve.clear( i );
                })
    .count();
  }
  public FactorTimes[] factorsOf( int num ){
    FactorTimes[] fts = sieve.stream()
    .limit( num/2 )
    .filter( x -> num % x == 0 ) 
    .mapToObj( x -> { int n = 1;
                      int k = num/x;
                      while( k % x == 0 ){ k /= x; n++; }
                      return new  FactorTimes( x, n );
                    } )
    .toArray( FactorTimes[]::new );
    return fts;
  }
  public static void main( String[] args ){
    PrimeFactors pf = new PrimeFactors();
    for( FactorTimes ft: pf.factorsOf( 4504500 ) ){
      System.out.println( ft );
    }
  }
}

class FactorTimes {
  private int factor, multiplicity;
  public FactorTimes(int f, int m) {
    factor = f; multiplicity = m;
  }
  public int getFactor() { return factor; }
  public int getMultiplicity() { return multiplicity; }
  public String toString(){
    return multiplicity > 1 ? factor + "(" + multiplicity + ")"
                            : Integer.toString( factor ); }
}

【讨论】:

    【解决方案3】:

    要生成主要因素,您需要跟踪几个状态。因此Streams 不太适合这项任务。

    您可以做的是提供自己的Spliterator 来创建IntStream。现在您可以生成数组或分组操作了:

    public static IntStream primeFactors(int n) {
      int characteristics = Spliterator.ORDERED | Spliterator.SORTED | Spliterator.IMMUTABLE | Spliterator.NONNULL;
      Spliterator.OfInt spliterator = new Spliterators.AbstractIntSpliterator(Long.MAX_VALUE, characteristics) {
        int val = n;
        int div = 2;
    
        @Override
        public boolean tryAdvance(IntConsumer action) {
          while (div <= val) {
            if (val % div == 0) {
              action.accept(div);
              val /= div;
              return true;
            }
            div += div == 2 ? 1 : 2;
          }
          return false;
        }
    
        @Override
        public Comparator<? super Integer> getComparator() {
          return null;
        }
      };
      return StreamSupport.intStream(spliterator, false);
    }
    

    然后这样调用:

    int n = 40500;
    System.out.println(Arrays.toString(primeFactors(n).toArray()));
    System.out.println(primeFactors(n).boxed().collect(
        Collectors.groupingBy(Function.identity(), Collectors.summingInt(i -> 1)))
    );
    

    你应该得到你想要的结果:

    [2, 2, 3, 3, 3, 3, 5, 5, 5]
    {2=2, 3=4, 5=3}
    

    【讨论】:

      【解决方案4】:

      当因式分解整数时,最有利可图的优化是尝试除数直到数字的平方根(包括,例如:尝试因式分解 49)。此外,在检查 2 后,您可以只检查奇数。

      int num = getPositiveInt();
      ArrayList<Integer> factors = new ArrayList<>();
      ArrayList<Integer> multiplicities = new ArrayList<>();
      int factor = 2;
      int f_delta = 1;  // to increase by +1 only once (2 to 3) 
      while ((factor*factor)<=num) {
        int count = 0;
        while (num % factor == 0) {
          num /= factor;
          count++;
        }  
        if (count > 0) {
          factors.add(factor);
          multiplicities.add(count);
        }
        factor += f_delta;
        f_delta = 2;
      }
      

      【讨论】:

      • 非常感谢您的建议!虽然它没有使用 Streams,所以它并没有真正教我这个问题想要教的内容。关于如何在没有 Stream 的情况下执行此操作已经存在很多问题。
      【解决方案5】:

      经过彻底调查,我发现与我在问题中发布的内容相比,这在速度上是一个巨大的进步。唯一更快的是 @Misha 在将他们的factors 函数更改为使用.rangeClosed(prevFactor, Math.sqrt(num)) 后发布的那个。但是,this other solution 是最快的解决方案...期间...但不使用 Streams。

      public static Map<Long, Integer> factorize(long num) { //NOW USING LONG
          Map<Long, Integer> factors = new LinkedHashMap<>(); //NOW USING MAP
          long lastRemainder = LongStream.rangeClosed(2, (long) Math.sqrt(num)) //NOW USING SQRT
                  .filter(x -> (x== 2||x%2>0)&&(x==3||x%3>0)&&(x==5||x%5>0)) //ADDED THIS
                  .reduce(num, (temp, factor) -> {
                      if (factor <= temp / factor) { //ADDED THIS
                          int count = 0;
                          while (temp % factor == 0) {
                              temp /= factor;
                              count++;
                          }
                          if (count > 0)
                              factors.put(factor, count);
                      }
                      return temp;
                  });
          if (lastRemainder != num && lastRemainder > 1) //ADDED THIS
              factors.put(lastRemainder, 1);
          return factors;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-05-20
        相关资源
        最近更新 更多