從乙個我常用的面試題,也是真實需求開始聊起:
你需要在前端展示 5000 條甚至更多的資料,每一條資料的資料結構是乙個物件,裡面有格式各樣的屬性。每個屬性的值又可以是基本型別,物件,甚至陣列。這裡的物件或者陣列內部的元素又可以繼續包含物件或者陣列並且允許無限巢狀下去。比如
,
"age": 23,
"roles": ['developer', 'admin'],
"projects":
}
如何設計這個功能,讓搜尋功能盡可能的快?
如果你稍有程式設計師的敏感度,此時你的腦海裡應該有兩個念頭:
的確,遍歷是最簡單但也是最慢的。所以通常的優化方法之一是通過空間換取時間;而另乙個方法……稍後再引出。
這裡我們嘗試通過建立字典樹(trie)來優化搜尋。
如果你還不了解什麼是字典樹,下面做簡單的介紹:假設我們有乙個簡單的物件,鍵值的對應關係如下:
我們根據「鍵」的字母出現順次構建出一棵樹出來,葉子節點值即有可能是某個「鍵」的值
那麼此時無論使用者想訪問任何屬性的值,只要從樹的根節點出發,依據屬性字母出現的順序訪問樹的葉子節點,即可得到該屬性的值。比如當我們想訪問tea
時:
但是在我們需要解決的場景中,我們不需要關心「屬性」,我們只關心「值」是否匹配上搜尋的內容。所以我們只需要對「值」建立字典樹。
假設有以下的物件值
const o =
建立的樹狀結構如下:
root--a
|--c
|--k
|--p
|--p
|--l
|--e
|--n
|--n
|--a
但實際工作中我們會有非常多個物件值,多個物件值之間可能有重複的值,所以匹配時,我們要把所有可能的匹配結果都返回。比如
[
, ,
]
上面兩個物件有相同的值ack
和an
,所以在樹上的葉子節點中我們還要新增物件的 id 辨識資訊
root--a
|--c
|--k (ids: [1,2])
|--p
|--p
|--l
|--e (ids: [1])
|--n (ids: [1, 2])
|--n
|--a (ids: [1])
這樣當使用者搜尋an
時,我們能返回所有的匹配項
ok,有了思路之後我們開始實現**。
首先要解決的乙個問題是如果快速的偽造 5000 條資料?這裡我們使用 開源 api。為了簡單起見,我們讓它只返回gender
,email
,phone
,cell
,nat
基本資料型別的值,而不返回巢狀結構(物件和陣列)。注意這裡只是為了便於**展示和理解,略去了複雜的結構,也就避免了複雜的**。加入複雜結構之後**其實也沒有大的變化,只是增加了遍歷的邏輯和遞迴邏輯而已。
請求 ?results=5000&inc=gender,email,phone,cell,nat 結果如下:
,
//...
]}
根據思路中的描述,資料結構描述如下:
class leaf ;
} share(id)
}
share
方法用於向該葉子節點新增多個相同的匹配的id
在編碼的過程中我們需要一些幫助函式,比如:
這兩個函式可以借用lodash
類庫實現,即使手動實現起來也很簡單,這裡就不贅述了
另乙個重要的方法是normalize
,我更習慣將normalize
翻譯為「扁平化」(而不是「標準化」),因為這樣更形象。該方法用於將乙個陣列裡的物件拆分為 id 與物件的對映關係。
比如將
[
, ,
]
扁平化之後為
,
'2':
}
之所以要這麼做是為了當檢索結果返回乙個 id 陣列時:[1, 2, 3]
,我們只需要遍歷一邊返回結果就能通過 id 在扁平化的map
裡立即找到對應的資料。否則還要不停的遍歷原始資料陣列找到對應的資料.
因為 randomuser.me 返回的資訊中不包含 id 資訊,所以我們暫時用 email 資訊作為唯一標示。normalize
的實現如下:
function normalize(identify, data) ;
data.foreach(item => );
return id2value;
}
這部分**就沒有什麼秘密了,完全是按照遞演算法歸構建一顆樹了
fetch("?results=5000&inc=gender,email,phone,cell,nat")
.then(response => )
.then(data => = data;
const root = new leaf();
const identifykey = "email";
results.foreach(item => else
temproot = temproot.children[character];
}});
});});
搜尋部分**也沒有什麼秘密,按圖索驥而已:
function searchblurry(root, keyword, usermap) else
if (keywordarr.length - 1 === i)
} return distinct(result).map(id => );
}
為了對比效率,並且為了測試搜尋結果的正確性,我們仍然需要編寫乙個常規的遍歷的搜尋方法:
function regularsearch(searchkeyword)
}});
return regularsearchresults
}
效能的對比結果是很有意思的:
效率反而低的問題不難想到是為什麼:當你搜尋詞簡單時,訪問的葉子節點會少,所以只能掃瞄children
收集子節點的所有的可能 id,這步操作中遍歷的過程占用了大部分時間
但是我們仍然需要滿足這部分的查詢需求,所以我們要針對這個場景做一些優化
我們回想一下簡單搜尋的場景,效能的瓶頸主要在於我們需要遍歷葉子節點下的所有子節點。好辦,鑑於樹構建完之後不會再發生變化,那麼我們只需要提前計算好每個葉子節點的所以子 id 就好了,這就是文章開頭說的第二類優化方案,即預計算。
我編寫了乙個新的方法,用於遞迴的給每個葉子節點新增它所有子節點的 id:
function decoratewithchildrenids(root) = root;
root.childrenids = collectchildreninsideids(root.children);
for (const character in children)
}
那麼在構建完樹之後,用這個方法把所有葉子節點「裝飾」一遍就好了
在通過預計算之後,在 5000 條資料的情況下,無論是短搜尋還是長搜尋,字典樹的查詢效率基本是在 1ms 左右,而常規的遍歷查詢則處於 10ms 左右,的確是十倍的提公升。但是這個提公升的代價是建立在犧牲空間,以及提前花費了時間計算的情況下。相信如果資料結構變得更複雜,效率提公升會更明顯
本文源**的位址是 (
最後留下乙個問題給大家:當需要搜尋的資料量變大時,比如 1000 時,偶爾會出現字典樹搜尋結果和遍歷搜尋結果不一致的情況,而當資料量變得更大時,比如 5000 條,那麼這個「問題」會穩定出現。這個問題算不上 bug,但是問題出在哪呢 ?
關鍵字搜尋
關鍵字搜尋 function sercah waitmsg 已找到對應的 g keys count 處關鍵字!1 相同關鍵字查詢時返回 reading children reading box m p css span keys removeattr style removeattr id g ke...
關鍵字查詢
題目描述 每次給你一篇文章,和一些關鍵字,需要你告訴我多少關鍵字將匹配於文章。輸入描述 第一行包含乙個整數,表示有多少篇文章。最後一行是文章,長度不超過1000000。輸出描述 輸出文章中包含多少關鍵字。輸入樣例 15 shehe sayshr heryasherhs 輸出樣例 3源 include...
ntext搜尋關鍵字
選擇自 zjcxc 的 blog ntext搜尋 按 tb 表中的 keyword 在 ta 中查詢 content 列出每個 keyword 在 content 中的具體位置 鄒建 2004.07 測試資料 create table ta id int identity 1,1 content n...