一起學習PHP中的DS資料結構擴充套件(一)

硬核專案經理發表於2021-12-21

在之前學習 SPL 相關的文章中,我們已經學習過 SPL 中的一些資料結構相關的資料結構物件,非常強大也非常好用,最主要的是 SPL 已經整合在 PHP 原始碼中不需要我們再單獨地安裝別的什麼擴充套件。但是,今天我們要學習的這個 DataStruct 擴充套件庫的內容則更加地豐富,不過相對應的,這套擴充套件是需要我們自己手動再進行安裝的。如果大家對於資料結構的需求不高的話,使用 SPL 中的相關物件就夠用了,但是如果需要更加豐富的資料結構型別的話,這套 DS 擴充套件是更好的選擇。

DS 擴充套件的安裝和其它普通的擴充套件安裝沒有什麼區別,也不需要額外的作業系統上的元件支援,直接安裝即可。

首先還是從棧這個最基本的資料結構說起。DS 中的棧結構非常地簡單好用。

$stack = new \Ds\Stack();
var_dump($stack);
// object(Ds\Stack)#1 (0) {
// }

$stack = new \Ds\Stack([1, 2, 3]);
var_dump($stack);
// object(Ds\Stack)#2 (3) {
//     [0]=>
//     int(3)
//     [1]=>
//     int(2)
//     [2]=>
//     int(1)
//   }

兩種方式例項化棧物件,其實就是引數的不同,如果我們直接給建構函式傳遞一個陣列的話,那麼這個陣列就會做為棧內部的元素供我們使用。

$stack->push(4);
$stack->push(5);
var_dump($stack);
// object(Ds\Stack)#2 (5) {
//     [0]=>
//     int(5)
//     [1]=>
//     int(4)
//     [2]=>
//     int(3)
//     [3]=>
//     int(2)
//     [4]=>
//     int(1)
//   }

var_dump($stack->pop()); // int(5)
var_dump($stack->pop()); // int(4)
var_dump($stack);
// object(Ds\Stack)#2 (3) {
//     [0]=>
//     int(3)
//     [1]=>
//     int(2)
//     [2]=>
//     int(1)
//   }

push() 就是將資料壓棧,pop() 則是將棧頂的元素彈出。關於棧的最主要的操作其實就是這兩個方法函式了。

var_dump($stack->peek()); // int(3)
// object(Ds\Stack)#2 (3) {
//     [0]=>
//     int(3)
//     [1]=>
//     int(2)
//     [2]=>
//     int(1)
//   }

peek() 這個函式是直接獲取棧頂的資料,但是需要注意的是,它不會彈出棧頂的元素。也就是說,這個 peek() 方法只會取得資料的內容,不會改變棧內部的資料。

var_dump($stack->count()); // int(3)
var_dump($stack->isEmpty()); // bool(false)
var_dump($stack->toArray());
// array(3) {
//     [0]=>
//     int(3)
//     [1]=>
//     int(2)
//     [2]=>
//     int(1)
//   }

$stack->clear();
var_dump($stack);
// object(Ds\Stack)#2 (0) {
// }

count() 返回棧內部元素的數量,isEmpty() 用於判斷棧是否為空,toArray() 直接以陣列的格式返回棧內部的資料,clear() 方法用於清空棧。這些方法函式都非常地簡單,所以就不多做解釋了。最後我們來看看棧物件的賦值拷貝操作。

$a = $stack;
$a->push(4);
var_dump($stack);
// object(Ds\Stack)#2 (1) {
//     [0]=>
//     int(4)
//   }

$b = $stack->copy();
$b->push(5);

var_dump($stack);
// object(Ds\Stack)#2 (1) {
//     [0]=>
//     int(4)
//   }

var_dump($b);
// object(Ds\Stack)#1 (2) {
//     [0]=>
//     int(5)
//     [1]=>
//     int(4)
//   }

\$stack 物件是例項化之後的物件,在普通的賦值操作中是引用傳遞的。上文中我們清空了 $stack ,然後在這裡我們讓 $a 等於這個 $stack ,然後操作 $a 相應地 $stack 裡面的內容也發生了變化。對於引用傳遞這個問題,我們一般會使用 \_\_clone() 魔術方法來解決, Stack 類直接就為我們提供了一個 copy() 方法,直接可以獲得一個棧物件的拷貝,也可以說是一個新的棧物件。就像上面程式碼中的 $b 一樣,當使用 copy() 方法賦值給 $b 之後,它就成為了一個新的棧物件,任何 $b 的操作和 $stack 物件就沒有什麼關係了。我們可以看到 物件id 也完全不同了。

佇列

對於佇列來說,整體上的功能方法和棧的內容差不多,它們實現的方法基本上是一模一樣的。具體在實現層面上的不同就體現在彈棧和出隊的不同,也就是 push() 方法在實現中有差別。

$queue = new \Ds\Queue();
var_dump($queue);
// object(Ds\Queue)#3 (0) {
// }

$queue = new \Ds\Queue([1, 2, 3]);
var_dump($queue);
// object(Ds\Queue)#4 (3) {
//     [0]=>
//     int(1)
//     [1]=>
//     int(2)
//     [2]=>
//     int(3)
//   }

$queue->push(4);
$queue->push(5);
var_dump($queue);
// object(Ds\Queue)#4 (5) {
//     [0]=>
//     int(1)
//     [1]=>
//     int(2)
//     [2]=>
//     int(3)
//     [3]=>
//     int(4)
//     [4]=>
//     int(5)
//   }

var_dump($queue->pop()); // int(1)
var_dump($queue->pop()); // int(2)
var_dump($queue);
// object(Ds\Queue)#4 (3) {
//     [0]=>
//     int(3)
//     [1]=>
//     int(4)
//     [2]=>
//     int(5)
//   }

可以看出,在佇列中,我們 push() 進來的資料的順序是 1,2,3,4,5 這樣正序的,也就是將資料放到內部這個陣列的底部,出隊 pop() 直接拿最頂上的資料也就實現了先進先出的效果。對比上面棧的資料內容,就可以發現棧的資料在 push() 的時候就是反過來的,5、4、3、2、1 這樣的,然後在 pop() 的時候其實也是從頂部拿出資料,只不過棧是將資料 push() 到內部陣列的頂部的,然後從頂部直接拿出資料實現了 後進先出 的效果。

優先佇列

最重要的兩個資料結構說完了,我們再來看一個佇列的擴充套件結構,也就是優先佇列的實現。其實這個佇列就是在 push() 資料的時候多了一個引數,也就是資料的優先順序,越大的越靠前,其它的方法和普通佇列以及棧的方法都沒什麼太大的區別。

$pQueue = new \Ds\PriorityQueue();

$pQueue->push(1, 100);
$pQueue->push(2, 101);
$pQueue->push(3, 99);

var_dump($pQueue);
// object(Ds\PriorityQueue)#3 (3) {
//     [0]=>
//     int(2)
//     [1]=>
//     int(1)
//     [2]=>
//     int(3)
//   }

var_dump($pQueue->pop()); // int(2)
var_dump($pQueue->pop()); // int(1)
var_dump($pQueue->pop()); // int(3)

Map

最後我們來學習一個 Map 資料結構,其實也就是 HaspMap 這種 K/V 的鍵值對形式的資料結構。只能說 PHP 中的陣列實在是太強大了,完全相容了這種資料結構,所以使得單獨的 Map 結構並沒有什麼實際的意義。

$map = new \Ds\Map(['a'=>1, 2, 5=>3]);
var_dump($map);
// object(Ds\Map)#5 (3) {
//     [0]=>
//     object(Ds\Pair)#6 (2) {
//       ["key"]=>
//       string(1) "a"
//       ["value"]=>
//       int(1)
//     }
//     [1]=>
//     object(Ds\Pair)#7 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//     [2]=>
//     object(Ds\Pair)#8 (2) {
//       ["key"]=>
//       int(5)
//       ["value"]=>
//       int(3)
//     }
//   }


var_dump($map->get(0)); // int(2)
var_dump($map->get(5)); // int(3)

$map->put('b', '4');
$map->put('c', [1, 2, 3]);
$map->put('d', new class{public $t = 't';});

var_dump($map);
// object(Ds\Map)#5 (6) {
//     [0]=>
//     object(Ds\Pair)#7 (2) {
//       ["key"]=>
//       string(1) "a"
//       ["value"]=>
//       int(1)
//     }
//     [1]=>
//     object(Ds\Pair)#6 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//     [2]=>
//     object(Ds\Pair)#9 (2) {
//       ["key"]=>
//       int(5)
//       ["value"]=>
//       int(3)
//     }
//     [3]=>
//     object(Ds\Pair)#10 (2) {
//       ["key"]=>
//       string(1) "b"
//       ["value"]=>
//       string(1) "4"
//     }
//     [4]=>
//     object(Ds\Pair)#11 (2) {
//       ["key"]=>
//       string(1) "c"
//       ["value"]=>
//       array(3) {
//         [0]=>
//         int(1)
//         [1]=>
//         int(2)
//         [2]=>
//         int(3)
//       }
//     }
//     [5]=>
//     object(Ds\Pair)#12 (2) {
//       ["key"]=>
//       string(1) "d"
//       ["value"]=>
//       object(class@anonymous)#8 (1) {
//         ["t"]=>
//         string(1) "t"
//       }
//     }
//   }

$map->remove('d');
$map->remove('c');

var_dump($map);
// object(Ds\Map)#5 (4) {
//     [0]=>
//     object(Ds\Pair)#8 (2) {
//       ["key"]=>
//       string(1) "a"
//       ["value"]=>
//       int(1)
//     }
//     [1]=>
//     object(Ds\Pair)#12 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//     [2]=>
//     object(Ds\Pair)#11 (2) {
//       ["key"]=>
//       int(5)
//       ["value"]=>
//       int(3)
//     }
//     [3]=>
//     object(Ds\Pair)#10 (2) {
//       ["key"]=>
//       string(1) "b"
//       ["value"]=>
//       string(1) "4"
//     }
//   }

在 Java 之類的語言中,陣列 和 HashMap 是兩種東西,或者說是兩種集合物件,比如 List\<Obj\> 就是一個資料集合,而 Map\<Obj\> 就是一個 HashMap 集合。相對應的,在 Java 的這種泛型集合中,我們需要新增和獲取資料就要像這個 DS 擴充套件中的 Map 一樣使用 get()、put()、remove() 類似的方法來實現。

Map 這個資料結構與上面的棧、佇列之類的資料結構中實現的方法差別還是挺大的。

var_dump($map->first());
// object(Ds\Pair)#8 (2) {
//     ["key"]=>
//     string(1) "a"
//     ["value"]=>
//     int(1)
//   }

var_dump($map->last());
// object(Ds\Pair)#8 (2) {
//     ["key"]=>
//     int(5)
//     ["value"]=>
//     int(3)
//   }

var_dump($map->sum()); // int(10)

var_dump($map->hasKey('b')); // bool(true)
var_dump($map->haskey('bb')); // bool(false)

var_dump($map->hasValue('4')); // bool(true)
var_dump($map->hasValue(4)); // bool(false)

它有 first() 和 last() 方法直接獲取第一個和最後一個資料元素。也有 sum() 方法獲得資料元素的個數,同時可以通過 hasKey() 和 hasValue() 來判斷指定的鍵或者值是存在。是不是有點像 key_exists() 和 in_array() 這兩個方法。當然,相對應的我們也可以直接獲取這些 Key 和 Value 的內容。

var_dump($map->keys());
// object(Ds\Set)#10 (4) {
//     [0]=>
//     string(1) "a"
//     [1]=>
//     int(0)
//     [2]=>
//     int(5)
//     [3]=>
//     string(1) "b"
//   }

var_dump($map->values());
// object(Ds\Vector)#10 (4) {
//     [0]=>
//     int(1)
//     [1]=>
//     int(2)
//     [2]=>
//     int(3)
//     [3]=>
//     string(1) "4"
//   }

我們可以看到,keys() 返回的內容是 Set 型別的物件,而 values() 返回的內容是 Vector 型別的物件,這兩種也是 DS 中的資料結構型別,我們將在下篇文章中再學習。除了 Key 和 Values 之外,它還可以直接返回一個 Vector 型別的物件集合結構,使用 paris() 方法。

var_dump($map->pairs());
// object(Ds\Vector)#9 (4) {
//     [0]=>
//     object(Ds\Pair)#10 (2) {
//       ["key"]=>
//       string(1) "a"
//       ["value"]=>
//       int(1)
//     }
//     [1]=>
//     object(Ds\Pair)#11 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//     [2]=>
//     object(Ds\Pair)#12 (2) {
//       ["key"]=>
//       int(5)
//       ["value"]=>
//       int(3)
//     }
//     [3]=>
//     object(Ds\Pair)#8 (2) {
//       ["key"]=>
//       string(1) "b"
//       ["value"]=>
//       string(1) "4"
//     }
//   }

在 Map 物件中,還提供了一些為資料排序、合併兩個 Map 以及擷取一部分資料的方法,直接貼出程式碼吧。

$map->ksort();
var_dump($map);
// object(Ds\Map)#5 (4) {
//     [0]=>
//     object(Ds\Pair)#9 (2) {
//       ["key"]=>
//       string(1) "a"
//       ["value"]=>
//       int(1)
//     }
//     [1]=>
//     object(Ds\Pair)#8 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//     [2]=>
//     object(Ds\Pair)#12 (2) {
//       ["key"]=>
//       string(1) "b"
//       ["value"]=>
//       string(1) "4"
//     }
//     [3]=>
//     object(Ds\Pair)#11 (2) {
//       ["key"]=>
//       int(5)
//       ["value"]=>
//       int(3)
//     }
//   }

$map->reverse();
var_dump($map);
// object(Ds\Map)#5 (4) {
//     [0]=>
//     object(Ds\Pair)#11 (2) {
//       ["key"]=>
//       int(5)
//       ["value"]=>
//       int(3)
//     }
//     [1]=>
//     object(Ds\Pair)#12 (2) {
//       ["key"]=>
//       string(1) "b"
//       ["value"]=>
//       string(1) "4"
//     }
//     [2]=>
//     object(Ds\Pair)#8 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//     [3]=>
//     object(Ds\Pair)#9 (2) {
//       ["key"]=>
//       string(1) "a"
//       ["value"]=>
//       int(1)
//     }
//   }

$newMap = new \Ds\Map();
$newMap->put('a', 'a');
$newMap->put('b', 'b');
$newMap->put('e', 'e');

var_dump($map->diff($newMap));
// object(Ds\Map)#8 (2) {
//     [0]=>
//     object(Ds\Pair)#12 (2) {
//       ["key"]=>
//       int(5)
//       ["value"]=>
//       int(3)
//     }
//     [1]=>
//     object(Ds\Pair)#11 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//   }

var_dump($map->union($newMap));
// object(Ds\Map)#8 (5) {
//     [0]=>
//     object(Ds\Pair)#11 (2) {
//       ["key"]=>
//       int(5)
//       ["value"]=>
//       int(3)
//     }
//     [1]=>
//     object(Ds\Pair)#12 (2) {
//       ["key"]=>
//       string(1) "b"
//       ["value"]=>
//       string(1) "b"
//     }
//     [2]=>
//     object(Ds\Pair)#10 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//     [3]=>
//     object(Ds\Pair)#6 (2) {
//       ["key"]=>
//       string(1) "a"
//       ["value"]=>
//       string(1) "a"
//     }
//     [4]=>
//     object(Ds\Pair)#7 (2) {
//       ["key"]=>
//       string(1) "e"
//       ["value"]=>
//       string(1) "e"
//     }
//   }

var_dump($map->xor($newMap));
// object(Ds\Map)#8 (3) {
//     [0]=>
//     object(Ds\Pair)#7 (2) {
//       ["key"]=>
//       int(5)
//       ["value"]=>
//       int(3)
//     }
//     [1]=>
//     object(Ds\Pair)#6 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//     [2]=>
//     object(Ds\Pair)#10 (2) {
//       ["key"]=>
//       string(1) "e"
//       ["value"]=>
//       string(1) "e"
//     }
//   }

var_dump($map->intersect($newMap));
// object(Ds\Map)#8 (2) {
//     [0]=>
//     object(Ds\Pair)#10 (2) {
//       ["key"]=>
//       string(1) "b"
//       ["value"]=>
//       string(1) "4"
//     }
//     [1]=>
//     object(Ds\Pair)#6 (2) {
//       ["key"]=>
//       string(1) "a"
//       ["value"]=>
//       int(1)
//     }
//   }

$map->putAll($newMap);
var_dump($map);
// object(Ds\Map)#5 (5) {
//     [0]=>
//     object(Ds\Pair)#8 (2) {
//       ["key"]=>
//       int(5)
//       ["value"]=>
//       int(3)
//     }
//     [1]=>
//     object(Ds\Pair)#6 (2) {
//       ["key"]=>
//       string(1) "b"
//       ["value"]=>
//       string(1) "b"
//     }
//     [2]=>
//     object(Ds\Pair)#10 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//     [3]=>
//     object(Ds\Pair)#7 (2) {
//       ["key"]=>
//       string(1) "a"
//       ["value"]=>
//       string(1) "a"
//     }
//     [4]=>
//     object(Ds\Pair)#12 (2) {
//       ["key"]=>
//       string(1) "e"
//       ["value"]=>
//       string(1) "e"
//     }
//   }

var_dump($map->slice(1, 2));
// object(Ds\Map)#12 (2) {
//     [0]=>
//     object(Ds\Pair)#7 (2) {
//       ["key"]=>
//       string(1) "b"
//       ["value"]=>
//       string(1) "b"
//     }
//     [1]=>
//     object(Ds\Pair)#10 (2) {
//       ["key"]=>
//       int(0)
//       ["value"]=>
//       int(2)
//     }
//   }

var_dump($map->skip(2));
// object(Ds\Pair)#12 (2) {
//     ["key"]=>
//     int(0)
//     ["value"]=>
//     int(2)
//   }

程式碼內容很多,展示的註釋內容也就是我們執行的結果。可以看出,Map 這個資料結構提供的方法功能真的是非常豐富的。而且我們這裡還沒有完全展示出來,它還有一些類似的方法,大家有興趣的可以自己多多地去探索。不過就像上面說過的,PHP 中的陣列實在是太方便了,所以這個 Map 的應用場景有限,或者某些特殊的必須需要物件來表示陣列結構的場景會有用。

總結

是不是有點意思呀,就像在開頭時我們說過的,瞭解學習可以,但如果真實業務中只是需要一些簡單的棧或佇列的實現的話,直接使用 SPL 擴充套件庫中的資料結構就可以了。當然,DS 中的內容還沒有講完,Vector 和 Set 相信學習過 Java 的同學一定不陌生,下篇文章我們將繼續學習 DS 中剩餘的資料結構。

測試程式碼:

https://github.com/zhangyue0503/dev-blog/blob/master/php/2021/02/source/2.一起學習PHP中的DS資料結構擴充套件(一).php

參考文件:

https://www.php.net/manual/zh/book.ds.php

相關文章