--dd334c3952b0e707 x-next-cache-tags: _N_T_/layout,_N_T_/blog/layout,_N_T_/blog/[...slug]/layout,_N_T_/blog/[...slug]/page,_N_T_/blog/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/heap vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch heap(堆) | YBinary
Published on

heap(堆)

Authors

堆(heap)

堆(Heap)是计算机科学中的一种特别的完全二叉树。满足以下两点,它就是一个堆

  • 堆是一个完全二叉树

  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。(任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性))

堆的表示或实现

因为堆是完全二叉树,而用数组存储完全二叉树是非常节省存储空间的,所以堆也选择数组存储

heap

对于一个存储在数组中的二叉堆,假设根节点的索引为 1(有时从 0 开始),则对于任意节点i:

  • 父结点 parent(i) = (i/2) 「向下取整」

  • 左子节点的索引为left(i)=2i

  • 右子节点的索引为right(i)=2i+1

heap

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

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

  • 左子节点:2i + 1

  • 右子节点:2i + 2

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

因为堆是完全二叉树,所以说,数组中的元素的父节点、孩子节点都是可以用公式算出来的!(层序遍历下)

不管是插入还是删除后都有可能不再满足堆的定义即:

  • 堆是一颗完全二叉树

  • 堆中每个节点都必须大于等于(或小于等于)其左右子节点

在插入或删除操作后需要进行调整,让其重新满足堆的特性,这个调整的过程叫做堆化(heapify)

两种堆化方式

  1. 上滤

(1).定义

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

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

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

(2).实操

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

B. 比较结果

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

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

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

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

  1. 下滤

(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,可以一直循环,反之终止循环

最大堆封装

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


class Heap<T>{
    //底层用数组来实现堆结构
    private data:T[] = [];
    //堆中元素的个数
    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) {
        [this.data[i],this.data[j]] = [this.data[j],this.data[i]]
     }
  1. 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[index] <= this.data[parentIndex]) {
                break;
            } 

            this.swap(index,parentIndex)

            index = parentIndex
        }
    }
  1. delete方法(有的地方叫提取 extract)

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

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

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

A. 获取要删除(提取)的元素,同时将最后的元素提取到第一个

B. 对提取大第一个的元素进行下滤操作


    /**
	 * 04-删除元素(提取元素)
	 * @returns 返回删除的元素, 不存在则返回null
	 */
    public delete():T | null {
        //1.没有元素或只有一个元素的情况
        if(this.data === null || this.data.length<=0) {
            return null;
        }
        if(this.data.length === 1) {
            this.length--;
            return this.data.pop()!;
        }

        //2. 删除元素,并将最后一个元素提到第一个位置
        let returnValue = this.data[0];
        this.data[0] = this.data.pop()!;//删除的同时并返回
        this.length--;

        //3.下滤操作
        this.heapify_down();

        return returnValue;

    }

    public 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
            }

            // 若是当前节点,比左右子节点都大,则跳出循环
            if(this.data[index] >= this.data[largerChildIndex]) {
                break;
            }

            // 交换位置
            this.swap(index,largerIndex)

            index = largerChildIndex;

        }
    }
  1. 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[] = []) {
        // 赋值
        this.data = arr;
        this.length = arr.length;

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

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

  1. 其它方法

    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. 目标

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

  1. 实操:

(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;
   	}
   }

Refernce

https://www.cnblogs.com/yaopengfei/p/17946538

--dd334c3952b0e707--