Erlang/Elixir [Elixir 基础] 复合类型: List

skpark1987 · 2017年09月14日 · 最后由 skpark1987 回复于 2017年09月18日 · 8166 次阅读

最近在学习 Elixir 开发,因为它是面向函数的编程语言,像我这种一直做面向对象开发的人来说很多东西很难理解。因此我打算写一系列基础篇的文章,既巩固自己的知识,也希望对其他学习该技术的人有帮助。

我们会以定义,常用的操作符,方法,性能的顺序来进行学习。

定义

Elixir 中的 List 是使用中括号来表示的。Elixir 中的 List 是一个高性能的链表,它意味着 List 内部是[头部, 尾部]来组成的

iex(27)> [1, 2, true, 3]
iex(28)> [head | tail] = [1, 2, 3]
iex(29)> head
1
iex(30)> tail
[2, 3]

注意,大部分情况下 head 是某一个元素,而 tail 是包含除了第一个元素的其他元素的数组。 因为内部是链表的方式实现的,在头部添加某个元素是很快速的(恒定时间),而添加到尾部所消耗的时间是随着 List 的大小而增加。稍后我们会演示用两种方式追加时候的性能。

常用的操作符

# 添加元素
[1, 2] ++ [3] # [1, 2, 3]
# 删除元素
[1, 2, 3, 4] -- [3, 2] # [1, 4]
# 进行比较
[3, 2] === [3, 2] # true

常用的方法

基本上看List 模块Enum 模块就能了解大部分方法了,下面列出常用的几个。

# 获取头部元素
iex(81)> hd [1,2,3]
1
# 获取尾部数组
iex(82)> tl [1,2,3]
[2, 3]
# 获取列表长度
iex(83)> length [1,2,3]
3
# 遍历元素
iex(84)> Enum.each [1,2,3], fn(x) ->
...(84)>   IO.puts x
...(84)> end
1
2
3
# 根据下标进行访问
iex(86)> Enum.fetch([1, 2, 3], 2)
{:ok, 3}
# 删除某个元素
iex(106)> List.delete([1,2,3,4,"a"], "a")
[1, 2, 3, 4] #不保证肯定删除(找到了才删,没找到返回原来的列表)
# 删除指定下标的元素
iex(110)> List.delete_at([1,2,3,4,"a"], 4)
[1, 2, 3, 4] #不保证肯定删除(找到了才删,没找到返回原来的列表)
# 在指定的位置插入新的元素
iex(112)> List.insert_at([1,2,3,4,"a"], 4, 5)
[1, 2, 3, 4, 5, "a"]
# 删除重复元素
iex(88)> Enum.uniq([1, 2, 3, 3, 2, 1])
[1, 2, 3, 2, 1]
# 数组元素进行连接
iex(90)> Enum.join([1, 2, 3])
"123"
iex(91)> Enum.join([1, 2, 3], " = ")
"1 = 2 = 3"
# 随机获取某一个元素
iex(92)> Enum.random [1,2,3,4,5]
1
iex(93)> Enum.random [1,2,3,4,5]
3
# 顺序打乱
iex(94)> Enum.shuffle [1,2,3,4,5]
[4, 5, 3, 2, 1]
# 进行排序
iex(100)> Enum.shuffle([1,2,3,4,5]) |> Enum.sort
[1, 2, 3, 4, 5]
# 从头部获取n个元素
iex(101)> Enum.take([1,2,3,4,5], 3)
[1, 2, 3]
# 随机获取n个元素
iex(102)> Enum.take_random([1,2,3,4,5], 3)
[5, 1, 4]
# 对数组里的数字进行求和
iex(105)> Enum.sum 1..10
55

性能

下面的程序为在头部追加和尾部追加时候的性能对比

defmodule Recursion do
  def prepend_elem(list, elem, n) when n <= 1 do
    list = elem ++ list
  end

  def prepend_elem(list, elem, n) do
    list = elem ++ list
    prepend_elem(list, elem, n - 1)
  end

  def append_elem(list, elem, n) when n <= 1 do
    list = list ++ elem
  end

  def append_elem(list, elem, n) do
    list = list ++ elem
    append_elem(list, elem, n - 1)
  end
end
IO.puts "start prepend"
prepend_start_time = Time.utc_now |> Time.to_string
list = Recursion.prepend_elem([], [1], 100000)
prepend_end_time = Time.utc_now |> Time.to_string
IO.puts "start at #{prepend_start_time}" 
IO.puts "end at #{prepend_end_time}"
IO.puts "------------------------"
IO.puts "start append"
append_start_time = Time.utc_now |> Time.to_string
list = Recursion.append_elem([], [1], 100000)
append_end_time = Time.utc_now |> Time.to_string
IO.puts "start at #{append_start_time}" 
IO.puts "end at #{append_end_time}"

start prepend
start at 15:10:24.463439
end at 15:10:24.474485
------------------------
start append
start at 15:10:24.483897
end at 15:10:48.547505

结论: 头部追加 (prepend)10 万次:10 毫秒。 尾部追加 (append)10 万次:24 秒(元素越多越慢),2400 倍

性能这块,我的理解是因为 Elixir 中 List 是一个链表(而且是单链表,只保留了第一个元素的索引,叫做 Head 吧),因此如果往头部增加元素,直接操作 Head 就可以了;如果尾部追加,就需要首先遍历到最后一个元素,然后才在最后一个元素上面添加元素,因此元素越多性能会越差。

chalvern 回复

恩,我也是这么理解的。目前我也才刚开始学习,理解的不是很深,只是把学习后的成果分享了出来。

你从哪里学的 elixir 啊?

List 是单链表结构,每次添加到尾部都会完全遍历一遍。如果有这种操作(写某些算法经常出现),建议添加到 List 头部最后 Enum.reverse ,这样一次遍历效率高很多

jeffhappily 回复

都是在官网啊,也有一些外国人写了博客,学习不难。 难的是转换思维,不仅仅是面向对象思维的转换也是框架学习思维的转换 (Rails -> Phoenix)。

skpark1987 回复

其实我是想知道别人都怎么学习新的语言或框架。我学习 ruby 已经快一年了,主要都用在 rails,基础也学得差不多,但最近学习进度开始慢了。我看别人开始学习一个语言或框架不久,就开始搭建自己的项目或 gem,或者参与到一些开源项目。可是我总觉得自己距离那步好远,我不知道应该从哪里再让自己进步。

jeffhappily 回复

去解决一些实际的问题,做一些学习之上的研究。学习固然重要且终生受用,但也不能一直处于被动学习的状态。另外,不要和别人比,要找到自己的目标和节奏;如果你之前没有什么基础,那么仅仅学了一年 Ruby 根本算不上什么积累,你所艳羡的那些人哪个不是拥有五年十年实际开发工作经验的?做好自己,每天进步一点就好,日积月累,持之以恒。

nightire 回复

做一些学习之上的研究

具体指的是什么呢? 每天进步一点也要有进步方向吧

jeffhappily 回复

话说这个问题你还要继续追问我,那你的自我驱动能力是不是有点捉急啊?

举个例子好了,你上网搜教程(不管什么框架/工具)大抵都是教你怎么用,在学会用的同时有没有想过:这个是怎么实现的呢?于是主动去翻翻源码也好,上 SO 提问题询问也罢,总之这都是在单纯学习之上的深入探索。

darkbaby123 回复

恩,试了一下Enum.reverse的性能很快,应该能应付大部分的情况。

skpark1987 关闭了讨论。 09月19日 10:21
需要 登录 后方可回复, 如果你还没有账号请 注册新账号