1. 程式人生 > 其它 >後端返回name_為了解析後端資料,我竟然寫了個遞迴?

後端返回name_為了解析後端資料,我竟然寫了個遞迴?

技術標籤:後端返回name

程式碼倉庫: https:// github.com/Haixiang6123 /tree-parser 代理均經過單元測試

曾經的我特別討厭 LeetCode 演算法題,當時就覺得寫專案好玩,演算法沒什麼用。不喜歡歸不喜歡,為了面試,還是寫了 476 道題 = =。非常感激默默地刷題的那段時光,在處理資料方面確實給了我不一樣的思路。演算法和資料結構果然還是基本功呀。

需求

我接到的需求很簡單:後端返回一個 JSON,頁面展示多個下拉選擇器,根據使用者不同的選擇篩選不同的資料。例如:

c4f81084365e185fd901f30cf5c84908.png

而後端給我們的資料是這樣的:

const data = {
  '2020-10-10': {
    success: {
      text: [
        {name: '張三', content: '你好'},
        {name: '李四', content: '哈哈哈哈'},
        {name: '王五', content: 'EZEZ'},
      ],
      audio: [
        {name: '小明', content: '喂喂喂'},
        {name: '小紅', content: 'Hello'},
      ]
    },
    fail: {
      text: [
        {name: '張三', content: 'Yoyoyo'},
        {name: '李四', content: 'yeyeye'},
        {name: '王五', content: 'rerere'},
      ],
      audio: [
        {name: '小明', content: '失敗了哦'},
        {name: '小紅', content: '你好呀'},
      ]
    },
    sending: {
      text: [
        {name: '張三', content: '正在傳送'},
        {name: '李四', content: '傳送著的文字'},
      ],
      audio: [
        {name: '小明', content: '在弄了'},
        {name: '小紅', content: '很簡單'},
      ]
    }
  },
  '2020-10-11': {
   ...
  }
  ...
}

乍一看,感覺這個結構還是很清晰的,時間,訊息狀態,訊息型別,再到訊息內容都以樹狀結構返回。但是,對應到我們的需求,就感覺有點不對勁。

問題

首先,我們要展示可選項,按這裡的需求就是時間、訊息狀態、訊息型別。那就要對應3個數組:

const dateOptions = ['2020-10-10', '2020-10-11']
const statusOptions = ['success', 'fail', 'sending']
const typeOptions = ['text', 'audio']

剛開始可能會想用 Object.keys() 去拿,但是如果我們不斷地迴圈,迴圈再迴圈把這些拼起來就會現好麻煩啊。

第二個問題我們要面對的就是怎麼去獲取選中結果的過濾結果。假如選中 2020-10-10 就要在這個物件裡將數組裡的內容都拼在一起,以此類推。

其實這兩個問題抽象出來就是怎麼在一棵樹裡收集結果,那都簡單的方法就是 DFS 或者 BFS 去找。

實現

收集所有結果

最好的效果就是丟什麼物件進去都可以直接返回那個物件下所有陣列的合集,例如:

// 返回所有資料
collectArraysDFS(data)
// 返回 2020-10-10 下面的資料
collectArraysDFS(data['2020-10-10'])
// 返回 2020-10-10 且成功的資料
collectArraysDFS(data['2020-10-10']['success'])
...

這個問題還算比較簡單,使用 DFS 是比較好做的,只要判斷當前是否為 Array,如果是 Array,則加入結果,否則如果是 Object,則進入下一步的遞迴。

const collectArraysDFS = (object) => {
  if (!object) { return [] }

  // 如果本身就是陣列,直接返回
  if (object instanceof Array) { return object }

  return Object.values(object).reduce((prev, value) => {
    // 繼續遞迴
    if (value instanceof Object) {
      prev = prev.concat(collectArraysDFS(value));
    }

    return prev;
  }, []);
};

BFS 實現的版本:

const collectArraysBFS = (object) => {
  if (!object) { return [] }

  let queue = [object];
  let result = [];

  while (queue.length > 0) {
    const curtNode = queue.pop();

    // 如果是陣列,則存起來
    if (curtNode instanceof Array) {
      result = result.concat(curtNode);
    }

    // 如果還是物件,則繼續下一層
    if (curtNode instanceof Object) {
      const values = Object.values(curtNode);
      queue = queue.concat(values);
    }
  }

  return result;
};

收集所有選項

我們希望的效果是,給一個物件,我要哪一層的 key,就返回哪一層的 key,如:

// 返回第2層的所有的key
collectKeysDFS(data, 2, 1)

這裡的思路是 DFS 走完整個樹,然後設定好一個 targetLevel,表示只會收集那一層的所有 keys 就好,同時我們還需要 step 來計算當前層。只要在到了 targetLevel,就 Object.keys() 一下,表返回結果,在前面的層則負責收集結果就好了。最後回到 root,就能收集到所有的 key。

簡單的 DFS 實現如下:

const collectKeysDFS = (object, targetLevel, step) => {
  if (!object || targetLevel < step) { return [] }

  // 到達層數,返回所有 keys
  if (step === targetLevel) {
    return Object.keys(object);
  }

  // 繼續遞迴
  return Object.values(object).reduce((prev, value) => {
    if (value instanceof Object) {
      return prev.concat(collectKeysDFS(value, targetLevel, step + 1));
    }
  }, []);
};

使用 BFS 的版本:

const collectArraysBFS = (object) => {
  if (!object) { return [] }

  let queue = [object];
  let result = [];

  while (queue.length > 0) {
    const curtNode = queue.pop();

    // 如果是陣列,則存起來
    if (curtNode instanceof Array) {
      result = result.concat(curtNode);
    }

    // 如果還是物件,則繼續下一層
    if (curtNode instanceof Object) {
      const values = Object.values(curtNode);
      queue = queue.concat(values);
    }
  }

  return result;
};

另一種思路

另一種我想到的思路是將上面的樹狀結構變回資料表那樣的 Table 結構,即陣列:

[
  { date: 'xxx', status: 'xxx', type: 'xxx', data: {...} }
]

有了這個表結構,過濾陣列這個需求就會變得更加簡單,不再需要上面的BFS或者DFS了:

// 直接選出所有 date 為 'xxx' 的資料
table.filter(item => item.date === 'xxx')

要變成 Table 結構,很簡單的一個想法就是要求出原始資料裡的每條 root 到 leaf 的 path,同時對每個 key 都設定一個名字,這裡就叫 keyName,物件裡的 key 反而變成了 value,可以參考 LeetCode 這道題。

思路就是我們先要給定每個 key 對應的 keyName 陣列,每到一層的時候獲取這個 key,以及 keyName 就好了。到最後一層就直接包住最後一層的物件即可完成該次遞迴。下面直接展示我的解法吧:

// Helper 函式
const dfsHelper = (object, names, step, tempRow, result) => {
  // 異常值的情況
  if (!object) return;

  // 如果超過 names 長度,則開始蒐集結果
  if (step === names.length) {
    return result.push({ ...tempRow });
  }

  // 獲取對應的 key 的名字
  const keyName = names[step];

  // 如果值為陣列,則直接 push 數組裡的每個元素
  if (object instanceof Array) {
    return object.map(item => {
      result.push({
        ...tempRow,
        [keyName]: item,
      });
    })
  }

  const [key] = Object.keys(object);
  const values = Object.values(object);

  // 複製一份臨時的物件
  tempRow = { ...tempRow, [keyName]: key };

  // 繼續下一層的遞迴
  values.forEach((value => {
    dfsHelper(value, names, step + 1, tempRow, result);
  }))
}

const toTable = (object, names) => {
  if (!object) return [];

  let result = [];

  dfsHelper(object, names, 0, {}, result);

  return result;
}

最後

這次的思考給我的感受就是演算法雖然在平時專案沒什麼用,但是在解決基礎底層問題的時候確實可以考驗一個程式設計師的基本素養,會給自己不一樣的解決思路。

(完)