Python 二分法取两组有序数列中位数
- 题: 取两组有序数列中位数,并且要求算法的时间复杂度为 O(log(m + n))。
- 现有两组有序数列 (Python 起始 index 为 0)
A n = { a 0 , a 1 , a 2 , ⋯ , a n − 1 } , ( a 0 ≤ a 1 ≤ ⋯ ≤ a n − 1 ) A_n = \big\{a_0,a_1, a_2, \cdots, a_{n-1}\big\}, \; (a_0 \leq a_1 \leq \cdots \leq a_{n-1}) An={a0,a1,a2,⋯,an−1},(a0≤a1≤⋯≤an−1)
B m = { b 0 , b 1 , b 2 , ⋯ , b m − 1 } , ( b 0 ≤ b 1 ≤ ⋯ ≤ b m − 1 ) B_m = \big\{b_0,b_1, b_2, \cdots, b_{m-1}\big\}, \; (b_0 \leq b_1 \leq \cdots \leq b_{m-1}) Bm={b0,b1,b2,⋯,bm−1},(b0≤b1≤⋯≤bm−1) - 为了方便表示 n ≤ m n\leq m n≤m
# 例如
A_4 = [1,3,5,7]
B_7 = [2,4,6,8,10,12,14]
C
n
+
m
=
{
A
n
,
B
m
}
C_{n+m} = \{A_n,B_m\}
Cn+m={An,Bm}
C
n
+
m
=
{
c
0
,
c
1
,
c
2
,
⋯
,
c
n
+
m
−
1
}
C_{n+m} = \big\{c_0,c_1, c_2, \cdots, c_{n+m-1}\big\}
Cn+m={c0,c1,c2,⋯,cn+m−1}
- 中位数:
- 两数值的中位数 为两数组合并的有序数列
C
n
+
m
C_{n+m}
Cn+m 的中间数
- 若为奇,为中间序号为 ( m + n ) / / 2 (m+n)//2 (m+n)//2 的那一个数。
- 若为偶,为中间序号为 ( m + n ) / / 2 − 1 , ( m + n ) / / 2 (m+n)//2-1,(m+n)//2 (m+n)//2−1,(m+n)//2 的两个数的平均数。
- 两数值的中位数 为两数组合并的有序数列
C
n
+
m
C_{n+m}
Cn+m 的中间数
C_11 = A_4+B_7
C_11.sort()
n, m = len(A_4), len(B_7)
print('C_11 = A_4 + B_7: ', C_11)
print(f'中位数: {C_11[(n+m)//2]}, index: {(n+m)//2}')
C_11 = A_4 + B_7: [1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14]
中位数: 6, index: 5
D_18 = C_11+B_7
D_18.sort()
n, m = len(C_11), len(B_7)
print('D_18 = C_11 + B_7: ', D_18)
print(f'中位数: {(D_18[(n+m)//2-1]+D_18[(n+m)//2])/2}, index: {(n+m)//2-1,(n+m)//2}')
D_18 = C_11 + B_7: [1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 10, 10, 12, 12, 14, 14]
中位数: 6.5, index: (8, 9)
-
一般计算是给所有数列排序,再进行查找。
-
为了更快速的计算,需要使用特殊算法来优化它。
-
主要目的:
- 若为奇: 找到 第 k k k 那个数。 (令 k = ( m + n ) / / 2 k = (m+n)//2 k=(m+n)//2)
- 若为偶: 找到 第 k , k − 1 k,k-1 k,k−1 那两个数。
-
查找 c k c_k ck , 我们可以使用二分法
-
一般是 把 C C C 分成两份 C 右 , C 左 C_右, C_左 C右,C左
C = { C → : { c k , ⋯ , c n + m − 1 } C ← : { c 0 , c 1 , ⋯ , c k − 1 } \begin{aligned}C = \begin{cases}C_{\rightarrow} : \{c_{k}, &\cdots, &c_{n+m-1} \} \\C_{\leftarrow}: \{c_0,c_1, &\cdots, &c_{k-1} \} \end{cases}\end{aligned} C={C→:{ck,C←:{c0,c1,⋯,⋯,cn+m−1}ck−1}- 然后查找 c k c_{k} ck 或 c k − 1 c_{k-1} ck−1
-
在本题中, 由于 C = A + B C = A + B C=A+B ,虽然 A A A 和 B B B 为有序数组,但 C C C 在相加后并不是。
print('C_11: ',A_4+B_7)
C_11: [1, 3, 5, 7, 2, 4, 6, 8, 10, 12, 14]
-
因为先整合再排序,计算步骤会增多,所以 需要从 A A A 和 B B B 中直接提取 做比较
-
找 k k k 解题逻辑:
- 从两组数组 各取 第 < k / / 2 <k//2 <k//2 的值进行比较
- 排除掉较小一边的值
- 递归至排除所有 C ← C_\leftarrow C← 找到第 k k k 值
A_4 = [1,3,5,7]
B_7 = [2,4,6,8,10,12,14]
n, m = len(A_4), len(B_7)
k = (n+m)//2
print('寻找 k = ', k)
寻找 k = 5
i, j = (k-1)//2, (k-1)//2
print(f'A_4: {A_4}[i={i}]: {A_4[i]}')
print(f'B_7: {B_7}[j={j}]: {B_7[j]}')
A_4: [1, 3, 5, 7][i=2]: 5
B_7: [2, 4, 6, 8, 10, 12, 14][j=2]: 6
- a k / / 2 → a i , b k / / 2 → b j a_{k//2}\to a_i,\; b_{k//2}\to b_j ak//2→ai,bk//2→bj
- 若 a i < b j a_i<b_j ai<bj
- 可知 a i ∈ C ← a_i \in C_\leftarrow ai∈C←
- 排除掉 a n ≤ i a_{n\leq i} an≤i 取 C n e w = { A n [ i + 1 : ] , B m } C_{new} = \{A_{n}[i+1:],B_m\} Cnew={An[i+1:],Bm}
- 重新查找 第 k − ( i + 1 ) k-(i+1) k−(i+1) 个数 在 C n e w = { C i + 1 , C i + 2 , ⋯ , C n + m } 中 C_{new} = \{C_{i+1},C_{i+2},\cdots, C_{n+m}\}中 Cnew={Ci+1,Ci+2,⋯,Cn+m}中
ai = i + 1 # A 起始位
k_new = k - ai
print('new k: ', k_new)
new k: 2
i, j = (k_new-1)//2, (k_new-1)//2
print(f'A_4: {A_4}[i={ai+i}]: {A_4[ai+i]}')
print(f'B_7: {B_7}[j={j}]: {B_7[j]}')
A_4: [1, 3, 5, 7][i=3]: 7
B_7: [2, 4, 6, 8, 10, 12, 14][j=0]: 2
bj = j+1
k_new = k-ai-bj
print('new k: ', k_new)
new k: 1
i, j = (k_new-1)//2, (k_new-1)//2
print(f'A_4: {A_4}[i={ai+i}]: {A_4[ai+i]}')
print(f'B_7: {B_7}[j={bj+j}]: {B_7[bj+j]}')
A_4: [1, 3, 5, 7][i=3]: 7
B_7: [2, 4, 6, 8, 10, 12, 14][j=1]: 4
bj = bj+j+ 1
k_new = k-ai-bj
print('new k: ', k_new)
new k: 0
print('Find ', min(A_4[ai],B_7[bj]))
Find 6
-
输出条件:
- k = 0 k = 0 k=0 时: 取 较小值
- A A A 或 B B B 为空时( a i > n a_i>n ai>n): 取不空的k 值
-
开始编写
def takeKthItem(A:list, B:list,k,ai:int=0, bj:int=0):
n,m = len(A),len(B)
k_new = k-ai-bj
if (n-ai)>(m-bj): # 默认 n<m, 若不是 交换
return takeKthItem(B,A,k,bj,ai)
if ai>=n: # 为空时
return B[bj+k_new]
if k_new == 0:
return min(A[ai],B[bj])
i,j = min((k_new-1)//2,n-ai-1),min((k_new-1)//2,m-bj-1) # index 不能超过list长度
if A[ai+i]<B[bj+j]:
return takeKthItem(A, B, k,ai+i+1,bj)
else:
return takeKthItem(A, B, k,ai,bj+j+1)
A = [1,2,3,4,5]
B = [2,4,6,8,10,12,14]
print('take 10th item: ',takeKthItem(A,B,10))
A + B = [1, 2, 2, 3, 4, 4, 5, 6, 8, 10, 12, 14]
take 10th item: 12
- 完成
def findMedianSortedList(A:list,B:list)->float:
def takeKthItem(A:list, B:list,k,ai:int=0, bj:int=0):
n,m = len(A),len(B)
k_new = k-ai-bj
if (n-ai)>(m-bj): # 默认 n<m, 若不是 交换
return takeKthItem(B,A,k,bj,ai)
if ai>=n: # 为空时
return B[bj+k_new]
if k_new == 0:
return min(A[ai],B[bj])
i,j = min((k_new-1)//2,n-ai-1),min((k_new-1)//2,m-bj-1) # index 不能超过list长度
if A[ai+i]<B[bj+j]:
return takeKthItem(A, B, k,ai+i+1,bj)
else:
return takeKthItem(A, B, k,ai,bj+j+1)
n,m = len(A), len(B)
right,left = (𝑚+𝑛)//2, (𝑚+𝑛)//2-1
if (n+m)%2: # 如果奇数
return takeKthItem(A, B, right)
else: # 如果偶数
return (takeKthItem(A, B, right)+takeKthItem(A, B, left))/2
findMedianSortedList(A,B)
4.5
findMedianSortedList([2,3],[1,4,5])
3
A = [i for i in range(1,1000000,2)]
B = [i for i in range(5000,3000000,3)]
def normal(A,B):
C = A+B
C.sort()
length = len(C)
if length%2:
return C[length//2]
else:
return (C[length//2]+C[length//2-1])/2
%time normal(A,B)
Wall time: 32.9 ms
901000.0
- 普通的方法,计算复杂的两组数列需要 32.9 ms。
%time findMedianSortedList(A,B)
Wall time: 0 ns
901000.0
- 优化过后,二分法计算复杂的两组数列需要 < 1 < 1 <1 ns。