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

一. 堆简介

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<T> {
	//底层用数组来实现堆结构
	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<T> {
	//底层用数组来实现堆结构
	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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2024-01-05 08:41  Yaopengfei  阅读(471)  评论(1编辑  收藏  举报