程序員編程藝術第三十六~三十七章、搜索智能提示suggestion,附近點搜索

        第三十六~三十七章、搜索智能提示suggestion,附近地點搜索


作者:July。致謝:caopengcs、胡果果。
時間:二零一三年九月七日。


題記

    寫博的近三年,整理了太多太多的筆試面試題,如微軟面試100題系列,和眼下這個程序員編程藝術系列,真心覺得題目年年變,但解決問題的方法永遠都是那幾種,用心準備後,自會發現一切有跡可循。

    故爲更好的幫助人們找到工作,特准備在北京舉辦一系列面試&算法講座。時間定爲週末,每次一個上午或下午,受衆對象爲要找工作或換工作或對算法感興趣的朋友,費用前期暫願交就交,交多少全由自己決定。主講人:我和目前zoj排名第一的caopengcs博士。9月15日爲第1次講座http://blog.csdn.net/v_july_v/article/details/7237351#t22

    OK,切入正題。上面說整理過很多筆試面試題,但好的筆試面試題真心難求,包括在編程藝術系列每一章的選題,越到後面越難挑,而本文寫兩個跟實際掛鉤的問題,它們來自此文http://blog.csdn.net/v_july_v/article/details/7974418 的3.6題,和第87題,即

  • 第三十六章、搜索引擎中中的關鍵詞智能提示suggestion;
  • 第三十七章、附近地點的搜索;
    本文的兩個選題都是比較開放的,沒有固定標準的答案。讀者若有何意見,或是發現了任何問題,歡迎隨時於本文評論下留言或指正,感謝。


第三十六章、搜索關鍵詞智能提示suggestion

題目詳情:百度搜索框中,輸入“北京”,搜索框下面會以北京爲前綴,展示“北京愛情故事”、“北京公交”、“北京醫院”等等搜索詞,輸入“結構之”,會提示“結構之法”,“結構之法 算法之道”等搜索詞。
請問,如何設計此係統,使得空間和時間複雜度儘量低。

題目分析:本題來源於去年2012年百度的一套實習生筆試題中的系統設計題(爲尊重願題,本章主要使用百度搜索引擎展開論述,而不是google等其它搜索引擎,但原理不會差太多。然脫離本題,平時搜的時候,鼓勵用...),題目比較開放,考察的目的在於看應聘者解決問題的思路是否清晰明確,其次便是看能考慮到多少細節。

    我去年整理此題的時候,曾簡單解析過,提出的方法是:
  • 直接上Trie樹Trie樹的介紹見:從Trie樹(字典樹)談到後綴樹」 +  TOP Khashmap+堆,hashmap+堆 統計出如10個近似的熱詞,也就是說,只存與關鍵詞近似的比如10個熱詞
    方法就是這樣子的:Trie樹+TOP K算法,但在實際中,真的只要Trie樹 + TOP K算法就夠了麼,有什麼需要考慮的細節?OK,請看下文娓娓道來。

解法一、Trie樹 + TOP K

 步驟一、trie樹存儲前綴後綴

   若看過博客內這篇介紹Trie樹和後綴樹的文章http://blog.csdn.net/v_july_v/article/details/6897097的話,應該就能對trie樹有個大致的瞭解,爲示本文完整性,引用下原文內容,如下:
1.1、什麼是Trie樹

    Trie樹,即字典樹,又稱單詞查找樹或鍵樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計和排序大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:最大限度地減少無謂的字符串比較,查詢效率比哈希表高。

    Trie的核心思想是空間換時間。利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的。
它有3個基本性質:

  1. 根節點不包含字符,除根節點外每一個節點都只包含一個字符。
  2. 從根節點到某一節點,路徑上經過的字符連接起來,爲該節點對應的字符串。
  3. 每個節點的所有子節點包含的字符都不相同。

1.2、樹的構建

舉個在網上流傳頗廣的例子,如下:
    題目:給你100000個長度不超過10的單詞。對於每一個單詞,我們要判斷他出沒出現過,如果出現了,求第一次出現在第幾個位置。
    分析:這題當然可以用hash來解決,但是本文重點介紹的是trie樹,因爲在某些方面它的用途更大。比如說對於某一個單詞,我們要詢問它的前綴是否出現過。這樣hash就不好搞了,而用trie還是很簡單。
    現在回到例子中,如果我們用最傻的方法,對於每一個單詞,我們都要去查找它前面的單詞中是否有它。那麼這個算法的複雜度就是O(n^2)。顯然對於100000的範圍難以接受。現在我們換個思路想。假設我要查詢的單詞是abcd,那麼在他前面的單詞中,以b,c,d,f之類開頭的我顯然不必考慮。而只要找以a開頭的中是否存在abcd就可以了。同樣的,在以a開頭中的單詞中,我們只要考慮以b作爲第二個字母的,一次次縮小範圍和提高針對性,這樣一個樹的模型就漸漸清晰了。
    好比假設有b,abc,abd,bcd,abcd,efg,hii 這6個單詞,我們構建的樹就是如下圖這樣的:
  當時第一次看到這幅圖的時候,便立馬感到此樹之不凡構造了。單單從上幅圖便可窺知一二,好比大海搜人,立馬就能確定東南西北中的到底哪個方位,如此迅速縮小查找的範圍和提高查找的針對性,不失爲一創舉。
    ok,如上圖所示,對於每一個節點,從根遍歷到他的過程就是一個單詞,如果這個節點被標記爲色,就表示這個單詞存在,否則不存在。
    那麼,對於一個單詞,我只要順着他從根走到對應的節點,再看這個節點是否被標記爲紅色就可以知道它是否出現過了。把這個節點標記爲紅色,就相當於插入了這個單詞。
    借用上面的圖,當用戶輸入前綴a的時候,搜索框可能會展示以a爲前綴的“abcd”,“abd”等關鍵詞,再當用戶輸入前綴b的時候,搜索框下面可能會提示以b爲前綴的“bcd”等關鍵詞,如此,實現搜索引擎智能提示suggestion的第一個步驟便清晰了,即用trie樹存儲大量字符串,當前綴固定時,存儲相對來說比較熱的後綴。那又如何統計熱詞呢?請看下文步驟二、TOP K算法統計熱詞。

 步驟二、TOP K算法統計熱詞

    當每個搜索引擎輸入一個前綴時,下面它只會展示0~10個候選詞,但若是碰到那種候選詞很多的時候,如何取捨,哪些展示在前面,哪些展示在後面?這就是一個搜索熱度的問題。

    如本題描述所說,在去年的這個時候,當我在搜索框內搜索“北京”時,它下面會提示以“北京”爲前綴的諸如“北京愛情故事”,“北京公交”,“北京醫院”,且“ 北京愛情故事”展示在第一個:

    爲何輸入“北京”,會首先提示“北京愛情故事”呢?因爲去年的這個時候,正是《北京愛情故事》這部電視劇上映正火的時候(其上映日期爲2012年1月8日,火了至少一年),那個時候大家都一個勁的搜索這部電視劇的相關信息,當10個人中輸入“北京”後,其中有8個人會繼續敲入“愛情故事”(連起來就是“北京愛情故事”)的時候,搜索引擎對此當然不會無動於衷。

    也就是說,搜索引擎知道了這個時間段,大家都在瘋狂查找北京愛情故事,故當用戶輸入以“北京”爲前綴的時候,搜索引擎猜測用戶有80%的機率是要查找“北京愛情故事”,故把“北京愛情故事”在下面提示出來,並放在第一個位置上。

    但爲何今年這個時候再次搜索“北京”的時候,它展示出來的詞不同了呢?

    

    原因在於隨着時間變化,人們對《北京愛情故事》這部電視劇的關注度逐漸下降,與此同時,又出現了新的熱詞,或新的電影,故現在雖然同樣是輸入“北京”,後面提示的詞也相應跟着起了變化。那解決這個問題的辦法是什麼呢?如開頭所說:定期分析某段時間內的人們搜索的關鍵詞,統計出搜索次數比較多的熱詞,繼而當用戶輸入某個前綴時,優先展示熱詞。

    故說白了,這個問題的第二個步驟便是統計熱詞,我們把統計熱詞的方法稱爲TOP K算法,此算法的應用場景便是此文http://blog.csdn.net/v_july_v/article/details/7382693中的第2個問題,再次原文引用:

尋找熱門查詢,300萬個查詢字符串中統計最熱門的10個查詢

    原題:搜索引擎會通過日誌文件把用戶每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度爲1-255字節。假設目前有一千萬個記錄(這些查詢串的重複度比較高,雖然總數是1千萬,但如果除去重複後,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就是越熱門),請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。

    解答:由上面第1題,我們知道,數據大則劃爲小的,如一億個Ip求Top 10,可先%1000將ip分到1000個小文件中去,並保證一種ip只出現在一個文件中,再對每個小文件中的ip進行hashmap計數統計並按數量排序,最後歸併或者最小堆依次處理每個小文件的top10以得到最後的結果。

    但如果數據規模本身就比較小,能一次性裝入內存呢?比如這第2題,雖然有一千萬個Query,但是由於重複度比較高,因此事實上只有300萬的Query,每個Query255Byte,因此我們可以考慮把他們都放進內存中去(300萬個字符串假設沒有重複,都是最大長度,那麼最多佔用內存3M*1K/4=0.75G。所以可以將所有字符串都存放在內存中進行處理),而現在只是需要一個合適的數據結構,在這裏,HashTable絕對是我們優先的選擇。

    所以我們放棄分而治之/hash映射的步驟,直接上hash統計,然後排序。So,針對此類典型的TOP K問題,採取的對策往往是:hashmap + 堆。如下所示:

  1. hashmap統計:先對這批海量數據預處理。具體方法是:維護一個Key爲Query字串,Value爲該Query出現次數的HashTable,即hash_map(Query,Value),每次讀取一個Query,如果該字串不在Table中,那麼加入該字串,並且將Value值設爲1;如果該字串在Table中,那麼將該字串的計數加一即可。最終我們在O(N)的時間複雜度內用Hash表完成了統計;
  2. 排序:第二步、藉助堆這個數據結構,找出Top K,時間複雜度爲N‘logK。即藉助堆結構,我們可以在log量級的時間內查找和調整/移動。因此,維護一個K(該題目中是10)大小的小根堆,然後遍歷300萬的Query,分別和根元素進行對比。所以,我們最終的時間複雜度是:O(N) + N' * O(logK),(N爲1000萬,N’爲300萬)。

    別忘了這篇文章中所述的堆排序思路:‘維護k個元素的最小堆,即用容量爲k的最小堆存儲最先遍歷到的k個數,並假設它們即是最大的k個數,建堆費時O(k),並調整堆(費時O(logk))後,有k1>k2>...kmin(kmin設爲小頂堆中最小元素)。繼續遍歷數列,每次遍歷一個元素x,與堆頂元素比較,若x>kmin,則更新堆(x入堆,用時logk),否則不更新堆。這樣下來,總費時O(k*logk+(n-k)*logk)=O(n*logk)。此方法得益於在堆中,查找等各項操作時間複雜度均爲logk。’--第三章續、Top K算法問題的實現
    當然,你也可以採用trie樹,關鍵字域存該查詢串出現的次數,沒有出現爲0。最後用10個元素的最小推來對出現頻率進行排序。

    相信,如此,也就不難理解開頭所提出的方法了:Trie樹+  TOP K「hashmap+堆,hashmap+堆 統計出如10個近似的熱詞,也就是說,只存與關鍵詞近似的比如10個熱詞」。

    而且你以後就可以告訴你身邊的夥伴們,爲何輸入“結構之”,會提示出來一堆以“結構之”爲前綴的詞了:

    

    方法貌似成型了,但有哪些需要注意的細節呢?如@江申_Johnson所說:“實際工作裏,比如當前綴很短的時候,候選詞很多的時候,查詢和排序性能可能有問題,也許可以加一層索引trie(這層索引可以只索引頻率高於某一個閾值的詞,很短的時候查這個就可以了。數量不夠的話再去查索引了全部詞的trie樹);而且有時候不能根據query頻率來排,而要引導用戶輸入信息量更全面的query,或者或不僅僅是前綴匹配這麼簡單。”

擴展閱讀

    除了上文提到的trie樹,三叉樹或許也是一個不錯的解決方案:http://igoro.com/archive/efficient-auto-complete-with-a-ternary-search-tree/。此外,StackOverflow上也有兩個討論帖子,大家可以看看:①http://stackoverflow.com/questions/2901831/algorithm-for-autocomplete,②http://stackoverflow.com/questions/1783652/what-is-the-best-autocomplete-suggest-algorithm-datastructure-c-c


第三十七章、附近地點搜索

題目詳情:找一個點集中與給定點距離最近的點,同時,給定的二維點集都是固定的,查詢可能有很多次,時間複雜度O(n)無法接受,請設計數據結構和相應的算法。

題目分析:此題是去年微軟的三面題,類似於一朋友@陳利人 出的這題:附近地點搜索,就是搜索用戶附近有哪些地點。隨着GPS和帶有GPS功能的移動設備的普及,附近地點搜索也變得炙手可熱。在龐大的地理數據庫中搜索地點,索引是很重要的。但是,我們的需求是搜索附近地點,例如,座標(39.91, 116.37)附近500米內有什麼餐館,那麼讓你來設計,該怎麼做?

解法一、R樹二維搜索

    假定只允許你初中數學知識,那麼你可能建一個X-Y座標系,即以座標(39.91, 116.37)爲圓心,以500的長度爲半徑,畫一個園,然後一個一個座標點的去查找。此法看似可行,但複雜度可想而知,即便你自以爲聰明的說把整個平面劃分爲四個象限,一個一個象限的查找,此舉雖然優化程度不夠,但也說明你一步步想到點子上去了。

    即不一個一個座標點的查找,而是一個一個區域的查找,相對來說,其平均查找速度和效率會顯著提升。如此,便自然而然的想到了有沒有一種一次查找定位於一個區域的數據結構呢?

    若看過博客內之前介紹R樹的這篇文章http://blog.csdn.net/v_JULY_v/article/details/6530142#t2 的讀者立馬便能意識到,R樹就是解決這個區域查找繼而不斷縮小規模的問題。特直接引用原文:

R樹的數據結構

    R樹是B樹在高維空間的擴展,是一棵平衡樹。每個R樹的葉子結點包含了多個指向不同數據的指針,這些數據可以是存放在硬盤中的,也可以是存在內存中。根據R樹的這種數據結構,當我們需要進行一個高維空間查詢時,我們只需要遍歷少數幾個葉子結點所包含的指針,查看這些指針指向的數據是否滿足要求即可。這種方式使我們不必遍歷所有數據即可獲得答案,效率顯著提高。下圖1是R樹的一個簡單實例:

    我們在上面說過,R樹運用了空間分割的理念,這種理念是如何實現的呢?R樹採用了一種稱爲MBR(Minimal Bounding Rectangle)的方法,在此我把它譯作“最小邊界矩形”。從葉子結點開始用矩形(rectangle)將空間框起來,結點越往上,框住的空間就越大,以此對空間進行分割。有點不懂?沒關係,繼續往下看。在這裏我還想提一下,R樹中的R應該代表的是Rectangle(此處參考wikipedia上關於R樹的介紹),而不是大多數國內教材中所說的Region(很多書把R樹稱爲區域樹,這是有誤的)。我們就拿二維空間來舉例。下圖是Guttman論文中的一幅圖:

 

    我來詳細解釋一下這張圖。

  1. 先來看圖(b),首先我們假設所有數據都是二維空間下的點,圖中僅僅標誌了R8區域中的數據,也就是那個shape of data object。別把那一塊不規則圖形看成一個數據,我們把它看作是多個數據圍成的一個區域。爲了實現R樹結構,我們用一個最小邊界矩形恰好框住這個不規則區域,這樣,我們就構造出了一個區域:R8。R8的特點很明顯,就是正正好好框住所有在此區域中的數據。
  2. 其他實線包圍住的區域,如R9,R10,R12等都是同樣的道理。這樣一來,我們一共得到了12個最最基本的最小矩形。這些矩形都將被存儲在子結點中。
  3. 下一步操作就是進行高一層次的處理。我們發現R8,R9,R10三個矩形距離最爲靠近,因此就可以用一個更大的矩形R3恰好框住這3個矩形。
  4. 同樣道理,R15,R16被R6恰好框住,R11,R12被R4恰好框住,等等。所有最基本的最小邊界矩形被框入更大的矩形中之後,再次迭代,用更大的框去框住這些矩形。

    我想大家都應該理解這個數據結構的特徵了。用地圖的例子來解釋,就是所有的數據都是餐廳所對應的地點,先把相鄰的餐廳劃分到同一塊區域,劃分好所有餐廳之後,再把鄰近的區域劃分到更大的區域,劃分完畢後再次進行更高層次的劃分,直到劃分到只剩下兩個最大的區域爲止。要查找的時候就方便了。

    下面就可以把這些大大小小的矩形存入我們的R樹中去了。根結點存放的是兩個最大的矩形,這兩個最大的矩形框住了所有的剩餘的矩形,當然也就框住了所有的數據。下一層的結點存放了次大的矩形,這些矩形縮小了範圍。每個葉子結點都是存放的最小的矩形,這些矩形中可能包含有n個數據。

地圖查找的實例

    講完了基本的數據結構,我們來講個實例,如何查詢特定的數據。又以餐廳爲例,假設我要查詢廣州市天河區天河城附近一公里的所有餐廳地址怎麼辦?

  1. 打開地圖(也就是整個R樹),先選擇國內還是國外(也就是根結點);
  2. 然後選擇華南地區(對應第一層結點),選擇廣州市(對應第二層結點),
  3. 再選擇天河區(對應第三層結點);
  4. 最後選擇天河城所在的那個區域(對應葉子結點,存放有最小矩形);

    遍歷所有在此區域內的結點,看是否滿足我們的要求即可。怎麼樣,其實R樹的查找規則跟查地圖很像吧?對應下圖:

        

一棵R樹滿足如下的性質:

  1. 除非它是根結點之外,所有葉子結點包含有m至M個記錄索引(條目)。作爲根結點的葉子結點所具有的記錄個數可以少於m。通常,m=M/2。
  2. 對於所有在葉子中存儲的記錄(條目),I是最小的可以在空間中完全覆蓋這些記錄所代表的點的矩形(注意:此處所說的“矩形”是可以擴展到高維空間的)。
  3. 每一個非葉子結點擁有m至M個孩子結點,除非它是根結點。
  4. 對於在非葉子結點上的每一個條目,i是最小的可以在空間上完全覆蓋這些條目所代表的店的矩形(同性質2)。
  5. 所有葉子結點都位於同一層,因此R樹爲平衡樹。

葉子結點的結構

先來探究一下葉子結點的結構。葉子結點所保存的數據形式爲:(I, tuple-identifier)。

      其中,tuple-identifier表示的是一個存放於數據庫中的tuple,也就是一條記錄,它是n維的。I是一個n維空間的矩形,並可以恰好框住這個葉子結點中所有記錄代表的n維空間中的點。I=(I0,I1,…,In-1)。其結構如下圖所示:

    下圖描述的就是在二維空間中的葉子結點所要存儲的信息。

 

    在這張圖中,I所代表的就是圖中的矩形,其範圍是a<=I0<=b,c<=I1<=d。有兩個tuple-identifier,在圖中即表示爲那兩個點。這種形式完全可以推廣到高維空間。大家簡單想想三維空間中的樣子就可以了。這樣,葉子結點的結構就介紹完了。

非葉子結點

      非葉子結點的結構其實與葉子結點非常類似。想象一下B樹就知道了,B樹的葉子結點存放的是真實存在的數據,而非葉子結點存放的是這些數據的“邊界”,或者說也算是一種索引(有疑問的讀者可以回顧一下上述第一節中講解B樹的部分)。

      同樣道理,R樹的非葉子結點存放的數據結構爲:(I, child-pointer)。

      其中,child-pointer是指向孩子結點的指針,I是覆蓋所有孩子結點對應矩形的矩形。這邊有點拗口,但我想不是很難懂?給張圖:

 

    D,E,F,G爲孩子結點所對應的矩形。A爲能夠覆蓋這些矩形的更大的矩形。這個A就是這個非葉子結點所對應的矩形。這時候你應該悟到了吧?無論是葉子結點還是非葉子結點,它們都對應着一個矩形。樹形結構上層的結點所對應的矩形能夠完全覆蓋它的孩子結點所對應的矩形。根結點也唯一對應一個矩形,而這個矩形是可以覆蓋所有我們擁有的數據信息在空間中代表的點的。

    我個人感覺這張圖畫的不那麼精確,應該是矩形A要恰好覆蓋D,E,F,G,而不應該再留出這麼多沒用的空間了。但爲尊重原圖的繪製者,特不作修改。

    但R樹有些什麼問題呢?如@宋梟_CD所說:“單純用R樹來作索引,搜索附近的地點,可能會遍歷樹的很多個分支。而且當全國的地圖或者全省的地圖時候,樹的葉節點數目很多,樹的深度也會是一個問題。一般會把地理位置上附近的節點(二維地圖中點線面)預處理成page(大小爲4K的倍數),在這些page上建立R樹的索引。”

解法二、GeoHash算法索引地理位置信息

    我在微博上跟一些朋友討論這個附近點搜索的問題時,除了談到R樹,有幾個朋友都指出GeoHash算法可以解決,故才瞭解了下GeoHash算法,此文http://blog.nosqlfan.com/html/1811.html 清晰闡述了MongoDB藉助GeoHash算法實現地理位置索引的原理,特引用其內容加以說明,如下:

支持地理位置索引是MongoDB的一大亮點,這也是全球最流行的LBS服務foursquare 選擇MongoDB的原因之一。我們知道,通常的數據庫索引結構是B+ Tree,如何將地理位置轉化爲可建立B+Tree的形式。首先假設我們將需要索引的整個地圖分成16×16的方格,如下圖(左下角爲座標0,0 右上角爲座標16,16):

    單純的[x,y]的數據是無法建立索引的,所以MongoDB在建立索引的時候,會根據相應字段的座標計算一個可以用來做索引的hash值,這個值叫做geohash,下面我們以地圖上座標爲[4,6]的點(圖中紅叉位置)爲例。我們第一步將整個地圖分成等大小的四塊,如下圖:

    劃分成四塊後我們可以定義這四塊的值,如下(左下爲00,左上爲01,右下爲10,右上爲11):

    
    這樣[4,6]點的geohash值目前爲 00然後再將四個小塊每一塊進行切割,如下:
    這時[4,6]點位於右上區域,右上的值爲11,這樣[4,6]點的geohash值變爲:0011繼續往下做兩次切分:

    最終得到[4,6]點的geohash值爲:00110100
    這樣我們用這個值來做索引,則地圖上點相近的點就可以轉化成有相同前綴的geohash值了。
    我們可以看到,這個geohash值的精確度是與劃分地圖的次數成正比的,上例對地圖劃分了四次。而MongoDB默認是進行26次劃分,這個值在建立索引時是可控的。具體建立二維地理位置索引的命令如下:
db.map.ensureIndex({point : "2d"}, {min : 0, max : 16, bits : 4})
    其中的bits參數就是劃分幾次,默認爲26次。 

    讀者點評@yuotulck:首先多謝博主的文章,不過如果是新手(例如我)看到geohash那裏可能會有誤解:是否相鄰可以靠前綴來比較?其實這是錯的,例如邊界那一塊的相鄰區域編碼的前綴從第一個就不一樣了,也就是說在geohash裏相近的點hash值不一定相近。
    上面的知識點了解自:http://www.cnblogs.com/step1/archive/2009/04/22/1441689.html,而geohash的進一步用法在這裏可以瞭解到:
http://tech.idv2.com/2011/07/05/geohash-intro/

    本章完。


參考鏈接及推薦閱讀

  1. 2012年九月十月筆試面試八十題:http://blog.csdn.net/v_july_v/article/details/7974418
  2. 從Trie樹(字典樹)談到後綴樹:http://blog.csdn.net/v_july_v/article/details/6897097
  3. 教你如何迅速秒殺掉:99%的海量數據處理面試題:http://blog.csdn.net/v_july_v/article/details/7382693
  4. 從B樹、B+樹、B*樹談到R樹:http://blog.csdn.net/v_july_v/article/details/6530142
  5. 圖解 MongoDB 地理位置索引的實現原理:http://blog.nosqlfan.com/html/1811.html
  6. 《Hbase實戰》第8章、在HBase上查詢地理信息系統;

發佈了166 篇原創文章 · 獲贊 1萬+ · 訪問量 1604萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章