1 堆
堆 (heap) 是一种存放数据的结构,其特性保证每次从堆中取出的数据是当前堆中的值最大 (或者最小,取决于你的定义) 的那一个。其他的数据结构如有序链表也能起到类似的作用,但涉及到频繁存取数据时效率不如堆。
1.1 堆的形式和成员
通常使用数组形式表达的二叉树来表示一个堆。堆中的成员可以是基本的数据类型 (整数、浮点数或者字符) 或者自定义的数据类型 (比如结构体对象),但要保证这些值可按照某种规则排序。
当使用堆存取自定义的数据类型 (结构体之类) 时,频繁移动整个数据块的内存显然不是个好主意,但如果只存放它们的指针 (在某个数组内的位置或者内存地址) 的话计算代价通常会小很多。
2 堆的操作集
堆的操作集有 建立堆、删除堆、向堆中存入数据、从堆中弹出成员。
这里用存放整型值的小顶堆 (堆顶元素总是堆中值最小者) 举例,示例展示程序逻辑。
大顶堆的情形,只需改变函数内的一些大小判断关系即可。
当使用堆存取自定义的数据类型 (结构体之类) 时,频繁移动整个数据块的内存显然不是个好主意,但如果只存放它们的指针 (在某个数组内的位置或者内存地址) 的话计算代价通常会小很多。
出于封装的考虑,创建堆时将表示堆的数组与指示堆的当前规模和容量的变量组成一个结构体。
struct node
{
int* body; //存放堆的数组,建堆时再为其分配空间
int size; //当前堆的规模
int capacity; //堆的最大容量
};
typedef struct node* Heap;
Heap NewHeap(int size)
{
Heap heap = (Heap)calloc(1, sizeof(struct node));
heap->body = (int*)calloc(size+1, sizeof(int)); //这里堆顶元素是 body[1]
heap->body[0] = MINVAL; //body[0] 可以放置一个足够小的数作为哨兵,但这不是必须的
heap->size = 0;
heap->capacity = size;
return heap;
}
void DelKit(Heap heap) //相同的结构体除了表示堆还能表示其他的数据结构
{
free(heap->body);
heap->body = NULL;
free(heap);
heap = NULL;
return;
}
向堆中存入数据:
int EnHeap(Heap heap, int tmp) //当然,你可以使用布尔值作为返回值
{
int x;
if(heap->size == heap->capacity) //如果堆满了就无法存入(我们假设执行这一函数前确定堆是存在的)
{
return 1;
}
x = ++heap->size; //先使 size 自增 1,然后预定的插入位置 x 定在增加后的 size 位置
for(; heap->body[x>>1]>tmp && x>0; x>>=1) //如果循环条件里限定 x>=1,哨兵(body[0])不会起作用
heap->body[x] = heap->body[x>>1]; //上滤
heap->body[x] = tmp;
return 0;
}
从堆中取出数据:
int PopHeap(Heap heap) //这里这个函数的返回值是堆中成员的数据类型,你也可以做一些其他的改动
{
int Parent, Child;
int minData, x;
if(heap->size == 0) //空的堆无法取出数据,需要事先定义 EMPTY 的值。这里可以插入提示信息
{
return EMPTY;
}
minData = heap->body[1]; //如果堆中的成员是指针类型而不是具体的数字,这一步就需要做一些调整
x = heap->body[heap->size--]; //取出原来末尾的元素,假设它先移动并覆盖了堆顶元素
for(Parent = 1; Parent<<1 <= heap->size; Parent = Child) //假设 x 在 Parent 的位置
{
Child = Parent<<1;
if( (Child < heap->size) && (heap->body[Child] > heap->body[Child+1]) ) //从两个子节点里挑一个更小的
Child++;
if(x <= heap->body[Child]) //假设 x 在 Parent 的位置,而且 x 比它较小的那个子节点更小
break;
else
heap->body[Parent] = heap->body[Child]; //下滤
}
heap->body[Parent] = x;
return minData;
}
2.1 关于上滤和下滤
从堆中存放和取出数据时都需要经过一系列比较来保证堆是有序的。由于堆的结构使然 (这里提到的堆使用二叉树表示,但也存在其他形式的堆),这种比较不会很频繁,比较次数不会超过堆当前规模以2为底数的对数。
向堆中存入数据时先假定存入的数据在堆的末尾 (相应地,堆自身的规模扩增了 1),然后比较新数据位置作为子节点的子树。对于小顶堆,每一个子树的父节点都要比它的任何一个存在的子节点都要小。在出现新增元素后保证堆的有序性做出的调整就是“上滤”。
堆弹出一个成员后堆顶可以视为空位 (相应地,堆自身的规模缩减了 1),这时既要保证堆的有序又要尽量少地改动成员位置,把弹出成员前在末尾的元素移动到堆顶的空位再调整是个好主意,不过在这里依然是“假装我们已经移动了它”。从上到下检查新的堆顶与它的子节点的大小关系,对于小顶堆,就是把每个子树里最小的那个子节点扶上去,相应地原本在堆顶空置的位置就下沉了。我们管它叫“下滤”,这个空位下沉到合适的位置时就可以把那个原末尾成员搬过去。
2.2 特殊情况——比如这个堆要存取一些封装好的结构体
typedef char wMSG[11];
struct worder
{
wMSG msg;
int rank;
};
typedef struct worder* ordTbl;
那么用于表示消息队列的堆就写成这样:
struct node
{
ordTbl body;
int size;
int capacity;
};
typedef struct node* Heap;
建立和删除堆也只需少量调整即成,甚至向堆中插入元素的函数也只是给堆成员的类型换个名字:
bool EnOrderTable(Heap WOrd, wMSG ch, int r) //是的这是返回值为布尔型的样子,r 是 rank
{
if(WOrd->size == WOrd->capacity)
{
return false;
}
int i = ++WOrd->size;
for(; WOrd->body[i>>1].rank > r; i>>=1)
WOrd->body[i] = WOrd->body[i>>1];
WOrd->body[i].rank = r; //分别插入消息结构体的两个成员
strcpy(WOrd->body[i].msg, ch);
return true;
}
但是从堆中弹出呢?在存放整数的堆中我们简单地用一个变量复制了一份堆顶成员的值,而堆顶成员在随后的下滤过程中被覆盖了。如果在上述的例子中只是简单地用一个临时指针指向堆顶成员,那么在下滤后堆顶成员被覆盖,就无法取出正确的值。
void GetOrderTable(Heap WOrd, ordTbl MinNode) //MinNode 已经在函数外分配了内存,用于复写提取出的堆顶元素
{
if(WOrd->size == 0)
{
printf("EMPTY QUEUE!\n");
return;
}
int Parent, Child;
ordTbl tempNode;
//堆顶元素在下滤后会被覆盖,需要先完整复制
MinNode->rank = WOrd->body[1].rank;
strcpy(MinNode->msg, WOrd->body[1].msg);
//下滤节点在调整前的堆区域外,可直接取地址
tempNode = &WOrd->body[WOrd->heapSize--];
for(Parent = 1; Parent<<1 <= WOrd->heapSize; Parent = Child)
{
Child = Parent<<1;
if( (Child != WOrd->size)
&& (WOrd->body[Child].rank > WOrd->body[Child+1].rank) )
Child++;
if( tempNode->rank <= WOrd->body[Child].rank )
break;
else
WOrd->body[Parent] = WOrd->body[Child]; //下滤
}
WOrd->body[Parent] = *tempNode; //下滤后将节点放入
ShowMessage(MinNode);
return;
}
当然也可以在函数内产生一个新节点用于放置弹出的堆成员,然后返回这个新节点,使函数的返回值仍然是堆成员的数据类型。