数据结构与算法_堆

堆的概念

概念

​ 堆是在概念上是一个完全二叉树,在具体是物理实现上是一个数组。完全二叉树有个一优美的性质:有关双亲结点和孩子结点,由层序遍历对应于数组标号直接按的关系。说人话就是:把完全二叉树的层序遍历的一个设置为1号(不是 0 开头),我们发现一个节点编号为的 i 的左孩子标号一定是 2i ,其右孩子标号一定是 2i+1。

​ 利用这样的性质,我们可以用来实现优先队列。(用普通的树也是可以实现的,按照邓老师的说法就是:杀鸡用牛刀)

大顶堆和小顶堆

​ 所谓大顶堆就是,堆中最大的元素是在根的位置,任意子树的所有节点都小于等于根节点。小顶堆也是同理。

堆的操作

上浮(向上调整)

不停的和双亲节点比较。(大顶堆)把比双亲大的孩子节点换到双亲节点的位置上。一直这样比较,直到到达堆顶。

这里解释一个疑惑: 比较是从下往上比较,我们找最后一个非叶子节点。此时这个非叶子节点的左右孩子一定只有一个节点。通过完全二叉树的性质我们可以知道:叶子节点有 n/2(向上取整)。所以数组下标[1, n/2(向下取整)]就都是非叶子节点。我们就可以倒着对这些非叶子节点做处理。

void UpAdjust(int low, int high)
{
	int i = high, j = i / 2;
	while(j >= low)
	{
		if(heap[j] < heap[i])
		{
			swap(heap[j], heap[i]);
			i = j;
			j = i / 2;
		}
		else 
			break;
	} 
}

下沉(向下调整)

下沉和上浮在另一视角下是相反的,下沉是节点和自己的左右孩子比较。(上浮是自己和自己的双亲节点比较)

同时两者的时间复杂都是O(log n)

void DownAdjust(int low, int high)
{
	int i = low, j = i * 2;
	while(j <= high)
	{
		if(j + 1 <= high && heap[j+1] > heap[j])
			j = j + 1;
		if(heap[j] > heap[i])
		{
			swap(heap[j], heap[i]);
			i = j;
			j = i * 2;
		}
		else
			break;
	}
	
}

具体来说,这个上浮和下沉操作具体的怎么使用呢?
对于堆来说,插入 和 删除操作都是从最后一个元素开始。当我们插入元素,元素需要上浮(新的元素可能不符合规则)。当我们删除元素,元素需要下沉(新的根节点可能不符合要求)

建堆

或许你会使用反复执行insert()来建堆,这样做没有问题,只是效率还不是很理想;
O(log1 + log2 + … + logn) = O(log n!) = O(nlogn)
这里介绍一种自底向上建堆的方式:Floyd算法
其实代码非常简单。

void heapify(int n)
{
   for(int i = LastInternal(n); InHeap(n,i);i--)
   	UpAdjust(n,i);
}

其中 InHeap(n,i) 判断优先队列是否合法(其实就是i < n && i > -1);LastInternal(n)返回了n的祖父结点。

Code(C/C++)

#include <iostream>
#include <algorithm> 
using namespace std;

const int max_size = 1000;
int heap[max_size] = {0, 2, 9, 7, 4, 1, 8, 3, 6, 5};
int n = 10;
void DownAdjust(int low, int high);
void CreateHeap();
void DeleteTop();
void UpAdujust(int low, int high);
void Insert(int x);
void HeapSort();
void Print();

int main()
{
	CreateHeap();
//	cout<<"main"<<endl;
	Print();
	Insert(11);
	Insert(15);
	Insert(19);
	Insert(12);
	HeapSort();
	Print();
	DeleteTop();
	HeapSort();
	Print();
	return 0;
} 
void Print()
{
	for(int i = 0; i < n; i++)
		cout<<heap[i]<<" ";
	cout<<endl;
}
void DownAdjust(int low, int high)
{
	int i = low, j = i * 2;
	while(j <= high)
	{
		if(j + 1 <= high && heap[j+1] > heap[j])
			j = j + 1;
		if(heap[j] > heap[i])
		{
			swap(heap[j], heap[i]);
			i = j;
			j = i * 2;
		}
		else
			break;
	}
	
}
void CreateHeap()
{
	for(int i = n/2; i >= 1; i--)
		DownAdjust(i,n);
}
void DeleteTop()
{
	heap[1] = heap[n--];
	DownAdjust(1,n); 
}
void UpAdjust(int low, int high)
{
	int i = high, j = i / 2;
	while(j >= low)
	{
		if(heap[j] < heap[i])
		{
			swap(heap[j], heap[i]);
			i = j;
			j = i / 2;
		}
		else 
			break;
	} 
}
void Insert(int x)
{
	heap[++n] = x;
	UpAdjust(1,n);
}
void HeapSort()
{
	CreateHeap();
//	cout<<"HeapSort"<<endl;
	for(int i = n; i > 1; i--)
	{
		swap(heap[i], heap[1]);
//		cout<<"HeapSortFor"<<endl;
		DownAdjust(1, i-1);
	}
	
}

优先队列

概念

优先队列,理解上来说是一种队列。但是相比于普通的队列,优先队列并不是按照先进先出的条件出队,而是根据某一种属性,即在队列存在一种排序机制,使得元素的属性可以按照我们规定的一种顺序所排列。这个属性我们把他称之为优先级(实际上优先级的具体概念是程序员根据需求所赋予的)

实现

优先队列的实现是通过堆来实现的,也可以使用树来实现(不过性能来说不如堆)

具体实现按照堆的方式排序即可。可以按照不同的思路适当增加自己需要的接口。

STL对于堆和优先队列的接口

STL——heap

概念

在STL中也封装的了堆这一个数据结构。但是这个heap并没有独立成为一个头文件,而是在<algprithm>中。其主要有以下四个方法 :

make_heap();

pop_heap();

push_heap();

sort_heap();

make_heap()

make_heap():有三个参数 (1)数组(向量)的起点,(2)数组(向量)的结束(3)选择大顶堆还是小顶堆

int arr[] = {1, 3, 2, 5 ,4};
vector<int> v(arr,arr+5);

make_heap(v.begin(), v.end(),less<int>());
//默认是大顶堆,所以less<int>()不写也是可以的。
//大顶堆是 greater<int>()
make_heap(arr, arr+5, greater<int>());
push_heap()

push_heap() : 这个方法可能并不是大家想的和vector的push_back一样的类型,而是先要在数组(向量)中把元素加入和,在用和make_heap()基本一样的方式调用

make_heap(v.begin(),v.end(),less<int>());
v.push_back(12);
push_heap(v.begin(),v.end(),less<int>());
pop_heap()

pop_heap():本身并不pop元素,仅仅是做了一个交换。然后针对前n-1个元素使用了make_heap()。所以要自己手动pop元素

make_heap(v.begin(), v.end(), less<int>());
pop_heap(v.begin(), v.end(), less<int>());
v.pop_back();
sort_heap()

sort_heap:对其进行堆排序,但是这样会使堆失去堆的性质

sort_heap(arr1.begin(), arr1.end(), greater<int>());
Print(arr1);

priority_queue

在STL中<queue>中已经有大神帮我们实现的优先队列。

操作接口

top : 访问队头元素

empty : 队列判空

size :返回元素个数

push:插入元素(插入的使用会自动排序)

emplace:构造一个元素并插入队列

pop : 弹出队头

swap : 交换内容

使用
#include <iostream>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
	priority_queue<int, vector<int>,greater<int> > Q;
	priority_queue<int, vector<int>,less<int> > q;
	int array[] = {2, 1, 5, 7, 9, 0, 2};
	for(int i = 0; i < 7; i++)
		Q.push(array[i]);
	while(!Q.empty())
	{
		cout<<Q.top()<<" "; 
		Q.pop();
	}

	return 0; 
} 

其中 greater<int> 是从小到大,less<int> 是从大到小,如果有特别的需求可以自己写cmp比较函数。

关于cmp比较函数

这里的cmp函数只有一个推荐写法(如果你不打算用默认的话)。

struct node{
	int value;
	int age;
};
struct cmp{
	bool operator()(const node &a, const node &b)const
	{
		return a.value < b.value;
	}
		 
} ;
int main()
{
	priority_queue<node, vector<node>, cmp> Q;
	/*  .......  */
	return 0;
}

还有一种方式是重载友元的 < 运算符,这个办法有一个问题:没办法写两个不同的cmp。因为重载会覆盖。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值