Clojure中Vector和List的区别及其相关函数

  Clojure是一门动态类型的语言,运行时才会做类型检查。它也不会像java这种面向对象编程语言,在调用一个对象的函数时,首先这个函数必须是属于该对象的函数,否则检查报错。所以对于clojure中的数据类型为Vector或者List的变量,总是会让初学者在选择时比较发愁,不知道什么情况下使用vector好?什么情况下使用list好?哪些函数是接收list作为参数的?而哪些函数又是接收vector作为参数的?它们的区别又是什么?本博客关于近期对clojure的list和vector的学习做一个总结:

1.List与Vector的特点:

  Clojure中既有列表(list),也有向量(vector),两者都是序列数据结构,它们的特点对比如下:

  • 存储类型:
    list是链式存储结构,而vector是顺序存储类型
  • 数据插入:
    list支持高效的前端插入,而vector在尾端插入会更高效
  • 数据查找:
    在数据查找方面,因为存储类型的不同,vector的时间复杂度会明显低于list,因为vector采取的理想b树的存储结构(http://blog.csdn.net/zdplife/article/details/52138512),所以其查找效率接近常数时间(O(log32n)),而list的查找操作需要O(N)的时间复杂度,所以在存在大量查找操作时,尽量选择vector。

2.创建相关函数:

  • vector的相关创建函数:
;;方法1:直接使用字面值常量表示法
[1 2 3]
;;=> [1 2 3]

;;方法2:使用vector,接收n个元素
(vector 1 2 3)
;;=> [1 2 3]

;;方法3:在已有结构的基础上创建向量
(vec '(1 2 3))
;;=> [1 2 3]
;;map中的元素转换为序列时,是以键值对的形式作为元素返回的
(vec {:a 1 :b 2})
;;=> [[:a 1] [:b 2]]

;;方法4:使用into函数在已有结构基础上创建向量
(into [] {:a 1 :b 2})
;;=> [[:a 1] [:b 2]]
(into []  '(1 2 3))
;;=> [1 2 3]

  以上是vector四个创建函数,经常用到的是最后两个,我在博客(http://blog.csdn.net/zdplife/article/details/52138512)中讲到,因为into使用了transient(瞬态存储结构),所以其效率会vec函数快30%,vec的可读性比较强,而into函数的效率更大,所以在使用时可以折中选择。
  还有就是map被转换为序列时,是按照键值对返回的,这个在写代码时需要注意。

  • list的相关创建函数:
    list的创建函数与vector的类似,如下:
;;由于()在clojue中会被当作函数操作符,所以这里在用字面值常量创建列表时,需要加单引号,阻止求求值
'(1 2 3)
;;=> (1 2 3)

;;该list与vector的用法类似
(list 1 2 3)
;;=> (1 2 3)

;;该into用法与上述介绍的一致
(into '() [1 2 3])
;;=> (3 2 1)

  into在效率上依然是首要的选择,而使用单引号创建list时,有一点缺点就是它不会解释里面的变量或者函数调用,所以这种方法要尽量少用:

(def a 4)
;;=> #'insight.main/a
;;单引号会阻止对a求值,返回字面值
'(1 2 a)
;;=> (1 2 a)

;;而list函数则会对a进行解析,所以尽量选择list函数
(list 1 2 a)
;;=> (1 2 4)

  其中在使用into函数时,vector和list中元素的序列是不一样的,这是因为into函数会以此调用conj函数将后者的元素一个一个加入到前面的序列中,但由于这两个存储结构不同,conj加入的位置正好相反,conj函数会在后面介绍到。

3.插入元素相关函数:
  在vector和list中插入元素经常用到的两个函数是conj和cons,这两个函数的参数位置刚刚好相反,conj是被插入的元素放在后面,而cons是将被插入的元素放在前面。需要注意的是,conj返回的是一个新的具体类对象,原有序列是什么类型,它便返回什么类型,而cons则会返回一个序列(clojure.lang.Cons),也就是会改变原有对象的类型:

;;对于conj函数返回的总是原有序列的类型
(class (conj [1 2] 3))
;;=> clojure.lang.PersistentVector
(class (conj '(1 2) 3))
;;=> clojure.lang.PersistentList

;;而cons函数则永远返回的是一个惰性序列clojure.lang.Cons
(class (cons 3 '(1 2)))
;;=> clojure.lang.Cons
(class (cons 3 [1 2]))
;;=> clojure.lang.Cons

  cons函数不管是向什么类型的序列中插入元素都会放在序列的头上,而conj函数在插入元素时,会考虑到原有序列类型的插入效率,因为list支持头部高速插入,所以会放在list头部,而如果是vector则会放到其尾部:

;;conj函数对于list和vector的插入位置刚好相反,使用时一定要注意
(conj [1 2] 3)
;;=> [1 2 3]
(conj '(1 2) 3)
;;=> (3 1 2)

;;cons不管什么类型插入位置总是相同
(cons 3 [1 2])
=> (3 1 2)
(cons 3 '(1 2))
=> (3 1 2)

  这两个函数还有一个区别就是,conj支持一次调用连续插入多个元素,而cons则只支持插入一个元素:

(conj [1 2 3] 4 5)
;;=> [1 2 3 4 5]
(cons 4 5 [1 2 3])
;;ArityException Wrong number of args (3) passed to: core/cons--4331  

  最后我们讨论一下这两个函数在对于map操作时的区别,虽然不建议这样使用:

;;conj对于map依然保持原有类型,可以既可以接收map,也可以接收键值对,或者两种类型同时接受
;;插入map
(conj {:a 1 :b 2} {:c 3 :d 4})
;;=> {:a 1, :b 2, :c 3, :d 4}
;;插入键值对
(conj {:a 1 :b 2} [:c 3])
;;=> {:a 1, :b 2, :c 3}
;;同时插入map和键值对
(conj {:a 1 :b 2} {:c 3 :d 4} [:e 5])
;;=> {:a 1, :b 2, :c 3, :d 4, :e 5}

;;cons函数返回的永远是一个序列,而且插入map和键值对的情况不同,所以建议尽量少用
(cons {:c 3} {:a 1 :b 2})
;;=> ({:c 3} [:a 1] [:b 2])
(cons [:c 3] {:a 1 :b 2})
;;=> ([:c 3] [:a 1] [:b 2])

4.删除元素相关函数:
  删除序列中的元素常用的函数有rest,pop,subvec,其中rest函数类似cons是序列操作函数,返回的是一个序列类型,而pop函数会保持原有数据类型不变,subvec函数对vector操作,返回原有类型,它创建的向量与原来的内部结构一样,非常有效,执行时间为常数,建议对于vector使用subvec函数:

  • pop函数:
;;pop函数
;;和conj函数类似,pop函数的删除元素的位置对于vector和list恰好相反
(pop [1 2 3])
;;=> [1 2]
(pop '(1 2 3))
;;=> (2 3)
;;对于空的序列,pop函数会抛出异常,所以使用一定要谨慎!!!!!
(pop [])
;;IllegalStateException Can't pop empty vector  clojure.lang.PersistentVector.pop 
  • rest函数:
;;rest函数与cons函数类似,都是删除序列头部元素,对于空序列,依然返回为空
(rest [1 2 3])
;;=> (2 3)
(rest '(1 2 3))
;;=> (2 3)
(rest '())
;;=> ()
;;这里想说一点,rest函数还可以用于map,而pop则不能,注意注意!!!!
(rest {:a 1 :b 2 :c 3})
;;=> ([:b 2] [:c 3])
  • subvec函数
;;subvec函数只能使用在vector中,它会获取从第m个到第n个的元素,但是不包括第n元素
(subvec [1 2 3] 0 2)
;;=> [1 2]
;;subvec不能越界操作,或者m大于n操作,会抛出异常
(subvec [1 2 3] 2 5)
;;IndexOutOfBoundsException

5.获取元素相关函数:
  在序列中获取元素常用的函数nth,take,take-nth,take-last,get函数。

  • nth函数:
      nth函数用于获取vector或者list中某个序号的元素:
;;这里的vector和list用法一样
(nth [1 2 3] 2)
;;=> 3
;;如果序号不在vector的范围内,会抛出异常
(nth [1 2 3] 4)
;;IndexOutOfBoundsException
;;如果序号不在范围内,可以给nth函数指定默认值,下面是指定默认值-1,这样我们可以对返回结果进行判断
(nth [1 2 3] 4 -1)
;;=> -1
  • get函数:
      get函数我们在clojure中的关联数据结构(http://blog.csdn.net/zdplife/article/details/52104888)这一文中已经讲过,get是个万能的函数,可以用于各种数据结构,而且不会报错,但是get用于list中是没有意义的,返回结果是nil,所以不要将get函数用于list:
;;get函数的用法在vector中与nth用法类似,只是如果下标超出范围,get函数不会报错,而是会返回nil,
(get [1 2 3] 1)
;;=> 2
;;当然,get函数也可以在找不到元素的情况下给其赋值默认值
(get [1 2 3] 4 -1)
;;=> -1
(get [1 2 3] 4)
;;=> nil

;;get用在list时,总会返回nil,所以不要使用
(get '(1 2 3) 1)
;;=> nil
  • take/take-nth/take-last函数:
      这三个函数也是获取序列中的元素,但是它们返回的是一个序列,take函数返回序列的前n个函数,take-nth是从序列的第一个元素起,每间隔k个元素顺序返回新的序列,take-last是返回序列最后几个元素:
(take 2 [1 2 3])
;;=> (1 2)
(take-nth 2 [1 2 3 4 5])
;;=> (1 3 5)
(take-last 2 [1 2 3 4 5])
;;=> (4 5)
;;这三个函数是序列化函数,所以返回的都是seq,即使在空的情况下也不会抛出异常
(take-last 2 [])
;;=> nil
(take-nth 2 [])
;;=> ()
(take 2 [])
;;=> ()
  • vector作为函数获取元素:
      当然,因为vector是关联数据结构,它可以像map一样当作函数,获取其中某个位置的元素:
([1 2 3] 2)
;;=> 3
  • 对于vector三种获取元素函数的区别:
    这里写图片描述

6.更改元素值相关函数:
  目前还没有发现可以更新list中某个位置元素的函数,因为vector是关联数据结构,可以使用map中的函数assoc/update来更新vector中的元素(key就是元素的index):

;;update函数第二个参数的范围必须从0到2
(update [1 2 3] 2 str)
;;=> [1 2 "3"]
;;assoc函数的第二个参数范围可以从0到3,如果是3,就相当于在末尾增加一个元素
(assoc [1 2 3] 2 "hello")
;;=> [1 2 "hello"]
;;这时的assoc与conj类似
(assoc [1 2 3] 3 "yes")
;;=> [1 2 3 "yes"]

7.判断序列相关函数:
  有时候我们需要判断某个序列是什么类型,可以使用vector?/list?/seq?这三个谓词函数进行判断,vector函数用于判断某个变量是不是clojure.lang.PersistentVector类型,而list?函数用于判断某个变量是不是clojure.lang.PersistentList类型,seq?函数用于是不是clojure.lang.PersistentList或者cons,lazyseq等类型,所以如果我们不关心其底层实现的话,使用seq?判断几乎总是比list?要好:

(vector? [1 2 3])
;;=> true

;;list?函数只用于判断list类型
(list? '(1 2 3))
;;=> true
;;因为rest函数返回惰性序列,所以使用list?会判断失败
(list? (rest [1 2 3]))
;;=> false

;;但是seq?对于惰性序列和list都作判断,所以seq?是很好的选择。
(seq? (rest [1 2 3]))
;;=> true
(seq? '(1 2 3))
;;=> true

  最后讲一下contain?函数,这个函数看上去貌似是判断某个元素是否存在序列中,这里大部分人会被误解,其实该函数只是针对关联数据结构中判断key是否存在,所以对于vector来说用处基本为零,只是为了判断vector的某个index是否存在而已。

8.函数效率问题:
  最后想要谈到的就是函数效率问题,在transient(http://blog.csdn.net/zdplife/article/details/52138512)那篇文章中介绍了,clojure中有些函数使用该数据特性可以提高效率,所以我们也尽量选择使用一些利用了transient特性的函数,尤其在效率要求比较高的工程中:

;;因为pop函数使用了transient数据结构,所以尽量使用第二种写法:
(vec (take 2 [1 2 3]))
;;=> [1 2]
(pop [1 2 3])
;;=> [1 2]
;;因为into函数使用了transient数据结构,所以尽量使用第二种写法:
(vec (concat [1 2] [3 4]))
;;=> [1 2 3 4]
(into [1 2] [3 4])
;;=> [1 2 3 4]

猜你喜欢

转载自blog.csdn.net/zdplife/article/details/52200402