算法与数据结构#理解回溯和递归

大学后两年主要在学习数据分析、数据挖掘算法,好久没有复习数据结构和算法这门课的知识。拾起来的过程不易,多做笔记。

一.递归

程序调用自身的编程技巧称为递归( recursion)。 
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模
较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

通常来说,为了描述问题的某一状态,必须用到该状态的上一个状态;而如果要描述上一个状态,又必须用到上一个状态的上一个状态…… 这样用自己来定义自己的方法就是递归。

递归函数的格式

  • 函数不在递归地情况称作基本情形(base case,也称基本情况)。
  • 函数调用自身来执行子任务的情况就称作递归情形(recursive case)。
if (判断是否为基本情形)
    return 该基本情形时的函数值
else if (判断是否为另一种基本情形)
    return 该基本情形时的函数值
//递归情形
else return (执行某些工作并递归调用)

如利用递归算法阶乘

public static int Print(int n) {
        if (n == 0)
            return 0;
        else {
            System.out.println(n);
            return Print(n - 1);
        }
    }

算法与数据结构#理解回溯和递归_第1张图片

递归和栈有紧密关联。我们可以将递归看做一次次入栈出栈的过程。

对于这样一颗二叉树来说,我们拿中序遍历为例:

算法与数据结构#理解回溯和递归_第2张图片

//中序遍历采用递归的方式
public void inOrder(BinaryTreeNode root){ 
    if(null!=root){
        inOrder(root.getLeft());
        System.out.print(root.getData()+"\t");
        inOrder(root.getRight());
    }
}

执行步骤为:

1.根节点1,根节点不为空,入栈,执行到 inOrder(根节点1,左节点2);
2.节点2,不为空,入栈,执行inOder(节点2, 左节点4);
3.节点4,不为空, 入栈, 执行inOder(节点4, 左节点null);
4.节点null, 为空,结束;
5.接着执行第三步的 System.out.print(), 将节点4的data值 4 输出,此时打印出来的就是 【4】;
6.执行 inOder(结点4, 右节点 null);
7.节点null, 为空,结束, 这样就把第三步的代码就执行完了,节点4出栈,此时出栈的4节点后,下一步就会从节点2开始,此时就发生了回溯;
8.这样系统栈就剩 (从上到下)节点2、节点1,接着执行 System.out.print(),并执行下一步 inOder(节点2, 右节点5),此时打印出来的就是 【4, 2】;
9.接着会重复3、4、5、6、7 ,此时打印出来的就是 【4,2, 5】;
10.这时候将节点 2 出栈,然后执行根节点的剩余的代码,先打印根节点数据,然后执行inOrder(根节点, 右节点3), 此时打印出来的就是 【4,2, 5, 1】;
11.剩下的步骤都是和左子树相同的步骤。

对于步骤中出栈的过程,就可以看作是回溯的过程。

二.回溯

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。

回溯的思路基本如下:当前局面下,我们有若干种选择,所以我们对每一种选择进行尝试。如果发现某种选择违反了某些限定条件,此时 return;如果尝试某种选择到了最后,发现该选择是正确解,那么就将其加入到解集中。 

回溯的格式
我们需要清晰的找出三个要素:选择 (Options),限制 (Restraints),结束条件 (Termination)。

以剑指offer中”矩阵中的路径“为例,我们判断的要素是

0.根据给定数组,初始化一个标志位数组,初始化为false,表示未走过,true表示已经走过,不能走第二次
1.根据行数和列数,遍历数组,先找到一个与str字符串的第一个元素相匹配的矩阵元素,进入judge
2.根据i和j先确定一维数组的位置,因为给定的matrix是一个一维数组
3.确定递归终止条件:越界,当前找到的矩阵值不等于数组对应位置的值,已经走过的,这三类情况,都直接false,说明这条路不通
4.若k,就是待判定的字符串str的索引已经判断到了最后一位,此时说明是匹配成功的
5.下面就是本题的精髓,递归不断地寻找周围四个格子是否符合条件,只要有一个格子符合条件,就继续再找这个符合条件的格子的四周是否存在符合条件的格子,直到k到达末尾或者不满足递归条件就停止。
6.走到这一步,说明本次是不成功的,我们要还原一下标志位数组index处的标志位,进入下一轮的判断。

代码

public class Solution {
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
    {
        //标志位,初始化为false
        boolean[] flag = new boolean[matrix.length];
        for(int i=0;i=rows || j>=cols || matrix[index] != str[k] || flag[index] == true)
            return false;
        //若k已经到达str末尾了,说明之前的都已经匹配成功了,直接返回true即可
        if(k == str.length-1)
            return true;
        //要走的第一个位置置为true,表示已经走过了
        flag[index] = true;
         
        //回溯,递归寻找,每次找到了就给k加一,找不到,还原
        if(judge(matrix,i-1,j,rows,cols,flag,str,k+1) ||
           judge(matrix,i+1,j,rows,cols,flag,str,k+1) ||
           judge(matrix,i,j-1,rows,cols,flag,str,k+1) ||
           judge(matrix,i,j+1,rows,cols,flag,str,k+1)  )
        {
            return true;
        }
        //走到这,说明这一条路不通,还原,再试其他的路径
        flag[index] = false;
        return false;
    }
 
 
}

可以看到回溯之中会用到递归函数,回溯是一种算法思想,它是用递归实现的。回溯的过程类似于穷举法,但回溯有“剪枝”功能,即自我判断过程。例如有求和问题,给定有 7 个元素的组合 [1, 2, 3, 4, 5, 6, 7],求加和为 7 的子集。累加计算中,选择 1+2+3+4 时,判断得到结果为 10 大于 7,那么后面的 5, 6, 7 就没有必要计算了。这种方法属于搜索过程中的优化,即“剪枝”功能。

三.回溯和递归的关系

用一个比较通俗的说法来解释递归和回溯: 
我们在路上走着,前面是一个多岔路口,因为我们并不知道应该走哪条路,所以我们需要尝试。尝试的过程就是一个函数。 
我们选择了一个方向,后来发现又有一个多岔路口,这时候又需要进行一次选择。所以我们需要在上一次尝试结果的基础上,再做一次尝试,即在函数内部再调用一次函数,这就是递归的过程。 
这样重复了若干次之后,发现这次选择的这条路走不通,这时候我们知道我们上一个路口选错了,所以我们要回到上一个路口重新选择其他路,这就是回溯的思想。

 

 

你可能感兴趣的:(算法与数据结构)