手把手教你写个大顶堆

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

堆是一个完全二叉树,很适合用顺序结构来实现,这里我们选择数组。用数组实现堆时,我们不使用数组的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]<<" ";
    }
  }
};

参考简书算法课,侵删。

推荐阅读更多精彩内容

  • 1 序 2016年6月25日夜,帝都,天下着大雨,拖着行李箱和同学在校门口照了最后一张合照,搬离寝室打车去了提前租...
    StarryThrone阅读 4,279评论 0 12
  • 第3章 基本概念 3.1 语法 3.2 关键字和保留字 3.3 变量 3.4 数据类型 5种简单数据类型:Unde...
    RickCole阅读 4,013评论 0 20
  • 1 初级排序算法 排序算法关注的主要是重新排列数组元素,其中每个元素都有一个主键。排序算法是将所有元素主键按某种方...
    深度沉迷学习阅读 748评论 0 1
  • 像对每位异性都怀有期待一样 来看望我 问候手掌上的缺口,问候干糙的脸颊 ———请竭力付出一点 用来吻吻我的落满灰尘...
    汤米呢阅读 90评论 0 3
  • 儿行千里,母担忧,可怜天下父母心。看到这边书,忍不住读下去。对于我来说,除了读书看报,以及写文章。才是我的最爱,其...
    我爱吃任何鱼阅读 230评论 1 4
  • 题目:每2天1次作业,选取一个产品很小的一个切入点去写,力求写得深入彻底。 第二篇 知乎live 本文将已第一场知...
    Lewis08阅读 358评论 2 3
  • 安装mysql数据库 sudo docker run -d -p 3306:3306 --name mysql ...
    曾经的曾经兜是曾经阅读 87评论 0 0