10分鐘詳解高級數據結構:優先隊列、圖、前綴樹、分段樹以及樹狀數組

優秀的算法每每取決於你採用哪一種數據結構,那麼在這節課裏,咱們將重點介紹幾種高級的數據結構,它們是:優先隊列、圖、前綴樹、分段樹以及樹狀數組。面試

之因此稱它們爲高級的數據結構,是由於它們的實現要比那些經常使用的數據結構要複雜得多,這些高級的數據結構可以讓你在處理一些複雜問題的過程當中多擁有一把利器。同時,掌握好它們的性質以及所適用的場合,在分析問題的時候迴歸本質,那麼不少題目都能迎刃而解了。算法

下面咱們就來一一介紹它們。編程

優先隊列(Priority Queue)
圖片: https://uploader.shimo.im/f/l...後端

首先咱們來介紹一下優先隊列。 -- 用圖來講明數組

和普通隊列不一樣的是,優先隊列最大的做用是能保證每次取出的元素都是隊列中優先級別最高的,這個優先級別能夠是自定義的,例如,數據的數值越大,優先級越高,或者是數據的數值越小,優先級越高,優先級別甚至能夠經過各類複雜的計算獲得。網絡

優先隊列最經常使用的場景是從一堆雜亂無章的數據當中按照必定的順序(或者優先級)逐步地篩選出部分的乃至所有的數據。數據結構

例如,任意給定一個數組,要求找出前k大的數。試想一下,最直接的辦法就是先對這個數組進行排序,而後依次輸出前k大的數,這樣的複雜度將會是O(nlogn),其中,n是數組的元素個數。編程語言

有沒有更好的辦法呢?若是咱們借用優先隊列,就能將複雜度優化成O(k + nlogk),當數據量很大(即n很大),而k相對較小的時候,顯然,利用優先隊列能有效地下降算法複雜度,其本質就在於,要找出前k大的數,咱們並不須要對全部的數進行排序。如今問題來了,這個複雜度是如何計算出來的呢?要理解它,咱們先來看看優先隊列的實現方法。oop

優先隊列的本質是一個二叉堆結構,堆在英文裏叫Binary Heap,它是利用一個數組結構來實現的徹底二叉樹。換句話說,優先隊列的本質是一個數組,數組裏的每一個元素既有多是其餘元素的父節點,也有多是其餘元素的子節點,並且,每一個父節點只能有兩個子節點,這就很像一棵二叉樹的結構了。
這裏有三個重要的性質須要牢記:
數組裏的第一個元素array[0]擁有最高的優先級別。性能

給定一個下標 i,那麼對於元素 array[i] 而言:
它的父節點所對應的元素下標是 (i-1) / 2
它的左孩子所對應的元素下標是 2*i + 1
它的右孩子所對應的元素下標是 2*i + 2

數組裏每一個元素的優先級別都要高於它兩個孩子的優先級別。

圖片: https://uploader.shimo.im/f/7...

優先隊列最基本的操做就是兩個:

向上篩選(sift up / bubble up)
向下篩選(sift down / bubble down)

什麼是向上篩選呢?當有新的數據加入到優先隊列中,新的數據首先被放置在二叉堆的底部,而後不斷地對它進行向上篩選的操做,即若是發現它的優先級別比父節點的優先級別還要高,那麼就和父節點的元素相互交換,再接着網上進行比較,直到沒法再繼續交換爲止。因爲二叉堆是一棵徹底二叉樹,並假設堆的大小爲k,所以整個過程其實就是沿着樹的高度網上爬,因此只須要O(logk)的時間。

3
                                /      \
                               5      9                3, 5, 9

                                   3
                                 /    \
                                5    9                 3,5,9,2
                                /
                              2

                                   3
                                 /    \
                                2    9                 3,2,9,5
                                /
                              5

                                   2
                                 /    \
                                3    9                 2,3,9,5
                                /
                              5

如何進行向下篩選呢?當堆頂的元素被取出時,咱們要更新堆頂的元素來做爲下一次按照優先級順序被取出的對象,咱們所須要的是將堆底部的元素放置到堆頂,而後不斷地對它執行向下篩選的操做,在這個過程當中,該元素和它的兩個孩子節點對比,看看哪一個優先級最高,若是優先級最高的是其中一個孩子,就將該元素和那個孩子進行交換,而後反覆進行下去,直到沒法繼續交換爲止,整個過程就是沿着樹的高度往下爬,因此時間複雜度也是O(logk)。

2
                                 /    \
                                3    9                 2,3,9,5
                                /
                              5

                                   5
                                 /    \
                                3    9                 5,3,9

                                   3
                                 /    \
                                5    9                 3,5,9

所以,不管是添加新的數據仍是取出堆頂的元素,都須要O(logk)的時間。

另一個最重要的時間複雜度是優先隊列的初始化,這是分析運用優先隊列性能時必不可少的,也是常常容易弄錯的地方。

假設咱們有n個數據,咱們須要建立一個大小爲n的堆,乍一看,每當把一個數據加入到堆裏,咱們都要對其執行向上篩選的操做,這樣以來就是O(nlogn)。可是,在建立這個堆的過程當中,二叉樹的大小是從1逐漸增加到n的,因此整個算法的複雜度是:

圖片: https://uploader.shimo.im/f/q...

通過進一步的推導,最終的結果是O(n)。算法面試中是不要求推導的,你只須要記住,初始化一個大小爲n的堆,所須要的時間是O(n)便可。

注:
向上篩選,用這個靜態圖

3
                                 /    \
                                5    9            
                                /
                              2

例題分析

LeetCode 第347題. Top K Frequent Words 從一系列單詞中找出使用頻率最高的前K個單詞

這道題的輸入是一個字符串數組,數組裏的元素可能會重複一次甚至屢次,要求按順序輸出前K個出現次數最多的字符串。

當咱們拿到這個題目的時候,看到」前K個「這樣的字眼就應該很天然地聯想到運用優先隊列。

那麼優先級別怎麼計算呢?讓咱們來分析一下,優先級別能夠由字符串出現的次數來決定,出現的次數越多,優先級別越高,反之越低。

統計詞頻的最佳數據結構就是哈希表(Hash Map),利用一個哈希表,咱們就能快速的知道每一個單詞出現的次數。

而後將單詞和其出現的次數做爲一個新的對象來構建一個優先隊列,那麼這個問題就很垂手可得地解決了。

能夠看到,解這類求前K個的題目,關鍵是看如何定義優先級以及優先隊列中元素的數據結構。這道題能夠說是利用優先對列處理問題的典型,建議好好看看。

Desk (3)
                                 /    \
                          car(2)   book(1)

圖(Graph)
圖片: https://uploader.shimo.im/f/f...

接下來讓咱們看看如何準備圖論的知識點。

圖能夠說是全部數據結構裏面知識點最豐富的一個,最基本的知識點就有以下這些:
階(Order)、度:出度(Out-Degree)、入度(In-Degree)
樹(Tree)、森林(Forest)、環(Loop)
有向圖(Directed Graph)、無向圖(Undirected Graph)、徹底有向圖、徹底無向圖
連通圖(Connected Graph)、連通份量(Connected Component)
存儲和表達方式:鄰接矩陣(Adjacency Matrix)、鄰接鏈表(Adjacency List)
圍繞圖的算法也是五花八門:

圖的遍歷:深度優先、廣度優先
環的檢測:有向圖、無向圖
拓撲排序
最短路徑算法:Dijkstra、Bellman-Ford、Floyd Warshall
連通性相關算法:Kosaraju、Tarjan、求解孤島的數量、判斷是否爲樹
圖的着色、旅行商問題等
以上的知識點只是圖論裏的冰山一角,對於算法面試而言,咱們徹底不須要對每一個知識點都一一掌握,而應該有的放矢地進行準備。

力扣裏邊有許多關於圖論的算法題,並且都是很是經典的題目,根據個人概括和經驗歸納,如下的知識點是必須充分掌握並反覆練習的:
圖的存儲和表達方式:鄰接矩陣(Adjacency Matrix)、鄰接鏈表(Adjacency List)
圖的遍歷:深度優先、廣度優先
二部圖的檢測(Bipartite)、樹的檢測、環的檢測:有向圖、無向圖
拓撲排序
聯合-查找算法(Union-Find)
最短路徑:Dijkstra、Bellman-Ford

其中,環的檢測、二部圖的檢測、樹的檢測以及拓撲排序都是基於圖的遍歷,尤爲是深度優先方式的遍歷。而遍歷能夠在鄰接矩陣或者鄰接鏈表上進行,因此掌握好圖的遍歷是重中之重!由於它是全部其餘圖論算法的基礎。

至於最短路徑算法,能區分它們的不一樣特色,知道在什麼狀況下用哪一種算法就很好了。對於有充足時間準備的面試者,能熟練掌握它們的寫法固然是最好的。

下面讓咱們經過一道例題來複習圖論的知識。

例題分析

LeetCode 第785題. Is Graph Bipartite? 檢測一個圖是否爲二部圖

什麼是二部圖?

圖片: https://uploader.shimo.im/f/s...

所謂二部圖,就是在圖裏面,圖的全部頂點能夠分紅兩個子集U和V,子集裏的頂點互不直接相連,圖裏面全部的邊,一頭連着子集U裏的頂點,一頭連着子集V裏的頂點。

如今,給定一個任意的圖,如何判斷它是否爲二部圖呢?很顯然,咱們必需要對這個圖進行一次遍歷。遍歷的方法有深度優先以及廣度優先。關於深度優先和廣度優先算法,咱們將在第06節課進行詳細地討論。

基本的思想是,咱們來給圖裏的頂點塗上顏色,子集U裏的頂點都塗上紅色,子集V裏的頂點都塗上藍色。下面咱們開始遍歷這個圖的全部頂點了,想象一下咱們手裏握有兩種顏色(紅色和藍色)的畫筆,每次咱們交替地給遍歷當中遇到的頂點塗上顏色,若是這個頂點尚未顏色,那咱們就給它塗上顏色,而後換成另一支畫筆,遇到下一個頂點的時候,若是發現這個頂點已經塗上了顏色,並且顏色跟我手裏畫筆的顏色不一樣,那麼表示這個頂點它既能在子集U裏,也能在子集V裏,因此,它不是一個二部圖。

前綴樹(Trie)
圖片: https://uploader.shimo.im/f/N...

接下來咱們來看看前綴樹,它的英文念Trie。

前綴樹也被稱爲字典樹,由於這種數據結構被普遍地運用在字典查找當中。什麼是字典查找呢?舉個例子,給定一系列字符串,這些字符串構成了一種字典,要求你在這個字典當中找出全部以「ABC」開頭的字符串。

對於這樣的問題,也許你會認爲,那不是很簡單嘛,直接遍歷一遍字典,而後逐個判斷每一個字符串是否由「ABC」開頭就行了。假設字典很大,有N個單詞,咱們要對比的不是「ABC」,而是任意的,那咱們不妨假設所要對比的開頭平均長度爲M,那麼這種暴力的搜索算法,時間複雜度是O(M*N)。

若是咱們用前綴樹頭幫助咱們對字典的存儲進行優化,那麼咱們能夠把搜索的時間複雜度降低爲O(M),其中M表示字典裏最長的那個單詞的字符個數,在不少狀況下,字典裏的單詞個數N是遠遠大於M的。所以,前綴樹在這種場合中是很是高效的。

前綴樹的經典應用有哪些呢?

咱們在網站上的搜索框中輸入要搜索的文字時,搜索框會羅列出以搜索文字做爲開頭的相關搜索信息,這裏就運用到了前綴樹,在後端進行快速地檢索。

另外,咱們常常會用到漢字拼音輸入法,它的聯想輸出功能也運用到了前綴樹。

好,既然前綴樹如此強大,那就讓咱們來看看它的結構吧。讓咱們經過一個例子來深刻理解它,假如咱們有一個字典,字典裏面有以下單詞:"A","to","tea","ted","ten","i","in","inn",每一個單詞還能有本身的一些權重值,那麼用前綴樹來構建這個字典將會是以下的樣子:

圖片: https://uploader.shimo.im/f/0...

前綴樹有以下幾個重要的性質:

每一個節點至少包含兩個基本屬性:
children:數組或者集合,羅列出每一個分支當中包含的全部字符
isEnd: 布爾值,表示該節點是否爲某字符串的結尾
前綴樹的根節點是空的,所謂空,也就是說咱們只利用到了這個節點的children屬性,即咱們只關心在這個字典裏,有哪些打頭的字符。
除了根節點,其餘全部節點都有多是單詞的結尾,葉子節點必定都是單詞的結尾。

前綴樹最基本的操做就是兩個:建立和搜索,下面咱們就分別來談談它們。

建立前綴樹的方法其實很直觀,遍歷一遍輸入的字符串,對每一個字符串的字符進行遍歷,在遍歷的過程當中,咱們從前綴樹的根節點開始,將每一個字符加入到節點的children字符集當中,若是字符集已經包含了這個字符,就能夠跳過,若是當前字符是字符串的最後一個,那麼就把當前節點的isEnd標記爲真。

前綴樹真正強大的地方在於,每一個節點還能用來保存額外的信息,好比能夠用來記錄擁有相同前綴的全部字符串。這樣一來,當用戶輸入某個前綴時,咱們就能在O(1)的時間內給出對應的推薦字符串。

建立好前綴樹以後,搜索就變得容易了,方法相似,咱們從前綴樹的根節點出發,逐個匹配輸入的前綴字符,若是遇到了就繼續往下一層搜索,若是沒遇到,就當即返回。

例題分析

LeetCode 第212題:Word Search II 單詞查找 II

這是一道出現較爲頻繁的難題,題目給出了一個二維的字符矩陣,而後還給出了一個字典,如今要求在這個字符矩陣中找到出如今字典裏的單詞。好比咱們有以下的字符矩陣:

board = [
['o', 'a', 'a', 'n'],
['e', 't', 'a', 'e'],
['i', 'h', 'k', 'r'],
['i', 'f', 'l', 'v']
]

字典是 = ["oath", "pea", "eat", "rain"]

要求輸出: ["eat","oath"]

解這道題目的基本思想是,因爲字符矩陣的每一個點都能做爲一個字符串的開頭,因此咱們必須得嘗試從矩陣中的全部字符出發,上下左右一步步地走,而後去和字典進行匹配,若是發現那些通過的字符能組成字典裏的單詞,咱們就把它記錄下來。

那麼,分別從矩陣的每一個字符出發,上下左右一步步地走,咱們能夠借用深度優先的算法。關於深度優先算法,咱們將在第06節課深刻探討,若是你對它不熟悉,能夠把它想象成走迷宮。

好,基本的算法有了,那麼還剩下一個問題,就是如何和字典匹配呢?直觀的作法是每次都循環遍歷字典,看看是否存在字典裏面,若是咱們把輸入的字典變爲哈希集合的話,彷佛只須要O(1)的時間就能完成匹配。

可是,這樣的對比並不能進行前綴的對比,也就是說,咱們必須每次都要進行一次全面的深度優先搜索,或者搜索的長度爲字典裏最長的字符串長度,這樣仍是不夠高效。假如咱們在矩陣裏遇到了一個字符」V」,而咱們的字典里根本就沒有以「V」開頭的字符串,那麼咱們根本就不須要將深度優先搜索進行下去,這樣一來,能夠幫助咱們大大地提升搜索效率。

剛纔咱們提到了對比字符串的前綴,你應該能想到咱們須要藉助前綴樹來幫咱們從新構建字典了吧。構建好了前綴樹以後,每次咱們從矩陣裏的某個字符出發進行搜索的時候,同步地對前綴樹進行對比,若是發現字符一直能被找到,就繼續進行下去,一步一步地匹配,直到在前綴樹裏發現一個完整的字符串,把它輸出便可。

分段樹(Segment Tree)
圖片: https://uploader.shimo.im/f/3...

接下來咱們來看看分段樹。

要理解好什麼是分段樹以及爲何會有這樣的數據結構,讓咱們從一個問題出發進行深刻的探討吧。

假設咱們有一個數組array[0 … n-1], 裏面有n個元素,如今咱們要常常對這個數組作兩件事:

更新數組元素的數值
求數組任意一段區間裏元素的總和(或者平均值)

咱們知道,更新數組元素的數值總能在O(1)的時間內完成,對於求數組元素的總和,直觀上看必須遍歷一遍數組,因此平均得用O(n)的時間。那麼對於數據不少,並且須要頻繁地更新並求和的操做,有沒有更好的辦法呢?答案就是分段樹。

分段樹能讓咱們在O(logn)的時間裏更新元素的數值以及在O(logn)的時間裏完成對數組的求和。

所謂分段樹,就是一種按照二叉樹的形式存儲數據的結構,每一個節點保存的都是數組裏某一段的總和。例如,數組是[1, 3, 5, 7, 9, 11],那麼它的分段樹就是:

圖片: https://uploader.shimo.im/f/O...

能夠看出,根節點保存的是從下標0到下標5的全部元素的總和,即36,它的左右兩個子節點分別保存左右兩半元素的總和,按照這樣的邏輯不斷地切分下去,最終的葉子節點保存的就是每一個元素的數值。

當咱們更新數組裏某個元素的數值時,咱們得從分段樹的根節點出發,更新節點的數值,由於它保存的是數組元素的總和。接下來,修改的元素有可能會落在分段樹裏一些區間裏,至少葉子節點是確定須要更新的,因此,咱們要作的是從根節點往下,判斷元素的下標是否在左邊仍是右邊,而後更新分支裏的節點大小。所以,複雜度就是遍歷樹的高度,即O(logn)。

完成了元素數值的更新以後,咱們要對數組某個區間段裏的元素進行求和了。方法和更新操做相似,首先從跟節點出發,判斷所求的區間是否落在節點所表明的區間中,若是所要求的區間徹底包含了節點所表明的區間,那麼就得加上該節點的數值,意味着該節點所記錄的區間總和只是咱們所要求解總和的一部分。接下來,不斷地往下尋找其餘的子區間,最終得出所要求的總和。

分段樹的實如今書寫起來有些繁瑣,須要不斷地練習才能加深印象。

例題分析
LeetCode 第315題:Count of Smaller Numbers After Self

題目看起來很是簡單,給定一個數組nums,裏面都是一些整數,如今要求打印輸出一個新的數組counts,counts數組的每一個元素counts[i]表示nums中第i個元素右邊有多少個數小於nums[i]。

例如:輸入數組是[5, 2, 6, 1],應該輸出的結果是[2, 1, 1, 0]。什麼意思呢?好比對於5來講,它的右邊有兩個數比它小,分別是2和1,因此輸出的結果中,第一個元素是2,對於2來講,右邊只有1比它小,因此第二個元素是1,以此類推。

理解了問題以後,咱們來看看如何借用分段樹來幫助咱們。
既然是分段樹,咱們就得想一想分段樹的每一個節點應該須要包含什麼樣的信息,能夠想象得出,它應該包含某個區間裏的數的某種信息。

咱們在以前介紹分段樹知識點的時候,每一個節點記錄的區間是數組下標所造成的區間,然而對於這道題來講,由於咱們要統計的是比某個數還要小的數的總和,若是咱們把分段的區間設計成按照數值的大小來劃分,並記錄下在這個區間中的數的總和的話,那麼我就能快速地知道比當前數還要小的數有多少個了。

若是你仍是一頭霧水,咱們來看看具體是如何實現的。

首先,讓咱們從分段樹的根節點開始,根節點記錄的是數組裏最小值到最大值之間的全部元素的總和,而後分割根節點成左區間和右區間,不斷地分割下去,最後咱們的分段樹成爲這樣的模樣:

[1 - 6] (0)
                                                              /                       \
                                                  [1 - 3] (0)                 [4 - 6] (0)
                                                    /       \                    /              \
                                           [1 - 2] (0)    [3] (0)        [4 - 5] (0)     [6] (0)
                                              /    \                            /     \
                                        [1] (0)   [2] (0)            [4] (0)    [5] (0)

初始化的時候,每一個節點記錄的在此區間內的元素數量是0,接下來咱們從數組的最後一位開始往前遍歷,每次遍歷,判斷這個數落在哪一個區間,那麼那個區間的數量加一。

當咱們遇到1的時候,把它加入到分段樹裏,此時分段樹裏各個節點所統計的數量會發生必定的變化:

[1 - 6] (1)
                                                              /                       \
                                                  [1 - 3] (1)                 [4 - 6] (0)
                                                    /       \                    /              \
                                        [1 - 2] (1)      [3] (0)        [4 - 5] (0)     [6] (0)
                                              /    \                            /     \
                                        [1] (1)   [2] (0)            [4] (0)    [5] (0)

同時咱們知道當前所遇到的最小值就是1。

接下來咱們來看看6,當咱們把6加入到分段樹裏時,分段樹會發生以下變化:

[1 - 6] (2)
                                                              /                       \
                                                  [1 - 3] (1)                 [4 - 6] (1)
                                                    /       \                    /              \
                                        [1 - 2] (1)      [3] (0)        [4 - 5] (0)     [6] (1)
                                              /    \                            /     \
                                        [1] (1)   [2] (0)            [4] (0)    [5] (0)

在這個時候,讓咱們來求一下比6小的數有多少個?其實也就是查詢一下分段樹,從1到5之間有多少個數。

咱們從根節點開始查詢。因爲所要查詢的區間是1到5,它沒法包含根節點的區間1到6,因此繼續往下查詢。咱們來看左邊,很明顯,區間1到3被徹底包含在1到5之間,咱們把該節點所統計好的數返回。咱們再看來右邊,區間1到5跟區間4到6有交叉,繼續往下看,區間4到5徹底被包含在1到5之間,因此咱們能夠立刻返回,並把統計的數量相加。

最後咱們得出,在當前位置,在6的右邊比6小的數只有一個。

咱們經過這樣的方法,每次把當前的數用分段樹進行個數統計,而後再計算出比它小的數便可。算法複雜度是O(nlogn)。

樹狀數組(Fenwick Tree / Binary Indexed Tree)
圖片: https://uploader.shimo.im/f/c...

最後咱們來看看樹狀數組。

樹狀數組也被稱爲Binary Indexed Tree,一樣的,爲了瞭解樹狀數組,讓咱們經過一個問題來好好理解一下這個數據結構吧。

假設咱們有一個數組array[0 … n-1], 裏面有n個元素,如今咱們要常常對這個數組作兩件事:

更新數組元素的數值
求數組前k個元素的總和(或者平均值)

乍一看,咱們彷佛能夠運用分段樹來求解,的確,分段樹是能夠幫助咱們在O(logn)的時間裏更新和求解前k個元素的總和。可是,讓咱們仔細看看這個問題,問題只要求求解前k個元素的總和,並不要求任意一個區間,在這種狀況下,咱們能夠借用樹狀數組了,它可讓咱們一樣在O(logn)的時間裏完成上述的操做。並且,相對於分段樹的實現,樹狀數組顯得更簡單。

樹狀數組的數據結構有如下幾個重要的基本特徵:

它是利用數組來表示多叉樹的結構,在這一點上和優先隊列有些相似,只不過,優先隊列是用數組來表示徹底二叉樹,而樹狀數組是多叉樹。
樹狀數組的第一個元素是空節點。
若是節點tree[y]是tree[x]的父節點,那麼須要知足以下條件
y = x - (x & (-x))

因爲樹狀數組所解決的問題跟分段樹有些相似,因此咱們就不花篇幅進行問題的討論了,LeetCode上有不少經典的題目能夠用樹狀數組來解決,好比LeetCode第308題,求一個動態變化的二維矩陣裏,任意子矩陣裏的數的總和。
結語
圖片: https://uploader.shimo.im/f/k...

經過這節課的學習,咱們瞭解到了一些高級的數據結構。

優先隊列能夠說是常常出如今考題裏的,它的實現過程比較繁瑣,可是不少編程語言裏都有它的實現,因此在解決面試中的問題時,實行「拿來主義」便可。我仍是會鼓勵你本身練習實現一個優先隊列,在實現它的過程當中更好地去了解它的結構和特色。

圖是被普遍運用的數據結構,不少涉及大數據的問題都得運用到圖論的知識,好比在社交網絡裏,每一個人能夠用圖的頂點表示,人與人直接的關係能夠用圖的邊表示,在好比,在地圖上,要求解從起始點到目的地,如何行駛會更快捷,就得須要運用圖論裏的最短路徑算法。

前綴樹出如今許多面試的難題當中,不少時候,你得本身實現一棵前綴樹,這要求你能熟練地書寫它的實現以及運用它。

分段樹和樹狀數組的應用場合比較明確,在這節課裏咱們對一維的狀況進行了探討,若是問題變爲在一幅圖片當中修改像素的顏色,而後求解任意矩形區間的灰度平均值,那麼能夠考慮採用二維的分段樹了。

力扣平臺上,針對上面的這些高級數據結構都有豐富的題目,但願你能用功地學習。

內容摘取自《300分鐘搞定算法面試》

第02講:高級數據結構

主講人:蘇勇,谷歌資深技術工程師

相關文章
相關標籤/搜索