STL笔记之优先队列

STL笔记之优先队列

程序人生  2014-07-05  95 阅读
堆  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来实现。

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操作。

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

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

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

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操作来实现。

// ============================================================================
// 插入新节点
// ============================================================================
// 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),所以源代码相对很简单。

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性质。

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不成立)。

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,我的解题代码如下:

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大的数,我的解题代码如下:

#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)