【每日一题】2012考研数据结构 - 求字符串链表公共后缀
本篇文章将为大家讲解一道关于链表的经典题目——求两个链表的共同后缀节点。该问题在实际开发中同样具有很大的应用价值,是每一位数据结构学习者不可错过的重要题目。
问题描述
假设我们有一个带头结点的单链表来保存单词,每个节点包含一个字符和指向下一个节点的指针。若两个单词的后缀相同,它们可以共享相同的后缀存储空间。例如:
- 单词
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)),这涵盖了所有操作的最坏情况时间复杂度。这种算法之所以更高效,是因为它只需要遍历每个链表最多两次。
结语
双指针法通过巧妙地同步遍历来提高效率,非常适合此类链表问题。在链表题目中,理解如何通过双指针控制遍历过程,可以显著提升算法的效率。希望这篇文章能够帮助大家更好地理解链表的双指针技巧。
相关文章:

【每日一题】2012考研数据结构 - 求字符串链表公共后缀
本篇文章将为大家讲解一道关于链表的经典题目——求两个链表的共同后缀节点。该问题在实际开发中同样具有很大的应用价值,是每一位数据结构学习者不可错过的重要题目。 问题描述 假设我们有一个带头结点的单链表来保存单词,每个节点包含一个字符和指向…...

数据结构和算法-贪心算法01- 认识贪心
贪心算法 什么是贪心算法 一个贪心算法总是做出当前最好的选择,也就是说,它期望通过局部最优选择从而得到全局最优的解决方案。 ----《算法导论》 贪心算法(Greedy Method): 所谓贪心算法就是重复地(或贪婪地)根据一个法则挑选解的一部分。当挑选完毕…...

Bash Shell - 获取日期、时间
1. 使用date获取日期 以下代码将date的执行结果存储在today变量中。date 是获取日期和时间的命令。 选择使用 quotes()或$ #!/bin/bashtodaydate echo $todaytoday$(date) echo $today 2. 使用 Format 输出所需日期和时间 date FORMAT 2.1 "MM-DD-YY" 形式输出…...

runnable和callable区别和底层原理
确实,Runnable 可以直接通过 Thread 类来运行,而 Callable 不能直接用于创建和运行线程。Callable 和 Runnable 都是 Java 中用于定义异步任务的接口,但它们的用法和目的有所不同。 ### Runnable 和 Thread Runnable 是接口,它不返…...

Springboot 整合 Java DL4J 打造自然语言处理之语音识别系统
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,…...

虚幻引擎5(UE5)学习教程
虚幻引擎5(UE5)学习教程 引言 虚幻引擎5(Unreal Engine 5,简称UE5)是Epic Games开发的一款强大的游戏引擎,广泛应用于游戏开发、影视制作、建筑可视化等多个领域。UE5引入了许多先进的技术,如…...

从0开始深度学习(26)——汇聚层/池化层
池化层通过减少特征图的尺寸来降低计算量和参数数量,同时增加模型的平移不变性和鲁棒性。汇聚层的主要优点之一是减轻卷积层对位置的过度敏感。 1 最大汇聚层、平均汇聚层 汇聚层和卷积核一样,是在输入图片上进行滑动计算,但是不同于卷积层的…...

兼职发薪系统:高效、便捷的劳务发薪解决方案
在快速发展的数字化时代,企业对于高效、便捷的薪酬发放和管理解决方案的需求日益增长。特别是对于兼职人员众多的企业,如何实现快速、准确的发薪,同时确保员工信息的安全与保密,成为了一个亟待解决的问题。今天,我们将…...

MySQL数据库单表查询习题
目录 数据内容介绍习题题目答案 数据内容介绍 数据库中有两个表 内容如下: 习题 题目 查询出部门编号为D2019060011的所有员工所有财务总监的姓名、编号和部门编号。找出奖金高于工资的员工。找出奖金高于工资40%的员工。找出部门编号为D2019090011中所有…...

多模态PaliGemma——Google推出的基于SigLIP和Gemma的视觉语言模型
前言 本文怎么来的呢?其实很简单,源于上一篇文章《π0——用于通用机器人控制的流匹配VLA模型:一套框架控制7种机械臂(改造了PaliGemma和ACT的3B模型)》中的π0用到了PaliGemma 故本文便来解读下这个PaliGemma 第一部分 PaliGemma 1.1 Pal…...

电路原理:电阻桥。
电路的基础是电阻电路。电阻电路有两种基本接线方法(串连和并连,二者有不同的解算与用法:串连分压、并连分流)。电阻电路就是使用基本接线方法的组合方案,其解算方法主要内容是判断好整体布局以及各个局部的串并连关系…...

实践出真知:MVEL表达式中for循环的坑
目录标题 背景MVEL脚本(有问题的)MVEL脚本(正确的)结论分析 背景 需要从一个URL的拼接参数中解析出id的值并输出 比如: 存在URLhttps://xxxxxxxxxx?id999999&type123&name345 然后需要输出id999999 MVEL脚本(有问题的) 入参:parseThisUrlhttp…...

Flutter运行App时出现“Running Gradle task ‘assembleDebug“问题解决
在参考了众多解决办法中最有用并且最快的方法 Gradle Distributions 在这个地方下载对应你这个文件中的gradle版本 然后将 最后一行本来不是这样的,我们把下载好的zip包保存到本地,然后用这个代替网址,最后成功运行...

基于SSM(Spring + Spring MVC + MyBatis)框架的咖啡馆管理系统
基于SSM(Spring Spring MVC MyBatis)框架的咖啡馆管理系统是一个综合性的Web应用程序,用于管理和优化咖啡馆的运营。下面我将提供一个详细的案例程序概述,包括主要的功能模块和技术栈介绍。 项目概述 功能需求 用户管理&…...

【SpringBoot】18 上传文件到数据库(Thymeleaf + MySQL)
Git仓库 https://gitee.com/Lin_DH/system 介绍 使用 Thymeleaf 写的页面,将(txt、jpg、png)格式文件上传到 MySQL 数据库中。 依赖 pom.xml <!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j --><depende…...

计算机体系结构之系统吞吐量(三)
前面章节《计算机体系结构之多级缓存、缓存miss及缓存hit(二)》讲了关于系统多级缓存的相关内容,其中提及了系统吞吐量一词。在此章将对其进行讲解。 系统吞吐量是计算机体系结构的一个重要指标,其衡量的是系统在单位时间内处理工…...

高级 HarmonyOS主题课—— 帮助快速构建各种文本识别应用的课后习题
天地不仁,以万物为刍狗; 圣人不仁,以百姓为刍狗。 天地之间,其犹橐龠乎? 虚而不屈,动而俞出。 多闻数穷,不若守于中。 本文内容主要来自 <HarmonyOS主题课>帮助快速构建各种文本识别应用 …...

windows C#-异常和异常处理概述
C# 语言的异常处理功能有助于处理在程序运行期间发生的任何意外或异常情况。 异常处理功能使用 try、catch 和 finally 关键字来尝试执行可能失败的操作、在你确定合理的情况下处理故障,以及在事后清除资源。 公共语言运行时 (CLR)、.NET/第三方库或应用程序代码都可…...

每日一题——第一百二十四题
题目:进制转换 #pragma once#include<stdio.h> #include<ctype.h> #include<string.h>/// <summary> /// //将字符串表示的任意进制数转为十进制 /// </summary> /// <param name"str">字符串</param> /// &l…...

在 CentOS 7 上设置 OpenResty 开机启动
在 CentOS 7 上设置 OpenResty 开机启动,可以按照以下步骤进行操作: 创建 Systemd 服务文件: 首先,您需要为 OpenResty 创建一个 Systemd 服务文件。使用文本编辑器(如 vi 或 nano)创建一个新的服务文件。 …...

势不可挡 创新引领 | 生信科技SOLIDWORKS 2025新品发布会·苏州站精彩回顾
2024年11月01日,由生信科技举办的SOLIDWORKS 2025新产品发布会在江苏苏州圆满落幕。现场邀请到制造业的专家学者们一同感受SOLIDWORKS 2025最新功能,探索制造业数字化转型之路。 在苏州站活动开场,达索系统专业客户事业部华东区渠道经理马腾飞…...

数仓之全量表、增量表、快照表、切片表、拉链表的基本概念
文章摘自:数仓之全量表、增量表、快照表、切片表、拉链表-腾讯云开发者社区-腾讯云 一、全量表 记录每天所有最新状态的数据,有无变化都要上报,每次往全量表里面写数据都会覆盖之前的数据 缺点:不能记录数据的历史变化ÿ…...

【富集分析GSEA】如何理解富集分析以及应用
如何理解富集分析 富集分析不同的方式 富集分析 不同的方式 直接使用疾病特征进行富集分析(不翻转上调和下调的基因) 目的:如果你的目标是了解疾病状态的生物学特征和功能路径,那么应该直接使用疾病特征(包含疾病状态…...

一七五、HTML 不同类型的事件及其说明和示例
HTML 事件处理程序是通过 JavaScript 来捕获和响应不同的用户操作、系统事件或浏览器事件。下面是不同类型的事件及其说明和示例。 Window 事件 1. onresize 当浏览器窗口的大小发生变化时触发。 <!DOCTYPE html> <html lang"en"> <head><m…...

数量少的连锁店要不要用智能巡检?
无论是在新闻报道中,还是企业定制目标客户时,人们都更喜欢聚焦原本就已经站在各行业金字塔尖的那 1%,剩下的 99% 却常常被忽略。 比如此刻我正在搜索中小型连锁企业智能巡检相关的资讯,但网页展示的结果基本围绕着「中大型、1000门…...

【CSS】外边距塌陷
问题背景 在移动应用页面开发中,父元素和子元素外边距合并,导致布局效果和预期不一致。 <template><view class"container"><view class"card"><p>TEST</p></view></view> </templa…...

WPF MVVM入门系列教程(二、依赖属性)
说明:本文是介绍WPF中的依赖属性功能,如果对依赖属性已经有了解了,可以浏览后面的文章。 为什么要介绍依赖属性 在WPF的数据绑定中,密不可分的就是依赖属性。而MVVM又是跟数据绑定紧密相连的,所以在学习MVVM之前&…...

Springboot集成syslog+logstash收集日志到ES
Springboot集成sysloglogstash收集日志到ES 1、背景 Logstash 是一个实时数据收集引擎,可收集各类型数据并对其进行分析,过滤和归纳。按照自己条件分析过滤出符合的数据,导入到可视化界面。它可以实现多样化的数据源数据全量或增量传输&…...

Devops业务价值流:软件研发最佳实践
在当今快速迭代的软件开发环境中,DevOps业务价值流已成为推动软件研发高效与质量并重的关键实践。软件研发阶段作为产品生命周期的核心环节,其每一步都承载着将创意转化为现实的重要使命。在历经需求澄清的精准定位、架构设计的宏观规划以及项目初始化的…...

Matplotlib 绘图艺术:从新手到高手的全面指南
引言 在数据科学和机器学习领域,数据可视化是一项至关重要的技能。一个优秀的可视化图表可以直观地展示数据的内在规律,帮助我们更好地理解数据,并做出更明智的决策。而在众多的绘图库中,Matplotlib 是 Python 中最强大、最灵活的…...