用C++自己实现一个堆

无图无真相, 先上个图:
![最大堆](http://g.gravizo.com/g?
graph G {
20 -- 15;
15 -- 10;
15 -- 6;
20 -- 9;
9 -- 7;
}
)
上图就是一颗特殊的二叉树, 著名的堆; 在C++, Java等语言中又叫优先队列.

堆的基本性质:

  1. 堆分为最大堆和最小堆, 它们主要的差异就是: 最大堆是每个节点比它的每个子女节点大(或相等), 最小堆是每个节点比它的每个子女节点小(或相等); 本文以最大堆为例来介绍堆;
  2. 首先它是一颗完全二叉树, 也就是说: 除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点(如上图所示, 只有7的右边位置是缺少节点的);
  3. 要么根节点没有左子女节点, 要么它比左子女节点大;
  4. 要么根节点没有右子女节点, 要么它比右子女节点大;
  5. 它的左子树也要符合这样的定义;
  6. 它的右子树也要符合这样的定义;
  7. 父节点和子女节点之间有严格大小关系. 但是, 如果两个节点之间, 任意一个节点都不是另外一个节点的祖先节点, 那么它们之间的大小关系是不确定的.

堆的基本操作:

  1. 插入新元素;
  2. 删除堆顶元素;

一些约定:

  1. 动态扩容: 由于本人能力有限, 在模仿java的ArrayList等的扩容策略时发现, 跟c++的vector对比效率太低, 故该功能就交给vector来实现了;
  2. 由于堆是一颗完全二叉树, 所以我们可以用数组模拟, 数组下标为0的节点不用, 根节点下标为1, 它的左子女下标为2, 右子女下标为3. 整体上可以这样描述: 下标为i的节点, 它的左子女节点下标为i * 2, 右子女下标为i * 2 + 1, 父节点下标为i / 2(取整), 下面是一张7个节点的堆的下标图:
    ![堆的下标](http://g.gravizo.com/g?
    graph G {
    label = "(节点内的数字代表节点所在数组的下标, 不是节点的值)"
    1 -- 2;
    1 -- 3;
    2 --4;
    2 --5
    3 -- 6;
    3 -- 7;
    }
    )

代码实现

  1. 先看成员参数:
vector<T> data; //节点信息载体
int _size = 0; //堆的大小
static const int MIN_SIZE = 15; //堆最小的长度
static const int MAX_SIZE = 1 << 29; //堆最大的长度
  1. 两个构造方法:
PriorityQueue() {
      ensureCapacity(MIN_SIZE);
  }

  PriorityQueue(int size) {
      ensureCapacity(max(MIN_SIZE, size));
  }

它们都调用了:

void ensureCapacity(int size) {
  assert(size < MAX_SIZE && size >= 0);
  // only data[1, data.size()-1] available
  data.resize(size + 1);
}

这边稍微解释一下: 原本data的下标使用范围是[0, data.size()-1], 但是下标0弃用, 所以需要size+1长度的data才能存放下size个节点, vector::resize(size)的意思是, 将数组长度重置为size.

  1. 两个简单的方法:
void clear() {
  _size = 0;
}
int size() {
  return _size;
}

清空堆的时候不需要多余操作(如果是Java实现, 则需要将data里的每个对象置空, 方便GC), 只要将_size置为0即可; 获取堆长度时返回_size可以了.

  1. push(T t):
void push(T t) {
  ensureCapacity(++_size);
  data[_size] = t;
  siftUp(_size);
}

它调用了siftUp(index):

void siftUp(int index) {
      while (index > 1) {
          int parentIndex = index >> 1;
          if (data[parentIndex] < data[index]) {
              swap(data[parentIndex], data[index]);
              index = parentIndex;
          } else {
              break;
          }
      }
  }

向上调整策略: 当堆新加入一个元素时, 我们先将它放在最后一层的从左往右数的第一个空位(如果最后一层满了, 则放在下一层的最左边的位置). 这时候, 它暂时不满足堆的定义, 我们通过调整, 不断跟父节点比较, 如果父节点比它小, 就跟父节点交换位置, 并向上递归调整, 直到父节点不小于它为止, 下面是一个简单的演示:
初始时的堆:
![初始堆](http://g.gravizo.com/g?
graph G {
20 -- 15;
15 -- 10;
15 -- 6;
20 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
6 -- 2;
}
)
插入16, 由于第一个空位是6的右子女位置, 所以加入之后:
![中间状态1](http://g.gravizo.com/g?
graph G {
20 -- 15;
15 -- 10;
15 -- 6;
20 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
6 -- 2;
6 -- 16[label = "6 < 16" color=yellow];
16[color=red];
}
)
由于16的父节点6比它小, 所以要交换位置, 这时候树变成了:
![中间状态2](http://g.gravizo.com/g?
graph G {
20 -- 15;
15 -- 10;
15 -- 16[label="15 < 16", color=yellow];
20 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
16 -- 2;
16 -- 6;
16[color=red];
}
)
调整之后, 16的父节点是15, 还是比它小, 16继续跟父节点交换位置:
![调整完成后的堆](http://g.gravizo.com/g?
graph G {
20 -- 16;
16 -- 10;
16 -- 15;
20 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
15 -- 2;
15 -- 6;
}
)
调整之后, 父节点为20, 已经比它大了, 至此, 调整结束, 这颗二叉树又已经符合堆的定义了.

  1. 取堆顶元素:
  T top() {
      assert(_size > 0);
      return data[1];
  }

很简单, 堆顶元素所处数组的下标永远是1;

  1. 删除堆顶元素:
  void pop() {
      assert(_size > 0);
      if (--_size > 0) {
          data[1] = data[_size + 1];
          siftDown(1);
      }
  }

它调用了siftDown(index):

  void siftDown(int index) {
      while (true) {
          int leftChildIndex = index << 1;
          int rightChildIndex = index << 1 | 1;
          if (leftChildIndex > _size) {
              break;
          }
          //find max child node
          int maxChildIndex = leftChildIndex;
          if (rightChildIndex <= _size && data[leftChildIndex] < data[rightChildIndex]) {
              maxChildIndex = rightChildIndex;
          }
          if (data[index] < data[maxChildIndex]) {
              swap(data[index], data[maxChildIndex]);
              index = maxChildIndex;
          } else {
              break;
          }
      }
  }

向下调整策略: 当删除了堆顶元素之后, 若当前堆非空, 则将最右边的节点放到堆顶位置, 这时也暂时不满足了堆的定义. 我们将不断向下调整, 每次找到这个节点的子女节点中最大的跟其比较, 如果比它大, 则交换位置, 然后继续递归下去调整, 直到它的所有子女节点都不比它大为止. 以上图中刚刚插入新节点的堆为例:
将最右边的节点(6)放入堆顶位置:
![中间状态1](http://g.gravizo.com/g?
graph G {
6 -- 16[label="16 > 6"];
16 -- 10;
16 -- 15;
6 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
15 -- 2;
6[color=red]
16[color=yellow]
}
)
由于6的最大的子女节点为16, 比它大, 交换位置:
![中间状态2](http://g.gravizo.com/g?
graph G {
16 -- 6;
6 -- 10;
6 -- 15[label="15 > 6"];
16 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
15 -- 2;
6[color=red]
15[color=yellow]
}
)
这时候, 6的最大子女节点为15, 还是比它大, 继续调整:
![t调整完整后的堆](http://g.gravizo.com/g?
graph G {
16 -- 15;
15 -- 10;
15 -- 6;
16 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
6 -- 2;
}
)
最终, 这颗二叉树又满足堆的定义了. 思考: 为什么向下调整时要选择较大的节点?

最后, 用一个SGU上的题目来验证自己写的堆.

友情链接

题解

源码

PriorityQueue实现完整代码

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

推荐阅读更多精彩内容