Skip to content

2012年 408 数据结构 第 42 题

数据结构2012年综合题12分

题目

假定采用带头结点的单链表保存单词,当两个单词有相同的后缀时,则可共享相同的后缀存储空间。例如,"loading" 和 "being" 的存储映像如下所示:

singly-linked-list
list str1: l, o, a, d
list str2: b, e
shared: i, n, g
pointer p: shared:0

设 str1 和 str2 分别指向两个单词所在单链表的头结点,链表结点结构为 data | next,请设计一个时间上尽可能高效的算法,找出由 str1 和 str2 所指向两个链表共同后缀的起始位置(如上图中字符 i 所在结点的位置 p)。要求:

(1) 给出算法的基本设计思想。

(2) 根据设计思想,采用 C 或 C++ 或 Java 语言描述算法,关键之处给出注释。

(3) 说明你所设计算法的时间复杂度。

解析

(1) 设计思想

答案:先量出两条链表的长度 m、n(不含头结点);让较长那条的指针先走 |m − n| 步;然后两个指针同步前进,第一个让 p == q 成立的位置就是共同后缀的起始点。

为什么这样能找到共同后缀的起点?

题面说"共同后缀共享存储"——即共同后缀的所有结点是同一段内存地址,不是值相同的两段独立链表。所以判等用 p == q(指针相同)而不是 p->data == q->data

由于两条链表共享尾部,从尾部往前看,后缀长度一定相同。如果两条链表的非共享前缀部分长度分别是 (满足 为共同后缀长),那么:

  • 让较长一方先走 步,两个指针就距离尾部一样远
  • 同步前进,从前缀进入共同后缀的那一刻,两指针正好都跨入第一个共享结点——p == q 立即成立。

关键洞察:本题的关键不是"找最长公共子串"——共享是物理共享,而非数值相同。所以不需要任何字符比较,只比指针。

(2) 代码实现

c
typedef struct ListNode {
    char data;
    struct ListNode *next;
} ListNode;

ListNode* findCommonSuffix(ListNode *str1, ListNode *str2) {
    int m = 0, n = 0;

    // ① 量两条链的长度(不含头结点)
    for (ListNode *p = str1->next; p != NULL; p = p->next) m++;
    for (ListNode *p = str2->next; p != NULL; p = p->next) n++;

    // ② 让较长的那条先走 |m - n| 步,使两指针距尾部相同
    ListNode *p = str1->next;
    ListNode *q = str2->next;
    while (m > n) { p = p->next; m--; }
    while (n > m) { q = q->next; n--; }

    // ③ 同步前进,第一个 p == q 的位置就是共同后缀起点
    while (p != NULL && q != NULL && p != q) {
        p = p->next;
        q = q->next;
    }

    return p;     // 若无共同后缀,最终 p、q 都走到 NULL,返回 NULL
}

关键点说明

  • 判等用指针 p != q,不是 p->data != q->data:题目要求"找共享存储的起始结点",必须按地址判定。值相同但地址不同的两个独立结点不算共同后缀。
  • 量长度时跳过头结点:题面用"带头结点"的链表,遍历从 str1->next 开始。
  • 不需要修改链表:本算法只读不写,原链表结构不变。
  • 若两链表完全无共同后缀:最终 p 与 q 同步走到 NULL,循环退出时 p == q == NULL,返回 NULL(没有共同后缀)。

(3) 时间复杂度

  • 时间复杂度 :第一遍量长度 O(m+n);第二遍同步前进最坏走完较长链表 O(max(m,n))。
  • 空间复杂度 :只用 m、n、p、q 几个标量。

编者注(生僻术语):本题的"先对齐再齐步走"思想是单链表问题的常见模式——同样的技巧也用在「找两条链表的相交点」「找倒数第 k 个结点」等场景。考场中能写出这套模式就稳拿满分。

最后更新:

🎬 可视化演示
加载中...

提示:可在可视化区直接操作播放、步进、修改参数