在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

拓撲排序算法原理是什么

新材料在線 ? 來源:labuladong ? 作者:labuladong ? 2021-08-16 15:02 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

很多讀者留言說要看「圖」相關的算法,那就滿足大家,結合算法題把圖相關的技巧給大家過一遍。

前文 學習數據結構的框架思維 說了,數據結構相關的算法無非兩點:遍歷 + 訪問。那么圖的基本遍歷方法也很簡單,前文 圖算法基礎 就講了如何從多叉樹的遍歷框架擴展到圖的遍歷。

圖這種數據結構還有一些比較特殊的算法,比如二分圖判斷,有環(huán)圖無環(huán)圖的判斷,拓撲排序,以及最經典的最小生成樹,單源最短路徑問題,更難的就是類似網絡流這樣的問題。

不過以我的經驗呢,像網絡流這種問題,你又不是打競賽的,除非自己特別有興趣,否則就沒必要學了;像最小生成樹和最短路徑問題,雖然從刷題的角度用到的不多,但它們屬于經典算法,學有余力可以掌握一下;像拓撲排序這一類,屬于比較基本且有用的算法,應該比較熟練地掌握。

那么本文就結合具體的算法題,來說說拓撲排序算法原理,因為拓撲排序的對象是有向無環(huán)圖,所以順帶說一下如何判斷圖是否有環(huán)。

判斷有向圖是否存在環(huán)

函數簽名如下:

int[] findOrder(int numCourses, int[][] prerequisites);

題目應該不難理解,什么時候無法修完所有課程?當存在循環(huán)依賴的時候。

其實這種場景在現實生活中也十分常見,比如我們寫代碼 import 包也是一個例子,必須合理設計代碼目錄結構,否則會出現循環(huán)依賴,編譯器會報錯,所以編譯器實際上也使用了類似算法來判斷你的代碼是否能夠成功編譯。

看到依賴問題,首先想到的就是把問題轉化成「有向圖」這種數據結構,只要圖中存在環(huán),那就說明存在循環(huán)依賴。

具體來說,我們首先可以把課程看成「有向圖」中的節(jié)點,節(jié)點編號分別是0, 1, ..., numCourses-1,把課程之間的依賴關系看做節(jié)點之間的有向邊。

比如說必須修完課程1才能去修課程3,那么就有一條有向邊從節(jié)點1指向3。

所以我們可以根據題目輸入的prerequisites數組生成一幅類似這樣的圖:

0275083a-fe50-11eb-9bcf-12bb97331649.jpg

如果發(fā)現這幅有向圖中存在環(huán),那就說明課程之間存在循環(huán)依賴,肯定沒辦法全部上完;反之,如果沒有環(huán),那么肯定能上完全部課程。

好,那么想解決這個問題,首先我們要把題目的輸入轉化成一幅有向圖,然后再判斷圖中是否存在環(huán)。

如何轉換成圖呢?我們前文 圖論基礎 寫過圖的兩種存儲形式,鄰接矩陣和鄰接表。

以我刷題的經驗,常見的存儲方式是使用鄰接表,比如下面這種結構:

List《Integer》[] graph;

graph[s]是一個列表,存儲著節(jié)點s所指向的節(jié)點。

所以我們首先可以寫一個建圖函數:

List《Integer》[] buildGraph(int numCourses, int[][] prerequisites) {

// 圖中共有 numCourses 個節(jié)點

List《Integer》[] graph = new LinkedList[numCourses];

for (int i = 0; i 《 numCourses; i++) {

graph[i] = new LinkedList《》();

}

for (int[] edge : prerequisites) {

int from = edge[1];

int to = edge[0];

// 修完課程 from 才能修課程 to

// 在圖中添加一條從 from 指向 to 的有向邊

graph[from].add(to);

}

return graph;

}

圖建出來了,怎么判斷圖中有沒有環(huán)呢?

先不要急,我們先來思考如何遍歷這幅圖,只要會遍歷,就可以判斷圖中是否存在環(huán)了。

前文 圖論基礎 寫了 DFS 算法遍歷圖的框架,無非就是從多叉樹遍歷框架擴展出來的,加了個visited數組罷了:

// 防止重復遍歷同一個節(jié)點boolean[] visited;

// 從節(jié)點 s 開始 BFS 遍歷,將遍歷過的節(jié)點標記為 truevoid traverse(List《Integer》[] graph, int s) {

if (visited[s]) {

return;

}

/* 前序遍歷代碼位置 */

// 將當前節(jié)點標記為已遍歷

visited[s] = true;

for (int t : graph[s]) {

traverse(graph, t);

}

/* 后序遍歷代碼位置 */

}

那么我們就可以直接套用這個遍歷代碼:

// 防止重復遍歷同一個節(jié)點boolean[] visited;

boolean canFinish(int numCourses, int[][] prerequisites) {

List《Integer》[] graph = buildGraph(numCourses, prerequisites);

visited = new boolean[numCourses];

for (int i = 0; i 《 numCourses; i++) {

traverse(graph, i);

}

}

void traverse(List《Integer》[] graph, int s) {

// 代碼見上文

}

注意圖中并不是所有節(jié)點都相連,所以要用一個 for 循環(huán)將所有節(jié)點都作為起點調用一次 DFS 搜索算法。

這樣,就能遍歷這幅圖中的所有節(jié)點了,你打印一下visited數組,應該全是 true。

前文 學習數據結構和算法的框架思維 說過,圖的遍歷和遍歷多叉樹差不多,所以到這里你應該都能很容易理解。

那么如何判斷這幅圖中是否存在環(huán)呢?

我們前文 回溯算法核心套路詳解 說過,你可以把遞歸函數看成一個在遞歸樹上游走的指針,這里也是類似的:

你也可以把traverse看做在圖中節(jié)點上游走的指針,只需要再添加一個布爾數組onPath記錄當前traverse經過的路徑:

boolean[] onPath;

boolean hasCycle = false;

boolean[] visited;

void traverse(List《Integer》[] graph, int s) {

if (onPath[s]) {

// 發(fā)現環(huán)!!!

hasCycle = true;

}

if (visited[s]) {

return;

}

// 將節(jié)點 s 標記為已遍歷

visited[s] = true;

// 開始遍歷節(jié)點 s

onPath[s] = true;

for (int t : graph[s]) {

traverse(graph, t);

}

// 節(jié)點 s 遍歷完成

onPath[s] = false;

}

這里就有點回溯算法的味道了,在進入節(jié)點s的時候將onPath[s]標記為 true,離開時標記回 false,如果發(fā)現onPath[s]已經被標記,說明出現了環(huán)。

PS:參考貪吃蛇沒繞過彎兒咬到自己的場景。

這樣,就可以在遍歷圖的過程中順便判斷是否存在環(huán)了,完整代碼如下:

// 記錄一次 traverse 遞歸經過的節(jié)點boolean[] onPath;

// 記錄遍歷過的節(jié)點,防止走回頭路boolean[] visited;

// 記錄圖中是否有環(huán)boolean hasCycle = false;

boolean canFinish(int numCourses, int[][] prerequisites) {

List《Integer》[] graph = buildGraph(numCourses, prerequisites);

visited = new boolean[numCourses];

onPath = new boolean[numCourses];

for (int i = 0; i 《 numCourses; i++) {

// 遍歷圖中的所有節(jié)點

traverse(graph, i);

}

// 只要沒有循環(huán)依賴可以完成所有課程

return !hasCycle;

}

void traverse(List《Integer》[] graph, int s) {

if (onPath[s]) {

// 出現環(huán)

hasCycle = true;

}

if (visited[s] || hasCycle) {

// 如果已經找到了環(huán),也不用再遍歷了

return;

}

// 前序遍歷代碼位置

visited[s] = true;

onPath[s] = true;

for (int t : graph[s]) {

traverse(graph, t);

}

// 后序遍歷代碼位置

onPath[s] = false;

}

List《Integer》[] buildGraph(int numCourses, int[][] prerequisites) {

// 代碼見前文

}

這道題就解決了,核心就是判斷一幅有向圖中是否存在環(huán)。

不過如果出題人繼續(xù)惡心你,讓你不僅要判斷是否存在環(huán),還要返回這個環(huán)具體有哪些節(jié)點,怎么辦?

你可能說,onPath里面為 true 的索引,不就是組成環(huán)的節(jié)點編號嗎?

不是的,假設下圖中綠色的節(jié)點是遞歸的路徑,它們在onPath中的值都是 true,但顯然成環(huán)的節(jié)點只是其中的一部分:

0280b39c-fe50-11eb-9bcf-12bb97331649.jpg

那么接下來,我們來再講一個經典的圖算法:拓撲排序。

拓撲排序

這道題就是上道題的進階版,不是僅僅讓你判斷是否可以完成所有課程,而是進一步讓你返回一個合理的上課順序,保證開始修每個課程時,前置的課程都已經修完。

函數簽名如下:

int[] findOrder(int numCourses, int[][] prerequisites);

這里我先說一下拓撲排序(Topological Sorting)這個名詞,網上搜出來的定義很數學,這里干脆用百度百科的一幅圖來讓你直觀地感受下

直觀地說就是,讓你把一幅圖「拉平」,而且這個「拉平」的圖里面,所有箭頭方向都是一致的,比如上圖所有箭頭都是朝右的。

很顯然,如果一幅有向圖中存在環(huán),是無法進行拓撲排序的,因為肯定做不到所有箭頭方向一致;反過來,如果一幅圖是「有向無環(huán)圖」,那么一定可以進行拓撲排序。

但是我們這道題和拓撲排序有什么關系呢?

其實也不難看出來,如果把課程抽象成節(jié)點,課程之間的依賴關系抽象成有向邊,那么這幅圖的拓撲排序結果就是上課順序。

首先,我們先判斷一下題目輸入的課程依賴是否成環(huán),成環(huán)的話是無法進行拓撲排序的,所以我們可以復用上一道題的主函數:

public int[] findOrder(int numCourses, int[][] prerequisites) {

if (!canFinish(numCourses, prerequisites)) {

// 不可能完成所有課程

return new int[]{};

}

// 。..

}

PS:簡單起見,canFinish 直接復用了之前實現的函數,但實際上可以把環(huán)檢測的邏輯和拓撲排序的邏輯結合起來,同時在 traverse 函數里完成,這個可以留給大家自己去實現。

那么關鍵問題來了,如何進行拓撲排序?是不是又要秀什么高大上的技巧了?

其實特別簡單,將后序遍歷的結果進行反轉,就是拓撲排序的結果。

直接看解法代碼:

boolean[] visited;

// 記錄后序遍歷結果

List《Integer》 postorder = new ArrayList《》();

int[] findOrder(int numCourses, int[][] prerequisites) {

// 先保證圖中無環(huán)

if (!canFinish(numCourses, prerequisites)) {

return new int[]{};

}

// 建圖

List《Integer》[] graph = buildGraph(numCourses, prerequisites);

// 進行 DFS 遍歷

visited = new boolean[numCourses];

for (int i = 0; i 《 numCourses; i++) {

traverse(graph, i);

}

// 將后序遍歷結果反轉,轉化成 int[] 類型

Collections.reverse(postorder);

int[] res = new int[numCourses];

for (int i = 0; i 《 numCourses; i++) {

res[i] = postorder.get(i);

}

return res;

}

void traverse(List《Integer》[] graph, int s) {

if (visited[s]) {

return;

}

visited[s] = true;

for (int t : graph[s]) {

traverse(graph, t);

}

// 后序遍歷位置

postorder.add(s);

}

// 參考上一題的解法boolean canFinish(int numCourses, int[][] prerequisites);

// 參考前文代碼

List《Integer》[] buildGraph(int numCourses, int[][] prerequisites);

代碼雖然看起來多,但是邏輯應該是很清楚的,只要圖中無環(huán),那么我們就調用traverse函數對圖進行 BFS 遍歷,記錄后序遍歷結果,最后把后序遍歷結果反轉,作為最終的答案。

那么為什么后序遍歷的反轉結果就是拓撲排序呢?

我這里也避免數學證明,用一個直觀地例子來解釋,我們就說二叉樹,這是我們說過很多次的二叉樹遍歷框架:

void traverse(TreeNode root) {

// 前序遍歷代碼位置

traverse(root.left)

// 中序遍歷代碼位置

traverse(root.right)

// 后序遍歷代碼位置

}

二叉樹的后序遍歷是什么時候?遍歷完左右子樹之后才會執(zhí)行后序遍歷位置的代碼。換句話說,當左右子樹的節(jié)點都被裝到結果列表里面了,根節(jié)點才會被裝進去。

后序遍歷的這一特點很重要,之所以拓撲排序的基礎是后序遍歷,是因為一個任務必須在等到所有的依賴任務都完成之后才能開始開始執(zhí)行。

你把每個任務理解成二叉樹里面的節(jié)點,這個任務所依賴的任務理解成子節(jié)點,那你是不是應該先把所有子節(jié)點處理完再處理父節(jié)點?這是不是就是后序遍歷?

下圖是一個二叉樹的后序遍歷結果:

02cd8668-fe50-11eb-9bcf-12bb97331649.jpg

結合這個圖說一說為什么還要把后序遍歷結果反轉,才是最終的拓撲排序結果。

我們說一個節(jié)點可以理解為一個任務,這個節(jié)點的子節(jié)點理解為這個任務的依賴,但你注意我們之前說的依賴關系的表示:如果做完A才能去做B,那么就有一條從A指向B的有向邊,表示B依賴A。

那么,父節(jié)點依賴子節(jié)點,體現在二叉樹里面應該是這樣的

是不是和我們正常的二叉樹指針指向反過來了?所以正常的后序遍歷結果應該進行反轉,才是拓撲排序的結果。

以上,我簡單解釋了一下為什么「拓撲排序的結果就是反轉之后的后序遍歷結果」,當然,我的解釋雖然比較直觀,但并沒有嚴格的數學證明,有興趣的讀者可以自己查一下。

總之,你記住拓撲排序就是后序遍歷反轉之后的結果,且拓撲排序只能針對有向無環(huán)圖,進行拓撲排序之前要進行環(huán)檢測,這些知識點已經足夠了。

責任編輯:haq

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯系本站處理。 舉報投訴
  • 數據
    +關注

    關注

    8

    文章

    7252

    瀏覽量

    91660
  • 拓撲結構
    +關注

    關注

    6

    文章

    327

    瀏覽量

    40023

原文標題:拓撲排序,YYDS!

文章出處:【微信號:xincailiaozaixian,微信公眾號:新材料在線】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關推薦
    熱點推薦

    開關電源設計如何選擇合適拓撲

    在設計你的變換器前,你必須首先選擇電路拓撲。因為其它所有電路元件設計,像元件選擇,磁芯設計,閉環(huán)補償等等都取決于拓撲。所以在設計開始之前,你得首先仔細研究所要開發(fā)的電源的要求和技術規(guī)范:輸入、輸出電壓,輸出功率、輸出紋波、電磁兼容要求等等,以保證選擇適當的
    的頭像 發(fā)表于 05-23 09:55 ?4587次閱讀
    開關電源設計如何選擇合適<b class='flag-5'>拓撲</b>

    低成本電源排序器解決方案

    絕大多數負載點DC-DC轉換器可以將上一個轉換器的電源就緒輸出連接至下一個轉換器的使能輸入,實現上電排序。這種方法只適合比較簡單的設計,不能滿足多數現代微處理器和DSP的要求一這類器件要求斷電順序必須與上電順序相反。許多廠商針對這類應用推出了可編程排序IC,但器件價格較為
    的頭像 發(fā)表于 05-21 09:55 ?486次閱讀
    低成本電源<b class='flag-5'>排序</b>器解決方案

    開關電源功率變換器拓撲與設計

    詳細講解開關電源功率變換器的各種拓撲電路,通過實例詳細講解。 共分為12章,包括功率變換器的主要拓撲介紹和工程設計指南兩大部分內容。其中,拓撲部分主要包括正激、反激、對稱驅動橋式、隔離Boost
    發(fā)表于 05-19 16:26

    開關電源拓撲結構介紹

    一 、緒論開關電源電路拓撲是指功率器件和電磁元件連接在電路中的方式,而磁性元件設計、閉環(huán)補償電路以及所有其他電路元件的設計都依賴于拓撲拓撲可分為:開關型和非開關型兩大類。其中開關型拓撲
    發(fā)表于 05-12 16:04

    反向降壓拓撲如何替代非隔離反激式拓撲 德州儀器反向降壓拓撲詳細解析

    歡迎來到 《電源設計小貼士集錦》系列文章 ? 本期,我們將介紹 反向降壓拓撲 的詳細知識 ? 最常見的電源之一是 離線電源 ,也稱為交流電源。隨著旨在集成典型家庭功能的產品越來越多,市場對輸出能力
    發(fā)表于 05-10 10:19 ?366次閱讀
    反向降壓<b class='flag-5'>拓撲</b>如何替代非隔離反激式<b class='flag-5'>拓撲</b> 德州儀器反向降壓<b class='flag-5'>拓撲</b>詳細解析

    移相全橋ZVS及ZVZCS拓撲結構分析

    移相全橋 ZVS 及 ZVZCS 拓撲結構分析 1.引言 移相控制方式是控制型軟開關技術在全開關 PWM 拓撲的兩態(tài)開關模式(通態(tài)和斷態(tài))通過控制方法變?yōu)槿龖B(tài)開關工作模式(通態(tài)斷態(tài)和續(xù)流態(tài)),在
    發(fā)表于 03-04 16:42

    詳解Linux sort命令之掌握排序技巧與實用案例

    在linux系統(tǒng)使用過程中,提供了sort排序命令,支持常用的排序功能。 常用參數 sort命令支持很多參數,常用參數如下: ? 短參數 長參數 說明 -n – number-sort 按字符串數值
    的頭像 發(fā)表于 01-09 10:10 ?887次閱讀

    TimSort:一個在標準函數庫中廣泛使用的排序算法

    在計算機科學的領域,排序算法是每位學生必學的基礎,而排序的需求是每位程序員在編程過程中都會遇到的。 在你輕松調用 .sort() 方法對數據進行排序時,是否曾好奇過,這個簡單的方法背后
    的頭像 發(fā)表于 01-03 11:42 ?556次閱讀

    【「從算法到電路—數字芯片算法的電路實現」閱讀體驗】+內容簡介

    內容簡介這是一本深入解讀基礎算法及其電路設計,以打通算法研發(fā)到數字IC設計的實現屏障,以及指導芯片設計工程師從底層掌握復雜電路設計與優(yōu)化方法為目標的專業(yè)技術書。任何芯片(如WiFi芯片、5G芯片
    發(fā)表于 11-21 17:14

    CAN總線十萬個為什么 | 聊聊幾種常見的CAN網絡拓撲

    導讀隨著CAN總線的應用越來越廣泛,工程師在面對各種不同工況下,如何選擇合適的網絡拓撲方式就變成了一個讓人頭疼的問題。這篇文章會介紹主流的幾種總線拓撲方式,可以幫您快速了解如何選擇。大家好!歡迎
    的頭像 發(fā)表于 11-21 01:03 ?1461次閱讀
    CAN總線十萬個為什么 | 聊聊幾種常見的CAN網絡<b class='flag-5'>拓撲</b>

    電源拓撲快速參考指南

    電子發(fā)燒友網站提供《電源拓撲快速參考指南.pdf》資料免費下載
    發(fā)表于 11-13 15:25 ?6次下載
    電源<b class='flag-5'>拓撲</b>快速參考指南

    時間復雜度為 O(n^2) 的排序算法

    作者:京東保險 王奕龍 對于小規(guī)模數據,我們可以選用時間復雜度為 O(n2) 的排序算法。因為時間復雜度并不代表實際代碼的執(zhí)行時間,它省去了低階、系數和常數,僅代表的增長趨勢,所以在小規(guī)模數據情況下
    的頭像 發(fā)表于 10-19 16:31 ?1690次閱讀
    時間復雜度為 O(n^2) 的<b class='flag-5'>排序</b><b class='flag-5'>算法</b>

    TPS54120排序和跟蹤

    電子發(fā)燒友網站提供《TPS54120排序和跟蹤.pdf》資料免費下載
    發(fā)表于 10-10 10:54 ?0次下載
    TPS54120<b class='flag-5'>排序</b>和跟蹤

    三電平dcdc拓撲結構有幾種

    三電平DC-DC拓撲結構是一種高效的電力轉換技術,廣泛應用于電力電子領域。 三電平DC-DC拓撲結構的基本原理 三電平DC-DC拓撲結構是一種將輸入電壓轉換為輸出電壓的電力轉換器。它通過控制開關器件
    的頭像 發(fā)表于 07-12 09:45 ?2916次閱讀

    ANPC三電平拓撲優(yōu)缺點是什么

    ANPC(Active Neutral-Point Clamped)三電平拓撲是一種廣泛應用于中高功率應用的電力電子轉換器。它具有許多優(yōu)點,但也存在一些局限性。 一、ANPC三電平拓撲的基本概念
    的頭像 發(fā)表于 07-12 09:43 ?5425次閱讀
    主站蜘蛛池模板: 欧美日韩一日韩一线不卡 | 免费色黄网站 | 夜夜操夜夜骑 | 男女视频免费 | 一区二区三区四区在线免费观看 | 天天添天天干 | 色偷偷男人天堂 | 亚洲邪恶天堂影院在线观看 | 午夜aaaaaaaaa视频在线 | 狠狠干狠狠鲁 | 人人搞人人干 | 欧美性淫爽www视频播放 | 起碰成人免费公开网视频 | 亚洲国产成人成上人色 | 萌白酱香蕉白丝护士服喷浆 | 大又大粗又爽又黄少妇毛片 | 成年片色大黄全免费网址 | aa视频在线观看 | 欧美精品一二区 | 999毛片 | 动漫精品成人免费网站 | 天天搞天天搞 | 久久久久久青草大香综合精品 | 色婷婷5月 | 好爽毛片一区二区三区四 | 五月天婷婷导航 | 女人张开腿男人猛桶视频 | 国内一级野外a一级毛片 | 一级毛毛片毛片毛片毛片在线看 | 4虎影院永久地址www | 人人看人人干 | 日韩精品另类天天更新影院 | 欧美一级特黄aaa大片 | 精品国产第一页 | 激情综合婷婷丁香六月花 | 美女扒开尿口让男生添 漫画 | 国产福利影视 | 四虎在线最新永久免费 | 爽好舒服快受不了了老师 | 国产亚洲美女精品久久久2020 | 99国产精品久久久久久久成人热 |