如何优化多(矩阵)开关/案例算法?

时间:2009-10-31 00:14:09

标签: algorithm matrix switch-statement

是否可以优化这种(矩阵)算法:

//        | case 1 | case 2 | case 3 |
//  ------|--------|--------|--------|
//        |        |        |        |
//  case a|   a1   |   a2   |   a3   |
//        |        |        |        |
//  case b|   b1   |   b2   |   b3   |
//        |        |        |        |
//  case c|   c1   |   c2   |   c3   |
//        |        |        |        |

switch (var)
    {
      case 1:
      switch (subvar)
      {
        case a:
          process a1;
        case b:
          process b1;    
        case c:
          process c1;
      }
      case 2:
      switch (subvar)
      {
        case a:
          process a2;
        case b:
          process b2;    
        case c:
          process c2;
      }
      case 3:
      switch (subvar)
      {
        case a:
          process a3;
        case b:
          process b3;    
        case c:
          process c3;
      }
    }

代码相当简单,但你必须想象更复杂的“开关/案例”。

我使用3个变量。根据他们取值1,2,3或a,b,c或alpha,beta,charlie有不同的过程来实现。是否可以通过一系列“开关/案例”来优化它?

(问题已在法语here中提出)。

编辑 :(来自Dran Dane对以下评论的回应。这些也可能在这个更显眼的地方!)
optimize ”应理解为编写更少代码,更少“切换/案例”。我们的想法是提高可读性,可维护性性能。

有一种方法可以通过“责任链”编写更少的代码,但是这个解决方案在所有方面都不是最优的,因为它需要在内存中创建许多对象。

11 个答案:

答案 0 :(得分:13)

听起来你想要的是'Finite State Machine',在这种情况下你可以激活不同的过程或“状态”。在C中,这通常使用函数指针的数组(矩阵)来完成。

所以你基本上创建一个数组并将正确的函数指针放在正确的标记上,然后使用'var'作为正确'process'的索引,然后你调用它。您可以在大多数语言中执行此操作。这样,机器的不同输入激活不同的过程并将其带到不同的状态。这对许多应用非常有用;我自己一直在MCU开发中使用它。

编辑:Valya指出我可能应该展示一个基本模型:

stateMachine[var1][var2]();   // calls the right 'process' for input var1, var2

答案 1 :(得分:5)

这个问题没有好的答案:-(

因为这么多的回应取决于

  • 有效目标(“优化”的含义,嵌套交换机的不足之处)
  • 将应用此构造的上下文(应用程序隐含的最终需求)

TokenMacGuy明智地询问目标。我花时间检查问题及其在法国网站上的回复,我仍然对目标感到疑惑...... Dran Dane最近的回应似乎指向减少代码量/提高可读性但是让我们回顾一下:

  • 处理速度:嵌套开关效率非常高,可能需要少于3次乘法才能获得地图表中的索引,但可能不均匀。
  • 可读性:是的可能是一个问题,随着变量和级别的增加,组合爆炸开始增加,并且switch语句的格式也倾向于在长的垂直延伸上扩展分支点和相关值。在这种情况下,使用fct初始化3维(或更多)表。指针将分支值和要在一行上调用的函数放回去。
  • 写少量代码:对不起,这里帮助不大;在一天结束时,我们需要考虑相对较多的组合,“地图”,无论其形式如何,都必须写在某处。诸如TokenMacGuy之类的代码生成器可能会派上用场,在这种情况下看起来有点过分。发电机有它们的位置,但我不确定是这种情况。两种情况之一:如果变量和级别的数量足够小,那么生成器是不值得的(如果变量和级别的数量是变量,那么设置起来比创建实际代码要花费更多时间)重要的是,生成的代码难以阅读,难以维护...)

简而言之,我的建议 关于使代码更具可读性 (并且写入速度更快)是法国网站上描述的表/矩阵方法点。

这个解决方案分为两部分:
一次初始化3维数组(3级); (或者如果愿意的话,可以使用“发烧友”的容器结构:例如树)。这是通过以下代码完成的:

// This is positively more compact / readable
...
FctMap[1][4][0] = fctAlphaOne;
FctMap[1][4][1] = fctAlphaOne;
..
FctMap[3][0][0] = fctBravoCharlie4;
FctMap[3][0][1] = NULL;   // impossible case
FctMap[3][0][2] = fctBravoCharlie4;    // note how the same fct may serve in mult. places

在需要调用函数的地方使用相对简单的代码段:

if (FctMap[cond1][cond2][cond3]) {
  retVal = FctMap[cond1][cond2][cond3](Arg1, Arg2);
  if (retVal < 0)
      DoSomething(); // anyway we're leveraging the common api to these fct not the switch alternative ....
}

如果组合空间相对稀疏,那么可以使用上述解决方案提示一个 NOT 的情况为(交换机“树”中的许多“分支”不是使用)或如果某些功能需要一组不同的参数;对于这两种情况,我想插入 Joel Goodwin首先提出的解决方案,它基本上将多个级别的各种键组合成一个更长的键(如果需要,可以使用分隔符) ,基本上将问题归结为一个冗长但单级的切换语句

现在...

真正的讨论应该是关于为什么我们首先需要这样的映射/决策树。不幸的是,要回答这个问题需要了解底层应用程序的真实性质。可以肯定的是,我并不是说这表明设计不好。在某些应用程序中,一个大的调度部分可能有意义。但是,即使使用C语言(法语站点贡献者似乎不符合面向对象的设计),也可以采用面向对象的方法和模式。无论如何,我正在分歧...)有可能整个应用程序总体上更好地服务于替代设计模式,其中“关于什么时候调用的信息树”已经分布在几个模块和/或几个对象中。

抱歉以相当抽象的方式谈论这一点,只是缺乏应用程序细节......重点仍然是:挑战我们需要这个大型调度树的想法;想一想应用程序的替代方法。

Alors,bonne的机会! ; - )

答案 2 :(得分:4)

根据语言的不同,某种形式的哈希映射将(var, subvar)对作为键,将第一类函数作为值(或者您的语言提供的最佳近似值,例如扩展某些类的实例) Java中的接口可能提供最高性能 - 并且基于密钥从地图中获取适当的函数(或其他;-)并执行它的完全简洁性使得熟悉该语言的读者具有高可读性。这样的功能成语。

答案 3 :(得分:1)

函数指针的想法可能是最好的(根据mjv,Shhnap)。但是,如果每种情况下的代码都相当小,则可能过度杀戮并导致比预期更多的混淆。在这种情况下,我可能会实现像这样快速和快速阅读的东西:

string decision = var1.ToString() + var2.ToString() + var3.ToString();
switch(decision)
{
    case "1aa":
        ....
    case "1ab":
        ....

}

不熟悉您的特定情况,因此以前的建议可能更合适。

答案 4 :(得分:1)

是的,肯定有更简单的方法,更快更简单。这个想法与Alex Martelli提出的想法基本相同。而不是将问题视为二维问题,而是将其视为一维查找表。

这意味着将var,subvar,subsubvar等组合起来获取一个唯一键并将其用作查找表入口点。

这样做的方法取决于使用的语言。使用python结合var,subvar等来构建一个元组并将其用作词典中的键就足够了。

使用C或类似的方法将每个键转换为枚举通常更简单,然后使用逻辑运算符将它们组合起来只得到一个可以在交换机中使用的数字(这也是使用switch而不是带有级联的字符串比较的简单方法IFS)。你也可以获得另一个好处。通常,初始开关的不同分支中的几种处理是相同的。对于初始形式,很难做到这一点。您可能会对相同的函数进行一些调用,但它位于代码中的不同点。现在,您可以在编写交换机时对相同的案例进行分组。

我在生产代码中多次使用这种转换,这很容易做和维护。

总之,你可以得到这样的东西......混合功能显然取决于你的应用程序细节。

switch (mix(var, subvar))
{
      case a1:
          process a1;
      case b1:
          process b1;    
      case c1:
          process c1;
      case a2:
          process a2;
      case b2:
          process b2;    
      case c2:
          process c2;
      case a3:
          process a3;
      case b3:
          process b3;    
      case c3:
          process c3;
}

答案 5 :(得分:1)

我有一次完全相同的问题,尽管是一个5参数嵌套开关的内在混乱。我想,为什么我自己输入所有这些O(N 5 )的情况,为什么甚至发明'嵌套'函数名称,如果编译器可以为我做这个。所有这些都导致了“嵌套的专用模板切换”,指的是“专用模板数据库”。

写起来有点复杂。但我发现它值得:它导致一个“知识”数据库非常易于维护,调试,添加等等......我必须承认:自豪感。 / p>

// the return type: might be an object actually _doing_ something
struct Result {
  const char* value;
  Result(): value(NULL){}
  Result( const char* p ):value(p){};
};

用于切换的一些变量类型:

 // types used:
 struct A { enum e { a1, a2, a3 }; };
 struct B { enum e { b1, b2 }; };
 struct C { enum e { c1, c2 }; };

知识库的“前向声明”:嵌套开关的“api”。

 // template database declaration (and default value - omit if not needed)
 // specializations may execute code in stead of returning values...
 template< A::e, B::e, C::e > Result valuedb() { return "not defined"; };

实际切换逻辑(浓缩)

 // template layer 1: work away the first parameter, then the next, ...
 struct Switch {

   static Result value( A::e a, B::e b, C::e c ) {
     switch( a ) {
          case A::a1: return SwitchA<A::a1>::value( b, c );
          case A::a2: return SwitchA<A::a2>::value( b, c );
          case A::a3: return SwitchA<A::a3>::value( b, c );
          default: return Result();
     }
   }

   template< A::e a > struct SwitchA {

     static Result value( B::e b, C::e c ) {
       switch( b ) {
            case B::b1: return SwitchB<a, B::b1>::value( c );
            case B::b2: return SwitchB<a, B::b2>::value( c );
            default: return Result();
       }
     }

     template< A::e a, B::e b > struct SwitchB {

       static Result value( C::e c ) {
         switch( c ) {
               case C::c1: return valuedb< a, b, C::c1 >();
               case C::c2: return valuedb< a, b, C::c2 >();
               default: return Result();
         }
       };
     };

   };
 };

知识库本身

 // the template database
 //
 template<> Result valuedb<A::a1, B::b1, C::c1 >() { return "a1b1c1"; }
 template<> Result valuedb<A::a1, B::b2, C::c2 >() { return "a1b2c2"; }

这是如何使用的。

int main()
{
     // usage:
     Result r = Switch::value( A::a1, B::b2, C::c2 ); 
     return 0;
}

答案 6 :(得分:0)

也许您想要的是代码生成?

#! /usr/bin/python
first = [1, 2, 3]
second = ['a', 'b', 'c']

def emit(first, second):
    result = "switch (var)\n{\n"
    for f in first:
        result += " case {0}:\n switch (subvar)\n {{\n".format(f)
        for s in second:
            result += "  case {1}:\n   process {1}{0};\n".format(f,s)
        result += " }\n"
    result += "}\n"
    return result

print emit(first,second)
#file("autogen.c","w").write(emit(first,second))

当然,这很难阅读,你可能真的想要一种更好的模板语言来完成你的肮脏工作,但这会减轻你任务的某些部分。

答案 7 :(得分:0)

如果C ++是一个选项,我会尝试使用虚函数,也许双重调度。这可以使它更清洁。但只有当你有更多的案件时才会得到回报。

article on DDJ.com可能是一个很好的条目。

答案 8 :(得分:0)

如果您只是想消除两级开关/ case语句(并保存一些垂直空间),您可以将两个变量值编码为单个值,然后打开它:

// Assumes var is in [1,3] and subvar in [1,3]
// and that var and subvar can be cast to int values
switch (10*var + subvar)
{
    case 10+1:
        process a1;
    case 10+2:
        process b1;
    case 10+3:
        process c1;  
//
    case 20+1:
        process a2;
    case 20+2:
        process b2;
    case 20+3:
        process c2;  
//
    case 30+1:
        process a3;
    case 30+2:
        process b3;
    case 30+3:
        process c3;
//
    default:
        process error;
}

答案 9 :(得分:0)

如果您的语言是C#,并且您的选择足够短并且不包含特殊字符,则可以使用反射并仅使用几行代码执行此操作。这样,不是手动创建和维护一个函数指针数组,而是使用框架提供的一个!

像这样:

using System.Reflection;
...

void DispatchCall(string var, string subvar)
{
  string functionName="Func_"+var+"_"+subvar;
  MethodInfo m=this.GetType().GetMethod(fName);
  if (m == null) throw new ArgumentException("Invalid function name "+ functionName);
  m.Invoke(this, new object[] { /* put parameters here if needed */ });
}

void Func_1_a()
{
   //executed when var=1 and subvar=a
}

void Func_2_charlie()
{
   //executed when var=2 and subvar=charlie
}

答案 10 :(得分:0)

来自developpez.com的解决方案 是的,你可以优化它并使它更清洁。你不能使用这样的“链条 责任“与工厂:

public class ProcessFactory {
    private ArrayList<Process> processses = null;

    public ProcessFactory(){
        super();

        processses = new ArrayList<Process>();

        processses.add(new ProcessC1());
        processses.add(new ProcessC2());
        processses.add(new ProcessC3());
        processses.add(new ProcessC4());                    
        processses.add(new ProcessC5(6));                   
        processses.add(new ProcessC5(22));
    }

    public Process getProcess(int var, int subvar){
        for(Process process : processses){
            if(process.canDo(var, subvar)){
                return process;
            }
        }

        return null;
    }
}

然后,就像您的流程使用canXXX实现接口流程一样,您可以轻松使用:

new ProcessFactory().getProcess(var,subvar).launch();