堆
概念
- 堆通常是⼀个可以被看做⼀棵树的数组对象。
- 堆的实现通过构造⼆叉堆,实为⼆叉树的⼀种。这种数据结构具有以下性质。
- 任意节点⼩于(或⼤于)它的所有⼦节点 堆总是⼀棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层从左到右填⼊。
- 将根节点最⼤的堆叫做最⼤堆或⼤根堆,根节点最⼩的堆叫做最⼩堆或⼩根堆。
- 优先队列也完全可以⽤堆来实现,操作是⼀模⼀样的。
实现⼤根堆
- 堆的每个节点的左边⼦节点索引是 i * 2 + 1 ,右边是 i * 2 + 2 ,⽗节点是 (i - 1) /2 。
- 堆有两个核⼼的操作,分别是 shiftUp 和 shiftDown 。前者⽤于添加元素,后者⽤于删除根节点。
- shiftUp 的核⼼思路是⼀路将节点与⽗节点对⽐⼤⼩,如果⽐⽗节点⼤,就和⽗节点交换位置。
- shiftDown 的核⼼思路是先将根节点和末尾交换位置,然后移除末尾元素。接下来循环判断⽗节点和两个⼦节点的⼤⼩,如果⼦节点⼤,就把最⼤的⼦节点和⽗节点交换
class MaxHeap {
constructor() {
this.heap = []
}
size() {
return this.heap.length
}
empty() {
return this.size() == 0
}
add(item) {
this.heap.push(item)
this._shiftUp(this.size() - 1)
}
removeMax() {
this._shiftDown(0)
}
getParentIndex(k) {
return parseInt((k - 1) / 2)
}
getLeftIndex(k) {
return k * 2 + 1
}
_shiftUp(k) {
// 如果当前节点⽐⽗节点⼤,就交换
while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
this._swap(k, this.getParentIndex(k))
// 将索引变成⽗节点
k = this.getParentIndex(k)
}
}
_shiftDown(k) {
// 交换⾸位并删除末尾
this._swap(k, this.size() - 1)
this.heap.splice(this.size() - 1, 1)
// 判断节点是否有左孩⼦,因为⼆叉堆的特性,有右必有左
while (this.getLeftIndex(k) < this.size()) {
let j = this.getLeftIndex(k)
// 判断是否有右孩⼦,并且右孩⼦是否⼤于左孩⼦
if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
// 判断⽗节点是否已经⽐⼦节点都⼤
if (this.heap[k] >= this.heap[j]) break
this._swap(k, j)
k = j
}
}
_swap(left, right) {
let rightValue = this.heap[right]
this.heap[right] = this.heap[left]
this.heap[left] = rightValue
}
}
时间复杂度
- 通常使⽤最差的时间复杂度来衡量⼀个算法的好坏。
- 常数时间 O(1) 代表这个操作和数据量没关系,是⼀个固定时间的操作,⽐如说四则运算。
- 对于⼀个算法来说,可能会计算出如下操作次数 aN + 1, N 代表数据量。那么该算法的时间复杂度就是 O(N) 。因为我们在计算时间复杂度的时候,数据量通常是⾮常⼤的,这时候低阶项和常数项可以忽略不计。
- 当然可能会出现两个算法都是 O(N) 的时间复杂度,那么对⽐两个算法的好坏就要通过对⽐低阶项和常数项了
位运算
- 位运算在算法中很有⽤,速度可以⽐四则运算快很多。
- 在学习位运算之前应该知道⼗进制如何转⼆进制,⼆进制如何转⼗进制。这⾥说明下简单的计算⽅式
- ⼗进制 33 可以看成是 32 + 1 ,并且 33 应该是六位⼆进制的(因为 33 近似32 ,⽽ 32 是 2 的五次⽅,所以是六位),那么 ⼗进制 33 就是 100001 ,只要是 2 的次⽅,那么就是 1 否则都为 0 那么⼆进制 100001 同理,⾸位是 2^5 ,末位是 2^0 ,相加得出 33
左移 <<
10 << 1 // -> 20
- 左移就是将⼆进制全部往左移动, 10 在⼆进制中表示为 1010 ,左移⼀位后变成 10100 ,转换为⼗进制也就是 20 ,所以基本可以把左移看成以下公式 a * (2 ^ b)
算数右移 >>
10 >> 1 // -> 5
- 算数右移就是将⼆进制全部往右移动并去除多余的右边,10 在⼆进制中表示为 1010 ,右移⼀位后变成 101 ,转换为⼗进制也就是 5 ,所以基本可以把右移看成以下公式int v = a / (2 ^ b)
- 右移很好⽤,⽐如可以⽤在⼆分算法中取中间值
13 >> 1 // -> 6
按位操作
按位与
- 每⼀位都为 1,结果才为 1
8 & 7 // -> 0
// 1000 & 0111 -> 0000 -> 0
按位或
- 其中⼀位为 1,结果就是 1
8 | 7 // -> 15
// 1000 | 0111 -> 1111 -> 15
按位异或
每⼀位都不同,结果才为 1
8 ^ 7 // -> 15
8 ^ 8 // -> 0
// 1000 ^ 0111 -> 1111 -> 15
// 1000 ^ 1000 -> 0000 -> 0
⾯试题:两个数不使⽤四则运算得出和
- 这道题中可以按位异或,因为按位异或就是不进位加法, 8 ^ 8 = 0 如果进位了,就是 16 了,所以我们只需要将两个数进⾏异或操作,然后进位。那么也就是说两个⼆进制都是 1 的位置,左边应该有⼀个进位 1 ,所以可以得出以下公式 a + b = (a ^ b) + ((a & b) << 1) ,然后通过迭代的⽅式模拟加法
function sum(a, b) {
if (a == 0) return b
if (b == 0) return a
let newA = a ^ b
let newB = (a & b) << 1
return sum(newA, newB)
}