PHP 程式設計師的堆學習

thus發表於2020-04-01

堆的基本儲存

堆是一種資料結構,它是一顆完全二叉樹(不清楚自行百度)。因此可以用陣列儲存二叉堆。堆分為最大堆和最小堆。

最大堆:任意節點的值不大於其父親節點的值。

下圖為最大堆的結構:

使用堆可以實現優先順序佇列,也可以實現多路歸併排序。多路歸併排序在常見資料結構書中都有涉及,主要用於外部排序,外部排序就是在記憶體容納不下,需要存在檔案中。優先順序佇列可以自己去百度,這裡不展開。

重要我們從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

  1. 將陣列的值逐步複製到$this->array

  2. 第一個非葉子節點開始,執行shift_down

  3. 重複第 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可以實現原地堆排序

  1. Heapify先構成最大堆,第一個元素為最大值,與最後一個元素交換。
  2. 前面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)

陣列中的元素位置發生了改變,我們才能將其構建為最大堆。

由於陣列中元素位置的改變,我們將面臨著幾個侷限性:

  1. 如果我們的元素是十分複雜的話,那麼交換它們之間的位置將產生大量的時間消耗。
  2. 由於我們的陣列元素的位置在構建成堆之後發生了改變,那麼我們之後就很難索引到它,很難去改變它。例如我們在構建成堆後,想去改變一個原來元素的優先順序(值),將會變得非常困難。

針對以上問題,我們就需要引入索引堆(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 協議》,轉載必須註明作者和本文連結
  • 如果可以,我要變成光

相關文章