首页 文章 精选 留言 我的

精选列表

搜索[超融合软件],共10000篇文章
优秀的个人博客,低调大师

【建议使用】告别if,Java超好用参数校验工具类

一、前言 今天和小伙伴们分享一个常用的工具类,共计四个方法,使用场景比较广泛,有用于校验某个对象或对象中指定属性为空值时,直接返回异常,常用语校验前端请求参数;也有当值不为空时,执行指定动作,可减少大量的if条件,如:mybatis请求参数设置;还有用于判断当值不为空时,替代为新值,完成后续动作。 这样描述可能不够清晰,这里我列举了几个使用场景,更多的场景需要小伙伴们根据自己业务需求合理使用。 //场景一,点击登录时,后端校验用户名 if(StringUtils.isEmpty(name)){ throw new Exception("登录用户名不能为空"); } ​ //场景二:属性内容转换为新值 String address = "浙江省杭州市"; if(StringUtils.isNotEmpty(address)){ address ="地址:"+address; } ​ //场景三:替代过多的if条件 SysUserDto userDto = new SysUserDto();//前端参数 ​ SysUser user = new SysUser();//mybatis参数 if(StringUtils.isEmpty(userDto.getName())){ user.setUserName(userDto.getName()) } if(StringUtils.isEmpty(userDto.getPwd())){ user.setPassword(userDto.getPwd()) } 复制代码 \ 二、正文 首先创建一个测试实体类: import lombok.Data; ​ @Data public class SysUser{ private String name; private String password; } 复制代码 2.1 检查多个对象不能为空 测试范例 SysUser user = new SysUser(); SysUser user2 = null; Args.notEmptys(user,user2); 复制代码 方法内容 public static void notEmptys(Object... objects) { for (Object obj : objects) { if (obj == null) { throw new BusinessException("属性不能为空"); } if (obj.toString().trim().isEmpty()) { throw new BusinessException("属性不能为空"); } } } 复制代码 测试结果 Exception in thread "main" 属性不能为空 at com.basic.business.utils.Args.notEmptys(Args.java:27) at com.basic.business.demo.DemoController.main(DemoController.java:43) 复制代码 2.2 校验对象属性不能为空 若将参数【Boolean isAll】设置为true,则会校验全部属性;若设置为false,就需要将检测字段放入【propertys】中。 测试范例 SysUser user = new SysUser(); //用法一:校验全部参数 Args.checkField(user,true,""); //用法二:只校验password参数 Args.checkField(user,false,"password"); 复制代码 方法内容 /** * 对象多字段判空检查 * @param obj 被检查的对象 * @param isAll 是否检查全部参数 * @param propertys 被检查对象中的字段 可多个 */ public static void checkField(Object obj, Boolean isAll, String... propertys) { if (obj != null) { Class<? extends Object> clazz = obj.getClass(); if (isAll) { PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(clazz); for (int p = 0; p < propertyDescriptors.length; p++) { checkEachField(obj, propertyDescriptors[p]); } } else { if (propertys != null && propertys.length > 0) { //遍历所有属性 for (int i = 0; i < propertys.length; i++) { String property = propertys[i]; //获取属性信息 BeanUtils.getPropertyDescriptors(clazz); PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(clazz, property); checkEachField(obj, pd); } } } } } ​ private static void checkEachField(Object obj, PropertyDescriptor pd) { Class<? extends Object> clazz = obj.getClass(); String property = pd.getName(); if (pd != null) { //获取当前字段的javabean读方法 Method readMethod = pd.getReadMethod(); if (readMethod != null) { ​ Object invoke = null; try { invoke = readMethod.invoke(obj); } catch (Exception e) { throw new BusinessException("方法 " + readMethod.getName() + "无法执行"); } ​ if (invoke != null) { //String类型单独处理 Class<?> propertyType = pd.getPropertyType(); if ("java.lang.String".equals(propertyType.getName())) { if (StringUtils.isBlank((String) invoke)) { throw new BusinessException("错误 : [ " + property + " ] 不能为空!"); } } else if ("java.util.List".equals(propertyType.getName())) { List list = (List) invoke; if (list.size() == 0) { throw new BusinessException("错误 : [ " + property + " ] 不能为空!"); } } } else { throw new BusinessException("错误 : [ " + property + " ] 不能为空!"); } } else { throw new BusinessException("在 " + clazz + "中 找不到" + "[ " + property + " ] 的 读方法"); } } else { throw new BusinessException("在 " + clazz + "中 找不到" + "[ " + property + " ] 属性"); } } 复制代码 测试结果 用法一结果: Exception in thread "main" 错误 : [ name ] 不能为空! at com.basic.business.utils.Args.checkEachField(Args.java:116) at com.basic.business.utils.Args.checkField(Args.java:77) 用法二结果: Exception in thread "main" 错误 : [ password ] 不能为空! at com.basic.business.utils.Args.checkEachField(Args.java:116) at com.basic.business.utils.Args.checkField(Args.java:77) 复制代码 2.3 参数不为空时执行指定动作 我们经常会遇到这种场景,根据前端参数来组装数据库查询条件,如果不为空我们就将前端值设置到是实体类中,或者如下面这个例子中,当name不为空时,加入到list当中。 测试范例 List list = Lists.newArrayList(); ​ SysUser user = new SysUser(); user.setName("huage"); Args.doIfNotEmpty(user.getName(),list::add); System.out.println(list); 复制代码 方法内容 public static <R, T> R doIfNotNull(T t, Callback<T, R> callback) { try { return t == null ? null : callback.call(t); } catch (Exception e) { throw new BusinessException("[doIfNotNull error]", e); } } 复制代码 测试结果 [huage] 复制代码 2.4 参数为空时使用新值,并完成指定动作 本方法和2.较为类似,只是当目标值不为空时,使用新值进行后续的操作,如下面这个例子中,name初始值为【test】,执行完doIfNotNullNewValue后会将新值【huage】添加到list中,。 测试范例 List list = Lists.newArrayList(); ​ SysUser user = new SysUser(); user.setName("test"); Args.doIfNotNullNewValue(user.getName(),"huage",list::add); System.out.println(list); 复制代码 方法内容 public static <R, T> R doIfNotNullNewValue(T t,T newt, Callback<T, R> callback) { try { return t == null ? null : callback.call(newt); } catch (Exception e) { throw new BusinessException("[doIfNotNull error]", e); } } 复制代码 测试结果 [huage] 最后 如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163相互学习,我们会有专业的技术答疑解惑 如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star:https://gitee.com/ZhongBangKeJi/crmeb_java不胜感激!

优秀的个人博客,低调大师

超使用的15个前端小知识点

本文章转自:乐字节 文章主要讲解:前端小知识 获取更多前端相关资料可以关注公众号《乐字节》 发送:999 ### 1. css 一行文本超出... ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/674afe2ba49848f298883b0455d4ac53~tplv-k3u1fbpfcp-watermark.image) ### 2.多行文本超出显示... ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f58932f58e8a4ee897e0e908d184c3a2~tplv-k3u1fbpfcp-watermark.image) ### 3.IOS 手机容器滚动条滑动不流畅 ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1c94b0dfc4004421991f9da41af5337d~tplv-k3u1fbpfcp-watermark.image) ### 4.修改滚动条样式 隐藏 div 元素的滚动条 ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/59c1e55c2846450480eedd5d3805bb8a~tplv-k3u1fbpfcp-watermark.image) ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d5424fa59a544f758fee96f9e4b6df04~tplv-k3u1fbpfcp-watermark.image) ### 5.使用 css 写出一个三角形角标 元素宽高设置为 0,通过 border 属性来设置,让其它三个方向的 border 颜色为透明或者和背景色保持一致,剩余一条 border 的颜色设置为需要的颜色。 ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f178ea4f9b7c402499b864ca8f2f5aff~tplv-k3u1fbpfcp-watermark.image) ### 6.解决 ios audio 无法自动播放、循环播放的问题 ios 手机在使用 audio 或者 video 播放的时候,个别机型无法实现自动播放,可使用下面的代码 hack。 ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bfbb93a75f9849cca6cfcb091a130a65~tplv-k3u1fbpfcp-watermark.image) ### 7.水平垂直居中 我一般只使用两种方式 定位 或者 flex,我觉得够用了。 ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e3bcbb433f8d4fbdba618129ad09f26c~tplv-k3u1fbpfcp-watermark.image) 父级控制子集居中 ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1d185f9e47d4f0faee15d8e77a0f86a~tplv-k3u1fbpfcp-watermark.image) ### 8.隐藏页面元素 ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/10f27c82903144189ec0db53826d97e5~tplv-k3u1fbpfcp-watermark.image) ### 9.前端工程化 一提到前端工程化很多人想到的都是 webpack,这是不对的,webpack 仅仅是前端工程化中的一环。在整个工程化过程中他帮我们解决了绝大多数的问题,但并没有解决所有问题。 前端工程化是通过工具提升效率,降低成本的一种手段。 近些年被广泛的关注和探讨,究其原因主要是因为现代化前端应用功能要求不断提高,业务逻辑日益复杂,作为当下互联网时代唯一不可或缺的技术,前端可以说是占据了整个开发行业的半壁江山。从传统的网站,到现在的 H5,移动 App,桌面应用,以及小程序。前端技术几乎是无所不能的全面覆盖。 在这些表象的背后呢,实际上是行业对开发人员的要求发生了天翻地覆的变化,以往前端写 demo,套模板,调页面这种刀耕火种的方式已经完全不符合当下对开发效率的要求,前端工程化就是在这样一个背景下被提上台面,成为前端工程师必备的手段之一。 一般来说前端工程包含,项目初始化,项目开发,集成,构建,打包,测试,部署等流程。工程化就是以工程的角度来解决这些问题。比如项目初始化我们一般使用npm init, 创建页面模板使用 plop,我们喜欢使用 ES6+开发,但是需要通过 babel 编码成 ES5,持续集成的时候我们使用 git,但是为了保持开发规范我们引入了 ESLint,部署一般使用 ci/cd 或者 jenkins 等等。 前端工程化是一个比较大的话题,后面我们会单开话题来讲。 ### 10.contenteditable html 中大部分标签都是不可以编辑的,但是添加了 contenteditable 属性之后,标签会变成可编辑状态。 ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e568b39576084c96a4240c566f637caf~tplv-k3u1fbpfcp-watermark.image) 不过通过这个属性把标签变为可编辑状态后只有 input 事件,没有 change 事件。也不能像表单一样通过 maxlength 控制最大长度。我也忘记我在什么情况下用到过了,后面想起来再补吧。 ### 11.calc 这是一个 css 属性,我一般称之为 css 表达式。可以计算 css 的值。最有趣的是他可以计算不同单位的差值。很好用的一个功能,缺点是不容易阅读。接盘侠没办法一眼看出 20px 是啥。 ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4550834a36434c6fa077b6987a7ea78d~tplv-k3u1fbpfcp-watermark.image) ### 12.Proxy 和 Object.defineProperty 区别 Proxy 的意思是代理,我一般教他拦截器,可以拦截对象上的一个操作。用法如下,通过 new 的方式创建对象,第一个参数是被拦截的对象,第二个参数是对象操作的描述。实例化后返回一个新的对象,当我们对这个新的对象进行操作时就会调用我们描述中对应的方法。 ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4f35bb42115541aba43b1f041356db45~tplv-k3u1fbpfcp-watermark.image) Proxy 区别于 Object.definedProperty。 Object.defineProperty 只能监听到属性的读写,而 Proxy 除读写外还可以监听属性的删除,方法的调用等。 通常情况下我们想要监视数组的变化,基本要依靠重写数组方法的方式实现,这也是 Vue 的实现方式,而 Proxy 可以直接监视数组的变化。 ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8cbc21d1c848410e9bb80e37edb5a482~tplv-k3u1fbpfcp-watermark.image) Proxy 是以非***的方式监管了对象的读写,而 defineProperty 需要按特定的方式定义对象的属性。 ### 13.Reflect 他是 ES2015 新增的对象,纯静态对象也就是不能被实例画,只能通过静态方法的方式调用,和 Math 对象类似,只能类似 Math.random 的方式调用。 Reflect 内部封装了一系列对对象的底层操作,一共 14 个,其中 1 个被废弃,还剩下 13 个。 Reflect 的静态方法和 Proxy 描述中的方法完全一致。也就是说 Reflect 成员方法就是 Proxy 处理对象的默认实现。 Proxy 对象默认的方法就是调用了 Reflect 内部的处理逻辑,也就是如果我们调用 get 方法,那么在内部,proxy 就是将 get 原封不动的交给了 Reflect,如下。 ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3264c8bf353749f2b1145fe951748cd7~tplv-k3u1fbpfcp-watermark.image) Reflect 和 Proxy 没有绝对的关系,我们一般将他们两个放在一起讲是为了方便对二者的理解。 那为什么会有 Reflect 对象呢,其实他最大的用处就是提供了一套统一操作 Object 的 API。判断对象是否存在某一个属性,可以使用 in 操作符,但是不够优雅,还可以使用Reflect.has(obj, name); 删除一个属性可以使用 delete,也可以使用 Reflect.deleteProperty(obj, name); 获取所有属性名可以使用 Object.keys, 也可以使用 Reflect.ownKeys(obj); 我们更推荐使用 Reflect 的 API 来操作对象,因为他才是未来。 ### 14.解析 get 参数 通过 replace 方法获取 url 中的参数键值对,可以快速解析 get 参数。 ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1e9b31861ff7421ea14bfc86a77acf24~tplv-k3u1fbpfcp-watermark.image) ### 15.解析连接 url 可以通过创建 a 标签,给 a 标签赋值 href 属性的方式,获取到协议,pathname,origin 等 location 对象上的属性。 ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/def24930085744bf94dee57b75eff14d~tplv-k3u1fbpfcp-watermark.image) **感谢大家的认同与支持,小编会持续转发《乐字节》优质文章**

优秀的个人博客,低调大师

五分钟了解什么是Mybatis(超详细)

MyBatis框架是什么? MyBatis 框架: MyBatis 本是apache 的一个开源项目 iBatis, 2010 年这个项目由apache software foundation 迁移到了google code,并且改名为 MyBatis 。2013 年 11 月迁移到Github。 MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。 通过本视频的学习,可以在最短的时间内学会使用持久层框架MyBatis,在该视频中没有废话,都是干货,该视频的讲解不是学术性研究,项目中用什么,这里就讲什么,如果您现在项目中马上要使用MyBatis框架,那么您只需要学习完,就可以顺利的使用MyBatis开发了。 在线观看:2020最新MyBatis教程【IDEA版】-MyBatis从入门到精通_哔哩哔哩_bilibili 资料下载:Mybatis视频教程全套免费下载【适合零基础入门学习】 - 动力节点 (bjpowernode.com) MyBatis解决的主要问题 减轻使用JDBC 的复杂性,不用编写重复的创建Connetion , Statement ; 不用编写关闭资源代码。 直接使用java 对象,表示结果数据。让开发者专注 SQL 的处理。 其他分心的工作由MyBatis 代劳。 MyBatis 可以完成: 1. 注册数据库的驱动,例如Class.forName(“com.mysql.jdbc.Driver”)) 2. 创建JDBC 中必须使用的 Connection , Statement, ResultSet 对象 3. 从xml 中获取sql,并执行sql 语句,把ResultSet 结果转换java 对象 List<Student> list = new ArrayLsit<>(); ResultSet rs = state.executeQuery(“select * from student”); while(rs.next){ Student student = new Student(); student.setName(rs.getString(“name”)); student.setAge(rs.getInt(“age”)); list.add(student); } 4. 关闭资源 ResultSet.close() , Statement.close() , Conenection.close() 入门案例 MyBatis 开发准备 搭建MyBatis 开发环境,实现第一个案例 使用Mybatis 准备 下载 mybatishttps://github.com/mybatis/mybatis-3/releases 搭建MyBatis 开发环境 (1) 创建 mysql 数据库和表 数据库名 ssm ;表名student CREATE TABLE `student` ( `id` int(11) NOT NULL , `name` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; (2) 创建 maven 工程 创建maven 工程,信息如下: 模板: 工程坐标: (3) 删除默认创建的 App 类文件 (4) 加入 maven 坐标 pom.xml 加入maven 坐标: (5) 加入 maven 插件 (6) 编写 Student 实体类 创建包 com.bjpowernode.domain, 包中创建Student 类 (7) 编写 Dao 接口 StudentDao (8) 编写 Dao 接口 Mapper 映射文件 StudentDao.xml 要求: 1. 在dao 包中创建文件StudentDao.xml 2. 要StudentDao.xml 文件名称和接口StudentDao 一样,区分大小写的一样。 (9) 创建 MyBatis 主配置文件 项目src/main 下创建 resources 目录,设置 resources 目录为 resources root 创建主配置文件:名称为mybatis.xml 说明:主配置文件名称是自定义的,内容如下: 支持中文的url jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf-8 (10) 创建测试类 MyBatisTest src/test/java/com/bjpowernode/ 创建MyBatisTest.java 文件 (11) 配置日志功能 mybatis.xml 文件加入日志配置,可以在控制台输出执行的sql 语句和参数 insert 操作 (1) StudentDao 接口中增加方法 int insertStudent(Student student); (2) StudentDao.xml 加入 sql 语句 (3) 增加测试方法 MyBatis 对象分析 对象使用 SqlSession , SqlSessionFactory 等 (1) Resources 类 Resources 类,顾名思义就是资源,用于读取资源文件。其有很多方法通过加载并解析资源文件,返回不同类型的IO 流对象。 (2) SqlSessionFactoryBuilder 类 SqlSessionFactory 的创建,需要使用 SqlSessionFactoryBuilder 对象的build()方法。由于 SqlSessionFactoryBuilder 对象在创建完工厂对象后,就完成了其历史使命,即可被销毁。所以,一般会将该 SqlSessionFactoryBuilder 对象创建为一个方法内的局部对象,方法结束,对象销毁。 (3) SqlSessionFactory 接口 SqlSessionFactory 接口对象是一个重量级对象(系统开销大的对象),是线程安全的,所以一个应用只需要一个该对象即可。创建 SqlSession 需要使用SqlSessionFactory 接口的的openSession()方法。 openSession(true):创建一个有自动提交功能的 SqlSession openSession(false):创建一个非自动提交功能的 SqlSession,需手动提交 openSession():同 openSession(false) (4) SqlSession 接口 SqlSession 接口对象用于执行持久化操作。一个 SqlSession 对应着一次数据库会话,一次会话以 SqlSession 对象的创建开始,以 SqlSession 对象的关闭结束。 SqlSession 接口对象是线程不安全的,所以每次数据库会话结束前,需要马上调用其 close()方法,将其关闭。再次需要会话,再次创建。 SqlSession 在方法内部创建,使用完毕后关闭。 创建工具类 (1) 创建 MyBatisUtil 类 package com.bjpowernode.common; (2) 使用 MyBatisUtil 类 MyBatis 使用传统 Dao 开发方式 使用Dao 的实现类,操作数据库 Dao 开发 (1) 创建 Dao 接口实现类 public class StudentDaoImpl implements StudentDao (2) 实现接口中 select 方法 测试查询操作: MyBatisTest 类中创建StudentDaoImpl 对象 (3) 实现接口中 insert 方法 测试insert 传统Dao 开发方式的分析 在前面例子中自定义Dao 接口实现类时发现一个问题:Dao 的实现类其实并没有干什么实质性的工作,它仅仅就是通过 SqlSession 的相关 API 定位到映射文件mapper 中相应 id 的 SQL 语句,真正对 DB 进行操作的工作其实是由框架通过mapper 中的 SQL 完成的。 所以,MyBatis 框架就抛开了Dao 的实现类,直接定位到映射文件mapper 中的相应 SQL 语句,对DB 进行操作。这种对Dao 的实现方式称为Mapper 的动态代理方式。 Mapper 动态代理方式无需程序员实现Dao 接口。接口是由 MyBatis 结合映射文件自动生成的动态代理实现的。

优秀的个人博客,低调大师

【超详细】一文学会链表解题

前言 如果说数据结构是算法的基础,那么数组和链表就是数据结构的基础。 因为像堆,栈,对,图等比较复杂的数组结基本上都可以由数组和链表来表示,所以掌握数组和链表的基本操作十分重要。 今天就来看看链表的基本操作及其在面试中的常见解题思路,本文将从以下几个点来讲解链表的核心知识 什么是链表,链表的优缺点 链表的表示及基本操作 面试中链表的常见解题思路---翻转 面试中链表的常见解题思路---快慢指针 什么是链表 相信大家已经开始迫不及待地想用链表解题了,不过在开始之前我们还是要先来温习下链表的定义,以及它的优势与劣势,磨刀不误砍柴功! 链表的定义 链表是物理存储单元上非连续的、非顺序的存储结构,它是由一个个结点,通过指针来联系起来的,其中每个结点包括数据和指针。 链表的非连续,非顺序,对应数组的连续,顺序,我们来看看整型数组 1,2,3,4 在内存中是如何表示的 可以看到数组的每个元素都是连续紧邻分配的,这叫连续性,同时由于数组的元素占用的大小是一样的,在 Java 中 int 型大小固定为 4 个字节,所以如果数组的起始地址是 100, 由于这些元素在内存中都是连续紧邻分配的,大小也一样,可以很容易地找出数组中任意一个元素的位置,比如数组中的第三个元素起始地址为 100 + 2 * 4 = 108,这就叫顺序性。查找的时间复杂度是O(1),效率很高! 那链表在内存中是怎么表示的呢 可以看到每个结点都分配在非连续的位置,结点与结点之间通过指针连在了一起,所以如果我们要找比如值为 3 的结点时,只能通过结点 1 从头到尾遍历寻找,如果元素少还好,如果元素太多(比如超过一万个),每个元素的查找都要从头开始查找,时间复杂度是O(n),比起数组的 O(1),差距不小。 除了查找性能链表不如数组外,还有一个优势让数组的性能高于链表,这里引入程序局部性原理,啥叫程序局部性原理。 我们知道 CPU 运行速度是非常快的,如果 CPU 每次运算都要到内存里去取数据无疑是很耗时的,所以在 CPU 与内存之间往往集成了挺多层级的缓存,这些缓存越接近CPU,速度越快,所以如果能提前把内存中的数据加载到如下图中的 L1, L2, L3 缓存中,那么下一次 CPU 取数的话直接从这些缓存里取即可,能让CPU执行速度加快,那什么情况下内存中的数据会被提前加载到 L1,L2,L3 缓存中呢,答案是当某个元素被用到的时候,那么这个元素地址附近的的元素会被提前加载到缓存中 以上文整型数组 1,2,3,4为例,当程序用到了数组中的第一个元素(即 1)时,由于 CPU 认为既然 1 被用到了,那么紧邻它的元素 2,3,4 被用到的概率会很大,所以会提前把 2,3,4 加到 L1,L2,L3 缓存中去,这样 CPU 再次执行的时候如果用到 2,3,4,直接从 L1,L2,L3 缓存里取就行了,能提升不少性能 画外音:如果把CPU的一个时种看成一秒,则从 L1 读取数据需要 3 秒,从 L2 读取需要 11 秒,L3读取需要 25秒,而从内存读取呢,需要 1 分 40 秒,所以程序局部性原理能对 CPU 执行性能有很大的提升 而链表呢,由于链表的每个结点在内存里都是随机分布的,只是通过指针联系在一起,所以这些结点的地址并不相邻,自然无法利用 程序局部性原理 来提前加载到 L1,L2,L3 缓存中来提升程序性能。 画外音:程序局部性原理是计算机中非常重要的原理,这里不做展开,建议大家查阅相关资料详细了解一下 如上所述,相比数组,链表的非连续,非顺序确实让它在性能上处于劣势,那什么情况下该使用链表呢?考虑以下情况 大内存空间分配 由于数组空间的连续性,如果要为数组分配 500M 的空间,这 500M 的空间必须是连续的,未使用的,所以在内存空间的分配上数组的要求会比较严格,如果内存碎片太多,分配连续的大空间很可能导致失败。而链表由于是非连续的,所以这种情况下选择链表更合适。 元素频繁删除和插入 如果涉及到元素的频繁删除和插入,用链表就会高效很多,对于数组来说,如果要在元素间插入一个元素,需要把其余元素一个个往后移(如图示),以为新元素腾空间(同理,如果是删除则需要把被删除元素之后的元素一个个往前移),效率上无疑是比较低的。 (在 1,2 间插入 5,需要把2,3,4 同时往后移一位) 而链表的插入删除相对来说就比较简单了,修改指针位置即可,其他元素无需做任何移动操作(如图示:以插入为例) 综上所述:如果数据以查为主,很少涉及到增和删,选择数组,如果数据涉及到频繁的插入和删除,或元素所需分配空间过大,倾向于选择链表。 说了这么多理论,相信读者对数组和链表的区别应该有了更深刻地认识了,尤其是 程序局部性原理,是不是开了不少眼界^_^,如果面试中问到数组和链表的区别能回答到程序局部性原理,会是一个非常大的亮点! 接下来我们来看看链表的表现形式和解题技巧 需要说明的是有些代码像打印链表等限于篇幅的关系没有在文中展示,我把文中所有相关代码都放到 github 中了,大家如果需要,可以访问我的 github 地址: https://github.com/allentofight/algorithm 下载运行(微信不支持外链,建议大家 copy 之后浏览器打开再下载运行),文中所有代码均已用 Java 实现并运行通过 链表的表示 由于链表的特点(查询或删除元素都要从头结点开始),所以我们只要在链表中定义头结点即可,另外如果要频繁用到链表的长度,还可以额外定义一个变量来表示。 需要注意的是这个头结点的定义是有讲究的,一般来说头结点有两种定义形式,一种是直接以某个元素结点为头结点,如下 一种是以一个虚拟的节点作为头结点,即我们常说的哨兵,如下 定义这个哨兵有啥好处呢,假设我们不定义这个哨兵,来看看链表及添加元素的基本操作怎么定义的 /** *链表中的结点,data代表节点的值,next是指向下一个节点的引用 */classNode{intdata;//结点的数组域,值Nodenext=null;//节点的引用,指向下一个节点publicNode(intdata){this.data=data; } }/** 链表*/public class LinkedList {int length = 0; // 链表长度,非必须,可不加Node head = null; // 头结点public void addNode(int val) {if (head == null) {head = new Node(val);} else {Node tmp = head;while (tmp.next != null) {tmp = tmp.next;}tmp.next = new Node(val);}}} 发现问题了吗,注意看下面代码 有两个问题: 每插入一个元素都要对头结点进行判空比较,如果一个链表有很多元素需要插入,就需要进行很多次的判空处理,不是那么高效 头结点与其他结点插入逻辑不统一(一个需要判空后再插入,一个不需要判空直接插入),从程序逻辑性来说不是那么合理(因为结点与结点是平级,添加逻辑理应相同) 如果定义了哨兵结点,以上两个问题都可解决,来看下使用哨兵结点的链表定义 publicclassLinkedList{intlength=0;//链表长度,非必须,可不加Nodehead=newNode(0);//哨兵结点publicvoidaddNode(intval){ Nodetmp=head;while(tmp.next!=null){ tmp=tmp.next; } tmp.next=newNode(val); length++ } } 可以看到,定义了哨兵结点的链表逻辑上清楚了很多,不用每次插入元素都对头结点进行判空,也统一了每一个结点的添加逻辑。 所以之后的习题讲解中我们使用的链表都是使用定义了哨兵结点的形式。 做了这么多前期的准备工作,终于要开始我们的正餐了:链表解题常用套路--翻转! 链表常见解题套路--翻转 热身赛 既然我们要用链表解题,那我们首先就构造一个链表吧 题目:给定数组 1,2,3,4 构造成如下链表 head-->4---->3---->2---->1 看清楚了,是逆序构造链表!顺序构造我们都知道怎么构造,对每个元素持续调用上文代码定义的 addNode 方法即可(即尾插法),与尾插法对应的,是头插法,即把每一个元素插到头节点后面即可,这样就能做到逆序构造链表,如图示(以插入1,2 为例) 头插法比较简单,直接上代码,直接按以上动图的步骤来完成逻辑,如下 publicclassLinkedList{intlength=0;//链表长度,非必须,可不加Nodehead=newNode(0);//哨兵节点 小试牛刀 现在我们加大一下难度,来看下曾经的 Google 面试题: 给定单向链表的头指针和一个节点指针,定义一个函数在 O(1) 内删除这个节点。 如图示:即给定值为 2 的结点,如何把 2 给删了。 我们知道,如果给定一个结点要删除它的后继结点是很简单的,只要把这个结点的指针指向后继结点的后继结点即可 如图示:给定结点 2,删除它的后继结点 3, 把结点 2 的 next 指针指向 3 的后继结点 4 即可。 但给定结点 2,该怎么删除结点 2 本身呢,注意题目没有规定说不能改变结点中的值,所以有一种很巧妙的方法,狸猫换太子!我们先通过结点 2 找到结点 3,再把节点 3 的值赋给结点 2,此时结点 2 的值变成了 3,这时候问题就转化成了上图这种比较简单的需求,即根据结点 2 把结点 3 移除即可,看图 不过需要注意的是这种解题技巧只适用于被删除的指定结点是中间结点的情况,如果指定结点是尾结点,还是要老老实实地找到尾结点的前继结点,再把尾结点删除,代码如下 /** *删除指定的结点 *@paramdeletedNode */publicvoidremoveSelectedNode(NodedeletedNode){//如果此结点是尾结点我们还是要从头遍历到尾结点的前继结点,再将尾结点删除if(deletedNode.next==null){ Nodetmp=head;while(tmp.next!=deletedNode){ tmp=tmp.next; }//找到尾结点的前继结点,把尾结点删除tmp.next=null; }else{ NodenextNode=deletedNode.next;//将删除结点的后继结点的值赋给被删除结点deletedNode.data=nextNode.data;//将nextNode结点删除deletedNode.next=nextNode.next; nextNode.next=null; } } 入门到进阶:链表翻转 接下来我们会重点看一下链表的翻转,链表的翻转可以衍生出很多的变形,是面试中非常热门的考点,基本上考链表必考翻转!所以掌握链表的翻转是必修课! 什么是链表的翻转:给定链表 head-->4--->3-->2-->1,将其翻转成 head-->1-->2-->3-->4 ,由于翻转链表是如此常见,如此重要,所以我们分别详细讲解下如何用递归和非递归这两种方式来解题 递归翻转 关于递归的文章之前写了三篇,如果之前没读过的,强烈建议点击这里查看,总结了递归的常见解题套路,给出了递归解题的常见四步曲,如果看完对以下递归的解题套路会更加深刻,这里不做赘述了,我们直接套递归的解题思路: 首先我们要查看翻转链表是否符合递归规律:问题可以分解成具有相同解决思路的子问题,子子问题...,直到最终的子问题再也无法分解。 要翻转 head--->4--->3-->2-->1 链表,不考虑 head 结点,分析 4--->3-->2-->1,仔细观察我们发现只要先把 3-->2-->1 翻转成 3<----2<----1,之后再把 3 指向 4 即可(如下图示) 图:翻转链表主要三步骤 只要按以上步骤定义好这个翻转函数的功能即可, 这样由于子问题与最初的问题具有相同的解决思路,拆分后的子问题持续调用这个翻转函数即可达到目的。 注意看上面的步骤1,问题的规模是不是缩小了(如下图),从翻转整个链表变成了只翻转部分链表!问题与子问题都是从某个结点开始翻转,具有相同的解决思路,另外当缩小到只翻转一个结点时,显然是终止条件,符合递归的条件!之后的翻转 3-->2-->1, 2-->1 持续调用这个定义好的递归函数即可! 既然符合递归的条件,那我们就可以套用递归四步曲来解题了(注意翻转之后 head 的后继节点变了,需要重新设置!别忘了这一步) 1、定义递归函数,明确函数的功能 根据以上分析,这个递归函数的功能显然是翻转某个节点开始的链表,然后返回新的头结点 /** *翻转结点node开始的链表 */publicNodeinvertLinkedList(Nodenode){ } 2、寻找递推公式 上文中已经详细画出了翻转链表的步骤,简单总结一下递推步骤如下 针对结点 node (值为 4), 先翻转 node 之后的结点 invert(node->next) ,翻转之后 4--->3--->2--->1 变成了 4--->3<---2<---1 再把 node 节点的下个节点(3)指向 node,node 的后继节点设置为空(避免形成环),此时变成了 4<---3<---2<---1 返回新的头结点,因为此时新的头节点从原来的 4 变成了 1,需要重新设置一下 head 3、将递推公式代入第一步定义好的函数中,如下 (invertLinkedList) /** *递归翻转结点node开始的链表 */publicNodeinvertLinkedList(Nodenode){if(node.next==null){returnnode; } 画外音:翻转后由于 head 的后继结点变了,别忘了重新设置哦! 4、计算时间/空间复杂度 由于递归调用了 n 次 invertLinkedList 函数,所以时间复杂度显然是 O(n), 空间复杂度呢,没有用到额外的空间,但是由于递归调用了 n 次 invertLinkedList 函数,压了 n 次栈,所以空间复杂度也是 O(n)。 递归一定要从函数的功能去理解,从函数的功能看,定义的递归函数清晰易懂,定义好了之后,由于问题与被拆分的子问题具有相同的解决思路,所以子问题只要持续调用定义好的功能函数即可,切勿层层展开子问题,此乃递归常见的陷阱!仔细看函数的功能,其实就是按照下图实现的。(对照着代码看,是不是清晰易懂^_^) 非递归翻转链表(迭代解法) 我们知道递归比较容易造成栈溢出,所以如果有其他时间/空间复杂度相近或更好的算法,应该优先选择非递归的解法,那我们看看如何用迭代来翻转链表,主要思路如下 步骤 1: 定义两个节点:pre, cur ,其中 cur 是 pre 的后继结点,如果是首次定义, 需要把 pre 指向 cur 的指针去掉,否则由于之后链表翻转,cur 会指向 pre, 就进行了一个环(如下),这一点需要注意 步骤2:知道了 cur 和 pre,翻转就容易了,把 cur 指向 pre 即可,之后把 cur 设置为 pre ,cur 的后继结点设置为 cur 一直往前重复此步骤即可,完整动图如下 注意:同递归翻转一样,迭代翻转完了之后 head 的后继结点从 4 变成了 1,记得重新设置一下。 知道了解题思路,实现代码就容易多了,直接上代码 /** *迭代翻转 */publicvoiditerationInvertLinkedList(){//步骤1Nodepre=head.next; Nodecur=pre.next; pre.next=null; 用迭代的思路来做由于循环了 n 次,显然时间复杂度为 O(n),另外由于没有额外的空间使用,也未像递归那样调用递归函数不断压栈,所以空间复杂度是 O(1),对比递归,显然应该使用迭代的方式来处理! 花了这么大的精力我们总算把翻转链表给搞懂了,如果大家看了之后几道翻转链表的变形,会发现我们花了这么大篇幅讲解翻转链表是值得的。 接下来我们来看看链表翻转的变形 变形题 1: 给定一个链表的头结点 head,以及两个整数 from 和 to ,在链表上把第 from 个节点和第 to 个节点这一部分进行翻转。 例如:给定如下链表,from = 2, to = 4 head-->5-->4-->3-->2-->1 将其翻转后,链表变成 head-->5--->2-->3-->4-->1 有了之前翻转整个链表的解题思路,现在要翻转部分链表就相对简单多了,主要步骤如下: 根据 from 和 to 找到 from-1, from, to, to+1 四个结点(注意临界条件,如果 from 从头结点开始,则 from-1 结点为空, 翻转后需要把 to 设置为头结点的后继结点, from 和 to 结点也可能超过尾结点,这两种情况不符合条件不翻转)。 对 from 到 to 的结点进行翻转 将 from-1 节点指向 to 结点,将 from 结点指向 to + 1 结点 知道了以上的思路,代码就简单了,按上面的步骤1,2,3 实现,注释也写得很详细,看以下代码(对 from 到 to 结点的翻转我们使用迭代翻转,当然使用递归也是可以的,限于篇幅关系不展开,大家可以尝试一下)。 /** *迭代翻转from到to的结点 */publicvoiditerationInvertLinkedList(intfromIndex,inttoIndex)throwsException{ 变形题 2: 给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么将最后剩余节点保持原有顺序。 示例 : 给定这个链表:head-->1->2->3->4->5 当 k = 2 时,应当返回: head-->2->1->4->3->5 当 k = 3 时,应当返回: head-->3->2->1->4->5 说明 : 你的算法只能使用常数的额外空间。 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 这道题是 LeetCode 的原题,属于 hard 级别,如果这一题你懂了,那对链表的翻转应该基本没问题了,有了之前的翻转链表基础,相信这题不难。 只要我们能找到翻一组 k 个结点的方法,问题就解决了(之后只要重复对 k 个结点一组的链表进行翻转即可)。 接下来,我们以以下链表为例来看看怎么翻转 3 个一组的链表(此例中 k = 3) 首先,我们要记录 3 个一组这一段链表的前继结点,定义为 startKPre,然后再定义一个 step, 从这一段的头结点 (1)开始遍历 2 次,找出这段链表的起始和终止结点,如下图示 找到 startK 和 endK 之后,根据之前的迭代翻转法对 startK 和 endK 的这段链表进行翻转 然后将 startKPre 指向 endK,将 startK 指向 endKNext,即完成了对 k 个一组结点的翻转。 知道了一组 k 个怎么翻转,之后只要重复对 k 个结点一组的链表进行翻转即可,对照图示看如下代码应该还是比较容易理解的 /** *每k个一组翻转链表 *@paramk */publicvoiditerationInvertLinkedListEveryK(intk){ Nodetmp=head.next;intstep=0;//计数,用来找出首结点和尾结点 时间复杂度是多少呢,对链表从头到尾循环了 n 次,同时每 k 个结点翻转一次,可以认为总共翻转了 n 次,所以时间复杂度是O(2n),去掉常数项,即为 O(n)。 注:这题时间复杂度比较误认为是O(k * n),实际上并不是每一次链表的循环都会翻转链表,只是在循环链表元素每 k 个结点的时候才会翻转 变形3: 变形 2 针对的是顺序的 k 个一组翻转,那如何逆序 k 个一组进行翻转呢 例如:给定如下链表, head-->1-->2-->3-->4-->5 逆序 k 个一组翻转后,链表变成(k = 2 时) head-->1--->3-->2-->5-->4 这道题是字节跳动的面试题,确实够变态的,顺序 k 个一组翻转都已经属于 hard 级别了,逆序 k 个一组翻转更是属于 super hard 级别了,不过其实有了之前知识的铺垫,应该不难,只是稍微变形了一下,只要对链表做如下变形即可 代码的每一步其实都是用了我们之前实现好的函数,所以我们之前做的每一步都是有伏笔的哦!就是为了解决字节跳动这道终极面试题! /** *逆序每k个一组翻转链表 *@paramk */publicvoidreverseIterationInvertLinkedListEveryK(intk){//先翻转链表iterationInvertLinkedList();//k个一组翻转链表iterationInvertLinkedListEveryK(k);//再次翻转链表iterationInvertLinkedList(); } 由此可见,掌握基本的链表翻转非常重要!难题多是在此基础了做了相应的变形而已 链表解题利器---快慢指针 快慢指针在面试中出现的概率也很大,也是务必要掌握的一个要点,本文总结了市面上常见的快慢指针解题技巧,相信看完后此类问题能手到擒来。本文将详细讲述如何用快慢指针解决以下两大类问题 寻找/删除第 K 个结点 有关链表环问题的相关解法 寻找/删除第 K 个结点 小试牛刀之一 LeetCode 876:给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。 解法一 要知道链表的中间结点,首先我们需要知道链表的长度,说到链表长度大家想到了啥,还记得我们在上文中说过哨兵结点可以保存链表的长度吗,这样直接 从 head 的后继结点 开始遍历 链表长度 / 2 次即可找到中间结点。为啥中间结点是 链表长度/2,我们仔细分析一下 假如链表长度是奇数: head--->1--->2--->3--->4--->5, 从 1 开始遍历 5/2 = 2 (取整)次,到达 3,3确实是中间结点 假如链表长度是偶数: head--->1--->2--->3--->4--->5--->6, 从 1 开始遍历 6/2 = 3次,到达 4,4 确实是中间结点的第二个结点 画外音:多画画图,举举例,能看清事情的本质! 哨后结点的长度派上用场了,这种方式最简单,直接上代码 publicNodefindMiddleNode(){ Nodetmp=head.next;intmiddleLength=length/2;while(middleLength>0){ tmp=tmp.next; middleLength--; }returntmp; } 解法二 如果哨兵结点里没有定义长度呢,那就要遍历一遍链表拿到链表长度(定义为 length)了,然后再从头结点开始遍历 length / 2 次即为中间结点 publicNodefindMiddleNodeWithoutHead(){ Nodetmp=head.next;intlength=1;//选遍历一遍拿到链表长度while(tmp.next!=null){ tmp=tmp.next; length++; } 解法三 解法二由于要遍历两次链表,显得不是那么高效,那能否只遍历一次链表就能拿到中间结点呢。 这里就引入我们的快慢指针了,主要有三步 1、 快慢指针同时指向 head 的后继结点 2、 慢指针走一步,快指针走两步 3、 不断地重复步骤2,什么时候停下来呢,这取决于链表的长度是奇数还是偶数 如果链表长度为奇数,当 fast.next = null 时,slow 为中间结点 如果链表长度为偶数,当 fast = null 时,slow 为中间结点由以上分析可知:当 fast = null 或者 fast.next = null 时,此时的 slow 结点即为我们要求的中间结点,否则不断地重复步骤 2, 知道了思路,代码实现就简单了 /** *使用快慢指针查找找到中间结点 *@return */publicNodefindMiddleNodeWithSlowFastPointer(){ Nodeslow=head.next; Nodefast=head.next;while(fast!=null&&fast.next!=null){//快指针走两步fast=fast.next.next;//慢指针走一步slow=slow.next; }//此时的slow结点即为哨兵结点returnslow; } 有了上面的基础,我们现在再大一下难度,看下下面这道题 输入一个链表,输出该链表中的倒数第 k 个结点。比如链表为 head-->1-->2-->3-->4-->5。求倒数第三个结点(即值为 3 的节点) 分析:我们知道如果要求顺序的第 k 个结点还是比较简单的,从 head 开始遍历 k 次即可,如果要求逆序的第 k 个结点,常规的做法是先顺序遍历一遍链表,拿到链表长度,然后再遍历 链表长度-k 次即可,这样要遍历两次链表,不是那么高效,如何只遍历一次呢,还是用我们的说的快慢指针解法 首先让快慢指针同时指向 head 的后继结点 快指针往前走 k- 1 步,先走到第 k 个结点 快慢指针同时往后走一步,不断重复此步骤,直到快指针走到尾结点,此时的 slow 结点即为我们要找的倒序第 k 个结点 注:需要注意临界情况:k 大于链表的长度,这种异常情况应该抛异常 publicNodefindKthToTail(intk)throwsException{ Nodeslow=head.next; Nodefast=head.next; 知道了如何求倒序第 k 个结点,再来看看下面这道题 给定一个单链表,设计一个算法实现链表向右旋转 K 个位置。举例: 给定 head->1->2->3->4->5->NULL, K=3,右旋后即为 head->3->4->5-->1->2->NULL 分析:这道题其实是对求倒序第 K 个位置的的一个变形,主要思路如下 先找到倒数第 K+1 个结点, 此结点的后继结点即为倒数第 K 个结点 将倒数第 K+1 结点的的后继结点设置为 null 将 head 的后继结点设置为以上所得的倒数第 K 个结点,将原尾结点的后继结点设置为原 head 的后继结点 publicvoidreversedKthToTail(intk)throwsException{//直接调已实现的寻找倒序k个结点的方法,这里是k+1NodeKPreNode=findKthToTail(k+1);//倒数第K个结点NodekNode=KPreNode.next; NodeheadNext=head.next; 有了上面两道题的铺垫,相信下面这道题不是什么难事,限于篇幅关系,这里不展开,大家可以自己试试 输入一个链表,删除该链表中的倒数第 k 个结点 小试牛刀之二 判断两个单链表是否相交及找到第一个交点,要求空间复杂度 O(1)。 如图示:如果两个链表相交,5为这两个链表相交的第一个交点 画外音:如果没有空间复杂度O(1)的限制,其实有多种解法,一种是遍历链表 1,将链表 1 的所有的结点都放到一个 set 中,再次遍历链表 2,每遍历一个结点,就判断这个结点是否在 set,如果发现结点在这个 set 中,则这个结点就是链表第一个相交的结点 分析:首先我们要明白,由于链表本身的性质,如果有一个结点相交,那么相交结点之后的所有结点都是这两个链表共用的,也就是说两个链表的长度主要相差在相交结点之前的结点长度,于是我们有以下思路 1、如果链表没有定义长度,则我们先遍历这两个链表拿到两个链表长度,假设分别为 L1, L2 (L1 >= L2), 定义 p1, p2 指针分别指向各自链表 head 结点,然后 p1 先往前走 L1 - L2 步。这一步保证了 p1,p2 指向的指针与相交结点(如果有的话)一样近。 2、 然后 p1,p2 不断往后遍历,每次走一步,边遍历边判断相应结点是否相等,如果相等即为这两个链表的相交结点 publicstaticNodedetectCommonNode(LinkedListlist1,LinkedListlist2){intlength1=0;//链表list1的长度intlength2=0;//链表list2的长度 进阶 接下来我们来看如何用快慢指针来判断链表是否有环,这是快慢指针最常见的用法 判断链表是否有环,如果有,找到环的入口位置(下图中的 2),要求空间复杂度为O(1) 首先我们要看如果链表有环有什么规律,如果从 head 结点开始遍历,则这个遍历指针一定会在以上的环中绕圈子,所以我们可以分别定义快慢指针,慢指针走一步,快指针走两步, 由于最后快慢指针在遍历过程中一直会在圈中里绕,且快慢指针每次的遍历步长不一样,所以它们在里面不断绕圈子的过程一定会相遇,就像 5000 米长跑,一人跑的快,一人快的慢,跑得快的人一定会追上跑得慢的(即套圈)。 还不明白?那我们简单证明一下 1、 假如快指针离慢指针相差一个结点,则再一次遍历,慢指针走一步,快指针走两步,相遇 2、 假如快指针离慢指针相差两个结点,则再一次遍历,慢指针走一步,快指针走两步,相差一个结点,转成上述 1 的情况 3、 假如快指针离慢指针相差 N 个结点(N大于2),则下一次遍历由于慢指针走一步,快指针走两步,所以相差 N+1-2 = N-1 个结点,发现了吗,相差的结点从 N 变成了 N-1,缩小了!不断地遍历,相差的结点会不断地缩小,当 N 缩小为 2 时,即转为上述步骤 2 的情况,由此得证,如果有环,快慢指针一定会相遇! **画外音:如果慢指针走一步,快指针走的不是两步,而是大于两步,会有什么问题,大家可以考虑一下 ** /** *判断是否有环,返回快慢指针相遇结点,否则返回空指针 */publicNodedetectCrossNode(){ Nodeslow=head; Nodefast=head; 判断有环为啥要返回相遇的结点,而不是返回 true 或 false 呢。 因为题目中还有一个要求,判断环的入口位置,就是为了这个做铺垫的,一起来看看怎么找环的入口,需要一些分析的技巧 假设上图中的 7 为快慢指针相遇的结点,不难看出慢指针走了 L + S 步,快指针走得比慢指针更快,它除了走了 L + S 步外,还额外在环里绕了 n 圈,所以快指针走了 L+S+nR 步(R为图中环的长度),另外我们知道每遍历一次,慢指针走了一步,快指针走了两步,所以快指针走的路程是慢指针的两倍,即 2 (L+S) = L+S+nR,即 L+S = nR 当 n = 1 时,则 L+S = R 时,则从相遇点 7 开始遍历走到环入口点 2 的距离为 R - S = L,刚好是环的入口结点,而 head 与环入口点 2 的距离恰好也为 L,所以只要在头结点定义一个指针,在相遇点(7)定义另外一个指针,两个指针同时遍历,每次走一步,必然在环的入口位置 2 相遇 当 n > 1 时,L + S = nR,即 L = nR - S, nR-S 怎么理解?可以看作是指针从结点 7 出发,走了 n 圈后,回退 S 步,此时刚好指向环入口位置,也就是说如果设置一个指针指向 head(定义为p1), 另设一个指针指向 7(定义为p2),不断遍历,p2 走了 nR-S 时(即环的入口位置),p1也刚好走到这里(此时 p1 走了 nR-S = L步,刚好是环入口位置),即两者相遇! 综上所述,要找到入口结点,只需定义两个指针,一个指针指向head, 一个指针指向快慢指向的相遇点,然后这两个指针不断遍历(同时走一步),当它们指向同一个结点时即是环的入口结点 publicNodegetRingEntryNode(){//获取快慢指针相遇结点NodecrossNode=detectCrossNode();<spanclass="hljs-comment"style="color:#998;font-style:italic;line-height:26px;">//如果没有相遇点,则没有环</span> <spanclass="hljs-keyword"style="color:#333;font-weight:bold;line-height:26px;">if</span>(crossNode==<spanclass="hljs-keyword"style="color:#333;font-weight:bold;line-height:26px;">null</span>){ <spanclass="hljs-keyword"style="color:#333;font-weight:bold;line-height:26px;">return</span><spanclass="hljs-keyword"style="color:#333;font-weight:bold;line-height:26px;">null</span>; } <spanclass="hljs-comment"style="color:#998;font-style:italic;line-height:26px;">//分别定义两个指针,一个指向头结点,一个指向相交结点</span> Nodetmp1=head; Nodetmp2=crossNode; <spanclass="hljs-comment"style="color:#998;font-style:italic;line-height:26px;">//两者相遇点即为环的入口结点</span> <spanclass="hljs-keyword"style="color:#333;font-weight:bold;line-height:26px;">while</span>(tmp1.data!=tmp2.data){ tmp1=tmp1.next; tmp2=tmp2.next; } <spanclass="hljs-keyword"style="color:#333;font-weight:bold;line-height:26px;">return</span>tmp1; } 思考题:知道了环的入口结点,怎么求环的长度? 总结 本文详细讲解了链表与数组的本质区别,相信大家对两者的区别应该有了比较深刻的认识,尤其是程序局部性原理,相信大家看了应该会眼前一亮,之后通过对链表的翻转由浅入深地介绍,相信之后的链表翻转对大家应该不是什么难事了,之后再介绍了链表的另一个重要的解题技巧:快慢指针,这两大类是面试的高频题,大家一定要掌握!建议大家亲自实现一遍文中的代码哦,这样印象会更深刻一些!有一些看起来思路是这么一回事,但真正操作起来还是会有不少坑,纸上得来终觉浅,绝知此事要躬行!

优秀的个人博客,低调大师

超简单使用redisson延迟队列做定时任务

前言:啰嗦的就不写了,就是传一个多久之后执行,然后到点就执行的任务调度,我之前写过一次基于quartz的定时任务,前端和分布式还需要完善https://my.oschina.net/wangnian/blog/758054 ,这个直接是基于时间的,其实多久之后和啥时候执行都一会事。 今天是我疫情复工的第一天上班,有很多小伙伴都已经复工了,戴了半天的口罩,很难受,准备下班之后把耳戴式改造成头戴式。 这个没有啥理论知识,就是对于redisson的delayedQueue延迟队列的封装,接下来直接粘贴代码,有需要的直接粘贴过去去,没需要的可以看看我的代码指指毛病 1.导入redisson包 注意,如果是Springboot的项目强制使用,别单独引入,再自己配置config <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.10.5</version> </dependency> 2.配置SpringBootRedis标准配置 spring.redis.host=192.168.1.100 spring.redis.port=6379 3.唯一一个封装的工具类 package com.test.redis.demo; import org.redisson.api.RBlockingQueue; import org.redisson.api.RDelayedQueue; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class RedisDelayedQueue { /** * 任务回调监听 * * @param <T> */ public abstract static class TaskEventListener<T> { /** * 执行方法 * * @param t */ public abstract void invoke(T t); } @Autowired RedissonClient redissonClient; /** * 添加队列 * * @param t DTO传输类 * @param delay 时间数量 * @param timeUnit 时间单位 * @param <T> 泛型 */ public <T> void addQueue(T t, long delay, TimeUnit timeUnit) { RBlockingQueue<T> blockingFairQueue = redissonClient.getBlockingQueue(t.getClass().getName()); RDelayedQueue<T> delayedQueue = redissonClient.getDelayedQueue(blockingFairQueue); delayedQueue.offer(t, delay, timeUnit); delayedQueue.destroy(); } /** * 获取队列 * * @param zClass DTO泛型 * @param taskEventListener 任务回调监听 * @param <T> 泛型 * @return */ public <T> void getQueue(Class zClass, TaskEventListener taskEventListener) { RBlockingQueue<T> blockingFairQueue = redissonClient.getBlockingQueue(zClass.getName()); //由于此线程需要常驻,可以新建线程,不用交给线程池管理 ((Runnable) () -> { while (true) { try { T t = blockingFairQueue.take(); taskEventListener.invoke(t); } catch (InterruptedException e) { e.printStackTrace(); } } }).run(); } } 4.运行并测试 @Override public void run(String... args) throws Exception { //添加需要延迟的DTO TaskBodyDTO taskBody = new TaskBodyDTO(); taskBody.setBody("测试DTO实体类的BODY,3秒之后执行"); taskBody.setName("测试DTO实体类的姓名,3秒之后执行"); //添加队列3秒之后执行 redisDelayedQueue.addQueue(taskBody, 3, TimeUnit.SECONDS); taskBody.setBody("测试DTO实体类的BODY,10秒之后执行"); taskBody.setName("测试DTO实体类的姓名,10秒之后执行"); //添加队列10秒之后执行 redisDelayedQueue.addQueue(taskBody, 10, TimeUnit.SECONDS); taskBody.setBody("测试DTO实体类的BODY,20秒之后执行"); taskBody.setName("测试DTO实体类的姓名,20秒之后执行"); //添加队列20秒之后执行 redisDelayedQueue.addQueue(taskBody, 20, TimeUnit.SECONDS); //监听延迟队列 RedisDelayedQueue.TaskEventListener<TaskBodyDTO> taskEventListener = new RedisDelayedQueue.TaskEventListener<TaskBodyDTO>() { @Override public void invoke(TaskBodyDTO taskBodyDTO) { //这里调用你延迟之后的代码 logger.info("执行...." + taskBodyDTO.getBody() + "===" + taskBodyDTO.getName()); } }; redisDelayedQueue.getQueue(TaskBodyDTO.class, taskEventListener); } 打印的值 2020-02-17 13:45:08.371 INFO 14700 --- [ main] com.test.redis.demo.DemoApplication : 执行....测试DTO实体类的BODY,3秒之后执行===测试DTO实体类的姓名,3秒之后执行 2020-02-17 13:45:15.467 INFO 14700 --- [ main] com.test.redis.demo.DemoApplication : 执行....测试DTO实体类的BODY,10秒之后执行===测试DTO实体类的姓名,10秒之后执行 2020-02-17 13:45:25.463 INFO 14700 --- [ main] com.test.redis.demo.DemoApplication : 执行....测试DTO实体类的BODY,20秒之后执行===测试DTO实体类的姓名,20秒之后执行 结束: 使用是不是很简单,我这里是通过DTO的名字来区分队列名的,所以每一个定时任务的DTO都不要相同。 大家对于定时任务有其他好用的方式,或者开源的好项目,欢迎下方留言评论,我将在评论区选出100位中奖用户送出iPhone5. 哎呀,不对 我没有粉丝,等我粉丝到10000了再选中奖用户吧,哈哈

优秀的个人博客,低调大师

超详细Dkhadoop虚拟机图文安装步骤

前两天看到有人留言问在什么情况下需要部署hadoop,我给的回答也很简单,就是在需要处理海量数据的时候才需要考虑部署hadoop。关于这个问题在很早之前的一篇分享文档也有说到这个问题,数据量少的完全发挥不了hadoop的优势,所以也没必要部署。但对于正在学习hadoop的朋友来说,hadoop运行环境部署真的是件非常头疼的事情。 计划在接下来的一段时间里,以我之前学习研究大快搜索DKHadoop时经验为基础,给大家整理分享虚拟机的安装、操作系统安装、服务器操作系统配置、DKH系统安装等相关内容。文章篇幅可能会比较长,所以每篇就只分享一到两个点。本篇给大家分享DKHadoop虚拟机的环境安装。 虚拟机的安装 1、下载的链接地址可参考: https://download3.vmware.com/software/wkst/file/VMware-workstation-full-12.5.7-5813279.exe 2、 详细安装步骤: (1)在配置类型选项中,我们选择自定义(高级),选择好之后点击“下一步” 图1 (2)虚拟机的硬件兼容性设置页面,参考下图设置好之后点击“下一步”; 图2 (3)在安装客户机操作系统界面我们选择最后一个“稍后安装操作系统”(见图3-1),然后点击下一步,在客户机操作系统中选择“Linux”(见图3-2)。 图3-1 图3-2 (4)为虚拟机命名,如下图4-1,(命名按照自己喜欢即可),点击“下一步”进行处理器配置。见图4-2 图4-1 图4-2 (5)设置虚拟机的内存,见下图 图5 (6)网络连接类型有四种,在四种网络连接类型中选择“使用桥连接网络”,点击“下一步”; 图6 (7) SCSI控制器类型选择LSI Logic 的推荐类型即可; 图7 (8)磁盘类型同样选择推荐的SCSI; 图8 (9)选择创建新虚拟磁盘(如图9-1),点击下一步设置磁盘容量,详细见图9-2 图9-1 图9-2 (10)设置磁盘存储位置 图10 (11)按照上述步骤完成设置后,单击下图中的“完成”即可,至此虚拟机安装成功。 图11

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册