堆的基本实现及优先级队列问题

本文深入讲解堆数据结构的实现原理,包括最大堆和最小堆的构造、元素的增删操作及时间复杂度分析,并探讨了堆在优先级队列中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

堆数据结构是一种数组对象,它可以被视为一棵完全二叉树结构。堆结构的二叉树存储有两种:

最大堆:每个父节点的都大于孩子节点;

最小堆:每个父节点的都小于孩子节点。

下来以大堆为例:

1.如何建堆?

1>首先我们选取vector来对堆中元素进行存储;

2>由于堆数据结构可被视为一棵完全二叉树,我们可以从最后一个非叶子节点起往上调整;但是须得注意,这个首先得满足左右子树都是大堆得情况

3>上调过程中,先让child指向左孩子,然后进行左,右孩子的比较,选出较大的一个,再与父节点比较,如果父节点比两孩子中较大那个值小,则需交换父节点与子节点,直至调整为大堆。


Heap(T* a,size_t size)
{
     assert(a);
     _a.reserve(size);
     for (size_t i=0;i<size;++i)
     {
	_a.push_back(a[i]);
      }

      //建堆,从第一个非叶子节点的父节点开始调整
     for (int i=(size-2)/2;i>=0;--i)   
     {
	AdjustDown(i);
     }
}

void AdjustDown(size_t root)
{
   size_t parent=root;
   size_t child=parent*2+1;   //先让child指向左孩子
   while (child<_a.size())
   {
	//保证右孩子不为空
	if (child+1<_a.size() && _a[child+1]>_a[child])
	{
	   ++child;
	}
	if (_a[parent]<_a[child])
	{
	   std::swap(_a[parent],_a[child]);
	   parent=child;
	   child=parent*2+1;
	}
	else
	   break;
	}
}


2.建好堆后如何在堆中进行push元素?

先将元素插入到数组最后一个位置,然后进行上调操作,直至满足大堆的特点。

3.pop元素?

1>我们采取先将堆中最后一个元素转移至第一个元素位置,也就是用最后一个元素覆盖掉第一个元素;

2>将最后那个pop掉;

3>从0位置处开始往下调整;

4>在向下调整时,需注意找公共父节点parent=(child-1)/2,以及循环条件


void Pop()
	{
		assert(!_a.empty());
		std::swap(_a[0],_a[_a.size()-1]);
		_a.pop_back();
		AdjustDown(0);
	}
void AdjustUp(size_t child)
	{
		while (child>0)
		{
			size_t parent=(child-1)/2;
			if (_a[parent]<_a[child])
			{
				std::swap(_a[parent],_a[child]);
				child=parent;
			}
			else
				break;
		}
	}


实现大堆的完整代码:

Heap.h

#pragma once

#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;

template<class T>
class Heap
{
public:
	Heap(T* a,size_t size)
	{
		assert(a);
		_a.reserve(size);
		for (size_t i=0;i<size;++i)
		{
			_a.push_back(a[i]);
		}

		//建堆,从第一个非叶子节点的父节点开始调整
		for (int i=(size-2)/2;i>=0;--i)   
		{
			AdjustDown(i);
		}
	}

	void Push(const T& d)
	{
		_a.push_back(d);
		AdjustUp(_a.size()-1);
	}

	void Pop()
	{
		assert(!_a.empty());
		std::swap(_a[0],_a[_a.size()-1]);
		_a.pop_back();
		AdjustDown(0);
	}

	const T& Top()
	{
		assert(!_a.empty());
		return _a[0];
	}

	bool Empty()
	{
		return _a.empty();
	}

	size_t Size()
	{
		return _a.size();
	}

	void Print()
	{
		for (size_t i=0;i<_a.size();++i)
		{
			cout<<_a[i]<<" ";
		}
		cout<<endl;
	}
protected:
	void AdjustDown(size_t root)
	{
		size_t parent=root;
		size_t child=parent*2+1;   //先让child指向左孩子
		while (child<_a.size())
		{
			//保证右孩子不为空
			if (child+1<_a.size() && _a[child+1]>_a[child])
			{
				++child;
			}
			if (_a[parent]<_a[child])
			{
				std::swap(_a[parent],_a[child]);
				parent=child;
				child=parent*2+1;
			}
			else
				break;
		}
	}

	void AdjustUp(size_t child)
	{
		while (child>0)
		{
			size_t parent=(child-1)/2;
			if (_a[parent]<_a[child])
			{
				std::swap(_a[parent],_a[child]);
				child=parent;
			}
			else
				break;
		}
	}
protected:
	vector<T> _a;
};

建小堆,其实思想都类似,所以可以采取一种方式,增强代码的复用性,即定义一个仿函数Less,larger实现数据的比较,从而实现父节点与子节点的交换,进而满足大小堆的特点。

#pragma once

#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;


template<class T>
struct Less
{
	bool operator()(const T& l,const T& r)
	{
		return l<r;
	}
};

template<class T>
struct Larger
{
	bool operator()(const T& l,const T& r)
	{
		return l>r;
	}
};

template<class T,class compare=Larger<T>>
class Heap
{
public:
	Heap(T* a,size_t size)
	{
		assert(a);
		_a.reserve(size);
		for (size_t i=0;i<size;++i)
		{
			_a.push_back(a[i]);
		}

		//建堆,从第一个非叶子节点的父节点开始调整
		for (int i=(size-2)/2;i>=0;--i)   
		{
			AdjustDown(i);
		}
	}

	void Push(const T& d)
	{
		_a.push_back(d);
		AdjustUp(_a.size()-1);
	}

	void Pop()
	{
		assert(!_a.empty());
		std::swap(_a[0],_a[_a.size()-1]);
		_a.pop_back();
		AdjustDown(0);
	}

	const T& Top()
	{
		assert(!_a.empty());
		return _a[0];
	}

	bool Empty()
	{
		return _a.empty();
	}

	size_t Size()
	{
		return _a.size();
	}

	void Print()
	{
		for (size_t i=0;i<_a.size();++i)
		{
			cout<<_a[i]<<" ";
		}
		cout<<endl;
	}
protected:
	void AdjustDown(size_t root)
	{
		compare com;
		size_t parent=root;
		size_t child=parent*2+1;   //先让child指向左孩子
		while (child<_a.size())
		{
			//保证右孩子不为空,比较左右孩子
			if (child+1<_a.size() && com(_a[child+1],_a[child]))
			{
				++child;
			}
			if (com(_a[child],_a[parent]))
			{
				std::swap(_a[parent],_a[child]);
				parent=child;
				child=parent*2+1;
			}
			else
				break;
		}
	}

	void AdjustUp(size_t child)
	{
		while (child>0)
		{
			compare com;
			size_t parent=(child-1)/2;
			if (com(_a[child],_a[parent]))
			{
				std::swap(_a[parent],_a[child]);
				child=parent;
			}
			else
				break;
		}
	}
protected:
	vector<T> _a;
};


测试:

#include"Heap.h"

void Test1()
{
	int a[] = {10,11,13,12,16,18,15,17,14,19};
	Heap<int> hp(a,sizeof(a)/sizeof(a[0]));     //默认大堆
	hp.Print();
	
	cout<<"Empty> "<<hp.Empty()<<endl;
	cout<<"Size> "<<hp.Size()<<endl;
	cout<<"Top> "<<hp.Top()<<endl;

	hp.Push(20);
	hp.Print();
	hp.Pop();
	hp.Print();
}

void Test2()
{
	int a[] = {10,11,13,12,16,18,15,17,14,19};
	Heap<int,Less<int>> hp(a,sizeof(a)/sizeof(a[0]));
	hp.Print();

	cout<<"Empty> "<<hp.Empty()<<endl;
	cout<<"Size> "<<hp.Size()<<endl;
	cout<<"Top> "<<hp.Top()<<endl;

	hp.Push(9);
	hp.Print();
	hp.Pop();
	hp.Print();

}

int main()
{
	//Test1();
	Test2();
	system("pause");
	return 0;
}


下面谈谈堆的建立,Push,Pop时间复杂度问题:

1.在建堆的过程,时间复杂度为O(n*lgn);

2.在Push时,时间复杂度为O(lgn);

3.在Pop时,时间复杂度为O(lgn).

再引入一个优先级队列的问题,队列本来是“先进先出”的,但是在一些特殊情况下排在队中间的需要比对头元素先出去,这就是优先级问题。

以前解决的方法是:1.在优先位置插入元素,时间复杂度为O(n);在队头删除元素,时间复杂度O(1).

                                  2.在队尾插入元素,时间复杂度O(1);先找到优先出队列元素,再Pop,时间复杂度为O(n).

这两种方法,总的来说Push,Pop时间复杂度都为O(n).

学习堆之后,我们解决这个问题就可以更高效了,堆的Push,Pop总的时间复杂度才O(lgn).相比上面的方法,在处理大量数据时这种方法很占优势。

在堆得基本操作实现后,优先级队列可实现为:

template<class T,class compare=Larger<T>>
class PriorityQueue
{
public:
	PriorityQueue(T* a,size_t size)
		:_h(a,size)
	{}
	void Push(const T& d)
	{
		_h.Push(d);
	}
	void Pop()
	{
		_h.Pop();
	}
	bool Empty()
	{
		return _h.Empty()==0;
	}
	size_t Size()
	{
		return _h.Size();
	}
	const T& Front()
	{
		return _h.Top();
	}
	void PrintPriorityQueue()
	{
		_h.Print();
	}
protected:
	Heap<T,compare> _h;
};

测试用例:

void Test3()
{
	int a[] = {10,11,13,12,16,18,15,17,14,19};
	PriorityQueue<int,Larger<int>> h(a,sizeof(a)/sizeof(a[0]));
	h.PrintPriorityQueue();  //19 17 18 14 16 13 15 12 10 11

	cout<<"Empty> "<<h.Empty()<<endl;
	cout<<"Size> "<<h.Size()<<endl;
	cout<<"Top> "<<h.Front()<<endl;

	h.Push(20);               //20 19 18 14 17 13 15 12 10 11 16
	h.PrintPriorityQueue();
	h.Pop();
	h.PrintPriorityQueue();

	PriorityQueue<int,Less<int>> h1(a,sizeof(a)/sizeof(a[0]));
	h1.PrintPriorityQueue();   //10 11 13 12 16 18 15 17 14 19
	h1.Push(9);               
	h1.PrintPriorityQueue();    //9 10 13 12 11 18 15 17 14 19 16
	h1.Pop();
	h1.PrintPriorityQueue();
}





















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值