2.3.7线性表—链表存储
一,选择题:(只有一部分做错或者我觉得有意思的题)
1,描述正确的是:2,4
1,线性表的顺序存储结构优于链式存储结构 X //各有特点
2,链式存储比顺序存储更方便表示各种逻辑结构 √
3,若频繁的使用插入和删除操作,顺序存储优于链式存储 X //反了
4,顺序存储和链式存储都可以用顺序存取 √
2,对于一个线性表,要求能较快的插入和删除,又要存储结构能反映数据之间的逻辑关系应该用: 链式存储方式
3,对于顺序存储的线性表,时间复杂度为O(1)的算法是:
改变第i个元素的值
4,描述正确的是:3,4,5
1,顺序存储只能用于存储线性表 X //还可以存图和树
2,取线性表的第i个元素的时间和i的大小相关 X //只有链式存储会影响顺序存储是O(1)
3,静态链表需要分配较大的连续空间,插入和删除都不需要移动元素 √
4,一个长度为n的有序单链表中插入一个新结点并且保持有序的时间复杂度是O(n) √
5,若用单链表表示队列,应该使用带尾指针的循环列表 √
06,在一个单链表中,已知q所指的节点是p所指的前驱结点,若在q和p中间插入s则执行
q->next = s, s - > next = p
07,将一个有n个元素的一维数组,建立一个有序单链表的时间复杂度:先排序O(nlogn) 建链表O(n) 所以 时间复杂度为O(nlogn) + O(n) = O(nlogn)
08,将长度为n的链表链接在长度为m的单链表后面所需要的时间是O(m)
19,设对有n个元素的线性表的运算只有四种:删除第一个元素;删除最后一个元素;在第一个元素之前插入元素;在最后一个元素之后插入新元素。
只有头结点指针的循环双链表
头指针的前驱就是尾指针,就可以进行最后一个元素插入和删除操作,且方便
20,一个链表常用的操作是在最后一个元素后插入 和 删除第一个元素
不带头结点且有尾指针的单循环链表
很好操作
如果只说了链表则说明只给头结点的指针。有尾指针会单独说明。
21,静态链表中的指针表示
静态链表的指针的意思就是游标的意思,所以是下一个元素在数组中的位置
26,已知头指针h指向一个带头结点的非空单循环链表,结点的结构为[data][next],其中next是指向直接后继结点的指针,p是尾指针,q是临时指针,现要删除该链表的第一个元素:
q = h->next;
h->next = q -> next;
if(q == p) p = h;//代表该链表只有一个元素,该元素删了后是空表,尾指针应该指向头结点。
free (q);
二,综合应用题
01,递归删除链表中值为x的节点。
void solf(Linklist &L, int x)
{
Lnode *p;
if(L==NULL)
{
return;
}
else
{
if(L->data==x)
{
p = L;
L = L->next;
free(p);//不仅在链表中删除结点也要将该结点的内存释放
solf(L,x);
}
}
solf(L->next,x);
return;
}
时间复杂度O(n)需要工作栈深度为O(n)
02在带头节点的单链表L中,删除值为x的结点并且释放空间
void solf(Linklist &L, int x)
{
Lnode *p;
p = L->next;//因为有头结点所以从头节点的next开始遍历
Lnode *pre;//p的前置节点用于将x的值的结点分离出去
Lnode *q;//用于释放空间
pre = L;
while(p != NULL)
{
if(p->data == x)
{
q = p;
p = p->next;
pre->next = p;
free(q);
}
else
{
p = p->next;
pre = pre->next;
}
}
return ;
}
时间复杂度O(n)空间复杂度O(1)
03,反向输出单链表L
void solf(Linklist &L)
{
if(L==NULL)return;
solf(L->next);
cout<<L->data<<" ";
}//递归输出就行了
时间复杂度O(n)需要工作栈深度为O(n)
04在单链表中设计一个删除最小值的高效算法
void solf(Linklist &L)
{
Lnode *p;
Lnode *q;
Lnode *pre;
pre = L;
p = L->next;
q = L;
int minn = p->data;
while(p!=NULL)
{
if(p->data < minn)
{
minn = p->data;
q = pre;
pre = p;
p = p->next;
}
else
{
pre = p;
p = p->next;
}
}
Lnode *s;
s = q->next;
if(s->next==NULL)
{
q->next=NULL;
free(s);
}
else
{
q->next = s->next;
free(s);
}
return;
}
每次更新最小点时记录最小点的前驱结点。
05,将单链表就地逆置空间复杂度为O(1);
思路:很简单就是用链表的头插法重新插一遍就好了。
void solf(Linklist &L1)
{
Lnode *p;
Lnode *s;
p = L1->next;
L1->next = NULL;
while(p!=NULL)
{
s = p;
p = p->next;
s->next = L1->next;
L1->next = s;
}
}
06,设计一个算法给单链表排序。
思路:类似于数组中的插入排序
void solf(Linklist &L1)
{
Lnode *p,*o;
Lnode *s;
p = L1->next->next;
L1->next->next = NULL;
while(p!=NULL)
{
o = L1;
s = p->next;
while(o->next!= NULL && o->next->data<p->data)
o = o->next;
p->next = o->next;
o->next = p;
p = s;
}
}
时间复杂度为O(n)空间复杂度为O(1);
如果用空间换时间将链表中的数据读到数组中排完序再重新放回链表,时间复杂度为O(nlog2n)
07,设计一个算法,给定两个值X,Y,删除链表中所有值介于两者之间的节点
思路:就重头到尾遍历一遍,在两者之间的就删掉就是。
void solf(Linklist &L1,int x,int y)
{
Lnode *p,*s,*o;
p = L1->next;
s = L1;
while(p->next!=NULL)
{
if(p->data>=x&&p->data<=y)
{
o = p;
s->next = p->next;
p = p->next;
free(o);
}
else
{
s = p;
p = p->next;
}
}
}
时间复杂度O(n)
08,给定两个单链表,找出两个链表的公共结点。
思路:因为是单链表,有公共结点就代表第一个公共结点后面的也都是一样的,所以这就相当于是一个Y字型的,我们只需要将长的一端“剪短”到和短的一端一样长然后一起开始往后遍历,找到第一个相同的点就是公共结点。
void solf(Linklist &L1,Linklist &L2)
{
int a=L1->lenth,b=L2->lenth;
int midd = abs(a-b);//计算出两条的长度差距
Lnode *p = L1,*q = L2;
if(a>b)//L1比L2长就先将L1先后移midd位,此时L1到公共结点的距离就和L2一样
{
while(midd)
{
p = p->next;
midd--;
}
}
else//同上
{
while(midd)
{
q = q->next;
midd--;
}
}
while(q->data!=p->data)//一起开始往后遍历,找到第一个相同的点。
{
q = q->next;
p = p->next;
}
cout<<q->data<<" "<<p->data;
return ;
}
09,给定一个单链表,按递增输出链表的数据,并且释放结点所占的存储空间,不能用额外的数组空间。
思路:不能取出来排序输出,所以我们就循环n次,每次找最小,找到后把其删掉并且释放空间。
void solf(Linklist &L1)
{
while(L1->next!=NULL)
{
Lnode *pre = L1;
Lnode *p = pre->next;
Lnode *u;
while(p->next!=NULL)
{
if(p->next->data<pre->next->data)
pre = p;
p = p->next;
}
cout<<pre->next->data<<" ";
u = pre->next;
pre->next = pre->next->next;
free(u);
}
free(L1);
return ;
}
时间复杂度O(n*n);
10,将单链表分为两个链表,分别存储序号为奇数的结点和序号位偶数的结点,且保持原有相对顺序。
思路:很简单,从前往后遍历一遍,奇数个直接跳过,偶数个,将偶数个从这个链表中删除,添加到另一个链表的尾部,因为要保持原来的相对位置,所以使用尾插法会更方便。
void solf(Linklist &L1,Linklist &L2)
{
Lnode *p =L1,*q = L2;
Lnode *s;
int i =1;
while(p->next!=NULL)
{
if(i %2 == 0)//偶数个时从L1中去除,添加到L2的末尾,q是指向L2末尾的指针。
s =p->next;
p->next = p->next->next;
s->next = q->next;
q->next = s;
q = q->next;
i++;
}
else
{
p = p->next;
i++;
}
}
}
时间复杂度O(n)
11,设C=(a1,b1,a2,b2…,an,bn)为线性表,采用带头指针的单链表存储,设计一个就地算法,将其拆分为A{a1,a2…an},B{bn,bn-1…b1}两个链表。
思路:主题思路和10题一样就是因为b的要求,在插入b时要使用头插法
void solf(Linklist &L1,Linklist &L2)
{
Lnode *p =L1,*q = L2;
Lnode *s;
int i =1;
while(p->next!=NULL)
{
if(i%2 == 0)//偶数个时将其头插法插入L2,q是指向L2头部的指针。
{
s =p->next;
p->next = p->next->next;
s->next = q->next;
q->next = s;
i++;
}
else
{
p = p->next;
i++;
}
}
}
12,一个递增的链表有值相同的结点,设计一个算法,使得链表中不存在值相同的结点。
思路:从头到尾遍历一次,后一个结点值与当前结点值相等,就删除到下一个结点并且释放空间。否则就向后移动一位。
void solf(Linklist &L1)
{
Lnode *pre =L1;
Lnode *p = pre->next;
while(p->next!=NULL)
{
p = pre->next;
if(p->data == pre->data)
{
pre->next = p->next;
free(p);
}
else
{
pre = pre->next;
}
}
}
时间复杂度O(n)空间复杂度O(1);
13,有两个按增序的的单链表编写算法将这两个单链表归并为一个递减的单链表,并且要求用链表原有的结点存放。
思路:双指针分别指向两个链表的表头,哪个小就先动哪个,将L1->next 设置为空,然后将结点按次序头插法插入L1。
void solf(Linklist &L1,Linklist &L2)
{
Lnode *p = L1->next;
Lnode *q = L2->next;
Lnode *s;
L1->next = NULL;
free(L2);//L2的头结点用不到了,可以释放掉
while(p!=NULL&&q!=NULL)//遍历一遍,按次序头插L1。
{
if(p->data<q->data)
{
s = p;
p = p->next;
s->next = L1->next;
L1->next = s;
}
else
{
s = q;
q = q->next;
s->next = L1->next;
L1->next = s;
}
}
//按照上面条件遍历,会有一个没遍历完,所以需要将剩余的结点都遍历完。
while(p!=NULL)//将剩余的结点全部按头插插入L1
{
s = p;
p = p->next;
s->next = L1->next;
L1->next = s;
}
while(q!=NULL)
{
s = q;
q = q->next;
s->next = L1->next;
L1->next = s;
}
}
时间复杂度O(n),空间复杂度O(1)
14,设A和B是两个单链表,其中的元素递增,设计一个算法将A,B中的公共元素组成一个新的链表C。要求不破坏A,B.
思路:就是从前到后遍历一遍,每次后移只移动最小值,确保能完成比较。
Linklist solf(Linklist &L1,Linklist &L2)
{
Lnode *p = L1->next;
Lnode *q = L2->next;
Lnode *s,*rc;//rc用于指向L3的末尾结点
Linklist L3;
init(L3);
rc = L3;
while(p!=NULL&&q!=NULL)
{
if(p->data == q->data)
{
s = (Lnode *)malloc(sizeof(Lnode));
s->data = p->data;
s->next = rc->next;
rc->next = s;
rc = rc->next;
p = p->next;
q = q->next;
}
else
if(p->data < q->data) p = p->next;
else
q = q->next;
}
return L3;
}
时间复杂度O(n);
15,已知两个链表A,B分别表示两个集合,其元素递增有序,设计函数,将A,B的交集,存放于A链中。
思路:双指针,遍历比较,因为有序,每次更新最小的,如果相同就存入,否则就更新最小的,相同就一起到下一个。
void solf(Linklist &L1,Linklist &L2)
{
Lnode *p = L1->next;
Lnode *q = L2->next;
Lnode *s,*rc;
L1->next = NULL;
rc = L1;
free(L2);
while(p!=NULL&&q!=NULL)
{
if(p->data == q->data)
{
s = q;
rc->next = p;
p = p->next;
q = q->next;
rc->next->next = NULL;
rc = rc->next;
free(s);
}
else
if(p->data < q->data)
{
s = p;
p = p->next;
free(s);
}
else
{
s = q;
q = q->next;
free(s);
}
}
//之后的值都不可能是交集的元素。
while(p!=NULL)
{
s = p;
p = p->next;
free(s);
}
while(q!=NULL)
{
s = q;
q = q->next;
free(s);
}
}
16,两个整数数列A = a1,a2…am和B = b1,b2…bn,设计一个算法判断序列B是不是序列A的连续子序列。
思路:连续子序列,要求是不仅B的所有元素都在A中找到,并且在A中的相对位置与在B中的相对位置相等,所以我们只需要从A中每一个等于B中第一个的元素的位置开始,相同就向后移,如果能遍历到B的最后一位,则表示是,否则就不是。
bool solf(Linklist &L1,Linklist &L2)
{
Lnode *p = L1->next;
Lnode *q = L2->next;
Lnode *s;
s = p;//s用于记录每次p开始和q进行比较的位置,就是L1中和L2第一个相同的位置。
bool flag = false;
while(p&&q)
{
if(p->data == q->data)
{
p = p->next;
q = q->next;
}
else
{
s = s->next;//更新s到下一个为止
p =s;//p重新回到之前开始的位置
q = L2->next;
}
}
if(q==NULL)return true;//q == NULL;表示L2已经遍历完了,就证明确实是子序列。
else return false;
}
时间复杂度O(n*n);
17,设计一个算法用于判断带头结点的循环双链表是否对称。
思路:很简单,循环双链表,直接从两头往中间找,只要有一个地方不一样就直接返回false直到招完,全一样就返回true;
bool solf(Linklist &L1)
{
Lnode *p = L1->next;
Lnode *q = L1->pre;
while(p!=q && p->next!= q)//当个数为奇数时会有一个中心点,而偶数时不会。
{
if(p->data != q->data)return false;
else
{
p = p->next;
q = q->pre;
}
}
return true;//一直到这儿都没有返回说明就是对称的。
}
时间复杂度O(n)
18,有两个循环单链表,分别为h1,h2,设计算法将h2接到h1之后,并且使接上后的链表依然是循环链表。
思路:就只需要找到h1的尾使其next指向h2头,找到h2的尾结点,使其next指向h1的头。
void solf(Linklist &L1,Linklist &L2)
{
Lnode *p = L1;
Lnode *q = L2;
while(p->next!=L1)
p = p->next;
while(q->next!=L2)
q = q->next;
p->next = L2;
q->next = L1;
}
时间复杂度为O(n);
19,设有一个带头结点的循环单链表,其结点的值均为正数,设计一个算法反复找出表中结点的最小值输出并删除,直到表为空。再删除表头结点。
思路:很简单就循环判断,直到为空。
void solf(Linklist &L1)
{
Lnode *p,*s,*q;//p用来表示当前位置,s表示当前位置的前驱,q用来删除最小值的结点。
p = L1;
int minn = L1->next->data;
while(L1->next!=L1)//L1的next是它自己的时候就代表循环单链表为空
{
if(p->next->data == L1->data)//遍历完一遍了,要输出最小值并且删除。
{
q = s->next;
s->next = q->next;
cout<<minn<<" ";
free(q);
minn = L1->next->data;
p = L1;
}
else
if(p->next->data <= minn)
{
minn = p->next->data;
s = p;
p = p->next;
}
else
{
p = p->next;
}
}
free(L1);//释放头结点。
return;
}
时间复杂度为O(n*n);
20,有一个非循环的双向链表,带头结点,每个结点包括pre(前驱),data(值),next(后驱),freq(访问频度:就是初始都为0某个结点被访问一次就+1),设计一个Locate(L,X)算法,使L中值为X的结点freq+1,并且保持链表从前到后按freq降序排序,freq相同则最近被访问的排在最前面。
思路:L中找到值为X的结点,freq++,然后找到第一个大于freq的数插在他后面就行。
注意:我这里直接用的Linklist但是实际上结构体是改变了的。并不是单链表的结构。而是非循环的双向链表。
Linklist solf(Linklist &L1,int x)
{
Lnode *p,*s;
p = L1->next;
while(p->data!=x&&p!=NULL)//找打值为x的结点。
p = p->next;
if(!p)//没找到值为x的结点。
{
cout<<"出错了,没有找到值为x的结点";
exit(0);
}
else
{
p->freq++;
if(p->pre==L1||p->pre->freq>p->freq)//如果当前结点就是第一个结点,或者前一个结点的freq的值比当前的freq的值大就不需要往前移动了。
return p;
p->pre->next = p->next;
if(p->next)//因为是双向,所以要考虑下一个的pre。
p->next->pre = p->pre;
s = p->pre;
while(s!=L1&&s->data<=p->data)//找到第一个值大于x的结点。
{
s = s->pre;
}
if(s->next)s->next->pre = p;
p->next = s->next;
p->pre = s;
s->next = p;
}
return p;
}
时间复杂度为O(n)
21,单链表有环,是指链表最后一个结点的next指向了链表中的某一个结点,正常情况下,最后一个结点的next应该是空,试着编写算法判断是否有环。
思路:最简单的思路是用空间换时间,用map,但是呢考研最好别用stl库,所以我们就要想象别的办法,比如用两个指正同时开始遍历链表,但是一个跑得快一个跑得慢,因为有环的存在,所以跑的快的一定会追上跑的慢的。然后我们设head到环结点的距离为a,环周长为r,fast指针跑了n圈环 ,环结点到相遇点的距离为x一定有:a + n*r +x == 2(a+x);
解得:n * r ==a+x —> a = n*r - x;所以当一个点从head开始另一个从相遇的点x开始循环,当第一次相遇就是环的结点。
void solf(Linklist &L1,int x)
{
Lnode *fast , *slow;
fast = slow = L1;
while(fast!=NULL&&fast->next!=NULL)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow )break;
}
if(fast == NULL||fast->next==NULL) return NULL;
fast = L1;
while(fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
22,假设给定一单链表的头指针,在不改变链表结构的情况下,设计一个尽可能高效的算法,查找链表中倒数第k个结点的,查找成功输出结点的值返回1,否则直接返回0.
思路:一个fast在前面,当fast和slow相差k个的时候一起动,直到fast到达最后一个结点时,slow就到达了倒数第k个的结点处。
步骤:
1,设置两个指针分别为fast 和 slow 皆指向L1->next 处。
2,fast向前进,k–;如果k大于0则只有fast前进,如果k减到0了,slow和fast一起前进,k不变,直到fast到达链表最后
3,如果fast到最后k值仍然大于0则表示当前链表长度小于k,查找失败返回0
4,查找成功,输出值然后返回1.
5,算法结束
int solf(Linklist &L1,int x)
{
Lnode *fast , *slow;
fast = slow = L1->next;
while(fast!=NULL)
{
if(!x)
{
slow = slow->next;
}
else
{
x--;
}
fast = fast->next;
}
if(x)return 0;
cout<<slow->d
ata;
return 1;
}
时间复杂度为O(n);
23,这道题核心内容就是给定一个Y型的链表,求两个单链表相交的结点的位置。
思路:就是从尾往前看,交点处到两个链表的最后的距离是一样的,所以我们只需要从距离末尾相同的最远的点往后找,找到第一个next结点相同的点就是我们要找的交点。
Lnode* solf(Linklist &L1,Linklist &L2)
{
Lnode *p,*q;
p = L1->next;
q = L2->next;
int m,n;//第一步先求出两个链表的长度。时间为O(n+m)
m=n=0;
while(p)
{
p = p->next;
m++;
}
while(q)
{
q = q->next;
n++;
}
p = L1->next;
q = L2->next;
int mm = abs(m-n);
if(m>n)//将长的那条链表先向后移动abs(m-n)个,使两条链表从同一长度开始
{
while(mm)
{
p = p->next;
mm--;
}
}
else
{
while(mm)
{
q = q->next;
mm--;
}
}
//遍历找交点
while(p->next !=NULL && p->next != q->next)
{
p = p->next;
q = q->next;
}
//找到就直接返回
return p->next;
}
时间复杂度为O(max(len1,len2))len1,len2表示两个链表的长度。
24,给定一条单链表,设计算法使单链表中绝对值相同的值只存在一个。单链表的长度为m,里面值的绝对值小于等于n
思路:因为|data|<= n 所以我们可以用一个大小为n的数组表示该绝对值是否出现过,出现过就删掉,没出现就不删。
int abs(int x)//求绝对值
{
return x>0?x:-x;
}
void solf(Linklist &L1,int n)
{
Lnode *p,*q;
p = L1;
int *vis;
vis = (int *)malloc(sizeof(int)*(n+1));//给数组n+1空间这样下标才能到n
for(int i=0;i<=n;i++) *(vis+i) = 0;
while(p->next!=NULL)
{
if(*(vis+abs(p->next->data)))//出现过就删掉
{
q = p->next;
p->next = q->next;
free(q);
}
else
{
*(vis+abs(p->next->data)) = 1;//没出现过就标记,再出现就删
p = p->next;
}
}
}
时间复杂度O(m),空间复杂度O(n)
25,设线性表L(a1,a2,a3…an)采用头结点的单链表存储,表中结点定义如下
typedef struct Lnode{
int data;
struct Lnode *next;
}Lnode;
请你设计一个空间复杂度为O(1)且尽可能高效的算法使链表变为L(a1,an,a2,an-1,a3,an-2…);
思路:先将后面一半原地逆置,然后挨个插入前面一半。
void solf(Linklist &L1)
{
Lnode *p,*q,*s,*r;
p = q = L1;
while(q->next!=NULL)
{
q = q->next;
p = p->next;
if(q->next!=NULL)q = q->next;
}
//就地逆置后面一半
q = p->next;
p->next = NULL;
while(q!=NULL)
{
s = q;
q = q->next;
s->next = p->next;
p->next = s;
}
//接下来将后面这一半,间隔按顺序插入前面一半。
q = L1->next;
r = p;
p = p->next;
r->next = NULL;//将前面那一半与后面一半断开连接。
while(p!=NULL)
{
s = p;
p = p->next;
s->next = q->next;
q->next =s;
q = s->next;
}
}
时间复杂度O(n),空间复杂度O(1);