堆(Heap)

定义

  1. 完全二叉树(即除了最后一层外,其他层都是满的,最后一层的节点全部靠左排列,适合用数组存储数据)
  2. 节点的值大于等于(或小于等于)子树节点的值(等价于左右子树节点)
    ps. 同一组数据,堆不唯一。

存储

完全二叉树适合使用数组存储数据,可以直接使用下标来找到父亲的左右儿子和儿子的父亲。
比如根从1开始,下标为 i i i 的节点的左右儿子为 i ∗ 2 i*2 i2 i ∗ 2 + 1 i*2+1 i2+1,父亲为 i / 2 i/2 i/2;下标为 i i i 的节点的左右儿子为 i ∗ 2 + 1 i*2+1 i2+1 i ∗ 2 + 2 i*2+2 i2+2,父亲为 ( i − 1 ) / 2 (i-1)/2 (i1)/2

操作

插入元素

把元素放到数组的最后一个位置,然后调整成堆。
从下往上:让新的节点和父节点进行大小比较,若不满足,则交换。

//伪代码
count ++; //堆中元素个数
a[count] = data;
while(i/2>0 && a[i]>a[i/2]){ //不到根(或i>1) 并且 不满足大顶堆的定义
    swap(a, i, i/2);
    i = i / 2;
}
删除元素

删除的一般是堆顶元素,它是数组中最大或者最小的元素。删除之后,把数组中的最后一个元素放到堆顶,然后调整成堆。
从上往下:让堆顶节点和子节点进行比较,交换到合适位置。

data = a[1];
a[1] = a[count];
--count;
while(i*2<=count){//i有左儿子
    j = i*2;
    if (i*2+1<=count && a[i*2+1]>a[i*2])//i有右儿子,并且右儿子比较大
        j = i*2 + 1;
    if (a[i]>=a[j])
        break;//父节点最大,退出循环
    swap(a, i, j);
    i = j;
}

堆排序

堆排序分为两大步骤,即建堆和排序。

1.建堆

直接的思路是把每个元素加入堆中,完成建堆,是从下往上的方法。
更好的思路是从后往前处理数据,并且每个数据都是从上往下堆化,重点是可以从第一个非叶子节点开始处理,即 n / 2 n/2 n/2 1 1 1,其余节点都是叶子节点。

//伪代码,建堆的复杂度为O(N)
bulidHeap(int a[], int n){
    for(int i=n/2; i>=1; --i){
        heapify(a, n, i)
    }
}
void heapify(int a[], int n, int i){
    int maxPos = i;
    if (i*2<=n && a[i*2]>a[maxPos]) maxPos = i*2;
    if (i*2+1<=n && a[i*2+1]>a[maxPos]) maxPos = i*2+1;
    if (maxPos == i) break;
    swap(a, i, maxPos);
    i = maxPos;
}
2.排序

类似于删除堆顶元素,首先将堆顶元素与下标为 n 的元素交换,将剩下的 n-1 个元素堆化,再将堆顶元素与下标为 n-1 的元素交换,将剩下的 n-2 个元素堆化,依次下去,完成排序。

bulidHeap(a, n);
int k = n;
while(k>1){
    swap(a, 1, k);
    --k;
    heapify(a, k, 1);
}

分析

  1. 堆排序是原地排序算法,只用到了个别的临时存储空间。

  2. 建堆复杂度为 O ( N ) O(N) O(N),堆化为 O ( l o g N ) O(logN) O(logN),排序过程为 O ( N l o g N ) O(NlogN) O(NlogN)

  3. 堆排序不是稳定的排序算法。在排序过程中,将堆顶节点与最后一个节点交换可能会改变相同数据的相对位置。

  4. 堆排序的交换此处比快速排序多。

实现优先队列

优先队列本质上是一个队列,但入队和出队的顺序不是先进先出,而是按照优先级的大小,优先队列也可以用数组实现,但是用堆实现优先队列是最高效的。

优先队列的入队过程就是向堆中添加元素,出队过程就是取出堆顶元素。复杂度都为 O ( l o g N ) O(logN) O(logN)

使用优先队列可以实现多个有序数组的合并。例如leetcode23-合并K个排序链表

实现 Top K 问题

求解 top K 问题就是从 N 个数据中,选取最大的 K 个,最直接的办法就是排序,取 K,时间复杂度最优为 O ( N l o g N ) O(NlogN) O(NlogN)

利用堆,可以实现更高效的算法。我们可以维护一个大小为 K 的小顶堆(若是取第 K 小的元素,则维护大小为 N-K+1 大小的小顶堆),然后接着遍历数组,从数组中取出取数据与堆顶元素比较。如果比堆顶元素大,我们就用这个元素替换堆顶元素。如果比堆顶元素小,则不做任何处理。遍历完成后,堆中的数据就是前 K 大数据了。这样处理的时间复杂度为 O ( N l o g K ) O(NlogK) O(NlogK).
例题1:leetcode347-前K个高频元素
解答:

import heapq as hq
class Solution:
    def topKFrequent(self, nums, k):
        """
        :type nums: List[int]
        :type k: int
        :rtype: List[int]
        """
        # 1. 排序 前k个
        # 2. 小顶堆 自己实现
        # 3. 小顶堆 heapq库实现
        data = []
        d = dict()
        for num in nums:
            d[num] = d.get(num, 0) + 1
        for key, val in d.items():
            if len(data) < k:
                hq.heappush(data, (val, key))
            elif val > data[0][0]:
                hq.heappop(data)
                hq.heappush(data, (val, key))
        ans = []
        while(len(data)>0):
            temp = hq.heappop(data)
            ans.append(temp[1]);
        return ans[::-1]

例题2:leetcode692-前K个高频单词
解答:

import heapq as hq
class Solution:
    def topKFrequent(self, words, k):
        """
        :type words: List[str]
        :type k: int
        :rtype: List[str]
        """
        class Freq:
            def __init__(self, word, freq):
                self.word = word
                self.freq = freq
            def __lt__(self, other):
                if self.freq < other.freq:
                    return -1
                if self.freq == other.freq and self.word > other.word: #字母序小的优先级大
                    return -1
        d = {}
        for word in words:
            d[word] = d.get(word, 0) + 1
        data = []
        for key, val in d.items():
            f = Freq(key, val)
            if (len(data) < k):
                hq.heappush(data, f)
            elif data[0] < f:
                hq.heappop(data)
                hq.heappush(data, f)
        ans = []
        while(len(data)>0):
            ans.append(hq.heappop(data).word)
            
        return ans[::-1]
                

求解中位数

针对动态数据,每次排序之后再求解中位数效率比较低,应用堆,可以不用排序,求得中位数。

使用一个大顶堆和一个小顶堆,初始时,两个堆为空,将第一个元素添加到任意一个堆中。逐渐添加元素,并保证,大顶堆中的堆顶元素比小于等于小顶堆中的堆顶元素,并且大顶堆中的元素个数至多比小顶堆中的元素个数多一,保证两边平衡。

即添加元素时,如果小于等于大顶堆的堆顶元素,就加入到大顶堆中,否则,加入到小顶堆中。
如果小顶堆中元素个数大于大顶堆中元素个数,则移动小顶堆堆顶元素到大顶堆中。如果大顶堆元素个数比小顶堆元素个数多于 1,将堆顶元素个数移动到小顶堆中。

这样如果有 n 个数,n 为偶数,两个堆中各有一半元素,取各自堆顶元素,取平均即为中位数。若 n 为奇数, 大顶堆中有 n/2+1 个元素,小顶堆中有 n 个元素,大顶堆的堆顶元素即为中位数。

例题:leetcode295
解答:

import heapq as hq
class MaxHeap:
    def __init__(self):
        self.data = []
        # self.n = 0
        self.cmp = lambda x: -x

    def compare(self, x):
        return -x

    def push(self, x):
        hq.heappush(self.data, (self.compare(x), x))
    def pop(self):
        return hq.heappop(self.data)[1]
    def size(self):
        return len(self.data)
    def top(self):
        return self.data[0][1]
    def println(self):
        print("MaxHeap", end=":")
        for num in self.data:
            print(num, end=',')
        print()
class MinHeap:
    def __init__(self):
        self.data = []
    # def compare(self, x):
    def push(self, x):
        hq.heappush(self.data, x)
    def pop(self):
        return hq.heappop(self.data)
    def size(self):
        return len(self.data)
    def top(self):
        return self.data[0]
    def println(self):
        print("MinHeap", end=":")
        for num in self.data:
            print(num, end=',')
        print()
class MedianFinder:
    
    def __init__(self):
        """
        initialize your data structure here.
        """
        self.maxheap = MaxHeap()
        self.minheap = MinHeap()
        

    def addNum(self, num):
        """
        :type num: int
        :rtype: void
        """
        # if (num > self.minheap.data[0]):
        #     self.maxheap.push(num)
        # else:
        #     self.minheap.push(num)
        # 如果小于等于大顶堆的元素,插入到大顶堆
        if (self.maxheap.size() > 0) and (num < self.maxheap.top()):
            self.maxheap.push(num)
        else:
            self.minheap.push(num)
        # 移动元素, 保证大顶堆中有n/2(+1)个元素 
        if (self.minheap.size() > self.maxheap.size()):
            self.maxheap.push(self.minheap.pop())
        elif (self.maxheap.size() - self.minheap.size()>1):
            self.minheap.push(self.maxheap.pop())
        # self.minheap.println()
        # self.maxheap.println()
        #print("-"*10)
    def findMedian(self):
        """
        :rtype: float
        """
        if (self.minheap.size()==0) and (self.maxheap.size()==0):
            return 0
        if ( self.minheap.size() == self.maxheap.size()):
            return ((self.minheap.top()+ self.maxheap.top()) / 2.0)
            
        else:
            return self.maxheap.top()

你可能感兴趣的:(数据结构)