泛函編程(7)-數據結構-List-折疊算法


     折疊算法是List的典型算法。通過折疊算法可以實現眾多函數組合(function composition)。所以折疊算法也是泛函編程里的基本組件(function combinator)。了解折疊算法的原理對了解泛函組合有着至關緊要的幫助。折疊算法又可分右折疊和左折疊。我們先從右折疊(foldRight)開始:

 foldRightfoldRight

從以上兩圖示可以得出對List(a,b,c)的右折疊算法:op(a,op(b,op(c,z))) 可以看出括號是從右開始的。計算方式如圖二:op(a,sub), sub是重復子樹,可以肯定要用遞歸算法。這里z代表了一個起始值。我們現在可以推算出foldRight的函數款式(function signature)了:

1 def foldRight[A,B](l: List[A], z: B)(op: (A,B) => B): B = l match { 2 case Nil => z 3 case Cons(h,t) => op(h,foldRight(t,z)(f)) 4 }

注意foldRight不是一個尾遞歸算法(tail recursive)。我們試着對一個List(1,2,3)進行操作,先來個加法: 

1 foldRight(List(1,2,3),0)((x,y) => x + y) //> res13: Int = 6 2 foldRight(List(1,2,3),0){_ + _} //> res14: Int = 6

我們可以用”等量替換“方法簡約:

1 // (List(x1,x2,x3...x{n-1}, xn) foldRight acc) op => x1 op (...(xn op acc)...) 2 // foldRight(Cons(1,Cons(2,Cons(3,Nil))), 0) {_ + _} 3 // 1 + foldRight(Cons(2,Cons(3,Nil)), 0) {_ + _} 4 // 1 + (2 + foldRight(Cons(3,Nil), 0) {_ + _}) 5 // 1 + (2 + (3 + foldRight(Nil, 0) {_ + _})) 6 // 1 + (2 + (3 + 0)) = 6
1 foldRight(List(1,2,3),1){_ * _} //> res16: Int = 6 2 foldRight(List(1,2,3),Nil:List[Int]) { (a,b) => Cons(a+10,b) }  //> res17: ch3.list.List[Int] = Cons(11,Cons(12,Cons(13,Nil)))

注意以上的起始值1和Nil:List[Int]。z的類型可以不是A,所以op的結果也有可能不是A類型,但在以上的加法和乘法的例子里z都是Int類型的。但在List重構例子里z是List[Int]類型,所以op的結果也是List[Int]類型的,這點要特別注意。

再來看看左折疊算法:

foldLeftfoldLeft

從以上圖示分析,左折疊算法就是所有List元素對z的操作op。從圖二可見,op對z,a操作后op的結果再作為z與b再進行op操作,如此循環。看來又是一個遞歸算法,而z就是一個用op累積的值了:op(op(op(z,a),b),c)。左折疊算法的括號是從左邊開始的。來看看foldLeft的實現:

1 def foldLeft[A,B](l: List[A], acc: B)(op: (B,A) => B): B = l match { 2 case Nil => acc 3 case Cons(h,t) => foldLeft(t,op(acc,h))(op) 4 }

注意z (zero) 變成了 acc (accumulator),op: (B,A) = B, 和foldRight的op函數入參順序是顛倒的。foldLeft是個尾遞歸方法。

1 foldLeft(List(1,2,3),0)((b,a) => a + b) //> res18: Int = 6 2 foldLeft(List(1,2,3),0){_ + _} //> res19: Int = 6 3 foldLeft(List(1,2,3),1)((b,a) => a * b) //> res20: Int = 6 4 foldLeft(List(1,2,3),1){_ * _} //> res21: Int = 6 5 foldLeft(List(1,2,3),Nil:List[Int]) { (b,a) => Cons(a+10,b) } 6 //> res22: ch3.list.List[Int] = Cons(13,Cons(12,Cons(11,Nil)))

以上加法和乘法的累積值acc都是A類型,但注意List重構的acc是List[Int]類型的,這個時候op入參的位置就很重要了。再注意一下,foldLeft重構的List的元素排列是反向的Cons(13,Cons(12,Cons(11,Nil))。我們還是可以用“等量替換”方法進行簡約:

1 // (List(x1,x2,x3...x{n-1}, xn) foldLeft acc) op => (...(acc op x1) op x2)...) op x{n-1}) op xn 2 // foldLeft(Cons(1,Cons(2,Cons(3,Nil))), 0) {_ + _} 3 // foldLeft(Cons(2,Cons(3,Nil)), (0 + 1)) {_ + _} 4 // foldLeft(Cons(3,Nil), ((0 + 1) + 2)) {_ + _} 5 // foldLeft(Nil, (((0 + 1) + 2) + 3)) {_ + _} 6 // (((0 + 1) + 2) + 3) + 0 = 6

除foldRight,foldLeft之外,折疊算法還包括了:reduceRight,reduceLeft,scanRight,scanLeft。

 reduceLeftreduceRight

reduceLeft是以第一個,reduceRight是以最后一個List元素作為起始值的折疊算法,沒有單獨的起始值:

1 def reduceLeft[A](l: List[A])(op: (A,A) => A): A = l match { 2 case Nil => sys.error("Empty list!") 3 case Cons(h,t) => foldLeft(t,h)(op) 4  } 5 def reduceRight[A](l: List[A])(op: (A,A) => A): A = l match { 6 case Cons(h,Nil) => h 7 case Cons(h,t) => op(h,reduceRight(t)(op)) 8 }
1 reduceLeft(List(1,2,3)) {_ + _} //> res23: Int = 6 2 reduceRight(List(1,2,3)) {_ + _} //> res24: Int = 6

scanLeft, scanRight 分別把每次op的結果插入新產生的List作為返回結果。

 先實現scanLeft:

1 def scanLeft[A](l: List[A],z: A)(op: (A,A) => A): List[A] = l match { 2 case Nil => Cons(z,Nil) 3 case Cons(h,t) => Cons(z,scanLeft(t,op(z,h))(op)) 4 }
1 scanLeft(List(1,2,3),0) {_ + _} //> res25: ch3.list.List[Int] = Cons(0,Cons(1,Cons(3,Cons(6,Nil))))

試試簡約:

 1 // (List(x1,x2,x3...x{n-1}, xn) scanLeft acc) op => (...(acc op x1) op x2)...) op x{n-1}) op xn  2 // scanLeft(Cons(1,Cons(2,Cons(3,Nil))), 0) {_ + _}  3 // Cons(0,scanLeft(Cons(1,Cons(2,Cons(3,Nil))), 0) {_ + _})  4 // Cons(0,Cons((0 + 1), scanLeft(Cons(2,Cons(3,Nil)), (0 + 1)) {_ + _}))  5 // ==> Cons(0,Cons(1,scanLeft(Cons(2,Cons(3,Nil)), 1) {_ + _}))  6 // Cons(0,Cons(1,Cons(2 + 1,scanLeft(Cons(3,Nil), 1 + 2) {_ + _})))  7 // ==> Cons(0,Cons(1,Cons(3,scanLeft(Cons(3,Nil), 3) {_ + _})))  8 // Cons(0,Cons(1,Cons(3,Cons(3 + 3,foldLeft(Nil, 3 + 3) {_ + _}))))  9 // ==> Cons(0,Cons(1,Cons(3,Cons(6,foldLeft(Nil, 6) {_ + _})))) 10 // Cons(0,Cons(1,Cons(3,Cons(6,Nil))))

再實現scanRight:

 1 def reverse[A](l: List[A]): List[A] = foldLeft(l,Nil:List[A]){(acc,h) => Cons(h,acc)}  2  3 def scanRight[A](l: List[A],z: A)(op: (A,A) => A): List[A] = {  4 var scanned = List(z)  5 var acc = z  6 var ll = reverse(l)  7 var x = z  8 while (  9  ll match { 10       case Nil => false 11      case Cons(h,t) => { x = h; ll = t; true } 12  } 13  ) { 14        acc = op(acc,x) 15       scanned = Cons(acc,scanned) 16  } 17  scanned 18 }

實在沒能想出用遞歸算法實現scanRight的方法,只能用while loop來解決了。注意雖然使用了臨時變量,但這些變量都是本地封閉的,所以scanRight還是純函數。scanRight元素遍歷(traverse)順序是反向的,所以用reverse函數把List(1,2,3)先變成List(3,2,1)。

 

1 scanRight(List(1,2,3),0) {_ + _} //> res26: ch3.list.List[Int] = Cons(6,Cons(5,Cons(3,Cons(0,Nil))))

注意scanRight和scanLeft的結果不同。這是因為算法不同:元素遍歷(traverse)順序不同。

下面開始示范一下折疊算法作為基本組件(combinator)來實現一些函數功能:

上次實現了函數++,即append。我們同樣可以用foldLeft和foldRight來實現:

1 def appendByFoldRight[A](l1: List[A], l2: List[A]): List[A] = foldRight(l1,l2){(h,acc) => Cons(h,acc)} 2 def appendByFoldLeft[A](l1: List[A], l2: List[A]): List[A] = foldLeft(reverse(l1),l2){(acc,h) => Cons(h,acc)}
1 appendByFoldLeft(List(1,2),List(3,4)) //> res27: ch3.list.List[Int] = Cons(1,Cons(2,Cons(3,Cons(4,Nil)))) 2 appendByFoldRight(List(1,2),List(3,4)) //> res28: ch3.list.List[Int] = Cons(1,Cons(2,Cons(3,Cons(4,Nil))))

由於append的功能是將兩個List拼接起來,必須保證最終結果List元素的順序。所以在appendByFoldLeft里使用了reverse。再注意foldLeft和foldRight在op參數位置是相反的。

之前遞歸算法實現的函數有些是可以用折疊算法實現的:

1 def map_1[A,B](l: List[A])(f: A => B): List[B] = foldRight(l,Nil: List[B]){(h,acc) => Cons(f(h),acc)}
1 def filter_1[A](l: List[A])(f: A => Boolean): List[A] = foldRight(l,Nil: List[A]){(h,acc) => if (f(h)) Cons(h,acc) else acc } 2 def flatMap_1[A,B](l: List[A])(f: A => List[B]): List[B] = foldRight(l,Nil: List[B]){(h,acc) => appendByFoldRight(f(h),acc)}
1 def lengthByFoldRight[A](l: List[A]): Int = foldRight(l,0){(_,acc) => acc + 1 } 2 def lengthByFoldLeft[A](l: List[A]): Int = foldLeft(l,0){(acc,_) => acc + 1 }


還有些比較間接的:

1 def conCat[A](ll: List[List[A]]): List[A] = foldRight(ll,Nil: List[A]){appendByFoldRight}

這個函數可以用來實現flatMap:

 

1 def flatMap_1[A,B](l: List[A])(f: A => List[B]): List[B] = conCat(map(l)(f))

如果理解以上函數實現方式有困難時可以先從類型匹配上下手,或者試着用“等量替換”方法簡約跟蹤一下。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM