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

堆是什麼?

堆是基於樹抽象數據類型的一種特殊的數據結構,用於許多算法和數據結構中。一個常見的例子就是優先隊列,還有排序算法之一的堆排序。這篇文章咱們將討論堆的屬性、不一樣類型的堆以及 堆的常見操做。另外咱們還將學習堆排序,並將使用SPL實現堆。php

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

clipboard.png

咱們看根節點,值100大於兩個子節點19和36。對於19來講,該值大於17和3。其餘節點也適用相同的規則。咱們能夠看到,這棵樹沒有徹底排序。但重要的事實是咱們總能找到樹的最大值或最小值,在許多特殊的狀況下這是很是有用的。github

堆結構有不少種,如二叉堆、B堆、斐波那契堆、三元堆,樹堆、弱堆等。二叉堆是堆實現中最流行的一種。二叉堆是一個徹底二叉樹(不瞭解二叉樹的朋友能夠看PHP實現二叉樹),樹的全部內部節點都被徹底填充,最後一層能夠徹底填充的或部分填充。對於二叉堆,咱們能夠在對數時間複雜度內執行大部分操做。算法

堆的操做

堆是一個特殊的樹數據結構。咱們首先根據給定的數據構建堆。因爲堆有嚴格的構建規則,因此咱們每一步操做都必須知足這個規則。下面是堆的一些核心操做。api

  • 建立堆bash

  • 插入新值數據結構

  • 從堆中提取最小值或最大值數據結構和算法

  • 刪除一個值post

  • 交換學習

從給定的項或數字集合建立堆須要咱們確保堆規則和二叉樹屬性獲得知足。這意味着父節點必須大於或小於子節點。對於樹中的全部節點,都須要遵照這個規則。一樣,樹必須是一個徹底的二叉樹。在建立堆時,咱們從一個節點開始,並向堆中插入一個新節點。

當插入節點操做時,咱們不能從任意節點開始。插入操做以下

  • 將新節點插入堆的底部

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

  • 若是它們不是正確的順序,交換它們而後繼續前一步的檢查。這一步驟與前一步一塊兒被稱爲篩分或上升,等等。

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

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

在堆中,一個重要的操做是交換。如今咱們將使用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語言特色的深刻研究。

相關文章
相關標籤/搜索