时间复杂度
时间复杂度是算法效率的一个重要指标,它描述了算法运行时间与输入数据量之间的关系,给出了算法运行时间的上界估计。
时间复杂度主要关注算法中基本操作的执行次数,特别是当输入规模(通常用n表示)增大时,这些操作次数的增长趋势。在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度为O(f(N))。算法的时间复杂度通常为最坏情况下的时间复杂度。下面通过几个例子来说明如何计算时间复杂度:
-
常数时间复杂度O(1):一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。这些操作的时间复杂度被标记为 O(1)。例如赋值操作
x=1
,不论给x赋值为多少,这个操作的时间都不变。 -
线性时间复杂度O(n):
def linear_time_example(arr):result = 0for i in range(len(arr)):result += arr[i]return result
这个函数遍历数组arr的每一个元素并求和。如果数组长度为n,则循环将执行n次,因此时间复杂度为O(n)O(n)。
-
平方时间复杂度 O(n^2)
def quadratic_time_example(matrix):result = 0for i in range(len(matrix)):for j in range(len(matrix[i])):result += matrix[i][j]return result
在这个例子中,我们有两个嵌套循环,它们都依赖于矩阵的行数(假设矩阵为方阵)。每个循环都将执行n次,所以总的操作次数为n×n=n2,因此时间复杂度为O(n2)。
-
对数时间复杂度O(logn)
def binary_search(arr, target):low, high = 0, len(arr) - 1while low <= high:mid = (low + high) // 2if arr[mid] == target:return midelif arr[mid] < target:low = mid + 1else:high = mid - 1return -1
上述代码为二分查找实现,每次迭代,搜索范围都会减半,因此操作次数与输入大小的对数成比例,即时间复杂度为O(logn)。
-
线性对数时间复杂度O(nlogn)
归并排序是一个典型的O(nlogn)算法。它将数组分为两半,递归地排序每一半,然后合并两个有序的部分。虽然每个递归调用的时间复杂度为O(n),但由于递归深度为logn,总的时间复杂度为O(nlogn)。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是“常数项时间”。
空间复杂度
空间复杂度是衡量算法在运行过程中所需存储空间的一个指标。它描述了算法消耗的内存与输入数据量之间的关系,通常也使用大O符号(Big O notation)来表示。空间复杂度分析包括了算法运行时临时占用的额外空间,但不包括输入数据本身占用的空间(除非算法需要复制输入数据)。
以下是一些常见的空间复杂度类别:
- 常数空间复杂度 - O(1):算法所需的额外空间不随输入数据量的增加而改变。例如,一个简单的函数,仅使用几个变量进行计算,无论输入数据多大,所占空间都是固定的。
- 线性空间复杂度 - O(n):算法所需的额外空间直接与输入数据量成正比。例如,在排序算法中,如果需要创建一个与输入数组同样大小的新数组,则空间复杂度为O(n)。
- 对数空间复杂度 - O(logn):这种空间复杂度较少见,通常发生在使用分治策略的算法中,如一些递归算法,它们可能只需要存储输入数据的层次结构,而非全部数据。
- 平方空间复杂度 - O(n2):如果算法需要创建一个二维数组,且每个维度的大小都与输入数据量相关,那么空间复杂度可能是O(n2)。
- 递归算法的空间复杂度:递归算法的空间复杂度通常由递归调用栈的深度决定。例如,二叉树的前序遍历在最坏情况下可能需要O(n)的空间,因为递归调用栈的深度可能达到树的高度。
空间复杂度中最常见的就是O(1)和O(n)。理解空间复杂度对于优化算法和系统设计非常重要,特别是在资源受限的环境中,如嵌入式系统或移动设备。有时候,为了节省空间,可能需要牺牲时间效率,反之亦然。因此,在实际应用中,经常需要在时间和空间之间做出权衡。