做网站需要学什么专业兰州专业网站建设报价
本篇文章将为大家讲解一道关于链表的经典题目——求两个链表的共同后缀节点。该问题在实际开发中同样具有很大的应用价值,是每一位数据结构学习者不可错过的重要题目。
问题描述
假设我们有一个带头结点的单链表来保存单词,每个节点包含一个字符和指向下一个节点的指针。若两个单词的后缀相同,它们可以共享相同的后缀存储空间。例如:
- 单词
loading和being有相同的后缀ing,可以共享存储。 
题目要求
设 str1 和 str2 分别指向两个单词所在链表的头节点,请设计一个尽可能高效的算法,找出两个链表共同后缀的起始位置。
要求:
- 给出算法的设计思想。
 - 使用 C++ 代码实现算法,并在关键部分给出注释。
 - 分析算法的时间复杂度。
 
设计思路
1. 暴力匹配法
最直接的思路是将链表 str1 的每一个节点依次与链表 str2 的节点进行匹配,如果匹配到相同的节点,则继续向后比较,直到找到第一个完全相同的后缀位置。
实现步骤:
- 从链表 
str1开始的每个节点,分别与str2中的节点开始逐一匹配。 - 若匹配的节点数据相同,继续匹配下一个节点;若不相同则跳到下一个节点。
 - 当匹配到第一个后缀起始点时,返回该节点位置。
 
#include "bits/stdc++.h"using namespace std;struct Node{int value;Node* next;
};bool check(Node* first, Node* second) {Node* l = first;Node* r = second;while(l != nullptr && r != nullptr) {if(l -> value != r ->value) return false;l = l -> next;r = r -> next;}return l  == nullptr &&  r  == nullptr;
}Node* search(Node* first, Node* second) {Node* x = first;while(x != nullptr ) {Node*  y = second;while(y != nullptr) {if(x -> value == y -> value && check(x, y)) {return x;}y = y -> next;}x = x -> next;}return nullptr;
}int main() {Node* common = new Node{'i', new Node{'n', new Node{'g', nullptr}}};// 创建两个测试链表Node* str1 = new Node{ 'l', nullptr };str1->next = new Node{ 'o', nullptr };str1->next->next = new Node{ 'a', nullptr };str1->next->next->next = new Node{ 'd', nullptr };str1->next->next->next->next = common;Node* str2 = new Node{ 'b', nullptr };str2->next = new Node{ 'e', nullptr };str2->next->next = common; // 共享后缀部分// 查找共同后缀的起始位置Node* result = search(str1, str2);if (result != nullptr) {cout << "共同后缀的起始位置: " << (char)result->value << endl;} else {cout << "没有共同后缀" << endl;}return 0;
}
 
代码结构分析
-  
search函数:这是主函数,它通过双层循环在链表first和second中查找共同后缀的起始位置。- 外层循环:遍历链表 
first的每个节点x。 - 内层循环:对于每个节点 
x,遍历链表second的每个节点y。 
 - 外层循环:遍历链表 
 -  
check函数:search函数在if(x -> value == y -> value && check(x, y))中调用check函数,用于比较链表first从节点x开始的后缀和链表second从节点y开始的后缀。check函数的时间复杂度为 (O(n)),因为它从两个节点开始逐一比较剩下的所有节点。
 
时间复杂度计算
- 外层循环遍历链表 
first的每个节点,时间复杂度为 (O(m))(m是链表first的长度)。 - 内层循环遍历链表 
second的每个节点,时间复杂度为 (O(n))(n是链表second的长度)。 check函数的复杂度为 (O(k)),其中k是x和y之后的节点数量,最多接近O(max(m, n))。
因此,整体时间复杂度为: O ( m × n × k ) ≈ O ( n 3 ) O(m \times n \times k) \approx O(n^3) O(m×n×k)≈O(n3)
2. 双指针法
考虑一种更为高效的 双指针法。
思路
我们先遍历两个链表,得到它们的长度差 d。然后让较长的链表先走 d 步,这样两个链表剩下的节点数相等。之后同时遍历两个链表,直到找到第一个相同的节点,即为共同后缀的起始位置。
实现步骤
- 计算两个链表的长度 
len1和len2,求得它们的长度差d。 - 将较长的链表向前移动 
d步。 - 同时遍历两个链表,直到找到第一个相同的节点。
 
优点:通过两次遍历(计算长度 + 查找共同节点),时间复杂度降低到 O(m + n)。
C++ 实现代码
以下是使用 C++ 实现的双指针法代码:
#include "bits/stdc++.h"
using namespace std;struct Node{int value;Node* next;
}; // 计算链表长度
int getLength(Node* node) {int length = 0;while(node != nullptr) {length++;node = node -> next;}return length;
}// 查找共同后缀起始节点
Node* search(Node* n, Node* m) {int len1 = getLength(n);int len2 = getLength(m);// 长链表先走 |len1 - len2| 步while(len1 > len2) {len1--;n = n -> next;}while(len2 > len1) {len2--;m = m -> next;}// 双指针同时遍历寻找共同节点while(n != nullptr && m != nullptr) {if(n == m ) return n;n = n -> next;m = m -> next;}return nullptr;
}int main() {Node* common = new Node{'i', new Node{'n', new Node{'g', nullptr}}};Node* str1 = new Node{ 'l', nullptr };str1->next = new Node{ 'o', nullptr };str1->next->next = new Node{ 'a', nullptr };str1->next->next->next = new Node{ 'd', nullptr };str1->next->next->next->next = common;Node* str2 = new Node{ 'b', nullptr };str2->next = new Node{ 'e', nullptr };str2->next->next = common;Node* result = search(str1, str2);if (result != nullptr) {cout << "共同后缀的起始位置: " << (char)result->value << endl;} else {cout << "没有共同后缀" << endl;}return 0;
}
 
这段代码的核心思想是通过调整两个链表的起始位置,使得它们在同一个位置开始比较,以便找到第一个相同的节点作为共同后缀的起始点。以下是逐步的代码讲解,特别是对为什么较长的链表需要先移动几步的解释。
代码分解与讲解
#include "bits/stdc++.h"
using namespace std;struct Node{int value;Node* next;
}; 
 
- 这里定义了链表节点的结构体 
Node,包含一个整型数据value和一个指向下一个节点的指针next。 
int getLength(Node* node) {int length = 0;while(node != nullptr) {length++;node = node -> next;}return length;
}
 
getLength函数用于计算链表的长度。它遍历链表并计数,直到链表末尾。时间复杂度为 (O(n)),其中 (n) 是链表的长度。
Node* search(Node* n, Node* m) {int x = getLength(n);int y = getLength(m);
 
search函数是主函数,用来找到两个链表的共同后缀。首先调用getLength获取链表n和m的长度,分别记为x和y。
为什么较长的链表要先移动?
假设链表 n 比链表 m 长。如果直接从头开始同时遍历两个链表,由于长度不相等,较长链表的指针会先到达结尾,而我们需要两个链表在“对齐的起始位置”进行比较。
因此,为了让两个链表末尾部分对齐:
- 让较长的链表先移动 
|x - y|步。这样移动后,两个链表的剩余部分长度相等。 - 从这个新位置开始,两个链表的指针将同步向后遍历,确保同时到达链表的末尾。
 
    while(x > y) {x--;n = n -> next;} while(y > x) {y--;m = m -> next;}
 
- 在这里,较长的链表会先移动 
|x - y|步,以确保后续同步遍历时,两链表末尾部分的节点数相同。 
    while(n != nullptr && m != nullptr) {if(n == m) return n;n = n -> next;m = m -> next;}return nullptr;
}
 
- 现在,两个链表 
n和m同时从新的位置开始遍历。 - 如果发现 
n和m指向同一个节点(即它们有相同的地址),则该节点即为第一个公共节点,返回该节点。 - 如果遍历结束未找到公共节点,则返回 
nullptr,表示没有共同后缀。 
算法时间复杂度分析
-  
计算长度:
- 函数 
getLength分别计算链表n和m的长度,耗时为 (O(m)) 和 (O(n))。 
 - 函数 
 -  
对齐链表长度:
- 假设 
str1比str2长。为了让两个链表的尾端对齐,较长的链表str1向前移动|m - n|步。无论如何,对齐过程的时间复杂度是 (O(|m - n|)),这可以简化为 (O(m + n))。 
 - 假设 
 -  
寻找共同后缀:
- 接下来,两个链表同时从对齐位置向后遍历,寻找第一个相同的节点。这部分的时间复杂度为 O ( m i n ( m , n ) ) O(min(m, n)) O(min(m,n)),但可以包含在 O ( m i n ( m , n ) ) O(min(m, n)) O(min(m,n))的上界内。
 
 
因此,整个算法的时间复杂度为 O ( m i n ( m , n ) ) O(min(m, n)) O(min(m,n)),这涵盖了所有操作的最坏情况时间复杂度。这种算法之所以更高效,是因为它只需要遍历每个链表最多两次。
结语
双指针法通过巧妙地同步遍历来提高效率,非常适合此类链表问题。在链表题目中,理解如何通过双指针控制遍历过程,可以显著提升算法的效率。希望这篇文章能够帮助大家更好地理解链表的双指针技巧。
