数据结构中的堆和程序语言中的堆的意义并不相同,这里的堆是一种数据结构。二叉堆可以分为最大堆和最小堆,这里介绍最大堆。在最大堆中,结点的值满足堆的性质:除了根节点外,所有结点都不会比它的父节点大。
可以看到,逻辑上的堆是二叉树,但堆的物理结构却是数组,这得益于完全二叉树的性质:
假设数组下标从0开始,二叉树中父亲和孩子的关系可以方便的通过下标计算得到(图中数字是下标而不是实际的值)
1 2 3 |
#define PARENT(i) ((i-1)/2) #define LCHILD(i) (2*i+1) #define RCHILD(i) (2*(i+1)) |
堆的主要操作
最大堆主要有三种操作:获取最大值,向堆中插入元素并保持堆的性质,删除最大值并保持堆的性质。
由于最大堆的定义,堆中元素最大值必定出现在树根,也就是下标0的地方,因此获取最大值可以在常数时间完成。
插入元素
插入元素插入数组尾部,这样仍然保持了完全二叉树的性质,但堆序性可能得到破坏。
这可以通过比较新插入元素和它的父节点元素的值,如果新插入的元素更大,则要与父节点交换。交换后,新的结点和新的父节点又面临相同的问题。反复执行到新结点比父节点小或者到根节点,这个过程就可以停止。
由于完全二叉树树高为O(logn),在最坏情况下,需要O(logn)的时间完成插入过程。
但是,由于树中大部分结点都位于底层,实际插入的过程中,甚至只需要常数时间,上滤过程就可以终止。
删除最大元素
最大元素位于树根处,为了保持完全二叉树的形状,将尾部元素移到根节点处,然后通过下滤过程来重新调整使之满足堆序性。
每次比较根节点和两个孩子节点中的较大者,若不满足堆序性就进行交换。
在最坏情况下,时间复杂度也是树高,即O(logn)
批量建堆
批量建堆采取从下到上逐渐下滤的过程,左右的叶子结点都看做一个满足堆序性的堆。只需要从最后一个非叶节点开始,从下到上,从右到左的对每个结点进行下滤。这需要遍历n/2个结点,每个结点所用时间是该节点的高度。由于树中大部分结点位于底部,所以大部分结点高度都很小。
实际上,完全二叉树中,最底层元素最多n/2个,而倒数第二层元素最多n/4个,依次类推。高度为h的元素最多有n/(2^h)个元素,每个元素下滤用时为h ,依次求和
$$ \sum_{h=0}^{h=lgn} \frac{n}{2^h} h = n \sum_{h=0}^{h=lgn} \frac{h}{2^h} < n \sum_{h=0}^{\infty} h (\frac{1}{2})^h$$
因为 $$ \sum_{h=0}^{\infty} h x^h = x \sum_{h=0}^{\infty} h x^{h-1} = x \sum_{h=0}^{\infty} \frac{d(x^h)}{dx} = x \frac{d (\sum_{h=0}^{\infty} x^h )}{dx} = x \frac{d (\frac{1}{1-x})}{dx} = \frac{x}{(1-x)^2}$$
因此$$\sum_{h=0}^{\infty} h (\frac{1}{2})^h = \frac{\frac{1}{2}}{(1-(\frac{1}{2})^2)} = 2$$
因此时间复杂度为 O(n)
堆排序
类似于选择排序的过程,只不过这次选择前半部分的最大值不再直接遍历,而是通过堆来快速得到,然后用时O(logn)重新调整堆。这样总的用时为O(nlogn)。
c++实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
#include <cstdio> #include <cstdlib> #include <ctime> #include <cmath> #include <iostream> using std::cout; using std::endl; /* 0 1 2 3 4 5 6 */ #define PARENT(i) ((i-1)/2) #define LCHILD(i) (2*i+1) #define RCHILD(i) (2*(i+1)) template <typename T> class Heap{ bool isnew; T *_elem; int capacity; //堆的最大容量 int size; //现有元素个数 void percolateUp(int i); //上滤调整 void percolateDown(int i); //下滤调整 public: Heap(int n){ capacity = n; _elem = new T[n]; isnew = true; size = 0; } Heap(int *a, int n) { _elem = a; isnew = false; capacity = n; size = n; } ~Heap(){ if (isnew) delete[] _elem; } T delMax(); T getMax(); void insert(T a); void heapify(); void heapSort(); void display(); }; //删除最大元素并返回 template <typename T> T Heap<T>::delMax() { if (size == 0) return 0; int max = _elem[0]; _elem[0] = _elem[--size]; percolateDown(0); return max; } //返回最大元素 template <typename T> T Heap<T>::getMax() { if (size == 0) return 0; return _elem[0]; } //向堆中插入一个元素 template <typename T> void Heap<T>::insert(T a) { _elem[size++] = a; percolateUp(size-1); } //下滤调整 template <typename T> void Heap<T>::percolateDown(int i) { int tmp = _elem[i]; int m = i; do { int l = LCHILD(i); int r = RCHILD(i); if (l >= size) break; if (l == size-1) //处理只有左孩子或没有孩子的情况 m = l; else if (_elem[l] > _elem[r]) m = l; else m = r; if (_elem[m] > tmp) { _elem[i] = _elem[m]; i = m; } else break; } while (m < size && _elem[m] > tmp); _elem[i] = tmp; } //上滤调整 template <typename T> void Heap<T>::percolateUp(int i) { int tmp = _elem[i]; int p = PARENT(i); while (i > 0 && _elem[p] < tmp) { _elem[i] = _elem[p]; i = p; p = PARENT(i); } _elem[i] = tmp; } //批量建堆 用时O(n) template <typename T> void Heap<T>::heapify() { int i = PARENT(size-1); for (; i >= 0; --i) { percolateDown(i); } } //堆排序 template <typename T> void Heap<T>::heapSort() { heapify(); while (size > 0) { _elem[size-1] = delMax(); } } //屏幕输出堆中元素 template <typename T> void Heap<T>::display() { for (int i = 0; i < size; ++i) cout << " " << _elem[i] << " "; } int main() { const int N = 100; int a[N]; int i; srand((int)time(0)); //生成随机数组 for (i = 0; i < N; i++) a[i] = rand() % N; for (int i = 0; i < N; ++i) printf("%d ", a[i]); putchar('\n'); //进行堆排序 Heap<int> h(a, N); h.heapSort(); for (int i = 0; i < N; ++i) printf("%d ", a[i]); return 0; } |