计算机科学及编程导论(麻省理工)

计算机科学及编程导论(麻省理工)

5 (18人评价)
  • 课时:(24)

  • 学员:(660)

  • 浏览:(26210)

  • 加入课程
  • 课程目标,数据类型,运算,变量的笔记

    【技能】1、计算思维2、理解代码3、计算的基本能力和局限3、提炼问题的描述,并在计算机中实现【问问题】问一个通过查找材料就可以解决的问题是没有意义的,被提倡的是问一些复杂性的问题【计算相关】陈述性知识:能做什么程序性知识:怎么做如何通过建立计算机程序去捕捉一系列的计算?如何在一种语言的模式中设计东西?一种语言的不同维度?- 高级 低级- 面向的对象(局部 较广)- 编译 解释得到问题->分解问题->编码区分语法和语义static语义 说明哪些程序有意义full语义  运行时会发生什么形成良好的编程风格  

  • 分支,条件和循环的笔记

    数字和字符串   数值和种类 

  • 课程目标,数据类型,运算,变量的笔记

    记笔记有益于学习,锻炼左右脑。像计算机科学家一样思考!计算机和计算机思维的不同。知识的分类:陈述型知识和过程性知识 stored-promgram computer 编程得有自己的风格

  • 动态规划,重叠的子问题,最优子结构的笔记

    Lecture 13: Dynamic programming: overlapping subproblems, optimal substrcture 本课重点讲述了动态规划,介绍了其核心思想,即默记法。本课前半部分用斐波纳契数的例子详细介绍默记法的使用方法。后半部分介绍了决策树和用它解决0/1背包问题的两个程序样例。 动态规划(Dynamic programming),根据百度百科所述,是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。在20世纪50年代初,美国数学家R.E.Bellman(贝尔曼)等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。动态规划最关键的地方在于,存在一种子问题和最理想子结构重叠的情况。这个说法非常抽象,但简单来说就是如果子问题的最优解被重复使用的话,我们就可以使用动态规划。下面就举一个例子来详细解释:在Lecture 4的时候,有一个例子斐波那契数Fibonacci numbers,当时有如下程序:def Fib(n):    if n == 1 or n == 0:        return 1    else:        return Fib(n - 1) + Fib(n - 2)那么,如果要计算Fib(5)的结果,我们会用到Fib(4)和Fib(3)的结果,而Fib(4)又用到了Fib(3)和Fib(2)的结果,依此类推。我们会发现,Fib(3),Fib(2)的结果被多次调用,而我们又需要花时间去计算Fib(3)和Fib(2),那么这就造成了累赘计算。相信到这里,大家都会这样觉得,为什么不把这些结果存下来,这样以后用到的时候就可以直接使用了呢?没错,这就是我们所说的默记法。 默记法(Memorization)是动态规划的核心内容。具体方法如下:我们在第一次计算的时候就记录一个值然后再后续的问题中使用这个值这个概念非常简单,容易理解。那么对于上面的斐波那契的例子,我们就可以运用默记法,标程如下:def FastFib(n, memo):    if not n in memo:        memo[n] = FastFib(n - 1, memo) + FastFib(n - 2, memo)    return memo[n] def FastFibonacci(n):    memo = {0:1, 1:1} # initialize the memo    return FastFib(n, memo) 本课的后半部分则讲了0/1背包问题的两个解法,下面例1是普通解法,只是用了简单的决策树,而例2则用了动态规划,优化了例1。两个例子如下:例1:普通解法def maxVal(w, v, i, aW):    if i == 0:    if w[i] <= aW:        return v[i]    else:        return 0    without_i = maxVal(w, v, i-1, aW)    if w[i] > aW:        return without_i    else:        with_i = v[i] + maxVal(w, v, i-1, aW - w[i])    return max(with_i, without_i) 例2:动态规划def fastMaxVal(w, v, i, aW, m):    try:        return m[(i, aW)] # to check whether the thing is in the memo or not    except KeyError:        if i == 0:        if w[i] <= aW:            m[(i, aW)] = v[i]            return v[i]        else:            m[(i, aW)] = 0            return 0        without_i = fastMaxVal(w, v, i-1, aW, m)        if w[i] > aW:            m[(i, aW)] = without_i            return without_i        else:            with_i = v[i] + fastMaxVal(w, v, i-1, aW - w[i], m)        res = max(with_i, without_i)        m[(i, aW)] = res        return res def MaxVal0(w, v, i, aW):    m = {}    return fastMaxVal(w, v, i, aW, m)这个程序的算法复杂度为O(ns),n为可选择物品的总数量,s为背包可以装的物品数量。这个算法不仅需要O(ns)的时间,也需要O(ns)的空间。可以说,这也是一个用空间换时间的算法。 那么我们再来从复杂度的方面考虑动态规划。根据百度资料,能用动态规划解决的问题,肯定能用搜索解决。但是搜素时间复杂度太高了,怎么优化呢?我们就想到了记忆化搜索,也就是默记法,就是搜完某个解之后把它保存起来,下一次搜到这个地方的时候,调用上一次的搜索出来的结果。这样就解决了处理重复状态的问题。动态规划之所以速度快是因为解决了重复处理某个状态的问题。记忆化搜索是动态规划的一种实现方法。搜索到i状态,首先确定要解决i首先要解决什么状态。那么那些状态必然可以转移给i状态。于是我们就确定了状态转移方程。然后我们需要确定边界条件。将边界条件赋予初值。此时就可以从前往后枚举状态进行状态转移拉。(以上三段均来自于百度:http://zhidao.baidu.com/question/407709820.html) 总结:详细介绍了动态规划和其核心内容默记法,非常有用,初学者必看!

  • 调试的更多内容:背包问题,动态规划简介的笔记

    Lecture 12: More about debugging, knapsack problem, introduction to dynamic programming 本课主要分为三个部分,第一部分介绍了更多在调试时候的注意点;第二部分例举了多个经典优化问题;而第三部分则简要介绍了动态规划。 第一部分提出了许多在调试时应该注意的地方,下面大概有三个重要点,将逐个讲明。第一个要点是在调试的时候,各位可以注意程序是否出现了以下错误(有些可能非常简单,但仍需要小心谨慎):自变量顺序错误(Reversed order of arguments);拼写问题(Spell mistakes);忘记初始化(Initialization),注意在使用循环时,初始化应该在循环内部还是外部;对象与值对等(Objects vs. value equality);注意别名混淆(Aliasing),例如列表的深复制和浅复制;副作用(Side-effects),注意程序是否会产生不需要的操作而影响结果。第二个要点,是在平时写程序并调试的时候,要注意养成时时记录自己错误的习惯,总结成一个个人犯错模型(Personal model of mistakes)。那么在调试的时候,就可以首先查找是否又出现了以前犯过的错误。第三个要点是在调试的时候,要记录下已经尝试过的修改。不是所有修改都会一次成功,而且有时候调试和修改会花很长时候。因此,这些记录就可以避免重复进行同样的无效修改。另外,由于修改可能会导致程序变得更加糟糕,确保程序可以回转则非常重要。因此,要保存原本的未修改程序和每个修改过的版本(可命名为V1.0, V1.1, V2.1等等) 第二部分则提出了新的问题类型,即最优化问题(Optimization problem)。这些问题都有以下两个显著的特点:包括取最大值或最小值的函数必须满足一定的约束条件最优化问题包括以下五个非常经典的子类型:最短路径问题(Shortest Path Problem)旅行商人问题(Travelling Saleperson Problem)装箱问题(Bin Packing Problem)调整问题(Sequence Alignment Problem)背包问题(Knapsack Problem)大多数新的优化问题可以简化为以上五个子类型,而运用这些已知问题的解法可以解决新的问题。 本课重点讲述的是第五种题型,即背包问题。背包问题又分为两种类型,一是连续背包问题,二是0/1背包问题。背包问题类型1:连续背包问题例:假设一个贼闯入一户人家,他有一个背包可以装10kg的东西。屋里有以下值钱的东西:4kg金沙3kg银沙10kg葡萄干那么我们就来系统化一下这类问题:① 创建一个函数,要求这个函数的最大值或最小值。在这个例子中,就是求背包里面装的东西的最大价值。我们就可以由函数:f = 金沙的价值 x 金沙的重量 + 银沙的价值 x 银沙的重量 + 葡萄干的价值 + 葡萄干的重量。求这个函数f的最大值。② 考虑约束条件。在这个例子中,金沙,银沙和葡萄干的重量总和 <= 10kg这个问题的解法非常简单:我们先取4kg金沙,3kg银沙,再取3kg葡萄干。这就是一个简单的贪心算法(Greedy algorithm),即每一步都是最优解的算法。然而局部最优策略并不代表全局最优策略,我们可以另一种背包问题,即0/1背包问题。 背包问题类型2:0/1背包问题(基本上这是不连续背包问题)例:假设这个小偷的背包仍然只能装10kg的东西,但是他闯入的屋主家只有如下物品:一个手表(重1kg,价值10)一个收音机(重3kg,价值5)一个花瓶(重5kg,价值7)一幅油画(重8kg,价值9)我们也来系统化这个问题:① 我们有n件物品,但每一次只能选择拿整件物品或者不拿这件物品;② 每个物品都有重量和价值,我们要找到最优解,即最大值。那么,我们会有如下两种方法去解决这个问题:贪心算法(Greedy algorithm)穷举法(Brute-force algorithm)贪心算法前面已经提到过了,在每一步都取最优解,即取价值最高的物品。所以就是拿一个手表,再拿一幅油画,所以总价值为10 + 9 = 19。这样的算法即快速又简单,但并不是最优解。最优解是什么?其实就是一个手表,一个收音机加一个花瓶,总价值为10 + 5 + 7 = 22。这个用穷举法也可以做,但是非常慢,在这里一共有2^4 = 16种可能性。但当输入值n很大的时候,就有2^n种可能性,当n足够大的时候,比如n=50,穷举法就需要花很长很长的时候去算出最优解。那么,这时我们为了提高算法效率,就可以运用动态规划。 什么是动态规划?这就是第三部分讲的内容。由于动态规划是下一节课的内容重点,我就会在下一个笔记中详细讲述,并且写出0/1背包问题的两种解法。 总结:介绍最优化问题系列,重点讲述背包问题,非常重要,初学者必看!

  • 测试与调试的笔记

    Lecture 11: Testing and debugging 本节课主要讲了两个内容,正如标题所述,测试和调试。测试和调试多有不同之处,而本课的偏重点在于基本概念,或者说理论基础。因此,本课并没有举出程序例子。 测试和调试是两种不同的过程。测试(Testing)是比较多组输入值和输出值,看是否符合题目要求或规格说明书(Specification)。而调试(Debugging)则是找出程序中的错误并加以改正的过程。测试有两种不同的分类,如下:单元测试(Unit Test):逐一验证各个单独的程序段,如函数(function)和类(class)集成测试(Integration Test):验证整个程序调试的内容大概分为以下两个方面:方法(Function):解题的正确性性能(Performance):运算效率和速度一个比较常用和有效的调试方法是用二分法搜索出错点,具体操作如下:在程序中间位置输入print statement,将程序分为上下两部分;如果输出值和预计值不符,则错误在程序上半部分;相符则在下半部分;再将剩下的程序二分,重复操作第二步。 在上节课的最后提到了异常处理,上一篇笔记并没有特别详细说明,因为课上讲的内容并不是很完整。在参考《王纯业Python学习笔记》和《Python基础教程》第二版之后,整理出如下两个try-except的使用方式: 使用方式①(在上篇笔记中提到的格式):    try:        statement 1    except ExceptionType:        statement 2这个使用方式的意思是这样的:首先执行statement 1,如果没有exception也就是异常发生,那么就一切正常,跳过except继续运行程序;如果发生了exception就比较ExceptionType,如果一致就执行statement 2;不一致就直接返回,把exception抛给函数的调用者,判断是否和这一级的ExceptionType一致,如此一级一级向上抛,直到有try语句捕获了exception;若没有任何try语句可以捕获exception,那么程序就异常退出,程序崩溃。 使用方式②(较为复杂的使用方式):    try:        statement 1    except (ExceptionType1, ExceptionType2):        statement 2    except(ExceptionType3, ExceptionType4):        statement 3    except:        statement 4不难看出,这个使用方式相比于前一个更为高级,也更为细致。在同一级别的比较中,不同的ExceptionType可以做不同的操作。若是exception与所述的ExceptionType 1-4都不符合也有except语句去执行statement 4。这个结构就跟if-elif-elif-else的结构相似,但作用不同。 总结:这是一堂调试基础课,可以略过。

  • 分治法,合并排序,异常的笔记

    Lecture 10: Divide and conquer methods, merge sort, exceptions 本课前半部分由二分法引申到分治法,再应用分治法到排序之中并以归并排序为例。课程后半部分提出了哈希算法,一种以空间换速度的经典算法。最后简要介绍了异常处理的except语法。 根据《算法导论》第2章算法入门所述,有很多算法在结构上是递归的:为了解决一个给定的问题,算法要一次或多次地递归调用其自身来解决相关的子问题。这些算法通常采用分治策略(Divide and conquer method):将原问题划分成n个规模较小而结构与原问题相似的子问题;递归地解决这些子问题,然后再合并其结果,就得到原问题的解。分治模式在每一层递归上都有三个步骤:分解(Divide):将原问题分解成一系列子问题;解决(Conquer):递归地解各子问题。若子问题足够小,则直接求解;合并(Combine):将子问题的结果合并成原问题的解。(注意:以上对于分治法的解释和定义均来源于《算法导论》)在本课程中,对于分治法的定义和解释基本与算法导论中的说法相同,因此便不再累述。 对于归并排序(Merge sort),则完全依照了分治模式,即上述模式,直观地进行如下操作:分解:将n个元素分成各含n/2个元素的子序列;解决:用归并排序法对两个子序列递归地排序;合并:合并两个已排序的子序列已得到排序结果。归并排序的思路非常简单,试想我们有两叠已经排好的扑克牌,假设每叠有4张。第一叠:[1, 2, 4, 8]第二叠:[3, 5, 6, 7]那么我们是怎么理牌的呢?假设两叠拍都是最小的牌朝上,那么第一张牌是1,因为1<3,第二张牌是2,因为2<3,第三张牌是3,因为3<4,依次类推。根据这样的思路,我们可以有如下归并排序的Python标程:import sysdef Merge(nums, first, middle, last):""" This function is used to merge two ordered lists """    n1 = middle - first + 1    n2 = last - middle    lnums = nums[first:middle+1]    rnums = nums[middle+1:last+1]    lnums.append(sys.maxint)    rnums.append(sys.maxint)    p = 0    q = 0    for i in range(first, last+1):        if lnums[p] < rnums[q]:        nums[i] = lnums[p]        p += 1    else:        nums[i] = rnums[q]        q += 1(注意:其中lnums.append(sys.maxint)和rnums.append(sys.maxint)的思想在于:有可能两叠牌的数量是不一样的,一叠有3张,一叠有4张,那3张的排完了,4张的怎么办?这时候,4张的那叠剩下的牌就和3张的那叠最后一张哨兵牌相比。哨兵牌是在每叠牌最后放上的一个足够大的数,例如无限。这样就可以确保所有牌都有效归序)然而,Merge这个函数只解决了一个子问题,即合并两个以排好的序列,那么还有前两步,即分解和解决。因此,我们可以写出一个递归函数,Python标程如下:def MergeSort(nums, first, last):""" This function is used to sort unordered lists by merge sorting """    middle = 0    if first < last:        middle = (first + last) / 2        MergeSort(nums, first, middle)        MergeSort(nums, middle + 1, last)        Merge(nums, first, middle, last)    return nums那么将两个函数放到一起,我们就可以用归并排序的方法去排列一个无序数组了。归并排序的算法复杂度要比上节课所提到的冒泡排序和选择排序低得多,即O(n logn)。可以说,归并排序的效率大大提高了。 本课后半部分提到了哈希算法(Hashing Algorithm),这是一个用空间(即计算机内存)换时间(即计算速度)的算法。下面有两个例子:例1:储存一组正整数,搜索某一数是否存在,存在为真(True),不存在为假(False)def create(smallest, largest):    intSet = [None] * (largest + 1)    return intSet def insert(intSet, e):    intSet[e] = 1 def search(intSet, e):    return intSet[e] == 1 例2:储存字符,并搜索。def hashChar(c):    # c is a character    # function returns a different integer in the range 0 - 255 for each possible value of c    return ord(c) def cSetCreate():    cSet = []    for i in range(256):        cSet.append(None)    return cSet def cSetInsert(cSet, e):    cSet[hashChar(e)] = 1 def cSetSearch(cSet, e):    return cSet[hashChar(e)] == 1 根据上面的两个例子,不难看出,哈希算法的思路在于:创键一个很长的序列,每一个位置代表一个数,比如数列的第一个数代表0,第二个数代表1,以此类推。如果数0储存进数列的话,则在数0,即数列第一个的位置做上记号(这里是赋值为1,当然也可以赋值为真)搜索时只需要查看在所对应位置上的值是否被标注过记号,即是否为1,那么就可以知道是否存在这个数了不难想到,这个算法的复杂度为常数,即O(c),c为常数。那么这个算法的速度是非常快的,而且无论输入值的大小如何,运算速度都是一样的。但是这时,也不难看出这种算法有一个致命的缺陷:如果输入值非常大,此算法会占据大量的存储空间。因此,空间与时间,这种权衡就由程序设计者考虑了。 最后一个话题为异常处理,即except的用法。这里最主要的是try-except的格式,如下:    try:        block of code    except:        block of code具体关于异常处理的系列会在下节课中详细说明,也可以参考Python学习笔记,或者Python基础教程(第二版),都非常有用。 总结:本课主讲分治法和哈希法,都很经典有效,建议听讲。

  • 二分法搜索,冒泡排序与选择排序的笔记

    Lecture 9: Binary search, bubble and selection sorts 本课前半部分重温了上节课略过的二分法搜索,后半部分开始讲排序问题,并举了两个最经典但是最没有效率的两种排序,冒泡排序和选择排序。 鉴于上节课最后已经提到过了二分法搜索,那么这里就不再累述具体细节,标程也可以从上节课的笔记中找到。但是这里必须要提到的,是二分法的通用模板(Template/generalizing binary search),这个模板是一个简单的分治法(Divide and conquer method),在下节课会讲到。那么,二分法的模板如下:找到数据的中点检查是否是符合要求的答案如果不是,那么就将问题规模缩小,重复相同的操作(注意:运用此模板可以讲问题规模以常数倍逐次缩小,从而大大提高运算效率) 本课后半节讲到了排序,并举了排序中最经典,最简单,但也是效率最低的两种排序——选择排序和冒泡排序。可以说,这两种排序的思路有类似之处,复杂度都为二次方级,即O(n^2)。那么下面我就会给出这两种排序的标程,当然初学者也可以自己尝试:例1:选择排序def selectSort(L):    for i in range(len(L)-1):         minIndex = i         minVal = L[i]         j = i + 1         while j < len(L):              if minVal > L[j]:              minIndex = j              minVal = L[j]              j = j + 1         temp = L[i]         L[i] = L[minIndex]         L[minIndex] = temp    return L选择排序的算法思想是这样的:取minIndex表示最小值的元素号码,取minVal表示最小值的值。每次循环中找出最小值存入minVal,其元素号码存入minIndex,然后交换数据。第一次循环找出最小的数,第二次找出第二小的数,依此类推。 例2.1:冒泡排序def bubbleSort1(L):     l = len(L) - 2     i = 0     while i < l:           j = l           while j >= i:                  if L[j + 1] < L[j]:                  L[j], L[j + 1] = L[j + 1], L[j]                  j -= 1           i += 1     return L冒泡排序的算法思想是这样的:每次从最后开始往前滚,邻接元素两两相比,小元素交换到前面。第一轮循环把最小的元素上浮至第一位置,第二小的元素上浮至第二位置,依此类推。 例2.2:冒泡排序(优化)def bubbleSort2(L):     sort = True     while sort:            sort = False            for i in range(len(L) - 1):                    if L[i] > L[i + 1]:                            L[i + 1], L[i] = L[i], L[i + 1]                            sort = True     return L这里优化之处在于:当for循环中没有交换时,列表则被视为已经排好,则不再进行多余的操作。 在本课中还需要提到的一个知识点是循环不变量(Loop Invariant),这表示在循环结构中每次循环都为真的属性。在这些排序中,循环不变量是这样的:列表被分为两部分:前半部分已经被排好,后半部分则是未排好的每次循环被排好的部分不变,但是规模+1,直到后半部分什么都不存在,而整个列表都被排好(注意:对于循环不变量的具体证明,各位可以参考《算法导论》第二章算法入门,有详细解释) 总结:这里介绍的两种排序都不常用,因为效率太低,但是对于初学者来说,这是排序入门的两个例子。

  • 算法的复杂度:对数级,线性级,平方级,指数级的笔记

    Lecture 8: Complexity; log, linear, quadratic, exponential algorithm 本课通过大量例子展示了不同的算法复杂度,包括对数级,线性级,二次方级和指数级。本课所讲的算法复杂度并未涉及高深的数学知识,因此并不需要强大的数学基础。不同于本课程,MIT的“算法导论”课程需要非常强大的数学理论基础。 此笔记详尽包括了本课展示的四个例子七个程序段。那么下面就开始第一个例子。 例1:仅用加减乘数四则运算来计算a的b次方,即a**b第一个方法用了普通while循环,非常简单,程序段1.1如下:def exp1(a, b):    ans = 1    while b > 0:        ans *= a        b -= 1    return ans在这个函数的while循环中,一共做了3b次操作,加上while循环之外的操作,总操作数可表达为 T(b) = 3b + 2        (1) 第二个方法用了递归算法,思路也非常简单,如下:a**b = a*(a**(b-1))a**1 = a    (base-case)那么这个程序就可以这样实现,程序段1.2如下:def exp2(a, b):    if b == 1:        return a    else:        return a * exp2(a, b-1)在这个递归函数中,我们其实运用了这样一个递归式:T(b) = 3 + T(b-1),如果我们把T(b-1)也表达成同样的形式,即T(b-1) = 3 + T(b-2),那么我们可以得出:T(b) = 3k + T(b-k)。由此可得,当b-k = 1时,总操作数T(b) = 3b - 2        (2) 将(1)与(2)比较,我们发现函数exp2比函数exp1少做了4次操作。这个差别并不是特别显著,说明性能改进也不是特别明显。那么我们就有了更优化的程序,程序段1.3如下:def exp3(a, b):    if b == 1:        return a    if b % 2 == 0 and b != 0:        return exp3(a*a, b/2)    else:        return a * exp3(a, b-1)在这个函数exp3中,考虑了指数b的奇偶性,简要思路如下:当b为偶数时:a**b = (a*a)**(b/2)当b为奇数时:a**b = a*(a**(b-1))那么根据这个思路,我们可以写出如下递归式:当b为偶数时:T(b) = 6 + T(b/2)当b为奇数时:T(b) = 6 + T(b-1)总体来说:T(b) = 12 + T((b-1)/2)那么,我们可以简化成 T(b) = 12 + T(b/2) = 12k + T(b/(2**k)),所以当2**k = b时, k = log2(b)。由此可得,T(b) = 12log2(b) + 1        (3)将(1),(2)和(3)进行比较,通过分析T(b)的函数图像,我们会发现:当b足够大时,log2(b)的增长率比3b的增长率小很多。 这种增长率(Rate of growth as input size grows)可以用渐近记号(Asymptotic notation)来表示。这里常用的是Big O Notation,即O()。它表示函数的上界,即当输入值足够大的时候,这个函数的增长值不会超过上界(upper limit to the growth of function as input get large)。简单举个例子:f(x) = O(n) 的意思就是 当x足够大的时候,0<= f(x) <= cn,其中c为常数。这是一个渐进分析的表示方法,具体定义可以参考《算法导论》。那么了解了O记号之后,我们就可以将程序段(1),(2),(3)的算法复杂度表示出来了:(1) = O(n)(2) = O(n)(3) = O(log n)由此可得,程序段(3)的算法复杂度远远低于前面两种算法。 那么在见到了线性级算法(O(n))和对数级算法(O(logn))之后,下面两个例子则是二次方级算法和指数级算法。例2:这是一个只用加法计算a*b的算法,复杂度为O(n^2)def g(a, b):    x = 0    for i in range(a):         for j in range(b):             x += 1    return x 例3:这是一个解汉诺塔游戏的算法,复杂度为O(2^n)。(根据百度百科,汉诺塔问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。)def Towers(size, fromStack, toStack, spareStack):    if size == 1:        print "move disk from ", fromStack, " to ", toStack    else:        Tower(size - 1, fromStack, spareStack, toStack)        Tower(1, fromStack, toStack, spareStack)        Tower(size - 1, spareStack, toStack, fromStack)具体证明过程就不累述了,可以参考百度百科里面汉诺塔的递归算法。 现在,我们来比较一下不同算法的运算速度:假设输入值大小为n = 1000位对数级(Logarithm)要花10纳秒(nanosecs)线性级(Linear)要花1微秒(microsecs)二次方级(Quadratic)要花1厘秒(millisecs)指数级(Exponential)要花10^284年!更直观地比较前三个:对数级花10厘秒线性级花1秒二次方级要花16分钟因此,我们可以通过算法复杂度看出算法的性能和运算效率。可以说,在输入数据很大时,要尽可能避免指数级算法。而对数级算法在输入数据很大情况下,可以大大提高运算效率,因此在设计算法时要尽可能向这里靠拢。 本课后半部分介绍了搜索(search)的算法。这里主要举了普通搜索和二分搜索两个例子。例1:普通搜索,复杂度为O(n)def search(s, e):    answer = None    i = 0    numCompare = 0    while i < len(s)  and answer == None:        numCompare += 1        if e == s[i]:             answer = True        elif e < s[i]:             answer = False        i += 1    return answer 例2:二分搜索,复杂度为O(logn)def bsearch(s, e, first, last):    if last - first < 2:        return s[first] == e or s[last] == e    mid = first + (last - first) / 2    if s[mid] == e:        return True    if s[mid] > e:        return bsearch(s, e, first, mid - 1)    return bsearch(s, e, mid + 1, last) 总结:本课介绍了算法复杂度的基本概念,非常重要,初学者本课必看!

  • 数组以及可变性,字典,伪码,对于代码运行效率的简介的笔记

    Lecture 7: Lists and mutability, dictionaries, pseudocode, introduction to efficiency. 本课介绍的内容比较多,比较杂,但都是非常基础细小的知识点。本课介绍了列表的另外一个基本操作:赋值。另外,本课也简要介绍了字典,伪代码以及效率。 列表的赋值操作可以改变列表内元素所关联的值。通过List[0] = Value这样的形式进行赋值。其中[0]是列表中第一个元素,这个在前面的课程中已经提到过了。但是在使用列表赋值的语句时需要注意一点:是否存在其他变量也对应了此列表,那么改变此列表中的元素,会导致所有对应这个列表的变量进行改变。举个简单的例子,我有如下程序段(1):L1 = [1, 2, 3]L2 = L1L1[0] = 4print L1>>> [4, 2, 3]print L2>>> [4, 2, 3]这就正如我上面所提到的,由于L1和L2都对应[1, 2, 3]这个列表,改变这个列表的第一项,会导致L1和L2都改变。但是,再看这个程序段(2):L1 = [1, 2, 3]L1 = L2L1 = [4, 2, 3]print L1>>> [4, 2, 3]print L2>>> [1, 2, 3]我们就发现程序段(1)和程序段(2)的差别在于(1)改变了L1和L2,而(2)仅仅改变了L1。这是由于(1)改变了所对应列表本身,而(2)则是改变了L1和列表的对应关系,使L1重新对应了一个新的列表[4, 2, 3],同时,L2保留了原有的对应关系,即L1对应的仍然是[1, 2, 3]因此,在写程序的时候特别要注意这种细微的地方,一不小心就会出现错误。 字典(Dictionaries)可以当做是列表的高级形态,不过它的括号是大括号,而不是方括号。它有以下三个最显著的特点:没有一定的排列次序没有一定的元素号码每一个元素都是<key, value>这样的形式(注意:由于没有一定的元素号码,我们就需要在key里设定这个元素的号码,因此就有了<key, value>这样特殊的形式)<key, value>这样的形式可以这样理解,比如我们有一个单词"abcdefg",它的解释是这样的"a boy can do everything for girls" (^~^) 那么我们把它写成这样的形式:dict = { "abcdefg" : "a boy can do everything for girls" }当我需要用到这个解释的时候,我就只需要使用那个单词就好了。我可以这样做:print dict["abcdefg"]>>> "a boy can do everything for girls"这样就变得非常简便了。当然我们也可以把这个解释存到列表里面去,但是它所对应的元素号码就仅仅是自然数了,达不到解释说明的效果。所以把它输到字典里面,简单明了,通俗易懂,调用起来也非常方便。相信在后面的课程中,会遇到更多的使用字典的例子,在这里就不累述了。 下面讲的是伪代码(Pseudocode),这是一个非常实用和有效的方法。它便于我们整理思路,在写复杂程序的时候,它就是我们的“操作指南”。【注意】 对于初学者而言,在写任何较为复杂的程序之前,一定要先写好伪代码,明确自己到底要做什么。虽然看上去会花一些时间,但是在写程序的时候就会速度非常快,因为已经明确了每一步要做什么。相信尝试伪代码之后,初学者会有更深刻的体会。 最后,本课简单介绍了效率,以及效率的重要性。这里涉及了一些计算机的历史和内部结构。本人认为,本课所讲的效率和算法远不如《算法导论》(第二版)来的深刻,但是简单易懂,适合初学者学习。如果想进一步更全面的了解效率和算法,可以参考《算法导论》的书,也可以学习MIT另外一个课程“算法导论”。相信各位会对那个长发飘飘的号称MIT最年轻教授印象很深刻的。 总结:本课内容比较杂,但都很简单,略过即可。

  • 二分法,牛顿,拉复生方法,对于数组的简介的笔记

    Lecture 6: Bisection methods, Newton/Raphson, Introduction to lists. 本课内容简单明晰,前半部分介绍了牛顿法,举了牛顿法求平方根的例子,然后将其与二分法进行比较。后半部分简单介绍了一些列表的基本操作,主要包括增加和删减列表项。 上节课举的求平方根的例子里面,出现了一个bug而导致程序的崩溃。这也是上节课最后留下的思考题。这个bug是这样的:若所求数x在0-1之间,SquareRootBi(x)会出现错误。最根本的原因在于x的平方根已经超过了原定的上界x。举个最简单的例子:SquareRootBi(0.25),在原来的二分法程序中,上界是high = x = 0.25,但是我们知道0.25的平方根是0.5,而0.5大于0.25,那么原定上界是错误的。因此,修改这个程序的关键之处就在于修改上界high。这里只需要比较x和1的大小即可。当x>1时,平方根上界为x,当x<1时平方根上界则为1。那么在Python中一个简单的high = max(x, 1)就可以实现了。这个例子就给了我们一些警示,即在使用二分法时需要注意以下两点:边界非常重要,在解题时需要慎重考虑边界的选择。在选择边界时往往需要分类讨论,这时比较max的用法和if-else的用法就尤为重要。 (注意:二分法是非常经典和常用的方法,但仍然存在一些缺陷,如上面所提到的bug。那么在使用二分法之前,需要仔细考虑各种情况,划定边界值,以防止程序崩溃。) 本课后半部分简单介绍了牛顿法(Newton's method)。与二分法相比,牛顿法的优势有以下两点:减少迭代次数提高运算效率运算庞大数据时,牛顿法的优势最为显著最直观的比较就是在求平方根的运算中,牛顿法的收敛速度(speed of convergence)要比二分法快很多。换句话说,牛顿法做的迭代次数(iteraction times)要比二分法少很多。那么假设在相同时间内做的迭代次数是一样的,牛顿法就比二分法所化的运算时间要少很多。可以说,输入数据越大,运算效率差就越大。牛顿法是编程中的最优化算法之一。(注意:这里使用的是牛顿切线法,具体定义可参考百度百科) 根据百度百科,牛顿切线法是由开方公式引出的:        X(n+1) = Xn + (A / X^(k-1) - Xn)(1 / k)       (n,n+1表示下角标,k为指数)这个公式看上去非常复杂,对于初学者而言只需要记住如下结构即可:A = f(guess)B = fd(guess)    # B为A的导数表达式diff = A - x    # x为输入值/精确值guess = guess - diff / B根据这个结构我们就可以轻松构建出一个开平方函数了(注意:这个函数和本课例举的函数有细微差别),程序如下:def f(guess):    return guess ** 2def fd(guess):    return 2 * guessdef SquareRootNR(x, epsilon):    guess = x / 2.0    diff = f(guess) - x    ctr = 1    while abs(diff) > epsilon and ctr <= 100:         guess = guess - diff / fd(guess)         diff = f(guess) - x         ctr += 1    return guess(注意:guess的值非常重要,因为某些guess的值会导致程序崩溃,即切线和x轴没有任何交点。所以在输入initial guess的值的时候需要注意避免这些值。) 后半部分简要介绍了列表(List)的基本用法,List是一个非常灵活和强大的工具,可以进行许多神奇的操作。这个会在以后讲到类(Class)和方法(Method)的时候提到。由于本人对于列表的应用进行了较多遍,这里就不重复记录列表的基本用法了。对于初学者来说,可以去参考一下Python基础教程(第二版)中List的应用,非常有帮助。 总结:本课重点为牛顿法,这个并不如二分法那样被人所知。不知道的可以看看这一节课。

  • 浮点数和二分法(逐次近似)的笔记

    Lecture 5: Floating point numbers, successive refinement, finding roots. 本课前半节基本介绍了浮点数和运用浮点数的一些注意点。后半节介绍了计算机常用的逐次逼近法和最经典的二分法。 浮点数(Floating numbers)也可以理解为数学中的实数(Real numbers),但是在计算机浮点数运算中时常无法算出精确值。这是由于计算机使用了二进制数进行运算。在使用浮点数进行运算时,需注意以下几点:浮点数只能取到小数点后17位由于浮点数运算结果为近似值,多次运算会导致误差的累计,从而使结果非常不精确不可以用“==”判断浮点数,因为它们是不精确的,所以只能判断他们与理想数的误差。例如:abs(a**2 - 2.0) < 0.0001 这就算出了根号2的近似值(注意:在浮点数问题中,我们无法使用穷举法,因为浮点数的数量是无限的) 本课后半节介绍了计算机中非常有效和常用的方法,即逐次逼近法(Successive approximation method)。其基本程序结构如下:    guess = initial guess    for iter in range(ctr):        if f(guess) close enough:             return the guess        else:             guess =  a better guess(注意:其中ctr是用于限制循环次数的控制变量,其用处是避免程序出现死循环) 另外,二分法(Bi-section method)是逐次逼近法中最经典的一种,它每次运算都可以使计算量减少一半,从而大大提高计算速度。本课中举了一个开平方的例子,程序如下:def sqrt(x, epsilon):    low = 0    high = x    guess = (low + high) / 2.0    ctr = 1    while abs(guess ** 2 - x) > epsilon and ctr <= 100:        if guess ** 2 < x:            low = guess        else:            high =  guess        guess = (low + high) / 2.0        ctr += 1    return guess(注意:这个程序是有bug的,在Lecture 6一开始的时候会提到,当然这里也可以思考一下) 总结:本课最主要的是介绍逐次逼近法,并没有其他特别重要的东西。

  • 函数抽象与递归简介的笔记

    Lecture 4: Decomposition and abstraction through; introduction to recursion本课标题的真正翻译为:函数的分解与抽象化及递归介绍 本课基本介绍了如何建立一个函数。这是一个非常简单和基础的知识,我在这里就不重复了。但是初学者时常会出现这样的状况:很多人喜欢滥用函数,在不需要用到函数的时候依然要建立一个函数。那么一下几个注意点就解释了什么时候函数是必要的:函数的最主要作用是把一个程序段储存起来,并使之抽象化。在建好函数之后,我们只需要知道此函数的功能,并不需要知道它具体的运作过程。函数的最主要好处就是避免在不同的地方写重复的程序段,只需调用函数即可。因此,只有在多处需要写同样的程序段时,函数才是必要的。(注意:在写递归函数的时候,定义函数时会调用此函数本身,这就避免了N次重复程序段,从而使函数变得十分简洁。) 在定义函数时还需要注意的一点是局部变量(Local Variable)和全局变量(Global Variable)的用法。本课仅强调了局部变量的用法,定义函数中所用到的变量仅仅存在于“黑箱”之中。出了这个函数,这些变量就是未定义的(Undefined)。当然,函数中也可以调用全局变量,应该在之后的课程中会提到。这里简要说明就是:在global这个关键词之后写上需要调用的全局变量。 本课举了小学奥数中最经典的“鸡兔同笼”问题,并且用穷举(Brute-force Algorithm)的方法暴力破解。可能在视频中看不清楚,我重新写了一下鸡兔同笼问题的程序,如下:def solve(numHeads, numLegs): """ this function is to calculate numbers of chickens and pigs """    test = False    for numChickens in range(0, numHeads + 1):        numPigs = numHeads - numChickens        if 2 * numChickens + 4 * numPigs == numLegs:        test = True        return numChickens, numPigs    if not test:        return None, Nonedef Farmyard(): """ this function is to get inputs from users and print solutions """    numHeads = int(raw_input("Number of heads is: "))    numLegs = int(raw_input("Number of legs is: "))    numChickens, numPigs = solve(numHeads, numLegs)    if numChickens == None and numPigs == None:        print "No solution"    else:        print "Number of chickens is:", numChickens        print "Number of pigs is:", numPigs 本课的第二部分简单介绍了递归式,这里涉及的仅仅是最基本的递归式,例如:U(n) = U(n-1) + U(n-2)。这是一个斐波那契数列的递归式。当然,U(0) = 1, U(1) = 1。由此,我们可以写出斐波那契数的递归解法,程序如下:def Fibonacci(n):    if n == 1 or n == 0:        return 1    else:        return Fibonacci(n - 1) + Fibonacci(n - 2)我们发现:在定义这个函数中,我们又调用了这个函数本身,这就达到了递归的效果。这仅仅是一个非常简单的例子,我们还可以任意变换一下这个递归式,例如:T(0) = 0, T(1) = 0, T(2) = 0, T(n) = T(n-1) + T(n-3) + 3那么我们可以用一下程序解出T(n):def T(n):    if n == 0 or n == 1 or n == 2:        return 0    else:        return T(n-1) + T(n-3) + 3这就构成了一个最基本的解递归的程序结构。 总结:本课最主要的就是递归入门,没有学过递归的需要来这里看一下。

  • 一般代码样式,循环式程序的笔记

    Lecture 3: Common code patterns: iterative programs. 本课基本讲述简单的while和for循环格式和创建循环的基本步骤及注意点。本课也介绍了一种新的数据结构Tuple,以及提取(Selection)和切片(Slicing)的基本方法。 在上节课所提到的,对于初学者而言,需要培养良好的程序风格,分为以下三点:在必要时写注释(Comment)不可更改变量类型(Type)过于频繁使用描述性(Descriptive)的变量名称(注意:注释不必面面俱到,只需要在必要时讲明以上程序段或者以下程序段的作用。或者在某一关键步骤后面加以注明,以方便自己或他人在以后阅读中理解) 创建一个基本while循环的步骤如下:选择合适的计数变量(Counting Variable)在循环外初始化这个变量设置正确的测试(End Test),即紧跟while之后,冒号之前的语句在冒号之后建立一个有效程序段注意增加或减少计数变量(注意:在循环之后,计数变量往往会比理想值(Expected Value)多或少,此时需要通过增加或减去一定值将其回归理想值。这可以通过手动模拟进行操作)【重点技巧】在创建较为复杂的循环时,需注意使用流程图(Flow Chart)来明确思路。 For循环相较于while循环,有以下两点优势:for循环一般不会出现死循环,只要所创建的数组是有限的。因此,不需要创建计数变量。for循环可以使用任意数组,并不局限于等差数列或者等比数列 本课提到的新的数据结构为Tuple,不同于List,Tuple的表达方式为:(1, 2, 3, 4);而List的表达方式为[1, 2, 3, 4]。不同之处在于一个是圆括号,一个是方括号。但对于提取(selection)和切片(slicing),无论是tuple还是list都是通用的。例:test = (1, 2, 3, 4)提取:test[0] = 1切片:test[1:3] 等于提取test[1]和test[2]但不包括test[3]。因此test[1:3] = (2, 3)注意:tuple和list的相加必须保证两个tuple或者list至少都存入两个或以上的数值。经测试,在python 2.7.3中,如下程序是非法的:a = (1, 2, 3)b = (4)print a + b出现syntax error 最后需要注意的是:字符串也支持提取和切片,因此在读入字符串之后并不必要把字符串转换成list或者tuple而是可以直接进行操作。 总结:本课所讲述内容非常基本,瞄一眼即可。

  • 浮点数和二分法(逐次近似)的笔记

    python的两种数值类型:1.整数型2.浮点型:用来描述实数**表示次方符号,例如2**2计算结果为4L表示长度abs表示取绝对值目前所有的编程语言,包括python都采用IEEE 754标准表示数字,将数字用尾数加指数的形式表示1≥尾数范围<2;-1022≥指数范围<1023浮点数可以表示至17位小数的精度,因此在输入小数之后,输出的小数精度保留为小数点后17位,其实质输出的是字符举例:输入 a math.sprt(2)【解释:调用math模块的sprt函数】输入a之后结果为1.414....至17位浮点数无法使用==进行比较,判别会出现false,这是因为浮点数只能意味着无限接近

  • 课程目标,数据类型,运算,变量的笔记

    *学习目地:1.写小规模程序,写出小段代码2.锻炼理解别人所写程序的能力3.了解计算机的功能与局限4.将其他的程序转换为可处理的程序*Python是高级的、广泛的、解释型语言

  • 函数抽象与递归简介的笔记

    掌握了四种语句从理论上而言,、你已经可以写出任意程序,但是事实上这远远不够,你必须还要掌握更多的东西,你需要掌握“分解”和“抽象”。1.分解:让代码结构化的一种方式,即把代码分解为模块,每个模块是程序的独立组件,具有独立的意义,可以单独运行2.抽象:即忽略具体的细节函数包含了分解和抽象两个部分,如何建立函数?def+函数名+(变量名)+形式参数return:计算到此处停止,返回值binds x to 16,绑定x为16 

你感兴趣的课程

3万+浏览/ 931学员/ 4.7评分
¥9.90
理论基础 数学之美
2万+浏览/ 708学员/ 4.4评分
免费
2万+浏览/ 826学员/ 4.8评分
免费