注意:本篇内容仅包含一些在算法题目中较为常用的内置函数、方法、技巧,不包含所有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()
都有两个形参,reverse
和key
,前者表示是否倒序排列,后者表示排序的依据,通常会涉及到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_lst
和b_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一切皆对象的设计思想,此处的a
和b
是同一个列表对象的两个别名,它们表示同一个列表,因此修改b
会牵连到a
。这也是初学者非常容易犯的一个错误。
为了使得a
和b
指向两个不同的列表对象,我们在赋值操作时可以有两种操作,来让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]
以上两种方法,都可以使得a
和b
表示两个不同的(但值相同的)列表对象。这个技巧在回溯算法中非常常见。
使用内置模块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
呢?
可以初始化变量num
为0
,然后从左到右遍历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()
函数来实现。ceil
和floor
的意思分别是天花板和地板,非常形象。
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
,它的前缀和数列S
中S[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 |
假设要对两个数字a
和b
进行位运算,那么应该先将其转化为两个二进制数(仅包含0
和1
的两个二进制数),然后逐位地根据运算符进行运算,得到一个新的二进制数,再将其转换为十进制数即为结果。
与运算的符号是&
。
参与运算的两个数,如果两个相对应的位的均为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 = 0
和 a ^ 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
的幂若数字n
为2
的幂,即存在
n
=
2
k
n = 2^k
n=2k成立,那么其二进制的首位一定是1
,后面包含了k
个0
。
如16 = 0b10000
,其二进制的首位为1
,后面包含了4
个0
。
如果考虑n-1
的二进制,容易发现其位数应该比n
少1
,同时均为1
构成。
如15 = 0b1111
,其二进制为4
个1
由于只有当n
是2
的幂,才会具备上述性质,我们可以使用与运算来判断n
是否为2
的幂。
当且仅当 n & (n-1) == 0
成立时,n
为2
的幂。题目 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"
。数字序就是按照数字大小来进行排序。
10
和2
的比较,显然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
再看一个有参数函数的例子。假设我们需要一个计算两个参数x
和y
相加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
,直接传入两个数字10
和20
,得到结果:
# 这里的(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
是某种数据类型初始化函数的函数名,如int
,list
,dict
等等。
假设我们想要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算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务100+同学成功上岸!
课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化
每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!
60+天陪伴式学习,40+直播课时,300+动画图解视频,300+LeetCode经典题,200+华为OD真题/大厂真题,还有简历修改、模拟面试、专属HR对接将为你解锁
可上全网独家的欧弟OJ系统练习华子OD、大厂真题
可查看链接 大厂真题汇总 & OD真题汇总(持续更新)
绿色聊天软件戳 od1336
了解更多