資料結構之PHP(最大堆)實現優先佇列

thinkabel發表於2021-01-19

圖片

前面我們一起學習了陣列, 棧, 佇列, 連結串列, 二分搜尋樹. 今天我們來學習一下使用最大堆這種資料結構實現優先佇列. 先了解一下什麼是優先佇列。

一、優先佇列概念
聽這個名字就能知道,優先佇列也是一種佇列。在普通佇列遵循的原則是 先進先出;後進後出 原則。但是在優先佇列中, 出隊順序和入隊順序無關,和優先順序有關係。在有些情況下,可能需要找到元素集合中的最小或者最大元素,就可以使用優先佇列來完成操作。
他支援插入和刪除最小值操作(返回並刪除最小元素)或刪除最大值操作(返回並刪除最大元素). 這些操作等同於 佇列的 enQueue *和 *deQueue操作,區別在於,對於優先佇列,元素進入佇列的順序可能與其被操作的順序不同,作業排程是優先佇列的一個應用例項,它根據優先順序的高低而不是先到先服務的方式來進行排程;

二、二叉堆

一、堆性質

二叉堆 是一種特殊的完全二叉樹, 二叉堆也叫 堆()

當 堆 中的所有節點大於等於它的子節點, 我們稱這個堆為最大堆,相反,當堆中所有的節點小於等於它的子節點,我們稱這個堆為最小堆。

圖片

(堆 的示意圖)

二、堆實現本質

堆本質上是一個完全二叉樹, 所以可以使用陣列這樣的資料結構進行標識,

若索引是從 i=0 開始的,這樣對於下標為i 的結點 a[i]來說,其左孩子的下標為 left(i) = 2∗i+1,右孩子的下標為 2∗i+2。且不論 i 是奇數還是偶數,其父親結點(如果有的話)就是 parent = (i - 1)/2 取整。

圖片

(下標從0開始 的示意圖)

若索引是從 i=1 開始的,這樣對於下標為i 的結點 a[i]來說,其左孩子的下標為 left(i) = 2∗i,右孩子的下標為 right(i) = 2∗i+1。且不論 i 是奇數還是偶數,其父親結點(如果有的話)就是 parent(i) = **i/2 取整。**

圖片

(下標從1開始 的示意圖)

三、優先佇列最基本的操作

1、新增元素( sift up)

當有新的資料加入到優先佇列的時, 新的資料首先被放在二叉堆的底部。不斷的進行向上篩選的操作,即如果發現該資料的優先順序別比父節點的優先順序別還要高,那麼就和父節點的元素進行相互交換,再接著繼續往上進行比較,直到無法進行交換為止。

圖片

圖片

(新增一個元素52 的示意圖)

2、取出元素( sift down)

當堆頂的元素被取出時,要更新堆頂的元素來作為下一次按照優先順序順序被取出的物件,需要將堆底部的元素放置到堆頂, 然後不斷的對他執行往下篩選操作。
將該元素和他兩個孩子節點比較優先順序,如果優先順序最高的是其中的一個孩子,就將該元素和那個孩子進行交換,然後反覆進行下去,直到無法繼續交換為止。

圖片

圖片

圖片

(取出最大元素 的示意圖)
四、程式碼實現

MaxHeap.php

<?php
/**
 * Created by : PhpStorm
 * User: think abel
 * Date: 2021/1/18 0018
 * Time: 23:45
 */

include 'ArrayStructure/ArrayStructure.php';

class MaxHeap
{
    private $data;

    public function __construct(int $capacity)
    {
        $this->data = new ArrayStructure($capacity);
    }

    /**
     * Notes: 獲取數量
     * User: think abel
     * Date: 2021/1/18 0018
     * Time: 23:47
     *
     * @return int
     */
    public function getSize(): int
    {
        return $this->data->getSize();
    }

    /**
     * Notes: 陣列是否為空
     * User: think abel
     * Date: 2021/1/18 0018
     * Time: 23:48
     *
     * @return bool
     */
    public function isEmpty(): bool
    {
        return $this->data->isEmpty();
    }

    /**
     * Notes: 返回完全二叉樹的陣列表示中, 一個索引所表示元素的父親節點的索引
     * User: think abel
     * Date: 2021/1/18 0018
     * Time: 23:49
     *
     * @param int $index
     *
     * @return string
     */
    private function parent(int $index)
    {
        if ($index == 0) {
            return "index-0 doesn't have parent.";
        }

        return ($index - 1) / 2;
    }

    /**
     * Notes: 返回完全二叉樹的陣列表示中, 一個索引所表示元素的左孩子節點的索引
     * User: think abel
     * Date: 2021/1/18 0018
     * Time: 23:52
     *
     * @param int $index
     *
     * @return int
     */
    private function left(int $index): int
    {
        return ($index * 2) + 1;
    }

    /**
     * Notes: 返回完全二叉樹的陣列表示中, 一個索引所表示元素右孩子節點的索引
     * User: think abel
     * Date: 2021/1/18 0018
     * Time: 23:52
     *
     * @param int $index
     *
     * @return int
     */
    private function right(int $index): int
    {
        return ($index * 2) + 2;
    }

    /**
     * Notes: 往堆中新增元素
     * User: think abel
     * Date: 2021/1/19 0019
     * Time: 0:00
     *
     * * @param $val
     */
    public function add($val)
    {
        $this->data->addLast($val);
        $this->siftUp($this->data->getSize() - 1);
    }

    /**
     * Notes: 向堆中新增元素 (資料上浮)
     * User: think abel
     * Date: 2021/1/19 0019
     * Time: 0:11
     *
     * @param $key
     */
    private function siftUp($key)
    {
        // 傳遞的key 要大於 0 並且 傳過來索引位置的元素 還要 小於新增的元素
        while ($key > 0 && $this->data->get($this->parent($key)) < $this->data->get($key)) {
            $this->data->swap($key, $this->parent($key));
            $key = $this->parent($key);
        }
    }

    /**
     * Notes: 往堆中移除元素
     * User: think abel
     * Date: 2021/1/19 0019
     * Time: 0:00
     *
     */
    public function remove()
    {
        $max = $this->findMax();
        // 把最大的元素和最後一位元素進行交換
        $this->data->swap(0, $this->data->getSize() - 1);
        $this->data->removeLast();

        $this->siftDown(0);
        return $max;
    }

    /**
     * Notes: 往堆中移除元素  (資料下沉)
     * User: think abel
     * Date: 2021/1/19 0019
     * Time: 0:15
     *
     * @param $key
     */
    public function siftDown($key)
    {
        while ($this->left($key) < $this->data->getSize()) {
            $leftChild = $this->left($key);

            // 如果右子樹索引 小於 當前 陣列大小 (說明有右子樹)
            // 並且 當前右子樹索引位置上的值 大於 左子樹索引位置上的值
            if (
                $leftChild + 1 < $this->data->getSize()
                && $this->data->get($leftChild + 1) > $this->data->get($leftChild)
            ) {
                $leftChild = $this->right($key);
            }

            // 此時 $this->data[$leftChild] 是 leftChild 和 rightChild 中 最大的值
            if ($this->data->get($key) >= $this->data->get($leftChild)) {
                break;
            }

            $this->data->swap($key, $leftChild);
            $key = $leftChild;
        }

    }

    /**
     * Notes: 發現元素中最大元素
     * User: think abel
     * Date: 2021/1/19 0019
     * Time: 0:18
     */
    public function findMax()
    {
        if (!$this->data->getSize()) {
            return "Can not findMax when heap is empty";
        }

        return $this->data->getFirst();
    }
}

ArrayStructure.php

<?php

class ArrayStructure
{
    // 陣列實際元素
    private $size = 0;

    // 陣列的容量大小
    private $capacity = 0;

    // 用於存放資料
    private $data = [];

    /**
     * ArrayStruct constructor.
     *
     * @param int $capacity 陣列容量大小
     */
    public function __construct($capacity = 10)
    {
        $this->capacity = $capacity;
    }

    /**
     * Notes: 獲取陣列實際元素個數
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 15:12
     *
     * @return int
     */
    public function getSize(): int
    {
        return $this->size;

    }

    /**
     * Notes: 擴容
     * User: think abel
     * Date: 2021/1/10 0010
     * Time: 17:08
     *
     * @param $factor
     */
    protected function resize($factor)
    {
        $this->capacity = $factor * $this->capacity;

    }

    /**
     * Notes: 獲取陣列的容量
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 15:14
     *
     * @return int
     */
    public function getCapacity(): int
    {
        return $this->capacity;

    }

    /**
     * Notes: 陣列是否為空
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 15:16
     *
     * @return bool
     */
    public function isEmpty(): bool
    {
        return $this->size === 0;

    }

    /**
     * Notes: 往陣列指定位置插入資料
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 15:27
     *
     * @param int $index 需要插入的下標/索引
     * @param int $ele   需要插入的元素
     */
    public function add(int $index, $ele): void
    {
        // 如果當前的實際大小 等於 當前容量, 則不可以進行插入
        try {
            if ($this->size === $this->capacity) {
                throw new Exception("新增失敗, Array 已經到達最大容量");
            }

            if ($index < 0 || $index > $this->size) {
                throw new Exception("新增失敗, index require >= 0 and <= " . $this->size);
            }

            /**
             * 從最後一個元素開始進行遍歷, 直到下標 為 當前輸入的 下標, 終止
             * 每次將當前遍歷的數值 往後 移動一位
             */
            for ($i = $this->size - 1; $i >= $index; $i --) {
                $this->data[$i + 1] = $this->data[$i];
            }

            // 目前當前下標還是存在之前的元素, 因為當前元素已經移動到後面一位, 所以直接覆蓋就好
            $this->data[$index] = $ele;

            // 維護當前實際大小
            $this->size ++;
        }
        catch (Exception $e) {
            echo $e->getMessage();
        }

    }

    /**
     * Notes: 向所有元素後新增一個元素
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 15:19
     */
    public function addLast($element): void
    {
        $this->add($this->size, $element);

    }

    /**
     * Notes: 向第一位新增一個元素
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 15:19
     */
    public function addFirst(int $element): void
    {
        $this->add(0, $element);

    }

    /**
     * Notes: 陣列轉字串
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 15:50
     *
     * @return string
     */
    public function toString(): string
    {
        $str = 'Array: size = ' . $this->size . ',' . 'capacity = ' . $this->capacity . PHP_EOL;
        $str .= '[';
        for ($i = 0; $i < $this->size; $i ++) {
            $str .= $this->data[$i];
            if ($i != $this->size - 1) {
                $str .= ',';
            }
        }

        $str .= ']';
        return $str . PHP_EOL;

    }

    /**
     * Notes: 獲取指定下標位置的元素
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 16:13
     *
     * @param int $index
     *
     * @return int
     */
    public function get($index)
    {
        try {
            if ($index < 0 || $index >= $this->size) {
                throw new Exception("獲取失敗, index require >= 0 and < " . $this->size);
            }

            return $this->data[$index];
        }
        catch (Exception $e) {
            return $e->getMessage();
        }

    }

    /**
     * Notes: 獲取最後一個
     * User: think abel
     * Date: 2021/1/10 0010
     * Time: 15:48
     *
     * @return int
     */
    public function getLast()
    {
        return $this->get($this->size - 1);
    }

    /**
     * Notes: 獲取第一個
     * User: think abel
     * Date: 2021/1/10 0010
     * Time: 15:48
     *
     * @return int
     */
    public function getFirst()
    {
        return $this->get(0);
    }

    /**
     * Notes: 修改指定下標位置的元素為 ele
     * User: think abel
     * Date: 2021/1/9 0009
     * Time: 12:04
     *
     * @param int $index
     * @param int $ele
     *
     * @return string
     */
    public function set(int $index, int $ele)
    {
        try {
            if ($index < 0 || $index >= $this->size) {
                throw new Exception("獲取失敗, index require >= 0 and < " . $this->size);
            }

            $this->data[$index] = $ele;
        }
        catch (Exception $e) {
            return $e->getMessage();
        }

    }

    /**
     * Notes: 刪除指定位置上的元素
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 16:19
     *
     * @param int $index
     *
     * @return int|string
     */
    public function remove(int $index): int
    {
        try {
            if ($index < 0 || $index >= $this->size) {
                throw new Exception("移除失敗, index require >= 0 and < " . $this->size);
            }

            $return = $this->data[$index];

            for ($i = $index + 1; $i < $this->size; $i ++) {
                $this->data[$i - 1] = $this->data[$i];
            }

            $this->size --;
            return $return;
        }
        catch (Exception $e) {
            return $e->getMessage();
        }

    }

    /**
     * Notes: 刪除第一個
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 16:39
     *
     * @return int
     */
    public function removeFirst(): int
    {
        try {
            return $this->remove(0);
        }
        catch (Exception $e) {
            return $e->getMessage();
        }

    }

    /**
     * Notes: 刪除最後一個
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 16:39
     *
     * @return int
     */
    public function removeLast()
    {
        try {
            return $this->remove($this->size - 1);
        }
        catch (Exception $e) {
            return $e->getMessage();
        }

    }

    /**
     * Notes: 如果有元素, 就刪除
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 16:44
     *
     * @param int $ele
     *
     * @return bool
     */
    public function removeElement(int $ele): bool
    {
        $index = $this->find($ele);
        if ($index != - 1) {
            $this->remove($index);
        }

    }

    /**
     * Notes: 是否包含元素
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 16:22
     *
     * @param int $ele
     *
     * @return bool
     */
    public function contains(int $ele): bool
    {
        for ($i = 0; $i < $this->size; $i ++) {
            if ($this->data[$i] == $ele) {
                return true;
            }
        }

        return false;

    }

    /**
     * Notes: 獲取當前元素的索引
     * Author: PhpStorm
     * Date: 2021/1/8 0008
     * Time: 16:22
     *
     * @param int $ele
     *
     * @return int
     */
    public function find(int $ele): int
    {
        for ($i = 0; $i < $this->size; $i ++) {
            if ($this->data[$i] == $ele) {
                return $i;
            }
        }

        return - 1;

    }

    /**
     * Notes: 交換元素位置
     * User: think abel
     * Date: 2021/1/19 0019
     * Time: 0:06
     *
     * @param int $i
     * @param int $j
     *
     * @return string
     */
    public function swap(int $i, int $j)
    {
        try {
            if ($i < 0 || $i >= $this->size || $j < 0 || $j >= $this->size) {
                throw new Exception("Index is illegal.");
            }

            $t = $this->data[$i];
            $this->data[$i] = $this->data[$j];
            $this->data[$j] = $t;
        }
        catch (Exception $e) {
            return $e->getMessage();
        }

    }
}

五、優先佇列複雜度對比

功能
入隊 出隊(取最大元素)
普通線性結構(陣列、連結串列)
O(1)
O(n)(遍歷所有元素)
順序線性結構

O(N)
O(1)

O(log n)
O(log n)

六、應用場景

    從一堆雜亂無章的資料當中按照一定的順序(或者優先順序)逐步地篩選岀部分乃至全部的資料。

舉例:

    任意一個陣列,找出前k大的數。

    解法1:

        先對這個陣列進行排序,然後依次輸出前k大的數,複雜度將會是 o(nlogn),其中,n是陣列的元素個數。這是一種直接的辦法

    解法2:

        使用優先佇列,複雜度優化成O(logn)

        當資料量很大(即n很大),而k相對較小的時候,顯然,利用優先佇列能有效地降低演算法複雜度, 因為要找出前k大的數,並不需要對所有的數進行排序

有錯誤的地方, 歡迎指正! 祝大家生活愉快~~
圖片

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章