爲何我認爲數據結構與算法對前端開發很重要?

從一個需求談起

在我以前的項目中,曾經遇到過這樣一個需求,編寫一個級聯選擇器,大概是這樣:前端

圖中的示例使用的是Ant-Design的Cascader組件。面試

要實現這一功能,我須要相似這樣的數據結構:算法

var data = [{
  "value": "浙江",
  "children": [{
    "value": "杭州",
    "children": [{
      "value": "西湖"
    }]
  }]
}, {
  "value": "四川",
  "children": [{
    "value": "成都",
    "children": [{
      "value": "錦裏"
    }, {
      "value": "方所"
    }]
  }, {
    "value": "阿壩",
    "children": [{
      "value": "九寨溝"
    }]
  }]
}]

一個具備層級結構的數據,實現這個功能很是容易,由於這個結構和組件的結構是一致的,遞歸遍歷就能夠了。數據庫

可是,因爲後端一般採用的是關係型數據庫,因此返回的數據一般會是這個樣子:編程

var data = [{
  "province": "浙江",
  "city": "杭州",
  "name": "西湖"
}, {
  "province": "四川",
  "city": "成都",
  "name": "錦裏"
}, {
  "province": "四川",
  "city": "成都",
  "name": "方所"
}, {
  "province": "四川",
  "city": "阿壩",
  "name": "九寨溝"
}]

前端這邊想要將數據轉換一下其實也不難,由於要合併重複項,能夠參考數據去重的方法來作,因而我寫了這樣一個版本。windows

'use strict'

/**
 * 將一個沒有層級的扁平對象,轉換爲樹形結構({value, children})結構的對象
 * @param {array} tableData - 一個由對象構成的數組,裏面的對象都是扁平的
 * @param {array} route - 一個由字符串構成的數組,字符串爲前一數組中對象的key,最終
 * 輸出的對象層級順序爲keys中字符串key的順序
 * @return {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樹,還請讀者本身閱讀相關材料)數據結構

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的長短無關了。svg

這大概是我第一次,真正將數據結構的知識和前端項目需求結合在一塊兒。

再談談我在面試遇到的問題

目前爲止我參加過幾回前端開發方面的面試,確實有很多面試官會問道一些算法。一般會涉及的,是鏈表、樹、字符串、數組相關的知識。前端面試對算法要求不高,彷佛已是業內的一種共識了。雖然說算法好的前端面試確定會加分,可是僅憑常見的面試題,而不去聯繫需求,很難讓人以爲,算法對於前端真的很重要。

直到有一天,有一位面試官問我這樣一個問題,下面我按照本身的回憶把對話模擬出來,A指面試官,B指我:

A:你有寫過瀑布流嗎?

B:我寫過等寬瀑布流。實現是當用戶拉到底部的必定高度的時候,向後端請求必定數量的圖片,而後再插入到頁面中。

A:那我問一下,如何讓幾列圖片之間的高度差最小?

B:這個須要後端發來的數據裏面有圖片的高度,而後我就能夠看當前高度最小的是哪裏列,將新圖片插入那一列,而後再看看新的高度最小的是哪一列。

A:我以爲你沒有理解個人問題,個人意思是如何給後端發來的圖片排序,讓幾列圖片之間的高度差最小?

B:(想了一段時間)對不起,這個問題我沒有思路。

A:你是軟件工程專業的對吧?大家數據結構課有沒有學動態規劃?

B:可能有講吧,可是我沒什麼印象了。

對話大概就是這樣,雖然面試最終仍是pass了,但這個問題確實讓我很在乎,由於我以爲,高度差「最」小,真的能用很簡單的算法就解決嗎?

這個問題的實質,其實就是有一個數組,將數組元素分紅n份,每份全部元素求和,如何使每份的和的差最小。

搜索上面這個問題,很快就能找到相關的解答,很基本的一類動態規劃問題——揹包問題。

以前我確實看過揹包問題的相關概念(也僅僅是相關概念)。當時我看到這樣一段話:

許多使用遞歸去解決的編程問題,能夠重寫爲使用動態規劃的技巧去解決。動態規劃方案一般會使用一個數組來創建一張表,用於存放被分解成衆多子問題的解。當算法執行完畢,最終的解將會在這個表中很明顯的地方被找到。

後面是一個用動態規劃重寫斐波那契數列的例子。我看到它只是將遞歸的結果,保存在了一個數組中,就天真的覺得動態規劃是優化遞歸的一種方法,並無深刻去理解。

不求甚解,確實遲早會出問題的。當時我雖然覺得本身知道了算法的重要性,但其實仍是太年輕。

動態規劃能夠求解一類「最優解」問題,這在某種程度上讓我耳目一新。因爲本文主要仍是爲了說明數據結構與算法對於前端的意義,關於動態規劃的細節,本文也不會涉及,並且水平確實也不夠。網上有許多很是好的博文,尤爲推薦《揹包九講》。

多說兩句——一道思考題

將以下扁平對象,轉爲樹形對象。parent字段爲空字符串的節點爲根節點:

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上找到了答案:

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裏面的引用類型,以後的思路,和操做指針沒什麼區別,就是構造一棵樹。

但對於我來講,歷來都沒有往樹和指針的那方面思考,就很被動了。

結語

以上列舉了三道題,但願能夠引發你們對於在前端應用數據結構與算法相關知識的共鳴。

相關文章
相關標籤/搜索