PHP7-数据结构和算法(一)

116 阅读47分钟

PHP7 数据结构和算法(一)

原文:zh.annas-archive.org/md5/eb90534f20ff388513beb1e54fb823ef

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

数据结构和算法是软件应用开发的一个重要组成部分。无论是构建基于 Web 的应用程序、CMS 还是使用 PHP 构建独立的后端系统,我们都需要一直应用算法和数据结构。有时,我们在不知不觉中这样做,有时则没有给予适当的关注。大多数开发人员认为这两个主题非常困难,而且没有必要关注细节,因为 PHP 对数据结构和算法有很多内置支持。在本书中,我们将专注于 PHP 数据结构和算法的基础知识和实际示例,以便了解数据结构是什么,为什么选择它们,以及在哪里应用哪种算法。本书旨在面向初学者和有经验的 PHP 程序员。本书从基础主题开始,逐渐深入更高级的主题。我们在本书中尽量提供了许多示例、图片和解释,以便您可以以视觉形式和实际示例正确理解概念。

本书内容

第一章,数据结构和算法简介,着重介绍不同的数据结构,它们的定义、属性和示例。本章还包括我们分析算法并找到它们的复杂性的方法,特别强调大 O(O)符号。

第二章,理解 PHP 数组,着重介绍了 PHP 中一个非常基本和内置的数据结构--PHP 数组。还介绍了通过 PHP 数组可以实现什么以及它们的优缺点。我们着重介绍了如何使用数组来实现其他数据结构。

第三章,使用链表,涵盖了不同类型的链表。它着重于不同变体链表的分类及其构造过程,并提供示例。

第四章,构建栈和队列,着重介绍了本章中最重要的两种数据结构--栈和队列。我们看到如何使用不同的方法构建栈和队列,并通过示例讨论它们的操作和用法。

第五章,应用递归算法-递归,着重介绍了算法中的一个重要主题--递归。我们介绍了使用递归算法解决问题的不同方法,以及使用这种技术的优缺点。我们还介绍了一些基本的日常编程问题,可以使用递归来解决。

第六章,理解和实现树,讨论了一种非层次化的数据结构--树。我们介绍了树的属性以及如何构建它们,并了解树数据结构对我们重要的情况。

第七章,使用排序算法,演示了如何实现不同的排序算法及其复杂性,因为排序在编程世界中是一个非常重要的主题,寻找高效的排序算法一直是一个问题。在本章末尾,我们还介绍了内置的 PHP 排序算法。

第八章,探索搜索选项,阐述了搜索在编程世界中的重要性。在本章中,我们着重介绍了不同的搜索技术以及何时使用哪种算法。我们还讨论了是否应该在搜索之前进行排序。本章包含了许多不同算法的示例和实现。

第九章,将图形应用,解释了图形算法是编程范式中最广泛使用的算法之一。在本章中,我们着重介绍了不同与图形相关的问题,并使用不同的算法来解决它们。我们通过示例和解释介绍了最短路径算法和最小生成树的实现。

第十章,理解和使用堆,讨论了本书中的最后一个数据结构主题——堆。它是一种非常高效的数据结构,在现实世界中有许多实现。我们展示了如何构建堆及其用途,包括堆排序算法的实现。

第十一章,高级技术解决问题,侧重于不同的技术来解决问题。我们讨论的重点是记忆化、动态规划、贪婪算法和回溯等主题,以及实际问题的示例和解决方案。

第十二章,PHP 对数据结构和算法的内置支持,展示了我们对数据结构和算法的内置支持。我们讨论了 PHP 的函数、PECL 库,以及一些在线资源的参考资料。

第十三章,使用 PHP 进行函数式数据结构,为我们介绍了使用 PHP 进行函数式编程和函数式数据结构的一些信息,因为函数式编程如今正引起很多关注。我们介绍了一个名为 Tarsana 的函数式编程库,并展示了不同的使用示例。

您需要为本书准备什么

您所需要的只是在您的机器上安装最新的 PHP 版本(最低要求是 PHP 7.x)。您可以从命令行运行示例,这不需要一个 Web 服务器。但是,如果您愿意,您可以安装 Apache 或 Nginx,或者以下内容:

  • PHP 7.x+

  • Nginx/apache(可选)

  • PHP IDE 或代码编辑器

这本书适合谁

这本书适用于那些希望通过 PHP 学习数据结构和算法,以更好地控制应用程序解决方案、提高效率和优化的人。需要对 PHP 数据类型、控制结构和其他基本特性有基本的了解。

约定

在本书中,您会发现一些区分不同信息类型的文本样式。以下是一些这些样式的示例及其含义的解释。

代码以与书本文本字体不同的字体编写,以突出显示代码块。

代码块设置如下:

[default]

class TreeNode {

    public $data = NULL;

    public $children = [];

    public function __construct(string $data = NULL) {

         $this->data = $data;

    }

    public function addChildren(TreeNode $node) {

         $this->children[] = $node;

    }

}

当我们希望引起您对代码块中特定部分的注意时,在解释过程中,代码会在文本中以如下方式突出显示:**addChildren**

任何命令行输入或输出都会以如下方式书写:

Final 

-Semi Final 1 

--Quarter Final 1 

--Quarter Final 2 

-Semi Final 2 

--Quarter Final 3 

--Quarter Final 4

新术语和重要单词以粗体显示。例如,屏幕上看到的菜单或对话框中的单词会在文本中出现,如:“单击“下一步”按钮将您移至下一个屏幕。”

警告或重要提示会以如下方式出现在一个框中。提示和技巧会以这种方式出现。

第一章:数据结构和算法简介

我们生活在一个数字时代。在我们生活和日常需求的每个领域,我们都有重要的技术应用。没有技术,世界将几乎停滞不前。你是否曾经尝试过准备简单的天气预报需要什么?大量的数据被分析以准备简单的信息,这些信息实时传递给我们。计算机是技术革命中最重要的发现,它们在过去几十年里彻底改变了世界。计算机处理这些大量的数据,并在每个依赖技术的任务和需求中帮助我们。为了使计算机操作高效,我们以不同的格式或不同的结构来表示数据,这就是所谓的数据结构。

数据结构是计算机和编程语言的非常重要的组成部分。除了数据结构之外,了解如何使用这些数据结构解决问题或找到解决方案也非常重要。从我们简单的手机联系人名单到复杂的 DNA 个人资料匹配系统,数据结构和算法的使用无处不在。

我们是否曾想过在超市排队结账可以代表数据结构?或者从一堆文件中取出一张账单可以是数据结构的另一种用途?事实上,我们几乎在生活中的每个地方都在遵循数据结构的概念。无论是管理支付账单的队列还是乘坐交通工具,或者为日常工作维护一堆书或文件的堆栈,数据结构无处不在,影响着我们的生活。

PHP 是一种非常流行的脚本语言,数十亿的网站和应用程序都是使用它构建的。人们使用超文本预处理器PHP)来开发简单的应用程序到非常复杂的应用程序,有些应用程序非常数据密集。一个重要的问题是-我们是否应该为任何数据密集型应用程序或算法解决方案使用 PHP?当然应该。随着 PHP 7 的新版本发布,PHP 已经进入了高效和健壮应用程序开发的新可能性。我们的任务将是展示并准备自己了解使用 PHP 7 的数据结构和算法的力量,以便我们可以在我们的应用程序和程序中利用它。

数据结构和算法的重要性

如果我们考虑现实生活中与计算机的情况,我们也会使用不同的方式来安排我们的物品和数据,以便在需要时能够高效地使用它们或轻松找到它们。如果我们以随机顺序输入我们的电话联系人名单会怎么样?我们能轻松找到联系人吗?由于联系人没有按特定顺序排列,我们可能最终会在通讯录中搜索每个联系人。只需考虑以下两幅图像:

其中一幅图表明书籍是零散的,找到特定的书籍将需要时间,因为书籍没有组织。另一幅图表明书籍是按照堆栈组织的。第二幅图不仅显示我们聪明地利用了空间,而且书籍的搜索变得更容易了。

让我们考虑另一个例子。我们要为一场重要的足球比赛买票。成千上万的人在等待售票亭开放。票将按先到先得的原则分发。如果我们考虑以下两幅图像,哪一种是处理如此庞大人群的最佳方式?:

左边的图片清楚地显示了没有适当的顺序,也没有办法知道谁先来拿票。但是如果我们知道人们是按照有序的方式排队等候,那么处理人群将更容易,我们将把票交给先来的人。这是一个被称为队列的常见现象,在编程世界中被广泛使用。编程术语并不是从外部世界产生的。事实上,大多数数据结构都受到现实生活的启发,它们大多数时候使用相同的术语。无论我们是在准备任务清单、联系人清单、书堆、饮食图表、家谱,还是组织层次结构,我们基本上都在使用计算世界中被称为数据结构的不同排列技术。

到目前为止,我们已经谈到了一些数据结构,但是算法呢?我们日常生活中难道不使用任何算法吗?当然我们会。每当我们从旧电话簿中搜索联系人时,肯定不是从头开始搜索。如果我们要搜索TOM,我们不会搜索写有ABC的页面。我们直接去T页面,然后查找TOM是否在那里列出。或者,如果我们需要从电话簿中找到一位医生,我们肯定不会在食品部分搜索。如果我们把电话簿或电话目录视为数据结构,那么我们搜索特定信息的方式就被称为算法。数据结构帮助我们高效地使用数据,而算法帮助我们高效地对这些数据执行不同的操作。

例如,如果我们的电话目录中有 10 万条记录,从头开始搜索特定条目可能需要很长时间。但是,如果我们知道医生的名字在第 200 页到第 220 页,我们可以只搜索这些页面来节省时间,而不是搜索整个目录。

我们也可以考虑另一种寻找医生的方法。前面的段落是搜索目录中的特定部分,我们甚至可以按字母顺序在目录中搜索,就像我们在字典中搜索单词一样。这可能会减少我们搜索的时间和条目。寻找问题的解决方案可能有许多不同的方法,每种方法都可以称为算法。从前面的讨论中,我们可以说对于特定的问题或任务,可能有多种方法或算法可供执行。那么我们应该考虑使用哪一种?我们很快就会讨论这个问题。在讨论这一点之前,我们将专注于PHP 数据类型抽象数据类型ADT)。为了理解数据结构的概念,我们必须对 PHP 数据类型和 ADT 有深入的理解。

理解抽象数据类型(ADT)

PHP 有八种原始数据类型,分别是布尔值、整数、浮点数、字符串、数组、对象、资源和空值。此外,我们必须记住 PHP 是一种弱类型语言,我们在创建这些数据时不需要关心数据类型声明。虽然 PHP 具有一些静态类型特性,但 PHP 主要是一种动态类型语言,这意味着在使用变量之前不需要声明。我们可以为新变量赋值并立即使用它。

到目前为止我们讨论的数据结构的例子,我们可以使用原始数据类型中的任何一个来表示这些结构吗?也许可以,也许不行。我们的原始数据类型有一个特定的目标:存储数据。为了在这些数据上执行一些灵活的操作,我们需要以一种特定的方式使用数据类型,以便我们可以将它们用作特定的模型并执行一些操作。通过概念模型处理数据的这种特定方式被称为抽象数据类型,或 ADT。ADT 还定义了数据的一组可能操作。

我们需要理解 ADT 主要是理论概念,用于算法、数据结构和软件设计的设计和分析。相比之下,数据结构是具体的表示。为了实现 ADT,我们可能需要使用数据类型或数据结构,或者两者兼而有之。ADT 的最常见例子是栈和队列。

考虑栈作为 ADT,它不仅是数据的集合,还有两个重要的操作称为推入和弹出。通常,我们将一个新条目放在栈的顶部,这被称为push,当我们想要取一个项目时,我们从顶部取出,这也被称为pop。如果我们将 PHP 数组视为栈,我们将需要额外的功能来实现这些推入和弹出操作,以将其视为栈 ADT。同样,队列也是一个 ADT,有两个必需的操作:在队列的末尾添加一个项目,也称为enqueue,从队列的开头移除一个项目,也称为dequeue。两者听起来很相似,但如果我们仔细观察,我们会发现栈作为后进先出LIFO)模型,而队列作为先进先出FIFO)模型。这两种不同的数学模型使它们成为两种不同的 ADT。

以下是一些常见的 ADT:

  • 列表

  • 映射

  • 集合

  • 队列

  • 优先队列

在接下来的章节中,我们将探索更多的 ADT,并使用 PHP 将它们实现为数据结构。

不同的数据结构

我们可以将数据结构分类为两种不同的组:

  • 线性数据结构

  • 非线性数据结构

在线性数据结构中,项目以线性或顺序方式结构化。数组、列表、栈和队列是线性结构的例子。在非线性结构中,数据不是以顺序方式结构化的。图和树是非线性数据结构的最常见例子。

现在让我们以一种总结的方式探索数据结构的世界,包括不同类型的数据结构及其目的。稍后,我们将详细探索每种数据结构。

在编程世界中存在许多不同类型的数据结构。其中,以下是最常用的:

  • 结构

  • 数组

  • 链表

  • 双向链表

  • 队列

  • 优先队列

  • 集合

  • 映射

结构

通常,一个变量只能存储一个数据类型,一个标量数据类型只能存储一个值。有许多情况下,我们可能需要将一些数据类型组合在一起作为一个复杂的数据类型。例如,我们想要将一些学生信息一起存储在一个学生数据类型中。我们需要学生的姓名、地址、电话号码、电子邮件、出生日期、当前班级等信息。为了将每个学生记录存储到一个独特的学生数据类型中,我们将需要一个特殊的结构来允许我们这样做。这可以很容易地通过struct实现。换句话说,结构是一个值的容器,通常使用名称访问。虽然结构在 C 编程语言中非常流行,但我们也可以在 PHP 中使用类似的概念。我们将在接下来的章节中探索这一点。

数组

虽然数组被认为是 PHP 中的一种数据类型,但数组实际上是一种在所有编程平台上广泛使用的数据结构。在 PHP 中,数组实际上是一个有序映射(我们将在几节后了解映射)。我们可以将多个值存储在单个数组中作为单个变量。矩阵类型的数据在数组中易于存储,因此它在所有编程平台上被广泛使用。通常数组是一个固定大小的集合,通过顺序数字索引访问。在 PHP 中,数组的实现方式不同,您可以定义动态数组而不定义数组的固定大小。我们将在下一章中更多地探讨 PHP 数组。数组可以有不同的维度。如果一个数组只有一个索引来访问元素,我们称之为单维数组。但如果需要两个或更多索引来访问元素,我们分别称之为二维或多维数组。以下是两个数组数据结构的图示:

链表

链表是一种线性数据结构,是数据元素的集合,也称为节点,并且可以具有不同的大小。通常,列出的项目通过一个指针连接,称为链接,因此它被称为链表。在链表中,一个列表元素通过指针链接到下一个元素。从下图中,我们可以看到链表实际上维护了一个有序集合。链表是编程语言中使用的最常见和最简单的数据结构形式。在单链表中,我们只能向前移动。在第三章中,使用链表,我们将深入探讨链表的概念和实现:

双向链表

双向链表是一种特殊类型的链表,我们不仅存储下一个节点是什么,还在节点结构中存储了前一个节点。因此,它可以在列表内前后移动。它比单链表或链表具有更多的灵活性,因为它同时具有前一个和后一个指针。我们将在第三章中更多地探讨这些内容,使用链表。以下图示了双向链表:

堆栈

正如我们在前面的页面中讨论过的堆栈,我们已经知道堆栈是具有 LIFO 原则的线性数据结构。因此,堆栈只有一个端口用于添加新项目或移除项目。它是计算机技术中最古老和最常用的数据结构之一。我们总是使用名为top的单点从堆栈中添加或移除项目。术语 push 用于指示要添加到堆栈顶部的项目,pop 用于从顶部移除项目;这在下图中显示。我们将在第四章中更多地讨论堆栈和队列的内容。

队列

队列是另一种遵循 FIFO 原则的线性数据结构。队列允许对集合进行两种基本操作。第一种是enqueue,它允许我们将项目添加到队列的后面。第二种是dequeue,它允许我们从队列的前面移除项目。队列是计算机技术中最常用的数据结构之一。我们将在第四章中学习有关队列的详细信息,构建堆栈和队列

集合

集合是一种抽象数据类型,用于存储特定的值。这些值不以任何特定顺序存储,但集合中不应该有重复的值。集合不像集合那样用于检索特定值;集合用于检查其中是否存在某个值。有时,集合数据结构可以被排序,我们称之为有序集。

映射

地图是一个键值对的集合,其中所有键都是唯一的。我们可以将地图视为一个关联数组,其中所有键都是唯一的。我们可以使用键和值对添加和删除,以及使用键更新和查找地图。事实上,PHP 数组是有序地图实现。我们将在下一章中探讨这一点。

树是计算世界中最广泛使用的非线性数据结构。它在分层数据结构中被广泛使用。树由节点组成,有一个特殊的节点被称为树的,它开始了树结构。其他节点从根节点下降。树数据结构是递归的,这意味着树可以包含许多子树。节点通过边连接在一起。我们将在第六章中讨论不同类型的树,它们的操作和目的,理解和实现树

图数据结构是一种特殊类型的非线性数据结构,由有限数量的顶点或节点和边或弧组成。图可以是有向的也可以是无向的。有向图清楚地指示边的方向,而无向图提到边,而不是方向。因此,在无向图中,边的两个方向被视为单个边。换句话说,我们可以说图是一对集合(V,E),其中V是顶点的集合,E是边的集合:

V = {A, B, C, D, E, F}

E = {AB, BC, CE, ED, EF, DB}

在有向图中,边AB与边BA不同,而在无向图中,ABBA都是相同的。图在编程世界中解决许多复杂问题。我们将在第九章中继续讨论图数据结构,将图形投入实践。在下图中,我们有:

堆是一种特殊的基于树的数据结构,满足堆属性。最大键是根,较小的键是叶子,这被称为最大堆。或者,最小键是根,较大的键是叶子,这被称为最小堆。尽管堆结构的根是树的最大或最小键,但它不一定是一个排序结构。堆用于以高效方式解决图算法以及排序。我们将在第十章中探讨堆数据结构,理解和使用堆

解决问题 - 算法方法

到目前为止,我们已经讨论了不同类型的数据结构及其用途。但是,我们必须记住的一件事是,仅仅将数据放入适当的结构中可能并不能解决我们的问题。我们需要利用数据结构来解决问题,换句话说,我们将使用数据结构来解决问题。我们需要算法来解决问题。

算法是一种逐步过程,它定义了一组指令,按照特定顺序执行以获得期望的输出。一般来说,算法不限于任何编程语言或平台。它们独立于编程语言。算法必须具有以下特征:

  • 输入:算法必须有明确定义的输入。可以是 0 个或多个输入。

  • 输出:算法必须有明确定义的输出。它必须与期望的输出匹配。

  • 精确性:所有步骤都被精确定义。

  • 有限性:算法必须在一定数量的步骤之后停止。它不应该无限期运行。

  • 无歧义:算法应该清晰,任何步骤都不应该有任何歧义。

  • 独立性:算法应该独立于任何编程语言或平台。

现在让我们创建一个算法。但是为了做到这一点,我们需要一个问题陈述。所以让我们假设我们的图书馆有一批新书。有 1000 本书,它们没有按任何特定顺序排序。我们需要按照列表找到书,并将它们存放在指定的书架上。我们如何从一堆书中找到它们呢?

现在,我们可以以不同的方式解决问题。每种方式都有不同的解决问题的方法。我们称这些方法为算法。为了简洁明了地讨论,我们将只考虑两种解决问题的方法。我们知道还有其他几种方法,但为了简单起见,让我们只讨论一种算法。

我们将把书籍存放在一个简单的行中,以便我们可以看到书名。现在,我们将从列表中挑选一本书名,并从一端到另一端搜索,直到找到这本书。所以基本上,我们将为每本书进行顺序搜索。我们将重复这些步骤,直到将所有书放在指定的位置。

编写伪代码

计算机程序是为了机器阅读而编写的。我们必须以一定的格式编写它们,以便为机器理解而进行编译。但是,通常这些编写的代码对于程序员以外的人来说并不容易理解。为了以一种非正式的方式展示这些代码,以便人类也能理解,我们准备伪代码。虽然它不是实际的编程语言代码,但伪代码具有编程语言的类似结构约定。由于伪代码不能像真正的程序一样运行,因此没有标准的伪代码编写方式。我们可以按照自己的方式编写伪代码。

以下是我们用于查找书籍的算法的伪代码:

Algorithm FindABook(L,book_name) 

  Input: list of Books L & name of the search book_name 

  Output: False if not found or position of the book we are looking for. 

  if L.size = 0 return null 

  found := false 

  for each item in L, do 

    if item = book_name, then 

      found := position of the item 

  return found 

现在让我们检查我们写的伪代码。我们提供了一份书籍清单和一个我们正在搜索的名字。我们正在运行一个foreach循环,以迭代每一本书,并与我们正在搜索的书名进行匹配。如果找到了,我们将返回我们找到它的位置,否则返回false。因此,我们编写了一份伪代码来查找书名。但是其他剩下的书呢?我们如何继续搜索,直到所有书都找到并放在正确的书架上呢?

  Algorithm placeAllBooks 

    Input: list of Ordered Books OL, List of received books L 

    Output: nothing. 

    for each book_name in OL, do 

       if FindABook(L,book_name), then 

         remove the book from the list L 

         place it to the bookshelf 

现在我们有了解决书籍组织问题的算法的完整伪代码。在这里,我们正在浏览有序书籍列表,并在交付部分找到书籍。如果找到书籍,我们将其从列表中删除,并将其放到正确的书架上。

编写伪代码的这种简单方法可以帮助我们以结构化的方式解决更复杂的问题。由于伪代码独立于编程语言和平台,因此大多数时间算法都以伪代码的形式表达。

将伪代码转换为实际代码

现在我们将把我们的伪代码转换为实际的 PHP 7 代码,如下所示:

function findABook(Array $bookList, String $bookName) { 

    $found = FALSE; 

    foreach($bookList as $index => $book) { 

        if($book === $bookName) { 

             $found = $index; 

             break; 

        } 

    } 

    return $found; 

} 

function placeAllBooks(Array $orderedBooks, Array &$bookList) { 

    foreach ($orderedBooks as $book) { 

    $bookFound = findABook($bookList, $book); 

    if($bookFound !== FALSE) { 

        array_splice($bookList, $bookFound, 1); 

    } 

  } 

} 

$bookList = ['PHP','MySQL','PGSQL','Oracle','Java']; 

$orderedBooks = ['MySQL','PGSQL','Java']; 

placeAllBooks($orderedBooks, $bookList);

echo implode(",", $bookList); 

现在让我们理解前面的代码发生了什么。首先,我们在代码开头定义了一个新函数findABook。该函数定义了两个参数。一个是Array $bookList,另一个是String $bookName。在函数的开头,我们将$found初始化为FALSE,这意味着还没有找到任何东西。foreach循环遍历书籍列表数组$bookList,对于每本书,它与我们提供的书名$bookName进行匹配。如果我们正在寻找的书名与$bookList中的书名匹配,我们将匹配的索引(我们找到匹配的地方)赋给我们的$found变量。既然我们找到了,继续循环就没有意义了。因此,我们使用break命令来跳出循环。刚刚跳出循环,我们返回我们的$found变量。如果找到了书,通常$found将返回大于 0 的任何整数值,否则将返回false

function placeAllBooks(Array $orderedBooks, Array &$bookList) { 

    foreach ($orderedBooks as $book) { 

    $bookFound = findABook($bookList, $book); 

    if($bookFound !== FALSE) { 

        array_splice($bookList, $bookFound, 1); 

    } 

  } 

} 

这个特定的函数placeAllBooks实际上遍历了我们的有序书籍$orderedBooks。我们正在遍历我们的有序书籍列表,并使用findABook函数在我们的交付列表中搜索每本书。如果在有序列表中找到了这本书($bookFound !== FALSE),我们将使用 PHP 的array_splice()函数从交付书籍列表中移除该书:

$bookList = ['PHP','MySQL','PGSQL','Oracle','Java'];

$orderedBooks = ['MySQL','PGSQL','Java'];

这两行实际上显示了两个 PHP 数组,用于我们收到的书籍列表$bookList和我们实际订购的书籍列表$orderedBooks。我们只是使用一些虚拟数据来测试我们实现的代码,如下所示:

placeAllBooks($orderedBooks, $bookList);

我们的代码的最后一部分实际上调用函数placeAllBooks来执行整个操作,检查每本书在我们收到的书中的位置并将其移除,如果它在列表中。所以基本上,我们已经将我们的伪代码实现为实际的 PHP 代码,我们可以用它来解决我们的问题。

算法分析

我们在前一节完成了我们的算法。但我们还没有对我们的算法进行分析。在当前情况下一个有效的问题可能是,为什么我们真的需要对我们的算法进行分析?虽然我们已经编写了实现,但我们不确定我们编写的代码将利用多少资源。当我们说资源时,我们指的是运行应用程序所利用的时间和存储资源。我们编写算法来处理任何长度的输入。为了了解当输入增长时我们的算法的行为以及利用了多少资源,我们通常通过将输入长度与步骤数(时间复杂度)或存储空间(空间复杂度)相关联来衡量算法的效率。对于找到解决问题的最有效算法,进行算法分析非常重要。

我们可以在两个不同的阶段进行算法分析。一个是在实施之前完成的,另一个是在实施之后完成的。我们在实施之前进行的分析也被称为理论分析,我们假设其他因素如处理能力和空间将保持不变。实施后的分析被称为算法的经验分析,它可以因平台或语言而异。在经验分析中,我们可以从系统中获得关于时间和空间利用的可靠统计数据。

对于我们的放置书籍和从购买物品中找到书籍的算法,我们可以进行类似的分析。这次,我们将更关注时间复杂度而不是空间复杂度。我们将在接下来的章节中探讨空间复杂度。

计算复杂度

在算法分析中,我们测量两种复杂度:

  • 时间复杂度:时间复杂度是算法中关键操作的数量。换句话说,时间复杂度量化了算法从开始到结束所花费的时间量。

  • 空间复杂度:空间复杂度定义了算法在其生命周期中所需的空间(内存)量。它取决于数据结构和平台的选择。

现在让我们专注于我们实现的算法,并了解我们为算法执行的操作。在我们的placeAllBooks函数中,我们正在搜索我们的每一本订购的书。所以如果我们有 10 本书,我们会搜索 10 次。如果数量是 1000,我们会搜索 1000 次。所以简单地说,如果有n本书,我们将搜索n次。在算法分析中,输入数量通常用n表示。

对于我们有序书籍中的每一项,我们都在使用findABook函数进行搜索。在函数内部,我们再次搜索从placeAllBooks函数接收到的每一本书的名称。现在,如果我们足够幸运,我们可以在接收到的书籍列表的开头找到书的名称。在这种情况下,我们就不必搜索剩下的项目。但是如果我们非常不幸,我们搜索的书在列表的末尾怎么办?那么我们必须搜索每一本书,最后找到它。如果接收到的书籍数量也是n,那么我们必须进行n次比较。

如果我们假设其他操作是固定的,唯一的变量应该是输入大小。然后我们可以定义一个边界或数学方程来定义计算其运行时性能的情况。我们称之为渐近分析。渐近分析是输入边界,这意味着如果没有输入,其他因素是恒定的。我们使用渐近分析来找出算法的最佳情况、最坏情况和平均情况:

  • 最佳情况:最佳情况表示执行程序所需的最短时间。对于我们的示例算法,最佳情况可能是,对于每本书,我们只搜索第一项。因此,我们最终搜索的时间非常短。我们使用Ω符号(Σ符号)来表示最佳情况。

  • 平均情况:它表示执行程序所需的平均时间。对于我们的算法,平均情况将是大部分时间在列表中间找到书,或者一半时间在列表开头,剩下一半在列表末尾。

  • 最坏情况:它表示程序的最大运行时间。最坏情况的例子将是一直在列表末尾找书。我们使用O(大 O)符号来描述最坏情况。对于我们的算法中的每本书搜索,它可能需要**O(n)**的运行时间。从现在开始,我们将使用这个符号来表示我们算法的复杂性。

理解大 O(大 O)符号

大 O 符号对于算法分析非常重要。我们需要对这个符号有扎实的理解,并且知道如何在将来使用它。我们将在本节中讨论大 O 符号。

我们用于查找书籍并放置它们的算法有n个项目。对于第一本书的搜索,它将在最坏情况下比较n本书。如果我们说时间复杂度是T,那么对于第一本书,时间复杂度将是:

T(1) = n

当我们从列表中删除找到的书时,列表的大小现在是n-1。对于第二本书的搜索,它将在最坏情况下比较n-1本书。然后对于第二本书,时间复杂度将是n-1。结合这两个时间复杂度,对于前两本书,它将是:

T(2) = n + (n - 1)

如果我们像这样继续下去,经过n-1步后,最后一本书的搜索将只剩下1本书需要比较。因此,总复杂度看起来像:

T(n) = n + (n - 1) + (n - 2) + . . . . . . .  . . . . + 3 + 2 + 1 

现在,如果我们看一下前面的系列,它不是很熟悉吗?它也被称为n 个数字的和方程,如下所示:

因此我们可以写成:

T(n) = n(n + 1)/2 

或者:

T(n) = n2/2 + n/2 

对于渐近分析,我们忽略低阶项和常数乘数。由于我们有n2,我们可以轻松地忽略这里的n。此外,1/2 的常数乘数也可以被忽略。现在我们可以用大 O 符号表示时间复杂度为n的平方:

T(n) = O(n2) 

在整本书中,我们将使用这个大O符号来描述算法或操作的复杂性。以下是一些常见的大O符号:

类型符号
常数O (1)
线性O (n)
对数O (log n)
n log nO (n log n)
二次O (n² )
立方O (n³ )
指数O (2^n )

标准 PHP 库(SPL)和数据结构

标准 PHP 库SPL)是 PHP 语言近年来可能的最佳功能之一。SPL 被创建用来解决 PHP 中缺乏的常见问题。SPL 在许多方面扩展了语言,但 SPL 引人注目的特点之一是它对数据结构的支持。虽然 SPL 用于许多其他目的,但我们将专注于 SPL 的数据结构部分。SPL 随着核心 PHP 安装而提供,并不需要任何扩展或更改配置来启用它。

SPL 通过 PHP 中的面向对象编程提供了一组标准数据结构。支持的数据结构有:

  • 双向链表:它是在SplDoublyLinkedList中实现的。

  • :它是通过使用SplDoublyLinkedListSplStack中实现的。

  • 队列:它是通过使用SplDoublyLinkedListSplQueue中实现的。

  • :它是在SplHeap中实现的。它还支持SplMaxHeap中的最大堆和SplMinHeap中的最小堆。

  • 优先队列:它是通过使用SplHeapSplPriorityQueue中实现的。

  • 数组:它是在SplFixedArray中实现的,用于固定大小的数组。

  • 映射:它是在SplObjectStorage中实现的。

在接下来的章节中,我们将探索 SPL 数据结构的每个实现,并了解它们的优缺点,以及与我们相应数据结构的实现的性能分析。但由于这些数据结构已经内置,我们可以用它们来快速实现功能和应用程序。

在 PHP 7 发布后,人们对 PHP 应用程序的性能提升感到高兴。在许多情况下,PHP SPL 并没有类似的性能提升,但我们将在即将到来的章节中对其进行分析。

摘要

在本章中,我们专注于基本数据结构及其名称的讨论。我们还学习了解决问题的定义步骤,即算法。我们还学习了分析算法和大O符号,以及如何计算复杂性。我们简要介绍了 PHP 中内置数据结构的 SPL 形式。

在下一章中,我们将专注于 PHP 数组,这是 PHP 中最强大、灵活的数据类型之一。我们将探索 PHP 数组的不同用途,以实现不同的数据结构,如哈希表、映射、结构等。

第二章:理解 PHP 数组

PHP 数组是 PHP 中最常用的数据类型之一。大多数时候,我们在不考虑 PHP 数组对我们开发的代码或应用程序的影响的情况下使用它。它非常易于使用和动态的;我们喜欢几乎可以用 PHP 数组来实现任何目的。有时,我们甚至不想探索是否有其他可用的解决方案可以代替 PHP 数组。在本章中,我们将探索 PHP 数组的优缺点,以及如何在不同的数据结构实现中使用数组以及提高性能。我们将从解释 PHP 中不同类型的数组开始,然后创建固定大小的数组。然后我们将看到 PHP 数组元素的内存占用情况,以及如何改进它们以及一些数据结构的实现。

更好地理解 PHP 数组

PHP 数组是如此动态和灵活,以至于我们必须考虑它是常规数组,关联数组还是多维数组,就像其他一些语言一样。我们不需要定义要使用的数组的大小和数据类型。PHP 如何做到这一点,而其他语言如 C 和 Java 却不能做到呢?答案很简单:PHP 中的数组概念实际上并不是真正的数组,它实际上是一个 HashMap。换句话说,PHP 数组不是我们从其他语言中得到的简单数组概念。一个简单的数组看起来像这样:

但是,我们绝对可以用 PHP 做到。让我们通过一个例子来检查:

$array = [1,2,3,4,5];

这一行显示了典型数组的外观。类似类型的数据具有顺序索引(从 0 到 4),以访问值。那么谁说 PHP 数组不是典型数组呢?让我们探索更多的例子。考虑以下:

$mixedArray = [];

$mixedArray[0] = 200;

$mixedArray['name'] = "Mixed array";

$mixedArray[1] = 10.65;

$mixedArray[2] = ['I', 'am', 'another', 'array'];

这是我们每天都在使用的 PHP 数组;我们不定义大小,我们存储整数、浮点数、字符串,甚至另一个数组。这听起来奇怪还是 PHP 的超能力?我们可以从php.net的定义中了解。

在 PHP 中,数组实际上是一个有序映射。映射是一种将值与键关联的类型。这种类型针对多种不同的用途进行了优化;它可以被视为数组、列表(向量)、哈希表(映射的一种实现)、字典、集合、栈、队列,可能还有更多。由于数组的值可以是其他数组,因此也可以存在树和多维数组。

因此,PHP 数组具有真正的超能力,可以用于所有可能的数据结构,如列表/向量、哈希表、字典、集合、栈、队列、双向链表等。看起来 PHP 数组是以这样一种方式构建的,要么对所有事情进行了优化,要么对任何事情都没有进行优化。我们将在本章中探索这一点。

如果我们想对数组进行分类,那么主要有三种类型的数组:

  • 数字数组

  • 关联数组

  • 多维数组

我们将通过一些例子和解释来探索每种类型的数组。

数字数组

数字数组并不意味着它只包含数字数据。实际上,它意味着索引只能是数字。在 PHP 中,它们可以是顺序的或非顺序的,但它们必须是数字。在数字数组中,值以线性方式存储和访问。以下是一些 PHP 数字数组的例子:

$array = [10,20,30,40,50]; 

$array[] = 70;  

$array[] = 80; 

$arraySize = count($array); 

for($i = 0;$i<$arraySize;$i++) { 

    echo "Position ".$i." holds the value ".$array[$i]."\n"; 

} 

这将产生以下输出:

Position 0 holds the value 10 

Position 1 holds the value 20 

Position 2 holds the value 30 

Position 3 holds the value 40 

Position 4 holds the value 50 

Position 5 holds the value 70 

Position 6 holds the value 80 

这是一个非常简单的例子,我们定义了一个数组,并且索引是从 0 自动生成的,并且随着数组的值递增。当我们使用$array[]在数组中添加一个新元素时,它实际上会递增索引并将值分配给新索引。这就是为什么值 70 具有索引 5,80 具有索引 6。

如果我们的数据是连续的,我们总是可以使用for循环而不会出现任何问题。当我们说连续时,我们不仅仅是指 0,1,2,3....,n。它可以是 0,5,10,15,20,......,n,其中n是 5 的倍数。或者它可以是 1,3,5,7,9......,n,其中n是奇数。我们可以创建数百种这样的序列来使数组成为数字。

一个重要的问题可能是,如果索引不是连续的,我们不能构造一个数字数组吗?是的,我们肯定可以。我们只需要采用不同的迭代方式。考虑以下示例:

$array = []; 

$array[10] = 100; 

$array[21] = 200; 

$array[29] = 300; 

$array[500] = 1000; 

$array[1001] = 10000; 

$array[71] = 1971; 

foreach($array as $index => $value) { 

    echo "Position ".$index." holds the value ".$value."\n"; 

} 

如果我们看索引,它们不是连续的。它们具有随机索引,例如10后面是2129等等。甚至在最后,我们有索引71,它比之前的1001要小得多。所以,最后一个索引应该在 29 和 500 之间吗?以下是输出:

Position 10 holds the value 100 

Position 21 holds the value 200 

Position 29 holds the value 300 

Position 500 holds the value 1000 

Position 1001 holds the value 10000 

Position 71 holds the value 1971 

这里有几件事情需要注意:

我们按照输入数据的方式迭代数组。索引没有任何内部排序,尽管它们都是数字。

另一个有趣的事实是数组$array的大小只有6。它不像 C++、Java 或其他语言中需要在使用之前预定义数组大小的1002,最大索引可以是n-1,其中n是数组的大小。

关联数组

关联数组是通过可以是任何字符串的键来访问的。在关联数组中,值存储在键而不是线性索引之间。我们可以使用关联数组来存储任何类型的数据,就像数字数组一样。让我们创建一个学生数组,我们将在其中存储学生信息:

$studentInfo = []; 

$studentInfo['Name'] = "Adiyan"; 

$studentInfo['Age'] = 11; 

$studentInfo['Class'] = 6; 

$studentInfo['RollNumber'] = 71; 

$studentInfo['Contact'] = "info@adiyan.com"; 

foreach($studentInfo as $key => $value) { 

    echo $key.": ".$value."\n"; 

} 

以下是代码的输出:

Name: Adiyan 

Age: 11 

Class: 6 

RollNumber: 71 

Contact: info@adiyan.com 

在这里,我们使用每个键来保存一条数据。我们可以根据需要添加任意多个键而不会出现任何问题。这使我们能够使用 PHP 关联数组来表示类似结构、映射和字典的数据结构。

多维数组

多维数组包含多个数组。换句话说,它是一个数组的数组。在本书中,我们将在不同的示例中使用多维数组,因为它们是存储图形和其他树状数据结构的数据的最流行和高效的方式之一。让我们使用一个示例来探索 PHP 多维数组:

$players = [];

$players[] = ["Name" => "Ronaldo", "Age" => 31, "Country" => "Portugal", "Team" => "Real Madrid"];

$players[] = ["Name" => "Messi", "Age" => 27, "Country" => "Argentina", "Team" => "Barcelona"];

$players[] = ["Name" => "Neymar", "Age" => 24, "Country" => "Brazil", "Team" => "Barcelona"];

$players[] = ["Name" => "Rooney", "Age" => 30, "Country" => "England", "Team" => "Man United"];

foreach($players as $index => $playerInfo) { 

    echo "Info of player # ".($index+1)."\n";

    foreach($playerInfo as $key => $value) { 

        echo $key.": ".$value."\n";

    } 

    echo "\n";

} 

我们刚刚看到的示例是一个二维数组的示例。因此,我们使用两个foreach循环来迭代二维数组。以下是代码的输出:

Info of player # 1 

Name: Ronaldo 

Age: 31 

Country: Portugal 

Team: Real Madrid 

Info of player # 2 

Name: Messi 

Age: 27 

Country: Argentina 

Team: Barcelona 

Info of player # 3 

Name: Neymar 

Age: 24 

Country: Brazil 

Team: Barcelona 

Info of player # 4 

Name: Rooney 

Age: 30 

Country: England 

Team: Man United  

我们可以根据需要使用 PHP 创建 n 维数组,但是我们必须记住一件事:我们添加的维度越多,结构就会变得越复杂。我们通常可以想象三维,所以为了拥有超过三维的数组,我们必须对多维数组的工作原理有扎实的理解。

我们可以在 PHP 中将数字数组和关联数组作为单个数组使用。但在这种情况下,我们必须非常谨慎地选择正确的方法来迭代数组元素。在这种情况下,foreach将比forwhile循环更好。

使用数组作为灵活的存储

到目前为止,我们已经看到 PHP 数组作为一种动态的、混合的数据结构,用于存储任何类型的数据。这给了我们更多的自由度,可以将数组用作灵活的存储容器。我们可以在单个数组中混合不同的数据类型和不同维度的数据。我们甚至不必定义我们将要使用的数组的大小或类型。我们可以在需要时随时增加、缩小和修改数据到数组中。

PHP 不仅允许我们创建动态数组,而且还为数组提供了许多内置功能。例如:array_intersectarray_mergearray_diffarray_pusharray_popprevnextcurrentend等等。

使用多维数组表示数据结构

在接下来的章节中,我们将讨论许多不同的数据结构和算法。我们将重点讨论图形。我们已经知道图形数据结构的定义。大多数时候,我们将使用 PHP 多维数组来表示数据,作为邻接矩阵。让我们考虑以下图表:

现在,如果我们把图的每个节点看作是一个数组的值,我们可以表示节点为:

$nodes = ['A', 'B', 'C', 'D', 'E'];

但这只会给我们节点名称。我们无法连接或创建节点之间的关系。为了做到这一点,我们需要构建一个二维数组,其中节点名称将是键,值将基于两个节点的互连性为 0 或 1。由于图中没有提供方向,我们不知道A是否连接到C或连接到A。所以我们假设两者彼此连接。

首先,我们需要为图创建一个数组,并将二维数组的每个节点初始化为 0。以下代码将确切地做到这一点:

$graph = [];

$nodes = ['A', 'B', 'C', 'D', 'E'];

foreach ($nodes as $xNode) {

    foreach ($nodes as $yNode) {

        $graph[$xNode][$yNode] = 0;

    }

}

让我们使用以下代码打印数组,以便在定义节点之间的连接之前看到它的实际外观:

foreach ($nodes as $xNode) {

    foreach ($nodes as $yNode) {

        echo $graph[$xNode][$yNode] . "\t";

    }

    echo "\n";

}

由于节点之间的连接未定义,所有单元格都显示为 0。因此输出看起来像这样:

0       0       0       0       0

0       0       0       0       0

0       0       0       0       0

0       0       0       0       0

0       0       0       0       0

现在我们将定义节点之间的连接,使两个节点之间的连接表示为 1 的值,就像以下代码一样:

$graph["A"]["B"] = 1;

$graph["B"]["A"] = 1;

$graph["A"]["C"] = 1;

$graph["C"]["A"] = 1;

$graph["A"]["E"] = 1;

$graph["E"]["A"] = 1;

$graph["B"]["E"] = 1;

$graph["E"]["B"] = 1;

$graph["B"]["D"] = 1;

$graph["D"]["B"] = 1;

由于图表中没有给出方向,我们将其视为无向图,因此我们为每个连接设置了两个值为 1。对于AB之间的连接,我们将$graph["A"]["B"]$graph["B"]["A"]都设置为1。我们将在后面的章节中了解更多关于定义节点之间连接的内容以及为什么我们这样做。现在我们只关注如何使用多维数组来表示数据结构。我们可以重新打印矩阵,这次输出看起来像这样:

0       1       1       0       1

1       0       0       1       1

1       0       0       0       0

0       1       0       0       0

1       1       0       0       0

在第九章中,将图形投入实践,更有趣和有趣地了解图形及其操作。

使用 SplFixedArray 方法创建固定大小的数组

到目前为止,我们已经探讨了 PHP 数组,我们知道,我们不定义数组的大小。PHP 数组可以根据我们的需求增长或缩小。这种灵活性带来了关于内存使用的巨大不便。我们将在本节中探讨这一点。现在,让我们专注于使用 SPL 库创建固定大小的数组。

为什么我们需要一个固定大小的数组?它有什么额外的优势吗?答案是,当我们知道我们只需要数组中的一定数量的元素时,我们可以使用固定数组来减少内存使用。在进行内存使用分析之前,让我们举一些使用SplFixedArray方法的例子:

$array = new SplFixedArray(10);

for ($i = 0; $i < 10; $i++)

    $array[$i] = $i;

for ($i = 0; $i < 10; $i++)

    echo $array[$i] . "\n";

首先,我们创建一个具有定义大小为 10 的新SplFixedArray对象。其余行实际上遵循我们在常规 PHP 数组值分配和检索中使用的相同原则。如果我们想访问超出范围的索引(这里是 10),它将抛出一个异常:

PHP Fatal error:  Uncaught RuntimeException: Index invalid or out of range

PHP 数组和SplFixedArray之间的基本区别是:

  • SplFixedArray必须有一个固定的定义大小

  • SplFixedArray的索引必须是整数,并且在 0 到n的范围内,其中n是我们定义的数组的大小

当我们有许多已知大小的定义数组或数组的最大所需大小有一个上限时,SplFixedArray方法可能非常方便。但如果我们不知道数组的大小,那么最好使用 PHP 数组。

常规 PHP 数组和 SplFixedArray 之间的性能比较

在上一节中我们遇到的一个关键问题是,为什么我们应该使用SplFixedArray而不是 PHP 数组?我们现在准备探讨答案。我们发现 PHP 数组实际上不是数组,而是哈希映射。让我们在 PHP 5.x 版本中运行一个小例子代码,看看 PHP 数组的内存使用情况。

让我们创建一个包含 100,000 个唯一 PHP 整数的数组。由于我正在运行 64 位机器,我期望每个整数占用 8 个字节。因此,我们将为数组消耗大约 800,000 字节的内存。以下是代码:

$startMemory = memory_get_usage();

$array = range(1,100000);

$endMemory = memory_get_usage();

echo ($endMemory - $startMemory)." bytes";

如果我们在命令提示符中运行这段代码,我们将看到一个输出为 14,649,040 字节。是的,没错。内存使用量几乎是我们计划的 18.5 倍。这意味着对于一个 PHP 数组中的每个元素,会有 144 字节(18 * 8 字节)的开销。那么,这额外的 144 字节是从哪里来的,为什么 PHP 为每个数组元素使用这么多额外的内存?以下是 PHP 数组使用的额外字节的解释:

这张图表展示了 PHP 数组的内部工作原理。它将数据存储在一个桶中,以避免冲突并容纳更多数据。为了管理这种动态性,它在数组的内部实现了双向链表和哈希表。最终,这将为数组中的每个单独元素消耗大量额外的内存空间。以下是基于 PHP 数组实现代码(C 代码)的每个元素的内存消耗的详细情况:

32 位64 位
zval16 字节24 字节
+循环 GC 信息4 字节8 字节
+分配头8 字节16 字节
zval(值)总计28 字节48 字节
bucket36 字节72 字节
+分配头8 字节16 字节
+指针4 字节8 字节
bucket(数组元素)总计48 字节96 字节
总计(bucket+zval)76 字节144 字节

为了理解 PHP 数组的内部结构,我们需要深入研究 PHP 内部。这超出了本书的范围。一个很好的推荐阅读是:nikic.github.io/2011/12/12/How-big-are-PHP-arrays-really-Hint-BIG.html

在新的 PHP 7 版本中,PHP 数组的内部构造有了很大的改进。结果,每个元素的 144 字节的开销仅降至 36 字节。这是一个很大的改进,适用于 32 位和 64 位操作系统。下面是一个比较图表,包含一个包含 100,000 个项目的数组:

$array = Range(1,100000)32 位64 位
PHP 5.6 或更低7.4 MB14 MB
PHP 73 MB4 MB

换句话说,对于 32 位系统,PHP 7 的改进系数为 2.5 倍,对于 64 位系统为 3.5 倍。这是一个真正的改进。但这一切都是关于 PHP 数组的,那么SplFixedArray呢?让我们在 PHP 7 和 PHP 5.x 中使用SplFixArray运行相同的示例:

$items = 100000; 

$startMemory = memory_get_usage(); 

$array = new SplFixedArray($items); 

for ($i = 0; $i < $items; $i++) { 

    $array[$i] = $i; 

} 

$endMemory = memory_get_usage(); 

$memoryConsumed = ($endMemory - $startMemory) / (1024*1024); 

$memoryConsumed = ceil($memoryConsumed); 

echo "memory = {$memoryConsumed} MB\n"; 

我们在这里写了SplFixedArray的内存消耗功能。如果我们只是将行$array = new SplFixedArray($items);更改为$array = [];,我们将得到与 PHP 数组相同的代码运行。

基准测试结果可能因机器而异,因为可能有不同的操作系统、内存大小、调试器开/关等。建议在自己的机器上运行代码,以生成类似的基准测试进行比较。

以下是 64 位系统中包含 100,000 个整数的 PHP 数组和SplFixedArray的内存消耗比较:

100,000 个项目使用 PHP 数组SplFixedArray
PHP 5.6 或更低14 MB6 MB
PHP 75 MB2 MB

SplFixedArray 不仅在内存使用上更快,而且在执行速度上也比一般的 PHP 数组操作更快,比如访问值,赋值等等。

尽管我们可以像数组一样使用SplFixedArray对象,但 PHP 数组函数不适用于SplFixedArray。我们不能直接应用任何 PHP 数组函数,比如array_sumarray_filter等等。

使用 SplFixedArray 的更多示例

由于SplFixedArray具有良好的性能提升指标,我们可以在大多数数据结构和算法中利用它,而不是使用常规的 PHP 数组。现在我们将探讨在不同场景中使用SplFixedArray的更多示例。

从 PHP 数组转换为 SplFixedArray

我们已经看到了如何创建一个具有固定长度的SplFixedArray。如果我想在运行时创建一个SplFixedArray数组呢?以下代码块显示了如何实现:

$array =[1 => 10, 2 => 100, 3 => 1000, 4 => 10000]; 

$splArray = SplFixedArray::fromArray($array); 

print_r($splArray); 

在这里,我们使用SplFixedArray类的静态方法fromArray从现有数组$array构造了一个SplFixedArray。然后我们使用 PHP 的print_r函数打印数组。它将显示如下输出:

SplFixedArray Object 

( 

    [0] => 

    [1] => 10 

    [2] => 100 

    [3] => 1000 

    [4] => 10000 

) 

我们可以看到数组现在已经转换为SplFixedArray,并且它保持了与实际数组中完全相同的索引号。由于实际数组没有定义 0 索引,因此索引 0 保持为 null。但是,如果我们想忽略以前数组的索引并为它们分配新的索引,那么我们必须将上一个代码的第二行更改为这样:

$splArray = SplFixedArray::fromArray($array,false); 

现在,如果我们再次打印数组,将会得到以下输出:

SplFixedArray Object

( 

    [0] => 10

    [1] => 100

    [2] => 1000

    [3] => 10000

) 

如果我们想在运行时将数组转换为固定数组,最好是在不再使用常规 PHP 数组时取消它。如果数组很大,这将节省内存使用。

将 SplFixedArray 转换为 PHP 数组

我们可能还需要将SplFixedArray转换为常规 PHP 数组,以应用 PHP 中的一些预定义数组函数。与前面的示例一样,这也是一件非常简单的事情:

$items = 5; 

$array = new SplFixedArray($items); 

for ($i = 0; $i < $items; $i++) { 

    $array[$i] = $i * 10; 

} 

$newArray = $array->toArray(); 

print_r($newArray); 

这将产生以下输出:

Array 

( 

    [0] => 0 

    [1] => 10 

    [2] => 20 

    [3] => 30 

    [4] => 40 

) 

在声明后更改 SplFixedArray 大小

由于我们在开始时定义了数组大小,可能需要稍后更改大小。为了做到这一点,我们必须使用SplFixedArray类的setSize()方法。示例如下:

$items = 5; 

$array = new SplFixedArray($items); 

for ($i = 0; $i < $items; $i++) { 

    $array[$i] = $i * 10; 

} 

$array->setSize(10); 

$array[7] = 100; 

使用 SplFixedArray 创建多维数组

我们可能还需要使用SplFixedArray创建两个或更多维数组。为了做到这一点,建议按照以下示例操作:

$array = new SplFixedArray(100);

for ($i = 0; $i < 100; $i++) 

$array[$i] = new SplFixedArray(100);

实际上,我们在每个数组索引内部创建了另一个SplFixedArray。我们可以添加任意多的维度。但我们必须记住,随着维度的增加,数组的大小也会增加。因此,它可能会非常快速地变得非常大。

理解哈希表

在编程语言中,哈希表是一种数据结构,用于使数组成为关联数组。这意味着我们可以使用键来映射值,而不是使用索引。哈希表必须使用哈希函数来计算数组桶或槽的索引,从中可以找到所需的值:

正如我们已经多次提到的,PHP 数组实际上是一个哈希表,因此支持关联数组。我们需要记住一件事:对于关联数组实现,我们不需要定义哈希函数。PHP 会在内部为我们做这件事。因此,当我们在 PHP 中创建关联数组时,实际上是在创建一个哈希表。例如,以下代码可以被视为哈希表:

$array = []; 

$array['Germany'] = "Position 1"; 

$array['Argentina'] = "Position 2"; 

$array['Portugal'] = "Position 6"; 

$array['Fifa_World_Cup'] = "2018 Russia";  

事实上,我们可以直接调用任何键,复杂度只有O(1)。键将始终引用桶内相同的索引,因为 PHP 将使用相同的哈希函数来计算索引。

使用 PHP 数组实现结构

正如我们已经知道的,结构是一种复杂的数据类型,我们在其中定义多个属性作为一组,以便我们可以将其用作单个数据类型。我们可以使用 PHP 数组和类来编写结构。以下是使用 PHP 数组编写结构的示例:

$player = [ 

    "name" => "Ronaldo", 

    "country" => "Portugal", 

    "age" => 31, 

    "currentTeam" => "Real Madrid" 

]; 

它只是一个具有字符串键的关联数组。可以使用单个或多个结构来构造复杂的结构作为其属性。例如,使用 player 结构,我们可以使用 team 结构:

$ronaldo = [ 

    "name" => "Ronaldo", 

    "country" => "Portugal", 

    "age" => 31, 

    "currentTeam" => "Real Madrid" 

]; 

$messi = [ 

    "name" => "Messi", 

    "country" => "Argentina", 

    "age" => 27, 

    "currentTeam" => "Barcelona" 

]; 

$team = [ 

    "player1" => $ronaldo, 

    "player2" => $messi 

]; 

The same thing we can achieve using PHP Class. The example will look like:  

Class Player { 

    public $name; 

    public $country; 

    public $age; 

    public $currentTeam; 

} 

$ronaldo = new Player; 

$ronaldo->name = "Ronaldo"; 

$ronaldo->country = "Portugal"; 

$ronaldo->age = 31; 

$ronaldo->currentTeam = "Real Madrid"; 

由于我们已经看到了定义结构的两种方式,我们必须选择其中一种来实现结构。虽然创建对象可能看起来更方便,但与数组实现相比,它的速度较慢。数组具有速度的优势,但它也有一个缺点,即它占用比对象更多的内存空间。现在我们必须根据自己的偏好做出决定。

使用 PHP 数组实现集合

集合只是一个没有特定顺序的值的集合。它可以包含任何数据类型,我们可以运行不同的集合操作,如并集、交集、补集等。由于集合只包含值,我们可以构建一个基本的 PHP 数组,并为其分配值,使其动态增长。以下示例显示了我们定义的两个集合;一个包含一些奇数,另一个包含一些质数:

$odd = []; 

$odd[] = 1; 

$odd[] = 3; 

$odd[] = 5; 

$odd[] = 7; 

$odd[] = 9; 

$prime = []; 

$prime[] = 2; 

$prime[] = 3; 

$prime[] = 5; 

为了检查值在集合中的存在以及并集、交集和补集操作,我们可以使用以下示例:

if (in_array(2, $prime)) { 

    echo "2 is a prime"; 

} 

$union = array_merge($prime, $odd); 

$intersection = array_intersect($prime, $odd); 

$compliment = array_diff($prime, $odd);  

PHP 有许多用于此类操作的内置函数,我们可以利用它们进行集合操作。但是我们必须考虑一个事实:由于集合没有以任何特定方式排序,使用in_array()函数进行搜索在最坏的情况下可能具有**O(n)**的复杂度。array_merge()函数也是如此,它将检查一个数组中的每个值与另一个数组。为了加快速度,我们可以稍微修改我们的代码,使其更加高效:

$odd = []; 

$odd[1] = true; 

$odd[3] = true; 

$odd[5] = true; 

$odd[7] = true; 

$odd[9] = true; 

$prime = []; 

$prime[2] = true; 

$prime[3] = true; 

$prime[5] = true; 

if (isset($prime[2])) { 

    echo "2 is a prime"; 

} 

$union = $prime + $odd; 

$intersection = array_intersect_key($prime, $odd); 

$compliment = array_diff_key($prime, $odd); 

如果我们分析这段代码,我们可以看到我们使用索引或键来定义集合。由于 PHP 数组索引或键查找的复杂度为O(1),这使得搜索速度更快。因此,所有查找、并集、交集和补集操作将比上一个示例花费更少的时间。

PHP 数组的最佳用法

虽然 PHP 数组消耗更多内存,但使用 PHP 数组的灵活性对于许多数据结构来说更为重要。因此,我们将在许多数据结构实现和算法中使用 PHP 常规数组以及SplFixedArray。如果我们只将 PHP 数组视为我们数据的容器,那么我们将更容易地利用其在许多数据结构实现中的强大功能。除了内置函数,PHP 数组绝对是使用 PHP 进行编程和开发应用程序时必不可少的数据结构。

PHP 有一些用于数组的内置排序函数。它可以使用键和值进行排序,并在排序时保持关联。我们将在第七章中探索这些内置函数,使用排序算法

PHP 数组,它是性能杀手吗?

在本章中,我们已经看到 PHP 数组中的每个元素都具有非常大的内存开销。由于这是语言本身完成的,除了在适用的情况下使用SplFixedArray而不是常规数组之外,我们几乎无能为力。但是,如果我们从 PHP 5.x 版本迁移到新的 PHP 7,那么我们的应用程序将有巨大的改进,无论我们使用常规 PHP 数组还是SplFixedArray

在 PHP 7 中,哈希表的内部实现发生了巨大的变化,它并不是为了效率而构建的。因此,每个元素的开销内存消耗显著减少。虽然我们可以争论较少的内存消耗并不会使代码更快,但我们可以反驳,如果我们有更少的内存来管理,我们可以更多地专注于执行而不是内存管理。因此,这对性能产生了一些影响。

从讨论中可以很容易地得出结论,PHP 7 中新改进的数组绝对是开发人员解决复杂和内存高效应用程序的推荐选择。

总结

在本章中,我们专注于讨论 PHP 数组以及使用 PHP 数组作为数据结构可以做什么。我们将在接下来的章节中继续探讨数组的特性。在下一章中,我们将专注于链表数据结构和不同变体的链表。我们还将探索关于链表及其最佳用法的不同类型的实际示例。

第三章:使用链表

我们已经对数组有了很多了解。现在,我们将把重点转移到一种称为list的新类型的数据结构上。它是编程世界中最常用的数据结构之一。在大多数编程语言中,数组是一个固定大小的结构。因此,它无法动态增长,从固定大小数组中缩小或删除项目也是有问题的,因为我们必须移动数组的项目来填补空白。因此,许多开发人员更喜欢使用列表而不是数组。考虑到每个数组元素可能有一些额外字节的开销,链表可以在内存效率是一个重要因素的情况下使用。在本章中,我们将探讨 PHP 中不同类型的链表及其实现。我们还将看看可以使用链表解决的现实世界问题。

什么是链表?

链表是一组称为节点的对象。每个节点都与下一个节点连接,连接是一个对象引用。如果我们考虑以下图像,每个框代表一个节点。箭头表示节点之间的链接。这是一个单向链表的示例。最后一个节点包含 NULL 的下一个链接,因此它标记了列表的结束:

节点是一个对象,意味着它可以存储任何数据类型,如字符串、整数、浮点数,或者复杂的数据类型,如数组、数组的数组、对象或对象数组。我们可以根据需要存储任何东西。

我们还可以在链表上执行各种操作,例如以下操作:

  • 检查列表是否为空

  • 显示列表中的所有项目

  • 在列表中搜索项目

  • 获取列表的大小

  • 在列表的开头或结尾插入新项目

  • 从列表的开头或结尾删除项目

  • 在特定位置或在某个项目之前/之后插入新项目

  • 反转列表

这些只是可以在链表上执行的一些操作。

让我们编写一个简单的链表来存储一些名称:

class ListNode { 

    public $data = NULL; 

    public $next = NULL; 

    public function __construct(string $data = NULL) { 

        $this->data = $data; 

    } 

}

我们之前提到链表由节点组成。我们为我们的节点创建了一个简单的类。ListNode类有两个属性:一个用于存储数据,另一个用于称为next的链接。现在,我们将使用ListNode类实现一个链表。为简单起见,我们只有两个操作:insertdisplay

class LinkedList { 

    private $_firstNode = NULL; 

    private $_totalNodes = 0; 

    public function insert(string $data = NULL) { 

       $newNode = new ListNode($data); 

        if ($this->_firstNode === NULL) {           

            $this->_firstNode = &$newNode;             

        } else { 

            $currentNode = $this->_firstNode; 

            while ($currentNode->next !== NULL) { 

                $currentNode = $currentNode->next; 

            } 

            $currentNode->next = $newNode; 

        } 

       $this->_totalNode++; 

        return TRUE; 

    } 

    public function display() { 

      echo "Total book titles: ".$this->_totalNode."\n"; 

        $currentNode = $this->_firstNode; 

        while ($currentNode !== NULL) { 

            echo $currentNode->data . "\n"; 

            $currentNode = $currentNode->next; 

        } 

    } 

} 

前面的代码实际上实现了我们的两个基本操作insertdisplay节点。在LinkedList类中,我们有两个私有属性:$_firstNode$_totalNodes。它们的默认值分别为NULL0。我们需要标记头节点或第一个节点,以便我们始终知道从哪里开始。我们也可以称之为前节点。无论我们提供什么名称,它主要用于指示链表的开始。现在,让我们转到insert操作代码。

插入方法接受一个参数,即数据本身。我们将使用ListNode类使用数据创建一个新节点。在我们的链表中插入书名之前,我们必须考虑两种可能性:

  • 列表为空,我们正在插入第一个标题

  • 列表不为空,标题将被添加到末尾

为什么我们需要考虑两种情况?答案很简单。如果我们不知道列表是否为空,我们的操作可能会得到不同的结果。我们还可能在节点之间创建无效的链接。因此,如果列表为空,我们的插入项将成为列表的第一项。这是代码的第一部分在做的事情:

$newNode = new ListNode($data); 

if ($this->_firstNode === NULL) {             

          $this->_firstNode = &$newNode; 

}

从上述代码片段中,我们可以看到我们正在创建一个带有数据的新节点,并将节点对象命名为$newNode。之后,它检查$_firstNode是否为NULL。如果是NULL,那么列表是空的。如果为空,那么我们将$newNode对象分配给$_firstNode属性。现在,insert方法的剩余部分代表了我们的第二个条件,即列表不为空,我们必须在列表的末尾添加新项目:

$currentNode = $this->_firstNode;    

while ($currentNode->next !== NULL) { 

  $currentNode = $currentNode->next; 

} 

$currentNode->next = $newNode; 

在这里,我们从$_firstNode属性获取列表的第一个节点。现在,我们将从第一个节点迭代到列表的末尾。我们将通过检查当前节点的下一个链接是否为NULL来确保这一点。如果它是NULL,那么我们已经到达了列表的末尾。为了确保我们不会一直循环到同一个节点,我们在迭代过程中将下一个节点设置为当前节点的当前项。while循环代码实现了这个逻辑。一旦我们退出while循环,我们将链表的最后一个节点设置为$currentNode。现在,我们必须将当前最后一个节点的下一个链接分配给新创建的名为$newNode的节点,所以我们简单地将对象放到节点的下一个链接中。这个对象引用将作为两个节点对象之间的链接。最后,我们通过后增加$_totalNode属性来增加总节点计数值 1。

我们本可以轻松地为列表创建另一个属性,用于跟踪最后一个节点。这样可以避免在插入新节点时每次都循环整个列表。我们故意忽略了这个选项,以便通过对链表的基本理解来进行工作。在本章的后面,我们将实现这一点以实现更快的操作。

如果我们看看我们的display方法,我们会发现我们几乎使用了类似的逻辑来迭代每个节点并显示其内容。我们首先获取链表的头节点。然后,我们迭代列表直到列表项为 NULL。在循环内,我们通过显示其$data属性来显示节点数据。现在,我们有一个节点类ListNode来为链表创建单独的节点,还有一个LinkedList类来执行基本的insertdisplay操作。让我们编写一小段代码来利用LinkedList类来创建一个书名的链表:

$BookTitles = new LinkedList(); 

$BookTitles->insert("Introduction to Algorithm"); 

$BookTitles->insert("Introduction to PHP and Data structures"); 

$BookTitles->insert("Programming Intelligence"); 

$BookTitles->display(); 

在这里,我们为LinkedList创建一个新对象,并将其命名为$BookTitles。然后,我们使用insert方法插入新的书籍项目。我们添加了三本书,然后使用display方法显示书名。如果我们运行上述代码,我们将看到以下输出:

Total book titles: 3

Introduction to Algorithm

Introduction to PHP and Data structures

Programming Intelligence

正如我们所看到的,第一行有一个计数器,显示我们有三本书的标题,以及它们的名称。如果我们仔细看,我们会发现书名的显示方式与我们输入的方式相同。这意味着我们实现的链表实际上是保持顺序的。这是因为我们总是在列表的末尾输入新节点。如果我们愿意,我们可以以不同的方式做到这一点。作为我们的第一个例子,我们已经涵盖了很多关于链表以及如何构建它们的内容。在接下来的章节中,我们将更多地探索如何创建不同类型的链表,并且使用更复杂的例子。现在,我们将专注于不同类型的链表。

不同类型的链表

到目前为止,我们已经处理了一种称为单链表或线性链表的列表类型。然而,还有基于涉及的操作的几种不同类型的链表:

  • 双向链表

  • 循环链表

  • 多链表

双向链表

在双向链表中,每个节点有两个链接:一个指向下一个节点,另一个指向前一个节点。单向链表是单向的,双向链表是双向的。我们可以在列表中前进或后退而不会出现任何问题。以下图片显示了一个示例双向链表。稍后,在在 PHP 中实现双向链表部分,我们将探讨如何实现双向链表:

循环链表

在单向或双向链表中,最后一个节点后面没有节点,因此最后一个节点没有任何后续节点可以迭代。如果允许最后一个节点指向第一个节点,我们就形成了一个循环。这样的链表称为循环链表。我们可以将单向链表和双向链表都作为循环链表。在本章中,我们还将实现一个循环链表。以下图片展示了一个循环链表:

多链表

多链表,或多重链表,是一种特殊类型的链表,每个节点与另一个节点有两个或更多个链接。它可以根据链表的目的多向增长。例如,如果我们以学生列表为例,每个学生都是一个具有姓名、年龄、性别、系别、专业等属性的节点,那么我们可以将每个学生的节点不仅与下一个和上一个节点链接,还与年龄、性别、系别和专业链接。尽管使用这样的链表需要对链表概念有很好的理解,但我们可以在需要时使用这样的特殊链表。以下图片展示了一个多链表:

插入、删除和搜索项目

到目前为止,我们只看到了插入节点和显示所有节点内容的操作。现在,我们将探索链表中的其他操作。我们主要关注以下操作:

  • 在第一个节点插入

  • 搜索节点

  • 在特定节点之前插入

  • 在特定节点之后插入

  • 删除第一个节点

  • 删除最后一个节点

  • 搜索并删除一个节点

  • 反转链表

  • 获取第 N 个位置的元素

在第一个节点插入

当我们在前面或头部添加一个节点时,我们必须考虑两种简单的可能性。列表可能为空,因此新节点是头节点。这种可能性就是简单得不能再简单了。但是,如果列表已经有一个头节点,那么我们必须执行以下操作:

  1. 创建新节点。

  2. 将新节点作为第一个节点或头节点。

  3. 将前一个头或第一个节点分配为新创建的第一个节点的下一个跟随节点。

以下是此代码:

    public function insertAtFirst(string $data = NULL) { 

        $newNode = new ListNode($data); 

        if ($this->_firstNode === NULL) {             

            $this->_firstNode = &$newNode;             

        } else { 

            $currentFirstNode = $this->_firstNode; 

            $this->_firstNode = &$newNode; 

            $newNode->next = $currentFirstNode;            

        } 

        $this->_totalNode++; 

        return TRUE; 

    } 

搜索节点

搜索节点非常简单。我们需要遍历每个节点,并检查目标数据是否与节点数据匹配。如果找到数据,将返回节点;否则,返回FALSE。实现如下:

    public function search(string $data = NULL) { 

        if ($this->_totalNode) { 

            $currentNode = $this->_firstNode; 

            while ($currentNode !== NULL) { 

                if ($currentNode->data === $data) { 

                    return $currentNode; 

                } 

                $currentNode = $currentNode->next; 

            } 

        } 

        return FALSE; 

    } 

在特定节点之前插入

这个过程类似于我们查看的第一个操作。主要区别在于我们需要找到特定节点,然后在其之前插入一个新节点。当找到目标节点时,我们可以更改下一个节点,使其指向新创建的节点,然后更改新创建节点后面的节点,使其指向我们搜索的节点。如下图所示:

以下是实现前面展示的逻辑的代码:

public function insertBefore(string $data = NULL, string $query = NULL) { 

        $newNode = new ListNode($data); 

        if ($this->_firstNode) { 

            $previous = NULL; 

            $currentNode = $this->_firstNode; 

            while ($currentNode !== NULL) { 

                if ($currentNode->data === $query) { 

                    $newNode->next = $currentNode; 

                    $previous->next = $newNode; 

                    $this->_totalNode++; 

                    break; 

                } 

                $previous = $currentNode; 

                $currentNode = $currentNode->next; 

            } 

        } 

    } 

如果我们检查前面的代码,我们可以看到逻辑非常简单。在这个方法中有两个参数:一个是data,一个是query。我们遍历每个节点。在这样做的同时,我们还跟踪当前节点和前一个节点。跟踪前一个节点很重要,因为当找到目标节点时,我们将把前一个节点的下一个节点设置为新创建的节点。

在特定节点之后插入

这个过程类似于在目标节点之前插入一个节点。不同之处在于,我们需要在目标节点之后插入新节点。在这里,我们需要考虑目标节点以及它指向的下一个节点。当我们找到目标节点时,我们可以更改下一个节点,使其指向新创建的节点,然后我们可以更改紧随新创建节点的节点,使其指向目标节点之后的下一个节点。以下是用于实现此操作的代码:

    public function insertAfter(string $data = NULL, string $query = 

      NULL) { 

        $newNode = new ListNode($data); 

        if ($this->_firstNode) { 

            $nextNode = NULL; 

            $currentNode = $this->_firstNode; 

            while ($currentNode !== NULL) { 

                if ($currentNode->data === $query) { 

                    if($nextNode !== NULL) { 

                        $newNode->next = $nextNode; 

                    } 

                    $currentNode->next = $newNode; 

                    $this->_totalNode++; 

                    break; 

                } 

                $currentNode = $currentNode->next; 

                $nextNode = $currentNode->next; 

            } 

        } 

    } 

删除第一个节点

删除节点只是意味着取出节点并重新排列前一个和后续节点的链接。如果我们只是删除一个节点并将前一个节点的下一个链接与删除节点后面的节点连接起来,我们就完成了删除操作。请看以下示例:

当我们删除第一个节点时,我们只需将第二个节点作为我们的头节点或第一个节点。我们可以通过以下代码轻松实现这一点:

public function deleteFirst() { 

        if ($this->_firstNode !== NULL) { 

            if ($this->_firstNode->next !== NULL) { 

                $this->_firstNode = $this->_firstNode->next; 

            } else { 

                $this->_firstNode = NULL; 

            } 

            $this->_totalNode--; 

            return TRUE; 

        } 

        return FALSE; 

    } 

现在,我们必须考虑一个特殊情况,即将总节点数减少一个。

删除最后一个节点

删除最后一个节点将需要我们将倒数第二个节点的下一个链接指定为NULL。我们将迭代直到最后一个节点,并在迭代过程中跟踪前一个节点。一旦到达最后一个节点,下一个的前一个节点属性将被设置为NULL,如下例所示:

    public function deleteLast() { 

        if ($this->_firstNode !== NULL) { 

            $currentNode = $this->_firstNode; 

            if ($currentNode->next === NULL) { 

                $this->_firstNode = NULL; 

            } else { 

                $previousNode = NULL; 

                while ($currentNode->next !== NULL) { 

                    $previousNode = $currentNode; 

                    $currentNode = $currentNode->next; 

                } 

                $previousNode->next = NULL; 

                $this->_totalNode--; 

                return TRUE; 

            } 

        } 

        return FALSE; 

    } 

首先,我们检查列表是否为空。之后,我们检查列表是否有多于一个节点。根据答案,我们迭代到最后一个节点并跟踪前一个节点。然后,我们将前一个节点的下一个链接指定为NULL,以便从列表中省略最后一个节点。

搜索并删除节点

我们可以使用搜索和删除操作从列表中删除任何节点。首先,我们从列表中搜索节点,然后通过删除节点的引用来删除节点。以下是实现此操作的代码:

    public function delete(string $query = NULL) { 

        if ($this->_firstNode) { 

            $previous = NULL; 

            $currentNode = $this->_firstNode; 

            while ($currentNode !== NULL) { 

                if ($currentNode->data === $query) { 

                    if ($currentNode->next === NULL) { 

                        $previous->next = NULL; 

                    } else { 

                        $previous->next = $currentNode->next; 

                    } 

                    $this->_totalNode--; 

                    break; 

                } 

                $previous = $currentNode; 

                $currentNode = $currentNode->next; 

            } 

        } 

    } 

反转列表

有许多方法可以反转链表。我们将使用一种简单的方法来反转列表,即原地反转。我们遍历节点并将下一个节点更改为前一个节点,前一个节点更改为当前节点,当前节点更改为下一个节点。逻辑的伪算法如下所示:

prev   = NULL; 

current = first_node; 

next = NULL; 

while (current != NULL) 

{ 

  next  = current->next;   

  current->next = prev;    

  prev = current; 

  current = next; 

} 

first_node = prev; 

如果我们根据这个伪代码实现我们的反转函数,它将如下所示:

    public function reverse() { 

        if ($this->_firstNode !== NULL) { 

            if ($this->_firstNode->next !== NULL) { 

                $reversedList = NULL; 

                $next = NULL; 

                $currentNode = $this->_firstNode; 

                while ($currentNode !== NULL) { 

                    $next = $currentNode->next; 

                    $currentNode->next = $reversedList; 

                    $reversedList = $currentNode; 

                    $currentNode = $next; 

                } 

                $this->_firstNode = $reversedList; 

            } 

        } 

    } 

获取第 N 个位置的元素

由于列表与数组不同,直接从它们的位置获取元素并不容易。为了获取第 N 个位置的元素,我们必须迭代到该位置并获取元素。以下是此方法的代码示例:

    public function getNthNode(int $n = 0) { 

        $count = 1; 

        if ($this->_firstNode !== NULL) { 

            $currentNode = $this->_firstNode; 

            while ($currentNode !== NULL) { 

                if ($count === $n) { 

                    return $currentNode; 

                } 

                $count++; 

                $currentNode = $currentNode->next; 

            } 

        } 

    } 

我们现在已经为我们的LinkedList类编写了所有必需的操作。现在,让我们用不同的操作运行程序。如果我们运行以下程序,我们将大多数覆盖我们已经编写的所有操作:

$BookTitles = new LinkedList(); 

$BookTitles->insert("Introduction to Algorithm"); 

$BookTitles->insert("Introduction to PHP and Data structures"); 

$BookTitles->insert("Programming Intelligence"); 

$BookTitles->insertAtFirst("Mediawiki Administrative tutorial guide"); 

$BookTitles->insertBefore("Introduction to Calculus", "Programming Intelligence"); 

$BookTitles->insertAfter("Introduction to Calculus", "Programming Intelligence"); 

$BookTitles->display(); 

$BookTitles->deleteFirst(); 

$BookTitles->deleteLast(); 

$BookTitles->delete("Introduction to PHP and Data structures"); 

$BookTitles->reverse(); 

$BookTitles->display(); 

echo "2nd Item is: ".$BookTitles->getNthNode(2)->data; 

上述代码的输出将如下所示:

Total book titles: 6

Mediawiki Administrative tutorial guide

Introduction to Algorithm

Introduction to PHP and Data structures

Introduction to Calculus

Programming Intelligence

Introduction to Calculus

Total book titles: 3

Programming Intelligence

Introduction to Calculus

Introduction to Algorithm

2nd Item is: Introduction to Calculus

现在我们已经使用 PHP 7 完整实现了一个链表。到目前为止,我们已经了解到,与数组的实现不同,我们必须通过编写代码手动执行许多操作。我们还必须记住一件事:这不是我们实现链表的唯一方式。许多人更喜欢跟踪列表的第一个和最后一个节点,以获得更好的插入操作。现在,我们将查看链表操作在平均和最坏情况下的复杂性。

理解链表的复杂性

以下是链表操作的最佳、最坏和平均情况复杂性:

操作时间复杂度:最坏情况时间复杂度:平均情况
在开头或结尾插入O(1)O(1)
在开头或结尾删除O(1)O(1)
搜索O(n)O(n)
访问O(n)O(n)

我们可以通过跟踪最后一个节点来实现在链表末尾的O(1)插入复杂度,就像我们在示例中对第一个节点所做的那样。这将帮助我们直接跳转到链表的最后一个节点,而无需进行任何迭代。

将链表变成可迭代的

到目前为止,我们已经看到可以使用while循环在方法内部遍历链表的每个节点。如果我们需要从外部使用链表对象进行迭代,该怎么办?实现这一点是完全可能的。PHP 有一个非常直观的迭代器接口,允许任何外部迭代器在对象内部进行迭代。Iterator接口提供以下方法:

  • Current:返回当前元素

  • Next:向前移动到下一个元素

  • Key:返回当前元素的键

  • Rewind:将Iterator倒回到第一个元素

  • Valid:检查当前位置是否有效

现在,我们将在我们的LinkedList类中实现这些方法,使我们的对象可以直接通过节点进行迭代。为了在迭代期间跟踪当前节点和列表中的当前位置,我们需要为我们的LinkedList类添加两个新属性:

private $_currentNode = NULL; 

private $_currentPosition = 0; 

$_currentNode属性将在迭代期间跟踪当前节点,$_currentPosition将在迭代期间跟踪当前位置。我们还需要确保我们的LinkedList类也实现了Iterator接口。代码如下:

class LinkedList implements Iterator{ 

} 

现在,让我们实现这五个新方法,使我们的链表对象可迭代。这五个方法非常直接和简单。代码如下:

    public function current() { 

        return $this->_currentNode->data; 

    } 

    public function next() { 

        $this->_currentPosition++; 

        $this->_currentNode = $this->_currentNode->next; 

    } 

    public function key() { 

        return $this->_currentPosition; 

    } 

    public function rewind() { 

        $this->_currentPosition = 0; 

        $this->_currentNode = $this->_firstNode; 

    } 

    public function valid() { 

        return $this->_currentNode !== NULL; 

    } 

现在,我们有一个可迭代的列表。这意味着现在我们可以使用foreach循环或任何其他迭代过程来遍历我们的链表对象。因此,如果我们编写以下代码,我们将看到所有的书名:

foreach ($BookTitles as $title) { 

    echo $title . "\n"; 

}

另一种方法是使用可迭代接口的rewindvalidnextcurrent方法。它将产生与前面代码相同的输出:

for ($BookTitles->rewind(); $BookTitles->valid(); 

  $BookTitles->next()) { 

    echo $BookTitles->current() . "\n"; 

}

构建循环链表

构建循环链表并不像名字听起来那么难。到目前为止,我们已经看到在末尾添加新节点非常简单;我们将最后一个节点的下一个引用设置为NULL。在循环链表中,最后一个节点的下一个引用实际上将指向第一个节点,从而创建一个循环列表。让我们编写一个简单的循环链表,其中节点将被插入到列表的末尾:

class CircularLinkedList { 

    private $_firstNode = NULL; 

    private $_totalNode = 0; 

    public function insertAtEnd(string $data = NULL) { 

        $newNode = new ListNode($data); 

        if ($this->_firstNode === NULL) { 

            $this->_firstNode = &$newNode; 

        } else { 

            $currentNode = $this->_firstNode; 

            while ($currentNode->next !== $this->_firstNode) { 

                $currentNode = $currentNode->next; 

            } 

            $currentNode->next = $newNode; 

        } 

        $newNode->next = $this->_firstNode; 

        $this->_totalNode++; 

        return TRUE; 

    } 

}

如果我们仔细观察前面的代码,它看起来与我们的单向链表实现完全相同。唯一的区别是我们不检查列表的末尾,而是确保当前节点与第一个节点不同。此外,在以下行中,我们将新创建的节点的下一个引用分配给列表的第一个节点:

$newNode->next = $this->_firstNode; 

在我们实现这一点的过程中,新节点被添加到列表的末尾。我们所需要做的就是将新节点的下一个引用设置为列表中的第一个节点。通过这样做,我们实际上创建了一个循环链表。我们必须确保不会陷入无限循环。这就是为什么我们要比较$currentNode->next$this->_firstNode。当我们显示循环链表中的所有元素时,同样的原则也适用。我们需要确保在显示标题时不会陷入无限循环。以下是显示循环链表中所有标题的代码:

    public function display() { 

        echo "Total book titles: " . $this->_totalNode . "\n"; 

        $currentNode = $this->_firstNode; 

        while ($currentNode->next !== $this->_firstNode) { 

            echo $currentNode->data . "\n"; 

            $currentNode = $currentNode->next; 

        } 

        if ($currentNode) { 

            echo $currentNode->data . "\n"; 

        } 

    }

到目前为止,我们已经构建了一个单向链表并实现了一个循环链表。现在,我们将使用 PHP 实现一个双向链表。

在 PHP 中实现双向链表

我们已经从双向链表的定义中知道,双向链表节点将有两个链接:一个指向下一个节点,另一个指向前一个节点。此外,当我们添加新节点或删除新节点时,我们需要为每个受影响的节点设置下一个和上一个引用。我们在单向链表实现中看到了一种不同的方法,我们没有跟踪最后一个节点,因此,我们每次都必须使用迭代器来到达最后一个节点。这一次,我们将跟踪最后一个节点,以及我们的插入和删除操作,以确保我们的插入、删除和结束操作具有O(1)复杂度。

以下是新节点类的外观,具有两个链接指针,后面是我们双向链表类的基本结构:

class ListNode {

    public $data = NULL; 

    public $next = NULL; 

    public $prev = NULL; 

    public function __construct(string $data = NULL) {

        $this->data = $data;

    }

}

class DoublyLinkedList {

    private $_firstNode = NULL;

    private $_lastNode = NULL;

    private $_totalNode = 0;

}

在下一节中,我们将探讨双向链表的不同操作,以便我们了解单向链表和双向链表之间的基本区别。

双向链表操作

我们将在双向链表实现中探讨以下操作。虽然它们听起来与单向链表中使用的操作类似,但它们在实现上有一个重大区别:

  • 在第一个节点插入

  • 在最后一个节点插入

  • 在特定节点之前插入

  • 在特定节点之后插入

  • 删除第一个节点

  • 删除最后一个节点

  • 搜索并删除一个节点

  • 向前显示列表

  • 向后显示列表

首先插入节点

当我们在前面或头部添加节点时,我们必须检查列表是否为空。如果列表为空,则第一个和最后一个节点将指向新创建的节点。但是,如果列表已经有一个头,则我们必须执行以下操作:

  1. 创建新节点。

  2. 将新节点作为第一个节点或头。

  3. 将前一个头或第一个节点作为下一个,以跟随新创建的第一个节点。

  4. 将前一个第一个节点的前链接指向新的第一个节点。

以下是代码:

    public function insertAtFirst(string $data = NULL) {

        $newNode = new ListNode($data);

        if ($this->_firstNode === NULL) {

            $this->_firstNode = &$newNode;

            $this->_lastNode = $newNode; 

        } else {

            $currentFirstNode = $this->_firstNode; 

            $this->_firstNode = &$newNode; 

            $newNode->next = $currentFirstNode; 

            $currentFirstNode->prev = $newNode; 

        }

        $this->_totalNode++; 

        return TRUE;

    }

在最后一个节点插入

由于我们现在正在跟踪最后一个节点,因此在末尾插入新节点将更容易。首先,我们需要检查列表是否为空。如果为空,则新节点成为第一个和最后一个节点。但是,如果列表已经有最后一个节点,那么我们必须执行以下操作:

  1. 创建新节点。

  2. 将新节点作为最后一个节点。

  3. 将前一个最后一个节点作为当前最后一个节点的前链接。

  4. 将前一个最后一个节点的下一个链接指向新的最后一个节点的前链接。

以下是代码:

    public function insertAtLast(string $data = NULL) { 

        $newNode = new ListNode($data);

        if ($this->_firstNode === NULL) {

            $this->_firstNode = &$newNode; 

            $this->_lastNode = $newNode; 

        } else {

            $currentNode = $this->_lastNode; 

            $currentNode->next = $newNode; 

            $newNode->prev = $currentNode; 

            $this->_lastNode = $newNode; 

        }

        $this->_totalNode++; 

        return TRUE;

    }

在特定节点之前插入

在特定节点之前插入需要我们先找到节点,然后根据其位置,我们需要改变新节点、目标节点和目标节点之前的节点的下一个和上一个节点,如下所示:

    public function insertBefore(string $data = NULL, string $query =  

      NULL) {

        $newNode = new ListNode($data); 

        if ($this->_firstNode) { 

            $previous = NULL; 

            $currentNode = $this->_firstNode; 

            while ($currentNode !== NULL) { 

                if ($currentNode->data === $query) { 

                    $newNode->next = $currentNode; 

                    $currentNode->prev = $newNode; 

                    $previous->next = $newNode; 

                    $newNode->prev = $previous; 

                    $this->_totalNode++; 

                    break; 

                }

                $previous = $currentNode; 

                $currentNode = $currentNode->next; 

            }

        }

    }

在特定节点之后插入

在特定节点之后插入类似于我们刚刚讨论的方法。在这里,我们需要改变新节点、目标节点和目标节点后面的节点的下一个和上一个节点。以下是代码:

    public function insertAfter(string $data = NULL, string $query = 

      NULL) { 

        $newNode = new ListNode($data);

        if ($this->_firstNode) { 

            $nextNode = NULL; 

            $currentNode = $this->_firstNode; 

            while ($currentNode !== NULL) { 

                if ($currentNode->data === $query) { 

                    if ($nextNode !== NULL) { 

                        $newNode->next = $nextNode; 

                    } 

                    if ($currentNode === $this->_lastNode) { 

                        $this->_lastNode = $newNode; 

                    } 

                    $currentNode->next = $newNode; 

                    $nextNode->prev = $newNode; 

                    $newNode->prev = $currentNode; 

                    $this->_totalNode++; 

                    break; 

                } 

                $currentNode = $currentNode->next; 

                $nextNode = $currentNode->next; 

            }

        }

    }

删除第一个节点

当我们从双向链表中删除第一个节点时,我们只需要将第二个节点设置为第一个节点。将新的第一个节点的前一个节点设置为NULL,并减少总节点数,就像以下代码一样:

    public function deleteFirst() { 

        if ($this->_firstNode !== NULL) { 

            if ($this->_firstNode->next !== NULL) { 

                $this->_firstNode = $this->_firstNode->next; 

                $this->_firstNode->prev = NULL; 

            } else { 

                $this->_firstNode = NULL; 

            } 

            $this->_totalNode--; 

            return TRUE; 

        } 

        return FALSE; 

    }

删除最后一个节点

删除最后一个节点需要我们将倒数第二个节点设置为新的最后一个节点。此外,新创建的最后一个节点不应该有任何下一个引用。代码示例如下:

    public function deleteLast() { 

        if ($this->_lastNode !== NULL) { 

            $currentNode = $this->_lastNode; 

            if ($currentNode->prev === NULL) { 

                $this->_firstNode = NULL; 

                $this->_lastNode = NULL; 

            } else { 

                $previousNode = $currentNode->prev; 

                $this->_lastNode = $previousNode; 

                $previousNode->next = NULL; 

                $this->_totalNode--; 

                return TRUE; 

            } 

        } 

        return FALSE; 

    }

搜索并删除一个节点

当我们从列表中间删除一个节点时,我们必须重新调整目标节点的前一个节点和后一个节点。首先,我们会找到目标节点。获取目标节点的前一个节点以及下一个节点。然后,将前一个节点后面的节点指向目标节点后面的节点,前一个节点同样也是如此。以下是代码:

    public function delete(string $query = NULL) { 

        if ($this->_firstNode) { 

            $previous = NULL;

            $currentNode = $this->_firstNode; 

            while ($currentNode !== NULL) { 

                if ($currentNode->data === $query) { 

                    if ($currentNode->next === NULL) { 

                        $previous->next = NULL; 

                    } else { 

                        $previous->next = $currentNode->next; 

                        $currentNode->next->prev = $previous; 

                    }

                    $this->_totalNode--; 

                    break; 

                }

                $previous = $currentNode; 

                $currentNode = $currentNode->next; 

            }

        }

    }

显示列表向前

双向链表使我们有机会以两个方向显示列表。到目前为止,我们已经看到在单向链表中工作时可以以单向方式显示列表。现在,我们将从两个方向查看列表。以下是用于显示列表向前的代码:

    public function displayForward() { 

        echo "Total book titles: " . $this->_totalNode . "\n"; 

        $currentNode = $this->_firstNode; 

        while ($currentNode !== NULL) { 

            echo $currentNode->data . "\n"; 

            $currentNode = $currentNode->next; 

        } 

    } 

显示列表向后

要显示列表向后,我们必须从最后一个节点开始,继续使用前一个链接向后移动,直到到达列表的末尾。这为我们在操作期间以任何方向移动提供了一种独特的方式。以下是代码:

    public function displayBackward() { 

        echo "Total book titles: " . $this->_totalNode . "\n"; 

        $currentNode = $this->_lastNode; 

        while ($currentNode !== NULL) { 

            echo $currentNode->data . "\n"; 

            $currentNode = $currentNode->prev; 

        }

    }

双向链表的复杂性

以下是双向链表操作的最佳、最坏和平均情况复杂性。与单向链表操作类似:

操作时间复杂度:最坏情况时间复杂度:平均情况
在开头或结尾插入O(1)O(1)
删除开头或结尾O(1)O(1)
搜索O(n)O(n)
访问O(n)O(n)

使用 PHP SplDoublyLinkedList

PHP 标准 PHP 库SPL)有一个双向链表的实现,称为SplDoublyLinkedList。如果我们使用内置类,就不需要自己实现双向链表。双向链表的实现实际上也可以作为堆栈和队列。PHP 实现的双向链表有许多额外的功能。以下是SplDoublyLinkedList的一些常见特性:

方法描述
Add在指定索引处添加一个新节点
Bottom从列表开头窥视一个节点
Count返回列表的大小
Current返回当前节点
getIteratorMode返回迭代模式
setIteratorMode设置迭代模式。例如,LIFO,FIFO 等
Key返回当前节点索引
next移动到下一个节点
pop从列表末尾弹出一个节点
prev移动到前一个节点
push在列表末尾添加一个新节点
rewind将迭代器倒回顶部
shift从链表开头移除一个节点
top从列表末尾窥视一个节点
unshift在列表中添加一个元素
valid检查列表中是否还有节点

现在,让我们使用SplDoublyLinkedList为我们的书名应用程序编写一个小程序:

$BookTitles = new SplDoublyLinkedList(); 

$BookTitles->push("Introduction to Algorithm");

$BookTitles->push("Introduction to PHP and Data structures"); 

$BookTitles->push("Programming Intelligence");

$BookTitles->push("Mediawiki Administrative tutorial guide"); 

$BookTitles->add(1,"Introduction to Calculus");

$BookTitles->add(3,"Introduction to Graph Theory");

for($BookTitles->rewind();$BookTitles->valid();$BookTitles->next()){    

    echo $BookTitles->current()."\n";

}

前面的代码将产生以下输出:

Introduction to Algorithm

Introduction to Calculus

Introduction to PHP and Data structures

Introduction to Graph Theory

Programming Intelligence

Mediawiki Administrative tutorial guide

摘要

链表是最流行的数据结构之一,用于解决不同的问题。无论是关于堆栈、队列、优先队列,还是实现复杂的图算法,链表都是一个非常方便的数据结构,可以解决你可能遇到的任何问题。在本章中,我们探讨了关于单向链表、双向链表和循环链表的所有可能细节,以及它们的复杂性分析。在接下来的章节中,我们将利用链表来实现不同的数据结构和编写算法。