堆有序:当一棵二叉树的每一个结点都大于等于它的两个子结点时,它被称为堆有序。数组
**二叉堆:**二叉堆是一组可以用堆有序的彻底二叉树排序的元素,并在数组中按照层级储存(不使用数组的第一个位置)。缓存
咱们的下标从1
开始,下标变量为ind
bash
那对于给定位置ind
的节点:数据结构
2*ind
2*ind+ 1
parseInt(ind /2)
parseInt(3/2) === parseInt(2/2) === 1
less
咱们使用堆这个数据结构主要有三个操做函数
pusk(val)
:向堆中插入一个新的值pop(val)
:弹出最值top()
:查看最值咱们先假设咱们堆里面已是堆有序的,且含有的元素为2,3,4
测试
这个时候,若是咱们往里面添加1
ui
class MinHeap {
constructor() {
this.heap = []
this.len = 0
}
push(val) {
this.heap[++this.len] = val
}
}
复制代码
this.heap[++this.len]
先进行this.len
加加后再赋值,若是此时this.len
为0的话,那么实际是this.heap[1] = val
this这种写法就达到了咱们数组首位为空的目的spa
为了达到堆有序,咱们应该对添加的元素进行调整,由于每次咱们都是在末尾添加元素的,那咱们把这个调整的过程称为上浮swim
push(val) {
this.heap[++this.len] = val
this.swim(this.len)
}
复制代码
那咱们如今来思考swim
的实现,咱们先却次明确堆有序的概念
堆有序:当一棵二叉树的每一个结点都大于等于它的两个子结点时,它被称为堆有序。
若是咱们是要实现一个最小堆,那它的父节点必定是比子节点大的,为了方便咱们使用一个函数来表示比较
more(i, j) {
return this.heap[i] > this.heap[j]
}
复制代码
若是此时节点为ind
那么父节点的下标就是parseInt(ind/2)
咱们是想创建最小堆,因此小的值应该在更上头
若是父节点比该节点还大
那就应该交换二者的位置
而后咱们不断重复该过程
直到父节点小于子节点
即达到了堆有序
那须要的条件就是
while ( this.more(parseInt(ind / 2), ind))
复制代码
为 了避免当parseInt(ind / 2) === 0
的时候,会对不存在的this.heap[0]
进行操做
咱们须要确保ind > 1
因此循环的添加应该是
while (ind > 1 && this.more(parseInt(ind / 2), ind))
复制代码
swim(ind) {
while (ind > 1 && this.more(parseInt(ind / 2), ind)) {
this.swap(parseInt(ind / 2), ind)
ind = parseInt(ind / 2)
}
}
复制代码
交换元素swap
的函数实现
swap(i, j) {
let temp = this.heap[i]
this.heap[i] = this.heap[j]
this.heap[j] = temp
}
复制代码
每次弹出的都是最值,即根节点,若是是最小堆就是最小值,最大堆就是最大值
根据咱们上面的讲述及图,咱们很容易知道最值就是
pop() {
const top = this.heap[1]
return top
}
复制代码
可是若是把根元素直接删除的话,整个堆就毁了
因此咱们思考思考着使用内部的某一个元素先顶替根节点的位置
这个元素显而易见的是最后一个元素
由于最后一个元素的移动不会使得树的结构改变
pop() {
const top = this.heap[1]
this.swap(1,this.len)
return top
}
复制代码
这里就会又遇到上面插入元素时遇到的问题,此时的堆多是无序的
很明显,咱们是不须要缓存本来的根节点的
this.swap(1,this.len--)
复制代码
这表示,咱们在交换完后,就对堆的长度减一
可是实际上咱们的数组里仍是对该元素有引用的,由于这里咱们只是让咱们所谓的堆的长度删减,为了防止内存泄漏,咱们须要让数组取消对该节点的引用
在真实项目中,咱们存储都是一个对象里的
key
,因此咱们须要解除对对象的引用,使其内存回收
this.heap[len + 1] = undefined
复制代码
如今代码就有
pop() {
const ret = this.heap[1]
this.swap(1, this.len--)
this.heap[this.len + 1] = undefined
return ret
}
复制代码
虽然如今终于去掉了这个不要的节点了,可是咱们堆的有序性仍是没有解决
本来这个末尾节点就是在下层的,因此此时应该也是慢慢的回到下层,咱们就把这个下沉操做称为sink
一样这个操做应该也是不断循环的直至ind
所指的节点下面再无元素
若是该节点子节点,下标可能就是2*ind
和2*ind + 1
,
因此当2*ind
还在堆的长度范围内,就说明还要和子节点进行大小比较
sink(ind) {
while (2 * ind <= this.len) {
// ...
}
}
复制代码
固然咱们不能忽略了2*ind + 1
的存在
咱们是最值堆,指望的固然是把子节点中更小的往上放
因此若是2*ind
比2*ind + 1
还大的话,咱们应该让j++
,而后就会指向2*ind + 1
,即更小的值
let j = 2 *ind
if (this.more(j, j + 1)) j++
复制代码
这里须要考虑j
此时可能就等于 this.len
,那么根本就不存在j+1
的元素了
因此咱们须要让j < this.len
,那么这样就说明必定有j+1
存在
sink(ind) {
while (2 * ind <= this.len) {
let j = 2 * ind
if (j < this.len && this.more(j, j + 1)) j++
// 此时j表示的就是子节点最小的那个了
}
}
复制代码
上面这么多只是确认与ind
要判断的节点
如今咱们能够开始进行判断了
若是ind
比j
小的话,咱们就break
,中止向下循环了,由于此时ind
的位置就是正确的
不然,咱们就交换二者的位置
而后再把ind
改成交换后的位置,即j
,再进行下次循环
sink(ind) {
while (2 * ind <= this.len) {
let j = 2 * ind
if (j < this.len && this.more(j, j + 1)) j++
if (!this.more(ind, j)) break
this.swap(ind, j)
ind = j
}
}
复制代码
当咱们把sink
方法实现完后, 咱们就能够完成弹出的所有操做了
pop() {
const top = this.heap[1]
this.swap(1, this.len--)
this.heap[this.len + 1] = undefined
this.sink(1)
return top
}
复制代码
top
查看最值size
查看堆长度isEmpty
查看是否为空关于堆,咱们还要须要提供一个API,top
让使用者知道当前的最值是多少
top(){
return this.heap[1]
}
复制代码
size() {
return this.len
}
复制代码
isEmpty() {
return this.len === 0
}
复制代码
class MinHeap {
constructor() {
this.heap = []
this.len = 0
}
push(val) {
this.heap[++this.len] = val
this.swim(this.len)
}
pop() {
const top = this.heap[1]
this.swap(1, this.len--)
this.heap[this.len + 1] = undefined
this.sink(1)
return top
}
top() {
return this.heap[1]
}
size() {
return this.len
}
isEmpty() {
return this.len === 0
}
swim(ind) {
while (ind > 1 && this.more(parseInt(ind / 2), ind)) {
this.swap(parseInt(ind / 2), ind)
ind = parseInt(ind / 2)
}
}
sink(ind) {
while (2 * ind <= this.len) {
let j = 2 * ind
if (j < this.len && this.more(j, j + 1)) j++
if (!this.more(ind, j)) break
this.swap(ind, j)
ind = j
}
}
more(i, j) {
return this.heap[i] > this.heap[j]
}
swap(i, j) {
let temp = this.heap[i]
this.heap[i] = this.heap[j]
this.heap[j] = temp
}
}
复制代码
对于this.heap
和this.len
属性,咱们显然是不想暴露的,可是js中没有私有属性,咱们就用__
来表示私有属性
改成this_heap
和this._len
咱们拿LeetCode 215题测试
var findKthLargest = function (nums, k) {
let minHeap = new MinHeap()
for (let i = 0; i < nums.length; i++) {
if (minHeap.size() < k) {
minHeap.push(nums[i])
} else if (minHeap.top() < nums[i]) {
minHeap.pop()
minHeap.push(nums[i])
}
}
return minHeap.top()
};
复制代码
经过了👌
最大堆与最小堆的区别就是咱们在下沉或者上浮时,是让小的仍是让大的上浮或者下沉
在代码中咱们都是经过
sink(ind) {
while (2 * ind <= this.len) {
let j = 2 * ind
if (j < this.len && this.more(j, j + 1)) j++
if (!this.more(ind, j)) break
this.swap(ind, j)
ind = j
}
}
复制代码
咱们注意看第5行代码if (!this.more(ind, j)) break
说明若是this.more(ind, j)
为真, 就会执行后面的交换函数
ind
指的当下元素j
指的是ind*2
或者ind*2 + 1
,即ind
的子节点若是ind
比j
大,就交换,因此就是把大的往下沉,最后这个堆就是一个最小堆了
more(i, j) {
return this.heap[i] > this.heap[j]
}
复制代码
若是把>
改为<
就是反面,即此时的最小堆变成了最大堆
那咱们思考着能不能在建立的时候,经过传入参数来肯定是最小堆仍是最大堆呢
class Heap {
constructor(maxOfMin = 0) {
this.heap = []
this.len = 0
this.maxOfMin = parseInt(maxOfMin)
}
more(i, j) {
let ret = this.heap[i] > this.heap[j]
return this.maxOfMin === 0 ? ret : !ret
}
}
复制代码
如今默认是0
,就是最小堆,若是是别的就是最大堆了
const maxHeap = new Heap(1)
const minHeap = new Heap()
let arr = [11, 2, 33, 4, 55, 6]
for (let i = 0; i < arr.length; i++) {
maxHeap.push(arr[i])
minHeap.push(arr[i])
}
console.log('最大堆');
console.log(maxHeap.pop());
console.log(maxHeap.pop());
console.log(maxHeap.pop());
console.log(maxHeap.pop());
console.log(maxHeap.pop());
console.log(maxHeap.pop());
console.log('最小堆');
console.log(minHeap.pop());
console.log(minHeap.pop());
console.log(minHeap.pop());
console.log(minHeap.pop());
console.log(minHeap.pop());
console.log(minHeap.pop());
复制代码
最大堆
55
33
11
6
4
2
最小堆
2
4
6
11
33
55
复制代码
忽然发现一个特别有趣的点,上面的输出都是有序的了,咱们能够利用这一特性来对数组进行排序
function heapSort(arr) {
let len = arr.length
for (let i = parseInt(len / 2); i >= 1; i--) {
sink(arr, i, len)
}
while (len > 1) {
swap(arr, 1, len--)
sink(arr, 1, len)
}
return arr
}
function sink(arr, ind, len) {
while (2 * ind <= len) {
let j = 2 * ind
if (j < len && more(arr, j, j + 1)) j++
if (!more(arr, ind, j)) break
swap(arr, ind, j)
ind = j
}
}
function swap(arr, i, j) {
i--; j--;
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
function more(arr, i, j) {
i--; j--;
return arr[i] < arr[j]
}
let arr = [11, 2, 33, 4, 55]
let ret = heapSort(arr)
console.log(ret);
复制代码
咱们来分析上面的代码
function heapSort(arr) {
let len = arr.length
// 构造最大堆
for (let i = parseInt(len / 2); i >= 1; i--) {
sink(arr, i, len)
}
console.log('<<<===arr==>>>');
console.log(arr);
console.log('==>>>><<<==');
//...
}
复制代码
打印出来的结果是
[ 55, 11, 33, 4, 2 ]
复制代码
在实际待排序的数组是从
arr[0 ~ (len - 1)]
,而不是咱们图上的arr[1 ~ len]
因此,咱们在后面有个小技巧能够弥补,使得咱们仍是伪装是在
1~len
间操做
那上面的代码是怎么实现使得数组转换为最大堆的呢
上图是原始数组
咱们从最后一个节点的父子节点开始,往上遍历
每遍历到该节点时,执行sink
操做,使其下沉到属于他的位置
这样咱们就能够确保每次遍历某一节点时,他的子孙节点最大的就是子节点
最后一个节点的下标就是len
,那他的父节点的下标就是parseInt(len / 2)
而后咱们就不断i++
直到把把根节点也执行后就结束
根节点的位置就是i === 1
,那当i < 1
时,就无节点能够遍历了,故循环为
for (let i = parseInt(len / 2); i >= 1; i--) {
sink(arr, i, len)
}
复制代码
这里的sink
函数和咱们上面的代码实现是一致的,只不过,函数是独立的,咱们须要把咱们的数组,下标,及长度传入
这里的长度须要传入,是由于后续操做中咱们会对
len
进行修改,因此不能在函数里直接经过获取arr.length
实现
function sink(arr, ind, len) {
while (2 * ind <= len) {
let j = 2 * ind
if (j < len && less(arr, j, j + 1)) j++
if (!less(arr, ind, j)) break
swap(arr, ind, j)
ind = j
}
}
复制代码
这里的比较函数是less
,表示arr[ind]
小于arr[j]
时返回true
,因此上面的逻辑是使得更小的元素arr[ind]
下沉,即咱们实现的是最大堆
除了less
还有一个swap
辅助函数,这两个函数的实现要具体讲下,由于和上面的堆结构稍微有点不同
function swap(arr, i, j) {
i--; j--;
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
function less(arr, i, j) {
i--; j--;
return arr[i] < arr[j]
}
复制代码
不同在,在执行操做时,咱们对传入的参数都进行了减减操做
何故呢?
咱们在上面的操做时,创建的基础都是把数组的下标从1
开始的,因此咱们在涉及到真正的数组操做时,下标是从0
开始的
就像一个逻辑地址和物理地址的区别,只不过这个转换特别简单的,就是把地址减1
while (len > 1) {
swap(arr, 1, len--)
sink(arr, 1, len)
}
复制代码
如今顶点就是最大值了,如今咱们就把首元素和数组的最尾交换
swap(arr, 1, len--)
复制代码
这里的len--
,说明咱们首位和尾位交换后,就把堆的长度减减,可是实际上数组是没有改变的,这也是前面使用sink(arr,ind,len)
这里要传长度而不是在函数里经过arr.length
获取的缘由.
此时的数组仍是
交换后的位置,堆就不是有序的了,因此咱们须要把首位下沉
while (len > 1) {
swap(arr, 1, len--)
sink(arr, 1, len)
}
复制代码
而后咱们不断上面的操做
最后咱们就会把最大的数都不断累积在后面
此时堆的长度为1
虽然此时的堆的长度为len === 1
可是这个数组已经就有序了