【算法题解】26. 求串联子串的位置

news2024/11/16 9:28:28

这是一道 困难
来自:
https://leetcode.cn/problems/substring-with-concatenation-of-all-words/

题目

给定一个字符串 s 和一个字符串数组 wordswords 中所有字符串 长度相同

s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。

例如,如果 words = [“ab”,“cd”,“ef”], 那么 "abcdef""abefcd""cdabef""cdefab""efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。

返回所有串联字串在 s 中的开始索引。你可以以 任意顺序 返回答案。

示例 1:

输入:s = "barfoothefoobarman", words = ["foo","bar"] 
输出:[0,9] 
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。 
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。 
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。 
输出顺序无关紧要。返回 [9,0] 也是可以的。 

示例 2:

输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"] 
输出:[] 
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。 
s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。 所以我们返回一个空数组。 

示例 3:

输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"] 
输出:[6,9,12] 
解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。 
子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。 
子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。 
子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。

提示:

  • 1 < = s . l e n g t h < = 1 0 4 1 <= s.length <= 10^4 1<=s.length<=104
  • 1 < = w o r d s . l e n g t h < = 5000 1 <= words.length <= 5000 1<=words.length<=5000
  • 1 < = w o r d s [ i ] . l e n g t h < = 30 1 <= words[i].length <= 30 1<=words[i].length<=30
  • w o r d s [ i ] words[i] words[i] s s s 由小写英文字母组成

暴力递归解法

  1. 递归计算出字符串数组 words 的所有的串联子串。
  2. 假如 words 中的每个单词长度为 k,子串长度为 n = k ∗ w o r d s . l e n g t h n = k * words.length n=kwords.length。根据下标遍历字符串 s, 每次截取 n 个字符,如果截取的字符串包含于第一步计算出的子串列表,则将当前下标计入答案。

详细见代码实现,很容易理解。

Java 代码实现

class Solution {
   
    // 每个单词的长度
    private int k = 0;

    // 串联子串长度
    private int n = 0;

    // 记录所有串联子串
    private Set<String> subStrs = new HashSet<>();

    // 记录已经访问过的位置
    private Set<Integer> visited = new HashSet<>();

    // 记录字符串拼接
    private StringBuilder sb = new StringBuilder();
    
    public List<Integer> findSubstring(String s, String[] words) {

        
        k = words[0].length();
        n = words.length * k;

        // 1. 先找出所有的串联子串
        recursion(words);

        // 2. 用串联子串和字符串s匹配,获取下标
        List<Integer> ans = new ArrayList<>();
        for(int i = 0; i <= s.length() - n; i++){
            String subStr = s.substring(i, i + n);
            if(subStrs.contains(subStr)){
                ans.add(i);
            }
        }
        return ans;
    }

	// 递归
    private void recursion(String[] words){
        int m = visited.size();
        if(m == words.length){
            subStrs.add(sb.toString());
            return;
        }
        for(int i = 0; i < words.length; i++){
            if(visited.contains(i)){
                continue;
            }
            visited.add(i);
            sb.append(words[i]);
            recursion(words);
            visited.remove(i);
            sb.delete(sb.length() - k, sb.length());
        }
    }
}

暴力解法在数据量小的情况下是可以的。

数据量大了就会超时。

哈希表解法

其实我们可以不用计算出 words 数组的所有子串,因为题目给定 串联子串 是以 任意顺序 排列连接起来的。

既然和顺序没有有关系,那么可以断定:如果一个字符串中的每个单词出现的次数和 words 数组中每个单词出现的次数相同,那么这个字符串一定是 words 数组的众多串联子串中的一个。

所以我们的思路为:

  1. words 数组中的单词计数。
  2. 假如串联子串的长度为 n 。我们可以遍历给定字符串 s ,每 n 个字符截取一个子串 subStr,然后对 subStr 中的单词计数。
  3. 如果 subStr 的单词计数和 words 数组的单词计数一样,那么 subStr 就是一个串联子串,记录其起始位置。

关于单词计数,可以使用 哈希表 这个数据结果,key 为单词本身,value 为这个单词出现的次数。

Java 代码实现

class Solution {

    // 单词计数
    private Map<String, Integer> wordsCount = new HashMap<String, Integer>();

    public List<Integer> findSubstring(String s, String[] words) {

        // 单词计数
        for(int i = 0; i < words.length; i++){
            int val = wordsCount.getOrDefault(words[i], 0);
            wordsCount.put(words[i], val + 1);
        }

        // 一个单词的长度
        int k = words[0].length();

        // 总字符串长度
        int n = k * words.length;

        List<Integer> ans = new ArrayList<>();

        // 滑动窗口中的子串单词计数
        Map<String, Integer> tempCount = new HashMap<>();

        for(int i = 0; i <= s.length() - n; i++){
           
            String subStr = s.substring(i, i + n);
            if(invalid(subStr, tempCount, k)){
                ans.add(i);
            }
            // 用完了就清空,以便重复使用
            tempCount.clear();
        }

        return ans;
    }


    private boolean invalid(String str, Map<String, Integer> tempCount, int k)  {
        
        for(int i = 0; i <= str.length() - k; i = i + k){
            String word = str.substring(i, i + k);
            tempCount.put(word, tempCount.getOrDefault(word, 0) + 1);
        }

        return equalsMap(tempCount, wordsCount);
    }


    // 判断两个计数的Map是否相等。
    private boolean equalsMap(Map<String, Integer> a, Map<String, Integer> b){
        if(a == null && b == null){
            return true;
        }else if(a == null && b != null){
            return false;
        }else if(a != null & b == null){
            return false;
        }
        
        if(a.isEmpty() && b.isEmpty()){
            return true;
        }

        if(a.size() != b.size()){
            return false;
        }

        for(String key: a.keySet()){
            if(!a.get(key).equals(b.get(key))){
                return false;
            }
        }

        return true;

    }
}

滑动窗口 + 哈希表解法

上一个解法中每次对子串中的单词计数都是重新计算的,这里是一个优化点。

我们可以将 n 个字符长度的子串看作是一个滑动窗口,每次向后滑动一个单词( k 个字符),那么我们只需要将前面移出去的单词个数减一,后面移动到窗口内部的单词个数加一,中间的其他单词个数因为没有变所以就无需再重新计算了。

Java 代码实现

class Solution {

    // 单词计数
    private Map<String, Integer> wordsCount = new HashMap<String, Integer>();

    public List<Integer> findSubstring(String s, String[] words) {

        List<Integer> ans = new ArrayList<>();
        // 一个单词的长度
        int k = words[0].length();

        // 单词个数
        int c = words.length;

        // 总字符串长度
        int n = k * c;

        if(s.length() < n){
            return ans;
        }

        // 单词计数
        for(int i = 0; i < c; i++){
            int val = wordsCount.getOrDefault(words[i], 0);
            wordsCount.put(words[i], val + 1);
        }

        // 滑动窗口中的子串单词计数
        Map<String, Integer> tempCount = new HashMap<>();

        for(int i = 0; i < k; i++){
            tempCount.clear(); 

            // 每次滑动一个单词,单词长度为k
            int left = i, right = left + n;
            if(right > s.length()){
                break;
            }

            // 对窗口中的字符串单词计数
            String subStr = s.substring(left, right);

            for(int j = 0; j <= n - k; j = j + k){
                String word = subStr.substring(j, j + k);
                tempCount.put(word, tempCount.getOrDefault(word, 0) + 1);
            }

            if(equalsMap(wordsCount, tempCount)){
                ans.add(left);
            }

            // 滑动窗口
            while(right + k <= s.length()){
                // 前面移出去一个单词
                String rmWord = s.substring(left, left + k);

                int val = tempCount.get(rmWord);
                if(val == 1){
                    tempCount.remove(rmWord);
                }else{
                    tempCount.put(rmWord, val - 1);
                }
                
                left = left + k;

                // 后面移进去一个单词
                String newWord = s.substring(right, right + k);
    
                tempCount.put(newWord, tempCount.getOrDefault(newWord, 0) + 1);
                right = right + k;

                if(equalsMap(wordsCount, tempCount)){
                    ans.add(left);
                }
            }


        }

        return ans;
    }

    // 判断两个计数的Map是否相等。
    private boolean equalsMap(Map<String, Integer> a, Map<String, Integer> b){
        if(a == null && b == null){
            return true;
        }else if(a == null && b != null){
            return false;
        }else if(a != null & b == null){
            return false;
        }
        
        if(a.isEmpty() && b.isEmpty()){
            return true;
        }

        if(a.size() != b.size()){
            return false;
        }

        for(String key: a.keySet()){
            if(!a.get(key).equals(b.get(key))){
                return false;
            }
        }

        return true;

    }
}

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

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

相关文章

REXROTH液压方向阀安装须知

安装规程 阀安装到系统之前&#xff0c;应该对照订货型号比较其型号说明。 确认阀的连接表面和底板无水分&#xff0c;没有油。 &#xff0d; 清洁&#xff1a; ‧ 安装元件时&#xff0c;确认工业阀和周围干净 ‧ 油箱须密闭&#xff0c;以防止外部污染 ‧ 安装之前&…

【youcans的深度学习 D02】PyTorch例程:创建 LeNet 模型进行图像分类

欢迎关注『youcans的深度学习』系列 【youcans的深度学习 D02】PyTorch例程&#xff1a;创建 LeNet 模型进行图像分类 1. PyTorch 深度学习建模的基本步骤2. 加载 CIFAR-10 数据集3. 定义 LeNet-5 模型类3.1 LeNet 网络3.2 LeNet-5 网络3.3 定义 LeNet-5 网络模型类3.4 构建网络…

AI大模型加速RPAxAI时代到来,谁会是RPA领域的杀手级应用?

GPT等AI大模型震撼来袭&#xff0c;基于RPA的超级自动化仍是最佳落地载体 对话弘玑CPO贾岿&#xff0c;深入了解国产RPA厂商对AI大模型的探索与实践 文/王吉伟 关于RPA已死的说法&#xff0c;在中国RPA元年&#xff08;2019年&#xff09;投资机构疯狂抢项目之时就已经有了。…

算法训练Day39:62.不同路径 63. 不同路径 II 动态规划

文章目录 不同路径题解(动态规划)数论方法 [不同路径 II](https://leetcode.cn/problems/unique-paths-ii/description/)题解 不同路径 CategoryDifficultyLikesDislikesContestSlugProblemIndexScorealgorithmsMedium (67.70%)17460--0 Tags Companies 一个机器人位于一个 …

Linux基础—网络设置

Linux基础—网络设置 一、查看网络配置1.查看网络接口信息 ifconfig2.查看主机名称 hostname3.查看路由表条目 route4.查看网络连接情况 netstat5.获取socket统计信息 ss 二、测试网络连接1.测试网络连接 ping2.跟踪数据包 traceroute3.域名解析 nslookup 三、使用网络配置命令…

拷贝构造与深浅拷贝

文章目录 一、拷贝构造函数二、拷贝初始化三、深浅拷贝 一、拷贝构造函数 如果一个构造函数的第一个参数是自身类型的引用&#xff0c;而且任何额外参数都有默认值&#xff0c;则此构造函数是拷贝构造函数。 class person { public: person(); //默认构造函数 pe…

54家备案法人信用评级机构名单

2023年4月20日&#xff0c;中国人民银行官网公示备案法人信用评级机构名单&#xff0c;共有54家机构获得了信用评级备案&#xff0c;并进行如下提示&#xff1a; 1.2019年11月26日&#xff0c;人民银行、发展改革委、财政部、证监会联合发布《信用评级业管理暂行办法》&#xf…

C语言之 顺序表(sequence chart)

线性表 线性表是n个具有相同特性的数据元素的有限序列。 常见的有&#xff1a;顺序表、链表、栈、队列、字符串.... 线性表在逻辑上是线性结构的&#xff0c;即一条连续的直线 但在物理结构上不一定是连续的&#xff0c;其在物理存储时&#xff0c;通常以数组和链式结构的形式…

观察者模式解读

目录 问题引进 天气预报项目需求 天气预报设计方案 1-普通方案 传统方式代码实现 观察者模式原理 观察者模式解决天气预报需求 代码实现 观察者模式的好处 问题引进 天气预报项目需求 1) 气象站可以将每天测量到的温度&#xff0c;湿度&#xff0c;气压等等以公告的形式…

idm下载器是免费的吗?有哪些功能

对于PC用户来说&#xff0c;拥有一款好用和快速的下载工具&#xff0c;对我们来说至关重要&#xff0c;可以极大提高我们的工作效率和PC用户体验。IDM可以实现高速下载&#xff0c;其核心原理就是多线程下载&#xff0c;理论上可以达到带宽的峰值速度&#xff0c;深受用户的喜爱…

Python单向链表操作

目录 一、单向链表 单向链表示例图 二、单向链表的操作 1、判断链表是否为空 2&#xff0c;链表长度 3&#xff0c;遍历整个链表 4&#xff0c;在链表头部添加元素 5、链表尾部添加元素 6&#xff0c;在指定位置插入元素 7&#xff0c;修改指定位置的元素 8&#xff…

可视化Echarts中title、tooltip、legend、grid属性的常用设置

title中常用的设置 配置项--tooltip ​编辑 配置项--legend 配置项--grid title中常用的设置 title 标题组件&#xff0c;包含主标题和副标题。 以下是常用的对标题的设置 title:{//设置图表的标题text:"主标题",link:"baidu.com", //设置标题超链接…

详解C语言string.h中常见的14个库函数(三)

本篇博客继续讲解C语言string.h头文件中的库函数。本篇博客计划讲解3个函数&#xff0c;分别是&#xff1a;strstr, strtok, strerror。其中strstr函数我会用一种最简单的方式模拟实现。 strstr char * strstr ( const char * str1, const char * str2 );strstr可以在str1中查…

用yolov5+playwright过滑动验证码

目录 梳理思路 训练模型 编写代码 总结与提高 源码下载 在上一节&#xff0c;我们通过opencv-pythonplaywright成功过掉了QQ空间的滑动验证码。在本节&#xff0c;我们将使用yolov5playwright来实现相同效果。 注&#xff1a;因为yolov5的配置教程网上已经很多了&#xff…

C++初阶之函数重载

目录 前言 函数重载 1.函数重载的概念 2.C支持函数重载的原理--名字修饰(name Mangling) 前言 今天小编给大家带来的是C中关于函数重载的内容&#xff0c;和C语言不一样&#xff0c;函数重载是C语言特有的&#xff0c;那么该功能实现的底层原理是什么呢&#xff1f;请大家…

Idea配置maven,指定settings.xml文件不生效

一.简介 最近单位要求把项目的仓库配置从阿里云改为nexus私服&#xff0c;配置了一个settings-nexus.xml的配置文件&#xff0c;idea的maven配置指定了该settings-nexus.xml文件&#xff0c;发现走的还是阿里云的&#xff0c;新的settings-nexus.xml竟然不生效&#xff0c;依赖…

分支和循环语句(1)

文章目录 目录1. 什么是语句2. 分支语句&#xff08;选择结构&#xff09;2.1 if语句2.1.1 悬空else2.1.2 if书写形式的对比2.1.3 练习 2.2 switch语句2.2.1 在switch语句中的 break2.2.2 default子句2.2.3 练习 3. 循环语句3.1 while循环3.1.1 while语句中的break和continue 附…

记一次fastjson反序列化到内网靶标

声明&#xff1a;文中涉及到的技术和工具&#xff0c;仅供学习使用&#xff0c;禁止从事任何非法活动&#xff0c;如因此造成的直接或间接损失&#xff0c;均由使用者自行承担责任。 点点关注不迷路&#xff0c;每周不定时持续分享各种干货。 众亦信安&#xff0c;中意你啊&a…

多种方法解决This is usually caused by another repository pushing to the same ref的错误

文章目录 1. 复现错误2. 分析错误3. 解决错误4. 解决该错误的其他方法 1. 复现错误 今天使用git status查看文件状态&#xff0c;发现有一个文件未提交&#xff0c;如下代码所示&#xff1a; D:\project\test>git status On branch master Your branch is up to date with …

sftp常用命令介绍

sftp常用命令&#xff1a; 1. sftp 登录sftp服务器 sftp userip ​​​​​​ 如需要看全部命令&#xff1a;则使用help即可 2. pwd和lpwd 、 ls和lls 、cd和lcd 等 sftp登录之后默认操作是远程服务器&#xff0c;当需要操作本地时&#xff0c;就需要在前边加“l”&#…