【解法4】链表操作,前后指针

强化讲义 · 原始转写 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照

## 【解法4】链表操作,前后指针

(1)初始时设置两个指针left和right都指向头结点,right指针先后移k次,若此过程中left指向了NULL则查找失败,返回0。然后将left和right同时后移,直到right指向NULL为止,因为left和right直接相差k次移动,所以此时left一定指向倒数第k个结点,输出left,返回1。(2)算法V如下:

<!-- verified-by-hand -->
> [!success] 已结合原文第 138 页及 Markdown 上下文人工校对
> 形参间逗号丢失,函数末尾花括号和孤立字符丢失。
int ans(node *list, int k)
{
    node *left = list, *right = list;
    for (int i = 0; i &lt; k; i++)
    {
        right = right-&gt;link;
        if (right == NULL) return 0;
    }
    while (right != NULL)
    {
        left = left-&gt;link;
        right = right-&gt;link;
    }
    printf(&quot;%d\n&quot;, left-&gt;data);
    return 1;
}
(3) 时间复杂度:O(n)空间复杂度:O(1) ## 2012年42题 假定采用带头结点的单链表保存单词,当两个单词有相同的后缀时,则可共享相同的后缀存储空间,例如,“loading”和“being”的存储映像如下图所示。
原资料配图
原资料配图 · 转写内容请回源核验

和str2分别指向两个单词所在单链表的头结点,链表结点结构为:

设str1

<!-- page 139 -->

<table><tr><td>data 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>0(1)</td><td rowspan=1 colspan=1>15</td></tr></table>

请注意本题是返回地址,而对于相同后缀来说,只要发现两个链指向了同一个结点,后面的部分也会相同。即地址相同等价于后缀完全相同。 ”

## 【解法1】暴力解:枚举str1和str2起始位置

(1)设str1和str2的长度较大的值是n,两层for循环枚举str1和str2相同后缀的起始位置。

(2) 算法如下:

Node* ans(Node *str1, Node *str2)
{
    Node* p1=str1-&gt;next; // str1的后缀起始位置
    Node* p2; //之后循环中会赋初值
    while (p1!=NULL)
    {
        p2=str2-&gt;next;
        while (p2!=NULL)
        if (p1==p2)
        return p1; //后缀完全匹配,返回p1
        p2=p2-&gt;next;
        p1=p1-&gt;next;
    }
    return NULL;
}
(3)时间复杂度:O(n²) 空间复杂度:O(1) 【解法2】暴力解:链表转数组,数组保存结点地址 <!-- page 140 --> (1) 设 str1 和str2的长度较大的值是n,设置两个新数组 S1 和 S2 分别保存 str1 和 str2 链中的每个结点的地址。(2)算法如下: 研
Node* ans(Node *str1, Node *str2)
{
    Node*S1[maxn],S2[maxn]; //保存str1和str2各结点地址,maxn是最大可能长度
    Node* p1=str1-&gt;next; //_str1的后缀起始位置
    Node* p2=str2-&gt;next;
    int Len1=0, Len2=0;
    while (p1!=NULL)
    S1[Len1]=p1;
    Len1++;
    p1=p1-&gt;next;
    while (p2!=NULL) // 草机考研
    {
        S2[Len2]=p2;
        Len2++;
        p2=p2-&gt;next;
        if (S1[Len1-1]!=S2[Len2-1])
        return NULL; //无相同后缀,可不写
        for (int i=1; i&lt;=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)算法如下: X
Node* ans(Node *str1, Node *str2)
{
    Node* p1=str1-&gt;next; //指针p1用来遍历str1
    Node* p2=str2-&gt;next; //指针p2用来遍历str2
    int Len1=0, Len2=0;
    while (p1!=NULL)
    {
        Len1++;
        p1=p1-&gt;next;
        while (p2!=NULL)
        {
            Len2++;
            p2=p2-&gt;next;
        }
        p1=str1-&gt;next;
        p2=str2-&gt;next;
        if (Len1&gt;Len2)
        p1=p1-&gt;next;
        else
        for (int i=0; i&lt;Len2-Len1; i++) // str2更长,p2先移动—段
        p2=p2-&gt;next;
        while (p1!=NULL &amp;&amp; p2!=NULL)
        {
            if (p1==p2) return p1;
            p1=p1-&gt;next;
            p2=p2-&gt;next;
        }
        return NULL; //无相同后缀,可不写
    }
}
<!-- page 141 --> (3) 时间复杂度:O(n) 空间复杂度:O(1) ## 2015年41题 (15 分)用单链表保存 m 个整数,结点的结构为 [data,link],且|data|≤n(n 为正整数)。现要求设计一个时间复杂度尽可能高效的算法,对于链表中data的绝对值相等的结点,仅保留第一次出现的结点而删除其余绝对值相等的结点。例如,若给定的单链表HEAD 如下:
原资料配图
原资料配图 · 转写内容请回源核验

(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(m2)</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】暴力解:枚举,每一个结点都和其他结点比较

<!-- page 142 -->

(1)两层循环枚举所有结点的两两比较,外层遍历链表中的结点p,内层q遍历p之后的结点,将和p绝对值相等的结点删除(只保留第一个出现的p)。

## (2) 结点数据结构定义:

typedef struct Node{
    int data; // 该结点权值
    struct Node *link; // 下—个结点
}Node;
## (3) 算法如下:
void ans(Node* HEAD)
{
    // 外层遍历结点p
    Node *pre, *r; // pre是q前一个结点
    //外层循环固定p,pre从p的下一个结点开始,遍历对比p的数据
    while (p!=NULL)
    {
        pre=p; //每次都需要更新pre,从p开始
        while (pre-&gt;link!=NULL)
        {
            q=pre-&gt;link; // q表示待比较结点
            if (abs(p-&gt;data)==abs(q-&gt;data))
            {
                pre-&gt;link=q-&gt;link;
                free(q);
            }
            else
            pre=pre-&gt;link;
        }
        p=p-&gt;link;
    }
}
## (3) 时间复杂度:O(m²) 空间复杂度:O(1) ## 【解法2】优化解:散列表 (1)data的绝对值≤n,那可以用数组count[0\~n]来保存对于每一个绝对值是否出现过,比如count[1]>0表示该绝对值已经出现过了,之后再出现就可以删除当前访问的结点。遍历一次链表,更新count数组以及删除后面重复出现的结点。 ## (2) 结点数据结构定义:
typedef struct Node{
    int data; // 该结点权值
    struct Node *link; // 下—个结点
}Node;
<!-- page 143 --> ## (3) 算法如下:
void ans(Node* HEAD, int m, int n)
{
    Node* p=HEAD-&gt;link; // 当前访问结点p
    Node* pre=HEAD;
    int count[n+1]={0};
    while (p!=NULL)
    {
        if (count[abs(p-&gt;data)]==0)
        {
            count[abs(p-&gt;data)]=1; // p的权值第一次出现
            pre=p;
            p=p-&gt;next;
        }
        else
        { //p的权值前面已经出现过,删除p结点
            pre-&gt;link=p-&gt;link;
            free(p);
            p=pre-&gt;link;
        }
    }
}
(4)时间复杂度:O(m)空间复杂度:O(n) ## 2019年41题 (13 分)设线性表采用带头结点的单链表保存,链表中的结点定义如下:
typedef struct node {
    int data;
    struct node* next;
} NODE;
请设计一个空间复杂度为O(1)且时间上尽可能高效的算法,重新排列L中的各结点,得到线性表L =(a1, 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(n2)</td><td rowspan=1 colspan=1>0(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></td><td rowspan=1 colspan=1>O(1)</td><td rowspan=1 colspan=1>15</td></tr></table> <!-- page 144 --> ## 【解法1】暴力解:枚举,直接插入 (1) 步骤如下: 1. 遍历链表,统计链表的结点数 n。 2. 将链表分成两半,前一半是 (n+1)/2 个结点,后一半 n/2 个结点。 3. 将链表后半的所有结点依次插入。(2)算法如下:
void ans(Node* L)
{
    // 统计链表结点数
    Node *p = L-&gt;next;
    int n = 0;
    while (p != NULL)
    {
        n++;
    }
    //链表分成两半,p指向后一半第一个结点
    p = L;
    for (int i=0; i&lt;(n+1)/2; i++)
    p = p-&gt;next;
    Node *temp = p;
    p = p-&gt;next;
    temp-&gt;next = NULL;
    //将后一半结点依次插入到前一半
    for (int i=0; i&lt;n/2; i++)
    {
        temp = p-&gt;next;
        Node *pre = L;
        for (int j=0; j&lt;n/2-i; j++)
        pre = pre-&gt;next;
        p-&gt;next = pre-&gt;next;
        pre-&gt;next = p;
        p = temp; // 更新下一个待处理元素 p
    }
}
(3) 时间复杂度:O(n²) ## 【解法2】优化解:逆置+多指针法 (1) 步骤如下: 1. 前后指针找到中间结点。 2. 从中间结点处将链表分成两半,前一半是 (n+1)/2 个结点,后一半 n/2 个结点。 3. 使用头插法将链表后半的所有结点逆置。 4. 将前一半链表和逆置后的后一半链表交叉排列合并成一个链表。(2) 算法如下: <!-- page 145 -->
//头插法
void Head_Insert(Node* L, Node* p)
{
    p-&gt;next=L-&gt;next;
    L-&gt;next=p;
}
void ans(Node* L)
{
    // 找中间结点 p
    Node *p, *q;
    p = q = L-&gt;next;
    while (p != NULL &amp;&amp; q != NULL)
    {
        q = q-&gt;next;
        if (q == NULL I1 q-&gt;next == NULL)
        break;
        q = q-&gt;next;
        p = p-&gt;next;
    }
    // 链表从 p 后分成两半
    Node *temp = p;
    p = p-&gt;next;
    temp-&gt;next = NULL;
    //为链表后一半设置新的头结点
    Node *L2 = (Node*)malloc(sizeof(Node))
    L2-&gt;next = NULL;
    //将后一半结点逆置
    while (p != NULL)
    {
        temp = p-&gt;next;
        Head_Insert(L2, p);
        p = temp;
        Node *p2 = L2-&gt;next;
        // 将前一半链表和逆置后的后一半链表交叉合并
        while (p2 != NULL)
        暂存 p1 和 p2 的下一个结点
        Node* temp1 = p1-&gt;next;
        Node* temp2 = p2-&gt;next;
        // p2 插入到 p1 后面
        p2-&gt;next = p1-&gt;next;
        p1-&gt;next = p2;
        // 更新 p1 和 p2
        p1 = temp1;
        p2 = temp2;
    }
}
<!-- page 146 --> (3) 时间复杂度:O(n) <!-- page 147 --> ## 4树 由 2014 年真题引入 【2014年真题,15 分】二叉树的带权路径长度(WPL)是二叉树中所有叶结点的带权路径长度之和。给定一棵二叉树T,采用二叉链表存储,结点结构如下: <table><tr><td>left</td><td>weight</td><td>right</td></tr></table> 其中叶结点的 weight 域保存该结点的非负权值。设 root 为指向T的根结点的指针,请设计求T的 WPL 的算法,要求: 成村 1)给出算法的基本设计思想。 2)使用C 或C++语言,给出二叉树结点的数据类型定义。 3)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。