说明:《数据结构题集(C语言版)》(严蔚敏等编著,清华大学出版社,旧版电子书封面为蓝灰色,简称《题集》)是《数据结构(C语言版)》(严蔚敏等编著,清华大学出版社,封面为紫色,下称“主书”)的配套书籍。《题集》的习题篇与主书相对应,分为12章。
题号后的带圈数字表示难度系数,难度级别从①至⑤逐渐加深。题号前有标注◆的题目是编者自认为值得向读者推荐的“好题”。(难度系数与“好题”均为《题集》编者标注,非本人标注)
部分算法使用了主书的配套代码中定义的数据类型和函数,在这些算法的头部省略了#include指令。
《题集》第1章为绪论,理论知识较多,本人也进行了自解答,不公开发布。本文是第2章(线性表)习题的部分自解答。后续章节不再进行自解答。本自解答在完成后参考了《题集》中的提示和答案、康建伟给出的自解答来进行订正,并经过简单测试运行。如发现错误,可留言指出。
2020年4月2日更新,2020年2-3月编写
◆2.12②
int ListCompare_Sq(SqList A,SqList B)
{//本算法比较顺序表A和B的大小
int la=A.length,lb=B.length;
int i=0,d=0;
while(i<la&&i<lb&&d==0)
{
d=A.elem[i]-B.elem[i];
i++;
}
if(d!=0) return d;
else return la-lb;
}
2.15②
Status ListConnect(LinkList ha,LinkList hb,LinkList &hc){
//本程序将以hb为头结点的链表连接到以ha为头结点的链表之后,
//并用hc返回连接后链表的头结点,已知两个链表的长度分别为m和n
LinkList p,q;//p指向短链表,q指向长链表
if(m<n) {p=ha;q=hb;}
else{p=hb;q=ha;}
hc=p;
while(p->next) p=p->next;
p->next=q->next;
free(q);
return OK;
}
时间复杂度:O(min(m,n))
2.17②
注释中有标明本算法与主书P29算法2.9的不同之处。
Status ListInsert(LinkList &L,int i,ElemType e) // 算法2.9。不改变L
{ // 在无头结点的单链线性表L中第i个位置之前插入元素e
int j=1;//主书:j=0
LinkList p=L,s;
while(p&&j<i-1) // 寻找第i-1个结点
{
p=p->next;
j++;
}
if(!p L&&p==NULL||j>i-1&&i!=1) // 此条件不同,空表的L=NULL;i小于1或者大于表长(i=1时j>0)
return ERROR;
s=(LinkList)malloc(sizeof(LNode)); // 生成新结点
if(!s) return OVERFLOW;
s->data=e; // 插入L中
if(i==1)//增加此段
{
s->next=p;//插在p的前面
L=s;
}
else
{
s->next=p->next;
p->next=s;
}
return OK;
}
注意:无头结点的单链表的InitList(LinkList &L)函数简化为一句话:L=NULL;
2.18②
注释中有标明本算法与主书P30算法2.10的不同之处。
Status ListDelete(LinkList L,int i,ElemType &e) // 算法2.10。不改变L
{ // 在无头结点的单链线性表L中,删除第i个元素,并由e返回其值
int j=1;//主书:j=0
LinkList p=L,q;
if(!p) return ERROR;//此句新增,空表:出错
while(p->next&&j<i-1) // 寻找第i个结点,并令p指向其前趋
{
p=p->next;
j++;
}
if(!p->next||j>i-1&&i!=1) // 此句不同,删除位置不合理
return ERROR;
if(i==1)//此段新增
{
q=p;//首元结点无前驱
L=p->next;
}
else
{
q=p->next; // 删除并释放结点
p->next=q->next;
}
e=q->data;
free(q);
return OK;
}
◆2.19③
Status ListDelete(LinkList L,int mink,int maxk)
{//本算法删除单链表L中所有值大于mink且小于maxk的元素
if(mink>maxk) return ERROR;
LinkList p=L,q=p->next,r;
while(q&&q->data<=mink)
{
p=q;
q=q->next;
}//使p指向删除段的前驱
while(q&&q->data<maxk)
{
r=q;
q=q->next;//使q指向删除段的后继
free(r);
}
p->next=q;
return OK;
}
2.20②
Status ListDelete(LinkList &L)
{//本算法删除递增有序的单链表L中所有值相同的多余元素
LinkList p=L->next,q,r;
while(p)//p指向删除段前驱
{
q=p->next;
while(q&&q->data==p->data)
{
r=q;
q=q->next;
free(r);
}//出循环时,q指向删除段后继
p->next=q;
p=p->next;
}
return OK;
}
时间复杂度:O(n)
◆2.21③
void Reverse(SqList &L)
{//本算法实现顺序表的就地逆置
ElemType t;
ElemType *p=L.elem,*q=p+L.length-1;
while(p<q)
{
t=*p;
*p=*q;
*q=t;
p++;
q--;
}
}
◆2.22③
对单链表实现就地逆置。
第一次写出的算法如下。
void Reverse(LinkList &L){
if(!L->next) return;
LinkList p=L->next,q=p->next,r;
while(q){
r=q->next;
q->next=p;
p=q;
q=r;
}
L->next->next=NULL;
L->next=p;
}
(答案提示)正确做法:将原链表中的头结点和第一个元素结点断开,先构成一个新的空表,然后将原链表中各结点依次插入新表的头部。
以下为根据此提示写出的算法。
void Reverse(LinkList L){
LinkList p=L->next,q;
L->next=NULL;
while(p){
q=p->next;
p->next=L->next;
L->next=p;
p=q;
}
}
◆2.24④
void ListMerge_OrderReverse(LinkList &A,LinkList &B,LinkList &C)
{//本算法将两个递增有序单链表B合并为递减有序单链表C
LinkList pa=A->next,pb=B->next,q,p;
C=A; C->next=NULL;//头插法注意头结点初始化
while(pa&&pb)
{
if(pa->data<=pb->data)
{
q=pa->next;
pa->next=C->next;
C->next=pa;
pa=q;
}
else
{
q=pb->next;
pb->next=C->next;
C->next=pb;
pb=q;
}
}
p=pa?pa:pb;
while(p)
{
q=p->next;
p->next=C->next;
C->next=p;
p=q;
}
free(B);
}
2.25④
假设以两个元素依值递增有序排列的线性表A和B分别表示两个集合(即同一表中的元素值各不相同),现要求另辟空间构成一个线性表C,其元素为A和B中元素的交集,且表C中的元素也依值有序递增排列。试对顺序表编写求C的算法。
void ListIntersection(SqList A,SqList B,SqList &C){
//本算法求两个递增有序顺序表A和B的交集C,C同样递增有序
//设指针pa和pb分别指向两表剩余部分顶端,每次循环比较二者的值,
//相等则插入表C,否则较小者后移。
InitList(C);
ElemType *pa=A.elem,*ma=pa+A.length,
*pb=B.elem,*mb=pb+B.length;
ElemType a,b;int i=1;
while(pa<ma&&pb<mb){
a=*pa,b=*pb;
if(a!=b){
if(a<b) pa++;
else pb++;
}
else{
ListInsert(C,i++,a);
pa++;pb++;
}
}
}
2.26④
要求同2.25题。试对单链表编写求C的算法。
void ListIntersection(LinkList A,LinkList B,LinkList &C){
//本算法求两个递增有序单链表A和B的交集C,C同样递增有序
InitList(C);
LinkList pa=A->next,pb=B->next,pc=C,s;
ElemType a,b;
while(pa&&pb){
a=pa->data,b=pb->data;
if(a!=b){
if(a<b) pa=pa->next;
else pb=pb->next;
}
else{
s=(LinkList)malloc(sizeof(LNode));
s->data=a;
pc->next=s;
pc=s;
pa=pa->next;
pb=pb->next;
}
}
pc->next=NULL;
}
◆2.27④
对2.25题的条件作以下两点修改,对顺序表重新编写求C的算法。
(1)假设在同一表(A或B)中可能存在值相同的元素,但要求新生成的表C中的元素值各不相同;
(2)利用A表空间存放表C。
void ListIntersection(SqList &A,SqList B){
//要求(2)实现:从A表首部开始存放表C。
//要求(1)实现:表C新元素生成后,后移pa和pb直到指向新元素
ElemType *pa=A.elem,*ma=pa+A.length,
*pb=B.elem,*mb=pb+B.length;
ElemType a,b;int i=0;
while(pa<ma&&pb<mb){
a=*pa,b=*pb;
if(a!=b){
if(a<b) pa++;
else pb++;
}
else{
A.elem[i++]=a;
do{pa++;}while(pa<ma&&*pa==a);
do{pb++;}while(pa<mb&&*pb==b);
}
}
A.length=i;
}
◆2.28④
对2.25题的条件作以下两点修改,对单链表重新编写求C的算法。
(1)假设在同一表(A或B)中可能存在值相同的元素,但要求新生成的表C中的元素值各不相同;
(2)利用原表(A表或B表)中的结点构造表C,并释放A表中的无用结点空间。
void ListIntersection(LinkList A,LinkList B,LinkList &C){
//设pc指向表C末端,pa去重在匹配前进行,pb去重在匹配后进行。
LinkList pa=A->next,pb=B->next,pc=A,r;
ElemType a,b;
while(pa&&pb){
a=pa->data,b=pb->data;
if(a!=b){
if(a<b) {
r=pa;
pa=pa->next;
free(r);
}
else pb=pb->next;
}
else{
pc->next=pa;
pc=pc->next;
pa=pa->next;
do{pb=pb->next;}
while(pb&&pb->data==b);
}
}
pc->next=NULL;
C=A;
}
◆2.29⑤
已知A,B和C为三个递增有序的线性表,现要求对A表进行如下操作:删去那些既在B表中出现又在C表中出现的元素。试对顺序表编写实现上述操作的算法,并分析你的算法的时间复杂度(注意:题中没有特别指明同一表中的元素值各不相同)。
void ListDelete(SqList &A,SqList B,SqList C){
//另设集合D=A∩B∩C,A=A-D(“-”表示求集合差的运算)
SqList D=A;
D.elem=(ElemType*)malloc(sizeof(ElemType)*D.listsize);
memcpy(D.elem,A.elem,sizeof(ElemType)*D.listsize);
ListIntersection(D,B);//2.27题的算法
ListIntersection(D,C);
ElemType *pa=A.elem,*ma=pa+A.length,
*pd=D.elem,*md=pd+D.length;
ElemType *p=pa;
while(pa<ma){
if(*pa!=*pd){
*(p++)=*pa;
pa++;
continue;
}
else{
do{pa++;}while(pa<ma&&*pa==*pd);
pd++;if(pd==md) break;pb--;
//B表比较完,应退回一位,因为A表可能未比较完
}
}
A.length=p-A.elem;
}
从以上代码分离出求集合差A=A-B的算法:
void SetSub(SqList &A,SqList B){
//初始条件:B是A的子集
ElemType *pa=A.elem,*ma=pa+A.length,
*pb=B.elem,*mb=pb+B.length;
ElemType *p=pa;
while(pa<ma){
if(*pa!=*pb){
*(p++)=*pa;
pa++;
continue;
}
else{
do{pa++;}while(pa<ma&&*pa==*pb);
ElemType e=*pb;
do{pb++;}while(pb<mb&&e==*pb);
if(pb==mb) pb--;
}
}
A.length=p-A.elem;
}
使用SetSub函数,并用表B存放表D,可将代码进一步简化。
void ListDelete(SqList &A,SqList B,SqList C){
ListIntersection(B,C);//时间复杂度:O(B.length+C.length)
ListIntersection(B,A); //时间复杂度:O(B.length+A.length)
SetSub(A,B); //时间复杂度:O(A.length+B.length)
}
最坏情况下的时间复杂度:O(B.length)
◆2.30⑤
要求同2.29题。试对单链表编写算法,请释放A表中的无用结点空间。
void ListIntersection(LinkList A,LinkList B,LinkList &C){
//本算法求两个递增有序单链表A和B的交集C,C同样递增有序
InitList(C);
LinkList pa=A->next,pb=B->next,pc=C,s;
ElemType a,b;
while(pa&&pb){
a=pa->data,b=pb->data;
if(a!=b){
if(a<b) pa=pa->next;
else pb=pb->next;
}
else{
s=(LinkList)malloc(sizeof(LNode));
s->data=a;
pc->next=s;
pc=s;
pa=pa->next;
do{pb=pb->next;}
while(pb&&pb->data==b);
}
}
pc->next=NULL;
}
集合差算法链表版:
void SetSub(LinkList &A,LinkList B){
//初始条件:B是A的子集
LinkList pa=A->next,pb=B->next;
LinkList p=pa;
while(pa){
ElemType a=pa->data,b=pb->data;
if(a!=b){
p->data=a;
p=p->next;
pa=pa->next;
}
else{
do{pa=pa->next;}while(pa&&pa->data==b);
while(pb->next&&pb->data==b) pb=pb->next;
}
}
}
void ListDelete(LinkList &A,LinkList B,LinkList C){
LinkList D;InitList(D);
ListIntersection(B,C,D);//2.26题的算法,O(B.length+C.length)
ListIntersection(D,A,D);//O(D.length+A.length)
SetSub(A,D); //O(A.length+D.length)
}
最坏情况下的时间复杂度:O(A.length)
2.31②
假设某个单向循环链表的长度大于1,且表中既无头结点也无头指针。已知s为指向链表中某个结点的指针,试编写算法在链表中删除指针s所指结点的前驱结点。
void ListDelete_Prior(CLinkList s)
{
s=s->prior;
s->prior->next=s->next;
s->next->prior=s->prior;
free(s);
}
2.32②
已知有一个单向循环链表,其每个结点中含三个域:prior,data和next,其中data为数据域,next为指向后继结点的指针域,prior也为指针域,但它的值为空(NULL),试编写算法将此单向循环链表改为双向循环链表,即使prior成为指向前驱结点的指针域。
void PriorGenerate(DuLinkList &L)
{
DuLinkList p=L,q=L->next;
do{
q->prior=p;
p=q;
q=q->next;
}while(p!=L);
}
◆2.33③
已知由一个线性链表表示的线性表中含有三类字符的数据元素(如:字母字符、数字字符和其他字符),试编写算法将该线性链表分隔为三个循环链表,其中每个循环链表表示的线性表中均只含一类字符。
// 线性表的单循环链表存储结构(改写自"c2-4.h")
typedef struct CLNode
{
ElemType data;
CLNode *prior,*next;
}CLNode,*CLinkList;
Status InitList(CLinkList &L){
//产生空的单循环链表L
L=(CLinkList)malloc(sizeof(CLNode));
if(!L) return OVERFLOW;
L->next=L;
return OK;
}
Status EndInsert(CLinkList &t,ElemType e){
//本函数在以t为尾指针的单循环链表末尾添加元素e
CLinkList h=t->next;
CLinkList s=(CLinkList)malloc(sizeof(CLNode));
s->data=e;
t->next=s;
s->next=h;
t=s;
return OK;
}
void ListDivide(LinkList L,CLinkList &A,CLinkList &B,CLinkList &C){
//本算法将由三种字符组成的单链表分隔为三个各含一种字符的循环链表
InitList(A);
InitList(B);
InitList(C);
CLinkList a=A,b=B,c=C;
int n=0;
char c1,c2,c3;
LinkList p=L->next;
while(p){
char ch=p->data;
switch(n){
case 0:n++;c1=ch;EndInsert(a,ch);
break;
case 1:if(ch==c1) EndInsert(a,ch);
else{n++;c2=ch;EndInsert(b,ch);}
break;
case 2:if(ch==c1) {EndInsert(a,ch);break;}
else if(ch==c2) {EndInsert(b,ch);break;}
else{n++;c3=ch;EndInsert(c,ch);
for(p=p->next;p;p=p->next)
switch(p->data){
case c1:EndInsert(a,c1);break;
case c2:EndInsert(b,c2);break;
case c3:EndInsert(c,c3);break;
}
}
}
p=p->next;
}
}
2.34④
XorPointer XorP(XorPointer p, XorPointer q)
{//指针异或函数XorP返回指针p和q的异或(XOR)值
//因为指针变量不支持按位异或运算,需要强制类型转换为数值类型
//(其值为指针变量内存储的内存地址,长度与机器有关,C将其封装为size_t型)
size_t x = (size_t)p;
size_t y = (size_t)q;
return (XorPointer)(x^y);
}
Status CreateList(XorLinkedList &L,char s[]){
//本算法建立一个2.34题所述的存储结构定义的双向链表L
//数据源为字符数组a
const int N=15;//数据长度上限
int n=strlen(s);
if(n>N) return ERROR;
XorPointer p[N];
for(int i=0;i<n;i++){
p[i]=(XorPointer)malloc(sizeof(XorNode));
if(!p[i]) return OVERFLOW;
p[i]->data=s[i];
}
if(n>=1){L.Left=p[0];L.Right=p[n-1];p[0]->LRPtr=NULL;}
else L.Left=L.Right=NULL;
if(n>=2) {p[n-1]->LRPtr=p[n-2];p[0]->LRPtr=p[1];}
for(int i=1;i<n-1;i++)
p[i]->LRPtr=XorP(p[i-1],p[i+1]);
return OK;
}
先写出正向遍历的算法:
void PrintList(XorLinkedList L){
XorPointer p=NULL,q=L.Left;
if(!q) return;//空表
putchar(q->data);//输出结点1
XorPointer pq,r;
for(pq=q->LRPtr;pq!=p;pq=q->LRPtr){
//结点1->LRPtr=结点0○+结点2;
//结点2->LRPtr=结点1○+结点3
r=q;//保存结点1
q=XorP(p,pq);//结点2= 结点0○+ (结点0○+结点2)
putchar(q->data);//输出结点2
p=r;//取出结点1
}
}
可以证明,只需修改以上算法中的一条语句即可实现从右向左遍历,见注释。(约定“结点-x”为指向倒数第x个结点的指针)
void PrintList_Anti(XorLinkedList L){
XorPointer p=NULL,q=L.Right;
if(!q) return;//空表
putchar(q->data);//输出结点-1
XorPointer pq,r;
for(pq=q->LRPtr;pq!=p;pq=q->LRPtr){
//结点-1->LRPtr=结点0○+结点-2;
//结点-2->LRPtr=结点-1○+结点-3
r=q;//保存结点-1
q=XorP(p,pq);//结点-2= 结点0○+ (结点0○+结点-2)
putchar(q->data);//输出结点-2
p=r;//取出结点-1
}
}
将以上两个算法进行合并:
void PrintList(XorLinkedList L, Boolean forward){
//本算法以任意方向遍历2.34题所述的存储结构定义的双向链表L,
//forward指定遍历方向:true为从左向右,false为从右向左。
XorPointer p=NULL, q=forward?L.Left:L.Right;
if(!q) return;//空表
putchar(q->data);//输出结点±1
XorPointer pq,r;
for(pq=q->LRPtr;pq!=p;pq=q->LRPtr){
//结点1->LRPtr=结点0○+结点±2;
//结点2->LRPtr=结点±1○+结点±3
r=q;//保存结点±1
q=XorP(p,pq);//结点±2= 结点0○+ (结点0○+结点±2)
putchar(q->data);//输出结点±2
p=r;//取出结点±1
}
}
2.37④
设以带头结点的双向循环链表表示的线性表L=(a1,a2,…,an)。试写一个时间复杂度为O(n)的算法,将L改造为L=(a1,a3,…,an,…,a4,a2)。
void ListTransform(DuLinkList &L){
DuLinkList p=L->next,q;
while(true){
if(p->next==L){
p->next=p->prior;
p=p->next;
break;
}
else{
q=p->next->next;
if(q==L){
p=p->next;
break;
}
else{
p->next=q;
p=p->next;
}
}
}//生成原奇数位序结点的next域
while(p!=L){
p->next=p->prior->prior;
p=p->next;
}//生成原偶数位序结点的next域
for(p=L->next;p!=L;){
q=p->next;
q->prior=p;
p=q;
}//生成所有结点的prior域
}
◆2.38④
设有一个双向循环链表,每个结点中除有prior,data和next三个域外,还增设了一个访问频度域freq。在链表被起用之前,频度域freq的值均初始化为零,而每当对链表进行一次LOCATE(L,x)的操作后,被访问的结点(即元素值等于x的结点)的频度域freq的值便增1,同时调整链表中结点之间的次序,使其按访问频度非递增的次序顺序排列,以便始终保持被频繁访问的结点总是靠近表头结点。试编写符合上述要求的LOCATE操作的算法。
int LOCATE(DuLinkList &L,ElemType x){
//初始条件: 增加了频度域freq的双向循环链表L已存在,x为待查找元素的数值
//操作结果: 返回L中该元素的位序,若这样的数据元素不存在,则返回值为0
DuLinkList p=L,q=L->next,r;
//p指向上一“频度段”末尾,q指向当前“频度段”中某一结点
int oldn=0,n=1;//oldn为p位序,n为q位序
while(q!=L&&q->data!=x){
r=q->next;
if(r!=L&&r->freq!=q->freq) {oldn=n;p=q;}
q=r;n++;
}
if(q==L) return 0;
q->freq++;
if(q->prior==L||q->prior->freq==q->freq) return n;
q->prior->next=q->next;
q->next->prior=q->prior;//删除结点q
q->next=p->next;
q->prior=p;
p->next=q;
q->next->prior=q;//结点p之后插入结点q
//移动:删除插入二合一,无需新建结点
return oldn+1;
}
◆2.39③
double Pn(SqPoly p,double x0){
// p.data[0].exp=0,从p.data[1]开始存储数据
PolyTerm *e=p.data;
int m=p.length;
double sum=0,xei=1;
for(int i=1;i<=m;i++){
for(int j=e[i-1].exp;j<e[i].exp;j++)
xei*=x0;//累乘一共进行了em次
sum=sum+e[i].coef*xei;//累加一共进行了m次
}
return sum;
}
时间复杂度:O(em+m)
2.40③
void Sub(SqPoly p1,SqPoly p2,SqPoly &p3){
//本算法计算多项式减法P[3](x)=P[1](x)-P[2](x)
p3.data=(PolyTerm *)malloc(sizeof(PolyTerm)*(p1.length+p2.length));
p3.length=0;//对于不设listsize的顺序表派生数据类型,
//其表长最好由实际情况决定
PolyTerm *t1=p1.data,*t2=p2.data,*t3=p3.data;
//t1,t2,t3分别为p1,p2,p3中的末尾游标
PolyTerm *m1=t1+p1.length,m2=t2+p2.length;
while(t1<m1&&t2<m2){
int ei=t1->exp,ej=t2->exp;
if(ej<ei) {*t3=*t2;t2++;t3++;}
else{
if(ej>ei){*t3=*t1;t3++;}
else{
t1->coef-=t2->coef;
if(t1->coef!=0){*t3=*t1;t3++;}
t2++;
}
t1++;
}
}
while(t1<m1) {*t3=*t1;t1++;t3++;}
while(t2<m2) {*t3=*t2;t3->exp/=(-1);t2++;t3++;}
p3.length=t3-p3.data;
p3.data=(PolyTerm *)realloc(p3.data,sizeof(PolyTerm)*(p3.length));
//去除多余空间
}
时间复杂度:O(p1.length+p2.length)
◆2.41②
Status Differentiate(LinkedPoly &L){
LinkedPoly p=L,q=p->next,r=q->next;
while(r!=L){
q->data.coef*=q->data.exp;
q->data.exp--;
q=p;r=q;r=r->next;
}
if(q->data.coef==0){
free(q);
p->next=L;
}
return OK;
}
2.42②
试编写算法,将一个用循环链表表示的稀疏多项式分解成两个多项式,使这两个多项式中各自仅含奇次项或偶次项,并要求利用原链表中的结点空间构成这两个链表。
Status ParitySeparation(LinkedPoly &L0,LinkedPoly &L1,LinkedPoly &L2){
//本算法将稀疏多项式L0分解成仅含奇次项的稀疏多项式L1和仅含偶次项
//的稀疏多项式L2
L1=L0;
if(InitPoly(L2)==OVERFLOW) return OVERFLOW;
LinkedPoly p0=L1->next,p1=L1,p2=L2;
while(p0!=L){
if(p0->data.exp%2==1){
p1->next=p0;p1=p1->next;
}
else{
p2->next=p0;p2=p2->next;
}
p0=p0->next;
}
p1->next=L1;p2->next=L2;
return OK;
}