【解法3】优化解:遍历链表两次
原始讲义 · 非增强校订版 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照
## 【解法3】优化解:遍历链表两次
(1)要找到倒数第k个结点,但链表不能回退,所以需要知道他是从前往后数第t个结点(不算头结点),t=n-k+1,n是总结点数,那我们只需要先遍历一次链表求出n,然后再遍历一次链表找到第t个结点。
(2) 算法如下:
(3) // 时间复杂度:O(n) 空间复杂度:O(1)
## 【解法4】链表操作,前后指针
(1)初始时设置两个指针left和right都指向头结点,right指针先后移k次,若此过程中left指向了NULL则查找失败,返回0。然后将left和right同时后移,直到right指向NULL为止,因为left和right直接相差k次移动,所以此时left一定指向倒数第k个结点,输出left,返回1。
(2) 算法如下:
int ans(node* list, int k)
{
node* left=list;
node* right=list;
for (int i=0; i<k; i++)
{
right=right->link;
if (right==NULL) return 0;
}
while(right!=NULL)
left=left->link;
right=right->link;
}
printf("%d\n", left->data);
return 1;
<!-- page 99 -->
(3) 时间复杂度:O(n) 空间复杂度:O(1)
## 2012年42题
假定采用带头结点的单链表保存单词,当两个单词有相同的后缀时,则可共享相同的后缀存储空间,例如,“loading”和“being”的存储映像如下图所示。
原资料配图 · 转写内容请回源核验
设str1和str2分别指向两个单词所在单链表的头结点,链表结点结构为:
<table><tr><td>data</td><td>next</td></tr></table>
请设计一个时间上尽可能高效的算法,找出由str1和str2所指向两个链表共同后缀的起始位置(如图中字符i所在结点的位置p)。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度。
<table><tr><td rowspan=1 colspan=1></td><td rowspan=1 colspan=1>做题思路</td><td rowspan=1 colspan=1>时间复杂度</td><td rowspan=1 colspan=1>空间复杂度</td><td rowspan=1 colspan=1>预计得分</td></tr><tr><td rowspan=1 colspan=1>暴力解</td><td rowspan=1 colspan=1>枚举</td><td rowspan=1 colspan=1>O(n2)</td><td rowspan=1 colspan=1>0(1)</td><td rowspan=1 colspan=1>11</td></tr><tr><td rowspan=1 colspan=1>暴力解</td><td rowspan=1 colspan=1>链表转数组</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>13</td></tr><tr><td rowspan=1 colspan=1>优化解</td><td rowspan=1 colspan=1>多指针法</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>O(1)</td><td rowspan=1 colspan=1>15</td></tr></table>
请注意本题是返回地址,而对于相同后缀来说,只要发现两个链指向了同一个结点,后面的部分也会相同。即地址相同等价于后缀完全相同。 20周
## 【解法1】暴力解:枚举str1和str2起始位置
(1)设str1和str2的长度较大的值是n,两层for循环枚举str1和str2相同后缀的起始位置。
(2) 算法如下:
Node* ans(Node *str1, Node *str2)
{
Node* p1=str1->next;
Node* p2;
while (p1!=NULL)
{
p2=str2->next;
while (p2!=NULL)
{
if (p1==p2)
return p1;
p2=p2->next;
}
p1=p1->next;
}
return NULL;
}
<!-- page 100 -->
(3) // 时间复杂度:O(n²) 空间复杂度:O(1)
【解法2】暴力解:链表转数组,数组保存结点地址
(1)设str1和str2的长度较大的值是n.设置两个新数组S1和S2分别保存str1和str2链中的每个结点的地址(这里用为了方便,从S1[1]和S2[1]开始存)。
(2) 算法如下:
Node* ans(Node *str1, Node *str2)
{
Node* S1[maxn], S2[maxn]; // 保存str1和str2各结点地址,maxn是最大可
能长度
Node* p1=str1->next; // str1的后缀起始位置
Node* p2=str2->next; // str2的后缀起始位置
int Len1=0, Len2=0;
while (p1!=NULL)
{
S1[Len1]=p1;
Len1++;
p1=p1->next;
}
while (p2!=NULL)
{ S2[Len2]=p2;
Len2++;
p2=p2->next;
}
if (S1[Len1-1]!=S2[Len2-1])
return NULL; //无相同后缀,可不写
for (int i=1; i<=min(Len1, Len2); i++) // 讨论相同后缀长度
if (S1[Len1-i]!=S2[Len2-i])
return S1[Len1-i+1]; //输出起始地址
return s1[0]; // 如果S1和S2完全相同则返回他们的第一个字母
地址 机考研
}
(3) 时间复杂度:O(n) 空间复杂度:O(n)
【解法3】优化解:多指针法,前后指针
(1)每个链表设置一个指针,先求出两个链的长度,然后长的链指针往后移动x次(x是长度差的绝对值),使得两链剩余未访问结点数相同,然后两链表的指针同步往后移动比较,直到指向的是同一个结点。
(2) 算法如下:
<!-- page 101 -->
Node* ans(Node *str1, Node *str2)
{
Node* p1=str1->next; // 指针p1用来遍历str1
Node* p2=str2->next; // 指针p2用来遍历str2
int Len1=0, Len2=0;
while (p1!=NULL)
{
Len1++;
p1=p1->next;
}
while (p2!=NULL)
{ 十
Len2++;
p2=p2->next;
}
p1=str1->next;
p2=str2->next;
if (Len1>Len2)
for (int i=0; i<Len1-Len2; i++) // str1更长,p1先移动一段
p1=p1->next;
else
for (int i=0; i<Len2-Len1; i++) // str2更长,p2先移动一段
p2=p2->next;
while (p1!=NULL && p2!=NULL)
{
if (p1==p2) return p1; //返回相同后缀起始地址
p1=p1->next;
p2=p2->next;
} return NULL; // 无相同后缀,可不写
}
(3)时间复杂度:O(n) 空间复杂度:O(1)
## 2015年41题
(15 分)用单链表保存m 个整数,结点的结构为[data,link],且|datal≤n(n为正整数)。现要求设计一个时间复杂度尽可能高效的算法,对于链表中data的绝对值相等的结点,仅保留第一次出现的结点而删除其余绝对值相等的结点。例如,若给定的单链表HEAD如下:
原资料配图 · 转写内容请回源核验
则删除结点后的 HEAD 为
<!-- page 102 -->
原资料配图 · 转写内容请回源核验
要求:
(1)给出算法的基本设计思想。
(2)使用C或C++语言,给出单链表结点的数据类型定义。
(3)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
(4)说明你所设计算法的时间复杂度和空间复杂度。
<table><tr><td rowspan=1 colspan=1></td><td rowspan=1 colspan=1>做题思路</td><td rowspan=1 colspan=1>时间复杂度</td><td rowspan=1 colspan=1>空间复杂度</td><td rowspan=1 colspan=1>预计得分</td></tr><tr><td rowspan=1 colspan=1>暴力解</td><td rowspan=1 colspan=1>枚举</td><td rowspan=1 colspan=1>O(m²)</td><td rowspan=1 colspan=1>0(1)</td><td rowspan=1 colspan=1>11</td></tr><tr><td rowspan=1 colspan=1>优化解</td><td rowspan=1 colspan=1>散列表</td><td rowspan=1 colspan=1>O(m)</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>15</td></tr></table>
## 【解法1】暴力解:枚举,每一个结点都和其他结点比较
(1)两层循环枚举所有结点的两两比较,外层遍历链表中的结点p,内层q遍历p之后的结点,将和p绝对值相等的结点删除(只保留第一个出现的p)。
(2) 结点数据结构定义:
typedef struct Node{
int data;
struct Node *link;
}Node;
(3) 算法如下:
<!-- verified-by-hand -->
> [!success] 已结合原文第 102 页及 Markdown 上下文人工校对
> q 未声明,if/else 缺花括号,p 更新与函数闭括号错位。
void ans(Node *HEAD)
{
Node *p = HEAD->link;
while (p != NULL)
{
Node *pre = p;
while (pre->link != NULL)
{
Node *q = pre->link;
if (abs(p->data) == abs(q->data))
{
pre->link = q->link;
free(q);
}
else pre = pre->link;
}
p = p->link;
}
}
<!-- page 103 -->
(3) // 时间复杂度: O(m²) 空间复杂度:O(1)
【解法2】优化解:散列表
(1)data的绝对值≤n,那可以用数组count[0\~n]来保存对于每一个绝对值是否出现过,比如count[1]>0表示该绝对值已经出现过了,之后再出现就可以删除当前访问的结点。遍历一次链表,更新count数组以及删除后面重复出现的结点。
(2) 结点数据结构定义:
typedef struct Node{
int data; kaoya //该结点权值
struct Node *link; // 下一个结点
}Node;
(3) 算法如下:
void ans(Node* HEAD, int m, int n)
{
Node* p=HEAD->link; // 当前访问结点p
Node* pre=HEAD; // p的前一个结点pre
int count[n+1]={0};
while (p!=NULL)
{
if (count[abs(p->data)]==0)
{
count[abs(p->data)]=1; // p的权值第一次出现
pre=p;
p=p->next;
}
else
{
pre->link=p->link;
free(p);
p=pre->link; WW
}
}
}
<!-- code-audit: false_positive -->
> [!info] 已人工审读:这是复杂度结论正文,被错误放入 C 代码围栏,不是程序代码。
<!-- page 104 -->
(13分)设线性表采用带头结点的单链表保存,链表中的结点定义如下:
typedef struct node {
int data;
struct node* next;
} NODE;
请设计一个空间复杂度为 O(1)且时间上尽可能高效的算法,重新排列L中的各结点,得到线性表L = (a₁, an, a2,an-1,a3, an-2…)。要求: 车研
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
(3)说明你所设计的算法的时间复杂度。
<table><tr><td rowspan=1 colspan=1></td><td rowspan=1 colspan=1>做题思路</td><td rowspan=1 colspan=1>时间复杂度</td><td rowspan=1 colspan=1>空间复杂度</td><td rowspan=1 colspan=1>预计得分</td></tr><tr><td rowspan=1 colspan=1>暴力解</td><td rowspan=1 colspan=1>枚举,直接插入</td><td rowspan=1 colspan=1>O(n²)</td><td rowspan=1 colspan=1>O(1)</td><td rowspan=1 colspan=1>12</td></tr><tr><td rowspan=1 colspan=1>优化解</td><td rowspan=1 colspan=1>逆置+多指针法</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>O(1)</td><td rowspan=1 colspan=1>15</td></tr></table>
## 【解法1】暴力解:枚举,直接插入
(1) 步骤如下:
1. 遍历链表,统计链表的结点数n。
2. 将链表分成两半,前一半是(n+1)/2 个结点,后一半n/2 个结点。
3. 将链表后半的所有结点依次插入。
(2) 算法如下:
void ans(Node* L)
{
// 统计链表结点数
Node *p = L->next;
int n = 0;
while (p != NULL)
{
n++;
p = p->next;
}
//链表分成两半,p指向后一半第一个结点
p = L;
for (int i=0; i<(n+1)/2; i++)
p = p->next;
Node *temp = p;
p = p->next;
temp->next = NULL;
//将后一半结点依次插入到前一半
for (int i=0; i<n/2; i++)
// 更新下一个待处理元素 p
{
temp = p->next;
Node *pre = L;
for (int j=0; j<n/2-i; j++)
pre = pre->next;
p->next = pre->next;
pre->next = p;
p = temp;
}
}
<!-- page 105 -->
## 【解法2】优化解:逆置+多指针法
(1) 步骤如下:
1. 前后指针找到中间结点。
2. 从中间结点处将链表分成两半,前一半是(n+1)/2 个结点,后一半 n/2 个结点。
3. 使用头插法将链表后半的所有结点逆置。
4. 将前一半链表和逆置后的后一半链表交叉排列合并成一个链表。
(2) 算法如下:
//头插法
void Head_Insert(Node* L, Node* p)
{
p->next=L->next;
L->next=p;
}
void ans(Node* L)
{
// 找中间结点 p
Node *p, *q;
p = q = L->next;
while (p != NULL && q != NULL)
{
q = q->next;
if (q == NULL || q->next == NULL)
break;
q = q->next;
p = p->next;
}
// 链表从 p 后分成两半
Node *temp = p;
p = p->next;
temp->next = NULL;
//为链表后一半设置新的头结点
Node *L2 = (Node*)malloc(sizeof(Node));
L2->next = NULL;
//将后一半结点逆置
while (p != NULL)
{
temp = p->next;
Head_Insert(L2, p);
p = temp;
}
Node *p1 = L->next; // 机考研 o m
Node *p2 = L2->next;
// 将前一半链表和逆置后的后一半链表交叉合并
while (p2 != NULL)
{
暂存 p1 和 p2 的下一个结点
Node* temp1 = p1->next;
Node* temp2 = p2->next;
// p2 插入到 p1 后面
p2->next = p1->next;
p1->next = p2;
// 更新 p1 和 p2
p1 = temp1;
p2 = temp2;
}
}
<!-- page 106 -->
(3) 时间复杂度:O(n)