杂凑(Hash)(又译散列)

0 views
Skip to first unread message

虎^_^

unread,
Jun 6, 2005, 7:33:42 AM6/6/05
to 金中讨论组
5. 杂凑(Hash)(又译散列)
杂凑对一般使用者大概都非常不熟悉,尤其是没接触过Perl的人来说,杂凑对他们来说都是全新名词。但是在现实生活中,杂凑却是不断出现在一般人的生活之中。因此只要搞懂杂凑到底在讲甚么,你就会觉得这个东西用起来真是自然极了,而且没有了杂凑还可能让很多事情显得不知所措,因为你要花大量的时间跟精力才能利用其他数据结构做出杂凑所达到的结果。
听起来杂凑确实非常吸引人,那我们先来了解一下甚么是杂凑。所谓的杂凑,其实用最简单的话来说,也就是一对键值(key-value),没错,就是这么简单,一个键搭配着一个值的对应方式。当然,你可以搭配Perl所提供复杂的方式来建立多层的杂凑来符合程序的需要,不过那不是基本的杂凑,而且所谓复杂的结构,还是依循着最简单的原理,也就是键跟值的相对应关係。
5.1 日常生活的杂凑
没错,如果我们只说杂凑是一对键值的组合,那要让人真正理解显然并不容易。所以如果我们可以使用一般人常用的词汇来解释杂凑这个东西,显然应该会容易许多。既然如此,我们就来看看大家每天接触的资料中,有甚么是能够以杂凑精确的表现出来的。
最简单的例子应该算是身份证字号了吧,我们可以很容易的用身份证字号知道一个人的姓名,其中的身份证字号就是杂凑的键(key),而利用这个键所得到的值(value)就是姓名。而且键是这个杂凑中唯一的值,也就是一个杂凑中,不能有重复的键。这也应该很明显,如果有两个一模一样的身份证字号,那么我们要怎么确认使用者希望找到的是那一个呢?所以这也是杂凑中的限制,我们必须要求杂凑中的键值必须是不重复的,很显然,这样的限制是非常合理的。另外,我们每个人的行动电话中也藏着使用杂凑的好素材。如果你曾经使用行动电话的电话簿功能,那么你也许每天都会接触这种非常杂凑式的结构,因为电话簿功能也是杂凑足以发挥功用的地方。电话簿就是一整个的杂凑,他里面的键是以姓名为主,值则是这个人的电话。所以你必须为每一个人键入一个独特的键,大多也就是名字,以及这个键所对应的值,当然就是电话了。因为我们只要找到键(姓名)就可以查到依附在这个键的值(电话)。如此一来,我们应该很容易可以理解杂凑的代表意义了。
5.2 杂凑的表达
杂凑在Perl中是以百分比符号(%)作为表示,变数的命名方式则维持一贯原则,也就是可以包含字母,数字及下划线的字串,但是不能以数字作为开头。所以你可以像这样的方式定义一个杂凑变数:

my %hash; # 基本的命名方式
my %ID_Hash; # 包含下划线的变数
my %id_hash; # 大小写还是被认为是不同的字串
my %_underline; # 以下划线开始的变数名称
my %2hash; # 程序会产生错误,因为这不是合法的变数


杂凑的存取,我们可以利用大括号来进行,因此我们把所想要取得的杂凑键放入大括号中,就可以藉此找到相对应的值。同样的方式,我们也可以利用这样的形式指定某一对键值,这样的作法非常接近我们存取数组的形式:

my %hash;
$hash{key} = 'value'; # 最简单的赋值形式
print $hash{key};


就如我们说的,我们是使用大括号({})来标示所要存取的杂凑键,这和使用数组是不同的。不过更重要的是千万别把你的程序写得像这个样子:

my $var = 1;
my @var = (1, 2, 3, 4, 5, 6);
my %var;
$var{1} = 2;
$var{3} = 4;
$var{5} = 6;

print $var[2];


这样的形式对于Perl来说当然是合法的,不过我们显然不希望你用这样的形式来写程序,否则即使Perl可以很容易的分辨出来,只怕写程序或维护的人自己还先搞混了。
有时候,我们会忽略一些小地方,那就会让自己找不到杂凑中的值,其中有一个非常重要的部份,也就是杂凑键的数据类型。Perl会把杂凑键全部转为字串,这样的转换其实是有些道理的。我们来研究一下这样的程序会发生甚么状况呢:

my %hash;
$hash{2} = 'two'; # 指定杂凑的一对键值
$hash{'4/2'} = '这是字串 4/2'; # 注意引号的使用
print $hash{4/2}; # 先运算后转为字串的键


你认为Perl会输出甚么样的结果呢?答案是'two'。没错,很有趣吧,所以你可以在杂凑键的地方放上一个运算式,那么Perl会先进行运算,然后把运算结果转为字串,所以上面的例子,我们所要求Perl输出的其实是$hash{2},否则你可以利用引号来指定字串,就像$hash{'4/2'}这样的方式。我们再看看另一个例子:

my %hash;
for (1...5) {
$hash{$_*2} = $_**2;
}


那我们可以得到的杂凑就是像是这个样子:

$hash{2} = 1;
$hash{4} = 4;
$hash{6} = 9;
$hash{8} = 16;
$hash{10} = 25;


没错,正如我们所预料的,Perl会把运算出来的结果转为字串后当成杂凑的键。还记得我们可以利用字串的内插方式来插入变数到字串吗?你可以猜测以下的程序会产生出甚么不同的结果:

my %hash;
for (1...5) {
$hash{"$_*2"} = $_**2;
}


如果你可以想办法看到杂凑的内容,你会发现你得到的杂凑键变成了
"1*2","2*2"......。没错,因为他们被视为一个字串了。所以如果你以为你可以利用$hash{2}或$hash{4}来得到杂凑内的值,恐怕会失望了。所以当你要开始使用杂凑时,可就要小心别搞混了。

5.3 杂凑赋值
我们刚刚学到了利用 $hash{2} = 4;
这样的方式来指定一对键值给杂凑,没错,这是赋值给杂凑的最基本方式,不过就跟我们使用数组一样,我们经常需要一次指定大量的杂凑键值,想必Perl的开发者一定也会遇到相同的问题,而且应该有一些合理的解决方案。既然如此,我们应该有其他方式可以一次指定超过一组的键值。利用串列的方式赋值给杂凑就是其中之一,而且当你在定义某个杂凑时就预先知道他的一些键值时特别有用,看看下面的例子:

my %hash = qw/1 one 2 two 3 three/;


这样的赋值方式看起来跟处理数组时候的方式非常接近,我们利用qw//来指定一个串列,并且将这个串列赋值给杂凑。这时候,Perl会按照串列的顺序,分别为【键】,【值】,并且赋予杂凑。所以在这个例子中,所得到的结果就跟我们这么写是一样的:

$hash{1} = 'one';
$hash{2} = 'two';
$hash{3} = 'three';


或许你会想到某个状况,也就是键值的个数不一的时候。这时候,Perl会把最后一个键所对应的值设为undef(注二),你可以利用这个程序来确认:

my %hash = (1, 2, 3, 4, 5);
print 'false' unless defined($hash{5});


当然利用串列赋值的方式是方便了一些,可是就像我们刚刚遇到的问题,有时候会发现利用串列赋值的情况似乎比较容易发生错误。尤其当一个串列的元素足够多的时候,你要怎么确认某个串列中的元素应该是键,还是值呢?最简单的方式大概就是进行人工比对,所以你或许可以考虑用另外的方式来赋值给杂凑,就像这样的写法:

my %hash = (
1 => 'one',
2 => 'two',
3 => 'three',
);


在这里,我们利用箭号(=>)来表示杂凑中键跟值的相对关係,而且在一对键值的后面加上逗号作为区隔。这样的方式就显得方便、也直观了许多。不过当你在使用箭号进行指定时,你可能会发现一些不同。因为箭号左边的杂凑键已经完全被视为一个字串,所以你如果使用这样的方式:

my %hash = (
4/2 => 3,
);
print $hash{'4/2'};
print $hash{2};


别忘了,跟之前的状况一样,Perl还是会帮你先把箭号左边的运算式算出结果,然后转成字串,作为杂凑的键。所以当你在取值时使用了引号确保你要找杂凑键等于'4/2'的值时,你就没办法找到任何结果,因为目前杂凑中只有一个杂凑键为'2'的值。
要从杂凑中取出现有的值以目前的方式应该足够方便,你只需要知道杂凑中的键,就可以取得他的内容值。不过这样显然还不够,因为杂凑跟数组还是有着相当的差异。在数组中,你可以很清楚的知道数组的索引值是从0到最后一个数组的大小减1,可是在杂凑中却并不是这么一回事。如果你没办法知道杂凑的键,又怎么取出他的值呢?那么这个时候,你应该考虑先把整个杂凑读过一次。

5.4 each
就像在数组当中,你可以使用foreach这样的循环来找到数组中的每一个值,当然我们也经常需要在杂凑中进行类似的工作,我们希望可以在杂凑中能一次取出所有的键,值。所以你必须仰赖类似foreach的工具来帮助你,那就是each函数。例如你可以利用下面的写法读出刚刚我们所建立起来的杂凑:

while (my ($key, $value) = each (%hash)) {
# 取出杂凑中的每一对键值,并且分别放入$key, $value
print "$key => $value\n";
}


很明显的,每次each函数都会送回了一个包含两个值的串列,其中这两个值分别是一个杂凑键跟相对应的值。因此我们把取回的串列指定给$key和$value两个变数,接着印出结果,就可以看到一对一对的键值了。而当传回空数组时,while判断就会变成伪值,while循环也就结束了。利用这样的函式对我们有很大的帮助,如果我们想要整理一个杂凑的内容,我们可以在完全不知道杂凑中有什么内容的状况下开始进行处理。使用each函数在处理杂凑时是让事情显得容易许多,可是有时候还是有点不方便的地方,举例来说:如果我有一个包含着主机ip跟主机名称的杂凑,虽然我不知道杂凑里面到底有多少资料,可是我却希望能找出所有的杂凑键值,然后取出以192开始的ip位址。这时候如果使用each来作,那就必须先把所有的键值取出,然后再一一进行比对,所以也许程序就像这样:

my %hash = (
'168.1.2.1' => 'verdi',
'192.1.2.2' => 'wagner',
'168.1.2.3' => 'beethoven',
); # 定义主机跟ip 的对应
my @hostname;
while (my ($key, $value) = each (%hash)) {
if ($key =~ /^192/) { # 要找出ip以192开头的部份
push @hostname, $value; # 找到之后放入新的数组中
}
}

print @hostname;


很显然,这样的写法确实可以让程序正确的找出我们要的结果,不过我们总是还会继续思考可以有更干净俐落的写法,毕竟使用Perl的程序设计师都不太喜欢拉拉杂杂的程序。所以有甚么方法可以让过滤出需要的键值可以显得方便些呢?

5.5 keys跟values
如果我们可以用简单的方式一次取得杂凑的所有键(keys),那么要进行过去的过程就非常容易,而我们所需要的就是过滤后留下来的键,跟他们的相对值。当然,有某些时候,你可能只想要拿到杂凑中的所有值,这时候你就不需要担心他们是属于什么键的相关。为了因应这样的需求,有两个函数可以满足我们,他们分别是keys跟values。很显然的,这两个函数所作的工作就是取出杂凑的键跟值。和使用
each相当不同的是:你可以只单读取出所有的键,或所有的值,而不需要一次全部取出。
例如我们可以用这样来把杂凑键放在同一个数组中:

my @keys = keys(%hash);


如果你希望取出所有的值,那么不妨这样写:

my @values = values(%hash);


当然,你可以用他来完成each的工作,就像这样:

my @keys = keys(%hash);
for (@keys) {
print "$_ => $hash{$_}\n";
}

其实跟这么写是一样的效果:

while (my ($key, $value) = each(%hash)) {
print "$key => $value\n";
}


不过你显然会发现,有时候用keys/values比较简单,有时候用each比较方便,当然,至于要使用何者是完全取决于你所想要得出的结果,或者你认为最省力,简洁,或是效率比较好的写法。

在杂凑中使用keys/values这两个函数都传回串列,因此我们可以把我们所得到的串列轻易的放入数组,接下来再以数组的方式进行运算。这样的方便之处在于我们可以有很多可供利用的数组函数,所以我们可以把刚刚的那个例子改写成这样:

my %hash = (
'168.1.2.1' => 'verdi',
'192.1.2.2' => 'wagner',
'168.1.2.3' => 'beethoven',
);

my @keys = map { $hash{$_} }
grep { (m/^192/) } keys(%hash);

print @keys;


这样的写法比起之前的方式看起来是不是干净许多了呢?我们来看看最关键的一行,结果到底怎么产生的:我们先用keys函数取出杂凑中的所有键,就如我们所说的,这个函数传回一个串列。然后我们对所得到的串列进行过滤,利用grep取出串列中以192开头的ip子串列,最后利用map一一比对得出杂凑中以对应这些ip的主机名称。

5.6 杂凑的操作
毫无疑问,杂凑这样的数据结构对于程序的写作有着莫大的帮助,但是我们必须能熟悉对杂凑的操作才能够让我们更容易发挥杂凑的功能。其中最重要的大概就是exists跟delete两个函数了,这两个函式能让我们有效的掌握杂凑的元素,同时它们也是perl内建相关于杂凑函数的最后两个(注一)。
5.6.1 exists
我们就继续用ip跟主机的杂凑当例子吧。假如我有一个ip,我不确定我是否有这部主机的资料,如果我们只用刚刚的方法,那我们就必须取得所有的ip,然后把手上的ip跟取得的ip串列一一比对,以便确定自己有没有这个ip的主机资料。所以我们的程序也许长的像这样:

my %hash = (
'168.1.2.1' => 'verdi',
'192.1.2.2' => 'wagner',
'168.1.2.3' => 'beethoven',
);
my $ip = '192.1.2.2';
print "bingo" if ($hash{$ip});


在这里,我们有一个杂凑,其中三个键分别是'168.1.2.1','192.1.2.2','168.1.2.3',而我们希望判定目前手上的一组ip'192.1.2.2'是不是我们主机所拥有的ip。于是我们利用这个ip作为杂凑键,并判断如果取得的值为真,那么我们就说这个ip属于杂凑的其中一个键,这样的想法似乎暂时解决了我们的需求。不过我们来看看下面的例子:

my %hash = (
'cd' => 2,
'book' => 10,
'video' => 0,
);
my $media = 'video';
print "bingo" if ($hash{$media});


我们假设这是某个社区图书馆目前外借的东西数量,其中的键就是代表则可以外借的图书馆资产,其中包含了CD,书跟录影带。而所对应到的值则是他们目前被借出的数量。我们看到,CD被借走了两套,书被借走了十本,而录影带则是原封不动,一卷也没被借走。是的,大家都不喜欢录影带了。
这时候,我们希望知道图书馆是否提供录影带外借,也就是要检查video这个键是否存在。于是我们利用刚刚的方式,看看$hash{$media}是否传回真值。很遗憾,因为录影带这个键目前的值是0,因此当我们利用录影带当成键来取的相对应的值时,Perl会传回0给我们。而我们知道0其实是个伪值。于是我们以为'video'这个键并不存在于这个杂凑中,也就是说这个图书馆并没有录影带出借,但是这样的结果跟我们的认知有所不同,因为取得的值为0只是代表目前没人借出。所以我们发现这个方法并不正确,至少我们已经知道他会产生错误的结果。所以我们必须尝试其他方法,例如利用keys找到包含所有索引键的串列,然后进行一一的比对。就像这样:

print "exist" if (grep { $_ eq 'video' } keys (%hash));


这样就可以确定某个键是否存在于这个杂凑,可是程序还是有点长,而且我们也许必须经常去判断某个值是否为杂凑的键。所幸Perl提供了简洁的函式可以使用,所以利用exists这个函式让我们有了极佳的判断方式。有了exist之后,对于刚刚那一行程序,我们只需要这么改写:

print "exists" if (exists $hash{video});


这样的写法显然轻松了许多。

5.6.2 delete
有些时候,我们也会遇到某些键值我们不再需要的状况,这时候如果可以把这些没有必要的键值移除似乎是非常必要的。所以Perl也提供了移除杂凑键值的函式,也就是delete。这个函式的使用其实非常容易,你只需要指定想要删除的某一个杂凑键,就像这样:

delete $hash{video};


当然,所谓的移除是指这个键将不再存在于这个杂凑,而不是指让这个键对应的杂凑值消失。所以并不是把需要被delete的这对键值设为undef。也就是说,即使有一个键所对应的杂凑值为undef,那这个键依然被视为存在(exists)的,这在刚刚解释exists这个函数的例子中就可以了解了。

5.7 怎么让杂凑上手
在Perl中要使用杂凑,有一些重点也许还是应该提醒大家的。首先,Perl对于杂凑的大小限制依然採取了「放任」的态度,也就是以最没有限制的方式。只要电脑可以容量的大小,Perl都可以接受。因此程序设计师可以有很大的挥洒空间,只是也必须注意避免让系统因为被Perl佔用太多资源而导致无法正常运作。
另外,使用者可以利用任何的纯量值来表示杂凑中的键与值。可是在杂凑键的部份,Perl会把所有的键转换为字串。所以如果你在不注意的情况下把运算式当成杂凑的键,Perl会帮你先进行运算,然后利用运算所得的结果作为杂凑键,这样的情况可能会有出乎意料的结果。当然,如果你使用运算式来作为杂凑的键值,那就应该有些准备,因此应该会更小心的注意,而我们也在前面提到了不少例子。
另外,你还会希望知道自己甚么时候该用杂凑,这就必须依赖你对于杂凑的感觉,最基本的原则还是以杂凑的特性来看,如果你有一个可以辨识的键,而且希望藉由这个键找到相关连的值,这时候你几乎就可以放心的使用杂凑了,只不过这里所谓的值当然不限定单指特定的值,而可能是任何一种纯量值,也就是因为这个特性,可以让我们搭建出复杂的杂凑结构,不过这个部份则是属于进阶的内容,我们就不在这里解释。就像我们所举的例子,你可以利用ip作为每一部主机的辨识,那么你可以藉由ip找到那部机器的相关资料。
还有一个常常被搞混的问题,也就是杂凑的顺序。许多人想当然尔,以为杂凑的顺序是依照新增的顺序来决定的。其实事实并非如此,杂凑的排列方式并非按照使用者加入的顺序,而是Perl会依照内部的演算法找出最佳化的排列。

习题:
1. 将下列资料建立一个杂凑:
John => 1982.1.5
Paul => 1978.11.3
Lee => 1976.3.2
Mary => 1980.6.23
2. 印出1980年以后出生的人跟他们的生日。
3. 新增两笔资料到杂凑中:
Kayle => 1984.6.12
Ray => 1978.5.29
4. 检查在不修改程序码的情况下,能否达成第二题的题目需求

注一:可以利用perldoc perlfunc来查看perl所提供的函数。
注二:其实,如果你在程序里打开了警告讯息的选项,这样的指定会让Perl产生警告讯息:"Odd number of elements in
hash assignment"。

--
^ō^猪样年华之黯然消魂^ō^

Reply all
Reply to author
Forward
0 new messages