时间复杂度
阶乘递归的时间复杂度
斐波那契的时间复杂度
每次递归中仅仅是比较,没有循环,故每次递归调用次数为1
x为最低端时缺的次数,但可忽略不计(右侧n-1,n-2…会提前到达n-1)
等比数列求和公式:Sn=a1(1-q^n)/(1-q)(q≠1)
故最终的总次数为2^n-1
提示:有些算法在往下走的流程中,我们不能仅仅看它的实现,需要画图分析
空间复杂度
实例一
进入第一个for循环时,为end创建一个空间
进入第二个for循环时,为i创建一个空间。但出循环后,i的临时空间被销毁。当再次进入第二个循环时,会在原来销毁的空间上重新创建i
所以额外使用了两个空间
最终空间复杂度为o(1)–最终空间复杂度为常数个
实例三
实例八的空间复杂度
空间是可以重复利用的,时间是一去不复返的
栈帧在空间上利用是这样的:计算n的斐波那契额时,要计算n-1和n-2的斐波拉那。但是系统选择先计算n-1这一支的数,往下递归也是如此。当递归回归时,系统会使用重复的空间。
故空间复杂度为O(N)–即递归的深度
常见复杂度对比
常数阶与线性阶速度最快,2^n与n的阶乘速度较慢
习题
1.消失的数值
我的代码:
写到一半突然发现,要先进行排序才能进行一个个比较,不符合要求
老师思路:
思路四:
1.异或运算的性质:异或运算(^)具有以下性质:
任何数与0异或都等于其本身:a ^ 0 = a
任何数与自己异或都等于0:a ^ a = 0
这里的关键是,由于异或运算满足交换律和结合律,因此对于数组中缺失的整数2,它会与0到numsize - 1的整数以及数组中的其他元素相抵消。具体地说:
2与2异或等于0:2 ^ 2 = 0
2与其他整数i(0到4)异或等于i:2 ^ i = i
2与数组中的其他元素异或等于元素本身:2 ^ element = element
因此,2与其他整数和数组中的元素异或后都会得到0,而0与任何数异或都等于那个数本身。因此,最终的结果将是缺失的整数2。
2.旋转数组
思路二:
将旋转的部分和不动的部分分别拷贝到一个新开辟的数组中去,再将新的数组拷贝回原来数组中。
思路三代码:
顺序表和链表
驼峰法
定义变量:
1.PushBack
2.push_back
顺序表
1.日后想要数据表存储字符或其他类型变量,建议将类型名重定义,避免日后大范围修改
2.结构体名字太长,也重定义一下
3.大型程序写好一个功能后就立马测试,避免写完后到处调bug
增容分析
情况三处理:
情况一和二的处理:
空间不够时有两种情况,第一是刚初始化时,空间为无
第二是size和capacity相等时,空间不够
为避免反复扩容的情况,选择每次扩容时将容量翻倍
注意:当指针为空时,realloc的作用等价于malloc
注意:代码要写一点测一点,不然很容易出错!!
数组越界了编译器检查不出来,只有在清除内存的时候才会报错
检查越界的方式:
第一种:
第二种:
头插越界
头插在使用过程中,超出容量时不会报错,但free后会报错
将检查容量的代码封装成函数
插入函数复用
尾插重定义
头插重定义
顺序表缺点
1.空间不够了需要增容,增容对性能有损耗。若动态内存后的空间足够,可以原地扩,影响不大。若空间不够,新开辟一块空间再把数据拷贝进去就会比较麻烦。
2.为避免频繁扩容,容量满时选择空间扩大两倍,这会导致一定的内存浪费。
3.顺序表要求数据从开始位置连续存储,当选择在头部或者中间插入时,需要连续挪动数字,效率不高
顺序表习题
1.删除有序数组中的重复项
分析:
用三指针实现:
1.dst指针指向去重后数字的位置
2.i,j指针用于比较数据的异同。
3.去重完成后i的位置放到j的位置,j重新开始寻找
单链表
创建节点
即便为空链表,函数也没有问题
链表的打印函数
链表的尾插
1.找到尾巴
2.让原尾巴指向新尾巴
对应的主函数
函数实现
错误分析
要明白类比的思想。实参是int*,需要传递给int**并解引用才能改变形参
头删与尾删
头删必定需要传入二级指针修改
尾删平时不需要,等到将链表删空时就需要
1.tail->next:整型、指针都可做逻辑判断,它们会被隐式类型转换为逻辑判断的值。0(空指针)就是假,非0就是真
2.这样的写法有什么问题?
free将节点还给系统
tail(d2)内还有指向d3的地址,会造成野指针
free后,2节点的next仍指向原先尾部的地址
单链表的缺点:
知道当前节点的地址,可以找下一个节点,但找不到上一个节点地址
解决办法1:
每次走之前,先把当前位置的地址保存一下
解决办法2:
少定义一个变量
两种方法都存在的问题:当只剩下一个节点时
链表存在多个相同的数据该如何查找?
反转单链表
我的实现:
思路
实现思路:
1.原地翻转
2.头插法
3.递归(若链表过长会导致栈溢出)
1.原地翻转
要再创建一个变量n3,便于n2修改方向后的移动
结束条件:n2为空
2.头插法
寻找中间节点
定义快慢节点
快节点一次走两步,慢节点一次走一步
链表的个数为奇数时,当fast走到链表的结尾时,slow位于链表的中间
链表的个数为偶数时,当fast走到倒数第二个元素时,slow为于链表中间第一个元素。当fast走到链表最后的NULL指针时,slow为于链表中间第二个元素
寻找倒数第k个节点
找倒数第k个节点,就是找正数n-k个节点
依旧定义快慢节点(不同于上小节)
先让fast走k个位置,再让fast与slow同时走,直到fast走到链表最后的NULL指针
对链表进行冒泡排序(我写的!!)
思路:创建四个指针,max,min,point,head
max,min,point指向原链表的首元素,point会不断迭代指向下一个指针。head为新开辟的头节点,data沿用原链表第一个节点的值
当point指向的节点的元素大于max的值,或者小于min的值时,更新两个指针的指向,并将新的min值头插head链表,新的max值尾插head链表。当point指向的值介于最大与最小值,会遍历head链表,将point的值放置在合适的位置。
//用结构体创建节点
SLTNode* BuyListNode1(SLTNode* head)
{
SLTNode* newnode= (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
printf("内存申请失败");
exit(-1);
}
newnode->next = NULL;
newnode->data = head->data;
return newnode;
}
//用结构体创建尾插
void SListPushBack1(SLTNode* PPhead, SLTNode** head) {
SLTNode* tail = *head;
while (tail->next != NULL) {
tail = tail->next;
}
tail->next = BuyListNode1(PPhead);
}
void SListmaopao(SLTNode** Phead)
{
SLTNode* max, * min,*point;
max = min = point = *Phead;
SLTNode* head = BuyListNode1(point);
while (point) {
point = point->next;
if (point == NULL)
goto S;
if (point->data >= max->data) {
max = point;
SListPushBack1(max, &head);
}
else if (point->data <= min->data) {
min = point;
SLTNode* newhead = BuyListNode1(min);
newhead->next = head;
head = newhead;
}
else
{
SLTNode* tail = head;
SLTNode* pre = NULL;
while(tail->next){
pre = tail;
tail = tail->next;
if ((point->data) >= (pre->data) && (point->data) <= (tail->next->data))
{
SLTNode* next = tail->next;
SLTNode* newcode = BuyListNode1(point);
tail->next = newcode;
newcode->next = next;
break;
}
}
}
}
合并两个升序链表
void SListcombine(SLTNode* L1, SLTNode* L2)
{
if (L1 == NULL)
{
SListPrint(L2);
}
if (L2 == NULL)
{
SListPrint(L1);
}
SLTNode* tail = NULL, *head = NULL;
while (L1 && L2)
{
if (L1->data < L2->data)
{
if (head == NULL)
{
head = tail = L1;
}
else
{
tail->next = L1;
tail = L1;
}
L1 = L1->next;
}
else
{
if (head == NULL)
{
head = tail = L2;
}
else
{
tail->next = L2;
tail = L2;
}
L2 = L2->next;
}
}
if (L1)
{
tail->next = L1;
}
if (L2)
{
tail->next = L2;
}
SListPrint(head);
}
带头或不带头的链表
带头链表的特点
当带头链表头插入一个新节点时,只需要更改哨兵位指向的下一个节点,不需要更改plist,因此不需要使用二级指针
合并两个升序链表的改进
注意不要打印head,结果会有哨兵位的随机值
注意结尾要释放哨兵位的空间
链表的分割
思路:创建两个链表,对原链表进行遍历。小于特定值的尾插放入less链表中,大于特定值的尾插放入greater链表中。最终将两个链表的值链接起来
两个链表都定义哨兵位,方便进行尾插。再针对两个链表定义两个尾指针
隐患:大于x值的链表的最后一个值可能还链接着第一个链表中的某个值,形成死循环
链表的回文结构
将链表的原始值刻录在数组中,逆序链表后,再遍历链表和数组一一比较
链表的相交
我的想法:让两个链表同时开始走,判断它们的next地址是否相同,直到某一方的地址为空。
忽略的地方:两个链表长度不相同
abs函数返回差值的绝对值
假设a长b短
下面if语句判断假设是否正确
环形链表
不能用正常方法去遍历,否则会死循环(用判断下一节点等于该节点的方法也不行,万一环的长度很长?)
列举
右边的带环链表也称循环链表
判断是否带环
定义快慢指针
延伸问题:
1证明:
2证明:
若fast一次走4步,则距离每次-3。若n为3的倍数,就能追上。n不为3的倍数时,出现距离变成-1,-2…的情况。还需判断c-1,c-2…是否为3的倍数,如果不是就追不上
如何求环的入口点?
1.实现方法一
证明:
快指针只需走C+L+X说法错误,存在两大变量:L,C。若L大C小,可能在slow进入环之前,fast已经绕环多圈。若L小C大,可能在slow进入环之前,fast还未绕环走完一圈。
正确公式:
当环很大时,fast最多在环内多走一圈。L = C-X
当环很小时,fast在环内多走很多圈。L = (N-1)*C+C-X
故当head从头开始走,另一个指针从meetNode开始走,必定在入口点相遇(C-X)
代码实现:
2.实现方法二(容易理解,实现比较复杂)
找到相遇点后,定义相遇点的下一个节点为list,将相遇点指向空。让head指针与list指针从头开始走到相遇点,期间它们的交点就是入口点
复杂链表的复制
测试用例:每个矩形中,右边的下标代表节点所处的位置
难点:ramdom如何指向正确的节点(eg.13节点内的random不知道7的相对位置在哪儿?)
重点逻辑:新复制的13节点如何复制romdom?就是原来13的ramdom的next指向的节点!
ramdom链接好以后,把新节点与旧节点的链接清除,重新链接成新的链表
代码实现:
插入节点逻辑:
重新插入的代码
重新链接节点:
双向链表
双向链表哨兵节点设置
既是第一个也是最后一个,两个指针都指向自己
(不需要传二级指针,已经设置过哨兵节点了)
初始化与尾插
要改结构体的内容,需要传结构体的指针。要改结构体的指针,需传结构体指针的指针
打印
因为链表不指向NULL,从phead的下一个节点开始遍历,当指针指向phead的时候结束。
双向循环链表的结构优势
双向链表只需要实现两个函数,就可以进行头中尾的增删
双向循环链表的消除
若传一级指针,可能存在野指针的隐患
顺序表与链表的区别
顺序表在头部或中部插入需要挪动数据
若realloc函数后有足够的内存,消耗不大。若空间不够,需要重新开辟一整块空间,并把数据复制上去,消耗较大
计算机的存储体系
本地磁盘往上都是带电存储
从物理上讲,顺序表是连续的内存,链表是多块不连续的空间
cpu通过汇编语言产生的机械码会通过三级缓存结构访问内存中的数据。
链表不仅仅缓存命中率更低,还可能造成缓存污染(把没有用的内容放进珍贵的内存中)
拓展:与程序员相关的cpu缓存知识