第17章 二叉树,堆和优先队列(2/3)

来源:互联网 发布:eia数据对美元的影响 编辑:程序博客网 时间:2024/04/28 21:50

2.堆
堆是一种有用的数据结构,常用在排序算法和优先队列中,堆是一个有下面两个属性的二叉树。

  1. 是一个完全二叉树
  2. 每个节点都大于或等于其孩子节点

  上面两条说明了堆的根节点必然是堆中最大的元素,也称为大顶堆,其实也可以创建小顶堆,原理是一样的。创建大顶堆对于排序很方便,假设我们有个长度为N的数组,每趟排序输出最大值,然后与当前数组末尾元素交换,堆的长度减1,再进行下一趟堆排序,直到堆的大小为1时结束,每趟排序时间复杂度为O(logN),进行N-1趟排序,时间复杂度为O(NlogN)。

  对于一个堆,长度为L,从根节点开始层次遍历,根节点序号为0开始递增标号,最后一个节点标号为L-1,有特别属性,对于一个下标为K的节点,其父亲节点(如果存在)下标为(K-1)/2,其左右孩子(如果存在)下标分别为 k*2+1, k*2+2。这样我们可以用一个数组很方便的表示一个堆。下面这个例子可以说明堆如何存储在数组里的,就是层次遍历堆的结果存在了数组里面。

//A heap://                     98(0)//                 /          \//            91(1)            41(2)//          /     \          /       \//       72(3)     61(4)   15(5)      10(6)//     /    \      ///   37(7)  30(8) 41(9)

A heap in an array:

Index 0 1 2 3 4 5 6 7 8 9 Value 98 91 41 72 61 15 10 37 30 41

堆的操作有建堆(create),移除(remove)最大节点,加入一个节点(add)的操作。本书作者Liang教授方法和清华大学严教授的数据结构书上方法不太一样。Liang的建堆过程是逐个加入元素的方法,而严的方法是先构建一个二叉树,从最后一个非叶子节点开始,使得堆逐步有序,我们在这就按照Liang的方法来构建堆,由于是逐个加入元素的过程,所以建堆(create)时要用到加入节点(add)函数。

1.加入一个节点(add)

假设目前有个大顶堆,我们加入一个元素,加入元素是从堆的尾部加入,逐步向上走,如果大于父亲节点就交换元素,如果不大于就停止。

2.建堆(create)

假设我们有个在数组中按照{30, 91, 15, 72, 61, 41, 10, 37, 98 41}顺序存储的原始序列,建堆就是一个一个元素加入堆的过程,每次加入时加在最后一个节点位置,按照Liang的方法我们建立堆的过程如下:

Step 1:        30(0)Step 2:新加入元素91,比30大,交换元素位置        30(0)               91(0)       /         ==>        /      91(1)               30(1)Step 3:新加入元素15,比91小,位置不变        91(0)       /     \       30(1)    15(2)Step 4:新加入元素72,比其父节点元素30大,交换位置,再向上看比其父亲节点元素91小,本次加入元素完成。        91(0)                    91(0)       /     \                 /      \    30(1)    15(2)  ==>      72(1)    15(2)    /                        / 72(3)                      30(3)Step 5: 新加入元素61,比其父节点元素72小,本次加入元素完成。        91(0)              /     \           72(1)    15(2)    /   \                30(3)  61(4)Step 6: 新加入元素41,比其父节点元素15大,交换位置,再向上看比其父亲节点元素91小,本次加入元素完成。          91(0)                      91(0)       /        \                   /       \    72(1)       15(2)  ==>       72(1)      41(2)    /   \       /               /    \      / 30(3) 61(4) 41(5)            30(3) 61(4)  15(5)Step 7: 新加入元素10,比其父节点元素41小,本次加入元素完成。          91(0)                      91(0)       /        \                   /       \    72(1)       41(2)     ==>    72(1)      41(2)    /   \       /   \           /    \      /    \ 30(3) 61(4) 15(5)  41(6)     30(3) 61(4) 15(5)  10(6)Step 8: 新加入元素37,比其父节点元素30大,交换位置,再向上看比其父亲节点元素72小,本次加入元素完成。              91(0)                      91(0)           /        \                   /       \       72(1)        41(2)     ==>    72(1)      41(2)       /   \       /   \            /    \      /    \    30(3) 61(4) 15(5)   41(6)     37(3) 61(4) 15(5)  10(6)    /                             /  37(7)                        30(7)Step 9: 新加入元素98下标为8,比其下标为3的父节点元素37大,交换位置,再向上看比其父亲节点下标为1元素72大,交换位置,再向上看,比其父亲节点下标为0元素91大,交换位置。              91(0)                      91(0)           /        \                   /       \       72(1)        41(2)     ==>    72(1)      41(2)       /   \       /   \            /    \      /    \    37(3) 61(4) 15(5)   41(6)     98(3) 61(4) 15(5)  10(6)    /   \                        /   \  30(7) 98(8)                 30(7)  37(8)              91(0)                      98(0)           /        \                   /       \==>     98(1)       41(2)     ==>    91(1)      41(2)       /   \       /   \            /    \      /    \     72(3) 61(4) 15(5)  41(6)     72(3) 61(4) 15(5)  10(6)    /   \                        /   \  30(7) 37(8)                 30(7)  37(8)Step 10: 新加入元素41,比其父节点元素61小,本次加入元素完成。                       98(0)                   /          \              91(1)            41(2)            /     \          /       \         72(3)     61(4)   15(5)      10(6)       /    \      /     30(7)  37(8) 41(9)     

从以上过程可以看到,每个Step过程都是增加一个元素的过程,对于N个元素,我们调用N次add函数,每个add函数内部也有一层循环,不过这层循环只要循环logN次即可,所以建堆的时间复杂度为O(NlogN)。

3.移除一个元素(remove)

        对于上面的一个堆,我们移除一个元素是移除其顶点,也就是移除最大值,那么根节点就空了出来,我们可以将最后的元素移到根节点的位置,然后自顶向下按照堆的要求遍历一遍,这样就又成为了一个大顶堆。其主要思想是,对于一个移走根节点点的堆,我们需要确定其根节点到底该是由哪个节点填补空缺,而对于整个二叉树来说,其最大值只有可能是顶点的左孩子和右孩子之间得出来,但是左孩子和右孩子大小不好确定,我们需要比较,而比较出来后根节点较大的一棵子树又遇到了填补空缺的问题;另外一方面,原始根节点移走后,整个完全二叉树少了一个节点,所以最后一个节点必然要挪动位子,所以为了确定最后一个节点的位子和填补空缺,可以将最后一个节点移到顶点,再循环一遍使得二叉树成为一个堆。

        向下比较时与孩子中最大的元素比较,若小于最大的元素,则与最大的孩子交换位置,若不小于,则目前已经是一个合法的堆,结束循环。

Step 1: 输出98,最后一个元素41移到根节点位置,再与左右孩子中最大的孩子比较,比1号节点91小,交换位置。                       41(0)                   /          \              91(1)            41(2)              /     \          /       \         72(3)     61(4)   15(5)      10(6)       /    \        30(7)  37(8)  41到了下标为1的位置,再与左右孩子中最大的孩子比较,比3号节点72小,交换位置。                      91(0)                   /          \==>           41(1)            41(2)            /     \          /       \         72(3)     61(4)   15(5)      10(6)       /    \        30(7)  37(8)  41到了下标为3的位置,再与左右孩子比较,左右孩子都小于41,则堆调整完成,是个合法的堆。                      91(0)                   /          \==>           72(1)            41(2)            /     \          /       \         41(3)     61(4)   15(5)      10(6)       /    \        30(7)  37(8)  Step 2: 输出91,最后一个元素37到了0号位置,与左右孩子最大的72比较,小于72,与72交换位置。                      37(0)                   /          \              72(1)            41(2)            /     \          /       \         41(3)     61(4)   15(5)      10(6)       /       30(7)   37到了1号位置后与左右孩子最大的4号节点61比较,比61小,交换位置                        72(0)                   /          \==>           37(1)            41(2)            /     \          /       \         41(3)     61(4)   15(5)      10(6)       /       30(7)    37到了4号位置后没有左右孩子了,完成堆调整。                      72(0)                   /          \==>           61(1)            41(2)            /     \          /       \         41(3)     37(4)   15(5)      10(6)       /       30(7)  Step 3: 输出72, 最后一个元素30到了0号位置,与左右孩子最大的61比较,小于61,与61交换位置                      30(0)                   /          \              61(1)            41(2)            /     \          /       \         41(3)     37(4)   15(5)      10(6) 30与左右孩子中最大的41比较,比41小,交换位置                       61(0)                   /          \==>           30(1)            41(2)            /     \          /       \         41(3)     37(4)   15(5)     10(6)  30已经到了叶子节点,没有孩子,结束堆调整过程。                       61(0)                   /          \==>           41(1)            41(2)            /     \          /       \         30(3)     37(4)   15(5)     10(6)Step 4: 输出61,将10移到0号位置,再调整堆。          10(0)                     41(0)        /      \                    /    \    41(1)      41(2)  ==>      10(1)     41(2)   /    \      /              /    \      /30(3) 37(4) 15(5)            30(3) 37(4)  15(5)              41(0)==>        /       \         37(1)     41(2)        /    \      /      30(3)  10(4) 15(5)Step 5: 输出41,将15移到0号位置,再调整堆             15(0)                      41(0)           /       \                    /     \         37(1)     41(2)   ==>      37(1)    15(2)        /    \                      /     \          30(3)  10(4)               30(3)   10(4)Step 6: 输出41,将10移到0号位置,再调整堆           10(0)                        37(0)         /       \                      /     \       37(1)     15(2)    ==>         10(1)   15(2)      /                              /       30(3)                         30(3)                   37(0)                 /      \    ==>       30(1)     15(2)              /            10(3)Step 7: 输出37,将10移到0号位置,再调整堆        10(0)                     30(0)        /    \        ==>         /   \      30(1)  15(2)              10(1)   15(2)Step 8: 输出30,将15移到0号位置,再调整堆        15(0)        /      10(1)Step 9: 输出15,将10移到0号位置,再调整堆       10(0)Step 10: 输出10

整个过程如上述过程,可以看到输出有序序列过程时间复杂度为O(NlogN)所以堆排序的时间复杂度为O(NlogN),堆排序空间复杂度为O(1),由于建堆是比较费时间的,所以堆排序比较适合大量数据排序,另外要说明一点,堆排序不是稳定排序,从前述例子中的41输出顺序可以看出来,后进入的41先输出了。

堆排序C++代码如下:

辅助函数

//Common.hpp#ifndef Common_hpp#define Common_hpp#include <stdio.h>#include "vector"#include "stack"#include "iostream"using namespace std;template<typename T>struct TreeNode{    T val;    TreeNode *lchild, *rchild;    TreeNode(T i): val(i), lchild(nullptr), rchild(nullptr){};};vector<int> getRand(size_t len,        int max = 100,        unsigned int seed = 0);template <typename T = int>voidprintVector(const vector<T>& vec,            const string& msg = "") {    cout << msg << "\t";    for (auto i: vec) {        cout << i << " ";    }    cout << endl;}#endif /* Common_hpp *///Common.cpp#include "Common.hpp"vector<int> getRand(size_t len,                    int max,                    unsigned int seed){    srand(seed);    vector<int> res(len, 0);    for (size_t i = 0; i < len; i++) {        res[i] = rand() % max;    }    return res;}

类的定义和实现

#ifndef Heap_hpp#define Heap_hpp#include <stdio.h>#include "vector"#include "Common.hpp"using namespace std;template <typename T>class Heap{public:    Heap();    Heap(const vector<T>&);    T remove() throw(runtime_error);    void add(const T&);    size_t getSize();private:    vector<T> vec;};// 默认构造函数template <typename T>Heap<T>::Heap(){}// 接受一个vector的构造函数,并且建堆template <typename T>Heap<T>::Heap(const vector<T>& cvec){    for (size_t i = 0; i < cvec.size(); i++) {        add(cvec[i]);    }}// 移除堆的顶,并返回顶template <typename T>T Heap<T>::remove() throw (runtime_error){    if (vec.empty()) {        throw runtime_error("Heap is empty");    }    T res = vec[0];    vec[0] = vec.back();    vec.pop_back();    size_t len = vec.size();    size_t parentIdx = 0;    while (parentIdx < vec.size()) {        size_t childIdx = parentIdx * 2 + 1;        if (childIdx >= len) {            break;        }        if (childIdx + 1 < len && vec[childIdx] < vec[childIdx + 1]) {            ++childIdx;        }        if (vec[parentIdx] < vec[childIdx]) {            swap(vec[parentIdx], vec[childIdx]);            parentIdx = childIdx;        }        else{            break;        }    }    return res;}// 增加一个元素template <typename T>void Heap<T>::add(const T & val){    vec.push_back(val);    int currentIdx = static_cast<int>(getSize()) - 1;    while (currentIdx > 0) {        int parentIdx = (currentIdx - 1) / 2;        if (vec[parentIdx] < vec[currentIdx]) {            swap(vec[parentIdx], vec[currentIdx]);        }        else{            break;        }        currentIdx = parentIdx;    }}// 得到当前堆大小template <typename T>size_t Heap<T>::getSize(){    return vec.size();}#endif /* Heap_hpp */

测试

#include <stdio.h>#include "string"#include "Heap.hpp"using namespace std;int main(int argc, char* argv[]){    vector<int> ivec = getRand(10, 100, 0);    printVector(ivec, "Original");    Heap<int> myHeap1(ivec);    cout << "Result:\t\t";    while (myHeap1.getSize() != 0) {        cout << myHeap1.remove() << " ";    }    cout << endl;    string names[] = {"George", "Michael", "Tom", "Adam", "Jones", "Peter", "Daniel"};    vector<string> svec(names, names + sizeof(names) / sizeof(names[0]));    printVector(svec, "Original");    Heap<string> myHeap2(svec);    cout << "Result:\t\t";    while (myHeap2.getSize() != 0) {        cout << myHeap2.remove() << " ";    }    cout << endl;    return 0;}/* OutputOriginal    30 91 15 72 61 41 10 37 98 41 Result      98 91 72 61 41 41 37 30 15 10 Original    George Michael Tom Adam Jones Peter Daniel Result      Tom Peter Michael Jones George Daniel Adam Program ended with exit code: 0*/
0 0
原创粉丝点击