Leetcode. 212 单词搜索II

题目信息

LeetoCode地址: 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

题目理解

该题目也是匹配字符串,但是高级一点。首先,要找到的字符串不再是单一一个,而是一个列表words,最大有3 *10^4个。其次,我们要在一个二维字符网格board上找寻,不再是若干个单词,即一个一维的字符数组。有点像需要棋盘上从任何一个位置开始走出一条蛇形路径,该路径刚好匹配字符串word。而每一步该怎么走呢?有四个方向,当然就有四个走法,到下一个位置后,同样有四种走法。那终止条件是什么?撞墙或者是四个方向的位置都已经用过了!所以我们每走一步,都要标记该位置以免走重复。而在该位置探索完返回时,要将标记抹除,就像一条蛇沿原路返回回退一样。简单地说,就是DFS+回溯。

写法1

我在刚开始思考这道题时,希望能够将board上的每一种可能路径都包含在Trie树中。在dfs过程中,由于words中每个字符串长度不超过10,所以dfs的深度也可以控制在10层以内。

其次,对于products每个字符串首字母没出现过的字符,都可以在遍历board每一个字符时过滤掉,无需进行dfs。

在构建好trie树后,再依次对words中的每一个元素进行遍历搜索,命中的元素加入到最终结果集中。

在board 为 m*n 大小,words 长度为p,每个字符串平均长度为q的情况下

时间复杂度: O(max(m*n*4^10, p*q)),前者是dfs构建Trie树操作, 后者是搜索words字符串操作

额外空间复杂度: O(max(m*n, p*q)), 前者是保存当前board字符占用的临时树组空间开销,后者是保存Trie树的空间开销。

Trie root;int h;int w;char[][] board;Set<String> searched;public List<String> findWords(char[][] board, String[] words) {h = board.length;w = board[0].length;searched = new HashSet<>();this.board = board;root = new Trie();int[] firstCharArray = new int[26];for (String word : words) {firstCharArray[word.charAt(0) - 'a'] = 1;}for (int i = 0; i < h; i++) {for (int j = 0; j < w; j++) {if (firstCharArray[board[i][j] - 'a'] == 0) {continue;}int[][] usedMap = new int[h][w];StringBuilder sb = new StringBuilder(board[i][j]);dfs(sb, i, j, usedMap);}}List<String> result = new ArrayList<>();for (String word : words) {Trie current = root;for (char c : word.toCharArray()) {if (current.nextList[c-'a'] != null) {current = current.nextList[c-'a'];} else {current = null;break;}}if (current != null && current.end) {result.add(word);}}return result;}private void dfs(StringBuilder sb, int i, int j, int[][] usedMap) {if (sb.length() >= 10 || i < 0 || i >= h || j < 0 || j>=w || usedMap[i][j] == 1) {String s = sb.toString();if (!searched.contains(s)) {searched.add(s);addWord(s);}return;}usedMap[i][j] = 1;sb.append(board[i][j]);boolean hasWay = false;if(i - 1 >=0) {hasWay = true;dfs(sb, i-1, j, usedMap);}if(i + 1 < h) {hasWay = true;dfs(sb, i+1, j, usedMap);}if(j - 1 >=0) {hasWay = true;dfs(sb, i, j-1, usedMap);}if(j + 1 < w) {hasWay = true;dfs(sb, i, j+1, usedMap);}if (!hasWay && !searched.contains(sb.toString())) {searched.add(sb.toString());addWord(sb.toString());}usedMap[i][j] = 0;sb.delete(sb.length()-1, sb.length());}public void addWord(String word) {Trie current = root;for (char c : word.toCharArray()) {if (current.nextList[c - 'a'] != null) {current = current.nextList[c - 'a'];} else {Trie next = new Trie();next.end = true;current.nextList[c - 'a'] = next;current = next;}}current.value = word;current.end = true;}class Trie {boolean end;String value;Trie[] nextList;public Trie() {this.end = false;this.nextList = new Trie[26];}}

写法2

除了对board进行Trie树构建,还有一种方法是对words树组进行Trie树构建,并在此树上通过对board进行dfs+回溯搜索的做法。这种做法时间复杂度上更有优势,因为在dfs时能够更加快速的收敛。写法1可能会遍历很多无效的字符串。

必须强调的是,由于回溯会将占用的字符复原,我们其实可以在board上记录哪些字符被占用,这节省了一部分空间。

而且Trie树节点也无需用end标识该节点是否是字符串终止节点,可以直接拿value的值替代,如果value为空,则没有字符串在该字符上终止,否则有。

Trie root;int h;int w;char[][] board;List<String> result;int[][] directions;public List<String> findWords(char[][] board, String[] words) {h = board.length;w = board[0].length;this.board = board;root = new Trie();directions = new int[][]{{1, 0}, {-2, 0}, {1, 1}, {0, -2}};for (String word : words) {addWord(word);}result = new ArrayList<>();for (int i = 0; i < h; i++) {for (int j = 0; j < w; j++) {dfs(i,j,root);}}return result;}private void dfs(int i, int j, Trie root) {char temp = board[i][j];if (board[i][j] == '$' || root.nextList[temp - 'a'] == null) {return;}Trie trie = root.nextList[temp - 'a'];if (trie.value != null) {result.add(trie.value);trie.value = null;}board[i][j] = '$';for (int[] direction : directions) {i += direction[0];j += direction[1];if (i < 0 || i >= h || j < 0 || j>=w) {continue;}dfs(i, j, trie);}board[i][j+1] = temp;}public void addWord(String word) {Trie current = root;for (char c : word.toCharArray()) {if (current.nextList[c - 'a'] == null) {current.nextList[c - 'a'] = new Trie();}current = current.nextList[c - 'a'];}current.value = word;}class Trie {boolean end;String value;Trie[] nextList;public Trie() {this.end = false;this.nextList = new Trie[26];}}

在board 为 m*n 大小,words 长度为p,每个字符串平均长度为q的情况下:

时间复杂度: O(max(m*n*3^10), p*q),前者是dfs构建Trie树操作, 后者是搜索words字符串操作

额外空间复杂度: O(p*q), 保存Trie树的空间开销。

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

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

相关文章

ThinkPhp3.2(qidian)部署文档

宝塔环境部署 申请域名以及域名解析 具体配置&#xff0c;可百度之 在宝塔面板中创建网站 上传代码导入数据配置运行目录 注意&#xff1a;&#xff08;如果版本&#xff1a;thinkphp3.2 &#xff09;配置 运行目录要特别注意&#xff1a;运行目录要选择根目录“/”&#xff…

什么是数据库的三级模式两级映象?

三级模式两级映象结构图 概念 三级模式 内模式&#xff1a;也称为存储模式&#xff0c;是数据物理结构和存储方式的描述&#xff0c;是数据在数据库内部的表示方式。定义所有的内部记录类型、索引和文件组织方式&#xff0c;以及数据控制方面的细节。模式&#xff1a;又称概念…

计算机今年炸了,究竟炸到什么程度呢❓

小兄弟&#xff0c;计算机哪年不爆炸啊&#xff01; 尤其是19年&#xff0c;20年&#xff0c;21年&#xff0c;可以说是计算机最卷的几年&#xff0c;这几年也刚好是互联网企业风头正盛的几年 从这里大家可以看出来&#xff0c;任何一个行业都有他的周期&#xff0c;任何一个专…

中等题 ------ 数组以及字符串

以前刷的都是一些简单题&#xff0c;从一些基本的数据结构到算法&#xff0c;得有400多道了&#xff0c;简单题就先这样吧&#xff0c;从今天以后就开始着手中等题和困难题了。 做了一些中等题&#xff0c;感觉确实和简单题没法比&#xff0c;简单题有些直接模拟&#xff0c;暴…

vue3框架基本使用

一、安装包管理工具 vite和vue-cli一样&#xff0c;都是脚手架。 1.node版本 PS E:\vuecode\vite1> node -v v18.12.12.安装yarn工具 2.1 yarn简单介绍 yarn是一个包管理工具&#xff0c;也是一个构建、打包工具 yarn需要借助npm进行安装&#xff1a;执行的命令行npm i…

linux安装docker-compose

前言 如果你的docker版本是23&#xff0c;请移步到linux安装新版docker&#xff08;23&#xff09;和docker-compose这篇博客 查看docker版本命令&#xff1a; docker --version今天安装docker-compose的时候&#xff0c;找了很多教程&#xff0c;但是本地一直报错&#xff0…

c++学习第十三讲---STL常用容器---string容器

string容器&#xff1a; 一、string的本质&#xff1a; string和char*的区别&#xff1a; char*是一个指针 string是一个类&#xff0c;封装了char*&#xff0c;管理这个字符串&#xff0c;是char*的容器。 二、string构造函数&#xff1a; string() ; …

C#常见内存泄漏

背景 在开发中由于对语言特性不了解或经验不足或疏忽&#xff0c;往往会造成一些低级bug。而内存泄漏就是最常见的一个&#xff0c;这个问题在测试过程中&#xff0c;因为操作频次低&#xff0c;而不能完全被暴露出来&#xff1b;而在正式使用时&#xff0c;由于使用次数增加&…

STM32之IIC总线控制ATC24C04

一、存储器介绍 1、电子密码存储概述 单片机的电子密码存储是一种将密码信息以电子形式存储在单片机内部的技术。它通常用于需要保护敏感信息或限制访问权限的应用程序&#xff0c;如安全系统、门禁系统、电子锁等。 电子密码存储可以通过多种方式实现&#xff0c;以下是其中…

Linux内核进程管理

什么是进程 进程的概念 进程是处于执行期的程序和他所占用资源的总称。进程就是运行的代码&#xff0c;进程的声明从代码开始运行那一刻开始&#xff1b;单纯的程序并非是是一个进程&#xff0c;一个程序也可能不只包含一个进程。 进程和线程的区别&#xff0c;与联系 线程…

Redis常用数据类型--String

String 常用命令SETGETMGETMSETSETNXINCR/DECRINCRBY/DECRBYINCRBYFLOATAPPENDGETRANGESETRANGESTRLEN 内部编码典型应用场景 常用命令 SET 将 string 类型的 value 设置到 key 中。如果 key 之前存在&#xff0c;则覆盖&#xff0c;⽆论原来的数据类型是什么。之 前关于此 k…

mysql8安装基础操作(一)

一、下载mysql8.0 1.查看系统glibc版本 这里可以看到glibc版本为2.17&#xff0c;所以下载mysql8.0的版本时候尽量和glibc版本对应 [rootnode2 ~]# rpm -qa |grep -w glibc glibc-2.17-222.el7.x86_64 glibc-devel-2.17-222.el7.x86_64 glibc-common-2.17-222.el7.x86_64 gl…

PingCode:引领敏捷开发的项目管理新范式

引言&#xff1a; 在快速变化的软件开发行业中&#xff0c;项目管理工具的选择对于团队的协作效率和项目的成功率至关重要。PingCode作为一款集成了敏捷开发理念的项目管理工具&#xff0c;正逐渐成为业界的新宠。本文将深入探讨PingCode的核心功能、使用场景以及如何利用这款工…

.NET高级面试指南专题三【线程和进程】

在C#中&#xff0c;线程&#xff08;Thread&#xff09;和进程&#xff08;Process&#xff09;是多任务编程中的重要概念&#xff0c;它们用于实现并发执行和多任务处理。 进程&#xff08;Process&#xff09;&#xff1a; 定义&#xff1a; 进程是正在运行的程序的实例&…

js的编码和解码

在 JavaScript 中&#xff0c;可以使用以下内置函数来进行编码和解码&#xff1a; 编码 encodeURIComponent(): 该函数用于对 URI 组件进行编码&#xff0c;它可以将字符串中的特殊字符转换为对应的编码形式。例如&#xff0c;空格会被编码为 %20。 var originalString &qu…

volatile内存语义

文章目录 volatile写的内存语义volatile读的内存语义&#xff1a;volatile内存语义的实现原理volatile禁止重排序规则volatile禁止重排序场景有序性案例分析案例描述错误代码&#xff1a;如何纠正&#xff1a;纠正后 volatile写的内存语义 当写一个volatile变量时&#xff0c;J…

鸿蒙架构Android架构分析

鸿蒙&#xff08;HarmonyOS&#xff09;和Android是两种主要的智能设备操作系统&#xff0c;它们在架构设计、功能特性和开发者支持等方面展现出不同的理念和优势。以下是对鸿蒙架构和Android架构的详细分析&#xff1a; 架构设计 鸿蒙OS架构&#xff1a; 鸿蒙OS采用微内核设…

C#,数据检索算法之插值搜索(Interpolation Search)的源代码

数据检索算法是指从数据集合&#xff08;数组、表、哈希表等&#xff09;中检索指定的数据项。 数据检索算法是所有算法的基础算法之一。 本文提供插值搜索&#xff08;Interpolation Search&#xff09;的源代码。 1 文本格式 using System; namespace Legalsoft.Truffer.…

CSS color探索

CSS 颜色探索 在 CSS 的世界里&#xff0c;颜色为网页元素赋予了丰富的视觉效果。通过预定义的颜色名称、RGB、HEX、HSL&#xff0c;以及支持透明度的 RGBA 和 HSLA&#xff0c;我们可以创造出各种吸引人的设计。接下来&#xff0c;我们将通过示例代码来深入了解这些颜色应用。…

kafka-顺序消息实现

kafka-顺序消息实现 场景 在购物付款的时候&#xff0c;订单会有不同的订单状态&#xff0c;对应不同的状态事件&#xff0c;比如&#xff1a;待支付&#xff0c;支付成功&#xff0c;支付失败等等&#xff0c;我们会将这些消息推送给消息队列 &#xff0c;后续的服务会根据订…