一,堆的有序性
使堆快速执行的性质是堆的有序性
每个节点都大于或者小于其儿子节点的关键字
结论:
堆的插入和删除都是O(logN)
数据结构 | 入队 | 出队 |
---|---|---|
普通数组 | O(1) | O(n) |
顺序数组 | O(n) | O(1) |
堆 | O(logN) | O(logN) |
堆的结构性质:
①堆是一棵被完全填满的二叉树,有可能的例外是在底层,底层上的元素从左到右被填入,
这样的树称为完全二叉树
容易证明:
②一棵高是h的完全二叉树有2h到2h+1-1个节点,这意味着完全二叉树的高是O(logN)
③堆中索引为i的元素,左儿子在2i,右儿子在2i+1,父节点在[i/2]位置上
二,堆的操作(最小堆)
①Insert(插入)
在末尾的空穴位置(数组下标为:current_size+1)作为待插入元素的预插入位置,
父节点和插入的关键字比较大小,如果插入的更小,把父节点i/2拷贝到子节点i
注:hole就是空穴的意思!!
自己常犯的错误:
我自己手写堆排的时候就是经常把父节点和子节点弄混了,应该是比较父节点和关键字的值
而不是比较父节点和子节点的关键字值,哎。。。。因为把要在父节点和子节点中间插入一个
关键字。。
template<typename T>
void Heap<T>::insert(const T& ele) {
int hole = ++m_current_size;//在新的位置创建一个空穴
//hole==1时意味着到了堆顶,要跳出循环了,所以循环的条件就是i>1且空穴比父节点更小
for (; hole > 1 && ele < m_array[hole / 2]; hole /= 2) {//父节点上移(hole/=2)
m_array[hole] = m_array[hole / 2];//把父节点(hole/2)移入空穴(hole)中
}
m_array[hole] = ele;//循环终止可能是到了堆顶,或者父节点比小,最后要把元素放到空穴的位置
}
配上C++描述的《数据结构和算法分析》理解一下
不同插入方法的效率差别分析
有一个结论就是:N个节点的完全二叉树(堆),那么你第一个非有序的节点下标就是N/2,
因此从下标为N/2的节点开始下滤,效率最高!!
法一:效率高,复杂度O(N)
void buildHeap() {
for (int i = current_size / 2; i >= 1; i--) {
percolateDown(i);
}
}
线性的插入方法效率低
法二:复杂度:O(N * logN)
void buildHeap(T arr[],int n){
for(int i=0;i< n ;i++){
array[i+1]=arr[i];
}
}
②DeleteMin
通常的做法是先用一个临时变量储存了堆顶元素,用于返回最小值,
然后把堆中最后一个元素先拷贝到堆顶,然后进行下滤,
原因是:
删除最小元的时候,由于堆中少了一个元素,堆的尾元素必须移动到某个合适的位置
由于删除了堆顶元素,尾元素放到堆顶是比较合适的
所以拷贝到堆顶之后(此时空穴就是堆顶),尾元素多了2个儿子,把尾元素和其两个儿子的较小者比较,
如果尾元素就是最小的了,那么删除操作就完成了,但通常这不太可能。
当儿子的较小者比尾元素更小,那么把儿子拷贝上去,空穴下移,空穴再和新的儿子的
较小者比较,如果儿子更小,空穴继续下移,否则,就把空穴填入之前的尾元素,删除完成。
我自己感觉呢,就是通过2次比较,得到(待删除元素tmp,2个儿子)3个元素中最小的那个然后把最小的那个放到当前的空穴中,(如果放入的是tmp,则完成删除)然后空穴下移,接着就是继续比较。。。
配上C++描述的《数据结构和算法分析》理解一下
template<typename T>
void Heap<T>::delete_min(T& ele)
{
if (!isEmpty()) {
ele = m_array[1];//对引用操作,使得函数不需要返回值了
m_array[1] = m_array[m_current_size--];拷贝尾元素到堆顶
percolateDown(1);
}
}
template<typename T>
void Heap<T>::percolateDown(int hole)
{
T tmp = m_array[hole];
int child;
//判断有没有左儿子,如果没有儿子,就说明已经到了最底层了,直接把tmp放到空穴即可
for (; 2 * hole <= m_current_size; hole = child)
{
child = 2 * hole;//获取儿子的下标
//先判断有没有右儿子,&&再判断哪个儿子更小,获取更小的儿子的下标
if (child + 1 <= m_current_size && m_array[child + 1] < m_array[child])
{
child++;
}
if (m_array[child] < tmp) 空穴元素和儿子中的较小者比较
{
m_array[hole] = m_array[child];//儿子更小就拷贝更小的儿子节点到父节点(空穴)
}
else {
break;//如果空穴更小,就说明不需要下滤了,直接跳出循环后把,待删除元素拷贝到空穴里面
}
}
m_array[hole] = tmp;//待删除元素拷贝到空穴里面
}
完整堆排序代码如下:
先给出模板堆排序,再给出测试的主函数
#pragma once
#include<iostream>
#include<vector>
using namespace std;
template<typename T>
class Heap
{
private:
vector<T> m_array;
unsigned int m_current_size;
public:
Heap(const unsigned int size):m_array(size),m_current_size(0){
buildHeap();
}
Heap(const vector<T> & v) : m_array(v.size()+10),m_current_size(v.size())
{
for (int i = 0; i < v.size(); i++) {
m_array[i + 1] = v[i];
}
buildHeap();
}
void buildHeap();
void percolateDown(int i);
void insert(const T& ele);
void delete_min(T& ele);
void delete_min();
bool isEmpty()const;
bool isFull()const;
void for_each();
};
template<typename T>
void Heap<T>::buildHeap() {
for (int i = m_current_size / 2; i >= 1; i--) {
percolateDown(i);
}
}
template<typename T>
void Heap<T>::percolateDown(int hole) {
T tmp = m_array[hole];
int child;
for (; 2 * hole <= m_current_size; hole = child) {
child = 2 * hole;
if (child + 1 <= m_current_size && m_array[child + 1] < m_array[child]) {
child++;
}if (m_array[child] < tmp) {
m_array[hole] = m_array[child];
}
else {
break;
}
}
m_array[hole] = tmp;
}
template<typename T>
void Heap<T>::insert(const T& ele) {
//if (!isFull()) {
int hole = ++m_current_size;
for (; hole > 1 && ele < m_array[hole / 2]; hole /= 2) {
m_array[hole] = m_array[hole / 2];
}
m_array[hole] = ele;
//}
//else {
//return;
//}
}
template<typename T>
void Heap<T>::delete_min(T& ele)
{
if (!isEmpty()) {
ele = m_array[1];
m_array[1] = m_array[m_current_size--];
percolateDown(1);
}
}
template<typename T>
void Heap<T>::delete_min()
{
if (!isEmpty()) {
m_array[1] = m_array[m_current_size--];
percolateDown(1);
}
}
template<typename T>
inline bool Heap<T>::isEmpty() const
{
return m_current_size == 0;
}
template<typename T>
void Heap<T>::for_each()
{
int a;
while (!isEmpty()) {
delete_min(a);
cout << a << "\t";
}
}
// MyHeap.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include"Heap.h"
#include <iostream>
#include<cstdlib>
#include<ctime>
int main()
{
const int MAX = 50;
srand(time(NULL));
vector<int> v;
Heap<int> heap(10);
heap.insert(888);
heap.insert(555);
heap.insert(552);
heap.insert(587);
heap.insert(365);
heap.insert(582);
heap.insert(999);
heap.for_each();
}
个人对堆操作的一些理解
《数据结构与算法分析C语言描述》P147页
①DecreaseKey降低关键字的值,有可能破坏堆的有序性,因此从指定下标P处改变关键字的值需要进行上滤操作!!
原因是本书前面讨论是最小堆,所以在P处降低关键字的值,不需要更新P下面的节点,相反,
有可能因为P的关键字变小了,比父节点更小,就是需要【上滤】了!!
②IncreaseKey提升关键字的值,同①的分析
对n个数组的前第k大个数的理解
最快的排序也就O(N*log(N)),直接暴力也就这样,但是堆排在这题里面比较特殊
可以优化到O(N+ k * log(N)),弹出来的时候控制一下输出格式,只弹出第k个就行