第 2 章 配列¶
はじめに¶
配列(Array)は、同じ型のデータを連続して格納するデータ構造です。この章では、Ruby の Array を使って配列操作のアルゴリズムを TDD で実装します。
1. データ構造と配列¶
データ構造とは¶
データ構造とは、データ単位とデータ自身との間の物理的または論理的な関係を表したものです。適切なデータ構造を選ぶことで、効率的なアルゴリズムを実装することができます。
配列の必要性¶
まず、配列がなぜ必要なのかを考えてみましょう。例えば、5人の学生の点数を読み込んで合計点と平均点を求めるプログラムを考えます。
配列を使わない場合、以下のようなコードになります:
puts '5人の点数の合計と平均点を求めます。'
print '1番の点数:'; tensu1 = gets.to_i
print '2番の点数:'; tensu2 = gets.to_i
print '3番の点数:'; tensu3 = gets.to_i
print '4番の点数:'; tensu4 = gets.to_i
print '5番の点数:'; tensu5 = gets.to_i
total = tensu1 + tensu2 + tensu3 + tensu4 + tensu5
puts "合計は#{total}点です。"
puts "平均は#{total / 5.0}点です。"
このコードでは、5人分の変数を個別に用意しています。しかし、100人や1000人の点数を扱う場合、このアプローチは現実的ではありません。配列を使えば、任意の人数を簡潔に扱えます:
n = 5
tensu = Array.new(n) { |i| print "#{i + 1}番の点数:"; gets.to_i }
total = tensu.sum
puts "合計は#{total}点です。"
puts "平均は#{total / n.to_f}点です。"
Ruby における配列¶
Ruby の配列(Array)は、異なる型の要素を混在させることができる動的な配列です。
x = [11, 22, 33, 44, 55, 66, 77] # 整数の配列
names = ['John', 'Paul', 'George', 'Ringo'] # 文字列の配列
mixed = [1, 'hello', 3.14, true] # 異なる型を混在
インデックスによるアクセス¶
配列の要素には、インデックス(添え字)を使ってアクセスします。Ruby のインデックスは 0 から始まります。
x = [11, 22, 33, 44, 55, 66, 77]
puts x[2] # => 33(3番目の要素)
puts x[-3] # => 55(後ろから3番目の要素)
x[-4] = 3.14 # 要素の値を変更
スライスによるアクセス¶
Ruby では、範囲オブジェクトやスライスを使って配列の一部を取り出すことができます。
s = [11, 22, 33, 44, 55, 66, 77]
puts s[0..5].inspect # => [11, 22, 33, 44, 55, 66](0番目から5番目)
puts s[0...5].inspect # => [11, 22, 33, 44, 55](0番目から4番目)
puts s[-4..-2].inspect # => [44, 55, 66](後ろから4番目から後ろから2番目)
配列の走査¶
配列の全要素を処理するには、いくつかの方法があります。
インデックスを使った走査¶
x = ['John', 'George', 'Paul', 'Ringo']
x.each_with_index do |name, i|
puts "x[#{i}] = #{name}"
end
each を使った走査¶
x = ['John', 'George', 'Paul', 'Ringo']
x.each { |name| puts name }
Python 版との違い¶
| 概念 | Python | Ruby |
|---|---|---|
| 配列の宣言 | x = [11, 22, ...] |
x = [11, 22, ...](同様) |
| タプル(不変) | t = (4, 7, 5.6) |
Ruby にタプルはない(配列で代替) |
| アンパック | a, b, c = x |
a, b, c = x(同様) |
| スライス(ステップ指定) | s[0:7:2] |
Ruby 標準では対応なし(each_slice 等を使用) |
| enumerate | enumerate(x) |
x.each_with_index |
2. 配列の要素の最大値¶
Red — テストを書く¶
describe "Algorithm.max_of" do
it "配列の最大値を返す" do
expect(Algorithm.max_of([172, 153, 192, 140, 165])).to eq(192)
end
end
Green — 実装¶
def self.max_of(a)
maximum = a[0]
(1...a.length).each do |i|
maximum = a[i] if a[i] > maximum
end
maximum
end
Python 版との違い¶
| 概念 | Python | Ruby |
|---|---|---|
| 配列の長さ | len(a) |
a.length または a.size |
| 範囲(右端除く) | range(1, len(a)) |
(1...a.length) |
3. 配列の要素の並びを反転¶
Red — テストを書く¶
describe "Algorithm.reverse_array" do
it "配列を逆順にする(破壊的)" do
a = [2, 5, 1, 3, 9, 6, 7]
Algorithm.reverse_array(a)
expect(a).to eq([7, 6, 9, 3, 1, 5, 2])
end
end
Green — 実装¶
def self.reverse_array(a)
n = a.length
(n / 2).times do |i|
a[i], a[n - i - 1] = a[n - i - 1], a[i]
end
end
Python 版との違い¶
| 概念 | Python | Ruby |
|---|---|---|
| 多重代入(スワップ) | a[i], a[j] = a[j], a[i] |
a[i], a[j] = a[j], a[i](同様) |
| 整数除算 | n // 2 |
n / 2(整数同士は自動的に整数除算) |
Ruby も Python と同様に多重代入でスワップを簡潔に書けます。
4. 基数変換¶
整数値を指定された基数(r 進数)に変換します。
Red — テストを書く¶
describe "Algorithm.card_conv" do
it "2進数変換" do
expect(Algorithm.card_conv(29, 2)).to eq("11101")
end
it "16進数変換" do
expect(Algorithm.card_conv(255, 16)).to eq("FF")
end
end
Green — 実装¶
def self.card_conv(x, r)
d = ""
dchar = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
while x > 0
d += dchar[x % r]
x /= r
end
d.reverse
end
Python 版との違い¶
| 概念 | Python | Ruby |
|---|---|---|
| 文字列の逆順 | d[::-1] |
d.reverse |
| 文字列のインデックスアクセス | dchar[x % r] |
dchar[x % r](同様) |
Ruby の d.reverse は Python のスライス d[::-1] より直感的です。
5. 素数の列挙¶
x 以下の素数を列挙するアルゴリズムを 3 段階で最適化します。
4-1. 第 1 版(基本実装)¶
# 除算回数を返す
def self.prime1(x)
counter = 0
(2..x).each do |n|
(2...n).each do |i|
counter += 1
break if (n % i).zero?
end
end
counter
end
4-2. 第 2 版(奇数のみチェック)¶
def self.prime2(x)
counter = 0
ptr = 0
prime = Array.new(500)
prime[ptr] = 2
ptr += 1
(3..x).step(2) do |n|
found_divisor = false
(1...ptr).each do |i|
counter += 1
if (n % prime[i]).zero?
found_divisor = true
break
end
end
unless found_divisor
prime[ptr] = n
ptr += 1
end
end
counter
end
Python 版との違い¶
| 概念 | Python | Ruby |
|---|---|---|
| ステップ付きループ | range(3, x+1, 2) |
(3..x).step(2) |
for...else |
Python 固有の構文 | found_divisor フラグで代替 |
| 空配列 | [None] * 500 |
Array.new(500) |
Python の for...else は Ruby にはない構文です。Ruby ではフラグ変数を使って同等のロジックを実現します。
4-3. 第 3 版(平方根最適化)¶
def self.prime3(x)
counter = 0
ptr = 0
prime = Array.new(500)
prime[ptr] = 2; ptr += 1
prime[ptr] = 3; ptr += 1
(5..1000).step(2) do |n|
i = 1
found_divisor = false
while prime[i] * prime[i] <= n
counter += 2
if (n % prime[i]).zero?
found_divisor = true
break
end
i += 1
end
unless found_divisor
prime[ptr] = n
ptr += 1
counter += 1
end
end
counter
end
テスト¶
describe "素数列挙" do
it "prime1: 1000以下の素数の除算回数" do
expect(Algorithm.prime1(1000)).to eq(78022)
end
it "prime2: 最適化版" do
expect(Algorithm.prime2(1000)).to eq(14622)
end
it "prime3: 平方根最適化版" do
expect(Algorithm.prime3(1000)).to eq(3774)
end
end
6. 配列のコピー¶
Ruby の配列には、浅いコピー(shallow copy)と深いコピー(deep copy)の 2 種類があります。
# 浅いコピー(dup)
x = [[1, 2, 3], [4, 5, 6]]
y = x.dup
x[0][1] = 9 # y も変更される(内部の配列は同じオブジェクト)
p y # => [[1, 9, 3], [4, 5, 6]]
# 深いコピー(Marshal)
x = [[1, 2, 3], [4, 5, 6]]
y = Marshal.load(Marshal.dump(x))
x[0][1] = 9 # y は変更されない
p y # => [[1, 2, 3], [4, 5, 6]]
dup による浅いコピーでは、配列の要素への参照がコピーされるため、ネストした配列の要素を変更すると、コピー元とコピー先の両方に影響します。Marshal.load(Marshal.dump(x)) による深いコピーでは、配列とその中のすべての要素が再帰的にコピーされます。
Python 版との違い¶
| 概念 | Python | Ruby |
|---|---|---|
| 浅いコピー | x.copy() |
x.dup |
| 深いコピー | copy.deepcopy(x) |
Marshal.load(Marshal.dump(x)) |
| 異なる型の混在 | Python リストは可能 | Ruby 配列も可能 |
まとめ¶
| アルゴリズム | メソッド | 計算量 |
|---|---|---|
| 配列の最大値 | max_of |
O(n) |
| 配列の反転 | reverse_array |
O(n) |
| 基数変換 | card_conv |
O(log x) |
| 素数列挙(基本) | prime1 |
O(n²) |
| 素数列挙(最適化) | prime2 |
O(n √n) |
| 素数列挙(平方根) | prime3 |
O(n √n / ln n) |
Ruby の配列の特徴¶
Array.new(n)で n 要素の配列を作成(初期値はnil)Array.new(n, val)で初期値を指定arr.lengthまたはarr.sizeで要素数を取得- スライス:
arr[start..end]またはarr[start...end] arr.reverseで逆順の新しい配列を返す(!付きは破壊的)