前置知识:无
递归
用获取数组最大值这个例子来理解递归
func getMax(arr []int) int {
n := len(arr)
if n == 0 {
panic("arr is empty")
}
return process(arr, 0, n - 1)
}
func process(arr []int, left, right int) int {
if left == right { // base case
return arr[left]
}
mid := left + (right - left) >> 1
lMax = process(arr, left, mid) // 自己调用自己,递归
rMax = process(arr, mid + 1, right)
return max(lMax, rMax)
}
base case
递归压栈终止条件,子问题已经不能再分解了
从思想上理解递归
对于新手来说,递归去画调用图是非常重要的,有利于分析递归
从实际上理解递归
递归不是玄学,底层是利用系统栈(函数调用栈)来实现的
任何递归函数都一定可以改成非递归
不用系统帮你压栈(系统栈空间),自己压栈呗(内存空间)
递归改成非递归的必要性
- 工程上几乎一定要改,除非确定数据量再大递归也一定不深,归并排序、快速排序、线段树、很多的平衡树等,后面都讲
- 算法笔试或者比赛中(能通过就不改)
master 公式
master 公式用于估算子问题规模相同的递归算法的时间复杂度
所有子问题规模相同的递归才能用 master 公式
master 公式 :,、、 都是常数
- :几个子问题(一个递归会开几个子递归)
- :一个子问题处理 的数据量(一个子递归处理父递归的多少数据量)
- :非递归部分的时间复杂度
计算递归算法时间复杂度:
- 如果 ,复杂度为:
- 如果 ,复杂度为:
- 如果 ,复杂度为:
一个补充:,虽然它不符合 master 公式的形式,但是比较常见
其时间复杂度是 ,证明过程比较复杂,记住即可
举例 1
用 master 公式计算上面 获取数组最大值递归算法 的时间复杂度
func process(arr []int, left, right int) int {
// O(1)
if left == right {
return arr[left]
}
// O(1)
mid := left + (right - left) >> 1
// 两个子递归,每个子递归处理一半数据
lMax = process(arr, left, mid)
rMax = process(arr, mid + 1, right)
// O(1)
return max(lMax, rMax)
}
每次递归将一个问题分解成两个子问题,每个子问题处理当前递归 的数据(一共两个子问题,两个子问题规模相同),即:所有子问题规模相同,可以用 master 公式
,代入 master 公式:,,所以该递归算法的时间复杂度为
举例 2
修改一下:
func process(arr []int, left, right int) int {
// O(1)
if left == right {
return arr[left]
}
// O(1)
leftThreeTwo := left + (right - left) / 3 * 2
rightThreeTwo := right - (right - left) / 3 * 2
// 两个子递归,每个子递归处理 2/3 数据
lMax = process(arr, left, leftThreeTwo)
rMax = process(arr, rightThreeTwo, right)
// O(n)
for i := left; i <= right; i++ {
fmt.Println(arr[i])
}
// O(1)
return max(lMax, rMax)
}
,,,时间复杂度
举例 3
修改一下:
func process(arr []int, left, right int) int {
// O(1)
if left == right {
return arr[left]
}
// O(1)
leftThreeTwo := left + (right - left) / 3 * 2
// 两个子递归
lMax = process(arr, left, leftThreeTwo) // 处理 2/3 数据
rMax = process(arr, leftThreeTwo + 1, right) // 处理 1/3 数据
// O(1)
return max(lMax, rMax)
}
两个子问题规模不同,不适用 master 公式