[TS] Set、flatMap 優化資料搜尋

最近在工作中遇到需要從複雜的資料結構中提取特定資料的問題,面對這種多層結構的資料直接暴力搜尋效能明顯不太理想,正確的方式應該是先將資料做「扁平化處理」,再透過更高效的搜尋策略來提升效能。
情境與實作
最近工作上碰到需要從一個多層結構的資料中撈出特定資料,然後從另一筆資料中撈出 id 互相匹配後,再組合出一個乾淨的資料做渲染。
假設現在需要從 product_variation 的 categories 中先撈出 giveaways,其中會夾雜不同資料,你需要透過 isGiveaway 去判斷。
結構可能會長這樣:
{ "categories": [ { "id": 1, "name": "優惠活動", "subcategories": [ { "id": 11, "name": "滿額贈品", "products": [ { "product_id": 102456, "name": "高級禮盒", "isGiveaway": true, "details": { "price": 0, "attributes": { "color": "紅色", "size": "大" }, "availability": { "inStock": true, "limitedEdition": false } } }, { "product_id": 102457, "name": "豪華贈品", "isGiveaway": true, "details": { "price": 0, "attributes": { "color": "金色", "size": "中" }, "availability": { "inStock": false, "limitedEdition": true } } } ] } ] }, { ... } ]}
另外一個 product_id 已經提供,只需要從 selectedProducts 中撈出即可。
{ "selectedProducts": [ { "product_id": 102780, "name": "智能手機", "isGiveaway": false }, { "product_id": 102457, "name": "豪華贈品", "isGiveaway": true }, ],}
大致看一下結構有三層,至少需要先用三個迭代,再透過搜尋撈出特定資料後進行匹配。
const giveaways = productTable.categories.map((category) => category.subcategories.map((subcategory) => subcategory.products.filter((product) => productSelected.selectedProducts?.some((selectedProduct) => selectedProduct.product_id === product.product_id && selectedProduct.isGiveaway ) ) )).flat(2) // 使用 flat 將資料扁平化成二維
結果可能會像這樣
但這樣的寫法效能是很差的,過程中使用了兩個 map 又一個 filter 和 some,雖然最後有使用了 flat 將資料攤平,但還有很大的優化空間。
-
盡量避免嵌套式結構,必要時寫成一個 constants
-
flat 和 map 可以整合成 flatMap
-
搜尋策略可以改用效能更高的 set
優化後的結果:
const selectedProductIds = new Set( productSelected.selectedProducts.map((selectedProduct) => selectedProduct.product_id))const subcategoriesMap = productTable.categories.flatMap((category) => category.subcategories)const allProducts = subcategoriesMap.flatMap((subcategory) => subcategory.products)const giveaways = allProducts.filter((product) => selectedProductIds.has(product.product_id) && product.isGiveaway)
來說說這段優化的過程中做了什麼
-
將 selectedProducts 的 product_id 抽出來到 Set 中,Set 有更高的搜尋效率(後續會解釋),減少查找 product_id 的時間成本。
-
使用 flatMap 來將每個 category 中的 subcategories 做扁平化處理,這樣我們就得到了所有的子類別資料,而不需要在內部多次嵌套 map。
-
接著再使用 flatMap 來將每個 subcategory 中的 products 提取出來,這樣所有產品都在一個扁平化的陣列中,避免了多次遍歷和過多的嵌套(這邊 flatMap 其實也可以寫在一起,讓扁平化只做一次,不過抽出來的可讀性與維護性更高)。
-
最後使用 filter 來檢查每個產品是否在 selectedProductIds 中,並且判斷是否為贈品。
這樣優化過後程式碼也變得更簡潔易讀,且在資料量更大時能有更高的效能。
關於 Set 和 flatMap
為何 Set 相較 some 或 find 的效能更高?
Set 的優勢
首先 Set 是一種「無重複的集合」,在搜尋以及去重複的情境具高效能。
它的底層是使用「Hash Table」來實作資料儲存,其搜尋的時間複雜度最高為 𝑂(1)。
而 some 或 find 是基於 Array 實現的,這表示說它們是線性搜尋 𝑂(n) 的,會一個個地遍歷陣列中的元素,直到找到符合條件的元素為止,或者遍歷完整個陣列。
因此在大數據集合中,檢查某個元素是否存在的速度,Set 會比遍歷整個陣列的 some 或 find 快得非常多。
應用情境:
-
去重複:
const setA = [1, 2, 3, 4];const setB = [3, 4, 5, 6];const combinedData = new Set([...setA, ...setB]);console.log([...combinedData]); // [1, 2, 3, 4, 5, 6]這段也可以使用 filter 和 indexOf 實作,但 Set 去重複效能比兩者來的更高其時間複雜度從 𝑂(𝑛^2)(基於 filter + indexOf)降低到 𝑂(𝑛)。
flatMap 的優勢
flatMap 是一個合併了 map 和 flat 操作的方法,如果是分別使用 flat 和 map 就需要遍歷兩次,合併後通過一次遍歷就能完成兩個操作,提高效能。
較適合需要對陣列進行多層處理及扁平化處理的情境。
例如:
const employees = [ { department: 'IT', members: ['Alice', 'Bob'] }, { department: 'HR', members: ['Carol', 'David'] },];const allMembers = employees.flatMap((employee) => employee.members);// ['Alice', 'Bob', 'Carol', 'David']
總結
遇到複雜的資料結構可以先嘗試將資料攤平,做扁平化處理後再透過更高效的搜尋策略來過濾出對應的資料。
-
Set:
- 底層使用 Hash Table 實作,善於處理重複數據和集合操作,時間複雜度更低。
- 適合需要頻繁搜尋和比較的情境。
-
flatMap:
- 合併 flat 和 map 方法,在單次遍歷中同時完成映射和扁平化,降低記憶體和時間的消耗。
- 適合處理嵌套結構的資料,將資料映射並進行扁平化處理,從而有效簡化處理過程。