完全二叉树适合使用数组存储数据,可以直接使用下标来找到父亲的左右儿子和儿子的父亲。
比如根从1开始,下标为 i i i 的节点的左右儿子为 i ∗ 2 i*2 i∗2 和 i ∗ 2 + 1 i*2+1 i∗2+1,父亲为 i / 2 i/2 i/2;下标为 i i i 的节点的左右儿子为 i ∗ 2 + 1 i*2+1 i∗2+1 和 i ∗ 2 + 2 i*2+2 i∗2+2,父亲为 ( i − 1 ) / 2 (i-1)/2 (i−1)/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;
}
堆排序分为两大步骤,即建堆和排序。
直接的思路是把每个元素加入堆中,完成建堆,是从下往上的方法。
更好的思路是从后往前处理数据,并且每个数据都是从上往下堆化,重点是可以从第一个非叶子节点开始处理,即 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;
}
类似于删除堆顶元素,首先将堆顶元素与下标为 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);
}
分析:
堆排序是原地排序算法,只用到了个别的临时存储空间。
建堆复杂度为 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)。
堆排序不是稳定的排序算法。在排序过程中,将堆顶节点与最后一个节点交换可能会改变相同数据的相对位置。
堆排序的交换此处比快速排序多。
优先队列本质上是一个队列,但入队和出队的顺序不是先进先出,而是按照优先级的大小,优先队列也可以用数组实现,但是用堆实现优先队列是最高效的。
优先队列的入队过程就是向堆中添加元素,出队过程就是取出堆顶元素。复杂度都为 O ( l o g N ) O(logN) O(logN)。
使用优先队列可以实现多个有序数组的合并。例如leetcode23-合并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()