C#圖表算法之最小生成樹
加權(quán)圖是一種為每條邊關(guān)聯(lián)一個(gè)權(quán)值或是成本的圖模型。這種圖能夠自然地表示許多應(yīng)用。在一幅航空?qǐng)D中,邊表示航線,權(quán)值則可以表示距離或是費(fèi)用。在這些情形中,最令人感興趣的自然是將成本最小化。這里用加權(quán)無向圖模型來解決最小生成樹:給定一幅加權(quán)無向圖,找到它的一棵最小生成樹。
圖的生成樹是它的一棵含有其所有頂點(diǎn)的無環(huán)連通子圖。一幅加權(quán)圖的最小生成樹(MST)是它的一棵權(quán)值(樹中所有邊的權(quán)值之和)最小的生成樹。
計(jì)算最小生成樹的兩種經(jīng)典算法:Prim 算法和 Kruskal 算法。
1.原理
樹的兩個(gè)重要性質(zhì):
1.用一條邊連接樹中的任意兩個(gè)頂點(diǎn)都會(huì)產(chǎn)生一個(gè)新的環(huán);
2.從樹中刪除一條邊將會(huì)得到兩棵獨(dú)立的樹。
這兩條性質(zhì)是證明最小生成樹的另一條基本性質(zhì)的基礎(chǔ),而由這條基本性質(zhì)就能夠得到最小生成樹的算法。
1.切分定理
我們稱之為切分定理的這條性質(zhì)將會(huì)把加權(quán)圖中的所有所有頂點(diǎn)分為兩個(gè)集合,檢查橫跨兩個(gè)集合的所有邊并識(shí)別哪條邊應(yīng)屬于圖的最小生成樹。
通常,我們指定一個(gè)頂點(diǎn)集并隱式地認(rèn)為它地補(bǔ)集為另一個(gè)頂點(diǎn)集來指定一個(gè)切分。這樣,一條橫切邊就是連接該集合地一個(gè)頂點(diǎn)和不在該集合地另一個(gè)頂點(diǎn)地一條邊。
切分定理:在一幅加權(quán)圖中,給定任意得切分,它的橫切邊中權(quán)重最小者必然屬于圖的最小生成樹。
注意,權(quán)重最小的橫切邊并不一定是所有橫切邊中唯一屬于圖的最小生成樹的邊。實(shí)際上,許多切分都會(huì)產(chǎn)生若干條屬于最小生成樹的橫切邊:
2.貪心算法
切分定理是解決最小生成樹問題的所有算法的基礎(chǔ)。更確切的說,這些算法都是一種貪心算法的特殊情況:使用切分定理找到最小生成樹的一條邊,不斷重復(fù)直到找到最小生成樹的所有便。這些算法之間的不同之處在于保存切分和判定權(quán)重最小的橫切邊的方式,但它們都是一下性質(zhì)特殊情況:
最小生成樹的貪心算法:下面這種算法會(huì)將含有 V 個(gè)頂點(diǎn)的任意加權(quán)連通圖中屬于最小生成樹的邊標(biāo)記為黑色:初始狀態(tài)下所有邊均為灰色,找到一種切分,它產(chǎn)生的橫切邊均不為黑色。將它權(quán)重最小的橫切邊標(biāo)記為黑色。反復(fù),直到標(biāo)記了 V - 1 條黑色邊為止。
2.加權(quán)無向圖的數(shù)據(jù)類型
namespace MinimumSpanningTrees { public class Edge:IComparable<Edge> { private int v;//頂點(diǎn)之一 private int w;//另一個(gè)頂點(diǎn) private double weight;//邊的權(quán)重 public Edge(int v, int w, double weight) { this.v = v; this.w = w; this.weight = weight; } public double Weight() { return weight; } public int Either() { return v; } public int Other(int vertex) { if (vertex == v) return w; else if (vertex == w) return v; else throw new ArgumentException(); } public int CompareTo([AllowNull] Edge other) { if (this.Weight() < other.Weight()) return -1; else if (this.Weight() > other.Weight()) return 1; else return 0; } public override string ToString() { return string.Format("%d-%d %.2f",v,w,weight); } } }
加權(quán)無向圖的實(shí)現(xiàn)使用了 Edge 對(duì)象。
namespace MinimumSpanningTrees { public class EdgeWeightedGraph { private int v;//頂點(diǎn)總數(shù) private int e;//邊的總數(shù) private List<Edge>[] adj;//鄰接表 public EdgeWeightedGraph(int V) { this.v = V; e = 0; adj = new List<Edge>[V]; for (int _v = 0; _v < V; _v++) { adj[_v] = new List<Edge>(); } } public int V() { return v; } public int E() { return e; } public void AddEdge(Edge edge) { int v = edge.Either(); int w = edge.Other(v); adj[v].Add(edge); adj[w].Add(edge); e++; } public IEnumerable<Edge> Adj(int _v) { return adj[_v]; } } }
為了整潔,用一對(duì) int 值和一個(gè) double 值表示每個(gè) Edge 對(duì)象。實(shí)際的數(shù)據(jù)結(jié)構(gòu)是一個(gè)鏈表,其中每個(gè)元素都是一個(gè)指向含有這些值的對(duì)象的指針。需要注意的是,雖然每個(gè)Edge對(duì)象都有兩個(gè)引用,但圖中的每條邊所對(duì)應(yīng)的 Edge 對(duì)象只有一個(gè)。
- 用權(quán)重來比較邊
Edge 類實(shí)現(xiàn)了 IComparable 接口的CompareTo 方法。一幅加權(quán)無向圖中的邊的自然次序就是按權(quán)重排序。
- 平行邊
和無環(huán)圖的實(shí)現(xiàn)一樣,這里也允許存在平行邊。我們也可以用更復(fù)雜的方法來消除平行邊,比如保留平行邊中權(quán)重最小者。
- 自環(huán)
允許存在自環(huán),這對(duì)最小生成樹算法沒有影響,因?yàn)樽钚∩蓸淇隙ú粫?huì)含有自環(huán)。
有個(gè) Edge 對(duì)象之后用例的代碼就可以變得更加干凈整潔。這也有小的代價(jià):每個(gè)鄰接表的結(jié)點(diǎn)都是一個(gè)都是一個(gè)指向 Edge 對(duì)象的引用,它們含有一些冗余的信息, v 的鄰接鏈表中的每個(gè)結(jié)點(diǎn)都會(huì)用一個(gè)變量保存 v 。使用對(duì)象也會(huì)帶來一些開銷。雖然每條邊的 Edge 對(duì)象都只有一個(gè),但鄰接表中還是會(huì)含有兩個(gè)指向同一 Edge 對(duì)象的引用。
另一種廣泛使用的方案是與 Graph 一樣,用兩個(gè)結(jié)點(diǎn)對(duì)象表示一條邊,每個(gè)結(jié)點(diǎn)對(duì)象都會(huì)保存頂點(diǎn)的信息和邊的權(quán)重。這種方法也是有代價(jià)的 -- 需要兩個(gè)結(jié)點(diǎn),每條邊的權(quán)重都會(huì)被保存兩次。
3.最小生成樹 API
按照慣例,在 API 中會(huì)定義一個(gè)接受加權(quán)無向圖為參數(shù)的構(gòu)造函數(shù)并且支持能夠?yàn)橛美祷貓D的最小生成樹和其權(quán)重的方法。那么應(yīng)該如何表示最小生成樹?由于圖的最小生成樹是圖的一幅子圖并且同時(shí)也是一棵樹,因此可以有一下選擇:
- 1.一組邊的列表;
- 2.一幅加權(quán)無向圖;
- 3.一個(gè)以頂點(diǎn)為索引且含有父結(jié)點(diǎn)鏈接的數(shù)組。
在為了各種應(yīng)用選擇這些表示方法時(shí),我們希望盡可能給予最小生成樹的實(shí)現(xiàn)以最大的靈活性,采用如下 API :
4.Prim 算法
Prim 算法,它的每一步都會(huì)為一棵生長(zhǎng)中的樹添加一條邊。一開始這棵樹只有一個(gè)頂點(diǎn),然后會(huì)向它添加 V-1 條邊,每次總是將下一條連接樹中的頂點(diǎn)與不在樹中的頂點(diǎn)且權(quán)重最小的邊加入樹中。
Prim 算法能夠得到任意加權(quán)連通圖的最小生成樹。這棵不斷生長(zhǎng)的樹定義了一個(gè)切分且不存在樹中的橫切邊。該算法會(huì)選擇權(quán)重最小的橫切邊并根據(jù)貪心算法不斷將他們標(biāo)記。
數(shù)據(jù)結(jié)構(gòu)
實(shí)現(xiàn) Prim 算法需要一些簡(jiǎn)單常見的數(shù)據(jù)結(jié)構(gòu)。具體來說,我們會(huì)用一下方法表示樹中的頂點(diǎn),邊和橫切邊:
1.頂點(diǎn):使用一個(gè)由頂點(diǎn)索引的布爾數(shù)組 marked[ ] ,如果頂點(diǎn) v 在樹中,那么 marked[v] 的值為 true。
2.邊:選擇以下兩種數(shù)據(jù)結(jié)構(gòu)之一:一條隊(duì)列 mst 來保存最小生成樹中的邊,或者一個(gè)由頂點(diǎn)索引的Edge對(duì)象的數(shù)組 edgeTo[ ] ,其中edgeTo[ v ] 為將 v 連接到樹中的 Edge 對(duì)象;
3.橫切邊:使用一條優(yōu)先隊(duì)列 MinPQ<Edge> 來根據(jù)權(quán)重比較所有邊。
有了這些數(shù)據(jù)結(jié)構(gòu)就可以回答 “哪條邊的權(quán)重最小” 這個(gè)基本問題了。
維護(hù)橫切邊的集合
每當(dāng)我們向樹中添加了一條邊后,也向樹中添加一個(gè)頂點(diǎn)。要維護(hù)一個(gè)包含所有橫切邊的集合,就要將連接這個(gè)頂點(diǎn)(新加入樹)和其他所有不在樹中的頂點(diǎn)的邊加入優(yōu)先隊(duì)列。但還有一點(diǎn):連接新加入樹中的頂點(diǎn)和其他已經(jīng)在樹中的頂點(diǎn)的所有邊都要失效(這樣的邊已經(jīng)不是橫切邊了,因?yàn)樗膬蓚€(gè)頂點(diǎn)都在樹中)。Prim 算法的即時(shí)實(shí)現(xiàn)可以將這樣的邊從優(yōu)先隊(duì)列中刪掉,先來看一種延時(shí)實(shí)現(xiàn),將這些邊先留在優(yōu)先隊(duì)列中,等到要?jiǎng)h除它們的時(shí)候再檢查邊的有效性。
算法構(gòu)造最小生成樹的過程:
1.將頂點(diǎn) 0 添加到最小生成樹,將它的鄰接鏈表中的所有邊添加到優(yōu)先隊(duì)列中。
2.取權(quán)重最小的邊 0-7,將頂點(diǎn) 7 和 邊 0-7 添加到最小生成樹中,將頂點(diǎn) 7 的鄰接鏈表中的所有邊添加到優(yōu)先隊(duì)列中。
3.將頂點(diǎn) 1 和邊 1-7 添加到最小生成樹中,將頂點(diǎn)的鄰接鏈表中的所有邊添加到優(yōu)先隊(duì)列中。
4.將頂點(diǎn) 2 和邊 0-2 添加到最小生成樹,將邊 2-3 和 6-2 添加到優(yōu)先隊(duì)列。邊 2-7 和 1-2 失效(因?yàn)檫叺膬蓚€(gè)頂點(diǎn)都在樹中,最后刪除)。
5.將頂點(diǎn) 3 和邊 2-3 添加到最小生成樹,將邊 3-6 添加到優(yōu)先隊(duì)列。邊 1-3 失效。
6.將頂點(diǎn) 5 和邊 5-7 添加到最小生成樹,將邊 4-5 添加到優(yōu)先隊(duì)列。邊 1-5 失效。
7.從優(yōu)先隊(duì)列刪除失效的邊 1-3,1-5 和 2-7 。
8.將頂點(diǎn) 4 和邊 4-5 添加到最小生成樹中,將邊 6-4 添加到優(yōu)先隊(duì)列。邊 4-7 和 0-4 失效。
9.從優(yōu)先隊(duì)列刪除失效的邊 1-2,4-7 和 0-4 。
10.將頂點(diǎn) 6 和邊 6-2 添加到最小生成樹中,和頂點(diǎn) 6 相關(guān)聯(lián)的其他邊均失效。
在添加了 V 個(gè)頂點(diǎn)以及 V-1 條邊之后,最小生成樹就完成了。優(yōu)先隊(duì)列中剩下的邊都是無效的,可以不用去檢查。
實(shí)現(xiàn)
我們使用一個(gè)私有方法 Visit()來為樹添加一個(gè)頂點(diǎn),將它標(biāo)記為“已訪問”并將與它關(guān)聯(lián)的所有未失效的邊加入優(yōu)先隊(duì)列,以保證隊(duì)列含有所有連接樹頂點(diǎn)和非樹頂點(diǎn)的邊(也可能含有一些已失效的邊)。代碼的內(nèi)循環(huán)是算法的具體實(shí)現(xiàn):從優(yōu)先隊(duì)列中取出一條邊并將它添加到樹中(如果沒有失效),再把這條邊的另一個(gè)頂點(diǎn)也添加到樹中,然后用新頂點(diǎn)作為參數(shù)調(diào)用 Visit 方法來更新橫切邊的集合。 Weight 方法可以遍歷樹的所有邊并得到權(quán)重之和。
namespace MinimumSpanningTrees { public class LazyPrimMST { private bool[] marked;//最小生成樹的頂點(diǎn) private Queue<Edge> mst;//最小生成樹的邊 private MinPQ<Edge> pq;//橫切邊(包括失效的邊) public LazyPrimMST(EdgeWeightedGraph G) { pq = new MinPQ<Edge>(); marked = new bool[G.V()]; mst = new Queue<Edge>(); Visit(G,0); while (!pq.Count > 0) { Edge e = pq.Dequeue(); int v = e.Either(); int w = e.Other(v); if (marked[v] && marked[w]) continue; pq.Enqueue(e); if (!marked[v]) Visit(G,v); if (!marked[w]) Visit(G,w); } } private void Visit(EdgeWeightedGraph G, int v) { marked[v] = true; foreach (Edge e in G.Adj(v)) { if (!marked[e.Other(v)]) pq.Enqueue(e); } } public IEnumerable<Edge> Edges() { return mst; } public double Weight() { return mst.Sum(o=>o.Weight()); } } }
性能
Prim 算法的延時(shí)實(shí)現(xiàn)計(jì)算一幅含有 V 個(gè)頂點(diǎn)和 E 條邊的連通加權(quán)無向圖的最小生成樹所需的空間和 E 成正比,所需的時(shí)間與 ElogE成正比(最壞情況)。算法的瓶頸在于優(yōu)先隊(duì)列的刪除和添加方法中比較邊的權(quán)重的次數(shù)。優(yōu)先隊(duì)列最多可能含有 E 條邊,這就是空間需求的上限。在最壞情況下,一次插入成本為 ~lgE ,刪除最小元素的成本為 ~2lgE 。因?yàn)樽疃嘀荒懿迦?E 條邊,刪除 E 次最小元素,時(shí)間上限顯而易見。
5. Prim 算法的即時(shí)實(shí)現(xiàn)
要改進(jìn)LazyPrimMST ,可以嘗試從優(yōu)先隊(duì)列中刪除失效的邊,這樣優(yōu)先隊(duì)列就只有樹頂點(diǎn)和非樹頂點(diǎn)之間的橫切邊,但還可以刪除更多的邊。關(guān)鍵在于,我們感興趣的只是連接樹頂點(diǎn)和非樹頂點(diǎn)中權(quán)重最小的邊。當(dāng)我們將頂點(diǎn) v 添加到樹中,對(duì)于每個(gè)非樹頂點(diǎn) w 產(chǎn)生的變化只可能使得 w 到最小生成樹的距離更近。簡(jiǎn)而言之,我們不需要在優(yōu)先隊(duì)列中保存所有從 w 到樹頂點(diǎn)的邊 —— 而只需保存其中權(quán)重最小的那條。在將 v 添加到樹中后檢查是否需要更新這條權(quán)重最小的邊(因?yàn)?v-w 的權(quán)重可能更?。?。我們只需遍歷 v 的鄰接鏈表就可以完成這個(gè)任務(wù)。換句話說,我們只會(huì)在優(yōu)先隊(duì)列中保存每個(gè)非樹頂點(diǎn) w 的一條邊:將它與樹中頂點(diǎn)連起來的權(quán)重最小的那條邊。將 w 和樹的頂點(diǎn)連接起來的其他權(quán)重較大的邊遲早都會(huì)失效,所以沒必要在優(yōu)先隊(duì)列中保存他們。
算法類 PrimMST 將 LazyPrimMST 類中 marked[ ] 和 mst[ ] 替換為兩個(gè)頂點(diǎn)索引的數(shù)組 edgeTo[ ] 和 distTo[ ] :
- 1.如果頂點(diǎn) v 不在樹中但至少含有一條邊和樹相連,那么 edgeTo[v] 是將 v 和樹連接的最短邊,distTo[v] 為這條邊的權(quán)重。
- 2.所有這類頂點(diǎn) v 都保存在一條索引優(yōu)先隊(duì)列中,索引 v 關(guān)聯(lián)的值是 edgeTo[v] 的邊的權(quán)重。
這些性質(zhì)的關(guān)鍵在于優(yōu)先隊(duì)列中的最小鍵即是權(quán)重最小的橫切邊的權(quán)重,而和它相關(guān)聯(lián)的頂點(diǎn) v 就是下一個(gè)將被添加到樹中的頂點(diǎn)。marked[ ] 數(shù)組已經(jīng)沒有必要了,因?yàn)榕袛鄺l件 !marked[w] 等價(jià)于 distTo[w] 是無窮的(且 edgeTo[w] 為 null)。要維護(hù)這些數(shù)據(jù)結(jié)構(gòu),PrimMST 會(huì)從優(yōu)先隊(duì)列中取出一條邊并檢查它的鄰接鏈表中的每條邊 v-w 。如果 w 已經(jīng)被標(biāo)記過,那這條邊就已經(jīng)失效了;如果 w 不在優(yōu)先隊(duì)列中或者 v-w 的權(quán)重小于目前已知的最小值 edgeTo[w],代碼會(huì)更新數(shù)組,將 v-w 作為將 w 和樹連接的最佳選擇。
namespace MinimumSpanningTrees { public class PrimMST { private Edge[] edgeTo;//距離樹最近的邊 private double[] distTo;//distTo[w]=edgeTo[w].weight() private bool[] marked;//如果 v 在樹中則為 true public IndexMinPQ<double> pq;//有效的橫切邊 public PrimMST(EdgeWeightedGraph G) { edgeTo = new Edge[G.V()]; distTo = new double[G.V()]; marked = new bool[G.V()]; for (int v = 0; v < G.V(); v++) { distTo[v] = Double.MaxValue; } pq = new IndexMinPQ<double>(G.V()); distTo[0] = 0.0; pq.Insert(0,0.0); //用頂點(diǎn) 0 和權(quán)重 0 初始化 pq while (pq.Count > 0) { Visit(G,pq.DelMin());//將最近的頂點(diǎn)添加到樹中 } } private void Visit(EdgeWeightedGraph G, int v) { //將頂點(diǎn) v 添加到樹中,更新數(shù)據(jù) marked[v] = true; foreach (Edge e in G.Adj(v)) { int w = e.Other(v); if (marked[w]) continue; if (e.Weight() < distTo[w]) { edgeTo[w] = e; distTo[w] = e.Weight(); if (pq.Contains(w)) pq.Change(w, distTo[w]); else pq.Insert(w,distTo[w]); } } } } }
Prim 算法的即時(shí)實(shí)現(xiàn)計(jì)算一幅含有 V 個(gè)頂點(diǎn)和 E 條邊的連通加權(quán)無向圖的最小生成樹所需的空間和 V 成正比,所需的時(shí)間與 ElogV成正比(最壞情況)。
6.Kruskal 算法
第二種生成最小生成樹的主要思想是按照邊的權(quán)重順序(從小到大)處理它們,將邊加入最小生成樹中,加入的邊不會(huì)與已經(jīng)加入的邊構(gòu)成環(huán),直到樹中含有 V-1 條邊為止。這些邊逐漸由一片森林合共成一棵樹。
為什么 Kruskal 算法能夠計(jì)算任意加權(quán)連通圖的最小生成樹?
如果下一條將被加入最小生成樹中的邊不會(huì)和已有的邊構(gòu)成環(huán),那么它就跨越了由所有和樹頂點(diǎn)相鄰的頂點(diǎn)組合的合集以及它們的補(bǔ)集所構(gòu)成的一個(gè)切分。因?yàn)榧尤氲倪叢粫?huì)形成環(huán),它是目前已知的唯一一條橫切邊且是按照權(quán)重順序選擇的邊,所以它必然是權(quán)重最小的橫切邊。因此,該算法能夠連續(xù)選擇權(quán)重最小的橫切邊,和貪心算法一致。
Prim 算法是一條邊一條邊地來構(gòu)造最小生成樹,每一步都為一棵樹添加一條邊。 Kruskal 算法構(gòu)造最小生成樹地時(shí)候也是一條邊一條邊地構(gòu)造,但不同的是它尋找的邊會(huì)連接一片森林中的兩棵樹。我們從一片由 V 棵單頂點(diǎn)的樹構(gòu)成的森林開始并不斷將兩棵樹合并(用可以找到的最短邊)直到只剩下一棵樹,它就是最小生成樹。
實(shí)現(xiàn)
我們將會(huì)使用一條優(yōu)先隊(duì)列來將邊按照權(quán)重排序,用一個(gè) union-find 數(shù)據(jù)結(jié)構(gòu)來識(shí)別會(huì)形成環(huán)的邊,以及一條隊(duì)列來保存最小生成樹的所有邊。
public class KruskalMST { private Queue<Edge> mst; public KruskalMST(EdgeWeightedGraph G) { mst = new Queue<Edge>();//最小生成樹的邊 MinPQ<Edge> pq = new MinPQ<Edge>(G.Edges);//所有邊 UF uf = new UF(G.V()); while (!pq.IsEmpty() && mst.Count < G.V() - 1) { Edge e = pq.DelMin();//從 pq 得到權(quán)重最小的邊和它的頂點(diǎn) int v = e.Either(), w = e.Other(v); if (uf.Connected(v, w))//忽略失效的邊 continue; uf.Union(v,w);//合并分量 mst.Enqueue(e);//將邊添加到最小生成樹中 } } }
Kruskal 算法的計(jì)算一幅含有 V 個(gè)頂點(diǎn)和 E 條邊的連通加權(quán)無向圖的最小生成樹所需的空間和 E 成正比,所需的時(shí)間和 ElogE 成正比(最壞情況)。
算法的實(shí)現(xiàn)在構(gòu)造函數(shù)中使用所有邊初始化優(yōu)先隊(duì)列,成本最多為 E 次比較(請(qǐng)見 union-find)。優(yōu)先隊(duì)列構(gòu)造完成后,其余的部分和 Prim 算法完全相同。優(yōu)先隊(duì)列中最多可能含有 E 條邊,即所需空間的上限。每次操作的成本最多為 2lgE 次比較,這就是時(shí)間上限的由來。 Kruskal 算法最多還會(huì)進(jìn)行 E 次 Connected() 和 V 次 Union()操作,但這些成本相比 ElogE 的總時(shí)間的增長(zhǎng)數(shù)量級(jí)可以忽略不計(jì)。
與 Prim 算法一樣,這個(gè)估計(jì)是比較保守的,因?yàn)樗惴ㄔ谡业?V-1 條邊之后就會(huì)終止。實(shí)際成本應(yīng)該與 E+E0 logE 成正比,其中 E0 是權(quán)重小于最小生成樹中權(quán)重最大的邊的所有邊的總數(shù)。盡管擁有這個(gè)優(yōu)勢(shì),Kruskal 算法一般還是比 Prim 算法要慢,因?yàn)樵谔幚砻織l邊時(shí),兩種方法都要完成優(yōu)先隊(duì)列操作之外,它還需要進(jìn)行一次 Connect() 操作。
到此這篇關(guān)于C#圖表算法之最小生成樹的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
unity 文件流讀取圖片與www讀取圖片的區(qū)別介紹
這篇文章主要介紹了unity 文件流讀取圖片與www讀取圖片的對(duì)比分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-04-04C#網(wǎng)絡(luò)編程基礎(chǔ)之進(jìn)程和線程詳解
這篇文章主要介紹了C#網(wǎng)絡(luò)編程基礎(chǔ)之進(jìn)程和線程詳解,本文對(duì)進(jìn)程、線程、線程池知識(shí)做了淺顯易懂的講解,并配有代碼實(shí)例,需要的朋友可以參考下2014-08-08C# 正則判斷一個(gè)數(shù)字的格式是否有逗號(hào)的代碼
c#正則判斷一個(gè)格式化數(shù)字里是否有逗號(hào)的解決方法2008-07-07基于c# Task自己動(dòng)手寫個(gè)異步IO函數(shù)
這篇文章主要介紹了如何基于c# Task自己動(dòng)手寫個(gè)異步IO函數(shù),幫助大家更好的理解和學(xué)習(xí)使用c#,感興趣的朋友可以了解下2021-03-03C#實(shí)例代碼之抽獎(jiǎng)升級(jí)版可以經(jīng)表格數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫(kù),抽獎(jiǎng)設(shè)置,補(bǔ)抽
這篇文章主要介紹了C#實(shí)例代碼之抽獎(jiǎng)升級(jí)版可以經(jīng)表格數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫(kù),抽獎(jiǎng)設(shè)置,補(bǔ)抽 的相關(guān)資料,需要的朋友可以參考下2016-01-01