Arrays

Voltar ao começo do guia.

Arrays são tipos de dados que podem armazenar elementos de qualquer tipo. Os elementos de um array são separados por vírgula, e os elementos de um array são delimitados por colchetes ([]).

Já trabalhamos com Arrays em vários momentos nesse guia, quando por exemplo utilizamos o método char de uma String, que retorna um Array ou quando passamos vários parâmetros opcionais para um método, que são agrupados em um Array, ou quando fazemos uma iteração em um for. No entanto, em todos esses casos, estávamos apenas utilizando um Array que foi criado pela linguagem. Chegou o momento de nós criarmos os nossos próprios Arrays.

Criando arrays

Em Ruby, a maneira mais simples de se criar um Array é da seguinte forma:

telefones = [1, 2, 3, 4]
emails = [
  "gustavo@google.com",
  "gustavo@yahoo.com",
  "mail@gustavopinto.org"
]

puts telefones
puts emails

Perceba que cada elemento do Array é separado por uma vírgula. Mais acima mencionei que era a forma mais simples, o que deixou entender que existem outras formas. Sim, em Ruby há sempre mais de uma forma de se fazer a mesma coisa. Caso queiramos criar um Array em que cada elemento seja uma única palavra, podemos usar a notação %w{} que cria um Array de palavras (word array):

emails =  %w{gustavo@google.com gustavo@yahoo.com mail@gustavopinto.org}

puts emails

Embora seja equivalente a notação de colchetes ([]), usar o %w{} pode simplificar a leitura pois não exige que aspas e virgulas sejam fornecidas. Outra forma de criar um Array com valores específicos é usando um Range, junto com o método to_a, que converte um Range em um Array. Por exemplo:

(1..5).to_a       #  => [1, 2, 3, 4, 5]
("a".."e").to_a   #  => ["a", "b", "c", "d", "e"]

Como Ruby é uma linguagem que emprega fortemente o paradigma orientado a objetos, Arrays também ser criados usando a estrutura emails = Array.new(["gustavo@google.com", "gustavo@yahoo.com", "mail@gustavopinto.org"]). Quais dessas você achou mais simples?

Acessando elementos do array

No nosso exemplo, criamos um Array com elementos do tipo Integer e outro Array com elementos do tipo String. No entanto, Arrays podem ser heterogêneos, comportando elementos de tipos diferentes. Por exemplo:

dados_pessoais = [
  "Gustavo Pinto",
  "mail@gustavopinto.org",
  00011122233,
  :gustavopinto
]

puts dados_pessoais

Para acessar um determinado elemento no Array, usamos também a notação de colchetes ([]). No exemplo do Array acima, acessamos o primeiro elemento utilizando: dados_pessoais[0]. A posição zero (0) informada dentro dos colchetes se refere ao primeiro elemento do Array. Podemos acessar qualquer elemento do Array usando a notação de colchetes ([]).

dados_pessoais = [
  "Gustavo Pinto",
  "mail@gustavopinto.org",
  00011122233,
  :gustavopinto
]

puts dados_pessoais[0]   # => Gustavo Pinto
puts dados_pessoais[1]   # => mail@gustavopinto.org
puts dados_pessoais[2]   # => 00011122233
puts dados_pessoais[3]   # => gustavopinto
puts dados_pessoais[4]   # => nil

Diferente de outras linguagens de programação, acessar uma posição sem que exista um elemento armazenado nele não retorna um erro; em Ruby, isto retorna um nil. Uma outra forma de acessar os elementos de um Array é através do método at, que funciona de forma análoga a notação de colchetes ([]).

puts dados_pessoais.at(0)   # => Gustavo Pinto
puts dados_pessoais.at(3)   # => gustavopinto

Para conveniência, podemos acessar o primeiro elemento usando o método first e o último elemento usando o método last.

puts dados_pessoais.at(0)   # => Gustavo Pinto
puts dados_pessoais.at(3)   # => gustavopinto

Perceba também que estamos acessando os elementos do Array de forma crescente, do primeiro elemento ao último elemento (ou da esquerda para a direita). Mas e se quiséssemos acessar os elementos do Array da forma inversa, do último ao primeiro (ou da direita para a esquerda)? Ruby também conta com açúcares sintáticos para lidar com esses casos. Para isso, basta fornecer um índice negativo. Por exemplo:

dados_pessoais = [
  "Gustavo Pinto",
  "mail@gustavopinto.org",
  00011122233,
  :gustavopinto
]

puts dados_pessoais[-1]   # => gustavopinto
puts dados_pessoais[-2]   # => 00011122233
puts dados_pessoais[-3]   # => mail@gustavopinto.org
puts dados_pessoais[-4]   # => Gustavo Pinto
puts dados_pessoais[-5]   # => nil

Logo, o índice -1 acessa a última posição do Array, enquanto que o -2 acessa a penúltima, e assim por diante. Novamente, se passarmos um índice sem elemento na posição, Ruby retorna um nil.

Assim como aprendemos a utilizar o operador de Range em Strings, podemos também usar o mesmo operador com Arrays.

dados_pessoais = # ...

dados_pessoais[1..2]     # => ["mail@gustavopinto.org", 2401435]
dados_pessoais[-3..-1]   # => ["mail@gustavopinto.org", 2401435, :gustavopinto]
dados_pessoais[1..-2]    # => ["mail@gustavopinto.org", 2401435]
dados_pessoais[-3..-1]   # => []

Como já discutimos em capítulos anteriores, vários operadores em Ruby são, na realidade, métodos. O operador [] é outro exemplo. Logo, quando fazemos uma chamada como dados_pessoais[1..2], é o equivalente a fazer dados_pessoais.[](1..2). Como também já discutimos no capítulo sobre tipos de dados, o operador .. é implementado pela classe Range. Logo, a expressão 1..2 é equivalente a Range.new(1,2). Logo, a nossa chamada dados_pessoais[1..2] nada mais é do que um açúcar sintático da expressão dados_pessoais.[](Range.new(1,2)).

Manipulando arrays

Podemos também usar a notação de colchetes para manipular elementos no Array. Por exemplo, a instrução dados_pessoais[0] = "Gustavo" altera o valor que estava na posição zero para o novo valor que foi passado, no caso a String Gustavo. Podemos também fazer atribuições paralelas em Arrays. Nesse caso, ao invés de passar um índice específico, podemos passar um Range. Por exemplo:

dados_pessoais = [
  "Gustavo Pinto",
  "mail@gustavopinto.org",
  00011122233,
  :gustavopinto
]

dados_pessoais[0..1] = "Gustavo", "gpinto@ufpa.br"

puts dados_pessoais

Até agora estamos trabalhando em um Array pre-definido. E se quiséssemos adicionar outros elementos no Array ao longo da execução do programa? Podemos usar o operador shovel (<<) que utilizamos para concatenar Strings também para adicionar novos elementos em um Array.

[1,2,3] << 4          # => [1,2,3,4]

Em outras linguagens, o operador + é utilizado para adicionar elementos no Array. Como vimos, em Ruby fazemos isso usando o <<. No entanto, podemos também utilizar o operador + em Arrays. Nesse caso, o seu comportamento é ligeiramente diferente. Ao invés de adicionar um elemento ao Array, o operador + concatena dois Arrays. Vamos experimentar um pouco com esses dois operadores:

[1,2,3] + [4]          # => [1,2,3,4,5]
[1,2,3] + 4            # => TypeError
[1,2,3] << [4]         # => [1,2,3,[4]]

Enquanto que usar o operador + com um Array e um Inteiro retorna um TypeError (não há conversão clara de Inteiro para Array), usar o << com dois Arrays acabou inserindo um elemento do tipo Array dentro do nosso Array.

Existe também o método push que funciona de maneira similar ao operador shovel (<<). No entanto há uma sutil diferença entre os dois: enquanto que o << aceita somente um único argumento e o insere ao final do Array, o método push aceita um ou mais argumentos (mas também insere ao final do Array). Por exemplo:

[1,2,3] << 4          # => [1,2,3,4]
[1,2,3].push 4        # => [1,2,3,4]

[1,2,3] << 4,5        # => SyntaxError!
[1,2,3].push 4,5      # => [1,2,3,4,5]

[1,2,3] << [4,5]        # => [1,2,3,[4,5]]
[1,2,3].push [4,5]      # => [1,2,3,[4,5]]

Além de acessar, alterar e incluir elementos, podemos também remove-los durante a execução do programa. Há várias formas de remover elementos de um Array. Algumas delas:

[1,2,3].shift            # => 1
[1,2,3].pop              # => 3
[1,2,3].delete(1)        # => 1
[1,2,3].delete_at(1)     # => 2

Enquanto que o shift remove o primeiro elemento, o pop remove o último elemento, o delete remove um específico, e o delete_at remove um elemento de uma posição específica. Note que todos os métodos acima retornam o elemento removido, e não o Array modificado.

Por fim, assim como podemos combinar Arrays usando o operador +, podemos fazer subtração de elementos usando o operador -.

[1,2,3,4,5] - [1]        #=>  [2,3,4,5]
[1,2,3,4,5] - [1,3,5]    #=>  [2,4]

[1,2,3,4,5] - 1          #=>  TypeError

Perceba que o operador - recebe dois Arrays como operandos. Embora pareça intuitivo tentar remover um elemento sem precisar engloba-lo em um Array, essa operação retorna um TypeError.

Percorrendo elementos do Array

Para percorrer elementos em um Array, podemos usar um for, como vimos no capítulo anterior. Por exemplo:

dados_pessoais = # ...

for i in dados_pessoais
  puts i
end

Embora essa seja uma forma muito comum de se percorrer Arrays em algumas linguagens de programação, em Ruby ela é pouco utilizada. Isso pois Ruby conta com várias outras formas (menos procedurais) que são preferidas pela comunidade. Podemos por exemplo utilizar o método each, que recebe um bloco.

dados_pessoais = # ...

dados_pessoais.each do |i|
  puts i
end

Como esperado, poderíamos escrever blocos de uma única linha usando a notação de chaves {}, como dados_pessoais.each {|i| puts i}.

Na classe Array há diversos outros métodos que podem ser utilizados para iterar por seus elementos. Outro exemplo é o método reverse_each, que itera pela order reversa dos elementos:

items = (1..5).to_a
items.reverse_each {|i| puts i}  # => 5, 4, 3, 2, 1

Outro método bastante utilizado é o map. O map não somente itera pelos elementos de um Array, também é capaz de fazer manipulações nesses elementos. Por exemplo, poderíamos converter um Array dos primeiros 50 inteiros positivos para sua representação binária usando um map:

items = (1..50).to_a
items.map {|i| puts i.to_s(2)}

Além do map, o select também é outro método muito útil. O select filtra elementos do Array baseado em alguma condição. Por exemplo, poderíamos remover todos os elementos ímpares de um Array da seguinte forma:

items = (1..50).to_a
items.select {|i| i.even? }  # => [2, 4, 6, 8, ..., 50]

Podemos também usar os métodos em conjunto. Por exemplo, se quiséssemos saber a representação binária somente dos números pares, poderíamos fazer:

items = (1..50).to_a
items.select {|i| i.even? }.map {|i| puts i.to_s(2)}

Há diversos outros métodos que valem a pena ser estudados na documentação oficial da linguagem.

Matrizes

A capacidade de criar um Array dentro de outro é fundamental para que possamos manipular matrizes. Um exemplo de matriz 3x3 (ou seja, três linhas por três colunas) pode ser visto abaixo:

a = [0,0,0]
b = [1,1,1]
c = [0,1,0]

matriz = [a,b,c] # => [[0, 0, 0], [1, 1, 1], [0,1,0]]

Imprimir o objeto matriz em uma única linha não ajuda a entender o seu formato. Vamos quebra-lo em várias linhas apenas para fins de facilitar a visualização:

[
  [0, 0, 0],
  [1, 1, 1],
  [0, 1, 0]
]

Exercícios de fixação

  • Crie um algoritmo de busca binária.

  • Quantos bytes são necessários para armazenar o seguinte array: [1,2,3,4]. Justifique sua resposta.

  • Explique o resultado das seguintes expressões:

[1] == [1]       # => true
[1].equal? [1]   # => false
  • Implemente o jogo do campo minado. No jogo do campo minado, é criado um mapa (representado por uma matriz), em que o usuário deve informar uma determinada posição. A posição pode tanto ter 1) uma bandeira, e o usuário ganha o jogo, 2) uma bomba, e o usuário perder o jogo, ou nenhum item. A quantidade de bombas deve ocupar aproximadamente 50% da matriz. Ou seja, em uma matriz 4x4, haverão 12 posições, das quais 6 delas com bomba, 1 com a bandeira, e as outras 5 sem item.
  • Implemente o jogo da velha. No jogo da velha, dois jogadores devem preencher com zeros ou uns uma matriz 3x3. Ganha o jogador que primeiro uma coluna, uma linha, ou uma diagonal com os mesmos elementos. Há um empate caso nenhum jogador ganhe o jogo.
  • Justifique a saída para cada uma das entradas a baixo:
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"]
  • Crie um método que receba um array de inteiros com valores duplicados e imprima como saída um hash com a quantidade de ocorrência de cada item do array. Por exemplo, dado o array [1,2,2,2,2,3,3,3,4,4] como entrada imprima o seguinte hash {1 => 1, 2 => 4, 3 => 3, 4 => 2}.

  • Crie um programa que leia um arquivo de texto (.txt) de um diretório, separe as palavras pelo espaço em branco, e converta as palavras para minúsculo. Imprima o total de palavras do arquivo, e o número de vezes cada palavra foi utilizada.

  • Crie um método chamado particiona que receba um Array de inteiros e retorne um array englobando dois outros arrays: um somente com os números pares e outro somente com os números ímpares. Por ex:

    particionado = particiona (1..10).to_a
    puts particionado # =>  [[2, 4, 6, 8, 10], [1, 3, 5, 7, 9]]