堆——最小堆代码实现与分析(C++)

最近跟着黑皮书学习数据结构,主要代码均与书上相仿。代码量有些大,所以全代码之后丢在结尾。先摘抄部分来进行分析吧。

优先队列(堆):

    一种能够赋予每个节点不同优先级的数据结构。有“最小堆”和“最大堆”两种基础类型。实现的根本原理有两种,一种是“数组”,另外一种则是“树”(大多是指二叉树)。但在实现最大/最小堆时,使用数组更优。因为堆并不像树那样需要很多功能支持,自然也不需要用到指针(当然,高级结构还是会用到的,比如“左式堆”等)。

    如果您此前已经看过堆的基本结构概念,那应该大致明白堆长什么样了,基础结构的堆就是一颗符合特定条件的二叉树罢了。

    特定条件:对每一个节点的关键字,都要比其子树中的任何一个关键字都小(任何一个节点的关键字是其子树以及其自身中最小的那个)。这个条件是针对最小堆的,最大堆则反之。

    因为基础的堆结构只支持“插入”和“最小值出堆”这两种操作。在处理任务进程的时候,对应的也有“增加任务”和“处理任务量最少的任务”这种解释,或许这样更容易让人明白堆的作用。而最大堆则可将其解释为“处理优先级最高的任务”。(当然,实际上还需要对任务量/优先级进行变动,包括增/减关键字的大小这样的操作,自然也能够进行特定关键字的删改了)。

//-----------声明部分----------//
struct HeapStruct;
struct ElementType;
typedef struct HeapStruct* PriorityQueue;//堆指针
#define MinElements 1
#define Max 99999
bool IsEmpty(PriorityQueue H);//是否为空堆
bool IsFull(PriorityQueue H);//是否为满堆

void Insert(ElementType key, PriorityQueue H);//插入关键字
ElementType DeleteMin(PriorityQueue H);//删除最小值
PriorityQueue BuiltHeap(ElementType* Key, int N);//成堆
void PercolateDown(int i, PriorityQueue H);
PriorityQueue Initialize(int MaxElements);//建空堆
void IncreaseKey(int P, int Add, PriorityQueue H);//增加关键字值
void DecreaseKey(int P, int sub, PriorityQueue H);//降低关键字值
void Delete(int P, PriorityQueue H);//删除关键字
struct ElementType//关键字数据块
{
	int Key;
};
struct HeapStruct//堆结构
{
	int Capacity;
	int Size;
	ElementType* Element;
};
ElementType MinData;//最小数据块

//-----------声明部分----------//

     看注释大概就能明白了。但值得说明的是,因为最终是通过数组来实现的,而数组必须先行规定好它的尺寸,所以建立的堆也必须面临“被装满”的情况(当然,用new函数重新开辟也行,谁让这是C++呢)

建立空堆Initialize:

PriorityQueue Initialize(int MaxElements)//形参为堆的总节点数
{
	PriorityQueue H;
	if (MaxElements < MinElements)
		return NULL;
	H = new HeapStruct;
	H->Element = new ElementType[MaxElements + 1];
	H->Capacity = MaxElements;
	H->Size = 0;
	H->Element[0] = MinData;
	return H;
}

    注:我并没有把new函数失败的情形写出来,但那些内容并不影响对数据结构的学习。对这方面有需求请自行添加。

    注:MinData是一个最小数据块,同时也只是一个冗余块。在之后的任何操作中,都不会对存有MinData的Element[0]进行任何操作。只是通过占用[0]节点,使得之后的操作变得更加可行了。需要注意的是,这个0节点并不是根节点(当时没绕过来,在这里浪费了太多时间)。

插入Insert:

void Insert(ElementType key, PriorityQueue H)
{
	int i;
	if (IsFull(H))
		exit;
	++H->Size;
	for (i = H->Size; H->Element[i / 2].Key > key.Key; i /= 2)
		H->Element[i] = H->Element[i / 2];
	H->Element[i] = key;
}

    for循环中的判断方式被称之为“上滤”,也是这种方式得以实现的重要规则。对于根节点从 Element[1] 开始的这个堆,Element[i]的左儿子必然是Element[2*i],除非它没有左儿子。

    回到这个函数,因为int型会自动取整舍弃小数位,所以 Element[i/2] 必定指向 Element[i] 的父节点,不论它是不是单数。

    而这个寻路条件则是在不断的比较子节点与父节点的大小。流程如下:

    ①先将新节点放在数组的最后一位(并不是指数组的末尾,而是按照顺序装填的最后一位),然后比较它与父节点的大小。

    ②若它小于父节点,那么将其与父节点交换位置,此时 i/=2 , Element[i]再次指向它。

    ③继续相同操作。直到父节点小于它,或是没有父节点为止。

    不得不承认,这种操作很棒。因为它让函数的最坏时间复杂度降到了logN(因为实际操作中,不一定都要上履到最顶层)。

最小值出堆DeleteMin:

ElementType DeleteMin(PriorityQueue H)
{
	int i, Child;
	ElementType MinElement, LastElement;
	if (IsEmpty(H))
		return H->Element[0];
	MinElement = H->Element[1];
	LastElement = H->Element[H->Size--];
	for (i = 1; i * 2 <= H->Size; i = Child)
	{
		Child = 2 * i;
		if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)
			Child++;
		if (LastElement.Key > H->Element[Child].Key)
			H->Element[i] = H->Element[Child];
		else
			break;
	}
	H->Element[i] = LastElement;
	return MinElement;
}

    同“上滤‘相近,在这个函数中运用的方法为”下滤“。简要谈谈过程吧:

    ①声明各种各样的变量,并判断H是不是一个空堆。

    ②将堆中最小的值Element[1]拷贝到MinElement中,同理将最后一个值放进LastElement中。(这个Element[1]将会被新值替换,而这个LastElement则要用来填补某个空缺)

    ③从i=1开始,Child则指向根节点Element[1]的左儿子,同时比较根节点的左右儿子大小,将Child指向小的那一个,我是说,H->Element[Child]会指向小的那个。

    ④然后再判断最后一个数和 H->Element[Child] 的大小。如果最后一个比较大,那就把父节点Element[i]用它的子节点替代。

    ⑤重新回到循环,现在的 i 已经指向了本来的子节点,并开始重复上述从③开始的操作,直到当前Element[i]的子节点中较小的那一个Element[Child]比最后一个节点的值要小为止。

   ⑥将现在的父节点Element[i]用最后一个替代。

    或许从途中就会觉得有些怪异,这究竟是个怎么回事。

    事实上,经过上述操作直到步骤⑤,最终的Element[i]将会指向某片叶子,这片叶子是根据其上的操作逐层筛选出来的。最后通过

	H->Element[i] = LastElement;

    将这个位置用最后一位来替代,并返回了刚开始拷贝好的最小值,实现了删除最小值的操作。当然,实际上,这个数组的最后一位仍然保存着某个关键字,但并不需要太担心,因为经过了H->Size--,当下次插入节点的时候,遇到合适的数值,将会直接把这个位置覆盖掉。并且,也如您所见,所有的操作单元均在[1,H->Size]的范围内,对于范围外的元素,即便它还留有关键字,也不会再造成影响了。

成堆BuiltHeap:

    通常,我们将会导入一整串数组,然后再利用它们来生成一个堆结构。实际上,当然也可以通过Insert来一个个安置。以下是没套用Insert的例程,主要通过递归来实现。

PriorityQueue BuiltHeap(ElementType *Key,int N)//Key指向将要导入的数据数组
{
	int i;
	PriorityQueue H;
	H = Initialize(N);
	for (i = 1; i <= N; i++)
		H->Element[i] = Key[i - 1];
	H->Size = N;
	for (i = N / 2; i > 0; i--)
		PercolateDown(i, H);
	return H;
}
void PercolateDown(int i,PriorityQueue H)
{
	int MinSon;
	ElementType Tmp;
	if (i <( H->Size / 2))
	{
		if (2 * i + 1 <= H->Size && H->Element[2 * i].Key > H->Element[2 * i + 1].Key)
			MinSon = 2 * i+1;
		else
			MinSon = 2 * i;
		if (H->Element[i].Key > H->Element[MinSon].Key)
		{
			Tmp = H->Element[i];
			H->Element[i] = H->Element[MinSon];
			H->Element[MinSon] = Tmp;
		}
		PercolateDown(MinSon, H);
	}
}

    ①声明,并建立空堆,然后把所有元素全都不按规则的塞进去,再指定好H->Size。

    ②从最后一个具有“父节点”性质的节点进入下滤函数。过程与DeleteMin相近:选出子节点中小的,再与父节点比较,将较小的那一个放在父节点的位置,而较大的那一个下沉到子节点。并且再次进入这个函数。

    ③实现全部的过滤之后,返回H。

    递归在这里是非常好用的。在BuiltHeap函数中,for循环实现了对每一个具有“父节点”性质的节点进行下滤(这是根据数组节点的排列顺序实现的,父节点必然都能按顺序排下去)。而递归则实现了对整条路径的下滤操作。假设从根节点开始下滤,那么必然会进入PercolateDown(MinSon,H)中,将较小的那个子节点作为本次递归的新的父节点同样进行下滤。最终实现了堆序(Heap order)。

    剩下的就是一些无关紧要的函数了,看看思路就行。因为是我自己写的,可能会有错误,如有发现,还请务必告知我,我会尽量修正。

void DecreaseKey(int P,int sub,PriorityQueue H)//降低关键字的值
{
	H->Element[P].Key -= sub;
	int i;
	ElementType Tmp;
	for (i = P; H->Element[i / 2].Key > H->Element[i].Key; i /= 2)
	{
		Tmp = H->Element[i / 2];
		H->Element[i / 2] = H->Element[i];
		H->Element[i] = Tmp;
	}
}
void IncreaseKey(int P, int Add, PriorityQueue H)//提高关键字的值
{
	int i,Child;
	ElementType Tmp;
	H->Element[P].Key += Add;
	for (i = P; 2 * i <= H->Size; i = Child)
	{
		Child = 2 * i;
		if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)
			Child++;
		if (H->Element[i].Key > H->Element[Child].Key)
		{
			Tmp = H->Element[Child];
			H->Element[Child] = H->Element[i];
			H->Element[i] = Tmp;
		}
		else
			break;
	}
}
void Delete(int P,PriorityQueue H)//删除指定关键字
{
	DecreaseKey(P, Max, H);
	DeleteMin(H);
}

    原理同上面的其他函数一样的,建议自己实现一下。

//----------------------最后是完整代码----------------------------//

#include<iostream>
using namespace std;
//------------------------//
struct HeapStruct;
struct ElementType;
typedef struct HeapStruct* PriorityQueue;
#define MinElements 1
#define Max 99999
bool IsEmpty(PriorityQueue H);
bool IsFull(PriorityQueue H);

void Insert(ElementType key, PriorityQueue H);
ElementType DeleteMin(PriorityQueue H);
PriorityQueue BuiltHeap(ElementType* Key, int N);
void PercolateDown(int i, PriorityQueue H);
PriorityQueue Initialize(int MaxElements);
void IncreaseKey(int P, int Add, PriorityQueue H);
void DecreaseKey(int P, int sub, PriorityQueue H);
void Delete(int P, PriorityQueue H);
struct ElementType
{
	int Key;
};
struct HeapStruct
{
	int Capacity;
	int Size;
	ElementType* Element;
};
ElementType MinData;
//-----------------------//
PriorityQueue Initialize(int MaxElements)//形参为堆的总节点数//最小堆
{
	PriorityQueue H;
	if (MaxElements < MinElements)
		return NULL;
	H = new HeapStruct;
	H->Element = new ElementType[MaxElements + 1];
	H->Capacity = MaxElements;
	H->Size = 0;
	H->Element[0] = MinData;
	return H;
}
void Insert(ElementType key, PriorityQueue H)
{
	int i;
	if (IsFull(H))
		exit;
	++H->Size;
	for (i = H->Size; H->Element[i / 2].Key > key.Key; i /= 2)
		H->Element[i] = H->Element[i / 2];
	H->Element[i] = key;
}
bool IsEmpty(PriorityQueue H)
{
	if (H->Size == 0)
		return true;
	else
		return false;
}
bool IsFull(PriorityQueue H)
{
	if (H->Size == H->Capacity)
		return true;
	else
		return false;
}
ElementType DeleteMin(PriorityQueue H)
{
	int i, Child;

	ElementType MinElement, LastElement;
	if (IsEmpty(H))
		return H->Element[0];
	MinElement = H->Element[1];
	LastElement = H->Element[H->Size--];
	for (i = 1; i * 2 <= H->Size; i = Child)
	{
		Child = 2 * i;
		if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)
			Child++;
		if (LastElement.Key > H->Element[Child].Key)
			H->Element[i] = H->Element[Child];
		else
			break;
	}
	H->Element[i] = LastElement;
	return MinElement;
}
void DecreaseKey(int P,int sub,PriorityQueue H)
{
	H->Element[P].Key -= sub;
	int i;
	ElementType Tmp;
	for (i = P; H->Element[i / 2].Key > H->Element[i].Key; i /= 2)
	{
		Tmp = H->Element[i / 2];
		H->Element[i / 2] = H->Element[i];
		H->Element[i] = Tmp;
	}
}
void IncreaseKey(int P, int Add, PriorityQueue H)
{
	int i,Child;
	ElementType Tmp;
	H->Element[P].Key += Add;
	for (i = P; 2 * i <= H->Size; i = Child)
	{
		Child = 2 * i;
		if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)
			Child++;
		if (H->Element[i].Key > H->Element[Child].Key)
		{
			Tmp = H->Element[Child];
			H->Element[Child] = H->Element[i];
			H->Element[i] = Tmp;
		}
		else
			break;
	}
}
void Delete(int P,PriorityQueue H)
{
	DecreaseKey(P, Max, H);
	DeleteMin(H);
}
PriorityQueue BuiltHeap(ElementType *Key,int N)
{
	int i;
	PriorityQueue H;
	H = Initialize(N);
	for (i = 1; i <= N; i++)
		H->Element[i] = Key[i - 1];
	H->Size = N;
	for (i = N / 2; i > 0; i--)
		PercolateDown(i, H);
	return H;
}
void PercolateDown(int i,PriorityQueue H)
{
	int MinSon;
	ElementType Tmp;
	if (i <( H->Size / 2))
	{
		if (2 * i + 1 <= H->Size && H->Element[2 * i].Key > H->Element[2 * i + 1].Key)
			MinSon = 2 * i+1;
		else
			MinSon = 2 * i;
		if (H->Element[i].Key > H->Element[MinSon].Key)
		{
			Tmp = H->Element[i];
			H->Element[i] = H->Element[MinSon];
			H->Element[MinSon] = Tmp;
		}
		PercolateDown(MinSon, H);
	}
}

猜你喜欢

转载自blog.csdn.net/Tokameine/article/details/113805933
今日推荐