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

#38 Coding 面試 - LeetCode #2 Add Two Numbers

原文題目 https://leetcode.com/problems/add-two-numbers/

這題是個基本的 Linked List 考題,如果題到這類型的 List 題目,若題目沒特別說的話,就一律想成是 Single Linked List 了.其實,我個人不是很喜歡 List 考題,原因是大部份的問題都蠻無聊的,可能就在 List 上把元素搬來搬去,而且不一定會有一個很好的邏輯來搬這些元素,所以就會造成一個 function 寫的長長的.像這一題就是這樣.

題目給了兩個 input list,要把每個元素加在一起後,然後輸出一個最後的 list.其實也蠻簡單的,你只要在這兩個 input list 上一個一個元素去拜訪,然後把加起來的答案寫到新的位置上,同時再注意進位的事情.並且你也要考慮到兩個 input list 的長度可能是一樣長,也可能是不一樣長,甚至可能是空的.如果你按照這個想法寫,答案就可以寫的出來.

但是,就這麼簡單嗎 ? 基本上是的,但要注意一件事,上面的想法是會製造出另一個新的 list,算到最後把這個 list return 出去.這樣做的話就等於空間複雜度是 O(n+m) 了,假設 n,m 分別是兩個 input list 的長度.

所以,比較好的做法是讓空間複雜度變成 O(1),也就是說每當計算一次元素的加法時,就把答案寫到某一個 input list 上,最後 return 這一個 input list 就行了,所以就不需要宣告出其他不是 O(1) 的空間.

參考的程式碼如下:



沒錯吧,又臭又長! 如果你能想到更好的方法來簡化以上的程式碼,再麻煩你教教我! ^_^

Share:

#37 資料庫基礎 - Clustered Index 與 Non-clustered Index

在編號 #33 的文章中介紹了什麼是 Index.這可以說是資料庫對資料能快速尋找的主要方法,基本上也是一個用空間換取時間的方法,也就是為了更快速地找到資料,於是犧牲了更多的硬碟儲存空間來達成這件事.也因為如此,所以資料庫引擎也需要有相對應的功能來妥善管理這些特別的儲存空間.然而,儲存空間的內容不同也會影響不同的管理方法,所以這一篇文章將來介紹不同的儲存空間 -  Clustered Index 和 Non-clustered Index.

如果你曾撰寫過資料庫應用的相關程式或是你本身是資料庫管理員,相信你一定聽過 Clustered Index 和 Non-clustered Index.這兩個 Index 有什麼不同呢 ? 給一個比較簡單直覺的答案就是 Clustered Index 是根據某個資料在儲存空間上排列的順序而建立,而 Non-clustered Index 不一定要按照實體的資料排列順序而建立.

把 #33 那篇文章中的圖片再貼過來一次.


之前提過這是一份資料,按照學生證號碼排列寫入儲存空間的概念圖,所以你可以看到學生證的號碼有 1,5,7,11,13,16,23,24,36 這些號碼,而這些號碼基本上會被選做為 primary key,所以資料庫引擎可以利用這個號碼做為資料儲存位置的順序.因此,號碼小的一定寫在號碼大的前面,若不是在同一個 page 上的話,那一定是在前面的 page 上.所以,如果資料庫引擎用學生證號碼做為依據來建立 Index 的話,它的 Index tree 就有如上圖所承現.這一個 Index tree 我們就稱為 Clustered Index,也就是這個 Index 是依據決定實體順序排列的資料而建立起來的.其他種類的 Index 就稱為 Non-clustered Index.

所以,你就能知道為什麼在建立 Clustered Index 的時候你只能建立一個,而 Non-clustered Index 可以建立好多個,那就是因為實體順序排列只會有一種.

在尋找資料時,Index 特別強大的功能是在於某一個範圍內的尋找,比如,你要找學生證號碼 5 號的資料,或是找學生證號碼大於 20 號的資料.這類型的範圍尋找對使用 Index 來說是最好的.而 Clustered Index 和 Non-clustered Index 在這方面又有什麼差別呢 ? Clustered Index 的建立是依據資料實體的排列順序,所以當你執行了一個 SQL command 如 select * from students where ( ID < 10 and ID >0) ,你就會發現這只需要讀取一個 page 而己,這是以上圖為例子.如果你執行了另一個 SQL command 如 select * from students where street = '1st',假設有一個 Non-clustered Index 是根據 street 資料而建立的,所以當你要尋找 street = 1st 時,這一個 Non-clustered Index 就會被用到,而且很有可能會指到許多不同的 page,比如 1 號學生和 36 號學生都住在 1st street.所以,Clustered Index 的尋找所需要讀取的 page 數量理論上會小於或等於 Non-clustered Index 所需要讀取的 page 數量.

總結,對資料庫引擎而言,不論是 Clustered Index 或是 Non-clustered Index,尋找資料的過程基本上都是一樣的,由於這兩種 Index 的特性不同,所以造成 Clustered Index 所帶來的維護成本較高 (因為實體的儲存順序就是 Index 上資料的順序,要變動比較麻煩),而 Non-clustered Index 所帶來的維護成本相對較低 (因為只要改 pointer 指到新的 page).

Share:

#36 Code Review - 檢視你的程式碼

不論是寫好一個新的功能或是修改 bug,在程式碼都能正確執行並且在 check-in 到 source control server 之前,一定都會有一個 code review 的動作.這件事情通常是由比較資深的人員或對整體程式較為熟悉的人員來執行.Code review 帶來的好處很多,在這裡不一一描述,這對比較資淺的人員或新進的人員來說是一件好事.透過 code review,新進或資淺的人員可以比較快進入情況.有時我們進入了一個龐大程式碼專案,短時間之內蠻難完全了解所有細節和該團隊寫程式碼的文化,所以透過 code review 來了解程式碼,也是一種學習.

如果你工作的地方有執行 code reivew 的動作,真的恭喜你,畢竟教學相長,透過檢視彼此的程式碼是一種良好的學習方式.因為這跟團隊文化與團隊技術能力與要求上有很大的關係,所以 code review 實在很難說有什麼標準可言.除了程式碼要符合團隊的寫作規定外,要求嚴格的團隊甚至是極為嚴苛的地步.

接下來,我分享一些我以前在 code review 中學習到的經驗.

1. 每個公司或團隊因為人的不同,所以要求的標準也不同,先不用假設對方是來找你麻煩,如果他對你寫的程式碼有意見,他一定能說的出原因,讓你相信他是對的.

2. 如果你寫的是 Java/C# 之類的物件導向的程式,可能就有點小複雜.因為同一個功能因每個人對物件設計理解程度不同而會有不同的設計,再加上會導入一些 pattern 等因素,容易造成你不明白全部的內容時,就犯了一些錯.也不用過於放在心上,這種事情是常常發生的.

3. 以我個人而言,在程式碼效能發揮到極致後,程式碼寫的簡單乾淨好閱讀就行了,但是你很可能會遇到資深的工程師要求你要改的更簡潔.例如,

bool visited;
if (isActive == true) {
    visited = true
}

改成

bool visited;
visited |= isActive

基本上這兩個邏輯是一樣的,如果你被要求要寫成下面那樣的語法,就當做是個學習經驗吧!
其他像是變數名稱,物件名稱或屬性名稱也是會有類似的情況.

4. Comment 寫註解.做產品的公司會連註解也有嚴格的格式要求.如果是一般的程式註解,也是要寫到讓人看的懂,否則有寫跟沒寫是一樣的.

所以,如果你是新人,那就好好享受 code review 的過程吧,因為當你遇到一位功力深厚的前輩幫你 code review 時,保證你會收益良多的.

Share:

#35 Coding面試 - LeetCode #94 Binary Tree Inorder Traversal

這題目出現在這 https://leetcode.com/problems/binary-tree-inorder-traversal/

如果要考 Tree 相關的題目,這一題算是相當經典的考試題目了.我想經典的原因就是 Tree inorder traversal 是資料結構課本裡面一定會教到的內容.大部份的課本裡面都會提供 recursive 的方式來做 inorder traversal,其程式如下

        public List<int> InorderTraversal(TreeNode root)
        {
            List<int> result = new List<int> ();
            if (root == null) return result;
            if (root.left != null)
                result.AddRange(InorderTraversal(root.left));
            result.Add(root.val);
            if (root.right != null)
                result.AddRange(InorderTraversal(root.right));
            return result;
        }

但面試官既然要考你這一題的話,絕對不可能只問你 recursive 如何寫,也一定還會問你怎麼用 iterative 的方式來寫.
如果你完全沒做過相關的練習,一時之間還真的很難想的出來該怎麼把 recursive 改成 iterative. 在這可以分享一件小事,因為寫 recursive 有階層的關係,所以程式的 call stack 就一層一層往上加,上一層結束回到下一層時也自然知道該從什麼地方繼續執行.但是若改用 iterative 的話,就沒有這種記住上次執行到那的好處了.對於這種需要記住位置的情況,就可以直接想想 Stack,因為 Stack 能幫助我們記住走過的痕跡.

所以,改成 iterative 的程式碼如下

        public List<int> InorderTraversal(TreeNode root)
        {
            List<int> result = new List<int> ();
            if (root == null) return result;
            Stack<TreeNode> stack = new Stack<TreeNode>();
            TreeNode node = root;
            while (stack.Count != 0 || node != null)
            {
                if (node != null)
                {
                    stack.Push(node);     // <-- 幫我們記住位置
                    node = node.left;
                }
                else
                {
                    TreeNode temp = stack.Pop();    // <-- 把上次記住的位置拿出來
                    result.Add(temp.val);
                    node = temp.right;
                }                              
            }
            return result;
        }

沒感覺嗎 ? 多寫幾次就會有了.

Share:

#34 資料結構 - 非電腦科系的工程師們,你們體會多少了呢 ?

我第一次念資料結構時是因為準備台灣的資工研究所考試而念的,當時為了考試再加上時間也不夠多,所以念的很匆忙,沒有太多的時間思考著這門課程的精華.後來,念了研究所之後,研讀了一些學術論文,才慢慢了解到資料結構的精華.在許多學術論文裡都是討論著許多真實世界上的問題,通常這些問題會用一個數學公式或模型來表示,所以接下來的內容討論就可以直接在這數學公式或模型上直接去模擬.而這類的數學公式或模型若要用電腦程式來表達時,通常會發展出適合的演算法與資料結構.所以,這類的論文看多了之後,反而有幫助自己漸漸了解資料結構的精華.

基本上,電腦有二個基本的東西,一個運算器,一個是儲存空間.運算器就是大家所知的 CPU,而儲存空間就是記憶體和硬碟,其中記憶體是速度快但記憶時間較短,而硬碟是速度慢但記憶時間較長.這兩個東西你可以想成他們是長長的紙條,運算器可以在這長長的紙條上寫下資料,也可以讀取資料.

資料結構基本上就包含了兩件內容,一個是你需要用的資料是要寫在紙條上的何處,另一個就是如何在這些資料之間進行讀取或寫入.舉個例子,之前的文章提到 Array 這個資料結構.它的特性就是當你建立這個資料結構時,你必須先在紙條上找到一個符合你需要的足夠大的空間,而在資料之間進行讀取寫入的方式就是直接透過計算要讀或寫第幾個元素,就可以直接算出在紙條上的位址.例如,宣告了一個 byte array,一個有 20 個元素,如果第一個 byte 就是紙條上第 1000 個 byte 的位址,當我們要讀取第 5 個元素時,我們就知道要去 1005 byte 的位址上去讀取資料.

再舉另一個例子,Linked List.當你宣告了一個 Linked List 時,一開始你會先加入一個元素,這個元素可以寫在紙條上任何足夠空間的地方,而當你再加入另一個元素時,此時運算器就在紙條上任意一個足夠空間的地方把資料寫下,然後再回到第一個元素的位址上,把第二個元素的位址寫在第一個元素的空間中.所以,你可以知道在每個元素的空間中,除了元素的資料以後,還有下一個元素的地址.因此,當你想知道這個 Linked List 有多少元素時,你就必須從第一個元素一直讀到最後一個元素.

因此,課本裡面教的都是一些最基本的資料結構,就好像數學裡的四則運算一樣.電腦世界裡面的執行過程都是把紙條上的資料讀過來寫過去.當你發現你需要的問題很難用這些基本的資料結構來表達時,這時就是可以發明新的資料結構的時候了.

也許你會問,那這些跟平常的工作有什麼關係嗎 ? 比如,每天都在寫 JavaScript 搞前端畫面或是都在寫 java 寫後端程式.其實,多多少少會有關係的,尤其是你用物件導向在寫程式時,你所宣告的 class 就像是你定義了一種資料結構,這個 class 裡面所用到的儲存方式和運算方式都會大大地對程式在各方面有影響.就像是你知道了 Array 和 List 有何不同,你才會選得較適合的資料結構,也才能寫出比較快的程式碼.

未來的文章,不論是討論資料庫或是面試題目,你們都可以好好地思考一下是否還有其他可用的資料結構.不同的資料結構就代表程式的內容是不同的,可能會更好,也可能一樣,但也可能更差.所以,非電腦科系的工程師們,你們體會多少了呢 ?



Share:

#33 資料庫基礎 - 什麼是 Index

Index 在資料庫的領域裡算是很基本且極為重要的項目,因為它幫助我們可以在龐大的資料裡快速地找到資料.這一篇文章就來說明 Index 運作的原理.

Index 也是一種典型的用空間換取時間的做法.這感覺就像是書籍裡最後面會有一些專有名詞在那一個頁數中可以找到,透過書籍的 Index,你可以很快找到你要找的專有名詞.同樣的,在資料庫裡也是類似像這樣的做法.資料庫引擎可以將你感興趣的資料製做成 Index,如此一來,資料庫引擎只要在 Index 上尋找目標,就可以很快速地得知該筆資料的位置是在資料庫檔案裡的什麼地方.這些就是 Index 概念.舉個例子,在資料庫裡有一個學生資料的表格,表格的 primary key 是學生證號碼,其他的欄位有名字,班級,地址,電話,性別等等.誠如以前的文章曾提過,這個表格有 primary key,所以基本上來說資料庫引擎就會以 primary key 的排序順序做為資料在 page 上儲存的順序.因此,當我們用學生證號碼做為尋找資料的依據時,資料庫引擎就會在 page 上依序地找出我們要的學生證號碼那筆資料.這是在沒有 Index 的情況下.如果你腦筋動的快,你會發現既然學生的資料已經是用學生證號碼排序好了,當我們要用學生證號碼來尋找時,何不用 binary search 呢 ? 沒錯,若你能這樣想,恭喜你已經漸漸習慣了用電腦科學來想事情了.但在這裡,binary search 真正能派上用場嗎 ? 那就要看資料是用何種方式儲存在 page 裡了.如果是用 directory based 的方式,還可行,但若是其他的儲存方式,那基本上不太實用.所以,為了不受儲存方式的干擾,我們可以用更多的空間來儲存成一個方便資料庫引擎搜找的資料結構,同時也享受快速尋找的好處.於是,有什麼資料結構適合呢 ? 答案就是 Tree.

如果我們把學生證號碼做成 Tree,如下的範例圖:


圖中數字為學生證號碼.資料庫引擎會依據學生證號碼的資料做成 Tree,也就是圖片上 Index tree 的部份,然後在 Index Tree 的末端節點上會放入該筆資料位置的 pointer.因此,只要 Index 一建立好之後,資料庫引擎就可以在 Tree 上遊走尋找想要的資料,若找到目標時,也可以馬上切換到該筆資料的位置.這就是為什麼透過 Index 的使用可以讓資料庫引擎快速找到資料的原因.

如果現在的情況改成要用學生的名字來做為搜尋目標,那麼上圖的 Index 就幫不上忙了,因為那個 Index 是以學生證號碼來建立 Tree.所以若我們希望用學生名字來搜尋時也能像之前的效果一樣,則資料庫引擎就必須以學生名字再來建立另外一個 Tree.所以,Index 的建立就必須是有意義的,如果隨便建立一些資料庫引擎用不到的 Index,那只是增加了資料庫引擎對資料維護上的成本而且也浪費更多硬碟空間.

這篇文章先為基本的 Index 概念先開個頭,之後的文章會再來介紹更多有關 Index 的故事.

Share:

#32 資料庫基礎 - 資料讀取的成本評估

在前面的文章裡曾提供當資料庫引擎要儲存資料時可能有三種方式可以選擇,分別是 heap file, sorted file, 和 hashed file.

Heap file

基本上就是沒依照任何的規則來儲存資料,也就是說你先把資料A寫進去,然後再寫資料B,則自然而然地 A 的位置就會在 B 的前面.正常情況下,我們似乎不太希望用這種方式來儲存資料,因為它對資料搜找的速度上並沒有什麼幫助.

所以,假設資料一共分佈在 p 個 pages,然後每個 page 的讀取或寫入的平均時間為 d,那麼在尋找資料時所花費的成本便是 p*d,這是以最壞的情況來看的,因為我們無法知道我們要找的資料是在前面的 page 還是後面的 page,所以在成本評估時就用最壞的情況來看.

如果做 insert 動作,heap file 的特性會讓新的資料一律從最後面加入,所以必須要從第一個 page 走到最後一個 page,然後再做寫入的動作,所以寫入的成本是 p*d+d.

如果換成是一個資料刪除的動作,那就是尋找資料的時間再加上一個資料寫入的時間,所以也是 p*d+d.這也是以最壞的情況來打算的.

如果我們平均來看的話,假設需要找的資料有一半在前面一半的 page,另外一半落在後面一半的 page,而每筆資料都會被尋找,則搜尋的時間成本就可以想像成 p/2 * d,也就是說長時間下來每一筆資料都會被尋找,所以搜尋成本才會除以2.同樣的想法也可以推廣到 delete 的動作.Insert 的動作都是加在最後一個 page,所以平均來看的話,這並沒有差別.

Sorted file

資料會依照某一個規則來排放在 page 之中,這個規則如果能讓每筆資料能顯露出獨立的效果是最好的,比如依照身分證號碼或是員工編號,但這不是必要條件.我們也可以用員工的姓氏來做為排列的規則,這樣至少你可以確定同一個姓氏的員工資料就會被放在同一個或是附近的 page.所以,我們可以明確地知道如果資料是用 sorted file 方式來儲存,這將對於搜尋會很有幫助 ? 為什麼呢 ? 還記得之前文章曾提過的 binary search 嗎 ? 當資料以某一種規則排列好時,binary search 可以讓搜尋更快速,有多快呢 ? 還記得 binary search 的時間複雜度嗎 ? 它是 O(log n),再重複一次,在電腦科學的世界裡,大部份的情況下 log 是以 2 為基底.

因此,對於 sorted file 而言,尋找資料的成本就是 log (p*d),這裡的 p 和 d 跟前面的定義是一樣的.

如果是 insert 的動作,因為這是 sorted file,所以需要找到適當的位置然後再做 insert 的動作,所以 insert 的成本就是尋找 + 寫入,因此就是 log (p*d) + d,前提 insert 動作不會造成 page split 的現象發生,也就是說 page 裡面有足夠的空白空間可以寫入新資料.

如果是一個刪除的動作,資料必須要先找到,然後再加上寫入的動作,所以成本也是 log(p*d) +d.以上這些成本評估都是基於資料排序的規則會被應用到,比如,如果是員工資料用員工編號來排序時,那麼資料在被尋找,被新增與刪除時,都必須提供員工編號才能達到上述的成本評估.

Hashed file

Hashed file 的安放方式是讓資料經過 hash function 的計算之後才決定要放到那一個 page.如果 page 有足夠的量而且 hash function 夠好的話,就不會有 overflow page 的發生,也就是說 hash function 計算出一樣答案的資料在同一個 page 能提供足夠的空間了,不需要再連結到其他的 page.由於 hash function 的時間複雜度是 O(1),所以這是相當有吸引力的儲存方式.

因此,如果是要找資料的話,只是經過 hash function 運算,然後就知道去那一個 page 進行讀取,所以成本是 d,這前提是沒有 overflow page.如果有 overflow page,那成本就還是加上拜訪這些 overflow page 的成本.

如果動作是 insert,一樣經過 hash function 運算後,就知道要去那一個 page 進行資料寫入動作.假設該 page 仍有足夠大的空白空間,則成本也是 d.如果動作是 delete,一樣經過 hash function 運算後就知道要去那一個 page 做資料寫入的動作.假設沒有 overflow page 的情況,則 delete 的成本也是 d.

你可能會覺得用 hashed file 有很好的成本效果.在真實世界的情況下,資料量通常會大,當然硬碟空間也相對地很大,所以 page 的量也會很大.如果你今天要找的資料是很接近的,比如要找員工第一號和第二號,用 hashed file 的方式很可能會造成兩筆資料會被儲存在相距很遠的 page,雖然資料庫引擎很快就計算出來要去那些 page 抓資料,但是硬碟就會忙於東奔西跑去讀取資料,所以也別被 hashed file 的低成本給騙了.

不同的實體儲存方式都有各自的優點和缺點,資料庫引擎會看資料的情況或管理者的安排來決定什麼樣的安排方式會比較好.如果我們不是專門的資料庫管理員,這種實體的儲存方式通常不需要我們來操心.但若你是專門的資料庫管理員而且資料已經成長到相當大的數量時,此時你就必須要知道你的資料是如何被儲存,這樣才能幫助你思考效能改進的事情.再講下去的話就會偏向市面上的商業產品與工具了,所以就到此打住,畢竟此網誌儘量不講產品和工具,只講跟電腦科學有關的想法.

Share:

#31 程式該怎麼分辨好壞呢 ?

上個星期看到一篇短文,文章網址如下
http://buzzorange.com/techorange/2015/10/08/the-six-most-common-species-of-code/

這文章很有趣,它列出不同的人會如何寫出同一個 function.

看到學生寫的就是很標準的學生該有的答案.儘管 recursive 的寫法會有 call stack overflow 的問題,但對一個學生來說,重點是練習 recursive 的思考,所以這樣寫蠻好的.

看到由 Hackathon 寫出來的答案,哈哈,老實說,我真的是打從心裡笑了出來.這個笑可不是嘲笑的笑,而是一種打從心裡佩服的笑,尤其是看到那一句註解 // good enough for the demo

再來看看新創公司寫出來的,其實看不出來這和學生寫的有什麼大差別.也許作者在暗示些什麼?

再來看看大公司.還真的是有大公司寫法的樣子.你是否曾想過大公司的寫法為何是這樣子呢 ? 明明是個很簡單的數學式子而己.我能想到的幾個原因如下:

1.  大公司所生產的產品都具有一定的規模,所以在具有規模的產品下,一定會有許多設計是為了滿足軟體設計的一致性.所以,可想而知,這種具有規模的程式碼一定都是抽象再抽象化,把物件導向常用到的觀念一定都會套用進去,所以你才會看到也許明明是一個簡單的動作卻要搞的好像很複雜的樣子.

2. 大公司也是從小公司漸漸演變上來的,程式經過長時間的演變並且很可能經過許多不同工程師的改進與維護,所以,一般人很難能很快速地看懂程式碼,因為實在有太多故事在裡面了.

3. 在大公司工作的工程師們,平均來說也都是書念的比較好,程式寫的比較好的人.這些人心中或許有一些優越感存在.這些人也許為了顯出自己的優秀,真的會寫出很優秀的架構與運作流程,但由於實在太優秀了,所以對一般人來說就比較不容易懂.

最後,我們再回到 Fibonacci 數列.在電腦與數學的世界裡,這是一個非常有名的數列.在學校學習有關 recursive 或程式設計時的基本練習題目.也許你也可能在面試的時候會遇到這一個題目.一般來說,大家都會直覺地用 recursive 來寫這個題目,因為學校課本是這樣練習的.以時間複雜度的角度來看,recursive 的寫法並不是最好的,更何況它會有 call stack overflow 的問題.我會建議大家改成用 dynamic programming 的寫法,從小的數字算起,一直累積到大的數字,時間複雜度只有 O(n),而要付出的代價就是較多的記憶體空間.所以,Fibonacci 若把 recursive 改成用 dynamic programming 來寫的話就是典型的用空間換時間的方法.

再回到主題,那程式到底要怎麼寫才好呢 ? 其實,我欣賞 Hackathon 的寫法,重點並不在於寫的好不好,重點是這樣的寫法非常能表達出來作者明白所需要達成的目的是什麼.所以,把問題回歸本質,我們應該要問的是,輸入參數會是什麼,需要輸出什麼,有多少的 CPU 與記憶體可以用.把目的搞懂,再把限制條件搞清楚,這樣寫出來的程式不會離好程式太遠了.我們只要在我們關心的情況下讓我們的程式能正常運作就行了.在限制條件或需求之外,程式會有問題的話,那也不是我們需要關心的.

Share:

#30 Coding面試 - LeetCode #125 Valid Palindrome

題目的網址: https://leetcode.com/problems/valid-palindrome/

這一種題目算是蠻基本的,以前在台灣找工作時曾遇過有個公司的考卷出這一題,而在美國找工作時,目前還沒遇過有人考這一題或是相似的題目.

基本上,這題就是要寫一個 function 來驗證輸入的字串參數是不是 palindrome.所謂的 palindrome 就是字串中第一個位置和最後一個位置的值是一樣,第二個位置和倒數第二個位置的值是一樣,以此類推.所以一個很簡單的想法就是只要一個 for loop 就可以做完這樣的工作,而且不需要線性成長的空間,因此時間上是 O(n),空間上是O(1),這是對答案的要求了.

但 LeetCode 出的這一題有一點點小小的變化,因為輸入字串中可能會有其他的符號,而這些符號是不列入規則的,所以題目上寫 "A man, a plan, a canal: Panama" 是一個合格的 palindrome,因為只要不是符號類的字元都一律跳過.因為有這樣的小變化,我們除了用一個變數來記錄前面開始的比較位置,也還多用了一個變數來記錄從後面過來的比較位置.由於這是固定的兩個變數,數量不會隨著輸入參數而改變,所以在記憶體空間使用上是固定的.

另外,我們透過一個基本的 function (IsLetterOrDigit) 來幫助我們辦別輸入字元是字母還是數字,我們假設 IsLetterOrDigit 的時間複雜度是 O(1).實際上 O(1) 是可以做的到.最後,整個程式如下:



這個程式碼的時間複雜度是 O(n) , 而空間複雜度是 O(1).
這是一個很基本的考題,如果你的面試遇到這種題目,那表示面試官根本不想為難你.




Share:

#29 測試,該如何開始 ?

對一個好品質的軟體而言,測試的確是件很重要的事情.你寫的程式是否能在你假設的情況下做出正確的反應,這也是許多軟體工程師們的挑戰.一般較年輕的軟體工程師們往往過於著重在寫程式的技巧,所以常常忽略了花時間在學習如何測試你的程式.其實,軟體測試的學問完全不亞於一般的軟體開發所需的知識,而很多人可能會有一個先入為主的想法,那就是程式如何都寫不出來了,那學如何測試有什麼用呢 ? 聽起來好像還有幾分道理,至少在十多年前我是這樣想的.後來接觸多了之後也漸漸發現,如何不知道如何測試的話,那你怎麼能知道你寫出來的程式一定能用呢 ?

一般來說 (至少在我的工作環境下是如此),軟體工程師自己寫出來的 function 一定要自己寫好 unit test.這是蠻重要的事情,因為除了你,應該沒有人比你更了解你寫的 function 是什麼,但也因為如此,也容易造成自己在測試時會陷入一些假設的情境下而忘了一些測試條件.

我們來看一個很簡單的例子,

int Add(int x, int y) {
    return x+y;
}

以上是一個很簡單的加法,每個人都會寫.如果我們要為這個加法 function 寫一些 unit test,你會寫那些東西呢 ?

首先,我們先來找一些可行的例子,先來試試 test for pass,例如輸入 x=0, y=0 或輸入 x= 10, y = 10 之類的,如果你能得到正確的答案,那看來這個 function 是可以用的.

接下來,我們就要再多想一想,我需要把所有可能的數字組合都測試過一遍以確保這個 function 是正常的嗎 ? 如果你有時間的話,當然應該是要這麼做,因為一旦這個 function 送到客戶那執行時,你已經把所有可能的輸入組合都測試過了,所以你當然知道會不會有問題.但我們真的需要這麼做嗎 ? 其實也不用.關於這點,你可以在一般的軟體測試文章或書藉裡看到一個所謂邊界值條件的測試.據經驗來看,讓程式發生問題時有較高的機率會發生在邊界值的情況,例如 x = int.MaxValue 或 y=int.MaxValue,當你一輸入進去時,你就會發生這個 function 其實是有問題的,因為 integer overflow 了.

接下來,既然 x 和 y 是 int,那表示負數也是接收的,所以你還可以測試負數的邊界值,x = int.MinValue , y = int.MinValue,同樣的也會發生 overflow 的錯誤.所以,你就可以看到只要你把 x, y 的數值在可接受的範圍上跑了一遍,你就會發現這個加法 function 到底有沒有問題了.

接下來,你可能會說現在市面上寫的專案程式不是那麼單純呀.沒錯,我同意你的看法,的確都不單純,但寫 unit test 的精神還是一樣的.再看一個簡單的例子,

Result[] GetData( Connection conn, Command cmd) {
  .....
}

以上的 function 是一個根據命令 (Command) 在連線 (Connection) 上做一個取得資料的動作,取得到的資料將會以 Result[] array 的形式傳出來.

按照上述的第一步,你應該要先測試  test for pass 的情況,傳入正確的 connection 以及正確的 command,然後檢查是不是會得到結果.

接下來,我們可以調整 conn 和 cmd 的物件屬性,來測試 GetData 會如何反應.例如,故意把 cmd 輸入誤會的命令,或是故意把 conn 的狀態設定為關閉.甚至你還可以傳入空物件,看看 GetData 的行為是不是符合預期.

所以,你應該可以感覺到撰寫這些 unit test 的時間與力氣並不會比較少.尤其是一旦軟體能做的事情越多時,這些輸入參數的可能變化將是成指數形式往上成長,我們根本很難把所有可能的情況都試過一遍.如果你真的都試過了,那麼客戶會遇到的問題一定都會在你的掌握之中,甚至你可以在交貨之前就先修正這些問題了.但畢竟這只是理想,對一個具有某程規模以上的軟體而言,這幾乎是不太可能的事情,一來是時間,二來是預算等等的考量.所以,我們要做的也是在時間與預算範圍內能包含大部份的情況,比如 90% 的客戶都不會有問題等.

其實在設計程式時也有些技巧可以幫助你做測試,這些將都是未來文章的內容.


Share: