博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[Erlang]List结构和性能分析
阅读量:2058 次
发布时间:2019-04-29

本文共 2330 字,大约阅读时间需要 7 分钟。

Erlang里通过尾递归方式对列表中元素依次进行操作时,程序员们采用的方法总是先在尾递归中将处理后的元素加在已处理列表的头部,最后通过lists:reverse(List)来恢复原来次序。为什么不直接以自然顺序将表头元素加到已处理列表的尾部呢?这里面都是有故事的。

先看两个函数:

1234567891011121314151617
-module(list_time).-export([add_list_head/1, add_list_tail/1]). add_list_head(N) ->     add_list_head(N, []).add_list_head(0, L) ->     lists:reverse(L),     ok;add_list_head(N, L) ->     add_list_head(N-1, [65 | L]). add_list_tail(N) ->     add_list_tail(N, []).add_list_tail(0, L) ->     ok;add_list_tail(N, L) ->     add_list_tail(N-1, L ++ [65]).

add_list_head/1和add_list_tail/1都用于依次向列表中加入一个元素,元素个数由参数N指定。add_list_head/1中,新加入的元素放在当前列表的表头,最后调用lists:reverse(L)恢复顺序;add_list_tail/1中,新加入的元素直接放在当前列表的表尾。add_list_tail/1的实现方法更符合我们的逻辑,但效率上却是add_list_head/1远远占优。有如下基准测试数据为证:

12345678910111213141516
1> c (list_time).{ok,list_time}2> timer:tc(list_time, add_list_head, [10000]).{564,ok}3> timer:tc(list_time, add_list_head, [100000]).{8875,ok}4> timer:tc(list_time, add_list_head, [1000000]).{98275,ok}5> timer:tc(list_time, add_list_head, [10000000]).{1745467,ok} 6>6> timer:tc(list_time, add_list_tail, [10000]).{561634,ok}7> timer:tc(list_time, add_list_tail, [100000]).{59663921,ok}8> timer:tc(list_time, add_list_tail, [1000000]).  %半小时仍未返回 =。=

从测试数据看,add_list_head/1在依次添加10k, 100k, 1m, 10m个元素的时间分别为0.56ms, 8.87ms, 98.27ms, 1745.46ms。而add_list_tail/1添加10k, 100k个元素的时间分别为561.63ms和59663.92ms,当元素个数为1m时,运行半个小时仍未返回。造成性能差距如此之大的原因得从列表在Erlang里的实现原理中找。

在Erlang里,空列表用[]表示,所有非空列表都可以用[Element | Remain]的方式表示,比如,[a]可以表示成[a | []],[a, b, c]等价于[a | [b | [c | []]]]。但列表不是通过数组实现的,实际上,列表的底层数据结构是一个单链表(singly-linked list)。如下列表定义:

1
Foo = [a, b].

其对应的存储结构为:

123
Foo ↓ a → b → null

其中,Foo可以看做是单链表a→b的表头指针。

现在分析向表头添加元素生成列表的情况,向Foo表头添加元素c:

1
Bar = [c | Foo].

它的存储结构为:

123
Bar Foo ↓   ↓ c → a → b → null

向表头添加元素c只需把该节点的next指针指向Foo列表的表头即可,时间复杂度是O(1)。如果再向Foo的表头添加其他元素,如:

1
Baz = [d, e | Foo].

那么,Baz的存储结构为:

1234567
Bar Foo ↓   ↓ c → a → b → null     ↑ d → e ↑Baz

这个例子中, Bar, Baz直接重用了Foo的元素(存储空间),因为Foo是不可变的,所以这样的处理方式没有副作用。由此可以看出,通过向表头添加元素生成列表的方式的时间复杂度是O(n)。

现在再来分析向表尾添加元素的处理流程。实际上,Erlang中构造列表总是从表尾开始,然后依次在表头添加元素。通过++操作符和append/2函数向表尾“追加“元素只是一种假象,例如:

123
List1 = [a, b],List2 = [c, d],List3 =  List1 ++ List2.

构造列表List3的过程中,List1的数据被复制了一份,依次加到列表List2的表头,每次++操作,都会产生一个新的临时列表!从底层的指令看,每次表尾追加元素总是相当于先遍历一遍List1,所以向表尾追加元素的时间复杂度是O(n2)!

通过表头和表尾来操作列表的时间复杂度分别是O(n)和O(n2),因此,提倡以add_list_head/1的方式来操作列表。

转载地址:http://kxxlf.baihongyu.com/

你可能感兴趣的文章
结构型模式之桥接模式(Bridge)
查看>>
行为型模式之状态模式(State)
查看>>
行为型模式之策略模式(Strategy)
查看>>
行为型模式之模板方法模式(TemplateMethod)
查看>>
行为型模式之访问者模式(Visitor)
查看>>
大小端详解
查看>>
source insight使用方法简介
查看>>
<stdarg.h>头文件的使用
查看>>
C++/C 宏定义(define)中# ## 的含义 宏拼接
查看>>
Git安装配置
查看>>
linux中fork()函数详解
查看>>
C语言字符、字符串操作偏僻函数总结
查看>>
Git的Patch功能
查看>>
分析C语言的声明
查看>>
TCP为什么是三次握手,为什么不是两次或者四次 && TCP四次挥手
查看>>
C结构体、C++结构体、C++类的区别
查看>>
进程和线程的概念、区别和联系
查看>>
CMake 入门实战
查看>>
绑定CPU逻辑核心的利器——taskset
查看>>
Linux下perf性能测试火焰图只显示函数地址不显示函数名的问题
查看>>