回溯和递归有什么区别?

34

回溯和递归有什么区别?这个程序是如何工作的?

void generate_all(int n)
{
    if(n<1) printf("%s\n", ar);
    else{
            ar[n-1]='0';        //fix (n)th bit as '0'
            generate_all(n-1);  //generate all combinations for other n-1 positions.
            ar[n-1]='1';        //fix (n)th bit as '1'
            generate_all(n-1);  //generate all combinations for other n-1 positions.
    }
}

4
我认为你最好把问题表述得更清楚一些。在你提供的代码中,ar 甚至没有被定义。 - Asherah
好问题!如你所示的递归,作为实现机制用于完全枚举所有可能的结果;在基本情况下不仅仅打印,添加一个测试,一个条件打印,当测试通过时,以及可选的退出,你就拥有了一个针对特定问题的迷你Prolog。 - Will Ness
9个回答

57

那个问题就像问汽车和DeLorean之间的区别。

在递归中,函数调用自身直到达到基本情况。

在回溯中,您使用递归以便探索所有可能性,直到获得问题的最佳结果。

有点难理解,我附上了这里的一些文本:

“概念上,你从树的根开始;树可能有一些好叶子和一些坏叶子,尽管可能是所有叶子都是好的或所有叶子都是坏的。您想要到达一个好的叶子。在每个节点上,从根开始,您选择其中一个子节点移动,并继续保持这种情况,直到到达一个叶子。

假设你到达了一个坏的叶子。您可以通过撤销最近的选择并尝试在该选项集中尝试下一个选项来回溯以继续搜索好叶子。如果选项用完,则撤销使您到达此处的选择,并在该节点尝试另一个选择。如果您在没有剩余选项的情况下最终到达根,则无法找到好的叶子。

这需要一个例子:

带有坏节点和一个好节点的树

您的代码片段只是递归,因为如果结果不符合目标,您永远不会返回。


不,OP代码在第一次尝试ar[n-1]='0';后会回退,并将其恢复为ar[n-1]='1';,然后再次前进,选择一个新的选项。至于你的图片,在树中可能有多个好的叶子,特别是每个叶子都可以是好的。回溯通过枚举和测试所有节点/叶子来列举出所有好的叶子,如果每个节点/叶子都是好的,则最终将报告/收集/生成所有叶子。这正是OP代码的情况。 - Will Ness
回溯实际上并不一定需要使用递归 - 它只是意味着重新访问之前做出的决策点,并考虑新的信息(即你所选择的路径)。 - ryanwebjackson

20

递归是指调用当前函数本身的行为。典型的例子是阶乘函数,即类似于:

int fact(int n) {
    int result;
    if(n==1) return 1;
    result = fact(n-1) * n;
    return result;
}

您在这里看到的是事实自身调用。这就是所谓的递归。您总是需要一个使递归停止的条件。这里是if(n==1),结合每次调用时n都会减少(fact(n-1))的事实。

回溯是一种算法,它尝试在给定参数的情况下找到解决方案。它为解决方案构建候选项,并放弃无法满足条件的候选项。典型的解决任务的例子是八皇后问题。回溯也常用于神经网络中。

您描述的程序使用递归。类似于阶乘函数,它将参数减少1,并在n<1时结束(因为那样它将打印ar而不是执行其余部分)。


不,OP程序会减少参数,继续使用该值,然后返回,将ar[n-1]更改为'1',再次使用相同的(n-1)值前进。 - Will Ness

10

递归 - 不放弃任何值;

回溯 - 放弃一些解决方案候选;

同时注意,回溯在函数体内可能会多次调用自身,而递归通常不会如此。


没有要求放弃某些解决方案。如果每个列举的解决方案都是好的,那就这样吧。这仍然是一种回溯。同样地,如果程序在回溯后发现没有更多的选择可供做出,它只会从那个层级返回,所以无论那是第一个且唯一的选择还是有更多选择,都无关紧要。列表是一棵退化的树,但它仍然是一棵树。 :) - Will Ness

7

在我看来,回溯是一种算法,就像其他算法(如BFS和DFS)一样,但递归和迭代都是方法,它们处于比算法更高的层次。例如,要实现DFS,你可以使用相当直观的递归,也可以使用带有堆栈的迭代,或者你也可以认为递归和迭代只是支持算法的方法。


3

递归 就像一种自下而上的过程。你可以通过使用子问题的结果来解决问题。

例如,使用递归反转链表只需要在已反转的子列表上添加头节点。https://leetcode.com/problems/reverse-linked-list/discuss/386764/Java-recursive

回溯 仍然像一种自上而下的过程。有时候,你不能仅通过使用子问题的结果来解决问题,你需要传递你已经得到的信息给子问题。这个问题的答案将在最底层计算,然后这些答案将带着你沿途获取的信息返回到问题中。


解决这个问题的答案将在最低层级上产生,这是回溯法与(结构/原始)递归最重要的区别。递归是从底部到顶部逐步构建结果,并最终在顶部生成(产生)。而对于一般递归,我们无法预知其行为。 - Will Ness

2

递归 -

  • 将问题分解为同类型的较小问题。
  • 需要设定基本情况。

回溯 -

  • 一种通用算法技术,可以考虑所有可能的组合来解决计算问题。

2
递归就像你展示的那样。如果函数A调用A,或者A调用B并且B调用A,那么这就是递归。
回溯不是一个算法,而是一种控制结构。使用回溯时,程序似乎能够向后运行。在编写类似国际象棋游戏的程序时非常有用,因为您想要预测几步。当程序想要进行移动时,它选择一种移动方式,然后切换到其镜像对手程序,该程序选择一种移动方式,以此类推。如果初始程序到达“好位置”,它希望说“耶”并执行它所做的第一步。如果任何程序到达“坏位置”,它都希望退出并尝试另一种移动方式。
您可以将此视为搜索树,但如果移动选择很复杂,则将其视为程序“备份”到上次选择移动的最后位置,选择不同的移动方式,然后再次前进可能更直观。
好了,如果这个想法有吸引力,怎么做呢? 首先,您需要一种表示选择(选择移动)的语句,您可以“备份”到并修改您的选择。 其次,在像A;B这样的一系列语句中,您将A设置为一个函数,并传递一个能够执行B的函数。类似于A(lambda()B(...))。因此,在A执行完成之前,它在返回之前调用其调用B的函数。 如果A想要“失败”并开始“向后运行”,它只需返回而不调用调用B的函数。 我知道这很难理解。我已经通过LISP中的宏来实现它,并且效果很好。在像C++这样的普通编译器语言中进行操作非常困难。
我已经在C/C++中完成了类似的操作,以保留“漂亮性”,但实际上并没有向后运行。 这个想法是您正在执行某种深度优先搜索树的操作。 但是,您可以将其作为从树的根部进行的一系列“刺”,每次“刺”都按不同的路径进行。 这可能会因性能问题而受到反对,但实际上并没有太多的成本,因为大部分工作发生在树的叶子上。如果树有3层深度,并且每个节点的分支因子为5,则意味着它有5 + 25 + 125或155个节点。但是在从根部进行的一系列“刺”中,它访问了125 * 3 = 375个节点,性能损失不到3倍,这可能是可以接受的,除非性能真正成为问题。 (请记住,真正的回溯可能涉及相当多的机制,生成lambda等。)
以下是我使用的基本代码:
#define NLEVEL 20
int ia[NLEVEL];
int na[NLEVEL];
int iLevel = 0;
int choose(int n){
    if (ilevel >= ns){ na[ns]=n; ia[ns]=0; ns++; }
    return ia[ilevel++];
}
void step(){
    while (ns > 0){
        if (++ia[ns-1] >= na[ns-1]) ns--;
        else break;
    }
}
bool search(int iLevel){
    iLevel++;
    switch(choose(2)){
    break; case 0:;
        // see if 0 is a win. if not, fail by returning false
    break; case 1:
        // choose move 1 and recur
        return search(iLevel);
    }
    return true;
}
// this is the "top level" routine
void running(){
    ns = 0;
    // repeat for stabs into choice tree until success
    do {
        bool bSuccess = search(0);
        if (bSuccess){
            // Yay!
            break;
        }
        step(); // this advances the stack of choices
    } while(ns > 0); // stop when success or there are no more choices
}

我使用这段代码来构建一个自制的定理证明器,取代了search函数。


1
首先了解什么是回溯法:
回溯法是一种解决问题的方法,它涉及探索所有可能的路径。
这种方法假设我们有一个问题,并且有多个可用选项来进行解决。假设一个或多个(可能为零)选项可以导致完整的解决方案。然后我们逐个探索所有可用的选项。
请注意,在探索一个选项之后,如果我们没有找到任何解决方案,或者在需要多个解决方案的情况下,我们会回到相同的先前状态(回溯步骤),并探索其他可用的选项。

Explained in image

现在这种方法可以通过迭代或递归来实现。
所以简单地说,回溯是一种可以使用递归算法来实现的方法。

0
递归通常采用自顶向下的方法,将问题分解为更小的可管理子问题,直到达到基本情况,然后将结果组合起来得到最终解决方案。它从顶部开始,并沿着路径一直到达基本情况。
另一方面,回溯通常采用系统性的方法。它们逐步探索解空间,在每个步骤尝试不同的选项。如果它们达到无效或不满意的状态,它们会回溯并探索替代选择。这个过程持续进行,直到找到一个有效的解决方案或所有可能性都被耗尽。
回溯算法可以看作是一种系统地探索解空间的方法,通过尝试选项并在必要时进行回溯来测试不同的组合和配置。关键思想是做出选择,探索其后果,如果导致死路,则撤销选择(回溯)并尝试其他选择。

1
您的回答可以通过附加支持性信息来改进。请编辑以添加更多细节,如引用或文档,以便他人可以确认您的回答是否正确。您可以在帮助中心找到有关编写良好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接