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:1var_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