第九节:堆结构详解(上滤、下滤、最大堆、最小堆、二叉堆)

一. 堆简介

1. 什么是堆结构?

堆的本质是一种特殊的树形数据结构,使用完全二叉树来实现,平时使用的基本都是二叉堆

二叉堆用树形结构表示出来是一颗完全二叉树,通常在实现的时候我们底层会使用数组来实现。

二叉堆又可以划分为最大堆和最小堆。

(1) 最小堆:堆中每一个节点都小于等于(<=)它的子节点;

(2) 最大堆:堆中每一个节点都大于等于(>=)它的子节点;

2. 用来解决什么问题?

堆结构通常是用来解决Top K问题的:Top K问题是指在一组数据中,找出最前面的K个最大/最小的元素

3. 总结的公式

(下图是堆结构和对应的数组存储关系图,很重要!!!)

(1) 如果 i = 0 ,它是根节点;

父节点的公式:floor( (i – 1) / 2 )

左子节点:2i + 1 右子节点:2i + 2

(2) 第一个非叶子节点的索引:Math.floor(length/2 - 1) 【公式记住即可,后面补充推导过程】

4. 上滤

(1).定义

从下而上对堆进行重构,维护最大堆的性质。

比如insert方法,每次插入元素后(数组的最后push),需要对堆进行重构,以维护最大堆的性质,这种策略叫做上滤(从下而上)。

结合下面的insert方法封装查看

(2).实操

A. 获取该子节点的父节点,和它比较大小. 【子节点索引:this.length-1 父节点索引:floor((i-1)/2) 】

B. 比较结果

①. 父节点 >= 子节点,直接结束,不需要进行上滤操作;

②. 如果父节点 < 子节点, 需要将二者进行内容交换swap;并将索引改为父节点的索引.

C. 然后继续与父节点操作,重复上述操作;

D. 循环结束的条件:当索引值>0一直循环, 索引值<=0, 则终止循环。

5. 下滤

(1).定义

从上而下对堆进行重构,维护最大堆的性质。

比如delete方法,删除元素后(删除的是第1个元素),同时需要把最后一个元素提到第一个的位置,此时需要对堆结构进行重构,以维护最大堆的特性, 这个操作就叫下滤(从上而下)。

结合下面的delete方法封装查看

(2).实操

A. 获取该节点(首次为第一个节点 , 索引为index)的左右子节点索引。比较左右节点value值的大小

B. 可能只有左节点, 没有右节点, 所以需要特殊处理:largerIndex默认赋值leftChildIndex, 然后比较左右大小的时候,额外加一个条件, rightChildIndex < this.length

C. 比较大小后,将较大值的索引赋值给largerIndex

D. 比较 data[largerIndex] 和 data[index]大小,

a. 如果data[index] 大(或等于),直接break,结束即可。

b. 如果data[largerIndxe] 大, 则需要进行swap交换, index被赋值为largerIndex, 然后继续获取index索引的左右子节点, 重复上述操作

F. 循环结束的条件: 左子节点索引 2i+1 < this.length,可以一直循环,反之终止循环

二. 最大堆封装

1. 基本封装

底层用数组来实现堆结构,然后length记录堆中元素的个数

class Heap {

//底层用数组来实现堆结构

data: T[] = []; //为了测试,暂时去掉private

//堆中元素的个数

private length: number = 0;

constructor(arr: T[] = []) {

this.buildHeap(arr);

}

}

声明swap方法,交换索引i和j位置的元素

/**

* 01-交换索引i和j位置的元素

* @param i 位置i

* @param j 位置j

*/

private swap(i: number, j: number) {

let temp = this.data[i];

this.data[i] = this.data[j];

this.data[j] = temp;

}

借助hy-algokit工具集,封装print方法,用来打印

import { cbtPrint } from 'hy-algokit';

/**

* 02-打印二叉堆

*/

public print() {

cbtPrint(this.data);

}

2. insert方法

尾部插入元素后,进行上滤操作即可

/**

* 03-插入元素

* @param val 元素值

*/

public insert(val: T) {

//1.向最后位置插入元素

this.data.push(val);

this.length++;

//2.进行上滤操作

this.heapify_up();

}

/**

* 上滤操作

*/

private heapify_up() {

let index = this.length - 1; //最后位置的索引

while (index > 0) {

let parentIndex = Math.floor((index - 1) / 2); //父节点的索引

if (this.data[parentIndex] >= this.data[index]) {

break; //跳出循环

}

this.swap(index, parentIndex); //交换两个索引位置的内容

index = parentIndex;

}

}

测试:

{

console.log('----------------------01-测试insert方法-----------------------');

const arr = [19, 100, 36, 17, 3, 25, 1, 2, 7];

for (const item of arr) {

heap.insert(item);

}

heap.print();

console.log(heap.data);

}

3. delete方法(有的地方叫提取 extract)

情况1: 当没有元素或1个元素的情况

直接返回null 或者 这个元素即可,无须进行额外操作。

情况2: 当有多个元素的时候

A. 获取要删除的元素,并将最后位置的元素放到第一个,同时最后位置元素的删除

B. 对第一个元素进行下滤操作

/**

* 04-删除元素(提取元素)

* @returns 返回删除的元素, 不存在则返回null

*/

public delete(): T | null {

//1. 没有元素或只有一个元素的情况

if (this.length === 0) return null;

if (this.length === 1) {

this.length--;

return this.data.pop()!;

}

//2. 删除元素,并将最后一个元素提到第一个位置

let topValue = this.data[0];

this.data[0] = this.data.pop()!; //删除的同时并返回

this.length--;

//3.下滤操作

this.heapify_down();

return topValue;

}

/**

* 下滤操作

* @param start 从该索引开始下滤,默认从头部开始,即索引为零

*/

private heapify_down(start: number = 0) {

let index = start;

while (2 * index + 1 < this.length) {

let leftChildIndex = 2 * index + 1;

let rightChildIndex = 2 * index + 2;

let largerChildIndex = leftChildIndex; //默认赋值左子节点索引,因为右子节点可能不存在

//比较左右子节点的大小

if (rightChildIndex < this.length && this.data[rightChildIndex] >= this.data[leftChildIndex]) {

largerChildIndex = rightChildIndex;

}

//比较index和largerChildIndex索引对应值的大小

if (this.data[index] >= this.data[largerChildIndex]) {

break; //跳出循环

}

//交换位置

this.swap(index, largerChildIndex);

index = largerChildIndex;

}

}

4. buildHeap原地建堆

(1).含义

是指建立堆的过程中,不使用额外的内存空间,直接在原有数组上进行操作。

这种原地建堆的方式,我们称之为自下而上的下滤。也可以使用自上而下的上滤,但是效率较低。

(2).实操

A. 给基础的data、length属性赋值

B. 从第一个非叶子节点开始,进行下滤操作。 第一个非叶子节点的索引:math.floor(length/2 - 1) 【公式记住即可,后面补充推导过程】

C. 结合构造函数进一步封装

constructor(arr: T[] = []) {

this.buildHeap(arr);

}

/**

* 05-原地建堆

* @param arr 需要建堆的数组

*/

buildHeap(arr: T[]) {

//1. 赋值

this.data = arr;

this.length = arr.length;

//2. 从第一个非叶子节点开始,进行下滤操作

let start = Math.floor(this.length / 2 - 1);

for (let i = start; i >= 0; i--) {

this.heapify_down(i);

}

}

测试:

{

console.log('----------------------03-原地建堆-----------------------');

const arr = [19, 100, 36, 17, 3, 25, 1, 2, 7];

//1.直接调用方法

let heap1 = new Heap();

heap1.buildHeap(arr); //调用了4次下滤操作,分别是索引 3,2,1,0

heap1.print();

//2. 使用构造函数

let heap2 = new Heap(arr);

heap2.print();

}

5. 其它方法

peek、size、isEmpty

/** 其他方法 */

peek(): T | undefined {

return this.data[0];

}

size() {

return this.length;

}

isEmpty() {

return this.length === 0;

}

三. 最小堆封装

1. 前提

在最大堆的基础上进行改造

2. heapify_up 上滤

将this.data[parentIndex] >= this.data[index] 改为 this.data[parentIndex] <= this.data[index]

3. heapify_down 下滤

将 this.data[rightChildIndex] >= this.data[leftChildIndex] 改为 this.data[rightChildIndex] <= this.data[leftChildIndex]

将 this.data[index] >= this.data[largerChildIndex] 改为 this.data[index] <= this.data[largerChildIndex]

为了更加符合语义名称, largerChildIndex 改名为 smallerChildIndex

/**

* 上滤操作

*/

private heapify_up() {

let index = this.length - 1; //最后位置的索引

while (index > 0) {

let parentIndex = Math.floor((index - 1) / 2); //父节点的索引

if (this.data[parentIndex] <= this.data[index]) {

break; //跳出循环

}

this.swap(index, parentIndex); //交换两个索引位置的内容

index = parentIndex;

}

}

/**

* 下滤操作

* @param start 从该索引开始下滤,默认从头部开始,即索引为零

*/

private heapify_down(start: number = 0) {

let index = start;

while (2 * index + 1 < this.length) {

let leftChildIndex = 2 * index + 1;

let rightChildIndex = 2 * index + 2;

let smallerChildIndex = leftChildIndex; //默认赋值左子节点索引,因为右子节点可能不存在

//比较左右子节点的大小

if (rightChildIndex < this.length && this.data[rightChildIndex] <= this.data[leftChildIndex]) {

smallerChildIndex = rightChildIndex;

}

//比较index和largerChildIndex索引对应值的大小

if (this.data[index] <= this.data[smallerChildIndex]) {

break; //跳出循环

}

//交换位置

this.swap(index, smallerChildIndex);

index = smallerChildIndex;

}

}

四. 二叉堆封装

1. 目标

根据传入的参数,来决定构建最大堆 还是 最小堆

2. 实操:

(1). 属性isMax,true表示最大堆, false表示最小堆,在构造函数中初始化

class Heap {

//底层用数组来实现堆结构

data: T[] = []; //为了测试,暂时去掉private

//堆中元素的个数

private length: number = 0;

//是否是最大堆,默认为ture, false代表最小堆

private isMax: boolean = true;

constructor(arr: T[] = [], ismax = true) {

this.isMax = ismax;

this.buildHeap(arr);

}

}

(2). 观察最大堆 和 最小堆中的,上滤和下滤中的判断条件, 最大堆都是 >= , 最小堆都是 <=

(3). 封装compare方法, 用来比较

/**

* 比较两个索引值的大小

* @param i 索引i

* @param j 索引j

* @returns ture 或 false

*/

private compare(i: number, j: number): boolean {

if (this.isMax) {

return this.data[i] >= this.data[j];

} else {

return this.data[i] <= this.data[j];

}

}

(4). 将上滤和下滤中的if比较换成compare方法即可

/**

* 上滤操作

*/

private heapify_up() {

let index = this.length - 1; //最后位置的索引

while (index > 0) {

let parentIndex = Math.floor((index - 1) / 2); //父节点的索引

if (this.compare(parentIndex, index)) {

break; //跳出循环

}

this.swap(index, parentIndex); //交换两个索引位置的内容

index = parentIndex;

}

}

/**

* 下滤操作

* @param start 从该索引开始下滤,默认从头部开始,即索引为零

*/

private heapify_down(start: number = 0) {

let index = start;

while (2 * index + 1 < this.length) {

let leftChildIndex = 2 * index + 1;

let rightChildIndex = 2 * index + 2;

// (needChildIndex最大堆:表示的是largerChildIndex,最小堆表示的是 smallerChildIndex)

let needChildIndex = leftChildIndex; //默认赋值左子节点索引,因为右子节点可能不存在

//比较左右子节点的大小

if (rightChildIndex < this.length && this.compare(rightChildIndex, leftChildIndex)) {

needChildIndex = rightChildIndex;

}

//比较index和largerChildIndex索引对应值的大小

if (this.compare(index, needChildIndex)) {

break; //跳出循环

}

//交换位置

this.swap(index, needChildIndex);

index = needChildIndex;

}

}

!

作 者 : Yaopengfei(姚鹏飞)

博客地址 : http://www.cnblogs.com/yaopengfei/

声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。

声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。

最新发表

友情链接