秋招面試題---拼多多

****拼多多是要求手寫代碼的****

拼多多算法題

1.生活中用到棧和隊列的例子

對於隊列很好想到關於任何排隊的都是,但是棧就不是特別好想,唯一想到的就是火車調度。


2.n個節點的樹可能的高度。最高和最低的高度

首先是不是二叉樹,如果不是當然最高就是n,最低就是2

是二叉樹的話,最高就n,最小的情況是完全二叉樹,高度是[logn]+1


3.BST樹的高度

BST(二叉查找樹/二叉搜索樹)


4.問了一個訂單數據表(uid,orderId,time,moblie)每天2000w條,要存1年。應該怎麼存?

https://www.jianshu.com/p/84da619ce203

(1)訂單數據劃分

由於是訂單表,一般最近一個月的是比較常查詢的,因此可以將訂單數據劃分成兩大類型:分別是熱數據和冷數據。

  • 熱數據:1個月內的訂單數據,查詢實時性較高;
  • 冷數據A:1個月 ~ 3個月前的訂單數據,查詢頻率不高;
  • 冷數據B:3個月到一年的訂單數據,幾乎不會查詢,只有偶爾的查詢需求;

可能這裏有個疑惑爲什麼要將冷數據分成兩類,因爲根據實際場景需求,用戶基本不會去查看3個月以後的數據,如果將這部分數據還存儲在db中,那麼成本會非常高,而且也不便於維護。另外如果真遇到有個別用戶需要查看3個月以後的訂單信息,可以讓用戶走離線數據查看。

對於這三類數據的存儲,目前規劃如下:

  • 熱數據: 使用mysql進行存儲,當然需要分庫分表;
  • 冷數據A: 對於這類數據可以存儲在ES中,利用搜索引擎的特性基本上也可以做到比較快的查詢;
  • 冷數據B: 對於這類不經常查詢的數據,可以存放到Hive

(2)MySql 如何分庫分表

  • 按業務拆分(和本問題無關,但是實際當中需要將數據庫按照業務拆分到不同的庫中存儲)

  • 分庫與分表

我們知道每臺機器無論配置多麼好它都有自身的物理上限,所以當我們應用已經能觸及或遠遠超出單臺機器的某個上限的時候,我們惟有尋找別的機器的幫助或者繼續升級的我們的硬件,但常見的方案還是通過添加更多的機器來共同承擔壓力。

(1)分表策略

我們假設預估單個庫需要分配100個表滿足我們的業務需求,我們可以簡單的取模計算出訂單在哪個子表中,例如: order_id % 100

(2)分庫實現策略

數據庫分表能夠解決單表數據量很大的時候數據查詢的效率問題,但是無法給數據庫的併發操作帶來效率上的提高,因爲分表的實質還是在一個數據庫上進行的操作,很容易受數據庫IO性能的限制。因此,如何將數據庫IO性能的問題平均分配出來,很顯然將數據進行分庫操作可以很好地解決單臺數據庫的性能問題。分庫策略與分表策略的實現很相似,最簡單的都是可以通過取模的方式進行。

例如:order_id % 庫容量。

(3)分庫分表結合使用策略

數據庫分表可以解決單表海量數據的查詢性能問題,分庫可以解決單臺數據庫的併發訪問壓力問題。有時候,我們需要同時考慮這兩個問題,因此,我們既需要對單表進行分表操作,還需要進行分庫操作,以便同時擴展系統的併發處理能力和提升單表的查詢性能,就是我們使用到的分庫分表。

如果使用分庫分表結合使用的話,不能簡單進行order_id 取模操作,需要加一箇中間變量用來打散到不同的子表

中間變量 = shard key %(庫數量*單個庫的表數量);

庫序號 = 取整(中間變量/單個庫的表數量);

表序號 = 中間變量%單個庫的表數量。

例如:數據庫有10個,每一個庫中有100個數據表,用戶的order_id=1001,按照上述的路由策略,可得:

中間變量=1001%(10*100)=1;

庫序號=取整(1/100)=0;

表序號=1%100=1

這樣的話,對於order_id=1001,將被路由到第1個數據庫的第2個表中(索引0 代表1,依次類推)。

(3)整體架構設計

  • 寫操作還是很簡單的,就通過分區分表策略決定寫到哪個庫哪個表,但是都是寫到Mysql中的。
  • 讀操作需要根據訂單id先判斷出讀的數據是熱數據還是冷數據,再去相應數據庫讀取數據。訂單id通常使用:商戶所在地區號+時間戳+隨機數組成,這樣就可以根據時間戳選取查詢數據庫。
  • Mysql中冷數據要定期的遷移到冷數據庫中。

5.NIO

(1)IO與NIO區別

  1. IO是面向流的,NIO是面向緩衝的;
  2. IO是阻塞的,NIO是非阻塞的;
  3. IO是單線程的,NIO 是通過選擇器來模擬多線程的;

NIO在基礎的IO流上發展處新的特點,分別是:內存映射技術,字符及編碼,非阻塞I/O和文件鎖定

(2)內存映射

因爲磁盤讀取是很慢的,所以提出了緩存,但是對於大文件,內存是不能存下的,因此提出了內存映射文件:允許我們創建和修改那些因爲太大而不能放入內存的文件此時就可以假定整個文件都放在內存中,而且可以完全把它當成非常大的數組來訪問(隨機訪問)。

這個功能主要是爲了提高大文件的讀寫速度而設計的。內存映射文件(memory-mappedfile)能讓你創建和修改那些大到無法讀入內存的文件。有了內存映射文件,你就可以認爲文件已經全部讀進了內存,然後把它當成一個非常大的數組來訪問了。將文件的一段區域映射到內存中,比傳統的文件處理速度要快很多。內存映射文件它雖然最終也是要從磁盤讀取數據,但是它並不需要將數據讀取到OS內核緩衝區,而是直接將進程的用戶私有地址空間中的一部分區域與文件對象建立起映射關係,就好像直接從內存中讀、寫文件一樣,速度當然快了。

實現:

1首先,從文件中獲得一個通道(channel)通道是用於磁盤文件的一種抽象,它使我們可以訪問諸如內存映射文件加鎖機制(下文緩衝區數據結構部分將提到)文件間快速數據傳遞等操作系統特性。

FileChannel channel = FileChannel.open(filename);

2然後,通過調用FileChannel類的map方法進行內存映射,map方法從這個通道中獲得一個MappedByteBuffer對象(ByteBuffer的子類)

MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, length);

可以指定想要映射的文件區域與映射模式,支持的模式有3種:

  • FileChannel.MapMode.READ_ONLY:產生只讀緩衝區,對緩衝區的寫入操作將導致ReadOnlyBufferException;
  • FileChannel.MapMode.READ_WRITE:產生可寫緩衝區,任何修改將在某個時刻寫回到文件中,而這某個時刻是依賴OS的,其他映射同一個文件的程序可能不能立即看到這些修改,多個程序同時進行文件映射的確切行爲是依賴於系統的,但是它是線程安全的
  • FileChannel.MapMode.PRIVATE:產生可寫緩衝區,但任何修改是緩衝區私有的,不會回到文件中。。。

3、一旦有了緩衝區,就可以使用ByteBuffer類和Buffer超類的方法來讀寫數據

緩衝區支持順序隨機數據訪問:

順序:有一個可以通過get和put操作來移動的位置

1 while(buffer.hasRemaining()){
2     byte b = buffer.get(); //get當前位置
3     ...
4 }

隨機:可以按內存數組索引訪問

1 for(int i=0; i<buffer.limit(); i++){
2     byte b = buffer.get(i); //這個get能指定索引
3     ...
4 }

(3)字符及編碼

  • 編碼方案:

編碼方案定義瞭如何把字符編碼的序列表達爲字節序列。字符編碼的數值不需要與編碼字節相同,也不需要是一對一或一對多個的關係。原則上,把字符集編碼和解碼近似視爲對象的序列化和反序列化

大部分的操作系統在I/O與文件存儲方面仍是以字節爲導向的,所以無論使用何種編碼,Unicode或其他編碼,在字節序列和字符集編碼之間仍需要進行轉化。

由java.nio.charset包組成的類滿足了這個需求。這不是Java平臺第一次處理字符集編碼,但是它是最系統、最全面、以及最靈活的解決方式。

(4)非阻塞IO

五種IO模型舉例引用levin

1.阻塞I/O模型

老李去火車站買票,排隊三天買到一張退票。

耗費:在車站吃喝拉撒睡 3天,其他事一件沒幹。

2.非阻塞I/O模型

老李去火車站買票,隔12小時去火車站問有沒有退票,三天後買到一張票。

耗費:往返車站6次,路上6小時,其他時間做了好多事。

3.I/O複用模型

老李去火車站買票,委託黃牛,然後每隔6小時電話黃牛詢問,黃牛三天內買到票,然後老李去火車站交錢領票。

耗費:往返車站2次,路上2小時,黃牛手續費100元,打電話17次

  • epoll

老李去火車站買票,委託黃牛,黃牛買到後即通知老李去領,然後老李去火車站交錢領票。

耗費:往返車站2次,路上2小時,黃牛手續費100元,無需打電話

4.信號驅動I/O模型

老李去火車站買票,給售票員留下電話,有票後,售票員電話通知老李,然後老李去火車站交錢領票。

耗費:往返車站2次,路上2小時,免黃牛費100元,無需打電話

5.異步I/O模型

老李去火車站買票,給售票員留下電話,有票後,售票員電話通知老李並快遞送票上門。

耗費:往返車站1次,路上1小時,免黃牛費100元,無需打電話

 

典型的非阻塞IO模型一般如下:

while(true){
    data = socket.read();
    if(data!= error){
        處理數據
        break;
    }
}

但是對於非阻塞IO就有一個非常嚴重的問題,在while循環中需要不斷地去詢問內核數據是否就緒,這樣會導致CPU佔用率非常高,因此一般情況下很少使用while循環這種方式來讀取數據。所以這就不得不說到下面這個概念–多路複用IO模型。

如果一個I/O流進來,我們就開啓一個進程處理這個I/O流。那麼假設現在有一百萬個I/O流進來,那我們就需要開啓一百萬個進程一一對應處理這些I/O流(——這就是傳統意義下的多進程併發處理)。思考一下,一百萬個進程,你的CPU佔有率會多高,這個實現方式及其的不合理。所以人們提出了I/O多路複用這個模型,一個線程,通過記錄I/O流的狀態來同時管理多個I/O,可以提高服務器的吞吐能力。(相當於黃牛)

(5)文件鎖定

NIO中的文件通道(FileChannel)在讀寫數據的時候主 要使用了阻塞模式,它不能支持非阻塞模式的讀寫,而且FileChannel的對象是不能夠直接實例化的, 他的實例只能通過getChannel()從一個打開的文件對象上邊讀取(RandomAccessFile、 FileInputStream、FileOutputStream),並且通過調用getChannel()方法返回一個 Channel對象去連接同一個文件,也就是針對同一個文件進行讀寫操作。
文件鎖的出現解決了很多Java應用程序和非Java程序之間共享文件數據的問題,在以前的JDK版本中,沒有文件鎖機制使得Java應用程序和其他非Java進程程序之間不能夠針對同一個文件共享 數據,有可能造成很多問題,JDK1.4裏面有了FileChannel,它的鎖機制使得文件能夠針對很多非 Java應用程序以及其他Java應用程序可見。但是Java裏面 的文件鎖機制主要是基於共 享鎖模型,在不支持共享鎖模型的操作系統上,文件鎖本身也起不了作用,JDK1.4使用文件通道讀寫方式可以向一些文件 發送鎖請求,FileChannel的 鎖模型主要針對的是每一個文件,並不是每一個線程和每一個讀寫通道,也就是以文件爲中心進行共享以及獨佔,也就是文件鎖本身並不適合於同一個JVM的不同 線程之間。


6.java併發之原子性、可見性、有序性

(1)原子性:

在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。

實現:可以通過synchronizedLock來實現。由於synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那麼自然就不存在原子性問題了,從而保證了原子性。

(2)可見性

指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

實現:volatile關鍵字來保證可見性。

通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。

(3)有序性

即程序執行的順序按照代碼的先後順序執行

實現:可以通過volatile關鍵字來保證一定的“有序性”(具體原理在下一節講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性。


7.nat是什麼?屬於哪一層?

NAT是網絡地址轉換,是一種在IP數據包通過路由器或防火牆時重寫來源IP地址或目的IP地址的技術。

在第三層,網絡層。


8.網絡分層的模型和協議


9.布隆過濾器

https://www.jianshu.com/p/2104d11ee0a2

(1)介紹

本質上布隆過濾器是一種數據結構,比較巧妙的概率型數據結構(probabilistic data structure),特點是高效地插入和查詢,可以用來告訴你 “某樣東西一定不存在或者可能存在”。

相比於傳統的 List、Set、Map 等數據結構,它更高效、佔用空間更少,但是缺點是其返回的結果是概率性的,而不是確切的。

(2)原理

講述布隆過濾器的原理之前,我們先思考一下,通常你判斷某個元素是否存在用的是什麼?應該蠻多人回答 HashMap 吧,確實可以將值映射到 HashMap 的 Key,然後可以在 O(1) 的時間複雜度內返回結果,效率奇高。但是 HashMap 的實現也有缺點,例如存儲容量佔比高,考慮到負載因子的存在,通常空間是不能被用滿的,而一旦你的值很多例如上億的時候,那 HashMap 佔據的內存大小就變得很可觀了。

還比如說你的數據集存儲在遠程服務器上,本地服務接受輸入,而數據集非常大不可能一次性讀進內存構建 HashMap 的時候,也會存在問題。

布隆過濾器數據結構

布隆過濾器是一個 bit 向量或者說 bit 數組,長這樣:

如果我們要映射一個值到布隆過濾器中,我們需要使用多個不同的哈希函數生成多個哈希值,並對每個生成的哈希值指向的 bit 位置 1,例如針對值 “baidu” 和三個不同的哈希函數分別生成了哈希值 1、4、7,則上圖轉變爲:

Ok,我們現在再存一個值 “tencent”,如果哈希函數返回 3、4、8 的話,圖繼續變爲:

當查詢一個key是否存在時,將這個key的所有hash函數結果得出,如果存在有爲0的,這個key一定不存在,但如果沒有,說明這個key可能存在的。是有一定概率的,不能完全確保存在。

(3)其他問題

  • 支持刪除麼

基本的是值支持添加和查詢操作,不支持刪除的,原因顯而易見。但如果想支持刪除可以採用計數法,但會佔用內存的。即添加數據時,map的相應位value++;刪除的時候value--,如果value==0,查詢不存在。

  • 如何選擇哈希函數個數和布隆過濾器長度


10.HTTP緩存機制

https://www.cnblogs.com/chenqf/p/6386163.html

Http 緩存機制作爲 web 性能優化的重要手段,對於從事 Web 開發的同學們來說,應該是知識體系庫中的一個基礎環節

(1)在介紹HTTP緩存之前,作爲知識鋪墊,先簡單介紹一下HTTP報文

HTTP報文就是瀏覽器和服務器間通信時發送及響應的數據塊。
瀏覽器向服務器請求數據,發送請求(request)報文;服務器向瀏覽器返回數據,返回響應(response)報文。
報文信息主要分爲兩部分
1.包含屬性的首部(header)--------------------------附加信息(cookie,緩存信息等)與緩存相關的規則信息,均包含在header中
2.包含數據的主體部分(body)-----------------------HTTP請求真正想要傳輸的部分

(2)緩存規則解析

在客戶端第一次請求數據時,此時緩存數據庫中沒有對應的緩存數據,需要請求服務器,服務器返回後,將數據存儲至緩存數據庫中。

(3)分類

  • 強制緩存

在緩存數據未失效的情況下,可以直接使用緩存數據,那麼瀏覽器是如何判斷緩存數據是否失效呢?
我們知道,在沒有緩存數據的時候,瀏覽器向服務器請求數據時,服務器會將數據和緩存規則一併返回,緩存規則信息包含在響應header中。對於強制緩存來說,響應header中會有兩個字段來標明失效規則(Expires/Cache-Control

Expires的值爲服務端返回的到期時間,即下一次請求時,請求時間小於服務端返回的到期時間,直接使用緩存數據。
到期時間是由服務端生成的,但是客戶端時間可能跟服務端時間有誤差,這就會導致緩存命中的誤差。
所以HTTP 1.1 的版本,使用Cache-Control替代。

圖中Cache-Control僅指定了max-age,所以默認爲private,緩存時間爲31536000秒(365天)
也就是說,在365天內再次請求這條數據,都會直接獲取緩存數據庫中的數據,直接使用。

  • 對比緩存

基於對比緩存的流程下,不管是否使用緩存,都需要向服務器發送請求,那麼還用緩存幹什麼?

簡單的說就是緩存的數據可能有被修改過,

Last-Modified:服務器在響應請求時,告訴瀏覽器資源的最後修改時間。

If-Modified-Since:
再次請求服務器時,通過此字段通知服務器上次請求時,服務器返回的資源最後修改時間。

服務器收到請求後發現有頭If-Modified-Since 則與被請求資源的最後修改時間進行比對。

若資源的最後修改時間大於If-Modified-Since,說明資源又被改動過,則響應整片資源內容,返回狀態碼200;

若資源的最後修改時間小於或等於If-Modified-Since,說明資源無新修改,則響應HTTP 304,告知瀏覽器繼續使用所保存的cache。

不不用修改時,返回影響時間是很快的,因爲不需要穿數據,因此並不浪費時間的。


11.hashMap產生死鎖的原因

多線程,在擴容的時候產生死鎖


12.Hash索引和B+樹索引的區別

(1)如果是等值查詢,那麼hash索引明顯有絕對優勢:但如果查詢健不是唯一的話,因爲hash碰撞問題,需要在同一個entry下面的鏈表中查詢,這樣查詢速度就不是很快了;不是很穩定的。B+索引穩定。

(2)範圍查找,排序:hash不支持的。B+樹支持的;

(3)哈希索引也不支持多列聯合索引的最左匹配規則


13.Redis數據類型的實現原理

(1)數據類型:字符串,鏈表,set集合,hash,有序集合

(2)實現

  • 字符串

int 編碼:保存的是可以用 long 類型表示的整數值。

raw 編碼:保存長度大於44字節的字符串(redis3.2版本之前是39字節,之後是44字節)。

embstr 編碼:保存長度小於44字節的字符串(redis3.2版本之前是39字節,之後是44字節)。

  • list對象

ziplist(壓縮列表) 和 linkedlist(雙端鏈表)

  • hash對象

ziplist(壓縮列表) 或者 hashtable(字典)

  • set對象

intset(整數集合)或者 hashtable(字典)

  • sorted_set對象

ziplist (壓縮列表) 或者 skiplist(跳躍表


14.跳躍表的原理

基於並聯的鏈表,其效率可比擬於二叉查找樹(對於大多數操作需要O(log n)平均時間)。基本上,跳躍列表是對有序的鏈表增加上附加的前進鏈接,增加是以隨機化的方式進行的,所以在列表中的查找可以快速的跳過部分列表(因此得名)。所有操作都以對數隨機化的時間進行。Skip List可以很好解決有序鏈表查找特定值的困難。

跳錶具有如下性質:

(1) 由很多層結構組成

(2) 每一層都是一個有序的鏈表

(3) 最底層(Level 1)的鏈表包含所有元素

(4) 如果一個元素出現在 Level i 的鏈表中,則它在 Level i 之下的鏈表也都會出現。

(5) 每個節點包含兩個指針,一個指向同一鏈表中的下一個元素,一個指向下面一層的元素。


15. Comparator和Comparable

(1)Comparable:與compareTo一起使用

Comparable可以認爲是一個內比較器,實現了Comparable接口的類有一個特點,就是這些類是可以和自己比較的。(同一個類的兩個對象進行比較)

至於具體和另一個實現了Comparable接口的類如何比較,則依賴compareTo方法的實現,compareTo方法也被稱爲自然比較方法

如果開發者add進入一個Collection的對象想要Collections的sort方法幫你自動進行排序的話,那麼這個對象必須實現Comparable接口。

compareTo方法的返回值是int,有三種情況:

1、比較者大於被比較者(也就是compareTo方法裏面的對象),那麼返回正整數

2、比較者等於被比較者,那麼返回0

3、比較者小於被比較者,那麼返回負整數

例子:

public class Domain implements Comparable<Domain>{
    private String str;

    public Domain(String str)
    {
        this.str = str;
    }
    //重寫compareTo
    public int compareTo(Domain domain)
    {
        if (this.str.compareTo(domain.str) > 0)
            return 1;
        else if (this.str.compareTo(domain.str) == 0)
            return 0;
        else 
            return -1;
    }
    
    public String getStr()
    {
        return str;
    }
}
public class test {
	public static void main(String[] args)
    {
        Domain d1 = new Domain("c");
        Domain d2 = new Domain("c");
        Domain d3 = new Domain("b");
        Domain d4 = new Domain("d");
        System.out.println(d1.compareTo(d2));
        System.out.println(d1.compareTo(d3));
        System.out.println(d1.compareTo(d4));
        //加入集合中進行排序
        List<Domain>ld=new ArrayList<>();
        ld.add(d1);
        ld.add(d2);
        ld.add(d3);
        ld.add(d4);
        System.out.println("排序前:");
        for(Domain d:ld) {
        	System.out.print(d.getStr()+" ");
        }
        System.out.println();
        System.out.println("排序後:");
        Collections.sort(ld);
        for(Domain d:ld) {
        	System.out.print(d.getStr()+" ");
        }
    }
}

結果:

 

(2)Comparator:與compare()一起使用

Comparator可以認爲是是一個外比較器,個人認爲有兩種情況可以使用實現Comparator接口的方式:

1、一個對象不支持自己和自己比較(沒有實現Comparable接口),但是又想對兩個對象進行比較

2、一個對象實現了Comparable接口,但是開發者認爲compareTo方法中的比較方式並不是自己想要的那種比較方式

Comparator接口裏面有一個compare方法,方法有兩個參數T o1和T o2,是泛型的表示方式,分別表示待比較的兩個對象,方法返回值和Comparable接口一樣是int,有三種情況:

1、o1大於o2,返回正整數

2、o1等於o2,返回0

3、o1小於o2,返回負整數

例子:上面例子Domian類不用變

DomainComparator

public class DomainComparator implements Comparator<Domain>
{
    public int compare(Domain domain1, Domain domain2)
    {
        if (domain1.getStr().compareTo(domain2.getStr()) > 0)
            return 1;
        else if (domain1.getStr().compareTo(domain2.getStr()) == 0)
            return 0;
        else 
            return -1;
    }
}
public class test {
	public static void main(String[] args)
	{
	    Domain d1 = new Domain("c");
	    Domain d2 = new Domain("c");
	    Domain d3 = new Domain("b");
	    Domain d4 = new Domain("d");
	    DomainComparator dc = new DomainComparator();
	    System.out.println(dc.compare(d1, d2));
	    System.out.println(dc.compare(d1, d3));
	    System.out.println(dc.compare(d1, d4));
	}
}

結果:

(3)總結

1.使用:Comparable與compareTo()結合使用,Comparator與compare()結合使用;

2.作用:Comparable是內比較器,用於同一個類的比較,當然可也有用於兩個類,但這兩個類需要重寫compareTo();Comparator是外部比較器,通過實現Comparator,重寫compare()比較兩個類。

3.優缺點:實現Comparable接口的方式比實現Comparator接口的耦合性要強一些,如果要修改比較算法,要修改Comparable接口的實現類,而實現Comparator的類是在外部進行比較的,不需要對實現類有任何修改。從這個角度說,其實有些不太好,尤其在我們將實現類的.class文件打成一個.jar文件提供給開發者使用的時候。實際上實現Comparator 接口的方式後面會寫到就是一種典型的策略模式

當然,這不是鼓勵用Comparator,意思是開發者還是要在具體場景下選擇最合適的那種比較器而已。

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章