算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。
那么我们应该如何去衡量不同算法之间的优劣呢?
主要还是从算法所占用的「时间」和「空间」两个维度去考量。
因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。
算法复杂度分析是评估算法效率和资源消耗的关键方法。它主要关注两个方面:
我们通常使用大O表示法 (Big O Notation) 来描述复杂度,它表示算法执行时间/空间消耗的上界,关注的是当输入规模n
趋向于无穷大时的增长率,忽略常数项和低阶项。
常见的复杂度级别(从低到高):
n
的对数增长。n
线性增长。n
的平方增长,常见于简单排序算法(如冒泡、选择)。n
的立方增长。n
指数级增长,非常慢。n
。n
的执行次数函数 f(n)
。f(n)
是一个多项式,只保留最高阶项。关注点:
识别额外空间
:确定算法在执行过程中除了存储输入数据本身外,还需要哪些额外的存储空间。这包括:
计算额外空间大小:计算这些额外空间相对于输入规模n
的大小函数 g(n)
。
表示为大O:O(g(n))
即为空间复杂度。
注意:
package main
import "fmt"
// linearSearch 在一个整数切片中查找目标值
// arr: 输入的整数切片
// target: 要查找的目标值
// 返回目标值的索引,如果未找到则返回 -1
func linearSearch(arr []int, target int) int {
// 1. n 是输入切片 arr 的长度
n := len(arr)
// 2. 这是一个单层循环
// 在最坏情况下(目标值在最后或不存在),循环会执行 n 次
for i := 0; i < n; i++ {
// 3. 核心操作是比较 arr[i] 和 target
if arr[i] == target {
return i // 找到目标,立即返回
}
}
return -1 // 遍历完整个切片都未找到
}
func main() {
data := []int{10, 20, 80, 30, 60, 50, 110, 100, 130, 170}
target1 := 30
target2 := 99
index1 := linearSearch(data, target1)
fmt.Printf("Target %d found at index: %d\n", target1, index1) // Target 30 found at index: 3
index2 := linearSearch(data, target2)
fmt.Printf("Target %d found at index: %d\n", target2, index2) // Target 99 found at index: -1
}
复杂度分析 (linearSearch):
n = len(arr)
是输入规模。for
循环在最坏情况下(目标元素在最后或不存在)会执行 n
次。arr[i] == target
)是常数时间 O(1)。n
成正比。所以时间复杂度是 O(n)。n
(存储长度), i
(循环计数器)。n
的变化而变化。arr
和 target
的空间不计入额外空间复杂度。package main
import "fmt"
// bubbleSort 使用冒泡排序对整数切片进行升序排序
// arr: 输入的整数切片
func bubbleSort(arr []int) {
// 1. n 是输入切片 arr 的长度
n := len(arr)
if n <= 1 {
return // 如果切片为空或只有一个元素,则无需排序
}
// 2. 外层循环控制排序的轮数
// 对于长度为 n 的数组,最多需要 n-1 轮
for i := 0; i < n-1; i++ {
swapped := false // 优化:如果在一轮中没有发生交换,说明数组已经有序
// 3. 内层循环进行相邻元素的比较和交换
// 每一轮会将当前未排序部分的最大(或最小)元素“冒泡”到正确位置
// 内层循环的次数会随着 i 的增加而减少:n-1, n-2, ..., 1
for j := 0; j < n-1-i; j++ {
// 4. 核心操作是比较和可能的交换
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j] // 交换
swapped = true
}
}
if !swapped { // 如果本轮没有发生任何交换,则数组已经完全排序
break
}
}
}
func main() {
data1 := []int{64, 34, 25, 12, 22, 11, 90}
fmt.Println("Original array:", data1)
bubbleSort(data1)
fmt.Println("Sorted array: ", data1) // Sorted array: [11 12 22 25 34 64 90]
data2 := []int{1, 2, 3, 4, 5}
fmt.Println("Original array:", data2)
bubbleSort(data2)
fmt.Println("Sorted array: ", data2) // Sorted array: [1 2 3 4 5] (优化会提前结束)
}
复杂度分析 (bubbleSort):
n = len(arr)
是输入规模。n-1
次(近似 n
次)。n-1
次比较n-2
次比较1
次比较(n-1) + (n-2) + ... + 1 = n*(n-1)/2
。n^2
级别的操作数。去掉常数系数 1/2
和低阶项 -n/2
,得到 O(n^2)。swapped
优化,外层循环只执行一次,内层循环执行 n-1
次,时间复杂度为 O(n)。但大O通常指最坏情况。n
(存储长度), i
(外层循环计数器), j
(内层循环计数器), swapped
(布尔标志)。n
的变化而变化。n
相关的数组或数据结构。归并排序是一种分治算法,通过递归地将数组分成两半,分别排序后合并。
package main
import "fmt"
// MergeSort 归并排序主函数
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)
}
// merge 合并两个有序数组
func merge(left, right []int) []int {
result := make([]int, 0, len(left)+len(right))
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{3, 1, 4, 1, 5, 9, 2, 6, 5, 3}
sorted := MergeSort(arr)
fmt.Println("排序后数组:", sorted)
}
复杂度分析:
merge
操作需要比较和合并两个子数组,总共处理 (n) 个元素,因此每层的工作量为 (O(n))。merge
操作中需要一个额外的数组来存储合并结果,大小为 (n),因此额外空间为 (O(n))。排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
插入排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
希尔排序 | O(nlogn)~O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n2) | O(logn)~O(n) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n2) | O(n+k) | 稳定 |
基数排序 | O(n×k) | O(n×k) | O(n×k) | O(n+k) | 稳定 |
n
很大时会比 O(n log n) 算法慢得多。testing
包中的基准测试 (benchmark) 功能来实际测量代码段的执行时间,这对于比较不同实现或优化效果很有帮助。但基准测试结果受具体机器和环境影响,而复杂度分析提供的是更通用的理论指导。