10.递归

递归是一种应用非常广泛的算法(或者编程技巧)

很多数据结构和算法的编码实现都要用到递归,比如 DFS 深度优先搜索、前中后序二叉树遍历等等

递归需要满足的三个条件

1. 一个问题的解可以分解为几个子问题的解

2. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样

3. 存在递归终止条件

分析:

有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶有多少种走法?如果有 7 个台阶,你可以2,2,2,1 这样子上去,也可以 1,2,1,1,2 这样子上去,总之走法有很多,那如何用编程求得总共有多少种走法呢...

1.第一步走法分两类:走一步和走两步;

2.n个台阶的走法 = n-1个台阶的走法 + n-2个台阶的走法,即f(n) = f(n-1) + f(n-2)

3.终止条件:当只有一个台阶的时候只有1中走法,当只有两个台阶的时候,只有两种走法,即f(1) = 1, f(2) = 2.

到达n阶只可能来自n-1和n-2,所以f(n)=f(n-1)+f(n-2),n = 1时,f(1) = 1, n = 2时, f(n) = 2.

f(1) = 1;
f(2) = 2; 
f(n) = f(n-1)+f(n-2)

找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码

计算机擅长做重复的事情,所以递归正和它的胃口。而我们人脑更喜欢平铺直叙的思维方式。当我们看到递归时,我们总想把递归平铺展开,脑子里就会循环,一层一层往下调,然后再一层一层返回,试图想搞清楚计算机每一步都是怎么执行的,很容易被绕进去。

编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。

注意:

1.递归代码要警惕堆栈溢出

函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。

避免溢出方式:

限制递归调用的最大深度,如1000.只在最大深度比较小的场合使用。

// 全局变量,表示递归的深度。 
depth := 0
func f(n int) (int,error) {
     ++depth
     if depth > 1000 { 
          return 0,error 
     } 
    if n == 1{
         return 1,nil
     } 
    return f(n-1) + 1,nil 
 }

2.要警惕重复计算

如下图,计算台阶,f(3)被计算多次。

避免方式:

使用一个数据结构如散列表,保存已经求解过的f(k)。当递归调用f(k)的时候,先在散列表中查看是否已经求解过了,如果是,直接从散列表中返回。

func f(n int) int{
    if n == 1{
        return 1
    }
    if n == 2{
        return 2
    }
    // hasSolvedList 可以理解成一个 Map,key 是 n,value 是 f(n)
   if hasSolvedList.containsKey(n){
        return  hasSovledList.get(n)
  }

   ret := f(n-1) + f(n-2)
   hasSovledList.put(n, ret)
   return ret
}

3.时间成本和空间成本

递归代码中多次调用函数,当调用的数量较大时,会有很高的时间成本;

每调用一次就要在内存栈中保存一次现场数据;

递归代码改非递归代码

func f(n int)int {
    if n == 1{
        return 1
    } 
     if n == 2{
        return 2
     }
  
   ret := 0
  pre := 2
  prepre := 1
  for i := 3;  i <= n; i++ {
    ret = pre + prepre
    prepre = pre
    pre = ret
  }
  return ret
}

推荐人递归中环的检测:

检测环可以构造一个散列表。每次获取到上层推荐人就去散列表里先查,没有查到的话就加入,如果存在则表示存在环了。当然,每一次查询都是一个自己的散列表,不能共用。

递归实现阶乘

package Recursion

// 迭代实现阶乘
type Fac struct {
	val map[int]int
}

func  NewFactorial(n int) *Fac {
	return &Fac{
		make(map[int]int, n),
	}
}

func (fac *Fac) Factorial(n int) int {
	if fac.val[n] != 0{
		return fac.val[n]
	}

	if n <= 1{
		fac.val[n] = 1
		return 1
	}else {
		res := n * fac.Factorial(n-1)
		fac.val[n] =res
		return res
	}
}

func (fac *Fac) Print(n int )  {
	println(fac.val[n])
}

实现一组数据集合的全排列

package Recursion

import (
	"fmt"
)
// 实现一组数据集合的全排列
type RangeType struct {
	value []interface{}
}

func NewRangeArray(n int) *RangeType  {
	return &RangeType{
		make([]interface{},n),
	}
}

func (slice *RangeType)RangeALL( start int)  {
	len := len(slice.value)
	if start == len-1{
		// 如果已经是最后位置,直接将数组数据合并输出
		fmt.Println(slice.value)
	}

	for i:=start; i<len; i++{
		// i = start 时输出自己
		// 如果i和start的值相同就没有必要交换
		if i==start || slice.value[i] != slice.value[start]{
			//交换当前这个与后面的位置
			slice.value[i], slice.value[start] = slice.value[start], slice.value[i]
			//递归处理索引+1
			slice.RangeALL(start+1)
			//换回来,因为是递归,如果不换回来会影响后面的操作,并且出现重复
			slice.value[i], slice.value[start] = slice.value[start], slice.value[i]
		}
	}
}
发布了127 篇原创文章 · 获赞 24 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/Linzhongyilisha/article/details/100054574
今日推荐