The two most important days in your life are the day you were born and the day you find out why. -Mark Twain

#53 Coding 面試- LeetCode #143 Reorder List

題目的網址 https://leetcode.com/problems/reorder-list/

這題跟一般的 List 考題一樣,都是把 List element 做順序上的改變.這題的順序改變也比較特別,因為是一前一後一前一後的順序.

一般來說,List 考這種順序排列的考題時,通常都不希望你用到額外的記憶體空間,也就是說所有的動作都要在原來的 List 空間上完成.記得當初在寫這題時,花了不少時間在想要怎麼排出新順序,後來才想到把 List 對折,這樣就變成第一個對到最後一個,第二個對到最後第二個,以此類推,中間的過程也要將成雙成對的配對中把前一對的尾色連到後一對的頭,這樣就完成了.考這種題目真的蠻討論的,因為要很快想到折一半再成雙成對的搭配起來還真不容易.然而,List 的題目就是這樣,只要想法正確了,寫程式就不是問題了.



以上的解決,Space complexity 是 O(1) ,而 Time complexity 是 O(n)


Share:

#52 物件導向程式設計的一個小技巧 - 切開 dependency

這次放長假回到台灣,到台中參加了一場 Study4TW 社群舉辦的活動.在活動中有個問題時間,讓現場的朋友可以提問問題,我則盡量回答我所知道的.其中有一個問題是 "台灣存在著多數不願意跟上時代變化傳統產業, 若不考慮重構, 開發人員該如何面對舊版本的軟體設計呢?" 這個問題的確不是那麼容易可以完整地回答,我當時的回答是跟大家說至少可以先從降低 dependency 的動作開始.所以,這篇文章就來說明我所謂的降低 dependency 是什麼 !

我們在寫物件導向程式時,一定會常常呼叫到其他人寫的程式,也就是說你的程式裡一定會建立一個別人程式的 instance. 例如,你的程式裡有一個 class 叫 DataWriterHelper ,裡面有一個 WriteSecret(),這方法裡面的內容其實是透過其他人寫的程式來達成.假設其他人寫的 class  叫 SecretHelper,而裡面有一個 Write().所以你的程式一開始看起來如下:

class DataWriterHelper {

    public void WriteSecret(string data) {
        SecretHelper _helper = new SecretHelper();
        _helper.Write(data);
    }
}

這樣子就可以很清楚的看到你的程式 DataWriterHelper 和別人的程式 SecretHelper 有一個關係了,換句話說,你的 DataWriterHelper 依賴了 SecretHelper,因為若 SecretHelper 不存在的話,你的 WriteSecret() 便發揮不了功能.

但這樣寫,有什麼不對嗎 ? 好像沒什麼不對,只是失去了一些彈性.如果 SecretHelper 改了一個版本,把 Write() 改成 WriteData(),那麼你的程式也就必須跟著變動.另外,當你要測試 WriteSecret() 時,你會發現你非得把 SecretHelper 的元件也一起加入到測試專案才行,因為你的程式依賴著 SecretHelper.如果你將它 Mock,也是一種可行的方法,只怕真實情況不是一個 Mock 就能滿足你的需要.

接下來,說明什麼叫降低 dependency. 降低的方法可以用一個神奇的東西 - Interface. 首先,先定義好 Interface 的內容.

interface IWriteSecret {
    void WriteSecret(string data);
}

這個 Interface 定義好之後,理論上就應該不會輕易改變. 接著讓你所依賴的程式去實做這一個 Interface.所以 SecretHelper 就變成如下:

clas SecretHelper : IWrtieSecret {

    public void Write(string data) {
        // code for writing secret data
          .... 
    }

    public void WriteSecret(string data) {
        Write(data);
    }
}

Interface 所定義的 WriteSecret() 去呼叫 Write().接著,DataWriterHelper 就會改成如下:

class DataWriterHelper {

    private IWriteSecret  _secretWriter;
    public DataWriterHelper(IWriteSecret  secretWriter) {
        _secretWriter = secretWriter;
    }

    public void WriteSecret(string data) {
        _secretWriter.WriteSecret(data);
    }
}

經過這樣的修改,DataWriterHelper 便不再依賴 SecretHelper 了,而是變成依賴 IWriteSecret.因此,這樣做就把 class 和 class 之間的 dependency 切開,變成 class 依賴新的 interface. 這樣改變的想法來源是根據物件導向設計的 SOLID 原則.上面程式碼 IWirteSecret 的實作物件是透過 constructor injection 的方式傳來的,換句話說,DataWriterHelper 本身不參與 IWriterSecret 實作物件的建立過程,而是由外部的呼叫物件來決定要傳入那一個 IWriterSecret 的物件.

這樣的改變,將使得 DataWriterHelper 的測試力變得強一點,因為你可以傳入測試用的 IWrtieSecret 物件用在 DataWrtierHelper 的 unit test 或 integration test 的情境中.

Share:

#51 Coding 面試 - LeetCode #9 Palindrome Number

原文網址 https://leetcode.com/problems/palindrome-number/

這一題跟之前講的有一題 palindrome string 有點異巧同工之妙,但不同的是這題指的是數字,而不是字串.如果你把數字當成字母來看待的話,則也能用之前相同的解法.這在演算法來說算是一種 reduction 的技巧.然後,數字和字串的特性畢竟不同,所以若把數字轉成字串來處理是可以的,但就浪費了一些數字的特質.

其實要把數字反轉,最簡單的方法就是用十來取餘數,也就是說 2 取十的餘數是 2 , 12 取十的餘數是 2 , 102 數十的餘數是 2.因此,對十取餘數之後,我們就能得到個位數的值,然後再對商再取一次餘數,用這個方法一直做到商變成 0,這樣子就可以把原來的數字一個一個取了出來.

剩下的就是將取出來的數字排好,第一個取出來的放最前面,最後取出來的放最後面,這裡不需要 "儲存" 的觀念來做,其實只要在每取出一次餘數時,把之前取出來的數字乘十就行了,這樣就等於先取出來的會被 "進位",然後再加上最新取出來的餘數.

最後,還要記得處理正負號.這只需要在一開始的時候判斷是正號或負號,若是負號的話,就一律不符合答案,若是正號的話,就檢查反轉前和反轉後的數字是不是一樣的,這樣就完成了.

參考的程式碼如下:



Share:

#50 Coding 面試 - LeetCode #349 Intersection of Two Arrays

原文網址 https://leetcode.com/problems/intersection-of-two-arrays

這一題在原文網址上列為簡易型的題目,看了題目之後,只要你熟悉一些基本的資料結構,應該可以很快想出不錯的解法.題目輸入是兩個 integer array,然後輸出一個 array,這個 array 包含的元素要出現在兩個輸入的 array 裡.

若用最直覺的想法,你可以做一個 for loop 在第一個輸入 array 上,把每一個元素和第二個輸入 array 的元素相比較.以這樣的做法來看,時間複雜度將會是 O(n2).以這個題目來說,這樣的時間複雜度並不理想.所以得再想想一個比較好的處理方式.

我在寫這題時一開始就想到利用 hash table 來運作,因為 hash table 有極佳的 O(1) 的動作速度.所以一開始把第一個輸入 array 所有的元素透過一個 for loop 加入到 hash table.然後再將第二個輸入 array 利用另一個 for loop 來查看是否有同樣的數字已經存在於 hash table 之中.如果有的話,它就是我們要尋找的對象,但把它加入到輸出的結果裡,這裡是用一個 List 來代表.最後再將整個 List 轉換成 array 輸出,這樣的轉換只需要 O(n). 因此,整個時間複雜度將是 O(n+n+n).有二個 for loop 和最後的轉換,所以一共是三個 n.程式碼如下:



最後我還發現了 LeetCode 提供了與其他同樣語言解法的比較.上述的解法打敗使用同樣語言 75% 的解法.我想這是以他們電腦執行速度來計算的.由此可見,上述的程式碼在最佳化上還有很大的空間可以改進.

Share:

#49 - 物件導向的最基礎 - Access Modifier

若我們用 Java 語言來做為說明,則 access modifier 有三種,分別是 public, protected, private. 他們分別控制了那些對象能使用他們所定義的東西.

https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html 裡面有一個表格呈現出了整個控制力的對照圖.例如,private method 只能被同一個 class 裡面的 method 所使用,只要超過同一個 class 的範圍是無法使用的.public method 剛好相反,它可以被任何的 class 看到來使用,甚至也可以被其他元件看到來使用.因此,當你將 class, property, method 設定為 public 時,你就要非常地小心.這個稍後說明.最後一個是 protected,它能被同一個 class 裡的東西來使用,而也能被該 class 的 child class 來使用.

Private 的控制力道最嚴格,因為除了在同一個 class 的 method, property 以外,其他人都無法存取.因此,在最初的設計上時比較不會對 private method, class, property 等做太多的說明,甚至會跳過他們.Public 就完全相反了.誠如前面所說,一旦你將 class 設定為 public 時,那表示不止你的程式可以看到來使用,其他人的程式也可以看到來使用,這時候你就要非常小心,因為你得考慮到這個 class 一旦被使用了之後,它所執行的程式碼需要具有什麼意義.同時,也要注意在寫程式時是不是有做一些檢查.

來看一個很笨的例子,假設你今天要設計一個傳送訊息通知的 class,而訊息傳送的功能是透過其他的元件來執行,你需要設計的只是一個簡單的 wrapper ,將該元件包起來,然後只要露出一些你需要提供的功能即可.所以程式碼可能是這樣

public class MessageSender 
{
 private Some.Other.Sender _component;
 
 public void Start()
 {
  _component = new Some.Other.Sender();
  _component.OpenConnection();
 }
 
 public void Send(string message)
 {
  _component.Send(message);
 }
 
 public void End()
 {
  _component.CloseConnection();
 }
}

從上面的程式碼你可以看到 MessageSender class 的 access modifier 是 public,這表示全世界都可以看到它,因此任何人都可以使用它.在這個 class 裡面定義了三個 methods,分別是 Start(), Send(), End().會這樣設計可能因為你想要讓使用它的人比較清楚知道該如何使用,因為顧名思義看起來就會覺得一開始的時候就要使用 Start(),需要傳送資料時就用 Send(),而最後在結束時就使用 End().這樣的想法也沒什麼不對,只是漏掉了一點,那就是 public method 是大家都看的到.別人可以看的到不代表他就一定得用它.也就是說,當我 new MessageSender 時,有規定我一定要先呼叫 Start() 嗎 ? 似乎沒有.有規定我最後結束時一定要呼叫 End() 嗎 ? 似乎也沒有.如果我直接呼叫 Send 來傳送字串時,那會發生什麼事呢 ? 答案很明顯,會發生 null object 的現象.

用以上這個笨笨的例子來提醒大家,在設計 public class, public property, public method 時一定要注意到這些 public 的東西被使用時是沒有順序可言的,所以千萬不能自作主張去以為所有人會知道該如何使用,甚至你寫了說明文件該如何使用也不能這樣寫程式.因此,設計 public 時一定要非常地小心,要想到當 public class, public property, public method 是第一個被存取的對象時會發生什麼事.這樣你就會知道上面的程式碼是行不通的,而且相信你也能將上述的程式碼改成一個比較好的程式碼.

Share:

#48 老鼠走迷宮 (mouse maze)

在大學的資料結構的課程裡,大約在教過 Stack, Queue 和 List 這些基本的資料結構之後會有一個老鼠走迷宮的作業.這份作業說難不難,說簡單也沒那麼簡單,對一個正在學習資料結構的學生來說,花個一兩個晚上來寫這份作業是很正常的.如果寫程式正是你的工作,而且你以前沒念過資料結構的話,不妨試著做做看這份作業.

老鼠走迷宮的內容是,要隨機產生一個矩陣,矩陣的大小最好由使用者輸入設定的.然後在矩陣裡的每個位置也會隨機產生三種不同的地形,例如平地,河流與高山.你可以再設計其他不同的地形,老鼠在每一個地形上走動時會花費不同的體力,例如平地是 1 ,河流是 2,而老鼠是無法穿越高山.老鼠的出發點是矩陣的左上角,也就是 (0,0) 的位置, 然後終點在右下角,也就是 (n,m) 的位置,n,m 代表的是矩陣的兩個邊長.

你所要寫的程式是讓使用者輸入矩陣的大小,然後為矩陣的每一個格子隨機產生地形,接著老鼠從出發點開始移動,看看老鼠是否可以走到終點.因為地形中會有高山,所以老鼠不一定都會有一條路可以走到終點.然而,老鼠能走的路也可能不只一條能到終點,你可以試著找出一條就行了.

整個題目的重點在於兩個地方.第一,移動的方向要定出一套規則,第二,要能夠記錄走過的路跡.移動方向的規則可以用最簡單的方式來實行,除了在邊邊與角落以外的格子,每個格子都可以有八種移動方向,上,下,左,右,左上,左下,右上,右下.每到達一個格子時,就可以依照固定的順序來移動到下一個格子,如果在下一個格子都沒有路走或是每個方向都試過了,那就回到上一個格子.因此,路跡的記錄是很重要的,因為需要回到上一個格子.

以下我提供十多年前寫的版本



如果你順利寫完之後,還有一個延伸題目給你繼續挑戰,那就是如果有多條可以走的路跡時,列出最省力的路跡.這延伸題目就不是那麼簡單了,未來再來寫篇文章來討論.

Share:

#47 資料庫基礎 - 以 Hash 為基礎的 Index

前面的文章介紹了資料庫的 Index 是以 tree 為基礎的方式,一般來說大部份資料庫產品都用 B-tree 來建立 Index.然而,除了用 tree 以外,還可以用 Hash 的方式來建立.


可以用上圖來說明 hash index 的運作方式.首先,輸入值會先經過 hash function 的運算後而得到一個 hashed value,這個運作如以前的文章講的是一個 constant time 的運算.輸入值就是使用者要過濾的條件,也就是 SQL statement 中 where 的內容的欄位值.例如,學生證號碼,病歷編號等等.然而,這種輸入值若不是 key 的話,便會發生多筆同樣資料會得到一樣的 hashed value,例如我們是用學生的姓來做 hash index 的話,則同樣的姓就會得到一樣的 hashed value.因此,一個 hashed value 可能會對應到一個或多個在 Data page 上的資料.,以上圖來看,每個 hashed index 的資料可以對應到一筆或多筆在 data page 上的資料,這樣的做法其實並不太好,因為每個 hashed index 的資料大小是無法確定的,就因為我們無法預測每個 hashed index 會對應到多少筆 data page 的資料.因此,可以再改一下如下圖,


我們規定每個 hashed index 的資料只能對應到一筆 data page 上的資料,這樣就可以固定住每個 hashed index 的資料大小.因此,當輸入值經由 hash function 運算之後到達第一個 hashed index 上的位置,便可以得到一筆 data page 的位置而取得資料,然後還要往下一個 hashed index 移動,如果 hashed value 也是一樣的話,就要再取得它所對應在 data page 上的資料,一直到 hashed index 上的值不同了或是沒有下一個了.

以 hashed index 的運作方式來看,其效率比以 tree 為基礎的 index 還要來的好,那為什麼一般的資料庫產品不採用它呢 ? 我想主要的原因就在於 hashed index 無法支援有範圍的尋找.例如,如果用學生證編號做 index,當你下 SQL statement where ID > 10 and ID < 100 時,這在 tree-based index 上可以有很好的尋找效果,只要找到 10 之後就再延著 tree node 一直往下或往旁邊找到 100 就可以停住了.但 hashed index 來說並不是這樣,因為在 hashed index 裡, 10 旁邊的很可能就是 100,要看 hash function 如何設計.所以在 hashed index 上我們找不到一直可以連續讀取資料的方式.如果我們把編號從 10 開始代入一直到 100,這聽起來好像可行,但如果 index 的資料不是數字而是文字,難道要把筆畫漸大的字都帶進去運算嗎 ? 顯然不可能.

雖然 hashed index 不適合用在有範圍的尋找,所以就不適合被一些商業資料庫產品所採用,但還是透過這篇文章介紹 hashed index 讓讀者們可以了解 index 的做法不見得只有 tree 的方式,不同的方式有好有壞,有長處也有短處.



Share:

#46 Coding 面試 - LeetCode #62 Unique Paths

原文網址 https://leetcode.com/problems/unique-paths/

這一題主要是考 dynamic programming 的思考,但老實說,這題過於直覺,所以寫完答案時還沒馬上觀察到這是 dynamic programming 的解法


參考的解法如下:



從 First row 上的可行走法都是1, 因為只有可能從左邊走過來的方式.從 second row 開始,就有可能是從上面或從左邊來的走法,因此才是相加的,也就是上述解法中 r[i,j] = r[i-1,j] + r[i,j-1];

我相信如果你的主考官想要考你 dynamic programming 的話,應該不會用這一題來考你.若是要考簡單的,應該會是用 Fibonacci number 來考你,因為最直覺的 Fibonacci number 的寫法並不是用 dynamic programming 的寫法,而是用 recursive 的寫法.以時間複雜度來看,dynamic programming 的寫法是比較快的,而以空間複雜度來看,recursive 寫法是比較優的.

Share:

#45 資料庫基礎 - Page 的 Fill Factor

前面的文章介紹了許多資料庫引擎中有關 Storage management 的部份,讓你能了解資料庫引擎在處理儲存與讀取資料時所使用的原理基礎.但這些也都只是基礎的運作原理,真正實作的方式也會因為產品的不同也有些差異,但至少你知道了原理,這就能幫助你了解資料庫引擎的工作,也希望對你的學習與工作能有所幫助.

這篇文章將說明的是一個 Storage management 中管理資料的選項,對我而言,這是一個設計技巧.還記得前面的文章中曾提過 Page - 這是資料庫引擎用來儲存資料時所用的一個管理單位.一個 page 可能會有 4K 或 8K 或其他大小的容量設計,所以每個 page 的容量幾乎都是固定的.對一般正常運作的資料庫系統來說,對資料一定會進行新增刪除與修改的功能,所以這樣子的工作就常常會導致 page split 的現象,這個現象在前面的文章曾提過.page split 就相當於是在一個 list 中插入一個新的元素一樣,其運作成本比較高,如果再加上硬碟資料散亂度過大的話.所以,為了減少 page split 的現象,因此存在了 fill factor 的設計.

Fill factor 的意思就是當一個新的 page 產生要寫入資料時,資料庫引擎不會將所有資料空間都拿來寫入資料,而是會留下某一個百分比的空閒空間.這個百分比的大小基本上是可人為控制的.比如,當我們告訴資料庫引擎 fill factor 為 80% 時,則資料庫引擎在 page split 發生或是需要建立新的 page 時,當它寫入資料時只會佔用掉  80% 的可用空間.而剩下的 20% 就留做未來在做資料新增修改時可以利用的空閒空間.舉個例子說明,你有一份學生資料放在資料庫中,而學生資料假設定依照學生姓名排序 (primary key).假設一開始並沒有設定 fill factor,而它的預設值是 100%,所以這表示所有的學生資料都依序寫入到 page 之中.資料越多,所需要的 page 也會越多.而在這些 page 之間都沒有留下任何的空閒空間.而今天來了一個新學生,他的名字剛好是排在所有學生的中間,當它的資料寫入到資料庫時,資料庫引擎發現它需要在這些 page 中間做一個 page split.Page split 做完後,一個 page 變成 2 個 page,所以這兩個 page 都多有了 50% 的空閒空間,因此新學生的資料就可以寫進去這中間的 page 了.所以,如果一開始設定了 fill factor = 50%,那表示在一開始學生資料寫入到 page 時只會佔用一半的空間,因此每個 page 都有一半的空閒空間.當新的學生資料需要新增時,資料庫引擎找到目的 page  後發現還有一半空閒空間並且足夠用來寫入新資料,則就直接將新資料寫入而不需要做 page split 的動作,因此也讓資料新增的動作可以更快地完成.所以,你可以把 fill factor 想成是一種利用空間來換取時間的方法.這對於經常需要做新增動作的資料庫來說相當有幫助.對於資料變動很小的資料庫來說並沒有什麼太大的好處.

因此,如果你採用的資料庫引擎有提供 page split 的觀察指標,你可以查看你有那些 table 經常發生 page split 的現象,如果有的話,你可以將 table 的 fill factor 改小一點來降低 page split 的發生.

其實 fill factor 也只是一個應用,像這樣預先預留空間做為未來使用的情況,在很多產品的設計都會用到這樣子的 idea,基本上都是用空間來換點時間的做法.希望這樣的 idea 有一天也能幫助你的設計.


Share:

#44 Coding 面試 - 列出公司組織人數

這一題是我以前面試時遇到的題目,不知道當初面試的老闆是在那找來的題目,總之我沒有看過類似題.先來說明題目,

你要寫一個程式,程式的 input 是一個字串,這字串包含了公司裡員工的名字,同時字串裡也包含了老闆與直屬員工的關係.以下是一個例子

A-B,C-B,B-D   這字串代表 A 的老闆是 B,C的老闆是 B, B 的老闆是 D,以此類推,中間會用逗號隔開每一組資料,然後一橫代表老闆和員工,左邊是員工,右邊是老闆.所以,這一個字串其實上就代表了整個公司的組織圖.你可以用手動的方式把每個資訊解析一下就可以把這組織圖的 tree 給畫了出來.

題目要求你要列出某一層次的老闆們下面一共有多少員工 ? 以上述的例子來說,D 是在 level 1, B 是在 level 2, A 和 C 是在 level 3,如果要求你列出 level 2 的老闆時,你的程式就要顯示 B 有兩個員工.

看到這裡,不知道你心裡有沒有想法要怎麼解決這一題.當初這個面試更特別的是,這位面試的老闆帶他自己的筆電過來,Visual Studio 已經開好了,題目的 function 和輸入字串也都打好了,直接要我寫 function 裡面需要的 code.就是上機考.這是我第一次面試遇到上機考,以前遇過的面試都是寫白板而己.寫白板的好處就是有一點小錯誤的話也沒關係,因為面試者主要是看你寫的邏輯對不對,所以不會在乎你是不是打錯 method name 或少了結尾分號之類的.但上機考就完全不一樣了,你一定要讓 compiler 很開心,不然既便有一個再小的錯誤,compile 會失敗的.

基本上,只有 30 分鐘可以這一題,我一開始想了一下,難道真的要建 tree 嗎 ?  寫了建立 tree 的功能之後,還要加上 tree traversal 的功能,這樣才能在 tree 上遊走找人數.當時我還很認真地思考要如何做 tree 和相關功能.後來想想不太對,因為這樣太麻煩了,我只剩 25 分鐘而己.後來就想了一招,雖然不是很好,但是用來解這個題目一定夠用.那就是改用 recursive 的觀念來做.做法如下

1. Parse 輸入字串來建立一個 hash table,key 是員工名,value 是老闆名
2. 用 recursive 的方式在這個 hash table 上找出某一個 level 的員工名稱
3. 根據步驟 2 的結果,把 hash table 上所有員工也是用 recursive 的方式來找看誰的上面老闆是在步驟 2 的結果內,同時把該老闆的下屬員工數做人數統計

因為老闆給的公司組織是真實的資料,所以我知道當我用 recursive 的方式來運作時並不會發生 call stack overflow 的現象.這個觀念在前面的文章中曾提過.

結果,最後我用了 40 分鐘才把整個程式做完,然後直接跑程式給面試老闆看,接下來就是討論我如何做,有沒有什麼改進之處等等.

很抱歉,當時的程式碼就在那面試老闆的電腦裡,沒機會貼在這裡供大家參考.但還是把我當時的做法寫出來供大家參考.

如果你看到在那邊有相似的題目,再麻煩你分享網址.^_^

Share: