Master theorem是用來快速估計一個能寫成recurrence relation的演算法的running time,就是一個被證明的公式:
舉例來說,之前的naiive multiplication algorithm的recurrence relations如下,用master theorem分析之:
改良過的karatsuba乘法,將subproblem減少成只有三個,所以a = 3,用master theorem分析如下:
大約是O(n^1.58),這不是常數倍數的改良,是指數的改良,相當的好。
如果有人能把乘法演算法改成只有2個subproblem呢?事實上目前沒發現過,不過這個recurrence relations就是merge sort的特徵:
所以目前sorting最好的running time只能是O(nlogn)。
這個是什麼?沒錯,就是Binary search:
code
2016年11月22日 星期二
2016年11月21日 星期一
Algorithms筆記12 - Binary search using D&C strategy
Binary Search
Binary search是一個divide and conquer algorithm:binary search將一個problem分成兩個等分的subproblem,由於input是sorted array,所以每次都能刪除一個不可能得到答案的subproblem,等於每次problem size變成一半:
最後的combine所有subproblem的答案時,那些被捨棄掉的subproblem是空集合,所以最後一個subproblem中找到(或確定沒找到)的index就是整個母問題的解。
Worst-case Running time analysis
binary search最差的結果就是找不到這個數的index,如果寫成recurrence relation(recursive equations) 的話:最差都沒找到的話,總共做了 log_2(n) + 1次的midpoint check,以n = 4來說,總共找了n = 4, n = 2, n = 1,總共3次的midpoint check。所以這是theta(log(n))。
Algorithms筆記11 - Divide and Conquer
分而擊之
聽起來很酷,這種策略有以下幾個特徵:1. 分成許多一樣問題但是input size較小者:
一樣的subproblem很重要,跟greedy algorithm一樣,都必須是同類型的,這樣能解決小的,就能解決大的,例如把一個矩形面積拆成好幾個矩形來算:
但是不能拆成其他形狀,因為這就不是subproblem:
2. 各個擊破後,要把答案拼成原本大問題的答案,這是一個很大的特徵,所以除了切成不同size的subproblem之外,這些subproblems必須要剛好能組合成母問題答案,所以以下的分法是錯的,因為subproblems重疊了:
3. 根據1. 的特性,自然會形成recursive algorithms。
一個D&C linear search例子
這個例子其實沒有什麼divide and conquer 的好處,因為每個subproblem只是原本的input size - 1。這邊是用來解釋如何估算running time:
每次recursion level都做了O(1)的事情,最差會總共做了n次,所以是O(n),或更精確地說是theta(n)。
2016年11月19日 星期六
Scala筆記 30 - Loops
While Loop
mutable variable已經能做到所有imperative programming的事,現在要介紹control structure: loop。在Scala中,while是一個key word,但事實上我們也可以用pure functional的方式來定義while:
def WHILE(condition: => Boolean)(command: => Unit): Unit = if (condition) { command WHILE(condition)(command) } else ()
注意condition要call by name,這樣才能在每次deference的時候重新被evaluate,要不然真的變成無窮迴圈了。另外這是一個tail recursion,所以是constant stack size,跟imperative programming中的while一樣。
我們也可以模擬出do-while (但是這邊是while condition is not met),課堂中稱為repeat:
def REPEAT(condition: => Boolean)(command: => Unit): Unit = { command if (!condition) REPEAT(condition)(command) } var x = 1REPEAT( x >= 5 ) { x = x + 1; println(x) }
另外模擬repeat-untill,要能寫出以下這種形式:
REPEAT {
command
} UNTIL ( condition )
神奇,我不會做!想一下之後補上!
For Loop
之前已經用過for expression,這實際上是collection中的foreach method來implement的:Scala筆記 29 - stateful objects
State in Scala
scala不是純粹的functional language,他可以有state。所謂的state就是一個變數值它會隨著執行的歷史而改變。此時substitution model就不能適用,因為一個變數值在每個時間點可能不一樣。如果要宣告一個可以改變的變數,用var而非val keyword來宣告。如果一個object的狀態會隨著history改變的話,就是一個stateful object。
在Stream中用mutable variable實作lazy val
之前在筆記25我們說Stream可以是以下的implementation:def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] { def head = hd lazy val tail = tl ... }
我們宣告tail為lazy val(compiler會儲存evaluation,免得stream每次要用到tail就要重新evaluate tail一次(原本的non lazy-val版本如下):
def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] { def isEmpty = false def head = hd def tail = tl }
我們可以把lazy val的tail改成var來宣告:
def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] { override def head = hd private var tlOpt: Option[Stream[T]] = None override def tail: Stream[T] = tlOpt match { case Some(x) => x case None => tlOpt = Some(tl); tail } }
可以看到tail method是一個option,如果已經有值了(case Some(x)),那就回傳那個tail Stream,否則就去evaluate tl參數,將mutable variable tlOpt重新assign Some(tl),並且回傳重新呼叫自己tail method,此時不會進入recursion,因為tlOpt的match type已經改變成Some(x)了。
所以在以上例子可以看到,mutable variable可以用來實作lazy val。
stateful objects的等值與等效
我們之前非常容易判斷兩個value是不是等值,只要看其定義的expression是否一樣,所以可以用substitution model來重寫expression。但是在stateful objects就無法了,因為每個object的state不一樣的話,即便定義一樣,也不能說兩個instance是同一個。但是兩個不同的instances,如果所有可能的不同順序的operations都能產生一樣的效果,可以說他們是operational equivalence,等效。
2016年11月18日 星期五
Algorithm筆記 10.5 - greedy演算法練習: 將一數拆成找出最多相異數字
如果你也在修Coursera Algorithm Toolbox的話,這是解答!請斟酌使用
問題定義
如何將某數n拆成最多個相異正整數?例如6可以拆成{1,2,3},10可以拆成{1,2,3,4}。看起來可以提出一個safe move的假設,“永遠從目前最小的未選過的正整數開始”。
證明safe move
假設一數n,其optimal solution為拆成m個數 n = k1 + k2 + k3 + ... + km,且a < k1 < k2 < ... < km。令 x = k1 - a。我們很輕易可以改寫成以下:
n = (k1-x) + k2 + ... + (km+x),也是一個optimal solution,因為k1-x = a < k2 < ... <km < km+x,都是相異自然數,故此策略為safe move
實作
選取最小正整數a有一個條件,就是n' = n-a >= a+1 = a',這樣在下一個subproblem中最小能選的數a'才能小於等於n',才有解。舉例來說,8要怎麼拆成最多個正整數相加?按照我們演算法:
8-1 = 7, (7 >= 2)
7-2 = 5, (5 >= 3)
5-5 = 0, (分配完畢)
private static List<Integer> optimalSummands(int n) { List<Integer> summands = new ArrayList<Integer>(); //find the min int min = 1; while(n != 0) { int a = n - min; if (a == 0 || a >= min+1) { summands.add(min); n = a; } min = min + 1; } //write your code here return summands; }
running time為O(n)。
2016年11月15日 星期二
Algorithm筆記 10.4 - greedy演算法練習: 最少停留點問題
如果你也在修Coursera Algorithm Toolbox的話,這是解答!請斟酌使用
問題定義
有一棟建築物住了n個人,每個人在家的時間都不一樣(可能有重疊),如何能最少次抵達此建築物而能拜訪所有人?這個問題可以map成數線上的線段和點,假設有3個人,在家的時段分別為(1點~3點), (2點~5點), (3點~6點),則在數線上表示
0 [1 [2 [3] 4 5] 6] 7 8 9 10 ....
由上圖來推論,我們可以假設safe move為“從最左邊的區間終點a找起(上圖為3),包含a的區間都能用a來表示可以拜訪的時段”。
證明safe move
假設某optimal solution中,最左邊的區間終點y屬於線段(x,y),且y不屬於任何線段的代表點:1. 某線段(w,z)終點z >= y,且此線段包含y,可以用y表示,則optimal solution會減少1。
_ _ (x _ _ _ y) _ _ z) _
2. 某線段終點z <=y,不成立,因為y是最左邊的區間終點。
3. 某線段(w,z) 終點z > y,且w > y,y不能代表(w,z),不改變optimal solutoin。
以上三種情況包含了任意線段與y所有可能的關係,故根據1. 選擇y為代表點是符合optimal solution,此策略為safe move。
演算法
private static int[] optimalPoints(Segment[] segments) { ArrayList<Integer> result = new ArrayList<>(); //O(nlogn) sort segments ascendingly based on end point Arrays.sort(segments, new Comparator<Segment>() { @Override public int compare(Segment o1, Segment o2) { return o1.end - o2.end; } }); //O(n) int leftmostEnd = segments[0].end; for(int i = 0; i < segments.length; i++) { if (segments[i].contains(leftmostEnd)) continue; else { result.add(leftmostEnd); leftmostEnd = segments[i].end; } } //add the last match result.add(leftmostEnd); int[] r = new int[result.size()]; for (int i = 0; i < r.length; i++) r[i] = result.get(i); return r; }
Algorithm筆記 10.2 - greedy演算法練習: 最大化dot-product
如果你也在修Coursera Algorithm Toolbox的話,這是解答!請斟酌使用
問題定義
給兩個整數vector v1 = {a1,a2, ... , an} , v2 = {b1, b2, ... , bn},找出最大的dot product?按照greedy algorithm步驟,我們要先找一個safe move,並且證明其為某個optimal solution的第一步。依照題目,我們假設“v1和v2從大到小排序後所得的dot product為optimal”。
證明safe move
假設一optimal solution w = {a1*b1 + a2*b2 +.. + aj*bj + .... + ak*bk + .... + an*bn},且ak = max(a1, a2,..,an), bj = max(b1, b2, ...,bn)。如果w' = 把w中的 aj*bj 和 ak*bk 交換成 ak*bj和 aj*bk,則w' - w = ak*bj+aj*bk - aj*bj - ak*bk = ak*(bj-bk) + aj*(bk-bj) = (ak-aj)*(bj-bk) > 0,因為ak 和 bj分別是兩個數列中的最大值。
演算法
注意要integer相乘可能會oveflow,先cast成為long
private static long maxDotProduct(int[] a, int[] b) { Integer[] A = new Integer[a.length]; Integer[] B = new Integer[a.length]; //O(n) for (int i = 0; i < a.length; i++) { A[i] = a[i]; B[i] = b[i]; } //O(nlogn) Arrays.sort(A, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); //O(nlogn) Arrays.sort(B, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); //O(n) long result = 0; for (int i = 0; i < a.length; i++) { result += (long)A[i] * (long)B[i]; } return result; }
2016年11月14日 星期一
Algorithm筆記 10.3 - greedy演算法練習: 注意眉角
如果你也在修Coursera Algorithm Toolbox的話,這是解答!請斟酌使用
public class FractionalKnapsack { private static double getOptimalValue(int capacity, int[] values, int[] weights) { double value = 0; class Item { public int value; public int weight; public double valuesPerWeight; public Item(int value, int weight) { this.value = value; this.weight = weight; this.valuesPerWeight = (double)value / (double)weight; } } //O(n) initalization Item[] items = new Item[values.length]; for (int i = 0; i < items.length; i++) { items[i] = new Item(values[i], weights[i]); } //O(nlogn) sort Arrays.sort(items, new Comparator<Item>() { @Override public int compare(Item o1, Item o2) { if (o2.valuesPerWeight - o1.valuesPerWeight > 0) return 1; else if (o2.valuesPerWeight - o1.valuesPerWeight < 0) return -1; else return 0; } }); for(int i = 0; i < items.length; i++) { double load = Math.min((double)capacity, (double)items[i].weight); if (load >= items[i].weight) { //use all items[i] value += items[i].value; capacity -= load; } else { //use fracation of items[i] value += load * items[i].valuesPerWeight; return value; } } return value; } public static void stressTest() { while (true) { int n = ThreadLocalRandom.current().nextInt(1, 1000); int capacity = ThreadLocalRandom.current().nextInt(0, 2000000); int[] values = new int[n]; int[] weights = new int[n]; for (int i = 0; i < n; i++) { values[i] = ThreadLocalRandom.current().nextInt(0, 2000000); weights[i] = ThreadLocalRandom.current().nextInt(0, 2000000); } System.out.println(getOptimalValue(capacity, values, weights)); } } public static void main(String args[]) { Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); int capacity = scanner.nextInt(); int[] values = new int[n]; int[] weights = new int[n]; for (int i = 0; i < n; i++) { values[i] = scanner.nextInt(); weights[i] = scanner.nextInt(); } System.out.println(getOptimalValue(capacity, values, weights)); //stressTest(); }
Algorithm筆記 10.1 - greedy演算法練習: 找零問題
如果你也在修Coursera Algorithm Toolbox的話,這是解答!請斟酌使用
問題定義
給一個金額n,怎麼找出最少的零錢幣數目?限定幣值為{1, 5, 10}。按照greedy algorithm步驟,我們要先找一個safe move,並且證明其為某個optimal solution的第一步。依照題目,我們假設“盡量先找幣值大的零錢是一個safe move”。
證明safe move
假設某個optimal solution = w的硬幣中有x個一元硬幣,y個5元硬幣,z個10元硬幣,如果x >= 5,我們可以拿出5個一元硬幣換成1個5元硬幣,所以optimal solution會變成w - 5 + 1 = w - 4,同理把2個y硬幣換成1個z硬幣的話,w值也會變少,得證。演算法
private static int getChange(int m) { //write your code here int[] denominations = {10, 5, 1}; int count = 0; for (int i = 0; i < denominations.length; i++) { int coins = m / denominations[i]; count += coins; m = m - coins*denominations[i]; if (m == 0) return count; } return count; }
這是O(n),因為我們人工去排序了幣值了。
Algorithm筆記 9 - Knapsack演算法 (maximization problem)
問題定義
假設有個set = {(x1,y1), (x2,y2), ... (xn, yn)},我們想要取某個subset使得其中的元素的 y值相加的和最大,前提是x值相加的和不能超過W。x值可以只取部分,例如x1/2, x2/3, ....
以裝袋為例
一個袋子最多只能裝7個units的物體,我們有三個物體分別是(4 unit, $20), (3 unit, $18), (7 unit, $14),試問要怎麼最大化裝入袋子的錢?
這個knapsack問題已經被證明可以用greedy algorithm找出最佳解:
其safe move就是“優先使用單位額度($/unit)最高者”。
證明Safe move
我們還是要走過一遍證明過程,才能對以上的lemma產生信心對吧?假設我們有一個optimal solution,裡面至少裝入某些非最高單位額度的物體X。
如果我們把袋中的X取出一單位,放入最高單位額度的物體Y的一單位,則此袋子的總體額度增加了,所以原本的optimal solution並不是optimal solution,且必定至少包含一單位的Y。
我們如果對剩下的物體們的單位一一抽換成Y,此袋子的總體額度就會繼續增加,直到所有袋子中的物體被替換完成,或是Y被完全使用為止。若是後者,則次高單位額度的物體Z也遵從以上的邏輯,每次替換成Z都會增加總體額度。
得證。
演算法pseudocode和running time
上面的implementation是O(n^2),因為外層有一個for n loop,內層要找max單位額度的時候,又要看過所有的Wi,所以是O(n),這個很明顯有改善空間。
如果先把 vi/wi算出來並且排序(最好是O(nlogn)),就可以免去loop body中的linear search,所以knapsack algorithm最後會是O(nlogn) + O(n) = O(nlogn)。
2016年11月13日 星期日
Scala筆記 28 - 用Streams解決Search Problem,以倒水問題為例
Search Problem
所謂search problem就是在一個問題的可能解決地圖中,找到一條解決路徑,最好也是optimal的解決路徑,Scala課程中以倒水問題為例,而功課則要學生找出Bloxorz這個遊戲的解決路徑。倒水問題如下:
有n個已知容量的空杯子,試求最少步驟能裝出x容量的水?我們能對杯子做的動作有:
1. 倒滿
2. 清空
3. 從a杯倒到b杯
Modeling States
這個跟我之前上Berkely AI課程的小精靈問題一樣,我們先把n個空杯子用一個Vector[Int]來代表,任何Vector[Int]可能的值就是一個state,並且先列出所有可以走的actions組合,因為待會就要在search space中利用這些actions來建立search path:class Pouring(capacity: Vector[Int]) { //各種容量的杯子 //states type State = Vector[Int] val initalState = capacity map (_ => 0) //一開始所有杯子是空的 //move types trait Move case class Empty(glass: Int) extends Move case class Fill(glass: Int) extends Move case class Pour(from: Int, to: Int) extends Move //all indices val glasses = 0 until capacity.length //all possible moves/actions val moves = (for(g <- glasses) yield Empty(g)) ++ (for(g <- glasses) yield Fill(g)) ++ (for(g1 <- glasses; g2 <- glasses; if g1 != g2) yield Pour(g1,g2)) }
Modeling Moves
我們會從initial state開始長出三個可能的move path,分別是Empty, Fill, Pour,所以我們還需要知道從某個state經過一個move之後,會變成哪一個state? 我們在trait Move中加入一個change method:(這邊的邏輯比較不直覺,我會寫成State有一個change(move:Move)的方法,而不是在Move class中寫一個change(state:State))//move types trait Move { def change(state:State):State } case class Empty(glass: Int) extends Move { override def change(state: State): State = state updated(glass, 0) } case class Fill(glass: Int) extends Move { override def change(state: State): State = state updated(glass, capacity(glass)) } case class Pour(from: Int, to: Int) extends Move{ override def change(state: State): State = { val amount = math.min( state(from), capacity(to) - state(to) ) (state updated(from, state(from)-amount)) updated (to, state(to) + amount) } }
collection的updated method是一個immutable method,會產生一個新的collection,但是在某個index的value不同而已。
Modeling Paths
我們接著要來model path,某一條path其實就是每次move的歷史,我們把最新的move放在list head,有了這個history,我們可以利用每個move間的change來計算出最後的end state://Pathsclass Path(history:List[Move]) {def extend(move:Move) = (new Path(move::history))def endState: State = trackState(history) private def trackState(xs: List[Move]): State = xs match { case Nil => initalState case move::xs1 => move change trackState(xs1) } }override def toString = (history.reverse mkString(" ")) + "-->" + endState
val initialPath = new Path(Nil)
上面trackState會先recursively抵達Nil,然後把initialState當作參數傳給第一個move的change method:
(lastMove change ...... (move2 chage (move1 change initialState)))
所以這是一個foldRight operation,我們可以把endState改寫成:
def endState: State = history.foldRight(initalState)( _ change _ )
Search space exploration
我們有了所有的models,現在可以開始做Breadth-first search。BFS search的過程類似從洋蔥的核心(initalState),一層層往外extend出同樣長度的path,對洋蔥中的某一層來說,已經長出了一些paths,接下來要長出下一層,這邊就要用到Streams:def from(paths:Set[Path]): Stream[Set[Path]] = if (paths.isEmpty) Stream.empty else { val more:Set[Path] = for { path:Path <- paths extendedPath:Path <- moves map(x => path.extend(x)) } yield extendedPath paths #:: from(more) //union the extended paths } val pathSets = from(Set(initialPath))
用set的原因是,重複的path對我們來說沒有意義。用stream的原因則是要控制exploration深度,如果用list的話,必須要加上搜尋深度的限制才行,否則變成無窮迴圈。
開始搜尋解答
假設有兩個容量為4和6的空瓶:val problem = new Pouring(Vector(4,6)) problem.moves
所有的可能moves如下:
res0: IndexedSeq[problem.Move] = Vector(Empty(0), Empty(1), Fill(0), Fill(1), Pour(0,1), Pour(1,0))
搜尋一層可能的paths,problem.pathSets.take(1).toList
res1: List[Set[problem.Path]] = List(Set(-->Vector(0, 0)))
搜尋二層可能的paths,problem.pathSets.take(2).toList
res1: List[Set[problem.Path]] = List(
Set(-->Vector(0, 0)),
Set(
Pour(0,1)-->Vector(0, 0),
Fill(0)-->Vector(4, 0),
Empty(1)-->Vector(0, 0),
Empty(0)-->Vector(0, 0),
Pour(1,0)-->Vector(0, 0),
Fill(1)-->Vector(0, 6))
)
我們現在可以寫這個倒水問題的solution method:
def solution(target:Int): Stream[Path] = for { pathSet <- pathSets path <- pathSet if path.endState contains target } yield path
這個邏輯就是stream神奇的地方,pathSets是一個Stream[Set[Path]] type,本來應該是不會evaluate,但是由於stream的head是strict evaluation,所以head一定會evaluate,直到遇到第一個滿足if條件為止,此時stream就不會在evaluate下去。
深度不能確定是多少,要看滿足第一個if的path何時出現。This is not an optimized solution, might be very slow and not optimal!
total source code for now:
class Pouring(capacity: Vector[Int]) { //各種容量的杯子 //states type State = Vector[Int] val initalState = capacity map (_ => 0) //一開始所有杯子是空的 //move types trait Move { def change(state:State):State } case class Empty(glass: Int) extends Move { override def change(state: State): State = state updated(glass, 0) } case class Fill(glass: Int) extends Move { override def change(state: State): State = state updated(glass, capacity(glass)) } case class Pour(from: Int, to: Int) extends Move{ override def change(state: State): State = { val amount = math.min( state(from), capacity(to) - state(to) ) (state updated(from, state(from)-amount)) updated (to, state(to) + amount) } } //all indices val glasses = 0 until capacity.length //all possible moves/actions val moves:IndexedSeq[Move] = (for(g <- glasses) yield Empty(g)) ++ (for(g <- glasses) yield Fill(g)) ++ (for(g1 <- glasses; g2 <- glasses; if g1 != g2)
yield Pour(g1,g2)) //Paths (我們記錄所有的moves,List head是最後的move) class Path(history:List[Move]) { def extend(move:Move) = (new Path(move::history)) def endState: State = history.foldRight(initalState)( _ change _ ) override def toString = (history.reverse mkString(" ")) + "-->" + endState } val initialPath = new Path(Nil) def from(paths:Set[Path]): Stream[Set[Path]] = if (paths.isEmpty) Stream.empty
else { val more:Set[Path] = for { path:Path <- paths extendedPath:Path <- moves map(x => path.extend(x)) } yield extendedPath paths #:: from(more) //union the extended paths } val pathSets = from(Set(initialPath)) def solution(target:Int): Stream[Path] = for { pathSet <- pathSets path <- pathSet if path.endState contains target } yield path }
2016年11月11日 星期五
Scala筆記 27 - 重寫牛頓法開平方根,利用Streams
利用Streams重寫牛頓法開平方根
在筆記4中,我們之前嘗試做了牛頓法來開平方根,基本上是一個不斷收斂的過程,是否收斂是利用isGoodEnough function來檢驗:如果改成Stream版本非常簡潔:
def sqrtStream(x: Double): Stream[Double] = { def improve(guess: Double) = (guess + x / guess) / 2 lazy val guesses: Stream[Double] = { println("eval") 1 #:: (guesses map improve) } guesses }
sqrtStream(2).take(10).toList
但是guesses的定義相當奇怪,它是一個stream,從1開始去append到遞迴定義的tail Stream,而這個tail Stream就是把guess Stream中所有的數字都去做improve,有點難懂。嘗試來解譯上面的expression怎麼evaluate的:
首先直到toList被evaluate,guesses才會被evaluate,且只要evaluate stream中的前十個items:
itreation 1
此時guesses是的head是1,剩下的tail stream的定義是(guesses map improve)
iteration 2
此時要evaluate iteration 1中的tail stream的head,也就是
improve(1) = (1 + 2 / 1) / 2
剩下的tail stream 為head = improve(1)的stream
iteration 3
此時要evaluate iteration 2中的tail stream的head,也就是
improve( improve(1) )
.
.
所以 sqrtStream(2).take(10).toList
相當於精準控制了iteration做了10次,不用去管convergence的問題。
這個範例讓我感覺如果寫一個recursive stream定義的話,這個stream被take幾次相當于對其items重新加上幾次的recursive定義,所以第二個item被定義成recursion 1次,第三個item被定義成recursion 2次,以此類推。
在此例中,guesses也可以用def keyword定義成function,但是 "eval"字串被印出了10次,而lazy val宣告的guesses只會印出一次。我猜測由於stream本身是lazy val的關係,def版本的guesses並不會多做其他的事情,不過不知道怎麼驗證這件事。
Scala筆記 26 - 無限長度的Streams!
無限制長度的Stream
有了lazy evaluation這個工具,我們可以寫出無限長度的數列Stream,我是把它看成一種,例如自然數Stream:def nats(from:Int) :Stream[Int] = Stream.cons(from, nats(from+1)) nats(1).take(10).toList //res0: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
上面的expression只有當toList被evaluate時,才會去evalute nats的每個item。所以take(10)會開始evaluate nats(1)這個Stream的前十個item:
Streams.cons(1, nats(2)) //1st item is 1
Steams.cons(2, nats(3)) //2nd item is 2
.
.
Streams.cons(10, nats(11)) //10th item is 10
停止evaluation
有了這個自然數列,我們可以根據某個演算法來計算出一個Prime Stream。此古老演算法檢視所有的自然數,每遇到一個新的自然數,就把它的倍數從自然數列中去除掉,非常天真的演算法,但是符合Streams特性:
def sievePrimes(nats:Stream[Int]) : Stream[Int] = { nats.head #:: sievePrimes( nats filter(x => x% nats.head != 0 )) } sievePrimes(nats(2)).take(10).toList //res1: List[Int] = List(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)
Scala筆記 25 - Lazy Evaluation
三種evaluation modes
某個定義在使用到的時候才會被evaluate,並且儲存已經evaluate過的結果,所以不會重複evaluate同一個定義,這稱作lazy evaluation:lazy val y = { print(”y”); 2 }
如果每次使用都會被evaluate一次,稱為by-name evaluation,這就是一般call by reference的參數:
def z = 4 //z: z[] => Int,這就是一個function z //res0: Int = 4,此時才被evaluate
最後一種就是strict evaluation,只要宣告時就馬上evaluate:
val x = { print(”x”); 1 }
之前的Stream cons的定義用到了by-name evaluation,這會變得非常慢,可以改成lazy evaluation就能避免這個問題,但是就會用到額外的記憶體空間:
def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] { def head = hd lazy val tail = tl ... }
Haskell default是使用lazy evaluation,但是Scala因為有mutable data types,所以怕造成不可預測的混亂,提供了optional的lazy evaluation。
所以以下的expr,如果被evaluate的話:
def expr = { val x = { print(”x”); 1 } lazy val y = { print(”y”); 2 } def z = { print(”z”); 3 } z + y + x + z + y + x }
expr
1. 印出x,並且x被assigned成1
2. y不被evaluete,因為沒人用到它
3. z不被evaluate,因為沒人用到它
4. 開始evaluate z + y + x + z + y + x
5. evaluate z,印出z,z被mapped成3
6. evaluate y,印出y,且y被assigned成2,總共3+2
7. 取出x值,總共3+2+1
8. evaluate z,印出z,總共3+2+1+3
9. 取出y值,總共3+2+1+3+2
10. 取出x值,總共3+2+1+3+2+1
執行結果印出 xzyz,然後expr被evaluate成12
Scala筆記 24 - Streams
lazy evaluate的Stream
Streams是個有趣的資料結構,動態性的evaluate list的tail,我在之前用過的語言Java, C, C++都未曾遇過,也有可能我鑽研不夠深入,或許現在有了?我知道Java 8有stream,不過不確定跟Scala這個stream相不相同。Streams跟List幾乎是一樣的使用方式,差只差在constructor中的tail list是call by reference:
val xs = Stream.cons(1, Stream.cons(2, Stream.empty))
有趣的是看REPL的type是
xs: Stream.Cons[Int] = Stream(1, ?)
如果寫一個range function,stream的版本如下:
def streamRange(lo: Int, hi: Int): Stream[Int] = if (lo >= hi) Stream.empty
else Stream.cons(lo, streamRange(lo + 1, hi))
這跟我們一般會寫的list版本一模一樣:
def listRange(lo: Int, hi: Int): List[Int] = if (lo >= hi) Nil
else lo :: listRange(lo + 1, hi)
Stream怎麼implement出來的?
stream是implement這個trait:
trait Stream[+A] extends Seq[A] { def isEmpty: Boolean def head: A def tail: Stream[A] ... }
跟list一樣,只要定義了以上三個methods,其他的所有methods都能建築其上。
它的singleton companion object:
object Stream { def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] { def isEmpty = false def head = hd def tail = tl } val empty = new Stream[Nothing] { def isEmpty = true def head = throw new NoSuchElementException(”empty.head”) def tail = throw new NoSuchElementException(”empty.tail”) } }
可以看到cons中的tl參數是call by reference,所以只有在tail method被呼叫到的時候才會evaulate。
所以如果我們執行以下的function:
def streamRange(lo: Int, hi: Int): Stream[Int] = { print(lo+” ”) if (lo >= hi) Stream.empty else Stream.cons(lo, streamRange(lo + 1, hi)) }
streamRange(1, 10).take(3).toList
實際上會印出123,因為toList會真正要evaluate整個Stream,並且轉換成list。
Scala筆記 23 - 用for做random value generator
用trait做random value generator
這是常見的一種架構,先定義一個trait:trait Generator[T] { def generate : T}
然後藉由實做這個trait得出各種不同type的random value generators:
val integers = new Generator[Int] { override def generate: Int = new Random().nextInt() } val booleans = new Generator[Boolean] { override def generate: Boolean = integers.generate > 0} val pairs = new Generator[(Int,Int)] { override def generate: (Int, Int) = (integers.generate, integers.generate) }
這樣定義ok, 但是使用上很麻煩,每次延伸一個新的種類,都要new一個generator出來,能不能不要用這種方式呢?
嘗試用for試試看
我們希望達到的類似這樣的架構來定義新的種類的generators (所以for statment中的generators不限於collection type, 而是某個有implement map的object都可):根據我們之前在解讀for的筆記中知道,compiler會把以上的for statement解譯成以下:
所以我們只要幫我們的random generator 實作map, flatmap就可以用for來使用。
我們的Generator trait新增了兩個 methods:
trait Generator[T] { self => //相當於 Generator.this
def generate : T //產生random value //回傳一個Generator[S],
//其generate的定義為把self產生的random value經過f mapping def map[S](f:T => S):Generator[S] = new Generator[S] { override def generate: S = f(self.generate) } //回傳一個Generator[S],
//其generate的定義為把self產生的random value經過f map到 Generator[S]的instance
//然後回傳此generator的generate method
def flatMap[S](f: T => Generator[S]): Generator[S] = new Generator[S] {
def generate : S = f(self.generate).generate } }
如何解讀新的Generator?
如果用這個Generator trait的定義來實作booleans的隨機產生器的話,可以看到以下的substitution steps:所以booleans被替換成跟我們在上一段的寫法一樣。
why all the efforts?!
解讀用for定義pairs的generator:
好難懂!!!!! 以後有遇到需求再回來研究吧!
訂閱:
文章 (Atom)