首页 > STL源码剖析 > STL笔记之优先队列

STL笔记之优先队列

在STL中队列queue是基于deque实现的,优先队列priority_queue则是基于堆实现的。所谓优先队列即元素具有优先级的队列,在最大优先级队列中,队列最前面的元素具有最高的优先级,最大优先级队列基于最大堆(max-heap)实现。

1. 堆的基本性质
二叉堆是一颗完全二叉树,可以分为最小堆和最大堆,以最大堆为例来说,对于堆中的每一个节点p,都满足条件key[p] >= key[p->left] && key[p] >= key[p->right],即以p为根的子树中,根节点p的值是最大的,堆中的所有节点都满足这个性质。

因为二叉堆是一颗完全二叉树,所以,所以根节点的索引从1开始算,则对于索引为i的节点,其左子结点的索引为2*i,右子节点的索引为2*i+1,父节点的索引为i/2,这些操作都可以基于移位运算快速实现。因为这个特性,通常使用数组存储堆的节点。

CLRS 6.1-7
对于拥有n个节点的堆而言,其叶子节点的下标为[n/2]+1, [n/2]+2, …, n。
证明:因为有n个元素,最后一个元素序号为n,那么它的parent结点应该是序号最大的parent结点,那么这个parent结点就为[n/2],其之后都是叶子结点,因而为[n/2]+1, [n/2]+2, …, n。
(也可以从二叉树节点的度与二叉树节点之间的关系进行证明,具体过程略)

max_heapify 最大堆性质的维护
如果一个节点的左右两颗子树都满足最大堆的性质,但是节点本身可能不满足最大堆的性质,这时候可以通过对该节点执行max_heapify操作来保持以该节点为根的堆的性质。max_heapify通过找出节点p,p->left,p->right三个节点中值最大的节点,然后将最大节点的值与节点i的值进行交换,然后在原有的最大节点上递归调用max_heapify来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void maxheapify(int a[], int i, int heapsize)
{
    int l = (i<<1);
    int r = (i<<1) + 1;
    int largest = i;
 
    if (l <= heapsize && a[l] > a[largest])
    {
        largest = l;
    }
    if (r <= heapsize && a[r] > a[largest])
    {
        largest = r;
    }
    if (largest != i)
    {
        swap(a[largest], a[i]);
        maxheapify(a, largest, heapsize);
    }
}

maxheapify的时间复杂度为O(lgN)

堆的建立
我们可以从后往前扫描数组,对每一个节点都进行maxheapify操作,这样就建立了一个堆。但是对于叶子节点而言,调用maxheapify操作是没有意义的。而上面的CLRS 6.1-7提到,对于拥有n个节点的堆而言,其叶子节点的下标为[n/2]+1, [n/2]+2, …, n。因此,我们可以从n/2开始往前进行maxheapify操作。

1
2
3
4
5
6
7
void build_heap(int a[], int n)
{
    for (int i = n/2; i >= 1; --i)
    {
        max_heapify(a, i, n);
    }
}

需要注意的一点是,建堆操作的时间复杂度看上去为O(NlgN),实际上为O(N),可以从数学上进行证明。以满二叉树为例,如下图所示:
二叉堆-满二叉树
令堆所对应的二叉树的高度为h,节点的个数为n,对于满二叉树有n = 2^(h+1) – 1,对最坏情况而言,其中有2^(h-1)个结点向下访问1次,2^(h-2)个结点向下访问2次,…1个结点向下访问h次,时间复杂度推导过程如下:
建堆时间复杂度O(N)

堆排序
在建立好一个堆之后,堆排序就比较简单了。每次把第一个节点和最后一个节点的值交换,然后对第一个节点调用maxheapify操作,直到堆的元素个数减小到1.
堆排序的时间复杂度为O(NlgN),因为maxheapify中,前面两个if语句(也就是从左右子节点取得最大值节点)的顺序是可以随意安排的,所以堆排序不是稳定排序。

1
2
3
4
5
6
7
8
9
void heap_sort(int a[], int n)
{
    build_heap(a, n);
    for (int i = n; i >= 2; --i)
    {
        swap(a[1], a[i]);
        max_heapify(a, 1, i-1);
    }
}

2. STL heap
SGI STL的heap的操作基本就和上面提到的差不多了,只是许多过程都是地推来实现的,而且,并没有采用下标从1开始的基数规则,而是采用从0开始。
其中adjust_heap和max_heapify操作思路有所不同,adjust_heap的实现思路是:首先把子节点往上移动,最后调用push_heap操作来实现。

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
// ============================================================================
// 插入新节点
// ============================================================================
// push_heap实现
// holeIndex为空洞节点的索引,最开始即为末尾待加入堆的节点的索引
// topIndex为根节点的索引
// value为待加入节点的值
template <class _RandomAccessIterator, class _Distance, class _Tp>
void 
__push_heap(_RandomAccessIterator __first,
            _Distance __holeIndex, _Distance __topIndex, _Tp __value)
{
  // 获取父节点的索引值
  _Distance __parent = (__holeIndex - 1) / 2;
  // 如果还没有上升到根节点,且父节点的值小于待插入节点的值
  while (__holeIndex > __topIndex && *(__first + __parent) < __value) {
    // 父节点下降到holeIndex
    *(__first + __holeIndex) = *(__first + __parent);
    // 继续往上检查
    __holeIndex = __parent;
    __parent = (__holeIndex - 1) / 2;
  }
  // 插入节点
  *(__first + __holeIndex) = __value;
}
 
template <class _RandomAccessIterator, class _Distance, class _Tp>
inline void 
__push_heap_aux(_RandomAccessIterator __first,
                _RandomAccessIterator __last, _Distance*, _Tp*)
{
  __push_heap(__first, _Distance((__last - __first) - 1), _Distance(0), 
              _Tp(*(__last - 1)));
}
 
// 公开接口,假定[first, last-1)已经是一个堆,此时把*(last-1)压入堆中
template <class _RandomAccessIterator>
inline void 
push_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
  __push_heap_aux(__first, __last,
                  __DISTANCE_TYPE(__first), __VALUE_TYPE(__first));
}
 
// ============================================================================
// 保持堆的性质
// ============================================================================
// first 起始位置
// holeIndex 要进行调整操作的位置
// len 长度
// value holeIndex新设置的值
template <class _RandomAccessIterator, class _Distance, class _Tp>
void 
__adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex,
              _Distance __len, _Tp __value)
{
  // 当前根节点的索引值
  _Distance __topIndex = __holeIndex;
  // 右孩子节点的索引值
  _Distance __secondChild = 2 * __holeIndex + 2;
  // 如果没有到末尾
  while (__secondChild < __len) {
    // 如果右孩子节点的值比左孩子节点的值要小,那么secondChild指向左孩子
    if (*(__first + __secondChild) < *(__first + (__secondChild - 1)))
      __secondChild--;
    // 子节点的往上升
    *(__first + __holeIndex) = *(__first + __secondChild);
    // 继续处理
    __holeIndex = __secondChild;
    __secondChild = 2 * (__secondChild + 1);
  }
  // 如果没有右子节点
  if (__secondChild == __len) {
    *(__first + __holeIndex) = *(__first + (__secondChild - 1));
    __holeIndex = __secondChild - 1;
  }
  // 针对节点topIndex调用push_heap操作
  __push_heap(__first, __holeIndex, __topIndex, __value);
}
 
// ============================================================================
// 弹出一个节点
// ============================================================================
// 区间:[first, last)
// result: 保存根节点的值
// value: 原来末尾节点的值
template <class _RandomAccessIterator, class _Tp, class _Distance>
inline void 
__pop_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
           _RandomAccessIterator __result, _Tp __value, _Distance*)
{
  // 取出根节点的值
  *__result = *__first;
  // 对根节点调用adjust_heap
  __adjust_heap(__first, _Distance(0), _Distance(__last - __first), __value);
}
 
template <class _RandomAccessIterator, class _Tp>
inline void 
__pop_heap_aux(_RandomAccessIterator __first, _RandomAccessIterator __last,
               _Tp*)
{
  __pop_heap(__first, __last - 1, __last - 1, 
             _Tp(*(__last - 1)), __DISTANCE_TYPE(__first));
}
 
// 对外接口:取出根节点的值放入到末尾节点并保持堆的性质
template <class _RandomAccessIterator>
inline void pop_heap(_RandomAccessIterator __first, 
                     _RandomAccessIterator __last)
{
  __pop_heap_aux(__first, __last, __VALUE_TYPE(__first));
}
 
// ============================================================================
// 建堆操作
// ============================================================================
template <class _RandomAccessIterator, class _Tp, class _Distance>
void 
__make_heap(_RandomAccessIterator __first,
            _RandomAccessIterator __last, _Tp*, _Distance*)
{
  // 只有一个元素不需要进行任何操作
  if (__last - __first < 2) return;
  _Distance __len = __last - __first;
  _Distance __parent = (__len - 2)/2;
 
  // 从第一个不是叶子节点的索引从后往前调用adjust_heap操作
  while (true) {
    __adjust_heap(__first, __parent, __len, _Tp(*(__first + __parent)));
    if (__parent == 0) return;
    __parent--;
  }
}
 
// 公开接口
template <class _RandomAccessIterator>
inline void 
make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
  __make_heap(__first, __last,
              __VALUE_TYPE(__first), __DISTANCE_TYPE(__first));
}
 
// ============================================================================
// 堆排序
// ============================================================================
// 建好堆之后才能调用sort_heap
template <class _RandomAccessIterator>
void sort_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
  while (__last - __first > 1)
    pop_heap(__first, __last--);
}

3. STL priority_queue
priority_queue底层基于heap实现,属于配接器(adapter),所以源代码相对很简单。

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
template <class _Tp, 
          class _Sequence __STL_DEPENDENT_DEFAULT_TMPL(vector<_Tp>),
          class _Compare
          __STL_DEPENDENT_DEFAULT_TMPL(less<typename _Sequence::value_type>) >
class priority_queue {
  typedef typename _Sequence::value_type _Sequence_value_type;
 
public:
  typedef typename _Sequence::value_type      value_type;
  typedef typename _Sequence::size_type       size_type;
  typedef          _Sequence                  container_type;
 
  typedef typename _Sequence::reference       reference;
  typedef typename _Sequence::const_reference const_reference;
protected:
  // c即底层存放数据的容器,默认使用vector<T>
  _Sequence c;
  // comp即为比较函数对象,默认为less<T>
  _Compare comp;
 
public:
  // 构造函数
  priority_queue() : c() {}
  explicit priority_queue(const _Compare& __x) :  c(), comp(__x) {}
  priority_queue(const _Compare& __x, const _Sequence& __s) 
    : c(__s), comp(__x) 
    { make_heap(c.begin(), c.end(), comp); }
 
  priority_queue(const value_type* __first, const value_type* __last) 
    : c(__first, __last) { make_heap(c.begin(), c.end(), comp); }
 
  priority_queue(const value_type* __first, const value_type* __last, 
                 const _Compare& __x) 
    : c(__first, __last), comp(__x)
    { make_heap(c.begin(), c.end(), comp); }
 
  priority_queue(const value_type* __first, const value_type* __last, 
                 const _Compare& __x, const _Sequence& __c)
    : c(__c), comp(__x) 
  { 
    c.insert(c.end(), __first, __last);
    make_heap(c.begin(), c.end(), comp);
  }
 
  // empty, size, top是对底层容器的包装
  bool empty() const { return c.empty(); }
  size_type size() const { return c.size(); }
  // 注意top返回const_reference
  const_reference top() const { return c.front(); }
 
  // push操作
  void push(const value_type& __x) {
    __STL_TRY {
      c.push_back(__x); 
      push_heap(c.begin(), c.end(), comp);
    }
    __STL_UNWIND(c.clear());
  }
  // pop操作
  void pop() {
    __STL_TRY {
      pop_heap(c.begin(), c.end(), comp);
      c.pop_back();
    }
    __STL_UNWIND(c.clear());
  }
};

4. 基于优先队列实现queue和stack
基于priority_queue可以实现queue。queue的性质为FIFO,那么如果基于最小优先队列,我们给每一个元素都设置一个优先级,每次push操作之后,优先级增加1,那么栈顶的元素总是优先级最小的元素,也就是最先入队的元素,这样就满足了FIFO性质。

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
template<class T>
class Queue
{
public:
    Queue() : priority(0) {}
 
    void push(const T& t)
    {
        q.push(Node(t, priority++));
    }
 
    bool empty()
    {
        return q.empty();
    }
 
    int size()
    {
        return q.size();
    }
 
    void pop()
    {
        q.pop();
    }
 
    const T& top()
    {
        return q.top().t;
    }
 
private:
    struct Node
    {
        T t;
        int p;
        Node(const T& _t, int _p) : t(_t), p(_p) {}
        bool operator>(const Node& rhs) const
        {
            return t > rhs.t;
        }
    };
 
private:
    int priority;
    std::priority_queue<Node, std::vector<Node>, std::greater<Node> > q;
};

同样,基于priority_queue可以实现stack。stack的性质为LIFO,那么如果基于最大优先队列,我们给每一个元素都设置一个优先级,每次push操作之后,优先级增加1,那么栈顶的元素总是优先级最大的元素,也就是最后入队的元素,这样就满足了LIFO性质。每次pop操作之后,我们可以将优先级记录值减小1(注意这个对于Queue不成立)。

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
template<class T>
class Stack
{
public:
    Stack() : priority(0) {}
 
    void push(const T& t)
    {
        q.push(Node(t, priority++));
    }
 
    bool empty()
    {
        return q.empty();
    }
 
    int size()
    {
        return q.size();
    }
 
    void pop()
    {
        q.pop();
        --priority;
    }
 
    const T& top()
    {
        return q.top().t;
    }
 
private:
    struct Node
    {
        T t;
        int p;
        Node(const T& _t, int _p) : t(_t), p(_p) {}
        bool operator<(const Node& rhs) const
        {
            return t < rhs.t;
        }
    };
 
private:
    int priority;
    std::priority_queue<Node> q;
};

5. 最小堆K路合并
请给出一个时间为O(nlgk)、用来将k个已排序链表合并为一个排序链表的算法,此处n为所有输入链表中元素的总数。
算法思想:
1. 从k个链表中取出每个链表的第一个元素,组成一个大小为k的数组arr,然后将数组arr转换为最小堆,那么arr[0]就为最小元素了;
2. 取出arr[0],将其放到新的链表中,然后将arr[0]元素在原链表中的下一个元素补到arr[0]处,即arr[0].next,如果 arr[0].next为空,即它所在的链表的元素已经取完了,那么将堆的最后一个元素补到arr[0]处,堆的大小自动减1,循环即可。

LeetCode提供了一个练习题Merge k Sorted Lists,我的解题代码如下:

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
struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};
 
class Solution
{
private:
    // 其实可以不用这个索引结构,直接通过节点的next指针即可获取下一个节点
    struct Node
    {
        ListNode *ptr;
        int index;
        Node(ListNode *p, int i) : ptr(p), index(i) {}
        bool operator>(const Node& rhs) const
        {
            return ptr->val > rhs.ptr->val;
        }
    };
 
private:
    vector<Node> arr;
    ListNode head;
 
private:
    void min_heapify(int i)
    {
        int left = i*2;
        int right = i*2 + 1;
        int smallest = i;
        int heapsize = arr.size()-1;
 
        if (left <= heapsize && arr[smallest] > arr[left])
        {
            smallest = left;
        }
        if (right <= heapsize && arr[smallest] > arr[right])
        {
            smallest = right;
        }
 
        if (smallest != i)
        {
            swap(arr[smallest], arr[i]);
            min_heapify(smallest);
        }
    }
 
    void build_heap()
    {
        int heapsize = arr.size()-1;
        for (int i = heapsize/2+1; i >= 1; --i)
        {
            min_heapify(i);
        }
    }
 
public:
    Solution() : head(0) {}
    ListNode *mergeKLists(vector<ListNode *> &lists)
    {
        int n = lists.size();
        arr.clear();
        arr.reserve(n+1);
        arr.push_back(Node(NULL, 0));
 
        for (int i = 0; i < n; ++i)
        {
            if (lists[i] != NULL)
            {
                arr.push_back(Node(lists[i], i));
                lists[i] = lists[i]->next;
            }
        }
 
        ListNode *p = &head;
        build_heap();
        while (arr.size() > 1)
        {
            p->next = arr[1].ptr;
            p = p->next;
            int i = arr[1].index;
            if (lists[i])
            {
                arr[1] = Node(lists[i], i);
                lists[i] = lists[i]->next;
            }
            else
            {
                i = arr.size()-1;
                arr[1] = arr[i];
                arr.erase(arr.end()-1);
            }
            min_heapify(1);
        }
 
        return head.next;
    }
};

6. 输出数据集前K大的数
对于一个数组,要求输出前K大的所有数。
思路:如果采用排序之后再输出,则复杂度为O(NlgN)。如果我们先建立一个堆,然后取出前K大的数,那么复杂度就是O(N)+O(KlgN),效率更高。HDU提供了一个练习题前K大的数,我的解题代码如下:

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
#include <stdio.h>
#include <stdlib.h>
 
int a[1000010];
 
inline void swap(int &a, int &b)
{
    int tmp = a;
    a = b;
    b = tmp;
}
 
void max_heapify(int i, int size)
{
    int left = i * 2;
    int right = left + 1;
    int largest = i;
 
    if (left <= size && a[left] > a[largest])
    {
        largest = left;
    }
    if (right <= size && a[right] > a[largest])
    {
        largest = right;
    }
    if (largest != i)
    {
        swap(a[largest], a[i]);
        max_heapify(largest, size);
    }
}
 
void build_heap(int n)
{
    for (int i = n/2+1; i >= 1; --i)
    {
        max_heapify(i, n);
    }
}
 
int main(int argc, char **argv)
{
    int n, m;
    while (EOF != scanf("%d %d", &n, &m))
    {
        for (int i = 1; i <= n; ++i)
        {
            scanf("%d", &a[i]);
        }
        build_heap(n);
        printf("%d", a[1]);
        for (int i = 2; i <= m; ++i)
        {
            swap(a[1], a[n-i+2]);
            max_heapify(1, n-i+1);
            printf(" %d", a[1]);
        }
        printf("\n");
    }
 
    return 0;
}

STL源码剖析笔记系列
1. STL笔记之空间配置器
2. STL笔记之迭代器
3. STL笔记之vector
4. STL笔记之list
5. STL笔记之优先队列

参考
http://www.cnblogs.com/shuaiwhu/archive/2011/03/20/2065081.html
http://blog.csdn.net/anonymalias/article/details/8807895
http://www.cnblogs.com/shuaiwhu/archive/2011/03/20/2065077.html
《算法导论》


觉得文章还不错?点击此处对作者进行打赏!


本文地址: 程序人生 >> STL笔记之优先队列
作者:代码疯子(Wins0n) 本站内容如无声明均属原创,转载请保留作者信息与原文链接,谢谢!


更多



  1. 昏人
    2014年7月10日00:20 | #1

    推导建堆复杂度那张图第二步起貌似漏了个2的h方,虽然最后结果对了

    [回复]

    代码疯子 回复:

    @昏人, 确实是漏了,谢谢指正。

    [回复]

  1. 本文目前尚无任何 trackbacks 和 pingbacks.