DS2.3

01.在带头结点的单链表L中,删除所有值为x的结点,并释放其空间,假设值为x的结点不唯一,试编写算法以实现上述操作。

解法1:用p从头至尾扫描单链表,pre指向*p结点的前驱。若p所指结点的值为x,则删
除,并让p移向下一个结点,否则让pre、p指针同步后移一个结点。
本题代码如下:
void Del_X_1(LinkList &L, ElemType x) {
  LNode *p = L->next, *pre = L, *q; // 置p和pre的初始值
  while (p != NULL) {
    if (p->data == x) {
      q = p; // q指向被删结点
      p = p->next;
      pre->next = p; // 将*q结点从链表中断开
      free(q);       // 释放*q结点的空间
    } else {         // 否则,pre和p同步后移
      pre = p;
      p = p->next;
    } // else
  }   // while
}
本算法是在无序单链表中删除满足某种条件的所有结点,这里的条件是结点的值为x。实际
上,这个条件是可以任意指定的,只要修改if条件即可。比如,我们要求删除值介于mink和
maxk之间的所有结点,则只需将if语句修改为if(p->data>mink&&p->data<maxk)。

解法2:采用尾插法建立单链表。用P指针扫描L的所有结点,当其值不为x时,将其链接
到L之后,否则将其释放。
本题代码如下:
void Del_X_2(LinkList &L, ElemType x) {
  LNode *p = L->next, *r = L, *q; // r指向尾结点,其初值为头结点
  while (p != NULL) {
    if (p->data != x) { // *p结点值不为x时将其链接到L尾部
      r->next = p;
      r = p;
      p = p->next; // 继续扫描
    } else {       // *p结点值为x时将其释放
      q = p;
      p = p->next; // 继续扫描
      free(q);     // 释放空间
    }
  }               // while
  r->next = NULL; // 插入结束后置尾结点指针为NULL
}
上述两个算法扫描一遍链表,时间复杂度为O(n),空间复杂度为O(1)。

02.试编写在带头结点的单链表L中删除一个最小值结点的高效算法(假设该结点唯一)。

算法思想:用p从头至尾扫描单链表,pre指向*p结点的前驱,用minp保存值最小的结
点指针(初值为p),minpre指向*minp结点的前驱(初值为pre)。一边扫描,一边比较,若
p->data小于minp->data,则将p、pre分别赋值给minp、minpre,如下图所示。当p扫
描完毕时,minp指向最小值结点,minpre指向最小值结点的前驱结点,再将minp所指结点删
除即可。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

本题代码如下:
LinkList Delete_Min(LinkList &L) {
  LNode *pre = L, *p = pre->next; // p为工作指针,pre指向其前驱
  LNode *minpre = pre, *minp = p; // 保存最小值结点及其前驱
  while (p != NULL) {
    if (p->data < minp->data) {
      minp = p; // 找到比之前找到的最小值结点更小的结点
      minpre = pre;
    }
    pre = p; // 继续扫描下一个结点
    p = p->next;
  }
  minpre->next = minp->next; // 删除最小值结点
  free(minp);
  return L;
}
算法需要从头至尾扫描链表,时间复杂度为O(n),空间复杂度为O(1)。

若本题改为不带头结点的单链表,则实现上会有所不同,请读者自行思考。

03.试编写算法将带头结点的单链表就地逆置,所谓“就地”是指辅助空间复杂度为O(1)。

解法1:将头结点摘下,然后从第一结点开始,依次插入到头结点的后面(头插法建立单链
表),直到最后一个结点为止,这样就实现了链表的逆置,如下图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

本题代码如下:
LinkList Reverse_1(LinkList L) {
  LNode *p, *r;        // p 为工作指针,r为p的后继,以防断链
  p = L->next;         // 从第一个元素结点开始
  L->next = NULL;      // 先将头结点L的next域置为NULL
  while (p != NULL) {  // 依次将元素结点摘下
    r = p->next;       // 暂存p的后继
    p->next = L->next; // 将p结点插入到头结点之后
    L->next = p;
    p = r;
  }
  return L;
}
解法2:大部分辅导书都只介绍解法1,这对读者的理解和思维是不利的。为了将调整指针
这个复杂的过程分析清楚,我们借助图形来进行直观的分析。

假设pre、p和r指向三个相邻的结点,如下图所示。假设经过若干操作后,*pre之前的
结点的指针都已调整完毕,它们的next都指向其原前驱结点。现在令*p结点的next域指向
*pre结点,注意到一旦调整指针的指向,*p的后继结点的链就会断开,为此需要用r来指向原
*p的后继结点。处理时需要注意两点:一是在处理第一个结点时,应将其next域置为NULL,
而不是指向头结点(因为它将作为新表的尾结点):二是在处理完最后一个结点后,需要将头结
点的指针指向它。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

本题代码如下:
LinkList Reverse_2(LinkList L) {
  LNode *pre, *p = L->next, *r = p->next;
  p->next = NULL;     // 处理第一个结点
  while (r != NULL) { // r为空,则说明p为最后一个结点
    pre = p;          // 依次继续遍历
    p = r;
    r = r->next;
    p->next = pre; // 指针反转
  }
  L->next = p; // 处理最后一个结点
  return L;
}
上述两个算法的时间复杂度为O(n),空间复杂度为O(1)。  

04.设在一个带表头结点的单链表中,所有结点的元素值无序,试编写一个函数,删除表中
所有介于给定的两个值(作为函数参数给出)之间的元素(若存在)。

因为链表是无序的,所以只能逐个结点进行检查,执行删除。
本题代码如下:
void RangeDelete(LinkList &L, int min, int max) {
  LNode *pr = L, *p = L->link; // p是检测指针,pr是其前驱
  while (p != NULL)
    if (p->data > min && p->data < max) { // 寻找到被删结点,删除
      pr->link = p->link;
      free(p);
      p = pr->link;
    } else { // 否则继续寻找被删结点
      pr = p;
      p = p->link;
    }
}

05.给定两个单链表,试分析找出两个链表的公共结点的思想(不用写代码)。

两个单链表有公共结点,即两个链表从某一结点开始,它们的next都指向同一结点。由于每
个单链表结点只有一个next域,因此从第一个公共结点开始,之后的所有结点都是重合的,不可
能再出现分叉。所以两个有公共结点而部分重合的单链表,拓扑形状看起来像Y,而不可能像X。

本题极容易联想到“蛮”方法:在第一个链表上顺序遍历每个结点,每遍历一个结点,在第
二个链表上顺序遍历所有结点,若找到两个相同的结点,则找到了它们的公共结点。显然,该算
法的时间复杂度为O(len1×len2)。

接下来我们试着去寻找一个线性时间复杂度的算法。先把问题简化:如何判断两个单向链表
有没有公共结点?应注意到这样一个事实:若两个链表有一个公共结点,则该公共结点之后的所
有结点都是重合的,即它们的最后一个结点必然是重合的。因此,我们判断两个链表是不是有重
合的部分时,只需要分别遍历两个链表到最后一个结点。若两个尾结点是一样的,则说明它们有
公共结点,否则两个链表没有公共结点。

然而,在上面的思路中,顺序遍历两个链表到尾结点时,并不能保证在两个链表上同时到达
尾结点。这是因为两个链表长度不一定一样。但假设一个链表比另一个长k个结点,我们先在长
的链表上遍历k个结点,之后再同步遍历,此时我们就能保证同时到达最后一个结点。由于两个
链表从第一个公共结点开始到链表的尾结点,这一部分是重合的,因此它们肯定也是同时到达第
一公共结点的。于是在遍历中,第一个相同的结点就是第一个公共的结点。

根据这一思路中,我们先要分别遍历两个链表得到它们的长度,并求出两个长度之差。在长
的链表上先遍历长度之差个结点之后,再同步遍历两个链表,直到找到相同的结点,或者一直到
链表结束。此时,该方法的时间复杂度为O(lenl+len2)。

06.设C={a1,b1,a2,b2,…,an,bn}为线性表,采用带头结点的单链表存放,设计一个就地算
法,将其拆分为两个线性表,使得A={a1,a2,…,an},B={bn,…,b2,b1}.

算法思想:循环遍历链表C,采用尾插法将一个结点插入表A,这个结点为奇数号结点,这
样建立的表A与原来的结点顺序相同:采用头插法将下一结点插入表B,这个结点为偶数号结点,
这样建立的表B与原来的结点顺序正好相反。
本题代码如下:
LinkList DisCreat_2(LinkList &A) {
  LinkList B = (LinkList)malloc(sizeof(LNode)); // 创建B表表头
  B->next = NULL;                               // B表的初始化
  LNode *p = A->next, *q;                       // p为工作指针
  LNode *ra = A;                                // ra始终指向A的尾结点
  while (p != NULL) {
    ra->next = p;
    ra = p; // 将*p链到A的表尾
    p = p->next;
    if (p != NULL) {
      q = p->next; // 头插后,*p将断链,因此用q记忆*p的后继
      p->next = B->next; // 将*p插入到B的前端
      B->next = p;
      p = q;
    }
  }
  ra->next = NULL; // A尾结点的next域置空
  return B;
}
该算法特别需要注意的是,采用头插法插入结点后,*p的指针域已改变,若不设变量保存
其后继结点,则会引起断链,从而导致算法出错。

07.在一个递增有序的单链表中,存在重复的元素。设计算法删除重复的元素,例如(7,10,
10,21,30,42,42,42,51,70)将变为(7,10,21,30,42,51,70)。

算法思想:由于是有序表,因此所有相同值域的结点都是相邻的。用p扫描递增单链表L,
若*p结点的值域等于其后继结点的值域,则删除后者,否则p移向下一个结点。
本题代码如下:
void Del_Same(LinkList &L) {
  LNode *p = L->next, *q; // p为扫描工作指针
  if (p == NULL)
    return;
  while (p->next != NULL) {
    q = p->next;              // q指向*p的后继结点
    if (p->data == q->data) { // 找到重复值的结点
      p->next = q->next;      //  释放*q结点
      free(q);                // 释放相同元素值的结点
    } else
      p = p->next;
  }
}
本算法的时间复杂度为O(n),空间复杂度为O1)。
本题也可采用尾插法,将头结点摘下,然后从第一结点开始,依次与已经插入结点的链表的
最后一个结点比较,若不等则直接插入,否则将当前遍历的结点删除并处理下一个结点,直到最
后一个结点为止。

08.设A和B是两个单链表(带头结点),其中元素递增有序。设计一个算法从A和B中的
公共元素产生单链表C,要求不破坏A、B的结点。

算法思想:表A、B都有序,可从第一个元素起依次比较A、B两表的元素,若元素值不等,
则值小的指针往后移,若元素值相等,则创建一个值等于两结点的元素值的新结点,使用尾插法
插入到新的链表中,并将两个原表指针后移一位,直到其中一个链表遍历到表尾。
本题代码如下:
void Get_Common(LinkList A, LinkList B) {
  LNode *p = A->next, *q = B->next, *r, *s;
  LinkList C = (LinkList)malloc(sizeof(LNode)); // 建立表C
  r = C;                                        // r始终指向C的尾结点
  while (p != NULL && q != NULL) {              // 循环跳出条件
    if (p->data < q->data)
      p = p->next; // 若A的当前元素较小,后移指针
    else if (p->data > q->data)
      q = q->next; // 若B的当前元素较小,后移指针
    else {         // 找到公共元素结点
      s = (LNode *)malloc(sizeof(LNode));
      s->data = p->data; // 复制产生结点*s
      r->next = s;       // 将*s链接到C上(尾插法)
      r = s;
      p = p->next; // 表A和B继续向后扫描
      q = q->next;
    }
  }
  r->next = NULL; // 置C尾结点指针为空
}

09.已知两个链表A和B分别表示两个集合,其元素递增排列。编制函数,求A与B的交
集,并存放于A链表中。

算法思想:采用归并的思想,设置两个工作指针pa和pb,对两个链表进行归并扫描,只有
同时出现在两集合中的元素才链接到结果表中且仅保留一个,其他的结点全部释放。当一个链表
遍历完毕后,释放另一个表中剩下的全部结点。
本题代码如下:
LinkList Union(LinkList &la, LinkList &lb) {
  LNode *pa = la->next; // 设工作指针分别为pa和pb
  LNode *pb = lb->next;
  LNode *u, *pc = la; // 结果表中当前合并结点的前驱指针pc
  while (pa && pb) {
    if (pa->data == pb->data) { // 交集并入结果表中
      pc->next = pa;            // A中结点链接到结果表
      pc = pa;
      pa = pa->next;
      u = pb; // B中结点释放
      pb = pb->next;
      free(u);
    } else if (pa->data < pb->data) { // 若A中当前结点值小于B中当前结点值
      u = pa;
      pa = pa->next; // 后移指针
      free(u);       // 释放A中当前结点
    } else {         // 若B中当前结点值小于A中当前结点值
      u = pb;
      pb = pb->next; // 后移指针
      free(u);       // 释放B中当前结点
    }
  }            // while结東
  while (pa) { // B已遍历完,A未完
    u = pa;
    pa = pa->next;
    free(u); // 释放A中剩余结点
  }
  while (pb) { // aA已遍历完,B未完
    u = pb;
    pb = pb->next;

    free(u); // 释放B中剩余结点
  }
  pc->next = NULL; // 置结果链表尾指针为NOLL
  free(lb);        // 释放B表的头结点
  return la;
}
链表归并类型的试题在各学校历年真题中出现的频率很高,故应扎实掌握解决此类问题的思
想。该算法的时间复杂度为O(len1+len2),空间复杂度为O(1)。

10.两个整数序列A=a1,a2,a3,…,am和B=b1,b2,b3,…,bn已经存入两个单链表中,设计一
个算法,判断序列B是否是序列A的连续子序列。

算法思想:因为两个整数序列已存入两个链表中,操作从两个链表的第一个结点开始,若对
应数据相等,则后移指针:若对应数据不等,则A链表从上次开始比较结点的后继开始,B链表
仍从第一个结点开始比较,直到B链表到尾表示匹配成功。A链表到尾而B链表未到尾表示失败。
操作中应记住A链表每次的开始结点,以便下次匹配时好从其后继开始。
本题代码如下:
int Pattern(LinkList A, LinkList B) {
  LNode *p = A; // p为A链表的工作指针,本题假定A和B均无头结点
  LNode *pre = p; // pre记住每趟比较中A链表的开始结点
  LNode *q = B;   // q是B链表的工作指针
  while (p && q)
    if (p->data = q->data) { // 结点值相同
      p = p->next;
      q = q->next;
    } else {
      pre = pre->next;
      p = pre; // A链表新的开始比较结点
      q = B;   // q从B链表第一个结点开始
    }
  if (q == NULL) // B已经比较结束
    return 1;    // 说明B是A的子序列
  else
    return 0; // B不是A的子序列
}
该题其实是字符串模式匹配的链式表示形式,读者应该结合字符串模式匹配的内容重新考
虑能否优化该算法。

11.设计一个算法用于判断带头结点的循环双链表是否对称。

算法思想:让p从左向右扫描,q从右向左扫描,直到它们指向同一结点(p=q,当循环
双链表中结点个数为奇数时)或相邻(p->next=g或q->prior=p,当循环双链表中结点个数
为偶数时)为止,若它们所指结点值相同,则继续进行下去,否则返回0。若比较全部相等,则
返回1。
本题代码如下:
int Symmetry(DLinkList L) {
  DNode *p = L->next, *q = L->prior; // 两头工作指针
  while (p != q && q->next != p)     // 循环跳出条件
    if (p->data == q->data) {        // 所指结点值相同则继续比较
      p = p->next;
      q = q->prior;
    } else // 否则,返回0
      return 0;
  return 1; // 比较结束后返回1
}
whi1e循环第二个判断条件易误写成p->next!=q,分析这样会产生什么问题。

12.有两个循环单链表,链表头指针分别为h1和h2,编写一个函数将链表h2链接到链表
h1之后,要求链接后的链表仍保持循环链表形式.

算法思想:先找到两个链表的尾指针,将第一个链表的尾指针与第二个链表的头结点链接起
来,再使之成为循环的。
本题代码如下:
LinkList Link(LinkList &h1,LinkList &h2) {
  // 将循环链表h2链接到循环链表h1之后,使之仍保持循环链表的形式
  LNode *p, *q; // 分别指向两个链表的尾结点
  p = h1;
  while (p->next != h1) // 得找h1的尾结点
    p = p->next;
  q = h2;
  while (q->next != h2) // 寻找h2的尾结点
    q = q->next;
  p->next = h2; // 将h2链接到h1之后
  q->next = h1; // 令h2的尾结点指向h1
  return h1;
}

13.设有一个带头结点的非循环双链表L,其每个结点中除有pre、data和next域外,
还有一个访问频度域freq,其值均初始化为零。每当在链表中进行一次Locate(L,x)
运算时,令值为x的结点中freq域的值增1,并使此链表中的结点保持按访问频度递
减的顺序排列,且最近访问的结点排在频度相同的结点之前,以便使频繁访问的结点总
是靠近表头。试编写符合上述要求的Locate(L,x)函数,返回找到结点的地址,类型
为指针型。

算法思想:首先在双向链表中查找数据值为×的结点,查到后,将结点从链表上摘下,然后
顺着结点的前驱链查找该结点的插入位置(频度递减,且排在同频度的第一个,即向前找到第一
个比它的频度大的结点,插入位置为该结点之后),并插入到该位置。
本题代码如下:
DLinkList Locate(DLinkList &L, ElemType x) {
  DNode *p = L->next, *q; // p为工作指针,q为p的前驱,用于查找插入位置
  while (p && p->data != x)
    p = p->next; // 查找值为x的结点
  if (!p)
    exit(0); // 不存在值为x的结点
  else {
    p->freq++; // 令元素值为x的结点的freq域加1
    if (p->pre == L || p->pre->freq > p->freq)
      return p; // p是链表首结点,或freq值小于前驱
    if (p->next != NULL)
      p->next->pre = p->pre;
    p->pre->next = p->next; // 将p结点从链表上摘下
    q = p->pre;             // 以下查找p结点的插入位置
    while (q != L && q->freq <= p->freq)
      q = q->pre;
    p->next = q->next;
    if (q->next != NULL)
      q->next->pre = p; // 将p结点排在同频率的第一个
    p->pre = q;
    q->next = p;
  }
  return p; // 返回值为x的结点的指针
}

14.设将n(n>1)个整数存放到不带头结点的单链表L中,设计算法将L中保存的序列循环
右移k(0<k<n)个位置。例如,若k=1,则将链表{0,1,2,3}变为{3,0,1,2}。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。

1)算法的基本设计思想:
首先,遍历链表计算表长,并找到链表的尾结点,将其与首结点相连,得到一个循环单链
表。然后,找到新链表的尾结点,它为原链表的第n-k个结点,令L指向新链表尾结点的下一
个结点,并将环断开,得到新链表。
2)本题代码如下:
LNode *Converse(LNode *L, int k) {
  int n = 1;                // n用来保存链表的长度
  LNode *p = L;             // p为工作指针
  while (p->next != NULL) { // 计算链表的长度
    p = p->next;
    n++;
  }            // 循环执行完后,p指向链表尾结点
  p->next = L; // 将链表连成一个环
  for (int i = 1; i <= n - k; i++) // 寻找链表的第n-k个结点
    p = p->next;
  L = p->next;    // 令工指向新链表尾结点的下一个结点
  p->next = NULL; // 将环断开
  return L;
}
3)本算法的时间复杂度为O(n),空间复杂度为O(1)。

15.单链表有环,是指单链表的最后一个结点的指针指向了链表中的某个结点(通常单链表
的最后一个结点的指针域是空的)。试编写算法判断单链表是否存在环。
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关健之处给出注释.
3)说明你所设计算法的时间复杂度和空间复杂度。

1)算法的基本设计思想
设置快慢两个指针分别为fast和slow最初都指向链表头head。slow每次走一步,即
slow=slow->next;fast每次走两步,即fast=fast->next->next。fast比slow走得
快,若有环,则fast一定先进入环,而slow后进入环。两个指针都进入环后,经过若干操作
后两个指针定能在环上相遇。这样就可以判断一个链表是否有环。

如下图所示,当slow刚进入环时,fast早已进入环。因为fast每次比slow多走一步
且fast与slow的距离小于环的长度,所以fast与slow相遇时,slow所走的距离不超过环的
长度。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如下图所示,设头结点到环的入口点的距离为a,环的入口点沿着环的方向到相遇点的距离
为x,环长为r,相遇时fast绕过了n圈。

!外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

则有2(a+x)=a+n*r+x,即a=nr-x。显然从头结点到环的入口点的距离等于n倍的环长减去
环的入口点到相遇点的距离。因此可设置两个指针,一个指向head,一个指向相遇点,两个指
针同步移动(均为一次走一步),相遇点即为环的入口点。
2)本题代码如下:

LNode *FindLoopstart(LNode *head) {
  LNode *fast = head, *slow = head; // 设置快慢两个指针
  while (fast != NULL && fast->next != NULL) {
    slow = slow->next;       // 每次走一步
    fast = fast->next->next; // 每次走两步
    if (slow == fast)
      break; // 相遇
  }
  if (fast == NULL || fast->next == NULL)
    return NULL;                // 没有环,返回NULL
  LNode *p1 = head, *p2 = slow; // 分别指向开始点、相遇点
  while (p1 != p2) {
    p1 = p1->next;
    p2 = p2->next;
  }
  return p1; // 返回入口点
}
3)当fast与slow相遇时,slow肯定没有遍历完链表,故算法的时间复杂度为O(n),空
间复杂度为O(1)。
  1. 设有一个长度n(n为偶数)的不带头结点的单链表,且结点值都大于0,设计算法求这
    个单链表的最大孪生和。孪生和定义为一个结点值与其孪生结点值之和,对于第i个结
    点(从0开始),其孪生结点为第n-i-1个结点。要求:
    1)给出算法的基本设计思想。
    2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
    3)说明你的算法的时间复杂度和空间复杂度。

    1)算法的基本设计思想:
    设置快、慢两个指针分别为fast和slow,初始时slow指向L(第一个结点),fast指向
    L->next(第二个结点),之后slow每次走一步,fast每次走两步。当fast指向表尾(第n个
    结点)时,slow正好指向链表的中间点(第/2个结点),即slow正好指向链表前半部分的最后
    一个结点。将链表的后半部分逆置,然后设置两个指针分别指向链表前半部分和后半部分的首结点,
    在遍历过程中计算两个指针所指结点的元素之和,并维护最大值。
    2)本题代码如下:

int PairSum(LinkList L) {
  LNode *fast = L->next, *slow = L; // 利用快慢双指针找到链表的中间点
  while (fast != NULL && fast->next != NULL) {
    fast = fast->next->next; // 快指针每次走两步
    slow = slow->next; // 慢指针每次走一步
  }
  LNode *newHead = NULL, *p = slow->next, *tmp;
  while (p != NULL) { // 反转链表后一半部分的元素,采用头插法
    tmp = p->next; // p指向当前待插入结点,令tmp指向其下一结点
    p->next = newHead; // 将P所指结点插入到新链表的首结点之前
    newHead = p; // newHead指向刚才新插入的结点,作为新的首结点
    p = tmp; // 当前待处理结点变为下一结点
  }
  int mx = 0;
  p = L;
  LNode *q = newHead;
  while (p != NULL) { // 用p和q分别遍历两个链表
    if ((p->data + q->data) > mx) // 用mx记录最大值
      mx = p->data + q->data;
    p = p->next;
    q = q->next;
  }
  return mx;
}
3)本算法的时间复杂度为O(n),空间复杂度为O(1)。

17.【2009统考真题】已知一个带有表头结点的单链表,结点结构为
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

假设该链表只给出了头指针list。在不改变链表的前提下,请设计一个尽可能高效的
算法,查找链表中倒数第k个位置上的结点(k为正整数)。若查找成功,算法输出该结
点的data域的值,并返回1;否则,只返回0。要求:
1)描述算法的基本设计思想。
2)描述算法的详细实现步骤。
3)根据设计思想和实现步骤,采用程序设计语言描述算法(使用C、C++或Jva语言
实现),关键之处请给出简要注释。

1)算法的基本设计思想如下:
问题的关键是设计一个尽可能高效的算法,通过链表的一次遍历,找到倒数第k个结点的位
置。算法的基本设计思想是:定义两个指针变量P和q,初始时均指向头结点的下一个结点(链
表的第一个结点),P指针沿链表移动:当P指针移动到第k个结点时,q指针开始与P指针同
步移动:当P指针移动到最后一个结点时,q指针所指示结点为倒数第k个结点。以上过程对链
表仅进行一遍扫描。

2)算法的详细实现步骤如下:
①count=0,p和q指向链表表头结点的下一个结点。
②若p为空,转⑤。
③若count等于k,则g指向下一个结点:否则,count=count+1。
④p指向下一个结点,转②。
⑤若count等于k,则查找成功,输出该结点的data域的值,返回1:否则,说明k值
超过了线性表的长度,查找失败,返回0。
⑥算法结束。
3)算法实现如下:
 typedef int ElemType;
 // 链表数据的类型定义
 typedef struct LNode {
   // 链表结点的结构定义
   ElemType data;      // 结点数据
   struct LNode *link; // 结点链接指针
 } LNode, *LinkList;

int SearchK(LinkList list, int k) {
  LNode *p = list->link, *q = list->link; // 指针p、q指示第一个结点
  int count = 0;
  while (p != NULL) { // 遍历链表直到最后一个结点
    if (count < k)
      count++; // 计数,若count<k只移动p
    else
      q = q->link;
    p = p->link; // 之后让P、q同步移动
  }              // while
  if (count < k)
    return 0; // 查找失败返回0
  else {
    // 否则打印并返回1
    printf("%d", q->data);
    return 1;
  }
}
评分说明:若所给出的算法采用一遍扫描方式就能得到正确结果,可给满分15分:若采用
遍或多遍扫描才能得到正确结果,最高分为10分,若采用递归算法得到正确结果,最高给10分:
若实现算法的空间复杂度过高(使用了大小与k有关的辅助数组,但结果正确,最高给10分。

18.【2012统考真题】假定采用带头结点的单链表保存单词,当两个单词有相同的后缀时
可共享相同的后缀存储空间,例如,loading和being的存储映像如下图所示
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

假设str1和str2分别指向两个单词所在单链表的头结点,链表结点结构为![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=2.3.18(2&pos_id=img-hujNwYde-1714451147549).png),,请设计一个时间上尽可能高效的算法,找出由str1和str2所指向两个链表共同后缀
的起始位置(如图中字符i所在结点的位置p)。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度。

顺序遍历两个链表到尾结点时,并不能保证两个链表同时到达尾结点。这是因为两个链表的
长度不同。假设一个链表比另一个链表长k个结点,我们先在长链表上遍历k个结点,之后同步
遍历两个链表,这样就能够保证它们同时到达最后一个结点。因为两个链表从第一个公共结点到
链表的尾结点都是重合的,所以它们肯定同时到达第一个公共结点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1)算法的基本设计思想如下:
①分别求出str1和str2所指的两个链表的长度m和n。
②将两个链表以表尾对齐:令指针p、q分别指向str1和str2的头结点,若m≥n,则
指针p先走,使p指向链表中的第m-n+1个结点:若m<n,则使g指向链表中的第
n-m+1个结点,即使指针p和g所指的结点到表尾的长度相等。
③反复将指针p和q同步向后移动,并判断它们是否指向同一结点。当p、q指向同一结
点,则该点即为所求的共同后缀的起始位置。
2)本题代码如下:
typedef struct Node {
  char data;
  struct Node *next;
} SNode;

// 求链表长度的函数
int listLen(SNode *head) {
  int len = 0;
  while (head->next != NULL) {
    len++;
    head = head->next;
  }
  return len;
}

// 找出共同后缀的起始地址
SNode *findList(SNode *str1, SNode *str2) {
  int m, n;
  SNode *p, *q;
  m = listLen(str1);           // 求str1的长度,O(m)
  n = listLen(str2);           // 求str2的长度,O(n)
  for (p = str1; m > n; m--) { // 若m>n,使p指向链表中的第m-n+1个结点
    p = p->next;
  }
  for (q = str2; m < n; n--) { // 若m<n,使q指向链表中的第n-m+1个结点
    q = q->next;
  }
  while (p->next != NULL && p->next != q->next) { // 查找共同后缀起始点
    p = p->next; // 两个指针同步向后移动
    q = q->next;
  }
  return p->next; // 返回共同后缀的起始地址
}
3)时间复杂度为O(len1+len2)或O(max(len1,len2)),其中lenl、len2分别为两个链表的长度。

19.【20l5统考真题】用单链表保存m个整数,结点的结构为[data][link],且|data|≤
n(n为正整数)。现要求设计一个时间复杂度尽可能高效的算法,对于链表中data的
绝对值相等的结点,仅保留第一次出现的结点而删除其余绝对值相等的结点。例如,若
给定的单链表head如下:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

则删除结,点后的head为
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

要求:
1)给出算法的基本设计思想
2)使用C或C++语言,给出单链表结点的数据类型定义。
3)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
4)说明你所设计算法的时间复杂度和空间复杂度。

1)算法的基本设计思想:
·算法的核心思想是用空间换时间。使用辅助数组记录链表中已出现的数值,从而只需对链
表进行一趟扫描。
·因为1data≤n,故辅助数组q的大小为n+l,各元素的初值均为0。依次扫描链表中
的各结点,同时检查q[|data|]的值,若为0则保留该结点,并令q[|data|]=1;否
则将该结点从链表中删除。
2)使用C语言描述的单链表结点的数据类型定义:
typedef struct node {
    int data;
    struct node *link:
}NODE;
Typedef NODE *PNODE;
3)算法实现如下:
void func(PNODE h, int n) {
  PNODE p = h, r;
  int *q, m;
  q = (int *)malloc(sizeof(int) * (n + 1)); // 申请n+1个位置的辅助空间
  for (int i = 0; i < n + 1; i++) {         // 数组元素初值置0
    *(q + i) = 0;
  }
  while (p->link != NULL) {
    m = p->link->data > 0 ? p->link->data : -p->link->data;
    if (*(q + m) == 0) { // 判断该结点的data是否已出现过
      *(q + m) = 1;      // 首次出现
      p = p->link;       // 保留
    } else {             // 重复出现
      r = p->link;       // 删除
      p->link = r->link;
      free(r);
    }
  }
  free(q);
}
4)参考答案所给算法的时间复杂度为O(m),空间复杂度为O(n)。

20.【2019统考真题】设线性表L=(a,42,4,…,an-2,an-1,an)采用带头结点的单链表保存,链
表中的结点定义如下:

typedef struct node
{
    int data;
    struct node *next;
}NODE:

请设计一个空间复杂度为O1)且时间上尽可能高效的算法,重新排列L中的各结点,得
到线性表L’=(a1,an,a2,an-1,a3,an-2,…)。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
3)说明你所设计的算法的时间复杂度。

1)算法的基本设计思想
先观察L=(a1,a2,a3,...,an-2,an-1,an)和L'=(a1,an,a2,an-1,a3,an-2,...),发现L'是由L摘取第
一个元素,再摘取倒数第一个元素······依次合并而成的。为了方便链表后半段取元素,需要先将
L后半段原地逆置[题目要求空间复杂度为O(1),不能借助栈],否则每取最后一个结点都需要遍
历一次链表。①先找出链表L的中间结点,为此设置两个指针p和q,指针p每次走一步,指针
q每次走两步,当指针q到达链尾时,指针P正好在链表的中间结点;②然后将L的后半段结点
原地逆置。③从单链表前后两段中依次各取一个结点,按要求重排。
2)算法实现
void changeList(NODE *h) {
  NODE *p, *q, *r, *s;
  p = q = h;
  while (q->next != NULL) { // 寻找中间结点
    p = p->next;            // p走一步
    q = q->next;
    if (q->next != NULL)
      q = q->next; // q走两步
    p->next = NULL; // p所指结点为中间结点,q为后半段链表的首结点
  }
  while (q != NULL) { // 将链表后半段逆置
    r = q->next;
    q->next = p->next;
    p->next = q;
    q = r;
  }
  s = h->next; // s指向前半段的第一个数据结点,即插入点
  q = p->next; // q指向后半段的第一个数据结点
  p->next = NULL;
  while (q != NULL) {  // 将链表后半段的结点插入到指定位置
    r = q->next;       // x指向后半段的下一个结点
    q->next = s->next; // 将q所指结点插入到s所指结点之后
    s->next = q;       // s指向前半段的下一个插入点
    s = q->next;
    q = r;
  }
}
3)第一步找中间结点的时间复杂度为O(n),第二步逆置的时间复杂度为O(n),第三步合并
链表的时间复杂度为O(n),所以该算法的时间复杂度为O(n)。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值