forループを Elixir の Enum.reduce()で書き換える例でも
紹介する記事を書こうと思っておりましたが、
最近の LeetCode の最近の問題で簡単な例があったので、回答例をご紹介します。
ちなみに、Enum.reduce_while()の例は、また別の記事にしよっかな...
2951. Find the Peaks
問題の内容は、厳密に要件を説明すると長くなる(直訳すると長くなる)ので簡単に書きますが、配列の中でピークとなっている(隣接する要素と比べて値が大きくなっている)箇所があれば、その要素番号を配列もしくはリストに格納して返しなさい。というものです。
ちなみに、私の回答例は下記に配置しています。
1. Javaでの回答例
Elixirとの比較のために、まずはJavaやPython3での回答例
public class Solution {
public List<Integer> findPeaks(int[] mountain) {
// 1ms
List<Integer> res = new ArrayList<>();
for (int i = 1; i < mountain.length - 1; i++) {
if (mountain[i - 1] < mountain[i] && mountain[i] > mountain[i + 1]) {
res.add(i);
}
}
return res;
}
}
次にPython3
class Solution:
def findPeaks(self, mountain: List[int]) -> List[int]:
ans = []
for i in range(1, len(mountain) - 1):
if mountain[i - 1] < mountain[i] > mountain[i + 1]:
ans.append(i)
return ans
内包表記を使えば、1行で書けますね。
class Solution:
def findPeaks(self, mountain: List[int]) -> List[int]:
return [i for i in range(1, len(mountain) - 1) if mountain[i - 1] < mountain[i] > mountain[i + 1]]
2. Elixirで書いてみる1(ListをMapに変換して処理)
つぎに、Elixirに移植してみます。
基本的に、forループは Elixirでは Enum.reduce()や Enum.reduce_while()で書き換えることができますが、考慮が必要な箇所があります。
それは、ループの終了条件です。
Java など、他の言語では
for (int i = 1; i < 1; i++)
は、初回から条件式が偽になりますので、forループ内の処理は実行されませんが、
Enum.reduce()に与える第一引数はあくまでリストですので、
1..1 や、1..0 といったリストが渡される可能性を考慮する必要があります。
(今回の処理では、i-1 といったインデックス番号の参照がありますが、インデックス番号が -1 になるわけにはいかない)
Enum.reduce(1..n, [], fn i, res ->
の処理は、n = 0 の場合、
Enum.reduce(1..0, [], fn i, res ->
となってしまいます。
ですので、Enum.reduce()の処理に入る前に、
リストの終端値については先に判定しておく必要があります。
今回は、別関数を作り、Enum.reduce()の処理に入る前に、
事前にnの値(リストの要素数)をwhen句で判定させています。
ちなみに、Elixirでは Mapは
Map.get()の他に、要素名[index番号]で参照できますので、
ListよりMapの方が配列っぽく書けますね。
とりあえず、Mapを使った私の回答例です。
なお、Enum.reduce()の末尾の
>| Enum.reverse()
は、今回の問題では順序は問われないのでやらなくても Accepted になりましたが、
逆順でリストに連結しているので、念のため付加してます。
(正順で連結しても、今回の問題では数ms程度しか遅くないようです。)
# 263ms - 360ms
defmodule SolutionMap do
@spec find_peaks(mountain :: [integer]) :: [integer]
def find_peaks(mountain) do
n = mountain |> Enum.count()
map = Enum.reduce(mountain, {0, Map.new()}, fn cur, {i, map} ->
{i + 1, Map.put(map, i, cur)}
end) |> elem(2)
find_peaks(map, n)
end
@spec find_peaks(map :: %{}, n :: integer) :: [integer]
def find_peaks(_map, n) when n < 3 do
[]
end
def find_peaks(map, n) do
Enum.reduce(1..n-2, [], fn i, res ->
if map[i - 1] < map[i] and map[i] > map[i + 1] do
[i] ++ res
else
res
end
end) |> Enum.reverse()
end
end
3. Elixirで書いてみる2(ListのままEnum.at()を使用して処理)
次に、Mapに変換せずにListのまま処理した場合の回答例です。
Enum.at()の記述が冗長ですね。
今回の問題のテストケースでは、処理速度は問題にはならず、
MapでもListでもそれほど処理時間は変わりませんでしたが、
Enum.at()を多用する処理では、処理時間に差が開いてきます。
# 263ms - 351ms
defmodule Solution do
@spec find_peaks(mountain :: [integer]) :: [integer]
def find_peaks(mountain) do
find_peaks(mountain, Enum.count(mountain))
end
@spec find_peaks(mountain :: [integer], n :: integer) :: [integer]
def find_peaks(_mountain, n) when n < 3 do
[]
end
def find_peaks(mountain, n) do
Enum.reduce(1..n-2, [], fn i, res ->
if Enum.at(mountain, i - 1) < Enum.at(mountain, i) and Enum.at(mountain, i) > Enum.at(mountain, i + 1) do
[i] ++ res
else
res
end
end) |> Enum.reverse()
end
end
関連記事
Elixir Enum.at()がどれだけ遅いか。そしてMap
https://qiita.com/gx3n-inue/items/6f04bbc260c70c5730a2