模拟实现堆算法,以及堆应用 (Top k问题)

堆数据结构是一种数组对象,它可以被视为一颗完全二叉树结构
最大堆:每个父节点都大于孩子结点
最小堆:每个父节点都小于孩子结点

堆的优势:效率高 增加删除的时间复杂度均为O( lg N),不用堆vector插入删除总有一个为O(1),一个为O(N)
堆的基本实现:

我们先来建个 最大堆,首先堆的成员是个数组,而且这个数组不能是固定的大小,因为我们还要实现堆的插入删除,因此我们可以用库中的vector来创建数组。
最大堆:

#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;
template <class T>
class Heap
{
public:
	Heap()//无参构造,缺省构造  并非什么都不做,初始化列表写不写都会初始化,整形初始化为随机值,一般为0.   此处去掉vector的缺省构造函数
	{};
	Heap(T* a, size_t n)
	{
		_a.reserve(n);//开空间  用_resize()也可以就要配合赋值来用,因为_resize是已经初始化了的
		for (size_t i = 0; i < n; ++i)
		{
			_a.push_back(a[i]);
		}
		//建堆  找最后一个非叶子结点,依次向下调整
		for (int i = (_a.size() - 2) / 2; i >= 0; i--)//如何找最后一个非叶子结点  即(最后一个结点下标-1)/2,即(_a.size()-2)/2
		{  //注意此处不能用size_t会导致死循环

			Adjustdown(i);
		}
	}
	void Adjustdown(int root)
	{
		int parent = root;
		int child = parent * 2 + 1;//默认左孩子
		while (child<_a.size())//结束条件有两个  1.左孩子不存在(当然右孩子不存在)完全二叉树2.父亲大于大的那个孩子
		{
			if (child + 1 < _a.size() && _a[child + 1] > _a[child])
			{
				child = child + 1;
			}
			//此时默认child指向大的那个孩子   这里不关心左孩子大还是右孩子大,只关心child指向的是最大的孩子
			if (_a[child] > _a[parent])//如果孩子大于父亲则交换,否则跳出循环
			{
				swap(_a[child], _a[parent]);
				parent = child;
				child = 2 * parent - 1;
			}
			else//父亲大于大的那个孩子
			{
				break;
			}
		}
	}
	void Adjustup(int Child)//向上调整
	{
		int child = Child;
		int parent = (child - 1) / 2;
		while (parent>=0)//或者child>0都可以
		{

			if (_a[child] > _a[parent])
			{
				swap(_a[child],_a[parent]);
				child = parent;
				parent = (child - 1) / 2;
			}
			else
			{
				break;
			}
		}

	}
	bool  IsHeap()//递归判断大堆
	{
		int _root = 0;
		return _IsHeap(_root);
	}

	bool _IsHeap(int root)
	{
		if (root >= _a.size())//不存在  是大堆
			return true;//不存在是大堆   否则叶子结点怎样判断都不是堆
		int left = root * 2 + 1;
		int right = root * 2 + 2;
		if (left < _a.size())
		{
			if (_a[left]>_a[root])
			{
				return false;
			}
			if (right < _a.size())
			{
				if (_a[right]>_a[root])
				{
					return false;
				}
			}
		}
		
		return _IsHeap(left) && _IsHeap(right);
	}

	bool IsHeapNor()//非递归判断是否为大堆
	{
		for (size_t i = 0; i < (_a.size() - 2) / 2; i++)
		{
			if (_a[i] < _a[i * 2 + 1] || _a[i] < _a[i * 2 + 2])
			{
				return false;
			}
		}
		return true;
	}
	void Push(const T& x)//向上调整,会影响本结点到根节点路径上上的所有结点,不会影响其他
	{
		_a.push_back(x);
		Adjustup(_a.size() - 1);//注意此处一定要减1,否则报错
	}
	void Pop()
	{
		swap(_a[_a.size() - 1], _a[0]);
		_a.pop_back();
		Adjustdown(_a[0]);
	}
	size_t Size()
	{
		return _a.size();
	}
	bool Empty()
	{
		return _a.pop();
	}
	T& Top()
	{
		assert(!_a.empty());
		return _a[0];
	}
private:
	vector<int> _a;
};
void test()
{

	int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
	Heap<int>hp(a, sizeof(a) / sizeof(a[0]));
	cout << hp.IsHeap() << endl;
    
}
int main()
{
	test();
	system("pause");
	return 0;
}

建堆时间复杂度:O(NlgN)
Push  时间复杂度:O(lgN)  Pop时间复杂度:O(lg N)
用仿函数建立大堆或者小堆     (实现复用)

#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;
template <class T>
struct Less
{
	bool operator() (T&left, T&right)
	{
		return left < right;
	}
};
template <class T>
struct Greater
{
	bool operator() (T&left, T&right)
	{
		return left >right;
	}
};
template <class T,class Compare=Greater<T>>
class Heap
{
public:
	Heap()//无参构造,缺省构造  并非什么都不做,初始化列表写不写都会初始化,整形初始化为随机值,一般为0.   此处去掉vector的缺省构造函数
	{};
	Heap(T* a, size_t n)
	{
		_a.reserve(n);//开空间  用_resize()也可以就要配合赋值来用,因为_resize是已经初始化了的
		for (size_t i = 0; i < n; ++i)
		{
			_a.push_back(a[i]);
		}
		//建堆  找最后一个非叶子结点,依次向下调整
		for (int i = (_a.size() - 2) / 2; i >= 0; i--)//如何找最后一个非叶子结点  即(最后一个结点下标-1)/2,即(_a.size()-2)/2
		{  //注意此处不能用size_t会导致死循环

			Adjustdown(i);
		}
	}
	void Adjustdown(int root)
	{
		Compare com;
		int parent = root;
		int child = parent * 2 + 1;//默认左孩子
		while (child<_a.size())//结束条件有两个  1.左孩子不存在(当然右孩子不存在)完全二叉树2.父亲大于大的那个孩子
		{
			if (child + 1 < _a.size() && com(_a[child + 1], _a[child]))
			{
				child = child + 1;
			}
			//此时默认child指向大的那个孩子   这里不关心左孩子大还是右孩子大,只关心child指向的是最大的孩子
			//if (_a[child] > _a[parent])//如果孩子大于父亲则交换,否则跳出循环
			if (com(_a[child], _a[parent]))
			{
				swap(_a[child], _a[parent]);
				parent = child;
				child = 2 * parent - 1;
			}
			else//父亲大于大的那个孩子
			{
				break;
			}
		}
	}
	void Adjustup(int Child)//向上调整
	{
		Compare com;
		int child = Child;
		int parent = (child - 1) / 2;
		while (parent>=0)//或者child>0都可以
		{

		//if (_a[child] > _a[parent])
			if (com(_a[child], _a[parent]))
			{
				swap(_a[child],_a[parent]);
				child = parent;
				parent = (child - 1) / 2;
			}
			else
			{
				break;
			}
		}

	}
	bool  IsHeap()//递归判断大堆
	{
		int _root = 0;
		return _IsHeap(_root);
	}

	bool _IsHeap(int root)
	{
		Compare com;
		if (root >= _a.size())//不存在  是大堆
			return true;//不存在是大堆   否则叶子结点怎样判断都不是堆
		int left = root * 2 + 1;
		int right = root * 2 + 2;
		if (left < _a.size())
		{
			//if (_a[left]>_a[root])
			if (com(_a[left], _a[root]))
			{
				return false;
			}
			if (right<_a.size())
			{
				//if (_a[right]>_a[root])
				if (com(_a[right],_a[root]))
				{
					return false;
				}
			}
		}
		
		return _IsHeap(left) && _IsHeap(right);
	}

	bool IsHeapNor()//非递归判断是否为大堆
	{
		Compare com;
		for (size_t i = 0; i < (_a.size() - 2) / 2; i++)
		{
			//if (_a[i] < _a[i * 2 + 1] || _a[i] < _a[i * 2 + 2])

			if (com(_a[i * 2 + 1], _a[i]) || com(_a[i * 2 + 2], _a[i]))
			{
				return false;
			}
		}
		return true;
	}
	void Push(const T& x)//向上调整,会影响本结点到根节点路径上上的所有结点,不会影响其他
	{
		_a.push_back(x);
		Adjustup(_a.size() - 1);//注意此处一定要减1,否则报错
	}
	void Pop()
	{
		swap(_a[_a.size() - 1], _a[0]);
		_a.pop_back();
		Adjustdown(_a[0]);
	}
	size_t Size()
	{
		return _a.size();
	}
	bool Empty()
	{
		return _a.pop();
	}
	T& Top()
	{
		assert(!_a.empty());
		return _a[0];
	}
private:
	vector<int> _a;
};
void test()
{

	int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
	/*Heap<int>hp(a, sizeof(a) / sizeof(a[0]));
	cout << hp.IsHeap() << endl;*/
	Heap<int, Less<int>>hp(a, sizeof(a) / sizeof(a[0]));
	cout << hp.IsHeap() << endl;
}
int main()
{
	
	test();
	system("pause");
	return 0;
}

堆的应用:

1.TopK问题  N个数,找出最大的前K个。
思想:
如果我们的N非常大,内存是放不下的

建立一个K大小的堆,如果建立最大堆,那么只能找到最大的一个,我们要建立小堆 

方法:

用前N个数建立一个小堆,从第N+1个开始,如果比堆顶的数据大,替代堆顶数据(因为堆顶是K个元素中最小的),并且向下
调整,最后堆里面就是最大的前K个元素。
这种问题就是要用堆来解决,有的人肯定会说排序就可以解决,

我们来看下堆处理和排序处理的优缺点:
目前最快的排序:时间复杂度   O(N*lgN)                                         空间复杂度O(N)

 堆:  时间复杂度  建堆K*lgK  +(N-K)*lgK=O(N*lg K)     空间复杂度  O(1)

其实仅仅看时间复杂度排序和堆是差不多的,但是空间复杂度就相差很多了。试想,如果有10亿个元素,用排序内存根本放不下

具体实现代码如下:

#include<iostream>
#include<assert.h>
using namespace std;
const size_t M = 1000;
const size_t K = 5;
void Adjustdown(int* Heap, int n, int pos)//n代表堆的大小,pos代表位置,表示调整哪个
{
	assert(Heap);
	int parent = pos;
	int child = parent * 2 + 1;
	while (child < n)
	{
		if ((child + 1 < n) && (Heap[child + 1] < Heap[child]))
		{
			child = child + 1;
		}
		//此处child表示最大的孩子
		if (Heap[child] < Heap[parent])
		{
			swap(Heap[child], Heap[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void TopK()
{
	int arr[M] = { 0 };
	for (int i = 0; i < M; i++)
	{
		arr[i] = rand() % M;//这些数都小于M
	}
	//待会打印的前K个元素,就应该是下面这5个。
	arr[0] = 1111;
	arr[99] = 1911;
	arr[987] = 1023;
	arr[100] = 9999;
	arr[235] = 5678;
	//建一个K个元素的堆
	int Heap[K] = { 0 };
	for (size_t i = 0; i < K; i++)//用前K个元素建堆
	{
		Heap[i] = arr[i];
	}
	//建堆
	for (int i = (K-2)/2; i >=0; i--)//用int 否则死循环
	{
		Adjustdown(Heap, (K - 2) / 2,i);
	}
	for (size_t i = K; i < M; i++)//用K+1个元素之后的元素和堆顶比较,如果大于堆顶,交换然后调整
	{
		if (arr[i]>Heap[0])
		{
			Heap[0] = arr[i];
			Adjustdown(Heap, K, 0);
		}
	}
	for (size_t i = 0; i < K; i++)
	{
		cout << Heap[i] << " ";
	}
}
int main()
{
	TopK();
	system("pause");
	return 0;
}

2.堆排序(升序)
升序:建立最大堆,然后最大的和最后一个结点进行交换,然后size--,然后再用次大的和/size-1交换,依次进行

升序:建立最小堆,将最小的和0位置结点进行交换,但是根节点变了,又要重新建堆,之前建的堆用不上,因此只能排好一个           元素,所以升序就要建大堆。


void Adjustdown(int* Heap, int n, int pos)//n代表堆的大小,pos代表位置,表示调整哪个
{
	assert(Heap);
	int parent = pos;
	int child = parent * 2 + 1;
	while (child < n)
	{
		if ((child + 1 < n) && (Heap[child + 1] > Heap[child]))
		{
			child = child + 1;
		}
		//此处child表示最大的孩子
		if (Heap[child]>Heap[parent])//建立大堆
		{
			swap(Heap[child], Heap[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapSort(int *a, size_t n)
{

	//建堆,升序建大堆
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		Adjustdown(a, n, i);
	}
	int end = n - 1;
	while (end >= 0)
	{
		swap(a[0], a[end]);
		Adjustdown(a, end, 0);
		end=end-1;
		
		
	}
}
int main()
{
	int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
	HeapSort(a, sizeof(a) / sizeof(a[0]));
	for (int i = 0; i <sizeof(a) / sizeof(a[0]); i++)
	{
		cout << a[i] << " ";
	}
	system("pause");
	return 0;
}
堆排序时间复杂度:建堆NlgN +调整NlgN =O(lgN).

3.优先级队列
(谁的优先级高,谁先出)用堆实现
template<class T,class Compare=Greater<int>>//注意栈是用vector适配的,优先级队列是用堆算法完成  不是适配器 库里面没有叫堆的容器,但是有叫堆的算法
class PriorityQueue
{

	void Push(const T& x)
	{
		_hp.Push(x);
	}
	void Pop()
	{
		_hp.Pop();
	}
	T& Top()
	{
		return _hp.Top();
	}
private:
	Heap<T, Compare> _hp;
};
template<class T, class Container>//这里container默认vector但是给list也可以适配        这时适配器适配的
class Stack
{};
要区分清楚,优先级队列不是用适配器适配的,是用堆实现的写死了,不能适配

猜你喜欢

转载自blog.csdn.net/baidu_37964044/article/details/79836701