Ruby 集合指南(1):陣列

類別: IT
標籤: ruby

程式設計中有大量的排序和搜尋。在比較老的C語言中,你可能要自己寫資料結構和演算法才能完成這些任務。然而,在Ruby中為了能更加關注任務處理已經將這些構造為抽象的方式。

下面這些指導將介紹這些抽象結構。這些指導不可能很全面-光講Ruby集合就能寫一本書了-但我能廣泛撒網,而且我覆蓋了我認為做為一個Ruby程式員將經常要計算的方面。它們可以被分為4個方面:

  1. 陣列和迭代
  2. 雜湊,集合和排列
  3. 列舉和計數器
  4. 技巧和提示

這個指導例子非常多。我覺得學習這些最好的方式是開啟irb命令列跟著例子學習,通過你自己變化多端的方式來建立程式。

陣列

陣列是Ruby集合中一匹戰馬。很多在集合中操作方法的返回值都是陣列,即使原本集合不是陣列。事實上,它們並不是真正的陣列,它是一種萬能的資料結構。你可以把它們用做集合,棧,或著佇列。其功能和Python list相似。

建立

Ruby陣列的建立和一些動態語言建立方式類似。

>> numbers = [1, 0, 7]
>> numbers[2]
=> 7
>> numbers.size
=> 3

陣列中的型別不必一樣。它們是多樣的。

>> data = ["bob", 3, 0.931, true]

因為Ruby是完全基於物件的,陣列將描述為一個物件而不只是特殊的翻譯規則。這意味著你可以和其它物件一樣來構造陣列。

>> numbers = Array.new([1,2,3]) 
=> [1, 2, 3]

陣列的建構函式可以傳入一個開始的大小,但是它可能不會和你想象的一樣工作。因為Ruby陣列是動態的,沒必要提前申請足夠的空間。當你用Array.new的方式來傳入一個數字,一個將包含nil的物件將被建立。

>> numbers = Array.new(3)
=> [nil, nil, nil]

儘管nil是個獨立的物件,它也在集合中和其它物件一樣佔據位置。所以當你給陣列新增一個nil物件,它會在陣列末尾新增。

>> numbers << 3
=> [nil, nil, nil, 3]
>> numbers.size
=> 4

如果你在Array.new中傳入第二個引數,陣列將會用它代替nil進行初始化。

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

>> Array.new(3, :DEFAULT)
=> [:DEFAULT, :DEFAULT, :DEFAULT]

在增加標準文字方面,Ruby提供一些別的通過%來實現的語法捷徑。符號。

>> ORD = "ord"
>> %W{This is an interpolated array of w#{ORD}s}
=> ["This", "is", "an", "interpolated", "array", "of", "words"] 

>> %w{This is a non-interpolated array of w#{ORD}s}
=> ["This", "is", "a", "non-interpolated", "array", "of", "w\#{ORD}s"]

陣列目錄

很多語言中如果你試圖訪問不存在的陣列索引會丟擲異常。如果你試圖讀一個不存在的索引,Ruby返回nil。

>> spanish_days = []
>> spanish_days[3]
=> nil

如果你寫一個不存在的索引,Ruby會在其索引位置寫一個nil。

>> spanish_days[3] = "jueves"  
=> [nil, nil, nil, "jueves"] 
>> spanish_days[6] = "domingo" 
=> [nil, nil, nil, "jueves", nil, nil, "domingo"]

在很多語言中,如果你訪問一個負索引也會引發錯誤。而Ruby認為負索引是陣列末尾的開始,反向增加。

>> ["a","b","c"][-1]
=> "c"
>> ["a","b","c"][-3]
=> "a"

如果你提供一個不存在,負陣列索引,結果都是不存在-nil

>> ["a","b","c"][-4]
=> nil

陣列分類

Ruby陣列中另一個有用的特性就是可以對元素進行分類。然而,在Ruby可以用比較苛刻和許多不同的方式來指定元素的分類。

>> letters = %w{a b c d e f}
=> ["a", "b", "c", "d", "e", "f"]
>> letters[0..1]
=> ["a", "b"]
>> letters[0, 2]
=> ["a", "b"]
>> letters[0...2]
=> ["a", "b"]
>> letters[0..-5]
=> ["a", "b"]
>> letters[-6, 2]
=> ["a", "b"]

下面是對例子的介紹

  1. letters[0..1] – 返回從0到1的元素
  2. letters[0, 2] – 返回從0位置開始後的2個元素
  3. letters[0...2] – 返回從0直到位置2的元素
  4. letters[0..-5] – 返回從0到倒數位置為5的元素
  5. letters[-6, 2] – 返回從倒數第6個開始2個元素

如果你剛開始學習Ruby,你可能想要知道這些是怎樣實現的。訪問陣列的最好方式就是用[]方法。

>> letters.[](0..1)
=> ["a", "b"]

另外,0..1不過是一個偽裝的Range物件。你可以用class方法來驗證。

>> (0..1).class
=> Range

所以實際上是Range物件給Array[]傳遞目標分類。

>> letters.[](Range.new(0,1))
=> ["a", "b"]

如果你喜歡,這種在Ruby中以基於物件導向的方式可以建立出很漂亮的東西。那麼如果你想要用數字來帶英文字元呢?

numerouno gem可以解析英文數字。

$ gem install numerouno

>> require 'numerouno'
>> "one-hundred sixty three".as_number
=> 163

用numerouno你可以讓陣列類擁有英文索引了。

class EnglishArray < Array
  def [](idx)     
    if String === idx
      self.at(idx.as_number)  
    end
  end
end 

>> arr = EnglishArray.new(["a","b","c"])
>> arr["one"]
=> "b"

變形

記得我之前說過Ruby的Array型別是萬能結構嗎?下面是你可以用在Array上操作的例子。

增加元素

>> [1,2,3] << "a" 
=> [1,2,3,"a"]

>> [1,2,3].push("a")
=> [1,2,3,"a"]

>> [1,2,3].unshift("a")
=> ["a",1,2,3]

>> [1,2,3] << [4,5,6]
=> [1,2,3,[4,5,6]]

刪除元素

>> arr = [1,2,3]
>> arr.pop
=> 3
>> arr
=> [1,2]

>> arr = ["a",1,2,3]
>> arr.shift
=> "a"
>> arr
=> [1,2,3]

>> arr = [:a, :b, :c]
>> arr.delete(:b)
=> :b
>> arr
=> [:a, :c]
>> arr.delete_at(1)
=> :c
>> arr
=> [:a]

新增陣列

>> [1,2,3] + [4,5,6]
=> [1,2,3,4,5,6]

>> [1,2,3].concat([4,5,6])
=> [1,2,3,4,5,6]

>> ["a",1,2,3,"b"] - [2,"a","b"]
=> [1,3]

布林操作

>> [1,2,3] & [2,3,4]
=> [2,3]

>> [1,2,3] | [2,3,4]
=> [1,2,3,4]

>> arr1 = [1,2,3]
>> arr2 = [2,3,4]
>> xor = arr1 + arr2 - (arr1 & arr2)
=> [1,4]

移動元素

>> [1,2,3].reverse
=> [3,2,1]

>> [1,2,3].rotate
=> [2,3,1]

>> [1,2,3].rotate(-1)
=> [3,1,2]

安全提示

>> arr = [1,2,3]
>> arr.freeze
>> arr << 4  
=> RuntimeError: can't modify frozen Array

給String新增元素

>> words = ["every","good","boy","does","fine"]
>> words.join
=> "everygoodboydoesfine"

>> words.join(" ")
=> "every good boy does fine"

刪除巢狀

>> [1,[2,3],[4,["a", nil]]].flatten
=> [1,2,3,4,"a",nil]

>> [1,[2,3],[4,["a", nil]]].flatten(1)
=> [1,2,3,4,["a", nil]]

刪除副本

>> [4,1,2,1,5,4].uniq
=> [4,1,2,5]

切割

>> arr = [1,2,3,4,5]
>> arr.first(3)
=> [1,2,3]

>> arr.last(3)
=> [3,4,5]

查詢

>> ["a","b","c"].include? "d"
=> false

>> ["a", "a", "b"].count "a"
=> 2

>> ["a", "a", "b"].count "b"
=> 1

>> [1,2,[3,4]].size
=> 3

迭代

迭代可以說是Ruby真正亮點。在很多語言中迭代感覺都是很笨拙。然而,在Ruby中你不會感到你需要寫一個典型的for迴圈。

在Ruby中迭代的核心構造器是each方法。

>> ["first", "middle", "last"].each { |i| puts i.capitalize }
First
Middle
Last

儘管each是Ruby迭代中的核心,但還有很多其他的方式。比如,你可以通過一個reverse_each來反向迭代集合。

>> ["first", "middle", "last"].reverse_each { |i| puts i.upcase }
LAST
MIDDLE
FIRST

另一個需要處理的方法是each_with_index可以在第二個引數中傳入一個目前處理的索引。

>> ["a", "b", "c"].each_with_index do |letter, index| 
>>   puts "#{letter.upcase}: #{index}"
>> end
A: 0
B: 1
C: 2

那each方法是怎麼工作的呢?最好的理解方式就是你建立自己的each。

class Colors
  def each
    yield "red"
    yield "green"
    yield "blue"
  end
end

>> c = Colors.new
>> c.each { |i| puts i }
red
green
blue

yieId方法呼叫塊來給each傳遞資料。這對於Ruby初學者來說把注意力都放這裡可能有些難。那就認為yieId是呼叫一個匿名程式碼體,然後你在裡面提供了方法yieId。在前面的例子中,yieId被呼叫3次,所以"{|i| puts i}"執行了3次。

區域性迭代

迴圈中一個比較好的事情是開始和結束點可以指定。each方法可以迭代整個集合。

如果你只想迭代集合的某一部分,至少可以有如下的兩種方法來實現:

將集合切開然後迭代切過後的集合

>> [1,2,3,4,5][0..2].each { |i| puts i }
1
2
3
用Range來生成索引


>> arr = [1,2,3,4,5]
>> (0..2).each { |i| puts arr[i] }
1
2
3
儘管開起來比較醜,我敢打賭當陣列元素數量增大而且需要很長時間來拷貝時第二種方式更加有效。

#each與#map/#collect

除了#each之外,你可能還經常碰到#map。#map類似於#each,只是它是在each塊呼叫的結果上生成陣列。這很有用,因為#each返回的僅僅是呼叫each的哪個陣列。

>> [1,2,3].each { |i| i + 1 }
=> [1,2,3]

>> [1,2,3].map { |i| i + 1 }
=> [2,3,4]

如果你只是對每個元素呼叫同一方法,那麼你可以使用方便的快捷簡寫:

>> [1,2,3].map(&:to_f)
=> [1.0, 2.0, 3.0]
這個等同於:
>> [1,2,3].map { |i| i.to_f }
=> [1.0, 2.0, 3.0]
注意: #map不會更改原始的陣列值。它僅僅返回的是基於each塊呼叫結果而生成的陣列。如果你想在原始陣列上反映出這種更改的話,請使用#map!。
>> numbers = [1,2,3]
>> numbers.map(&:to_f)
=> [1.0, 2.0, 3.0]
>> numbers
=> [1, 2, 3]
>> numbers.map!(&:to_f)
=> [1.0, 2.0, 3.0]
>> numbers
=> [1.0, 2.0, 3.0]
你還可能注意到Ruby程式碼裡的#collect。它與#map相同,因此它們兩個是可互換的。
>> letters = ["a", "b", "c"]
>> letters.map(&:capitalize)
=> ["A", "B", "C"]
>> letters.collect(&:capitalize)
=> ["A", "B", "C"]

傳統的迭代

Ruby提供了傳統的"for"風格的迭代。雖然這種風格看起來更清晰,而且對哪些從其他語言轉過來的新手來收更熟悉,但是這種風格不符合語言習慣,因為它不是物件導向的,同時也不接受塊引數。

>> animals = ["cat", "dog", "bird", "chuck testa"]
>> for animal in animals
>>   puts animal.upcase
>> end
CAT
DOG
BIRD
CHUCK TESTA

如果“不符合語言習慣”還不足以阻止你的話,可以看看下面這個由Nikals B.在stackoverflow上展示的異常結果:

>> results = []
>> (1..3).each { |i| results << lambda { i } }
>> results.map(&:call)
=> [1, 2, 3]

>> results = []
>> for i in 1..3
>>   results << lambda { i }
>> end
>> result.map(&:call)
=> [3, 3, 3]
另外,“for"迴圈所建立的“臨時”變數根本就不是臨時的。那上面for迴圈中animals中的anmial變數怎麼是正確的呢? 不,不正確。
>> animal
=> "chuck testa"
結論:Ruby的for最適合於讓你疑惑的眼珠轉個不停,或者最適合於讓哪些參加會議的格外關心。
Ruby 集合指南(1):陣列原文請看這裡

推薦文章