1. 文章概述
本文是根据《Python数据结构与算法分析》(第2版) 第二章算法分析的编程练习题“针对列表和字典比较del操作的性能”此题给出自己的解答,如有问题欢迎提出。
Python版本:3.9 系统:win10 设备:ASUA Vivobook 无畏pro14
2. 解答角度
回答从三个角度展开:
① del 对象最后一个元素
② del 整个对象
③ del 随机索引位置元素(执行1000次)
3. 所需知识
在数据结构研究算法的执行效率时,有两个角度分别为:算法占用的空间资源和算法的执行时间,算法的执行时间根据大O法进行衡量。为了得到算法的执行时间可以使用Python的第三方库timeit模块(无需安装)。而使用该模块,必须先创建一个Timer对象,该对象有两个参数:1:需要进行计时的Python语句;2.需要建立的测试的语句[1]。默认Timeit对象执行100万次,但这样会导致计算机高负荷运行很长时间,这里只是做个测试,因此规定执行1000次。执行次数根据Timer对象参数number进行设置。
本题Timer对象所需语法知识:
Timer("所需测试语句","将测试对象从__main__命名空间导入到timeit设置的计时的命名空间") [1]
4.实现
① del 对象最后一个元素
from timeit import Timer
from random
print("i del lst[-1] del dic[end]") # 结构属性指标,方便看
for i in range(10000, 1000001, 20000): # 假设问题规模从10000到1000001开始
t1 = Timer("del x[-1]",
"from __main__ import x") # Timer 对象(列表),类似导入第三方库
x = list(range(i)) # 生成列表对象,注意这种生成方式执行时间最短
lt = t1.timeit(number=1000) # 调用Timer对象,执行1000次,并统计时间
x = {j: None for j in range(i)} # 生成字典对象,注意这种生成方式执行时间最短
lst = list(x.keys())
t = Timer("del x[lst[-1]]; lst.pop()", # Timer对象(字典),注意对象参数一的执行命令
"from __main__ import x, lst")
dt = t.timeit(number=1000)
print("%d %10.5f %10.5f" % (i, lt, dt)) # 输出结果
print()
执行结果(未全给出):
结论:无论是列表还是字典,del 最后一个元素是常数阶,即O(1),虽然删除了所有的元素,你可能想时间复杂度会是n阶,但由于是删除最后一个元素,并逐步向索引小的位置移动,这时就类似pop(),而字典和列表的pop()都是常数阶。
你能发现字典的执行时间比列表的执行时间大将近一倍,其实不然,由于我为了实现这个效果,我在字典的Timer对象中的调用命令中添加了一个lst.pop(),假设有更好的方法实现,那么字典执行时间将与列表一致。总结:字典和列表在本操作中的执行时间相同。
② del 整个对象
print("i del lst del dic")
for i in range(10000, 1000001, 20000):
t = Timer("del x",
"from __main__ import x") # 只创建一个Timer对象
x = list(range(i))
lt = t.timeit(number=1)
x = {j: None for j in range(i)}
dt = t.timeit(number=1)
print("%d %10.7f %10.7f" % (i, lt, dt))
print()
执行结果:
结论:无论是列表还是字典,虽然执行时间有时候会抽风,但总体看del 整个对象(删除对象的引用)的时间复杂度为O(1),但需要注意的是删除列表中的所有元素(逐一)的时间复杂度为O(n),因为删除所有的元素,假设从索引为0开始删除,由于第一个元素被删除,列表后面的元素都要向前移动一个位置,所以时间复杂度如上所述。且这个操作的消耗的时间比以上del 对象最后一个元素的时间稍微长,可能因为是删除对象的引用吧。
③ del 随机索引位置元素(执行1000次)
print("i del lst[random] del dic[random]")
for i in range(10000, 1000001, 20000):
x = list(range(i))
t1 = Timer("del x[%d]" % random.randint(0, len(x)-1), # 注意参数一的执行设置
"from __main__ import x")
lt = t1.timeit(number=1000)
x = {j: None for j in range(i)}
t2 = Timer("del x[random.choice(list(x.keys()))]", # 这里同样用了random模块
"from __main__ import random, x")
dt = t2.timeit(number=1000)
print("%d %10.7f %10.7f" % (i, lt, dt))
执行结果:
结论:结果发现,虽然同样执行时间会抽风(可能跟计算机现在执行的其他程序的影响),但总体上列表和字典随机删除元素的操作的时间复杂度为O(n),且字典的时间复杂度随着问题规模的增大,两者相差的倍数也越大,且字典永远是最大的一个。事实是在问题规模为400000时,字典的执行时间已经超过4s了!
总体代码:
from timeit import Timer
from random
# 针对列表和字典比较del操作的性能
print("i del lst[-1] del dic[end]")
for i in range(10000, 1000001, 20000):
t1 = Timer("del x[-1]",
"from __main__ import x")
x = list(range(i))
lt = t1.timeit(number=1000)
x = {j: None for j in range(i)}
lst = list(x.keys())
t = Timer("del x[lst[-1]]; lst.pop()",
"from __main__ import x, lst")
dt = t.timeit(number=1000)
print("%d %10.5f %10.5f" % (i, lt, dt))
print()
# del 整个对象
print("i del lst del dic")
for i in range(10000, 1000001, 20000):
t = Timer("del x",
"from __main__ import x")
x = list(range(i))
lt = t.timeit(number=1)
x = {j: None for j in range(i)}
dt = t.timeit(number=1)
print("%d %10.7f %10.7f" % (i, lt, dt))
print()
# del 随机索引
print("i del lst[random] del dic[random]")
for i in range(10000, 1000001, 20000):
x = list(range(i))
t1 = Timer("del x[%d]" % random.randint(0, len(x)-1),
"from __main__ import x")
lt = t1.timeit(number=1000)
x = {j: None for j in range(i)}
t2 = Timer("del x[random.choice(list(x.keys()))]",
"from __main__ import random, x")
dt = t2.timeit(number=1000)
print("%d %10.7f %10.7f" % (i, lt, dt))
最后值得说的就是几乎所有计算机老师都会说的,就是你的执行结果不一定跟我一样,请以自身所得结果为衡量标准。
参考文献
[1]《Python数据结构与算法分析》