目录
1、背景
分析算法的时间复杂度和空间复杂度是衡量算法性能的关键,它们分别用于评估算法的运行时间和占用内存,本文只讲相关概念和示例。
2、时间复杂度
时间复杂度表示的是算法随着输入规模的增长,执行所需的时间增长的速率,常见时间复杂度如下:
时间复杂度 | 含义 |
---|---|
O(1) | 常数时间复杂度。无论输入规模如何,执行时间始终相同 |
O(log n) | 对数时间复杂度。每次操作都能将问题规模缩小一半,如二分法查找 |
O(n) | 线性时间复杂度。随着输入规模的增加,执行时间成正比增加,如遍历数组 |
O(n log n) | 线性对数时间复杂度。常见于分治算法,如归并排序、快速排序 |
O(n^2) | 二次时间复杂度。通常出现嵌套循环中,如冒泡排序、插入排序 |
O(2^n) | 指数时间复杂度。随着输入规模增加,执行时间呈指数级增长,通常出现在递归算法中 |
O(n!) | 阶乘时间复杂度。通常出现在排列组合相关问题中 |
3、时间复杂度示例
【1】O(1)
go
package main
import "fmt"
func constantTime(arr []int) int {
return arr[0] // 仅访问一次数组元素,不依赖于数组大小
}
func main() {
arr := []int{1, 2, 3, 4, 5}
fmt.Println(constantTime(arr)) // 输出: 1
}
无论数组大小如何,执行时间是固定的。
【2】O(log n)
go
package main
import "fmt"
// 二分查找
func binarySearch(arr []int, target int) int {
left, right := 0, len(arr)-1
for left <= right {
mid := left + (right-left)/2
if arr[mid] == target {
return mid
} else if arr[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
func main() {
arr := []int{1, 3, 5, 7, 9, 11, 13}
fmt.Println(binarySearch(arr, 5)) // 输出: 2
}
每次将搜索区间缩小一半。
【3】O(n)
go
package main
import "fmt"
// 遍历数组
func linearTime(arr []int) int {
sum := 0
for _, v := range arr {
sum += v // 遍历每个元素
}
return sum
}
func main() {
arr := []int{1, 2, 3, 4, 5}
fmt.Println(linearTime(arr)) // 输出: 15
}
遍历整个数组,执行次数与数组大小成正比。
【4】O(n log n)
go
package main
import "fmt"
// 归并排序
func mergeSort(arr []int) []int {
if len(arr) <= 1 {
return arr
}
mid := len(arr) / 2
left := mergeSort(arr[:mid])
right := mergeSort(arr[mid:])
return merge(left, right)
}
func merge(left, right []int) []int {
result := []int{}
i, j := 0, 0
for i < len(left) && j < len(right) {
if left[i] < right[j] {
result = append(result, left[i])
i++
} else {
result = append(result, right[j])
j++
}
}
result = append(result, left[i:]...)
result = append(result, right[j:]...)
return result
}
func main() {
arr := []int{5, 4, 3, 2, 1}
fmt.Println(mergeSort(arr)) // 输出: [1 2 3 4 5]
}
归并排序的时间复杂度是O(n log n),因为它不断将数组分为两半,递归的深度是log n,而合并过程是O(n)。
【5】O(n^2)
go
package main
import "fmt"
// 冒泡排序
func bubbleSort(arr []int) {
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr)-1-i; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j] // 交换元素
}
}
}
}
func main() {
arr := []int{5, 4, 3, 2, 1}
bubbleSort(arr)
fmt.Println(arr) // 输出: [1 2 3 4 5]
}
冒泡排序中有两个嵌套的循环,执行次数为n^2。
【6】O(2^n)
sql
package main
import "fmt"
// 斐波那契递归
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
func main() {
fmt.Println(fibonacci(10)) // 输出: 55
}
递归调用中每个 fibonacci 调用会产生两个新的递归调用,因此递归树的大小是指数级的。
【7】O(n!)
go
package main
import "fmt"
// 全排列
func permute(nums []int) [][]int {
var result [][]int
var backtrack func(nums []int, path []int)
backtrack = func(nums []int, path []int) {
if len(nums) == 0 {
result = append(result, append([]int{}, path...))
return
}
for i := 0; i < len(nums); i++ {
backtrack(append(append([]int{}, nums[:i]...), nums[i+1:]...), append(path, nums[i]))
}
}
backtrack(nums, []int{})
return result
}
func main() {
arr := []int{1, 2, 3}
fmt.Println(permute(arr)) // 输出: [[1 2 3] [1 3 2] [2 1 3] [2 3 1] [3 1 2] [3 2 1]]
}
全排列的个数是 n!,因为每次选择一个元素,并继续递归生成排列。
4、空间复杂度
空间复杂度表示的是算法执行时需要占用的内存空间的增长速率,常见的空间复杂度有:
空间复杂度 | 含义 |
---|---|
O(1) | 常数空间复杂度。算法所需内存空间不会随着输入规模的增加而增加 |
O(n) | 线性空间复杂度。算法的内存需求与输入规模成正比 |
O(n^2) | 二次空间复杂度。通常出现在需要存储二维数据结构的情况下 |
5、空间复杂度示例
【1】O(1)
go
package main
import "fmt"
// 交换两个数
func swap(a, b int) (int, int) {
return b, a
}
func main() {
a, b := 5, 10
a, b = swap(a, b)
fmt.Println(a, b) // 输出: 10 5
}
只用了常量空间存储两个整数。
【2】O(n)
go
package main
import "fmt"
// 创建一个数组
func createArray(n int) []int {
arr := make([]int, n)
return arr
}
func main() {
arr := createArray(5)
fmt.Println(arr) // 输出: [0 0 0 0 0]
}
创建了一个大小为 n 的数组。
【3】O(n^2)
go
package main
import "fmt"
// 创建二维数组
func create2DArray(n int) [][]int {
arr := make([][]int, n)
for i := range arr {
arr[i] = make([]int, n)
}
return arr
}
func main() {
arr := create2DArray(3)
fmt.Println(arr) // 输出: [[0 0 0] [0 0 0] [0 0 0]]
}
创建了一个 n x n 的二维数组。