堆以及堆排序

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++描述》(第三版)  人民邮电出版社

原文地址:https://www.cnblogs.com/vincently/p/4193474.html