1. 堆
堆是一颗被完全填满的二叉树,可能的例外是在底层,底层上的元素从左到右填入。这样的树称为完全二叉树。对于一个n个节点的完全二叉树,树高为logN.
一个重要的观察发现,因为完全二叉树很有规律,所以可以用一个数组表示而不需要使用链。这种用数组表示树的方法称为隐式表述法(implicit representation)
二叉堆有两种形式:最大堆和最小堆。在最大堆中,父节点的值总是大于等于任何一个子节点的值。因此,堆中的最大元素放在根节点中,并且在任一子树中,该字数包含的所有节点的值都不大于该子树根节点的值。最小堆是指父节点的值总是小于或等于任一子节点的值。
在堆排序算法中,我们使用最大堆。最小堆通常用于构造优先队列。
2.堆的存储
一般都用数组来表示堆。一般位置0不用来存储元素,即存储区域为A[1,...,len]。所以对于一个节点。它的父节点的下标为2/i,左孩子的下标为2*i,右孩子的节点为2*i+1.
如果要使用下标为0的位置,那么父节点的下标就为(i-1)/2。它的左右子节点下标为2*i+1和2*i+2。
本文中将不使用0位置。
3. 向下调整
向下调整是为了维护堆的性质。它的输入为数组A和下标i。它首先看该节点是否大于其左右子节点的值,若不是,将左右子节点中较大值与之交换。交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到该节点为根的子结构成堆为止。时间复杂度为O(lgn)
1 void AdjustDown(ElemType A[], int k, int heap_size) { 2 int child; 3 ElemType tmp = A[i]; 4 5 for (int i = k << 1; i <= heap_size; i *= 2) { // 沿键值较大的子节点向下筛选 6 if (i < heap_size && A[i + 1] > A[i]) { 7 i++; 8 } 9 if (A[child] < tmp) { 10 A[k] = A[child]; //将A[child]调整到双亲的位置 11 k = i; //修改键值,以便继续向下筛选 12 } else { 13 break; 14 } 15 16 } 17 18 a[k] = tmp; //被筛选的值放入最终的位置 19 }
算法导论中给出的递归的方法:
1 inline int Left(int i) { 2 return i << 1; 3 } 4 5 inline int Right(int i) { 6 return i << 1 + 1; 7 } 8 9 void AdjustDown(int A[], int k, int heap_size) { 10 int left_child = Left(k); 11 int right_child = Right(k); 12 13 int largest = k; 14 15 if (left_child <= heap_size && A[left_child] > A[largest]) { 16 largest = leftc_child; 17 } 18 if (right_child <= heap_size && A[right_child] > A[largest]) { 19 largest = right_child; 20 } 21 22 if (largest != k) { 23 swap(A[k], A[largest]); 24 AdjustDown(A, largest, heap_size); 25 }
4.建堆
1 void BuildMaxHeap(ElemType A[], int len) { 2 for (int i = len / 2; i > 0; --i) //下标从0开始,i = len / 2 - 1; i>= 0; --i 3 AdjustDown(A, i, len); //AdjustDown(A,ilen - 1): 4 }
5.向上操作与堆的插入操作
堆的插入操作时,先将新节点放在堆的末端,在对这个新节点执行向上调整操作。假设新插入的节点在下标为k的位置。此项操作意味着二叉堆的数据从数组下标1处开始。
1 //下标从1开始 2 template<typename T> 3 void PercolateUp(vector<T> &data, int i) { 4 T temp = data[i]; 5 for (; i > 1 && temp > data[i / 2]; i /= 2) { 6 data[i] = data[i / 2]; 7 } 8 9 data[i] = temp; 10 } 11 12 //下标从0开始 13 template<typename T> 14 void PercolateUp(vector<T> &data, int i) { 15 T temp = data[i]; 16 for (; i > 0 && temp > data[(i-1)/2]; i = (i - 1) / 2) { 17 data[i] = data[(i-1)/2]; 18 } 19 20 data[i] = temp; 21 }
6.堆排序
堆排序首先将存放在L[1..n]中的n个元素建立成初始堆,由于堆本身的特点,堆顶元素就是最大值。输出堆顶的元素,通常将堆底的元素送入堆顶。此时根节点已不满足堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持最大堆的性质,输出堆顶元素。如此重复直到堆中元素剩下一个元素为止。
对于存储从下标1开始:
1 void HeapSort(int A[], int len) { 2 BuildMaxHeap(A,len); 3 for (int i = len; i > 1; --i) { 4 swap(A[i], A[1]); 5 AdjustDown(A, 1, i - 1); 6 } 7 }
对于存储下标从0开始:
1 int Left(int i) { 2 return (i << 1) + 1; 3 } 4 5 void AdjustDown(int A[], int i, int n) { 6 int temp = a[i]; 7 8 for (int child = Left(i); child <= n; i = child ) { 9 if (child < n && A[child] < A[child + 1]) 10 ++child; 11 if (temp < A[child]) 12 A[i] = A[child]; 13 else 14 break; 15 } 16 17 A[i] = temp; 18 19 } 20 21 void HeapSort(int A[], int len) { 22 for (int i = len / 2 - 1; i >= 0; --i) { 23 AdjustDown(A, i, len); 24 } 25 for (int i = len - 1; i > 0; --i) { 26 swap(A[0], A[i]); 27 AdjustDown(A, 0, i - 1); 28 } 29 }
堆的相关题目:
1. 最小的k个数
输入n个整数,输出其中最小的k个数。例如输入1,2,3,4,5,6,7,8这8个数字,则最小的4个数字是1,2,3和4.(《王道程序员求职宝典》)
解答:可以使用快排的partition来解决此题,时间复杂度为O(N).但此种方法也有限制。首先需要一次性读入所有数据,其次,需要修改输入的数组。
首先我们读入k个数创建一个大小为k的大根堆,然以后我们依次读入剩余数据,如果当前数据比大根堆的堆顶小,则用这个数替换当前堆顶,并调整使其保持
大根堆的性质;如果当前数据比堆顶大,那么这个数不可能是最小的k个整数之一,故可以抛弃。
其中,BuildMaxHeap函数与AdjustDown函数为前面提到的函数。
当需要求最大的k个数时,只需要将大根堆改为小根堆即可。
时间复杂度为O(NlogK)
1 void PercolateDown(vector<int> &data, int i, int heap_size) { 2 int temp = data[i]; 3 4 for (int child = i * 2 + 1; child <= heap_size;child = child * 2 + 1) { 5 if (child < heap_size && data[child + 1] > data[child]) 6 child++; 7 8 if (temp < data[child]) { 9 data[i] = data[child]; 10 i = child; 11 } else { 12 break; 13 } 14 } 15 16 data[i] = temp; 17 } 18 19 void BuildMaxHeap(vector<int> &data) { 20 for (int i = data.size() / 2 - 1; i >= 0; --i) { 21 PercolateDown(data,i,data.size() - 1); 22 } 23 } 24 25 vector<int> FindSmallestK(vector<int> &data, int k) { 26 vector<int> res(k); 27 if (data.empty() || k > data.size()) 28 return res; 29 30 for (int i = 0; i < k; ++i) 31 res[i] = data[i]; 32 33 BuildMaxHeap(res); 34 35 for (int i = k; i < data.size(); ++i) { 36 if (data[i] < res[0]) { 37 res[0] = data[i]; 38 PercolateDown(res, 0, k); 39 } 40 } 41 42 return res; 43 }
2. 设计一个时间复杂度为O(nlgk) 的算法,它能将k个有序链表合并成一个有序链表,这里了k是所有输入链表包含的总的元素个数(算法导论,P93)
编程思路:
假设k个链表都是非降序排列的。
(1)取k个元素建立最小堆,这k个元素分别是k个链表的第一个元素。建堆的时间复杂度O(k)。
(2)堆顶元素就是k个链表中最小的那个元素,取出它。时间复杂度O(1)。
(3)若堆顶元素所在链表不为空,则取下一个元素放到堆顶位置,这可能破坏了最小堆性质,所以进行堆调整。堆调整时间复杂度O(lgk)。若为空,则此子链表已经被合并完毕,则删除最小堆的堆顶元素,此时最小堆的heapSize减小了1 。删除指定元素时间复杂度O(lgk)。
(4)重复步骤(2)~(3)n-k次。总的时间复杂度是O(k)+O(nlgk)即O(nlgk)。
1 struct ListNode { 2 int val; 3 ListNode *next; 4 ListNode(int x): val(x), next(NULL){} 5 }; 6 7 bool cmp(ListNode *l1, ListNode *l2) { 8 return l1->val > l2->val; 9 } 10 11 ListNode *mergeKLists(vector<ListNode *> &lists) { 12 vector<ListNode *> heap; 13 14 for (int i = 0; i < lists.size(); ++i) { 15 if (lists[i] != NULL) 16 heap.push_back(lists[i]); 17 } 18 19 make_heap(heap.begin(), heap.end(), cmp); 20 21 ListNode *head = NULL; 22 ListNode *curr = NULL; 23 while (!heap.empty()) { 24 ListNode *min = heap.front(); 25 pop_heap(heap.begin(), heap.end(), cmp); 26 heap.pop_back(); 27 28 if (head == NULL) 29 head = min; 30 if (curr) 31 curr->next = min; 32 curr = min; 33 34 if (curr->next) { 35 heap.push_back(curr->next); 36 push_heap(heap.begin(), heap.end(),cmp); 37 } 38 } 39 40 return head; 41 42 43 }
参考资料:
1. 《数据结构与算法分析 C++描述》(第三版) 人民邮电出版社