diff --git a/README.md b/README.md index 8e5becc..1c7180e 100644 --- a/README.md +++ b/README.md @@ -10,37 +10,24 @@ 👉 Java学习资源汇总(个人总结) -- Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://github.com/hello-go-maker/cs-learn-source +- **Java基础到Java实战全套学习视频教程,包括多个企业级实战项目** -- 面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂:https://urlify.cn/N7vIj2 密码: ijoi +- **面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂** -- 大厂面试资料,一年时间总结,覆盖Java所有技术点:https://urlify.cn/Vzmeqy 密码: j9t2 +- **大厂面试资料,一年时间总结,覆盖Java所有技术点** -- 面试思维导图,手打总结: https://urlify.cn/vUNF7z 密码: adbo +- **面试思维导图,手打总结** -👉 Java各种电子书:如果你需要各种电子书,可以移步这个仓库 [Java电子书合集](https://github.com/hello-go-maker/cs-books) +👉 **Java各种电子书:各种技术相关的电子书** -👉 Java面试思维导图(手打) +👉 **Java面试思维导图(手打)**,我靠这些导图拿到了一线互联网公司的offer,关注公众号,回复:`思维导图`; -👉 这里再分享一些我总结的**Java面试思维导图**,我靠这些导图拿到了一线互联网公司的offer,预览在下方,先来瞧瞧。 -![](http://image.ouyangsihai.cn/FtJ3PbBRdNSa1NaUr96JmUq24_yS) +**划重点**:获取上面的资源,请关注我的公众号 `程序员的技术圈子`,**微信扫描下面二维码**,回复:`Java资料`,获取思维导图,绿色通道关注福利,等你拿。 +
-**划重点**:更多`Java面试思维导图`,请关注我的公众号 **程序员的技术圈子**,`微信扫描下面二维码`,回复:**思维导图**,获取思维导图,绿色通道关注福利,等你拿。 -![](http://image.ouyangsihai.cn/FuRA5sT9JUaVbx-YD-Acor04AWhF) - - - - -
-
- -
- -[![微信群](https://camo.githubusercontent.com/59d7f19ba1af85247e016858a63045f8fe9a8c19/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7765436861742de5beaee4bfa1e7bea42d626c75652e737667)](https://github.com/OUYANGSIHAI/JavaInterview#%E8%81%94%E7%B3%BB%E6%88%91) [![公众号](https://img.shields.io/badge/%E5%85%AC%E4%BC%97%E5%8F%B7-%E5%A5%BD%E5%A5%BD%E5%AD%A6java-orange)](https://github.com/OUYANGSIHAI/JavaInterview#%E5%85%AC%E4%BC%97%E5%8F%B7) [![公众号](https://camo.githubusercontent.com/6d206aa03f27a851cf994123ef7be1a8d3192d54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a75656a696e2de68e98e987912d626c75652e737667)](https://juejin.im/user/5a672822f265da3e55380f0b) [![投稿](https://camo.githubusercontent.com/85a04ac4953a80940570b5c86ce73a1d34ff1542/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6373646e2d4353444e2d7265642e737667)](https://blog.csdn.net/sihai12345) [![投稿](https://camo.githubusercontent.com/6efc9c83ef8e85b19ce2853b5f69d68255f0c037/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f62696c6962696c692de59394e593a9e59394e593a92d637269746963616c)](https://space.bilibili.com/441147490) -
### 目录(ctrl + f 查找更香:不能点击的,还在写) @@ -155,51 +142,52 @@ #### 基础容器 -- ArrayList源码分析及真实大厂面试题精讲 -- LinkedList源码分析及真实大厂面试题精讲 -- HashMap源码分析及真实大厂面试题精讲 +- [ArrayList源码分析及真实大厂面试题精讲](https://blog.csdn.net/sihai12345/article/details/138413307?spm=1001.2014.3001.5501) +- [LinkedList源码分析及真实大厂面试题精讲](https://blog.csdn.net/sihai12345/article/details/138413722?spm=1001.2014.3001.5501) +- [HashMap源码分析及真实大厂面试题精讲](https://blog.csdn.net/sihai12345/article/details/138416578?spm=1001.2014.3001.5501) - TreeMap源码分析及真实大厂面试题精讲 - TreeSet源码分析及真实大厂面试题精讲 - LinkedHashMap源码分析及真实大厂面试题精讲 #### 阻塞容器 -- ConcurrentHashMap源码分析及真实大厂面试题精讲 +- [ConcurrentHashMap源码分析及真实大厂面试题精讲](https://blog.csdn.net/sihai12345/article/details/138420403) - ArrayBlockingQueue源码分析及真实大厂面试题精讲 - LinkedBlockingQueue源码分析及真实大厂面试题精讲 - PriorityBlockingQueue源码分析及真实大厂面试题精讲 ### 并发 -- Synchronized关键字精讲及真实大厂面试题解析 -- Volitale关键字精讲及真实大厂面试题解析 +- [Synchronized关键字精讲及真实大厂面试题解析](https://blog.csdn.net/sihai12345/article/details/138420474) +- [Volitale关键字精讲及真实大厂面试题解析](https://blog.csdn.net/sihai12345/article/details/138420521) - 关于LRU的实现 -- ThreadLocal面试中会怎么提问呢? -- 线程池的面试题,这篇文章帮你搞定它! +- [ThreadLocal面试中会怎么提问呢?](https://blog.csdn.net/sihai12345/article/details/138420558) +- [线程池的面试题,这篇文章帮你搞定它!](https://blog.csdn.net/sihai12345/article/details/138420591) ### JVM - [深入理解Java虚拟机系列](https://mp.weixin.qq.com/s/SZ87s3fmKL3Kc_tAMcOFQw) - [深入理解Java虚拟机系列--完全解决面试问题](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-xi-lie-jiao-cheng.html) -- [深入理解Java虚拟机-Java内存区域透彻分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-java-nei-cun-qu-yu-tou-che-fen-xi.html) -- [深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-jvm-nei-cun-fen-pei-yu-hui-shou-ce-lue-yuan-li-cong-ci-gao-bie-jvm-nei-cun-fen-pei-wen-mang.html) -- [深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html) -- [深入理解Java虚拟机-如何利用JDK自带的命令行工具监控上百万的高并发的虚拟机性能](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-jdk-zi-dai-de-ming-ling-xing-gong-ju-jian-kong-shang-bai-wan-de-gao-bing-fa-de-xu-ni-ji-xing-neng.html) -- [深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html) -- [深入理解Java虚拟机-你了解GC算法原理吗](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ni-liao-jie-gc-suan-fa-yuan-li-ma.html) -- [几个面试官常问的垃圾回收器,下次面试就拿这篇文章怼回去!](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-jian-de-la-ji-hui-shou-qi.html) -- [面试官100%会严刑拷打的 CMS 垃圾回收器,下次面试就拿这篇文章怼回去!](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-cms-la-ji-hui-shou-qi.html) +- [深入理解Java虚拟机-Java内存区域透彻分析](https://mp.weixin.qq.com/s/WuyxyelaXbU-lg-HVZ95TA) +- [深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲](https://mp.weixin.qq.com/s/IG_zU5xa7y4BB6PVP0Fmow) +- [深入理解Java虚拟机-常用vm参数分析](https://mp.weixin.qq.com/s/l8fsq07jI0svqBdBGxuOzA) +- [深入理解Java虚拟机-如何利用JDK自带的命令行工具监控上百万的高并发的虚拟机性能](https://mp.weixin.qq.com/s/wPgA5SDURCAqPsWkZGGX0g) +- [深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析](https://mp.weixin.qq.com/s/hhA9tI_rYNkJVbF-R45hbA) +- [深入理解Java虚拟机-你了解GC算法原理吗](https://mp.weixin.qq.com/s/SZ87s3fmKL3Kc_tAMcOFQw) +- [几个面试官常问的垃圾回收器,下次面试就拿这篇文章怼回去!](https://sihai.blog.csdn.net/article/details/105700527) +- [面试官100%会严刑拷打的 CMS 垃圾回收器,下次面试就拿这篇文章怼回去!](https://sihai.blog.csdn.net/article/details/105808878) - [JVM 面试题 87 题详解](https://sihai.blog.csdn.net/article/details/118737581) ### Java8 -- [Java8快速学习教程](https://blog.ouyangsihai.cn/java8-zui-xin-jiao-cheng-bu-yi-yang-de-java8.html) -- [Java11的最新特性](https://blog.ouyangsihai.cn/java11-zheng-shi-fa-bu-liao-wo-men-gai-zen-me-ban.html) -- [Java8 之 lambda 表达式、方法引用、函数式接口、默认方式、静态方法](https://blog.ouyangsihai.cn/java8-zhi-lambda-biao-da-shi-fang-fa-yin-yong-han-shu-shi-jie-kou-mo-ren-fang-shi-jing-tai-fang-fa.html) -- [Java8之Consumer、Supplier、Predicate和Function攻略](https://blog.ouyangsihai.cn/java8-zhi-consumer-supplier-predicate-he-function-gong-lue.html) -- [Java8 的 Stream 流式操作之王者归来](https://blog.ouyangsihai.cn/java8-de-stream-liu-shi-cao-zuo-zhi-wang-zhe-gui-lai.html) +- [Java8 Stream:2万字20个实例,玩转集合的筛选、归约、分组、聚合](https://mp.weixin.qq.com/s/u042M2Sw2glBlevIDVoSXg) +- [利用Java8新特征,重构传统设计模式,你学会了吗?](https://mp.weixin.qq.com/s/zZ6rWz_t_snYNiNyOtaGiQ) +- [Java8 之 lambda 表达式、方法引用、函数式接口、默认方式、静态方法](https://mp.weixin.qq.com/s/FdzNWIsEmHVe9Nehxvfa3w) +- [Java8之Consumer、Supplier、Predicate和Function攻略](https://sihai.blog.csdn.net/article/details/98193777) +- [Java8 的 Stream 流式操作之王者归来](https://sihai.blog.csdn.net/article/details/100434684) +- [Java11-17的最新特性](https://mp.weixin.qq.com/s/QPGdNn56mCCDIUS047_1cQ) ## 计算机网络 @@ -215,7 +203,7 @@ ## Linux -- [java工程师linux命令,这篇文章就够了](https://blog.ouyangsihai.cn/java-gong-cheng-shi-linux-ming-ling-zhe-pian-wen-zhang-jiu-gou-liao.html) +- [java工程师linux命令,这篇文章就够了](https://mp.weixin.qq.com/s/bj28tvF9TwgwrH65OPjXZg) - [linux常见面试题(基础版)](https://sihai.blog.csdn.net/article/details/118737736) - [linux高频面试题](docs/operating-system/linux高频面试题.md) - 常问的几个Linux面试题,通通解决它 @@ -224,9 +212,9 @@ ### 数据结构 -- 跳表这种数据结构,你真的清楚吗,面试官可能会问这些问题! +- [跳表这种数据结构,你真的清楚吗,面试官可能会问这些问题!](https://blog.csdn.net/sihai12345/article/details/138419109) - 红黑树你了解多少,不会肯定会被面试官怼坏 -- [B树,B+树,你了解多少,面试官问那些问题?](https://blog.ouyangsihai.cn/mian-shi-guan-wen-ni-b-shu-he-b-shu-jiu-ba-zhe-pian-wen-zhang-diu-gei-ta.html) +- [B树,B+树,你了解多少,面试官问那些问题?](https://segmentfault.com/a/1190000020416577) - [这篇文章带你彻底理解红黑树](https://sihai.blog.csdn.net/article/details/118738496) - 二叉树、二叉搜索树、二叉平衡树、红黑树、B树、B+树 @@ -248,14 +236,12 @@ ### MySQL -- [MySQL深入理解教程-解决面试中的各种问题](https://blog.ouyangsihai.cn/mysql-shen-ru-li-jie-jiao-cheng-mysql-de-yi-zhu-shi-jie.html) -- [InnoDB与MyISAM等存储引擎对比](https://blog.ouyangsihai.cn/innodb-yu-myisam-deng-cun-chu-yin-qing-dui-bi.html) -- [ 面试官问你B树和B+树,就把这篇文章丢给他](https://blog.ouyangsihai.cn/mian-shi-guan-wen-ni-b-shu-he-b-shu-jiu-ba-zhe-pian-wen-zhang-diu-gei-ta.html) -- [MySQL的B+树索引的概念、使用、优化及使用场景](https://blog.ouyangsihai.cn/mysql-de-b-shu-suo-yin.html) -- [ MySQL全文索引最强教程](https://blog.ouyangsihai.cn/mysql-quan-wen-suo-yin.html) -- [ MySQL的又一神器-锁,MySQL面试必备](https://blog.ouyangsihai.cn/mysql-de-you-yi-shen-qi-suo.html) -- [ MySQL事务,这篇文章就够了](https://blog.ouyangsihai.cn/mysql-shi-wu-zhe-pian-wen-zhang-jiu-gou-liao.html) -- [ mysqldump工具命令参数大全](https://blog.ouyangsihai.cn/mysqldump-gong-ju-ming-ling-can-shu-da-quan.html) +- [InnoDB与MyISAM等存储引擎对比](https://sihai.blog.csdn.net/article/details/100832158) +- [MySQL:从B树到B+树到索引再到存储引擎](https://mp.weixin.qq.com/s/QmG1FyWPp23klTVkTJvcUQ) +- [MySQL全文索引最强教程](https://blog.ouyangsihai.cn/mysql-quan-wen-suo-yin.html) +- [MySQL的又一神器-锁,MySQL面试必备](https://sihai.blog.csdn.net/article/details/102680104) +- [MySQL事务,这篇文章就够了](https://sihai.blog.csdn.net/article/details/102815801) +- [mysqldump工具命令参数大全](https://blog.ouyangsihai.cn/mysqldump-gong-ju-ming-ling-can-shu-da-quan.html) - [看完这篇MySQL备份的文章,再也不用担心删库跑路了](https://blog.ouyangsihai.cn/kan-wan-zhe-pian-mysql-bei-fen-de-wen-zhang-zai-ye-bu-yong-dan-xin-shan-ku-pao-lu-liao.html) - 关于MySQL索引,面试中面试官会怎么为难你,一定得注意 - MySQL中的乐观锁、悲观锁,JDK中的乐观锁、悲观锁? @@ -264,7 +250,8 @@ - [MySQL高频面试题](https://mp.weixin.qq.com/s/KFCkvfF84l6Eu43CH_TmXA) - [MySQL查询优化过程](https://mp.weixin.qq.com/s/jtuLb8uAIHJNvNpwcIZfpA) -- MySQL面试官会怎么死怼你呢,我告诉你回怼他 +- [面试官:MySQL 上亿大表,如何深度优化?](https://mp.weixin.qq.com/s/g-_Oz9CLJfBn_asJrzn6Yg) +- [老司机总结的12条 SQL 优化方案(非常实用)](https://mp.weixin.qq.com/s/7QuASKTpXOm54CgLiHqEJg) ## 系统设计 @@ -295,14 +282,14 @@ #### SpringBoot -- [springboot史上最全教程,11篇文章全解析](https://blog.ouyangsihai.cn/categories/springboot2-0%E6%9C%80%E6%96%B0%E6%95%99%E7%A8%8B/) +- [springboot史上最全教程,11篇文章全解析](https://blog.csdn.net/sihai12345/category_7779682.html) - [微服务面试相关资料](docs/microservice/微服务相关资料.md) ## 分布式 ### dubbo -- [dubbo入门实战教程,这篇文章真的再好不过了](https://blog.ouyangsihai.cn/dubbo-yi-pian-wen-zhang-jiu-gou-liao-dubbo-yu-dao-chu-lian.html) +- [dubbo入门实战教程,这篇文章真的再好不过了](https://segmentfault.com/a/1190000019896723) - [dubbo源码分析](http://cmsblogs.com/?p=5324) - [dubbo面试题](https://mp.weixin.qq.com/s/PdWRHgm83XwPYP08KnkIsw) - [dubbo面试题2](https://mp.weixin.qq.com/s/Kz0s9K3J9Lpvh37oP_CtCA) @@ -310,13 +297,7 @@ ### zookeeper - [什么是zookeeper?](https://mp.weixin.qq.com/s/i2_c4A0146B7Ev8QnofbfQ) - -- [Zookeeper教程](http://cmsblogs.com/?p=4139) - -- [zookeeper源码分析](http://cmsblogs.com/?p=4190) - - [zookeeeper面试题](https://segmentfault.com/a/1190000014479433) - - [zookeeper面试题2](https://juejin.im/post/5dbac7a0f265da4d2c5e9b3b) @@ -324,7 +305,6 @@ - [RocketMQ简单教程](https://juejin.im/post/5af02571f265da0b9e64fcfd) - [RocketMQ教程](https://mp.weixin.qq.com/s/VAZaU1DuKbpnaALjp_-9Qw) -- [RocketMQ源码分析](http://cmsblogs.com/?p=3236) - [RocketMQ面试题](https://blog.csdn.net/dingshuo168/article/details/102970988) ### RabbitMQ @@ -342,8 +322,6 @@ - [kafka面试题](https://blog.csdn.net/qq_28900249/article/details/90346599) - [kafka面试题2](http://trumandu.github.io/2019/04/13/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98%E4%B8%8E%E7%AD%94%E6%A1%88%E5%85%A8%E5%A5%97%E6%95%B4%E7%90%86/) -- [分布式架构文章](https://blog.ouyangsihai.cn/fen-bu-shi-jia-gou-xi-lie-wen-zhang.html) - ### 消息中间件 - [消息中间件面试题总结](docs/project/消息中间件面试题.md) @@ -353,7 +331,6 @@ - [Redis设计与实现总结文章](https://blog.csdn.net/qq_41594698/category_9067680.html) - [Redis面试题必备:基础,面试题](https://mp.weixin.qq.com/s/3Fmv7h5p2QDtLxc9n1dp5A) - [Redis面试相关:其中包含redis知识](https://blog.csdn.net/qq_35190492/article/details/103105780) -- [Redis源码分析](http://cmsblogs.com/?p=4570) - [redis其他数据结构](https://blog.csdn.net/c_royi/article/details/82011208) ### 分布式系统 @@ -362,8 +339,8 @@ - [垃圾收集器ZGC](https://juejin.im/post/5dc361d3f265da4d1f51c670) - [jvm系列文章](https://crowhawk.github.io/tags/#JVM) - [一次JVM FullGC的背后,竟隐藏着惊心动魄的线上生产事故!](https://mp.weixin.qq.com/s/5SeGxKtwp6KZhUKn8jXi6A) -- [Java虚拟机调优文章](https://blog.ouyangsihai.cn/categories/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/) -- [利用VisualVM对高并发项目进行性能分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html#toc-heading-8) +- [深入理解Java虚拟机-如何利用JDK自带的命令行工具监控上百万的高并发的虚拟机性能](https://mp.weixin.qq.com/s/wPgA5SDURCAqPsWkZGGX0g) +- [深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析](https://mp.weixin.qq.com/s/hhA9tI_rYNkJVbF-R45hbA) - [JVM性能调优](https://www.iteye.com/blog/uule-2114697) - [百亿吞吐量服务的JVM性能调优实战](https://mp.weixin.qq.com/s?__biz=MzIwMzY1OTU1NQ==&mid=2247484236&idx=1&sn=b9743b2d7436f84e4617ff34e07abdd8&chksm=96cd4300a1baca1635a137294bc93c518c033ce01f843c9e012a1454b9f3ea3158fa1412e9da&scene=27&ascene=0&devicetype=android-24&version=26060638&nettype=WIFI&abtest_cookie=BAABAAoACwASABMABAAjlx4AUJkeAFmZHgBomR4AAAA%3D&lang=zh_CN&pass_ticket=%2F%2BLqr9N2EZtrEGLFo9vLA6Eqs89DSJ2CBKoAJFZ%2BBngphEP28dwmMQeSZcUB77qZ&wx_header=1) - [一次线上JVM调优实践,FullGC40次/天到10天一次的优化过程](https://blog.csdn.net/cml_blog/article/details/81057966) @@ -377,7 +354,7 @@ ### Git -- [实际开发中的git命令大全](https://blog.ouyangsihai.cn/wo-zai-shi-ji-gong-zuo-zhong-yong-de-zui-duo-de-git-ming-ling.html) +- [实际开发中的git命令大全](https://sihai.blog.csdn.net/article/details/106418135) ### Docker @@ -398,7 +375,7 @@ ## Java书籍推荐 -- [从入门到拿大厂offer,必须看的数据结构与算法书籍推荐](https://blog.ouyangsihai.cn/cong-ru-men-dao-na-da-han-offer-bi-xu-kan-de-suan-fa-shu-ji-tui-jian-bu-hao-bu-tui-jian.html) +- [从入门到拿大厂offer,必须看的数据结构与算法书籍推荐](https://blog.csdn.net/sihai12345/article/details/106011624) - [全网最全电子书下载](https://github.com/hello-go-maker/cs-books) ## 实战项目推荐 @@ -410,12 +387,10 @@ ## 程序人生 -- [我想是时候跟大学告别了](https://blog.ouyangsihai.cn/wo-xiang-shi-shi-hou-he-da-xue-gao-bie-liao.html) -- [坚持,这两个字非常重要!](https://blog.ouyangsihai.cn/jian-chi-zhe-liang-ge-zi-fei-chang-chong-yao.html) -- [2018年年终总结,你的呢?](https://blog.ouyangsihai.cn/zhe-shi-wo-de-2018-nian-zhong-zong-jie-ni-de-ni.html) -- [多去了解了解自己](https://blog.ouyangsihai.cn/duo-wen-wen-zi-ji-xiang-cheng-wei-shi-me-yang-de-ren.html) -- [关于考研,这是我给大家的经验](https://blog.ouyangsihai.cn/guan-yu-zhe-jian-shi-wo-you-hua-yao-shuo.html) -- [从普通二本到研究生再到自媒体的年轻人,这是我的故事](https://blog.ouyangsihai.cn/cong-pu-ben-dao-zha-shuo-cong-da-xue-sheng-dao-zi-mei-ti-de-nian-qing-ren-wo-fen-xiang-wo-de-coding-sheng-huo.html) +- [我想是时候跟大学告别了](https://blog.csdn.net/sihai12345/article/details/86934341) +- [坚持,这两个字非常重要!](https://blog.csdn.net/sihai12345/article/details/89507366) +- [关于考研,这是我给大家的经验](https://blog.csdn.net/sihai12345/article/details/88548630) +- [从普通二本到研究生再到自媒体的年轻人,这是我的故事](https://segmentfault.com/a/1190000020317748) ## 说明 @@ -445,15 +420,12 @@ 添加我的微信备注 **github**, 即可入群。 -![](http://image.ouyangsihai.cn/FldnPFgz_u_3kt7YH_sHhAQL1kyt) - + ### 公众号 如果大家想要实时关注我更新的文章以及分享的干货的话,关注我的公众号 **程序员的技术圈子**。 -![](http://image.ouyangsihai.cn/FuRA5sT9JUaVbx-YD-Acor04AWhF) - - + diff --git a/assets/wx.jpg b/assets/wx.jpg new file mode 100644 index 0000000..3fd7f11 Binary files /dev/null and b/assets/wx.jpg differ diff --git "a/assets/\347\250\213\345\272\217\345\221\230\346\212\200\346\234\257\345\234\210\345\255\220.jpg" "b/assets/\347\250\213\345\272\217\345\221\230\346\212\200\346\234\257\345\234\210\345\255\220.jpg" new file mode 100644 index 0000000..8561507 Binary files /dev/null and "b/assets/\347\250\213\345\272\217\345\221\230\346\212\200\346\234\257\345\234\210\345\255\220.jpg" differ diff --git "a/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" index 1d1c4bc..3c616a3 100644 --- "a/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" +++ "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" @@ -5,68 +5,445 @@ Ctrl+Shift+P(MacOS:cmd+shift+p)呼出命令面板,输入Markdown Preview -- [翻转链表](#翻转链表) -- [实现二叉树先序,中序和后序遍历](#实现二叉树先序中序和后序遍历) -- [设计LRU缓存结构](#设计lru缓存结构) -- [两个链表的第一个公共结点](#两个链表的第一个公共结点) -- [求平方根](#求平方根) -- [寻找第K大](#寻找第k大) -- [判断链表中是否有环](#判断链表中是否有环) -- [合并有序链表](#合并有序链表) -- [合并k个已排序的链表](#合并k个已排序的链表) -- [数组中相加和为0的三元组](#数组中相加和为0的三元组) -- [删除链表的倒数第n个节点](#删除链表的倒数第n个节点) -- [二分查找](#二分查找) -- [两个链表生成相加链表](#两个链表生成相加链表) -- [二叉树的之字形层序遍历](#二叉树的之字形层序遍历) -- [链表内指定区间反转](#链表内指定区间反转) -- [二叉树的镜像](#二叉树的镜像) -- [数组中只出现一次的数字](#数组中只出现一次的数字) -- [最长的括号子串](#最长的括号子串) -- [把二叉树打印成多行](#把二叉树打印成多行) -- [合并两个有序的数组](#合并两个有序的数组) -- [二叉树的最大路径和](#二叉树的最大路径和) -- [买卖股票的最佳时机](#买卖股票的最佳时机) -- [二叉树中是否存在节点和为指定值的路径](#二叉树中是否存在节点和为指定值的路径) -- [设计getMin功能的栈](#设计getmin功能的栈) -- [LFU缓存结构设计](#lfu缓存结构设计) -- [N皇后问题](#n皇后问题) -- [带权值的最小路径和](#带权值的最小路径和) -- [反转数字](#反转数字) -- [二叉搜索树的第k个结点](#二叉搜索树的第k个结点) -- [子数组最大乘积](#子数组最大乘积) -- [最长递增子序列](#最长递增子序列) -- [在两个长度相等的排序数组中找到上中位数](#在两个长度相等的排序数组中找到上中位数) -- [判断t1树中是否有与t2树拓扑结构完全相同的子树](#判断t1树中是否有与t2树拓扑结构完全相同的子树) -- [反转字符串](#反转字符串) -- [最大正方形](#最大正方形) -- [链表中的节点每K个一组翻转](#链表中的节点每k个一组翻转) -- [数组中的最长无重复子串的长度](#数组中的最长无重复子串的长度) -- [判断链表是否为回文结构](#判断链表是否为回文结构) -- [岛屿的数量](#岛屿的数量) -- [在二叉树中找到两个节点的最近公共祖先](#在二叉树中找到两个节点的最近公共祖先) -- [重复项数字的所有排列](#重复项数字的所有排列) -- [最长回文子串的长度](#最长回文子串的长度) -- [最长公共子序列](#最长公共子序列) -- [最小编辑代价](#最小编辑代价) -- [矩阵的最小路径和](#矩阵的最小路径和) -- [顺时针旋转数组](#顺时针旋转数组) -- [判断一棵树是否是搜索二叉树和完全二叉树](#判断一棵树是否是搜索二叉树和完全二叉树) -- [连续子数组的最大和(sum < 0置为0)](#连续子数组的最大和sum-0置为0httpswwwnowcodercompractice459bd355da1549fa8a49e350bf3df484tpid13tqid11183rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) -- [两数之和](#两数之和) -- [删除有序链表中重复出现的元素](#删除有序链表中重复出现的元素) -- [在转动过的有序数组中寻找目标值](#在转动过的有序数组中寻找目标值) -- [数组中未出现的最小正整数](#数组中未出现的最小正整数) -- [数组中最长连续子序列](#数组中最长连续子序列) -- [判断二叉树是否对称](#判断二叉树是否对称) -- [没有重复项数字的所有排列](#没有重复项数字的所有排列) -- [集合的所有子集](#集合的所有子集) +- [Java API整理](#java-api整理) +- [Go API整理](#go-api整理) +- [链表](#链表) + - [排序](#排序) + - [翻转链表, NC78](#翻转链表-nc78) + - [LFU缓存结构设计](#lfu缓存结构设计) + - [设计LRU缓存结构, NC93](#设计lru缓存结构-nc93) + - [合并有序链表, NC33](#合并有序链表-nc33) + - [链表中的节点每K个一组翻转](#链表中的节点每k个一组翻转) + - [判断链表中是否有环](#判断链表中是否有环) + - [链表中环的入口结点](#链表中环的入口结点) + - [删除链表的倒数第n个节点](#删除链表的倒数第n个节点) + - [两个链表的第一个公共结点](#两个链表的第一个公共结点) + - [两个链表生成相加链表](#两个链表生成相加链表) + - [合并k个已排序的链表](#合并k个已排序的链表) + - [单链表的排序,NC70](#单链表的排序nc70) + - [判断链表是否为回文结构](#判断链表是否为回文结构) + - [链表内指定区间反转](#链表内指定区间反转) + - [删除有序链表中重复出现的元素](#删除有序链表中重复出现的元素) + - [环形链表的约瑟夫问题](#环形链表的约瑟夫问题) + - [链表的奇偶重排](#链表的奇偶重排) + - [重排链表(1->n->2->n-1)](#重排链表1-n-2-n-1) + - [二叉搜索树与双向链表](#二叉搜索树与双向链表) +- [队列、栈](#队列栈) + - [用两个栈实现队列](#用两个栈实现队列) + - [有效括号序列](#有效括号序列) + - [包含 min 函数的栈](#包含-min-函数的栈) + - [表达式求值](#表达式求值) + - [最长括号子串](#最长括号子串) + - [括号生成](#括号生成) +- [二叉树](#二叉树) + - [实现二叉树先序,中序和后序遍历](#实现二叉树先序中序和后序遍历) + - [二叉树的层序遍历](#二叉树的层序遍历) + - [二叉树的之字形层序遍历](#二叉树的之字形层序遍历) + - [在二叉树中找到两个节点的最近公共祖先](#在二叉树中找到两个节点的最近公共祖先) + - [重建二叉树](#重建二叉树) + - [输出二叉树的右视图(先重建,再输出右视图)](#输出二叉树的右视图先重建再输出右视图) + - [二叉树的最大深度](#二叉树的最大深度) + - [判断是不是平衡二叉树](#判断是不是平衡二叉树) + - [二叉树根节点到叶子节点的所有路径和](#二叉树根节点到叶子节点的所有路径和) + - [二叉树中和为某一值的路径,返回所有路径](#二叉树中和为某一值的路径返回所有路径) + - [判断一棵二叉树是否为搜索二叉树和完全二叉树](#判断一棵二叉树是否为搜索二叉树和完全二叉树) + - [二叉树的最大路径和](#二叉树的最大路径和) + - [判断二叉树是否对称](#判断二叉树是否对称) + - [二叉树中是否存在节点和为指定值的路径](#二叉树中是否存在节点和为指定值的路径) + - [序列化二叉树](#序列化二叉树) + - [二叉搜索树的第k个结点](#二叉搜索树的第k个结点) + - [把二叉树打印成多行](#把二叉树打印成多行) + - [二叉树的镜像](#二叉树的镜像) + - [判断t1树中是否有与t2树拓扑结构完全相同的子树](#判断t1树中是否有与t2树拓扑结构完全相同的子树) + - [合并二叉树](#合并二叉树) + - [字典树的实现](#字典树的实现) + - [找到二叉搜索树中的两个错误节点](#找到二叉搜索树中的两个错误节点) +- [堆](#堆) + - [最小的K个数](#最小的k个数) + - [字符串出现次数的TopK问题](#字符串出现次数的topk问题) + - [寻找第K大](#寻找第k大) +- [双指针](#双指针) + - [最长无重复子数组的长度](#最长无重复子数组的长度) + - [滑动窗口的最大值](#滑动窗口的最大值) + - [合并区间(区间重叠)](#合并区间区间重叠) + - [反转字符串](#反转字符串) + - [数组中相加和为0的三元组](#数组中相加和为0的三元组) + - [接雨水问题](#接雨水问题) + - [最小覆盖子串(T包含S的最小子串)](#最小覆盖子串t包含s的最小子串) + - [两数之和](#两数之和) + - [最长重复子串(连续两个相同的字符串)](#最长重复子串连续两个相同的字符串) +- [动态规划](#动态规划) + - [跳台阶](#跳台阶) + - [连续子数组的最大和(sum < 0置为0)](#连续子数组的最大和sum--0置为0) + - [最长公共子串(返回具体字符串/长度)](#最长公共子串返回具体字符串长度) + - [斐波那契数列](#斐波那契数列) + - [最长回文子串的长度](#最长回文子串的长度) + - [最长递增子序列](#最长递增子序列) + - [买卖股票的最佳时机](#买卖股票的最佳时机) + - [矩阵的最小路径和](#矩阵的最小路径和) + - [编辑距离](#编辑距离) + - [不同路径的数目](#不同路径的数目) + - [最长公共子序列](#最长公共子序列) + - [最长的括号子串](#最长的括号子串) + - [高空扔鸡蛋](#高空扔鸡蛋) + - [兑换零钱](#兑换零钱) + - [最大正方形](#最大正方形) + - [通配符匹配](#通配符匹配) + - [正则表达式匹配](#正则表达式匹配) + - [矩阵最长递增路径](#矩阵最长递增路径) + - [最长上升子序列](#最长上升子序列) + - [目标和(完全背包)](#目标和完全背包) + - [打家劫舍](#打家劫舍) + - [带权值的最小路径和](#带权值的最小路径和) + - [最长不含重复字符的子字符串](#最长不含重复字符的子字符串) + - [把数字翻译成字符串](#把数字翻译成字符串) +- [二分](#二分) + - [求平方根](#求平方根) + - [在旋转过的有序数组中寻找目标值](#在旋转过的有序数组中寻找目标值) + - [在两个长度相等的排序数组中找到上中位数](#在两个长度相等的排序数组中找到上中位数) + - [有序矩阵元素查找](#有序矩阵元素查找) + - [二分查找](#二分查找) + - [旋转数组的最小数字](#旋转数组的最小数字) + - [数字在升序数组中出现的次数](#数字在升序数组中出现的次数) + - [峰值](#峰值) +- [数组](#数组) + - [数组中只出现一次的数字](#数组中只出现一次的数字) + - [合并两个有序的数组](#合并两个有序的数组) + - [子数组最大乘积](#子数组最大乘积) + - [数组中最长连续子序列](#数组中最长连续子序列) + - [数组中未出现的最小正整数](#数组中未出现的最小正整数) + - [顺时针旋转数组](#顺时针旋转数组) + - [旋转数组](#旋转数组) + - [逆序对](#逆序对) + - [调整数组顺序使奇数位于偶数前面](#调整数组顺序使奇数位于偶数前面) + - [矩阵乘法](#矩阵乘法) +- [回溯](#回溯) + - [字符串的全排列](#字符串的全排列) + - [岛屿的数量](#岛屿的数量) + - [没有重复项数字的所有排列(全排列)](#没有重复项数字的所有排列全排列) + - [集合的所有子集](#集合的所有子集) + - [重复项数字的所有排列](#重复项数字的所有排列) + - [N皇后问题](#n皇后问题) + - [把数组字符串转换为 ip 地址](#把数组字符串转换为-ip-地址) + - [加起来和为目标值的组合](#加起来和为目标值的组合) +- [其他](#其他) + - [螺旋矩阵](#螺旋矩阵) + - [顺时针旋转矩阵](#顺时针旋转矩阵) + - [进制转换](#进制转换) + - [反转数字](#反转数字) + - [大数加法](#大数加法) + - [把字符串转换成整数(atoi)](#把字符串转换成整数atoi) + - [最长公共前缀](#最长公共前缀) + - [回文数字](#回文数字) + - [字符串变形(反序,大写)](#字符串变形反序大写) + - [最大值(数组拼接最大数)](#最大值数组拼接最大数) + - [验证ip地址](#验证ip地址) + - [二进制中1的个数](#二进制中1的个数) + - [第一个只出现一次的字符](#第一个只出现一次的字符) +- [其他编程题(golang、java)](#其他编程题golangjava) + - [单例模式](#单例模式) + - [实现线程安全的生产者消费者](#实现线程安全的生产者消费者) + - [一个10G的文件,里面全部是自然数,一行一个,乱序排列,对其排序。在32位机器上面完成,内存限制为 2G(bitmap原理知道吗?)](#一个10g的文件里面全部是自然数一行一个乱序排列对其排序在32位机器上面完成内存限制为-2gbitmap原理知道吗) + - [实现使用字符串函数名,调用函数](#实现使用字符串函数名调用函数) + - [负载均衡算法。(一致性哈希)](#负载均衡算法一致性哈希) + - [(Goroutine)有三个函数,分别打印"cat", "fish","dog"要求每一个函数都用一个goroutine,按照顺序打印100次](#goroutine有三个函数分别打印cat-fishdog要求每一个函数都用一个goroutine按照顺序打印100次) + - [两个协程交替打印10个字母和数字](#两个协程交替打印10个字母和数字) + - [启动 2个groutine 2秒后取消, 第一个协程1秒执行完,第二个协程3秒执行完。](#启动-2个groutine-2秒后取消-第一个协程1秒执行完第二个协程3秒执行完) + - [当select监控多个chan同时到达就绪态时,如何先执行某个任务?](#当select监控多个chan同时到达就绪态时如何先执行某个任务) +## Java API整理 +- api -### 翻转链表 +https://blog.csdn.net/qq_34756156/article/details/120713595 + +## Go API整理 + +- api + +https://www.pseudoyu.com/zh/2021/05/29/algorithm_data_structure_go/ +https://greyireland.gitbook.io/algorithm-pattern/ru-men-pian/golang + +- 刷题模板 + +https://greyireland.gitbook.io/algorithm-pattern/ + +## 链表 + +### 排序 + +```java +import java.util.*; + + +public class Solution { + + public int[] MySort(int[] arr) { +// 选择排序 +// return selectSort(arr); +// 冒泡排序 +// return bubbleSort(arr); +// 插入排序 +// return insertSort(arr); +// 希尔排序 +// return shellSort(arr); +// 归并排序 +// return mergeSort(arr,0,arr.length-1); +// 快速排序 +// quickSort(arr,0,arr.length-1); +// return arr; +// 计数排序 +// return countSort(arr); +// 基数排序 +// return radixSort(arr); +// 桶排序 + return bucketSort(arr); + } + // 选择排序---选择最小的数与当前数交换 + public int[] selectSort(int[] arr){ + if(arr.length<2)return arr; + for(int i=0;iarr[j])swap(arr,i,j); + } + } + return arr; + } + + // 插入排序---与当前位置之前的所有元素比较,交换元素 + public int[] insertSort(int[] arr){ + if(arr.length<2)return arr; + for(int i=1;i0;j--){ + if(arr[j]0;gap=(gap-1)/3){ + for(int i=gap;i=0;j=j-gap){ + if(arr[j]= right) return ; + int pivot = arr[left]; + int i = left,j = right; + while(i < j){ + while(arr[j] >= pivot && j>i){ + j--; + } + while(arr[i] <= pivot && i0){count++;temp = temp/10;} + if(count>max)max = count; + } + + for(int m=0;m0){temp=temp/10;} + int result = temp%10; + for(int k=0;k0){ + arr[k++] = countArr[i][j]; + } + } + } + return arr; + } + + // 桶排序---给定n个桶,找到最大数与最小数, + // 计算出每个桶能装的数的范围,将数分别放入符合条件的桶中, + // 对每个桶进行快速排序,最后合并 + public int[] bucketSort(int[] arr){ + // 设置桶的个数 + int bucket = 4; + // 找到数组中的最大最小值 + int min = arr[0],max=arr[0]; + for(int i=0;imax)max=arr[i]; + if(arr[i]= min && temp < min+range){ + for(int k =0;k= min+range && temp < min+2*range){ + for(int k =0;k= min+2*range && temp < max - range){ + for(int k =0;k= max - range && temp <= max){ + for(int k =0;k list1 = new ArrayList<>(); - ArrayList list2 = new ArrayList<>(); - ArrayList list3 = new ArrayList<>(); - front(root,list1,list2,list3); - int[][] ints = new int[3][list1.size()]; - for (int i = 0; i < list1.size(); i++) { - ints[0][i] = list1.get(i); - ints[1][i] = list2.get(i); - ints[2][i] = list3.get(i); +class LFUCache { + Map cache; // 存储缓存的内容 + Map> freqMap; // 存储每个频次对应的双向链表 + int size; + int capacity; + int min; // 存储当前最小频次 + + public LFUCache(int capacity) { + cache = new HashMap<> (capacity); + freqMap = new HashMap<>(); + this.capacity = capacity; + } + + public int get(int key) { + Node node = cache.get(key); + if (node == null) { + return -1; } - return ints; + freqInc(node); + return node.value; } - - public void front(TreeNode root,ArrayList list1, - ArrayList list2,ArrayList list3){ - if(root == null){ + + public void put(int key, int value) { + if (capacity == 0) { return; } - - list1.add(root.val); - front(root.left,list1,list2,list3); - list2.add(root.val); - front(root.right,list1,list2,list3); - list3.add(root.val); + Node node = cache.get(key); + if (node != null) { + node.value = value; + freqInc(node); + } else { + if (size == capacity) { + Node deadNode = removeNode(); + cache.remove(deadNode.key); + size--; + } + Node newNode = new Node(key, value); + cache.put(key, newNode); + addNode(newNode); + size++; + } } -} -``` - -- 非递归遍历 - -- 前序遍历 - -用栈来保存信息,但是遍历的时候,是:**先输出根节点信息,然后压入右节点信息,然后再压入左节点信息。** -```java -public void pre(Node head){ - if(head == null){ - return; - } - Stack stack = new Stack<>(); - stack.push(head); - while(!stack.isEmpty()){ - head = stack.poll(); - System.out.println(head.value + " "); - if(head.right != null){ - stack.push(head.right); + void freqInc(Node node) { + // 从原freq对应的链表里移除, 并更新min + int freq = node.freq; + LinkedHashSet set = freqMap.get(freq); + set.remove(node); + if (freq == min && set.size() == 0) { + min = freq + 1; } - if(head.left != null){ - stack.push(head.left); + // 加入新freq对应的链表 + node.freq++; + LinkedHashSet newSet = freqMap.get(freq + 1); + if (newSet == null) { + newSet = new LinkedHashSet<>(); + freqMap.put(freq + 1, newSet); } + newSet.add(node); } - System.out.println(); -} -``` - -- 中序遍历 -中序遍历的顺序是**左中右**,先一直左节点遍历,并压入栈中,当做节点为空时,输出当前节点,往右节点遍历。 - -```java -public void inorder(Node head){ - if(head == null){ - return; + void addNode(Node node) { + LinkedHashSet set = freqMap.get(1); + if (set == null) { + set = new LinkedHashSet<>(); + freqMap.put(1, set); + } + set.add(node); + min = 1; } - Stack stack = new Stack<>(); - stack.push(head); - while(!stack.isEmpty() || head != null){ - if(head != null){ - stack.push(head); - head = head.left - } else { - head = stack.poll(); - System.out.println(head.value + " "); - head = head.right; - } + + Node removeNode() { + LinkedHashSet set = freqMap.get(min); + Node deadNode = set.iterator().next(); + set.remove(deadNode); + return deadNode; } - System.out.println(); } -``` -- 后序遍历 +class Node { + int key; + int value; + int freq = 1; -用两个栈来实现,压入栈1的时候为**先左后右**,栈1弹出来就是**中右左**,栈2收集起来就是**左右中**。 + public Node() {} + + public Node(int key, int value) { + this.key = key; + this.value = value; + } +} +``` +### 设计LRU缓存结构, NC93 -### 设计LRU缓存结构 +- lru-k算法:https://blog.csdn.net/love254443233/article/details/82598381 ```java import java.util.*; - - + public class Solution { /** * lru design @@ -284,119 +652,151 @@ class LRUCache{ } ``` -### 两个链表的第一个公共结点 +```go +type LRUCache struct { + capacity int + m map[int]*Node + head, tail *Node +} -```java -/* -public class ListNode { - int val; - ListNode next = null; - - ListNode(int val) { - this.val = val; +type Node struct { + Key int + Value int + Pre, Next *Node +} + +func (this *LRUCache) Get(key int) int { + if v, ok := this.m[key]; ok { + this.moveToHead(v) + return v.Value } -}*/ -public class Solution { - public ListNode FindFirstCommonNode(ListNode pHead1, - ListNode pHead2) { - if(pHead1 == null || pHead2 == null){ - return null; - } - - ListNode p1 = pHead1; - ListNode p2 = pHead2; - - while(p1 != p2){ - p1 = p1.next; - p2 = p2.next; - if(p1 != p2){ - if(p1 == null) p1 = pHead2; - if(p2 == null) p2 = pHead1; - } - } - - return p1; - + return -1 +} + +func (this *LRUCache) moveToHead(node *Node) { + this.deleteNode(node) + this.addToHead(node) +} + +func (this *LRUCache) deleteNode(node *Node) { + node.Pre.Next = node.Next + node.Next.Pre = node.Pre +} + +func (this *LRUCache) removeTail() int { + node := this.tail.Pre + this.deleteNode(node) + return node.Key +} + +func (this *LRUCache) addToHead(node *Node) { + this.head.Next.Pre = node + node.Next = this.head.Next + node.Pre = this.head + this.head.Next = node +} + +func (this *LRUCache) Put(key int, value int) { + if v, ok := this.m[key]; ok { + v.Value = value + this.moveToHead(v) + return + } + + if this.capacity == len(this.m) { + rmKey := this.removeTail() + delete(this.m, rmKey) + } + + newNode := &Node{Key: key, Value: value} + this.addToHead(newNode) + this.m[key] = newNode +} + +func Constructor(capacity int) LRUCache { + head, tail := &Node{}, &Node{} + head.Next = tail + tail.Pre = head + return LRUCache{ + capacity: capacity, + m: map[int]*Node{}, + head: head, + tail: tail, } } ``` -### 求平方根 +### 合并有序链表, NC33 ```java import java.util.*; +/* + * public class ListNode { + * int val; + * ListNode next = null; + * } + */ public class Solution { /** * - * @param x int整型 - * @return int整型 + * @param l1 ListNode类 + * @param l2 ListNode类 + * @return ListNode类 */ - public int sqrt (int x) { - // write code here - if(x < 2){ - return x; - } - int left = 1; - int right = x / 2; - while(left <= right){ - int mid = left + (right - left) / 2; - if(x / mid == mid){ - return mid; - } else if(x / mid < mid){ - right = mid - 1; - } else if(x / mid > mid){ - left = mid + 1; + public ListNode mergeTwoLists (ListNode l1, ListNode l2) { + ListNode node = new ListNode(0); + ListNode res = node; + while(l1 != null && l2 != null){ + if(l1.val > l2.val){ + node.next = l2; + l2 = l2.next; + } else { + node.next = l1; + l1 = l1.next; } + node = node.next; } - return right; + if(l1 != null){ + node.next = l1; + } + + if(l2 != null){ + node.next = l2; + } + + return res.next; } } ``` -### 寻找第K大 +### 链表中的节点每K个一组翻转 ```java -import java.util.*; - -public class Finder { - public int findKth(int[] a, int n, int K) { - // write code here - return find(a, 0, n-1, K); - } - - public int find(int[] a, int low, int high, int K){ - int pivot = partition(a, low, high); - - if(pivot + 1 < K){ - return find(a, pivot + 1, high, K); - } else if(pivot + 1 > K){ - return find(a, low, pivot - 1, K); - } else { - return a[pivot]; - } - } - - int partition(int arr[], int startIndex, int endIndex){ - int small = startIndex - 1; - for (int i = startIndex; i < endIndex; ++i) { - if(arr[i] > arr[endIndex]) { - swap(arr,++small, i); - } - } - swap(arr,++small,endIndex); - return small; +//明显递归解决,翻转第一组之后,以第二组的开头为头节点,继续翻转,转翻到最后,返回。 +public ListNode reverseKGroup(ListNode head, int k) { + if(head==null||head.next==null) + return head; + ListNode h=new ListNode(0); + h.next=head; + ListNode next=null,tmp=head,cur=head; + for(int i=1;i l2.val){ - node.next = l2; - l2 = l2.next; - } else { - node.next = l1; - l1 = l1.next; - } - node = node.next; + public ListNode removeNthFromEnd (ListNode head, int n) { + // write code here + ListNode dummyNode = new ListNode(0); + dummyNode.next = head; + ListNode fast = dummyNode; + ListNode slow = dummyNode; + for(int i = 0; i <= n; i++){ + fast = fast.next; } - if(l1 != null){ - node.next = l1; + while(fast != null){ + fast = fast.next; + slow = slow.next; } - if(l2 != null){ - node.next = l2; + slow.next = slow.next.next; + + return dummyNode.next; + } +} +``` + +### 两个链表的第一个公共结点 + +```java +/* +public class ListNode { + int val; + ListNode next = null; + + ListNode(int val) { + this.val = val; + } +}*/ +public class Solution { + public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + if(pHead1 == null || pHead2 == null){ + return null; } - return res.next; + ListNode p1 = pHead1; + ListNode p2 = pHead2; + + while(p1 != p2){ + p1 = p1.next; + p2 = p2.next; + if(p1 != p2){ + if(p1 == null) p1 = pHead2; + if(p2 == null) p2 = pHead1; + } + } + + return p1; + } } +``` + +### 两个链表生成相加链表 +```java +import java.util.*; + +/* + * public class ListNode { + * int val; + * ListNode next = null; + * } + */ + +public class Solution { + /** + * + * @param head1 ListNode类 + * @param head2 ListNode类 + * @return ListNode类 + */ + public ListNode addInList (ListNode head1, ListNode head2) { + // write code here + if(head1==null) return head2; + if(head2==null) return head1; + ListNode l1=reverse(head1); + ListNode l2=reverse(head2); + ListNode result=new ListNode(0); + int c=0; + while(l1!=null||l2!=null||c!=0) + { + int v1=l1!=null?l1.val:0; + int v2=l2!=null?l2.val:0; + int val=v1+v2+c; + c=val/10; + ListNode cur=new ListNode(val%10); + cur.next=result.next; + result.next=cur; + if(l1!=null) + l1=l1.next; + if(l2!=null) + l2=l2.next; + } + return result.next; + } + + public ListNode reverse(ListNode node) + { + if(node==null) return node; + ListNode pre=null,next=null; + while(node!=null) + { + next=node.next; + node.next=pre; + pre=node; + node=next; + } + return pre; + } +} ``` ### 合并k个已排序的链表 @@ -540,227 +1061,134 @@ public class Solution { } ``` -### 数组中相加和为0的三元组 +### 单链表的排序,NC70 + +- 堆排序 ```java import java.util.*; - -public class Solution { - public ArrayList> threeSum(int[] num) { - ArrayList> list = new ArrayList<>(); - Arrays.sort(num); - int left,right,sum; - for(int i = 0; i < num.length - 2; i++){ - if(i > 0 && num[i] == num[i-1]) continue; - left = i + 1; - right = num.length - 1; - while(left < right){ - sum = num[i] + num[left] + num[right]; - if(sum == 0){ - ArrayList temp = new ArrayList<>(); - temp.add(num[i]); - temp.add(num[left]); - temp.add(num[right]); - list.add(temp); - right--; - left++; - while(left < right && num[left] == num[left-1]){ - left++; - } - while(left < right && num[right] == num[right+1]){ - right--; - } - } else if(sum < 0){ - left++; - } else { - right--; - } - } - } - return list; - } -} - -``` - -### 删除链表的倒数第n个节点 - -```java -import java.util.*; - -/* - * public class ListNode { - * int val; - * ListNode next = null; - * } - */ - + public class Solution { /** - * - * @param head ListNode类 - * @param n int整型 + * + * @param head ListNode类 the head node * @return ListNode类 */ - public ListNode removeNthFromEnd (ListNode head, int n) { + public ListNode sortInList (ListNode head) { // write code here - ListNode dummyNode = new ListNode(0); - dummyNode.next = head; - ListNode fast = dummyNode; - ListNode slow = dummyNode; - for(int i = 0; i <= n; i++){ - fast = fast.next; + PriorityQueue heap = new PriorityQueue<>((n1, n2) -> n1.val - n2.val); + while (head != null) { + heap.add(head); + head = head.next; } - - while(fast != null){ - fast = fast.next; - slow = slow.next; + ListNode dummy = new ListNode(-1); + ListNode cur = dummy; + while (!heap.isEmpty()) { + cur.next = heap.poll(); + cur = cur.next; } - - slow.next = slow.next.next; - - return dummyNode.next; + cur.next = null; + return dummy.next; } } ``` -### 二分查找 +- 归并排序 ```java import java.util.*; - - public class Solution { - /** - * 二分查找 - * @param n int整型 数组长度 - * @param v int整型 查找值 - * @param a int整型一维数组 有序数组 - * @return int整型 - */ - public int upper_bound_ (int n, int v, int[] a) { - // write code here - int left = 0, right = n; - while(left < right){ - int mid = left + (right - left) / 2; - if(a[mid] == v){ - right = mid; - } else if(a[mid] > v){ - right = mid; - } else { - left = mid + 1; + //合并两段有序链表 + ListNode merge(ListNode pHead1, ListNode pHead2) { + //一个已经为空了,直接返回另一个 + if(pHead1 == null) + return pHead2; + if(pHead2 == null) + return pHead1; + //加一个表头 + ListNode head = new ListNode(0); + ListNode cur = head; + //两个链表都要不为空 + while(pHead1 != null && pHead2 != null){ + //取较小值的节点 + if(pHead1.val <= pHead2.val){ + cur.next = pHead1; + //只移动取值的指针 + pHead1 = pHead1.next; + }else{ + cur.next = pHead2; + //只移动取值的指针 + pHead2 = pHead2.next; } + //指针后移 + cur = cur.next; } - return left+1; - } -} -``` - -### 两个链表生成相加链表 - -```java -import java.util.*; - -/* - * public class ListNode { - * int val; - * ListNode next = null; - * } - */ - -public class Solution { - /** - * - * @param head1 ListNode类 - * @param head2 ListNode类 - * @return ListNode类 - */ - public ListNode addInList (ListNode head1, ListNode head2) { - // write code here - if(head1==null) return head2; - if(head2==null) return head1; - ListNode l1=reverse(head1); - ListNode l2=reverse(head2); - ListNode result=new ListNode(0); - int c=0; - while(l1!=null||l2!=null||c!=0) - { - int v1=l1!=null?l1.val:0; - int v2=l2!=null?l2.val:0; - int val=v1+v2+c; - c=val/10; - ListNode cur=new ListNode(val%10); - cur.next=result.next; - result.next=cur; - if(l1!=null) - l1=l1.next; - if(l2!=null) - l2=l2.next; - } - return result.next; + //哪个链表还有剩,直接连在后面 + if(pHead1 != null) + cur.next = pHead1; + else + cur.next = pHead2; + //返回值去掉表头 + return head.next; } - - public ListNode reverse(ListNode node) - { - if(node==null) return node; - ListNode pre=null,next=null; - while(node!=null) - { - next=node.next; - node.next=pre; - pre=node; - node=next; + + public ListNode sortInList (ListNode head) { + //链表为空或者只有一个元素,直接就是有序的 + if(head == null || head.next == null) + return head; + ListNode left = head; + ListNode mid = head.next; + ListNode right = head.next.next; + //右边的指针到达末尾时,中间的指针指向该段链表的中间 + while(right != null && right.next != null){ + left = left.next; + mid = mid.next; + right = right.next.next; } - return pre; + //左边指针指向左段的左右一个节点,从这里断开 + left.next = null; + //分成两段排序,合并排好序的两段 + return merge(sortInList(head), sortInList(mid)); } } ``` -### 二叉树的之字形层序遍历 +### 判断链表是否为回文结构 ```java import java.util.*; - -/* - * public class TreeNode { - * int val = 0; - * TreeNode left = null; - * TreeNode right = null; - * } - */ - + public class Solution { /** * - * @param root TreeNode类 - * @return int整型ArrayList> + * @param head ListNode类 the head + * @return bool布尔型 */ - public ArrayList> zigzagLevelOrder (TreeNode root) { - // write code here - Queue queue = new LinkedList<>(); - ArrayList> list = new ArrayList<>(); - if(root != null) queue.add(root); - while(!queue.isEmpty()){ - ArrayList temp = new ArrayList<>(); - for(int i = queue.size(); i > 0; i--){ - TreeNode node = queue.poll(); - temp.add(node.val); - if(node.left != null){ - queue.add(node.left); - } - if(node.right != null){ - queue.add(node.right); - } - } - if(list.size() % 2 == 1){ - Collections.reverse(temp); + public boolean isPail (ListNode head) { + ListNode slow = head; + ListNode fast = head; + while(fast != null && fast.next != null){ + fast = fast.next.next; + slow = slow.next; + } + + Stack stack = new Stack<>(); + while(slow != null){ + stack.add(slow.val); + slow = slow.next; + } + + while(!stack.isEmpty()){ + if(stack.pop() != head.val){ + return false; } - list.add(temp); + + head = head.next; } - return list; + + return true; } } - ``` ### 链表内指定区间反转 @@ -808,1591 +1236,5650 @@ public class Solution { return dummy.next; } } - ``` -### 二叉树的镜像 +### 删除有序链表中重复出现的元素 ```java -public class Solution { - public void Mirror(TreeNode root) { - if(root == null){ - return; - } - if(root.left == null && root.right == null){ - return; - } - Stack stack = new Stack<>(); - stack.push(root); - while(!stack.isEmpty()){ - TreeNode node = stack.pop(); - - if(node.left != null || node.right != null){ - TreeNode temp = node.left; - node.left = node.right; - node.right = temp; - } - - if(node.left != null){ - stack.push(node.left); - } - - if(node.right != null){ - stack.push(node.right); +public ListNode deleteDuplicates (ListNode head) { + ListNode dummy=new ListNode(0); + dummy.next=head; + ListNode pre=dummy; + ListNode p=head; + while(p!=null&&p.next!=null){ + if(p.val==p.next.val){ + while(p.next!=null&&p.val==p.next.val){ + p=p.next; } + pre.next=p.next; + p=p.next; + } + else{ + pre=p; + p=p.next; + } + } + return dummy.next; +} +``` + +### 环形链表的约瑟夫问题 + +```java +public int ysf (int n, int m) { + // write code here + ListNode head = new ListNode(1) ,p1=head; + for(int i=2;i<=n;i++){ + ListNode temp = new ListNode(i); + p1.next=temp; + p1=p1.next; + } + p1.next=head; + while(n-->1){ + int num=m; + while(num-->1){ + p1=p1.next; } + p1.next=p1.next.next; } + return p1.val; } - +``` + +### 链表的奇偶重排 + +```java +import java.util.*; + /* + * public class ListNode { + * int val; + * ListNode next = null; + * } + */ + public class Solution { - public void Mirror(TreeNode root) { - if(root == null){ - return; - } - if(root.left == null && root.right == null){ - return; - } - - TreeNode temp = root.left; - root.left = root.right; - root.right = temp; - - if(root.left != null){ - Mirror(root.left); - } - - if(root.right != null){ - Mirror(root.right); - } - } -} -*/ - -``` - -### 数组中只出现一次的数字 - -```java -public class Solution { - - public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) { - int num = 0; - for(int i = 0; i < array.length; i++){ - num^=array[i]; - } - - int count = 0; - // 标志位,记录num中的第一个1出现的位置 - for(;count < array.length; count++){ - if((num&(1< set = new HashSet<>(); - for(int i = 0; i < array.length; i++){ - if(!set.add(array[i])){ - set.remove(array[i]); - } - } - - Object[] temp = set.toArray(); - num1[0] = (int)temp[0]; - num2[0] = (int)temp[1]; - }*/ } - ``` -### 最长的括号子串 +### 重排链表(1->n->2->n-1) ```java +import java.util.*; public class Solution { - /** - * - * @param s string字符串 - * @return int整型 - */ - public int longestValidParentheses (String s) { - // write code here - if(s == null || s.length() <= 0){ - return 0; - } - - Stack stack = new Stack<>(); - int last = -1; - int maxLen = 0; - for(int i = 0; i < s.length(); i++){ - if(s.charAt(i) == '('){ - stack.push(i); - } else { - if(stack.isEmpty()){ - last = i; - } else { - stack.pop(); - if(stack.isEmpty()){ - maxLen = Math.max(maxLen, i - last); - } else { - maxLen = Math.max(maxLen, i - stack.peek()); - } - } - } + public void reorderList(ListNode head) { + if (head == null || head.next == null) return; + List list = new ArrayList<>(); + ListNode cur = head; + while (cur != null) { + list.add(cur); + cur = cur.next; } - - return maxLen; - } -} - -// 动态规划 -public int longestValidParentheses2(String s) { - if (s == null || s.length() == 0) - return 0; - int[] dp = new int[s.length()]; - int ans = 0; - for (int i = 1; i < s.length(); i++) { - // 如果是'('直接跳过,默认为0 - if (s.charAt(i) == ')') { - if (s.charAt(i - 1) == '(') - dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; - // 说明s.charAt(i - 1)==')' - else if (i - dp[i - 1] > 0 && - s.charAt(i - dp[i - 1] - 1) == '(') { - dp[i] = (i - dp[i - 1] > 1 ? - dp[i - dp[i - 1] - 2] : 0) + dp[i - 1] + 2; - // 因为加了一个左括号和一个右括号,所以是加2 - } + int l = 0, r = list.size() - 1; + while (l < r) { + list.get(l).next = list.get(r); + l++; + list.get(r).next = list.get(l); + r--; } - ans = Math.max(ans, dp[i]); + list.get(l).next = null; } - return ans; } ``` -### 把二叉树打印成多行 +### 二叉搜索树与双向链表 ```java -import java.util.*; - - -/* +/** public class TreeNode { int val = 0; TreeNode left = null; TreeNode right = null; - + public TreeNode(int val) { this.val = val; - + } - } */ public class Solution { - ArrayList > Print(TreeNode pRoot) { - if(pRoot == null){ - return new ArrayList>(); - } - ArrayList> list = new ArrayList<>(); - - Queue queue = new LinkedList<>(); - queue.add(pRoot); - while(!queue.isEmpty()){ - ArrayList temp = new ArrayList<>(); - for(int i = queue.size(); i > 0; i--){ - TreeNode node = queue.poll(); - temp.add(node.val); - if(node.left != null){ - queue.add(node.left); - } - if(node.right != null){ - queue.add(node.right); - } - } - list.add(temp); + public TreeNode head = null; + public TreeNode dummy = null; + public TreeNode Convert(TreeNode pRootOfTree) { + ConvertSub(pRootOfTree); + return dummy; + } + + public void ConvertSub(TreeNode root){ + if(root == null) return; + ConvertSub(root.left); + + if(head == null){ + head = root; + dummy = root; + } else { + head.right = root; + root.left = head; + head = root; } - - return list; + + ConvertSub(root.right); } - } ``` -### 合并两个有序的数组 +## 队列、栈 + +### 用两个栈实现队列 ```java +import java.util.Stack; public class Solution { - public void merge(int A[], int m, int B[], int n) { - int i = m-1, j = n-1, k = m+n-1; - while(i >= 0 && j >= 0){ - if(A[i] > B[j]){ - A[k--] = A[i--]; - } else { - A[k--] = B[j--]; + Stack stack1 = new Stack(); + Stack stack2 = new Stack(); + + public void push(int node) { + stack1.add(node); + } + + public void pushToPop(){ + if(stack2.isEmpty()){ + while(!stack1.isEmpty()){ + stack2.add(stack1.pop()); } } - - while(j >= 0){ - A[k--] = B[j--]; - } + } + + public int pop() { + pushToPop(); + return stack2.pop(); } } ``` -### 二叉树的最大路径和 +### 有效括号序列 ```java +import java.util.*; public class Solution { - int max = Integer.MIN_VALUE; - /** - * - * @param root TreeNode类 - * @return int整型 - */ - public int maxPathSum (TreeNode root) { + + public boolean isValid (String s) { // write code here - maxSum(root); - return max; - } - - public int maxSum(TreeNode root){ - if(root == null){ - return 0; + Stack stack = new Stack<>(); + char[] chs = s.toCharArray(); + for(int i = 0; i < chs.length; i++){ + if(stack.isEmpty()){ + stack.push(chs[i]); + } else if(chs[i] == '{' || chs[i] == '[' + || chs[i] == '('){ + stack.push(chs[i]); + } else if((chs[i] == '}' && stack.peek() == '{') || + (chs[i] == ']' && stack.peek() == '[') || + (chs[i] == ')' && stack.peek() == '(')){ + stack.pop(); + } } - //三种情况:1.包含一个子树和顶点,2.仅包含顶点,3.包含左子树和右子树以及顶点。 - int left = Math.max(maxSum(root.left),0); - int right = Math.max(maxSum(root.right),0); - - max = Math.max(max,left+right+root.val); - - //对于每一个子树,返回包含该子树顶点的深度方向的路径和的最大值。 - return root.val + Math.max(left,right); + return stack.isEmpty() ? true : false; } } ``` -### 买卖股票的最佳时机 - -base case: -dp[-1][k][0] = dp[i][0][0] = 0 -dp[-1][k][1] = dp[i][0][1] = -infinity - -状态转移⽅程: -dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) -dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) +### 包含 min 函数的栈 ```java +import java.util.Stack; + public class Solution { - /** - * - * @param prices int整型一维数组 - * @return int整型 - */ - public int maxProfit (int[] prices) { - if(prices.length == 0) return 0; - // write code here - int n = prices.length; - int[][] dp = new int[n][2]; - for(int i = 0; i < n; i++){ - if(i - 1 == -1){ - dp[i][0] = 0; - dp[i][1] = -prices[i]; - continue; - } - dp[i][0] = Math.max(dp[i-1][0], - dp[i-1][1] + prices[i]); - dp[i][1] = Math.max(dp[i-1][1],-prices[i]); + Stack minStack = new Stack<>(); + Stack stack = new Stack<>(); + + public void push(int node) { + if(minStack.isEmpty()){ + minStack.push(node); } - return dp[n-1][0]; + if(node < minStack.peek().intValue()){ + minStack.push(node); + } else { + minStack.push(minStack.peek()); + } + + stack.push(node); + } + + public void pop() { + if(stack.isEmpty()){ + return; + } + stack.pop(); + minStack.pop(); + } + + public int top() { + return minStack.peek(); + } + + public int min() { + return minStack.peek(); } } ``` -### 二叉树中是否存在节点和为指定值的路径 +### 表达式求值 + +step 1:使用栈辅助处理优先级,默认符号为加号。 +step 2:遍历字符串,遇到数字,则将连续的数字字符部分转化为int型数字。 +step 3:遇到左括号,则将括号后的部分送入递归,处理子问题; +遇到右括号代表已经到了这个子问题的结尾,结束继续遍历字符串,将子问题的加法部分相加为一个数字,返回。 +step 4:当遇到符号的时候如果是+,得到的数字正常入栈,如果是-,则将其相反数入栈, +如果是*,则将栈中内容弹出与后一个元素相乘再入栈。 +step 5:最后将栈中剩余的所有元素,进行一次全部相加。 + ```java +import java.util.*; public class Solution { - /** - * - * @param root TreeNode类 - * @param sum int整型 - * @return bool布尔型 - */ - public boolean hasPathSum (TreeNode root, int sum) { - // write code here - if(root == null){ - return false; - } - - if(root.left == null && root.right == null){ - return sum - root.val == 0; + public ArrayList function(String s, int index){ + Stack stack = new Stack(); + int num = 0; + char op = '+'; + int i; + for(i = index; i < s.length(); i++){ + //数字转换成int数字 + //判断是否为数字 + if(s.charAt(i) >= '0' && s.charAt(i) <= '9'){ + num = num * 10 + s.charAt(i) - '0'; + if(i != s.length() - 1) + continue; + } + //碰到'('时,把整个括号内的当成一个数字处理 + if(s.charAt(i) == '('){ + //递归处理括号 + ArrayList res = function(s, i + 1); + num = res.get(0); + i = res.get(1); + if(i != s.length() - 1) + continue; + } + switch(op){ + //加减号先入栈 + case '+': + stack.push(num); + break; + case '-': + //相反数 + stack.push(-num); + break; + //优先计算乘号 + case '*': + int temp = stack.pop(); + stack.push(temp * num); + break; + } + num = 0; + //右括号结束递归 + if(s.charAt(i) == ')') + break; + else + op = s.charAt(i); } - - return hasPathSum(root.left,sum - root.val) || - hasPathSum(root.right,sum - root.val); + int sum = 0; + //栈中元素相加 + while(!stack.isEmpty()) + sum += stack.pop(); + ArrayList temp = new ArrayList(); + temp.add(sum); + temp.add(i); + return temp; + } + public int solve (String s) { + ArrayList res = function(s, 0); + return res.get(0); } } ``` -### 设计getMin功能的栈 +### 最长括号子串 + +step 1:可以使用栈来记录左括号下标。 +step 2:遍历字符串,左括号入栈,每次遇到右括号则弹出左括号的下标。 +step 3:然后长度则更新为当前下标与栈顶下标的距离。 +step 4:遇到不符合的括号,可能会使栈为空,因此需要使用start记录上一次结束的位置, +这样用当前下标减去start即可获取长度,即得到子串。 +step 5:循环中最后维护子串长度最大值。 ```java +import java.util.*; public class Solution { - /** - * return a array which include all ans for op3 - * @param op int整型二维数组 operator - * @return int整型一维数组 - */ - public int[] getMinStack (int[][] op) { - // write code here - LinkedList stack = new LinkedList<>(); - LinkedList minStack = new LinkedList<>(); - ArrayList ans = new ArrayList<>(); - - for(int i = 0; i < op.length; i++){ - int type = op[i][0]; - if(type == 1){ - if(minStack.size() == 0){ - minStack.push(op[i][1]); - } else if(op[i][1] <= minStack.peek()){ - minStack.push(op[i][1]); + public int longestValidParentheses(String s) { + if (s == null || s.length() == 0) + return 0; + int[] dp = new int[s.length()]; + int ans = 0; + for (int i = 1; i < s.length(); i++) { + // 如果是'('直接跳过,默认为0 + if (s.charAt(i) == ')') { + if (s.charAt(i - 1) == '(') + dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; + // 说明s.charAt(i - 1)==')' + else if (i - dp[i - 1] > 0 + && s.charAt(i - dp[i - 1] - 1) == '(') { + dp[i] = (i - dp[i - 1] > 1 + ? dp[i - dp[i - 1] - 2] : 0) + dp[i - 1] + 2; + // 因为加了一个左括号和一个右括号,所以是加2 } - stack.push(op[i][1]); - } else if(type == 2) { - if(stack.peek().equals(minStack.peek())){ - minStack.pop(); - } - stack.pop(); - } else { - ans.add(minStack.peek()); } + ans = Math.max(ans, dp[i]); } - - int[] res = new int[ans.size()]; - for(int i = 0; i < ans.size(); i++){ - res[i] = ans.get(i); - } - return res; + return ans; } } ``` -### LFU缓存结构设计 - ```java -// 解法一 - +import java.util.*; public class Solution { - /** - * lfu design - * @param operators int整型二维数组 ops - * @param k int整型 the k - * @return int整型一维数组 - */ - public int[] LFU (int[][] operators, int k) { - // write code here - if(operators == null) return new int[]{-1}; - HashMap map = new HashMap<>();// key -> value - HashMap count = new HashMap<>(); // key -> count - List list = new ArrayList<>(); - for(int[] ops : operators){ - int type = ops[0]; - int key = ops[1]; - if(type == 1){ - // set操作 - if(map.containsKey(key)){ - map.put(key,ops[2]); - count.put(key,count.get(key)+1); - } else { - if(map.size() == k){ - int minKey = getMinKey(count); - map.remove(minKey); - count.remove(minKey); - } - map.put(key,ops[2]); - if(count.containsKey(key)){ - count.put(key,count.get(key)+1); - } else { - count.put(key,1); - } - } - } else if(type == 2) { - if(map.containsKey(key)){ - int value = map.get(key); - count.put(key,count.get(key)+1); - list.add(value); - } else { - list.add(-1); + public int longestValidParentheses (String s) { + int res = 0; + //记录上一次连续括号结束的位置 + int start = -1; + Stack st = new Stack(); + for(int i = 0; i < s.length(); i++){ + //左括号入栈 + if(s.charAt(i) == '(') + st.push(i); + //右括号 + else{ + //如果右括号时栈为空,不合法,设置为结束位置 + if(st.isEmpty()) + start = i; + else{ + //弹出左括号 + st.pop(); + //栈中还有左括号,说明右括号不够,减去栈顶位置就是长度 + if(!st.empty()) + res = Math.max(res, i - st.peek()); + //栈中没有括号,说明左右括号行号,减去上一次结束的位置就是长度 + else + res = Math.max(res, i - start); } } } - - int[] ans = new int[list.size()]; - for(int i = 0; i < list.size(); i++){ - ans[i] = list.get(i); - } - return ans; - } - - public int getMinKey(HashMap map){ - int minCount = Integer.MAX_VALUE; - int key = 0; - for(Entry entry : map.entrySet()){ - if(entry.getValue() < minCount){ - minCount = entry.getValue(); - key = entry.getKey(); - } - } - return key; + return res; } } +``` -// 解法二 - -import java.util.*; - - -public -class Solution { +### 括号生成 - public class LFUCache { +对于括号的题,核心基本都是: +"一个字符串是合法的括号组合"的*充分必要*条件是: - private class Node { - int k; - int v; - int count; //调用次数 +1. 字符串中开口数等于闭口数 (这是废话) +2. 字符串的所有prefix都满足: 开口数>=闭口数 +举个栗子,比如 "()(())": +prefix: "(", "()", "()(", "()((", "()(()", "()(())". - public Node(int k, int v) { - this.k = k; - this.v = v; - count = 1; - } - } +那么对与这道题,为满足1,2, 每一个位置可以有的permutation就是: - private int size; - private int maxSize; - private Map key2node; - private Map> count2list; //<调用次数,对于调用次数的链表> +1. 如果有多余的开口 -> 可以选开口 +2. 如果有多余未闭合的开口 -> 可以选闭口 - public LFUCache(int maxSize) { - this.maxSize = maxSize; - size = 0; - key2node = new HashMap<>(); - count2list = new HashMap<>(); - } +剩下的就是正常的递归+回溯了 +时间: O(2^n), 每一位最多2个permutation +空间: O(n), 栈高是n - public void set(int k, int v) { - if (key2node.containsKey(k)) { //存在 - key2node.get(k).v = v; - get(k); //get 一次用于改变调用次数 - return; - } - //不存在:建一个新节点 - Node node = new Node(k, v); - //如果调用次数map中不存在,就添加一个新链表 - if (!count2list.containsKey(node.count)) - count2list.put(node.count, new LinkedList<>()); - LinkedList list = count2list.get(node.count); - list.addFirst(node);//插入到该链表的头部,表示该调用次数中,是最近调用的,链表尾才是最久没有调动的 - key2node.put(k, node);//同时加入核心map - size++; - if (size > maxSize) {//超过容量了,删除一个 - key2node.remove(list.getLast().k); - list.removeLast(); - size--; - } - } +```java +import java.util.*; - public int get(int k) { - if (!key2node.containsKey(k)) return -1; - Node node = key2node.get(k);//获取之后 - //还需要更新调用次数: - LinkedList oldList = count2list.get(node.count); - oldList.remove(node);//原来的链表中删除 - if (oldList.isEmpty()) count2list.remove(node.count); - node.count++; - //建立并加入新链表 - if (!count2list.containsKey(node.count)) - count2list.put(node.count, new LinkedList<>()); - LinkedList list = count2list.get(node.count); - list.addFirst(node); - return node.v; - } +public class Solution { + ArrayList ans = new ArrayList<>(); + + public ArrayList generateParenthesis (int n) { + permute(n, n, 0, new StringBuilder()); + return ans; } - - public int[] LFU(int[][] operators, int k) { - LFUCache cache = new LFUCache(k); - List list = new ArrayList<>(); - for (int[] e : operators) { - if (e[0] == 1) cache.set(e[1], e[2]); - else list.add(cache.get(e[1])); - } - int[] res = new int[list.size()]; - for (int i = 0; i < res.length; i++) res[i] = list.get(i); - return res; + + void permute(int open, int close, int unclosedOpen, StringBuilder sb) { + // base case,开口闭口都用完了 + if (open == 0 && close == 0) { + ans.add(sb.toString()); + return; + } + + // always ok to pick an open bracket if there are any open-bracket + if (open > 0) { + sb.append("("); + permute(open-1, close, unclosedOpen+1, sb); + sb.deleteCharAt(sb.length()-1); + } + // can pick close bracket if there is any unclosed open-bracket + if (unclosedOpen > 0) { + sb.append(")"); + permute(open, close-1, unclosedOpen-1, sb); + sb.deleteCharAt(sb.length()-1); + } } } ``` -### N皇后问题 +## 二叉树 + +### 实现二叉树先序,中序和后序遍历 ```java +import java.util.*; + +/* + * public class TreeNode { + * int val = 0; + * TreeNode left = null; + * TreeNode right = null; + * } + */ + public class Solution { /** - * - * @param n int整型 the n - * @return int整型 + * + * @param root TreeNode类 the root of binary tree + * @return int整型二维数组 */ - public int Nqueen (int n) { + public int[][] threeOrders (TreeNode root) { // write code here - List res=new ArrayList<>(); - char[][] chess=new char[n][n]; - for(int i=0;i list1 = new ArrayList<>(); + ArrayList list2 = new ArrayList<>(); + ArrayList list3 = new ArrayList<>(); + front(root,list1,list2,list3); + int[][] ints = new int[3][list1.size()]; + for (int i = 0; i < list1.size(); i++) { + ints[0][i] = list1.get(i); + ints[1][i] = list2.get(i); + ints[2][i] = list3.get(i); } - - backtrack(0,chess,res); - return res.size(); + return ints; } - - // 回溯 - public void backtrack(int row,char[][] chess,List res){ - if(row==chess.length){ - res.add(1); + + public void front(TreeNode root,ArrayList list1, + ArrayList list2,ArrayList list3){ + if(root == null){ return; } - for(int col=0;col=0&&j=0&&i>=0;i--,j--){ - if(chess[i][j]=='Q'){ - return false; - } - } - return true; + + list1.add(root.val); + front(root.left,list1,list2,list3); + list2.add(root.val); + front(root.right,list1,list2,list3); + list3.add(root.val); } } ``` -### 带权值的最小路径和 +- 非递归遍历 + +- 前序遍历 + +用栈来保存信息,但是遍历的时候,是:**先输出根节点信息,然后压入右节点信息,然后再压入左节点信息。** ```java -public class Solution { - /** - * - * @param grid int整型二维数组 - * @return int整型 - */ - public int minPathSum (int[][] grid) { - // write code here - int row = grid.length; - int col = grid[0].length; - int[][] dp = new int[row][col]; - dp[0][0] = grid[0][0]; - for(int i = 1; i < row; i++){ - dp[i][0] = dp[i-1][0] + grid[i][0]; - } - for(int j = 1; j < col; j++){ - dp[0][j] = dp[0][j-1] + grid[0][j]; +public void pre(Node head){ + if(head == null){ + return; + } + Stack stack = new Stack<>(); + stack.push(head); + while(!stack.isEmpty()){ + head = stack.poll(); + System.out.println(head.value + " "); + if(head.right != null){ + stack.push(head.right); } - - for(int i = 1; i < row; i++){ - for(int j = 1; j < col; j++){ - dp[i][j] = Math.min(dp[i][j-1],dp[i-1][j]) + grid[i][j]; - } + if(head.left != null){ + stack.push(head.left); } - - return dp[row-1][col-1]; } + System.out.println(); } ``` -### 反转数字 +- 中序遍历 + +中序遍历的顺序是**左中右**,先一直左节点遍历,并压入栈中,当做节点为空时,输出当前节点,往右节点遍历。 ```java -public class Solution { - /** - * - * @param x int整型 - * @return int整型 - */ - public int reverse (int x) { - // write code here - int res = 0; - while(x != 0){ - // 获取最后一位 - int tail = x % 10; - int newRes = res * 10 + tail; - // 如果不等于,说明溢出 - if((newRes - tail) / 10 != res){ - return 0; - } - res = newRes; - x /= 10; +public void inorder(Node head){ + if(head == null){ + return; + } + Stack stack = new Stack<>(); + stack.push(head); + while(!stack.isEmpty() || head != null){ + if(head != null){ + stack.push(head); + head = head.left + } else { + head = stack.poll(); + System.out.println(head.value + " "); + head = head.right; } - - return res; } + System.out.println(); } ``` -### 二叉搜索树的第k个结点 +- 后序遍历 + +用两个栈来实现,压入栈1的时候为**先左后右**,栈1弹出来就是**中右左**,栈2收集起来就是**左右中**。 ```java -public class Solution { - int index = 0; - TreeNode target = null; - TreeNode KthNode(TreeNode pRoot, int k){ - getKthNode(pRoot,k); - return target; +// 后序遍历-迭代 +public void postIteOrders(TreeNode root, List postList) { + if (root == null) { + return; } - - public void getKthNode(TreeNode pRoot, int k){ - if(pRoot == null){ - return; + // 用两个栈来实现 + // 通过 stack1 和 stack2 来配合可以实现 左 - 右 - 中的顺序 + Stack stack1 = new Stack<>(); + Stack stack2 = new Stack<>(); + stack1.push(root); + while (!stack1.isEmpty()) { + TreeNode node = stack1.pop(); + stack2.push(node); + // 先入左节点 + if (node.left != null) { + stack1.push(node.left); } - getKthNode(pRoot.left,k); - index++; - if(index == k){ - target = pRoot; - return; + // 在入右节点 + if (node.right != null) { + stack1.push(node.right); } - getKthNode(pRoot.right,k); + } + // 弹出元素 + while (!stack2.isEmpty()) { + postList.add(stack2.pop().val); + } +} +``` +### 二叉树的层序遍历 +```java +public ArrayList> levelOrder (TreeNode root) { + // write code here + ArrayList> result = new ArrayList<>(); + if (root == null) { + return result; + } + // 队列,用于存储元素 + Queue queue = new LinkedList<>(); + // 根节点先入队 + queue.offer(root); + // 当队列不为空的时候 + while(!queue.isEmpty()) { + // 队列的大小就是这一层的元素数量 + int size = queue.size(); + ArrayList list = new ArrayList<>(); + // 开始遍历这一层的所有元素 + for (int i = 0; i < size; i ++) { + TreeNode node = queue.poll(); + // 如果左节点不为空,则入队,作为下一层来遍历 + if(node.left != null) { + queue.offer(node.left); + } + // 同上 + if (node.right != null) { + queue.offer(node.right); + } + // 存储一层的节点 + list.add(node.val); + } + // 将一层所有的节点汇入到总的结果集中 + result.add(list); + } + return result; } ``` -### 子数组最大乘积 +### 二叉树的之字形层序遍历 ```java +import java.util.Queue; +import java.util.LinkedList; + public class Solution { - public double maxProduct(double[] arr) { - if(arr.length == 0 || arr == null){ - return 0.0; - } - double[] max = new double[arr.length]; - double[] min = new double[arr.length]; - max[0] = min[0] = arr[0]; - for(int i = 1; i < arr.length; i++){ - max[i] = Math.max(Math.max(max[i-1]*arr[i], - min[i-1]*arr[i]),arr[i]); - min[i] = Math.min(Math.min(max[i-1]*arr[i], - min[i-1]*arr[i]),arr[i]); - } - - double ans = max[0]; - for(int i = 0; i < max.length; i++){ - if(max[i] > ans){ - ans = max[i]; + + public ArrayList> Print(TreeNode root) { + ArrayList> res = new ArrayList<>(); + if (root == null) + return res; + Queue queue = new LinkedList<>(); + queue.add(root); + boolean leftToRight = true; + while (!queue.isEmpty()) { + ArrayList level = new ArrayList<>(); + //统计这一行有多少个节点 + int count = queue.size(); + //遍历这一行的所有节点 + for (int i = 0; i < count; i++) { + //poll移除队列头部元素(队列在头部移除,尾部添加) + TreeNode node = queue.poll(); + //判断是从左往右打印还是从右往左打印。 + if (leftToRight) { + level.add(node.val); + } else { + level.add(0, node.val); + } + //左右子节点如果不为空会被加入到队列中 + if (node.left != null) + queue.add(node.left); + if (node.right != null) + queue.add(node.right); } + res.add(level); + leftToRight = !leftToRight; } - return ans; + return res; } } ``` -### 最长递增子序列 +### 在二叉树中找到两个节点的最近公共祖先 ```java -public int[] LIS (int[] arr) { - // write code here - if(arr == null || arr.length <= 0){ - return null; +import java.util.*; + +public class Solution { + public int lowestCommonAncestor (TreeNode root, int o1, int o2) { + // root为空则说明越过了叶子节点 + if(root == null) return -1; + // 如果root为o1或o2中任意一个,则root就是公共祖先 + if(root.val == o1 || root.val == o2) return root.val; + + //root不为o1或o2 + int left = lowestCommonAncestor(root.left, o1, o2); + int right = lowestCommonAncestor(root.right, o1, o2); + //如果left=-1,说明在左子树中一直找到叶子节点,也没找到最近公共祖先 + //所以最近公共祖先,必在右子树中,right即为最近公共祖先 + if(left == -1) return right; + //同理,最近公共祖先必在左子树中,left即为最近公共祖先 + else if(right == -1) return left; + //若left和right都不为-1,则说明o1,o2节点在root的异侧,则root为最近公共祖先 + else return root.val; } +} +``` - int len = arr.length; - int[] count = new int[len]; // 存长度 - int[] end = new int[len]; // 存最长递增子序列 +### 重建二叉树 - //init - int index = 0; // end 数组下标 - end[index] = arr[0]; - count[0] = 1; +```java - for(int i = 0; i < len; i++){ - if(end[index] < arr[i]){ - end[++index] = arr[i]; - count[i] = index; +import java.util.*; +public class Solution { + public TreeNode reConstructBinaryTree(int [] pre,int [] in) { + if(pre.length == 0||in.length == 0){ + return null; } - else{ - int left = 0, right = index; - while(left <= right){ - int mid = (left + right) >> 1; - if(end[mid] >= arr[i]){ - right = mid - 1; - } - else{ - left = mid + 1; - } + TreeNode node = new TreeNode(pre[0]); + for(int i = 0; i < in.length; i++){ + if(pre[0] == in[i]){ + node.left = reConstructBinaryTree( + Arrays.copyOfRange(pre, 1, i+1), + Arrays.copyOfRange(in, 0, i)); + node.right = reConstructBinaryTree( + Arrays.copyOfRange(pre, i+1, pre.length), + Arrays.copyOfRange(in, i+1,in.length)); } - end[left] = arr[i]; - count[i] = left; } + return node; } +} +``` - //因为返回的数组要求是字典序,所以从后向前遍历 - int[] res = new int[index + 1]; - for(int i = len - 1; i >= 0; i--){ - if(count[i] == index){ - res[index--] = arr[i]; +```java +public TreeNode reConstructBinaryTree(int [] pre, int [] in) { + TreeNode root = rebuild(pre, 0, pre.length - 1, + in, 0, in.length - 1); + return root; +} + +public TreeNode rebuild(int[] preorder, int preStart, + int preEnd, int[] inorder, int inStart, int inEnd) { + if (preStart < 0 || inStart < 0 || + preStart > preEnd || inStart > inEnd) { + return null; + } + TreeNode root = new TreeNode(preorder[preStart]); + + int index = 0; + for (int i = 0; i < inorder.length; i++) { + if (inorder[i] == root.val) { + index = i; + break; } } - return res; + int leftLen = index - inStart; + int rightLen = inEnd - index; + root.left = rebuild(preorder, preStart + 1, + leftLen + preStart, inorder, inStart, index - 1); + root.right = rebuild(preorder, leftLen + preStart + 1, + preEnd, inorder, index + 1, inEnd); + return root; } ``` -### 在两个长度相等的排序数组中找到上中位数 +### 输出二叉树的右视图(先重建,再输出右视图) ```java -public int findMedianinTwoSortedAray (int[] arr1, int[] arr2) { - // write code here - int n = arr1.length; - if(n==0){ - return 0; - } - //arr1左右两端 - int l1=0,r1=n-1; - //arr2左右两端 - int l2=0,r2=n-1; - int mid1,mid2; - //终止条件为l1=r1,即两个数组都只有一个元素,此时的上中位数为两数的最小值 - while(l1< r1){ - //arr1中位数 - mid1 = l1+((r1-l1)>>1); - //arr2中位数 - mid2 = l2+((r2-l2)>>1); - int k = r1-l1+1; - if(arr1[mid1] == arr2[mid2]){ //若两数组中位数相等,整体中位数也是这个 - return arr1[mid1]; +public class Solution { + /** + * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 + * 求二叉树的右视图 + * @param xianxu int整型一维数组 先序遍历 + * @param zhongxu int整型一维数组 中序遍历 + * @return int整型一维数组 + */ + public int[] solve (int[] preorder, int[] inorder) { + // write code here + if (preorder == null || preorder.length < 1 + || inorder == null || inorder.length < 1) { + return new int[0]; } - else if(arr1[mid1] > arr2[mid2]){ - if(k%2 == 0){//区间元素个数为偶数 - r1 = mid1; //整体中位数在arr1左区间,包括mid1 - l2 = mid2+1; //整体中位数在arr2右区间,不包括mid2 - } - else if(k%2 == 1){ //区间元素个数为奇数 - r1 = mid1; //整体中位数在arr1左区间,包括mid1 - l2 = mid2; //整体中位数在arr2右区间,包括mid2 + TreeNode root = rebuild(preorder, 0, preorder.length - 1, + inorder, 0, inorder.length - 1); + LinkedList queue = new LinkedList<>(); + TreeNode cur = root; + queue.offer(cur); + List list = new ArrayList<>(); + while (!queue.isEmpty()) { + int size = queue.size(); + list.add(queue.peekLast().val); + for (int i = 0; i < size; i++) { + cur = queue.poll(); + if (cur.left != null) { + queue.offer(cur.left); + } + if (cur.right != null) { + queue.offer(cur.right); + } } } - else if (arr1[mid1] < arr2[mid2]){ - if(k%2 == 0){//区间元素个数为偶数 - r2 = mid2; //整体中位数在arr2左区间,包括mid2 - l1 = mid1+1; //整体中位数在arr1右区间,不包括mid1 - } - else if(k%2 == 1){ //区间元素个数为奇数 - r2 = mid2; //整体中位数在arr2左区间,包括mid2 - l1 = mid1; //整体中位数在arr1右区间,包括mid1 + int[] res = new int[list.size()]; + for (int i = 0; i < res.length; i++) { + res[i] = list.get(i); + } + return res; + } + + public TreeNode rebuild(int[] preorder, int preStart, + int preEnd, int[] inorder, int inStart, int inEnd) { + if (preStart < 0 || inStart < 0 || + preStart > preEnd || inStart > inEnd) { + return null; + } + TreeNode root = new TreeNode(preorder[preStart]); + + int index = 0; + for (int i = 0; i < inorder.length; i++) { + if (inorder[i] == root.val) { + index = i; + break; } } + int leftLen = index - inStart; + int rightLen = inEnd - index; + root.left = rebuild(preorder, preStart + 1, + leftLen + preStart, inorder, inStart, index - 1); + root.right = rebuild(preorder, leftLen + preStart + 1, + preEnd, inorder, index + 1, inEnd); + return root; + } + + public static class TreeNode { + public int val; + public TreeNode left; + public TreeNode right; + + public TreeNode(int val) { + this.val = val; + } } - //当区间内只有一个元素时,两个区间中最小值即为整体中位数 - return Math.min(arr1[l1],arr2[l2]); } ``` -### 判断t1树中是否有与t2树拓扑结构完全相同的子树 +### 二叉树的最大深度 ```java -/** - * - * @param root1 TreeNode类 - * @param root2 TreeNode类 - * @return bool布尔型 - */ -public boolean isContains (TreeNode root1, TreeNode root2) { - // write code here - if(root1 == null || root2 == null){ - return false; - } - return recur(root1, root2) || isContains(root1.left,root2) - || isContains(root1.right,root2); +public int maxDepth(TreeNode root) { + return root==null? 0 : + Math.max(maxDepth(root.left), maxDepth(root.right))+1; } +``` -public boolean recur(TreeNode root1, TreeNode root2){ - if(root2 == null){ - return true; +### 判断是不是平衡二叉树 + +```java +import java.util.*; +public class Solution { + public boolean IsBalanced_Solution(TreeNode root) { + //可以分别求出左右子树的高度,然后进行对比 + return TreeDepth(root) >= 0; } - if(root1 == null || root1.val != root2.val){ - return false; + //求二叉树深度的方法 + public int TreeDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftHeight = TreeDepth(root.left); + int rightHeight = TreeDepth(root.right); + if (leftHeight == -1 || rightHeight == -1 + || Math.abs(leftHeight - rightHeight) > 1) { + return -1; + } else { + return Math.max(leftHeight, rightHeight) + 1; + } } - return recur(root1.left,root2.left) && - recur(root1.right,root2.right); } ``` -### 反转字符串 +### 二叉树根节点到叶子节点的所有路径和 ```java -/** -* 反转字符串 -* @param str string字符串 -* @return string字符串 -*/ -public String solve (String str) { - // write code here - if(str == null){ - return null; +public class Solution { + /** + * + * @param root TreeNode类 + * @return int整型 + */ + public int sumNumbers (TreeNode root) { + // 调用dfs + return dfs(root,0); } - char[] c = new char[str.length()]; - int left = 0, right = str.length() - 1; - - while(left <= right){ - c[left] = str.charAt(right); - c[right] = str.charAt(left); - left++; - right--; + //深度优先搜索 + public int dfs(TreeNode root,int sum){ + if(root==null){ + return 0; + } + int total = sum*10+root.val; + //已达到叶子节点,返回结果 + if(root.left==null && root.right==null){ + return total; + }else{ + //递归调用 + return dfs(root.left,total)+dfs(root.right,total); + } + } - return new String(c); } ``` -### 最大正方形 +### 二叉树中和为某一值的路径,返回所有路径 ```java -/** -* 最大正方形 -* @param matrix char字符型二维数组 -* @return int整型 -*/ -public int solve (char[][] matrix) { - // write code here - int m = matrix.length; - int n = matrix[0].length; - int dp[][] = new int [m][n]; - for(int i = 0; i < m; i++){ - dp[i][0] = matrix[i][0] - '0'; +import java.util.ArrayList; +public class Solution { + ArrayList> lists = new ArrayList<>(); + ArrayList list = new ArrayList<>(); + public ArrayList> FindPath(TreeNode root,int target) { + if(root == null){ + return lists; } - for(int j = 0; j < n; j++){ - dp[0][j] = matrix[0][j] - '0'; - } - int max = 0; - for(int i = 1; i < m; i++){ - for(int j = 1; j < n; j++){ - if(matrix[i][j] == '1'){ - dp[i][j] = Math.min(Math.min(dp[i-1][j-1], - dp[i][j-1]),dp[i-1][j]) + 1; - max = Math.max(max,dp[i][j]); - } + list.add(root.val); + target -= root.val; + if(target == 0 && root.left == null + && root.right == null){ + lists.add(new ArrayList(list)); } + FindPath(root.left,target); + FindPath(root.right,target); + list.remove(list.size() - 1); + return lists; } - return max*max; } ``` -### 链表中的节点每K个一组翻转 +### 判断一棵二叉树是否为搜索二叉树和完全二叉树 ```java -//明显递归解决,翻转第一组之后,以第二组的开头为头节点,继续翻转,转翻到最后,返回。 -public ListNode reverseKGroup(ListNode head, int k) { - if(head==null||head.next==null) - return head; - ListNode h=new ListNode(0); - h.next=head; - ListNode next=null,tmp=head,cur=head; - for(int i=1;i=root.val){ + return false; } - head.next=reverseKGroup(next,k); - return h.next; + num=root.val; + boolean right = isSearch(root.right); + return left && right; } -``` -### 数组中的最长无重复子串的长度 - -```java -public int maxLength (int[] arr) { - int left = 0, right = 0; - Set set = new HashSet<>(); - int res = 1; - while(right < arr.length){ - if(!set.contains(arr[right])){ - set.add(arr[right]); - right++; +public boolean isFull(TreeNode root){ + Queue queue = new LinkedList<>(); + queue.add(root); + while(!queue.isEmpty()){ + TreeNode node = queue.poll(); + if(node==null){ + flag=true; }else{ - set.remove(arr[left]); - left++; + if(flag){ + return false; + }else{ + queue.add(node.left); + queue.add(node.right); + } } - res = Math.max(res, set.size()); } - return res; + return true; } ``` -### 判断链表是否为回文结构 +### 二叉树的最大路径和 ```java -import java.util.*; - public class Solution { + int max = Integer.MIN_VALUE; /** - * - * @param head ListNode类 the head - * @return bool布尔型 + * + * @param root TreeNode类 + * @return int整型 */ - public boolean isPail (ListNode head) { - ListNode slow = head; - ListNode fast = head; - while(fast != null && fast.next != null){ - fast = fast.next.next; - slow = slow.next; + public int maxPathSum (TreeNode root) { + // write code here + maxSum(root); + return max; + } + + public int maxSum(TreeNode root){ + if(root == null){ + return 0; } - Stack stack = new Stack<>(); - while(slow != null){ - stack.add(slow.val); - slow = slow.next; - } + //三种情况:1.包含一个子树和顶点,2.仅包含顶点,3.包含左子树和右子树以及顶点。 + int left = Math.max(maxSum(root.left),0); + int right = Math.max(maxSum(root.right),0); - while(!stack.isEmpty()){ - if(stack.pop() != head.val){ - return false; - } - - head = head.next; - } + max = Math.max(max,left+right+root.val); - return true; + //对于每一个子树,返回包含该子树顶点的深度方向的路径和的最大值。 + return root.val + Math.max(left,right); } } ``` -### 岛屿的数量 +### 判断二叉树是否对称 ```java -class Solution { - public int numIslands(char[][] grid) { - int count = 0; - for(int i = 0; i < grid.length; i++) { - for(int j = 0; j < grid[0].length; j++) { - if(grid[i][j] == '1'){ - bfs(grid, i, j); - count++; - } - } - } - return count; +public class Solution { + /** + * + * @param root TreeNode类 + * @return bool布尔型 + */ + public boolean isSymmetric (TreeNode root) { + // write code here + return isSymmetricNode(root,root); } - public void bfs(char[][] grid, int i, int j){ - Queue list = new LinkedList<>(); - list.add(new int[] { i, j }); - while(!list.isEmpty()){ - int[] cur = list.remove(); - i = cur[0]; j = cur[1]; - if(inArea(i,j,grid) && grid[i][j] == '1') { - grid[i][j] = '0'; - list.add(new int[] { i + 1, j }); - list.add(new int[] { i - 1, j }); - list.add(new int[] { i, j + 1 }); - list.add(new int[] { i, j - 1 }); - } + public boolean isSymmetricNode(TreeNode node1, TreeNode node2){ + if(node1 == null && node2 == null){ + return true; } - } - - public boolean inArea(int i, int j, char[][] grid){ - return i >=0 && j >= 0 && i < grid.length - && j < grid[0].length; + if(node1 == null || node2 == null){ + return false; + } + if(node1.val != node2.val){ + return false; + } + return isSymmetricNode(node1.left,node2.right) + && isSymmetricNode(node1.right,node2.left); } } ``` -### 在二叉树中找到两个节点的最近公共祖先 +### 二叉树中是否存在节点和为指定值的路径 ```java public class Solution { /** * * @param root TreeNode类 - * @param o1 int整型 - * @param o2 int整型 - * @return int整型 + * @param sum int整型 + * @return bool布尔型 */ - public int lowestCommonAncestor (TreeNode root, int o1, int o2) { - if (root == null) { - return 0; - } - if (root.val == o1 || root.val == o2) { - return root.val; - } - int left = lowestCommonAncestor(root.left, o1, o2); - int right = lowestCommonAncestor(root.right, o1, o2); - if (left != 0 && right != 0) { - return root.val; - } - if (left == 0) { - return right; + public boolean hasPathSum (TreeNode root, int sum) { + // write code here + if(root == null){ + return false; } - if (right == 0) { - return left; + + if(root.left == null && root.right == null){ + return sum - root.val == 0; } - return 0; + + return hasPathSum(root.left,sum - root.val) || + hasPathSum(root.right,sum - root.val); } } ``` -### 重复项数字的所有排列 +### 序列化二叉树 ```java -public class Solution { - private ArrayList> res; - private boolean[] visited; - - public ArrayList> permute(int[] nums) { +import java.util.*; +/* +public class TreeNode { + int val = 0; + TreeNode left = null; + TreeNode right = null; - res = new ArrayList<>(); - visited = new boolean[nums.length]; - List list = new ArrayList<>(); - backtrace(nums, list); + public TreeNode(int val) { + this.val = val; - return res; } - private void backtrace(int[] nums, List list) { - - if (list.size() == nums.length) { - res.add(new ArrayList<>(list)); - } - - for (int i = 0; i < nums.length; i++) { - - if (visited[i]) continue; +} +*/ +public class Solution { + private int index = -1; - visited[i] = true; - list.add(nums[i]); + public String Serialize(TreeNode root) { + StringBuilder builder = new StringBuilder(); + intervalSerialize(builder, root); + return builder.toString(); + } - backtrace(nums, list); + private void intervalSerialize(StringBuilder builder, TreeNode root) { + if (root == null) { + builder.append("#,"); + return; + } + builder.append(root.val).append(","); + intervalSerialize(builder, root.left); + intervalSerialize(builder, root.right); + } - visited[i] = false; - list.remove(list.size() - 1); + public TreeNode Deserialize(String str) { + if (str == null || str.isEmpty()) { + return null; + } + String[] words = str.split(","); + return internalDeserialize(words); + } + private TreeNode internalDeserialize(String[] words) { + index++; + if (index == words.length) { + return null; + } + if ("#".equals(words[index])) { + return null; } + TreeNode root = new TreeNode(Integer.parseInt(words[index])); + root.left = internalDeserialize(words); + root.right = internalDeserialize(words); + return root; } } ``` -### 最长回文子串的长度 +### 二叉搜索树的第k个结点 ```java -public class Palindrome { - public int getLongestPalindrome(String A, int n) { - // write code here - int max=0; - for (int i=1;i=0&&right=0&&right > Print(TreeNode pRoot) { + if(pRoot == null){ + return new ArrayList>(); + } + ArrayList> list = new ArrayList<>(); + + Queue queue = new LinkedList<>(); + queue.add(pRoot); + while(!queue.isEmpty()){ + ArrayList temp = new ArrayList<>(); + for(int i = queue.size(); i > 0; i--){ + TreeNode node = queue.poll(); + temp.add(node.val); + if(node.left != null){ + queue.add(node.left); + } + if(node.right != null){ + queue.add(node.right); } } + list.add(temp); } - return res.toString(); + + return list; } + } ``` -### 最小编辑代价 +### 二叉树的镜像 ```java -import java.util.*; - - public class Solution { - /** - * min edit cost - * @param str1 string字符串 the string - * @param str2 string字符串 the string - * @param ic int整型 insert cost - * @param dc int整型 delete cost - * @param rc int整型 replace cost - * @return int整型 - */ - public int minEditCost (String str1, String str2, - int ic, int dc, int rc) { - // write code here - int len1=str1.length(); - int len2=str2.length(); - char[] char1=str1.toCharArray(); - char[] char2=str2.toCharArray(); - int[][] dp=new int[len1+1][len2+1]; - for(int i=1;i<=len1;i++){ - dp[i][0]=dp[i-1][0]+dc; + public void Mirror(TreeNode root) { + if(root == null){ + return; } - for(int i=1;i<=len2;i++){ - dp[0][i]=dp[0][i-1]+ic; + if(root.left == null && root.right == null){ + return; } - for(int i=0;i stack = new Stack<>(); + stack.push(root); + while(!stack.isEmpty()){ + TreeNode node = stack.pop(); + + if(node.left != null || node.right != null){ + TreeNode temp = node.left; + node.left = node.right; + node.right = temp; + } + + if(node.left != null){ + stack.push(node.left); + } + + if(node.right != null){ + stack.push(node.right); } } - return dp[len1][len2]; } } + +/* +public class Solution { + public void Mirror(TreeNode root) { + if(root == null){ + return; + } + if(root.left == null && root.right == null){ + return; + } + + TreeNode temp = root.left; + root.left = root.right; + root.right = temp; + + if(root.left != null){ + Mirror(root.left); + } + + if(root.right != null){ + Mirror(root.right); + } + } +} +*/ + ``` -### 矩阵的最小路径和 +### 判断t1树中是否有与t2树拓扑结构完全相同的子树 ```java -import java.util.*; - +/** + * + * @param root1 TreeNode类 + * @param root2 TreeNode类 + * @return bool布尔型 + */ +public boolean isContains (TreeNode root1, TreeNode root2) { + // write code here + if(root1 == null || root2 == null){ + return false; + } + return recur(root1, root2) || isContains(root1.left,root2) + || isContains(root1.right,root2); +} -public class Solution { - /** - * - * @param matrix int整型二维数组 the matrix - * @return int整型 - */ - public int minPathSum (int[][] matrix) { - // write code here - int m = matrix.length, n = matrix[0].length; - if (m == 0 || n == 0) return 0; - - for (int i = 1; i < m; i++) - matrix[i][0] += matrix[i-1][0]; - for (int i = 1; i < n; i++) - matrix[0][i] += matrix[0][i-1]; - - for (int i = 1; i < m; i++) { - for (int j = 1; j < n; j++) { - matrix[i][j] += - Math.min(matrix[i-1][j], matrix[i][j-1]); - } - } - return matrix[m-1][n-1]; +public boolean recur(TreeNode root1, TreeNode root2){ + if(root2 == null){ + return true; } + if(root1 == null || root1.val != root2.val){ + return false; + } + return recur(root1.left,root2.left) && + recur(root1.right,root2.right); } ``` -### 顺时针旋转数组 +### 合并二叉树 ```java -// 找规律:mat[i][j]被旋转到了mat[j][n-i-1]的位置 -public class Rotate { - public int[][] rotateMatrix(int[][] mat, int n) { - // write code here - int[][] temp=new int[n][n]; - - for(int i=0;i=max){ - return false; + String[] res=new String[len]; + Trie trie=new Trie(); + int id=0; + + for(String[] opera:operators){ + if(opera[0].equals("1")){ + //添加单词 + trie.insert(opera[1]); + } + else if(opera[0].equals("2")){ + //删除单词 + trie.delete(opera[1]); + } + else if(opera[0].equals("3")){ + //查询单词是否存在 + res[id++]=trie.search(opera[1])?"YES":"NO"; + } + else if(opera[0].equals("4")){ + //查找以word为前缀的单词数量 + String preNumber=String.valueOf(trie.prefixNumber(opera[1])); + res[id++]=preNumber; + } } - return isBST(root.left,min,root.val) && isBST(root.right,root.val,max); + return res; } - - - private boolean isComplete(TreeNode root){ - Queue queue = new LinkedList<>(); - queue.offer(root); - - while(!queue.isEmpty()){ - TreeNode tmp = queue.poll(); - if(tmp == null){ - break; + + class Trie{ + //构建字典树节点 + class TrieNode{ + //child数组记录所有子节点 + TrieNode[] child; + //pre_number表示插入单词时,当前节点被访问次数 + int pre_number; + //end表示当前节点是否是某个单词的末尾 + boolean end; + TrieNode(){ + child=new TrieNode[26]; + pre_number=0; + end=false; } - queue.offer(tmp.left); - queue.offer(tmp.right); } - while(!queue.isEmpty()){ - if(queue.poll() != null){ - return false; + + Trie(){} + + //初始化根节点 + TrieNode root=new TrieNode(); + + //添加单词 + void insert(String word){ + TrieNode node=root; + char[] arr=word.toCharArray(); + for(char c:arr){ + //如果子节点不存在,则新建 + if(node.child[c-'a']==null){ + node.child[c-'a']=new TrieNode(); + } + //往子节点方向移动 + node=node.child[c-'a']; + node.pre_number++; } + node.end=true; + } + + void delete(String word){ + TrieNode node=root; + char[] arr=word.toCharArray(); + for(char c:arr){ + //往子节点方向移动,将访问次数减一 + node=node.child[c-'a']; + node.pre_number--; + } + //如果访问次数为0,说明不存在该单词为前缀的单词,以及该单词 + if(node.pre_number==0){ + node.end=false; + } + } + + boolean search(String word){ + TrieNode node=root; + char[] arr=word.toCharArray(); + for(char c:arr){ + //如果子节点不存在,说明不存在该单词 + if(node.child[c-'a']==null){ + return false; + } + node=node.child[c-'a']; + } + + //如果前面的节点都存在,并且该节点末尾标识为true,则存在该单词 + return node.end; + } + + int prefixNumber(String pre){ + TrieNode node=root; + char[] arr=pre.toCharArray(); + for(char c:arr){ + //如果子节点不存在,说明不存在该前缀 + if(node.child[c-'a']==null){ + return 0; + } + node=node.child[c-'a']; + } + + //返回以该单词为前缀的数量 + return node.pre_number; } - return true; } } ``` -### [连续子数组的最大和(sum < 0置为0)](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&&tqId=11183&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) +### 找到二叉搜索树中的两个错误节点 + +![](http://image.ouyangsihai.cn/FgMj1e8uJv5aZqSy__ZFJ2aN4cCZ) ```java +import java.util.*; +public class Solution { + /** + * + * @param root TreeNode类 the root + * @return int整型一维数组 + */ -// dp -public int FindGreatestSumOfSubArray(int[] array) { - if(array.length == 0){ - return 0; - } - - int max = Integer.MIN_VALUE; - int[] dp = new int[array.length]; - for(int i = 0; i < array.length; i++){ - dp[i] = array[i]; - } - for(int i = 1; i < array.length; i++){ - dp[i] = Math.max(dp[i-1] + array[i],dp[i]); - } - - for(int i = 0; i < dp.length; i++){ - if(dp[i] > max){ - max = dp[i]; + //存储结果集的二维数组 + int[] result = new int[2]; + int index = 1; + TreeNode preNode; + public int[] findError (TreeNode root) { + // 特判 + if(root == null) { + return result; } + // 递归左子树,寻找该树符合条件的节点 + findError(root.left); + if(preNode == null) { + preNode = root; + } + // 判断是否是出错的节点 + if(index == 1 && root.val < preNode.val) { + result[index] = preNode.val; + index--; + } + if(index == 0 && root.val < preNode.val) { + result[index] = root.val; + } + preNode = root; + // 递归右子树,寻找该树符合条件的节点 + findError(root.right); + return result; } - return max; -} - -// sum < 0置为0 -public int FindGreatestSumOfSubArray(int[] array) { - if(array.length == 0){ - return 0; - } - - int max = Integer.MIN_VALUE; - int cur = 0; - for(int i = 0; i < array.length; i++){ - cur += array[i]; - max = Math.max(max,cur); - cur = cur < 0 ? 0 : cur; - } - return max; } - ``` -### 两数之和 +## 堆 + +### 最小的K个数 ```java -import java.util.HashMap; +import java.util.*; public class Solution { - public int[] twoSum(int[] nums, int target) { - HashMap map = new HashMap<>(); - for (int i = 0; i < nums.length; i++) { - if (map.containsKey(nums[i])){ - return new int[]{map.get(nums[i])+1,i+1}; + public ArrayList GetLeastNumbers_Solution(int [] input, int k) { + ArrayList res = new ArrayList(); + //排除特殊情况 + if(k == 0 || input.length == 0) + return res; + //大根堆 + PriorityQueue q = + new PriorityQueue<>((o1, o2)->o2.compareTo(o1)); + //构建一个k个大小的堆 + for(int i = 0; i < k; i++) + q.offer(input[i]); + for(int i = k; i < input.length; i++){ + //较小元素入堆 + if(q.peek() > input[i]){ + q.poll(); + q.offer(input[i]); } - map.put(target - nums[i],i); } - return null; + //堆中元素取出入数组 + for(int i = 0; i < k; i++) + res.add(q.poll()); + return res; } } -``` - -### 删除有序链表中重复出现的元素 - -```java -public ListNode deleteDuplicates (ListNode head) { - ListNode dummy=new ListNode(0); - dummy.next=head; - ListNode pre=dummy; - ListNode p=head; - while(p!=null&&p.next!=null){ - if(p.val==p.next.val){ - while(p.next!=null&&p.val==p.next.val){ - p=p.next; +// 自己实现堆排序 +public class Solution { + public ArrayList + GetLeastNumbers_Solution(int [] input, int k) { + ArrayList list = new ArrayList<>(); + if (input == null || input.length == 0 + || k > input.length || k == 0) + return list; + int[] arr = new int[k + 1];//数组下标0的位置作为哨兵,不存储数据 + //初始化数组 + for (int i = 1; i < k + 1; i++) + arr[i] = input[i - 1]; + buildMaxHeap(arr, k + 1);//构造大根堆 + for (int i = k; i < input.length; i++) { + if (input[i] < arr[1]) { + arr[1] = input[i]; + adjustDown(arr, 1, k + 1);//将改变了根节点的二叉树继续调整为大根堆 } - pre.next=p.next; - p=p.next; - } - else{ - pre=p; - p=p.next; - } + } + for (int i = 1; i < arr.length; i++) { + list.add(arr[i]); + } + return list; + } + /** + * @Author: ZwZ + * @Description: 构造大根堆 + * @Param: [arr, length] length:数组长度 作为是否跳出循环的条件 + * @return: void + * @Date: 2020/1/30-22:06 + */ + public void buildMaxHeap(int[] arr, int length) { + if (arr == null || arr.length == 0 || arr.length == 1) + return; + for (int i = (length - 1) / 2; i > 0; i--) { + adjustDown(arr, i, arr.length); + } + } + /** + * @Author: ZwZ + * @Description: 堆排序中对一个子二叉树进行堆排序 + * @Param: [arr, k, length] + * @return: + * @Date: 2020/1/30-21:55 + */ + public void adjustDown(int[] arr, int k, int length) { + arr[0] = arr[k];//哨兵 + for (int i = 2 * k; i <= length; i *= 2) { + if (i < length - 1 && arr[i] < arr[i + 1]) + i++;//取k较大的子结点的下标 + if (i > length - 1 || arr[0] >= arr[i]) + break; + else { + arr[k] = arr[i]; + k = i; //向下筛选 + } + } + arr[k] = arr[0]; } - return dummy.next; } ``` -### 在转动过的有序数组中寻找目标值 +### 字符串出现次数的TopK问题 ```java -public int search (int[] a, int target) { - // write code here - if(a==null||a.length<=0){ - return -1; - } - int low = 0; - int high = a.length-1; - while(low<=high){ - int mid = low+(high-low)/2; - if(a[mid]==target){ - return mid; - }else if(a[mid] map = new HashMap<>(); + for(int i = 0; i < strings.length; i++){ + if(map.containsKey(strings[i])){ + map.put(strings[i], map.get(strings[i]) + 1); }else{ - high = mid-1; + map.put(strings[i], 1); } - }else{ - if(a[low]<=target&&target> pq = + new PriorityQueue<>((o1,o2) -> o1.getValue().equals(o2.getValue()) + ? o2.getKey().compareTo(o1.getKey()) : o1.getValue()-o2.getValue()); + int size = 0; + //维护size为k的小根堆 + for(Map.Entry m : map.entrySet()){ + if(size < k){ + pq.offer(m); + size++; + } + //大于堆顶元素插入 + else if((m.getValue().equals(pq.peek().getValue()) + ? pq.peek().getKey().compareTo(m.getKey()) + : m.getValue() - pq.peek().getValue()) > 0){ + pq.poll(); + pq.offer(m); } } + //取出堆中元素,从后向前放置 + for(int i = k - 1; i >= 0; i--){ + Map.Entry entry =(Map.Entry)pq.poll(); + res[i][0] = entry.getKey(); + res[i][1] = String.valueOf(entry.getValue()); + } + return res; } - return -1; } ``` -### 数组中未出现的最小正整数 +### 寻找第K大 + +```java +public int findKth(int[] a, int n, int K){ + // 暂存K个较大的值,优先队列默认是自然排序(升序), + // 队头元素(根)是堆内的最小元素,也就是小根堆 + PriorityQueue queue = new PriorityQueue<>(K); + // 遍历每一个元素,调整小根堆 + for (int num : a) { + // 对于小根堆来说,只要没满就可以加入(不需要比较); + // 如果满了,才判断是否需要替换第一个元素 + if (queue.size() < K) { + queue.add(num); + } else { + // 在小根堆内,存储着K个较大的元素,根是这K个中最小的, + // 如果出现比根还要大的元素,说明可以替换根 + if (num > queue.peek()) { + queue.poll(); // 高个中挑矮个,矮个淘汰 + queue.add(num); + } + } + } + return queue.isEmpty() ? 0 : queue.peek(); +} +``` ```java -public int minNumberdisappered (int[] arr) { - int n=arr.length; - for(int i=0;i=1&&arr[i]<=n&&arr[arr[i]-1]!=arr[i]){ - swap(arr,arr[i]-1,i); +import java.util.*; + +public class Finder { + public int findKth(int[] a, int n, int K) { + // write code here + return find(a, 0, n-1, K); + } + + public int find(int[] a, int low, int high, int K){ + int pivot = partition(a, low, high); + + if(pivot + 1 < K){ + return find(a, pivot + 1, high, K); + } else if(pivot + 1 > K){ + return find(a, low, pivot - 1, K); + } else { + return a[pivot]; } } - for(int i=0;i arr[endIndex]) { + swap(arr,++small, i); + } } + swap(arr,++small,endIndex); + return small; + } + + public void swap(int[] arr, int i, int j){ + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; } - return n+1; -} -private void swap(int[] arr,int i,int j){ - int temp=arr[i]; - arr[i]=arr[j]; - arr[j]=temp; } + ``` -### 数组中最长连续子序列 +## 双指针 + +### 最长无重复子数组的长度 ```java -public int MLS (int[] arr) { - if(arr==null || arr.length==0) +// 方法1 +public int maxLength (int[] arr) { + int left = 0, right = 0; + Set set = new HashSet<>(); + int res = 1; + while(right < arr.length){ + if(!set.contains(arr[right])){ + set.add(arr[right]); + right++; + }else{ + set.remove(arr[left]); + left++; + } + res = Math.max(res, set.size()); + } + return res; +} + +// 方法2 +public int maxLength(int[] arr) { + if (arr.length == 0) return 0; - if(arr.length==1) - return 1; - Arrays.sort(arr); - int max = -1; - int count = 1; - for(int i = 0;i map = new HashMap<>(); + int max = 0; + for (int i = 0, j = 0; i < arr.length; ++i) { + if (map.containsKey(arr[i])) { + j = Math.max(j, map.get(arr[i]) + 1); } + map.put(arr[i], i); + max = Math.max(max, i - j + 1); } - max=max maxInWindows(int [] num, int size) + { + ArrayList res = new ArrayList<>(); + if(size == 0) return res; + int begin; + ArrayDeque q = new ArrayDeque<>(); + for(int i = 0; i < num.length; i++){ + begin = i - size + 1; + if(q.isEmpty()) + q.add(i); + else if(begin > q.peekFirst()) + q.pollFirst(); + + while((!q.isEmpty()) && num[q.peekLast()] <= num[i]) + q.pollLast(); + q.add(i); + if(begin >= 0) + res.add(num[q.peekFirst()]); + } + return res; + } +} +``` + +### 合并区间(区间重叠) + +首先将各个区间进行排序,排序规则为首先根据start进行排序, +如果start相等则根据end从小到大排序new一个新的List result存放结果, +遍历给定的intervals,比较当前interval的start是否大于result中最后一个元素的end, +若大于,说明从开了一个区间,若区间有重叠,则更新result中最后一个元素的end。 + +```java +import java.util.*; +/** + * Definition for an interval. + * public class Interval { + * int start; + * int end; + * Interval() { start = 0; end = 0; } + * Interval(int s, int e) { start = s; end = e; } + * } + */ +public class Solution { + public ArrayList merge(ArrayList intervals) { + // 首先根据start排序,如果start相等,根据end排序 + Collections.sort(intervals, + (o1, o2) -> (o1.start != o2.start + ? o1.start - o2.start : o1.end - o2.end)); + ArrayList result = new ArrayList<>(); + if(intervals.size() == 0) { + return result; + } + // 放入第一个区间 + result.add(intervals.get(0)); + int count = 0; + // 遍历后续区间,查看是否与末尾有重叠 + for(int i = 1; i < intervals.size(); i++) { + Interval o1 = intervals.get(i); + Interval origin = result.get(result.size() - 1); + // 如果当前Interval的start比List里面最后一个元素的end大,说明从开一个区间 + if(o1.start > origin.end) { + result.add(o1); + } else { // 区间有重叠,更新结尾 + if(o1.end > origin.end) { + result.get(result.size() - 1).end = o1.end; + } + } + } + return result; + } +} +``` + +### 反转字符串 ```java +import java.util.*; public class Solution { /** - * - * @param root TreeNode类 - * @return bool布尔型 + * 反转字符串 + * @param str string字符串 + * @return string字符串 */ - public boolean isSymmetric (TreeNode root) { + public String solve (String str) { // write code here - return isSymmetricNode(root,root); - } - - public boolean isSymmetricNode(TreeNode node1, TreeNode node2){ - if(node1 == null && node2 == null){ - return true; - } - if(node1 == null || node2 == null){ - return false; + if(str == null){ + return null; } - if(node1.val != node2.val){ - return false; + char[] c = new char[str.length()]; + int left = 0, right = str.length() - 1; + + while(left <= right){ + c[left] = str.charAt(right); + c[right] = str.charAt(left); + left++; + right--; } - return isSymmetricNode(node1.left,node2.right) - && isSymmetricNode(node1.right,node2.left); + return new String(c); } } ``` -### 没有重复项数字的所有排列 +### 数组中相加和为0的三元组 ```java -public class Demo1 { - ArrayList> res; - - public ArrayList> permute(int[] nums) { - res = new ArrayList>(); - if (nums == null || nums.length < 1) - return res; - //对数组元素进行从小到大排序 - Arrays.sort(nums); - ArrayList list = new ArrayList(); +import java.util.*; + +public class Solution { + public ArrayList> threeSum(int[] num) { + ArrayList> list = new ArrayList<>(); + Arrays.sort(num); + int left,right,sum; + for(int i = 0; i < num.length - 2; i++){ + if(i > 0 && num[i] == num[i-1]) continue; + left = i + 1; + right = num.length - 1; + while(left < right){ + sum = num[i] + num[left] + num[right]; + if(sum == 0){ + ArrayList temp = new ArrayList<>(); + temp.add(num[i]); + temp.add(num[left]); + temp.add(num[right]); + list.add(temp); + right--; + left++; + while(left < right && num[left] == num[left-1]){ + left++; + } + while(left < right && num[right] == num[right+1]){ + right--; + } + } else if(sum < 0){ + left++; + } else { + right--; + } + } + } + return list; + } +} +``` - solve(list, nums); +### 接雨水问题 - return res; - } +```java + public long maxWater(int[] arr) { + if (arr.length <= 2) + return 0; + //找到最高的柱子的下标 + int max = Integer.MIN_VALUE; + int maxIndex = -1; + for (int i = 0; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + maxIndex = i; + } + } + + //统计最高柱子左边能接的雨水数量 + int left = arr[0]; + int right = 0; + long water = 0; + for (int i = 1; i < maxIndex; i++) { + right = arr[i]; + if (right > left) { + left = right; + } else { + water += left - right; + } + } + + //统计最高柱子右边能接的雨水数量 + right = arr[arr.length - 1]; + for (int i = arr.length - 2; i > maxIndex; i--) { + left = arr[i]; + if (arr[i] > right) { + right = left; + } else { + water += right - left; + } + } + + //返回盛水量 + return water; + } +``` - private void solve(ArrayList list, int[] nums) { - if (list.size() == nums.length) { - res.add(new ArrayList(list)); - return; - } - for (int i = 0; i < nums.length; i++) { - if (!list.contains(nums[i])) { - list.add(nums[i]); - solve(list, nums); - list.remove(list.size() - 1); - } - } - } +```java +public long maxWater (int[] arr) { + int l = 0, r = arr.length-1; + int maxL = 0, maxR = 0; + long res = 0; + while(l < r){ + maxL = Math.max(arr[l],maxL); // 求出左边界的最大值 + maxR = Math.max(arr[r],maxR); // 求出右边界的最大值 + if(maxR > maxL){ // 如果 + res += maxL - arr[l++]; + }else{ + res += maxR - arr[r--]; + } + } + return res; } ``` -### 集合的所有子集 +### 最小覆盖子串(T包含S的最小子串) ```java import java.util.*; + public class Solution { - public ArrayList> subsets(int[] S) { - ArrayList> res=new ArrayList<>(); - Arrays.sort(S); - LinkedList list=new LinkedList<>(); - dfs(res,list,0,S); - return res; - } - public void dfs(ArrayList> res, - LinkedList list, int k, int[] S){ - res.add(new ArrayList<>(list)); - for(int i=k;i window = new HashMap<>(); + HashMap need = new HashMap<>(); + for (int i = 0; i < t.length(); i++) { + Integer count = need.get(t.charAt(i)); + count = count == null ? 1 : ++count; + need.put(t.charAt(i),count); } + int left =0 , right = 0; + int vaild = 0; + int len = Integer.MAX_VALUE,start = 0; + //最小覆盖字串起始索引 + while (right < s.length()){ + char c = s.charAt(right); + right++; + if (need.containsKey(c)){ + Integer count = window.get(c); + count = count == null ? 1 : ++count; + window.put(c,count); + if (window.get(c).equals(need.get(c))){ + vaild++; + } + } + + //都包含了,right找到了,可以考虑收缩 + while (vaild == need.size()){ + if (right -left < len){ + start = left; + len = right - left; + } + //d是将要移出窗口的字符 + char d = s.charAt(left); + //左移窗口 + left++; + //数据更新 + if (need.containsKey(d)){ + if (window.get(d).equals(need.get(d))){ + vaild--; + } + window.put(d,window.get(d)-1); + } + } + } + return len == Integer.MAX_VALUE + ? "" : s.substring(start,start+len); } } +``` + +### 两数之和 + +```java +import java.util.HashMap; +public class Solution { + public int[] twoSum(int[] nums, int target) { + HashMap map = new HashMap<>(); + for (int i = 0; i < nums.length; i++) { + if (map.containsKey(nums[i])){ + return new int[]{map.get(nums[i])+1,i+1}; + } + map.put(target - nums[i],i); + } + return null; + } +} +``` + +### 最长重复子串(连续两个相同的字符串) + +```java +import java.util.*; +public class Solution { + // 使用滑动窗口 滑动窗口中的字符串是重复字符串的一半 + public int solve (String a) { + //枚举长度为i的窗口(按窗口大小倒序枚举),找到第一个满足条件的窗口(窗口为重复子串) + char[] cs = a.toCharArray(); + int len = cs.length; + int cnt = 0; + // 滑动窗口的大小不可能大于数组长度的一半 + for (int i=len/2; i>0; i--) { //枚举一半窗口大小 + //窗口右侧节点范围len-i + for (int j=0; j max){ + max = dp[i]; + } + } + return max; +} + +// sum < 0置为0 +public int FindGreatestSumOfSubArray(int[] array) { + if(array.length == 0){ + return 0; + } + + int max = Integer.MIN_VALUE; + int cur = 0; + for(int i = 0; i < array.length; i++){ + cur += array[i]; + max = Math.max(max,cur); + cur = cur < 0 ? 0 : cur; + } + return max; +} +``` + +### 最长公共子串(返回具体字符串/长度) + +```java +// 返回字符串 +public class Solution { + public String LCS (String str1, String str2) { + // write code here + int m = str1.length(); + int n = str2.length(); + int[][] dp = new int[m][n]; + int maxLength = 0; + int lastIndex = 0; + //用来记录str1中最长公共串的最后一个字符的下标 + for(int i = 0; i < m; i++){ + for(int j = 0; j < n; j++){ + if(str1.charAt(i) == str2.charAt(j)){ + //判断str1中第i个字符是否和str2中第j个字符相等 + if(i == 0 || j == 0){ + dp[i][j] = 1; + }else{ + dp[i][j] = dp[i - 1][j - 1] + 1; + } + + if(dp[i][j] > maxLength){ + //判断是否需要更新最长公共子串 + maxLength = dp[i][j]; + lastIndex = i; + } + } + } + } + + //通过str1来截取长度为maxLength, 最后字符坐标为lastIndex的子串 + return str1.substring(lastIndex - maxLength + 1, + lastIndex + 1); + } +} + +// 返回长度 +public class Solution { + public int LCS (String s1, String s2) { + // write code here + int mLength = s1.length(); + int nLength = s2.length(); + int[][] dp = new int[mLength + 1][nLength + 1]; + char[] c1 = s1.toCharArray(); + char[] c2 = s2.toCharArray(); + for (int i = 0; i <= mLength; i++) { + dp[i][0] = 0; + } + for (int j = 0; j <= nLength; j++) { + dp[0][j] = 0; + } + for (int i = 1; i <= mLength; i++) { + for (int j = 1; j <= nLength; j++) { + if (c1[i - 1] != c2[j - 1]) { + dp[i][j] = + Math.max(dp[i- 1][j], dp[i][j - 1]); + } else { + dp[i][j] = dp[i - 1][j - 1] + 1; + } + } + } + return dp[mLength][nLength]; + } +} +``` + +### 斐波那契数列 + +```java +public class Solution { + public int Fibonacci(int n) { + if(n == 0){ + return 0; + } + if(n == 1 || n == 2){ + return 1; + } + return Fibonacci(n-1) + Fibonacci(n-2); + } +} +``` + +### 最长回文子串的长度 + +```java +public String longestPalindrome1(String s) { + if (s == null || s.length() == 0) { + return ""; + } + int strLen = s.length(); + int left = 0; + int right = 0; + int len = 1; + int maxStart = 0; + int maxLen = 0; + + for (int i = 0; i < strLen; i++) { + left = i - 1; + right = i + 1; + while (left >= 0 && + s.charAt(left) == s.charAt(i)) { + len++; + left--; + } + while (right < strLen && + s.charAt(right) == s.charAt(i)) { + len++; + right++; + } + while (left >= 0 && right < strLen + && s.charAt(right) == s.charAt(left)) { + len = len + 2; + left--; + right++; + } + if (len > maxLen) { + maxLen = len; + maxStart = left; + } + len = 1; + } + return s.substring(maxStart + 1, + maxStart + maxLen + 1); + +} +``` + +### 最长递增子序列 + +```java +// 求长度 +class Solution { + public int lengthOfLIS(int[] nums) { + if(nums.length == 0) return 0; + int[] dp = new int[nums.length]; + int res = 0; + Arrays.fill(dp, 1); + for(int i = 0; i < nums.length; i++) { + for(int j = 0; j < i; j++) { + if(nums[j] < nums[i]) + dp[i] = Math.max(dp[i], dp[j] + 1); + } + res = Math.max(res, dp[i]); + } + return res; + } +} +``` + +```java +public int[] LIS (int[] arr) { + // write code here + if(arr == null || arr.length <= 0){ + return null; + } + + int len = arr.length; + int[] count = new int[len]; // 存长度 + int[] end = new int[len]; // 存最长递增子序列 + + //init + int index = 0; // end 数组下标 + end[index] = arr[0]; + count[0] = 1; + + for(int i = 0; i < len; i++){ + if(end[index] < arr[i]){ + end[++index] = arr[i]; + count[i] = index; + } + else{ + int left = 0, right = index; + while(left <= right){ + int mid = (left + right) >> 1; + if(end[mid] >= arr[i]){ + right = mid - 1; + } + else{ + left = mid + 1; + } + } + end[left] = arr[i]; + count[i] = left; + } + } + + //因为返回的数组要求是字典序,所以从后向前遍历 + int[] res = new int[index + 1]; + for(int i = len - 1; i >= 0; i--){ + if(count[i] == index){ + res[index--] = arr[i]; + } + } + return res; +} +``` + +### 买卖股票的最佳时机 + +base case: +dp[-1][k][0] = dp[i][0][0] = 0 +dp[-1][k][1] = dp[i][0][1] = -infinity + +状态转移⽅程: +dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) +dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) + +- k == 1 + +```java +public class Solution { + /** + * + * @param prices int整型一维数组 + * @return int整型 + */ + public int maxProfit (int[] prices) { + if(prices.length == 0) return 0; + // write code here + int n = prices.length; + int[][] dp = new int[n][2]; + for(int i = 0; i < n; i++){ + if(i - 1 == -1){ + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i-1][0], + dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1],-prices[i]); + } + + return dp[n-1][0]; + } +} + +// 空间复杂度优化版本 +int maxProfit(int[] prices) { + int n = prices.length; + // base case: dp[-1][0] = 0, dp[-1][1] = -infinity + int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; + for (int i = 0; i < n; i++) { + // dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) + dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]); + // dp[i][1] = max(dp[i-1][1], -prices[i]) + dp_i_1 = Math.max(dp_i_1, -prices[i]); + } + return dp_i_0; +} +``` + +- k 为正无穷 + +```java +// 原始版本 +int maxProfit_k_inf(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i-1][0], + dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1], + dp[i-1][0] - prices[i]); + } + return dp[n - 1][0]; +} + +// 空间复杂度优化版本 +int maxProfit_k_inf(int[] prices) { + int n = prices.length; + int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; + for (int i = 0; i < n; i++) { + int temp = dp_i_0; + dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]); + dp_i_1 = Math.max(dp_i_1, temp - prices[i]); + } + return dp_i_0; +} +``` + +- k == 2 + +```java +// 原始版本 +int maxProfit(int[] prices) { + int max_k = 2, n = prices.length; + int[][][] dp = new int[n][max_k + 1][2]; + for (int i = 0; i < n; i++) { + for (int k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + dp[i][k][0] = Math.max(dp[i-1][k][0], + dp[i-1][k][1] + prices[i]); + dp[i][k][1] = Math.max(dp[i-1][k][1], + dp[i-1][k-1][0] - prices[i]); + } + } + // 穷举了 n × max_k × 2 个状态,正确。 + return dp[n - 1][max_k][0]; +} + +// 空间复杂度优化版本 +int maxProfit_k_2(int[] prices) { + // base case + int dp_i10 = 0, dp_i11 = Integer.MIN_VALUE; + int dp_i20 = 0, dp_i21 = Integer.MIN_VALUE; + for (int price : prices) { + dp_i20 = Math.max(dp_i20, dp_i21 + price); + dp_i21 = Math.max(dp_i21, dp_i10 - price); + dp_i10 = Math.max(dp_i10, dp_i11 + price); + dp_i11 = Math.max(dp_i11, -price); + } + return dp_i20; +} +``` + +- k 为正无穷,但含有交易冷冻期 + +```java +// 原始版本 +int maxProfit_with_cool(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case 1 + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + if (i - 2 == -1) { + // base case 2 + dp[i][0] = Math.max(dp[i-1][0], + dp[i-1][1] + prices[i]); + // i - 2 小于 0 时根据状态转移方程推出对应 base case + dp[i][1] = Math.max(dp[i-1][1], -prices[i]); + // dp[i][1] + // = max(dp[i-1][1], dp[-1][0] - prices[i]) + // = max(dp[i-1][1], 0 - prices[i]) + // = max(dp[i-1][1], -prices[i]) + continue; + } + dp[i][0] = Math.max(dp[i-1][0], + dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1], + dp[i-2][0] - prices[i]); + } + return dp[n - 1][0]; +} + +// 空间复杂度优化版本 +int maxProfit_with_cool(int[] prices) { + int n = prices.length; + int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; + int dp_pre_0 = 0; // 代表 dp[i-2][0] + for (int i = 0; i < n; i++) { + int temp = dp_i_0; + dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]); + dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]); + dp_pre_0 = temp; + } + return dp_i_0; +} +``` + +- k 为正无穷且考虑交易手续费 + +```java +// 原始版本 +int maxProfit_with_fee(int[] prices, int fee) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i] - fee; + // dp[i][1] + // = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee) + // = max(dp[-1][1], dp[-1][0] - prices[i] - fee) + // = max(-inf, 0 - prices[i] - fee) + // = -prices[i] - fee + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], + dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], + dp[i - 1][0] - prices[i] - fee); + } + return dp[n - 1][0]; +} + +// 空间复杂度优化版本 +int maxProfit_with_fee(int[] prices, int fee) { + int n = prices.length; + int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; + for (int i = 0; i < n; i++) { + int temp = dp_i_0; + dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]); + dp_i_1 = Math.max(dp_i_1, temp - prices[i] - fee); + } + return dp_i_0; +} +``` + +- 指定 k + +```java +int maxProfit_k_any(int max_k, int[] prices) { + int n = prices.length; + if (n <= 0) { + return 0; + } + if (max_k > n / 2) { + // 复用之前交易次数 k 没有限制的情况 + return maxProfit_k_inf(prices); + } + + // base case: + // dp[-1][...][0] = dp[...][0][0] = 0 + // dp[-1][...][1] = dp[...][0][1] = -infinity + int[][][] dp = new int[n][max_k + 1][2]; + // k = 0 时的 base case + for (int i = 0; i < n; i++) { + dp[i][0][1] = Integer.MIN_VALUE; + dp[i][0][0] = 0; + } + + for (int i = 0; i < n; i++) + for (int k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 i = -1 时的 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + dp[i][k][0] = Math.max(dp[i-1][k][0], + dp[i-1][k][1] + prices[i]); + dp[i][k][1] = Math.max(dp[i-1][k][1], + dp[i-1][k-1][0] - prices[i]); + } + return dp[n - 1][max_k][0]; +} +``` + +### 矩阵的最小路径和 + +```java +import java.util.*; +public class Solution { + /** + * + * @param matrix int整型二维数组 the matrix + * @return int整型 + */ + public int minPathSum (int[][] matrix) { + // write code here + int m = matrix.length, n = matrix[0].length; + if (m == 0 || n == 0) return 0; + + for (int i = 1; i < m; i++) + matrix[i][0] += matrix[i-1][0]; + for (int i = 1; i < n; i++) + matrix[0][i] += matrix[0][i-1]; + + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + matrix[i][j] += + Math.min(matrix[i-1][j], matrix[i][j-1]); + } + } + return matrix[m-1][n-1]; + } +} +``` + +### 编辑距离 + +```java +import java.util.*; + + +public class Solution { + /** + * min edit cost + * @param str1 string字符串 the string + * @param str2 string字符串 the string + * @param ic int整型 insert cost + * @param dc int整型 delete cost + * @param rc int整型 replace cost + * @return int整型 + */ + public int minEditCost (String str1, String str2, + int ic, int dc, int rc) { + // write code here + int len1=str1.length(); + int len2=str2.length(); + char[] char1=str1.toCharArray(); + char[] char2=str2.toCharArray(); + int[][] dp=new int[len1+1][len2+1]; + for(int i=1;i<=len1;i++){ + dp[i][0]=dp[i-1][0]+dc; + } + for(int i=1;i<=len2;i++){ + dp[0][i]=dp[0][i-1]+ic; + } + for(int i=0;i stack = new Stack<>(); + int last = -1; + int maxLen = 0; + for(int i = 0; i < s.length(); i++){ + if(s.charAt(i) == '('){ + stack.push(i); + } else { + if(stack.isEmpty()){ + last = i; + } else { + stack.pop(); + if(stack.isEmpty()){ + maxLen = + Math.max(maxLen, i - last); + } else { + maxLen = + Math.max(maxLen, i - stack.peek()); + } + } + } + } + + return maxLen; + } +} + +// 动态规划 +public int longestValidParentheses2(String s) { + if (s == null || s.length() == 0) + return 0; + int[] dp = new int[s.length()]; + int ans = 0; + for (int i = 1; i < s.length(); i++) { + // 如果是'('直接跳过,默认为0 + if (s.charAt(i) == ')') { + if (s.charAt(i - 1) == '(') + dp[i] = + (i >= 2 ? dp[i - 2] : 0) + 2; + // 说明s.charAt(i - 1)==')' + else if (i - dp[i - 1] > 0 && + s.charAt(i - dp[i - 1] - 1) == '(') { + dp[i] = + (i - dp[i - 1] > 1 ? + dp[i - dp[i - 1] - 2] : 0) + dp[i - 1] + 2; + // 因为加了一个左括号和一个右括号,所以是加2 + } + } + ans = Math.max(ans, dp[i]); + } + return ans; +} +``` + +### 高空扔鸡蛋 + +```java +// 如果棋子碎了,那么棋子的个数K应该减一, +// 搜索的楼层区间应该从[1..N]变为[1..i-1]共i-1层楼; +// 如果棋子没碎,那么棋子的个数K不变, +// 搜索的楼层区间应该从 [1..N]变为[i+1..N]共N-i层楼。 +import java.util.*; +public class Solution { + /** + * 返回最差情况下扔棋子的最小次数 + * @param k int整型 棋子数 + * @param n int整型 楼层数 + * @return int整型 + */ + int[][] memo; + public int solve (int n, int k) { + memo = new int[k + 1][n + 1]; + for(int[] m : memo) { + Arrays.fill(m, -1); + } + return dp(k, n); + } + + // 定义:有K个棋子面对N层楼,最少需要扔 dp(K, N) 次 + int dp(int k, int n) { + // 状态:棋子数k,需要测试的楼层n + if(k == 1) { + return n; + } + // 尝试到底层 + if(n == 0) { + return 0; + } + if(memo[k][n] != -1) { + return memo[k][n]; + } + int res = Integer.MAX_VALUE; + // 寻找第一层到第n层的最少扔的次数 + for(int i = 1; i <= n; i++) { + res = Math.min(res, + // 取决于最差情况(碎了,没碎) + Math.max(dp(k-1, i-1), dp(k, n-i)) + 1); + } + memo[k][n] = res; + return res; + } +} +``` + +### 兑换零钱 + +```java +public class Solution { + /** + * 最少货币数 + * @param arr int整型一维数组 the array + * @param aim int整型 the target + * @return int整型 + */ + public int minMoney (int[] arr, int aim) { + // write code here + //如何使用最少arr元素 构成 aim值 + //dp[i] 代表给定钱数为i的时候最少货币数 + // 就是凑成 i 元钱,需要dp[i] 张arr中面值纸币 + //没办法兑换 arr[i] dp[i] = dp[i] + //可以dp[i] = dp[i - arr[i]] + 1 + //dp[i] = min(dp[i], dp[i-a[j]]) + if(arr == null || arr.length == 0){ + return -1; + } + int[] dp = new int[aim+1]; + for(int i = 0;i<=aim;i++){ + dp[i] = aim+1; + } + + dp[0] = 0; + for(int i = 1;i<=aim;i++){ + for(int j = 0;j< arr.length;j++){ + if(arr[j] <= i){ + //给了一张 3 元钱,小于 需要找零的4 元钱, + // 那 就等于 1 + 需要找零剩下的钱dp[i -arr[j]] 4 - 3 + dp[i] = + Math.min(dp[i], dp[i-arr[j]] +1); + } + } + } + return (dp[aim] > aim) ?-1 : dp[aim]; + } +} +``` + +### 最大正方形 + +1.确定dp[][]数组的含义 + +此题的dp[i][j],代表以坐标为(i,j)的元素为右下角的正方形的边长。 + +2.状态转移方程 + +dp[i][j]的值取决于dp[i-1][j],dp[i-1][j-1],dp[i][j-1]的最小值 +即左方正方形的边长,左上方正方形的边长,上方正方形的边长三者的最小值。 + +```java +import java.util.*; + +// dp[i][j],代表以坐标为(i,j)的元素为右下角的正方形的边长 +public class Solution { + /** + * 最大正方形 + * @param matrix char字符型二维数组 + * @return int整型 + */ + public int solve (char[][] matrix) { + // write code here + if(matrix.length ==0 || matrix[0].length == 0) return 0; + int rows = matrix.length; + int cols = matrix[0].length; + int[][] dp = new int[rows][cols]; + int maxSquareLength = 0; + for(int i = 0; i < rows; i++){ + if(matrix[i][0] == '1') dp[i][0] = 1; + } + for(int i = 0; i < cols; i++){ + if(matrix[0][i] == '1') dp[0][i] = 1; + } + for(int i =1; i < rows; i++){ + for(int j = 1; j < cols; j++){ + if(matrix[i][j] == '1'){ + dp[i][j] = + Math.min(Math.min(dp[i-1][j-1], + dp[i-1][j]),dp[i][j-1])+1; + if(dp[i][j] > maxSquareLength) + maxSquareLength = dp[i][j]; + } + } + } + return maxSquareLength*maxSquareLength; + } +} +``` + +### 通配符匹配 + +![](http://image.ouyangsihai.cn/FtLlLxIBxM4nN1yyIi7VqfxsAXMB) + +```java +import java.util.*; +public class Solution { + public boolean isMatch(String s, String p) { + if (p == null || p.isEmpty())return s == null || s.isEmpty(); + int slen = s.length(), plen = p.length(); + boolean[][] dp = new boolean[slen + 1][plen + 1]; + //初始化dp数组,dp[1][0]~dp[s.length][0]默认值flase不需要显式初始化为false + dp[0][0] = true; + //dp[0][1]~dp[0][p.length]只有p的j字符以及前面所有字符都为'*'才为true + for (int j = 1; j <= plen; j++)dp[0][j] = p.charAt(j - 1) == '*' && + dp[0][j - 1]; + //填写dp数组剩余部分 + for (int i = 1; i <= slen; i++) { + for (int j = 1; j <= plen; j++) { + char si = s.charAt(i - 1), pj = p.charAt(j - 1); + if (si == pj || pj == '?') { + dp[i][j] = dp[i - 1][j - 1]; + } else if (pj == '*') { + dp[i][j] = dp[i - 1][j] || dp[i][j - 1]; + } + } + } + return dp[slen][plen]; + } +} +``` + +### 正则表达式匹配 + +![](http://image.ouyangsihai.cn/FsEkqX8hk4n_JgGhpWxZgFGtV5Qs) + +```java +import java.util.*; +public class Solution { + public boolean match (String str, String pattern) { + int n=str.length(); + int m=pattern.length(); + boolean[][] dp=new boolean[n+1][m+1]; + + //初始化 + dp[0][0]=true; + for(int i=1;i<=n;i++){ + dp[i][0]=false; + } + + //分模式串的后一个位置是否为*进行讨论,为*时, + // 将*与前一个位置合并起来进行考虑 + for(int i=0;i<=n;i++){ + for(int j=1;j<=m;j++){ + if(pattern.charAt(j-1)!='*'){ + //当前模式串字符和原串字符匹配 + if(i>0 + &&(str.charAt(i-1)==pattern.charAt(j-1) + ||(pattern.charAt(j-1)=='.'))){ + dp[i][j]=dp[i-1][j-1]; + } + } + else{ + if(j>=2){ + //不管是否匹配,都可以将当前字符绑定上*匹配原串字符0次 + dp[i][j]=dp[i][j-2]; + //当前模式串字符和原串字符匹配 + if(i>0 + &&(str.charAt(i-1)==pattern.charAt(j-2) + ||(pattern.charAt(j-2)=='.'))){ + dp[i][j]=dp[i-1][j]||dp[i][j-2]; + } + } + } + } + } + return dp[n][m]; + } +} +``` + +### 矩阵最长递增路径 + +```java +class Solution { + public int longestIncreasingPath(int[][] matrix) { + int m = matrix.length; + int n = matrix[0].length; + int ans = 0; + int[][] dp = new int[m][n];//储存当前节点最长路径 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (dp[i][j] == 0) { + ans = Math.max(ans, dfs(matrix, dp, i, j)); + } + } + } + return ans; + } + public int dfs(int[][] matrix , int[][] dp, int i , int j){ + if (dp[i][j] == 0) { + int dir1 = 0, dir2 = 0, dir3 = 0, dir4 = 0; + if (i > 0 && matrix[i - 1][j] > matrix[i][j]) { + dir1 = dfs(matrix, dp, i - 1, j); + } + if (j > 0 && matrix[i][j - 1] > matrix[i][j]) { + dir2 = dfs(matrix, dp, i, j - 1); + } + if (i < matrix.length - 1 + && matrix[i + 1][j] > matrix[i][j]) { + dir3 = dfs(matrix, dp, i + 1, j); + } + if (j < matrix[0].length - 1 + && matrix[i][j + 1] > matrix[i][j]) { + dir4 = dfs(matrix, dp, i, j + 1); + } + //选出四个方向的最长子串,加1后赋值给当前节点 + dp[i][j] = 1 + Math.max(dir1, + Math.max(dir2, Math.max(dir3, dir4))); + } + return dp[i][j]; + } +} +``` + +### 最长上升子序列 + +- 返回长度 + +```java +import java.util.*; + + +public class Solution { + /** + * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 + * + * 给定数组的最长严格上升子序列的长度。 + * @param arr int整型一维数组 给定的数组 + * @return int整型 + */ + public int LIS (int[] arr) { + // write code here + int len = arr.length; + if (arr == null && len == 0){ + return 0; + } + int maxLen = 0; + int[] dp = new int[len]; + Arrays.fill(dp, 1); + for (int i = 1; i < len; i++) { + for (int j = i-1; j >=0 ; j--) { + if (arr[i]> arr[j]){ + dp[i] = Math.max(dp[i],dp[j]+1); + } + } + maxLen = Math.max(maxLen,dp[i]); + } + return maxLen; + } +} +``` + +- 返回字符串 + +// 方法一:dp +状态定义:dp[i]表示以i位置元素结尾的最长上升子序列长度。 +状态初始化:以每个位置结尾的上升子序列长度至少为1。 +状态转移:遍历arr数组,假设当前位置为i, +比较当前位置i之前的每一个元素与当前位置元素的大小, +如果小于当前位置元素arr[i],说明可以接在arr前面。 +即 dp[i]=Math.max(dp[i],dp[j]+1)。 + +// 方法二:dp+数组 +与方法一相同的是,也需要建立一个dp数组找到每一个位置对应的最长上升子序列长度, +最后再通过逆序遍历arr数组的方式,找到每一个长度对应的那个元素,赋值给结果序列。 +不过确定dp数组的方式有所不同。 + +为了找到最长的上升子序列,我们可以维护一个单调递增的数组tail记录当前的最长上升子序列, +如果后面出现的数arr[i]比tail数组末尾元素大,则可以直接加在后面; +如果不是,则找到tail数组中第一个大于等于arr[i]的元素位置, +并将该位置的元素替换为arr[i],因为在长度相同的情况下,当前值越小,则后面出现更长子序列的概率越大。 + +```java +// 方法一:dp +public class Solution { + public int[] LIS (int[] arr) { + int n=arr.length; + //dp[i]表示以i位置元素结尾的最长上升子序列长度 + int[] dp=new int[n+1]; + //初始化为1 + Arrays.fill(dp,1); + //记录最长子序列的长度 + int len=0; + for(int i=0;i=0;i--){ + if(dp[i]==len){ + res[--len]=arr[i]; + } + return res; + } +} + +// 方法二:dp+二分 +public class Solution { + public int[] LIS (int[] arr) { + int n=arr.length; + //维护一个单调递增tail数组 + int[] tail=new int[n]; + //dp[i]表示以i位置结尾的最长上升子序列长度 + int[] dp=new int[n]; + //最长上升子序列长度 + int len=0; + for(int i=0;i=0;i--){ + if(dp[i]==len){ + res[--len]=arr[i]; + } + } + return res; + } + + //二分法找tail数组中第一个大于等于arr[i]的元素位置 + private int search(int[] nums,int len,int k){ + int low=0,high=len-1; + while(low=k){ + high=mid; + } + //否则排除mid以及mid往左的所有元素 + else{ + low=mid+1; + } + } + return low; + } +} +``` + +### 目标和(完全背包) + +```java +public class Solution { + public int findTargetSumWays (int[] nums, int target) { + //边界情况判断 + int n=nums.length; + if(n==0) return 0; + //记录累加和 + int sum=0; + //遍历nums数组 + for(int num:nums){ + sum+=num; + } + //计算背包容量 + int V=(sum+target)/2; + //如果为奇数,说明nums数组中找不打和为(sum+target)/2的若干数字 + if((sum+target)%2==1) return 0; + + //dp[j]表示有多少种不同的组合,其累加和为j + int[] dp=new int[V+1]; + //初始化 + dp[0]=1; + for(int i=0;i=nums[i];j--){ + dp[j]+=dp[j-nums[i]]; + } + } + return dp[V]; + } +} +``` + +### 打家劫舍 + +```java +public class Solution { + public int rob (int[] nums) { + // write code here + + // 一些特殊情况的处理 + if (1 == nums.length) { + return nums[0]; + } + if (2 == nums.length) { + return Math.max(nums[0], nums[1]); + } + + int l = nums.length; + int[] dp = new int[l]; + + dp[0] = nums[0]; + dp[1] = Math.max(nums[0], nums[1]); + for (int i = 2; i < l; i++) { + dp[i] = Math.max(dp[i - 2] + + nums[i], dp[i - 1]); + } + return dp[l - 1]; + } +} + +// 圆形情况 +public class Solution { + public int rob (int[] nums) { + int n=nums.length; + //在0到n-2范围内找 + int rob1=getRob(Arrays.copyOfRange(nums,0,n-1)); + //在1到n-1范围内找 + int rob2=getRob(Arrays.copyOfRange(nums,1,n)); + + return Math.max(rob1,rob2); + } + + private int getRob(int[] nums){ + int n=nums.length; + //边界情况处理 + if(n==0) return 0; + if(n==1) return nums[0]; + if(n==2) return Math.max(nums[0],nums[1]); + //定义dp数组 + int[] dp=new int[n]; + //初始化 + dp[0]=nums[0]; + dp[1]=Math.max(nums[0],nums[1]); + for(int i=2;i +如果map中没有当前这个元素,那么dp[i]=dp[i-1]+1 +如果map中存在当前的元素,一开始的想法是 dp[i]=i-map.get(array[i]), +但是这样想有点问题,如果当前的字符串是abba的时候,按照刚才的思路dp[0]=1 dp[1]=2 dp[2]=1 dp[3]=3 +但是dp[3]是错误的,因为中间存在了重复的字符。所以要加一种情况。 +dp[i]=Math.min(dp[i-1]+1,i-map.get(array[i])) + +public class Solution { + /** + * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 + * + * + * @param s string字符串 + * @return int整型 + */ + public int lengthOfLongestSubstring (String s) { + if(s==null) return 0; + char[]array=s.toCharArray(); + if(array.length==1){ + return 1; + } + int[]dp=new int[array.length]; + int maxLength=1; + HashMapmap=new HashMap<>(); + dp[0]=1; + map.put(array[0],0); + for(int i=1;i= 10 && num <= 26){ + if(i == 1){ + dp[i] += 1; + }else{ + dp[i] += dp[i-2]; + } + } + } + return dp[nums.length()-1]; + + } +``` + +## 二分 + +### 求平方根 + +```java +import java.util.*; + + +public class Solution { + /** + * + * @param x int整型 + * @return int整型 + */ + public int sqrt (int x) { + // write code here + if(x < 2){ + return x; + } + int left = 1; + int right = x / 2; + while(left <= right){ + int mid = left + (right - left) / 2; + if(x / mid == mid){ + return mid; + } else if(x / mid < mid){ + right = mid - 1; + } else if(x / mid > mid){ + left = mid + 1; + } + } + + return right; + } +} +``` + +### 在旋转过的有序数组中寻找目标值 + +```java +public class Solution { + public int search (int[] nums, int target) { + // write code here + if (nums == null || nums.length < 1) { + return -1; + } + if (nums.length == 1) { + return nums[0] == target ? 0 : -1; + } + + int start = 0, end = nums.length - 1; + while (end >= start) { + // 找到 左右指针中间位置 + int mid = (end + start) >> 1; + if (nums[mid] == target) { + return mid; + } + // 在左侧升序数组中 + if (nums[0] <= nums[mid]) { + // 在开头和 mid 之间,那么 右指针则为 mid -1 + if (target >= nums[0] + && target < nums[mid]) { + end = mid -1; + } else { + start = mid + 1; + } + } else { + // 如果在 mid 和end 之间,更新 start 为 mid = 1 + if (target > nums[mid] + && target <= nums[end]) { + start = mid + 1; + } else { + end = mid - 1; + } + } + } + return -1; + } +} +``` + +### 在两个长度相等的排序数组中找到上中位数 + +```java +// 普通 +public class Solution { + public int findMedianinTwoSortedAray (int[] arr1, int[] arr2) { + // write code here + int length1 = arr1.length; + int length2 = arr2.length; + int i=0; + int j=0; + int[] arr = new int[length1+length2]; + int index = 0; + //合并数组 + for(;i>1); + //arr2中位数 + mid2 = l2+((r2-l2)>>1); + int k = r1-l1+1; + if(arr1[mid1] == arr2[mid2]){ + //若两数组中位数相等,整体中位数也是这个 + return arr1[mid1]; + } + else if(arr1[mid1] > arr2[mid2]){ + if(k%2 == 0){//区间元素个数为偶数 + r1 = mid1; //整体中位数在arr1左区间,包括mid1 + l2 = mid2+1; //整体中位数在arr2右区间,不包括mid2 + } + else if(k%2 == 1){ //区间元素个数为奇数 + r1 = mid1; //整体中位数在arr1左区间,包括mid1 + l2 = mid2; //整体中位数在arr2右区间,包括mid2 + } + } + else if (arr1[mid1] < arr2[mid2]){ + if(k%2 == 0){//区间元素个数为偶数 + r2 = mid2; //整体中位数在arr2左区间,包括mid2 + l1 = mid1+1; //整体中位数在arr1右区间,不包括mid1 + } + else if(k%2 == 1){ //区间元素个数为奇数 + r2 = mid2; //整体中位数在arr2左区间,包括mid2 + l1 = mid1; //整体中位数在arr1右区间,包括mid1 + } + } + } + //当区间内只有一个元素时,两个区间中最小值即为整体中位数 + return Math.min(arr1[l1],arr2[l2]); +} +``` + +### 有序矩阵元素查找 + +```java +public class Solution { + public int[] findElement(int[][] mat, int n, int m, int x) { + // write code here + int[] result = new int[2]; + int row = 0; + int col = m - 1; + while(row < n && col >= 0) { + if(mat[row][col] == x) { + result[0] = row; + result[1] = col; + break; + } + if(x > mat[row][col]) { + row ++; + } else { + col --; + } + } + return result; + } +} +``` + +### 二分查找 + +```java +public int binarySearch(int target, int[] nums) { + int left = 0; + int right = nums.length-1; //取最后一个下标 + int mid = 0; + //左下标大于右下标,直接返回-1 + if (left > right || nums.length == 0 || nums == null) { + return -1; + } + // 初始化 right 的赋值是 nums.length - 1, + // 即最后一个元素的索引,而不是 nums.length + while (left <= right) { + mid = left + (right - left) / 2; //如果下标之和除以2有小数,则直接去掉 + if (target == nums[mid]) { + return mid; //找到目标值,然后返回 + } else if (target > nums[mid]) { //目标值大于中间值,向右遍历 + left = mid + 1; //所以向右遍历的第一个下标是:中间下标+1 + } else if (target < nums[mid]) { //目标值小于中间值,向左遍历 + right = mid - 1; //所以向左遍历的最后一个下标是:中间下标-1 + } + } + return -1; //找不到对应目标值,直接返回-1 +} +``` + +```java +// 查找最右值 +public class Solution { + public int upper_bound_search (int n, int v, int[] a) { + // write code here + int left = 0, right = n; + while(left < right){ + int mid = left + (right - left) / 2; + if(a[mid] == v){ + right = mid; + } else if(a[mid] > v){ + right = mid; + } else { + left = mid + 1; + } + } + return left+1; + } +} +``` + +```java +// 查找最左值 +public class Solution { + public int left_bound_search (int[] nums, int target) { + // write code here + if (nums == null || nums.length == 0) { + return -1; + } + int left = 0, right = nums.length - 1; + + while (left < right) { + int mid = (left + right) / 2; + if (nums[mid] >= target) { + right = mid; + } else { + left = mid + 1; + } + } + return nums[right] == target ? right : -1; + } +} +``` + +### 旋转数组的最小数字 + +1.如果mid>right,说明mid-right之间存在被旋转数组,left = mid+1 +2.如果mid array[right]){ + left = mid + 1; + } + else{ + right = right - 1; + } + } + return array[left]; + } +} +``` + +### 数字在升序数组中出现的次数 + +```java +public class Solution { + public int GetNumberOfK(int [] array , int k) { + if(array.length == 0 || k < array[0] + || k > array[array.length-1]){ + return 0; + } + int left = 0; + int right = array.length -1; + int count = 0; + int found = 0; + int mid = -1; + while(left < right){ + mid = (left+right)/2; + if(array[mid] > k){ + right = mid-1; + }else if(array[mid] < k){ + left = mid+1; + }else{ + count++; + found = mid; + break; + } + } + + int prev = mid-1; + int foll = mid+1; + while(prev >= left){ + if(array[prev] == k){ + count++; + prev--; + }else{ + break; + } + } + + while(foll <= right){ + if(array[foll] == k){ + count++; + foll++; + }else{ + break; + } + } + return count; + } +} +``` + +### 峰值 + +本题之所以可以使用二分,使复杂度讲到lgn,是因为题目中的nums[i] != nums[i + 1]条件, +当中间元素mid不是峰时,一定有一边比mid中间值大, +假设右边的值,即mid+1位置的值大于mid的值,则右边一定存在峰, +因为右边的值从mid开始要么是 /\ 这个单调性,要么是 / 这种单调性,两种都一定存在峰 + +```java +import java.util.*; +public class Solution { + public int findPeakElement (int[] nums) { + int left = 0; + int right = nums.length - 1; + //二分法 + while(left < right){ + int mid = (left + right) / 2; + //右边是往下,不一定有坡峰 + if(nums[mid] > nums[mid + 1]) + right = mid; + //右边是往上,一定能找到波峰 + else + left = mid + 1; + } + //其中一个波峰 + return right; + } +} +``` + +## 数组 + +### 数组中只出现一次的数字 + +```java +public class Solution { + + public void FindNumsAppearOnce(int [] array, + int num1[] , int num2[]) { + int num = 0; + for(int i = 0; i < array.length; i++){ + num^=array[i]; + } + + int count = 0; + // 标志位,记录num中的第一个1出现的位置 + for(;count < array.length; count++){ + if((num&(1< set = new HashSet<>(); + for(int i = 0; i < array.length; i++){ + if(!set.add(array[i])){ + set.remove(array[i]); + } + } + + Object[] temp = set.toArray(); + num1[0] = (int)temp[0]; + num2[0] = (int)temp[1]; + }*/ +} + +``` + +### 合并两个有序的数组 + +```java +public class Solution { + public void merge(int A[], int m, int B[], int n) { + int i = m-1, j = n-1, k = m+n-1; + while(i >= 0 && j >= 0){ + if(A[i] > B[j]){ + A[k--] = A[i--]; + } else { + A[k--] = B[j--]; + } + } + + while(j >= 0){ + A[k--] = B[j--]; + } + } +} +``` + +### 子数组最大乘积 + +```java +public class Solution { + public double maxProduct(double[] arr) { + if(arr.length == 0 || arr == null){ + return 0.0; + } + double[] max = new double[arr.length]; + double[] min = new double[arr.length]; + max[0] = min[0] = arr[0]; + for(int i = 1; i < arr.length; i++){ + max[i] = + Math.max(Math.max(max[i-1]*arr[i], + min[i-1]*arr[i]),arr[i]); + min[i] = + Math.min(Math.min(max[i-1]*arr[i], + min[i-1]*arr[i]),arr[i]); + } + + double ans = max[0]; + for(int i = 0; i < max.length; i++){ + if(max[i] > ans){ + ans = max[i]; + } + } + return ans; + } +} + +public int maxProduct (int[] nums) { + int max = nums[0]; + int min = nums[0]; + int result = nums[0]; + for (int i = 1; i < nums.length; i++) { + int t = max; + max = Math.max(nums[i], + Math.max(max * nums[i], min * nums[i])); + min = Math.min(nums[i], + Math.min(t * nums[i], min * nums[i])); + result = Math.max(result,max); + } + return result; +} +``` + +### 数组中最长连续子序列 + +```java +public int MLS(int[] arr) { + if (arr == null || arr.length == 0) + return 0; + int longest = 1;//记录最长的有序序列 + int count = 1;//目前有序序列的长度 + //先对数组进行排序 + Arrays.sort(arr); + for (int i = 1; i < arr.length; i++) { + //跳过重复的 + if (arr[i] == arr[i - 1]) + continue; + //比前一个大1,可以构成连续的序列,count++ + if ((arr[i] - arr[i - 1]) == 1) { + count++; + } else { + //没有比前一个大1,不可能构成连续的, + //count重置为1 + count = 1; + } + //记录最长的序列长度 + longest = Math.max(longest, count); + } + return longest; +} +``` + +### 数组中未出现的最小正整数 + +时间复杂度O(n),空间复杂度O(n)的做法:开辟一个新的数组arr,长度为nums.length+1,遍历nums数组, +如果非负且值小于nums的长度,则把arr[nums[i]]置1。然后遍历辅助数组,找到下标不为1的第一个元素即可。 + +```java +public class Solution { + public int minNumberDisappeared (int[] nums) { + int []arr = new int [nums.length+1]; + for(int i=0;i0){ + arr[nums[i]] =1; + } + } + for(int i=1;i= end){ + return; + } + int mid = start + (end - start)/2; + + mergeSort(array,start,mid); + mergeSort(array,mid + 1,end); + + merge(array,start,mid,end); + } + + public void merge(int[] array,int start,int mid,int end){ + int[] temp = new int[end-start+1]; + int k = 0; + int i = start; + int j = mid + 1; + while(i <= mid && j <= end){ + if(array[i] < array[j]){ + temp[k++] = array[i++]; + } else { + temp[k++] = array[j++]; + res = (res + mid - i + 1) % 1000000007; + } + } + + while(i <= mid){ + temp[k++] = array[i++]; + } + + while(j <= end){ + temp[k++] = array[j++]; + } + + for(k = 0; k < temp.length; k++){ + array[start + k] = temp[k]; + } + } + + + /* + public int InversePairs(int [] array) { + int sum = 0; + for(int i = 0; i < array.length - 1; i++){ + for(int j = i + 1; j < array.length; j++){ + if(array[i] > array[j]){ + sum ++; + } + } + } + return sum % 1000000007; + } + */ +} +``` + +### 调整数组顺序使奇数位于偶数前面 + +```java +// 方法一:先记录下技术的个数,然后遍历数组,用另外一个数组接收奇数和偶数 +import java.util.*; +public class Solution { + public int[] reOrderArray (int[] array) { + // write code here + int[] arr=new int[array.length]; + int num=0; + for(int a:array){ + if((a&1)==1) num++;//奇数 + } + int i=0; + for(int a:array){ + if((a&1)==1){ //奇数 + arr[i++]=a; + }else{ + arr[num++]=a; + } + } + return arr; + } +} + +// 方法二:记录已经是奇数的位置下标(视作为有序区域),然后向后遍历, +// 一经发现是奇数则进行“插入排序”,然后有序区下标加1。 +public class Solution { + public int[] reOrderArray (int[] array) { + // 首先是对数值长度进行特判 + if(array==null||array.length==0) return array; + //记录已经是奇数的位置 + int j=0; + int temp = 0; + for(int i =0;ij){ + //这区间整体向后移动一位 + array[k] = array[k-1]; + k--; + } + //移位之后将对应的值赋值 + array[k] = temp; + j++; + } + } + //返回结果数数组 + return array; + } +} +``` + +### 矩阵乘法 + +```java +import java.util.*; +public class Solution { + public int[][] solve (int[][] a, int[][] b) { + // write code here + //矩阵相乘条件:a的列和b的行必须相等 + //记录a的行、a的列(b的行)、b的列 + int aRow = a.length; + int aColumn = a[0].length; + int bColumn = b[0].length; + //新矩阵行为a的行,列为b的列 + int[][] res = new int[aRow][bColumn]; + for(int i=0; i> result = new ArrayList<>(); + //暂存结果 + List path = new ArrayList<>(); + + public List> permuteUnique(int[] nums) { + boolean[] used = new boolean[nums.length]; + Arrays.fill(used, false); + Arrays.sort(nums); + backTrack(nums, used); + return result; + } + + private void backTrack(int[] nums, boolean[] used) { + if (path.size() == nums.length) { + result.add(new ArrayList<>(path)); + return; + } + for (int i = 0; i < nums.length; i++) { + // used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过 + // used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过 + // 如果同⼀树层nums[i - 1]使⽤过则直接跳过 + if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { + continue; + } + //如果同⼀树⽀nums[i]没使⽤过开始处理 + if (used[i] == false) { + used[i] = true;//标记同⼀树⽀nums[i]使⽤过,防止同一树支重复使用 + path.add(nums[i]); + backTrack(nums, used); + //回溯,说明同⼀树层nums[i]使⽤过,防止下一树层重复 + path.remove(path.size() - 1); + used[i] = false;//回溯 + } + } + } +} +``` + +### 岛屿的数量 + +```java +class Solution { + public int numIslands(char[][] grid) { + int count = 0; + for(int i = 0; i < grid.length; i++) { + for(int j = 0; j < grid[0].length; j++) { + if(grid[i][j] == '1'){ + bfs(grid, i, j); + count++; + } + } + } + return count; + } + + public void bfs(char[][] grid, int i, int j){ + Queue list = new LinkedList<>(); + list.add(new int[] { i, j }); + while(!list.isEmpty()){ + int[] cur = list.remove(); + i = cur[0]; j = cur[1]; + if(inArea(i,j,grid) + && grid[i][j] == '1') { + grid[i][j] = '0'; + list.add(new int[] { i + 1, j }); + list.add(new int[] { i - 1, j }); + list.add(new int[] { i, j + 1 }); + list.add(new int[] { i, j - 1 }); + } + } + } + + public boolean inArea(int i, int j, char[][] grid){ + return i >=0 && j >= 0 + && i < grid.length + && j < grid[0].length; + } +} +``` + +### 没有重复项数字的所有排列(全排列) + +```java +public class Demo1 { + ArrayList> res; + + public ArrayList> permute(int[] nums) { + res = new ArrayList>(); + if (nums == null || nums.length < 1) + return res; + //对数组元素进行从小到大排序 + Arrays.sort(nums); + ArrayList list = new ArrayList(); + + solve(list, nums); + + return res; + } + + private void solve(ArrayList list, int[] nums) { + if (list.size() == nums.length) { + res.add(new ArrayList(list)); + return; + } + for (int i = 0; i < nums.length; i++) { + if (!list.contains(nums[i])) { + list.add(nums[i]); + solve(list, nums); + list.remove(list.size() - 1); + } + } + } +} +``` + +### 集合的所有子集 + +```java +import java.util.*; +public class Solution { + public ArrayList> subsets(int[] S) { + ArrayList> res=new ArrayList<>(); + Arrays.sort(S); + LinkedList list=new LinkedList<>(); + dfs(res,list,0,S); + return res; + } + public void dfs(ArrayList> res, + LinkedList list, int k, int[] S){ + res.add(new ArrayList<>(list)); + for(int i=k;i> res; + private boolean[] visited; + + public ArrayList> permute(int[] nums) { + res = new ArrayList<>(); + visited = new boolean[nums.length]; + List list = new ArrayList<>(); + backtrace(nums, list); + return res; + } + + private void backtrace(int[] nums, List list) { + if (list.size() == nums.length) { + res.add(new ArrayList<>(list)); + } + + for (int i = 0; i < nums.length; i++) { + if (visited[i]) continue; + visited[i] = true; + list.add(nums[i]); + backtrace(nums, list); + visited[i] = false; + list.remove(list.size() - 1); + } + } +} +``` + +### N皇后问题 + +```java +public class Solution { + /** + * + * @param n int整型 the n + * @return int整型 + */ + public int Nqueen (int n) { + // write code here + List res=new ArrayList<>(); + char[][] chess=new char[n][n]; + for(int i=0;i res){ + if(row==chess.length){ + res.add(1); + return; + } + for(int col=0;col=0&&j=0&&i>=0;i--,j--){ + if(chess[i][j]=='Q'){ + return false; + } + } + return true; + } +} +``` + +### 把数组字符串转换为 ip 地址 + +回溯法插入'.',每次可以插入到1个,2个或者3个字符后面,插入3次之后对得到的字符串进行验证 + +```java +public class Solution { + ArrayList res = new ArrayList<>(); + public ArrayList restoreIpAddresses (String s) { + // write code here + if(s.length() == 0) + return res; + //表示当前字符串s,可以从第0个位置开始插入'.' ,还有3个'.'可以插入 + backTrack(s, 0, 3); + return res; + } + + public void backTrack(String s, int start, int cnt){ + if(cnt == 0){ + String[] splits = s.split("\\."); + //没有插入4个合法的小数点 + if(splits.length < 4) + return; + //判断每一位是否合法 + for(String str:splits){ + if(str.length() > 1 && str.charAt(0) == '0') return; //最前面的数字不能为0 + if(Integer.valueOf(str) > 255) return; //每一位都不能大于255 + } + res.add(s); + return; + } + + if(start >= s.length()) return; //没有插完全部的点 就已经超出字符串的范围了 + int len = s.length(); + //每次将一个字符作为一位 + backTrack(s.substring(0,start+1)+'.' + +s.substring(start+1,len), start+2, cnt-1); + //每次将两位字符作为一位 + if(start < len-2) + backTrack(s.substring(0,start+2)+'.' + +s.substring(start+2,len), start+3, cnt-1); + //每次将三位字符作为一位 + if(start < len-3) + backTrack(s.substring(0,start+3)+'.' + +s.substring(start+3,len), start+4, cnt-1); + } +} +``` + +### 加起来和为目标值的组合 + +```java +import java.util.* ; +public class Solution { + public ArrayList> combinationSum2(int[] num, int target) { + Arrays.sort(num) ; + ArrayList> res = new ArrayList<>() ; + help(target , num , 0 , res , new ArrayList()) ; + return res ; + } + public void help(int target , int[] num , int idx , + ArrayList> res , ArrayList tmp) { + if(target == 0) { + res.add(new ArrayList(tmp)) ; + return ; + } + for(int i = idx ; i < num.length ; i ++) { + if(num[i] > target) return ;//剪枝 + if((i > idx && num[i] == num[i-1])) continue ;//去重 + tmp.add(num[i]) ; + help(target-num[i] , num , i + 1 , res , tmp) ;//递归 + tmp.remove(tmp.size() - 1) ;//回溯 + } + } +} +``` + +## 其他 + +### 螺旋矩阵 + +给定一个m x n大小的矩阵(m行,n列),按螺旋的顺序返回矩阵中的所有元素 + +```java +import java.util.*; +public class Solution { + public ArrayList spiralOrder(int[][] matrix) { + ArrayList list = new ArrayList<>(); + + if(matrix.length == 0) { + return list; + } + + int left = 0; + int right = matrix[0].length - 1; + int top = 0; + int bottom = matrix.length - 1; + int x = 0; + + + while(true) { + for(int i = left; i <= right; i++) { //从左到右 + list.add(matrix[top][i]) ; + } + + if(++top > bottom){ + break; + } + for(int i = top; i <= bottom; i++){ + list.add( matrix[i][right]); //从上到下 + } + + if(left > --right){ + break; + } + for(int i = right; i >= left; i--){ + list.add(matrix[bottom][i]); //从右到左 + } + + if(top > --bottom){ + break; + } + for(int i = bottom; i >= top; i--){ + list.add(matrix[i][left]); //从下到上 + } + + if(++left > right){ + break; + } + } + return list; + } +} +``` + +### 顺时针旋转矩阵 + +原矩阵元素的列数变成新矩阵元素的行数: 原矩阵元素的行数是第2行,旋转后元素的列数是从右往左倒数第2列。 +因此对于原矩阵mat[i][j],旋转后该值应该在新矩阵ans[j][n-i-1]的位置。 + +```java +class Solution { +public: + vector > rotateMatrix(vector > mat, int n) { + // write code here + vector> ans(n,vector(n)); + for(int i=0;i=10){ + sb.append(arr[temp-10]); + } + //小于10,直接加到sb + else{ + sb.append(temp); + } + M/=N; + } + //负数要多加一个负号 + if(f){ + sb.append('-'); + } + //反转后转为字符串返回 + return sb.reverse().toString(); + } +} +``` + +### 反转数字 + +```java +public class Solution { + /** + * + * @param x int整型 + * @return int整型 + */ + public int reverse (int x) { + // write code here + int res = 0; + while(x != 0){ + // 获取最后一位 + int tail = x % 10; + int newRes = res * 10 + tail; + // 如果不等于,说明溢出 + if((newRes - tail) / 10 != res){ + return 0; + } + res = newRes; + x /= 10; + } + + return res; + } +} +``` + +### 大数加法 + +```java +public class Solution { + public String solve (String s, String t){ + int i = s.length() - 1, j = t.length() - 1; + int temp = 0; + StringBuilder out = new Stringbuilder(); + while (i >= 0 || j >= 0 || temp != 0) { + temp += i >= 0 ? s.charAt(i--) - '0' : 0; + temp += j >= 0 ? t.charAt(j--) - '0' : 0; + out.append(temp % 10); + temp = temp / 10; + } + return out.reverse().toString(); + } +} + +public class Solution { + public String solveByJava(String s, String t){ + BigInteger num1 = new BigInteger(s); + BigInteger num2 = new BigInteger(t); + return num1.add(num2).toString(); + } +} +``` + +### 把字符串转换成整数(atoi) + +1、首位空格:通过trim()函数即可处理 +2、正负:通过判断第一位,使用变量储存符号即可 +3、非数字字符:对每一位进行判断,非数字则结束 +4、越界:通过提前预判,判断拼接后是否大于阈值,进行处理 + +```java +public class Solution { + public int StrToInt (String s) { + // write code here + char[] array = s.trim().toCharArray(); + if(array.length==0){ + return 0; + } + int sign = 1; + int res = 0; + int i = 0; + + if(array[i] == '+' || array[i] == '-'){ + sign = array[i++] == '+' ? 1 : -1; + } + while(i < array.length){ + char cur = array[i]; + if(cur < '0' || cur>'9'){ + break; + } + if (res >= Integer.MAX_VALUE / 10) { + if(res > Integer.MAX_VALUE / 10){ + return sign==1 + ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + if(res == Integer.MAX_VALUE / 10){ + if(sign == 1 && (cur - '0') > 7){ + return Integer.MAX_VALUE; + }else if(sign == -1 && (cur - '0') > 8){ + return Integer.MIN_VALUE; + } + } + } + res = res * 10 + (cur - '0'); + i++; + } + return sign * res; + } +} +``` + +### 最长公共前缀 + +```java +// 方法一:先取第一个字符串当做他们的公共前缀 +// 然后找出他和第2个字符串的公共前缀,然后再用这个找出的公共前缀分别和第3个,第4个……判断 +public String longestCommonPrefix(String[] strs) { + //边界条件判断 + if (strs == null || strs.length == 0) + return ""; + //默认第一个字符串是他们的公共前缀 + String pre = strs[0]; + int i = 1; + while (i < strs.length) { + //不断的截取 + while (strs[i].indexOf(pre) != 0) + pre = pre.substring(0, pre.length() - 1); + i++; + } + return pre; +} + +// 方法二:按照字典序排序之后比较字典序最小的子串和字典序最大的子串的相同部分, +// 得到的最长公共前缀就是所有字符串的最长公共前缀 +public class Solution { + public String longestCommonPrefix (String[] strs) { + int len = strs.length; + if(len==0) return ""; + Arrays.sort(strs); + //枚举第一个最小的子串和最后一个最大的子串 + int i = 0; + String a = strs[0]; + String b = strs[len-1]; + for(i = 0;i < a.length()&&a.charAt(i)==b.charAt(i);i++); + return a.substring(0,i); + } +} +``` + +### 回文数字 + +```java +// 方法一:双指针 +import java.util.*; +public class Solution { + /** + * + * @param x int整型 + * @return bool布尔型 + */ + public boolean isPalindrome (int x) { + if(x<0) return false; + // 转换成字符串 + String xs = String.valueOf(x); + // 利用双指针 + int left = 0; + int right = xs.length()-1; + // 比较字符串的头部和尾部是否相同 + while(left < right){ + // 不相同直接返回 + if(xs.charAt(left) != xs.charAt(right)) return false; + left++; + right--; + } + return true; + } +} + +// 方法二:翻转数字 +public class Solution { + /** + * + * @param x int整型 + * @return bool布尔型 + */ + public boolean isPalindrome (int x) { + // write code here + if(x<0) return false; + int reverse = 0; + int tmp = x; + while(tmp>0){ + int div = tmp%10; + // 判断是否会溢出 + if(reverse >= Integer.MAX_VALUE/10 && div > 7) return false; + // 获得反向数字 + reverse = reverse*10 + div; + tmp = tmp/10; + } + return x == reverse; + } +} +``` + +### 字符串变形(反序,大写) + +利用split切割为String数组 +String数组从后往前遍历,拿到具体的String从0到str.length()遍历,并判断大小写然后转换 +需要注意的地方就是s.split(" ",-1),limit需要设置为-1来不舍弃最后的空串 + +```java +import java.util.*; + +public class Solution { + public String trans(String s, int n) { + // write code here + String[] strArr = s.split(" ",-1); // 注意这里limit为-1,不舍弃最后的空串 + StringBuilder sb = new StringBuilder(); + for(int i = strArr.length - 1; i >= 0; i--) { + for(int j = 0; j < strArr[i].length(); j++) { + if(Character.isUpperCase(strArr[i].charAt(j))) { + sb.append(Character.toLowerCase(strArr[i].charAt(j))); + } else { + sb.append(Character.toUpperCase(strArr[i].charAt(j))); + } + } + if(i != 0) { + sb.append(" "); + } + } + return sb.toString(); + } +} +``` + +### 最大值(数组拼接最大数) + +```java +import java.util.*; +public class Solution { + /** + * 最大数 + * @param nums int整型一维数组 + * @return string字符串 + */ + public String solve (int[] nums) { + String[] strArr = new String[nums.length]; + for (int i = 0 ; i < nums.length ; i++) { + strArr[i] = String.valueOf(nums[i]); + } + Arrays.sort(strArr, + (o1, o2) -> Integer.parseInt(o2 + o1) - Integer.parseInt(o1 + o2)); + StringBuilder maxString = new StringBuilder(); + if (strArr[0].equals( "0")) { + return "0"; + } + for (int i = 0 ; i < strArr.length; i++) { + maxString.append(strArr[i]); + } + return maxString.toString(); + } +} +``` + +### 验证ip地址 + +```java +public String validIPAddress(String IP) { + return validIPv4(IP) + ? "IPv4" : (validIPv6(IP) ? "IPv6" : "Neither"); +} + +private boolean validIPv4(String IP) { + String[] strs = IP.split("\\.", -1); + if (strs.length != 4) { + return false; + } + + for (String str : strs) { + if (str.length() > 1 && str.startsWith("0")) { + return false; + } + try { + int val = Integer.parseInt(str); + if (!(val >= 0 && val <= 255)) { + return false; + } + } catch (NumberFormatException numberFormatException) { + return false; + } + } + return true; +} + +private boolean validIPv6(String IP) { + String[] strs = IP.split(":", -1); + if (strs.length != 8) { + return false; + } + + for (String str : strs) { + if (str.length() > 4 || str.length() == 0) { + return false; + } + try { + int val = Integer.parseInt(str, 16); + } catch (NumberFormatException numberFormatException) { + return false; + } + } + return true; +} +``` + +### 二进制中1的个数 + +将数字与1进行与运算,返回结果为1则表明数字二进制最后一位是1,通过对不断数字右移运算判断有多少个1。 + +```java +public class Solution { + // replace替换 + public int NumberOf1(int n) { + return Integer.toBinaryString(n).replace("0","").length(); + } + + // 遍历字符串记录 + public int NumberOf1(int n) { + String str = Integer.toBinaryString(n); + int length = str.length(); + int count = 0; + for (int i=0; i>>= 1; + } + return res; + } +} +``` + +### 第一个只出现一次的字符 + +```java +import java.util.*; +public class Solution { + public static int FirstNotRepeatingChar(String str) { + int[] map = new int[58]; + char[] chs = str.toCharArray(); + for(int i = 0; i < chs.length; i++){ + map[chs[i] - 'A'] ++; + } + int res = -1; + for(int i = 0; i < chs.length; i++){ + if(map[chs[i] - 'A'] == 1){ + res = i; + break; + } + } + return res; + } +} +``` + +## 其他编程题(golang、java) + +### 单例模式 + +双重否定单例模式:即可以保证线程的安全性 +(避免两个线程同时进入到 synchronized (Singleton.class)时, +线程1先获取到了锁,释放后,线程2执行,如果没有第二次空的判断,会导致多次创建对象), +也可以实现只有第一次创建new的时候才会执行到同步代码块中的代码,提高了效率。 + +- java + +```java +// 方法一 +//优点:线程安全,volatile关键词主要是保证多线程之间的可见性, +// 保证线程在每次取值volatile变量都是最新值 +//volatile关键字主要是禁止命令重排序的,但是volatile不是原子性的 +public class Singleton { + private static volatile Singleton instance = null; + private Singleton() {} + public static Singleton getInstance() { + if (null == instance) { + synchronized (Singleton.class) { + if (null == instance) { + instance = new Singleton(); + } + } + } + return instance; + } +} + +// 方法二 +public class Singleton { + private Singleton() {} + private static Singleton getInstance() { + return SingletonHolder.instance; + } + private static class SingletonHolder { + //静态变量值会初始化一次 + private static Singleton instance = new Singleton(); + } +} +``` + +- go + +```go +package main + +import ( + "fmt" + "sync" +) + +var lock = &sync.Mutex{} // 创建互锁 +type Singleton struct {} // 创建结构体 + +var singletonInstance *Singleton // 创建指针 + +func getInstance() *single { + if singletonInstance == nil { //!!!注意这里check nil了两次 + lock.Lock() + defer lock.Unlock() + if singletonInstance == nil { + fmt.Println("创建单例") + singletonInstance = &Singleton{} + } else { + fmt.Println("单例对象已创建") + } + } else { + fmt.Println("单例对象已创建") + } + return singletonInstance +} +``` + +### 实现线程安全的生产者消费者 + +- java + +https://blog.csdn.net/u010983881/article/details/78554671 + +- golang + +https://blog.csdn.net/weixin_50005436/article/details/123065703 + +### 一个10G的文件,里面全部是自然数,一行一个,乱序排列,对其排序。在32位机器上面完成,内存限制为 2G(bitmap原理知道吗?) + +首先,10G文件是不可能一次性放到内存里的。这类问题一般有两种解决方案: + +- 将10G文件分成多个小文件,分别排序,最后合并一个文件; +- 采用bitmap + +如果面试大数据类岗位,可能面试官就想考察你对Mapreduce熟悉程度,要采用第一种merge and sort。 + +如果是算法类岗位,就要考虑bitmap,但需要注意的是bitmap**不能对重复数据进行排序**。这里我们详细介绍一下: + +定量分析一下,32位机器自然数有2^32个,用一个bit来存放一个整数,那么所需的内存是, +`2^32/(8<<20) = 512MB` ,这些数存放在文件中,一行一个,需要20G容量, +所以题目问10G文件,只需要256MB内存就可以完成。 + +bitmap实现具体分为两步:插入一个数,和排序。 + +```go +type BitMap struct { + vec []byte + size int +} + +func New(size int) *BitMap { + return &BitMap{ + size: size, + vec: make([]byte, size), + } +} + +func (bm *BitMap) Set(num int) (ok bool, err error) { + if num/8 >= bm.size { + return false, errors.New("the num overflows the size of bitmap") + } + bm.vec[num/8] |= 1 << (num % 8) + return true, nil +} + +func (bm *BitMap) Exist(num int) bool { + if num/8 >= bm.size { + return false + } + return bm.vec[num/8]&(1<<(num%8)) > 0 +} + +func (bm *BitMap) Sort() (ret []int) { + ret = make([]int, 0) + for i := 0; i < (8 * bm.size); i++ { + if bm.Exist(i) { + ret = append(ret, i) + } + } + return +} +``` + +### 实现使用字符串函数名,调用函数 + +思路:采用反射的Call方法实现。 + +```go +package main +import ( + "fmt" + "reflect" +) + +type Animal struct{ + +} + +func (a *Animal) Eat(){ + fmt.Println("Eat") +} + +func main(){ + a := Animal{} + reflect.ValueOf(&a).MethodByName("Eat").Call([]reflect.Value{}) + +} +``` + +### 负载均衡算法。(一致性哈希) + +```go +package main + +import ( + "fmt" + "sort" + "strconv" +) + +type HashFunc func(key []byte) uint32 + +type ConsistentHash struct { + hash HashFunc + hashvals []int + hashToKey map[int]string + virtualNum int +} + +func NewConsistentHash(virtualNum int, fn HashFunc) *ConsistentHash { + return &ConsistentHash{ + hash: fn, + virtualNum: virtualNum, + hashToKey: make(map[int]string), + } +} + +func (ch *ConsistentHash) AddNode(keys ...string) { + for _, k := range keys { + for i := 0; i < ch.virtualNum; i++ { + conv := strconv.Itoa(i) + hashval := int(ch.hash([]byte(conv + k))) + ch.hashvals = append(ch.hashvals, hashval) + ch.hashToKey[hashval] = k + } + } + sort.Ints(ch.hashvals) +} + +func (ch *ConsistentHash) GetNode(key string) string { + if len(ch.hashToKey) == 0 { + return "" + } + keyhash := int(ch.hash([]byte(key))) + id := sort.Search(len(ch.hashToKey), func(i int) bool { + return ch.hashvals[i] >= keyhash + }) + return ch.hashToKey[ch.hashvals[id%len(ch.hashvals)]] +} + +func main() { + ch := NewConsistentHash(3, func(key []byte) uint32 { + ret, _ := strconv.Atoi(string(key)) + return uint32(ret) + }) + ch.AddNode("1", "3", "5", "7") + testkeys := []string{"12", "4", "7", "8"} + for _, k := range testkeys { + fmt.Printf("k:%s,node:%s\n", k, ch.GetNode(k)) + } +} +``` + +### (Goroutine)有三个函数,分别打印"cat", "fish","dog"要求每一个函数都用一个goroutine,按照顺序打印100次 + +此题目考察channel,用三个无缓冲channel,如果一个channel收到信号则通知下一个。 + +```go +package main + +import ( + "fmt" + "time" +) + +var dog = make(chan struct{}) +var cat = make(chan struct{}) +var fish = make(chan struct{}) + +func Dog() { + <-fish + fmt.Println("dog") + dog <- struct{}{} +} + +func Cat() { + <-dog + fmt.Println("cat") + cat <- struct{}{} +} + +func Fish() { + <-cat + fmt.Println("fish") + fish <- struct{}{} +} + +func main() { + for i := 0; i < 100; i++ { + go Dog() + go Cat() + go Fish() + } + fish <- struct{}{} + + time.Sleep(10 * time.Second) +} +``` + +- Java 实现 + +https://www.cnblogs.com/jyx140521/p/6747750.html + +### 两个协程交替打印10个字母和数字 + +思路:采用channel来协调goroutine之间顺序。 + +主线程一般要waitGroup等待协程退出,这里简化了一下直接sleep。 + +```go +package main + +import ( + "fmt" + "time" +) + +var word = make(chan struct{}, 1) +var num = make(chan struct{}, 1) + +func printNums() { + for i := 0; i < 10; i++ { + <-word + fmt.Println(1) + num <- struct{}{} + } +} +func printWords() { + for i := 0; i < 10; i++ { + <-num + fmt.Println("a") + word <- struct{}{} + } +} + +func main() { + num <- struct{}{} + go printNums() + go printWords() + time.Sleep(time.Second * 1) +} +``` + +- Java 实现 + +```java +package com.lutongnet.util; + +import org.junit.Test; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TransferQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.LockSupport; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @author mifei + * @version 1.0.0 + * @description 多线程测试 + * @date 2020-11-28 15:18 + */ +public class CommonThreadTest { + + Thread t1 = null; + Thread t2 = null; + + /** + * 测试Synchronized的wait和notify写法 + */ + @Test + public void testSynchronized() { + Object o = new Object(); + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numbberArray = "1234567890".toCharArray(); + + t1 = new Thread(()->{ + synchronized (o) { + for (char c: letterArray) { + System.out.println("字母:" + c); + try { + o.notify(); + o.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + o.notify(); + } + }); + + t2 = new Thread(()->{ + synchronized (o) { + for (char c: numbberArray) { + System.out.println("数字:" + c); + try { + o.notify(); + o.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + o.notify(); + } + }); + + t1.start(); + t2.start(); + } + + /** + * 测试ReenTrantLock写法 + */ + @Test + public void testReenTrantlock() { + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numberArray = "1234567890".toCharArray(); + + Lock lock = new ReentrantLock(); + Condition letterCondition = lock.newCondition(); + Condition numberCondition = lock.newCondition(); + + new Thread(()->{ + try { + lock.lock(); + for (char c: letterArray) { + System.out.println("字母:" + c); + numberCondition.signal(); + letterCondition.await(); + } + numberCondition.signal(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "t1").start(); + + new Thread(()->{ + try { + lock.lock(); + for (char c: numberArray) { + System.out.println("数字:" + c); + letterCondition.signal(); + numberCondition.await(); + } + letterCondition.signal(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "t2").start(); + } + + /** + * 测试LockSupport写法 + */ + @Test + public void testLockSupport() { + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numberArray = "1234567890".toCharArray(); + + t1 = new Thread(()->{ + for (char c: letterArray) { + System.out.println("字母:" + c); + LockSupport.unpark(t2); + LockSupport.park(); + } + }); + + t2 = new Thread(()->{ + for (char c: numberArray) { + LockSupport.park(); + System.out.println("数字:" + c); + LockSupport.unpark(t1); + } + }); + + t1.start(); + t2.start(); + } + + /** + * 测试BlockingQueue写法 + */ + @Test + public void testBlockingQueue() { + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numberArray = "1234567890".toCharArray(); + + BlockingQueue q1 = new ArrayBlockingQueue(1); + BlockingQueue q2 = new ArrayBlockingQueue(1); + + new Thread(()->{ + for (char c: letterArray) { + System.out.println("字母:" + c); + try { + q1.put("ok"); + q2.take(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + } + }, "t1").start(); + + new Thread(()->{ + for (char c: numberArray) { + try { + q1.take(); + System.out.println("数字:" + c); + q2.put("ok"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "t2").start(); + } + + /** + * 测试AtomicInteger写法 + */ + @Test + public void testAtomicInteger() { + AtomicInteger threadNo = new AtomicInteger(1); + + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numberArray = "1234567890".toCharArray(); + + new Thread(()->{ + for (char c: letterArray) { + while (threadNo.get() != 1) {} + System.out.println("字母:" + c); + threadNo.set(2); + } + }, "t1").start(); + + new Thread(()->{ + for (char c: numberArray) { + while (threadNo.get() != 2) {} + System.out.println("数字:" + c); + threadNo.set(1); + } + }, "t2").start(); + } + + /** + * 测试TransferQueue写法 + */ + @Test + public void testTransferQueue() { + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numberArray = "1234567890".toCharArray(); + + TransferQueue queue = new LinkedTransferQueue<>(); + new Thread(()->{ + try { + for (char c : letterArray) { + System.out.println("数字:" + queue.take()); + queue.transfer(c); + } + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, "t1").start(); + + new Thread(()->{ + try { + for (char c : numberArray) { + queue.transfer(c); + System.out.println("字母:" + queue.take()); + } + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, "t2").start(); + + } +} +``` + +### 启动 2个groutine 2秒后取消, 第一个协程1秒执行完,第二个协程3秒执行完。 + +思路:采用`ctx, _ := context.WithTimeout(context.Background(), time.Second*2)`实现2s取消。 +协程执行完后通过channel通知,是否超时。 + +```go +package main + +import ( + "context" + "fmt" + "time" +) + +func f1(in chan struct{}) { + + time.Sleep(1 * time.Second) + in <- struct{}{} + +} + +func f2(in chan struct{}) { + time.Sleep(3 * time.Second) + in <- struct{}{} +} + +func main() { + ch1 := make(chan struct{}) + ch2 := make(chan struct{}) + ctx, _ := context.WithTimeout(context.Background(), 2*time.Second) + + go func() { + go f1(ch1) + select { + case <-ctx.Done(): + fmt.Println("f1 timeout") + break + case <-ch1: + fmt.Println("f1 done") + } + }() + + go func() { + go f2(ch2) + select { + case <-ctx.Done(): + fmt.Println("f2 timeout") + break + case <-ch2: + fmt.Println("f2 done") + } + }() + time.Sleep(time.Second * 5) +} +``` + + +### 当select监控多个chan同时到达就绪态时,如何先执行某个任务? + +可以在子case再加一个for select语句。 + +```go +func priority_select(ch1, ch2 <-chan string) { + for { + select { + case val := <-ch1: + fmt.Println(val) + case val2 := <-ch2: + priority: + for { + select { + case val1 := <-ch1: + fmt.Println(val1) + + default: + break priority + } + } + fmt.Println(val2) + } + } + +} ``` \ No newline at end of file diff --git "a/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" "b/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" index 9297b4d..aa25c45 100644 --- "a/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" +++ "b/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" @@ -197,6 +197,10 @@ i++; 如果我们需要避免不可重复读的问题的发生,那么我们可以使用**Next-Key Lock算法**(设置事务的隔离级别为`READ REPEATABLE`)来避免,在MySQL中,不可重复读问题就是Phantom Problem,也就是**幻像问题**。 +#### 幻读 + +幻读本质上也属于不可重复读的情况,会话 A 读取某个范围的数据,会话 B 在这个范围内**插入**新的数据,会话 A 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 + #### 丢失更新 **丢失更新:** 指的是一个事务的更新操作会被另外一个事务的更新操作所覆盖,从而导致数据的不一致。在当前数据库的任何隔离级别下都不会导致丢失更新问题,要出现这个问题,在多用户计算机系统环境下有可能出现这种问题。 @@ -233,7 +237,37 @@ i++; ![](http://image.ouyangsihai.cn/Fh1arOgS78BnrjBXKwGw8NSP27uM) +### MVCC 实现原理 + +在理解 MVCC 的实现原理之前,需要先带大家了解一下 **版本链**。 + +我们都知道,在 InnoDB 每个事务都有一个唯一的事务 ID(transaction id),该 ID 是在启动一个事务时申请的并且严格顺序递增。 + +另外,数据表中的每行数据都是有多个版本的,每次事务更新都会生成新的版本,并且把本次事务的 transaction id 赋值给这个数据版本的事务 ID(row trx_id)。 + +除此之外,还有一个 roll_pointer指针,该指针 ROLL_PTR 把一个数据行的所有快照版本记录连接起来。 + +undo log 的回滚机制也是依靠这个版本链,每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样: + +![](http://image.ouyangsihai.cn/Fidy__nsyaUj1N3MGfGlu1hjbYMM) + +有了上面的知识储备,所谓的 MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用**读已提交(READ COMMITTD)**、**可重复读(REPEATABLE READ)** 这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。 + +这两个隔离级别的一个很大不同就是:生成 ReadView 的时机不同,READ COMMITTD 在每一次进行普通 SELECT 操作前都会生成一个 ReadView,而 REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个ReadView,数据的可重复读其实就是 ReadView 的重复使用。 +#### **ReadView** + +MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, ...},还有该列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。 + +在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用: + +- TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。 +- TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。 +- TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断: + - 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。 + - 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。 + +在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。 ### 锁相关 > 数据库中锁机制,说说数据库中锁的类型 @@ -258,10 +292,17 @@ InnoDB存储引擎中存在着不同类型的锁,下面一一介绍一下。 为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB存储引擎支持一种额外的锁方式,就称为**意向锁**,意向锁在 InnoDB 中是**表级锁**,意向锁分为: - - 意向共享锁:表达一个事务想要获取一张表中某几行的共享锁。 - 意向排他锁:表达一个事务想要获取一张表中某几行的排他锁。 +在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。 + +意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定: + +- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁; +- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。 + +通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。 另外,这些锁之间的并不是一定可以共存的,有些锁之间是不兼容的,所谓**兼容性**就是指事务 A 获得一个某行某种锁之后,事务 B 同样的在这个行上尝试获取某种锁,如果能立即获取,则称锁兼容,反之叫冲突。 @@ -269,12 +310,14 @@ InnoDB存储引擎中存在着不同类型的锁,下面一一介绍一下。 - S or X (共享锁、排他锁)的兼容性 -![](http://image.ouyangsihai.cn/Fvr5OSfX9nP2Ip30O088kVx-Pdza) +![](https://img-blog.csdnimg.cn/20191022121422475.png) - IS or IX (共享、排他)意向锁的兼容性 -![](http://image.ouyangsihai.cn/Fgf-Pg6JPVJ7CmPyrdcow_5q-VZm) +![](https://img-blog.csdnimg.cn/20191022121422644.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) + +**注意:** 任意 IS/IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁。 > MySQL中锁的粒度 @@ -310,7 +353,7 @@ InnoDB存储引擎中存在着不同类型的锁,下面一一介绍一下。 ##### MySQL 不同引擎支持的锁的粒度 -![](http://image.ouyangsihai.cn/FvpMs-9FmTS7IKUBEv3WCm0IFJJH) +![](https://img-blog.csdnimg.cn/20191022121422292.png) > 了解一致性非锁定读和一致性锁定读吗? @@ -329,7 +372,7 @@ InnoDB存储引擎中存在着不同类型的锁,下面一一介绍一下。 **一致性非锁定读(consistent nonlocking read)** 是指InnoDB存储引擎通过多版本控制(MVVC)读取当前数据库中行数据的方式。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB会去读取行的一个快照。所以,非锁定读机制大大提高了数据库的并发性。 - ![来自网络:侵权删](http://image.ouyangsihai.cn/FhF001C8JBPwaEXPgJ9TEzFT8C6X) + ![来自网络:侵权删](https://img-blog.csdnimg.cn/2019102212142395.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) 一致性非锁定读是InnoDB默认的读取方式,即读取不会占用和等待行上的锁。在事务隔离级别`READ COMMITTED`和`REPEATABLE READ`下,InnoDB使用一致性非锁定读。 @@ -339,7 +382,7 @@ InnoDB存储引擎中存在着不同类型的锁,下面一一介绍一下。 首先创建一张表; -![](http://image.ouyangsihai.cn/FqzGMzTgnaAxaSX2Ko9YVUjnmFWt) +![](https://img-blog.csdnimg.cn/20191022121423315.png) 插入一条数据; @@ -353,19 +396,19 @@ insert into lock_test values(1); select @@tx_isolation; ``` -![](http://image.ouyangsihai.cn/Fn3A5-fYhDs8rb0VvNMC1OL6B9sW) +![](https://img-blog.csdnimg.cn/20191022121423531.png) 下面分为两种事务进行操作。 在`REPEATABLE READ`事务隔离级别下; -![](http://image.ouyangsihai.cn/FhE1WGAeSkZGAq90Cx1alh3dZTVa) +![](https://img-blog.csdnimg.cn/20191022121423748.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) 在`REPEATABLE READ`事务隔离级别下,读取事务开始时的行数据,所以当会话B修改了数据之后,通过以前的查询,还是可以查询到数据的。 在`READ COMMITTED`事务隔离级别下; -![](http://image.ouyangsihai.cn/FrdXfAw47rFAzRks4-4Y9IoWKtl4) +![](https://img-blog.csdnimg.cn/20191022121423939.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) 在`READ COMMITTED`事务隔离级别下,读取该行版本最新的一个快照数据,所以,由于B会话修改了数据,并且提交了事务,所以,A读取不到数据了。 @@ -380,10 +423,10 @@ InnoDB存储引擎有3种行锁的算法,其分别是: **Record Lock**:总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。 **Next-Key Lock**:结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。举个例子10,20,30,那么该索引可能被Next-Key Locking的区间为: -![](http://image.ouyangsihai.cn/FrOLmJtKBxs70A0QHAc35CccZg2Y) +![](https://img-blog.csdnimg.cn/20191022121424137.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) 除了Next-Key Locking,还有**Previous-Key Locking**技术,这种技术跟Next-Key Lock正好相反,锁定的区间是区间范围和前一个值。同样上述的值,使用Previous-Key Locking技术,那么可锁定的区间为: -![](http://image.ouyangsihai.cn/Fr-S9vxXpA--rHGiPkWMpCdhEKxT) +![](https://img-blog.csdnimg.cn/20191022121424338.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) 不是所有索引都会加上Next-key Lock的,这里有一种**特殊的情况**,在查询的列是唯一索引(包含主键索引)的情况下,`Next-key Lock`会降级为`Record Lock`。 @@ -409,12 +452,12 @@ SELECT * FROM test WHERE y = 3 FOR UPDATE - 对于主键x -![](http://image.ouyangsihai.cn/Fo3ptcBFShRMwLAC1guVV4mUO-93) +![](https://img-blog.csdnimg.cn/20191022121424525.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) - 辅助索引y -![](http://image.ouyangsihai.cn/Fj8hUM6slurRWb5SImLWWDM2YDu0) +![](https://img-blog.csdnimg.cn/20191022121424732.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) 用户可以通过以下两种方式来显示的关闭Gap Lock: @@ -696,5 +739,45 @@ MyISAM引擎会把表自增主键的最大id记录到数据文件中,做了持 关系型数据库的优势在于支持更加复杂的sql操作、事务支持和使用表结构更加易于维护。 +> binlog、redo log和undo log + +redo log(重做日志)是InnoDB存储引擎独有的,它让MySQL拥有了崩溃恢复能力。 + +比如 MySQL 实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的**持久性与完整性**。 + +redo log 记录的是数据的物理变化。 + + +binlog 记录了数据库表结构和表数据变更,比如update/delete/insert/truncate/create。它不会记录select(因为这没有对表没有进行变更),存储着每条变更的SQL语句(当然从下面的图看来看,不止SQL,还有XID「事务Id」等等)。 + +主要有两个作用:**复制和恢复数据** + +- MySQL在公司使用的时候往往都是一主多从结构的,从服务器需要与主服务器的数据保持一致,这就是通过binlog来实现的 + +- 数据库的数据被干掉了,我们可以通过binlog来对数据进行恢复。 + +undo log主要有两个作用:**回滚和多版本控制(MVCC)** + +在数据修改的时候,不仅记录了redo log,还记录undo log,如果因为某些原因导致事务失败或回滚了,可以用undo log进行回滚 + +undo log 主要存储的也是**逻辑日志**,比如我们要insert一条数据了,那undo log会记录的一条对应的delete日志。我们要update一条记录时,它会记录一条对应相反的update记录。 + +这也应该容易理解,毕竟回滚嘛,跟需要修改的操作相反就好,这样就能达到回滚的目的。因为支持回滚操作,所以我们就能保证**原子性**。 + +> mysql 优化思路 + +https://mp.weixin.qq.com/s/jtuLb8uAIHJNvNpwcIZfpA + +https://www.cnblogs.com/jay-huaxiao/p/12995510.html + +> mysql 语法和复杂语句练习题 + +- 常用语法 + +https://www.jb51.net/article/156898.htm +www.cyc2018.xyz/算法/基础/算法 - 排序.html +- 练习题 +https://www.jianshu.com/p/476b52ee4f1b +www.cyc2018.xyz/算法/基础/算法 - 排序.html diff --git "a/docs/golang/\351\235\242\350\257\225\351\242\230/golang\351\235\242\350\257\225\351\242\230\346\225\264\347\220\206-\345\276\256\344\277\241\345\256\214\346\225\264.md" "b/docs/golang/\351\235\242\350\257\225\351\242\230/golang\351\235\242\350\257\225\351\242\230\346\225\264\347\220\206-\345\276\256\344\277\241\345\256\214\346\225\264.md" new file mode 100644 index 0000000..e65120c --- /dev/null +++ "b/docs/golang/\351\235\242\350\257\225\351\242\230/golang\351\235\242\350\257\225\351\242\230\346\225\264\347\220\206-\345\276\256\344\277\241\345\256\214\346\225\264.md" @@ -0,0 +1,836 @@ +> ❝ +> +> 原文: https://zhuanlan.zhihu.com/p/519979757,作者:沪猿小韩。为适于阅读,部分样式及内容有做简单修改。 +> +> ❞ + +# 前言 + +文章部分题目来源于网络,答案系个人结合5月份面试了近30家公司整理所得,最后附录参考原文链接,如有遗漏的原文出处请联系本人。不对之处望批评指正,答案需要加上自己的思考,最好是代码实践下。 + +参与过面试的企业有:zg人寿,睿科lun,qi猫,yun汉商城,zi节跳动,特斯la,虾P,chuan音,qi安信,ai立信等大大小小企业近30家,BAT简历都过不了。 + +## 面试建议 + +### 技术部分 + +1)算法部分,刷LeetCode就完事了,这是一个长期的过程,短期突击没啥效果,因为题目太多了。 + +2)语言基础,细分为: + +* golang基础及原理,就是本文主要内容了; +* mysql基础及原理; +* redis基础及原理; +* kafka或其他消息中间件(如果没用过,需要了解大概的底层原理及结构); +* linux常用的命令,比如定时脚本几个参数时间分别代表啥,文件权限需要搞清楚,进程内存占用命令;小公司还要懂一些前端的知识,因为他们希望你什么都会。 + +3)项目经验,可以搞一个基于gin的后端接口服务的web框架,一般会问你怎么实现的;以及微服务了解一下。 + +### 非技术部分 + +1)因为上海5月份居家办公,远程面试,这些题目准备一份,遇到卡壳的题目完全可以参考你准备好的答案,因为视频面试你眼睛是看着面试官还是题目是不太容易区分的(把题目窗口置顶)。 + +2)HR面也可以完全准备一份可能问到的问题的答案,并不是说你不会回答,而是会让你的表达更顺畅,其次也说明你是有备而来的,我在某拉公司面试就吃了这个亏,技术通过,HR说我的表达能力不行(后续我也会把这个模板分享出来,感谢我媳妇充当面试官,以及指导如何高情商的回答HR的问题)。 + +3)可以自己录音面试回答,看看自己的语气、音量,顺畅度,如果自己听了都不舒服,面试官可能也不舒服。 + +## 一、基础部分 + +#### 1、golang 中 make 和 new 的区别?(基本必问) + +共同点:给变量分配内存 + +不同点: + +* 作用变量类型不同,new给string,int和数组分配内存,make给切片,map,channel分配内存; + +* 返回类型不一样,new返回指向变量的指针,make返回变量本身; + +* new 分配的空间被清零。make 分配空间后,会进行初始化; + +* 字节的面试官还说了另外一个区别,就是分配的位置,在堆上还是在栈上?这块我比较模糊,大家可以自己探究下,我搜索出来的答案是golang会弱化分配的位置的概念,因为编译的时候会自动内存逃逸处理,懂的大佬帮忙补充下:make、new内存分配是在堆上还是在栈上? + +#### string 底层数据结构 + +```go +// from: string.go 在GoLand IDE中双击shift快速找到 +type stringStruct struct { + array unsafe.Pointer // 指向一个 [len]byte 的数组 + length int // 长度 +} +``` + +#### []string 和 []byte 的区别 + +string + +1 .是一个指针,指向某个数组的首地址 +[]byte + +1 .是一个切片slice。一个封装了数组的结构体 +2 .slice结构 +```go +type slice struct { + array unsafe.Pointer + len int + cap int +``` +使用场景 + +1 .想要在本身原地修改,就只能使用[]byte +2 .string不能为nil,想要返回nil表达特殊含义,只能使用[]byte +3 .string可以直接比较,而[]byte不可以,所以[]byte不可以当map的key值。 +4 .因为无法修改string中的某个字符,需要粒度小到操作一个字符时,用[]byte +5 .[]byte切片这么灵活,想要用切片的特性就用[]byte +6 .需要大量字符串处理的时候用[]byte,性能好很多 +区别 + +1 .string的指针指向的内容是不可以改变的,所以每次更改一次字符串,都需要重新分配内存。之前的内存还需要GC收回,这是导致string效率底下的根本原因 +2 .如果我们保存的字符在 ASCII 表的,比如[0-1, a-z,A-Z..]直接可以保存到 byte +3 .如果我们保存的字符对应码值大于 255,这时我们可以考虑使用 int 类型保存 + +#### 2、数组和切片的区别 (基本必问) + +相同点: + +* 只能存储一组相同类型的数据结构 + +* 都是通过下标来访问,并且有容量长度,长度通过 len 获取,容量通过 cap 获取 + +区别: + +* 数组是定长,访问和复制不能超过数组定义的长度,否则就会下标越界,切片长度和容量可以自动扩容 + +* 数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组,内存地址也就随之改变 + +简洁的回答: + +1)定义方式不一样 2)初始化方式不一样,数组需要指定大小,大小不改变 3)函数的传递方式不一样,数组传递的是值,切片传的是地址。 + +数组的定义 + +```go +var a1 [3]int + +var a2 [...]int{1,2,3} +``` + +切片的定义 + +```go +var a1 []int + +var a2 :=make([]int,3,5) +``` + +数组的初始化 + +```go +a1 := [...]int{1,2,3} + +a2 := [5]int{1,2,3} +``` + +切片的初始化 + +```go +b:= make([]int,3,5) +``` +#### 3、for range 的时候它的地址会发生变化么? + +答:在 `for a,b := range c` 遍历中, a 和 b 在内存中只会存在一份,即之后每次循环时遍历到的数据都是以值覆盖的方式赋给 a 和 b,a,b 的内存地址始终不变。由于有这个特性,**「for 循环里面如果开协程,不要直接把 a 或者 b 的地址传给协程」**。解决办法:在每次循环时,创建一个临时变量。 + +#### 4、go defer,多个 defer 的顺序,defer 在什么时机会修改返回值? + +作用:defer延迟函数,释放资源,收尾工作;如释放锁,关闭文件,关闭链接;捕获panic; + +避坑指南:defer函数紧跟在资源打开后面,否则defer可能得不到执行,导致内存泄露。 + +多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中 + +解析:函数的 return 语句并不是原子级的,实际上 return 语句只代理汇编指令 ret。defer 语句是在返回前执行,所以返回过程是:「设置返回值—>执行defer—>ret」。defer可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针 参考:[Go高阶指南07,一文搞懂 defer 实现原理)](https://mp.weixin.qq.com/s?__biz=Mzk0NzI3Mjk1Mg==&mid=2247484683&idx=1&sn=262b205caeef06489e5ac8cf84d44112&chksm=c378289cf40fa18aef1d26e1437232b6b20a2f1f7129951d3b094eefa04007ee3fcd0af4655f&token=1319045915&lang=zh_CN&scene=21#wechat_redirect) + +有名返回值 + +```go +func b() (i int) { + defer func() { + i++ + fmt.Println("defer2:", i) + }() + defer func() { + i++ + fmt.Println("defer1:", i) + }() + return i //或者直接写成return +} +func main() { + fmt.Println("return:", b()) +} + +//运行结果: +//defer1: 1 +//defer2: 2 +//return: 2 +``` + +函数返回指针 + +```go +func c() *int { + var i int + defer func() { + i++ + fmt.Println("defer2:", i) + }() + defer func() { + i++ + fmt.Println("defer1:", i) + }() + return &i +} +func main() { + fmt.Println("return:", *(c())) +} + +//运行结果: +//defer1: 1 +//defer2: 2 +//return: 2 +``` +#### 5、uint 类型溢出问题 + +超过最大存储值如uint8最大是255 + +var a uint8 =255 + +var b uint8 =1 + +a+b = 0总之类型溢出会出现难以意料的事 + +![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==) +#### 6、能介绍下 rune 类型吗? + +相当int32 + +golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8 + +byte 等同于int8,常用来处理ascii字符 + +rune 等同于int32,常用来处理unicode或utf-8字符 + +![图片](https://mmbiz.qpic.cn/mmbiz_png/3wgqfEribn6fXH7I19WrA9zDKjjmfnh6uvufYzu884pVVTs93HTicKncwIbXWzxftNmicFZDkkP6bqdojwN5LicfXw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) +#### 7、 golang 中解析 tag 是怎么实现的?反射原理是什么?(中高级肯定会问,比较难,需要自己多去总结) + +参考如下连接golang中struct关于反射tag (https://blog.csdn.net/paladinosment/article/details/42570937) + +```go +type User struct { + name string `json:name-field` + age int +} +func main() { + user := &User{"John Doe The Fourth", 20} + + field, ok := reflect.TypeOf(user).Elem().FieldByName("name") + if !ok { + panic("Field not found") + } + fmt.Println(getStructTag(field)) +} + +func getStructTag(f reflect.StructField) string { + return string(f.Tag) +} +``` + +o 中解析的 tag 是通过反射实现的,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力或动态知道给定数据对象的类型和结构,并有机会修改它。反射将接口变量转换成反射对象 Type 和 Value;反射可以通过反射对象 Value 还原成原先的接口变量;反射可以用来修改一个变量的值,前提是这个值可以被修改;tag是啥:结构体支持标记,name string `json:name-field` 就是 `json:name-field` 这部分 + +gorm json yaml gRPC protobuf gin.Bind()都是通过反射来实现的 + +#### 8、调用函数传入结构体时,应该传值还是指针?(Golang 都是传值) + +Go 的函数参数传递都是值传递。所谓值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。参数传递还有引用传递,所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数 + +因为 Go 里面的 map,slice,chan 是引用类型。变量区分值类型和引用类型。 +所谓值类型:变量和变量的值存在同一个位置。 +所谓引用类型:变量和变量的值是不同的位置,变量的值存储的是对值的引用。 +但并不是 map,slice,chan 的所有的变量在函数内都能被修改,不同数据类型的底层存储结构和实现可能不太一样,情况也就不一样。 + +#### 9、讲讲 Go 的 slice 底层数据结构和一些特性? + +答:Go 的 slice 底层数据结构是由一个 array 指针指向底层数组,len 表示切片长度,cap 表示切片容量。slice 的主要实现是扩容。对于 append 向 slice 添加元素时,假如 slice 容量够用,则追加新元素进去,slice.len++,返回原来的 slice。当原容量不够,则 slice 先扩容,扩容之后 slice 得到新的 slice,将元素追加进新的 slice,slice.len++,返回新的 slice。对于切片的扩容规则:当切片比较小时(容量小于 1024),则采用较大的扩容倍速进行扩容(新的扩容会是原来的 2 倍),避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价。当切片较大的时(原来的 slice 的容量大于或者等于 1024),采用较小的扩容倍速(新的扩容将扩大大于或者等于原来 1.25 倍),主要避免空间浪费,网上其实很多总结的是 1.25 倍,那是在不考虑内存对齐的情况下,实际上还要考虑内存对齐,扩容是大于或者等于 1.25 倍。 + +(关于刚才问的 slice 为什么传到函数内可能被修改,如果 slice 在函数内没有出现扩容,函数外和函数内 slice 变量指向是同一个数组,则函数内复制的 slice 变量值出现更改,函数外这个 slice 变量值也会被修改。如果 slice 在函数内出现扩容,则函数内变量的值会新生成一个数组(也就是新的 slice,而函数外的 slice 指向的还是原来的 slice,则函数内的修改不会影响函数外的 slice。) + +#### 10、讲讲 Go 的 select 底层数据结构和一些特性?(难点,没有项目经常可能说不清,面试一般会问你项目中怎么使用select) + +答:go 的 select 为 golang 提供了多路 IO 复用机制,和其他 IO 复用一样,用于检测是否有读写事件是否 ready。linux 的系统 IO 模型有 select,poll,epoll,go 的 select 和 linux 系统 select 非常相似。 + +select 结构组成主要是由 case 语句和执行的函数组成 select 实现的多路复用是:每个线程或者进程都先到注册和接受的 channel(装置)注册,然后阻塞,然后只有一个线程在运输,当注册的线程和进程准备好数据后,装置会根据注册的信息得到相应的数据。 + +select 的特性 + +* select 操作至少要有一个 case 语句,出现读写 nil 的 channel 该分支会忽略,在 nil 的 channel 上操作则会报错。 +* select 仅支持管道,而且是单协程操作。 +* 每个 case 语句仅能处理一个管道,要么读要么写。 +* 多个 case 语句的执行顺序是随机的。 +* 存在 default 语句,select 将不会阻塞,但是存在 default 会影响性能。 + +- select 和 channel 的使用场景:https://blog.csdn.net/u011240877/article/details/123611525 + +#### 11、讲讲 Go 的 defer 底层数据结构和一些特性? + +答:每个 defer 语句都对应一个_defer 实例,多个实例使用指针连接起来形成一个单连表,保存在 gotoutine 数据结构中,每次插入_defer 实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。 + +defer 的规则总结: + +* 延迟函数的参数是 defer 语句出现的时候就已经确定了的。 +* 延迟函数执行按照后进先出的顺序执行,即先出现的 defer 最后执行。 +* 延迟函数可能操作主函数的返回值。 +* 申请资源后立即使用 defer 关闭资源是个好习惯。 + +#### 12、单引号,双引号,反引号的区别? + +单引号,表示byte类型或rune类型,对应 uint8和int32类型,默认是 rune 类型。byte用来强调数据是raw data,而不是数字;而rune用来表示Unicode的code point。 + +双引号,才是字符串,实际上是字符数组。可以用索引号访问某字节,也可以用len()函数来获取字符串所占的字节长度。 + +反引号,表示字符串字面量,但不支持任何转义序列。字面量 raw literal string 的意思是,你定义时写的啥样,它就啥样,你有换行,它就换行。你写转义字符,它也就展示转义字符。 + +# 二、map相关 + +#### 1、map 使用注意的点,是否并发安全? + +map的类型是map[key],key类型的ke必须是可比较的,通常情况,会选择内建的基本类型,比如整数、字符串做key的类型。如果要使用struct作为key,要保证struct对象在逻辑上是不可变的。在Go语言中,map[key]函数返回结果可以是一个值,也可以是两个值。map是无序的,如果我们想要保证遍历map时元素有序,可以使用辅助的数据结构,例如orderedmap。 + +第一,一定要先初始化,否则panic + +第二,map类型是容易发生并发访问问题的。不注意就容易发生程序运行时并发读写导致的panic。Go语言内建的map对象不是线程安全的,并发读写的时候运行时会有检查,遇到并发问题就会导致panic。 + +#### 2、map 循环是有序的还是无序的? + +无序的, map 因扩张⽽重新哈希时,各键值项存储位置都可能会发生改变,顺序自然也没法保证了,所以官方避免大家依赖顺序,直接打乱处理。就是 for range map 在开始处理循环逻辑的时候,就做了随机播种 + +#### 3、 map 中删除一个 key,它的内存会释放么?(常问) + +如果删除的元素是值类型,如int,float,bool,string以及数组和struct,map的内存不会自动释放 + +如果删除的元素是引用类型,如指针,slice,map,chan等,map的内存会自动释放,但释放的内存是子元素应用类型的内存占用 + +将map设置为nil后,内存被回收。 + +这个问题还需要大家去搜索下答案,我记得有不一样的说法,谨慎采用本题答案。 + +#### 4、怎么处理对 map 进行并发访问?有没有其他方案?区别是什么? + +方式一、使用内置sync.Map,详细参考#### golang sync.map 原理和使用 #### (https://yebd1h.smartapps.cn/pages/blog/index?_swebFromHost=baiduboxapp&blogId=114628932&_swebfr=1) + +方式二、使用读写锁实现并发安全mapgolang线程安全map (https://yebd1h.smartapps.cn/pages/blog/index?_swebFromHost=baiduboxapp&blogId=123259483&_swebfr=1) + +#### 5、 nil map 和空 map 有何不同? + +1. 可以对未初始化的map进行取值,但取出来的东西是空: + +```go +var m1 map[string]string + +fmt.Println(m1["1"]) +``` + +1. 不能对未初始化的map进行赋值,这样将会抛出一个异常: + +```go +var m1 map[string]string + +m1["1"] = "1" + +panic: assignment to entry in nil map +``` + +1. 通过fmt打印map时,空map和nil map结果是一样的,都为map[]。所以,这个时候别断定map是空还是nil,而应该通过map == nil来判断。 + nil map 未初始化,空map是长度为空 + +#### 6、map 的数据结构是什么?是怎么实现扩容? + +答:golang 中 map 是一个 kv 对集合。底层使用 hash table,用链表来解决冲突 ,出现冲突时,不是每一个 key 都申请一个结构通过链表串起来,而是以 bmap 为最小粒度挂载,一个 bmap 可以放 8 个 kv。在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。每个 map 的底层结构是 hmap,是有若干个结构为 bmap 的 bucket 组成的数组。每个 bucket 底层都采用链表结构。 + +hmap 的结构如下: + +```go +type hmap struct { + count int // 元素个数 + flags uint8 + B uint8 // 扩容常量相关字段B是buckets数组的长度的对数 2^B + noverflow uint16 // 溢出的bucket个数 + hash0 uint32 // hash seed + buckets unsafe.Pointer // buckets 数组指针 + oldbuckets unsafe.Pointer // 结构扩容的时候用于赋值的buckets数组 + nevacuate uintptr // 搬迁进度 + extra *mapextra // 用于扩容的指针 +} +``` + +map 的容量大小 + +底层调用 makemap 函数,计算得到合适的 B,map 容量最多可容纳 6.52^B 个元素,6.5 为装载因子阈值常量。装载因子的计算公式是:装载因子=填入表中的元素个数/散列表的长度,装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。底层调用 makemap 函数,计算得到合适的 B,map 容量最多可容纳 6.52^B 个元素,6.5 为装载因子阈值常量。装载因子的计算公式是:装载因子=填入表中的元素个数/散列表的长度,装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。 + +触发 map 扩容的条件 + +* 装载因子超过阈值,源码里定义的阈值是 6.5。 +* overflow 的 bucket 数量过多 map 的 bucket 定位和 key 的定位高八位用于定位 bucket,低八位用于定位 key,快速试错后再进行完整对比。 + +#### 7、slices能作为map类型的key吗? + +当时被问得一脸懵逼,其实是这个问题的变种:golang 哪些类型可以作为map key? + +答案是:在golang规范中,可比较的类型都可以作为map key;这个问题又延伸到在:golang规范中,哪些数据类型可以比较? + +不能作为map key 的类型包括: + +slices maps functions + +# 三、context相关 + +#### 1、context 结构是什么样的?context 使用场景和用途? + +(难,也常常问你项目中怎么用,光靠记答案很难让面试官满意,反正有各种结合实际的问题) + +参考链接:go context详解 (https://www.cnblogs.com/juanmaofeifei/p/14439957.html) + +答:Go 的 Context 的数据结构包含 Deadline,Done,Err,Value,Deadline 方法返回一个 time.Time,表示当前 Context 应该结束的时间,ok 则表示有结束时间,Done 方法当 Context 被取消或者超时时候返回的一个 close 的 channel,告诉给 context 相关的函数要停止当前工作然后返回了,Err 表示 context 被取消的原因,Value 方法表示 context 实现共享数据存储的地方,是协程安全的。context 在业务中是经常被使用的, + +其主要的应用 : + +1:上下文控制,2:多个 goroutine 之间的数据交互等,3:超时控制:到某个时间点超时,过多久超时。 + +# 四、channel相关 + +#### 1、channel 是否线程安全?锁用在什么地方? + +![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==) + +#### 2、go channel 的底层实现原理 (数据结构) + +![图片](https://mmbiz.qpic.cn/mmbiz_png/3wgqfEribn6fXH7I19WrA9zDKjjmfnh6uAhBkuiacRMG4zqoayxUrPtUZQHoNxic9GOvXljTCre35EUTwzrp88LaQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +底层结构需要描述出来,这个简单,buf,发送队列,接收队列,lock。 + +#### 3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?(各类变种题型,重要) + +![图片](https://mmbiz.qpic.cn/mmbiz_png/3wgqfEribn6fXH7I19WrA9zDKjjmfnh6ucsjGfTGOQjmefjjU9H1cVmvqKCNniaszG72rlBlJsl5yKic2kDG0WVcA/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +还要去了解一下单向channel,如只读或者只写通道常见的异常问题,这块还需要大家自己总结总结,有懂的大佬也可以评论发送答案。 + +#### 4、向 channel 发送数据和从 channel 读数据的流程是什么样的? + +发送流程: + +![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==) + +接收流程:![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==) + +这个没啥好说的,底层原理,1、2、3描述出来,保证面试官满意。具体的文字描述下面一题有,channel的概念多且复杂,脑海中有个总分的概念,否则你说的再多,面试官也抓不住你说的重点,等于白说。问题5已经为大家总结好了。 + +#### 5、讲讲 Go 的 chan 底层数据结构和主要使用场景 + +答:channel 的数据结构包含 qccount 当前队列中剩余元素个数,dataqsiz 环形队列长度,即可以存放的元素个数,buf 环形队列指针,elemsize 每个元素的大小,closed 标识关闭状态,elemtype 元素类型,sendx 队列下表,指示元素写入时存放到队列中的位置,recv 队列下表,指示元素从队列的该位置读出。recvq 等待读消息的 goroutine 队列,sendq 等待写消息的 goroutine 队列,lock 互斥锁,chan 不允许并发读写。 + +无缓冲和有缓冲区别:管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据。管道有缓冲区但缓冲区没有数据,从管道读取数据也会阻塞,直到协程写入数据,如果管道满了,写数据也会阻塞,直到协程从缓冲区读取数据。 + +channel 的一些特点 1)、读写值 nil 管道会永久阻塞 2)、关闭的管道读数据仍然可以读数据 3)、往关闭的管道写数据会 panic 4)、关闭为 nil 的管道 panic 5)、关闭已经关闭的管道 panic + +向 channel 写数据的流程:如果等待接收队列 recvq 不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从 recvq 取出 G,并把数据写入,最后把该 G 唤醒,结束发送过程;如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;如果缓冲区中没有空余位置,将待发送数据写入 G,将当前 G 加入 sendq,进入睡眠,等待被读 goroutine 唤醒; + +向 channel 读数据的流程:如果等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G,把 G 中数据读出,最后把 G 唤醒,结束读取过程;如果等待发送队列 sendq 不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程;如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;将当前 goroutine 加入 recvq,进入睡眠,等待被写 goroutine 唤醒; + +使用场景:消息传递、消息过滤,信号广播,事件订阅与广播,请求、响应转发,任务分发,结果汇总,并发控制,限流,同步与异步 + +# 五、GMP相关 + +#### 1、什么是 GMP?(必问) + +答:G 代表着 goroutine,P 代表着上下文处理器,M 代表 thread 线程,在 GPM 模型,有一个全局队列(Global Queue):存放等待运行的 G,还有一个 P 的本地队列:也是存放等待运行的 G,但数量有限,不超过 256 个。GPM 的调度流程从 go func()开始创建一个 goroutine,新建的 goroutine 优先保存在 P 的本地队列中,如果 P 的本地队列已经满了,则会保存到全局队列中。M 会从 P 的队列中取一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会从其他的 MP 组合偷取一个可执行的 G 来执行,当 M 执行某一个 G 时候发生系统调用或者阻塞,M 阻塞,如果这个时候 G 在执行,runtime 会把这个线程 M 从 P 中摘除,然后创建一个新的操作系统线程来服务于这个 P,当 M 系统调用结束时,这个 G 会尝试获取一个空闲的 P 来执行,并放入到这个 P 的本地队列,如果这个线程 M 变成休眠状态,加入到空闲线程中,然后整个 G 就会被放入到全局队列中。 + +关于 G,P,M 的个数问题,G 的个数理论上是无限制的,但是受内存限制,P 的数量一般建议是逻辑 CPU 数量的 2 倍,M 的数据默认启动的时候是 10000,内核很难支持这么多线程数,所以整个限制客户忽略,M 一般不做设置,设置好 P,M 一般都是要大于 P。 + +#### 2、进程、线程、协程有什么区别?(必问) + +进程:是应用程序的启动实例,每个进程都有独立的内存空间,不同的进程通过进程间的通信方式来通信。 + +线程:从属于进程,每个进程至少包含一个线程,线程是 CPU 调度的基本单位,多个线程之间可以共享进程的资源并通过共享内存等线程间的通信方式来通信。 + +协程:为轻量级线程,与线程相比,协程不受操作系统的调度,协程的调度器由用户应用程序提供,协程调度器按照调度策略把协程调度到线程中运行 + +#### 3、抢占式调度是如何抢占的? + +基于协作式抢占 + +基于信号量抢占 + +就像操作系统要负责线程的调度一样,Go的runtime要负责goroutine的调度。现代操作系统调度线程都是抢占式的,我们不能依赖用户代码主动让出CPU,或者因为IO、锁等待而让出,这样会造成调度的不公平。基于经典的时间片算法,当线程的时间片用完之后,会被时钟中断给打断,调度器会将当前线程的执行上下文进行保存,然后恢复下一个线程的上下文,分配新的时间片令其开始执行。这种抢占对于线程本身是无感知的,系统底层支持,不需要开发人员特殊处理。 + +基于时间片的抢占式调度有个明显的优点,能够避免CPU资源持续被少数线程占用,从而使其他线程长时间处于饥饿状态。goroutine的调度器也用到了时间片算法,但是和操作系统的线程调度还是有些区别的,因为整个Go程序都是运行在用户态的,所以不能像操作系统那样利用时钟中断来打断运行中的goroutine。也得益于完全在用户态实现,goroutine的调度切换更加轻量。 + +上面这两段文字只是对调度的一个概括,具体的协作式调度、信号量调度大家还需要去详细了解,这偏底层了,大厂或者中高级开发会问。(字节就问了) + +#### 4、M 和 P 的数量问题? + +p默认cpu内核数 + +M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来 + +【Go语言调度模型G、M、P的数量多少合适?】 + +详细参考这篇文章Go语言调度模型G、M、P的数量多少合适? (https://zoyi14.smartapps.cn/pages/note/index?_swebFromHost=baiduboxapp&origin=share&slug=1a50330adf1b&_swebfr=1) + +GMP数量这一块,结论很好记,没用项目经验的话,问了项目中怎么用可能容易卡壳。 + +# 六、锁相关 + +#### 1、除了 mutex 以外还有哪些方式安全读写共享变量? + +* 将共享变量的读写放到一个 goroutine 中,其它 goroutine 通过 channel 进行读写操作。 + +* 可以用个数为 1 的信号量(semaphore)实现互斥 + +* 通过 Mutex 锁实现 + +#### 2、Go 如何实现原子操作? + +答:原子操作就是不可中断的操作,外界是看不到原子操作的中间状态,要么看到原子操作已经完成,要么看到原子操作已经结束。在某个值的原子操作执行的过程中,CPU 绝对不会再去执行其他针对该值的操作,那么其他操作也是原子操作。 + +Go 语言的标准库代码包 sync/atomic 提供了原子的读取(Load 为前缀的函数)或写入(Store 为前缀的函数)某个值(这里细节还要多去查查资料)。 + +原子操作与互斥锁的区别 + +* 互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。 +* 原子操作是针对某个值的单个互斥操作。 +* Mutex 是悲观锁还是乐观锁?悲观锁、乐观锁是什么? + +悲观锁 + +悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。 + +乐观锁 + +乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量 + +#### 4、Mutex 有几种模式? + +1. 正常模式 + +当前的mutex只有一个goruntine来获取,那么没有竞争,直接返回。新的goruntine进来,如果当前mutex已经被获取了,则该goruntine进入一个先入先出的waiter队列,在mutex被释放后,waiter按照先进先出的方式获取锁。该goruntine会处于自旋状态(不挂起,继续占有cpu)。新的goruntine进来,mutex处于空闲状态,将参与竞争。新来的 goroutine 有先天的优势,它们正在 CPU 中运行,可能它们的数量还不少,所以,在高并发情况下,被唤醒的 waiter 可能比较悲剧地获取不到锁,这时,它会被插入到队列的前面。如果 waiter 获取不到锁的时间超过阈值 1 毫秒,那么,这个 Mutex 就进入到了饥饿模式。 + +1. 饥饿模式 + +在饥饿模式下,Mutex 的拥有者将直接把锁交给队列最前面的 waiter。新来的 goroutine 不会尝试获取锁,即使看起来锁没有被持有,它也不会去抢,也不会 spin(自旋),它会乖乖地加入到等待队列的尾部。如果拥有 Mutex 的 waiter 发现下面两种情况的其中之一,它就会把这个 Mutex 转换成正常模式: + +此 waiter 已经是队列中的最后一个 waiter 了,没有其它的等待锁的 goroutine 了;此 waiter 的等待时间小于 1 毫秒。 + +#### 5、goroutine 的自旋占用资源如何解决 + +自旋锁是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断是否能够被成功获取,直到获取到锁才会退出循环。 + +自旋的条件如下: + +* 还没自旋超过 4 次, +* 多核处理器, +* GOMAXPROCS > 1, +* p 上本地 goroutine 队列为空。 + +mutex 会让当前的 goroutine 去空转 CPU,在空转完后再次调用 CAS 方法去尝试性的占有锁资源,直到不满足自旋条件,则最终会加入到等待队列里。 + +# 七、并发相关 + +#### 1、怎么控制并发数? + +1. 有缓冲通道 + +根据通道中没有数据时读取操作陷入阻塞和通道已满时继续写入操作陷入阻塞的特性,正好实现控制并发数量。 + +```go +func main() { + count := 10 // 最大支持并发 + sum := 100 // 任务总数 + wg := sync.WaitGroup{} //控制主协程等待所有子协程执行完之后再退出。 + + c := make(chan struct{}, count) // 控制任务并发的chan + defer close(c) + + for i:=0; i接口,自动垃圾回收和goroutine等让人**拍案叫绝**的设计。 + +有许多基于Go的优秀项目。Docker,Kubernetes,etcd,deis,flynn,lime,revel等等。Go无疑是**云时代的最好语言**! + +题外话到此为止,在面试中,我们需要深入了解Go**语言特性**,并适当辅以**源码阅读**(Go源码非常**人性化,注释非常详细,**基本上只要你学过Go就能看懂)来提升能力。常考的点包括:切片,通道,异常处理,Goroutine,GMP模型,字符串高效拼接,指针,反射,接口,sync,go test和相关工具链。 + +一切问题的最权威的回答一定来自**官方**,这里力荐golang官方FAQ,虽然是英文的,但是也希望你花3-4天看完。**从使用者的角度去提问题, 从设计者的角度回答问题**。 + +![](https://pic3.zhimg.com/80/v2-1b6d660f2d2fc3599a2d82ef1245e3ea_1440w.jpg)官方FAQ问题 + [https://golang.org/doc/faq​golang.org/doc/faq](https://link.zhihu.com/?target=https%3A//golang.org/doc/faq) + +**面试题都是来源于网上和自己平时遇到的,但是很少有解答的版本,所以我专门回答了一下,放在专栏。** + +【所有试题已注明来源,侵删】 + +* * * + +## **面试题1** + +来源:[geektutu](https://link.zhihu.com/?target=https%3A//geektutu.com/post/qa-golang.html) + +### 基础语法 + +### 01 `=` 和 `:=` 的区别? + +=是赋值变量,:=是定义变量。 + +### 02 指针的作用 + +一个指针可以指向任意变量的地址,它所指向的地址在32位或64位机器上分别**固定**占4或8个字节。指针的作用有: + +* 获取变量的值 + + ```go +import fmt + ​ + func main(){ + a := 1 + p := &a//取址& + fmt.Printf("%d\n", *p);//取值* + } +``` + +* 改变变量的值 + + ```go +// 交换函数 + func swap(a, b *int) { + *a, *b = *b, *a + } +``` + +* 用指针替代值传入函数,比如类的接收器就是这样的。 + + ```go +type A struct{} + ​ + func (a *A) fun(){} +``` + +### 03 Go 允许多个返回值吗? + +可以。通常函数除了一般返回值还会返回一个error。 + +### 04 Go 有异常类型吗? + +有。Go用error类型代替try...catch语句,这样可以节省资源。同时增加代码可读性: + + ```go +_,err := errorDemo() + if err!=nil{ + fmt.Println(err) + return + } +``` + +也可以用errors.New()来定义自己的异常。errors.Error()会返回异常的字符串表示。只要实现error接口就可以定义自己的异常, + + ```go +type errorString struct { + s string + } + ​ + func (e *errorString) Error() string { + return e.s + } + ​ + // 多一个函数当作构造函数 + func New(text string) error { + return &errorString{text} + } +``` + +### 05 什么是协程(Goroutine) + +协程是**用户态轻量级线程**,它是**线程调度的基本单位**。通常在函数前加上go关键字就能实现并发。一个Goroutine会以一个很小的栈启动2KB或4KB,当遇到栈空间不足时,栈会**自动伸缩**, 因此可以轻易实现成千上万个goroutine同时启动。 + +### 06 ❤ 如何高效地拼接字符串 + +拼接字符串的方式有:"+", fmt.Sprintf, strings.Builder, bytes.Buffer, strings.Join + +1 "+" + +使用`+`操作符进行拼接时,会对字符串进行遍历,计算并开辟一个新的空间来存储原来的两个字符串。 + +2 fmt.Sprintf + +由于采用了接口参数,必须要用反射获取值,因此有性能损耗。 + +3 strings.Builder: + +用WriteString()进行拼接,内部实现是指针+切片,同时String()返回拼接后的字符串,它是直接把[]byte转换为string,从而避免变量拷贝。 + +`strings.builder`的实现原理很简单,结构如下: + +```go +type Builder struct { + addr *Builder // of receiver, to detect copies by value + buf []byte // 1 + } +``` + +`addr`字段主要是做`copycheck`,`buf`字段是一个`byte`类型的切片,这个就是用来存放字符串内容的,提供的`writeString()`方法就是像切片`buf`中追加数据: + +```go +func (b *Builder) WriteString(s string) (int, error) { + b.copyCheck() + b.buf = append(b.buf, s...) + return len(s), nil + } +``` + +提供的`String`方法就是将`[]byte`转换为`string`类型,这里为了避免内存拷贝的问题,使用了强制转换来避免内存拷贝: + +```go +func (b *Builder) String() string { + return *(*string)(unsafe.Pointer(&b.buf)) + } +``` + +4 bytes.Buffer + +`bytes.Buffer`是一个一个缓冲`byte`类型的缓冲器,这个缓冲器里存放着都是`byte`。使用方式如下: + +`bytes.buffer`底层也是一个`[]byte`切片,结构体如下: + +```go +type Buffer struct { + buf []byte // contents are the bytes buf[off : len(buf)] + off int // read at &buf[off], write at &buf[len(buf)] + lastRead readOp // last read operation, so that Unread* can work correctly. +} +``` + +因为`bytes.Buffer`可以持续向`Buffer`尾部写入数据,从`Buffer`头部读取数据,所以`off`字段用来记录读取位置,再利用切片的`cap`特性来知道写入位置,这个不是本次的重点,重点看一下`WriteString`方法是如何拼接字符串的: + +```go +func (b *Buffer) WriteString(s string) (n int, err error) { + b.lastRead = opInvalid + m, ok := b.tryGrowByReslice(len(s)) + if !ok { + m = b.grow(len(s)) + } + return copy(b.buf[m:], s), nil +} +``` + +切片在创建时并不会申请内存块,只有在往里写数据时才会申请,首次申请的大小即为写入数据的大小。如果写入的数据小于64字节,则按64字节申请。采用动态扩展`slice`的机制,字符串追加采用`copy`的方式将追加的部分拷贝到尾部,`copy`是内置的拷贝函数,可以减少内存分配。 + +但是在将`[]byte`转换为`string`类型依旧使用了标准类型,所以会发生内存分配: + +```go +func (b *Buffer) String() string { + if b == nil { + // Special case, useful in debugging. + return "" + } + return string(b.buf[b.off:]) +} +``` + +5 strings.join + +`strings.join`也是基于`strings.builder`来实现的,并且可以自定义分隔符,代码如下: + +```go +func Join(elems []string, sep string) string { + switch len(elems) { + case 0: + return "" + case 1: + return elems[0] + } + n := len(sep) * (len(elems) - 1) + for i := 0; i < len(elems); i++ { + n += len(elems[i]) + } + + var b Builder + b.Grow(n) + b.WriteString(elems[0]) + for _, s := range elems[1:] { + b.WriteString(sep) + b.WriteString(s) + } + return b.String() +} +``` + +唯一不同在于在`join`方法内调用了`b.Grow(n)`方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。 + +```go +func main(){ + a := []string{"a", "b", "c"} + //方式1: ret := a[0] + a[1] + a[2] + //方式2: ret := fmt.Sprintf("%s%s%s", a[0],a[1],a[2]) + //方式3: var sb strings.Builder + sb.WriteString(a[0]) + sb.WriteString(a[1]) + sb.WriteString(a[2]) + ret := sb.String() + //方式4: buf := new(bytes.Buffer) + buf.Write(a[0]) + buf.Write(a[1]) + buf.Write(a[2]) + ret := buf.String() + //方式5: ret := strings.Join(a,"") +} +``` + +总结: + +strings.Join ≈ strings.Builder > bytes.Buffer > "+" > fmt.Sprintf + +> 参考资料:[字符串拼接性能及原理 | Go 语言高性能编程 | 极客兔兔](https://link.zhihu.com/?target=https%3A//geektutu.com/post/hpg-string-concat.html) + +### 07 什么是 rune 类型 + +ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的128个字符,为了表示世界上大部分的文字系统,发明了 Unicode, 它是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。 + +Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。 + +```go +sample := "我爱GO" +runeSamp := []rune(sample) +runeSamp[0] = '你' +fmt.Println(string(runeSamp)) // "你爱GO" fmt.Println(len(runeSamp)) // 4 +``` + +### 08 如何判断 map 中是否包含某个 key ? + +```go +var sample map[int]int +if _, ok := sample[10];ok{ + +}else{ + +} +``` + +### 09 Go 支持默认参数或可选参数吗? + +不支持。但是可以利用结构体参数,或者...传入参数切片。 + +### 10 defer 的执行顺序 + +defer执行顺序和调用顺序相反,类似于栈后进先出(LIFO)。 + +defer在return之后执行,但在函数退出之前,defer可以修改返回值。下面是一个例子: + +```go +func test() int { + i := 0 + defer func() { + fmt.Println("defer1") + }() + defer func() { + i += 1 + fmt.Println("defer2") + }() + return i +} + +func main() { + fmt.Println("return", test()) +} +// defer2 // defer1 // return 0 +``` + +上面这个例子中,test返回值并没有修改,这是由于Go的返回机制决定的,执行Return语句后,Go会创建一个临时变量保存返回值。如果是有名返回(也就是指明返回值functest()(i int)) + +```go +func test() (i int) { + i = 0 + defer func() { + i += 1 + fmt.Println("defer2") + }() + return i +} + +func main() { + fmt.Println("return", test()) +} +// defer2 // return 1 +``` + +这个例子中,返回值被修改了。对于有名返回值的函数,执行 return 语句时,并不会再创建临时变量保存,因此,defer 语句修改了 i,即对返回值产生了影响。 + +### 11 如何交换 2 个变量的值? + +对于变量而言`a,b = b,a`; 对于指针而言`*a,*b = *b, *a` + +### 12 Go 语言 tag 的用处? + +tag可以为结构体成员提供属性。常见的: + +1. json序列化或反序列化时字段的名称 +2. db: sqlx模块中对应的数据库字段名 +3. form: gin框架中对应的前端的数据字段名 +4. binding: 搭配 form 使用, 默认如果没查找到结构体中的某个字段则不报错值为空, binding为 required 代表没找到返回错误给前端 + +### 13 如何获取一个结构体的所有tag? + +利用反射: + +```go +import reflect +type Author struct { + Name int `json:Name` + Publications []string `json:Publication,omitempty` +} + +func main() { + t := reflect.TypeOf(Author{}) + for i := 0; i < t.NumField(); i++ { + name := t.Field(i).Name + s, _ := t.FieldByName(name) + fmt.Println(name, s.Tag) + } +} +``` + +上述例子中,`reflect.TypeOf`方法获取对象的类型,之后`NumField()`获取结构体成员的数量。 通过`Field(i)`获取第i个成员的名字。 再通过其`Tag` 方法获得标签。 + +### 14 如何判断 2 个字符串切片(slice) 是相等的? + +reflect.DeepEqual(), 但反射非常影响性能。 + +### 15 结构体打印时,`%v` 和 `%+v` 的区别 + +`%v`输出结构体各成员的值; + +`%+v`输出结构体各成员的**名称**和**值**; + +`%#v`输出结构体名称和结构体各成员的名称和值 + +### 16 Go 语言中如何表示枚举值(enums)? + +在常量中用iota可以表示枚举。iota从0开始。 + +```go +const ( + B = 1 << (10 * iota) + KiB + MiB + GiB + TiB + PiB + EiB +) +``` + +### 17 空 struct{} 的用途 + +* 用map模拟一个set,那么就要把值置为struct{},struct{}本身不占任何空间,可以避免任何多余的内存分配。 + +```go +type Set map[string]struct{} + +func main() { + set := make(Set) + + for _, item := range []string{"A", "A", "B", "C"} { + set[item] = struct{}{} + } + fmt.Println(len(set)) // 3 + if _, ok := set["A"]; ok { + fmt.Println("A exists") // A exists + } +} +``` + +* 有时候给通道发送一个空结构体,channel<-struct{}{},也是节省了空间。 + +```go +func main() { + ch := make(chan struct{}, 1) + go func() { + <-ch + // do something + }() + ch <- struct{}{} + // ... +} +``` + +* 仅有方法的结构体 + +```go +type Lamp struct{} +``` + +### **18 go里面的int和int32是同一个概念吗?** + +不是一个概念!千万不能混淆。go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统,int类型的大小就是4字节。如果是64位操作系统,int类型的大小就是8个字节。除此之外uint也与操作系统有关。 + +int8占1个字节,int16占2个字节,int32占4个字节,int64占8个字节。 + +### 实现原理 + +### 01 init() 函数是什么时候执行的? + +**简答**: 在main函数之前执行。 + +**详细**:init()函数是go初始化的一部分,由runtime初始化每个导入的包,初始化不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。 + +每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的`init()`函数。同一个包,甚至是同一个源文件可以有多个`init()`函数。`init()`函数没有入参和返回值,不能被其他函数调用,同一个包内多个`init()`函数的执行顺序不作保证。 + +执行顺序:import –> const –> var –>`init()`–>`main()` + +一个文件可以有多个`init()`函数! + +### 02 ❤如何知道一个对象是分配在栈上还是堆上? + +Go和C++不同,Go局部变量会进行**逃逸分析**。如果**变量离开作用域后没有被引用**,则**优先**分配到栈上,否则分配到堆上。那么如何判断是否发生了逃逸呢? + +`go build -gcflags '-m -m -l' xxx.go`. + +关于逃逸的可能情况:变量大小不确定,变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针。 + +### 03 2 个 interface 可以比较吗 ? + +Go 语言中,interface 的内部实现包含了 2 个字段,类型 `T` 和 值 `V`,interface 可以使用 `==` 或 `!=` 比较。2 个 interface 相等有以下 2 种情况 + +1. 两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态) +2. 类型 T 相同,且对应的值 V 相等。 + +看下面的例子: + +```go +type Stu struct { + Name string +} + +type StuInt interface{} + +func main() { + var stu1, stu2 StuInt = &Stu{"Tom"}, &Stu{"Tom"} + var stu3, stu4 StuInt = Stu{"Tom"}, Stu{"Tom"} + fmt.Println(stu1 == stu2) // false + fmt.Println(stu3 == stu4) // true } +``` + +`stu1` 和 `stu2` 对应的类型是 `*Stu`,值是 Stu 结构体的地址,两个地址不同,因此结果为 false。 +`stu3` 和 `stu4` 对应的类型是 `Stu`,值是 Stu 结构体,且各字段相等,因此结果为 true。 + +### 04 2 个 nil 可能不相等吗? + +可能不等。interface在运行时绑定值,只有值为nil接口值才为nil,但是与指针的nil不相等。举个例子: + +```go +var p *int = nil +var i interface{} = nil +if(p == i){ + fmt.Println("Equal") +} +``` + +两者并不相同。总结:**两个nil只有在类型相同时才相等**。 + +### 05 ❤简述 Go 语言GC(垃圾回收)的工作原理 + +垃圾回收机制是Go一大特(nan)色(dian)。Go1.3采用**标记清除法**, Go1.5采用**三色标记法**,Go1.8采用**三色标记法+混合写屏障**。 + +**_标记清除法_** + +分为两个阶段:标记和清除 + +标记阶段:从根对象出发寻找并标记所有存活的对象。 + +清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表。 + +缺点是需要暂停程序STW。 + +**_三色标记法_**: + +将对象标记为白色,灰色或黑色。 + +白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。 + +标记开始时,先将所有对象加入白色集合(需要STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。 + +这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此Go采用了**写屏障技术**,当对象新增或者更新会将其着色为灰色。 + +一次完整的GC分为四个阶段: + +1. 准备标记(需要STW),开启写屏障。 +2. 开始标记 +3. 标记结束(STW),关闭写屏障 +4. 清理(并发) + +基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,带来性能瓶颈。**混合写屏障**分为以下四步: + +1. GC开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需STW); +2. GC期间,任何栈上创建的新对象均为黑色 +3. 被删除引用的对象标记为灰色 +4. 被添加引用的对象标记为灰色 + +总而言之就是确保黑色对象不能引用白色对象,这个改进直接使得GC时间从 2s降低到2us。 + +### 06 函数返回局部变量的指针是否安全? + +这一点和C++不同,在Go里面返回局部变量的指针是安全的。因为Go会进行**逃逸分析**,如果发现局部变量的作用域超过该函数则会**把指针分配到堆区**,避免内存泄漏。 + +### 07 非接口的任意类型 T() 都能够调用 `*T` 的方法吗?反过来呢? + +一个T类型的值可以调用*T类型声明的方法,当且仅当T是**可寻址的**。 + +反之:*T 可以调用T()的方法,因为指针可以解引用。 + +### 08 go slice是怎么扩容的? + +如果当前容量小于1024,则判断所需容量是否大于原来容量2倍,如果大于,当前容量加上所需容量;否则当前容量乘2。 + +如果当前容量大于1024,则每次按照1.25倍速度递增容量,也就是每次加上cap/4。 + +### [并发编程](https://link.zhihu.com/?target=https%3A//geektutu.com/post/qa-golang-3.html) + +### 01 ❤无缓冲的 channel 和有缓冲的 channel 的区别? + +(这个问题笔者也纠结了很久,直到看到一篇文章,阻塞与否是分别针对发送接收方而言的,才茅塞顿开) + +对于无缓冲区channel: + +发送的数据如果没有被接收方接收,那么**发送方阻塞;**如果一直接收不到发送方的数据,**接收方阻塞**; + +有缓冲的channel: + +发送方在缓冲区满的时候阻塞,接收方不阻塞;接收方在缓冲区为空的时候阻塞,发送方不阻塞。 + +可以类比生产者与消费者问题。 + +![](https://pic3.zhimg.com/80/v2-b770e5632874d40780ecfe79701324f2_1440w.jpg) +### 02 为什么有协程泄露(Goroutine Leak)? + +协程泄漏是指协程创建之后没有得到释放。主要原因有: + +1. 缺少接收器,导致发送阻塞 +2. 缺少发送器,导致接收阻塞 +3. 死锁。多个协程由于竞争资源导致死锁。 +4. WaitGroup Add()和Done()不相等,前者更大。 + +### 03 Go 可以限制运行时操作系统线程的数量吗? 常见的goroutine操作函数有哪些? + +可以,使用runtime.GOMAXPROCS(num int)可以设置线程数目。该值默认为CPU逻辑核数,如果设的太大,会引起频繁的线程切换,降低性能。 + +runtime.Gosched(),用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。 +runtime.Goexit(),调用此函数会立即使当前的goroutine的运行终止(终止协程),而其它的goroutine并不会受此影响。runtime.Goexit在终止当前goroutine前会先执行此goroutine的还未执行的defer语句。请注意千万别在主函数调用runtime.Goexit,因为会引发panic。 + +### 04 如何控制协程数目。 + +> The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit. + +可以使用环境变量 `GOMAXPROCS` 或 `runtime.GOMAXPROCS(num int)` 设置,例如: + +```go +runtime.GOMAXPROCS(1) // 限制同时执行Go代码的操作系统线程数为 1 +``` + +从官方文档的解释可以看到,`GOMAXPROCS` 限制的是同时执行用户态 Go 代码的操作系统线程的数量,但是对于被系统调用阻塞的线程数量是没有限制的。`GOMAXPROCS` 的默认值等于 CPU 的逻辑核数,同一时间,一个核只能绑定一个线程,然后运行被调度的协程。因此对于 CPU 密集型的任务,若该值过大,例如设置为 CPU 逻辑核数的 2 倍,会增加线程切换的开销,降低性能。对于 I/O 密集型应用,适当地调大该值,可以提高 I/O 吞吐率。 + +另外对于协程,可以用带缓冲区的channel来控制,下面的例子是协程数为1024的例子 + +```go +var wg sync.WaitGroup +ch := make(chan struct{}, 1024) +for i:=0; i<20000; i++{ + wg.Add(1) + ch<-struct{}{} + go func(){ + defer wg.Done() + <-ch + } +} +wg.Wait() +``` + +此外还可以用**协程池**:其原理无外乎是将上述代码中通道和协程函数解耦,并封装成单独的结构体。常见第三方协程池库,比如[tunny](https://link.zhihu.com/?target=http%3A//github.com/Jeffail/tunny)等。 + +面试题评价:★★★☆☆。偏容易和基础。❤表示需要重点关注。 + +* * * + +## **面试题2** + +来源:Durant Thorvalds + +### ❤new和make的区别? + +* new只用于分配内存,返回一个指向地址的**指针**。它为每个新类型分配一片内存,初始化为0且返回类型*T的内存地址,它相当于&T{} +* make只可用于**slice,map,channel**的初始化,返回的是**引用**。 + +```go +a := new(int) +*a = 46 +fmt.Println(*a) +``` + +### 请你讲一下Go面向对象是如何实现的? + +Go实现面向对象的两个关键是struct和interface。 + +封装:对于同一个包,对象对包内的文件可见;对不同的包,需要将对象以大写开头才是可见的。 + +继承:继承是编译时特征,在struct内加入所需要继承的类即可: + +```go +type A struct{} +type B struct{ +A +} +``` + +多态:多态是运行时特征,Go多态通过interface来实现。类型和接口是松耦合的,某个类型的实例可以赋给它所实现的任意接口类型的变量。 + +Go支持多重继承,就是在类型中嵌入所有必要的父类型。 + +### 二维切片如何初始化 + +一种方式是对每一个维度都初始化。 + +另一种方式是用一个单独的一维切片初始化。 + +```go +// Allocate the top-level slice. +picture := make([][]uint8, YSize) // One row per unit of y. +// Loop over the rows, allocating the slice for each row. +for i := range picture { + picture[i] = make([]uint8, XSize) +} +// Allocate the top-level slice, the same as before. +picture := make([][]uint8, YSize) // One row per unit of y. +// Allocate one large slice to hold all the pixels. +pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8. +// Loop over the rows, slicingog each row from the front of the remaining pixels slice. +for i := range picture { + picture[i], pixels = pixels[:XSize], pixels[XSize:] +``` + +### uint型变量值分别为 1,2,它们相减的结果是多少? + + ```go +var a uint = 1 + var b uint = 2 + fmt.Println(a - b) +``` + +答案,结果会溢出,如果是32位系统,结果是2^32-1,如果是64位系统,结果2^64-1. + +### 讲一下go有没有函数在main之前执行?怎么用? + +go的init函数在main函数之前执行,它有如下特点: + +* 初始化不能采用初始化表达式初始化的变量; +* 程序运行前执行注册 +* 实现sync.Once功能 +* 不能被其它函数调用 +* init函数没有入口参数和返回值: + +```go +func init(){ + register... +} +``` + +* 每个包可以有多个init函数,**每个源文件也可以有多个init函数**。 +* 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。 +* 不同包的init函数按照包导入的依赖关系决定执行顺序。 + +### 下面这句代码是什么作用,为什么要定义一个空值? + +```go +var _ Codec = (*GobCodec)(nil) +type GobCodec struct{ + conn io.ReadWriteCloser + buf *bufio.Writer + dec *gob.Decoder + enc *gob.Encoder +} + +type Codec interface { + io.Closer + ReadHeader(*Header) error + ReadBody(interface{}) error + Write(*Header, interface{}) error +} +``` + +答:将nil转换为*GobCodec类型,然后再转换为Codec接口,如果转换失败,说明*GobCodec没有实现Codec接口的所有方法。 + +### ❤golang的内存管理的原理清楚吗?简述go内存管理机制。 + +golang内存管理基本是参考tcmalloc来进行的。go内存管理本质上是一个内存池,只不过内部做了很多优化:自动伸缩内存池大小,合理的切割内存块。 + +> 一些基本概念: +> 页Page:一块8K大小的内存空间。Go向操作系统申请和释放内存都是以页为单位的。 +> span : 内存块,一个或多个连续的 page 组成一个 span 。如果把 page 比喻成工人, span 可看成是小队,工人被分成若干个队伍,不同的队伍干不同的活。 +> sizeclass : 空间规格,每个 span 都带有一个 sizeclass ,标记着该 span 中的 page 应该如何使用。使用上面的比喻,就是 sizeclass 标志着 span 是一个什么样的队伍。 +> object : 对象,用来存储一个变量数据内存空间,一个 span 在初始化时,会被切割成一堆等大的 object 。假设 object 的大小是 16B , span 大小是 8K ,那么就会把 span 中的 page 就会被初始化 8K / 16B = 512 个 object 。所谓内存分配,就是分配一个 object 出去。 + +1. **mheap** + +一开始go从操作系统索取一大块内存作为内存池,并放在一个叫mheap的内存池进行管理,mheap将一整块内存切割为不同的区域,并将一部分内存切割为合适的大小。 + +![](https://pic3.zhimg.com/80/v2-05f622a5c88a9a9456d43ee301622582_1440w.jpg) + +mheap.spans :用来存储 page 和 span 信息,比如一个 span 的起始地址是多少,有几个 page,已使用了多大等等。 + +mheap.bitmap 存储着各个 span 中对象的标记信息,比如对象是否可回收等等。 + +mheap.arena_start : 将要分配给应用程序使用的空间。 + +1. **mcentral** + +用途相同的span会以链表的形式组织在一起存放在mcentral中。这里用途用**sizeclass**来表示,就是该span存储哪种大小的对象。 + +找到合适的 span 后,会从中取一个 object 返回给上层使用。 + +1. **mcache** + +为了提高内存并发申请效率,加入缓存层mcache。每一个mcache和处理器P对应。Go申请内存首先从P的mcache中分配,如果没有可用的span再从mcentral中获取。 + +> 参考资料:[Go 语言内存管理(二):Go 内存管理](https://link.zhihu.com/?target=https%3A//cloud.tencent.com/developer/article/1422392) + +### ❤mutex有几种模式? + +mutex有两种模式:**normal** 和 **starvation** + +正常模式 + +所有goroutine按照FIFO的顺序进行锁获取,被唤醒的goroutine和新请求锁的goroutine同时进行锁获取,通常**新请求锁的goroutine更容易获取锁**(持续占有cpu),被唤醒的goroutine则不容易获取到锁。公平性:否。 + +饥饿模式 + +所有尝试获取锁的goroutine进行等待排队,**新请求锁的goroutine不会进行锁获取**(禁用自旋),而是加入队列尾部等待获取锁。公平性:是。 + +> 参考链接:[Go Mutex 饥饿模式](https://link.zhihu.com/?target=https%3A//blog.csdn.net/qq_37102984/article/details/115322706),[GO 互斥锁(Mutex)原理](https://link.zhihu.com/?target=https%3A//blog.csdn.net/baolingye/article/details/111357407%23%3A~%3Atext%3D%25E6%25AF%258F%25E4%25B8%25AAMutex%25E9%2583%25BD%2Ctarving%25E3%2580%2582) + +* * * + +## **面试题3** + +来源**:**[如果你是一个Golang面试官,你会问哪些问题?](https://www.zhihu.com/question/67846139/answer/1983588716) + +### ❤go如何进行调度的。GMP中状态流转。 + +Go里面GMP分别代表:G:goroutine,M:线程(真正在CPU上跑的),P:调度器。 + +![](https://pic3.zhimg.com/80/v2-63a317972091b6d43863c5144a6badce_1440w.jpg)GMP模型 + +调度器是M和G之间桥梁。 + +go进行调度过程: + +* 某个线程尝试创建一个新的G,那么这个G就会被安排到这个线程的G本地队列LRQ中,如果LRQ满了,就会分配到全局队列GRQ中; +* 尝试获取当前线程的M,如果无法获取,就会从空闲的M列表中找一个,如果空闲列表也没有,那么就创建一个M,然后绑定G与P运行。 +* 进入调度循环: + +* 找到一个合适的G +* 执行G,完成以后退出 + +### Go什么时候发生阻塞?阻塞时,调度器会怎么做。 + +* 用于**原子、互斥量或通道**操作导致goroutine阻塞,调度器将把当前阻塞的goroutine从本地运行队列**LRQ换出**,并重新调度其它goroutine; +* 由于**网络请求**和**IO**导致的阻塞,Go提供了网络轮询器(Netpoller)来处理,后台用epoll等技术实现IO多路复用。 + +其它回答: + +* **channel阻塞**:当goroutine读写channel发生阻塞时,会调用gopark函数,该G脱离当前的M和P,调度器将新的G放入当前M。 +* **系统调用**:当某个G由于系统调用陷入内核态,该P就会脱离当前M,此时P会更新自己的状态为Psyscall,M与G相互绑定,进行系统调用。结束以后,若该P状态还是Psyscall,则直接关联该M和G,否则使用闲置的处理器处理该G。 +* **系统监控**:当某个G在P上运行的时间超过10ms时候,或者P处于Psyscall状态过长等情况就会调用retake函数,触发新的调度。 +* **主动让出**:由于是协作式调度,该G会主动让出当前的P(通过GoSched),更新状态为Grunnable,该P会调度队列中的G运行。 + +> 更多关于netpoller的内容可以参看:[https://strikefreedom.top/go-netpoll-io-multiplexing-reactor](https://link.zhihu.com/?target=https%3A//strikefreedom.top/go-netpoll-io-multiplexing-reactor) + +### ❤Go中GMP有哪些状态? + +G的状态: + +**_Gidle**:刚刚被分配并且还没有被初始化,值为0,为创建goroutine后的默认值 + +**_Grunnable**: 没有执行代码,没有栈的所有权,存储在运行队列中,可能在某个P的本地队列或全局队列中(如上图)。 + +**_Grunning**: 正在执行代码的goroutine,拥有栈的所有权(如上图)。 + +**_Gsyscall**:正在执行系统调用,拥有栈的所有权,与P脱离,但是与某个M绑定,会在调用结束后被分配到运行队列(如上图)。 + +**_Gwaiting**:被阻塞的goroutine,阻塞在某个channel的发送或者接收队列(如上图)。 + +**_Gdead**: 当前goroutine未被使用,没有执行代码,可能有分配的栈,分布在空闲列表gFree,可能是一个刚刚初始化的goroutine,也可能是执行了goexit退出的goroutine(如上图)。 + +**_Gcopystac**:栈正在被拷贝,没有执行代码,不在运行队列上,执行权在 + +**_Gscan** : GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在。 + +P的状态: + +**_Pidle** :处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空 + +**_Prunning** :被线程 M 持有,并且正在执行用户代码或者调度器(如上图) + +**_Psyscall**:没有执行用户代码,当前线程陷入系统调用(如上图) + +**_Pgcstop** :被线程 M 持有,当前处理器由于垃圾回收被停止 + +**_Pdead** :当前处理器已经不被使用 + +M的状态: + +**自旋线程**:处于运行状态但是没有可执行goroutine的线程,数量最多为GOMAXPROC,若是数量大于GOMAXPROC就会进入休眠。 + +**非自旋线程**:处于运行状态有可执行goroutine的线程。 + +下面一张图很好的展示了Goroutine状态流转: + +![](https://pic4.zhimg.com/80/v2-3312a9b7852f67257a1266bd56e2aa1f_1440w.jpg)Goroutine状态流转 +### GMP能不能去掉P层?会怎么样? + +去掉p会导致,当G进行**系统调用时候,会一直阻塞**,其它G无法获得M。 + +### 如果有一个G一直占用资源怎么办。 + +如果有个goroutine一直占用资源,那么GMP模型会**从正常模式转变为饥饿模式**(类似于mutex),允许其它goroutine抢占(禁用自旋锁)。 + +### 若干线程一个线程发生OOM(Out of memory)会怎么办。 + +对于线程而言:发生内存溢出的线程会被kill,其它线程不受影响。 + +### goroutine什么情况会发生内存泄漏?如何避免。 + +在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。 + +**暂时性内存泄露** + +* 获取长字符串中的一段导致长字符串未释放 +* 获取长slice中的一段导致长slice未释放 +* 在长slice新建slice导致泄漏 + +string相比切片少了一个容量的cap字段,可以把string当成一个只读的切片类型。获取长string或者切片中的一段内容,由于新生成的对象和老的string或者切片共用一个内存空间,会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄漏 + +**永久性内存泄露** + +* goroutine永久阻塞而导致泄漏 +* time.Ticker未关闭导致泄漏 +* 不正确使用Finalizer导致泄漏 + +### 怎么调试go? + +在vscode设置mode为debug。需要go-dlv插件。 + +### Go GC有几个阶段 + +目前的go GC采用**三色标记法**和**混合写屏障**技术。 + +Go GC有**四**个阶段: + +* STW,开启混合写屏障,扫描栈对象; +* 将所有对象加入白色集合,从根对象开始,将其放入灰色集合。每次从灰色集合取出一个对象标记为黑色,然后遍历其子对象,标记为灰色,放入灰色集合; +* 如此循环直到灰色集合为空。剩余的白色对象就是需要清理的对象。 +* STW,关闭混合写屏障; +* 在后台进行GC(并发)。 + +### go竞态条件了解吗? + +所谓竞态竞争,就是当**两个或以上的goroutine访问相同资源时候,对资源进行读/写。** + +比如`var a int = 0`,有两个协程分别对a+=1,我们发现最后a不一定为2.这就是竞态竞争。 + +通常我们可以用`go run -race xx.go`来进行检测。 + +解决方法是,对临界区资源上锁,或者使用原子操作(atomics),原子操作的开销小于上锁。 + +### 如果若干个goroutine,有一个panic会怎么做? + +有一个panic,那么剩余goroutine也会退出。 + +> 参考理解:[goroutine配上panic会怎样](https://link.zhihu.com/?target=https%3A//blog.csdn.net/huorongbj/article/details/123013273)。 + +### defer可以捕获goroutine的子goroutine吗? + +不可以。它们处于不同的调度器P中。对于子goroutine,正确的做法是: + +1. 必须通过 defer 关键字来调用 recover()。 +2. 当通过 goroutine 调用某个方法,一定要确保内部有 recover() 机制。 + +### ❤gRPC是什么? + +基于go的**远程过程调用**。RPC 框架的目标就是让远程服务调用更加简单、透明,RPC 框架负责屏蔽底层的传输方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二进制)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。 + +![](https://pic3.zhimg.com/80/v2-53fcdf2682c027f2d16292c4b4ba20d6_1440w.jpg)gRPC框架图 +## 面试题4 + +需要面试者有一定的大型项目经验经验,了解使用**微服务,etcd,gin,gorm,gRPC**等典型框架等模型或框架。 + +### 微服务了解吗? + +微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成。微服务架构使应用程序更易于扩展和更快地开发,从而加速创新并缩短新功能的上市时间。 + +![](https://pic3.zhimg.com/80/v2-56601175dc48fbec496c79284488ecee_1440w.jpg)微服务示意图 + +微服务有着自主,专用,灵活性等优点。 + +> 参考资料:[什么是微服务?| AWS](https://link.zhihu.com/?target=https%3A//aws.amazon.com/cn/microservices/) + +### 服务发现是怎么做的? + +主要有两种服务发现机制:**客户端发现**和**服务端发现**。 + +**客户端发现模式**:当我们使用客户端发现的时候,客户端负责决定可用服务实例的网络地址并且在集群中对请求负载均衡, 客户端访问**服务登记表**,也就是一个可用服务的数据库,然后客户端使用一种**负载均衡算法**选择一个可用的服务实例然后发起请求。该模式如下图所示: + +![](https://pic2.zhimg.com/80/v2-915e057bb7b6783393cdf1bfd2d0d745_1440w.jpg)客户端发现模式 + +**服务端发现模式**:客户端通过**负载均衡器**向某个服务提出请求,负载均衡器查询服务注册表,并将请求转发到可用的服务实例。如同客户端发现,服务实例在服务注册表中注册或注销。 + +![](https://pic3.zhimg.com/80/v2-fe7926e3a7007f985a87e102743a842e_1440w.jpg)服务端发现模式 + +参考资料:[「Chris Richardson 微服务系列」服务发现的可行方案以及实践案例](https://link.zhihu.com/?target=http%3A//blog.daocloud.io/3289.html) + +### ETCD用过吗? + +**etcd**是一个**高度一致**的**分布式键值存储**,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。它可以优雅地处理网络分区期间的领导者**选举**,即使在领导者节点中也可以容忍机器故障。 + +etcd 是用**Go语言**编写的,它具有出色的跨平台支持,小的二进制文件和强大的社区。etcd机器之间的通信通过**Raft共识算法**处理。 + +关于文档可以参考:[v3.5 docs](https://link.zhihu.com/?target=https%3A//etcd.io/docs/v3.5/) + +### GIN怎么做参数校验? + +go采用validator作参数校验。 + +它具有以下独特功能: + +* 使用验证tag或自定义validator进行跨字段Field和跨结构体验证。 +* 允许切片、数组和哈希表,多维字段的任何或所有级别进行校验。 +* 能够对哈希表key和value进行验证 +* 通过在验证之前确定它的基础类型来处理类型接口。 +* 别名验证标签,允许将多个验证映射到单个标签,以便更轻松地定义结构体上的验证 +* gin web 框架的默认验证器; + +参考资料:[validator package - pkg.go.dev](https://link.zhihu.com/?target=https%3A//pkg.go.dev/github.com/go-playground/validator%23section-readme) + +### 中间件用过吗? + +Middleware是Web的重要组成部分,中间件(通常)是一小段代码,它们接受一个请求,对其进行处理,每个中间件只处理一件事情,完成后将其传递给另一个中间件或最终处理程序,这样就做到了程序的解耦。 + +### Go解析Tag是怎么实现的? + +Go解析tag采用的是**反射**。 + +具体来说使用reflect.ValueOf方法获取其反射值,然后获取其Type属性,之后再通过Field(i)获取第i+1个field,再.Tag获得Tag。 + +反射实现的原理在: `src/reflect/type.go`中 + +### 你项目有优雅的启停吗? + +所谓「优雅」启停就是在启动退出服务时要满足以下几个条件: + +* **不可以关闭现有连接**(进程) +* 新的进程启动并「**接管**」旧进程 +* 连接要**随时响应用户请求**,不可以出现拒绝请求的情况 +* 停止的时候,必须**处理完既有连接**,并且**停止接收新的连接**。 + +为此我们必须引用**信号**来完成这些目的: + +启动: + +* 监听SIGHUP(在用户终端连接(正常或非正常)结束时发出); +* 收到信号后将服务监听的文件描述符传递给新的子进程,此时新老进程同时接收请求; + +退出: + +* 监听SIGINT和SIGSTP和SIGQUIT等。 +* 父进程停止接收新请求,等待旧请求完成(或超时); +* 父进程退出。 + +实现:go1.8采用Http.Server内置的Shutdown方法支持优雅关机。 然后[fvbock/endless](https://link.zhihu.com/?target=http%3A//github.com/fvbock/endless)可以实现优雅重启。 + +> 参考资料:[gin框架实践连载八 | 如何优雅重启和停止 - 掘金](https://link.zhihu.com/?target=https%3A//juejin.cn/post/6867074626427502600%23heading-3),[优雅地关闭或重启 go web 项目](https://link.zhihu.com/?target=http%3A//www.phpxs.com/post/7186/) + +### 持久化怎么做的? + +所谓持久化就是将要保存的字符串写到硬盘等设备。 + +* 最简单的方式就是采用ioutil的WriteFile()方法将字符串写到磁盘上,这种方法面临**格式化**方面的问题。 +* 更好的做法是将数据按照**固定协议**进行组织再进行读写,比如JSON,XML,Gob,csv等。 +* 如果要考虑**高并发**和**高可用**,必须把数据放入到数据库中,比如MySQL,PostgreDB,MongoDB等。 + +参考链接:[Golang 持久化](https://link.zhihu.com/?target=https%3A//www.jianshu.com/p/015aca3e11ae) + +* * * + +## **面试题5** + +作者:Dylan2333 链接: + + [测开转Go开发-面经&总结_笔经面经_牛客网​www.nowcoder.com/discuss/826193?type=post&order=recall&pos=&page=1&ncTraceId=&channel=-1&source_id=search_post_nctrack&gio_id=9C5DC1FFB3FC3BE29281D7CCFC420365-1645173894793![](https://pic1.zhimg.com/v2-ed411b6288d53218e5e9dd056d1df020_ipico.jpg)](https://link.zhihu.com/?target=https%3A//www.nowcoder.com/discuss/826193%3Ftype%3Dpost%26order%3Drecall%26pos%3D%26page%3D1%26ncTraceId%3D%26channel%3D-1%26source_id%3Dsearch_post_nctrack%26gio_id%3D9C5DC1FFB3FC3BE29281D7CCFC420365-1645173894793) + +该试题需要面试者有非常丰富的项目阅历和底层原理经验,熟练使用**微服务,etcd,gin,gorm,gRPC**等典型框架等模型或框架。 + +### channel 死锁的场景 + +* 当一个`channel`中没有数据,而直接读取时,会发生死锁: + +```go +q := make(chan int,2) +<-q +``` + +解决方案是采用select语句,再default放默认处理方式: + +```go +q := make(chan int,2) +select{ + case val:=<-q: + default: + ... + +} +``` + +* 当channel数据满了,再尝试写数据会造成死锁: + +```go +q := make(chan int,2) +q<-1 +q<-2 +q<-3 +``` + +解决方法,采用select + +```go +func main() { + q := make(chan int, 2) + q <- 1 + q <- 2 + select { + case q <- 3: + fmt.Println("ok") + default: + fmt.Println("wrong") + } + +} +``` + +* 向一个关闭的channel写数据。 + +注意:一个已经关闭的channel,只能读数据,不能写数据。 + +参考资料:[Golang关于channel死锁情况的汇总以及解决方案](https://link.zhihu.com/?target=https%3A//blog.csdn.net/qq_35976351/article/details/81984117) + +### 读写channel应该先关哪个? + +应该写channel先关。因为对于已经关闭的channel只能读,不能写。 + +### 对已经关闭的chan进行读写会怎么样? + +* 读已经关闭的chan能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。 + +* 如果chan关闭前,buffer内有元素还未读,会正确读到chan内的值,且返回的第二个bool值(是否读成功)为true。 +* 如果chan关闭前,buffer内有元素已经被读完,chan内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个bool值一直为false。 + +写已经关闭的chan会panic。 + +### 说说 atomic底层怎么实现的. + +atomic源码位于`sync\atomic`。通过阅读源码可知,atomic采用**CAS**(CompareAndSwap)的方式实现的。所谓CAS就是使用了CPU中的原子性操作。在操作共享变量的时候,CAS不需要对其进行加锁,而是通过类似于乐观锁的方式进行检测,总是假设被操作的值未曾改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换。本质上是**不断占用CPU资源来避免加锁的开销**。 + +> 参考资料:[Go语言的原子操作atomic - 编程猎人](https://link.zhihu.com/?target=https%3A//www.programminghunter.com/article/37392193442/) + +### channel底层实现?是否线程安全。 + +channel底层实现在`src/runtime/chan.go`中 + +channel内部是一个循环链表。内部包含buf, sendx, recvx, lock ,recvq, sendq几个部分; + +buf是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表; + +* sendx和recvx用于记录buf这个循环链表中的发送或者接收的index; +* lock是个互斥锁; +* recvq和sendq分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表。 + +channel是**线程安全**的。 + +> 参考资料:[Kitou:Golang 深度剖析 -- channel的底层实现](https://zhuanlan.zhihu.com/p/264305133) + +### map的底层实现。 + +源码位于`src\runtime\map.go` 中。 + +go的map和C++map不一样,底层实现是哈希表,包括两个部分:**hmap**和**bucket**。 + +hmap结构体如图: + +```go +type hmap struct { + count int //map元素的个数,调用len()直接返回此值 + + // map标记: + // 1\. key和value是否包指针 + // 2\. 是否正在扩容 + // 3\. 是否是同样大小的扩容 + // 4\. 是否正在 `range`方式访问当前的buckets + // 5\. 是否有 `range`方式访问旧的bucket + flags uint8 + + B uint8 // buckets 的对数 log_2, buckets 数组的长度就是 2^B + noverflow uint16 // overflow 的 bucket 近似数 + hash0 uint32 // hash种子 计算 key 的哈希的时候会传入哈希函数 + buckets unsafe.Pointer // 指向 buckets 数组,大小为 2^B 如果元素个数为0,就为 nil + + // 扩容的时候,buckets 长度会是 oldbuckets 的两倍 + oldbuckets unsafe.Pointer // bucket slice指针,仅当在扩容的时候不为nil + + nevacuate uintptr // 扩容时已经移到新的map中的bucket数量 + extra *mapextra // optional fields +} +``` + +里面最重要的是buckets(桶)。buckets是一个指针,最终它指向的是一个结构体: + +```go +// A bucket for a Go map. +type bmap struct { + tophash [bucketCnt]uint8 +} +``` + +每个bucket固定包含8个key和value(可以查看源码bucketCnt=8).实现上面是一个固定的大小连续内存块,分成四部分:每个条目的状态,8个key值,8个value值,指向下个bucket的指针。 + +创建哈希表使用的是`makemap`函数.map 的一个关键点在于,**哈希函数**的选择。在程序启动时,会检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。这是在函数 alginit() 中完成,位于路径:`src/runtime/alg.go` 下。 + +map查找就是将key哈希后得到64位(64位机)用最后B个比特位计算在哪个桶。在 bucket 中,从前往后找到第一个空位。这样,在查找某个 key 时,先找到对应的桶,再去遍历 bucket 中的 key。 + +关于map的查找和扩容可以参考[map的用法到map底层实现分析](https://link.zhihu.com/?target=https%3A//blog.csdn.net/chenxun_2010/article/details/103768011%3Futm_medium%3Ddistribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0.pc_relevant_aa%26spm%3D1001.2101.3001.4242.1%26utm_relevant_index%3D3)。 + +### select的实现原理? + +select源码位于`src\runtime\select.go`,最重要的`scase` 数据结构为: + +```go +type scase struct { + c *hchan // chan + elem unsafe.Pointer // data element +} +``` + +scase.c为当前case语句所操作的channel指针,这也说明了一个case语句只能操作一个channel。 + +scase.elem表示缓冲区地址: + +* caseRecv : scase.elem表示读出channel的数据存放地址; +* caseSend : scase.elem表示将要写入channel的数据存放地址; + +select的主要实现位于:`selectgo`函数:其主要功能如下: + +```go +//1\. 锁定scase语句中所有的channel + //2\. 按照随机顺序检测scase中的channel是否ready + // 2.1 如果case可读,则读取channel中数据,解锁所有的channel,然后返回(case index, true) + // 2.2 如果case可写,则将数据写入channel,解锁所有的channel,然后返回(case index, false) + // 2.3 所有case都未ready,则解锁所有的channel,然后返回(default index, false) + //3\. 所有case都未ready,且没有default语句 + // 3.1 将当前协程加入到所有channel的等待队列 + // 3.2 当将协程转入阻塞,等待被唤醒 + //4\. 唤醒后返回channel对应的case index + // 4.1 如果是读操作,解锁所有的channel,然后返回(case index, true) + // 4.2 如果是写操作,解锁所有的channel,然后返回(case index, false) +``` + +参考资料:[Go select的使用和实现原理](https://link.zhihu.com/?target=https%3A//www.cnblogs.com/wuyepeng/p/13910678.html%23%3A~%3Atext%3D%25E4%25B8%2580%25E3%2580%2581select%25E7%25AE%2580%25E4%25BB%258B.%25201.Go%25E7%259A%2584select%25E8%25AF%25AD%25E5%258F%25A5%25E6%2598%25AF%25E4%25B8%2580%25E7%25A7%258D%25E4%25BB%2585%25E8%2583%25BD%25E7%2594%25A8%25E4%25BA%258Echannl%25E5%258F%2591%25E9%2580%2581%25E5%2592%258C%25E6%258E%25A5%25E6%2594%25B6%25E6%25B6%2588%25E6%2581%25AF%25E7%259A%2584%25E4%25B8%2593%25E7%2594%25A8%25E8%25AF%25AD%25E5%258F%25A5%25EF%25BC%258C%25E6%25AD%25A4%25E8%25AF%25AD%25E5%258F%25A5%25E8%25BF%2590%25E8%25A1%258C%25E6%259C%259F%25E9%2597%25B4%25E6%2598%25AF%25E9%2598%25BB%25E5%25A1%259E%25E7%259A%2584%25EF%25BC%259B%25E5%25BD%2593select%25E4%25B8%25AD%25E6%25B2%25A1%25E6%259C%2589case%25E8%25AF%25AD%25E5%258F%25A5%25E7%259A%2584%25E6%2597%25B6%25E5%2580%2599%25EF%25BC%258C%25E4%25BC%259A%25E9%2598%25BB%25E5%25A1%259E%25E5%25BD%2593%25E5%2589%258Dgroutine%25E3%2580%2582.%25202.select%25E6%2598%25AFGolang%25E5%259C%25A8%25E8%25AF%25AD%25E8%25A8%2580%25E5%25B1%2582%25E9%259D%25A2%25E6%258F%2590%25E4%25BE%259B%25E7%259A%2584I%252FO%25E5%25A4%259A%25E8%25B7%25AF%25E5%25A4%258D%25E7%2594%25A8%25E7%259A%2584%25E6%259C%25BA%25E5%2588%25B6%25EF%25BC%258C%25E5%2585%25B6%25E4%25B8%2593%25E9%2597%25A8%25E7%2594%25A8%25E6%259D%25A5%25E6%25A3%2580%25E6%25B5%258B%25E5%25A4%259A%25E4%25B8%25AAchannel%25E6%2598%25AF%25E5%2590%25A6%25E5%2587%2586%25E5%25A4%2587%25E5%25AE%258C%25E6%25AF%2595%25EF%25BC%259A%25E5%258F%25AF%25E8%25AF%25BB%25E6%2588%2596%25E5%258F%25AF%25E5%2586%2599%25E3%2580%2582.%2C3.select%25E8%25AF%25AD%25E5%258F%25A5%25E4%25B8%25AD%25E9%2599%25A4default%25E5%25A4%2596%25EF%25BC%258C%25E6%25AF%258F%25E4%25B8%25AAcase%25E6%2593%258D%25E4%25BD%259C%25E4%25B8%2580%25E4%25B8%25AAchannel%25EF%25BC%258C%25E8%25A6%2581%25E4%25B9%2588%25E8%25AF%25BB%25E8%25A6%2581%25E4%25B9%2588%25E5%2586%2599.%25204.select%25E8%25AF%25AD%25E5%258F%25A5%25E4%25B8%25AD%25E9%2599%25A4default%25E5%25A4%2596%25EF%25BC%258C%25E5%2590%2584case%25E6%2589%25A7%25E8%25A1%258C%25E9%25A1%25BA%25E5%25BA%258F%25E6%2598%25AF%25E9%259A%258F%25E6%259C%25BA%25E7%259A%2584.%25205.select%25E8%25AF%25AD%25E5%258F%25A5%25E4%25B8%25AD%25E5%25A6%2582%25E6%259E%259C%25E6%25B2%25A1%25E6%259C%2589default%25E8%25AF%25AD%25E5%258F%25A5%25EF%25BC%258C%25E5%2588%2599%25E4%25BC%259A%25E9%2598%25BB%25E5%25A1%259E%25E7%25AD%2589%25E5%25BE%2585%25E4%25BB%25BB%25E4%25B8%2580case.%25206.select%25E8%25AF%25AD%25E5%258F%25A5%25E4%25B8%25AD%25E8%25AF%25BB%25E6%2593%258D%25E4%25BD%259C%25E8%25A6%2581%25E5%2588%25A4%25E6%2596%25AD%25E6%2598%25AF%25E5%2590%25A6%25E6%2588%2590%25E5%258A%259F%25E8%25AF%25BB%25E5%258F%2596%25EF%25BC%258C%25E5%2585%25B3%25E9%2597%25AD%25E7%259A%2584channel%25E4%25B9%259F%25E5%258F%25AF%25E4%25BB%25A5%25E8%25AF%25BB%25E5%258F%2596). + +### go的interface怎么实现的? + +go interface源码在`runtime\iface.go`中。 + +go的接口由两种类型实现`iface`和`eface`。iface是包含方法的接口,而eface不包含方法。 + +* `iface` + +对应的数据结构是(位于`src\runtime\runtime2.go`): + +```go +type iface struct { + tab *itab + data unsafe.Pointer +} +``` + +可以简单理解为,tab表示接口的具体结构类型,而data是接口的值。 + +itab: + +```go +type itab struct { + inter *interfacetype //此属性用于定位到具体interface + _type *_type //此属性用于定位到具体interface + hash uint32 // copy of _type.hash. Used for type switches. + _ [4]byte + fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. +} +``` + +属性`interfacetype`类似于`_type`,其作用就是interface的公共描述,类似的还有`maptype`、`arraytype`、`chantype`…其都是各个结构的公共描述,可以理解为一种外在的表现信息。interfaetype和type唯一确定了接口类型,而hash用于查询和类型判断。fun表示方法集。 + +* `eface` + +与iface基本一致,但是用`_type`直接表示类型,这样的话就无法使用方法。 + +```go +type eface struct { + _type *_type + data unsafe.Pointer +} +``` + +这里篇幅有限,深入讨论可以看:[深入研究 Go interface 底层实现](https://link.zhihu.com/?target=https%3A//halfrost.com/go_interface/%23toc-1) + +### go的reflect 底层实现 + +go reflect源码位于`src\reflect\`下面,作为一个库独立存在。反射是基于**接口**实现的。 + +Go反射有三大法则: + +* 反射从**接口**映射到**反射对象;** + +![](https://pic2.zhimg.com/80/v2-350518add3d5e2757a8bc98f3c6fc15d_1440w.jpg)法则1 + +* 反射从**反射对象**映射到**接口值**; + +![](https://pic3.zhimg.com/80/v2-c2354d13a1514a482efa60e3d8cff816_1440w.jpg)法则2 + +* 只有**值可以修改**(settable),才可以**修改**反射对象。 + +Go反射基于上述三点实现。我们先从最核心的两个源文件入手`type.go`和`value.go`. + +type用于获取当前值的类型。value用于获取当前的值。 + +> 参考资料:[The Laws of Reflection](https://link.zhihu.com/?target=https%3A//go.dev/blog/laws-of-reflection), [图解go反射实现原理](https://link.zhihu.com/?target=https%3A//i6448038.github.io/2020/02/15/golang-reflection/) + +### go GC的原理知道吗? + +如果需要从源码角度解释GC,推荐阅读(非常详细,图文并茂): + +[https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/](https://link.zhihu.com/?target=https%3A//draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/) + +### go里用过哪些设计模式 。[Go 语言垃圾收集器的实现原理](https://link.zhihu.com/?target=https%3A//draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/)go里用过哪些设计模式 。 + +工厂模式:比如New()方法返回一个对象实例。 + +单例模式:比如日志系统,只需要一个实例就可以完成日志记录了。 + +更多的可以模式可以参考:[Go语言设计模式汇总](https://link.zhihu.com/?target=https%3A//www.cnblogs.com/Survivalist/p/11207789.html) + +### go的调试/分析工具用过哪些。 + +go的自带工具链相当丰富, + +* go cover : 测试代码覆盖率; +* godoc: 用于生成go文档; +* pprof:用于性能调优,针对cpu,内存和并发; +* race:用于竞争检测; + +### 进程被kill,如何保证所有goroutine顺利退出 + +goroutine监听SIGKILL信号,一旦接收到SIGKILL,则立刻退出。可采用select方法。 + +### 说说context包的作用?你用过哪些,原理知道吗? + +`context`可以用来在`goroutine`之间传递上下文信息,相同的`context`可以传递给运行在不同`goroutine`中的函数,上下文对于多个`goroutine`同时使用是安全的,`context`包定义了上下文类型,可以使用`background`、`TODO`创建一个上下文,在函数调用链之间传播`context`,也可以使用`WithDeadline`、`WithTimeout`、`WithCancel` 或 `WithValue` 创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:`context`的作用就是在不同的`goroutine`之间同步请求特定的数据、取消信号以及处理请求的截止日期。 + +关于context原理,可以参看:[小白也能看懂的context包详解:从入门到精通](https://link.zhihu.com/?target=https%3A//cloud.tencent.com/developer/article/1900658) + +### grpc为啥好,基本原理是什么,和http比呢 + +官方介绍:gRPC 是一个现代开源的**高性能远程过程调用** (RPC) 框架,可以在**任何环境**中运行。它可以通过对负载平衡、跟踪、健康检查和身份验证的可插拔支持有效地连接数据中心内和跨数据中心的服务。它也适用于分布式计算的最后一英里,将设备、移动应用程序和浏览器连接到后端服务。 + +区别: +- rpc是远程过程调用,就是本地去调用一个远程的函数,而http是通过 url和符合restful风格的数据包去发送和获取数据; +- rpc的一般使用的编解码协议更加高效,比如grpc使用protobuf编解码。而http的一般使用json进行编解码,数据相比rpc更加直观,但是数据包也更大,效率低下; +- rpc一般用在服务内部的相互调用,而http则用于和用户交互; +相似点: +都有类似的机制,例如grpc的metadata机制和http的头机制作用相似,而且web框架,和rpc框架中都有拦截器的概念。grpc使用的是http2.0协议。 +官网:[gRPC](https://link.zhihu.com/?target=https%3A//grpc.io/) + +### etcd怎么搭建的,具体怎么用的 + +### 熔断怎么做的 + +### 服务降级怎么搞 + +### 1亿条数据动态增长,取top10,怎么实现 + +### 进程挂了怎么办 + +### nginx配置过吗,有哪些注意的点 + +### 设计一个阻塞队列 + +### mq消费阻塞怎么办 + +### 性能没达到预期,有什么解决方案 + +* * * + +## 编程系列 + +### 一个10G的文件,里面全部是自然数,一行一个,乱序排列,对其排序。在32位机器上面完成,内存限制为 2G(bitmap原理知道吗?) + +首先,10G文件是不可能一次性放到内存里的。这类问题一般有两种解决方案: + +* 将10G文件分成多个小文件,分别排序,最后合并一个文件; +* 采用bitmap + +如果面试大数据类岗位,可能面试官就想考察你对Mapreduce熟悉程度,要采用第一种merge and sort。 + +如果是算法类岗位,就要考虑bitmap,但需要注意的是bitmap**不能对重复数据进行排序**。这里我们详细介绍一下: + +定量分析一下,32位机器自然数有2^32个,用一个bit来存放一个整数,那么所需的内存是,`2^32/(8<<20) = 512MB` ,这些数存放在文件中,一行一个,需要20G容量,所以题目问10G文件,只需要256MB内存就可以完成。 + +bitmap实现具体分为两步:插入一个数,和排序。 + +```go +type BitMap struct { + vec []byte + size int +} + +func New(size int) *BitMap { + return &BitMap{ + size: size, + vec: make([]byte, size), + } +} + +func (bm *BitMap) Set(num int) (ok bool, err error) { + if num/8 >= bm.size { + return false, errors.New("the num overflows the size of bitmap") + } + bm.vec[num/8] |= 1 << (num % 8) + return true, nil +} + +func (bm *BitMap) Exist(num int) bool { + if num/8 >= bm.size { + return false + } + return bm.vec[num/8]&(1<<(num%8)) > 0 +} + +func (bm *BitMap) Sort() (ret []int) { + ret = make([]int, 0) + for i := 0; i < (8 * bm.size); i++ { + if bm.Exist(i) { + ret = append(ret, i) + } + } + return +} +``` + +### 实现使用字符串函数名,调用函数。 + +思路:采用反射的Call方法实现。 + +```go +package main +import ( + "fmt" + "reflect" +) + +type Animal struct{ + +} + +func (a *Animal) Eat(){ + fmt.Println("Eat") +} + +func main(){ + a := Animal{} + reflect.ValueOf(&a).MethodByName("Eat").Call([]reflect.Value{}) + +} +``` + +### 负载均衡算法。(一致性哈希) + +```go +package main + +import ( + "fmt" + "sort" + "strconv" +) + +type HashFunc func(key []byte) uint32 + +type ConsistentHash struct { + hash HashFunc + hashvals []int + hashToKey map[int]string + virtualNum int +} + +func NewConsistentHash(virtualNum int, fn HashFunc) *ConsistentHash { + return &ConsistentHash{ + hash: fn, + virtualNum: virtualNum, + hashToKey: make(map[int]string), + } +} + +func (ch *ConsistentHash) AddNode(keys ...string) { + for _, k := range keys { + for i := 0; i < ch.virtualNum; i++ { + conv := strconv.Itoa(i) + hashval := int(ch.hash([]byte(conv + k))) + ch.hashvals = append(ch.hashvals, hashval) + ch.hashToKey[hashval] = k + } + } + sort.Ints(ch.hashvals) +} + +func (ch *ConsistentHash) GetNode(key string) string { + if len(ch.hashToKey) == 0 { + return "" + } + keyhash := int(ch.hash([]byte(key))) + id := sort.Search(len(ch.hashToKey), func(i int) bool { + return ch.hashvals[i] >= keyhash + }) + return ch.hashToKey[ch.hashvals[id%len(ch.hashvals)]] +} + +func main() { + ch := NewConsistentHash(3, func(key []byte) uint32 { + ret, _ := strconv.Atoi(string(key)) + return uint32(ret) + }) + ch.AddNode("1", "3", "5", "7") + testkeys := []string{"12", "4", "7", "8"} + for _, k := range testkeys { + fmt.Printf("k:%s,node:%s\n", k, ch.GetNode(k)) + } +} +``` + +### (Goroutine)有三个函数,分别打印"cat", "fish","dog"要求每一个函数都用一个goroutine,按照顺序打印100次。 + +此题目考察channel,用三个无缓冲channel,如果一个channel收到信号则通知下一个。 + +```go +package main + +import ( + "fmt" + "time" +) + +var dog = make(chan struct{}) +var cat = make(chan struct{}) +var fish = make(chan struct{}) + +func Dog() { + <-fish + fmt.Println("dog") + dog <- struct{}{} +} + +func Cat() { + <-dog + fmt.Println("cat") + cat <- struct{}{} +} + +func Fish() { + <-cat + fmt.Println("fish") + fish <- struct{}{} +} + +func main() { + for i := 0; i < 100; i++ { + go Dog() + go Cat() + go Fish() + } + fish <- struct{}{} + + time.Sleep(10 * time.Second) +} +``` + +### 两个协程交替打印10个字母和数字 + +思路:采用channel来协调goroutine之间顺序。 + +主线程一般要waitGroup等待协程退出,这里简化了一下直接sleep。 + +```go +package main + +import ( + "fmt" + "time" +) + +var word = make(chan struct{}, 1) +var num = make(chan struct{}, 1) + +func printNums() { + for i := 0; i < 10; i++ { + <-word + fmt.Println(1) + num <- struct{}{} + } +} +func printWords() { + for i := 0; i < 10; i++ { + <-num + fmt.Println("a") + word <- struct{}{} + } +} + +func main() { + num <- struct{}{} + go printNums() + go printWords() + time.Sleep(time.Second * 1) +} +``` + +代码: + + [@中二的灰太狼](https://www.zhihu.com/people/2d6eba22144ae58788cef3ed60485e9d) + +### 启动 2个groutine 2秒后取消, 第一个协程1秒执行完,第二个协程3秒执行完。 + +思路:采用`ctx, _ := context.WithTimeout(context.Background(), time.Second*2)`实现2s取消。协程执行完后通过channel通知,是否超时。 + +```go +package main + +import ( + "context" + "fmt" + "time" +) + +func f1(in chan struct{}) { + + time.Sleep(1 * time.Second) + in <- struct{}{} + +} + +func f2(in chan struct{}) { + time.Sleep(3 * time.Second) + in <- struct{}{} +} + +func main() { + ch1 := make(chan struct{}) + ch2 := make(chan struct{}) + ctx, _ := context.WithTimeout(context.Background(), 2*time.Second) + + go func() { + go f1(ch1) + select { + case <-ctx.Done(): + fmt.Println("f1 timeout") + break + case <-ch1: + fmt.Println("f1 done") + } + }() + + go func() { + go f2(ch2) + select { + case <-ctx.Done(): + fmt.Println("f2 timeout") + break + case <-ch2: + fmt.Println("f2 done") + } + }() + time.Sleep(time.Second * 5) +} +``` + +代码: + + [@中二的灰太狼](https://www.zhihu.com/people/2d6eba22144ae58788cef3ed60485e9d) + +### 当select监控多个chan同时到达就绪态时,如何先执行某个任务? + +可以在子case再加一个for select语句。 + +```go +func priority_select(ch1, ch2 <-chan string) { + for { + select { + case val := <-ch1: + fmt.Println(val) + case val2 := <-ch2: + priority: + for { + select { + case val1 := <-ch1: + fmt.Println(val1) + + default: + break priority + } + } + fmt.Println(val2) + } + } + +} +``` \ No newline at end of file diff --git "a/docs/golang/\351\235\242\350\257\225\351\242\230/test.md" "b/docs/golang/\351\235\242\350\257\225\351\242\230/test.md" new file mode 100644 index 0000000..34eb5a3 --- /dev/null +++ "b/docs/golang/\351\235\242\350\257\225\351\242\230/test.md" @@ -0,0 +1,4279 @@ + +### 文章目录 + +* [SpringBoot](https://www.pudn.com/news/62d0d14055398e076b87721e.html#SpringBoot_0) +* * [1、SpringBoot入门](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1SpringBoot_7) + * * [1.1、Spring程序与SpringBoot程序对比](https://www.pudn.com/news/62d0d14055398e076b87721e.html#11SpringSpringBoot_9) + * [1.2、parent](https://www.pudn.com/news/62d0d14055398e076b87721e.html#12parent_13) + * [1.3、引导类](https://www.pudn.com/news/62d0d14055398e076b87721e.html#13_36) + * [1.4、内置Tomcat](https://www.pudn.com/news/62d0d14055398e076b87721e.html#14Tomcat_59) + * [2、Rest风格](https://www.pudn.com/news/62d0d14055398e076b87721e.html#2Rest_75) + * * [2.1、什么是Rest](https://www.pudn.com/news/62d0d14055398e076b87721e.html#21Rest_77) + * [2.2、Rest入门案例](https://www.pudn.com/news/62d0d14055398e076b87721e.html#22Rest_114) + * [2.3、Restful快速开发](https://www.pudn.com/news/62d0d14055398e076b87721e.html#23Restful_153) + * [3、配置文档](https://www.pudn.com/news/62d0d14055398e076b87721e.html#3_163) + * [3.1、基础配置](https://www.pudn.com/news/62d0d14055398e076b87721e.html#31_165) + * [3.2、配置文件类型](https://www.pudn.com/news/62d0d14055398e076b87721e.html#32_191) + * [3.3、配置文件加载优先级](https://www.pudn.com/news/62d0d14055398e076b87721e.html#33_221) + * [3.4、yaml数据格式](https://www.pudn.com/news/62d0d14055398e076b87721e.html#34yaml_230) + * [数据类型](https://www.pudn.com/news/62d0d14055398e076b87721e.html#_262) + * [3.5、读取yaml单一属性数据](https://www.pudn.com/news/62d0d14055398e076b87721e.html#35yaml_321) + * [3.6、yaml文件中的变量应用](https://www.pudn.com/news/62d0d14055398e076b87721e.html#36yaml_353) + * [3.7、读取yaml全部属性数据](https://www.pudn.com/news/62d0d14055398e076b87721e.html#37yaml_367) + * [3.8、读取yaml应用类型属性数据](https://www.pudn.com/news/62d0d14055398e076b87721e.html#38yaml_376) + * [4、SpringBoot整合JUnit](https://www.pudn.com/news/62d0d14055398e076b87721e.html#4SpringBootJUnit_430) + * * [4.1、整合JUnit](https://www.pudn.com/news/62d0d14055398e076b87721e.html#41JUnit_432) + * [4.2、整合JUnit——classes属性](https://www.pudn.com/news/62d0d14055398e076b87721e.html#42JUnitclasses_473) + * [5、SpringBoot整合MyBatis、MyBatisPlus](https://www.pudn.com/news/62d0d14055398e076b87721e.html#5SpringBootMyBatisMyBatisPlus_499) + * * [5.1、整合MyBatis](https://www.pudn.com/news/62d0d14055398e076b87721e.html#51MyBatis_501) + * [5.2、常见问题处理](https://www.pudn.com/news/62d0d14055398e076b87721e.html#52_597) + * [5.3、整合MyBatisPlus](https://www.pudn.com/news/62d0d14055398e076b87721e.html#53MyBatisPlus_614) + * [6、SpringBoot整合Druid](https://www.pudn.com/news/62d0d14055398e076b87721e.html#6SpringBootDruid_674) + * [7、SSMP](https://www.pudn.com/news/62d0d14055398e076b87721e.html#7SSMP_715) + * * [7.1、数据配置](https://www.pudn.com/news/62d0d14055398e076b87721e.html#71_717) + * [7.2、分页](https://www.pudn.com/news/62d0d14055398e076b87721e.html#72_934) + * [7.3、数据层标准开发](https://www.pudn.com/news/62d0d14055398e076b87721e.html#73_994) + * [7.4、业务层标准开发(基础CRUD)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#74CRUD_1031) + * [7.5、业务层快速开发(基于MyBatisPlus构建)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#75MyBatisPlus_1158) + * [7.7、表现层标准开发](https://www.pudn.com/news/62d0d14055398e076b87721e.html#77_1285) + * [7.8、表现层数据一致性处理(R对象)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#78R_1357) + * [8、Springboot工程打包与运行](https://www.pudn.com/news/62d0d14055398e076b87721e.html#8Springboot_1448) + * * [8.1、程序为什么要打包](https://www.pudn.com/news/62d0d14055398e076b87721e.html#81_1450) + * [8.2、SpringBoot项目快速启动(Windows版)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#82SpringBootWindows_1456) + * [8.3、打包插件](https://www.pudn.com/news/62d0d14055398e076b87721e.html#83_1511) + * [8.4、Boot工程快速启动](https://www.pudn.com/news/62d0d14055398e076b87721e.html#84Boot_1580) + * [8.6、临时属性](https://www.pudn.com/news/62d0d14055398e076b87721e.html#86_1635) + * * [8.6.1、临时属性](https://www.pudn.com/news/62d0d14055398e076b87721e.html#861_1637) + * [8.6.2、开发环境](https://www.pudn.com/news/62d0d14055398e076b87721e.html#862_1657) + * [8.7、配置环境](https://www.pudn.com/news/62d0d14055398e076b87721e.html#87_1688) + * * [8.7.1、配置文件分类](https://www.pudn.com/news/62d0d14055398e076b87721e.html#871_1691) + * [8.7.2、自定义配置文件](https://www.pudn.com/news/62d0d14055398e076b87721e.html#872_1711) + * [8.7.3、多环境开发(yaml)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#873yaml_1745) + * [8.7.4、多环境开发文件(yaml)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#874yaml_1783) + * [8.7.5、多环境分组管理](https://www.pudn.com/news/62d0d14055398e076b87721e.html#875_1821) + * [8.7.6、多环境开发控制](https://www.pudn.com/news/62d0d14055398e076b87721e.html#876_1871) + * [9、日志](https://www.pudn.com/news/62d0d14055398e076b87721e.html#9_1923) + * * [9.1、日志基础操作](https://www.pudn.com/news/62d0d14055398e076b87721e.html#91_1925) + * [9.2、快速创建日志对象](https://www.pudn.com/news/62d0d14055398e076b87721e.html#92_2006) + * [9.3、日志输出格式控制](https://www.pudn.com/news/62d0d14055398e076b87721e.html#93_2034) + * [9.4、文件记录日志](https://www.pudn.com/news/62d0d14055398e076b87721e.html#94_2061) + * [10、热部署](https://www.pudn.com/news/62d0d14055398e076b87721e.html#10_2084) + * [11、属性绑定](https://www.pudn.com/news/62d0d14055398e076b87721e.html#11_2113) + * [12、常用计量单位应用](https://www.pudn.com/news/62d0d14055398e076b87721e.html#12_2158) + * [13、测试类](https://www.pudn.com/news/62d0d14055398e076b87721e.html#13_2230) + * * [13.1、加载专用属性](https://www.pudn.com/news/62d0d14055398e076b87721e.html#131_2232) + * [13.2、加载专用类](https://www.pudn.com/news/62d0d14055398e076b87721e.html#132_2262) + * [13.3、业务层测试事务回滚](https://www.pudn.com/news/62d0d14055398e076b87721e.html#133_2381) + * [13.4、测试用例数据设定](https://www.pudn.com/news/62d0d14055398e076b87721e.html#134_2404) + * [14、数据层解决方案](https://www.pudn.com/news/62d0d14055398e076b87721e.html#14_2431) + * * [14.1、SQL](https://www.pudn.com/news/62d0d14055398e076b87721e.html#141SQL_2433) + * [14.2、MongoDB](https://www.pudn.com/news/62d0d14055398e076b87721e.html#142MongoDB_2641) + * * [14.2.1、MongoDB的使用](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1421MongoDB_2647) + * [14.2.2、MongoDB可视化客户端](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1422MongoDB_2671) + * [14.2.3、Springboot集成MongoDB](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1423SpringbootMongoDB_2689) + * [14.3、ElasticSearch(ES)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#143ElasticSearchES_2730) + * * [14.3.1、ES下载](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1431ES_2738) + * [14.3.2、ES索引、分词器](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1432ES_2750) + * [14.3.3、文档操作(增删改查)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1433_2792) + * [14.3.4、Springboot集成ES](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1434SpringbootES_2850) + * [14.3.5、索引](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1435_2939) + * [15、缓存](https://www.pudn.com/news/62d0d14055398e076b87721e.html#15_3057) + * * [15.1、缓存简介](https://www.pudn.com/news/62d0d14055398e076b87721e.html#151_3059) + * [15.2、缓存使用](https://www.pudn.com/news/62d0d14055398e076b87721e.html#152_3073) + * [15.3、其他缓存](https://www.pudn.com/news/62d0d14055398e076b87721e.html#153_3115) + * [15.4、缓存使用案例——手机验证码](https://www.pudn.com/news/62d0d14055398e076b87721e.html#154_3132) + * * [15.4.1、Cache](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1541Cache_3146) + * [15.4.2、Ehcache](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1542Ehcache_3217) + * [15.4.3、Redis](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1543Redis_3276) + * [15.4.4、memcached](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1544memcached_3316) + * [15.4.5、jetcache](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1545jetcache_3445) + * [15.4.6、j2cache](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1546j2cache_3652) + * [16、定时](https://www.pudn.com/news/62d0d14055398e076b87721e.html#16_3730) + * * [16.1、SpringBoot整合Quartz](https://www.pudn.com/news/62d0d14055398e076b87721e.html#161SpringBootQuartz_3745) + * [16.2、Spring Task](https://www.pudn.com/news/62d0d14055398e076b87721e.html#162Spring_Task_3795) + * [16.3、SpringBoot整合JavaMail](https://www.pudn.com/news/62d0d14055398e076b87721e.html#163SpringBootJavaMail_3841) + * [17、消息](https://www.pudn.com/news/62d0d14055398e076b87721e.html#17_3925) + * * [17.1、JMS](https://www.pudn.com/news/62d0d14055398e076b87721e.html#171JMS_3935) + * [17.2、AMQP](https://www.pudn.com/news/62d0d14055398e076b87721e.html#172AMQP_3954) + * [17.3、MQTT](https://www.pudn.com/news/62d0d14055398e076b87721e.html#173MQTT_3970) + * [17.4、Kafka](https://www.pudn.com/news/62d0d14055398e076b87721e.html#174Kafka_3976) + * [17.5、消息案例](https://www.pudn.com/news/62d0d14055398e076b87721e.html#175_3982) + * [17.6、ActiveMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#176ActiveMQ_4000) + * * [17.6.1、SpringBoot整合ActiveMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1761SpringBootActiveMQ_4018) + * [17.7、RabbitMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#177RabbitMQ_4104) + * * [17.7.1、SpringBoot整合RabbitMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1771SpringBootRabbitMQ_4148) + * [17.8、RocketMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#178RocketMQ_4323) + * * [17.8.1、SpringBoot整合RocketMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1781SpringBootRocketMQ_4358) + * [17.9、Kafka](https://www.pudn.com/news/62d0d14055398e076b87721e.html#179Kafka_4434) + * * [17.9.1、SpringBoot整合Kafka](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1791SpringBootKafka_4476) + * [18、监控](https://www.pudn.com/news/62d0d14055398e076b87721e.html#18_4525) + * * [18.1、简介](https://www.pudn.com/news/62d0d14055398e076b87721e.html#181_4527) + * [18.2、可视化监控平台](https://www.pudn.com/news/62d0d14055398e076b87721e.html#182_4545) + * [18.3、监控原理](https://www.pudn.com/news/62d0d14055398e076b87721e.html#183_4658) + * [18.4、自定义监控指标](https://www.pudn.com/news/62d0d14055398e076b87721e.html#184_4703) + * [18.3、监控原理](https://www.pudn.com/news/62d0d14055398e076b87721e.html#183_4879) + * [18.4、自定义监控指标](https://www.pudn.com/news/62d0d14055398e076b87721e.html#184_4928) + +## 1、SpringBoot入门 + +### 1.1、Spring程序与SpringBoot程序对比 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/43e3ba472de049f9a489b0773ecf88a7.png) + +### 1.2、parent + +![在这里插入图片描述](https://img-blog.csdnimg.cn/51971e3edda741028356f6cd3947d4a2.png) + +* starter + + SpringBoot中常见项目名称,定义了当前项目使用的所有依赖坐标,以达到减少依赖配置的目的 + +* parent + + 所有SpringBoot项目要继承的项目,定义了若干个坐标版本号(依赖管理,而非依赖),以达到减少依赖冲突的目的 + + spring-boot-starter-parent各版本间存在着诸多坐标版本不同 + +* 实际开发 + + 使用任意坐标时,仅书写GAV(groupId, artifactId, version)中的G和A,V由SpringBoot提供,除非SpringBoot未提供对应版本V + + 如发生坐标错误,再指定Version(要小心版本冲突) + +### 1.3、引导类 + +* 启动方式 + + @SpringBootApplication + public class Springboot0101QuickstartApplication { + + public static void main(String[] args) { + ConfigurableApplicationContext ctx = SpringApplication.run(Springboot0101QuickstartApplication.class, args); + //获取bean对象 + BookController bean = ctx.getBean(BookController.class); + System.out.println("bean======>" + bean); + } + } + + SpringBoot的引导类是Boot工程的执行入口,运行main方法就可以启动项目 + + SpringBoot工程运行后初始化Spring容器,扫描引导类所在包加载bean + +### 1.4、内置Tomcat + +![在这里插入图片描述](https://img-blog.csdnimg.cn/17e3b9fb27e745dab90d4f73c4168c27.png) + +* Jetty比Tomcat更轻量级,可扩展性更强(相较于Tomcat),谷歌应用引擎(GAE)已经全面切换为Jetty + +* 内置服务器 + + tomcat(默认) apache出品,粉丝多,应用面广,负载了若干较重的组件 + + jetty 更轻量级,负载性能远不及tomcat + + undertow undertow,负载性能勉强跑赢tomcat + +## 2、Rest风格 + +### 2.1、什么是Rest + +1. 什么是 rest : + + REST(Representational State Transfer)表现形式状态转换 + + 传统风格资源描述形式 + http://localhost/user/getById?id=1 (得到id为1的用户) + http://localhost/user/saveUser (保存用户) + + REST风格描述形式 + http://localhost/user/1 (得到id为1的用户) + http://localhost/user (保存用户) + +2. 优点: + + 隐藏资源的访问行为, 无法通过地址得知对资源是何种操作 + 书写简化 + +3. 按照REST风格访问资源时使用行为动作区分对资源进行了何种操作 + + GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源 + + http://localhost/users 查询全部用户信息 GET (查询) + http://localhost/users/1 查询指定用户信息 GET (查询) + http://localhost/users 添加用户信息 POST (新增/保存) + http://localhost/users 修改用户信息 PUT (修改/更新) + http://localhost/users/1 删除用户信息 DELETE (删除) + +注意: + +上述行为是约定方式,约定不是规范,可以打破,所以称REST风格,而不是REST规范 +描述模块的名称通常使用复数,也就是加s的格式描述,表示此类资源,而非单个资源,例如: users、 + +1. 根据REST风格对资源进行访问称为**RESTful** + +### 2.2、Rest入门案例 + +步骤: + +①设定http请求动作(动词) + +使用 @RequestMapping 注解的 method 属性声明请求的方式 + +使用 @RequestBody 注解 获取请求体内容。直接使用得到是 key=value&key=value…结构的数据。get 请求方式不适用。 + +使用@ResponseBody 注解实现将 controller 方法返回对象转换为 json 响应给客户端。 + +@RequestMapping(value=“/users”,method=RequestMethod.POST) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/0a4ded7bb220415bbcd779c230f2d94f.png) + +②:设定请求参数(路径变量) + +使用`@PathVariable` 用于绑定 url 中的占位符。例如:请求 url 中 /delete/`{id}`,这个`{id}`就是 url 占位符。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/fd8f1fc9f45b429ebabf148df521d3e0.png) + +@RequestMapping + +![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HhzZsWGO-1657811363433)(SpringBoot.assets/image-20220312164532116.png)]](https://img-blog.csdnimg.cn/7fb5465b71a346d4b2e8920493c5e36c.png) + +@PathVariable + +![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yaepxdng-1657811363434)(SpringBoot.assets/image-20220312164552778.png)]](https://img-blog.csdnimg.cn/a9c22e3367a7480ea219baaa21de868e.png) + +@RequestBody @RequestParam @PathVariable +![在这里插入图片描述](https://img-blog.csdnimg.cn/1ce13dc308fe4147981ce26622eec1ca.png) + +### 2.3、Restful快速开发 + +使用 `@RestController` 注解开发 RESTful 风格 +![在这里插入图片描述](https://img-blog.csdnimg.cn/9e782ebc0c8b458ea30bd3132f9e7f57.png) + +使用 @GetMapping @PostMapping @PutMapping @DeleteMapping 简化 `@RequestMapping` 注解开发 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/4b55439a2e094dae84f37f2f59f7b0f2.png) + +### 3、配置文档 + +### 3.1、基础配置 + +1. 修改配置 + 修改服务器端口 + server.port=80 + 关闭运行日志图标(banner) + spring.main.banner-mode=off + 设置日志相关 + logging.level.root=debug + +# 服务器端口配置 +server.port=80 + +# 修改banner +# spring.main.banner-mode=off +# spring.banner.image.location=logo.png + +# 日志 +logging.level.root=info + +1. SpringBoot内置属性查询 + https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties + 官方文档中参考文档第一项:Application Propertie + +### 3.2、配置文件类型 + +* 配置文件格式 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/cf1d1024f92142d69b1b1e30038a9434.png) + +* SpringBoot提供了多种属性配置方式 + + application.properties + + server.port=80 + + application.yml + + server: + port: 81 + + application.yaml + + server: + port: 82 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/aa8415068c074278ad3085414efc96c8.png) + +### 3.3、配置文件加载优先级 + +* SpringBoot配置文件加载顺序 + application.properties > application.yml > application.yaml +* 常用配置文件种类 + application.yml + +### 3.4、yaml数据格式 + +yaml + +YAML(YAML Ain’t Markup Language),一种数据序列化格式 + +优点: + 容易阅读 + 容易与脚本语言交互 + 以数据为核心,重数据轻格式 + +YAML文件扩展名 + .yml(主流) + .yaml + +yaml语法规则 +基本语法 + +key: value -> value 前面一定要有空格 +大小写敏感 +属性层级关系使用多行描述,每行结尾使用冒号结束 +使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键) +属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔) +# 表示注释 +核心规则:数据前面要加空格与冒号隔开 server: + servlet: + context-path: /hello + port: 82 +### 数据类型 + +* 字面值表示方式 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/8f1261fad4f842d8ab8e8bd591d4aaf6.png) + +# 字面值表示方式 + +boolean: TRUE #TRUE,true,True,FALSE,false , False 均可 +float: 3.14 #6.8523015e+5 # 支持科学计数法 +int: 123 #0b1010_0111_0100_1010_1110 # 支持二进制、八进制、十六进制 +# null: ~ # 使用 ~ 表示 null +string: HelloWorld # 字符串可以直接书写 +string2: "Hello World" # 可以使用双引号包裹特殊字符 +date: 2018-02-17 # 日期必须使用 yyyy-MM-dd 格式 +datetime: 2018-02-17T15:02:31+08:00 # 时间和日期之间使用 T 连接,最后使用 + 代表时区 + +* 数组表示方式:在属性名书写位置的下方使用减号作为数据开始符号,每行书写一个数据,减号与数据间空格分隔 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/450bae74c7904daf8c26854788698b18.png) + +subject: + - Java + - 前端 + - 大数据 + +enterprise: + name: zhangsan + age: 16 + +subject2: + - Java + - 前端 + - 大数据 +likes: [王者荣耀,刺激战场] # 数组书写缩略格式 + +users: # 对象数组格式 + - name: Tom + age: 4 + + - name: Jerry + age: 5 +users2: # 对象数组格式二 + - + name: Tom + age: 4 + - + name: Jerry + age: 5 + +# 对象数组缩略格式 +users3: [ { name:Tom , age:4 } , { name:Jerry , age:5 } ] +### 3.5、读取yaml单一属性数据 + +* 使用@Value读取单个数据,属性名引用方式:${一级属性名.二级属性名……} + ![在这里插入图片描述](https://img-blog.csdnimg.cn/332e7c2fc82449e29984c7caaa3f8c31.png) + + @Value("${country}") + private String country1; + + @Value("${user.age}") + private String age1; + + @Value("${likes[1]}") + private String likes1; + + @Value("${users[1].name}") + private String name1; + + @GetMapping + public String getById() { + System.out.println("springboot is running2..."); + System.out.println("country1=>" + country1); + System.out.println("age1=>" + age1); + System.out.println("likes1=>" + likes1); + System.out.println("name1=>" + name1); + return "springboot is running2..."; + } +### 3.6、yaml文件中的变量应用 + +* 在配置文件中可以使用属性名引用方式引用属性 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/519e7e02e15b4ae996a53faa03bfb44b.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/d20085f899a84d9c81365897c8c01775.png) + +* 属性值中如果出现转移字符,需要使用双引号包裹 + + lesson: "Spring\tboot\nlesson" + +### 3.7、读取yaml全部属性数据 + +* 封装全部数据到Environment对象 +* 注意 要导这个 包 +* **import org.springframework.core.env.Environment** + ![在这里插入图片描述](https://img-blog.csdnimg.cn/e8e576fdf9244d9692f6dd8d92c18602.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/0d64b9e6621a424999400a063743cb6d.png) + +### 3.8、读取yaml应用类型属性数据 + +* 自定义对象封装指定数据 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/feb05e9432c0461e87474bde8cbf5db1.png) + +* 自定义对象封装指定数据的作用 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/4ffb01bae1ba4e088750c5882ca71094.png) + +# 创建类,用于封装下面的数据 +# 由spring帮我们去加载数据到对象中,一定要告诉spring加载这组信息 +# 使用时候从spring中直接获取信息使用 + +datasource: + driver: com.mysql.jdbc.Driver + url: jdbc:mysql://localhost/springboot_db + username: root + password: root666123 //1.定义数据模型封装yaml文件中对应的数据 +//2.定义为spring管控的bean +@Component +//3.指定加载的数据 +@ConfigurationProperties(prefix = "datasource") +public class MyDataSource { + + private String driver; + private String url; + private String username; + private String password; + + //省略get/set/tostring 方法 +} + +使用自动装配封装指定数据 + + @Autowired + private MyDataSource myDataSource; + +输出查看 + +System.out.println(myDataSource); +## 4、SpringBoot整合JUnit + +### 4.1、整合JUnit + +* 添加Junit的起步依赖 Spring Initializr 创建时自带 + + + + org.springframework.boot + spring-boot-starter-test + test + +* SpringBoot整合JUnit + + @SpringBootTest + class Springboot07JunitApplicationTests { + @Autowired + private BookService bookService; + @Test + public void testSave(){ + bookService.save(); + } + } +* @SpringBootTest + 名称:@SpringBootTest + 类型:测试类注解 + 位置:测试类定义上方 + 作用:设置JUnit加载的SpringBoot启动类 + 范例: + + @SpringBootTest + class Springboot05JUnitApplicationTests {} + +### 4.2、整合JUnit——classes属性 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/11b8072d4158454d829dfaece9287625.png) + +@SpringBootTest(classes = Springboot04JunitApplication.class) +//@ContextConfiguration(classes = Springboot04JunitApplication.class) +class Springboot04JunitApplicationTests { + //1.注入你要测试的对象 + @Autowired + private BookDao bookDao; + + @Test + void contextLoads() { + //2.执行要测试的对象对应的方法 + bookDao.save(); + System.out.println("two..."); + } +} + +注意: + +* 如果测试类在SpringBoot启动类的包或子包中,可以省略启动类的设置,也就是省略classes的设定 + +## 5、SpringBoot整合MyBatis、MyBatisPlus + +### 5.1、整合MyBatis + +①:创建新模块,选择Spring初始化,并配置模块相关基础信息 + +②:选择当前模块需要使用的技术集(MyBatis、MySQL) + +③:设置数据源参数 + +#DB Configuration: +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/springboot_db + username: root + password: 123456 + +④:创建user表 +在 springboot_db 数据库中创建 user 表 + +-- ---------------------------- +-- Table structure for `user` +-- ---------------------------- +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(50) DEFAULT NULL, + `password` varchar(50) DEFAULT NULL, + `name` varchar(50) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Records of user +-- ---------------------------- +INSERT INTO `user` VALUES ('1', 'zhangsan', '123', '张三'); +INSERT INTO `user` VALUES ('2', 'lisi', '123', '李四'); + +⑤:创建实体Bean + +public class User { + // 主键 + private Long id; + // 用户名 + private String username; + // 密码 + private String password; + // 姓名 + private String name; + + //此处省略getter,setter,toString方法 .. .. + +} + +⑥: 定义数据层接口与映射配置 + +@Mapper +public interface UserDao { + + @Select("select * from user") + public List getAll(); +} + +⑦:测试类中注入dao接口,测试功能 + +@SpringBootTest +class Springboot05MybatisApplicationTests { + + @Autowired + private UserDao userDao; + + @Test + void contextLoads() { + List userList = userDao.getAll(); + System.out.println(userList); + } + +} + +⑧:运行如下 + +[User{id=1, username='zhangsan', password='123', name='张三'}, User{id=2, username='lisi', password='123', name='李四'}] +### 5.2、常见问题处理 + +SpringBoot版本低于2.4.3(不含),Mysql驱动版本大于8.0时,需要在url连接串中配置时区 + +jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC + +或在MySQL数据库端配置时区解决此问题 + +1.MySQL 8.X驱动强制要求设置时区 + +修改url,添加serverTimezone设定 +修改MySQL数据库配置(略) + +2.驱动类过时,提醒更换为com.mysql.cj.jdbc.Driver + +### 5.3、整合MyBatisPlus + +①:手动添加SpringBoot整合MyBatis-Plus的坐标,可以通过mvnrepository获取 + + + com.baomidou + mybatis-plus-boot-starter + 3.4.3 + + +注意事项: 由于SpringBoot中未收录MyBatis-Plus的坐标版本,需要指定对应的Version + +②:定义数据层接口与映射配置,继承BaseMapper + +@Mapper +public interface UserDao extends BaseMapper { + +} + +③:其他同SpringBoot整合MyBatis +(略) + +④:测试类中注入dao接口,测试功能 + +@SpringBootTest +class Springboot06MybatisPlusApplicationTests { + + @Autowired + private UserDao userDao; + + @Test + void contextLoads() { + List users = userDao.selectList(null); + System.out.println(users); + } + +} + +⑤: 运行如下: + +[User{id=1, username='zhangsan', password='123', name='张三'}, User{id=2, username='lisi', password='123', name='李四'}] + +注意: 如果你的数据库表有前缀要在 application.yml 添加如下配制 + +#设置Mp相关的配置 +mybatis-plus: + global-config: + db-config: + table-prefix: tbl_ +### 6、SpringBoot整合Druid + +①: 导入Druid对应的starter + + + com.alibaba + druid-spring-boot-starter + 1.2.6 + #DB Configuration: +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC + username: root + password: Lemon + type: com.alibaba.druid.pool.DruidDataSource + +②: 指定数据源类型 (这种方式只需导入一个 Druid 的坐标) + +spring: + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC + username: root + password: 123456 + +或者 变更Druid的配置方式(推荐) 这种方式需要导入 Druid对应的starter + +## 7、SSMP + +### 7.1、数据配置 + +1\. 案例实现方案分析 + 实体类开发————使用Lombok快速制作实体类 + Dao开发————整合MyBatisPlus,制作数据层测试类 + Service开发————基于MyBatisPlus进行增量开发,制作业务层测试类 + Controller开发————基于Restful开发,使用PostMan测试接口功能 + Controller开发————前后端开发协议制作 + 页面开发————基于VUE+ElementUI制作,前后端联调,页面数据处理,页面消息处理 + 列表、新增、修改、删除、分页、查询 + 项目异常处理 + 按条件查询————页面功能调整、Controller修正功能、Service修正功能 +2\. SSMP案例制作流程解析 + 先开发基础CRUD功能,做一层测一层 + 调通页面,确认异步提交成功后,制作所有功能 + 添加分页功能与查询功能 DROP TABLE IF EXISTS `tbl_book`; +CREATE TABLE `tbl_book` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `type` varchar(20) DEFAULT NULL, + `name` varchar(50) DEFAULT NULL, + `description` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Records of tbl_book +-- ---------------------------- +INSERT INTO `tbl_book` VALUES ('1', '计算机理论', 'Spring实战第5版', 'Spring入门经典教程,深入理解Spring原理技术内幕'); +INSERT INTO `tbl_book` VALUES ('2', '计算机理论', 'Spring 5核心原理与30个类手写实战', '十年沉淀之作,写Spring精华思想'); +INSERT INTO `tbl_book` VALUES ('3', '计算机理论', 'Spring 5设计模式', '深入Spring源码剖析Spring源码中蕴含的10大设计模式'); +INSERT INTO `tbl_book` VALUES ('4', '计算机理论', 'Spring MVC+ MyBatis开发从入门到项目实战', '全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手'); +INSERT INTO `tbl_book` VALUES ('5', '计算机理论', '轻量级Java Web企业应用实战', '源码级剖析Spring框架,适合已掌握Java基础的读者'); +INSERT INTO `tbl_book` VALUES ('6', '计算机理论', 'Java核心技术卷|基础知识(原书第11版)', 'Core Java第11版,Jolt大奖获奖作品,针对Java SE9、10、 11全面更新'); +INSERT INTO `tbl_book` VALUES ('7', '计算机理论', '深入理解Java虚拟机', '5个维度全面剖析JVM,面试知识点全覆盖'); +INSERT INTO `tbl_book` VALUES ('8', '计算机理论', 'Java编程思想(第4版)', 'Java学习必读经典殿堂级著作!赢得了全球程序员的广泛赞誉'); +INSERT INTO `tbl_book` VALUES ('9', '计算机理论', '零基础学Java (全彩版)', '零基础自学编程的入门]图书,由浅入深,详解Java语言的编程思想和核心技术'); +INSERT INTO `tbl_book` VALUES ('10', '市场营销', '直播就该这么做:主播高效沟通实战指南', '李子柒、李佳琦、薇娅成长为网红的秘密都在书中'); +INSERT INTO `tbl_book` VALUES ('11', '市场营销', '直播销讲实战一本通', '和秋叶一起学系列网络营销书籍'); +INSERT INTO `tbl_book` VALUES ('12', '市场营销', '直播带货:淘宝、天猫直播从新手到高手', '一本教你如何玩转直播的书, 10堂课轻松实现带货月入3W+'); + + org.springframework.boot + spring-boot-starter-web + + + + mysql + mysql-connector-java + runtime + + + + com.baomidou + mybatis-plus-boot-starter + 3.4.3 + + + + com.alibaba + druid-spring-boot-starter + 1.2.6 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.projectlombok + lombok + + + + com.baomidou + mybatis-plus-boot-starter + 3.4.3 + + + + com.alibaba + druid-spring-boot-starter + 1.2.6 + + + server: + port: 80 +# druid 数据源配制 +spring: + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC + username: root + password: Lemon + +# mybatis-plus +mybatis-plus: + global-config: + db-config: + table-prefix: tbl_ + id-type: auto # 主键策略 + +Book.class + +@Data +public class Book { + private Integer id; + private String type; + private String name; + private String description; +} + +BookDao.class + +@Mapper +public interface BookDao extends BaseMapper { + + /** + * 查询一个 + * 这是 Mybatis 开发 + * @param id + * @return + */ + @Select("select * from tbl_book where id = #{id}") + Book getById(Integer id); +} + +测试类 + +@SpringBootTest +public class BookDaoTestCase { + + @Autowired + private BookDao bookDao; + + @Test + void testGetById() { + System.out.println(bookDao.getById(1)); + System.out.println(bookDao.selectById(1)); + } + + @Test + void testSave() { + Book book = new Book(); + book.setType("测试数据123"); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookDao.insert(book); + } + + @Test + void testUpdate() { + Book book = new Book(); + book.setId(13); + book.setType("测试数据asfd"); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookDao.updateById(book); + } + + @Test + void testDelete() { + bookDao.deleteById(13); + } + + @Test + void testGetAll() { + System.out.println(bookDao.selectList(null)); + } + + @Test + void testGetPage() { + } + + @Test + void testGetBy() { + } +} + +开启MybatisPlus运行日志 + +# mybatis-plus +mybatis-plus: + global-config: + db-config: + table-prefix: tbl_ + id-type: auto # 主键策略 + configuration: + # 开启MyBatisPlus的日志 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +### 7.2、分页 + +分页操作需要设定分页对象IPage + +@Test +void testGetPage() { + IPage page = new Page(1, 5); + bookDao.selectPage(page, null); +} + +* IPage对象中封装了分页操作中的所有数据 + + 数据 + + 当前页码值 + + 每页数据总量 + + 最大页码值 + + 数据总量 + +* 分页操作是在MyBatisPlus的常规操作基础上增强得到,内部是动态的拼写SQL语句,因此需要增强对应的功能 + +使用MyBatisPlus拦截器实现 + +@Configuration +public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + //1\. 定义 Mp 拦截器 + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //2\. 添加具体的拦截器 分页拦截器 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + return interceptor; + } +} + +测试 + +@Test +void testGetPage() { + IPage page = new Page(1, 5); + bookDao.selectPage(page, null); + System.out.println(page.getCurrent()); + System.out.println(page.getSize()); + System.out.println(page.getPages()); + System.out.println(page.getTotal()); + System.out.println(page.getRecords()); +} +### 7.3、数据层标准开发 + +* 使用QueryWrapper对象封装查询条件,推荐使用LambdaQueryWrapper对象,所有查询操作封装成方法调用 + +@Test +void testGetBy2() { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.like(Book::getName, "Spring"); + bookDao.selectList(lambdaQueryWrapper); +} @Test +void testGetBy() { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.like("name", "Spring"); + bookDao.selectList(queryWrapper); +} + +* 支持动态拼写查询条件 + +@Test +void testGetBy2() { + String name = "1"; + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + //if (name != null) lambdaQueryWrapper.like(Book::getName,name); + lambdaQueryWrapper.like(Strings.isNotEmpty(name), Book::getName, name); + bookDao.selectList(lambdaQueryWrapper); +} +### 7.4、业务层标准开发(基础CRUD) + +* Service层接口定义与数据层接口定义具有较大区别,不要混用 + selectByUserNameAndPassword(String username,String password); 数据层接口 + login(String username,String password); Service层接口 + +* 接口定义 + + public interface BookService { + + Boolean save(Book book); + + Boolean update(Book book); + + Boolean delete(Integer id); + + Book getById(Integer id); + + List getAll(); + + IPage getPage(int currentPage,int pageSize); + } +* 实现类定义 + +@Service +public class BookServiceImpl implements BookService { + + @Autowired + private BookDao bookDao; + + @Override + public Boolean save(Book book) { + return bookDao.insert(book) > 0; + } + + @Override + public Boolean update(Book book) { + return bookDao.updateById(book) > 0; + } + + @Override + public Boolean delete(Integer id) { + return bookDao.deleteById(id) > 0; + } + + @Override + public Book getById(Integer id) { + return bookDao.selectById(id); + } + + @Override + public List getAll() { + return bookDao.selectList(null); + } + + @Override + public IPage getPage(int currentPage, int pageSize) { + IPage page = new Page(currentPage, pageSize); + bookDao.selectPage(page, null); + return page; + } +} + +* 测试类定义 + +@SpringBootTest +public class BookServiceTestCase { + + @Autowired + private BookService bookService; + + @Test + void testGetById() { + System.out.println(bookService.getById(4)); + } + + @Test + void testSave() { + Book book = new Book(); + book.setType("测试数据123"); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookService.save(book); + } + + @Test + void testUpdate() { + Book book = new Book(); + book.setId(14); + book.setType("测试数据asfd"); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookService.update(book); + } + + @Test + void testDelete() { + bookService.delete(14); + } + + @Test + void testGetAll() { + System.out.println(bookService.getAll()); + } + + @Test + void testGetPage() { + IPage page = bookService.getPage(2, 5); + System.out.println(page.getCurrent()); + System.out.println(page.getSize()); + System.out.println(page.getPages()); + System.out.println(page.getTotal()); + System.out.println(page.getRecords()); + } +} +### 7.5、业务层快速开发(基于MyBatisPlus构建) + +* 快速开发方案 + + 使用MyBatisPlus提供有业务层通用接口(ISerivce)与业务层通用实现类(ServiceImpl) + + 在通用类基础上做功能重载或功能追加 + + 注意重载时不要覆盖原始操作,避免原始提供的功能丢失 + +* 接口定义 + +public interface IBookService extends IService { +} + +* 接口追加功能 + +public interface IBookService extends IService { + + // 追加的操作与原始操作通过名称区分,功能类似 + Boolean delete(Integer id); + + Boolean insert(Book book); + + Boolean modify(Book book); + + Book get(Integer id); +} + +![在这里插入图片描述](https://img-blog.csdnimg.cn/ac8db44e41344a03964803b29491c6a6.png) + +* 实现类定义 + +@Service +public class BookServiceImpl extends ServiceImpl implements IBookService { +} + +* 实现类追加功能 + +@Service +public class BookServiceImpl extends ServiceImpl implements IBookService { + + @Autowired + private BookDao bookDao; + + public Boolean insert(Book book) { + return bookDao.insert(book) > 0; + } + + public Boolean modify(Book book) { + return bookDao.updateById(book) > 0; + } + + public Boolean delete(Integer id) { + return bookDao.deleteById(id) > 0; + } + + public Book get(Integer id) { + return bookDao.selectById(id); + } +} + +* 测试类定义 + +@SpringBootTest +public class BookServiceTest { + + @Autowired + private IBookService bookService; + + @Test + void testGetById() { + System.out.println(bookService.getById(4)); + } + + @Test + void testSave() { + Book book = new Book(); + book.setType("测试数据123"); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookService.save(book); + } + + @Test + void testUpdate() { + Book book = new Book(); + book.setId(14); + book.setType("==========="); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookService.updateById(book); + } + + @Test + void testDelete() { + bookService.removeById(14); + } + + @Test + void testGetAll() { + System.out.println(bookService.list()); + } + + @Test + void testGetPage() { + IPage page = new Page<>(2, 5); + bookService.page(page); + System.out.println(page.getCurrent()); + System.out.println(page.getSize()); + System.out.println(page.getPages()); + System.out.println(page.getTotal()); + System.out.println(page.getRecords()); + } +} +### 7.7、表现层标准开发 + +* 基于Restful进行表现层接口开发 +* 使用Postman测试表现层接口功能 + +@RestController +@RequestMapping("/books") +public class BookController { + + @Autowired + private IBookService bookService; + + @GetMapping + public List getAll() { + return bookService.list(); + } + + @PostMapping + public Boolean save(@RequestBody Book book) { + return bookService.save(book); + } + + @PutMapping + public Boolean update(@RequestBody Book book) { + return bookService.modify(book); + } + + @DeleteMapping("{id}") + public Boolean delete(@PathVariable Integer id) { + return bookService.delete(id); + } + + @GetMapping("{id}") + public Book getById(@PathVariable Integer id) { + return bookService.getById(id); + } + + @GetMapping("{currentPage}/{pageSize}") + public IPage getPage(@PathVariable Integer currentPage, @PathVariable int pageSize) { + return bookService.getPage(currentPage, pageSize); + } + +} + +添加 分页的业务层方法 + +IBookService + + IPage getPage(int currentPage,int pageSize); + +BookServiceImpl + +@Override +public IPage getPage(int currentPage, int pageSize) { + + IPage page = new Page(currentPage, pageSize); + bookDao.selectPage(page, null); + + return page; +} + +![在这里插入图片描述](https://img-blog.csdnimg.cn/6f1ad431b53e47229e88f1898ff062de.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/2fc40d92ea0846c7b38142f9e755ed77.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/f1c3c746038b49619af5de1b04e7935f.png) + +### 7.8、表现层数据一致性处理(R对象) + +统一格式 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/a1ca89547dba4aeb9b164e195e572e6a.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/ee0fa35fce3542b4991d3be29736a2bb.png) + +* 设计表现层返回结果的模型类,用于后端与前端进行数据格式统一,也称为**前后端数据协议** + +@Data +public class R { + private Boolean flag; + private Object data; + + public R() { + } + + /** + * 不返回数据的构造方法 + * + * @param flag + */ + public R(Boolean flag) { + this.flag = flag; + } + + /** + * 返回数据的构造方法 + * + * @param flag + * @param data + */ + public R(Boolean flag, Object data) { + this.flag = flag; + this.data = data; + } +} + +* 表现层接口统一返回值类型结果 + +@RestController +@RequestMapping("/books") +public class BookController { + + @Autowired + private IBookService bookService; + + @GetMapping + public R getAll() { + return new R(true, bookService.list()); + } + + @PostMapping + public R save(@RequestBody Book book) { + return new R(bookService.save(book)); + + } + + @PutMapping + public R update(@RequestBody Book book) { + return new R(bookService.modify(book)); + } + + @DeleteMapping("{id}") + public R delete(@PathVariable Integer id) { + return new R(bookService.delete(id)); + } + + @GetMapping("{id}") + public R getById(@PathVariable Integer id) { + return new R(true, bookService.getById(id)); + } + + @GetMapping("{currentPage}/{pageSize}") + public R getPage(@PathVariable Integer currentPage, @PathVariable int pageSize) { + return new R(true, bookService.getPage(currentPage, pageSize)); + } + +} + +![在这里插入图片描述](https://img-blog.csdnimg.cn/c0c4ac51439b4d418abc0dcbbea2862c.png) + +**前端部分省略** + +## 8、Springboot工程打包与运行 + +### 8.1、程序为什么要打包 + +将程序部署在独立的服务器上 +![在这里插入图片描述](https://img-blog.csdnimg.cn/cf0d396d48bb49fa9c55847904a5f0b1.png) + +### 8.2、SpringBoot项目快速启动(Windows版) + +步骤 + +①:对SpringBoot项目打包(执行[Maven](https://so.csdn.net/so/search?q=Maven&spm=1001.2101.3001.7020)构建指令package) +执行 package 打包命令之前 先执行 **mvn clean** 删除 target 目录及内容 + +mvn package + +![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Eg4DFXqd-1657811363451)(SpringBoot.assets/image-20220317212408717.png)]](https://img-blog.csdnimg.cn/a19f1b3a110349cd9e16dbbd229ded5e.png) + +打包完成 生成对应的 jar 文件 +![在这里插入图片描述](https://img-blog.csdnimg.cn/9b4a4c84a82a44328fced7242662b4b8.png) + +可能出现的问题: IDEA下 执行 Maven 命令控制台中文乱码 +Ctr+Alt+S 打开设置,在Build,Execution ,Deployment找到Build Tools下Maven项下的Runner ,在VM Options 添加 +-Dfile.encoding=GB2312 ,点击OK。 +![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z5rHQGqK-1657811363452)(SpringBoot.assets/image-20220317212514627.png)]](https://img-blog.csdnimg.cn/74c0cddee08a4766bf1390fb3f7fac3f.png) + +②:运行项目(执行启动指令) java -jar <打包文件名> + +java –jar springboot.jar + +注意事项: +jar支持命令行启动需要依赖maven插件支持,请确认打包时是否具有SpringBoot对应的maven插件 + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/bbd941c93bed424e88de917ee0f8f406.png) + +地址栏输入 cmd 回车 +![在这里插入图片描述](https://img-blog.csdnimg.cn/db84686fb76149ce85ed43d38a91b783.png) + +执行 java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar +![在这里插入图片描述](https://img-blog.csdnimg.cn/e2c9fa1457f44b62affb67d232ae9bf4.png) + +打包优化:跳过 test 生命周期 +![在这里插入图片描述](https://img-blog.csdnimg.cn/3ab5f5b1493346c6876ae477f1930402.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/1c91e19c0c1e4e57b8a313c93141249d.png) + +### 8.3、打包插件 + +如果没有配制spring boot 打包插件可能遇到下面的问题: +![在这里插入图片描述](https://img-blog.csdnimg.cn/7e7f2f26252240a5b0997570a230b3f1.png) + +使用SpringBoot提供的maven插件可以将工程打包成可执行jar包 + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +可执行jar包目录 +![在这里插入图片描述](https://img-blog.csdnimg.cn/ab51e088e01643a384a434f8961dde04.png) + +ar包描述文件(MANIFEST.MF) + +普通工程 + +Manifest-Version: 1.0 +Implementation-Title: springboot_08_ssmp +Implementation-Version: 0.0.1-SNAPSHOT +Build-Jdk-Spec: 1.8 +Created-By: Maven Jar Plugin 3.2.0 + +基于spring-boot-maven-plugin打包的工程 + +Manifest-Version: 1.0 +Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx +Implementation-Title: springboot_08_ssmp +Implementation-Version: 0.0.1-SNAPSHOT +Spring-Boot-Layers-Index: BOOT-INF/layers.idx +Start-Class: com.example.SSMPApplication 启动类 +Spring-Boot-Classes: BOOT-INF/classes/ +Spring-Boot-Lib: BOOT-INF/lib/ +Build-Jdk-Spec: 1.8 +Spring-Boot-Version: 2.5.6 +Created-By: Maven Jar Plugin 3.2.0 +Main-Class: org.springframework.boot.loader.JarLauncher jar启动器 + +命令行启动常见问题及解决方案 + +* Windonws端口被占用 + +# 查询端口 +netstat -ano +# 查询指定端口 +netstat -ano |findstr "端口号" +# 根据进程PID查询进程名称 +tasklist |findstr "进程PID号" +# 根据PID杀死任务 +taskkill /F /PID "进程PID号" +# 根据进程名称杀死任务 +taskkill -f -t -im "进程名称" +### 8.4、Boot工程快速启动 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/a23a18d50a0246b89bf02a2ffeb549e2.png) + +* 基于Linux(CenterOS7) + +* 安装JDK,且版本不低于打包时使用的JDK版本 + + * 可以使用 yum 安装 +* 安装 MySQL + + * 可以参考: https://blog.csdn.net/qq_42324086/article/details/120579197 +* 安装包保存在/usr/local/自定义目录中或$HOME下 + +* 其他操作参照Windows版进行 + +**启动成功无法访问** + +添加 80 端口 + +* 添加 端口 + +firewall-cmd --zone=public --permanent --add-port=80/tcp + +重启 + +systemctl restart firewalld + +后台启动命令 + +nohup java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar > server.log 2>&1 & + +停止服务 + +* ps -ef | grep “java -jar” +* kill -9 PID +* cat server.log (查看日志) + +[root@cjbCentos01 app]# ps -ef | grep "java -jar" +UID PID PPID C STIME TTY TIME CMD +root 6848 6021 7 14:45 pts/2 00:00:19 java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar +root 6919 6021 0 14:49 pts/2 00:00:00 grep --color=auto java -jar +[root@cjbCentos01 app]# kill -9 6848 +[root@cjbCentos01 app]# ps -ef | grep "java -jar" +root 7016 6021 0 14:52 pts/2 00:00:00 grep --color=auto java -jar +[1]+ 已杀死 nohup java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar > server.log 2>&1 +[root@cjbCentos01 app]# +### 8.6、临时属性 + +#### 8.6.1、临时属性 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/5b127fc48b0a44639d06f08bc0bd977c.png) + +* 带属性数启动SpringBoot + +java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar --server.port=8080 + +* 携带多个属性启动SpringBoot,属性间使用空格分隔 + +属性加载优先顺序 + +1. 参看 https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config + ![在这里插入图片描述](https://img-blog.csdnimg.cn/945e0efcbcad4926a418bdad28e1be08.png) + +#### 8.6.2、开发环境 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/29a1d15b010747c0977800bea5d0e007.png) + +* 带属性启动SpringBoot程序,为程序添加运行属性 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/13411376a16645bf8bae8fbcf6fc4964.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/57e571fe9ce641bba055af96dce44e5d.png) + +在启动类中 main 可以通过 System.out.println(Arrays.toString(args)); 查看配制的属性 + +通过编程形式带参数启动SpringBoot程序,为程序添加运行参数 + +public static void main(String[] args) { + String[] arg = new String[1]; + arg[0] = "--server.port=8080"; + SpringApplication.run(SSMPApplication.class, arg); +} + +不携带参数启动SpringBoot程序 + +public static void main(String[] args) { + //可以在启动boot程序时断开读取外部临时配置对应的入口,也就是去掉读取外部参数的形参 + SpringApplication.run(SSMPApplication.class); +} +### 8.7、配置环境 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/205956af5f974a54a1e4c10015dae30d.png) + +#### 8.7.1、配置文件分类 + +* SpringBoot中4级配置文件 + + * 1级:file :config/application.yml 【最高】 + + * 2级:file :application.yml + + * 3级:classpath:config/application.yml + + * 4级:classpath:application.yml 【最低】 + +* 作用: + + * 1级与2级留做系统打包后设置通用属性,1级常用于运维经理进行线上整体项目部署方案调控 + + * 3级与4级用于系统开发阶段设置通用属性,3级常用于项目经理进行整体项目属性调控 + +#### 8.7.2、自定义配置文件 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/de7f3627fec54dd2852e44890f192b1d.png) + +通过启动参数加载配置文件(无需书写配置文件扩展名) --spring.config.name=eban +![在这里插入图片描述](https://img-blog.csdnimg.cn/70cebcbbd6c84a8eac3f9e997dca9a38.png) + +properties与yml文件格式均支持 + +* 通过启动参数加载指定文件路径下的配置文件 --spring.config.location=classpath:/ebank.yml + ![在这里插入图片描述](https://img-blog.csdnimg.cn/135c2375c371451e9bd86eee59f64668.png) + +properties与yml文件格式均支持 + +* 通过启动参数加载指定文件路径下的配置文件时可以加载多个配置,后面的会覆盖前面的 + +--spring.config.location=classpath:/ebank.yml,classpath:/ebank-server.yml + +![在这里插入图片描述](https://img-blog.csdnimg.cn/dc8e74a2c8c74d379f0869260493e458.png) + +注意事项: +多配置文件常用于将配置进行分类,进行独立管理,或将可选配置单独制作便于上线更新维护 + +自定义配置文件——重要说明 + +* 单服务器项目:使用自定义配置文件需求较低 + +* 多服务器项目:使用自定义配置文件需求较高,将所有配置放置在一个目录中,统一管理 + +* 基于SpringCloud技术,所有的服务器将不再设置配置文件,而是通过配置中心进行设定,动态加载配置信息 + +#### 8.7.3、多环境开发(yaml) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/6aa91cdd68fd4e74aefce1746f502c5c.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/a286e356b075446e852abbaeea3b02bd.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/f331e0b270dd4a23afe009c231694d9e.png) + +#应用环境 +#公共配制 +spring: + profiles: + active: dev + +#设置环境 +#开发环境 +--- +spring: + config: + activate: + on-profile: dev +server: + port: 81 + +#生产环境 +--- +spring: + profiles: pro +server: + port: 80 + +#测试环境 +--- +spring: + profiles: test +server: + port: 82 +#### 8.7.4、多环境开发文件(yaml) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/31e2fdc7804242488e594f9a04352d29.png) + +多环境开发(YAML版)多配置文件格式 +主启动配置文件application.yml + +#应用环境 +#公共配制 +spring: + profiles: + active: test + +环境分类配置文件application-pro.yml + +server: + port: 81 + +环境分类配置文件application-dev.yml + +server: + port: 82 + +环境分类配置文件application-test.yml + +server: + port: 83 +#### 8.7.5、多环境分组管理 + +* 根据功能对配置文件中的信息进行拆分,并制作成独立的配置文件,命名规则如下 + + * application-devDB.yml + + * application-devRedis.yml + + * application-devMVC.yml + +* 使用include属性在激活指定环境的情况下,同时对多个环境进行加载使其生效,多个环境间使用逗号分隔 + + spring: + profiles: + active: dev + include: devDB,devMVC + + 注意事项: + **当主环境dev与其他环境有相同属性时,主环境属性生效;其他环境中有相同属性时,最后加载的环境属性生效** + + The following profiles are active: devDB,devMVC,dev +* 从Spring2.4版开始使用group属性替代include属性,降低了配置书写量 + +* 使用**group**属性定义多种主环境与子环境的包含关系 + +spring: + profiles: + active: dev + group: + "dev": devDB,devMVC + "pro": proDB,proMVC + "test": testDB,testRedis,testMVC + +注意事项: +**使用group属性,会覆盖 主环境dev (active) 的内容,最后加载的环境属性生效** + +The following profiles are active: dev,devDB,devMVC +#### 8.7.6、多环境开发控制 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/7cac87f3998c403db8301a601178c9e8.png) +Maven与SpringBoot多环境兼容 +①:Maven中设置多环境属性 + + + + + dev_env + + dev + + + true + + + + pro_env + + pro + + + + + test_env + + test + + + + +②:SpringBoot中引用Maven属性 + +spring: + profiles: + active: @profile.active@ + group: + "dev": devDB,devMVC + "pro": proDB,proMVC + +![在这里插入图片描述](https://img-blog.csdnimg.cn/706db57f170d4b42ab1c71e4fd02888e.png) + +③:执行Maven打包指令,并在生成的boot打包文件.jar文件中查看对应信息 +问题:**修改pom.xml 文件后,启动没有生效 手动 compile 即可** +![在这里插入图片描述](https://img-blog.csdnimg.cn/41ea64df2c1b4e5393eaabfb1410ea4c.png) + +或者 设置 IDEA进行自动编译 +![在这里插入图片描述](https://img-blog.csdnimg.cn/281978a65c304f43a811da62b26ac626.png) + +## 9、日志 + +### 9.1、日志基础操作 + +日志(log)作用 + +1. 编程期调试代码 +2. 运营期记录信息 + * 记录日常运营重要信息(峰值流量、平均响应时长……) + * 记录应用报错信息(错误堆栈) + * 记录运维过程数据(扩容、宕机、报警……) + +代码中使用日志工具记录日志 + +* 先引入 Lombok 工具类 + + + + org.projectlombok + lombok + + + ①:添加日志记录操作 + + @RestController + @RequestMapping("/books") + public class BookController { + private static final Logger log = LoggerFactory.getLogger(BookController.class); + + @GetMapping + public String getById() { + System.out.println("springboot is running..."); + log.debug("debug ..."); + log.info("info ..."); + log.warn("warn ..."); + log.error("error ..."); + return "springboot is running..."; + } + } +* 日志级别 + +TRACE:运行堆栈信息,使用率低 +DEBUG:程序员调试代码使用 +INFO:记录运维过程数据 +WARN:记录运维过程报警数据 +ERROR:记录错误堆栈信息 +FATAL:灾难信息,合并计入ERRO + +②:设置日志输出级别 + +# 开启 debug 模式,输出调试信息,常用于检查系统运行状况 +debug: true +# 设置日志级别, root 表示根节点,即整体应用日志级别 +logging: + level: + root: debug + +③:设置日志组,控制指定包对应的日志输出级别,也可以直接控制指定包对应的日志输出级别 + +logging: + # 设置分组 + group: + # 自定义组名,设置当前组中所包含的包 + ebank: com.example.controller,com.example.service,com.example.dao + iservice: com.alibaba + level: + root: info + # 设置某个包的日志级别 +# com.example.controller: debug + # 为对应组设置日志级别 + ebank: warn +### 9.2、快速创建日志对象 + +* 使用lombok提供的注解@Slf4j简化开发,减少日志对象的声明操作 + + @Slf4j + //Rest模式 + @RestController + @RequestMapping("/books") + public class BookController { + + @GetMapping + public String getById(){ + System.out.println("springboot is running...2"); + + log.debug("debug..."); + log.info("info..."); + log.warn("warn..."); + log.error("error..."); + + return "springboot is running...2"; + } + + } + +### 9.3、日志输出格式控制 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/7b1ae90e16854890af7661f94570bd5d.png) + +* PID:进程ID,用于表明当前操作所处的进程,当多服务同时记录日志时,该值可用于协助程序员调试程序 +* 所属类/接口名:当前显示信息为SpringBoot重写后的信息,名称过长时,简化包名书写为首字母,甚至直接删除 +* 设置日志输出格式 + +logging: + pattern: + console: "%d - %m%n" + +%d:日期 +%m:消息 +%n:换行 +![在这里插入图片描述](https://img-blog.csdnimg.cn/91223acd27344b03ace835d17d35dd95.png) + +logging: +pattern: +# console: "%d - %m%n" +console: "%d %clr(%5p) --- [%16t] %clr(%-40.40c){cyan} : %m %n" +### 9.4、文件记录日志 + +* 设置日志文件 + +logging: + file: + name: server.log + +* 日志文件详细配置 + +logging: + file: + name: server.log + logback: + rollingpolicy: + max-file-size: 4KB + file-name-pattern: server.%d{yyyy-MM-dd}.%i.log + +![在这里插入图片描述](https://img-blog.csdnimg.cn/756bf00cf4f44ea286b6fac6f31ccf44.png) + +## 10、热部署 + + + org.springframework.boot + spring-boot-devtools + true + spring: + # 热部署范围配置 + devtools: + restart: + # 设置不参与热部署的文件和文件夹(即修改后不重启) + exclude: static/**,public/**,config/application.yml + #是否可用 + enabled: false + +如果配置文件比较多的时候找热部署对应配置比较麻烦,可以在`springboot`启动类的main方法中设置,此处设置的优先级将比配置文件高,一定会生效。 + +System.setProperty("spring.devtools.restart.enabled", "false"); +## 11、属性绑定 + +1. 先在要配置的类上面加@Component注解将该类交由spring容器管理; +2. `@ConfigurationProperties(prefix="xxx")`,xxx跟application.yml配置文件中的属性对应; +3. 如果多个配置类想统一管理也可以通过`@EnableConfigurationProperties({xxx.class, yyy.class})`的方式完成配置,不过该注解会与@Component配置发生冲突,二选一即可; +4. 第三方类对象想通过配置进行属性注入,可以通过创建一个方法,在方法体上加@Bean和`@ConfigurationProperties(prefix="xxx")`注解,然后方法返回这个第三方对象的方式。 +5. 使用`@ConfigurationProperties(prefix="xxx")`注解后idea工具会报一个警告Spring Boot Configuration Annotation Processor not configured + +@ConfigurationProperties(prefix="xxx") +@Data +public class ServerConfig { + private String inAddress; + private int port; + private long timeout; +} + +`@ConfigurationProperties`绑定属性支持属性名宽松绑定,又叫松散绑定。 + +比如要将`ServerConfig.class`作为配置类,并通过配置文件`application.yml`绑定属性 + +ServerConfig.class serverConfig: + # ipAddress: 192.168.0.1 # 驼峰模式 + # ipaddress: 192.168.0.1 + # IPADDRESS: 192.168.0.1 + ip-address: 192.168.0.1 # 主流配置方式,烤肉串模式 + # ip_address: 192.168.0.1 # 下划线模式 + # IP_ADDRESS: 192.168.0.1 # 常量模式 + # ip_Add_rEss: 192.168.0.1 + # ipaddress: 192.168.0.1 + port: 8888 + timeout: -1 + +以ipAddress属性为例,上面的多种配置方式皆可生效,这就是松散绑定。而@Value不支持松散绑定,必须一一对应。 + +`@ConfigurationProperties(prefix="serverconfig")`中的prefix的值为serverconfig或者server-config,如果是serverConfig就会报错,这与松散绑定的前缀命名规范有关:仅能使用纯小写字母、数字、中划线作为合法的字符 + +## 12、常用计量单位应用 + +//@Component +@ConfigurationProperties(prefix = "server-config") +@Data +public class ServerConfig { + private String ipAddress; + private int port; + @DurationUnit(ChronoUnit.MINUTES) + private Duration timeout; + + @DataSizeUnit(DataUnit.MEGABYTES) + private DataSize dataSize; +} + +引入`Bean`属性校验框架的步骤: + +1. 在`pom.xml`中添加`JSR303`规范和`hibernate`校验框架的依赖: + + + + javax.validation + validation-api + + + + org.hibernate.validator + hibernate-validator + + +1. 在要校验的类上加`@Validated`注解 + +2. 设置具体的校验规则,如:`@Max(value=8888, message="最大值不能超过8888")` + +@ConfigurationProperties(prefix = "server-config") +@Data +// 2.开启对当前bean的属性注入校验 +@Validated +public class ServerConfig { + private String ipAddress; + // 设置具体的规则 + @Max(value = 8888, message = "最大值不能超过8888") + @Min(value = 1000, message = "最小值不能低于1000") + private int port; + @DurationUnit(ChronoUnit.MINUTES) + private Duration timeout; + + @DataSizeUnit(DataUnit.MEGABYTES) + private DataSize dataSize; +} + +进制转换中的一些问题: + +如`application.yml`文件中对数据库有如下配置: + +datasource: + driverClassName: com.mysql.cj.jdbc.Driver123 + # 不加引号读取的时候默认解析为了8进制数,转成十进制就是87 + # 所以想让这里正确识别,需要加上引号 + # password: 0127 + password: "0127" +## 13、测试类 + +### 13.1、加载专用属性 + +@SpringBootTest注解中可以设置properties和args属性,这里的args属性的作用跟idea工具中自带的程序参数类似,只不过这里的配置是源码级别的,会随着源码的移动而跟随,而idea中的程序参数的配置会丢失。并且这里的args属性的配置的作用范围比较小,仅在当前测试类生效。 + +application.yml + +test: + prop: testValue // properties属性可以为当前测试用例添加临时的属性配置 +//@SpringBootTest(properties = {"test.prop=testValue1"}) +// args属性可以为当前测试用例添加临时的命令行参数 +//@SpringBootTest(args = {"--test.prop=testValue2"}) +// 优先级排序: args > properties > 配置文件 +@SpringBootTest(args = {"--test.prop=testValue2"}, properties = {"test.prop=testValue1"}) +class PropertiesAndArgsTest { + @Value("${test.prop}") + private String prop; + @Test + public void testProperties() { + System.out.println("prop = " + prop); + } +} +### 13.2、加载专用类 + +某些测试类中需要用到第三方的类,而其他测试类则不需要用到,这里可以在类上加载`@Import({xxx.class, yyy.class})` + +@RestController +@RequestMapping("/books") +public class BookController { + /*@GetMapping("/{id}") + public String getById(@PathVariable int id) { + System.out.println("id = " + id); + return "getById..."; + }*/ + + @GetMapping("/{id}") + public Book getById(@PathVariable int id) { + System.out.println("id = " + id); + Book book = new Book(); + book.setId(5); + book.setName("springboot"); + book.setType("springboot"); + book.setDescription("springboot"); + return book; + } +} + +相应测试类 + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +// 开启虚拟mvc调用 +@AutoConfigureMockMvc +public class WebTest { + @Test + public void testRandomPort() { + } + + @Test + public void testWeb(@Autowired MockMvc mvc) throws Exception { + // 创建虚拟请求,当前访问 /books + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/5"); + mvc.perform(builder); + } + + @Test + public void testStatus(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books1/6"); + ResultActions action = mvc.perform(builder); + // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过 + // 定义本次调用的预期值 + StatusResultMatchers srm = MockMvcResultMatchers.status(); + // 预计本次调用成功的状态码:200 + ResultMatcher ok = srm.isOk(); + // 添加预计值到本次调用过程中进行匹配 + action.andExpect(ok); + } + + @Test + public void testBody(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/6"); + ResultActions action = mvc.perform(builder); + // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过 + // 定义本次调用的预期值 + ContentResultMatchers crm = MockMvcResultMatchers.content(); + // 预计本次调用成功的状态码:200 + ResultMatcher rm = crm.string("getById..."); + // 添加预计值到本次调用过程中进行匹配 + action.andExpect(rm); + } + + @Test + public void testJson(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/7"); + ResultActions action = mvc.perform(builder); + // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过 + // 定义本次调用的预期值 + ContentResultMatchers jsonMatcher = MockMvcResultMatchers.content(); + ResultMatcher rm = jsonMatcher.json("{\"id\":5,\"name\":\"springboot\",\"type\":\"springboot\",\"description\":\"springboot1\"}"); + action.andExpect(rm); + } + + @Test + public void testContentType(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/7"); + ResultActions action = mvc.perform(builder); + // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过 + // 定义本次调用的预期值 + HeaderResultMatchers hrm = MockMvcResultMatchers.header(); + ResultMatcher rm = hrm.string("Content-Type", "application/json"); + action.andExpect(rm); + } + + @Test + // 完整测试 + public void testGetById(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/8"); + ResultActions action = mvc.perform(builder); + + // 1、比较状态码 + StatusResultMatchers statusResultMatchers = MockMvcResultMatchers.status(); + ResultMatcher statusResultMatcher = statusResultMatchers.isOk(); + action.andExpect(statusResultMatcher); + + // 2、比较返回值类型 + HeaderResultMatchers headerResultMatchers = MockMvcResultMatchers.header(); + ResultMatcher headerResultMatcher = headerResultMatchers.string("Content-Type", "application/json"); + action.andExpect(headerResultMatcher); + + /// 3、比较json返回值 + ContentResultMatchers contentResultMatchers = MockMvcResultMatchers.content(); + ResultMatcher jsonResultMatcher = contentResultMatchers.json("{\"id\":5,\"name\":\"springboot\",\"type\":\"springboot\",\"description\":\"springboot\"}"); + action.andExpect(jsonResultMatcher); + } +} +### 13.3、业务层测试事务回滚 + +* 为测试用例添加事务,SpringBoot会对测试用例对应的事务提交操作进行回滚 + + @SpringBootTest + @Transactional public class DaoTest { + @Autowired + private BookService bookService; } +* l如果想在测试用例中提交事务,可以通过@Rollback注解设置 + + @SpringBootTest + @Transactional + @Rollback(false) + public class DaoTest { + } + +### 13.4、测试用例数据设定 + +* 测试用例数据通常采用随机值进行测试,使用SpringBoot提供的随机数为其赋值 + +testcast: + book: + id: ${random.int} # 随机整数 + id2: ${random.int(10)} # 10以内随机数 + type: ${random.int(10,20)} # 10到20随机数 + uuid: ${random.uuid} # 随机uuid + name: ${random.value} # 随机字符串,MD5字符串,32位 + publishTime: ${random.long} # 随机整数(long范围) u${random.int}表示随机整数 + +u${random.int(10)}表示10以内的随机数 + +u${random.int(10,20)}表示10到20的随机数 + +u其中()可以是任意字符,例如[],!!均可 +## 14、数据层解决方案 + +### 14.1、SQL + +现有数据层解决方案技术选型 + +Druid + MyBatis-Plus + MySQL + +* 数据源:DruidDataSource +* 持久化技术:MyBatis-Plus / MyBatis +* 数据库:MySQL + +格式一: + +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC + username: root + password: root + +格式二: + +spring: + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC + username: root + password: root + +* SpringBoot提供了3种内嵌的数据源对象供开发者选择 + * HikariCP + * Tomcat提供DataSource + * Commons DBCP + +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC + username: root + password: root spring: + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC + username: root + password: root + +* SpringBoot提供了3种内嵌的数据源对象供开发者选择 + + * HikariCP:默认内置数据源对象 + + * Tomcat提供DataSource:HikariCP不可用的情况下,且在web环境中,将使用tomcat服务器配置的数据源对象 + + * Commons DBCP:Hikari不可用,tomcat数据源也不可用,将使用dbcp数据源 + +* 通用配置无法设置具体的数据源配置信息,仅提供基本的连接相关配置,如需配置,在下一级配置中设置具体设定 + +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/ssm_db + username: root + password: root + hikari: + maximum-pool-size: 50 + +* 内置持久化解决方案——JdbcTemplate + +@SpringBootTest +class Springboot15SqlApplicationTests { + @Autowired + private JdbcTemplate jdbcTemplate; + @Test + void testJdbc(){ + String sql = "select * from tbl_book where id = 1"; + List query = jdbcTemplate.query(sql, new RowMapper() { + @Override + public Book mapRow(ResultSet rs, int rowNum) throws SQLException { + Book temp = new Book(); + temp.setId(rs.getInt("id")); + temp.setName(rs.getString("name")); + temp.setType(rs.getString("type")); + temp.setDescription(rs.getString("description")); + return temp; + } + }); + System.out.println(query); + } +} + org.springframework.boot + spring-boot-starter-jdbc + spring: + jdbc: + template: + query-timeout: -1 # 查询超时时间 + max-rows: 500 # 最大行数 + fetch-size: -1 # 缓存行数 + +* SpringBoot提供了3种内嵌数据库供开发者选择,提高开发测试效率 + + * H2 + * HSQL + * Derby +* 导入H2相关坐标 + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + +* 设置当前项目为web工程,并配置H2管理控制台参数 + +server: + port: 80 +spring: + h2: + console: + path: /h2 + enabled: true + +访问用户名sa,默认密码123456 + +操作数据库(创建表) + +create table tbl_book (id int,name varchar,type varchar,description varchar) #设置访问数据源 +server: + port: 80 +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:~/test + username: sa + password: 123456 +h2: + console: + path: /h2 + enabled: true + +H2数据库控制台仅用于开发阶段,线上项目请务必关闭控制台功能 + +server: + port: 80 +spring: + h2: + console: + path: /h2 + enabled: false + +SpringBoot可以根据url地址自动识别数据库种类,在保障驱动类存在的情况下,可以省略配置 + +server: + port: 80 +spring: + datasource: +# driver-class-name: org.h2.Driver + url: jdbc:h2:~/test + username: sa + password: 123456 + h2: + console: + path: /h2 + enabled: true + +![在这里插入图片描述](https://img-blog.csdnimg.cn/cb7f9780f7884e9bb8b993ee2363dd6e.png) + +### 14.2、MongoDB + +MongoDB是一个开源、高性能、无模式的文档型数据库。NoSQL数据库产品中的一种,是最像关系型数据库的非关系型数据库 +![在这里插入图片描述](https://img-blog.csdnimg.cn/bc3313adaf3a471da66e8a9a062f552b.png) + +#### 14.2.1、MongoDB的使用 + +* Windows版Mongo下载 + +https://www.mongodb.com/try/download + +* Windows版Mongo安装 + +解压缩后设置数据目录 + +* Windows版Mongo启动 + +服务端启动 + +在bin目录下 + +`mongod --dbpath=..\data\db` + +客户端启动 + +`mongo --host=127.0.0.1 --port=27017` + +#### 14.2.2、MongoDB可视化客户端 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/48f4a3f143be40cbaf09e018df91dc5e.png) + +* 新增 + + `db.集合名称.insert/save/insertOne(文档)` + +* 修改 + + `db.集合名称.remove(条件)` + +* 删除 + + `db.集合名称.update(条件,{操作种类:{文档}})` + ![在这里插入图片描述](https://img-blog.csdnimg.cn/349eb759436043b88ec23d8541c41340.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/f5140aed83ee407db4a4652f81fe7d9a.png) + +#### 14.2.3、Springboot集成MongoDB + +导入MongoDB驱动 + + + org.springframework.boot + spring-boot-starter-data-mongodb + + +配置客户端 + +spring: + data: + mongodb: + uri: mongodb://localhost/itheima + +客户端读写MongoDB + +@Test +void testSave(@Autowired MongoTemplate mongoTemplate){ + Book book = new Book(); + book.setId(1); + book.setType("springboot"); + book.setName("springboot"); + book.setDescription("springboot"); + mongoTemplate.save(book); +} +@Test +void testFind(@Autowired MongoTemplate mongoTemplate){ + List all = mongoTemplate.findAll(Book.class); + System.out.println(all); +} +### 14.3、ElasticSearch(ES) + +Elasticsearch是一个分布式全文搜索引擎 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/a5bb0c141ef24ed8bfedf270201dae1e.png) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/a17cd8d3332d42c4a1161c8541f1fbbb.png) + +#### 14.3.1、ES下载 + +* Windows版ES下载 + +[https://](https://www.elastic.co/cn/downloads/elasticsearch)[www.elastic.co/cn/downloads/elasticsearch](https://www.elastic.co/cn/downloads/elasticsearch) + +* Windows版ES安装与启动 + +运行:`elasticsearch.bat` + +#### 14.3.2、ES索引、分词器 + +* 创建/查询/删除索引 + +put:`http://localhost:9200/books` + +get:`http://localhost:9200/books` + +delete:`http://localhost:9200/books` + +* IK分词器 + + 下载:https://github.com/medcl/elasticsearch-analysis-ik/releases + +* 创建索引并指定规则 + +{ + "mappings":{ + "properties":{ + "id":{ + "type":"keyword" + }, + "name":{ + "type":"text", "analyzer":"ik_max_word", "copy_to":"all" + }, + "type":{ + "type":"keyword" + }, + "description":{ + "type":"text", "analyzer":"ik_max_word", "copy_to":"all" + }, + "all":{ + "type":"text", "analyzer":"ik_max_word" + } + } + } +} +#### 14.3.3、文档操作(增删改查) + +* 创建文档 + + post:`http://localhost:9200/books/_doc`(使用系统生成的id) + + post:`http://localhost:9200/books/_create/1`(使用指定id) + + post:`http://localhost:9200/books/_doc/1`(使用指定id,不存在创建,存在更新,版本递增) + + { + "name":"springboot", + "type":"springboot", + "description":"springboot" + } +* 查询文档 + + get:`http://localhost:9200/books/_doc/1` + + get:`http://localhost:9200/books/_search` + +* 条件查询 + + get:`http://localhost:9200/books/_search?q=name:springboot` + +* 删除文档 + + delete:`http://localhost:9200/books/_doc/1` + +* 修改文档(全量修改) + + put:`http://localhost:9200/books/_doc/1` + + { + "name":"springboot", + "type":"springboot", + "description":"springboot" + } +* 修改文档(部分修改) + + post:`http://localhost:9200/books/_update/1` + + { + "doc":{ + "name":"springboot" + } + } + +#### 14.3.4、Springboot集成ES + +* 导入坐标 + + + org.springframework.boot + spring-boot-starter-data-elasticsearch + +* 配置 + + spring: + elasticsearch: + rest: + uris: http://localhost:9200 +* 客户端 + + @SpringBootTest + class Springboot18EsApplicationTests { + @Autowired + private ElasticsearchRestTemplate template; + } +* SpringBoot平台并没有跟随ES的更新速度进行同步更新,ES提供了High Level Client操作ES + + 导入坐标 + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + + + 无需配置 + +* 客户端 + + @Test + void test() throws IOException { + HttpHost host = HttpHost.create("http://localhost:9200"); + RestClientBuilder builder = RestClient.builder(host); + RestHighLevelClient client = new RestHighLevelClient(builder); + //客户端操作 + CreateIndexRequest request = new CreateIndexRequest("books"); + //获取操作索引的客户端对象,调用创建索引操作 + client.indices().create(request, RequestOptions.DEFAULT); + //关闭客户端 + client.close(); + } +* 客户端改进 + + @SpringBootTest + class Springboot18EsApplicationTests { + @Autowired + private BookDao bookDao; + @Autowired + RestHighLevelClient client; + @BeforeEach + void setUp() { + this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://localhost:9200"))); + } + @AfterEach + void tearDown() throws IOException { + this.client.close(); + } + @Test + void test() throws IOException { + //客户端操作 + CreateIndexRequest request = new CreateIndexRequest("books"); + //获取操作索引的客户端对象,调用创建索引操作 + client.indices().create(request, RequestOptions.DEFAULT); + } + } + +#### 14.3.5、索引 + +* 创建索引 + + //创建索引 + @Test + void testCreateIndexByIK() throws IOException { + HttpHost host = HttpHost.create("http://localhost:9200"); + RestClientBuilder builder = RestClient.builder(host); + RestHighLevelClient client = new RestHighLevelClient(builder); + //客户端操作 + CreateIndexRequest request = new CreateIndexRequest("books"); + //设置要执行操作 + String json = ""; + //设置请求参数,参数类型json数据 + request.source(json,XContentType.JSON); + //获取操作索引的客户端对象,调用创建索引操作 + client.indices().create(request, RequestOptions.DEFAULT); + //关闭客户端 + client.close(); + } String json = "{\n" + + " \"mappings\":{\n" + + " \"properties\":{\n" + + " \"id\":{\n" + + " \"type\":\"keyword\"\n" + + " },\n" + + " \"name\":{\n" + + " \"type\":\"text\",\n" + + " \"analyzer\":\"ik_max_word\",\n" + + " \"copy_to\":\"all\"\n" + + " },\n" + + " \"type\":{\n" + + " \"type\":\"keyword\"\n" + + " },\n" + + " \"description\":{\n" + + " \"type\":\"text\",\n" + + " \"analyzer\":\"ik_max_word\",\n" + + " \"copy_to\":\"all\"\n" + + " },\n" + + " \"all\":{\n" + + " \"type\":\"text\",\n" + + " \"analyzer\":\"ik_max_word\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; +* 添加文档 + + //添加文档 + @Test + void testCreateDoc() throws IOException { + Book book = bookDao.selectById(1); + IndexRequest request = new IndexRequest("books").id(book.getId().toString()); + String json = JSON.toJSONString(book); + request.source(json,XContentType.JSON); + client.index(request, RequestOptions.DEFAULT); + } +* 批量添加文档 + + //批量添加文档 + @Test + void testCreateDocAll() throws IOException { + List bookList = bookDao.selectList(null); + BulkRequest bulk = new BulkRequest(); + for (Book book : bookList) { + IndexRequest request = new IndexRequest("books").id(book.getId().toString()); + String json = JSON.toJSONString(book); + request.source(json,XContentType.JSON); + bulk.add(request); + } + client.bulk(bulk,RequestOptions.DEFAULT); + } +* 按id查询文档 + + @Test + void testGet() throws IOException { + GetRequest request = new GetRequest("books","1"); + GetResponse response = client.get(request, RequestOptions.DEFAULT); + String json = response.getSourceAsString(); + System.out.println(json); + } +* 按条件查询文档 + +@Test +void testSearch() throws IOException { + SearchRequest request = new SearchRequest("books"); + SearchSourceBuilder builder = new SearchSourceBuilder(); + builder.query(QueryBuilders.termQuery("all",“java")); + request.source(builder); + SearchResponse response = client.search(request, RequestOptions.DEFAULT); + SearchHits hits = response.getHits(); + for (SearchHit hit : hits) { + String source = hit.getSourceAsString(); + Book book = JSON.parseObject(source, Book.class); + System.out.println(book); + } +} +## 15、缓存 + +### 15.1、缓存简介 + +缓存是一种介于数据永久存储介质与数据应用之间的数据临时存储介质 +![在这里插入图片描述](https://img-blog.csdnimg.cn/be3b3d385ea7423cb66544139b28591d.png) + +* 缓存是一种介于数据永久存储介质与数据应用之间的数据临时存储介质 + +* 使用缓存可以有效的减少低速数据读取过程的次数(例如磁盘IO),提高系统性能 + +* 缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/340efee416e64d83957abf3c421c3575.png) + +* SpringBoot提供了缓存技术,方便缓存使用 + +### 15.2、缓存使用 + +* 启用缓存 + +* 设置进入缓存的数据 + +* 设置读取缓存的数据 + +* 导入缓存技术对应的starter + + + org.springframework.boot + spring-boot-starter-cache + +* 启用缓存 + + @SpringBootApplication + @EnableCaching + public class Springboot19CacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot19CacheApplication.class, args); + } + } +* 设置当前操作的结果数据进入缓存 + +@Cacheable(value="cacheSpace",key="#id") +public Book getById(Integer id) { + return bookDao.selectById(id); +} +### 15.3、其他缓存 + +* SpringBoot提供的缓存技术除了提供默认的缓存方案,还可以对其他缓存技术进行整合,统一接口,方便缓存技术的开发与管理 + * Generic + * JCache + * **Ehcache** + * Hazelcast + * Infinispan + * Couchbase + * **Redis** + * Caffeine + * Simple(默认) + * **memcached** + * jetcache(阿里) + +### 15.4、缓存使用案例——手机验证码 + +* 需求 + + * 输入手机号获取验证码,组织文档以短信形式发送给用户(页面模拟) + + * 输入手机号和验证码验证结果 + +* 需求分析 + + * 提供controller,传入手机号,业务层通过手机号计算出独有的6位验证码数据,存入缓存后返回此数据 + + * 提供controller,传入手机号与验证码,业务层通过手机号从缓存中读取验证码与输入验证码进行比对,返回比对结果 + +#### 15.4.1、Cache + +* 开启缓存 + + @SpringBootApplication + @EnableCaching + public class Springboot19CacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot19CacheApplication.class, args); + } + } +* 业务层接口 + + public interface SMSCodeService { + /** + * 传入手机号获取验证码,存入缓存 + * @param tele + * @return + */ + String sendCodeToSMS(String tele); + + /** + * 传入手机号与验证码,校验匹配是否成功 + * @param smsCode + * @return + */ + boolean checkCode(SMSCode smsCode); + } +* 业务层设置获取验证码操作,并存储缓存,手机号为key,验证码为value + + @Autowired + private CodeUtils codeUtils; + @CachePut(value = "smsCode",key="#tele") + public String sendCodeToSMS(String tele) { + String code = codeUtils.generator(tele); + return code; + } +* 业务层设置校验验证码操作,校验码通过缓存读取,返回校验结果 + + @Autowired + private CodeUtils codeUtils; + public boolean checkCode(SMSCode smsCode) { + //取出内存中的验证码与传递过来的验证码比对,如果相同,返回true + String code = smsCode.getCode(); + String cacheCode = codeUtils.get(smsCode.getTele()); + return code.equals(cacheCode); + } @Component + public class CodeUtils { + @Cacheable(value = "smsCode",key="#tele") + public String get(String tele){ + return null; + } + } + +#### 15.4.2、Ehcache + +* 加入Ehcache坐标(缓存供应商实现) + + + net.sf.ehcache + ehcache + +* 缓存设定为使用Ehcache + + spring: + cache: + type: ehcache + ehcache: + config: ehcache.xml +* 提供ehcache配置文件ehcache.xml + + + + + + + + + + + + + + + + +#### 15.4.3、Redis + +* 加入Redis坐标(缓存供应商实现) + + + org.springframework.boot + spring-boot-starter-data-redis + + +* 配置Redis服务器,缓存设定为使用Redis + + spring: + redis: + host: localhost + port: 6379 + cache: + type: redis +* 设置Redis相关配置 + + spring: + redis: + host: localhost + port: 6379 + cache: + type: redis + redis: + use-key-prefix: true # 是否使用前缀名(系统定义前缀名) + key-prefix: sms_ # 追加自定义前缀名 + time-to-live: 10s # 有效时长 + cache-null-values: false # 是否允许存储空值 + +#### 15.4.4、memcached + +* 下载memcached + +* 地址:https://www.runoob.com/memcached/window-install-memcached.html + +* 安装memcached + + * 使用管理员身份运行cmd指令 + + * 安装 + + `memcached.exe -d install` + +* 运行 + + * 启动服务 + + `memcached.exe -d start` + + * 定制服务 + + `memcached.exe -d stop` + +* memcached客户端选择 + + * Memcached Client for Java:最早期客户端,稳定可靠,用户群广 + * SpyMemcached:效率更高 + * Xmemcached:并发处理更好 +* SpringBoot未提供对memcached的整合,需要使用硬编码方式实现客户端初始化管理 + +* 加入Xmemcached坐标(缓存供应商实现) + + + com.googlecode.xmemcached + xmemcached + 2.4.7 + +* 配置memcached服务器必要属性 + + memcached: + # memcached服务器地址 + servers: localhost:11211 + # 连接池的数量 + poolSize: 10 + # 设置默认操作超时 + opTimeout: 3000 +* 创建读取属性配置信息类,加载配置 + + @Component + @ConfigurationProperties(prefix = "memcached") + @Data + public class XMemcachedProperties { + private String servers; + private Integer poolSize; + private Long opTimeout; + } +* 创建客户端配置类 + + @Configuration + public class XMemcachedConfig { + @Autowired + private XMemcachedProperties xMemcachedProperties; + @Bean + public MemcachedClient getMemcachedClinet() throws IOException { + MemcachedClientBuilder builder = new XMemcachedClientBuilder(xMemcachedProperties.getServers()); + MemcachedClient memcachedClient = builder.build(); + return memcachedClient; + } + } +* 配置memcached属性 + + @Service + public class SMSCodeServiceMemcacheImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + @Autowired + private MemcachedClient memcachedClient; + @Override + public String sendCodeToSMS(String tele) { + String code = this.codeUtils.generator(tele); + //将数据加入memcache + try { + memcachedClient.set(tele,0,code); // key,timeout,value + } catch (Exception e) { + e.printStackTrace(); + } + return code; + } + } @Service + public class SMSCodeServiceMemcacheImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + @Autowired + private MemcachedClient memcachedClient; + @Override + public boolean checkCode(CodeMsg codeMsg) { + String value = null; + try { + value = memcachedClient.get(codeMsg.getTele()).toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return codeMsg.getCode().equals(value); + } + } + +#### 15.4.5、jetcache + +* jetCache对SpringCache进行了封装,在原有功能基础上实现了多级缓存、缓存统计、自动刷新、异步调用、数据报表等功能 + +* jetCache设定了本地缓存与远程缓存的多级缓存解决方案 + + * 本地缓存(local) + + * LinkedHashMap + * Caffeine + * 远程缓存(remote) + + * Redis + * Tair +* 加入jetcache坐标 + + + com.alicp.jetcache + jetcache-starter-redis + 2.6.2 + +* 配置**远程**缓存必要属性 + + jetcache: + remote: + default: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 jetcache: + remote: + default: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 + sms: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 +* 配置**本地**缓存必要属性 + + jetcache: + local: + default: + type: linkedhashmap + keyConvertor: fastjson +* 配置范例 + + jetcache: + statIntervalMinutes: 15 + areaInCacheName: false + local: + default: + type: linkedhashmap + keyConvertor: fastjson + limit: 100 + remote: + default: + host: localhost + port: 6379 + type: redis + keyConvertor: fastjson + valueEncoder: java + valueDecoder: java + poolConfig: + minIdle: 5 + maxIdle: 20 + maxTotal: 50 +* 配置属性说明 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/bb8230091ee84b91828bf74555e8d64b.png) + +* 开启jetcache注解支持 + + @SpringBootApplication + @EnableCreateCacheAnnotation + public class Springboot19CacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot19CacheApplication.class, args); + } + } +* 声明缓存对象 + + @Service + public class SMSCodeServiceImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + @CreateCache(name = "smsCache", expire = 3600) + private Cache jetSMSCache; + } +* 操作缓存 + + @Service + public class SMSCodeServiceImpl implements SMSCodeService { + @Override + public String sendCodeToSMS(String tele) { + String code = this.codeUtils.generator(tele); + jetSMSCache.put(tele,code); + return code; + } + @Override + public boolean checkCode(CodeMsg codeMsg) { + String value = jetSMSCache.get(codeMsg.getTele()); + return codeMsg.getCode().equals(value); + } + } +* 启用方法注解 + + @SpringBootApplication + @EnableCreateCacheAnnotation + @EnableMethodCache(basePackages = "com.itheima") + public class Springboot20JetCacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot20JetCacheApplication.class, args); + } + } +* 使用方法注解操作缓存 + + @Service + public class BookServiceImpl implements BookService { + @Autowired + private BookDao bookDao; + @Cached(name = "smsCache_", key = "#id", expire = 3600) + @CacheRefresh(refresh = 10,timeUnit = TimeUnit.SECONDS) + public Book getById(Integer id) { + return bookDao.selectById(id); + } + } @Service + public class BookServiceImpl implements BookService { + + @CacheUpdate(name = "smsCache_", key = "#book.id", value = "#book") + public boolean update(Book book) { + return bookDao.updateById(book) > 0; + } + + @CacheInvalidate(name = "smsCache_", key = "#id") + public boolean delete(Integer id) { + return bookDao.deleteById(id) > 0; + } + } +* 缓存对象必须保障可序列化 + + @Data + public class Book implements Serializable { + } jetcache: + remote: + default: + type: redis + keyConvertor: fastjson + valueEncoder: java + valueDecoder: java +* 查看缓存统计报告 + + jetcache: + statIntervalMinutes: 15 + +#### 15.4.6、j2cache + +* j2cache是一个缓存整合框架,可以提供缓存的整合方案,使各种缓存搭配使用,自身不提供缓存功能 + +* 基于 ehcache + redis 进行整合 + +* 加入j2cache坐标,加入整合缓存的坐标 + + + net.oschina.j2cache + j2cache-spring-boot2-starter + 2.8.0-release + + + net.oschina.j2cache + j2cache-core + 2.8.4-release + + + net.sf.ehcache + ehcache + +* 配置使用j2cache(application.yml) + + j2cache: + config-location: j2cache.properties +* 配置一级缓存与二级缓存以及一级缓存数据到二级缓存的发送方式(j2cache.properties) + + # 配置1级缓存 + j2cache.L1.provider_class = ehcache + ehcache.configXml = ehcache.xml + + # 配置1级缓存数据到2级缓存的广播方式:可以使用redis提供的消息订阅模式,也可以使用jgroups多播实现 + j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy + + # 配置2级缓存 + j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider + j2cache.L2.config_section = redis + redis.hosts = localhost:6379 +* 设置使用缓存 + + @Service + public class SMSCodeServiceImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + @Autowired + private CacheChannel cacheChannel; + } @Service + public class SMSCodeServiceImpl implements SMSCodeService { + @Override + public String sendCodeToSMS(String tele) { + String code = codeUtils.generator(tele); + cacheChannel.set("sms",tele,code); + return code; + } + @Override + public boolean checkCode(SMSCode smsCode) { + String code = cacheChannel.get("sms",smsCode.getTele()).asString(); + return smsCode.getCode().equals(code); + } + } + +## 16、定时 + +任务 + +* 定时任务是企业级应用中的常见操作 + + * 年度报表 + * 缓存统计报告 + * … … +* 市面上流行的定时任务技术 + + * Quartz + * Spring Task + +### 16.1、SpringBoot整合Quartz + +* 相关概念 + + * 工作(Job):用于定义具体执行的工作 + * 工作明细(JobDetail):用于描述定时工作相关的信息 + * 触发器(Trigger):用于描述触发工作的规则,通常使用cron表达式定义调度规则 + * 调度器(Scheduler):描述了工作明细与触发器的对应关系 +* 导入SpringBoot整合quartz的坐标 + + + org.springframework.boot + spring-boot-starter-quartz + +* 定义具体要执行的任务,继承QuartzJobBean + + public class QuartzTaskBean extends QuartzJobBean { + @Override + protected void executeInternal(JobExecutionContext context) throws JobExecutionException { + System.out.println(“quartz job run... "); + } + } +* 定义工作明细与触发器,并绑定对应关系 + + @Configuration + public class QuartzConfig { + @Bean + public JobDetail printJobDetail(){ + return JobBuilder.newJob(QuartzTaskBean.class).storeDurably().build(); + } + @Bean + public Trigger printJobTrigger() { + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/3 * * * * ?"); + return TriggerBuilder.newTrigger().forJob(printJobDetail()) + .withSchedule(cronScheduleBuilder).build(); + } + } + +### 16.2、Spring Task + +* 开启定时任务功能 + + @SpringBootApplication + @EnableScheduling + public class Springboot22TaskApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot22TaskApplication.class, args); + } + } +* 设置定时执行的任务,并设定执行周期 + + @Component + public class ScheduledBean { + @Scheduled(cron = "0/5 * * * * ?") + public void printLog(){ + System.out.println(Thread.currentThread().getName()+":run..."); + } + } +* 定时任务相关配置 + + spring: + task: + scheduling: + # 任务调度线程池大小 默认 1 + pool: + size: 1 + # 调度线程名称前缀 默认 scheduling- + thread-name-prefix: ssm_ + shutdown: + # 线程池关闭时等待所有任务完成 + await-termination: false + # 调度线程关闭前最大等待时间,确保最后一定关闭 + await-termination-period: 10s + +### 16.3、SpringBoot整合JavaMail + +* SMTP(Simple Mail Transfer Protocol):简单邮件传输协议,用于**发送**电子邮件的传输协议 + +* POP3(Post Office Protocol - Version 3):用于**接收**电子邮件的标准协议 + +* IMAP(Internet Mail Access Protocol):互联网消息协议,是POP3的替代协议 + +* 导入SpringBoot整合JavaMail的坐标 + + + org.springframework.boot + spring-boot-starter-mail + +* 配置JavaMail + + spring: + mail: + host: smtp.qq.com + username: *********@qq.com + password: ********* + +![在这里插入图片描述](https://img-blog.csdnimg.cn/8c4baf0029074afaaca6ff4c670edd56.png) + +* 开启定时任务功能 + + @Service + public class SendMailServiceImpl implements SendMailService { + private String from = “********@qq.com"; // 发送人 + private String to = "********@126.com"; // 接收人 + private String subject = "测试邮件"; // 邮件主题 + private String text = "测试邮件正文"; // 邮件内容 + } +* 开启定时任务功能 + + @Service + public class SendMailServiceImpl implements SendMailService { + @Autowired + private JavaMailSender javaMailSender; + @Override + public void sendMail() { + SimpleMailMessage mailMessage = new SimpleMailMessage(); + mailMessage.setFrom(from); + mailMessage.setTo(to); + mailMessage.setSubject(subject); + mailMessage.setText(text); + javaMailSender.send(mailMessage); + } + } +* 附件与HTML文本支持 + + private String text = "传智教育"; + @Override + public void sendMail() { + try { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage,true); + mimeMessageHelper.setFrom(from); + mimeMessageHelper.setTo(to); + mimeMessageHelper.setSubject(subject); + mimeMessageHelper.setText(text,true); + File file = new File("logo.png"); + mimeMessageHelper.addAttachment("美图.png",file); + javaMailSender.send(mimeMessage); + } catch (Exception e) { + e.printStackTrace(); + } + } + +## 17、消息 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/fbb603a59b2b417fab3a7e26c48b2987.png) + +* 企业级应用中广泛使用的三种异步消息传递技术 + * JMS + * AMQP + * MQTT + +### 17.1、JMS + +* JMS(Java Message Service):一个规范,等同于JDBC规范,提供了与消息服务相关的API接口 + +* JMS消息模型 + + * peer-2-peer:点对点模型,消息发送到一个队列中,队列保存消息。队列的消息只能被一个消费者消费,或超时 + * **pub**lish-**sub**scribe:发布订阅模型,消息可以被多个消费者消费,生产者和消费者完全独立,不需要感知对方的存在 +* JMS消息种类 + + * TextMessage + * MapMessage + * **BytesMessage** + * StreamMessage + * ObjectMessage + * Message (只有消息头和属性) +* JMS实现:ActiveMQ、Redis、HornetMQ、RabbitMQ、RocketMQ(没有完全遵守JMS规范 + +### 17.2、AMQP + +* AMQP(advanced message queuing protocol):一种协议(高级消息队列协议,也是消息代理规范),规范了网络交换的数据格式,兼容JMS + +* 优点:具有跨平台性,服务器供应商,生产者,消费者可以使用不同的语言来实现 + +* AMQP消息模型 + + * direct exchange + * fanout exchange + * topic exchange + * headers exchange + * system exchange +* AMQP消息种类:byte[] + +* AMQP实现:RabbitMQ、StormMQ、RocketMQ + +### 17.3、MQTT + +* MQTT(Message Queueing Telemetry Transport)消息队列遥测传输,专为小设备设计,是物联网(IOT)生态系统中主要成分之一 + +### 17.4、Kafka + +* Kafka,一种高吞吐量的分布式发布订阅消息系统,提供实时消息功能。 + +### 17.5、消息案例 + +* 购物订单业务 + * 登录状态检测 + * 生成主单 + * 生成子单 + * 库存检测与变更 + * 积分变更 + * 支付 + * 短信通知(异步) + * 购物车维护 + * 运单信息初始化 + * 商品库存维护 + * 会员维护 + * … + +### 17.6、ActiveMQ + +* 下载地址:[https://activemq.apache.org/components/classic/download](https://activemq.apache.org/components/classic/download/)[/](https://activemq.apache.org/components/classic/download/) + +* 安装:解压缩 + +* 启动服务 + + `activemq.bat` + +* 访问服务器 + + `http://127.0.0.1:8161/` + + * 服务端口:61616,管理后台端口:8161 + * 用户名&密码:**admin** + +#### 17.6.1、SpringBoot整合ActiveMQ + +* 导入SpringBoot整合ActiveMQ坐标 + + + org.springframework.boot + spring-boot-starter-activemq + +* 配置ActiveMQ(采用默认配置) + + spring: + activemq: + broker-url: tcp://localhost:61616 + jms: + pub-sub-domain: true + template: + default-destination: itheima +* 生产与消费消息(使用默认消息存储队列) + + @Service + public class MessageServiceActivemqImpl implements MessageService { + @Autowired + private JmsMessagingTemplate jmsMessagingTemplate; + public void sendMessage(String id) { + System.out.println("使用Active将待发送短信的订单纳入处理队列,id:"+id); + jmsMessagingTemplate.convertAndSend(id); + } + public String doMessage() { + return jmsMessagingTemplate.receiveAndConvert(String.class); + } + } +* 生产与消费消息(指定消息存储队列) + + @Service + public class MessageServiceActivemqImpl implements MessageService { + @Autowired + private JmsMessagingTemplate jmsMessagingTemplate; + + public void sendMessage(String id) { + System.out.println("使用Active将待发送短信的订单纳入处理队列,id:"+id); + jmsMessagingTemplate.convertAndSend("order.sm.queue.id",id); + } + public String doMessage() { + return jmsMessagingTemplate.receiveAndConvert("order.sm.queue.id",String.class); + } + } +* 使用消息监听器对消息队列监听 + + @Component + public class MessageListener { + @JmsListener(destination = "order.sm.queue.id") + public void receive(String id){ + System.out.println("已完成短信发送业务,id:"+id); + } + } +* 流程性业务消息消费完转入下一个消息队列 + + @Component + public class MessageListener { + @JmsListener(destination = "order.sm.queue.id") + @SendTo("order.other.queue.id") + public String receive(String id){ + System.out.println("已完成短信发送业务,id:"+id); + return "new:"+id; + } + } + +### 17.7、RabbitMQ + +* RabbitMQ基于Erlang语言编写,需要安装Erlang + +* Erlang + + * 下载地址:[https](https://www.erlang.org/downloads)[😕/www.erlang.org/downloads](https://www.erlang.org/downloads) + * 安装:一键傻瓜式安装,安装完毕需要重启,需要依赖Windows组件 + * 环境变量配置 + * ERLANG_HOME + * PATH +* 下载地址:[https://](https://rabbitmq.com/install-windows.html)[rabbitmq.com/install-windows.html](https://rabbitmq.com/install-windows.html) + +* 安装:一键傻瓜式安装 + +* 启动服务 + + `rabbitmq-service.bat start` + +* 关闭服务 + + `rabbitmq-service.bat stop` + +* 查看服务状态 + + `rabbitmqctl status` + +* 服务管理可视化(插件形式) + +* 查看已安装的插件列表 + +* 开启服务管理插件 + + `rabbitmq-plugins.bat enable rabbitmq_management` + +* 访问服务器 + + `http://localhost:15672` + + * 服务端口:5672,管理后台端口:15672 + * 用户名&密码:**guest** + +#### 17.7.1、SpringBoot整合RabbitMQ + +* 导入SpringBoot整合RabbitMQ坐标 + + + org.springframework.boot + spring-boot-starter-amqp + +* 配置RabbitMQ (采用默认配置) + + spring: + rabbitmq: + host: localhost + port: 5672 +* 定义消息队列(direct) + + @Configuration + public class RabbitDirectConfig { + @Bean + public Queue queue(){ + return new Queue("simple_queue"); + } + } @Configuration + public class RabbitDirectConfig { + @Bean + public Queue queue(){ + // durable:是否持久化,默认false + // exclusive:是否当前连接专用,默认false,连接关闭后队列即被删除 + // autoDelete:是否自动删除,当生产者或消费者不再使用此队列,自动删除 + return new Queue("simple_queue",true,false,false); + } + } @Configuration + public class RabbitDirectConfig { + @Bean + public Queue directQueue(){ + return new Queue("direct_queue"); + } + @Bean + public Queue directQueue2(){ + return new Queue("direct_queue2"); + } + @Bean + public DirectExchange directExchange(){ + return new DirectExchange("directExchange"); + } + @Bean + public Binding bindingDirect(){ + return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct"); + } + @Bean + public Binding bindingDirect2(){ + return BindingBuilder.bind(directQueue2()).to(directExchange()).with("direct2"); + } + } +* 生产与消费消息(direct) + + @Service + public class MessageServiceRabbitmqDirectImpl implements MessageService { + @Autowired + private AmqpTemplate amqpTemplate; + @Override + public void sendMessage(String id) { + System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:"+id); + amqpTemplate.convertAndSend("directExchange","direct",id); + } + } +* 使用消息监听器对消息队列监听(direct) + + @Component + public class RabbitMessageListener { + @RabbitListener(queues = "direct_queue") + public void receive(String id){ + System.out.println("已完成短信发送业务,id:"+id); + } + } +* 使用多消息监听器对消息队列监听进行消息轮循处理(direct) + + @Component + public class RabbitMessageListener2 { + @RabbitListener(queues = "direct_queue") + public void receive(String id){ + System.out.println("已完成短信发送业务(two),id:"+id); + } + } +* 定义消息队列(topic) + + @Configuration + public class RabbitTopicConfig { + @Bean + public Queue topicQueue(){ return new Queue("topic_queue"); } + @Bean + public Queue topicQueue2(){ return new Queue("topic_queue2"); } + @Bean + public TopicExchange topicExchange(){ + return new TopicExchange("topicExchange"); + } + @Bean + public Binding bindingTopic(){ + return BindingBuilder.bind(topicQueue()).to(topicExchange()).with("topic.*.*"); + } + @Bean + public Binding bindingTopic2(){ + return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.#"); + } + } +* 绑定键匹配规则 + + * `*`(星号): 用来表示一个单词 ,且该单词是必须出现的 + * `#`(井号): 用来表示任意数量 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/bcfc4cb557714c3da413337a052de3ff.png) + +* 生产与消费消息(topic) + + @Service + public class MessageServiceRabbitmqTopicmpl implements MessageService { + @Autowired + private AmqpTemplate amqpTemplate; + @Override + public void sendMessage(String id) { + System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:"+id); + amqpTemplate.convertAndSend("topicExchange","topic.order.id",id); + } + } +* 使用消息监听器对消息队列监听(topic) + + @Component + public class RabbitTopicMessageListener { + @RabbitListener(queues = "topic_queue") + public void receive(String id){ + System.out.println("已完成短信发送业务,id:"+id); + } + @RabbitListener(queues = "topic_queue2") + public void receive2(String id){ + System.out.println("已完成短信发送业务(two),id:"+id); + } + } + +### 17.8、RocketMQ + +* 下载地址:[https://rocketmq.apache.org](https://rocketmq.apache.org/)[/](https://rocketmq.apache.org/) + +* 安装:解压缩 + + * 默认服务端口:9876 +* 环境变量配置 + +* ROCKETMQ_HOME + +* PATH + +* NAMESRV_ADDR (建议): 127.0.0.1:9876 + +* 命名服务器与broker + ![在这里插入图片描述](https://img-blog.csdnimg.cn/c9ec39af08124eb7904e5cdc2abc3b43.png) + +* 启动命名服务器 + + `mqnamesrv` + +* 启动broker + + `mqbroker` + +* 服务器功能测试:生产者 + + `tools org.apache.rocketmq.example.quickstart.Producer` + +* 服务器功能测试:消费者 + + `tools org.apache.rocketmq.example.quickstart.Consumer` + +#### 17.8.1、SpringBoot整合RocketMQ + +* 导入SpringBoot整合RocketMQ坐标 + + + org.apache.rocketmq + rocketmq-spring-boot-starter + 2.2.1 + + +* 配置RocketMQ (采用默认配置) + + rocketmq: + name-server: localhost:9876 + producer: + group: group_rocketmq +* 生产消息 + + @Service + public class MessageServiceRocketmqImpl implements MessageService { + @Autowired + private RocketMQTemplate rocketMQTemplate; + @Override + public void sendMessage(String id) { + rocketMQTemplate.convertAndSend("order_sm_id",id); + System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:"+id); + } + } +* 生产异步消息 + + @Service + public class MessageServiceRocketmqImpl implements MessageService { + @Autowired + private RocketMQTemplate rocketMQTemplate; + @Override + public void sendMessage(String id) { + SendCallback callback = new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + System.out.println("消息发送成功"); + } + @Override + public void onException(Throwable throwable) { + System.out.println("消息发送失败!!!!!!!!!!!"); + } + }; + System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:"+id); + rocketMQTemplate.asyncSend("order_sm_id",id,callback); + } + } +* 使用消息监听器对消息队列监听 + + @Component + @RocketMQMessageListener(topic="order_sm_id",consumerGroup = "group_rocketmq") + public class RocketmqMessageListener implements RocketMQListener { + @Override + public void onMessage(String id) { + System.out.println("已完成短信发送业务,id:"+id); + } + } + +### 17.9、Kafka + +* 下载地址:[https://](https://kafka.apache.org/downloads)[kafka.apache.org/downloads](https://kafka.apache.org/downloads) + +* windows 系统下3.0.0版本存在BUG,建议使用2.X版本 + +* 安装:解压缩 + +* 启动zookeeper + + `zookeeper-server-start.bat ..\..\config\zookeeper.properties` + + * 默认端口:2181 +* 启动kafka + + `kafka-server-start.bat ..\..\config\server.properties` + + * 默认端口:9092 +* 创建topic + + `kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic itheima` + +* 查看topic + + `kafka-topics.bat --zookeeper 127.0.0.1:2181 --list` + +* 删除topic + + `kafka-topics.bat --delete --zookeeper localhost:2181 --topic itheima` + +* 生产者功能测试 + + `kafka-console-producer.bat --broker-list localhost:9092 --topic itheima` + +* 消费者功能测试 + + `kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic itheima --from-beginning` + +#### 17.9.1、SpringBoot整合Kafka + +* 导入SpringBoot整合Kafka坐标 + + + org.springframework.kafka + spring-kafka + +* 配置Kafka (采用默认配置) + + spring: + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: order +* 生产消息 + + @Service + public class MessageServiceKafkaImpl implements MessageService { + @Autowired + private KafkaTemplate kafkaTemplate; + @Override + public void sendMessage(String id) { + System.out.println("使用Kafka将待发送短信的订单纳入处理队列,id:"+id); + kafkaTemplate.send("kafka_topic",id); } + } +* 使用消息监听器对消息队列监听 + + @Component + public class KafkaMessageListener{ + @KafkaListener(topics = {"kafka_topic"}) + public void onMessage(ConsumerRecord record) { + System.out.println("已完成短信发送业务,id:"+record.value()); + } + } + +## 18、监控 + +### 18.1、简介 + +监控的意义: + +* 监控服务状态是否宕机 +* 监控服务运行指标(内存、虚拟机、线程、请求等) +* 监控日志 +* 管理服务(服务下线) + +监控的实施方式: + +* 显示监控信息的服务器:用于获取服务信息,并显示对应的信息 +* 运行的服务:启动时主动上报,告知监控服务器自己需要受到监控 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/394df97f929849a9ae91c4510b5f27ee.png) + +### 18.2、可视化监控平台 + +* Spring Boot Admin,开源社区项目,用于管理和监控SpringBoot应用程序。 客户端注册到服务端后,通过HTTP请求方式,服务端定期从客户端获取对应的信息,并通过UI界面展示对应信息。 + +* Admin服务端 + + + 2.5.4 + + + + de.codecentric + spring-boot-admin-starter-server + + + + + + de.codecentric + spring-boot-admin-dependencies + ${spring-boot-admin.version} + pom + import + + + +* Admin客户端 + + + 2.5.4 + + + + de.codecentric + spring-boot-admin-starter-client + + + + + + de.codecentric + spring-boot-admin-dependencies + ${spring-boot-admin.version} + pom + import + + + +* Admin服务端 + + + de.codecentric + spring-boot-admin-starter-server + 2.5.4 + +* Admin客户端 + + + de.codecentric + spring-boot-admin-starter-client + 2.5.4 + +* Admin服务端 + + server: + port: 8080 +* 设置启用Spring-Admin + + @SpringBootApplication + @EnableAdminServer + public class Springboot25ActuatorServerApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot25ActuatorServerApplication.class, args); + } + } +* Admin客户端 + + spring: + boot: + admin: + client: + url: http://localhost:8080 + management: + endpoint: + health: + show-details: always + endpoints: + web: + exposure: + include: "*" + +![在这里插入图片描述](https://img-blog.csdnimg.cn/7102e30a17524139b1bb8f20c1776756.png) + +### 18.3、监控原理 + +* Actuator提供了SpringBoot生产就绪功能,通过端点的配置与访问,获取端点信息 + +* 端点描述了一组监控信息,SpringBoot提供了多个内置端点,也可以根据需要自定义端点信息 + +* 访问当前应用所有端点信息:**/actuator** + +* 访问端点详细信息:/actuator**/****端点名称** + ![在这里插入图片描述](https://img-blog.csdnimg.cn/342450058b5940738d2dfe692fa63a10.png) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/8c62c1bf3be0479d85f2ddcb18c34422.png) + +* Web程序专用端点 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/13dfbc9b4b3e4cd6b91cc22519c4e2a4.png) + +* 启用指定端点 + + management: + endpoint: + health: # 端点名称 + enabled: true show-details: always beans: # 端点名称 enabled: true +* 启用所有端点 + + management: + endpoints: + enabled-by-default: true +* 暴露端点功能 + + * 端点中包含的信息存在敏感信息,需要对外暴露端点功能时手动设定指定端点信息 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/cf157f09d9274dd68ffe9b83f6efc106.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/1d82e3fd9f254fd381acc37c05dfc09f.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/76cc94690520440fa27368ebb195eee9.png) + +### 18.4、自定义监控指标 + +* 为info端点添加自定义指标 + + info: + appName: @project.artifactId@ + version: @project.version@ + author: itheima @Component + public class AppInfoContributor implements InfoContributor { + @Override + public void contribute(Info.Builder builder) { + Map infoMap = new HashMap<>(); + infoMap.put("buildTime","2006"); + builder.withDetail("runTime",System.currentTimeMillis()) + .withDetail("company","传智教育"); + builder.withDetails(infoMap); + } + } +* 为Health端点添加自定义指标 + + @Component + public class AppHealthContributor extends AbstractHealthIndicator { + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + boolean condition = true; + if(condition){ + Map infoMap = new HashMap<>(); + infoMap.put("buildTime","2006"); + builder.withDetail("runTime",System.currentTimeMillis()) + .withDetail("company","传智教育"); + builder.withDetails(infoMap); + builder.status(Status.UP); + }else{ + builder.status(Status.DOWN); + } + } + } +* 为Metrics端点添加自定义指标 + + @Service + public class BookServiceImpl extends ServiceImpl implements IBookService { + private Counter counter; + public BookServiceImpl(MeterRegistry meterRegistry){ + counter = meterRegistry.counter("用户付费操作次数:"); + } + @Override + public boolean delete(Integer id) { + counter.increment(); + return bookDao.deleteById(id) > 0; + } + } +* 自定义端点 + + @Component + @Endpoint(id="pay") + public class PayEndPoint { + @ReadOperation + public Object getPay(){ + //调用业务操作,获取支付相关信息结果,最终return出去 + Map payMap = new HashMap(); + payMap.put("level 1",103); + payMap.put("level 2",315); + payMap.put("level 3",666); + return payMap; + } + } + +pom +import + + - Admin客户端 + +```xml + + 2.5.4 + + + + de.codecentric + spring-boot-admin-starter-client + + + + + + de.codecentric + spring-boot-admin-dependencies + ${spring-boot-admin.version} + pom + import + + + + +* Admin服务端 + + + de.codecentric + spring-boot-admin-starter-server + 2.5.4 + +* Admin客户端 + + + de.codecentric + spring-boot-admin-starter-client + 2.5.4 + +* Admin服务端 + + server: + port: 8080 +* 设置启用Spring-Admin + + @SpringBootApplication + @EnableAdminServer + public class Springboot25ActuatorServerApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot25ActuatorServerApplication.class, args); + } + } +* Admin客户端 + + spring: + boot: + admin: + client: + url: http://localhost:8080 + management: + endpoint: + health: + show-details: always + endpoints: + web: + exposure: + include: "*" + + [外链图片转存中…(img-cXWfQSEx-1657811363485)] + +### 18.3、监控原理 + +* Actuator提供了SpringBoot生产就绪功能,通过端点的配置与访问,获取端点信息 + +* 端点描述了一组监控信息,SpringBoot提供了多个内置端点,也可以根据需要自定义端点信息 + +* 访问当前应用所有端点信息:**/actuator** + +* 访问端点详细信息:/actuator**/****端点名称** + + [外链图片转存中…(img-KSdMaD18-1657811363486)] + + [外链图片转存中…(img-UlUbALwe-1657811363487)] + +* Web程序专用端点 + + [外链图片转存中…(img-maGlhCAb-1657811363487)] + +* 启用指定端点 + + management: + endpoint: + health: # 端点名称 + enabled: true show-details: always beans: # 端点名称 enabled: true +* 启用所有端点 + + management: + endpoints: + enabled-by-default: true +* 暴露端点功能 + + * 端点中包含的信息存在敏感信息,需要对外暴露端点功能时手动设定指定端点信息 + + [外链图片转存中…(img-6UeTKYgJ-1657811363488)] + +[外链图片转存中…(img-bLPYJP4S-1657811363489)] + +[外链图片转存中…(img-RwCI70cm-1657811363490)] + +### 18.4、自定义监控指标 + +* 为info端点添加自定义指标 + + info: + appName: @project.artifactId@ + version: @project.version@ + author: itheima @Component + public class AppInfoContributor implements InfoContributor { + @Override + public void contribute(Info.Builder builder) { + Map infoMap = new HashMap<>(); + infoMap.put("buildTime","2006"); + builder.withDetail("runTime",System.currentTimeMillis()) + .withDetail("company","传智教育"); + builder.withDetails(infoMap); + } + } +* 为Health端点添加自定义指标 + + @Component + public class AppHealthContributor extends AbstractHealthIndicator { + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + boolean condition = true; + if(condition){ + Map infoMap = new HashMap<>(); + infoMap.put("buildTime","2006"); + builder.withDetail("runTime",System.currentTimeMillis()) + .withDetail("company","传智教育"); + builder.withDetails(infoMap); + builder.status(Status.UP); + }else{ + builder.status(Status.DOWN); + } + } + } +* 为Metrics端点添加自定义指标 + + @Service + public class BookServiceImpl extends ServiceImpl implements IBookService { + private Counter counter; + public BookServiceImpl(MeterRegistry meterRegistry){ + counter = meterRegistry.counter("用户付费操作次数:"); + } + @Override + public boolean delete(Integer id) { + counter.increment(); + return bookDao.deleteById(id) > 0; + } + } +* 自定义端点 + + @Component + @Endpoint(id="pay") + public class PayEndPoint { + @ReadOperation + public Object getPay(){ + //调用业务操作,获取支付相关信息结果,最终return出去 + Map payMap = new HashMap(); + payMap.put("level 1",103); + payMap.put("level 2",315); + payMap.put("level 3",666); + return payMap; + } + } \ No newline at end of file diff --git "a/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" index d8269e5..9e46964 100644 --- "a/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" +++ "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" @@ -1,10 +1,10 @@ -> 简单的说一下Java的垃圾回收机制,解决了什么问题? +## 简单的说一下Java的垃圾回收机制,解决了什么问题? 这个题目其实我是不太想写的,原因在于太笼统了,容易让面试者陷入误区,难以正确的回答,但是,如何加上一个解决了什么问题,那么,这个面试题还是有意义的,可以看看面试者的理解。 在c语言和c++中,对于内存的管理是非常的让人头疼的,也是很多人放弃的原因,因此,在后面的一些高级语言中,设计者就想解决这一个问题,让开发者专注于自己的业务开发即可,因此,在Java中也引入了垃圾回收机制,它使得开发者在编写程序的时候不再需要考虑内存管理,由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。 -> 了解JVM的内存模型吗? +## 了解JVM的内存模型吗? JVM载执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。 @@ -259,7 +259,7 @@ vm参数介绍: 在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统的限制),从而导致动态扩展时出现**OutOfMemoryError**异常。 -> JVM的四种引用? +## JVM的四种引用? Java把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。 @@ -296,14 +296,14 @@ Java把对象的引用分为四种级别,从而使程序能更加灵活的控 **注意:** 在实际程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。 -> GC用的可达性分析算法中,哪些对象可以作为GC Roots对象? +## GC用的可达性分析算法中,哪些对象可以作为GC Roots对象? - 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象。 - 方法区中类静态属性引用的对象。 - 方法区中常量引用的对象。 - 本地方法栈中JNI(即一般说的Native方法)引用的对象。 -> 如何判断对象是不是垃圾? +## 如何判断对象是不是垃圾? #### 引用计数算法 @@ -365,7 +365,7 @@ class Instance{ - 方法区中常量引用的对象。 - 本地方法栈中JNI(即一般说的Native方法)引用的对象。 -> 介绍一下JVM的堆 +## 介绍一下JVM的堆 这个面试题其实和上面的有点重合,但是,这里单独拿出来再介绍一下,因为这个确实是比较常见的,这样大家也都有印象。 @@ -427,7 +427,7 @@ public class HeapTest { 图中出现了`java.lang.OutOfMemoryError`,并且提示了`Java heap space`,这就说明是Java堆内存溢出的情况。 -> 介绍一下Minor GC和Full GC +## 介绍一下Minor GC和Full GC 这个概念首先我们要了解JVM的内存分区,在上面的面试题中已经做了介绍,这里就不再介绍了。 @@ -444,7 +444,7 @@ public class HeapTest { 在这里更加详细的规则介绍可以参考这篇文章:[深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-jvm-nei-cun-fen-pei-yu-hui-shou-ce-lue-yuan-li-cong-ci-gao-bie-jvm-nei-cun-fen-pei-wen-mang.html) -> 说一下Java对象创建的方法 +## 说一下Java对象创建的方法 这个问题其实很简单,但是很多人却只知道new的方式。 @@ -453,7 +453,7 @@ public class HeapTest { - 使用反射,Class.forName(); - 运用反序列化机制,java.io.ObjectInputStream对象的readObject()方法。 -> 介绍一下几种垃圾收集算法? +## 介绍一下几种垃圾收集算法? #### 标记-清除(Mark-Sweep)算法 @@ -496,7 +496,7 @@ public class HeapTest { * **老年代** 在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用**标记-清除**或**标记-整理**算法来进行回收。 -> 如何减少gc出现的次数 +## 如何减少gc出现的次数 上面的面试题已经讲解到了,从年轻代空间(包括Eden和 Survivor 区域)回收内存被称为Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。 @@ -578,7 +578,7 @@ promotion failed是在进行Minor GC时,`survivor space`放不下、对象只 为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即`-XX:+UseCMSCompactAtFullCollection`开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 `-XX:CMSFullGCsBeforeCompaction`,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。 -> 数组多大会放在JVM老年代,永久代对象如何GC?如果想不被GC怎么办?如果想在GC中生存一次怎么办? +## 数组多大会放在JVM老年代,永久代对象如何GC?如果想不被GC怎么办?如果想在GC中生存一次怎么办? 虚拟机提供了一个`-XX:PretenureSizeThreshold`参数(通常是3MB),令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。(新生代采用复制算法收集内存) @@ -586,7 +586,7 @@ promotion failed是在进行Minor GC时,`survivor space`放不下、对象只 让对象实现finalize()方法,一次对象的自我拯救。 -> JVM 常见的参数有哪些,介绍几个常见的,并说说在你工作中实际用到的地方? +## JVM 常见的参数有哪些,介绍几个常见的,并说说在你工作中实际用到的地方? 首先,JVM 中的参数是非常多的,而且可以分为不同的类型,主要可以分为以下三种类型: - `标准参数(-)`,所有的JVM实现都必须实现这些参数的功能,而且向后兼容。 @@ -841,7 +841,7 @@ public class MethodTest { 更多的使用方法可以参考这篇文章 [如何利用VisualVM对高并发项目进行性能分析](http://www.java1000.com/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html),有更加细致的介绍。 -> 说说你了解的常见的内存调试工具:jps、jmap、jhat等。 +## 说说你了解的常见的内存调试工具:jps、jmap、jhat等。 |名称|解释| |-----|-----| @@ -1039,7 +1039,7 @@ GCT:垃圾回收消耗总时间 通过以上参数分析,发现老年代Full GC没有,说明没有大对象进入到老年代,整个老年代的GC情况还是不错的。另外,年轻代回收次数`12`次,使用时间`1.672`s,每次用时100ms左右,这个时间稍微长了一点,可以将新生代的空间调低一点,以降低每一次的GC时间。 -> 常见的垃圾回收器有哪些? +## 常见的垃圾回收器有哪些? 先上一张图,这张图是Java虚拟机的jdk1.7及以前版本的所有垃圾回收器,也可以说是比较成熟的垃圾回收器,除了这些垃圾回收器,面试的时候最多也就再怼怼G1和ZGC了。 @@ -1349,7 +1349,7 @@ G1来源:https://blog.51cto.com/lqding/1770055 |**G1**|并发|老年代|标记-整理|高吞吐量|面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器 -> 内存泄漏和内存溢出,什么时候会出现,怎么解决? +## 内存泄漏和内存溢出,什么时候会出现,怎么解决? 内存泄漏:(Memory Leak) 不再会被使用的对象的内存不能被回收,就是内存泄露。 @@ -1432,7 +1432,7 @@ G1来源:https://blog.51cto.com/lqding/1770055 2) 将 java heap 的 -Xmx 尽量调小,但是保证在不影响应用使用的前提下。 3) 限制对 native memory 的消耗,比如:将 thread 的 -Xss 调小,并且限制产生大量的线程;限制文件的 io 操作次数和数量;限制网络的使用等等。 -> Java 的类加载过程 +## Java 的类加载过程 JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。 @@ -1452,7 +1452,7 @@ JVM类加载机制分为五个部分:加载,验证,准备,解析,初 此阶段主要确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。 1. 文件格式验证:基于字节流验证。 -2. 元数据验证:基于**_方法区_**的存储结构验证。 +2. 元数据验证:基于方法区的存储结构验证。 3. 字节码验证:基于方法区的存储结构验证。 4. 符号引用验证:基于方法区的存储结构验证。 @@ -1498,7 +1498,7 @@ Java 中,对于初始化阶段,有且只有以下五种情况才会对要求 4. 虚拟机启动时,用户会先初始化要执行的主类(含有main) 5. jdk 1.7后,如果 java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic 方法句柄,并且这个方法所在类没有初始化,则先初始化。 -> 聊聊双亲委派机制 +## 聊聊双亲委派机制 ![](http://image.ouyangsihai.cn/FoFkvh_XGVUdHkp3F6dYBpwE8tgb) diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" index 077a179..217da0b 100644 --- "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" @@ -1,21 +1,34 @@ +假设你是一名资深的 Java 开发工程师,有 5-10 年的大厂开发经验,现在你正在面试,需要你回答下面的一些问题,并且答案需要满足下列要求: +1、用中文回答; +2、以 markdown 的格式回答,中英文左右有空格,同时,对你的答案进行重点的突出标注等; +3、对于特别有需要解释的,难以理解的、有深度的内容,加以代码进行解释; +4、对相关实现的底层原理进行对比和分析; +你可以帮助我完成吗? -## 集合容器概述 -### 什么是集合 +假设你是一名资深的 Java 开发工程师,有 5-10 年的大厂开发经验,现在你是一名面试官,现在你正在面试一名有着 5 年大厂经验的 Java 开发工程师。 -* 集合就是一个放数据的容器,准确的说是放数据对象引用的容器 -* 集合类存放的都是对象的引用,而不是对象的本身 +ConcurrentHashMap 和 Hashtable 的区别是什么? +需要你详细的回答,对底层的实现原理进行分析。 +然后,用 markdown 格式,重点突出,同时,如果 pdf 的内容有不完善的地方,结合你的理解补充完整。 -* 集合类型主要有3种:set(集)、list(列表)和map(映射)。 -### 集合的特点 +ConcurrentHashMap 和 Hashtable 的区别,需要你详细的回答,对底层的实现原理进行分析。 +然后,用 markdown 格式,重点突出。 -* 集合的特点主要有如下两点: - * 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。 - * 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小 + +## 集合容器概述 + +### 什么是集合 + +简单来说,集合就是一个放数据容器,它主要包括 Collection 和 Map 集合 + +- 集合只能存放对象,Java中每一种基本数据类型都有对应的引用类型。例如在集合中存储一个int型数据时,要先自动转换成Integer类后再存入; +- 集合存放的是对对象的引用,对象本身还是存放在堆内存中; +- 集合可以存放不同类型、不限数量的数据类型。 ### 集合和数组的区别 @@ -25,16 +38,10 @@ * 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。 -### 使用集合框架的好处 - -1. 容量自增长; -2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量; -3. 可以方便地扩展或改写集合,提高代码复用性和可操作性。 -4. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。 ### 常用的集合类有哪些? -* Map接口和Collection接口是所有集合框架的父接口: +常用的Java集合主要由三大体系:Set、List和Map。其中Set和List是基于Collection接口的实现类,Set中常用的有HashSet和TreeSet,List中常用的有ArrayList,基于Map接口的常用实现类有HashMap和TreeMap。 1. Collection接口的子接口包括:Set接口和List接口 2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等 diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/test.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/test.md" new file mode 100644 index 0000000..e69de29 diff --git "a/docs/project/\350\205\276\350\256\257\351\241\271\347\233\256\346\200\273\347\273\223.md" "b/docs/project/\350\205\276\350\256\257\351\241\271\347\233\256\346\200\273\347\273\223.md" new file mode 100644 index 0000000..9d46f22 --- /dev/null +++ "b/docs/project/\350\205\276\350\256\257\351\241\271\347\233\256\346\200\273\347\273\223.md" @@ -0,0 +1,26 @@ +# 面试查漏补缺 + +- 面试前,查看该公司的面经 +- 逻辑表达慢一点,表达清楚 +- golang使用不够熟练 +- Mysql + - mysql语句实战不够熟练 +- redis +- kafka + - kafka 的实现 +- 项目(actor-go) + - 项目介绍 + - 介绍整个项目的情况 + - 介绍个人负责的模块 + - actor-sdk-go + - actormodel + - 项目重点、难点 + - 设计模式的应用和抽象,提升的架构设计和编码能力 + - DDD 的开发模式应用 + - 更加深入的理解了 DevOps 开发理念 + - DDD + - 消息驱动模型 + https://cloud.tencent.com/developer/article/1656611?from=article.detail.1857333 + - 设计模式(工厂、模板、单例、回调) + - 分布式项目介绍 + https://blog.csdn.net/fanrenxiang/article/details/85083243 diff --git a/img.png b/img.png new file mode 100644 index 0000000..4f986f7 Binary files /dev/null and b/img.png differ