数据结构与算法学习笔记1
效率
时间复杂度(Time Complexity)
-
最好/最坏/平均
-
遵循的原则:
- 复杂度与其具体的常系数无关。
- 多项式级复杂度相加的时候,把其高项作为结果。
- O(1)含义为,某个任务通过有限可数的资源即可完成。(ps:有限可数的意思是与输入的数据量n无关)
-
经验性结论:(非一定 也有特殊情况)
-
一个顺序结构的代码,时间复杂度为O(1)
-
采用分治法的二分策略,时间复杂度为O( l o g 2 n log_2n log2n)
-
一个简单的for循环,时间复杂度为O(n)
-
两个顺序执行的for循环(不是嵌套),时间复杂性是取高项
-
两个嵌套的for循环,时间复杂度是O( n 2 n^2 n2)
-
-
递归: T ( n ) = a T ( n / b ) + f ( n ) T(n)=aT(n/b)+f(n) T(n)=aT(n/b)+f(n)
空间复杂度(Space Complexity)
- 一般情况下,如果时空不兼得,那么用空间来换时间,因为时间不可逆。但也看特定情况及实际应用情况,并非一定的。
- 如果消耗的空间不会随着变化而变化,则空间复杂度为O(1)
一对一:线性结构
- 数组 array / vector
- 链表 list
- 队列 queue
- 栈 stack
- 串 string
数组
查找
-
针对于内容的查找:O(n) 无法通过下标来找,需要遍历整个数组
-
针对于下标的查找:O(1) 可直接找到下标对应的内容
- 优势在此,如果是顺序数组/倒序数组/或数组下标与内容存在某种数学关系等,则可以利用该特性以节省时间。
增加/删除: O(n)
-
尾添加/删除:
尾添加需要申请一块大的空间,然后把元素放进去
-
头or中间添加/删除:
添加时需要申请一块大的空间,同时添加后后面的元素向后位移,删除时后面的元素需要向前位移
数组元素的跨界访问
- int a[5]={1,2,3,4,5} 问:*(int *)((int)&a+1) = ? (模糊边界/小端存储问题)
首先,&a是数组的首地址,假设其为0x10
其次,(int)&a强制转换为0x10的实际整型数值,再+1则无关地址字节,是数值上的+1,结果为0x11
最后,*(int *)读取0x11往后的四个字节,内容为0010 0000 0000 0000 = 2 25 2^{25} 225,原因如下:
- 1的32位表示为0000 0000 0000 0000 0000 0000 0000 0001 ,bit0位为1,bit31位为0
- 小端存储方向为从右往左,因此从0x10-0x13(4个字节存一个int数值a[0]=1)依次存储为 0001 0000 0000 0000,0x14为a[1]=2的右4位0010,然后读取0x11-0x14,读取的内容为0010 0000 0000 0000 = 2 25 2^{25} 225,最终*(int *)((int)&a+1) = 2 25 2^{25} 225
题1
- 一个数组内有n个元素,每个元素的值范围在0~n-1之间,请判断:数组内是否有重复元素出现,如果有,是坏数据;没有,是好数据。
-
方案分析:
-
当前元素与其他所有元素比较 (暴力解法)
- 每个元素都要和其他元素比较,程序运行次数为n*(n-1),所以时间消耗O( n 2 n^2 n2) ,由于没有申请额外的空间,所以空间消耗O(1)
-
Set(红黑树)
- 时间消耗少,但空间消耗多。由于 红黑树还没学,后面再来补分析~
-
HashSet(哈希表变体)
- 时间消耗少O(n),空间消耗大。由于哈希表还没学,后面再来补分析~
-
排序
- Bubble Sort(冒泡排序):相邻两个元素进行大小比较,如果前一个元素比后一个大,二者交换。时间O( n 2 n^2 n2)
-
数据值作为数组下标使用
- 数值和下标匹配,如果下标和原数值不同,则俩数值换位,如果下标与原数值相同,则说明重复。时间O(n),没有new新的数组,空间消耗也小,因此为最优方案。
-
计数(类似于红黑树~)
-
链表
增加/删除
- 头/尾/中间三种情况
- 链表的增删确实简单,但其建立在搜索之上,搜索还是很耗费时间的,以尾部删除为例,搜索O(n),删除O(1),基于两个顺序执行的程序,时间复杂性是取高项,因此尾删除时间复杂度是O(n)
#include <stdio.h>
#include <stdlib.h>
typedef struct node{
int nValue;
struct node *pNext;
}List;
void CreateList(List **pHead){
//辅助变量
List *pTemp = NULL;
List *pTail = NULL;
int nlenth;
printf("请输入链表初始长度:\n");
scanf("%d",&nlenth);
int nNum;
while(nlenth){
printf("请输入值:\n");
scanf("%d",&nNum);
//临时节点
pTemp = (List*)malloc(sizeof(List));
pTemp->nValue = nNum;
pTemp->pNext = NULL;
//添加
if (*pHead == NULL) { //链表为空的情况
*pHead = pTemp;
}else { //链表不为空的情况
pTail->pNext = pTemp;
}
pTail = pTemp;
nlenth--;
}
}
void Print(List* pHead){
while (pHead) {
printf("%d ",pHead->nValue);
pHead = pHead->pNext;
}
printf("\n");
}
int main(){
List *pHead = NULL;
CreateList(&pHead);
Print(pHead);
return 0;
}
倒置
以A->B->C->D->E为例,倒置为E->D->C->B->A
-
方案
- 头插法/三个指针(最优解)
- 依次拿出来,然后头插,头插的特点就是倒置,其时间复杂度为O(n)
- 没有申请额外的空间,所以空间复杂度是O(1)
- 相邻交换
- 每两个换,B->A->C->D->E B->C->A->D->E B->C->D->A->E B->C->D->E->A
- 以此类推,换n-1+n-2+…+1次,时间消耗为O( n 2 n^2 n2),没有新空间,空间消耗O(1)
- 头尾交换
- 遍历找到E,A和E换,再遍历到D,B和D换
- 时间消耗还是O( n 2 n^2 n2),空间消耗O(1)
- 入栈出栈/不让用栈可以用数组
- 遍历链表入栈,然后栈顶出栈,时间消耗O(n),但是空间消耗O(n)
- 暴力解法
- n先从头到尾遍历到E,把E拿出来,再从头遍历到D……以此类推,其时间复杂度为n+n-1+n-2+……+1,等差数列,所以其时间复杂度为O( n 2 n^2 n2)
- 头插法/三个指针(最优解)
List *ReverseList(List *pHead){ //倒置链表
printf("倒置链表:\n");
//如果函数参数不是地址,而是变量这种,那一定要检测数据是否异常(代码的鲁棒性)
if (pHead==NULL||pHead->pNext==NULL) { //不存链表或链表只有一个节点
return pHead; //直接返回原来的就行,不用倒置了
}
List *p1 = NULL;
List *p2 = pHead;
List *p3 = pHead->pNext;
while (p3 != NULL) {
//断开 改向
p2->pNext = p1;
//移动
p1 = p2;
p2 = p3;
p3 = p3->pNext;
}
p2->pNext = p1;
return p2;
}
链表折叠
题目: 原:A->B->C->D->1->2->3->4 折叠为:A->4->B->3->C->2->D->1
- 方案:①找到中间位置;②后半部分倒置;③两部分合并(间隔插入)
顺序链表合并
题目: 1->3->5->6和2->4->7->9 合并为1->2->3->4->5->6->7->9
- 方案:①定表头;②合并(依次比较+标记点移动);③剩余部分拼接
回文链表
题目: A->B->C->D->D->C->B->A 检测该单向链表是否回文
- 方案:①找到中间位置断开(奇数个的话最中间那个不动就行);②后半部分倒置;③两部分比较看是否一致
链表形式的加法
题目:将7243表示为7->2->4->3,将564表示为5->6->4,将其和7807以链表形式表示出来。
- 方案:①倒叙;②按位相加再进位(放在长的链表上);③再倒置链表
复杂/随机链表的复制
-
方案:
①先复制一个,合并,变成A->A1->B->B1->C->C1->D->D1->E->E1;
②原关系依然存在,然后A的下一个指向D的下一个,以此类推进行其他复杂指向的复制;
③分离,A的下一个指向B,A1的下一个指向B1,以此类推。
ps:三个步骤都是遍历一次链表,所以时间复杂度O(n)+O(n)+O(n),比直接复制节省了很多时间,直接复制要遍历n*n次,时间复杂度为O( n 2 n^2 n2)
单向链表相交问题
题目:判断两个单向链表是否相交,如果相交,请找到交点。
分析:单向链表,一个指针不能指向两个地址,所以如果相交一定是一个Y型的样子,而不会是X型的样子。
方案:
-
①暴力解法:
-
1)遍历两个链表 (时间消耗高,链表长度为n和m的话,需要遍历n*m次)
-
2)创建一个表存入其中一个链表的每个节点的地址,然后利用另一个链表的每个节点的地址来查询表中是否存在(空间消耗高)
-
-
②看看较短的那个链表的尾节点能否在另一个链表中遍历到(可以判断是否相交但找不到交点)
-
③链表合并(如果有相交的话会出现重复插入),看元素个数(可以判断是否相交但找不到交点,同时不建议使用,因为原链表的结构被破坏了,后面如果要使用的话还需要复原)
-
④入栈,然后出栈看节点是否存在一致到不一致的交替
-
⑤计算两链表的长度差,先走完长度差的部分,再同时走两个链表进行相互比较(最优方案)
-
⑥尾部节点连上某一个链表(假如为A)的头结点,然后遍历另一个链表(B),看是否能够遍历到A链表的节点(可以判断是否相交但找交点很麻烦,需要知道A的所有节点才行)
单向链表是否有环
问题:单向链表是否有环,如果有,找到环的入口结点。
分析:呈6型,(PS:O型是循环链表,不是单向链表,也不是单向链表有环的情况)
- 题外话:循环链表特性(尾的下一个是头,从任意节点出发,均可遍历整个链表。)
方案:
-
① 遍历+地址保存(暴力)
- 时间消耗O( n 2 n^2 n2),空间消耗O(n)
-
② 利用set(还没学红黑树,暂留一下这个问题)
- 时间消耗O( n l o g 2 n nlog_2n nlog2n),申请了空间所以空间消耗也很大
-
③ 6->Y
- 环中相遇点断开,即可变成上一题中的Y型,找其交点
-
④ 路程差值
- 先算环长,然后都从头走,A先走环长,然后AB一起走,AB相遇的点就是环的入口结点
-
⑤ 倒置+计数
-
对每个结点的处理次数进行计数,重复处理的就是环的入口结点,但其问题在于申请的计数器数量不确定而且可能很多
-
优化,不使用计数。方案:
例:单向环形链表为A->B->C->D->E->F->G,G->C
-
①倒置,获得倒置长度;
-
②申请倒置长度更长一些的空间;
-
③再倒置一次,存变量到数组中且将链表进行还原;
-
④数组两端依次进行遍历,一样->不一样的那个边界就是环的入口结点,也就是图中的C处
-
-