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

#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:

#28 資料庫基礎 - 存取 Page (Hashed File)

前面兩篇文章提到了存取 page 的兩種方式,在這一篇文章裡要介紹的是另一種方式,我們稱它為 hashed file.聽到這個名字,你可能已經猜到這是和 hash function 有關係的儲存方式.沒錯,它就是利用 hash function 來決定儲存位置,也是就說透過 hash function 的計算來決定要儲存到那一個 page 上.

前面的文章曾提到過基本的 hash function,也介紹了它最簡單的運算方式.所以,假設一開始有十個 page,然後有五筆資料,我們可以將每筆資料傳給 hash function,計算出來的結果是 0 - 9 的數字,而這數字就用來代表要儲存到那一個 page 上.一旦資料變多時,資料的筆數一定會遠遠大於 page 的數量,所以同一個 page 上一定會有許多筆資料.因此,透過 hash function 的方式來計算儲存位置也是個蠻快速的方法.不過,資料庫引擎通常不會被設計成將整筆資料拿來 hash,因為我們在找資料時很少會用全部的欄位做為搜尋的條件,因此,我們通常會選擇幾個重要的欄位或是只選擇 primary key 的欄位來做為 hash function 的輸入值,而 hash function 輸出值就是 page 的號碼.因此,資料庫引擎在為我們找資料的時候,只要欄位條件符合的話,就可以透過 hash function 的計算而快速地得到 page 號碼.

接下來,你可能會問到,一旦資料量越來越大時,一定會有很多的資料經過 hash function 計算後會得到同樣的答案,而這些資料量會遠遠超過一個 page 得記錄的資料量.這種情況在我們談論 hash function 的時候也會發生,當時所採用的方法就是在 bucket 上做一個 list 來承接更多的資料.相同地,在這裡也是可以用相同的觀念.如下圖:


如上圖的上面那個 page,當它沒有足夠的空間承載更多資料時,此時資料庫引擎就必需要一個空白的 page ,然後在前面的 page 做一個連結到空白的 page,接著就可以把屬於同一個 bucket 的資料寫進去.因此,你可以看見的是,如果 hash function 的 bucket 準備不多的話,那麼資料就會長成像幾條很長的 page life,我們並不希望碰到這樣的情況.所以若要採用 hashed file 的儲存方式,則該 hash function 需要有能力產生足夠多的 bucket,甚至最好是能彈性處理,依照資料的多少來決定,但相對地要做多的配套措施.但若以好的角度來看,用 hashed file 的結構來儲存資料,對資料庫引擎而言可以大大提供資料搜尋的速度.

有關資料如何儲存在 page 上,從儲存的方式來看基本上有三種方式,而每一種方式都有其優點和缺點,所以針對不同的資料,根據他們的特性來決定用那一種儲存方式將是比較好的決定.要怎麼評估呢 ? 我會把評估的成本估算寫在未來的文章裡,讓大家知道資料庫引擎是根據什麼樣的數據來決定要用什麼方式儲存資料.


Share:

#27 Coding 面試 Leetcode #98 Validate Binary Search Tree

我記得這一題我被問過兩次,是兩個不同的團隊,而且都是跟資料庫有關的工作.所以,若你也要尋找資料庫相關的開發工作,看來 Tree 的題目是很難避免的.

這題的題目很單純,就是要寫一個 function 用來驗證題目的 Tree 是不是 Binary Search Tree (BST).
根據 BST 的定義,左邊的節點值必需小於父節點值,而右邊的節點值必需大於父節點值.
但這題有一點要小心的是,別只是檢查父節點值而己,應該要檢查所有上層的父節點值都要小於或是大於.

如果題目只給你 tree root,那麼你可以寫出像下面的 function 來檢查這個 tree 是不是 BST.

        public bool IsValidBST(TreeNode root)
        {
            return BST(root, null, null);
        }

        bool BST(TreeNode node, int? min, int? max)
        {
            if (node == null) return true;
            if (min == null && max == null)
            {
                    return BST(node.left, min, node.val ) && BST(node.right, node.val , max);
            }
            else if (min != null && max == null)
            {
                if (node.val > min )
                    return BST(node.left, min, node.val) && BST(node.right, node.val, max);
            }
            else if (min == null && max !=null)
            {
                if ( node.val < max)
                    return BST(node.left, min, node.val) && BST(node.right, node.val, max);
            }
            else
            {
                if (node.val > min && node.val < max)
                    return BST(node.left, min, node.val) && BST(node.right, node.val, max);
            }
            return false;
        }

若你仔細看的話,我是把每個分支出去時可能的節點值範圍也傳遞過去.這樣的寫法也許不是最好,但就如之前文章所說,要在五分鐘內想好然後在半小時內寫完再測試,所以就勉強用了.

這時候如果遇到要求更高的面試人員,他可能會說 recursive 的方法不適合用在 tree,所以要改成 iterative 的方式來寫.
其實要改成 iterative 的方式,說難不難,說簡單也不簡單,因為要在那短時間想出來也是件不容易的事.比較幸運的是之前我有練習過 BST 用 In-order traversal 的走法來驗證 BST,所以這題也就順利寫完了,以下就是改成 iterative 的方式,用 In-order 走訪 BST 之後,得到的答案一定是從小排到大的數值,所以只要多一個 for loop 來檢查數值是不是從小排到大即可.

  public bool IsValidBST(TreeNode root)
        {
            if (root == null) return true;
            List<int> re = InOrder(root);
             if (re.Count == 1) return true;
            for (int i = 0; i < re.Count - 1; i++)
            {
                if (re[i] >= re[i + 1])
                    return false;
            }
            return true;
        }

        List<int> InOrder(TreeNode node)
        {
            List<int> result = new List<int>();
            Stack<TreeNode> stack = new Stack<TreeNode>();
            while (stack.Count != 0 || node != null)
            {
                if (node != null)
                {
                    stack.Push(node);
                    node = node.left;
                }
                else
                {
                    TreeNode t = stack.Pop();
                    result.Add(t.val);
                    node = t.right;
                }
            }
            return result;
        }

雖然在初步面談中都把上述的題目做完了,但這也不代表就有機會可以繼續做後續的面試.所以,很可惜沒能做成資料庫開發的工作,找工作真的一切都是個緣份.





Share:

#26 資料庫基礎 - 存取 Page (Sorted File)

上一篇文章談的內容是針對資料之間是沒有順序的情況,因此在尋找資料時就是採用依照資料的排列順序來尋找.因此整個尋找過程的時間複雜度是 O(n) .通常來說,資料庫設計者不會讓這樣的事情發生,除非資料本身是一個相當小的參考資料而己,否則資料一旦多了,  O(n) 實在不是一個好現象.

在真實世界中的應用,我們常常可以為資料找到至少一種排列方法,例如,在學校的資料庫中,基本上可以用學生證的號碼來排序學生的資料,因為在同一個學校裡不會有相同的學生證號碼.我們在前面的文章中也提過,一旦資料排序過了之後,時間複雜度就會從 O(n) 下降成 O(log n),基本上就是 Binary search.因此,如果資料可排序的話,則資料庫引擎就可以依序地將資料寫在檔案中.

資料庫引擎為這些可排序的資料寫入到檔案中有兩個方式.第一種方式是直接將資料依序排列好寫在檔案中,所以每個資料的前後順序就跟排列的順序是一模一樣的,如果中間要插入一筆新資料時,就很有可能會發生 page split 的動作.如圖是一個例子.紅色的框框代表 page,而藍底的框框帶表一筆資料,而藍框裡的數字代表資料的序號,序號不一定要連續,只要不重複即可.所以一開始把資料寫入時,它的排列長相如下:


這時候如果要新增一筆序號 7 的資料,那麼它要排在 5 和 10 之間,但是該 page 已經沒有位置了,因此發生 page split.


資料 7 會放在前面的 page 還是後面的 page,這將由資料庫引擎內定的邏輯來決定.

第二種將資料寫入檔案的方式是讓資料的實體位置不需要改變,另外在建立 "目錄",然後在這目錄上做 binary search 的動作.


在這種方式下,新增一筆序號 7 的資料時,它就可以直接在 page n+1 的地方寫入,然後資料庫引擎只要更新目錄的內容即可.

當新增一筆序號 7 的資料在目錄 page 時,此時會發現  page split 還是在有足夠的空間下把後面的資料往後移,這將由資料庫引擎的內定邏輯而定.因此,第二種方式就變成直接在目錄 page 上做  binary search ,然後再依照其內容 (pointer) 就可以找到該筆資料.

所以,你的 table schema 的設定理應當是都有 primary key 的存在,如此一來才能做為資料庫引擎排序的依據.其實,以上的內容基本上也就是 clustered index 和 non-clustered index 的精神.

Share: