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

文章目录

  • 列表相关操作
    • 排序
    • 翻转
    • 枚举
    • 合并
    • 去重
    • 拷贝
  • 哈希表相关操作
    • 设置值的默认类型
    • 计数器
    • 获得键、值或者键值对
  • 字符串相关操作
    • `str`类型转`int`类型
    • 判断字符串是否均为字母、数字、或者字母或数字
    • 判断字符串是否均为大写或小写字母
    • 字母大小写转换
    • 替换
    • 分割
    • 合并
  • 数字相关操作
    • 整除与求余
    • 取整
    • 无穷大
    • 进制转换
  • 优先队列相关操作
    • 堆化
    • 入堆
    • 出堆
    • 获取堆顶元素
    • 小根堆与大根堆
  • 前缀和
    • 定义
    • 构建
  • 位运算
    • 与运算
    • 或运算
    • 异或运算
    • 左移运算和右移运算
    • 位运算定律
    • 判断`n`是否为`2`的幂
  • 推导式
    • 列表推导式
    • 元组推导式
    • 集合推导式
    • 字典推导式
  • 其他知识补充
    • 字典序与数字序
    • 三目运算符/三元表达式
    • `lambda`匿名函数
    • `ord()`与`chr()`
    • `all()`与`any()`
    • 全局变量
    • 最大递归深度
  • 华为OD算法/大厂面试高频题算法练习冲刺训练

注意:本篇内容仅包含一些在算法题目中较为常用的内置函数、方法、技巧,不包含所有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))

注意:无论是方法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))

翻转

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

# 对lst本身进行翻转,使用reverse()方法
lst = [3, 1, 2]
lst.reverse()        

# 得到一个新的翻转列表,lst保持不变,使用reversed()内置函数
lst = [3, 1, 2]
lst_reversed = list(reversed(lst))

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

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

枚举

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

lst = ["a", "b", "c"]
for i, v in enumerate(lst):
    print(i, v)

# 依次输出
# 0 a
# 1 b
# 2 c

这种写法等价于:

lst = ["a", "b", "c"]
for i in range(len(lst)):
    v = lst[i]
    print(i, v)

另外,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

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

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

合并

有两个长度相等的列表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))

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

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

去重

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

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

拷贝

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

a = [1,2,3]
b = a
b[0] = 0

print(b)
# 得到[0,2,3]
print(a)
# 得到[0,2,3]

这实际上涉及到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]

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

a = [1,2,3]
b = a.copy()
b[0] = 0

print(b)
# 得到[0,2,3]
print(a)
# 得到[1,2,3]

以上两种方法,都可以使得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])
  • 如果设置了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"

计数器

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

from collections import Counter

lst = [1, 1, 2, 3, 3]
cnt = Counter(lst)

注意,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])

如果使用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])

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

lst = [1, 1, 2, 3, 3]
cnt = dict()

# 更新lst中各个数字num的频率
for num in lst:
    cnt[num] += 1

# 访问数字1对应的频率
print(cnt[1])

则会出现键错误的报错

KeyError: 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())

字符串相关操作

str类型转int类型

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

num_str = "12345"
num = int(num_str)

如果不允许直接使用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)

注意,该技巧通常用于处理数字字符和其他字符混杂的字符串,在诸多字符串解码相关的栈题和模拟题中出现,如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

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

字符串中的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

字母大小写转换

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

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

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

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

替换

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

s = "aBc123"
s1 = s.replace("a", "d")
s2 = s.replace("123", "4")

在真题【栈】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"]

合并

字符串中的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"

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

注意:

字符串属于一种不可变数据类型,并不能直接进行修改操作。当题目要求对一个字符串进行修改时,通常会先将原字符串使用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"

数字相关操作

整除与求余

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

div = 10 // 4
mod = 10 % 4

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

div, mod = divmod(10, 4)

取整

取整操作分为向上取整向下取整两种,这两个操作可以用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

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

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

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

无穷大

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

from math import inf

ans = inf
res = -inf

注意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:])

有几点需要注意:

转换后的数据类型是字符串而不是整数
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)

实际上,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)

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

优先队列相关操作

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

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

from heapq import *

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

堆化

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

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

从单词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入堆后的结果

入堆操作的时间复杂度为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出堆后的结果

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

获取堆顶元素

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

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

小根堆与大根堆

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

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

在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]的位置

前缀和

定义

  • 对于一个给定的数列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]

譬如对于上述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]

也可以直接调用内置库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]

需要注意两点:

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

位运算

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

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

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

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

与运算

与运算的符号是&

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

0 1
0 0 0
1 0 1

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

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

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

或运算

或运算的符号是|

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

0 1
0 0 1
1 1 1

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

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

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

异或运算

异或运算的符号是^

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

0 1
0 0 1
1 1 0

两个位置只要有一个不为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

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

(a & b) & c = a & (b & c)
(a | b) | c = a | (b | c)
(a ^ b) ^ c = a ^ (b ^ c)

判断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)]

元组推导式

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

集合推导式

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

字典推导式

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)}

其他知识补充

字典序与数字序

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

字典序就是按照字符在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

lambda匿名函数

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

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

# 使用def关键字进行函数定义
def get_one():
    return 1

# 输出1
print(get_one())

如果我们使用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

把上述代码进行合并操作,可以无需声明函数名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

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

# 使用def关键字进行函数定义
def add(x, y):
    return x+y

# 输出30
print(add(10, 20))

如果我们使用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后面加上括号并传入参数,才能正确地输出结果

把上述代码进行合并操作,可以无需声明函数名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))
# 在匿名函数后面加上括号并传入参数,才能正确地输出结果

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

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])

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

from collections import defaultdict

# 哈希表d1的value的默认值即为1
d_lambda = defaultdict(lambda: 1)

# 输出1
print(d_lambda[0])

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)

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

lst = ["123", "4567", "0", "12", "789"]

# 注意这里key参数传入是函数名len,而不是调用函数len()
lst.sort(key = lambda x: len(x))

# 输出['0', '12', '123', '789', '4567']
print(lst)

如果对于长度相同的字符串,我们还想要按照数字序降序排列,那么仅用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)

内置函数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)

使用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)

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"

一个常用的技巧是,如果有一个小写字母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")

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

关于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]))

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

全局变量

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

def f():
    global ans
    ans += 1

ans = 0
print(ans)
f()
print(ans)

最大递归深度

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

RecursionError: maximum recursion depth exceeded in comparison

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

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

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


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

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

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

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

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

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

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

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

你可能感兴趣的:(大厂秋招真题,最新华为OD真题,算法刷题技巧,python,算法,windows)