手把手教你寫個大頂堆

今天我們來實現一個大頂堆,所謂大頂堆,即根節點的值大于等于其孩子節點的值。廢話少絮,直接開始。

堆是一個完全二叉樹,很適合用順序結構來實現,這里我們選擇數組。用數組實現堆時,我們不使用數組的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日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,157評論 0 12
  • 第3章 基本概念 3.1 語法 3.2 關鍵字和保留字 3.3 變量 3.4 數據類型 5種簡單數據類型:Unde...
    RickCole閱讀 5,161評論 0 21
  • 1 初級排序算法 排序算法關注的主要是重新排列數組元素,其中每個元素都有一個主鍵。排序算法是將所有元素主鍵按某種方...
    深度沉迷學習閱讀 1,444評論 0 1
  • 像對每位異性都懷有期待一樣 來看望我 問候手掌上的缺口,問候干糙的臉頰 ———請竭力付出一點 用來吻吻我的落滿灰塵...
    湯米呢閱讀 255評論 0 3
  • 兒行千里,母擔憂,可憐天下父母心??吹竭@邊書,忍不住讀下去。對于我來說,除了讀書看報,以及寫文章。才是我的最愛,其...
    我愛吃任何魚閱讀 382評論 1 4