数据结构练手05 关于堆的up策略和down策略实现
堆,是一个相当重要的数据结构,它是优先队列,堆排序,Dijkstra等算法的实现保证!
堆的主要特性是:
1、根结点是最大/最小的,而这个主要的区别,就是实现比较操作时是less or greater, 因此可以使用纯虚化 比较接口,把实现放到子类。 附: STL中采用的是模板默认参数的方法实现。
2、需要两个表示大小的变量来标定堆or数组的大小。因为pop操作因让堆的有效长度变小,而数组的长度不变。
3、堆的插入操作一般是插入到数组的末尾,这里最好用vector, 因为它可以在常数时间内尾插入数据且能够动态生长。
1 #include <vector> 2 #include <ostream> 3 using namespace std; 4 template<class T> 5 class HEAP{ 6 protected: 7 vector<T> elements; 8 int totalSize; 9 public: 10 HEAP() : totalSize(0),elements(){} 11 virtual ~HEAP(){} 12 int left(int index){return 2*index+1;} 13 int right(int index){return 2*index+2;} 14 int parent(int index){return (index-1)/2;} 15 16 void upHeapity(int index); 17 void downheapity(int index); 18 19 void makeHeap_up(); 20 void makeHeap_down(); 21 22 T& pop_up(); 23 T& pop_down(); 24 25 void insert_up(const T& x); 26 void insert_down(const T& x); 27 void swap(T& a, T& b){int tmp=a; a=b; b=tmp;} 28 void show(ostream& os) const; 29 void sort(); 30 31 virtual bool comp(int a, int b)=0; 32 };
1 template<class T> 2 class maxHeap : public HEAP<T>{ 3 public: 4 maxHeap() : HEAP<T>(){} 5 ~maxHeap(){}; 6 bool comp(int a, int b) { return elements[a]>elements[b]? true:false;} 7 }; 8 9 template<class T> 10 class minHeap : public HEAP<T>{ 11 public: 12 minHeap() : HEAP<T>(){} 13 ~minHeap(){}; 14 bool comp(int a, int b) { return elements[a]<elements[b]? true: false;} 15 };
要保证这个heapity特性,(假定是讨论最大堆)一般有两种方式:从根到叶(down) 和 从叶到根(up),这两种实现各有优缺点。我们先说下各种操作的文字表述:
<<算法导论>>中采用了down的策略,即比较本结点,左右结点,得到最大值的下标索引,若不是最大索引不是本结点,则交换本结点和最大索引,接着用最大索引实现递归操作。downHeapity保证了该子树满足堆的特性。
1 template<class T> 2 void HEAP<T>::downheapity(int index) 3 { 4 int l = left (index); 5 int r = right(index); 6 int large = index; 7 if(l<totalSize) 8 if(comp(l,index)) 9 large = l; 10 else 11 large = index; 12 if(r<totalSize) 13 if(comp(r,large)) 14 large = r; 15 if(index != large){ 16 swap(elements[index], elements[large]); 17 downheapity(large); 18 } 19 }
StL中采用了是up的策略,即新增加结点时,仅需层层比较和父节点的大小,最多到根节点。up策略满足了从该叶到根的路径满足了堆的特性。
1 template<typename T> 2 void HEAP<T>::upHeapity(int index) 3 { 4 while((index!=0) && (comp(index, parent(index)))){ 5 swap(elements[index], elements[parent(index)]); 6 index = parent(index); 7 } 8 }
另外,在建堆操作中,若采用down策略,则可以从index= lenArray/2 的位置进行downHeapity(index)一直递减到根结点就行。
1 template<class T> 2 void HEAP<T>::makeHeap_down() 3 { 4 totalSize = elements.size(); // 很重要,重新调整堆大小 5 for(int i=totalSize>>1; i>=0; --i){ // 从一半到零 6 downheapity(i); 7 } 8 }
而采用up策略,则从index = lenArray-1的位置递减到 lenArray/2的位置,中间一直调用upHeapity(index)就行。
1 template<class T> 2 void HEAP<T>::makeHeap_up() 3 { 4 totalSize = elements.size(); 5 for (int i=elements.size()-1; i>elements.size()/2; --i) { 6 upHeapity(i); 7 } 8 }
我们对堆进行pop操作时,一般来说要保留根结点的值用于最后的返回。实现中我们还需要将根结点和尾结点的值进行交换。这里继续说下down策略和up策略的做法。
down策略时,由于前一步已经交换了根结点和尾结点,且调整了堆大小的长度,这样,我们就可以从索引为堆大小一半的位置开始,递减到根,一直调用 downheapity(index)就行。
1 template<class T> 2 T& HEAP<T>::pop_down() 3 { 4 T rval = elements[0]; 5 swap(elements[0],elements[totalSize-1]); 6 totalSize--; 7 for(int i=totalSize/2; i>=0; i--){ 8 downheapity(i); 9 } 10 return rval; 11 }
up策略是,就比较麻烦点了,需要将交换后的根结点下放到某个小值的位置,然后再调用次upheapity保证堆特性的满足。这个实现起来比较讨厌,要判断一些边界条件。
1 template<class T> 2 T& HEAP<T>::pop_up() 3 { 4 T rval = elements[0]; 5 swap(elements[0],elements[totalSize-1]); 6 totalSize--; 7 int i=0; 8 int tmp=i; 9 while(left(i)<totalSize){ 10 if( (right(i)<totalSize) && comp(left(i),right(i)) || (right(i)>=totalSize)){ 11 tmp = left(i); 12 } 13 if( (right(i)<totalSize) && (!comp(left(i),right(i)))){ 14 tmp = right(i); 15 } 16 swap(elements[i],elements[tmp]); 17 i = tmp; 18 } 19 upHeapity(tmp); 20 return rval; 21 }
而对于堆排序,则就是遍历调用pop n-1次就行。
1 template<class T> 2 void HEAP<T>::sort() 3 { 4 int t = totalSize; 5 for(int i=0; i<t; ++i){ 6 pop_up(); 7 // pop_down(); 8 } 9 }
对于元素的插入来说,那肯定是StL的up占优了。因为up策略就是从尾结点开始向父节点进行比较,最多比较到根节点。 而若采用down策略,则从堆大小一半的位置递减到根,其中一直调用downheapity(index)操作。
1 template<typename T> 2 void HEAP<T>::insert_up(const T& x) 3 { 4 if(totalSize == elements.size()) 5 elements.push_back(x); 6 else 7 elements[totalSize] = x; 8 totalSize++; 9 upHeapity(totalSize-1); 10 } 11 12 template<typename T> 13 void HEAP<T>::insert_down(const T& x) 14 { 15 if(totalSize == elements.size()) 16 elements.push_back(x); 17 else 18 elements[totalSize] = x; 19 totalSize++; 20 for(int i=totalSize/2; i>=0; i--){ 21 downheapity (i); 22 } 23 }
因此,这里我们可以总结下使用堆时候的策略:
除了插入操作使用up策略外,其他所有的操作都使用down策略。调用down策略,都从一半堆大小的位置开始递减到根。
另外,对于边界条件来说,down策略一般从尾结点的父节点开始,及 parent(totalSize-1), 即 (totalSize-1)/2; 由于下标从0开始,边界条件就很让人纠结,我们宁愿多操作一次,及从total/2位置开始,这样也没啥影响。
- 之前我的总结有问题,今天反思了下,若都从一半堆大小开始递减到根,那么时间复杂度挺大的。其实所有的操作都采取down策略更易理解。
- 对于从(size-1)/2还是size/2开始,我觉得还是从(size-1)/2更好,这样循环代码可以修改下,且也能避免上面所误解的地方
1 insert_down 和 pop_down 中的
/*for(int i=totalSize/2; i>=0; i--){ 2 downheapity(i); 3 }*/ 4 改为: 5 int p = parent (totalSize-1); 6 while( p != parent(p)){ 7 downheapity (p); 8 p = parent(p); 9 } 10 downheapity(p);
使用堆的时候,一定要注意是采用数组长度还是堆长度。我们可以总结下:仅建堆时采用数组长度,同时堆长度等于数组长度; 其余操作使用的都是堆长度,同时要注意调整堆长度水位
测试代码:
1 #include "myHeap.h" 2 #include <iostream> 3 using namespace std; 4 5 int main() 6 { 7 maxHeap<int> mh; 8 mh.insert_down(170); 9 mh.insert_down(20); 10 mh.insert_down(30); 11 mh.insert_down(1); 12 mh.insert_down(7); 13 14 mh.insert_down(10); 15 mh.insert_down(90); 16 17 cout << mh << endl; 18 cout << "--------" << endl; 19 mh.sort(); 20 cout << mh << endl; 21 mh.makeHeap_up(); 22 cout << mh << endl; 23 }