数组
数组(Array)是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
但是在 PHP 这种动态语言中,因为数组底层是通过散列表(后面我们会介绍这个数据结构)实现的,所以功能异常强大,这段常规的数组定义在 PHP 中并不成立,PHP 的数组可以存储任何类型数据,如果与 Java 对比的话,PHP 数组集成了 Java 的数组、List、Set、Map 于一身,所以写代码的效率比 Java 高了几个量级。
传统意义上的数组,查找算法复杂度是 O(1),非常高效,删除和插入都需要移动数组,所以算法复杂度是 O(n),但是,PHP不受此约束
链表
和数组不同,链表并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,如图所示:
单链表
链表有多种类型,最简单的是单链表,单链表是最原生的链表,其结构如图所示:
单链表中有两个节点比较特殊,分别是第一个结点和最后一个结点。我们通常把第一个结点叫作头结点
,把最后一个结点叫作尾结点
。
其中,头结点用来记录链表的基地址
,有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表上最后一个结点。
对单链表而言,理论
上来说,插入和删除节点的时间复杂度是 O(1),查询节点的时间复杂度是 O(n)。
循环链表
循环链表和单链表的区别是尾节点指向了头结点,从而首尾相连,有点像贪吃蛇,可用于解决「约瑟夫环」(猴子选大王)问题,循环链表的结构如图所示:
双向链表
与单链表的区别是双向链表除了有一个指向下一个节点的指针外,还有一个用于指向上一个节点的指针,从而实现通过 O(1) 复杂度找到上一个节点。
正是因为这个节点,使得双向链表在插入、删除节点时比单链表更高效,虽然我们前面已经提到单链表插入、删除时间复杂度已经是 O(1) 了,但是这没有考虑还只是针对插入、删除操作本身而言,以删除为例,删除某个节点后,需要将其前驱节点的指针指向被删除节点的下一个节点,这样,我们还需要获取其前驱节点,在单链表中获取前驱节点的时间复杂度是 O(n)。
所以综合来看单链表的删除、插入操作时间复杂度也是 O(n),而双向链表则不然,它有一个指针指向上一个节点,所以其插入和删除时间复杂度才是真正的 O(1)。
此外,对于有序链表而言,双向链表的查询效率显然也要高于单链表,不过更优的时间复杂度是靠更差的空间复杂度换取的,双向链表始终需要单链表的两倍空间,但是正如我们之前说的,在 Web 应用中,时间效率优先级更高,所以我们通常都是空间换时间来提高性能,Java 的 LinkedHashMap 底层就用到了双向链表。
双向循环链表
这个有啥用,哈哈哈,暂时我还不晓得,等我去谷歌下
PHP 代码模拟实现单链表
在 PHP 中,数组集成了所有数据结构,如数组、List、散列表、Map等,而且由于 PHP 不支持指针,所以不能实现真正的链表,比如在插入、删除的时候指针指向的变动无法实现,只能笼统通过 array_splice 函数实现元素插入和删除,也就无法区别实现单向链表、双向链表和循环链表。
不过 PHP 底层提供的 SPL 库中包含了一个双向链表的实现:http://php.net/manual/zh/class.spldoublylinkedlist.php
<?php
/**
* 通过 PHP 数组模拟实现单链表
*
* User: 舒孝元
* Date: 2020/8/28 13:57
* Mail: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
*/
namespace App\Http\Controllers\Algorithm;
class LinkedListController {
private $list = [];
// 获取链表指定位置的元素值,从0开始
public function get($index) {
$value = NULL;
while (current($this->list)) {
if (key($this->list) == $index) {
$value = current($this->list);
}
next($this->list);
}
reset($this->list);
return $value;
}
// 在链表指定位置插入值,默认插到链表头部
public function add($value, $index = 0) {
array_splice($this->list, $index, 0, $value);
}
// 从链表指定位置删除元素
public function remove($index) {
array_splice($this->list, $index, 1);
}
public function isEmpty() {
return !next($this->list);
}
public function size() {
return count($this->list);
}
}
$linkedList = new LinkedListController();
$linkedList->add(4);
$linkedList->add(5);
$linkedList->add(3);
print $linkedList->get(1); # 输出5
$linkedList->add(1, 1); # 在结点1的位置上插入1
print $linkedList->get(1); # 输出1
$linkedList->remove(1); # 移除结点1上的元素
print $linkedList->get(1); # 输出5
print $linkedList->size(); # 输出3
特殊线性表-栈
在 PHP 底层 SPL 库中也提供了堆栈的实现类 SplStack
堆栈在日常开发和软件使用中,应用非常广泛,比如我们的浏览器前进、倒退功能,编辑器/IDE 中的撤销、取消撤销功能,程序代码中的函数调用、递归、四则运算等等,都是基于堆栈这种数据结构来实现的,就连著名的 stackoverflow 网站也是取「栈溢出」,需要求教之意。
前面我们聊了两种基本的数据结构 —— 数组和链表,从逻辑角度来说,它们都是线性结构(就是排成一条线的结构,只有前后两个方向,非线性结构包括树、图等,后面会讲到),从存储角度来说,一个是顺序存储,一个是链式存储,各有利弊,数组需要预先申请连续内存,超出限制会溢出,但是对明确知道规模的小型数据集而言,使用数组会更加高效,随机访问的特性也更加方面数组读取,但插入和删除性能要差一些;链表的话没有空间限制,但是需要额外空间存储指针,插入、删除效率很高,但不支持随机访问。
栈又叫堆栈,是限定只能在一端进行插入和删除操作的线性表,并且满足后进先出(LIFO)的特点。我们把允许插入和删除的一端叫做栈顶,另一个端叫做栈底,不含任何数据的栈叫做空栈。
栈支持通过数组/链表实现,通过数组实现的通常叫做顺序栈,通过链表实现的叫做链栈。由于是在 PHP 中演示,我尽量偏向 PHP 语言,化繁就简,下面我们简单演示下如何在 PHP 中通过数组实现一个简单的顺序栈:
特殊线性表-队列
介绍完栈之后,接下来我们要介绍的是另一种跟栈很相似的数据结构 —— 队列,和栈一样,队列也是一中特殊的线性表结构,只不过队列是在一端插入,另一端删除,就跟我们平常排队一样的道理,从队尾入队,在队头出去,所以队列的特性是先入先出(FIFO),允许插入的一端叫队尾,允许删除的一端叫队头。
和栈一样,队列也可以通过数组和链表实现,通过数组实现的叫顺序队列,通过链表实现的叫做链式队列,栈只需要一个栈顶指针就可以了,因为只允许在栈顶插入删除,但是队列需要两个指针,一个指向队头,一个指向队尾。我们先来看通过 PHP 数组实现的顺序队列代码:
<?php
/**
* 通过 PHP 数组实现的队列
*
* User: 舒孝元
* Date: 2020/8/28 14:26
* Mail: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
*/
namespace App\Http\Controllers\Algorithm;
class SimpleQueueController {
private $_queue = [];
private $_size = 0;
public function __construct($size = 10) {
$this->_size = $size;
}
// 入队
public function enqueue($value) {
if (count($this->_queue) > $this->_size) {
return false;
}
array_push($this->_queue, $value);
}
// 出队
public function dequeue() {
if (count($this->_queue) == 0) {
return false;
}
return array_shift($this->_queue);
}
public function size() {
return count($this->_queue);
}
}
$queue = new SimpleQueueController(5);
$queue->enqueue(1);
$queue->enqueue(3);
$queue->enqueue(5);
var_dump($queue->dequeue()); # 1
var_dump($queue->size()); # 2
通过数组实现的顺序队列有一个问题,就是随着队列元素的插入和删除,队尾指针和队头指针不断后移,而导致队尾指针指向末尾无法插入数据,这时候有可能队列头部还是有剩余空间的,如下图所示:
我们当然可以通过数据搬移的方式把所有队列数据往前移,但这会增加额外的时间复杂度,如果频繁操作数据量很大的队列,显然对性能有严重损耗,对此问题的解决方案是循环队列,即把队列头尾连起来:
这样一来就不会出现之前的问题了,此时判断队列是否为空的条件还是 tail==head
,但是判断队列是否满的条件就变成了 (tail+1) % maxsize == head
,maxsize
是数组的长度,浪费一个空间是为了避免混淆判断空队列的条件。当然如果通过链表来实现队列的话,显然没有这类问题,因为链表没有空间限制。
队列的应用也非常广泛,比如我们常见的消息队列就是队列的典型应用场景。
递归
一种编程技巧和思想,自己调用自己
递归应用:遍历文件夹,斐波那契数列等
/**
* 通过递归实现斐波那契数列
*/
function fibonacci($n)
{
if ($n == 0) {
return 0;
}
if ($n == 1) {
return 1;
}
return fibonacci($n - 2) + fibonacci($n - 1);
}