代码之家  ›  专栏  ›  技术社区  ›  Bastien Vandamme

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

  •  5
  • Bastien Vandamme  · 技术社区  · 15 年前

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

    //        | 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;
          }
        }
    

    代码相当简单,但是您必须想象更多“switch/case”会更复杂。

    我使用3个变量。根据值1、2、3或a、b、c或alpha、beta,charlie需要实现不同的过程。除了通过一系列“开关/外壳”之外,是否可以以其他方式优化它?

    (问题已经用法语问过了 here )

    编辑 (摘自德兰·丹对以下评论的回复。这些可能就在这个更显眼的地方!)
    优化 “要理解为 少写代码 ,更少的“开关/外壳”。这个想法是 提高可读性、可维护性 ,请 性能。

    也许有一种方法可以通过“责任链”来减少代码的写入量,但这种解决方案并非在所有方面都是最佳的,因为它需要在内存中创建许多对象。

    11 回复  |  直到 15 年前
        1
  •  13
  •   Robert Massaioli    15 年前

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

    因此,您基本上创建一个数组,并将正确的函数指针放在正确的指示符上,然后使用“var”作为右“process”的索引,然后调用它。在大多数语言中都可以这样做。这样,机器的不同输入激活不同的过程,并使其进入不同的状态。这对于许多应用程序都非常有用;我自己在MCU开发中一直使用它。

    编辑:瓦利亚指出,我可能应该展示一个基本模型:

    stateMachine[var1][var2]();   // calls the right 'process' for input var1, var2
    
        2
  •  5
  •   Peter Mortensen icecrime    15 年前

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

    因为这么多的反应取决于

    • 有效的目标(什么是“优化”,什么是取消嵌套的交换机)
    • 将应用此构造的上下文(应用程序隐含的最终需求是什么)

    tokenmacguy明智地询问目标。我花了点时间在法国网站上查看了这个问题及其回复,但我仍然对目标感到困惑……dran dane的最新回应似乎指向减少代码数量/提高可读性,但让我们回顾一下:

    • 处理速度:这不是一个问题,嵌套的开关非常有效,可能是一个小于3倍的乘法,以获得映射表的索引,但可能不是甚至。
    • 可读性:是的,可能是一个问题,因为变量和级别的数量增加,组合爆炸开始,而且switch语句的格式倾向于将分支点和相关值扩展到一个长的垂直扩展。在这种情况下,用fct初始化的3维(或更多)表。指针将分支值和要在单行上调用的函数放回一起。
    • 写更少的代码:抱歉,这里没有太多帮助;在一天结束的时候,我们需要考虑到相对较多的组合,“地图”无论是什么形式,都必须写在某个地方。像tokenmacguy这样的代码生成器可能会派上用场,在这种情况下,它确实有点杀伤力过度。发电机有自己的位置,但我不确定情况是否如此。两种情况之一:如果变量和级别的数量足够小,生成器就不值得(设置它比首先编写实际代码要花更多的时间),如果变量和级别的数量很大,生成的代码就很难读取,很难维护…)

    简而言之,我的建议 关于提高代码的可读性 (并且写得更快)是法语站点上描述的表/矩阵方法。

    该解决方案分为两部分:
    三维数组的一次性初始化(用于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 ....
    }
    

    可能引发一个 不是 使用上述解决方案是 如果组合空间相对稀疏 (开关“树”中的许多“分支”未使用)或 如果某些函数需要一组不同的参数 对于这两种情况,我想插上 乔尔·古德温首先提出了一个解决方案 ,基本上将多个级别的各个键组合成一个较长的键(如果需要,还可以使用分隔符)。 将问题扁平化为一个长的、但只有一个级别的switch语句 .

    现在。。。

    真正的讨论应该是 为什么我们首先需要这样一个映射/决策树 . 不幸的是,要回答这个问题,需要了解底层应用程序的真实性质。当然,我不是说这意味着设计不好。在某些应用中,大的调度部分可能是有意义的。然而,即使使用C语言(法国网站的贡献者似乎不适合面向对象的设计),也有可能采用面向对象的方法和模式。不管怎样,我在转移…)有可能整个应用程序会更好地服务于替代设计模式,其中“关于何时调用的信息树”已经分布在多个模块和/或多个对象中。

    很抱歉用相当抽象的术语谈论这个问题,这只是缺乏应用程序的细节…重点仍然是:挑战我们需要这个大的调度树的想法;考虑应用程序的其他方法。

    再见,波恩,机会!;-)

        3
  •  4
  •   Alex Martelli    15 年前

    根据语言的不同,有些形式的哈希映射与 (var, subvar) 作为关键和一流的函数作为值(或者无论您的语言提供什么最好的近似,例如,在Java中扩展适当的接口的类的实例)很可能提供最高性能——以及基于键从地图中获取适当函数(或任何;;),以及执行它的完全简洁性,LEA。对熟悉该语言和此类功能性习语的读者具有较高的可读性。

        4
  •  1
  •   Joel Goodwin    15 年前

    函数指针的想法可能是最好的(根据mjv,shhnap)。但是,如果每种情况下的代码都相当小,那么它可能会被过度杀戮,导致比预期更多的混淆。在这种情况下,我可能会实现一些快速易读的东西,比如:

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

    不熟悉您的特定场景,因此前面的建议可能更合适。

        5
  •  1
  •   xtofl Adam Rosenfield    15 年前

    我曾经遇到过同样的问题,尽管对于一个5参数嵌套开关的内在混乱。我想,为什么要输入这些O(N) )如果编译器可以为我这样做,为什么还要发明“嵌套”函数名呢?所有这些导致了一个“嵌套的专用模板开关”,引用了一个“专用模板数据库”。

    写起来有点复杂。但我发现它是值得的:它产生了一个“知识”数据库 非常 易于维护、调试、添加等。我必须承认:一种自豪感。

    // 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
  •  1
  •   kriss    15 年前

    是的,这两种方法都很简单 更快 简单的 . 这个想法与亚历克斯·马泰利提出的基本相同。不要将问题视为二维问题,而是将其视为一维查找表。

    它意味着组合var、subbar、subsubvar等以获得一个唯一的键,并将其用作查找表的入口点。

    这取决于使用的语言。使用python结合var、subbar等来构建元组,并将其用作字典中的键就足够了。

    使用C或类似的方法,通常可以更简单地将每个键转换为枚举,然后使用逻辑运算符组合它们,得到一个可以在开关中使用的数字(这也是使用开关而不是使用串与级联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;
    }
    
        7
  •  0
  •   SingleNegationElimination    15 年前

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

    #! /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))
    

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

        8
  •  0
  •   RED SOFT ADAIR    15 年前

    如果C++是一种选择,我会尝试使用虚拟函数,或者可能双调度。这可以使它更干净。但只有当你有更多的病例时,它才有可能得到回报。

    这个 article on DDJ.com 可能是个不错的入口。

        9
  •  0
  •   David R Tribble    15 年前

    如果您只是想消除两级switch/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;
    }
    
        10
  •  0
  •   yu_sha    15 年前

    如果您的语言是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
    }
    
        11
  •  0
  •   Bastien Vandamme    15 年前

    解决方案 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();