295. 数据流的中位数 🟥
题目
leetcode 困难
题目
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4] 的中位数是 3 。
例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder 类:
- MedianFinder() 初始化 MedianFinder 对象。
- void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
- double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。
题解
方法:二分查找
- 时间复杂度:
- addNum: 二分查找是 O(logN),但是数组 splice 插入的最坏时间复杂度是 O(N)。因此单次插入是 O(N)。
- findMedian: O(1),直接通过索引取值。
- 空间复杂度:O(N)
js
class MedianFinder {
constructor() {
this.arr = []
}
addNum(num) {
// 如果数组为空,或者 num 比数组最后一个元素还大,直接 push
if (this.arr.length === 0 || num >= this.arr[this.arr.length - 1]) {
this.arr.push(num)
console.log('📌 直接 push', this.arr)
return
}
// 二分查找第一个大于 num 的位置
let left = 0,
right = this.arr.length - 1
while (left <= right) {
let mid = left + ((right - left) >> 1)
console.log(
`left: ${left}: ${this.arr[left]}, right: ${right}: ${this.arr[right]}, mid: ${mid}: ${this.arr[mid]}`,
)
if (this.arr[mid] === num) {
left = mid
break
} else if (this.arr[mid] < num) {
left = mid + 1
} else {
right = mid - 1
}
}
// 将 num 插入到保持有序的位置
this.arr.splice(left, 0, num)
console.log('📌 插入后', this.arr)
}
findMedian() {
const len = this.arr.length
const mid = len >> 1 // 相当于 Math.floor(len / 2)
// 如果是奇数,直接返回中间的元素
if (len % 2 !== 0) {
return this.arr[mid]
}
// 如果是偶数,返回中间两个元素的平均值
return (this.arr[mid - 1] + this.arr[mid]) / 2
}
}
const medianFinder = new MedianFinder()
medianFinder.addNum(1)
medianFinder.addNum(2)
console.log('🌰', medianFinder.findMedian()) // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3)
console.log('🌰', medianFinder.findMedian()) // 返回 2.0
medianFinder.addNum(4)
medianFinder.addNum(1)
console.log('🌰', medianFinder.findMedian()) // 返回 2.0
执行结果
📌 直接 push [ 1 ]
📌 直接 push [ 1, 2 ]
🌰 1.5
📌 直接 push [ 1, 2, 3 ]
🌰 2
📌 直接 push [ 1, 2, 3, 4 ]
left: 0: 1, right: 3: 4, mid: 1: 2
left: 0: 1, right: 0: 1, mid: 0: 1
📌 插入后 [ 1, 1, 2, 3, 4 ]
🌰 2
方法:两个堆
js
// 1. 手写一个基础的堆(支持传入比较函数决定是最大堆还是最小堆)
class Heap {
constructor(compare) {
this.data = []
// 如果 compare(a, b) < 0,说明 a 的优先级高,排在堆顶
this.compare = compare
}
size() {
return this.data.length
}
peek() {
return this.size() === 0 ? null : this.data[0]
}
push(val) {
this.data.push(val)
this.bubbleUp(this.size() - 1)
}
pop() {
if (this.size() === 0) return null
if (this.size() === 1) return this.data.pop()
const top = this.data[0]
this.data[0] = this.data.pop()
this.bubbleDown(0)
return top
}
bubbleUp(index) {
while (index > 0) {
const parent = (index - 1) >> 1 // 相当于 Math.floor((index - 1) / 2)
if (this.compare(this.data[index], this.data[parent]) < 0) {
this.swap(index, parent)
index = parent
} else {
break
}
}
}
bubbleDown(index) {
const lastIndex = this.size() - 1
while (true) {
const left = index * 2 + 1
const right = index * 2 + 2
let find = index
if (
left <= lastIndex &&
this.compare(this.data[left], this.data[find]) < 0
) {
find = left
}
if (
right <= lastIndex &&
this.compare(this.data[right], this.data[find]) < 0
) {
find = right
}
if (find !== index) {
this.swap(index, find)
index = find
} else {
break
}
}
}
swap(i, j) {
;[this.data[i], this.data[j]] = [this.data[j], this.data[i]]
}
}
// 2. 主逻辑:利用双堆实现数据流中位数
class MedianFinder {
constructor() {
// 最大堆,保存较小的一半 (b - a < 0 意味着 b > a 时优先,即大顶堆)
this.maxHeap = new Heap((a, b) => b - a)
// 最小堆,保存较大的一半 (a - b < 0 意味着 a < b 时优先,即小顶堆)
this.minHeap = new Heap((a, b) => a - b)
}
addNum(num) {
// 先推入最大堆
this.maxHeap.push(num)
// 把最大堆的最大值给最小堆,保证最大堆的元素都小于最小堆
this.minHeap.push(this.maxHeap.pop())
// 平衡两个堆的大小(保证 maxHeap 个数 >= minHeap 个数)
if (this.maxHeap.size() < this.minHeap.size()) {
this.maxHeap.push(this.minHeap.pop())
}
console.log('📌 ⬇️', this.maxHeap.data)
console.log('📌 ⬆️', this.minHeap.data)
}
findMedian() {
if (this.maxHeap.size() > this.minHeap.size()) {
return this.maxHeap.peek() // 奇数个时,直接返回最大堆堆顶
} else {
return (this.maxHeap.peek() + this.minHeap.peek()) / 2 // 偶数个时,取两个堆顶平均值
}
}
}
const medianFinder = new MedianFinder()
medianFinder.addNum(1)
medianFinder.addNum(2)
console.log('🌰', medianFinder.findMedian()) // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3)
console.log('🌰', medianFinder.findMedian()) // 返回 2.0
medianFinder.addNum(4)
medianFinder.addNum(1)
console.log('🌰', medianFinder.findMedian()) // 返回 2.0
执行结果
📌 ⬇️ [ 1 ]
📌 ⬆️ []
📌 ⬇️ [ 1 ]
📌 ⬆️ [ 2 ]
🌰 1.5
📌 ⬇️ [ 2, 1 ]
📌 ⬆️ [ 3 ]
🌰 2
📌 ⬇️ [ 2, 1 ]
📌 ⬆️ [ 3, 4 ]
📌 ⬇️ [ 2, 1, 1 ]
📌 ⬆️ [ 3, 4 ]
🌰 2