用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 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实现完整代码

你可能感兴趣的:(用C++自己实现一个堆)