【问题标题】:Eliminate tail recursion from selection sort从选择排序中消除尾递归
【发布时间】:2014-02-19 02:38:14
【问题描述】:

代码:(我尝试翻译成 Java。它是我不知道的另一种语言):

selection_sort(int a, int b){
  if(a == b+1){
      //do nothing
  }else{
      i = minIndex(arr, a, b); //minIndex finds minimum value of 2 index's an array
      if(i != a)
          swap(arr, i, a);
      selection_sort(a+1, b);
  }
}

作业问题要求消除此代码中的尾递归。这是否意味着用迭代循环替换最后一个递归调用?或者添加额外的递归调用?

我认为问题可能源于我不知道常规递归算法和尾递归算法之间的区别。如果我错了,请纠正我,但我的理解是尾递归用于通过减少递归调用的数量来提高效率,除非调用将是代码中的最后一条指令。关键是更少的递归调用意味着更少的递归堆栈存储在内存中。

编辑:修复了代码中的交换调用错误。

【问题讨论】:

    标签: algorithm recursion


    【解决方案1】:

    基本上,尾递归是函数调用自身时所拥有的,但之后不需要做任何事情,除了可能返回递归调用的结果。这是递归的一个特例……不在于它是如何工作的,而是在于它很容易变成一个循环。对于不优化尾递归的编译器/解释器,这可能意味着正常工作和溢出调用堆栈之间的区别。

    例如:

    void countdown(int t) {
        if (t <= 1) return;
        printf("%d\n", t);
        countdown(t - 1);
    }
    

    这是尾递归,因为在countdown 调用自身之后不需要发生任何其他事情。

    对比

    int factorial(int n) {
        if (n <= 1) return 1;
        return n * factorial(n - 1);
    }
    

    不是尾递归的。不同的是,函数调用自身后需要重新获得控制权,所以可以将结果乘以n

    (额外的一点是:您可以使用一种称为“连续传递样式”的技术将其转换为尾递归函数。基本上,在这种情况下,您可以将结果与其他参数一起传递。)

    int factorial(int n, int product = 1) {
        if (n <= 1) return product;
        return factorial(n - 1, product * n);
    }
    

    但我离题了。

    无论如何,删除尾递归的编译器基本上只是调整当前调用帧,使变量成为调用时的样子,然后跳回到函数的开头。你的老师可能不会很喜欢那样,有几个很好的理由。但让我们从那里开始吧。

    selection_sort(int a, int b){
      tail_call:
        if(a == b+1){
          //do nothing
        }else{
          i = minIndex(arr, a, b);
          if(i != a)
              swap(arr, i, a);
    
          // tweak the variables so the next iteration sees (a+1, b)
          a += 1;
          goto tail_call;
        }
    }
    

    虽然我们正在重新排列东西,但空的条件块相当可怕。这也可以改写为

    selection_sort(int a, int b){
      tail_call:
        if(a != b+1){
          i = minIndex(arr, a, b);
          if(i != a)
              swap(arr, i, a);
    
          // tweak the variables so the next iteration sees (a+1, b)
          a += 1;
    
          goto tail_call;
        }
    }
    

    现在,您可能已经被告知goto 是魔鬼。 :) (这有点言过其实,但它不应该用在有更好选择的地方。)所以重构以摆脱它。

    我们如何摆脱它?我们找到了一个控制结构,它可以完成goto 试图做的事情。在这种情况下,while 循环非常符合要求。 condition 中没有范围差异,此代码:

    loop:
        if (condition) {
            ...magic...
            goto loop;
        }
    

    和这个完全一样

    while (condition) {
        ...magic...
    }
    

    以至于许多编译器甚至会为两者生成完全相同的代码。

    所以让我们用while 循环清理代码。

    selection_sort(int a, int b){
        while (a != b+1) {
            int i = minIndex(arr, a, b);
            if(i != a)
                swap(arr, i, a);
    
            a += 1;
        }
    }
    

    【讨论】:

    • 非常感谢您一步步指导我。不过,在 goto 部分,你确实失去了我。我实际上从未使用过 goto,并认为这只是一个伪代码。是实际代码吗?
    • @yazan:在许多语言中都是这样。 Java 没有goto,所以如果你坚持使用它,你可以认为它是一个伪代码。但 C'ish 家族中的大多数其他语言(以及许多其他语言,就此而言)都可以。
    • 谢谢你:)。我再次感谢它
    【解决方案2】:

    你可以在这里看到关于尾递归What is tail recursion?What is tail-recursion elimination?,你可以试试这个代码,我没有测试,只是想法

    selection_sort(){
      int firstIndex = 0;
      int lastIndex = arr.length;
      while(firstIndex < lastIndex) {
          int i = minIndex(arr, firstIndex, lastIndex);
          if(i != a) {
              swap(entries, i, firstIndex); 
          }
          firstIndex ++;
      }
    }
    

    因为你可以看到尾递归版本,它只是更新a,所以我们可以尝试while循环

    【讨论】:

    • 谢谢。这是有道理的
    猜你喜欢
    • 2015-02-13
    • 2015-08-13
    • 2018-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-21
    • 2010-11-17
    • 2019-05-09
    相关资源
    最近更新 更多