28、Perl高级编程:引用、多维数组与哈希引用

Perl高级编程:引用、多维数组与哈希引用

1. 引用与二维数组

在编程中,我们常常会遇到需要处理复杂数据结构的情况。在Perl里,引用和二维数组就是处理这类数据的重要工具。

1.1 一维数组回顾

到目前为止,我们使用的数组大多是一维的。例如:

my @array = ('Mouse', 'Mus musculus', 'Rodent');

这个数组按线性顺序存储了三个值。如果把这些数据放到电子表格中,可以将它们放在同一行或同一列。

Mouse Mus musculus Rodent

但如果要将整个电子表格的内容存储在数组中,该如何表示像下面这样的数据呢?

Mouse Mus musculus Rodent
Human Homo sapiens Primate
Cow Bos taurus Ungulate

一种自然的表示方式是使用二维数组。不过,在介绍如何创建二维数组之前,我们需要先了解Perl中的引用。

1.2 引用的概念

引用可以类比为Windows系统中的快捷方式或Mac系统中的别名。它们就像可点击的“书签”,指向经常访问的应用程序或文件。创建快捷方式或别名时,会得到一个模仿原始文件的图标,但不会复制文件本身。可以为单个应用程序创建多个快捷方式或别名,并将它们放在文件系统的不同文件夹中。打开文档的快捷方式或别名时,可以进行一些编辑,更改会反映在原始文件中。但删除快捷方式或别名不会删除原始文件。Perl中的引用与快捷方式或别名的行为非常相似,虽然这个类比并不完美,但它是理解引用的一个有用起点。

1.3 数组引用

下面是一个简单的数组赋值:

my @author = ('Keith', 'Ian');

如果要创建这个数组的引用,需要使用反斜杠运算符:

my $author_ref = \@author;

这里创建了 $author_ref ,它是一个标量变量,“指向” @author 数组。如果没有反斜杠字符,只是将数组赋值给一个列表, $author_ref 最终会包含 @author 的大小。因此,反斜杠字符对于使 $author_ref 成为引用至关重要。

需要注意的是, $author_ref 仍然是一个标量,因为它只保存一个东西的内容,但它指向的东西可以是一个包含多个元素的数组。如果尝试打印 $author_ref 的值,会得到一个奇怪的字符串,如 ARRAY(0x100800f40) ,这表明该变量是一个数组引用,括号中的十六进制数字是其内存地址。

Perl的 ref() 函数可以用来判断一个标量变量是否是引用(与普通标量变量相对)。在本章中,我们只关注数组引用。如果对不是引用的东西使用 ref() ,它会返回假。

print ref($author_ref); # 输出 ARRAY
print ref("something"); # 无输出
1.4 使用数组引用

数组引用的使用方式与数组非常相似,但语法略有不同。由于 $author_ref 是一个标量变量,不能直接使用 $author_ref[0] ,而需要使用箭头运算符 ->

print $author_ref->[0]; # 输出 'Keith'

可以将其理解为“打印由名为 $author_ref 的标量指向的数组的第零个元素”。 $author[0] $author_ref->[0] 在计算机内存中占据完全相同的位置。如果更改其中一个,另一个也会随之更改:

$author_ref->[0] = 'Keith Bradnam';
print $author[0]; # 现在会输出 'Keith Bradnam'

相反,如果将数组引用设置为未定义或转换为普通标量变量,不会影响它以前指向的数组:

$author_ref = undef; # 使引用未定义
print $author[0];  # 仍然会输出 'Keith'
$author_ref = "cheese"; # 可以将变量重新用作普通标量

箭头运算符 -> 可以用来解引用数组的单个元素。但有时需要解引用整个数组,例如在排序时。要解引用整个数组,可以在标量前加上数组符号,或者在包含标量的块前使用数组符号。以下三个语句是等价的:

push @author,  'Nigel';
push @$author_ref, 'Nigel';
push @{$author_ref}, 'Nigel';

这里唯一的区别是,第一个示例是将字符串添加到数组中,而后两个示例是将字符串添加到解引用的数组中,但修改的始终是同一个数组。

刚开始可能需要一些时间来适应 @ $ 符号的组合。随着经验的积累,看到这样的代码时,会本能地意识到正在处理整个数组的引用。对数组执行的任何操作都可以同样轻松地对解引用的数组执行。例如,遍历数组引用的元素非常简单:

foreach my $author (@$author_ref) {
    print "$author\n";
}
1.5 匿名数组构造器

在前面的示例中, $author_ref @author 数组的引用。当在需要创建引用之前已经创建了数组时,这种创建引用的方式是有意义的。但如果想在创建引用的同时创建数组,可以使用匿名数组构造器。它的名字听起来很复杂,但实际上只是用方括号代替了表示数组的圆括号:

my $author_ref = ['Keith', 'Ian'];

之所以称其为“匿名”,是因为数组没有名字。数组引用有名字( $author_ref ),但它指向的数组没有名字。以下两种方法的效果是相同的:

命名数组 匿名数组
perl<br>my @array = ('Keith', 'Ian');<br>my $array_ref = \@array;<br>print $array_ref->[0];<br> perl<br>my $array_ref = ['Keith', 'Ian'];<br>print $array_ref->[0];<br>

讨论引用和匿名数组构造器的原因是,它们是创建多维数组的关键。如果只需要处理一维数据,使用匿名数组可能可以,但使用普通数组可能更有意义。

1.6 二维数组

现在,让我们来创建一个二维数组。以下是要表示的数据:

Mouse Mus musculus Rodent
Human Homo sapiens Primate

可以为每一行创建普通数组:

my @row0 = ('Mouse', 'Mus musculus', 'Rodent');
my @row1 = ('Human', 'Homo sapiens', 'Primate');

然后将这两个一维数组放入另一个数组中,使其成为二维数组。有四种不同的方法可以实现这一点。

方法一:使用数组引用

my $row_ref0 = \@row0;
my $row_ref1 = \@row1;
my @table = (
    $row_ref0,
    $row_ref1,
);

现在有了一个包含两个元素的数组,每个元素都是对另一个数组的引用。值得停下来思考一下我们刚刚创建的数据结构。 @table 是一个普通数组,可以像使用其他数组一样使用它。唯一的区别是,它包含的不是普通标量,而是对其他数组的引用。需要注意的是,在解引用任何数组元素的内容之前,它仍然是一个一维数组。只有在那个时候,才会处理二维数据结构。

方法二:直接添加数组引用

my @table = (
    \@row0,
    \@row1,
);

除非有特定需求需要为特定行创建命名引用,否则这是一种更直接的创建二维表的方法。

方法三:使用匿名数组构造器和命名变量

my $row0_ref = ['Mouse', 'Mus musculus', 'Rodent'];
my $row1_ref = ['Human', 'Homo sapiens', 'Primate'];
my @table = (
    $row0_ref,
    $row1_ref,
);

最终结果与之前相同,但现在避免了创建数组来保存每一行的数据。然而,这又回到了创建可能不再需要使用的命名数组引用的情况。

方法四:只命名二维数组

my @table = (
    ['Mouse', 'Mus musculus', 'Rodent'],
    ['Human', 'Homo sapiens', 'Primate'],
);

这是一种更简单、更整洁的创建二维数组的方法。注意,方括号外的逗号表示第一维的每个元素,方括号内的逗号表示第二维的元素。可以使用 $table[1] 访问整行,它包含一个数组引用。如果打印 $table[1] ,会得到一个奇怪的字符串,如 ARRAY(0x1008000c0) ,因为它是一个数组引用。要访问单个单元格,可以使用箭头运算符 ->

print $table[1]->[1]; # 输出 Homo sapiens

在这种情况下,箭头是可选的, $table[1][1] 看起来更好。如果要打印表中第一个“单元格”的内容,可以使用以下两种方法之一:

print $table[0]->[0];
print $table[0][0];

可以将这两行代码理解为“打印由 @table 数组的第零个元素中的引用指向的数组的第零个元素”。

下面是一个完整的示例脚本:

#!/usr/bin/perl
# zoo.pl
use strict;
use warnings;

my @zoo = (
    ["Mouse", "Mus musculus", "Rodent"],
    ["Human", "Homo sapiens", "Primate"],
    ["Cow", "Bos taurus", "Ungulate"],
);

print "$zoo[1][2]\n"; # 输出 'Primate'

for (my $i = 0; $i < @zoo; $i++) {
    for (my $j = 0; $j < @{$zoo[$i]}; $j++) {
        print "$zoo[$i][$j]\n";
    }
}

foreach my $row (@zoo) {
    foreach my $column (@$row) {
        print "$column\n";
    }
}

这个脚本的执行流程如下:

graph TD;
    A[开始] --> B[定义二维数组 @zoo];
    B --> C[打印 @zoo[1][2]];
    C --> D[使用嵌套 for 循环遍历 @zoo];
    D --> E[打印每个元素];
    E --> F[使用嵌套 foreach 循环遍历 @zoo];
    F --> G[打印每个元素];
    G --> H[结束];

在编写这个脚本时,由于它比大多数脚本长,建议分部分编写,编写一部分后检查是否能运行,然后再继续编写。例如,先编写第1 - 9行,然后到第11行,接着到第17行,最后到第23行。编写第14行时,确保使用 @{$zoo[$i]} 而不是 @$zoo[$i] ,因为 @$zoo 的绑定比 $zoo[$i] 更紧密,Perl会认为 $zoo 是一个标量变量。为了让Perl识别数组,必须将其放在块 {$zoo[$i]} 中。

1.7 从文件中读取二维数组

如果有大量数据,将所有内容作为Perl代码的一部分写出来会很繁琐,而且可能会在输入时出错。因此,从文件中读取数据要好得多。即使在互联网时代,基于逗号分隔值(CSV)和制表符分隔值(TSV)格式的纯文本文件仍然非常常见。下面是一个从CSV文件读取数据的示例:

首先,创建一个名为 species.csv 的文件,内容如下:

Mouse,Mus musculus,Rodent
Human,Homo sapiens,Primate
Cow,Bos taurus,Ungulate

然后创建以下程序:

#!/usr/bin/perl
# zoo_reader.pl
use strict;
use warnings;

my @table;
while (<>) {
    my ($common, $scientific, $family) = split(/,/, $_);
    push @table, [$common, $scientific, $family];
}

这个脚本的执行流程如下:

graph TD;
    A[开始] --> B[定义空数组 @table];
    B --> C[使用 while 循环读取文件];
    C --> D[使用 split 函数分割每行数据];
    D --> E[将分割后的数据放入匿名数组并添加到 @table];
    E --> F[结束];

这个脚本的 while 循环从文件操作符 <> 指定的文件中读取数据,该文件可以是命令行指定的任何文件,甚至是标准输入。第7行将文件中的每行输入分割成三个变量,下一行将这三个变量添加到一个匿名数组中,然后将该匿名数组作为单个元素添加到 @table 数组的末尾。

然而,这个脚本存在一些问题,它硬编码为只处理输入文件中的三列数据,并且选择的变量名暗示了特定类型的数据。为了使脚本更通用,应该能够处理任意数量的列,可以修改第7行和第8行:

my @array = split(/,/, $_);
push @table, \@array;

现在,CSV文件的每一行都被分割成一个数组。如果有100列,数组将有100个元素,然后将其添加到 @table 中,形成一个二维数组。

要从使用CSV文件改为使用TSV文件,只需要编辑 split() 函数中的模式:

my @array = split(/\t/, $_);

为了更具通用性,可以将 split 中的字符串改为一个变量,该变量可以在程序的其他地方定义,甚至更好的是,由用户作为命令行参数定义:

my @array = split(/$separator/, $_);

再看一下 Example 6.3.2 中的代码,第8行可能看起来有些奇怪。如果担心 @array 在每次 while 循环迭代时都会被删除,说明思考得很深入。词法变量通常在花括号内创建和销毁,但 @array 虽然每次循环都会创建,但由于它仍然有引用,其对应的内存不会被销毁,关于这个有点令人困惑的话题,会在垃圾回收的子部分进行讨论。

2. 记录和其他哈希引用

哈希引用在Perl编程中也扮演着重要角色,它能让我们更高效地处理复杂的数据结构。

2.1 哈希引用和匿名哈希构造器

之前介绍了数组引用,现在来看如何创建哈希引用。哈希引用的工作方式与数组引用非常相似,创建哈希引用同样要使用反斜杠运算符。下面是一个简单的哈希创建及引用示例:

my %sound = (dog => 'woof', cat => 'meow');
my $sound_ref = \%sound;

可以看到,创建哈希引用的语法和创建数组引用的语法完全相同。此外,还有匿名哈希构造器,它允许我们在不命名哈希的情况下创建哈希引用,语法上只需用花括号代替圆括号:

my $sound_ref = {dog => 'woof', cat => 'meow'};

这会创建一个没有名字的哈希,由变量 $sound_ref 引用它。要解引用哈希的单个元素,需要使用 -> 运算符:

print $sound_ref->{dog}; # 输出 'woof'

可以理解为“打印由 $sound_ref 引用所指向的哈希中键为 dog 的值”。要解引用整个哈希,在标量前加上 % 符号,这样就可以在任何能使用哈希的地方使用解引用的哈希,有时也需要使用块表示法:

my @animals = keys %{$sound_ref};

下面通过一个脚本测试这些哈希引用的概念:

#!/usr/bin/perl
# hash_ref.pl
use strict;
use warnings;

my $sound = {dog => 'woof', cat => 'meow'};
$sound->{cow} = 'moo';
foreach my $animal (keys %$sound) {
    print "$animal says $sound->{$animal}\n";
}

这个脚本的执行流程如下:

graph TD;
    A[开始] --> B[创建匿名哈希引用 $sound];
    B --> C[向 $sound 中添加键值对];
    C --> D[遍历 $sound 的键];
    D --> E[打印每个键值对];
    E --> F[结束];

脚本中第5行创建了一个匿名哈希的引用,第6行向哈希中添加了另一个键值对,最后第7 - 9行遍历整个哈希,显示键值对。

2.2 哈希引用作为数据库记录

哈希引用最常见的用途之一是表示数据库中的记录。例如,有一个简单的地址簿,地址簿中的每张“卡片”包含一个人的姓名和电子邮件属性,一张特定的卡片可以这样表示:

my $card = {
    name => 'Ian',
    email => 'atagcgaat@gmail.com'
};

一个地址簿可能包含多张卡片,当然要用数组来保存多个卡片。哈希引用数组是一种二维数据结构,第一维包含卡片,第二维包含每张卡片上的属性。要将哈希引用添加到数组中,只需使用 push 操作:

my @address_book;
push @address_book, $card;

这里 $card 是一个指向匿名哈希的引用,由于它表现得像一个普通标量变量,所以可以作为单个项添加到数组中。 @address_book 是一个普通数组,只是它的元素是哈希引用。

也可以不创建哈希的命名引用,直接将 @address_book 数组的元素定义为哈希引用:

my @address_book = (
    {name => 'Ian', email => 'atagcgaat@gmail.com'},
    {name => 'Keith', email => 'whykeith@me.com'}
);

下面是一个实际应用的示例脚本:

#!/usr/bin/perl
# address_book.pl
use strict;
use warnings;

my @address_book = (
    {name => 'Ian',  email => 'atagcgaat@gmail.com'},
    {name => 'Keith', email => 'whykeith@me.com'}
);

foreach my $card (@address_book) {
    print "$card->{name} $card->{email}\n";
}

这个脚本的执行流程如下:

graph TD;
    A[开始] --> B[定义地址簿数组 @address_book];
    B --> C[遍历 @address_book 中的每个卡片];
    C --> D[打印每个卡片的姓名和电子邮件];
    D --> E[结束];

脚本中第5 - 8行创建了一个包含两张卡片的地址簿,注意使用空格对齐属性,使代码更易读。第10 - 12行展示了如何遍历数组中的所有哈希引用, $card 是一个临时变量,代表 @address_book 中的每个元素,每个元素都是一个哈希引用。

2.3 从文件中读取记录

之前我们将CSV数据读入仅使用数组的二维数据结构中,例如:

Mouse,Mus musculus,Rodent
Human,Homo sapiens,Primate
Cow,Bos taurus,Ungulate

但二维数组并不是描述这类数据的最佳方式,哈希可以为属性命名,而不是使用编号。比较下面两种方式,哪种更具描述性一目了然:

$animal->[0];
$animal->{common_name};

下面将数据读入哈希数组,并使用 while 循环遍历数据结构:

#!/usr/bin/perl
# zoo_reader2.pl
use strict;
use warnings;

my @database;
while (<>) {
    my ($com, $sci, $fam) = split(/,/, $_);
    push @database, {
        common_name => $com,
        scientific => $sci,
        family  => $fam
    };
}

foreach my $animal (@database) {
    print "$animal->{common_name}\n";
}

这个脚本的执行流程如下:

graph TD;
    A[开始] --> B[定义空数组 @database];
    B --> C[使用 while 循环读取文件];
    C --> D[使用 split 函数分割每行数据];
    D --> E[将分割后的数据转换为匿名哈希并添加到 @database];
    E --> F[遍历 @database 中的每个动物];
    F --> G[打印每个动物的通用名称];
    G --> H[结束];

脚本中第5 - 12行从命令行指定的CSV文件中逐行读取数据,第7 - 11行将数据转换为匿名哈希,并将哈希引用添加到 @database 数组中。注意第8 - 10行的键值对相互对齐,每个属性占一行,这是一种常见的编程习惯,使逻辑更清晰。第14 - 16行遍历哈希数组,只打印每个动物的通用名称。

2.4 哈希的哈希和哈希的数组

前面探讨了二维数组和哈希数组,在这两种情况中,第一维都是数组。其实也可以将其改为哈希,只需为元素分配名称而不是编号,这样做的一个原因是可以加快搜索速度。

以地址簿为例,假设地址簿中有很多“卡片”,并且已经有代码从文件中检索这些数据:

my @address_book = read_address_book("some_file");

假设 @address_book 包含数百万条记录,使用以下结构搜索单个记录会比较耗时:

foreach my $card (@address_book) {
    if ($card->{name} eq 'Ian') {
        do_something($card);
    }
}

使用未排序的列表意味着平均需要搜索列表的一半才能找到匹配项。如果将姓名放在哈希中,检索速度会快很多。例如,按姓名索引地址簿,可以使用以下代码:

my %index;
foreach my $card (@address_book) {
    $index{$card->{name}} = $card;
}

这段代码创建了一个新的哈希 %index ,哈希的每个键是哈希数组中的“姓名”字段,与每个键关联的值是指向该人所有信息数组的引用。如果觉得这个语法难以理解,记住Perl通常是从内到外工作的。上述代码中,Perl首先解析 $card->{name} 部分,得到一个字符串,然后将其分配给新哈希 %index 的键,最后将该键与一个值关联。有了这个哈希,只需简单查找就能检索任何卡片:

my $card = $index{'Ian'};

这种方法假设姓名是唯一的,但实际数据可能并非如此。如果存在重复姓名,需要为每个姓名存储一个卡片数组:

foreach my $card (@address_book) {
    push @{ $index{$card->{name}} }, $card;
}

此时 %index 是一个三维数据结构,第一维是一个哈希,键是姓名,关联的值是数组的引用;第二维是与每个姓名关联的卡片数组;第三维是卡片上的属性。为了更清楚地说明维度,假设要获取姓名为“Ian”的第一张卡片的电子邮件地址,可以这样做:

print $index{Ian}[0]{email};

通过以上对引用、二维数组和哈希引用的介绍,我们可以看到Perl在处理复杂数据结构方面的强大能力,合理运用这些特性可以让代码更加简洁、高效。

基于径向基函数神经网络RBFNN的自适应滑模控制学习(Matlab代码实现)内容概要:本文介绍了基于径向基函数神经网络(RBFNN)的自适应滑模控制方法,并提供了相应的Matlab代码实现。该方法结合了RBF神经网络的非线性逼近能力和滑模控制的强鲁棒性,用于解决复杂系统的控制问题,尤其适用于存在不确定性和外部干扰的动态系统。文中详细阐述了控制算法的设计思路、RBFNN的结构权重更新机制、滑模面的构建以及自适应律的推导过程,并通过Matlab仿真验证了所提方法的有效性和稳定性。此外,文档还列举了大量相关的科研方向和技术应用,涵盖智能优化算法、机器学习、电力系统、路径规划等多个领域,展示了该技术的广泛应用前景。; 适合人群:具备一定自动控制理论基础和Matlab编程能力的研究生、科研人员及工程技术人员,特别是从事智能控制、非线性系统控制及相关领域的研究人员; 使用场景及目标:①学习和掌握RBF神经网络滑模控制相结合的自适应控制策略设计方法;②应用于电机控制、机器人轨迹跟踪、电力电子系统等存在模型不确定性或外界扰动的实际控制系统中,提升控制精度鲁棒性; 阅读建议:建议读者结合提供的Matlab代码进行仿真实践,深入理解算法实现细节,同时可参考文中提及的相关技术方向拓展研究思路,注重理论分析仿真验证相结合。
先展示下效果 https://pan.quark.cn/s/a4b39357ea24 本项目是本人参加BAT等其他公司电话、现场面试之后总结出来的针对Java面试的知识点或真题,每个点或题目都是在面试中被问过的。 除开知识点,一定要准备好以下套路: 个人介绍,需要准备一个1分钟的介绍,包括学习经历、工作经历、项目经历、个人优势、一句话总结。 一定要自己背得滚瓜烂熟,张口就来 抽象概念,当面试官问你是如何理解多线程的时候,你要知道从定义、来源、实现、问题、优化、应用方面系统性地回答 项目强化,至少知识点的比例是五五开,所以必须针对简历中的两个以上的项目,形成包括【架构和实现细节】,【正常流程和异常流程的处理】,【难点+坑+复盘优化】三位一体的组合拳 压力练习,面试的时候难免紧张,可能会严重影响发挥,通过平时多找机会参交流分享,或找人做压力面试来改善 表达练习,表达能力非常影响在面试中的表现,能否简练地将答案告诉面试官,可以通过给自己讲解的方式刻意练习 重点针对,面试官会针对简历提问,所以请针对简历上写的所有技术点进行重点准备 Java基础 JVM原理 集合 多线程 IO 问题排查 Web框架、数据库 Spring MySQL Redis 通用基础 操作系统 网络通信协议 排序算法 常用设计模式 从URL到看到网页的过程 分布式 CAP理论 锁 事务 消息队列 协调器 ID生成方式 一致性hash 限流 微服务 微服务介绍 服务发现 API网关 服务容错保护 服务配置中心 算法 数组-快速排序-第k大个数 数组-对撞指针-最大蓄水 数组-滑动窗口-最小连续子数组 数组-归并排序-合并有序数组 数组-顺时针打印矩形 数组-24点游戏 链表-链表反转-链表相加 链表-...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值