2. 堆
堆就是用数组实现的二叉树,所有它没有使用父指针或者子指针。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。
堆的常用方法:
- 构建优先队列
- 支持堆排序
- 快速找出一个集合中的最小值(或者最大值)
堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。
在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。这就是所谓的“堆属性”,并且这个属性对堆中的每一个节点都成立。
这里有一篇介绍的非常好的文章:
https://www.jianshu.com/p/6b526aa481b1
堆排序
-
创建一个堆 H[0……n-1];
-
把堆首(最大值)和堆尾互换;
-
把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
-
重复步骤 2,直到堆的尺寸为 1。
#include <stdio.h>
void Swap(int *heap, int len); /* 交换根节点和数组末尾元素的值 */
void BuildMaxHeap(int *heap, int len);/* 构建大顶堆 */
int main()
{
int a[6] = {7, 3, 8, 5, 1, 2};
int len = 6; /* 数组长度 */
int i;
for (i = len; i > 0; i--)
{
BuildMaxHeap(a, i);
Swap(a, i);
}
for (i = 0; i < len; i++)
{
printf("%d ", a[i]);
}
return 0;
}
/* Function: 构建大顶堆 */
void BuildMaxHeap(int *heap, int len)
{
int i;
int temp;
for (i = len/2-1; i >= 0; i--)
{
if ((2*i+1) < len && heap[i] < heap[2*i+1]) /* 根节点大于左子树 */
{
temp = heap[i];
heap[i] = heap[2*i+1];
heap[2*i+1] = temp;
/* 检查交换后的左子树是否满足大顶堆性质 如果不满足 则重新调整子树结构 */
if ((2*(2*i+1)+1 < len && heap[2*i+1] < heap[2*(2*i+1)+1]) || (2*(2*i+1)+2 < len && heap[2*i+1] < heap[2*(2*i+1)+2]))
{
BuildMaxHeap(heap, len);
}
}
if ((2*i+2) < len && heap[i] < heap[2*i+2]) /* 根节点大于右子树 */
{
temp = heap[i];
heap[i] = heap[2*i+2];
heap[2*i+2] = temp;
/* 检查交换后的右子树是否满足大顶堆性质 如果不满足 则重新调整子树结构 */
if ((2*(2*i+2)+1 < len && heap[2*i+2] < heap[2*(2*i+2)+1]) || (2*(2*i+2)+2 < len && heap[2*i+2] < heap[2*(2*i+2)+2]))
{
BuildMaxHeap(heap, len);
}
}
}
}
/* Function: 交换交换根节点和数组末尾元素的值*/
void Swap(int *heap, int len)
{
int temp;
temp = heap[0];
heap[0] = heap[len-1];
heap[len-1] = temp;
}
top K问题
LeetCode374
https://leetcode-cn.com/problems/top-k-frequent-elements/solution/qian-k-ge-gao-pin-yuan-su-by-leetcode/
- 快速排序法
在快速排序中,每一轮排序都会将序列一分为二,左子区间的数都小于基准数,右子区间的数都大于基准数,而快速排序用来解决TopK问题,也是基于此的。N个数经过一轮快速排序后,如果基准数的位置被换到了i,那么区间[0,N-1]就被分为了[0,i-1]和[i+1,N-1],这也就是说,此时有N-1-i个数比基准数大,i个数比基准数小,假设N-1-i=X那么就会有以下几种情况:
①X=K。这种情况说明比基准数大的有K个,其他的都比基准数小,那么就说明这K个比基准数大的数就是TopK了;
②X<K。这种情况说明比基准数大的数不到K个,但是这X肯定是属于TopK中的TopX,而剩下的K-X就在[0,i]之间,此时就应当在[0,i]中找到Top(K-X),这就转换为了TopK的子问题,可以选择用递归解决;
③X>K。这种情况说明比基准数大的数超过了K个,那么就说明TopK必定位于[i+1,N-1]中,此时就应当继续在[i+1,N-1]找TopK,这样又成了TopK的一个子问题,也可以选择用递归解决。
代码思路如下(有点问题,超时)
class Solution {
public:
int partition(vector<int> &nums, int l, int r){
int base = nums[l];
int base_index = l;
while(l<r){
while(l<r && nums[r]>base) --r;
while(l<r && nums[l]<base) ++l;
swap(nums[l], nums[r]);
}
nums[base_index] = nums[l];
nums[l] = base;
return l;
}
int find_topK_index(vector<int> &nums, int k, int l, int r){
int index = partition(nums, l, r);
int count = r-index;
if(count == k){
return index;
}
else if(count<k){
// cout<<index<<endl;
return find_topK_index(nums, k-count, l, index-1);
}
return find_topK_index(nums, k, index+1, r);
}
vector<int> topKFrequent(vector<int>& nums, int k) {
int len = nums.size();
if(len==k){
return nums;
}
vector<int> temp(nums.begin(), nums.end());
int index = find_topK_index(temp, k, 0, len-1);
vector<int> result;
for(int i=len-1; i>index; --i){
result.push_back(index);
}
return result;
}
};
- 堆排序法
出自LeetCode上面的题解:
https://leetcode-cn.com/problems/top-k-frequent-elements/solution/leetcode-di-347-hao-wen-ti-qian-k-ge-gao-pin-yuan-/
题目最终需要返回的是前 kk 个频率最大的元素,可以想到借助堆这种数据结构,对于 kk 频率之后的元素不用再去处理,进一步优化时间复杂度。
具体操作为:
- 借助 哈希表 来建立数字和其出现次数的映射,遍历一遍数组统计元素的频率
- 维护一个元素数目为 kk 的最小堆
- 每次都将新的元素与堆顶元素(堆中频率最小的元素)进行比较
- 如果新的元素的频率比堆顶端的元素大,则弹出堆顶端的元素,将新的元素添加进堆中
- 最终,堆中的 kk 个元素即为前 kk 个高频元素
- 桶排序法
首先依旧使用哈希表统计频率,统计完成后,创建一个数组,将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标即可。
O(n)的复杂度,python写起来比较简单。
C++写也可以,需要用unsorted_map实现哈希表,用list容器实现桶。
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
times = {}
for i in nums:
if i in times:
times[i] += 1
else:
times[i] = 1
bottom = [[] for _ in range(len(nums)+1)]
for i in times.keys():
time = times[i]
bottom[time].append(i)
res = []
for i in bottom[::-1]:
res.extend(i)
return res[0:k]