第九章 关联数组/哈希表

by flamephoenix

一、数组变量的限制
二、定义
三、访问关联数组的元素
四、增加元素
五、创建关联数组
六、从数组变量复制到关联数组
七、元素的增删
八、列出数组的索引和值
九、用关联数组循环
十、用关联数组创建数据结构
  1、(单)链表
  2、结构
  3、树

一、数组变量的限制
    在前面讲的数组变量中,可以通过下标访问其中的元素。例如,下列语句访问数组@array的第三个元素:
    $scalar = $array[2];
    虽然数组很有用,但它们有一个显著缺陷,即很难记住哪个元素存贮的什么内容。假如我们来写一个程序计算某文件中首字母大写的单词出现的次数,用数组来实现就比较困难,程序代码如下:

1 : #!/usr/local/bin/perl
2 :
3 : while ($inputline = <STDIN>) {
4 :   while ($inputline =~ /\b[A-Z]\S+/g) {
5 :     $word = $&;
6 :     $word =~ s/[;.,:-]$//; # remove punctuation
7 :     for ($count = 1; $count <= @wordlist;
8 :         $count++) {
9 :       $found = 0;
10:       if ($wordlist[$count-1] eq $word) {
11:         $found = 1;
12:         $wordcount[$count-1] += 1;
13:         last;
14:       }
15:     }
16:     if ($found == 0) {
17:       $oldlength = @wordlist;
18:       $wordlist[$oldlength] = $word;
19:       $wordcount[$oldlength] = 1;
20:     }
21:   }
22: }
23: print ("Capitalized words and number of occurrences:\n");
24: for ($count = 1; $count <= @wordlist; $count++) {
25:   print ("$wordlist[$count-1]: $wordcount[$count-1]\n");
26: }

运行结果如下:

Here is a line of Input.
This Input contains some Capitalized words.
^D
Capitalized words and number of occurrences:
Here: 1
Input: 2
This: 1
Capitalized: 1

这个程序每次从标准输入文件读一行文字,第四行起的循环匹配每行中首字母大写的单词,每找到一个循环一次,赋给简单变量$word。在第六行中去掉标点后,查看该单词是否曾出现过,7~15行中在@wordlist中挨个元素做此检查,如果某个元素与$word相等,@wordcount中相应的元素就增加一个数。如果没有出现过,即@wordlist中没有元素与$word相等,16~20行给@wordlist和@wordcount增加一个新元素。
二、定义
    正如你所看到的,使用数组元素产生了一些问题。首先,@wordlist中哪个元素对应着哪个单词并不明显;更糟的是,每读进一个新单词,程序必须检查整个列表才能知道该单词是否曾经出现过,当列表变得较大时,这是很耗费时间的。
    这些问题产生的原因是数组元素通过数字下标访问,为了解决这类问题,Perl定义了另一种数组,可以用任意简单变量值来访问其元素,这种数组叫做关联数组,也叫哈希表。
    为了区分关联数组变量与普通的数组变量,Perl使用%作为其首字符,而数组变量以@打头。与其它变量名一样,%后的第一个字符必须为字母,后续字符可以为字母、数字或下划线。
三、访问关联数组的元素
    关联数组的下标可以为任何简单/标量值,访问单个元素时以$符号打头,下标用大括号围起来。例如:

$fruit{"bananas"}
$number{3.14159}
$integer{-7}

简单变量也可作为下标,如:
    $fruit{$my_fruit}
四、增加元素
    创建一个关联数组元素最简单的方法是赋值,如语句$fruit{"bananas"} = 1; 把1赋给关联数组%fruit下标为bananas的元素,如果该元素不存在,则被创建,如果数组%fruit从未使用过,也被创建。
    这一特性使得关联数组很容易用于计数。下面我们用关联数组改写上面的程序,注意实现同样的功能此程序简化了许多。

1 : #!/usr/local/bin/perl
2 :
3 : while ($inputline = ) {
4 :   while ($inputline =~ /\b[A-Z]\S+/g) {
5 :     $word = $&;
6 :     $word =~ s/[;.,:-]$//; # remove punctuation
7 :     $wordlist{$word} += 1;
8 :   }
9 : }
10: print ("Capitalized words and number of occurrences:\n");
11: foreach $capword (keys(%wordlist)) {
12:   print ("$capword: $wordlist{$capword}\n");
13: }

运行结果如下:

Here is a line of Input.
This Input contains some Capitalized words.
^D
Capitalized words and number of occurrences:
This: 1
Input: 2
Here: 1
Capitalized: 1

你可以看到,这次程序简单多了,读取输入并存贮各单词数目从20行减少到了7行。
    本程序用关联数组%wordlist跟踪首字母大写的单词,下标就用单词本身,元素值为该单词出现的次数。第11行使用了内嵌函数keys()。这个函数返回关联数组的下标列表,foreach语句就用此列表循环。
    注:关联数组总是随机存贮的,因此当你用keys()访问其所有元素时,不保证元素以任何顺序出现,特别值得一提的是,它们不会以被创建的顺序出现。
    要想控制关联数组元素出现的次序,可以用sort()函数对keys()返回值进行排列,如:

foreach $capword (sort keys(%wordlist)) {
  print ("$capword: $wordlist{$capword}\n");
}

五、创建关联数组
    可以用单个赋值语句创建关联数组,如:
    %fruit = ("apples",17,"bananas",9,"oranges","none");
    此语句创建的关联数组含有下面三个元素:

  • 下标为apples的元素,值为17
  • 下标为bananas的元素,值为9
  • 下标为oranges的元素,值为none

注:用列表给关联数组赋值时,Perl5允许使用"=>"或","来分隔下标与值,用"=>"可读性更好些,上面语句等效于:
    %fruit = ("apples"=>17,"bananas"=>9,"oranges"=>"none");
六、从数组变量复制到关联数组
    与列表一样,也可以通过数组变量创建关联数组,当然,其元素数目应该为偶数,如:
    @fruit = ("apples",17,"bananas",9,"oranges","none");
    %fruit = @fruit;
    反之,可以把关联数组赋给数组变量,如:
    %fruit = ("grapes",11,"lemons",27);
    @fruit = %fruit;
    注意,此语句中元素次序未定义,那么数组变量@fruit可能为("grapes",11,"lemons",27)或("lemons",27,"grapes",11)。
    关联数组变量之间可以直接赋值,如:%fruit2 = %fruit1; 还可以把数组变量同时赋给一些简单变量和一个关联数组变量,如:
    ($var1, $var2, %myarray) = @list;
    此语句把@list的第一个元素赋给$var1,第二个赋给$var2,其余的赋给%myarray。
    最后,关联数组可以通过返回值为列表的内嵌函数或用户定义的子程序来创建,下例中把split()函数的返回值--一个列表--赋给一个关联数组变量。

1: #!/usr/local/bin/perl
2:
3: $inputline = <STDIN>;
4: $inputline =~ s/^\s+|\s+\n$//g;
5: %fruit = split(/\s+/, $inputline);
6: print ("Number of bananas: $fruit{\"bananas\"}\n");

运行结果如下:

oranges 5 apples 7 bananas 11 cherries 6
Number of bananas: 11

七、元素的增删
    增加元素已经讲过,可以通过给一个未出现过的元素赋值来向关联数组中增加新元素,如$fruit{"lime"} = 1;创建下标为lime、值为1的新元素。
    删除元素的方法是用内嵌函数delete,如欲删除上述元素,则:
    delete ($fruit{"lime"});
注意:

1、一定要使用delete函数来删除关联数组的元素,这是唯一的方法。
2、一定不要对关联数组使用内嵌函数push、pop、shift及splice,因为其元素位置是随机的。

八、列出数组的索引和值
    上面已经提到,keys()函数返回关联数组下标的列表,如:

%fruit = ("apples", 9,
          "bananas", 23,
          "cherries", 11);
@fruitsubs = keys(%fruits);

这里,@fruitsubs被赋给apples、bananas、cherries构成的列表,再次提请注意,此列表没有次序,若想按字母顺序排列,可使用sort()函数。
    @fruitindexes = sort keys(%fruits);
    这样结果为("apples","bananas","cherries")。类似的,内嵌函数values()返回关联数组值的列表,如:

%fruit = ("apples", 9,
           "bananas", 23,
           "cherries", 11);
@fruitvalues = values(%fruits);

这里,@fruitvalues可能的结果为(9,23.11),次序可能不同。
九、用关联数组循环
    前面已经出现过利用keys()函数的foreach循环语句,这种循环效率比较低,因为每返回一个下标,还得再去寻找其值,如:

foreach $holder (keys(%records)){
  $record = $records{$holder};
}

Perl提供一种更有效的循环方式,使用内嵌函数each(),如:

%records = ("Maris", 61, "Aaron", 755, "Young", 511);
while (($holder, $record) = each(%records)) {
  # stuff goes here
}

each()函数每次返回一个双元素的列表,其第一个元素为下标,第二个元素为相应的值,最后返回一个空列表。
    注意:千万不要在each()循环中添加或删除元素,否则会产生不可预料的后果。
十、用关联数组创建数据结构
    用关联数组可以模拟在其它高级语言中常见的多种数据结构,本节讲述如何用之实现:链表、结构和树。
1、(单)链表
    链表是一种比较简单的数据结构,可以按一定的次序存贮值。每个元素含有两个域,一个是值,一个是引用(或称指针),指向链表中下一个元素。一个特殊的头指针指向链表的第一个元素。
    在Perl中,链表很容易用关联数组实现,因为一个元素的值可以作为下一个元素的索引。下例为按字母顺序排列的单词链表:

%words = ("abel", "baker",
          "baker", "charlie",
          "charlie", "delta",
          "delta", "");
$header = "abel";

上例中,简单变量$header含有链表中第一个单词,它同时也是关联数组第一个元素的下标,其值baker又是下一个元素的下标,依此类推。
    下标为delta的最后一个元素的值为空串,表示链表的结束。
    在将要处理的数据个数未知或其随程序运行而增长的情况下,链表十分有用。下例用链表按字母次序输出一个文件中的单词。

1 : #!/usr/local/bin/perl
2 :
3 : # initialize list to empty
4 : $header = "";
5 : while ($line = <STDIN>) {
6 :   # remove leading and trailing spaces
7 :   $line =~ s/^\s+|\s+$//g;
8 :   @words = split(/\s+/, $line);
9 :   foreach $word (@words) {
10:     # remove closing punctuation, if any
11:     $word =~ s/[.,;:-]$//;
12:     # convert all words to lower case
13:     $word =~ tr/A-Z/a-z/;
14:     &add_word_to_list($word);
15:   }
16: }
17: &print_list;
18:
19: sub add_word_to_list {
20:   local($word) = @_;
21:   local($pointer);
22:
23:   # if list is empty, add first item
24:   if ($header eq "") {
25:     $header = $word;
26:     $wordlist{$word} = "";
27:     return;
28:   }
29:   # if word identical to first element in list,
30:   # do nothing
31:   return if ($header eq $word);
32:   # see whether word should be the new
33:   # first word in the list
34:   if ($header gt $word) {
35:     $wordlist{$word} = $header;
36:     $header = $word;
37:     return;
38:   }
39:   # find place where word belongs
40:   $pointer = $header;
41:   while ($wordlist{$pointer} ne "" &&
42:     $wordlist{$pointer} lt $word) {
43:     $pointer = $wordlist{$pointer};
44:   }
45:   # if word already seen, do nothing
46:   return if ($word eq $wordlist{$pointer});
47:   $wordlist{$word} = $wordlist{$pointer};
48:   $wordlist{$pointer} = $word;
49: }
50:
51: sub print_list {
52:   local ($pointer);
53:   print ("Words in this file:\n");
54:   $pointer = $header;
55:   while ($pointer ne "") {
56:     print ("$pointer\n");
57:     $pointer = $wordlist{$pointer};
58:   }
59: }

运行结果如下:

Here are some words.
Here are more words.
Here are still more words.
^D
Words in this file:
are
here
more
some
still
words

此程序分为三个部分:

  • 主程序:读取输入并转换到相应的格式。
  • 子程序:add_word_to_list,建立排序单词链表。
  • 子程序:print_list,输出单词链表

第3~17行为主程序,第4行初始化链表,将表头变量$header设为空串,第5行起的循环每次读取一行输入,第7行去掉头、尾的空格,第8行将句子分割成单词。9~15行的内循环每次处理一个单词,如果该单词的最后一个字符是标点符号,就去掉。第13行把单词转换成全小写形式,第14行传递给子程序add_word_to_list。
    子程序add_word_to_list先在第24行处检查链表是否为空。如果是,第25行将单词赋给$header,26行创建链表第一个元素,存贮在关联数组%wordlist中。如果链表非空,37行检查第一个元素是否与该单词相同,如果相同,就立刻返回。下一步检查这一新单词是否应该为链表第一个元素,即其按字母顺序先于$header。如果是这样,则:
    1、创建一个新元素,下标为该新单词,其值为原第一个单词。
    2、该新单词赋给$header。
    如果该新单词不该为第一个元素,则40~44行利用局域变量$pointer寻找其合适的有效位置,41~44行循环到$wordlist{$pointer}大于或等于$word为止。接下来46行查看该单词是否已在链表中,如果在就返回,否则47~48行将其添加到链表中。首先47行创建新元素$wordlist{$word},其值为$wordlist{$pointer},这时$wordlist{$word}和$wordlist{$pointer}指向同一个单词。然后,48行将$wordlist{$pointer}的值赋为$word,即将$wordlist{$pointer}指向刚创建的新元素$wordlist{$word}。
    最后当处理完毕后,子程序print_list()依次输出链表,局域变量$pointer含有正在输出的值,$wordlist{$pointer}为下一个要输出的值。
    注:一般不需要用链表来做这些工作,用sort()和keys()在关联数组中循环就足够了,如:

foreach $word (sort keys(%wordlist)) {
  # print the sorted list, or whatever }

但是,这里涉及的指针的概念在其它数据结构中很有意义。
2、结构
    许多编程语言可以定义结构(structure),即一组数据的集合。结构中的每个元素有其自己的名字,并通过该名字来访问。
    Perl不直接提供结构这种数据结构,但可以用关联数组来模拟。例如模拟C语言中如下的结构:

struce{
  int field1;
  int field2;
  int field3; }mystructvar;

我们要做的是定义一个含有三个元素的关联数组,下标分别为field1、field2、field3,如:

%mystructvar = ("field1" , "" ,
      "field2" , "" ,
      "field3" , "" ,);

像上面C语言的定义一样,这个关联数组%mystrctvar有三个元素,下标分别为field1、field2、field3,各元素初始值均为空串。对各元素的访问和赋值通过指定下标来进行,如:
    $mystructvar{"field1"} = 17;
3、树
    另一个经常使用的数据结构是树。树与链表类似,但每个节点指向的元素多于一个。最简单的树是二叉树,每个节点指向另外两个元素,称为左子节点和右子节点(或称孩子),每个子节点又指向两个孙子节点,依此类推。
    注:此处所说的树像上述链表一样是单向的,每个节点指向其子节点,但子节点并不指向父节点。
    树的概念可以如下描述:

  • 因为每个子节点均为一个树,所以左/右子节点也称为左/右子树。(有时称左/右分支)
  • 第一个节点(不是任何节点的子节点的节点)称为树的根。
  • 没有孩子(子节点)的节点称为叶节点。

有多种使用关联数组实现树结构的方法,最好的一种应该是:给子节点分别加上left和right以访问之。例如,alphaleft和alpharight指向alpha的左右子节点。下面是用此方法创建二叉树并遍历的例程:

1 : #!/usr/local/bin/perl
2 :
3 : $rootname = "parent";
4 : %tree = ("parentleft", "child1",
5 :          "parentright", "child2",
6 :          "child1left", "grandchild1",
7 :          "child1right", "grandchild2",
8 :          "child2left", "grandchild3",
9 :          "child2right", "grandchild4");
10: # traverse tree, printing its elements
11: &print_tree($rootname);
12:
13: sub print_tree {
14:   local ($nodename) = @_;
15:   local ($leftchildname, $rightchildname);
16:
17:   $leftchildname = $nodename . "left";
18:   $rightchildname = $nodename . "right";
19:   if ($tree{$leftchildname} ne "") {
20:     &print_tree($tree{$leftchildname});
21:   }
22:   print ("$nodename\n");
23:   if ($tree{$rightchildname} ne "") {
24:     &print_tree($tree{$rightchildname});
25:   }
26: }

结果输出如下:

grandchild1
child1
grandchild2
parent
grandchild3
child2
grandchild4

该程序创建的二叉树如下图:

注意函数print_tree()以次序“左子树、节点、右子树”来输出各节点的名字,这种遍历次序称为“左序遍历”。如果把第22行移到19行前,先输出节点明,再输出左子树、右子树,则为“中序遍历”,如果把第22行移到25行后,输出次序为左子树、右子树、节点,则为“右序遍历”。
    可以用同样的方法,即连接字符串构成下标,来创建其它的数据结构,如数据库等。

上一章 下一章 目录

perl5 第九章 关联数组/哈希表的更多相关文章

  1. [数据结构 - 第8章] 查找之哈希表(C语言实现)

    首先是需要定义一个哈希表的结构以及一些相关的常数.其中 HashTable 就是哈希表结构.结构当中的 elem 为一个动态数组. #define HASHSIZE 12 // 定义哈希表长为数组的长 ...

  2. [LeetCode] #1# Two Sum : 数组/哈希表/二分查找/双指针

    一. 题目 1. Two SumTotal Accepted: 241484 Total Submissions: 1005339 Difficulty: Easy Given an array of ...

  3. 167. 两数之和 II - 输入有序数组 + 哈希表 + 双指针

    167. 两数之和 II - 输入有序数组 LeetCode_167 题目描述 方法一:暴力法(使用哈希表) class Solution { public int[] twoSum(int[] nu ...

  4. Swift4 基本数据类型(范围型, Stride型, 数组, 字符串, 哈希表)

    创建: 2018/02/28 完成: 2018/03/04 更新: 2018/05/03 给主要标题加上英语, 方便页内搜索 [任务表]TODO 范围型(Range)与Stride型  与范围运算符相 ...

  5. 15 BasicHashTable基本哈希表类(二)——Live555源码阅读(一)基本组件类

    这是Live555源码阅读的第一部分,包括了时间类,延时队列类,处理程序描述类,哈希表类这四个大类. 本文由乌合之众 lym瞎编,欢迎转载 http://www.cnblogs.com/oloroso ...

  6. JAVA数据结构--哈希表的实现(分离链接法)

    哈希表(散列)的定义 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度 ...

  7. 哈希表 -数据结构(C语言实现)

    读数据结构与算法分析 哈希表 一种用于以常数平均时间执行插入.删除和查找操作的数据结构. 但是是无序的 一般想法 通常为一个包含关键字的具有固定大小的数组 每个关键字通过散列函数映射到数组中 冲突:两 ...

  8. 一种适合于MC与SMC算法的哈希表设计

    MC算法与SMC算法中的三角片焊接问题 在之前的关于MC算法与SMC算法的博文中介绍了算法的实现,文章主要围绕算法的核心问题,即三角片如何产生的问题进行了详细的描述.但由于实际应用中需要的等值面Mes ...

  9. PHP关联数组和哈希表(hash table) 未指定

    PHP有数据的一个非常重要的一类,就是关联数组.又称为哈希表(hash table),是一种很好用的数据结构. 在程序中.我们可能会遇到须要消重的问题,举一个最简单的模型: 有一份username列表 ...

随机推荐

  1. Win10下Genymotion无法正常使用的解决方法

    原Win7下安装配置的genymotion正常使用,Eclipse的Genymotion插件也可以正常运行.系统升级后,忽然就不work了. 折腾了一天试了各种方式,网上的例子也五花八门.最后还是找到 ...

  2. CKEditor + CKFinder 实现编辑上传图片配置 (二)

    CKEditor + CKFinder 实现编辑上传图片配置 (二) 上传图片时,如果上传的图片过大,默认情况情况下回自动裁剪,代码如图 \ckfinder\config.php 目录下的配置文件co ...

  3. Linux各个发行版本的介绍, 以及VirtualBox+CentOS的安装步骤

    Linux和Unix系统有哪些主要的发行版本 Unix: (非开源传统商业操作系统) IBM AIX, HP HP-UX, Sun Solaris,等 各家硬件厂商的发行版本, 往往是和自家的硬件设备 ...

  4. The following classes could not be found: - android.support.v7.internal.widget.ActionBarOverlayLayou解决方案

    如图出现如下的错误的时候,一般都是升级Androdi Studio 后导致的,引入库不全,或者其他 东西缺少,可以如下解决方案: 或者如下的解决方案: 在布局文件的Design界面中,修改原来的App ...

  5. 树莓派高级GPIO库,wiringpi2 for python使用笔记(一)安装

    网上的教程,一般Python用RPi.GPIO来控制树莓派的GPIO,而C/C++一般用wringpi库来操作GPIO,RPi.GPIO过于简单,很多高级功能不支持,比如i2c/SPI库等,也缺乏高精 ...

  6. [LeetCode]题解(python):144-Binary Tree Preorder Traversal

    题目来源: https://leetcode.com/problems/binary-tree-preorder-traversal/ 题意分析: 前序遍历一棵树,递归的方法很简单.那么非递归的方法呢 ...

  7. MySQL show binglog event in 'log_name'

    二进制日志文件记录的内容:记录表的更改. 二进制日志文件记录的形式:基于语句的复制.基于行的复制. 两种记录形式的优点与不足: 基于语句的复制-->它不能保证复制的正确性.如随机函数可能在两台机 ...

  8. SQL Server 储存过程的output 参数

    要做的参数的回传一方面要做到有储存过程的配合,再一方面也要有调用方法的配合,也就是说错误的调用方法是没有办法把值回传的. 下面是例子 --1.储存过程方面的配合 create procedure db ...

  9. VS2012生成绿色版程序的方法

    方法就是在工程属性里设置: 配置属性-〉常规-〉项目默认值-〉MFC的使用-〉在静态库中使用MFC,见下图 之后重新编译即可.

  10. Candy----HDU4465----数学题

    题目地址:http://acm.hdu.edu.cn/showproblem.php?pid=4465 题目意思: 有两个箱子,每个箱子装有N个糖果 打开第一个箱子的概率是P,另外一个就是1-P 当小 ...