3

我有一个对象列表,需要对每个元素应用多个有条件的操作。采用“if-for”方法还是“for-if”方法更有效。换句话说,我应该尽量减少if语句的数量还是for循环的数量?有这方面的标准吗?

什么是确定这一点的可靠方法?

"If-For" 方法最小化 if 语句

public void ifForMethod() {
    if (conditionA) {
        for (Object o : listOfObjects) {
            doA(o);
        }
    }

    if (conditionB) {
        for (Object o : listOfObjects) {
            doB(o);
        }
    }
}

最小化 for 循环的“For-If” 方法

public void forIfMethod() {
    for (Object o : listOfObjects) {
        if (conditionA) {
            doA(o);
        }
        if (conditionB) {
            doB(o);
        }

    }
}

假设

  • 条件是简单的布尔值,在迭代时不会改变。
  • 一个或多个条件为真。(有2个以上条件)
  • 每个条件都独立于其他条件。
  • 内部方法根本不会相互冲突或交互。它们的执行顺序无关紧要。
4

10 回答 10

4

没有理由对列表进行 2 次传球。

假设:谓词是简单的布尔值,如果必须对它们进行评估,那么显然成本会改变事情。

If ((condtionA || conditionB) == true) 那么 If-for 和 For-If 都是 1 次通过。如果两个谓词都为真,那么显然您只想通过一次。

doA 和 doB 无关紧要,因为我们假设它们在 If-for 和 For-If 中是相同的。

如果谓词可以在评估过程中发生变化,那么必须考虑这一点。

你问的是一个一般性问题,所以答案是一般性和模糊的,没有更多细节。

好的,现在您已经提供了附加信息(列表只有 5 个元素长,这是构建过程的一部分,谓词是静态布尔值)我们可以看到这里的瓶颈是 doA/B 函数。因此,您应该只循环一次。静态布尔检查可以忽略不计。

于 2013-10-07T17:53:24.577 回答
2

使用您所谓的“If-For”方式而不是“For-If”是一种称为循环取消切换的优化(可能是更通用的版本)。它是否真的是一个好主意取决于几个因素,例如(但不限于)

  • 是否允许这种转换(即条件没有副作用,doA并且doB可以重新排序)
  • 您正在优化什么(例如速度、可读性或 w/e),但在这种情况下,这并没有真正的区别
  • 数组是否适合缓存(迭代两次可能会使缓存未命中数翻倍)
  • (JIT)编译器究竟是做什么的,例如条件是否实际编译为分支,或者编译器是否为您执行循环取消切换
  • 处理器微架构(一些 µarchs 比其他人更不喜欢循环内的分支,即使这些分支是高度可预测的)
于 2013-10-07T22:10:29.740 回答
1

首先,让我们看一下您目前所展示的方法的复杂性:

  • ifForMethod 执行k个检查,其中m个返回 true。对于这些m中的每一个,都存在对n 个对象的迭代。那么,复杂度是k+nm
  • forIfMethod迭代n 个对象并在每次迭代中执行k个比较那么,复杂度是k+n(k-1)=nk

在这两种情况下,所有k个条件都必须至少评估一次,因此这里的区别实际上在于nmn(k-1)加数。渐近地,m只是k的一小部分(你说m大约是0.75k),所以它们都是 O( nk ),但是k+nm < k+n(k-1),所以ifForMethod可能比forIfMethod. 实际运行时间的差异将取决于诸如迭代数组所需的实际时间以及k的大小等因素. 您将开始处理诸如内存局部性之类的问题(对于您的对象和您的代码)。

不过,这是一种您可能会觉得有趣的方法。理想情况下,您只想遍历对象列表一次,而不必多次检查布尔条件。您可以抽象出您正在执行的操作,这样您就可以将它们组合成一个单独的操作(并且您只会合并那些与真实条件相对应的操作),然后执行该复合操作列表中的每个元素。这是一些执行此操作的代码。

这个想法是有动作,你可以构造一个执行doA的动作和一个执行的动作doB。根据条件,您可以创建一个复合动作,其中包括条件为真时的动作和条件为真时doA的动作。然后您遍历对象,并调用对每个对象执行复合操作。渐近地说,这是一种k+nm方法,所以理论上它表现得很好,但同样,这里的实际性能将取决于一些棘手的常数和内存局部性问题。doAdoBdoB

import java.util.ArrayList;
import java.util.List;

public class CompoundActionExample {

    /**
     * An action is used to do something to an argument.
     */
    interface Action {
        void act( Object argument );
    }

    /**
     * A compound action is an action that acts on an argument
     * by passing the argument to some other actions.
     */
    static class CompoundAction implements Action {
        /**
         * The list of actions that the compound action will perform.  Additional
         * actions can be added using {@link #add(Action)}, and this list is only
         * accessed through the {@link #act(Object)} method.
         */
        private final List<CompoundActionExample.Action> actions;

        /**
         * Create a compound action with the specified list of actions.
         */
        CompoundAction( final List<CompoundActionExample.Action> actions ) {
            this.actions = actions;
        }

        /**
         * Create a compound action with a fresh list of actions.
         */
        CompoundAction() { 
            this( new ArrayList<CompoundActionExample.Action>() );
        }

        /**
         * Add an action to the compound action.
         */
        public void add( CompoundActionExample.Action action ) {
            actions.add( action );
        }

        /**
         * Act on an argument by passing the argument to each of the 
         * compound action's actions.
         */
        public void act( final Object argument) {
            for ( CompoundActionExample.Action action : actions ) {
                action.act( argument );
            }
        }
    }

    public static void main(String[] args) {
        // Some conditions and a list of objects
        final boolean conditionA = true;
        final boolean conditionB = false;
        final Object[] listOfObjects = { "object1", "object2", "object3" };

        // A compound action that encapsulates all the things you want to do
        final CompoundAction compoundAction = new CompoundAction();

        // If conditionA is true, add an action to the compound action that 
        // will perform doA.  conditionA is evaluated exactly once.
        if ( conditionA ) {
            compoundAction.add( new Action() {
                public void act( final Object argument) {
                    System.out.println( "doA("+argument+")" ); // doA( argument );
                }
            });
        }

        // If conditionB is true, add an action to the compound action that
        // will perform doB. conditionB is evaluted exactly once.
        if ( conditionB )  {
            compoundAction.add( new Action() {
                public void act(Object argument) {
                    System.out.println( "doB("+argument+")" ); // doB( argument );
                }
            });
        }

        // For each object, apply the compound action
        for ( final Object o : listOfObjects ) {
            compoundAction.act( o );
        }
    }
}
于 2013-10-07T21:39:34.237 回答
1

让我们考虑一下,我们在 for 循环和 if 内部都进行了相同数量的操作。根据这个标准,我将采用第一种方法,即在执行 for 循环之前使用 if 语句,以避免 for 循环中的迭代次数。

此外,当您使用高级 for 循环时,与普通 for 循环相比,执行相同操作需要更多时间。

如果我错了,请纠正我。

于 2013-10-07T17:57:42.813 回答
1

这取决于您的代码试图解决的业务问题的性质。如果 conditionA 和 conditionB 都是简单的布尔变量而不是表达式,那么 For-If 会更好,因为您只在对象列表中循环一次。

我们基本上是在比较哪个表现更好:多次从对象列表中枚举或多次评估布尔表达式。如果您的条件 A/条件 B 是非常复杂的布尔表达式,那么您的 If-For 将是一种更好的方法。

于 2013-10-07T17:54:30.173 回答
1

这取决于!ifForMethod()如果存在条件A 和条件B 都不为真的真实情况,则该解决方案是最好的。如果有条件A和条件B为真的情况,解决方案forIfMethod()是最好的,但在进入循环之前应该预先计算条件。

但是您可以修改forIfMethod()以使其适用于所有情况:

public void forIfMethod() {
  boolean a = conditionA;
  boolean b = conditionB;
  if (a || b) {
    for (Object o : listOfObjects) {
      if (a) {
        doA(o);
      }
      if (b) {
        doB(o);
      }
    }
  }
}
于 2013-10-07T18:02:43.533 回答
0

第一个(if-for)对我来说听起来不错..因为对于第一种情况,将对整个 for 循环进行一次检查。但在第二种情况下,将检查每个循环。

于 2013-10-07T17:42:00.287 回答
0

我认为这取决于更多的事情,例如:如果找到条件A,循环会中断吗?条件A和条件B可以共存吗?我可以和他们一起使用 if-else 吗?

只是寻找你所呈现的,我认为第二种方法更好。您只循环一次并在同一个循环中检查两次。在我看来,它也更具可读性。

于 2013-10-07T17:48:05.783 回答
0

第二个在您进行多少比较方面更有效。

检查条件a,1计算。如果为真,则 Object.size 计算。

检查条件 b,1 计算。如果为真,Object.size 计算。最小,2,最大对象.size * 2

对于方法 2,您将始终执行 Object.size * 2 计算。

如果两项检查都是错误的,请考虑您的“最坏情况”。方式 1 只会进行 2 次计算。方式 2 将执行 Object.size * 2。它与您的函数无关,因为在这两种情况下,在这两种情况下它总是需要相同的时间。

即使在您的“最佳情况”中,如果两项检查都为真,您仍然对 A 执行 N-1 次检查,对 B 执行 N-1 次检查。

我认为用最少的计算来做到这一点的最好方法。

public void ifForMethod() {
    if (conditionA) {
        if(conditionB){
            for (Object o : listOfObjects) {
                doA(o);
                doB(o);
            }
        else{
            for (Object o : listOfObjects) {
                doA(o);
            }    
        }
    }
    else if (conditionB) {
        for (Object o : listOfObjects) {
            doB(o);
        }
    }
}

您执行 2 次检查操作,然后最多只循环一次列表。

于 2013-10-07T21:17:04.987 回答
-1

几件事:

  1. 这是过早的优化吗?如果一种方式比另一种方式更快,这真的很重要吗,这取决于数据,它可能不是人类明显的差异。
  2. 我会质疑软件的设计。为什么同一个对象有两种可能的条件?我建议将设计分解为多个对象。也许使用子类来执行不同的逻辑或使用策略模式。如果没有更好地了解您在做什么,我无法更具体。
于 2013-10-07T17:42:14.023 回答