第一节:
- 树是不包含回路的联通无向图
- 基础
第二节:
- 二叉树
第三节:堆
建立堆
建立堆,书上讲了两种方法
这里用第二种方法(毕竟方便)
const h = [0, 99, 5, 36, 2, 19, 1, 46, 12, 7, 22, 25, 28, 17, 92]
const n = h.length - 1;
const siftdown = (i) => {
let t, flag = 0;
while (i * 2 <= n && flag === 0) {
if (h[i] > h[i * 2]) {
t = i * 2;
} else {
t = i;
}
// 如果有右孩子,在判断右孩子的情况
if (i * 2 + 1 <= n) {
if (h[t] > h[i * 2 + 1]) {
// 更新较小的结点编号
t = i * 2 + 1;
}
}
// 如果发现最小的节点不是自己,那么交换
if (t != i) {
swap(t, i);
i = t;
} else {
flag = 1;
}
}
}
const siftup = (i) => {
let flag = 0;
if (i == 1) return; // 已经到堆顶了
while (i != 1 && flag == 0) {
// 判断是否比父节点的小
let fa = parseInt(i / 2);
if (h[i] < h[fa]) {
swap(i, fa)
} else {
flag = 1;
}
i = parseInt(i / 2);
}
}
const swap = (i, j) => {
[h[j], h[i]] = [h[i], h[j]];
}
// 建立堆
for (let i = parseInt(n / 2); i >= 1; i--) {
siftdown(i);
}
console.log(h);
堆排序–小根堆
const h = [0, 99, 5, 36, 2, 19, 1, 46, 12, 7, 22, 25, 28, 17, 92]
let n = h.length - 1;
// 下沉操作
const siftdown = (i) => {
let t, flag = 0;
while (i * 2 <= n && flag === 0) {
// flag 控制是否需要继续下沉
if (h[i] > h[i * 2]) {
// 和左孩子比较
t = i * 2;
} else {
t = i;
}
// 如果有右孩子,在判断右孩子的情况
if (i * 2 + 1 <= n) {
if (h[t] > h[i * 2 + 1]) {
// 更新较小的结点编号
t = i * 2 + 1;
}
}
// 如果发现最小的节点不是自己,那么交换
if (t != i) {
swap(t, i);
i = t;
} else {
// 如果最小的是自己,那么就不需要下沉了。
flag = 1;
}
}
}
// 上浮
const siftup = (i) => {
let flag = 0;
if (i === 1) return; // 已经到堆顶了
while (i !== 1 && flag === 0) {
// 判断是否比父节点的小
let fa = parseInt(i / 2);
if (h[i] < h[fa]) {
swap(i, fa)
} else {
flag = 1;
}
i = fa;
}
}
const swap = (i, j) => {
[h[j], h[i]] = [h[i], h[j]];
}
/*
建立堆
思想就是,判断以当前节点为根节点的树是否满足最小根的规则
那么从哪里开始呢:叶子节点是不需要考虑的,倒数,从第一个非叶子节点开始
从后向前排查即可。
*/
for (let i = parseInt(n / 2); i >= 1; i--) {
siftdown(i);
}
console.log('小根堆建立完成:>>', h);
// 堆排序:从小到大排序
const deleteMax = () => {
let t = h[1]; // 把堆顶保存起来
h[1] = h[n]; // 把最后一个点赋值到堆顶
n--; // 缩小堆的大小
siftdown(1); // 下浮根节点,维护小根堆的规则。
return t;
}
let num = h.length - 1;
// for 循环里并不是 i <= n; 因为 n 在 deleteMax 中在自减。
for (let i = 1; i <= num; i++) {
console.log(deleteMax());
}
堆排序–大根堆
const h = [0, 99, 5, 36, 2, 19, 1, 46, 12, 7, 22, 25, 28, 17, 92]
let n = h.length - 1;
// 下沉操作
const siftdown = (i) => {
let t, flag = 0;
while (i * 2 <= n && flag === 0) {
// flag 控制是否需要继续下沉
if (h[i] < h[i * 2]) { // 最大堆
// 和左孩子比较
t = i * 2;
} else {
t = i;
}
// 如果有右孩子,在判断右孩子的情况
if (i * 2 + 1 <= n) {
if (h[t] < h[i * 2 + 1]) { // 最大堆
// 更新较小的结点编号
t = i * 2 + 1;
}
}
// 如果发现最小的节点不是自己,那么交换
if (t != i) {
swap(t, i);
i = t;
} else {
// 如果最小的是自己,那么就不需要下沉了。
flag = 1;
}
}
}
const siftup = (i) => {
let flag = 0;
if (i === 1) return; // 已经到堆顶了
while (i !== 1 && flag === 0) {
// 判断是否比父节点的小
let fa = parseInt(i / 2);
if (h[i] > h[fa]) { // 最大堆
swap(i, fa)
} else {
flag = 1;
}
i = fa;
}
}
const swap = (i, j) => {
[h[j], h[i]] = [h[i], h[j]];
}
/*
建立堆
思想就是,判断以当前节点为根节点的树是否满足最小根的规则
那么从哪里开始呢:叶子节点是不需要考虑的,倒数,从第一个非叶子节点开始
从后向前排查即可。
*/
for (let i = parseInt(n / 2); i >= 1; i--) {
siftdown(i);
}
console.log('大根堆建立完成:>>', h);
const heapSort = () => {
// 如果从小到大排序,那么这样做的前提是大根堆;
while (n >= 1) {
swap(1, n);
n--;
siftdown(1);
}
}
heapSort();
console.log('排序完成:>>', h);
小节总结
这一小节其实东西挺重要的,二叉树的典型应用
优先队列:支持插入元素并且寻找最大值、最小值的数据解构
应用场景:
- 求一个数列中第k大的树:我们可以用一个简单的例子,比方说
[1,2,3,4,5]
,找第三个最大的数,先建立规模为k的最小堆,然后从4开始,与最小堆的根作比较,如果根 小于 4 ,那么替换掉,并下沉,然后接着比较5,5 和 2 比较,也是小于,替换掉,并下沉,此时的根就是3了。如果大于的话,跳过替换的操作即可。(字好多啊,有时间提取出来写个小专题代码)
并查集
// 找集合
// 遵守两种规则:1、靠左法则 2、擒贼擒王
let f = new Array(11);
let n = 10,
m = 9;
// 初始化
for (let i = 1; i <= n; i++) {
f[i] = i;
}
// 获取最大 boss
const getf = (v) => {
if (f[v] === v) {
return v;
} else {
// 路径压缩
f[v] = getf(f[v]);
return f[v];
}
}
var count = 0;
console.log(`合并第${count}次的f:>>`, f);
// 合并
const merge = (v, u) => {
let t1 = getf(v),
t2 = getf(u);
if (t1 !== t2) {
f[t2] = t1; // f[右] = 左,所谓的靠左原则
}
count++;
console.log(`合并第${count}次的f:>>`, f);
}
let a = [
[0, 0], [1, 2], [3, 4], [5, 2], [4, 6], [2, 6], [8, 7], [9, 7], [1, 6], [2, 4]
]
for (let i = 1; i <= m; i++) {
merge(a[i][0], a[i][1])
}
let sum = 0;
for (let i = 1; i <= n; i++) {
if (f[i] === i) {
sum++;
}
}
console.log("最终f:>>", f);
console.log("并查集结果:>>", sum);