腾讯一面算法题:最长重复子串 1044,讲个比较好理解的思路

news2024/9/20 12:15:31

文章目录

  • 1044. 最长重复子串
  • 前言
  • 思路
    • Version 1:暴力
    • Version 2:引入二分,优化 O ( n 2 ) O(n^2) O(n2)
    • Version 3:引入自定义哈希,优化字符串比较
    • Version 4:计算所有字符串的哈希值
    • Version 5:引入无符号长整型,自动取模
    • Version 6:引入双哈希,大大降低哈希碰撞
    • 总结
  • 代码
  • 主要参考资料

1044. 最长重复子串

困难

给你一个字符串 s ,考虑其所有 重复子串 :即 s 的(连续)子串,在 s 中出现 2 次或更多次。这些出现之间可能存在重叠。

返回 任意一个 可能具有最长长度的重复子串。如果 s 不含重复子串,那么答案为 “” 。

示例 1:

输入:s = “banana”
输出:“ana”

示例 2:

输入:s = “abcd”
输出:“”


题目 End…

前言

这个解法我是学习了其他大佬的题解后,我自己琢磨出的一个比较好理解的版本,但是效率并没有那么高,并且这个解法有一定误判的可能(并且官网题解也可能误判:官网使用随机数来取模 + 随机数作为进制,并且保证这两随机数的大小,所以保证了误判的概率会非常非常小,但是如果对这个随机数予以定值,仍然有可能误判,大家可以试试。个人浅见,如果有错误欢迎指出问题

所以我写的这个解法主要有两个意义:

  1. 可以以较低的成本加深大家对字符串哈希算法的理解,并且可以 完成并通过 这个题目
  2. 看了这篇文章之后,再去看官网的或者其他大佬写的更高效的版本可能会有帮助

在这里插入图片描述

思路

Version 1:暴力

首先这个是最容易想到的版本,枚举所有的子字符串,并加入哈希表中,进行判重

class Solution {
public:
    string longestDupSubstring(string s) {
        unordered_map<string, int> mp;
        string res;

        for (int i = 0; i < s.size(); i ++) {
            for (int len = 1; i + len <= s.size(); len ++) {
                string sub = s.substr(i, len);
                if (++ mp[sub] >= 2 && sub.size() > res.size()) {
                    res = sub;
                }
            }
        }

        return res;
    }
};

但是很明显会超时,第 19 个用例就无法通过。这里有几个问题可以尝试优化

  1. 是不是不需要遍历所有长度的子字符串?来优化掉这个 O ( n 2 ) O(n^2) O(n2)
  2. 在数据量非常庞大的情况下,unordered_map 可能存在性能退化的问题,在哈希冲突的情况下,时间复杂度可能会退化为 l o g 2 n log_2n log2n
  3. 其次,如果发生了哈希冲突,又需要针对哈希表红黑树结点内存值来比较 K e y Key Key 是否相等 —— 存在字符串比较,如果一个字符串长度是 3 ∗ 1 0 4 3 * 10^4 3104,那其实性能开销还是不小的

Version 2:引入二分,优化 O ( n 2 ) O(n^2) O(n2)

这个其实不难理解:

  • 如果存在一个字符串长度为 n,并且重复了,那么一定存在重复的、长度为 n - 1 的字符串
  • 如果不存在一个字符串长度为 n 的重复字符串,那么一定不存在重复的、长度为 n + 1 的字符串

举个例子
b a n a n a banana banana,明显最长重复子字符串为 a n a ana ana

  • 存在 a n a ana ana 长度为 3 的重复字符串,那么一定存在 a n an an 长度为 2 的子字符串(或者 n a na na
  • 如果不存在长度为 4 的重复字符串,那么一定不存在长度为 5 的重复子字符串

所以这里就可以通过二分查找的方式 ′ 猜答 案 ′ '猜答案' 猜答,思路有点像二分答案。如果猜大了,并且不存在这个长度的重复字符串,那么说明一定没有更大的重复字符串。

Version 3:引入自定义哈希,优化字符串比较

首先明确一点:如果有两个字符串 a a a b b b,那么就算不讨论哈希带来的消耗,在我们将 a a a 放入哈希表后,再尝试插入 b b b,如果发生哈希冲突了怎么办?

会遍历哈希桶链表(甚至红黑树),逐个结点判断是否存在重复 K e y Key Key,即就是 a a a 会和 b b b 发生逐字节比较

  • 如果 a a a b b b 长度很长呢?
  • 如果发生哈希冲突的不止一个结点呢?还有 c c c d d d e e e 字符串,并且他们长度都很长呢?

所以我们需要针对这里的字符串制定更高效的哈希方式:

假设现在有两个字符串, " a b c " "abc" "abc" (称为 A), " b c d " "bcd" "bcd"(称为 B)

  • 如果我们把他们想象成一个整数?假设 a = 1, b = 2, c = 3, d = 4
  • 那么 A 字符串,就可以是: 1 ∗ 100 + 2 ∗ 10 + 3 = 123 1 * 100 + 2 * 10 + 3 = 123 1100+210+3=123
  • 那么 B 字符串,就可以是: 2 ∗ 100 + 3 ∗ 10 + 4 = 234 2 * 100 + 3 * 10 + 4 = 234 2100+310+4=234
  • 那么这两个字符串的比较是否就变成了整数的比较?成百上千个字节的比较可以直接优化成 4或8 个字节的比较

但是显然小写字母有 26 个,所以这里的进制不能是 10,可以定成 31 进制,32 进制等,但是最好定成 31、67 这些质数(有利于降低哈希冲突,提高性能,具体设为多少更合适那就是专业人员做的事了,我不知道)

那么如果不考虑数据范围,那么我们是不是就根本不用考虑哈希冲突了?没错,因为字符串越长,这个哈希值一直都是递增的,只要数据范围装得下,除非字符串相等,否则永远都不会碰撞,效率嘎嘎高。可惜的是我们需要考虑数据范围,但是这个问题等会再说

Version 4:计算所有字符串的哈希值

先解决一个问题,如果我们要计算长度为 3 的字符串哈希值,比如上面的 a b c abc abc,那还是很好操作的,那么如果现在有字符串 a b c d e f g abcdefg abcdefg,我们怎么高效地计算所有长度为 3 的字符串哈希值呢?

  • 上面说了 a b c abc abc = 1 ∗ 100 + 2 ∗ 10 + 3 = 123 1 * 100 + 2 * 10 + 3 = 123 1100+210+3=123
  • 那么如果我们要计算 b c d bcd bcd 呢?我们是不是要舍弃 a a a 字母,也就是减少 a ∗ 100 a * 100 a100 呢?
  • 好,现在减少了 a ∗ 100 a * 100 a100,但是现在 b b b 变成了百位, c c c 变成了个位,是不是需要 ( 2 ∗ 10 + 3 ) ∗ 10 (2 * 10 + 3) * 10 (210+3)10 呢?也就是 【乘上进制
  • 最后加上 d d d

OK,于是就可以通过这种滑动窗口的方式, O ( n ) O(n) O(n) 复杂度计算所有长度为 3 的子字符串

Version 5:引入无符号长整型,自动取模

为了防止数据溢出,我们就可以引入取模,来防止数据溢出,但是为了简化程序,简化思路,这里可以使用 unsigned long long,无符号长整形,自带取模效果。

Version 6:引入双哈希,大大降低哈希碰撞

但是这样问题又来了,如果取模了,那么原本递增、很大的数据有可能突然特别小,以至于和前面已经哈希的值发生碰撞,那是否重复就不好说了

于是引入二次哈希,我们使用另一个不同的进制来计算该字符串的哈希值。如果字符串 A A A,字符串 B B B 两次哈希值都一样,那么我们认为字符串相等,所以可能存在误判,但是概率极小。

总结

  1. 二分答案,猜最长重复子串的长度,并验证
  2. 用数字表示字符串,高效进行哈希
  3. 防止溢出,自动取模,使用 unsigned long long
  4. 取模后导致哈希冲突,使用双哈希降低冲突概率
  5. (ps:pair<ULL, ULL> 不支持哈希,故使用 set

代码

class Solution {
public:
    constexpr static int P1 = 31;			// 31 进制
    constexpr static int P2 = 67;			// 67 进制


    // 最长长度的重复子串
    string longestDupSubstring(string s) {
        string res ;

		// 二分答案
        int left = 0, right = s.size() - 1;
        while (left <= right) {
            // +1 即向上取整,向上取整可以防止 guess_len = 0,比如 left = 0, right = 1
            int guess_len = left + ((right - left + 1) >> 1);
            string search = find(s, guess_len);	// 开始找有没有这个长度的重复子字符串

            if (search.size() == 0) {       // 没找到捏
                right = guess_len - 1;
            }
            else {                          // 找到了
                left = guess_len + 1;
            }

            if (search.size() > res.size())  res = search;
        }

        return res;
    }

    using ULL = unsigned long long;     
    using PULL = pair<ULL, ULL>;		// pair<ULL, ULL> 用来存储两次哈希值
    string find(const string& s, int len) {
        // 通过滑动窗口的方式来计算所有子串的 哈希值,ULL 会自动帮我们取模
        ULL hash1 = 0, hash2 = 0;
        ULL base1 = 1, base2 = 1;       // 当前 len 的最大进制,用于窗口右移的删减

        set<PULL> vis;	// 判重
        for (int i = 0; i < len; i ++) {
            hash1 = (hash1 * P1) + s[i];
            hash2 = (hash2 * P2) + s[i];

            base1 *= P1;	// 计算最高位,比如上面 abc 中的 100,自己模拟一次就明白了
            base2 *= P2;
        }
        vis.insert({hash1, hash2});	// 插入两次哈希值

        for (int i = len; i < s.size(); i ++) {
            hash1 = (hash1 * P1 + s[i]) - base1 * s[i - len];
            hash2 = (hash2 * P2 + s[i]) - base2 * s[i - len];

            if (vis.count({hash1, hash2})) {
                return s.substr(i - len + 1, len);
            }
            vis.insert({hash1, hash2});
        }

        return "";
    }
};

主要参考资料

【字符串哈希】字符串哈希入门(宫水三叶)
【微扰理论】Rabin-Karp + 二分搜索

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2051774.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前后端项目交互异步请求JSON数据类型后端标准响应数据格式

java同步请求 当网页与后端交互时,前端不能再进行其他操作 服务器响应回来的内容,会把整个浏览器中的内容覆盖 这种请求方式在前后端交互时不太友好 现在的前后端交互请求都使用异步请求 异步请求(不同步) 通过在前端中使用js中提供的XMLHttpRequest对象实现发送异步请求…

人工智能与机器学习在医学领域的应用

作者主页: 知孤云出岫 人工智能与机器学习在医学中的应用 目录 作者主页:人工智能与机器学习在医学中的应用1. 引言2. 医学中的AI和ML技术概述2.1 人工智能和机器学习基础2.2 数据在医学AI中的重要性 3. 医学AI和ML的具体应用领域3.1 影像诊断3.2 基因组学与个性化医疗3.3 疾…

JavaEE篇:多线程(1)

一 认识线程(Thread) 1.1 概念 1.1.1 线程是什么&#xff1f; 线程被创建出来是为了完成分配给它的任务。线程又称轻量级进程&#xff0c;是操作系统的基本调度单位。一个线程就是一个执行流。线程的创建销毁和切换都比进程更加的方便。进程是操作系统分配资源的基本单位&am…

C++ //练习 17.16 如果前一题程序中的regex对象用“[^c]ei“进行初始化,将会发生什么?用此模式测试你的程序,检查你的答案是否正确。

C Primer&#xff08;第5版&#xff09; 练习 17.16 练习 17.16 如果前一题程序中的regex对象用"[^c]ei"进行初始化&#xff0c;将会发生什么&#xff1f;用此模式测试你的程序&#xff0c;检查你的答案是否正确。 环境&#xff1a;Linux Ubuntu&#xff08;云服务…

「C++系列」数据结构

文章目录 一、数据结构1. 线性数据结构2. 非线性数据结构3. 其他重要数据结构 二、定义数据结构1. 数组&#xff08;Array&#xff09;2. 链表&#xff08;LinkedList&#xff09;3. 栈&#xff08;Stack&#xff09; 三、指针、关键字1. 指针链表树 2. 关键字 四、相关链接 一…

【TCP/IP】UDP协议数据格式和报文格式

学习一个网络协议&#xff0c;主要就是学习“数据格式”/“报文格式” 源端口/目的端口 端口号是属于传输层的概念UDP 报头使用两个自己的长度来表示端口号之所以端口号的范围是 0~65535&#xff0c;是因为底层网络协议做出了强制要求如果使用一个 10 w 这样的端口&#xff0…

机器学习:多元线性回归模型

目录 前言 一、讲在前面 1.多元_血压.csv&#xff1a; 2.完整代码&#xff1a; 3.运行结果&#xff1a; 二、实现步骤 1.导入库 2.导入数据 3.绘制散点图&#xff08;这步可以省略&#xff09; ​编辑 4.求特征和标签的相关系数 5.建立并训练线性回归模型 6.检验模…

NtripShare全站仪自动化监测之气象改正

最近有幸和自动化监测领域权威专家进行交流&#xff0c;讨论到全站仪气象改正的问题&#xff0c;因为有些观点与专家不太一致&#xff0c;所以再次温习了一下全站仪气象改正的技术细节。 气象改正的概念 全站仪一般利用光波进行测距&#xff0c;首先仪器会处理测距光波的相位漂…

C++| QT图片调整透明度叠加

QT图片调整透明度叠加 实际效果界面UI放置控件设置布局界面自适应 代码项目工程的文件初始化按钮功能滑动条功能图片调整透明度叠加 实际效果 三个图片&#xff08;QLabel&#xff09;显示&#xff0c;两个按钮&#xff08;QPushButton&#xff09;加载图片&#xff0c;一个&a…

【Java学习】反射和枚举详解

所属专栏&#xff1a;Java学习 &#x1f341;1. 反射 在程序运行时&#xff0c;可以动态地创建对象、调用方法、访问和修改字段&#xff0c;以及获取类的各种属性信息&#xff08;如成员变量、方法、构造函数等&#xff09;&#xff0c;这种机制就称为反射 反射相关的类 类名用…

【算法】马踏棋盘(骑士周游)问题回溯算法实现以及使用贪心算法优化

目录 1.游戏规则 2.算法分析 3.解决步骤和思路 4.马踏棋盘算法的代码实现 4.1计算马儿还能走哪些位置 4.2马踏棋盘的核心代码 4.3马踏棋盘算法完整代码 4.4使用贪心算法进行优化 4.4.1思路 4.4.2代码实现 1.游戏规则 将马儿随机放在国际象棋的 8*8 棋盘的某个方格中…

阶段练习——minishell

目录 &#xff08;一&#xff09;文件复制&#xff08;my_cp函数&#xff09; &#xff08;二&#xff09;文件内容查看&#xff08;my_cat函数&#xff09; &#xff08;三&#xff09;切换目录&#xff08;my_cd函数&#xff09; &#xff08;四&#xff09;列出目录内容…

一款专为IntelliJ IDEA用户设计的插件,极大简化Spring项目中的API调试过程,功能强大(附源码)

前言 在软件开发过程中&#xff0c;尤其是Spring MVC(Boot)项目中&#xff0c;API调试调用是一项常见但繁琐的任务。现有的开发工具虽然提供了一些支持&#xff0c;但往往存在效率不高、操作复杂等问题。为了处理这些痛点&#xff0c;提升开发效率&#xff0c;一款新的工具应运…

python 捕获异常

捕获指定异常 e 是保存的异常信息 捕获多个异常

快速体验fastllm安装部署并支持AMD ROCm推理加速

序言 fastllm是纯c实现&#xff0c;无第三方依赖的高性能大模型推理库。 本文以国产海光DCU为例&#xff0c;在AMD ROCm平台下编译部署fastllm以实现LLMs模型推理加速。 测试平台&#xff1a;曙光超算互联网平台SCNet GPU/DCU&#xff1a;异构加速卡AI 显存64GB PCIE&#…

Selenium + Python 自动化测试18(数据驱动实现测试)

我们的目标是&#xff1a;按照这一套资料学习下来&#xff0c;大家可以独立完成自动化测试的任务。 上一篇我们讨论了数据驱动测试中如何读取Excel文件&#xff0c;今天我们试着进一步深入学习数据驱动。 本篇文章我们讨论一下如何使用数据驱动思想实现测试。 1、数据驱动框架…

从零开始学cv-5: 图像的仿射变换

文章目录 一&#xff0c;简介&#xff1a;二&#xff0c;图像仿射变换详解2.1&#xff0c;图像平移&#xff1a;2.2 &#xff0c;图像旋转&#xff1a;2.3&#xff0c;仿射变换&#xff1a; 一&#xff0c;简介&#xff1a; 仿射变换&#xff08;Affine Transformation 或 Aff…

Lumina学术引擎免费问世,性能超谷歌学术5倍

Lumina介绍 Lumina是一款完全免费的AI学术搜索引擎&#xff0c;借助强大的数据库和高效的匹配速度。利用超过 15 种模型从超过 100 万篇期刊文章中找出最相关的来源&#xff0c;从而构建答案。搜索结果相关性平均比谷歌学术高出5倍&#xff0c;支持超1亿研究对象搜索&#xff…

8.18日学习打卡---Spring Cloud Alibaba(五)

8.18日学习打卡 目录&#xff1a; 8.18日学习打卡 RocketMQ什么是RocketMQ生产者和消费者技术架构 RocketMQ安装与配置环境搭建与测试RocketMQ管理命令 RocketMQ发送消息普通消息顺序消息之全局消息顺序消息之局部消息消费者消费消息延迟消息延迟消息代码实现单向消息批量消息过…

【HarmonyOS】云开发-用户自动认证

背景 华为云服务提供了统一认证的云服务&#xff0c;支持手机、邮箱等自定义登录服务&#xff0c;并且提供了免费使用的额度&#xff0c;这样子方便中小企业或者项目快速的开发工作。下面是支持的认证方式&#xff1a; 操作步骤 1.AGC(AppGallery Connect)创建项目 在AGC界…