数组

数组类


  • https://www.kancloud.cn/imxieke/ruby-base/107301


数组

数组是带索引的对象的集合。

数组有以下特征:

  • 可以从数组中获取某个索引的元素(对象)

    例:print name[2]

  • 可以将任意的值(对象)保存到数组的某个索引的元素中

    例:name[0] = " 测试 "

  • 使用迭代器可以逐个取出数组中的元素

    例:names.each{|name| puts name}

数组的创建方法

nums = [1, 2, 3, 4, 5]
strs = ["a", "b", "c", "d"]

使用 Array.new

创建类的实例时使用的 new 方法,创建数组时也同样可以使用。

a = Array.new
p a                    #=> []
a = Array.new(5)
p a                    #=> [nil, nil, nil, nil, nil]
a = Array.new(5, 0)
p a                    #=> [0, 0, 0, 0, 0]

Array 类的情况下,若 new 方法没有参数,则会创建元素个数为 0 的数组;若 new 方法只有 1 个参数,则会创建元素个数为该参数个数,且各元素初始值都为 nil 的数组;若 new 方法有两个参数,则第 1 个参数代表元素的个数,第 2 个参数代表元素值的初始值。

当希望创建元素值相同的数组时,建议使用这个方法。

使用 %w 与 %i

创建不包含空白的字符串数组时,可以使用 %w

lang = %w(Ruby Perl Python Scheme Pike REBOL)
p lang #=> ["Ruby", "Perl", "Python", "Scheme", "Pike",
       #    "REBOL"]

虽然给人的感觉只是节省了书写 " ", 的时间,但是如果能掌握这种字符串数组的创建方法,就会使程序更加简洁。此外,Ruby2.0 还提供了创建符号(Symbol)数组的 %i

lang = %i(Ruby Perl Python Scheme Pike REBOL)
p lang   #=> [:Ruby, :Perl, :Python, :Scheme, :Pike, :REBOL]

在本例中,创建数组时使用了 () 将数组元素括了起来,但实际上还可以使用如 <>||!!@@AA 这样的任意字符。

虽然 Ruby 允许我们使用任意字符,但若用一些不常用的字符来创建数组的话,可能就会使程序不便于阅读。在选择表示字符串数组元素的字符时,还要注意该字符不能在要创建的字符串中出现,因此建议使用 ()<>||

使用 to_a 方法

到现在为止,我们已经介绍了三种传统的创建数组的方法,下面我们就来看看如何将其他对象转换为数组。

很多类都定义了 to_a 方法,该方法能把该类的对象转换为数组。

color_table = {black: "#000000", white: "#FFFFFF"}
p color_table.to_a  #=> [[:black, "#000000"],
                    #   [:white, "#FFFFFF"]]

对散列对象使用 to_a 方法,结果就会得到相应的数组的数组。具体来说就是,将散列中的各键、值作为一个数组,然后再把这样的数组放到一个大数组中。

使用字符串的 split 方法

我们再来介绍一个将对象转换为数组的方法。对用逗号或者空白间隔的字符串使用 split 方法,也可以创建数组。

column = "2013/05/30 22:33 foo.html proxy.example.jp".split()
p column
#=> ["2013/05/30", "22:33", "foo.html", "proxy.example.jp"]

索引的使用方法

在了解了如何创建数组之后,下面我们就来看看如何操作数组。

获取元素

对数组指定索引值,就可以获取相应的元素。我们可以逐个获取元素,也可以一次获取多个元素。

通过 [] 指定索引,获取元素。[] 有以下 3 种用法:

  • a [n]

  • a [n..m] 或者 a [n...m]

  • a [n, len]

用法(a)是我们在第 2 章中使用过的获取索引值为 n 的元素的方法。例如,通过 alpha[0] 获取数组 alpha 的首个元素。这里要注意,数组的索引值是从 0 开始的。

索引值为负数时,不是从数组的开头,而是从数组的末尾开始获取元素。如果指定的索引值大于元素个数,则返回 nil

用法(b)的 a [n..m] 表示获取从 a [n] 到 a [m] 的元素,然后用它们创建新数组并返回。a [n..m] 表示获取从 a [n] 到 a [m-1] 的元素,并用它们创建新数组返回。虽然下面的例子中只讨论 [n..m] 的形式,但能用 [n..m] 的地方同样能用 [n...m]。

如果 m 的值比数组长度大,则返回的结果与指定数组最后一个元素时是一样的

用法(c)[n, len] 表示从 a [n] 开始,获取之后的 len 个元素,用它们创建新数组并返回。

另外,我们还可以用普通的方法代替 []

  • a.at(n)       ……与 a[n] 等价

  • a.slice(n)        ……与 a[n] 等价

  • a.slice(n..m)     ……与 a[n..m] 等价

  • a.slice(n, len)   ……与 a[n, len] 等价

不过一般情况下我们很少会使用上述方法。

元素赋值

使用 []atslice 方法除了可以获取元素外,还可以对元素赋值。

  • a[n] = item

    这是将 a [n] 的元素值变更为 item。在下面的例子中,我们来尝试把 B 赋值给第 2 个元素,把 E 赋值给第 5 个元素。

    上面的例子介绍的是对一个元素赋值,实际上 Ruby 还可以一次对多个元素赋值。指定多个元素的方法与获取多个元素的方法是一样的。

    在下面的例子中,我们来尝试对数组的第 3 个元素到第 5 个元素赋值。下面是使用 [n, len] 的形式进行赋值的例子。

    alpha = ["a", "b", "c", "d", "e", "f"]
    alpha[2, 3] = ["C", "D", "E"]
    p alpha  #=> ["a", "b", "C", "D", "E", "f"]

插入元素

我们还可以在保持当前元素不变的情况下,对数组插入新的元素。

插入元素其实也可以被认为是对 0 个元素进行赋值。因此,指定 [n, 0] 后,就会在索引值为 n 的元素前插入新元素。

通过多个索引创建数组

使用 values_at 方法,就可以利用多个索引来分散获取多个元素,并用它们创建新数组。

  • a.values_at (n1, n2, …)

    用这个方法,我们就可以每隔一个元素获取一次

作为集合的数组

到目前为止的数组操作都是通过索引完成的。也就是说,从哪里获取元素、给哪个元素赋值、在哪里插入元素这些操作都是直接指定数组索引后进行的。

的确,数组、Array 类本来就是带有索引的对象,使用索引也是理所当然。不过有些时候我们会希望不通过索引而直接操作数组元素。

例如,我们可以把数组当成集合,这样一来,Array 类中的各元素就变了集合里的元素。

然而,由于集合没有顺序的概念,因此 ["a", "b", "c"]["b", "c", "a"]["c", "b", "a"] 就都可以被认为是同一个集合。

这样操作数组时,如果我们还关心“这个对象是数组的第几个元素”之类的问题,就可能会造成混乱。这是因为,索引操作实际上只是数组封装的一个功能而已。

接下来,我们就来看看如何把数组当作集合使用。而在下一节中,我们会再讨论把数组当作列使用时的方法。

集合的基本运算分为交集和并集。

  • 取出同时属于两个集合的元素,并创建新的集合

  • 取出两个集合中的所有元素,并创建新的集合

我们把第 1 种集合称为交集,第 2 种集合成为并集。

  • 交集……ary = ary1 & ary2

  • 并集……ary = ary1 | ary2

集合还有另外一种运算——补集,即获取某个集合中不属于另外一个集合的元素。但是 Array 类的情况下,由于没有全集的概念,因此也就没有补集。不过 Array 类有把某个集合中属于另外一个集合的元素删除的差运算。

  • 集合的差……ary = ary1 - ary2

由于数组 ary2 中包含的字符串 "d" 在数组 ary1 中并没有,因此不会被保留在执行结果中。

“|”与“+”的不同点

连接数组除了可以使用 | 外还可以使用 +。这两种方法看起来比较相似,但是有相同元素时它们的效果就不一样了。

num = [1, 2,3]
even = [2, 4, 6]
p (num + even)    #=> [1, 2, 3, 2, 4, 6]
p (num | even)    #=> [1, 2, 3, 4, 6]

数组 num 与数组 even 都有元素 2。使用 + 时元素 2 会有两个,使用 | 时相同的元素只会有一个。

作为列的数组

下面,我们来看看把数组对象当作列来看待时的情况。

数据结构的队列(queue)和栈(stack)都是典型的列结构。这两个相对的数据结构都有以下两种操作数据的方式。

  • 追加元素

  • 获取元素

    队列是一种按元素被追加时的顺序来获取元素的数据结构。这样的做法称为 FIFO(First-in First-out),也就是“先进先出”的意思。这与人们为等待某件事而排成一列时的情况一样,因此有时候也称为等待队列。

    而栈则是一种按照与元素被追加时的顺序相反的顺序来获取元素的数据结构。这样的做法称为 LIFO(Last-in First-out),是一种“先进后出”的数据结构。也就是说,在末尾追加元素,并从末尾开始获取元素。

简单地说就是,按 A、B、C 的顺序保存数据时,按照 A、B、C 的顺序取得数据的数据结构就是队列,按照 C、B、A 的顺序取得数据的数据结构就是栈。

队列与栈都是比较复杂的数据结构,同时也是提高程序运行效率所不可欠缺的工具。

在数组的开头或末尾插入元素,或者从数组的开头或末尾获取元素等操作,是实现队列、栈等数据结构所必须的前提条件。Ruby 的数组封装了如表 13.1 所示的方法,因此可以很轻松地实现这些前提条件。

操作数组开头与末尾的元素的方法

方法 | 对数组开头的元素的操作 | 对数组末尾的元素的操作

  • | - | - 追加元素 | unshift | push 删除元素 | shift | pop 引用元素 | first | last

利用图所示的 push 方法和 shift 方法可以实现队列

利用图所示的 push 方法和 pop 方法可以实现栈。

要注意的是,shift 方法和 pop 方法不只是获取数组元素,而且还会把该元素从数组中删除。如果只是单纯地希望引用元素,则应该使用 first 方法和 last 方法。

a = [1, 2, 3, 4, 5]
p a.first    #=> 1
p a.last     #=> 5
p a          #=> [1, 2, 3, 4, 5]

主要的数组方法

数组方法有很多,下面我们将选取最常用的几种方法,并把具有相同功能的方法归纳在一起来分别加以介绍。

为数组添加元素

  • a.unshift (item)

    将 item 元素添加到数组的开头。

    a = [1, 2, 3, 4, 5]
    a.unshift(0)
    p a    #=> [0, 1, 2, 3, 4, 5]
  • a << item

  • a.push (item)

    <<push 是等价的方法,在数组 a 的末尾添加新元素 item。

    a = [1, 2, 3, 4, 5]
    a << 6
    p a    #=> [1, 2, 3, 4, 5, 6]
  • a.concat (b)

  • a + b

    连接数组 a 和数组 b。concat 是具有破坏性的方法,而 + 则会根据原来的数组元素创建新的数组。

    a = [1, 2, 3, 4, 5]
    a.concat([8, 9])
    p a    #=> [1, 2, 3, 4, 5, 8, 9]
  • a [n] = item

  • a [n..m] = item

  • a [n, len] = item

    把数组 a 指定的部分的元素替换为 item。

    a = [1, 2, 3, 4, 5, 6, 7, 8]
    a[2..4] = 0
    p a    #=> [1, 2, 0, 6, 7, 8]
    a[1, 3] = 9
    p a    #=> [1, 9, 7, 8]

具有破坏性的方法

pop 方法、shift 方法那样,会改变接收者对象值的方法称为具有破坏性的方法。在使用具有破坏性的方法时需要特别小心,因为当有变量也引用了接收者对象时,如果接受者对象值发生了改变,变量值也会随之发生变化。我们来看看下面的例子。

a = [1, 2, 3, 4]
b = a
p b.pop    #=> 4
p b        #=> [1, 2, 3]
p a        #=> [1, 2, 3]

执行 pop 方法删除元素后,变量 a 引用的数组的元素也被删除,从 [1, 2, 3, 4] 变为了 [1, 2, 3],同时变量 b 引用的数组元素也被删除了。这是由于执行 b = a 后,并不是将变量 a 的内容复制给了变量 b,而是让变量 b 和变量 a 同时引用了一个对象。

在 Ruby 的方法中,有像 sortsort! 这样,在相同方法名后加上 ! 的方法。为了区分方法是否具有破坏性,在具有破坏性的方法末尾添加 ! 这一做法目前已经成为了通用的规则。

从数组中删除元素

根据某些条件从数组中删除元素。

  • a.compact

  • a.compact!

    从数组 a 中删除所有 nil 元素。compact 方法会返回新的数组,compact! 则直接替换原来的数组。compact! 方法返回的是删除 nil 元素后的 a,但是如果什么都没有删除的话就会返回 nil

    a = [1, nil, 3, nil, nil]
    a.compact!
    p a    #=> [1, 3]
  • a.delete(x)

    从数组 a 中删除 x 元素。

    a = [1, 2, 3, 2, 1]
    a.delete(2)
    p a #=> [1, 3, 1]
  • a.delete_at(n)

    从数组中删除 a[n] 元素。

    a = [1, 2, 3, 4, 5]
    a.delete_at(2)
    p a    #=> [1, 2, 4, 5]
  • a.delete_if{|item| … }

  • a.reject{|item| … }

  • a.reject!{|item| … }

    判断数组 a 中的各元素 item,如果块的执行结果为真,则从数组 a 中删除 item。delete_ifreject! 方法都是具有破坏性的方法。

    a = [1, 2, 3, 4, 5]
    a.delete_if{|i| i > 3}
    p a    #=> [1, 2, 3]
  • a.slice!(n)

  • a.slice!(n..m)

  • a.slice!(n, len)

    删除数组 a 中指定的部分,并返回删除部分的值。slice! 是具有破坏性的方法。

    a = [1, 2, 3, 4, 5]
    p a.slice!(1, 2)    #=> [2, 3]
    p a                 #=> [1, 4, 5]
  • a.uniq

  • a.uniq!

    删除数组 a 中重复的元素。uniq! 是具有破坏性的方法。

    a = [1, 2, 3, 4, 3, 2, 1]
    a.uniq!
    p a    #=> [1, 2, 3, 4]
  • a.shift

    删除数组 a 开头的元素,并返回删除的值。

    a = [1, 2, 3, 4, 5]
    a.shift    #=> 1
    p a        #=> [2, 3, 4, 5]
  • a.pop

    删除数组 a 末尾的元素,并返回删除的值。

    a = [1, 2, 3, 4, 5]
    a.pop    #=> 5
    p a      #=> [1, 2, 3, 4]

替换数组元素

将数组元素替换为别的元素的方法中,也分为带 ! 的和不带 ! 的方法,前者是具有破坏性的会改变接收者对象值的方法,后者则是直接返回新数组的方法。

  • a.collect{|item| … }

  • a.collect!{|item| … }

  • a.map{|item| … }

  • a.map!{|item| … }

    将数组 a 的各元素 item 传给块,并用块处理过的结果创建新的数组。从结果来看,数组的元素个数虽然不变,但由于经过了块处理,因此数组的元素和之前会不一样。

    a = [1, 2, 3, 4, 5]
    a.collect!{|item| item * 2}
    p a    #=> [2, 4, 6, 8, 10]
  • a.fill(value)

  • a.fill(value, begin)

  • a.fill(value, begin, len)

  • a.fill(value, n..m)

    将数组 a 的元素替换为 value。参数为一个时,数组 a 的所有元素值都会变为 value。参数为两个时,从 begin 到数组末尾的元素值都会变为 value。参数为三个时,从 begin 开始 len 个元素的值会变为 value。另外,当第 2 个参数指定为 [n..m] 时,则指定范围内的元素值都会变为 value。

    p [1, 2, 3, 4, 5].fill(0)        #=> [0, 0, 0, 0 ,0]
    p [1, 2, 3, 4, 5].fill(0, 2)     #=> [1, 2, 0, 0, 0]
    p [1, 2, 3, 4, 5].fill(0, 2, 2)  #=> [1, 2, 0, 0, 5]
    p [1, 2, 3, 4, 5].fill(0, 2..3)  #=> [1, 2, 0, 0, 5]
  • a.flatten

  • a.flatten!

    平坦化数组 a。所谓平坦化是指展开嵌套数组,使嵌套数组变为一个大数组。

    a = [1, [2, [3]], [4], 5]
    a.flatten!
    p a    #=> [1, 2, 3, 4, 5]
  • a.reverse

  • a.reverse!

    反转数组 a 的元素顺序。

    a = [1, 2, 3, 4, 5]
    a.reverse!
    p a #=> [5, 4, 3, 2, 1]
  • a.sort

  • a.sort!

  • a.sort{|i, j| … }

  • a.sort!{|i, j| … }

    对数组 a 进行排序。排序方法可以由块指定。没有块时,使用 <=> 运算符比较。

    a = [2, 4, 3, 5, 1]
    a.sort!
    p a    #=> [1, 2, 3, 4, 5]
  • a.sort_by{|i| … }

    对数组 a 进行排序。根据块的运行结果对数组的所有元素进行排序。

    a = [2, 4, 3, 5, 1]
    p a.sort_by{|i| -i }    #=> [5, 4, 3, 2, 1]

数组与迭代器

迭代器是实现循环处理的方法,而数组则是多个对象的集合。在对这些对象进行某种处理,或者取出某几个对象时,都需要大量用到迭代器。

例如,对数组的各元素进行相同的操作时使用的 each 方法就是典型的迭代器。该方法会遍历数组的所有元素,并对其进行特定的处理。

此外,接收者不是数组的情况下,为了让迭代器的执行结果能作为某个对象返回,也会用到数组。其中 collect 方法就是一个具有代表性的方法。collect 方法会收集某种处理的结果,并将其合并为一个数组后返回。

a = 1..5
b = a.collect{|i| i += 2}
p b    #=> [3, 4, 5, 6, 7]

在上面的例子中,接收者为范围对象,而结果则是数组对象。像这样,迭代器和数组就被紧密地结合在一起了。

处理数组中的元素

对数组中的元素进行处理时可以采取多种方法。

使用循环与索引

传统的方法是使用循环,也就是在遍历数组的同时,利用索引逐个访问数组元素。

例如,我们把数组的元素逐个取出来并输出。

list = ["a", "b", "c", "d"]
for i in 0..3
  print "第", i+1,"个元素是",list[i],"。\n"
end

对数值数组的元素进行合计的例子

list = [1, 3, 5, 7, 9]
sum = 0
for i in 0..4
  sum += list[i]
end
print "合计:",sum,"\n"

使用 each 方法逐个获取元素

数组中,通过 each 方法可以实现循环操作。下面,我们尝试使用 each 方法来改写

list = [1, 3, 5, 7, 9]
sum = 0
list.each do |elem|
  sum += elem
end
print "合计:",sum,"\n"

但是,使用 each 方法时,我们并不知道元素的索引值。因此,当需要指定元素的索引值时,可以使用 each_with_index 方法。

list = ["a", "b", "c", "d"]
list.each_with_index do |elem, i|
  print "第", i+1,"个元素是",elem,"。\n"
end

使用具有破坏性的方法实现循环

如果数组内各元素全部处理完毕后该数组就不需要了,那么我们就可以通过逐个删除数组元素使数组变空这样的手段来实现循环。

while item = a.pop
  ## 对item 进行处理
end

假设在循环开始前已经有元素在数组 a 中。如果逐个删除数组 a 中的元素,就会对删除的元素进行处理。最后,当数组为空时,循环结束。

使用其他迭代器

Ruby 中实现了不少像 collect、map 方法这样一眼就能看出其作用的基本操作。当希望创建某种迭代器时,请翻阅 Ruby 参考手册,一般情况下在里面都能找到我们需要的迭代器。这样就不会因为花精力创建了一个 Ruby 本来就有的迭代器而感到失望了。

数组的元素

数组中可以存放各种各样的对象。除了数值、字符串外,我们还可以在数组对象中存放别的数组对象或散列对象等等。

使用简单的矩阵

下面我们来看看用数组来表示矩阵的例子。

数组的各个元素也可以是数组,也就是所谓的数组的数组,这样的形式经常被用于表示矩阵。

例如,我们试试用数组的数组这种形式来表示矩阵。

/1,2,3\
|4,5,6|
\7,8,9/

第 1 行为 [1, 2, 3],第 2 行为 [4, 5, 6],第 3 行为 [7, ,8 , 9],把它们归纳为数组,如下所示。

a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

如果想取出元素 6,我们可以像下面这样做。

a[1][2]

首先用 a[1] 表示 [4, 5, 6] 这个数组,然后再指定第 3 位的元素,这样就能达到我们的目的了。

初始化时的注意事项

把数组对象或者散列对象作为数组元素时,需要注意该对象初始化时的问题。

a = Array.new(3, [0, 0, 0])

在上面的例子中,我们可能会以为 a[[0, 0, 0], [0, 0, 0], [0, 0, 0]],但实际却是另外一个结果

像下面那样,原本只是打算变更第 1 行的第 2 个元素,结果所有行的第 2 个元素都发生了改变。

a = Array.new(3, [0, 0, 0])
a[0][1] = 2
p a    #=> [[0, 2, 0], [0, 2, 0], [0, 2, 0]]

为了解决这个问题,我们可以指定 new 方法的块和元素个数。程序调用与元素个数一样次数的块,然后再将块的返回值赋值给元素。每次调用块都会生成新的对象,这样一来,各个元素引用同一个对象的问题就不会发生了。

a = Array.new(3) do
  [0, 0, 0]
end
p a    #=> [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

a[0][1] = 2
p a    #=> [[0, 2, 0], [0, 0, 0], [0, 0, 0]]

进行下述操作后,对应的元素的索引值就会被赋值给 i,这样就可以根据索引值初始化出不同的值了。

a = Array.new(5){|i| i + 1 }
p a    #=> [1, 2, 3, 4, 5]

同时访问多个数组

接下来我们来看看用相同的索引值同时访问对多个数组时的情况。在代码中,合计三个数组中索引值相同的元素,并将结果保存在新数组(result)中。

ary1 = [1, 2, 3, 4, 5]
ary2 = [10, 20, 30, 40, 50]
ary3 = [100, 200, 300, 400, 500]

i = 0
result = []
while i < ary1.length
  result << ary1[i] + ary2[i] + ary3[i]
  i += 1
end
p result  #=> [111, 222, 333, 444, 555]

使用 zip 方法可以程序变得更简单

ary1 = [1, 2, 3, 4, 5]
ary2 = [10, 20, 30, 40, 50]
ary3 = [100, 200, 300, 400, 500]

result = []
ary1.zip(ary2, ary3) do |a, b, c|
  result << a + b + c
end
p result  #=> [111, 222, 333, 444, 555]

zip 方法会将接收器和参数传来的数组元素逐一取出,而且每次都会启动块。参数可以是一个也可以是多个。