Hello大家好!相信在开始进入数据结构与算法的学习时,大家一定会碰到的第一个问题,就是时间与空间复杂度的求解。
简单来说,时间复杂度就是指代码运行的次数,空间复杂度就是指你定义的变量的个数,并且它们指的都是最坏的情况,即最大值。
1、时间复杂度
[1]基础概念
用于描述算法在运行过程中所需要的时间随输入规模变化的增长情况,通常使用大O符号(Big O notation)来表示。常见的有O(1) - 常数时间(这里有一个常见的误区,其中的1不代表一次,指的是常数次,可以是1,2,3,4等等,但不可以是n)、O(n) - 线性空间、O(log n) - 对数时间、O(n log n) - 线性对数时间等等。下面举一个例子,带大家计算一下。
def example_algorithm(n):
# 外层循环,运行 n 次
for i in range(n):
# 中层循环,运行 n 次
for j in range(n):
# 内层循环,运行 3 次
for k in range(3):
# 进行常数时间操作,例如打印或简单计算
print("Operation", i, j, k)
# 另一个单独的循环,运行 2n 次
for i in range(2 * n):
# 进行常数时间操作
print("Another Operation", i)
# 常数时间操作
print("Constant Time Operation")
# 运行示例算法
example_algorithm(2)
可以看出,这个算法的时间复杂度是3n^2+2n+1次,但试想一下当n趋向于无穷大的时候,n^2会是一个极大的数远大于2n+1,并且由于n^2过于大,导致3n^2与n^2并无太大区别,所以时间复杂度就记为O(n^2)
[2]时间复杂度的快慢
常见时间复杂度排序:
O(1) - 常数时间<O(log n) - 对数时间<O(n) - 线性时间<O(n log n) - 线性对数时间<O(n^2) - 二次时间<O(2^n) - 指数时间<O(n!) - 阶乘时间
(私以为大家可以就理解为里面变量的大小比较,比如1肯定是小于n的,不用想的太复杂)
这里再举一个经典的例子,二分法查找为什么是O(log n)?
def binary_search(arr, target):
# 初始化左右指针,分别指向数组的开始和末尾
left, right = 0, len(arr) - 1
# 当左指针不超过右指针时继续循环
while left <= right:
# 计算中间位置,使用整除来确保mid是整数
mid = (left + right) // 2
# 检查中间元素是否是目标元素
if arr[mid] == target:
return mid # 找到目标元素,返回其索引
# 如果中间元素小于目标元素,说明目标元素在右半部分
elif arr[mid] < target:
left = mid + 1 # 更新左指针为中间位置的右边一个位置
# 如果中间元素大于目标元素,说明目标元素在左半部分
else:
right = mid - 1 # 更新右指针为中间位置的左边一个位置
return -1 # 如果循环结束还未找到目标元素,返回-1表示未找到
在每次迭代中,算法通过计算中间位置并比较目标元素,将搜索范围缩小到原来的一半。假设数组长度为 n,那么经过一次迭代后,搜索范围变为 n/2,再经过一次迭代,搜索范围变为 n/4,以此类推。当我们去想最坏情况的时候,就是在分到第k次时,只剩两个数,此时其中一个为目标数。二分法每执行一次,就将数组除了2,只要一半的数组,我们可以称为有效数组,并继续寻找下一半,一共经历了k次迭代。我们从最后一个数往前推,目标数只有一个,即第k次有效数组,此时1=2^0,乘一个2,两个数,即第k-1次有效数组,此时2=2^1,以此类推。当我们乘完最后次,k=0时,回到原始位置,此时数组个数为n,乘了k次2,我们可以得出n=2^k,k为程序运行次数,所以时间复杂度为O(log n)。
#二分法查找极大的节省了运行次数,当n=1000000时,可能要循环1000000次才能找到目标数,但使用二分法的话最多需要约 log2(1000000)≈20 次比较。
2、空间复杂度
[1]基础概念
与时间复杂度类似,用于描述算法在运行过程中所需要的额外空间(内存)随输入规模变化的增长情况,也用大O符号(Big O notation)来表示。常见的有O(1) - 常数空间(这里的声明与时间复杂度一样,都代指常数)、O(n^2) - 二次空间、O(n) - 线性空间等。下面给大家举一个计算斐波那契数列的空间复杂度的例子
首先是用递归的方式进行计算
def fibonacci_d(n):
# 一开始,fibonacci(0) = 0, fibonacci(1) = 1
if n <= 1:
return n
# 递归调用
return fibonacci_d(n-1) + fibonacci_d(n-2)
# 空间复杂度分析:
# 递归调用的最大深度为 n。
# 在递归调用栈中,每个调用都占用一个栈帧。
# 因此,空间复杂度为 O(n)。
#递归调用的最大深度是指在一个程序中,递归函数嵌套调用的最大层数,就是你这次递归反复横跳了多少层
#递归调用的每一个栈都要算在空间复杂度中
接下来我们来看看用迭代的方式
def fibonacci_i(n):
# 一开始,fibonacci(0) = 0, fibonacci(1) = 1
if n <= 1:
return n
# 使用两个变量 a 和 b 来存储前两个斐波那契数
a, b = 0, 1
# 迭代计算斐波那契数
for _ in range(2, n + 1):
a, b = b, a + b
return b
# 空间复杂度分析:
# 只使用了固定数量的变量 a 和 b。
# 变量的数量不随输入规模 n 增长。
# 因此,空间复杂度为 O(1)。
#只使用了固定数量的变量 a和 b
来保存前两个斐波那契数,不会随着输入规模 n
的增加而增加额外的空间。
[2]空间复杂度的大小
常见空间复杂度排序(与时间复杂度类似):
O(1) - 常数空间<O(log n) - 对数空间<O(n) - 线性空间<O(n log n) - 线性对数空间<O(n^2) - 平方空间
3、常见例题分析
这里举一个力扣上面很经典的题目:数组nums包含0到n所有整数,但其中缺了一个。请编写代码找出其中缺失的整数。你有办法在O(n)时间内,O(1)空间内完成吗?
#由于题目限制了时间与空间的复杂度,所以不能够简单的用循环来求解,需要我们另辟蹊径。
码之呼吸·一之型·异或
#异或 (XOR) 是一种位运算符,用于对两个二进制数的每一位进行比较。如果两位相同,则结果为0;如果两位不同,则结果为1。
#异或运算的规则
- 0 XOR 0 = 0
- 1 XOR 1 = 0
- 0 XOR 1 = 1
- 1 XOR 0 = 1
#异或运算具有以下几个重要性质:
1.对任意整数 a,有 a⊕a=0。
2.对任意整数 aaa,有 a⊕0=a。
3.异或运算具有交换律和结合律,即 a⊕b⊕c=a⊕c⊕b。
#举个例子说明一下:
00000000=0
00000010=2
——————
00000010=2
00000001=1
——————
00000011=3
00000001=1
———————
00000010=2
【0与任何数异或都是其本身,不造成影响,在上述例子中,2出现一次,1出现两次,则最后异或得出的结果会是2——只出现了一次的数】
利用这些性质,可以通过对所有数(包括数组中的数和0到n的所有整数)进行异或运算,来找到缺失的整数。
def find_missing_number(nums):
n = len(nums)
# 计算0到n的所有数的异或结果
total = 0
for i in range(n + 1):
total ^= i
# 计算数组中所有数的异或结果
array = 0
for num in nums:
array ^= num
# 数组中缺失的数没有在 array中出现,所以缺失的数即为total与array的异或结果
missing_number = total ^ array
return missing_number
从时间上看,两个循环,每个循环都是 O(n),所以总时间复杂度是 O(n)。从空间上看,只用了常数级别的额外空间(用于存储几个整数变量),所以空间复杂度是 O(1)。
码之呼吸·二之型·求和
#个人认为这真的是一个非常巧妙的方法,super妙!!!
【计算出数组中所有数的总和,然后用总和减去数组中所有数的和,得到的结果就是缺失的数】
def find_missing_number(nums):
n = len(nums)
# 计算0到n的整数和
total = n * (n + 1) // 2
# 计算数组nums中所有数的和
array = sum(nums)
# 缺失的数
missing_number = total - array
return missing_number
从时间上来说,使用高斯公式来计算0到n的整数和,这个计算的时间复杂度是 O(1),使用 sum(nums)来计算数组中所有元素的和,这个操作的时间复杂度是 O(n),通过计算总和减去数组中的和,即可得到缺失的数,这个操作的时间复杂度是 O(1),即时间复杂度为O(n)。从空间上看,整个算法只用了常数级别的额外空间(用于存储几个整数变量),所以空间复杂度是 O(1)。
4、一些小tip
#时间与空间复杂度是一个重要的概念,通过它们我们可以评估、选择和优化算法,提高程序的性能和质量。理解这些概念对于处理大数据、优化系统性能和设计高效的解决方案至关重要。对于小白来说,有可能你写出来的一个代码运行不出来,就是因为时间复杂度太大了,大概如果要循环一万万次的话,普通笔记本可能就要干六七个小时。
#本期文章是结合自身经验和他人教学,进一步转化得到的,算是学习笔记的一种,也是我写的第一篇博客,记录了一些自己浅薄的理解与感悟,如果有不完善的地方,还请各位看官多多指教。
#如果觉得这篇文章对您有帮助的话,就点个赞支持一下吧~也欢迎评论区交流哦~我看到就会立马回复
#好好学习,天天向上,卷卷卷!