算法考试简单的笔记
基础知识
1、时间复杂度
确定基本操作单元
确定基本操作次数
忽略低阶项和常数项,最高阶(时间度指标)决定了复杂度
如果是两样本量未定,两时间复杂度的和无法化简,确定样本量后才可继续化简时间复杂度表达式
算法和数据样本量本身有关系,按照最差情况估计时间复杂度
2、对数器
1)随机数发生器:利用随机数发生器产生一个数组长度随机的数组
再随机产生数存入数组
2)准备一个绝对正确的方法:只需要保证正确(好写好实现一定正确)
3)大样本测试
3、递归
1)分析复杂度(master公式)
T(N) = aT(N/B)(子过程样本量*子过程发生次数)+ O(n^d)(T(N)内部剩下过程的时间复杂度)
if(log(b,a) > d)-> O(N^log(b,a))
if (log(b,a) = d) -> O(N^d * logN)
if (log(b,a) < d) -> O(N^d)
不用管展开是什么样,N/B子过程样本量从代码中直接体现出来,看源代码那一步就行
Eg1、
归并排序 :
实质:小范围和大范围,不会浪费比较
分成两组分别排序,两次
a = b = 2,排好序后外排,之间复杂度O(N),所以d = 1;
O(N^1LogN)= O(N * logN);
void Merge(int TR[], int SR[], int s, int k, int t) { //将有序的TR2[s..k]和有序的TR2[k+1..t]归并有序到SR[s..t]
int i, j;
i = s;
j = k + 1;
for (; s <= k && j <= t; i++) {
if (TR[s] < TR[j]) {
SR[i] = TR[s];
s++;
}
else {
SR[i] = TR[j];
j++;
}
}
if (s <= k) {
for (; s <= k; s++) {
SR[i] = TR[s];
i++;
}
}
if (j <= t) {
for (; j <= t; j++) {
SR[i] = TR[j];
i++;
}
}
}
void Msort(int SR[], int TR1[], int s, int t) //将SR[s..t]归并排序为TR1[s..t]
{
int k;
int TR2[MAXSIZE + 1];
if (s == t)
TR1[s] = SR[s];
else
{
k = (t + s) / 2;
Msort(SR, TR2, s, k);
Msort(SR, TR2, k + 1, t);
Merge(TR2, TR1, s, k, t);
}
}
void MergeSort(Sqlist* L) {
Msort(L->a, L->a, 1, L->length);
}
利用归并思想:
小和问题和逆序列问题
1)小和问题:一个数组中,每一个左边比当前数小的数累加起来,叫做这个数的小和。求一个数组的小和
归并过程中,分步对比,可以不重不漏,每次merge时都可以将小的一边和大的一边进行比对,分批查找
void Merge(int TR[], int SR[], int s, int k, int t) { //将有序的TR2[s..k]和有序的TR2[k+1..t]归并有序到SR[s..t]
int i, j;
i = s;
j = k + 1;
while(s<k && j<t){
res += TR[i] < TR[j] ?(t - j + 1) * TR[i] : 0; //加一行其余不变即可(目标变为找小和,其余稍作调整)
}
C\C++使用的注意事项
1、使用scanf和printf输入/输出
scanf:
1)除了%c之外,scanf以空格换行等作为结束判断标志,%c可以读入空格
2)使用string.h可以使用getline读入一行数据(其中包括了空格)。
printf:
1)输出格式%md,使得不足m位的int型变量进行 !!右对齐!!输出,高位用空格补齐
2)%xmd,同上,只是在不足m位时,高位补足x而不是再是空格。
3)%.mf,可以让浮点数保留m位小数输出
2、常用math函数
需要在头文件中加入math.h。
1)fabs(double x)
对于double型变量取绝对值。
2)floor(double x) ,ceil(double x)
用于对于double型变量向下取整和向上取整,返回类型为double。
3)sqrt(double x)
用于求double类型的算术平方根。
4)log(double x)
用于求以自然数为底的对数。
5)sin cos tan (double x)
6)asin acos atan(double x)
反函数值
7)round(double x)
用于四舍五入。
3、对于结构体的初始化
构造函数:不需要写返回类型,函数名和结构体相同。
注:如果自己重新定义了构造函数,则不能不经过初始化就定义结构体变量。
可以定义多个构造函数,应用于不同的场合。
struct bign{
int d[1000];
int len;
bign(){
len = 0;
memset(d, 0, sizeof(d));
}
}
基本数学问题
1、最大公约数和最小公倍数
1)最大公约数:
一般使用欧几里得算法来得出最大公约数(辗转相除法)。
使用gcd(a,b)来表示a和b的最大公约数
设a,b均为正整数,则gcd(a, b) = (b, a % b)。
可得求解最大公约数的代码
int gcd(int a, int b){
if(b == 0) return a;
else return gcd(b, a % b);
}
另一种简介写法
int gcd(int a, int b){
return !b ? a : gcd(b, a % b);
}
2)最小公倍数
在得到a,b之间的最大公约数d后,则最大公倍数为ab / d。
2、素数问题
1、素数的判断
只需要判断n是否能被2、3、……、sqr(n)整除即可判断n是否为素数。代码如下
bool is_prime(int n){
if(n <= 1) return false;
int sqr = sqrt(1.0 * n);
for(int i = 2; i < sqr; i++){
if(n % i == 0) return false;
}
return true;
}
2、素数表的获取
从1~n获取所有的素数即为获取素数表。这种方法的时间复杂度为O(n),判断素数的时间复杂度为O(n½),所以总时间复杂度为O(n*n½), 素数表获取代码如下
方法一:
const int maxn = 101 //表长
int prime[maxn],pNum = 0; //prime存放所有素数,pNum存放素数个数
bool p[maxn] = {0}; //p[i] == true 表示i是素数
void Find_Prime(){
for(int i = 0; i < maxn; i++){
if(is_prime(i) == true){
primep[pNum++] = i;
p[i] = true;
}
}
}
方法二:
使用筛法。可以使时间复杂度降为O(nloglogn)。
从小到大枚举每一个数,对每一个素数,筛去它的所有倍数,剩余的就都是素数了,默认知道2是第一个素数。
当从小到大达到某数a时,它未被筛去,则它一定是素数,如果a不是素数,则一定有小于它的素因子在之前已经将它筛去。
代码如下:
const int maxn = 101 //表长
int prime[maxn],pNum = 0; //prime存放所有素数,pNum存放素数个数
bool p[maxn] = {0}; //p[i] == true 表示i是素数
void Find_Prime(){
for(int i = 2; i < maxn; i++){
if(p[i] == true){
prime[pNUm++] = i;
for(int j = i + i; j < maxn; j += i){
p[j] = false;
}
}
}
}
注意问题:
1)1不是素数,第一个素数是2。
2)素数表长度至少要比n大1。
3)在Find_Prime中要注意i < maxn, 而不是i <= maxn (因为表长比n大1)。
4)在main函数中记得调用Find_Prime()函数。
3、质因子分解问题
将一个正整数n写成一个或多个质数乘积的形式。不妨先打印素数表
1)每一个质因子可能不止出现了一次,所以不妨定义结构体factor。
struct factor{
int x, cnt //x为质因子,cnt为个数。
}fac[10];
定义数组长度为10因为前十个素数之积已经超过了int范围。
2)已知:
对于一个正整数n来说,如果他存在[2, n]范围内的质因子,要么这些质因子全部小于等于sqrt(n),要么只存在一个大于sqrt(n)的质因子,其余均小于sqrt(n)。
得判断方法代码如下:
if(n % prime[i] == 0){
fac[num].x = prime[i];
fac[num].cnt = 0;
while(n % prime[i] == 0){
fac[num].cnt++;
n /= prime[i];
}
num++;
}
if(n != 1){ //无法被根号内的质因子除尽,则剩余的n就是最后一个大于sqrt(n)的质因子
fac[num].x = n;
fac[num].cnt == 1;
}
暴力递归和动态规划
递归和动态规划
暴力递归:1、把问题转化为规模缩小了的同类问题的子问题
2、有明确的不需要继续进行递归的条件(base case)
3、有当得到了子问题的结果之后的决策过程,不记录每一个子问题的解。
Eg1、求n!的结果
Eg2、汉诺塔问题
from(初始) to(目标) help(辅助)
1)1--n-1从from移到help
2)把单独的n移到to
3)把1--n-1从help移到to
Eg3、打印一个字符串所有的子序列包括空。(子序列可以不连续,子串必须连续)
每个位置要与不要的两种选择。
每个子过程都是选择两种情况。
递归出口就是来到了题给字符串的末尾。
暴力递归—————>动态规划
Eg1、一个矩阵从左上角到右下角的最短路径和,只能走右边或者走下边。
到达右下角就是出口。
暴力递归:找出口,分支,直接暴力得出。
递归展开过程中有重复状态,则可把暴力递归改为动态规划。(无后效性问题,状态和到达状态的递归路 径无关,之和状态到结果有关)
建立一个二维表dp,其中每一个值代表该点到目标点的最短距离
1)找到需要的位置。
2)回到base case中,找到不被依赖(比如最后一行和最后一列的值在上题目中是可以直接得出的)。
3)找到一般点的规律。
可变参数的变化范围来构建dp二维表。
栈和队列
栈:使用一个栈顶指针和一个size即可
队列:两个指针,一个头一个尾。一个size变量。
用队列结构实现栈结构:
使用两个队列,前面一堆数去另一个队列,剩一个元素返回,重复操作即可达到效果。
while(data.size() > 1){
help.push(data.pop());
}
res = data.push();
return res;
用栈结构实现队列结构:
使用两个栈结构,push栈和pop栈。新数永远进push栈,给出数永远从pop栈出。
push栈中东西进入pop栈即可改变顺序。
注!:push栈倒入pop栈时:1)pop栈一定为空
2)push栈一定倒完
二叉树
二叉树
访问结点的顺序是固定的,先中后序遍历只是打印的位置不同
1、遍历
1)先序[非递归]
使用一个栈,放入头结点。
有右孩子先压右,后压左。
使用栈而不用队列:要回到上一层。
2)中序[非递归]
使用一个栈,先放入头结点
while(栈不空或头结点指向空)
当前结点为空,从栈中拿一个,然后向右(拿出的那个结点的右)
当前节点不为空,当前结点压入栈,然后向左
2)后序[非递归]
使用两个栈,使用”中右左”这种类似先序的方法,将结果存入栈中
打印辅助战绩就会得到左右中的结果,即为后序遍历。
2、找到一个结点的后继结点(遍历结果中的后继,中序遍历才是后继)
` 一个结点有右子树,他的后继结点就是右子树的最左的结点。
没有右子树,找到父节点,如果该结点x是父节点的右孩子,则继续往上,直到有一个结点m是他的父节点n的左孩子停止,n就为x的后继。
反过来的逻辑即可找前驱。
3、二叉树的序列化和反序列化
序列化:保存下来二叉树的结构。
反序列化:将内存的东西读出来重构数。
1)先序方式序列化:遇到节点,存结点值加下划线,遇到空加#加下划线。
2)先序方式反序列化:按照先序的顺序恢复。
4、平衡二叉树
判断一个树是否为平衡二叉树需要四个信息:1)左子树是否平衡 2)右子树是否平衡
3)左子树的高度 4)右子树的高度
得到递归返回结构需要的东西。
高度为左子树高度和右子树高度的最大值+1。
5、树的求解套路
递归:1)先分析可能性
2)设计递归函数,函数需要收集的信息
3)根据子过程提供的信息和自身加工的信息返回给上一层。
4)结构必须完全一致。
6、搜索二叉树
左子树比他小,右子树比他大。
中序遍历是有序的就是搜索二叉树。
判断种插入顺序是否为同一个搜索二叉树:
方法一:根节点(第一个拿出,比他大的放一堆,比他小的放一堆,对比序列(递归方式))
方法二:建一棵树
7、完全二叉树
判断是否为完全二叉树
使用层序遍历
1)遍历过程中,任一个结点若有右孩子无左孩子直接返回false。
2)如果有一个节点有左无右或左右都无,则其后序所有结点都必须为叶结点。(添加一个标志位)
已知一颗完全二叉树,求其结点的个数 时间复杂度低于O(N) -->不能遍历所有结点
满二叉树的高度为L,则其结点个数为(2^L)- 1
先向左走到头,找到树的高度,然后判断右树的左边界是否到最后一层
没到:右子树是满的,高度少一层
到了:左子树是满的,高度就是所找到的树的高度。
1<<x = 2^x
时间复杂度为O(logN^2)
经典快排和堆排
1、荷兰国旗问题
给定一个数组和一个数字num,小于num放左边,大于num放右边,等于num放中间。
数组分三个区域,小于,等于,大于区域。(0—less小于num,more—L-1大于num),循环跳出(cur = more)
2、快排
1)经典快排
每次搞定一个位置的数,每次讲大于x,小于x放在右边和左边,x即找到了自己的位置。(相当于等于区域为1)
问题:小于和大于区域不对称,总拿最后一个数划分,和数据本身状况有关系。
2)使用荷兰国旗问题的方法
有相同值时,可以减少次数,等于区域不再进行递归排序。
3)随机快排 额外空间复杂度(O(logN))
随机选一个数,和最后一个数交换,让随机选的数去进行分割。(长期期望时间复杂度O(N*logN))
3、堆排序
完全二叉树(从左往右依次补齐,中间不能有空) 数组结构
1)大顶堆和小顶堆(堆就是完全二叉树) 建立一个大顶堆 O(N)
最大值 / 最小值是根节点。
heapinsert():
while (arr[index] > arr[index - 1] / 2){
swap();
index = (index - 1) / 2;
heapadjust():
}
从上到下,和孩子结点比较,直到越界之前。
2)应用举例:
对于一个不断给出数的数流,可随时接受数的同时找出已有数据中的中位数。
可建立两个堆,大顶堆收集较小的 N / 2 , 小顶堆收集较大的 N / 2。
排序算法的稳定性
1、排序算法的稳定性
稳定性:能否保证原始的相对次序保持不变(相同值在原始数据中的位置)
看相同的值是否会因为排序而导致顺序改变
稳定性的原因:可以保证在以不同元素值进行排序时,可以保存上次排序(以不同的元素)留下来的记录。
堆排序,快速排序无法保证稳定性
归并排序,冒泡排序,插入排序可以做到稳定性
2、工程中排序算法的使用(从稳定性出发)
1)基础类型(整型,字符型……):使用快排(大数组)
2)使用自己定义的结构中的某个元素:使用归并(同大数组)
3)当小数组的时候使用插入排序(因为常数时间的操作低)
3、有关排序问题的补充
1)归并排序的额外空间复杂度可以变为O(1),但是非常难。
2)快速排序可以做到稳定性问题,但是非常难
4、比较器的使用
p235 sort函数使用。cmp。
5、桶排序,计数排序、基数排序
1)非基于比较的排序,与被排序样本的实际数据状况很有关系,所以实际中并不常使用。
2)稳定的排序
3)时间复杂度O(N),额外空间复杂度O(N)
eg1、给定一个数组,求如果排序之后,相邻两数的最大差值,要求时间复杂度O(N),且不能使用非基于比较的排序
思路:1、使用N+1个桶,给定数组中的最大值和最小值的差值等分为N+1份。
2、将数值按照范围进行放置,但每个桶中只记录最大值和最小值。(因为一定有 一空桶,所以最大差值一定不可能在同一个范围内部产生,但不代表最大值一定在空桶两侧产生)
3、遍历记录相邻非空桶之间最大值和最小值的差值,遍历结束后最大值者即为所求。整体时间复杂度满足题目要求。
矩阵和链表问题
矩阵问题
1、循环打印矩阵
找顶点,打印一个正方形。打印完后左上角向右下角移动一个,得到新的左上角顶点。右下角向左上角移动一个,得到新的右下角顶点,再次打印一圈正方形。
打印时候的边界:两顶点为一行或者一列
循环时候的边界:左上角的行和列大于右下角。
矩阵(正方形)(长方形不能转)的旋转:和上例相同,有一样的思路。
2、之字形打印矩阵
两顶点A,B,A向右走,直到边界向下;B向下走,直到边界向右。之间过程,每组A,B都是一个对角线,只需要打印对角线元素即可。
加一个量用于判断从上往下打印还是从下往上打印。
3、在一个行和列均有序的矩阵中找一个数
比当前数大往左走,比当前数小往下走。从右上角开始。
比当前数小往上走,比当前数大往右走。从左下角开始。
4、最优解可以从数据状况和题目描述出发。
5、打印两个有序链表的公共部分(和外排类似)
链表问题
1、判断一个链表是否为回文结构
若可以使用额外空间:1)遍历过程中把所有节点放入栈中,第二次遍历从栈中取出元素和链表从头开始遍历进行比对。
2)或者只把后半段逆序和前半段比较,使用一个一步一跳的慢指针和一步两跳的快指针,找中点。
不使用辅助空间:快指针一次两步,慢指针一次一步,慢指针走完快指针走到中点。
2、将单向链表按照某值划分为左边小、中间相等,右边大的形式。
若可以使用额外空间:1)使用一个存放结点的数组,用数组对其重新排列连接。
不使用额外空间:三个区域,一个大于一个等于一个小于,相当于把一个大链表拆分成三个小链表,然后重连。
3、判断单链表是否有环
利用HASH表:看下一个结点是否加入过hashmap。直到next为空。
不使用HASH表:准备两个指针,一个快指针(一次两步)一个慢指针(一次一步),如果有环,快慢指针一定会在环上相遇,此时快指针回到头结点,然后快慢
指针一次走一步,一定会在第一个入环结点相遇。
4、判断两链表是否相交 (两链表都无环)
利用HASH表:把第一个head1的所有节点入hash,然后遍历链表二结点是否出现在hash。(这个方法同样适用于有环结点)
不使用HASH表:分别遍历第一个和第二个链表,得到四个量分别是表一的长度和尾结点,表二的长度和尾结点。 然后对比两者尾结点的地址是否相同,不相同则不想交,相同则相交但未必是第一个交点。 长度长的先走,然后一起走,直到走到相同的节点。返回即可。
并查集
1)查询两个元素是否在一个集合
2)合并两个集合(少元素的挂在多元素的底下)
优化:
任何一次查找代表结点的过程中,查找完成后改写结构,使得所有结点都直接连向代表结点。
只有头结点的size有意义,被合并后成为了子结点,其size也就失去了使用价值
结构:使用map。
Eg. 岛问题
一个矩阵中只有 0 1两种值,每个位置都可以和自己的上、下、左、右四个位置相连,如果有一片1连在一起,这个部分叫做一个岛,求一个矩阵中有多少个岛
eg 0 0 1 0 1 0
1 1 1 0 1 0
1 0 0 1 0 0
0 0 0 0 0 0
方法1:深度搜索,相邻的1被感染。
方法2(并行计算):
1)记录边界的感染源
2)合并过程中比较边界相邻1的感染源,不同的话合并并查集,然后岛的数量-1。