堆数据结构以及堆的应用
大根堆的维护
堆数据结构可以模拟成完全二叉树(每一层节点都是从左到右添加,这层满了才能添加下一层),堆又可以用一个数组来表示如下图(实际上底层就是个数组,树只是更直观地表达而已)
这棵树有很好的性质:
1:已知父节点下标为k,那么左孩子下标为2k,右孩子为2k+1(如果左右孩子都存在的情况下)
2:已知子节点(左右孩子都一样)下标为i,那么父节点下标为(i/2),如果i/2=0那么该节点没有父节点。
我们先说如何通过已有的数组维护成一个大根堆(小根堆类似,只改变符号)
大根堆:所有父节点的值都大于俩孩子的值 小根堆:所有父节点的值都小于俩孩子的值
我们从非叶子节点开始维护,通过数组长度9/2可以获得最后一个非叶子节点下标为4,记为k。
然后把k这个节点的值放到数组第一个位置缓存起来。要满足大根堆的情况则k=4这个父节点必须大于俩孩子的值(如果有两个孩子的话)。通过2k,2k+1,可以获得俩孩子的下标,然后取i为子节点中值更大的节点的下标。可以发现k=4没有右孩子,2k+1>heap_len。(heap_len是数组除去第一个位置剩下元素的个数)因此i为左孩子的小标i=2k=8。发现当前节点k的值(6)小于最大的孩子i的值(8),因此把孩子的值覆盖当前的节点的值。k跳到最大的孩子的下标。如果发现k所在的位置的值大于俩孩子的值,那么就可以把第一个位置缓存的值放到k位置,结束一轮非叶子节点的维护。
总结:k指针指向父节点,i指针指向两子节点中值更大的节点。arr[0]位置是缓存一开始要维护的非叶子节点的值。k就是要遍历寻找一个满足条件的节点位置(父值>子值),然后把arr[0]这个数放到该位置。
//从上(k位置)至下维护堆(不满足节点下沉)
void heapify(int* arr, int k, int heap_len)
{
//先把当前要调整的节点的值放在缓存区,等找到合适的根节点的位置再把它塞进去。合适的根节点位置:arr[0] > arr[i],即根节点的值大于它最大子节点的值
arr[0] = arr[k];
//i是当前根节点的子节点下标
for (int i = k * 2; i <= heap_len; i *= 2)
{
//i < heap_len 保证了有右孩子,如果右孩子比左孩子大则下标i更新为右孩子的下标,否则还是保持左孩子下标
if (i < heap_len && arr[i] < arr[i + 1])
i++;
if (arr[0] > arr[i])
break;
arr[k] = arr[i];
k = i;
}
arr[k] = arr[0];
}
大根堆插入元素
接下来讲大根堆插入元素,与上述不同的是,插入的操作指的是往一个维护好的大根堆里插入元素,其他位置都是维护好的。如下图,往一个大根堆里插入元素2
此时1和2不满足大根堆条件(父值>子值)因此要调整。这里只需要判断当前要插入元素的位置k与父节点i = k/2的值的大小关系,如果不满足条件则把他俩交换,交换完之后,k=k/2,考查父节点满不满足条件,终止条件要么都满足条件,要么没有父节点(k/2=0)
void insert(int* arr, int k)
{
//如果当前子节点有父节点
while (k / 2)
{
//父节点比当前子节点小,则把子节点换上去
if (arr[k / 2] < arr[k])
swap(arr[k / 2], arr[k]);
k /= 2;
}
}
堆排序
步骤:
1、堆顶元素就是最大值,把它扔到你想存储最终结果的地方。
2、然后在堆里将堆顶元素和最末尾的元素交换位置,并且heap_len减1,这样就不会再访问到最后那个元素了,相当于无形之间把它从堆里删除,当然如果用vector存储堆元素的话也可以pop_back()删除掉。
3、末尾元素换到堆顶肯定不满足大根堆的性质了,此时只要维护一下堆,因为其他非叶子节点没有变,只是换了堆顶的元素,所以只需要维护下标1这个位置就可以。
4、结束条件:heap_len减为0。
int main()
{
int arr[9] = { 0,4,5,1,6,2,7,3,8 };
int heap_len = 8;
for (int i = heap_len / 2; i >0; i--)
{
heapify(arr, i, heap_len);
}
vector<int> vec;
for (int i = heap_len; i > 0; i--)
{
vec.push_back(arr[1]);
swap(arr[1], arr[heap_len]);
heapify(arr, 1, --heap_len);
}
for (int &i :vec)
cout << i << " ";
return 0;
}
寻找数据流中的中位数
意思就是源源不断有数据往一个数组里面添加,要实时地更新这堆数的中位数。
核心:
1、维护一大根堆,一个小根堆。
2、大根堆里的数<小根堆里的数(大根堆堆顶元素<小根堆堆顶元素)
3、两堆元素数量不超过1
4、最终如果两堆元素数量相同,中位数等于两堆顶元素平均值;如果不同,哪堆元素数量多(多1个),那堆顶元素就是中位数。
下面给出两种实现:1、利用C++的优先队列(堆)实现 2、手撕堆(本人代码比较挫,各位见怪莫怪)
优先队列实现
class MedianFinder {
private:
// 大根堆
priority_queue<int, vector<int>, less<int>> maxHeap;
// 小根堆
priority_queue<int, vector<int>, greater<int>> minHeap;
public:
/** initialize your data structure here. */
MedianFinder() {
}
void addNum(int num) {
if(maxHeap.size()==0)
maxHeap.push(num);
else
{
if(num<maxHeap.top())
{
maxHeap.push(num);
if(maxHeap.size()-minHeap.size()>1)
{
minHeap.push(maxHeap.top());
maxHeap.pop();
}
}
else
{
minHeap.push(num);
if(minHeap.size()-maxHeap.size()>1)
{
maxHeap.push(minHeap.top());
minHeap.pop();
}
}
}
}
double findMedian() {
if(maxHeap.size()==minHeap.size())
{
return (maxHeap.top()+minHeap.top())/2.0;
}
else
return maxHeap.size()>minHeap.size()?maxHeap.top():minHeap.top();
}
};
手撕堆
class MedianFinder {
private:
int B_cont;
int S_cont;
vector<int> B_heap;
vector<int> S_heap;
public:
/** initialize your data structure here. */
MedianFinder() {
//大根堆维护较小的一半数
B_heap.push_back(0);
//大根堆里元素个数
B_cont = 0;
//小根堆维护较大的一半数
S_heap.push_back(0);
//小根堆里元素个数
S_cont = 0;
}
//所有对堆的操作都要传引用,否则操作的只是一个副本而已!
void B_insert(vector<int>& heap,int k)
{
while(k/2)
{
if(heap[k/2]<heap[k])
swap(heap[k/2],heap[k]);
k/=2;
}
}
void S_insert(vector<int>& heap,int k)
{
while(k/2)
{
if(heap[k/2] > heap[k])
swap(heap[k/2],heap[k]);
k/=2;
}
}
void B_heapify(vector<int>& heap,int k,int heap_len)
{
heap[0] = heap[k];
for(int i=k*2;i<=heap_len;i*=2)
{
if(i<heap_len && heap[i]<heap[i+1])
i++;
if(heap[0]>heap[i])
break;
heap[k] = heap[i];
k = i;
}
heap[k] = heap[0];
}
void S_heapify(vector<int>& heap,int k,int heap_len)
{
heap[0] = heap[k];
for(int i=k*2;i<=heap_len;i*=2)
{
if(i<heap_len && heap[i]>heap[i+1])
i++;
if(heap[0]<heap[i])
break;
heap[k] = heap[i];
k = i;
}
heap[k] = heap[0];
}
void addNum(int num) {
if(B_cont==0)
{
B_heap.push_back(num);
B_cont++;
}
else if(num>B_heap[1])
{
S_heap.push_back(num);
S_cont++;
S_insert(S_heap,S_cont);
if((S_cont-B_cont)>1)
{
B_heap.push_back(S_heap[1]);
B_cont++;
B_insert(B_heap,B_cont);
swap(S_heap[1],S_heap[S_cont]);
S_cont--;
//记得要删除!!!
S_heap.pop_back();
S_heapify(S_heap,1,S_cont);
}
}
else
{
B_heap.push_back(num);
B_cont++;
B_insert(B_heap,B_cont);
if((B_cont-S_cont)>1)
{
S_heap.push_back(B_heap[1]);
S_cont++;
S_insert(S_heap,S_cont);
// for(int &i:S_heap)
// cout<<i<<" ";
cout<<endl;
swap(B_heap[1],B_heap[B_cont]);
B_cont--;
//记得要删除!!!
B_heap.pop_back();
B_heapify(B_heap,1,B_cont);
}
}
}
double findMedian() {
//两堆数量相同返回两堆顶元素平均值
if(B_cont==S_cont)
return (B_heap[1]+S_heap[1])/2.0;
//否则谁数量多,谁堆顶就是中位数(两堆数量相差最多不超过1)
else if(S_cont>B_cont)
return S_heap[1];
else
return B_heap[1];
}
};
寻找出现频次最高的topK个单词
我这里只是简单的应用了一下,作为学习记录。很多条件没考虑进去,大佬们请忽略。。。
再次强盗一下:priority_queue与vector的sort规则完全相反
注意注意!!!!!这里priority_queue的排序和vector的sort完全相反;
如果vector的sort的话,a.second < b.second;会把第二项小的往前排,大的往后排,升序;
priority_queue刚好相反, a.second < b.second会把小的往后排,大的往前排,降序,即所谓的大根堆!堆顶元素最大。
int类型大根堆这么写:priority_queue<int, vector<int>, less<int>> p_q;
pair的话把所有int
换成pair
类型即可
//注意注意!!!!!这里priority_queue的排序和vector的sort完全相反
//如果vector的sort的话,a.second < b.second;会把第二项小的往前排,大的往后排,升序
//priority_queue刚好相反, a.second < b.second会把小的往后排,大的往前排,降序,即所谓的大根堆!堆顶元素最大
struct cmp {
bool operator()(pair<string, int>& a, pair<string, int>& b)
{
return a.second < b.second;
}
};
int main()
{
vector<string> strs = { "I","really","am","a","really","cat","small","small" };
//输出出现频次最高的前两个单词
int top_k = 2;
//key:单词 val:频次
unordered_map<string,int> hmap;
for (string &str : strs)
hmap[str]++;
//优先队列传入pair时,默认是对第一项就进行,现在频次出现在第二项,因此自定义一个排序函数
priority_queue<pair<string, int>, vector<pair<string, int>>, cmp> p_q;
for(string &str:strs)
//如果已经添加进优先队列的单词,频次置-1,下次在遇到就不会重复加进队列里
if (hmap[str] != -1)
{
p_q.push(make_pair(str, hmap[str]));
hmap[str] = -1;
}
while (top_k--)
{
cout << "key: "<< p_q.top().first <<"\t"<< "times: "<<p_q.top().second<<endl;
p_q.pop();
}
return 0;
}