【数据结构与算法阶段总结】
前言
学习一段时间数据结构后,感觉有点零乱,对线性表、栈和队列的基本概念基本熟悉,通过做一定数量的题目并加深理解,但面对新的问题时,仍然有种不知如何下手的感觉,或者花费时间太长。于是对近来所学进行总结,归纳其中的关键点,并通过具体问题说明把实际问题用算法描述,利用画图、伪码完善思路的过程。
一、算法复杂度
一种算法在保证其正确的前提下,其优劣主要取决于时间和空间复杂度。自己感觉这是最难的地方。有些算法其巧妙程度,让人看了后不禁woc,还能这样!这也是算法的魅力所在。这一点自认为对数学、抽象和逻辑能力要求很高,对于自己来说没有捷径可走(勤能补拙),要提升只能通过多刷题,多归纳思考。当然了,对于一些题目来说能够想出解决算法就已经很不错了。太难了T_T!
1.时间复杂度
一般我们把语句执行的最高数量级的次数作为衡量算法时间复杂度的标准(渐进表示)。若执行次数满足如下函数关系式:
其时间复杂度为。
下图给出了几种函数时间复杂度的增长趋势。
对于时间复杂度为的算法,要自然想到能不能转为的算法。例如查找一个数组中的元素,如果采取遍历的方式,显然时间复杂度为,但如果采用折半查找,每次查找的范围都缩小一半,即,显然时间复杂度为。
2.空间复杂度
算法的空间复杂度即算法所消耗的存储空间,是问题规模n的函数。有时候我们会采用空间换时间的方式设计一个在时间上较为高效的算法。
下面通过具体问题对空间换时间这个思想进行运用。
【例题】空间换时间
1.给定一个含n(n>1)个整数的数组,设计一个在时间上较为高效的算法,找出数组中未出现的最小正整数,例如数组{-5,3,2,3}中未出现的最小正整数为1,数组{1,2,3}中为出现的最小正整数是4。
算法思想:采用空间换时间的方法,对给定的数组遍历一遍,值作为新的数组下标并更新元素值为1,新数组的值开始是0。最后对新数组进行遍历,数据为零时输出数组下标,即为所求。
思路来源与进一步分析: 假设原数组有n个数,最极端的情况是:这个n个数数值从1 ~ n连续,那么最小的正整数是n+1。如果不是这种情况,那么,当有数值大于n时,必然存在小于n的数值不在原数组中(就是为了找出这样的数)。所以我们只需考虑数值范围在1~n的数进入数组。为了充分利用新数组的空间,即newA[0]要有用(为了排除最小正整数1时的情况)所以我们在操作的时候对原数组的元素值减1。
#include<iostream>
using namespace std;
int main()
{
int A[4]={-5,3,2,3};
//int *newA=n*new int;
int *newA=(int *)malloc(sizeof(int)*4)
int i;
for(i=0;i<4;i++)
{
//小于等于0,超过数组范围的的数直接舍弃,
//不进入数组,因为他们肯定不是最小正整数
if(A[i]>0&&A[i]<=4)
newA[A[i]-1]=1;//充分利用新数组空间
}
for(i=0;i<4;i++)
{
if(newA[i]==0)
break;
}
cout<<"未出现的最小正整数是:"<<i+1<<endl;
return 0;
}
小结:自我感觉这个题目如果站在原数组角度上分析问题会特别绕,如果用以终为始的思路,站在新数组的角度分析,会明朗很多。我们的目的是找最小正整数,新数组的下标就存在我们的期待值(或者这样理解,我们先准备好一组正整数,等下用排除法去掉不满足条件的数,那么剩下的就是我们的结果)。用排除法,一旦不满足某个条件,直接把新数组下标的标记(当前数组元素)改变,排除之。最后剩下的就是期待值。
2.用单链表保存m个整数,结点的结构为[data][next],且[data]<=n(n为正整数)。现在要求设计一个时间尽可能高效的算法,对于链表中,绝对值相等的结点,仅保留一次出现的结点而删除其余绝对值相等的结点。
算法思想: 首先这一题和上一题类似,都是利用数组不保留重复出现的元素。不同的是这题的数据范围不超过n,所以可以直接把数据作为数组下标。
//遍历链表,将链表中的数据存入对应下标的数组中,数组元素置为1,将数据已经出现的结点删除
//因为链表中涉及到删除,所以不从第一个元素开始,如果要从首元素开始遍历,必须再新建一个前置结点变量跟着一起移动。
//因为这个问题对结点的操作相对简单所以直接p=L;p->next->data作为判断条件
//p=L->next;//链表中第一个元素地址
p=L;
A[n+1]={0};
while(p->next!=NULL)
{
//求绝对值
m=p->next->data>0?p->next->data:-p->next->data;
if(A[m]==0)
{
A[m]=1;
p=p->next;
}
else
{
//删除结点
q=p->next;//先保存删除结点的地址
p->next=p->next->next;
free(q);
}
}
小结:对于链表关键还是要明确循环开始和结束,结点的状态是什么样的。比如要关注会不会越界的问题。如果循环判断条件while(p->next!=NULL),然而循环中出现了p->next->next->data;最后一次循环的时候p->next->next=NULL;所以程序必然会出问题。一般不要这样冗长地写,大部分情况下从链表第一个元素开始操作即p=L->next。
二、递归
递归的主要思想是把大问题转化成小问题,小问题解决了,上一级的问题就解决了,直到解决最后的大问题。在应用的时候可以两种角度使用递归。
1.自己调自己
比如我们建立了一个删除元素的函数,每次只删除一个数,那么要删除所有数就不断调用这个删除函数就行了,直到所有的数据删完,函数才返回。
p=L->next;
Node deleteX(Node p)
{
if(p!=NULL)
{
q=p->next;
p->next=q->next;
free(q);
deleteX(p->next);//递归调用
}
else
return p;
}
2.大问题化小问题
还有一个比较高大上的名字分而治之(可以试着求一下最大子列和问题)。一般这种问题都可以有一个递归函数,比如:
递归的本质是将函数调用过程中的返回点、局部变量、传入实参等压入栈中,先进入栈中的后释放。
3.递归函数的非递归实现
【例题】
利用一个栈实现下面递归函数的非递归计算:
算法分析: x通过给定其实相当于一个常数,变量只有n。设计一个栈,栈中的数据单元包括参与计算的两部分:当前层的位置即n和当前层的值。
//数据单元
struct P
{
int n;//相当于函数调用过程中的返回点
int val;
};
//新建一个栈
struct Stack
{
int top;
P data[maxSize];
};
//先入栈,假设要计算P5的值,x=3,n=5
int x=3,n=5;
int val1=1;
int val2=2*x;
//初始化栈就不写了
Stack s;
for(int i=n;i>1;i--)
{
s.data[++s.top].n=i;
}
//出栈
while(s.data[s.top].n<=n)
{
s.data[s.top].val=2*x*val2-2*(s.data[s.top].n-1)*val1;
val1=val2;
val2=s.data[s.top].val;
s.top--;
}
//val2即为所求
三、算法分析过程
例如这么一个问题。
某汽车轮渡口,过江渡船每次能载10辆车过江。过江车辆有客车和货车,上渡船有如下规定:同类车先到先到先上船,客车先于货车上船,且每上4辆货车,才允许放上1辆货车;若等待客车不足四辆,以货车代替;若无货车等待,允许客车都上船。试设计一个算法模拟渡口管理。
算法分析: ①首先选择站在谁的角度思考问题。两种车都要上船,显然在船的角度思考会更清晰。因为车只有两种,上船的方案有四种(货车客车都有;只有客车;只有货车;两车都没有)分别列出,把问题分解,每一种设计一个上船的方案。
②通过写伪码把代码的思路进一步完善。
③最后再不断debug程序。
#include<iostream>
using namespace std;
#define maxSize 11
//队列结构体
struct Queue
{
char A[maxSize];
int front,rear;
};
//队列初始化
Queue initQ(Queue boat)//可以用引用改写
{
boat.front=boat.rear=0;
return boat;
}
//入队函数
Queue inQueue(Queue boat,char c)
{
//先判断队列满否
if((boat.rear+1)%maxSize==boat.front)
{
cout<<"队满,入队失败!"<<endl;
return boat;
}
else
{
boat.A[boat.rear%maxSize]=c;
boat.rear++;
return boat;
}
}
//出队函数
Queue outQueue(Queue boat)
{
//先判断队列空否
if(boat.rear==boat.front)
{
cout<<"队空,出队失败!"<<endl;
return boat;
}
else
{
boat.front++;
return boat;
}
}
int main()
{
//初始化船队
Queue boat;
boat=initQ(boat);
//初始化客队
Queue bus;
bus=initQ(bus);
char B[]={'b','b','b','b','b'};
for(int i=0;i<5;i++)
{
bus=inQueue(bus,B[i]);
}
//初始化货队
Queue truck;
truck=initQ(truck);
char T[]={'t','t','t','t','t'};
for(int i=0;i<5;i++)
{
truck=inQueue(truck,T[i]);
}
/*for(int i=0;i<5;i++)
{
cout<<bus.A[i]<<",";
}*/
while((boat.rear+1)%maxSize!=boat.front)//船队未满
{
//客队和货队都不为空
if(bus.rear!=bus.front&&truck.rear!=truck.front)
{
for(int i=0;i<4;i++)
{
if(bus.rear!=bus.front)
{
boat=inQueue(boat,bus.A[bus.front%maxSize]);
bus=outQueue(bus);
}
else
{
break;
}
if((boat.rear+1)%maxSize==boat.front)
break;
}
boat=inQueue(boat,truck.A[truck.front%maxSize]);
truck=outQueue(truck);
}
//客队不为空和货队为空
else if(bus.rear!=bus.front&&truck.rear==truck.front)
{
while(bus.rear!=bus.front)//不为空
{
boat=inQueue(boat,bus.A[bus.front%maxSize]);
bus=outQueue(bus);
}
}
//客队为空,货队不为空
else if(bus.rear==bus.front&&truck.rear!=truck.front)
{
while(truck.rear!=truck.front)
{
boat=inQueue(boat,truck.A[truck.front%maxSize]);
truck=outQueue(truck);
}
}
//两队都为空
else
{
cout<<"已经没有车辆可以进入!"<<endl;
}
}//while
for(int i=0;i<10;i++)
{
cout<<boat.A[i]<<",";//b,b,b,b,t,b,t,t,t,t,
}
//else if在这里面起到了很大的作用,每次只能执行一个
//上面程序中每次进船队的时候应该都要判断一下船队有没有满
//这是一个简单的算法,但不是高效的算法
return 0;
}
上述是这一段时间学习数据结构线性表部分的总结,对算法也属于懵懂阶段,代码写的很臃肿。希望过来人能帮助指出其中的问题和不足,非常感谢。如果能给些算法学习的建议就更好啦!
plus: 上述的代码是直接在笔记中编辑的,可能会有语法细节错误!应该不影响理解。