此次來講說函數式的數據結構是什麼樣子的,本章會先用一個list來舉例子說明,最後給出一個Tree數據結構的練習,放在公衆號裏面,練習裏面給出了基本的結構,但代碼是空缺的須要補上,此外還有預留的testcase能夠驗證。html
關注公衆號:哈爾的數據城堡,回覆「函數式數據結構」能夠得到。(寫文章不容易,大哥大姐關注下吧[哭笑])java
而後是這系列的索引:算法
還記得前面說過,函數式編程最大的特色是什麼嗎?就是沒有反作用。那麼函數式的數據結構天然也是如此。app
無反作用的關鍵是:函數式編程
在java中,最經典的數據結構ArrayList,是經過一個全局的size變量,來控制ArrayList的大小的,這就說明ArrayList並不是無反作用。函數
在scala中,集合(List,Map等)默認是不可變的,以鏈表List爲例,是沒法經過push等操做,往一個鏈表裏面添加內容的。只能經過兩個鏈表相加的方式,生成一個新鏈表(Map也是同樣,經過兩個Map相加,Key相同的會覆蓋,以達到更新的目的)。這點卻是和String有點像。oop
不過其實這樣有一個問題,那就是很耗費內存。但這個問題能夠用懶加載來解決,限於篇幅,後面再介紹吧。
總結一下,函數式的數據結構,最大的特色,就是沒有反作用。那麼如何實現無反作用的數據結構呢,咱們下面用鏈表的例子來展現。
不過在這以前,須要先回顧下一些語法知識。
個人一個觀點是,語言的語法知識若是隻是看,背,而沒有實際用到,那是比較難記住的。這裏就把此次會用到的語法知識作個簡單介紹,若是有須要,能夠查閱前面寫的前兩章。
我這裏也有演示若是運用前面介紹的語法知識實現一個函數式的List()。
PS:若是不想看語法知識能夠直接跳到第三節。
前面的語法索引:
模式匹配相似於swtch語法,不過它能匹配的不止是值,還有數據類型。同時,它是一個匿名函數,在scala裏,函數不用return,能直接返回值。
val times = 1 //使用模式匹配來匹配值 times match { case 1 => "one" case 2 => "two" case _ => "some other number" } //使用模式匹配,匹配類型,再判斷值 times match { case i:Int if i == 1 => "one" case i:Int if i == 2 => "two" case _ => "some other number" }
若是有小夥伴想了解更多,能夠看看我這篇,scala模式匹配詳細解析。
前面介紹到,object是一個類的伴生對象,並且至關於static類,內存裏只能有一個對象。apply方法則是說,能夠在使用object對象的時候,直接默認使用。別說了,看代碼:
scala> class Foo {} defined class Foo //有一個apply方法 scala> object FooMaker { | def apply() = new Foo | } defined module FooMaker //新建object,自動得就調用了apply scala> val newFoo = FooMaker() //賦值的對象是Foo,由於調用了FooMaker()的apply newFoo: Foo = Foo@5b83f762
上面的代碼,FooMaker至關於一個工廠。
scala中的泛型,叫作型變或變性,英文叫variance。主要有三種狀況:
假設Dog是Animal的子類。那麼有以下關係:
協變是比較符合正常邏輯思考的,一羣狗確實也能夠說是一羣動物。逆變就比較反直覺了,不過這裏先不討論這點,後面有機會再討論。
OK,有了上面的基礎,就可以來構建一個函數式的數據結構了,不過在此以前,先讓咱們回顧下傳統的List數據結構。
還記得之前數據結構是怎樣設計的嗎?
最普通的鏈表,一般都是由一個又一個的Node組成,一個Node中存儲數據和下一個鏈表的變量。最後經過一個空值結尾,一般是Null。
在Java中,它的鏈表Linklist,是經過一個全局變量size來控制鏈表的。
經過for循環實現基礎的增刪查改等操做。而是,也是傳統List的常見寫法,但在函數式的List中可不能這樣。還記得嗎,函數式最大的特色就是無反作用。像java這裏用一個全局的size來控制,那可真是萬萬不可啊,在多線程的狀況下還不得崩潰。
關於爲何要寫無反作用的代碼,這裏就不作探討,詳細內容能夠看看這個系列的第一章。Scala函數式編程指南(一) 函數式思想介紹。
咱們要作的是寫出無反作用的集合,那要怎麼作呢?給5秒鐘閉上眼睛好好想想有沒有什麼思路。。。
可能有的同窗會想獲得,這個答案就是遞歸。經過遞歸,可以避免反作用的產生。如經常使用的增刪查改,若是使用遞歸,就能夠避免使用一個全局變量,固然遞歸一般都沒有直接使用for循環那麼直觀,因此充滿遞歸的代碼初次看會比較晦澀。但若是用多了,你會發現其實函數式的代碼,也是很是好懂的。
下面,咱們來看看若是使用遞歸實現一個List。
首先,咱們要定義每一個節點Node的類型,以及結尾Nil。因爲使用到了遞歸,咱們須要讓Node和Nil都有一樣的父類,由於遞歸函數的返回都是同樣的。
若是仍是不明白爲何要讓Node和Nil爲啥要有一樣的父類,那不妨先放一放,繼續看下去吧。
//定義本身的特質(至關於java的接口),泛型使用協變 sealed trait List[+A] //定義一個case類,做爲每個List的結尾 case object Nil extends List[Nothing] //定義List子類,也能夠說是List中的每一個Node,每一個List都是由一個又一個的Cons組成,以Nil結尾 case class Cons[+A](head: A, tail: List[A]) extends List[A]
注意第一行定義了List[+A]的特質,和scala集合中的List是區分開來的,只是名字叫同樣而已。這個是咱們本身的List!!
然後定義了Nil和Cons,分別做爲List的結尾和Node節點,注意case class也是scala的語法糖,能夠理解爲java bean。
之因此先定義了一個List的特質(接口),再分別用Nil和Cons繼承它,是由於在遞歸的狀況下,要讓節點和結尾保持同一類型,而這個就是經過多態實現的。
前面說到,一般是用object來做爲工廠,這裏也是同樣的,咱們能夠定義List工廠。
定義工廠方法以下:
object List { //使用可變長度,若是傳進來的參數是空,就返回Nil,不然使用遞歸返回Cons,注意,這裏的apply方法就是使用了遞歸 def apply[A](as: A*): List[A] = // Variadic function syntax if (as.isEmpty) Nil else Cons(as.head, apply(as.tail: _*)) }
這裏的applyA,括號裏面的A*的意思,是多個參數的意思,就是說能夠有不少個參數,是scala的一個語法糖。
在最後
else Cons(as.head, apply(as.tail: _*))
看到最後面的 _*了嗎,這個的意思,是除了第一個參數之外的其餘參數,也是語法糖。
在這一個小小的地方就用到了遞歸,不斷調用apply方法去解析後面的參數,最終生成一個List。初次看可能會比較迷,可能放在編譯器裏面運行一下,方便理解。而這種操做在scala函數式編程中,是很是廣泛的作法。
至此,咱們就創建了一個List的數據結構,先來看看咱們的成果
//一個遞歸的List scala> List(1,2,3) res0: List[Int] = Cons(1,Cons(2,Cons(3,Nil)))
如今的List數據結構只是初具雛形,咱們還得往裏面加方法。
一般來講,數據結構比較重要的是增刪查改等操做,但由於是不可變的,同時函數式中一般是不改變對象信息的,因此這些基本操做反而不是首要的。
咱們先來看一個簡單些的例子吧,讓一個List[Int]中的數據累加。
object List { ...... //傳入參數是一個Int類型的List,使用模式匹配 def sum(ints: List[Int]): Int = ints match { case Nil => 0 //使用遞歸累加 case Cons(x,xs) => x + sum(xs) } ...... }
這裏主要傳入的參數是一個Int類型的List,而後使用模式匹配,若是是結尾,則返回0,若是是中間節點,則使用遞歸累加。
上面那個例子比較簡單,明白後能夠來看看如何爲List構建更加通用的方法。一般比較經常使用的是前面介紹過的諸如map,filter等操做,下面先用一個map來講明一下吧。
object List { ...... //Map操做,使用模式匹配 def map[A,B](list: List[A],f: A => B): List[B] =list match { case Nil ⇒ Nil //使用遞歸 case Cons(head, tail) ⇒ Cons(f(head), map(tail,f)) } ...... }
map函數,須要傳進入一個待處理的list,以及一個函數做爲參數,用以對List中每一個元素作處理。
好比說想讓List中每一個元素+1,那就能夠傳入
val addOne = (num:Int) => num+1
還記得以前說,在scala中,函數也能看成變量嘛。將addOne這個函數做爲參數,這樣就會讓List中每一個元素都+1,而後返回一個新的List,固然,這個也是用遞歸實現的。
實現代碼看起來很簡潔,也是用模式匹配,匹配每一個元素的類型,就是是Node仍是結尾。若是是結尾,直接返回,若是是Node,那麼處理完當前數據,遞歸去處理後面的數據,並返回新的處理後的Node。
熟悉之後,會發現這樣的處理方式看着很舒服,代碼寫得也不多,很是簡潔。
在我看來,這就是遞歸的魅力所在。
除了map以外,還有其餘操做處理,包括filter,foldLeft,reduce等操做。我把代碼放在個人公衆號中,限於篇幅這裏就不講太多。關注公衆號:哈爾的數據城堡,回覆「函數式數據結」能夠得到。
代碼中使用了隱式轉換來擴充List的操做,並演示瞭如何使用隱式轉換,以及如何使用複用來組合功能以實現新的功能。有同窗可能不明白爲何簡單的List要搞這麼複雜,看了代碼可能會更加理解。
這部分我是做爲練習的,連同List代碼放在一塊,裏面有基本的結構,但一些缺失的內容須要你來補充。相信我,作了一遍,確定可以對函數式的數據結構有更深的理解。
對了,二叉搜索樹的練習還有幾個test case,作完跑一遍了,若是全過那基本上你寫的代碼就不會有太大的問題,good luck~
再說一遍我把練習的代碼放在了個人公衆號中,關注公衆號:哈爾的數據城堡,回覆「函數式數據結構」就能免費得到啦。
下一篇會再針對List和Tree的代碼來說一講,有不明白的地方到時候也能夠看看。
以上~~
推薦閱讀:
通俗得說線性迴歸算法(一)線性迴歸初步介紹
通俗得說線性迴歸算法(二)線性迴歸初步介紹
大數據存儲的進化史 --從 RAID 到 Hadoop Hdfs
C,java,Python,這些名字背後的江湖!