從一個需求談起
在我之前的專案中,曾經遇到過這樣一個需求,編寫一個級聯選擇器,大概是這樣:
圖中的示例使用的是Ant-Design的Cascader元件。
要實現這一功能,我需要類似這樣的資料結構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var data = [{ "value": "浙江", "children": [{ "value": "杭州", "children": [{ "value": "西湖" }] }] }, { "value": "四川", "children": [{ "value": "成都", "children": [{ "value": "錦裡" }, { "value": "方所" }] }, { "value": "阿壩", "children": [{ "value": "九寨溝" }] }] }] |
一個具有層級結構的資料,實現這個功能非常容易,因為這個結構和元件的結構是一致的,遞迴遍歷就可以了。
但是,由於後端通常採用的是關係型資料庫,所以返回的資料通常會是這個樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var data = [{ "province": "浙江", "city": "杭州", "name": "西湖" }, { "province": "四川", "city": "成都", "name": "錦裡" }, { "province": "四川", "city": "成都", "name": "方所" }, { "province": "四川", "city": "阿壩", "name": "九寨溝" }] |
前端這邊想要將資料轉換一下其實也不難,因為要合併重複項,可以參考資料去重的方法來做,於是我寫了這樣一個版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
'use strict' /** * 將一個沒有層級的扁平物件,轉換為樹形結構({value, children})結構的物件 * @param {array} tableData - 一個由物件構成的陣列,裡面的物件都是扁平的 * @param {array} route - 一個由字串構成的陣列,字串為前一陣列中物件的key,最終 * 輸出的物件層級順序為keys中字串key的順序 * <a href='http://www.jobbole.com/members/wx1409399284'>@return</a> {array} 儲存具有樹形結構的物件 */ var transObject = function(tableData, keys) { let hashTable = {}, res = [] for( let i = 0; i < tableData.length; i++ ) { if(!hashTable[tableData[i][keys[0]]]) { let len = res.push({ value: tableData[i][keys[0]], children: [] }) // 在這裡要儲存key對應的陣列序號,不然還要涉及到查詢 hashTable[tableData[i][keys[0]]] = { $$pos: len - 1 } } if(!hashTable[tableData[i][keys[0]]][tableData[i][keys[1]]]) { let len = res[hashTable[tableData[i][keys[0]]].$$pos].children.push({ value: tableData[i][keys[1]], children: [] }) hashTable[tableData[i][keys[0]]][tableData[i][keys[1]]] = { $$pos: len - 1 } } res[hashTable[tableData[i][keys[0]]].$$pos].children[hashTable[tableData[i][keys[0]]][tableData[i][keys[1]]].$$pos].children.push({ value: tableData[i][keys[2]] }) } return res } var data = [{ "province": "浙江", "city": "杭州", "name": "西湖" }, { "province": "四川", "city": "成都", "name": "錦裡" }, { "province": "四川", "city": "成都", "name": "方所" }, { "province": "四川", "city": "阿壩", "name": "九寨溝" }] var keys = ['province', 'city', 'name'] console.log(transObject(data, keys)) |
還好keys的長度只有3,這種東西長了根本沒辦法寫,很明顯可以看出來這裡面有重複的部分,可以通過迴圈搞定,但是想了很久都沒有思路,就擱置了。
後來,有一天晚飯後不是很忙,就跟旁邊做資料的同事聊了一下這個需求,請教一下該怎麼用迴圈來處理。他看了一下,就問我:“你知道trie樹嗎?”。我頭一次聽到這個概念,他簡單的給我講了一下,然後說感覺處理的問題有些類似,讓我可以研究一下trie樹的原理並試著優化一下。
講道理,trie樹這個資料結構網上確實有很多資料,但很少有使用JavaScript實現的,不過原理倒是不難。嘗試之後,我就將transObject
的程式碼優化成了這樣。(關於trie樹,還請讀者自己閱讀相關材料)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var transObject = function(tableData, keys) { let hashTable = {}, res = [] for (let i = 0; i < tableData.length; i++) { let arr = res, cur = hashTable for (let j = 0; j < keys.length; j++) { let key = keys[j], filed = tableData[i][key] if (!cur[filed]) { let pusher = { value: filed }, tmp if (j !== (keys.length - 1)) { tmp = [] pusher.children = tmp } cur[filed] = { $$pos: arr.push(pusher) - 1 } cur = cur[filed] arr = tmp } else { cur = cur[filed] arr = arr[cur.$$pos].children } } } return res } |
這樣,解決方案就和keys的長短無關了。
這大概是我第一次,真正將資料結構的知識和前端專案需求結合在一起。
再談談我在面試遇到的問題
目前為止我參加過幾次前端開發方面的面試,確實有不少面試官會問道一些演算法。通常會涉及的,是連結串列、樹、字串、陣列相關的知識。前端面試對演算法要求不高,似乎已經是業內的一種共識了。雖說演算法好的前端面試肯定會加分,但是僅憑常見的面試題,而不去聯絡需求,很難讓人覺得,演算法對於前端真的很重要。
直到有一天,有一位面試官問我這樣一個問題,下面我按照自己的回憶把對話模擬出來,A指面試官,B指我:
A:你有寫過瀑布流嗎?
B:我寫過等寬瀑布流。實現是當使用者拉到底部的一定高度的時候,向後端請求一定數量的圖片,然後再插入到頁面中。
A:那我問一下,如何讓幾列圖片之間的高度差最小?
B:這個需要後端發來的資料裡面有圖片的高度,然後我就可以看當前高度最小的是哪裡列,將新圖片插入那一列,然後再看看新的高度最小的是哪一列。
A:我覺得你沒有理解我的問題,我的意思是如何給後端發來的圖片排序,讓幾列圖片之間的高度差最小?
B:(想了一段時間)對不起,這個問題我沒有思路。
A:你是軟體工程專業的對吧?你們資料結構課有沒有學動態規劃?
B:可能有講吧,但是我沒什麼印象了。
對話大概就是這樣,雖然面試最終還是pass了,但這個問題確實讓我很在意,因為我覺得,高度差“最”小,真的能用很簡單的演算法就解決嗎?
這個問題的實質,其實就是有一個陣列,將陣列元素分成n份,每份所有元素求和,如何使每份的和的差最小。
搜尋上面這個問題,很快就能找到相關的解答,很基本的一類動態規劃問題——揹包問題。
之前我確實看過揹包問題的相關概念(也僅僅是相關概念)。當時我看到這樣一段話:
許多使用遞迴去解決的程式設計問題,可以重寫為使用動態規劃的技巧去解決。動態規劃方案通常會使用一個陣列來建立一張表,用於存放被分解成眾多子問題的解。當演算法執行完畢,最終的解將會在這個表中很明顯的地方被找到。
後面是一個用動態規劃重寫斐波那契數列的例子。我看到它只是將遞迴的結果,儲存在了一個陣列中,就天真的以為動態規劃是優化遞迴的一種方法,並沒有深入去理解。
不求甚解,確實早晚會出問題的。當時我雖然以為自己知道了演算法的重要性,但其實還是太年輕。
動態規劃可以求解一類“最優解”問題,這在某種程度上讓我耳目一新。由於本文主要還是為了說明資料結構與演算法對於前端的意義,關於動態規劃的細節,本文也不會涉及,而且水平確實也不夠。網上有許多非常好的博文,尤其推薦《揹包九講》。
多說兩句——一道思考題
將如下扁平物件,轉為樹形物件。parent
欄位為空字串的節點為根節點:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
var input = { h3: { parent: 'h2', name: '副總經理(市場)' }, h1: { parent: 'h0', name: '公司機構' }, h7: { parent: 'h6', name: '副總經理(總務)' }, h4: { parent: 'h3', name: '銷售經理' }, h2: { parent: 'h1', name: '總經理' }, h8: { parent: 'h0', name: '財務總監' }, h6: { parent: 'h4', name: '倉管總監' }, h5: { parent: 'h4', name: '銷售代表' }, h0: { parent: '', name: 'root' } }; |
這個需求在前端其實也很實際,示例中的物件是一個公司組織結構圖。如果需求是讓你在前端用svg之類的技術畫出這樣一張圖,就需要這個功能。(另外我想到的一種應用場景,就是在前端展示類似windows資源管理器的檔案樹)
我當時想了很久,沒有想到一個迴圈解決的方法,後來在stackoverflow上找到了答案:
1 2 3 4 5 6 7 8 9 10 11 12 |
var plain2Tree = function (obj) { var key, res for(key in obj) { var parent = obj[key].parent if(parent === '') { res = obj[key] } else { obj[parent][key] = obj[key] } } return res } |
這段程式碼,就是利用了JavaScript裡面的引用型別,之後的思路,和操作指標沒什麼區別,就是構造一棵樹。
但對於我來說,從來都沒有往樹和指標的那方面思考,就很被動了。
結語
以上列舉了三道題,希望可以引起大家對於在前端應用資料結構與演算法相關知識的共鳴。