到目前为止,我所知道的堆有两种,一是内存的一种,常见的用途就是动态内存分配了(在c/c++中就是这样),另一种是这里所要论述的一种数据结构。
一、堆
数据结构中的堆又叫二叉堆,顾名思义,我们可以把它看成一颗完全二叉树,每个元素最多有2个孩子,分别看做左孩子和右孩子。根据元素和它的孩子的关系,堆又可以分为最大堆和最小堆。最大堆的性质是父母的值不小于左右(如果都有)的值,相应地,最小堆就是父母的值不大于孩子的值。
和二叉树的表示法类似,堆既可以顺序存储,也可以链式存储。这里我使用顺序存储。链式存储要涉及到指针操作,相对来说会稍微麻烦一点。既然用数组存储,那么我们就要了解顺序存储下二叉树的父节点和孩子节点的下标关系。
Parent(i) = i/2; left(i) = 2i+1; right(i) = 2i+2
注意i是从0开始的,当i是从1开始计数时,关系略有不同。
堆的两个很常见的应用是堆排序和优先级队列,优先级队列在一些算法(如最小生成树算法)中会用到。在对堆进行操作的时候,比如插入元素或删除元素,我们都要对堆进行调整,以确保更改后堆的性质没有被破坏,即这个数据结构还是一个堆。如何保持堆的性质呢?
以最大堆为例,假设堆存储在数组array中,待调整的元素的下标为i,且其左右子树都是最大堆。那么我们进行如下操作:
1.找出array[i]和array[left(i)]、array[right(i)](如果都存在)中的最大值,将其下标记为largest,如果array[i]为最大值,那么以它为根的子树已经是最大堆,程序结束。否则,array[i]的某个子节点为最大值,那么将array[i]和这个孩子交换,然后进入步骤2
2.下标为largest的子节点在交换后的值为array[i],以它为根的子树可能违背堆的性质,所以对该子树执行步骤1.
在下面的函数里面,我使用Heap_Type表示堆是最大堆还是最小堆,并据此执行不同的操作。
template<typename T> void heapify( T *array, const int length, int i, Heap_Type type ) { if( i < length ) { int extre_ele_index; extre_ele_index = i; if( i*2+1 < length )//左孩子 { if( type == MAX_HEAP ) { if( array[extre_ele_index] < array[ i*2+1 ] ) extre_ele_index = i*2+1; }else{ if( array[extre_ele_index ] > array[ i*2+1 ] ) extre_ele_index = i*2+1; } } if( i*2+2 < length )//右孩子 { if( type == MAX_HEAP ) { if( array[extre_ele_index] < array[ i*2+2 ] ) extre_ele_index = i*2+2; }else{ if( array[extre_ele_index ] > array[ i*2+2 ] ) extre_ele_index = i*2+2; } } if( extre_ele_index != i ) { T extre_ele; extre_ele = array[i]; array[i] = array[extre_ele_index]; array[extre_ele_index] = extre_ele; heapify( array, length, extre_ele_index, type ); } } }
有了上面的子程序,我们就可以进行建堆了。
将堆array当成一个完全二叉树,我们需要从这个完全二叉树的第一个非叶子节点开始,依次对array中的元素进行调整,直至array的第一个元素,也就是这个完全二叉树的根。从第一个非叶子节点开始调整的原因在于叶子节点没有孩子,那么显然它也就满足堆的性质,也就没有调整的必要了。
template<typename T> void build_heap( T *array, const int length, Heap_Type type ) { int i; for( i = length/2; i >= 0; --i ) { heapify( array, length, i, type ); }//注意第一个非叶子节点的序号 } //注意数组中第i(从0开始)个元素的左孩子和右孩子的序号。
这样我们就建成一个堆了。
二、堆排序
堆排序,顾名思义,就是基于堆的排序。因为堆的性质(父母不大于或不小于孩子的值),所以堆的第一个元素(或者说完全二叉树的根)总是堆中的最(大、小)值。堆排序就是利用堆的这个特点,过程可以描述如下:
1.将第一个元素与堆的最后一个元素交换。
2.将堆的大小减一
3.被交换到第一个位置的新值可能违反堆的性质,所以调用heapify以保持堆。
4.重复1、2、3,直至堆只剩下1个元素。
因为步骤一将第一个元素放入排序后它应该放入的位置,所以它每执行一次,数组中就有一个元素被排好序,堆的大小自然减一。
//Sort_By表示升序排列还是降序排列.
template<typename T> void heap_sort( T *array, const int length, Sort_By type ) { int i; T extre_ele; if( type == ASCEND ) build_heap( array, length, MAX_HEAP ); else build_heap( array, length, MIN_HEAP );//首先建堆,保证第一个元素为极值。 for( i = length-1; i >= 1; --i )//i表示尚未排好序的子数组的最后元素的下标。 { extre_ele= array[0]; array[0] = array[i]; array[i] = extre_ele; if( type == ASCEND ) heapify( array, i, 0, MAX_HEAP ); else heapify( array, i, 0, MIN_HEAP ); } }
三、优先级队列
优先级队列分为最大优先级队列和最小优先级队列,分别基于最大堆和最小堆。作为队列的一种,优先级队列首先要有出队、入队、取队首、判空等基本操作,所不同的是对于改变队列的操作(出队、入队),我们要进行上面所说的调整来保证队列仍然是一个堆。
入队操作和heapify操作正好相反,heapify是从上至下调整,入队操作却是从下向上进行调整,将新入队的元素放在合适的位置上。
1.因为入队前,队列已经是一个堆了,所以入队的时候,只需要将它与父母比较,如果不合堆的性质,则将它与父母对换,然后在新的位置上与父母比较,直至它和父母的关系满足堆的性质。
template <typename T> void Priority_Queue<T>::enqueue( T *ele ) { if( heap_size+1 > max_size )//堆已满,需重新分配 { T *old_heap = heap; heap = new T[ max_size*2 ]; for( int i = 0; i < heap_size; ++i ) heap[i] = old_heap[i]; delete old_heap; old_heap = NULL; } heap[heap_size] = *ele; int cur_index = heap_size; int par_index = parent( heap_size ); ++heap_size; if( type == Minimum_Priority_Queue ) while( par_index >= 0 && heap[cur_index] < heap[par_index] ) { T tmp = heap[cur_index]; heap[cur_index] = heap[par_index]; heap[par_index] = tmp; cur_index = par_index; par_index = parent( cur_index ); } else//最大优先级队列. { while( par_index >= 0 && heap[cur_index] > heap[par_index] ) { T tmp = heap[cur_index]; heap[cur_index] = heap[par_index]; heap[par_index] = tmp; cur_index = par_index; par_index = parent( cur_index ); } } }
出队操作和堆排序进行的操作几乎完全相同。堆排序每次迭代都是将第一个元素与最后一个元素对调,然后将堆大小减一,最后调用heapify保持堆的性质。而出队操作是将最后一个元素赋值给第一个元素,堆大小减一,调用heapify保持堆的性质。哈哈,看到没,仅有的一点不同就是一个是对调,一个是赋值。
template <typename T> void Priority_Queue<T>::dequeue( T *ele ) { heap[0] = heap[heap_size-1]; --heap_size; if( type == Minimum_Priority_Queue ) heapify( heap, heap_size, 0, MIN_HEAP ); else heapify( heap, heap_size, 0, MAX_HEAP ); }
优先级队列主要的操作就是上面两个了,其他就简单一些了。
程序文件在我的资源里面,地址链接:点击打开链接