【每日一题】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)创建一个新的服务文件。 …...

centos 7 部署awstats 网站访问检测
一、基础环境准备(两种安装方式都要做) bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats࿰…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...

让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...

IT供电系统绝缘监测及故障定位解决方案
随着新能源的快速发展,光伏电站、储能系统及充电设备已广泛应用于现代能源网络。在光伏领域,IT供电系统凭借其持续供电性好、安全性高等优势成为光伏首选,但在长期运行中,例如老化、潮湿、隐裂、机械损伤等问题会影响光伏板绝缘层…...

自然语言处理——循环神经网络
自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元(GRU)长短期记忆神经网络(LSTM)…...

图表类系列各种样式PPT模版分享
图标图表系列PPT模版,柱状图PPT模版,线状图PPT模版,折线图PPT模版,饼状图PPT模版,雷达图PPT模版,树状图PPT模版 图表类系列各种样式PPT模版分享:图表系列PPT模板https://pan.quark.cn/s/20d40aa…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...

OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
LangChain知识库管理后端接口:数据库操作详解—— 构建本地知识库系统的基础《二》
这段 Python 代码是一个完整的 知识库数据库操作模块,用于对本地知识库系统中的知识库进行增删改查(CRUD)操作。它基于 SQLAlchemy ORM 框架 和一个自定义的装饰器 with_session 实现数据库会话管理。 📘 一、整体功能概述 该模块…...