数据结构和算法(六):贪心算法、分治算法、回溯算法、动态规划、拓扑排序

从广义上来讲:数据结构就是一组数据的存储结构 , 算法就是操作数据的方法

数据结构是为算法服务的,算法是要作用在特定的数据结构上的。

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阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容