堆的基本儲存
堆是一種資料結構,它是一顆完全二叉樹(不清楚自行百度)。因此可以用陣列儲存二叉堆。堆分為最大堆和最小堆。
最大堆:任意節點的值不大於其父親節點的值。
下圖為最大堆的結構:
使用堆可以實現優先順序佇列,也可以實現多路歸併排序。多路歸併排序在常見資料結構書中都有涉及,主要用於外部排序,外部排序就是在記憶體容納不下,需要存在檔案中。優先順序佇列可以自己去百度,這裡不展開。
重要:我們從1開始標號:對於完全二叉樹有
父節點(i)=i/2
左孩子(i)= 2*i
右孩子(i)=2*i+1
根據這個關係就可以用陣列實現堆了。
堆的PHP實現
入堆
入堆即向堆中新增新的元素,然後將元素移動到合適的位置,以保證堆的性質不變。在入堆的時候,需要shift_up
操作,向上調整。插入元素後,放在陣列尾部,不停將元素和上方父親元素比較,如果大於則交換元素;直到達到堆頂或者小於等於父親元素
。
public $array = [];
public $count = 0;//計數堆中元素
public function insert($random)
{
$this->array[$this->count + 1] = $random;
$this->count++;
$this->shift_up($this->count);
}
public function shift_up($k)
{
//如果新插入的值比父節點的值小, 則父節點的值下移,直到到達根節點滿足最大堆
while ($k > 1 && $this->array[(int)($k / 2)] < $this->array[$k]) {
$this->swap($this->array[(int)($k / 2)], $this->array[$k]);
$k = (int)($k / 2);
}
}
出堆
出堆只能彈出堆頂元素(最大堆就是最大的元素),然後將位於最後一個位置的元素放入堆頂,重新調整元素位置,這時需要shift_down
操作。此時元素應該和子節點比較,如果大於等於子節點或者沒有子節點,停止比較;否則,選擇子節點中最大的元素,進行交換,執行此步,直到結束。
public function take()
{
$res = $this->array[1];
$this->swap($this->array[1], $this->array[$this->count]);
$this->count--;
//取出第一個元素,堆中第一個元素為最大值
$this->shift_down(1);
return $res;
}
public function shift_down($k)
{
while (2 * $k <= $this->count) {
//$j為左孩子
$j = 2 * $k;
//存在右孩子且比左孩子大就取右孩子
if ($j + 1 <= $this->count && $this->array[$j + 1] > $this->array[$j]) {
$j += 1;
}
//資料大於孩子的最大值則不需要交換
if ($this->array[$k] >= $this->array[$j]) {
break;
}
$this->swap($this->array[$k], $this->array[$j]);
$k=$j;
}
}
改變堆元素
當你改變堆中某個元素的值之後,使用shift_up和shift_down來調整堆。
shift_up($k);
shift_down($k)
堆排序
普通堆排序
實現堆排序很簡單:將元素逐步insert
進入堆,然後再take
逐個取出即可。至此堆排序實現完成。
$xx = new Heap();
for ($i = 0; $i < 10; $i++) {
$xx->insert(random_int(10, 1000));
}
while (!$xx->isEmpty()) {
$res = $xx->take();
echo $res . PHP_EOL;
}
結果:
908
900
877
753
752
712
698
682
677
這個建堆的平均時間複雜度是O(n*logn)
建堆可以優化,這個過程叫做heapify
將陣列的值逐步複製到
$this->array
從第一個非葉子節點開始,執行
shift_down
重複第 2 步,直到堆頂元素
這種建堆方法的時間複雜度是:
O(n)
public function heapify($array)
{
$this->count = count($array);
for ($i = 0; $i < $this->count; $i++) {
$this->array[$i + 1] = $array[i];
}
//(int)($this->count/2)為第一個非葉子節點位置
for ($i = (int)($this->count / 2); $i >= 1; $i--) {
$this->shift_down($i);
}
}
完整程式碼如下:
<?php
class Heap
{
public $array = [];
public $count = 0;
public function insert($random)
{
$this->array[$this->count + 1] = $random;
$this->count++;
$this->shift_up($this->count);
}
public function heapify($array)
{
$this->count = count($array);
for ($i = 0; $i < $this->count; $i++) {
$this->array[$i + 1] = $array[$i];
}
for ($i = (int)($this->count / 2); $i >= 1; $i--) {
$this->shift_down($i);
}
}
public function shift_up($k)
{
//如果新插入的值比父節點的值小, 則父節點的值下移,直到到達根節點滿足最大堆
while ($k > 1 && $this->array[(int)($k / 2)] < $this->array[$k]) {
$this->swap($this->array[(int)($k / 2)], $this->array[$k]);
$k = (int)($k / 2);
}
}
public function shift_down($k)
{
while (2 * $k <= $this->count) {
//$j為左孩子
$j = 2 * $k;
//存在右孩子且比左孩子大就取右孩子
if ($j + 1 <= $this->count && $this->array[$j + 1] > $this->array[$j]) {
$j += 1;
}
//資料大於孩子的最大值則不需要交換
if ($this->array[$k] >= $this->array[$j]) {
break;
}
$this->swap($this->array[$k], $this->array[$j]);
$k = $j;
}
}
public function take()
{
$res = $this->array[1];
$this->swap($this->array[1], $this->array[$this->count]);
$this->count--;
$this->shift_down(1);
return $res;
}
private function swap(&$a, &$b)
{
list ($a, $b) = [$b, $a];
}
public function isEmpty()
{
return $this->count == 0;
}
}
$xx = new Heap();
for ($i = 0; $i < 10; $i++) {
$xx->insert(random_int(10, 1000));
}
while (!$xx->isEmpty()) {
$res = $xx->take();
echo $res . PHP_EOL;
}
原地堆排序
上面闡述的排序方法,藉助實現的最大堆這個類需要開闢空間儲存陣列,空間複雜度為O(n)
。其實藉助shift_down
可以實現原地堆排序
- Heapify先構成最大堆,第一個元素為最大值,與最後一個元素交換。
- 前面n-1元素進行
shift_down
轉化為最大堆,重複第一步直到所有元素都排序完整
此處
__heapify
__shift_down
函式均需要修改為對傳入陣列的修改,和之前程式碼類似,只是不是用$this->array
,具體程式碼就不給出了,可以自己嘗試實現。
···
public function sort($array) {
$this->count = count($array);
// 將一個無序的陣列組成了一個最大堆,第 1 個元素就是最大值
$this->__heapify($array);
// 重複取出最大元素交換來排序完整
for ($i = $this->count-1; $i>=0; $i--) {
$this->swap($array[0],$array[$i]);
$this->__shift_down($i);
}
}
···
$xx = new Heap();
$xx->sort([1,2,4,6,3,9,11]);
索引堆(Index Heap)
陣列中的元素位置發生了改變,我們才能將其構建為最大堆。
由於陣列中元素位置的改變,我們將面臨著幾個侷限性:
- 如果我們的元素是十分複雜的話,那麼交換它們之間的位置將產生大量的時間消耗。
- 由於我們的陣列元素的位置在構建成堆之後發生了改變,那麼我們之後就很難索引到它,很難去改變它。例如我們在構建成堆後,想去改變一個原來元素的優先順序(值),將會變得非常困難。
針對以上問題,我們就需要引入索引堆(Index Heap)的概念。對於索引堆來說,我們將資料和索引這兩部分分開儲存。構建完堆之後,data
域並沒有發生改變,位置改變的是index
域。
在實現索引堆的過程中,我想需要修改insert,shift_up,shift_down,take
為其對應的索引進行操作。
具體PHP實現如下:
<?php
class HeapIndex
{
public $array = [];
public $index = [];
public $count = 0;
public function insert($random)
{
$this->count++;
$this->array[$this->count] = $random;
$this->index[$this->count] = $this->count;
$this->shift_up($this->count);
}
/***
* 堆的向上調整
* @param $k
*/
public function shift_up($k)
{
//如果新插入的值比父節點的值小, 則父節點的值下移,直到到達根節點滿足最大堆
while ($k > 1 && $this->array[$this->index[(int)($k / 2)]]
< $this->array[$this->index[$k]]) {
$this->swap($this->index[(int)($k / 2)], $this->index[$k]);
$k = (int)($k / 2);
}
}
public function shift_down($k)
{
while (2 * $k <= $this->count) {
//$j為左孩子
$j = 2 * $k;
//存在右孩子且比左孩子大就取右孩子
if ($j + 1 <= $this->count &&
$this->array[$this->index[$j + 1]] > $this->array[$this->index[$j]]) {
$j += 1;
}
//資料大於孩子的最大值則不需要交換
if ($this->array[$this->index[$k]] >= $this->array[$this->index[$j]]) {
break;
}
$this->swap($this->index[$k], $this->index[$j]);
$k = $j;
}
}
public function take()
{
$res = $this->array[$this->index[1]];
$this->swap($this->index[1], $this->index[$this->count]);
$this->count--;
$this->shift_down(1);
return $res;
}
private function swap(&$a, &$b)
{
list ($a, $b) = [$b, $a];
}
public function isEmpty()
{
return $this->count == 0;
}
}
$xx = new HeapIndex();
for ($i = 0; $i < 10; $i++) {
$xx->insert(random_int(10, 1000));
}
while (!$xx->isEmpty()) {
$res = $xx->take();
echo $res . PHP_EOL;
}
碼字不易。
本作品採用《CC 協議》,轉載必須註明作者和本文連結