数据结构与算法

时间复杂度

阶乘递归的时间复杂度

在这里插入图片描述

斐波那契的时间复杂度

每次递归中仅仅是比较,没有循环,故每次递归调用次数为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缓存知识

栈与队列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值