主頁 > 後端開發 > 研究了一個月的「拓撲排序」,給大家說一說

研究了一個月的「拓撲排序」,給大家說一說

2020-10-02 20:25:56 後端開發

**前言**

Topological sort 又稱 Topological order,這個名字有點迷惑性,因為拓撲排序并不是一個純粹的排序演算法,它只是針對某一類圖,找到一個可以執行的線性順序,

 

這個演算法聽起來高大上,如今的面試也很愛考,比如當時我在面我司時有整整一輪是基于拓撲排序的設計,

 

但它其實是一個很好理解的演算法,跟著我的思路,讓你再也不會忘記她,

 

有向無環圖

剛剛我們提到,拓撲排序只是針對特定的一類圖,那么是針對哪類圖的呢?

 

答:Directed acyclic graph (DAG),有向無環圖,即:

 

  1. 這個圖的邊必須是有方向的;

  2. 圖內無環,

 

那么什么是方向呢?

 

比如微信好友就是有向的,你加了他好友他可能把你刪了你卻不知道,,,那這個朋友關系就是單向的,,

 

什么是環?環是和方向有關的,從一個點出發能回到自己,這是環,

 

所以下圖左邊不是環,右邊是,

img

 

那么如果一個圖里有環,比如右圖,想執行1就要先執行3,想執行3就要先執行2,想執行2就要先執行1,這成了個死回圈,無法找到正確的打開方式,所以找不到它的一個拓撲序,

 

總結:

  • 如果這個圖不是 DAG,那么它是沒有拓撲序的;

  • 如果是 DAG,那么它至少有一個拓撲序;

  • 反之,如果它存在一個拓撲序,那么這個圖必定是 DGA.

 

所以這是一個充分必要條件,

img

 

拓撲排序

那么這么一個圖的「拓撲序」是什么意思呢?

 

我們借用百度百科[1]的這個課程表來說明,

課程代號課程名稱先修課程
C1 高等數學
C2 程式設計基礎
C3 離散數學 C1, C2
C4 資料結構 C3, C5
C5 演算法語言 C2
C6 編譯技術 C4, C5
C7 作業系統 C4, C9
C8 普通物理 C1
C9 計算機原理 C8

 

這里有 9 門課程,有些課程是有先修課程的要求的,就是你要先學了「最右側這一欄要求的這個課」才能再去選「高階」的課程,

那么這個例子中拓撲排序的意思就是: 就是求解一種可行的順序,能夠讓我把所有課都學了,

 

那怎么做呢?

 

首先我們可以用圖來描述它,圖的兩個要素是頂點和邊,那么在這里:

  • 頂點:每門課

  • 邊:起點的課程是終點的課程的先修課

 

畫出來長這個樣:

img

這種圖叫 AOV (Activity On Vertex) 網路,在這種圖里:

  • 頂點:表示活動;

  • 邊:表示活動間的先后關系

 

所以一個 AOV 網應該是一個 DAG,即有向無環圖,否則某些活動會無法進行,

那么所有活動可以排成一個可行線性序列,這個序列就是拓撲序列,

 

那么這個序列的實際意義是:

按照這個順序,在每個專案開始時,能夠保證它的前驅活動都已完成,從而使整個工程順利進行,

 

回到我們這個例子中:

 

  1. 我們一眼可以看出來要先學 C1, C2,因為這兩門課沒有任何要求嘛,大一的時候就學唄;

  2. 大二就可以學第二行的 C3, C5, C8 了,因為這三門課的先修課程就是 C1, C2,我們都學完了;

  3. 大三可以學第三行的 C4, C9;

  4. 最后一年選剩下的 C6, C7,

 

這樣,我們就把所有課程學完了,也就得到了這個圖的一個拓撲排序,

 

注意,有時候拓撲序并不是唯一的,比如在這個例子中,先學 C1 再學 C2,和先 C2 后 C1 都行,都是這個圖的正確的拓撲序,但這是兩個順序了,

所以面試的時候要問下面試官,是要求解任意解,還是列出所有解,

我們總結一下,在這個圖里的邊表示的是一種依賴關系,如果要修下一門課,就要先把前一門課修了,

 

這和打游戲里一樣一樣的嘛,要拿到一個道具,就要先做 A 任務,再完成 B 任務,最終終于能到達目的地了,

 

演算法詳解

在上面的圖里,大家很容易就看出來了它的拓撲序,但當工程越來越龐大時,依賴關系也會變得錯綜復雜,那就需要用一種系統性的方式方法來求解了,

那么我們回想一下剛剛自己找拓撲序的程序,為什么我們先看上了 C1, C2?

因為它們沒有依賴別人啊,也就是它的入度為 0.

入度:頂點的入度是指「指向該頂點的邊」的數量; 出度:頂點的出度是指該頂點指向其他點的邊的數量,

 

所以我們先執行入度為 0 的那些點,那也就是要記錄每個頂點的入度,

 

因為只有當它的 入度 = 0 的時候,我們才能執行它,

 

在剛才的例子里,最開始 C1, C2 的入度就是 0,所以我們可以先執行這兩個,

 

那在這個演算法里第一步就是得到每個頂點的入度,

 

Step0: 預處理得到每個點的入度

 

我們可以用一個 HashMap 來存放這個資訊,或者用一個陣列會更精巧,

 

在文中為了方便展示,我就用表格了:

 C1C2C3C4C5C6C7C8C9
入度 0 0 2 2 1 2 2 1 1

 

Step1

 

拿到了這個之后,就可以執行入度為 0 的這些點了,也就是 C1, C2.

 

那我們把可以被執行的這些點,放入一個待執行的容器里,這樣之后我們一個個的從這個容器里取頂點就好了,

 

至于這個容器究竟選哪種資料結構,這取決于我們需要做哪些操作,再看哪種資料結構可以為之服務,

 

那么首先可以把[C1, C2]放入容器中,然后想想我們需要哪些操作吧!

 

我們最常做的操作無非就是把點放進來,把點拿出去執行了,也就是需要一個 offer 和 poll 操作比較高效的資料結構,那么 queue 就夠用了,

 

(其他的也行,放進來這個容器里的頂點的地位都是一樣的,都是可以執行的,和進來的順序無關,但何必非得給自己找麻煩呢?一個常規順序的簡簡單單的 queue 就夠用了,)

 

然后就需要把某些點拿出去執行了,

【劃重點】當我們把 C1 拿出來執行,那這意味這什么?

答:意味著「以 C1 為頂點」的「指向其他點」的「邊」都消失了,也就是 C1 的出度變成了 0.

 

如下圖,也就是這兩條邊可以消失了,

img

那么此時我們就可以更新 C1 所指向的那些點也就是 C3 和 C8 的 入度 了,更新后的陣列如下:

 

 C3C4C5C6C7C8C9
入度 1 2 1 2 2 0 1

 

那我們這里看到很關鍵的一步,C8 的入度變成了 0!

 

也就意味著 C8 此時沒有了任何依賴,可以放到我們的 queue 里等待執行了,

 

此時我們的 queue 里就是:[C2, C8].

 

Step2

 

下一個我們再執行 C2,

img

那么 C2 所指向的 C3, C5 的 入度-1,更新表格:

 

 C3C4C5C6C7C9
入度 0 2 0 2 2 1

 

也就是 C3 和 C5 都沒有了任何束縛,可以放進 queue 里執行了,

 

queue 此時變成:[C8, C3, C5]

 

Step3

 

那么下一步我們執行 C8,

img

相應的 C8 所指的 C9 的入度-1.更新表格:

 

 C4C6C7C9
入度 2 2 2 0

 

那么 C9 沒有了任何要求,可以放進 queue 里執行了,

 

queue 此時變成:[C3, C5, C9]

 

Step4

 

接下來執行 C3,

img

相應的 C3 所指的 C4 的入度-1.更新表格:

 

 C4C6C7
入度 1 2 2

 

但是 C4 的入度并沒有變成 0,所以這一步沒有任何點可以加入 queue.

queue 此時變成 [C5, C9]

 

Step5

 

再執行 C5,

img

那么 C5 所指的 C4 和 C6 的入度- 1.更新表格:

 

 C4C6C7
入度 0 1 2

 

這里 C4 的依賴全都消失啦,那么可以把 C4 放進 queue 里了:

queue = [C9, C4]

 

Step6

 

然后執行 C9,

img

那么 C9 所指的 C7 的入度- 1.

 

 C6C7
入度 1 1

這里 C7 的入度并不為 0,還不能加入 queue,此時 queue = [C4]

 

Step7

 

接著執行 C4,

img

所以 C4 所指向的 C6 和 C7 的入度-1,更新表格:

 

 C6C7
入度 0 0

 

C6 和 C7 的入度都變成 0 啦!!把它們放入 queue,繼續執行到直到 queue 為空即可,

 

總結

好了,那我們梳理一下這個演算法:

 

資料結構

這里我們的入度表格可以用 map 來存放

 

Map: <key = Vertex, value = https://www.cnblogs.com/coderhf/p/入度>

但實際代碼中,我們用一個 int array 來存盤也就夠了,陣列的 index 表示每個頂點,陣列里的數值來表示每個頂點的入度,這樣比 Map 更精巧,

然后用了一個普通的 queue,用來存放可以被執行的那些 node.

 

程序

我們把入度為 0 的那些頂點放入 queue 中,然后通過每次執行 queue 中的頂點,就可以讓依賴這個被執行的頂點的那些點的 入度-1,如果有頂點的入度變成了 0,就可以放入 queue 了,直到 queue 為空,

 

細節

這里有幾點實作上的細節:

 

當我們 check 是否有新的頂點的 入度 == 0 時,沒必要過一遍整個 map 或者陣列,只需要 check 剛剛改動過的就好了,

 

另一個是如果題目沒有給這個圖是 DAG 的條件的話,那么有可能是不存在可行解的,那怎么判斷呢?很簡單的一個方法就是比較一下最后結果中的頂點的個數和圖中所有頂點的個數是否相等,或者加個計數器,如果不相等,說明就不存在有效解,所以這個演算法也可以用來判斷一個圖是不是有向無環圖,

 

很多題目給的條件可能是給這個圖的 edge list,也是表示圖的一種常用的方式,那么給的這個 list 就是表示圖中的邊,這里要注意審題哦,看清楚是誰 depends on 誰,其實圖的題一般都不會直接給你這個圖,而是給一個場景,需要你把它變回一個圖,

 

時間復雜度

注意??:對于圖的時間復雜度分析一定是兩個引數,因為圖的頂點數和邊的數量沒有固定的關系,然而面試的時候很多同學張口就是 O(n)...

 

對于有 v 個頂點和 e 條邊的圖來說,

 

第一步,預處理得到 map 或者 array,需要過一遍所有的邊才行,所以是 O(e);

 

第二步,把 入度 == 0 的點入隊出隊的操作是 O(v),如果是一個 DAG,那所有的點都需要入隊出隊一次;

 

第三步,每次執行一個頂點的時候,要把它指向的那條邊消除了,這個總共執行 e 次;

 

總:O(v + e)

 

空間復雜度

用了一個陣列來存所有點的 indegree,之后的 queue 也是最多把所有的點放進去,所以是 O(v).

 

代碼

 

關于這課程排序的問題,Leetcode 上有兩道題,一道是 207,問你能否完成所有課程,也就是問拓撲排序是否存在;另一道是 210 題,是讓你回傳任意一個拓撲順序,如果不能完成,那就回傳一個空 array,

 

這里我們以 210 這道題來寫,更完整也更常考一些,

 

這里給的 input 就是我們剛剛說到的 edge list.

 

Example 1.

 

Input: 2, [[1,0]]

Output: [0,1]

Explanation: 這里一共2門課,1的先修課程是0. 所以正確的選課順序是[0, 1].

 

              1
Example 2. 
   

Input: 4, [[1,0],[2,0],[3,1],[3,2]]

Output: [0,1,2,3] or [0,2,1,3]

Explanation: 這里這個例子畫出來如下圖

 

 

img

 

Example 3.

Input: 2, [[1,0],[0,1]]

Output: null

Explanation: 這課沒法上了

              1
?
2
class Solution {
3
    public int[] findOrder(int numCourses, int[][] prerequisites) {
4
        int[] res = new int[numCourses];
5
        int[] indegree = new int[numCourses];
6
?
7
        // get the indegree for each course
8
        for(int[] pre : prerequisites) {
9
            indegree[pre[0]] ++;
10
        }
11
?
12
        // put courses with indegree == 0 to queue
13
        Queue<Integer> queue = new ArrayDeque<>();
14
        for(int i = 0; i < numCourses; i++) {
15
            if(indegree[i] == 0) {
16
                queue.offer(i);
17
            }
18
        }
19
?
20
        // execute the course
21
        int i = 0;
22
        while(!queue.isEmpty()) {
23
            Integer curr = queue.poll();
24
            res[i++] = curr;
25
?
26
            // remove the pre = curr
27
            for(int[] pre : prerequisites) {
28
                if(pre[1] == curr) {
29
                    indegree[pre[0]] --;
30
                    if(indegree[pre[0]] == 0) {
31
                        queue.offer(pre[0]);
32
                    }
33
                }
34
            }
35
        }
36
?
37
        return i == numCourses ? res : new int[]{};
38
    }
39
}
   

還是附上題目吧,just in case, if you want to see the details.

 

img

 

另外,拓撲排序還可以用 DFS - 深度優先搜索 來實作

 

實際應用

我們上文已經提到了它的一個 use case,就是選課系統,這也是最常考的題目,

 

而拓撲排序最重要的應用就是關鍵路徑問題,這個問題對應的是 AOE (Activity on Edge) 網路,

AOE 網路:頂點表示事件,邊表示活動,邊上的權重來表示活動所需要的時間, AOV 網路:頂點表示活動,邊表示活動之間的依賴關系,

在 AOE 網中,從起點到終點具有最大長度的路徑稱為關鍵路徑,在關鍵路徑上的活動稱為關鍵活動,AOE 網路一般用來分析一個大專案的工序,分析至少需要花多少時間完成,以及每個活動能有多少機動時間,

 

其實對于任何一個任務之間有依賴關系的圖,都是適用的,

 

比如 pom 依賴引入 jar 包時,大家有沒有想過它是怎么導進來一些你并沒有直接引入的 jar 包的?比如你并沒有引入 aop 的 jar 包,但它自動出現了,這就是因為你匯入的一些包是依賴于 aop 這個 jar 包的,那么 maven 就自動幫你匯入了,

 

其他的實際應用,知乎上專門有個帖子,在這里我總結一下:

  1. 語音識別系統的預處理;

  2. 管理目標檔案之間的依賴關系,就像我剛剛說的 jar 包匯入;

  3. 深度學習中的網路結構處理,

 

如有其他補充,歡迎大家在評論區不吝賜教,

如果覺得作者寫的還可以的,可以給個贊和關注,我還會繼續出新的優質文章,

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/149328.html

標籤:Java

上一篇:2020最新的Spring Boot 分布式鎖的具體實作(內附代碼)

下一篇:Jenkins Pipeline 部署 SpringBoot 應用

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more