PHP的SPL擴充套件庫(一)資料結構

硬核專案經理發表於2021-09-30

SPL 庫也叫做 PHP 標準庫,主要就是用於解決典型問題的一組介面或類的集合。這些典型問題包括什麼呢?比如我們今天要講的資料結構,還有一些設計模式的實現,就像我們之前講過的觀察者模式相關的介面在 SPL 庫中都有提供。話說回來,在 PHP 中,由於語言的特點,其實很多資料結構都和我們用 C 語言實現的略有不同,比如說連結串列,由於沒有結構的概念,所以我們一般會使用類來代表連結串列的結點。除了這個之外,要手寫連結串列還需要連結串列的增、刪、改、查等操作,而 SPL 庫中其實已經幫我們提供了一個雙向連結串列的實現,並且還可以在這個連結串列的基礎上直接實現棧和佇列的操作。

雙向連結串列

在 SPL 庫中,雙向連結串列只需要例項化一個 SplDoublyLinkedList 類就可以了,然後我們就可以對這個例項化之後的雙向連結串列物件進行各種操作。

$dll = new SplDoublyLinkedList();

var_dump($dll->isEmpty()); // bool(true)

$dll->push(200);
$dll->push(300);
$dll->unshift("五號");
$dll->add(2, "六號");

var_dump($dll->isEmpty()); // bool(false)

var_dump($dll);
// object(SplDoublyLinkedList)#1 (2) {
//     ["flags":"SplDoublyLinkedList":private]=>
//     int(0)
//     ["dllist":"SplDoublyLinkedList":private]=>
//     array(4) {
//       [0]=>
//       string(6) "五號"
//       [1]=>
//       int(200)
//       [2]=>
//       string(6) "六號"
//       [3]=>
//       int(300)
//     }
//   }

從程式碼中可以看出,push() 、 unshift() 、add() 方法都是向連結串列中新增資料,而 isEmpty() 則用於判斷連結串列是否為空。直接列印顯示連結串列的內容,可以看到連結串列的內部是一個陣列資料。

var_dump($dll->top()); // int(300)
var_dump($dll->bottom()); // string(6) "五號"

var_dump($dll->pop()); // int(300)
var_dump($dll->shift()); // string(6) "五號"

var_dump($dll->serialize()); // string(25) "i:0;:i:200;:s:6:"六號";"
var_dump($dll->count()); // int(2)

top() 和 bottom() 分別獲取的是連結串列的頂部和底部的資料。而 pop() 和 shift() 則是分別從底部和頂部彈出資料。後面我們會看到,根據設定的不同,它他們也會遵循使用棧還是佇列的方式來彈出資料。

serialize() 方法可以直接獲得序列化後的連結串列內容。count() 方法就是返回連結串列內元素的數量了。

$dll->offsetSet(1, '修改成新六號');
var_dump($dll->offsetGet(1)); // string(18) "修改成新六號"
var_dump($dll->offsetExists(1)); // bool(true)
$dll->offsetUnset(1);
var_dump($dll->offsetExists(1)); // bool(false)

offset 相關的方法函式是根據偏移值來操作連結串列內的資料,其實就可以理解成是根據位置下標來運算元據。

在預設情況下,我們遍歷連結串列的話,是類似於佇列的形式進行輸出的,也就是先進先出的狀態。

for($i=1;$i<5;$i++){
    $dll->push($i);
}

var_dump($dll->getIteratorMode()); // int(0)
$dll->rewind();
while($dll->valid()){
    echo '============', PHP_EOL;
    echo 'key:', $dll->key(), PHP_EOL;
    echo '    current:', $dll->current(), PHP_EOL;
    $dll->next();
}
// ============
// key:0
//     current:200
// ============
// key:1
//     current:1
// ============
// key:2
//     current:2
// ============
// key:3
//     current:3
// ============
// key:4
//     current:4

通過 rewind() 將連結串列指標恢復到開頭,然後通過 valid() 方法判斷當前資料是否有效,next() 用於將連結串列指標移動到下一個,就可以進行資料的遍歷。我們通過設定連結串列的迭代模式,就可以改變連結串列的迭代輸出規則,比如我們需要類似棧型別的後進先出。

$dll->setIteratorMode(SplDoublyLinkedList::IT_MODE_LIFO);
$dll->rewind();
while($dll->valid()){
    echo 'IT_MODE_LIFO============', PHP_EOL;
    echo 'key:', $dll->key(), PHP_EOL;
    echo '    current:', $dll->current(), PHP_EOL;
    $dll->next();
}
// IT_MODE_LIFO============
// key:4
//     current:4
// IT_MODE_LIFO============
// key:3
//     current:3
// IT_MODE_LIFO============
// key:2
//     current:2
// IT_MODE_LIFO============
// key:1
//     current:1
// IT_MODE_LIFO============
// key:0
//     current:200

另外,它還有一個比較好玩的迭代模式,就是直接邊遍歷,邊刪除。

$dll->setIteratorMode(SplDoublyLinkedList::IT_MODE_DELETE);
$dll->rewind();
while($dll->valid()){
    echo 'IT_MODE_DELETE============', PHP_EOL;
    echo 'key:', $dll->key(), PHP_EOL;
    echo '    current:', $dll->current(), PHP_EOL;
    $dll->next();
}
var_dump($dll);
// object(SplDoublyLinkedList)#1 (2) {
//     ["flags":"SplDoublyLinkedList":private]=>
//     int(1)
//     ["dllist":"SplDoublyLinkedList":private]=>
//     array(0) {
//     }
//   }

在使用 IT_MODE_DELETE 進行遍歷之後,連結串列裡面的資料內容也就變成空的了。預設情況下,這個 IteraotrMode 的內容是 SplDoublyLinkedList::IT_MODE_KEEP | SplDoublyLinkedList::IT_MODE_FIFO 這個值,表示的就是資料保持原來的狀態並且使用先進先出的規則。

棧類 SplStack 其實和後面的佇列類 SplQueue 一樣,都是繼承自連結串列類的,也就是說它們其實就是相當於設定好了 IteratorMode 的連結串列物件。所以它們的方法函式其實都沒有什麼太大的區別。

// 棧
$stack = new SplStack();
for($i=1;$i<5;$i++){
    $stack->push($i);
}
var_dump($stack->getIteratorMode()); // int(6)
var_dump($stack);
// object(SplStack)#2 (2) {
//     ["flags":"SplDoublyLinkedList":private]=>
//     int(6)
//     ["dllist":"SplDoublyLinkedList":private]=>
//     array(4) {
//       [0]=>
//       int(1)
//       [1]=>
//       int(2)
//       [2]=>
//       int(3)
//       [3]=>
//       int(4)
//     }
//   }

$stack->rewind();
while($stack->valid()){
    echo '============', PHP_EOL;
    echo 'key:', $stack->key(), PHP_EOL;
    echo '    current:', $stack->current(), PHP_EOL;
    $stack->next();
}
// ============
// key:3
//     current:4
// ============
// key:2
//     current:3
// ============
// key:1
//     current:2
// ============
// key:0
//     current:1

佇列

SplQueue 佇列相對於連結串列類和棧類來說,多了兩個方法。

// 佇列
$queue = new SplQueue();
for($i=1;$i<5;$i++){
    $queue->enqueue($i);
}
var_dump($queue->getIteratorMode()); // int(4)
var_dump($queue);
// object(SplQueue)#3 (2) {
//     ["flags":"SplDoublyLinkedList":private]=>
//     int(4)
//     ["dllist":"SplDoublyLinkedList":private]=>
//     array(4) {
//       [0]=>
//       int(1)
//       [1]=>
//       int(2)
//       [2]=>
//       int(3)
//       [3]=>
//       int(4)
//     }
//   }

$queue->rewind();
while($queue->valid()){
    echo '============', PHP_EOL;
    echo 'key:', $queue->key(), PHP_EOL;
    echo '    current:', $queue->current(), PHP_EOL;
    echo '    info:', $queue->dequeue(), PHP_EOL;
    $queue->next();
}
// ============
// key:0
//     current:1
//     info:1
// ============
// key:1
//     current:2
//     info:2
// ============
// key:2
//     current:3
//     info:3
// ============
// key:3
//     current:4
//     info:4

enqueue() 和 dequeue() 方法分別就是入隊和出隊的意思,其實就可以看成是 push() 和 shift() 的操作,底部新增頂部彈出。

堆疊堆疊,總會有人把堆和棧說成是一個東西,其實它們可是完全不同的兩個資料結構哦。棧是線性的邏輯結構,而堆則一般是樹形的邏輯結構,當然,它們的儲存結構都是可以用相同的連結串列或順序表來表示的。在堆中,有大頂堆和小頂堆的概念,SPL 也為我們分別提供了這兩種實現。(不瞭解堆的同學可以自行查閱相關資料)

大頂堆

$maxHeap = new SplMaxHeap();
for($i=1;$i<5;$i++){
    $maxHeap->insert($i);
}

var_dump($maxHeap);
// object(SplMaxHeap)#4 (3) {
//     ["flags":"SplHeap":private]=>
//     int(0)
//     ["isCorrupted":"SplHeap":private]=>
//     bool(false)
//     ["heap":"SplHeap":private]=>
//     array(4) {
//       [0]=>
//       int(4)
//       [1]=>
//       int(3)
//       [2]=>
//       int(2)
//       [3]=>
//       int(1)
//     }
//   }

var_dump($maxHeap->count()); // int(4)
var_dump($maxHeap->top()); // int(4)

var_dump($maxHeap->extract()); // int(4)

var_dump($maxHeap->count()); // int(3)
var_dump($maxHeap->top()); // int(3)

var_dump($maxHeap);
// object(SplMaxHeap)#4 (3) {
//     ["flags":"SplHeap":private]=>
//     int(0)
//     ["isCorrupted":"SplHeap":private]=>
//     bool(false)
//     ["heap":"SplHeap":private]=>
//     array(3) {
//       [0]=>
//       int(3)
//       [1]=>
//       int(1)
//       [2]=>
//       int(2)
//     }
//   }

$maxHeap->rewind();
while($maxHeap->valid()){
    echo '============', PHP_EOL;
    echo 'key:', $maxHeap->key(), PHP_EOL;
    echo '    current:', $maxHeap->current(), PHP_EOL;
    $maxHeap->next();
}
// ============
// key:2
//     current:3
// ============
// key:1
//     current:2
// ============
// key:0
//     current:1


var_dump($maxHeap->isCorrupted()); // bool(false)
$maxHeap->recoverFromCorruption(); 

SplMaxHeap 類就是用於生成大頂堆例項的類别範本。它通過 insert() 方法插入資料,通過 extract() 方法抽取資料,同樣也包括 count() 和 top() 這類的常用方法函式,以及遍歷相關的那些方法函式。

另外,堆的操作中還包括兩個方法函式,分別用於判斷堆是否處於損壞狀態 isCorrupted() 以及從損壞狀態恢復 recoverFromCorruption() 相關的操作函式。

小頂堆

小頂堆的內容和大頂堆就完全一樣了,只是它的內部結構不同,大頂堆是父結點總是最大的,而小頂堆就是反過來父結點總是最小的資料。

$minHeap = new SplMinHeap();
for($i=1;$i<5;$i++){
    $minHeap->insert($i);
}
var_dump($minHeap);
// object(SplMinHeap)#5 (3) {
//     ["flags":"SplHeap":private]=>
//     int(0)
//     ["isCorrupted":"SplHeap":private]=>
//     bool(false)
//     ["heap":"SplHeap":private]=>
//     array(4) {
//       [0]=>
//       int(1)
//       [1]=>
//       int(2)
//       [2]=>
//       int(3)
//       [3]=>
//       int(4)
//     }
//   }

var_dump($minHeap->top()); // int(1)

大頂堆實現的優先佇列

除了大頂堆和小頂堆的普通操作之外,SPL 庫中還有一個通過大頂堆來實現的優先佇列的類别範本。

$pQueue = new SplPriorityQueue();
for($i=1;$i<5;$i++){
    $pQueue->insert($i, random_int(1,10));
}
var_dump($pQueue);
// object(SplPriorityQueue)#6 (3) {
//     ["flags":"SplPriorityQueue":private]=>
//     int(1)
//     ["isCorrupted":"SplPriorityQueue":private]=>
//     bool(false)
//     ["heap":"SplPriorityQueue":private]=>
//     array(4) {
//       [0]=>
//       array(2) {
//         ["data"]=>
//         int(3)
//         ["priority"]=>
//         int(10)
//       }
//       [1]=>
//       array(2) {
//         ["data"]=>
//         int(4)
//         ["priority"]=>
//         int(7)
//       }
//       [2]=>
//       array(2) {
//         ["data"]=>
//         int(1)
//         ["priority"]=>
//         int(3)
//       }
//       [3]=>
//       array(2) {
//         ["data"]=>
//         int(2)
//         ["priority"]=>
//         int(2)
//       }
//     }
//   }

while($pQueue->valid()){
    var_dump($pQueue->extract());
}
// int(3)
// int(4)
// int(1)
// int(2)

它的操作方法函式和堆的操作方法函式都是一樣的,只是 insert() 方法中多了一個引數可以設定資料的優先順序。通過設定不同的優先順序我們可以看到資料以及遍歷輸出的結果都會發生變化,順序都是以優先順序來確定的。

固定陣列

什麼叫固定陣列呢?在 PHP 中,陣列這個結構非常強大,它即可以是普通下標型別的陣列,也可以 HashMap鍵值對 形式的陣列,它的長度也是不受限制的,只要記憶體夠就可以靈活地處理陣列的長度。不過在靜態語言中,特別是我們學習過的 C 語言中,陣列都是固定長度的,也就是說,陣列的記憶體大小是在陣列初始化的時候就確定好的,如果超出了陣列長度的操作發生,就會產生越界問題。還是通過一個例子來看吧。

// 陣列
$norArr = [];
$norArr[1] = 'b';
$norArr[4] = 'f';

var_dump($norArr);
// array(2) {
//     [1]=>
//     string(1) "b"
//     [4]=>
//     string(1) "f"
//   }

$fArr = new SplFixedArray(5);
$fArr[1] = 'b';
$fArr[4] = 'f';

var_dump($fArr);
// object(SplFixedArray)#7 (5) {
//     [0]=>
//     NULL
//     [1]=>
//     string(1) "b"
//     [2]=>
//     NULL
//     [3]=>
//     NULL
//     [4]=>
//     string(1) "f"
//   }

norArr 是普通的 PHP 陣列,我們新增了兩個資料之後在這個陣列中只有兩個元素。下面的 SplFixedArray 類例項化出來的 fArr 則是固定陣列。它在例項化的時候必須傳遞一個構造引數來指定陣列長度。可以看到,fArr 輸出的結果是固定有 5 個資料的,並且我們沒有賦值的資料都會給一個預設的 NULL 值。是不是和 C 的陣列一樣一樣的。

當然,固定陣列就會有陣列下標越界的問題了。

$fArr[6] = 'h'; // Fatal error: Uncaught RuntimeException: Index invalid or out of range 

不過我們可以手動地修改陣列的大小來改變資料的長度。

$fArr->setSize(7);
$fArr[6] = 'h';
var_dump($fArr->getSize()); // int(7)

現在,陣列的長度就是 7 了,可以存放 7 條資料。它也可以直接從一個普通陣列轉換過來,不過需要注意的是,轉換陣列必須是數字下標型別的陣列,字串鍵的 HashMap 陣列是不可以的哦。

$fArr2 = SplFixedArray::fromArray(range(1,3));
var_dump($fArr2);
// object(SplFixedArray)#8 (3) {
//     [0]=>
//     int(1)
//     [1]=>
//     int(2)
//     [2]=>
//     int(3)
//   }

// $fArr3 = SplFixedArray::fromArray(['new'=>1, 'old'=>2]);
// var_dump($fArr3);
// PHP Fatal error:  Uncaught InvalidArgumentException: array must contain only positive integer keys

剩下的就是和其它資料結構一樣的一些操作方法函式了。

var_dump($fArr->count()); // int(7)

var_dump($fArr->offsetGet(2)); // NULL
$fArr->offsetSet(2, 'new'); 
var_dump($fArr->offsetGet(2)); // string(3) "new"
var_dump($fArr->offsetExists(2)); // bool(true)
$fArr->offsetUnset(2);
var_dump($fArr->offsetExists(2)); // bool(false)


$fArr->rewind();
while($fArr->valid()){
    echo '============', PHP_EOL;
    echo 'key:', $fArr->key(), PHP_EOL;
    echo '    current:', $fArr->current(), PHP_EOL;
    $fArr->next();
}
// ============
// key:0
//     current:
// ============
// key:1
//     current:b
// ============
// key:2
//     current:
// ============
// key:3
//     current:
// ============
// key:4
//     current:f
// ============
// key:5
//     current:
// ============
// key:6
//     current:h

既然它是一種陣列物件,那麼迭代其實不用這麼麻煩的,我們直接通過 for 和 foreach 就可以了。它和其它的陣列結構一樣,都實現了 Iterator 和 Countable 這兩個介面,都是可以通過 for 和 foreach 來進行遍歷的。

foreach($fArr as $f){
    var_dump($f);
}
for($i=0;$i<count($fArr);$i++){
    var_dump($fArr[$i]);
}

物件資料對映

最後一種資料結構,物件資料對映。這是個什麼鬼?最簡單直接的理解其實就是把一個物件當成是 【鍵】,然後以這些鍵形成一個陣列結構。

$os = new SplObjectStorage();

$o1 = new stdClass;
$o2 = new stdClass;
$o3 = new stdClass;
$o4 = new stdClass;
$o5 = new stdClass;

$os->attach($o1);
$os->attach($o2);
$os->attach($o3);
$os->attach($o4, 'd4');
$os->attach($o5, 'd5');

var_dump($os);
// object(SplObjectStorage)#9 (1) {
//     ["storage":"SplObjectStorage":private]=>
//     array(5) {
//       ["00000000736a0aba000000002f97228d"]=>
//       array(2) {
//         ["obj"]=>
//         object(stdClass)#10 (0) {
//         }
//         ["inf"]=>
//         NULL
//       }
//       ["00000000736a0abb000000002f97228d"]=>
//       array(2) {
//         ["obj"]=>
//         object(stdClass)#11 (0) {
//         }
//         ["inf"]=>
//         NULL
//       }
//       ["00000000736a0abc000000002f97228d"]=>
//       array(2) {
//         ["obj"]=>
//         object(stdClass)#12 (0) {
//         }
//         ["inf"]=>
//         NULL
//       }
//       ["00000000736a0abd000000002f97228d"]=>
//       array(2) {
//         ["obj"]=>
//         object(stdClass)#13 (0) {
//         }
//         ["inf"]=>
//         string(2) "d4"
//       }
//       ["00000000736a0abe000000002f97228d"]=>
//       array(2) {
//         ["obj"]=>
//         object(stdClass)#14 (0) {
//         }
//         ["inf"]=>
//         string(2) "d5"
//       }
//     }
//   }

是不是有點意思,attach() 就可以向這個 SplObjectStorage 物件儲存對映類中新增資料。它的第二個引數可以指定一個資料內容,其實就可以看作是普通陣列中的 值 。

var_dump($os->count()); // int(5)
$os->detach($o2);
var_dump($os->count()); // int(4)

var_dump($os->contains($o2)); // bool(false)
var_dump($os->contains($o3)); // bool(true)

var_dump($os->getHash($o4)); // string(32) "000000003e67a2330000000040e598c9"

var_dump($os->offsetGet($o4)); // string(2) "d4"
$os->offsetSet($o4, 'new d4'); 
var_dump($os->offsetGet($o4)); // string(6) "new d4"
var_dump($os->offsetExists($o4)); // bool(true)
$os->offsetUnset($o4);
var_dump($os->offsetExists($o4)); // bool(false)

$os->rewind();
$os[$o1] = 'new d1';
while($os->valid()){
    echo '============', PHP_EOL;
    echo 'key:', $os->key(), PHP_EOL;
    if($os->getInfo() === NULL){
        $os->setInfo('new iter info');
    }
    echo '    info:', $os->getInfo(), PHP_EOL;
    echo '    current:', PHP_EOL;
    var_dump($os->current());
    
    $os->next();
}
// ============
// key:0
//     info:new d1
//     current:
// object(stdClass)#10 (0) {
// }
// ============
// key:1
//     info:new iter info
//     current:
// object(stdClass)#12 (0) {
// }
// ============
// key:2
//     info:d5
//     current:
// object(stdClass)#14 (0) {
// }

其它的遍歷查詢操作都是和其它資料結構的操作類似的,這裡就不多說了。其中比較特別的是 detach() 方法是刪除資料的,getHash() 則是獲取這個物件在儲存集合中的 Hash 值的,這個值也可以看做是這個物件在這個物件對映集合中的下標,我們其它的針對物件的操作判斷其實是都是在內部轉換成這個陣列下標來進行操作的。

總結

其實這一圈學習下來,突然發現有了 SPL 的這幾個資料結構之後,我們在 PHP 下面還真不太需要關心什麼資料結構方面的實現了,直接通用點就上個雙向連結串列就完了,簡單的就只是寫演算法了。好吧,學習還是要紮實點,資料結構和演算法真正要學習的其實是它內部的思想和邏輯。當然,既然已經提供了,那麼我們平常的業務開發中還是更建議直接使用 SPL 的這些資料結構來處理!

測試程式碼:

https://github.com/zhangyue0503/dev-blog/blob/master/php/2021/01/source/3.PHP的SPL擴充套件庫(一)資料結構.php

參考文件:

https://www.php.net/manual/zh/spl.datastructures.php

相關文章