数据结构篇--------算法

一、贪心算法

  1. 贪心算法的经典应用有:霍夫曼编码、Prim和Kruskal最小生成树算法、Dijkstra单源最短路径算法。
  2. 贪心算法思想:针对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的的情况下,期望值最大。
    严格证明贪心算法的正确性是非常复杂的,需要涉及较多的数学推理。而且从实践的角度来看,大部分能用贪心算法解决的问题正确性是显而易见的,不需要进行严格的数学证明。
    实际上,用贪心算法解决问题的思路,不一定总能给出最优解。
    经典案例:
    1.分糖果
    我们有m个糖果和n个孩子,现在要把糖果分给这些孩子吃,但是糖果少,孩子多(m 我们可以把这个问题抽象成,从n个孩子中,抽取一部分孩子分配糖果,让满足的孩子个数(期望值)是最大的,限制值是糖果个数m。对于一个孩子来说,如果小的糖果可以满足,就没必要用大的糖果,这样更大的就可以留给其他的对糖果需求更大的孩子。另一方面,对糖果的大小需求小的孩子更容易被满足,所以可以从需求小的孩子开始分配糖果因为满足一个对糖果需求大的孩子和需求小的孩子对我们的期望值的贡献是一样的。
    每次从剩下的孩子中找出对糖果的大小需求最小的,然后发给他剩下的糖果中能满足他的最小的糖果,这样得到的分配方案也就是满足最多孩子需求的个数最多的方案
    2.区间覆盖
    假设我们有n个区间,区间的起始端点和结束端点分别是[l1,r1],[l2,r2],[l3,r3],…,[ln,rn]。我们从这n个区间中选出一部分区间,这部分区间满足两两不相交(端点相交的地方不算相交),最多能选出几个区间呢?
    解决思路:假设这n个区间中最左端点是lmin,最右端点是rmax,这个问题就相当于选择几个不相交的区间,从左到右将[lmin,rmax]覆盖上,我们按照起始端点从小到大的顺序对这n个区间排序。
    每次选择的时候,左端点和前面的区间是不重合的,右端点又尽可能小的,这样可以让剩下的未覆盖区间尽可能的大,就可以放更多的区间,这实际上就是一种贪心的选择方法。

数据结构篇--------算法_第1张图片数据结构篇--------算法_第2张图片3.跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置,数组中的每个元素代表你可以在该位置跳跃的最大长度,判断你能否到达最后一个位置?
思路1.从右到左遍历数组,如果遇到的元素可以到达最后一个元素,则截断后面的元素,否则继续向前,若最后剩下的元素大于1个,则判断为假,否则为真,时间复杂度为O(n)

class Solution {
    public boolean canJump(int[] nums) {
        int n=1;
        for(int i=nums.length-2;i>=0;i--){
            if(nums[i]>=n){
                n=1;
            }else{
                n++;
            }
            if(i==0&&n>1){
                return false;
            }
        }

        return true;
    }
}

思路2:可达性分析:影响是否可达的关键是数组中值为0的节点
A:如果数组中只包含正整数,则一定可以到达最后一个位置,因为每个节点都可以往后跳,即使一次只跳一次也可以到达最后一个位置。
B:数组中除了正整数,还包含节点值为0的点,此时就需要对0节点进行可达性判断。
若节点值为0,则表示到达该节点,就不能往后跳。
所以为了避开这个节点,就需要从0节点开始向前寻找任意一个可以跳过该节点的节点,如果不存在这样一个节点,说明不管怎么跳,都会落到该0节点上。

public boolean canJump(int[] nums) {
		//两种特殊情况
        //nums长度为1,初始位置就是目标位置,直接返回true
        if(nums.length == 1) {
            return true;
        //如果长度不为1,但第一个位置值为0,返回false
        } else if(nums[0] == 0) {
            return false;
        }
        //从倒数第二个位置开始遍历
        for(int i = nums.length - 2; i > 0; i--) {
			//若值为0,则需要判断前面是否存在可以跳过该节点的结点
            if(nums[i] == 0) {
                for(int j = i; j >= 0; j-- ) {
                    if(nums[j] > (i - j)) {
                        //说明从nums[j]结点可以直接跳到“0”结点之后,那么直接就从nums[j]开始向前继续判断
                        i = j;
                        break;
                    //一直遍历到第一个位置还找不到可以跳过“0”结点的结点,说明不可达
                    } else if(j == 0) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

如何用贪心算法实现霍夫曼编码?
假设我有一个包含1000个字符的文件,每个字符占1个byte(1byte=8bits),存储这1000个字符就一共需要8000bits,那么也没有更节约存储空间的存储方式呢?
假设我们通过统计分析发现,这1000个字符中包含6种不同的字符,假设他们分别是a,b,c,d,e,f。而3个二进制位(bit)就可以表示8个不同的字符,所以,为了节约存储空间,每个字符我们用3个二进制位来表示。那么存储这1000个字符只需要3000bits就可以了,比原来节约很多的空间,但是有没有更节约的存储方式呢?
此时,霍夫曼编码就要出现了,霍夫曼编码是一种十分有效的编码方式,广泛应用于数据压缩中,其压缩率通常在20%~90%之间。霍夫曼编码不仅会考察文本中有多少个不同的字符,还会考察每个字符出现的频率,根据频率的不同,选择不同长度的编码,霍夫曼编码试图用这种不等长的编码方式来进一步增加压缩的效率。根据贪心的思想,可以把出现频率较高的字符用稍微短一些的编码,出现频率较低的字符,用稍微长一些的编码。
因为霍夫曼编码是不等长的,每次应该读取1位还是2位等等来解压缩呢?为了避免在解压缩的过程中的歧义,霍夫曼编码要求各个字符的编码之间,不会出现某个编码是另一个编码前缀的情况。
在霍夫曼编码中如何根据字符出现的频率的不同给不同的字符进行不同长度的编码呢?
我们把每个字符看作一个节点,并且附带着把频率放到优先级队列中。我们从队列中取出频率最小的两个节点A、B,然后新建也该节点C,把频率设置位两个节点的频率之和,并把这个新节点C作为节点A、B的父节点,最后再把C节点放到优先级队列中,重复这个过程,直到队列中没有数据。
数据结构篇--------算法_第3张图片
现在,给每一条边加上一个权值,指向左子节点的边统统标记为0,指向右子节点的边标记为1,那么从根节点到叶子节点的路径就是叶子节点对应字符的霍夫曼编码
数据结构篇--------算法_第4张图片
实际上,贪心算法适用的场景比较有限,这种算法思想更多的是指导设计基础算法,比如最小生成树算法、单源最短路径算法。最难的是如何将要解决的问题抽象成贪心算法模型。
思考:
1.在一个非负整数a中,我们希望从中移除k个数字,让剩下的数字值最小,如何选择移除哪k个数字呢?
2.假设有n个人等待被服务,但是服务窗口只有一个,每个人需要被服务的时间长度是不一样的,如何安排被服务的先后顺序,才能让这n个人总的等待时间最短?
由等待时间最短的先开始服务。

二、回溯算法

  1. 回溯算法的应用:深度优先算法、数独、八皇后、0-1背包、图的着色、旅行商问题、全排列等等。

  2. 回溯的处理思想:有点类似于枚举搜索,枚举所有的解,找到满足期望的解。为了有规律的枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段,每个阶段都会面对一个岔路口,我们随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一条走法继续走

  3. 八皇后问题:我们有一个8*8的棋盘,希望往里放8个棋子(皇后),每个棋子所在的行、列、对角线都不能有另一个棋子。我们把这个问题划分为八个阶段,依次i将8个棋子放到第一行、第二行、第三行…第八行,在放置的过程中,我们不停的检查当前放法,是否满足要求。如果满足,则跳到下一行继续放置棋子;如果不满足,那就再换一种放法,继续尝试。(回溯算法非常适合用递归来实现)

三、动态规划算法(一般解决最优解问题)

  • 动态规划算法:一个模型和三个特征
  • 一个模型:多阶段决策最优解模型
  • 三个特征:最优子结构、无后效性、重复子问题
    (1).最优子结构:最优子结构指的是问题的最优解包含子问题的最优解,反过来说就是,我们可以通过子问题的最优解推导出问题的最优解。
    (2).无后效性:第一层含义是,在推导后面阶段的状态时,我们之关心前面阶段的状态值,不关心这个状态是怎么一步一步推导出来的;第二层含义是,某个阶段状态一旦确定,就不受之后阶段的决策影响。只要满足前面提到的动态规划问题模型,基本上都会满足无后效性。
    (3).重复子问题:不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。
  • 动态规划解题思路总结:有两种思路:状态转移表法和状态转移方程法
    (1 )状态转移表法
    一般你用动态规划解决的问题都可以用回溯算法的暴力搜索解决。所以,当我们拿到问题时,我们可以先用简单的回溯算法解决,然后定义状态,每个状态表示一个节点,然后对应画出递归树,在递归树中我们可以很容易的看出来,是否存在重复子问题,以及重复子问题是如何产生的,由此来看规律,看是否可用动态规划解决。
    找到重复子问题后,有两种办法,一种是直接用回溯加备忘录的方法,来避免重复子问题,从执行效率上来看,这跟动态规划的解决思路没有差别。第二种是使用动态规划的解决办法,状态转移表法 。
    (2). 状态转移方程法

四、分治算法

  1. MapReduce是Google大数据处理的三驾马车之一,另外两个是GFS和Bigtable,他在倒排索引、PageRank计算、网页分析等搜索引擎相关的技术中都有大量的应用。MapReduce的本质就是分治算法。
  2. 分治算法:核心思想就是分而治之。也就是将原问题划分为n个规模较小、并且结构与原问题相似的子问题,递归的解决这些子问题,然后再合并其结果,就得到原问题的解。
    分治算法是一种处理问题的思想,递归是一种编程技巧,实际上分治算法一般都比较适合用递归来实现。
  3. 分治算法可以解决的问题一般需要满足下面几个条件:
  • 原问题与分解成的小问题具有相同的模式;

  • 原问题分解成的子问题可以独立求解,子问题之间没有相关性,这也是分治算法和动态规划算法的明显区别;

  • 具有分解终止条件,即当问题足够小时,可以直接求解;

  • 可以将子问题合并为原问题,而这个合并操作的复杂度不能太高,不然就起不到减小算法总体复杂的效果了。
    4.经典题型:

  • 二维平面上有n个点,如何快速的计算出两个距离最接近的点对?

  • 有两个nn的矩阵A、B,如何快速求解两个矩阵的乘积C=AB?

分治思想在海量数据处理中的应用

分治思想不仅仅应用于指导编程和算法设计,还经常用在海量数据的处理。我们之前讲的数据机构和算法,大部分都是基于内存存储和单机处理,但是当要处理的数据量非常大,没法一次性放到内存中,此时需要分治思想。
比如,给10GB的订单文件按照金额排序这个需求,因为机器的内存有限,无法一次加载到内存,也就无法单纯的通过归并排序、快排等基础算法来解决。
利用分治的思想将海量的数据通过某种方法,划分为几个小的数据集合,每个小的数据集合单独加载到内存来解决,然后再将小数据集合合并为大数据集合。先扫描一遍订单,根据订单的金额,将10GB的文件划分为几个金额区间,比如将订单金额为1到100的放到一个小文件,101到200的放一个小文件,以此类推,每个小文件都能单独加载到内存排序,最后将这些有序的小文件合并就是最终有序的10GB订单数据。
对于谷歌搜索引擎来说,网页爬取、清洗、分析、分词、计算权重、倒排索引等各个环节都会面对如此海量的数据,所以,利用集群并行处理是大势所趋。实际上,Map Reduce框架只是一个任务调度器,底层依赖于GFS来存储数据,依赖Borg中的机器执行,并且时刻监视机器执行的速度,一旦出现机器宕机、进度卡壳等就会重新从Borg中调度一台机器执行。
MapReduce提供了高可靠、高性能、高容错的并行计算框架,并行的处理这几十亿、上百亿的网页。

四种算法思想的比较分析

如果对这四种算法思想进行分类的话,贪心、回溯、动态规划为一类,分治算法为另一类。因为前三个算法解决问题的模型,都可以抽象成多阶段决策最优解模型,而分治算法解决的打本份问题也是最优解问题,但是大部分不能抽象为多决策模型。
基本上能用动态规划、贪心解决的问题都可以用回溯解决。回溯相当于穷举搜索,穷举所有的情况,然后对比得到最优解。但是,回溯的时间复杂度很高,是指数级别的,只能解决小规模数据的问题。
尽管动态规划比回溯算法高效,但是不是所有的问题都可以用动态规划来解决。能用动态规划解决的问题,需满足最优子结构、无后效性、重复子问题三个条件。在重复子问题上,动态规划和分治算法的区别十分明显。分治算法要求分割成的子问题,不能有重复子问题,而动态规划正好相反,动态规划之所以高效,就是因为回溯算法实现中存在大量的重复子问题。
贪心算法实际上是动态规划的一种特殊情况。他更高效,但是能解决的问题有限,它能解决的问题需要满足最优子结构、无后效性、贪心选择性。贪心选择性是指通过局部的最优的选择,能产生全局的最优选择。每个阶段,我们都选择当前看起来最优的决策,所有阶段的决策完成之后,最终由这些局部最优解构成全局最优解。

你可能感兴趣的:(数据结构)