公倍数与公因数
辗转相除法(欧几里得算法)
利用辗转相除法,可以很方便地求得两个数的最大公因数(greatest common divisor, gcd)。
证明:
g
c
d
(
a
,
b
)
=
g
c
d
(
b
,
a
%
b
)
gcd(a,b)=gcd(b,a\% b)
gcd(a,b)=gcd(b,a%b)
记
g
c
d
(
a
,
b
)
=
d
,
g
c
d
(
b
,
a
%
b
)
=
e
gcd(a,b)=d,gcd(b,a\% b)=e
gcd(a,b)=d,gcd(b,a%b)=e,
d
∣
a
,
d
∣
b
,
a
%
b
=
a
−
⌊
a
/
b
⌋
b
⇒
d
∣
a
%
b
d|a,\quad d|b,\quad a\% b = a-\lfloor a/b\rfloor b\Rightarrow d|a\%b
d∣a,d∣b,a%b=a−⌊a/b⌋b⇒d∣a%b
故
d
d
d 是
b
b
b 和
a
%
b
a\%b
a%b 的公因子,
d
≤
e
d\leq e
d≤e。
e
∣
b
,
e
∣
a
%
b
,
a
=
⌊
a
/
b
⌋
b
+
(
a
−
⌊
a
/
b
⌋
b
)
⇒
e
∣
a
e|b,\quad e|a\%b,\quad a= \lfloor a/b\rfloor b+(a-\lfloor a/b\rfloor b)\Rightarrow e|a
e∣b,e∣a%b,a=⌊a/b⌋b+(a−⌊a/b⌋b)⇒e∣a
故 e e e 是 a a a 和 b b b 的公因子, e ≤ d e\leq d e≤d, e = d e=d e=d。
具体做法:用较大数除以较小数,再用出现的余数(第一余数)去除除数,再用出现的余数(第二余数)去除第一余数,如此反复,直到最后余数是0为止,最后的除数就是这两个数的最大公约数。
将两个数相乘再除以最大公因数即可得到最小公倍数(least common multiple, lcm)。
def gcd(a,b):
return a if b==0 else gcd(b,a%b)
def lcm(a,b):
return a*b/gcd(a,b)
扩展的欧几里得算法
任意两个整数 a a a 与 b b b,必存在整数 x x x 与 y y y 使得 a x + b y = g c d ( a , b ) ax + by = gcd(a,b) ax+by=gcd(a,b)。且 g c d ( a , b ) gcd(a,b) gcd(a,b) 是 a a a 和 b b b 最小的正线性组合。
证明:
设
g
c
d
(
a
,
b
)
gcd(a,b)
gcd(a,b) 为
d
d
d,
a
a
a 和
b
b
b 的最小正线性组合为
s
s
s,下证
d
=
s
d=s
d=s。
d
=
g
c
d
(
a
,
b
)
⇒
d
∣
a
,
d
∣
b
⇒
d
∣
s
d=gcd(a,b)\Rightarrow d|a ,\quad d|b \Rightarrow d|s
d=gcd(a,b)⇒d∣a,d∣b⇒d∣s
a % s = a − ⌊ a / s ⌋ s = a − ⌊ a / s ⌋ ( a x + b y ) = a ( 1 − ⌊ a / s ⌋ x ) − b ⌊ a / s ⌋ y a\% s = a-\lfloor a/s\rfloor s = a-\lfloor a/s\rfloor (ax+by) = a(1-\lfloor a/s\rfloor x)-b\lfloor a/s\rfloor y a%s=a−⌊a/s⌋s=a−⌊a/s⌋(ax+by)=a(1−⌊a/s⌋x)−b⌊a/s⌋y
表明 a % s a\% s a%s也是 a a a 和 b b b 的线性组合,但 a % s < s a\% s<s a%s<s, a % s a\% s a%s不能是 a a a 和 b b b 的最小的正线性组合,故 $ a % s = 0 a\% s=0 a%s=0,即 s ∣ a s|a s∣a。同理有 s ∣ b s|b s∣b 。所以 s s s 是 a a a 和 b b b 的公因子,所以 s ≤ d s\leq d s≤d。又 d ∣ s d|s d∣s ,所以 d = s d=s d=s。
如何求出 x x x 和 y y y:
当
b
=
0
b=0
b=0 时,
g
c
d
(
a
,
b
)
=
a
gcd(a,b)=a
gcd(a,b)=a,此时
x
=
1
,
y
=
0
x=1,y=0
x=1,y=0;
当
b
≠
0
b\neq0
b=0时,设已经求出
g
c
d
(
b
,
a
%
b
)
gcd(b,a\%b)
gcd(b,a%b)的线性组合:
g
c
d
(
b
,
a
%
b
)
=
b
x
′
+
a
%
b
y
′
gcd(b,a\%b)=bx'+a\% b y'
gcd(b,a%b)=bx′+a%by′,则
g
c
d
(
a
,
b
)
=
g
c
d
(
b
,
a
%
b
)
=
b
x
′
+
a
%
b
y
′
=
b
x
′
+
(
a
−
⌊
a
/
b
⌋
b
)
y
′
=
a
y
′
+
b
(
x
′
−
⌊
a
/
b
⌋
y
′
)
gcd(a,b)=gcd(b,a\%b)=bx'+a\% b y'=bx'+(a-\lfloor a/b\rfloor b)y'=ay'+b(x'-\lfloor a/b\rfloor y')
gcd(a,b)=gcd(b,a%b)=bx′+a%by′=bx′+(a−⌊a/b⌋b)y′=ay′+b(x′−⌊a/b⌋y′)
令 x = y ′ , y = x ′ − ⌊ a / b ⌋ y ′ x=y',y=x'-\lfloor a/b\rfloor y' x=y′,y=x′−⌊a/b⌋y′ 即可递归的求得。
def xGCD(a,b):
if b==0:
x,y=1,0
return x,y,a
x1,y1,gcd=xGCD(b,a%b)
x,y=y1,x1-a//b*y1
return x,y,gcd
质数
204.计数质数【简单】
LeetCode传送门
统计所有小于非负整数 n 的质数的数量。
题解:
埃拉托斯特尼筛法(Sieve of Eratosthenes,简称埃氏筛法)是非常常用的判断一个整数是否是质数的方法,并且,它可以在判断一个整数
n
n
n 是不是质数的同时,判断所有小于
n
n
n 的整数。原理为:从 1 到
n
n
n 遍历,假设当前遍历到
m
m
m ,则把所有小于
n
n
n 的、且是
m
m
m 的倍数的整数标为和数;遍历完成后,没有被标为和数的数字即为质数。
class Solution:
def countPrimes(self, n: int) -> int:
if n<=2:return 0
prime=[True]*n
count=n-2 # 去掉1
for i in range(2,n):
if prime[i]:
j=2*i
while j<n:
if prime[j]:
prime[j]=False
count-=1
j+=i
return count
- 如果一个数不是素数是合数,那么一定可以由两个自然数相乘得到,其中一个大于或等于它的平方根,一个小于或等于它的平方根。
class Solution:
def countPrimes(self, n: int) -> int:
if n<=2:return 0
prime=[True]*n
i,sqrtn,count=3,n**0.5,n//2 # 偶数一定不是质数
while i<=sqrtn:
j=i*i
while j<n:
if prime[j]:
count-=1
prime[j]=False
j+=2*i
i+=2 # 排除偶数
while i<=sqrtn and not prime[i]:
i+=2
return count
数字处理
504.七进制数【简单】
LeetCode传送门
给定一个整数,将其转化为7进制,并以字符串形式输出。
注意: 输入范围是 [-1e7, 1e7] 。
题解:
进制转换类型的题,通常是利用除法和取模(mod)来进行计算,同时也要注意一些细节,如负数和零。如果输出是数字类型而非字符串,则也需要考虑是否会超出整数上下界。
class Solution:
def convertToBase7(self, num: int) -> str:
if num==0:return '0'
is_negative=num<0
if is_negative:
num=-num
ans=''
while num:
a,b=num//7,num%7
ans=str(b)+ans
num=a
if is_negative:
return '-'+ans
else:
return ans
168.Excel表列名称【简单】
LeetCode传送门
给定一个正整数,返回它在 Excel 表中相对应的列名称。
题解: 十进制转26进制:
A
,
B
,
C
,
D
,
⋯
,
Z
A,B,C,D,\cdots,Z
A,B,C,D,⋯,Z
在转换为26进制时,每次除26,余数与26进制数的对应关系为:
1
↔
A
,
2
↔
B
,
3
↔
C
,
4
↔
D
,
⋯
,
26
/
0
↔
Z
1\leftrightarrow A,2\leftrightarrow B,3\leftrightarrow C,4\leftrightarrow D,\cdots ,26/0\leftrightarrow Z
1↔A,2↔B,3↔C,4↔D,⋯,26/0↔Z
需要注意,余数为0时,26 个字母没有任何一个字母是表示0, 所以可以从 商 借一个给余数,即 0 ↔ 26 0\leftrightarrow 26 0↔26。
class Solution:
def convertToTitle(self, n: int) -> str:
ans=''
while n:
n,y=divmod(n,26)
if y==0:
n-=1
y=26
ans=chr(y+64)+ans
return ans
divmod() 函数把除数和余数运算结果结合起来,返回一个包含商和余数的元组(a // b, a % b)。
172.阶乘后的零【简单】
LeetCode传送门
给定一个整数 n,返回 n! 结果尾数中零的数量。
题解:
- 在一个阶乘中,把所有 1 1 1 和 n n n 之间的数相乘,这和把所有 1 1 1 和 n n n 之间所有数字的因子相乘是一样的。
- 每个尾部的 0 0 0 由 2 × 5 = 10 2\times5=10 2×5=10 而来,因此我们可以把阶乘的每一个元素拆成质数相乘,统计有多少个2 和5。
- 质因子2 的数量远多于质因子5 的数量,例如 n = 16 n=16 n=16,包含因子5的数字有5、10、15,包含因子2的数字有2、4、6、8、10、12、14、16。因此我们可以只统计阶乘结果里有多少个质因子5。
class Solution:
def trailingZeroes(self, n: int) -> int:
count=0
for i in range(5,n+1,5):
cur=i
while cur%5==0:
count+=1
cur//=5
return count
415.字符串相加【简单】
LeetCode传送门
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。
提示:
- num1 和num2 的长度都小于 5100
- num1 和num2 都只包含数字 0-9
- num1 和num2 都不包含任何前导零
- 你不能使用任何內建 BigInteger 库, 也不能直接将输入的字符串转换为整数形式
题解:
设定 i,j 两指针分别指向 num1,num2 尾部,模拟人工加法;
- 计算进位: 计算 carry = tmp // 10,代表当前位相加是否产生进位;
- 添加当前位: 计算 tmp = n1 + n2 + carry,并将当前位 tmp % 10 添加至 res 头部;
- 索引溢出处理: 当指针 i或j 走过数字首部后,给 n1,n2 赋值为 00,相当于给 num1,num2 中长度较短的数字前面填 00,以便后续计算。
- 当遍历完 num1,num2 后跳出循环,并根据 carry 值决定是否在头部添加进位 11,最终返回 res 即可。
class Solution:
def addStrings(self, num1: str, num2: str) -> str:
ans=''
i,j,carry=len(num1)-1,len(num2)-1,0
while i>=0 or j>=0:
n1=int(num1[i]) if i>=0 else 0
n2=int(num2[j]) if j>=0 else 0
tmp=n1+n2+carry
carry=tmp//10
ans=str(tmp%10)+ans
i,j=i-1,j-1
return str(carry)+ans if carry else ans
67.二进制求和【简单】
LeetCode传送门
给你两个二进制字符串,返回它们的和(用二进制表示)。
输入为 非空 字符串且只包含数字 1 和 0。
题解: 双指针模拟
class Solution:
def addBinary(self, a: str, b: str) -> str:
if not a or not b:return a or b
a,b,ans=a[::-1],b[::-1],[]
i,j,carry=0,0,0
while i<len(a) or j<len(b) or carry:
n1=int(a[i]) if i<len(a) else 0
n2=int(b[j]) if j<len(b) else 0
carry,cur=divmod(n1+n2+carry,2)
ans.append(str(cur))
i,j=i+1,j+1
return ''.join(ans[::-1])
326.3的幂【简单】
LeetCode传送门
给定一个整数,写一个函数来判断它是否是 3 的幂次方。
题解:
方法一:利用对数。设
log
3
n
=
m
\log_3 n=m
log3n=m,如果
n
n
n 是 3 的幂次方,则
m
m
m 一定是整数。
class Solution:
def isPowerOfThree(self, n: int) -> bool:
if n<=0:return False
import math
log3 = math.log(n,3)
if log3%1==0:
return True
return False
当 n = 243 n=243 n=243 时,答案错误,这是由于Python的精度问题。
class Solution:
def isPowerOfThree(self, n: int) -> bool:
if n<=0:return False
import math
log3 = math.log(n,3)
if 3**round(log3)==n:
return True
return False
方法二:因为在int 范围内3 的最大次方是 3 19 = 1162261467 3^{19} = 1162261467 319=1162261467,如果n 是3 的整数次方,那么1162261467 除以n 的余数一定是零;反之亦然。
class Solution:
def isPowerOfThree(self, n: int) -> bool:
return n>0 and 1162261467%n==0
补充:Python精度问题
浮点数在计算机中的表示

在计算机中无论是整数、浮点数、还是字符串最终都是用二进制来表示的。
根据国际标准IEEE 754,一个二进制浮点数分为3部分:s(符号位,0表示正数,1表示负数)、E(指数位)、M(有效数字位)。
-
对于32位的浮点数,最高1位是符号位,接着8位是指数位,即整数部分,剩下的23位为小数位。
-
对于64位的浮点数,最高1位是符号位,接着11位是指数位,剩下的52位为小数位。
Python中浮点数精度处理
因浮点数在计算机中实际是以二进制保存的,有些数不精确。比如说:0.1是十进制,转化为二进制后它是个无限循环的数:0.00011001100110011001100110011001100110011001100110011001100。Python是以双精度(64)位来保存浮点数,多余的位会被截掉,所以看到的是0.1,但在电脑上实际保存的已不是精确的0.1,参与运算后,也就有可能点误差。
如何在Python中获取特定位数精度值:
方法一:
round(x[,n])
:返回浮点数 x 的四舍五入值,n 默认值为0。
方法二:decimal 模块
238.除自身以外数组的乘积【中等】
LeetCode传送门
给你一个长度为 n 的整数数组 nums,其中 n > 1,返回输出数组 output ,其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
题解: 左右乘积列表 O ( n ) O(n) O(n)
- 初始化两个空数组 L 和 R。对于给定索引 i,L[i] 代表的是 i 左侧所有数字的乘积,R[i] 代表的是 i 右侧所有数字的乘积。
- 需要用两个循环来填充 L 和 R 数组的值。对于数组 L,L[0] 应该是 1,因为第一个元素的左边没有元素。对于其他元素:L[i] = L[i-1] * nums[i-1]。同理,对于数组 R,R[length-1] 应为 1,length 指的是输入数组的大小,其他元素:R[i] = R[i+1] * nums[i+1]。
- 当 R 和 L 数组填充完成,我们只需要在输入数组上迭代,且索引 i 处的值为:L[i] * R[i]。
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
length=len(nums)
L,R,ans=[0]*length,[0]*length,[0]*length
L[0]=1
for i in range(1,length):
L[i]=nums[i-1]*L[i-1]
R[length-1]=1
for i in range(length-2,-1,-1):
R[i]=nums[i+1]*R[i+1]
for i in range(length):
ans[i]=L[i]*R[i]
return ans
优化:空间复杂度
O
(
1
)
O(1)
O(1)
把 ans 作为方法一的 L 数组。这种方法的唯一变化就是我们没有构造 R 数组,而是用一个遍历来跟踪右边元素的乘积,并更新数组 ans[i]=ans[i]R。然后 R 更新为 R=Rnums[i],其中变量 R 表示的就是索引右侧数字的乘积。
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
length=len(nums)
ans=[0]*length
ans[0]=1
for i in range(1,length):
ans[i]=nums[i-1]*ans[i-1]
R=1
for i in range(length-1,-1,-1):
ans[i]=ans[i]*R
R*=nums[i]
return ans
453.最少移动次数使数组元素相等【简单】
LeetCode传送门
给定一个长度为 n 的非空整数数组,找到让数组所有元素相等的最小移动次数。每次移动将会使 n - 1 个元素增加 1。
题解:
暴力法:为了在最小移动内使所有元素相等,需要在数组的最大元素之外的所有元素中执行增加1。因此,暴力法将 1 添加到除最大元素之外的所有元素,并增加移动数的计数,直到最大元素和最小元素彼此相等。
优化暴力法:一次性增加增量 k=max-min,并将移动次数增加 k。然后对整个数组进行扫描,找到新的最大值和最小值,重复这一过程直到最大元素和最小元素相等。
排序:对数组排序后得到有序数组,用 diff=max-min 来更新数组。由于数组是有序的,在第一步中,最后的元素即为最大值,因此 diff=a[n-1]-a[0],对除了最后一个元素以外所有元素增加 diff,由于数组有序,此时 a [ n − 1 ] = a ′ [ 0 ] < a ′ [ 1 ] < ⋯ < a ′ [ n − 2 ] a[n-1]=a'[0]<a'[1]<\cdots<a'[n-2] a[n−1]=a′[0]<a′[1]<⋯<a′[n−2],第二次更新时,diff=a’[n-2]-a’[0],第二次更新后 a ′ [ n − 2 ] = a ′ ′ [ 0 ] = a ′ ′ [ n − 1 ] a'[n-2]=a''[0]=a''[n-1] a′[n−2]=a′′[0]=a′′[n−1],且 a ′ ′ [ 0 ] < a ′ ′ [ 1 ] < ⋯ < a ′ ′ [ n − 3 ] a''[0]<a''[1]<\cdots<a''[n-3] a′′[0]<a′′[1]<⋯<a′′[n−3],a’’[n-3]最大,……,因此, m o v e s = ∑ i = 1 n − 1 ( a [ i ] − a [ 0 ] ) moves=\sum_{i=1}^{n-1}(a[i]-a[0]) moves=∑i=1n−1(a[i]−a[0])。
class Solution:
def minMoves(self, nums: List[int]) -> int:
nums.sort()
count=0
for i in range(len(nums)-1,0,-1):
count+=nums[i]-nums[0]
return count
数学法:将除了一个元素之外的全部元素+1,等价于将该元素-1,因为我们只对元素的相对大小感兴趣。因此,该问题简化为需要进行的减法次数。显然,只需要将所有的数都减到最小的数即可。 m o v e s = ∑ i = 0 n − 1 ( a [ i ] − m i n ( a ) ) moves=\sum_{i=0}^{n-1}(a[i]-min(a)) moves=∑i=0n−1(a[i]−min(a))
class Solution:
def minMoves(self, nums: List[int]) -> int:
minnum=min(nums)
count=0
for i in range(len(nums)):
count+=nums[i]-minnum
return count
462.最少移动次数使数组元素相等2【中等】
LeetCode传送门
给定一个非空整数数组,找到使所有数组元素相等所需的最小移动数,其中每次移动可将选定的一个元素加1或减1。 您可以假设数组的长度最多为10000。
题解:
假设最终数组 a 中的每个数均为 x,那么需要移动的次数即为
∣
a
[
0
]
−
x
∣
+
∣
a
[
1
]
−
x
∣
+
.
.
.
+
∣
a
[
n
−
1
]
−
x
∣
|a[0] - x| + |a[1] - x| + ... + |a[n-1] - x|
∣a[0]−x∣+∣a[1]−x∣+...+∣a[n−1]−x∣。如果我们把数组 a 中的每个数看成水平轴上的一个点,那么根据上面的移动次数公式,我们需要找到在水平轴上找到一个点 x,使得这 N 个点到 x 的距离之和最小。这是一个经典的数学问题,当 x 为这个 N 个数的中位数时,可以使得距离最小。具体地,若 N 为奇数,则 x 必须为这 N 个数中的唯一中位数;若 N 为偶数,中间的两个数为 p 和 q,中位数为 (p + q) / 2,此时 x 只要是区间 [p, q] 中的任意一个数即可。
class Solution:
def minMoves2(self, nums: List[int]) -> int:
nums.sort()
sums=0
for num in nums:
sums+=abs(nums[len(nums)//2]-num)
return sums
169.多数元素【简单】
LeetCode传送门
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
题解: Boyer-Moore 投票算法
原理:如果我们把众数记为 +1+1,把其他数记为 -1−1,将它们全部加起来,显然和大于 0,从结果本身我们可以看出众数比其他数多。
算法:
- 维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值,count 为 0;
- 遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前,如果 count 的值为 0,我们先将 x 的值赋予candidate,随后判断 x:
- 如果 x 与 candidate 相等,那么计数器 count 的值增加 1;
- 如果 x 与 candidate 不等,那么计数器 count 的值减少 1。
- 在遍历完成后,candidate 即为整个数组的众数。
class Solution:
def majorityElement(self, nums: List[int]) -> int:
count=0
condidate=None
for num in nums:
if count==0:
condidate=num
count+=(1 if num==condidate else -1)
return condidate
随机与取样
384.打乱数组【中等】
LeetCode传送门
打乱一个没有重复元素的数组。
示例:
// 以数字集合 1, 2 和 3 初始化数组。
int[] nums = {1,2,3};
Solution solution = new Solution(nums);
// 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。
solution.shuffle();
// 重设数组到它的初始状态[1,2,3]。
solution.reset();
// 随机返回数组[1,2,3]打乱后的结果。
solution.shuffle();
题解:
方法一:暴力
把每个数放在一个 ”帽子“ 里面,每次从 ”帽子“ 里面随机摸一个数出来,直到 “帽子” 为空。下面是具体操作,首先我们把数组 array 复制一份给数组 aux,之后每次随机从 aux 中取一个数,为了防止数被重复取出,每次取完就把这个数从 aux 中移除。重置 的实现方式很简单,只需把 array 恢复称最开始的状态就可以了。
class Solution:
def __init__(self, nums: List[int]):
self.array=nums
self.original=list(nums)
def reset(self) -> List[int]:
self.array=self.original
self.original=list(self.original)
return self.array
def shuffle(self) -> List[int]:
aux=list(self.array)
for idx in range(len(self.array)):
remove_idx=random.randrange(len(aux))
self.array[idx]=aux.pop(remove_idx)
return self.array
方法二: Fisher-Yates 洗牌算法
对于洗牌问题,Fisher-Yates 洗牌算法即是通俗解法,同时也是渐进最优的解法。
Fisher-Yates 洗牌算法跟暴力算法很像。在每次迭代中,生成一个范围在当前下标到数组末尾元素下标之间的随机整数。接下来,将当前元素和随机选出的下标所指的元素互相交换,这一步模拟了每次从 “帽子” 里面摸一个元素的过程,其中选取下标范围的依据在于每个被摸出的元素都不可能再被摸出来了。此外还有一个需要注意的细节,当前元素是可以和它本身互相交换的,否则生成最后的排列组合的概率就不对了。
class Solution:
def __init__(self, nums: List[int]):
self.array=nums
self.original=list(nums)
def reset(self) -> List[int]:
self.array=self.original
self.original=list(self.original)
return self.array
def shuffle(self) -> List[int]:
for idx in range(len(self.array)):
swap_idx=random.randrange(idx,len(self.array))
self.array[idx],self.array[swap_idx]=self.array[swap_idx],self.array[idx]
return self.array
528.按权重随机选择【中等】
LeetCode传送门
给定一个正整数数组 w ,其中 w[i] 代表下标 i 的权重(下标从 0 开始),请写一个函数 pickIndex ,它可以随机地获取下标 i,选取下标 i 的概率与 w[i] 成正比。
例如,对于 w = [1, 3],挑选下标 0 的概率为 1 / (1 + 3) = 0.25 (即25%),而选取下标 1 的概率为 3 / (1 + 3) = 0.75(即75%)。也就是说,选取下标 i 的概率为 w[i] / sum(w) 。
提示:
- 1 <= w.length <= 10000
- 1 <= w[i] <= 10^5
- pickIndex 将被调用不超过 10000 次
题解: 前缀和+二分查找
先使用partial_sum 求前缀和(即到每个位置为止之前所有数字的和),这个结果对于正整数数组是单调递增的。每当需要采样时,我们可以先随机产生一个数字,然后使用二分法查找其在前缀和中的位置,以模拟加权采样的过程。这里的二分法可以用 bisect_left 实现,该函数用于处理将会插入重复数值的情况,返回将会插入的位置。以样例为例,权重数组[1,3] 的前缀和为[1,4]。如果我们随机生成的数字为1,那么 bisect_left 返回的位置为0;如果我们随机生成的数字是2、3、4,那么 bisect_left 返回的位置为1。
class Solution:
import random
from bisect import bisect
def __init__(self, w: List[int]):
self.partial_sum=[]
tmp=0
for x in w:
tmp+=x
self.partial_sum.append(tmp)
def pickIndex(self) -> int:
randnum=random.randint(1,self.partial_sum[-1])
return bisect.bisect_left(self.partial_sum,randnum)
382.链表随机节点【中等】
LeetCode传送门
给定一个单链表,随机选择链表的一个节点,并返回相应的节点值。保证每个节点被选的概率一样。
进阶:如果链表十分大且长度未知,如何解决这个问题?你能否使用常数级空间复杂度实现?
题解: 蓄水池抽样算法
算法背景:大数据流中的随机抽样问题。当内存无法加载全部数据时,如何从包含未知大小的数据流中随机选取k个数据,并且要保证每个数据被抽取到的概率相等。
算法原理:
一、假设每次只能读取一个数据,数据流中含有 N 个数,如果要保证所有的数被抽到的概率相等,那么每个数被抽到的概率应该为 1 N \frac{1}{N} N1。蓄水库算法的方案是:每次只保留一个数,当遇到第 i 个数时,以 1 i \frac{1}{i} i1 的概率保留它, i − 1 i \frac{i-1}{i} ii−1 的概率保留原来的数。 举例说明: 1 - 10
- 遇到1,概率为1,保留第一个数。
- 遇到2,概率为1/2,这个时候,1和2各1/2的概率被保留
- 遇到3,3被保留的概率为1/3,(之前剩下的数假设1被保留),2/3的概率 1 被保留,(此时1被保留的总概率为 2/3 * 1/2 = 1/3)
- 遇到4,4被保留的概率为1/4,(之前剩下的数假设1被保留),3/4的概率 1 被保留,(此时1被保留的总概率为 3/4 * 2/3 * 1/2 = 1/4)
以此类推,每个数被保留的概率都是1/N。
证明:第 i 个数最后被采样的充要条件是,它被选择,且之后都被保留。这种情况发生的概率为
1
i
×
i
i
+
1
×
i
+
1
i
+
2
×
⋯
×
N
−
1
N
=
1
N
\frac{1}{i}\times\frac{i}{i+1}\times\frac{i+1}{i+2}\times\cdots\times\frac{N-1}{N}=\frac{1}{N}
i1×i+1i×i+2i+1×⋯×NN−1=N1
二、假设每次只能读取 k(k>1) 个数据,数据流中含有 N 个数,如果要保证所有的数被抽到的概率相等,那么每个数被抽到的概率应该为 k N \frac{k}{N} Nk。蓄水库算法的方案是:对于前 k 个数,全部保留,对于第 i ( i > k ) i(i>k) i(i>k)个数,以 k i \frac{k}{i} ik 的概率保留第 i 个数,并以 1 k \frac{1}{k} k1 的概率与前面已选择的 k 个数中的任意一个替换。
证明:
1.对于第
i
(
i
≤
k
)
i(i\leq k)
i(i≤k) 个数。在 k 步之前,被选中的概率为1,当走到第 k+1 步时,
i
被
第
k
+
1
个
元
素
替
换
的
概
率
=
第
k
+
1
个
元
素
被
选
中
的
概
率
×
i
被
选
中
替
换
的
概
率
=
k
k
+
1
×
1
k
=
1
k
+
1
i 被第 k+1 个元素替换的概率=第 k+1 个元素被选中的概率\times i 被选中替换的概率=\frac{k}{k+1}\times\frac{1}{k}=\frac{1}{k+1}
i被第k+1个元素替换的概率=第k+1个元素被选中的概率×i被选中替换的概率=k+1k×k1=k+11
则 i 被保留的概率为 1 − 1 k + 1 = k k + 1 1-\frac{1}{k+1}=\frac{k}{k+1} 1−k+11=k+1k。以此类推,不被第 k+2 个元素替换的概率为 1 − k k + 2 × 1 k = k + 1 k + 2 1-\frac{k}{k+2}\times\frac{1}{k}=\frac{k+1}{k+2} 1−k+2k×k1=k+2k+1。则到第 N 步时,i 被保留的概率为: 1 × k k + 1 × k + 1 k + 2 × k + 2 k + 3 ⋯ N − 1 N = k N 1\times\frac{k}{k+1}\times\frac{k+1}{k+2}\times\frac{k+2}{k+3}\cdots\frac{N-1}{N}=\frac{k}{N} 1×k+1k×k+2k+1×k+3k+2⋯NN−1=Nk
2.对于第
j
(
j
>
k
)
j(j>k)
j(j>k) 个数。在第 j 步被选中的概率为
k
j
\frac{k}{j}
jk。不被第 j+1 个元素替换的概率
1
−
k
j
+
1
×
1
k
=
j
j
+
1
1-\frac{k}{j+1}\times\frac{1}{k}=\frac{j}{j+1}
1−j+1k×k1=j+1j,以此类推,到第 N 步,j 被保留的概率为:
k
j
×
j
j
+
1
×
j
+
1
j
+
2
×
j
+
2
j
+
3
×
⋯
×
N
−
1
N
=
k
N
\frac{k}{j}\times\frac{j}{j+1}\times\frac{j+1}{j+2}\times\frac{j+2}{j+3}\times\cdots\times\frac{N-1}{N}=\frac{k}{N}
jk×j+1j×j+2j+1×j+3j+2×⋯×NN−1=Nk
class Solution:
import random
def __init__(self, head: ListNode):
"""
@param head The linked list's head.
Note that the head is guaranteed to be not null, so it contains at least one node.
"""
self.head=head
def getRandom(self) -> int:
"""
Returns a random node's value.
"""
count,reserve=0,0
cur=self.head
while cur:
count+=1
rand=random.randint(1,count)
if rand==count:
reserve=cur.val
cur=cur.next
return reserve
470.用Rand7()实现Rand10()【中等】
LeetCode传送门
已有方法 rand7 可生成 1 到 7 范围内的均匀随机整数,试写一个方法 rand10 生成 1 到 10 范围内的均匀随机整数。不要使用系统的 Math.random() 方法。
题解: 拒绝采样
在拒绝采样中,如果生成的随机数满足要求,那么久返回该随机数,否则会不断生成直到一个满足要求的随机数为止。若我们调用两次 Rand7(),那么可以生成 [1, 49] 之间的随机整数,我们只用到其中的 40 个,用来实现Rand10(),而拒绝剩下的 9 个数,如下图所示。
class Solution:
def rand10(self):
row=rand7()
col=rand7()
idx=col+(row-1)*7
while idx>40:
row=rand7()
col=rand7()
idx=col+(row-1)*7
return 1+(idx-1)%10
其他题解:rand10到rand7,rand7到rand10
LeetCode 101: A LeetCode Grinding Guide (C++ Version) 作者:高畅
LeetCode题解