c++递归算法1到n的组合数_小姐姐带你学习算法,不信你还学不会

目录

  • 1. 为什么要学算法

  • 2. 整数乘法问题

    • 2.1 问题描述

    • 2.2 小学算法

    • 2.3 小学算法操作数量分析

    • 2.4 还能做的更好吗

  • 3. Karatsuba乘法

    • 3.1 一个例子

    • 3.2 一种递归算法

    • 3.3 Karatsuba乘法

  • 4. MergeSort算法

    • 4.1 为什么要学MergeSort(Motivation)

    • 4.2 排序问题描述

    • 4.3 一个例子

    • 4.4 伪代码

    • 4.5 Merge子程序伪代码

  • 5. MergeSort算法分析

    • 5.1 Merge的运行时间

    • 5.2 MergeSort运行时间

  • 6. 算法分析的指导原则

    • 6.1 原则1: 最坏情况分析

    • 6.2 原则2: 全局分析

    • 6.3 原则3: 渐进分析

    • 6.4 什么是最快算法

1. 为什么要学算法

eddc1e8c146ceb0dddd9bdbd75c4961a.png

有以下好处:

  • 算法对计算机科学的所有分支都重要: 想要完成实质性工作,理解算法的基础知识并掌握算法密切相关的数据结构知识是必不可少的。
  • 算法是技术革新的推动力: 一个例子是搜索引擎使用一系列算法高效的计算与特定搜索项相关联的各个web页面出现的频率。
  • 算法会对其它学科产生影射。
  • 学习算法有益于思维: 作者还是学生的时候,喜欢上那些具有挑战性的课程,如果他艰苦的征服这些课程,会感到自己智商比刚开始学的时候提高几个点 (我也有这样的感觉)。作者希望也能给读者带来类似的体验。
  • 算法很有趣: 希望学完后给大家带来的感觉是简单而愉快的事情。

2. 整数乘法问题

2.1 问题描述

3e4cb11e6ceea0a6bbde3e6d7bbe06dc.png

乘法问题的描述是,它输入是两个n位数字的整数,分别称为x和y。鼓励把n看作一个非常巨大的数据,几千甚至更大,输出是x和y的乘积。

2.2 小学算法

性能定义

我们通过测量它所执行的基本操作数量来评估这种算法的性能。基本操作看做下列的操作之一:

  • 两个个位数相加
  • 两个个位数相乘
  • 在一个数之前或之后添加一个0

举个例子

13c5e84a454eac2b3919618010db41e5.png

x是5678,y是1234,其中n=4,这两个整数相乘详细过程如上图:

  • 这种算法首先计算第一个数与第二个数最后一个数字的部分乘积:4*5678=227122
  • 计算这个部分乘积的过程中又可以细分为把第一个数字的每一位数字与4相乘,并在必要时产生进位
  • 在计算下一部分乘积5678*3=17034时我们执行相同的操作,并把结果左移一位,相当于在末尾添了一个0
  • 对于剩下的两个部分,乘积也是执行相同的操作
  • 最后把所有四部分乘积相加

2.3 小学算法操作数量分析

我们把4依次与第一个数的5,6,7,8相乘,这样就产生了4个基本操作,由于进位的原因,我们还需要执行一些加法操作。

一般而言,计算一个部分乘积涉及n个乘法(每位数字1个)以及最多n个加法(每位数字最多1个),总共最多就有2n个基本操作。

第一个部分乘积和其他部分乘积相比,并没有任何特别之处,每个部分乘积都最多需要2n个基本操作。

由于一共有n个部分乘积,第二个整数的每一位都会产生一个乘积,因此,所有部分乘积最多需要个基本操作。 我们还需要把所有的部分乘积相加得到最终答案,这个过程中仍需要相当数量的操作,于是该算法的基本操作数量总结如下:

54ed4daca981d2768759ac8afe2ba4a3.png

可以想象,当输入的位数越多时,这种乘法所执行的基本操作数量是急剧增加的,如果输入数的位数增加一倍,需要的基本操作数量是原来的4倍,如果输入的位数是原来的4倍,那么基本操作数量是原来的16倍。

2.4 还能做的更好吗

或许成为优秀算法设计师的最重要原则就是 拒绝满足 ,每个算法设计师都应该坚守下面的信条, 我还能做得更好吗 bb1b6dc2d24446b320131d38f9f9cbe9.png

3. Karatsuba乘法

3.1 一个例子

我们将执行一系列与之前小学算法与众不同的步骤,我们需要领悟一个关键的要点,就是我们可以通过许多眼花缭乱的方法来解决诸如整数乘法这样的计算问题。
c464ebc70492990756b9a06a24dce6e6.png
首先,我们把x的前半部分和后半部分划分开来,并命名它们为a和b,因此,a=56,b=78,类似的c,d分别表示12和34。
ef1e7cf733fb3bed0d6d3ad8bc3ac46d.png
上面是计算过程:
  • step1: 计算,结果是
  • step2:
  • step3:
  • step4: 把步骤3减去前两个步骤结果: 6164-672-2562=2840
  • step5: 计算
这时候你就发现,除了小学所学习的整数相乘算法之外,还存在其他完全不同的算法,那么我们就会想,这个算法会不会比小学算法做得更好呢,除了这种算法,是不是有更加优秀的另一种算法呢?

3.2 一种递归算法

我们先来看看另一个比较简单的整数乘法的递归算法。 一般而言,位数为偶数n的数x,可以表示为两个位的数,他的前半部分a和后半部分b,于是: 于是: 它表示用一种递归方法进行两个整数的相乘,为了计算xy这个乘积,我们对最后一个表达式进行计算,四个相关的乘积(ac,ad,bc,bd)所涉及的位数都是小于n的,所以我们可以用递归的方式计算,当这4个递归调用带着各自的答案返回时,我们就可以简单的计算表达式得值:ac后面加n个0,ad和bc相加,并得出的结果后面加上个零,最终把两个表达式与bd相加。伪代码如下:
bdbbbadc0cc5a3fc853ad90a1e92f1ae.png

3.3 Karatsuba乘法

还能改进吗 上面的算法使用了4个递归调用,我们事实上并不真正的关心ad或bc的值,只是关注他们的和ad+bc,所以我们真正关心的只有三个值: ac, ad+bc,bd,那是不是只用三个递归调用就可以了呢?
51b19010dee1e8ebff402963db4bb201.png
  • step1: 递归计算ac
  • step2: 递归计算bd
  • step3: 计算 a+b, c+d,递归计算 (a+b)*(c+d)
  • step4: 步骤3减去步骤1,步骤2,获得ad+bc的值
  • step5: 步骤1结果后面加n个0,步骤4后面加个0,然后与步骤2的结果相加就得到答案
伪代码如下:
dfc21da95482d10752057ef354722d54.png

4. MergeSort算法

4.1 为什么要学MergeSort(Motivation)

864f3dd1464b46bc31222e3b09ac566d.png
MergeSort是个相对古老的算法了,为什么现在我们还要讨论这么古老的东西呢?有几个原因:
  • 姜还是老的辣:它虽然年龄很大了,但是在实践中一直被沿用,仍然是很多程序库中的标准排序算法之一。

  • 经典的分治算法: 分治算法设计范式是一种通用解决问题的方法,它的基本思路是把原始问题分解为多个更小的子问题,并以递归的方式解决子问题,最终通过组合子问题的解决方案得到原始问题的答案,MergeSort可以作为良好的起点帮助我们理解分治算法,它的优点以及面临的分析挑战。

  • 校准预备条件: 本节对面的MergeSort的讨论,可以让读者明白自己当前的技术水平是否适合上这门课,假设读者已经具有一定的编程和数学背景,能够把MergeSort的高级思路转换为自己喜欢的编程语言,能够看懂我们对算法进行的是运行时间分析,如果读者能够适应本内容,那么对于本书的剩余部分也不会有什么问题。

  • 推动算法分析的指导原则: 本节对MergeSort运行时间的分析展示了一些更加基本的指导原则。

  • 为主方法热身: 我们将使用递归树方法对MergeSort进行分析,这是一种对递归算法所执行的操作进行累加的方法,后面将结合这些思路生成一个“主方法”,主方法是一种功能强大且容易使用的工具,用于界定许多不同的分治算法的运行时间。

4.2 排序问题描述

输入一个数组,数组里面的每个数字是不重复的,输出是已经排序好的数组。
416c64c8e11bfd3617d0fd0597194a1f.png
其它排序方法 可能之前我们有所知道一些排序算法:
  • SelectionSort: 扫描全数组找到最小元素,把它放到输出数组的第一位,接着扫描复制次小的元素,以此类推;
  • InsertionSort: 这是同一个思路的一种更灵巧的实现方法,它把输入数组中的每一个元素依次插入到有序的输出数组中的适当位置;
  • BubbleSort: 对相邻无序的元素进行比较,执行反复的交换,直到最后数组完成排序。
等等,但这些算法的问题就是运行时间是平方级的。那我们来看看今天这个排序算法用更少的运行时间是怎么实现的。

4.3 一个例子

想要理解MergeSort算法是如何运行的,一个最简单的方法就是看看具体的例子。
a53a2bab6ab5a677fb033618c6dbd308.png
整体过程就是: 它把数组 [5, ,4, 1, 8, 7, 2, 6, 3] 划分为更小的数组(子问题)[5, 4, 1, 8] 和 [7, 2, 6, 3]排序,通过神奇的递归操作,得到每个排序后的子数组,再将子数组合并起来。

4.4 伪代码

将上面的图换成伪代码就是这样的过程
4526ee71e814eeadc39dd34ae9fd97a4.png
伪代码省略了一些内容,但需要注意:
  • 作为一种递归算法,它必须有一个或多个基本条件,如果不再有进一步的递归就直接返回答案,因此,如果输入数组a只包含0或1个元素就返回该数组。
  • 这段伪代码并没有详细说明n是奇数时,前半部分和后半部分是怎么划分的,但是这种划分是显而易见的,比如说一半比另一半多一个元素,这种也是可行的。
  • 最后这段伪代码忽略了怎么把两个子数组实际传递给它们各自递归调用的实现细节,这个细节取决于编程语言,高级伪代码的要点就是忽略这些细节,把注意力集中在超越特定编程语言的概念上。

4.5 Merge子程序伪代码

由上面的图我们可以知道,Merge的时候,其实输入两个已经排序好的数组C, D,再把它们排序输出到B。
4d0663c36c682eff290ac941ac989b31.png
索引 k 操控的是输出数组,索引 i,j 操控的是已排序好的C和D子数组,都是从左向右扫描。每一次的for循环,其实就是找C和D中最小的数字,因为C和D是已排序好的数组,所以最小的数字就是i或j对应的元素。比较后把它放入输出数组B中,并将对应的索引+1,这样下次循环就跳过已复制的元素了。所以最后的数组B输出是按序方式生成的。

5. MergeSort算法分析

我们对算法进行运行时间分析,分析的是什么呢?“运行时间”表示算法的一个具体实现所执行的代码的行数,我们可以把它看成是在一个具体的实现中用调试器进行逐行追踪,每次追踪一个“基本操作”,我们所感兴趣的是程序结束之前调试器所执行的步数。

5.1 Merge的运行时间

eb57b05141fb479b042f29791eb84538.png
我们先来对Merge程序算算它的执行操作数量。 第1,2行有一次赋值操作。 第3行是一个for循环,每一个for循环里,有比较操作(C[i]和D(i)比较),赋值操作(B[k]的赋值),递增操作(i或j加1),循环变量k还要加1,所以每一次循环一共有4次操作。 而for循环一共要循环n次。 一共就是4n+2次操作,为了后面的计算方便,当n>=1时,4n+2<6n(去掉常数项), 我们取6n次操作作为Merge程序操作上界。

5.2 MergeSort运行时间

递归算法运行时间分析的难点 我们怎样才能从Merge程序的简明分析转到MergeSort的分析呢?要知道递归算法会产生更多的自身调用,更为可怕的是递归调用数量快速增长,随着递归深度的加深,递归调用的数量以指数级的速度增长,我们必须记住一个事实,传递给每个递归调用的输入又要明显小于上一级递归调用的输入。 相互制衡的竞争因素 一方面是需要解决的子问题的数量呈爆炸性增长,另一方面是这些子问题的输入越来越小,协调好两个竞争因素,有助于我们对MergeSort进行分析。 运行时间计算 为了简单起见,假如输入数组的长度是n的2次方(如果没有这个假设只需要额外工作就能消除这个假设),我们用递归树的方法来分析运行时间的上界,每一个节点就表示一次递归调用。
f137704f6c6ae5d35a49e486ccfa2317.png
最外层是整个原始的输入数组,它在进行MergeSort的时候会有2个递归调用,所以这是一个二叉树(每个节点都有两个子节点),第一层的2个节点就是原始数组的左半部分和右半部分,再次递归最后到达最底层,一个长度为1或0的数组。我们需要知道几个数量: 原始数组长度是n,递归树有多少层? 由于每深入一层,数组长度就缩小一半,第0层是n,第1层是n/2(除了一次2),第2层是n/4(除了2次2),最后一层的数组长度是不大于1的,就是除以了log2(n)次2,所以最后一层是log2(n)层。 (也可以假定n/2^a = 1, 求解a)如果没有n是2的次方这个假设,就向上取整。一共就是log2(n) +1层。 递归树的第j层有多少个节点(子问题)? 因为每一层的递归数量是前一层的两倍,所以第j层就有2^j个子问题。 每个节点的数组长度是多少? 每个节点的长度,总长度是n,均分到每个节点就是n/(2^j)个长度。 所以每一层的工作量: 所以总的运行时间就是: 所以有以下结论:
5740bd346a3690d6801303a6d7b712cb.png
MergSort算法和其他算法运行时间比较 对数表示的含义是: 在计算器中输入n,它将不断地除以2,直到结果小于等于1,于是我们就产生了一个直觉,就是的值要比n要小很多,尤其是当n非常大的时候,如下。
086763a7436721340d24825558a48d58.png
我们刚刚提到的简单排序算法,Selection,InsertSort和BubbleSort的运行时间,它们与输入长度n是平方及的关系,意味着他们所需要的操作数量是稳定的级,如上面的图所示,这意味着MergeSort的运行速度一般要比更简单的排序算法要快很多,尤其是当n非常大的时候。

6. 算法分析的指导原则

现在回过头来明确与运行时间的分析解析有关的三个假设,我们将这三个假设作为合法合理分析算法的指导原则。

如果我们想要进行准确的运行时间分析,只有那些最简单的算法才有可能,在更多情况下,算法都是很复杂的,所以我们需要进行妥协,找到正确的平衡点,可以为数十种基本算法提供良好的运行时间保证。这一保证可以让我们知道哪种算法更为快捷。

6.1 原则1: 最坏情况分析

715f76ee5cb798c0a18abde93f7c1828.png
 什么是最坏情况分析?

我们上面的MergeSort算法里面的运行时间上就是,如果有一个充满恶念的人,他的生活目标就是编造一个恶意的数组,使目的是MergeSort运行尽可能的慢,但是这个上界仍然是被满足的,这种类型的分析称为最坏情况分析,因为它给出了运行时间的上界,即使遇到最坏的输入,上界仍然是有效的。

平均情况分析和基准实例
平均情况分析:是指一种算法的平均运行时间进行分析,它需要对不同的输入的相对频率做出一些假设,比如说排序程序中,假设所有输入数组都是相差不大的,这样就可以研究不同排序算法的平均运行时间。
基准实例: 在一个较少的"基准实例"集合上的性能,这些实力被认为是具有代表性的,可以代表典型现实的世界输入。
情况分析 vs 平均情况分析和基准实例

如果了解了问题领域知识,并理解哪些输入更具有代表性后,平均情况分析和基准实例分析都是非常实用的。最坏情况分析并不需要考虑输入,它更适用于通用目的的子程序设计,目标就是范围很广的应用程序。为了使算法的适用性更广,他们更专注于通用目的子程序,因此一般使用最坏情况来判断算法的性能。

  最坏情况分析的优点

最坏情况分析还有一个额外的优点,相比其他分析,他通常更容易用数学方式实现。

原则2: 全局分析
c3d9c093ef5e8cd21c45c218b7742acc.png
全局分析的含义 : 我们没有必要在考虑运行时间上界时过多的关心较小常数因子和低阶项。
为什么要采用这种粗放的形式:
  • 便于数学处理:  进行全局分析的第一个理由是它比那些需要精确的常数因子和低阶项的方法更容易进行数学处理。

  • 常数因子往往依赖于环境:  第二个理由,它是非常重要的,我们在描述算法的粒度层中很容易被误导,从而过于重视常数因子的准确性,我们的目的是常数因子不可避免的是依赖于具体所用的编程语言,特定实现以及编译器和处理器的细节,我们的目的是把注意力集中在那些与编程语言计算机体系结构细节无关的算法属性上,并且这些属性并不受运行时间上界中的较小常数因子的变化而影响。

  • 预测能力的损失有限:  第三个理由,也是我们决定忽略常数因子的根本原因,可能很多人可能会担心忽略常数因子会导致我们错误判断,误以为自己的算法很快,但实际上使用中速度却慢很多。告诉大家一个愉快的消息,我们忽略了低阶项和常数因子,我们所进行的数学分析定性预测能力仍然是高精度的,当算法分析告诉我们一种算法运行较快的时候,它实际使用中也是比较快速的,反过来也是如此。

全局分析忽略了一些信息,但它保留了我们真正需要的东西,关于哪些算法较之其他算法运算速度更快的指导方针。

原则3: 渐进分析
ee850642d180fe3c25431a1d7e2ca50a.png

渐进性分析,是指把注意力集中在当输入长度n增长时算法的运行时间增长率上。

比如和,如果n<90的时候,是大,当n>90的时候,是大。

0483cbde041b4e23272b7503e9564056.png

我们需要关注大量数据而不是小量数据,因为大量数据才是算法的用武之地。

6.4 什么是最快算法

459d09ed2be6a0e1b17d65846ec5a6f0.png

所以快速算法就是指算法的最坏情况运行时间随着输入长度的增加而较慢增长。

对于第一个知道原则,我们想要保证运行时间并不需要任何领域知识为前提,这也是为什么我们把注意力集中在算法的最坏运行时间的原因。

第二和第三个指导原则表示常数因子往往依赖于编程语言和计算机,并且我们感兴趣的是大型的问题,这是我们把注意力集中在算法的运行时间增长率的原因。

     推荐一下小姐姐的公众号吧,想学算法的朋友,可以关注一下! 4d681e9367088ea8d746d1d1cb621637.png               喜欢本文的小伙伴点【在看】分享给你的朋友?↓↓↓
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值