(注:算法那一块儿结合机考题复习,可以直接粘贴 OI wiki 的讲解)
一、绪论
1. 复杂度
算法中所有语句的频度之和记为 T(n),算法中最深层循环内的语句的频度与 T(n) 同数量级,因此通常采用算法中基本运算的频度 f(n) 来分析算法的时间复杂度,记为 T(n)=O(f(n))
算法的空间复杂度 S(n) 定义为该算法所耗费的存储空间,它是问题规模 n 的函数,记为 S(n)=O( g(n) )。
一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外,还需要计算所需的辅助空间。
2. 数的存储结构
(1)顺序存储。 把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
(2)链式存储。 不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
(3)索引存储。 在存储信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)。
(4)散列存储。 根据元素的关键字直接计算出该元素的存储地址,又称哈希 (Hash) 存储。
3. 用循环比递归的效率高吗?
循环和递归两者是可以互换的,不能决定性的说循环的效率比递归高。
缺点是:当递归调用的次数较多时,要增加额外的堆栈处理,有可能产生堆栈溢出的情况,对执行效率有一定的影响。
缺点:它并不能解决全部问题,有的问题适合于用递归来解决不适合用循环。
4. 常见算法
做出在当前看来是最好的结果,它不从整体上加以考虑,也就是局部最优解。
贪心算法从上往下一步一步最优,能否保证全局最优解取决于贪心策略。
是把问题分解成子问题,这些子问题可能有重复,可以记录前面子问题的结果防止重复计算。前一个子问题的解对后一个子问题产生一定的影响。
在求解子问题的过程中保留局部最优解,直到解决最后一个问题时也就是初始问题的解。
5. 其他
二、 线性表
1. 顺序表和链表的比较
顺序表可以顺序存取,也可以随机存取;链表只能从表头顺序存取元素。
采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。
而采用链式存储时,逻辑上相邻的元素,物理存储位置则不一定相邻,对应的逻辑关系是通过指针链接来表示的。
对于按值查找,顺序表无序时,两者的时间复杂度均为 O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为 O(log2n)。
对于按序号查找,顺序表支持随机访问,时间复杂度仅为 O(1),而链表的平均时间复杂度为 O(n)。
顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需修改相关结点的指针域即可。
- 顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。
- 预先分配过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。
- 动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。
(2)链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。
三. 栈和队列
1. 栈和队列的区别
对于进入队列的元素按“先进先出”的规则处理,在表头进行删除在表尾进行插入。
由于队列要进行频繁的插入和删除,一般为了高效,选择用定长数组来存储队列元素,在对队列进行操作之前要判断队列是否为空或是否已满。如果想要动态长度也可以用链表来存储队列,这时要记住队头和对位指针的地址。
对于插入到栈的元素按“后进先出”的规则处理,插入和删除操作都在栈顶进行,与队列类似一般用定长数组存储栈元素。由于进栈和出栈都是在栈顶进行,因此要有一个size变量来记录当前栈的大小,当进栈时size不能超过数组长度。
2. 共享栈
利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。这样能够更有效的利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。
3. 队列在计算机系统中的应用
以主机和打印机之间速度不匹配的问题为例做简要说明。主机输出数据给打印机打印,输出数据的速度比打印数据的速度要快得多,由于速度不匹配,若直接把输出的数据送给打印机打印显然是不行的。解决的方法是设置一个打印数据缓冲区,主机把要打印输出的数据依次写入这个缓冲区,写满后就暂停输出,转去做其他的事情。打印机就从缓冲区中按照先进先出的原则依次取出数据并打印,打印完后再向主机发出请求。主机接到请求后再向缓冲区写入打印数据。这样做既保证了打印数据的正确,又使主机提高了效率。由此可见,打印数据缓冲区中所存储的数据就是一个队列。
(2)第二个方面是解决由多用户引起的资源竞争问题。在一个带有多终端的计算机系统上,有多个用户需要 CPU 各自运行自己的程序,它们分别通过各自的终端向操作系统提出占用 CPU 的请求。操作系统通常按照每个请求在时间上的先后顺序,把它们排成一个队列,每次把CPU分配给队首请求的用户使用。当相应的程序运行结束或用完规定的时间间隔后,令其出队,再把CPU分配给新的队首请求的用户使用。这样既能满足每个用户的请求,又使CPU能够正常运行。
4. 矩阵的压缩存储
数据结构中,提供针对某些特殊矩阵的压缩存储结构。这里所说的特殊矩阵,主要分为以下两类:
针对以上两类矩阵,数据结构的压缩存储思想是: 矩阵中的相同数据元素(包括元素0)只存储一个。
5. 思考问题
(1)设计一个栈,使它可以在 O(1) 的时间复杂度实现 Push, Pop 和 min 操作。
使用两个栈。一个栈是常规的栈,用来模拟 Pop 和 Push 操作;另一个栈用来存储最小值,当遇到 push 操作时,新数字小于栈顶元素才入栈。
四、串
子串的定位操作通常称为串的模式匹配,他求的是子串(常称模式串)在主串中的位置。
从主串的第一个字符起,与子串的第一个字符比较,相等则继续比较;不等则从主串的下一个位置起,继续和子串开始比较,直到最后看是否匹配成功。
如果已匹配相等的前缀序列中有某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,主串 i 指针无须回溯,并继续从该位置开始进行比较。而模式向后滑动位数的计算仅与模式本身的结构有关,与主串无关。
五、树与二叉树
1. 树与二叉树的相关概念
在树的结构中,每个节点都只有一个前继称为父节点,没有前继的节点为树的根节点\;
每个节点可以有多个后继成为节点的子节点,没有后继的节点称为叶子节点。
二叉树是另一种树形结构,其特点是每个结点至多只有两棵子树,并且二叉树的子树有左右之分,其次序不能任意颠倒。
2. 如何由遍历序列构造一棵二叉树
在先序遍历序列中,第一个结点一定是二叉树的根结点;而在中序遍历中,根结点必然将中 序序列分割成两个子序列,前一个子序列是根结点的左子树的中序序列,后一个子序列是根结点的右子树的中序序列。
根据这两个子序列,在先序序列中找到对应的左子序列和右子序列。(这个好确定,知道中序遍历的左子树了,那就查一查左子树大小m,然后先序遍历紧跟的m个就是左子树)
在先序序列中,左子序列的第一个结点是左子树的根结点,右子序列的第一个结点是右子树的根结点。如此递归地进行下去,便能唯一地确定这棵二叉树。
例题:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
因为后序序列的最后一个结点就如同先序序列的第一个结点,可以将中序序列分割成两个子序列,然后采用类似的方法递归地进行划分,进而得到一棵二叉树。
需要注意的是,若只知道二叉树的先序序列和后序序列,则无法唯一确定一棵二叉树。
3. 线索二叉树
对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
4. 树的存储结构
这种存储方式采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置。
孩子表示法是将每个结点的孩子结点都用单链表链接起来形成一个线性结构,此时n个结点就有n个孩子链表(叶子结点的孩子链表为空表)
孩子兄弟表示法又称二叉树表示法,即以二叉链表作为树的存储结构。孩子兄弟表示法使每个结点包括三部分内容:结点值、指向结点第一个孩子结点的指针,及指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)
5. 二叉排序树
二叉排序树(也称二叉查找树)或者是一棵空树,或者是具有下列特性的二叉树:
1) 若左子树非空,则左子树上所有结点的值均小于根结点的值。
2) 若右子树非空,则右子树上所有结点的值均大于根结点的值。
根据二叉排序树的定义,左子树结点值<根结点值<右子树结点值,所以对二叉排序树进行中序遍历,可以得到一个递增的有序序列。
二叉排序树的查找是从根节点开始的,延某个分支逐层向下比较的过程。若二叉树非空,先将给定值与根结点的关键字比较,若相等,则查找成功;若不等,如果小于根结点的关键字,则在根结点的左子树上查找,否则在根的右子树上查找。这显然是一个递归的过程。
6. 平衡二叉树
为避免树的高度增长过快,降低二叉排序树的性能,规定在插入和删除二叉树结点时,要保证任意结点的左、右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树(Balanced Binary Tree),简称平衡树。
定义结点左子树与右子树的高度差为该结点的平衡因子,则平衡二叉树结点的平衡因子的值只可能是-1、0或1。因此,平衡二叉树可定义为或者是一棵空树,或者是具有下列性质的二叉树:它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度差的绝对值不超过1。
如果再一个平衡二叉树中插入一个节点可能造成失衡,这时就要进行树结构的调整,即平衡旋转。包括4中情况:在左子树的左子树上插入节点时向右进行单向旋转;在右子树的右子树上插入节点时向左进行单向旋转;在左子树的右子树插入节点时先向左旋转再向右旋转;在右子树的左子树插入节点时先向右旋转再向左旋转。
7. 哈夫曼树和哈夫曼编码
在数据通信中,若对每个字符用相等长度的二进制位表示,称这种编码方式为固定长度编码。若允许对不同字符用不等长的二进制位表示,则这种编码方式称为可变长度编码。
可变长度编码比固定长度编码要好得多,因为对频率高的字符赋以短编码,而对频率较低的字符则赋以较长的编码,从而可以使字符的平均编码长度减短,起到压缩数据的效果。
哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码。若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。由哈夫曼树得到哈夫曼编码是很自然的过程。首先,将每个出现的字符当作一个独立的结点,其权值为它出现的频度(或次数),构造出对应的哈夫曼树。显然,所有字符结点都出现在叶结点中。我们可将字符的编码解释为从根至该字符的路径上边标记的序列,其中边标记为0示“转向左孩子”,标记为1表示“转向右孩子”。
六、图
1. 图的存储结构
其实就是,每个结点作为头节点,把所有与该结点相连的结点依次链接起来。
其实就是,顶点作为链表头节点,把连着这个点的所有边依次链接起来。
2. 图的遍历
3. 最小生成树
用来求最小生成树,其基本思想为:从联通网络N={V,E}中某一顶点u0出发,选择与他关联的最小权值的边,将其顶点加入到顶点集S中,此后就从一个顶点在S集中,另一个顶点不在S集中的所有顶点中选择出权值最小的边,把对应顶点加入到S集中,直到所有的顶点都加入到S集中为止。
用来求最小生成树,其基本思想为:设有一个有N个顶点的联通网络N={V,E},初试时建立一个只有N个顶点,没有边的非连通图T,T中每个顶点都看作是一个联通分支,从边集E中选择出权值最小的边且该边的两个端点不在一个联通分支中,则把该边加入到T中,否则就再从新选择一条权值最小的边,直到所有的顶点都在一个联通分支中为止。
4. 最短路径
(1)迪杰斯特拉(dijkstra)算法:
(2)弗洛伊德(Floyd)算法:
(3)Bellman-ford算法
5. 关键路径
(3)重复以上步骤直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
AOE网:在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销。
关键路径:从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。
七、查找
1. 对各种查找方法的概括
查找分为静态查找表和动态查找表;静态查找表包括:顺序查找、折半查找、分块查找;动态查找包括:二叉排序树和平衡二叉树。
(1)顺序查找:把待查关键字key放入哨兵位置(i=0),再从后往前依次把表中元素和key比较,如果返回值为0则查找失败,表中没有这个key值,如果返回值为元素的位置i(i!=0)则查找成功,设置哨兵的位置是为了加快执行速度。他的时间效率为O(n),其特点是:结构简单,对顺序结构和连式结构都适用,但是查找效率太低。
(2)折半查找:要求查找表为顺序存储结构并且有序,若关键字在表中则返回关键字的位置,若关键字不在表中时停止查找的典型标志是:查找范围的上界<=查找范围的下界。
(3)分块查找:先把查找表分为若干子表,要求每个子表的元素都要比他后面的子表的元素小,也就是保证块间是有序的(但是子表内不一定有序),把各子表中的最大关键字构成一张索引表,表中还包含各子表的起始地址。他的特点是:块间有序,块内无序,查找时块间进行索引查找,块内进行顺序查找。
(4)二叉排序树:二叉排序树的定义为:或者是一棵空树,或者是一棵具有如下特点的树:如果该树有左子树,则其左子树的所有节点值小于根的值;若该树有右子树,则其右子树的所有节点值均大于根的值;其左右子树也分别为二叉排序树。在查找时可以进行动态的插入,插入节点要符合二叉排序树的定义,这也是动态查找和静态查找的区别,静态查找不能进行动态插入。
(5)平衡二叉树:平衡二叉树又称为AVL树,它或者是一棵空树或者具有如下特点:他的左子树和右子树的高度差的绝对值不能大于1,且他的左右子树也都是平衡二叉树。
2. B树和B+树
B树
B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶B树或为空树,或为满足如下特性的m叉树:
(1)树中每个结点至多有m棵子树,即至多含有m-1个关键字。
(3)除根结点外的所有非叶结点至少有「m/2]棵子树,即至少含有「m/2]-1个关键字。
(4)所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似千折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
B+树
B+树是应数据库所需而出现的一种B树的变形树。一棵m阶的B+树需满足下列条件:
(2)非叶根结点至少有两棵子树,其他每个分支结点至少有「m/2]棵子树。(向上取整)
(4)所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来。
(5)所有分支结点(可视为索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针。
m阶的B+树与m阶的B树的主要差异
(1)关键字对应子树个数不同。在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个关键字的结点含有n+1棵子树。
(2)结点包含的关键字个数不同。在B+树中,每个结点(非根内部结点)的关键字个数n的范围是「m/2]<=n<=m(根结点:1<=n<=m);在B树中,每个结点(非根内部结点)的关键字个数n的范围是「m/2]-1<=n<=m-1。
(3)在B+树中,叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。B树非叶节点存储关键字及其信息,非叶节点不带信息。
(4)在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中;而在B树中,结点包含的关键字都是不重复的
3. 哈希表
哈希表又称为散列表,是根据关键字码的值直接进行访问的数据结构,即它通过把关键码的值映射到表中的一个位置以加快查找速度,其中映射函数叫做散列函数,存放记录的数组叫做散列表。
哈希函数的构造方法:直接定址法,除留余数法,数字分析法,平方取中法,折叠法,随机数法
(1)直接定址法:取关键字的某个线性函数值作为散列地址,H(key)=a*key+b。
(2)除留余数法:取关键字对p取余的值作为散列地址,其中p<m,即H(key)=key%p(p<m)。
(3)数字分析法:当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列的地址,适用于所有关键字都已知的情况。
(4)平方取中法:对关键字求平方,再取结果中的中间几位作为散列地址。
(5)折叠法:将关键字分为位数相同的几部分,然后取这几部分的叠加和作为散列地址。适用于关键字位数较多,且关键字中每一位上数字分布大致均匀。
(6)随机数法:选择一个随机函数,把关键字的随机函数值作为散列地址。适合于关键字的长度不相同时。
哈希冲突的解决方法包括:开放定址法和拉链法,当冲突发生时,使用某种探测技术形成一个探测序列,然后沿此序列逐个单单元查找,直到找到该关键字或者碰到一个开放的地址为止,探测到开放的地址表明该表中没有此关键字,若要插入,则探测到开放地址时可将新节点插入该地址单元。其中开放定址法包括:线性探查法,二次探查法,双重散列法。
基本思想,探查时从地址d开始,首先探查T[d],在探查T[d+1]...直到查到T[m-1],此后循环到T[0],T[1]...直到探测到T[d-1]为止。
基本思想,探查时从地址d开始,首先探查T[d],再探查T[d+12],T[d+22]...等,直到探查到有空余地址或者探查到T[d-1]为止,缺点是无法探查到整个散列空间。
基本思想,使用两个散列函数来确定地址,探查时从地址d开始,首先探查T[d],再探查T[d+h1(d)],T[d+2*h1(d)]...
将所有关键字为同义词的节点链接在同一个单链表中,若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组,凡是散列地址为i的节点均插入到头指针为i的单链表中。
八、排序
基本思想为:将序列分为有序部分和无序部分,从无序部分依次选择元素与有序部分比较找到合适的位置,将原来的元素往后移,将元素插入到相应位置上。时间复杂度为:O(n^2),空间复杂度为O(1)
基本思想为:设置三个变量low,high,mid,令mid=(low+high)/2,若a[mid]>key,则令high=mid-1,否则令low=mid+1,直到low>high时停止循环,对序列中的每个元素做以上处理,找到合适位置将其他元素后移进行插入。他的比较次数为O(nlog2n),但是因为要后移,因此时间复杂度为O(n^2),空间复杂度为O(1)。优点是:比较次数大大减少。
基本思想为:先将序列分为若干个子序列,对各子序列进行直接插入排序,等到序列基本有序时再对整个序列进行一次直接插入排序。优点是:让关键字值小的元素能够很快移动到前面,且序列基本有序时进行直接插入排序时间效率会提升很多,空间复杂度为O(1)。
基本思想为:将序列分为2部分,每经过一趟就在无序部分找到一个最小值然后与无序部分的第一个元素交换位置。优点是:实现起来特别简单,缺点是:每一趟只能确定一个元素的位置,时间效率低。时间复杂度为O(n^2),空间复杂度为O(1)。
设有一个任意序列,k1,k2,...,kn,当满足下面特点时称之为堆:让此序列排列成完全二叉树,该树具有以下特点,该树中任意节点均大于或小于其左右孩子,此树的根节点为最大值或者最小值。优点是:对大文件效率明显提高,但对小文件效率不明显。时间复杂度为O(nlog2n),空间复杂度为O(1)。
基本思路为:每一趟都将元素进行两两比较,并且按照“前小后大”的规则进行交换。优点是:每一趟不仅能找到一个最大的元素放到序列后面,而且还把其他元素理顺,如果下一趟排序没有发生交换则可以提前结束排序。时间复杂度为O(n^2),空间复杂度为O(1)。
基本思路为:在序列中任意选择一个元素作为中心,比它大的元素一律向后移动,比它小的元素一律向前移动,形成左右两个子序列,再把子序列按上述操作进行调整,直到所有的子序列中都只有一个元素时序列即为有序。优点是:每一趟不仅能确定一个元素,时间效率较高。时间复杂度为O(nlog2n),空间复杂度为O(log2n)。
基本思想为:把两个或者两个以上的有序表合并成一个新的有序表。时间复杂度为O(nlogn),空间复杂度和待排序的元素个数相同。
时间复杂度为:对于n个记录进行链式基数排序的时间复杂度为O(d(n+rd)),其中每一趟分配的时间复杂度为O(n),回收的时间复杂度为O(rd)。
外部排序是由于信息庞大,无法将整个文件放到内存中进行排序,需要将待排序的记录存储在外存上,排序时候再把数据一部分一部分调入内存进行排序。
九、脑筋急转弯
9.1 数组
(1)空间复杂度O(1):把所有数字加起来,然后用 (1+100)*100/2 减去数字和就是答案
2.给定一个长度为 n 的整数数组 nums,数组中所有的数字都在 0∼n−1 的范围内。找到任意一个重复的数字
空间复杂度O(1):遍历数组,若下标 i 与对应元素 nums[i] 不同,即 nums[i] != i,我们应该把 nums[i] 这个元素交换到正确的位置 nums[i]上。交换前,先判断 nums[i] 与 nums[nums[i]] 这两个元素是否相同,相同说明存在重复元素,直接返回,否则进行 swap 交换。交换过后,我们需要再次判断 i 位置上的元素,因此,我们使用 while 循环。
(1)用map存下来每个数,看看 sum-ai 是否出现过。
先用快速排序算法排序。用双指针,i 表示去重后数组的指针,j 表示原数组指针,每次比较 ai 与 aj 是否相等,相等的话j++;不想等的话令ai=aj,然后i++, j++.
9.2 链表
两个指针,指针 i 一次走两步,指针 j 一次走一步。指针 i 走到链表尾时,指针 j 就在中间节点。
2. 如何证明给定的链表是否包含循环? 如何找到循环的头节点?
两个指针,指针 i 一次走两步,指针 j 一次走一步。指针 i 走到链表尾时,两者没有相遇就说明没有环;如果两个指针相遇说明有环,相遇点就是循环的头结点。
(注:这里说的头结点并不是链与环的交点,而是环上任意一个点)
(1)迭代法:用三个指针,pre代表要当前节点的前驱,suc表示当前节点的后继,now表示当前节点,然后把now的指针指向pre,然后pre = now, now = suc,suc = suc->next 即可。
(2)空间复杂度O(1):用链表快速排序算法现对链表排序,然后就变成数组问题。
这个和高精度加法类似,用两个栈存链表,并记录一个进位标志,然后用头插法把新链表建立出来。
9.3 字符串
9.4 二叉树
9.5 其他
a=a+b;
b=a-b;
a=a^b;
b=a^b;
设第一个矩形范围为 xmin1, xmax1, ymin1, ymax1;第二个矩形范围是 xmin2, xmax2, ymin2, ymax2.
(1)一个矩形完全在另一个矩形的上方:ymin1 >= ymax2 || ymin2 >= ymax1