《算法导论》堆排序和优先队列

来源:互联网 发布:手机逛淘宝费流量吗 编辑:程序博客网 时间:2024/05/23 02:02

堆是一种树形结构,通常基于完全二叉树实现最大堆或最小堆。

堆排序最坏的时间复杂度为O(nlogn),和归并排序一样;又和插入排序一样,属于原地排序;堆排序综合了两种算法的优势。

堆结构可以用来实现海量数据的Top-N排序,也可以用来实现优先队列(Priority Queue)。


堆排序

实现要点:

  • 堆基于完全二叉树实现,可以用数组存储,容易获得当前节点i的父节点和左右子节点的索引。
  • 这里以最大堆为例,即堆中任何一个节点的值都大于其子树中的值。
  • 实现的主要操作:
    • 维护最大堆的特性,max_heapify(i),代价O(logn)
    • 从无序数组建最大堆,build_max_heap(),代价O(n)
    • 堆排序,heap_sort(),代价O(nlogn)

max_heapify(i)

这个函数用来维护最大堆的特性,本质是下溯(percolate down,《STL源码剖析》),检查当前节点i的值是否大于其左右子节点的值。如果不满足,则和左右节点中的最大值交换,使得三者中的最大值放在根节点,下放的原根节点继续递归调用,检查在新的位置是否大于新的左右子节点的值。

注意,这个函数保证以i为根的子树满足最大堆,前提是i的左右子节点的两棵子树都是最大堆。如果不满足,调用一次max_heapify(i)并不能确保整个i节点的子树为最大堆。

def max_heapify(self, i):    """        if A[i] < A[left_child(i)] or A[i] < A[right_child(i)]        percolate down        T(n) = O(log n)    """    l = self.left_child(i)    r = self.right_child(i)    if l < self.heap_size and self.heap[i] < self.heap[l]:        largest = l    else:        largest = i    if r < self.heap_size and self.heap[largest] < self.heap[r]:        largest = r    if largest != i:        # swap heap[largest] <-> heap[i]        self.heap[i], self.heap[largest] = self.heap[largest], self.heap[i]        self.max_heapify(largest) # decision    return self.heap

build_max_heap

建堆操作可以基于max_heapify,对非叶节点,自底向上执行max_heapify调整堆即可。

考虑到上述的注意,建堆操作,下标i必须要从最后一个非叶节点(heap_size - 1)/2递减到根节点。否则从上到下,并不能保证底部的节点满足最大堆。(《算法导论》习题6.3-2)

def build_max_heap(self):    for i in xrange((self.heap_size - 1)/2, -1, -1):        self.max_heapify(i)    return self.heap

heap_sort()

堆排序的前提是建好最大堆,每次取走堆的根节点(即堆中的最大值),重新用max_heapify调整堆,又取走根节点,能保证产生从大到小的元素。由于堆的动态调整复杂度不高,取决于堆的大小,能应对海量大数据的排序任务。如在1TB日志数据中找出Top-N大的数据,海量数据不能全部加载在内存中,可以借助堆来,实现动态的加载数据并产生有序的输出。

具体实现:

  1. 取根节点,和堆末节点交换。从后往前遍历堆的节点,取出堆的根节点(堆最大值),和堆的最后一个节点交换,从堆中删除最后一个节点,即取出的最大值(heap_size–)。

  2. 重新调整最大堆。从堆末换到根的节点一般都是当前堆里的较小值(不是BST,不一定最小),当前堆里除了根节点,其他均满足最大堆(原来就满足),所以只需要对根节点执行max_heapify,下放到合适的位置,实现重新调整堆。

重复上述步骤,在原来的数组中,前面部分的堆越来越小,后面输出的排好序的元素越来越多,直到遍历完最前面的根节点,原来的数组便是排好序(升序)的序列。

def heap_sort(self):    """        Build max_heap for list A        return ascending sorted A    """    # self.build_max_heap() # build max heap    for i in xrange(self.heap_size - 1, 0, -1):        self.heap[0], self.heap[i] = self.heap[i], self.heap[0]        self.heap_size -= 1        self.max_heapify(0)    return self.heap

优先队列

这里的优先级队列是基于最大堆实现的,加上操作:
- 出队dequeue(),代价O(logn)
- 入队enqueue(key),代价O(logn)

优先队列的出入队和传统队列一样:出队时在队头取出元素,入队时在队尾添加元素。但不同的是,队列的顺序是取决于排序的关键字大小,即所谓的优先级,跟入队顺序无关。因此,每次出队和入队操作后,都要对队列按照优先级重新排序。

dequeue

出队操作和堆排序相似,取出当前的堆的根节点,然后把堆末节点换到根节点,执行max_heapify,调整最大堆。

def dequeue(self):    """        HEAP-EXTRACT-MAX(A)         Similar to heap sort    """    if self.heap_size < 1:        print "heap underflow"    max_key = self.heap[0]    self.heap[0] = self.heap[self.heap_size - 1]    self.heap.pop(-1)    self.heap_size -= 1    self.max_heapify(0)    return max_key

enqueue(key)

入队操作,则要在堆序列的末尾加上新入队的元素,并重新调整最大堆。

调整的方法和max_heapify相反,实现的是对堆末元素的上溯(percolate up,《STL源码剖析》)。具体方法:比较新加入节点的值和其父节点的值,如果比父节点大,则要和父节点交换位置,实现上移,然后继续判断上移后的节点(i = parent(i))和新的父节点的大小。

def enqueue(self, key):    """        MAX-HEAP-INSERT(A, key) + HEAP-INCREASE-KEY(A, i, key)    """    self.heap.append(key)    self.heap_size += 1    i = self.heap_size - 1    while i > 0 and self.heap[self.parent(i)] < self.heap[i]:         self.heap[i], self.heap[self.parent(i)] = self.heap[self.parent(i)], self.heap[i]          i = self.parent(i)

完整的python实现代码如下:

"""    Heap: complete binary tree    @author: Shangru """class Heap(object):    def __init__(self, A):        self.heap_size = len(A)        self.heap = A        self.build_max_heap()    def parent(self, i):        return (i - 1)/2    def left_child(self, i):        return 2*i + 1    def right_child(self, i):        return 2*i + 2    def build_max_heap(self):        for i in xrange((self.heap_size - 1)/2, -1, -1):            self.max_heapify(i)        return self.heap    def max_heapify(self, i):        """            if A[i] < A[left_child(i)] or A[i] < A[right_child(i)]            percolate down            T(n) = O(log n)        """        l = self.left_child(i)        r = self.right_child(i)        if l < self.heap_size and self.heap[i] < self.heap[l]:            largest = l        else:            largest = i        if r < self.heap_size and self.heap[largest] < self.heap[r]:            largest = r        if largest != i:            # swap heap[largest] <-> heap[i]            self.heap[i], self.heap[largest] = self.heap[largest], self.heap[i]            self.max_heapify(largest) # decision        return self.heap    def heap_sort(self):        """            Build max_heap for list A            return ascending sorted A        """        # self.build_max_heap() # build max heap        for i in xrange(self.heap_size - 1, 0, -1):            self.heap[0], self.heap[i] = self.heap[i], self.heap[0]            self.heap_size -= 1            self.max_heapify(0)        return self.heap    # ======= priority queue =======    def dequeue(self):        """            HEAP-EXTRACT-MAX(A)             Similar to heap sort        """        if self.heap_size < 1:            print "heap underflow"        max_key = self.heap[0]        self.heap[0] = self.heap[self.heap_size - 1]        self.heap.pop(-1)        self.heap_size -= 1        self.max_heapify(0)        return max_key    def enqueue(self, key):        """            MAX-HEAP-INSERT(A, key) + HEAP-INCREASE-KEY(A, i, key)        """        self.heap.append(key)        self.heap_size += 1        i = self.heap_size - 1        while i > 0 and self.heap[self.parent(i)] < self.heap[i]:             self.heap[i], self.heap[self.parent(i)] = self.heap[self.parent(i)], self.heap[i]              i = self.parent(i)if __name__ == '__main__':    # test build_heap    h = Heap([1, 2, 3, 4, 5, 6, 7, 8])    print h.heap    # test max_heapify    h = Heap([16, 4, 10, 14, 7, 9, 3, 2, 8, 1])    print h.max_heapify(1)    # test heap_sort    h = Heap([8, 7, 6, 5, 4, 3, 2, 1])    print h.heap_sort()    # test priority queue    pq = h    print pq.dequeue()    print pq.heap    pq.enqueue(10)    print pq.heap

Python的heapq模块

Python自带的heapq模块可以轻松实现优先队列操作:
- heapq.heappush(heap, item)
- heapq.heappop(heap)
- heapq.heapify(x),实现线性时间in-place的列表x转化为堆

实现堆排序:

import heapqdef heapsort(iterable):    h = []    for value in iterable:        heappush(h, value)    return [heappop(h) for i in range(len(h))]

一些优先队列相关的题目

  • HDOJ 1242,某人被关在囚笼里等待朋友解救,问能否解救成功,最少需要多少时间,http://acm.hdu.edu.cn/showproblem.php?pid=1242
  • HDOJ 1053,给出一行字符串,求出其原编码需要的编码长度和哈夫曼编码所需的长度,并求其比值,http://acm.hdu.edu.cn/showproblem.php?pid=1053
  • POJ 2263,给出各城市间道路的限制载重量,求出从一个城市到另外一个城市的贷车能够运载的最大货物重量,http://acm.pku.edu.cn/JudgeOnline/problem?id=2263

Reference

  • 《算法导论》第二版
  • 《STL源码剖析》
  • Python heapq的官方手册,https://docs.python.org/2/library/heapq.html
0 0