数据结构之堆加强堆及优先级队列

本文详细介绍了堆的概念,包括大根堆和小根堆,并通过实例展示了如何从无序数组构建堆。堆的创建分为从上到下和从下到上的方法,时间复杂度分别为O(N*logN)和O(N)。此外,文章还讨论了优先级队列的使用,包括大堆和小堆的实现,以及如何在优先级队列中处理自定义类型的元素。最后,文章提出了加强堆的概念,解决了普通堆无法删除任意位置元素的问题。
摘要由CSDN通过智能技术生成

目录

一.堆的相关概念

二 堆的创建

 三 堆的实现

四优先级队列使用及其实现

五加强堆的实现


一.堆的相关概念

什么是堆?堆本质上是一种完全二叉树,它分为两个类型:

1.大根堆。

2.小根堆。

什么是大根堆?什么是小根堆?

性质:每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆;每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆。如下图:

我们对上面的图中每个数都进行了标记,上面的结构映射成数组就变成了下面这个样子:

二 堆的创建

将无序数组构造成一个大根堆(升序用大根堆,降序就用小根堆)

主要思路:第一次保证0~0位置大根堆结构(废话),第二次保证0~1位置大根堆结构,第三次保证0~2位置大根堆结构...直到保证0~n-1位置大根堆结构(每次新插入的数据都与其父结点进行比较,如果插入的数比父结点大,则与父结点交换,否则一直向上交换,直到小于等于父结点,或者来到了顶端)插入6的时候,6大于他的父结点3,即arr(1)>arr(0),则交换;此时,保证了0~1位置是大根堆结构,如下图:

 

  插入8的时候,8大于其父结点6,即arr(2)>arr(0),则交换;此时,保证了0~2位置是大根堆结构,如下图

 插入5的时候,5大于其父结点3,则交换,交换之后,5又发现比8小,所以不交换;此时,保证了0~3位置大根堆结构,如下图:

 插入7的时候,7大于其父结点5,则交换,交换之后,7又发现比8小,所以不交换;此时整个数组已经是大根堆结构:

 

建堆有两种方式一种方式是从上到下插入的方式建堆,一种方式是从下往上建堆两种方式都可以建堆但是第一种方式建堆的时间复杂度为O(N*logN).第二种方式时间复杂度为O(N)

 1.从上往下调整对应代码:


		void AdjustDown(int* a, int n, int parent) {
			int child = 2 * parent + 1;//父亲的左孩子为父亲的小标*2+1;
			while (child < n) {//从上往下调整
				//防止越界
				if (child+1<n &&a[child+1]>a[child]) {//选出左右孩子大的那一个
					++child;
				}

				if (a[child]>a[parent]) {//如果孩子大于父亲交换
					swap(a[child], a[parent]);//交换
					parent = child;//迭代继续向下调整
					child = 2 * parent + 1;//重新计算孩子的下标
				}

				else {//如果父亲不小于孩子节点调整结束
					break;
				}
			}

		   }
  

对应建堆代码:

void AdjustDown(int* a, int n, int parent) {
			int child = 2 * parent + 1;//父亲的左孩子为父亲的小标*2+1;
			while (child < n) {//从上往下调整
				//防止越界
				if (child+1<n &&a[child+1]>a[child]) {//选出左右孩子大的那一个
					++child;
				}

				if (a[child]>a[parent]) {//如果孩子大于父亲交换
					swap(a[child], a[parent]);//交换
					parent = child;//迭代继续向下调整
					child = 2 * parent + 1;//重新计算孩子的下标
				}

				else {//如果父亲不小于孩子节点调整结束
					break;
				}
			}

		   }

void  HeapSort(int* a, int n) {

	for (int i = (n - 2) / 2; i >= 0; i--) {
		 AdjustDown(a, i, n);       
	}
//此处为建堆代码上面下面是实现堆排序
	int end = n - 1;
	while (end>0) {
		swap(a[0], a[end]);
		AdjustDown(a, 0, end);
		end--;
	}
 
}

 2.从下往上调整也就是插入的方式

void AdjustDownUp(T* a, int child) {
			int parent = (child - 1) / 2;//由公式计算父亲节点的下标
			while (child > 0) {//知道调整到只有一个节点
				if (a[child] > a[parent]) {//孩子大于父亲
					swap(a[parent], a[child]);//交换
					child = parent;//重新及计算位置
					parent = (child - 1) / 2;
				}
				else {//调整结束
					break;
				}
			}
		}

		void HeapPush(T x)
		{
			if (_capacity == _size) {
				int newcapacity = _capacity == 0 ? 8 : _capacity * 2;
				T* tmp = new T[newcapacity];

				for (int i = 0; i < _size; i++) {
					tmp[i] = _a[i];//拷数据
				}
				delete[] _a;
				_capacity = newcapacity;
				_a = tmp;
			}

			_a[_size] = x;//放进去
			_size++;
			AdjustDownUp(_a, _size - 1);

		}

 3.建堆的时间复杂度:

····················下面我们来看一道OJ题:

判断数组中所有的数字是否只出现一次_牛客题霸_牛客网 (nowcoder.com)

题目描述:

描述

给定一个个数字arr,判断数组arr中是否所有的数字都只出现过一次。

输入描述:

输入包括两行,第一行一个整数n(1 \leq n \leq 10^5)(1≤n≤105),代表数组arr的长度。第二行包括n个整数,代表数组arr(1 \leq arr[i] \leq 10^7 )(1≤arr[i]≤107)。

输出描述:

如果arr中所有数字都只出现一次,输出“YES”,否则输出“NO”。

示例1

输入:

3
1 2 3

复制输出:

YES

复制

输入:

3
1 2 1

输出:

NO

要求

1.时间复杂度O(n)。2.额外空间复杂度O(1)

常规思路非常的简单使用一个哈西表统计次数 即可,但是题目要求空间复杂度为O(1)。这和上面我们提的建堆的思想有很大的关系,我们只需要在建堆的时候进行检查即可检查的情况分为三种:

1.父亲和父亲的左孩子相等

2.父亲和父亲的右孩子相等

3.父亲和父亲的兄弟节点相等

 对于代码:

#include<iostream>
#include<vector>
using namespace std;
bool AdjustDown(vector<int>&arr,int root,int n){
    int parent=root;
    int child=2*parent+1;
    while(child<n){
        //如果父亲和左右孩子是否相等如果相等就重复了
         if(arr[child]==arr[parent]||(child+1<n&&arr[child+1]==arr[parent])){
            return false;
        }
        //判断旁边的父亲
        if(parent>0&&arr[parent]==arr[parent-1]){
            return false;
        }
        if(child+1<n&&arr[child+1]>arr[child]){
            ++child;
        }
        if(arr[child]>arr[parent]){
            swap(arr[child],arr[parent]);
            parent=child;
            child=2*parent+1;
        }
        else{
            break;
        }
    }
     return true;   
}
int main(){
   int n;
   cin>>n;
    vector<int>arr(n);
    for(int i=0;i<n;i++){
        cin>>arr[i];
    }
    for(int i=(n-2)/2;i>=0;i--){
        bool ret=AdjustDown(arr,i,n);
        if(!ret){
           cout<<"NO";
          return 0;
        }
    }
    cout<<"YES";
    
}

 三 堆的实现

堆的插入了在前面已经说过了,删除也很简单就是将堆顶的元素和最后一个元素交换然后从堆顶向下调整具体请看代码:

#pragma once
#include<algorithm>
#include<iostream>
using namespace std;
namespace ksy {
	template<class T>
	class Heap
	{
	public:
		Heap()
			:_a(nullptr)
			, _size(0)
			, _capacity(0)
		{}

		void AdjustDown(int* a, int n, int parent) {
			int child = 2 * parent + 1;//父亲的左孩子为父亲的小标*2+1;
			while (child < n) {//从上往下调整
				//防止越界
				if (child+1<n &&a[child+1]>a[child]) {//选出左右孩子大的那一个
					++child;
				}

				if (a[child]>a[parent]) {//如果孩子大于父亲交换
					swap(a[child], a[parent]);//交换
					parent = child;//迭代继续向下调整
					child = 2 * parent + 1;//重新计算孩子的下标
				}

				else {//如果父亲不小于孩子节点调整结束
					break;
				}
			}

		   }


		void AdjustDownUp(T* a, int child) {
			int parent = (child - 1) / 2;//由公式计算父亲节点的下标
			while (child > 0) {//知道调整到只有一个节点
				if (a[child] > a[parent]) {//孩子大于父亲
					swap(a[parent], a[child]);//交换
					child = parent;//重新及计算位置
					parent = (child - 1) / 2;
				}
				else {//调整结束
					break;
				}
			}
		}

		void HeapPush(T x)
		{
			if (_capacity == _size) {
				int newcapacity = _capacity == 0 ? 8 : _capacity * 2;
				T* tmp = new T[newcapacity];

				for (int i = 0; i < _size; i++) {//一个一个插入
					tmp[i] = _a[i];//拷数据
				}

				delete[] _a;
				_capacity = newcapacity;
				_a = tmp;
			}

			_a[_size] = x;//放进去
			_size++;
			AdjustDownUp(_a, _size - 1);

		}

		void HeapPop() {
			swap(_a[0], _a[_size - 1]);//把堆顶元素和最后一个元素交换在将最后一个元素删除
			_size--;//个数减一
			AdjustDown(_a, _size, 0);//从堆顶向下调整
		}

		~Heap() {
			_a = nullptr;
			_capacity = _size = 0;
		}

		void PritHeap() {//打印堆
			for (int i = 0; i < _size; i++) {
				cout << _a[i] << " ";
			}
		}
		size_t size() {
			return _size;
		}
		bool empty() {
			return _size == 0;//判空
		}
		T& top() {//返回堆顶的元素
			return _a[0];
		}

	private:
		T* _a;
		int _size;//个数
		int _capacity;//容量
	};
	void test_Heap() {
		int a[] = { 70,56,30,25,15,10,75 };
		Heap<int>t;

		for (auto& e : a) {
			t.HeapPush(e);
		}
		t.HeapPop();
		t.PritHeap();
	}
}

四优先级队列使用及其实现

1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
2. 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元
素)。
3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。,并支持以下操作:

1.empty():检测容器是否为空(检测优先级队列是否为空,是返回true,否则返回
false)
2.size():返回容器中有效元素个数
3.push()插入一个元素

4.pop()弹出一个元素

5.top()返回堆顶的元素

 注意默认情况下优先级队列是大根堆

#include <vector>
#include <queue>
#include <functional> // greater算法的头文件
void TestPriorityQueue()
{
// 默认情况下,创建的是大堆,其底层按照小于号比较
vector<int> v{3,2,7,6,0,4,1,9,8,5};
priority_queue<int> q1;
for (auto& e : v)
q1.push(e);
cout << q1.top() << endl;
// 如果要创建小堆,将第三个模板参数换成greater比较方式
priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
cout << q2.top() << endl;
}

 使用优先级队列排序日期类:

2. 如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载。

class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
void TestPriorityQueue()
{
// 大堆,需要用户在自定义类型中提供<的重载
priority_queue<Date> q1;
q1.push(Date(2018, 10, 29));
q1.push(Date(2018, 10, 28));
q1.push(Date(2018, 10, 30));
cout << q1.top() << endl;
// 如果要创建小堆,需要用户提供>的重载
priority_queue<Date, vector<Date>, greater<Date>> q2;
q2.push(Date(2018, 10, 29));
q2.push(Date(2018, 10, 28));
q2.push(Date(2018, 10, 30));
cout << q2.top() << endl;
}

模拟实现

#include<vector>
#include<iostream>
namespace ksy
{
	//比较方式(使内部结构为大堆)
	template<class T>
	struct less
	{
		bool operator()(const T& l, const T& r)
		{
			return l < r;
		}
	};
	//比较方式(使内部结构为小堆)
	template<class T>
	struct greater
	{
		bool operator()(const T& l, const T& r)
		{
			return l > r;
		}
	};
	//仿函数默认为less
	template<class T, class  Container = std::vector<T>,class Compare=less<T>>
	class priority_queue
	{
	public:
		//向上调整算法,每push一个数都要调用向上调整算法,保证插入后是一个大堆
		void AdjustUp(int child)
		{
			Compare com;
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				if (com(_con[parent],_con[child]))
				{
					std::swap(_con[parent], _con[child]);
					child = parent;
					parent= (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
		//向下调整算法,每次调用pop都要进行向下调整算法重新构成大堆
		void AdjustDwon(int parent)
		{
			Compare com;
			int child = parent * 2 + 1;
			while (child < _con.size())
			{
				if (child + 1 < _con.size() &&com( _con[child] ,_con[child + 1]))
				{
					child++;
				}
				if (com(_con[parent] ,_con[child]))
				{
					std::swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}
		//迭代器初始化
		template<class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
			:_con(first, last)//在这里传迭代器进行初始化
		{
			for (size_t i = 1; i <_con.size(); i++)
			{
				AdjustUp(i);//向上调整建堆
			}
		}

		void push(const T& x)
		{
			_con.push_back(x);

			AdjustUp(_con.size() - 1);
		}
		void pop()
		{
			std::swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			AdjustDwon(0);
		}
		T top()
		{
			return _con[0];
		}
		size_t size()
		{
			return _con.size();
		}
		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

五加强堆的实现

加强堆了是用来弥补堆的一些不足之处,库里面实现的堆只能删除堆顶的元素,不能删除其他位置的元素。因为这样会破坏堆的结构。如果不破坏堆的结构就需要遍历去找这样一来时间复杂度为O(N)效率就降低了很多为了解决这一问题在这里引入加强堆。

加强堆相比普通的堆多了一张反向索引表用来记录每个元素在堆上的位置,有了位置删除和头部删除基本一样,和最后一个元素交换在将最后一个元素删除。然后再从删除的位置向上或者向下调整,注意调整时不仅要交换值反向索引表中的位置也要换。

对于代码:

 

#pragma once
#include<unordered_map>
#include<iostream>
#include<vector>
using namespace std;
//T一定要是非基础类型如果有基础类型需求封装一层
template<class T, class Compare>
class HeapGreater {
public:
	HeapGreater()
		:heapSize(0)
	{}
	bool empty() {
		return heapSize == 0;
	}
	size_t size() {
		return heapSize;
	}
	bool contains(T& obj) {
		return indexMap.count(T);
	}
	T& peek() {
		return heap[0];
	}
	void push(const T& obj) {
		heap.push_back(obj);//插入
		indexMap[obj] = heapSize;//记录位置
		AdjustUp(heapSize++);
	}
	T& pop() {
		T& ans = heap[0];
		Swap(0, heapSize - 1);
		indexMap.erase(ans);//表中的位置也要删除
	
		--heapSize;
		AdjustDown(0);//向下调整
		return ans;
	}
	void  AdjustDown(int root) {
		Compare cmp;
		int parent = root;
		int child = 2 * parent + 1;
		while (child < heapSize) {
			if (child + 1 < heapSize && cmp(heap[child], heap[child + 1])) {
				++child;
			}
			if (cmp(heap[parent], heap[child])) {
				Swap(parent, child);
				parent = child;
				child = parent * 2 + 1;
			}
			else {
				break;
			}

		}
	}
	void resign(const T& obj) {//obj位置的值发生改变
		AdjustUp(indexMap[obj]);//这两个只会发生一个
		AdjustDown(indexMap[obj]);
	}

	void Erase(const T& obj) {
		T& replace = heap[heapSize - 1];
		int index = indexMap[obj];
		indexMap.erase(obj);//删除表中的位置
		
		--heapSize;
		
		if (obj != replace) {//替换他的位置
			heap[index] = replace;
			indexMap[replace] = index;
			resign(replace);
		}
		heap.pop_back();
	}

	void set(const T& obj, const T& target) {
		int index = indexMap[obj];//先取出位置
		heap[index] = target;//
		indexMap.erase(obj);//删除表中位置
		indexMap[target] = index;//重写赋值
		AdjustDown(index);//向上或者向下调整
		AdjustUp(index);
	}
	
	void AdjustUp(int child) {
		Compare cmp;
		int parent = (child - 1) / 2;
		while (child > 0) {
			if (cmp(heap[parent], heap[child])) {
				Swap(parent, child);
				child = parent;
				parent = (child - 1) / 2;
			}
			else {
				break;
			}
		}
	}

	void Swap(int i, int j) {
		T o1 = heap[i];
		T o2 = heap[j];
		heap[i] = o2;
		heap[j] = o1;
		indexMap[o2] = i;
		indexMap[o1] = j;
	}



	//private void heapInsert(int index) {
	//	while (comp.compare(heap.get(index), heap.get((index - 1) / 2)) < 0) {
	//		swap(index, (index - 1) / 2);
	//		index = (index - 1) / 2;
	//	}
	//}

	vector<T>heap;
	unordered_map<T, int>indexMap;
	int heapSize;
	Compare cmp;
};

 

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个追梦的少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值