面试总结之-递归算法分析

递归的分析

         首先是,“你这个递归能不能写成个不需要栈空间的递归?”,答:“尾递归(tail-recursive)”。

好冷~不过真有一个面试官这样问我了。不过尾递归不怎么考,因为你如果能写成尾递归,说明系统也不需要用栈空间来保存递归的状态了,那你把它改成迭代的形式就不难了。

         递归的基本形式是:

 T Recursive(arg_list* args){
 if(args==BASIC_CASE)
    return T(BASIC_CASE);
 for(int i=0;i<n;i++)
    t[i] = Recursive(args+i);
 //combine results
}

         或者说:

T Recursive(arg_list* args){
 if(args==BASIC_CASE)
    return T(BASIC_CASE);
 //process the data
 for(int i=0;i<n;i++)
    t[i] = Recursive(args+i);
}

         两种递归的方式,你也可以说是一样的,但是我觉得这是两种不同的子问题的逻辑。第一种是先递归,再处理(比如MergeSort),一种是先处理,再递归(比如qsort)。一般递归的开始都有一个终止条件,当然也可以没有,只要当到达终止条件之后,所有判断需不需要继续递归下去的条件都为false,自然就终止(比如qsort),不过我觉得,终止条件还是写在函数的递归的开头比较好,这样的话别人看你的递归代码会更加容易理解。递归也是面试考察的重点,在写递归前,首先得想清楚这个题目是不是需要用递归解决(递归写起来更方便?题目定义本来就是递归形式?考官要求递归?= =!),这样说是因为,一般递归程序在时间和空间上都比不上迭代的程序,时间上来说,即使不考虑保存临时变量压栈出栈的消耗,递归的过程也是在解空间搜索,到底再一层层返回最顶层(把你的递归想象成一棵多叉树吧),而迭代只需要搜索到最底层就完成任务了;空间来说,就是栈空间了,为了保存当前程序的状态,递归到下一重时,栈空间的消耗也是不能忽略的,递归太深有时候还会stack overflow。

         讲到栈空间,要提一句的是,面试时有时候会碰到面试官要求一个constant space的程序,这时候你应该问一下面试官,递归的栈空间消耗算不算space。严格来说,这当然算space(栈空间不是空间啊!),不过有时候面试官就是脑抽了,自己觉得递归里面自己不用额外空间就不算了,那你也只能跟着他思维走了。

         确定要写递归之后,要想清楚你的递归要怎么写,几个关键步骤:

1. 递归终止条件,也就是那个问题是你不需要递归解决的,比如if(root==NULL)

2. 先递归还是先处理,参照上面的两个框架

3. 处理的逻辑怎么写,这部分特别搞笑~貌似见过不少人忘记写这一块的了,直接递归下去就觉得程序可以帮你解决这个问题,就像MergeSort,分段去MergeSort之后,回来却不把结果Merge。

         递归面的不少,而且而且,经常是等你写完一个递归程序之后,让你写一个非递归程序,这就转入下一part,怎么递归转非递归。

         理论上,所有递归都能写成非递归,你只要想一下在运行递归程序时,计算机是怎么搞的,然后在你的非递归代码中模拟递归的逻辑就好了:首先,进入递归前,要保存所有在递归结束后需要用到的变量,然后我们定义一个结构体,把这些变量都扔进去;另外,系统的话可以保存函数运行到哪里的信息,递归结束后直接到达递归代码的下一行继续运行,人工模拟递归时一般不会这样搞,这样我们可以需要检查栈顶的结构体,来判断,现在应该怎么做。一般来说,加一个bool变量,表示是不是回溯就好(这个不是一定行得通,有时候可能是其他参数)。具体通过下面一个例子描述:生成输入数组的全排列(假设数组元素都不同)

递归代码:

voidGetPermutation(vector<int>& arr,vector<vector<int>>&result,int k){
 if(k==arr.size()-1){
    result.push_back(arr);
    return;
 }
 for(int i=k;i<arr.size();i++){
    std::swap(arr[i],arr[k]);//i,k交换,然后生成k+1到n的所有排列
    GetPermutation(arr,result,k+1);
    std::swap(arr[i],arr[k]);//还原
 }
}
vector<vector<int>>GetPer(vector<int>& arr){
 vector<vector<int>> result;
 GetPermutation(arr,result,0);
 return result;
}

这是一个比较标准的递归生成所有排列的程序,在把它改成迭代之前,先说说这种程序。这里用到的递归框架应该属于第二种:先处理再递归。递归后面又多做一次swap是因为我改变了原来的数组,如果你copy一个新的数组出来,在后面也可以不用还原(但是会慢)。还有一个特点是,这种递归是在递归的最深处得到结果(其实就是因为先处理再递归嘛~),也由于这个原因,这种递归经常把解作为参数传入,相反,对于那种先递归再处理的,经常把解return回来。

         怎么改成迭代的程序呢,递归的部分是GetPermutation(arr,result,k+1),首先要想想,从这里递归,我们需要什么信息,才能在递归结束之后接着递归后面的语句继续工作,怎么看需要记录什么信息呢,其实就是看这个递归结束之后还需要做什么操作,这些操作需要用到的信息都记录下来就好了。这里应该需要记录i和k的值。也就是,先把pair(0,0)压栈,每次取出栈顶元素处理,如果栈顶元素的k==arr.size()-1,那么本层递归结束,把当前结果保存到result;否则进入循环递归(参考递归代码),如果top.i<arr.size(),表示成功进入循环,这时候首先swap,然后注意了,这里是最容易出错的地方,由于你不知道现在栈顶元素(对应于递归树的某个节点)是第一次经过还是回溯时经过,所以没法判断是不是需要pop掉。我在代码中多加了一个bool的变量来记录是不是回溯,如果是,就不进入子树递归了(也就是不加入新的节点入栈了)。这个过程比较绕,觉得乱的话就根据idea自己想一下要怎么做,然后自己实现一遍再重新看。

Code:

struct Node{
 int i,k;
 bool isBacktrack;
 Node(int a,int b){i=a;k=b;isBacktrack=false;}
};
vector<vector<int>>GetPer(vector<int>& arr){
 vector<vector<int>> result;
 stack<Node> stk;
 stk.push(Node(0,0));
 while(!stk.empty()){
    Node tmp = stk.top();
    if(tmp.k==arr.size()-1){ //这种情况相当于递归代码的终止条件,得pop一下
      result.push_back(arr);
      stk.pop();
    }else if(tmp.i<arr.size()){ //这种情况相当于递归代码在执行for循环
      std::swap(arr[tmp.i],arr[tmp.k]);
      if(!tmp.isBacktrack){   //这部分是for循环的第一个swap语句
        stk.top().isBacktrack = true;
        stk.push(Node(tmp.k+1,tmp.k+1)); //执行完第一个语句之后,会有一个往下一重的递归,所以有一个压栈动作
      }
      else{        //else表示现在是第二个swap,所以要pop掉旧的,进入下一重递归
        stk.pop();    
        stk.push(Node(tmp.i+1,tmp.k));
      }
    }else stk.pop();  //for循环搞完了,相当于另一个终止条件,还得pop一下
  }
 return result;
}

    主要内容写在代码注释了。代码有不少if,else,都是在判断现在到底在相当于递归程序的那一部分。这个代码有点繁琐,逻辑上如果没有递归代码的对比的话,也非常不好懂,写在这里主要是为了举个例子怎么样把递归代码转换成非递归代码。这个转换方法总结下就是:

1. 想好递归需要压栈的信息,封装成一个struct;

2. 想想递归的逻辑(你不能直接保存一个函数的指针),在代码中利用第一步的struct保存的信息来重构这个逻辑。

         要写出一个程序的非递归程序,先想递归再转换成非递归不是必须的,因为可能本来就有更好的非递归写法;真要这样转换时,模仿计算机保存所有需要的信息入栈也不是必须的,只能说这样做是一个普遍适用的方法,作为一个智能生物,对于具体的程序,想到具体的递归转非递归的方法也不奇怪(不像计算机,只能机械解决)。
         最后加几道leetcode上关于递归的题目(关于递归的题目是很多的了~),可以想想非递归解法:

Generate Parentheses

Palindrome Partitioning

Unique Binary Search Trees II

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值