数据结构核心数据结构之二叉堆的原理及实现

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构核心数据结构之二叉堆的原理及实现相关的知识,希望对你有一定的参考价值。


1.大顶堆和小顶堆原理

  • 什么是堆
  • 堆(Heap)是计算机科学中一类特殊的数据结构,通常是一个可以被看作一颗完全二叉树的数组对象。
  • 完全二叉树
  • 只有最下面两层节点的度可以小于2,并且最下层的叶节点集中在靠左连续的边界
  • 只允许最后一层有空缺结点且空缺在右边,完全二叉树需保证最后一个节点之前的节点都齐全;
  • 对任一结点,如果其右子树的深度为j,则其左子树的深度必为j或j+1

【数据结构】核心数据结构之二叉堆的原理及实现_父节点

  • 什么是大顶堆(最大堆)
  • 大顶堆是一种完全二叉树,其每个父节点的值都大于或等于其子节点的值,即根节点的值最大。
  • 每个节点的两个子节点顺序没做要求,和之前的二叉查找树不一样。

【数据结构】核心数据结构之二叉堆的原理及实现_算法_02

  • 什么是小顶堆(最小堆)
  • 小顶堆是一种完全二叉树,其每个父节点的值都小于或等于其子节点的值,即根节点的值最小。
  • 每个节点的两个子节点顺序没做要求,和之前的二叉查找树不一样

【数据结构】核心数据结构之二叉堆的原理及实现_算法_03

  • 存储原理
  • 一般升序采用大顶堆,降序采用小顶堆。
  • 堆是一种非线性结构,用数组来存储完全二叉树是非常节省空间的,把堆看作一个数组。
  • 方便操作,一般数组的下标0不存储,直接从1节点存储。
  • 堆其实就是利用完全二叉树的结构来维护一个数组
  • 数据下表为k的节点
  • 左子节点下标为2*k的节点。
  • 右子节点就是下表为2*k+1的节点。
  • 父节点就是下标为k/2取证的节点。
  • 公式描述一下堆的定义
  • 大顶堆:arr[k] >= arr[2k+1] && arr[k] >= arr[2k]
  • 小顶堆:arr[k] <= arr[2k+1] && arr[k] <=arr[ak]
  • 小顶堆动画效果演示
  • 往堆中插入新元素,就是往数组中从索引0或1开始依次存放数据,但是顺序需要满足堆的特性
  • 如何让堆满足:
  • 不断比较新节点 arr[k]和对应父节点arr[k/2]的大小,根据情况交互元素位置
  • 直到找到的父节点比当前新增节点大则结束

【数据结构】核心数据结构之二叉堆的原理及实现_java_04

2.大顶堆构编码实现

  • 大顶堆(最大堆)
  • 大顶堆是一种完全二叉树,其每个父节点的值都大于或等于其子节点的值,即根节点的值最大

【数据结构】核心数据结构之二叉堆的原理及实现_java_05

  • 编码实现
public class Heap 

//用数组存储堆中的元素
private int[] items;

//堆中元素的个数
private int num;

public Heap(int capacity)
//数组下标0不存储数据,所以容量+1
this.items = new int[capacity + 1];
this.num = 0;


/**
* 判断堆中 items[left] 元素是否小于 items[right] 的元素
*/
private boolean rightBig(int left, int right)
return items[left] < items[right];


/**
* 交换堆中的两个元素位置
*/
private void swap(int i, int j)
int temp = items[i];
items[i] = items[j];
items[j] = temp;


/**
* 往堆中插入一个元素,默认是最后面,++num先执行,然后进行上浮判断操作
*/
public void insert(int value)
items[++num] = value;
up(num);


/**
* 使用上浮操作,新增元素后,重新堆化
* 不断比较新节点 arr[k]和对应父节点arr[k/2]的大小,根据情况交互元素位置
* 直到找到的父节点比当前新增节点大则结束
* <p>
* 数组中下标为 k 的节点
* 左子节点下标为 2*k 的节点
* 右子节点就是下标 为 2*k+1 的节点
* 父节点就是下标为 k/2 取整的节点
*/
private void up(int k)
//父节点 在数组的下标是1,下标大于1都要比较
while (k > 1)
//比较 父结点 和 当前结点 大小
if (rightBig(k / 2, k))
//当前节点大,则和父节点交互位置
swap(k / 2, k);

// 往上一层比较,当前节点变为父节点
k = k / 2;



/**
* 删除堆中最大的元素,返回这个最大元素
*/
public int delMax()
int max = items[1];
//交换索引 堆顶的元素(数组索引1的)和 最大索引处的元素,放到完全二叉树中最右侧的元素,方便后续变为临时根结点
// 为啥不能直接删除顶部元素,因为删除后会断裂,成为森林,所以需要先交互,再删除
swap(1, num);
//最大索引处的元素删除掉, num--是后执行,元素个数需要减少1
items[num--] = 0;

//通过下浮调整堆,重新堆化
down(1);
return max;


/**
* 使用下沉操作,堆顶和最后一个元素交换后,重新堆化
* 不断比较 节点 arr[k]和对应 左节点arr[2*k] 和 右节点arr[2*k+1]的大小,如果当前结点小,则需要交换位置
* 直到找到 最后一个索引节点比较完成 则结束
* 数组中下标为 k 的节点
* 左子节点下标为 2*k 的节点
* 右子节点就是下标 为 2*k+1 的节点
* 父节点就是下标为 k/2 取整的节点
*/
private void down(int k)
//最后一个节点下标是num
while (2 * k <= num)
//记录当前结点的左右子结点中,较大的结点
int maxIndex;
if (2 * k + 1 <= num) //2 * k + 1 <= num 是判断 确保有右节点
//比较当前结点下的左右子节点哪个大
if (rightBig(2 * k, 2 * k + 1))
maxIndex = 2 * k + 1;
else
maxIndex = 2 * k;

else
maxIndex = 2 * k;


//比较当前结点 和 较大结点的值, 如果当前节点较大则结束
if (items[k] > items[maxIndex])
break;
else
//否则往下一层比较,当前节点k索引 变换为 子节点中较大的值
swap(k, maxIndex);
//变换k的值
k = maxIndex;





public static void main(String[] args)
Heap heap = new Heap(20);
heap.insert(42);
heap.insert(48);
heap.insert(93);
heap.insert(21);
heap.insert(90);
heap.insert(9);
heap.insert(3);
heap.insert(40);
heap.insert(32);

int top;
System.out.println("输出堆:");
while ((top = heap.delMax()) != 0)
System.out.print(top + " ");


【数据结构】核心数据结构之二叉堆的原理及实现_数据结构_06


C++ 不知树系列之二叉堆排序(递归和非递归实现上沉下沉算法)

1. 前言

什么是二叉堆?

二叉堆有序完全二叉树,在完全二叉树的基础上,二叉堆 提供了有序性特征

  • 二叉堆根结点上的值是整个堆中的最小值最大值

  • 根结点上的值是整个堆结构中的最小值时,此堆称为最小堆。最小堆中,任意节点的值大于父结点的值。

  • 根结点上的值是整个堆结构中的最大值时,则称堆为最大堆。最大堆中,任意节点的值小于父结点的值。

根据完全二叉树的特性,二叉堆的父结点与子结点之间满足下面的关系:

  • 如果知道了一个结点的位置 i,则其左子结点在 2*i 位置,右子结点在 2*i+1 位置。

  • 如果知道了一个结点的位置 i,则其父结点在 i除以 2 的位置。

如上图所示:

值为 5 的结点在 2 处,则其左结点 12 的位置应该在 2*2=4 处,而实际情况也是在 4 位置。其右子结点 13 的位置应该在 2*2+1=5 的位置,实际位置也是在 5 位置。

值为 19 的结点现在 7 位置,其父结点的根据公式 72 等于 3(取整),应该在 3 处,而实际情况也是在 3 处(位置在 3、 值为 8 的结点是其父结点)。

2 堆的数据结构

2.1 二叉堆的抽象数据结构

当谈论某种数据结构的抽象数据结构时,最基本的 API 无非就是增、删、改、查。

二叉堆的基本抽象数据结构:

  • Heap() :创建一个新堆。
  • insert(data): 向堆中添加新节点(数据)。
  • getRoot(): 返回最小(大)堆的最小(大)元素。
  • removeRoot() :删除根节点。
  • isEmpty():判断堆是否为空。
  • findAll():查询堆中所有数据。

根据二叉堆的特性,顺序存储应该成为堆的首选方案。

如有数列=[8,5,12,15,19,13,1],可以先创建一个一维数组。

数组第 0 位置初始为 0,从第 2 个位置也就是索引号为 1 的地方开始存储堆的数据。如下图,二叉堆中的数据在数组中的对应存储位置。

2.2 基础 API 实现

设计一个 Heap 类封装对二叉堆的操作方法,类中方法用来实现最小堆。

#include <iostream>
using namespace std;
/* 
* 堆类 
*/ 
template<typename T>
class Heap
    private:
    	
    	//数组
    	T heapList[100];
    	//实际大小
		int size=0; 
		
	public:
		
		/*
		*构造函数 
		*/ 
		Heap()
		 
		
		/*
		*返回根结点的值 
		*/
		T getRoot();
		
		/*
		*删除根结点 
		*/
		T removeRoot();
    	
    	/*
		*递归删除
		*/
		T removeRoot_();
		void removeRootByRecursion(int parentIdx );
		
		/*
		*初始化根结点 
		*/ 
		void setRoot(T val);
		
		/*
		*添加新结点,返回存储位置 
		*/
		int insert(T  val);
		
		/*
		*堆是否为空 
		*/ 
		bool isEmpty();
    
    	/*
		* 递归插入 
		*/
		int insert_(T  val);
		int insertByRecursion(int  pos);
		
		/*
		*输出所有结点
		*/
		void findAll() 
			for(int i=0; i<=size; i++)
				cout<<this->heapList[i]<<"\\t";
			cout<<endl;
			 
; 

Heap 类中的属性详解:

  • heapList:使用数组存储二叉堆的数据,初始时,列表的第 0 位置初始为默认值 0

  • size:用来存储二叉堆中数据的实际个数。

Heap 类中的方法介绍:

isEmpty:检查是不是空堆。逻辑较简单。

/*
*当 size 为 0 时,堆为空 
*/
template<typename T>
bool Heap<T>::isEmpty()
	return Heap::size==0;

setRoot:创建根结点。保证根节点始终存储在列表索引为 1 的位置。

/*
*初始化根结点
*/
template<typename T>
void Heap<T>::setRoot(T val) 
	if( Heap<T>::heapList[1]==0  )
		Heap<T>::heapList[1]=val;
		Heap<T>::size++;

getRoot:如果是最大堆,则返回二叉堆的最大值,如果是最小堆,则返回二叉堆的最小值。

/*
*返回根结点
*/
template<typename T>
T Heap<T>::getRoot() 
	if( !Heap<T>::isEmpty  )
		return	Heap<T>::heapList[1];

前面是几个基本方法,现在实现添加新结点,编码之前,先要知道如何在二叉堆中添加新结点:

2.3 上沉算法

添加新结点采用上沉算法。如下演示上沉算法的实现过程。

  • 新结点添加到已有的二叉堆的最后面。如下图,添加值为 4 的新结点,存储至索引号为 7 的位置。

  • 查找新结点父结点,并与父结点的值比较大小,如果比父结点的值小,则和父结点交换位置。如下图,值为 4 的结点小于值为 8 的父结点,两者交换位置。

  • 交换后再查询是否存在父结点,如果有,同样比较大小、交换,直到到达根结点或比父结点大为止。值为 4 的结点小于值为 5 的父结点,继续交换。交换后,新结点已经达到了根结点位置,整个添加过程可结束。观察后会发现,遵循此流程添加后,没有破坏二叉堆的有序性。

编码实现 insert 方法

/*
*添加新结点
*/
template<typename T>
T Heap<T>::insert(T val) 
	//存储在最后一个位置
	int pos= ++Heap<T>::size;
	Heap<T>::heapList[pos]=val;
	int temp=0;
	//上沉算法
	while(1) 
		//找到父结点位置
		int parentIdx=  pos / 2;
		if(parentIdx==0)
			//出口一,没有父结点
			break;
		if( Heap<T>::heapList[pos]>Heap<T>::heapList[parentIdx] )
			//出口二:大于父结点
			break;
		else 
			//和父亲结点交换
			temp=Heap<T>::heapList[pos];
			Heap<T>::heapList[pos]=Heap<T>::heapList[parentIdx];
			Heap<T>::heapList[parentIdx]=temp;
			pos=parentIdx
		
	

测试向二叉堆中添加数据。

int main(int argc, char** argv) 
	//实例化堆
	Heap<int> heap;
	//初始化根结点
	heap.setRoot(5);
	//检查根结点是否创建成功
	int rootVal=heap.getRoot();
	cout<<"根结点的值:"<<rootVal<<endl;
	//添加值为 12和值为  13 的 2个新结点,检查添加新结点后整个二叉堆的有序性是否正确。
	heap.insert(12);
	heap.insert(13);
	cout<<"测试一:"<<endl;
	heap.findAll();
	return 0;

输出结果:

添加值为 1 的新结点,并检查二叉堆的有序性。

int main(int argc, char** argv) 
	//省略……
    //添加值为 1 的结点
	heap.insert(1);
	cout<<"测试二:"<<endl;
	heap.findAll();
	return 0;

继续添加值为 151983 个新结点,并检查二叉堆的状况。

int main(int argc, char** argv) 
	//省略……
	heap.insert(15);
	heap.insert(19);
	heap.insert(8);
	cout<<"测试三:"<<endl;
	heap.findAll();
	return 0;

上沉算法同样可以使用递归实现。

/*
*递归实现插入
*/
template<typename T>
int Heap<T>::insert_(T  val) 
	//存储在最后一个位置
	int pos= ++Heap<T>::size;
	Heap<T>::heapList[pos]=val;
	//调用
	Heap<T>::insertByRecursion(pos);

template<typename T>
int Heap<T>::insertByRecursion(int  pos) 
//找到父结点位置
	int parentIdx=  pos / 2;
	if(parentIdx==0)
		//出口一,没有父结点
		return pos;
	if( Heap<T>::heapList[pos]>Heap<T>::heapList[parentIdx] )
		//出口二:大于父结点
		return pos;
	else 
		//和父亲结点交换
		int temp=Heap<T>::heapList[pos];
		Heap<T>::heapList[pos]=Heap<T>::heapList[parentIdx];
		Heap<T>::heapList[parentIdx]=temp;
		//递归 
		Heap<T>::insertByRecursion(parentIdx);
	

2.4 下沉算法

介绍完添加方法后,再来了解一下,如何使用下沉算法删除二叉堆中的结点。

二叉堆的删除操作从根结点开始,如下图删除根结点后,空出来的根结点位置,需要在整个二叉堆中重新找一个结点充当新的根结点。

二叉堆中使用下沉算法选择新的根结点:

  • 找到二叉堆中的最后一个结点,移到到根结点位置。如下图,把二叉堆中最后那个值为 19 的结点移到根结点位置。

  • 最小堆中,如果新的根结点的值比左或右子结点的值大,则和子结点交换位置。如下图,在二叉堆中把 195 的位置进行交换。

  • 交换后,如果还是不满足最小二叉堆父结点小于子结点的规则,则继续比较、交换新根结点直到下沉到二叉堆有序为止。如下,继续交换 1219 的值。如此反复经过多次交换直到整个堆结构符合二叉堆的特性。

removeoot 方法的具体实现:

/*
* 下沉算法,删除结点
*/
template<typename T>
T Heap<T>::removeRoot() 
	if(Heap<T>::size==0)return NULL;
	T root=Heap<T>::heapList[1];
	if(Heap<T>::size==1) 
		Heap<T>::size--;
		return root;
	
	//堆中最后一个结点移动根结点
	Heap<T>::heapList[1]=Heap<T>::heapList[Heap<T>::size];
	Heap<T>::size--;

	//下沉算法
	int parentIdx=1;
	//子结点值
	T minChild;
	//子结点位置
	int idx;
	while(1) 
		//左结点位置
		int leftIdx=parentIdx*2;
		//右结点位置
		int rightIdx=parentIdx*2+1;
		if( leftIdx<=Heap<T>::size && rightIdx<=Heap<T>::size ) 
			//记录较小的结点值和位置
			minChild=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?Heap<T>::heapList[leftIdx]:Heap<T>::heapList[rightIdx];
			idx=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?leftIdx:rightIdx;
		 else if( leftIdx<=Heap<T>::size) 
			minChild=Heap<T>::heapList[leftIdx];
			idx=leftIdx;
		 else if( rightIdx<=Heap<T>::size ) 
			minChild=Heap<T>::heapList[rightIdx];
			idx=rightIdx;
		else
			//没有子结点 
			break;
		
		//是否交换
		if( Heap<T>::heapList[parentIdx]>minChild ) 
			Heap<T>::heapList[idx]=Heap<T>::heapList[parentIdx];
			Heap<T>::heapList[parentIdx]=minChild;
			parentIdx=idx;
		 else 
			break;
		
	
	return root;
 

测试在二叉堆中删除结点:

int main(int argc, char** argv) 
    //省略……
	cout<<"测试删除一:"<<endl;
	heap.removeRoot();
	heap.findAll();
	return 0;

可以看到最后二叉堆的结构和有序性都得到了完整的保持。

"下沉算法" 同样可以使用递归实现。

/*
*递归实现下沉算法
*/
template<typename T>
T Heap<T>::removeRoot_() 
	if(Heap<T>::size==0)return NULL;
	//根结点值
	T root=Heap<T>::heapList[1];
	//
	if(Heap<T>::size==1) 
		Heap<T>::size--;
		return root;
	
	//堆中最后一个结点移动根结点
	Heap<T>::heapList[1]=Heap<T>::heapList[Heap<T>::size];
	Heap<T>::size--;
	//调用
	Heap<T>::removeRootByRecursion(1);
	return root;


template<typename T>
void Heap<T>::removeRootByRecursion(int parentIdx ) 
	//子结点值
	T minChild;
	//子结点位置
	int idx;
	//左结点位置
	int leftIdx=parentIdx*2;
	//右结点位置
	int rightIdx=parentIdx*2+1;
	if( leftIdx<=Heap<T>::size && rightIdx<=Heap<T>::size ) 
		//记录较小的结点值和位置
		minChild=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?Heap<T>::heapList[leftIdx]:Heap<T>::heapList[rightIdx];
		idx=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?leftIdx:rightIdx;
	 else if( leftIdx<=Heap<T>::size) 
		minChild=Heap<T>::heapList[leftIdx];
		idx=leftIdx;
	 else if( rightIdx<=Heap<T>::size ) 
		minChild=Heap<T>::heapList[rightIdx];
		idx=rightIdx;
	 else 
		//没有子结点
		return;
	
	//是否交换
	if( Heap<T>::heapList[parentIdx]>minChild ) 
		Heap<T>::heapList[idx]=Heap<T>::heapList[parentIdx];
		Heap<T>::heapList[parentIdx]=minChild;
        //递归
		Heap<T>::removeRootByRecursion(idx);
	 else 
		return;
	

3. 堆排序

堆排序指借助堆的有序性对数据进行排序。

  • 需要排序的数据以堆的方式保存。
  • 然后再从堆中以根结点方式取出来,无序数据就会变成有序数据 。

如有数列=[4,1,8,12,5,10,7,21,3],现通过堆的数据结构进行排序。

int main(int argc, char** argv) 
	//实例化堆
	Heap<int> heap;
	int nums[] = 4,1,8,12,5,10,7,21,3;
	int size=sizeof(nums)/4;
    // 创建根节点
	heap.setRoot(nums[0]);
    // 其它数据添加到二叉堆中
	for (int i=1; i<size; i++) 
		heap.insert(nums[i]);
	
	cout<<"堆中数据:"<<endl;
	heap.findAll();
    // 获取堆中的数据
	for(int i=0; i<size; i++ ) 
		nums[i]= heap.removeRoot();
		heap.findAll();
	
	for(int i=0; i<size; i++)
		cout<<nums[i]<<"\\t";
	return 0;

输出结果:

本例中的代码还有优化空间,本文试图讲清楚堆的使用,优化的地方交给有兴趣者。

4. 后记

在树结构上加上一些新特性要求,树会产生很多新的变种,如二叉树,限制子结点的个数,如满二叉树,限制叶结点的个数,如完全二叉树就是在满二叉树的“满”字上做点文章,让这个满"变成"不那么满"。

在完全二叉树上添加有序性,则会衍生出二叉堆数据结构。利用二叉堆的有序性,能轻松完成对数据的排序。

以上是关于数据结构核心数据结构之二叉堆的原理及实现的主要内容,如果未能解决你的问题,请参考以下文章

C++ 不知树系列之二叉堆排序(递归和非递归实现上沉下沉算法)

堆之二叉堆

二叉堆及优先级队列

直击网申系列直播:数据结构高频考点之二叉树二分搜索树二叉堆

面试官: 今天我们不谈二叉树, 谈谈你对二叉堆的理解?

算法之二叉树各种遍历