算法导论学习笔记

设计算法的技术:(所有的算法都可以用循环不变式思想来证明算法的正确性)

 

  1. 算法分析:
  • 算法设计关键在于考虑所有的可能性,即使需要利用穷举法进行分析
  • 标记:
  1. 本文档为了方便表示,上下界均用O表示,具体上下界会特别指出,如果没有具体指出上下界,则一律指上界下界均为O中所指的值
  2. 关于O(n2)的含义,具体指

an2+bn+c<=f(n)<= dn2+en+f

内部的n2 实际上来自于一个二次多项式,例如an2+bn+c,注意这里的

a b c d e f均为常数,省略低阶项和常数项得到O(n2)

  1. 文档中所有lg均是以2为底数
  • 算法分析—影响算法效率的两个因素
  1. 输入规模本身的特性,如果作优化,可以改进算法效率;输入规模并非输入的数量,而是单个输入的复杂度,例如输入一个数列,而数列长度length就是所谓输入规模n=A.length
  2. 运行时间,算法操作所进行的步数
  1. 最坏运行时间:大部分情况下,采用最坏情况分析
  2. 平均情况分析:输入信号具有随机性,采用平均运行时间;如果是随机算法,则考虑期望运行时间,但最坏情况分析仍然具有极大的参考价值,因为最坏运行时间往往和平均情况的复杂度非常相似
  1. 算法设计总结
  • 增量算法,例如插入排序,循环不变式即数学归纳法思想,这是算法的核心,递归、迭代等算法皆由此出
  • 分治算法:或者叫做递归法,将问题分解为几个小规模的与原问题相同的子问题,并且递归地解决这些子问题,直至触底为基本情况,基本情况可以直接处理并返回,最终,合并这些子问题的解来建立原问题的解
  • 概率分析:概率分布在输入上
  • 随机算法:算法本身作出随机选择
  • 递归法与迭代法的比较:
    1. 递归的核心是数学归纳法的程序表示
      1. 假设该递归可以获得解,结合已经获得的解,构造当前函数的正确解并返回
      2. 在递归函数的开头部分,增加最基本问题的解法
    2. 迭代是,迭代外壳+完全独立的子过程
    3. 两者本质上极其类似,迭代代码量大,但速度快,运算过程中会清栈,不必保留中间变量;而递归代码量小,速度慢,有可能造成栈溢出,代表运算结果的中间变量必须保留,不能清栈;如果可能,出于性能考虑,优先考虑迭代
    4. 区别上可以简单总结为:调用自身是递归,调用他人是迭代;也即递归本身可能也是一个迭代外壳,也存在迭代内部也可能存在一个递归子过程
  • 动态规划:解决传统的递归计算时,出现的相同子问题的重复计算问题,即时空权衡
    1. 本质:
      1. 根本上是解决最优解的问题,而分析动态规划时,需要在每一层递归中权衡更优的决策,按照数学归纳法,每次做出更优的决策,最后一步决策时,一定能得到最优解
      2. 过程解有两种:更优的决策和更差的决策,我们一般需要比较这两种决策,从而新的最优过程解
      3. 与贪心算法的区别:贪心算法可以直接根据最优解计算,而在动态规划中,需要从多个解中权衡取舍中得出最优解,贪心算法可以不经过比较就可以直接得出最优解
    2. 与递归的关系:由数学归纳法可知,动态规划的实质就是递归+备忘录,因此,自顶向下更符合动态规划的特性
    3. 动态规划是分治算法的子集,其比分治算法效率更高
    4. 核心:父问题严格依赖于小规模且相同的数个子问题,详见子问题图
    5. 要素:
      1. 最优子结构:子问题无关,也即一个子问题,不会影响另一个子问题的存在,可以共存
      2. 子问题重叠:子问题在求解过程中,会反复求解一些相同的子问题,而在求解过程中避免重复子问题的计算,是动态规划优于分治法则的原因
    6. 两种方案的对比:(都是n2复杂度)
      1. 自顶向下(备忘录法):由于需要频繁递归,因此大多数情况下其开销要比自底向上要小
      2. 自底向上:大多数情况下要比自顶向下的开销要小,尽管只有常数倍数的差别
      3. 由于自顶向下最接近动态规划的定义,因此,一般先用递归法求解问题,如果需要效率优化,则可以将自顶向下转换为自底向上的方案
  • 贪心算法的关键要素:
    1. 与动态规划的关系:
      1. 贪心算法的应用情况属于动态规划的子集,也即所有的贪心算法总可以用动态规划来实现,反之不成立
      2. 贪心算法能够极大地降低复杂度,例如如果其复杂度为O(n),那么动态规划则可能为O(n2)
      3. 贪心算法可以直接根据最优解计算,而在动态规划中,需要从多个解中权衡取舍中得出最优解,贪心算法可以不经过比较就可以直接得出最优解
  1. 贪心选择性质:
  1. 自顶向下,当作出一个贪心选择,不必事先分析构成该贪心选择的子问题的解,而是直接作出贪心选择(区别于动态规划)
  2. 贪心选择可能依赖于之前作出的选择,但绝对不会依赖于将来要作出的选择和子问题的解
  3. 输入单增预处理,可以改进贪心算法,如果不对输入做单调预处理,也能运用贪心算法,但代价是,每次递归或迭代时,都必须扫描所有的输入集,增加了工作量
  1. 最优子结构:子问题的最优解和贪心选择合并生成原问题的最优解
  2. 判断关键点:
    1. 假设先直接做出首次贪心选择(不必事先分析构成该贪心选择的子问题的解),将问题分解为贪心选择的解和余下的子问题的最优解
    2. 考量在贪心选择的子问题中,该贪心选择是不是该子问题的最优解,也即是否存在比贪心选择更好的最优解;
    3. 如果该贪心选择是最优解,即可以用来贪心选择
  • 摊还分析
    1. 聚合分析:如果n个操作的最坏运行时间为T(n),则摊还代价为T(n)/n,这对于所有的操作都是如此,即使某个操作的代价相比其他的操作高出很多,例如POP和MUTIPOP的区别非常大,后者最坏运行时间是前者的n倍(栈空间大小为n)
    2. 核算法:任何时候信用总为非负值,某个操作的摊还代价大于实际代价,而复杂的操作的摊还代价可以小于实际代价,甚至可以为0,从而简化了总摊还代价的分析,又由于总摊还代价是实际总代价的上界,因此也即分析出了这些操作的实际代价总和的复杂度
    3. 势能法:为每种数据结构的状态定义一个势函数放f(x),x0 表示初始状态,xn 表示第n次操作后的状态,需要保证对于任意的n值f(xn)>=f(x0),而每个操作的实际摊还代价为(c为实际代价)

C = c + f(xn)–f(xn-1)

而每个操作的复杂度可以精确地计算出来,乘以n即可算出摊还代价的总复杂度,而总摊还代价一定是总实际代价的上界,因此简化了分析

  1. 数据结构
  • 前言:
    1. 实际应用中,数据结构千变万化,但都离不开这些基本的数据结构,复杂度最终由一种数据结构的底层实现决定
    2. List是顺序表,顺序表是一种抽象的数据结构,其底层实现有两种:数组和链表。数组实现要求必须内存空间连续,而链表的内存区域是通过指针连接起来的,在随机访问方面,数组实现优于链表实现,但插入删除之类的,链表实现要优于数组
    3. 例如顺序表List有两种实现,数组实现ArrayList和链表实现LinkedList,而Set也有HashSet和TreeSet两种实现,分别基于散列表和红黑树,复杂度分析要基于这两种基本的数据结构分析
    4. 以HashMap为例,其内部以散列表为底层实现,每个元素是一个Entry类,而每个Entry是一个key关联一个value,而Hash值根据key值计算,key值一定不会重复,而hashcode值却可能重复而导致冲突
  1. 搜索操作,对用java中的contain等操作,本质上是根据关键字搜索对应的元素
  2. 而随机访问操作,对应java中的get操作,而对于数组则是A[i]操作,后者复杂度是o(1),而前者往往要慢一些
  3. Key是元素的唯一标识,以下讨论中,假设关键字互不相同,而下标则是顺序标记,类似于数组的下标
  • 数组:数组也是一种数据结构,但其本身不具备集合的基本操作,例如插入和删除,因为其长度是固定的,因此数组不是集合
    1. 由于采用直接寻址,随机访问一个元素时间复杂度为(1);搜索一个元素的复杂度为O(n)
    2. 对于已经排好序的数组来说,随机访问的复杂度不变;但搜索一个元素复杂度为O(lgn)
    3. 除数组外,别的数据结构通常情况下,不能在O(1)时间内随机访问任何一个元素,例如链表的get操作,时间复杂度为O(n),因为其每次必须从head指针开始,向后移动;因此集合的随机访问性能不如数组
  • 堆:也叫做二叉堆,本质上是一个数组,但可以看作一个近似的完全二叉树,但不是完全二叉树
    1. 最大堆:用于堆排序,父节点大于两个子节点,但左右节点的顺序没有要求,要还原数组,只需要从最上层开始,从左往右开始,依次排序,最终将会得到数组;按照java的起始位置0为参考,如果已知某节点为i,那么其左节点为2i+1,右节点为2i+2
    2. 最小堆:用于构造优先队列,父节点小于两个子节点,但左右节点的顺序没有要求
  • 优先队列:
    1. 优先队列可以借助最大堆或最小堆来实现,分为最大优先队列和最小优先队列
    2. 最大优先队列的应用场合例如任务调度,数组下标可以作为优先级或Key值句柄
    3. 最小优先队列应用在按照时间顺序的事件驱动等场合
  • 决策树模型:在分析四种比较排序中,借助决策树模型,可以得出最坏情况下界为O(nlgn),决策树模型是一颗完全二叉树

 

----------以下开始,都属于集合范畴----------

 

  • 栈:也称为堆栈,这里以数组实现为例;而单独的“堆”有两层含义,一种是垃圾回收机制中的由程序员分配的内存块,另一种是(二叉)堆
    1. 其具有三种基本操作,POP,PUSH,STACK-EMPTY(是否为空栈),三种复杂度都是O(1)
    2. 另有上溢和下溢两种错误提示,分别是POP超出栈大小和对空栈进行POP操作
  • 队列:这里以数组实现为例,以一个长度为n的数组为例,首尾相邻构成一个环形结构。Q.head指向队首元素,而Q.tail指向下一个元素将要插入的位置
    1. 如果队列未满,则Q.tail总是指向一个空的位置;如果队列已满,则Q.tail和队首元素Q.head重合。
    2. 队列包含两个基本操作,入队和出队,复杂度均为O(1)
    3. 当采用非数组实现的时候,入队出队操作对应于底层实现的插入和删除操作,复杂度仍然为O(1)
  • 链表:类似于数组类型,以双向链表为例,数组的顺序由下标决定,而链表的顺序则是由各个子对象元素里的指针决定的
    1. 每个元素都是一个子对象,每个子对象包含一个pre和next指针,前者指向前驱元素,后者指向后继元素;而链表内部有一个head指针,它属于链表对象本身而非元素对象,head指针指向首元素,然而首元素的pre始终为null,而且尾元素的next始终为null;
    2. 如果head指针为null,则链表此时为空
    3. 链表可以按照插入顺序维护数据结构,而java中的通用的HashSet集合则不会保存插入顺序,而其变体如果采用链式结构,则可以赋予Set集合的按次序排列
    4. 链表的变体包括:单链接(不含pre),循环(首尾相互指向),排序,非排序则表示插入的顺序可以在插入时按照key排序而动态调整
    5. 链表和数组的对比:链表在插入和删除时具有更好的性能,而数组对于随机访问具有更好的性能。这是由于如果超出数组边界,则需要创建新数组,并重新赋值,因此复杂度更高
    6. 链表中key的含义是元素对象某个属性值,而非链表的顺序标号,链表的顺序由指针来维护
    7. 从链表中依据key值,搜索某个元素,复杂度为O(n);而删除指定的某个元素,复杂度为O(1);插入一个元素,复杂度仍然为O(1);随机访问某个元素即get操作的复杂度为O(n)
    8. 哨兵:加入哨兵作为首元素的链表相当于一个循环双向链表,也即简化了首尾边界处理,删除了head指针,用L.nil代替head指针,该指针永远指向哨兵元素,将首尾用哨兵元素相连接,统一简化了各个基本操作;哨兵的加入,简化了编程,也在常数系数级别上降低了复杂度,然而,当存在很多短链表的时候,浪费了空间
  • 指针和对象:有些编程语言不支持对象或指针,这就提供指针和对象数据结构的实现
    1. 多数组同一下标构成一个对象,分别是pre,next,key,其中pre和next是整型数组,其元素值代表的下标可以指向另一个对象
    2. 单数组用三个连续的元素表示一个对象,分别是key,next,pre,next和pre的值指向别的元素key所在的位置,按照0,1,2的偏移量可以找到其余的对象属性
  • 有根树:借助链式结构可以实现树的数据类型
    1. 二叉树:每个父节点最多拥有两个子节点
    2. 分支无限制的有根树:采用左孩子右兄弟表示法(O(n)存储空间),每个元素内部仍然只有两个指针,一个指向该父节点的孩子,另一个指向该父节点的相邻的平级兄弟节点
  • 直接寻址表(容量非动态,易造成空间浪费)
    1. 把指向对象的指针存放在一个数组中,数组中存放对象的指针,而数组的下标即关键字,这个数组称为直接寻址表。直接寻址表本质上是一个数组,用空间换取效率,提高了性能
    2. 当关键字全域不是很大的时候,创建一个与关键字全域相同大小的的直接寻址表,当进行随机访问的时候,采用直接寻址,只需要O(1)复杂度,就可以找到对象的指针,进而返回该对象;当进行搜索操作的时候,复杂度为O(1),因为key值与下标相同,因此实质上与随机访问操作是一样的
    3. 某些情况下,可以直接在数组存储表中,存储对象而非对象的指针,可以进一步在常数级别上提高效率,但复杂度仍然为O(1)
    4. 缺点:直接寻址表是一个固定长度的数组,无法存储过多的对象,灵活性受限;而且当所存储的对象数量小于直接寻址表的大小时,会造成空间的极大浪费
  • 散列表:
    1. 散列表解决了直接寻址表的缺陷,将散列表的大小控制在O(K)范围内,K是所存储的元素总数;然而降低了搜索性能,尽管如此,其搜索性能仍然是优秀的
    2. 散列表以散列值为下标,本质上是一个数组,散列值根据key值经过散列函数算出,如果出现了重复,有相应的解决冲突的方案
    3. 散列表是数组概念的推广,其搜索功能的平均时间是O(1),最坏运行时间是O(n);插入和删除操作复杂度为O(1)
    4. 对于完全散列,由于对元素采用静态储存,即一旦存入后不再改变,因此提高了性能,其搜索操作的最坏运行时间也是O(1)
    5. 散列表类似于直接寻址表,但其下标由散列函数来计算,即下标

k = h(key)

T[k]即可取出对象的指针,进而返回对象;

    1. 解决冲突:
  • 基本概述:
  1. 散列表的大小一般会小于关键字全域
  2. 如果输入规模大于散列表大小,则一定会产生冲突
  3. 即使是输入规模小于散列表大小,仍然由于散列函数自身的原因,仍然可能会对不同的key产生相同的下标,造成冲突
  4. 开放寻址法对于删除的操作支持不友好,因此链表法更为常见
  1. 链表法:散列表的元素通过链表形式来储存,可以解决冲突问题,此外也不会影响搜索,因为关键字互不相同,只是散列函数产生的下标相同,因此仍然可以用key在链表中搜索对应的元素;采用链接法后,查找的复杂度变为O(n),然而其平均运行时间仍然为O(1);而插入和删除的复杂度不变仍然为O(1)
  2. 开放寻址法:开放寻址法的容量上限有限制,不比链表法;然而,其对于每种关键字,用关键字key作为参数,生成对应序列的特定排列,不会出现下标相同的情况,从左往右依次储存,提高了检索效率,直到整个数组被充满,以至于不能够继续增加元素;当装载因子n/m为一个常数的时候,插入和检索的复杂度均为O(1),因为插入和删除隐含检索操作,而检索操作的复杂度为O(1),总体上优于链表法;由于删除操作产生的标识会影响复杂度衡量,因此,不考虑删除的复杂度
  • 完全散列:该数据结构一旦存入后,不再能够删除,例如CD-ROM等介质;采用完全散列的技术,插入和检索的复杂度均为O(1)
  • 二叉树:
    1. 基本概述:
      1. 对于完全二叉树,高度为lgn
      2. 对于非完全二叉树,最大高度为线性链n
    2. 二叉树的每个节点除了key值外,还包含三个重要属性:p、left、right,分别指向父节点、左节点和右节点,而某些题目会省略父节点,需注意;然而,传统意义上的二叉树,必须包含三大属性
    3. 重要概念:深度h,也称高度h,每个小分支的长度为1深度,因此,只有根节点或者没有根节点(后者为空树)深度为0
    4. 二叉树的分类:(3、4为常用考点)
      1. 满二叉树:所有的非空节点,都有左右两个子节点,空节点不必再有子节点
      2. 完全二叉树:所有叶的深度均相同,也即最后一层不存在空节点;如果完全二叉树的最后一层不满,尽管不是完全二叉树,但高度仍为lgn的向下取整
      3. 一般二叉树:
        1. 左中右的节点大小顺序无限制
        2. 元素key值可以相同,也即可以包含相同元素
  1. 二叉搜索树:
          1. 所有节点的key值互不相同
          2. 左子树中的所有节点<父节点<右子树中的所有节点
          3. 注意这里是大于、小于号,不包括相等的情况
  1. 二叉树的三大遍历方法:区分依据是左右子树相对根节点遍历的先后顺序
    1. 若根节点输出位于左右子树之前,为前序遍历(preorder)
    2. 若根节点输出位于左右子树之间,为中序遍历(inorder)
    3. 若根节点输出位于左右子树之后,为后序遍历(postorder)
    4. 常用中序遍历,三种遍历方式的复杂度均为O(n)
    5. 关于左右子树的扫描顺序,一定是从左往右,即使是后序遍历也是一样的,区别只有根节点的位置,左右子树不变
  2. 搜索、最小值、最大值、前驱和后继操作复杂度均为O(h),h为树的高度;对于完全二叉树,h = lgn,因此复杂度为O(lgh)
  3. 插入和删除操作的复杂度为O(h),同理,对于完全二叉树为O(lgn)
  4. 仅通过插入操作,随机构建二叉树的期望高度为lgn
  • 红黑树:
  1. 众多平衡树中的一种,红黑树同时也是一种二叉树,但比起传统的二叉树性能更优秀
  2. 红黑树的最大高度为2lg(n+1),这一点优于二叉树,因为二叉树最大高度为n
  3. 红黑树的集合基本操作的复杂度降为O(lgn),而二叉树是O(h)
  4. 在java中的应用场合,例如TreeMap底层采用红黑树实现;而HashMap底层采用散列表来实现
    1. 就搜索功能来看,后者性能更优秀;
    2. 然而由于TreeMap是有序储存,而HashMap是无序储存,因此,如果要顺序遍历,TreeMap性能更好
  1. 算法总结:
  • 排序:
  • 基本概述:
      1. 这里假设元素互异
      2. 重点是前七种比较排序,因此存在下界O(nlgn)
      3. 后三种为线性时间排序,由于不使用比较,因此可以突破下界O(nlgn)
  1. 冒泡排序:O(n2),空间原址性
  1. 基本思想:每次都把最小值放在最左边,找最小值过程中多次交换,最终使得最小值从左往右依次排列
  2. 算法设计:嵌套循环,外层循环i从0到length,内层循环从i+1开始,找比A[i]小的数字,然后交换,将较小的值放在A[i]的位置,这样每次都能把最小的值放在最左边,内部循环需要多次交换
  1. 选择排序:O(n2),空间原址性
  1. 基本思想:与冒泡极其相似,每次都把最小值放在最左边,找最小值过程中仅需要一次交换,最终使得最小值从左往右依次排列
  2. 算法设计:其内部循环只需要交换一次,每次只要记录一个最小值所在的下标min即可,内部循环结束后,只需要交换A[i]和A[min]即可,因此从性能上略优于冒泡
  1. 插入排序:O(n2),空间原址性
  1. 基本思想:假设左边的部分数组已经排序好,每次把当前的数组放在左边数组的正确位置
  2. 算法设计:嵌套循环,外层循环i为1->A.length,如果A[i]<A[i-1]那么进入内层循环j,缓存tmp = A[i],从i-1开始到0,在内部判断:如果A[j]>tmp,则A[j]向后移动一位循环继续;直到A[j]<tmp,令A[j+1]=tmp,那么此时终止内循环,插入操作完成
  1. 希尔排序:O(nlogn)-O(n2)之间,优于插入排序,空间原址性
  1. 基本思想:将数组以间隔inc的不连续的元素组成inc组的新数组,对于每个新数组都进行插入排序,inc逐步递减,直到inc为1(inc=1时不执行排序),此时排序完成,一种递减方案是inc=inc/4+1
  2. 算法设计:最外层为一个do-while循环,while中为i>1;内部的嵌套循环,外部i为inc->A.length;内部插入操作时,一定时增量为i-inc开始倒数的,下一个元素为i-2*inc
  1. 堆排序:O(nlgn),渐进最优,空间原址性
  1. 基本思想:按照二叉堆构建数组,根节点一定是最大值,然后把根节点与未排序部分数组的最后一个元素交换,然后剩余的未排序部分重新调制符合二叉堆,然后以此类推
  2. 算法设计:把根节点和数组的最后一位n交换,这样最后一位n为当前最大值,然后把剩余的n-1个元素按照二叉堆调整,然后n--,直到i=0为止
  1. 归并排序:O(nlgn),渐进最优,不具有空间原址性
  1. 基本思想:把数组分为两部分,假设两部分已经排序完成,然后将两部分数组合并构成新数组即可,这是一个递归法
  2. 算法设计:难点在于归并过程,两个变量i和j分别代表两个数组的当前位置,创建一个新的数组,然后从左往右填充,填充值为A[i]和A[j]的最小值,取出后,记得i++或者j++
  1. 快速排序:O(nlgn),具有空间原址性
  1. 基本思想:用递归法,先完成基准排序然后返回排序后基准值的新位置,然后以基准值为中点,两次递归调用,将左右两边的子数组递归快速排序
  2. 算法设计:难点是基准值的排序,外面是一个大while,循环里面,首先将第一个元素作为基准值,然后保证该基准值的左边小于该基准值,右边大于该基准值,从右边开始往左找到小于基准值的数字,然后交换基准值和当前的数字,之后,从左往右找到大于该基准值的,然后交换当前值和基准值的位置,
  1. 计数排序:O(k+n),不具有空间原址性,但具有稳定性,即先出现的元素,在排序后的数组中,仍然最先出现,对于重复元素亦是如此;该算法有前提,即所有的输入必须为k个可能的取值之一
  1. 当k的上界为O(n)时,复杂度为O(n),此时一般采用计数排序
  2. 这里需正确理解上界为O(n)的含义,即k<=an+b,a b均为常数
  1. 基数排序:O(d(k+n)),其中d为所给输入的位数,k表示每一位可能的取值范围大小,n表示输入的d位数的个数,当d为常数且k的上界为O(n)时,该算法复杂度为O(n);由于该算法以计数排序作为子过程,因此不具备空间原址性;该算法的特别之处在于,排序顺序先从低位开始,然后延伸至最高位,当每次比较的位数r≈logmn时,达到最短线性时间,m是进制数
  1. 基数排序和计数排序的关系和区别,计数排序作为基数排序的子过程被重复调用,由于计数排序要求输入在k个可能取值之内,有时候k可能很大,因此不方便运算;而基数排序中的计数排序的k值为每个数位的可能取值之一,一般是十进制等,k值方便操作,而不必是输入的所有可能性范围,因此相比计数排序适用性更广
  2. 实际上按位比较并不一定是最优线性时间。以二进制数为例,进行基数排序时,可以灵活决定每个数位的实际位数,例如一个b位数可以表示为d个r位数,当b的上界为O(n)时(基数排序条件),经过分析,r≈lgn时,可以达到最优线性时间;如果是十进制等,则将log的底数设置为相应的进制数;当r≈logmn时,k=mr是一个常数,满足k的上界是O(n)(计数排序条件
  3. 基数排序和比较排序的对比:尽管基数排序时线性时间,但由于基数排序不具备空间原址性,因此更加耗用内存,也因此,基数排序每一次递归所耗费的时间往往相对长,此外,比较排序可以更好地利用硬件缓存;因此在内存珍贵的情况下,比较排序仍然是首选;两者的运行效率受诸多因素影响,如果内存充分,则选择基数排序
  1. 桶排序:假设输入为均匀分布,则期望运行时间为O(n);该算法运用了链表和插入排序,而插入排序的期望时间为n(2-1/n)为线性时间,因此复杂度始终为线性时间
  2. 顺序统计量(本部分的中位数一律指下中位数,向下取整)
  1. 由于本节着重点是选择问题,即从数组中找出第i小的元素;如果按照先排序后选择的方法,不是效率最快的;而本节的算法不需要事先把数组全排序,就可以找出问题的解
  2. 找出最大值或最小值:若采用比较法,则最少需要n-1次运算
  3. 同时找出最大最小值:将元素成对组合,若为奇数,则首元素同时为当前最大值和最小值,每对元素自身比较一次,分别和当前最值比较一次,每对元素共比较3次,最多比较3n/2次
  4. 期望时间为线性:期望运行时间为O(n),该方法可以用来找到第i大小的数,例如中位数、最大最小值,等等;具体原理是利用快速排序随机版本的partition函数,一次只定位一边,每次都找出一个中位数,直到锁定所找的第i大小的数字
  5. 最坏时间为线性:最坏情况复杂度为O(n),在上述基础上,保证每次的划分位置都是一个好的划分,首先以5个为一组划分,找出每组的中位数,再从中找出中位数,将其作为x作为划分点,每次递归都会是一个不错的划分,不会出现最坏情况的划分,因此最终复杂度为O(n)
  • 图算法:
    1. 输入规模:结点数V和边数E,例如O(VE),运行效率和两个参数有关,而不是非图算法时的n
    2. G.V和G.E表示图形G的结点集合和边集合
    3. 图的表示:
      1. 邻接链表和邻接矩阵
      2. 有向图:边是单向的,反过来相当于边不存在,因此有向图最大的边数会少于无向图
      3. 无向图:边是没有方向的,因此正反方向都存在边,因此无向图的边数最大值会比有向图多
      4. 对于邻接矩阵和邻接链表来说,两者都可以表示无向图和有向图
    4. 邻接链表:
      1. 特点:适合大多数情况,如边数较少,稀疏图,这里的参考点是E<<V^2
      2. 构成细节:先为点从0开始标号,邻接链表实质上是一个数组,每个元素是一个单向链表(当然,也可以增加父节点变为双向链表),包含了所有与当前点有邻接边的所有点,顺序随意
      3. 权重:链表中增加权重属性即可
    5. 邻接矩阵:
      1. 特点:适合稠密图,E≈V^2,如果需要判断两个结点之间是否有边相连,也需要邻接矩阵,此外,由于邻接矩阵存储空间占用多,但使用简单,因此在图形规模不是很大时,采用邻接矩阵是个不错的选择
      2. 详细细节:先为点从0开始标号,创建一个v*v的矩阵,对应的点之间有边时,则相应的值为1,没有则为0
      3. 权重:矩阵中的值就是权重的值,当然,没有边相当于权重为0,那么相应的元素值也是0,此外,oo无穷大也可以表示无边
    6. 广度优先搜索DFS(depth first search):
      1. 特点:每次都要扫描当前结点的所有子节点,且如果第一个被扫描的结点为s,那么扫描完成后,每个结点的d属性一定是原结点到本结点的最短路径长度
      2. 算法设计:黑色结点代表已经被扫描过的节点,灰色代表刚刚被扫描过,灰色结点会被放入队列,一个点的所有子节点都被扫描后,该结点由灰色变为黑色,白色节点表示完全没有被扫描过,每个节点至多只能被扫描一次,每个节点都有一个父节点π指向其父节点
      3. 广度优先树:广度优先搜索完之后,包含所有的π点、叶节点和树边构成广度优先树,广度优先树构成的图叫做前驱子图,树中的边称为树边,树会包含所有的结点,但边却不一定
    7. 深度优先搜索BFS(breadth first search):
      1. 特点:每次直驱向深入,直到没有节点可以寻找,然后返回上一级节点,最终没有可退的地方时,扫描结束
  1. 数学知识
  • 排列组合
    1. 全排列:n中挑k个全排列,也即顺序相关;计算方法是,n!/(n-k)!;例如,5个中挑2个全排列,5*4=20
    2. 组合:n中挑k个组合,也即顺序无关;计算方法是,n!/((n-k)!k!);例如,5个中挑2个组合,5*4/2=10

猜你喜欢

转载自blog.csdn.net/jiayuqicz/article/details/88615732