數據結構和算法(六):貪心算法、分治算法、回溯算法、動態規劃、拓撲排序

從廣義上來講:數據結構就是一組數據的存儲結構 , 算法就是操作數據的方法

數據結構是為算法服務的,算法是要作用在特定的數據結構上的。

10個最常用的數據結構:數組、鏈表、棧、隊列、散列表、二叉樹、堆、跳表、圖、Trie樹

10個最常用的算法:遞歸、排序、二分查找、搜索、哈希算法、貪心算法、分治算法、回溯算法、動態規劃、字符串匹配算法

本文總結了20個最常用的數據結構和算法,不管是應付面試還是工作需要,只要集中精力攻克這20個知識點就足夠了。

數據結構和算法(一):復雜度、數組、鏈表、棧、隊列的傳送門

數據結構和算法(二):遞歸、排序、通用排序算法的傳送門

數據結構和算法(三):二分查找、跳表、散列表、哈希算法的傳送門

數據結構和算法(四):二叉樹、紅黑樹、遞歸樹、堆和堆排序、堆的應用的傳送門

數據結構和算法(五):圖、深度優先搜索和廣度優先搜索、字符串匹配算法、Trie樹、AC自動機的傳送門

數據結構和算法(六):貪心算法、分治算法、回溯算法、動態規劃、拓撲排序的傳送門

第二十六章 貪心算法

一、什么是貪心算法
    1. 貪心算法是指在對問題求解時,總是做出在當前看來是最好的選擇,也就是說不從整體考慮,而是從局部看來是最優解,所以貪心算法得到的結果不一定是最優的。
    1. 貪心算法沒有固定的算法解決框架,算法的關鍵就是貪婪策略的選擇,根據不同問題選擇不同的策略。
    1. 貪心算法的適用場景比較有限,更多是用來指導設計基礎算法,比如最小生成樹算法、單源最短路徑算法等。
二、使用貪心算法的解決問題的思路
    1. 當我們看到此類數據時,首先要聯想到貪心算法:針對一組數據,我們定義了限制值和期望值,希望從中選擇幾個數據,在滿足限制值的前提下,期望值最大。(例如:從寶庫中只能拿100kg的物品,從黃金、白銀、純鐵怎么選擇,使得價值最大,這里面限制值就是100kg,期望值就是價值最大。)
    1. 將問題抽象成限制值、期望值后,就可以嘗試選擇合適的貪婪策略去解決了,在剛才那個例子中,貪婪策略就是盡量多拿單價最高的金屬。
    1. 選擇不同的貪婪策略后,看下貪心算法產生的結果是否是最優的。從實踐的角度來說,大部分能用貪心算法解決的問題,貪心算法的正確性都是顯而易見的,也不需要嚴格證明。
三、貪心算法實戰分析
    1. 分糖果
    • (1). 假設我們有m個糖果和n個孩子,要把糖果分給孩子吃,糖果多孩子少,每個糖果的大小不等,每個孩子對糖果的需求也不相同,只要糖果的大小超過了孩子的需求,那么這個孩子就會得到滿足,請問:如何分配糖果,才能滿足最多數量的孩子呢?

    • (2). 解決這個問題的第一步,我們把問題抽象成:在n個孩子中,選擇一部分孩子分配糖果,使得滿足的孩子最多。這里m個糖果就是限制值,最多滿足的孩子就是期望值。

    • (3). 解決這個問題的第二步,嘗試用貪心算法解決。對應一個孩子來說,如果小的糖果就可以解決,那么沒必要用大的糖果,所以分配糖果的時候我們可以用需求最小的孩子開始分配。

    • (4). 我們的分配策略就是:每次從剩下的孩子中選擇需求最小的孩子,分配給能滿足他的最少糖果,這樣的分配方案,就是滿足孩子最多的方案,這也是顯而易見的最優方案。

    1. 區間覆蓋
    • (1). 假設有n個區間,區間的起始端點和結束端點分別是[a1,a2]、[b1,b2]...,我們從這n個區間內選取一部分不相交的區間,問:怎么選擇,才能使得不相交的區間個數最多呢?

    • (2). 解決問題第一步,我們把問題抽象化,假設n個區間的最左側是min端點,最右側是max端點,這個問題就相當于,從n個區間中選取幾個不相交的區間,從左到右將[min,max]覆蓋完。

    • (3). 解決問題的第二步,選擇合適的貪婪策略嘗試解決,我們每次選擇的時候,選擇左邊端點不重合,右邊端點盡可能小的區間,使得剩下的區域盡可能大,就可以放置更多的區間。如下圖:


      區間覆蓋.png
    1. 如何用貪心算法實現霍夫曼壓縮編碼
    • (1). 假設有1000個字符,每個字符占1個字節,一共占1000個字節,也就是8000bit的存儲空間;如果我們統計發現這1000個字符,只有6種字符,分別為a、b、c、d、e、f的話,我們就可以用3個bit來表示他們,如下圖,這樣我們就可以只占用3000bit的空間了,比原來節省了很多,那么我們還有更節省空間的方法嗎?(3個bit其實可以存放8種不同的字符,2 x 2 x 2 = 8)
    a(000)、b(001)、c(010)、d(011)、e(100)、f(101)
    
    • (2). 霍夫曼編碼就登場了,霍夫曼編碼廣泛應用于數據壓縮中,壓縮率在20%~90%之間,霍夫曼編碼不僅會考察文本中有多少個字符,還會統計字符出現的頻率,根據頻率不同,選擇不同長度的編碼,頻率高的字符選用短編碼,頻率低的字符選用稍長編碼,霍夫曼編碼試圖使用不等長的編碼方式,來進一步增加壓縮效率。

    • (3). 由于霍夫曼編碼是不等長的,所以解壓縮的時候就比較困難,不知道該讀取1位還是2位還是3位來解壓縮,所以為了避免這種歧義,霍夫曼編碼要求各個字符的編碼之間,不能出現一個字符的編碼是另一個字符編碼的前綴。

    • (4). 在上面那個例子中,我們假設這6個字符出現的頻率從高到低依次是a、b、c、d、e、f,我們采用霍夫曼編碼的方式進行編碼后,就是下圖的樣子,任意一個字符的編碼都不是其他字符編碼的前綴,在解壓縮的時候,就可以以 讀取盡可能長的可解壓二進制串 的方式來解壓,就不會出現解壓歧義了,通過霍夫曼編碼來壓縮,這1000個字符只需要占用2100bit的存儲空間就夠了。

      采用霍夫曼編碼對1000個字符進行壓縮

    • (5). 霍夫曼編碼的思想并不難理解,但是如何根據字符出現的頻率,選用不同長度的編碼呢? 這里可以這樣處理,如下圖:把每個字符都看作一個節點,把頻率最低的兩個節點f、c組合在一起生成一個父節點x,x的頻率為f、c頻率之和,再把x節點和頻率次低的d節點組合生成父節點y,一直重復下去,直到把字符處理完;接下來,給每條邊上畫一個權值,指向左節點的邊統一記做0,指向右節點的邊統一記做1,那么從根節點到葉子節點的路徑,就是葉子節點對應字符的霍夫曼編碼了。


      霍夫曼編碼如何根據字符頻率選編碼

第二十七章 分治算法

一、什么是分治算法?
    1. 分治算法的核心思想就是四個字:分而治之,就是將原問題分解成n個小問題,解決這些小問題后,將結果合并,就可以得到原問題的解了。
    1. 想用分治算法解決問題,一般需要滿足以下條件:
    • (1). 原問題與分解成的小問題具有相同模式
    • (2). 子問題之間沒有關聯性,可以獨立求解(需要跟動態規劃區分開)
    • (3). 具有分解終止條件,也就是說,當問題足夠小時,可以直接求解
    • (4). 可以將子問題合并成原問題,并且合并操作復雜度不能太高,不然就起不到降低總體算法復雜度的目的了
二、分治思想在海量數據處理中的應用
    1. 假設要給10G的訂單數據按照金額進行排序,但是我們機器的內存只有2G,無法一次性加載到內存,也就無法單純的利用快排、歸并算法來解決了,就可以使用分治思想來解決
    1. 面對10G的訂單數據,我們可以先掃描一遍訂單,根據訂單金額劃分出幾個金額區間,例如1到100元的放到一個文件中,100到200元的放到另一個文件中,以此類推,這樣每個小文件都可以加載到內存中,最后將這些小文件合并,就得到有序的10G訂單數據了。
    1. 如果訂單數據存儲在類似于GFS的分布式系統上,就可以將多個小文件并行加載到多臺機器上并行處理,這樣處理速度就會加快很多,這就是分治思想的一個應用。

第二十八章 回溯算法

第二十九章 初始動態規劃

第三十章 動態規劃實戰

第三十一章 拓撲排序

一、什么是拓撲排序?
    1. 從局部有序推斷出全局的順序就叫做拓撲排序,例如,我們穿衣服的時候是有一定順序的,你必須先穿襪子才能穿鞋,你必須先穿內褲才能穿秋褲,假設我們有8件衣服,可以按照下面的順序進行穿衣服,就可以滿足局部關系的前提下滿足全局有序。
      拓撲排序.png
    1. 我們知道,算法是構建在具體數據結構之上的,我們想進行拓撲排序,就需要先把問題背景抽象成具體的數據結構,我們把衣服之間的依賴關系抽象成一個有向圖,每件衣服對應圖中的一個,依賴關系就是頂點之間的,而且這個圖不僅是有向圖,還得是有向無環圖,因為圖中一旦有了環,就無法進行拓撲排序了,所以拓撲排序是基于有向無環圖的一個算法。抽象成的數據結構如下:

public class Graph {
  private int v; // 頂點的個數
  private LinkedList<Integer> adj[]; // 鄰接表

  public Graph(int v) {
    this.v = v;
    adj = new LinkedList[v];
    for (int i=0; i<v; ++i) {
      adj[i] = new LinkedList<>();
    }
  }

  public void addEdge(int s, int t) { // s先于t,邊s->t
    adj[s].add(t);
  }
}
二、如何在有向無環圖上進行拓撲排序?
    1. Kahn算法
    • (1). 我們從圖中找到入度為0的頂點,進行輸出(如果一個頂點的入度為0,就意味著沒有頂點先于這個頂點了,所以這個頂點就應該輸出了),并且把這個頂點從圖中刪除,即把這個頂點可達的頂點的入度都減一;

    • (2). 然后我們重復上述過程,直到輸出所有頂點,這樣輸出的序列就是拓撲排序之后的序列了。(輸出的序列就是滿足局部依賴關系的全局序列)

    • (3). 具體代碼實現如下


public void topoSortByKahn() {
  int[] inDegree = new int[v]; // 統計每個頂點的入度
  for (int i = 0; i < v; ++i) {
    for (int j = 0; j < adj[i].size(); ++j) {
      int w = adj[i].get(j); // i->w
      inDegree[w]++;
    }
  }
  LinkedList<Integer> queue = new LinkedList<>();
  for (int i = 0; i < v; ++i) {
    if (inDegree[i] == 0) queue.add(i);
  }
  while (!queue.isEmpty()) {
    int i = queue.remove();
    System.out.print("->" + i);
    for (int j = 0; j < adj[i].size(); ++j) {
      int k = adj[i].get(j);
      inDegree[k]--;
      if (inDegree[k] == 0) queue.add(k);
    }
  }
}
    1. DFS算法
    • (1). 首先根據鄰接表,構造出逆鄰接表,在逆鄰接表中,邊s->t表示s依賴于t,也就是s后于t執行。
    • (2). 然后我們遞歸處理每個頂點,先輸出這個頂點可以到達的所有頂點,然后在輸出它自己。
    • (3). 代碼實現如下:

public void topoSortByDFS() {
  // 先構建逆鄰接表,邊s->t表示,s依賴于t,t先于s
  LinkedList<Integer> inverseAdj[] = new LinkedList[v];
  for (int i = 0; i < v; ++i) { // 申請空間
    inverseAdj[i] = new LinkedList<>();
  }
  for (int i = 0; i < v; ++i) { // 通過鄰接表生成逆鄰接表
    for (int j = 0; j < adj[i].size(); ++j) {
      int w = adj[i].get(j); // i->w
      inverseAdj[w].add(i); // w->i
    }
  }
  boolean[] visited = new boolean[v];
  for (int i = 0; i < v; ++i) { // 深度優先遍歷圖
    if (visited[i] == false) {
      visited[i] = true;
      dfs(i, inverseAdj, visited);
    }
  }
}

private void dfs(
    int vertex, LinkedList<Integer> inverseAdj[], boolean[] visited) {
  for (int i = 0; i < inverseAdj[vertex].size(); ++i) {
    int w = inverseAdj[vertex].get(i);
    if (visited[w] == true) continue;
    visited[w] = true;
    dfs(w, inverseAdj, visited);
  } // 先把vertex這個頂點可達的所有頂點都打印出來之后,再打印它自己
  System.out.print("->" + vertex);
}
三、Kahn算法和DFS算法進行拓撲排序的時間復雜度
    1. 從Kahn算法的代碼中可以看出,每個頂點和每條邊都被訪問了一次,所以時間復雜度是O(V+E),V是頂點個數,E是邊的個數
    1. 從DFS算法可以看出,每個頂點被訪問兩次,每條邊被訪問一次,所以時間復雜度也是O(V+E),V是頂點個數,E是邊的個數
    1. 如果我們想知道數據庫中所有用戶的推薦關系之間,有沒有存在環,就可以使用拓撲排序,把用戶之間的的推薦關系從數據庫加載到內存中,構建成今天所講的這種有向圖數據結構,再利用拓撲排序,就可以很快檢測出是否存在環了。

第三十二章 最短路徑

第三十三章 位圖

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容