• 最近在学习《Python数据结构与算法分析这本书》,想通过CSDN做个学习记录,记录主要内容是代码部分,详细内容可以看此。代码主要根据自己理解写出,如果有更好的方法,更好的变量名命名,大家可以指出,我会做出改正,加油!

2、 算法分析

2.1 异序词检测

如果一个字符串知识重新排列了另一个字符串的字符,那这两个字符串就是一组异序词,如python和typhon。

输入:两个字符串; 输出:True(是异序词),False(不是异序词)

方法1、清点法

  • 思路:对应str1中所有的字符,我们都在检测是否在str2中,如果存在,就将str2中这个字符设置为None。
  • 注意点:1、字符串是不可变类型,我们需要把str2先转换成列表; 2、对于这种方法,我们可以先判断两个字符串是否是等长度的,如果是,则使用上面的方法,如果不是,直接返回Flase。
def anagramSolution1(str1, str2):
    if len(str1) == len(str2):
        blist = list(str2)  # 字符串是不可变类型,所以先要转换成列表
        for pos1 in range(len(str1)):
            found = False  # 每次取出str1中一个字符,设置一个flag判断是否存在于str2中
            for pos2 in range(len(str2)):
                if str1[pos1] == str2[pos2]:
                    found = True  # 表明找到
                    blist[pos2] = None  # 对应字符设置为None
            if not found:
                return False  # 遍历str2后如果一直没找到,则返回False
        return True  # str1中全遍历结束后,如果一直没有返回False,说明全都找到了,返回True
    else:
        return False  # 长度不一样直接返回False


print(anagramSolution1('python', 'thpyon'), end=',')
print(anagramSolution1('python', 'thpsyon'), end=',')
print(anagramSolution1('abcddcba', 'dcbaabcd'))


[output]: True,False,True

两层循环,无特殊复杂度,故复杂度为O(n2)

方法2、排序法

  • 思路:异序词,根据字母排序后,结果将是同一个字符串
  • 注意点:字符串是不可变类型,可以先转换成列表,使用sort进行排序会很方便
def anagramSolution2(str1, str2):
    alist, blist = list(str1), list(str2)
    alist.sort()  # 注意sort()无返回值,不能赋值给另一个变量
    blist.sort()
    return alist == blist  # 二者相等返回True,不等返回False


print(anagramSolution2('python', 'thpyon'), end=',')
print(anagramSolution2('python', 'thpsyon'), end=',')
print(anagramSolution2('abcddcba', 'dcbaabcd'))


[output]: True,False,True

主要部分是调用两次的sort()函数,所以复杂度为sort()函数的复杂度,是O(n log n)

方法3、计数法

  • 思路:由于异序词都是有26个英文字母组合而成,所以每个词对应的字母数量是一样的,可以通过记录各自字符的数量判断是否相等。
  • 注意点:1、用1*26的列表进行计数,初始值为0;2、为了方便统计,我们可以使用ord函数,用当前字符与a对应Unicode编码的距离作为索引,传入给计数列表。
def anagramSolution3(str1, str2):
    if len(str1) == len(str2):
        alist, blist = [0] * 26, [0] * 26  # 初始化两个计数列表
        for pos1 in range(len(str1)):
            pos1_index = ord(str1[pos1]) - ord('a')  # 小技巧,计算与'a'编码的距离
            alist[pos1_index] += 1

        for pos2 in range(len(str2)):
            pos2_index = ord(str2[pos2]) - ord('a')  # 小技巧,计算与'a'编码的距离
            blist[pos2_index] += 1

        print(f'alist: {alist}\nblist:{blist}')  # 打印计数列表(可以注释掉)
        return alist == blist  # 返回是否相等
    else:
        return False


print(anagramSolution3('python', 'thpyon'))
print(anagramSolution3('python', 'thpsyon'))
print(anagramSolution3('abcddcba', 'dcbaabcd'))


[output]:
alist: [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]
blist: [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]
True
False
alist: [2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
blist: [2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
True

两个叠加的循环,复杂度是O(n)。但由于存在两个计数列表,故而有空间的损耗,以空间换取时间

方法4、将法3中的计数列表换成字典,会直观很多

def anagramSolution4(str1, str2):
    if len(str1) == len(str2):
        adict, bdict = {}, {}
        for pos1 in range(len(str1)):
            if str1[pos1] in adict.keys():
                adict[str1[pos1]] += 1  # 键存在,值加一
            else:
                adict[str1[pos1]] = 1  # 键不存在,添加进去,并且初始化为1

        for pos2 in range(len(str2)):
            if str2[pos2] in bdict.keys():
                bdict[str2[pos2]] += 1  # 键存在,值加一
            else:
                bdict[str2[pos2]] = 1  # 键不存在,添加进去,并且初始化为1

        print(f'adict: {adict}\nbdict: {bdict}')

        return adict == bdict
    else:
        return False


print(anagramSolution4('python', 'thpyon'))
print(anagramSolution4('python', 'thpsyon'))
print(anagramSolution4('abcddcba', 'dcbaabcd'))

[output]:
adict: {'p': 1, 'y': 1, 't': 1, 'h': 1, 'o': 1, 'n': 1}
bdict: {'t': 1, 'h': 1, 'p': 1, 'y': 1, 'o': 1, 'n': 1}
True
False
adict: {'a': 2, 'b': 2, 'c': 2, 'd': 2}
bdict: {'d': 2, 'c': 2, 'b': 2, 'a': 2}
True

2.2 Python的数据性能

2.2.1 使用timeit模块,测试四种生成列表的时间差别

from timeit import Timer


def test1():
    l = []
    for i in range(1000):
        l = l + [i]  # 列表的拼接,等价于extend


def test2():
    l = []
    for i in range(1000):
        l.append(i)  # 使用append方法


def test3():
    l = [i for i in range(1000)]  # 使用列表推导式的方法


def test4():
    l = list(range(1000))  # 使用range用list进行转换成列表

t1 = Timer("test1()", "from __main__ import test1")  # 先实例化这个对象
print(f'拼接列表耗时:{t1.timeit(number=1000):.5f}秒')  # number表示执行参数

t2 = Timer("test2()", "from __main__ import test2")
print(f'使用append方法耗时:{t2.timeit(number=1000):.5f}秒')

t3 = Timer("test3()", "from __main__ import test3")
print(f'使用列表推导式耗时:{t3.timeit(number=1000):.5f}秒')

t4 = Timer("test4()", "from __main__ import test4")
print(f'使用range的方法耗时:{t4.timeit(number=1000):.5f}秒')

[output]:
拼接列表耗时:0.77831秒
使用append方法耗时:0.04750秒
使用列表推导式耗时:0.02601秒
使用range的方法耗时:0.00751秒
速度排序:range方法 > 推导式方法 > append方法 > 列表拼接方法

李春葆数据结构教程Python第1版语言描述练习题答案 数据结构 python语言描述 第2版_python

2.2.2 pop()与pop(k)之间的性能差异

  • 长度为2百万的列表中两种pop方式的时间对比
from timeit import Timer

popzero = Timer("x.pop(0)", "from __main__ import x")
popend = Timer("x.pop()", "from __main__ import x")

x = list(range(2000000))
print(f'popzero的时间:{popzero.timeit(number=1000):.5f}')
x = list(range(2000000))
print(f'popend的时间:{popend.timeit(number=1000):.5f}')


[output]:
popzero的时间:0.90263
popend的时间:0.00004

很明显,popzero的时间远大于popend,前者是O(n)后者是O(1)

  • 增加列表的长度,观看二者的增长趋势对比
from timeit import Timer

popzero = Timer("x.pop(0)", "from __main__ import x")
popend = Timer("x.pop()", "from __main__ import x")

print(f'i\t\t\tpopzero\t\tpopend')

for i in range(1000000, 10000001, 1000000):
    x = list(range(i))
    zerotime = popzero.timeit(number=1000)
    x = list(range(i))
    endtime = popend.timeit(number=1000)
    print(f'{i}\t\t{zerotime:.5f}\t\t{endtime:.5f}')

[output]:
i			popzero		popend
1000000		0.14157		0.00004
2000000		0.86978		0.00004
3000000		1.83445		0.00004
4000000		2.47858		0.00004
5000000		3.52675		0.00004
6000000		4.76841		0.00004
7000000		5.44192		0.00004
8000000		5.87373		0.00005
9000000		6.43729		0.00004
10000000		7.22233		0.00004

popend时间几乎是不变的,popzero时间有呈现线下增长的趋势,符合O(n)

2.2.3 比较列表和字典中的包含操作

  • 思路:通过产生随机数,判断是否包含在列表和字典的方式
  • 注意点:Timer实例化对象的时候,要把random模块也放进去
from timeit import Timer
import random

print(f'i\t\tlist_time\t\tdict_time')

for i in range(10000, 200001, 20000):
    t = Timer(f"random.randrange({i}) in x", "from __main__ import random, x")
    x = list(range(i))
    list_time = t.timeit(number=2000)
    x = {k: None for k in range(i)}  # 利用推导式生成值为None的字典
    dict_time = t.timeit(number=2000)
    print(f'{i}\t{list_time:.5f}\t\t\t{dict_time:.5f}')


[output]:
i		list_time		dict_time
10000	0.07453			0.00117
30000	0.22840			0.00093
50000	0.37595			0.00099
70000	0.52806			0.00109
90000	0.68872			0.00120
110000	0.82673			0.00120
130000	0.99619			0.00126
150000	1.14923			0.00110
170000	1.31358			0.00154
190000	1.48863			0.00115
字典一直不变,更快

李春葆数据结构教程Python第1版语言描述练习题答案 数据结构 python语言描述 第2版_bc_02

2.3 小结

  • Python是一门变化中的语言,内部实现一直会有更新。追求时间还是空间,取决于我们面对的任务,有时候的取舍是必要的。
  • Python更多操作的时间复杂度可以访问:这里