手把手教你写个大顶堆

今天我们来实现一个大顶堆,所谓大顶堆,即根节点的值大于等于其孩子节点的值。废话少絮,直接开始。

堆是一个完全二叉树,很适合用顺序结构来实现,这里我们选择数组。用数组实现堆时,我们不使用数组的0号位置,用1号位置来存放堆顶(当然也可以使用0号位置来存放堆顶,只是下面的性质要改变),此时,有两个很重要的性质我们需要知道,如下:

  1. N号元素的父节点的位置为N/2。
  2. N号元素的左孩子的位置是2N,右孩子的位置是2N+1。

知道这些之后,我们就可以开始实现堆啦(撒花,终于开始了)。

我们从最基础的开始,对于一个堆来说,它需要一个数组来存放数据,需要一个变量来记录元素的个数,如下是一个基础的大顶堆类:

template<typename Item>
class MaxHeap{
  private:
    Item* data; //数据
    int count;  //数据个数
    int capacity; //容量
  public:
    MaxHeap(int capacity) { //构造函数分配空间
        data = new Item(capacity+1);
        count=0;
        this->capacity = capacity;
    }
    ~MaxHeap() { //析构函数释放内存
        delete[] data;
    }
    int size() { //返回堆元素的个数
        return count;
    }
    bool isEmpty() { //判断对是否为空
        return count == 0;
    }
};

data用来存放数据,由于不知道具体有多少数据,所以我们使用指针类型;count用来表示实际的元素个数;capacity表示最多能存放多少数据。用户不需要直接访问这些数据,所以我们将其作为私有类型。

然后我们提供一个构造函数,参数是堆的容量,我们在构造函数里为堆分配内存,并设置实际元素的个数和容量的大小。既然分配了内存,相应的,析构函数里就需要释放这些内存。

除此之外,我们还提供了两个工具函数,分别返回堆内元素的个数和判断堆是否为空。

现在我们有了一个基础的堆,并且可以给这个堆分配空间了:

MaxHeap<int> maxheap = MaxHeap<int>(100);

有了空间之后,我们怎么往这个空间里添加元素呢?我们需要提供一个插入函数:在不超过容量限制的情况下,每次将新元素放置在data的最后,此时,新元素可能不满足大顶堆的性质,我们比较新元素与其父元素之间的大小关系,调整新元素与父元素的位置,直到整个堆满足大顶堆的要求。

假设已经有了调整新元素与父元素之间位置的函数shiftUp,那么,插入函数就很简单了:

void insert(Item item) { //向堆中插入一个元素
  assert(count+1 <= capacity); //首先判断空间是否够用
  data[count+1] = item; //将新元素放到数组的最后
  count++; //元素个数加1
  shiftUp(count); //对堆进行调整
}

重点是shiftUp函数,由于用户不需要直接调用这个函数,所以我们将其作为私有的函数:

void shiftUp(int k) { //调整堆的第k个元素
  while(k > 1 && data[k/2] < data[k]) { //判断第k个元素是否比其父元素大
    swap(data[k/2], data[k]); //交换
    k /= 2;
  }
}

学会了插入元素,下面我们要学习怎么从堆里删除一个元素了:我们每次只能删除堆顶的元素,然后将最后一个元素交换到堆顶,此时新的堆顶元素可能不满足堆的性质,所以我们比较堆顶与其孩子之间的大小关系,并调整其位置,直到整个堆合法。

仍然假设已经有了调整新堆顶与孩子之间位置的函数shiftDown,那么,删除函数也很简单:

Item getMax() {
  assert(count > 0);
  Item res = data[1];
  swap(data[1], data[count]);
  count--;
  shiftDown(1);
  return res;
}

shiftDown函数的实现如下:首先判断该元素是否有孩子节点,如果该元素还有右孩子,则比较两个孩子的大小,找到较大的孩子,然后比较该元素与较大的孩子节点的大小,如果当前元素小于较大的孩子节点,则交换两者的位置。然后,交换后的元素再次与其孩子进行比较,直到满足堆的条件。

void shiftDown(int k) {
  while(2*k <= count) {
    int j = 2*k;
    if(j+1 <= count && data[j] < data[j+1]) {
      j += 1;
    }
    if(data[k] >= data[j]) {
      break;
    } 
    swap(data[k], data[j]);
    k = j;
  }
}

现在我们的对已经基本可以使用了。为了测试方便,我们还可以增加一个打印堆内元素的工具函数:

void printData() { //打印堆中的元素
  for(int i = 1; i <= count; i++) {
    cout<<data[i]<<" ";
  }
}

下面我们可以测试一下了:

int main() {
    MaxHeap<int> maxheap = MaxHeap<int>(100);//构造一个堆
    srand(time(NULL));
    for(int i = 0; i < 15; i++) {//给堆内的元素赋随机值
        maxheap.insert(rand() % 100);
    }
    maxheap.printData(); //打印堆内的数据
    cout<<maxheap.size()<<endl;//打印堆元素的个数

    while(!maxheap.isEmpty()) { //从大到小依次输出堆内的每一个元素
        cout<<maxheap.getMax()<<endl;
    }
    return 1;
}

我们的堆已经可以工作了,是不是很开心呀!也许不会说:

我不想每次都构造一个空的堆,然后再一个一个的插入元素啊,我想要直接用数组构造一个堆啊

聪明的你,这种想法简直太棒了。一个一个插入元素的做法,时间复杂度是O(nlgn),如果直接使用数组来构造堆,时间复杂度可以为O(lgN)哟,你说你是不是很棒!

好了,现在我们开始写这个构造函数吧。思路也很简单:我们直接将数组元素作为堆的元素,然后从第一个非叶节点的元素(第一个有孩子的元素)开始(我们可以认为每个叶节点都是大顶堆),一直到堆顶元素,如果某个元素不满足堆的性质,我们就调整其与孩子节点的位置。

MaxHeap(Item a[], int n) { //使用数组构造堆
  data = new Item(n+1); //初始化堆
  capacity = n;
  for(int i = 0; i < n; i++) {
    data[i+1] = a[i];
  }
  count = n;
  for(int i = count/2; i >= 1; i--) {//调整堆
    shiftDown(i);
  }
}

我们现在已经写完自己的堆啦!大顶堆类的完整代码如下:

template<typename Item>
class MaxHeap{
private:
  Item* data; //数据
  int count;  //数据个数
  int capacity; //容量
  void shiftUp(int k) { //调整堆的第k个元素
    while(k > 1 && data[k/2] < data[k]) { //判断第k个元素是否比其父元素大
      swap(data[k/2], data[k]); //交换
      k /= 2;
    }
  }
  void shiftDown(int k) {
    while(2*k <= count) {
      int j = 2*k;
      if(j+1 <= count && data[j] < data[j+1]) {
        j += 1;
      }
      if(data[k] >= data[j]) {
        break;
      } 
      swap(data[k], data[j]);
      k = j;
    }
  }
public:
  MaxHeap(int capacity) { //构造函数分配空间
    data = new Item(capacity+1);
    count=0;
    this->capacity = capacity;
  }
  MaxHeap(Item a[], int n) { //使用数组构造堆
    data = new Item(n+1);
    capacity = n;
    for(int i = 0; i < n; i++) {
      data[i+1] = a[i];
    }
    count = n;
    for(int i = count/2; i >= 1; i--) {
      shiftDown(i);
    }
  }

  ~MaxHeap() { //析构函数释放内存
    delete[] data;
  }
  int size() { //返回堆元素的个数
    return count;
  }
  bool isEmpty() { //判断对是否为空
    return count == 0;
  }

  void insert(Item item) { //向堆中插入一个元素
    assert(count+1 <= capacity); //首先判断空间是否够用
    data[count+1] = item; //将新元素放到数组的最后
    count++; //元素个数加1
    shiftUp(count); //对堆进行调整
  }
  Item getMax() {
    assert(count > 0);
    Item res = data[1];
    swap(data[1], data[count]);
    count--;
    shiftDown(1);
    return res;
  }
  void printData() { //打印堆中的元素
    for(int i = 1; i <= count; i++) {
      cout<<data[i]<<" ";
    }
  }
};

参考简书算法课,侵删。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 1 序 2016年6月25日夜,帝都,天下着大雨,拖着行李箱和同学在校门口照了最后一张合照,搬离寝室打车去了提前租...
    RichardJieChen阅读 5,016评论 0 12
  • 第3章 基本概念 3.1 语法 3.2 关键字和保留字 3.3 变量 3.4 数据类型 5种简单数据类型:Unde...
    RickCole阅读 5,037评论 0 21
  • 1 初级排序算法 排序算法关注的主要是重新排列数组元素,其中每个元素都有一个主键。排序算法是将所有元素主键按某种方...
    深度沉迷学习阅读 1,335评论 0 1
  • 像对每位异性都怀有期待一样 来看望我 问候手掌上的缺口,问候干糙的脸颊 ———请竭力付出一点 用来吻吻我的落满灰尘...
    汤米呢阅读 238评论 0 3
  • 儿行千里,母担忧,可怜天下父母心。看到这边书,忍不住读下去。对于我来说,除了读书看报,以及写文章。才是我的最爱,其...
    我爱吃任何鱼阅读 359评论 1 4