数据结构与算法应用

  • 7.1 分治法
  • 1、递归
  • 2、二分查找
  • 7.2 回溯法
  • 7.3 贪心法
  • 7.4 动态规划法
  • 7.5 案例分析


  • 前面的数据结构与算法基础主要是针对于上午题,包含数据结构的基本知识和常见基本算法。而这部分内容主要是针对于下午题中的算法难点。主要涉及分治法、回溯法、贪心法和动态规划法这四种较复杂的算法。

7.1 分治法

  • 基本思想:
    分治法就是分而治之的方法,把一个比较复杂的问题拆分成多个规模较小的子问题(降低了解决问题的难度),要求拆分出来的子问题要与原问题的结构相同,拆分出来的子问题又可以用同样的方式再拆分,所以在分治法中往往要用到递归的思想来解决问题。
    分治法先做分解,分解后把一系列小的问题解决掉,再把它们的解一步一步汇合合并得到最终原问题的解。
  • 数据结构 算法 JAVA PDF_数据结构

  • 问题要求
  • 数据结构 算法 JAVA PDF_贪心算法_02

1、递归

  • 递归就是自己调用自己。

    上面就是求斐波那契数列的项递归函数,而上面的树则表示了递归函数的拆解过程,最后整个问题的解就变成了若干个简单值的和。

2、二分查找

  • 二分查找就是利用分治法的思想做的。
  • 数据结构 算法 JAVA PDF_数组_03

  • L是存储数据的线性表,a和b分别表示下标的最小值和最大值,x就是要查找的数。
    查找的时候开始下标会越来越大,结束下标会越来越小,如果开始下标大于结束小标表示数列已经查找完了,return(-1)表示查找失败。
    开始下标不大于结束小标时表示还要继续查找,下标最小值和最大值的平均值就是数列中间元素的下标,如果待查找数与中间元素值与相等则表示查找成功,如果大于则表示待查找的数在中间元素右边的区间(m+1到b的区间),否则就在左边的区间(a到m-1的区间),这里就要用到递归。

7.2 回溯法

  • 基本思想
    是一种深度优先搜索法,如下图中的解空间树,回溯法就是从分支一步一步探下去,探到底不能继续了开始回溯,回到上一层再继续探下去,探到底再回溯。因为它有试探,探到一定程度,此路不通的时候开始回退,这就是回溯,可以解决很多问题,比如说经典的迷宫问题。
    如果在某一个问题的求解过程中,会涉及到要逐步试探下去,走到某个位置又开始回退,那么就可以用回溯法解决问题。

7.3 贪心法

  • 基本思想
    贪心就是贪多贪好,在得到最终的解决方案时定一个原则,这个原色就是每一步都要选到最好的东西,这样下来往往会得到一个不错的结果,但是很难得到最优解。官方的定义就是在花有限的时间得到一个令人满意的解但不一定是最优解,相当于一种性价比方案。最经典的应用就是解决背包问题。
  • 数据结构 算法 JAVA PDF_数组_04

  • 背包问题: 有三个物品分别重20、30、40,价值分别是140、180、200,现在只有一个容量70的背包,我们想让背包里面装尽可能多的有价值的物品,让整个背包的容量达到最大值,价值也最高。
    解决思路: 物品1单位物品价值是70,物品2是60,物品3是50,所以装物品时的贪心原则就是单位物品价值越高的越优先装。那么先装物品1,再装物品2,物品3已经装不下,这个方案就结束了,显然这不是最优解,最优解应该是装一个物品2和物品3,如果要求一个物品能重复装最优解就是两个物品1和一个物品2,所以要注意问题中的具体要求。
    注: 判断算法是否为贪心法时,基本就是看每一步是不是都取最优但最终结果不一定是最优,如果满足这个条件说明就用到了贪心法。

7.4 动态规划法

  • 在这一系列算法中动态规划法是逻辑上面最复杂的一种方法,有很多地方和分治法类似,比如动态规划法也会把原问题分成多个子问题再得到原问题的解。
  • 基本思想
  • 在考试中区分分治法和动态规划法最简单的原则就是动态规划法一般都要用到一种查表的方式来解决问题,因为在动态规划法中分出来的子问题可能不是独立的,它们之间可能会有关联,所以会把这些子问题的解存在表中,再求解更复杂的问题时可以通过查这张表得到后面的解。
  • 比如上图中的数列,用动态规划法会把前面的这些都求出来并存在一个数组里,如果再求后面的一项就查前面的两个值然后得到后面的值。所以动态规划法的程序中大量的代码是构造这一个表,然后输出结果时才查表得到相应的值。

7.5 案例分析

  • 考试中一般是给程序填空,问是哪种算法策略,求时间和空间复杂度等。做题时要把整个题目浏览一遍,看哪些问题可以先解决,哪些问题可以后解决,其中程序填空是最复杂的可以放到最后面分析。
  • 例题1
  • 数据结构 算法 JAVA PDF_算法_05


  • 数据结构 算法 JAVA PDF_数组_06


  • 数据结构 算法 JAVA PDF_贪心算法_07


  • 数据结构 算法 JAVA PDF_数组_08

  • 问题3题解:
  • 数据结构 算法 JAVA PDF_数据结构_09

  • 最先策略:
    第一个货物的体积为4,首先用第一个箱子把第一个货物装进来;
    第二个货物体积为2,第一个箱子已经装了4还能装6,所以把第二个货物装进来;
    第三个货物体积为7,第一个箱子已经装了6还能装4,所以第一个箱子装不下要用第二个箱子装;
    第四个货物体积为3,第一个箱子还能装4所以把第四个货物装进来;
    第五个货物体积为5,第一个箱子还剩1,第二个箱子还剩3,所以一二两个箱子都装不下要用第三个箱子装;
    第六个货物体积为4,第一个箱子还剩1,第二个箱子还剩3,第三个箱子还剩5,所以只能用第三个箱子装;
    第七个货物体积为2,第一个箱子还剩1,第二个箱子还剩3,所以第二个箱子能装下。
    以此类推可以得到共需要5个箱子。
    最优策略:
    第一个货物装到第一个箱子;
    第二个货物体积为2,第一个箱子还能装6,如果再开一个箱子能装10,所以第一个箱子的剩余容量最小且能装下就装到第一个箱子中;
    第三个货物体积为7,第一个箱子还剩4,所以装不下要用第二个箱子装;
    第四个货物体积为3,第一个箱子还剩4,第二个箱子还剩3,所以第二个箱子剩余容量最小且能装下就装到第二个箱子中;
    第五个货物体积为4,第一个箱子剩4,第二个箱子满了,所以装到第一个箱子最合适;
    第六个货物体积为5,一二两个箱子都满了,所以要用第三个箱子装;
    第七个货物体积为2,一二两个箱子都满了,第三个箱子剩5,所以装到第三个箱子最合适。
    以此类推可得到共需要4个箱子。
    这两种方法都不能得到最优解,最先策略是5个,最优策略是4个,所以最先策略肯定不是最优解;而这两种方法都是贪心法,每一步局部最优,最终整体不一定最优。
    答案:(9)5,(10)4,(11)否。
    问题2题解:
    最优策略每一步最优是典型的贪心法;最先策略实际也是贪心法,贪心法的原则是用最小的代价找到一个能装下的位置,也算是贪心法的基本策略。
    时间复杂度:主要看程序中的循环语句,一般如果不是循环语句就是O(1),是循环语句就看循环次数和嵌套层数(一般的双层循环就是O(n2)),整个程序的时间复杂度取数量级即阶数最高的。
    答案:(5)和(6)都是贪心,(7)和(8)都是O(n2)。
    问题1题解:
    (1)(2)空是最先策略:
    (1)处代码的下面的循环while(C-b[j]<s[j]){ j++; }中C表示一个箱子的容量,b[j]表示第j+1个箱子当前已经装入货物的体积,s[i]表示第i+1个货物的体积,C-b[j]表示第j+1个箱子剩余容量,所以这个循环的实际意思是当前的箱子装不下这一个货物就看下一个箱子;那么外层循环for(i=0; i<n; i++)实际意思就是把第i+1个货物装到哪个箱子,每一次外层循环就完成一个货物的装入。装每一个货物的时候都会从第一个箱子开始尝试,所以此处的代码应该是j=0。
    (2)处代码下面的k=k>(j+1)?k:(j+1),上面的while循环每次出来的j+1是表示这个货物装在第j+1个箱子中,而这个k则是记录下出现的最大的箱子号即共需箱子数,在确定了这个货物应该装在哪个箱子后,没有把货物真正装到箱子中,实际就是把箱子已有的容量加上要装货物的容量即修改的b[j]的值,所以此处代码应该是b[j]=b[j]+s[i]。
    (3)(4)是最优策略:
    (3)处代码上面的第一层循环for(i=0; i<n; i++)就是把第i+1个货物放到合适的箱子中;min是当前箱子剩余容量最小值,min=C,是针对于第一个货物装到第一个箱子的情况,剩余容量最小就是箱子的容量;m=k+1实际就是又多分配了一个箱子;第二层循环for(j=0; j<k+1; j++)中,temp=C-b[j]-s[i]记录每个箱子如果装入当前货物后剩余容量的临时值,后面再判断更新剩余容量最小值min,m=j就是记录当前所有箱子中能装下当前货物且剩余容量最小的箱子(m是下标,实际箱子号是m+1),这层循环实际就是在当前用到的所有的箱子中找能装下当前货物且剩余容量最小的箱子。所以此处代码应该是min=temp。
    (4)此处和最先策略中的第(2)处是相同的,也是把货物装到找到的合适的箱子中去,即下标为m的箱子,所以此处代码应该是b[m]=b[m]+s[i]。
    答案: (1)j = 0,(2)b[j] = b[j] + s[i],(3)min = temp,(4)b[m] =b[m] + s[i]。
  • 例题2
  • 数据结构 算法 JAVA PDF_数据结构 算法 JAVA PDF_10


  • 数据结构 算法 JAVA PDF_算法_11


  • 数据结构 算法 JAVA PDF_数据结构_12


  • 数据结构 算法 JAVA PDF_数据结构_13

  • 问题2题解:
    根据题目说明归并排序中把数组分成两个子数组排序,子数组又再分最后合并得到排序结果,这就是把一个大问题分成同类型的小问题,再对每一个小问题又拆分排序,其实就是分治法的策略。
    时间复杂度的递归式:程序中把数组分成两个子数组再进行递归的归并排序最后又合并,其实是把一个问题拆分成了两个规模为1/2的问题,假设当前问题的时间是T(n),那么两个子问题的时间为T(n/2),所以总的时间2T(n/2),然后归并操作的时间复杂度就看对应的程序merge函数中,只有单层循环所以时间复杂度为O(n),所以递归式就是T(n)=2T(n/2)+O(n),解出的时间复杂度为O(nlogn)(解的过程可参考原文)。
    空间复杂度:因为进行归并操作时额外需要一个数组来存归并后的新数列,所以空间的复杂度为O(n)。
    答案:(5)分治,(6)T(n)=2T(n/2)+O(n),(7)O(nlogn),(8)O(n)。
    问题1题解:
    (1)处前面的代码都是分子数组,在(1)处的循环,就是把left数组的第一个元素和right数组的第一个元素比较,把小的存到目标数组第一个位置,再把left数组的第二个元素和right数组的第一个元素比较,把小的存到目标数组第二个位置,直到两个数组的所有元素都互相比较完,待排序数组的最小下标是p,最大下标是r,那么就是从p循环到r,所以此处代码是k<=r。
    (2)处如果left大于right对应的元素,right对应的元素较小就存到目标数组中,所以此处代码就是arr[k]=right[j]。
    (3)归并排序中的递归部分,开始就要比较开始值和结束值,如果开始值还小于结束值表示还可以继续往下分,否则表示已经拆解到最底下只有单元素,就要开始进行回归合并的操作了,所以此处代码就是begin<end。
    (4)在(3)处判断之后有必要继续拆分下去就求出中间值,然后分别对两个子数组再进行归并排序,mergeSort(arr, begin, mid)是对前半部分数组排序,还缺少对后半部分数组排序,所以此处的代码就是mergeSort(arr, mid+1, end)。
    答案:
    (1)k <= r
    (2)arr[k] = right[j]
    (3)begin < end
    (4)mergeSort(arr, mid+1, end)。
    问题3题解:
    长为n1的数组和长为n2的数组每次比较都会得到一个较小的元素放到目标数组中,那么最终目标数组的长度为n1+n2,所以比较次数也就是n1+n2。
    疑惑猜想: 实际中比较应该是n1+n2-1次,但是题目中是根据上述代码求比较次数,所以在程序中肯定是比较了n1+n2次。
    答案:(9)n1+n2。