前面我們一起學習了陣列, 棧, 佇列, 連結串列, 二分搜尋樹. 今天我們來學習一下使用最大堆這種資料結構實現優先佇列. 先了解一下什麼是優先佇列。
一、優先佇列概念
聽這個名字就能知道,優先佇列也是一種佇列。在普通佇列遵循的原則是 先進先出;後進後出 原則。但是在優先佇列中, 出隊順序和入隊順序無關,和優先順序有關係。在有些情況下,可能需要找到元素集合中的最小或者最大元素,就可以使用優先佇列來完成操作。
他支援插入和刪除最小值操作(返回並刪除最小元素)或刪除最大值操作(返回並刪除最大元素). 這些操作等同於 佇列的 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 協議》,轉載必須註明作者和本文連結