中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
arr.sort()
n = len(arr)
if n % 2: median = arr[n // 2]
else: median = (arr[n // 2] + arr[n // 2 - 1]) / 2
median = (arr[(n - 1) // 2] + arr[n // 2]) / 2
arr.sort()
h = len(arr) // 2
(arr[h] + arr[~h])/2
a = [0,1,2,3,4,5],a[3] 为列表的第 4 位数,~3 = -4, a[-4] 实际上是对列表进行反向查找,为列表的第 3 位数,中值 (2 + 3) / 2 = 2.5。
a = [0,1,2,3,4,5,6] ,a[3] 为列表的第 4 位数,a[-4] 在列表中是正向反向查找中的第 4 位数,所以 a[3] == a[-4], 中位数就为 a[3] 或者 a[-4]。
from sortedcontainers import SortedList
class MedianFinder:
def __init__(self):
# self.sl = SortedList()
self.sl = []
def addNum(self, num: int) -> None:
insort_left(self.sl, num)
# self.sl.add(num)
def findMedian(self) -> float:
n = len(self.sl)
return (self.sl[n // 2] + self.sl[(n-1)//2]) / 2
中位数(Median)统计学名词,是指将统计总体当中的各个变量值按大小顺序排列起来,形成一个数列,处于变量数列中间位置的变量值就称为中位数,用 Me 表示。当变量值的项数 N 为奇数时,处于中间位置的变量值即为中位数;当 N 为偶数时,中位数则为处于中间位置的 2 个变量值的平均数。
本题考查动态维护数组的中位数。
对顶堆,所有 ≤ Me 的元素放到 small 中(大根堆),所有 > Me 的元素放到 big 中(小根堆),即一半小的放到 small,一半大的放到 big,small 至多多一个。
初始化:
先将前 k 个元素加入 small,然后从 small 弹出 k // 2 个元素到 big,small 至多多一个。
中位数:
当 k 为奇数时候,中位数是元素数量较多的 small 堆顶元素。
当 k 为偶数时候,中位数是 small 和 big 的堆顶元素平均值。
窗口滑动,左侧删除元素:
由于堆无法直接删除掉某个元素,所以需要延迟删除。d 记录这个元素欠账的个数。d[left]++;
堆两侧的平衡性发生了变化,如果 left ≤ small.top(),删掉的元素在 small 中,balance–; 否则,在 big 中,balance++;
右侧添加元素:
如果 right ≤ small.top(),right 放到 samll,balance++;否则放到 big,balance–。
操作后,balance = 0 或 2 或 -2。平衡调整后 balance = 0。
如果 balance = 0,以不用调整;
如果 balance = 2,small 比 big 多了两个,big.push(small.pop());
如果 balance = -2,small 比 big 少了两个,small.push(big.pop())。
清理堆顶删除的元素:
分别检查两边的堆顶元素,如果堆顶元素已被删除,则弹出堆顶元素,直到堆顶元素没有欠债为止。
计算中位数
class Solution:
def medianSlidingWindow(self, nums: List[int], k: int) -> List[float]:
# # 方法一:
# res = []
# for i in range(len(nums) - k + 1):
# w = sorted(nums[i:i + k]) # 需要优化
# res.append((w[(k - 1) // 2] + w[k // 2]) / 2)
# return res
# # 方法二:双堆对顶
# dh = DualHeap(k)
# for num in nums[:k]:
# dh.insert(num)
# ans = [dh.getMedian()]
# for i in range(k, len(nums)):
# dh.insert(nums[i])
# dh.erase(nums[i - k])
# ans.append(dh.getMedian())
# return ans
# 方法三:双堆对顶,简化版
def get(k): # 中位数
return -small[0] if k % 2 else (-small[0] + big[0]) / 2
small, big = [], [] # 大堆,小堆
d = defaultdict(int) # 延迟删除
# 1、初始化大小堆,分割前 k 个元素
for i in range(k): # 先把 k 个元素放入大堆
heappush(small, -nums[i])
for i in range(k // 2): # 一半较大的元素从大堆移到小堆
heappush(big, -heappop(small))
ans = [get(k)] # 第一个中位数
# 2、滑动窗口
for i in range(k, len(nums)):
balance = 0
# 2-1、记录将要删除的元素
left = nums[i - k] # 要删除的元素
d[left] += 1 # 先记下要删除的元素
if small and left <= -small[0]: balance -= 1 # 在小堆
else: balance += 1 # 在大堆
# 2-2、确定当前元素加入那个堆
if small and nums[i] <= -small[0]:
heappush(small, -nums[i])
balance += 1
else:
heappush(big, nums[i])
balance -= 1
# 2-3、平衡大小堆
if balance > 0:
heappush(big, -heappop(small))
if balance < 0:
heappush(small, -heappop(big))
# 2-4、清理堆顶
while small and d[-small[0]] > 0:
d[-heappop(small)] -= 1
while big and d[big[0]] > 0:
d[heappop(big)] -= 1
# 2-5、计算中位数
ans.append(get(k))
return ans
class DualHeap:
def __init__(self, k: int):
self.small = list() # 大根堆,维护较小的一半元素
self.large = list() # 小根堆,维护较大的一半元素
# 哈希表,记录「延迟删除」的元素,key 为元素,value 为需要删除的次数
self.delayed = Counter()
self.k = k
# small 和 large 当前包含的元素个数,需要扣除被「延迟删除」的元素
self.smallSize = 0
self.largeSize = 0
# 清理堆顶延迟删除的元素
def prune(self, heap: List[int]):
while heap:
x = heap[0]
if heap is self.small: x = -x
if x in self.delayed:
self.delayed[x] -= 1
if self.delayed[x] == 0:
self.delayed.pop(x)
heappop(heap)
else: break
# 调整 small 和 large 中的元素个数,使得二者的元素个数满足要求
def makeBalance(self):
if self.smallSize > self.largeSize + 1: # small 比 large 元素多 2 个
heappush(self.large, -heappop(self.small))
self.smallSize -= 1
self.largeSize += 1
self.prune(self.small) # 清理
elif self.smallSize < self.largeSize:
# large 比 small 元素多 1 个
heappush(self.small, -heappop(self.large))
self.smallSize += 1
self.largeSize -= 1
self.prune(self.large) # 清理
def insert(self, num: int):
if not self.small or num <= -self.small[0]:
heappush(self.small, -num)
self.smallSize += 1
else:
heappush(self.large, num)
self.largeSize += 1
self.makeBalance()
def erase(self, num: int):
self.delayed[num] += 1
if num <= -self.small[0]:
self.smallSize -= 1
if num == -self.small[0]:
self.prune(self.small)
else:
self.largeSize -= 1
if num == self.large[0]:
self.prune(self.large)
self.makeBalance()
def getMedian(self) -> float:
return float(-self.small[0]) if self.k % 2 else (-self.small[0] + self.large[0]) / 2
假设 需要 x 次操作,每次操作一定有 min 参与,最终每一个数都变成了 min + x。
s u m + x ∗ ( n − 1 ) = ( m i n + x ) ∗ n sum + x * (n - 1) = (min + x)* n sum+x∗(n−1)=(min+x)∗n
即: x = s u m − m i n ∗ n x = sum - min * n x=sum−min∗n
也就是 n - 1 个元素增加 1,相对于使 1 个元素减少 1。即将所有元素减少到最小值所需的操作数。
在实现中,为避免溢出,逐个累加每个元素与数组中元素最小值的差。
class Solution:
def minMoves(self, nums: List[int]) -> int:
min_ = min(nums)
return sum(x - min_ for x in nums)
# return sum(nums) - min(nums)*len(nums)
中位数性质:数组中所有数与中位数的绝对差之和最小。
class Solution:
def minMoves2(self, nums: List[int]) -> int:
nums.sort()
half = len(nums) // 2
median = (nums[half] + nums[~half])//2
return sum(abs(median - x) for x in nums)