• Python刷算法题常见内置函数、方法技巧【最全汇总】


    注意:本篇内容仅包含一些在算法题目中较为常用的内置函数、方法、技巧,不包含所有python基础语法知识,主要作为汇总与查阅使用。本篇内容也会持续地根据大家的学习情况来做补充。

    想要全面学习python基础语法的同学可以翻阅经典蟒蛇书python编程从入门到实践.pdf

    列表相关操作

    排序

    列表的方法sort()或者内置函数sorted()可以完成排序操作。

    # 对lst本身进行升序排列,使用sort()方法
    lst = [3, 1, 2]
    lst.sort()        
    
    # 得到一个新的升序列表,lst保持不变,使用sorted()内置函数
    lst = [3, 1, 2]
    lst_sorted = list(sorted(lst))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意:无论是方法sort()或者内置函数sorted()都有两个形参,reversekey前者表示是否倒序排列后者表示排序的依据,通常会涉及到lambda匿名函数的使用。关于key形参以及lambda匿名函数的使用可以详见真题【排序】2023Q1A-身高提供排序 中的进阶部分。

    # 对lst本身进行降序排列,使用sort()方法
    lst = [3, 1, 2]
    lst.sort(reverse = True)        
    
    # 得到一个新的降序列表,lst保持不变,使用sorted()内置函数
    lst = [3, 1, 2]
    lst_sorted = list(sorted(lst, reverse = True))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    翻转

    列表的方法reverse()或者内置函数reversed()可以完成翻转操作。

    # 对lst本身进行翻转,使用reverse()方法
    lst = [3, 1, 2]
    lst.reverse()        
    
    # 得到一个新的翻转列表,lst保持不变,使用reversed()内置函数
    lst = [3, 1, 2]
    lst_reversed = list(reversed(lst))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    也可以使用列表的切片操作来完成翻转,设置步长为-1即可。

    # 得到一个新的翻转列表,lst保持不变,使用切片操作
    lst = [3, 1, 2]
    lst_reversed = lst[::-1]
    
    • 1
    • 2
    • 3

    枚举

    如果想在for循环中同时获得列表的索引i和元素值v,可以使用枚举内置函数enumerate(lst),其语法如下:

    lst = ["a", "b", "c"]
    for i, v in enumerate(lst):
        print(i, v)
    
    # 依次输出
    # 0 a
    # 1 b
    # 2 c
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这种写法等价于:

    lst = ["a", "b", "c"]
    for i in range(len(lst)):
        v = lst[i]
        print(i, v)
    
    • 1
    • 2
    • 3
    • 4

    另外,enumerate(lst, start)支持传入第二个参数start,表示索引i的从start开始计数。譬如

    lst = ["a", "b", "c"]
    for i, v in enumerate(lst, 1):
        print(i, v)
    
    # 由于i会从1而不是从0开始,
    # 依次输出
    # 1 a
    # 2 b
    # 3 c
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上述用法经常在固定滑窗题目中,搭配列表的切片来使用。譬如

    k = 2        # 一个固定长度为2的滑窗
    lst = ["a", "b", "c", "d", "e"]
    for i, v in enumerate(lst[k:], k):
        print(i, v)
    
    # 由于i会从2而不是从0开始,
    # 依次输出
    # 2 c
    # 3 d
    # 4 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    合并

    有两个长度相等的列表a_lstb_lst,如果想获得由同一索引对应元素构成的二元元组列表,可以使用内置函数zip()

    a_lst = [0, 1, 2]
    b_lst = ["a", "b", "c"]
    # 得到二维列表[(0, 'a'), (1, 'b'), (2, 'c')]
    merge_lst = list(zip(a_lst, b_lst))
    
    • 1
    • 2
    • 3
    • 4

    更常见的使用场景是在for循环中:

    a_lst = [0, 1, 2]
    b_lst = ["a", "b", "c"]
    for a, b in zip(a_lst, b_lst):
        print(a, b)
    
    • 1
    • 2
    • 3
    • 4

    去重

    直接使用哈希集合set()的特性,即可完成列表的去重。

    lst = [1, 1, 2, 3, 3]
    lst_nonrepeat = list(set(lst))
    
    • 1
    • 2

    拷贝

    先思考执行以下代码,为什么列表a会随着列表b的变化而变化?

    a = [1,2,3]
    b = a
    b[0] = 0
    
    print(b)
    # 得到[0,2,3]
    print(a)
    # 得到[0,2,3]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这实际上涉及到python一切皆对象的设计思想,此处的ab是同一个列表对象的两个别名,它们表示同一个列表,因此修改b会牵连到a。这也是初学者非常容易犯的一个错误。

    为了使得ab指向两个不同的列表对象,我们在赋值操作时可以有两种操作,来让b获得a的拷贝:

    使用切片。即令b = a[:]

    a = [1,2,3]
    b = a[:]
    b[0] = 0
    
    print(b)
    # 得到[0,2,3]
    print(a)
    # 得到[1,2,3]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    使用copy()方法。即令b = a.copy()

    a = [1,2,3]
    b = a.copy()
    b[0] = 0
    
    print(b)
    # 得到[0,2,3]
    print(a)
    # 得到[1,2,3]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    以上两种方法,都可以使得ab表示两个不同的(但值相同的)列表对象。这个技巧在回溯算法中非常常见

    哈希表相关操作

    设置值的默认类型

    使用内置模块collections中的defaultdict(func),能够将哈希表的值value的默认类型设置为func。譬如要设置哈希表中value的默认类型为list,其语法如下:

    from collections import defaultdict
    dic = defaultdict(list)
    
    # dic[0]此时即为一个空列表,可以使用所有的列表操作
    dic[0].append(1)
    dic[0].extend([2, 3])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 如果设置了value的默认类型为list,那么访问key对应的value默认得到一个空列表[]
    • 如果设置了value的默认类型为int,那么访问key对应的value默认得到数字0
    • 如果设置了value的默认类型为str,那么访问key对应的value默认得到空字符串""

    除了设置默认类型,defaultdict()还可以结合lambda匿名函数,设置更加丰富的默认值。譬如想设置value的默认值为1,其语法如下:

    from collections import defaultdict
    dic = defaultdict(lambda: 1)
    print(dic[0])    # 会输出1
    
    dic = defaultdict(lambda: "aabbcc")
    print(dic[0])    # 会输出"aabbcc"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    计数器

    在内置模块collections中引入Counter(),可以直接统计字符串、列表等可迭代对象的元素频率。

    from collections import Counter
    
    lst = [1, 1, 2, 3, 3]
    cnt = Counter(lst)
    
    • 1
    • 2
    • 3
    • 4

    注意,Counter()value默认是0,如果某一个键key并不位于cnt中,也可以访问或添加。如以下语句是合法的:

    from collections import Counter
    
    lst = [1, 1, 2, 3, 3]
    cnt = Counter()
    
    # 更新lst中各个数字num的频率
    for num in lst:
        cnt[num] += 1
    
    # 访问数字1对应的频率
    print(cnt[1])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果使用dict(),那么上述代码逻辑应该修改为

    lst = [1, 1, 2, 3, 3]
    cnt = dict()
    
    # 更新lst中各个数字num的频率
    for num in lst:
        # 需要判断num是否已经作为key存入cnt中
        # 如果不存在,需要新建key-value对
        # 设置cnt[num]为1,表示num出现了1次
        if num not in cnt:
            cnt[num] = 1
        # 如果存在,才可以直接更新cnt[num]
        else:
            cnt[num] += 1
            
    # 访问数字1对应的频率
    print(cnt[1])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    显然,如果直接使用dict(),**并不是不能够完成计数的功能,只是在代码逻辑上需要多加一个判断,**较为冗长。如果没有加上述判断,直接执行以下代码,

    lst = [1, 1, 2, 3, 3]
    cnt = dict()
    
    # 更新lst中各个数字num的频率
    for num in lst:
        cnt[num] += 1
    
    # 访问数字1对应的频率
    print(cnt[1])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    则会出现键错误的报错

    KeyError: 1
    
    • 1

    表示1这个key不位于dict()中。

    换句话说,直接使用Counter()就可以不用像dict()那样繁琐地判断key是否位于dict()中。所以在需要进行元素频率记录的时候,直接使用Counter()会比使用dict()更加方便。

    对于defaultdict()来说,也是同理的。在已知所储存的value的类型时,用defaultdict()可以避免频繁的判断。

    获得键、值或者键值对

    哈希表的keys()values()以及items()方法分别可以获得键、值或者键值对的一个可迭代对象。

    dic = {0: 1, 2: 3, 4: 5}
    
    # 遍历key方法1
    for k in dic:
        print(k)
    # 遍历key方法2
    for k in dic.keys():
        print(k)
    # 获得哈希表中所有键key组成的一个列表
    keys = list(dic.keys())
    
    # 遍历values
    for v in dic.values():
        print(v)
    # 获得哈希表中所有值value组成的一个列表
    values = list(dic.values())
    
    # 遍历key和values
    for k, v in dic.items():
        print(k, v)  
    # 获得哈希表中所有键值对(key, value)组成的一个二元列表
    pairs = list(dic.items())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    字符串相关操作

    str类型转int类型

    给定一个包含若干数字的字符串num_str,如何将其转换为int类型的num呢?最简单的方式是使用强制类型转换,即

    num_str = "12345"
    num = int(num_str)
    
    • 1
    • 2

    如果不允许直接使用int(num_str),如何使用遍历的方式,将num_str转换成int类型的num呢?

    可以初始化变量num0,然后从左到右遍历num_str中的每一个字符ch,将num扩大十倍后将加上int(ch)的结果重新赋值给num,直到遍历结束。代码实现如下

    num_str = "12345"
    num = 0
    for ch in num_str:
        num = num * 10 + int(ch)
    
    • 1
    • 2
    • 3
    • 4

    注意,该技巧通常用于处理数字字符和其他字符混杂的字符串,在诸多字符串解码相关的栈题和模拟题中出现,如LC394. 字符串解码【栈】腾讯2020春招-压缩算法【栈】2023Q1A-解压缩算法 等题目中经常使用。

    判断字符串是否均为字母、数字、或者字母或数字

    字符串中的isalpha()方法用以判断字符串中是否均为字母,isdigit()方法用以判断字符串中是否均为数字,isalnum()方法用以判断字符串中是否均为字母和数字,根据判断结果返回一个布尔类型。

    s = "123abc"
    
    print(s[-1].isalpha())    # True
    print(s.isalpha())        # False
    
    print(s[0].isdigit())     # True
    print(s.isdigit())        # False
    
    print(s.isalnum())        # True
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    判断字符串是否均为大写或小写字母

    字符串中的islower()isupper()方法用以判断字符串中是否均为小写字母或大写字母,根据判断结果返回一个布尔类型。注意当字符串中同时包含大小写字母,或包含数字或其他符号时,均会返回False

    s1 = "abc"
    print(sislower())    # True
    print(sisupper())    # False
    
    s2 = "ABC"    
    print(s2.islower())    # False
    print(s2.isupper())    # True
    
    s3 = "aBC"
    print(s3.islower())    # False
    print(s3.isupper())    # False
    
    s4 = "1"
    print(s4.islower())    # False
    print(s4.isupper())    # False
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    字母大小写转换

    字符串中的lower()upper()方法可以将字符串中的所有小写字母转化为大写,或者将所有大写字母转化为小写,并返回一个新的字符串,原字符串不发生改变。

    s = "aBc123"
    s_upper = s.upper()    # 得到"ABC123"
    s_lower = s.lower()    # 得到"abc123"
    
    • 1
    • 2
    • 3

    字符串中的title()方法可以令字符串中的所有单词的首字母大写,并返回一个新的字符串,原字符串不发生改变。这个方法的使用频率较低。

    s = "i love python"
    s_title = s.title()    # 得到"I Love python"
    
    • 1
    • 2

    替换

    字符串中的replace(x, y)方法可以将原字符串中的字符串x替换为字符串y,并返回一个新的字符串,原字符串不发生改变。

    s = "aBc123"
    s1 = s.replace("a", "d")
    s2 = s.replace("123", "4")
    
    • 1
    • 2
    • 3

    在真题【栈】2023B-仿LISP运算 中对输入的字符串进行预处理的操作,就用到了replace()方法,将单个左括号"("和右括号")"替换成前后带有空格的左括号" ( "和右括号" ) ",方便后续的分割操作。

    分割

    字符串中的split(x)方法以字符串x为分割符,将原字符串分割为一个新的列表并返回,原字符串不发生改变。如果不传入参数x,则默认为按照空格" "进行分割。最常用的分隔符为空格" "或者逗号","

    s = "1 2 3 4 5"
    lst = s.split()
    # 等价于lst = s.split(" ")
    
    s = "1,2,3,4,5"
    lst = s.split(",")
    
    # 两种分割均会得到lst = ["1", "2", "3", "4", "5"]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    合并

    字符串中的join(lst)方法以原字符串为合并符,将列表lst合并为一个新的字符串并返回。注意lst中的元素必须是字符串。最常用的合并符为空字符串""、空格字符串" "

    lst = ["a", "b", "c"]
    s = "".join(lst)
    # 会得到s = "abc"
    
    s_space = " ".join(lst)
    # 会得到s_space = "a b c"
    
    s_star = "*".join(lst)
    # 会得到s_star = "a*b*c"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    字符串的分割与合并是一对相互对应的操作,常用于列表与字符串之间的相互转换。

    注意:

    字符串属于一种不可变数据类型,并不能直接进行修改操作。当题目要求对一个字符串进行修改时,通常会先将原字符串使用split()方法或list()转化成列表,对列表修改后再使用join()方法得到新字符串的方式来实现。
    2. 列表lst必须是一个字符串类型列表,即lst: List[str]。如果lst是一个整数类型列表,直接使用语句"".join(lst)会出现类型错误TypeError。如需进行合并操作,必须使用map()内置函数对lst中的元素进行类型转换,将lst中的所有int类型元素转换成str类型。即

    lst = [0, 4, 2]
    s = "".join(map(str, lst))    # 得到s = "042"
    
    • 1
    • 2

    数字相关操作

    整除与求余

    一般而言,我们使用整除运算//和求余运算%来计算两个整数相除的商和余数

    div = 10 // 4
    mod = 10 % 4
    
    • 1
    • 2

    如果想要同时得到商和余数,可以直接使用内置函数divmod()来完成。

    div, mod = divmod(10, 4)
    
    • 1

    取整

    取整操作分为向上取整向下取整两种,这两个操作可以用python中的math内置库的ceil()函数和floor()函数来实现。ceilfloor的意思分别是天花板和地板,非常形象。

    from math import ceil, floor
    
    print(ceil(5))    # 得到2
    print(ceil(-5))   # 得到-1
    
    print(floor(5))   # 得到1
    print(floor(-5))  # 得到-2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    直接使用int(),也可以完成取整。对于正整数而言,int()是向下取整,对于负整数而言,int()是向上取整。即int()操作是一种向零截断的取整操作,或者说int()操作是对一个浮点数取其整数部分

    print(int(5))    # 得到1
    print(int(-5))   # 得到-1
    
    • 1
    • 2

    注意取整后的值的数据类型是int而不是float

    无穷大

    某些题目在对变量进行初始化的时候,需要将其初始化为无穷大或者负无穷大,这可以用python中的math内置库的inf直接得到。

    from math import inf
    
    ans = inf
    res = -inf
    
    • 1
    • 2
    • 3
    • 4

    注意inf的数据类型是float而不是int

    进制转换

    最常用的进制是十进制,除此之外常用的进制还有二进制、八进制、十六进制。python中有丰富的内置函数来帮助我们实现不同进制之间的转换。

    • 十进制转其他进制

    内置函数bin()oct()hex()能够非常方便地将一个整数num转化为其对应的二进制、八进制、十六进制字符串。

    num = 20
    
    # 20的二进制数是10100
    # 输出0b10100,包含前缀"0b"
    print(bin(num))
    # 输出10100,用切片去除了前缀"0b"
    print(bin(num)[2:])
    
    # 20的八进制数是24
    # 输出0o24,包含前缀"0o"
    print(oct(num))
    # 输出24,用切片去除了前缀"0o"
    print(oct(num)[2:])
    
    # 20的十六进制数是14
    # 输出0x14,包含前缀"0x"
    print(hex(num))
    # 输出14,用切片去除了前缀"0x"
    print(hex(num)[2:])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    有几点需要注意:

    转换后的数据类型是字符串而不是整数
    2. 字符串包含前缀"0b"/"0o"/"0x",表示是二/八/十六进制。
    3. 基于第二点,如果想要获得纯数字的转换,可以使用字符串的切片来实现效果。
    4. bin()oct()hex()分别是英文单词binary、octal、hexadecimal的缩写。

    另外,在某些题目中,如真题【模拟】2023B-IPv4地址转换成整数 ,要求进制转换后的结果保持某一固定长度,这个可以用内置函数map()lambda匿名函数搭配来完成。

    • 其他进制转十进制

    内置函数int()可以将一个字符串转化为一个十进制整数。

    最常见的操作是将一个十进制字符串转化为十进制整数。

    num_str = "100"
    
    # 得到十进制的整数num = 100
    num = int(num_str)
    
    • 1
    • 2
    • 3
    • 4

    实际上,int()函数可以传入参数base,表示需要转换的原字符串num_str是用哪种进制来表示的。

    num_str = "100"
    
    # 得到从二进制数num_str转化的十进制数num_from_base2 = 4
    num_from_base2 = int(num_str, base = 2)
    
    # 得到从八进制数num_str转化而来的十进制数num_from_base8 = 64
    num_from_base8 = int(num_str, base = 8)
    
    # 得到从十六进制数num_str转化而来的十进制数num_from_base16 = 256
    num_from_base16 = int(num_str, base = 16)
    
    # 得到从七进制数num_str转化而来的十进制数num_from_base7 = 49
    num_from_base7 = int(num_str, base = 7)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    注意虽然常用的进制是二、八、十六进制,但base传入其他数字也是可以的。譬如选择base = 7,表示将一个七进制数字符串,转化为一个十进制整数。

    优先队列相关操作

    注意,**优先队列(priority queue也叫做堆(**heap)。谈到优先队列时,一般强调其功能或应用,谈到堆时,一般强调其树形结构,但这两个词是可以进行同义替换的,大部分时候不用做严格区分。

    在python中,我们使用内置库heapq来实现优先队列的相关操作。需要先导入heapq库。即

    from heapq import *
    
    • 1

    虽然优先队列的底层原理是用完全二叉树来实现的,但由于完全二叉树通过层序遍历(即树的BFS)可以得到序列化的结果(即数组),故通常而言我们无需显式地构建出一棵完全二叉树来实现堆,而是使用heapq内置库来对一个列表进行堆化和堆操作

    堆化

    使用heapq内置库中的内置函数heapify()来实现一个列表的堆化。所谓堆化,是指令一个列表按照堆排序的要求来排序的过程。

    heap = [1, 3, 4, 2]        # 构建一个叫做heap的列表
    heapify(heap)              # 令heap堆化
    print(heap)                # 输出[1, 2, 4, 3],是heap按照堆排序后的结果
    
    • 1
    • 2
    • 3

    从单词heapify()也可以看出,这是一个动词,故该函数是功能对heap列表进行原地排序,没有返回值。

    堆化/堆排序的时间复杂度是O(NlogN)

    入堆

    使用heapq内置库中的内置函数heappush(heap, element)来实现将元素element加入堆heap中。

    heap = [1, 3, 4, 2]        # 构建一个叫做heap的列表
    heapify(heap)              # 令heap堆化
    heappush(heap, 5)          # 令元素5入堆
    print(heap)                # 输出[1, 2, 4, 3, 5],是5入堆后的结果
    heappush(heap, 0)          # 令元素0入堆
    print(heap)                # 输出[0, 2, 1, 3, 5, 4],是0入堆后的结果
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    入堆操作的时间复杂度为O(logN)heappush()函数是没有返回值的。

    出堆

    使用heapq内置库中的内置函数heappop(heap)来实现弹出堆heap的堆顶元素。

    heap = [0, 1, 2, 3, 4, 5]  # 构建一个叫做heap的列表
    heapify(heap)              # 令heap堆化
    top = heappop(heap)        # 弹出堆顶元素
    print(top, heap)           # 输出0和[1, 3, 2, 5, 4],是堆顶元素0出堆后的结果
    top = heappop(heap)        # 弹出堆顶元素
    print(top, heap)           # 输出1和[2, 3, 4, 5],是堆顶元素1出堆后的结果
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    出堆操作的时间复杂度为O(logN)heappop()函数是有返回值的,返回出堆的堆顶元素。

    获取堆顶元素

    堆顶元素总是位于列表heap索引为0的位置,故直接使用索引操作即可获得堆顶元素。

    heap = [5, 4, 2, 0, 3, 1]  # 构建一个叫做heap的列表
    heapify(heap)              # 令heap堆化
    print(heap[0])             # 输出堆顶元素0
    
    • 1
    • 2
    • 3

    小根堆与大根堆

    小根堆是指较小值具有更高优先级的堆,在树形结构上体现为,每一个节点的值都小于其子节点的值。

    大根堆是指较大值具有更高优先级的堆,在树形结构上体现为,每一个节点的值都大于其子节点的值。

    在python的heapq库中,默认的操作是构建****小根堆

    如果想要构建一个大根堆,可以通过储存元素相反值的方式来构建一个伪大根堆,即实际上仍然按照小根堆来操作,但由于储存了相反数,原先的最大值会变成绝对值最大的最小值而储存在堆顶。如

    heap = [0, 1, 2, 3, 4, 5]      # 构建一个叫做heap的列表
    heap = [-num for num in heap]  # 储存相反数
    heapify(heap)                  # 令heap堆化,得到一个伪大根堆
    top = -heappop(heap)           # 弹出堆顶元素,再取反,可以得到原heap中的最大值
    print(top, heap)               # 输出5和[-4, -3, -2, 0, -1],是堆顶元素-5出堆后的结果
    heappush(heap, -10)            # 往堆中存入元素10,应该存入其相反数-10
    print(heap)                    # 输出[-10, -3, -4, 0, -1, -2],可以看到-10位于堆顶heap[0]的位置
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    前缀和

    定义

    • 对于一个给定的数列A ,它的前缀和数列SS[i+1]表示从第1个元素到第i个元素的总和。
    • 假设nums是一个int型列表,形如sum(nums[0:i+1])就是从索引0对应的元素开始,累加到索引i对应的元素的前缀和。
    • 譬如nums = [1, 2, 3, 4],那么其前缀和列表即为pre_sum_lst = [0, 1, 3, 6, 10]

    前缀和的作用是可以在O(1)的时间复杂度下快速地计算出某段连续子数组的和。即

    sum(nums[i:j]) = pre_sum_lst[j] - pre_sum_lst[i]
    
    • 1

    譬如对于上述nums = [1, 2, 3, 4]而言,如果想快速计算出子数组nums[1:4] = [2, 3, 4]的结果,只需要计算pre_sum_lst[4] - pre_sum_lst[1] = 10 - 1 = 9即为答案。

    前缀和的作用也可以用来解释,为什么我们会把0也视为一个前缀和并且放在前缀和列表的第一个位置。由于设置了pre_sum_lst[0] = 0,那么pre_sum_lst[i] - pre_sum_lst[0] = sum(nums[:i]),才能够得到起始位置为原数组nums中第一个元素的连续子数组的和。

    构建

    可以使用遍历的方式构建前缀和,即

    nums = [1, 2, 3, 4]
    pre_sum_lst = [0]
    # 遍历nums中的每一个元素num
    for num in nums:
        # pre_sum_lst的最后一个元素pre_sum_lst[-1],为上一个前缀和的计算结果
        # 将pre_sum_lst[-1]+num的结果,重新加入列表末尾,得到当前前缀和
        pre_sum_lst.append(num + pre_sum_lst[-1])
    print(pre_sum_lst)    # 输出[0, 1, 3, 6, 10]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    也可以直接调用内置库itertools中的accumulate()函数来构建前缀和

    from itertools import accumulate
    
    nums = [1, 2, 3, 4]
    pre_sum_lst = [0] + list(accumulate(nums))
    print(pre_sum_lst)    # 输出[0, 1, 3, 6, 10]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    需要注意两点:

    accumulate()函数返回的是一个accumulate可迭代对象,如果需要转化为前缀和数组,需要使用list()进行强制的类型转换
    2. accumulate()函数用于对nums中的元素进行累加,不包含前缀和数组最开头的[0],需要额外加上。

    位运算

    整数类型的变量int在内存中是按照二进制的方式进行存储的,位运算指的就是直接对整数在内存中的二进制位****进行操作

    我们可以把二进制中数字和布尔类型进行类比,把位运算符和布尔运算符进行类比。这样更容易理解。

    位运算中的概念布尔运算中的概念
    数字和布尔类型1True
    0False
    位运算符和布尔运算符按位与运算符&布尔与运算符and
    按位或运算符``布尔或运算符or
    按位取反运算符~布尔非运算符not

    假设要对两个数字ab进行位运算,那么应该先将其转化为两个二进制数(仅包含01的两个二进制数),然后逐位地根据运算符进行运算,得到一个新的二进制数,再将其转换为十进制数即为结果。

    与运算

    与运算的符号是&

    参与运算的两个数,如果两个相对应的位的均为1,那么该位的运算结果为1,否则为0。即有如下表格

    01
    000
    101

    两个位置只要有一个不为1结果就不为1

    举个例子,3 & 5 = 1,其计算过程如下。

    暂时无法在飞书文档外展示此内容

    或运算

    或运算的符号是|

    参与运算的两个数,如果两个相对应的位的均为0,那么该位的运算结果为0,否则为1。即有如下表格

    01
    001
    111

    两个位置只要有一个不为0结果就不为0

    举个例子,3 | 5 = 7,其计算过程如下。

    暂时无法在飞书文档外展示此内容

    异或运算

    异或运算的符号是^

    参与运算的两个数,如果两个相对应的位相等,那么该位的运算结果为0,否则为1。即有如下表格

    01
    001
    110

    两个位置只要有一个不为0结果就不为0

    举个例子,3 ^ 5 = 6,其计算过程如下。

    暂时无法在飞书文档外展示此内容

    特别地,对于任意整数a,存在 a ^ a = 0a ^ 0 = a 成立。

    左移运算和右移运算

    左移运算的符号是<<

    num << n表示是转换成二进制数的num整体向左移动n位,最右边空出来的位置用0补齐。

    5 << 2 = 20 为例,其计算过程如下。

    暂时无法在飞书文档外展示此内容

    右移运算的符号是>>

    num >> n表示是转换成二进制数的num整体向右移动n位,最右边的n位删除。

    23 >> 2 = 5 为例,其计算过程如下。

    暂时无法在飞书文档外展示此内容

    显然, num >> 1 等价于 num // 2

    位运算定律

    任意位运算均满足交换律。即存在

    a & b = b & a
    a | b = b | a
    a ^ b = b ^ a
    
    • 1
    • 2
    • 3

    任意位运算均满足结合律。即存在

    (a & b) & c = a & (b & c)
    (a | b) | c = a | (b | c)
    (a ^ b) ^ c = a ^ (b ^ c)
    
    • 1
    • 2
    • 3

    判断n是否为2的幂

    若数字n2的幂,即存在 n = 2 k n = 2^k n=2k成立,那么其二进制的首位一定是1,后面包含了k0

    16 = 0b10000,其二进制的首位为1,后面包含了40

    如果考虑n-1的二进制,容易发现其位数应该比n1,同时均为1构成。

    15 = 0b1111,其二进制为41

    由于只有当n2的幂,才会具备上述性质,我们可以使用与运算来判断n是否为2的幂。

    当且仅当 n & (n-1) == 0 成立时,n2的幂。题目 LeetCode23 2 的幂 即使用到该技巧。

    同理,如果要判断n的二进制是否均由1构成,也可以用类似的方法判断。

    当且仅当n & (n+1) == 0 成立时,n的二进制均由1构成。

    推导式

    python中的推导式是一种独特的数据处理方式,可以从一个数据序列构建另一个新的数据序列。可以简单理解为for循环语句(+if条件语句)的简写版本,其基础语法结构为

    • 待转换的数据类型(表达式 for 变量 in 可迭代对象)
    • 待转换的数据类型(表达式 for 变量 in 可迭代对象 if 条件)

    列表推导式

    nums1 = [i for i in range(10)]
    nums2 = [i for i in range(10) if i % 2 == 0]
    nums3 = [[0] * m for _ in range(n)]
    
    • 1
    • 2
    • 3

    元组推导式

    nums1 = (i for i in range(10))
    nums2 = (i for i in range(10) if i % 2 == 0)
    
    • 1
    • 2

    集合推导式

    nums1 = {i for i in range(10)}
    nums2 = {i for i in range(10) if i % 2 == 0}
    
    • 1
    • 2

    字典推导式

    nums1 = {i: i**2 for i in range(10)}
    nums2 = {i: i**2 for i in range(10) if i % 2 == 0}
    
    ks = "123"
    vs = "abc"
    dic = {k: v for k, v in zip(ks,vs)}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    其他知识补充

    字典序与数字序

    最常见的两种排序依据是字典序和数字序。

    字典序就是按照字符在ASCII码值表中的排列方式进行排序。其较为严格的定义是,对于两个字符串x和y,

    • 譬如"abandon""an"的比较,存在"abandon" < "an",因为第二个字符"n"在字符串排在"b"后面
    • 譬如"10""2"的比较,存在"10" < "2",因为第一个字符"1"在中排在"2"前面。
    • 空字符串比一切字符都小,譬如"abc""abcd"的比较,存在"abc" < "abcd",因为"abc"的第四位可以看作是一个空字符串"",该空字符串""小于"d"

    数字序就是按照数字大小来进行排序。

    • 譬如102的比较,显然10 > 2

    不管是字典序还是数字序,都有升序和降序两种排序形式,前者指从小到大排列,后者指从大到小排列。可以从下表的例子中看出他们之间的关系。

    排序形式:升序排序形式:降序
    排序依据:字典序["0", "1", "12", "123", "2", "4"]["4", "2", "123", 12"", "1", "0"]
    排序依据:数字序[0, 1, 2, 4, 12, 123][123, 12, 4, 2, 1, 0]

    三目运算符/三元表达式

    三目运算符(有些人也称三元表达式)可以看作是if语句的简写版本,其基本语法结构为:

    • 值1 if 条件 else 值2

    其含义为:如果if后所带的条件满足,则会返回值1,否则返回值2

    ans = 1 if True else 0
    print(ans)    # 得到1
    
    ans = 1 if False else 0
    print(ans)    # 得到0
    
    • 1
    • 2
    • 3
    • 4
    • 5

    lambda匿名函数

    匿名函数,顾名思义就是不需要具体定义函数名的函数。我们首先抛开复杂的定义,看两个具体例子。

    先看一个无参数函数的例子。假设我们需要一个return 1的函数,如果使用普通的函数定义方式,其代码为:

    # 使用def关键字进行函数定义
    def get_one():
        return 1
    
    # 输出1
    print(get_one())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果我们使用lambda匿名函数,我们则不需要使用def关键字而改用lambda关键字,其代码为:

    # 使用lambda匿名函数进行函数定义
    # 注意这里的get_one_lambda看似是一个变量名
    # 但实际上是一个函数名
    get_one_lambda = lambda: 1
    
    print(get_one_lambda)
    # 输出形如  at 0x00000249F7AD7B00> 的结果
    # 这一长串复杂的十六进制数是函数get_one_lambda的内存地址
     
    print(get_one_lambda())
    # 在函数名get_one_lambda后面加上括号,才能正确地输出1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    把上述代码进行合并操作,可以无需声明函数名get_one_lambda,直接得到结果:

    # 这里的(lambda: 1)等价于上面的函数名get_one_lambda
    # (lambda: 1)()等价于get_one_lambda()
    # 显然get_one_lambda这个函数名是不必要的,故称之为"lambda匿名函数"
    
    print(lambda: 1)        
    # 输出形如  at 0x00000249F7AD7B00> 的结果
    # 这一长串复杂的十六进制数是该匿名函数的内存地址
    
    print((lambda: 1)())
    # 在匿名函数后面加上括号,才能正确地输出1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    再看一个有参数函数的例子。假设我们需要一个计算两个参数xy相加return x+y的函数,如果使用普通的函数定义方式,其代码为:

    # 使用def关键字进行函数定义
    def add(x, y):
        return x+y
    
    # 输出30
    print(add(10, 20))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果我们使用lambda匿名函数,我们则不需要使用def关键字而改用lambda关键字,其代码为:

    # 使用lambda匿名函数进行函数定义
    # 注意这里的get_one_lambda看似是一个变量名
    # 但实际上是一个函数名
    add_lambda = lambda x,y: x+y
    
    print(add_lambda)        
    # 输出形如  at 0x00000249F7AD7B00> 的结果
    # 这一长串复杂的十六进制数是函数add_lambda的内存地址
    
    print(add_lambda(10, 20))
    # 在函数名add_lambda后面加上括号并传入参数,才能正确地输出结果
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    把上述代码进行合并操作,可以无需声明函数名add_lambda,直接传入两个数字1020,得到结果:

    # 这里的(lambda x,y: x+y)等价于上面的函数名add_lambda
    # (lambda x,y: x+y)(10, 20)等价于add_lambda(10, 20)
    # 显然add_lambda这个函数名是不必要的,故称之为"lambda匿名函数"
    
    print(lambda x,y: x+y)        
    # 输出形如  at 0x00000249F7AD7B00> 的结果
    # 这一长串复杂的十六进制数是该匿名函数的内存地址
    
    print((lambda x,y: x+y)(10, 20))
    # 在匿名函数后面加上括号并传入参数,才能正确地输出结果
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    根据上述两个例子已经能够看出端倪了,有以下重要结论:

    lambda匿名函数本质上是一个函数,而不是一个变量,使用lambda匿名函数可以得到一个函数。
    2. 定义lambda匿名函数的语法为:lambda [形参]: 返回值,形参的数量可以为0,即支持定义无参数匿名函数。
    3. 使用lambda匿名函数的语法为:(lambda [形参]: 返回值) ([实参]),一般情况下,实参的数量应该和定义的形参数量一致。

    *注:上述语句中的中括号[]表示非必须结构。

    最后我们来看lambda匿名函数在算法题中的几个经典用法。

    defaultdict()

    使用内置模块collections中的defaultdict(func),能够将哈希表的值value的默认类型设置为func,其中func是某种数据类型初始化函数的函数名,如intlistdict等等。

    假设我们想要value的默认值为1,其中一种方法通过def关键字定义一个叫做get_one()的函数,并且将函数名get_one作为函数名传入defaultdict(func)中。

    from collections import defaultdict
    
    # 使用def关键字进行函数定义
    def get_one():
        return 1
    
    # 哈希表d的value的默认值即为1
    # 注意这里传入的是函数名get_one,而不是调用函数get_one()
    d = defaultdict(get_one)
    
    # 输出1
    print(d[0])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    通过前面分析我们知道,get_one这个函数名实际上等同于lambda: 1,故我们不需要对get_one()显式地进行定义,使用lambda匿名函数能够使得代码更加整洁。

    from collections import defaultdict
    
    # 哈希表d1的value的默认值即为1
    d_lambda = defaultdict(lambda: 1)
    
    # 输出1
    print(d_lambda[0])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    sort()方法或内置函数sorted()

    sort()方法和内置函数sorted()均包含key参数,用来指定排序的依据。key参数传入的也是一个函数名func,可以简单理解为对列表中的所有元素均使用函数func后,以得到的结果为依据对原列表进行排序。

    假设我们想要对字符串按照长度来排序,可以指定len()内置函数为排序依据。

    lst = ["123", "4567", "0", "12", "789"]
    
    # 注意这里key参数传入是函数名len,而不是调用函数len()
    lst.sort(key = len)
    
    # 输出['0', '12', '123', '789', '4567']
    print(lst)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上述代码也可以写成lambda匿名函数的形式。

    lst = ["123", "4567", "0", "12", "789"]
    
    # 注意这里key参数传入是函数名len,而不是调用函数len()
    lst.sort(key = lambda x: len(x))
    
    # 输出['0', '12', '123', '789', '4567']
    print(lst)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果对于长度相同的字符串,我们还想要按照数字序降序排列,那么仅用len()函数是无法完成的,只能通过进一步完善lambda匿名函数来完成。

    lst = ["123", "4567", "0", "12", "789"]
    
    # 注意这里key参数传入是函数名len,而不是调用函数len()
    lst.sort(key = lambda x: (len(x), -int(x)))
    
    # 输出['0', '12', '789', '123', '4567']
    print(lst)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    内置函数map()

    内置函数map(func, iter)包含两个参数,分别是函数名func和可迭代对象iter。这里的func参数不仅可以传入已有的内置函数或自定义函数的函数名,也可以直接用lambda匿名函数完成。

    譬如想要对由二进制字符串组成的列表lst_bin中的每一个字符串用先导0填充至长度为4,如果我们使用def关键字定义一个叫做get_pre_0()的函数,可以这样实现:

    # 使用def关键字进行函数定义
    # (4-len(s))能够获得应该填充的先导零"0"的个数
    def get_pre_0(s):
        return (4-len(s))*"0" + s
    
    lst_bin = ["10", "101", "1100", "1", "0"]
    
    # 注意这里key参数传入是函数名get_pre_0,而不是调用函数get_pre_0()
    lst_with_pre_0 = list(map(get_pre_0, lst_bin))
    
    # 输出['0010', '0101', '1100', '0001', '0000']
    print(lst_with_pre_0)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    使用lambda匿名函数,无需具体定义函数get_pre_0(),亦可完成同样操作。

    lst_bin = ["10", "101", "1100", "1", "0"]
    
    # 使用lambda匿名函数,无需具体定义函数get_pre_0()
    lst_with_pre_0_lambda = list(map(lambda x: (4-len(x))*"0" + x, lst_bin))
    
    # 输出['0010', '0101', '1100', '0001', '0000']
    print(lst_with_pre_0_lambda)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ord()chr()

    这两个函数用于ASCII码值和对应字符之间的转化。

    • ord(ch)可以将字符ch转化为对应ASCII码值。其中ord()表示的是order的意思。
    • chr(num)可以将ASCII码值num转化为对应字符。其中chr()表示的是character的意思。
    print(ord("0"))    # 得到48
    print(ord("A"))    # 得到65
    print(ord("B"))    # 得到66
    print(ord("a"))    # 得到97
    print(ord("b"))    # 得到98
    
    print(chr(48))     # 得到"0"
    print(chr(66))     # 得到"C"
    print(chr(67))     # 得到"D"
    print(chr(99))     # 得到"c"
    print(chr(100))    # 得到"d"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    一个常用的技巧是,如果有一个小写字母ch,想令其拥有形如"a": 0"b": 1"c": 2,…,"z": 25这样的字母表顺序映射关系,我们可以通过以下代码得到。

    # 假设字符ch是"c",其映射在字母表中顺序的映射应该是2
    ch = "c"                    
    
    # ord(ch)是99,ord("a")是97,相减得到2
    idx = ord(ch) - ord("a")
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这个技巧常用于用列表表示已知范围的哈希表的操作。

    关于ASCII码值更加详细的内容,可以看百度百科-ASCII码值

    all()any()

    内置函数all()any()中传入的是一个可迭代对象iter(如一个列表lst),返回值为一个布尔类型。

    • 对于all(iter)而言,如果可迭代对象iter所有的值均为True,那么all(iter)会返回True,否则返回False
    • 对于any(iter)而言,如果可迭代对象iter任意的值满足True,那么any(iter)会返回True,否则返回False
    print(all([True, True, True]))
    print(all([True, True, False]))
    
    print(any([False, False, False]))
    print(any([True, True, False]))
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注意这两个内置函数,经常搭配推导式一起使用。

    全局变量

    如果在自定义函数体外声明了一个全局变量,可以用在函数体中用global关键字来修饰这个全局变量,那么该变量就可以在函数体中直接使用了。这个技巧在DFS题目中非常实用,用于避免写出逻辑复杂的return返回值。

    def f():
        global ans
        ans += 1
    
    ans = 0
    print(ans)
    f()
    print(ans)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最大递归深度

    当我们在做DFS和回溯题目的时候,通常会使用递归来实现。python中默认的最大递归深度是1000,但有时候由于用例本身的问题,程度的递归深度会超过1000,就会出现如下的报错

    RecursionError: maximum recursion depth exceeded in comparison
    
    • 1

    在确认代码无误的情况下,我们可以使用sys内置库中的setrecursionlimit()方法,来重新设置最大递归深度,其具体代码如下

    import sys
    sys.setrecursionlimit(1000000)    
    # 这里不一定设置1000000,设置成一个很大的数即可
    
    • 1
    • 2
    • 3

    上述两行代码加在程序的最前方即可。


    华为OD算法/大厂面试高频题算法练习冲刺训练

    • 华为OD算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务100+同学成功上岸!

    • 课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化

    • 每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!

    • 60+天陪伴式学习,40+直播课时,300+动画图解视频,300+LeetCode经典题,200+华为OD真题/大厂真题,还有简历修改、模拟面试、专属HR对接将为你解锁

    • 可上全网独家的欧弟OJ系统练习华子OD、大厂真题

    • 可查看链接 大厂真题汇总 & OD真题汇总(持续更新)

    • 绿色聊天软件戳 od1336了解更多

  • 相关阅读:
    快速入门一篇搞定RocketMq-实现微服务实战落地
    计算机毕业设计之java+javaweb的大学生就业帮助系统-就业招聘网站
    Python如何实现模板方法设计模式?什么是模板方法设计模式?Python 模板方法设计模式示例代码
    08_SpingBoot 集成Redis
    在预处理中用于预训练网络模型的均值和标准差的几种形式
    数据包伪造替换、会话劫持、https劫持之探索和测试
    windows 修改hosts映射,可以ping通,但是无法通过http url 路径访问,出现 500 Internal Privoxy Error
    基于JAVA银行客户管理计算机毕业设计源码+系统+mysql数据库+lw文档+部署
    【网络安全带你练爬虫-100练】第22练:数据包中参数提取与处理
    【MySql】mysql之连接查询和存储过程
  • 原文地址:https://blog.csdn.net/weixin_48157259/article/details/133653426