这道题博主今年在面试中就遇到过两次,分别是 富途和 LiblibAI
其实是一道简单题,但是要求你理解递归,回溯的核心思想,或者动态规划的优化解法,以及能分析出时空复杂度
解法一:回溯法(超时)
套用我们前面学习的框架即可
func climbStairs(n int) int {
var res int
total := 0
backtrack(n, total, &res)
return res
}
func backtrack(n int, total int,res *int){
if total >= n{
if total == n{
*res++
}
return
}
// 本次选择走一步
total+=1
backtrack(n, total, res)
total-=1
// 本次选择走2步
total+=2
backtrack(n, total, res)
total-=2
}
递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间。
首先计算子问题个数,即递归树中节点的总数,每个节点有2个分叉,分别决定走一步还是走2步。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。
然后计算解决一个子问题的时间,在本算法中,没有循环,只有一个简单的步数加法操作,时间为 O(1)。
所以,这个算法的时间复杂度为二者相乘,即 O(2^n),指数级别,时间复杂度太高了。
空间复杂度即为递归栈空间大小,O(n)。
解法二:动态规划(自顶向下的递归+备忘录)
核心是找到状态转移方程
func climbStairs(n int) int {
memo := make([]int, n+1)
return dp(memo, n)
}
func dp(memo []int, n int) int{
if n <= 2{
return n
}
if memo[n] > 0{ // 备忘录避免重复计算
return memo[n]
}
// 状态转移方程
// 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数 + 爬到 n - 2 的方法个数
memo[n] = dp(memo, n-1) + dp(memo, n-2)
return memo[n]
}
时间复杂度:O(n)。由于每个状态只会计算一次,动态规划的时间复杂度 = 状态个数 × 单个状态的计算时间。本题状态个数等于 O(n),单个状态的计算时间为 O(1),所以动态规划的时间复杂度为 O(n)。
空间复杂度:O(n),备忘录大小。
解法三:动态规划(自底向上的递推法)
func climbStairs(n int) int {
dp := make([]int, n+1)
dp[0] = 1
dp[1] = 1
for i:=2; i<len(dp); i++{
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
时间复杂度:O(n),一层循环。
空间复杂度:O(n),额外申请的dp数组空间
优化空间
观察状态转移方程,发现一旦算出 dp[i],那么 dp[i−2] 及其左边的状态就永远不会用到了。
意味着每次循环,只需要知道「上一个状态」和「上上一个状态」的dp值,可以直接用两个变量记录即可(dp1, dp2),初始值均为1,在循环中不断更新
func climbStairs(n int) int {
dp_i_1, dp_i_2 := 1, 1 // 分别代表dp[i-1]和dp[i-2]
for i:=2; i<=n; i++{
dp_i := dp_i_1 + dp_i_2
dp_i_2 = dp_i_1
dp_i_1 = dp_i
}
return dp_i_1 // 因为最后一次循环把dp_i赋值给了dp_i_1
}
时间复杂度:O(n),一层循环。 空间复杂度:O(1)