PHP面試:說下什麼是堆和堆排序?

蕭瀟發表於2018-08-19

堆是什麼?

堆是基於樹抽象資料型別的一種特殊的資料結構,用於許多演算法和資料結構中。一個常見的例子就是優先佇列,還有排序演算法之一的堆排序。這篇文章我們將討論堆的屬性、不同型別的堆以及 堆的常見操作。另外我們還將學習堆排序,並將使用SPL實現堆。

根據定義,堆是一個擁有堆特性的樹形資料結構。如果父節點大於子節點,那麼它被稱為最大堆,如果父節點小於子節點,則稱為最小堆。下圖是最大堆的例子

clipboard.png

我們看根節點,值100大於兩個子節點19和36。對於19來說,該值大於17和3。其他節點也適用相同的規則。我們可以看到,這棵樹沒有完全排序。但重要的事實是我們總能找到樹的最大值或最小值,在許多特殊的情況下這是非常有用的。

堆結構有很多種,如二叉堆、B堆、斐波那契堆、三元堆,樹堆、弱堆等。二叉堆是堆實現中最流行的一種。二叉堆是一個完全二叉樹(不瞭解二叉樹的朋友可以看PHP實現二叉樹),樹的所有內部節點都被完全填充,最後一層可以完全填充的或部分填充。對於二叉堆,我們可以在對數時間複雜度內執行大部分操作。

堆的操作

堆是一個特殊的樹資料結構。我們首先根據給定的資料構建堆。由於堆有嚴格的構建規則,所以我們每一步操作都必須滿足這個規則。下面是堆的一些核心操作。

  • 建立堆

  • 插入新值

  • 從堆中提取最小值或最大值

  • 刪除一個值

  • 交換

從給定的項或數字集合建立堆需要我們確保堆規則和二叉樹屬性得到滿足。這意味著父節點必須大於或小於子節點。對於樹中的所有節點,都需要遵守這個規則。同樣,樹必須是一個完全的二叉樹。在建立堆時,我們從一個節點開始,並向堆中插入一個新節點。

當插入節點操作時,我們不能從任意節點開始。插入操作如下

  • 將新節點插入堆的底部

  • 檢查新節點和父節點的大小順序,如果它們是正確的順序,停止。

  • 如果它們不是正確的順序,交換它們然後繼續前一步的檢查。這一步驟與前一步一起被稱為篩分或上升,等等。

提取操作(最小或最大)即從堆中取出根節點。在此之後,我們必須執行下列操作以確保剩餘節點然仍符合堆的特點。

  • 從堆移動最後一個節點作為新根
  • 將新根節點與子節點進行比較,如果它們處於正確的順序,則停止。
  • 如果不是,則將根節點與子節點交換(當是小根堆時為最小子節點,當大根堆時為最大子節點)並繼續前面的步驟。這一步與前一個步驟一起被稱為下堆。

在堆中,一個重要的操作是交換。現在我們將使用PHP7來實現二叉堆。

namespace DataStructure\Heap;

class MaxHeap
{
    public $heap;
    public $count;

    public function __construct(int $size)
    {
        //初始化堆
        $this->heap = array_fill(0, $size, 0);
        $this->count = 0;
    }

    public function create(array $arr = [])
    {
        array_map(function($item){
            $this->insert($item);
        }, $arr);
    }

    public function insert(int $data)
    {
        //插入資料操作
        if ($this->count == 0) {
            //插入第一條資料
            $this->heap[0] = $data;
            $this->count = 1;
        } else {
            //新插入的資料放到堆的最後面
            $this->heap[$this->count++] = $data;
            //上浮到合適位置
            $this->siftUp();
        }
    }

    public function display()
    {
		return implode(" ", array_slice($this->heap, 0));
    }

    public function siftUp()
    {
        //待上浮元素的臨時位置
        $tempPos = $this->count - 1;    
        //根據完全二叉樹性質找到父節點的位置
        $parentPos = intval($tempPos / 2);

        while ($tempPos > 0 && $this->heap[$parentPos] < $this->heap[$tempPos]) {
            //當不是根節點並且父節點的值小於臨時節點的值,就交換兩個節點的值
            $this->swap($parentPos, $tempPos);
            //重置上浮元素的位置
            $tempPos = $parentPos;
            //重置父節點的位置
            $parentPos = intval($tempPos / 2);
        }
    }

    public function swap(int $a, int $b)
    {
        $temp = $this->heap[$a];
        $this->heap[$a] = $this->heap[$b];
        $this->heap[$b] = $temp;
    }

    public function extractMax()
    {
        //最大值就是大跟堆的第一個值
        $max = $this->heap[0];
        //把堆的最後一個元素作為臨時的根節點
        $this->heap[0] = $this->heap[$this->count - 1];
        //把最後一個節點重置為0
        $this->heap[--$this->count] = 0;
        //下沉根節點到合適的位置
        $this->siftDown(0);

        return $max;
    }

    public function siftDown(int $k)
    {
        //最大值的位置
        $largest = $k;
        //左孩子的位置
        $left = 2 * $k + 1;
        //右孩子的位置
        $right = 2 * $k + 2;


        if ($left < $this->count && $this->heap[$largest] < $this->heap[$left]) {
            //如果左孩子大於最大值,重置最大值的位置為左孩子
            $largest = $left;
        }

        if ($right < $this->count && $this->heap[$largest] < $this->heap[$right]) {
            //如果右孩子大於最大值,重置最大值的位置為右孩子
            $largest = $right;
        }


        //如果最大值的位置發生改變
        if ($largest != $k) {
            //交換位置
            $this->swap($largest, $k);
            //繼續下沉直到初始位置不發生改變
            $this->siftDown($largest);
        }
    }
}
複製程式碼

複雜度分析

因為不同種類的堆有不同的實現,所以各種堆實現也有不同的複雜度。但是有一個堆的操作在各類實現中都是O(1)的複雜度,就是獲取最大值或者最小值。我看來看下二分堆的複雜度分析。

操作 平均複雜度 最壞複雜度
Search O(n) O(n)
Insert O(1) O(log n)
Delete O(log n) O(log n)
Extract O(1) O(1)

因為二叉堆不是完全排序的,所以搜尋操作會比二叉搜尋樹花更多的時間。

堆與優先佇列

一個最常用的操作就是將堆當作優先佇列來使用。在PHP實現棧PHP實現佇列中,我們已經瞭解到優先佇列是一種根據元素權重而不是入隊順序來進行出隊操作的結構。我們已經用連結串列實現優先佇列Spl實現優先佇列,現在我們使用堆來實現優先佇列。

namespace DataStructure\Heap;

class PriorityQueue extends MaxHeap
{
    public function __construct(int $size)
	{
		parent::__construct($size);
	}

	public function enqueue(int $val)
	{
		parent::insert($val);
	}

	public function dequeue()
	{
		return parent::extractMax();
	}
}
複製程式碼

堆排序

在堆排序中,我們需要用給定的值構建一個一個堆。然後連續的檢查堆的值以確保任何時候整個堆都是排序的。在正常的堆結構中,我們每當插入一個新的值到合適位置之後就停止檢查,但是在堆排序中,只要有下一個值,我們就不斷的去檢查構建堆。虛擬碼如下:

HeapSort(A)
BuildHeap(A)
for i = n-1 to 0
swap(A[0],A[i])
n = n - 1
Heapify(A, 0)

BuildHeap(A)
n = elemens_in(A)
for i = floor(n / 2) to 0
Heapify(A, i)

Heapify(A, i)
left = 2i+1;
right = 2i + 2;
max = i

if (left < n and A[left] > A[i])
max = left
if (right < n and A[right] > A[max])
max = right

if (max != i)
swap(A[i], A[max])
Heapify(A, max)
複製程式碼

從上面的虛擬碼可以看到,堆排序的第一步就是構建一個堆。每次我們向堆中新增新的元素,我們都呼叫heapify來滿足堆的特性。一旦堆構建好之後,我們對所有的元素都進行檢查,下面使用PHP的實現堆排序。完整的程式碼可以點這裡檢視。

function heapSort(&$arr)
{
    $length = count($arr);
    buildHeap($arr);
    $heapSize = $length - 1;
    for ($i = $heapSize; $i >= 0; $i--) {
        list($arr[0], $arr[$heapSize]) = [$arr[$heapSize], $arr[0]];
        $heapSize--;
        heapify(0, $heapSize, $arr);
    }
}

function buildHeap(&$arr)
{
    $length = count($arr);
    $heapSize = $length - 1;
    for ($i = ($length / 2); $i >= 0; $i--) {
        heapify($i, $heapSize, $arr);
    }
}

function heapify(int $k, int $heapSize, array &$arr)
{
    $largest = $k;
    $left = 2 * $k + 1;
    $right = 2 * $k + 2;

    if ($left <= $heapSize && $arr[$k] < $arr[$left]) {
        $largest = $left;
    }

    if ($right <= $heapSize && $arr[$largest] < $arr[$right]) {
        $largest = $right;
    }

    if ($largest != $k) {
        list($arr[$largest], $arr[$k]) = [$arr[$k], $arr[$largest]];
        heapify($largest, $heapSize, $arr);
    }
}
複製程式碼

堆排序的時間複雜度為O(nlog n),空間複雜度為O(1)。對比歸併排序,堆排序有更好的表現。

PHP中的SplHeap、SplMinHeap和SplMaxHeap

當然,方便的PHP內建的標準庫已經幫助我實現了堆,你可以通過SplHeapSplMinHeapSplMaxHeap來使用它們。

更多內容

PHP基礎資料結構專題系列目錄: 地址。主要使用PHP語法總結基礎的資料結構和演算法。還有我們日常PHP開發中容易忽略的基礎知識和現代PHP開發中關於規範、部署、優化的一些實戰性建議,同時還有對Javascript語言特點的深入研究。

相關文章