注释(堆排序解析)
堆是一个完全二叉树。
完全二叉树: 二叉树除开最后一层,其他层结点数都达到最大,最后一层的所有结点都集中在左边(左边结点排列满的情况下,右边才能缺失结点)。
大顶堆:根结点为最大值,每个结点的值大于或等于其孩子结点的值。
小顶堆:根结点为最小值,每个结点的值小于或等于其孩子结点的值。
堆的存储: 堆由数组来实现,相当于对二叉树做层序遍历。
①如果想要降序序列,那么就对给定数组形成大顶堆,然后将大顶堆的根节点放在数组的最后一位,将最后一位前的继续形成大顶堆,最后形成降序序列
②升序序列反过来即可
最小的k个数
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
解题思路1
利用数组内置函数排序
var getLeastNumbers = function(arr, k) {
return arr.sort((a,b) => a-b).slice(0,k)
};
其实就是快速排序,时间复杂度是O(NlogN),空间复杂度是O(logN)
解题思路2
利用最大堆,得到排序数组
function swap(A, i, j) {
let temp = A[i];
A[i] = A[j];
A[j] = temp;
}
function shiftDown(A, i, length) {
let temp = A[i]; // 当前父节点
// j<length 的目的是对结点 i 以下的结点全部做顺序调整
for(let j = 2*i+1; j<length; j = 2*j+1) {
temp = A[i]; // 将 A[i] 取出,整个过程相当于找到 A[i] 应处于的位置
if(j+1 < length && A[j] < A[j+1]) {
j++; // 找到两个孩子中较大的一个,再与父节点比较
}
if(temp < A[j]) {
swap(A, i, j) // 如果父节点小于子节点:交换;否则跳出
i = j; // 交换后,temp 的下标变为 j
} else {
break;
}
}
}
function heapSort(A) {
// 初始化大顶堆,从第一个非叶子结点开始
for(let i = Math.floor(A.length/2-1); i>=0; i--) {
shiftDown(A, i, A.length);
}
// 排序,每一次for循环找出一个当前最大值,数组长度减一
for(let i = Math.floor(A.length-1); i>0; i--) {
swap(A, 0, i); // 根节点与最后一个节点交换
shiftDown(A, 0, i); // 从根节点开始调整,并且最后一个结点已经为当
// 前最大值,不需要再参与比较,所以第三个参数
// 为 i,即比较到最后一个结点前一个即可
}
}
var getLeastNumbers = function(arr, k) {
heapSort(arr)
return arr.slice(0,k)
};
shiftDown的复杂度为 O(logn),而外层循环共有 f(n) 次,所以最终的复杂度为 O(nlogn)
数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
示例 1:
输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
示例 2:
输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]
解题思路1
暴力法
每次取出中位数的时候,都先将所有元素进行排序,然后再计算中位数。
var MedianFinder = function() {
this.data = [];
};
MedianFinder.prototype.addNum = function(num) {
this.data.push(num);
};
MedianFinder.prototype.findMedian = function() {
const length = this.data.length;
if (!length) {
return null;
}
this.data.sort((a, b) => a - b);
const mid = Math.floor((length - 1) / 2);
if (length % 2) {
return this.data[mid];
}
return (this.data[mid] + this.data[mid + 1]) / 2;
};
解题思路2
二分查找
其实不需要每次添加元素的时候,都对全部元素重新排序。如果之前一直保证元素是有序的,那么添加新元素的时候,只需要将元素插入到正确位置即可,查找正确位置可以通过「二分搜索」来完成。
为了保证之前的元素有序,针对每个新添加的元素都将其放入正确位置。
var MedianFinder = function() {
this.data = [];
};
MedianFinder.prototype.addNum = function(num) {
if (!this.data.length) {
this.data.push(num);
return;
}
let left = 0,
right = this.data.length - 1;
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (this.data[mid] === num) {
this.data.splice(mid, 0, num);
return;
} else if (this.data[mid] < num) {
left = mid + 1;
} else {
right = mid - 1;
}
}
this.data.splice(right + 1, 0, num);
};
MedianFinder.prototype.findMedian = function() {
const length = this.data.length;
if (!length) {
return null;
}
const mid = Math.floor((length - 1) / 2);
if (length % 2) {
return this.data[mid];
}
return (this.data[mid] + this.data[mid + 1]) / 2;
};
二分查找需要O(logN)的复杂度,移动元素需要O(N)复杂度,所以时间复杂度是O(N)。
解题思路3
大小堆(桥接模式)
利用最大堆或最小堆的思想,时间复杂库过高,AC不了。
对于这种动态数据,堆是极好的解决方案。准备两个堆:
最大堆:存放数据流中较小的一半元素
最小堆:存放数据流中较大的一半元素
需要保证这 2 个堆的“平衡”。这里的平衡指得是:最大堆的大小 = 最小堆的大小, 或者 最大堆的大小 = 最小堆的大小 + 1。
当调用 findMedian 查询中位数的时候,中位数就是最大堆的堆顶元素,或者 (最大堆的堆顶元素 + 最小堆的堆顶元素)/2
剩下的问题就是怎么保证堆的平衡?步骤如下
先让 num 入 maxHeap
取出 maxHeap 的堆顶元素,放入 minHeap
若此时最大堆的大小 < 最小堆的大小,取出 minHeap 的堆顶元素,让入 maxHeap
const defaultCmp = (x, y) => x > y; // 默认是最大堆
const swap = (arr, i, j) => ([arr[i], arr[j]] = [arr[j], arr[i]]);
class Heap {
/**
* 默认是最大堆
* @param {Function} cmp
*/
constructor(cmp = defaultCmp) {
this.container = [];
this.cmp = cmp;
}
insert(data) {
//相当于为下面的container和cmp绑定this,就不用写this.container之类的了
const { container, cmp } = this;
container.push(data);
let index = container.length - 1;
while (index) {
let parent = Math.floor((index - 1) / 2);
if (!cmp(container[index], container[parent])) {
return;
}
swap(container, index, parent);
index = parent;
}
}
extract() {
const { container, cmp } = this;
if (!container.length) {
return null;
}
swap(container, 0, container.length - 1);
const res = container.pop();
const length = container.length;
let index = 0,
exchange = index * 2 + 1;
while (exchange < length) {
// // 以最大堆的情况来说:如果有右节点,并且右节点的值大于左节点的值
let right = index * 2 + 2;
if (right < length && cmp(container[right], container[exchange])) {
exchange = right;
}
if (!cmp(container[exchange], container[index])) {
break;
}
swap(container, exchange, index);
index = exchange;
exchange = index * 2 + 1;
}
return res;
}
top() {
if (this.container.length) return this.container[0];
return null;
}
}
var MedianFinder = function() {
this.maxHeap = new Heap();
this.minHeap = new Heap((x, y) => x < y);
};
MedianFinder.prototype.addNum = function(num) {
this.maxHeap.insert(num);
this.minHeap.insert(this.maxHeap.top());
this.maxHeap.extract();
if (this.maxHeap.container.length < this.minHeap.container.length) {
this.maxHeap.insert(this.minHeap.top());
this.minHeap.extract();
}
};
MedianFinder.prototype.findMedian = function() {
return this.maxHeap.container.length > this.minHeap.container.length
? this.maxHeap.top()
: (this.maxHeap.top() + this.minHeap.top()) / 2;
};