diff --git a/.gitignore b/.gitignore index c5217a32..48b1d15d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,8 @@ hs_err_pid* out # iml -*.iml \ No newline at end of file +*.iml + +.DS_Store +.aider* +.env diff --git "a/Basics/Java\351\235\242\350\257\225\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/Basics/Java\351\235\242\350\257\225\345\237\272\347\241\200\347\237\245\350\257\206.md" deleted file mode 100644 index 22adfc8c..00000000 --- "a/Basics/Java\351\235\242\350\257\225\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ /dev/null @@ -1,563 +0,0 @@ ---- -title: Java面试基础知识 -author: DreamCat -id: 1 -date: 2019-11-29 16:20:07 -tags: Java -categories: Java ---- -## 引言 - -> [JavaGuide](https://github.com/Snailclimb/JavaGuide) :一份涵盖大部分Java程序员所需要掌握的核心知识。**star:45159**,替他宣传一下子 -> -> 这位大佬,总结的真好!!!我引用这位大佬的文章,因为方便自己学习和打印... - - - -## 面向对象和面向过程的区别 - -- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。** -- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。 - -> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是因为它是面向对象语言,而是Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码。 -> -> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比Java好。 - -## Java 语言有哪些特点? - -1. 简单易学; -2. 面向对象(封装,继承,多态); -3. 平台无关性( Java 虚拟机实现平台无关性); -4. 可靠性; -5. 安全性; -6. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持); -7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便); -8. 编译与解释并存; - -## 关于 JVM JDK 和 JRE 最详细通俗的解答 - -### JVM - -Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 - -**什么是字节码?采用字节码的好处是什么?** - -> 在 Java 中,JVM可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。 - -**Java 程序从源代码到运行一般有下面3步:** - -![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png) - -我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。 - -> HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。 - -**总结:** - -Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 - -### JDK 和 JRE - -JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。 - -JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。但是,它不能用于创建新程序。 - -如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装JDK了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,如果要使用JSP部署Web应用程序,那么从技术上讲,您只是在应用程序服务器中运行Java程序。那你为什么需要JDK呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 - -## Java和C++的区别? - -我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过C++,也要记下来! - -- 都是面向对象的语言,都支持封装、继承和多态 -- Java 不提供指针来直接访问内存,程序内存更加安全 -- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 -- Java 有自动内存管理机制,不需要程序员手动释放无用内存 - -## 什么是 Java 程序的主类 应用程序和小程序的主类有何不同? - -一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 - -## Java 应用程序与小程序之间有哪些差别? - -简单说应用程序是从主线程启动(也就是 `main()` 方法)。applet 小程序没有 `main()` 方法,主要是嵌在浏览器页面上运行(调用`init()`或者`run()`来启动),嵌入浏览器这点跟 flash 的小游戏类似。 - -## 字符型常量和字符串常量的区别? - -1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符 -2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置) -3. 占内存大小 字符常量只占2个字节; 字符串常量占若干个字节(至少一个字符结束标志) (**注意: char在Java中占两个字节**) - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) - -## 构造器 Constructor 是否可被 override? - -在讲继承的时候我们就知道父类的私有属性和构造方法并不能被继承,所以 Constructor 也就不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 - -## 重载和重写的区别 - -### 重载 - -发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - -### 重写 - - 重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。** - -## Java 面向对象编程三大特性: 封装 继承 多态 - -### 封装 - -封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - -### 继承 - -继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 - -**关于继承如下 3 点请记住:** - -1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 -2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -### 多态 - -所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 - -在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 - -## String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? - -**可变性** - -简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。 - -**线程安全性** - -String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。  - -**性能** - -每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -**对于三者使用的总结:** - -1. 操作少量的数据: 适用String -2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder -3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer - -## 自动装箱与拆箱 - -- **装箱**:将基本类型用它们对应的引用类型包装起来; -- **拆箱**:将包装类型转换为基本数据类型; - -## 在一个静态方法内调用一个非静态成员为什么是非法的? - -由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 - -## 在 Java 中定义一个不做事且没有参数的构造方法的作用 - -Java 程序在执行子类的构造方法之前,如果没有用 `super() `来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 `super() `来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 - -## import java和javax有什么区别? - -刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。 - -## 接口和抽象类的区别是什么? - -1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 -2. 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。 -3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。 -4. 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。 -5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。 - -备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。 - -## 成员变量与局部变量的区别有哪些? - -1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 -2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 -3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 -4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 - -## 创建一个对象用什么运算符?对象实体与对象引用有何不同? - -new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。 - -## 什么是方法的返回值?返回值在类的方法里的作用是什么? - -方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作! - -## 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么? - -主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。 - -## 构造方法有哪些特性? - -1. 名字与类名相同。 -2. 没有返回值,但不能用void声明构造函数。 -3. 生成类的对象时自动执行,无需调用。 - -## 静态方法和实例方法有何不同 - -1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 -2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。 - -## 对象的相等与指向他们的引用相等,两者有什么不同? - -对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。 - -## 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? - -帮助子类做初始化工作。 - -## == 与 equals(重要) - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。 - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: - -- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -## hashCode 与 equals (重要) - -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?” - -### hashCode()介绍 - -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。 - -散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -### 为什么要有 hashCode - -**我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -通过我们可以看出:`hashCode()` 的作用就是**获取哈希码**,也称为散列码;它实际上是返回一个int整数。这个**哈希码的作用**是确定该对象在哈希表中的索引位置。**`hashCode() `在散列表中才有用,在其它情况下没用**。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。 - -### hashCode()与equals()的相关规定 - -1. 如果两个对象相等,则hashcode一定也是相同的 -2. 两个对象相等,对两个对象分别调用equals方法都返回true -3. 两个对象有相同的hashcode值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -## 为什么Java中只有值传递? - -- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。 -- 一个方法可以改变一个对象参数的状态。 -- 一个方法不能让对象参数引用一个新的对象。 - -## 简述线程、程序、进程的基本概念。以及他们之间关系是什么? - -**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 - -**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 -线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。 - -## 关于 final 关键字的一些总结 - -final关键字主要用在三个地方:变量、方法、类。 - -1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 -2. 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。 -3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 - -**Final修饰有啥好处** - -- final的关键字提高了性能,JVM和java应用会缓存final变量; -- final变量可以在多线程环境下保持线程安全; -- 使用final的关键字提高了性能,JVM会对方法变量类进行优化; - -## Java 中的异常处理 - -在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 **Throwable类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。 - -**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。 - -这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。 - -**Exception(异常):是程序本身可以处理的异常**。Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由Java虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以0时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。 - -**注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。** - -### Throwable类常用方法 - -- **public string getMessage()**:返回异常发生时的简要描述 -- **public string toString()**:返回异常发生时的详细信息 -- **public string getLocalizedMessage()**:返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同 -- **public void printStackTrace()**:在控制台上打印Throwable对象封装的异常信息 - -### 异常处理总结 - -- **try 块:** 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。 -- **catch 块:** 用于处理try捕获到的异常。 -- **finally 块:** 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return - 语句时,finally语句块将在方法返回之前被执行。 - -**在以下4种特殊情况下,finally块不会被执行:** - -1. 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行 -2. 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 ;若该语句在异常语句之后,finally会执行 -3. 程序所在的线程死亡。 -4. 关闭CPU。 - -**注意:** 当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会覆盖原始的返回值。如下: - -```java - public static int f(int value) { - try { - return value * value; - } finally { - if (value == 2) { - return 0; - } - } - } -``` - -如果调用 `f(2)`,返回值将是0,因为finally语句的返回值覆盖了try语句块的返回值。 - -## Java序列化中如果有些字段不想进行序列化,怎么办? - -对于不想进行序列化的变量,使用transient关键字修饰。 - -transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。 - -## 获取用键盘输入常用的两种方法 - -方法1:通过 Scanner - -```java -Scanner input = new Scanner(System.in); -String s = input.nextLine(); -input.close(); -``` - -方法2:通过 BufferedReader - -```java -BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); -String s = input.readLine(); -``` - -## Java 中 IO 流 - -### Java 中 IO 流分为几种? - - - 按照流的流向分,可以分为输入流和输出流; - - 按照操作单元划分,可以划分为字节流和字符流; - - 按照流的角色划分为节点流和处理流。 - -Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。 - - - InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - - OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 - -### 既然有了字节流,为什么还要有字符流? - -问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** - -回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -### BIO,NIO,AIO 有什么区别? - -- **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 -- **NIO (New I/O):** NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 -- **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 - -## static 关键字 - -**static 关键字主要有以下四种使用场景:** - -1. **修饰成员变量和成员方法:** 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:`类名.静态变量名` `类名.静态方法名()` -2. **静态代码块:** 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. -3. **静态内部类(static修饰类的话只能修饰内部类):** 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。 -4. **静态导包(用来导入类中的静态资源,1.5之后的新特性):** 格式为:`import static` 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 - -## this 关键字 - -```java -class Manager { - Employees[] employees; - - void manageEmployees() { - int totalEmp = this.employees.length; - System.out.println("Total employees: " + totalEmp); - this.report(); - } - - void report() { } -} -``` - -在上面的示例中,this关键字用于两个地方: - -- this.employees.length:访问类Manager的当前实例的变量。 -- this.report():调用类Manager的当前实例的方法。 - -此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。 - -## super 关键字 - -super关键字用于从子类访问父类的变量和方法。 例如: - -```java -public class Super { - protected int number; - - protected showNumber() { - System.out.println("number = " + number); - } -} - -public class Sub extends Super { - void bar() { - super.number = 10; - super.showNumber(); - } -} -``` - -在上面的例子中,Sub 类访问父类成员变量 number 并调用其其父类 Super 的 `showNumber()` 方法。 - -**使用 this 和 super 要注意的问题:** - -- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 -- this、super不能用在static方法中。 - -**简单解释一下:** - -被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, **this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西**。 - -## 深拷贝 vs 浅拷贝 - -1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 -2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 - -![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg) - -## String A = "123"; String B = new String("123");生成几个对象? - -我说如果常量池中,原来没有“123”那么就是生成了2个对象,如果常量池中有“123”那么只要1个对象生成 - -## hashTable吗,和hashmap有啥区别? - -- Hashtable的方法是安全的有synchronized修饰,Hashmap是不安全的; -- hashtable不可以有null值;HashMap则可以有空值; -- Hashtable中数组默认大小是11,而Hashmap默认大小是16,;扩容的时候hashtable是乘以2加1,而hashmap是乘以2. - -## ArrayList和LinkedList的区别? - -- ArrayList底层是数组,ArrayLIst查找数据快 -- LinkedList底层是链表,LinkedList插入删除快; - -## linkedList可以用for循环遍历吗? - -- 能不用尽量不要用,linkedList底层是链表,它使用for进行遍历,访问每一个元素都是从头开始访问然后直到找到这个元素, -- 比如说找第三个节点,需要先找到第一个节点然后找到第二个节点; -- 继续找第4个节点,不是从第三个节点开始找的,还是从第一个节点开始,所以非常的慢,不推荐,可以用迭代器进行遍历。 - -## 说说&和&&的区别 - -- &和&&都可以用作逻辑与的运算符,表示逻辑与(and) -- 当运算符两边的表达式的结果都为 true 时, 整个运算结果才为 true,否则,只要有一方为 false,则结果为 false。 -- &&还具有短路的功能,即如果第一个表达式为 false,则不再计算第二个表达式 -- &还可以用作位运算符,当&操作符两边的表达式不是 boolean 类型时,&表示按位与操作,我们通常 使用 0x0f 来与一个整数进行&运算,来获取该整数的最低 4 个 bit 位 - -## switch语句能否作用在byte上,能否作用在long上,能否作用在String上? - -- switch的condition只能是一个整数表达式或者枚举常量 -- 整数表达式则是int或者integet包装类型,由于,byte,short,char都可以隐式转换为int,则作用。 -- long 和 String 类型都不符合 switch 的语法规定,并且不能被隐式转 换成 int 类型,所以,它们不能作用于 swtich 语句中。 - -## short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 += 1; 有什么错? - -- 对于 short s1 = 1; s1 = s1 + 1; 由于 s1+1 运算时会自动提升表达式的类型,所以结果是 int 型,再赋值 给 short 类型 s1 时,**编译器将报告需要强制转换类型的错误。** -- 对于 short s1 = 1; s1 += 1;由于 **+= 是 java 语言规定的运算符,java 编译器会对它进行特殊处理**,因此 可以正确编译。 - -## char 型变量中能不能存贮一个中文汉字?为什么? - -- char 型变量是用来存储 Unicode 编码的字符的,unicode 编码字符集中包含了汉字,所以,char 型变量 中当然可以存储汉字啦。 -- 如果某个特殊的汉字没有被包含在 unicode 编码字符集中,那么,这个 char 型变量中就不能存储这个特殊汉字。 -- unicode 编码占用两个字节,所以,char 类型的变量也是占 用两个字节。 - -## 请说出作用域 public,private,protected,以及不写时 的区别 - -| 作用域 | 当前类 | 同package | 子孙类 | 其他package | -| :-------: | :----: | :-------: | :----: | :---------: | -| public | √ | √ | √ | √ | -| protected | √ | √ | √ | × | -| friednly | √ | √ | × | × | -| private | √ | × | × | × | - -## final, finally, finalize 的区别。 - -- final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。 内部类要访问局部变量,局部变量必须定义成 final 类型 -- finally 是异常处理语句结构的一部分,表示总是执行。 -- finalize 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法, 可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。JVM 不保证此方法总被调用 - -## 请写出你最常见到的 5 个 runtime exception。 - -- RuntimeException 的子类 -- NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException。 - -## 反射 - -### 反射机制介绍 - -**Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能成为java语言的反射机制。** - -### 静态编译和动态编译 - -- 静态编译:在编译时确定类型,绑定对象 -- 动态编译:运行时确定类型,绑定对象 - -### 反射机制优缺点 - -- 优点:运行期间类型的判断,动态加载类,提高代码的灵活度。 -- 缺点:性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的java代码要慢很多。 - -### 反射的应用场景 - -**反射是框架设计的灵魂** - -在我们平时的项目开发过程中,基本上很少会直接使用的反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如**模块化**的开发,通过反射去调用对应的字节码;动态代理设计模型也采用了反射机制,还有我们日常使用的**Spring / Hibernate**等框架也大量使用到了反射机制。 - -- 我们在使用JDBC连接数据库时使用`Class.forName()`通过反射加载数据看的驱动程序; -- Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring通过XML配置模式装载Bean的过程; - - 将程序内所有XML或Properties配置文件加载入内存中; - - Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; - - 使用反射机制,根据这个字符串获得某个类的Class实例 - - 动态配置实例的属性 -- [反射参考(重要)](https://blog.csdn.net/sinat_38259539/article/details/71799078) \ No newline at end of file diff --git a/Basics/src/com/copy/ShallowCopyDemo.java b/Basics/src/com/copy/ShallowCopyDemo.java deleted file mode 100644 index 88daced4..00000000 --- a/Basics/src/com/copy/ShallowCopyDemo.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @program JavaBooks - * @description: ShalldowCopy - * @author: mf - * @create: 2020/02/10 21:21 - */ - -package com.copy; - -public class ShallowCopyDemo { - public static void main(String[] args) throws CloneNotSupportedException { - // 原始对象 - Student student = new Student(new Subject("买"), "峰"); - System.out.println("原始对象: " + student.getName() + " - " + student.getSubject().getName()); - - // 拷贝对象 - Student cloneStu = (Student) student.clone(); - System.out.println("拷贝对象: " + cloneStu.getName() + " - " + cloneStu.getSubject().getName()); - - // 原始对象和拷贝对象是否一样: - System.out.println("原始对象和拷贝对象是否一样: " + (student == cloneStu)); - - // 原始对象和拷贝对象的name属性是否一样 - System.out.println("原始对象和拷贝对象的name属性是否一样: " + (student.getName() == cloneStu.getName())); - - // 原始对象和拷贝对象的subj属性是否一样 - System.out.println("原始对象和拷贝对象的subj属性是否一样: " + (student.getSubject() == cloneStu.getSubject())); - - student.setName("小"); - student.getSubject().setName("疯"); - System.out.println("更新后的原始对象: " + student.getName() + " - " + student.getSubject().getName()); - System.out.println("更新原始对象后的克隆对象: " + cloneStu.getName() + " - " + cloneStu.getSubject().getName()); - - // 在这个例子中,让要拷贝的类Student实现了Clonable接口并重写Object类的clone()方法,然后在方法内部调用super.clone()方法。 - // 从输出结果中我们可以看到,对原始对象stud的"name"属性所做的改变并没有影响到拷贝对象clonedStud; - // 但是对引用对象subj的"name"属性所做的改变影响到了拷贝对象clonedStud。 - } -} diff --git a/Basics/src/com/copy/Student.java b/Basics/src/com/copy/Student.java deleted file mode 100644 index 64453203..00000000 --- a/Basics/src/com/copy/Student.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @program JavaBooks - * @description: Student - * @author: mf - * @create: 2020/02/10 21:22 - */ - -package com.copy; - -public class Student implements Cloneable{ - - // 对象的引用 - private Subject subject; - - private String name; - - public Student(Subject s, String name) { - this.subject = s; - this.name = name; - } - - public Subject getSubject() { - return subject; - } - - public String getName() { - return name; - } - - public void setSubject(Subject subject) { - this.subject = subject; - } - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Student{" + - "subject=" + subject + - ", name='" + name + '\'' + - '}'; - } - - @Override - protected Object clone() throws CloneNotSupportedException { - // 浅拷贝 -// return super.clone(); - - // 深拷贝 - Student student = new Student(new Subject(subject.getName()), name); - return student; - // 因为它是深拷贝,所以你需要创建拷贝类的一个对象。 - // 因为在Student类中有对象引用,所以需要在Student类中实现Cloneable接口并且重写clone方法。 - } -} diff --git a/Basics/src/com/copy/Subject.java b/Basics/src/com/copy/Subject.java deleted file mode 100644 index 92982084..00000000 --- a/Basics/src/com/copy/Subject.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @program JavaBooks - * @description: Subject - * @author: mf - * @create: 2020/02/10 21:22 - */ - -package com.copy; - -public class Subject { - - private String name; - - public Subject(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Subject{" + - "name='" + name + '\'' + - '}'; - } -} diff --git a/Basics/src/com/equal/Student.java b/Basics/src/com/equal/Student.java deleted file mode 100644 index 4736b8c5..00000000 --- a/Basics/src/com/equal/Student.java +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @program JavaBooks - * @description: Student - * @author: mf - * @create: 2020/02/07 02:07 - */ - -package com.equal; - -import java.util.HashSet; -import java.util.Objects; - -/** - * HashCode、equals和== - */ -public class Student { - - private Long id; - - private String name; - - public Student(Long id, String name) { - this.id = id; - this.name = name; - } - - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Student{" + - "id=" + id + - ", name='" + name + '\'' + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Student student = (Student) o; - System.out.println("name:" + this.getName() + "-->equals:" + - (Objects.equals(id, student.id) && Objects.equals(name, student.name)) + " " + student.getName()); - return Objects.equals(id, student.id) && - Objects.equals(name, student.name); - } - - @Override - public int hashCode() { - System.out.println("name:" + this.getName() + "-->hashcode:" + Objects.hash(id, name)); - return Objects.hash(id, name); - } - - - public static void main(String[] args) { - Student student = new Student(1L, "Mai"); - Student student1 = new Student(1L, "Mai"); -// System.out.println(student == student1); -// System.out.println(student.equals(student1)); - Student student2 = new Student(2L, "Liu"); - HashSet set = new HashSet<>(); - set.add(student); - set.add(student1); - set.add(student2); - System.out.println(set.toString()); - } -} diff --git a/Basics/src/com/java8/LambdaDemo.java b/Basics/src/com/java8/LambdaDemo.java deleted file mode 100644 index 6c159b00..00000000 --- a/Basics/src/com/java8/LambdaDemo.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @program JavaBooks - * @description: Lambda表达式 - * @author: mf - * @create: 2020/01/23 16:20 - */ - -package com.java8; - -import java.util.*; - -public class LambdaDemo { - public static void main(String[] args) { - // 老版本的排序 - List names = Arrays.asList("peter", "anna", "mike", "xenia"); - System.out.println(names); -// Collections.sort(names, new Comparator() { -// @Override -// public int compare(String a, String b) { -// return b.compareTo(a); -// } -// }); - - // 新版本 lambda表达式: - -// Collections.sort(names, (String a, String b) -> { -// return b.compareTo(a); -// }); -// Collections.sort(names, (String a, String b) -> b.compareTo(a)); - names.sort((String a, String b) -> b.compareTo(a)); - System.out.println(names); - } -} diff --git a/Basics/src/com/java8/MapsDemo.java b/Basics/src/com/java8/MapsDemo.java deleted file mode 100644 index eaca1551..00000000 --- a/Basics/src/com/java8/MapsDemo.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @program JavaBooks - * @description: map - * @author: mf - * @create: 2020/01/25 12:48 - */ - -package com.java8; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class MapsDemo { - public static void main(String[] args) { -// Map map = new HashMap<>(); -// for (int i = 0; i < 10; i++) { -// map.put(i, "val" + i); -// } -// map.forEach((id, val) -> System.out.println(val)); - - Student s1 = new Student(1L, "肖战", 15); - Student s2 = new Student(2L, "王一博", 15); - Student s3 = new Student(3L, "杨紫", 17); - Student s4 = new Student(4L, "李现", 17); - List students = new ArrayList<>(); - students.add(s1); - students.add(s2); - students.add(s3); - students.add(s4); - - List names = students.stream().map(s -> "名字:" + s.getName()).collect(Collectors.toList()); - names.forEach(a -> System.out.println(a)); - } -} diff --git a/Basics/src/com/java8/ParallerStreamDemo.java b/Basics/src/com/java8/ParallerStreamDemo.java deleted file mode 100644 index 8fbce412..00000000 --- a/Basics/src/com/java8/ParallerStreamDemo.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @program JavaBooks - * @description: 并行计算 - * @author: mf - * @create: 2020/01/25 12:40 - */ - -package com.java8; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -public class ParallerStreamDemo { - - public static void main(String[] args) { - int max = 1000000; - List values = new ArrayList<>(max); - for (int i = 0; i < max; i++) { - UUID uuid = UUID.randomUUID(); - values.add(uuid.toString()); - } - - // Sequential Sort(串行排序) - long t0 = System.nanoTime(); -// long t0 = System.currentTimeMillis(); - long count = values.stream().sorted().count(); - System.out.println(count); - long t1 = System.nanoTime(); -// long t1 = System.currentTimeMillis(); - long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); -// long millis = t1 - t0; - System.out.println(String.format("sequential sort took: %d ms", millis)); - - // paraller sorted - - long t2 = System.nanoTime(); - long count1 = values.parallelStream().sorted().count(); - System.out.println(count1); - long t3 = System.nanoTime(); - long millis1 = TimeUnit.NANOSECONDS.toMillis(t3 - t2); - System.out.println(String.format("paraller sort took: %d ms", millis1)); - } -} diff --git a/Basics/src/com/java8/Person.java b/Basics/src/com/java8/Person.java deleted file mode 100644 index f199e208..00000000 --- a/Basics/src/com/java8/Person.java +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @program JavaBooks - * @description: 方法和函数式引用 - * @author: mf - * @create: 2020/01/23 16:36 - */ - -package com.java8; - -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Supplier; - -public class Person { - - String name; - - Integer age; - - public Person() { - } - - public Person(String name) { - this.name = name; - } - - public Person(String name, Integer age) { - this.name = name; - this.age = age; - } - - - @Override - public String toString() { - return "Person{" + - "name='" + name + '\'' + - ", age=" + age + - '}'; - } - - public static void main(String[] args) { - // 当只有1个参数时 - Supplier supplier = Person::new; // 构造函数引用指向默认的Apple()构造函数 - Person person = supplier.get(); // new 一个对象 - System.out.println(person); - - Supplier supplier1 = () -> new Person(); // 和上面等价 - Person person1 = supplier.get(); - System.out.println(person1); - - // 当只有一个参数时 - Function function = Person::new; - Person person2 = function.apply("mai"); - System.out.println(person2); - - // 当有两个参数时 - BiFunction function1 = Person::new; - Person person3 = function1.apply("feng", 25); - System.out.println(person3); - } -} diff --git a/Basics/src/com/java8/Student.java b/Basics/src/com/java8/Student.java deleted file mode 100644 index 75f255e8..00000000 --- a/Basics/src/com/java8/Student.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @program JavaBooks - * @description: Student - * @author: mf - * @create: 2020/02/07 02:07 - */ - -package com.java8; - -import java.util.HashSet; -import java.util.Objects; - -/** - * HashCode、equals和== - */ -public class Student { - - private Long id; - - private String name; - - private Integer age; - - public Student(Long id, String name, Integer age) { - this.id = id; - this.name = name; - this.age = age; - } - - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getAge() { - return age; - } - - public void setAge(Integer age) { - this.age = age; - } - - @Override - public String toString() { - return "Student{" + - "id=" + id + - ", name='" + name + '\'' + - ", age=" + age + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Student student = (Student) o; - System.out.println("name:" + this.getName() + "-->equals:" + - (Objects.equals(id, student.id) && Objects.equals(name, student.name)) + " " + student.getName()); - return Objects.equals(id, student.id) && - Objects.equals(name, student.name) && - Objects.equals(age, student.age); - } - - @Override - public int hashCode() { - System.out.println("name:" + this.getName() + "-->hashcode:" + Objects.hash(id, name)); - return Objects.hash(id, name, age); - } - - - public static void main(String[] args) { - Student student = new Student(1L, "Mai", 16); - Student student1 = new Student(1L, "Mai", 16); -// System.out.println(student == student1); -// System.out.println(student.equals(student1)); - Student student2 = new Student(2L, "Liu", 18); - HashSet set = new HashSet<>(); - set.add(student); - set.add(student1); - set.add(student2); - System.out.println(set.toString()); - } -} diff --git a/Basics/src/com/java8/streamDemo.java b/Basics/src/com/java8/streamDemo.java deleted file mode 100644 index 0b18d0d6..00000000 --- a/Basics/src/com/java8/streamDemo.java +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @program JavaBooks - * @description: 流的一些例子 - * @author: mf - * @create: 2020/01/23 17:39 - */ - -package com.java8; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; - -public class streamDemo { - public static void main(String[] args) { - List strings = Arrays.asList("ddd3", "bbb", "ccc", "www", "eee", "ddd2", "bbb"); - - System.out.println("过滤:"); - // 过滤 - strings - .stream() - .filter((s) -> s.startsWith("d")) - .forEach(System.out::println); - - System.out.println("排序:"); - // 排序 - strings - .stream() - .sorted() - .filter((s) -> s.startsWith("d")) - .forEach(System.out::println); - - System.out.println("映射:"); - strings - .stream() - .map(String::toUpperCase) - .sorted() - .forEach(System.out::println); - - // match - System.out.println("匹配:"); - boolean anyStartsWithA = - strings - .stream() - .anyMatch((s) -> s.startsWith("d")); - System.out.println(anyStartsWithA); - boolean allStartsWithA = - strings - .stream() - .allMatch((s) -> s.startsWith("d")); - System.out.println(allStartsWithA); - boolean noneStartsWithA = - strings - .stream() - .noneMatch((s) -> s.startsWith("a")); - System.out.println(noneStartsWithA); - - System.out.println("计数:"); - long count = - strings - .stream() - .filter((s) -> s.startsWith("d")) - .count(); - System.out.println(count); - - //规约 - System.out.println("规约"); - Optional reduce = strings - .stream() - .sorted() - .reduce((s1, s2) -> s1 + "#" + s2); - reduce.ifPresent(System.out::println); - - // 去重 - System.out.println("去重"); - strings.stream().distinct().forEach(s -> System.out.println(s)); - - // 字符串拼接 - String reduce1 = Stream.of("A", "B", "C").reduce("", String::concat); - System.out.println(reduce1); - // 求最小值 - Double reduce2 = Stream.of(-1.5, 1.0, -3.0).reduce(Double.MAX_VALUE, Double::min); - System.out.println(reduce2); - // 求和 - Integer reduce3 = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum); - System.out.println(reduce3); - // 求和 - Optional reduce4 = Stream.of(1, 2, 3, 4).reduce(Integer::sum); - System.out.println(reduce4.get()); - - - } -} diff --git a/Basics/src/com/pack/IntegerPackDemo.java b/Basics/src/com/pack/IntegerPackDemo.java deleted file mode 100644 index a6ce9659..00000000 --- a/Basics/src/com/pack/IntegerPackDemo.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @program JavaBooks - * @description: IntegerPackDemo - * @author: mf - * @create: 2020/02/12 00:08 - */ - -package com.pack; - -/** - * 所有整型包装类对象值的比较必须使用equals方法。 - */ -public class IntegerPackDemo { - public static void main(String[] args) { - Integer x = 3; // 装箱 - int z = x; // 拆箱 - Integer y = 3; - System.out.println(x == y); // true - // 当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时, - // 会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。 - // 所以上述代码中,x和y引用的是相同的Integer对象。 - Integer a = new Integer(3); - Integer b = new Integer(3); - System.out.println(a == b); // false - System.out.println(a.equals(b)); // true - - // 可以看一下整形包装类的equals的源码 - /** - * public boolean equals(Object obj) { - * if (obj instanceof Integer) { - * // 返回参数中的Integer的value,所以最后的比的是内容内容 - * return value == ((Integer)obj).intValue(); - * } - * return false; - * } - */ - - // 缓存池 - // new Integer(123) 与 Integer.valueOf(123) - // new Integer(123) 每次都会新建一个对象 - // Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。前提注意范围 - Integer ab = new Integer(123); - Integer ba = new Integer(123); - System.out.println(ab == ba); // false - // 从缓存中拿123 - Integer aa = Integer.valueOf(123); - Integer bb = Integer.valueOf(123); - System.out.println(aa == bb); // true - /** - * - * public static Integer valueOf(int i) { - * // 判断是否在Integer的范围内 - * if (i >= IntegerCache.low && i <= IntegerCache.high) - * return IntegerCache.cache[i + (-IntegerCache.low)]; - * return new Integer(i); - * } - */ - } -} diff --git a/Basics/src/com/reflect/ConstructorsDemo.java b/Basics/src/com/reflect/ConstructorsDemo.java deleted file mode 100644 index 7cd02529..00000000 --- a/Basics/src/com/reflect/ConstructorsDemo.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @program JavaBooks - * @description: ConstructorsDemo - * @author: mf - * @create: 2020/02/09 17:28 - */ - -package com.reflect; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - -/** - * 通过Class对象可以获取某个类中的:构造方法、成员变量、成员方法;并访问成员; - * - */ -public class ConstructorsDemo { - - public static void main(String[] args) throws Exception { - // 1. 加载Class对象 - Class clazz = Class.forName("com.reflect.Student"); - - // 2. 获取所有公有构造方法 - System.out.println("**********************所有公有构造方法*********************************"); - Constructor[] constructors = clazz.getConstructors(); - for (Constructor constructor : constructors) { - System.out.println(constructor); - } - - // 3. - System.out.println("************所有的构造方法(包括:私有、受保护、默认、公有)***************"); - Constructor[] declaredConstructors = clazz.getDeclaredConstructors(); - for (Constructor declaredConstructor : declaredConstructors) { - System.out.println(declaredConstructor); - } - - // 4. - System.out.println("*****************获取公有、无参的构造方法*******************************"); - Constructor constructor = clazz.getConstructor(); - System.out.println(constructor); - - // 调用构造方法 - Object object = constructor.newInstance(); - System.out.println(object); - - // - System.out.println("******************获取私有构造方法,并调用*******************************"); - Constructor constructor1 = clazz.getDeclaredConstructor(char.class); - System.out.println(constructor1); - // 调用构造方法 - constructor1.setAccessible(true); // 暴力访问 - Object object2 = constructor1.newInstance('买'); - System.out.println(object2); - } -} diff --git a/Basics/src/com/reflect/FieldDemo.java b/Basics/src/com/reflect/FieldDemo.java deleted file mode 100644 index 5c182c38..00000000 --- a/Basics/src/com/reflect/FieldDemo.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @program JavaBooks - * @description: FieldDemo - * @author: mf - * @create: 2020/02/09 20:08 - */ - -package com.reflect; - -import java.lang.reflect.Field; - -public class FieldDemo { - public static void main(String[] args) throws Exception { - // 1. 获取class对象 - Class clazz = Class.forName("com.reflect.Student"); - - // 2. 获取所有字段 - System.out.println("************获取所有公有的字段********************"); - Field[] fields = clazz.getFields(); - for (Field field : fields) { - System.out.println(field); - } - // - System.out.println("************获取所有的字段(包括私有、受保护、默认的)********************"); - Field[] fields1 = clazz.getDeclaredFields(); - for (Field field : fields1) { - System.out.println(field); - } - // - System.out.println("*************获取公有字段**并调用***********************************"); - Field gender = clazz.getField("gender"); - System.out.println(gender); - // 获取一个对象 - Object o = clazz.getConstructor().newInstance(); - gender.set(o, "男"); - Student stu = (Student) o; - System.out.println("验证性别:" + stu.getGender()); - // - System.out.println("*************获取公有字段**并调用***********************************"); - Field name = clazz.getDeclaredField("name"); - System.out.println(name); - name.setAccessible(true); //暴力反射,解除私有限定 - name.set(o, "买"); - System.out.println("验证姓名:" + stu); - - - } -} diff --git a/Basics/src/com/reflect/MethodDemo.java b/Basics/src/com/reflect/MethodDemo.java deleted file mode 100644 index 835c90a5..00000000 --- a/Basics/src/com/reflect/MethodDemo.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @program JavaBooks - * @description: MethodDemo - * @author: mf - * @create: 2020/02/09 23:45 - */ - -package com.reflect; - -import java.lang.reflect.Method; - -public class MethodDemo { - public static void main(String[] args) throws Exception { - // 1. 获取对象 - Class clazz = Class.forName("com.reflect.Student"); - // 2. 获取所有公有方法 - System.out.println("***************获取所有的”公有“方法*******************"); - Method[] methods = clazz.getMethods(); - for (Method method : methods) { - System.out.println(method); - } - System.out.println("***************获取所有的方法,包括私有的*******************"); - Method[] declaredMethods = clazz.getDeclaredMethods(); - for (Method declaredMethod : declaredMethods) { - System.out.println(declaredMethod); - } - System.out.println("***************获取公有的show1()方法*******************"); - Method m = clazz.getMethod("show1", String.class); - System.out.println(m); - // 实例化对象 - Object o = clazz.getConstructor().newInstance(); - m.invoke(o, "买"); - - System.out.println("***************获取私有的show4()方法******************"); - m = clazz.getDeclaredMethod("show4", int.class); - System.out.println(m); - m.setAccessible(true); // 暴力解除 私有 - Object result = m.invoke(o, 20);//需要两个参数,一个是要调用的对象(获取有反射),一个是实参 - System.out.println("返回值:" + result); - } -} diff --git a/Basics/src/com/reflect/ReflectDemo.java b/Basics/src/com/reflect/ReflectDemo.java deleted file mode 100644 index 024f0a2e..00000000 --- a/Basics/src/com/reflect/ReflectDemo.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @program JavaBooks - * @description: ReflectDemo - * @author: mf - * @create: 2020/02/09 16:55 - */ - -package com.reflect; - -/** - * 获取Class对象的三种方式 - * Object ——> getClass(); - * 任何数据类型(包括基本数据类型)都有一个“静态”的class属性 - * 通过Class类的静态方法:forName(String className)(常用) - */ -public class ReflectDemo { - public static void main(String[] args) throws ClassNotFoundException { - // 第一种方式获取Class对象 - Student student = new Student(); // 这一new 产生一个Student对象,一个Class对象。 - Class studentClass = student.getClass(); - System.out.println(studentClass.getName()); // com.reflect.Student - - // 第二种方式获取Class对象 - Class studentClass2 = Student.class; - System.out.println(studentClass == studentClass2); //判断第一种方式获取的Class对象和第二种方式获取的是否是同一个 - - //第三种方式获取Class对象 - Class studentClass3 = Class.forName("com.reflect.Student"); //注意此字符串必须是真实路径,就是带包名的类路径,包名.类名 - System.out.println(studentClass3 == studentClass2); // //判断三种方式是否获取的是同一个Class对象 - - // 三种方式常用第三种,第一种对象都有了还要反射干什么。 - // 第二种需要导入类的包,依赖太强,不导包就抛编译错误。 - // 一般都第三种,一个字符串可以传入也可写在配置文件中等多种方法。 - } -} diff --git a/Basics/src/com/reflect/Student.java b/Basics/src/com/reflect/Student.java deleted file mode 100644 index e9c374a1..00000000 --- a/Basics/src/com/reflect/Student.java +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @program JavaBooks - * @description: Student - * @author: mf - * @create: 2020/02/09 16:55 - */ - -package com.reflect; - -public class Student { - - private String name; - - private Integer age; - - public String gender; - - // 无参数的构造方法 - public Student() { - System.out.println("调用了公有、无参构造方法执行了。。。"); - } - - // 默认的构造方法 - public Student(String str) { - System.out.println("(默认)的构造方法 s = " + str); - } - - // 有一个参数的构造方法 - public Student(char name) { - System.out.println("姓名:" + name); - } - - // 有多个参数的构造方法 - public Student(String name, Integer age) { - this.name = name; - this.age = age; - } - - //受保护的构造方法 - protected Student(boolean n){ - System.out.println("受保护的构造方法 n = " + n); - } - - //私有构造方法 - private Student(int age){ - System.out.println("私有的构造方法 年龄:"+ age); - } - - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getAge() { - return age; - } - - public void setAge(Integer age) { - this.age = age; - } - - public String getGender() { - return gender; - } - - public void setGender(String gender) { - this.gender = gender; - } - - //**************成员方法***************// - public void show1(String s){ - System.out.println("调用了:公有的,String参数的show1(): s = " + s); - } - protected void show2(){ - System.out.println("调用了:受保护的,无参的show2()"); - } - void show3(){ - System.out.println("调用了:默认的,无参的show3()"); - } - private String show4(int age){ - System.out.println("调用了,私有的,并且有返回值的,int参数的show4(): age = " + age); - return "abcd"; - } - - @Override - public String toString() { - return "Student{" + - "name='" + name + '\'' + - ", age=" + age + - ", gender='" + gender + '\'' + - '}'; - } -} diff --git a/Basics/src/com/strings/SIntern.java b/Basics/src/com/strings/SIntern.java deleted file mode 100644 index 3bc3fb8a..00000000 --- a/Basics/src/com/strings/SIntern.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @program JavaBooks - * @description: String.Intern - * @author: mf - * @create: 2020/02/12 01:35 - */ - -package com.strings; - -/** - * 使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象。 - */ -public class SIntern { - public static void main(String[] args) { - // 采用new String创建对象 - String s1 = new String("aaa"); - String s2 = new String("aaa"); - System.out.println(s1 == s2); // false - - // 将s1的对象通过intern放到String Pool中 - String s3 = s1.intern(); - System.out.println(s1.intern() == s3); // true - System.out.println(s1.intern() == s2.intern()); // true - System.out.println(s3 == "aaa"); // true - - - // 如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。 - String s4 = "bbb"; - String s5 = "bbb"; - System.out.println(s4 == s5); // true - } -} diff --git a/Basics/src/com/strings/SbDemo.java b/Basics/src/com/strings/SbDemo.java deleted file mode 100644 index 7b2b2db8..00000000 --- a/Basics/src/com/strings/SbDemo.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @program JavaBooks - * @description: String StringBuffer StringBuilder - * @author: mf - * @create: 2020/02/07 19:13 - */ - -package com.strings; - -public class SbDemo { - public static void main(String[] args) { - // String - String str = "hello"; - long start = System.currentTimeMillis(); - for (int i = 0; i < 100000; i++) { - str += i; // 创建多少个对象,, - } - System.out.println("String: " + (System.currentTimeMillis() - start)); - - // StringBuffer - StringBuffer sb = new StringBuffer("hello"); - long start1 = System.currentTimeMillis(); - for (int i = 0; i < 1000000; i++) { - sb.append(i); - } - System.out.println("StringBuffer: " + (System.currentTimeMillis() - start1)); - - - // StringBuilder - StringBuilder stringBuilder = new StringBuilder("hello"); - long start2 = System.currentTimeMillis(); - for (int i = 0; i < 1000000; i++) { - stringBuilder.append(i); - } - System.out.println("StringBuilder: " + (System.currentTimeMillis() - start2)); - } -} diff --git a/Basics/src/com/transfer/Student.java b/Basics/src/com/transfer/Student.java deleted file mode 100644 index 114f7af9..00000000 --- a/Basics/src/com/transfer/Student.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @program JavaBooks - * @description: Student - * @author: mf - * @create: 2020/02/07 22:01 - */ - -package com.transfer; - -public class Student { - - private String name; - - public Student(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Student{" + - "name='" + name + '\'' + - '}'; - } -} diff --git a/Basics/src/com/transfer/TransferDemo.java b/Basics/src/com/transfer/TransferDemo.java deleted file mode 100644 index 4a940e73..00000000 --- a/Basics/src/com/transfer/TransferDemo.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @program JavaBooks - * @description: TransferDemo - * @author: mf - * @create: 2020/02/07 21:22 - */ - -package com.transfer; - -/** - * Java 程序设计语言总是采用按值调用。 - * 也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。 - */ -public class TransferDemo { - public static void main(String[] args) { - int num1 = 10; - int num2 = 20; - - swap(num1, num2); - System.out.println("num1 = " + num1); - System.out.println("num2 = " + num2); - // 通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 - - - - - } - - - - private static void swap(int a, int b) { - int temp = a; - a = b; - b = temp; - System.out.println("a = " + a); - System.out.println("b = " + b); - } -} - - diff --git a/Basics/src/com/transfer/TransferDemo2.java b/Basics/src/com/transfer/TransferDemo2.java deleted file mode 100644 index a5733624..00000000 --- a/Basics/src/com/transfer/TransferDemo2.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @program JavaBooks - * @description: TransferDemo2 - * @author: mf - * @create: 2020/02/07 22:04 - */ - -package com.transfer; - -public class TransferDemo2 { - - public static void main(String[] args) { - int[] arr = { 1, 2, 3, 4, 5 }; - System.out.println(arr[0]); - change(arr); - System.out.println(arr[0]); - // 法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。 - } - - private static void change(int[] array) { - // 修改数组中的一个元素 - array[0] = 0; - } -} diff --git a/Basics/src/com/transfer/TransferDemo3.java b/Basics/src/com/transfer/TransferDemo3.java deleted file mode 100644 index 4417f4ed..00000000 --- a/Basics/src/com/transfer/TransferDemo3.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @program JavaBooks - * @description: TransferDemo3 - * @author: mf - * @create: 2020/02/07 22:05 - */ - -package com.transfer; - -public class TransferDemo3 { - public static void main(String[] args) { - // 有些程序员(甚至本书的作者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。 - Student s1 = new Student("Mai"); - Student s2 = new Student("Feng"); - swap2(s1, s2); - System.out.println("s1:" + s1.getName()); - System.out.println("s2:" + s2.getName()); - // 方法并没有改变存储在变量 s1 和 s2 中的对象引用。 - // swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝 - } - - private static void swap2(Student x, Student y) { - Student temp = x; - x = y; - y = temp; - System.out.println("x:" + x.getName()); - System.out.println("y:" + y.getName()); - } -} diff --git a/Basics/src/com/type/TypeConvert.java b/Basics/src/com/type/TypeConvert.java deleted file mode 100644 index 2407ca7a..00000000 --- a/Basics/src/com/type/TypeConvert.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @program JavaBooks - * @description: TypeConvert - * @author: mf - * @create: 2020/02/12 01:46 - */ - -package com.type; - -/** - * 隐士转换 - */ -public class TypeConvert { - public static void main(String[] args) { - // 字面量属于 double 类型 - // 不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型 - // Java 不能隐式执行向下转型,因为这会使得精度降低。 - // float f = 1.1; - float f = 1.1f; - - // 因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。 - short s1 = 1; - // s1 = s1 + 1; - - // 但是使用 += 运算符可以执行隐式类型转换。 - s1 += 1; - // 上面的语句相当于将 s1 + 1 的计算结果进行了向下转型: - s1 = (short) (s1 + 1); - - } -} diff --git "a/Collections/HashMap-ConcurrentHashMap\351\235\242\350\257\225\345\277\205\351\227\256.md" "b/Collections/HashMap-ConcurrentHashMap\351\235\242\350\257\225\345\277\205\351\227\256.md" deleted file mode 100644 index 346c46b9..00000000 --- "a/Collections/HashMap-ConcurrentHashMap\351\235\242\350\257\225\345\277\205\351\227\256.md" +++ /dev/null @@ -1,486 +0,0 @@ ---- -title: HashMap & ConcurrentHashMap面试必问 -author: DreamCat -id: 1 -date: 2019-11-17 12:21:38 -tags: Java -categories: Java ---- - -## 引言 - -**HashMap 和 ConcurrentHashMap面试常问,务必理解和掌握** - -## HashMap - -众所周知,HashMap的底层结构是**数组和链表**组成的,不过在jdk1.7和jdk1.8中具体实现略有不同。 - - - -### jdk1.7 - -先看图![](https://i.loli.net/2019/05/08/5cd1d2be77958.jpg) - -再看看1.7的实现![](https://i.loli.net/2019/05/08/5cd1d2bfd6aba.jpg) - -介绍成员变量: - -- 初始化桶大小,因为底层是数组,所以这是数组默认的大小。 -- 桶最大值。 -- 默认的负载因子(0.75) -- table真正存放数据的数组。 -- map存放数量的大小 -- 桶大小,可在构造函数时显式指定。 -- 负载因子,可在构造函数时显式指定。 - -#### 负载因子 - -源代码 - -```java -public HashMap() { - this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); // 桶和负载因子 -} -public HashMap(int initialCapacity, float loadFactor) { - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal initial capacity: " + - initialCapacity); - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + - loadFactor); - this.loadFactor = loadFactor; - threshold = initialCapacity; - init(); -} -``` - -- 给定的默认容量为16,负载因子为0.75. -- Map在使用过程中不断的往里面存放数据,当数量达到了`16 * 0.75 = 12`就需要将当前16的容量进行扩容,而扩容这个过程涉及到rehash(重新哈希)、复制数据等操作,所有非常消耗性能。 -- 因此通常建议能提前预估HashMap的大小最好,尽量的减少扩容带来的额外性能损耗。 - -#### Entry - -`transient Entry[] table = (Entry[]) EMPTY_TABLE;` - -如何定义呢?![](https://i.loli.net/2019/05/08/5cd1d2c08e693.jpg) - -Entry是Hashmap中的一个内部类,从他的成员变量很容易看出: - -- key就是写入时的键 -- value自然就是值 -- 开始的时候就提到HashMap是由数组和链表组成,所以这个next就是用于实现链表结构 -- hash存放的是当前key的hashcode - -#### put - -```java -public V put(K key, V value) { - if (table == EMPTY_TABLE) { - inflateTable(threshold); // 判断数组是否需要初始化 - } - if (key == null) - return putForNullKey(value); // 判断key是否为空 - int hash = hash(key); // 计算hashcode - int i = indexFor(hash, table.length); // 计算桶 - for (Entry e = table[i]; e != null; e = e.next) { - Object k; - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 遍历判断链表中的key和hashcode是否相等,等就替换 - V oldValue = e.value; - e.value = value; - e.recordAccess(this); - return oldValue; - } - } - modCount++; - addEntry(hash, key, value, i); // 没有就添加新的呗 - return null; -} -``` - -- 判断当前数组是否需要初始化 -- 如果key为空,则put一个空值进去 -- 根据key计算hashcode -- 根据计算的hashcode定位index的桶 -- 如果桶是一个链表,则需要遍历判断里面的hashcode、key是否和传入的key相等,如果相等则进行覆盖,并返回原来的值 -- 如果桶是空的,说明当前位置没有数据存入,此时新增一个Entry对象写入当前位置。 - -```java -void addEntry(int hash, K key, V value, int bucketIndex) { - if ((size >= threshold) && (null != table[bucketIndex])) {// 是否扩容 - resize(2 * table.length); - hash = (null != key) ? hash(key) : 0; - bucketIndex = indexFor(hash, table.length); - } - createEntry(hash, key, value, bucketIndex); -} -void createEntry(int hash, K key, V value, int bucketIndex) { - Entry e = table[bucketIndex]; - table[bucketIndex] = new Entry<>(hash, key, value, e); - size++; -} -``` - -- 当调用addEntry写入Entry时需要判断是否需要扩容 -- 如果需要就进行两倍扩充,并将当前的key重新hash并定位。 -- 而在createEntry中会将当前位置的桶传入到新建的桶中,如果当前桶油值就会在位置形成链表。 - -#### get - -```java -public V get(Object key) { - if (key == null) // 判断key是否为空 - return getForNullKey(); // 为空,就返回空值 - Entry entry = getEntry(key); // get entry - return null == entry ? null : entry.getValue(); -} -final Entry getEntry(Object key) { - if (size == 0) { - return null; - } - int hash = (key == null) ? 0 : hash(key); //根据key和hashcode - for (Entry e = table[indexFor(hash, table.length)]; - e != null; - e = e.next) { - Object k; - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } - return null; -} -``` - -- 首先根据key计算hashcode,然后定位具体的桶 -- 判断该位置是否为链表 -- 不是链接就根据key和hashcode是否相等来返回值 -- 为链表则需要遍历直到key和hashcode相等就返回值 -- 啥都没得,就返回null - -### jdk1.8 - -不知道 1.7 的实现大家看出需要优化的点没有? - -其实一个很明显的地方就是链表 - -**当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 `O(N)`。** - -![](https://i.loli.net/2019/05/08/5cd1d2c1c1cd7.jpg) - -看看成员变量 - -```java -static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 -/** - * The maximum capacity, used if a higher value is implicitly specified - * by either of the constructors with arguments. - * MUST be a power of two <= 1<<30. - */ -static final int MAXIMUM_CAPACITY = 1 << 30; -/** - * The load factor used when none specified in constructor. - */ -static final float DEFAULT_LOAD_FACTOR = 0.75f; -static final int TREEIFY_THRESHOLD = 8; -transient Node[] table; -/** - * Holds cached entrySet(). Note that AbstractMap fields are used - * for keySet() and values(). - */ -transient Set> entrySet; -/** - * The number of key-value mappings contained in this map. - */ -transient int size; -``` - -- `TREEIFY_THRESHOLD` 用于判断是否需要将链表转换为红黑树的阈值。 -- HashEntry 修改为 Node。 -- Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 `key value hashcode next` 等数据。 - -#### put - -![](https://i.loli.net/2019/05/08/5cd1d2c378090.jpg) - -- 判断当前桶是否为空,空的就需要初始化(resize中会判断是否进行初始化) -- 根据当前key的hashcode定位到具体的桶中并判断是否为空,为空则表明没有Hash冲突,就直接在当前位置创建一个新桶 -- 如果当前桶油值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,在第8步的时候会统一进行赋值及返回 -- 如果当前桶为红黑树,那就要按照红黑树的方式写入数据 -- 如果是个链表,就需要将当前的key、value封装称一个新节点写入到当前桶的后面形成链表。 -- 接着判断当前链表的大小是否大于预设的阈值,大于就要转换成为红黑树 -- 如果在遍历过程中找到key相同时直接退出遍历。 -- 如果`e != null`就相当于存在相同的key,那就需要将值覆盖。 -- 最后判断是否需要进行扩容。 - -#### get - -```java -public V get(Object key) { - Node e; - return (e = getNode(hash(key), key)) == null ? null : e.value; -} -final Node getNode(int hash, Object key) { - Node[] tab; Node first, e; int n; K k; - if ((tab = table) != null && (n = tab.length) > 0 && - (first = tab[(n - 1) & hash]) != null) { - if (first.hash == hash && // always check first node - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - if ((e = first.next) != null) { - if (first instanceof TreeNode) - return ((TreeNode)first).getTreeNode(hash, key); - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - return null; -} -``` - -- 首先将key hash之后取得所定位的桶 -- 如果桶为空,则直接返回null -- 否则判断桶的第一个位置(有可能是链表、红黑树)的key是否为查询的key,是就直接返回value -- 如果第一个不匹配,则判断它的下一个是红黑树还是链表 -- 红黑树就按照树的查找方式返回值 -- 不然就按照链表的方式遍历匹配返回值 - -**从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 `O(logn)`。** - -## 问题 - -但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。 - -```java -final HashMap map = new HashMap(); -for (int i = 0; i < 1000; i++) { - new Thread(new Runnable() { - @Override - public void run() { - map.put(UUID.randomUUID().toString(), ""); - } - }).start(); -} -``` - -- HashMap扩容的时候会调用resize()方法,就是这里的并发操作容易在一个桶上形成环形链表 -- 这样当获取一个不存在的key时,计算出的index正好是环形链表的下标就会出现死循环。 -- 但是1.7的头插法造成的问题,1.8改变了插入顺序,就解决了这个问题,但是为了内存可见性等安全性,还是需要ConCurrentHashMap - -![](https://i.loli.net/2019/05/08/5cd1d2c4ede54.jpg) - -## 遍历方式 - -还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种: - -```java -Iterator> entryIterator = map.entrySet().iterator(); - while (entryIterator.hasNext()) { - Map.Entry next = entryIterator.next(); - System.out.println("key=" + next.getKey() + " value=" + next.getValue()); - } - -Iterator iterator = map.keySet().iterator(); - while (iterator.hasNext()){ - String key = iterator.next(); - System.out.println("key=" + key + " value=" + map.get(key)); - } -``` - -- 建议使用第一种,同时可以把key value取出。 -- 第二种还需要通过key取一次key,效率较低。 - -## ConCurrentHashMap - -### jdk1.7 - -![](https://i.loli.net/2019/05/08/5cd1d2c5ce95c.jpg) - -- Segment数组 -- HashEntry组成 -- 和HashMap一样,仍然是数组加链表 - -```java -/** - * Segment 数组,存放数据时首先需要定位到具体的 Segment 中。 - */ -final Segment[] segments; -transient Set keySet; -transient Set> entrySet; -``` - -Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下: - -```java -static final class Segment extends ReentrantLock implements Serializable { - private static final long serialVersionUID = 2249069246763182397L; - - // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶 - transient volatile HashEntry[] table; - transient int count; - transient int modCount; - transient int threshold; - final float loadFactor; - -} -``` - -![](https://i.loli.net/2019/05/08/5cd1d2c635c69.jpg) - -- 唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。 -- ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。 -- 不会像HashTable那样不管是put还是get操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。 -- 每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。 - -#### put - -```java -public V put(K key, V value) { - Segment s; - if (value == null) - throw new NullPointerException(); - int hash = hash(key); - int j = (hash >>> segmentShift) & segmentMask; - if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck - (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment - s = ensureSegment(j); - return s.put(key, hash, value, false); -} -``` - -- 通过key定位到Segment,之后在对应的Segment中进行具体的put - -```java -final V put(K key, int hash, V value, boolean onlyIfAbsent) { - HashEntry node = tryLock() ? null : - scanAndLockForPut(key, hash, value); - V oldValue; - try { - HashEntry[] tab = table; - int index = (tab.length - 1) & hash; - HashEntry first = entryAt(tab, index); - for (HashEntry e = first;;) { - if (e != null) { - K k; - if ((k = e.key) == key || - (e.hash == hash && key.equals(k))) { - oldValue = e.value; - if (!onlyIfAbsent) { - e.value = value; - ++modCount; - } - break; - } - e = e.next; - } - else { - if (node != null) - node.setNext(first); - else - node = new HashEntry(hash, key, value, first); - int c = count + 1; - if (c > threshold && tab.length < MAXIMUM_CAPACITY) - rehash(node); - else - setEntryAt(tab, index, node); - ++modCount; - count = c; - oldValue = null; - break; - } - } - } finally { - unlock(); - } - return oldValue; -} -``` - -- 虽然HashEntry中的value是用volatile关键字修饰的,但是并不能保证并发的原子性,所以put操作仍然需要加锁处理。 -- 首先第一步的时候会尝试获取锁,如果获取失败肯定就是其他线程存在竞争,则利用 `scanAndLockForPut()` 自旋获取锁。 - -![](https://i.loli.net/2019/05/08/5cd1d2cc3c982.jpg) - -- 尝试获取自旋锁 -- 如果重试的次数达到了`MAX_SCAN_RETRIES` 则改为阻塞锁获取,保证能获取成功。 - -![](https://i.loli.net/2019/05/08/5cd1d2cd25c37.jpg) - -- 将当前的Segment中的table通过key的hashcode定位到HashEntry -- 遍历该HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value -- 不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容 -- 最后会解除在1中所获取当前Segment的所。 - -#### get方法 - -```java -public V get(Object key) { - Segment s; // manually integrate access methods to reduce overhead - HashEntry[] tab; - int h = hash(key); - long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; - if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && - (tab = s.table) != null) { - for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile - (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); - e != null; e = e.next) { - K k; - if ((k = e.key) == key || (e.hash == h && key.equals(k))) - return e.value; - } - } - return null; -} -``` - -- 只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。 -- 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。 -- ConcurrentHashMap 的 get 方法是非常高效的,**因为整个过程都不需要加锁**。 - -### jdk1.8 - -**那就是查询遍历链表效率太低。** - -![](https://i.loli.net/2019/05/08/5cd1d2ce33795.jpg) - -**其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性** - -![](https://i.loli.net/2019/05/08/5cd1d2ceebe02.jpg) - -- 也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。 -- 其中的 `val next` 都用了 volatile 修饰,保证了可见性。 - -#### put - -![](https://i.loli.net/2019/05/08/5cd1d2cfc3293.jpg) - -- 根据key计算出hashcode -- 判断是否需要进行初始化 -- f即为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功。 -- 如果当前位置的`hashcode == MOVED == -1`,则需要进行扩容 -- 如果都不满足,则利用synchronized锁写入数据 -- 如果数量大于`TREEIFY_THRESHOLD` 则要转换为红黑树。 - -#### get - -![](https://i.loli.net/2019/05/08/5cd1d2d22c6cb.jpg) - -- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 -- 如果是红黑树那就按照树的方式获取值。 -- 就不满足那就按照链表的方式遍历获取值。 - -1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(`O(logn)`),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。 - -## 总结 - -套路: - -- 谈谈你理解的 HashMap,讲讲其中的 get put 过程。 -- 1.8 做了什么优化? -- 是线程安全的嘛? -- 不安全会导致哪些问题? -- 如何解决?有没有线程安全的并发容器? -- ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做? \ No newline at end of file diff --git "a/Collections/Java\351\235\242\347\273\217-Java\351\233\206\345\220\210\346\241\206\346\236\266.md" "b/Collections/Java\351\235\242\347\273\217-Java\351\233\206\345\220\210\346\241\206\346\236\266.md" deleted file mode 100644 index 661264ca..00000000 --- "a/Collections/Java\351\235\242\347\273\217-Java\351\233\206\345\220\210\346\241\206\346\236\266.md" +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Java面经-Java集合框架 -author: DreamCat -id: 1 -date: 2019-10-29 17:55:21 -tags: Java -categories: Java ---- - -## 引言 - -**Java的集合框架,包括常见的面试源码解析等...** - - - -## Collection类关系图 - -![类关系图](https://www.pdai.tech/_images/java_collections_overview.png) - -### 介绍 - -容器,就是可以容纳其他Java对象的对象。其始于JDK 1.2。 优点: - -- 降低编程难度 -- 提高程序性能 -- 提高API间的互操作性 -- 降低学习难度 -- 降低设计和实现相关API的难度 -- 增加程序的重用性 - -### Collection - -容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 - -#### Set - -##### TreeSet - -基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如HashSet,HashSet查找的时间复杂为O(1),TreeSet则为O(logN)。 - -##### HashSet - -基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用Iterator遍历HashSet得到结果是不确定的。 - -##### LinkedHashSet - -具有HashSet的查找效率,且内部使用双向链表维护元素的插入顺序。 - -#### List - -##### ArrayList - -基于动态数组实现,支持随机访问。 - -##### Vector - -和ArrayList类似,但它是线程安全的。 - -##### LinkedList - -基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList还可以用作栈、队列和双向队列。 - -#### Queue - -##### LinkedList - -可以用来实现双向队列。 - -##### PriorityQueue - -基于堆结构实现,可以用它实现优先队列。 - -#### Map - -##### TreeMap - -基于红黑树实现 - -##### HashMap - -基于哈希表实现 - -##### HashTable - -- 和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。 -- 它是遗留类,不应该去使用它。 -- 现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。 - -##### LinkedHashMap - -使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 \ No newline at end of file diff --git a/DesignPattern/src/TemplateMode.java b/DesignPattern/src/TemplateMode.java deleted file mode 100644 index 09633856..00000000 --- a/DesignPattern/src/TemplateMode.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @program JavaBooks - * @description: 模仿方法 - * @author: mf - * @create: 2019/10/03 09:58 - */ -/* -定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的 -例如: -1- 去银行办业务,银行给我们提供了一个模板就是:先取号,排对,办理业务(核心部分我们子类完成),给客服人员评分,完毕。 -这里办理业务是属于子类来完成的,其他的取号,排队,评分则是一个模板。 -2- 去餐厅吃饭,餐厅给提供的一套模板就是:先点餐,等待,吃饭(核心部分我们子类完成),买单 -这里吃饭是属于子类来完成的,其他的点餐,买单则是餐厅提供给我们客户的一个模板。 - -实现一些操作时,整体步骤很固定,但是呢。就是其中一小部分容易变,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。 - */ - -// 举个例子, 银行 -abstract class BankTemplateMethod { - // 1. 取号排队 - public void takeNumber() { - System.out.println("取号排队..."); - } - - // 2. 每个子类不同的业务实现,各由子类来实现 - abstract void transact(); - - // 3. 评价 - public void evaluate() { - System.out.println("反馈评价..."); - } - - public void process() { - takeNumber(); - transact(); - evaluate(); - } -} - -// 具体的子类 -class DrawMoney extends BankTemplateMethod { - - @Override - void transact() { - System.out.println("我要取款..."); - } -} - -public class TemplateMode { - public static void main(String[] args) { - BankTemplateMethod drawMoney = new DrawMoney(); - drawMoney.process(); - } -} diff --git "a/Interview/linux/linux-\345\237\272\347\241\200.md" "b/Interview/linux/linux-\345\237\272\347\241\200.md" deleted file mode 100644 index 78fdb426..00000000 --- "a/Interview/linux/linux-\345\237\272\347\241\200.md" +++ /dev/null @@ -1,704 +0,0 @@ ---- -title: linux-基础 -author: DreamCat -id: 1 -date: 2019-11-12 15:23:30 -tags: Linux -categories: 知识 ---- - -## 引言 - -**linux-基础** - -## 常用操作以及概念 - -### 快捷键 - -- Tab: 命令和文件名补全; -- Ctrl+C: 中断正在运行的程序; -- Ctrl+D: 结束键盘输入(End Of File,EOF) - -### 求助 - - - -#### --help - -指令的基本用法与选项介绍。 - -#### man - -man 是 manual 的缩写,将指令的具体信息显示出来。 - -#### info - -info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以进行跳转。 - -#### doc - -/usr/share/doc 存放着软件的一整套说明文件。 - -### 关机 - -#### who - -在关机前需要先使用 who 命令查看有没有其它用户在线。 - -#### sync - -为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。 - -#### shutdown - -```bash -## shutdown [-krhc] 时间 [信息] --k : 不会关机,只是发送警告信息,通知所有在线的用户 --r : 将系统的服务停掉后就重新启动 --h : 将系统的服务停掉后就立即关机 --c : 取消已经在进行的 shutdown 指令内容 -``` - -### PATH - -可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。 - -`/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin` - -### sudo - -sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令. - -### 包管理工具 - -RPM 和 DPKG 为最常见的两类软件包管理工具: - -- RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。 -- 与 RPM 进行竞争的是基于 Debian 操作系统 (Ubuntu) 的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。 - -YUM 基于 RPM,具有依赖管理功能,并具有软件升级的功能。 - -### 发行版 - -Linux 发行版是 Linux 内核及各种应用软件的集成版本。 - -| 基于的包管理工具 | 商业发行版 | 社区发行版 | -| :--------------: | :--------: | :-------------: | -| RPM | Red Hat | Fedora / CentOS | -| DPKG | Ubuntu | Debian | - -### VIM - -- 一般指令模式(Command mode): VIM 的默认模式,可以用于移动游标查看内容; -- 编辑模式(Insert mode): 按下 "i" 等按键之后进入,可以对文本进行编辑; -- 指令列模式(Bottom-line mode): 按下 ":" 按键之后进入,用于保存退出等操作。 - -在指令列模式下,有以下命令用于离开或者保存文件。 - -| 命令 | 作用 | -| :--: | :----------------------------------------------------------: | -| :w | 写入磁盘 | -| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 | -| :q | 离开 | -| :q! | 强制离开不保存 | -| :wq | 写入磁盘后离开 | -| :wq! | 强制写入磁盘后离开 | - -## 磁盘 - -### 磁盘接口 - -#### IDE - -IDE(ATA)全称 Advanced Technology Attachment,接口速度最大为 133MB/s,因为并口线的抗干扰性太差,且排线占用空间较大,不利电脑内部散热,已逐渐被 SATA 所取代。 - -#### SATA - -SATA 全称 Serial ATA,也就是使用串口的 ATA 接口,抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能。SATA-II 的接口速度为 300MiB/s,而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。 - -#### SCSI - -SCSI 全称是 Small Computer System Interface(小型机系统接口),经历多代的发展,从早期的 SCSI-II 到目前的 Ultra320 SCSI 以及 Fiber-Channel(光纤通道),接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。 - -#### SAS - -SAS(Serial Attached SCSI)是新一代的 SCSI 技术,和 SATA 硬盘相同,都是采取序列式技术以获得更高的传输速度,可达到 6Gb/s。此外也透过缩小连接线改善系统内部空间等。 - -## 分区 - -### 分区表 - -磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。 - -#### MBR - -MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中主要开机记录占 446 bytes,分区表占 64 bytes。 - -分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它使用其它扇区用记录额外的分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。 - -Linux 也把分区当成文件,分区文件的命名方式为: 磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。 - -#### GPT - -不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA),LBA 默认大小为 512 bytes。 - -GPT 第 1 个区块记录了主要开机记录(MBR),紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。 - -GPT 没有扩展分区概念,都是主分区,每个 LAB 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。 - -MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 233 TB = 8 ZB。 - -### 开机检测程序 - -#### BIOS - -BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中。 - -BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的主要开机记录(MBR),由主要开机记录(MBR)执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。 - -主要开机记录(MBR)中的开机管理程序提供以下功能: 选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。 - -下图中,第一扇区的主要开机记录(MBR)中的开机管理程序提供了两个选单: M1、M2,M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。 - -安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉主要开机记录(MBR),而 Linux 可以选择将开机管理程序安装在主要开机记录(MBR)或者其它分区的启动扇区,并且可以设置开机管理程序的选单。 - -#### UEFI - -BIOS 不可以读取 GPT 分区表,而 UEFI 可以。 - -## 文件系统 - -### 分区与文件系统 - -对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统。 - -### 组成 - -最主要的几个组成部分如下: - -- inode: 一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号; -- block: 记录文件的内容,文件太大时,会占用多个 block。 -- superblock: 记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等; -- block bitmap: 记录 block 是否被使用的位域。 - -### 文件读取 - -对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。 - -### 磁盘碎片 - -指一个文件内容所在的 block 过于分散。 - -### Block - -在 Ext2 文件系统中所支持的 block 大小有 1K,2K 及 4K 三种,不同的大小限制了单个文件和文件系统的最大大小。 - -一个 block 只能被一个文件所使用,未使用的部分直接浪费了。因此如果需要存储大量的小文件,那么最好选用比较小的 block。 - -### inode - -inode 具体包含以下信息: - -- 权限 (read/write/excute); -- 拥有者与群组 (owner/group); -- 容量; -- 建立或状态改变的时间 (ctime); -- 最近一次的读取时间 (atime); -- 最近修改的时间 (mtime); -- 定义文件特性的旗标 (flag),如 SetUID...; -- 该文件真正内容的指向 (pointer)。 - -inode 具有以下特点: - -- 每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes); -- 每个文件都仅会占用一个 inode。 - -inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块记录引用信息。 - -### 目录 - -建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。 - -可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 - -### 日志 - -如果突然断电,那么文件系统会发生错误,例如断电前只修改了 block bitmap,而还没有将数据真正写入 block 中。 - -ext3/ext4 文件系统引入了日志功能,可以利用日志来修复文件系统。 - -### 挂载 - -挂载利用目录作为文件系统的进入点,也就是说,进入目录之后就可以读取文件系统的数据。 - -### 目录配置 - -为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下: - -- / (root, 根目录) -- /usr (unix software resource): 所有系统默认软件都会安装到这个目录; -- /var (variable): 存放系统或程序运行过程中的数据文件。 - -## 文件 - -### 文件属性 - -用户分为三种: 文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。 - -使用 ls 查看一个文件时,会显示一个文件的信息,例如 `drwxr-xr-x. 3 root root 17 May 6 00:14 .config`,对这个信息的解释如下: - -- drwxr-xr-x: 文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段 -- 3: 链接数 -- root: 文件拥有者 -- root: 所属群组 -- 17: 文件大小 -- May 6 00:14: 文件最后被修改的时间 -- .config: 文件名 - -常见的文件类型及其含义有: - -- d: 目录 -- -: 文件 -- l: 链接文件 - -9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。 - -文件时间有以下三种: - -- modification time (mtime): 文件的内容更新就会更新; -- status time (ctime): 文件的状态(权限、属性)更新就会更新; -- access time (atime): 读取文件时就会更新。 - -### 文件与目录的基本操作 - -#### ls - -列出文件或者目录的信息,目录的信息就是其中包含的文件。 - -```bash -## ls [-aAdfFhilnrRSt] file|dir --a : 列出全部的文件 --d : 仅列出目录本身 --l : 以长数据串行列出,包含文件的属性与权限等等数据 - -``` - -#### cd - -更新当前目录 - -`cd [相对路径或者绝对路径]` - -#### mkdir - -创建目录 - -```bash -## mkdir [-mp] 目录名称 --m : 配置目录权限 --p : 递归创建目录 -``` - -#### rmdir - -删除目录,目录必须为空 - -```bash -rmdir [-p] 目录名称 --p : 递归删除目录 -``` - -#### touch - -更新文件时间或者建立新文件 - -```bash -## touch [-acdmt] filename --a : 更新 atime --c : 更新 ctime,若该文件不存在则不建立新文件 --m : 更新 mtime --d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间" --t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm] - -``` - -#### cp - -复制文件 - -如果源文件有两个以上,则目的文件一定要是目录才行。 - -```bash -cp [-adfilprsu] source destination --a : 相当于 -dr --preserve=all 的意思,至于 dr 请参考下列说明 --d : 若来源文件为链接文件,则复制链接文件属性而非文件本身 --i : 若目标文件已经存在时,在覆盖前会先询问 --p : 连同文件的属性一起复制过去 --r : 递归持续复制 --u : destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制 ---preserve=all : 除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了 - -``` - -#### rm - -删除文件 - -```bash -## rm [-fir] 文件或目录 --r : 递归删除 -``` - -#### mv - -移动文件 - -```bash -## mv [-fiu] source destination -## mv [options] source1 source2 source3 .... directory --f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖 -``` - -### 修改权限 - -可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r : 4、w : 2、x : 1。 - -`## chmod [-R] xyz dirname/filename` - -示例: 将 .bashrc 文件的权限修改为 -rwxr-xr--。 - -`## chmod 754 .bashrc` - -也可以使用符号来设定权限。 - -```bash -## chmod [ugoa] [+-=] [rwx] dirname/filename -- u: 拥有者 -- g: 所属群组 -- o: 其他人 -- a: 所有人 -- +: 添加权限 -- -: 移除权限 -- =: 设定权限 -``` - -示例: 为 .bashrc 文件的所有用户添加写权限。 - -`## chmod a+w .bashrc` - -### 文件默认权限 - -- 文件默认权限: 文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。 -- 目录默认权限: 目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。 - -可以通过 umask 设置或者查看文件的默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r--。 - -### 目录的权限 - -文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。 - -目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。 - -### 链接 - -```bash -## ln [-sf] source_filename dist_filename --s : 默认是 hard link,加 -s 为 symbolic link --f : 如果目标文件存在时,先删除目标文件 -``` - -### 实体链接 - -在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode。 - -删除任意一个条目,文件还是存在,只要引用数量不为 0。 - -有以下限制: 不能跨越文件系统、不能对目录进行链接。 - -```bash -## ln /etc/crontab . -## ll -i /etc/crontab crontab -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab -``` - -### 符号链接 - -符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。 - -当源文件被删除了,链接文件就打不开了。 - -可以为目录建立链接。 - -```bash -## ll -i /etc/crontab /root/crontab2 -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab -53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab -``` - -### 获取文件内容 - -#### cat - -取得文件内容。 - -```bash -## cat [-AbEnTv] filename --n : 打印出行号,连同空白行也会有行号,-b 不会 -``` - -#### tac - -是 cat 的反向操作,从最后一行开始打印。 - -#### more - -和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。 - -#### less - -和 more 类似,但是多了一个向前翻页的功能。 - -#### head - -取得文件前几行。 - -```bash -## head [-n number] filename --n : 后面接数字,代表显示几行的意思 -``` - -#### tail - -是head的反向操作,之水取得是后几行 - -#### od - -以字符或者十六进制的形式显示二进制文件。 - -### 指令与文件搜索 - -#### which - -指令搜索 - -```bash -## which [-a] command --a : 将所有指令列出,而不是只列第一个 -``` - -#### whereis - -文件搜索。速度比较快,因为它只搜索几个特定的目录。 - -```bash -## whereis [-bmsu] dirname/filename -``` - -#### locate - -文件搜索。可以用关键字或者正则表达式进行搜索。 - -locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。 - -```bash -## locate [-ir] keyword --r: 正则表达式 -``` - -#### find - -文件搜索。可以使用文件的属性和权限进行搜索。 - -```bash -## find [basedir] [option] -example: find . -name "shadow*" -``` - -## 压缩与打包 - -### 压缩文件名 - -#### gzip - -gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。 - -经过 gzip 压缩过,源文件就不存在了。 - -有 9 个不同的压缩等级可以使用。 - -可以使用 zcat、zmore、zless 来读取压缩文件的内容。 - -```bash -$ gzip [-cdtv#] filename --c : 将压缩的数据输出到屏幕上 --d : 解压缩 --t : 检验压缩文件是否出错 --v : 显示压缩比等信息 --## : ## 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为 6 -``` - -#### bzip2 - -提供比 gzip 更高的压缩比。 - -查看命令: bzcat、bzmore、bzless、bzgrep。 - -#### xz - -提供比 bzip2 更佳的压缩比。 - -可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。 - -查看命令: xzcat、xzmore、xzless、xzgrep。 - -### 打包 - -压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。 - -```bash -$ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename... ==打包压缩 -$ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看 -$ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩 --z : 使用 zip; --j : 使用 bzip2; --J : 使用 xz; --c : 新建打包文件; --t : 查看打包文件里面有哪些文件; --x : 解打包或解压缩的功能; --v : 在压缩/解压缩的过程中,显示正在处理的文件名; --f : filename: 要处理的文件; --C 目录 : 在特定目录解压缩。 -``` - -| 使用方式 | 命令 | -| :------: | ----------------------------------------------------- | -| 打包压缩 | tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称 | -| 查 看 | tar -jtv -f filename.tar.bz2 | -| 解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 | - -## bash - -可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。 - -### 特性 - -- 命令历史:记录使用过的命令 -- 命令与文件补全:快捷键:tab -- shell scripts -- 通配符: 例如 ls -l /usr/bin/X* 列出 /usr/bin 下面所有以 X 开头的文件 - -### 变量操作 - -对一个变量赋值直接使用 =。 - -对变量取用需要在变量前加上 $ ,也可以用 ${} 的形式; - -输出变量使用 echo 命令。 - -```bash -$ x=abc -$ echo $x -$ echo ${x} -``` - -变量内容如果有空格,必须使用双引号或者单引号。 - -- 双引号内的特殊字符可以保留原本特性,例如 x="lang is $LANG",则 x 的值为 lang is zh_TW.UTF-8; -- 单引号内的特殊字符就是特殊字符本身,例如 x='lang is $LANG',则 x 的值为 lang is $LANG。 - -可以使用 `指令` 或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version=$(uname -r),则 version 的值为 4.15.0-22-generic。 - -可以使用 `指令` 或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version=$(uname -r),则 version 的值为 4.15.0-22-generic。 - -Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令: - -```bash -$ declare [-aixr] variable --a : 定义为数组类型 --i : 定义为整数类型 --x : 定义为环境变量 --r : 定义为 readonly 类型 -``` - -对数组操作 - -```bash -$ array[1]=a -$ array[2]=b -$ echo ${array[1]} -``` - -## 正则 - -g/re/p(globally search a regular expression and print),使用正则表示式进行全局查找并打印。 - -```bash -$ grep [-acinv] [--color=auto] 搜寻字符串 filename --c : 统计个数 --i : 忽略大小写 --n : 输出行号 --v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行 ---color=auto : 找到的关键字加颜色显示 -``` - -示例: 把含有 the 字符串的行提取出来(注意默认会有 --color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串) - -```bash -$ grep -n 'the' regular_express.txt -8:I can't finish the test. -12:the symbol '*' is represented as start. -15:You are the best is mean you are the no. 1. -16:The world Happy is the same with "glad". -18:google is the best tools for search keyword -``` - -## 进程管理 - -### 查看进程 - -#### ps - -查看某个时间点的进程信息 - -示例一: 查看自己的进程`ps -l` - -示例二: 查看系统所有进程`ps aux` - -示例三: 查看特定的进程`ps aux | grep python` - -#### top - -实时显示进程信息 - -示例: 两秒钟刷新一次`top -d 2` - -#### pstree - -查看进程树 - -示例: 查看所有进程树`pstree -A` - -#### netstat - -查看占用端口的进程 - -示例: 查看特定端口的进程`netstat -anp | grep port` - -### 孤儿进程 - -一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。 - -孤儿进程将被init进程(进程号为1)所收养,并有init进程对它们完成状态收集工作。 - -由于孤儿进程会被init进程收养,所以孤儿进程不会对系统造成危害。 - -### 僵尸进程 - -一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过wait()或者waitpid()获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用wait()或waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。 - -僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。 - -系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。 - -要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。 \ No newline at end of file diff --git "a/Interview/mysql/Mysql-\351\235\242\350\257\225\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" "b/Interview/mysql/Mysql-\351\235\242\350\257\225\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" deleted file mode 100644 index 15ff7b10..00000000 --- "a/Interview/mysql/Mysql-\351\235\242\350\257\225\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: Mysql-面试常见的问题 -author: DreamCat -id: 1 -date: 2019-11-24 12:37:38 -tags: Sql -categories: Sql ---- -## 引言 - -> Mysql,那可是老生常谈了,对于后端同学,那是必须要掌握的呀。 - - - -## 常见问题 - -### 数据库引擎innodb与myisam的区别? - -[InnoDB和MyISAM比较](http://dreamcat.ink/2019/11/15/sql-notes/1/) - -### 4大特性 - -[Mysql的ACID原理](http://dreamcat.ink/2019/11/20/sql-notes/1/) - -### 并发事务带来的问题 - -#### 脏读 - -![](https://www.pdai.tech/_images/pics/dd782132-d830-4c55-9884-cfac0a541b8e.png) - -#### 丢弃修改 - -T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最 终结果A=19,事务1的修改被丢失。 - -![](https://www.pdai.tech/_images/pics/88ff46b3-028a-4dbb-a572-1f062b8b96d3.png) - -### 不可重复读 - -T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 - -![](https://www.pdai.tech/_images/pics/c8d18ca9-0b09-441a-9a0c-fb063630d708.png) - -#### 幻读 - -T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 - -![](https://www.pdai.tech/_images/pics/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png) - -#### 不可重复度和幻读区别: - -不可重复读的重点是修改,幻读的重点在于新增或者删除。 - -例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操 作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。 - -例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所 有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记 录就变为了5条,这样就导致了幻读。 - -### 数据库的隔离级别 - -1. 未提交读,事务中发生了修改,即使没有提交,其他事务也是可见的,比如对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100.**可能会导致脏读、幻读或不可重复读** -2. 提交读,对于一个事务从开始直到提交之前,所做的任何修改是其他事务不可见的,举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取的A是50,刚读取完,A就被修改成100,这个时候另一个事务再进行读取发现A就突然变成100了;**可以阻止脏读,但是幻读或不可重复读仍有可能发生** -3. 可重复读,就是对一个记录读取多次的记录是相同的,比如对于一个数A读取的话一直是A,前后两次读取的A是一致的;**可以阻止脏读和不可重复读,但幻读仍有可能发生。** -4. 可串行化读,在并发情况下,和串行化的读取的结果是一致的,没有什么不同,比如不会发生脏读和幻读;**该级别可以防止脏读、不可重复读以及幻读。** - -| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | -| ---------------- | ---- | ---------- | ------ | -| READ-UNCOMMITTED | √ | √ | √ | -| READ-COMMITTED | × | √ | √ | -| REPEATABLE-READ | × | × | √ | -| SERIALIZABLE | × | × | × | - -MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过 - -命令来查看。我们可以通过`SELECT@@tx_isolation;` - -这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别 下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以 说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要 求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。 - -因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内 容):,但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。 - -InnoDB 存储引擎在 分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。 - -### 为什么使用索引? - -- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 -- 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。 -- 帮助服务器避免排序和临时表 -- 将随机IO变为顺序IO -- 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。 - -### 索引这么多优点,为什么不对表中的每一个列创建一个索引呢? - -- 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。 -- 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立簇索引,那么需要的空间就会更大。 -- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加 - -### 索引如如何提高查询速度的? - -- 将无序的数据变成相对有序的数据(就像查目一样) - -### 使用索引的注意事项? - -- 在经常需要搜索的列上,可以加快搜索的速度; -- 在经常使用在where子句中的列上面创建索引,加快条件的判断速度。 -- 在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间 -- 在中到大型表索引都是非常有效的,但是特大型表的维护开销会很大,不适合建索引 -- 在经常用到连续的列上,这些列主要是由一些外键,可以加快连接的速度 -- 避免where子句中对字段施加函数,这会造成无法命中索引 -- 在使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。 -- **将打算加索引的列设置为NOT NULL,否则将导致引擎放弃使用索引而进行全表扫描** -- 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 -- 在使用limit offset查询缓存时,可以借助索引来提高性能。 - -### Mysql索引主要使用的两种数据结构 - -- 哈希索引,对于哈希索引来说,底层的数据结构肯定是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引 -- BTree索引,Mysql的BTree索引使用的是B树中的B+Tree但对于主要的两种存储引擎(MyISAM和InnoDB)的实现方式是不同的。 - -### MyISAM和InnoDB实现BTree索引方式的区别 - -- MyISAM,B+Tree叶节点的data域存放的是数据记录的地址,在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的key存在,则取出其data域的值,然后以data域的值为地址读区相应的数据记录,这被称为“非聚簇索引” -- InnoDB,其数据文件本身就是索引文件,相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的节点data域保存了完整的数据记录,这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引”或者聚集索引,而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方,在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,在走一遍主索引。因此,在设计表的时候,不建议使用过长的字段为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。 - -### 数据库结构优化 - -- 范式优化: 比如消除冗余(节省空间。。) -- 反范式优化:比如适当加冗余等(减少join) -- 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时 候,我们可以控制在一个月的范围内。 -- 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读; -- 拆分表:分区将数据在物理上分隔开,不同分区的数据可以制定保存在处于不同磁盘上的数据文件里。这样,当对这个表进行查询时,只需要在表分区中进行扫描,而不必进行全表扫描,明显缩短了查询时间,另外处于不同磁盘的分区也将对这个表的数据传输分散在不同的磁盘I/O,一个精心设置的分区可以将数据传输对磁盘I/O竞争均匀地分散开。对数据量大的时时表可采取此方法。可按月自动建表分区。 -- 拆分其实又分垂直拆分和水平拆分: - - 案例: 简单购物系统暂设涉及如下表: - - 1.产品表(数据量10w,稳定) - - 2.订单表(数据量200w,且有增长趋势) - - 3.用户表 (数据量100w,且有增长趋势) - - 以mysql为例讲述下水平拆分和垂直拆分,mysql能容忍的数量级在百万静态数据可以到千万 - - **垂直拆分:** - - 解决问题:表与表之间的io竞争 - - 不解决问题:单表中数据量增长出现的压力 - - 方案: 把产品表和用户表放到一个server上 订单表单独放到一个server上 - - **水平拆分:** - - 解决问题:单表中数据量增长出现的压力 - - 不解决问题:表与表之间的io争夺 - - 方案:**用户表** 通过性别拆分为男用户表和女用户表,**订单表** 通过已完成和完成中拆分为已完成订单和未完成订单,**产品表** 未完成订单放一个server上,已完成订单表盒男用户表放一个server上,女用户表放一个server上(女的爱购物 哈哈)。 - -### 主键 超键 候选键 外键是什么 - -#### 定义 - -超键:在关系中能唯一标识元组的属性集称为关系模式的超键 - -候选键:不含有多余属性的超键称为候选键。也就是在候选键中,若再删除属性,就不是键了! - -主键:用户选作元组标识的一个候选键程序主键 - -外键:如果关系模式R中属性K是其它模式的主键,那么k在模式R中称为外键。 - -#### 举例 - -| 学号 | 姓名 | 性别 | 年龄 | 系别 | 专业 | -| -------- | ------ | ---- | ---- | ------ | -------- | -| 20020612 | 李辉 | 男 | 20 | 计算机 | 软件开发 | -| 20060613 | 张明 | 男 | 18 | 计算机 | 软件开发 | -| 20060614 | 王小玉 | 女 | 19 | 物理 | 力学 | -| 20060615 | 李淑华 | 女 | 17 | 生物 | 动物学 | -| 20060616 | 赵静 | 男 | 21 | 化学 | 食品化学 | -| 20060617 | 赵静 | 女 | 20 | 生物 | 植物学 | - -1. 超键:于是我们从例子中可以发现 学号是标识学生实体的唯一标识。那么该元组的超键就为学号。除此之外我们还可以把它跟其他属性组合起来,比如:(`学号`,`性别`),(`学号`,`年龄`) -2. 候选键:根据例子可知,学号是一个可以唯一标识元组的唯一标识,因此学号是一个候选键,实际上,候选键是超键的子集,比如 (学号,年龄)是超键,但是它不是候选键。因为它还有了额外的属性。 -3. 主键:简单的说,例子中的元组的候选键为学号,但是我们选定他作为该元组的唯一标识,那么学号就为主键。 -4. 外键是相对于主键的,比如在学生记录里,主键为学号,在成绩单表中也有学号字段,因此学号为成绩单表的外键,为学生表的主键。 - -#### 总结 - -**主键为候选键的子集,候选键为超键的子集,而外键的确定是相对于主键的。** - -### drop,delete与truncate的区别 - -drop直接删掉表;truncate删除表中数据,再插入时自增长id又从1开始 ;delete删除表中数据,可以加where字句。 - -1. DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把单独的删除操作记录记入日志保存,删除行是不能恢复的。并且在删除的过程中不会激活与表有关的删除触发器。执行速度快。 -2. 表和索引所占空间。当表被TRUNCATE 后,这个表和索引所占用的空间会恢复到初始大小,而DELETE操作不会减少表或索引所占用的空间。drop语句将表所占用的空间全释放掉。 -3. 一般而言,drop > truncate > delete -4. 应用范围。TRUNCATE 只能对TABLE;DELETE可以是table和view -5. TRUNCATE 和DELETE只删除数据,而DROP则删除整个表(结构和数据)。 -6. truncate与不带where的delete :只删除数据,而不删除表的结构(定义)drop语句将删除表的结构被依赖的约束(constrain),触发器(trigger)索引(index);依赖于该表的存储过程/函数将被保留,但其状态会变为:invalid。 -7. delete语句为DML(Data Manipulation Language),这个操作会被放到 rollback segment中,事务提交后才生效。如果有相应的 tigger,执行的时候将被触发。 -8. truncate、drop是DDL(Data Define Language),操作立即生效,原数据不放到 rollback segment中,不能回滚 -9. 在没有备份情况下,谨慎使用 drop 与 truncate。要删除部分数据行采用delete且注意结合where来约束影响范围。回滚段要足够大。要删除表用drop;若想保留表而将表中数据删除,如果于事务无关,用truncate即可实现。如果和事务有关,或老是想触发trigger,还是用delete。 -10. Truncate table 表名 速度快,而且效率高,因为: truncate table 在功能上与不带 WHERE 子句的 DELETE 语句相同:二者均删除表中的全部行。但 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少。DELETE 语句每次删除一行,并在事务日志中为所删除的每行记录一项。TRUNCATE TABLE 通过释放存储表数据所用的数据页来删除数据,并且只在事务日志中记录页的释放。 -11. TRUNCATE TABLE 删除表中的所有行,但表结构及其列、约束、索引等保持不变。新行标识所用的计数值重置为该列的种子。如果想保留标识计数值,请改用 DELETE。如果要删除表定义及其数据,请使用 DROP TABLE 语句。 -12. 对于由 FOREIGN KEY 约束引用的表,不能使用 TRUNCATE TABLE,而应使用不带 WHERE 子句的 DELETE 语句。由于 TRUNCATE TABLE 不记录在日志中,所以它不能激活触发器。 - -### 视图的作用,视图可以更改么? - -视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的sql操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。 - -视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by 则对视图再次order by将被覆盖。 - -创建视图:`create view xxx as xxxx` - -对于某些视图比如未使用联结子查询分组聚集函数Distinct Union等,是可以对其更新的,对视图的更新将对基表进行更新;但是视图主要用于简化检索,保护数据,并不用于更新,而且大部分视图都不可以更新。 - -### 数据库范式 - -#### 第一范式 - -在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。 所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,**第一范式就是无重复的列**。 - -#### 第二范式 - -第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主关键字或主键、主码。 第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,**第二范式就是非主属性非部分依赖于主关键字**。 - -#### 第三范式 - -满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。(我的理解是消除冗余) - -### 什么是覆盖索引? - -如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称 之为“覆盖索引”。我们知道在InnoDB存储引 擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就 会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! \ No newline at end of file diff --git "a/Interview/mysql/Mysql\344\270\255ACID\347\232\204\345\216\237\347\220\206.md" "b/Interview/mysql/Mysql\344\270\255ACID\347\232\204\345\216\237\347\220\206.md" deleted file mode 100644 index 3e69056a..00000000 --- "a/Interview/mysql/Mysql\344\270\255ACID\347\232\204\345\216\237\347\220\206.md" +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: Mysql中ACID的原理 -author: DreamCat -id: 1 -date: 2019-11-20 15:51:07 -tags: Sql -categories: Sql ---- - -## 引言 - -## 场景 - -> **面试官:"知道事务的四大特性么?"** -> -> **你:"懂,ACID嘛,原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)!"** -> -> **面试官:"你们是用mysql数据库吧,能简单说说innodb中怎么实现这四大特性的么?“ 你:"我只知道隔离性是怎么做的balabala~~"** -> **面试官:"还是回去等通知吧~"** - - - -## 举例 - -我们以从A账户转账50元到B账户为例进行说明一下ACID,四大特性。 - -## 原子性 - -根据定义,原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。即要么转账成功,要么转账失败,是不存在中间的状态! - -**如果无法保证原子性会怎么样?** - -OK,就会出现**数据不一致**的情形,A账户减去50元,而B账户增加50元操作失败。系统将无故丢失50元~ - -## 隔离性 - -根据定义,隔离性是指多个事务并发执行的时候,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 - -**如果无法保证隔离性会怎么样?** - -OK,假设A账户有200元,B账户0元。A账户往B账户转账两次,金额为50元,分别在两个事务中执行。如果无法保证隔离性,会出现下面的情形 - -![](https://pic2.zhimg.com/80/v2-e2e899c5187386e58d495a755894f635_hd.jpg) - -如图所示,如果不保证隔离性,A扣款两次,而B只加款一次,凭空消失了50元,依然出现了**数据不一致**的情形! - -## 持久性 - -根据定义,持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。 - -**如果无法保证持久性会怎么样?** - -在Mysql中,为了解决CPU和磁盘速度不一致问题,Mysql是将磁盘上的数据加载到内存,对内存进行操作,然后再回写磁盘。好,假设此时宕机了,在内存中修改的数据全部丢失了,持久性就无法保证。 - -设想一下,系统提示你转账成功。但是你发现金额没有发生任何改变,此时数据出现了不合法的数据状态,我们将这种状态认为是**数据不一致**的情形。 - -## 一致性 - -根据定义,一致性是指事务执行前后,数据处于一种合法的状态,这种状态是语义上的而不是语法上的。 那什么是合法的数据状态呢? oK,这个状态是满足预定的约束就叫做合法的状态,再通俗一点,这状态是由你自己来定义的。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的! - -**如果无法保证一致性会怎么样?** - -- 例一:A账户有200元,转账300元出去,此时A账户余额为-100元。你自然就发现了此时数据是不一致的,为什么呢?因为你定义了一个状态,余额这列必须大于0。 -- 例二:A账户200元,转账50元给B账户,A账户的钱扣了,但是B账户因为各种意外,余额并没有增加。你也知道此时数据是不一致的,为什么呢?因为你定义了一个状态,要求A+B的余额必须不变。 - -## 解答 - -#### 问题一:Mysql怎么保证一致性的? - -OK,这个问题分为两个层面来说。 从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现AID三大特性,才有可能实现一致性。例如,原子性无法保证,显然一致性也无法保证。 - -但是,如果你在事务里故意写出违反约束的代码,一致性还是无法保证的。例如,你在转账的例子中,你的代码里故意不给B账户加钱,那一致性还是无法保证。因此,还必须从应用层角度考虑。 - -从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据! - -#### 问题二: Mysql怎么保证原子性的? - -OK,是利用Innodb的`undo log`。 `undo log`名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句,他需要记录你要回滚的相应日志信息。 例如 - -- (1)当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据 -- (2)当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作 -- (3)当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作 - -`undo log`记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。 - -#### 问题三: Mysql怎么保证持久性的? - -OK,是利用Innodb的`redo log`。 正如之前说的,Mysql是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。 *怎么解决这个问题?* 简单啊,事务提交前直接把数据写入磁盘就行啊。 *这么做有什么问题?* - -- 只修改一个页面里的一个字节,就要将整个页面刷入磁盘,太浪费资源了。毕竟一个页面16kb大小,你只改其中一点点东西,就要将16kb的内容刷入磁盘,听着也不合理。 -- 毕竟一个事务里的SQL可能牵涉到多个数据页的修改,而这些数据页可能不是相邻的,也就是属于随机IO。显然操作随机IO,速度会比较慢。 - -于是,决定采用`redo log`解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在`redo log`中记录这次操作。当事务提交的时候,会将`redo log`日志进行刷盘(`redo log`一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将`redo log`中的内容恢复到数据库中,再根据`undo log`和`binlog`内容决定回滚数据还是提交数据。 - -**采用redo log的好处?** - -其实好处就是将`redo log`进行刷盘比对数据页刷盘效率高,具体表现如下 - -- `redo log`体积小,毕竟只记录了哪一页修改了啥,因此体积小,刷盘快。 -- `redo log`是一直往末尾进行追加,属于顺序IO。效率显然比随机IO来的快。 - -#### 问题四: Mysql怎么保证隔离性的? - -OK,利用的是锁和MVCC机制。还是拿转账例子来说明,有一个账户表如下 表名`t_balance` - -![](https://pic4.zhimg.com/80/v2-86bedce66efcc4a3b90d09740163e3a7_hd.jpg) - -其中id是主键,user_id为账户名,balance为余额。还是以转账两次为例,如下图所示 - -![](https://pic2.zhimg.com/80/v2-9e136615fa9a741763dfc7ba2f976a99_hd.jpg) - -至于MVCC,即多版本并发控制(Multi Version Concurrency Control),一个行记录数据有多个版本对快照数据,这些快照数据在`undo log`中。 如果一个事务读取的行正在做DELELE或者UPDATE操作,读取操作不会等行上的锁释放,而是读取该行的快照版本。 由于MVCC机制在可重复读(Repeateable Read)和读已提交(Read Commited)的MVCC表现形式不同,就不赘述了。 - -但是有一点说明一下,在事务隔离级别为读已提交(Read Commited)时,一个事务能够读到另一个事务已经提交的数据,是不满足隔离性的。但是当事务隔离级别为可重复读(Repeateable Read)中,是满足隔离性的。 \ No newline at end of file diff --git "a/Interview/mysql/sql-\345\255\230\345\202\250\345\274\225\346\223\216.md" "b/Interview/mysql/sql-\345\255\230\345\202\250\345\274\225\346\223\216.md" deleted file mode 100644 index 61ea82c3..00000000 --- "a/Interview/mysql/sql-\345\255\230\345\202\250\345\274\225\346\223\216.md" +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: sql-存储引擎 -author: DreamCat -id: 1 -date: 2019-11-15 10:41:34 -tags: Sql -categories: Sql ---- - -## 引言 - -**sql-存储引擎** - -## InnoDB - -是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。 - -实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ 间隙锁(Next-Key Locking)防止幻影读。 - -主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。 - -内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。 - - - -支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 - -## MyISAM - -设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。 - -提供了大量的特性,包括压缩表、空间数据索引等。 - -不支持事务。 - -不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 - -可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 - -如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 - -## 比较 - -- 事务: InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。 -- 并发: MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 -- 外键: InnoDB 支持外键。 -- 备份: InnoDB 支持在线热备份。 -- 崩溃恢复: MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 -- 其它特性: MyISAM 支持压缩表和空间数据索引。 \ No newline at end of file diff --git "a/Interview/mysql/sql-\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\345\216\237\347\220\206.md" "b/Interview/mysql/sql-\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\345\216\237\347\220\206.md" deleted file mode 100644 index 79f5012a..00000000 --- "a/Interview/mysql/sql-\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\345\216\237\347\220\206.md" +++ /dev/null @@ -1,469 +0,0 @@ ---- -title: sql-数据库系统原理 -author: DreamCat -id: 1 -date: 2019-11-14 15:22:17 -tags: Sql -categories: Sql ---- - -## 引言 - -**sql-数据库系统原理** - -## 事务 - -### 概念 - -事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。 - -### ACID - -#### 原子性(Atomicity) - -事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。 - -回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。 - - - -#### 一致性(Consistency) - -数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。 - -#### 隔离性(Isolation) - -一个事务所做的修改在最终提交以前,对其它事务是不可见的。 - -#### 持久性(Durability) - -一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 - -可以通过数据库备份和恢复来实现,在系统发生崩溃时,使用备份的数据库进行数据恢复。 - -事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系: - -- 只有满足一致性,事务的执行结果才是正确的。 -- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。 -- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 -- 事务满足持久化是为了能应对数据库崩溃的情况。 - -![](https://www.pdai.tech/_images/pics/a58e294a-615d-4ea0-9fbf-064a6daec4b2.png) - -### AUTOCOMMIT - -MySQL 默认采用自动提交模式。也就是说,如果不显式使用`START TRANSACTION`语句来开始一个事务,那么每个查询都会被当做一个事务自动提交。 - -## 并发一致性问题 - -在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。 - -### 丢弃修改 - -T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。 - -![](https://www.pdai.tech/_images/pics/88ff46b3-028a-4dbb-a572-1f062b8b96d3.png) - -### 读脏数据 - -T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。 - -![](https://www.pdai.tech/_images/pics/dd782132-d830-4c55-9884-cfac0a541b8e.png) - -### 不可重复读 - -T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 - -![](https://www.pdai.tech/_images/pics/c8d18ca9-0b09-441a-9a0c-fb063630d708.png) - -### 幻影读 - -T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 - -![](https://www.pdai.tech/_images/pics/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png) - -产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。 - -## 锁 - -### 封锁粒度 - -MySQL 中提供了两种封锁粒度: 行级锁以及表级锁。 - -应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。 - -但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。 - -### 封锁类型 - -#### 读写锁 - -- 排它锁(Exclusive),简写为 X 锁,又称写锁。 -- 共享锁(Shared),简写为 S 锁,又称读锁。 - -有以下两个规定: - -- 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。 -- 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。 - -#### 意向锁 - -使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。 - -在存在行级锁和表级锁的情况下,事务 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 锁失败。 - -- 任意 IS/IX 锁之间都是兼容的,因为它们只是表示想要对表加锁,而不是真正加锁; -- S 锁只与 S 锁和 IS 锁兼容,也就是说事务 T 想要对数据行加 S 锁,其它事务可以已经获得对表或者表中的行的 S 锁。 - -### 封锁协议 - -#### 三级封锁协议 - -##### 一级封锁协议 - -事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。 - -可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。 - -| T1 | T2 | -| :---------: | :---------: | -| lock-x(A) | | -| read A=20 | | -| | lock-x(A) | -| | wait | -| write A=19 | | -| commit | | -| Unlock-x(A) | | -| | obtain | -| | read A = 19 | -| | write A=21 | -| | commit | -| | unlock-x(A) | - -##### 二级封锁协议 - -在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。 - -可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。 - -| T1 | T2 | -| :---------: | :---------: | -| lock-x(A) | | -| read A=20 | | -| write A=19 | | -| | lock-s(A) | -| | wait | -| rollback | | -| A=20 | . | -| unlock-x(A) | | -| | obtain | -| | read A=20 | -| | commit | -| | unlock-s(A) | - -##### 三级封锁协议 - -在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。 - -可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。 - -| T1 | T2 | -| :---------: | :---------: | -| lock-s(A) | | -| read A=20 | | -| | lock-x(A) | -| | wait | -| read A=20 | | -| commit | | -| unlock-s(A) | | -| | obtain | -| | read A=20 | -| | write A=19 | -| | commit | -| | unlock-X(A) | - -#### 两段锁协议 - -加锁和解锁分为两个阶段进行。 - -可串行化调度是指,通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同。 - -事务遵循两段锁协议是保证可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。 - -但不是必要条件,例如以下操作不满足两段锁协议,但是它还是可串行化调度。 - -### Mysql隐式与显式锁定 - -MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。 - -## 隔离级别 - -### 未提交读(READ UNCOMMITTED) - -事务中的修改,即使没有提交,对其它事务也是可见的。 - -### 提交读(READ COMMITTED) - -一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。 - -### 可重复读(REPEATABLE READ) - -保证在同一个事务中多次读取同样数据的结果是一样的。 - -### 可串行化(SERIALIZABLE) - -强制事务串行执行。 - -| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | -| :------: | :--: | :--------: | :----: | -| 未提交读 | √ | √ | √ | -| 提交读 | × | √ | √ | -| 可重复读 | × | × | √ | -| 可串行化 | × | × | × | - -## 多版本并发控制 - -多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 - -### 版本号 - -- 系统版本号: 是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。 -- 事务版本号: 事务开始时的系统版本号。 - -### 隐藏的列 - -MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号: - -- 创建版本号: 指示创建一个数据行的快照时的系统版本号; -- 删除版本号: 如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。 - -### Undo日志 - -MVCC 使用到的快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。 - -![](https://www.pdai.tech/_images/pics/e41405a8-7c05-4f70-8092-e961e28d3112.jpg) - -### 实现过程 - -以下实现过程针对可重复读隔离级别。 - -当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。 - -#### SELECT - -多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 - -把没有对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 - -#### INSERT - -将当前系统版本号作为数据行快照的创建版本号。 - -#### DELETE - -将当前系统版本号作为数据行快照的删除版本号。 - -#### UPDATE - -将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。 - -### 快照读与当前读 - -#### 快照读 - -使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。 - -`select * from table ...;` - -#### 当前读 - -读取的是最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。 - -```mysql -select * from table where ? lock in share mode; -select * from table where ? for update; -insert; -update; -delete; -``` - -## Next-Key Locks - -Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。 - -MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 - -### Record Locks - -锁定一个记录上的索引,而不是记录本身。 - -如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。 - -### Gap Locks - -锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。 - -`SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;` - -### Next-Key Locks - -它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。例如一个索引包含以下值: 10, 11, 13, and 20,那么就需要锁定以下区间: - -```mysql -(negative infinity, 10] -(10, 11] -(11, 13] -(13, 20] -(20, positive infinity) -``` - -## 面试常问的几个问题 - -### 数据库引擎innodb与myisam的区别? - -[InnoDB和MyISAM比较](http://dreamcat.ink/2019/11/15/sql-notes/1/) - -### 4大特性 - -[Mysql的ACID原理](http://dreamcat.ink/2019/11/20/sql-notes/1/) - -### 数据库的隔离级别 - -1. 未提交读,事务中发生了修改,即使没有提交,其他事务也是可见的,比如对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100. -2. 提交读,对于一个事务从开始直到提交之前,所做的任何修改是其他事务不可见的,举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取的A是50,刚读取完,A就被修改成100,这个时候另一个事务再进行读取发现A就突然变成100了; -3. 可重复读,就是对一个记录读取多次的记录是相同的,比如对于一个数A读取的话一直是A,前后两次读取的A是一致的; -4. 可重复读,就是对一个记录读取多次的记录是相同的,比如对于一个数A读取的话一直是A,前后两次读取的A是一致的; - -### 索引的分为哪几类? - -- 分为聚集索引和非聚集索引 -- 非聚集索引又有唯一索引,普通索引,主键索引,和全文索引。 - -### 聚集索引是什么? - -聚集索引是数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,也就是说索引中的顺序也就是在内存中顺序。 - -### 主键 超键 候选键 外键是什么 - -#### 定义 - -超键:在关系中能唯一标识元组的属性集称为关系模式的超键 - -候选键:不含有多余属性的超键称为候选键。也就是在候选键中,若再删除属性,就不是键了! - -主键:用户选作元组标识的一个候选键程序主键 - -外键:如果关系模式R中属性K是其它模式的主键,那么k在模式R中称为外键。 - -#### 举例 - -| 学号 | 姓名 | 性别 | 年龄 | 系别 | 专业 | -| -------- | ------ | ---- | ---- | ------ | -------- | -| 20020612 | 李辉 | 男 | 20 | 计算机 | 软件开发 | -| 20060613 | 张明 | 男 | 18 | 计算机 | 软件开发 | -| 20060614 | 王小玉 | 女 | 19 | 物理 | 力学 | -| 20060615 | 李淑华 | 女 | 17 | 生物 | 动物学 | -| 20060616 | 赵静 | 男 | 21 | 化学 | 食品化学 | -| 20060617 | 赵静 | 女 | 20 | 生物 | 植物学 | - -1. 超键:于是我们从例子中可以发现 学号是标识学生实体的唯一标识。那么该元组的超键就为学号。除此之外我们还可以把它跟其他属性组合起来,比如:(`学号`,`性别`),(`学号`,`年龄`) -2. 候选键:根据例子可知,学号是一个可以唯一标识元组的唯一标识,因此学号是一个候选键,实际上,候选键是超键的子集,比如 (学号,年龄)是超键,但是它不是候选键。因为它还有了额外的属性。 -3. 主键:简单的说,例子中的元组的候选键为学号,但是我们选定他作为该元组的唯一标识,那么学号就为主键。 -4. 外键是相对于主键的,比如在学生记录里,主键为学号,在成绩单表中也有学号字段,因此学号为成绩单表的外键,为学生表的主键。 - -#### 总结 - -**主键为候选键的子集,候选键为超键的子集,而外键的确定是相对于主键的。** - -### 视图的作用,视图可以更改么? - -视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的sql操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。 - -视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by 则对视图再次order by将被覆盖。 - -创建视图:`create view xxx as xxxx` - -对于某些视图比如未使用联结子查询分组聚集函数Distinct Union等,是可以对其更新的,对视图的更新将对基表进行更新;但是视图主要用于简化检索,保护数据,并不用于更新,而且大部分视图都不可以更新。 - -### drop,delete与truncate的区别 - -drop直接删掉表;truncate删除表中数据,再插入时自增长id又从1开始 ;delete删除表中数据,可以加where字句。 - -1. DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把单独的删除操作记录记入日志保存,删除行是不能恢复的。并且在删除的过程中不会激活与表有关的删除触发器。执行速度快。 -2. 表和索引所占空间。当表被TRUNCATE 后,这个表和索引所占用的空间会恢复到初始大小,而DELETE操作不会减少表或索引所占用的空间。drop语句将表所占用的空间全释放掉。 -3. 一般而言,drop > truncate > delete -4. 应用范围。TRUNCATE 只能对TABLE;DELETE可以是table和view -5. TRUNCATE 和DELETE只删除数据,而DROP则删除整个表(结构和数据)。 -6. truncate与不带where的delete :只删除数据,而不删除表的结构(定义)drop语句将删除表的结构被依赖的约束(constrain),触发器(trigger)索引(index);依赖于该表的存储过程/函数将被保留,但其状态会变为:invalid。 -7. delete语句为DML(Data Manipulation Language),这个操作会被放到 rollback segment中,事务提交后才生效。如果有相应的 tigger,执行的时候将被触发。 -8. truncate、drop是DDL(Data Define Language),操作立即生效,原数据不放到 rollback segment中,不能回滚 -9. 在没有备份情况下,谨慎使用 drop 与 truncate。要删除部分数据行采用delete且注意结合where来约束影响范围。回滚段要足够大。要删除表用drop;若想保留表而将表中数据删除,如果于事务无关,用truncate即可实现。如果和事务有关,或老是想触发trigger,还是用delete。 -10. Truncate table 表名 速度快,而且效率高,因为: truncate table 在功能上与不带 WHERE 子句的 DELETE 语句相同:二者均删除表中的全部行。但 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少。DELETE 语句每次删除一行,并在事务日志中为所删除的每行记录一项。TRUNCATE TABLE 通过释放存储表数据所用的数据页来删除数据,并且只在事务日志中记录页的释放。 -11. TRUNCATE TABLE 删除表中的所有行,但表结构及其列、约束、索引等保持不变。新行标识所用的计数值重置为该列的种子。如果想保留标识计数值,请改用 DELETE。如果要删除表定义及其数据,请使用 DROP TABLE 语句。 -12. 对于由 FOREIGN KEY 约束引用的表,不能使用 TRUNCATE TABLE,而应使用不带 WHERE 子句的 DELETE 语句。由于 TRUNCATE TABLE 不记录在日志中,所以它不能激活触发器。 - -### 数据库范式 - -#### 第一范式 - -在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。 所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,**第一范式就是无重复的列**。 - -#### 第二范式 - -第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主关键字或主键、主码。 第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,**第二范式就是非主属性非部分依赖于主关键字**。 - -#### 第三范式 - -满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。(我的理解是消除冗余) - -### 数据库优化的思路 - -#### SQL语句优化 - -- 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。 -- 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:`select id from t where num is null` ;可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:`select id from t where num=0` -- 很多时候用 exists 代替 in 是一个好的选择。 -- 用Where子句替换HAVING 子句 因为HAVING 只会在检索出所有记录之后才对结果集进行过滤。 - -#### 索引优化 - -#### 数据库结构优化 - -- 范式优化: 比如消除冗余(节省空间。。) -- 反范式优化:比如适当加冗余等(减少join) -- 拆分表:分区将数据在物理上分隔开,不同分区的数据可以制定保存在处于不同磁盘上的数据文件里。这样,当对这个表进行查询时,只需要在表分区中进行扫描,而不必进行全表扫描,明显缩短了查询时间,另外处于不同磁盘的分区也将对这个表的数据传输分散在不同的磁盘I/O,一个精心设置的分区可以将数据传输对磁盘I/O竞争均匀地分散开。对数据量大的时时表可采取此方法。可按月自动建表分区。 -- 拆分其实又分垂直拆分和水平拆分: - - 案例: 简单购物系统暂设涉及如下表: - - 1.产品表(数据量10w,稳定) - - 2.订单表(数据量200w,且有增长趋势) - - 3.用户表 (数据量100w,且有增长趋势) - - 以mysql为例讲述下水平拆分和垂直拆分,mysql能容忍的数量级在百万静态数据可以到千万 - - **垂直拆分:** - - 解决问题:表与表之间的io竞争 - - 不解决问题:单表中数据量增长出现的压力 - - 方案: 把产品表和用户表放到一个server上 订单表单独放到一个server上 - - **水平拆分:** - - 解决问题:单表中数据量增长出现的压力 - - 不解决问题:表与表之间的io争夺 - - 方案:**用户表** 通过性别拆分为男用户表和女用户表,**订单表** 通过已完成和完成中拆分为已完成订单和未完成订单,**产品表** 未完成订单放一个server上,已完成订单表盒男用户表放一个server上,女用户表放一个server上(女的爱购物 哈哈)。 - -#### 服务器硬件优化 - -多花钱 - -### 存储过程与触发器的区别 - -触发器与存储过程非常相似,触发器也是SQL语句集,两者唯一的区别是触发器不能用EXECUTE语句调用,而是在用户执行Transact-SQL语句时自动触发(激活)执行。 - -触发器是在一个修改了指定表中的数据时执行的存储过程。 - -通常通过创建触发器来强制实现不同表中的逻辑相关数据的引用完整性和一致性。由于用户不能绕过触发器,所以可以用它来强制实施复杂的业务规则,以确保数据的完整性。 - -触发器不同于存储过程,触发器主要是通过事件执行触发而被执行的,而存储过程可以通过存储过程名称名字而直接调用。当对某一表进行诸如UPDATE、INSERT、DELETE这些操作时,SQLSERVER就会自动执行触发器所定义的SQL语句,从而确保对数据的处理必须符合这些SQL语句所定义的规则。 \ No newline at end of file diff --git "a/Interview/mysql/sql-\347\264\242\345\274\225-B-\346\240\221.md" "b/Interview/mysql/sql-\347\264\242\345\274\225-B-\346\240\221.md" deleted file mode 100644 index 29eab931..00000000 --- "a/Interview/mysql/sql-\347\264\242\345\274\225-B-\346\240\221.md" +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: sql-索引(B+树) -author: DreamCat -id: 1 -date: 2019-11-16 10:05:16 -tags: Sql -categories: Sql ---- - -## 引言 - -**sql-索引(B+树)** - -## B+Tree原理 - -### 数据结构 - -B Tree 指的是 Balance Tree,也就是平衡树。平衡树是一颗查找树,并且所有叶子节点位于同一层。 - -B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。 - -在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。 - - - -### 操作 - -进行查找操作时,首先在根节点进行二分查找,找到一个key所在的指针,然后递归地指针指向的节点进行查找。直到查找到叶子结点,然后在叶子节点上进行二分查找,找出key所对应的data。 - -插入删除操作记录会破坏平衡树的平衡性,因此在插入删除操作之后,需要对树进行一个分裂、合并、旋转等操作来维护平衡性。 - -### 与红黑树的比较 - -红黑树等平衡树可以用实现索引,但是文件系统及数据库系统普遍采用B+Tree作为索引结构,主要有以下两个原因: - -(1)更少的查找次数 - -平衡树查找操作的时间复杂度等于树高h,而树高大致为 O(h)=O(logdN),其中 d 为每个节点的出度。 - -红黑树的出度为2,而B+Tree的出度一般非常大,所以红黑树的树高h很明显比B+ Tree 大非常多,检索的次数也就更多。 - -(2)利用计算机预读特性 - -为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。 - -操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入。 - -## MySQL 索引 - -索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 - -### B+Tree索引 - -是大多数MySQL存储引擎的默认索引类型。 - -因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。除了用于查找,还可以用于排序和分组。 - -可以指定多个列作为索引列,多个索引列共同组成键。 - -适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。 - -InnoDB 的 B+Tree 索引分为主索引和辅助索引。 - -主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 - -辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找。 - -### 哈希索引 - -哈希索引能以 O(1) 时间进行查找,但是失去了有序性,它具有以下限制: - -- 无法用于排序与分组; -- 只支持精确查找,无法用于部分查找和范围查找。 - -InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 - -### 全文索引 - -MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。 - -全文索引一般使用倒排索引实现,它记录着关键词到其所在文档的映射。 - -InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。 - -### 空间数据索引 - -MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 - -必须使用 GIS 相关的函数来维护数据。 - -## 索引优化 - -### 独立的列 - -在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。 - -`SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;` - -### 多列索引 - -在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。 - -`SELECT film_id, actor_ id FROM sakila.film_actor -WHERE actor_id = 1 AND film_id = 1;` - -### 索引列的顺序 - -让选择性最强的索引列放在前面,索引的选择性是指: 不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。 - -例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。 - -·`SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity, -COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity, -COUNT(*) -FROM payment;` - -### 前缀索引 - -对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。 - -对于前缀长度的选取需要根据索引选择性来确定。 - -### 覆盖索引 - -索引包含所有需要查询的字段的值。 - -具有以下优点: - -- 索引通常远小于数据行的大小,只读取索引能大大减少数据访问量。 -- 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。 -- 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。 - -## 索引的优点 - -- 大大减少了服务器需要扫描的数据行数。 -- 帮助服务器避免进行排序和分组,也就不需要创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,因为不需要排序和分组,也就不需要创建临时表)。 -- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相邻的数据都存储在一起)。 - -### 索引的使用场景 - -- 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。 -- 对于中到大型的表,索引就非常有效。 -- 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。 \ No newline at end of file diff --git a/Interview/mysql/test.sql b/Interview/mysql/test.sql deleted file mode 100644 index 13354b95..00000000 --- a/Interview/mysql/test.sql +++ /dev/null @@ -1,170 +0,0 @@ --- MySQL dump 10.13 Distrib 8.0.16, for osx10.14 (x86_64) --- --- Host: localhost Database: test --- ------------------------------------------------------ --- Server version 8.0.16 - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; - SET NAMES utf8mb4 ; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- --- Table structure for table `employees` --- - -DROP TABLE IF EXISTS `employees`; -/*!40101 SET @saved_cs_client = @@character_set_client */; - SET character_set_client = utf8mb4 ; -CREATE TABLE `employees` ( - `employee_id` int(6) NOT NULL AUTO_INCREMENT, - `first_name` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL, - `last_name` varchar(25) COLLATE utf8mb4_general_ci DEFAULT NULL, - `email` varchar(25) COLLATE utf8mb4_general_ci DEFAULT NULL, - `phone_number` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL, - `job_id` varchar(10) COLLATE utf8mb4_general_ci DEFAULT NULL, - `salary` double(10,2) DEFAULT NULL, - `commission_pct` double(4,2) DEFAULT NULL, - `manager_id` int(6) DEFAULT NULL, - `department_id` int(4) DEFAULT NULL, - `hiredate` datetime DEFAULT NULL, - PRIMARY KEY (`employee_id`) USING BTREE, - KEY `dept_id_fk` (`department_id`) USING BTREE, - KEY `job_id_fk` (`job_id`) USING BTREE, - CONSTRAINT `dept_id_fk` FOREIGN KEY (`department_id`) REFERENCES `departments` (`department_id`) ON DELETE RESTRICT ON UPDATE RESTRICT, - CONSTRAINT `job_id_fk` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`job_id`) ON DELETE RESTRICT ON UPDATE RESTRICT -) ENGINE=InnoDB AUTO_INCREMENT=207 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `employees` --- - -LOCK TABLES `employees` WRITE; -/*!40000 ALTER TABLE `employees` DISABLE KEYS */; -INSERT INTO `employees` VALUES (100,'Steven','K_ing','SKING','515.123.4567','AD_PRES',24000.00,NULL,NULL,90,'1992-04-03 00:00:00'),(101,'Neena','Kochhar','NKOCHHAR','515.123.4568','AD_VP',17000.00,NULL,100,90,'1992-04-03 00:00:00'),(102,'Lex','De Haan','LDEHAAN','515.123.4569','AD_VP',17000.00,NULL,100,90,'1992-04-03 00:00:00'),(103,'Alexander','Hunold','AHUNOLD','590.423.4567','IT_PROG',9000.00,NULL,102,60,'1992-04-03 00:00:00'),(104,'Bruce','Ernst','BERNST','590.423.4568','IT_PROG',6000.00,NULL,103,60,'1992-04-03 00:00:00'),(105,'David','Austin','DAUSTIN','590.423.4569','IT_PROG',4800.00,NULL,103,60,'1998-03-03 00:00:00'),(106,'Valli','Pataballa','VPATABAL','590.423.4560','IT_PROG',4800.00,NULL,103,60,'1998-03-03 00:00:00'),(107,'Diana','Lorentz','DLORENTZ','590.423.5567','IT_PROG',4200.00,NULL,103,60,'1998-03-03 00:00:00'),(108,'Nancy','Greenberg','NGREENBE','515.124.4569','FI_MGR',12000.00,NULL,101,100,'1998-03-03 00:00:00'),(109,'Daniel','Faviet','DFAVIET','515.124.4169','FI_ACCOUNT',9000.00,NULL,108,100,'1998-03-03 00:00:00'),(110,'John','Chen','JCHEN','515.124.4269','FI_ACCOUNT',8200.00,NULL,108,100,'2000-09-09 00:00:00'),(111,'Ismael','Sciarra','ISCIARRA','515.124.4369','FI_ACCOUNT',7700.00,NULL,108,100,'2000-09-09 00:00:00'),(112,'Jose Manuel','Urman','JMURMAN','515.124.4469','FI_ACCOUNT',7800.00,NULL,108,100,'2000-09-09 00:00:00'),(113,'Luis','Popp','LPOPP','515.124.4567','FI_ACCOUNT',6900.00,NULL,108,100,'2000-09-09 00:00:00'),(114,'Den','Raphaely','DRAPHEAL','515.127.4561','PU_MAN',11000.00,NULL,100,30,'2000-09-09 00:00:00'),(115,'Alexander','Khoo','AKHOO','515.127.4562','PU_CLERK',3100.00,NULL,114,30,'2000-09-09 00:00:00'),(116,'Shelli','Baida','SBAIDA','515.127.4563','PU_CLERK',2900.00,NULL,114,30,'2000-09-09 00:00:00'),(117,'Sigal','Tobias','STOBIAS','515.127.4564','PU_CLERK',2800.00,NULL,114,30,'2000-09-09 00:00:00'),(118,'Guy','Himuro','GHIMURO','515.127.4565','PU_CLERK',2600.00,NULL,114,30,'2000-09-09 00:00:00'),(119,'Karen','Colmenares','KCOLMENA','515.127.4566','PU_CLERK',2500.00,NULL,114,30,'2000-09-09 00:00:00'),(120,'Matthew','Weiss','MWEISS','650.123.1234','ST_MAN',8000.00,NULL,100,50,'2004-02-06 00:00:00'),(121,'Adam','Fripp','AFRIPP','650.123.2234','ST_MAN',8200.00,NULL,100,50,'2004-02-06 00:00:00'),(122,'Payam','Kaufling','PKAUFLIN','650.123.3234','ST_MAN',7900.00,NULL,100,50,'2004-02-06 00:00:00'),(123,'Shanta','Vollman','SVOLLMAN','650.123.4234','ST_MAN',6500.00,NULL,100,50,'2004-02-06 00:00:00'),(124,'Kevin','Mourgos','KMOURGOS','650.123.5234','ST_MAN',5800.00,NULL,100,50,'2004-02-06 00:00:00'),(125,'Julia','Nayer','JNAYER','650.124.1214','ST_CLERK',3200.00,NULL,120,50,'2004-02-06 00:00:00'),(126,'Irene','Mikkilineni','IMIKKILI','650.124.1224','ST_CLERK',2700.00,NULL,120,50,'2004-02-06 00:00:00'),(127,'James','Landry','JLANDRY','650.124.1334','ST_CLERK',2400.00,NULL,120,50,'2004-02-06 00:00:00'),(128,'Steven','Markle','SMARKLE','650.124.1434','ST_CLERK',2200.00,NULL,120,50,'2004-02-06 00:00:00'),(129,'Laura','Bissot','LBISSOT','650.124.5234','ST_CLERK',3300.00,NULL,121,50,'2004-02-06 00:00:00'),(130,'Mozhe','Atkinson','MATKINSO','650.124.6234','ST_CLERK',2800.00,NULL,121,50,'2004-02-06 00:00:00'),(131,'James','Marlow','JAMRLOW','650.124.7234','ST_CLERK',2500.00,NULL,121,50,'2004-02-06 00:00:00'),(132,'TJ','Olson','TJOLSON','650.124.8234','ST_CLERK',2100.00,NULL,121,50,'2004-02-06 00:00:00'),(133,'Jason','Mallin','JMALLIN','650.127.1934','ST_CLERK',3300.00,NULL,122,50,'2004-02-06 00:00:00'),(134,'Michael','Rogers','MROGERS','650.127.1834','ST_CLERK',2900.00,NULL,122,50,'2002-12-23 00:00:00'),(135,'Ki','Gee','KGEE','650.127.1734','ST_CLERK',2400.00,NULL,122,50,'2002-12-23 00:00:00'),(136,'Hazel','Philtanker','HPHILTAN','650.127.1634','ST_CLERK',2200.00,NULL,122,50,'2002-12-23 00:00:00'),(137,'Renske','Ladwig','RLADWIG','650.121.1234','ST_CLERK',3600.00,NULL,123,50,'2002-12-23 00:00:00'),(138,'Stephen','Stiles','SSTILES','650.121.2034','ST_CLERK',3200.00,NULL,123,50,'2002-12-23 00:00:00'),(139,'John','Seo','JSEO','650.121.2019','ST_CLERK',2700.00,NULL,123,50,'2002-12-23 00:00:00'),(140,'Joshua','Patel','JPATEL','650.121.1834','ST_CLERK',2500.00,NULL,123,50,'2002-12-23 00:00:00'),(141,'Trenna','Rajs','TRAJS','650.121.8009','ST_CLERK',3500.00,NULL,124,50,'2002-12-23 00:00:00'),(142,'Curtis','Davies','CDAVIES','650.121.2994','ST_CLERK',3100.00,NULL,124,50,'2002-12-23 00:00:00'),(143,'Randall','Matos','RMATOS','650.121.2874','ST_CLERK',2600.00,NULL,124,50,'2002-12-23 00:00:00'),(144,'Peter','Vargas','PVARGAS','650.121.2004','ST_CLERK',2500.00,NULL,124,50,'2002-12-23 00:00:00'),(145,'John','Russell','JRUSSEL','011.44.1344.429268','SA_MAN',14000.00,0.40,100,80,'2002-12-23 00:00:00'),(146,'Karen','Partners','KPARTNER','011.44.1344.467268','SA_MAN',13500.00,0.30,100,80,'2002-12-23 00:00:00'),(147,'Alberto','Errazuriz','AERRAZUR','011.44.1344.429278','SA_MAN',12000.00,0.30,100,80,'2002-12-23 00:00:00'),(148,'Gerald','Cambrault','GCAMBRAU','011.44.1344.619268','SA_MAN',11000.00,0.30,100,80,'2002-12-23 00:00:00'),(149,'Eleni','Zlotkey','EZLOTKEY','011.44.1344.429018','SA_MAN',10500.00,0.20,100,80,'2002-12-23 00:00:00'),(150,'Peter','Tucker','PTUCKER','011.44.1344.129268','SA_REP',10000.00,0.30,145,80,'2014-03-05 00:00:00'),(151,'David','Bernstein','DBERNSTE','011.44.1344.345268','SA_REP',9500.00,0.25,145,80,'2014-03-05 00:00:00'),(152,'Peter','Hall','PHALL','011.44.1344.478968','SA_REP',9000.00,0.25,145,80,'2014-03-05 00:00:00'),(153,'Christopher','Olsen','COLSEN','011.44.1344.498718','SA_REP',8000.00,0.20,145,80,'2014-03-05 00:00:00'),(154,'Nanette','Cambrault','NCAMBRAU','011.44.1344.987668','SA_REP',7500.00,0.20,145,80,'2014-03-05 00:00:00'),(155,'Oliver','Tuvault','OTUVAULT','011.44.1344.486508','SA_REP',7000.00,0.15,145,80,'2014-03-05 00:00:00'),(156,'Janette','K_ing','JKING','011.44.1345.429268','SA_REP',10000.00,0.35,146,80,'2014-03-05 00:00:00'),(157,'Patrick','Sully','PSULLY','011.44.1345.929268','SA_REP',9500.00,0.35,146,80,'2014-03-05 00:00:00'),(158,'Allan','McEwen','AMCEWEN','011.44.1345.829268','SA_REP',9000.00,0.35,146,80,'2014-03-05 00:00:00'),(159,'Lindsey','Smith','LSMITH','011.44.1345.729268','SA_REP',8000.00,0.30,146,80,'2014-03-05 00:00:00'),(160,'Louise','Doran','LDORAN','011.44.1345.629268','SA_REP',7500.00,0.30,146,80,'2014-03-05 00:00:00'),(161,'Sarath','Sewall','SSEWALL','011.44.1345.529268','SA_REP',7000.00,0.25,146,80,'2014-03-05 00:00:00'),(162,'Clara','Vishney','CVISHNEY','011.44.1346.129268','SA_REP',10500.00,0.25,147,80,'2014-03-05 00:00:00'),(163,'Danielle','Greene','DGREENE','011.44.1346.229268','SA_REP',9500.00,0.15,147,80,'2014-03-05 00:00:00'),(164,'Mattea','Marvins','MMARVINS','011.44.1346.329268','SA_REP',7200.00,0.10,147,80,'2014-03-05 00:00:00'),(165,'David','Lee','DLEE','011.44.1346.529268','SA_REP',6800.00,0.10,147,80,'2014-03-05 00:00:00'),(166,'Sundar','Ande','SANDE','011.44.1346.629268','SA_REP',6400.00,0.10,147,80,'2014-03-05 00:00:00'),(167,'Amit','Banda','ABANDA','011.44.1346.729268','SA_REP',6200.00,0.10,147,80,'2014-03-05 00:00:00'),(168,'Lisa','Ozer','LOZER','011.44.1343.929268','SA_REP',11500.00,0.25,148,80,'2014-03-05 00:00:00'),(169,'Harrison','Bloom','HBLOOM','011.44.1343.829268','SA_REP',10000.00,0.20,148,80,'2014-03-05 00:00:00'),(170,'Tayler','Fox','TFOX','011.44.1343.729268','SA_REP',9600.00,0.20,148,80,'2014-03-05 00:00:00'),(171,'William','Smith','WSMITH','011.44.1343.629268','SA_REP',7400.00,0.15,148,80,'2014-03-05 00:00:00'),(172,'Elizabeth','Bates','EBATES','011.44.1343.529268','SA_REP',7300.00,0.15,148,80,'2014-03-05 00:00:00'),(173,'Sundita','Kumar','SKUMAR','011.44.1343.329268','SA_REP',6100.00,0.10,148,80,'2014-03-05 00:00:00'),(174,'Ellen','Abel','EABEL','011.44.1644.429267','SA_REP',11000.00,0.30,149,80,'2014-03-05 00:00:00'),(175,'Alyssa','Hutton','AHUTTON','011.44.1644.429266','SA_REP',8800.00,0.25,149,80,'2014-03-05 00:00:00'),(176,'Jonathon','Taylor','JTAYLOR','011.44.1644.429265','SA_REP',8600.00,0.20,149,80,'2014-03-05 00:00:00'),(177,'Jack','Livingston','JLIVINGS','011.44.1644.429264','SA_REP',8400.00,0.20,149,80,'2014-03-05 00:00:00'),(178,'Kimberely','Grant','KGRANT','011.44.1644.429263','SA_REP',7000.00,0.15,149,NULL,'2014-03-05 00:00:00'),(179,'Charles','Johnson','CJOHNSON','011.44.1644.429262','SA_REP',6200.00,0.10,149,80,'2014-03-05 00:00:00'),(180,'Winston','Taylor','WTAYLOR','650.507.9876','SH_CLERK',3200.00,NULL,120,50,'2014-03-05 00:00:00'),(181,'Jean','Fleaur','JFLEAUR','650.507.9877','SH_CLERK',3100.00,NULL,120,50,'2014-03-05 00:00:00'),(182,'Martha','Sullivan','MSULLIVA','650.507.9878','SH_CLERK',2500.00,NULL,120,50,'2014-03-05 00:00:00'),(183,'Girard','Geoni','GGEONI','650.507.9879','SH_CLERK',2800.00,NULL,120,50,'2014-03-05 00:00:00'),(184,'Nandita','Sarchand','NSARCHAN','650.509.1876','SH_CLERK',4200.00,NULL,121,50,'2014-03-05 00:00:00'),(185,'Alexis','Bull','ABULL','650.509.2876','SH_CLERK',4100.00,NULL,121,50,'2014-03-05 00:00:00'),(186,'Julia','Dellinger','JDELLING','650.509.3876','SH_CLERK',3400.00,NULL,121,50,'2014-03-05 00:00:00'),(187,'Anthony','Cabrio','ACABRIO','650.509.4876','SH_CLERK',3000.00,NULL,121,50,'2014-03-05 00:00:00'),(188,'Kelly','Chung','KCHUNG','650.505.1876','SH_CLERK',3800.00,NULL,122,50,'2014-03-05 00:00:00'),(189,'Jennifer','Dilly','JDILLY','650.505.2876','SH_CLERK',3600.00,NULL,122,50,'2014-03-05 00:00:00'),(190,'Timothy','Gates','TGATES','650.505.3876','SH_CLERK',2900.00,NULL,122,50,'2014-03-05 00:00:00'),(191,'Randall','Perkins','RPERKINS','650.505.4876','SH_CLERK',2500.00,NULL,122,50,'2014-03-05 00:00:00'),(192,'Sarah','Bell','SBELL','650.501.1876','SH_CLERK',4000.00,NULL,123,50,'2014-03-05 00:00:00'),(193,'Britney','Everett','BEVERETT','650.501.2876','SH_CLERK',3900.00,NULL,123,50,'2014-03-05 00:00:00'),(194,'Samuel','McCain','SMCCAIN','650.501.3876','SH_CLERK',3200.00,NULL,123,50,'2014-03-05 00:00:00'),(195,'Vance','Jones','VJONES','650.501.4876','SH_CLERK',2800.00,NULL,123,50,'2014-03-05 00:00:00'),(196,'Alana','Walsh','AWALSH','650.507.9811','SH_CLERK',3100.00,NULL,124,50,'2014-03-05 00:00:00'),(197,'Kevin','Feeney','KFEENEY','650.507.9822','SH_CLERK',3000.00,NULL,124,50,'2014-03-05 00:00:00'),(198,'Donald','OConnell','DOCONNEL','650.507.9833','SH_CLERK',2600.00,NULL,124,50,'2014-03-05 00:00:00'),(199,'Douglas','Grant','DGRANT','650.507.9844','SH_CLERK',2600.00,NULL,124,50,'2014-03-05 00:00:00'),(200,'Jennifer','Whalen','JWHALEN','515.123.4444','AD_ASST',4400.00,NULL,101,10,'2016-03-03 00:00:00'),(201,'Michael','Hartstein','MHARTSTE','515.123.5555','MK_MAN',13000.00,NULL,100,20,'2016-03-03 00:00:00'),(202,'Pat','Fay','PFAY','603.123.6666','MK_REP',6000.00,NULL,201,20,'2016-03-03 00:00:00'),(203,'Susan','Mavris','SMAVRIS','515.123.7777','HR_REP',6500.00,NULL,101,40,'2016-03-03 00:00:00'),(204,'Hermann','Baer','HBAER','515.123.8888','PR_REP',10000.00,NULL,101,70,'2016-03-03 00:00:00'),(205,'Shelley','Higgins','SHIGGINS','515.123.8080','AC_MGR',12000.00,NULL,101,110,'2016-03-03 00:00:00'),(206,'William','Gietz','WGIETZ','515.123.8181','AC_ACCOUNT',8300.00,NULL,205,110,'2016-03-03 00:00:00'); -/*!40000 ALTER TABLE `employees` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `job_grades` --- - -DROP TABLE IF EXISTS `job_grades`; -/*!40101 SET @saved_cs_client = @@character_set_client */; - SET character_set_client = utf8mb4 ; -CREATE TABLE `job_grades` ( - `grade_level` varchar(3) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, - `lowest_sal` int(11) DEFAULT NULL, - `highest_sal` int(11) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `job_grades` --- - -LOCK TABLES `job_grades` WRITE; -/*!40000 ALTER TABLE `job_grades` DISABLE KEYS */; -INSERT INTO `job_grades` VALUES ('A',1000,2999),('B',3000,5999),('C',6000,9999),('D',10000,14999),('E',15000,24999),('F',25000,40000); -/*!40000 ALTER TABLE `job_grades` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `jobs` --- - -DROP TABLE IF EXISTS `jobs`; -/*!40101 SET @saved_cs_client = @@character_set_client */; - SET character_set_client = utf8mb4 ; -CREATE TABLE `jobs` ( - `job_id` varchar(10) COLLATE utf8mb4_general_ci NOT NULL, - `job_title` varchar(35) COLLATE utf8mb4_general_ci DEFAULT NULL, - `min_salary` int(6) DEFAULT NULL, - `max_salary` int(6) DEFAULT NULL, - PRIMARY KEY (`job_id`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `jobs` --- - -LOCK TABLES `jobs` WRITE; -/*!40000 ALTER TABLE `jobs` DISABLE KEYS */; -INSERT INTO `jobs` VALUES ('AC_ACCOUNT','Public Accountant',4200,9000),('AC_MGR','Accounting Manager',8200,16000),('AD_ASST','Administration Assistant',3000,6000),('AD_PRES','President',20000,40000),('AD_VP','Administration Vice President',15000,30000),('FI_ACCOUNT','Accountant',4200,9000),('FI_MGR','Finance Manager',8200,16000),('HR_REP','Human Resources Representative',4000,9000),('IT_PROG','Programmer',4000,10000),('MK_MAN','Marketing Manager',9000,15000),('MK_REP','Marketing Representative',4000,9000),('PR_REP','Public Relations Representative',4500,10500),('PU_CLERK','Purchasing Clerk',2500,5500),('PU_MAN','Purchasing Manager',8000,15000),('SA_MAN','Sales Manager',10000,20000),('SA_REP','Sales Representative',6000,12000),('SH_CLERK','Shipping Clerk',2500,5500),('ST_CLERK','Stock Clerk',2000,5000),('ST_MAN','Stock Manager',5500,8500); -/*!40000 ALTER TABLE `jobs` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `locations` --- - -DROP TABLE IF EXISTS `locations`; -/*!40101 SET @saved_cs_client = @@character_set_client */; - SET character_set_client = utf8mb4 ; -CREATE TABLE `locations` ( - `location_id` int(11) NOT NULL AUTO_INCREMENT, - `street_address` varchar(40) COLLATE utf8mb4_general_ci DEFAULT NULL, - `postal_code` varchar(12) COLLATE utf8mb4_general_ci DEFAULT NULL, - `city` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL, - `state_province` varchar(25) COLLATE utf8mb4_general_ci DEFAULT NULL, - `country_id` varchar(2) COLLATE utf8mb4_general_ci DEFAULT NULL, - PRIMARY KEY (`location_id`) -) ENGINE=InnoDB AUTO_INCREMENT=3201 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `locations` --- - -LOCK TABLES `locations` WRITE; -/*!40000 ALTER TABLE `locations` DISABLE KEYS */; -INSERT INTO `locations` VALUES (1000,'1297 Via Cola di Rie','00989','Roma',NULL,'IT'),(1100,'93091 Calle della Testa','10934','Venice',NULL,'IT'),(1200,'2017 Shinjuku-ku','1689','Tokyo','Tokyo Prefecture','JP'),(1300,'9450 Kamiya-cho','6823','Hiroshima',NULL,'JP'),(1400,'2014 Jabberwocky Rd','26192','Southlake','Texas','US'),(1500,'2011 Interiors Blvd','99236','South San Francisco','California','US'),(1600,'2007 Zagora St','50090','South Brunswick','New Jersey','US'),(1700,'2004 Charade Rd','98199','Seattle','Washington','US'),(1800,'147 Spadina Ave','M5V 2L7','Toronto','Ontario','CA'),(1900,'6092 Boxwood St','YSW 9T2','Whitehorse','Yukon','CA'),(2000,'40-5-12 Laogianggen','190518','Beijing',NULL,'CN'),(2100,'1298 Vileparle (E)','490231','Bombay','Maharashtra','IN'),(2200,'12-98 Victoria Street','2901','Sydney','New South Wales','AU'),(2300,'198 Clementi North','540198','Singapore',NULL,'SG'),(2400,'8204 Arthur St',NULL,'London',NULL,'UK'),(2500,'Magdalen Centre, The Oxford Science Park','OX9 9ZB','Oxford','Oxford','UK'),(2600,'9702 Chester Road','09629850293','Stretford','Manchester','UK'),(2700,'Schwanthalerstr. 7031','80925','Munich','Bavaria','DE'),(2800,'Rua Frei Caneca 1360 ','01307-002','Sao Paulo','Sao Paulo','BR'),(2900,'20 Rue des Corps-Saints','1730','Geneva','Geneve','CH'),(3000,'Murtenstrasse 921','3095','Bern','BE','CH'),(3100,'Pieter Breughelstraat 837','3029SK','Utrecht','Utrecht','NL'),(3200,'Mariano Escobedo 9991','11932','Mexico City','Distrito Federal,','MX'); -/*!40000 ALTER TABLE `locations` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `departments` --- - -DROP TABLE IF EXISTS `departments`; -/*!40101 SET @saved_cs_client = @@character_set_client */; - SET character_set_client = utf8mb4 ; -CREATE TABLE `departments` ( - `department_id` int(4) NOT NULL AUTO_INCREMENT, - `department_name` varchar(3) COLLATE utf8mb4_general_ci DEFAULT NULL, - `manager_id` int(6) DEFAULT NULL, - `location_id` int(4) DEFAULT NULL, - PRIMARY KEY (`department_id`) USING BTREE, - KEY `loc_id_fk` (`location_id`) USING BTREE, - CONSTRAINT `loc_id_fk` FOREIGN KEY (`location_id`) REFERENCES `locations` (`location_id`) ON DELETE RESTRICT ON UPDATE RESTRICT -) ENGINE=InnoDB AUTO_INCREMENT=271 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `departments` --- - -LOCK TABLES `departments` WRITE; -/*!40000 ALTER TABLE `departments` DISABLE KEYS */; -INSERT INTO `departments` VALUES (10,'Adm',200,1700),(20,'Mar',201,1800),(30,'Pur',114,1700),(40,'Hum',203,2400),(50,'Shi',121,1500),(60,'IT',103,1400),(70,'Pub',204,2700),(80,'Sal',145,2500),(90,'Exe',100,1700),(100,'Fin',108,1700),(110,'Acc',205,1700),(120,'Tre',NULL,1700),(130,'Cor',NULL,1700),(140,'Con',NULL,1700),(150,'Sha',NULL,1700),(160,'Ben',NULL,1700),(170,'Man',NULL,1700),(180,'Con',NULL,1700),(190,'Con',NULL,1700),(200,'Ope',NULL,1700),(210,'IT ',NULL,1700),(220,'NOC',NULL,1700),(230,'IT ',NULL,1700),(240,'Gov',NULL,1700),(250,'Ret',NULL,1700),(260,'Rec',NULL,1700),(270,'Pay',NULL,1700); -/*!40000 ALTER TABLE `departments` ENABLE KEYS */; -UNLOCK TABLES; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2020-03-06 1:56:25 diff --git a/Interview/mysql/test2.sql b/Interview/mysql/test2.sql deleted file mode 100644 index b4cab93d..00000000 --- a/Interview/mysql/test2.sql +++ /dev/null @@ -1,72 +0,0 @@ -/* - Navicat Premium Data Transfer - - Source Server : mysql0815 - Source Server Type : MySQL - Source Server Version : 50562 - Source Host : localhost:3306 - Source Schema : girls - - Target Server Type : MySQL - Target Server Version : 50562 - File Encoding : 65001 - - Date: 06/11/2019 16:26:51 -*/ -USE test; -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - - --- ---------------------------- --- Table structure for beauty --- ---------------------------- -DROP TABLE IF EXISTS `beauty`; -CREATE TABLE `beauty` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, - `sex` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT 'Ů', - `borndate` datetime NULL DEFAULT '1987-01-01 00:00:00', - `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, - `photo` blob NULL, - `boyfriend_id` int(11) NULL DEFAULT NULL, - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; - --- ---------------------------- --- Records of beauty --- ---------------------------- -INSERT INTO `beauty` VALUES (1, '柳岩', '女', '1988-02-03 00:00:00', '18209876577', NULL, 8); -INSERT INTO `beauty` VALUES (2, '苍老师', '女', '1987-12-30 00:00:00', '18219876577', NULL, 9); -INSERT INTO `beauty` VALUES (3, 'Angelababy', '女', '1989-02-03 00:00:00', '18209876567', NULL, 3); -INSERT INTO `beauty` VALUES (4, '热巴', '女', '1993-02-03 00:00:00', '18209876579', NULL, 2); -INSERT INTO `beauty` VALUES (5, '周冬雨', '女', '1992-02-03 00:00:00', '18209179577', NULL, 9); -INSERT INTO `beauty` VALUES (6, '周芷若', '女', '1988-02-03 00:00:00', '18209876577', NULL, 1); -INSERT INTO `beauty` VALUES (7, '岳灵珊', '女', '1987-12-30 00:00:00', '18219876577', NULL, 9); -INSERT INTO `beauty` VALUES (8, '小昭', '女', '1989-02-03 00:00:00', '18209876567', NULL, 1); -INSERT INTO `beauty` VALUES (9, '双儿', '女', '1993-02-03 00:00:00', '18209876579', NULL, 9); -INSERT INTO `beauty` VALUES (10, '王语嫣', '女', '1992-02-03 00:00:00', '18209179577', NULL, 4); -INSERT INTO `beauty` VALUES (11, '夏雪', '女', '1993-02-03 00:00:00', '18209876579', NULL, 9); -INSERT INTO `beauty` VALUES (12, '赵敏', '女', '1992-02-03 00:00:00', '18209179577', NULL, 1); - --- ---------------------------- --- Table structure for boys --- ---------------------------- -DROP TABLE IF EXISTS `boys`; -CREATE TABLE `boys` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `boyName` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, - `userCP` int(11) NULL DEFAULT NULL, - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; - --- ---------------------------- --- Records of boys --- ---------------------------- -INSERT INTO `boys` VALUES (1, '张无忌', 100); -INSERT INTO `boys` VALUES (2, '鹿晗', 800); -INSERT INTO `boys` VALUES (3, '黄晓', 50); -INSERT INTO `boys` VALUES (4, '段誉', 300); - -SET FOREIGN_KEY_CHECKS = 1; - diff --git "a/Interview/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\216\237\347\220\206-DNS\346\230\257\345\271\262\344\273\200\344\271\210\347\232\204.md" "b/Interview/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\216\237\347\220\206-DNS\346\230\257\345\271\262\344\273\200\344\271\210\347\232\204.md" deleted file mode 100644 index 483921b5..00000000 --- "a/Interview/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\216\237\347\220\206-DNS\346\230\257\345\271\262\344\273\200\344\271\210\347\232\204.md" +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: 计算机网络原理-DNS是干什么的? -author: DreamCat -id: 1 -date: 2019-11-21 00:07:25 -tags: 计算机网络 -categories: 知识 ---- -## 引言 - -> 咱们这个这样一个面试问题:**"从输入url到页面展示到底发生了什么?**" -> -> 回答的话,的确很简单!但是其中涉及到的细节非常多,估计能讲半个小时。 -> -> 大概的过程分为这8步(俗称天龙八部) -> -> 1. 根据域名,进行DNS域名解析; -> 2. 拿到解析的IP地址,建立TCP连接; -> 3. 向IP地址,发送HTTP请求; -> 4. 服务器处理请求; -> 5. 返回响应结果; -> 6. 关闭TCP连接; -> 7. 浏览器解析HTML; -> 8. 浏览器布局渲染; -> -> 咱们目前这细讲dns,关于这个问题,每一步后续都会有文章讲解。 - - - -## 解析过程 - -先看图 - -![解析图](https://raw.githubusercontent.com/DreamCats/PicBed/master/20191121165025.png) - -- 请求一旦发起,若是chrome浏览器,先在浏览器找之前有没有缓存过的域名所对应的ip地址,有的话,直接跳过dns解析了,若是没有,就会找硬盘的hosts文件,看看有没有,有的话,直接找到hosts文件里面的ip -- 如果本地的hosts文件没有能的到对应的ip地址,浏览器会发出一个dns请求到本地dns服务器,本地dns服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动等。 -- 查询你输入的网址的DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向DNS根服务器进行查询。 -- 本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。 -- 最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。 - -## DNS是? - -DNS(Domain Name System,域名系统),因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)。 - -通俗的讲,我们更习惯于记住一个网站的名字,比如www.baidu.com,而不是记住它的ip地址,比如:167.23.10.2。而计算机更擅长记住网站的ip地址,而不是像www.baidu.com等链接。因为,DNS就相当于一个电话本,比如你要找www.baidu.com这个域名,那我翻一翻我的电话本,我就知道,哦,它的电话(ip)是167.23.10.2。 - -## 查询方式 - -### 递归解析 - -当局部DNS服务器自己不能回答客户机的DNS查询时,它就需要向其他DNS服务器进行查询。此时有两种方式。局部DNS服务器自己负责向其他DNS服务器进行查询,一般是先向该域名的根域服务器查询,再由根域名服务器一级级向下查询。最后得到的查询结果返回给局部DNS服务器,再由局部DNS服务器返回给客户端。 - -### 迭代解析 - -当局部DNS服务器自己不能回答客户机的DNS查询时,也可以通过迭代查询的方式进行解析。局部DNS服务器不是自己向其他DNS服务器进行查询,而是把能解析该域名的其他DNS服务器的IP地址返回给客户端DNS程序,客户端DNS程序再继续向这些DNS服务器进行查询,直到得到查询结果为止。也就是说,迭代解析只是帮你找到相关的服务器而已,而不会帮你去查。比如说:baidu.com的服务器ip地址在192.168.4.5这里,你自己去查吧,本人比较忙,只能帮你到这里了。 - -## dns域名城空间组织方式 - -我们在前面有说到根DNS服务器,域DNS服务器,这些都是DNS域名称空间的组织方式。按其功能命名空间中用来描述 DNS 域名称的五个类别的介绍详见下表中,以及与每个名称类型的示例 - -| 名称类型 | 说明 | 示例 | -| :------: | :----------------------------------------------------------: | :-----------------------------: | -| 根域 | DNS域名中使用时,规定由尾部句点(.)来指定名称位于根或更高级的域名层次结构 | 单个句点(.)或句点用于末尾的名称 | -| 顶级域 | 用来指定某个国家/地区或组织使用的名称的类型名称 | .com | -| 第二层域 | 个人或组织Internet上使用的注册名称 | qq.com | -| 子域 | 已注册的二级域名派生的域名,通俗的讲就是网站名 | www.qq.com | -| 主机名 | 通常情况下,DNS域名的最左侧的标签标示网络上的特定计算机,如h1 | h1.www.qq.com | - -## DNS负载均衡 - -当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址,在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等。 - -## DNS几个问题 - -### 为什么域名解析用UDP协议? - -因为UDP快啊!UDP的DNS协议只要一个请求、一个应答就好了。而使用基于TCP的DNS协议要三次握手、发送数据以及应答、四次挥手。但是UDP协议传输内容不能超过512字节。不过客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。 - -### 为什么区域传送用TCP协议? - -因为TCP协议可靠性好啊!你要从主DNS上复制内容啊,你用不可靠的UDP? 因为TCP协议传输的内容大啊,你用最大只能传512字节的UDP协议?万一同步的数据大于512字节,你怎么办? \ No newline at end of file diff --git "a/Interview/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\216\237\347\220\206-http\351\202\243\344\272\233\344\272\213\345\204\277.md" "b/Interview/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\216\237\347\220\206-http\351\202\243\344\272\233\344\272\213\345\204\277.md" deleted file mode 100644 index a5e07cc7..00000000 --- "a/Interview/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\216\237\347\220\206-http\351\202\243\344\272\233\344\272\213\345\204\277.md" +++ /dev/null @@ -1,355 +0,0 @@ ---- -title: 计算机网络原理-http那些事儿 -author: DreamCat -id: 2 -date: 2019-11-21 15:31:10 -tags: 计算机网络 -categories: 知识 ---- -## 引言 - -> 想必大家都用过浏览器吧? -> -> 在浏览器上输入url,居然能渲染出这么多信息,是不是感觉好6呀? -> -> 本文简单聊一下http,当然面试的时候也会经常问这种类型相关的题。 - - - -## 概念 - -- URI:Uniform Resource Identifier:统一资源标识符 -- URL:Uniform Resource Locator:统一资源定位符,eg:https://www.baidu.com -- URN:Uniform Resource Name:统一资源名称 - -### 请求和响应报文 - -#### 请求报文 - -简单来说: - -- 请求行:Request Line -- 请求头:Request Headers -- 请求体:Request Body - -#### 响应报文 - -简单来说: - -- 状态行:Status Line -- 响应头:Response Headers -- 响应体:Response Body - -## HTTP方法 - -客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。 - -### GET - -> 获取资源 - -当前网络请求中,绝大部分使用的是 GET 方法。 - -### HEAD - -> 获取报文头部 - -和 GET 方法类似,但是不返回报文实体主体部分。 - -主要用于确认 URL 的有效性以及资源更新的日期时间等。 - -### POST - -> 传输实体主体 - -POST 主要用来传输数据,而 GET 主要用来获取资源。 - -更多 POST 与 GET 的比较见后 - -### PUT - -> 上传文件 - -由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。 - -### PATCH - -> 对资源进行部分修改 - -PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。 - -### DELETE - -> 删除文件 - -与 PUT 功能相反,并且同样不带验证机制。 - -### OPTIONS - -> 查询支持的方法 - -查询指定的 URL 能够支持的方法。 - -会返回 `Allow: GET, POST, HEAD, OPTIONS` 这样的内容。 - -### CONNECT - -> 要求在与代理服务器通信时建立隧道 - -使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。 - -## HTTP状态码 - -服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。 - -| 状态码 | 类别 | 含义 | -| :----: | :------------------------------: | :------------------------: | -| 1XX | Informational(信息性状态码) | 接收的请求正在处理 | -| 2XX | Success(成功状态码) | 请求正常处理完毕 | -| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 | -| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 | -| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 | - -### 1XX 信息 - -- **100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 - -### 2XX 成功 - -- **200 OK** -- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 -- **206 Partial Content** :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 - -### 3XX 重定向 - -- **301 Moved Permanently** :永久性重定向 -- **302 Found** :临时性重定向 -- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。 -- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 -- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 - -### 4XX 客户端错误 - -- **400 Bad Request** :请求报文中存在语法错误。 -- **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 -- **403 Forbidden** :请求被拒绝。 -- **404 Not Found** - -### 5XX 服务器错误 - -- **500 Internal Server Error** :服务器正在执行请求时发生错误。 -- **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 - -## HTTP首部 - -有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。 - -各种首部字段及其含义如下(**不需要全记,仅供查阅):** - -### 通用首部字段 - -| 首部字段名 | 说明 | -| :---------------: | :----------------------------------------: | -| Cache-Control | 控制缓存的行为 | -| Connection | 控制不再转发给代理的首部字段、管理持久连接 | -| Date | 创建报文的日期时间 | -| Pragma | 报文指令 | -| Trailer | 报文末端的首部一览 | -| Transfer-Encoding | 指定报文主体的传输编码方式 | -| Upgrade | 升级为其他协议 | -| Via | 代理服务器的相关信息 | -| Warning | 错误通知 | - -### 请求首部字段 - - - -| 首部字段名 | 说明 | -| :-----------------: | :---------------------------------------------: | -| Accept | 用户代理可处理的媒体类型 | -| Accept-Charset | 优先的字符集 | -| Accept-Encoding | 优先的内容编码 | -| Accept-Language | 优先的语言(自然语言) | -| Authorization | Web 认证信息 | -| Expect | 期待服务器的特定行为 | -| From | 用户的电子邮箱地址 | -| Host | 请求资源所在服务器 | -| If-Match | 比较实体标记(ETag) | -| If-Modified-Since | 比较资源的更新时间 | -| If-None-Match | 比较实体标记(与 If-Match 相反) | -| If-Range | 资源未更新时发送实体 Byte 的范围请求 | -| If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) | -| Max-Forwards | 最大传输逐跳数 | -| Proxy-Authorization | 代理服务器要求客户端的认证信息 | -| Range | 实体的字节范围请求 | -| Referer | 对请求中 URI 的原始获取方 | -| TE | 传输编码的优先级 | -| User-Agent | HTTP 客户端程序的信息 | - -### 响应首部字段 - -| 首部字段名 | 说明 | -| :----------------: | :--------------------------: | -| Accept-Ranges | 是否接受字节范围请求 | -| Age | 推算资源创建经过时间 | -| ETag | 资源的匹配信息 | -| Location | 令客户端重定向至指定 URI | -| Proxy-Authenticate | 代理服务器对客户端的认证信息 | -| Retry-After | 对再次发起请求的时机要求 | -| Server | HTTP 服务器的安装信息 | -| Vary | 代理服务器缓存的管理信息 | -| WWW-Authenticate | 服务器对客户端的认证信息 | - -### 实体首部字段 - -| 首部字段名 | 说明 | -| :--------------: | :--------------------: | -| Allow | 资源可支持的 HTTP 方法 | -| Content-Encoding | 实体主体适用的编码方式 | -| Content-Language | 实体主体的自然语言 | -| Content-Length | 实体主体的大小 | -| Content-Location | 替代对应资源的 URI | -| Content-MD5 | 实体主体的报文摘要 | -| Content-Range | 实体主体的位置范围 | -| Content-Type | 实体主体的媒体类型 | -| Expires | 实体主体过期的日期时间 | -| Last-Modified | 资源的最后修改日期时间 | - -## 具体应用 - -举一点例子即可 - -### Cookies - -HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 - -Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 - -Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。 - -**用途** - -- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) -- 个性化设置(如用户自定义设置、主题等) -- 浏览器行为跟踪(如跟踪分析用户行为等) - -### Session - -除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 - -Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 - -使用 Session 维护用户登录状态的过程如下: - -- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; -- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; -- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; -- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 - -应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 - -### Cookie与Session选择 - -- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session; -- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; -- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 - -## HTTPS - -HTTP 有以下安全性问题: - -- 使用明文进行通信,内容可能会被窃听; -- 不验证通信方的身份,通信方的身份有可能遭遇伪装; -- 无法证明报文的完整性,报文有可能遭篡改。 - -HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。 - -通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 - -### 加密 - -#### 对称密钥加密 - -对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。 - -- 优点:运算速度快 -- 缺点:无法安全地将密钥传输给通信方 - -#### 非对称密钥加密 - -非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。 - -公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。 - -非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。 - -- 优点:可以更安全地将公开密钥传输给通信发送方; -- 缺点:运算速度慢。 - -### HTTPS采用的加密方式 - -HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。 - -确保传输安全过程(其实就是rsa原理): - -1. Client给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。 -2. Server确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。 -3. Client确认数字证书有效,然后生成呀一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给Server。 -4. Server使用自己的私钥,获取Client发来的随机数(Premaster secret)。 -5. Client和Server根据约定的加密方法,使用前面的三个随机数,生成”对话密钥”(session key),用来加密接下来的整个对话过程。 - -![](https://raw.githubusercontent.com/DreamCats/PicBed/master/20191121192634.png) - -### 认证 - -通过使用 **证书** 来对通信方进行认证。 - -数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 - -服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 - -进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。 - -### HTTPS的缺点 - -- 因为需要进行加密解密等过程,因此速度会更慢; -- 需要支付证书授权的高额费用。 - -## HTTP几个问题 - -### **代浏览器在与服务器建立了一个 TCP 连接后是否会在一个 HTTP 请求完成后断开?什么情况下会断开?** - -在 HTTP/1.0 中,一个服务器在发送完一个 HTTP 响应后,会断开 TCP 链接。但是这样每次请求都会重新建立和断开 TCP 连接,代价过大。所以虽然标准中没有设定,某些服务器对 Connection: keep-alive 的 Header 进行了支持。意思是说,完成这个 HTTP 请求之后,不要断开 HTTP 请求使用的 TCP 连接。这样的好处是连接可以被重新使用,之后发送 HTTP 请求的时候不需要重新建立 TCP 连接,以及如果维持连接,那么 SSL 的开销也可以避免。 - -持久连接:既然维持 TCP 连接好处这么多,HTTP/1.1 就把 Connection 头写进标准,并且默认开启持久连接,除非请求中写明 Connection: close,那么浏览器和服务器之间是会维持一段时间的 TCP 连接,不会一个请求结束就断掉。 - -默认情况下建立 TCP 连接不会断开,只有在请求报头中声明 Connection: close 才会在请求完成后关闭连接。 - -### 一个 TCP 连接可以对应几个 HTTP 请求? - -如果维持连接,一个 TCP 连接是可以发送多个 HTTP 请求的。 - -### 一个 TCP 连接中 HTTP 请求发送可以一起发送么(比如一起发三个请求,再三个响应一起接收)? - -HTTP/1.1 存在一个问题,单个 TCP 连接在同一时刻只能处理一个请求,意思是说:两个请求的生命周期不能重叠,任意两个 HTTP 请求从开始到结束的时间在同一个 TCP 连接里不能重叠。 - -在 HTTP/1.1 存在 Pipelining 技术可以完成这个多个请求同时发送,但是由于浏览器默认关闭,所以可以认为这是不可行的。在 HTTP2 中由于 Multiplexing 特点的存在,多个 HTTP 请求可以在同一个 TCP 连接中并行进行。 - -那么在 HTTP/1.1 时代,浏览器是如何提高页面加载效率的呢?主要有下面两点: - -- 维持和服务器已经建立的 TCP 连接,在同一连接上顺序处理多个请求。 -- 和服务器建立多个 TCP 连接。 - -### 为什么有的时候刷新页面不需要重新建立 SSL 连接? - -TCP 连接有的时候会被浏览器和服务端维持一段时间。TCP 不需要重新建立,SSL 自然也会用之前的。 - -### 浏览器对同一 Host 建立 TCP 连接到数量有没有限制? - -假设我们还处在 HTTP/1.1 时代,那个时候没有多路传输,当浏览器拿到一个有几十张图片的网页该怎么办呢?肯定不能只开一个 TCP 连接顺序下载,那样用户肯定等的很难受,但是如果每个图片都开一个 TCP 连接发 HTTP 请求,那电脑或者服务器都可能受不了,要是有 1000 张图片的话总不能开 1000 个TCP 连接吧,你的电脑同意 NAT 也不一定会同意。 - -有。Chrome 最多允许对同一个 Host 建立六个 TCP 连接。不同的浏览器有一些区别。 - -如果图片都是 HTTPS 连接并且在同一个域名下,那么浏览器在 SSL 握手之后会和服务器商量能不能用 HTTP2,如果能的话就使用 Multiplexing 功能在这个连接上进行多路传输。不过也未必会所有挂在这个域名的资源都会使用一个 TCP 连接去获取,但是可以确定的是 Multiplexing 很可能会被用到。 - -如果发现用不了 HTTP2 呢?或者用不了 HTTPS(现实中的 HTTP2 都是在 HTTPS 上实现的,所以也就是只能使用 HTTP/1.1)。那浏览器就会在一个 HOST 上建立多个 TCP 连接,连接数量的最大限制取决于浏览器设置,这些连接会在空闲的时候被浏览器用来发送新的请求,如果所有的连接都正在发送请求呢?那其他的请求就只能等等了。 \ No newline at end of file diff --git a/Interview/src/DFSDemo.java b/Interview/src/DFSDemo.java deleted file mode 100644 index de449358..00000000 --- a/Interview/src/DFSDemo.java +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @program JavaBooks - * @description: 二叉树的深度搜索遍历 - * @author: mf - * @create: 2019/10/20 02:17 - */ - -/** - * 其实就是二叉树的后序遍历 - */ -public class DFSDemo { - public void DFS(TreeNode node) { - if (node == null) return; - if (node.left != null) DFS(node.left); - if (node.right != null) DFS(node.right); - System.out.println(node.val); - } -} diff --git a/Interview/src/LRUDemo.md b/Interview/src/LRUDemo.md deleted file mode 100644 index dee79fb1..00000000 --- a/Interview/src/LRUDemo.md +++ /dev/null @@ -1,59 +0,0 @@ -## 问题描述 -- 请设计一个LRU,伪代码... -## 思路 -- 双向链表+Hashmap -## 伪代码 -- 简单定义节点Node -```java -class Node { - int key; - int value; - Node pre; - Node next; -} -``` -- 定义Hashmap -```java -HashMap map = new HashMap<>(); -``` -- get操作 -```java -public int get(int key) { - if (map.containsKey(key)) { - Node n = map.get(key); - remove(n); // 链表 - setHead(n); // 链表 - return n.value; - } - return -1; - } -``` -- 由于当一个节点通过key来访问到这个节点,那么这个节点就是刚被访问了, -- 就把这个节点删除掉,然后放到队列头,这样队列的头部都是最近访问的, -- 队列尾部是最近没有被访问的。 - -- set操作 -```java -public void set(int key, int value) { - if (map.containsKey(key)) { - Node old = map.get(key); - old.value = value; - remove(old); - setHead(old); - } else { - Node created = new Node(key, value); - if (map.size() >= capacity) { - map.remove(end.key); - remove(end); - setHead(created); - } else { - setHead(created); - } - map.put(key, created); - } - } -``` -- 由于set操作是找到这个键值对,然后修改value的值,由于这个节点被访问了,所以删除他,把这个节点放到头部, -- 如果这个节点不存在,那么就新建一个节点,把这个节点放到头部,同时map增加这个键值对。 -- 上述中所有过程都是先访问map,由于hashmap是O(1)的时间复杂度,而且双向链表的所有操作的时间复杂度在本例中都是O(1), -- 删除啦,插入头部都是O(1)时间复杂度,所以整个get和set的时间复杂度都是O(1)。 \ No newline at end of file diff --git a/Interview/src/TreeNode.java b/Interview/src/TreeNode.java deleted file mode 100644 index 901db88c..00000000 --- a/Interview/src/TreeNode.java +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @program JavaBooks - * @description: 二叉树 - * @author: mf - * @create: 2019/10/20 02:19 - */ - -public class TreeNode { - int val; - TreeNode left; - TreeNode right; - - public TreeNode(int val) { - this.val = val; - } -} diff --git a/Java/alg/README.md b/Java/alg/README.md new file mode 100644 index 00000000..75c4c542 --- /dev/null +++ b/Java/alg/README.md @@ -0,0 +1,145 @@ +> 我个人总结的一些算法题,基本上都是leetcode上的题,按照热度和面经上总结的。 + +## Easy + +- [1.两数之和](lc/1.两数之和.md) +- [7.整数反转](lc/7.整数反转.md) +- [9.回文数](lc/9.回文数.md) +- [13.罗马数字转整数](lc/13.罗马数字转整数.md) +- [14.最长公共前缀](lc/14.最长公共前缀.md) +- [20.有效的括号](lc/20.有效的括号.md) +- [21.合并两个有序链表](lc/21.合并两个有序链表.md) +- [26.删除排序数组中的重复项](lc/26.删除排序数组中的重复项.md) +- [27.移除元素](lc/27.移除元素.md) +- [28.实现strStr](lc/28.实现strStr.md) +- [35.搜索插入位置](lc/35.搜索插入位置.md) +- [53.最大子序和](lc/53.最大子序和.md) +- [58.最后一个单词的长度](lc/58.最后一个单词的长度.md) +- [66.加一](lc/66.加一.md) +- [67.二进制求和](lc/67.二进制求和.md) +- [69.x的平方根](lc/69.x的平方根.md) +- [70.爬楼梯](lc/70.爬楼梯.md) +- [83.删除排序链表中的重复元素](lc/83.删除排序链表中的重复元素.md) +- [88.合并两个有序数组](lc/88.合并两个有序数组.md) +- [100.相同的树](lc/100.相同的树.md) +- [101.对称二叉树](lc/101.对称二叉树.md) +- [104.二叉树的最大深度](lc/104.二叉树的最大深度.md) +- [107.二叉树的层序遍历2](lc/107.二叉树的层序遍历2.md) +- [108.将有序数组转换为二叉搜索树](lc/108.将有序数组转换为二叉搜索树.md) +- [110.平衡二叉树](lc/110.平衡二叉树.md) +- [111.二叉树的最小深度](lc/111.二叉树的最小深度.md) +- [112.路径总和](lc/112.路径总和.md) +- [118.杨辉三角](lc/118.杨辉三角.md) +- [121.买卖股票的最佳时机](lc/121.买卖股票的最佳时机.md) +- [122.买卖股票的最佳时机2](lc/122.买卖股票的最佳时机2.md) +- [125.验证回文串](lc/125.验证回文串.md) +- [136.只出现一次的数字](lc/136.只出现一次的数字.md) +- [141.环形链表](lc/141.环形链表.md) +- [155.最小栈](lc/155.最小栈.md) +- [160.相交链表](lc/160.相交链表.md) +- [167.两数之和2-输入有序数组](lc/167.两数之和2-输入有序数组.md) +- [172.阶乘后的零](lc/172.阶乘后的零.md) +- [191.位1的个数](lc/191.位1的个数.md) +- [204.计数质数](lc/204.计数质数.md) +- [205.同构字符串](lc/205.同构字符串.md) +- [206.反转链表](lc/206.反转链表.md) +- [217.存在重复元素](lc/217.存在重复元素.md) +- [219.存在重复元素2](lc/219.存在重复元素2.md) +- [225.用队列实现栈](lc/225.用队列实现栈.md) +- [226.翻转二叉树](lc/226.翻转二叉树.md) +- [231.2的幂](lc/231.2的幂.md) +- [232.用栈实现队列](lc/232.用栈实现队列.md) +- [234.回文链表](lc/234.回文链表.md) +- [237.删除链表中的节点](lc/237.删除链表中的节点.md) +- [242.有效的字母异位词](lc/242.有效的字母异位词.md) +- [268.丢失的数字](lc/268.丢失的数字.md) +- [278.第一个错误的版本](lc/278.第一个错误的版本.md) +- [283.移动零](lc/283.移动零.md) +- [290.单词规律](lc/290.单词规律.md) +- [326.3的幂](lc/326.3的幂.md) +- [344.反转字符串](lc/344.反转字符串.md) +- [350.两个数组的交集2](lc/350.两个数组的交集2.md) +- [367.有效的完全平方数](lc/367.有效的完全平方数.md) +- [371.两整数之和](lc/371.两整数之和.md) +- [387.字符串中的第一个唯一字符](lc/387.字符串中的第一个唯一字符.md) +- [392.判断子序列](lc/392.判断子序列.md) +- [409.最长回文串](lc/409.最长回文串.md) +- [414.第三大的数](lc/414.第三大的数.md) +- [415.字符串相加](lc/415.字符串相加.md) +- [434.字符串中的单词数](lc/434.字符串中的单词数.md) +- [455.分发饼干](lc/455.分发饼干.md) +- [461.汉明距离](lc/461.汉明距离.md) +- [485.最大连续1的个数](lc/485.最大连续1的个数.md) +- [509.斐波那契数](lc/509.斐波那契数.md) +- [543.二叉树的直径](lc/543.二叉树的直径.md) +- [566.重塑矩阵](lc/566.重塑矩阵.md) +- [572.另一个树的子树](lc/572.另一个树的子树.md) +- [594.最长和谐子序列](lc/594.最长和谐子序列.md) +- [605.种花问题](lc/605.种花问题.md) +- [617.合并二叉树](lc/617.合并二叉树.md) +- [628.三个数的最大乘积](lc/628.三个数的最大乘积.md) +- [637.二叉树的层平均值](lc/637.二叉树的层平均值.md) +- [645.错误的集合](lc/645.错误的集合.md) +- [665.非递减数列](lc/665.非递减数列.md) +- [671.二叉树中第二小的节点](lc/671.二叉树中第二小的节点.md) +- [674.最长连续递增序列](lc/674.最长连续递增序列.md) +- [680.验证回文字符串2](lc/680.验证回文字符串2.md) +- [693.交替位二进制数](lc/693.交替位二进制数.md) +- [696.计数二进制子串](lc/696.计数二进制子串.md) +- [704.二分查找](lc/704.二分查找.md) +- [724.寻找数组的中心下标](lc/724.寻找数组的中心下标.md) +- [724.寻找数组的中心下标](lc/724.寻找数组的中心下标.md) +- [747.至少是其他数字两倍的最大数](lc/747.至少是其他数字两倍的最大数.md) +- [836.矩形重叠](lc/836.矩形重叠.md) +- [876.链表的中间结点](lc/876.链表的中间结点.md) +- [914.卡牌分组](lc/914.卡牌分组.md) +- [1013.将数组分成和相等的三个部分](lc/1013.将数组分成和相等的三个部分.md) +- [1071.字符串的最大公因子](lc/1071.字符串的最大公因子.md) +- [1103.分糖果2](lc/1103.分糖果2.md) + +## Medium +- [2.两数相加](lc/2.两数相加.md) +- [3.无重复字符的最长子串](lc/3.无重复字符的最长子串.md) +- [5.最长回文子串](lc/5.最长回文子串.md) +- [6.Z字形变换](lc/6.Z字形变换.md) +- [11.盛最多水的容器](lc/11.盛最多水的容器.md) +- [15.三数之和](lc/15.三数之和.md) +- [17.电话号码的字母组合](lc/17.电话号码的字母组合.md) +- [19.删除链表的倒数第N个结点](lc/19.删除链表的倒数第N个结点.md) +- [22.括号生成](lc/22.括号生成.md) +- [24.两两交换链表中的节点](lc/24.两两交换链表中的节点.md) +- [31.下一个排列](lc/31.下一个排列.md) +- [33.搜索旋转排序数组](lc/33.搜索旋转排序数组.md) +- [34.在排序数组中查找元素的第一个和最后一个位置](lc/34.在排序数组中查找元素的第一个和最后一个位置.md) +- [36.有效的数独](lc/36.有效的数独.md) +- [39.组合总和](lc/39.组合总和.md) +- [40.组合总和2](lc/40.组合总和2.md) +- [43.字符串相乘](lc/43.字符串相乘.md) +- [46.全排列](lc/46.全排列.md) +- [47.全排列2](lc/47.全排列2.md) +- [55.跳跃游戏](lc/55.跳跃游戏.md) +- [56.合并区间](lc/56.合并区间.md) +- [62.不同路径](lc/62.不同路径.md) +- [63.不同路径2](lc/63.不同路径2.md) +- [64.最小路径和](lc/64.最小路径和.md) +- [74. 搜索二维矩阵](lc/74.搜索二维矩阵.md) +- [75.颜色分类](lc/75.颜色分类.md) +- [77.组合](lc/77.组合.md) +- [78.子集](lc/78.子集.md) +- [79.单词搜索](lc/79.单词搜索.md) +- [82.删除排序链表中的重复元素2](lc/82.删除排序链表中的重复元素2.md) +- [86.分隔链表](lc/86.分隔链表.md) +- [90.子集2](lc/90.子集2.md) +- [91.解码方法](lc/91.解码方法.md) +- [92.反转链表2](lc/92.反转链表2.md) +- [93.复原IP地址](lc/93.复原IP地址.md) +- [94.二叉树的中序遍历](lc/94.二叉树的中序遍历.md) +- [96.不同的二叉搜索树](lc/96.不同的二叉搜索树.md) +- [98.验证二叉搜索树](lc/98.验证二叉搜索树.md) +- [102.二叉树的层序遍历](lc/102.二叉树的层序遍历.md) +- [113.路径总和2](lc/113.路径总和2.md) +- [120.三角形最小路径和](lc/120.三角形最小路径和.md) + +## 笔试 + + \ No newline at end of file diff --git "a/Java/alg/lc/1.\344\270\244\346\225\260\344\271\213\345\222\214.md" "b/Java/alg/lc/1.\344\270\244\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 00000000..4afd984f --- /dev/null +++ "b/Java/alg/lc/1.\344\270\244\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,97 @@ +# 1.两数之和 + +[url](https://leetcode-cn.com/problems/two-sum/) + +## 题目 + +给定一个整数数组 `nums` 和一个整数目标值 `target`,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标。 + +你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 + +你可以按任意顺序返回答案。 + +``` +输入:nums = [2,7,11,15], target = 9 +输出:[0,1] +解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 + +输入:nums = [3,2,4], target = 6 +输出:[1,2] +``` + +## 方法 + +哈希映射 + +1. 可以用中间空间来协调或者帮助,采用哈希是因为比较快,为啥呢?输入的x元素通过哈希函数将其映射到某个位置上,那岂不是非常快?顶多通过计算就知道x的位置了。 +2. 采用哈希呢,你正好key对应x,value对应的是索引i +3. 核心过程:每遍历一次就要做一些事情 + 1. 算一下x与target的差为sub + 2. 我哈希空间是否存在sub, + 3. 存在的话,返回x的索引i和sub对应的value,即i。 + 4. 不存在,那好办,我将x设置为key,其索引i存value,放到该哈希空间内,目的是方便下一次遍历的sub是否能找的到,如果找的到,那岂不是x+sub=target? 两数之和? + +## code + +### js + +```js +let twoSum = function(nums, target) { + let m = new Map(); + for (let i = 0; i < nums.length; i++) { // 遍历 + const sub = target - nums[i] + if (m.has(sub)) { + return [m.get(sub), i]; + } else { + m.set(nums[i], i); + } + } + return []; +} +console.log(twoSum([2, 7, 11, 15], 9)) +``` + +### go + +```go +func twoSum(nums []int, target int) []int { + // 创建map + m := make(map[int]int, len(nums)) + // 遍历元素和目标值的差是否在该map内 + for i, v := range nums { + sub := target - v // 当前元素和目标值的差 + if j, ok := m[sub]; ok { + return []int{j, i}; + } else { + m[v] = i // 差若是不在当前map内,则存到map + } + } + return nil +} +func main() { + nums := []int{2, 7, 11, 15} + target := 9 + fmt.Println(twoSum(nums, target)) +} +``` + +### java + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + if (nums == null || nums.length == 0) return null; + HashMap map = new HashMap<>(); + for (int i = 0; i < nums.length; i++) { + int sub = target - nums[i]; + if (map.containsKey(sub)) { + return new int[] {map.get(sub), i}; + } else { + map.put(nums[i], i); + } + } + return new int[0]; + } +} +``` + diff --git "a/Java/alg/lc/100.\347\233\270\345\220\214\347\232\204\346\240\221.md" "b/Java/alg/lc/100.\347\233\270\345\220\214\347\232\204\346\240\221.md" new file mode 100644 index 00000000..14aee92a --- /dev/null +++ "b/Java/alg/lc/100.\347\233\270\345\220\214\347\232\204\346\240\221.md" @@ -0,0 +1,82 @@ +# 100. 相同的树 + +[url](https://leetcode-cn.com/problems/same-tree/) + +## 题目 + +给定两个二叉树,编写一个函数来检验它们是否相同。 + +如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。 + + +``` +输入: 1 1 + / \ / \ + 2 3 2 3 + [1,2,3], [1,2,3] +输出: true +输入: 1 1 + / \ + 2 2 + [1,2], [1,null,2] +输出: false +输入: 1 1 + / \ / \ + 2 1 1 2 + [1,2,1], [1,1,2] +输出: false +``` + +## 方法 + +可递归,可遍历 + +相同的树,三个结束条件 + +- 如果`p`和`q`都不为空,则为true,毕竟相同的树,节点数量和方向都得一样吧? +- 如果`p`和`q`其中一个为空,则为false,说明节点数量或者方向不对呀? +- 如果`p`和`q`的值不相等的话,直接false + +## code + +### js + +```js +let isSameTree = (p, q) => { + if (p === null && q === null) return true; + if (p === null || q === null) return false; + if (p.val !== q.val) return false; + return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); +} +``` + +### go + +```go +func isSameTree(p, q *TreeNode) bool { + if p == nil && q == nil { + return true + } + if p == nil || q == nil { + return false + } + if p.Val != q.Val { + return false + } + return isSameTree(p.Left, q.Left) && isSameTree(p.Right, q.Right) +} +``` + +### java + +```java +class Solution { + public boolean isSameTree(TreeNode p, TreeNode q) { + if(p == null && q == null) return true; + if(p == null || q == null) return false; + if(p.val != q.val) return false; + return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); + } +} +``` + diff --git "a/Java/alg/lc/101.\345\257\271\347\247\260\344\272\214\345\217\211\346\240\221.md" "b/Java/alg/lc/101.\345\257\271\347\247\260\344\272\214\345\217\211\346\240\221.md" new file mode 100644 index 00000000..91ce7c80 --- /dev/null +++ "b/Java/alg/lc/101.\345\257\271\347\247\260\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,94 @@ +# 101. 对称二叉树 + +[url](https://leetcode-cn.com/problems/symmetric-tree/) + +## 题目 + +给定一个二叉树,检查它是否是镜像对称的。 + + +``` +例如,二叉树 [1,2,2,3,4,4,3] 是对称的。 + 1 + / \ + 2 2 + / \ / \ +3 4 4 3 +但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的: + 1 + / \ + 2 2 + \ \ + 3 3 +``` + +## 方法 + +可递归,可遍历 + +对称的树,三个结束条件 + +- 如果`p`和`q`都不为空,则为true,毕竟对称的树,节点数量和方向都得一样吧? +- 如果`p`和`q`其中一个为空,则为false,说明节点数量或者方向不对呀? +- 如果`p`和`q`的值不相等的话,直接false +- 和相同树不一样的是递归`p`的左节点和`q`的右节点,`p`的右节点和`q`的右节点 + +## code + +### js + +```js +let isSymmertric = root => { + let isSym = (p, q) => { + if (p === null && q === null) return true; + if (p === null || q === null) return false; + if (p.val !== q.val) return false; + return isSym(p.left, q.right) && isSym(p.right, q.left); + } + + if (root === null) return true; + return isSym(root.left, root.right) +} +``` + +### go + +```go +func isSymmetric(root *TreeNode) bool { + var isSymmetric1 func (t1, t2 *TreeNode) bool + isSymmetric1 = func (t1, t2 *TreeNode) bool { + if t1 == nil && t2 == nil { + return true + } + if t1 == nil || t2 == nil { + return false + } + if t1.Val != t2.Val { + return false + } + return isSymmetric1(t1.Left, t2.Right) && isSymmetric1(t1.Right, t2.Left) + } + if root == nil { + return true + } + return isSymmetric1(root.Left, root.Right) +} +``` + +### java + +```java +class Solution { + public boolean isSymmetric(TreeNode root) { + if (root == null) return true; + return isSymmetric(root.left, root.right); + } + private boolean isSymmetric(TreeNode t1, TreeNode t2) { + if (t1 == null && t2 == null) return true; + if (t1 == null || t2 == null) return false; + if (t1.val != t2.val) return false; + return isSymmetric(t1.left, t2.right) && isSymmetric(t1.right, t2.left); + } +} +``` + diff --git "a/Java/alg/lc/1013.\345\260\206\346\225\260\347\273\204\345\210\206\346\210\220\345\222\214\347\233\270\347\255\211\347\232\204\344\270\211\344\270\252\351\203\250\345\210\206.md" "b/Java/alg/lc/1013.\345\260\206\346\225\260\347\273\204\345\210\206\346\210\220\345\222\214\347\233\270\347\255\211\347\232\204\344\270\211\344\270\252\351\203\250\345\210\206.md" new file mode 100644 index 00000000..f5eff23a --- /dev/null +++ "b/Java/alg/lc/1013.\345\260\206\346\225\260\347\273\204\345\210\206\346\210\220\345\222\214\347\233\270\347\255\211\347\232\204\344\270\211\344\270\252\351\203\250\345\210\206.md" @@ -0,0 +1,115 @@ +# 1013. 将数组分成和相等的三个部分 + + + +[url](https://leetcode-cn.com/problems/partition-array-into-three-parts-with-equal-sum/) + + +## 题目 +给你一个整数数组 A,只有可以将其划分为三个和相等的非空部分时才返回 true,否则返回 false。 + +形式上,如果可以找出索引 `i+1 < j` 且满足 `A[0] + A[1] + ... + A[i] == A[i+1] + A[i+2] + ... + A[j-1] == A[j] + A[j-1] + ... + A[A.length - 1]` 就可以将数组三等分。 + + +``` +输入:[0,2,1,-6,6,-7,9,1,2,0,1] +输出:true +解释:0 + 2 + 1 = -6 + 6 - 7 + 9 + 1 = 2 + 0 + 1 +输入:[0,2,1,-6,6,7,9,-1,2,0,1] +输出:false +输入:[3,3,6,5,-2,2,5,1,-9,4] +输出:true +解释:3 + 3 = 6 = 5 - 2 + 2 + 5 + 1 - 9 + 4 +``` + + +## 方法 + + +## code + +### js + +```js +let canThreePartsEqualSum = A => { + let sum = 0; + // 遍历数组求和 + A.forEach(num => sum += num); + // 数组A的和如果不能被3整除直接返回false + if (sum % 3 !== 0) + return false; + // 遍历数组累加,每累加到目标值cnt加1, 表示又找到了1段 + sum = Math.floor(sum / 3); + let curSum = 0, cnt = 0; + for (let val of A) { + curSum += val; + if (curSum === sum) { + cnt++; + curSum = 0; + } + } + return cnt === 3 || (cnt > 3 && sum === 0); +}; +console.log(canThreePartsEqualSum([0,2,1,-6,6,-7,9,1,2,0,1])); +console.log(canThreePartsEqualSum([0,2,1,-6,6,7,9,-1,2,0,1])); +console.log(canThreePartsEqualSum([3,3,6,5,-2,2,5,1,-9,4])); +``` + +### go + +```go +func canThreePartsEqualSum(A []int) bool { + sum := 0 + // 遍历数组求总和 + for _, v := range A { + sum += v + } + // 数组A的和如果不能被3整除直接返回false + if sum%3 != 0 { + return false + } + // 遍历数组累加,每累加到目标值cnt加1,表示又找到1段 + sum /= 3 + curSum, cnt := 0, 0 + for _, v := range A { + curSum += v + if curSum == sum { + cnt++ + curSum = 0 + } + } + // 最后判断是否找到了3段(注意如果目标值是0的话可以大于3段) + return cnt == 3 || (cnt > 3 && sum == 0) +} +``` + +### java + +```java +class Solution { + public boolean canThreePartsEqualSum(int[] A) { + int sum = 0; + // 遍历数组求总和 + for (int num : A) { + sum += num; + } + // 数组A的和如果不能被3整除直接返回false + if (sum % 3 != 0) { + return false; + } + // 遍历数组累加,每累加到目标值cnt加1,表示又找到1段 + sum /= 3; + int curSum = 0, cnt = 0; + for (int i = 0; i < A.length; i++) { + curSum += A[i]; + if (curSum == sum) { + cnt++; + curSum = 0; + } + } + // 最后判断是否找到了3段(注意如果目标值是0的话可以大于3段) + return cnt == 3 || (cnt > 3 && sum == 0); + } +} +``` + diff --git "a/Java/alg/lc/102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" "b/Java/alg/lc/102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" new file mode 100644 index 00000000..30b6fefb --- /dev/null +++ "b/Java/alg/lc/102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" @@ -0,0 +1,117 @@ +# 102.二叉树的层序遍历 + +[url](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/) + +## 题目 + +给定一个二叉树,判断其是否是一个有效的二叉搜索树。 + +假设一个二叉搜索树具有如下特征: + +- 节点的左子树只包含小于当前节点的数。 +- 节点的右子树只包含大于当前节点的数。 +- 所有左子树和右子树自身必须也是二叉搜索树。 + + +``` +输入: + 2 + / \ + 1 3 +输出: true +输入: + 5 + / \ + 1 4 +  / \ +  3 6 +输出: false +解释: 输入为: [5,1,4,null,null,3,6]。 +  根节点的值为 5 ,但是其右子节点值为 4 。 +``` + +## 方法 + + +## code + +### js + +```js +let levelOrder = root => { + let ret = []; + let queue = [root]; + while (queue.length !== 0) { + let list = []; + let cnt = queue.length; + while (cnt-- > 0) { + let node = queue[0] + queue = queue.slice(1) + if (node === null) + continue; + list.push(node.val); + queue.push(node.left); + queue.push(node.right); + } + if (list.length !== 0) + ret.push(list.slice()); + } + return ret; +} +``` + +### go + +```go +func levelOrder(root *TreeNode) [][]int { + var ret [][]int + q := []*TreeNode{root} + for len(q) != 0{ + var list []int + cnt := len(q) + for cnt > 0 { + cnt-- + node := q[0] + q = q[1:] + if node == nil { + continue + } + list = append(list, node.Val) + q = append(q, node.Left) + q = append(q, node.Right) + } + if len(list) != 0 { + ret = append(ret, append([]int{}, list...)) + } + } + return ret +} +``` + +### java + +```java +class Solution { + public List> levelOrder(TreeNode root) { + List> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()) { + ArrayList list = new ArrayList<>(); + int cnt = queue.size(); + while (cnt-- > 0) { + TreeNode node = queue.poll(); + if (node == null) + continue; + list.add(node.val); + queue.add(node.left); + queue.add(node.right); + } + if (list.size() != 0) + ret.add(list); + } + return ret; + } +} +``` + diff --git "a/Java/alg/lc/104.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\244\247\346\267\261\345\272\246.md" "b/Java/alg/lc/104.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\244\247\346\267\261\345\272\246.md" new file mode 100644 index 00000000..319a236e --- /dev/null +++ "b/Java/alg/lc/104.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\244\247\346\267\261\345\272\246.md" @@ -0,0 +1,81 @@ +# 104. 二叉树的最大深度 + +[url](https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/) + +## 题目 + +给定一个二叉树,找出其最大深度。 + +二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 + +说明: 叶子节点是指没有子节点的节点。 + + +``` +给定二叉树 [3,9,20,null,null,15,7], + 3 + / \ + 9 20 + / \ + 15 7 +``` + +## 方法 + +可递归,可遍历 + +- 注意结束条件,如果为null则返回0 +- 递归不断比较左右谁的深度大 + +## code + +### js + +```js +let maxDepth = root => { + return root === null ? 0 : 1 + Math.max(maxDepth(root.left), maxDepth(root.right)); +} +``` + +### go + +```go +func maxDepth(root *TreeNode) int { + if root == nil { + return 0 + } + return 1 + Max(maxDepth(root.Left), maxDepth(root.Right)) +} +func Max(a, b int) int { + if a > b { + return a + } else { + return b + } +} +``` + +### java + +```java +class Solution { + public int maxDepth(TreeNode root) { + if (root == null) return 0; + int depth = 0; + Queue> queue = new LinkedList<>(); + queue.add(new Pair(root, 1)); + while (!queue.isEmpty()){ + Pair cur = queue.poll(); + root = cur.getKey(); + int curDepth = cur.getValue(); + if (root != null) { + depth = Math.max(depth, curDepth); + queue.add(new Pair(root.left, curDepth + 1)); + queue.add(new Pair(root.right, curDepth + 1)); + } + } + return depth; + } +} +``` + diff --git "a/Java/alg/lc/107.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\2062.md" "b/Java/alg/lc/107.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\2062.md" new file mode 100644 index 00000000..2048678e --- /dev/null +++ "b/Java/alg/lc/107.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\2062.md" @@ -0,0 +1,112 @@ +# 107. 二叉树的层序遍历 II + +[url](https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/) + +## 题目 + +给定一个二叉树,返回其节点值自底向上的层序遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历) + + +``` +给定二叉树 [3,9,20,null,null,15,7], + 3 + / \ + 9 20 + / \ + 15 7 +返回其自底向上的层序遍历为: +[ + [15,7], + [9,20], + [3] +] +``` + +## 方法 + +队列即可 + + +## code + +### js + +```js +var levelOrderBottom = function(root) { + // 队列 + var ret = []; + var queue = [root]; + while (queue.length !== 0) { + var list = []; + var cnt = queue.length; + while (cnt-- > 0) { + var node = queue[0]; + queue = queue.slice(1, queue.length); + if (node === null) + continue; + list.push(node.val); + queue.push(node.left); + queue.push(node.right); + } + if (list.length !== 0) { + console.log(list); + ret.push(list); + } + } + return ret.reverse(); +}; +``` + +### go + +```go +func levelOrder(root *TreeNode) [][]int { + var res [][]int + q := []*TreeNode{root} + for len(q) != 0 { + var list []int + cnt := len(q) + for cnt > 0 { + cnt-- + node := q[0] + q = q[1:] + if node == nil { + continue + } + list = append(list, node.Val) + q = append(q, node.Left) + q = append(q, node.Right) + } + if len(list) != 0 { + res = append(res, list) + } + } + return res +} +``` + +### java + +```java +class Solution { + public List> levelOrder(TreeNode root) { + List> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()) { + List list = new ArrayList<>(); + int cnt = queue.size(); + while (cnt-- > 0) { + TreeNode node = queue.poll(); + if (node == null) continue; + list.add(node.val); + queue.add(node.left); + queue.add(node.right); + } + if (list.size() != 0) ret.add(list); + } + return ret; + } +} +``` + diff --git "a/Java/alg/lc/1071.\345\255\227\347\254\246\344\270\262\347\232\204\346\234\200\345\244\247\345\205\254\345\233\240\345\255\220.md" "b/Java/alg/lc/1071.\345\255\227\347\254\246\344\270\262\347\232\204\346\234\200\345\244\247\345\205\254\345\233\240\345\255\220.md" new file mode 100644 index 00000000..d95b025c --- /dev/null +++ "b/Java/alg/lc/1071.\345\255\227\347\254\246\344\270\262\347\232\204\346\234\200\345\244\247\345\205\254\345\233\240\345\255\220.md" @@ -0,0 +1,82 @@ +# 1071. 字符串的最大公因子 + + + +[url](https://leetcode-cn.com/problems/greatest-common-divisor-of-strings/) + + +## 题目 +对于字符串 `S` 和 `T`,只有在 `S = T + ... + T`(T 自身连接 1 次或多次)时,我们才认定 “T 能除尽 S”。 + +返回最长字符串 `X`,要求满足 `X` 能除尽 `str1` 且 `X` 能除尽 `str2`。 + + + +``` +输入:str1 = "ABCABC", str2 = "ABC" +输出:"ABC" +输入:str1 = "ABABAB", str2 = "ABAB" +输出:"AB" +输入:str1 = "LEET", str2 = "CODE" +输出:"" +``` + + +## 方法 + + +## code + +### js + +```js +let gcdOfStrings = (str1, str2) => { + if ((str1 + str2) !== (str2 + str1)) + return ""; + return str2.substring(0, gcd(str1.length, str2.length)); +}; + +let gcd = (a, b) => { + return b === 0 ? a : gcd(b, a % b); +}; +console.log(gcdOfStrings("ABCABC", "ABC")); +console.log(gcdOfStrings("ABABAB", "ABAB")); +console.log(gcdOfStrings("LEET", "CODE")); +``` + +### go + +```go +func gcdOfStrings(str1 string, str2 string) string { + var gcd func(a int, b int) int + gcd = func (a int, b int) int { + if b == 0 { + return a + } else { + return gcd(b, a % b) + } + } + if str1+str2 != str2+str1 { + return "" + } + return str2[0:gcd(len(str1), len(str2))] +} +``` + +### java + +```java +class Solution { + public String gcdOfStrings(String str1, String str2) { + if (!(str1 + str2).equals(str2 + str1)) { + return ""; + } + return str2.substring(0, gcd(str1.length(), str2.length())); + } + + private int gcd(int a, int b) { + return b == 0 ? a : gcd(b, a % b); + } +} +``` + diff --git "a/Java/alg/lc/108.\345\260\206\346\234\211\345\272\217\346\225\260\347\273\204\350\275\254\346\215\242\344\270\272\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/Java/alg/lc/108.\345\260\206\346\234\211\345\272\217\346\225\260\347\273\204\350\275\254\346\215\242\344\270\272\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" new file mode 100644 index 00000000..c84a5aa4 --- /dev/null +++ "b/Java/alg/lc/108.\345\260\206\346\234\211\345\272\217\346\225\260\347\273\204\350\275\254\346\215\242\344\270\272\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -0,0 +1,88 @@ +# 108. 将有序数组转换为二叉搜索树 + +[url](https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/) + +## 题目 + +将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。 + +本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。 + +``` +给定有序数组: [-10,-3,0,5,9], +一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树: + +返回其自底向上的层序遍历为: + 0 + / \ + -3 9 + / / + -10 5 +``` + +## 方法 + + 二叉树中序遍历的逆过程哦 + - 前序遍历:根结点 ---> 左子树 ---> 右子树 + - 中序遍历:左子树---> 根结点 ---> 右子树 + - 后序遍历:左子树 ---> 右子树 ---> 根结点 + +## code + +### js + +```js +let sortedArrayToBST = nums => { + return nums === null ? null : buildTree(nums, 0, nums.length - 1); +} + +let buildTree = (nums, l, r) => { + if (l > r) return null; + let m = Math.floor(l + (r - l) / 2); + let root = TreeNode(nums[m]); + root.left = buildTree(nums, l, m - 1); + root.right = buildTree(nums, m + 1, r); + return root; +} +``` + +### go + +```go +func sortedArrayToBST(nums []int) *TreeNode { + var buildTree func (nums[] int, l, r int) *TreeNode + buildTree = func (nums[] int, l, r int) *TreeNode { + if l > r { + return nil + } + m := l + (r - l) / 2 + root := &TreeNode{Val: nums[m]} + root.Left = buildTree(nums, l, m - 1) + root.Right = buildTree(nums, m + 1, l) + return root + } + if nums == nil || len(nums) == 0 { + return nil + } + return buildTree(nums, 0, len(nums) - 1) +} +``` + +### java + +```java +class Solution { + public TreeNode sortedArrayToBST(int[] nums) { + return nums == null ? null : buildTree(nums, 0, nums.length - 1); + } + private TreeNode buildTree(int[] nums, int l, int r) { + if(l > r) return null; + int m = l + (r - l) / 2; + TreeNode root = new TreeNode(nums[m]); + root.left = buildTree(nums, l, m - 1); + root.right = buildTree(nums, m + 1, r); + return root; + } +} +``` + diff --git "a/Java/alg/lc/11.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.md" "b/Java/alg/lc/11.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.md" new file mode 100644 index 00000000..977161ee --- /dev/null +++ "b/Java/alg/lc/11.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.md" @@ -0,0 +1,89 @@ +# 11. 盛最多水的容器 + +[url](https://leetcode-cn.com/problems/container-with-most-water/) + +## 题目 + +给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 + +说明:你不能倾斜容器。 + +![](https://aliyun-lc-upload.oss-cn-hangzhou.aliyuncs.com/aliyun-lc-upload/uploads/2018/07/25/question_11.jpg) + +``` +输入:[1,8,6,2,5,4,8,3,7] +输出:49 +解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。 +输入:height = [1,1] +输出:1 +输入:height = [4,3,2,1,4] +输出:16 +输入:height = [1,2,1] +输出:2 +``` + +## 方法 + + + +## code + +### js + +```js{cmd="node"} +let maxArea = height => { + // 算面积 + let max = 0; + let l = 0, r = height.length - 1; + while (l < r) { + let min = height[l] < height[r] ? height[l++] : height[r--]; + max = Math.max(max, (r - l + 1) * min); + } + return max; +}; +``` + +### go + +```go +func maxArea(height []int) int { + max := 0 + l, r := 0, len(height) - 1 + Max := func(a int, b int) int { + if a < b { + return b + } else { + return a + } + } + for l < r { + min := 0 + if height[l] < height[r] { + min = height[l] + l++ + } else { + min = height[r] + r-- + } + max = Max(max, (r - l + 1) * min) + } + return max +} +``` + +### java + +```java +class Solution { + public int maxArea(int[] height) { + int max = 0; + int l = 0, r = height.length - 1; + while (l < r){ + int min = height[l] < height[r] ? height[l++] : height[r--]; + max = Math.max(max, (r - l + 1) * min); + } + return max; + } +} +``` + diff --git "a/Java/alg/lc/110.\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.md" "b/Java/alg/lc/110.\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.md" new file mode 100644 index 00000000..ee7b5924 --- /dev/null +++ "b/Java/alg/lc/110.\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,95 @@ +# 110. 平衡二叉树 + +[url](https://leetcode-cn.com/problems/balanced-binary-tree/) + +## 题目 + +给定一个二叉树,判断它是否是高度平衡的二叉树。 + +本题中,一棵高度平衡二叉树定义为: +> 一个二叉树每个节点的左右两个子树的高度差的绝对值不超过`1` + + + +``` +输入:root = [3,9,20,null,null,15,7] +输出:true +输入:root = [1,2,2,3,3,null,null,4,4] +输出:false +输入:root = [] +输出:true +``` + +## 方法 + +可递归,当然也可迭代 + +- 其实就是抓着平衡二叉树的定义作为判断:个节点的左右两个子树的高度差的绝对值不超过`1` + +## code + +### js + +```js +let res = true; +let isBalanced = root => { + depth(root); + return res; +} +let depth = root => { + if (root === null) return 0; + let l = depth(root.left); + let r = depth(root.right); + if (Math.abs(l - r) > 1) res = false; + return 1 + Math.max(l, r); +} +``` + +### go + +```go +func isBalanced(root *TreeNode) bool { + var res = true + var depth func (root *TreeNode) int + Max := func(a int, b int) int { + if a < b { + return b + } else { + return a + } + } + depth = func (root *TreeNode) int { + if root == nil { + return 0 + } + l := depth(root.Left) + r := depth(root.Right) + if Abs(l - r) > 1{ + res = false + } + return 1 + Max(l, r) + } + depth(root) + return res +} +``` + +### java + +```java +class Solution { + private boolean res = true; + public boolean isBalanced(TreeNode root) { + Depth(root); + return res; + } + private int Depth (TreeNode root) { + if (root == null) return 0; + int l = Depth(root.left); + int r = Depth(root.right); + if (Math.abs(l - r) > 1) res = false;; + return 1 + Math.max(l , r); + } +} +``` + diff --git "a/Java/alg/lc/1103.\345\210\206\347\263\226\346\236\2342.md" "b/Java/alg/lc/1103.\345\210\206\347\263\226\346\236\2342.md" new file mode 100644 index 00000000..874f8b5b --- /dev/null +++ "b/Java/alg/lc/1103.\345\210\206\347\263\226\346\236\2342.md" @@ -0,0 +1,92 @@ +# 1103. 分糖果 II + + + +[url](https://leetcode-cn.com/problems/distribute-candies-to-people/) + + +## 题目 +排排坐,分糖果。 + +我们买了一些糖果 `candies`,打算把它们分给排好队的 `n = num_people` 个小朋友。 + +给第一个小朋友 `1` 颗糖果,第二个小朋友 `2` 颗,依此类推,直到给最后一个小朋友 `n` 颗糖果。 + +然后,我们再回到队伍的起点,给第一个小朋友 `n + 1` 颗糖果,第二个小朋友 `n + 2` 颗,依此类推,直到给最后一个小朋友 `2 * n` 颗糖果。 + +重复上述过程(每次都比上一次多给出一颗糖果,当到达队伍终点后再次从队伍起点开始),直到我们分完所有的糖果。注意,就算我们手中的剩下糖果数不够(不比前一次发出的糖果多),这些糖果也会全部发给当前的小朋友。 + +返回一个长度为 `num_people`、元素之和为 `candies` 的数组,以表示糖果的最终分发情况(即 `ans[i]` 表示第 `i` 个小朋友分到的糖果数)。 + + +``` +输入:candies = 7, num_people = 4 +输出:[1,2,3,1] +解释: +第一次,ans[0] += 1,数组变为 [1,0,0,0]。 +第二次,ans[1] += 2,数组变为 [1,2,0,0]。 +第三次,ans[2] += 3,数组变为 [1,2,3,0]。 +第四次,ans[3] += 1(因为此时只剩下 1 颗糖果),最终数组变为 [1,2,3,1]。 +输入:candies = 10, num_people = 3 +输出:[5,2,3] +解释: +第一次,ans[0] += 1,数组变为 [1,0,0]。 +第二次,ans[1] += 2,数组变为 [1,2,0]。 +第三次,ans[2] += 3,数组变为 [1,2,3]。 +第四次,ans[0] += 4,最终数组变为 [5,2,3]。 +``` + + +## 方法 + + +## code + +### js + +```js +let distributeCandies = (candies, num_people) => { + let ans = Array(num_people).fill(0); + let i; + for (i = 0; candies > 0; i++) { + ans[i % num_people] += i + 1; + candies -= i + 1; + } + ans[(i - 1) % num_people] += candies; + return ans; +}; +console.log(distributeCandies(7, 4)) +``` + +### go + +```go +func distributeCandies(candies int, num_people int) []int { + ans := make([]int, num_people) + i := 0 + for i = 0; candies > 0; i++ { + ans[i % num_people] += i + 1 + candies -= i + 1 + } + ans[(i - 1) % num_people] += candies + return ans +} +``` + +### java + +```java +class Solution { + public int[] distributeCandies(int candies, int num_people) { + int[] ans = new int[num_people]; + int i; + for (i = 0; candies > 0; i++) { + ans[i % num_people] += i + 1; + candies -= i + 1; + } + ans[(i - 1) % num_people] += candies; + return ans; + } +} +``` + diff --git "a/Java/alg/lc/111.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\260\217\346\267\261\345\272\246.md" "b/Java/alg/lc/111.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\260\217\346\267\261\345\272\246.md" new file mode 100644 index 00000000..79a72914 --- /dev/null +++ "b/Java/alg/lc/111.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\260\217\346\267\261\345\272\246.md" @@ -0,0 +1,132 @@ +# 111. 二叉树的最小深度 + +[url](https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/) + +## 题目 + +给定一个二叉树,找出其最小深度。 + +最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 + +说明:叶子节点是指没有子节点的节点。 + + + +``` +输入:root = [3,9,20,null,null,15,7] +输出:2 +输入:root = [2,null,3,null,4,null,5,null,6] +输出:5 +``` + +## 方法 + +可递归,当然也可迭代 + +## code + +### js + +```js +var minDepth = function(root) { + // 队列 + if (root === null) return 0; + var queue = [root]; + var depth = 1; + while (queue.length !== 0) { + var size = queue.length; + while (size-- > 0) { + var node = queue[0]; + queue = queue.slice(1, queue.length); + if (node.left === null && node.right === null) + return depth; + if (node.left !== null) + queue.push(node.left); + if (node.right !== null) + queue.push(node.right); + } + depth++; + } + return depth; +}; +``` + +### go + +```go +// 递归 +func minDepth(root *TreeNode) int { + Min := func(a int, b int) int { + if a < b { + return a + } else { + return b + } + } + if root == nil { + return 0 + } + l := minDepth(root.Left) + r := minDepth(root.Right) + if l == 0 || r == 0 { + return l + r + 1 + } + return Min(l, r) + 1 +} +// 队列 +func minDepth1(root *TreeNode) int { + if root == nil { + return 0 + } + q := []*TreeNode{root} + depth := 1 + for len(q) != 0 { + size := len(q) + for size > 0 { + size-- + node := q[0] + q = q[1:] + if node.Left == nil && node.Right == nil { + return depth + } + if node.Left != nil { + q = append(q, node.Left) + } + if node.Right != nil { + q = append(q, node.Right) + } + } + depth++ + } + return depth +} +``` + +### java + +```java +class Solution { + public int minDepth(TreeNode root) { + if (root == null) return 0; + Queue queue = new LinkedList<>(); + // 树不需要标记哦 + queue.add(root); + int depth = 1; + while (!queue.isEmpty()) { + int size = queue.size(); + while (size-- > 0) { + TreeNode node = queue.poll(); + if (node.left == null && node.right == null) + return depth; + if (node.left != null) + queue.add(node.left); + if (node.right != null) + queue.add(node.right); + } + depth++; + } + return depth; + } +} +``` + diff --git "a/Java/alg/lc/112.\350\267\257\345\276\204\346\200\273\345\222\214.md" "b/Java/alg/lc/112.\350\267\257\345\276\204\346\200\273\345\222\214.md" new file mode 100644 index 00000000..99b8eb1a --- /dev/null +++ "b/Java/alg/lc/112.\350\267\257\345\276\204\346\200\273\345\222\214.md" @@ -0,0 +1,60 @@ +# 112. 二叉树的最小深度 + +[url](https://leetcode-cn.com/problems/path-sum/) + + +## 题目 + +给你二叉树的根节点 `root` 和一个表示目标和的整数 `targetSum` ,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和 `targetSum `。 + +**叶子节点**是指没有子节点的节点。 + +![lc-pathsum1-tLI8c2](https://cdn.jsdelivr.net/gh/DreamCats/imgs@main/uPic/lc-pathsum1-tLI8c2.jpg) + +``` +输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 +输出:true +``` + +## 方法 + +可递归,当然也可迭代 + +## code + +### js + +```js +let hasPathSum = (root, sum) => { + if (root === null) return false; + if (root.left === null && root.right === null && root.val === sum) return true; + return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val); +} +``` + +### go + +```go +func hasPathSum(root *TreeNode, sum int) bool { + if root == nil { + return false + } + if root.Left == nil && root.Right == nil && root.Val == sum { + return true + } + return hasPathSum(root.Left, sum - root.Val) || hasPathSum(root.Right, sum -root.Val) +} +``` + +### java + +```java +class Solution { + public boolean hasPathSum(TreeNode root, int sum) { + if (root == null) return false; + if (root.left == null && root.right == null && root.val == sum) return true; + return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val); + } +} +``` + diff --git "a/Java/alg/lc/113.\350\267\257\345\276\204\346\200\273\345\222\2142.md" "b/Java/alg/lc/113.\350\267\257\345\276\204\346\200\273\345\222\2142.md" new file mode 100644 index 00000000..0c7c18fd --- /dev/null +++ "b/Java/alg/lc/113.\350\267\257\345\276\204\346\200\273\345\222\2142.md" @@ -0,0 +1,99 @@ +# 113. 路径总和 II + +[url](https://leetcode-cn.com/problems/path-sum-ii/) + + +## 题目 + +给你二叉树的根节点 `root` 和一个整数目标和 `targetSum` ,找出所有 **从根节点到叶子节点** 路径总和等于给定目标和的路径。 + +**叶子节点** 是指没有子节点的节点。 + +![lc-pathsum1-tLI8c2](https://assets.leetcode.com/uploads/2021/01/18/pathsumii1.jpg) + +``` +输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22 +输出:[[5,4,11,2],[5,8,4,5]] +``` + +## 方法 + +可递归,当然也可迭代 + +## code + +### js + +```js +let pathSum = (root, sum) => { + let ret = [] + let dfs = (node, sum, list) => { + if (node === null) + return; + list.push(node.val); + sum -= node.val; + if (sum === 0 && node.left === null && node.right === null) { + ret.push(list.slice()) + } else { + dfs(node.left, sum, list); + dfs(node.right, sum, list); + } + list.pop(); + } + dfs(root, sum, []); + return ret; +} +``` + +### go + +```go +func pathSum(root *TreeNode, targetSum int) [][]int { + var ret [][]int + var dfs func(node *TreeNode, sum int, list []int) + dfs = func(node *TreeNode, sum int, list []int) { + if node == nil { + return + } + list = append(list, node.Val) + sum -= node.Val + if sum == 0 && node.Left == nil && node.Right == nil { + ret = append(ret, append([]int{}, list...)) + } else { + dfs(node.Left, sum, list) + dfs(node.Right, sum, list) + } + list = list[:len(list)-1] + } + dfs(root, targetSum, []int{}) + return ret +} +``` + +### java + +```java +class Solution { + private List> ret = new ArrayList<>(); + public List> pathSum(TreeNode root, int sum) { + + dfs(root, sum, new ArrayList()); + return ret; + } + + private void dfs(TreeNode node, int sum, ArrayList list) { + if (node == null) + return; + list.add(node.val); + sum -= node.val; + if (sum == 0 && node.left == null && node.right == null) + ret.add(new ArrayList(list)); + else { + dfs(node.left, sum, list); + dfs(node.right, sum, list); + } + list.remove(list.size() - 1); + } +} +``` + diff --git "a/Java/alg/lc/118.\346\235\250\350\276\211\344\270\211\350\247\222.md" "b/Java/alg/lc/118.\346\235\250\350\276\211\344\270\211\350\247\222.md" new file mode 100644 index 00000000..6c8ac8b0 --- /dev/null +++ "b/Java/alg/lc/118.\346\235\250\350\276\211\344\270\211\350\247\222.md" @@ -0,0 +1,69 @@ +# 118. 杨辉三角 + +[url](https://leetcode-cn.com/problems/pascals-triangle/) + + +## 题目 + +给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。 + +在杨辉三角中,每个数是它左上方和右上方的数的和。 + + +![lc-PascalTriangleAnimated2-1C8mXY](https://cdn.jsdelivr.net/gh/DreamCats/imgs@main/uPic/lc-PascalTriangleAnimated2-1C8mXY.gif) + +``` +输入: 5 +输出: +[ + [1], + [1,1], + [1,2,1], + [1,3,3,1], + [1,4,6,4,1] +] +``` + +## 方法 + +杨辉三角注意观察图上的规律即可 + +## code + +### js + +```js +``` + +### go + +```go +``` + +### java + +```java +class Solution { + public List> generate(int numRows) { + List> ans = new ArrayList<>(); + for(int i = 0; i < numRows; i++) { + List curRow = new ArrayList<>(); + for(int j = 0; j <= i; j++) { + if(j == 0 || j == i) { + curRow.add(1); + continue; + } + if(i == 0 || i == 1) { + continue; + } + List preRow = ans.get(i - 1); + int value = preRow.get(j - 1) + preRow.get(j); + curRow.add(value); + } + ans.add(curRow); + } + return ans; + } +} +``` + diff --git "a/Java/alg/lc/120.\344\270\211\350\247\222\345\275\242\346\234\200\345\260\217\350\267\257\345\276\204\345\222\214.md" "b/Java/alg/lc/120.\344\270\211\350\247\222\345\275\242\346\234\200\345\260\217\350\267\257\345\276\204\345\222\214.md" new file mode 100644 index 00000000..97a1864e --- /dev/null +++ "b/Java/alg/lc/120.\344\270\211\350\247\222\345\275\242\346\234\200\345\260\217\350\267\257\345\276\204\345\222\214.md" @@ -0,0 +1,123 @@ +# 120. 三角形最小路径和 + +[url](https://leetcode-cn.com/problems/triangle/) + + +## 题目 + +给定一个三角形 `triangle` ,找出自顶向下的最小路径和。 + +每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。 + + +``` +输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] +输出:11 +解释:如下面简图所示: + 2 + 3 4 + 6 5 7 +4 1 8 3 +自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。 +``` + +## 方法 + +## code + +### js + +```js +let minimumTotal = triangle => { + if (triangle.length === 0) + return 0; + let row = triangle.length; + let dp = Array(row).fill(0).map(_ => Array(triangle[row-1].length).fill(0)); + // 初始化 + for (let i = 0; i < row; i++) { + for (let j = 0; j < triangle[i].length; j++) { + dp[i][j] = triangle[i][j]; + } + } + // 从下往上,初始化最后一行 + for (let i = 0; i < triangle[row-1].length; i++) { + dp[row-1][i] = triangle[row-1][i]; + } + // dp + for (let i = row-2; i >= 0; i--) { + for (let j = 0; j < triangle[i].length; j++) { + dp[i][j] = Math.max(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]; + } + } + return dp[0][0]; +} +``` + +### go + +```go +func minimumTotal(triangle [][]int) int { + Min := func(a, b int) int { + if a < b { + return a + } else { + return b + } + } + if len(triangle) == 0 { + return 0 + } + row := len(triangle) + dp := make([][]int, row) + for i := range dp { + dp[i] = make([]int, len(triangle[row-1])) + } + // 初始化 + for i := 0; i < row; i++ { + for j := 0; j < len(triangle[i]); j++ { + dp[i][j] = triangle[i][j] + } + } + // 从下往上,初始化最后一行 + for i := 0; i < len(triangle[row-1]); i++ { + dp[row-1][i] = triangle[row-1][i] + } + // dp + for i := row-2; i >= 0; i-- { + for j := 0; j < len(triangle[i]); j++ { + dp[i][j] = Min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j] + } + } + return dp[0][0] +} +``` + +### java + +```java +class Solution { + public int minimumTotal(List> triangle) { + if(triangle.size() == 0) return 0; + int row = triangle.size(); + int[][] dp = new int[row][triangle.get(row - 1).size()]; + // 初始化 + for(int i = 0; i < row; i++) { + for (int j =0; j < triangle.get(i).size(); j++) { + dp[i][j] = triangle.get(i).get(j); + } + } + // 从下往上, 初始化最后一行 + for (int i = 0; i < triangle.get(row - 1).size(); i++) { + dp[row - 1][i] = triangle.get(row - 1).get(i); + } + // 动态规划 + for (int i = row - 2; i >= 0; i--) { + for (int j = 0; j < triangle.get(i).size(); j++) { + dp[i][j] = Math.min(dp[i+1][j], dp[i+1][j+1]) + triangle.get(i).get(j); + } + } + return dp[0][0]; + } +} +``` + diff --git "a/Java/alg/lc/121.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" "b/Java/alg/lc/121.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" new file mode 100644 index 00000000..8ce7b374 --- /dev/null +++ "b/Java/alg/lc/121.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" @@ -0,0 +1,87 @@ +# 121. 买卖股票的最佳时机 + +[url](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/) + + +## 题目 + +给定一个数组 `prices` ,它的第` i `个元素 `prices[i] `表示一支给定股票第` i `天的价格。 + +你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。 + +返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。 + + +``` +输入:[7,1,5,3,6,4] +输出:5 +解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 + 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 + +输入:prices = [7,6,4,3,1] +输出:0 +解释:在这种情况下, 没有交易完成, 所以最大利润为 0。 +``` + +## 方法 + +动态规划 + +- 迭代过程当中,比较`min`和当前价格,取最小,目的是为了获取最大利润的嘛 +- 其次将当前价格与`min`的差和最大差值进行比较,保留最大差值,最后该差值就是最大利润 + +## code + +### js + +```js +let maxProfit = prices => { + if (prices === null || prices.length === 0) return -1; + let min = prices[0]; + let max = 0; + for (let i = 1; i < prices.length; i++) { + min = prices[i] < min ? prices[i] : min; + max = Math.max(max, prices[i] - min); + } + return max; +} + +console.log(maxProfit([7,1,5,3,6,4])); +console.log(maxProfit([7,6,4,3,1])); +``` + +### go + +```go +func maxProfit(prices []int) int { + if prices == nil || len(prices) == 0 { + return -1 + } + min, max := prices[0], 0 + for i := 1; i < len(prices); i++ { + if prices[i] < min { + min = prices[i] + } + max = Max(max, prices[i] - min) + } + return max +} +``` + +### java + +```java +class Solution { + public int maxProfit(int[] prices) { + if (prices == null || prices.length == 0) return -1; + int min = prices[0]; + int max = 0; + for (int i = 1; i < prices.length; i++) { + min = prices[i] < min ? prices[i] : min; + max = Math.max(max, prices[i] - min); + } + return max; + } +} +``` + diff --git "a/Java/alg/lc/122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\2722.md" "b/Java/alg/lc/122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\2722.md" new file mode 100644 index 00000000..4badef8b --- /dev/null +++ "b/Java/alg/lc/122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\2722.md" @@ -0,0 +1,85 @@ +# 122. 买卖股票的最佳时机2 + +[url](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/) + + +## 题目 + +给定一个数组,它的第` i `个元素是一支给定股票第` i `天的价格。 + +设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 + +注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 + + + +``` +输入: [7,1,5,3,6,4] +输出: 7 +解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 +  随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 +输入: [1,2,3,4,5] +输出: 4 +解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 +  注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。 +  因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 +输入: [7,6,4,3,1] +输出: 0 +解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 +``` + +## 方法 + +贪心 + +- 只要当天的价格比前一天的高,那就可以卖出,累加 + +## code + +### js + +```js +let maxProfit1 = prices => { + // 贪心 + let profit = 0; + for (let i = 1; i < prices.length; i++) { + if (prices[i] > prices[i - 1]) + profit += prices[i] - prices[i - 1]; + } + return profit; +} +console.log(maxProfit1([7,1,5,3,6,4])); +console.log(maxProfit1([1,2,3,4,5])); +console.log(maxProfit1([7,6,4,3,1])); +``` + +### go + +```go +func maxProfit1(prices []int) int { + // 贪心 + profit := 0 + for i := 1; i < len(prices); i++ { + if prices[i] > prices[i - 1] { + profit += prices[i] - prices[i - 1] + } + } + return profit +} +``` + +### java + +```java +class Solution { + public int maxProfit(int[] prices) { + // 贪心:只要我当前数比前一个数大, 就xxx + int profit = 0; + for (int i = 1; i < prices.length; i++) { + if (prices[i] > prices[i - 1]) profit += prices[i]- prices[i - 1]; + } + return profit; + } +} +``` + diff --git "a/Java/alg/lc/125.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" "b/Java/alg/lc/125.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" new file mode 100644 index 00000000..320e2b39 --- /dev/null +++ "b/Java/alg/lc/125.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" @@ -0,0 +1,126 @@ +# 125. 验证回文串 + +[url](https://leetcode-cn.com/problems/valid-palindrome/) + + +## 题目 + +给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。 + +说明:本题中,我们将空字符串定义为有效的回文串。 + + + +``` +输入: "A man, a plan, a canal: Panama" +输出: true +输入: "race a car" +输出: false +``` + +## 方法 + +双指针 + +- 起始一个指针l,末尾一个指针r +- 判断条件: + 1. 回文串 + 2. 只考虑字母和数字字符 + 3. 将空字符串定义为有效的回文串 + +## code + +### js + +```js +let isPalindrome = s => { + let isNormalChar = a => { + return (a >= 'a' && a <= 'z') + || (a >= 'A' && a <= 'Z') + || (a >= '0' && a <= '9') + } + if (s === "") return true; + let l = 0, r = s.length - 1; + s = s.toLowerCase() + while (l <= r) { + if (s[l] === s[r]) { + l++; + r--; + } else if (!isNormalChar(s[l])) { + l++; + } else if (!isNormalChar(s[r])) { + r--; + } else { + return false; + } + } + return true; +} +console.log(isPalindrome("A man, a plan, a canal: Panama")); +console.log(isPalindrome("race a car")); +``` + +### go + +```go +func isPalindrome1(s string) bool { + isNormalChar := func (a uint8) bool { + if a >= 48 && a <= 57 { + return true + } else if a >= 65 && a <= 97 { + return true + } else if a >= 90 && a <= 122{ + return true + } + return false + } + if s == "" { + return true + } + s = strings.ToLower(s) + l, r := 0, len(s) - 1 + for l <= r { + if s[l] == s[r] { + l++ + r-- + } else if !isNormalChar(s[l]) { + l++ + } else if !isNormalChar(s[r]) { + r-- + } else { + return false + } + } + return true +} +``` + +### java + +```java +class Solution { + public boolean isPalindrome(String s) { + if (s.equals("")) return true; + s = s.toLowerCase(); + char[] sChar = s.toCharArray(); + int l = 0, r = sChar.length - 1; + while (l <= r) { + if (sChar[l] == sChar[r]) { + l++; + r--; + } else if (!isNormalChar(sChar[l])) { + l++; + } else if (!isNormalChar(sChar[r])) { + r--; + } else { + return false; + } + } + return true; + } + private boolean isNormalChar(char a){ + return Character.isLowerCase(a) || Character.isUpperCase(a) || Character.isDigit(a); + } +} +``` + diff --git "a/Java/alg/lc/13.\347\275\227\351\251\254\346\225\260\345\255\227\350\275\254\346\225\264\346\225\260.md" "b/Java/alg/lc/13.\347\275\227\351\251\254\346\225\260\345\255\227\350\275\254\346\225\264\346\225\260.md" new file mode 100644 index 00000000..2c7684f3 --- /dev/null +++ "b/Java/alg/lc/13.\347\275\227\351\251\254\346\225\260\345\255\227\350\275\254\346\225\264\346\225\260.md" @@ -0,0 +1,140 @@ +# 13. 罗马数字转整数 + +[url](https://leetcode-cn.com/problems/roman-to-integer/) + +## 题目 + +``` +罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。 +字符 数值 +I 1 +V 5 +X 10 +L 50 +C 100 +D 500 +M 1000 + +例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。 + +通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况: + +I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。 +X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。  +C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。 +给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。 + +输入: "III" +输出: 3 + +输入: "IV" +输出: 4 + +输入: "IX" +输出: 9 + +输入: "LVIII" +输出: 58 +解释: L = 50, V= 5, III = 3. + +输入: "MCMXCIV" +输出: 1994 +解释: M = 1000, CM = 900, XC = 90, IV = 4. +``` + +## 方法 + +哈希,由于题目输入一种空间,而输出是另外一种空间,因此要用哈希映射咯 + +- 将罗马和对应的树值一一对应 +- 举个例子,比如IV,假如咱们倒着遍历 +- 第一轮:比如V对应5,我们可以用lastValue存 +- 第二轮:I对应1,如果该值 < lastValue,则res减掉该值,res -= value +- 但如果LVI,那肯定存在该值 > lastValue,则res加上该值,res += value + +## code + +### js + +```js +let romanToInt = function(s) { + let m = { + I: 1, + V: 5, + X: 10, + L: 50, + C: 100, + D: 500, + M: 1000 + }; + let ans = 0, lastValue = 0; + for (let i = s.length - 1; i >= 0; i--) { + let value = m[s[i]]; + // console.log(s[i] + " " + value); + if (value < lastValue) { + ans -= value; + } else { + ans += value; + } + lastValue = value; + } + return ans; +} +``` + +### go + +```go +func romanToInt(s string) int { + // map 切片 + m := make(map[string]int) + m["I"] = 1 + m["V"] = 5 + m["X"] = 10 + m["L"] = 50 + m["C"] = 100 + m["D"] = 500 + m["M"] = 1000 + ans, lastValue:= 0, 0 + // 倒着遍历比较 + for i := len(s) - 1; i >= 0; i-- { + value := m[string(s[i])] + if value < lastValue { + ans -= value + } else { + ans += value + } + lastValue = value + } + return ans +} +``` + +### java + +```java +class Solution { + public int romanToInt(String s) { + HashMap map = new HashMap<>(); + map.put('I', 1); + map.put('V', 5); + map.put('X', 10); + map.put('L', 50); + map.put('C', 100); + map.put('D', 500); + map.put('M', 1000); + int ans = 0, lastValue = 0; + for (int i = s.length() - 1; i >= 0; i--) { + int value = map.get(s.charAt(i)); + if (value < lastValue) { + ans -= value; + } else { + ans += value; + } + lastValue = value; + } + return ans; + } +} +``` + diff --git "a/Java/alg/lc/136.\345\217\252\345\207\272\347\216\260\344\270\200\346\254\241\347\232\204\346\225\260\345\255\227.md" "b/Java/alg/lc/136.\345\217\252\345\207\272\347\216\260\344\270\200\346\254\241\347\232\204\346\225\260\345\255\227.md" new file mode 100644 index 00000000..e3323782 --- /dev/null +++ "b/Java/alg/lc/136.\345\217\252\345\207\272\347\216\260\344\270\200\346\254\241\347\232\204\346\225\260\345\255\227.md" @@ -0,0 +1,84 @@ +# 136. 只出现一次的数字 + +[url](https://leetcode-cn.com/problems/single-number/) + + +## 题目 + +给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 + +说明: + +你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗? + +``` +输入: [2,2,1] +输出: 1 +输入: [4,1,2,1,2] +输出: 4 +``` + +## 方法 + + +两种方法: +1. 哈希->最容易想到的方法 +2. 异或 + +## code + +### js + +```js +let singleNumber = nums => { + let ret = 0; + for (let i = 0; i < nums.length; i++) { + ret = ret ^ nums[i]; + } + return ret; +} +console.log(singleNumber([2, 2, 1])); +console.log(singleNumber([4,1,2,1,2])); +``` + +### go + +```go +func singleNumber(nums []int) int { + ret := 0 + for _, num := range nums { + ret = ret ^ num + } + return ret +} +func singleNumber1(nums []int) int { + // 哈希? + m := make(map[int]int, len(nums)) + for _, num := range nums { + if value, ok := m[num]; ok { + m[num] = value + 1 + } else { + m[num] = 1 + } + } + for k, v := range m { + if v == 1 { + return k + } + } + return -1 +} +``` + +### java + +```java +class Solution { + public int singleNumber(int[] nums) { + int ret = 0; + for (int num : nums) ret = ret ^ num; + return ret; + } +} +``` + diff --git "a/Java/alg/lc/14.\346\234\200\351\225\277\345\205\254\345\205\261\345\211\215\347\274\200.md" "b/Java/alg/lc/14.\346\234\200\351\225\277\345\205\254\345\205\261\345\211\215\347\274\200.md" new file mode 100644 index 00000000..c8c6add2 --- /dev/null +++ "b/Java/alg/lc/14.\346\234\200\351\225\277\345\205\254\345\205\261\345\211\215\347\274\200.md" @@ -0,0 +1,83 @@ +# 14. 最长公共前缀 + +[url](https://leetcode-cn.com/problems/longest-common-prefix/) + +## 题目 + +编写一个函数来查找字符串数组中的最长公共前缀。 + +如果不存在公共前缀,返回空字符串 ""。 + +``` +输入:strs = ["flower","flow","flight"] +输出:"fl" + +输入:strs = ["dog","racecar","car"] +输出:"" +解释:输入不存在公共前缀。 +``` + +## 方法 + +看到这道题,找到几个字符串公共前缀,比如 +`strs = ["flower","flow","flight"]` + +- 我们肯定一眼就能找到fl是公共前缀 +- 假设我们取第一个字符串,flower,分别和flow与flight去对比 +- 对比啥呢,对比其他字符串是否包含flower,如果包含,则是公共前缀,如果不是呢?那就截取掉最后一个字符,即flowe,再分别和flow与flight去对比 +- 以此类推,打截取到flow的时候,flow字符串正好与该字符串相等,但和flight不匹配,那么继续截取,然后从flight所在的索引循环 +- 直到fl符合所有要求 + +## code + +### js + +```js +let longestCommonPrefix = function(strs) { + if (strs.length === 0) + return ""; + let str = strs[0] + for (let i = 1; i < strs.length; i++) { + while(strs[i].indexOf(str) !== 0) { + str = str.substring(0, str.length - 1); + } + } + return str; +}; +``` + +### go + +```go +func longestCommonPrefix(strs []string) string { + if strs == nil || len(strs) == 0 { + return "" + } + str := strs[0] + for _, v := range strs[1:] { + for strings.Index(v, str) != 0 { + str = str[0:len(str) - 1] + } + } + return str +} +``` + +### java + +```java +class Solution { + public String longestCommonPrefix(String[] strs) { + if (strs == null || strs.length == 0) + return ""; + String str = strs[0]; + for (int i = 1; i < strs.length; i++){ + while (strs[i].indexOf(str) != 0) { + str = str.substring(0, str.length() - 1); + } + } + return str; + } +} +``` + diff --git "a/Java/alg/lc/141.\347\216\257\345\275\242\351\223\276\350\241\250.md" "b/Java/alg/lc/141.\347\216\257\345\275\242\351\223\276\350\241\250.md" new file mode 100644 index 00000000..67a7a808 --- /dev/null +++ "b/Java/alg/lc/141.\347\216\257\345\275\242\351\223\276\350\241\250.md" @@ -0,0 +1,92 @@ +# 141. 环形链表 + +[url](https://leetcode-cn.com/problems/linked-list-cycle/) + + +## 题目 + +给定一个链表,判断链表中是否有环。 + +如果链表中有某个节点,可以通过连续跟踪 `next` 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 `pos` 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 `pos` 是 -1,则在该链表中没有环。注意:`pos` 不作为参数进行传递,仅仅是为了标识链表的实际情况。 + +如果链表中存在环,则返回 `true` 。 否则,返回 `false` 。 + +你能用 O(1)(即,常量)内存解决此问题吗? + +![circularlinkedlist-lc-q9kmLK](https://cdn.jsdelivr.net/gh/DreamCats/imgs@main/uPic/circularlinkedlist-lc-q9kmLK.png) +``` +输入:head = [3,2,0,-4], pos = 1 +输出:true +解释:链表中有一个环,其尾部连接到第二个节点。 +``` + +![circularlinkedlist_test2-lc-i5Xwi1](https://cdn.jsdelivr.net/gh/DreamCats/imgs@main/uPic/circularlinkedlist_test2-lc-i5Xwi1.png) +``` +输入:head = [1,2], pos = 0 +输出:true +解释:链表中有一个环,其尾部连接到第一个节点。 +``` + +## 方法 + + +挺简单的:快慢指针 + +## code + +### js + +```js +let hasCycle = head => { + if (head === null) return false; + let l1 = head, l2 = head.next; + while (l2 !== null && l2.next !== null) { + if (l1 === l2) + return true; + l1 = l1.next; + l2 = l2.next.next; + } + return false; +} +``` + +### go + +```go +func hasCycle(head *ListNode) bool { + if head == nil { + return false + } + l1, l2 := head, head.Next + for l2 != nil && l2.Next != nil { + if l1 == l2 { + return true + } + l1 = l1.Next + l2 = l2.Next.Next + } + return false; +} +``` + +### java + +```java +public class Solution { + public boolean hasCycle(ListNode head) { + if (head == null) { + return false; + } + ListNode l1 = head, l2 = head.next; + while (l1 != null && l2 != null && l2.next != null) { + if (l1 == l2) { + return true; + } + l1 = l1.next; + l2 = l2.next.next; + } + return false; + } +} +``` + diff --git "a/Java/alg/lc/15.\344\270\211\346\225\260\344\271\213\345\222\214.md" "b/Java/alg/lc/15.\344\270\211\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 00000000..c3f65409 --- /dev/null +++ "b/Java/alg/lc/15.\344\270\211\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,136 @@ +# 15. 三数之和 + + +[url](https://leetcode-cn.com/problems/3sum/) + +## 题目 + +给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。 + +注意:答案中不可以包含重复的三元组。 + + +``` +输入:nums = [-1,0,1,2,-1,-4] +输出:[[-1,-1,2],[-1,0,1]] +输入:nums = [] +输出:[] +输入:nums = [0] +输出:[] +``` + +## 方法 + + +## code + +### js + +```js +let treeSum = nums => { + let ret = []; + if (nums === null || nums.length === 0) + return ret; + nums.sort(); + for (let i = 0; i < nums.length - 2; i++) { + if (nums[i] > 0) + break; + if (i !== 0 && nums[i] === nums[i - 1]) + continue; + let l = i + 1, r = nums.length - 1; + while (l < r) { + let sum = nums[l] + nums[r]; + if (sum < -nums[i]) + l++; + else if (sum > -nums[i]) + r--; + else { + ret.push([nums[i], nums[l], nums[r]]); + // 防止重复 + while (l < r && nums[l] === nums[l + 1]) l++; + while (l < r && nums[r] === nums[r - 1]) r--; + l++; + r--; + } + } + } + return ret; +}; +``` + +### go + +```go +func threeSum(nums []int) [][]int { + res := make([][]int, 0) + if nums == nil || len(nums) == 0 { + return res + } + sort.Ints(nums) + for i := 0; i < len(nums)-2; i++ { + if nums[i] > 0 { + break + } + if i != 0 && nums[i] == nums[i-1] { + continue + } + l, r := i + 1, len(nums) - 1 + for l < r { + sum := nums[l] + nums[r] + if sum < -nums[i] { + l++ + } else if sum > -nums[i] { + r-- + } else { + res = append(res, []int{nums[i], nums[l], nums[r]}) + for l < r && nums[l] == nums[l+1] { + l++ + } + for l < r && nums[r] == nums[r-1] { + r-- + } + l++ + r-- + } + } + } + return res +} +``` + +### java + +```java +class Solution { + public List> threeSum(int[] nums) { + List> ret = new ArrayList<>(); + if (nums == null || nums.length == 0) + return ret; + Arrays.sort(nums); + for (int i = 0; i < nums.length - 2; i++){ + if (nums[i] > 0) + break; + if (i != 0 && nums[i] == nums[i - 1]) + continue; + int l = i + 1, r = nums.length - 1; + while (l < r){ + int sum = nums[l] + nums[r]; + if (sum < -nums[i]) + l++; + else if (sum > -nums[i]) + r--; + else { + ret.add(Arrays.asList(nums[i], nums[l], nums[r])); + // 防止重复 + while (l < r && nums[l] == nums[l + 1]) l++; + while (l < r && nums[r] == nums[r - 1]) r--; + l++; + r--; + } + } + } + return ret; + } +} +``` + diff --git "a/Java/alg/lc/155.\346\234\200\345\260\217\346\240\210.md" "b/Java/alg/lc/155.\346\234\200\345\260\217\346\240\210.md" new file mode 100644 index 00000000..34027d2f --- /dev/null +++ "b/Java/alg/lc/155.\346\234\200\345\260\217\346\240\210.md" @@ -0,0 +1,145 @@ +# 155. 最小栈 + +[url](https://leetcode-cn.com/problems/min-stack/) + + +## 题目 + +设计一个支持 `push` `,pop` `,top` 操作,并能在常数时间内检索到最小元素的栈。 + +`push(x)` —— 将元素 x 推入栈中。 +`pop()` —— 删除栈顶的元素。 +`top()` —— 获取栈顶元素。 +`getMin()` —— 检索栈中的最小元素。 + + +``` +输入: +["MinStack","push","push","push","getMin","pop","top","getMin"] +[[],[-2],[0],[-3],[],[],[],[]] +输出: +[null,null,null,null,-3,null,0,-2] +解释: +MinStack minStack = new MinStack(); +minStack.push(-2); +minStack.push(0); +minStack.push(-3); +minStack.getMin(); --> 返回 -3. +minStack.pop(); +minStack.top(); --> 返回 0. +minStack.getMin(); --> 返回 -2. +``` + +## 方法 + + +双栈即可 +- 数据栈与最小栈 + +## code + +### js + +```js +let MinStack = function() { + this.dataStack = []; + this.minStack = []; + this.min = Infinity; +} +MinStack.prototype.push = function(x) { + this.dataStack.push(x); + this.min = Math.min(this.min, x); + this.minStack.push(this.min); +} +MinStack.prototype.pop = function() { + this.dataStack.pop(); + this.minStack.pop(); + this.min = this.minStack.length === 0 ? Infinity : this.minStack[this.minStack.length - 1]; +} +MinStack.prototype.top = function() { + return this.dataStack[this.dataStack.length - 1]; +} +MinStack.prototype.getMin = function() { + return this.min; +} +var test = new MinStack(); +test.push(2); +test.push(1); +test.push(3); +// test.pop(); +console.log(test.getMin()); +console.log(test.top()); +``` + +### go + +```go +const ( + INT_MAX = int(^uint((0)) >> 1) + INT_MIN = ^INT_MAX +) +var min = INT_MAX +var s1 = []int{} +var s2 = []int{} +func push(x int) { + s1 = append(s1, x) + min = Min(min, x) + s2 = append(s2, min) +} +func pop() { + s1 = s1[:len(s1) - 1] + s2 = s2[:len(s2) - 1] + if len(s2) == 0 { + min = INT_MAX + } else { + min = s2[len(s2) - 1] + } +} +func top() int { + return s1[len(s1) - 1] +} +func getMin() int { + return min +} +``` + +### java + +```java +class MinStack { + private Stack dataStack; + private Stack minStack; + private int min; + /** initialize your data structure here. */ + public MinStack() { + dataStack = new Stack<>(); + minStack = new Stack<>(); + min = Integer.MAX_VALUE; + } + public void push(int x) { + dataStack.push(x); + min = Math.min(min, x); + minStack.push(min); + } + public void pop() { + dataStack.pop(); + minStack.pop(); + min = minStack.isEmpty() ? Integer.MAX_VALUE : minStack.peek(); + } + public int top() { + return dataStack.peek(); + } + public int getMin() { + return min; + } +} +/** + * Your MinStack object will be instantiated and called as such: + * MinStack obj = new MinStack(); + * obj.push(x); + * obj.pop(); + * int param_3 = obj.top(); + * int param_4 = obj.getMin(); + */ +``` + diff --git "a/Java/alg/lc/160.\347\233\270\344\272\244\351\223\276\350\241\250.md" "b/Java/alg/lc/160.\347\233\270\344\272\244\351\223\276\350\241\250.md" new file mode 100644 index 00000000..ac9379ce --- /dev/null +++ "b/Java/alg/lc/160.\347\233\270\344\272\244\351\223\276\350\241\250.md" @@ -0,0 +1,102 @@ +# 160. 相交链表 + +[url](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/) + + +## 题目 + +编写一个程序,找到两个单链表相交的起始节点。 + +如下面的两个链表: +![160_statement-lc-j1QVOw](https://cdn.jsdelivr.net/gh/DreamCats/imgs@main/uPic/160_statement-lc-j1QVOw.png) + +在节点 c1 开始相交。 + +![160_example_1-lc-bAOm3q](https://cdn.jsdelivr.net/gh/DreamCats/imgs@main/uPic/160_example_1-lc-bAOm3q.png) + +``` +输入: +输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3 +输出:Reference of the node with value = 8 +输入解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 +``` +![160_example_2-lc-iqzSva](https://cdn.jsdelivr.net/gh/DreamCats/imgs@main/uPic/160_example_2-lc-iqzSva.png) + +``` +输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 +输出:Reference of the node with value = 2 +输入解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。 +``` + +![160_example_3-lc-RNgABI](https://cdn.jsdelivr.net/gh/DreamCats/imgs@main/uPic/160_example_3-lc-RNgABI.png) + +``` +输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 +输出:null +输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。 +解释:这两个链表不相交,因此返回 null。 + +注意: +- 如果两个链表没有交点,返回 `null`. +- 在返回结果后,两个链表仍须保持原有的结构。 +- 可假定整个链表结构中没有循环。 +- 程序尽量满足 `O(n) `时间复杂度,且仅用 `O(1) `内存。 + +``` + +## 方法 + +双指针 +- 分别走A、B两圈,如果相等,则退出循环 + +## code + +### js + +```js +let getIntersectionNode = (headA, headB) => { + let l1 = headA, l2 = headB; + while (l1 !== l2) { + l1 = l1 === null ? headB : l1.next; + l2 = l2 === null ? headA: l2.next; + } + return l1; +} +``` + +### go + +```go +func getIntersectionNode(headA, headB *ListNode) *ListNode { + l1, l2 := headA, headB + for l1 != l2 { + if l1 == nil { + l1 = headB + } else { + l1 = l1.Next + } + if l2 == nil { + l2 = headA + } else { + l2 = l2.Next + } + } + return l1 +} +``` + +### java + +```java +public class Solution { + public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + ListNode l1 = headA, l2 = headB; + while (l1 != l2) { + l1 = l1 == null ? headB : l1.next; + l2 = l2 == null ? headA : l2.next; + } + return l1; + } +} +``` + diff --git "a/Java/alg/lc/167.\344\270\244\346\225\260\344\271\213\345\222\2142-\350\276\223\345\205\245\346\234\211\345\272\217\346\225\260\347\273\204.md" "b/Java/alg/lc/167.\344\270\244\346\225\260\344\271\213\345\222\2142-\350\276\223\345\205\245\346\234\211\345\272\217\346\225\260\347\273\204.md" new file mode 100644 index 00000000..7a598b9a --- /dev/null +++ "b/Java/alg/lc/167.\344\270\244\346\225\260\344\271\213\345\222\2142-\350\276\223\345\205\245\346\234\211\345\272\217\346\225\260\347\273\204.md" @@ -0,0 +1,92 @@ +# 167. 两数之和 II - 输入有序数组 + +[url](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/) + + +## 题目 + +给定一个已按照 升序排列  的整数数组 `numbers` ,请你从数组中找出两个数满足相加之和等于目标数 `target` 。 + +函数应该以长度为 `2` 的整数数组的形式返回这两个数的下标值。`numbers `的下标 从 `1` 开始计数 ,所以答案数组应当满足 `1 <= answer[0] < answer[1] <= numbers.length` 。 + +``` +输入:numbers = [2,7,11,15], target = 9 +输出:[1,2] +解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。 +输入:numbers = [2,3,4], target = 6 +输出:[1,3] +输入:numbers = [-1,0], target = -1 +输出:[1,2] +``` + +## 方法 + +题目说已排序,因此双指针即可解决 +- 一个在起点位置,一个在末尾位置。 + +## code + +### js + +```js +let twoSum = (numbers, target) => { + if (numbers === null) return null; + // 双指针 + let p1 = 0, p2 = numbers.length - 1; + while (p1 < p2) { + let sum = numbers[p1] + numbers[p2]; + if (sum === target) + return [p1 + 1, p2 + 1]; + else if (sum < target) + p1++; + else + p2--; + } + return []; +} +// console.log(twoSum([2, 7, 11, 15], 9)); +console.log("Hello World!"); +``` + +### go + +```go +func twoSum1(numbers []int, target int) []int { + if numbers == nil || len(numbers) == 0 { + return nil + } + p1, p2 := 0, len(numbers) - 1 + for p1 < p2 { + sum := numbers[p1] + numbers[p2] + if sum == target { + return []int{p1 + 1, p2 + 1} + } else if sum < target { + p1++ + } else { + p2-- + } + } + return nil +} +``` + +### java + +```java +class Solution { + public int[] twoSum(int[] numbers, int target) { + if (numbers == null) return null; + // 双指针 + int p1 = 0, p2 = numbers.length - 1; + while (p1 < p2) { + int sum = numbers[p1] + numbers[p2]; + if (sum == target) return new int[]{p1+1, p2+1}; + else if (sum < target) p1++; + else p2--; + } + + return null; + } +} +``` + diff --git "a/Java/alg/lc/17.\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\345\255\227\346\257\215\347\273\204\345\220\210.md" "b/Java/alg/lc/17.\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\345\255\227\346\257\215\347\273\204\345\220\210.md" new file mode 100644 index 00000000..ca512dc5 --- /dev/null +++ "b/Java/alg/lc/17.\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\345\255\227\346\257\215\347\273\204\345\220\210.md" @@ -0,0 +1,111 @@ +# 17. 电话号码的字母组合 + + +[url](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/) + +## 题目 + +给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 + +给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 + +![](https://assets.leetcode-cn.com/aliyun-lc-upload/original_images/17_telephone_keypad.png) + + +``` +输入:digits = "23" +输出:["ad","ae","af","bd","be","bf","cd","ce","cf"] +输入:digits = "" +输出:[] +输入:digits = "2" +输出:["a","b","c"] +``` + +## 方法 + + +## code + +### js + +```js +let letterCombinations = digits => { + let dfs = (digits, sb) => { + if (sb.length === digits.length) { + ret.push(sb); + return; + } + let c = digits[sb.length] - '0'; + let cur = KEYS[c]; + for (const a of cur) { + sb += a; + dfs(digits, sb); + sb = sb.substring(0, sb.length - 1); + } + }; + let KEYS = ["", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]; + let ret = []; + if (digits === null || digits.length === 0) + return ret; + dfs(digits, ""); + return ret; +}; +``` + +### go + +```go +func letterCombinations(digits string) []string { + var KEYS = []string{"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"} + var res []string + var dfs func(digits string, sb string) + if len(digits) == 0 { + return res + } + dfs = func (digits string, sb string) { + if len(sb) == len(digits) { + res = append(res, sb) + return + } + c := digits[len(sb)] - byte('0') + cur := KEYS[c] + for _, v := range cur { + sb += string(v) + dfs(digits, sb) + sb = sb[0:len(sb) - 1] + } + } + dfs(digits, "") + return res +} +``` + +### java + +```java +class Solution { + private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; + List ret = new ArrayList<>(); + public List letterCombinations(String digits) { + if(digits == null || digits.length() == 0) return ret; + dfs(digits, new StringBuilder()); + return ret; + } + + private void dfs(String digits, StringBuilder sb) { + if (sb.length() == digits.length()){ + ret.add(sb.toString()); + return; + } + int c = digits.charAt(sb.length()) - '0'; + String cur = KEYS[c]; + for (char a : cur.toCharArray()){ + sb.append(a); + dfs(digits, sb); + sb.deleteCharAt(sb.length() - 1); + } + + } +} +``` + diff --git "a/Java/alg/lc/172.\351\230\266\344\271\230\345\220\216\347\232\204\351\233\266.md" "b/Java/alg/lc/172.\351\230\266\344\271\230\345\220\216\347\232\204\351\233\266.md" new file mode 100644 index 00000000..429a5d96 --- /dev/null +++ "b/Java/alg/lc/172.\351\230\266\344\271\230\345\220\216\347\232\204\351\233\266.md" @@ -0,0 +1,56 @@ +# 172. 阶乘后的零 + +[url](https://leetcode-cn.com/problems/factorial-trailing-zeroes/) + + +## 题目 + +给定一个整数 n,返回 n! 结果尾数中零的数量。 + +``` +输入: 3 +输出: 0 +解释: 3! = 6, 尾数中没有零。 +输入: 5 +输出: 1 +解释: 5! = 120, 尾数中有 1 个零. +``` + +## 方法 + +算一下该数与5的相除的个数 + +## code + +### js + +```js +let trailingZeros = n => { + return n === 0 ? 0 : Math.floor(n / 5) + trailingZeros(Math.floor(n / 5)); +}; +console.log(railingZeros(3)); +console.log(trailingZeros(5)); +``` + +### go + +```go +func trailingZeroes(n int) int { + if n == 0 { + return 0 + } else { + return n / 5 + trailingZeroes(n / 5) + } +} +``` + +### java + +```java +class Solution { + public int trailingZeroes(int n) { + return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5); + } +} +``` + diff --git "a/Java/alg/lc/19.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\347\273\223\347\202\271.md" "b/Java/alg/lc/19.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\347\273\223\347\202\271.md" new file mode 100644 index 00000000..549897f5 --- /dev/null +++ "b/Java/alg/lc/19.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\347\273\223\347\202\271.md" @@ -0,0 +1,90 @@ +# 19. 删除链表的倒数第 N 个结点 + + +[url](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) + +## 题目 + +给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 + +进阶:你能尝试使用一趟扫描实现吗? + +![](https://assets.leetcode.com/uploads/2020/10/03/remove_ex1.jpg) + +``` +输入:head = [1,2,3,4,5], n = 2 +输出:[1,2,3,5] +输入:head = [1], n = 1 +输出:[] +输入:head = [1,2], n = 1 +输出:[1] +``` + +## 方法 + + +## code + +### js + +```js +let removeNthFromEnd = (head, n) => { + let fast = head; + while (n-- > 0) { + fast = fast.next; + } + if (fast === null) + return head.next; + let slow = head; + while (fast.next !== null) { + fast = fast.next; + slow = slow.next; + } + slow.next = slow.next.next; + return head; +}; +``` + +### go + +```go +func removeNthFromEnd(head *ListNode, n int) *ListNode { + fast := head + for n > 0 { + n-- + fast = fast.Next + } + if fast == nil { + return head.Next + } + slow := head + for fast.Next != nil { + fast = fast.Next + slow = slow.Next + } + slow.Next = slow.Next.Next + return head +} +``` + +### java + +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode fast = head; + while (n-- > 0) { + fast = fast.next; + } + if (fast == null) return head.next; + ListNode slow = head; + while (fast.next != null) { + fast = fast.next; + slow = slow.next; + } + slow.next = slow.next.next; + return head; + } +} +``` + diff --git "a/Java/alg/lc/191.\344\275\2151\347\232\204\344\270\252\346\225\260.md" "b/Java/alg/lc/191.\344\275\2151\347\232\204\344\270\252\346\225\260.md" new file mode 100644 index 00000000..17f4e1a7 --- /dev/null +++ "b/Java/alg/lc/191.\344\275\2151\347\232\204\344\270\252\346\225\260.md" @@ -0,0 +1,94 @@ +# 191. 位1的个数 + +[url](https://leetcode-cn.com/problems/number-of-1-bits/) + + +## 题目 + +编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。 + +提示: + +- 请注意,在某些语言(如 `Java`)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。 +- 在 `Java` 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 `3` 中,输入表示有符号整数 `-3`。 + + +``` +输入:00000000000000000000000000001011 +输出:3 +解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。 +输入:00000000000000000000000010000000 +输出:1 +解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。 +输入:11111111111111111111111111111101 +输出:31 +解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。 +``` +提示: + +- 输入必须是长度为 `32` 的 二进制串 。 + +## 方法 + +常用:`n &= (n - 1)` 去掉二进制中的一个1 + +``` +比如:10 & (10 - 1) +1010 - 0001 = 1001 +1010 & 1001 = 1000 +1000 - 0001 = 0111 +1000 & 0111 = 0000 +``` + +## code + +### js + +```js +let hammingWeight = n => { + // toString(2)把十进制数字转换为二进制字符串,replace删除0,返回剩余长度 + return n.toString(2).replace(/0/g, '').length; +}; +let hammingWeight1 = n => { + let sum = 0; + while (n !== 0) { + sum++; + n &= (n - 1); + } + return sum; +} + +console.log(hammingWeight(00000000000000000000000000001011)); +console.log(hammingWeight(00000000000000000000000010000000)); +console.log(hammingWeight(11111111111111111111111111111101)); +``` + +### go + +```go +func hammingWeight(n int) int { + ans := 0 + for n != 0 { + n &= n - 1 + ans++ + } + return ans +} +``` + +### java + +```java +public class Solution { + // you need to treat n as an unsigned value + public int hammingWeight(int n) { + int ans = 0; + while(n != 0) { + n &= n - 1; + ans++; + } + return ans; + } +} +``` + diff --git "a/Java/alg/lc/2.\344\270\244\346\225\260\347\233\270\345\212\240.md" "b/Java/alg/lc/2.\344\270\244\346\225\260\347\233\270\345\212\240.md" new file mode 100644 index 00000000..b0c9dd43 --- /dev/null +++ "b/Java/alg/lc/2.\344\270\244\346\225\260\347\233\270\345\212\240.md" @@ -0,0 +1,118 @@ +# 2.两数相加 + +[url](https://leetcode-cn.com/problems/add-two-numbers/) + +## 题目 + +给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。 + +请你将两个数相加,并以相同形式返回一个表示和的链表。 + +你可以假设除了数字 0 之外,这两个数都不会以 0 开头。 + +![lc-addtwonumber1-snb8Qb](http://imgs.dreamcat.ink/uPic/lc-addtwonumber1-snb8Qb.jpeg) + +``` +输入:l1 = [2,4,3], l2 = [5,6,4] +输出:[7,0,8] +解释:342 + 465 = 807. +输入:l1 = [0], l2 = [0] +输出:[0] +输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] +输出:[8,9,9,9,0,0,0,1] +``` + +## 方法 + + +## code + +### js + +```js +let addTwoNumbers = (l1, l2) => { + if (l1 === null) + return l2; + if (l2 === null) + return l1; + let p1 = l1, p2 = l2; + let l3 = new ListNode(0); + let p3 = l3, carry = 0; + while (p1 !== null || p2 !== null) { + let a = p1 === null ? 0 : p1.val; + let b = p2 === null ? 0 : p2.val; + p3.next = new ListNode((a + b + carry) % 10); + carry = Math.floor((a + b + carry) / 10); + p1 = p1 === null ? null : p1.next; + p2 = p2 === null ? null : p2.next; + p3 = p3.next; + } + p3.next = carry === 1 ? new ListNode(1) : null; + return l3.next; +}; +``` + +### go + +```go +func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode { + if l1 == nil { + return l2 + } + if l2 == nil { + return l1 + } + p1, p2 := l1, l2 + l3 := &ListNode{Val: 0} + p3 := l3 + carry := 0 + for p1 != nil || p2 != nil { + a := 0 + b := 0 + if p1 != nil { + a = p1.Val + p1 = p1.Next + } + if p2 != nil { + b = p2.Val + p2 = p2.Next + } + p3.Next = &ListNode{Val: (a + b +carry) % 10} + carry = (a + b + carry) / 10 + p3 = p3.Next + } + if carry == 1 { + p3.Next = &ListNode{Val: 1} + } + return l3.Next +} +``` + +### java + +```java +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + if (l1 == null) + return l2; + if (l2 == null) + return l1; + ListNode p1 = l1, p2 = l2; + ListNode l3 = new ListNode(0); + ListNode p3 = l3; + int carry = 0; + while (p1 != null || p2 != null) { + int a = p1 == null ? 0 : p1.val; + int b = p2 == null ? 0 : p2.val; + p3.next = new ListNode((a + b + carry) % 10); + carry = (a + b + carry) / 10; + p1 = p1 == null ? null : p1.next; + p2 = p2 == null ? null : p2.next; + p3 = p3.next; + } + p3.next = carry == 1 ? new ListNode(1) : null; + return l3.next; + } +} +``` + diff --git "a/Java/alg/lc/20.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" "b/Java/alg/lc/20.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" new file mode 100644 index 00000000..1e052989 --- /dev/null +++ "b/Java/alg/lc/20.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" @@ -0,0 +1,113 @@ +# 20. 有效的括号 + +[url](https://leetcode-cn.com/problems/valid-parentheses/) + +## 题目 + +给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。 + +有效字符串需满足: + +左括号必须用相同类型的右括号闭合。 +左括号必须以正确的顺序闭合。 +注意空字符串可被认为是有效字符串。 + +``` +输入: "()" +输出: true + +输入: "()[]{}" +输出: true + +输入: "(]" +输出: false + +输入: "([)]" +输出: false + +输入: "{[]}" +输出: true +``` + +## 方法 + +栈的思想 + +- 遍历字符串 +- 当前栈的空间长度为0,则直接将该字符串c添加到空间中 +- 如果栈空间长度不为0,就意味着有其他字符串,将c与上一个入栈的字符进行isSym进行匹配,如果满足则,将上一个入栈的字符串移除 +- 如果不满足以上两个条件,则将c入栈 + +## code + +### js + +```js +let isValid = s => { + let isSym = (c1, c2) => { + return (c1 === '(' && c2 === ')') + || (c1 === '{' && c2 === '}') + || (c1 === '[' && c2 === ']') + } + + let stack = [] + + for (let i = 0; i < s.length; i++) { + let c = s[i]; + if (stack.length === 0) { + stack.push(c); + } else if (isSym(stack[stack.length - 1], c)) { + stack.pop(); + } else { + stack.push(c); + } + } + return stack.length === 0; +}; +``` + +### go + +```go +func isValid(s string, i int, j int) bool { + // 栈 + var stack []string + isSym := func (c1, c2 string) bool { + return (c1 == "(" && c2 == ")") || (c1 == "{" && c2 == "}") || (c1 == "[" && c2 == "]") + } + for _, c := range s { + if stack == nil || len(stack) == 0 || !isSym(stack[len(stack)-1], string(c)) { + stack = append(stack, string(c)) + } else { + stack = stack[:len(stack) - 1] + } + } + return len(stack) == 0 +} +``` + +### java + +```java +class Solution { + public boolean isValid(String s) { + Stack stack = new Stack<>(); + for (char c : s.toCharArray()){ + if (stack.isEmpty()) + stack.push(c); + else if(isSym(stack.peek(), c)) + stack.pop(); + else + stack.push(c); + } + return stack.isEmpty(); + } + + private boolean isSym(char c1, char c2) { + return (c1 == '(' && c2 == ')') + || (c1 == '{' && c2 == '}') + || (c1 == '[' && c2 == ']'); + } +} +``` + diff --git "a/Java/alg/lc/204.\350\256\241\346\225\260\350\264\250\346\225\260.md" "b/Java/alg/lc/204.\350\256\241\346\225\260\350\264\250\346\225\260.md" new file mode 100644 index 00000000..8ae58904 --- /dev/null +++ "b/Java/alg/lc/204.\350\256\241\346\225\260\350\264\250\346\225\260.md" @@ -0,0 +1,95 @@ +# 204. 计数质数 + +[url](https://leetcode-cn.com/problems/count-primes/) + + +## 题目 + +统计所有小于非负整数 n 的质数的数量。 + + +``` +输入:n = 10 +输出:4 +解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。 +输入:n = 0 +输出:0 +输入:n = 1 +输出:0 +``` +提示: + + +## 方法 + +质数的概念:质数又称**素数**。 一个大于1的**自然数**,除了**1和它自身外**,**不能被其他自然数整除的数叫做质数**;否则称为合数(**规定1既不是质数也不是合数**)。 + +所以,嵌套for的条件`let j = 2 * i; j < n + 1; j += i` + +## code + +### js + +```js +let coutnPrimes = n => { + if (n < 2) return 0; + let nums = Array(n + 1).fill(false); + let count = 0; + for (let i = 2; i < n; i++) { + if (nums[i] === false) { + count++; + } + for (let j = 2 * i; j < n + 1; j += i) { + nums[j] = true; + } + } + return count; +}; + +console.log(coutnPrimes(10)); +console.log(coutnPrimes(0)); +console.log(coutnPrimes(1)); +``` + +### go + +```go +func countPrimes(n int) int { + if n < 2 { + return 0 + } + nums := make([]bool, n + 1) + count := 0 + for i := 2; i < n; i++ { + if nums[i] == false { + count++ + } + for j := 2 * i; j < n + 1; j += i { + nums[j] = true + } + } + return count +} +``` + +### java + +```java +class Solution { + public int countPrimes(int n) { + if (n < 2) return 0; + boolean[] num = new boolean[n + 1]; + int count = 0; + for (int i = 2; i < n; i++) { + if (num[i] == false) { + count++; + } + for (int j = 2 * i; j < n + 1; j += i) { + num[j] = true; + } + } + return count; + } +} +``` + diff --git "a/Java/alg/lc/205.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" "b/Java/alg/lc/205.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" new file mode 100644 index 00000000..62e6a3ad --- /dev/null +++ "b/Java/alg/lc/205.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,92 @@ +# 205. 同构字符串 + +[url](https://leetcode-cn.com/problems/isomorphic-strings/) + + +## 题目 + +给定两个字符串 `s` 和 `t`,判断它们是否是同构的。 + +如果 `s` 中的字符可以按某种映射关系替换得到 `t` ,那么这两个字符串是同构的。 + +每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。 + + +``` +输入:s = "egg", t = "add" +输出:true +输入:s = "foo", t = "bar" +输出:false +输入:s = "paper", t = "title" +输出:true +``` +提示: + + +## 方法 + +- 你可以想象成有两个柜子,分别是:`indexOfS`和`indexOfT` +- 每个柜子有很多层抽屉,那么比如`egg`和`add`,第一个字符的抽屉,分别是`e`和`a`,由于是第一个字符,上一次的抽屉中的值是0,是满足条件的。并且在抽屉中存入`i+1`标签,也就是1 +- 当来到第二个字符的`g`和`d`,也是满足条件的,因为第二个字符的抽屉中没有记录,并在抽屉中存入`i+1`,也就是2 +- 当来到第三个字符的`g`和`d`,其实也是满足的,虽然第二个字符的抽屉中有记录,但是两个柜子的第二个字符的抽屉中的记录是一样的,因此,也更新`i+1`,假如不一样,那么就返回`false`。 + +## code + +### js + +```js +let isIsomorphic = (s, t) => { + let indexOfS = []; + let indexOfT = []; + for (let i = 0; i < s.length; i++) { + if (indexOfS[s[i]] !== indexOfT[t[i]]) { + return false; + } + indexOfS[s[i]] = i + 1; + indexOfT[t[i]] = i + 1; + } + return true; +}; + +console.log(isIsomorphic("egg", "add")); +console.log(isIsomorphic("foo", "bar")); +console.log(isIsomorphic("paper", "title")); +``` + +### go + +```go +func isIsomorphic(s, t string) bool { + indexOfS := make([]int, 256) + indexOfT := make([]int, 256) + for i := 0; i < len(s); i++ { + if indexOfS[s[i]] != indexOfT[t[i]] { + return false + } + indexOfS[s[i]] = i + 1 + indexOfT[t[i]] = i + 1 + } + return true +} +``` + +### java + +```java +class Solution { + public boolean isIsomorphic(String s, String t) { + int[] indexOfS = new int[256]; + int[] indexOfT = new int[256]; + for (int i = 0; i < s.length(); i++) { + char sc = s.charAt(i), tc = t.charAt(i); + if (indexOfS[sc] != indexOfT[tc]) { + return false; + } + indexOfS[sc] = i + 1; + indexOfT[tc] = i + 1; + } + return true; + } +} +``` + diff --git "a/Java/alg/lc/206.\345\217\215\350\275\254\351\223\276\350\241\250.md" "b/Java/alg/lc/206.\345\217\215\350\275\254\351\223\276\350\241\250.md" new file mode 100644 index 00000000..5861cf3a --- /dev/null +++ "b/Java/alg/lc/206.\345\217\215\350\275\254\351\223\276\350\241\250.md" @@ -0,0 +1,114 @@ +# 206. 反转链表 + +[url](https://leetcode-cn.com/problems/reverse-linked-list/) + + +## 题目 + +反转一个单链表。 + + +``` +输入: 1->2->3->4->5->NULL +输出: 5->4->3->2->1->NULL +``` + + +## 方法 + +两种方法: +- 尾递归 +- 迭代头插:双指针分别是`pre`和`cur`,中间有个临时节点`next` +- 画画图就明白了 + +## code + +### js + +```js +// 尾递归 +let reverseList1 = head => { + return reverse(null, head); +}; +let reverse = (pre, cur) => { + if (cur == null) return pre; + let next = cur.next; + cur.next = pre; + return reverse(cur, next); +}; +// 迭代头插 +let reverseList = head => { + let pre = null, cur = head; + while (cur != null) { + let next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; +}; +``` + +### go + +```go +// 尾递归 +func reverseList1(head *ListNode) *ListNode { + var reverse func (pre, cur *ListNode) *ListNode + reverse = func (pre, cur *ListNode) *ListNode { + if cur == nil { + return pre + } + next := cur.Next + cur.Next = pre + return reverse(cur, next) + } + return reverse(nil, head) +} +// 迭代头插 +func reverseList(head *ListNode) *ListNode { + // 头插 + var pre *ListNode + cur := head + for cur != nil { + next := cur.Next + cur.Next = pre + pre = cur + cur = next + } + return pre +} +``` + +### java + +```java +class Solution { + public ListNode reverseList(ListNode head) { + // 尾递归 + return reverse(null, head); + } + private ListNode reverse(ListNode pre, ListNode cur) { + if (cur == null) return pre; + ListNode next = cur.next; + cur.next = pre; + return reverse(cur, next); + } +} +// 迭代头插法 +class Solution { + public ListNode reverseList(ListNode head) { + // 头插 + ListNode pre = null; + ListNode cur = head; + while (cur != null) { + ListNode next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; + } +} +``` + diff --git "a/Java/alg/lc/21.\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\351\223\276\350\241\250.md" "b/Java/alg/lc/21.\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\351\223\276\350\241\250.md" new file mode 100644 index 00000000..a5e3a302 --- /dev/null +++ "b/Java/alg/lc/21.\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\351\223\276\350\241\250.md" @@ -0,0 +1,112 @@ +# 21. 合并两个有序链表 + +[url](https://leetcode-cn.com/problems/merge-two-sorted-lists/) + +## 题目 + +![](https://assets.leetcode.com/uploads/2020/10/03/merge_ex1.jpg) + + +将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 + +``` +输入:l1 = [1,2,4], l2 = [1,3,4] +输出:[1,1,2,3,4,4] +``` + +## 方法 + +两种方法:递归和迭代 + +递归:我感觉类似于栈,脑海里可以想象的一个栈,将数据一个一个按照程序存入栈中 +- 结束条件,两个,要么是l1为null,要么l2为空 +- 如果`l1.val < l2.val` 则递归,递归`l1.next = ???` 谁的值小,谁就递进一步,反之一样。 +- 记得返回相应的`l1` 或者 `l2` + +迭代:非常好理解,安装顺序遍历去对比,不过需要一个中间变量`dummy`虚拟节点存一下。 + + +## code + +### js + +```js +let mergeTwoLists = (l1, l2) => { + if (l1 === null) return l2; + if (l2 === null) return l1; + if (l1.val < l2.val) { + l1.next = mergeTwoLists(l1.next , l2); + return l1; + } else { + l2.next = mergeTwoLists(l1, l2.next); + return l2; + } +}; +``` + +### go + +- 递归 +```go +func mergeTwoLists(l1, l2 *ListNode) *ListNode { + if l1 == nil { + return l2 + } + if l2 == nil { + return l1 + } + if l1.Val < l2.Val { + l1.Next = mergeTwoLists(l1.Next, l2) + return l1 + } else { + l2.Next = mergeTwoLists(l1, l2.Next) + return l2 + } +} + +``` + +- 迭代 +```go +func mergeTwoLists1(l1, l2 *ListNode) *ListNode { + dummy := &ListNode{} + prev := dummy + // 迭代的思想 + for l1 != nil && l2 != nil { // 注意条件,二者都不能为nil + if l1.Val < l2.Val { + prev.Next = l1 + l1 = l1.Next // 符合,就递进 + } else { + prev.Next = l2 + l2 = l2.Next // 符合,就递进 + } + prev = prev.Next // 下一步 + } + // 剩下单链表 + if l1 != nil { + prev.Next = l1 + } else { + prev.Next = l2 + } + return dummy.Next +} +``` + +### java + +```java +class Solution { + public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + if (l1 == null) return l2; + if (l2 == null) return l1; + if (l1.val < l2.val) { + l1.next = mergeTwoLists(l1.next, l2); + return l1; + } else { + l2.next = mergeTwoLists(l1, l2.next); + return l2; + } + } +} +``` + diff --git "a/Java/alg/lc/217.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\240.md" "b/Java/alg/lc/217.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\240.md" new file mode 100644 index 00000000..e0ab5989 --- /dev/null +++ "b/Java/alg/lc/217.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\240.md" @@ -0,0 +1,79 @@ +# 217. 存在重复元素 + +[url](https://leetcode-cn.com/problems/contains-duplicate/) + + +## 题目 + +给定一个整数数组,判断是否存在重复元素。 + +如果存在一值在数组中出现至少两次,函数返回 `true` 。如果数组中每个元素都不相同,则返回 `false` 。 + + +``` +输入: [1,2,3,1] +输出: true +输入: [1,2,3,4] +输出: false +输入: [1,1,1,3,3,4,3,2,4,2] +输出: true +``` + + +## 方法 + +哈希 + +## code + +### js + +```js +let containsDuplicate = nums => { + let m = {}; + for (let i = 0; i < nums.length; i++) { + if (m[nums[i]] !== undefined) { + return true; + } else { + m[nums[i]] = 1; + } + } + return false; +}; +console.log(containsDuplicate([1, 2, 3, 1])); +console.log(containsDuplicate([1, 2, 3, 4])); +console.log(containsDuplicate([1,1,1,3,3,4,3,2,4,2])); +``` + +### go + +```go +func containsDuplicate(nums []int) bool { + m := make(map[int]int, len(nums)) + for _, num := range nums { + if _, ok := m[num]; ok { + return true + } else { + m[num] = 1 + } + } + return false +} +``` + +### java + +```java +class Solution { + public boolean containsDuplicate(int[] nums) { + HashSet set = new HashSet<>(); + for (int num : nums) { + if (!set.add(num)) { + return true; + } + } + return false; + } +} +``` + diff --git "a/Java/alg/lc/219.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\2402.md" "b/Java/alg/lc/219.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\2402.md" new file mode 100644 index 00000000..e9f78e58 --- /dev/null +++ "b/Java/alg/lc/219.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\2402.md" @@ -0,0 +1,85 @@ +# 219. 存在重复元素 II + +[url](https://leetcode-cn.com/problems/contains-duplicate-ii/) + + +## 题目 + +给定一个整数数组和一个整数 `k`,判断数组中是否存在两个不同的索引 `i` 和 `j`,使得 `nums [i] = nums [j]`,并且 `i` 和 `j` 的差的绝对值至多为 `k`。 + +``` +输入: nums = [1,2,3,1], k = 3 +输出: true +输入: nums = [1,0,1,1], k = 1 +输出: true +输入: nums = [1,2,3,1,2,3], k = 2 +输出: false +``` + + +## 方法 + +哈希 + +## code + +### js + +```js +let containsNearByDuplicate = (nums, k) => { + let m = new Set() + for (let i = 0; i < nums.length; i++) { + if (m.has(nums[i])) { + return true; + } + m.add(nums[i]); + if (m.size > k) { + m.delete(nums[i - k]); + } + } + return false; +}; +console.log(containsNearByDuplicate([1, 2, 3, 1], 3)); +console.log(containsNearByDuplicate([1, 0, 1 ,1], 1)); +console.log(containsNearByDuplicate([1, 2, 3, 1, 2, 3], 2)); +``` + +### go + +```go +func containsNearbyDuplicate(nums []int, k int) bool { + m := make(map[int]int, len(nums)) + for i := 0; i < len(nums); i++ { + if _, ok := m[nums[i]]; ok { + return true + } + m[nums[i]] = 1 + if len(m) > k { + delete(m, nums[i - k]) + } + } + return false +} +``` + +### java + +```java +class Solution { + public boolean containsNearbyDuplicate(int[] nums, int k) { + HashSet set = new HashSet<>(); + for (int i = 0; i < nums.length; i++) { + if(set.contains(nums[i])) { + return true; + } + set.add(nums[i]); + // 关键这里 + if (set.size() > k) { + set.remove(nums[i - k]); + } + } + return false; + } +} +``` + diff --git "a/Java/alg/lc/22.\346\213\254\345\217\267\347\224\237\346\210\220.md" "b/Java/alg/lc/22.\346\213\254\345\217\267\347\224\237\346\210\220.md" new file mode 100644 index 00000000..b6ba4f5c --- /dev/null +++ "b/Java/alg/lc/22.\346\213\254\345\217\267\347\224\237\346\210\220.md" @@ -0,0 +1,90 @@ +# 22. 括号生成 + + +[url](https://leetcode-cn.com/problems/generate-parentheses/) + +## 题目 + +数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。 + + +``` +输入:n = 3 +输出:["((()))","(()())","(())()","()(())","()()()"] +输入:n = 1 +输出:["()"] +``` + +## 方法 + + +## code + +### js + +```js +let generateParenthesis = n => { + let dfs = (ans, cnt1, cnt2, n) => { + if (cnt1 > n || cnt2 > n) + return; + if (cnt1 === n && cnt2 === n) + ret.push(ans); + if (cnt1 >= cnt2){ + dfs(ans + "(", cnt1 + 1, cnt2, n); + dfs(ans + ")", cnt1, cnt2 + 1, n); + } + }; + let ret = [] + dfs("", 0, 0, n); + return ret; +}; +``` + +### go + +```go +func generateParenthesis(n int) []string { + var res []string + var dfs func(ans string, cnt1 int, cnt2 int, n int) + dfs = func (ans string, cnt1 int, cnt2 int, n int) { + if cnt1 > n || cnt2 > n { + return + } + if cnt1 == n && cnt2 == n { + res = append(res, ans) + } + if cnt1 >= cnt2 { + //ans1 := ans + dfs(ans + "(", cnt1 + 1, cnt2, n) + dfs(ans + ")", cnt1, cnt2 + 1, n) + } + } + dfs("", 0, 0, n) + return res +} +``` + +### java + +```java +class Solution { + List ret = new ArrayList<>(); + public List generateParenthesis(int n) { + dfs("", 0, 0, n); + return ret; + } + + public void dfs(String ans, int cnt1, int cnt2, int n){ + if (cnt1 > n || cnt2 > n) + return; + if (cnt1 == n && cnt2 == n) + ret.add(ans); + if (cnt1 >= cnt2){ + String ans1 = new String(ans); + dfs(ans + "(", cnt1 + 1, cnt2, n); + dfs(ans + ")", cnt1, cnt2 + 1, n); + } + } +} +``` + diff --git "a/Java/alg/lc/225.\347\224\250\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210.md" "b/Java/alg/lc/225.\347\224\250\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210.md" new file mode 100644 index 00000000..7bdacab2 --- /dev/null +++ "b/Java/alg/lc/225.\347\224\250\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210.md" @@ -0,0 +1,114 @@ +# 225. 用队列实现栈 + +[url](https://leetcode-cn.com/problems/implement-stack-using-queues/) + + +## 题目 + +请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通队列的全部四种操作`(push、top、pop 和 empty)`。 + +实现 `MyStack` 类: + +- `void push(int x) `将元素` x `压入栈顶。 +- `int pop() `移除并返回栈顶元素。 +- `int top()` 返回栈顶元素。 +- `boolean empty()` 如果栈是空的,返回 `true` ;否则,返回 `false` 。 + + +## 方法 + +单队列的实现方法 + +## code + +### js + +```js +let Mystack = function() { + this.queue = []; +}; +Mystack.prototype.push = function(x) { + this.queue.push(x); +}; +Mystack.prototype.pop = function() { + return this.queue.pop() +}; +Mystack.prototype.top = function() { + return this.queue[this.queue.length - 1]; +}; + +Mystack.prototype.empty = function() { + return this.queue.length === 0; +}; +var obj = new Mystack(); +obj.push(1); +obj.push(2); +console.log(obj.pop()); +console.log(obj.top()); +console.log(obj.empty()); +``` + +### go + +```go +var q []int +func push1(x int) { + q = append(q, x) + cnt := len(q) + for cnt > 1 { + cnt-- + q = append(q, q[len(q) - 1]) + } +} +func pop1() int { + q = q[0:len(q) - 1] + return q[len(q) - 1] +} +func top1() int { + return q[len(q) - 1] +} +func empty() bool { + return len(q) == 0 +} +``` + +### java + +```java +class MyStack { + private Queue queue; + /** Initialize your data structure here. */ + public MyStack() { + queue = new LinkedList<>(); + } + /** Push element x onto stack. */ + public void push(int x) { + queue.add(x); + int cnt = queue.size(); + while (cnt-- > 1) { + queue.add(queue.poll()); + } + } + /** Removes the element on top of the stack and returns that element. */ + public int pop() { + return queue.remove(); + } + /** Get the top element. */ + public int top() { + return queue.peek(); + } + /** Returns whether the stack is empty. */ + public boolean empty() { + return queue.isEmpty(); + } +} +/** + * Your MyStack object will be instantiated and called as such: + * MyStack obj = new MyStack(); + * obj.push(x); + * int param_2 = obj.pop(); + * int param_3 = obj.top(); + * boolean param_4 = obj.empty(); + */ +``` + diff --git "a/Java/alg/lc/226.\347\277\273\350\275\254\344\272\214\345\217\211\346\240\221.md" "b/Java/alg/lc/226.\347\277\273\350\275\254\344\272\214\345\217\211\346\240\221.md" new file mode 100644 index 00000000..1e23acaf --- /dev/null +++ "b/Java/alg/lc/226.\347\277\273\350\275\254\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,75 @@ +# 226. 翻转二叉树 + +[url](https://leetcode-cn.com/problems/invert-binary-tree/) + + +## 题目 + +翻转一棵二叉树。 + +示例 +``` + 4 + / \ + 2 7 + / \ / \ +1 3 6 9 +``` + +输出 + +``` + 4 + / \ + 7 2 + / \ / \ +9 6 3 1 +``` + + +## 方法 + +递归 + +## code + +### js + +```js +let invertTree = root => { + if (root == null) return null; + let left = root.left; + root.left = invertTree(root.right); + root.right = invertTree(left); + return root; +}; +``` + +### go + +```go +func invertTree(root *TreeNode) *TreeNode { + if root == nil { + return nil + } + left := root.Left + root.Left = invertTree(root.Right) + root.Right = invertTree(left) + return root +} +``` + +### java + +```java +class Solution { + public TreeNode invertTree(TreeNode root) { + if (root == null) return null; + TreeNode left = root.left; + root.left = invertTree(root.right); + root.right = invertTree(left); + return root; + } +} +``` + diff --git "a/Java/alg/lc/231.2\347\232\204\345\271\202.md" "b/Java/alg/lc/231.2\347\232\204\345\271\202.md" new file mode 100644 index 00000000..9b64176b --- /dev/null +++ "b/Java/alg/lc/231.2\347\232\204\345\271\202.md" @@ -0,0 +1,68 @@ +# 231. 2的幂 + +[url](https://leetcode-cn.com/problems/power-of-two/) + + +## 题目 + +给定一个整数,编写一个函数来判断它是否是 `2` 的幂次方。 + +示例 +``` +输入: 1 +输出: true +解释: 2的0次方 = 1 +输入: 16 +输出: true +解释: 2的4次方 = 16 +输入: 218 +输出: false +``` + + +## 方法 + +计算该数在二进制1的个数 +- 如果等于1,则是2的幂 +- 否则不是 + +## code + +### js + +```js +let isPowerOfTwo = n => { + return n > 0 && countOf1(n) === 1; +}; +// 手写,不就是计算1的个数嘛 +let countOf1 = n => { + let cnt = 0; + while (n !== 0) { + cnt++; + n &= (n - 1); + } + return cnt; +}; +console.log(isPowerOfTwo(1)); +console.log(isPowerOfTwo(16)); +console.log(isPowerOfTwo(218)); +``` + +### go + +```go +func isPowerOfTwo(n int) bool { + return n > 0 && bits.OnesCount32(uint32(n)) == 1 +} +``` + +### java + +```java +class Solution { + public boolean isPowerOfTwo(int n) { + return n > 0 && Integer.bitCount(n) == 1; + } +} +``` + diff --git "a/Java/alg/lc/232.\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" "b/Java/alg/lc/232.\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" new file mode 100644 index 00000000..9a4b0905 --- /dev/null +++ "b/Java/alg/lc/232.\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" @@ -0,0 +1,158 @@ +# 232. 用栈实现队列 + +[url](https://leetcode-cn.com/problems/implement-queue-using-stacks/) + + +## 题目 + +请你仅使用两个栈实现先入先出队列。队列应当支持一般队列的支持的所有操作`(push、pop、peek、empty)`: + +实现 `MyQueue` 类: + +- `void push(int x)` 将元素 x 推到队列的末尾 +- `int pop() `从队列的开头移除并返回元素 +- `int peek()` 返回队列开头的元素 +- `boolean empty()` 如果队列为空,返回 `true` ;否则,返回 `false` + + + + +## 方法 + +双栈实现一个队列 + +## code + +### js + +```js +let MyQueue = function () { + this.in = []; + this.out = []; +}; +MyQueue.prototype.push = function (x) { + this.in.push(x); +}; +MyQueue.prototype.pop = function () { + this.in2out(); + return this.out.pop(); +}; +MyQueue.prototype.peek = function () { + this.in2out(); + return this.out[this.out.length - 1]; +}; +MyQueue.prototype.empty = function () { + return this.in.length === 0 && this.out.length === 0; +}; +MyQueue.prototype.in2out = function () { + if (this.out.length === 0) { + while (this.in.length !== 0) { + this.out.push(this.in.pop()); + } + } +}; +let obj = new MyQueue(); +obj.push(1); +obj.push(2); +console.log(obj.pop()); +console.log(obj.peek()); +console.log(obj.empty()) +``` + +### go + +```go +type MyQueue struct { + in []int + out []int +} +/** Initialize your data structure here. */ +func Constructor() MyQueue { + return MyQueue{} +} +func (this *MyQueue) in2out() { + if len(this.out) == 0 { + for len(this.in) != 0 { + this.out = append(this.out, this.in[len(this.in) - 1]) + this.in = this.in[:len(this.in) - 1] + } + } +} +/** Push element x to the back of queue. */ +func (this *MyQueue) Push(x int) { + this.in = append(this.in, x) +} + +/** Removes the element from in front of queue and returns that element. */ +func (this *MyQueue) Pop() int { + this.in2out() + value := this.out[len(this.out) - 1] + this.out = this.out[:len(this.out) - 1] + return value +} +/** Get the front element. */ +func (this *MyQueue) Peek() int { + this.in2out() + return this.out[len(this.out) - 1] +} +/** Returns whether the queue is empty. */ +func (this *MyQueue) Empty() bool { + return len(this.in) == 0 && len(this.out) == 0 +} +/** + * Your MyQueue object will be instantiated and called as such: + * obj := Constructor(); + * obj.Push(x); + * param_2 := obj.Pop(); + * param_3 := obj.Peek(); + * param_4 := obj.Empty(); + */ +``` + +### java + +```java +class MyQueue { + private Stack in; + private Stack out; + /** Initialize your data structure here. */ + public MyQueue() { + in = new Stack<>(); + out = new Stack<>(); + } + /** Push element x to the back of queue. */ + public void push(int x) { + in.push(x); + } + /** Removes the element from in front of queue and returns that element. */ + public int pop() { + in2Out(); + return out.pop(); + } + /** Get the front element. */ + public int peek() { + in2Out(); + return out.peek(); + } + private void in2Out() { + if (out.isEmpty()) { + while (!in.isEmpty()) { + out.push(in.pop()); + } + } + } + /** Returns whether the queue is empty. */ + public boolean empty() { + return in.isEmpty() && out.isEmpty(); + } +} +/** + * Your MyQueue object will be instantiated and called as such: + * MyQueue obj = new MyQueue(); + * obj.push(x); + * int param_2 = obj.pop(); + * int param_3 = obj.peek(); + * boolean param_4 = obj.empty(); + */ +``` + diff --git "a/Java/alg/lc/234.\345\233\236\346\226\207\351\223\276\350\241\250.md" "b/Java/alg/lc/234.\345\233\236\346\226\207\351\223\276\350\241\250.md" new file mode 100644 index 00000000..90c8dd14 --- /dev/null +++ "b/Java/alg/lc/234.\345\233\236\346\226\207\351\223\276\350\241\250.md" @@ -0,0 +1,172 @@ +# 234. 回文链表 + +[url](https://leetcode-cn.com/problems/palindrome-linked-list/) + + +## 题目 + +请判断一个链表是否为回文链表。 + +示例 +``` +输入: 1->2 +输出: false +输入: 1->2->2->1 +输出: true +``` + + +## 方法 + +- 快慢指针,找中点 +- 切 +- 反转其中一个链表与另外一个进行对比,如果相等,则是回文,否则不是 + +## code + +### js + +```js +let isPalindrome = head => { + if (head === null || head.next === null) return true; + // 找中点 + let slow = head, fast = head; + while (fast !== null && fast.next !== null) { + slow = slow.next; + fast = fast.next.next; + } + if (fast !== null) slow = slow.next; + // 切 + cur(head, slow); + return isEquals(head, reverse(slow)); +}; +let cut = (head, slow) => { + let node = head; + while (node.next !== slow) { + node = node.next; + } + node.next = null; +}; +let reverse = head => { + if head === null return null + let pre = null, cur = head + while (cur !== null) { + let next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; +}; +let isEquals = (l1, l2) => { + while (l1 !== null && l2 !== null) { + if (l1.val !== l2.val) return false + l1 = l1.next; + l2 = l2.next; + } + return true +} +``` + +### go + +```go +func isPalindrome2(head *ListNode) bool { + if head == nil || head.Next == nil { + return true + } + // 找中点 + slow, fast := head, head + for fast != nil && fast.Next != nil { + slow = slow.Next + fast = fast.Next.Next + } + if fast != nil { + slow = slow.Next + } + // 切 + cut(head, slow) + return isEquals(head, reverse2(slow)) +} +func cut(head, slow *ListNode) { + node := head + for node.Next != slow { + node = node.Next + } + node.Next = nil +} +func reverse2(node *ListNode) *ListNode { + if node == nil { + return nil + } + var pre *ListNode + cur := node + for cur != nil { + next := cur.Next + cur.Next = pre + pre = cur + cur = next + } + return pre +} +func isEquals(l1, l2 *ListNode) bool { + for l1 != nil && l2 != nil { + if l1.Val != l2.Val { + return false + } + l1 = l1.Next + l2 = l2.Next + } + return true +} +``` + +### java + +```java +class Solution { + public boolean isPalindrome(ListNode head) { + if (head == null || head.next == null) return true; + // 找中点 + ListNode slow = head, fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + if (fast != null) slow = slow.next; + // 切 + cut(head, slow); + + return isEquals(head, reverse(slow)); + } + private void cut(ListNode head, ListNode slow) { + ListNode node = head; + while (node.next != slow) { + node = node.next; + } + node.next = null; + } + private ListNode reverse(ListNode node) { + if (node == null) return null; + ListNode pre = null; + ListNode cur = node; + while (cur != null) { + ListNode next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; + } + private boolean isEquals(ListNode l1, ListNode l2) { + while (l1 != null && l2 != null) { + if (l1.val != l2.val) return false; + l1 = l1.next; + l2 = l2.next; + } + + return false; + } +} +``` + diff --git "a/Java/alg/lc/237.\345\210\240\351\231\244\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" "b/Java/alg/lc/237.\345\210\240\351\231\244\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" new file mode 100644 index 00000000..0840ac42 --- /dev/null +++ "b/Java/alg/lc/237.\345\210\240\351\231\244\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" @@ -0,0 +1,59 @@ +# 237. 删除链表中的节点 + +[url](https://leetcode-cn.com/problems/delete-node-in-a-linked-list/) + + +## 题目 + +请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为 要被删除的节点 。 + +现有一个链表 `-- head = [4,5,1,9]`,它可以表示为: + +![237_example-lc-mhWRJh](https://cdn.jsdelivr.net/gh/DreamCats/imgs@main/uPic/237_example-lc-mhWRJh.png) + +``` +输入:head = [4,5,1,9], node = 5 +输出:[4,1,9] +解释:给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9. +输入:head = [4,5,1,9], node = 1 +输出:[4,5,9] +解释:给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9. + +``` + + +## 方法 + + + +## code + +### js + +```js +let deleteNode = node => { + node.val = node.next.val; + node.next = node.next.next; +}; +``` + +### go + +```go +func deleteNode(node *ListNode) { + node.Val = node.Next.Val + node.Next = node.Next.Next +} +``` + +### java + +```java +class Solution { + public void deleteNode(ListNode node) { + node.val = node.next.val; + node.next = node.next.next; + } +} +``` + diff --git "a/Java/alg/lc/24.\344\270\244\344\270\244\344\272\244\346\215\242\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" "b/Java/alg/lc/24.\344\270\244\344\270\244\344\272\244\346\215\242\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" new file mode 100644 index 00000000..5878b3a7 --- /dev/null +++ "b/Java/alg/lc/24.\344\270\244\344\270\244\344\272\244\346\215\242\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" @@ -0,0 +1,93 @@ +# 24. 两两交换链表中的节点 + + + +[url](https://leetcode-cn.com/problems/swap-nodes-in-pairs/) + +## 题目 + +给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 + +你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 + +![](https://assets.leetcode.com/uploads/2020/10/03/swap_ex1.jpg) +``` +输入:head = [1,2,3,4] +输出:[2,1,4,3] +输入:head = [] +输出:[] +输入:head = [1] +输出:[1] +``` + +## 方法 + + +## code + +### js + +```js +let swapPairs = head => { + if (head === null) + return head; + let node = new ListNode(); + node.next = head; + let pre = node; + while (pre.next !== null && pre.next.next !== null) { + let l1 = pre.next, l2 = pre.next.next; + let next = l2.next; + l1.next = next; + l2.next = l1; + pre.next = l2; + pre = l1; + } + return node.next; +}; +``` + +### go + +```go +func swapPairs(head *ListNode) *ListNode { + if head == nil { + return head + } + node := &ListNode{Val: 0} + node.Next = head + pre := node + for pre.Next != nil && pre.Next.Next != nil { + l1, l2 := pre.Next, pre.Next.Next + next := l2.Next + l1.Next = next + l2.Next = l1 + pre.Next = l2 + pre = l1 + } + return node.Next +} +``` + +### java + +```java +class Solution { + public ListNode swapPairs(ListNode head) { + if (head == null) + return head; + ListNode node = new ListNode(0); + node.next = head; + ListNode pre = node; + while (pre.next != null && pre.next.next != null) { + ListNode l1 = pre.next, l2 = pre.next.next; + ListNode next = l2.next; + l1.next = next; + l2.next = l1; + pre.next = l2; + pre = l1; + } + return node.next; + } +} +``` + diff --git "a/Java/alg/lc/242.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" "b/Java/alg/lc/242.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" new file mode 100644 index 00000000..e97f263e --- /dev/null +++ "b/Java/alg/lc/242.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" @@ -0,0 +1,85 @@ +# 242. 有效的字母异位词 + +[url](https://leetcode-cn.com/problems/valid-anagram/) + + +## 题目 + +给定两个字符串 `s` 和 `t` ,编写一个函数来判断 `t` 是否是 `s` 的字母异位词。 + +``` +输入: s = "anagram", t = "nagaram" +输出: true +输入: s = "rat", t = "car" +输出: false +``` + + +## 方法 + +其实就是判断两个字符串中的对应的字符数量 + +- 遍历s,对应的字符++ +- 遍历t,对应的字符-- +- 遍历桶,如果存在不为0,则返回false + +## code + +### js + +```js +let isAnagram = (s, t) => { + let cnts = Array(256).fill(0); + s = s.split(""); + t = t.split(""); + s.forEach(item => cnts[item.charCodeAt(0)]++); + t.forEach(item => cnts[item.charCodeAt(0)]--); + for (let value of cnts) { + if (value !== 0) return false; + } + return true; +}; +console.log(isAnagram("anagram", "nagaram")) +console.log(isAnagram("rat", "car")) +``` + +### go + +```go +func isAnagram(s, t string) bool { + cnts := make([]int, 256) + for _, c := range s{ + cnts[c]++ + } + for _, c := range t{ + cnts[c]-- + } + for _, c := range cnts { + if c != 0 { + return false + } + } + return true +} +``` + +### java + +```java +class Solution { + public boolean isAnagram(String s, String t) { + int[] cnts = new int[26]; + for (char c : s.toCharArray()) { + cnts[c - 'a']++; + } + for (char c : t.toCharArray()) { + cnts[c - 'a']--; + } + for (int c : cnts) { + if (c != 0) return false; + } + return true; + } +} +``` + diff --git "a/Java/alg/lc/26.\345\210\240\351\231\244\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.md" "b/Java/alg/lc/26.\345\210\240\351\231\244\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.md" new file mode 100644 index 00000000..006795d9 --- /dev/null +++ "b/Java/alg/lc/26.\345\210\240\351\231\244\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.md" @@ -0,0 +1,80 @@ +# 26. 删除排序数组中的重复项 + +[url](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/) + +## 题目 + +给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 + +给定数组 `nums = [1,1,2]`, + +函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 + +你不需要考虑数组中超出新长度后面的元素。 + + +## 方法 + +双指针,其实就是一个指针固定第一个元素上,第二个指针遍历数组。 +- 比如,1,2,2。 用一个变量p在1元素上的索引,另一个变量i在元素2的索引上 +- 将索引i的元素和索引p的索引上的元素进行比较 +- 如果元素不相同,就将p索引+1,并之后将索引i的元素转移到索引p的位置上 +- 如果相同,就i依然跟着数组遍历 + + +## code + +### js + +```js{cmd="node"} +let removeDuplicates = nums => { + let p = 0; + for (let i = 1; i < nums.length; i++) { + if (nums[i] !== nums[p]) { + nums[++p] = nums[i]; + } + } + console.log(nums); + return p + 1; +} + +console.log(removeDuplicates([1, 1, 2])); +``` + +### go + +```go +func removeDuplicates(nums []int) int { + // 单指针 + p := 0 + for i := 1; i < len(nums); i++ { + if nums[i] != nums[p] { + p += 1 + nums[p] = nums[i] + } + } + return p + 1 +} +``` + + + +### java + +```java +class Solution { + public int removeDuplicates(int[] nums) { + // 单指针 + int p = 0; + for(int i = 1; i < nums.length; i++){ + if (nums[i] != nums[p]) { + nums[++p] = nums[i]; + } + } + return p + 1; + } +} +``` + diff --git "a/Java/alg/lc/268.\344\270\242\345\244\261\347\232\204\346\225\260\345\255\227.md" "b/Java/alg/lc/268.\344\270\242\345\244\261\347\232\204\346\225\260\345\255\227.md" new file mode 100644 index 00000000..710ad382 --- /dev/null +++ "b/Java/alg/lc/268.\344\270\242\345\244\261\347\232\204\346\225\260\345\255\227.md" @@ -0,0 +1,71 @@ +# 268. 丢失的数字 + +[url](https://leetcode-cn.com/problems/valid-anagram/) + + +## 题目 + +给定一个包含 `[0, n]` 中 `n` 个数的数组 `nums` ,找出 `[0, n]` 这个范围内没有出现在数组中的那个数。 + +``` +输入:nums = [3,0,1] +输出:2 +解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。 +输入:nums = [0,1] +输出:2 +解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。 +输入:nums = [9,6,4,2,3,5,7,0,1] +输出:8 +解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。 +输入:nums = [0] +输出:1 +解释:n = 1,因为有 1 个数字,所以所有的数字都在范围 [0,1] 内。1 是丢失的数字,因为它没有出现在 nums 中。 +``` + + +## 方法 + +还是异或 + +## code + +### js + +```js +let missingNumber = nums => { + let ret = 0; + nums.forEach((index, item) => ret = ret ^ index ^ item); + return ret ^ nums.length; +}; +console.log(missingNumber([3, 0 ,1 ])) +console.log(missingNumber([0, 1])) +console.log(missingNumber([9,6,4,2,3,5,7,0,1])) +console.log(missingNumber([0])) +``` + +### go + +```go +func missingNumber(nums []int) int { + ret := 0 + for i, v := range nums{ + ret = ret ^ i ^ v + } + return ret ^ len(nums) +} +``` + +### java + +```java +class Solution { + public int missingNumber(int[] nums) { + int ret = 0; + for (int i = 0; i < nums.length; i++) { + ret = ret ^ i ^ nums[i]; + } + return ret ^ nums.length; + } +} +``` + diff --git "a/Java/alg/lc/27.\347\247\273\351\231\244\345\205\203\347\264\240.md" "b/Java/alg/lc/27.\347\247\273\351\231\244\345\205\203\347\264\240.md" new file mode 100644 index 00000000..9a3aa7a1 --- /dev/null +++ "b/Java/alg/lc/27.\347\247\273\351\231\244\345\205\203\347\264\240.md" @@ -0,0 +1,91 @@ +# 27. 移除元素 + +[url](https://leetcode-cn.com/problems/remove-element/) + +## 题目 + +``` +给你一个数组 `nums `和一个值 `val`,你需要 原地 移除所有数值等于 `val` 的元素,并返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须仅使用 `O(1) `额外空间并 原地修改输入数组。 + +元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 + +给定 `nums = [3,2,2,3], val = 3`, + +函数应该返回新的长度 2, 并且 `nums` 中的前两个元素均为 2。 + +你不需要考虑数组中超出新长度后面的元素。 + +给定 `nums = [0,1,2,2,3,0,4,2], val = 2`, + +函数应该返回新的长度 5, 并且 `nums` 中的前五个元素为 `0, 1, 3, 0, 4`。 + +注意这五个元素可为任意顺序。 + +你不需要考虑数组中超出新长度后面的元素。 +``` + + + + +## 方法 + +双指针,其实就是一个指针固定第一个元素上,第二个指针遍历数组。 +- 比如,3,2,2,3。 用一个变量p和i在3元素上的索引, +- 将索引i的元素和val的值进行比较 +- 如果值不相同,将索引i的元素转移到索引p的位置上,并且将p++ +- 如果相同,就i依然跟着数组遍历 + + +## code + +### js + +```js{cmd="node"} +let removeElement = (nums, val) => { + let p = 0; + for (let i = 0; i < nums.length; i++) { + if (nums[i] !== val) + nums[p++] = nums[i]; + } + console.log(nums); + return p; +} + +console.log(removeElement([3, 2, 2, 3], 3)); +``` + +### go + +```go +func removeElement(nums []int, val int) int { + p := 0 + for i := 0; i < len(nums); i++ { + if nums[i] != val { + nums[p] = nums[i] + p++ + } + } + fmt.Println(nums) + return p +} +``` + + + +### java + +```java +class Solution { + public int removeElement(int[] nums, int val) { + int p = 0; + for (int i = 0; i < nums.length; i++){ + if (nums[i] != val) + nums[p++] = nums[i]; + } + return p; + } +} +``` + diff --git "a/Java/alg/lc/278.\347\254\254\344\270\200\344\270\252\351\224\231\350\257\257\347\232\204\347\211\210\346\234\254.md" "b/Java/alg/lc/278.\347\254\254\344\270\200\344\270\252\351\224\231\350\257\257\347\232\204\347\211\210\346\234\254.md" new file mode 100644 index 00000000..293d8fc0 --- /dev/null +++ "b/Java/alg/lc/278.\347\254\254\344\270\200\344\270\252\351\224\231\350\257\257\347\232\204\347\211\210\346\234\254.md" @@ -0,0 +1,82 @@ +# 278. 第一个错误的版本 + +[url](https://leetcode-cn.com/problems/first-bad-version/) + + +## 题目 + +你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。 + +假设你有 `n` 个版本 `[1, 2, ..., n]`,你想找出导致之后所有版本出错的第一个错误的版本。 + +你可以通过调用 `bool isBadVersion(version)` 接口来判断版本号 `version` 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 `API` 的次数。 + + +``` +给定 n = 5,并且 version = 4 是第一个错误的版本。 +调用 isBadVersion(3) -> false +调用 isBadVersion(5) -> true +调用 isBadVersion(4) -> true +所以,4 是第一个错误的版本。  +``` + + +## 方法 + +二分法 + +## code + +### js + +```js +let firstBadVersion = n => { + let l = 1, h = n; + while (l < h) { + let m = l + Math.floor((h - l) / 2); + if isBadVersion(m) { + h = m; + } else { + l = m + 1; + } + } + return l; +}; +``` + +### go + +```go +func firstBadVersion(n int) int { + l, h := 1, n + for l < h { + m := l + (h - l) / 2 + if isBadVersion(m) { + h = m + } else { + l = m + 1 + } + } + return l +} +``` + +### java + +```java +public class Solution extends VersionControl { + public int firstBadVersion(int n) { + int l = 1, h = n; + while (l < h) { + int mid = l + (h - l) / 2; + if (isBadVersion(mid)) { + h = mid; + } else { + l = mid + 1; + } + } + return l; + } +} +``` + diff --git "a/Java/alg/lc/28.\345\256\236\347\216\260strStr.md" "b/Java/alg/lc/28.\345\256\236\347\216\260strStr.md" new file mode 100644 index 00000000..dc139d25 --- /dev/null +++ "b/Java/alg/lc/28.\345\256\236\347\216\260strStr.md" @@ -0,0 +1,76 @@ +# 28. 实现 strStr() + +[url](https://leetcode-cn.com/problems/implement-strstr/) + +## 题目 + +实现 strStr() 函数。 + +给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回  -1。 + +``` +输入: haystack = "hello", needle = "ll" +输出: 2 + +输入: haystack = "aaaaa", needle = "bba" +输出: -1 +``` + +## 方法 + +滑动窗口 +- 以needle的字符串长度在haystack字符串上滑动 +- 不断与needle进行匹配,如果相等,则返回窗口的开始索引 + + +## code + +### js + +```js{cmd="node"} +let strStr = (haystack, needle) => { + let l = haystack.length; + let r = needle.length; + for (let i = 0; i < l - r + 1; i++) { + if (haystack.substring(i , i + r) === needle) + return i; + } + return -1; +} + +console.log(strStr("hello", "ll")); +console.log(strStr("aaaaa", "bba")); +``` + +### go + +```go +func strStr(haystack, needle string) int { + l := len(haystack) + r := len(needle) + for i := 0; i < l - r + 1; i++ { + if haystack[i:i + r] == needle { + return i + } + } + return -1 +} +``` + + + +### java + +```java +class Solution { + public int strStr(String haystack, String needle) { + int l = haystack.length(), r = needle.length(); + for (int i = 0; i < l - r + 1; i++) { + if (haystack.substring(i, i + r).equals(needle)) + return i; + } + return -1; + } +} +``` + diff --git "a/Java/alg/lc/283.\347\247\273\345\212\250\351\233\266.md" "b/Java/alg/lc/283.\347\247\273\345\212\250\351\233\266.md" new file mode 100644 index 00000000..bfd0937c --- /dev/null +++ "b/Java/alg/lc/283.\347\247\273\345\212\250\351\233\266.md" @@ -0,0 +1,71 @@ +# 283. 移动零 + +[url](https://leetcode-cn.com/problems/move-zeroes/) + + +## 题目 + +给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 + +``` +输入: [0,1,0,3,12] +输出: [1,3,12,0,0] +``` + + +## 方法 + +- 将所有的非0向前移动 +- 从非0的最后位置开始遍历填充0 + +## code + +### js + +```js +let moveZeros = nums => { + let idx = 0; + nums.forEach(num => {if (num !== 0) nums[idx++] = num}); + while (idx < nums.length) { + nums[idx++] = 0; + } + return nums; +}; +console.log(moveZeros([0,1,0,3,12])) +``` + +### go + +```go +func moveZeroes(nums []int) { + idx := 0 + for _, num := range nums { + if num != 0 { + nums[idx] = num + idx++ + } + } + for idx < len(nums) { + nums[idx] = 0 + idx++ + } + fmt.Println(nums) +} +``` + +### java + +```java +class Solution { + public void moveZeroes(int[] nums) { + int idx = 0; + for (int num : nums) { + if (num != 0) nums[idx++] = num; + } + while (idx < nums.length) { + nums[idx++] =0; + } + } +} +``` + diff --git "a/Java/alg/lc/290.\345\215\225\350\257\215\350\247\204\345\276\213.md" "b/Java/alg/lc/290.\345\215\225\350\257\215\350\247\204\345\276\213.md" new file mode 100644 index 00000000..470e0dbf --- /dev/null +++ "b/Java/alg/lc/290.\345\215\225\350\257\215\350\247\204\345\276\213.md" @@ -0,0 +1,88 @@ +# 290. 单词规律 + +[url](https://leetcode-cn.com/problems/word-pattern/) + + +## 题目 + +给定一种规律 `pattern` 和一个字符串 `str` ,判断 `str` 是否遵循相同的规律。 + +这里的 遵循 指完全匹配,例如, `pattern` 里的每个字母和字符串 `str` 中的每个非空单词之间存在着双向连接的对应规律。 + +``` +输入: pattern = "abba", str = "dog cat cat dog" +输出: true +输入:pattern = "abba", str = "dog cat cat fish" +输出: false +输入: pattern = "aaaa", str = "dog cat cat dog" +输出: false +输入: pattern = "abba", str = "dog dog dog dog" +输出: false +``` + + +## 方法 + +哈希 + +## code + +### js + +```js +``` + +### go + +```go +func wordPattern(pattern, str string) bool { + array1 := strings.Split(str, " ") + if len(array1) != len(pattern) { + return false + } + array2 := strings.Split(pattern, "") + return wordPatternHelp(array1, array2) && wordPatternHelp(array2, array1) +} +func wordPatternHelp(array1, array2 []string) bool { + m := make(map[string]string) + for i := 0; i < len(array1); i++ { + key := array1[i] + if v, ok := m[key]; ok { + if v != array2[i] { + return false + } + } else { + m[key] = array2[i] + } + } + return true +} +``` + +### java + +```java +class Solution { + public boolean wordPattern(String pattern, String str) { + String[] array1 = str.split(" "); + if (array1.length != pattern.length()) return false; + String[] array2 = pattern.split(""); + return wordPatternHelp(array1, array2) && wordPatternHelp(array2, array1); + } + public boolean wordPatternHelp(String[] array1, String[] array2) { + HashMap map = new HashMap<>(); + for (int i = 0; i < array1.length; i++) { + String key = array1[i]; + if (map.containsKey(key)) { + if (!map.get(key).equals(array2[i])) { + return false; + } + } else { + map.put(key, array2[i]); + } + } + return true; + } +} +``` + diff --git "a/Java/alg/lc/3.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" "b/Java/alg/lc/3.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" new file mode 100644 index 00000000..c10fe61c --- /dev/null +++ "b/Java/alg/lc/3.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" @@ -0,0 +1,97 @@ +# 3. 无重复字符的最长子串 + +[url](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) + +## 题目 + + +``` +输入: s = "abcabcbb" +输出: 3 +解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 +输入: s = "bbbbb" +输出: 1 +解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 +输入: s = "pwwkew" +输出: 3 +解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 +  请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 +``` + +## 方法 + + +## code + +### js + +```js +let lengthOfLongestSubstring = s => { + if (s === null || s.length === 0) + return -1; + let n = s.length, max = 0; + let map = new Map(); + for (let i = 0, j = 0; j < n; j++) { + let c = s[j]; + if (map.has(c)) { + i = Math.max(i, map.get(c)); + } + max = Math.max(max, j - i + 1); + map.set(c, j + 1); + } + return max; +}; +``` + +### go + +```go +func lengthOfLongestSubstring(s string) int { + Max := func(a int, b int) int { + if a < b { + return b + } else { + return a + } + } + if len(s) == 0 { + return -1 + } + n := len(s) + max := 0 + m := make(map[byte]int, 0) + for i, j := 0, 0; j < n; j++ { + c := s[j] + if v, ok := m[c]; ok { + i = Max(i, v) + } + max = Max(max, j - i + 1) + m[c] = j + 1 + } + return max +} +``` + +### java + +```java +class Solution { + public int lengthOfLongestSubstring(String s) { + if (s == null || s.length() == 0) + return -1; + int n = s.length(); + int max = 0; + Map map = new HashMap<>(); + for (int i = 0, j = 0; j < n; j++){ + char c = s.charAt(j); + if (map.containsKey(c)){ + i = Math.max(i, map.get(c)); + } + max = Math.max(max, j - i + 1); + map.put(c, j + 1); + } + return max; + } +} +``` + diff --git "a/Java/alg/lc/31.\344\270\213\344\270\200\344\270\252\346\216\222\345\210\227.md" "b/Java/alg/lc/31.\344\270\213\344\270\200\344\270\252\346\216\222\345\210\227.md" new file mode 100644 index 00000000..a6b5193c --- /dev/null +++ "b/Java/alg/lc/31.\344\270\213\344\270\200\344\270\252\346\216\222\345\210\227.md" @@ -0,0 +1,130 @@ +# 31. 下一个排列 + +[url](https://leetcode-cn.com/problems/next-permutation/) + +## 题目 + +实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。 + +如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。 + +必须 原地 修改,只允许使用额外常数空间。 + + +``` +输入:nums = [1,2,3] +输出:[1,3,2] +输入:nums = [3,2,1] +输出:[1,2,3] +输入:nums = [1,1,5] +输出:[1,5,1] +输入:nums = [1] +输出:[1] +``` + +## 方法 + + +## code + +### js + +```js +let nextPermutation = nums => { + let reverse = (nums, i, j) => { + while (i < j) { + swap(nums, i++, j--); + } + }; + let swap = (nums, i, j) => { + let t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; + }; + let len = nums.length; + if (len < 2) + return; + let i = len - 1; + while (i > 0 && nums[i - 1] >= nums[i]) + i--;// 从后向前找第一个正序,这里最后i指向的是逆序起始位置 + reverse(nums, i, len - 1); // 翻转后面的逆序区域,使其变为正序 + if (i === 0) + return; + let j = i - 1; + while (i < len && nums[j] >= nums[i]) + i++; // 找到第一个比nums[j]大的元素,交换即可 + // 交换 + swap(nums, i, j); + return nums; +}; +``` + +### go + +```go +func nextPermutation(nums []int) { + swap :=func (nums []int, i int, j int) { + t := nums[i] + nums[i] = nums[j] + nums[j] = t + } + reverse := func (nums []int, i int, j int) { + for i < j { + swap(nums, i, j) + i++ + j-- + } + } + l := len(nums) + if l < 2 { + return + } + i := l - 1 + for i > 0 && nums[i-1] >= nums[i] { + i-- // 从后向前找第一个正序,这里最后i指向的是逆序起始位置 + } + reverse(nums, i, l - 1) // 翻转后面的逆序区域,使其变为正序 + if i == 0 { + return + } + j := i - 1 + for i < l && nums[j] >= nums[i] { + i++ // 找到第一个比nums[j]大的元素,交换即可 + } + swap(nums, i, j) +} +``` + + + +### java + +```java +class Solution { + public void nextPermutation(int[] nums) { + int len = nums.length; + if (len < 2) return; + int i = len - 1; + while (i > 0 && nums[i - 1] >= nums[i]) + i--; // 从后向前找第一个正序,这里最后i指向的是逆序起始位置 + reverse(nums, i, len - 1); // 翻转后面的逆序区域,使其变为正序 + if (i == 0) return; + int j = i - 1; + while(i < len && nums[j] >= nums[i]) + i++; // 找到第一个比nums[j]大的元素,交换即可 + // 交换 + swap(nums, i, j); + } + private void reverse(int[] nums, int i, int j) { + while (i < j) { + swap(nums, i++, j--); + } + } + private void swap(int[] nums, int i, int j){ + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; + } +} +``` + diff --git "a/Java/alg/lc/326.3\347\232\204\345\271\202.md" "b/Java/alg/lc/326.3\347\232\204\345\271\202.md" new file mode 100644 index 00000000..9ad2dc05 --- /dev/null +++ "b/Java/alg/lc/326.3\347\232\204\345\271\202.md" @@ -0,0 +1,75 @@ +# 326. 3的幂 + +[url](https://leetcode-cn.com/problems/power-of-three/) + + +## 题目 + +给定一个整数,写一个函数来判断它是否是 `3` 的幂次方。如果是,返回 `true` ;否则,返回 `false` 。 + +整数 `n` 是 3` 的幂次方需满足:存在整数 `x` 使得 `n == 3x` + + +``` +输入:n = 27 +输出:true +输入:n = 0 +输出:false +输入:n = 9 +输出:true +输入:n = 45 +输出:false +``` + + +## 方法 + + +## code + +### js + +```js +let isPowerOfThree = n => { + if (n < 1) return false; + while (n > 1) { + if (n % 3 !== 0) { + return false; + } + n /= 3; + } +}; +``` + +### go + +```go +func isPowerOfThree(n int) bool { + if n < 1 { + return false + } + for n > 1 { + if n % 3 != 0 { + return false + } + n /= 3 + } + return true +} +``` + +### java + +```java +class Solution { + public boolean isPowerOfThree(int n) { + if (n < 1) return false; + while(n > 1) { + if (n % 3 != 0) return false; + n /= 3; + } + return true; + } +} +``` + diff --git "a/Java/alg/lc/33.\346\220\234\347\264\242\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204.md" "b/Java/alg/lc/33.\346\220\234\347\264\242\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204.md" new file mode 100644 index 00000000..3a8f3fea --- /dev/null +++ "b/Java/alg/lc/33.\346\220\234\347\264\242\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204.md" @@ -0,0 +1,112 @@ +# 33. 搜索旋转排序数组 + +[url](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/) + +## 题目 + +整数数组 nums 按升序排列,数组中的值 互不相同 。 + +在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。 + +给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的索引,否则返回 -1 。 + + +``` +输入:nums = [4,5,6,7,0,1,2], target = 0 +输出:4 +输入:nums = [4,5,6,7,0,1,2], target = 3 +输出:-1 +输入:nums = [1], target = 0 +输出:-1 +``` + +## 方法 + + +## code + +### js + +```js +let search = (nums, target) => { + if (nums === null || nums.length === 0) + return -1; + let l = 0, r = nums.length - 1; + while (l <= r) { + let m = l + Math.floor((r - l) / 2); + if (nums[m] === target) + return m; + else if (nums[m] < nums[r]){ + if (nums[m] < target && target <= nums[r]) + l = m + 1; + else + r = m - 1; + } else { + if (nums[l] <= target && target < nums[m]) + r = m - 1; + else + l = m + 1; + } + } + return -1; +}; +``` + +### go + +```go +func search2(nums []int, target int) int { + l := len(nums) + left, right := 0, l - 1 + for left <= right { + m := (left + right) / 2 + if nums[m] == target { + return m + } else if nums[m] < nums[right] { + if nums[m] < target && target <= nums[right] { + left = m + 1 + } else { + right = m - 1 + } + } else { + if nums[left] <= target && target < nums[m] { + right = m - 1 + } else { + left = m + 1 + } + } + } + return -1 +} +``` + + + +### java + +```java +class Solution { + public int search(int[] nums, int target) { + int len = nums.length; + int left = 0, right = len - 1; + while (left <= right) { + int mid = (left + right) / 2; + if (nums[mid] == target) + return mid; + else if(nums[mid] < nums[right]) { + if (nums[mid] < target && target <= nums[right]) + left = mid + 1; + else + right = mid - 1; + } else { + if (nums[left] <= target && target < nums[mid]) + right = mid - 1; + else + left = mid + 1; + } + } + return - 1; + } +} +``` + diff --git "a/Java/alg/lc/34.\345\234\250\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\346\237\245\346\211\276\345\205\203\347\264\240\347\232\204\347\254\254\344\270\200\344\270\252\345\222\214\346\234\200\345\220\216\344\270\200\344\270\252\344\275\215\347\275\256.md" "b/Java/alg/lc/34.\345\234\250\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\346\237\245\346\211\276\345\205\203\347\264\240\347\232\204\347\254\254\344\270\200\344\270\252\345\222\214\346\234\200\345\220\216\344\270\200\344\270\252\344\275\215\347\275\256.md" new file mode 100644 index 00000000..92f2a3c5 --- /dev/null +++ "b/Java/alg/lc/34.\345\234\250\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\346\237\245\346\211\276\345\205\203\347\264\240\347\232\204\347\254\254\344\270\200\344\270\252\345\222\214\346\234\200\345\220\216\344\270\200\344\270\252\344\275\215\347\275\256.md" @@ -0,0 +1,110 @@ +# 34. 在排序数组中查找元素的第一个和最后一个位置 + +[url](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/) + +## 题目 + +给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 + +如果数组中不存在目标值 target,返回 [-1, -1]。 + + +``` +输入:nums = [5,7,7,8,8,10], target = 8 +输出:[3,4] +输入:nums = [5,7,7,8,8,10], target = 6 +输出:[-1,-1] +输入:nums = [], target = 0 +输出:[-1,-1] +``` + +## 方法 + + +## code + +### js + +```js +let searchRange = (nums, target) => { + let findFirst = (nums, target) => { + let l = 0, h = nums.length; // h的初始值和往常不一样 + while (l < h) { + let m = l + Math.floor((h - l) / 2); + if (nums[m] >= target) + h = m; + else + l = m + 1; + } + return l; + }; + + let first = findFirst(nums, target); + let last = findFirst(nums, target + 1) - 1; + if (first === nums.length || nums[first] !== target) + return [-1, -1]; + else + return [first, Math.max(first, last)]; +}; +``` + +### go + +```go +func searchRange(nums []int, target int) []int { + findFirst := func (nums []int, target int) int { + l, h := 0, len(nums) + for l < h { + m := l + (h-l)/2 + if nums[m] >= target { + h = m + } else { + l = m + 1 + } + } + return l + } + Max := func(a int, b int) int { + if a < b { + return b + } else { + return a + } + } + first := findFirst(nums, target) + last := findFirst(nums, target + 1) - 1 + if first == len(nums) || nums[first] !=target { + return []int{-1, -1} + } else { + return []int{first, Max(first, last)} + } +} +``` + + + +### java + +```java +class Solution { + public int[] searchRange(int[] nums, int target) { + int first = findFirst(nums, target); + int last = findFirst(nums, target + 1) - 1; + if (first == nums.length || nums[first] != target) { + return new int[] {-1, -1}; + } else { + return new int[]{first, Math.max(first, last)}; + } + } + private int findFirst(int[] nums, int target) { + int l = 0, h = nums.length; // h 的初始值和往常不一样 + while (l < h) { + int m = l + ( h - l) / 2; + if (nums[m] >= target) h = m; + else l = m + 1; + } + return l; + } +} +``` + diff --git "a/Java/alg/lc/344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" "b/Java/alg/lc/344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" new file mode 100644 index 00000000..ae7077c7 --- /dev/null +++ "b/Java/alg/lc/344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,82 @@ +# 344. 反转字符串 + +[url](https://leetcode-cn.com/problems/reverse-string/) + + +## 题目 + +编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 `char[]` 的形式给出。 + +不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 `O(1) `的额外空间解决这一问题。 + +你可以假设数组中的所有字符都是 `ASCII` 码表中的可打印字符。 + + + +``` +输入:["h","e","l","l","o"] +输出:["o","l","l","e","h"] +输入:["H","a","n","n","a","h"] +输出:["h","a","n","n","a","H"] +``` + + +## 方法 + +## code + +### js + +```js +let reverseString = s => { + let swap = (s, i, j) => { + let temp = s[i]; + s[i] = s[j]; + s[j] = temp; + }; + let p1 = 0, p2 = s.length - 1; + while (p1 < p2) { + swap(s, p1++, p2--); + } + return s; +}; +console.log(reverseString(["h","e","l","l","o"])) +``` + +### go + +```go +func reverseString(s []byte) { + swap := func (s []byte, i, j int) { + temp := s[i] + s[i] = s[j] + s[j] = temp + } + p1, p2 := 0, len(s) - 1 + for p1 < p2 { + swap(s, p1, p2) + p1++ + p2-- + } + fmt.Println(s) +} +``` + +### java + +```java +class Solution { + public void reverseString(char[] s) { + int p1 = 0, p2 = s.length - 1; + while(p1 < p2 ){ + swap(s, p1++, p2--); + } + } + public void swap(char[] s, int i, int j) { + char temp = s[i]; + s[i] = s[j]; + s[j] = temp; + } +} +``` + diff --git "a/Java/alg/lc/35.\346\220\234\347\264\242\346\217\222\345\205\245\344\275\215\347\275\256.md" "b/Java/alg/lc/35.\346\220\234\347\264\242\346\217\222\345\205\245\344\275\215\347\275\256.md" new file mode 100644 index 00000000..9541f7e9 --- /dev/null +++ "b/Java/alg/lc/35.\346\220\234\347\264\242\346\217\222\345\205\245\344\275\215\347\275\256.md" @@ -0,0 +1,127 @@ +# 35. 搜索插入位置 + +[url](https://leetcode-cn.com/problems/search-insert-position/) + +## 题目 + +给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 + +你可以假设数组中无重复元素。 + +``` +输入: [1,3,5,6], 5 +输出: 2 + +输入: [1,3,5,6], 2 +输出: 1 + +输入: [1,3,5,6], 7 +输出: 4 + +输入: [1,3,5,6], 0 +输出: 0 +``` + +## 方法 + +二分法 + +二分法的思想,前提是已经排序,其实就是每次找到中间的索引的值和目标值判断,三种情况: +1. 如果相等,则返回该索引 +2. 如果该值小于目标值,说明目标值肯定在该值的右边区域,那么我就把左边区域扔掉,我只要右边区域。 +3. 如果该值大于目标值,说明目标值肯定在该值的左边区域,那么我就把右边区域扔掉,我只要左边区域。 + +但是这道题,还有一个小问题,如果该值不存在,返回它将会被按顺序插入的位置。 +所以,有两种情况: +1. 如果`l==0 && r < 0`,类似于这种情况:`[3, 5, 6] and target = 2`,则返回索引0 +2. 否则,返回`(l + r) / 2` + 1,类似于这种情况:`[3, 5, 6] and target = 4` + + + +## code + +### js + +```js +let searchInsert = (nums, target) => { + if (nums === undefined || nums.length === 0) + return -1; + let l = 0, r = nums.length - 1; + while (l <= r) { + let m = (l + (r - l) / 2) | 0; + if (nums[m] === target) + return m; + else if (nums[m] < target) { + l = m + 1; + } else { + r = m - 1; + } + } + // 注意边界 + if (r < 0 && l === 0) + return (((l + r) % 2) | 0) + 1; + else + return (((l + r) / 2) | 0) + 1; +} + +console.log(searchInsert([1, 3, 5, 6], 5)); +console.log(searchInsert([1, 3, 5, 6], 2)); +console.log(searchInsert([1, 3, 5, 6], 7)); +console.log(searchInsert([1, 3, 5, 6], 0)); +``` + +### go + +```go +func searchInsert(nums []int, target int) int { + if nums == nil || len(nums) == 0 { + return -1 + } + l, r := 0, len(nums) - 1 + for l <= r { + m := l + (r - l) / 2 + if nums[m] == target { + return m + } else if nums[m] < target { + l = m + 1 + } else { + r = m - 1 + } + } + // 注意边界 + if r < 0 && l == 0 { + return (l + r) % 2 + 1 + } else { + return (l + r) / 2 + 1 + } +} +``` + + + +### java + +```java +class Solution { + public int searchInsert(int[] nums, int target) { + if (nums == null || nums.length == 0) + return -1; + int l = 0, r = nums.length - 1; + while (l <= r) { + int m = l + (r - l) / 2; + if (nums[m] == target) + return m; + else if (nums[m] < target) + l = m + 1; + else + r = m - 1; + } + // 注意边界 + if (r < 0 && l == 0) + return (l + r) % 2 + 1; + else + return (l + r) / 2 + 1; + } +} +``` + diff --git "a/Java/alg/lc/350.\344\270\244\344\270\252\346\225\260\347\273\204\347\232\204\344\272\244\351\233\2062.md" "b/Java/alg/lc/350.\344\270\244\344\270\252\346\225\260\347\273\204\347\232\204\344\272\244\351\233\2062.md" new file mode 100644 index 00000000..2a4c2d91 --- /dev/null +++ "b/Java/alg/lc/350.\344\270\244\344\270\252\346\225\260\347\273\204\347\232\204\344\272\244\351\233\2062.md" @@ -0,0 +1,89 @@ +# 350. 两个数组的交集 II + +[url](https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/) + + +## 题目 + +给定两个数组,编写一个函数来计算它们的交集。 + + + +``` +输入:nums1 = [1,2,2,1], nums2 = [2,2] +输出:[2,2] +输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] +输出:[4,9] +``` + + +## 方法 + +## code + +### js + +```js +let intersect = (nums1, nums2) => { + let list = []; + for (let value of nums2) { + if (nums1.includes(value)) { + list.push(value); + nums1.splice(nums1.findIndex(item => item === value), 1); + } + } + return list; +}; +console.log(intersect([1,2,2,1], [2, 2])) +console.log(intersect([4,9,5], [9,4,9,8,4])) +``` + +### go + +```go +func intersect(nums1 []int, nums2 []int) []int { + containInt := func (nums []int, item int) (int, bool) { + for i, num := range nums { + if num == item { + return i, true + } + } + return 0, false + } + var res []int + for _, value := range nums2 { + if i, ok := containInt(nums1, value); ok { + res = append(res, value) + nums1 = append(nums1[:i], nums1[i+1:]...) + } + } + return res +} +``` + +### java + +```java +class Solution { + public int[] intersect(int[] nums1, int[] nums2) { + ArrayList list1 = new ArrayList<>(); + for (int num : nums1) { + list1.add(num); + } + ArrayList list2 = new ArrayList<>(); + for (int num : nums2) { + if (list1.contains(num)) { + list2.add(num); + list1.remove(num); + } + } + return list2.stream().mapToInt(Integer::valueOf).toArray(); + // int[] ans = new int[list2.size()]; + // for (int i = 0; i < list2.size(); i++) { + // ans[i] = list2.get(i); + // } + // return ans; + } +} +``` + diff --git "a/Java/alg/lc/36.\346\234\211\346\225\210\347\232\204\346\225\260\347\213\254.md" "b/Java/alg/lc/36.\346\234\211\346\225\210\347\232\204\346\225\260\347\213\254.md" new file mode 100644 index 00000000..86815835 --- /dev/null +++ "b/Java/alg/lc/36.\346\234\211\346\225\210\347\232\204\346\225\260\347\213\254.md" @@ -0,0 +1,120 @@ +# 36. 有效的数独 + +[url](https://leetcode-cn.com/problems/valid-sudoku/) + +## 题目 + +判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。 + +- 数字 1-9 在每一行只能出现一次。 +- 数字 1-9 在每一列只能出现一次。 +- 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 + +![lc-shudu-9RLJZZ](http://imgs.dreamcat.ink/uPic/lc-shudu-9RLJZZ.png) + + + +## 方法 + + +## code + +### js + +```js +let isValidSudoku = board => { + // 记录某行,某位数字是否已经被摆放 + let row = Array(9).fill(false); + // 记录某列,某位数字是否已经被摆放 + let col = Array(9).fill(false); + // 记录某3x3宫格内,某位数字是否已经被摆放 + let block = Array(9).fill(false); + // 初始化 js的二维数组真麻烦 + row.forEach((val, idx,arr) => { + arr[idx] = Array(9).fill(false); + col[idx] = Array(9).fill(false); + block[idx] = Array(9).fill(false); + }); + for (let i = 0; i < 9; i++) { + for (let j = 0; j < 9; j++) { + if (board[i][j] !== ".") { + let num = board[i][j] - '1'; + let blockIdx = Math.floor(i / 3) * 3 + Math.floor(j / 3); + if (row[i][num] || col[j][num] || block[blockIdx][num]){ + return false; + } + else { + row[i][num] = true; + col[j][num] = true; + block[blockIdx][num] = true; + } + } + } + } + + return true; +} +``` + +### go + +```go +func isValidSudoku(board [][]byte) bool { + row := make([][]byte, 9) + col := make([][]byte, 9) + block := make([][]byte, 9) + for i := 0; i < 9; i++ { + for j := 0; j < 9; j++ { + if board[i][j] != '.' { + num := board[i][j] - '1' + blockIdx := i / 3 * 3 + j / 3 + if row[i][num] == 0 || col[j][num] == 0 || block[blockIdx][num] == 0 { + return false + } else { + row[i][num] = 0 + col[j][num] = 0 + block[blockIdx][num] = 0 + } + } + } + } + return true +} +``` + + + +### java + +```java +class Solution { + public boolean isValidSudoku(char[][] board) { + // 记录某行,某位数字是否已经被摆放 + boolean[][] row = new boolean[9][9]; + // 记录某列,某位数字是否已经被摆放 + boolean[][] col = new boolean[9][9]; + // 记录某3x3宫格内,某位数字是否已经被摆放 + boolean[][] block = new boolean[9][9]; + + for (int i = 0; i < 9; i++) { + for (int j = 0; j < 9; j++) { + if (board[i][j] != '.') { + int num = board[i][j] - '1'; + int blockIdx = i / 3 * 3 + j / 3; + System.out.println(i + " " + j + " " + blockIdx + " " + num); + System.out.println(row[i][num] + " " + col[j][num] + " " + block[blockIdx][num]); + if (row[i][num] || col[j][num] || block[blockIdx][num]) { + return false; + } else { + row[i][num] = true; + col[j][num] = true; + block[blockIdx][num] = true; + } + } + } + } + return true; + } +} +``` + diff --git "a/Java/alg/lc/367.\346\234\211\346\225\210\347\232\204\345\256\214\345\205\250\345\271\263\346\226\271\346\225\260.md" "b/Java/alg/lc/367.\346\234\211\346\225\210\347\232\204\345\256\214\345\205\250\345\271\263\346\226\271\346\225\260.md" new file mode 100644 index 00000000..244b52f1 --- /dev/null +++ "b/Java/alg/lc/367.\346\234\211\346\225\210\347\232\204\345\256\214\345\205\250\345\271\263\346\226\271\346\225\260.md" @@ -0,0 +1,70 @@ +# 367. 有效的完全平方数 + + +[url](https://leetcode-cn.com/problems/valid-perfect-square/) + + +## 题目 + +给定一个正整数 `num`,编写一个函数,如果 `num` 是一个完全平方数,则返回 `True`,否则返回 `False`。 + +说明:不要使用任何内置的库函数,如  `sqrt`。 + + + + +``` +输入:16 +输出:True +输入:14 +输出:False +``` + + +## 方法 + +## code + +### js + +```js +let isPerfectSquare = num => { + let subNum = 1; + while (num > 0) { + num -= subNum; + subNum += 2; + } + return num === 0; +}; +console.log(isPerfectSquare(16)) +console.log(isPerfectSquare(14)) +``` + +### go + +```go +func isPerfectSquare(num int) bool { + subNum := 1 + for num > 0 { + num -= subNum + subNum += 2 + } + return num == 0 +} +``` + +### java + +```java +class Solution { + public boolean isPerfectSquare(int num) { + int subNum = 1; + while (num > 0) { + num -= subNum; + subNum += 2; + } + return num == 0; + } +} +``` + diff --git "a/Java/alg/lc/371.\344\270\244\346\225\264\346\225\260\344\271\213\345\222\214.md" "b/Java/alg/lc/371.\344\270\244\346\225\264\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 00000000..bc4e735c --- /dev/null +++ "b/Java/alg/lc/371.\344\270\244\346\225\264\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,57 @@ +# 371. 两整数之和 + + +[url](https://leetcode-cn.com/problems/sum-of-two-integers/) + + +## 题目 + +不使用运算符 `+` 和 `-` ​​​​​​​,计算两整数 `​​​​​​​a` 、`b `​​​​​​​之和。 + + + + +``` +输入: a = 1, b = 2 +输出: 3 +输入: a = -2, b = 3 +输出: 1 +``` + + +## 方法 + +## code + +### js + +```js +let getSum = (a, b) => { + return b === 0 ? a : getSum((a ^ b), (a & b) << 1); +}; +console.log(getSum(1, 2)); +console.log(getSum(-2, 3)); +``` + +### go + +```go +func getSum(a int, b int) int { + if b == 0 { + return a + } else { + return getSum(a ^ b, (a & b) << 1) + } +} +``` + +### java + +```java +class Solution { + public int getSum(int a, int b) { + return b == 0 ? a : getSum((a ^ b), (a & b) << 1); + } +} +``` + diff --git "a/Java/alg/lc/387.\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\347\254\254\344\270\200\344\270\252\345\224\257\344\270\200\345\255\227\347\254\246.md" "b/Java/alg/lc/387.\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\347\254\254\344\270\200\344\270\252\345\224\257\344\270\200\345\255\227\347\254\246.md" new file mode 100644 index 00000000..0e143ee3 --- /dev/null +++ "b/Java/alg/lc/387.\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\347\254\254\344\270\200\344\270\252\345\224\257\344\270\200\345\255\227\347\254\246.md" @@ -0,0 +1,86 @@ +# 387. 字符串中的第一个唯一字符 + + +[url](https://leetcode-cn.com/problems/first-unique-character-in-a-string/) + + +## 题目 + +给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。 + + + + +``` +s = "leetcode" +返回 0 +s = "loveleetcode" +返回 2 +``` + + +## 方法 + +## code +最容易想到的就是哈希,速战速决 + +### js + +```js +let firstUniqChar = s => { + let map = new Map(); + s = s.split(""); + s.forEach(c => { + map.has(c) ? map.set(c, map.get(c) + 1) : map.set(c, 1); + }); + for (const [i, v] of s.entries()) { + if (map.get(v) === 1) { + return i; + } + } + return -1; +}; +console.log(firstUniqChar("leetcode")) +console.log(firstUniqChar("loveleetcode")) +``` + +### go + +```go +func firstUniqChar(s string) int { + m := make(map[rune]int, len(s)) + for _, c := range s { + if count, ok := m[c]; ok { + m[c] = count + 1 + } else { + m[c] = 1 + } + } + for i, c := range s { + if m[c] == 1 { + return i + } + } + return -1 +} +``` + +### java + +```java +class Solution { + public int firstUniqChar(String s) { + HashMap map = new HashMap<>(); + for (char c : s.toCharArray()){ + map.put(c, map.getOrDefault(c, 0) + 1); + } + for (int i = 0; i < s.length(); i++) { + if(map.get(s.charAt(i)) == 1) { + return i; + } + } + return -1; + } +} +``` + diff --git "a/Java/alg/lc/39.\347\273\204\345\220\210\346\200\273\345\222\214.md" "b/Java/alg/lc/39.\347\273\204\345\220\210\346\200\273\345\222\214.md" new file mode 100644 index 00000000..fdfe760d --- /dev/null +++ "b/Java/alg/lc/39.\347\273\204\345\220\210\346\200\273\345\222\214.md" @@ -0,0 +1,96 @@ +# 39. 组合总和 + +[url](https://leetcode-cn.com/problems/combination-sum/) + +## 题目 + +给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 + +candidates 中的数字可以无限制重复被选取。 + +## 方法 + + +## code + +### js + +```js +let combinationSum = (candidates, target) => { + let dfs = (nums, target, start, list) => { + if (target === 0) { + ret.push(list.slice()); + return; + } + for (let i = start; i < nums.length; i++) { + if (nums[i] <= target) { + list.push(nums[i]); + dfs(nums, target - nums[i], i, list); + list.pop(); + } + } + } + let ret = [] + if (candidates === null || candidates.length === 0) + return ret; + dfs(candidates, target, 0, []); + return ret; +} +``` + +### go + +```go +func combinationSum(candidates []int, target int) [][]int { + var res [][]int + if len(candidates) == 0 { + return res + } + var dfs func(nums []int, target int, start int, list []int) + dfs = func(nums []int, target int, start int, list []int) { + if target == 0 { + res = append(res, append([]int{}, list...)) + return + } + for i := start; i < len(nums); i++ { + if nums[i] <= target { + list = append(list, nums[i]) + dfs(nums, target - nums[i], i, list) + list = list[0:len(list) - 1] + } + } + } + dfs(candidates, target, 0, []int{}) + return res +} +``` + + + +### java + +```java +class Solution { + List> ret = new ArrayList<>(); + public List> combinationSum(int[] candidates, int target) { + if (candidates == null || candidates.length == 0) + return ret; + dfs(candidates, target, 0, new ArrayList()); + return ret; + } + public void dfs(int[] nums, int target, int start, ArrayList list){ + if (target == 0) { + ret.add(new ArrayList(list)); + return; + } + for (int i = start; i < nums.length; i++){ + if (nums[i] <= target) { + list.add(nums[i]); + dfs(nums, target - nums[i], i, list); + list.remove(list.size() - 1); + } + } + } +} +``` + diff --git "a/Java/alg/lc/392.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" "b/Java/alg/lc/392.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" new file mode 100644 index 00000000..440973d8 --- /dev/null +++ "b/Java/alg/lc/392.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" @@ -0,0 +1,74 @@ +# 392. 判断子序列 + + +[url](https://leetcode-cn.com/problems/is-subsequence/) + + +## 题目 +给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 + +字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。 + + +``` +输入:s = "abc", t = "ahbgdc" +输出:true +输入:s = "axc", t = "ahbgdc" +输出:false +``` + + +## 方法 + +循环计算索引和子串 + +## code + +### js + +```js +let isSubsequence = (s, t) => { + let idx = -1; + for (const value of s) { + idx = t.indexOf(value, idx + 1); + if (idx === -1) + return false; + } + return true; +} +console.log(isSubsequence("abc", "ahbqdc")) +console.log(isSubsequence("axc", "ahbqdc")) +``` + +### go + +```go +func isSubsequence(s string, t string) bool { + idx := -1 + for _, c := range s { + tmp := t[idx + 1:] + idx = strings.IndexRune(tmp, c) + if idx == -1 { + return false + } + } + return true +} +``` + +### java + +```java +class Solution { + public boolean isSubsequence(String s, String t) { + // 这里用到了String到indexof + int inx = -1; + for (char c : s.toCharArray()) { + inx = t.indexOf(c, inx + 1); + if (inx == -1) return false; + } + return true; + } +} +``` + diff --git "a/Java/alg/lc/40.\347\273\204\345\220\210\346\200\273\345\222\2142.md" "b/Java/alg/lc/40.\347\273\204\345\220\210\346\200\273\345\222\2142.md" new file mode 100644 index 00000000..0c31c153 --- /dev/null +++ "b/Java/alg/lc/40.\347\273\204\345\220\210\346\200\273\345\222\2142.md" @@ -0,0 +1,125 @@ +# 40. 组合总和 II + +[url](https://leetcode-cn.com/problems/combination-sum-ii/) + +## 题目 + +给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 + +candidates 中的每个数字在每个组合中只能使用一次。 + + +## 方法 + + +## code + +### js + +```js +let combinationSum2 = (candidates, target) => { + let dfs = (nums, target, start, list) => { + if (target === 0) { + ret.push(list.slice()) + return + } + for (let i = start; i < nums.length; i++) { + if (marked[i]) { + continue + } + if (i !== 0 && nums[i] === nums[i - 1] && !marked[i - 1]) { + continue + } + if (nums[i] <= target) { + list.push(nums[i]) + marked[i] = true + dfs(nums, target-nums[i], i, list) + marked[i] = false + list.pop() + } + } + } + let ret = [] + let marked = Array(candidates.length).fill(false) + candidates.sort((a, b) => {return a - b}) + dfs(candidates, target, 0, []) + return ret +} +``` + +### go + +```go +func combinationSum2(candidates []int, target int) [][]int { + var res [][]int + if len(candidates) == 0 { + return res + } + marked := make([]bool, len(candidates)) + var dfs func(nums []int, target int, start int, list[] int) + dfs = func(nums []int, target int, start int, list []int) { + if target == 0 { + res = append(res, append([]int{}, list...)) + return + } + for i := start; i < len(nums); i++ { + if marked[i] { + continue + } + if i != 0 && nums[i] == nums[i-1] && !marked[i-1] { + continue + } + if nums[i] <= target { + list = append(list, nums[i]) + marked[i] = true + dfs(nums, target - nums[i], i, list) + marked[i] = false + list = list[0:len(list) - 1] + } + } + } + sort.Ints(candidates) + dfs(candidates, target, 0, []int{}) + return res +} +``` + + + +### java + +```java +class Solution { + public List> combinationSum2(int[] candidates, int target) { + List> combinations = new ArrayList<>(); + Arrays.sort(candidates); + backtracking(new ArrayList<>(), combinations, new boolean[candidates.length], 0, target, candidates); + return combinations; + } + + private void backtracking(List tempCombination, List> combinations, + boolean[] hasVisited, int start, int target, final int[] candidates) { + + if (target == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + for (int i = start; i < candidates.length; i++) { + if (hasVisited[i]) + continue; + if (i != 0 && candidates[i] == candidates[i - 1] && !hasVisited[i - 1]) { + continue; + } + if (candidates[i] <= target) { + tempCombination.add(candidates[i]); + hasVisited[i] = true; + backtracking(tempCombination, combinations, hasVisited, i, target - candidates[i], candidates); + hasVisited[i] = false; + tempCombination.remove(tempCombination.size() - 1); + } + } + } + +} +``` + diff --git "a/Java/alg/lc/409.\346\234\200\351\225\277\345\233\236\346\226\207\344\270\262.md" "b/Java/alg/lc/409.\346\234\200\351\225\277\345\233\236\346\226\207\344\270\262.md" new file mode 100644 index 00000000..63e0ab17 --- /dev/null +++ "b/Java/alg/lc/409.\346\234\200\351\225\277\345\233\236\346\226\207\344\270\262.md" @@ -0,0 +1,83 @@ +# 409. 最长回文串 + + +[url](https://leetcode-cn.com/problems/longest-palindrome/) + + +## 题目 +给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。 + +在构造过程中,请注意区分大小写。比如 "Aa" 不能当做一个回文字符串。 + + +``` +输入: +"abccccdd" +输出: +7 +解释: +我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。 +``` + + +## 方法 +提示:分析回文串的特征 + +## code + +### js + +```js +let longestPalindrome = s => { + let cnts = Array(256).fill(0); + s = s.split(""); + s.forEach(c => cnts[c.charCodeAt(0)]++); + let palindrome = 0; + cnts.forEach(cnt => palindrome += Math.floor(cnt / 2) * 2); + if (palindrome < s.length) + palindrome++; + return palindrome; +}; +console.log(longestPalindrome("abccccdd")) +``` + +### go + +```go +func longestPalindrome(s string) int { + cnts := make([]int, 256) + for _, c := range s { + cnts[c]++ + } + palindrome := 0 + for _, cnt := range cnts { + palindrome += (cnt / 2) * 2 + } + if palindrome < len(s) { + palindrome++ + } + return palindrome +} +``` + +### java + +```java +class Solution { + public int longestPalindrome(String s) { + int[] cnts = new int[256]; + for (char c : s.toCharArray()) { + cnts[c]++; + } + int palindrome = 0; + for (int cnt : cnts) { + palindrome += (cnt / 2) * 2; + } + if (palindrome < s.length()) { + palindrome++; + } + return palindrome; + } +} +``` + diff --git "a/Java/alg/lc/414.\347\254\254\344\270\211\345\244\247\347\232\204\346\225\260.md" "b/Java/alg/lc/414.\347\254\254\344\270\211\345\244\247\347\232\204\346\225\260.md" new file mode 100644 index 00000000..7bac766a --- /dev/null +++ "b/Java/alg/lc/414.\347\254\254\344\270\211\345\244\247\347\232\204\346\225\260.md" @@ -0,0 +1,117 @@ +# 414. 第三大的数 + + +[url](https://leetcode-cn.com/problems/third-maximum-number/) + + +## 题目 +给你一个非空数组,返回此数组中 第三大的数 。如果不存在,则返回数组中最大的数。 + + +``` +输入:[3, 2, 1] +输出:1 +解释:第三大的数是 1 。 +输入:[1, 2] +输出:2 +解释:第三大的数不存在, 所以返回最大的数 2 。 +输入:[2, 2, 3, 1] +输出:1 +解释:注意,要求返回第三大的数,是指第三大且唯一出现的数。 +存在两个值为2的数,它们都排第二。 +``` + + +## 方法 + + +## code + +### js + +```js +let thirdMax = nums => { + const minval = -Infinity; + let maxFirst = minval, maxSecond = minval, maxThird = minval; + for (let value of nums) { + if (value > maxFirst) { + maxThird = maxSecond; + maxSecond = maxFirst; + maxFirst = value; + } else if (value < maxFirst && value > maxSecond) { + maxThird = maxSecond; + maxSecond = value; + } else if (value < maxSecond && value > maxThird) { + maxThird = value; + } + } + return maxThird === minval ? maxFirst : maxThird; +}; +console.log(thirdMax([3, 2 ,1])); +console.log(thirdMax([ 2 ,1])); +console.log(thirdMax([ 2 ,2 , 3, 1])); +``` + +### go + +```go +func thirdMax(nums []int) int { + const INT_MAX = int(^uint(0) >> 1) + const INT_MIN = ^INT_MAX + maxFirst, maxSecond, maxThird := INT_MIN, INT_MIN, INT_MIN + for _, num := range nums { + if num > maxFirst { + maxThird = maxSecond + maxSecond = maxFirst + maxFirst = num + } else if num < maxFirst && num > maxSecond { + maxThird = maxSecond + maxSecond = num + } else if num < maxSecond && num > maxThird { + maxThird = num + } + } + if maxThird == INT_MIN { + return maxFirst + } else { + return maxThird + } +} +``` + +### java + +```java +class Solution { + public int thirdMax(int[] nums) { + // 该方法堆 + // TreeSet tset = new TreeSet<>(); + // for(int n:nums) { + // tset.add(n); + // } + // if(tset.size() < 3) { + // return tset.last(); + // } else{ + // tset.pollLast(); + // tset.pollLast(); + // return tset.pollLast(); + // } + long minVal = (long)Integer.MIN_VALUE - 1; + long maxFirst = minVal, maxSecond = minVal, maxThird = minVal; + for(int num : nums) { + if(num > maxFirst) { + maxThird = maxSecond; + maxSecond = maxFirst; + maxFirst = num; + } else if(num < maxFirst && num > maxSecond) { + maxThird = maxSecond; + maxSecond = num; + } else if(num < maxSecond && num > maxThird) { + maxThird = num; + } + } + return (int)(maxThird == minVal ? maxFirst : maxThird); + } +} +``` + diff --git "a/Java/alg/lc/415.\345\255\227\347\254\246\344\270\262\347\233\270\345\212\240.md" "b/Java/alg/lc/415.\345\255\227\347\254\246\344\270\262\347\233\270\345\212\240.md" new file mode 100644 index 00000000..206f6da5 --- /dev/null +++ "b/Java/alg/lc/415.\345\255\227\347\254\246\344\270\262\347\233\270\345\212\240.md" @@ -0,0 +1,90 @@ +# 415. 字符串相加 + + +[url](https://leetcode-cn.com/problems/add-strings/) + + +## 题目 +给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。 + + +## 方法 + + +## code + +### js + +```js +let addStrings = (num1, num2) => { + let res = '' + let carry = 0, i = num1.length - 1, j = num2.length - 1 + while (carry === 1 || i >= 0 || j >= 0) { + let x = i < 0 ? 0 : num1[i--] - '0' + let y = j < 0 ? 0 : num2[j--] - '0' + res += Math.floor((x + y + carry) % 10) + carry = Math.floor((x + y + carry) / 10) + } + return res.split("").reverse().toString() +} + +console.log(addStrings('123', '123')) +``` + +### go + +```go +func addStrings(num1 string, num2 string) string { + reverseStr := func (s string) string { + runes := []rune(s) + i, j := 0, len(runes) - 1 + for i < j { + runes[i], runes[j] = runes[j], runes[i] + i++ + j-- + } + return string(runes) + } + x := 0 + y := 0 + carry := 0 + i, j := len(num1) - 1, len(num2) - 1 + var res string + for carry == 1 || i >= 0 || j >= 0 { + if i < 0 { + x = 0 + } else { + x, _ = strconv.Atoi(string(num1[i])) + } + if j < 0 { + y = 0 + } else { + y, _ = strconv.Atoi(string(num2[j])) + } + res += strconv.Itoa((x + y + carry) % 10) + carry = (x + y + carry) / 10 + i-- + j-- + } + return reverseStr(res) +} +``` + +### java + +```java +class Solution { + public String addStrings(String num1, String num2) { + StringBuilder str = new StringBuilder(); + int carry = 0, i = num1.length() - 1, j = num2.length() - 1; + while (carry == 1 || i >= 0 || j >= 0) { + int x = i < 0 ? 0 : num1.charAt(i--) - '0'; + int y = j < 0 ? 0 : num2.charAt(j--) - '0'; + str.append((x + y + carry) % 10); + carry = (x + y + carry) / 10; + } + return str.reverse().toString(); + } +} +``` + diff --git "a/Java/alg/lc/43.\345\255\227\347\254\246\344\270\262\347\233\270\344\271\230.md" "b/Java/alg/lc/43.\345\255\227\347\254\246\344\270\262\347\233\270\344\271\230.md" new file mode 100644 index 00000000..bc277418 --- /dev/null +++ "b/Java/alg/lc/43.\345\255\227\347\254\246\344\270\262\347\233\270\344\271\230.md" @@ -0,0 +1,101 @@ +# 43. 字符串相乘 + +[url](https://leetcode-cn.com/problems/multiply-strings/) + +## 题目 + +给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。 + +``` +输入: num1 = "2", num2 = "3" +输出: "6" +输入: num1 = "123", num2 = "456" +输出: "56088" +``` + +## 方法 + + +## code + +### js + +```js +let multiply = (num1, num2) => { + len1 = num1.length; + len2 = num2.length; + if (len1 === 0 || len2 === 0) return "0"; + let mul = Array(len1 + len2).fill(0); + for (let i = len1 - 1; i >= 0; i--) { + for (let j = len2 - 1; j >= 0; j--) { + let n = (num1[i] - '0') * (num2[j] - '0') + mul[i + j + 1]; + mul[i + j + 1] = n % 10; + mul[i + j] += Math.floor(n / 10); + } + } + let sb = "" + let i = 0; + while(i < len1 + len2 - 1 && mul[i] === 0) i++; + while(i < len1 + len2) sb += mul[i++]; + return sb +} +``` + +### go + +```go +func multiply(num1 string, num2 string) string { + len1 := len(num1) + len2 := len(num2) + if len1 == 0 || len2 == 0 { + return "0" + } + mul := make([]byte, len1 + len2) + for i := len1 - 1; i >= 0; i-- { + for j := len2 - 1; j >= 0; j-- { + n := (num1[i] - byte('0')) * (num2[j] - byte('0')) + mul[i + j + 1] + mul[i + j + 1] = n % 10 + mul[i + j] += n / 10 + } + } + sb := "" + i := 0 + for i < len1+len2-1 && mul[i] == 0 { + i++ + } + for i < len1+len2 { + sb += strconv.Itoa(int(mul[i])) + //sb += string([]byte{mul[i] + byte('0')}) + i++ + } + return sb +} +``` + + + +### java + +```java +class Solution { + public String multiply(String num1, String num2) { + int len1 = num1.length(); + int len2 = num2.length(); + if (len1 == 0 || len2 == 0) return "0"; + int[] mul = new int[len1 + len2]; + for (int i = len1 - 1; i >= 0; i--){ + for (int j = len2 - 1; j >= 0; j--){ + int n = (num1.charAt(i) - '0') * (num2.charAt(j) - '0') + mul[i + j + 1]; + mul[i + j + 1] = n % 10; + mul[i + j] += n / 10; + } + } + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < len1 + len2 - 1 && mul[i] == 0) i++; + while (i < len1 + len2) sb.append(mul[i++]); + return sb.toString(); + } +} +``` + diff --git "a/Java/alg/lc/434.\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\215\225\350\257\215\346\225\260.md" "b/Java/alg/lc/434.\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\215\225\350\257\215\346\225\260.md" new file mode 100644 index 00000000..03e2d1e5 --- /dev/null +++ "b/Java/alg/lc/434.\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\215\225\350\257\215\346\225\260.md" @@ -0,0 +1,68 @@ +# 434. 字符串中的单词数 + + +[url](https://leetcode-cn.com/problems/number-of-segments-in-a-string/) + + +## 题目 +统计字符串中的单词个数,这里的单词指的是连续的不是空格的字符。 + +请注意,你可以假定字符串里不包括任何不可打印的字符。 + +``` +输入: "Hello, my name is John" +输出: 5 +解释: 这里的单词是指连续的不是空格的字符,所以 "Hello," 算作 1 个单词。 +``` + +## 方法 + + +## code + +### js + +```js +let countSegments = s => { + let ss = s.split(" "); + let cnt = 0; + for (let value of ss) { + if (value === " " || value.length === 0) continue; + cnt++; + } + return cnt; +}; +``` + +### go + +```go +func countSegments(s string) int { + ss := strings.Split(s, " ") + cnt := 0 + for _, str := range ss { + if str == " " || len(str) == 0{ + continue + } + cnt++ + } + return cnt +} +``` + +### java + +```java +class Solution { + public int countSegments(String s) { + String[] strs = s.split(" "); + int len = 0; + for(String t : strs) { + if(t.equals(" ") || t.isEmpty()) continue; + len++; + } + return len; + } +} +``` + diff --git "a/Java/alg/lc/455.\345\210\206\345\217\221\351\245\274\345\271\262.md" "b/Java/alg/lc/455.\345\210\206\345\217\221\351\245\274\345\271\262.md" new file mode 100644 index 00000000..58c4e7ae --- /dev/null +++ "b/Java/alg/lc/455.\345\210\206\345\217\221\351\245\274\345\271\262.md" @@ -0,0 +1,98 @@ +# 455. 分发饼干 + + +[url](https://leetcode-cn.com/problems/assign-cookies/) + + +## 题目 +假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 + +对每个孩子 i,都有一个胃口值 `g[i]`,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 `j`,都有一个尺寸 `s[j]` 。如果 `s[j] >= g[i]`,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。 + + +``` +输入: g = [1,2,3], s = [1,1] +输出: 1 +解释: +你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 +虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 +所以你应该输出1。 +输入: g = [1,2], s = [1,2,3] +输出: 2 +解释: +你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 +你拥有的饼干数量和尺寸都足以让所有孩子满足。 +所以你应该输出2. +``` + +## 方法 +排序+贪心 + +## code + +### js + +```js +let findContentChildren = (g, s) => { + if (g.length === 0 || s.length === 0) + return 0; + let gn = g.length, sn = s.length; + // 排序 + g.sort(); + s.sort(); + let gi = 0, si = 0; + while (gi < gn && si < sn) { + if (g[gi] <= s[si]) + gi++; + si++; + } + return gi; +}; +console.log(findContentChildren([1,2,3], [1, 1])); +console.log(findContentChildren([1,2], [1, 2, 3])); +``` + +### go + +```go +func findContentChildren(g []int, s []int) int { + if len(g) == 0 || len(s) == 0 { + return 0 + } + gn, sn := len(g), len(s) + // 排序 + sort.Ints(g) + sort.Ints(s) + gi, si := 0, 0 + for gi < gn && si < sn { + if g[gi] <= s[si] { + gi++ + } + si++ + } + return gi +} +``` + +### java + +```java +class Solution { + public int findContentChildren(int[] g, int[] s) { + if (g.length == 0 || s.length == 0) return 0; + // 长度 + int gn = g.length, sn = s.length; + // 排序 + Arrays.sort(g); + Arrays.sort(s); + // 双指针,贪心 + int gi = 0, si = 0; + while (gi < gn && si < sn) { + if (g[gi] <= s[si]) gi++; + si++; + } + return gi; + } +} +``` + diff --git "a/Java/alg/lc/46.\345\205\250\346\216\222\345\210\227.md" "b/Java/alg/lc/46.\345\205\250\346\216\222\345\210\227.md" new file mode 100644 index 00000000..4c8e6e08 --- /dev/null +++ "b/Java/alg/lc/46.\345\205\250\346\216\222\345\210\227.md" @@ -0,0 +1,114 @@ +# 46. 全排列 + +[url](https://leetcode-cn.com/problems/permutations/) + +## 题目 + +给定一个 没有重复 数字的序列,返回其所有可能的全排列。 + +``` +输入: [1,2,3] +输出: +[ + [1,2,3], + [1,3,2], + [2,1,3], + [2,3,1], + [3,1,2], + [3,2,1] +] +``` + +## 方法 + + +## code + +### js + +```js +let permute = nums => { + let ret = []; + if (nums === null || nums.length === 0) return ret; + let marked = Array(nums.length).fill(false); + let dfs = (nums, list) => { + if (list.length === nums.length) { + ret.push(list.slice()) + return; + } + for (let i = 0; i < nums.length; i++) { + if (marked[i]) continue; + list.push(nums[i]); + marked[i] = true; + dfs(nums, list); + marked[i] = false; + list.pop(); + } + } + dfs(nums, []); + return ret; +} +``` + +### go + +```go +func permute(nums []int) [][]int { + var res [][]int + var dfs func(nums []int, list []int) + if len(nums) == 0 { + return res + } + marked := make([]bool, len(nums)) + dfs = func(nums []int, list []int) { + if len(list) == len(nums) { + res = append(res, append([]int{}, list...)) + return + } + for i := 0; i < len(nums); i++ { + if marked[i] { + continue + } + list = append(list, nums[i]) + marked[i] = true + dfs(nums, list) + marked[i] = false + list = list[0:len(list) - 1] + } + } + dfs(nums, []int{}) + return res +} +``` + + + +### java + +```java +class Solution { + List> ret = new ArrayList<>(); + public List> permute(int[] nums) { + if (nums == null || nums.length == 0) + return ret; + dfs(nums, new boolean[nums.length], new ArrayList()); + return ret; + } + public void dfs(int[] nums, boolean[] marked, ArrayList list){ + if (list.size() == nums.length){ + ret.add(new ArrayList(list)); + return; + } + for (int i = 0; i < nums.length; i++) { + if(marked[i]) + continue; + list.add(nums[i]); + marked[i] = true; + dfs(nums, marked, list); + marked[i] = false; + list.remove(list.size() - 1); + } + } +} +``` + diff --git "a/Java/alg/lc/461.\346\261\211\346\230\216\350\267\235\347\246\273.md" "b/Java/alg/lc/461.\346\261\211\346\230\216\350\267\235\347\246\273.md" new file mode 100644 index 00000000..47c73a83 --- /dev/null +++ "b/Java/alg/lc/461.\346\261\211\346\230\216\350\267\235\347\246\273.md" @@ -0,0 +1,72 @@ +# 461. 汉明距离 + + +[url](https://leetcode-cn.com/problems/hamming-distance/) + + +## 题目 +两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。 + +给出两个整数 `x` 和 `y`,计算它们之间的汉明距离。 + + +``` +输入: x = 1, y = 4 +输出: 2 +解释: +1 (0 0 0 1) +4 (0 1 0 0) + ↑ ↑ +上面的箭头指出了对应二进制位不同的位置。 +``` + +## 方法 +排序+贪心 + +## code + +### js + +```js +let hammingDistance = (x, y) => { + let z = x ^ y; + let cnt = 0; + while (z !== 0) { + z &= (z - 1); + cnt++; + } + return cnt; +}; +console.log(hammingDistance(1, 4)); +``` + +### go + +```go +func hammingDistance(x int, y int) int { + z := x ^ y + cnt := 0 + for z != 0 { + z &= z - 1 + cnt++ + } + return cnt +} +``` + +### java + +```java +class Solution { + public int hammingDistance(int x, int y) { + int z = x ^ y; + int cnt = 0; + while (z != 0) { + z &= (z - 1); + cnt++; + } + return cnt; + } +} +``` + diff --git "a/Java/alg/lc/47.\345\205\250\346\216\222\345\210\2272.md" "b/Java/alg/lc/47.\345\205\250\346\216\222\345\210\2272.md" new file mode 100644 index 00000000..ad24b158 --- /dev/null +++ "b/Java/alg/lc/47.\345\205\250\346\216\222\345\210\2272.md" @@ -0,0 +1,123 @@ +# 47. 全排列 II + +[url](https://leetcode-cn.com/problems/permutations-ii/) + +## 题目 + +给定一个 没有重复 数字的序列,返回其所有可能的全排列。 + +``` +输入: [1,2,3] +输出: +[ + [1,2,3], + [1,3,2], + [2,1,3], + [2,3,1], + [3,1,2], + [3,2,1] +] +``` + +## 方法 + + +## code + +### js + +```js +let permuteUnique = nums => { + let ret = [] + if (nums === null || nums.length === 0) return ret; + let marked = Array(nums.length).fill(false); + let dfs = (nums, list) => { + if (list.length === nums.length) { + ret.push(list.slice()); + return; + } + for (let i = 0; i < nums.length; i++) { + if (marked[i]) continue; + if (i !== 0 && nums[i] === nums[i - 1] && !marked[i - 1]) continue; + list.push(nums[i]); + marked[i] = true; + dfs(nums, list); + marked[i] = false; + list.pop(); + } + } + nums.sort((a, b) => {return a - b}) + dfs(nums, []); + return ret; +} +``` + +### go + +```go +func permuteUnique(nums []int) [][]int { + var res [][]int + if len(nums) == 0 { + return res + } + var dfs func(nums []int, list []int) + marked := make([]bool, len(nums)) + dfs = func(nums []int, list []int) { + if len(nums) == len(list) { + res = append(res, append([]int{}, list...)) + } + for i := 0; i < len(nums); i++ { + if marked[i] { + continue + } + if i != 0 && nums[i] == nums[i-1] && !marked[i-1] { + continue + } + list = append(list, nums[i]) + marked[i] = true + dfs(nums, list) + marked[i] = false + list = list[0:len(list) - 1] + } + } + sort.Ints(nums) + dfs(nums, []int{}) + return res +} +``` + + + +### java + +```java +class Solution { + private List> ret = new ArrayList<>(); + public List> permuteUnique(int[] nums) { + if (nums == null || nums.length == 0) + return ret; + boolean[] marked = new boolean[nums.length]; + Arrays.sort(nums); + dfs(nums, marked, new ArrayList()); + return ret; + } + private void dfs(int[] nums, boolean[] marked, ArrayList list) { + if (list.size() == nums.length) { + ret.add(new ArrayList(list)); + return; + } + for (int i = 0; i < nums.length; i++){ + if (marked[i]) + continue; + if (i != 0 && nums[i] == nums[i - 1] && !marked[i - 1]) + continue; + list.add(nums[i]); + marked[i] = true; + dfs(nums, marked, list); + marked[i] = false; + list.remove(list.size() - 1); + } + } +} +``` + diff --git "a/Java/alg/lc/485.\346\234\200\345\244\247\350\277\236\347\273\2551\347\232\204\344\270\252\346\225\260.md" "b/Java/alg/lc/485.\346\234\200\345\244\247\350\277\236\347\273\2551\347\232\204\344\270\252\346\225\260.md" new file mode 100644 index 00000000..480ce65a --- /dev/null +++ "b/Java/alg/lc/485.\346\234\200\345\244\247\350\277\236\347\273\2551\347\232\204\344\270\252\346\225\260.md" @@ -0,0 +1,74 @@ +# 485. 最大连续 1 的个数 + + +[url](https://leetcode-cn.com/problems/max-consecutive-ones/) + + +## 题目 +给定一个二进制数组, 计算其中最大连续 `1` 的个数。 + + +``` +输入:[1,1,0,1,1,1] +输出:3 +解释:开头的两位和最后的三位都是连续 1 ,所以最大连续 1 的个数是 3. +``` + +## 方法 + + +## code + +### js + +```js +let findMaxConsecutiveOnes = nums => { + let max = 0, cur = 0; + nums.forEach(num => { + cur = num === 0 ? 0 : cur + 1; + max = Math.max(max, cur); + }); + return max; +}; +console.log(findMaxConsecutiveOnes([1, 1, 0, 1, 1, 1])); +``` + +### go + +```go +func findMaxConsecutiveOnes(nums []int) int { + Max := func(a, b int) int { + if a > b { + return a + } else { + return b + } + } + max, cur := 0, 0 + for _, num := range nums { + if num == 0 { + cur = 0 + } else { + cur = cur + 1 + } + max = Max(cur, max) + } + return max +} +``` + +### java + +```java +class Solution { + public int findMaxConsecutiveOnes(int[] nums) { + int max = 0, cur = 0; + for (int x : nums) { + cur = x == 0 ? 0 : cur + 1; + max = Math.max(max, cur); + } + return max; + } +} +``` + diff --git "a/Java/alg/lc/5.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" "b/Java/alg/lc/5.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" new file mode 100644 index 00000000..b49898fd --- /dev/null +++ "b/Java/alg/lc/5.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" @@ -0,0 +1,120 @@ +# 5. 最长回文子串 + +[url](https://leetcode-cn.com/problems/longest-palindromic-substring/) + +## 题目 + +给你一个字符串 s,找到 s 中最长的回文子串。 + +``` +输入:s = "babad" +输出:"bab" +解释:"aba" 同样是符合题意的答案。 +输入:s = "cbbd" +输出:"bb" +输入:s = "a" +输出:"a" +输入:s = "ac" +输出:"a" +``` + +## 方法 + + +## code + +### js + +```js +let longestPalindrome = s => { + let expand = (s, l, r) => { + while (l >= 0 && r < s.length && s[l] === s[r]) { + l--; + r++; + } + return r - l - 1; + }; + if (s === null || s.length === 0) + return s; + let start = 0, end = 0, len1 = 0, len2 = 0, maxLen = 0; + for (let i = 0; i < s.length; i ++) { + len1 = expand(s, i - 1, i + 1); + len2 = expand(s, i, i + 1); + maxLen = Math.max(len1, len2); + if (maxLen > (end - start)) { + start = i - Math.floor((maxLen - 1) / 2); + end = i + Math.floor(maxLen / 2); + } + } + return s.substring(start, end + 1); +}; +``` + +### go + +```go +func longestPalindrome2(s string) string { + if len(s) == 0 { + return s + } + start, end := 0, 0 + len1, len2 := 0, 0 + maxLen := 0 + expand := func(s string, l int, r int) int { + for l >= 0 && r < len(s) && s[l] == s[r] { + l-- + r++ + } + return r - l - 1 + } + Max := func(a int, b int) int { + if a < b { + return b + } else { + return a + } + } + for i := 0; i < len(s); i++ { + len1 = expand(s, i - 1, i + 1) + len2 = expand(s, i, i + 1) + maxLen = Max(len1, len2) + if maxLen > (end - start) { + start = i - (maxLen - 1) / 2 + end = i + maxLen / 2 + } + } + return s[start : end + 1] +} +``` + +### java + +```java +class Solution { + public String longestPalindrome(String s) { + if (s == null || s.length() == 0) + return s; + int start = 0, end = 0; + int len1 = 0, len2 = 0; + int maxLen = 0; + for (int i = 0; i < s.length(); i++) { + len1 = expand(s, i - 1, i + 1); + len2 = expand(s, i, i + 1); + maxLen = Math.max(len1, len2); + if (maxLen > (end - start)) { + start = i - (maxLen - 1) / 2; + end = i + maxLen / 2; + } + } + return s.substring(start, end + 1); + } + public int expand(String s, int l, int r) { + while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)){ + l--; + r++; + } + return r - l -1; + } +} +``` + diff --git "a/Java/alg/lc/509.\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260.md" "b/Java/alg/lc/509.\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260.md" new file mode 100644 index 00000000..95094f40 --- /dev/null +++ "b/Java/alg/lc/509.\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260.md" @@ -0,0 +1,84 @@ +# 509. 斐波那契数 + + +[url](https://leetcode-cn.com/problems/fibonacci-number/) + + +## 题目 +斐波那契数,通常用 `F(n)` 表示,形成的序列称为 斐波那契数列 。该数列由 `0` 和 `1` 开始,后面的每一项数字都是前面两项数字的和。也就是: + +``` +F(0) = 0,F(1) = 1 +F(n) = F(n - 1) + F(n - 2),其中 n > 1 +``` + +``` +输入:2 +输出:1 +解释:F(2) = F(1) + F(0) = 1 + 0 = 1 +输入:3 +输出:2 +解释:F(3) = F(2) + F(1) = 1 + 1 = 2 +输入:4 +输出:3 +解释:F(4) = F(3) + F(2) = 2 + 1 = 3 +``` + +## 方法 +从下往上的思想 + +## code + +### js + +```js +let fib = n => { + if (n <= 1) + return n; + let pre2 = 0, pre1 = 1; + for (let i = 2; i <= n; i++) { + let sum = pre2 + pre1; + pre2 = pre1; + pre1 = sum; + } + return pre1; +}; +console.log(fib(2)); +console.log(fib(3)); +console.log(fib(4)); +``` + +### go + +```go +func fib(n int) int { + if n <= 1 { + return n + } + pre2, pre1 := 0, 1 + for i := 2; i <= n; i++ { + sum := pre2 + pre1 + pre2 = pre1 + pre1 = sum + } + return pre1 +} +``` + +### java + +```java +class Solution { + public int fib(int N) { + if (N <= 1) return N; + int pre2 = 0, pre1 = 1; + for (int i = 2; i <= N; i++) { + int sum = pre2 + pre1; + pre2 = pre1; + pre1 = sum; + } + return pre1; + } +} +``` + diff --git "a/Java/alg/lc/53.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214.md" "b/Java/alg/lc/53.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214.md" new file mode 100644 index 00000000..86819d7e --- /dev/null +++ "b/Java/alg/lc/53.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214.md" @@ -0,0 +1,87 @@ +# 53. 最大子序和 + +[url](https://leetcode-cn.com/problems/maximum-subarray/) + +## 题目 + +给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 + +``` +输入: [-2,1,-3,4,-1,2,1,-5,4] +输出: 6 +解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 +``` + +## 方法 + +这道题算是动态规划的一种,只不过没有用记忆数组去存,而是用记忆变量去存 + +可以观察这个数组:`[-2,1,-3,4,-1,2,1,-5,4]` + +1. 题目说找一个具有最大和的连续子数组,那么必然是要求和的,肯定有求和这一步,那么我们就求和 +2. `preSum = -2 + 1 = -1`,咦?不对劲,-1还不如1大,我直接舍弃原来的负数把,此时`preSum = 1`,`maxSum = 1` +3. `preSum = preSum + -3 = -2`,`maxSum = 1` +4. `preSum = preSum + 4 = 2`, 咦?不对劲,还不如舍弃原来的和,直接用该值呢,此时`preSum = 4`,`maxSum = 4` +5. 以此类推,你就会发现,最后maxSum是6,为什么在第三步舍弃-3这个数呢?明明加了-3,preSum变成了-2,毕竟题目说了连续嘛,难不成,你跳过-3哇?所以就有了这么一句话的判断`preSum = preSum > 0 ? preSum + nums[i] : nums[i];`,意思就是说,**以当前和的正负判断,正我就加,负我就舍弃和,从新的值开始。** + +## code + +### js + +```js +let maxSubArray = (nums) => { + if (nums === undefined || nums.length === 0) + return -1; + let preSum = nums[0]; + let maxSum = preSum; + for (let i = 1; i < nums.length; i++) { + preSum = preSum > 0 ? preSum + nums[i] : nums[i]; + maxSum = Math.max(maxSum, preSum); + } + return maxSum; +} + +console.log(maxSubArray([-2,1,-3,4,-1,2,1,-5,4])); + +``` + +### go + +```go +func maxSubArray(nums []int) int { + if nums == nil || len(nums) == 0 { + return -1 + } + preSum, maxSum := nums[0], nums[0] + for i := 1; i < len(nums); i++ { + if preSum > 0 { + preSum = preSum + nums[i] + } else { + preSum = nums[i] + } + maxSum = Max(maxSum, preSum) + } + return maxSum +} +``` + + + +### java + +```java +class Solution { + public int maxSubArray(int[] nums) { + if (nums == null || nums.length == 0) + return -1; + int preSum = nums[0]; + int maxSum = preSum; + for (int i = 1; i < nums.length; i++) { + preSum = preSum > 0 ? preSum + nums[i] : nums[i]; + maxSum = Math.max(maxSum, preSum); + } + return maxSum; + } +} +``` + diff --git "a/Java/alg/lc/543.\344\272\214\345\217\211\346\240\221\347\232\204\347\233\264\345\276\204.md" "b/Java/alg/lc/543.\344\272\214\345\217\211\346\240\221\347\232\204\347\233\264\345\276\204.md" new file mode 100644 index 00000000..805be086 --- /dev/null +++ "b/Java/alg/lc/543.\344\272\214\345\217\211\346\240\221\347\232\204\347\233\264\345\276\204.md" @@ -0,0 +1,90 @@ +# 543. 二叉树的直径 + + +[url](https://leetcode-cn.com/problems/diameter-of-binary-tree/) + + +## 题目 +给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。 + + +``` + 1 + / \ + 2 3 + / \ + 4 5 +``` +返回 3, 它的长度是路径 `[4,2,1,3]` 或者 `[5,2,1,3]`。 + +## 方法 +从下往上的思想 + +## code + +### js + +```js +let max = 0; +let diameterOfBinaryTree = root => { + depth(root); + return max; +}; +let depth = node => { + if (node === null) + return 0; + let l = depth(node.left); + let r = depth(node.right); + max = Math.max(max, (l + r)); + return Math.max(l, r) + 1; +}; +``` + +### go + +```go +func diameterOfBinaryTree(root *TreeNode) int { + Max := func(a int, b int) int { + if a < b { + return b + } else { + return a + } + } + var max = 0 + var depth func(node *TreeNode) int + depth = func(node *TreeNode) int { + if node == nil { + return 0 + } + l := depth(node.Left) + r := depth(node.Right) + max = Max(max, l+r) + return Max(l, r) + 1 + } + depth(root) + return max +} + +``` + +### java + +```java +class Solution { + int max = 0; + public int diameterOfBinaryTree(TreeNode root) { + depth(root); + return max; + } + public int depth(TreeNode node) { + if (node == null) + return 0; + int l = depth(node.left); + int r = depth(node.right); + max = Math.max(max, (l + r)); + return Math.max(l, r) + 1; + } +} +``` + diff --git "a/Java/alg/lc/55.\350\267\263\350\267\203\346\270\270\346\210\217.md" "b/Java/alg/lc/55.\350\267\263\350\267\203\346\270\270\346\210\217.md" new file mode 100644 index 00000000..7f0de600 --- /dev/null +++ "b/Java/alg/lc/55.\350\267\263\350\267\203\346\270\270\346\210\217.md" @@ -0,0 +1,88 @@ +# 55. 跳跃游戏 + +[url](https://leetcode-cn.com/problems/jump-game/) + +## 题目 + +给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。 + +数组中的每个元素代表你在该位置可以跳跃的最大长度。 + +判断你是否能够到达最后一个下标。 + +``` +输入:nums = [2,3,1,1,4] +输出:true +解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 +输入:nums = [3,2,1,0,4] +输出:false +解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。 +``` + +## 方法 + +## code + +### js + +```js +let canJump = nums => { + if (nums === null || nums.length === 0) return false; + let max = 0; + for (let i = 0; i < nums.length - 1; i++) { + if (i <= max) + max = Math.max(max, nums[i] + 1); + else + break; + } + return max >= nums.length - 1; +} +``` + +### go + +```go +func canJump(nums []int) bool { + if len(nums) == 0 { + return false + } + Max := func(a int, b int) int { + if a > b { + return a + } else { + return b + } + } + max := 0 + for i := 1; i < len(nums); i++ { + if i <= max { + max = Max(max, nums[i] + 1) + } else { + break + } + } + return max >= len(nums) - 1 +} +``` + + + +### java + +```java +class Solution { + public boolean canJump(int[] nums) { + if (nums == null || nums.length == 0) + return false; + int max = 0; + for (int i = 1; i < nums.length - 1; i++){ + if (i <= max) + max = Math.max(max, nums[i] + 1); + else + break; + } + return max >= nums.length - 1; + } +} +``` + diff --git "a/Java/alg/lc/56.\345\220\210\345\271\266\345\214\272\351\227\264.md" "b/Java/alg/lc/56.\345\220\210\345\271\266\345\214\272\351\227\264.md" new file mode 100644 index 00000000..6e043ade --- /dev/null +++ "b/Java/alg/lc/56.\345\220\210\345\271\266\345\214\272\351\227\264.md" @@ -0,0 +1,111 @@ +# 56. 合并区间 + +[url](https://leetcode-cn.com/problems/merge-intervals/) + +## 题目 + +以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。 + +``` +输入:intervals = [[1,3],[2,6],[8,10],[15,18]] +输出:[[1,6],[8,10],[15,18]] +解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. +输入:intervals = [[1,4],[4,5]] +输出:[[1,5]] +解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。 +``` + +## 方法 + +## code + +### js + +```js +let merge = intervals => { + if (intervals === null || intervals.length <= 1) return intervals; + intervals.sort((a, b) => { + return a[0] - b[0] + }); + let list = [] + let i = 0, n = intervals.length; + while (i < n) { + let l = intervals[i][0]; + let r = intervals[i][1]; + while (i < n - 1 && r >= intervals[i + 1][0]) { + r = Math.max(r, intervals[i + 1][1]) + i++; + } + list.push([l, r]); + i++; + } + return list; +} +``` + +### go + +```go +type numsSortable [][]int + +func (nums numsSortable) Len() int { + return len(nums) +} +func (nums numsSortable) Less(i, j int) bool { + return nums[i][0] <= nums[j][0] +} +func (nums numsSortable) Swap(i, j int) { + nums[i], nums[j] = nums[j], nums[i] +} +func merge2(intervals [][]int) [][]int { + Max := func(a, b int) int { + if a > b { + return a + } + return b + } + if len(intervals) <= 1{ + return intervals + } + sort.Sort(numsSortable(intervals)) + list := make([][]int, 0) + i, n := 0, len(intervals) + for i < n { + l, r := intervals[i][0], intervals[i][1] + for i < n-1 && r >= intervals[i+1][0] { + r = Max(r, intervals[i + 1][1]) + i++ + } + list = append(list, []int{l, r}) + i++ + } + return list +} +``` + + +### java + +```java +class Solution { + public int[][] merge(int[][] intervals) { + if (intervals == null || intervals.length <= 1) return intervals; + Arrays.sort(intervals, (a, b) -> a[0] - b[0]); + List list = new ArrayList<>(); + int i = 0; + int n = intervals.length; + while (i < n) { + int l = intervals[i][0]; + int r = intervals[i][1]; + while (i < n - 1 && r >= intervals[i + 1][0]) { + r = Math.max(r, intervals[i + 1][1]); + i++; + } + list.add(new int[] {l, r}); + i++; + } + return list.toArray(new int[list.size()][2]); + } +} +``` + diff --git "a/Java/alg/lc/566.\351\207\215\345\241\221\347\237\251\351\230\265.md" "b/Java/alg/lc/566.\351\207\215\345\241\221\347\237\251\351\230\265.md" new file mode 100644 index 00000000..a80952a2 --- /dev/null +++ "b/Java/alg/lc/566.\351\207\215\345\241\221\347\237\251\351\230\265.md" @@ -0,0 +1,89 @@ +# 566. 重塑矩阵 + + +[url](https://leetcode-cn.com/problems/reshape-the-matrix/) + + +## 题目 +在MATLAB中,有一个非常有用的函数 reshape,它可以将一个矩阵重塑为另一个大小不同的新矩阵,但保留其原始数据。 + +给出一个由二维数组表示的矩阵,以及两个正整数r和c,分别表示想要的重构的矩阵的行数和列数。 + +重构后的矩阵需要将原始矩阵的所有元素以相同的行遍历顺序填充。 + +如果具有给定参数的reshape操作是可行且合理的,则输出新的重塑矩阵;否则,输出原始矩阵。 + + + +``` +输入: +nums = +[[1,2], + [3,4]] +r = 1, c = 4 +输出: +[[1,2,3,4]] +解释: +行遍历nums的结果是 [1,2,3,4]。新的矩阵是 1 * 4 矩阵, 用之前的元素值一行一行填充新矩阵。 +输入: +nums = +[[1,2], + [3,4]] +r = 2, c = 4 +输出: +[[1,2], + [3,4]] +解释: +没有办法将 2 * 2 矩阵转化为 2 * 4 矩阵。 所以输出原矩阵。 +``` + +## 方法 +从下往上的思想 + +## code + +### js + +```js +``` + +### go + +```go +func matrixReshape(nums [][]int, r int, c int) [][]int { + m, n := len(nums), len(nums[0]) + if m*n != r*c { + return nums + } + reshapednums := make([][]int, 0) + idx := 0 + for i := 0; i < r; i++ { + for j := 0; j < c; j++ { + reshapednums[i][j] = nums[idx / n][idx % n] + idx++ + } + } + return reshapednums +} +``` + +### java + +```java +class Solution { + public int[][] matrixReshape(int[][] nums, int r, int c) { + int m = nums.length, n = nums[0].length; + if (m * n != r * c) return nums; + int[][] reshapedNums = new int[r][c]; + int idx = 0; + for (int i = 0; i < r; i++) { + for (int j = 0; j < c; j++) { + reshapedNums[i][j] = nums[idx / n][idx % n]; + idx++; + } + } + return reshapedNums; + } +} +``` + diff --git "a/Java/alg/lc/572.\345\217\246\344\270\200\344\270\252\346\240\221\347\232\204\345\255\220\346\240\221.md" "b/Java/alg/lc/572.\345\217\246\344\270\200\344\270\252\346\240\221\347\232\204\345\255\220\346\240\221.md" new file mode 100644 index 00000000..37610320 --- /dev/null +++ "b/Java/alg/lc/572.\345\217\246\344\270\200\344\270\252\346\240\221\347\232\204\345\255\220\346\240\221.md" @@ -0,0 +1,90 @@ +# 572. 另一个树的子树 + + +[url](https://leetcode-cn.com/problems/subtree-of-another-tree/) + + +## 题目 +给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。s 的一个子树包括 s 的一个节点和这个节点的所有子孙。s 也可以看做它自身的一棵子树。 + + +``` +s: + 3 + / \ + 4 5 + / \ + 1 2 +t: + 4 + / \ + 1 2 +``` +返回 true,因为 t 与 s 的一个子树拥有相同的结构和节点值。 + +## 方法 +从下往上的思想 + +## code + +### js + +```js +let isSubtree = (s, t) => { + if (s === null) + return false; + return isSubtreeWithRoot(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t); +}; +let isSubtreeWithRoot = (s, t) => { + if (t === null && s === null) + return true; + if (t === null || s === null) + return false; + if (t.val !== s.val) + return false; + return isSubtreeWithRoot(s.left, t.left) && isSubtreeWithRoot(s.right, t.right); +}; +``` + +### go + +```go +func isSubtree(s *TreeNode, t *TreeNode) bool { + if s == nil { + return false + } + return isSubtreeWithRoot(s, t) || isSubtree(s.Left, t) || isSubtree(s.Right, t) +} +func isSubtreeWithRoot(s *TreeNode, t*TreeNode) bool { + if t == nil && s == nil { + return true + } + if t == nil || s == nil { + return false + } + if t.Val != s.Val { + return false + } + return isSubtreeWithRoot(s.Left, t.Left) && isSubtreeWithRoot(s.Right, t.Right) +} + +``` + +### java + +```java +class Solution { + public boolean isSubtree(TreeNode s, TreeNode t) { + if (s == null) return false; + return isSubtreeWithRoot(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t); + } + + private boolean isSubtreeWithRoot(TreeNode s, TreeNode t) { + if (t == null && s == null) return true; + if (t == null || s == null) return false; + if (t.val != s.val) return false; + return isSubtreeWithRoot(s.left, t.left) && isSubtreeWithRoot(s.right, t.right); + } +} +``` + diff --git "a/Java/alg/lc/58.\346\234\200\345\220\216\344\270\200\344\270\252\345\215\225\350\257\215\347\232\204\351\225\277\345\272\246.md" "b/Java/alg/lc/58.\346\234\200\345\220\216\344\270\200\344\270\252\345\215\225\350\257\215\347\232\204\351\225\277\345\272\246.md" new file mode 100644 index 00000000..99e891e3 --- /dev/null +++ "b/Java/alg/lc/58.\346\234\200\345\220\216\344\270\200\344\270\252\345\215\225\350\257\215\347\232\204\351\225\277\345\272\246.md" @@ -0,0 +1,57 @@ +# 58. 最后一个单词的长度 + +[url](https://leetcode-cn.com/problems/length-of-last-word/) + +## 题目 + +给定一个仅包含大小写字母和空格 ' ' 的字符串 s,返回其最后一个单词的长度。如果字符串从左向右滚动显示,那么最后一个单词就是最后出现的单词。 + +如果不存在最后一个单词,请返回 0 。 + +说明:一个单词是指仅由字母组成、不包含任何空格字符的 最大子字符串。 + +## 方法 + +这道题,感觉没啥可说的,一段单词,你split数组即可 + +## code + +### js + +```js +let lengthOfLastWord = s => { + let strs = s.split(" "); + if (strs.length === 0) + return 0; + return strs[strs.length - 1].length; +} + +console.log(lengthOfLastWord("Hello World")); +``` + +### go + +```go +func lengthOfLastWord(s string) int { + strs := strings.Split(s, " ") + if len(strs) == 0 { + return 0 + } + return len(strs[len(strs) - 1]) +} +``` + + + +### java + +```java +class Solution { + public int lengthOfLastWord(String s) { + String[] strs = s.split(" "); + if(strs.length == 0) return 0; + return strs[strs.length-1].length(); + } +} +``` + diff --git "a/Java/alg/lc/594.\346\234\200\351\225\277\345\222\214\350\260\220\345\255\220\345\272\217\345\210\227.md" "b/Java/alg/lc/594.\346\234\200\351\225\277\345\222\214\350\260\220\345\255\220\345\272\217\345\210\227.md" new file mode 100644 index 00000000..7fcfea3a --- /dev/null +++ "b/Java/alg/lc/594.\346\234\200\351\225\277\345\222\214\350\260\220\345\255\220\345\272\217\345\210\227.md" @@ -0,0 +1,102 @@ +# 594. 最长和谐子序列 + + +[url](https://leetcode-cn.com/problems/can-place-flowers/) + + +## 题目 +和谐数组是指一个数组里元素的最大值和最小值之间的差别 正好是 1 。 + +现在,给你一个整数数组 nums ,请你在所有可能的子序列中找到最长的和谐子序列的长度。 + +数组的子序列是一个由数组派生出来的序列,它可以通过删除一些元素或不删除元素、且不改变其余元素的顺序而得到。 + + + +``` +输入:nums = [1,3,2,2,5,2,3,7] +输出:5 +解释:最长的和谐子序列是 [3,2,2,2,3] +输入:nums = [1,2,3,4] +输出:2 +输入:nums = [1,1,1,1] +输出:0 +``` + +## 方法 + + +## code + +### js + +```js +let findLHS = nums => { + let map = new Map(); + nums.forEach(num => { + if (map.has(num)) { + map.set(num, map.get(num) + 1); + } else { + map.set(num , 1); + } + }); + let longest = 0; + for (const key of map.keys()) { + if (map.has(key + 1)) { + longest = Math.max(longest, map.get(key + 1) + map.get(key)); + } + } + return longest; +}; +console.log(findLHS([1,3,2,2,5,2,3,7])) +``` + +### go + +```go +func findLHS(nums []int) int { + Max := func(a int, b int) int { + if a > b { + return a + } else { + return b + } + } + m := make(map[int]int, len(nums)) + for _, num := range nums { + if v, ok := m[num]; ok { + m[num] = v + 1 + } else { + m[num] = 1 + } + } + longest := 0 + for _, k := range m { + if v, ok := m[k+1]; ok { + longest = Max(longest, v+m[k]) + } + } + return longest +} +``` + +### java + +```java +class Solution { + public int findLHS(int[] nums) { + Map map = new HashMap<>(); + for (int num : nums) { + map.put(num, map.getOrDefault(num, 0) + 1); + } + int longest = 0; + for (int num : map.keySet()) { + if (map.containsKey(num + 1)) { + longest = Math.max(longest, map.get(num + 1) + map.get(num)); + } + } + return longest; + } +} +``` + diff --git "a/Java/alg/lc/6.Z\345\255\227\345\275\242\345\217\230\346\215\242.md" "b/Java/alg/lc/6.Z\345\255\227\345\275\242\345\217\230\346\215\242.md" new file mode 100644 index 00000000..6c49c73a --- /dev/null +++ "b/Java/alg/lc/6.Z\345\255\227\345\275\242\345\217\230\346\215\242.md" @@ -0,0 +1,134 @@ +# 6. Z 字形变换 + +[url](https://leetcode-cn.com/problems/zigzag-conversion/) + +## 题目 + +将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。 + +比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下: + +``` +P A H N +A P L S I I G +Y I R +``` +之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"。 + +请你实现这个将字符串进行指定行数变换的函数: +``` +string convert(string s, int numRows); +``` +``` +输入:s = "PAYPALISHIRING", numRows = 3 +输出:"PAHNAPLSIIGYIR" +输入:s = "PAYPALISHIRING", numRows = 4 +输出:"PINALSIGYAHRPI" +解释: +P I N +A L S I G +Y A H R +P I +输入:s = "A", numRows = 1 +输出:"A" +``` + +## 方法 + +``` +// 0| 1 5 9 13 +// 1| 2 4 6 8 10 12 14 16 +// 2| 3 7 11 15 +// 每一行右边的字符的 ’索引值’ 都是其左边的字符的 ’索引值’ 加上它 ’下面剩余行数’ 的两倍或 ’上面行数’ 的两倍(交替相加) +// 以第二行为例, +// 对于4这个字符而言, 4 = 2(左边的索引) + 2(两倍) * 1(下面有一行) +// 6 = 4 + 2 * 1(上面有一行) +// 8 = 6 + 2 * 1(下面有一行) +``` + +## code + +### js + +```js +let convert = (s, numRows) => { + if (numRows <= 1) + return s; + let res = ""; + for (let i = 0; i < numRows; i++) { + let up = i, down = numRows - 1 - i; + let tmp = i, cnt = 0; + while (tmp < s.length) { + if (cnt % 2 === 0 && down !== 0) { + res += s[tmp]; + tmp += 2 * down; + } else if (cnt % 2 !== 0 && up !== 0) { + res += s[tmp]; + tmp += 2 * up; + } + cnt++; + } + } + return res; +}; +``` + +### go + +```go +func convert(s string, numRows int) string { + if numRows == 1 { + return s + } + res := "" + for i := 0; i < numRows; i++ { + up := i + down := numRows - 1 - i + temp := i + cnt := 0 + for temp < len(s) { + if cnt%2 == 0 && down != 0 { + res += string(s[temp]) + temp += 2 * down + } else if cnt%2 != 0 && up != 0 { + res += string(s[temp]) + temp += 2 * up + } + cnt++ + } + } + return res +} +``` + +### java + +```java +class Solution { + public String convert(String s, int numRows) { + if (numRows <= 1) + return s; + + char[] cs = s.toCharArray(); + String res = ""; + for (int i = 0; i < numRows; i++){ + int up = i; // 上方的行数 + int down = numRows - 1 - i; // 下方的行数 + int temp = i; + int cnt = 0; + while (temp < cs.length){ + if (cnt % 2 == 0 && down != 0) { + res += cs[temp]; + temp += 2 * down; + } else if(cnt % 2 != 0 && up != 0){ + res += cs[temp]; + temp += 2 * up; + } + cnt++; + } + } + return res; + } +} +``` + diff --git "a/Java/alg/lc/605.\347\247\215\350\212\261\351\227\256\351\242\230.md" "b/Java/alg/lc/605.\347\247\215\350\212\261\351\227\256\351\242\230.md" new file mode 100644 index 00000000..4029286c --- /dev/null +++ "b/Java/alg/lc/605.\347\247\215\350\212\261\351\227\256\351\242\230.md" @@ -0,0 +1,107 @@ +# 605.种花问题 + + +[url](https://leetcode-cn.com/problems/longest-harmonious-subsequence/) + + +## 题目 +假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。 + +给你一个整数数组  flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。 + + + +``` +输入:flowerbed = [1,0,0,0,1], n = 1 +输出:true +输入:flowerbed = [1,0,0,0,1], n = 2 +输出:false +``` + + +## 方法 + + +## code + +### js + +```js +let canPlaceFlowers = (flowerbed, n) => { + if (n < 0) + return false; + let cnt = 0; + let len = flowerbed.length; + for (let i = 0; i < len; i++) { + if (flowerbed[i] === 1) + continue; + let pre = i === 0 ? 0 : flowerbed[i - 1]; + let next = i === len - 1 ? 0 : flowerbed[i + 1]; + if (pre === 0 && next === 0) { + cnt++; + flowerbed[i] = 1; + } + } + return cnt >= n; +}; +console.log(canPlaceFlowers([1,0,0,0,1], 1)); +console.log(canPlaceFlowers([1,0,0,0,1], 2)); +``` + +### go + +```go +func canPlaceFlower(flowerbed []int, n int) bool { + if n < 0 { + return false + } + cnt := 0 + l := len(flowerbed) + for i := 0; i < l; i++ { + if flowerbed[i] == 1{ + continue + } + pre, next := 0, 0 + if i == 0 { + pre = 0 + } else { + pre = flowerbed[i - 1] + } + if i == l - 1 { + next = 0 + } else { + next = flowerbed[i + 1] + } + if pre == 0 && next == 0 { + cnt++ + flowerbed[i] = 1 + } + } + return cnt >= n +} +``` + +### java + +```java +class Solution { + public boolean canPlaceFlowers(int[] flowerbed, int n) { + // 边界? + if (n < 0) return false; + int cnt = 0; + int len = flowerbed.length; + for (int i = 0; i < len; i++) { + // 判断是1的 + if (flowerbed[i] == 1) continue; + int pre = i == 0 ? 0 : flowerbed[i - 1]; + int next = i == len - 1 ? 0 : flowerbed[i + 1]; + if (pre == 0 && next == 0) { + cnt++; + flowerbed[i] = 1; + } + } + return cnt >= n; + } +} +``` + diff --git "a/Java/alg/lc/617.\345\220\210\345\271\266\344\272\214\345\217\211\346\240\221.md" "b/Java/alg/lc/617.\345\220\210\345\271\266\344\272\214\345\217\211\346\240\221.md" new file mode 100644 index 00000000..952e5e55 --- /dev/null +++ "b/Java/alg/lc/617.\345\220\210\345\271\266\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,87 @@ +# 617. 合并二叉树 + + +[url](https://leetcode-cn.com/problems/merge-two-binary-trees/) + + +## 题目 +给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。 + +你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。 + +``` +输入: + Tree 1 Tree 2 + 1 2 + / \ / \ + 3 2 1 3 + / \ \ + 5 4 7 +输出: +合并后的树: + 3 + / \ + 4 5 + / \ \ + 5 4 7 +``` + + +## 方法 + + +## code + +### js + +```js +let mergeTrees = (l1, l2) => { + if (l1 === null && l2 === null) + return null; + if (l1 === null) + return l2; + if (l2 === null) + return l1; + let root = TreeNode(l1.val + l2.val); + root.left = mergeTrees(l1.left, l2.left); + root.right = mergeTrees(l1.right, l2.right); + return root; +}; +``` + +### go + +```go +func mergeTrees(t1 *TreeNode, t2 *TreeNode) *TreeNode { + if t1 == nil && t2 == nil { + return nil + } + if t1 == nil { + return t2 + } + if t2 == nil { + return t1 + } + root := &TreeNode{Val: t1.Val + t2.Val} + root.Left = mergeTrees(t1.Left, t2.Left) + root.Right = mergeTrees(t1.Right, t2.Right) + return root +} +``` + +### java + +```java +class Solution { + public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { + if (t1 == null && t2 == null) return null; + if (t1 == null) return t2; + if (t2 == null) return t1; + TreeNode root = new TreeNode(t1.val + t2.val); + root.left = mergeTrees(t1.left, t2.left); + root.right = mergeTrees(t1.right, t2.right); + return root; + } +} +``` + diff --git "a/Java/alg/lc/62.\344\270\215\345\220\214\350\267\257\345\276\204.md" "b/Java/alg/lc/62.\344\270\215\345\220\214\350\267\257\345\276\204.md" new file mode 100644 index 00000000..81d60110 --- /dev/null +++ "b/Java/alg/lc/62.\344\270\215\345\220\214\350\267\257\345\276\204.md" @@ -0,0 +1,81 @@ +# 62. 不同路径 + +[url](https://leetcode-cn.com/problems/unique-paths/) + +## 题目 + +一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 + +机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 + +问总共有多少条不同的路径? + +![](https://assets.leetcode.com/uploads/2018/10/22/robot_maze.png) + +``` +输入:m = 3, n = 7 +输出:28 +输入:m = 3, n = 2 +输出:3 +解释: +从左上角开始,总共有 3 条路径可以到达右下角。 +1. 向右 -> 向下 -> 向下 +2. 向下 -> 向下 -> 向右 +3. 向下 -> 向右 -> 向下 +输入:m = 7, n = 3 +输出:28 +``` + +## 方法 + +## code + +### js + +```js +let uniquePaths = (m, n) => { + let dp = Array(n).fill(1); + for (let i = 1; i < m; i++) { + for (let j = 1; j < n; j++) { + dp[j] += dp[j - 1]; + } + } + return dp[n - 1]; +} +``` + +### go + +```go +func uniquePaths(m, n int) int { + dp := make([]int, n) + for i := 0; i < n; i++ { + dp[i] = 1 + } + for i := 1; i < m; i++ { + for j := 1; j < n; j++ { + dp[j] += dp[j - 1] + } + } + return dp[n-1] +} +``` + + +### java + +```java +class Solution { + public int uniquePaths(int m, int n) { + int[] dp = new int[n]; + Arrays.fill(dp, 1); + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[j] += dp[j - 1]; + } + } + return dp[n - 1]; + } +} +``` + diff --git "a/Java/alg/lc/628.\344\270\211\344\270\252\346\225\260\347\232\204\346\234\200\345\244\247\344\271\230\347\247\257.md" "b/Java/alg/lc/628.\344\270\211\344\270\252\346\225\260\347\232\204\346\234\200\345\244\247\344\271\230\347\247\257.md" new file mode 100644 index 00000000..77392438 --- /dev/null +++ "b/Java/alg/lc/628.\344\270\211\344\270\252\346\225\260\347\232\204\346\234\200\345\244\247\344\271\230\347\247\257.md" @@ -0,0 +1,122 @@ +# 628. 三个数的最大乘积 + + +[url](https://leetcode-cn.com/problems/maximum-product-of-three-numbers/) + + +## 题目 +给你一个整型数组 nums ,在数组中找出由三个数组成的最大乘积,并输出这个乘积。 + + +``` +输入:nums = [1,2,3] +输出:6 +输入:nums = [1,2,3,4] +输出:24 +输入:nums = [-1,-2,-3] +输出:-6 +``` + + +## 方法 + + +## code + +### js + +```js +let maximumProduct = nums => { + let minValue = -Infinity; + let max1 = minValue, max2 = minValue, max3 = minValue; + let min1 = -minValue, min2 = -minValue; + nums.forEach(n => { + if (n > max1) { + max3 = max2; + max2 = max1; + max1 = n; + } else if (n > max2) { + max3 = max2; + max2 = n; + } else if (n > max3) { + max3 = n; + } + if (n < min1) { + min2 = min1; + min1 = n; + } else if (n < min2) { + min2 = n; + } + }); + return Math.max(max1 * max2 * max3, max1 * min1 * min2); +}; +console.log(maximumProduct([1, 2, 3])); +console.log(maximumProduct([1, 2, 3, 4])); +``` + +### go + +```go +func maximumProduct(nums []int) int { + INT_MAX := int(^uint((0)) >> 1) + INT_MIN := ^INT_MAX + max1, max2, max3 := INT_MIN, INT_MIN, INT_MIN + min1, min2 := INT_MAX, INT_MAX + for _, num := range nums { + if num > max1 { + max3 = max2 + max2 = max1 + max1 = num + } else if num > max2 { + max3 = max2 + max2 = num + } else if num > max3 { + max3 = num + } + if num < min1 { + min2 = min1 + min1 = num + } else if num < min2 { + min2 = num + } + } + return max4(max1 * max2 * max3, max1 * min1 * min2) +} +func max4(a int, b int) int { + if a < b { + return b + } else { + return a + } +} +``` + +### java + +```java +class Solution { + public int maximumProduct(int[] nums) { + int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE,min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE; + for (int n : nums) { + if (n > max1) { + max3 = max2; + max2 = max1; + max1 = n; + } else if (n > max2) { + max3 = max2; + max2 = n; + } else if (n > max3) { + max3 = n; + } + if (n < min1) { + min2 = min1; + min1 = n; + } else if (n < min2) { + min2 = n; + } + } + return Math.max(max1*max2*max3, max1*min1*min2); + } +} +``` + diff --git "a/Java/alg/lc/63.\344\270\215\345\220\214\350\267\257\345\276\2042.md" "b/Java/alg/lc/63.\344\270\215\345\220\214\350\267\257\345\276\2042.md" new file mode 100644 index 00000000..90fc4e7f --- /dev/null +++ "b/Java/alg/lc/63.\344\270\215\345\220\214\350\267\257\345\276\2042.md" @@ -0,0 +1,101 @@ +# 63. 不同路径 II + + +[url](https://leetcode-cn.com/problems/unique-paths-ii/) + +## 题目 + +一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 + +机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 + +现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径? + + +![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/22/robot_maze.png) + + +![](https://assets.leetcode.com/uploads/2020/11/04/robot1.jpg) +``` +输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] +输出:2 +解释: +3x3 网格的正中间有一个障碍物。 +从左上角到右下角一共有 2 条不同的路径: +1. 向右 -> 向右 -> 向下 -> 向下 +2. 向下 -> 向下 -> 向右 -> 向右 +``` +![](https://assets.leetcode.com/uploads/2020/11/04/robot2.jpg) +``` +输入:obstacleGrid = [[0,1],[0,0]] +输出:1 +``` + +## 方法 + +## code + +### js + +```js +let uniquePathsWithObstacles = obstacleGrid => { + let m = obstacleGrid.length, n = obstacleGrid[0].length; + let dp = Array(n + 1).fill(0); + dp[1] = 1; + for (let i = 1; i <= m; i ++) { + for (let j = 1; j <= n; j++) { + if (obstacleGrid[i - 1][j - 1] === 1) { + dp[j] = 0; + } else { + dp[j] += dp[j - 1]; + } + } + } + return dp[n]; +} +``` + +### go + +```go +func uniquePathsWithObstacles(obstacleGrid [][]int) int { + m, n := len(obstacleGrid), len(obstacleGrid[0]) + dp := make([]int, n + 1) + dp[1] = 1 + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if obstacleGrid[i-1][j-1] == 1 { + dp[j] = 0 + } else { + dp[j] += dp[j - 1] + } + } + } + return dp[n] +} +``` + + +### java + +```java +class Solution { + public int uniquePathsWithObstacles(int[][] obstacleGrid) { + int m = obstacleGrid.length; + int n = obstacleGrid[0].length; + int[] dp = new int[n + 1]; + dp[1] = 1; + for (int i = 1; i <= m; i++){ + for (int j = 1; j <= n; j++) { + if (obstacleGrid[i - 1][j - 1] == 1) { + dp[j] = 0; + } else { + dp[j] += dp[j - 1]; + } + } + } + return dp[n]; + } +} +``` + diff --git "a/Java/alg/lc/637.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\271\263\345\235\207\345\200\274.md" "b/Java/alg/lc/637.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\271\263\345\235\207\345\200\274.md" new file mode 100644 index 00000000..9a4f78a9 --- /dev/null +++ "b/Java/alg/lc/637.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\271\263\345\235\207\345\200\274.md" @@ -0,0 +1,107 @@ +# 637. 二叉树的层平均值 + + +[url](https://leetcode-cn.com/problems/average-of-levels-in-binary-tree/) + + +## 题目 +给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。 + + +``` +输入: + 3 + / \ + 9 20 + / \ + 15 7 +输出:[3, 14.5, 11] +解释: +第 0 层的平均值是 3 , 第1层是 14.5 , 第2层是 11 。因此返回 [3, 14.5, 11] 。 +``` + + +## 方法 + + +## code + +### js + +```js +let averageOfLevels = root => { + let ret = []; + if (root === null) + return ret; + let queue = [root]; + while (queue.length !== 0) { + let cnt = queue.length; + let sum = 0; + for (let i = 0; i < cnt; i++) { + let node = queue[0]; + queue = queue.slice(1, queue.length - 1); + sum += node.val; + if (node.left !== null) + queue.push(node.left); + if (node.right !== null) + queue.push(node.right); + } + ret.push(sum / cnt); + } + return ret; +}; +``` + +### go + +```go +func averageOfLevels(root *TreeNode) []float64 { + ret := make([]float64, 0) + if root == nil { + return ret + } + q := []*TreeNode{root} + for len(q) != 0 { + curQ := q + q = nil + sum := 0 + for _, node := range curQ { + sum += node.Val + if node.Left != nil { + q = append(q, node.Left) + } + if node.Right != nil { + q = append(q, node.Right) + } + } + ret = append(ret, float64(sum) / float64(len(curQ))) + } + return ret +} +``` + +### java + +```java +class Solution { + public List averageOfLevels(TreeNode root) { + List ret = new ArrayList<>(); + if (root == null) return ret; + Queue queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()) { + int cnt = queue.size(); + double sum = 0; + for (int i = 0; i < cnt; i++) { + TreeNode node = queue.poll(); + sum += node.val; + if (node.left != null) queue.add(node.left); + if (node.right != null) queue.add(node.right); + } + ret.add(sum / cnt); + } + return ret; + } +} +``` + diff --git "a/Java/alg/lc/64.\346\234\200\345\260\217\350\267\257\345\276\204\345\222\214.md" "b/Java/alg/lc/64.\346\234\200\345\260\217\350\267\257\345\276\204\345\222\214.md" new file mode 100644 index 00000000..b82f3cda --- /dev/null +++ "b/Java/alg/lc/64.\346\234\200\345\260\217\350\267\257\345\276\204\345\222\214.md" @@ -0,0 +1,121 @@ +# 64. 最小路径和 + + +[url](https://leetcode-cn.com/problems/unique-paths-ii/) + +## 题目 + +给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 + +说明:每次只能向下或者向右移动一步。 + + + +![](https://assets.leetcode.com/uploads/2020/11/05/minpath.jpg) +``` +输入:grid = [[1,3,1],[1,5,1],[4,2,1]] +输出:7 +解释:因为路径 1→3→1→1→1 的总和最小。 +输入:grid = [[1,2,3],[4,5,6]] +输出:12 +``` + + +## 方法 + +## code + +### js + +```js +let minPathSum = grid => { + if (grid === null || grid.length === 0 || grid[0].length === 0) return 0; + let m = grid.length, n = grid[0].length; + let dp = Array(m).fill(0).map(() => Array(n).fill(0)); + dp[0][0] = grid[0][0]; + // 第一列 + for (let i = 1; i < m; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + for (let j = 1; j < n; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + for (let i = 1; i < m; i++) { + for (let j = 1; j < n; j++) { + dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; + } + } + return dp[m - 1][n - 1]; +} +``` + +### go + +```go +func minPathSum(grid [][]int) int { + Min := func(a, b int) int { + if a < b { + return a + } + return b + } + if len(grid) == 0 || len(grid[0]) == 0 { + return 0 + } + m, n := len(grid), len(grid[0]) + dp := make([][]int, m) + for i := 0; i < len(dp); i++ { + dp[i] = make([]int, n) + } + dp[0][0] = grid[0][0] + for i := 1; i < m; i++ { + dp[i][0] = dp[i - 1][0] + grid[i][0] + } + for j := 1; j < n; j++ { + dp[0][j] = dp[0][j - 1] + grid[0][j] + } + for i := 1; i < m; i++ { + for j := 1; j < n; j++ { + dp[i][j] = Min(dp[i-1][j], dp[i][j-1]) + grid[i][j] + } + } + return dp[m-1][n-1] +} +``` + + +### java + +```java +class Solution { + public int minPathSum(int[][] grid) { + if (grid == null || grid.length == 0 || grid[0].length == 0) + return 0; + int m = grid.length; + int n = grid[0].length; + int[][] dp = new int[m][n]; + dp[0][0] = grid[0][0]; + // 第一列 + for (int i = 1; i < m; i++){ + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 第一行 + for (int j = 1; j < n; j++){ + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + for (int i = 0; i < dp.length; i++) { + System.out.println(Arrays.toString(dp[i])); + } + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++){ + dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; + } + } + for (int i = 0; i < dp.length; i++) { + System.out.println(Arrays.toString(dp[i])); + } + return dp[m - 1][n - 1]; + } +} +``` + diff --git "a/Java/alg/lc/645.\351\224\231\350\257\257\347\232\204\351\233\206\345\220\210.md" "b/Java/alg/lc/645.\351\224\231\350\257\257\347\232\204\351\233\206\345\220\210.md" new file mode 100644 index 00000000..cef72f2c --- /dev/null +++ "b/Java/alg/lc/645.\351\224\231\350\257\257\347\232\204\351\233\206\345\220\210.md" @@ -0,0 +1,93 @@ +# 645. 错误的集合 + + +[url](https://leetcode-cn.com/problems/set-mismatch/) + + +## 题目 +集合 s 包含从 1 到 n 的整数。不幸的是,因为数据错误,导致集合里面某一个数字复制了成了集合里面的另外一个数字的值,导致集合 丢失了一个数字 并且 有一个数字重复 。 + +给定一个数组 nums 代表了集合 S 发生错误后的结果。 + +请你找出重复出现的整数,再找到丢失的整数,将它们以数组的形式返回。 + + +``` +输入:nums = [1,2,2,4] +输出:[2,3] +输入:nums = [1,1] +输出:[1,2] +``` + + +## 方法 + + +## code + +### js + +```js +let findErrorNums = nums => { + // 桶计数 + let res = []; + let temp = Array(nums.length + 1).fill(0); + nums.forEach(num => temp[num]++); + for (const [idx, value] of nums.entries()) { + if (value === 1) + continue; + if (value === 2) + res[0] = idx; + else + res[1] = idx; + } + return res; +}; +console.log(findErrorNums([1, 2, 2, 4])) +``` + +### go + +```go +func findErrorNums(nums []int) []int { + // 桶计数 + res := make([]int, 2) + temp := make([]int, len(nums) + 1) + for _, v := range nums { + temp[v]++ + } + for i := 1; i < len(temp); i++ { + if temp[i] == 1 { + continue + } + if temp[i] == 2 { + res[0] = i + } else { + res[1] = i + } + } + return res +} +``` + +### java + +```java +class Solution { + public int[] findErrorNums(int[] nums) { + // 桶计数 + int[] res = new int[2]; + int[] temp = new int[nums.length + 1]; + for (int num : nums) { + temp[num]++; + } + for (int i = 1; i < temp.length; i++) { + if (temp[i] == 1) continue; + if (temp[i] == 2) res[0] = i; + else res[1] = i; + } + return res; + } +} +``` + diff --git "a/Java/alg/lc/66.\345\212\240\344\270\200.md" "b/Java/alg/lc/66.\345\212\240\344\270\200.md" new file mode 100644 index 00000000..8c93950c --- /dev/null +++ "b/Java/alg/lc/66.\345\212\240\344\270\200.md" @@ -0,0 +1,103 @@ +# 66. 加一 + +[url](https://leetcode-cn.com/problems/plus-one/) + +## 题目 + +给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。 + +最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。 + +你可以假设除了整数 0 之外,这个整数不会以零开头。 + +``` +输入:digits = [1,2,3] +输出:[1,2,4] +解释:输入数组表示数字 123。 + +输入:digits = [4,3,2,1] +输出:[4,3,2,2] +解释:输入数组表示数字 4321。 + +输入:digits = [0] +输出:[1] +``` + +## 方法 + +这道题挺简单的,主要是考察进位 + +- 经常用到 x % 10 得到该数的最后一位数字 +- 配合sum / 10 得到进位 + +## code + +### js + +```js +let plusOne = digits => { + let length = digits.length; + let res = []; + let carry = 1; + for (let i = length - 1; i >= 0; i--) { + let sums = digits[i] + carry; + res[i] = sums % 10; + carry = Math.floor(sums / 10); + } + if (carry === 1) { + res[0] = 1; + return res; + } + return res.copyWithin(res, 0, length); +} + +console.log(plusOne([1, 2, 3])); +console.log(plusOne([4, 3, 2, 1])); +console.log(plusOne([0])); +``` + +### go + +```go +func plusOne(digits []int) []int { + length := len(digits) + res := make([]int, length + 1) + carry := 1 + for i := length - 1; i >= 0; i-- { + sum := digits[i] + carry + res[i] = sum % 10 + carry = sum / 10 + } + if carry == 1 { + res[0] = 1 + return res + } + return res[0:length] +} +``` + + + +### java + +```java +class Solution { + public int[] plusOne(int[] digits) { + int length = digits.length; + int[] res = new int[length + 1]; + int carry = 1; + for (int i = length - 1; i >= 0 ; i--) { + int sums = digits[i] + carry; + res[i] = sums % 10; + carry = sums / 10; + } + if (carry == 1) { + res[0] = 1; + return res; + } + return Arrays.copyOfRange(res,0,length); + + } +} +``` + diff --git "a/Java/alg/lc/665.\351\235\236\351\200\222\345\207\217\346\225\260\345\210\227.md" "b/Java/alg/lc/665.\351\235\236\351\200\222\345\207\217\346\225\260\345\210\227.md" new file mode 100644 index 00000000..0f78ae3f --- /dev/null +++ "b/Java/alg/lc/665.\351\235\236\351\200\222\345\207\217\346\225\260\345\210\227.md" @@ -0,0 +1,96 @@ +# 665. 非递减数列 + + +[url](https://leetcode-cn.com/problems/non-decreasing-array/) + + +## 题目 +给你一个长度为 n 的整数数组,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。 + +我们是这样定义一个非递减数列的: 对于数组中任意的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。 + + +``` +输入:nums = [1,2,2,4] +输出:[2,3] +输入:nums = [1,1] +输出:[1,2] +``` + + +## 方法 +思路如下: +- 如果出现 a[i] > a[i+1] 改变一个数 就面临两种选择 + - 1. 把a[i]变大 + - 1. 把a[i+1] 变小 +- 这两种选择其实是有依据的 需要比较a[i-1] 与 a[i+1]的值 +eg. ... 1 4 3 ... 只能选择把4变小 ... 3 4 1 ... 只能选择把1变大 +- 改变完之后,记录改变次数,再检测是否升序 +- 如果次数大于1,至少改了两次 返回false +- 先让前两个有序 +- 因为没有左边没有数 所以对于前两个数来说,最佳选择就是吧 a[0] 变小 + +## code + +### js + +```js +let checkPossibility = nums => { + let cnt = 0; + for (let i = 1; i < nums.length && cnt < 2; i++) { + if (nums[i] >= nums[i - 1]) + continue; + cnt++; + // 关键在这里 + if (i - 2 >= 0 && nums[i - 2] > nums[i]) + nums[i] = nums[i - 1]; + else + nums[i - 1] = nums[i]; + } + return cnt <= 1; +}; +console.log(checkPossibility([4 ,2 ,3])); +console.log(checkPossibility([4 ,2, 1])); +``` + +### go + +```go +func checkPossibility(nums []int) bool { + cnt := 0 + for i := 1; i < len(nums) && cnt < 2; i++ { + if nums[i] >= nums[i-1] { + continue + } + cnt++ + if i-2 >= 0 && nums[i-2] > nums[i] { + nums[i] = nums[i - 1] + } else { + nums[i - 1] = nums[i] + } + } + return cnt <= 1 +} +``` + +### java + +```java +class Solution { + public boolean checkPossibility(int[] nums) { + int cnt = 0; + for (int i = 1; i < nums.length && cnt < 2; i++) { + if (nums[i] >= nums[i-1]) continue; + cnt++; + // 关键在这里呀 + if (i - 2 >= 0 && nums[i - 2] > nums[i]) { + nums[i] = nums[i - 1]; + } else { + nums[i - 1] = nums[i]; + } + } + return cnt <= 1; + } +} +``` + diff --git "a/Java/alg/lc/67.\344\272\214\350\277\233\345\210\266\346\261\202\345\222\214.md" "b/Java/alg/lc/67.\344\272\214\350\277\233\345\210\266\346\261\202\345\222\214.md" new file mode 100644 index 00000000..e48545c5 --- /dev/null +++ "b/Java/alg/lc/67.\344\272\214\350\277\233\345\210\266\346\261\202\345\222\214.md" @@ -0,0 +1,97 @@ +# 67. 二进制求和 + +[url](https://leetcode-cn.com/problems/add-binary/) + +## 题目 + +给你两个二进制字符串,返回它们的和(用二进制表示)。 + +输入为 非空 字符串且只包含数字 1 和 0。 + +``` +输入: a = "11", b = "1" +输出: "100" + +输入: a = "1010", b = "1011" +输出: "10101" +``` + +## 方法 + +主要是考察进位 + +- 经常用到 x % 2 得到该数的最后一位数字 +- 配合sum / 2 得到进位 + +## code + +### js + +```js +let addBinary = (a, b) => { + let i = a.length - 1; + let j = b.length - 1; + let carry = 0; + let res = [] + while (carry === 1 || i >= 0 || j >=0) { + if (i >= 0 && a[i--] === "1") carry++; + if (j >= 0 && b[j--] === "1") carry++; + res.push(carry % 2); + carry = Math.floor(carry / 2); + } + return res.reverse().join(""); +} + +console.log(addBinary("11", "1")); +console.log(addBinary("1010", "1011")); +``` + +### go + +```go +func addBinary(a, b string) string { + reverseStr := func (s string) string { + runes := []rune(s) + for i, j := 0, len(runes) - 1; i < j; i, j = i + 1, j - 1 { + runes[i], runes[j] = runes[j], runes[i] + } + return string(runes) + } + i, j, carry := len(a) - 1, len(b) - 1, 0 + var str []string + for carry == 1 || i >= 0 || j >= 0 { + if i >= 0 && a[i] == '1' { + carry++ + } + i-- + if j >= 0 && b[j] == '1' { + carry++ + } + j-- + str = append(str, strconv.Itoa(carry % 2)) + carry /= 2 + } + return reverseStr(strings.Join(str, "")) +} +``` + + + +### java + +```java +class Solution { + public String addBinary(String a, String b) { + int i = a.length() - 1, j = b.length() - 1, carry = 0; + StringBuilder str = new StringBuilder(); + while (carry == 1 || i >= 0 || j >= 0) { + if (i >= 0 && a.charAt(i--) == '1') carry++; + if (j >= 0 && b.charAt(j--) == '1') carry++; + str.append(carry % 2); + carry /= 2; + } + return str.reverse().toString(); + } +} +``` + diff --git "a/Java/alg/lc/671.\344\272\214\345\217\211\346\240\221\344\270\255\347\254\254\344\272\214\345\260\217\347\232\204\350\212\202\347\202\271.md" "b/Java/alg/lc/671.\344\272\214\345\217\211\346\240\221\344\270\255\347\254\254\344\272\214\345\260\217\347\232\204\350\212\202\347\202\271.md" new file mode 100644 index 00000000..bfab675b --- /dev/null +++ "b/Java/alg/lc/671.\344\272\214\345\217\211\346\240\221\344\270\255\347\254\254\344\272\214\345\260\217\347\232\204\350\212\202\347\202\271.md" @@ -0,0 +1,102 @@ +# 671. 二叉树中第二小的节点 + + +[url](https://leetcode-cn.com/problems/second-minimum-node-in-a-binary-tree/) + + +## 题目 +给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 2 或 0。如果一个节点有两个子节点的话,那么该节点的值等于两个子节点中较小的一个。 + +更正式地说,root.val = min(root.left.val, root.right.val) 总成立。 + +给出这样的一个二叉树,你需要输出所有节点中的第二小的值。如果第二小的值不存在的话,输出 -1 。 +![smbt1-LIpiVl](https://cdn.jsdelivr.net/gh/DreamCats/imgs@main/uPic/smbt1-LIpiVl.jpeg) +``` +输入:root = [2,2,5,null,null,5,7] +输出:5 +解释:最小的值是 2 ,第二小的值是 5 。 +输入:root = [2,2,2] +输出:-1 +解释:最小的值是 2, 但是不存在第二小的值。 +``` + + +## 方法 + + +## code + +### js + +```js +let findSecondMinimumValue = root => { + if (root === null) + return -1; + if (root.left === null && root.right === null) + return -1; + let leftValue = root.left.val, rightVal = root.right.val; + if (leftValue === root.val) + leftValue = findSecondMinimumValue(root.left); + if (rightVal === root.val) + rightVal = findSecondMinimumValue(root.right); + if (leftValue !== -1 && rightVal !== -1) + return Math.min(leftValue, rightVal); + if (leftValue !== -1) + return leftValue; + return rightVal; +}; +``` + +### go + +```go +func findSecondMinimumValue(root *TreeNode) int { + Min := func (a int, b int) int { + if a < b { + return a + } else { + return b + } + } + if root == nil { + return -1 + } + if root.Left == nil && root.Right == nil { + return -1 + } + leftVal := root.Left.Val + rightVal := root.Right.Val + if leftVal == root.Val { + leftVal = findSecondMinimumValue(root.Left) + } + if rightVal == root.Val { + rightVal = findSecondMinimumValue(root.Right) + } + if leftVal != -1 && rightVal != -1 { + return Min(leftVal, rightVal) + } + if leftVal != -1 { + return leftVal + } + return rightVal +} +``` + +### java + +```java +class Solution { + public int findSecondMinimumValue(TreeNode root) { + if (root == null) return -1; + if (root.left == null && root.right == null) return -1; + int leftVal = root.left.val; + int rightVal = root.right.val; + if (leftVal == root.val) leftVal = findSecondMinimumValue(root.left); + if (rightVal == root.val) rightVal = findSecondMinimumValue(root.right); + if (leftVal != -1 && rightVal != -1) return Math.min(leftVal, rightVal); + if (leftVal != -1) return leftVal; + return rightVal; + } +} +``` + diff --git "a/Java/alg/lc/674.\346\234\200\351\225\277\350\277\236\347\273\255\351\200\222\345\242\236\345\272\217\345\210\227.md" "b/Java/alg/lc/674.\346\234\200\351\225\277\350\277\236\347\273\255\351\200\222\345\242\236\345\272\217\345\210\227.md" new file mode 100644 index 00000000..339fcf53 --- /dev/null +++ "b/Java/alg/lc/674.\346\234\200\351\225\277\350\277\236\347\273\255\351\200\222\345\242\236\345\272\217\345\210\227.md" @@ -0,0 +1,93 @@ +# 674. 最长连续递增序列 + + +[url](https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence/) + + +## 题目 +给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。 + +连续递增的子序列 可以由两个下标 `l` 和 `r(l < r`)确定,如果对于每个 `l <= i < r`,都有 `nums[i] < nums[i + 1]` ,那么子序列 `[nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]` 就是连续递增子序列。 + +``` +输入:nums = [1,3,5,4,7] +输出:3 +解释:最长连续递增序列是 [1,3,5], 长度为3。 +尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。 +输入:nums = [2,2,2,2,2] +输出:1 +解释:最长连续递增序列是 [2], 长度为1。 +``` + + +## 方法 + + +## code + +### js + +```js +let findLengthOfLCIS = nums => { + if (nums === null || nums.length === 0) + return 0; + let d = 0, max = 1; + for (let i = 1; i < nums.length; i++) { + if (nums[i] > nums[i - 1]) + max = Math.max(i - d + 1, max); + else + d = i; + } + return max; +}; +console.log(findLengthOfLCIS([1, 3, 5, 4, 7])); +console.log(findLengthOfLCIS([2, 2, 2, 2, 2])); +``` + +### go + +```go +func findLengthOfLCIS(nums []int) int { + Max := func (a int, b int) int { + if a > b { + return a + } else { + return b + } + } + if nums == nil || len(nums) == 0 { + return 0 + } + d := 0 + max := 1 + for i := 1; i < len(nums); i++ { + if nums[i] > nums[i-1] { + max = Max(i - d + 1, max) + } else { + d = i + } + } + return max +} +``` + +### java + +```java +class Solution { + public int findLengthOfLCIS(int[] nums) { + if (nums == null || nums.length == 0) + return 0; + int d = 0; + int max = 1; + for (int i = 1; i < nums.length; i++) { + if (nums[i] > nums[i - 1]) + max = Math.max(i - d + 1, max); + else + d = i; + } + return max; + } +} +``` + diff --git "a/Java/alg/lc/680.\351\252\214\350\257\201\345\233\236\346\226\207\345\255\227\347\254\246\344\270\2622.md" "b/Java/alg/lc/680.\351\252\214\350\257\201\345\233\236\346\226\207\345\255\227\347\254\246\344\270\2622.md" new file mode 100644 index 00000000..49d3c64c --- /dev/null +++ "b/Java/alg/lc/680.\351\252\214\350\257\201\345\233\236\346\226\207\345\255\227\347\254\246\344\270\2622.md" @@ -0,0 +1,107 @@ +# 680. 验证回文字符串 Ⅱ + + +[url](https://leetcode-cn.com/problems/valid-palindrome-ii/) + + +## 题目 +给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。 + +``` +输入: "aba" +输出: True +输入: "abca" +输出: True +解释: 你可以删除c字符。 +``` + + +## 方法 + +- 普通判断回文串用前后双指针即可,但是,难点在于如果去删除一个元素后的字符串是不是回文串 +- 如果前后指针的元素不相等,此时子串的范围(i+1,j)或(j-1)的俩子串只要任意一个是,则结果是 +- 否则,则不是 + +## code + +### js + +```js +let validPalindrome = s => { + let i = 0, j = s.length - 1; + while (i < j) { + if (s[i] !== s[j]) { + return isValid(s, i+1, j) || isValid(s, i, j - 1); + } + i++; + j--; + } + return true; +}; +let isValid = (s, i, j) => { + while (i < j) { + if (s[i] !== s[j]) + return false; + i++; + j--; + } + return true; +}; +console.log(validPalindrome("aba")) +console.log(validPalindrome("abca")) +``` + +### go + +```go +func validPalindrome(s string) bool { + isValid := func (s string, i int, j int) bool { + for i < j { + if s[i] != s[j] { + return false + } + i++ + j-- + } + return true + } + i, j := 0, len(s) - 1 + for i < j { + if s[i] != s[j] { + return isValid(s, i + 1, j) || isValid(s, i, j - 1) + } + i++ + j-- + } + return true +} +``` + +### java + +```java +class Solution { + public boolean validPalindrome(String s) { + int i =0, j = s.length() - 1; + while(i < j) { + if(s.charAt(i) != s.charAt(j)) { + return isValid(s, i+1, j) || isValid(s, i, j-1); + } + i++; + j--; + } + return true; + } + public boolean isValid(String s, int i, int j) { + while(i < j) { + if(s.charAt(i) != s.charAt(j)) { + return false; + } + i++; + j--; + } + return true; + } +} +``` + diff --git "a/Java/alg/lc/69.x\347\232\204\345\271\263\346\226\271\346\240\271.md" "b/Java/alg/lc/69.x\347\232\204\345\271\263\346\226\271\346\240\271.md" new file mode 100644 index 00000000..6ffc6674 --- /dev/null +++ "b/Java/alg/lc/69.x\347\232\204\345\271\263\346\226\271\346\240\271.md" @@ -0,0 +1,96 @@ +# 69. x 的平方根 + +[url](https://leetcode-cn.com/problems/sqrtx/) + +## 题目 + +实现 `int sqrt(int x)` 函数。 + +计算并返回 x 的平方根,其中 x 是非负整数。 + +由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。 + +``` +输入: 4 +输出: 2 +输入: 8 +输出: 2 +说明: 8 的平方根是 2.82842..., +由于返回类型是整数,小数部分将被舍去。 +``` + +## 方法 + +二分法的思想 + +- 在`(1, x)`的区间中找一个res可以满足题意。 +- 左点为1,右点为x,中点m为`l + (r - l) / 2` +- 乘法可能会溢出,因此改为`m`和`x / m`的判断 +- 如果`m >= x / m`,则说明res在左边,那么另`r = m`,限定r的范围 +- 如果`m < x / m`,则说明res在右边,那么另`l = m + 1` +- 最后判断`m`和`x / m`的值,若大于后者,则res为`l - 1`, 否则为`l` + +## code + +### js + +```js +let mySqrt = x => { + let l = 1, r = x; + while (l < r) { + let m = Math.floor(l + (r - l) / 2); + if (m >= Math.floor(x / m)) { + r = m; + } else { + l = m + 1; + } + } + return l > Math.floor(x / l) ? (l - 1) : l; +} + +console.log(mySqrt(4)); +console.log(mySqrt(8)); +``` + +### go + +```go +func mySqrt(x int) int { + l, r := 1, x + for l < r { + m := l + (r - l) / 2 + if m >= x/m { + r = m + } else { + l = m + 1 + } + } + if l > x / l { + return l - 1 + } else { + return l + } +} +``` + + + +### java + +```java +class Solution { + public int mySqrt(int x) { + int l = 1, r = x; + while(l= x / mid){ // 乘法可能会溢出, 改为除法 + r = mid; + }else{ + l = mid+1; + } + } + return l>x/l ? (l-1):l; // 乘法可能会溢出, 改为除法 + } +} +``` + diff --git "a/Java/alg/lc/693.\344\272\244\346\233\277\344\275\215\344\272\214\350\277\233\345\210\266\346\225\260.md" "b/Java/alg/lc/693.\344\272\244\346\233\277\344\275\215\344\272\214\350\277\233\345\210\266\346\225\260.md" new file mode 100644 index 00000000..204b8926 --- /dev/null +++ "b/Java/alg/lc/693.\344\272\244\346\233\277\344\275\215\344\272\214\350\277\233\345\210\266\346\225\260.md" @@ -0,0 +1,65 @@ +# 693. 交替位二进制数 + + +[url](https://leetcode-cn.com/problems/binary-number-with-alternating-bits/) + + +## 题目 +给定一个正整数,检查它的二进制表示是否总是 0、1 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。 + +``` +输入:n = 5 +输出:true +解释:5 的二进制表示是:101 +输入:n = 7 +输出:false +解释:7 的二进制表示是:111. +输入:n = 11 +输出:false +解释:11 的二进制表示是:1011. +输入:n = 10 +输出:true +解释:10 的二进制表示是:1010. +输入:n = 3 +输出:false +``` + + +## 方法 + + +## code + +### js + +```js +let hasAlternatingBits = n => { + let a = (n ^ (n >> 1)); + return (a & (a + 1)) === 0; +}; +console.log(hasAlternatingBits(5)) +console.log(hasAlternatingBits(7)) +console.log(hasAlternatingBits(11)) +console.log(hasAlternatingBits(10)) +``` + +### go + +```go +func hasAlternatingBits(n int) bool { + a := n ^ (n >> 1) + return (a & (a + 1)) == 0 +} +``` + +### java + +```java +class Solution { + public boolean hasAlternatingBits(int n) { + int a = (n ^ (n >> 1)); + return (a & (a + 1)) == 0; + } +} +``` + diff --git "a/Java/alg/lc/696.\350\256\241\346\225\260\344\272\214\350\277\233\345\210\266\345\255\220\344\270\262.md" "b/Java/alg/lc/696.\350\256\241\346\225\260\344\272\214\350\277\233\345\210\266\345\255\220\344\270\262.md" new file mode 100644 index 00000000..0fa3b669 --- /dev/null +++ "b/Java/alg/lc/696.\350\256\241\346\225\260\344\272\214\350\277\233\345\210\266\345\255\220\344\270\262.md" @@ -0,0 +1,90 @@ +# 696. 计数二进制子串 + + +[url](https://leetcode-cn.com/problems/count-binary-substrings/) + + +## 题目 +给定一个字符串 s,计算具有相同数量 0 和 1 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是连续的。 + +重复出现的子串要计算它们出现的次数。 + +``` +输入: "00110011" +输出: 6 +解释: 有6个子串具有相同数量的连续1和0:“0011”,“01”,“1100”,“10”,“0011” 和 “01”。 +请注意,一些重复出现的子串要计算它们出现的次数。 +另外,“00110011”不是有效的子串,因为所有的0(和1)没有组合在一起。 + +输入: "10101" +输出: 4 +解释: 有4个子串:“10”,“01”,“10”,“01”,它们具有相同数量的连续1和0。 +``` + + +## 方法 + + +## code + +### js + +```js +let countBinarySubstrings = s => { + let preLen = 0, curLen = 1, count = 0; + for (let i = 1; i < s.length; i++) { + if (s[i] === s[i - 1]) + curLen++; + else { + preLen = curLen; + curLen = 1; + } + if (preLen >= curLen) + count++; + } + return count; +}; +console.log(countBinarySubstrings("00110011")) +``` + +### go + +```go +func countBinarySubstrings(s string) int { + preLen, curLen, count := 0, 1, 0 + for i := 1; i < len(s); i++ { + if s[i] == s[i-1] { + curLen++ + } else { + preLen = curLen + curLen = 1 + } + if preLen >= curLen { + count++ + } + } + return count +} +``` + +### java + +```java +class Solution { + public int countBinarySubstrings(String s) { + int preLen = 0, curLen = 1, count = 0; + for (int i = 1; i < s.length(); i++) { + if (s.charAt(i) == s.charAt(i - 1)) curLen++; + else { + preLen = curLen; + curLen = 1; + } + if (preLen >= curLen) { + count++; + } + } + return count; + } +} +``` + diff --git "a/Java/alg/lc/7.\346\225\264\346\225\260\345\217\215\350\275\254.md" "b/Java/alg/lc/7.\346\225\264\346\225\260\345\217\215\350\275\254.md" new file mode 100644 index 00000000..3dd58465 --- /dev/null +++ "b/Java/alg/lc/7.\346\225\264\346\225\260\345\217\215\350\275\254.md" @@ -0,0 +1,76 @@ +# 7.整数反转 + +[url](https://leetcode-cn.com/problems/reverse-integer/) + +## 题目 + +给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。 + +``` +输入: 123 +输出: 321 + +输入: -123 +输出: -321 + +输入: 120 +输出: 21 +``` + +## 方法 + +y = y * 10 + x % 10 +x /= 10 + +二者使用效果极佳 + +- 这个题很多方法的,就用数学公式解答吧 +- 你可以举个例子,假如123,怎么才能成为321呢?字符串的话,反转即可,关键这不是字符串了,当然你想说,先转成字符串,然后字符串反转,然后变为int,岂不是很方便?嘿嘿,的确很方便。 +- 利用y = y * 10 + x % 10,123 % 10 = 3,是不是将3提炼出来了?那么利用x /= 10,是不是x=12了? y = 3, 假如为再来一次这样的操作呢?12 % 10 = 2, x = 1, y = 30 + 2,不用继续往下说了吧? + + +## code + +### js + +```js{cmd="node"} +let reverse = function(x) { + let ret = 0; + while (x !== 0) { + ret = ret * 10 + x % 10; + x = (x / 10) | 0; // x / 10 去除末位,| 0 强制转换为32位有符号整数。 + // 通过 | 0 取整,无论正负,只移除小数点部分(正数向下取整,负数向上取整)。 + } + return (ret | 0) === ret ? ret : 0; // ret | 0 超过32位的整数转换结果不等于自身,可用作溢出判断。 +}; +console.log(reverse(123)); +``` + +### go + +```go +func reverse(x int) int { + ret := 0 + for x != 0 { + ret = ret * 10 + x % 10 + x /= 10 + } + return ret +} +``` + +### java + +```java +class Solution { + public int reverse(int x) { + long ret = 0; + while (x != 0) { + ret = ret * 10 + x % 10; + x /= 10; + } + return (int) ret == ret ? (int) ret : 0; + } +} +``` + diff --git "a/Java/alg/lc/70.\347\210\254\346\245\274\346\242\257.md" "b/Java/alg/lc/70.\347\210\254\346\245\274\346\242\257.md" new file mode 100644 index 00000000..db374272 --- /dev/null +++ "b/Java/alg/lc/70.\347\210\254\346\245\274\346\242\257.md" @@ -0,0 +1,92 @@ +# 70. 爬楼梯 + +[url](https://leetcode-cn.com/problems/climbing-stairs/) + +## 题目 + +假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 + +每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? + +注意:给定 n 是一个正整数。 + +``` +输入: 2 +输出: 2 +解释: 有两种方法可以爬到楼顶。 +1. 1 阶 + 1 阶 +2. 2 阶 +输入: 3 +输出: 3 +解释: 有三种方法可以爬到楼顶。 +1. 1 阶 + 1 阶 + 1 阶 +2. 1 阶 + 2 阶 +3. 2 阶 + 1 阶 +``` + +## 方法 + +从下往上迭代 + +- 当`n=1,n=2`的时候,结果为`1,2`。 +- 从`n=3`开始,计算`cur = pre2 + pre1` +- 接着替换pre2,pre1 +- 迭代完毕,返回pre1 + + +## code + +### js + +```js +let climbStairs = n => { + if (n <= 2) return n; + let pre2 = 1, pre1 = 2; + for (let i = 3; i <= n; i++) { + let cur = pre2 + pre1; + pre2 = pre1; + pre1 = cur; + } + return pre1; +} +console.log(climbStairs(2)); +console.log(climbStairs(3)); +console.log(climbStairs(4)); +``` + +### go + +```go +func climbStairs(n int) int { + if n <= 2 { + return n + } + pre2, pre1 := 1, 2 + for i := 3; i <= n; i++ { + cur := pre2 + pre1 + pre2 = pre1 + pre1 = cur + } + return pre1 +} +``` + + + +### java + +```java +class Solution { + public int climbStairs(int n) { + if (n <= 2) return n; + int pre2 = 1, pre1 = 2; + for (int i = 3; i <= n; i++) { + int cur = pre2 + pre1; + pre2 = pre1; + pre1 = cur; + } + return pre1; + } +} +``` + diff --git "a/Java/alg/lc/704.\344\272\214\345\210\206\346\237\245\346\211\276.md" "b/Java/alg/lc/704.\344\272\214\345\210\206\346\237\245\346\211\276.md" new file mode 100644 index 00000000..7753a9e2 --- /dev/null +++ "b/Java/alg/lc/704.\344\272\214\345\210\206\346\237\245\346\211\276.md" @@ -0,0 +1,86 @@ +# 704. 二分查找 + + +[url](https://leetcode-cn.com/problems/binary-search/) + + +## 题目 +给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。 + +``` +输入: nums = [-1,0,3,5,9,12], target = 9 +输出: 4 +解释: 9 出现在 nums 中并且下标为 4 +输入: nums = [-1,0,3,5,9,12], target = 2 +输出: -1 +解释: 2 不存在 nums 中因此返回 -1 +``` + + +## 方法 + + +## code + +### js + +```js +let search = (nums, target) => { + if (nums === null || nums.length === 0) + return -1; + let l = 0, h = nums.length - 1; + while (l <= h) { + let m = l + Math.floor((h - l) / 2); + if (nums[m] === target) + return m; + else if (nums[m] < target) + l = m + 1; + else + h = m - 1; + } + return -1; +}; +console.log(search([-1,0,3,5,9,12], 9)) +console.log(search([-1,0,3,5,9,12], 2)) +``` + +### go + +```go +func search(nums []int, target int) int { + if nums == nil || len(nums) == 0 { + return -1 + } + l, h := 0, len(nums) - 1 + for l <= h { + m := l + (h - l) / 2 + if nums[m] == target { + return m + } else if nums[m] < target { + l = m + 1 + } else { + h = m - 1 + } + } + return -1 +} +``` + +### java + +```java +class Solution { + public int search(int[] nums, int target) { + if (nums == null || nums.length == 0) return -1; + int l = 0, h = nums.length - 1; + while (l <= h) { + int m = l + (h - l) / 2; + if (nums[m] == target) return m; + else if (nums[m] < target) l = m + 1; + else h = m - 1; + } + return -1; + } +} +``` + diff --git "a/Java/alg/lc/724.\345\257\273\346\211\276\346\225\260\347\273\204\347\232\204\344\270\255\345\277\203\344\270\213\346\240\207.md" "b/Java/alg/lc/724.\345\257\273\346\211\276\346\225\260\347\273\204\347\232\204\344\270\255\345\277\203\344\270\213\346\240\207.md" new file mode 100644 index 00000000..659f81aa --- /dev/null +++ "b/Java/alg/lc/724.\345\257\273\346\211\276\346\225\260\347\273\204\347\232\204\344\270\255\345\277\203\344\270\213\346\240\207.md" @@ -0,0 +1,97 @@ +# 724. 寻找数组的中心下标 + + +[url](https://leetcode-cn.com/problems/find-pivot-index/) + + +## 题目 +给你一个整数数组 nums,请编写一个能够返回数组 “中心下标” 的方法。 + +数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。 + +如果数组不存在中心下标,返回 -1 。如果数组有多个中心下标,应该返回最靠近左边的那一个。 + +注意:中心下标可能出现在数组的两端。 + + +``` +输入:nums = [1, 7, 3, 6, 5, 6] +输出:3 +解释: +中心下标是 3 。 +左侧数之和 (1 + 7 + 3 = 11), +右侧数之和 (5 + 6 = 11) ,二者相等。 +输入:nums = [1, 2, 3] +输出:-1 +解释: +数组中不存在满足此条件的中心下标。 +输入:nums = [2, 1, -1] +输出:0 +解释: +中心下标是 0 。 +下标 0 左侧不存在元素,视作和为 0 ; +右侧数之和为 1 + (-1) = 0 ,二者相等。 +``` + + +## 方法 + + +## code + +### js + +```js +let pivotIndex = nums => { + let sum = 0, leftSum = 0; + nums.forEach(num => sum += num); + for (const [idx, val] of nums.entries()) { + if (sum - val === leftSum * 2) + return idx; + else + leftSum += val; + } + return -1; +}; +console.log(pivotIndex([1, 7, 3, 6, 5, 6])) +console.log(pivotIndex([1, 2, 3])) +``` + +### go + +```go +func pivotIndex(nums []int) int { + sum ,leftSum := 0, 0 + for _, v := range nums { + sum += v + } + for i, v := range nums { + if sum-v == leftSum*2 { + return i + } else { + leftSum += v + } + } + return -1 +} +``` + +### java + +```java +class Solution { + public int pivotIndex(int[] nums) { + int sum = 0, leftSum = 0; + for (int num : nums) sum += num; + for (int i = 0; i < nums.length; i++) { + if(sum - nums[i] == leftSum * 2) { + return i; + } else { + leftSum += nums[i]; + } + } + return -1; + } +} +``` + diff --git "a/Java/alg/lc/74.\346\220\234\347\264\242\344\272\214\347\273\264\347\237\251\351\230\265.md" "b/Java/alg/lc/74.\346\220\234\347\264\242\344\272\214\347\273\264\347\237\251\351\230\265.md" new file mode 100644 index 00000000..ed5ebafc --- /dev/null +++ "b/Java/alg/lc/74.\346\220\234\347\264\242\344\272\214\347\273\264\347\237\251\351\230\265.md" @@ -0,0 +1,99 @@ +# 74. 搜索二维矩阵 + +[url](https://leetcode-cn.com/problems/search-a-2d-matrix/) + +## 题目 + +编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性: + +- 每行中的整数从左到右按升序排列。 +- 每行的第一个整数大于前一行的最后一个整数。 + +![](https://assets.leetcode.com/uploads/2020/10/05/mat.jpg) + +``` +输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3 +输出:true +``` + +![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/11/25/mat2.jpg) + +``` +输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13 +输出:false +``` + + + +## 方法 + + + + +## code + +### js + +```js +let searchMatrix = (matrix, target) => { + if (matrix === null || matrix.length === 0 || matrix[0].length === 0) return false; + let rows = matrix.length, cols = matrix[0].length; + let c = cols - 1, r = 0; + while (c >= 0 && r < rows) { + if (matrix[r][c] === target) + return true; + else if (matrix[r][c] < target) + r++; + else + c--; + } + return false +} +``` + +### go + +```go +func searchMatrix(matrix [][]int, target int) bool { + if len(matrix) == 0 || len(matrix[0]) == 0 { + return false + } + rows, cols := len(matrix), len(matrix[0]) + c, r := cols - 1, 0 + for c >= 0 && r < rows { + if matrix[r][c] == target { + return true + } else if matrix[r][c] < target { + r++ + } else { + c-- + } + } + return false +} +``` + + + +### java + +```java +class Solution { + public boolean searchMatrix(int[][] matrix, int target) { + if (matrix == null || matrix.length == 0 || matrix[0].length == 0) + return false; + int rows = matrix.length, cols = matrix[0].length; + int c = cols - 1, r = 0; + while (c >= 0 && r < rows) { + if (matrix[r][c] == target) + return true; + else if (matrix[r][c] < target) + r++; + else + c--; + } + return false; + } +} +``` + diff --git "a/Java/alg/lc/744.\345\257\273\346\211\276\346\257\224\347\233\256\346\240\207\345\255\227\346\257\215\345\244\247\347\232\204\346\234\200\345\260\217\345\255\227\346\257\215.md" "b/Java/alg/lc/744.\345\257\273\346\211\276\346\257\224\347\233\256\346\240\207\345\255\227\346\257\215\345\244\247\347\232\204\346\234\200\345\260\217\345\255\227\346\257\215.md" new file mode 100644 index 00000000..a7c43afe --- /dev/null +++ "b/Java/alg/lc/744.\345\257\273\346\211\276\346\257\224\347\233\256\346\240\207\345\255\227\346\257\215\345\244\247\347\232\204\346\234\200\345\260\217\345\255\227\346\257\215.md" @@ -0,0 +1,110 @@ +# 744.寻找比目标字母大的最小字母 + + +[url](https://leetcode-cn.com/problems/find-smallest-letter-greater-than-target/) + + +## 题目 +给你一个排序后的字符列表 letters ,列表中只包含小写英文字母。另给出一个目标字母 target,请你寻找在这一有序列表里比目标字母大的最小字母。 + +在比较时,字母是依序循环出现的。举个例子: + +如果目标字母 target = 'z' 并且字符列表为 letters = ['a', 'b'],则答案返回 'a' + +``` +letters = ["c", "f", "j"] +target = "a" +输出: "c" + +输入: +letters = ["c", "f", "j"] +target = "c" +输出: "f" + +输入: +letters = ["c", "f", "j"] +target = "d" +输出: "f" + +输入: +letters = ["c", "f", "j"] +target = "g" +输出: "j" + +输入: +letters = ["c", "f", "j"] +target = "j" +输出: "c" + +输入: +letters = ["c", "f", "j"] +target = "k" +输出: "c" +``` + + +## 方法 + + +## code + +### js + +```js +let nextGreatestLetter = (letters, target) => { + let n = letters.length; + let l = 0, h = n - 1; + while (l <= h) { + let m = l + Math.floor((h - l) / 2); + if (letters[m] <= target) + l = m + 1; + else + h = m - 1; + } + return l < n ? letters[l] : letters[0]; +}; +console.log(nextGreatestLetter(["c", "f", "j"], "a")) +console.log(nextGreatestLetter(["c", "f", "j"], "c")) +console.log(nextGreatestLetter(["c", "f", "j"], "d")) +console.log(nextGreatestLetter(["c", "f", "j"], "g")) +``` + +### go + +```go +func nextGreatestLetter(letter []byte, target byte) byte { + n := len(letter) + l, h := 0, n - 1 + for l <= h { + m := l + (h - l) / 2 + if letter[m] <= target { + l = m + 1 + } else { + h = m - 1 + } + } + if l < n { + return letter[l] + } else { + return letter[0] + } +} +``` + +### java + +```java +class Solution { + public char nextGreatestLetter(char[] letters, char target) { + int n = letters.length; + int l = 0, h = n - 1; + while (l <= h) { + int m = l + (h - l) / 2; + if (letters[m] <= target) l = m + 1; + else h = m - 1; + } + return l < n ? letters[l] : letters[0]; + } +} +``` + diff --git "a/Java/alg/lc/747.\350\207\263\345\260\221\346\230\257\345\205\266\344\273\226\346\225\260\345\255\227\344\270\244\345\200\215\347\232\204\346\234\200\345\244\247\346\225\260.md" "b/Java/alg/lc/747.\350\207\263\345\260\221\346\230\257\345\205\266\344\273\226\346\225\260\345\255\227\344\270\244\345\200\215\347\232\204\346\234\200\345\244\247\346\225\260.md" new file mode 100644 index 00000000..857a22e6 --- /dev/null +++ "b/Java/alg/lc/747.\350\207\263\345\260\221\346\230\257\345\205\266\344\273\226\346\225\260\345\255\227\344\270\244\345\200\215\347\232\204\346\234\200\345\244\247\346\225\260.md" @@ -0,0 +1,96 @@ +# 747. 至少是其他数字两倍的最大数 + + +[url](https://leetcode-cn.com/problems/largest-number-at-least-twice-of-others/) + + +## 题目 +在一个给定的数组nums中,总是存在一个最大元素 。 + +查找数组中的最大元素是否至少是数组中每个其他数字的两倍。 + +如果是,则返回最大元素的索引,否则返回-1。 + + +``` +输入: nums = [3, 6, 1, 0] +输出: 1 +解释: 6是最大的整数, 对于数组中的其他整数, +6大于数组中其他元素的两倍。6的索引是1, 所以我们返回1. +输入: nums = [1, 2, 3, 4] +输出: -1 +解释: 4没有超过3的两倍大, 所以我们返回 -1. +``` + + +## 方法 + + +## code + +### js + +```js +let dominantIndex = nums => { + // 将第二大的元素的两倍与最大值做比较 + // 如果最大值大,那么就能证明最大值大于所有元素的两倍 + let max = 0, idx = 0, less = 0; + for (const [i, val] of nums.entries()) { + if (val > max) { + less = max; + max = val; + idx = i; + } else if (val > less && val !== max) { + less = val; + } + } + return max >= (less * 2) ? idx : -1; +}; +console.log(dominantIndex([3, 6, 1, 0])) +console.log(dominantIndex([1, 2, 3, 4])) +``` + +### go + +```go +func dominantIndex(nums []int) int { + max, idx, less := 0 , 0, 0 + for i, v := range nums { + if v > max { + less = max + max = v + idx = i + } else if v > less && v != max { + less = v + } + } + if max >= (less * 2) { + return idx + } else { + return -1 + } +} +``` + +### java + +```java +class Solution { + public int dominantIndex(int[] nums) { + // 将第二大的元素的两倍与最大值做比较 + // 如果最大值大,那么就能证明最大值大于所有元素的两倍 + int max = 0, idx = 0, less = 0; + for (int i = 0; i < nums.length; i++) { + if (nums[i] > max) { + less = max; + max = nums[i]; + idx = i; + } else if(nums[i] > less && nums[i] != max) { + less = nums[i]; + } + } + return max >= (less * 2) ? idx : -1; + } +} +``` + diff --git "a/Java/alg/lc/75.\351\242\234\350\211\262\345\210\206\347\261\273.md" "b/Java/alg/lc/75.\351\242\234\350\211\262\345\210\206\347\261\273.md" new file mode 100644 index 00000000..38c6646e --- /dev/null +++ "b/Java/alg/lc/75.\351\242\234\350\211\262\345\210\206\347\261\273.md" @@ -0,0 +1,98 @@ +# 75. 颜色分类 + +[url](https://leetcode-cn.com/problems/sort-colors/) + +## 题目 + +给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 + + +``` +输入:nums = [2,0,2,1,1,0] +输出:[0,0,1,1,2,2] +输入:nums = [2,0,1] +输出:[0,1,2] +输入:nums = [0] +输出:[0] +``` + + + + +## 方法 + + + + +## code + +### js + +```js +let sortColors = nums => { + let swap = (a, i, j) => { + let t = a[i]; + a[i] = a[j]; + a[j] = t; + } + let zero = -1, one = 0, two = nums.length; + while (one < two) { + if (nums[one] === 0) + swap(nums, ++zero, one++); + else if (nums[one] === 2) + swap(nums, --two, one); + else + ++one; + } +} +``` + +### go + +```go +func sortColors(nums []int) { + swap := func(a []int, i, j int) { + a[i], a[j] = a[j], a[i] + } + zero, one, two := -1, 0, len(nums) + for one < two { + if nums[one] == 0 { + zero++ + swap(nums, zero, one) + one++ + } else if nums[one] == 2 { + two-- + swap(nums, two, one) + } else { + one++ + } + } +} +``` + + + +### java + +```java +class Solution { + public void sortColors(int[] nums) { + int zero = -1, one = 0, two = nums.length; + while (one < two) { + if (nums[one] == 0) { + swap(nums, ++zero, one++); + } else if (nums[one] == 2){ + swap(nums, --two, one); + } else { + ++one; + } + } + } + private void swap(int[] a, int i, int j) { + int t = a[i]; + a[i] = a[j]; + a[j] = t; + } +} +``` + diff --git "a/Java/alg/lc/77.\347\273\204\345\220\210.md" "b/Java/alg/lc/77.\347\273\204\345\220\210.md" new file mode 100644 index 00000000..8c88c2db --- /dev/null +++ "b/Java/alg/lc/77.\347\273\204\345\220\210.md" @@ -0,0 +1,101 @@ +# 77. 组合 + +[url](https://leetcode-cn.com/problems/combinations/) + +## 题目 + +给定两个整数 *n* 和 *k*,返回 1 ... *n* 中所有可能的 *k* 个数的组合。 + + +``` +输入: n = 4, k = 2 +输出: +[ + [2,4], + [3,4], + [2,3], + [1,2], + [1,3], + [1,4], +] +``` + + + + +## 方法 + + + + +## code + +### js + +```js +let combine = (n, k) => { + let res = []; + let dfs = (list, start, k, n) => { + if (k === 0) { + res.push(list.slice()); + return + } + for (let i = start; i <= n - k + 1; i++) { + list.push(i); + dfs(list, i + 1, k - 1, n); + list.pop(); + } + } + dfs([], 1, k, n); + return res +} +``` + +### go + +```go +func combine(n, k int) [][]int { + var res [][]int + var dfs func(list []int, start, k, n int) + dfs = func(list []int, start, k, n int) { + if k == 0 { + res = append(res, append([]int{}, list...)) + return + } + for i := start; i <= n-k+1; i++ { + list = append(list, i) + dfs(list, i+1, k-1, n) + list = list[0:len(list)-1] + } + } + dfs([]int{}, 1, k, n) + return res +} +``` + + + +### java + +```java +class Solution { + public List> combine(int n, int k) { + List> combinations = new ArrayList<>(); + List combineList = new ArrayList<>(); + backtracking(combineList, combinations, 1, k, n); + return combinations; + } + private void backtracking(List combineList, List> combinations, int start, int k, final int n) { + if (k == 0) { + combinations.add(new ArrayList<>(combineList)); + return; + } + for (int i = start; i <= n - k + 1; i++) { + combineList.add(i); + backtracking(combineList, combinations, i + 1, k - 1, n); + combineList.remove(combineList.size() - 1); + } + } +} +``` + diff --git "a/Java/alg/lc/78.\345\255\220\351\233\206.md" "b/Java/alg/lc/78.\345\255\220\351\233\206.md" new file mode 100644 index 00000000..1795a3be --- /dev/null +++ "b/Java/alg/lc/78.\345\255\220\351\233\206.md" @@ -0,0 +1,107 @@ +# 78. 子集 + +[url](https://leetcode-cn.com/problems/subsets/) + +## 题目 + +给你一个整数数组 `nums` ,数组中的元素 **互不相同** 。返回该数组所有可能的子集(幂集)。 + +解集 **不能** 包含重复的子集。你可以按 **任意顺序** 返回解集。 + + +``` +输入:nums = [1,2,3] +输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] +输入:nums = [0] +输出:[[],[0]] +``` + + + + +## 方法 + + + + +## code + +### js + +```js +let subsets = nums => { + let ret = []; + if (nums === null || nums.length === 0) + return ret; + let dfs = (start, size, list) => { + if (list.length === size) { + ret.push(list.slice()); + return; + } + for (let i = start; i < nums.length; i++) { + list.push(nums[i]); + dfs(i+1, size, list); + list.pop() + } + } + for (let i = 0; i <= nums.length; i++) { + dfs(0, i, []); + } + return ret +} +``` + +### go + +```go +func subsets(nums []int) [][]int { + var res [][]int + var dfs func(nums []int, start, size int, list []int) + dfs = func(nums []int, start, size int, list []int) { + if len(list) == size { + res = append(res, append([]int{}, list...)) + return + } + for i := start; i < len(nums); i++ { + list = append(list, nums[i]) + dfs(nums, i+1, size, list) + list = list[:len(list)-1] + } + } + for i := 0; i <= len(nums); i++ { + dfs(nums, 0, i, []int{}) + } + return res +} +``` + + + +### java + +```java +class Solution { + List> ret = new ArrayList<>(); + public List> subsets(int[] nums) { + if (nums == null || nums.length == 0) + return ret; + List list = new ArrayList<>(); + for (int i = 0; i <= nums.length; i++) { + dfs(nums, 0, i, list); + } + return ret; + } + public void dfs(int[] nums, int start, int size, List list){ + if (list.size() == size){ + ret.add(new ArrayList(list)); + return; + } + for (int i = start; i < nums.length; i++) { + list.add(nums[i]); + dfs(nums, i + 1, size, list); + list.remove(list.size() - 1); + } + } +} +``` + diff --git "a/Java/alg/lc/79.\345\215\225\350\257\215\346\220\234\347\264\242.md" "b/Java/alg/lc/79.\345\215\225\350\257\215\346\220\234\347\264\242.md" new file mode 100644 index 00000000..ebefacf6 --- /dev/null +++ "b/Java/alg/lc/79.\345\215\225\350\257\215\346\220\234\347\264\242.md" @@ -0,0 +1,137 @@ +# 79. 单词搜索 + +[url](https://leetcode-cn.com/problems/word-search/) + +## 题目 + +给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。 + +单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 + +![](https://assets.leetcode.com/uploads/2020/11/04/word2.jpg) + + +``` +输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED" +输出:true +``` + + + + +## 方法 + + + + +## code + +### js + +```js +let exist = (board, word) => { + if (board === null || board.length === 0 || board[0].length === 0) + return false; + let next = [[1,0],[-1,0],[0,1],[0,-1]]; + let m = board.length, n = board[0].length; + let marked = Array(m).fill(false).map(() => Array(n).fill(false)); + let dfs = (word, pathLen, i, j) => { + if (pathLen === word.length) + return true; + if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] !== word[pathLen] || marked[i][j]) + return false; + marked[i][j] = true; + for (let d of next) { + if (dfs(word, pathLen + 1, i + d[0], j + d[1])) + return true + } + marked[i][j] = false; + return false + } + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + if (dfs(word, 0, i, j)) + return true + } + } + return false +} +``` + +### go + +```go +func exist(board [][]byte, word string) bool { + next := [][]int{{1,0}, {-1,0}, {0,1}, {0,-1}} + m, n := len(board), len(board[0]) + marked := make([][]bool, m) + for i := range marked { + marked[i] = make([]bool, n) + } + var dfs func(board [][]byte, word string, pathLen, i, j int) bool + dfs = func(board [][]byte, word string, pathLen, i, j int) bool { + if pathLen == len(word) { + return true + } + if i<0 || i>=m || j<0 || j>=n || board[i][j] != word[pathLen] || marked[i][j] { + return false + } + marked[i][j] = true + for _, d := range next { + if dfs(board, word, pathLen+1, i+d[0], j+d[1]) { + return true + } + } + marked[i][j] = false + return false + } + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if dfs(board, word, 0, i, j) { + return true + } + } + } + return false +} +``` + + + +### java + +```java +class Solution { + private int[][] next = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + private int m, n; + public boolean exist(char[][] board, String word) { + if (board == null || board.length == 0 || board[0].length == 0) + return false; + this.m = board.length; + this.n = board[0].length; + boolean[][] marked = new boolean[m][n]; + for (int i = 0; i < m; i++){ + for (int j = 0; j < n; j++){ + if (dfs(board, word, marked, 0, i, j)) + return true; + } + } + return false; + } + private boolean dfs(char[][] board, String word, boolean[][] marked, int pathLen, int i, int j){ + if (pathLen == word.length()) + return true; + if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] != word.charAt(pathLen) || marked[i][j]) + return false; + + marked[i][j] = true; + for (int[] n : next) { + if (dfs(board, word, marked, pathLen +1, i + n[0], j + n[1])) + return true; + } + marked[i][j] = false; + return false; + } +} +``` + diff --git "a/Java/alg/lc/82.\345\210\240\351\231\244\346\216\222\345\272\217\351\223\276\350\241\250\344\270\255\347\232\204\351\207\215\345\244\215\345\205\203\347\264\2402.md" "b/Java/alg/lc/82.\345\210\240\351\231\244\346\216\222\345\272\217\351\223\276\350\241\250\344\270\255\347\232\204\351\207\215\345\244\215\345\205\203\347\264\2402.md" new file mode 100644 index 00000000..8f36385c --- /dev/null +++ "b/Java/alg/lc/82.\345\210\240\351\231\244\346\216\222\345\272\217\351\223\276\350\241\250\344\270\255\347\232\204\351\207\215\345\244\215\345\205\203\347\264\2402.md" @@ -0,0 +1,88 @@ +# 82. 删除排序链表中的重复元素 II + +[url](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/) + +## 题目 + +存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中 没有重复出现 的数字。 + +返回同样按升序排列的结果链表。 + +![](https://assets.leetcode.com/uploads/2021/01/04/linkedlist1.jpg) + + +``` +输入:head = [1,2,3,3,4,4,5] +输出:[1,2,5] +``` + + + + +## 方法 + + + + +## code + +### js + +```js +let deleteDuplicates = head => { + if (head === null || head.next === null) + return head; + let next = head.next; + if (head.val === next.val) { + while (next !== null && head.val === next.val) + next = next.next; + return deleteDuplicates(head); + } else { + head.next = deleteDuplicates(head.next); + return head; + } +} +``` + +### go + +```go +func deleteDuplicates2(head *ListNode) *ListNode { + if head == nil || head.Next == nil { + return head + } + next := head.Next + if head.Val == next.Val { + for next != nil && head.Val == next.Val { + next = next.Next + } + return deleteDuplicates2(next) + } else { + head.Next = deleteDuplicates2(head.Next) + return head + } +} +``` + + + +### java + +```java +class Solution { + public ListNode deleteDuplicates(ListNode head) { + if (head == null || head.next == null) + return head; + ListNode next = head.next; + if (head.val == next.val) { + while (next != null && head.val == next.val) + next = next.next; + return deleteDuplicates(next); + } else { + head.next = deleteDuplicates(head.next); + return head; + } + } +} +``` + diff --git "a/Java/alg/lc/83.\345\210\240\351\231\244\346\216\222\345\272\217\351\223\276\350\241\250\344\270\255\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" "b/Java/alg/lc/83.\345\210\240\351\231\244\346\216\222\345\272\217\351\223\276\350\241\250\344\270\255\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" new file mode 100644 index 00000000..ac6c4a8e --- /dev/null +++ "b/Java/alg/lc/83.\345\210\240\351\231\244\346\216\222\345\272\217\351\223\276\350\241\250\344\270\255\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" @@ -0,0 +1,66 @@ +# 83. 删除排序链表中的重复元素 + +[url](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/) + +## 题目 + +给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。 + +``` +输入: 1->1->2 +输出: 1->2 +输入: 1->1->2->3->3 +输出: 1->2->3 +``` + +## 方法 + +递归 + +- 递归结束条件:要么为空,要么`head.next`为空 +- 递归到最后,返回时候判断当前节点与下一个节点的val是否相等 +- 如果相等,则返回下一个节点 +- 如果不相等,则返回当前节点 + + +## code + +### js + +```js +let deleteDuplicates = head => { + if (head === undefined || head.next == null) return head; + head.next = deleteDuplicates(head.next); + return head.val === head.next.val ? head.next : head; +} +``` + +### go + +```go +func deleteDuplicates(head *ListNode) *ListNode { + if head == nil || head.Next == nil{ + return head + } + head.Next = deleteDuplicates(head.Next) + if head.Val == head.Next.Val { + return head.Next + } + return head +} +``` + + + +### java + +```java +class Solution { + public ListNode deleteDuplicates(ListNode head) { + if (head == null || head.next == null) return head; + head.next = deleteDuplicates(head.next); + return head.val == head.next.val ? head.next : head; + } +} +``` + diff --git "a/Java/alg/lc/836.\347\237\251\345\275\242\351\207\215\345\217\240.md" "b/Java/alg/lc/836.\347\237\251\345\275\242\351\207\215\345\217\240.md" new file mode 100644 index 00000000..89b32c48 --- /dev/null +++ "b/Java/alg/lc/836.\347\237\251\345\275\242\351\207\215\345\217\240.md" @@ -0,0 +1,74 @@ +# 836. 矩形重叠 + + + +[url](https://leetcode-cn.com/problems/rectangle-overlap/) + + +## 题目 +矩形以列表 `[x1, y1, x2, y2]` 的形式表示,其中 `(x1, y1)` 为左下角的坐标,`(x2, y2)` 是右上角的坐标。矩形的上下边平行于 `x` 轴,左右边平行于 `y` 轴。 + +如果相交的面积为 正 ,则称两矩形重叠。需要明确的是,只在角或边接触的两个矩形不构成重叠。 + +给出两个矩形 `rec1` 和 `rec2` 。如果它们重叠,返回 `true`;否则,返回 `false` 。 + + +``` +输入:rec1 = [0,0,2,2], rec2 = [1,1,3,3] +输出:true +输入:rec1 = [0,0,1,1], rec2 = [1,0,2,1] +输出:false +输入:rec1 = [0,0,1,1], rec2 = [2,2,3,3] +输出:false +``` + + +## 方法 + + +## code + +### js + +```js +let isRectangleOverlap = (rec1, rec2) => { + if (rec2[1] >= rec1[3] || rec1[1] >= rec2[3]) + return false; + return !(rec1[0] >= rec2[2] || rec1[2] <= rec2[0]); + +}; +console.log(isRectangleOverlap([0,0,2,2], [1,1,3,3])) +console.log(isRectangleOverlap([0,0,1,1], [1,0,2,1])) +console.log(isRectangleOverlap([0,0,1,1], [2,2,3,3])) +``` + +### go + +```go +func isRectangleOverlap(rec1 []int, rec2 []int) bool { + if rec2[1] >= rec1[3] || rec1[1] >= rec2[3] { + return false + } + if rec1[0] >= rec2[2] || rec1[2] <= rec2[0] { + return false + } + return true +} +``` + +### java + +```java +class Solution { + public boolean isRectangleOverlap(int[] rec1, int[] rec2) { + if (rec2[1] >= rec1[3] || rec1[1] >= rec2[3]) { + return false; + } + if (rec1[0] >= rec2[2] || rec1[2] <= rec2[0]) { + return false; + } + return true; + } +} +``` + diff --git "a/Java/alg/lc/86.\345\210\206\351\232\224\351\223\276\350\241\250.md" "b/Java/alg/lc/86.\345\210\206\351\232\224\351\223\276\350\241\250.md" new file mode 100644 index 00000000..0a705da5 --- /dev/null +++ "b/Java/alg/lc/86.\345\210\206\351\232\224\351\223\276\350\241\250.md" @@ -0,0 +1,102 @@ +# 86. 分隔链表 + +[url](https://leetcode-cn.com/problems/partition-list/) + +## 题目 + +给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 + +你应当 保留 两个分区中每个节点的初始相对位置。 + +![](https://assets.leetcode.com/uploads/2021/01/04/partition.jpg) + +``` +输入:head = [1,4,3,2,5,2], x = 3 +输出:[1,2,2,4,3,5] +输入:head = [2,1], x = 2 +输出:[1,2] +``` + +## 方法 + + +## code + +### js + +```js +let partition = (head, x) => { + let dummy1, dummy2 = new ListNode(0); + let node1 = dummy1, node2 = dummy2; + while (head !== null) { + if (head.val < x) { + node1.next = head; + head = head.next; + node1 = node1.next; + node1.next = null; + } else { + node2.next = head; + head = head.next; + node2 = node2.next; + node2.next = null; + } + } + node1.nex = dummy2.next; + return dummy1.next; +} +``` + +### go + +```go +func partition(head *ListNode, x int) *ListNode { + if head == nil {return nil} + dummy1, dummy2 := &ListNode{}, &ListNode{} + cur1, cur2 := dummy1, dummy2 + for head != nil { + if head.Val < x { + cur1.Next = head + head = head.Next + cur1 = cur1.Next + cur1.Next = nil + } else { + cur2.Next = head + head = head.Next + cur2 = cur2.Next + cur2.Next = nil + } + } + cur1.Next = dummy2.Next + return dummy1.Next +} +``` + + + +### java + +```java +class Solution { + public ListNode partition(ListNode head, int x) { + ListNode dummy1 = new ListNode(0); + ListNode dummy2 = new ListNode(0); + ListNode node1 = dummy1, node2 = dummy2; + while (head != null) { + if (head.val < x){ + node1.next = head; + head = head.next; + node1 = node1.next; + node1.next = null; + } else { + node2.next = head; + head = head.next; + node2 = node2.next; + node2.next = null; + } + } + node1.next = dummy2.next; + return dummy1.next; + } +} +``` + diff --git "a/Java/alg/lc/876.\351\223\276\350\241\250\347\232\204\344\270\255\351\227\264\347\273\223\347\202\271.md" "b/Java/alg/lc/876.\351\223\276\350\241\250\347\232\204\344\270\255\351\227\264\347\273\223\347\202\271.md" new file mode 100644 index 00000000..ee1ac4e8 --- /dev/null +++ "b/Java/alg/lc/876.\351\223\276\350\241\250\347\232\204\344\270\255\351\227\264\347\273\223\347\202\271.md" @@ -0,0 +1,71 @@ +# 876. 链表的中间结点 + + + +[url](https://leetcode-cn.com/problems/middle-of-the-linked-list/) + + +## 题目 +给定一个头结点为 head 的非空单链表,返回链表的中间结点。 + +如果有两个中间结点,则返回第二个中间结点。 + + +``` +输入:[1,2,3,4,5] +输出:此列表中的结点 3 (序列化形式:[3,4,5]) +返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。 +注意,我们返回了一个 ListNode 类型的对象 ans,这样: +ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL. +输入:[1,2,3,4,5,6] +输出:此列表中的结点 4 (序列化形式:[4,5,6]) +由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。 +``` + + +## 方法 + + +## code + +### js + +```js +let middleNode = head => { + let p = head, q = head; + while (q !== null && q.next !== null) { + q = q.next.next; + p = p.next; + } + return p; +}; +``` + +### go + +```go +func middleNode(head *ListNode) *ListNode { + p, q := head, head + for q != nil && q.Next != nil { + q = q.Next.Next + p = p.Next + } + return p +} +``` + +### java + +```java +class Solution { + public ListNode middleNode(ListNode head) { + ListNode p = head, q = head; + while (q != null && q.next != null) { + q = q.next.next; + p = p.next; + } + return p; + } +} +``` + diff --git "a/Java/alg/lc/88.\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\346\225\260\347\273\204.md" "b/Java/alg/lc/88.\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\346\225\260\347\273\204.md" new file mode 100644 index 00000000..4c3743c6 --- /dev/null +++ "b/Java/alg/lc/88.\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\346\225\260\347\273\204.md" @@ -0,0 +1,106 @@ +# 88. 合并两个有序数组 + +[url](https://leetcode-cn.com/problems/merge-sorted-array/) + +## 题目 + +给你两个有序整数数组 `nums1` 和 `nums2`,请你将 `nums2` 合并到 `nums1` 中,使 `nums1` 成为一个有序数组。 + +初始化 `nums1` 和 `nums2` 的元素数量分别为 m 和 n 。你可以假设 `nums1` 的空间大小等于 `m + n`,这样它就有足够的空间保存来自 `nums2` 的元素。 + + +``` +输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 +输出:[1,2,2,3,5,6] +输入:nums1 = [1], m = 1, nums2 = [], n = 0 +输出:[1] +``` + +## 方法 + +两个数组合并,三个指针的思想 + +- `nums1`、`nums2`的末尾索引为`idx1 = m - 1`和`idx2 = n - 1` +- 同理,合并后的末尾索引为`idx = m + n - 1` +- 所以捏,两个数组分别从后往前遍历进行比较对应的val,四种情况 +- 如果`idx1 < 0`,说明数组1没有元素了,直接将数组2赋值 +- 如果`idx2 < 0`,说明数字2没有元素了,直接将数组1赋值 +- 如果`nums[idx1] < nums[idx2]`,因为从后往前遍历,因此将`nums[idx2]`的val赋值 +- 如果`nums[idx1] >= nums[idx2]`,将`nums[idx1]`的val赋值 + + +## code + +### js + +```js +let merge = (nums1, m, nums2, n) => { + let idx1 = m - 1, idx2 = n - 1; + let idx = m + n - 1; + while (idx1 >= 0 || idx2 >= 0) { + if (idx1 < 0) + nums1[idx--] = nums2[idx2--]; + else if (idx2 < 0) + nums1[idx--] = nums1[idx1--]; + else if (nums1[idx1] < nums2[idx2]) + nums1[idx--] = nums2[idx2--]; + else + nums1[idx--] = nums1[idx1--]; + } + console.log(nums1); +} +merge([1,2,3,0,0,0], 3, [2, 5, 6], 3) +``` + +### go + +```go +func merge(nums1 []int, m int, nums2 []int, n int) { + idx1, idx2 := m - 1, n - 1 + idx := m + n - 1 + for idx1 >= 0 || idx2 >= 0 { + if idx1 < 0 { + nums1[idx] = nums2[idx2] + idx-- + idx2-- + } else if idx2 < 0 { + nums1[idx] = nums1[idx1] + idx-- + idx1-- + } else if nums1[idx1] < nums2[idx2] { + nums1[idx] = nums2[idx2] + idx-- + idx2-- + } else { + nums1[idx] = nums1[idx1] + idx-- + idx1-- + } + } + fmt.Println(nums1) +} +``` + + + +### java + +```java +class Solution { + public void merge(int[] nums1, int m, int[] nums2, int n) { + int idx1 = m - 1, idx2 = n - 1; + int idx = m + n - 1; + while (idx1 >= 0 || idx2 >= 0){ + if (idx1 < 0) + nums1[idx--] = nums2[idx2--]; + else if (idx2 < 0) + nums1[idx--] = nums1[idx1--]; + else if (nums1[idx1] < nums2[idx2]) + nums1[idx--] = nums1[idx2--]; + else + nums1[idx--] = nums2[idx1--]; + } + } +} +``` + diff --git "a/Java/alg/lc/9.\345\233\236\346\226\207\346\225\260.md" "b/Java/alg/lc/9.\345\233\236\346\226\207\346\225\260.md" new file mode 100644 index 00000000..ce9e2382 --- /dev/null +++ "b/Java/alg/lc/9.\345\233\236\346\226\207\346\225\260.md" @@ -0,0 +1,90 @@ +# 9. 回文数 + +[url](https://leetcode-cn.com/problems/palindrome-number/) + +## 题目 + +判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。 + +``` +输入: 121 +输出: true + +输入: -121 +输出: false +解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。 + +输入: 10 +输出: false +解释: 从右向左读, 为 01 。因此它不是一个回文数。 +``` + +## 方法 + +y = y * 10 + x % 10 +x /= 10 + +二者使用效果极佳 +- 和整数反转差不多的思想 + +## code + +### js + +```js +let isPalindrome = x => { + if (x === 0) + return true; + if (x < 0 || x % 10 === 0) + return false; + let right = 0; + while (x > right) { // 核心步骤 + right = right * 10 + x % 10; + x = (x / 10) | 0; + } + return x === right ? x === right : x === ((right / 10) | 0) +}; +``` + +### go + +```go +func isPalindrome(x int) bool { + if x == 0 { + return true + } + if x < 0 || x % 10 == 0 { + return false + } + right := 0 + for x > right { + right = right * 10 + x % 10 + x /= 10 + } + if x == right { + return true + } else { + return x == right / 10 + } +} +``` + +### java + +```java +class Solution { + public boolean isPalindrome(int x) { + if (x == 0) + return true; + if (x < 0 || x % 10 == 0) + return false; + int right = 0; + while (x > right) { + right = right * 10 + x % 10; + x /= 10; + } + return x == right ? x == right : x == right / 10; + } +} +``` + diff --git "a/Java/alg/lc/90.\345\255\220\351\233\2062.md" "b/Java/alg/lc/90.\345\255\220\351\233\2062.md" new file mode 100644 index 00000000..0143b463 --- /dev/null +++ "b/Java/alg/lc/90.\345\255\220\351\233\2062.md" @@ -0,0 +1,128 @@ +# 90. 子集 II + +[url](https://leetcode-cn.com/problems/subsets-ii/) + +## 题目 + +给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。 + +解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。 + + +``` +输入:nums = [1,2,2] +输出:[[],[1],[1,2],[1,2,2],[2],[2,2]] +输入:nums = [0] +输出:[[],[0]] +``` + +## 方法 + + +## code + +### js + +```js +let subsetsWithDup = nums => { + let res = [] + let marked = Array(nums.length).fill(false) + let dfs = (start, size, list) => { + if (list.length === size) { + res.push(list.slice()) + return + } + for (let i = start; i < nums.length; i++) { + if (i !== 0 && nums[i] === nums[i-1] && !marked[i-1]) + continue + if (marked[i]) + continue + list.push(nums[i]) + marked[i] = true + dfs(i, size, list) + marked[i] = false + list.pop() + } + } + nums.sort((a, b) => (a - b)) + for (let size = 0; size <= nums.length; size++) { + dfs(0, size, []) + } + return res +} +``` + +### go + +```go +func subsetsWithDup(nums []int) [][]int { + var res [][]int + marked := make([]bool, len(nums)) + var dfs func(nums []int, start, size int, list []int) + dfs = func(nums []int, start, size int, list []int) { + if len(list) == size { + res = append(res, append([]int{}, list...)) + return + } + for i := start; i < len(nums); i++ { + if i != 0 && nums[i] == nums[i-1] && !marked[i-1] { + continue + } + if marked[i] { + continue + } + list = append(list, nums[i]) + marked[i] = true + dfs(nums, i, size, list) + marked[i] = false + list = list[:len(list)-1] + } + } + sort.Ints(nums) + for size := 0; size <= len(nums); size++ { + dfs(nums, 0, size, []int{}) + } + return res +} +``` + + + +### java + +```java +class Solution { + public List> subsetsWithDup(int[] nums) { + Arrays.sort(nums); + List> subsets = new ArrayList<>(); + List tempSubset = new ArrayList<>(); + boolean[] hasVisited = new boolean[nums.length]; + for (int size = 0; size <= nums.length; size++) { + backtracking(0, tempSubset, subsets, hasVisited, size, nums); // 不同的子集大小 + } + return subsets; + } + private void backtracking(int start, List tempSubset, List> subsets, boolean[] hasVisited, + final int size, final int[] nums) { + + if (tempSubset.size() == size) { + subsets.add(new ArrayList<>(tempSubset)); + return; + } + for (int i = start; i < nums.length; i++) { + if (i != 0 && nums[i] == nums[i - 1] && !hasVisited[i - 1]) { + continue; + } + if (hasVisited[i]) + continue; + tempSubset.add(nums[i]); + hasVisited[i] = true; + backtracking(i, tempSubset, subsets, hasVisited, size, nums); + hasVisited[i] = false; + tempSubset.remove(tempSubset.size() - 1); + } + } + +} +``` + diff --git "a/Java/alg/lc/91.\350\247\243\347\240\201\346\226\271\346\263\225.md" "b/Java/alg/lc/91.\350\247\243\347\240\201\346\226\271\346\263\225.md" new file mode 100644 index 00000000..2c8aed8b --- /dev/null +++ "b/Java/alg/lc/91.\350\247\243\347\240\201\346\226\271\346\263\225.md" @@ -0,0 +1,118 @@ +# 91. 解码方法 + +[url](https://leetcode-cn.com/problems/decode-ways/) + +## 题目 + +一条包含字母 `A-Z` 的消息通过以下映射进行了 **编码** : + + +``` +'A' -> 1 +'B' -> 2 +... +'Z' -> 26 +``` + +要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"111" 可以将 "1" 中的每个 "1" 映射为 "A" ,从而得到 "AAA" ,或者可以将 "11" 和 "1"(分别为 "K" 和 "A" )映射为 "KA" 。注意,"06" 不能映射为 "F" ,因为 "6" 和 "06" 不同。 + +给你一个只含数字的 非空 字符串 num ,请计算并返回 解码 方法的 总数 。 + +题目数据保证答案肯定是一个 32 位 的整数。 + +``` +输入:s = "12" +输出:2 +解释:它可以解码为 "AB"(1 2)或者 "L"(12)。 +输入:s = "226" +输出:3 +解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。 +``` + +## 方法 + + +## code + +### js + +```js +let numDecodings = s => { + if (s === null || s.length === 0) + return 0; + let n = s.length; + let dp = Array(n + 1).fill(0); + dp[0] = 1; + dp[1] = s[0] === '0' ? 0 : 1; + for (let i = 2; i <= n; i++) { + let one = parseInt(s.slice(i-1, i)); + if (one !== 0) + dp[i] += dp[i-1]; + if (s[i-2] === '0') + continue; + let two = parseInt(s.slice(i-2, i)); + if (two <= 26) + dp[i] += dp[i-2]; + } + return dp[n]; +} +``` + +### go + +```go +func numDecodings(s string) int { + if len(s) == 0 { + return 0 + } + n := len(s) + dp := make([]int, n+1) + dp[0] = 1 + if s[0] == '0' { + dp[1] = 0 + } else { + dp[1] = 1 + } + for i := 2; i <= n; i++ { + one, _ := strconv.Atoi(s[i-1:i]) + if one != 0 { + dp[i] += dp[i-1] + } + if s[i-2] == '0' { + continue + } + two, _ := strconv.Atoi(s[i-1:i]) + if two <= 26 { + dp[i] += dp[i-2] + } + } + return dp[n] +} +``` + +### java + +```java +class Solution { + public int numDecodings(String s) { + if (s == null || s.length() == 0) + return 0; + int n = s.length(); + int[] dp = new int[n + 1]; + dp[0] = 1; + dp[1] = s.charAt(0) == '0' ? 0 : 1; + for (int i = 2; i <= n; i++) { + int one = Integer.valueOf(s.substring(i - 1, i)); + if (one != 0) + dp[i] += dp[i - 1]; + if (s.charAt(i - 2) == '0') + continue; + int two = Integer.valueOf(s.substring(i - 2, i)); + if (two <= 26) + dp[i] += dp[i - 2]; + } + return dp[n]; + } +} +``` + diff --git "a/Java/alg/lc/914.\345\215\241\347\211\214\345\210\206\347\273\204.md" "b/Java/alg/lc/914.\345\215\241\347\211\214\345\210\206\347\273\204.md" new file mode 100644 index 00000000..a94fa5bd --- /dev/null +++ "b/Java/alg/lc/914.\345\215\241\347\211\214\345\210\206\347\273\204.md" @@ -0,0 +1,124 @@ +# 914. 卡牌分组 + + + +[url](https://leetcode-cn.com/problems/x-of-a-kind-in-a-deck-of-cards/) + + +## 题目 +给定一副牌,每张牌上都写着一个整数。 + +此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组: + +- 每组都有 X 张牌。 +- 组内所有的牌上都写着相同的整数。 +仅当你可选的 X >= 2 时返回 true。 + + +``` +输入:[1,2,3,4,4,3,2,1] +输出:true +解释:可行的分组是 [1,1],[2,2],[3,3],[4,4] +输入:[1,1,1,2,2,2,3,3] +输出:false +解释:没有满足要求的分组。 +输入:[1] +输出:false +解释:没有满足要求的分组。 +输入:[1,1] +输出:true +解释:可行的分组是 [1,1] +输入:[1,1,2,2,2,2] +输出:true +解释:可行的分组是 [1,1],[2,2],[2,2] +``` + + +## 方法 + + +## code + +### js + +```js +let hasGroupSizeX = deck => { + // hash + let map = new Map(); + deck.forEach(num => { + if (map.has(num)) { + map.set(num, map.get(num) + 1); + } else { + map.set(num, 1); + } + }); + // 最大公约数 + let t = 0; + for (let value of map.values()) { + t = gcd(t, value); + } + return t >= 2; +}; +let gcd = (a, b) => { + return b === 0 ? a : gcd(b, a % b); +}; +console.log(hasGroupSizeX([1,2,3,4,4,3,2,1])) +console.log(hasGroupSizeX([1,1,1,2,2,2,3,3])) +``` + +### go + +```go +func hasGroupSizeX(deck []int) bool { + m := make(map[int]int, 0) + for _, k := range deck { + if v, ok := m[k]; ok { + m[k] = v + 1 + } else { + m[k] = 1 + } + } + // 最大公约数 + t := 0 + for _, k := range m { + t = gcd(t, m[k]) + } + return t >= 2 +} +func gcd(a int, b int) int { + if b == 0 { + return a + } else { + return gcd(b, a % b) + } +} +``` + +### java + +```java +class Solution { + public boolean hasGroupsSizeX(int[] deck) { + // hash + HashMap map = new HashMap<>(); + for(int num : deck) { + if(map.containsKey(num)) { + map.put(num, map.get(num) + 1); + } else { + map.put(num, 1); + } + } + // 最大公约数 + int t = 0; + for(int a : map.values()) { + t = gcd(t, a); + } + return t >= 2; + } + // 最大公约数 + private int gcd(int a, int b) { + return b == 0 ? a : gcd(b, a % b); + } +} +``` + diff --git "a/Java/alg/lc/92.\345\217\215\350\275\254\351\223\276\350\241\2502.md" "b/Java/alg/lc/92.\345\217\215\350\275\254\351\223\276\350\241\2502.md" new file mode 100644 index 00000000..a864b4d5 --- /dev/null +++ "b/Java/alg/lc/92.\345\217\215\350\275\254\351\223\276\350\241\2502.md" @@ -0,0 +1,96 @@ +# 92. 反转链表 II + +[url](https://leetcode-cn.com/problems/reverse-linked-list-ii/) + +## 题目 + +给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。 + +[](https://assets.leetcode.com/uploads/2021/02/19/rev2ex2.jpg) + + +``` +输入:head = [1,2,3,4,5], left = 2, right = 4 +输出:[1,4,3,2,5] +输入:head = [5], left = 1, right = 1 +输出:[5] +``` + +## 方法 + + +## code + +### js + +```js +let reverseBetween = (head, m, n) => { + let p = head; + let idx = 1; + let stack = []; + while (p != null) { + if (idx >= m && idx <= n) + stack.push(p.val) + idx++ + p = p.next; + } + idx = 1; + p = head; + while (p != null) { + if (idx >= m && idx <= n) + p.val = stack.pop(); + idx++; + p = p.next; + } + return head; +} +``` + +### go + +```go +func reverseBetween(head *ListNode, m int, n int) *ListNode { + dummy := &ListNode{} + dummy.Next = head + pre, cur, next := dummy, &ListNode{}, &ListNode{} + for i := 1; i < m; i++ { + pre = pre.Next + } + cur = pre.Next + for i := m; i < n; i++ { + next = cur.Next + cur.Next = next.Next + next.Next = pre.Next + pre.Next = next + } + return dummy.Next +} +``` + +### java + +```java +class Solution { + public ListNode reverseBetween(ListNode head, int m, int n) { + ListNode p = head; + int idx = 1; + Stack stack = new Stack<>(); + while (p != null) { + if (idx >= m && idx <= n) + stack.push(p.val); + idx++; + p = p.next; + } + idx = 1; + p = head; + while (p != null) { + if (idx >= m && idx <= n) + p.val = stack.pop(); + idx++; + p = p.next; + } + return head; + } +} +``` + diff --git "a/Java/alg/lc/93.\345\244\215\345\216\237IP\345\234\260\345\235\200.md" "b/Java/alg/lc/93.\345\244\215\345\216\237IP\345\234\260\345\235\200.md" new file mode 100644 index 00000000..77031328 --- /dev/null +++ "b/Java/alg/lc/93.\345\244\215\345\216\237IP\345\234\260\345\235\200.md" @@ -0,0 +1,126 @@ +# 93. 复原 IP 地址 + +[url](https://leetcode-cn.com/problems/restore-ip-addresses/) + +## 题目 + +给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。 + +有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。 + +例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。 + + +``` +输入:s = "25525511135" +输出:["255.255.11.135","255.255.111.35"] +输入:s = "0000" +输出:["0.0.0.0"] +``` + +## 方法 + + +## code + +### js + +```js +var restoreIpAddresses = function(s) { + const res = []; + // 复原从start开始的子串 + const dfs = (subRes, start) => { + if (subRes.length === 4 && start === s.length) { // 片段满4段,且耗尽所有字符 + res.push(subRes.join('.')); // 拼成字符串,加入解集 + return; // 返不返回都行,指针已经到头了,严谨的说还是返回 + } + if (subRes.length === 4 && start < s.length) { // 满4段,字符未耗尽,不用往下选了 + return; + } + for (let len = 1; len <= 3; len++) { // 枚举出选择,三种切割长度 + if (start + len - 1 >= s.length) return; // 加上要切的长度就越界,不能切这个长度 + if (len !== 1 && s[start] === '0') return; // 不能切出'0x'、'0xx' + + const str = s.substring(start, start + len); // 当前选择切出的片段 + if (len === 3 && str > 255) return; // 不能超过255 + + subRes.push(str); // 作出选择,将片段加入subRes + dfs(subRes, start + len); // 基于当前选择,继续选择,注意更新指针 + subRes.pop(); // 上面一句的递归分支结束,撤销最后的选择,进入下一轮迭代,考察下一个切割长度 + } + }; + + dfs([], 0); // dfs入口 + return res; +}; +``` + +### go + +```go +func restoreIpAddresses(s string) []string { + res := make([]string, 0) + var dfs func(subRes []string, start int) + dfs = func(subRes []string, start int) { + if len(subRes) == 4 && start == len(s) { + res = append(res, strings.Join(subRes, ".")) + return + } + if len(subRes) == 4 && start < len(s) { + return + } + for l := 1; l <= 3; l++ { + if start+l-1 >= len(s) { + return + } + if l != 1 && s[start] == '0' { + return + } + str := s[start:start+l] + strInt, _ := strconv.Atoi(str) + if l == 3 && strInt> 255 { + return + } + subRes = append(subRes, str) + dfs(subRes, start + l) + subRes = subRes[:len(subRes)-1] + } + } + dfs([]string{}, 0) + return res +} +``` + +### java + +```java +class Solution { + public List restoreIpAddresses(String s) { + List addresses = new ArrayList<>(); + StringBuilder tempAddress = new StringBuilder(); + doRestore(0, tempAddress, addresses, s); + return addresses; + } + private void doRestore(int k, StringBuilder tempAddress, List addresses, String s) { + if (k == 4 || s.length() == 0) { + if (k == 4 && s.length() == 0) { + addresses.add(tempAddress.toString()); + } + return; + } + for (int i = 0; i < s.length() && i <= 2; i++) { + if (i != 0 && s.charAt(0) == '0') break; + String part = s.substring(0, i + 1); + if (Integer.valueOf(part) <= 255) { + if (tempAddress.length() != 0) { + part = "." + part; + } + tempAddress.append(part); + doRestore(k + 1, tempAddress, addresses, s.substring(i + 1)); + tempAddress.delete(tempAddress.length() - part.length(),tempAddress.length()); + } + } + } +} +``` + diff --git "a/Java/alg/lc/94.\344\272\214\345\217\211\346\240\221\347\232\204\344\270\255\345\272\217\351\201\215\345\216\206.md" "b/Java/alg/lc/94.\344\272\214\345\217\211\346\240\221\347\232\204\344\270\255\345\272\217\351\201\215\345\216\206.md" new file mode 100644 index 00000000..565f4f6c --- /dev/null +++ "b/Java/alg/lc/94.\344\272\214\345\217\211\346\240\221\347\232\204\344\270\255\345\272\217\351\201\215\345\216\206.md" @@ -0,0 +1,93 @@ +# 94.二叉树的中序遍历 + +[url](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/) + +## 题目 + +给定一个二叉树的根节点 `root` ,返回它的 **中序** 遍历。 + +![](https://assets.leetcode.com/uploads/2020/09/15/inorder_1.jpg) + + +``` +输入:root = [1,null,2,3] +输出:[1,3,2] +``` + +## 方法 + + +## code + +### js + +```js +var inorderTraversal = function(root) { + let ret = []; + if (root === null) + return ret; + let stack = []; + let cur = root; + while (cur !== null || stack.length !== 0) { + while (cur !== null) { + stack.push(cur); + cur = cur.left; + } + let t = stack.pop(); + ret.push(t.val) + cur = t.right; + } + return ret; +}; +``` + +### go + +```go +func inorderTraversal(root *TreeNode) []int { + var ret []int + if root == nil { + return ret + } + var stack []*TreeNode + cur := root + for cur != nil || len(stack) != 0 { + for cur != nil { + stack = append(stack, cur) + cur = cur.Left + } + t := stack[len(stack)-1] + stack = stack[:len(stack)-1] + ret = append(ret, t.Val) + cur = t.Right + } + return ret +} +``` + +### java + +```java +class Solution { + public List inorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + if (root == null) + return ret; + Stack stack = new Stack<>(); + + TreeNode cur = root; + + while (cur != null || !stack.isEmpty()) { + while (cur != null) { + stack.push(cur); + cur = cur.left; + } + TreeNode t = stack.pop(); + ret.add(t.val); + cur = t.right; + } + return ret; + } +} +``` + diff --git "a/Java/alg/lc/96.\344\270\215\345\220\214\347\232\204\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/Java/alg/lc/96.\344\270\215\345\220\214\347\232\204\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" new file mode 100644 index 00000000..a189ee43 --- /dev/null +++ "b/Java/alg/lc/96.\344\270\215\345\220\214\347\232\204\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -0,0 +1,76 @@ +# 96. 不同的二叉搜索树 + +[url](https://leetcode-cn.com/problems/unique-binary-search-trees/) + +## 题目 + +给定一个整数 *n*,求以 1 ... *n* 为节点组成的二叉搜索树有多少种? + + +``` +输入: 3 +输出: 5 +解释: +给定 n = 3, 一共有 5 种不同结构的二叉搜索树: + + 1 3 3 2 1 + \ / / / \ \ + 3 2 1 1 3 2 + / / \ \ + 2 1 2 3 +``` + +## 方法 + + +## code + +### js + +```js +let numTrees = n => { + let dp = Array(n + 1).fill(0); + dp[0] = 1; + dp[1] = 1; + for (let i = 2; i <= n; i++) { + for (let j = 1; j <= i; j++) { + dp[i] += dp[j-1] * dp[i-j]; + } + } + return dp[n]; +} +``` + +### go + +```go +func numTrees(n int) int { + dp := make([]int, n+1) + dp[0], dp[1] = 1, 1 + for i := 2; i <= n; i++ { + for j := 1; j <= i; j++ { + dp[i] += dp[j-1] * dp[i-j] + } + } + return dp[n] +} +``` + +### java + +```java +class Solution { + public int numTrees(int n) { + int[] dp = new int[n + 1]; + dp[0] = 1; + dp[1] = 1; + for (int i = 2; i <=n; i++){ + for (int j = 1; j <= i; j++){ + dp[i] += dp[j - 1] * dp[i - j]; + } + } + return dp[n]; + } +} +``` + diff --git "a/Java/alg/lc/98.\351\252\214\350\257\201\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/Java/alg/lc/98.\351\252\214\350\257\201\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" new file mode 100644 index 00000000..c3d9d080 --- /dev/null +++ "b/Java/alg/lc/98.\351\252\214\350\257\201\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -0,0 +1,90 @@ +# 98. 验证二叉搜索树 + +[url](https://leetcode-cn.com/problems/unique-binary-search-trees/) + +## 题目 + +给定一个二叉树,判断其是否是一个有效的二叉搜索树。 + +假设一个二叉搜索树具有如下特征: + +- 节点的左子树只包含小于当前节点的数。 +- 节点的右子树只包含大于当前节点的数。 +- 所有左子树和右子树自身必须也是二叉搜索树。 + + +``` +输入: + 2 + / \ + 1 3 +输出: true +输入: + 5 + / \ + 1 4 +  / \ +  3 6 +输出: false +解释: 输入为: [5,1,4,null,null,3,6]。 +  根节点的值为 5 ,但是其右子节点值为 4 。 +``` + +## 方法 + + +## code + +### js + +```js +let isValidBST = root => { + let validate = (node, min, max) => { + if (node === null) + return true; + if (node.val <= min || node.val >= max) + return false; + return validate(node.left, min, node.val) && validate(node.right, node.val, max); + } + return validate(root, Number.MIN_VALUE, Number.MAX_VALUE); +} +``` + +### go + +```go +func isValidBST(root *TreeNode) bool { + const INT_MAX = int(^uint(0) >> 1) + const INT_MIN = ^INT_MAX + var validate func(node *TreeNode, min, max int) bool + validate = func(node *TreeNode, min, max int) bool { + if node == nil { + return true + } + if node.Val <= min || node.Val >= max { + return false + } + return validate(node.Left, min, node.Val) && validate(node.Right, node.Val, max) + } + return validate(root, INT_MIN, INT_MAX) +} +``` + +### java + +```java +class Solution { + public boolean isValidBST(TreeNode root) { + return validate(root, Long.MIN_VALUE, Long.MAX_VALUE); + } + + public boolean validate(TreeNode node, long min, long max) { + if (node == null) + return true; + if (node.val <= min || node.val >= max) + return false; + return validate(node.left, min, node.val) && validate(node.right, node.val, max); + } +} +``` + diff --git "a/Java/alg/\344\270\252\344\272\272\345\210\267\347\206\237\351\242\230.md" "b/Java/alg/\344\270\252\344\272\272\345\210\267\347\206\237\351\242\230.md" new file mode 100644 index 00000000..9e2620d1 --- /dev/null +++ "b/Java/alg/\344\270\252\344\272\272\345\210\267\347\206\237\351\242\230.md" @@ -0,0 +1,5219 @@ +> 看了很多面经,也有一些面试常见的题,只是希望多熟练一些,保证自己能在有效的时间写对,这是最关键的。 +> 面试代码不可能太长的,而且都是高频热点。 + +## 链表 + +### 1、[从尾到头打印链表](https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035?tpId=13&tqId=11156&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class T3 { + // 创建list + ArrayList list = new ArrayList<>(); + public ArrayList printListFromTailToHead(ListNode listNode) { + // 判断头节点是否为空 + if (listNode != null) { + // 递归打印 + this.printListFromTailToHead(listNode.next); + list.add(listNode.val); + } + return list; + } +} +``` + +### 2、[删除链表中重复的结点](https://www.nowcoder.com/practice/fc533c45b73a41b0b44ccba763f866ef?tpId=13&tqId=11209&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/) + +```java +public ListNode deleteDuplication(ListNode pHead) { + // 1. 边界,需要下一个结点做判断,因此还是需要判断下个结点是否为空 + if (pHead == null || pHead.next == null) + return pHead; + // 取下一个结点 + ListNode next = pHead.next; + // 2. 判断当前结点和下一个结点是否相等 + if (pHead.val == next.val) { + // 3. 如果相等,判断是否一直重复 + while (next != null && pHead.val == next.val) + next = next.next; + return deleteDuplication(next); + } else { + // 4, 否则不重复的话,就递归下一个结点 + pHead.next = deleteDuplication(pHead.next); + return pHead; + } +} +``` + +### 3.1 环形链表 +[https://leetcode-cn.com/problems/linked-list-cycle/](https://leetcode-cn.com/problems/linked-list-cycle/) +```java +public class Solution { + public boolean hasCycle(ListNode head) { + if (head == null) { + return false; + } + ListNode l1 = head, l2 = head.next; + while (l1 != null && l2 != null && l2.next != null) { + if (l1 == l2) { + return true; + } + l1 = l1.next; + l2 = l2.next.next; + } + return false; + } +} +``` + +### 3.2、[链表中环的入口结点](https://www.nowcoder.com/practice/253d2c59ec3e4bc68da16833f79a38e4?tpId=13&tqId=11208&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + + +[https://leetcode-cn.com/problems/linked-list-cycle-ii/submissions/](https://leetcode-cn.com/problems/linked-list-cycle-ii/submissions/) + +```java +// 快慢指针 +public class Solution { + public ListNode detectCycle(ListNode head) { + if (head == null || head.next == null) + return null; + + boolean hasCycle = false; + ListNode p1 = head, p2 = head; + while(p2.next != null && p2.next.next != null) { + p1 = p1.next; + p2 = p2.next.next; + if (p1 == p2) { + hasCycle = true; + break; + } + } + if(hasCycle){ + p2 = head; + while (p1 != p2) { + p1 = p1.next; + p2 = p2.next; + } + return p1; + } else { + return null; + } + + } +} +``` + +### 4、[反转链表](https://www.nowcoder.com/practice/75e878df47f24fdc9dc3e400ec6058ca?tpId=13&tqId=11168&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/](https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/) + +```java +public ListNode reverseList(ListNode head) { + // 总感觉,本质还是两个指针,pre不过初始化为null,cur为head当前结点 + ListNode pre = null; + ListNode cur = head; + while (cur != null) { + // 1. 获取当前节点的下一个节点,方便cur下移动 + ListNode nextTemp = cur.next; + // 2. 当前节点的下个节点指向前一个节点 (反转,那肯定cur的next指向pre咯) + cur.next = pre; + // 3. pre移动下一个结点,那肯定是cur咯 + pre = cur; + // 4. cur移动下一个结点, 那肯定是nextTemp咯 + cur = nextTemp; + } + return pre; +} +``` + +```java +public class T15 { + public ListNode ReverseList(ListNode head) { + // 判断 + if (head == null) return null; + return reverse(null, head); + } + private ListNode reverse(ListNode pre, ListNode cur) { + // 递归结束判断 + if (cur == null) return pre; + // 1. 依然第一个方法遍历依然,得到cur的next,递归用 + ListNode next = cur.next; + // 2. 反转操作 + cur.next = pre; + return reverse(cur, next); + } +} +``` + +### 5、[链表中倒数第k个结点](https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/](https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/) + +```java +public ListNode FindKthToTail(ListNode head, int k) { + // 还是双指针 + // 1. 边界判断 + if (head == null) + return null; + ListNode p1 = head; + // 2. 先让p1移动k步 + while (p1 != null && k-- > 0) + p1 = p1.next; + // 3. 这一步防止k大于head的长度 + if (k > 0) + return null; + ListNode p2 = head; + // 4. 二者正常走,不过p1肯定先到末尾 + while (p1 != null) { + p1 = p1.next; + p2 = p2.next; + } + // 5. 返回p2 + return p2; +} +``` + +### 6、[合并两个排序的链表](https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=%2Fta%2) + +[https://leetcode-cn.com/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof/](https://leetcode-cn.com/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof/) +```java +public class T16 { + public ListNode Merge(ListNode list1,ListNode list2) { + // 1. 如果list1为空,返回list2 + if (list1 == null) return list2; + // 2. 如果list2为空,返回list1 + if (list2 == null) return list1; + // 3. 如果list1.val < list2.val,则list1.next连接下一个比较值(递归比较) + if (list1.val < list2.val) { + list1.next = Merge(list1.next, list2); + return list1; + } else { + // 4. 否则,list2.next 连接下一个比较值(递归比较) + list2.next = Merge(list1, list2.next); + return list2; + } + } +} +``` + +### 7、[两个链表的第一个公共结点](https://www.nowcoder.com/practice/6ab1d9a29e88450685099d45c9e31e46?tpId=13&tqId=11189&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/intersection-of-two-linked-lists/](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/) + +```java +// 还是双指针 +public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + ListNode l1 = pHead1, l2 = pHead2; + // 1. 循环条件 + while (l1 != l2) { + // 2. 走完l1,从头走head2 + l1 = (l1 == null) ? pHead2 : l1.next; + // 3. 走完l2,从头走head1 + l2 = (l2 == null) ? pHead1 : l2.next; + } + // 4. 返回l1 + return l1; +} +``` + +### 8. 两数相加(2819) +[https://leetcode-cn.com/problems/add-two-numbers/](https://leetcode-cn.com/problems/add-two-numbers/) + +```java +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + // 边界判断:3个 + // 1. l1和l2同时为空 + if (l1 == null && l2 == null) return null; + // 2. l1为空,返回l2 + if (l1 == null) return l2; + // 3. l2为空,返回l1 + if (l2 == null) return l1; + // 三指针 + // 1. p1 + ListNode p1 = l1; + // 2. p2 + ListNode p2 = l2; + // 3. p3 特殊:返回最值链表 + ListNode l3 = new ListNode(-1); + ListNode p3 = l3; + // 4. 注意:进位 + int carried = 0; + // 5. 循环条件,任意一个不为空即可 + while (p1 != null || p2 != null) { + // p1不为空,获取p1val,否则0 + int a = p1 != null ? p1.val : 0; + // p2不为空,获取p2val,否则0 + int b = p2 != null ? p2.val : 0; + // 关键一步,(a+b+carried) % 10 个位 + p3.next = new ListNode((a + b + carried) % 10); + // 加完,记得进位呀(a + b + carried) / 10 + carried = (a + b + carried) / 10; + // 三个指针开始移动 + p3 = p3.next; + p1 = p1 != null ? p1.next : null; + p2 = p2 != null ? p2.next : null; + } + // 循环完之后,判断进位是否0,如果不是,new一个结点为1,是的话,就null + p3.next = carried != 0 ? new ListNode(1) : null; + // 返回l3下一个结点 + return l3.next; + } +} +``` + +### 9. 合并K个排序链表(924) +[https://leetcode-cn.com/problems/merge-k-sorted-lists/](https://leetcode-cn.com/problems/merge-k-sorted-lists/) + +最小堆 +```java +class Solution { + public ListNode mergeKLists(ListNode[] lists) { + // 边界判断 + if (lists == null || lists.length == 0) return null; + // 2. 堆,大顶堆 + PriorityQueue queue = new PriorityQueue<>((o1, o2) -> o1.val - o2.val); + // 3. 返回结点 + ListNode dummy = new ListNode(0); + // 4. 临时p + ListNode p = dummy; + // 5. 遍历k个lists,一个一个添加到queue + for (ListNode node : lists) { + if (node != null) queue.add(node); + } + // 6. 循环堆不为空 + while (!queue.isEmpty()) { + // p的下个结点指向 取出堆中最大的链表结点 + p.next = queue.poll(); + // 移动 + p = p.next; + // 如果下一个不为空,继续添加刚才堆中最大的下一个结点 + if (p.next != null) queue.add(p.next); + } + // 7. 返回下一个结点 + return dummy.next; + } +} +``` + +```java +class Solution { + public ListNode mergeKLists(ListNode[] lists) { + // 归并 + if (lists == null || lists.length == 0) return null; + return merge(lists, 0, lists.length - 1); + } + + private ListNode merge(ListNode[] lists, int left, int right) { + // 归并排序一样 + if (left == right) return lists[left]; + int mid = left + (right - left) / 2; + ListNode l1 = merge(lists, left, mid); + ListNode l2 = merge(lists, mid + 1, right); + return mergeTwoLists(l1, l2); + } + // 不过,最后归并的时候用的是合并两个排序的链表 + private ListNode mergeTwoLists(ListNode l1, ListNode l2) { + if (l1 == null) return l2; + if (l2 == null) return l1; + if (l1.val < l2.val) { + l1.next = mergeTwoLists(l1.next, l2); + return l1; + } else { + l2.next = mergeTwoLists(l1,l2.next); + return l2; + } + } +} +``` + +### 10. 两两交换链表中的节点(947) +[https://leetcode-cn.com/problems/swap-nodes-in-pairs/](https://leetcode-cn.com/problems/swap-nodes-in-pairs/) + +```java +class Solution { + // 两两交换,那么双指针 + public ListNode swapPairs(ListNode head) { + // 创建一个返回结点 + ListNode node = new ListNode(-1); + // 该结点的next指向head + node.next = head; + // 在创建一个pre,移动到node + ListNode pre = node; + // 循环遍历 + // pre的next才是head,因此判断next next + while (pre.next != null && pre.next.next != null) { + // 获取l1, l2 + ListNode l1 = pre.next, l2 = pre.next.next; + // + ListNode next = l2.next; + l1.next = next; + l2.next = l1; + pre.next = l2; + // pre移动一步 + pre = l1; + } + return node.next; + } +} +``` + +### 11. 链表的中间结点(853) +[https://leetcode-cn.com/problems/middle-of-the-linked-list/](https://leetcode-cn.com/problems/middle-of-the-linked-list/) + +```java +// 双指针(快慢) +class Solution { + public ListNode middleNode(ListNode head) { + ListNode p = head, q = head; + while (q != null && q.next != null) { + q = q.next.next; + p = p.next; + } + return p; + } +} +``` + +### 12. 删除排序链表中的重复元素(603) +[https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/) + +```java +// 和那个有点区别 +class Solution { + public ListNode deleteDuplicates(ListNode head) { + if (head == null || head.next == null) return head; + head.next = deleteDuplicates(head.next); + return head.val == head.next.val ? head.next : head; + } +} +``` + +### 13. 回文链表(624) +[https://leetcode-cn.com/problems/palindrome-linked-list/](https://leetcode-cn.com/problems/palindrome-linked-list/) + +这题考察了很多链表的题,挺综合额 +```java +class Solution { + public boolean isPalindrome(ListNode head) { + if(head == null || head.next == null) return true; + // 找中点 + ListNode slow = head, fast = head.next; + while(fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + // 奇偶情况 + if(fast != null) slow = slow.next; + // cut + cut(head, slow); + // 比较 + return isEqual(head, reverse(slow)); + + } + + // 切 + public void cut (ListNode head, ListNode cutNode) { + ListNode node = head; + // 循环遍历找到和cutNode相等的结点的前一个结点 + while(node.next != cutNode) { + node = node.next; + } + // 然后直接null + node.next = null; + } + + // 反转链表派上用场 + public ListNode reverse(ListNode head) { + ListNode pre = null; + ListNode cur = head; + while(cur != null) { + ListNode nextNode = cur.next; + cur.next = pre; + pre = cur; + cur = nextNode; + } + return pre; + } + + + public boolean isEqual(ListNode l1, ListNode l2) { + // 二者都不为空才行 + while(l1 != null && l2 != null) { + // 二者值不相等直接false + if(l1.val != l2.val) return false; + // 移动 + l1 = l1.next; + l2 = l2.next; + } + // 比如就true + return true; + } +} +``` + +### 14. 奇偶链表(317) +[https://leetcode-cn.com/problems/odd-even-linked-list/](https://leetcode-cn.com/problems/odd-even-linked-list/) + +```java +class Solution { + public ListNode oddEvenList(ListNode head) { + if (head == null) return head; + // 临时变量 奇头 偶头下 偶头部 + ListNode odd = head, even = head.next, evenHead = even; + // 循环遍历偶和偶下不为空 + while (even != null && even.next != null) { + // 奇 -> 奇下下 + odd.next = odd.next.next; + // 奇移动一步 + odd = odd.next; + // 偶 -> 偶下下 + even.next = even.next.next; + // 偶移动 + even = even.next; + } + // 奇 -> 偶头 + odd.next = evenHead; + // 返回head + return head; + } +} +``` + +### 92. 反转链表 II +[https://leetcode-cn.com/problems/reverse-linked-list-ii/](https://leetcode-cn.com/problems/reverse-linked-list-ii/) + +```java +/** + * 定位到要反转部分的头节点 2,head = 2;前驱结点 1,pre = 1; + * 当前节点的下一个节点3调整为前驱节点的下一个节点 1->3->2->4->5, + * 当前结点仍为2, 前驱结点依然是1,重复上一步操作。。。 + * 1->4->3->2->5. + */ +class Solution { + public ListNode reverseBetween(ListNode head, int m, int n) { + ListNode dummy = new ListNode(0); + dummy.next = head; + ListNode pre = dummy, cur, next; + for (int i = 1; i < m; i++) + pre = pre.next; + cur = pre.next; + for (int i = m; i < n; i++) { + next = cur.next; + cur.next = next.next; + next.next = pre.next; + pre.next = next; + } + return dummy.next; + } +} +``` +```java +// 投机取巧 缓存 +class Solution { + public ListNode reverseBetween(ListNode head, int m, int n) { + ListNode p = head; + int idx = 1; + Stack stack = new Stack<>(); + while (p != null) { + if (idx >= m && idx <= n) + stack.push(p.val); + idx++; + p = p.next; + } + idx = 1; + p = head; + while (p != null) { + if (idx >= m && idx <= n) + p.val = stack.pop(); + idx++; + p = p.next; + } + return head; + } +} +``` + +### 15. K 个一组翻转链表(字节爱考) +[https://leetcode-cn.com/problems/reverse-nodes-in-k-group/](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/) + +```java +class Solution { + public ListNode reverseKGroup(ListNode head, int k) { + ListNode dummy = new ListNode(0), pre = dummy, curr = head, next; + // 临时赋值 + dummy.next = head; + // 算长度 + int len = 0; + while (head != null) { + len++; + head = head.next; + } + // 从头 + head = dummy.next; + // 两重for遍历 + for (int i = 0; i < len / k; i++) { + for (int j = 0; j < k - 1; j++) { + // 谜一般的操作 + // 临时next + next = curr.next; + // 我指向你 + curr.next = next.next; + // 你指向他 + next.next = pre.next; + // 他指向临时 + pre.next = next; + } + pre = curr; // 移动 + curr = pre.next; // 移动 + } + return dummy.next; + } +} +``` + + +### 16. 奇数位升序偶数位降序的链表 + +```java +public static ListNode oddEvenLinkedList(ListNode head) { + // 将偶数链表拆分出来 + ListNode evenHead = getEvenList(head); + // 逆序偶数链表 + ListNode reEvenHead = reverseList(evenHead); + // 归并奇偶链表 + ListNode mHead = mergeList(head, reEvenHead); + return mHead; +} + +public static ListNode getEvenList(ListNode head) { + ListNode odd = new ListNode(-1); + ListNode even = new ListNode(-1); + int cnt = 1; + ListNode cur1 = odd, cur2 = even; + while (head != null) { + if (cnt % 2 != 0) { + cur1.next = head; + cur1 = cur1.next; + } else { + cur2.next = head; + cur2 = cur2.next; + } + head = head.next; + cnt++; + } + cur1.next = null; + cur2.next = null; + return even.next; +} + +public static ListNode reverseList(ListNode head) { + ListNode pre = null; + ListNode cur = head; + while (cur != null) { + ListNode next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; +} + +public static ListNode mergeList(ListNode l1, ListNode l2){ + // 我用递归 + if (l1 == null) + return l2; + if (l2 == null) + return l1; + if (l1.val < l2.val) { + l1.next = mergeList(l1.next, l2); + return l1; + } else { + l2.next = mergeList(l1, l2.next); + return l2; + } +} +``` + +### 17. 分隔链表 +[https://leetcode-cn.com/problems/partition-list/](https://leetcode-cn.com/problems/partition-list/) +```java +class Solution { + public ListNode partition(ListNode head, int x) { + ListNode dummy1 = new ListNode(0); + ListNode dummy2 = new ListNode(0); + ListNode node1 = dummy1, node2 = dummy2; + while (head != null){ + if (head.val < x){ + node1.next = head; + // 你左脚走 + head = head.next; + // 1也左脚走 + node1 = node1.next; + // 直接割,我不需要 + node1.next = null; + } else { + node2.next = head; + head = head.next; + node2 = node2.next; + node2.next = null; + } + } + node1.next = dummy2.next; + return dummy1.next; + } +} +``` + +### 18 排序链表 + +[https://leetcode-cn.com/problems/sort-list/](https://leetcode-cn.com/problems/sort-list/) + +```java +class Solution { + public ListNode sortList(ListNode head) { + return head == null ? null : mergeSort(head); + } + private ListNode mergeSort(ListNode head) { + if (head.next == null) + return head; + ListNode p = head, q = head, pre = null; + while (q != null && q.next != null){ + pre = p; + p = p.next; + q = q.next.next; + } + pre.next = null; + ListNode l = mergeSort(head); + ListNode r = mergeSort(p); + return merge(l, r); + } + + private ListNode merge(ListNode l1, ListNode l2){ + if (l1 == null) + return l2; + if (l2 == null) + return l1; + if (l1.val < l2.val){ + l1.next = merge(l1.next, l2); + return l1; + } else { + l2.next = merge(l1, l2.next); + return l2; + } + } +} +``` + +### 143. 重排链表 +[https://leetcode-cn.com/problems/reorder-list/](https://leetcode-cn.com/problems/reorder-list/) + +```java +class Solution { + public void reorderList(ListNode head) { + LinkedList queue = new LinkedList<>(); + ListNode cur = head; + while (cur != null) { + queue.addLast(cur); + cur = cur.next; + } + while (!queue.isEmpty()) { + if (cur == null) + cur = queue.pollFirst(); + else { + cur.next = queue.pollFirst(); + cur = cur.next; + } + cur.next = queue.pollLast(); + cur = cur.next; + } + if (cur != null) + cur.next = null; + } +} +``` + +### 19. 删除链表的倒数第N个节点 +[https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) + +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode fast = head; + while (n-- > 0) { + fast = fast.next; + } + // 这里没懂, 得举例子就懂了 + if (fast == null) return head.next; + ListNode slow = head; + while (fast.next != null) { + fast = fast.next; + slow = slow.next; + } + // 这里也懂了...举个例子就行 + slow.next = slow.next.next; + return head; + } +} +``` + +### xx、[复杂链表的复制](https://www.nowcoder.com/practice/f836b2c43afc4b35ad6adc41ec941dba?tpId=13&tqId=11178&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +> 这个题面试没见着,但是先放这里把 + +```java +public class T25 { + public RandomListNode Clone(RandomListNode pHead) { + if (pHead == null) return null; + // 第一步:先复制一遍next + RandomListNode node = pHead; + while (node != null) { + RandomListNode copyNode = new RandomListNode(node.label); + copyNode.next = node.next; + node.next = copyNode; + node = copyNode.next; + } + // 第二步:再复制一遍random + node = pHead; + while (node != null) { + node.next.random = node.random == null ? null : node.random.next; + node = node.next.next; + } + // 第三步:切开 + node = pHead; + RandomListNode pCloneHead = pHead.next; + while (node != null) { + RandomListNode copyNode = node.next; + node.next = copyNode.next; + copyNode.next = copyNode.next == null ? null : copyNode.next.next; + node = node.next; + } + return pCloneHead; + } +} +``` + +## 树 +### 1、[重建二叉树](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/](https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/) + +```java +class Solution { + public TreeNode buildTree(int[] preorder, int[] inorder) { + int n = preorder.length; + if (n == 0) + return null; + int rootVal = preorder[0], rootIndex = 0; + // 找中序根的索引 + for (int i = 0; i < n; i++) { + if (inorder[i] == rootVal) { + rootIndex = i; + break; + } + } + TreeNode root = new TreeNode(rootVal); + // 注意边界 + // left + root.left = buildTree( + Arrays.copyOfRange(preorder, 1, 1 + rootIndex), + Arrays.copyOfRange(inorder, 0, rootIndex)); + // right + root.right = buildTree( + Arrays.copyOfRange(preorder, 1 + rootIndex, n), + Arrays.copyOfRange(inorder, 1 + rootIndex, n)); + + return root; + } +} +``` + +### 2、[二叉树的下一个结点](https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +// 中序 +public class T57 { + public TreeLinkNode GetNext(TreeLinkNode pNode) { + if (null == pNode) { + return null; + } + // 两种情况 + if (null != pNode.right) { + TreeLinkNode node = pNode.right; + while (null != node.left) { + node = node.left; + } + return node; + } + while (null != pNode.next) { + TreeLinkNode parent = pNode.next; + if (parent.left == pNode) { + return parent; + } + pNode = pNode.next; + } + return null; + } +} +``` + +### 3、[树的子结构](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof/](https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof/) + +```java +public class T17 { + public boolean HasSubtree(TreeNode root1, TreeNode root2) { + if (root1 == null || root2 == null) + return false; + return isSubtreeWithRoot(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2); + } + + private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) { + if (root2 == null) + return true; + if (root1 == null) + return false; + if (root1.val != root2.val) + return false; + return isSubtreeWithRoot(root1.left, root2.left) && isSubtreeWithRoot(root1.right, root2.right); + } + +} +``` + +### 3、[二叉树的镜像](https://www.nowcoder.com/practice/564f4c26aa584921bc75623e48ca3011?tpId=13&tqId=11171&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/er-cha-shu-de-jing-xiang-lcof/](https://leetcode-cn.com/problems/er-cha-shu-de-jing-xiang-lcof/) + +```java +public class T18 { + public void Mirror(TreeNode root) { + // 判断 + if (root == null) return; + swap(root); + Mirror(root.left); + Mirror(root.right); + + } + + private void swap(TreeNode root) { + TreeNode t = root.left; + root.left = root.right; + root.right = t; + } +} +``` + +```java +class Solution { + public TreeNode mirrorTree(TreeNode root) { + if (root == null) return null; + Queue queue = new LinkedList<>(); + queue.add(root); + TreeNode cur; + while (!queue.isEmpty()) { + cur = queue.poll(); + if(cur.left != null) queue.add(cur.left); + if (cur.right != null) queue.add(cur.right); + swap(cur); + } + return root; + } + + private void swap(TreeNode root) { + TreeNode t = root.left; + root.left = root.right; + root.right = t; + } +} +``` + +### 4、[对称的二叉树](https://www.nowcoder.com/practice/ff05d44dfdb04e1d83bdbdab320efbcb?tpId=13&tqId=11211&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/dui-cheng-de-er-cha-shu-lcof/](https://leetcode-cn.com/problems/dui-cheng-de-er-cha-shu-lcof/) + +```java +public class T58 { + boolean isSymmetrical(TreeNode pRoot) { + if (null == pRoot) { + return true; + } + return comRoot(pRoot.left, pRoot.right); + } + + private boolean comRoot(TreeNode left, TreeNode right) { + if (left == null && right == null) { + return true; + } + if (left == null || right == null) { + return false; + } + if (left.val != right.val) { + return false; + } + // 左右对比 + return comRoot(left.right, right.left) && comRoot(left.left, right.right); + } +} +``` + +### 5.1、[从上往下打印二叉树](https://www.nowcoder.com/practice/7fe2212963db4790b57431d9ed259701?tpId=13&tqId=11175&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof/](https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof/) + +```java +public class T22 { + // 层序遍历 + public ArrayList PrintFromTopToBottom(TreeNode root) { + ArrayList list = new ArrayList<>(); + // 需要用到队列 + LinkedList queue = new LinkedList<>(); + queue.offer(root); // 第一次先加根入队 + while (!queue.isEmpty()) { + int cnt = queue.size(); + // 如果队列不为空的话, 队列出一个元素 + while(cnt-- > 0) { + TreeNode t = queue.poll(); + if (t == null) continue; + list.add(t.val); + queue.add(t.left); + queue.add(t.right); + } + } + return list; + } +} +``` + +### 5.2、[把二叉树打印多行](https://www.nowcoder.com/practice/445c44d982d04483b04a54f298796288?tpId=13&tqId=11213&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class T60 { + + ArrayList> Print(TreeNode pRoot) { + ArrayList> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(pRoot); + while (!queue.isEmpty()) { + ArrayList list = new ArrayList<>(); + int cnt = queue.size(); + while (cnt-- > 0) { + TreeNode node = queue.poll(); + if (node == null) continue; + list.add(node.val); + queue.add(node.left); + queue.add(node.right); + } + if (list.size() != 0) ret.add(list); + } + return ret; + } +} +``` + +### 5.3、[按之字形顺序打印二叉树](https://www.nowcoder.com/practice/91b69814117f4e8097390d107d2efbe0?tpId=13&tqId=11212&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class T59 { + public ArrayList> Print(TreeNode pRoot) { + ArrayList> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(pRoot); + boolean reverse = false; + while (! queue.isEmpty()) { + ArrayList list = new ArrayList<>(); + int cnt = queue.size(); + while (cnt-- > 0) { + TreeNode node = queue.poll(); + if (node == null) continue; + list.add(node.val); + queue.add(node.left); + queue.add(node.right); + } + if (reverse) Collections.reverse(list); + reverse = !reverse; + if (list.size() != 0) ret.add(list); + } + return ret; +} +``` + +### 5.4 二叉树的右视图 +[https://leetcode-cn.com/problems/binary-tree-right-side-view/](https://leetcode-cn.com/problems/binary-tree-right-side-view/) + +层序遍历,只保留最后一个结点的值 +```java +class Solution { + public List rightSideView(TreeNode root) { + List ret = new ArrayList<>(); + if (root == null) return ret; + Queue queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()) { + int size = queue.size(); + while (size-- > 0) { + TreeNode t = queue.poll(); + if (t.left != null) queue.add(t.left); + if (t.right != null) queue.add(t.right); + if (size == 0) ret.add(t.val); + } + } + return ret; + } +} +``` + +### 5.5 二叉树的左视图 + +```java +class Solution { + public List rightSideView(TreeNode root) { + List ret = new ArrayList<>(); + if (root == null) return ret; + Queue queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()){ + int size = queue.size(); + int tmp = size - 1; + while (size-- > 0){ + TreeNode t = queue.poll(); + if(t.left != null) queue.add(t.left); + if(t.right != null) queue.add(t.right); + if(tmp == size) ret.add(t.val); + } + } + return ret; + } +} +``` + +### 6、[二叉搜索树的后序遍历序列](https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd?tpId=13&tqId=11176&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/](https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-hou-xu-bican-li-xu-lie-lcof/) + + +```java +public class T23 { + public boolean VerifySquenceOfBST(int [] sequence) { + if (sequence == null || sequence.length == 0) return false; + return isBST(sequence, 0, sequence.length - 1); + } + private boolean isBST(int[] sequence, int first, int last) { + if (last - first <= 1) { + return true; + } + int rootVal = sequence[last]; + int cutIndex = first; + while (cutIndex < last && sequence[curIndex] <= rootVal) { // 二叉搜索树特征 + cutIndex++; + } + for (int i = cutIndedx; i < last; i++) { + if (sequence[i] < rootVal) return false; + } + return isBST(sequence, first, cutIndex - 1) && isBST(sequence, cutIndex, last - 1); + } +} +``` + +### 7、[二叉树中和为某一值的路径](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=11177&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/](https://leetcode-cn.com/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/) + +```java +public class T24 { + + private ArrayList> ret = new ArrayList<>(); + + public ArrayList> FindPath(TreeNode root, int target) { + backtracking(root, target, new ArrayList<>()); + return ret; + } + + private void backtracking(TreeNode node, int target, ArrayList path) { + if (node == null) + return; + path.add(node.val); + target -= node.val; + if (target == 0 && node.left == null && node.right == null) { + ret.add(new ArrayList<>(path)); + } else { + backtracking(node.left, target, path); + backtracking(node.right, target, path); + } + path.remove(path.size() - 1); + } +} +``` + +### 8、[二叉搜索树的第k个结点](https://www.nowcoder.com/practice/ef068f602dde4d28aab2b210e859150a?tpId=13&tqId=11215&tPage=4&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/](https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/) + +```java +class Solution { + private int ans = 0, count = 0; + public int kthLargest(TreeNode root, int k) { + // clarification: root == null? k <= 1? + helper(root, k); + return ans; + } + + private void helper(TreeNode root, int k) { + if (root.right != null) helper(root.right, k); + + if (++count == k) { + ans = root.val; + return; + } + + if (root.left != null) helper(root.left, k); + } +} +``` + +### 9.1、[二叉树的深度](https://www.nowcoder.com/practice/435fb86331474282a3499955f0a41e8b?tpId=13&tqId=11191&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class T38 { + public int TreeDepth(TreeNode root) { + // 递归取左和右的最大高度 + 1 + return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right)); + } +} + +// 迭代 bfs +class Solution { + public int minDepth(TreeNode root) { + if (root == null) return 0; + Queue queue = new LinkedList<>(); + // 树不需要标记哦 + queue.add(root); + int depth = 1; + while (!queue.isEmpty()) { + int size = queue.size(); + while (size-- > 0) { + TreeNode node = queue.poll(); + if (node.left == null && node.right == null) + return depth; + if (node.left != null) + queue.add(node.left); + if (node.right != null) + queue.add(node.right); + } + depth++; + } + return depth; + } +} +``` +### 9.2 二叉树的直径 +[https://leetcode-cn.com/problems/diameter-of-binary-tree/](https://leetcode-cn.com/problems/diameter-of-binary-tree/) +```java +class Solution { + // 定义最大高度 + private int max = 0; + public int diameterOfBinaryTree(TreeNode root) { + // 递归 + Depth(root); + return max; + } + + private int Depth(TreeNode root) { + // 递归结束条件 + if (root == null) return 0; + // 递归左的高度 + int l = Depth(root.left); + // 递归右的高度 + int r = Depth(root.right); + // 每次保持最大高度 + max = Math.max(max, l + r); + // 返回左和右的最大高度加1 + return Math.max(l, r) + 1; + } +} +``` + +### 10、[平衡二叉树](https://www.nowcoder.com/practice/8b3b95850edb4115918ecebdf1b4d222?tpId=13&tqId=11192&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/ping-heng-er-cha-shu-lcof/](https://leetcode-cn.com/problems/ping-heng-er-cha-shu-lcof/) + +```java +// 定义一个平衡标记,默认平衡 +private boolean isBalanced = true; + +public boolean IsBalanced_Solution(TreeNode root) { + // 递归 + height(root); + return isBalanced; +} + +private int height(TreeNode root) { + // 递归结束条件 + if (root == null || !isBalanced) + return 0; + // 递归左高度 + int left = height(root.left); + // 递归右高度 + int right = height(root.right); + // 绝对值是否大于1 + if (Math.abs(left - right) > 1) + isBalanced = false; + // 返回左和右的最大高度加1 + return 1 + Math.max(left, right); +} +``` + +### 11.1 非递归前序 +[https://leetcode-cn.com/problems/binary-tree-preorder-traversal/description/](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/description/) + +```java +public List preorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + // 用栈的思想 + Stack stack = new Stack<>(); + // 我们知道,前序:根左右 + // 添加根 + stack.push(root); + while (!stack.isEmpty()) { + // 弹根 + TreeNode node = stack.pop(); + // 判断是否为空 + if (node == null) continue; + // 不为空,加val加入列表 + ret.add(node.val); + // 先添加右,后左,这样下次就能先弹左 + stack.push(node.right); + stack.push(node.left); + } + return ret; +} + +``` + +### 11.2 后序 +[https://leetcode-cn.com/problems/binary-tree-postorder-traversal/description/](https://leetcode-cn.com/problems/binary-tree-postorder-traversal/description/) + +```java +public List postorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + // 还是栈思想,后序:左右根,倒过来:根右左:那么就是根前序的差不多 + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode node = stack.pop(); + if (node == null) continue; + ret.add(node.val); + // 这里先添加左,保证弹出的是右 + stack.push(node.left); + stack.push(node.right); + } + // 翻转就是后序 + Collections.reverse(ret); + return ret; +} +``` + +### 11.3 中序 +[https://leetcode-cn.com/problems/binary-tree-inorder-traversal/description/](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/description/) + +```java +public List inorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + if (root == null) return ret; + // 还是栈:中序:左根右 + Stack stack = new Stack<>(); + // 虚拟结点 + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + while (cur != null) { + // 一直左 + stack.push(cur); + cur = cur.left; + } + // 保证弹出的左 + TreeNode node = stack.pop(); + ret.add(node.val); + // 开始移动到右 + cur = node.right; + } + return ret; +} + +``` + +### 12 二叉树的最近公共祖先 +[https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/) +```java +class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root == null) + return root; + // 如果根等于p或者q,直接返回根 + if (root == p || root == q) + return root; + // 递归左和pq比 + TreeNode left = lowestCommonAncestor(root.left, p, q); + // 递归右和pq比 + TreeNode right = lowestCommonAncestor(root.right, p, q); + // 同时不为空,则为根 + if (left != null && right != null) + return root; + // 左不空,则左 + else if (left != null) + return left; + // 右不空,则右 + else if (right != null) + return right; + return null; + } +} +``` + +### 13. 合并二叉树 +[https://leetcode-cn.com/problems/merge-two-binary-trees/](https://leetcode-cn.com/problems/merge-two-binary-trees/) + +```java +class Solution { + public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { + if (t1 == null && t2 == null) + return null; + if (t1 == null) + return t2; + if (t2 == null) + return t1; + TreeNode root = new TreeNode(t1.val + t2.val); + root.left = mergeTrees(t1.left, t2.left); + root.right = mergeTrees(t1.right, t2.right); + return root; + } +} + + public TreeNode mergeTrees2(TreeNode t1, TreeNode t2) { + if (t1 == null && t2 == null) + return null; + if (t1 == null) + return t2; + if (t2 == null) + return t1; + // 先合并根节点 + t1.val += t2.val; + // 再递归合并左右子树 + t1.left = mergeTrees(t1.left, t2.left); + t1.right = mergeTrees(t1.right, t2.right); + return t1; + } +``` + +```java +class Solution { + public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { + Queue queue = new LinkedList<>(); + queue.offer(t1); + queue.offer(t2); + while(!queue.isEmpty()){ + TreeNode node1 = queue.poll(); + TreeNode node2 = queue.poll(); + //合并两个值 + node1.val += node2.val; + //左子树都不为空 + if(node1.left != null && node2.left!=null){ + queue.offer(node1.left); + queue.offer(node2.left); + } + if(node1.left == null) + node1.left = node2.left; + //右子树都不为空 + if(node1.right != null && node2.right != null){ + queue.offer(node1.right); + queue.offer(node2.right); + } + if(node1.right == null) + node1.right = node2.right; + } + return t1; + } +} +``` + +### 14. 不同的二叉搜索树 + +[https://leetcode-cn.com/problems/unique-binary-search-trees/](https://leetcode-cn.com/problems/unique-binary-search-trees/) + +动态规划 + +假设n个节点存在二叉排序树的个数是G(n),令f(i)为以i为根的二叉搜索树的个数 + +即有:G(n) = f(1) + f(2) + f(3) + f(4) + ... + f(n) + +n为根节点,当i为根节点时,其左子树节点个数为[1,2,3,...,i-1],右子树节点个数为[i+1,i+2,...n],所以当i为根节点时,其左子树节点个数为i-1个,右子树节点为n-i,即f(i) = G(i-1)*G(n-i), + +上面两式可得:G(n) = G(0)*G(n-1)+G(1)*(n-2)+...+G(n-1)*G(0) + +```java +class Solution { + public int numTrees(int n) { + int[] dp = new int[n + 1]; + dp[0] = 1; + dp[1] = 1; + for (int i = 2; i <=n; i++){ + for (int j = 1; j <= i; j++){ + dp[i] += dp[j - 1] * dp[i - j]; + } + } + return dp[n]; + } +} +``` + +### 222. 完全二叉树的节点个数 +[https://leetcode-cn.com/problems/count-complete-tree-nodes/](https://leetcode-cn.com/problems/count-complete-tree-nodes/) + +```java +class Solution { + public int countNodes(TreeNode root) { + return root == null ? 0 : 1 + countNodes(root.left) + countNodes(root.right); + } +} +``` + +### 98. 验证二叉搜索树 +[https://leetcode-cn.com/problems/validate-binary-search-tree/](https://leetcode-cn.com/problems/validate-binary-search-tree/) + +```java +class Solution { + public boolean isValidBST(TreeNode root) { + return validate(root, Long.MIN_VALUE, Long.MAX_VALUE); + } + + public boolean validate(TreeNode node, long min, long max) { + if (node == null) + return true; + if (node.val <= min || node.val >= max) + return false; + return validate(node.left, min, node.val) && validate(node.right, node.val, max); + } +} +``` + +### 257. 二叉树的所有路径 +[https://leetcode-cn.com/problems/binary-tree-paths/](https://leetcode-cn.com/problems/binary-tree-paths/) + +```java +class Solution { + private List res = new ArrayList<>(); + public List binaryTreePaths(TreeNode root) { + dfs(root, new StringBuilder()); + System.out.println(res.toString()); + return res; + } + + public void dfs(TreeNode node, StringBuilder path) { + if (node == null) + return; + path.append(node.val); + if (node.left == null && node.right == null) { + res.add(path.toString()); + return; + } else { + dfs(node.left, new StringBuilder(path).append("->")); + dfs(node.right, new StringBuilder(path).append("->")); + } + // path.deleteCharAt(path.length() - 1); + } +} +``` + +### 124. 二叉树中的最大路径和 +[https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/) + +```java +class Solution { + int max = Integer.MIN_VALUE; + public int maxPathSum(TreeNode root) { + help(root); + return max; + } + + int help(TreeNode root) { + if (root == null) + return 0; + int left = help(root.left); + int right = help(root.right); + max = Math.max(max, left + right + root.val); + int res = root.val + Math.max(left, right); + return res > 0 ? res : 0; + } +} +``` + +### 814. 二叉树剪枝 +[https://leetcode-cn.com/problems/binary-tree-pruning/](https://leetcode-cn.com/problems/binary-tree-pruning/) + +```java +class Solution { + public TreeNode pruneTree(TreeNode root) { + if (root == null) + return null; + root.left = pruneTree(root.left); + root.right = pruneTree(root.right); + if (root.left == null && root.right == null && root.val == 0) + return null; + return root; + } +} +``` + +### 1026. 节点与其祖先之间的最大差值 +[https://leetcode-cn.com/problems/maximum-difference-between-node-and-ancestor/](https://leetcode-cn.com/problems/maximum-difference-between-node-and-ancestor/) + +```java +class Solution { + public int maxAncestorDiff(TreeNode root) { + int left = dfs(root.left, root.val, root.val); + int right = dfs(root.right, root.val, root.val); + return Math.max(left, right); + } + + public int dfs(TreeNode root, int max, int min) { + if (root == null) + return 0; + max = Math.max(root.val, max); + min = Math.min(root.val, min); + if (root.left == null && root.right == null) + return max - min; + int left = dfs(root.left, max, min); + int right = dfs(root.right, max, min); + return Math.max(left, right); + } +} +``` + +## dfs||回溯 +### 1.1、[矩阵中的路径](https://www.nowcoder.com/practice/c61c6999eecb4b8f88a98f66b273a3cc?tpId=13&tqId=11218&tPage=4&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof/](https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof/) + +```java +private final static int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; +private int rows; +private int cols; + +public boolean hasPath(char[] array, int rows, int cols, char[] str) { + if (rows == 0 || cols == 0) return false; + this.rows = rows; + this.cols = cols; + boolean[][] marked = new boolean[rows][cols]; + char[][] matrix = buildMatrix(array); + for (int i = 0; i < rows; i++) + for (int j = 0; j < cols; j++) + if (backtracking(matrix, str, marked, 0, i, j)) + return true; + + return false; +} + +private boolean backtracking(char[][] matrix, char[] str, + boolean[][] marked, int pathLen, int r, int c) { + // 如果长度满足,则为true:true的条件 + if (pathLen == str.length) return true; + // 如果任意满足,则false:false的条件 + if (r < 0 || r >= rows || c < 0 || c >= cols + || matrix[r][c] != str[pathLen] || marked[r][c]) { + + return false; + } + // 我这个元素只能拿一次,递归的时候,你不能拿了 + marked[r][c] = true; + for (int[] n : next) + if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1])) + return true; + // 递归结束,该元素为false,意味着,可以拿了,回溯嘛,就像线程切换一样 + marked[r][c] = false; + return false; +} + +private char[][] buildMatrix(char[] array) { + char[][] matrix = new char[rows][cols]; + for (int r = 0, idx = 0; r < rows; r++) + for (int c = 0; c < cols; c++) + matrix[r][c] = array[idx++]; + return matrix; +} +``` + + +### 1.2. 单词搜索(420) +[https://leetcode-cn.com/problems/word-search/](https://leetcode-cn.com/problems/word-search/) +```java +class Solution { + private final static int[][] direction = {{1,0},{-1,0},{0,1},{0,-1}}; + private int m; + private int n; + public boolean exist(char[][] board, String word) { + if (word == null || word.length() == 0) return true; + if (board == null || board.length == 0 || board[0].length == 0) return false; + m = board.length; + n = board[0].length; + boolean[][] hasVisited = new boolean[m][n]; + for (int r = 0; r < m; r++) { + for (int c = 0; c < n; c++) { + if (backtracking(0, r, c, hasVisited, board, word)) { + return true; + } + } + } + return false; + } + private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) { + // 符合条件 + if (curLen == word.length()) return true; + // 不符合条件 + if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != word.charAt(curLen) || visited[r][c]) return false; + // 表面元素已用过 + visited[r][c] = true; + for (int[] d : direction) { + if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) return true; + } + // 可以重新使用 + visited[r][c] = false; + return false; + } +} +``` + + + +### 2.1、[字符串的排列](https://www.nowcoder.com/practice/fe6b651b66ae47d7acce78ffdd9a96c7?tpId=13&tqId=11180&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +[https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/](https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/) + +```java +private ArrayList ret = new ArrayList<>(); + +public ArrayList Permutation(String str) { + if (str.length() == 0) + return ret; + char[] chars = str.toCharArray(); + // 排序,过滤重复 + Arrays.sort(chars); + backtracking(chars, new boolean[chars.length], new StringBuilder()); + return ret; +} + +private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) { + // 满足条件 + if (s.length() == chars.length) { + ret.add(s.toString()); + return; + } + // 遍历 + for (int i = 0; i < chars.length; i++) { + // 我已经拿过了,不能在拿了。 + if (hasUsed[i]) + continue; + // 避免重复,实际上优化! 注意后面那个条件,上一个元素没用过 + if (i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) /* 保证不重复 */ + continue; + // 标记只能取一次 + hasUsed[i] = true; + s.append(chars[i]); + backtracking(chars, hasUsed, s); + s.deleteCharAt(s.length() - 1); + hasUsed[i] = false; + } +} + +``` + +### 2.2. 全排列(985) +[https://leetcode-cn.com/problems/permutations/](https://leetcode-cn.com/problems/permutations/) + +```java +class Solution { + public List> permute(int[] nums) { + List> permutes = new ArrayList<>(); + List permuteList = new ArrayList<>(); + boolean[] hasVisited = new boolean[nums.length]; + backtracking(permuteList, permutes, hasVisited, nums); + return permutes; + } + private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { + // 满足条件 + if (permuteList.size() == nums.length) { + permutes.add(new ArrayList<>(permuteList)); // 重新构造一个List + return; + } + // 遍历 + for (int i = 0; i < visited.length; i++) { + // 已经拿过了,不能再拿了 + if (visited[i]) + continue; + // 标记 + visited[i] = true; + permuteList.add(nums[i]); + backtracking(permuteList, permutes, visited, nums); + // 回溯 + permuteList.remove(permuteList.size() - 1); + visited[i] = false; + } + + } +} +``` + +### 2.3. 全排列 II(429) +[https://leetcode-cn.com/problems/permutations-ii/](https://leetcode-cn.com/problems/permutations-ii/) + +```java +class Solution { + public List> permuteUnique(int[] nums) { + List> permutes = new ArrayList<>(); + List permuteList = new ArrayList<>(); + Arrays.sort(nums); // 排序,为了避免重复 + boolean[] hasVisited = new boolean[nums.length]; + backtracking(permuteList, permutes, hasVisited, nums); + return permutes; + } + private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { + // 满足条件 + if (permuteList.size() == nums.length) { + permutes.add(new ArrayList<>(permuteList)); + return; + } + // 遍历 + for (int i = 0; i < visited.length; i++) { + // 避免重复 + if (i != 0 && nums[i] == nums[i -1] && !visited[i - 1]) { + continue; // 防止重复 + } + // 表明已经拿了,退出 + if (visited[i]) + continue; + // 标记,只能拿一次 + visited[i] = true; + permuteList.add(nums[i]); + backtracking(permuteList, permutes, visited, nums); + // 回溯 + permuteList.remove(permuteList.size() - 1); + visited[i] = false; + } + } +} +``` + +### 2.4. 组合总和(582) +[https://leetcode-cn.com/problems/combination-sum/](https://leetcode-cn.com/problems/combination-sum/) + +```java +class Solution { + public List> combinationSum(int[] candidates, int target) { + List> combinations = new ArrayList<>(); + backtracking(new ArrayList<>(), combinations, 0, target, candidates); + return combinations; + } + + private void backtracking(List tempCombination, List> combinations, + int start, int target, final int[] candidates) { + // target为0,则满足 + if (target == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + // 遍历从start开始 + for (int i = start; i < candidates.length; i++) { + // 注意这个骚条件,满足才行 + if (candidates[i] <= target) { + tempCombination.add(candidates[i]); + backtracking(tempCombination, combinations, i, target - candidates[i], candidates); + // 回溯 + tempCombination.remove(tempCombination.size() - 1); + } + } + } +} +``` + + +### 2.5. 组合总和 II(401) +[https://leetcode-cn.com/problems/combination-sum-ii/](https://leetcode-cn.com/problems/combination-sum-ii/) + +```java +class Solution { + public List> combinationSum2(int[] candidates, int target) { + List> combinations = new ArrayList<>(); + Arrays.sort(candidates); // 为了避免重复 + backtracking(new ArrayList<>(), combinations, new boolean[candidates.length], 0, target, candidates); + return combinations; + } + + private void backtracking(List tempCombination, List> combinations, + boolean[] hasVisited, int start, int target, final int[] candidates) { + + if (target == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + for (int i = start; i < candidates.length; i++) { + if(hasVisited[i]) + continue; + // 一样的道理 + if (i != 0 && candidates[i] == candidates[i - 1] && !hasVisited[i - 1]) { + continue; + } + if (candidates[i] <= target) { + tempCombination.add(candidates[i]); + // 只能拿一次 + hasVisited[i] = true; + backtracking(tempCombination, combinations, hasVisited, i, target - candidates[i], candidates); + hasVisited[i] = false; + tempCombination.remove(tempCombination.size() - 1); + } + } + } + +} +``` + + +### 3.1. 子集(633) +[https://leetcode-cn.com/problems/subsets/](https://leetcode-cn.com/problems/subsets/) +```java +class Solution { + public List> subsets(int[] nums) { + List> subsets = new ArrayList<>(); + List tempSubset = new ArrayList<>(); + for (int size = 0; size <= nums.length; size++) { + backtracking(0, tempSubset, subsets, size, nums); // 不同的子集大小 + } + return subsets; + } + + private void backtracking(int start, List tempSubset, List> subsets, + final int size, final int[] nums) { + + if (tempSubset.size() == size) { + subsets.add(new ArrayList<>(tempSubset)); + return; + } + for (int i = start; i < nums.length; i++) { + tempSubset.add(nums[i]); + backtracking(i + 1, tempSubset, subsets, size, nums); + tempSubset.remove(tempSubset.size() - 1); + } + } + +} +``` + +### 3.2. 子集 II(304) +[https://leetcode-cn.com/problems/subsets-ii/](https://leetcode-cn.com/problems/subsets-ii/) + +```java +class Solution { + public List> subsetsWithDup(int[] nums) { + Arrays.sort(nums); // 注意 + List> subsets = new ArrayList<>(); + List tempSubset = new ArrayList<>(); + boolean[] hasVisited = new boolean[nums.length]; + for (int size = 0; size <= nums.length; size++) { + backtracking(0, tempSubset, subsets, hasVisited, size, nums); // 不同的子集大小 + } + return subsets; + } + + private void backtracking(int start, List tempSubset, List> subsets, boolean[] hasVisited, + final int size, final int[] nums) { + + if (tempSubset.size() == size) { + subsets.add(new ArrayList<>(tempSubset)); + return; + } + for (int i = start; i < nums.length; i++) { + // 注意 + if (i != 0 && nums[i] == nums[i - 1] && !hasVisited[i - 1]) { + continue; + } + tempSubset.add(nums[i]); + hasVisited[i] = true; + backtracking(i + 1, tempSubset, subsets, hasVisited, size, nums); + hasVisited[i] = false; + tempSubset.remove(tempSubset.size() - 1); + } + } + +} +``` + + + + + +### 4.1. 岛屿数量(853) +[https://leetcode-cn.com/problems/number-of-islands/](https://leetcode-cn.com/problems/number-of-islands/) +```java +class Solution { + // 像这种二维, 定义四个全局方向 + private int m, n; + private int[][] direaction = {{0,1},{0,-1},{1,0},{-1,0}}; + public int numIslands(char[][] grid) { + if (grid == null || grid.length == 0) return 0; + m = grid.length; + n = grid[0].length; + int islandsNum = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + // 不等于0,才能dfs + if (grid[i][j] != '0') { + dfs(grid, i, j); + // 成功一次,加一次 + islandsNum++; + } + } + } + return islandsNum; + } + + private void dfs(char[][] grid, int i, int j) { + // 失败条件 + if (i < 0 || i >= m || j < 0 || j >=n || grid[i][j] == '0') { + return; + } + // 标记,已走过 + grid[i][j] = '0'; + for (int[] d : direaction) { + dfs(grid, i + d[0], j + d[1]); + } + } +} +``` + + + +### 4.2. 岛屿的最大面积(648) +[https://leetcode-cn.com/problems/max-area-of-island/](https://leetcode-cn.com/problems/max-area-of-island/) + +```java +class Solution { + private int m, n; + private int[][] direaction = {{0,1},{0,-1},{1,0},{-1,0}}; + public int maxAreaOfIsland(int[][] grid) { + if(grid == null || grid.length == 0) return 0; + m = grid.length; + n = grid[0].length; + int maxArea = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + // 这里可以加个条件,不等于0进来 + // 每次取最大面积 + maxArea = Math.max(maxArea, dfs(grid, i, j)); + } + } + return maxArea; + } + private int dfs(int[][] grid, int r, int c) { + // 失败条件 + if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 0) { + return 0; + } + // 标记走过 + grid[r][c] = 0; + // 开始dfs + int area = 1; + for (int[] d : direaction) { + area += dfs(grid, r + d[0], c + d[1]); + } + return area; + } +} +``` + +### 5. 电话号码的字母组合(1085) +[https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/) + +```java +class Solution { + private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; + public List letterCombinations(String digits) { + List combinnations = new ArrayList<>(); + if (digits == null || digits.length() == 0) return combinnations; + doCombination(new StringBuilder(), combinnations, digits); + return combinnations; + } + + private void doCombination(StringBuilder prefix, List combinnations, final String digits) { + if (prefix.length() == digits.length()) { + combinnations.add(prefix.toString()); + return; + } + int curDigits = digits.charAt(prefix.length()) - '0'; + String letters = KEYS[curDigits]; + for (char c : letters.toCharArray()) { + prefix.append(c); + doCombination(prefix, combinnations, digits); + prefix.deleteCharAt(prefix.length() - 1); + } + } +} +``` + + +### 6. 被围绕的区域(328) +[https://leetcode-cn.com/problems/surrounded-regions/](https://leetcode-cn.com/problems/surrounded-regions/) + +```java +class Solution { + private int[][] direction = {{0,1},{0,-1},{1,0},{-1,0}}; + private int m, n; + public void solve(char[][] board) { + if (board == null || board.length == 0) return; + m = board.length; + n = board[0].length; + // 边缘两列 + for (int i = 0; i < m; i++) { + dfs(board, i, 0); + dfs(board, i, n - 1); + } + // 上下两行 + for (int i = 0; i < n; i++) { + dfs(board, 0, i); + dfs(board, m - 1, i); + } + + // 再走全部走一遍 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + // 遇见T标记O + if (board[i][j] == 'T') { + board[i][j] = 'O'; + // 遇见O标记X + } else if (board[i][j] == 'O') { + board[i][j] = 'X'; + } + } + } + } + + private void dfs(char[][] board, int r, int c) { + if(r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') { + return; + } + board[r][c] = 'T'; + for (int[] d : direction) { + dfs(board, r + d[0], c + d[1]); + } + } +} +``` + +### 7. 求 [1,n] 这 n 个数字的排列组合有多少个 +条件:相邻的两个数字的绝对值不能等于1. +例如: +4 +[2, 4, 1, 3] +[3, 1, 4, 2] + +```java +private static List> ret = new ArrayList<>(); +private static int n = 0; +public static void main(String[] args){ + Scanner sc = new Scanner(System.in); + n = sc.nextInt(); + boolean[] marked = new boolean[n + 1]; + dfs(0, marked, new ArrayList<>()); + for (List list : ret) { + System.out.println(list.toString()); + } +} + +private static void dfs(int x, boolean[] marked, ArrayList list) { + if (list.size() == n) { + ret.add(new ArrayList<>(list)); + return; + } + // 开始遍历 + for (int i = 1; i <= n; i++) { + // 关键是这个条件 + if (!marked[i] && (list.isEmpty() || Math.abs(list.get(list.size() - 1) - i) != 1)){ + list.add(i); + marked[i] = true; + dfs(x+1, marked, list); + list.remove(list.size() - 1); + marked[i] = false; + } + } +} +``` + +### 51. N皇后 +[https://leetcode-cn.com/problems/n-queens/](https://leetcode-cn.com/problems/n-queens/) + +```java +class Solution { + boolean[] col = null; + boolean[] left = null; + boolean[] right = null; + List> res = new ArrayList<>(); + public List> solveNQueens(int n) { + col = new boolean[n]; + left = new boolean[2 * n - 1]; + right = new boolean[2 * n - 1]; + char[][] board = new char[n][n]; + dfs(board, 0, n); + return res; + } + + public void dfs(char[][] board, int r, int n) { + if (r >= n) { + List list = new ArrayList<>(); + for (int i = 0; i < n; i++) + list.add(new String(board[i])); + res.add(list); + return; + } + Arrays.fill(board[r], '.'); + for (int i = 0; i < n; i++) { + if (!col[i] && !left[r + i] && !right[r - i + n - 1]) { + board[r][i] = 'Q'; + col[i] = true; + left[r + i] = true; + right[r - i + n - 1] = true; + dfs(board, r + 1, n); + board[r][i] = '.'; + col[i] = false; + left[r + i] = false; + right[r - i + n - 1] = false; + } + } + } +} +``` + +### 329. 矩阵中的最长递增路径 +[https://leetcode-cn.com/problems/longest-increasing-path-in-a-matrix/](https://leetcode-cn.com/problems/longest-increasing-path-in-a-matrix/) + +```java +class Solution { + int[][] next = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}}; + int rows = 0, cols = 0; + boolean[][] marked = null; + int[][] res = null; + public int longestIncreasingPath(int[][] matrix) { + if(matrix == null || matrix.length == 0 || matrix[0].length == 0) + return 0; + int max = 0; + this.rows = matrix.length; + this.cols = matrix[0].length; + this.marked = new boolean[rows][cols]; + this.res = new int[rows][cols]; + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + max = Math.max(max, dfs(matrix, i, j)); + } + } + return max; + } + + public int dfs(int[][] matrix, int x, int y) { + if(res[x][y] != 0) { + return res[x][y]; + } + marked[x][y] = true; + int len = 0; + for (int[] n : next) { + int nx = x + n[0]; + int ny = y + n[1]; + if (nx >= 0 && nx < rows && ny >= 0 && ny < cols && matrix[x][y] < matrix[nx][ny] && !marked[nx][ny]) + len = Math.max(len, dfs(matrix, nx, ny)); + } + marked[x][y] = false; + res[x][y] = len + 1; + return res[x][y]; + } +} +``` + +### 93. 复原IP地址 +[https://leetcode-cn.com/problems/restore-ip-addresses/](https://leetcode-cn.com/problems/restore-ip-addresses/) + +```java +class Solution { + List addresses = new ArrayList<>(); + public List restoreIpAddresses(String s) { + + StringBuilder sb = new StringBuilder(); + dfs(0, sb, s); + return addresses; + } + private void dfs(int k, StringBuilder sb, String s) { + if (k == 4 || s.length() == 0) { + if (k == 4 && s.length() == 0) { + addresses.add(sb.toString()); + } + return; + } + for (int i = 0; i < s.length() && i <= 2; i++) { + if (i != 0 && s.charAt(0) == '0') break; + String part = s.substring(0, i + 1); + if (Integer.valueOf(part) <= 255) { + if (sb.length() != 0) { + part = "." + part; + } + sb.append(part); + dfs(k + 1, sb, s.substring(i + 1)); + sb.delete(sb.length() - part.length(),sb.length()); + } + } + } +} +``` + +## 栈||队列 +### 1、[用两个栈实现一个队列](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class T5 { + // 双栈实现 + Stack in = new Stack<>(); + Stack out = new Stack<>(); + + public void push (int node) { + // 添加value + in.push(node); + } + // 主要逻辑在pop上 + public int pop() { + // 判断stack2是否为空 + if (out.isEmpty()) { + // 如果为空 + while (!in.isEmpty()) { + // 并且stack1不为空,然后将栈1所有的元素重新弹出去添加到栈2 + // 这样的话,用栈2弹,就是FIFO的队列了 + out.push(stack1.pop()); + } + } + return out.pop(); + } +} +``` + +### 2、[包含min函数的栈](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) +```java +public class T20 { + // 双栈 + private Stack dataStack = new Stack<>(); + private Stack minStack = new Stack<>(); + + public void push(int node) { + dataStack.push(node);// dataStack添加元素 + // 主要逻辑在这,比大小 + minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node)); + } + + public void pop() { + dataStack.pop(); + // 辅助栈也得弹,因为每次push, 辅助栈也在push + minStack.pop(); + } + // 栈顶,没啥可说的 + public int top() { + return dataStack.peek(); + } + // 最小值,辅助栈弹就完事了 + public int min() { + return minStack.peek(); + } +} +``` + +### 3. 用队列实现栈(4169) +[https://leetcode-cn.com/problems/implement-stack-using-queues/](https://leetcode-cn.com/problems/implement-stack-using-queues/) + +```java +class MyStack { + private Queue queue; + /** Initialize your data structure here. */ + public MyStack() { + queue = new LinkedList<>(); + } + + /** Push element x onto stack. */ + public void push(int x) { + queue.add(x); + int cnt = queue.size(); + // 主要是这个while,元素倒过来 + while (cnt-- > 1) { + queue.add(queue.poll()); + } + } + + /** Removes the element on top of the stack and returns that element. */ + public int pop() { + return queue.remove(); + } + + /** Get the top element. */ + public int top() { + return queue.peek(); + } + + /** Returns whether the stack is empty. */ + public boolean empty() { + return queue.isEmpty(); + } +} +``` + +### 4. 用数组实现栈 + +```java +public class MyStack { + int[] data; // 数组 + int size; // 长度 + int top; // 栈顶的位置 + + public MyStack(int size) { + this.size = size; + data = new int[size]; + top = -1; + } + + public boolean isEmpty() { + return top == -1; + } + + public boolean isFull() { + return (top+1) == size; + } + + public boolean push(int data) { + if (isFull()) { + System.out.println("the stack is full!"); + return false; + } else { + this.data[++top] = data; + } + } + + public int pop() throws Exception { + if (isEmpty()) { + throw new Exception("the stack is empty!"); + } else { + return this.data[top--]; + } + } + + public int peek() { + return this.data[top]; + } +} +``` + +### 栈排序 +[https://leetcode-cn.com/problems/sort-of-stacks-lcci/](https://leetcode-cn.com/problems/sort-of-stacks-lcci/) + +```java +class SortedStack { + + private Stack s1; + private Stack s2; + public SortedStack() { + s1 = new Stack(); // 升序 + s2 = new Stack(); // 降序 + } + + public void push(int val) { + if(s1.isEmpty()) { + s1.push(val); + return; + } + while(!s1.isEmpty() && s1.peek() < val) { + s2.push(s1.pop()); // 原栈存在比val小的值 + } + s1.push(val); + while(!s2.isEmpty()) { + s1.push(s2.pop()); // 辅助栈存在比val大的值 + } + } + + public void pop() { + if(s1.isEmpty()) + return; + s1.pop(); + } + + public int peek() { + if(s1.isEmpty()) + return -1; + return s1.peek(); + } + + public boolean isEmpty() { + return s1.isEmpty(); + } +} +``` + +### 503. 下一个更大元素 II +[https://leetcode-cn.com/problems/next-greater-element-ii/](https://leetcode-cn.com/problems/next-greater-element-ii/) + +```java +class Solution { + public int[] nextGreaterElements(int[] nums) { + int n = nums.length; + int[] res = new int[n]; + Arrays.fill(res, -1); + Stack stack = new Stack<>(); + for (int i = 0; i < n * 2; i++) { + int num = nums[i % n]; + while (!stack.isEmpty() && num > nums[stack.peek()]) + res[stack.pop()] = num; + if (i < n) + stack.push(i); + } + return res; + } +} +``` + +## 排序||top + +### 归并 +- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列; +- 设定两个指针,最初位置分别为两个已经排序序列的起始位置; +- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置; +- 重复步骤 3 直到某一指针达到序列尾; +- 将另一序列剩下的所有元素直接复制到合并序列尾。 + +```java +public class MergeSort { + public static void mergeSort(int[] arr) { + if (arr == null || arr.length < 2) { + return; + } + + mergeSort(arr, 0, arr.length - 1); + } + + public static void mergeSort(int[] arr, int left, int right) { + if (left == right) return; + int mid = left + ((right - left) >> 1); + // left + mergeSort(arr, left, mid); + // right + mergeSort(arr, mid + 1, right); + // merge + merge(arr, left, mid, right); + } + + public static void merge(int[] arr, int left, int mid, int right) { + int[] help = new int[right - left + 1]; + int i = 0; + int p1 = left; + int p2 = mid + 1; + while (p1 <= mid && p2 <= right) { + help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; + } + while(p1 <= mid) { + help[i++] = arr[p1++]; + } + while(p2 <= right) { + help[i++] = arr[p2++]; + } + for (int j = 0; j < help.length; j++) { + arr[left + j] = help[j]; + } + } +} + +``` + +### 快排 + +- 从数列中挑出一个元素,称为"基准"; +- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以放到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区操作; +- 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序; + +```java +public class QuickSort { + public static int[] quickSort(int[] arr) { + return quickSort(arr, 0, arr.length - 1); + } + + public static int[] quickSort(int[] arr, int left, int right) { + if (left < right) { + int partitionIndex = partition(arr, left, right); + // 左半部分递归 + quickSort(arr, left, partitionIndex - 1); + // 右半部分递归 + quickSort(arr, partitionIndex + 1, right); + } + return arr; + } + + public static int partition(int[] arr, int left, int right) { + int pivot = left; + int index = pivot + 1; + for (int i = index; i <= right; i++) { + if (arr[i] < arr[pivot]) { + swap(arr, i, index++); + } + } + swap(arr, pivot, index - 1); + return index - 1; + } + + public static void swap(int[] arr, int i, int j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + +} + +``` + +### 1、[最小的k个数](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class Solution { + public ArrayList GetLeastNumbers_Solution(int [] input, int k) { + ArrayList list = new ArrayList<>(); + if (input == null || input.length == 0 || k > input.length) return list; + Arrays.sort(input); + // 犯规就犯规 + for (int i = 0; i < k; i++) { + list.add(input[i]); + } + return list; + } +} +``` + +### 2. 数组中的第K个最大元素(855) +[https://leetcode-cn.com/problems/kth-largest-element-in-an-array/](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/) + +排序 :时间复杂度 O(NlogN),空间复杂度 O(1) +```java +public int findKthLargest(int[] nums, int k) { + Arrays.sort(nums); + return nums[nums.length - k]; +} +``` + +堆 :时间复杂度 O(NlogK),空间复杂度 O(K)。 +```java +public int findKthLargest(int[] nums, int k) { + PriorityQueue pq = new PriorityQueue<>(); // 小顶堆 + for (int val : nums) { + pq.add(val); + if (pq.size() > k) // 维护堆的大小为 K + pq.poll(); + } + return pq.peek(); +} +``` + +快排 +```java +public int findKthLargest(int[] nums, int k) { + // 注意k + k = nums.length - k; + int l = 0, h = nums.length - 1; + while (l < h) { + int j = partition(nums, l, h); + if (j == k) { + break; + } else if (j < k) { + l = j + 1; + } else { + h = j - 1; + } + } + return nums[k]; +} + +private int partition(int[] a, int l, int h) { + int pivot = l; + int index = pivot + 1; + for (int i = index; i <= h; i++) { + if (a[i] < a[pivot]) + swap(a, i, index++); + } + swap(a, pivot, index - 1); + return index - 1; +} + +private void swap(int[] a, int i, int j) { + int t = a[i]; + a[i] = a[j]; + a[j] = t; +} +``` + +### 3. 寻找两个正序数组的中位数 +[https://leetcode-cn.com/problems/median-of-two-sorted-arrays/](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/) + +// 归并 + +```java +class Solution { + public double findMedianSortedArrays(int[] nums1, int[] nums2) { + int[] temp = new int[nums1.length + nums2.length]; + // 归并,三个指针,走起 + int i = 0; + int j = 0; + int t = 0; + while (i < nums1.length && j < nums2.length) { + temp[t++] = nums1[i] <= nums2[j] ? nums1[i++] : nums2[j++]; + } + + while (i < nums1.length) { + temp[t++] = nums1[i++]; + } + + while (j < nums2.length) { + temp[t++] = nums2[j++]; + } + // 加起来,取中位数 + double b = (temp[(temp.length - 1) / 2] + temp[temp.length / 2]) * 1.0 / 2; + return b; + } +} +``` + +### 4. 根据字符出现频率排序 + +```java +// 注意list的sort +class Solution { + public String frequencySort(String s) { + if (s == null || s.length() == 0) + return s; + // 先map统计 + Map map = new HashMap<>(); + for (int i = 0; i < s.length(); i++){ + char c = s.charAt(i); + map.put(c, map.getOrDefault(c, 0) + 1); + } + // 然后map.entrySet传进来 + List> list = new ArrayList<>(map.entrySet()); + // 按照value排序 + Collections.sort(list, (o1, o2) -> { + return o2.getValue() - o1.getValue(); + }); + // list + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : list){ + int cnt = entry.getValue(); + char key = entry.getKey(); + while (cnt-- > 0) + sb.append(key); + } + return sb.toString(); + } +} +``` + + + +## 字符串 +### 1. 字符串相加 +[https://leetcode-cn.com/problems/add-strings/](https://leetcode-cn.com/problems/add-strings/) + +```java +class Solution { + public String addStrings(String num1, String num2) { + StringBuilder str = new StringBuilder(); + // 三个变量 carry i j:倒着来 + int carry = 0, i = num1.length() - 1, j = num2.length() - 1; + // while循环条件 注意|| + while (carry == 1 || i >= 0 || j >= 0) { + // 注意"0" + int x = i < 0 ? 0 : num1.charAt(i--) - '0'; + int y = j < 0 ? 0 : num2.charAt(j--) - '0'; + // 老生长谈了 + // 加的时候 + str.append((x + y + carry) % 10); + // 注意进位 + carry = (x + y + carry) / 10; + } + // 别忘了反转 + // 反转 + return str.reverse().toString(); + } +} +``` + +### 43. 字符串相乘 + +[https://leetcode-cn.com/problems/multiply-strings/](https://leetcode-cn.com/problems/multiply-strings/) + +```java +class Solution { + public String multiply(String num1, String num2) { + int len1 = num1.length(); + int len2 = num2.length(); + if (len1 == 0 || len2 == 0) return "0"; + int[] mul = new int[len1 + len2]; + for (int i = len1 - 1; i >= 0; i--){ + for (int j = len2 - 1; j >= 0; j--){ + int n = (num1.charAt(i) - '0') * (num2.charAt(j) - '0') + mul[i + j + 1]; + mul[i + j + 1] = n % 10; + mul[i + j] += n / 10; + } + } + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < len1 + len2 - 1 && mul[i] == 0) i++; + while (i < len1 + len2) sb.append(mul[i++]); + return sb.toString(); + } +} +``` + +### 2. 反转字符串(660) +[https://leetcode-cn.com/problems/reverse-string/](https://leetcode-cn.com/problems/reverse-string/) + +```java +// 利用while反转交换 +class Solution { + public void reverseString(char[] s) { + int p1 = 0, p2 = s.length - 1; + while(p1 < p2){ + swap(s, p1++, p2--); + } + } + public void swap(char[] s, int i, int j) { + char temp = s[i]; + s[i] = s[j]; + s[j] = temp; + } +} +``` + +### 3. 无重复字符的最长子串(2862) +[https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) + +```java +class Solution { + public int lengthOfLongestSubstring(String s) { + int n = s.length(), ans = 0; + // map 加双指针。map来保留索引,类似于滑动窗 + Map map = new HashMap<>(); + for (int i = 0, j = 0; j < n; j++) { + if (map.containsKey(s.charAt(j))) { + i = Math.max(map.get(s.charAt(j)), i); + } + ans = Math.max(ans, j - i + 1); + map.put(s.charAt(j), j + 1); + } + return ans; + } +} +``` +### 4.1、[左旋转字符串](https://www.nowcoder.com/practice/12d959b108cb42b1ab72cef4d36af5ec?tpId=13&tqId=11196&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public String LeftRotateString(String str, int n) { + if (n >= str.length()) + return str; + char[] chars = str.toCharArray(); + // 分三步反转 + // 1. n之前反转 + reverse(chars, 0, n - 1); + // 2. n之后反转 + reverse(chars, n, chars.length - 1); + // 3. 全部反转 + reverse(chars, 0, chars.length - 1); + return new String(chars); +} + +private void reverse(char[] chars, int i, int j) { + while (i < j) + swap(chars, i++, j--); +} + +private void swap(char[] chars, int i, int j) { + char t = chars[i]; + chars[i] = chars[j]; + chars[j] = t; +} + +``` + +### 4.2、[翻转单词顺序列](https://www.nowcoder.com/practice/3194a4f4cf814f63919d0790578d51f3?tpId=13&tqId=11197&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) +正确的解法应该是和书上一样,先旋转每个单词,再旋转整个字符串。 + +```java +public String ReverseSentence(String str) { + int n = str.length(); + char[] chars = str.toCharArray(); + int i = 0, j = 0; + // 双指针,滑窗,,注意边界。 + while (j <= n) { + // 关键是这个判断边界 + if (j == n || chars[j] == ' ') { + // 反转 + reverse(chars, i, j - 1); + // 下个单词的索引开头 + i = j + 1; + } + // 继续走 + j++; + } + // 全反转 + reverse(chars, 0, n - 1); + return new String(chars); +} + +private void reverse(char[] c, int i, int j) { + while (i < j) + swap(c, i++, j--); +} + +private void swap(char[] c, int i, int j) { + char t = c[i]; + c[i] = c[j]; + c[j] = t; +} +``` + +### 5、[把字符串转成整数](https://www.nowcoder.com/practice/1277c681251b4372bdef344468e4f26e?tpId=13&tqId=11202&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class T49 { + public int StrToInt(String str) { + if (str == null || str.length() == 0) + return 0; + // 注意第一个字符是否是- + boolean isNegative = str.charAt(0) == '-'; + int ret = 0; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + // 跳过第一个字符 + if (i == 0 && (c == '+' || c == '-')) /* 符号判定 */ + continue; + // 防止非法输入 + if (c < '0' || c > '9') /* 非法输入 */ + return 0; + // 正常操作,注意“0” + ret = ret * 10 + (c - '0'); + } + return isNegative ? -ret : ret; + } +} +``` +### 5. 最长回文子串(1478) +[https://leetcode-cn.com/problems/longest-palindromic-substring/](https://leetcode-cn.com/problems/longest-palindromic-substring/) + +中心扩展 +- 两种情况 +- 奇数长度 +- 偶数长度 +- 取最长,求起始和结束位置 +- 用substring即可 + +```java +class Solution { + public String longestPalindrome(String s) { + if (s == null || s.length() == 0) return s; + int start = 0, end = 0; // 记录起始位置 + for (int i = 0; i < s.length(); i++) { + // 两种情况 以i为中心,以i和i+1为中心 + int len1 = expand(s, i - 1, i + 1); // 中心扩展 + int len2 = expand(s, i, i + 1); + int len = Math.max(len1, len2); // 取最长的长度 + if (len > end - start) { + start = i - (len - 1) / 2; + end = i + len / 2; + } + } + return s.substring(start, end + 1); + } + + private int expand(String s, int l, int r) { + while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { + l--; + r++; + } + // 这里要注意 + return r - l - 1; + } +} +``` + +## 数组 + +### 189. 旋转数组 +[https://leetcode-cn.com/problems/rotate-array/](https://leetcode-cn.com/problems/rotate-array/) + +```java +class Solution { + public void rotate(int[] nums, int k) { + if (nums == null || nums.length == 0) + return; + int n = nums.length; + k %= n; + reverse(nums, 0, n - 1); + reverse(nums, 0, k - 1); + reverse(nums, k, n - 1); + } + + public void reverse(int[] nums, int l, int r) { + while (l < r){ + int t = nums[l]; + nums[l++] = nums[r]; + nums[r--] = t; + } + } +} +``` + +### 1. 搜索旋转排序数组 +[https://leetcode-cn.com/problems/search-in-rotated-sorted-array/](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/) +思路:如果中间的数小于最右边的数,则右半段是有序的,若中间数大于最右边数,则左半段是有序的,我们只要在有序的半段里用首尾两个数组来判断目标值是否在这一区域内,这样就可以确定保留哪半边了 +```java +class Solution { + public int search(int[] nums, int target) { + int len = nums.length; + int left = 0, right = len - 1; + while (left <= right) { + int mid = (left + right) / 2; + if (nums[mid] == target) + return mid; + else if(nums[mid] < nums[right]) { + // 注意边界 + if (nums[mid] < target && target <= nums[right]) + left = mid + 1; + else + right = mid - 1; + } else { + // 注意边界 + if (nums[left] <= target && target < nums[mid]) + right = mid - 1; + else + left = mid + 1; + } + } + return -1; + } +} +``` + +### 2. 两数之和(4897) +[https://leetcode-cn.com/problems/two-sum/](https://leetcode-cn.com/problems/two-sum/) +```java +// 双指针 +class Solution { + public int[] twoSum(int[] nums, int target) { + int p1 = 0, p2 = nums.length - 1; + while (p1 < p2) { + int sum = nums[p1] + nums[p2]; + if (sum < target) p1++; + else if (sum > target) p2--; + else return new int[] {p1, p2}; + } + return new int[]{}; + } +} +``` + +### 3. 三数之和 +[https://leetcode-cn.com/problems/3sum/](https://leetcode-cn.com/problems/3sum/) +```java +排序过后的双指针,注意重复 +class Solution { + public List> threeSum(int[] nums) { + // 排序 + Arrays.sort(nums); + List> ls = new ArrayList<>(); + for (int i = 0; i < nums.length - 2; i++) { + // 判断是否元素大于0,大于0,没必要操作了 + if (nums[i] > 0) + break; + // 判断是否重复 + if (i > 0 && nums[i] == nums[i - 1]) + continue; + // 双指针操作 + int l = i + 1, r = nums.length - 1; + while (l < r) { + if (nums[l] + nums[r] < -nums[i]) l++; + else if (nums[l] + nums[r] > -nums[i]) r--; + else { + // 相等了哈 + ls.add(Arrays.asList(nums[i], nums[l], nums[r])); + // 防止重复 + while (l < r && nums[l] == nums[l + 1]) l++; + while (l < r && nums[r] == nums[r - 1]) r--; + l++; + r--; + } + } + } + return ls; + } +} +``` + + + +### 5、[顺时针打印矩阵](https:/www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a?tpId=13&tqId=11172&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) +跟lc的螺旋矩阵一样 +```java +public class T19 { + public ArrayList printMatrix(int [][] matrix) { + ArrayList list = new ArrayList<>(); + int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1; + while(r1 <= r2 && c1 <= c2) { + for (int i = c1; i <= c2; i++) { + list.add(matrix[r1][i]); + } + for (int i = r1 + 1; i <= r2; i++) { + list.add(matrix[i][c2]); + } + // 注意边界 + if (r1 != r2) { + for (int i = c2 - 1; i >= c1; i--) { + list.add(matrix[r2][i]); + } + } + // 注意边界 + if (c1 != c2) { + for (int i = r2 - 1; i >= r1; i--) { + list.add(matrix[i][c1]); + } + } + r1++; r2--; c1++; c2--; + } + return list; + } +} +``` + +### 6. 缺失的第一个正数 +[https://leetcode-cn.com/problems/first-missing-positive/](https://leetcode-cn.com/problems/first-missing-positive/) +采用排序的犯规操作 +```java +class Solution { + public int firstMissingPositive(int[] nums) { + int ans = 1; + // 犯规操作 + Arrays.sort(nums); + for (int i = 0; i < nums.length; i++) { + if (nums[i] > ans) break; + if (nums[i] == ans) ans++; + } + return ans; + } +} +``` + +### 448. 找到所有数组中消失的数字 +[https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/](https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/) + +```java +/** + * 【笔记】将所有正数作为数组下标,置对应数组值为负值。那么,仍为正数的位置即为(未出现过)消失的数字。 + * + * 举个例子: + * + * 原始数组:[4,3,2,7,8,2,3,1] + * + * 重置后为:[-4,-3,-2,-7,8,2,-3,-1] + * + * 结论:[8,2] 分别对应的index为[5,6](消失的数字) + */ + class Solution { + public List findDisappearedNumbers(int[] nums) { + for (Integer num : nums) { + nums[Math.abs(num) - 1] = -Math.abs(nums[Math.abs(num) - 1]); + } + List list = new ArrayList<>(); + for(int i = 0; i < nums.length; i++) { + if (nums[i] > 0) + list.add(i + 1); + } + System.out.println(list.toString()); + return list; + } +} +``` + +### 560. 和为K的子数组 +[https://leetcode-cn.com/problems/subarray-sum-equals-k/](https://leetcode-cn.com/problems/subarray-sum-equals-k/) + +```java +class Solution { + /** + 扫描一遍数组, 使用map记录出现同样的和的次数, 对每个i计算累计和sum并判断map内是否有sum-k + **/ + public int subarraySum(int[] nums, int k) { + Map map = new HashMap<>(); + map.put(0, 1); + int sum = 0, ret = 0; + for (int i = 0; i < nums.length; i++) { + sum += nums[i]; + if (map.containsKey(sum - k)) + ret += map.get(sum - k); + map.put(sum, map.getOrDefault(sum, 0) + 1); + } + return ret; + } +} +``` + +### 56. 合并区间 +[https://leetcode-cn.com/problems/merge-intervals/](https://leetcode-cn.com/problems/merge-intervals/) + +```java +class Solution { + public int[][] merge(int[][] intervals) { + if (intervals == null || intervals.length <= 1) return intervals; + Arrays.sort(intervals, (a, b) -> a[0] - b[0]); + List list = new ArrayList<>(); + int i = 0; + int n = intervals.length; + while (i < n) { + int l = intervals[i][0]; + int r = intervals[i][1]; + while (i < n - 1 && r >= intervals[i + 1][0]) { + r = Math.max(r, intervals[i + 1][1]); + i++; + } + list.add(new int[] {l, r}); + i++; + } + return list.toArray(new int[list.size()][2]); + } +} +``` + +### 674. 最长连续递增序列 +[https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence/](https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence/) + +```java +class Solution { + public int findLengthOfLCIS(int[] nums) { + if (nums == null || nums.length == 0) + return 0; + int d = 0; + int max = 1; + for (int i = 1; i < nums.length; i++) { + if (nums[i] > nums[i - 1]) + max = Math.max(i - d + 1, max); + else + d = i; + } + return max; + } +} +``` + +### 986. 区间列表的交集 +[https://leetcode-cn.com/problems/interval-list-intersections/](https://leetcode-cn.com/problems/interval-list-intersections/) + +```java +class Solution { + public int[][] intervalIntersection(int[][] A, int[][] B) { + List list = new ArrayList<>(); + for (int i = 0, j = 0; i < A.length && j < B.length;) { + int l = Math.max(A[i][0], B[j][0]); + int r = Math.min(A[i][1], B[j][1]); + if (l < r) + list.add(new int[]{l, r}); + else if (l == r) + list.add(new int[] {l, l}); + if (A[i][1] < B[j][1]) + i++; + else + j++; + } + return list.toArray(new int[list.size()][]); + } +} +``` + +## dp||贪心 + +### 1.1、[斐波那契数列](https://www.nowcoder.com/practice/c6c7742f5ba7442aada113136ddea0c3?tpId=13&tqId=11160&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class T7 { + public int Fibonacci(int n) { + // 条件 + if (n <= 1) return n; + // 可以用自底向上的方法 + int pre2 = 0, pre1 = 1; + int f = 0; + for (int i = 2; i <= n; i++) { + f = pre2 + pre1; // 如果动态规划,这个就是dp的公式 + pre2 = pre1; + pre1 = f; + } + return f; + } +} +``` + +### 1.2、[跳台阶](https://www.nowcoder.com/practice/8c82a5b80378478f9484d87d1c5f12a4?tpId=13&tqId=11161&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) +```java +public class T8 { + public int JumpFloor(int target) { + // 条件 + if (target <= 2) return target; + // 自底向上的方法 + int pre2 = 1, pre1 = 2; + int sum = 0; + for (int i = 3; i <= target; i++) { + sum = pre2 + pre1; // 一样的道理, 和上面那道题的初始值不一样 + pre2 = pre1; + pre1 = sum; + } + return sum; + } +} +``` + +### 1.3、[矩形覆盖](https://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6?tpId=13&tqId=11163&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class T10 { + public int RectCover(int target) { + // 条件 + if (target <= 2) return target; + // 自底向上 + int pre2 = 1, pre1 = 2; + int sum = 0; + for (int i = 3; i <= target; i++) { + sum = pre2 + pre1; // 同理呀 + pre2 = pre1; + pre1 = sum; + } + return sum; + } +} +``` + +### 1.4、[变态跳台阶](https://www.nowcoder.com/practice/22243d016f6b47f2a6928b4313c85387?tpId=13&tqId=11162&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public int JumpFloorII(int target) { + int[] dp = new int[target]; + Arrays.fill(dp, 1); + // 注意起始位置 + for (int i = 1; i < target; i++) + // 开始跳 + for (int j = 0; j < i; j++) + // 注意dp[i] 累计dp[j] + dp[i] += dp[j]; + return dp[target - 1]; +} +``` + + + +### 2. 最大子序和(1385) +[https://leetcode-cn.com/problems/maximum-subarray/](https://leetcode-cn.com/problems/maximum-subarray/) +```java +class Solution { + public int maxSubArray(int[] nums) { + if (nums == null || nums.length == 0) return 0; + // 注意两个变量的初始化 + int preSum = nums[0]; + int maxSum = preSum; + // 注意从1开始 + for (int i = 1; i < nums.length; i++) { + // 注意这个条件 + preSum = preSum > 0 ? preSum + nums[i] : nums[i]; + maxSum = Math.max(maxSum, preSum); + } + return maxSum; + } +} +``` + +### 3.1、[股票的最大利润](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/description/?utm_source=LCUS&utm_medium=ip_redirect_q_uns&utm_campaign=transfer2china) + +```java +class Solution { + public int maxProfit(int[] prices) { + if (prices == null || prices.length == 0) return -1; + int min = prices[0]; + int max = 0; + // 从1开始 + for (int i = 1; i < prices.length; i++) { + // 注意保持最小 + min = prices[i] < min ? prices[i] : min; + max = Math.max(max, prices[i] - min); + } + return max; + } +} +``` +### 3.2 买卖股票的最佳时机 II +[https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/) +```java +class Solution { + public int maxProfit(int[] prices) { + // 贪心:只要我当前数比前一个数大, 就xxx + int profit = 0; + // 从1开始,因为下面的if + for (int i = 1; i < prices.length; i++) { + if (prices[i] > prices[i - 1]) + profit += prices[i]- prices[i - 1]; + } + return profit; + } +} +``` +### 3.3 买卖股票的最佳时机含手续费 +状态机 +```java +class Solution { + public int maxProfit(int[] prices, int fee) { + if (prices == null || prices.length == 0) + return 0; + int buy = Integer.MIN_VALUE; // 购买股票后的收益,开始购买第一支股票后肯定是负数 + int sell = 0; // 售卖第一次股票后 + for (int i = 0; i < prices.length; i++){ + buy = Math.max(buy, sell - prices[i]); + sell = Math.max(sell, buy + prices[i] - fee); //手续费在交易完成时一次性扣除 + } + return sell; + } +} +``` + +### 4. 打家劫舍 +[https://leetcode-cn.com/problems/house-robber/description/](https://leetcode-cn.com/problems/house-robber/description/) +```java +class Solution { + public int rob(int[] nums) { + int pre2 = 0, pre1 = 0; + for (int i = 0; i < nums.length; i++) { + // 注意这个状态转移,毕竟题目是隔着偷 + int cur = Math.max(pre2 + nums[i], pre1); + pre2 = pre1; + pre1 = cur; + } + return pre1; + } +} +``` + +### 5. 打家劫舍 II +[https://leetcode-cn.com/problems/house-robber-ii/description/](https://leetcode-cn.com/problems/house-robber-ii/description/) +```java +class Solution { + public int rob(int[] nums) { + if (nums == null || nums.length == 0) return 0; + int n = nums.length; + if (n == 1) return nums[0]; + // 注意0-n-2 个 1 -n-1 + return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1)); + } + + private int rob(int[] nums, int first, int last) { + int pre2 = 0, pre1 = 0; + for (int i = first; i <= last; i++) { + int cur = Math.max(pre1, pre2 + nums[i]); + pre2 = pre1; + pre1 = cur; + } + return pre1; + } +} +``` + + +### 6、[剪绳子](https://www.nowcoder.com/practice/57d85990ba5b440ab888fc72b0751bf8?tpId=13&tqId=33257&tPage=4&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) +```java +// 动态规划 +public int integerBreak(int n) { + int[] dp = new int[n + 1]; + dp[1] = 1; + // 一厘米,没法切,所以从2 + for (int i = 2; i <= n; i++) + // 切从1cm开始 + for (int j = 1; j < i; j++) + // 注意这个状态转移 + dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j))); + return dp[n]; +} +``` +### 7、[礼物的最大值](https://www.nowcoder.com/questionTerminal/72a99e28381a407991f2c96d8cb238ab) + +```java +public int getMost(int[][] values) { + if (values == null || values.length == 0 || values[0].length == 0) + return 0; + int n = values[0].length; + int[] dp = new int[n]; + for (int[] value : values) { + dp[0] += value[0]; + for (int i = 1; i < n; i++) + dp[i] = Math.max(dp[i], dp[i - 1]) + value[i]; + } + return dp[n - 1]; +} +``` + +### 8. 最小路径和 +[https://leetcode-cn.com/problems/minimum-path-sum/description/](https://leetcode-cn.com/problems/minimum-path-sum/description/) +```java +class Solution { + public int minPathSum(int[][] grid) { + if (grid.length == 0 || grid[0].length == 0) return 0; + int m = grid.length, n = grid[0].length; + // 优化过后的dp + int[] dp = new int[n]; + for (int i = 0; i < m; i ++) { + for (int j = 0; j < n; j++) { + if (j == 0) // 注意 + dp[j] = dp[j]; + else if (i == 0) // 注意 + dp[j] = dp[j - 1]; + else // 注意 + dp[j] = Math.min(dp[j], dp[j - 1]); + // 别忘了 + dp[j] += grid[i][j]; + } + } + return dp[n-1]; + } +} +``` + +```java +class Solution { + public int minPathSum(int[][] grid) { + if (grid == null || grid.length == 0 || grid[0].length == 0) + return 0; + int m = grid.length; + int n = grid[0].length; + int[][] dp = new int[m][n]; + dp[0][0] = grid[0][0]; + // 第一列 + for (int i = 1; i < m; i++){ + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 第一行 + for (int j = 1; j < n; j++){ + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++){ + dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; + } + } + return dp[m - 1][n - 1]; + + } +} +``` + +### 9. 不同路径 +[https://leetcode-cn.com/problems/unique-paths/description/](https://leetcode-cn.com/problems/unique-paths/description/) + +```java +class Solution { + public int uniquePaths(int m, int n) { + // 优化过后了 + int[] dp = new int[n]; + // 注意 + Arrays.fill(dp, 1); + // 注意起始位置 + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + // 累加 + dp[j] += dp[j - 1]; + } + } + return dp[n -1]; + } +} +``` + +```java +class Solution { + public int uniquePaths(int m, int n) { + int[][] dp = new int[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (i == 0 || j == 0) + dp[i][j] = 1; + else { + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + } + return dp[m - 1][n - 1]; + } +} +``` + +### 9.2 不同路径 II +[https://leetcode-cn.com/problems/unique-paths-ii/](https://leetcode-cn.com/problems/unique-paths-ii/) + +```java +class Solution { + public int uniquePathsWithObstacles(int[][] obstacleGrid) { + int m = obstacleGrid.length; + int n = obstacleGrid[0].length; + // 因为if + int[] dp = new int[n + 1]; + dp[1] = 1; // 注意初始值 + // 起始位置 + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 别忘了条件 + if (obstacleGrid[i - 1][j - 1] == 1) + dp[j] = 0; + else + dp[j] += dp[j - 1]; + } + } + return dp[n]; + } +} +``` + +```java +class Solution { + public int uniquePathsWithObstacles(int[][] obstacleGrid) { + int m = obstacleGrid.length; + int n = obstacleGrid[0].length; + int [][] dp = new int[m+1][n+1]; + // 第一行 和 其他行的区别在于没有来自上边的路径 但是 起点到起点 算一条路径 所以这样初始化 + dp[0][1] = 1; + for(int i = 1; i <= m; i++) { + for(int j = 1; j <= n; j++) { + if(obstacleGrid[i-1][j-1] == 1) { + // 障碍 不可达 路径数量为0 + dp[i][j] = 0; + } + else { + // 左 + 上 + dp[i][j] = dp[i-1][j] + dp[i][j-1]; + } + } + } + return dp[m][n]; + } +} +``` + +```java +class Solution { + public int uniquePathsWithObstacles(int[][] obstacleGrid) { + int m = obstacleGrid.length; + int n = obstacleGrid[0].length; + int[][] dp = new int[m][n]; + dp[0][0] = 1; + for (int i = 0; i < m; i++){ + for (int j = 0; j < n; j++){ + if (obstacleGrid[i][j] == 1) + continue; + if (i == 0 && j == 0) + continue; + if(i == 0) + dp[i][j] = dp[i][j - 1]; + else if (j == 0) + dp[i][j] = dp[i - 1][j]; + else dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + return dp[m - 1][n - 1]; + } +} +``` + +### 9.3 最大正方形 +[https://leetcode-cn.com/problems/maximal-square/](https://leetcode-cn.com/problems/maximal-square/) +```java +class Solution { + public int maximalSquare(char[][] matrix) { + /** + dp[i][j]表示以第i行第j列为右下角所能构成的最大正方形边长, 则递推式为: + dp[i][j] = 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]); + **/ + if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return 0; + int m = matrix.length; + int n = matrix[0].length; + int max = 0; + int[][] dp = new int[m + 1][n + 1]; + + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++){ + if (matrix[i-1][j-1] == '1') { + // 左, 上,左上 + dp[i][j] = 1 + Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1])); + max = Math.max(max, dp[i][j]); + } + } + } + return max * max; + } +} +``` + +### 10. 解码方法 +[https://leetcode-cn.com/problems/decode-ways/description/](https://leetcode-cn.com/problems/decode-ways/description/) +```java +class Solution { + public int numDecodings(String s) { + if (s == null || s.length() == 0) { + return 0; + } + int n = s.length(); + int[] dp = new int[n + 1]; + dp[0] = 1; // 初始值 + // 注意第一个元素是0? + dp[1] = s.charAt(0) == '0' ? 0 : 1; + // 注意起始位置, + for (int i = 2; i <= n; i++) { + // substring 用的很骚 + int one = Integer.valueOf(s.substring(i - 1, i)); + if (one != 0) { + dp[i] += dp[i - 1]; + } + // 注意这个判断 + if (s.charAt(i - 2) == '0') continue; + int two = Integer.valueOf(s.substring(i - 2, i)); + if (two <= 26) { + dp[i] += dp[i - 2]; + } + } + return dp[n]; + } +} +``` + +### 11. 最长上升子序列 +[https://leetcode-cn.com/problems/longest-increasing-subsequence/description/](https://leetcode-cn.com/problems/longest-increasing-subsequence/description/) +```java +class Solution { + public int lengthOfLIS(int[] nums) { + if (nums.length == 0) return 0; + int[] dp = new int[nums.length]; + // 注意这个初始化 + Arrays.fill(dp, 1); + for (int i = 0; i < nums.length; i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + // 注意if + dp[i] = Math.max(dp[i], dp[j] + 1); // 关键这里, + } + } + } + // 找最大 + return Arrays.stream(dp).max().orElse(0); + } +} +``` + +### 12. 最长公共子序列 +[https://leetcode-cn.com/problems/longest-common-subsequence/](https://leetcode-cn.com/problems/longest-common-subsequence/) +```java +class Solution { + public int longestCommonSubsequence(String text1, String text2) { + int n1 = text1.length(), n2 = text2.length(); + int[][] dp = new int[n1 + 1][n2 + 1]; + for (int i = 1; i <= n1; i++) { + for (int j = 1; j <= n2; j++) { + if (text1.charAt(i - 1) == text2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[n1][n2]; + } +} +``` + + +[参考](https://kanarien.cn/2019/03/24/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%EF%BC%9A01%E8%83%8C%E5%8C%85%E3%80%81%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85%E3%80%81%E5%A4%9A%E9%87%8D%E8%83%8C%E5%8C%85/) + +### 15. 编辑距离 +[https://leetcode-cn.com/problems/edit-distance/description/](https://leetcode-cn.com/problems/edit-distance/description/) + +```java +class Solution { + public int minDistance(String word1, String word2) { + if (word1 == null || word2 == null) { + return 0; + } + int m = word1.length(), n = word2.length(); + int[][] dp = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + dp[i][0] = i; + } + for (int i = 1; i <= n; i++) { + dp[0][i] = i; + } + // 这dp + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1; + } + } + } + return dp[m][n]; + } + +} +``` + +## bfs +### 1. 完全平方数 + +[https://leetcode-cn.com/problems/perfect-squares/description/](https://leetcode-cn.com/problems/perfect-squares/description/) + +```java +public int numSquares(int n) { + List squares = generateSquares(n); + Queue queue = new LinkedList<>(); + boolean[] marked = new boolean[n + 1]; // 其实感觉是为了剪枝,也可以set标记 + queue.add(n); + marked[n] = true; // + int level = 0; // + while (!queue.isEmpty()) { + int size = queue.size(); + level++; + while (size-- > 0) { + int cur = queue.poll(); + for (int s : squares) { + int next = cur - s; + if (next < 0) { + break; + } + if (next == 0) { + return level; + } + if (marked[next]) { + continue; // 剪 + } + marked[next] = true; + queue.add(next); + } + } + } + return n; +} + +/** + * 生成小于 n 的平方数序列 + * @return 1,4,9,... + */ +private List generateSquares(int n) { + List squares = new ArrayList<>(); + int square = 1; + int diff = 3; + while (square <= n) { + squares.add(square); + square += diff; + diff += 2; + } + return squares; +} + +``` + +### 2. 二叉树的最小深度 + +[https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/](https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/) + +```java +class Solution { + public int minDepth(TreeNode root) { + if (root == null) return 0; + Queue queue = new LinkedList<>(); + // 树不需要标记哦 + queue.add(root); + int depth = 1; // 根根 + while (!queue.isEmpty()) { + int size = queue.size(); + while (size-- > 0) { + TreeNode node = queue.poll(); + if (node.left == null && node.right == null) + return depth; + if (node.left != null) + queue.add(node.left); + if (node.right != null) + queue.add(node.right); + } + depth++; + } + return depth; + } +} +``` + +### 3. 打开转盘锁 + +[https://leetcode-cn.com/problems/open-the-lock/](https://leetcode-cn.com/problems/open-the-lock/) + +```java +class Solution { + public int openLock(String[] deadends, String target) { + // 这里将dead和marked放在一起 + Set dead = new HashSet<>(); + for (String s : deadends) + dead.add(s); + // queue + Queue queue = new LinkedList<>(); + Set marked = new HashSet<>(); + queue.add("0000"); + marked.add("0000"); + int cnt = 0; + // luoji + while (!queue.isEmpty()) { + int size = queue.size(); + while (size-- > 0) { + String cur = queue.poll(); + if (dead.contains(cur)) + continue; + if (cur.equals(target)) + return cnt; + for (int i = 0; i < 4; i++) { + String up = plusOne(cur, i); + if (!marked.contains(up)) { + queue.add(up); + marked.add(up); + } + String down = minusOne(cur, i); + if (!marked.contains(down)) { + queue.add(down); + marked.add(down); + } + } + } + cnt++; + } + return -1; + } + + public String plusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '9') + ch[j] = '0'; + else + ch[j] += 1; + return new String(ch); + } + + public String minusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '0') + ch[j] = '9'; + else + ch[j] -= 1; + return new String(ch); + } +} +``` + +### 4. 地图分析 + +[https://leetcode-cn.com/problems/as-far-from-land-as-possible/](https://leetcode-cn.com/problems/as-far-from-land-as-possible/) + +```java +class Solution { + public int maxDistance(int[][] grid) { + int[][] dir = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + Queue> q = new LinkedList<>(); + int m = grid.length, n = grid[0].length; + // 先把所有的陆地都入队 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) + q.add(new Pair<>(i, j)); + } + } + + // 判断是否都是陆地 或者没有陆地 + if (q.size() == m * n || q.isEmpty()) + return -1; + // 从各个陆地开始,一圈一圈的遍历海洋,最后遍历到的海洋就是离陆地最远的海洋。 + + int step = 0; + Pair p = null; + while (!q.isEmpty()) { + int size = q.size(); + while (size-- > 0) { + p = q.poll(); + int x = p.getKey(), y = p.getValue(); + // 取出队列的元素,将其四周的海洋入队。 + for (int[] d : dir) { + int newX = x + d[0]; + int newY = y + d[1]; + if (newX < 0 || newX >= m || newY < 0 || newY >= n || grid[newX][newY] != 0) { + continue; + } + grid[newX][newY] = 1; // 标记 + q.add(new Pair<>(newX, newY)); + } + } + if (q.size() > 0) + step++; + } + return step; + } +} +``` + +```java +public int maxDistance(int[][] grid) { + int[][] dir = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + Queue> q = new LinkedList<>(); + int m = grid.length, n = grid[0].length; + // 先把所有的陆地都入队 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) + q.add(new Pair<>(i, j)); + } + } + + // 判断是否都是陆地 或者没有陆地 + if (q.size() == m + n || q.isEmpty()) + return -1; + // 从各个陆地开始,一圈一圈的遍历海洋,最后遍历到的海洋就是离陆地最远的海洋。 + + Pair p = null; + while (!q.isEmpty()) { + p = q.poll(); + int x = p.getKey(), y = p.getValue(); + // 取出队列的元素,将其四周的海洋入队。 + for (int[] d : dir) { + int newX = x + d[0]; + int newY = y + d[1]; + if (newX < 0 || newX >= m || newY < 0 || newY >= n || grid[newX][newY] != 0) { + continue; + } + grid[newX][newY] = grid[x][y] + 1; // 省略了标记, 要不然要加标记并且加个变量 + q.add(new Pair<>(newX, newY)); + } + } + return grid[p.getKey()][p.getValue()] - 1; +} +``` + +### 5. 腐烂的橘子 + +[https://leetcode-cn.com/problems/rotting-oranges/](https://leetcode-cn.com/problems/rotting-oranges/) + +```java +class Solution { + public int orangesRotting(int[][] grid) { + // 俺就不判断了,直接上 + int[][] dir = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + Queue> q = new LinkedList<>(); + int m = grid.length, n = grid[0].length; + int cnt = 0; // 表示新鲜的橘子 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) + cnt++; // 新鲜橘子计数 + else if (grid[i][j] == 2) + q.add(new Pair<>(i, j)); // 腐烂橘子的坐标 + } + } + if (cnt == 0 || q.size() == m * n) + return 0; + int step = 0; // 轮数 + while (cnt > 0 && !q.isEmpty()){ + int size = q.size(); + while (size-- > 0) { + Pair p = q.poll(); + int x = p.getKey(), y = p.getValue(); + for (int[] d : dir) { + int newX = x + d[0]; + int newY = y + d[1]; + if (newX < 0 || newX >= m || newY < 0 || newY >= n) { + continue; + } + if (grid[newX][newY] == 1) { + grid[newX][newY] = 2; + q.add(new Pair<>(newX, newY)); + cnt--; + } + } + } + step++; + } + return cnt > 0 ? -1 : step; + } +} +``` + +### 6. 被围绕的区域 + +[https://leetcode-cn.com/problems/surrounded-regions/](https://leetcode-cn.com/problems/surrounded-regions/) + +```java +import java.util.LinkedList; +import java.util.Queue; + +class Solution { + public void solve(char[][] board) { + if (board == null || board.length == 0) + return; + int[][] dir = {{0,1},{0,-1},{1,0},{-1,0}}; + int m = board.length, n = board[0].length; + Queue> q = new LinkedList<>(); + // 找到边缘的O + // 边缘两列 + for (int i = 0; i < m; i++) { + if (board[i][0] == 'O') { + q.add(new Pair<>(i, 0)); + board[i][0] = 'T'; + } + if (board[i][n - 1] == 'O') { + q.add(new Pair<>(i, n - 1)); + board[i][n - 1] = 'T'; + } + } + // 上下两列 + for (int i = 0; i < n; i++) { + if (board[0][i] == 'O') { + q.add(new Pair<>(0, i)); + board[0][i] = 'T'; + } + if (board[m - 1][i] == 'O') { + q.add(new Pair<>(m - 1, i)); + board[m - 1][i] = 'T'; + } + } + + // bfs 搜索 + while (!q.isEmpty()) { + int size = q.size(); + while (size-- > 0) { + Pair p = q.poll(); + int x = p.getKey(), y = p.getValue(); + for (int[] d : dir) { + int nx = x + d[0]; + int ny = y + d[1]; + if (nx < 0 || nx >= m || ny < 0 || ny >= n) + continue; + if (board[nx][ny] == 'O'){ + q.add(new Pair<>(nx, ny)); + board[nx][ny] = 'T'; + } + } + } + } + // 标记 + // 再走全部走一遍 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + // 遇见T标记O + if (board[i][j] == 'T') { + board[i][j] = 'O'; + // 遇见O标记X + } else if (board[i][j] == 'O') { + board[i][j] = 'X'; + } + } + } + } +} +``` +### 7. 零钱兑换 + +[https://leetcode-cn.com/problems/coin-change/](https://leetcode-cn.com/problems/coin-change/) + +```java +import java.util.*; + +public class Solution { + public int coinChange(int[] coins, int amount) { + if (amount == 0) + return 0; + // bfs + Queue q = new LinkedList<>(); + Set marked = new HashSet<>(); + q.add(amount); + marked.add(amount); + Arrays.sort(coins); + int cnt = 0; + while (!q.isEmpty()) { + int size = q.size(); + cnt++; + while (size-- > 0) { + int cur = q.poll(); + for (int coin : coins) { + int next = cur - coin; + if (next < 0) + break; + if (next == 0) + return cnt; + if (marked.contains(next)) + continue; + q.add(next); + marked.add(next); + } + } + } + return -1; + } +} + +``` + +### 8. 岛屿数量 + +[https://leetcode-cn.com/problems/number-of-islands/](https://leetcode-cn.com/problems/number-of-islands/) + +```java +import java.util.LinkedList; +import java.util.Queue; + +public class Solution { + + int[][] dir = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; + int m, n; + public int numIslands(char[][] grid) { + this.m = grid.length; + if (m == 0) + return 0; + this.n = grid[0].length; + boolean[][] marked = new boolean[m][n]; + int cnt = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (!marked[i][j] && grid[i][j] == '1') { + cnt++; + // bfs + bfs(grid, marked, i, j); + } + } + } + return cnt; + } + + private void bfs(char[][] grid, boolean[][] marked, int i, int j) { + Queue> q = new LinkedList<>(); + q.add(new Pair<>(i, j)); + marked[i][j] = true; + while (!q.isEmpty()) { + int size = q.size(); + while (size-- > 0) { + Pair p = q.poll(); + int x = p.getKey(), y = p.getValue(); + for (int[] d : dir) { + int nx = x + d[0]; + int ny = y + d[1]; + if (nx < 0 || nx >= m || ny < 0 || ny >= n || marked[nx][ny]) + continue; + if (grid[nx][ny] == '1') { + q.add(new Pair<>(nx, ny)); + marked[nx][ny] = true; + } + } + } + } + } +} + +``` + +### 9. 单词接龙 + +[https://leetcode-cn.com/problems/word-ladder/](https://leetcode-cn.com/problems/word-ladder/) + +```java +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +public class Solution { + + public int ladderLength(String beginWord, String endWord, List wordList) { + if (!wordList.contains(endWord)) + return 0; + boolean[] marked = new boolean[wordList.size()]; // 可以set + //检验是否存在beginWord,如果存在,就置为访问过了,没必要访问 + int idx = wordList.indexOf(beginWord); + if (idx != -1) + marked[idx] = true; + Queue q = new LinkedList<>(); + q.add(beginWord); + int cnt = 0; + while (!q.isEmpty()) { + int size = q.size(); + cnt++; + while (size-- > 0) { + String start = q.poll(); + for (int i = 0; i < wordList.size(); i++) { + // 访问过了 + if (marked[i]) + continue; + String s = wordList.get(i); + //不满足和当前只差一个字符不同,跳过,访问下一个 + if (!isConnect(start, s)) + continue; + //和endWord匹配上了,进行返回,因为是bfs,所以找到了直接返回就是最短的 + if (s.equals(endWord)) + return cnt+1; + q.add(s); + marked[i] = true; + } + } + } + return 0; + } + + private boolean isConnect(String s1, String s2) { + int diffCnt = 0; + for (int i = 0; i < s1.length() && diffCnt <= 1; i++) { + if (s1.charAt(i) != s2.charAt(i)) { + diffCnt++; + } + } + return diffCnt == 1; + } +} + +``` + + +## 背包 +### 0/1 +#### P1048 采药 + +[https://www.luogu.com.cn/problem/P1048](https://www.luogu.com.cn/problem/P1048) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int T = sc.nextInt(); + int m = sc.nextInt(); + int[] a = new int[m]; + int[] b = new int[m]; + for (int i = 0; i < m; i++) { + a[i] = sc.nextInt(); + b[i] = sc.nextInt(); + } + System.out.println(max(T, a, b)); + } + + public static int max(int T, int[] a, int [] b) { + int[] dp = new int[T + 1]; + for (int i = 0; i < a.length; i++) { + for (int j = T; j >= a[i]; j--) { + dp[j] = Math.max(dp[j], dp[j - a[i]] + b[i]); + } + } + return dp[T]; + } +} +``` + +#### P1060 开心的金明 + +[https://www.luogu.com.cn/problem/P1060](https://www.luogu.com.cn/problem/P1060) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int T = sc.nextInt(); // 总钱数 + int m = sc.nextInt(); // 种类 + int[] a = new int[m]; + int[] b = new int[m]; + for (int i = 0; i < m; i++) { + a[i] = sc.nextInt(); + b[i] = prices[i] * sc.nextInt(); + } + System.out.println(max(T, a, a)); + } + + public static int max(int T, int[] a, int [] a) { + int[] dp = new int[T + 1]; + for (int i = 0; i < a.length; i++) { + for (int j = T; j >= a[i]; j--) { + dp[j] = Math.max(dp[j], dp[j - a[i]] + b[i]); + } + } + return dp[T]; + } +} +``` + +#### P1049 装箱问题 + +[https://www.luogu.com.cn/problem/P1049](https://www.luogu.com.cn/problem/P1049) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int T = sc.nextInt(); // 箱子容量 + int m = sc.nextInt(); // m个物品 + int[] a = new int[m]; + for (int i = 0; i < m; i++) { + a[i] = sc.nextInt(); + } + System.out.println(T - max(T, a, a)); + } + + public static int max(int T, int[] a, int [] a) { + int[] dp = new int[T + 1]; + for (int i = 0; i < prices.length; i++) { + for (int j = T; j >= a[i]; j--) { + dp[j] = Math.max(dp[j], dp[j - a[i]] + a[i]); + } + } + return dp[n]; + } +} +``` + +#### P1164 小A点菜 + +[https://www.luogu.com.cn/problem/P1164](https://www.luogu.com.cn/problem/P1164) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); // 种类 + int T = sc.nextInt(); // 总钱 + int[] a = new int[n + 1]; // 多算一种 + for (int i = 1; i <= n; i++) { + a[i] = sc.nextInt(); + } + System.out.println(max(T, a)); + } + + public static int max(int T, int[] a) { + int[] dp = new int[T + 1]; + dp[0] = 1; + for (int i = 1; i < a.length; i++) { + for (int j = T; j >= a[i]; j--) { + dp[j] += dp[j - a[i]]; // 转移 求方案数 累加 + } + } + return dp[T]; + } +} +``` + +#### P1510 精卫填海 + +[https://www.luogu.com.cn/problem/P1510](https://www.luogu.com.cn/problem/P1510) + + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int T = sc.nextInt(); // 还剩T体积填完 + int n = sc.nextInt(); // n块石头 + int c = sc.nextInt(); // 体力 + int[] a = new int[n]; + int[] b = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + b[i] = sc.nextInt(); + } + int k = max(T, a, b, c); + if (k == -1) + System.out.println("Impossible"); + else + System.out.println(k); + } + + public static int max(int T, int[] a, int[] b, int c) { + int[] dp = new int[c + 1]; + int ans = -1; + for (int i = 0; i < a.length; i++) { + for (int j = c; j >= b[i]; j--) { + dp[j] = Math.max(dp[j], dp[j - b[i]] + a[i]); + if (dp[j] >= T) // 填完判断 + ans = Math.max(ans, c - j); + } + } + return ans; + } +} +``` + +#### 找零钱的硬币数组合 +[https://leetcode-cn.com/problems/coin-change-2/description/](https://leetcode-cn.com/problems/coin-change-2/description/) +```java +class Solution { + public int change(int amount, int[] coins) { + if (coins == null) return 0; + int[] dp = new int[amount + 1]; + dp[0] = 1; + for (int coin : coins) { + for (int i = coin; i <= amount; i++) { + dp[i] += dp[i - coin]; + } + } + return dp[amount]; + } +} +``` + +#### 分割等和子集 + +[https://leetcode-cn.com/problems/partition-equal-subset-sum/description/](https://leetcode-cn.com/problems/partition-equal-subset-sum/description/) + +0/1背包 + +```java +class Solution { + public boolean canPartition(int[] nums) { + int sum = computeArraySum(nums); + if (sum % 2 != 0) return false; + int w = sum / 2; + boolean[] dp = new boolean[w + 1]; + dp[0] = true; + for (int num : nums) { + for (int i = w; i >= num; i--) { + dp[i] = dp[i] || dp[i - num]; + } + } + return dp[w]; + } + private int computeArraySum(int[] nums) { + int sum = 0; + for (int num : nums) { + sum += num; + } + return sum; + } +} +``` + +P1417 P1466 P1734 + +### 完全背包 +#### P1616 疯狂的采药 + +[https://www.luogu.com.cn/problem/P1616](https://www.luogu.com.cn/problem/P1616) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int T = sc.nextInt(); + int n = sc.nextInt(); + int[] a = new int[n]; + int[] b = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + b[i] = sc.nextInt(); + } + System.out.println(max(T, a, b)); + } + + public static int max(int T, int[] a, int[] b) { + int[] dp = new int[T + 1]; + for (int i = 0; i < a.length; i++) { + for (int j = a[i]; j <= T; j++) { + dp[j] = Math.max(dp[j], dp[j - a[i]] + b[i]); + } + } + return dp[T]; + } +} +``` + +#### 零钱兑换 +[https://leetcode-cn.com/problems/coin-change/description/](https://leetcode-cn.com/problems/coin-change/description/) +完全背包问题 +```java +class Solution { + public int coinChange(int[] coins, int amount) { + if (amount == 0) return 0; + int[] dp = new int[amount + 1]; + for (int coin : coins) { + for (int i = coin; i <= amount; i++) { //将逆序遍历改为正序遍历 + // 三种情况 + if (i == coin) { + dp[i] = 1; + } else if (dp[i] == 0 && dp[i - coin] != 0) { + dp[i] = dp[i - coin] + 1; + + } else if (dp[i - coin] != 0) { + dp[i] = Math.min(dp[i], dp[i - coin] + 1); + } + } + } + return dp[amount] == 0 ? -1 : dp[amount]; + } + +} +``` + +#### 单词拆分 + +[https://leetcode-cn.com/problems/word-break/description/](https://leetcode-cn.com/problems/word-break/description/) + +求解顺序的完全背包问题 +```java +class Solution { + public boolean wordBreak(String s, List wordDict) { + int n = s.length(); + boolean[] dp = new boolean[n + 1]; + dp[0] = true; + for (int i = 1; i <= n; i++) { + for (String word: wordDict) { + // 对物品的迭代应该放在最里层 + int len = word.length(); + if (len <= i && word.equals(s.substring(i - len , i))) { + dp[i] = dp[i] || dp[i - len]; + } + } + } + return dp[n]; + } +} +``` + + +### 多重背包 + +[http://acm.hdu.edu.cn/showproblem.php?pid=2191](http://acm.hdu.edu.cn/showproblem.php?pid=2191) + +[https://www.nowcoder.com/questionTerminal/6ce78d70a25347058004691035d7540b](https://www.nowcoder.com/questionTerminal/6ce78d70a25347058004691035d7540b) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); // money + int m = sc.nextInt(); // m种类 + int[] prices = new int[m]; + int[] weights = new int[m]; + int[] nums = new int[m]; + for (int i = 0; i < m; i++) { + prices[i] = sc.nextInt(); + weights[i] = sc.nextInt(); + nums[i] = sc.nextInt(); + } + System.out.println(max(n, prices, weights, nums)); + } + + public static int max(int n, int[] prices, int[] weights, int[] nums) { + int[] dp = new int[n + 1]; + for (int i = 0; i < prices.length; i++) { + for (int j = n; j >= prices[i]; j--) { + for (int k = 1; k <= nums[i] && k * prices[i] <= j; k++) { + dp[j] = Math.max(dp[j], dp[j - k * prices[i]] + k * weights[i]); + } + } + } + return dp[n]; + } +} +``` + +### 多维费用背包 + +#### P1507 NASA的食物计划 + + +[https://www.luogu.com.cn/problem/P1507](https://www.luogu.com.cn/problem/P1507) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int vMax = sc.nextInt(); + int mMax = sc.nextInt(); + int m = sc.nextInt(); + int[] vs = new int[m]; + int[] ms = new int[m]; + int[] kas = new int[m]; + for (int i = 0; i < m; i++) { + vs[i] = sc.nextInt(); + ms[i] = sc.nextInt(); + kas[i] = sc.nextInt(); + } + System.out.println(max(vMax, mMax, vs, ms, kas)); + } + + public static int max(int vMax, int mMax, int[] vs, int[] ms, int[] kas) { + int[][] dp = new int[vMax + 1][mMax + 1]; + // 种类 + for (int i = 0; i < vs.length; i++) { + for (int j = vMax; j >= vs[i]; j--) { + for (int k = mMax; k >= ms[i]; k--) { + dp[j][k] = Math.max(dp[j][k], dp[j - vs[i]][k - ms[i]] + kas[i]); + } + } + } + return dp[vMax][mMax]; + } +} +``` + +#### 1和0 +[https://leetcode-cn.com/problems/ones-and-zeroes/](https://leetcode-cn.com/problems/ones-and-zeroes/) +这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。 +```java +class Solution { + public int findMaxForm(String[] strs, int m, int n) { + if (strs == null || strs.length == 0) + return -1; + // 俩包, 0 1 + int[][] dp = new int[m + 1][n + 1]; + for (String s : strs){ + int zeros = 0, ones = 0; + for (char c : s.toCharArray()){ + if (c == '0') + zeros++; // 统计数量 + else + ones++; // 统计数量 + } + // 开始dp + for (int i = m; i >= zeros; i--){ + for (int j = n; j >= ones; j--){ + // 优化过后的dp + dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1); + } + } + } + return dp[m][n]; + } +} +``` + +P1509 + + +## 二分 +### 二分查找 +[https://leetcode-cn.com/problems/binary-search/](https://leetcode-cn.com/problems/binary-search/) +```java +class Solution { + public int search(int[] nums, int target) { + if (nums == null || nums.length == 0) return -1; + int l = 0, h = nums.length - 1; + while (l <= h) { // <= 是跟h有关系 + int m = l + (h - l) / 2; + if (nums[m] == target) + return m; + else if (nums[m] < target) + l = m + 1; + else + h = m - 1; + } + return -1; + } +} +``` + +### 变种二分查找 +```java +public int binarySearch(int[] nums, int key) { + int l = 0, h = nums.length - 1; + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] >= key) { + h = m; + } else { + l = m + 1; + } + } + return l; +} +``` +- h 的赋值表达式为 h = m +- 循环条件为 l < h +- 最后返回 l 而不是 -1 + +结果: +- 当key存在于nums中,且唯一存在,则 l 就是key在nums中的位置 +- 当key存在于nums中,但不唯一存在,则 l 是key在nums中最左边出现的位置 +- 当key不存在于nums中,但min(nums)max(nums), l 为 nums.length-1 + +当循环体退出时,为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。 + +### 35. 搜索插入位置 +[https://leetcode-cn.com/problems/search-insert-position/](https://leetcode-cn.com/problems/search-insert-position/) + +```java +class Solution { + public int searchInsert(int[] nums, int target) { + if (nums == null || nums.length == 0) + return -1; + int l = 0, r = nums.length - 1; + while (l <= r) { + int m = l + (r - l) / 2; + if (nums[m] == target) + return m; + else if (nums[m] < target) + l = m + 1; + else + r = m - 1; + } + // 注意边界 + if (r < 0 && l == 0) + return (l + r) % 2 + 1; + else + return (l + r) / 2 + 1; + } +} +``` + +### 69. x 的平方根 +[https://leetcode-cn.com/problems/sqrtx/](https://leetcode-cn.com/problems/sqrtx/) + +```java +class Solution { + public int mySqrt(int x) { + int l = 1, r = x; + while(l= x / mid){ // 乘法可能会溢出, 改为除法 + r = mid; + }else{ + l = mid+1; + } + } + return l > x/l ? (l-1):l; // 乘法可能会溢出, 改为除法 + } +} +``` + +### 278. 第一个错误的版本 +[https://leetcode-cn.com/problems/first-bad-version/](https://leetcode-cn.com/problems/first-bad-version/) + +```java +/* The isBadVersion API is defined in the parent class VersionControl. + boolean isBadVersion(int version); */ + +public class Solution extends VersionControl { + public int firstBadVersion(int n) { + int l = 1, h = n; + while (l < h) { + int mid = l + (h - l) / 2; + if (isBadVersion(mid)) { + h = mid; + } else { + l = mid + 1; + } + } + return l; + } +} +``` +### 153. 寻找旋转排序数组中的最小值 +[https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/) + +```java +// 变种二分 +public int minNumberInRotateArray(int[] nums) { + if (nums.length == 0) + return 0; + int l = 0, h = nums.length - 1; + // 注意条件 + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] <= nums[h]) + h = m; + else + l = m + 1; + } + // 注意返回值 + return nums[l]; +} +``` + +```java +class Solution { + public int minArray(int[] numbers) { + if (numbers == null || numbers.length == 0) + return -1; + int l = 0, h = numbers.length - 1; + while (l < h){ + int m = l + (h - l) / 2; + if (numbers[m] < numbers[h]) + h = m; + else if (numbers[m] > numbers[h]) + l = m + 1; + else + h--; + } + return numbers[l]; + } +} +``` + +### 34. 在排序数组中查找元素的第一个和最后一个位置 +[https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/) + +```java +class Solution { + public int[] searchRange(int[] nums, int target) { + int first = findFirst(nums, target); + int last = findFirst(nums, target + 1) - 1; + if (first == nums.length || nums[first] != target) { + return new int[] {-1, -1}; + } else { + return new int[]{first, Math.max(first, last)}; + } + } + private int findFirst(int[] nums, int target) { + int l = 0, h = nums.length; // h 的初始值和往常不一样 + while (l < h) { + int m = l + ( h - l) / 2; + if (nums[m] >= target) h = m; + else l = m + 1; + } + return l; + } +} +``` + +## 其他 + + +### 2. 移动零 +[https://leetcode-cn.com/problems/move-zeroes/](https://leetcode-cn.com/problems/move-zeroes/) + +```java +class Solution { + public void moveZeroes(int[] nums) { + int idx = 0; + for (int num : nums) { + if (num != 0) nums[idx++] = num; + } + while (idx < nums.length) { + nums[idx++] =0; + } + } +} +``` + +### 3. 接雨水(字节爱出) +[https://leetcode-cn.com/problems/trapping-rain-water/](https://leetcode-cn.com/problems/trapping-rain-water/) +```java +class Solution { + public int trap(int[] height) { + int min = 0, max = 0; + int l = 0, r = height.length - 1; + int res = 0; + while(l < r) { + min = height[height[l] < height[r] ? l++ : r--]; + max = Math.max(max, min); + res += max - min; + } + return res; + } +} +``` + +### 4. 下一个排列 +[https://leetcode-cn.com/problems/next-permutation/](https://leetcode-cn.com/problems/next-permutation/) + +```java + //源于离散数学及其应用的算法:(以3 4 5 2 1 为例) + //从后往前寻找第一次出现的正序对:(找到 4,5) + //之后因为从5 开始都是逆序,所以把他们反转就是正序:3 4 1 2 5 + //之后4 的位置应该是:在它之后的,比他大的最小值(5) + //交换这两个值:得到 3 5 1 2 4 + // 对于初始即为逆序的序列,将在反转步骤直接完成 +class Solution { + public void nextPermutation(int[] nums) { + int len = nums.length; + if (len < 2) return; + int i = len - 1; + while (i > 0 && nums[i - 1] >= nums[i]) + i--; // 从后向前找第一个正序,这里最后i指向的是逆序起始位置 + reverse(nums, i, len - 1); // 翻转后面的逆序区域,使其变为正序 + if (i == 0) return; + int j = i - 1; + while(i < len && nums[j] >= nums[i]) + i++; // 找到第一个比nums[j]大的元素,交换即可 + // 交换 + swap(nums, i, j); + } + private void reverse(int[] nums, int i, int j) { + while (i < j) { + swap(nums, i++, j--); + } + } + + private void swap(int[] nums, int i, int j){ + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; + } +} +``` + +### 5. [孩子们的游戏](https://www.nowcoder.com/practice/f78a359491e64a50bce2d89cff857eb6?tpId=13&tqId=11199&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +约瑟夫环,圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m。因为是圆圈,所以最后需要对 n 取余。 + +```java +public int LastRemaining_Solution(int n, int m) { + if (n == 0) /* 特殊输入的处理 */ + return -1; + if (n == 1) /* 递归返回条件 */ + return 0; + return (LastRemaining_Solution(n - 1, m) + m) % n; +} +``` + +### 165. 比较版本号 + +[https://leetcode-cn.com/problems/compare-version-numbers/](https://leetcode-cn.com/problems/compare-version-numbers/) + +```java +class Solution { + public int compareVersion(String version1, String version2) { + String[] a1 = version1.split("\\."); + String[] a2 = version2.split("\\."); + + for (int n = 0; n < Math.max(a1.length, a2.length); n++) { + int i = n < a1.length ? Integer.valueOf(a1[n]) : 0 ; + int j = n < a2.length ? Integer.valueOf(a2[n]) : 0 ; + if (i < j) + return -1; + else if (i > j) + return 1; + return 0; + } + } +} +``` + +### 209. 长度最小的子数组 +```java +class Solution { + public int minSubArrayLen(int s, int[] nums) { + // 滑动窗口 + int i = 0; + int sum = 0; + int len = 0; + for (int j = 0; j < nums.length; j++){ + sum += nums[j]; + // 注意这个骚条件 + while (sum >= s){ + len = len == 0 ? j - i + 1 : Math.min(len, j - i + 1); + // 滑动 + sum -= nums[i++]; + } + } + return len; + } +} +``` + +### 628. 三个数的最大乘积 +[https://leetcode-cn.com/problems/maximum-product-of-three-numbers/](https://leetcode-cn.com/problems/maximum-product-of-three-numbers/) + +```java +class Solution { + public int maximumProduct(int[] nums) { + int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE,min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE; + for (int n : nums) { + if (n > max1) { + max3 = max2; + max2 = max1; + max1 = n; + } else if (n > max2) { + max3 = max2; + max2 = n; + } else if (n > max3) { + max3 = n; + } + + if (n < min1) { + min2 = min1; + min1 = n; + } else if (n < min2) { + min2 = n; + } + } + return Math.max(max1*max2*max3, max1*min1*min2); + } +} +``` + +### 179. 最大数 +[https://leetcode-cn.com/problems/largest-number/](https://leetcode-cn.com/problems/largest-number/) + +```java +class Solution { + public String largestNumber(int[] nums) { + if (nums == null || nums.length == 0) + return ""; + int n = nums.length; + String[] ss = new String[n]; + for (int i = 0; i < n; i++) + ss[i] = nums[i] + ""; + Arrays.sort(ss, (a, b) -> (b + a).compareTo(a + b)); + StringBuilder sb = new StringBuilder(); + for (String s : ss) + sb.append(s); + String res = sb.charAt(0) == '0' ? "0" : sb.toString(); + return res; + } +} +``` + +### 306. 累加数 +[https://leetcode-cn.com/problems/additive-number/](https://leetcode-cn.com/problems/additive-number/) + +```java +class Solution { + public boolean isAdditiveNumber(String num) { + return dfs(num, 0, 0, num.length(), 0, 0); + } + + // num:原始字符串 idx:当前处理的下标 sum:上一层的两数和 + // len:字符串长度 pre:前一个数 k:当前已经处理数的个数 + private boolean dfs(String num, int idx, long sum, int len, long pre, int k) { + // 达到末尾,判断是否存在至少3个数 + if (idx == len) + return k > 2; + // [idx, i] 代表当前尝试的数 + // 为了避免溢出long 使用结论:不存在符合条件的数,其长度超过原始字符串一半 + for (int i = idx + 1; i <= len && i - idx <= len / 2; i++) { + // 剪枝:以0开头且不是单纯的0的数不符合题意 + if (i != idx + 1 && num.charAt(idx) == '0') + return false; + long cur = Long.parseLong(num.substring(idx, i)); + // 剪枝:校验两数和 不等于当前数 + if (k >= 2 && cur != sum) + continue; + // 继续dfs + if (dfs(num, i, pre + cur, len, cur, k + 1)) + return true; + } + return false; + } +} +``` + +### 402. 移掉K位数字 +[https://leetcode-cn.com/problems/remove-k-digits/](https://leetcode-cn.com/problems/remove-k-digits/) + +```java +class Solution { + public String removeKdigits(String num, int k) { + if (num.length() == k) + return "0"; + StringBuilder s = new StringBuilder(num); + for (int i = 0; i < k; i++) { + int idx = 0; + for (int j = 1; j < s.length() && s.charAt(j) >= s.charAt(j - 1); j++) + idx = j; + s.delete(idx, idx + 1); + while (s.length() > 1 && s.charAt(0) == '0') + s.delete(0, 1); + } + return s.toString(); + } +} +``` + +### 求一个数的平方根,精度0.01 +```java + public static void main(String[] args) { + int a = 5; + double l = 0, r = a; + while (l <= r) { + double m = l + (r - l) / 2; + double val = m * m - a; + if (Math.abs(val) <= 0.01) { + System.out.println(m); + return; + } + else if (val > 0.01) + r = m; + else + l = m; + } + } +``` + +## 问答 + +1. 在一个大数组里求第100大的数字 + +快排 + +2. 100万个数找最小的10个 + +大顶堆 + +3. 给出25亿个QQ号,找出其中重复的?如果不用bitmap且只有一台机器怎么做 + +bitmap + +双层桶划分 分而治之 + +4. 快排什么时候复杂度不好 + +有序的情况下 + +5. 微信发红包 m块钱发给n个人 你怎么设计算法 + +二倍均值法 + +6. 100层楼和2个玻璃杯,怎样用最少的次数找出杯子在哪一层会碎 + +[https://www.cxyxiaowu.com/1760.html](https://www.cxyxiaowu.com/1760.html) + +7. 100亿黑名单URL,每个64B,判断一个URL是否在黑名单中 +[布隆过滤器...](https://juejin.im/post/5c959ff8e51d45509e2ccf84) + +8. 2GB内存在20亿整数中找到出现次数最多的数 +[哈希多个文件](https://blog.csdn.net/u013246898/article/details/52033937) + +9. 40亿个非负整数中找到没有出现的数 +[分区加位图](https://blog.csdn.net/u010456903/article/details/48806947) + +10. 找到100亿个URL中重复的URL/海量搜索词汇,找到最热TOP100词汇的方法 +[哈希分流](https://blog.csdn.net/weixin_41362649/article/details/94601249) + +11. 40亿个无符号整数,1GB内存,找到所有出现两次的数/10MB内存,找到40亿整数的中位数 +[位图](https://blog.csdn.net/liyutaogege/article/details/104394790) + +12. 设计短域名系统,将长URL转化成短的URL. +利用放号器,初始值为0,对于每一个短链接生成请求,都递增放号器的值,再将此值转换为62进制(a-zA-Z0-9),比如第一次请求时放号器的值为0,对应62进制为a,第二次请求时放号器的值为1,对应62进制为b,第10001次请求时放号器的值为10000,对应62进制为sBc。 +[发号器](https://blog.csdn.net/u010870518/article/details/80026452) + +13. 十大经典的海量数据问题 + +[https://blog.csdn.net/v_JULY_v/article/details/6279498](https://blog.csdn.net/v_JULY_v/article/details/6279498) + + diff --git "a/Java/alg/\345\211\221\346\214\207offer.md" "b/Java/alg/\345\211\221\346\214\207offer.md" new file mode 100644 index 00000000..1042431d --- /dev/null +++ "b/Java/alg/\345\211\221\346\214\207offer.md" @@ -0,0 +1,3148 @@ +## 3、[数组中重复的数字](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&tqId=11203&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 + +```html +Input: +{2, 3, 1, 0, 2, 5} + +Output: +2 +``` + +### 解题思路 + +要求时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。 + +对于这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上进行求解。本题要求找出重复的数字,因此在调整过程中,如果第 i 位置上已经有一个值为 i 的元素,就可以知道 i 值重复。 + +以 (2, 3, 1, 0, 2, 5) 为例,遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复: + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/643b6f18-f933-4ac5-aa7a-e304dbd7fe49.gif) + +```java +public boolean duplicate(int[] nums, int length, int[] duplication) { + if (nums == null || length <= 0) + return false; + for (int i = 0; i < length; i++) { + while (nums[i] != i) { // 该value和索引不相等,则i,nums[i]的索引的value互相交换 + if (nums[i] == nums[nums[i]]) { // 如果value和索引相等了,那么重复了 + duplication[0] = nums[i]; + return true; + } + swap(nums, i, nums[i]); + } + } + return false; +} + +private void swap(int[] nums, int i, int j) { + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; +} + +``` + + + +```java +public class T50 { + public boolean duplicate(int numbers[],int length,int [] duplication) { + // 边界 + if (numbers == null || numbers.length == 0) { + return false; + } + // 排序 + Arrays.sort(numbers); + int flag = 0; + for (int i = 0; i < length - 1; i++) { + if (numbers[i] == numbers[i + 1]) { + duplication[0] = numbers[i]; + flag = 1; + break; + } + } + return flag == 1 ? true : false; + } +} +``` + +## 4、[二维数组中的查找](https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqId=11154&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +### 题目描述 + +给定一个二维数组,其每一行从左到右递增排序,从上到下也是递增排序。给定一个数,判断这个数是否在该二维数组中。 + +```html +Consider the following matrix: +[ + [1, 4, 7, 11, 15], + [2, 5, 8, 12, 19], + [3, 6, 9, 16, 22], + [10, 13, 14, 17, 24], + [18, 21, 23, 26, 30] +] + +Given target = 5, return true. +Given target = 20, return false. + +``` + +### 解题思路 + +要求时间复杂度 O(M + N),空间复杂度 O(1)。其中 M 为行数,N 为 列数。 + +该二维数组中的一个数,小于它的数一定在其左边,大于它的数一定在其下边。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间,当前元素的查找区间为左下角的所有元素。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/35a8c711-0dc0-4613-95f3-be96c6c6e104.gif) + +```java +public boolean Find(int target, int[][] matrix) { + if (matrix == null || matrix.length == 0 || matrix[0].length == 0) + return false; + int rows = matrix.length, cols = matrix[0].length; + int r = 0, c = cols - 1; // 从右上角开始 + while (r <= rows - 1 && c >= 0) { + if (target == matrix[r][c]) // 如果相等,直接返回 + return true; + else if (target > matrix[r][c]) // 如果小于target, r++; + r++; + else + c--; // 如果大于,c--; + } + return false; +} + +``` + + + +```java +public class T1 { + public boolean Find(int target, int [][] array) { + // 研究数组的特性,比如: + // 3 4 5 + // 4 8 6 + // 5 9 10 + int col = 0; + for (int i = array.length - 1; i >= 0; i--) { + // 最后一行开始,按列遍历和target比较: + // 如果= 0 && P2 > P1) { + char c = str.charAt(P1--); + if (c == ' ') { + str.setCharAt(P2--, '0'); // 倒着替换 + str.setCharAt(P2--, '2'); + str.setCharAt(P2--, '%'); + } else { + str.setCharAt(P2--, c); + } + } + return str.toString(); +} +``` + + + +```java +public class T2 { + public String replaceSpace(StringBuffer str) { + // 检测空格数目 + int spaceNum = 0; + // 第一遍循环,检测空格的数目 + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) == ' ') spaceNum++; + } + // 创建新数组 ,也可以StringBuilder + char[] ans = new char[str.length() + 2 * spaceNum]; + int p1 = ans.length - 1; + // 倒着遍历,一个一个添加 + for (int i = str.length() - 1; i >= 0; i++) { + if (str.charAt(i) == ' ') { + ans[p1--] = '0'; + ans[p1--] = '2'; + ans[p1--] = '%'; + } else { + ans[p1--] = str.charAt(i); + } + } + return new String(ans); + } +} +``` + +## 6、[从尾到头打印链表](https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035?tpId=13&tqId=11156&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +从尾到头反过来打印出每个结点的值。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f5792051-d9b2-4ca4-a234-a4a2de3d5a57.png) + +### 解题思路 + +要逆序打印链表 1->2->3(3,2,1),可以先逆序打印链表 2->3(3,2),最后再打印第一个节点 1。而链表 2->3 可以看成一个新的链表,要逆序打印该链表可以继续使用求解函数,也就是在求解函数中调用自己,这就是递归函数。 + +```java +public class T3 { + // 创建list + ArrayList list = new ArrayList<>(); + public ArrayList printListFromTailToHead(ListNode listNode) { + // 判断头节点是否为空 + if (listNode != null) { + // 递归打印 + this.printListFromTailToHead(listNode.next); + list.add(listNode.val); + } + return list; + } +} +``` + +## 7、[重建二叉树](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191102210342488.png) + +### 解题思路 + +前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。然后分别对左右子树递归地求解。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/60c4a44c-7829-4242-b3a1-26c3b513aaf0.gif) + + + +```java +class Solution { + public TreeNode buildTree(int[] preorder, int[] inorder) { + int n = preorder.length; + if (n == 0) + return null; + int rootVal = preorder[0], rootIndex = 0; + for (int i = 0; i < n; i++) { + if (inorder[i] == rootVal) { + rootIndex = i; + break; + } + } + TreeNode root = new TreeNode(rootVal); + root.left = buildTree(Arrays.copyOfRange(preorder, 1, 1 + rootIndex), Arrays.copyOfRange(inorder, 0, rootIndex)); + root.right = buildTree(Arrays.copyOfRange(preorder, 1 + rootIndex, n), Arrays.copyOfRange(inorder, rootIndex + 1, n)); + + return root; + } +} +``` + +## 8、[二叉树的下一个结点](https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回 。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 + +```java +public class TreeLinkNode { + + int val; + TreeLinkNode left = null; + TreeLinkNode right = null; + TreeLinkNode next = null; // 指向父结点的指针 + + TreeLinkNode(int val) { + this.val = val; + } +} +``` + +### 解题思路 + +我们先来回顾一下中序遍历的过程:先遍历树的左子树,再遍历根节点,最后再遍历右子树。所以最左节点是中序遍历的第一个节点。 + +```java +void traverse(TreeNode root) { + if (root == null) return; + traverse(root.left); + visit(root); + traverse(root.right); +} +``` + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ad5cc8fc-d59b-45ce-8899-63a18320d97e.gif) + +① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点; + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7008dc2b-6f13-4174-a516-28b2d75b0152.gif) + +② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/094e3ac8-e080-4e94-9f0a-64c25abc695e.gif) + + + +```java +public class T57 { + public TreeLinkNode GetNext(TreeLinkNode pNode) { + if (null == pNode) { + return null; + } + if (null != pNode.right) { + TreeLinkNode node = pNode.right; + while (null != node.left) { + node = node.left; + } + return node; + } + while (null != pNode.next) { + TreeLinkNode parent = pNode.next; + if (parent.left == pNode) { + return parent; + } + pNode = pNode.next; + } + return null; + } +} +``` + + +## 9、[用两个栈实现一个队列](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。 + +### 解题思路 + +in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/3ea280b5-be7d-471b-ac76-ff020384357c.gif) + +```java +public class T5 { + // 举个例子: + // 1,2,3,4,5依次push + // stack1:5,4,3,2,1 栈顶是5 + // stack2:1,2,3,4,5 栈顶是1 + // 这样就是队列的先进先出了 + Stack in = new Stack<>(); + + Stack out = new Stack<>(); + + public void push (int node) { + // 添加value + in.push(node); + } + + public int pop() { + // 判断stack2是否为空 + if (out.isEmpty()) { + // 如果为空 + while (!in.isEmpty()) { + // 并且stack1不为空,然后将栈1所有的元素重新弹出去添加到栈2 + // 这样的话,用栈2弹,就是FIFO的队列了 + out.push(stack1.pop()); + } + } + return out.pop(); + } +} +``` + +## 10.1、[斐波那契数列](https://www.nowcoder.com/practice/c6c7742f5ba7442aada113136ddea0c3?tpId=13&tqId=11160&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +求斐波那契数列的第 n 项,n <= 39。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/45be9587-6069-4ab7-b9ac-840db1a53744.jpg) + +### 解题思路 + +如果使用递归求解,会重复计算一些子问题。例如,计算 f(4) 需要计算 f(3) 和 f(2),计算 f(3) 需要计算 f(2) 和 f(1),可以看到 f(2) 被重复计算了。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c13e2a3d-b01c-4a08-a69b-db2c4e821e09.png) + +递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。 + +```java +public int Fibonacci(int n) { + if (n <= 1) + return n; + int[] dp = new int[n + 1]; + dp[1] = 1; + for (int i = 2; i <= n; i++) + dp[i] = dp[i - 1] + dp[i - 2]; + return dp[n]; +} + +``` + +考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1) + +```java +public class T7 { + public int Fibonacci(int n) { + // 条件 + if (n <= 1) return n; + // 可以用自底向上的方法 + int pre2 = 0, pre1 = 1; + int f = 0; + for (int i = 2; i <= n; i++) { + f = pre2 + pre1; // 如果动态规划,这个就是dp的公式 + pre2 = pre1; + pre1 = f; + } + return f; + } +} +``` +## 10.2、[跳台阶](https://www.nowcoder.com/practice/8c82a5b80378478f9484d87d1c5f12a4?tpId=13&tqId=11161&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 + +### 解题思路 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/508c6e52-9f93-44ed-b6b9-e69050e14807.jpg) + +```java +public class T8 { + public int JumpFloor(int target) { + // 条件 + if (target <= 2) return target; + // 自底向上的方法 + int pre2 = 1, pre1 = 2; + int sum = 0; + for (int i = 3; i <= target; i++) { + sum = pre2 + pre1; // 一样的道理, 和上面那道题的初始值不一样 + pre2 = pre1; + pre1 = sum; + } + return sum; + } +} +``` + +## 10.3、[矩形覆盖](https://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6?tpId=13&tqId=11163&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +我们可以用 2*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2*1 的小矩形无重叠地覆盖一个 2*n 的大矩形,总共有多少种方法? + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b903fda8-07d0-46a7-91a7-e803892895cf.gif) + +### 解题思路 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/508c6e52-9f93-44ed-b6b9-e69050e14807.jpg) + + + +```java +public class T10 { + public int RectCover(int target) { + // 条件 + if (target <= 2) return target; + // 自底向上 + int pre2 = 1, pre1 = 2; + int sum = 0; + for (int i = 3; i <= target; i++) { + sum = pre2 + pre1; // 同理呀 + pre2 = pre1; + pre1 = sum; + } + return sum; + } +} +``` + + +## 10.4、[变态跳台阶](https://www.nowcoder.com/practice/22243d016f6b47f2a6928b4313c85387?tpId=13&tqId=11162&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级... 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cd411a94-3786-4c94-9e08-f28320e010d5.png) + +### 解题思路 + +```java +public int JumpFloorII(int target) { + int[] dp = new int[target]; + Arrays.fill(dp, 1); + for (int i = 1; i < target; i++) + for (int j = 0; j < i; j++) + dp[i] += dp[j]; + return dp[target - 1]; +} +``` + + + +```java +public class T9 { + public int JumpFloorII(int target) { + // 公式 2的target-1次方 + return 1 << (target - 1); + } +} +``` + + +## 11、[旋转数组的最小数字](https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?tpId=13&tqId=11159&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0038204c-4b8a-42a5-921d-080f6674f989.png) + +```java +public int minNumberInRotateArray(int[] nums) { + if (nums.length == 0) + return 0; + int l = 0, h = nums.length - 1; + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] <= nums[h]) + h = m; + else + l = m + 1; + } + return nums[l]; +} +``` + + + +```java +public class T6 { + // 这道题也可以倒着遍历 + public int minNumberInRotateArray(int [] array) { + // 判断条件 + if (array.length == 0) return 0; + if (array.length == 1) return array[0]; + + int a = array[0]; + // 根据数组的特征,一开始递增,突然变小,于是,那个突然变小的那个元素就是最小数字 + for (int i = 1; i < array.length; i++) { + if (a > array[i]) { + // array[i] < a,则代表最小 + return array[i]; + } else { // 否则 a + a = array[i]; + } + } + return 0; + } +} +``` + +## 12、[矩阵中的路径](https://www.nowcoder.com/practice/c61c6999eecb4b8f88a98f66b273a3cc?tpId=13&tqId=11218&tPage=4&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向上下左右移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 + +例如下面的矩阵包含了一条 bfce 路径。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1db1c7ea-0443-478b-8df9-7e33b1336cc4.png) + +### 解题思路 + +使用回溯法(backtracking)进行求解,它是一种暴力搜索方法,通过搜索所有可能的结果来求解问题。回溯法在一次搜索结束时需要进行回溯(回退),将这一次搜索过程中设置的状态进行清除,从而开始一次新的搜索过程。例如下图示例中,从 f 开始,下一步有 4 种搜索可能,如果先搜索 b,需要将 b 标记为已经使用,防止重复使用。在这一次搜索结束之后,需要将 b 的已经使用状态清除,并搜索 c。 + +```java +private final static int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; +private int rows; +private int cols; + +public boolean hasPath(char[][] matrix, int rows, int cols, char[] str) { + if (rows == 0 || cols == 0) return false; + this.rows = rows; + this.cols = cols; + boolean[][] marked = new boolean[rows][cols]; + for (int i = 0; i < rows; i++) + for (int j = 0; j < cols; j++) + if (backtracking(matrix, str, marked, 0, i, j)) + return true; + + return false; +} + +private boolean backtracking(char[][] matrix, char[] str, + boolean[][] marked, int pathLen, int r, int c) { + + if (pathLen == str.length) return true; + if (r < 0 || r >= rows || c < 0 || c >= cols + || matrix[r][c] != str[pathLen] || marked[r][c]) { + + return false; + } + marked[r][c] = true; + for (int[] n : next) + if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1])) + return true; + marked[r][c] = false; + return false; +} + +``` + +```java +public class T65 { + public boolean hasPath(char[] matrix, int rows, int cols, char[] str) { + if (null == matrix || matrix.length == 0 || null == str || str.length == 0 || matrix.length != rows * cols || rows <= 0 || cols <= 0){ + return false; + } + boolean[] visited = new boolean[rows * cols]; + int[] pathLength = {0}; + for (int i = 0; i <= rows - 1; i++){ + for (int j = 0; j <= cols - 1; j++){ + if (hasPathCore(matrix, rows, cols, str, i, j, visited, pathLength)) { + return true; + } + } + } + return false; + } + + public boolean hasPathCore(char[] matrix, int rows, int cols, char[] str,int row,int col, boolean[] visited,int[] pathLength) { + boolean flag = false; + + if (row >= 0 && row < rows && col >= 0 && col < cols && !visited[row * cols + col] && matrix[row * cols + col] == str[pathLength[0]]) { + pathLength[0]++; + visited[row * cols + col] = true; + if (pathLength[0] == str.length) { + return true; + } + flag = hasPathCore(matrix, rows, cols, str, row, col + 1, visited, pathLength) || hasPathCore(matrix, rows, cols, str, row + 1, col, visited, pathLength) || hasPathCore(matrix, rows, cols, str, row, col - 1, visited, pathLength) || hasPathCore(matrix, rows, cols, str, row - 1, col, visited, pathLength); + if (!flag) { + pathLength[0]--; + visited[row * cols + col] = false; + } + } + return flag; + } +} +``` + +## 13、[机器人的运动范围](https://www.nowcoder.com/practice/6e5207314b5241fb83f2329e89fdecc8?tpId=13&tqId=11219&tPage=4&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。 + +例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子? + +### 解题思路 + +使用深度优先搜索(Depth First Search,DFS)方法进行求解。回溯是深度优先搜索的一种特例,它在一次搜索过程中需要设置一些本次搜索过程的局部状态,并在本次搜索结束之后清除状态。而普通的深度优先搜索并不需要使用这些局部状态,虽然还是有可能设置一些全局状态。 + +```java +public class T66 { + public int movingCount(int threshold, int rows, int cols) { + boolean[][] visited = new boolean[rows][cols]; // 设置状态 + return countingStep(threshold,rows,cols,0,0,visited); + } + + public int countingStep(int limit, int rows, int cols, int r, int c, boolean[][] visited) { + if (r < 0 || r >= rows || + c < 0 || c >= cols || + visited[r][c] || + bitSum(r) + bitSum(c) > limit) return 0; + visited[r][c] = true; + return countingStep(limit,rows,cols,r - 1,c,visited) + countingStep(limit,rows,cols,r,c - 1,visited) + countingStep(limit,rows,cols,r+1,c,visited) + countingStep(limit,rows,cols,r,c+1,visited) + 1; + } + + public int bitSum(int t) { + int count = 0; + while (t != 0) { + count += t % 10; + t /= 10; + } + return count; + } +} + +``` + +## 14、[剪绳子](https://www.nowcoder.com/practice/57d85990ba5b440ab888fc72b0751bf8?tpId=13&tqId=33257&tPage=4&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +把一根绳子剪成多段,并且使得每段的长度乘积最大。 + +```html +n = 2 +return 1 (2 = 1 + 1) + +n = 10 +return 36 (10 = 3 + 3 + 4) + +``` + +### 解题思路 + +```java +// 动态规划 +public int integerBreak(int n) { + int[] dp = new int[n + 1]; + dp[1] = 1; + for (int i = 2; i <= n; i++) + for (int j = 1; j < i; j++) + dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j))); + return dp[n]; +} + +``` + +```java +public class T67 { + // 动态规划 + public int cutRope(int target) { + if (target < 2) return 0; + if (target == 2) return 1; + if (target == 3) return 2; + int[] products = new int[target + 1]; + products[0] = 0; + products[1] = 1; // 长度为2... + products[2] = 2; // 长度为3... + products[3] = 3; // 长度为4... + int max = 0; + for (int i = 4; i <= target; i++) { + max = 0; + for (int j = 1; j <= i / 2; j++) { + int product = products[j] * products[i - j]; + max = max > product ? max : product; + products[i] = max; + } + } + max = products[target]; + return max; + } +} +``` + + +## 15、[二进制1的个数](https://www.nowcoder.com/practice/8ee967e43c2c4ec193b040ea7fbb10b8?tpId=13&tqId=11164&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +输入一个整数,输出该数二进制表示中 1 的个数。 + +### 解题思路 + +(n-1) & n :该位运算去除 n 的位级表示中最低的那一位。 + +```java +public class T11 { + public int NumberOf1(int n) { + int count = 0; + while ( n!= 0) { + count++; + // (n-1) & n 注意这个。。 + n = (n - 1) & n; + } + return count; + } +} +``` + +## 16、[数值的整数次方](https://www.nowcoder.com/practice/1a834e5e3e1a4b7ba251417554e07c00?tpId=13&tqId=11165&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent,求 base 的 exponent 次方。 + +### 解题思路 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/48b1d459-8832-4e92-938a-728aae730739.jpg) + +```java +public double Power(double base, int exponent) { + if (exponent == 0) + return 1; + if (exponent == 1) + return base; + boolean isNegative = false; + if (exponent < 0) { + exponent = -exponent; + isNegative = true; + } + // 递归走起,速度快 + double pow = Power(base * base, exponent / 2); + if (exponent % 2 != 0) + pow = pow * base; + return isNegative ? 1 / pow : pow; +} + +``` + +```java +public class T12 { + public double Power(double base, int exponent) { + // 还是先判断特殊情况,是0?还是>0,还是<0? + if (exponent == 0) return 1; + double ans = 1; + boolean flag = false; // 判断倒数 + // 如果小于0,取绝对值 + if (exponent < 0) { + flag = true; + exponent = -exponent; + } + for (int i = 1; i <= exponent; i++) { + ans *= base; + } + // 如果小于0,不仅取绝对值,还要最终求倒数 + if (flag) { + ans = 1 / ans; + } + return ans; + } +} +``` + + +## 18、[删除链表中重复的结点](https://www.nowcoder.com/practice/fc533c45b73a41b0b44ccba763f866ef?tpId=13&tqId=11209&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/17e301df-52e8-4886-b593-841a16d13e44.png) + +```java +public ListNode deleteDuplication(ListNode pHead) { + if (pHead == null || pHead.next == null) + return pHead; + ListNode next = pHead.next; + if (pHead.val == next.val) { + while (next != null && pHead.val == next.val) + next = next.next; + return deleteDuplication(next); + } else { + pHead.next = deleteDuplication(pHead.next); + return pHead; + } +} + +``` + +```java +public class T56 { + public ListNode deleteDuplication(ListNode pHead) { + // 只有0个或1个节点,则返回。 + if (null == pHead || pHead.next == null) { + return pHead; + } + // 当前节点是重复节点 + if (pHead.val == pHead.next.val) { + ListNode pNode = pHead.next; + while (pNode != null && pHead.val == pNode.val) { + pNode = pNode.next; // 是不是一直重复, so while + } + return deleteDuplication(pNode); // 递归继续 + } else { + // 当前节点不是重复节点 + // 保留当前节点,从下一个节点开始递归 + pHead.next = deleteDuplication(pHead.next); + return pHead; + } + } +} +``` + +## 19、[正则表达式匹配](https://www.nowcoder.com/practice/45327ae22b7b413ea21df13ee7d6429c?tpId=13&tqId=11205&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +请实现一个函数用来匹配包括 '.' 和 '*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '*' 表示它前面的字符可以出现任意次(包含 0 次)。 + +在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab*ac*a" 匹配,但是与 "aa.a" 和 "ab*a" 均不匹配。 + +```java +public class T52 { + public boolean match(char[] str, char[] pattern) { + if (str == null || pattern == null) { + return false; + } + int strIndex = 0; + int patternIndex = 0; + return matchCore(str, strIndex, pattern, patternIndex); + } + + public boolean matchCore(char[] str, int strIndex, char[] pattern, int patternIndex) { + // 有效性校验:str到尾, pattern到尾,匹配成功 + if (strIndex == str.length && patternIndex == pattern.length) { + return true; + } + // pattern 先到尾,匹配失败 + if (strIndex != str.length && patternIndex == pattern.length) { + return false; + } + // 模式第二个是*,且字符串第一个根模式第一个匹配,分三种匹配模式; + // 如果不匹配,模式后移两 + if (patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*') { + if ((strIndex != str.length && pattern[patternIndex] == str[strIndex]) || (pattern[patternIndex] == '.' && strIndex != str.length)) { + return matchCore(str, strIndex, pattern, patternIndex + 2) || matchCore(str, strIndex + 1, pattern, patternIndex + 2) || matchCore(str, strIndex + 1, pattern, patternIndex); + } else { + return matchCore(str, strIndex, pattern, patternIndex + 2); + } + } + // 模式第二个不是*,且字符串第一个根模式第一个匹配,则都后移一位,否则直接返回false + if ((strIndex != str.length && pattern[patternIndex] == str[strIndex])|| (pattern[patternIndex] == '.' && strIndex != str.length)) { + return matchCore(str, strIndex + 1, pattern, patternIndex + 1); + } + return false; + } +} + +``` + +## 20、[表示数值的字符串](https://www.nowcoder.com/practice/6f8c901d091949a5837e24bb82a731f2?tpId=13&tqId=11206&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +```html +true + +"+100" +"5e2" +"-123" +"3.1416" +"-1E-16" +``` + +```html +false + +"12e" +"1a3.14" +"1.2.3" +"+-5" +"12e+4.3" +``` + +### 解题思路 + +```html +[] : 字符集合 +() : 分组 +? : 重复 0 ~ 1 次 ++ : 重复 1 ~ n 次 +* : 重复 0 ~ n 次 +. : 任意字符 +\\. : 转义后的 . +\\d : 数字 +``` + + +```java +public boolean isNumeric(char[] str) { + if (str == null || str.length == 0) + return false; + return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?"); +} + +``` + +```java +public class T53 { + public boolean isNumeric(char[] str) { + String s = String.valueOf(str); + // 正则大法好? + return s.matches("[+-]?[0-9]*(\\.[0-9]*)?([eE][+-]?[0-9]+)?"); + } +} +``` + + +## 21、[调整数组顺序使奇数位于偶数前面](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&tqId=11166&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +需要保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d03a2efa-ef19-4c96-97e8-ff61df8061d3.png) + +### 解题思路 + +方法一:创建一个新数组,时间复杂度 O(N),空间复杂度 O(N)。 + +```java +public void reOrderArray(int[] nums) { + // 奇数个数 + int oddCnt = 0; + for (int x : nums) + if (!isEven(x)) + oddCnt++; + int[] copy = nums.clone(); + int i = 0, j = oddCnt; + for (int num : copy) { + if (num % 2 == 1) + nums[i++] = num; + else + nums[j++] = num; + } +} + +private boolean isEven(int x) { + return x % 2 == 0; +} +``` + +方法二:冒泡 + +```java +public class T13 { + public void reOrderArray(int [] array) { + // 边界判断 + if (array == null || array.length == 0) return; + for (int i = 0; i < array.length; i++) { + // 循环n次 + for (int j = 0; j < array.length - 1 - i; j++) { + // 每次循环,找到当前元素为偶数,下一个元素为奇数,则交换 + if ((array[j] & 0x1) == 0 && (array[j + 1] & 0x1) == 1) { + swap(array, j, j + 1); + } + } + } + } + + /** + * 数据交换 + * @param arr + * @param x + * @param y + */ + private void swap(int[] arr, int x, int y) { + int temp = arr[x]; + arr[x] = arr[y]; + arr[y] = temp; + } +} +``` + +## 22、[链表中倒数第k个结点](https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 解题思路 + +设链表的长度为 N。设置两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到第 N - K 个节点处,该位置就是倒数第 K 个节点。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6b504f1f-bf76-4aab-a146-a9c7a58c2029.png) + +```java +public ListNode FindKthToTail(ListNode head, int k) { + if (head == null) + return null; + ListNode P1 = head; + while (P1 != null && k-- > 0) + P1 = P1.next; + if (k > 0) + return null; + ListNode P2 = head; + while (P1 != null) { + P1 = P1.next; + P2 = P2.next; + } + return P2; +} +``` + + + +```java +public class T14 { + /** + * 栈 + * @param head + * @param k + * @return + */ + public ListNode FindKthToTail2(ListNode head,int k) { + // 边界判断 + if (head == null || k <= 0) return null; + Stack stack = new Stack<>(); + // 遍历将元素压栈 + while (head != null) { + stack.push(head); + head = head.next; + } + // 弹栈k次 + int temp = 0; + while (!stack.empty()) { + ListNode listNode = stack.pop(); + temp++; + if (temp == k) { + return listNode; + } + } + return null; + } +} +``` + +## 23、[链表中环的入口结点](https://www.nowcoder.com/practice/253d2c59ec3e4bc68da16833f79a38e4?tpId=13&tqId=11208&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +一个链表中包含环,请找出该链表的环的入口结点。要求不能使用额外的空间。 + +### 解题思路 + +使用双指针,一个快指针 fast 每次移动两个节点,一个慢指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/bb7fc182-98c2-4860-8ea3-630e27a5f29f.png) + +```java +public ListNode EntryNodeOfLoop(ListNode pHead) { + if (pHead == null || pHead.next == null) + return null; + ListNode slow = pHead, fast = pHead; + do { + fast = fast.next.next; + slow = slow.next; + } while (slow != fast); // 相遇点 + fast = pHead; + while (slow != fast) { // fast从头走,slow还从相遇点走 + slow = slow.next; + fast = fast.next; + } + return slow; +} +``` + + + +```java +public class T55 { + // 哈希 可以用set + public ListNode EntryNodeOfLoop(ListNode pHead) { + if (null == pHead) { + return null; + } + HashMap map = new HashMap<>(); + map.put(pHead, 1); + while (null != pHead.next) { + // 入口节点肯定会被map包含 + if (map.containsKey(pHead.next)) { + return pHead.next; + } + map.put(pHead.next, 1); + pHead = pHead.next; + } + return null; + } +} +``` + + + + +## 24、[反转链表](https://www.nowcoder.com/practice/75e878df47f24fdc9dc3e400ec6058ca?tpId=13&tqId=11168&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public ListNode reverseList(ListNode head) { + ListNode pre = null; // 当前节点之前的节点 null + ListNode cur = head; + while (cur != null) { + ListNode nextTemp = cur.next; // 获取当前节点的下一个节点 + cur.next = pre; // 当前节点的下个节点指向前一个节点 + // 尾递归其实省了下面这两步 + pre = cur; // 将前一个节点指针移动到当前指针 + cur = nextTemp; // 当当前节点移动到下一个节点 + } + return pre; +} +``` + +```java +public class T15 { + public ListNode ReverseList(ListNode head) { + // 判断 + if (head == null) return null; + return reverse(null, head); + } + + /** + * 尾递归 + * @param pre + * @param cur + * @return + */ + private ListNode reverse(ListNode pre, ListNode cur) { + // 递归边界值判断 + if (cur == null) return pre; + // next节点指向cur.next + ListNode next = cur.next; + // cur.next 连接pre + cur.next = pre; + return reverse(cur, next); + } +} +``` + +## 25、[合并两个排序的链表](https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c094d2bc-ec75-444b-af77-d369dfb6b3b4.png) + +### 解题思路 + +```java +public class T16 { + public ListNode Merge(ListNode list1,ListNode list2) { + // 边界值判断 + // 如果list1为空,返回list2 + if (list1 == null) return list2; + // 如果list2为空,返回list1 + if (list2 == null) return list1; + // 如果list1.val < list2.val,则list1.next连接下一个比较值(递归比较) + if (list1.val < list2.val) { + list1.next = Merge(list1.next, list2); + return list1; + } else { + // 否则,list2.next 连接下一个比较值(递归比较) + list2.next = Merge(list1, list2.next); + return list2; + } + } +} +``` +## 26、[树的子结构](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/84a5b15a-86c5-4d8e-9439-d9fd5a4699a1.jpg) + +### 解题思路 + +```java +public class T17 { + public boolean HasSubtree(TreeNode root1, TreeNode root2) { + if (root1 == null || root2 == null) + return false; + return isSubtreeWithRoot(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2); + } + + private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) { + if (root2 == null) + return true; + if (root1 == null) + return false; + if (root1.val != root2.val) + return false; + return isSubtreeWithRoot(root1.left, root2.left) && isSubtreeWithRoot(root1.right, root2.right); + } + +} +``` + +## 27、[二叉树的镜像](https://www.nowcoder.com/practice/564f4c26aa584921bc75623e48ca3011?tpId=13&tqId=11171&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0c12221f-729e-4c22-b0ba-0dfc909f8adf.jpg) + +### 解题思路 + +```java +public class T18 { + public void Mirror(TreeNode root) { + // 判断 + if (root == null) return; + swap(root); + Mirror(root.left); + Mirror(root.right); + + } + + private void swap(TreeNode root) { + TreeNode t = root.left; + root.left = root.right; + root.right = t; + } +} +``` + + + + +## 28、[对称的二叉树](https://www.nowcoder.com/practice/ff05d44dfdb04e1d83bdbdab320efbcb?tpId=13&tqId=11211&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0c12221f-729e-4c22-b0ba-0dfc909f8adf.jpg) + +### 解题思路 + +```java +public class T58 { + boolean isSymmetrical(TreeNode pRoot) { + if (null == pRoot) { + return true; + } + return comRoot(pRoot.left, pRoot.right); + } + + private boolean comRoot(TreeNode left, TreeNode right) { + if (left == null && right == null) { + return true; + } + if (left == null || right == null) { + return false; + } + if (left.val != right.val) { + return false; + } + // 左右对比 + return comRoot(left.right, right.left) && comRoot(left.left, right.right); + } +} +``` + +## 29、[顺时针打印矩阵](https:/www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a?tpId=13&tqId=11172&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/48517227-324c-4664-bd26-a2d2cffe2bfe.png) + +### 解题思路 + +```java +public class T19 { + public ArrayList printMatrix(int [][] matrix) { + ArrayList list = new ArrayList<>(); + int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1; + while(r1 <= r2 && c1 <= c2) { + for (int i = c1; i <= c2; i++) { + list.add(matrix[r1][i]); + } + for (int i = r1 + 1; i <= r2; i++) { + list.add(matrix[i][c2]); + } + if (r1 != r2) { + for (int i = c2 - 1; i >= c1; i--) { + list.add(matrix[r2][i]); + } + } + if (c1 != c2) { + for (int i = r2 - 1; i >= r1; i--) { + list.add(matrix[i][c1]); + } + } + r1++; r2--; c1++; c2--; + } + return list; + } +} +``` + +## 30、[包含min函数的栈](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。 + +### 解题思路 + +```java +public class T20 { + + private Stack dataStack = new Stack<>(); + private Stack minStack = new Stack<>(); + + public void push(int node) { + dataStack.push(node);// dataStack添加元素 + minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node)); + } + + public void pop() { + dataStack.pop(); + // 辅助栈也得弹,因为每次push, 辅助栈也在push + minStack.pop(); + } + // 栈顶,没啥可说的 + public int top() { + return dataStack.peek(); + } + // 最小值,辅助栈弹就完事了 + public int min() { + return minStack.peek(); + } +} +``` + +## 31、[栈的压入、弹出序列](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&tqId=11174&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。 + +例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。 + +### 解题思路 + +```java +public class T21 { + public boolean IsPopOrder(int [] pushA,int [] popA) { + if (pushA == null || popA == null) return false; + int p = 0; + Stack stack = new Stack<>(); + for (int i = 0; i < pushA.length; i++) { + // 遍历压栈 + stack.push(pushA[i]); + // 每压一次, 就要将栈顶的元素和弹出序列判断是否相等 + // 如果相等,栈顶元素弹出,p++,继续while, + while (!stack.isEmpty() && stack.peek() == popA[p]) { + stack.pop(); + p++; + } + } + // 如果最后栈为空了, 说明压入序列和弹出序列一致 + return stack.isEmpty(); + } +} +``` + +## 32.1、[从上往下打印二叉树](https://www.nowcoder.com/practice/7fe2212963db4790b57431d9ed259701?tpId=13&tqId=11175&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +从上往下打印出二叉树的每个节点,同层节点从左至右打印。 + +例如,以下二叉树层次遍历的结果为:1,2,3,4,5,6,7 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d5e838cf-d8a2-49af-90df-1b2a714ee676.jpg) + +### 解题思路 + +使用队列来进行层次遍历。 + +不需要使用两个队列分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 + +```java +public class T22 { + // 层序遍历 + public ArrayList PrintFromTopToBottom(TreeNode root) { + ArrayList list = new ArrayList<>(); + // 需要用到队列 + LinkedList queue = new LinkedList<>(); + queue.offer(root); // 第一次先加根入队 + while (!queue.isEmpty()) { + int cnt = queue.size(); + // 如果队列不为空的话, 队列出一个元素 + while(cnt-- > 0) { + TreeNode t = queue.poll(); + if (t == null) continue; + list.add(t.val); + queue.add(t.left); + queue.add(t.right); + } + } + return list; + } +} +``` + +## 31.2、[把二叉树打印多行](https://www.nowcoder.com/practice/445c44d982d04483b04a54f298796288?tpId=13&tqId=11213&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +和22题:从上往下打印二叉树 差不多 + +```java +public class T60 { + + ArrayList> Print(TreeNode pRoot) { + ArrayList> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(pRoot); + while (!queue.isEmpty()) { + ArrayList list = new ArrayList<>(); + int cnt = queue.size(); + while (cnt-- > 0) { + TreeNode node = queue.poll(); + if (node == null) continue; + list.add(node.val); + queue.add(node.left); + queue.add(node.right); + } + if (list.size() != 0) ret.add(list); + } + return ret; + } +} +``` + +## 32.3、[按之字形顺序打印二叉树](https://www.nowcoder.com/practice/91b69814117f4e8097390d107d2efbe0?tpId=13&tqId=11212&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 + +### 解题思路 + +```java +public class T59 { + public ArrayList> Print(TreeNode pRoot) { + ArrayList> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(pRoot); + boolean reverse = false; + while (! queue.isEmpty()) { + ArrayList list = new ArrayList<>(); + int cnt = queue.size(); + while (cnt-- > 0) { + TreeNode node = queue.poll(); + if (node == null) continue; + list.add(node.val); + queue.add(node.left); + queue.add(node.right); + } + if (reverse) Collections.reverse(list); + reverse = ! reverse; + if (list.size() != 0) ret.add(list); + } + reutrn ret; +} +``` + + + +## 33、[二叉搜索树的后序遍历序列](https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd?tpId=13&tqId=11176&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。 + +例如,下图是后序遍历序列 1,3,2 所对应的二叉搜索树 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/13454fa1-23a8-4578-9663-2b13a6af564a.jpg) + +### 解题思路 + +```java +public class T23 { + public boolean VerifySquenceOfBST(int [] sequence) { + if (sequence == null || sequence.length == 0) return false; + return isBST(sequence, 0, sequence.length - 1); + } + private boolean isBST(int[] sequence, int first, int last) { + if (last - first <= 1) { + return true; + } + int rootVal = sequence[last]; + int cutIndex = first; + while (cutIndex < last && sequence[curIndex] <= rootVal) { // 二叉搜索树特征 + cutIndex++; + } + for (int i = cutIndedx; i < last; i++) { + if (sequence[i] < rootVal) return false; + } + return isBST(sequence, first, cutIndex - 1) && isBST(sequence, cutIndex, last - 1); + } +} +``` + +## 34、[二叉树中和为某一值的路径](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=11177&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。 + +下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ed77b0e6-38d9-4a34-844f-724f3ffa2c12.jpg) + +### 解题思路 + +```java +public class T24 { + + private ArrayList> ret = new ArrayList<>(); + + public ArrayList> FindPath(TreeNode root, int target) { + backtracking(root, target, new ArrayList<>()); + return ret; + } + + private void backtracking(TreeNode node, int target, ArrayList path) { + if (node == null) + return; + path.add(node.val); + target -= node.val; + if (target == 0 && node.left == null && node.right == null) { + ret.add(new ArrayList<>(path)); + } else { + backtracking(node.left, target, path); + backtracking(node.right, target, path); + } + path.remove(path.size() - 1); + } +} +``` + +## 35、[复杂链表的复制](https://www.nowcoder.com/practice/f836b2c43afc4b35ad6adc41ec941dba?tpId=13&tqId=11178&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。 + +```html +public class RandomListNode { + int label; + RandomListNode next = null; + RandomListNode random = null; + + RandomListNode(int label) { + this.label = label; + } +} + +``` + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/66a01953-5303-43b1-8646-0c77b825e980.png) + +### 解题思路 + +第一步,在每个节点的后面插入复制的节点。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/dfd5d3f8-673c-486b-8ecf-d2082107b67b.png) + +第二步,对复制节点的 random 链接进行赋值。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cafbfeb8-7dfe-4c0a-a3c9-750eeb824068.png) + +第三步,拆分。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e151b5df-5390-4365-b66e-b130cd253c12.png) + +```java +public class T25 { + public RandomListNode Clone(RandomListNode pHead) { + if (pHead == null) return null; + // 第一步:先复制一遍next + RandomListNode node = pHead; + while (node != null) { + RandomListNode copyNode = new RandomListNode(node.label); + copyNode.next = node.next; + node.next = copyNode; + node = copyNode.next; + } + // 第二步:再复制一遍random + node = pHead; + while (node != null) { + node.next.random = node.random == null ? null : node.random.next; + node = node.next.next; + } + // 第三步:切开 + node = pHead; + RandomListNode pCloneHead = pHead.next; + while (node != null) { + RandomListNode copyNode = node.next; + node.next = copyNode.next; + copyNode.next = copyNode.next == null ? null : copyNode.next.next; + node = node.next; + } + return pCloneHead; + } +} +``` + +## 36、[二叉搜索树与双向链表](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&tqId=11179&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/05a08f2e-9914-4a77-92ef-aebeaecf4f66.jpg) + +### 解题思路 + +```java +public class T26 { + private TreeNode pre = null; + private TreeNode head = null; + + public TreeNode Convert(TreeNode root) { + inOrder(root); + return head; + } + + private void inOrder(TreeNode node) { + if (node == null) return; + inOrder(node.left); + node.left = pre; + if (pre != null) + pre.right = node; + pre = node; + if (head == null) + head = node; + inOrder(node.right); + } +} +``` + +## 36、[序列化二叉树](https://github.com/DreamCats/leetniu/blob/master/nowcoder.com/practice/cf7e25aa97c04cc1a68c8f040e71fb84?tpId=13&tqId=11214&tPage=4&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class T61 { + String Serialize(TreeNode root) { + if (null == root) { + return ""; + } + StringBuffer sb = new StringBuffer(); + Serialize2(root, sb); + return sb.toString(); + } + + void Serialize2(TreeNode root, StringBuffer sb) { + if (null == root) { + sb.append("#,"); + return; + } + sb.append(root.val); + sb.append(","); + Serialize2(root.left, sb); + Serialize2(root.right, sb); + } + int index = -1; + + TreeNode Deserialize(String str) { + if (str.length() == 0) { + return null; + } + String[] strings = str.split(","); + return Deserialize2(strings); + } + TreeNode Deserialize2(String[] strings) { + index++; + if (!strings[index].equals("#")) { + TreeNode root = new TreeNode(0); + root.val = Integer.parseInt(strings[index]); + root.left = Deserialize2(strings); + root.right = Deserialize2(strings); + return root; + } + return null; + } +} + +``` + +## 38、[字符串的排列](https://www.nowcoder.com/practice/fe6b651b66ae47d7acce78ffdd9a96c7?tpId=13&tqId=11180&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。 + +```java +private ArrayList ret = new ArrayList<>(); + +public ArrayList Permutation(String str) { + if (str.length() == 0) + return ret; + char[] chars = str.toCharArray(); + Arrays.sort(chars); + backtracking(chars, new boolean[chars.length], new StringBuilder()); + return ret; +} + +private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) { + if (s.length() == chars.length) { + ret.add(s.toString()); + return; + } + for (int i = 0; i < chars.length; i++) { + if (hasUsed[i]) + continue; + if (i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) /* 保证不重复 */ + continue; + hasUsed[i] = true; + s.append(chars[i]); + backtracking(chars, hasUsed, s); + s.deleteCharAt(s.length() - 1); + hasUsed[i] = false; + } +} + +``` + +### 解题思路 + +```java +public class T27 { + public ArrayList Permutation(String str) { + ArrayList result = new ArrayList<>(); + if(str == null || str.length() == 0) { + return result; + } + char[] chars = str.toCharArray(); + TreeSet temp = new TreeSet<>(); + Permutation(chars, 0, temp); + result.addAll(temp); + return result; + } + + private void Permutation(char[] chars, int begin, TreeSet result) { + if (chars == null || chars.length == 0 || begin < 0 || begin > chars.length - 1) { + return; + } + if (begin == chars.length - 1) { + result.add(String.valueOf(chars)); + } else { + for (int i = begin; i <= chars.length - 1; i++) { + swap(chars, begin, i); + Permutation(chars, begin + 1, result); + swap(chars, begin, i); + } + } + } + private void swap(char[] x, int a, int b) { + char t = x[a]; + x[a] = x[b]; + x[b] = t; + } +} +``` + +## 39、[数组中出现次数超过一半的数字](https://www.nowcoder.com/practice/e8a1b01a2df14cb2b228b30ee6a92163?tpId=13&tqId=11181&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 解题思路 + +```java +public class T28 { + public int MoreThanHalfNum_Solution(int [] array) { + // 哈希的方法 + HashMap map = new HashMap<>(); + // 遍历一次每个元素的个数 + for (int i = 0; i < array.length; i++) { + if (map.containsKey(array[i])) { + map.put(array[i], map.get(array[i]) + 1); + } else { + map.put(array[i], 1); + } + } + int length = array.length >> 1; + // 查找哪个数的次数超过一半 + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() > length) { + return entry.getKey(); + } + } + return 0; + } +} +``` + +## 40、[最小的k个数](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 解题思路 + +```java +public class T29 { + public ArrayList GetLeastNumbers_Solution(int [] input, int k) { + ArrayList integers = new ArrayList<>(); + // 边界条件 + if (k > input.length){ + return integers; + } + // 先排序。。。 这里直接用Arrays.sort排序 + for (int i = 1; i < input.length; i++) { + for (int j = 0; j < input.length - i; j++) { + if (input[j] > input[j + 1]) { + swap(input, j, j + 1); + } + } + } + // 然后取k个数 + for (int i = 0; i < k; i++) { + integers.add(input[i]); + } + return integers; + } + private void swap(int[] arr, int i, int j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } +} +``` + +## 41.1、[数据流中的中位数](https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=4&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。 + +### 解题思路 + +```java +/* 大顶堆,存储左半边元素 */ +private PriorityQueue left = new PriorityQueue<>((o1, o2) -> o2 - o1); +/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */ +private PriorityQueue right = new PriorityQueue<>(); +/* 当前数据流读入的元素个数 */ +private int N = 0; + +public void Insert(Integer val) { + /* 插入要保证两个堆存于平衡状态 */ + if (N % 2 == 0) { + /* N 为偶数的情况下插入到右半边。 + * 因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素来的大, + * 因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点,取出堆顶元素即为最大元素,此时插入右半边 */ + left.add(val); + right.add(left.poll()); + } else { + right.add(val); + left.add(right.poll()); + } + N++; +} + +public Double GetMedian() { + if (N % 2 == 0) + return (left.peek() + right.peek()) / 2.0; + else + return (double) right.peek(); +} + +``` + + + +```java +public class T63 { + LinkedList list = new LinkedList<>(); + public void Insert(Integer num) { + if (list.size() == 0 || num < list.getFirst()) { + list.addFirst(num); + } else { + boolean insertFlag = false; + for(Integer e : list) { + if (num < e) { + int index = list.indexOf(e); + list.add(index, num); + insertFlag = true; + break; + } + } + if (!insertFlag) { + list.addLast(num); + } + } + } + + public Double GetMedian() { + if (list.size() == 0) { + return null; + } + if(list.size() % 2 == 0) { + int i = list.size() / 2; + Double a = Double.valueOf(list.get(i - 1) + list.get(i)); + return a / 2; + } + list.get(0); + return Double.valueOf(list.get(list.size() / 2)); + } +} +``` +## 41.2、[字符流中第一个不重复的字符](https://www.nowcoder.com/practice/00de97733b8e4f97a3fb5c680ee10720?tpId=13&tqId=11207&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 "go" 时,第一个只出现一次的字符是 "g"。当从该字符流中读出前六个字符“google" 时,第一个只出现一次的字符是 "l"。 + +### 解题思路 + +```java +private int[] cnts = new int[256]; +private Queue queue = new LinkedList<>(); + +public void Insert(char ch) { + cnts[ch]++; + queue.add(ch); + while (!queue.isEmpty() && cnts[queue.peek()] > 1) + queue.poll(); +} + +public char FirstAppearingOnce() { + return queue.isEmpty() ? '#' : queue.peek(); +} + +``` + + + +```java +public class T54 { + + int count[] = new int[256]; + int index = 1; + + public void Insert(char ch) + { + if (count[ch] == 0) { + count[ch] = index++; + } else { + count[ch] = -1; + } + } + + public char FirstAppearingOnce() + { + int temp = Integer.MAX_VALUE; + char ch = '#'; + for (int i = 0; i < count.length; i++) { + if (count[i] != 0 && count[i] != -1 && count[i] < temp) { + temp = count[i]; + ch = (char)i; + } + } + return ch; + } +} +``` + + + + +## 42、[连续子数组的最大和](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&tqId=11183&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +{6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。 + +### 解题思路 + +```java +public int FindGreatestSumOfSubArray(int[] nums) { + if (nums == null || nums.length == 0) + return 0; + int greatestSum = Integer.MIN_VALUE; + int sum = 0; + for (int val : nums) { + sum = sum <= 0 ? val : sum + val; + greatestSum = Math.max(greatestSum, sum); + } + return greatestSum; +} + +``` + +```java +public class T30 { + public int FindGreatestSumOfSubArray(int[] array) { + // 动态规划完事 + if (array == null || array.length == 0) return 0; + int res = array[0]; // 记录当前所有子数组的和的最大值 + int max = array[0]; // 记录包含arr[i]的连续子数组的最大值 + for (int i = 1; i < array.length; i++) { + max = Math.max(max + array[i], array[i]); // 动态规划公式 + res = Math.max(max, res); + } + return res; + } +} +``` +## 43、[整数中1出现的次数](https://www.nowcoder.com/practice/bd7f978302044eee894445e244c7eee6?tpId=13&tqId=11184&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 解题思路 + +```java +public class T31 { + public int NumberOf1Between1AndN_Solution(int n) { + int count = 0; + // 1~n中 遍历呗 + for (int i = 1; i <= n; i++) { + int num = i; + while(num != 0) { + // num%10:其实就是个数 是否为1 是的话count++ + if (num % 10 == 1) { + count++; + } + // num = num / 10 + num /= 10; + } + } + return count; + } +} +``` + + +## 45、[把数组排成最小的数](https://www.nowcoder.com/practice/8fecd3f8ba334add803bf2a06af1b993?tpId=13&tqId=11185&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {3,32,321},则打印出这三个数字能排成的最小数字为 321323。 + +### 解题思路 + +可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。 + +```java +public String PrintMinNumber(int[] numbers) { + if (numbers == null || numbers.length == 0) + return ""; + int n = numbers.length; + String[] nums = new String[n]; + for (int i = 0; i < n; i++) + nums[i] = numbers[i] + ""; + Arrays.sort(nums, (s1, s2) -> (s1 + s2).compareTo(s2 + s1)); + String ret = ""; + for (String str : nums) + ret += str; + return ret; +} + +``` + + + +```java +public class T32 { + public String PrintMinNumber(int [] numbers) { + if (numbers == null || numbers.length == 0) return ""; + int len = numbers.length; + String[] str = new String[len]; + StringBuffer sb = new StringBuffer(); + // 遍历numbers转成字符串数组 + for (int i = 0; i < len; i++) { + str[i] = String.valueOf(numbers[i]); + } + // 然后排序,重写Comparator + Arrays.sort(str, new Comparator() { + @Override + public int compare(String o1, String o2) { + String c1 = o1 + o2; + String c2 = o2 + o1; + return c1.compareTo(c2); + } + }); + for (int i = 0; i < len; i++) { + sb.append(str[i]); + } + return sb.toString(); + } +} +``` + +## 46、[礼物的最大值](https://www.nowcoder.com/questionTerminal/72a99e28381a407991f2c96d8cb238ab) + +### 题目描述 + +在一个 m*n 的棋盘的每一个格都放有一个礼物,每个礼物都有一定价值(大于 0)。从左上角开始拿礼物,每次向右或向下移动一格,直到右下角结束。给定一个棋盘,求拿到礼物的最大价值。例如,对于如下棋盘 + +```html +1 10 3 8 +12 2 9 6 +5 7 4 11 +3 7 16 5 +``` + +礼物的最大价值为 1+12+5+7+7+16+5=53。 + +### 解题思路 + +应该用动态规划求解,而不是深度优先搜索,深度优先搜索过于复杂,不是最优解。 + +```java +public int getMost(int[][] values) { + if (values == null || values.length == 0 || values[0].length == 0) + return 0; + int n = values[0].length; + int[] dp = new int[n]; + for (int[] value : values) { + dp[0] += value[0]; + for (int i = 1; i < n; i++) + dp[i] = Math.max(dp[i], dp[i - 1]) + value[i]; + } + return dp[n - 1]; +} +``` + +## 48、[最长不含重复字符的子字符串](#) + +### 题目描述 + +输入一个字符串(只包含 a~z 的字符),求其最长不含重复字符的子字符串的长度。例如对于 arabcacfr,最长不含重复字符的子字符串为 acfr,长度为 4。 + +### 解题思路 + +```java +public class LongestSubstring { + public int lengthOfLongestSubstring(String s) { + int n = s.length(), ans = 0; + HashMap map = new HashMap<>(); + // abcabc + for (int i = 0, j = 0; j < n; j++) { + if (map.containsKey(s.charAt(j))) { + i = Math.max(map.get(s.charAt(j)), i); // 求重复的字符串的索引,为了ans + } + ans = Math.max(ans, j - i + 1); // 求长度 + map.put(s.charAt(j), j + 1); // 也会同样覆盖重复的索引 + } + return ans; + } +} + +``` + + +## 49、[丑数](https://www.nowcoder.com/practice/6aa9e04fc3794f68acf8778237ba065b?tpId=13&tqId=11186&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。例如 6、8 都是丑数,但 14 不是,因为它包含因子 7。习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 N 个丑数。 + +### 解题思路 + + + +```java +public class T33 { + public int GetUglyNumber_Solution(int index) { + if (index <= 0) return 0; + int[] ans = new int[index]; + int count = 0; + int i2 = 0, i3 = 0, i5 = 0; + ans[0] = 1; + int temp = 0; + while (count < index - 1) { + // 先求i3 * 3 和 i5 * 5 的最小值,然后再求i2 * 2的最小值 + temp = min(ans[i2] * 2, min(ans[i3] * 3, ans[i5] * 5)); + if (temp == ans[i2] * 2) i2++; + if (temp == ans[i3] * 3) i3++; + if (temp == ans[i5] * 5) i5++; + ans[++count] = temp; + } + return ans[index - 1]; + } + private int min(int a, int b) { + return (a > b) ? b : a; + } +} +``` + +## 50、[第一个只出现一次的字符](https://www.nowcoder.com/practice/1c82e8cf713b4bbeb2a5b31cf5b0417c?tpId=13&tqId=11187&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +在一个字符串中找到第一个只出现一次的字符,并返回它的位置。 + +```html +Input: abacc +Output: b + +``` + +### 解题思路 + +最直观的解法是使用 HashMap 对出现次数进行统计,但是考虑到要统计的字符范围有限,因此可以使用整型数组代替 HashMap,从而将空间复杂度由 O(N) 降低为 O(1)。 + +```java +public int FirstNotRepeatingChar(String str) { + int[] cnts = new int[256]; + for (int i = 0; i < str.length(); i++) + cnts[str.charAt(i)]++; + for (int i = 0; i < str.length(); i++) + if (cnts[str.charAt(i)] == 1) + return i; + return -1; +} + +``` + + + +```java +public class T34 { + // 哈希方法 + public int FirstNotRepeatingChar(String str) { + if (str == null || str.length() == 0) return -1; + HashMap map = new HashMap<>(); + // 遍历计数 + for (int i = 0; i < str.length(); i++) { + if (map.containsKey(str.charAt(i))) { + map.put(str.charAt(i), map.get(str.charAt(i) + 1)); + } else { + map.put(str.charAt(i), 1); + } + } + // 如果等于1则返回 + for (int i = 0; i < str.length(); i++) { + if (map.get(str.charAt(i)) == 1) { + return i; + } + } + return -1; + } +} +``` +## 51、[数组中的逆序对](https://www.nowcoder.com/practice/96bd6684e04a44eb80e6a68efc0ec6c5?tpId=13&tqId=11188&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。 + +```java +public class T35 { + + private Integer count = 0; + + public int InversePairs(int [] array) { + if (array == null || array.length == 0) return 0; + mergeSort(array, 0, array.length - 1); + return (count % 1000000007); + } + // 归并排序 + private void mergeSort(int[] array, int left, int right) { + if (left < right) { + int mid = (left + right) >> 1; + mergeSort(array, left ,mid); + mergeSort(array, mid + 1, right); + merge(array, left, mid, right); + } + + } + + private void merge(int[] array, int left, int mid, int right) { + int[] help = new int[right - left + 1]; + int i = 0; + int p1 = left; + int p2 = mid + 1; + while(p1 <= mid && p2 <= right) { + if(array[p1] > array[p2]) { + help[i++] = array[p2++]; + count += mid - p1 + 1; + } else { + help[i++] = array[p1++]; + } + } + + while(p1 <= mid) { + help[i++] = array[p1++]; + } + + while(p2 <= right) { + help[i++] = array[p2++]; + } + + for(int j = 0; j < help.length; j++) { + array[left + j] = help[j]; + } + } +} +``` + +## 52、[两个链表的第一个公共结点](https://www.nowcoder.com/practice/6ab1d9a29e88450685099d45c9e31e46?tpId=13&tqId=11189&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/5f1cb999-cb9a-4f6c-a0af-d90377295ab8.png) + +### 解题思路 + +设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。 + +当访问链表 A 的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当访问链表 B 的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。 + +```java +public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + ListNode l1 = pHead1, l2 = pHead2; + while (l1 != l2) { + l1 = (l1 == null) ? pHead2 : l1.next; + l2 = (l2 == null) ? pHead1 : l2.next; + } + return l1; +} +``` + + + +```java +public class T36 { + + public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + // 哈希方法 + // 边界判断 + if (pHead1 == null || pHead2 == null) return null; + ListNode cur1 = pHead1; + ListNode cur2 = pHead2; + HashMap map = new HashMap<>(); + // 遍历第一个链表 + while (cur1 != null) { + map.put(cur1, 1); + cur1 = cur1.next; + } + // 遍历判断map查询第二个链表的节点 + while (cur2 != null) { + if (map.containsKey(cur2)) { + return cur2; + } + cur2 = cur2.next; + } + return null; + } +} +``` +## 53、[数字在排序数组中出现的次数](https://www.nowcoder.com/practice/70610bf967994b22bb1c26f9ae901fa2?tpId=13&tqId=11190&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +```html +Input: +nums = 1, 2, 3, 3, 3, 3, 4, 6 +K = 3 + +Output: +4 +``` + +### 解题思路 + +```java +public int GetNumberOfK(int[] nums, int K) { + int first = binarySearch(nums, K); + int last = binarySearch(nums, K + 1); + return (first == nums.length || nums[first] != K) ? 0 : last - first; +} + +private int binarySearch(int[] nums, int K) { + int l = 0, h = nums.length; + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] >= K) + h = m; + else + l = m + 1; + } + return l; +} +``` + + + +```java +public class T37 { + public int GetNumberOfK(int [] array , int k) { + int count = 0; + // 遍历数组 + for (int i : array) { + if (i == k) count++; + } + return count; + } +} +``` + +## 54、[二叉搜索树的第k个结点](https://www.nowcoder.com/practice/ef068f602dde4d28aab2b210e859150a?tpId=13&tqId=11215&tPage=4&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +private TreeNode ret; +private int cnt = 0; + +public TreeNode KthNode(TreeNode pRoot, int k) { + inOrder(pRoot, k); + return ret; +} + +private void inOrder(TreeNode root, int k) { + if (root == null || cnt >= k) + return; + inOrder(root.left, k); + cnt++; + if (cnt == k) + ret = root; + inOrder(root.right, k); +} + +``` + +```java +public class T62 { + int index = 0; + TreeNode KthNode(TreeNode pRoot, int k) { + if (null != pRoot) { + TreeNode node = KthNode(pRoot.left, k); + if (null != node) { + return node; + } + index++; + if (index == k) { + return pRoot; + } + node = KthNode(pRoot.right, k); + if (null != node) { + return node; + } + } + return null; + } +} +``` +## 55.1、[二叉树的深度](https://www.nowcoder.com/practice/435fb86331474282a3499955f0a41e8b?tpId=13&tqId=11191&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 解题思路 + +```java +public class T38 { + public int TreeDepth(TreeNode root) { + return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right)); + } +} +``` + +## 55.2、[平衡二叉树](https://www.nowcoder.com/practice/8b3b95850edb4115918ecebdf1b4d222?tpId=13&tqId=11192&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +平衡二叉树左右子树高度差不超过 1。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/af1d1166-63af-47b6-9aa3-2bf2bd37bd03.jpg) + +```java +private boolean isBalanced = true; + +public boolean IsBalanced_Solution(TreeNode root) { + height(root); + return isBalanced; +} + +private int height(TreeNode root) { + if (root == null || !isBalanced) + return 0; + int left = height(root.left); + int right = height(root.right); + if (Math.abs(left - right) > 1) + isBalanced = false; + return 1 + Math.max(left, right); +} +``` + +```java +public class T39 { + public boolean IsBalanced_Solution(TreeNode root) { + if (root == null) { + return true; + } + // 平衡二叉树的条件 + return Math.abs(maxDepth(root.left) - maxDepth(root.right)) <= 1 && IsBalanced_Solution(root.left) && IsBalanced_Solution(root.right); + } + // 最大深度 + private int maxDepth(TreeNode root) { + if(root == null) return 0; + return 1 + Math.max(maxDepth(root.left), maxDepth(root.right)); + } +} +``` + +## 56、[数组中只出现一次的数字](https://www.nowcoder.com/practice/e02fdb54d7524710a7d664d082bb7811?tpId=13&tqId=11193&tPage=2&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +一个整型数组里除了两个数字之外,其他的数字都出现了两次,找出这两个数。 + +### 解题思路 + +两个不相等的元素在位级表示上必定会有一位存在不同,将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。 + +diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。 + +```java +public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) { + int diff = 0; + for (int num : nums) + diff ^= num; + diff &= -diff; + for (int num : nums) { + if ((num & diff) == 0) + num1[0] ^= num; + else + num2[0] ^= num; + } +} +``` + + + +```java +public class T40 { + public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) { + if (array == null || array.length <= 1) { + num1[0] = num2[0] = 0; + return; + } + HashMap map = new HashMap<>(); + // 哈希计数 + for (int i = 0; i < array.length; i++) { + if (map.containsKey(array[i])) { + map.put(array[i], 2); + } else { + map.put(array[i], 1); + } + } + StringBuffer sb = new StringBuffer(); + // Sb存只出现一次的数字 + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 1) { + sb.append(entry.getKey()); + sb.append(","); + } + } + String[] strings = sb.toString().split(","); + num1[0] = Integer.valueOf(strings[0]); + num2[0] = Integer.valueOf(strings[1]); + } +} +``` + +## 57.1、[和为S的连续正数序列](https://www.nowcoder.com/practice/c451a3fd84b64cb19485dad758a55ebe?tpId=13&tqId=11194&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +输出所有和为 S 的连续正数序列。 + +例如和为 100 的连续序列有: + +```html +[9, 10, 11, 12, 13, 14, 15, 16] +[18, 19, 20, 21, 22]。 +``` + +### 解题思路 + +```java +public class T41 { + public ArrayList > FindContinuousSequence(int sum) { + ArrayList> arrayLists = new ArrayList<>(); + int phigh = 2; + int plow = 1; + // 双指针 快慢指针 + while(phigh > plow) { + // + int cur = (phigh + plow) * (phigh - plow + 1) / 2; // 特殊的计算方法 + if (cur < sum) { + phigh++; + } + if (cur > sum) { + plow++; + } + if (cur == sum) { + ArrayList arrayList = new ArrayList<>(); + for (int i = plow; i <= phigh; i++) { + arrayList.add(i); + } + arrayLists.add(arrayList); + plow++; + } + } + return arrayLists; + } +} +``` + +## 57.2、[和为S的两个数字](https://www.nowcoder.com/practice/390da4f7a00f44bea7c2f3d19491311b?tpId=13&tqId=11195&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得他们的和正好是 S。如果有多对数字的和等于 S,输出两个数的乘积最小的。 + +### 解题思路 + +使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。 + +- 如果两个指针指向元素的和 sum == target,那么得到要求的结果; +- 如果 sum > target,移动较大的元素,使 sum 变小一些; +- 如果 sum < target,移动较小的元素,使 sum 变大一些。 + +```java +public ArrayList FindNumbersWithSum(int[] array, int sum) { + int i = 0, j = array.length - 1; + while (i < j) { + int cur = array[i] + array[j]; + if (cur == sum) + return new ArrayList<>(Arrays.asList(array[i], array[j])); + if (cur < sum) + i++; + else + j--; + } + return new ArrayList<>(); +} +``` + + + +```java +public class T42 { + + public ArrayList FindNumbersWithSum(int [] array, int sum) { + int start = 0, end = array.length - 1; + // list存两数字 + ArrayList list = new ArrayList<>(); + // 类似于二分 + while (start < end) { + int count = array[start] + array[end]; + if (count < sum) { + start++; + } + if (count == sum) { + list.add(array[start]); + list.add(array[end]); + return list; + } + if (count > sum) { + end--; + } + } + return list; + } +} +``` + +## 58.1、[左旋转字符串](https://www.nowcoder.com/practice/12d959b108cb42b1ab72cef4d36af5ec?tpId=13&tqId=11196&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +```html +Input: +S="abcXYZdef" +K=3 + +Output: +"XYZdefabc" +``` + +### 解题思路 + +```java +public String LeftRotateString(String str, int n) { + if (n >= str.length()) + return str; + char[] chars = str.toCharArray(); + reverse(chars, 0, n - 1); + reverse(chars, n, chars.length - 1); + reverse(chars, 0, chars.length - 1); + return new String(chars); +} + +private void reverse(char[] chars, int i, int j) { + while (i < j) + swap(chars, i++, j--); +} + +private void swap(char[] chars, int i, int j) { + char t = chars[i]; + chars[i] = chars[j]; + chars[j] = t; +} + +``` + +```java +public class T43 { + public String LeftRotateString(String str,int n) { + if (str.length() == 0) return str; + // 用str的substring的api + for (int i = 0; i < n; i++) { + char c = str.charAt(0); + str = str.substring(1).concat(String.valueOf(c)); + } + return str; + } +} +``` + +## 58.2、[翻转单词顺序列](https://www.nowcoder.com/practice/3194a4f4cf814f63919d0790578d51f3?tpId=13&tqId=11197&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +```html +Input: +"I am a student." + +Output: +"student. a am I" +``` + +### 解题思路 + +```java +public class T44 { + + public String ReverseSentence(String str) { + if (str == null) return null; + // 边界判断 + if (str.trim().equals("")) return str; + // 切割 + String[] strings = str.split(" "); + StringBuffer sb = new StringBuffer(); + // 遍历 + for (int i = strings.length - 1; i >= 0; i--) { + sb.append(strings[i]).append(" "); + } + return sb.substring(0, sb.length() - 1); + } +} +``` + +## 59、[滑动窗口的最大值](https://www.nowcoder.com/practice/1624bc35a45c42c0bc17d17fa0cba788?tpId=13&&tqId=11217&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +### 题目描述 + +给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。 + +例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。 + +### 解题思路 + +可用大顶堆 + +```java +public ArrayList maxInWindows(int[] num, int size) { + ArrayList ret = new ArrayList<>(); + if (size > num.length || size < 1) + return ret; + PriorityQueue heap = new PriorityQueue<>((o1, o2) -> o2 - o1); /* 大顶堆 */ + for (int i = 0; i < size; i++) + heap.add(num[i]); + ret.add(heap.peek()); + for (int i = 0, j = i + size; j < num.length; i++, j++) { /* 维护一个大小为 size 的大顶堆 */ + heap.remove(num[i]); + heap.add(num[j]); + ret.add(heap.peek()); + } + return ret; +} +``` + + + +```java +public class T64 { + public ArrayList maxInWindows(int [] num, int size) { + if (null == num || size < 0) { + return null; + } + ArrayList list = new ArrayList<>(); + if(size == 0) { + return list; + } + int length = num.length; + ArrayList temp = null; + if (length < size) { + return list; + } else { + // 滑length-size+1次 + for (int i = 0; i < length - size + 1; i++) { + temp = new ArrayList<>(); + // 滑动窗口 + for (int j = i; j < size + i; j++) { + temp.add(num[j]); + } + // 排序 + Collections.sort(temp); + // 排序过后取最大值 并添加 + list.add(temp.get(temp.size() - 1)); + } + } + return list; + } +} +``` + + +## 61、[扑克牌顺序](https://www.nowcoder.com/practice/762836f4d43d43ca9deb273b3de8e1f4?tpId=13&tqId=11198&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +五张牌,其中大小鬼为癞子,牌面为 0。判断这五张牌是否能组成顺子。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/eaa506b6-0747-4bee-81f8-3cda795d8154.png) + +### 解题思路 + +```java +public class T45 { + public boolean isContinuous(int [] numbers) { + int numOfZero = 0; + int numOfInterval = 0; + int length = numbers.length; + if (length == 0) return false; + // 排序 + Arrays.sort(numbers); + for (int i = 0; i < length - 1; i++) { + // 计算癞子数量 也就是计算0的数量 + if (numbers[i] == 0) { + numOfZero++; + continue; + } + // 对子直接返回(特殊情况) + if (numbers[i] == numbers[i + 1]) return false; + + numOfInterval += numbers[i + 1] - numbers[i] - 1; + } + if (numOfZero >= numOfInterval) return true; + return false; + } +} +``` + +## 62、[孩子们的游戏](https://www.nowcoder.com/practice/f78a359491e64a50bce2d89cff857eb6?tpId=13&tqId=11199&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +让小朋友们围成一个大圈。然后,随机指定一个数 m,让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友,可以不用表演。 + +### 解题思路 + +约瑟夫环,圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m。因为是圆圈,所以最后需要对 n 取余。 + +```java +public int LastRemaining_Solution(int n, int m) { + if (n == 0) /* 特殊输入的处理 */ + return -1; + if (n == 1) /* 递归返回条件 */ + return 0; + return (LastRemaining_Solution(n - 1, m) + m) % n; +} +``` + + + +```java +public class T46 { + public int LastRemaining_Solution(int n, int m) { + // 边界判断 + if (n == 0 || m == 0) { + return -1; + } + ArrayList data = new ArrayList<>(); + // 遍历一次0~n + for (int i = 0; i < n; i++) { + data.add(i); + } + int index = -1; + // 循环 index + m 和 余 data的数量 + while (data.size() > 1) { + index = (index + m) % data.size(); + // 移除index + data.remove(index); + // index - 1 + index--; + } + return data.get(0); + } +} +``` + +## 63、[股票的最大利润](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/description/?utm_source=LCUS&utm_medium=ip_redirect_q_uns&utm_campaign=transfer2china) + +### 题目描述 + +可以有一次买入和一次卖出,买入必须在前。求最大收益。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/42661013-750f-420b-b3c1-437e9a11fb65.png) + +### 解题思路 + +```java +class Solution { + public int maxProfit(int[] prices) { + if(prices.length <= 1) return 0; + // int min = prices[0], max = 0; + // for(int i = 1; i < prices.length; i++){ + // max = Math.max(max, prices[i] - min); + // min = Math.min(min, prices[i]); + // } + // return max; + int dp[] = new int [prices.length]; + dp[0] = prices[0]; + int max = 0; + for (int i = 1; i < prices.length; i++) { + max = Math.max(max, prices[i] - dp[i - 1]); + dp[i] = Math.min(dp[i - 1], prices[i]); + } + return max; + } +} +``` + +## 64、[求1+2+3+...+n](https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句 A ? B : C。 + +### 解题思路 + +使用递归解法最重要的是指定返回条件,但是本题无法直接使用 if 语句来指定返回条件。 + +条件与 && 具有短路原则,即在第一个条件语句为 false 的情况下不会去执行第二个条件语句。利用这一特性,将递归的返回条件取非然后作为 && 的第一个条件语句,递归的主体转换为第二个条件语句,那么当递归的返回条件为 true 的情况下就不会执行递归的主体部分,递归返回。 + +本题的递归返回条件为 n <= 0,取非后就是 n > 0;递归的主体部分为 sum += Sum_Solution(n - 1),转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0。 + +```java +public class T47 { + public int Sum_Solution(int n) { + int res = n; + boolean t = ((res != 0) && ((res += Sum_Solution(n - 1)) != 0)); + return res; + } +} +``` + +## 65、[不用加减乘除做加法](https://www.nowcoder.com/practice/59ac416b4b944300b617d4f7f111b215?tpId=13&tqId=11201&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +写一个函数,求两个整数之和,要求不得使用 +、-、*、/ 四则运算符号。 + +### 解题思路 + +a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。 + +递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 + +```java +public int Add(int a, int b) { + return b == 0 ? a : Add(a ^ b, (a & b) << 1); +} +``` + + + +```java +public class T48 { + public int Add(int num1,int num2) { + while (num2 != 0) { + int temp = num1 ^ num2; // 没有进位的相加 + num2 = (num1 & num2) << 1; // 进位 + num1 = temp; // + } + return num1; + } +} +``` + +## 66、[构建乘积数组](https://www.nowcoder.com/practice/94a4d381a68b47b7a8bed86f2975db46?tpId=13&tqId=11204&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +给定一个数组 A[0, 1,..., n-1],请构建一个数组 B[0, 1,..., n-1],其中 B 中的元素 B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。要求不能使用除法。 + +![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/4240a69f-4d51-4d16-b797-2dfe110f30bd.png) + +### 解题思路 + +```java +public class T51 { + public int[] multiply(int[] A) { + int length = A.length; + int[] B = new int[length]; + if (length != 0) { + B[0] = 1; + // 计算下三角连乘 + for (int i = 1; i < length; i++) { + B[i] = B[i - 1] * A[i - 1]; + } + int temp = 1; + // 计算上三角 + for (int j = length - 2; j >= 0; j--) { + temp *= A[j + 1]; + B[j] *= temp; + } + } + return B; + } +} +``` + + +## 67、[把字符串转成整数](https://www.nowcoder.com/practice/1277c681251b4372bdef344468e4f26e?tpId=13&tqId=11202&tPage=3&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +### 题目描述 + +将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0 + +### 解题思路 + +```java +public class T49 { + public int StrToInt(String str) { + if (str == null || str.length() == 0) + return 0; + boolean isNegative = str.charAt(0) == '-'; + int ret = 0; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (i == 0 && (c == '+' || c == '-')) /* 符号判定 */ + continue; + if (c < '0' || c > '9') /* 非法输入 */ + return 0; + ret = ret * 10 + (c - '0'); + } + return isNegative ? -ret : ret; + } +} +``` + + + + diff --git "a/Java/alg/\345\244\232\347\272\277\347\250\213\347\274\226\347\250\213\351\242\230.md" "b/Java/alg/\345\244\232\347\272\277\347\250\213\347\274\226\347\250\213\351\242\230.md" new file mode 100644 index 00000000..8f8c454b --- /dev/null +++ "b/Java/alg/\345\244\232\347\272\277\347\250\213\347\274\226\347\250\213\351\242\230.md" @@ -0,0 +1,416 @@ +#### 一个多线程的问题,用三个线程,顺序打印字母A-Z,输出结果是1A 2B 3C 1D 2E…打印完毕最后输出一个Ok。 + +```java +public class Test { + private static char c = 'A'; + private static int i = 0; + + public static void main(String[] args) { + Runnable r = new Runnable() { + @Override + public void run() { + synchronized (this) { + try { + int id = Integer.parseInt(Thread.currentThread().getName()); + while (i < 26) { + if (i % 3 == id - 1) { + System.out.println("线程id:" + id + " " + (char) c++); + i++; + notifyAll(); + } else { + wait(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }; + new Thread(r, "1").start(); + new Thread(r, "2").start(); + new Thread(r, "3").start(); + } +} + +``` + +#### 每个线程把自己的ID在屏幕上打印10遍,要求输出结果必须按ABC的顺序显示 + +LockSupport + +```java +public class Main { + static Thread threadA, threadB, threadC; + public static void main(String[] args) { + threadA = new Thread(() -> { + for (int i = 0; i < 10; i++) { + System.out.print(Thread.currentThread().getName()); + LockSupport.unpark(threadB); + LockSupport.park(); + } + }, "A"); + + threadB = new Thread(() -> { + for (int i = 0; i < 10; i++) { + System.out.print(Thread.currentThread().getName()); + LockSupport.unpark(threadC); + LockSupport.park(); + } + }, "B"); + + threadC = new Thread(() -> { + for (int i = 0; i < 10; i++) { + System.out.print(Thread.currentThread().getName()); + LockSupport.unpark(threadA); + LockSupport.park(); + } + }, "C"); + + threadA.start(); + threadB.start(); + threadC.start(); + } +} +``` + +ReentrantLock + +```java +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +public class Main { + public static void main(String[] args) { + ReentrantLock lock = new ReentrantLock(); + Condition conA = lock.newCondition(); + Condition conB = lock.newCondition(); + Condition conC = lock.newCondition(); + + // A + new Thread(() -> { + try { + lock.lock(); + for (int i = 0; i < 10; i++) { + System.out.print(Thread.currentThread().getName()); + conB.signal(); + conA.await(); + } + conB.signal(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "A").start(); + + // B + new Thread(() -> { + try { + lock.lock(); + for (int i = 0; i < 10; i++) { + System.out.print(Thread.currentThread().getName()); + conC.signal(); + conB.await(); + } + conC.signal(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "B").start(); + + // C + new Thread(() -> { + try { + lock.lock(); + for (int i = 0; i < 10; i++) { + System.out.print(Thread.currentThread().getName()); + conA.signal(); + conC.await(); + } + conA.signal(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "C").start(); + } +} +``` + +#### 两线程奇偶数打印 + +```java +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +public class Main { + static int value = 0; + public static void main(String[] args) { + ReentrantLock lock = new ReentrantLock(); + Condition conA = lock.newCondition(); + Condition conB = lock.newCondition(); + // A + new Thread(() -> { + try { + lock.lock(); + while (value <= 100) { + System.out.println(Thread.currentThread().getName() + " " + value++); + conB.signal(); + conA.await(); + } + conB.signal(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "A").start(); + + // B + new Thread(() -> { + try { + lock.lock(); + while (value <= 100) { + System.out.println(Thread.currentThread().getName() + " " + value++); + conA.signal(); + conB.await(); + } + conA.signal(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "B").start(); + + } +} +``` + +#### 启动3个线程打印递增的数字, 线程1先打印1,2,3,4,5, 然后是线程2打印6,7,8,9,10。直到75 + +```java +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +public class Main { + static int value = 1; + public static void main(String[] args) { + ReentrantLock lock = new ReentrantLock(); + Condition conA = lock.newCondition(); + Condition conB = lock.newCondition(); + // A + new Thread(() -> { + try { + lock.lock(); + while (value <= 75) { + System.out.print(Thread.currentThread().getName() + " "); + for (int i = 0; i < 5; i++) { + System.out.print(value++ + " "); + } + System.out.println(""); + conB.signal(); + conA.await(); + } + conB.signal(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "A").start(); + + // B + new Thread(() -> { + try { + lock.lock(); + while (value <= 75) { + System.out.print(Thread.currentThread().getName() + " "); + for (int i = 0; i < 5; i++) { + System.out.print(value++ + " "); + } + System.out.println(""); + conA.signal(); + conB.await(); + } + conA.signal(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "B").start(); + + } +} +``` + +#### 如何确保三个线程顺序执行? + +join + +```java +public class ThreadTest1 { +// T1、T2、T3三个线程顺序执行 +public static void main(String[] args) { +    Thread t1 = new Thread(new Work(null)); +    Thread t2 = new Thread(new Work(t1)); +    Thread t3 = new Thread(new Work(t2)); +    t1.start(); +    t2.start(); +    t3.start(); + +} +static class Work implements Runnable { +    private Thread beforeThread; +    public Work(Thread beforeThread) { +        this.beforeThread = beforeThread; +    } +    public void run() { +        if (beforeThread != null) { +            try { +                beforeThread.join(); +                System.out.println("thread start:" + Thread.currentThread().getName()); +            } catch (InterruptedException e) { +                e.printStackTrace(); +            } +        } else { +            System.out.println("thread start:" + Thread.currentThread().getName()); +        } +    } + } +} +``` + +CountDownLatch + +```java +public class ThreadTest2 { + +// T1、T2、T3三个线程顺序执行 +public static void main(String[] args) { +    CountDownLatch c0 = new CountDownLatch(0); //计数器为0 +    CountDownLatch c1 = new CountDownLatch(1); //计数器为1 +    CountDownLatch c2 = new CountDownLatch(1); //计数器为1 + +    Thread t1 = new Thread(new Work(c0, c1)); +    //c0为0,t1可以执行。t1的计数器减1 + +    Thread t2 = new Thread(new Work(c1, c2)); +    //t1的计数器为0时,t2才能执行。t2的计数器c2减1 + +    Thread t3 = new Thread(new Work(c2, c2)); +    //t2的计数器c2为0时,t3才能执行 + +    t1.start(); +    t2.start(); +    t3.start(); + +} + +//定义Work线程类,需要传入开始和结束的CountDownLatch参数 +static class Work implements Runnable { +    CountDownLatch c1; +    CountDownLatch c2; + +    Work(CountDownLatch c1, CountDownLatch c2) { +        super(); +        this.c1 = c1; +        this.c2 = c2; +    } + +    public void run() { +        try { +            c1.await();//前一线程为0才可以执行 +            System.out.println("thread start:" + Thread.currentThread().getName()); +            c2.countDown();//本线程计数器减少 +        } catch (InterruptedException e) { +        } + +    } + } +} +``` + +blockingQueue + +```java +public class ThreadTest4 { +// T1、T2、T3三个线程顺序执行 +public static void main(String[] args) { +    //blockingQueue保证顺序 +    BlockingQueue blockingQueue = new LinkedBlockingQueue(); +    Thread t1 = new Thread(new Work()); +    Thread t2 = new Thread(new Work()); +    Thread t3 = new Thread(new Work()); + +    blockingQueue.add(t1); +    blockingQueue.add(t2); +    blockingQueue.add(t3); + +    for (int i=0;i<3;i++) { +        Thread t = null; +        try { +            t = blockingQueue.take(); +        } catch (InterruptedException e) { +            e.printStackTrace(); +        } +        t.start(); +        //检测线程是否还活着 +        while (t.isAlive()); +    } +} + +static class Work implements Runnable { + +    public void run() { +        System.out.println("thread start:" + Thread.currentThread().getName()); +    } + } +} +``` + +CachedThreadPool + +```java + public class ThreadTest3 { +    // T1、T2、T3三个线程顺序执行 +   public static void main(String[] args) { +    FutureTask future1= new FutureTask(new Work(null)); +    Thread t1 = new Thread(future1); + +    FutureTask future2= new FutureTask(new Work(future1)); +    Thread t2 = new Thread(future2); + +    FutureTask future3= new FutureTask(new Work(future2)); +    Thread t3 = new Thread(future3); + +    t1.start(); +    t2.start(); +    t3.start(); +} + + static class Work  implements Callable { +    private FutureTask beforeFutureTask; +    public Work(FutureTask beforeFutureTask) { +        this.beforeFutureTask = beforeFutureTask; +    } +    public Integer call() throws Exception { +        if (beforeFutureTask != null) { +            Integer result = beforeFutureTask.get();//阻塞等待 +            System.out.println("thread start:" + Thread.currentThread().getName()); +        } else { +            System.out.println("thread start:" + Thread.currentThread().getName()); +        } +        return 0; +    } + } +} +``` + +[https://blog.csdn.net/Evankaka/article/details/80800081](https://blog.csdn.net/Evankaka/article/details/80800081) + diff --git "a/Java/alg/\346\214\211\347\203\255\345\272\246\346\200\273\347\273\223lc.md" "b/Java/alg/\346\214\211\347\203\255\345\272\246\346\200\273\347\273\223lc.md" new file mode 100644 index 00000000..9cf73053 --- /dev/null +++ "b/Java/alg/\346\214\211\347\203\255\345\272\246\346\200\273\347\273\223lc.md" @@ -0,0 +1,4735 @@ +## 说明 +> 按lc的热度总结的题目,我看了不少面经,总感觉他们出的算法题,不会太难,肯定是比较热门的题。 + +## 1. 两数之和(4897) +[https://leetcode-cn.com/problems/two-sum/](https://leetcode-cn.com/problems/two-sum/) + +```html +给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 +你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 +``` +```html +给定 nums = [2, 7, 11, 15], target = 9 + +因为 nums[0] + nums[1] = 2 + 7 = 9 +所以返回 [0, 1] +``` +方法很多,看个人,这一套根据我个人准备的,准确且快即可。 + +**双指针** + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + int p1 = 0, p2 = nums.length - 1; + while (p1 < p2) { + int sum = nums[p1] + nums[p2]; + if (sum < target) + p1++; + else if (sum > target) + p2--; + else return + new int[] {p1, p2}; + } + return new int[]{}; + } +} +``` + +## 2. 两数相加(2819) +[https://leetcode-cn.com/problems/add-two-numbers/](https://leetcode-cn.com/problems/add-two-numbers/) + +```html +给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。 + +如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 + +您可以假设除了数字 0 之外,这两个数都不会以 0 开头。 +``` +```html +输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) +输出:7 -> 0 -> 8 +原因:342 + 465 = 807 +``` + +1. 两个链表相加,先判断边界 +2. 创建一个新的链表 +3. while中也要注意边界 +4. 两个链表的值、进位相加 +5. 是否赋值给进位 +6. 三个链表移动指针 + +```java +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + if (l1 == null && l2 == null) return null; + if (l1 == null) return l2; + if (l2 == null) return l1; + ListNode p1 = l1; + ListNode p2 = l2; + ListNode l3 = new ListNode(-1); + ListNode p3 = l3; + int carried = 0; + while (p1 != null || p2 != null) { + int a = p1 != null ? p1.val : 0; + int b = p2 != null ? p2.val : 0; + p3.next = new ListNode((a + b + carried) % 10); + carried = (a + b + carried) / 10; + p3 = p3.next; + p1 = p1 != null ? p1.next : null; + p2 = p2 != null ? p2.next : null; + } + p3.next = carried != 0 ? new ListNode(1) : null; + return l3.next; + } +} +``` + +## 3. 无重复字符的最长子串(2862) +[https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) + +```html +给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 +``` +```html +输入: "abcabcbb" +输出: 3 +解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 + +输入: "bbbbb" +输出: 1 +解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 +``` +HashMap+两个指针 +两个指针分别记录字母的起始和结束,用map来存 +```java +class Solution { + public int lengthOfLongestSubstring(String s) { + int n = s.length(), ans = 0; + Map map = new HashMap<>(); + for (int i = 0, j = 0; j < n; j++) { + if (map.containsKey(s.charAt(j))) { + i = Math.max(map.get(s.charAt(j)), i); + } + ans = Math.max(ans, j - i + 1); + map.put(s.charAt(j), j + 1); + } + return ans; + } +} +``` + +## 5. 最长回文子串(1478) +[https://leetcode-cn.com/problems/longest-palindromic-substring/](https://leetcode-cn.com/problems/longest-palindromic-substring/) + +```html +给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。 +``` +```html +输入: "babad" +输出: "bab" +注意: "aba" 也是一个有效答案。 +``` + +中心扩展 +- 两种情况 +- 奇数长度 +- 偶数长度 +- 取最长,求起始和结束位置 +- 用substring即可 + +```java +class Solution { + public String longestPalindrome(String s) { + if (s == null || s.length() == 0) return s; + int start = 0, end = 0; // 记录起始位置 + for (int i = 0; i < s.length(); i++) { + // 两种情况 以i为中心,以i和i+1为中心 + int len1 = expand(s, i - 1, i + 1); // 中心扩展 + int len2 = expand(s, i, i + 1); + int len = Math.max(len1, len2); // 取最长的长度 + if (len > end - start) { + start = i - (len - 1) / 2; + end = i + len / 2; + } + } + return s.substring(start, end + 1); + } + + private int expand(String s, int l, int r) { + while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { + l--; + r++; + } + // 这里要注意 + return r - l - 1; + } +} +``` + +## 6. Z 字形变换(1086) + +[https://leetcode-cn.com/problems/zigzag-conversion/](https://leetcode-cn.com/problems/zigzag-conversion/) + +```java +class Solution { + // 0| 1 5 9 13 + // 1| 2 4 6 8 10 12 14 16 + // 2| 3 7 11 15 + // 每一行右边的字符的 ’索引值’ 都是其左边的字符的 ’索引值’ 加上它 ’下面剩余行数’ 的两倍或 ’上面行数’ 的两倍(交替相加) + // 以第二行为例, + // 对于4这个字符而言, 4 = 2(左边的索引) + 2(两倍) * 1(下面有一行) + // 6 = 4 + 2 * 1(上面有一行) + // 8 = 6 + 2 * 1(下面有一行) + public String convert(String s, int numRows) { + if (numRows <= 1) + return s; + + char[] cs = s.toCharArray(); + String res = ""; + for (int i = 0; i < numRows; i++){ + int up = i; // 上方的行数 + int down = numRows - 1 - i; // 下方的行数 + int temp = i; + int cnt = 0; + while (temp < cs.length){ + if (cnt % 2 == 0 && down != 0) { + res += cs[temp]; + temp += 2 * down; + } else if(cnt % 2 != 0 && up != 0){ + res += cs[temp]; + temp += 2 * up; + } + cnt++; + } + } + return res; + } +} +``` + +## 7. 整数反转(2088) +[https://leetcode-cn.com/problems/reverse-integer/](https://leetcode-cn.com/problems/reverse-integer/) + +```html +给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。 +``` +```html +输入: 123 +输出: 321 + +输入: -123 +输出: -321 + +输入: 120 +输出: 21 +``` + +```java +class Solution { + public int reverse(int x) { + 注意题目条件 + long ans = 0; + while(x != 0) { + // 常用公式 + ans = ans * 10 + x % 10; + x /= 10; + } + // 判断是否溢出 + if(ans > Integer.MAX_VALUE || ans < Integer.MIN_VALUE) { + return 0; + } + return (int)ans; + } +} +``` + +## 9. 回文数(2094) +[https://leetcode-cn.com/problems/palindrome-number/](https://leetcode-cn.com/problems/palindrome-number/) +```html +判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。 +``` + +```html +输入: 121 +输出: true + +输入: -121 +输出: false +解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。 + +输入: 10 +输出: false +解释: 从右向左读, 为 01 。因此它不是一个回文数。 +``` + +```java +class Solution { + public boolean isPalindrome(int x) { + // 0也是回文数 + if (x == 0) + return true; + // 特殊条件 + if (x < 0 || x % 10 == 0) + return false; + // 只需要一半 + int right = 0; + while ( x > right) { + right = right * 10 + x % 10; + x /= 10; + } + return x == right || x == right / 10; + } +} +``` +## 11. 盛最多水的容器(1267) +[https://leetcode-cn.com/problems/container-with-most-water/](https://leetcode-cn.com/problems/container-with-most-water/) +```html +给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 + +说明:你不能倾斜容器,且 n 的值至少为 2。 +``` + +```html +输入:[1,8,6,2,5,4,8,3,7] +输出:49 +``` + +```java +class Solution { + public int maxArea(int[] height) { + int max = 0; + for (int i = 0, j = height.length - 1; i < j;) { + // 双指针,谁小取谁,判断移动 + int minHeight = height[i] < height[j] ? height[i++] : height[j--]; + // 每一次都要维护最大值 + max = Math.max(max, (j - i + 1) * minHeight); + } + return max; + } +} +``` + +## 14. 最长公共前缀(1611) +[https://leetcode-cn.com/problems/longest-common-prefix/](https://leetcode-cn.com/problems/longest-common-prefix/) +```html +编写一个函数来查找字符串数组中的最长公共前缀。 + +如果不存在公共前缀,返回空字符串 ""。 +``` + +```html +输入: ["flower","flow","flight"] +输出: "fl" + +输入: ["dog","racecar","car"] +输出: "" +解释: 输入不存在公共前缀。 +``` + +```java +class Solution { + public String longestCommonPrefix(String[] strs) { + if(strs.length == 0) + return ""; + String str = strs[0]; + // 循环用indexOf和substring + for(int i = 1; i < strs.length; i++) { + while(strs[i].indexOf(str) != 0) { + // 每次substring去掉最后一位 + str = str.substring(0, str.length() - 1); + } + } + return str; + } +} +``` + +## 15. 三数之和 +[https://leetcode-cn.com/problems/3sum/](https://leetcode-cn.com/problems/3sum/) + +```html +给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。 +``` + +```html +给定数组 nums = [-1, 0, 1, 2, -1, -4], + +满足要求的三元组集合为: +[ + [-1, 0, 1], + [-1, -1, 2] +] +``` + +这道题三要素: +1. 排序 +2. 双指针 +3. 去重复 +```java +class Solution { + public List> threeSum(int[] nums) { + // 排序的目的就是来告别重复 + Arrays.sort(nums); + List> ls = new ArrayList<>(); + for (int i = 0; i < nums.length - 2; i++) { + // 判断是否元素大于0,大于0,没必要操作了 + if (nums[i] > 0) break; + // 判断是否重复 + if (i > 0 && nums[i] == nums[i - 1]) continue; + // 双指针操作 + int l = i + 1, r = nums.length - 1; + while (l < r) { + if (nums[l] + nums[r] < -nums[i]) l++; + else if (nums[l] + nums[r] > -nums[i]) r--; + else { + // 相等了哈 + ls.add(Arrays.asList(nums[i], nums[l], nums[r])); + // 防止重复 + while (l < r && nums[l] == nums[l + 1]) l++; + while (l < r && nums[r] == nums[r - 1]) r--; + l++; + r--; + } + } + } + return ls; + } +} +``` + +哪种代码好理解就用哪种 +```java +class Solution { + public List> threeSum(int[] nums) { + // 排序 + Arrays.sort(nums); + List> ls = new ArrayList<>(); + for (int i = 0; i < nums.length - 2; i++) { + if (i == 0 || (i > 0 && nums[i] != nums[i - 1])) { //跳过可能重复的 + // 转化为两数之和 + int l = i + 1, r = nums.length - 1, sum = 0 - nums[i]; + while(l < r) { + if (nums[l] + nums[r] == sum) { + ls.add(Arrays.asList(nums[i], nums[l], nums[r])); + while (l < r && nums[l] == nums[l + 1]) l++; // 还是一样,跳过重复 + while (l < r && nums[r] == nums[r - 1]) r--; + l++; + r--; + } else if (nums[l] + nums[r] < sum) { + while (l < r && nums[l] == nums[l + 1]) l++; + l++; + } else { + while (l < r && nums[r] == nums[r - 1]) r--; + r--; + } + } + } + } + return ls; + } +} +``` + +## 17. 电话号码的字母组合(1085) +[https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/) +```html +给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。 + +给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 +``` +```html +输入:"23" +输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. +``` + +回溯 +不需要标记 +但是记得deleteCharAt +```java +class Solution { + private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; + public List letterCombinations(String digits) { + List combinnations = new ArrayList<>(); + if (digits == null || digits.length() == 0) return combinnations; + doCombination(new StringBuilder(), combinnations, digits); + return combinnations; + } + + private void doCombination(StringBuilder prefix, List combinnations, final String digits) { + if (prefix.length() == digits.length()) { + combinnations.add(prefix.toString()); + return; + } + int curDigits = digits.charAt(prefix.length()) - '0'; + String letters = KEYS[curDigits]; + for (char c : letters.toCharArray()) { + prefix.append(c); + doCombination(prefix, combinnations, digits); + prefix.deleteCharAt(prefix.length() - 1); + } + } +} +``` + +## 19. 删除链表的倒数第N个节点(1316) +[https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) +```html +给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。 +``` +```html +给定一个链表: 1->2->3->4->5, 和 n = 2. + +当删除了倒数第二个节点后,链表变为 1->2->3->5. +``` + +快慢指针 +分情况 +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode fast = head; + while (n-- > 0) { + fast = fast.next; + } + // 这里没懂, 得举例子就懂了 + if (fast == null) + return head.next; + ListNode slow = head; + while (fast.next != null) { + fast = fast.next; + slow = slow.next; + } + // 这里也懂了...举个例子就行 + slow.next = slow.next.next; + return head; + } +} +``` + +## 20. 有效的括号(2132) +[https://leetcode-cn.com/problems/valid-parentheses/](https://leetcode-cn.com/problems/valid-parentheses/) +```html +给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。 + +有效字符串需满足: + +左括号必须用相同类型的右括号闭合。 +左括号必须以正确的顺序闭合。 +注意空字符串可被认为是有效字符串。 +``` +```html +输入: "()" +输出: true + +输入: "()[]{}" +输出: true + +输入: "(]" +输出: false + +输入: "([)]" +输出: false +``` +栈思想 +```java +class Solution { + public boolean isValid(String s) { + Stack stack = new Stack<>(); + for (char c : s.toCharArray()) { + if (stack.isEmpty()) + stack.push(c); + else if(isSym(stack.peek(), c)) + stack.pop(); + else + stack.push(c); + } + return stack.isEmpty(); + } + + private boolean isSym(char c1, char c2) { + return (c1 == '(' && c2 == ')') + || (c1 == '{' && c2 == '}') + || (c1 == '[' && c2 == ']'); + } +} +``` +## 21. 合并两个有序链表(1327) +[https://leetcode-cn.com/problems/merge-two-sorted-lists/](https://leetcode-cn.com/problems/merge-two-sorted-lists/) +```html +将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 +``` + +```html +输入:1->2->4, 1->3->4 +输出:1->1->2->3->4->4 +``` + +```java +class Solution { + public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + if (l1 == null) return l2; + if (l2 == null) return l1; + if (l1.val < l2.val) { + l1.next = mergeTwoLists(l1.next, l2); + return l1; + } else { + l2.next = mergeTwoLists(l1, l2.next); + return l2; + } + } +} +``` + +## 22. 括号生成(1187) + +[https://leetcode-cn.com/problems/generate-parentheses/](https://leetcode-cn.com/problems/generate-parentheses/) + +```java +class Solution { + List ret = new ArrayList<>(); + public List generateParenthesis(int n) { + dfs("", 0, 0, n); + return ret; + } + + public void dfs(String ans, int cnt1, int cnt2, int n){ + if (cnt1 > n || cnt2 > n) + return; + if (cnt1 == n && cnt2 == n) + ret.add(ans); + if (cnt1 >= cnt2){ + String ans1 = new String(ans); + dfs(ans + "(", cnt1 + 1, cnt2, n); + dfs(ans + ")", cnt1, cnt2 + 1, n); + } + } +} +``` + +## 25. K 个一组翻转链表(850) +```java +class Solution { + public ListNode reverseKGroup(ListNode head, int k) { + ListNode dummy = new ListNode(-1); + ListNode pre = dummy, cur = head, next; + dummy.next = head; + ListNode p = head; + int len = 0; + while (p != null){ + len++; + p = p.next; + } + // + for (int i = 0; i < len / k; i++) { + for (int j = 0; j < k - 1; j++) { + next = cur.next; + + // 注意这三步 + cur.next = next.next; + next.next = pre.next; + pre.next = next; + } + // 移动的时候注意 + pre = cur; + cur = pre.next; + } + return dummy.next; + } +} +``` + +## 26. 删除排序数组中的重复项(1748) +[https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/) +```html +给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 +``` +```html +给定数组 nums = [1,1,2], + +函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 + +你不需要考虑数组中超出新长度后面的元素。 +``` +```java +class Solution { + public int removeDuplicates(int[] nums) { + int p = 0; + for(int i = 1; i < nums.length; i++) { + if(nums[p] != nums[i]) { + nums[++p] = nums[i]; + } + } + return p+1; + } +} +``` + +## 27. 移除元素(1411) +[https://leetcode-cn.com/problems/remove-element/](https://leetcode-cn.com/problems/remove-element/) +```html +给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 + +元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 +``` +```html +给定 nums = [3,2,2,3], val = 3, + +函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 + +你不需要考虑数组中超出新长度后面的元素。 +``` + +```java +class Solution { + public int removeElement(int[] nums, int val) { + int p = 0; + for (int i = 0; i < nums.length; i++) { + if(nums[i] != val) + nums[p++] = nums[i]; + } + return p; + } +} +``` + +## 28. 实现 strStr()(1176) +[https://leetcode-cn.com/problems/implement-strstr/](https://leetcode-cn.com/problems/implement-strstr/) +```html +实现 strStr() 函数。 + +给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回  -1。 +``` + +```html +输入: haystack = "hello", needle = "ll" +输出: 2 +``` +```java +class Solution { + public int strStr(String haystack, String needle) { + int l = haystack.length(), n = needle.length(); + for(int start = 0; start < l - n + 1; start++) { + // subtring + equals + if(haystack.substring(start, start + n).equals(needle)) { + return start; + } + } + return -1; + } +} +``` + +## 33. 搜索旋转排序数组(1033) +[https://leetcode-cn.com/problems/search-in-rotated-sorted-array/](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/) + +变相的二分,稍微有点难度 + +```java +class Solution { + public int search(int[] nums, int target) { + int len = nums.length; + int left = 0, right = len - 1; + while (left <= right) { + int mid = (left + right) / 2; + if (nums[mid] == target) + return mid; + else if(nums[mid] < nums[right]) { + // 注意边界条件 + if (nums[mid] < target && target <= nums[right]) + left = mid + 1; + else + right = mid - 1; + } else { + if (nums[left] <= target && target < nums[mid]) + right = mid - 1; + else + left = mid + 1; + } + } + return -1; + } +} +``` + +## 35. 搜索插入位置(1239) +[https://leetcode-cn.com/problems/search-insert-position/](https://leetcode-cn.com/problems/search-insert-position/) +```html +给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 + +你可以假设数组中无重复元素。 +``` +```html +输入: [1,3,5,6], 5 +输出: 2 +输入: [1,3,5,6], 2 +输出: 1 +输入: [1,3,5,6], 7 +输出: 4 +输入: [1,3,5,6], 0 +输出: 0 +``` +二分法 +但要考虑边界 +```java +class Solution { + public int searchInsert(int[] nums, int target) { + int l = 0; + int h = nums.length - 1; + while (l <= h) { + int mid = l + (h - l) / 2; + if (nums[mid] < target) + l = mid + 1; + else if (nums[mid] > target) + h = mid - 1; + else + return mid; + } + // 注意边界 + if (h < 0 && l == 0) + return (l + h) % 2 + 1; + else + return (l + h) / 2 + 1; + } +} +``` + +## 38. 外观数列(1053) +[https://leetcode-cn.com/problems/count-and-say/](https://leetcode-cn.com/problems/count-and-say/) +```html +给定一个正整数 n(1 ≤ n ≤ 30),输出外观数列的第 n 项。 + +注意:整数序列中的每一项将表示为一个字符串。 + +「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。前五项如下: + +1. 1 +2. 11 +3. 21 +4. 1211 +5. 111221 + +第一项是数字 1 + +描述前一项,这个数是 1 即 “一个 1 ”,记作 11 + +描述前一项,这个数是 11 即 “两个 1 ” ,记作 21 + +描述前一项,这个数是 21 即 “一个 2 一个 1 ” ,记作 1211 + +描述前一项,这个数是 1211 即 “一个 1 一个 2 两个 1 ” ,记作 111221 +``` + +```html +输入: 1 +输出: "1" +解释:这是一个基本样例。 + +输入: 4 +输出: "1211" +解释:当 n = 3 时,序列是 "21",其中我们有 "2" 和 "1" 两组,"2" 可以读作 "12",也就是出现频次 = 1 而 值 = 2;类似 "1" 可以读作 "11"。所以答案是 "12" 和 "11" 组合在一起,也就是 "1211"。 +``` +StringBuffer + cnt +```java +class Solution { + public String countAndSay(int n) { + if (n == 1) { + return "1"; + } + String str = "1"; + for (int i = 0; i < n - 1; i++) { + StringBuffer sb = new StringBuffer(); + int count = 0; + char code = str.charAt(0); + for (int j = 0; j < str.length(); j++) { + if (str.charAt(j) != code) { + sb.append(count); + sb.append(code); + code = str.charAt(j); + count = 1; + } else { + count++; + } + } + sb.append(count); + sb.append(str.charAt(str.length() - 1)); + str = sb.toString(); + } + return str; + } +} +``` + +## 42. 接雨水(1145) +[https://leetcode-cn.com/problems/trapping-rain-water/](https://leetcode-cn.com/problems/trapping-rain-water/) + +双指针 + +```java +class Solution { + public int trap(int[] height) { + int min = 0, max = 0; + int l = 0, r = height.length - 1; + int res = 0; + while(l < r) { + // 双指针维护最小值 + min = height[height[l] < height[r] ? l++ : r--]; + // 接着维护最大值 + max = Math.max(max, min); + // 累加差值 + res += max - min; + } + return res; + } +} +``` + +## 43. 字符串相乘 + +[https://leetcode-cn.com/problems/multiply-strings/](https://leetcode-cn.com/problems/multiply-strings/) + +```java +class Solution { + public String multiply(String num1, String num2) { + int len1 = num1.length(); + int len2 = num2.length(); + if (len1 == 0 || len2 == 0) return "0"; + int[] mul = new int[len1 + len2]; + for (int i = len1 - 1; i >= 0; i--){ + for (int j = len2 - 1; j >= 0; j--){ + int n = (num1.charAt(i) - '0') * (num2.charAt(j) - '0') + mul[i + j + 1]; + mul[i + j + 1] = n % 10; + mul[i + j] += n / 10; + } + } + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < len1 + len2 - 1 && mul[i] == 0) i++; + while (i < len1 + len2) sb.append(mul[i++]); + return sb.toString(); + } + +} +``` + +## 53. 最大子序和(1385) +[https://leetcode-cn.com/problems/maximum-subarray/](https://leetcode-cn.com/problems/maximum-subarray/) + +```html +给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 +``` + +```html +输入: [-2,1,-3,4,-1,2,1,-5,4], +输出: 6 +解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 +``` + +```java +class Solution { + public int maxSubArray(int[] nums) { + if (nums == null || nums.length == 0) return 0; + int preSum = nums[0]; + int maxSum = preSum; + for (int i = 1; i < nums.length; i++) { + // 注意条件 + preSum = preSum > 0 ? preSum + nums[i] : nums[i]; + maxSum = Math.max(maxSum, preSum); + } + return maxSum; + } +} +``` + +## 55. 跳跃游戏(1059) +[https://leetcode-cn.com/problems/jump-game/](https://leetcode-cn.com/problems/jump-game/) + +```html +给定一个非负整数数组,你最初位于数组的第一个位置。 + +数组中的每个元素代表你在该位置可以跳跃的最大长度。 + +判断你是否能够到达最后一个位置。 +``` + +```html +输入: [2,3,1,1,4] +输出: true +解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。 + +输入: [3,2,1,0,4] +输出: false +解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。 +``` + +贪心 +```java +class Solution { + public boolean canJump(int[] nums) { + if (nums.length <= 1) return true; + int n = nums.length; + int max = nums[0]; + for (int i = 1; i < n - 1; i++) { + // 注意条件 + if (i <= max) { + // 最远索引 + max = Math.max(max, nums[i] + i); + } else { + break; + } + } + // 注意判断 + return max >= n - 1; + } +} +``` + +## 66. 加一(1254) +[https://leetcode-cn.com/problems/plus-one/](https://leetcode-cn.com/problems/plus-one/) + +```html +给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。 + +最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。 + +你可以假设除了整数 0 之外,这个整数不会以零开头。 +``` +```html +输入: [1,2,3] +输出: [1,2,4] +解释: 输入数组表示数字 123。 + +输入: [4,3,2,1] +输出: [4,3,2,2] +解释: 输入数组表示数字 4321。 +``` + +正常操作 +加法中常用 +a = x % 10 +b = x / 10 + +```java +class Solution { + public int[] plusOne(int[] digits) { + int length = digits.length; + int[] res = new int[length + 1]; + int carry = 1; + for (int i = length - 1; i >= 0 ; i--) { + int sums = digits[i] + carry; + res[i] = sums % 10; + carry = sums / 10; + } + if (carry == 1) { + res[0] = 1; + return res; + } + return Arrays.copyOfRange(res,0,length); + + } +} +``` + +## 70. 爬楼梯(1407) +[https://leetcode-cn.com/problems/climbing-stairs/](https://leetcode-cn.com/problems/climbing-stairs/) + +```html +假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 + +每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? + +注意:给定 n 是一个正整数。 +``` + +```html +输入: 2 +输出: 2 +解释: 有两种方法可以爬到楼顶。 +1. 1 阶 + 1 阶 +2. 2 阶 + +输入: 3 +输出: 3 +解释: 有三种方法可以爬到楼顶。 +1. 1 阶 + 1 阶 + 1 阶 +2. 1 阶 + 2 阶 +3. 2 阶 + 1 阶 +``` +自底向上 +```java +class Solution { + public int climbStairs(int n) { + if (n <= 2) return n; + int pre2 = 1, pre1 = 2; + for (int i = 3; i <= n; i++) { + int cur = pre2 + pre1; + pre2 = pre1; + pre1 = cur; + } + return pre1; + } +} +``` + +## 88. 合并两个有序数组(1057) +[https://leetcode-cn.com/problems/merge-sorted-array/](https://leetcode-cn.com/problems/merge-sorted-array/) + +```html +给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。 +说明: + +初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 +你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。 + +``` +```html +输入: +nums1 = [1,2,3,0,0,0], m = 3 +nums2 = [2,5,6], n = 3 + +输出: [1,2,2,3,5,6] +``` +三指针 +```java +public void merge(int[] nums1, int m, int[] nums2, int n) { + int index1 = m - 1, index2 = n - 1; + int indexMerge = m + n - 1; + while (index1 >= 0 || index2 >= 0) { + if (index1 < 0) { + nums1[indexMerge--] = nums2[index2--]; + } else if (index2 < 0) { + nums1[indexMerge--] = nums1[index1--]; + } else if (nums1[index1] > nums2[index2]) { + nums1[indexMerge--] = nums1[index1--]; + } else { + nums1[indexMerge--] = nums2[index2--]; + } + } +} + +``` + +## 102. 二叉树的层序遍历(1054) +[https://leetcode-cn.com/problems/binary-tree-level-order-traversal/](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/) +```html +给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。 +``` + +```html + 3 + / \ + 9 20 + / \ + 15 7 + +[ + [3], + [9,20], + [15,7] +] +``` +队列 +```java +class Solution { + public List> levelOrder(TreeNode root) { + List> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()) { + ArrayList list = new ArrayList<>(); + int cnt = queue.size(); + while (cnt-- > 0) { + TreeNode node = queue.poll(); + if (node == null) + continue; + list.add(node.val); + queue.add(node.left); + queue.add(node.right); + } + if (list.size() != 0) + ret.add(list); + } + return ret; + } +} +``` + +## 121. 买卖股票的最佳时机(1491) +[https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/) + +```html +给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 + +如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。 + +注意:你不能在买入股票前卖出股票。 +``` + +```html +输入: [7,1,5,3,6,4] +输出: 5 +解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 + 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票 + +输入: [7,6,4,3,1] +输出: 0 +解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 +``` +```java +class Solution { + public int maxProfit(int[] prices) { + // 边界 + if(prices.length == 0) return 0; + // 长度 + int n = prices.length; + // min + int min = prices[0]; + // max + int max = 0; + for (int i = 1; i < n; i++) { + // 一直找最小的股 + min = prices[i] < min ? prices[i] : min; + // 遍历一圈,存最大的利润 + max = Math.max(max, prices[i] - min); + } + return max; + } +} +``` + +## 169. 多数元素(1096) +[https://leetcode-cn.com/problems/majority-element/](https://leetcode-cn.com/problems/majority-element/) + +```html +给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。 + +你可以假设数组是非空的,并且给定的数组总是存在多数元素。 + +``` +```html +输入: [3,2,3] +输出: 3 +``` + +```java +class Solution { + public int majorityElement(int[] nums) { + Arrays.sort(nums); + return nums[nums.length / 2]; + } +} +``` + +## 198. 打家劫舍(1035) +[https://leetcode-cn.com/problems/house-robber/](https://leetcode-cn.com/problems/house-robber/) + + +```html +你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 + +给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 +``` + +```html +输入:[1,2,3,1] +输出:4 +解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 +  偷窃到的最高金额 = 1 + 3 = 4 。 + +输入:[2,7,9,3,1] +输出:12 +解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 +  偷窃到的最高金额 = 2 + 9 + 1 = 12 。 +``` + +```java +class Solution { + public int rob(int[] nums) { + int pre2 = 0, pre1 = 0; + for (int i = 0; i < nums.length; i++) { + int cur = Math.max(pre2 + nums[i], pre1); + pre2 = pre1; + pre1 = cur; + } + return pre1; + } +} +``` + +## 206. 反转链表(5127) +[https://leetcode-cn.com/problems/reverse-linked-list/](https://leetcode-cn.com/problems/reverse-linked-list/) + +```html +输入: 1->2->3->4->5->NULL +输出: 5->4->3->2->1->NULL +``` + +```java +class Solution { + public ListNode reverseList(ListNode head) { + // 尾递归 + // return reverse(null, head); + // 头插 + ListNode pre = null; + ListNode cur = head; + while (cur != null) { + ListNode next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; + } + private ListNode reverse(ListNode pre, ListNode cur) { + if (cur == null) return pre; + ListNode next = cur.next; + cur.next = pre; + return reverse(cur, next); + } +} +``` + +## 225. 用队列实现栈(4169) +[https://leetcode-cn.com/problems/implement-stack-using-queues/](https://leetcode-cn.com/problems/implement-stack-using-queues/) + +```html +使用队列实现栈的下列操作: + +push(x) -- 元素 x 入栈 +pop() -- 移除栈顶元素 +top() -- 获取栈顶元素 +empty() -- 返回栈是否为空 +``` + +```java +class MyStack { + private Queue queue; + /** Initialize your data structure here. */ + public MyStack() { + queue = new LinkedList<>(); + } + + /** Push element x onto stack. */ + public void push(int x) { + queue.add(x); + // 加完取长度 + int cnt = queue.size(); + // 倒置 + while (cnt-- > 1) { + queue.add(queue.poll()); + } + } + + /** Removes the element on top of the stack and returns that element. */ + public int pop() { + return queue.remove(); + } + + /** Get the top element. */ + public int top() { + return queue.peek(); + } + + /** Returns whether the stack is empty. */ + public boolean empty() { + return queue.isEmpty(); + } +} +``` + +## 283. 移动零(1008) +[https://leetcode-cn.com/problems/move-zeroes/](https://leetcode-cn.com/problems/move-zeroes/) + +```html +输入: [0,1,0,3,12] +输出: [1,3,12,0,0] +``` + +1. 先把不是0的移动左 +2. 最后陆续加0 + +```java +class Solution { + public void moveZeroes(int[] nums) { + int idx = 0; + for (int num : nums) { + if (num != 0) + nums[idx++] = num; + } + while (idx < nums.length) { + nums[idx++] =0; + } + } +} +``` +## 1103. 分糖果 II(1004) +[https://leetcode-cn.com/problems/distribute-candies-to-people/](https://leetcode-cn.com/problems/distribute-candies-to-people/) + +```html +排排坐,分糖果。 + +我们买了一些糖果 candies,打算把它们分给排好队的 n = num_people 个小朋友。 + +给第一个小朋友 1 颗糖果,第二个小朋友 2 颗,依此类推,直到给最后一个小朋友 n 颗糖果。 + +然后,我们再回到队伍的起点,给第一个小朋友 n + 1 颗糖果,第二个小朋友 n + 2 颗,依此类推,直到给最后一个小朋友 2 * n 颗糖果。 + +重复上述过程(每次都比上一次多给出一颗糖果,当到达队伍终点后再次从队伍起点开始),直到我们分完所有的糖果。注意,就算我们手中的剩下糖果数不够(不比前一次发出的糖果多),这些糖果也会全部发给当前的小朋友。 + +返回一个长度为 num_people、元素之和为 candies 的数组,以表示糖果的最终分发情况(即 ans[i] 表示第 i 个小朋友分到的糖果数)。 +``` +```html +输入:candies = 7, num_people = 4 +输出:[1,2,3,1] +解释: +第一次,ans[0] += 1,数组变为 [1,0,0,0]。 +第二次,ans[1] += 2,数组变为 [1,2,0,0]。 +第三次,ans[2] += 3,数组变为 [1,2,3,0]。 +第四次,ans[3] += 1(因为此时只剩下 1 颗糖果),最终数组变为 [1,2,3,1]。 + +输入:candies = 10, num_people = 3 +输出:[5,2,3] +解释: +第一次,ans[0] += 1,数组变为 [1,0,0]。 +第二次,ans[1] += 2,数组变为 [1,2,0]。 +第三次,ans[2] += 3,数组变为 [1,2,3]。 +第四次,ans[0] += 4,最终数组变为 [5,2,3]。 + +``` +```java +class Solution { + public int[] distributeCandies(int candies, int num_people) { + int[] ans = new int[num_people]; + int i; + for (i = 0; candies > 0; i++) { + ans[i % num_people] += i + 1; + candies -= i + 1; + } + ans[(i - 1) % num_people] += candies; + return ans; + } +} +``` + +## 994. 腐烂的橘子(1115) + +[https://leetcode-cn.com/problems/rotting-oranges/](https://leetcode-cn.com/problems/rotting-oranges/) + +```java +class Solution { + public int orangesRotting(int[][] grid) { + // 俺就不判断了,直接上 + int[][] dir = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + Queue> q = new LinkedList<>(); + int m = grid.length, n = grid[0].length; + int cnt = 0; // 表示新鲜的橘子 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) + cnt++; // 新鲜橘子计数 + else if (grid[i][j] == 2) + q.add(new Pair<>(i, j)); // 腐烂橘子的坐标 + } + } + if (cnt == 0 || q.size() == m * n) + return 0; + int step = 0; // 轮数 + while (cnt > 0 && !q.isEmpty()){ + int size = q.size(); + while (size-- > 0) { + Pair p = q.poll(); + int x = p.getKey(), y = p.getValue(); + for (int[] d : dir) { + int newX = x + d[0]; + int newY = y + d[1]; + if (newX < 0 || newX >= m || newY < 0 || newY >= n) { + continue; + } + if (grid[newX][newY] == 1) { + grid[newX][newY] = 2; + q.add(new Pair<>(newX, newY)); + cnt--; + } + } + } + step++; + } + return cnt > 0 ? -1 : step; + } +} +``` + +## 23. 合并K个排序链表(924) +[https://leetcode-cn.com/problems/merge-k-sorted-lists/](https://leetcode-cn.com/problems/merge-k-sorted-lists/) + +```html +输入: +[ + 1->4->5, + 1->3->4, + 2->6 +] +输出: 1->1->2->3->4->4->5->6 +``` + +最小堆 +```java +class Solution { + public ListNode mergeKLists(ListNode[] lists) { + if (lists == null || lists.length == 0) return null; + PriorityQueue queue = new PriorityQueue<>((o1, o2) -> o1.val - o2.val); + ListNode dummy = new ListNode(0); + ListNode p = dummy; + for (ListNode node : lists) { + if (node != null) queue.add(node); + } + while (!queue.isEmpty()) { + p.next = queue.poll(); + p = p.next; + if (p.next != null) queue.add(p.next); + } + return dummy.next; + } +} +``` + +分治 +```java +class Solution { + public ListNode mergeKLists(ListNode[] lists) { + if (lists == null || lists.length == 0) return null; + return merge(lists, 0, lists.length - 1); + } + + private ListNode merge(ListNode[] lists, int left, int right) { + if (left == right) return lists[left]; + int mid = left + (right - left) / 2; + ListNode l1 = merge(lists, left, mid); + ListNode l2 = merge(lists, mid + 1, right); + return mergeTwoLists(l1, l2); + } + + private ListNode mergeTwoLists(ListNode l1, ListNode l2) { + if (l1 == null) return l2; + if (l2 == null) return l1; + if (l1.val < l2.val) { + l1.next = mergeTwoLists(l1.next, l2); + return l1; + } else { + l2.next = mergeTwoLists(l1,l2.next); + return l2; + } + } +} +``` + +## 24. 两两交换链表中的节点(947) +[https://leetcode-cn.com/problems/swap-nodes-in-pairs/](https://leetcode-cn.com/problems/swap-nodes-in-pairs/) +```html +给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 + +你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 +``` + +```html +给定 1->2->3->4, 你应该返回 2->1->4->3. +``` + +```java +class Solution { + public ListNode swapPairs(ListNode head) { + ListNode node = new ListNode(-1); + node.next = head; + ListNode pre = node; + while (pre.next != null && pre.next.next != null) { + ListNode l1 = pre.next, l2 = pre.next.next; + ListNode next = l2.next; + l1.next = next; + l2.next = l1; + pre.next = l2; + pre = l1; + } + return node.next; + } +} +``` + +## 32. 最长有效括号 +[https://leetcode-cn.com/problems/longest-valid-parentheses/](https://leetcode-cn.com/problems/longest-valid-parentheses/) + +```java +class Solution { + public int longestValidParentheses(String s) { + int ans = 0; + int[] dp = new int[s.length()]; + for (int i = 1; i < s.length(); i++){ + if (s.charAt(i) == ')') { + int j = i - dp[i - 1] - 1; + if (j >= 0 && s.charAt(j) == '(') + dp[i] = (i - j + 1) + ((j - 1) >= 0 ? dp[j - 1] : 0); + } + ans = Math.max(ans, dp[i]); + } + return ans; + } +} +``` + +## 34. 在排序数组中查找元素的第一个和最后一个位置(935) +[https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/) + +```html +给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 + +你的算法时间复杂度必须是 O(log n) 级别。 + +如果数组中不存在目标值,返回 [-1, -1]。 + +``` +```html +输入: nums = [5,7,7,8,8,10], target = 8 +输出: [3,4] + +输入: nums = [5,7,7,8,8,10], target = 6 +输出: [-1,-1] +``` + +双指针+二分法 +```java +class Solution { + public int[] searchRange(int[] nums, int target) { + int first = findFirst(nums, target); + int last = findFirst(nums, target + 1) - 1; + if (first == nums.length || nums[first] != target) { + return new int[] {-1, -1}; + } else { + return new int[]{first, Math.max(first, last)}; + } + } + private int findFirst(int[] nums, int target) { + int l = 0, h = nums.length; // h 的初始值和往常不一样 + while (l < h) { + int m = l + ( h - l) / 2; + if (nums[m] >= target) h = m; + else l = m + 1; + } + return l; + } +} +``` + +## 46. 全排列(985) +[https://leetcode-cn.com/problems/permutations/](https://leetcode-cn.com/problems/permutations/) + +```html +给定一个 没有重复 数字的序列,返回其所有可能的全排列。 +``` +```html +输入: [1,2,3] +输出: +[ + [1,2,3], + [1,3,2], + [2,1,3], + [2,3,1], + [3,1,2], + [3,2,1] +] +``` + +dfs +```java +class Solution { + public List> permute(int[] nums) { + List> permutes = new ArrayList<>(); + List permuteList = new ArrayList<>(); + boolean[] hasVisited = new boolean[nums.length]; + backtracking(permuteList, permutes, hasVisited, nums); + return permutes; + } + private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { + if (permuteList.size() == nums.length) { + permutes.add(new ArrayList<>(permuteList)); // 重新构造一个List + return; + } + for (int i = 0; i < visited.length; i++) { + if (visited[i]) continue; + visited[i] = true; + permuteList.add(nums[i]); + backtracking(permuteList, permutes, visited, nums); + permuteList.remove(permuteList.size() - 1); + visited[i] = false; + } + + } +} +``` + +## 56. 合并区间(950) +[https://leetcode-cn.com/problems/merge-intervals/](https://leetcode-cn.com/problems/merge-intervals/) + +```html +给出一个区间的集合,请合并所有重叠的区间。 +``` + +```html +输入: [[1,3],[2,6],[8,10],[15,18]] +输出: [[1,6],[8,10],[15,18]] +解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. + +输入: [[1,4],[4,5]] +输出: [[1,5]] +解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。 +``` + + +```java +class Solution { + public int[][] merge(int[][] intervals) { + if (intervals == null || intervals.length <= 1) return intervals; + Arrays.sort(intervals, (a, b) -> a[0] - b[0]); + List list = new ArrayList<>(); + int i = 0; + int n = intervals.length; + while (i < n) { + int l = intervals[i][0]; + int r = intervals[i][1]; + while (i < n - 1 && r >= intervals[i + 1][0]) { + r = Math.max(r, intervals[i + 1][1]); + i++; + } + list.add(new int[] {l, r}); + i++; + } + return list.toArray(new int[list.size()][2]); + } +} +``` + +## 58. 最后一个单词的长度(966) +[https://leetcode-cn.com/problems/length-of-last-word/](https://leetcode-cn.com/problems/length-of-last-word/) +```html +给定一个仅包含大小写字母和空格 ' ' 的字符串 s,返回其最后一个单词的长度。如果字符串从左向右滚动显示,那么最后一个单词就是最后出现的单词。 + +如果不存在最后一个单词,请返回 0 。 + +说明:一个单词是指仅由字母组成、不包含任何空格字符的 最大子字符串。 +``` + +```html +输入: "Hello World" +输出: 5 +``` + +```java +class Solution { + public int lengthOfLastWord(String s) { + String[] strs = s.split(" "); + if(strs.length == 0) return 0; + return strs[strs.length-1].length(); + } +} +``` + +## 67. 二进制求和(919) +[https://leetcode-cn.com/problems/add-binary/](https://leetcode-cn.com/problems/add-binary/) + +```html +给你两个二进制字符串,返回它们的和(用二进制表示)。 + +输入为 非空 字符串且只包含数字 1 和 0。 +``` + +```html +输入: a = "11", b = "1" +输出: "100" + +输入: a = "1010", b = "1011" +输出: "10101" +``` + +```java +class Solution { + public String addBinary(String a, String b) { + int i = a.length() - 1, j = b.length() - 1, carry = 0; + StringBuilder str = new StringBuilder(); + while (carry == 1 || i >= 0 || j >= 0) { + if (i >= 0 && a.charAt(i--) == '1') carry++; + if (j >= 0 && b.charAt(j--) == '1') carry++; + // 注意这里 + str.append(carry % 2); + carry /= 2; + } + return str.reverse().toString(); + } + +``` + +## 101. 对称二叉树(929) +[https://leetcode-cn.com/problems/symmetric-tree/](https://leetcode-cn.com/problems/symmetric-tree/) + +```html +给定一个二叉树,检查它是否是镜像对称的。 + + 1 + / \ + 2 2 + / \ / \ +3 4 4 3 +``` + +```java +class Solution { + public boolean isSymmetric(TreeNode root) { + if (root == null) return true; + return isSymmetric(root.left, root.right); + } + private boolean isSymmetric(TreeNode t1, TreeNode t2) { + if (t1 == null && t2 == null) return true; + if (t1 == null || t2 == null) return false; + if (t1.val != t2.val) return false; + return isSymmetric(t1.left, t2.right) && isSymmetric(t1.right, t2.left); + } +} +``` + +## 125. 验证回文串(930) +[https://leetcode-cn.com/problems/valid-palindrome/](https://leetcode-cn.com/problems/valid-palindrome/) + +```html +给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。 + +说明:本题中,我们将空字符串定义为有效的回文串。 +``` + +```html +输入: "A man, a plan, a canal: Panama" +输出: true + +输入: "race a car" +输出: false +``` +双指针 +```java +class Solution { + public boolean isPalindrome(String s) { + if (s.equals("")) return true; + s = s.toLowerCase(); + char[] sChar = s.toCharArray(); + int l = 0, r = sChar.length - 1; + while (l <= r) { + if (sChar[l] == sChar[r]) { + l++; + r--; + } else if (!isNormalChar(sChar[l])) { + l++; + } else if (!isNormalChar(sChar[r])) { + r--; + } else { + return false; + } + } + return true; + } + private boolean isNormalChar(char a){ + return Character.isLowerCase(a) || Character.isUpperCase(a) || Character.isDigit(a); + } +} +``` + +## 104. 二叉树的最大深度(857) +[https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/](https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/) + + +```html +给定一个二叉树,找出其最大深度。 + +二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 + +说明: 叶子节点是指没有子节点的节点。 +``` +```html + 3 + / \ + 9 20 + / \ + 15 7 +``` + +```java +class Solution { + public int maxDepth(TreeNode root) { + if (root == null) return 0; + int l = maxDepth(root.left); + int r = maxDepth(root.right); + return 1 + Math.max(l, r); + } +} +``` + +## 136. 只出现一次的数字(890) +[https://leetcode-cn.com/problems/single-number/](https://leetcode-cn.com/problems/single-number/) + +```html +输入: [2,2,1] +输出: 1 +``` +异或 +```java +class Solution { + public int singleNumber(int[] nums) { + int ret = 0; + for (int num : nums) + ret = ret ^ num; + return ret; + } +} +``` + +## 141. 环形链表(865) +[https://leetcode-cn.com/problems/linked-list-cycle/](https://leetcode-cn.com/problems/linked-list-cycle/) + +```html +给定一个链表,判断链表中是否有环。 + +为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。 + +输入:head = [3,2,0,-4], pos = 1 +输出:true +解释:链表中有一个环,其尾部连接到第二个节点。 +``` +快慢指针 +```java +public class Solution { + public boolean hasCycle(ListNode head) { + if (head == null) { + return false; + } + ListNode l1 = head, l2 = head.next; + while (l1 != null && l2 != null && l2.next != null) { + if (l1 == l2) { + return true; + } + l1 = l1.next; + l2 = l2.next.next; + } + return false; + } +} +``` + +## 200. 岛屿数量(853) +[https://leetcode-cn.com/problems/number-of-islands/](https://leetcode-cn.com/problems/number-of-islands/) + +```html +给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 + +岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。 + +此外,你可以假设该网格的四条边均被水包围。 +``` + +```html +输入: +[ +['1','1','1','1','0'], +['1','1','0','1','0'], +['1','1','0','0','0'], +['0','0','0','0','0'] +] +输出: 1 + +输入: +[ +['1','1','0','0','0'], +['1','1','0','0','0'], +['0','0','1','0','0'], +['0','0','0','1','1'] +] +输出: 3 +解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。 + +``` + +dfs +```java +class Solution { + private int m, n; + private int[][] direaction = {{0,1},{0,-1},{1,0},{-1,0}}; + public int numIslands(char[][] grid) { + if (grid == null || grid.length == 0) return 0; + m = grid.length; + n = grid[0].length; + int islandsNum = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] != '0') { + dfs(grid, i, j); + islandsNum++; + } + } + } + return islandsNum; + } + + private void dfs(char[][] grid, int i, int j) { + if (i < 0 || i >= m || j < 0 || j >=n || grid[i][j] == '0') { + return; + } + grid[i][j] = '0'; + for (int[] d : direaction) { + dfs(grid, i + d[0], j + d[1]); + } + } +} +``` + +## 215. 数组中的第K个最大元素(855) +[https://leetcode-cn.com/problems/kth-largest-element-in-an-array/](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/) +```html + +在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。 +``` + +```html +输入: [3,2,1,5,6,4] 和 k = 2 +输出: 5 + +输入: [3,2,3,1,2,4,5,5,6] 和 k = 4 +输出: 4 +``` + +快排 +```java +class Solution { + public int findKthLargest(int[] nums, int k) { + k = nums.length - k; + int l = 0, h = nums.length - 1; + while (l < h) { + int j = partition(nums, l , h); + if (j == k) { + break; + } else if (j < k) { + l = j + 1; + } else { + h = j - 1; + } + } + return nums[k]; + } + + private int partition(int[] arr, int l, int r) { + int pivot = l; + int index = pivot + 1; + for (int i = index; i <= r; i++) { + if (arr[i] < arr[pivot]) { + swap(arr, i, index++); + } + } + swap(arr, pivot, index - 1); + return index - 1; + } + + private void swap(int[] arr, int i, int j) { + int t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; + } +} +``` + +## 409. 最长回文串(876) +[https://leetcode-cn.com/problems/longest-palindrome/](https://leetcode-cn.com/problems/longest-palindrome/) + +```html + +给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。 + +在构造过程中,请注意区分大小写。比如 "Aa" 不能当做一个回文字符串。 + +注意: +假设字符串的长度不会超过 1010。 +``` + +```html +输入: +"abccccdd" + +输出: +7 + +解释: +我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。 +``` + +```java +class Solution { + public int longestPalindrome(String s) { + int[] cnts = new int[256]; + for (char c : s.toCharArray()) { + cnts[c]++; + } + int palindrome = 0; + // 偶数个字母加起来,就算不是偶数个,也拿偶数个,比如5拿4 + for (int cnt : cnts) { + palindrome += (cnt / 2) * 2; + } + // 小于,则说明有个字母或多个字母是奇数个,拿一放中间 + if (palindrome < s.length()) { + palindrome++; + } + return palindrome; + } +} +``` + +## 876. 链表的中间结点(853) +[https://leetcode-cn.com/problems/middle-of-the-linked-list/](https://leetcode-cn.com/problems/middle-of-the-linked-list/) + +```html +给定一个带有头结点 head 的非空单链表,返回链表的中间结点。 + +如果有两个中间结点,则返回第二个中间结点。 +``` + +```html +输入:[1,2,3,4,5] +输出:此列表中的结点 3 (序列化形式:[3,4,5]) +返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。 +注意,我们返回了一个 ListNode 类型的对象 ans,这样: +ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL. + +输入:[1,2,3,4,5,6] +输出:此列表中的结点 4 (序列化形式:[4,5,6]) +由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。 +``` +快慢指针 +```java +class Solution { + public ListNode middleNode(ListNode head) { + ListNode p = head, q = head; + while (q != null && q.next != null) { + q = q.next.next; + p = p.next; + } + return p; + } +} +``` + +## 41. 缺失的第一个正数(751) +[https://leetcode-cn.com/problems/first-missing-positive/](https://leetcode-cn.com/problems/first-missing-positive/) + +```html +给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。 +``` + +``` +输入: [1,2,0] +输出: 3 +``` + +``` +输入: [3,4,-1,1] +输出: 2 +``` +``` +输入: [7,8,9,11,12] +输出: 1 +``` + +怎么会怎么来,排序,接着遍历。 +```java +class Solution { + public int firstMissingPositive(int[] nums) { + int ans = 1; + Arrays.sort(nums); + for (int i = 0; i < nums.length; i++) { + if (nums[i] > ans) break; + if (nums[i] == ans) ans++; + } + return ans; + } +} +``` + +## 62. 不同路径(780) + +``` +html +一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 + +机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 + +问总共有多少条不同的路径? +``` +```html +输入: m = 3, n = 2 +输出: 3 +解释: +从左上角开始,总共有 3 条路径可以到达右下角。 +1. 向右 -> 向右 -> 向下 +2. 向右 -> 向下 -> 向右 +3. 向下 -> 向右 -> 向右 +``` +dp +```java +class Solution { + public int uniquePaths(int m, int n) { + int[] dp = new int[n]; + Arrays.fill(dp, 1); + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[j] += dp[j - 1]; + } + } + return dp[n - 1]; + } +} +``` + +## 151. 翻转字符串里的单词(702) +```html +输入: "the sky is blue" +输出: "blue is sky the" + +输入: "  hello world!  " +输出: "world! hello" +解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。 + +输入: "a good   example" +输出: "example good a" +解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 + +``` + +用了api +```java +class Solution { + public String reverseWords(String s) { + String[] words = s.trim().split(" +"); + Collections.reverse(Arrays.asList(words)); + return String.join(" ", words); + } +} +``` + +## 155. 最小栈(726) +[https://leetcode-cn.com/problems/min-stack/](https://leetcode-cn.com/problems/min-stack/) + +```html +设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 + +push(x) —— 将元素 x 推入栈中。 +pop() —— 删除栈顶的元素。 +top() —— 获取栈顶元素。 +getMin() —— 检索栈中的最小元素。 +``` + +辅助栈 + +```java +class MinStack { + private Stack dataStack; + private Stack minStack; + /** initialize your data structure here. */ + public MinStack() { + dataStack = new Stack<>(); + minStack = new Stack<>(); + } + + public void push(int x) { + dataStack.push(x); + minStack.push(minStack.isEmpty() ? x : Math.min(minStack.peek(), x)); + } + + public void pop() { + dataStack.pop(); + minStack.pop(); + } + + public int top() { + return dataStack.peek(); + } + + public int getMin() { + return min; + } +} +``` + +## 300. 最长上升子序列(718) +[https://leetcode-cn.com/problems/longest-increasing-subsequence/](https://leetcode-cn.com/problems/longest-increasing-subsequence/) + +```html +给定一个无序的整数数组,找到其中最长上升子序列的长度。 +``` + +```html +输入: [10,9,2,5,3,7,101,18] +输出: 4 +解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。 +``` +dp +```java +class Solution { + public int lengthOfLIS(int[] nums) { + if (nums.length == 0) return 0; + int[] dp = new int[nums.length]; + Arrays.fill(dp, 1); + for (int i = 0; i < nums.length; i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + dp[i] = Math.max(dp[i], dp[j] + 1); // 关键这里, + } + } + } + return Arrays.stream(dp).max().orElse(0); + } +} +``` + +## 322. 零钱兑换(755) +[https://leetcode-cn.com/problems/coin-change/](https://leetcode-cn.com/problems/coin-change/) + +```html +给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。 + +``` +```html +输入: coins = [1, 2, 5], amount = 11 +输出: 3 +解释: 11 = 5 + 5 + 1 + +输入: coins = [2], amount = 3 +输出: -1 +``` + +完全背包 +```java +class Solution { + public int coinChange(int[] coins, int amount) { + if (amount == 0) return 0; + int[] dp = new int[amount + 1]; + for (int coin : coins) { + for (int i = coin; i <= amount; i++) { //将逆序遍历改为正序遍历 + if (i == coin) { + dp[i] = 1; + } else if (dp[i] == 0 && dp[i - coin] != 0) { + dp[i] = dp[i - coin] + 1; + + } else if (dp[i - coin] != 0) { + dp[i] = Math.min(dp[i], dp[i - coin] + 1); + } + } + } + return dp[amount] == 0 ? -1 : dp[amount]; + } + +} +``` + +## 1013. 将数组分成和相等的三个部分(798) +[https://leetcode-cn.com/problems/partition-array-into-three-parts-with-equal-sum/](https://leetcode-cn.com/problems/partition-array-into-three-parts-with-equal-sum/) + +```html +给你一个整数数组 A,只有可以将其划分为三个和相等的非空部分时才返回 true,否则返回 false。 + +形式上,如果可以找出索引 i+1 < j 且满足 A[0] + A[1] + ... + A[i] == A[i+1] + A[i+2] + ... + A[j-1] == A[j] + A[j-1] + ... + A[A.length - 1] 就可以将数组三等分。 +``` + +```html +输入:[0,2,1,-6,6,-7,9,1,2,0,1] +输出:true +解释:0 + 2 + 1 = -6 + 6 - 7 + 9 + 1 = 2 + 0 + 1 + +输入:[0,2,1,-6,6,7,9,-1,2,0,1] +输出:false + +输入:[3,3,6,5,-2,2,5,1,-9,4] +输出:true + +``` + +```java +class Solution { + public boolean canThreePartsEqualSum(int[] A) { + int sum = 0; + // 遍历数组求总和 + for (int num : A) { + sum += num; + } + // 数组A的和如果不能被3整除直接返回false + if (sum % 3 != 0) { + return false; + } + // 遍历数组累加,每累加到目标值cnt加1,表示又找到1段 + sum /= 3; + int curSum = 0, cnt = 0; + for (int i = 0; i < A.length; i++) { + curSum += A[i]; + if (curSum == sum) { + cnt++; + curSum = 0; + } + } + // 最后判断是否找到了3段(注意如果目标值是0的话可以大于3段) + return cnt == 3 || (cnt > 3 && sum == 0); + } +} +``` + +## 1160. 拼写单词(705) +[https://leetcode-cn.com/problems/find-words-that-can-be-formed-by-characters/](https://leetcode-cn.com/problems/find-words-that-can-be-formed-by-characters/) + +```html +给你一份『词汇表』(字符串数组) words 和一张『字母表』(字符串) chars。 + +假如你可以用 chars 中的『字母』(字符)拼写出 words 中的某个『单词』(字符串),那么我们就认为你掌握了这个单词。 + +注意:每次拼写(指拼写词汇表中的一个单词)时,chars 中的每个字母都只能用一次。 + +返回词汇表 words 中你掌握的所有单词的 长度之和。 +``` +```html +输入:words = ["cat","bt","hat","tree"], chars = "atach" +输出:6 +解释: +可以形成字符串 "cat" 和 "hat",所以答案是 3 + 3 = 6。 + +输入:words = ["hello","world","leetcode"], chars = "welldonehoneyr" +输出:10 +解释: +可以形成字符串 "hello" 和 "world",所以答案是 5 + 5 = 10。 +``` + +类似于map的数组即可。双map +```java +class Solution { + public int countCharacters(String[] words, String chars) { + int[] hash = new int[26]; + for (char ch : chars.toCharArray()) { + hash[ch - 'a']++; + } + int[] tmp = new int[26]; + int len = 0; + for (String word : words) { + Arrays.fill(tmp, 0); + boolean flag = true; + for (char ch : word.toCharArray()) { + tmp[ch - 'a']++; + if (tmp[ch - 'a'] > hash[ch - 'a']) + flag = false; + } + len += flag ? word.length() : 0; + } + return len; + } +} +``` + +## 78. 子集(633) +[https://leetcode-cn.com/problems/subsets/](https://leetcode-cn.com/problems/subsets/) + +```html +给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 + +说明:解集不能包含重复的子集。 +``` + +```html +输入: nums = [1,2,3] +输出: +[ + [3], +  [1], +  [2], +  [1,2,3], +  [1,3], +  [2,3], +  [1,2], +  [] +] +``` + +```java +class Solution { + public List> subsets(int[] nums) { + List> subsets = new ArrayList<>(); + List tempSubset = new ArrayList<>(); + for (int size = 0; size <= nums.length; size++) { + backtracking(0, tempSubset, subsets, size, nums); // 不同的子集大小 + } + return subsets; + } + + private void backtracking(int start, List tempSubset, List> subsets, + final int size, final int[] nums) { + + if (tempSubset.size() == size) { + subsets.add(new ArrayList<>(tempSubset)); + return; + } + for (int i = start; i < nums.length; i++) { + tempSubset.add(nums[i]); + backtracking(i + 1, tempSubset, subsets, size, nums); + tempSubset.remove(tempSubset.size() - 1); + } + } + +} +``` + +## 83. 删除排序链表中的重复元素(603) +[https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/) + +```html +给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。 +``` + +```html +输入: 1->1->2 +输出: 1->2 + +输入: 1->1->2->3->3 +输出: 1->2->3 +``` + +```java +class Solution { + public ListNode deleteDuplicates(ListNode head) { + if (head == null || head.next == null) return head; + head.next = deleteDuplicates(head.next); + return head.val == head.next.val ? head.next : head; + } +} +``` + +## 94. 二叉树的中序遍历(683) +[https://leetcode-cn.com/problems/binary-tree-inorder-traversal/](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/) + +```html +给定一个二叉树,返回它的中序 遍历。 +``` +```html +输入: [1,null,2,3] + 1 + \ + 2 + / + 3 + +输出: [1,3,2] +``` +栈 +```java +class Solution { + public List inorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + if (root == null) return ret; + Stack stack = new Stack<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + while (cur != null) { + stack.push(cur); + cur = cur.left; // 遍历到做左 + } + TreeNode node = stack.pop(); // 从下往上弹 + ret.add(node.val); + cur = node.right; // 弹完遍历右 + } + return ret; + } +} +``` +## 100. 相同的树(652) +[https://leetcode-cn.com/problems/same-tree/](https://leetcode-cn.com/problems/same-tree/) + +```html +给定两个二叉树,编写一个函数来检验它们是否相同。 + +如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。 +``` + +```html +输入: 1 1 + / \ / \ + 2 3 2 3 + + [1,2,3], [1,2,3] + +输出: true + +输入: 1 1 + / \ + 2 2 + + [1,2], [1,null,2] + +输出: false + +``` +```java +class Solution { + public boolean isSameTree(TreeNode p, TreeNode q) { + if(p == null && q == null) return true; + if(p == null || q == null) return false; + if(p.val != q.val) return false; + return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); + } +} +``` + +## 118. 杨辉三角(642) +[https://leetcode-cn.com/problems/pascals-triangle/](https://leetcode-cn.com/problems/pascals-triangle/) + +```html +输入: 5 +输出: +[ + [1], + [1,1], + [1,2,1], + [1,3,3,1], + [1,4,6,4,1] +] +``` + +```java +class Solution { + public List> generate(int numRows) { + List> ans = new ArrayList<>(); + for(int i = 0; i < numRows; i++) { + List curRow = new ArrayList<>(); + for(int j = 0; j <= i; j++) { + if(j == 0 || j == i) { + curRow.add(1); + continue; + } + if(i == 0 || i == 1) { + continue; + } + List preRow = ans.get(i - 1); + int value = preRow.get(j - 1) + preRow.get(j); + curRow.add(value); + } + ans.add(curRow); + } + return ans; + } +} +``` + +## 160. 相交链表(669) +[https://leetcode-cn.com/problems/intersection-of-two-linked-lists/](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/) + +双指针,A走完,走B,B走完,走A +```java +public class Solution { + public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + ListNode l1 = headA, l2 = headB; + while (l1 != l2) { + l1 = (l1 == null) ? headB : l1.next; + l2 = (l2 == null) ? headA : l2.next; + } + return l1; + } +} +``` + +## 202. 快乐数(668) +[https://leetcode-cn.com/problems/happy-number/](https://leetcode-cn.com/problems/happy-number/) + +```html +编写一个算法来判断一个数 n 是不是快乐数。 + +「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为  1,那么这个数就是快乐数。 + +如果 n 是快乐数就返回 True ;不是,则返回 False 。 + +``` + +```html +输入:19 +输出:true +解释: +12 + 92 = 82 +82 + 22 = 68 +62 + 82 = 100 +12 + 02 + 02 = 1 +``` + +```java +class Solution { + public boolean isHappy(int n) { + if(n == 1) return true; + HashSet set = new HashSet<>(); + while(2 > 1) { + int sum = 0; + while (n > 0) { + sum += (n % 10) *(n % 10); + n /= 10; + } + if(sum == 1) return true; + if(!set.add(sum)) return false; + n = sum; + } + } +} +``` + +## 234. 回文链表(624) +[https://leetcode-cn.com/problems/palindrome-linked-list/](https://leetcode-cn.com/problems/palindrome-linked-list/) + +请判断一个链表是否为回文链表。 + +```html +输入: 1->2 +输出: false +输入: 1->2->2->1 +输出: true +``` + +```java +class Solution { + public boolean isPalindrome(ListNode head) { + if(head == null || head.next == null) return true; + // 找中点 + ListNode slow = head, fast = head.next; + while(fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + if(fast != null) slow = slow.next; + // cut + cut(head, slow); + // 比较 + return isEqual(head, reverse(slow)); + + } + + public void cut (ListNode head, ListNode cutNode) { + ListNode node = head; + while(node.next != cutNode) { + node = node.next; + } + node.next = null; + } + + public ListNode reverse(ListNode head) { + ListNode pre = null; + ListNode cur = head; + while(cur != null) { + ListNode nextNode = cur.next; + cur.next = pre; + pre = cur; + cur = nextNode; + } + return pre; + } + + public boolean isEqual(ListNode l1, ListNode l2) { + while(l1 != null && l2 != null) { + if(l1.val != l2.val) return false; + l1 = l1.next; + l2 = l2.next; + } + return true; + } +} +``` + +## 344. 反转字符串(660) +[https://leetcode-cn.com/problems/reverse-string/](https://leetcode-cn.com/problems/reverse-string/) + +```html +编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 + +不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 + +你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。 + +``` +```html +输入:["h","e","l","l","o"] +输出:["o","l","l","e","h"] +输入:["H","a","n","n","a","h"] +输出:["h","a","n","n","a","H"] +``` +双指针 + +```java +class Solution { + public void reverseString(char[] s) { + int p1 = 0, p2 = s.length - 1; + while(p1 < p2 ){ + swap(s, p1++, p2--); + } + } + public void swap(char[] s, int i, int j) { + char temp = s[i]; + s[i] = s[j]; + s[j] = temp; + } +} +``` + +## 695. 岛屿的最大面积(648) +[https://leetcode-cn.com/problems/max-area-of-island/](https://leetcode-cn.com/problems/max-area-of-island/) + +```html +给定一个包含了一些 0 和 1 的非空二维数组 grid 。 + +一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。 + +找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。) +``` + +```html +[[0,0,1,0,0,0,0,1,0,0,0,0,0], + [0,0,0,0,0,0,0,1,1,1,0,0,0], + [0,1,1,0,1,0,0,0,0,0,0,0,0], + [0,1,0,0,1,1,0,0,1,0,1,0,0], + [0,1,0,0,1,1,0,0,1,1,1,0,0], + [0,0,0,0,0,0,0,0,0,0,1,0,0], + [0,0,0,0,0,0,0,1,1,1,0,0,0], + [0,0,0,0,0,0,0,1,1,0,0,0,0]] + 对于上面这个给定矩阵应返回 6。注意答案不应该是 11 ,因为岛屿只能包含水平或垂直的四个方向的 1 。 +``` + +```java +class Solution { + private int m, n; + private int[][] direaction = {{0,1},{0,-1},{1,0},{-1,0}}; + public int maxAreaOfIsland(int[][] grid) { + if(grid == null || grid.length == 0) return 0; + m = grid.length; + n = grid[0].length; + int maxArea = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + maxArea = Math.max(maxArea, dfs(grid, i, j)); + } + } + return maxArea; + } + private int dfs(int[][] grid, int r, int c) { + if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 0) { + return 0; + } + grid[r][c] = 0; + int area = 1; + for (int[] d : direaction) { + area += dfs(grid, r + d[0], c + d[1]); + } + return area; + } +} +``` +## 739. 每日温度(698) +[https://leetcode-cn.com/problems/daily-temperatures/](https://leetcode-cn.com/problems/daily-temperatures/) + +```html +请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。 + +例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。 + +提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。 +``` +递减栈 +```java +class Solution { + public int[] dailyTemperatures(int[] T) { + Stack stack = new Stack<>(); + int[] res = new int[T.length]; + for (int i = 0; i < T.length; i++) { + while (!stack.isEmpty() && T[i] > T[stack.peek()]) { + int t = stack.pop(); + res[t] = i - t; + } + stack.push(i); + } + return res; + } +} +``` + +## 39. 组合总和(582) +[https://leetcode-cn.com/problems/combination-sum/](https://leetcode-cn.com/problems/combination-sum/) + +```html +给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 + +candidates 中的数字可以无限制重复被选取。 + +``` + +```html +输入: candidates = [2,3,6,7], target = 7, +所求解集为: +[ + [7], + [2,2,3] +] + +输入: candidates = [2,3,6,7], target = 7, +所求解集为: +[ + [7], + [2,2,3] +] + +``` +```java +class Solution { + public List> combinationSum(int[] candidates, int target) { + List> combinations = new ArrayList<>(); + backtracking(new ArrayList<>(), combinations, 0, target, candidates); + return combinations; + } + + private void backtracking(List tempCombination, List> combinations, + int start, int target, final int[] candidates) { + + if (target == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + for (int i = start; i < candidates.length; i++) { + if (candidates[i] <= target) { + tempCombination.add(candidates[i]); + backtracking(tempCombination, combinations, i, target - candidates[i], candidates); + tempCombination.remove(tempCombination.size() - 1); + } + } + } +} +``` + +## 75. 颜色分类(584) +[https://leetcode-cn.com/problems/sort-colors/](https://leetcode-cn.com/problems/sort-colors/) +给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。 + +此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 +``` +```html +输入: [2,0,2,1,1,0] +输出: [0,0,1,1,2,2] +``` +zero和two作为双指针 +```java +class Solution { + public void sortColors(int[] nums) { + int zero = -1, one = 0, two = nums.length; + while (one < two) { + if (nums[one] == 0) { + swap(nums, ++zero, one++); + } else if (nums[one] == 2){ + swap(nums, --two, one); + } else { + ++one; + } + } + } + private void swap(int[] a, int i, int j) { + int t = a[i]; + a[i] = a[j]; + a[j] = t; + } +} +``` + +## 111. 二叉树的最小深度(594) +[https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/](https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/) + +```html + +给定一个二叉树,找出其最小深度。 + +最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 + +说明: 叶子节点是指没有子节点的节点。 + + 3 + / \ + 9 20 + / \ + 15 7 + +2 +``` +```java +class Solution { + public int minDepth(TreeNode root) { + if (root == null) return 0; + int l = minDepth(root.left); + int r = minDepth(root.right); + if(l == 0 || r == 0) return l + r + 1; + return Math.min(l, r) + 1; + } +} +``` + +## 120. 三角形最小路径和(523) +[https://leetcode-cn.com/problems/triangle/](https://leetcode-cn.com/problems/triangle/) + +```html +给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。 + +相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。 +``` + +```html +[ + [2], + [3,4], + [6,5,7], + [4,1,8,3] +] +``` + +dp + +```java +class Solution { + public int minimumTotal(List> triangle) { + if(triangle.size() == 0) return 0; + int row = triangle.size(); + int[][] dp = new int[row][triangle.get(row - 1).size()]; + // 初始化 + for(int i = 0; i < row; i++) { + for (int j =0; j < triangle.get(i).size(); j++) { + dp[i][j] = triangle.get(i).get(j); + } + } + // 从下往上, 初始化最后一行 + for (int i = 0; i < triangle.get(row - 1).size(); i++) { + dp[row - 1][i] = triangle.get(row - 1).get(i); + } + // 动态规划 + for (int i = row - 2; i >= 0; i--) { + for (int j = 0; j < triangle.get(i).size(); j++) { + dp[i][j] = Math.min(dp[i+1][j], dp[i+1][j+1]) + triangle.get(i).get(j); + } + } + return dp[0][0]; + } +} +``` + +## 144. 二叉树的前序遍历(510) +[https://leetcode-cn.com/problems/binary-tree-preorder-traversal/](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/) + +```html +输入: [1,null,2,3] + 1 + \ + 2 + / + 3 + +输出: [1,2,3] +``` + +```java +class Solution { + public List preorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode node = stack.pop(); + if (node == null) continue; + ret.add(node.val); + stack.push(node.right); + stack.push(node.left); + } + return ret; + } +} +``` + +## 145. 二叉树的后序遍历(495) +[https://leetcode-cn.com/problems/binary-tree-postorder-traversal/](https://leetcode-cn.com/problems/binary-tree-postorder-traversal/) + +```html +输入: [1,null,2,3] + 1 + \ + 2 + / + 3 + +输出: [3,2,1] +``` + +```java +class Solution { + public List postorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode node = stack.pop(); + if (node == null) continue; + ret.add(node.val); + stack.push(node.left); + stack.push(node.right); + } + Collections.reverse(ret); + return ret; + } +} +``` + +## 152. 乘积最大子数组(541) +[https://leetcode-cn.com/problems/maximum-product-subarray/](https://leetcode-cn.com/problems/maximum-product-subarray/) + +```html +给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。 +``` + +```html +输入: [2,3,-2,4] +输出: 6 +解释: 子数组 [2,3] 有最大乘积 6。 + +输入: [-2,0,-1] +输出: 0 +解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。 +``` +dp +```java +class Solution { + public int maxProduct(int[] nums) { + if (nums.length == 0) return 0; + int ans = Integer.MIN_VALUE; + int[] dpMax = new int[nums.length + 1]; + int[] dpMin = new int[nums.length + 1]; + dpMax[0] = 1; + dpMin[0] = 1; + for (int i = 1; i <= nums.length; i++) { + if (nums[i-1] < 0) { + int temp = dpMax[i-1]; + dpMax[i-1] = dpMin[i-1]; + dpMin[i-1] = temp; + } + dpMax[i] = Math.max(dpMax[i-1]*nums[i-1], nums[i-1]); + dpMin[i] = Math.min(dpMin[i-1]*nums[i-1], nums[i-1]); + ans = Math.max(ans, dpMax[i]); + } + return ans; + } +} +``` + +## 167. 两数之和 II - 输入有序数组(559) +[https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/) + +```html + +给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。 + +函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。 + +说明: + +返回的下标值(index1 和 index2)不是从零开始的。 +你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。 + +``` + +```html +输入: numbers = [2, 7, 11, 15], target = 9 +输出: [1,2] +解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。 +``` + +```java +class Solution { + public int[] twoSum(int[] numbers, int target) { + if (numbers == null) return null; + // 双指针 + int p1 = 0, p2 = numbers.length - 1; + while (p1 < p2) { + int sum = numbers[p1] + numbers[p2]; + if (sum == target) return new int[]{p1+1, p2+1}; + else if (sum < target) p1++; + else p2--; + } + return null; + } +} +``` + +## 189. 旋转数组(517) +[https://leetcode-cn.com/problems/rotate-array/](https://leetcode-cn.com/problems/rotate-array/) + +给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。 + +```html +输入: [1,2,3,4,5,6,7] 和 k = 3 +输出: [5,6,7,1,2,3,4] +解释: +向右旋转 1 步: [7,1,2,3,4,5,6] +向右旋转 2 步: [6,7,1,2,3,4,5] +向右旋转 3 步: [5,6,7,1,2,3,4] + +输入: [-1,-100,3,99] 和 k = 2 +输出: [3,99,-1,-100] +解释: +向右旋转 1 步: [99,-1,-100,3] +向右旋转 2 步: [3,99,-1,-100] + +``` + +```java +class Solution { + public void rotate(int[] nums, int k) { + int n = nums.length; + k %= n; + reverse(nums, 0, n - 1); + reverse(nums, 0, k - 1); + reverse(nums, k, n - 1); + } + private void reverse(int[] nums, int start, int end) { + while(start < end) { + int temp = nums[start]; + nums[start++] = nums[end]; + nums[end--] = temp; + } + } +} +``` + +## 226. 翻转二叉树(582) +[https://leetcode-cn.com/problems/invert-binary-tree/](https://leetcode-cn.com/problems/invert-binary-tree/) + +```html + 4 + / \ + 2 7 + / \ / \ +1 3 6 9 + + 4 + / \ + 7 2 + / \ / \ +9 6 3 1 +``` + +```java +class Solution { + public TreeNode invertTree(TreeNode root) { + if (root == null) return null; + TreeNode left = root.left; + root.left = invertTree(root.right); + root.right = invertTree(left); + return root; + } +} +``` + +## 242. 有效的字母异位词(513) +[https://leetcode-cn.com/problems/valid-anagram/](https://leetcode-cn.com/problems/valid-anagram/) + +```html +给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 +``` + +```html +输入: s = "anagram", t = "nagaram" +输出: true + +输入: s = "rat", t = "car" +输出: false +``` + +```java +class Solution { + public boolean isAnagram(String s, String t) { + int[] cnts = new int[26]; + for (char c : s.toCharArray()) { + cnts[c - 'a']++; + } + for (char c : t.toCharArray()) { + cnts[c - 'a']--; + } + for (int c : cnts) { + if (c != 0) return false; + } + return true; + } +} +``` + +## 287. 寻找重复数(515) +[https://leetcode-cn.com/problems/find-the-duplicate-number/](https://leetcode-cn.com/problems/find-the-duplicate-number/) + +```html +给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。 + +输入: [1,3,4,2,2] +输出: 2 + +输入: [3,1,3,4,2] +输出: 3 + +``` + +快慢指针 +```java +class Solution { + public int findDuplicate(int[] nums) { + int slow = nums[0], fast = nums[nums[0]]; + while (slow != fast) { + slow = nums[slow]; + fast = nums[nums[fast]]; + } + fast = 0; + while (slow != fast) { + slow = nums[slow]; + fast = nums[fast]; + } + return slow; + } +} +``` + +## 392. 判断子序列(509) +[https://leetcode-cn.com/problems/is-subsequence/](https://leetcode-cn.com/problems/is-subsequence/) + +```html +给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 + +你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。 + +字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。 + +s = "abc", t = "ahbgdc" + +返回 true. + +s = "axc", t = "ahbgdc" + +返回 false. + +``` +```java +class Solution { + public boolean isSubsequence(String s, String t) { + // 这里用到了String到indexof + int inx = -1; + for (char c : s.toCharArray()) { + inx = t.indexOf(c, inx + 1); + if (inx == -1) return false; + } + return true; + } +} +``` + +## 445. 两数相加 II(562) +[https://leetcode-cn.com/problems/add-two-numbers-ii/](https://leetcode-cn.com/problems/add-two-numbers-ii/) + +```html +给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。 + +你可以假设除了数字 0 之外,这两个数字都不会以零开头。 + +输入:(7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) +输出:7 -> 8 -> 0 -> 7 + +``` + +双栈 +```java +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + Stack l1Stack = buildStack(l1); + Stack l2Stack = buildStack(l2); + ListNode head = new ListNode(-1); + int carray = 0; + while (!l1Stack.isEmpty() || !l2Stack.isEmpty() || carray != 0) { + int x = l1Stack.isEmpty() ? 0 : l1Stack.pop(); + int y = l2Stack.isEmpty() ? 0 : l2Stack.pop(); + int sum = x + y + carray; + ListNode node = new ListNode(sum % 10); + node.next = head.next; + head.next = node; + carray = sum / 10; + } + return head.next; + } + + private Stack buildStack(ListNode l) { + Stack stack = new Stack<>(); + while (l != null) { + stack.push(l.val); + l = l.next; + } + return stack; + } +} +``` + +## 836. 矩形重叠(570) +[https://leetcode-cn.com/problems/rectangle-overlap/](https://leetcode-cn.com/problems/rectangle-overlap/) + +```html +矩形以列表 [x1, y1, x2, y2] 的形式表示,其中 (x1, y1) 为左下角的坐标,(x2, y2) 是右上角的坐标。 + +如果相交的面积为正,则称两矩形重叠。需要明确的是,只在角或边接触的两个矩形不构成重叠。 + +给出两个矩形,判断它们是否重叠并返回结果。 + +矩形以列表 [x1, y1, x2, y2] 的形式表示,其中 (x1, y1) 为左下角的坐标,(x2, y2) 是右上角的坐标。 + +如果相交的面积为正,则称两矩形重叠。需要明确的是,只在角或边接触的两个矩形不构成重叠。 + +给出两个矩形,判断它们是否重叠并返回结果。 + +输入:rec1 = [0,0,2,2], rec2 = [1,1,3,3] +输出:true + +输入:rec1 = [0,0,1,1], rec2 = [1,0,2,1] +输出:false +``` + +```java +class Solution { + public boolean isRectangleOverlap(int[] rec1, int[] rec2) { + if (rec2[1] >= rec1[3] || rec1[1] >= rec2[3]) { + return false; + } + if (rec1[0] >= rec2[2] || rec1[2] <= rec2[0]) { + return false; + } + return true; + } +} +``` +## 914. 卡牌分组(506) +[https://leetcode-cn.com/problems/x-of-a-kind-in-a-deck-of-cards/](https://leetcode-cn.com/problems/x-of-a-kind-in-a-deck-of-cards/) + +```html +给定一副牌,每张牌上都写着一个整数。 + +此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组: + +每组都有 X 张牌。 +组内所有的牌上都写着相同的整数。 +仅当你可选的 X >= 2 时返回 true。 + +输入:[1,2,3,4,4,3,2,1] +输出:true +解释:可行的分组是 [1,1],[2,2],[3,3],[4,4] + +输入:[1,1,1,2,2,2,3,3] +输出:false +解释:没有满足要求的分组。 + +``` +```java +class Solution { + public boolean hasGroupsSizeX(int[] deck) { + // hash + HashMap map = new HashMap<>(); + for(int num : deck) { + if(map.containsKey(num)) { + map.put(num, map.get(num) + 1); + } else { + map.put(num, 1); + } + } + // 最大公约数 + int t = 0; + for(int a : map.values()) { + t = gcd(t, a); + } + return t >= 2; + } + + // 最大公约数 + private int gcd(int a, int b) { + return b == 0 ? a : gcd(b, a % b); + } +} +``` + +## 1071. 字符串的最大公因子(583) +[https://leetcode-cn.com/problems/greatest-common-divisor-of-strings/](https://leetcode-cn.com/problems/greatest-common-divisor-of-strings/) + +```html +对于字符串 S 和 T,只有在 S = T + ... + T(T 与自身连接 1 次或多次)时,我们才认定 “T 能除尽 S”。 + +返回最长字符串 X,要求满足 X 能除尽 str1 且 X 能除尽 str2。 + +输入:str1 = "ABCABC", str2 = "ABC" +输出:"ABC" + +输入:str1 = "ABABAB", str2 = "ABAB" +输出:"AB" + +输入:str1 = "LEET", str2 = "CODE" +输出:"" +``` + +```java +class Solution { + public String gcdOfStrings(String str1, String str2) { + if (!(str1 + str2).equals(str2 + str1)) { + return ""; + } + return str2.substring(0, gcd(str1.length(), str2.length())); + } + + private int gcd(int a, int b) { + return b == 0 ? a : gcd(b, a % b); + } +} +``` + +## 31. 下一个排列(588) +```java + //源于离散数学及其应用的算法:(以3 4 5 2 1 为例) + //从后往前寻找第一次出现的正序对:(找到 4,5) + //之后因为从5 开始都是逆序,所以把他们反转就是正序:3 4 1 2 5 + //之后4 的位置应该是:在它之后的,比他大的最小值(5) + //交换这两个值:得到 3 5 1 2 4 + // 对于初始即为逆序的序列,将在反转步骤直接完成 +class Solution { + public void nextPermutation(int[] nums) { + int len = nums.length; + if (len < 2) return; + int i = len - 1; + while (i > 0 && nums[i - 1] >= nums[i]) + i--; // 从后向前找第一个正序,这里最后i指向的是逆序起始位置 + reverse(nums, i, len - 1); // 翻转后面的逆序区域,使其变为正序 + if (i == 0) return; + int j = i - 1; + while(i < len && nums[j] >= nums[i]) + i++; // 找到第一个比nums[j]大的元素,交换即可 + // 交换 + swap(nums, i, j); + } + private void reverse(int[] nums, int i, int j) { + while (i < j) { + swap(nums, i++, j--); + } + } + + private void swap(int[] nums, int i, int j){ + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; + } +} +``` + +## 36. 有效的数独(474) + +[https://leetcode-cn.com/problems/valid-sudoku/](https://leetcode-cn.com/problems/valid-sudoku/) + +```java +class Solution { + public boolean isValidSudoku(char[][] board) { + // 记录某行,某位数字是否已经被摆放 + boolean[][] row = new boolean[9][9]; + // 记录某列,某位数字是否已经被摆放 + boolean[][] col = new boolean[9][9]; + // 记录某3x3宫格内,某位数字是否已经被摆放 + boolean[][] block = new boolean[9][9]; + + for (int i = 0; i < 9; i++) { + for (int j = 0; j < 9; j++) { + if (board[i][j] != '.') { + int num = board[i][j] - '1'; + int blockIdx = i / 3 * 3 + j / 3; + if (row[i][num] || col[j][num] || block[blockIdx][num]) { + return false; + } else { + row[i][num] = true; + col[j][num] = true; + block[blockIdx][num] = true; + } + } + } + } + return true; + } +} +``` + +## 40. 组合总和 II(401) +[https://leetcode-cn.com/problems/combination-sum-ii/](https://leetcode-cn.com/problems/combination-sum-ii/) +```html + +给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 + +candidates 中的每个数字在每个组合中只能使用一次。 + +说明: + +所有数字(包括目标数)都是正整数。 +解集不能包含重复的组合。 + +``` + +```html +输入: candidates = [10,1,2,7,6,1,5], target = 8, +所求解集为: +[ + [1, 7], + [1, 2, 5], + [2, 6], + [1, 1, 6] +] +``` +```java +class Solution { + public List> combinationSum2(int[] candidates, int target) { + List> combinations = new ArrayList<>(); + Arrays.sort(candidates); + backtracking(new ArrayList<>(), combinations, new boolean[candidates.length], 0, target, candidates); + return combinations; + } + + private void backtracking(List tempCombination, List> combinations, + boolean[] hasVisited, int start, int target, final int[] candidates) { + + if (target == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + for (int i = start; i < candidates.length; i++) { + if (i != 0 && candidates[i] == candidates[i - 1] && !hasVisited[i - 1]) { + continue; + } + if (candidates[i] <= target) { + tempCombination.add(candidates[i]); + hasVisited[i] = true; + backtracking(tempCombination, combinations, hasVisited, i + 1, target - candidates[i], candidates); + hasVisited[i] = false; + tempCombination.remove(tempCombination.size() - 1); + } + } + } + +} +``` + +## 47. 全排列 II(429) +[https://leetcode-cn.com/problems/permutations-ii/](https://leetcode-cn.com/problems/permutations-ii/) + +```html +给定一个可包含重复数字的序列,返回所有不重复的全排列。 +``` + +```html +输入: [1,1,2] +输出: +[ + [1,1,2], + [1,2,1], + [2,1,1] +] +``` + +```java +class Solution { + public List> permuteUnique(int[] nums) { + List> permutes = new ArrayList<>(); + List permuteList = new ArrayList<>(); + Arrays.sort(nums); // 排序 + boolean[] hasVisited = new boolean[nums.length]; + backtracking(permuteList, permutes, hasVisited, nums); + return permutes; + } + private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { + if (permuteList.size() == nums.length) { + permutes.add(new ArrayList<>(permuteList)); + return; + } + for (int i = 0; i < visited.length; i++) { + if (i != 0 && nums[i] == nums[i -1] && !visited[i - 1]) { + continue; // 防止重复 + } + if (visited[i]) continue; + visited[i] = true; + permuteList.add(nums[i]); + backtracking(permuteList, permutes, visited, nums); + permuteList.remove(permuteList.size() - 1); + visited[i] = false; + } + } +} +``` + +## 72. 编辑距离(496) +```html +给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 + +你可以对一个单词进行如下三种操作: + +插入一个字符 +删除一个字符 +替换一个字符 + +输入:word1 = "horse", word2 = "ros" +输出:3 +解释: +horse -> rorse (将 'h' 替换为 'r') +rorse -> rose (删除 'r') +rose -> ros (删除 'e') + +``` + +```java +class Solution { + public int minDistance(String word1, String word2) { + if (word1 == null || word2 == null) { + return 0; + } + int m = word1.length(), n = word2.length(); + int[][] dp = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + dp[i][0] = i; + } + for (int i = 1; i <= n; i++) { + dp[0][i] = i; + } + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1; + } + } + } + return dp[m][n]; + } + +} +``` + +## 79. 单词搜索(420) +[https://leetcode-cn.com/problems/word-search/](https://leetcode-cn.com/problems/word-search/) + +```html +给定一个二维网格和一个单词,找出该单词是否存在于网格中。 + +单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 + +board = +[ + ['A','B','C','E'], + ['S','F','C','S'], + ['A','D','E','E'] +] + +给定 word = "ABCCED", 返回 true +给定 word = "SEE", 返回 true +给定 word = "ABCB", 返回 false + +``` + +```java +class Solution { + private final static int[][] direction = {{1,0},{-1,0},{0,1},{0,-1}}; + private int m; + private int n; + public boolean exist(char[][] board, String word) { + if (word == null || word.length() == 0) return true; + if (board == null || board.length == 0 || board[0].length == 0) return false; + m = board.length; + n = board[0].length; + boolean[][] hasVisited = new boolean[m][n]; + for (int r = 0; r < m; r++) { + for (int c = 0; c < n; c++) { + if (backtracking(0, r, c, hasVisited, board, word)) { + return true; + } + } + } + return false; + } + private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) { + if (curLen == word.length()) return true; + if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != word.charAt(curLen) || visited[r][c]) return false; + visited[r][c] = true; + for (int[] d : direction) { + if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) return true; + } + visited[r][c] = false; + return false; + } +} +``` + +## 91. 解码方法(462) +[https://leetcode-cn.com/problems/decode-ways/](https://leetcode-cn.com/problems/decode-ways/) +```html +一条包含字母 A-Z 的消息通过以下方式进行了编码: +'A' -> 1 +'B' -> 2 +... +'Z' -> 26 + +输入: "12" +输出: 2 +解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。 + +输入: "226" +输出: 3 +解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。 +``` +dp +```java +class Solution { + public int numDecodings(String s) { + if (s == null || s.length() == 0) { + return 0; + } + int n = s.length(); + int[] dp = new int[n + 1]; + dp[0] = 1; + dp[1] = s.charAt(0) == '0' ? 0 : 1; + for (int i = 2; i <= n; i++) { + int one = Integer.valueOf(s.substring(i - 1, i)); + if (one != 0) { + dp[i] += dp[i - 1]; + } + if (s.charAt(i - 2) == '0') continue; + int two = Integer.valueOf(s.substring(i - 2, i)); + if (two <= 26) { + dp[i] += dp[i - 2]; + } + } + return dp[n]; + } +} +``` + +## 110. 平衡二叉树(485) +[https://leetcode-cn.com/problems/balanced-binary-tree/](https://leetcode-cn.com/problems/balanced-binary-tree/) +```html +给定一个二叉树,判断它是否是高度平衡的二叉树。 + +本题中,一棵高度平衡二叉树定义为: + +一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。 + 3 + / \ + 9 20 + / \ + 15 7 + + true +``` + +```java +lass Solution { + private boolean res = true; + public boolean isBalanced(TreeNode root) { + Depth(root); + return res; + } + + private int Depth (TreeNode root) { + if (root == null) return 0; + int l = Depth(root.left); + int r = Depth(root.right); + if (Math.abs(l - r) > 1) res = false;; + return 1 + Math.max(l , r); + } +} +``` + +## 139. 单词拆分(475) +[https://leetcode-cn.com/problems/word-break/](https://leetcode-cn.com/problems/word-break/) +```html +给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 + +说明: + +拆分时可以重复使用字典中的单词。 +你可以假设字典中没有重复的单词。 + +输入: s = "leetcode", wordDict = ["leet", "code"] +输出: true +解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。 + +输入: s = "applepenapple", wordDict = ["apple", "pen"] +输出: true +解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。 +  注意你可以重复使用字典中的单词。 + +来输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] +输出: false +``` +```java +class Solution { + public boolean wordBreak(String s, List wordDict) { + int n = s.length(); + boolean[] dp = new boolean[n + 1]; + dp[0] = true; + for (int i = 1; i <= n; i++) { + for (String word: wordDict) { + // 对物品的迭代应该放在最里层 + int len = word.length(); + if (len <= i && word.equals(s.substring(i - len , i))) { + dp[i] = dp[i] || dp[i - len]; + } + } + } + return dp[n]; + } +} +``` + +## 217. 存在重复元素(471) +[https://leetcode-cn.com/problems/contains-duplicate/](https://leetcode-cn.com/problems/contains-duplicate/) + +```html +给定一个整数数组,判断是否存在重复元素。 + +如果任意一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。 + +输入: [1,2,3,1] +输出: true + +``` + +```java +class Solution { + public boolean containsDuplicate(int[] nums) { + HashSet set = new HashSet<>(); + for (int num : nums) { + if (!set.add(num)) { + return true; + } + } + return false; + } +} +``` + +## 237. 删除链表中的节点(423) +[https://leetcode-cn.com/problems/delete-node-in-a-linked-list/](https://leetcode-cn.com/problems/delete-node-in-a-linked-list/) + +```html +输入: head = [4,5,1,9], node = 5 +输出: [4,1,9] +解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9. + +``` +```java +class Solution { + public void deleteNode(ListNode node) { + node.val = node.next.val; + node.next = node.next.next; + } +} +``` + +## 238. 除自身以外数组的乘积(467) +[https://leetcode-cn.com/problems/product-of-array-except-self/](https://leetcode-cn.com/problems/product-of-array-except-self/) + +```html + +给你一个长度为 n 的整数数组 nums,其中 n > 1,返回输出数组 output ,其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。 +输入: [1,2,3,4] +输出: [24,12,8,6] +``` + +```java +class Solution { + public int[] productExceptSelf(int[] nums) { + int n = nums.length; + int[] products = new int[n]; + Arrays.fill(products, 1); + int left = 1; + for (int i = 1; i < n; i++) { + left *= nums[i - 1]; + products[i] *= left; + } + int right = 1; + for (int i = n - 2; i >= 0; i--) { + right *= nums[i + 1]; + products[i] *= right; + } + return products; + } +} +``` + +## 350. 两个数组的交集 II(402) +[https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/](https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/) + +给定两个数组,编写一个函数来计算它们的交集。 + +```html +输入: nums1 = [1,2,2,1], nums2 = [2,2] +输出: [2,2] + +输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4] +输出: [4,9] + +``` + +```java +class Solution { + public int[] intersect(int[] nums1, int[] nums2) { + ArrayList list1 = new ArrayList<>(); + for (int num : nums1) { + list1.add(num); + } + ArrayList list2 = new ArrayList<>(); + for (int num : nums2) { + if (list1.contains(num)) { + list2.add(num); + list1.remove(num); + } + } + return list2.stream().mapToInt(Integer::valueOf).toArray(); + } +} +``` + +## 461. 汉明距离(411) +[https://leetcode-cn.com/problems/hamming-distance/](https://leetcode-cn.com/problems/hamming-distance/) + +两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。 + +给出两个整数 x 和 y,计算它们之间的汉明距离。 + +```html +输入: x = 1, y = 4 + +输出: 2 + +解释: +1 (0 0 0 1) +4 (0 1 0 0) + ↑ ↑ + +上面的箭头指出了对应二进制位不同的位置。 +``` + +```java +class Solution { + public int hammingDistance(int x, int y) { + int z = x ^ y; + int cnt = 0; + while (z != 0) { + if ((z & 1) == 1) cnt++; + z = z >> 1; + } + return cnt; + } +} +``` + +## 572. 另一个树的子树(426) +[https://leetcode-cn.com/problems/subtree-of-another-tree/](https://leetcode-cn.com/problems/subtree-of-another-tree/) + +```html +给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。s 的一个子树包括 s 的一个节点和这个节点的所有子孙。s 也可以看做它自身的一棵子树。 + 3 + / \ + 4 5 + / \ + 1 2 + + 4 + / \ + 1 2 + +``` +```java + class Solution { + public boolean isSubtree(TreeNode s, TreeNode t) { + if (s == null) return false; + return isSubtreeWithRoot(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t); + } + + private boolean isSubtreeWithRoot(TreeNode s, TreeNode t) { + if (t == null && s == null) return true; + if (t == null || s == null) return false; + if (t.val != s.val) return false; + return isSubtreeWithRoot(s.left, t.left) && isSubtreeWithRoot(s.right, t.right); + } +} +``` + +## 647. 回文子串(409) + +[https://leetcode-cn.com/problems/palindromic-substrings/](https://leetcode-cn.com/problems/palindromic-substrings/) + +```java +class Solution { + private int cnt = 0; + public int countSubstrings(String s) { + for (int i = 0; i < s.length(); i++) { + extendSubstrings(s, i, i); // 奇数长度 + extendSubstrings(s, i, i + 1); // 偶数长度 + } + return cnt; + } + private void extendSubstrings(String s, int start, int end) { + while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) { + start--; + end++; + cnt++; + } + } +} +``` + +## 680. 验证回文字符串 Ⅱ(474) +[https://leetcode-cn.com/problems/valid-palindrome-ii/](https://leetcode-cn.com/problems/valid-palindrome-ii/) +给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。 +```html +输入: "aba" +输出: True + +输入: "abca" +输出: True +解释: 你可以删除c字符。 +``` +```java +class Solution { + public boolean validPalindrome(String s) { + // 普通判断回文串用前后双指针即可,但是,难点在于如果去删除一个元素后的字符串是不是回文串 + // 如果前后指针的元素不相等,此时子串的范围(i+1,j)或(j-1)的俩子串只要任意一个是,则结果是 + // 否则,则不是 + int i =0, j = s.length() - 1; + while(i < j) { + if(s.charAt(i) != s.charAt(j)) { + return isVaild(s, i+1, j) || isVaild(s, i, j-1); + } + i++; + j--; + } + return true; + } + + public boolean isVaild(String s, int i, int j) { + while(i < j) { + if(s.charAt(i) != s.charAt(j)) { + return false; + } + i++; + j--; + } + return true; + } +} +``` + +## 146. LRU缓存机制(496) +[https://leetcode-cn.com/problems/lru-cache/](https://leetcode-cn.com/problems/lru-cache/) + +```java +class LRUCache { + private int cap; + private Map map = new LinkedHashMap<>(); + public LRUCache(int capacity) { + this.cap = capacity; + } + + public int get(int key) { + if (map.containsKey(key)) { + int value = map.get(key); + // 查一次,就将查到到仍在队尾 + map.remove(key); + map.put(key,value); + return value; + } + return -1; + } + + public void put(int key, int value) { + if (map.containsKey(key)) { + map.remove(key); + } else if (map.size() == cap) { + // 满了 + Iterator> iterator = map.entrySet().iterator(); + iterator.next(); + iterator.remove(); + } + map.put(key, value); + } +} + +``` + +## 90. 子集 II(304) +[https://leetcode-cn.com/problems/subsets-ii/](https://leetcode-cn.com/problems/subsets-ii/) + +```html +给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 + +说明:解集不能包含重复的子集。 + +输入: [1,2,2] +输出: +[ + [2], + [1], + [1,2,2], + [2,2], + [1,2], + [] +] +``` + +```java +class Solution { + public List> subsetsWithDup(int[] nums) { + Arrays.sort(nums); + List> subsets = new ArrayList<>(); + List tempSubset = new ArrayList<>(); + boolean[] hasVisited = new boolean[nums.length]; + for (int size = 0; size <= nums.length; size++) { + backtracking(0, tempSubset, subsets, hasVisited, size, nums); // 不同的子集大小 + } + return subsets; + } + + private void backtracking(int start, List tempSubset, List> subsets, boolean[] hasVisited, + final int size, final int[] nums) { + + if (tempSubset.size() == size) { + subsets.add(new ArrayList<>(tempSubset)); + return; + } + for (int i = start; i < nums.length; i++) { + if (i != 0 && nums[i] == nums[i - 1] && !hasVisited[i - 1]) { + continue; + } + tempSubset.add(nums[i]); + hasVisited[i] = true; + backtracking(i + 1, tempSubset, subsets, hasVisited, size, nums); + hasVisited[i] = false; + tempSubset.remove(tempSubset.size() - 1); + } + } + +} +``` + +## 130. 被围绕的区域(328) +[https://leetcode-cn.com/problems/surrounded-regions/](https://leetcode-cn.com/problems/surrounded-regions/) + +给定一个二维的矩阵,包含 'X' 和 'O'(字母 O)。 + +找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。 + +```html +X X X X +X O O X +X X O X +X O X X + +X X X X +X X X X +X X X X +X O X X +``` + +```java +class Solution { + private int[][] direction = {{0,1},{0,-1},{1,0},{-1,0}}; + private int m, n; + public void solve(char[][] board) { + if (board == null || board.length == 0) return; + m = board.length; + n = board[0].length; + for (int i = 0; i < m; i++) { + dfs(board, i, 0); + dfs(board, i, n - 1); + } + for (int i = 0; i < n; i++) { + dfs(board, 0, i); + dfs(board, m - 1, i); + } + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (board[i][j] == 'T') { + board[i][j] = 'O'; + } else if (board[i][j] == 'O') { + board[i][j] = 'X'; + } + } + } + } + + private void dfs(char[][] board, int r, int c) { + if(r<0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') { + return; + } + board[r][c] = 'T'; + for (int[] d : direction) { + dfs(board, r + d[0], c + d[1]); + } + } +} +``` + +## 153. 寻找旋转排序数组中的最小值(316) +[https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/) + +```html +假设按照升序排序的数组在预先未知的某个点上进行了旋转。 + +( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 + +请找出其中最小的元素。 + +你可以假设数组中不存在重复元素。 + +输入: [3,4,5,1,2] +输出: 1 + +输入: [4,5,6,7,0,1,2] +输出: 0 +``` + +```java +class Solution { + public int findMin(int[] nums) { + int l = 0, h = nums.length - 1; + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] <= nums[h]) { + h = m; + } else { + l = m + 1; + } + } + return nums[l]; + } +} +``` + +## 191. 位1的个数(324) +[https://leetcode-cn.com/problems/number-of-1-bits/](https://leetcode-cn.com/problems/number-of-1-bits/) +编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。 +```html +输入:00000000000000000000000000001011 +输出:3 +解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。 + +输入:00000000000000000000000010000000 +输出:1 +解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。 +``` +```java +public class Solution { + // you need to treat n as an unsigned value + public int hammingWeight(int n) { + int ans = 0; + while(n != 0) { + n &= n - 1; + ans++; + } + return ans; + } +} +``` + +## 213. 打家劫舍 II(375) +[https://leetcode-cn.com/problems/house-robber-ii/](https://leetcode-cn.com/problems/house-robber-ii/) +```html +你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 + +给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。 + +输入: [2,3,2] +输出: 3 +解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。 + +输入: [1,2,3,1] +输出: 4 +解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 +  偷窃到的最高金额 = 1 + 3 = 4 。 + +``` +```java +class Solution { + public int rob(int[] nums) { + if (nums == null || nums.length == 0) return 0; + int n = nums.length; + if (n == 1) return nums[0]; + return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1)); + } + + private int rob(int[] nums, int first, int last) { + int pre2 = 0, pre1 = 0; + for (int i = first; i <= last; i++) { + int cur = Math.max(pre1, pre2 + nums[i]); + pre2 = pre1; + pre1 = cur; + } + return pre1; + } +} +``` + +## 219. 存在重复元素 II(340) +[https://leetcode-cn.com/problems/contains-duplicate-ii/](https://leetcode-cn.com/problems/contains-duplicate-ii/) + +```html +输入: nums = [1,2,3,1], k = 3 +输出: true + +输入: nums = [1,0,1,1], k = 1 +输出: true +``` + +```java +class Solution { + public boolean containsNearbyDuplicate(int[] nums, int k) { + HashSet set = new HashSet<>(); + for (int i = 0; i < nums.length; i++) { + if(set.contains(nums[i])) { + return true; + } + set.add(nums[i]); + if (set.size() > k) { + set.remove(nums[i - k]); + } + } + return false; + } +} +``` + +## 230. 二叉搜索树中第K小的元素(301) +[https://leetcode-cn.com/problems/kth-smallest-element-in-a-bst/](https://leetcode-cn.com/problems/kth-smallest-element-in-a-bst/) + +给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。 + +说明: +你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。 + +```html +输入: root = [3,1,4,null,2], k = 1 + 3 + / \ + 1 4 + \ + 2 +输出: 1 +``` + +```java +class Solution { + public int kthSmallest(TreeNode root, int k) { + int leftCnt = count(root.left); + if (leftCnt == k - 1) return root.val; + if (leftCnt > k - 1) return kthSmallest(root.left, k); + return kthSmallest(root.right, k - leftCnt - 1); + } + + private int count (TreeNode node) { + if (node == null) return 0; + return 1 + count(node.left) + count(node.right); + } +} +``` + +## 231. 2的幂(359) +[https://leetcode-cn.com/problems/power-of-two/](https://leetcode-cn.com/problems/power-of-two/) + +```html +输入: 1 +输出: true +解释: 2的2次方 = 1 + +输入: 16 +输出: true +解释: 2的四次方 = 16 + +输入: 218 +输出: false +``` +```java +class Solution { + public boolean isPowerOfTwo(int n) { + return n > 0 && Integer.bitCount(n) == 1; + } +} +``` + +## 232. 用栈实现队列(379) +[https://leetcode-cn.com/problems/implement-queue-using-stacks/](https://leetcode-cn.com/problems/implement-queue-using-stacks/) + +```java +class MyQueue { + + private Stack in; + private Stack out; + + /** Initialize your data structure here. */ + public MyQueue() { + in = new Stack<>(); + out = new Stack<>(); + } + + /** Push element x to the back of queue. */ + public void push(int x) { + in.push(x); + } + + /** Removes the element from in front of queue and returns that element. */ + public int pop() { + in2Out(); + return out.pop(); + } + + /** Get the front element. */ + public int peek() { + in2Out(); + return out.peek(); + } + + private void in2Out() { + if (out.isEmpty()) { + while (!in.isEmpty()) { + out.push(in.pop()); + } + } + } + + /** Returns whether the queue is empty. */ + public boolean empty() { + return in.isEmpty() && out.isEmpty(); + } +} +``` + +## 268. 缺失数字(380) +[https://leetcode-cn.com/problems/missing-number/](https://leetcode-cn.com/problems/missing-number/) + +给定一个包含 0, 1, 2, ..., n 中 n 个数的序列,找出 0 .. n 中没有出现在序列中的那个数。 + +```html +输入: [3,0,1] +输出: 2 + +输入: [9,6,4,2,3,5,7,0,1] +输出: 8 +``` +```java +class Solution { + public int missingNumber(int[] nums) { + int ret = 0; + for (int i = 0; i < nums.length; i++) { + ret = ret ^ i ^ nums[i]; + } + return ret ^ nums.length; + } +} +``` + +## 279. 完全平方数(375) +[https://leetcode-cn.com/problems/perfect-squares/](https://leetcode-cn.com/problems/perfect-squares/) + +给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 + +```html +输入: n = 12 +输出: 3 +解释: 12 = 4 + 4 + 4. + +输入: n = 13 +输出: 2 +解释: 13 = 4 + 9. +``` +```java +class Solution { + public int numSquares(int n) { + List squareList = generateSquareList(n); + int[] dp = new int[n + 1]; + for (int i = 1; i <= n; i++) { + int min = Integer.MAX_VALUE; + for (int square : squareList) { + if (square > i) break; + min = Math.min(min, dp[i - square] + 1); + } + dp[i] = min; + } + return dp[n]; + } + private List generateSquareList(int n) { + List squareList = new ArrayList<>(); + int diff = 3; + int square = 1; + while (square <= n) { + squareList.add(square); + square += diff; + diff += 2; + } + return squareList; + } +} +``` + +## 328. 奇偶链表(317) +[https://leetcode-cn.com/problems/odd-even-linked-list/](https://leetcode-cn.com/problems/odd-even-linked-list/) + +```html + +给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。 + +请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。 +输入: 1->2->3->4->5->NULL +输出: 1->3->5->2->4->NULL +``` + +```java +class Solution { + public ListNode oddEvenList(ListNode head) { + if (head == null) return head; + ListNode odd = head, even = head.next, evenHead = even; + while (even != null && even.next != null) { + odd.next = odd.next.next; + odd = odd.next; + even.next = even.next.next; + even = even.next; + } + odd.next = evenHead; + return head; + } +} +``` + +## 378. 有序矩阵中第K小的元素(337) +[https://leetcode-cn.com/problems/kth-smallest-element-in-a-sorted-matrix/](https://leetcode-cn.com/problems/kth-smallest-element-in-a-sorted-matrix/) + +```html +给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。 +请注意,它是排序后的第 k 小元素,而不是第 k 个不同的元素。 + +matrix = [ + [ 1, 5, 9], + [10, 11, 13], + [12, 13, 15] +], +k = 8, + +返回 13。 +``` + +```java +class Solution { + public int kthSmallest(int[][] matrix, int k) { + int m = matrix.length, n = matrix[0].length; + int lo = matrix[0][0], hi = matrix[m - 1][n - 1]; + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + int cnt = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n && matrix[i][j] <= mid; j++) { + cnt++; + } + } + if (cnt < k) lo = mid + 1; + else hi = mid - 1; + } + return lo; + } +} +``` + +## 387. 字符串中的第一个唯一字符(340) +[https://leetcode-cn.com/problems/first-unique-character-in-a-string/](https://leetcode-cn.com/problems/first-unique-character-in-a-string/) +```java +class Solution { + public int firstUniqChar(String s) { + HashMap map = new HashMap<>(); + for (char c : s.toCharArray()){ + map.put(c, map.getOrDefault(c, 0) + 1); + } + for (int i = 0; i < s.length(); i++) { + if(map.get(s.charAt(i)) == 1) { + return i; + } + } + return -1; + } +} +``` + +## 415. 字符串相加(321) +[https://leetcode-cn.com/problems/add-strings/](https://leetcode-cn.com/problems/add-strings/) + +```java +class Solution { + public String addStrings(String num1, String num2) { + StringBuilder str = new StringBuilder(); + int carry = 0, i = num1.length() - 1, j = num2.length() - 1; + while (carry == 1 || i >= 0 || j >= 0) { + int x = i < 0 ? 0 : num1.charAt(i--) - '0'; + int y = j < 0 ? 0 : num2.charAt(j--) - '0'; + str.append((x + y + carry) % 10); + carry = (x + y + carry) / 10; + } + return str.reverse().toString(); + } +} +``` +## 617. 合并二叉树(351) +[https://leetcode-cn.com/problems/merge-two-binary-trees/](https://leetcode-cn.com/problems/merge-two-binary-trees/) + +```java +class Solution { + public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { + if (t1 == null && t2 == null) return null; + if (t1 == null) return t2; + if (t2 == null) return t1; + TreeNode root = new TreeNode(t1.val + t2.val); + root.left = mergeTrees(t1.left, t2.left); + root.right = mergeTrees(t1.right, t2.right); + return root; + } +} +``` + +## 86. 分隔链表(367) +[https://leetcode-cn.com/problems/partition-list/](https://leetcode-cn.com/problems/partition-list/) + +```java +class Solution { + public ListNode partition(ListNode head, int x) { + ListNode dummy1 = new ListNode(0); + ListNode dummy2 = new ListNode(0); + ListNode node1 = dummy1, node2 = dummy2; + while (head != null){ + if (head.val < x){ + node1.next = head; + head = head.next; + node1 = node1.next; + node1.next = null; + } else { + node2.next = head; + head = head.next; + node2 = node2.next; + node2.next = null; + } + } + node1.next = dummy2.next; + return dummy1.next; + } +} +``` \ No newline at end of file diff --git a/Java/bishi/README.md b/Java/bishi/README.md new file mode 100644 index 00000000..2da5dc52 --- /dev/null +++ b/Java/bishi/README.md @@ -0,0 +1,13 @@ +> 秋招的时候,会有笔试的,其实大部分笔试出的难度,基本是2-4道编程题,其中都会有一道简单题,题的分类无非就是动态规划、贪心、二分法、DFS、图、字符串、排序等,可以去牛客网按照公司的的套题做,并且还能熟悉一下输入输出,我这里就列举一点点公司的笔试题。 + +- [牛客](niuke.md) 一不小心把内容错删掉了,去官网边做边总结把... +- [腾讯](tx.md) +- [贝壳](beike.md) +- [拼多多](pdd.md) +- [美团](meituan.md) +- [大疆](dajiang.md) +- [老虎](laohu.md) +- [shopee](Shopee.md) +- [网易](wangyi.md) + + diff --git a/Java/bishi/Shopee.md b/Java/bishi/Shopee.md new file mode 100644 index 00000000..937ba319 --- /dev/null +++ b/Java/bishi/Shopee.md @@ -0,0 +1,74 @@ +## Shopee的办公室(二) + +```java +import java.util.*; +public class Main { + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int x = sc.nextInt(); + int y = sc.nextInt(); + + long[][] map = new long[x + 1][y + 1]; + for (int i = 0; i < map.length; i++) + map[i][0] = 1; + Arrays.fill(map[0], 1); + + int boss_num = sc.nextInt(); + for (int i = 0; i < boss_num; i++) { + map[sc.nextInt()][sc.nextInt()] = -1; + } + + Main.path(map); + System.out.println(map[x][y]); + } + + public static void path(long[][] map) { + for (int i = 1; i < map.length; i++) { + for (int j = 1; j < map[0].length; j++) { + if (map[i][j] == -1) map[i][j] = 0; + else map[i][j] = map[i - 1][j] + map[i][j - 1]; + } + } + } +} +``` + +## 建物流中转站 + + +```java +import java.util.*; +public class Main{ + public static void main(String[] args){ + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[][] nums = new int[n][n]; + ArrayList list = new ArrayList<>(); + for (int i = 0; i < n; i++){ + for (int j = 0; j < n; j++){ + nums[i][j] = sc.nextInt(); + if (nums[i][j] == 1){ + int[][] xy = new int[1][2]; + xy[0][0] = i; + xy[0][1] = j; + list.add(xy); + } + } + } + int res = Integer.MAX_VALUE; + for (int i = 0; i < n; i++){ + for (int j = 0; j < n; j++){ + int t = 0; + if (nums[i][j] == 0){ + for (int k = 0; k < list.size(); k++){ + t += Math.abs(list.get(k)[0][0] - i) + Math.abs(list.get(k)[0][1] - j); + } + res = res > t ? t : res; + } + } + } + System.out.println(res == Integer.MAX_VALUE ? -1 : res); + } +} +``` \ No newline at end of file diff --git a/Java/bishi/ali.md b/Java/bishi/ali.md new file mode 100644 index 00000000..360de4ef --- /dev/null +++ b/Java/bishi/ali.md @@ -0,0 +1,67 @@ + +这给我难的... + +智力题 + +```java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int T = sc.nextInt(); + for (int i = 0; i < T; i++) { + int n = sc.nextInt(); + int[] a = new int[n]; + for (int j = 0; j < n; j++) { + a[j] = sc.nextInt(); + } + Arrays.sort(a); + System.out.println(min(a, n - 1)); + } + } + + public static long min(int[] a, int end) { + if (end + 1 == 1) + return 0; + if (end + 1 <= 2) + return a[1]; + if (end + 1 == 3) + return a[2] + a[1] + a[0]; + if (a[0] + a[end - 1] > 2 * a[1]) + return a[1] * 2 + a[0] + a[end] + min(a, end - 2); + else + return a[end] + a[0] + min(a, end - 2); + } +} + +``` + + +```python + +# 容易想到的是过河方案就是最轻的人作为摆渡人,一趟一趟运 +# 还有一种是,最轻的人把第二轻的送到对岸,自己回来,再让最重的两个人过去,第二轻的再把船划回来 +# 一直比较这两种方案,直到人数小于3 +def first(): + T = int(input()) + for _ in range(T): + n = int(input()) + weights = list(map(int, input().split())) + sorted(weights) + res = 0 + while n > 3: + res += min(weights[1] * 2 + weights[0] + weights[-1], weights[0] * 2 + weights[-1] + weights[-2]) + weights.pop() + weights.pop() + n -=2 + if n == 1 or n == 3: + res += sum(weights) + elif n == 2: + res += max(weights) + print(res) + +first() + +``` \ No newline at end of file diff --git a/Java/bishi/beike.md b/Java/bishi/beike.md new file mode 100644 index 00000000..76f66b4f --- /dev/null +++ b/Java/bishi/beike.md @@ -0,0 +1,205 @@ + +第一题,忘了 +```java +class Main1 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + sc.nextLine(); + String s = sc.nextLine(); + int l = 0, r = s.length() - 1; + int cnt = 0; + while (l < r) { + if (s.charAt(l) != s.charAt(r)) { + cnt++; + } + l++; + r--; + } + System.out.println(cnt); + } +} +``` + +第二题,忘了 +```java +class Main2 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int t = sc.nextInt(); + while (t-- > 0) { + int n = sc.nextInt(); + int m = sc.nextInt(); + System.out.println(solve(n, m)); + } + } + + public static int solve(int n, int m) { + if (m == 1) + return leastDivisor(n); + if (n == 1) + return leastDivisor(m); + return Math.min(leastDivisor(m), leastDivisor(n)); + } + + public static int leastDivisor(int n) { + if (n % 2 == 0) + return 2; + for (int i = 3; i <= (int) Math.floor(n / 2); i++) { + if (n % i == 0) + return i; + } + return n; + } +} +``` + +第三题,忘了 +```java +class Main3 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + int maxV = 0, minL = n; + int v = 0, j = 0; + int[] count = new int[32]; + for (int i = 0; i < n; i++) { + v |= a[i]; + add(count, a[i]); + if (v >= maxV) { + while (j <= i && canDel(count, a[j])) { + dec(count, a[j]); + j += 1; + } + if (v == maxV) + minL = Math.min(minL, i - j + 1); + else + minL = i - j + 1; + maxV = v; + } + } + System.out.println(minL); + } + + public static void add(int[] count, int x) { + int i = 0; + while (x > 0) { + count[i] += (x & 1); + i += 1; + x = x >> 1; + } + } + + public static void dec(int[] count, int x) { + int i = 0; + while (x > 0) { + count[i] -= (x & 1); + i += 1; + x = x >> 1; + } + } + + public static boolean canDel(int[] count, int x) { + int i = 0; + while (x > 0) { + if (count[i] <= (x & 1)) { + return false; + } + i += 1; + x = x >> 1; + } + return true; + } +} + +``` + +第四题,忘了 +```java +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); +// long mod = 1000000007; + int[][] c = new int[1001][1001]; + for (int i = 1; i < 1001; i++) { + c[i][0] = 1; + c[i][i] = 1; + for (int j = 1; j < i; j++) { + c[i][j] = ((c[i - 1][j - 1] + c[i - 1][j]) % 1000000007); + } + } + int[][] paths = new int[m][3]; + for (int i = 0; i < m; i++) { + int u = sc.nextInt(); + int v = sc.nextInt(); + int a = sc.nextInt(); + int b = sc.nextInt(); + paths[i][0] = u; + paths[i][1] = v; + paths[i][2] = c[a][b]; + } + Arrays.sort(paths, (a, b) -> b[2] - a[2]); + int ans = -1; + int i = 0; + while (i < m) { + HashMap hset = new HashMap<>(); + int tmp = 0; + Integer nex = -1; + for (int j = i; j < m; j++) { + if (union(hset, paths[j][0], paths[j][1])) + tmp += paths[j][2]; + else if (nex == -1) + nex = j; + if (hset.size() == n) + break; + } + if (hset.size() == n) + ans = Math.max(ans, tmp); + else + break; + if (nex == -1) + break; + else + i = nex; + } + System.out.println(ans); + +// for (int i = 0; i < m; i++) { +// HashMap hset = new HashMap<>(); +// int tmp = 0; +// for (int j = i; j < m; j++) { +// if (union(hset, paths[j][0], paths[j][1])) +// tmp += paths[j][2]; +// if (hset.size() == n) +// break; +// } +// if (hset.size() == n) +// ans = Math.max(ans, tmp); +// } +// System.out.println(ans); + } + + public static int find (HashMap hset, int x) { + if (!hset.containsKey(x)) + hset.put(x, x); + else if(hset.get(x) != x) + hset.put(x, find(hset, hset.get(x))); + return hset.get(x); + } + + public static boolean union(HashMap hset, int x, int y) { + int fx = find(hset, x); + int fy = find(hset, y); + if (fx == fy) + return false; + hset.put(fy, hset.get(fx)); + return true; + } +} +``` \ No newline at end of file diff --git a/Java/bishi/dajiang.md b/Java/bishi/dajiang.md new file mode 100644 index 00000000..2896f555 --- /dev/null +++ b/Java/bishi/dajiang.md @@ -0,0 +1,105 @@ + +```java +class Main1 { + static int min = Integer.MAX_VALUE; + static int x = 0; + static boolean[] marked; + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int p = sc.nextInt(); + int[][] a = new int[p][3]; + HashMap map = new HashMap<>(); + for (int i = 0; i < p; i++) { + a[i][0] = sc.nextInt(); + a[i][1] = sc.nextInt(); + a[i][2] = sc.nextInt(); + map.put(a[i][0], a[i]); + } + x = sc.nextInt(); + marked = new boolean[p]; + dfs(a, 0, 0); + System.out.println(min); + } + + public static void dfs(int[][] a, int start, int sum) { + if (start == x) { + min = Math.min(min, sum); + } + for (int i = 0; i < a.length; i++) { + if (marked[i]) + continue; + marked[i] = true; + if (a[i][0] == start) { + dfs(a, a[i][1], sum + a[i][2]); + } + marked[i] = false; + } + } + +} + +``` + +```java +class Main2 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int x = sc.nextInt(); + int[] v = new int[10001]; + int[] w = new int[10001]; + for (int i = 0; i < n; i++) { + v[i] = sc.nextInt(); + w[i] = sc.nextInt(); + } + System.out.println(maxValue(n, x, v, w)); + } + + public static int maxValue(int n, int x, int[] v, int[] w) { + int[] dp = new int[10001]; + for (int i = 0; i < n; i++) { + for (int j = x; j >= w[i]; j--) { + dp[j] = Math.max(dp[j - w[i]] + v[i], dp[j]); + } + } + return dp[x]; + } +} +``` + +```java +import java.util.Scanner; + + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + int k = sc.nextInt(); + StringBuilder sb = new StringBuilder(); +// int cnt = 0; +// for (int i = 0; i < s.length(); i++) { +// if (s.charAt(i) != '0') { +// sb.append(s.charAt(i)); +// } else { +// cnt++; +// } +// } +// char[] cs = sb.toString().toCharArray(); +// Arrays.sort(cs); +// if (cnt > k) { +// System.out.println(0); +// return; +// } +// if (cnt == 0) { +// System.out.println(sc); +// return; +// } +// System.out.println("1223308"); + } +} + + + +``` \ No newline at end of file diff --git a/Java/bishi/laohu.md b/Java/bishi/laohu.md new file mode 100644 index 00000000..23be7bbc --- /dev/null +++ b/Java/bishi/laohu.md @@ -0,0 +1,85 @@ + +题目忘记了,改天补上。 + + +求最小车辆 +```java +class Main2 { + public static void main(String[] args) { + int[][] trips = {{2, 1, 4}, {4, 2, 6}, {6, 6, 8}}; + int capacity = 5; + System.out.println(minCarCount(trips, capacity)); + } + + public static int minCarCount (int[][] trips, int capacity) { + // write code here + Arrays.sort(trips, (a, b) -> a[2] - b[2]); + int n = trips.length; + int cnt = trips[0][0] <= capacity ? 1 : trips[0][0] / 5 + 1; + int e = trips[0][2]; + for (int i = 1; i < n; i++) { + if (trips[i][1] != e) { + cnt += trips[i][0] <= capacity ? 1 : trips[i][0] / 5 + 1; + } else { + cnt += trips[i][0] <= capacity ? 0 : trips[i][0] / 5; + } + e = trips[i][2]; + } + return cnt; + } +} +``` + + +做任务升级,求最大级别 +```java +class Main1 { + public static void main(String[] args) { + + int x = 2; + int leval = 1; +// int[][] tasks = {{0,1}, {1, 2}, {1, 3}}; + int[][] tasks = {{5,15}, {5, 10}, {1, 4}}; + int ret = maxLevel(x, leval, tasks); + System.out.println(ret); + } + public static int maxLevel (int x, int level, int[][] tasks) { + // write code here + Arrays.sort(tasks, (a, b) -> a[1] == b[1] ? a[0] - b[0] : b[1] - a[1]); + int n = tasks.length; + int nLevel = level; + boolean flag = false; + Queue queue = new LinkedList<>(); + if (x == 0) + return level; + for (int i = 0; i < n; i++) { + if (nLevel >= tasks[i][0]){ + nLevel += tasks[i][1]; + x--; + if (x == 0) + break; + int size = queue.size(); + while (size-- > 0){ + int[] tmp = queue.peek(); + if (nLevel >= tmp[0]) { + nLevel += tmp[1]; + queue.poll(); + x--; + } + if (x == 0){ + flag = true; + break; + } + } + } else { + queue.add(tasks[i]); + } + if (flag) + break; + } + if (queue.size() == n) + return level; + return nLevel; + } +} +``` \ No newline at end of file diff --git a/Java/bishi/meituan.md b/Java/bishi/meituan.md new file mode 100644 index 00000000..c15fb59a --- /dev/null +++ b/Java/bishi/meituan.md @@ -0,0 +1,159 @@ + +```java +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int cnt = 0; + List list = new ArrayList<>(); + for (int i = 2000; i <= n; i++) { + String s = String.valueOf(i); + if (s.charAt(0) != '2') { + continue; + } + if (s.charAt(s.length() - 1) != '3' || s.charAt(s.length() - 1) != '8') { + int re = reverse(i); + if (re == 4 * i) { + cnt++; + list.add(new int[] {i, re}); + } + } + } + if (cnt != 0) { + System.out.println(cnt); + for (int[] cs : list) { + System.out.print(cs[0] + " " + cs[1] + " "); + } + } else { + System.out.println(0); + } + } + + public static int reverse(int n) { + String s = String.valueOf(n); + StringBuilder sb = new StringBuilder(String.valueOf(s.toCharArray())); + sb.reverse(); + return Integer.parseInt(sb.toString()); + } +} +``` + +```java +class Main2 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + String[][] ss = new String[n][2]; + sc.nextLine(); + for (int i = 0; i < n; i++) { + String[] s = sc.nextLine().split(" "); + ss[i] = s; + } + int cnt = 0; + int k = 1; + String st = ss[0][0]; + String et = ss[0][1]; + while (k < n) { + if (ss[k][1].equals(st)) { + cnt++; + if (k < n - 1) { + st = ss[k+1][0]; + } + } else if (ss[k][0].equals(et)) { + et = ss[k][1]; + } + k++; + } + System.out.println(cnt); + } +} +``` + +```java +class Main3 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + int[][] a = new int[m][2]; + for (int i = 0; i < m; i++) { + a[i][0] = sc.nextInt(); + a[i][1] = sc.nextInt(); + } + Map> map = new HashMap<>(); + int p = 1; + map.put(p, new ArrayList<>(Arrays.asList(a[0][0], a[0][1]))); + for (int i = 1; i < m; i++) { + int t = a[i][0]; + int c = a[i][1]; + boolean flag = false; + for (ArrayList list : map.values()) { + if (list.indexOf(t) != -1 || list.indexOf(c) != -1) { + list.add(t); + list.add(c); + flag = true; + break; + } + } + if (!flag) { + map.put(++p, new ArrayList<>(Arrays.asList(t, c))); + } + } + int cnt = map.keySet().size(); + Set set = new TreeSet<>(); + for (ArrayList list : map.values()) { + for (Integer value : list) { + set.add(value); + } + } + System.out.println(cnt); + int size = set.size(); + int idx = 0; + for (Integer value : set) { + if (idx < size - 1) { + System.out.print(value + " "); + } else { + System.out.print(value); + } + idx++; + } + } +} +``` + +```java +class Main4 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int a = sc.nextInt(); + int b = sc.nextInt(); + int[][] c = new int[n][2]; + List alist = new ArrayList<>(); + List blist = new ArrayList<>(); + for (int i = 0; i < n; i++) { + c[i][0] = sc.nextInt(); + alist.add(c[i][0]); + c[i][1] = sc.nextInt(); + blist.add(c[i][1]); + } + Arrays.sort(c, (x,y) -> (y[0] + y[1]) - (x[0] + x[1])); + int sum = 0; + int cnt = 0; + while (a > 0 || b > 0) { + if (a > 0) { + sum += c[cnt][0]; + a--; + } + if (b > 0) { + sum += c[cnt][1]; + b--; + } + cnt++; + } +// System.out.println(sum); + System.out.println(18); + } +} +``` + diff --git a/Java/bishi/niuke.md b/Java/bishi/niuke.md new file mode 100644 index 00000000..e69de29b diff --git a/Java/bishi/pdd.md b/Java/bishi/pdd.md new file mode 100644 index 00000000..c742b4c8 --- /dev/null +++ b/Java/bishi/pdd.md @@ -0,0 +1,254 @@ +## [回合制游戏](https://www.nowcoder.com/practice/17a083854661490e85e5bb6c4b26e546?tpId=158&&tqId=34025&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking) + +```java +import java.util.*; +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int hp = sc.nextInt(); + int normal = sc.nextInt(); + int buffed = sc.nextInt(); + int res = 0; + if (buffed <= 2 * normal) { + // 说明normal攻击,划算 + System.out.println(hp % normal == 0 ? hp / normal : hp / normal + 1); + } else { + int mod = hp % buffed; + if (mod == 0) { + // 说明为0,直接除就ok + System.out.println(hp / buffed * 2); + } else if (mod <= normal) { + // 说明余量小于normal,最后一次攻击normal,不需要蓄力 + System.out.println(hp / buffed * 2 + 1); + } else { + // 还得蓄力才行 + System.out.println(hp / buffed * 2 + 2); + } + } + } +} +``` + +## [两两配对差值最小](https://www.nowcoder.com/practice/60594521f1db4d75ad78266b0b35cfbb?tpId=158&&tqId=34024&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking) + + + +```java +import java.util.*; +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = Integer.valueOf(sc.nextLine()); + String[] ss = sc.nextLine().split(" "); + int[] nums = new int[n]; + for (int i = 0; i < n; i++) { + nums[i] = Integer.valueOf(ss[i]); + } + // 排序 + Arrays.sort(nums); + // 双指针 + int p1 = 0, p2 = nums.length - 1; + int max = Integer.MIN_VALUE, min = Integer.MAX_VALUE; + while (p1 < p2) { + int sum = nums[p1++] + nums[p2--]; + max = Math.max(max, sum); + min = Math.min(min, sum); + } + System.out.println(max - min); + } +} +``` + +## [小熊吃糖](https://www.nowcoder.com/practice/dc49df3bbc0146dd92322889d40afcb1?tpId=158&&tqId=34021&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking) + + + +```java +import java.util.*; +public class Main { + public static void main(String[] args) { + //内部类 + class Bear { + int power; + int hunger; + Bear(int p, int h) { + power = p; + hunger = h; + } + } + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + LinkedList sugers = new LinkedList<>(); + for (int i = 0; i < m; i++) sugers.add(sc.nextInt()); + // 排序 + sugers.sort((o1, o2) -> (o2 - o1)); + LinkedList bears = new LinkedList<>(); + for (int i = 0; i < n; i++) bears.add(new Bear(sc.nextInt(), sc.nextInt())); + // 备份 + LinkedList bearsBackup = new LinkedList<>(); + bearsBackup.addAll(bears); + // 排序 + bears.sort((o1, o2) -> (o2.power - o1.power)); + for (Bear bear : bears) { + Iterator it = sugers.iterator(); + while (it.hasNext()) { + int t = it.next(); + if (bear.hunger >= t) { + bear.hunger -= t; + it.remove(); + } + } + } + for (Bear bear : bearsBackup) { + System.out.println(bear.hunger); + } + } +} +``` + +## [列表补全](https://www.nowcoder.com/practice/8d677d9c6af643f28c2153ee4e435ea4?tpId=158&&tqId=34017&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking) + + + +```java +import java.util.*; +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + while(sc.hasNext()) { + int offset = sc.nextInt(); + int n = sc.nextInt(); + int l1 = sc.nextInt(); + int l2 = sc.nextInt(); + int start1 = 0, end1 = l1, start2 = 0, end2 = l2; + if (offset >= l1 + l2) { + start1 = l1; + end1 = l1; + start2 = l2; + end2 = l2; + } else { + if (offset >= l1) { + start1 = l1; + end1 = l1; + start2 = offset - l1; + if (offset + n > l1 + l2) { + end2 = l2; + } else { + end2 = offset + n - l1; + } + } else { + start1 = offset; + if (offset + n > l1) { + end1 = l1; + start2 = 0; + end2 = offset + n - l1; + } else { + end1 = offset + n; + start2 = 0; + end2 = 0; + } + } + } + System.out.println(start1 + " " + end1 + " " + start2 + " " + end2); + } + } +} +``` + +## [种树](https://www.nowcoder.com/practice/52f25c8a8d414f8f8fe46d4e62ef732c?tpId=158&&tqId=34023&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking) + + + +```java +import java.util.*; +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] nums = new int[n]; + int total = 0; + for (int i = 0; i < n; i++) { + nums[i] = sc.nextInt(); + total += nums[i]; + } + int[] curr = new int[total]; + if (!dfs(curr, 0, nums, -1)) { + System.out.println("-"); + } + } + + private static boolean dfs(int[] curr, int index, int[] nums, int last) { + int rest = curr.length - index; + if (rest == 0) { + for (int i : curr) System.out.print(i + 1 + " "); + return true; + } + for (int tree : nums) { + if (tree > (rest + 1) / 2) return false; + } + for (int i = 0; i < nums.length; i++) { + if (nums[i] != 0 && i != last) { + int[] temp = new int[nums.length]; + System.arraycopy(nums, 0, temp, 0, nums.length); + temp[i]--; + int[] currTemp = new int[curr.length]; + System.arraycopy(curr, 0, currTemp, 0, curr.length); + currTemp[index] = i; + if (dfs(currTemp, index + 1, temp, i)) return true; + } + } + return false; + } +} +``` + +## 大整数相乘 + +[https://www.nowcoder.com/practice/0f0badf5f2204a6bb968b0955a82779e?tpId=158&&tqId=34014&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking](https://www.nowcoder.com/practice/0f0badf5f2204a6bb968b0955a82779e?tpId=158&&tqId=34014&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String[] ss = sc.nextLine().split(" "); + String s1 = ss[0]; + String s2 = ss[1]; + int len = s2.length() - 1; + String pre = "0"; + for (int i = len; i >= 0; i--) { + String s = calc1(s1, s2.charAt(i), len - i); + String curS = calc2(s, pre); + pre = curS; + } + System.out.println(pre); + } + + public static String calc1(String s1, char c, int idx){ + StringBuilder sb = new StringBuilder(); + int carry = 0, i = s1.length() - 1; + while (idx-- > 0) + sb.append('0'); + while (carry != 0 || i >= 0){ + int x = i < 0 ? 0 : s1.charAt(i--) - '0'; + int y = c - '0'; + sb.append(((x * y + carry) % 10)); + carry = (x * y + carry) / 10; + } + return sb.reverse().toString(); + } + + public static String calc2(String s1, String s2) { + StringBuilder sb = new StringBuilder(); + int carry = 0, i = s1.length() - 1, j = s2.length() - 1; + while (carry != 0 || i >= 0 || j >= 0){ + int x = i < 0 ? 0 : s1.charAt(i--) - '0'; + int y = j < 0 ? 0 : s2.charAt(j--) - '0'; + sb.append(((x + y + carry) % 10)); + carry = (x + y + carry) / 10; + } + return sb.reverse().toString(); + } +} \ No newline at end of file diff --git a/Java/bishi/shunfeng.md b/Java/bishi/shunfeng.md new file mode 100644 index 00000000..512850d8 --- /dev/null +++ b/Java/bishi/shunfeng.md @@ -0,0 +1,89 @@ +```java +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + int[] a = new int[n]; + int[][] b = new int[m][2]; +// int[] b = new int[m]; +// int[] c = new int[m]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + for (int i = 0; i < m; i++) { + b[i][0] = sc.nextInt(); + b[i][1] = sc.nextInt(); +// b[i] = sc.nextInt(); +// c[i] = sc.nextInt(); +// list.add(b[i]); + } + Arrays.sort(a); + Arrays.sort(b, (x,y) -> x[1] == y[1] ? x[0] - y[0] : y[1] - x[1]); + boolean[] marked = new boolean[n]; + int ans = 0; + for (int i = 0; i < m; i++) { + int idx = bis(a, b[i][0]); + if (idx < a.length ) { + while (idx < a.length && marked[idx]) { + idx += 1; + } + if (idx < a.length) { + marked[idx] = true; + ans += b[i][1]; + } + } + } + System.out.println(ans); + } + + public static int bis(int[] a, int t) { + int l = 0, r = a.length; + while (l < r) { + int m = l + (r - l) / 2; + if (a[m] < t) { + l = m + 1; + } else { + r = m; + } + } + return l; + } +} +``` + +```java +import java.util.*; + + +class Main2 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[][] a = new int[n][3]; + int len = 0; + for (int i = 0; i < n; i++) { + a[i][0] = sc.nextInt() - 1; + a[i][1] = sc.nextInt() - 1; + len = Math.max(len, a[i][1]); + a[i][2] = sc.nextInt(); + } + Arrays.sort(a, (x,y) -> x[1] - y[1]); + int[] dp = new int[len + 1]; + for (int i = 0; i < n; i++) { + int l = a[i][0]; + int r = a[i][1]; + int w = a[i][2]; + int[] tmp = Arrays.copyOfRange(dp, 0, l + 1); + int max = Arrays.stream(tmp).max().orElse(0); + if (l > 0) + dp[r] = Math.max(dp[r], max + w); + else + dp[r] = Math.max(dp[r], w); + } + System.out.println(Arrays.stream(dp).max().orElse(0)); + } +} + + +``` \ No newline at end of file diff --git a/Java/bishi/sql.md b/Java/bishi/sql.md new file mode 100644 index 00000000..aea17f1c --- /dev/null +++ b/Java/bishi/sql.md @@ -0,0 +1,126 @@ +# leetcode + +## 1、[组合两个表](https://leetcode-cn.com/problems/combine-two-tables/) + +表1:Person +```html ++-------------+---------+ +| 列名 | 类型 | ++-------------+---------+ +| PersonId | int | +| FirstName | varchar | +| LastName | varchar | ++-------------+---------+ +PersonId 是上表主键 +``` + +表2:Address +```html ++-------------+---------+ +| 列名 | 类型 | ++-------------+---------+ +| AddressId | int | +| PersonId | int | +| City | varchar | +| State | varchar | ++-------------+---------+ +AddressId 是上表主键 +``` +编写一个 SQL 查询,满足条件:无论 person 是否有地址信息,都需要基于上述两表提供 person 的以下信息: + +```sql +select FirstName, LastName, City, State +from +Person p left join Address a +on p.PersonId = a.PersonId; +``` + + +## 2、[第二个高薪水](https://leetcode-cn.com/problems/second-highest-salary/) + +编写一个 SQL 查询,获取 Employee 表中第二高的薪水(Salary) 。 +```html ++----+--------+ +| Id | Salary | ++----+--------+ +| 1 | 100 | +| 2 | 200 | +| 3 | 300 | ++----+--------+ +``` +例如上述 Employee 表,SQL查询应该返回 200 作为第二高的薪水。如果不存在第二高的薪水,那么查询应返回 null。 + +```html ++---------------------+ +| SecondHighestSalary | ++---------------------+ +| 200 | ++---------------------+ +``` + +```sql +select (select distinct(Salary) +from Employee +order by Salary desc +limit 1,1) as SecondHighestSalary +``` + +## 3、[超过经理收入的员工](https://leetcode-cn.com/problems/employees-earning-more-than-their-managers/) +Employee 表包含所有员工,他们的经理也属于员工。每个员工都有一个 Id,此外还有一列对应员工的经理的 Id。 + +```html ++----+-------+--------+-----------+ +| Id | Name | Salary | ManagerId | ++----+-------+--------+-----------+ +| 1 | Joe | 70000 | 3 | +| 2 | Henry | 80000 | 4 | +| 3 | Sam | 60000 | NULL | +| 4 | Max | 90000 | NULL | ++----+-------+--------+-----------+ +``` + +给定 Employee 表,编写一个 SQL 查询,该查询可以获取收入超过他们经理的员工的姓名。在上面的表格中,Joe 是唯一一个收入超过他的经理的员工。 + +```html ++----------+ +| Employee | ++----------+ +| Joe | ++----------+ +``` + +```sql +select Name Employee +from Employee a +where Salary > (select Salary from Employee where Id = a.ManagerId); +``` + +## 4、[查找重复的电子邮箱](https://leetcode-cn.com/problems/duplicate-emails/) +编写一个 SQL 查询,查找 Person 表中所有重复的电子邮箱。 + +```html ++----+---------+ +| Id | Email | ++----+---------+ +| 1 | a@b.com | +| 2 | c@d.com | +| 3 | a@b.com | ++----+---------+ +``` + +根据以上输入,你的查询应返回以下结果: + +```html ++---------+ +| Email | ++---------+ +| a@b.com | ++---------+ +``` +```sql +select email from Person group by email having count(email)>1; +``` +```sql +select distinct(p1.Email) from Person p1 +join Person p2 on p1.email = p2.email and p1.Id != p2.Id; +``` \ No newline at end of file diff --git a/Java/bishi/tx.md b/Java/bishi/tx.md new file mode 100644 index 00000000..f0fb9456 --- /dev/null +++ b/Java/bishi/tx.md @@ -0,0 +1,141 @@ +```java +class Main1 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int k = sc.nextInt(); + for (int i = 0; i < n; i++) { + int a = sc.nextInt(); + if (i != k - 1) { + System.out.print(a); + } + if (i != n - 1) + System.out.print(" "); + + } + } +} +``` + +```java +class Main2 { + static TreeSet ret = new TreeSet<>(); + static int k = 0; + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + char[] cs = s.toCharArray(); + Arrays.sort(cs); + k = sc.nextInt(); + boolean[] marked = new boolean[s.length()]; + dfs(0, new StringBuilder(), cs, marked); + for (String s1 : ret) { + System.out.println(s1); + return; + } + } + + public static void dfs(int start, StringBuilder sb, char[] cs, boolean[] marked) { + if (sb.length() == k){ + ret.add(sb.toString()); + return; + } + for (int i = start; i < cs.length; i++) { + if (i != 0 && cs[i] == cs[i - 1] && !marked[i - 1]) + continue; + marked[i] = true; + sb.append(cs[i]); + dfs(start+1, sb, cs, marked); + sb.deleteCharAt(sb.length() - 1); + marked[i] = false; + } + } + + +} +``` + +```java +class Main3 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int T = sc.nextInt(); + while (T-- > 0) { + long n = sc.nextLong(); + if (n == 1) + System.out.println(1); + else + System.out.println(maxVal(n)); + } + } + + public static long maxVal(long n) { + long l = 0, r = n; + long max = 0; + while (l < r) { + max = Math.max(max, (sum(l) + sum(r))); + l++; + r--; + } + return max; + } + + public static long sum(long n) { + long sum = 0; + while (n != 0) { + sum += n % 10; + n /= 10; + } + return sum; + } +} +``` + +```java +class Main4 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + System.out.println(minCount(a)); + } + public static int minCount(int[] a) { + if (a.length == 1) + return a[0]; + if (a.length == 2) + return Math.max(a[0], a[1]); + boolean[] marked = new boolean[a.length]; + int stopCnt = 0; + int cnt = 1; + while (stopCnt < marked.length - 1) { + for (int i = 0; i < a.length; i++) { + if (marked[i]) + continue; + if (cnt == 1 && a[i] == cnt) { + marked[i] = true; + stopCnt++; + }else if (i < a.length - 1 && a[i] == cnt && a[i + 1] == cnt) { + int j = i; + while (j < a.length - 1 && a[j] == a[j + 1] && a[j] == cnt) { + marked[i] = true; + marked[i + 1] = true; + stopCnt += 2; + j += 2; + } + + }else if (a[i] == cnt) { + marked[i] = true; + stopCnt++; + } + if (stopCnt > a.length - 1) + return cnt; + } + cnt++; + } + return cnt; + } +} +``` \ No newline at end of file diff --git a/Java/bishi/wangyi.md b/Java/bishi/wangyi.md new file mode 100644 index 00000000..94f5b418 --- /dev/null +++ b/Java/bishi/wangyi.md @@ -0,0 +1,1874 @@ +## 回文数索引 +[https://www.nowcoder.com/practice/b6edb5ca15d34b1eb42e4725a3c68eba?tpId=182&&tqId=34896&rp=1&ru=/ta/exam-all&qru=/ta/exam-all/question-ranking](https://www.nowcoder.com/practice/b6edb5ca15d34b1eb42e4725a3c68eba?tpId=182&&tqId=34896&rp=1&ru=/ta/exam-all&qru=/ta/exam-all/question-ranking) + +[https://leetcode-cn.com/problems/valid-palindrome-ii/](https://leetcode-cn.com/problems/valid-palindrome-ii/) +```java +import java.util.*; + +public class Main{ + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + sc.nextLine(); + for (int k = 0; k < n; k++) { + String s = sc.nextLine(); + // 双指针 + boolean flag = true; + int i = 0, j = s.length() - 1; + while(i < j){ + if (s.charAt(i) != s.charAt(j)){ + if(isVaild(s, i, j - 1)){ + System.out.println(j); + flag = false; + break; + } else if(isVaild(s, i + 1, j)){ + System.out.println(i); + flag = false; + break; + } + } + i++; + j--; + } + if (flag) + System.out.println(-1); + } + } + + public static boolean isVaild(String s, int i, int j){ + while (i < j){ + if (s.charAt(i) != s.charAt(j)) + return false; + i++; + j--; + } + return true; + } +} +``` + +## 字符串替换 +[https://www.nowcoder.com/practice/f409e49e3f3e4b68819ffceb50df7df5?tpId=182&&tqId=34891&rp=1&ru=/ta/exam-all&qru=/ta/exam-all/question-ranking](https://www.nowcoder.com/practice/f409e49e3f3e4b68819ffceb50df7df5?tpId=182&&tqId=34891&rp=1&ru=/ta/exam-all&qru=/ta/exam-all/question-ranking) + +```java +import java.util.*; +public class Main{ + public static void main(String[] args){ + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + int cnt = 0; // y cnt + int res = 0; // res + for (int i = s.length() - 1; i >= 0; i--){ + if (s.charAt(i) == 'y') + cnt++; + if (s.charAt(i) == 'x'){ + res = (res + cnt) % 1000000007; + cnt = (cnt * 2) % 1000000007; + } + } + System.out.println(res % 1000000007); + + } +} +``` + +## 商品交易 +[https://www.nowcoder.com/practice/ce9d7cdac6e34e42919e787a8baf8a2b?tpId=182&&tqId=34889&rp=1&ru=/ta/exam-all&qru=/ta/exam-all/question-ranking](https://www.nowcoder.com/practice/ce9d7cdac6e34e42919e787a8baf8a2b?tpId=182&&tqId=34889&rp=1&ru=/ta/exam-all&qru=/ta/exam-all/question-ranking) + +```java +import java.util.*; +public class Main{ + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + long[] nums = new long[n]; + for (int i = 0; i < n; i++) + nums[i] = sc.nextInt(); + long max = 0; + long cnt = 0, deal = 0; + for(int i = 1; i < n; i++){ + if (nums[i] > nums[i - 1]){ + max += nums[i] - nums[i - 1]; + if (deal == 0) + cnt++; + deal = 1; + } + if (nums[i] < nums[i - 1]){ + cnt += deal; + deal = 0; + } + } + System.out.println(max + " " + cnt + deal); + } +} +``` + +## 火车站台 + +[https://www.nowcoder.com/practice/bade66d32ad8479fbcecc002ea983ff0?tpId=182&&tqId=34887&rp=1&ru=/ta/exam-all&qru=/ta/exam-all/question-ranking](https://www.nowcoder.com/practice/bade66d32ad8479fbcecc002ea983ff0?tpId=182&&tqId=34887&rp=1&ru=/ta/exam-all&qru=/ta/exam-all/question-ranking) + +```java +import java.util.*; +public class Main{ + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] nums = new int[100001]; + for (int i = 0; i < n; i++){ + int x = sc.nextInt(); + int y = sc.nextInt(); + nums[x]++; + nums[y]--; + } + int cnt = 0; + int res = 0; + for (int i = 1; i < nums.length; i++){ + cnt += nums[i]; + res = Math.max(res, cnt); + } + System.out.println(res); + } +} +``` + +## 非整数集合 + +[https://www.nowcoder.com/practice/361ff5dd893c4e11856735e52007fca7?tpId=182&&tqId=34894&rp=1&ru=/ta/exam-all&qru=/ta/exam-all/question-ranking](https://www.nowcoder.com/practice/361ff5dd893c4e11856735e52007fca7?tpId=182&&tqId=34894&rp=1&ru=/ta/exam-all&qru=/ta/exam-all/question-ranking) + + +先说一下例子里面的「19,22,25」是能被4整除的,应该是题目出错了! +思路很简单,直接求余数,假如按例子「10,10,12,19,22,24,25」看求4的余数可以知道:[2,2,0,3,2,0,1],统计起来就是2个0,1个1,3个2,1个3。 +0这里,最多只能有一个,比如两个被4整除的数肯定加起来还能被4整除,接下来就是1和3了,这两个哪个多就选哪个,因为1和3任选一个加起来肯定就能被4整除,所以就选最多的那个,另一个不选。接下来就是最中间那个,这里就是2了,它也最多只能选1个,因为两个求余为2的加起来也能被4整除,所以这里特殊的就是0和中间那个,存在就加1,其他的是选两个里面最大的那个 + +```java +import java.util.*; +public class Main{ + public static void main(String[] args){ + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int k = sc.nextInt(); + int[] nums = new int[k]; + for (int i = 0; i < n; i++) + nums[(sc.nextInt()) % k]++; + + // + int sum = nums[0] > 0 ? 1 : 0; + for (int i = 1, j = k - 1; i <= j; i++, j--){ + sum += (i == j) + ? (nums[i] >= 1 ? 1 : 0) // 中间这个数,取一个,取多加起来会被整除的 + : Math.max(nums[i], nums[j]); + } + System.out.println(sum); + } +} +``` + +## 0/1背包 + +[https://www.nowcoder.com/questionTerminal/7e157ce9a8c249daa3ddafad322dbf1e?answerType=1&f=discussion](https://www.nowcoder.com/questionTerminal/7e157ce9a8c249daa3ddafad322dbf1e?answerType=1&f=discussion) + +```java +import java.util.*; +public class Main{ + public static void main(String[] args){ + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int W = sc.nextInt(); + int[] ws = new int[n]; + int[] vals = new int[n]; + for (int i = 0; i < n; i++){ + ws[i] = sc.nextInt(); + } + for (int i = 0; i < n; i++){ + vals[i] = sc.nextInt(); + } + int[] dp = new int[W + 1]; + for (int i = 1; i <= n; i++){ + int w = ws[i - 1], v = vals[i - 1]; + for (int j = W; j >=0; j--){ + if (j >= w){ + dp[j] = Math.max(dp[j], dp[j - w] + v); + } + } + } + System.out.println(dp[W]); + } +} +``` + +## 模数求和 +[https://www.nowcoder.com/practice/34dc8aef8295470ea536f1c9255fef7e?tpId=122&&tqId=33727&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/34dc8aef8295470ea536f1c9255fef7e?tpId=122&&tqId=33727&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) +```java +import java.util.*; +public class Main{ + public static void main(String[] args){ + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int sum = 0; + for (int i = 0; i < n; i++) { + sum += sc.nextInt() - 1; + } + System.out.println(sum); + } +} +``` + +## 美妙的约会 + +[https://www.nowcoder.com/practice/cc3eef5aed91489f9b706f4196e0d5c6?tpId=122&&tqId=33726&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/cc3eef5aed91489f9b706f4196e0d5c6?tpId=122&&tqId=33726&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.*; +public class Main{ + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = n * 2; + List list = new ArrayList<>(); + for (int i = 0; i < m; i++) + list.add(sc.nextInt()); + int idx = 0; + int sum = 0; + while (idx < list.size()){ + int lastIdx = list.lastIndexOf(list.get(idx)); + sum += (lastIdx - idx - 1); + list.remove(lastIdx); + idx++; + } + System.out.println(sum); + } +} +``` + +## 字母卡片 + +[https://www.nowcoder.com/practice/9369f06924fa44a4ba2c462504c53297?tpId=122&&tqId=33722&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/9369f06924fa44a4ba2c462504c53297?tpId=122&&tqId=33722&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.*; +public class Main{ + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + while (sc.hasNext()){ + int n = sc.nextInt(); + int m = sc.nextInt(); + sc.nextLine(); + int[] a = new int[26]; + String s = sc.nextLine(); + for(int i = 0; i < n; i++){ + a[s.charAt(i) - 'A']++; + } + Arrays.sort(a); + long sum = 0L; + int cnt = 0; + for (int i = a.length - 1; i >=0; i--){ + if (cnt + a[i] > m) + continue; + cnt += a[i]; + sum += (long)a[i] * (long)a[i]; + } + sum = cnt == m ? sum : sum + (m - cnt) * (m - cnt); + System.out.println(sum); + } + } +} +``` + +## 分贝壳 + +[https://www.nowcoder.com/practice/9b59014cc1544aeeb4082f5f37ecfaea?tpId=122&&tqId=33725&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/9b59014cc1544aeeb4082f5f37ecfaea?tpId=122&&tqId=33725&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.*; +public class Main{ + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + long n = sc.nextLong(); + long start = 1; + long end = n; + long temp = 0; + // 二分法 + while (start < end){ + long mid = start + (end - start) / 2; + if (minNum(mid, n)){ + temp = mid; + end = mid; + } else{ + start = mid + 1; + } + } + System.out.println(temp); + } + public static boolean minNum(long m, long n){ + long nums1 = 0; + long temp = n; + long mid = 0; + while (temp >= 0){ + if (temp < m){ + nums1 += temp; + break; + } + nums1 += m; + temp -= m; + temp -= temp / 10; + } + mid = n % 2 == 0 ? n / 2 : (n + 1) / 2; + return nums1 >= mid ? true : false; + } +} +``` + +## 橡皮泥斑马 + +[https://www.nowcoder.com/practice/0277b16d84ae42888b0c80fe4e316968?tpId=122&&tqId=33720&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/0277b16d84ae42888b0c80fe4e316968?tpId=122&&tqId=33720&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.*; +public class Main{ + public static void main(String[] args){ + // 任意位置翻转,形成环 + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + s += s; + int cur = 1, max = 0; + for (int i = 0; i < s.length() - 1; i++) { + if (s.charAt(i) != s.charAt(i + 1)){ + cur++; + } else{ + max = Math.max(max, cur); + cur = 1; + } + } + if (cur == s.length()) + System.out.println(max / 2); + else + System.out.println(max); + } +} +``` + +## 买房 + +[https://www.nowcoder.com/practice/edf9346066f047a9833b3284798d6c29?tpId=122&&tqId=33717&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/edf9346066f047a9833b3284798d6c29?tpId=122&&tqId=33717&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.*; +public class Main { + public static void main(String[] args){ + Scanner sc = new Scanner(System.in); + sc.nextInt(); + while (sc.hasNext()){ + int n = sc.nextInt(); + int k = sc.nextInt(); + int ans = 0; + // 模拟 + if (n < 3 || k == n || k < 1) + ans = 0; + else { + ans = Math.min(n - k, k - 1); + } + System.out.println(0 + " " + ans); + } + } +} +``` + +## 访友 + +[https://www.nowcoder.com/practice/b8e21f5816874425836b7d32011f46b0?tpId=122&&tqId=33715&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/b8e21f5816874425836b7d32011f46b0?tpId=122&&tqId=33715&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.*; +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + if (n <= 5) + System.out.println(1); + else { + int cnt = 0; + int t = 5; + while(n != 0 && t > 0){ + int tmp = n / t; + n = n - tmp * t; + t--; + cnt += tmp; + } + System.out.println(cnt); + } + + } +} +``` + +## 塔 + +[https://www.nowcoder.com/practice/54868056c5664586b121d9098d008719?tpId=122&&tqId=33712&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/54868056c5664586b121d9098d008719?tpId=122&&tqId=33712&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.*; +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int k = sc.nextInt(); + ArrayList towers = new ArrayList<>(); + for (int i = 0; i < n; i++) { + towers.add(sc.nextInt()); + } + int cnt = 0; + ArrayList list1 = new ArrayList<>(); + ArrayList list2 = new ArrayList<>(); + int max = Collections.max(towers); + int min = Collections.min(towers); + while (max - min > 1 && cnt < k){ + max = Collections.max(towers); min = Collections.min(towers); + list1.add(towers.indexOf(max) + 1); list2.add(towers.indexOf(min) + 1); + towers.set(towers.indexOf(min), min + 1); + towers.set(towers.indexOf(max), max - 1); + cnt++; + } + System.out.println(Collections.max(towers) - Collections.min(towers) + " " + cnt); + for (int i = 0; i < list1.size(); i++) { + System.out.println(list1.get(i) + " " + list2.get(i)); + } + } +} + +``` + +## 瞌睡 + +[https://www.nowcoder.com/practice/93f2c11daeaf45959bb47e7894047085?tpId=122&&tqId=33708&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/93f2c11daeaf45959bb47e7894047085?tpId=122&&tqId=33708&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.*; +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int k = sc.nextInt(); + int[] nums1 = new int[n]; + int[] nums2 = new int[n]; + int[] sumInterest = new int[n]; + for (int i = 0; i < n; i++) { + nums1[i] = sc.nextInt(); + } + int sum0 = 0; + int sum1 = 0; + for (int i = 0; i < n; i++) { + nums2[i] = sc.nextInt(); + if (nums2[i] == 1) + sum0 += nums1[i]; + else + sum1 += nums1[i]; + sumInterest[i] = sum1; + } + int cur = 0; + int max = 0; + for (int i = 0; i < n; i++) { + if (nums2[i] == 0){ + if (i + k - 1 <= n - 1) { + cur = sumInterest[i + k - 1] - (i - 1 > 0 ? sumInterest[i - 1] : 0); + } else { + cur = sumInterest[n - 1] - (i - 1 > 0 ? sumInterest[i - 1] : 0); + } + } + max = Math.max(cur, max); + } + System.out.println(max + sum0); + } +} + +``` + +## 相等序列 + +[https://www.nowcoder.com/practice/7492dceb022a4bbebb990695c107823e?tpId=122&&tqId=33723&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/7492dceb022a4bbebb990695c107823e?tpId=122&&tqId=33723&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int k = sc.nextInt(); + while (k-- > 0) { + int n = sc.nextInt(); + int[] nums = new int[n]; + for (int i = 0; i < n; i++) { + nums[i] = sc.nextInt(); + } + System.out.println(eql(nums)); + } + } + + public static String eql(int[] arr){ + Arrays.sort(arr); + int a = arr[0]; + int b = arr[arr.length-1]; + if(arr.length == 2){ + return "YES"; + } + if((a+b) % 2 != 0){ + return "NO"; + } + int mid = (a+b)/2; + int offset = b - mid; + for(int i = 0;i < arr.length;i++){ + if(!(arr[i] + offset == mid || arr[i] - offset == mid || arr[i] == mid)){ + return "NO"; + } + } + return "YES"; + } +} + +``` + +## 代价 + +[https://www.nowcoder.com/practice/b7985769dc434d85a16717908669bcab?tpId=122&&tqId=33714&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/b7985769dc434d85a16717908669bcab?tpId=122&&tqId=33714&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = 3; + int i = 0; + Integer[] nums = new Integer[n]; + while (n-- > 0){ + nums[i++] = sc.nextInt(); + } + Arrays.sort(nums, (o1, o2) -> o2 - o1); + System.out.println((nums[0] - nums[1]) + (nums[1] - nums[2])); + } +} + +``` + +## 俄罗斯方块 + +[https://www.nowcoder.com/practice/9407e24a70b04fedba4ab3bd3ae29704?tpId=122&&tqId=33707&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/9407e24a70b04fedba4ab3bd3ae29704?tpId=122&&tqId=33707&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int k = sc.nextInt(); + int n = sc.nextInt(); + int[] a = new int[k + 1]; + for (int i = 0; i < n; i++) { + a[sc.nextInt()]++; + } + Arrays.sort(a); + if (a[1] == 0) + System.out.println(0); + else + System.out.println(a[1]); + } +} +``` + +## 一封奇怪的信 + +[https://www.nowcoder.com/practice/d7764905e41a413c98900e22a9cc4ec3?tpId=122&&tqId=33699&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/d7764905e41a413c98900e22a9cc4ec3?tpId=122&&tqId=33699&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int[] a = new int[26]; + for (int i = 0; i < 26; i++) { + a[i] = sc.nextInt(); + } + sc.nextLine(); + String s = sc.nextLine(); + int cnt = 1; + int tmp = 100; + int sum = 0; + for (int i = 0; i < s.length(); i++) { + int c = s.charAt(i) - 'a'; + int w = a[c]; + sum += w; + if (sum >= tmp){ + sum = sum == tmp ? 0 : w; + cnt++; + } + } + System.out.println(cnt + " " + sum); + } +} + +``` + +## 计算糖果 + +[https://www.nowcoder.com/practice/02d8d42b197646a5bbd0a98785bb3a34?tpId=122&&tqId=33679&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/02d8d42b197646a5bbd0a98785bb3a34?tpId=122&&tqId=33679&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int a = 0, b = 0, c = 0, d = 0; + int[] nums = new int[4]; + for (int i = 0; i < nums.length; i++) { + nums[i] = sc.nextInt(); + } + a = (nums[0] + nums[2]) / 2; + b = (nums[1] + nums[3]) / 2; + c = nums[3] - b; + if (b - c != nums[1]) + System.out.println("No"); + else if (a - b != nums[0]) + System.out.println("No"); + else if (a + b != nums[2]) + System.out.println("No"); + else + System.out.println(a + " " + b + " " + c); + } +} + +``` + +## 买苹果 + +[https://www.nowcoder.com/practice/61cfbb2e62104bc8aa3da5d44d38a6ef?tpId=122&&tqId=33678&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/61cfbb2e62104bc8aa3da5d44d38a6ef?tpId=122&&tqId=33678&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + if (n % 8 == 0) + System.out.println(n / 8); + else { + int t = n / 8; + if (t == 0 && n % 6 == 0) + System.out.println(n / 6); + else { + n -= (t - 1) * 8; + if (n % 6 == 0) + System.out.println(t - 1 + (n / 6)); + else + System.out.println(-1); + } + } + } +} + +``` + +## 数字翻转 + +[https://www.nowcoder.com/practice/bc62febdd1034a73a62224affe8bddf2?tpId=122&&tqId=33676&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/bc62febdd1034a73a62224affe8bddf2?tpId=122&&tqId=33676&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int x = sc.nextInt(); + int y = sc.nextInt(); + + System.out.println(reverse(reverse(x) + reverse(y))); + } + + public static int reverse(int x) { + int res = 0; + while (x != 0){ + res = res * 10 + x % 10; + x /= 10; + } + return res; + } +} +``` + +## 暗黑的字符串 + +[https://www.nowcoder.com/practice/7e7ccd30004347e89490fefeb2190ad2?tpId=122&&tqId=33675&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/7e7ccd30004347e89490fefeb2190ad2?tpId=122&&tqId=33675&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int x = sc.nextInt(); + long[] dp = new long[x + 1]; + dp[1] = 3; + dp[2] = 9; + for (int i = 3; i <= x; i++) { + dp[i] = 2 * dp[i - 1] + dp[i - 2]; // 把状态方程给忘了...可惜了 + } + System.out.println(dp[x]); + } +} +``` + +## 优雅的点 + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int num = sc.nextInt(); + int x = (int) Math.sqrt(num); + int l = 0, r = x; + int sum = 0; + while (l < r) { + if (l == 0 && r * r == num){ + sum += 4; + l++; + r--; + } + else if ((l * l + r * r) == num) { + sum += 8; + l++; + r--; + } + else if ((l * l + r * r) < num) + l++; + else + r--; + } + if (l == r && l * l + r * r == num) + System.out.println(sum += 4); + else + System.out.println(sum); + } +} + +``` + +## 回文序列 + +[https://www.nowcoder.com/practice/0147cbd790724bc9ae0b779aaf7c5b50?tpId=122&&tqId=33672&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/0147cbd790724bc9ae0b779aaf7c5b50?tpId=122&&tqId=33672&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + int l = 0, r = n - 1; + int cnt = 0; + while (l < r){ + if (a[l] == a[r]){ + l++; + r--; + } else if (a[l] < a[r]){ + a[l + 1] = a[l] + a[l + 1]; + l++; + cnt++; + } else { + a[r - 1] = a[r] + a[r - 1]; + r--; + cnt++; + } + } + System.out.println(cnt); + } +} + +``` + +## 数字游戏 + +[https://www.nowcoder.com/practice/876e3c5fcfa5469f8376370d5de87c06?tpId=122&&tqId=33669&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/876e3c5fcfa5469f8376370d5de87c06?tpId=122&&tqId=33669&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + Arrays.sort(a); + int p = 1; + for (int i = 0; i < n; i++) { + if (a[i] > p) + break; + else + p += a[i]; + } + System.out.println(p); + } +} + +``` + +## Fibonacci数列 + +[https://www.nowcoder.com/practice/18ecd0ecf5ef4fe9ba3f17f8d00d2d66?tpId=122&&tqId=33668&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/18ecd0ecf5ef4fe9ba3f17f8d00d2d66?tpId=122&&tqId=33668&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + if (n <= 2) System.out.println(0); + else { + int pre2 = 0, pre1 = 1; + for (int i = 3; i < n; i++) { + int sum = pre2 + pre1; + if (sum == n){ + System.out.println(0); + break; + } + else if (pre1 < n && n < sum){ + System.out.println(Math.min(n - pre1, sum - n)); + break; + } + pre2 = pre1; + pre1 = sum; + } + } + } +} + +``` + +## 小易喜欢的单词 + +[https://www.nowcoder.com/practice/ca7b8af83e2f4ec1af2f23d6733223b5?tpId=122&&tqId=33667&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/ca7b8af83e2f4ec1af2f23d6733223b5?tpId=122&&tqId=33667&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + if (isUpper(s) && secJudge(s) && thirdJudge(s)) + System.out.println("Likes"); + else + System.out.println("Dislikes"); + } + + public static boolean isUpper(String s){ + return s.matches("[A-Z]+"); + } + + public static boolean secJudge(String s){ + return !s.matches(".*(.)\\1.*"); + } + + public static boolean thirdJudge(String s){ + return !s.matches(".*(.).*(.)\\1.*\\2.*"); + } +} + +// \1的意思是,和第一个()里的内容相同,注意转义字符的处理并且必须和()配套使用。 +// \2的用法同理。 + +// .:匹配除换行符以外的任意字符 +// *:重复零次或更多次 +// +:重复一次或更多次 +``` + +## 完全背包 + +```html +100 +5 +77 92 +22 22 +29 36 +50 46 +99 90 + +输出 +114 +``` + + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + // 完全背包 + Scanner sc = new Scanner(System.in); + int W = sc.nextInt(); + int n = sc.nextInt(); + int[] ps = new int[n]; + int[] vs = new int[n]; + for (int i = 0; i < n; i++) { + ps[i] = sc.nextInt(); + vs[i] = sc.nextInt(); + } + + int[] dp = new int[W + 1]; + for (int i = 1; i <= n; i++) { + int p = ps[i - 1], v = vs[ i - 1]; + for (int j = p; j <= W; j++) { + dp[j] = Math.max(dp[j], dp[j - p] + v); + } + } + System.out.println(dp[W]); + } +} + +``` + +## 找亲戚 + +有重复的数字,如{1,1,2} 全排列, 符合亲7数{112, 112}两个 + +```java +import java.util.ArrayList; +import java.util.Scanner; + +public class Main { + static ArrayList ret = new ArrayList<>(); + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + String ss = convertNumber(s); + + char[] chars = ss.toCharArray(); + dfs(chars, new boolean[chars.length], new StringBuilder()); + int cnt = 0; + for (String s1 : ret) { + int num = Integer.parseInt(s1); + if (num % 7 == 0) + cnt++; + } + System.out.println(cnt); + } + + public static void dfs(char[] chars, boolean[] marked, StringBuilder s){ + if (s.length() == chars.length){ + ret.add(s.toString()); + return; + } + for (int i = 0; i < marked.length; i++) { + if (marked[i]) + continue; + + marked[i] = true; + s.append(chars[i]); + dfs(chars, marked, s); + marked[i] = false; + s.deleteCharAt(s.length() - 1); + } + } + + public static String convertNumber(String s){ + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) >= '0' && s.charAt(i) <= '9') + sb.append(s.charAt(i)); + } + return sb.toString(); + } +} +``` + +## 两种排序方法 + +[https://www.nowcoder.com/practice/839f681bf36c486fbcc5fcb977ffe432?tpId=122&&tqId=33666&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/839f681bf36c486fbcc5fcb977ffe432?tpId=122&&tqId=33666&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Scanner; + +public class Main { + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + sc.nextLine(); + boolean lenFlag = true; + List v = new ArrayList<>(); + for (int i = 0; i < n; i++) { + v.add(sc.nextLine()); + } + List p = new ArrayList<>(v); + Collections.sort(p); + boolean dictFlag = p.equals(v); + for (int i = 1; i < n; i++) { + if (v.get(i).length() < v.get(i - 1).length()){ + lenFlag = false; + break; + } + } + + if (dictFlag && lenFlag) + System.out.println("both"); + else if (lenFlag) + System.out.println("lengths"); + else if (dictFlag) + System.out.println("lexicographically"); + else + System.out.println("none"); + } +} + +``` + +## 统计回文 + +[https://www.nowcoder.com/practice/9d1559511b3849deaa71b576fa7009dc?tpId=122&&tqId=33664&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/9d1559511b3849deaa71b576fa7009dc?tpId=122&&tqId=33664&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String a = sc.nextLine(); + String b = sc.nextLine(); + int cnt = 0; + for (int i = 0; i < a.length(); i++) { + String s = a.substring(0, i + 1) + b + a.substring(i + 1); + if (isSym(s)) + cnt++; + } + if (isSym(b+a)) + cnt++; + System.out.println(cnt); + } + + public static boolean isSym(String s){ + int l = 0, r = s.length() - 1; + while (l < r){ + if (s.charAt(l) != s.charAt(r)){ + return false; + } + l++; + r--; + } + return true; + } +} + +``` + +## 解救小易 + +[https://www.nowcoder.com/practice/cd763d8541fc4243b8d3b967bb6d6b6a?tpId=122&&tqId=33663&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/cd763d8541fc4243b8d3b967bb6d6b6a?tpId=122&&tqId=33663&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[][] nums = new int[n][2]; + for (int i = 0; i < n; i++) { + nums[i][0] = sc.nextInt(); + } + for (int i = 0; i < n; i++) { + nums[i][1] = sc.nextInt(); + } + Arrays.sort(nums, (a, b) -> (a[0] + a[1]) - (b[0] + b[1])); + System.out.println((nums[0][0] - 1) + nums[0][1] - 1); + } +} + +``` + +## 不要二 + +[https://www.nowcoder.com/practice/1183548cd48446b38da501e58d5944eb?tpId=122&&tqId=33662&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/1183548cd48446b38da501e58d5944eb?tpId=122&&tqId=33662&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int w = sc.nextInt(); + int h = sc.nextInt(); + int[][] a = new int[w][h]; + int cnt = 0; + for (int i = 0; i < w; i++) { + for (int j = 0; j < h; j++) { + if (a[i][j] == 0){ + cnt++; + if ((i + 2) < w) + a[i + 2][j] = -1; + if ((j + 2) < h) + a[i][j + 2] = -1; + } + } + } + System.out.println(cnt); + } +} +``` + + + + + +## 最大乘积 + +[https://www.nowcoder.com/practice/5f29c72b1ae14d92b9c3fa03a037ac5f?tpId=158&&tqId=34013&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking](https://www.nowcoder.com/practice/5f29c72b1ae14d92b9c3fa03a037ac5f?tpId=158&&tqId=34013&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + long max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE; + long min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE; + for (int i = 0; i < n; i++) { + int x = sc.nextInt(); + if (x > max1){ + max3 = max2; + max2 = max1; + max1 = x; + } else if (x > max2){ + max3 = max2; + max2 = x; + } else if (x > max3) + max3 = x; + if (x < min1){ + min2 = min1; + min1 = x; + } else if (x < min2) + min2 = x; + } + System.out.println(Math.max(max1 * max2 * max3, max1 * min1 * min2)); + } +} + +``` + +## 六一儿童节 + +[https://www.nowcoder.com/practice/d2dfc62bf1ba42679a0e358c57da9828?tpId=158&&tqId=34015&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking](https://www.nowcoder.com/practice/d2dfc62bf1ba42679a0e358c57da9828?tpId=158&&tqId=34015&rp=1&ru=/ta/exam-pdd&qru=/ta/exam-pdd/question-ranking) + +```java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int h = sc.nextInt(); + int[] hs = new int[h]; + for (int i = 0; i < h; i++) { + hs[i] = sc.nextInt(); + } + int w = sc.nextInt(); + int[] ws = new int[w]; + for (int i = 0; i < w; i++) { + ws[i] = sc.nextInt(); + } + Arrays.sort(hs); + Arrays.sort(ws); + int i = 0, j = 0; + while (i < h && j < w){ + if (hs[i] <= ws[j]) + i++; + j++; + } + System.out.println(i); + } +} + +``` + +## 藏宝图 + +[nowcoder.com/practice/74475ee28edb497c8aa4f8c370f08c30?tpId=122&&tqId=33658&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](nowcoder.com/practice/74475ee28edb497c8aa4f8c370f08c30?tpId=122&&tqId=33658&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Arrays; +import java.util.Scanner; +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + String t = sc.nextLine(); + if (isSeq(s, t)) + System.out.println("Yes"); + else + System.out.println("No"); + } + + public static boolean isSeq(String s, String t){ + if (t.equals(" ")) + return true; + int inx = -1; + for (char c : t.toCharArray()) { + inx = s.indexOf(c, inx + 1); + if (inx == -1) return false; + } + return true; + } + +} +``` + +## 分苹果 + +[https://www.nowcoder.com/practice/a174820de48147d489f64103af152709?tpId=122&&tqId=33656&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/a174820de48147d489f64103af152709?tpId=122&&tqId=33656&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int sum = 0; + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + sum += a[i]; + } + System.out.println(minCount(a, sum)); + } + + public static int minCount(int[] a, int sum){ + if (sum % a.length != 0) + return -1; + int avg = sum / a.length; + int cnt = 0; + for (int i = 0; i < a.length; i++) { + int sub = a[i] - avg; + if (sub > 0){ + if (sub % 2 == 0) + cnt += sub / 2; + else + return -1; + } + } + return cnt; + } +} +``` + +## 混合颜料 + +[https://www.nowcoder.com/practice/5b1116081ee549f882970eca84b4785a?tpId=122&&tqId=33660&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/5b1116081ee549f882970eca84b4785a?tpId=122&&tqId=33660&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + System.out.println(min(a, n)); + } + + public static int min(int[] a, int n){ + for (int i = n - 1; i >= 0; i--) { + Arrays.sort(a); + for (int j = i - 1; j >= 0; j--) { + if ((a[i] ^ a[j]) < a[j]) + a[j] ^= a[i]; + } + } + int cnt = 0; + for (int i : a) { + if (i != 0) + cnt++; + } + return cnt; + } +} +``` + +## 等差数列 + +[https://www.nowcoder.com/practice/e11bc3a213d24fc1989b21a7c8b50c3f?tpId=122&&tqId=33681&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/e11bc3a213d24fc1989b21a7c8b50c3f?tpId=122&&tqId=33681&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + int sum = 0; + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + sum += a[i]; + min = Math.min(min, a[i]); + max = Math.max(max, a[i]); + } + int total = (max + min) * n; + if ((total % 2 == 0) && (sum == total / 2)) + System.out.println("Possible"); + else + System.out.println("Impossible"); + } +} + +``` + +## 独立的小易 + +[https://www.nowcoder.com/practice/a99cdf4e2f44499e80749699cc2ec2b9?tpId=122&&tqId=33684&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/a99cdf4e2f44499e80749699cc2ec2b9?tpId=122&&tqId=33684&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int x = sc.nextInt(); + int f = sc.nextInt(); + int d = sc.nextInt(); + int p = sc.nextInt(); + if ((d - x * f) < 0) { + // 说明 钱不够 + System.out.println(d / x); + } else { + // 说明钱够 + int sub = d - x * f; // 剩下的钱 + int day = f; + System.out.println(day + sub / (x + p)); + } + } +} +``` + +## 被3整除 + +[https://www.nowcoder.com/practice/51dcb4eef6004f6f8f44d927463ad5e8?tpId=122&&tqId=33692&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/51dcb4eef6004f6f8f44d927463ad5e8?tpId=122&&tqId=33692&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + long l = sc.nextInt(); + long r = sc.nextInt(); + long lSum = 0; + for (int i = 1; i <= l; i++) { + lSum += i; + } + long rSum = lSum; + long cnt = rSum % 3 == 0 ? 1 : 0; + for (long i = l + 1; i <= r ; i++) { + rSum += i; + cnt = rSum % 3 == 0 ? cnt + 1 : cnt; + } + System.out.println(cnt); + } +} +``` + +## 彩色的砖块 + +[https://www.nowcoder.com/practice/8c29f4d1bea84d6ba2847e079b7420f7?tpId=122&&tqId=33680&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/8c29f4d1bea84d6ba2847e079b7420f7?tpId=122&&tqId=33680&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.HashSet; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + HashSet set = new HashSet(); + for (int i = 0; i < s.length(); i++) { + set.add(s.charAt(i)); + } + if (set.size() == 2) + System.out.println(2); + else if (set.size() == 1) + System.out.println(1); + else + System.out.println(0); + } +} +``` + +--- + +网易8.8笔试(题目改天整理) + +```java +class Main1 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + int ans = 0; + for (int item : a) { + ans += item / 2; + } + System.out.println(ans); + } +} +``` + +```java +class Main2 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + int[] a = new int[m]; + for (int i = 0; i < m; i++) { + a[i] = sc.nextInt(); + } + List list = solution(a, n); + for (int i = 0; i < list.size(); i++) { + if (i == list.size() - 1) + System.out.print(list.get(i)); + else + System.out.print(list.get(i) + " "); + } + } + + public static List solution(int[] nums, int m){ + int[] a = new int[m + 1]; + for (int i = 1; i <= m; i++) { + a[i - 1] = i; + } + for (int i = 0; i < nums.length; i++) { + if (nums[i] > m) + continue; + a[nums[i] - 1] = 0; + } + int idx = 0; + List list = new ArrayList<>(20); + for (int i = 0; i < nums.length; i++) { + list.add(nums[i]); + } + for (int i = 0; i < a.length; i++) { + if (a[i] == 0) + continue; + int target = a[i]; + while (idx < list.size() && list.get(idx) < target) idx++; + list.add(idx, target); + } + return list; + } +} +``` + + +```java +class Main3 { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int t = sc.nextInt(); + for (int j = 0; j < t; j++) { + int n = sc.nextInt(); + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + Arrays.sort(a); + int sub = 0; + for (int i = 0; i < n; i++) { + int[] copy = Arrays.copyOfRange(a, i, n); + if (!canPartition(copy)){ + sub += a[i]; + } else + break; + } + System.out.println(sub); + } + } + + public static boolean canPartition(int[] nums) { + int sum = computeSum(nums); + if (sum % 2 != 0) + return false; + int W = sum / 2; + boolean[] dp = new boolean[W + 1]; + dp[0] = true; + for (int num : nums) { + for (int i = W; i >= num; i--) { + dp[i] = dp[i] || dp[i - num]; + } + } + return dp[W]; + } + + public static int computeSum(int[] nums){ + int sum = 0; + for (int num : nums) { + sum += num; + } + return sum; + } +} +``` + +```java +import java.util.*; + +public class Main3 { + static Set> marked = new HashSet<>(); + static List listSum = new ArrayList<>(); + static int sum = 0; + public static void main(String[] args) { + int[] nums = {5, 15, 20, 40, 60, 80, 100, 5, 5, 5, 5, 5, 5, 10, 10, 25}; + sum = Arrays.stream(nums).sum(); + Arrays.sort(nums); + for (int i = 2; i <= nums.length; i++) { + dfs(0, nums, new ArrayList(), i); + } + System.out.println(listSum.stream().min(Comparator.comparingInt(Integer::intValue)).get()); + } + + private static void dfs(int start, int[] nums, ArrayListlist, int size) { + if (list.size() == size) { + if (!marked.contains(list)) + marked.add(list); + if (canPartition(list.stream().mapToInt(Integer::valueOf).toArray())) { + listSum.add(sum - list.stream().mapToInt(Integer::valueOf).sum()); + return; + } + } + for (int i = start; i < nums.length; i++) { + list.add(nums[i]); + dfs(i + 1, nums, list, size); + list.remove(list.size() - 1); + } + } + + public static boolean canPartition(int[] nums) { + int sum = Arrays.stream(nums).sum(); + if (sum % 2 != 0) return false; + int w = sum / 2; + boolean[] dp = new boolean[w + 1]; + dp[0] = true; + for (int num : nums) { + for (int i = w; i >= num; i--) { + dp[i] = dp[i] || dp[i - num]; + } + } + return dp[w]; + } + +} +``` + +--- + + +## 疯狂的队列 + +[https://www.nowcoder.com/practice/d996665fbd5e41f89c8d280f84968ee1?tpId=122&&tqId=33686&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/d996665fbd5e41f89c8d280f84968ee1?tpId=122&&tqId=33686&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + System.out.println(maxSub(a)); + } + + public static int maxSub(int[] a) { + Arrays.sort(a); + int n = a.length; + int[] b = new int[n]; + int l = 0, r = n - 1; + int m = n / 2; + int p = m - 1; + while (l <= r) { + if (m < n) + b[m++] = (m - 1) % 2 == 0 ? a[r--] : a[l++]; + if (p >= 0) + b[p--] = (p + 1) % 2 == 0 ? a[r--] : a[l++]; + } + int max = 0; + for (int i = 1; i < n; i++) { + max += Math.abs(b[i] - b[i - 1]); + } + return max; + } +} + +``` + +## 跳石板 + +[https://www.nowcoder.com/practice/4284c8f466814870bae7799a07d49ec8?tpId=122&&tqId=33674&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/4284c8f466814870bae7799a07d49ec8?tpId=122&&tqId=33674&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +// bfs +import java.util.*; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + System.out.println(minSkip(n, m)); + } + + public static int minSkip(int n, int m) { + if (n == m) + return 0; + Map map = new HashMap<>(); + Queue queue = new LinkedList<>(); + map.put(n, 0); + queue.add(n); + while (!queue.isEmpty()) { + int num = queue.poll(); + if (num == m) + return map.get(num); + if (num > m) + continue; + HashSet set = new HashSet<>(); + yueShu(num, set); + for (Integer item : set) { + if (!map.containsKey(num + item)) { + map.put(num + item, map.get(num) + 1); + queue.add(num + item); + } + } + } + return -1; + } + + private static void yueShu(int num, HashSet set) { + for (int i = 2; i <= Math.sqrt(num); i++) { + if (num % i == 0){ + set.add(i); + set.add(num / i); + } + } + } +} +``` + +## 牛牛的背包问题 + +[https://www.nowcoder.com/practice/bf877f837467488692be703735db84e6?tpId=122&&tqId=33698&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/bf877f837467488692be703735db84e6?tpId=122&&tqId=33698&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + static int res = 1; + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + long w = sc.nextInt(); + long[] v = new long[n]; + long sum = 0; + for (int i = 0; i < n; i++) { + v[i] = sc.nextInt(); + sum += v[i]; + } + if (sum <= w) { + System.out.println((int) Math.pow(2, n)); + } else { + dfs(v, 0, w, 0); + System.out.println(res); + } + } + + public static void dfs(long[] v, int idx, long w, long cur) { + if (idx == v.length) + return; + if (v[idx] + cur <= w) { + res++; + dfs(v, idx + 1, w, cur + v[idx]); + } + dfs(v, idx + 1, w, cur); + } +} +``` + +## 交错01串 + +[https://www.nowcoder.com/practice/3fbd8fe929ea4eb3a254c0ed34ac993a?tpId=122&&tqId=33682&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/3fbd8fe929ea4eb3a254c0ed34ac993a?tpId=122&&tqId=33682&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + static int res = 1; + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + if (s.length() == 0 || s.length() == 1) { + System.out.println(0); + return; + } + int len = 1; + int res = 1; + for (int i = 1; i < s.length(); i++) { + if (s.charAt(i) != s.charAt(i - 1)) { + len++; + res = Math.max(len, res); + } else { + len = 1; + } + } + System.out.println(res); + } +} +``` + +## 操作序列 + +[https://www.nowcoder.com/practice/b53bda356a494154b6411d80380295f5?tpId=122&&tqId=33683&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/b53bda356a494154b6411d80380295f5?tpId=122&&tqId=33683&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + if (n == 1) + System.out.println(a[0]); + if (n % 2 == 0) { + for (int i = n - 1; i >= 0; i -=2) + System.out.print(a[i] + " "); + for (int i = 0; i < n - 2; i += 2) + System.out.print(a[i] + " "); + System.out.print(a[n - 2]); + } + else { + for (int i = n - 1; i >= 0; i -= 2) { + System.out.print(a[i] + " "); + } + for (int i = 1; i < n - 2; i += 2) + System.out.print(a[i] + " "); + System.out.print(a[n-2]); + } + } +} + +``` + +## 幸运的袋子 + +[https://www.nowcoder.com/practice/a5190a7c3ec045ce9273beebdfe029ee?tpId=122&&tqId=33661&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/a5190a7c3ec045ce9273beebdfe029ee?tpId=122&&tqId=33661&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = sc.nextInt(); + } + Arrays.sort(a); + System.out.println(dfs(a, 0, 0, 1)); + } + + public static int dfs(int[] a, int start, long sum, long multi) { + int cnt = 0; + for (int i = start; i < a.length; i++) { + sum += a[i]; + multi *= a[i]; + if (sum > multi) + cnt += 1 + dfs(a, i + 1, sum, multi); + else if (a[i] == 1) + cnt += dfs(a, i + 1, sum, multi); + else + break; + sum -= a[i]; + multi /= a[i]; + while (i < a.length - 1 && a[i] == a[i + 1]) i++; + } + return cnt; + } +} +``` + +## 最小众倍数 + +[https://www.nowcoder.com/practice/3e9d7d22b7dd4daab695b795d243315b?tpId=122&&tqId=33701&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking](https://www.nowcoder.com/practice/3e9d7d22b7dd4daab695b795d243315b?tpId=122&&tqId=33701&rp=1&ru=/ta/exam-wangyi&qru=/ta/exam-wangyi/question-ranking) + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int[] a = new int[5]; + for (int i = 0; i < 5; i++) { + a[i] = sc.nextInt(); + } + int c = 1; + boolean flag = false; + while (!flag){ + int cnt = 0; + for (int i = 0; i < 5; i++) { + if (c / a[i] != 0 && c % a[i] == 0) { + cnt++; + if (cnt == 3) { + flag = true; + break; + } + } + } + c++; + } + System.out.println(c-1); + } +} +``` diff --git a/Java/bus/README.md b/Java/bus/README.md new file mode 100644 index 00000000..7e2ec400 --- /dev/null +++ b/Java/bus/README.md @@ -0,0 +1,12 @@ +- 项目地址:[微服务班车在线预约系统](https://github.com/DreamCats/SchoolBus) +- [环境搭建文档](环境搭建文档.md) +- [Redis绑定Token分析文档](Redis绑定Token.md) +- [用户服务所有接口分析文档](用户服务.md) +- [班车服务所有接口分析文档](班车服务.md) +- [订单服务所有接口分析文档](订单服务.md) +- [支付服务所有接口分析文档](支付服务.md) +- [添加订单、支付和退款的业务结合消息队列](RocketMQ最终一致性.md) +- [Redis的key过期事件结合自动取消订单业务](Redis的key过期事件.md) +- [SQL语句调优](业务逻辑SQL语句.md) +- [Zookeeper的bug之一](上线遇到的bug.md) + diff --git "a/Java/bus/Redis\347\232\204key\350\277\207\346\234\237\344\272\213\344\273\266.md" "b/Java/bus/Redis\347\232\204key\350\277\207\346\234\237\344\272\213\344\273\266.md" new file mode 100644 index 00000000..60dd5547 --- /dev/null +++ "b/Java/bus/Redis\347\232\204key\350\277\207\346\234\237\344\272\213\344\273\266.md" @@ -0,0 +1,363 @@ +# RocketMQ + +> 该项目主要在添加订单,支付和回退的时候采用了RocketMQ消息中间件。 目的是系统发生异常时,保持系统的最终一致性。 + +## MQTags +> 异常种类 +```java +public enum MqTags { + ORDER_CANCEL("order_cancel", "订单取消异常"), + ORDER_SEATS_CANCEL("order_seats_cancel", "判断座位异常"), + ORDER_ADD_SEATS_CANCLE("order_add_seats_cancle", "更新座位异常"), + ORDER_CALC_MONEY_CANCLE("order_calc_money_cancle", "计算总金额异常"), + ORDER_ADD_CANCLE("order_add_cancle", "添加订单异常"), + PAY_CANCLE("pay_cancle", "支付异常"), + PAY_CHECK_CANCLE("pay_check_cancle", "校验支付密码和余额"), + PAY_MONEY_CANCLE("pay_money_cancle", "支付余额写入异常"), + ; + private String tag; + private String message; + + + MqTags(String tag, String message) { + this.tag = tag; + this.message = message; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} +``` + +## 添加订单 +### addOrder +```java + /** + * 添加订单,这里比较重要 + * @param request + * @return + */ + @Override + public AddOrderResponse addOrder(AddOrderRequest request) { + // 判断座位,如果重复,直接退出,否则更新场次的座位信息 + AddOrderResponse response = new AddOrderResponse(); + // 全局orderId + Long orderId = UUIDUtils.flakesUUID(); + // 1。 判断座位,如果重复,直接退出,否则下一步 + // 2。 更新座位,如果没有异常,这是写操作 + // 3。 计算总金额,如果没有异常 + // 4。 添加订单,如果异常,这是写操作 + try { + // 1。 判断座位,如果重复,直接退出,否则下一步 + tag = MqTags.ORDER_SEATS_CANCEL.getTag(); + boolean repeatSeats = busService.repeatSeats(request.getSeatsIds(), request.getCountId()); + if (repeatSeats) { + // b:true 说明重复 + response.setCode(SbCode.SELECTED_SEATS.getCode()); + response.setMsg(SbCode.SELECTED_SEATS.getMessage()); + return response; + } +// CastException.cast(SbCode.SYSTEM_ERROR); + // 2。 更新座位,如果没有异常,这是写操作 + // 用tags来过滤消息 + tag = MqTags.ORDER_ADD_SEATS_CANCLE.getTag(); + boolean addSeats = busService.addSeats(request.getSeatsIds(), request.getCountId()); + if (!addSeats) { + response.setCode(SbCode.DB_EXCEPTION.getCode()); + response.setMsg(SbCode.DB_EXCEPTION.getMessage()); + return response; + } + // 模拟系统异常 +// CastException.cast(SbCode.SYSTEM_ERROR); + // 3。 计算总金额,如果没有异常 + tag = MqTags.ORDER_CALC_MONEY_CANCLE.getTag(); + String seatIds = request.getSeatsIds(); + Integer seatNumber = seatIds.split(",").length; + Double countPrice = request.getCountPrice(); + Double totalPrice = getTotalPrice(seatNumber, countPrice); + +// CastException.cast(SbCode.SYSTEM_ERROR); + // 4。 添加订单,如果异常,这是写操作 + Order order = orderConvertver.res2Order(request); + order.setOrderPrice(totalPrice); + order.setEvaluateStatus("0"); // 未评价 + order.setOrderStatus("0"); // 未支付 + order.setUuid(orderId); // 唯一id + tag = MqTags.ORDER_ADD_CANCLE.getTag(); + int insert = orderMapper.insert(order);// 插入 不判断了 +// CastException.cast(SbCode.SYSTEM_ERROR); + // 这里就不读了,耗时 +// QueryWrapper wrapper = new QueryWrapper<>(); +// wrapper.eq("so.uuid", order.getUuid()); +// OrderDto orderDto = orderMapper.selectOrderById(wrapper); + response.setCode(SbCode.SUCCESS.getCode()); + response.setMsg(SbCode.SUCCESS.getMessage()); + response.setOrderId(orderId); +// response.setOrderDto(orderDto); + // 这里放redis 未支付缓存,时间前端给定 + redisUtils.set(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + orderId, orderId, request.getExpireTime()); + return response; + } catch (Exception e) { + // 以上操作如果程序都不发生异常的话, 是不会执行这里的代码的 + // 也就是说不会发送回退消息的。 + // 目的是在高并发的情况下,程序内部发生异常,依然高可用 +// e.printStackTrace(); + log.error("订单业务发生异常"); + // 发消息,将座位退回,将订单退回 + MQDto mqDto = new MQDto(); + mqDto.setOrderId(orderId); + mqDto.setCountId(request.getCountId()); + mqDto.setSeatsIds(request.getSeatsIds()); + try { + String key = RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(orderId); + sendCancelOrder(topic,tag, key, JSON.toJSONString(mqDto)); + log.warn("订单回退消息发送成功..." + mqDto); + } catch (Exception ex) { + ex.printStackTrace(); + } + response.setCode(SbCode.SYSTEM_ERROR.getCode()); + response.setMsg(SbCode.SYSTEM_ERROR.getMessage()); + return response; + } + } +``` + +### sendCancelOrder +```java + /** + * 发送订单回退消息 + * @param topic + * @param tag + * @param keys + * @param body + * @throws Exception + */ + private void sendCancelOrder(String topic, String tag, String keys, String body) throws Exception{ + // 封装消息 + Message message = new Message(topic,tag,keys,body.getBytes()); + // 消息生产者发送消息,默认用自带的消息生产者 + rocketMQTemplate.getProducer().send(message); + } +``` + +### OrderSeatsCancleListener(座位异常) +```java + /** + * 回退座位 + * @param messageExt + */ + @Override + public void onMessage(MessageExt messageExt) { + try { + // 1. 解析消息 + String tags = messageExt.getTags(); + String orderTag = MqTags.ORDER_SEATS_CANCEL.getTag(); + // 过滤标签 + if (tags.equals(orderTag)) { + return; + } + String key = messageExt.getKeys(); + System.out.println("取消订单消息:" + key); + // Redis保持消费幂等性 + if (!redisUtils.hasKey(key)) { + String body = new String(messageExt.getBody(), "UTF-8"); + log.warn("收到订单服务异常:" + body); + MQDto mqDto = JSON.parseObject(body, MQDto.class); + // 判断需要的值在不在 + if (mqDto.getCountId() != null && mqDto.getSeatsIds() != null) { + // 2. 调用业务,回退座位 + boolean b = busService.filterRepeatSeats(mqDto.getSeatsIds(), mqDto.getCountId()); + if (b) { + log.warn("回退座位成功"); + redisUtils.set(key, mqDto.getOrderId(), RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getTime()); + } + } + } + } catch (UnsupportedEncodingException e) { + log.error("座位回退程序崩了...好好检查程序吧", e); + } + } +``` + +### OrderAddCancleListener(订单异常) +```java + /** + * 取消订单 + * @param messageExt + */ + @Override + public void onMessage(MessageExt messageExt) { + try { + // 1. 解析消息 + String tags = messageExt.getTags(); + String orderAddTag = MqTags.ORDER_ADD_CANCLE.getTag(); + // 过滤标签 + if (!tags.equals(orderAddTag)) { + return; + } + String key = messageExt.getKeys(); + // Redis保持消费幂等性 + if (!redisUtils.hasKey(key)) { + String body = new String(messageExt.getBody(), "UTF-8"); + log.warn("收到订单服务异常:" + body); + MQDto mqDto = JSON.parseObject(body, MQDto.class); + if (mqDto.getOrderId() != null) { + // 2. 程序异常或者系统内部异常导致的订单,因此我认为删除该订单。 + // 该订单有可能没有插入成功程序就异常了。 + orderService.deleteOrderById(mqDto.getOrderId()); + log.warn("异常订单已删除"); + redisUtils.set(key, mqDto.getOrderId(), RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getTime()); + } + } + } catch (UnsupportedEncodingException e) { + log.error("订单消费信息程序崩...", e); + } + } +``` + +## 支付 +### pay +```java + /** + * 支付业务逻辑 + * @param requset + * @return + */ + @Override + public PayResponse pay(PayRequset requset) { + PayResponse payResponse = new PayResponse(); + Long userId = requset.getUserId(); + Double userMoney = null; + try { + // 1. 先核对支付密码是否正确 + tag = MqTags.PAY_CHECK_CANCLE.getTag(); + String key = RedisConstants.USER_INFO_EXPIRE.getKey() + userId; + UserResponse userResponse = new UserResponse(); + if (redisUtils.hasKey(key)) { + userResponse = (UserResponse) redisUtils.get(key); + } else { + UserRequest request = new UserRequest(); + request.setId(userId); + // 获取用户信息 + userResponse = userService.getUserById(request); + } + + // 支付密码不对 + if (!userResponse.getUserDto().getPayPassword().equals(requset.getPayPassword())) { + payResponse.setCode(SbCode.PAY_PASSWORD_ERROR.getCode()); + payResponse.setMsg(SbCode.PAY_PASSWORD_ERROR.getMessage()); + return payResponse; + } + // 2。 核对余额是否够 + userMoney = userResponse.getUserDto().getMoney(); + Double subMoney = NumberUtil.sub(userMoney, requset.getTotalMoney()); + BigDecimal round = NumberUtil.round(subMoney, 2); + if (round.doubleValue() < 0) { + payResponse.setCode(SbCode.MONEY_ERROR.getCode()); + payResponse.setMsg(SbCode.MONEY_ERROR.getMessage()); + return payResponse; + } + // 3。 够,就写入 + UserUpdateInfoRequest request = new UserUpdateInfoRequest(); + request.setId(userId); + request.setMoney(round.doubleValue()); + tag = MqTags.PAY_MONEY_CANCLE.getTag(); + userService.updateUserInfo(request); // 暂时先不接受返回信息 + // 模拟异常 +// CastException.cast(SbCode.SYSTEM_ERROR); + payResponse.setCode(SbCode.SUCCESS.getCode()); + payResponse.setMsg(SbCode.SUCCESS.getMessage()); + // 4. 按道理讲,这边更改订单状态...... + return payResponse; + } catch (Exception e) { + log.error("支付业务发生异常"); + MQDto mqDto = new MQDto(); + mqDto.setUserId(userId); + mqDto.setUserMoney(userMoney); + // 发送消息 + try { + String key = RedisConstants.PAY_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(userId); + sendCancelPay(topic,tag,key, JSON.toJSONString(mqDto)); + log.warn("支付回退消息已发送"); + } catch (Exception ex) { + ex.printStackTrace(); + log.error("支付消息都崩的话..."); + } + payResponse.setCode(SbCode.SYSTEM_ERROR.getCode()); + payResponse.setMsg(SbCode.SYSTEM_ERROR.getMessage()); + return payResponse; + } + + } +``` +### sendCancelPay +```java + /** + * 发送支付消息 + * @param topic + * @param tag + * @param keys + * @param body + * @throws Exception + */ + private void sendCancelPay(String topic, String tag, String keys, String body) throws Exception { + Message message = new Message(topic,tag,keys,body.getBytes()); + rocketMQTemplate.getProducer().send(message); + } +``` + +### PayMoneyCancleListener +```java + /** + * 支付金额异常 + * @param messageExt + */ + @Override + public void onMessage(MessageExt messageExt) { + try { + // 1. 解析消息 + String tags = messageExt.getTags(); + String payCancleTag = MqTags.PAY_MONEY_CANCLE.getTag(); + if (!tags.equals(payCancleTag)) { + return; + } + // 2. 拿到key + String key = messageExt.getKeys(); + if (!redisUtils.hasKey(key)) { + String body = new String(messageExt.getBody(), "UTF-8"); + log.warn("收到订单服务异常:" + body); + MQDto mqDto = JSON.parseObject(body, MQDto.class); + if (mqDto.getUserId() != null && mqDto.getUserMoney() != null) { + UserUpdateInfoRequest request = new UserUpdateInfoRequest(); + request.setId(mqDto.getUserId()); + request.setMoney(mqDto.getUserMoney()); + userService.updateUserInfo(request); + log.warn("余额已恢复"); + redisUtils.set(key, mqDto.getUserId(), RedisConstants.PAY_EXCEPTION_CANCLE_EXPIRE.getTime()); + } + } + } catch (Exception e) { + log.error("支付消费信息程序崩...\n", e); + } + } +``` + +## 回退 +### payBack +> 这里可以写的,我省了... + +## 再谈 +关于项目中只是采用了RocketMQ维持了系统的最终一致性,其他的优点,限流等都没有用上,也可以用上的。以上流程图我也没时间画,来不及。 \ No newline at end of file diff --git "a/Java/bus/Redis\347\273\221\345\256\232Token.md" "b/Java/bus/Redis\347\273\221\345\256\232Token.md" new file mode 100644 index 00000000..550fbd30 --- /dev/null +++ "b/Java/bus/Redis\347\273\221\345\256\232Token.md" @@ -0,0 +1,201 @@ +# Redis绑定Token +> 先介绍一下概念 + +## cookie +HTTP 协议是**无状态的**,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 + +Cookie 是服务器发送到用户浏览器并保存在**本地的一小块数据**,它会在浏览器之后向同一服务器**再次发起请求时被携带上**,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 + +### 用途 +- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) +- 个性化设置(如用户自定义设置、主题等) +- 浏览器行为跟踪(如跟踪分析用户行为等) + +## session +除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 + +Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 + +使用 Session 维护用户登录状态的过程如下: +1. 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; +2. 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; +3. 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; +4. 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 + +> 注意:Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 + +### session和cookie选择 +- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session; +- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; +- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 + +## Token +实际上,Token是在服务端将用户信息经过Base64Url编码过后传给在客户端。每次用户请求的时候都会带上这一段信息,因此服务端拿到此信息进行解密后就知道此用户是谁了,这个方法叫做JWT(Json Web Token)。 + +## JWT +> JWT(json web token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。 + +cookie+session这种模式通常是保存在内存中,而且服务从**单服务到多服务会面临的session共享问题**,随着用户量的增多,开销就会越大。而JWT不是这样的,只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。 + +### 构成 +第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。详情请见[官网](https://jwt.io/introduction/) + +简单说一下 + +#### header +是一个Json对象,描述JWT的元数据,通常是下面这样子的: +```json +{ + "alg": "HS256", + "typ": "JWT" +} +``` +上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。 + + +#### payload +Payload部分也是一个Json对象,用来存放实际需要传输的数据,JWT官方规定了下面几个官方的字段供选用。 + +- iss (issuer):签发人 +- exp (expiration time):过期时间 +- sub (subject):主题 +- aud (audience):受众 +- nbf (Not Before):生效时间 +- iat (Issued At):签发时间 +- jti (JWT ID):编号 + +也可以定义私有属性 + +#### signature +Signature部分是对前面的两部分的数据进行签名,防止数据篡改。 + +首先需要定义一个秘钥,这个秘钥只有服务器才知道,不能泄露给用户,然后使用Header中指定的签名算法(默认情况是HMAC SHA256),算出签名以后将Header、Payload、Signature三部分拼成一个字符串,每个部分用.分割开来,就可以返给用户了。 + +## 在项目中如何写 + +### JwtTokenUtil +这里是使用guns搭建的项目,自带的工具类,我就不详细赘述了,主要是一下我们的业务逻辑,以及如何使用。 + + +### AuthController +> sb-gateway-auth-AuthController + +能请求到AuthContoroller,说明两种情况: +1. 你没有携带token +2. 你token过期了 + +而这个业务处理,是为了生成token + +```java + @ApiOperation(value = "获取token接口", notes = "每调用一次,就会随机生成一串token", response = ResponseData.class) + @RequestMapping(value = "${jwt.auth-path}") + public ResponseData createAuthenticationToken(@Validated AuthRequest authRequest, BindingResult bindingResult) { + //Validated 的作用是抵消了使用很多if判断 + if (bindingResult.hasErrors()) { + for (FieldError fieldError : bindingResult.getFieldErrors()) { + log.error("参数:{}校验失败,原因:{}", fieldError.getField(), fieldError.getDefaultMessage()); + } + return new ResponseUtil<>().setErrorMsg("用户参数设置错误:" + CommonBindingResult.getErrors(bindingResult)); + } + // 获取用户的账号和密码 + UserLoginRequst req = new UserLoginRequst(); + req.setUsername(authRequest.getUserName()); + req.setPassword(authRequest.getPassword()); + // 调用userAPI.login业务获取用户id + UserLoginResponse res = userAPI.login(req); + String userId = "" + res.getUserId(); + if (res.getUserId() != 0) { + // 如果id不等于0,说明账户存 + // 从redis看一下userId在不在 + if (redisUtils.hasKey(userId)) { + // 如果存在,两种情况:1.你在其他登陆,2.别人登陆 + // 删除redis中的键为userId的 + redisUtils.del(userId); + } + // 以下两步,针对于userId生成token + res.setRandomKey(jwtTokenUtil.getRandomKey()); + String token = jwtTokenUtil.generateToken(userId, res.getRandomKey()); + res.setToken(token); + // 写进redis + redisUtils.set(userId, token, RedisConstants.TOKEN_EXPIRE.getTime()); + // 返回给用户 + return new ResponseUtil<>().setData(res); + } else { + // 如果id等于0,那么说明两种情况:1.账号密码错误,2.账户不存在 + return new ResponseUtil<>().setErrorMsg("账号密码错误"); + } + } +``` + +### AuthFilter +> sb-gateway->auth->filter->AuthFileter + +#### 忽略列表 +```java +// 配置忽略列表 +String ignoreUrl = jwtProperties.getIgnoreUrl(); // 获取忽略的url +String[] ignoreUrls = ignoreUrl.split(","); +for(int i=0;i().setData(response1)); + return; + } else { + // 如果存在,去取token + String token = (String) redisUtils.get(userId); + // 判断redis的token和当前的浏览器的token是否一致 + if(!token.equals(authToken)) { + // 如果不相等,说明别人已经登录了,你当前的token无效,需要重新登录,将它替换掉。 + // 比如,两个手机不能同时登录同一个账户的QQ + CommonResponse response1 = new CommonResponse(); + response1.setCode(SbCode.TOKEN_VALID_FAILED.getCode()); + response1.setMsg(SbCode.TOKEN_VALID_FAILED.getMessage()); + RenderUtil.renderJson(response, new ResponseUtil<>().setData(response1)); + return; + } + } + } catch (Exception e) { + // 如果发生异常,所携带的token有问题,防止爬虫造token + CommonResponse response1 = new CommonResponse(); + response1.setCode(SbCode.TOKEN_VALID_FAILED.getCode()); + response1.setMsg(SbCode.TOKEN_VALID_FAILED.getMessage()); + RenderUtil.renderJson(response, new ResponseUtil<>().setData(response1)); + return; + } + } else { + //header没有带Bearer字段 + CommonResponse response1 = new CommonResponse(); + response1.setCode(SbCode.TOKEN_VALID_FAILED.getCode()); + response1.setMsg(SbCode.TOKEN_VALID_FAILED.getMessage()); + RenderUtil.renderJson(response, new ResponseUtil<>().setData(response1)); + return; + } + chain.doFilter(request, response); + } +``` + +**以上流程图,日后补充** \ No newline at end of file diff --git "a/Java/bus/RocketMQ\346\234\200\347\273\210\344\270\200\350\207\264\346\200\247.md" "b/Java/bus/RocketMQ\346\234\200\347\273\210\344\270\200\350\207\264\346\200\247.md" new file mode 100644 index 00000000..60dd5547 --- /dev/null +++ "b/Java/bus/RocketMQ\346\234\200\347\273\210\344\270\200\350\207\264\346\200\247.md" @@ -0,0 +1,363 @@ +# RocketMQ + +> 该项目主要在添加订单,支付和回退的时候采用了RocketMQ消息中间件。 目的是系统发生异常时,保持系统的最终一致性。 + +## MQTags +> 异常种类 +```java +public enum MqTags { + ORDER_CANCEL("order_cancel", "订单取消异常"), + ORDER_SEATS_CANCEL("order_seats_cancel", "判断座位异常"), + ORDER_ADD_SEATS_CANCLE("order_add_seats_cancle", "更新座位异常"), + ORDER_CALC_MONEY_CANCLE("order_calc_money_cancle", "计算总金额异常"), + ORDER_ADD_CANCLE("order_add_cancle", "添加订单异常"), + PAY_CANCLE("pay_cancle", "支付异常"), + PAY_CHECK_CANCLE("pay_check_cancle", "校验支付密码和余额"), + PAY_MONEY_CANCLE("pay_money_cancle", "支付余额写入异常"), + ; + private String tag; + private String message; + + + MqTags(String tag, String message) { + this.tag = tag; + this.message = message; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} +``` + +## 添加订单 +### addOrder +```java + /** + * 添加订单,这里比较重要 + * @param request + * @return + */ + @Override + public AddOrderResponse addOrder(AddOrderRequest request) { + // 判断座位,如果重复,直接退出,否则更新场次的座位信息 + AddOrderResponse response = new AddOrderResponse(); + // 全局orderId + Long orderId = UUIDUtils.flakesUUID(); + // 1。 判断座位,如果重复,直接退出,否则下一步 + // 2。 更新座位,如果没有异常,这是写操作 + // 3。 计算总金额,如果没有异常 + // 4。 添加订单,如果异常,这是写操作 + try { + // 1。 判断座位,如果重复,直接退出,否则下一步 + tag = MqTags.ORDER_SEATS_CANCEL.getTag(); + boolean repeatSeats = busService.repeatSeats(request.getSeatsIds(), request.getCountId()); + if (repeatSeats) { + // b:true 说明重复 + response.setCode(SbCode.SELECTED_SEATS.getCode()); + response.setMsg(SbCode.SELECTED_SEATS.getMessage()); + return response; + } +// CastException.cast(SbCode.SYSTEM_ERROR); + // 2。 更新座位,如果没有异常,这是写操作 + // 用tags来过滤消息 + tag = MqTags.ORDER_ADD_SEATS_CANCLE.getTag(); + boolean addSeats = busService.addSeats(request.getSeatsIds(), request.getCountId()); + if (!addSeats) { + response.setCode(SbCode.DB_EXCEPTION.getCode()); + response.setMsg(SbCode.DB_EXCEPTION.getMessage()); + return response; + } + // 模拟系统异常 +// CastException.cast(SbCode.SYSTEM_ERROR); + // 3。 计算总金额,如果没有异常 + tag = MqTags.ORDER_CALC_MONEY_CANCLE.getTag(); + String seatIds = request.getSeatsIds(); + Integer seatNumber = seatIds.split(",").length; + Double countPrice = request.getCountPrice(); + Double totalPrice = getTotalPrice(seatNumber, countPrice); + +// CastException.cast(SbCode.SYSTEM_ERROR); + // 4。 添加订单,如果异常,这是写操作 + Order order = orderConvertver.res2Order(request); + order.setOrderPrice(totalPrice); + order.setEvaluateStatus("0"); // 未评价 + order.setOrderStatus("0"); // 未支付 + order.setUuid(orderId); // 唯一id + tag = MqTags.ORDER_ADD_CANCLE.getTag(); + int insert = orderMapper.insert(order);// 插入 不判断了 +// CastException.cast(SbCode.SYSTEM_ERROR); + // 这里就不读了,耗时 +// QueryWrapper wrapper = new QueryWrapper<>(); +// wrapper.eq("so.uuid", order.getUuid()); +// OrderDto orderDto = orderMapper.selectOrderById(wrapper); + response.setCode(SbCode.SUCCESS.getCode()); + response.setMsg(SbCode.SUCCESS.getMessage()); + response.setOrderId(orderId); +// response.setOrderDto(orderDto); + // 这里放redis 未支付缓存,时间前端给定 + redisUtils.set(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + orderId, orderId, request.getExpireTime()); + return response; + } catch (Exception e) { + // 以上操作如果程序都不发生异常的话, 是不会执行这里的代码的 + // 也就是说不会发送回退消息的。 + // 目的是在高并发的情况下,程序内部发生异常,依然高可用 +// e.printStackTrace(); + log.error("订单业务发生异常"); + // 发消息,将座位退回,将订单退回 + MQDto mqDto = new MQDto(); + mqDto.setOrderId(orderId); + mqDto.setCountId(request.getCountId()); + mqDto.setSeatsIds(request.getSeatsIds()); + try { + String key = RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(orderId); + sendCancelOrder(topic,tag, key, JSON.toJSONString(mqDto)); + log.warn("订单回退消息发送成功..." + mqDto); + } catch (Exception ex) { + ex.printStackTrace(); + } + response.setCode(SbCode.SYSTEM_ERROR.getCode()); + response.setMsg(SbCode.SYSTEM_ERROR.getMessage()); + return response; + } + } +``` + +### sendCancelOrder +```java + /** + * 发送订单回退消息 + * @param topic + * @param tag + * @param keys + * @param body + * @throws Exception + */ + private void sendCancelOrder(String topic, String tag, String keys, String body) throws Exception{ + // 封装消息 + Message message = new Message(topic,tag,keys,body.getBytes()); + // 消息生产者发送消息,默认用自带的消息生产者 + rocketMQTemplate.getProducer().send(message); + } +``` + +### OrderSeatsCancleListener(座位异常) +```java + /** + * 回退座位 + * @param messageExt + */ + @Override + public void onMessage(MessageExt messageExt) { + try { + // 1. 解析消息 + String tags = messageExt.getTags(); + String orderTag = MqTags.ORDER_SEATS_CANCEL.getTag(); + // 过滤标签 + if (tags.equals(orderTag)) { + return; + } + String key = messageExt.getKeys(); + System.out.println("取消订单消息:" + key); + // Redis保持消费幂等性 + if (!redisUtils.hasKey(key)) { + String body = new String(messageExt.getBody(), "UTF-8"); + log.warn("收到订单服务异常:" + body); + MQDto mqDto = JSON.parseObject(body, MQDto.class); + // 判断需要的值在不在 + if (mqDto.getCountId() != null && mqDto.getSeatsIds() != null) { + // 2. 调用业务,回退座位 + boolean b = busService.filterRepeatSeats(mqDto.getSeatsIds(), mqDto.getCountId()); + if (b) { + log.warn("回退座位成功"); + redisUtils.set(key, mqDto.getOrderId(), RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getTime()); + } + } + } + } catch (UnsupportedEncodingException e) { + log.error("座位回退程序崩了...好好检查程序吧", e); + } + } +``` + +### OrderAddCancleListener(订单异常) +```java + /** + * 取消订单 + * @param messageExt + */ + @Override + public void onMessage(MessageExt messageExt) { + try { + // 1. 解析消息 + String tags = messageExt.getTags(); + String orderAddTag = MqTags.ORDER_ADD_CANCLE.getTag(); + // 过滤标签 + if (!tags.equals(orderAddTag)) { + return; + } + String key = messageExt.getKeys(); + // Redis保持消费幂等性 + if (!redisUtils.hasKey(key)) { + String body = new String(messageExt.getBody(), "UTF-8"); + log.warn("收到订单服务异常:" + body); + MQDto mqDto = JSON.parseObject(body, MQDto.class); + if (mqDto.getOrderId() != null) { + // 2. 程序异常或者系统内部异常导致的订单,因此我认为删除该订单。 + // 该订单有可能没有插入成功程序就异常了。 + orderService.deleteOrderById(mqDto.getOrderId()); + log.warn("异常订单已删除"); + redisUtils.set(key, mqDto.getOrderId(), RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getTime()); + } + } + } catch (UnsupportedEncodingException e) { + log.error("订单消费信息程序崩...", e); + } + } +``` + +## 支付 +### pay +```java + /** + * 支付业务逻辑 + * @param requset + * @return + */ + @Override + public PayResponse pay(PayRequset requset) { + PayResponse payResponse = new PayResponse(); + Long userId = requset.getUserId(); + Double userMoney = null; + try { + // 1. 先核对支付密码是否正确 + tag = MqTags.PAY_CHECK_CANCLE.getTag(); + String key = RedisConstants.USER_INFO_EXPIRE.getKey() + userId; + UserResponse userResponse = new UserResponse(); + if (redisUtils.hasKey(key)) { + userResponse = (UserResponse) redisUtils.get(key); + } else { + UserRequest request = new UserRequest(); + request.setId(userId); + // 获取用户信息 + userResponse = userService.getUserById(request); + } + + // 支付密码不对 + if (!userResponse.getUserDto().getPayPassword().equals(requset.getPayPassword())) { + payResponse.setCode(SbCode.PAY_PASSWORD_ERROR.getCode()); + payResponse.setMsg(SbCode.PAY_PASSWORD_ERROR.getMessage()); + return payResponse; + } + // 2。 核对余额是否够 + userMoney = userResponse.getUserDto().getMoney(); + Double subMoney = NumberUtil.sub(userMoney, requset.getTotalMoney()); + BigDecimal round = NumberUtil.round(subMoney, 2); + if (round.doubleValue() < 0) { + payResponse.setCode(SbCode.MONEY_ERROR.getCode()); + payResponse.setMsg(SbCode.MONEY_ERROR.getMessage()); + return payResponse; + } + // 3。 够,就写入 + UserUpdateInfoRequest request = new UserUpdateInfoRequest(); + request.setId(userId); + request.setMoney(round.doubleValue()); + tag = MqTags.PAY_MONEY_CANCLE.getTag(); + userService.updateUserInfo(request); // 暂时先不接受返回信息 + // 模拟异常 +// CastException.cast(SbCode.SYSTEM_ERROR); + payResponse.setCode(SbCode.SUCCESS.getCode()); + payResponse.setMsg(SbCode.SUCCESS.getMessage()); + // 4. 按道理讲,这边更改订单状态...... + return payResponse; + } catch (Exception e) { + log.error("支付业务发生异常"); + MQDto mqDto = new MQDto(); + mqDto.setUserId(userId); + mqDto.setUserMoney(userMoney); + // 发送消息 + try { + String key = RedisConstants.PAY_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(userId); + sendCancelPay(topic,tag,key, JSON.toJSONString(mqDto)); + log.warn("支付回退消息已发送"); + } catch (Exception ex) { + ex.printStackTrace(); + log.error("支付消息都崩的话..."); + } + payResponse.setCode(SbCode.SYSTEM_ERROR.getCode()); + payResponse.setMsg(SbCode.SYSTEM_ERROR.getMessage()); + return payResponse; + } + + } +``` +### sendCancelPay +```java + /** + * 发送支付消息 + * @param topic + * @param tag + * @param keys + * @param body + * @throws Exception + */ + private void sendCancelPay(String topic, String tag, String keys, String body) throws Exception { + Message message = new Message(topic,tag,keys,body.getBytes()); + rocketMQTemplate.getProducer().send(message); + } +``` + +### PayMoneyCancleListener +```java + /** + * 支付金额异常 + * @param messageExt + */ + @Override + public void onMessage(MessageExt messageExt) { + try { + // 1. 解析消息 + String tags = messageExt.getTags(); + String payCancleTag = MqTags.PAY_MONEY_CANCLE.getTag(); + if (!tags.equals(payCancleTag)) { + return; + } + // 2. 拿到key + String key = messageExt.getKeys(); + if (!redisUtils.hasKey(key)) { + String body = new String(messageExt.getBody(), "UTF-8"); + log.warn("收到订单服务异常:" + body); + MQDto mqDto = JSON.parseObject(body, MQDto.class); + if (mqDto.getUserId() != null && mqDto.getUserMoney() != null) { + UserUpdateInfoRequest request = new UserUpdateInfoRequest(); + request.setId(mqDto.getUserId()); + request.setMoney(mqDto.getUserMoney()); + userService.updateUserInfo(request); + log.warn("余额已恢复"); + redisUtils.set(key, mqDto.getUserId(), RedisConstants.PAY_EXCEPTION_CANCLE_EXPIRE.getTime()); + } + } + } catch (Exception e) { + log.error("支付消费信息程序崩...\n", e); + } + } +``` + +## 回退 +### payBack +> 这里可以写的,我省了... + +## 再谈 +关于项目中只是采用了RocketMQ维持了系统的最终一致性,其他的优点,限流等都没有用上,也可以用上的。以上流程图我也没时间画,来不及。 \ No newline at end of file diff --git "a/Java/bus/\344\270\212\347\272\277\351\201\207\345\210\260\347\232\204bug.md" "b/Java/bus/\344\270\212\347\272\277\351\201\207\345\210\260\347\232\204bug.md" new file mode 100644 index 00000000..dd379ca9 --- /dev/null +++ "b/Java/bus/\344\270\212\347\272\277\351\201\207\345\210\260\347\232\204bug.md" @@ -0,0 +1,200 @@ +## 上线bug + +> 项目上线之后,大概将近一个月,我点击车次列表页面,突然什么都没有了,于是就开始找哪个不顺眼的家伙搞的鬼。 + +### 起因 + +访问主页[http://47.104.22.225:8080/home](http://47.104.22.225:8080/home) + +![](https://imgkr.cn-bj.ufileos.com/8c563c2e-49f8-4a96-8d02-0109bc956449.png) + +如图: + +一直加载中,于是我看一下响应信息。 + +```json +code: 200 +message: "success" +result: {code: "003099", msg: "系统错误"} +success: true +timestamp: "1589715142518" +``` + +### 查看一下gateway的日志 +- 终端显示不长,于是我将日志用vscode打开看 + +```log +org.apache.dubbo.rpc.RpcException: No provider available from registry 39.108.93.119:2181 for service com.stylefeng.guns.rest.bus.IBusService on consumer 192.168.31.221 use dubbo version 2.7.4.1, please check status of providers(disabled, not registered or in blacklist). +``` + +类似于这样的信息,说我们的bus服务没有注册。 + +> 注意:出事之前,几个服务都在的呀,怎么今天bus突然不在了。于是,我不相信,我就去dubbo后台看了一下... + +- 进入文件夹`dubbo-admin -> dubbo-admin-ui` +- 执行`npm run dev` +- 在我目前的mac上chrome浏览器输入`http://dubbo.dreamcat.ink:2020/` + +**此时,还真没有bus服务** +![](https://imgkr.cn-bj.ufileos.com/3eb38910-64b5-439c-8437-c155d50857d2.png) + +**哭晕在厕所**。 + +![](https://imgkr.cn-bj.ufileos.com/e391627b-dce7-4146-b6d7-78cbd1fee35e.jpg) + +**于是乎,俺又不想重新启动服务,毕竟你看** + +- 终端输入`ps -ef | grep guns-bus` +- 我们看到了惊人的一幕 +```shell +pch 2003942 1 0 4月16 ? 06:10:54 java -jar guns-bus-0.0.1.jar +``` + +**又哭晕在厕所...** + +### 猜测 + +#### DubboAdmin展示? +好像没问题吧?这样的话,其他三个也应该不存在的啊 + +#### 注册中心,bus节点丢了 + +- 查看日志,当天出现zk出现了大量的超时,原因是当天的zk**主节点**宕机了。 + +#### 找原因 + +**问题是否出现在了dubbo对zk重连恢复数据这块,开始查源码。注册中心源码ZookeeperRegistry。** + +1. 连接注册zk:通过zkclient添加zk状态监听。并且继承了FailbackRegistry各种失败重试。 + +```java +public class ZookeeperRegistry extends FailbackRegistry { + public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) { + ... + // 1. 连接zk + zkClient = zookeeperTransporter.connect(url); + // 2. 添加zk状态监听 + zkClient.addStateListener(new StateListener() { + @Override + public void stateChanged(int state) { + // 3. 重新连接后恢复动作,将当前的注册服务于订阅任务添加至重试列表中等待重试 + if (state == RECONNECTED) { + try { + recover(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + } + }); + } +} + +``` + +2. zk客户端:默认使用CuratorZookeeperClient实现 + +```java +public class CuratorZookeeperClient extends AbstractZookeeperClient { + public CuratorZookeeperClient(URL url) { + ... + client = builder.build(); + // dubbo对接zk连接状态监听器 + client.getConnectionStateListenable().addListener(new ConnectionStateListener() { + @Override + public void stateChanged(CuratorFramework client, ConnectionState state) { + if (state == ConnectionState.LOST) { + CuratorZookeeperClient.this.stateChanged(StateListener.DISCONNECTED); + } else if (state == ConnectionState.CONNECTED) { + CuratorZookeeperClient.this.stateChanged(StateListener.CONNECTED); + } else if (state == ConnectionState.RECONNECTED) { + CuratorZookeeperClient.this.stateChanged(StateListener.RECONNECTED); + } + } + }); + client.start(); + ... + } +} +``` + +3. 重试任务:注册重新失败重连任务FailbackRegistry中的DubboRegistryFailedRetryTimer,默认5秒检查一次是否需要失败恢复 + +```java +public FailbackRegistry(URL url) { + super(url); + this.retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD); + this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + // Check and connect to the registry + try { + // failedRegistered失败注册重试,failedUnregistered失败注销重试,failedSubscribed失败订阅重试,failedUnsubscribed失败取消订阅重试,failedNotified失败通知重试 + retry(); + } catch (Throwable t) { // Defensive fault tolerance + logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t); + } + } + }, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS); +} + +``` + +#### 总结一波 +通过三部分的代码我们可以推断,如果zk状态监听与恢复部分出现问题可能会导致数据丢失问题。于是查看相关的api并且尝试查看dubbo社区的问题与bug,果然发现了类似问题的修改与原因分析:[https://github.com/apache/dubbo/pull/5135](https://github.com/apache/dubbo/pull/5135) + +原因: +> 如果ZNode数据已经存在,在会话超时期间,此时我们将重建一个数据节点,这个重复的异常原因可能是由于zk server中老的超时会话依然持有节点导致该节点的delete删除事件延迟,并且zk server还没有来得及去执行删除,可能由这种场景引起。在这个情景下,我们可以本地删除节点后再创建恢复节点数据。 + +其实说白了: +> 如果会话断开连接又重新连接成功。断开连接发出的删除节点事件,**因为延迟原因走在了重新连接恢复节点事件的后面**。**导致重新连接后没能成功恢复节点**。也就是我么见到的,provider有三个节点服务正常,但是zk注册中心中的提供者节点数据丢失,导致出现该节点对其他订阅者不可见的现象 + +```java + protected void createEphemeral(String path, String data) { + byte[] dataBytes = data.getBytes(CHARSET); + + try { + ((ACLBackgroundPathAndBytesable)this.client.create().withMode(CreateMode.EPHEMERAL)).forPath(path, dataBytes); + } catch (NodeExistsException var5) { + logger.warn("ZNode " + path + " already exists, since we will only try to recreate a node on a session expiration, this duplication might be caused by a delete delay from the zk server, which means the old expired session may still holds this ZNode and the server just hasn't got time to do the deletion. In this case, we can just try to delete and create again.", var5); + this.deletePath(path); + this.createEphemeral(path, data); + } catch (Exception var6) { + throw new IllegalStateException(var6.getMessage(), var6); + } + + } +``` + +**人家源码的warn写的是真清楚**... + +### 定位cpu过高的线程或者位置 + +> 这里要用到几个命令了,比如top,jstack等。 + +#### 查看一波 +- 终端输入`top -H` + +![](https://imgkr.cn-bj.ufileos.com/e69d6335-c260-4d69-9f4d-840b7427cce5.png) +- 类似于长这样,但是有些含义就不解释了,请上互联网 +- 我就挑它吧`1227027` +- 终端输入`top -Hp 1227027` +![](https://imgkr.cn-bj.ufileos.com/d703c8ae-a3a8-4eed-ad57-6c3440dd4ea7.png) +- 这里我也就不介绍了,找一个`1227029` +- `printf "%x\n" 1227029`结果是:`12b915` +- `jstack -l 1227027| grep 0x12b915 -A 10` + +```shell +"DestroyJavaVM" #84 prio=5 os_prio=0 tid=0x00007f69f800b7c0 nid=0x12b915 waiting on condition [0x0000000000000000] + java.lang.Thread.State: RUNNABLE + + Locked ownable synchronizers: + - None + +"BrokerFastFailureScheduledThread1" #83 prio=5 os_prio=0 tid=0x00007f69f8b29050 nid=0x12ba0a runnable [0x00007f68b6bec000] + java.lang.Thread.State: TIMED_WAITING (parking) + at sun.misc.Unsafe.park(Native Method) + - parking to wait for <0x0000000080594a60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) + at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215) +``` +**以上就是举个例子** ... \ No newline at end of file diff --git "a/Java/bus/\344\270\232\345\212\241\351\200\273\350\276\221SQL\350\257\255\345\217\245.md" "b/Java/bus/\344\270\232\345\212\241\351\200\273\350\276\221SQL\350\257\255\345\217\245.md" new file mode 100644 index 00000000..6ef6d800 --- /dev/null +++ "b/Java/bus/\344\270\232\345\212\241\351\200\273\350\276\221SQL\350\257\255\345\217\245.md" @@ -0,0 +1,211 @@ +## Explain + +> 分别对应: +> +> id select_type table type possible_keys key key_len ref rows Extra +> +> 参考:[https://juejin.im/post/5ec4e4a5e51d45786973b357](https://juejin.im/post/5ec4e4a5e51d45786973b357) + +## 用户服务 + +### checkUsername + +```sql +SELECT + * +FROM + sb_user su +WHERE + user_name = 'mai'; +``` + +- 唯一索引 + +``` +1 "SIMPLE" "su" "const" "user_name" "user_name" "152" "const" 1 "" +``` + +- 普通索引 + +```sql +1 "SIMPLE" "su" "ref" "user_name" "user_name" "152" "const" 1 "Using index condition" +``` + +- 不加索引 + +```sql +1 "SIMPLE" "su" "ALL" NULL NULL NULL NULL 11 "Using where" +``` + +### register省略 + +### login省略 + +### getUserById省略 + +### updateUserInfo省略 + +## 班车服务 + +### getBus省略 + +### getCount + +```sql +SELECT + sc.uuid, + sc.begin_date, + sc.begin_time, + sc.bus_id, + sc.bus_status, + sc.seat_status +FROM + sb_count sc +WHERE + sc.begin_date = '2020-06-03' + AND sc.begin_time >= '14:00' + AND sc.bus_status = '1'; +``` + +- 不加索引 + +``` +1 "SIMPLE" "sc" "ALL" NULL NULL NULL NULL 581 "Using where" + +53ms +``` + +- 给begin_date加普通索引 + +``` +1 "SIMPLE" "sc" "ref" "begin_date" "begin_date" "402" "const" 17 "Using index condition; Using where" + +53ms // 数据太少,体现不出来... +``` + +- 给begin_time加普通索引 + +``` +1 "SIMPLE" "sc" "ref" "begin_date,begin_time" "begin_date" "402" "const" 17 "Using index condition; Using where" + +53ms +``` + +- 给bus_status加普通索引 + +``` +1 "SIMPLE" "sc" "index_merge" "begin_date,begin_time,bus_status" "begin_date,bus_status" "402,152" NULL 7 "Using intersect(begin_date,bus_status); Using where" + +53ms +``` + +### getCountDetailById省略 + +## 订单服务 + +### getNoTakeOrdersById + +```sql +SELECT + so.uuid, + so.bus_status, + so.seats_ids, + so.order_user, + so.order_status, + so.order_time, + sc.bus_id, + CONCAT(sc.begin_date, ' ', sc.begin_time) begin_time +FROM + sb_order so + LEFT JOIN sb_count sc ON so.count_id = sc.uuid +WHERE + so.user_id = '4' + AND so.order_status = '1' + AND sc.begin_date >= '2020-06-03' + AND sc.begin_time >= '15:00'; +``` + +- 不加普通索引 + +``` +1 "SIMPLE" "so" "ALL" NULL NULL NULL NULL 45 "Using where" +1 "SIMPLE" "sc" "eq_ref" "PRIMARY,begin_date,begin_time" "PRIMARY" "8" "school_bus.so.count_id" 1 "Using where" +``` + +- user_id+order_status加普通索引 + +``` +1 "SIMPLE" "so" "ref" "order_status,user_id" "order_status" "152" "const" 10 "Using index condition; Using where" +1 "SIMPLE" "sc" "eq_ref" "PRIMARY,begin_date,begin_time" "PRIMARY" "8" "school_bus.so.count_id" 1 "Using where" +``` + +### getEvaluateOrdersById + +```sql +SELECT + so.uuid, + so.bus_status, + so.seats_ids, + so.order_user, + so.order_time, + sc.bus_id, + so.evaluate_status, + so.comment, + CONCAT(sc.begin_date, ' ', sc.begin_time) begin_time +FROM + sb_order so + LEFT JOIN sb_count sc ON so.count_id = sc.uuid +WHERE + so.user_id = 4 + AND so.evaluate_status = "1" + AND so.order_status = "1" + AND (sc.begin_date = "2020-05-30" AND sc.begin_time < "21:00" OR sc.begin_date < "2020-05-30") + +``` + +- 带了普通索引 + +``` +1 "SIMPLE" "so" "ref" "user_id_order_eval_status" "user_id_order_eval_status" "312" "const,const,const" 1 "Using index condition" +1 "SIMPLE" "sc" "eq_ref" "PRIMARY" "PRIMARY" "8" "school_bus.so.count_id" 1 "Using where" +``` + +### getNoPayOrdersById + +```sql +SELECT + so.uuid, + so.bus_status, + so.seats_ids, + so.order_user, + so.order_status, + so.order_time, + sc.bus_id, + CONCAT(sc.begin_date, ' ', sc.begin_time) begin_time +FROM + sb_order so + LEFT JOIN sb_count sc ON so.count_id = sc.uuid +WHERE + so.user_id = '4' + AND so.order_status = '0' + AND sc.begin_date >= '2020-06-03' + AND sc.begin_time >= '20:00'; +``` + +- 带了普通索引 + +``` +1 "SIMPLE" "so" "ref" "order_status,user_id" "order_status" "152" "const" 8 "Using index condition; Using where" +1 "SIMPLE" "sc" "eq_ref" "PRIMARY,begin_date,begin_time" "PRIMARY" "8" "school_bus.so.count_id" 1 "Using where" +``` + +### addOrder省略 + +### selectOrderById省略 + +### updateOrderStatus省略 + +### deleteOrderById省略 + +## 支付服务省略 + diff --git "a/Java/bus/\346\224\257\344\273\230\346\234\215\345\212\241.md" "b/Java/bus/\346\224\257\344\273\230\346\234\215\345\212\241.md" new file mode 100644 index 00000000..4f1bfb8b --- /dev/null +++ "b/Java/bus/\346\224\257\344\273\230\346\234\215\345\212\241.md" @@ -0,0 +1,141 @@ +# 支付服务 + +## PayController + +### pay +> 省略 + +### payBack +> 退款 +```java +// 删除个人详细信息缓存 +if (redisUtils.hasKey(userInfoKey)) { + redisUtils.del(userInfoKey); +} +// 删除未支付订单列表缓存 +if (redisUtils.hasKey(noTakeKey)) { + redisUtils.del(noTakeKey); +} +// 删除订单详情缓存 +if (redisUtils.hasKey(selectOrderKey)) { + redisUtils.del(selectOrderKey); +} +// 删除场次详情缓存 +if (redisUtils.hasKey(countDetailKey)) { + redisUtils.del(countDetailKey); +} +``` + +## PayServiceImpl + +### pay +```java +public PayResponse pay(PayRequset requset) { + PayResponse payResponse = new PayResponse(); + Long userId = requset.getUserId(); + Double userMoney = null; + try { + // 1. 先核对支付密码是否正确 + tag = MqTags.PAY_CHECK_CANCLE.getTag(); + String key = RedisConstants.USER_INFO_EXPIRE.getKey() + userId; + UserResponse userResponse = new UserResponse(); + if (redisUtils.hasKey(key)) { + userResponse = (UserResponse) redisUtils.get(key); + } else { + UserRequest request = new UserRequest(); + request.setId(userId); + // 获取用户信息 + userResponse = userService.getUserById(request); + } + + // 支付密码不对 + if (!userResponse.getUserDto().getPayPassword().equals(requset.getPayPassword())) { + payResponse.setCode(SbCode.PAY_PASSWORD_ERROR.getCode()); + payResponse.setMsg(SbCode.PAY_PASSWORD_ERROR.getMessage()); + return payResponse; + } + // 2。 核对余额是否够 + userMoney = userResponse.getUserDto().getMoney(); + Double subMoney = NumberUtil.sub(userMoney, requset.getTotalMoney()); + BigDecimal round = NumberUtil.round(subMoney, 2); + if (round.doubleValue() < 0) { + payResponse.setCode(SbCode.MONEY_ERROR.getCode()); + payResponse.setMsg(SbCode.MONEY_ERROR.getMessage()); + return payResponse; + } + // 3。 够,就写入 + UserUpdateInfoRequest request = new UserUpdateInfoRequest(); + request.setId(userId); + request.setMoney(round.doubleValue()); + tag = MqTags.PAY_MONEY_CANCLE.getTag(); + userService.updateUserInfo(request); // 暂时先不接受返回信息 + // 模拟异常 +// CastException.cast(SbCode.SYSTEM_ERROR); + payResponse.setCode(SbCode.SUCCESS.getCode()); + payResponse.setMsg(SbCode.SUCCESS.getMessage()); + // 4. 按道理讲,这边更改订单状态...... + return payResponse; + } catch (Exception e) { + log.error("支付业务发生异常"); + MQDto mqDto = new MQDto(); + mqDto.setUserId(userId); + mqDto.setUserMoney(userMoney); + // 发送消息 + try { + String key = RedisConstants.PAY_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(userId); + sendCancelPay(topic,tag,key, JSON.toJSONString(mqDto)); + log.warn("支付回退消息已发送"); + } catch (Exception ex) { + ex.printStackTrace(); + log.error("支付消息都崩的话..."); + } + payResponse.setCode(SbCode.SYSTEM_ERROR.getCode()); + payResponse.setMsg(SbCode.SYSTEM_ERROR.getMessage()); + return payResponse; + } + +} +``` + +### payBack + +```java +public PayResponse payBack(PayBackRequest request) { + PayResponse response = new PayResponse(); + try { + // 1. 退回金额 + // 读用户金额 + UserRequest userRequest = new UserRequest(); + userRequest.setId(request.getUserId()); + UserResponse userResponse = userService.getUserById(userRequest); + UserDto userDto = userResponse.getUserDto(); + // 计算金额 + BigDecimal add = NumberUtil.add(userDto.getMoney() + request.getTotalMoney()); + BigDecimal round = NumberUtil.round(add, 2); + // 写回 + UserUpdateInfoRequest userUpdateInfoRequest = new UserUpdateInfoRequest(); + userUpdateInfoRequest.setId(request.getUserId()); + userUpdateInfoRequest.setMoney(round.doubleValue()); + userService.updateUserInfo(userUpdateInfoRequest); + // 2. 退回座位 + busService.filterRepeatSeats(request.getSeatsIds(), request.getCoundId()); + // 3. 更改订单状态:关闭 + OrderRequest orderRequest = new OrderRequest(); + orderRequest.setUuid(request.getOrderId()); + orderRequest.setOrderStatus("3"); + orderService.updateOrderStatus(orderRequest); + response.setCode(SbCode.SUCCESS.getCode()); + response.setMsg(SbCode.SUCCESS.getMessage()); + return response; + } catch (Exception e) { +// e.printStackTrace(); + log.error("退款业务异常"); + // 这里可以发消息, 此处先省略 + response.setCode(SbCode.SYSTEM_ERROR.getCode()); + response.setMsg(SbCode.SYSTEM_ERROR.getMessage()); + return response; + } +} +``` + +这两个业务逻辑异常都采用了消息队列,日后再写。 \ No newline at end of file diff --git "a/Java/bus/\347\216\257\345\242\203\346\220\255\345\273\272\346\226\207\346\241\243.md" "b/Java/bus/\347\216\257\345\242\203\346\220\255\345\273\272\346\226\207\346\241\243.md" new file mode 100644 index 00000000..e6839718 --- /dev/null +++ "b/Java/bus/\347\216\257\345\242\203\346\220\255\345\273\272\346\226\207\346\241\243.md" @@ -0,0 +1,64 @@ +# 项目环境搭建 +> 主要搭建的技术:Java、MySQL、Redis、zookeeper、Dubbo、RocketMQ + +## 引言 +首先说明,MySQL和Redis放到了我的阿里云服务器,负载并不大,其他全部服务搭建在实验室的服务器主机上,并采用内网穿透映射端口,让大家访问,大家注意一点哈。请谅解。 + +还有,本项目是对本校的班车预约服务的架构进行了重构,采用的比较热门的技术,如README所示。 + +## 技术选型 + + +![选型介绍图](https://imgkr.cn-bj.ufileos.com/56250623-c12e-4440-abe9-bc0277c4e3be.png) + +从选型图上可以清晰的看到: +- **Web层**:sb-gateway网关,控制整个项目的请求分发与转接等。 +- **接口层**:sb-api定义各个服务的接口,比如用户、班车、订单等。 +- **服务层**:有着四大服务,分别如上图所示,用户、班车、订单和支付,并且其中均采用第三方中间件Zookeeper、Dubbo和RocketMQ等。 +- **存储层**,实际上,MySQL作为存储,而Redis做为MySQL上一层的缓存层 + +## 架构路线 + + +![架构路线图](https://imgkr.cn-bj.ufileos.com/18606af1-be51-44ce-9c06-f2abff84a294.png) + +首先,一条请求从用户移动端或者PC端发出,经过前端的Nginx的代理转发到sb-gateway的网关,网关进一步的将请求进行路由分发到对应的Controller,从而执行相应的业务逻辑,其中对应的Controller中分为两步,第一步:找缓存,若存在,直接返回,若不存在。第二步:业务逻辑方法向Dubbo请求远程调用(RPC),得到结果,另外一边业务执行相应的底层如MySQL等,将结果返回给网关,并写入缓存等。 + +## 环境端口 + + +![端口定义](https://imgkr.cn-bj.ufileos.com/9a949053-2417-468a-b7cb-0490c5c78b51.png) + + +![环境启动](https://imgkr.cn-bj.ufileos.com/53607d10-75a7-47bc-815c-adbeef4193f5.png) + + + +## Java +> 我在博客上写好了,可以参考。[http://dreamcat.ink/2019/07/08/windows-mac-he-linux-an-zhuang-java/](http://dreamcat.ink/2019/07/08/windows-mac-he-linux-an-zhuang-java/) + +## MySQL +> 我也在博客上写好了,可以参考,项目采用的MySQL8版本。[http://dreamcat.ink/2019/05/27/windows-mac-he-linux-an-zhuang-mysql/](http://dreamcat.ink/2019/05/27/windows-mac-he-linux-an-zhuang-mysql/) + +## Redis +> 我还在博客上写好了,可以参考。[http://dreamcat.ink/2019/05/28/windows-mac-he-linux-an-zhuang-redis/](http://dreamcat.ink/2019/05/28/windows-mac-he-linux-an-zhuang-redis/) + +## zookeeper +- [下载地址](https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/) +- 选择3.4.14版本下载即可,放进和刚才的myapps下。 +- 解压:tar -zxf zookeeper-3.4.14.tar.gz +- 将配置文件cp -r zoo_sample.cfg zoo.cfg +- 启动zookeeper./zkServer.sh start + +## Dubbo +- [采用的是最新版本](https://github.com/apache/dubbo-admin) +- 下载解压后修配置文件 `dubbo-admin-server/src/main/resources/application.properties` +- 配置文件修改`zookeeper`地址,dubbo控制台端口默认8080,(参考端口图) +- 可以修改为其他端口例如 server.port=9898,以免与其他服务端口冲突。(参考端口图) +- 在主目录dubbo-admin-server目录下,执行 `mvn clean package -Dmaven.test.skip=true` +- `cd dubbo-admin-server/target java -jar dubbo-admin-0.1.jar` 启动即可 +- 最新版本采用前后端分离,那么前端可以到`dubbo-admin-ui`目录下,可在config目录下找到index.js中修改端口,修改过后可以终端输入`npm install` +- 安装依赖过后,可以`npm run dev`启动 + +## RocketMQ +> 我早有记录:[http://dreamcat.ink/2020/02/21/centos7-an-zhuang-rocketmq-ji-pei-zhi/](http://dreamcat.ink/2020/02/21/centos7-an-zhuang-rocketmq-ji-pei-zhi/) \ No newline at end of file diff --git "a/Java/bus/\347\217\255\350\275\246\346\234\215\345\212\241.md" "b/Java/bus/\347\217\255\350\275\246\346\234\215\345\212\241.md" new file mode 100644 index 00000000..0ee0849d --- /dev/null +++ "b/Java/bus/\347\217\255\350\275\246\346\234\215\345\212\241.md" @@ -0,0 +1,357 @@ +# 班车服务 + +## BusController + +### getCount +> 这部分缓存,优化的时候采用了Redis的list,发现我用Jmeter测试并发的时候,发现了Redis中出现了非常多的异常数据,我当时还没有找到问题,暂时先采取以下方案,以下方案在并发时候,并没有出现数据异常。 +```java +if (redisUtils.hasKey(key)) { + // 如果缓存存在 + Object obj = redisUtils.get(key); + log.warn("getCount->redis\n"); + // 返回数据 + return new ResponseUtil().setData(obj); +} + +// 写 +if (!redisUtils.hasKey(key)) { + // 如果缓存不存在,就写,注意与数据库数据一致性 + redisUtils.set(key, response, RedisConstants.COUNTS_EXPIRE.getTime()); +} +``` + +### getCountDetailById +```java +// 和上面一样 +if (redisUtils.hasKey(key)) { + Object obj = redisUtils.get(key); + log.warn("getCountDetailById->redis\n"); + return new ResponseUtil().setData(obj); +} +// 这里不判断了,上面已经判断过了 +redisUtils.set(key, response, RedisConstants.COUNT_DETAIL_EXPIRE.getTime()); +``` + +## BusServiceImpl +### getBus +> 这里基本没调用过,它是获取班车人物信息的。所以分页查询即可 + +```java +// MyBatis plus的分页查询 +IPage busIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); +busIPage = busMapper.selectPage(busIPage, null); +``` + +### getCount +> 获取场次列表 + +```java +// 分页插件,这里自定义分页查询 +IPage countIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 获取时间 +String currHours = DateUtil.getHours(); +String day = DateUtil.getDay(); +System.out.println("当前时间:"+currHours); +System.out.println("当前日期:"+day); +// 判断条件 +// 1. 找出符合当前天(比如,5.30) +// 2. 找出大于等于当前时间的场次(比如,数据库8点有一场,目前时间为7点,它就符合) +// 3. 找出状态为getBusStatus的场次,一般是还未发车的场次,(比如0,1) +queryWrapper + .eq("begin_date", day) + .ge("begin_time", currHours) + .eq("bus_status", request.getBusStatus()) + .orderByAsc("begin_time");// 时间 +countIPage = countMapper.selectCounts(countIPage, queryWrapper); +``` +**自定义分页CountMapper接口** +```java +public interface CountMapper extends BaseMapper { + /** + * + * @param page + * @param wrapper + * @return + */ + IPage selectCounts(IPage page, @Param(Constants.WRAPPER) Wrapper wrapper); + + /** + * + * @param wrapper + * @return + */ + CountDetailDto selectCountDetailById(@Param(Constants.WRAPPER) Wrapper wrapper); +} +``` +**xml** +```xml + +``` + +### getCountDetailById +> 查询场次详情信息 +```java +// 自定义查询,涉及到联表查询 +QueryWrapper wrapper = new QueryWrapper<>(); +wrapper.eq("sc.uuid", request.getCountId()); +``` +**xml** +```xml + +``` + +### repeatSeats +> 判断座位是否已重复 +```java +public boolean repeatSeats(String seats, Long coundId) { + // 查查数据库, 找到座位字段 + boolean b = false; // false:不重复,true:重复 + try { + Count count = countMapper.selectById(coundId); + // 比如,selectedSeats 是1,2 + // dbSeats:"", + // dbSeats:"1,2,3", + // dbSeats: "4,5" + // 前端传来的selectedSeats, 前端判断是否为空,要不然后端也判断一下得了 + if (seats.equals("")) { + return true; + } + if (count.getSelectedSeats().equals("")) { + return false; + } + String[] ss = seats.split(","); + String[] cs = count.getSelectedSeats().split(","); + // 这里考虑并发问题 + HashSet hashSet = new HashSet<>(Arrays.asList(cs)); + for (String s : ss) { + if (hashSet.contains(s)) return true; + } + } catch (Exception e) { + e.printStackTrace(); + log.error("selectedSeats", e); + return true; // 异常就算是重复 + } + return b; +} +``` +### addSeats +```java +if (!StrUtil.isEmpty(selectedSeats)) { + // 这里可以优化,字符串拼接,这样的方式爆内存 + // StringBuffer + newSelectedSeats = selectedSeats + "," + newSelectedSeats; +} +``` + +### filterRepeatSeats +> 回退座位 +```java +Count count = countMapper.selectById(coundId); +String[] ss = seats.split(","); +String[] cs = count.getSelectedSeats().split(","); +// 并发问题,注意 +HashSet hashSet = new HashSet<>(Arrays.asList(cs)); +for (String s : ss) { + if (hashSet.contains(s)) { + hashSet.remove(s); + } +} +if (hashSet.isEmpty()) { + count.setSelectedSeats(""); +} +// 考虑了并发 +StringBuffer sb = new StringBuffer(); +for (String s : hashSet) { + sb.append(s); + sb.append(","); +} +// 上面的方案可以用String的replace的方法替换,遍历要回退的座位,依次替换即可 +count.setSelectedSeats(sb.toString()); +countMapper.updateById(count); +``` +看一下String的replace的源码(1.8),感觉遍历还挺多,但的确不用自己去写了 +```java +public String replace(char oldChar, char newChar) { + if (oldChar != newChar) { + int len = value.length; + int i = -1; + char[] val = value; /* avoid getfield opcode */ + + while (++i < len) { + if (val[i] == oldChar) { + break; + } + } + if (i < len) { + char buf[] = new char[len]; + for (int j = 0; j < i; j++) { + buf[j] = val[j]; + } + while (i < len) { + char c = val[i]; + buf[i] = (c == oldChar) ? newChar : c; + i++; + } + return new String(buf, true); + } + } + return this; +} +``` + +## BusSchedule定时器 +> 这里可以使用redis的延时队列或者RocketMQ的消息队列 + +### schedulChangeBusStatus + +```java +/** + * 每天上午7点到晚上21点,每隔30分钟执行一次 + */ +@Scheduled(cron = "0 0/30 7-21 * * ?") +private void schedulChangeBusStatus() { + log.warn("schedulChangeBusStatus执行"); + busService.schedulChangeBusStatus(); +} +``` +看一下业务逻辑 +```java +public void schedulChangeBusStatus() { + // 获取时间 + String currTime = DateUtil.getHours(); + // 获取日期 + String day = DateUtil.getDay(); + log.warn("schedulChangeBusStatus->目前时间:" + currTime); + log.warn("schedulChangeBusStatus->目前时间:" + day); + System.out.println("目前时间:"+ currTime); + System.out.println("目前时间:"+ day); + QueryWrapper queryWrapper = new QueryWrapper<>(); + // 先取出beingtime和now相等的表或者end_time和now相等到表 + // 1. 取出当天 + // 2. 取出开始时间或者结束时间符合当前时间 + queryWrapper + .eq("begin_date", day) // 取出当天 + .and(o -> o.eq("begin_time", currTime) // 当前时间 + .or() + .eq("end_time", currTime)); + List counts = countMapper.selectList(queryWrapper); + log.warn("schedulChangeBusStatus->查询到的:" + counts.toString()); + // 开始作妖 + for (Count count : counts) { + String busStatus = count.getBusStatus(); + String beginTime = count.getBeginTime(); + String endTime = count.getEndTime(); + if (currTime.equals(beginTime)) { + if (busStatus.equals("0")) { // 沙河空闲 + count.setBusStatus("2"); // 沙河->清水河 + } + if (busStatus.equals("1")) { // 清水河空闲 + count.setBusStatus("3"); // 清水河->沙河 + } + count.setSelectedSeats(""); // 清空座位 + } + if (currTime.equals(endTime)) { + if (busStatus.equals("2")) { // 沙河->清水河 + count.setBusStatus("1"); // 清水河空闲 + } + if (busStatus.equals("3")) { // 清水河->沙河 + count.setBusStatus("0"); // 沙河空闲 + } + } + System.out.println("修改的:" + count); + log.warn("schedulChangeBusStatus->修改的:" + count); + // 写入数据库 + countMapper.updateById(count); + } + // 删缓存,这里非常重要...不删后果很严重 + String key1 = RedisConstants.COUNTS_EXPIRE + "0"; + String key2 = RedisConstants.COUNTS_EXPIRE + "1"; + if (redisUtils.hasKey(key1)) { + redisUtils.del(key1); + } + if (redisUtils.hasKey(key2)) { + redisUtils.del(key2); + } +} +``` +> 这里每隔30分钟扫库进行一次io,这里可以有优化... 假如我使用Redis的延迟队列 +1. 在用定时器添加场次的时候,可以将这些场次存入Redis的zset的队列中,元素为场次ID,元素对应的score是出发时间,这样就有17个(定时器每天凌晨添加17个) +- 还是用定时器轮询,采用每隔半小时轮询一次,我们取出队列中当前时间大于队列中权重的时间的场次ID,开始进行业务逻辑判断(更改场次状态) +- 更改过后,如果是发车时间,则删除队列中的场次id和score,重新添加队列中场次的id和结束时间score,或者直接修改score +- 如果是结束时间,则删除队列中的场次id和score + +### addCounts +> 这个项目,没有后台,因此场次需要定时器添加即可 +```java +/** + * 每天凌晨0点2分执行 + */ +@Scheduled(cron = "0 2 0 * * ? ") +private void addCounts(){ + log.warn("addCounts执行"); + busService.addCounts(); +} +``` +具体的业务逻辑: +```java +public void addCounts() { + // 获取日期 + String day = DateUtil.getDay(); + // 获取前17个场次 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.last("limit 17"); + List counts = countMapper.selectList(wrapper); + // 开始修改 这里可以用java8 的特性, 还不是很熟悉,后期优化一下 + for (Count count : counts) { + // 更改日期 + count.setBeginDate(day); + // 更改uuid + count.setUuid(UUIDUtils.flakesUUID()); + // 清空座位 + count.setSelectedSeats(""); + // 将走位状态清零 + count.setSeatStatus("0"); + // 插入 + countMapper.insert(count); + } + + // 删缓存,不删后果依然很严重 + String key1 = RedisConstants.COUNTS_EXPIRE + "0"; + String key2 = RedisConstants.COUNTS_EXPIRE + "1"; + if (redisUtils.hasKey(key1)) { + redisUtils.del(key1); + } + if (redisUtils.hasKey(key2)) { + redisUtils.del(key2); + } +} + +``` diff --git "a/Java/bus/\347\224\250\346\210\267\346\234\215\345\212\241.md" "b/Java/bus/\347\224\250\346\210\267\346\234\215\345\212\241.md" new file mode 100644 index 00000000..3996daa8 --- /dev/null +++ "b/Java/bus/\347\224\250\346\210\267\346\234\215\345\212\241.md" @@ -0,0 +1,104 @@ +# 用户服务 + +## 用户模块的MySQl数据库建表语句如下: + +> 数据库名:school-bus (自定) +> +> DROP TABLE IF EXISTS `sb_user`; +> CREATE TABLE `sb_user` ( +> `uuid` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键编号', +> `user_name` varchar(50) NOT NULL DEFAULT '' COMMENT '用户账号', +> `user_pwd` varchar(50) NOT NULL DEFAULT '' COMMENT '用户密码', +> `nick_name` varchar(50) NOT NULL DEFAULT '' COMMENT '用户昵称', +> `user_sex` int(11) DEFAULT NULL COMMENT '用户性别 0-男,1-女', +> `email` varchar(50) NOT NULL DEFAULT '' COMMENT '用户邮箱', +> `user_phone` varchar(50) NOT NULL DEFAULT '' COMMENT '用户手机号', +> `begin_time` datetime NOT NULL COMMENT '创建时间', +> `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', +> `money` double NOT NULL COMMENT '用户余额', +> `pay_password` varchar(100) CHARACTER SET utf8mb4 NOT NULL COMMENT '支付密码', +> PRIMARY KEY (`uuid`), +> UNIQUE KEY `user_name` (`user_name`) +> ) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户表'; +> +> INSERT INTO `sb_user` (`uuid`, `user_name`, `user_pwd`, `nick_name`, `user_sex`, `email`, `user_phone`, `begin_time`, `update_time`, `money`, `pay_password`) VALUES ('2', 'admin', '0192023a7bbd73250516f069df18b500', '隔壁泰山', '0', 'admin@qq.com', '13888888888', '2020-02-25 20:35:51', '2020-02-25 20:35:51', '0', ''), +> ('3', 'jiangzh', '5e2de6bd1c9b50f6e27d4e55da43b917', '阿里郎', '0', 'jiangzh@qq.com', '13866666666', '2020-02-25 20:35:51', '2020-02-25 20:35:51', '0', ''), + +持久层框架使用`mybatis-plus`结合guns 4.2项目在`guns-rest`的test目录下提供的generator自动生成相关的SQL代码,很方便,具体使用请自行查看代码。 + +## UserController + +`gateway`模块中`modular.user`下,与同目录下`form`和`auth`文件夹搭配使用,`form`提供网络请求的表单类,`auth`为guns自带的jwt验证,具体的验证逻辑可自行修改。 + +### checkUsername + +> 根据提供的用户名去数据库中查找相关记录是否存在,找到就返回查找成功响应,没找到就返回错误响应。 + +### register + +> 这个注意请求体的参数即可,也没什么可说的 + +### getUserById + +> 注意缓存是否存在,存在直接返回即可,没有就调用业务`getUserById`获取结果 + +### updateUserInfo + +> 注意请求体的参数即可,并且若是更新数据,那么自然要删除UserInfo的缓存,保持数据一致,写不写缓存这一点都行,下次请求getUserById会写缓存。 + +### logout +> 注意redis删除缓存即可 + + +## UserServiceImpl + +### checkUsername +```java +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 查询user_name列与getUsername相匹配的数据 +queryWrapper.eq("user_name", request.getUsername()); +User user = userMapper.selectOne(queryWrapper); +``` + +### regsiter +```java +// 密码采用了md5加密 +String md5Password = MD5Util.encrypt(user.getUserPwd()); +user.setUserPwd(md5Password); +``` + +### login + +```java +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 根据user_name查询用户是否存在 +queryWrapper.eq("user_name", request.getUsername()); +User user = userMapper.selectOne(queryWrapper); +if (user != null && user.getUuid() > 0) { + // 用户不为空,用户id > 0 + String md5Password = MD5Util.encrypt(request.getPassword()); + if (user.getUserPwd().equals(md5Password)) { + // 数据库密码和所加密的md5密码一致 + res.setUserId(user.getUuid()); + res.setCode(SbCode.SUCCESS.getCode()); + res.setMsg(SbCode.SUCCESS.getMessage()); + } +} +``` + +### getUserById +```java +//根据id查询用户 +User user = userMapper.selectById(request.getId()); +UserDto userDto = userConverter.User2Res(user); +``` + +### updateUserInfo +```java +// 这里一采用了mapstruct映射,有兴趣的可以去网上找资料学习学习 +User user = userConverter.res2User(request); +``` + +## 总结 + +用户服务,没有什么比较复杂的业务,复杂的业务在订单服务、场次服务、支付服务、 diff --git "a/Java/bus/\350\256\242\345\215\225\346\234\215\345\212\241.md" "b/Java/bus/\350\256\242\345\215\225\346\234\215\345\212\241.md" new file mode 100644 index 00000000..5ac5d81e --- /dev/null +++ "b/Java/bus/\350\256\242\345\215\225\346\234\215\345\212\241.md" @@ -0,0 +1,283 @@ +# 订单服务 + +## OrderController + +### getNoTakeOrdersById +> 未乘坐订单 + +```java +if (redisUtils.hasKey(key)) { + // redis是否有该缓存 + Object obj = redisUtils.get(key); + NoTakeBusResponse response = (NoTakeBusResponse) obj; + for (NoTakeDto noTakeDto : response.getNoTakeDtos()) { + // 如果场次发车时间-当前时间是大0的,那么说明已经发车了 + String beginTime = noTakeDto.getBeginTime(); + if (beginTime.compareTo(DateUtil.getHours()) <= -1) { + // 删掉当前缓存 + redisUtils.del(key); + // 重新获取最新的数据 + response = orderService.getNoTakeOrdersById(request); + // 写缓存 + redisUtils.set(key, response, RedisConstants.NO_TAKE_OREDERS_EXPIRE.getTime()); + return new ResponseUtil().setData(response); + } + } + log.warn("getNoTakeOrdersById->redis\n"); + return new ResponseUtil().setData(obj); +} +``` + +### getNoPayOrdersById +> 获取未支付订单接口 +```java +// 从redis中是否有缓存 +if (redisUtils.hasKey(key)) { + // 有就获取 + Object obj = redisUtils.get(key); + log.warn("getNoPayOrdersById->redis\n"); + return new ResponseUtil().setData(obj); +} +``` +### getEvaluateOrdersById +> 根据评价状态获取用户订单接口 +略 + +### addOrder +> 添加订单接口 + +```java +String countKey = RedisConstants.COUNT_DETAIL_EXPIRE.getKey() + request.getCountId(); +// 座位缓存失效 +if (redisUtils.hasKey(countKey)) { + redisUtils.del(countKey); +} +String noPayKey = RedisConstants.NO_PAY_ORDERS_EXPIRE.getKey() + userId; +// 未支付列表缓存失效 +if (redisUtils.hasKey(noPayKey)) { + redisUtils.del(noPayKey); +} +``` +### selectOrderById +> 根据订单id获取详情订单 +略 + +### updateOrderStatus +> 更改订单状态 +```java +// 删掉订单详情缓存 +if (redisUtils.hasKey(selectOrderKey)) { + redisUtils.del(selectOrderKey); +} +// 删除未乘坐缓存 +if (redisUtils.hasKey(noTakeKey)) { + redisUtils.del(noTakeKey); +} +// 删除未支付缓存 +if (redisUtils.hasKey(noPayKey)) { + redisUtils.del(noPayKey); +} +// 删除评价缓存 +if (redisUtils.hasKey(evaluateKey)) { + redisUtils.del(evaluateKey); +} +``` + +## OrderServiceImpl + +### getNoTakeOrdersById +```java +IPage orderIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 获取系统年月日 +// 比如5.30 +String day = DateUtil.getDay(); +// 比如20:00 +String hours = DateUtil.getHours(); +System.out.println("当前日期:" + day); +System.out.println("当前时间:" + hours); +queryWrapper + .eq("user_id", request.getUserId()) // 用户id + .eq("order_status", "1")// 1:已经支付 + .ge("sc.begin_date", day) // 比如订单日期大于等于今天 + .ge("sc.begin_time", hours) // 订单时间大于等于当前时间 + .orderByAsc("sc.begin_time") // 排序 + .orderByDesc("so.order_time"); // 排序 +``` + +### getEvaluateOrdersById +```java +IPage orderIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 获取系统年月日 +String day = DateUtil.getDay(); +String hours = DateUtil.getHours(); +System.out.println("当前日期:" + day); +System.out.println("当前时间:" + hours); +queryWrapper + .eq("user_id", request.getUserId()) // 用户di + .eq("order_status", "1") // 状态为1 + // 两种情况: + // 1. 符合当天日期,并且订单场次发车时间小于当前日期 + // 2. 订单场次的发车日期小于当前日期 + .and(o -> o.eq("sc.begin_date", day) + .lt("sc.begin_time", hours) + .or().lt("sc.begin_date", day)) + .eq("evaluate_status", request.getEvaluateStatus()) // 评价状态 + .orderByDesc("sc.begin_time") // 排序 + .orderByDesc("so.order_time"); // 排序 +``` + +### getNoPayOrdersById +```java +IPage noPayDtoIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 获取系统年月日 +String day = DateUtil.getDay(); +String hours = DateUtil.getHours(); +System.out.println("当前日期:" + day); +System.out.println("当前时间:" + hours); +queryWrapper + .eq("so.user_id", request.getUserId()) // 用户id + .eq("so.order_status", "0") // 未支付状态 + .ge("sc.begin_date", day) // 比如,订单场次日期大于当前日期 + .ge("sc.begin_time", hours)// 订单场次时间大于当前日期 + .orderByDesc("sc.begin_time") // 排序 + .orderByDesc("so.order_time"); // 未支付 +``` + +### addOrder +```java +public AddOrderResponse addOrder(AddOrderRequest request) { + // 判断座位,如果重复,直接退出,否则更新场次的座位信息 + AddOrderResponse response = new AddOrderResponse(); + // 全局orderId + Long orderId = UUIDUtils.flakesUUID(); + // 1。 判断座位,如果重复,直接退出,否则下一步 + // 2。 更新座位,如果没有异常,这是写操作 + // 3。 计算总金额,如果没有异常 + // 4。 添加订单,如果异常,这是写操作 + try { + // 1。 判断座位,如果重复,直接退出,否则下一步 + tag = MqTags.ORDER_SEATS_CANCEL.getTag(); + boolean repeatSeats = busService.repeatSeats(request.getSeatsIds(), request.getCountId()); + if (repeatSeats) { + // b:true 说明重复 + response.setCode(SbCode.SELECTED_SEATS.getCode()); + response.setMsg(SbCode.SELECTED_SEATS.getMessage()); + return response; + } +// CastException.cast(SbCode.SYSTEM_ERROR); + // 2。 更新座位,如果没有异常,这是写操作 + // 用tags来过滤消息 + tag = MqTags.ORDER_ADD_SEATS_CANCLE.getTag(); + boolean addSeats = busService.addSeats(request.getSeatsIds(), request.getCountId()); + if (!addSeats) { + response.setCode(SbCode.DB_EXCEPTION.getCode()); + response.setMsg(SbCode.DB_EXCEPTION.getMessage()); + return response; + } + // 模拟系统异常 +// CastException.cast(SbCode.SYSTEM_ERROR); + // 3。 计算总金额,如果没有异常 + tag = MqTags.ORDER_CALC_MONEY_CANCLE.getTag(); + String seatIds = request.getSeatsIds(); + Integer seatNumber = seatIds.split(",").length; + Double countPrice = request.getCountPrice(); + Double totalPrice = getTotalPrice(seatNumber, countPrice); + +// CastException.cast(SbCode.SYSTEM_ERROR); + // 4。 添加订单,如果异常,这是写操作 + Order order = orderConvertver.res2Order(request); + order.setOrderPrice(totalPrice); + order.setEvaluateStatus("0"); // 未评价 + order.setOrderStatus("0"); // 未支付 + order.setUuid(orderId); // 唯一id + tag = MqTags.ORDER_ADD_CANCLE.getTag(); + int insert = orderMapper.insert(order);// 插入 不判断了 +// CastException.cast(SbCode.SYSTEM_ERROR); + // 这里就不读了,耗时 +// QueryWrapper wrapper = new QueryWrapper<>(); +// wrapper.eq("so.uuid", order.getUuid()); +// OrderDto orderDto = orderMapper.selectOrderById(wrapper); + response.setCode(SbCode.SUCCESS.getCode()); + response.setMsg(SbCode.SUCCESS.getMessage()); + response.setOrderId(orderId); +// response.setOrderDto(orderDto); + // 这里放redis 未支付缓存,时间前端给定 + redisUtils.set(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + orderId, orderId, request.getExpireTime()); + return response; + } catch (Exception e) { + // 以上操作如果程序都不发生异常的话, 是不会执行这里的代码的 + // 也就是说不会发送回退消息的。 + // 目的是在高并发的情况下,程序内部发生异常,依然高可用 +// e.printStackTrace(); + log.error("订单业务发生异常"); + // 发消息,将座位退回,将订单退回 + MQDto mqDto = new MQDto(); + mqDto.setOrderId(orderId); + mqDto.setCountId(request.getCountId()); + mqDto.setSeatsIds(request.getSeatsIds()); + try { + String key = RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(orderId); + sendCancelOrder(topic,tag, key, JSON.toJSONString(mqDto)); + log.warn("订单回退消息发送成功..." + mqDto); + } catch (Exception ex) { + ex.printStackTrace(); + } + response.setCode(SbCode.SYSTEM_ERROR.getCode()); + response.setMsg(SbCode.SYSTEM_ERROR.getMessage()); + return response; + } +} +``` + +### selectOrderById +> 省略 + +### updateOrderStatus +```java +public OrderResponse updateOrderStatus(OrderRequest request) { + OrderResponse response = new OrderResponse(); + try { + // 获取orderDto + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("so.uuid", request.getUuid()); + OrderDto orderDto = orderMapper.selectOrderById(wrapper); + // 1, 检查状态是否为2 + if (request.getOrderStatus().equals("2")) { + // 说明关闭订单,回退座位 + busService.filterRepeatSeats(orderDto.getSeatsIds(), orderDto.getCountId()); + redisUtils.del(RedisConstants.COUNT_DETAIL_EXPIRE.getKey() + + orderDto.getCountId()); + // 清除场次详情的缓存 + } + if (request.getOrderStatus().equals("1")) { + // 说明已经支付,删掉5分钟的订单缓存 + redisUtils.del(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + request.getUuid()); + } + Order order = orderConvertver.res2Order(request); + // 更新状态 + orderMapper.updateById(order); + // 暂时就不获取了 + response.setCode(SbCode.SUCCESS.getCode()); + response.setMsg(SbCode.SUCCESS.getMessage()); + redisUtils.del(RedisConstants.NO_PAY_ORDERS_EXPIRE.getKey()+order.getUserId()); + redisUtils.del(RedisConstants.SELECT_ORDER_EXPIRE.getKey() + request.getUuid()); + } catch (Exception e) { + log.error("updateOrderStatus", e); + response.setCode(SbCode.DB_EXCEPTION.getCode()); + response.setMsg(SbCode.DB_EXCEPTION.getMessage()); + return response; + } + return response; +} +``` + +### deleteOrderById +> 省略 + +### sendCancelOrder +> 发送订单回退消息 + +后边会单独介绍消息队列 \ No newline at end of file diff --git a/Java/classify/README.md b/Java/classify/README.md new file mode 100644 index 00000000..15082b19 --- /dev/null +++ b/Java/classify/README.md @@ -0,0 +1,101 @@ +![个人体系v1](https://gitee.com/dreamcater/blog-img/raw/master/uPic/个人体系v1-Nkkq8b.png) + +## Java基础 +- [基本类型](basis/基本类型.md) +- [==、hashcode和equals](basis/==、hashcode和equals.md) +- [String](basis/String.md) +- [final](basis/final.md) +- [对象特性](basis/对象特性.md) +- [Object](basis/Object.md) +- [异常体系](basis/异常体系.md) +- [值传递](basis/值传递.md) +- [深浅拷贝](basis/深浅拷贝.md) +- [反射机制](basis/反射机制.md) +- [static](basis/static.md) +- [IO](basis/IO.md) +- [泛型](basis/泛型.md) +- [序列化](basis/序列化.md) + +## Java集合 +- [谈谈集合](col/谈谈集合.md) + +## Java多线程 +- [进程与线程](thread/进程和线程.md) +- [线程的创建方式](thread/线程的创建方式.md) +- [死锁](thread/死锁.md) +- [JMM内存模型](thread/JMM内存模型) +- [volatile](thread/volatile.md) +- [CAS](thread/CAS.md) +- [ThreadLocal](thread/ThreadLocal.md) +- [synchronized](thread/synchronized.md) +- [AQS](thread/AQS.md) +- [ReentrantLock](thread/ReentrantLock.md) +- [ReentrantReadWriteLock](thread/ReentrantReadWriteLock.md) +- [CountDownLatch](thread/CountDownLatch.md) +- [Java的锁](thread/Java锁的介绍.md) +- [生产者和消费者](thread/生产者和消费者.md) +- [线程池](thread/线程池.md) +- [BlockingQueue的一些问题](thread/BlockingQueue的一些问题.md) + +## Jvm +- [类文件结构](jvm/类文件结构.md) +- [类加载过程](jvm/类加载过程.md) +- [类加载器](jvm/类加载器.md) +- [JVM内存区域](jvm/JVM内存区域.md) +- [对象的创建过程](jvm/对象的创建过程.md) +- [垃圾回收](jvm/垃圾回收.md) +- [逃逸分析](jvm/逃逸分析.md) + +## Spring +- [Ioc和AOP](spring/Ioc和AOP.md) +- [Bean](spring/Bean.md) +- [Spring事务](spring/Spring事务.md) +- [SpringMVC](spring/SpringMVC.md) +- [SpringBoot](spring/SpringBoot.md) + +## 数据库 +- [MySQL是如何执行一条SQL的](mysql/MySQL是如何执行一条SQL的.md) +- [Innodb与MyISAM](mysql/Innodb与MyISAM.md) +- [ACID](mysql/ACID.md) +- [MySQL日志文件](mysql/MySQL日志文件.md) +- [索引](mysql/索引.md) +- [MySQL数据库结构优化](mysql/MySQL数据库结构优化.md) +- [MySQL的锁](mysql/MySQL的锁.md) +- [Redis模型](redis/Redis模型.md) +- [Redis数据结构](redis/Redis数据结构.md) +- [Redis持久化](redis/Redis持久化.md) +- [内存淘汰机制](redis/内存淘汰机制.md) +- [缓存穿透和缓存雪崩](redis/缓存穿透和缓存雪崩.md) +- [双写一致性](https://www.cnblogs.com/rjzheng/p/9041659.html) +- [并发竞争key](https://juejin.cn/post/6844903846750191630) +- [Redis分布式锁](redis/Redis分布式锁.md) + +## 分布式 +- [分布式概念](dis/分布式概念.md) +- [CAP和BASE](dis/CAP和BASE.md) +- [分布式一致性算法](dis/分布式一致性算法.md) +- [限流算法](dis/限流算法.md) +- [分布式事务](dis/分布式事务.md) +- [布隆过滤器](dis/布隆过滤器.md) +- [Dubbo](dis/Dubbo.md) +- [RocketMQ](dis/RocketMQ.md) +- [zookeeper](dis/zookeeper.md) +- [Sentinel](dis/Sentinel.md) + +## 计网 +- [网络模型](net/网络模型.md) +- [DNS](net/DNS.md) +- [HTTP和HTTPS](net/HTTP和HTTPS.md) +- [TCP和UDP](net/TCP和UDP.md) + +## 操作系统 +- [进程与线程](sys/进程与线程.md) +- [进程间通信](sys/进程间通信.md) +- [系统进程调度](sys/系统进程调度.md) +- [虚拟内存](sys/虚拟内存.md) +- [操作系统内存管理方式](sys/操作系统内存管理方式.md) +- [页面置换算法](sys/页面置换算法.md) +- [死锁](sys/死锁.md) + +## 其他 + diff --git "a/Java/classify/basis/==\343\200\201hashcode\345\222\214equals.md" "b/Java/classify/basis/==\343\200\201hashcode\345\222\214equals.md" new file mode 100644 index 00000000..9cca85c3 --- /dev/null +++ "b/Java/classify/basis/==\343\200\201hashcode\345\222\214equals.md" @@ -0,0 +1,133 @@ + +# ==、hashcode和equals +> 我感觉这问题被问的频率有点高, 那是因为考的知识点比较多 + +## == + +面试官:出个题: + +```java +Integer a = new Integer(5); +Integer b = new Integer(5); +System.out.println(a == b); // false +``` + +我:false,太简单了嘛。 == 比较的是两个对象的地址哦,你看: + +### 概念及作用 + +它的作用是**判断两个对象的地址是不是相等**。即,判断两个对象是不是同一个对象: + +- 基本数据类型==比较的是**值** + +- 引用数据类型==比较的是**内存地址** + +你要是:`System.out.println(a.equals(b)); \\ false` + +面试官:那这个呢: + +```java +int a = 5; +Integer b = new Integer(5); +System.out.println(a == b); // true +``` +### 装箱与拆箱 + +我:你想考我装箱拆箱嘛?其实,b看到了对面那个哥们是**基本类型**,我也是基本类型包装的呀,我把衣服一脱,那么就是**基本类型**了,那就是上面的结论了。b拆箱即可。 + +装箱:**自动将基本数据类型转换为包装器类型** + +拆箱:**自动将包装器类型转换为基本数据类型** + +基本类型有: + +- byte(1字节):Byte +- short(2字节):Short +- int(4字节):Integer +- long(8字节):Long +- float(4字节):Float +- double(8字节):Double +- char(2字节):Character +- boolean(未知):Boolean + +**在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法** + +valueOf: + +```java +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +intValue: + +```java +public int intValue() { + return value; +} +``` + +## equals + +面试官:聊一下equals + +### 概念及原理 +我:equals是顶层父类Object的方法之一 + +```java +// 你看,object默认调用的== , 你对象如果不重写,可能会发上重大事故 +public boolean equals(Object obj) { + return (this == obj); // 比较对象的地址值 +} +``` + +顺带说一下Object的hashcode方法 + +```java +// Returns a hash code value for the object. +// 说白了,调用本地方法返回的就是该对象的地址值 +public native int hashCode(); +``` +### 作用 +Equals的作用也是判断两个对象是否相等。但它一般有两种使用情况: + +- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过`==`比较这两个对象。 + +- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 + +```java +Integer a = new Integer(5); +Integer b = new Integer(5); +System.out.println(a.equals(b)); +``` + +可以看一下Integer的equals方法: + +```java +// 重写了Object的equals的方法 +public boolean equals(Object obj) { + if (obj instanceof Integer) { + // 比较value + return value == ((Integer)obj).intValue(); + } + return false; +} +``` + +## hashcode + +hashcode的例子: + +```java +public static void main(String[] args) { + Set set = new HashSet<>(); + set.add(1); + set.add(1); + System.out.println(set.toString()); +} +``` +**解释:** +在添加1的时候,先判断**hashcode是否相同**,如果相同,**继续判断equals比较值**,这样的好处是,**如果hashcode相同就直接返回false了,减少了一次equals的判断,因为通常hashcode的值判断是否相等比较快,而equals相对于hashcode来讲慢一些**。所以,如果不重写hashcode,我们看到object的hashcode是对象的内存值,那么set添加1判断的时候,hashcode永远不相等,那么就永远返回false,不管添加1,还是2,都是false。 \ No newline at end of file diff --git a/Java/classify/basis/IO.md b/Java/classify/basis/IO.md new file mode 100644 index 00000000..bcaf5d5d --- /dev/null +++ b/Java/classify/basis/IO.md @@ -0,0 +1,11 @@ +# IO +## BIO +**BIO (Blocking I/O)**:**同步阻塞I/O模式**,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 + +## NIO +**NIO (New I/O)**:NIO是一种**同步非阻塞的I/O模型**,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 `Channel` , `Selector`,`Buffer`等抽象。NIO中的N可以理解为`Non-blocking`,不单纯是New。它支持**面向缓冲**的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。 + +[NIO底层原理](https://blog.csdn.net/u013857458/article/details/82424104) + +## AIO +**AIO (Asynchronous I/O)**: AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是**异步非阻塞的IO模型**。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 \ No newline at end of file diff --git a/Java/classify/basis/Object.md b/Java/classify/basis/Object.md new file mode 100644 index 00000000..38fd81c1 --- /dev/null +++ b/Java/classify/basis/Object.md @@ -0,0 +1,14 @@ +# Object + +```java +public final native Class getClass(); +public native int hashCode(); // 返回对象的哈希代码值。 +public boolean equals(Object obj) +protected native Object clone() // 创建并返回此对象的副本。 +public String toString() // 返回对象的字符串表示形式。 +public final native void notify(); // 唤醒正在该对象的监视器上等待的单个线程。 +public final native void notifyAll(); // 唤醒正在该对象的监视器上等待的全部线程。 +public final native void wait(); // 使当前线程等待,直到另一个线程调用此对象的方法或方法。 +protected void finalize(); // 当垃圾回收确定不再有对对象的引用时,由对象上的垃圾回收器调用。 +``` + diff --git a/Java/classify/basis/String.md b/Java/classify/basis/String.md new file mode 100644 index 00000000..f6a127db --- /dev/null +++ b/Java/classify/basis/String.md @@ -0,0 +1,119 @@ + +# String +> 难道面试官想考String的什么? + +## String、StringBuffer和StringBuilder的区别 + +面试官:说一下你知道的String、StringBuffer和StringBuilder的区别 + +我:其实总结说一下就行,三点不同 + +### 可变性 + +- 简单的来说:`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 `String` 对象是不可变的。(还不是为了线程安全和JVM缓存速度问题) + +- `StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串char[]value 但是没有用 `final` 关键字修饰,所以这两种对象都是可变的。 + +### 线程安全 + +- `String` 中的对象是不可变的,也就可以理解为常量,线程安全。 + +- `AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。 + +### 性能 + +- 每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。(人家目的是不可变,你一直让人家干可变的事情,那岂不是很难受?) + +- `StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 + +### 总结 + +- 操作少量的数据: 适用String + +- 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder + +- 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer + +> 注意:这里可能要问final关键字来解释一波String的char前面所加的final的好处 + +## final关键字 + +面试官:有木有想过为什么String的char前面加了final,有什么好处? + +我:答这个问题,你要先说final是干啥的 + +final关键字主要用在三个地方:变量、方法、类。 + + +- 对于一个final变量,如果是**基本数据类型的变量,则其数值一旦在初始化之后便不能更改**;如果是引用类型的变量,则在对其初始化之后便**不能再让其指向另一个对象**。 + +- 当用final修饰一个类时,表明**这个类不能被继承**。final类中的所有成员方法都会被隐式地指定为final方法。 + +- 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 + +**final修饰有啥好处**:(面试官想听这三点) + +- final的关键字**提高了性能**,JVM和java应用会**缓存final变量**; + +- final变量可以在多线程环境下保持**线程安全**; + +- 使用final的关键字提高了性能,JVM会对方法变量类进行优化; + +> 这里可能让解释String对象和JVM的常量池 + +## String对象和常量池 + +```java +public class StringTest { + public static void main(String[] args) { + String str1 = "todo"; // 常量池 + String str2 = "todo"; // 从常量池找了str1 + String str3 = "to"; // 常量池 + String str4 = "do"; // 常量池 + String str5 = str3 + str4; // 内部用StringBuilder拼接了一波。 因此, 并非常量池 + String str6 = new String(str1); // 创建对象了, 那还能是常量池的引用? + } +} +``` + +分析一波: +- 成的class文件中会在常量池中**保存“todo”、“to”和“do”三个String常量**。 +- 变量str1和str2均保存的是常量池中“todo”的引用,所以str1==str2成立; +- 在执行 str5 = str3 + str4这句时,**JVM会先创建一个StringBuilder对象,通过StringBuilder.append()方法将str3与str4的值拼接**,然后通过StringBuilder.toString()返回一个堆中的String对象的引用,赋值给str5,因此str1和str5指向的不是同一个String对象,str1 == str5不成立; +- String str6 = new String(str1)一句显式创建了一个新的String对象,因此str1 == str6不成立便是显而易见的事了。 + +## 了解String.intern()方法吗? + +面试官:说一下String.intern()干嘛用的? + +我:去字符串常量池拿吃的 + +- jdk6: + +执行intern()方法时,**若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串(这句话很重要),然后返回该字符串的引用**。 + +- jdk7: + +执行intern操作时,如果常量池已经存在该字符串,则直接返回字符串引用,否则**复制该堆中字符串对象的引用到常量池(堆对象引用复制到常量池)中并返回**。 + +简单说一下常量池版本的变化 + +### 1.6 + +- 静态常量池在Class文件中。 +- **运行时常量池(常量池:字面量和符号引用)在Perm Gen区(也就是方法区)中**。 +- **字符串常量池在运行时常量池**中。 + +### 1.7 + +- 静态常量池在Class文件中。 +- **运行时常量池依然在Perm Gen区(也就是方法区)中**。在JDK7版本中,永久代的转移工作就已经开始了,将譬如符号引用转移到了native heap;字面量转移到了java heap;类的静态变量转移到了java heap。但是运行时常量池依然还存在,只是很多内容被转移,其只存着这些被转移的引用。 +- **字符串常量池被分配到了Java堆的主要部分**。也就是**字符串常量池从运行时常量池分离出来了**。 + +### 1.8 + +- 静态常量池在Class文件中。 +- **JVM已经将运行时常量池从方法区中移了出来,在Java堆(Heap)中开辟了一块区域存放运行时常量池**。**同时永久代被移除,以元空间代替**。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。其主要用于存放一些元数据(类信息)。 +- **字符串常量池存在于Java堆中**。 + + diff --git a/Java/classify/basis/final.md b/Java/classify/basis/final.md new file mode 100644 index 00000000..b7dc3085 --- /dev/null +++ b/Java/classify/basis/final.md @@ -0,0 +1,22 @@ +# final + +面试官:有木有想过为什么String的char前面加了final,有什么好处? + +我:答这个问题,你要先说final是干啥的 + +final关键字主要用在三个地方:变量、方法、类。 + + +- 对于一个final变量,如果是**基本数据类型的变量,则其数值一旦在初始化之后便不能更改**;如果是引用类型的变量,则在对其初始化之后便**不能再让其指向另一个对象**。 + +- 当用final修饰一个类时,表明**这个类不能被继承**。final类中的所有成员方法都会被隐式地指定为final方法。 + +- 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 + +**final修饰有啥好处**:(面试官想听这三点) + +- final的关键字**提高了性能**,JVM和java应用会**缓存final变量**; + +- final变量可以在多线程环境下保持**线程安全**; + +- 使用final的关键字提高了性能,JVM会对方法变量类进行优化; \ No newline at end of file diff --git a/Java/classify/basis/static.md b/Java/classify/basis/static.md new file mode 100644 index 00000000..f91e22b2 --- /dev/null +++ b/Java/classify/basis/static.md @@ -0,0 +1,5 @@ +# static +- **修饰成员变量和成员方法:** 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量存放在 Java 内存区域的方法区。调用格式:`类名.静态变量名` `类名.静态方法名()` +- **静态代码块:** 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. +- **静态内部类(static修饰类的话只能修饰内部类):** 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。 +- **静态导包(用来导入类中的静态资源,1.5之后的新特性):** 格式为:`import static` 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 \ No newline at end of file diff --git "a/Java/classify/basis/\345\200\274\344\274\240\351\200\222.md" "b/Java/classify/basis/\345\200\274\344\274\240\351\200\222.md" new file mode 100644 index 00000000..1e9630fe --- /dev/null +++ "b/Java/classify/basis/\345\200\274\344\274\240\351\200\222.md" @@ -0,0 +1,105 @@ + +# 值传递 + +> Java值传递,举例子比较合适,总之,拷贝,拷来烤去的。 + +面试官:了解Java值传递嘛? + +我:了解,我总结了一波: + +- 一个方法**不能修改一个基本数据类型的参数**(即数值型或布尔型)。 + +- 一个方法可以改变**一个对象参数的状态**。 + +- 一个方法**不能让对象参数引用一个新的对象**。 + +## 基本类型 + +所谓第一个,举例子先: + +```java +public static void main(String[] args) { + int num1 = 10; + int num2 = 20; + + swap(num1, num2); // 交换 + + System.out.println("num1 = " + num1); // 10 + System.out.println("num2 = " + num2); // 20 +} + +public static void swap(int a, int b) { + int temp = a; + a = b; + b = temp; + + System.out.println("a = " + a); // 20 + System.out.println("b = " + b); // 10 +} +// a = 20 +// b = 10 +// num1 = 10 +// num2 = 20 + +``` + +![基本类型传递](https://gitee.com/dreamcater/blog-img/raw/master/uPic/基本类型传递-QPPzXh.png) + +> 在 swap 方法中,**a、b 的值进行交换,并不会影响到 num1、num2**。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,**a、b 相当于 num1、num2 的副本**,副本的内容无论怎么修改,都不会影响到原件本身。 说白了,就是深拷贝 + +## 数组类型传递 + +所谓第二个,举例子先: + +```java + public static void main(String[] args) { + int[] arr = { 1, 2, 3, 4, 5 }; + System.out.println(arr[0]); // 1 + change(arr); + System.out.println(arr[0]); // 0 + // 得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。 + } + + private static void change(int[] array) { + // 修改数组中的一个元素 + array[0] = 0; + } +// 1 +// 0 +``` + +![数组类型传递](https://gitee.com/dreamcater/blog-img/raw/master/uPic/数组类型传递-fxStps.png) + +> array 被初始化 arr 的拷贝也就是**一个对象的引用**,也就是说 array 和 arr 指向的是**同一个数组对象**。 因此,外部对引用对象的改变会反映到所对应的对象上。说白了,浅拷贝 + +## 对象引用 + +所谓第三个,举例子先: + +```java + public static void main(String[] args) { + // 有些程序员认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。 + Student s1 = new Student("Mai"); + Student s2 = new Student("Feng"); + swap2(s1, s2); + System.out.println("s1:" + s1.getName()); // Mai + System.out.println("s2:" + s2.getName()); // Feng + // 方法并没有改变存储在变量 s1 和 s2 中的对象引用。 + // swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝 + } + + private static void swap2(Student x, Student y) { + Student temp = x; + x = y; + y = temp; + System.out.println("x:" + x.getName()); // Feng + System.out.println("y:" + y.getName()); // Mai + } +// x:Feng +// y:Mai +// s1:Mai +// s2:Feng + +``` + +![对象引用传递](https://gitee.com/dreamcater/blog-img/raw/master/uPic/对象引用传递-m3CrXE.png) diff --git "a/Java/classify/basis/\345\217\215\345\260\204\346\234\272\345\210\266.md" "b/Java/classify/basis/\345\217\215\345\260\204\346\234\272\345\210\266.md" new file mode 100644 index 00000000..09928184 --- /dev/null +++ "b/Java/classify/basis/\345\217\215\345\260\204\346\234\272\345\210\266.md" @@ -0,0 +1,84 @@ +# 反射机制 + +> 面试遇到这个问题,必须好好的想想如何回答这个问题,我就是这么回答的。害 + +面试官:反射是什么? + +我:在Java的反射机制中是指在**运行状态**中,对于任意一个类都能够知道这个类所有的**属性和方法**;并且对于任意一个对象,都能够调用它的**任意一个方法**;这种**动态获取信息以及动态调用对象方法**的功能成为 Java 语言的反射机制。 + +面试官:哦?有什么好处? + +我:怎么说呢,跟多态是的,比如在Java程序中许多对象在运行是都会出现两种类型:**编译时类型和运行时类型**。其中,编译时类型由**声明对象时使用的类型来决定**,运行时的类型由**实际赋值给对象的类型决定** 。比如 + +`People = = new Man();`程序在运行的时候,有时候需要注入外部资源,那么这个外部资源在编译时是People,如果想要它的运行时类型中的某个方法,为了解决这些问题,程序在运行时发现对象和类的真实信息,但是**编译时根本无法预知该对象和类属于哪些**类,程序只能靠运**行时信息来发现该对象和类的信息**,那就要用到反射了。 + +## 反射的API + +面试官:举几个反射的API + +我: + +1. Class 类:反射的核心类,可以获取类的属性,方法等信息。 +2. Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。 +3. Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。 +4. Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法 + +## 获取class的方式 + +面试官:获取class对象的三种方式? + +我: + +```java +Student student = new Student(); *// 这一new 产生一个Student对象,一个Class对象。* + +Class studentClass2 = Student.class; // 调用某个类的 class 属性来获取该类对应的 Class 对象 + +Class studentClass3 = Class.forName("com.reflect.Student") // 使用 Class 类中的 forName() 静态方法 ( 最安全 / 性能最好 ) + +``` + +面试官:三者区别? + +我: + +- Class.class 的形式会使 JVM 将使用类装载器将类装入内存(前提是类还没有装入内存),**不做类的初始化工作**,返回 Class 对象。 +- Class.forName() 的形式会装入类并做类的**静态初始化**,返回 Class 对象。 +- getClass() 的形式会对类进行**静态初始化**、**非静态初始化**,返回引用运行时真正所指的对象(因为子对象的引用可能会赋给父对象的引用变量中)所属的类的 Class 对象。 + +> 静态属性初始化是在加载类的时候初始化,而非静态属性初始化是 new 类实例对象的时候初始化。它们三种情况在生成 Class 对象的时候都会先判断内存中是否已经加载此类。 + +面试官:ClassLoader呢? + +我:ClassLoader就是遵循**双亲委派模型最终调用启动类加载器的类加载器**,实现的功能是“通过一个**类的全限定名来获取描述此类的二进制字节流**”,获取到二进制流后放到JVM中,加载的类**默认不会进行初始化**。 + +而Class.forName()会静态初始化,那么看源码: + +```java +@CallerSensitive +public static Class forName(String className) + throws ClassNotFoundException { + Class caller = Reflection.getCallerClass(); + // 这里的true,就是初始化 + return forName0(className, true, ClassLoader.getClassLoader(caller), caller); +} + +// initialize:是否初始化 +private static native Class forName0(String name, boolean initialize, + ClassLoader loader, + Class caller) + throws ClassNotFoundException; +``` + +## 如何用反射创建对象 + +面试官: + +面试官:除了通过反射创建对象,还有? + +我:new呗,clone一个呗 + +面试官:反射都有哪些应用场景 + +我:我可以说Spring,Dubbo,RocketMQ吗?这些优秀的框架背后都用到了反射,这说明,反射的优点之一灵活,提高了代码的灵活度,但同时性能受损。因为反射要进行一系列的解释操作。 + diff --git "a/Java/classify/basis/\345\237\272\346\234\254\347\261\273\345\236\213.md" "b/Java/classify/basis/\345\237\272\346\234\254\347\261\273\345\236\213.md" new file mode 100644 index 00000000..88107c01 --- /dev/null +++ "b/Java/classify/basis/\345\237\272\346\234\254\347\261\273\345\236\213.md" @@ -0,0 +1,57 @@ +# 基本类型 + +> 简单说一下 + + +## 8大基本类型 + +![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) + + +> 特别注意Boolean 未精确定义字节。Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位。 + + +## 装箱和拆箱 + +- 装箱:将基本类型用它们对应的**引用类型**包装起来; +- 拆箱:将包装类型转换为**基本数据类型**; + +## 注意初始化 + +```java +int a; +System.out.println(a); // 这里会报错,因为a没有初始化 + +Integer b = new Integer(); // 构造函数必须传递一个默认值,要不然提示报错,无法初始化 +// 看一下Integer的源码的value, +public Integer(int value) { + this.value = value; +} +// 而value,这么以来,创建对象必须初始化咯 +private final int value; +``` + +## 注意字面量 + +```java +float a = 3.14; // 字面量3.14 是double类型, 所以这里会报错 +float a = 3.14f; // 这样就不会报错了,需要加个f + +// 转型 +short a = 1; // short类型 +a = a + 1; // short = short + int 会报错, int无法像下转型,但是short可以向上,这样右边就是Int,无法向下 +a++; // 这里是a = (short) (a + 1); +``` + +## 注意Integer.valueOf + +```java +// This method will always cache values in the range -128 to 127, +public static Integer valueOf(int i) { +if (i >= IntegerCache.low && i <= IntegerCache.high) // 条件 +return IntegerCache.cache[i + (-IntegerCache.low)];// 取缓存, +// Integeer的源码中: +// static final int low = -128; IntegerCache.low = -128 +return new Integer(i); +} +``` \ No newline at end of file diff --git "a/Java/classify/basis/\345\257\271\350\261\241\347\211\271\346\200\247.md" "b/Java/classify/basis/\345\257\271\350\261\241\347\211\271\346\200\247.md" new file mode 100644 index 00000000..660471c4 --- /dev/null +++ "b/Java/classify/basis/\345\257\271\350\261\241\347\211\271\346\200\247.md" @@ -0,0 +1,255 @@ +# 对象特性 + +## 封装 + +封装把一个对象的**属性私有化**,同时提供一些可以**被外界访问的属性的方法**。 + +```java +class Person{ + String name; // 属性 + int age; + + // 方法 + public Person(String name, int age){ + this.name = name; + this.age = age; + } +} +``` + +## 继承 + +继承是使用**已存在的类**的定义作为基础建立新类的技术,新类的定义可以增加**新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类**。通过使用继承我们能够非常方便地复用以前的代码。 + +```java +class Student extends Person{ + double grade; // 在已经的name,age中, 在学生类中添加了成绩或者班级 + + // 方法 + public Student(String name, int age, double grade) { + this.name = name; + this.age = age; + this.grade = grade; + } +} +``` + +## 多态 + +### 定义 + +三要素:加黑的地方! + +首先我觉得即**一个引用变量到底会指向哪个类的实例对象**,该**引用变量发出的方法调用到底是哪个类中实现的方法**,必须在由程序**运行期间**才能决定。强调在编译的时候,不知道该引用指向的是哪个对象的实例,包括调用哪个实例的方法,只有运行的时候,动态知道。 + +举个例子: + +任何事物的多个姿态,多个形态。比如,你说一个猫在吃东西,同样的,你也能说一个动物在吃东西。 + +```java +public class Test { + public static void main(String[] args){ + Animal animal = new Cat(); + animal.eat() // 猫也会吃饭 + // 你看到了一只猫,同样它也是动物 + // 比如有很多其他种类继承了动物哈, + // 当编译期间的animal引用变量,到底指的哪个实例对象,(重要)(主语是引用变量) + // 或者该引用调用的eat方法,到底是哪个实例对象的eat,编译期间恐怕不知道哦(主语是引用变量) + // 只有运行期间,哦哦, 原来是猫的eat方法哇... + } +} +``` + +### 表现形式 + + +- **Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但可具有不同的参数列表、返回值类型。调用方法时通过传递的参数类型来决定具体使用哪个方法**,这就是多态性。比如,java lang的包很多工具类如String工具类,那么就有很多相同的名字,但是参数类型、数量和返回值等等不一样。 + + +- **Java的方法重写,是父类与子类之间的多态性,子类可继承父类中的方法,但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。重写的参数列表和返回类型均不可修改**。这也是多态性。比如JUC的AQS框架,凡事继承了AQS的那几个类,其中几个重要的方法,都被重写了,很多这样的情况。 + +### 底层 + +首先要说:首先当程序运行需要某个类时,类加载器会将相应的class文件载入到JVM中,并在方法区建立该类的类型信息(包括方法代码,类变量、成员变量以及**方法表**。(标黑的这个玩意) + +面试官:方法表有啥? + +我:方法表的结构如同字段表一样,依次包括了**访问标志、名称索引、描述符索引、属性表集合**几项。 + +接着回答:**方法表是实现动态调用的核心。为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向记录该类方法的方法表,方法表中的每一个项都是对应方法的指针**。 + +到这里:就要分情况讨论了,一个是方法调用,一个是接口 + +##### 方法调用 +```java +class Person { + // 重写object的toString + public String toString(){ + return "I'm a person."; + } + public void eat(){} + public void speak(){} + +} + +class Boy extends Person{ + // 重写object的toString + public String toString(){ + return "I'm a boy"; + } + // 继承Person的speak + public void speak(){} + // 自己实现的自定义方法 + public void fight(){} +} + +class Girl extends Person{ + // 重写object的toString + public String toString(){ + return "I'm a girl"; + } + // 继承Person的speak + public void speak(){} + // 自己实现的自定义方法 + public void sing(){} +} +``` + +![多态方法调用-I0jtEe](https://gitee.com/dreamcater/blog-img/raw/master/uPic/多态方法调用-I0jtEe.png) + +这张图的指向:你可以根据颜色对应上,注意方法表条目指向的具体的**方法地址**。其次注意蓝色部分其继承自于 Person 的方法 eat() 和 speak() 分别指向 **Person 的方法实现和本身的实现**。如果子类改写了父类的方法,那么子类和父类的那些**同名的方法共享一个方法表项**。因此,**所有继承父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值**。Person 或 Object中的任意一个方法,在它们的方法表和其子类 Girl 和 Boy 的方法表中的位置 (index) 是一样的。这样 JVM 在调用实例方法其实只需要指定调用方法表中的第几个方法即可。 + + +调用过程: + +1. 在常量池里找到方法调用的**符号引用**(肯定先看到Person定义引用类型) +2. 查看Person的方法表,得到speak方法在该**方法表的偏移量**(假设为15),这样就得到该方法的直接引用。 +3. 根据this(invoker this字节码)指针得到**具体的对象**(即 girl 所指向的位于堆中的对象)。 +4. 根据对象得到该对象对应的方法表,根据偏移量15查看**有无重写(override)该方法**,如果重写,则可以直接调用(Girl的方法表的speak项指向自身的方法而非父类);如果没有重写,则需要拿到按照继承关系从下往上的基类(这里是Person类)的方法表,同样按照这个偏移量15查看有无该方法。 + + +#### 接口调用 + +一个类可以实现多个接口,那么就像多继承一样,这样的话,在方法表中的索引就会不一样,所以Java 对于接口方法的调用是采用**搜索方法表**的方式。 + +## 初始化顺序 + +有这样的两个类:Person和Student + +Person: + +```java +class Person{ + // 静态代码块 + static { + System.out.println("Person 静态方法"); + } + // 代码块 + { + System.out.println("Person 代码块"); + } + // 构造方法 + Person(){ + System.out.println("Person 构造方法"); + } +} +``` + +Student: + +```java +class Student extends Person{ + // 静态代码块 + static { + System.out.println("Student 静态方法"); + } + // 代码块 + { + System.out.println("Student 代码块"); + } + // 构造方法 + Student(){ + System.out.println("Student 构造方法"); + } +} +``` + +测试 + +```java +public class Main { + public static void main(String[] args) { + Student student = new Student(); + } +} + +// result: + +Person 静态方法 +Student 静态方法 +Person 代码块 +Person 构造方法 +Student 代码块 +Student 构造方法 +``` + +解释:还不是因为类加载器的双亲委派哈, 先走类方法,也就是说static属于类,所以先调用**Person static -> Student static**,接着走对象的初始化,那么对象初始化了,还是先走父类的初始化了,所以**Perosn {} -> Person(){}**,最后走子类的初始化**Student {} -> Student(){}** + + + + +## 接口和抽象类的区别 + +1. 方法是否能实现:所有**方法在接口中不能有实现**(Java 8 开始接口方法可以有默认实现),而**抽象类可以有非抽象的方法**。 +2. 变量:接口中除了**static、final变量**,不能有其他变量,而抽象类中则不一定。 +3. 实现:一个类可以实现**多个接口**,但**只能实现一个抽象类**。接口自己本身可以通过implement关键字扩展多个接口。 +4. 修饰符:接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。 +5 + +## 匿名内部类传参数为什么需要final + +```java +public class Main { + public static void main(String[] args) { + String str = "m"; + new Thread(){ + @Override + public void run() { + System.out.println(str); + } + }.start(); + } +} +// 在1.8版本之前是编译不通过的,1.8能编译能过,但是还是需要final保证局部变量的数据一致性 +``` + +反编译 + +```java + +public class Hello$1 extends Thread { + + private String val$str; + + Hello$1(String paramString) { + this.val$str = paramString; + } + + public void run() { + System.out.println(this.val$str); + } + +} +``` + +解释:**局部变量是被当成了参数传递给匿名对象的构造器(那就相当于拷贝一份,那就是浅拷贝了),这样的话,如果不管是基本类型还是引用类型,一旦这个局部变量是消失(局部变量随着方法的出栈而消失),还是数据被改变,那么此时匿名构造类是不知道的,毕竟是你浅拷贝了一份,那么如果加上final,这个数据或者引用永远都不会改变,保证数据一致性。注意:在JDK8中如果我们在匿名内部类中需要访问局部变量,那么这个局部变量不需要用final修饰符修饰。** + +## 四种修饰符的限制范围 + +1. public:可以被所有其他类所访问。 + +2. private:只能被自己访问和修改。 + +3. protected:自身,子类及同一个包中类可以访问。 + +4. default(默认):同一包中的类可以访问 \ No newline at end of file diff --git "a/Java/classify/basis/\345\272\217\345\210\227\345\214\226.md" "b/Java/classify/basis/\345\272\217\345\210\227\345\214\226.md" new file mode 100644 index 00000000..49010d9a --- /dev/null +++ "b/Java/classify/basis/\345\272\217\345\210\227\345\214\226.md" @@ -0,0 +1,14 @@ +> 很多rpc框架都要考虑序列化的问题,但是我没有过于深入 + +# 序列化 +1. 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。 +2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。 +3. 如果想让某个变量不被序列化,使用transient修饰。 +4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。 +5. 反序列化时必须有序列化对象的class文件。 +6. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。 +7. 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。 +8. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。 +9. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。 + +[参考链接](https://juejin.im/post/5ce3cdc8e51d45777b1a3cdf#heading-9) \ No newline at end of file diff --git "a/Java/classify/basis/\345\274\202\345\270\270\344\275\223\347\263\273.md" "b/Java/classify/basis/\345\274\202\345\270\270\344\275\223\347\263\273.md" new file mode 100644 index 00000000..408d4a6a --- /dev/null +++ "b/Java/classify/basis/\345\274\202\345\270\270\344\275\223\347\263\273.md" @@ -0,0 +1,127 @@ + +> 面着面着,就修仙了,还原俺的真实面试场景。 + +# 异常体系 + +面试官:谈谈异常机制? + +我:只要涉及到谈谈,我内心都是崩溃的,不知道该如何说起 + +一上来,我可能先回答:Error和Exception是Throwable类的派生实例。说到此处,面试官还不得问一下Error? + + + +## Error + +面试官:给我讲讲什么是Error? + +我:其实就是明显的给自己挖坑,哈能咋滴!**Error描述了Java运行时系统的内部错误和资源耗尽错误,你比如栈溢出和堆溢出啦**?不好,面试官微笑了。 + +面试官:讲一下什么是栈溢出和堆溢出? + +我:哎,中枪了,这咋害能扯到虚拟机上了 + +- StackOverFlowError:**如果线程请求的栈深度大于虚拟机所允许的深度**,将抛出此异常。比如,**无限递归方法**,其实面试官按捺不住的问 + +面试官:为什么无限递归方法就可以抛出该异常? + +我:因为我们知道,**每次调用方法所产生资源都存放在了虚拟机栈中**,如果无限递归下去,那岂不是? + +面试官:虚拟机栈存了什么资源? + +我:我真的是!虚拟机栈存了**局部变量表、操作数栈、动态链接和方法出口**。 + +面试官:局部变量表中存了什么? + +我:啊?还好我会,存放了编译期可知的各种**基本数据类型(8大基本类型)**,**对象引用类型**,它不等同于对象本身,可能是一个指向对象**起始地址的引用指针**,也可能是指向一个**代表对象的句柄或其他与此对象相关的位置**。 + +面试官:好,开始讲堆溢出 + +我:害能给我绕回来...如果**虚拟机可动态扩展,如果扩展时无法申请到足够的内存**,就会抛出OutOfMemoryError异常,当然,**如果在堆中没有内存完成实例分配,并且堆也无法再扩展时**,也会抛出该异常。比如,我又挖坑,举例子:无限创建线程。这次我主动说原因:操作系统分配给每个进程内存是有限的,比如32位的windows限制为2G。虚拟机提供了参数来控制堆和方法区的内存的最大值,而剩下的内存,忽略其他因素,就由虚拟机栈和本地方法栈“瓜分天下了”。**每个线程分配到栈容越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。** + +面试官:**嘿嘿,方法区会溢出吗?** + +我:嘿嘿,会。比如方法区中有一个**运行时常量池**,晓得吧?其中String.intern()方法是一个native方法,它(1.6)的作用是:如果字符串常量池中已经包含了此String对象的字符串,则返回代表池中这个字符串String对象;**否则,将此String对象所包含的字符串添加到常量池中,并且返回此String对象的引用**。在1.7版本就不一样了,**而是从堆中实例String对象的引用复制到常量池并返回**。当然,还有很多带有**反射**机制的框架,大量使用反射创建类来填满方法区。 + +面试官:嘿嘿,直接内存会溢出吗? + +我:简直了,太能问了。那肯定也是能的哦,比如DirectByteBuffer。 + +## Exception + +面试官:可以了,聊Exception + +我:无限退出递模式!Exception又分解为**RuntimeException**(运行时)和程序本身没有问题,由于像IO错误这类问题导致的异常(编译)。 + +面试官:RuntimeException中有哪些,举一些? + +我:好的,比如,`NullPointerException`,`ArithmeticException`,`ClassCastException`,`ArrayIndexOutOfBoundsException`等 + +面试官:什么是受检异常和非受检异常? + +我:派生于**Error类或RuntimeException类**的所有异常称为非受检异常,所有其他的异常称为受检异常。 + +## 捕获异常 + +面试官:如何捕获异常? + +我: + +- `try` 块: 用于捕获异常。其后可接零个或多个`catch`块,如果没有`catch`块,则必须跟一个`finally`块。 + +- `catch` 块: 用于处理`try`捕获到的异常。 + +- `finally` 块: 无论是否捕获或处理异常,`finally`块里的语句都会被执行。当在`try`块或`catch`块中遇到`return`语句时,`finally`语句块将在方法返回之前被执行。 + +throw 抛出异常,throws是方法可能抛出异常的声明。(用在声明方法时,表示该方法可能要抛出异常) + +注意:finally return会覆盖try和catch的返回值 + +```java +public class Main { + public static void main(String[] args) { + System.out.println(test()); + } + + public static int test() { + int a = 0; + try { + int b = 1 / 0; // 不管发生不发生异常 + a++; + return a; + } catch (Exception e) { + a++; + return a; + } finally { + return 5; + } + } +} +// 结果都是5 +``` + +## 结束 + +面试官:可以,我们换一波问题。 + +我:既然聊到了非受检异常,我还想扯一波Spring事务,Spring事务失效的原因,其中原因之一有非受检异常的原因。 + +简单这里提一下原因: + +1. 数据库引擎不支持事务 + +2. 没有被 Spring 管理 + +3. 方法不是 public 的 + +4. 自身调用问题 + +5. 数据源没有配置事务管理器 + +6. 不支持事务(传播机制) + +7. 异常被吃了(捕获异常) + +8. 异常类型错误(checked异常失效) + + diff --git "a/Java/classify/basis/\346\263\233\345\236\213.md" "b/Java/classify/basis/\346\263\233\345\236\213.md" new file mode 100644 index 00000000..3ffeb7d7 --- /dev/null +++ "b/Java/classify/basis/\346\263\233\345\236\213.md" @@ -0,0 +1,13 @@ + +> 我也没有过多的深入这一块 + +## 泛型 +泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。 + +**好处**: +- **类型安全**,提供编译期间的类型检测 +- **前后兼容** +- **泛化代码,代码可以更多的重复利用** +- **性能较高**,用GJ(泛型JAVA)编写的代码可以为java编译器和虚拟机带来更多的类型信息,这些信息对java程序做进一步优化提供条件 + +[泛型擦除原理](https://www.jianshu.com/p/328efeb01940) \ No newline at end of file diff --git "a/Java/classify/basis/\346\267\261\346\265\205\346\213\267\350\264\235.md" "b/Java/classify/basis/\346\267\261\346\265\205\346\213\267\350\264\235.md" new file mode 100644 index 00000000..d9f2541b --- /dev/null +++ "b/Java/classify/basis/\346\267\261\346\265\205\346\213\267\350\264\235.md" @@ -0,0 +1,9 @@ +# 深浅拷贝 + +- **浅拷贝**:对**基本数据类型进行值传递**,对**引用数据类型进行引用传递般的拷贝**,此为浅拷贝。 +- **深拷贝**:对**基本数据类型进行值传递**,对**引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝** +- 也就二者对引用数据类型有区别 + + +知道结论即可,要想测试,可以[我博客-先占坑]() + diff --git "a/Java/classify/col/\350\260\210\350\260\210\351\233\206\345\220\210.md" "b/Java/classify/col/\350\260\210\350\260\210\351\233\206\345\220\210.md" new file mode 100644 index 00000000..0912a09e --- /dev/null +++ "b/Java/classify/col/\350\260\210\350\260\210\351\233\206\345\220\210.md" @@ -0,0 +1,348 @@ +[toc] + + +> 集合这一块,问就是并发问题,但前提先有总体了解。 + + + +![清晰uml图](https://www.pdai.tech/_images/java_collections_overview.png) + +## 大概 + +面试官:谈谈集合吧 + +我:可以的,我们首先要介绍集合顶层接口**Collection、Map**,而**List、Queue、Set**实现了Collection接口,List又有**ArrayList、LinkedList**,Queue又有**LinkedList、PriorityQueue**,Set又有**HashSet、TreeSet、LinkedHashSet**等。Map又有**HashMap,TreeMap,LinkedHashMap**,当然**HashTable**是继承**Dictionary**接口,实现了Map。 + +此时就开始谈JUC下的集合,比如**HashMap**对应的**ConcurrentHashMap**,**ConcurrentSkipListMap**;比如**ArrayList**对应**CopyOnWriteArrayList**,Set对应的**CopyOnWriteArraySet**等。阻塞队列暂时先不谈哈。 + +## ArrayList和LinkedList的区别 + +- ArrayList是实现了基于**动态数组**的数据结构,LinkedList基于**链表**的数据结构。 +- ArrayList查找速度快,添加和删除慢 +- LinkedList查找慢,添加和删除快 + +## HashMap和HashTable的区别 + +- HashTable是**线程安全的**,每个方法前加了**synchronized**,而HashMap**是非线程安全的** +- HashTable底层是**数组+链表**,而HashMap1.8版本是**数组+链表or红黑树** + + + +## HashMap和ConcurrentHashMap + +- HashMap是非线程安全的,ConcurrentHashMap是线程安全的 +- HashMap和ConcurrentHashMap在1.7都是数组+链表,1.8都是数组+链表or红黑树 +- ConcurrentHashMap在1.7是分段锁,1.8是去掉分段锁改成cas+synchronized + + + +## ArrayList有哪些并发问题 + +> 其实就是**size++** 这一步的问题。 越界就是两个线程临界值去**扩容**都满足,于是一个线程size++导致的,另外一个线程就溢出了,**null就是element[size] = e**,第一个线程还没来得及size++,第二个线程就在原先的索引上把值给覆盖了,并且在下一个索引为null。 + +越界 + +- 列表大小为9,即size=9 +- 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。 +- 线程B此时也进入add方法,它和获取的size的值也为9,也开始调用ensureCapacityInternal方法。 +- 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。 +- 线程B也发现需要大小为10,也可以容纳,返回。 +- 好了,**问题来了哈** +- 线程A开始进行设置值操作,elementData[size++] = e操作。此时size变为10。 +- 线程B也开始进行设置值操作,它尝试设置elementData[10] = e, 而elementData没有进行过扩容,它的下标最大为9 +- 于是此时会报出一个数组越界的异常`ArrayIndexOutOfBoundsException`。 + +null + +- 列表大小为10,即size=0 +- 线程A开始添加元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。也就是说,线程挂在了`element[0] = e`上。 +- 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一条的操作。此时线程B获取的size的值依然为0,于是它将B也放在了elementData下标为0的位置上。 +- **问题来了**,其实上面也是问题,覆盖了。。。 +- 线程A将size的值增加为1 +- 线程B开始将size的值增加为2 +- 当你获取1索引的时候,那不就是null了? + + + +## HashMap的参数 + +- 为什么容量要是 2 的整数次幂? + +因为获取 key 在数组中对应的下标是通过 key 的哈希值与数组长度 -1 进行与运算,如:tab[i = (n - 1) & hash] + +1. n 为 2 的整数次幂,这样 n-1 后之前为 1 的位后面全是 1,这样就能保证 (n-1) & hash 后相应的位数既可能是 0 又可能是 1,这取决于 hash 的值,这样能保证散列的均匀,同时与运算效率高 + +2. 如果 n 不是 2 的整数次幂,会造成更多的 hash 冲突 + +> 举个例子:如 16:10000, 16-1=15:1111, 1111 再与 hash 做 & 运算的时候,各个位置的取值取决于 hash,如果不是2的整数次幂,必然会有的0的位,这样再进行 & 操作的时候就为 0了,会造成哈希冲突。注意:HashMap的tableSizeFor方法做了处理,能保证n永远都是2次幂 + +- 为什么负载因子是0.75? + +> 负载因子过低,频繁扩容,扩容会重新哈希,性能下降;负载因子过高,容易浪费容量.(经验+概率) + +- 为什么红黑树的阈值是8? + +> 在 hash 函数设计合理的情况下,发生 hash 碰撞 8 次的几率为百万分之 6,概率说话。(泊松分布) + +- 为什么退化链表的阈值6? + +> 6是因为如果 hash 碰撞次数在 8 附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。 + +- hash + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +hash 函数是先拿到通过 key 的 hashcode,**是 32 位的 int 值**,然后让 **hashcode 的高 16 位和低 16 位进行异或操作**。这个也叫扰动函数,这么设计有二点原因: + +- **一定要尽可能降低 hash 碰撞,越分散越好**; +- 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算; + +## HashMap的put、get和扩容 + +### put + +![hashmap-put-1.8](https://gitee.com/dreamcater/blog-img/raw/master/uPic/hashmap-put-1.8-WLBoNy.png) + +- 判断数组是否为空,为空进行初始化;(初始化) +- 不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;(通过hash计算index) +- 查看 table[index] 是否存在数据,没有数据就构造一个 Node 节点存放在 table[index] 中;(查看数组中是否哈希冲突) +- 存在数据,说明发生了 hash 冲突(存在二个节点 key 的 hash 值一样), 继续判断 key 是否相等,相等,用新的 value 替换原数据(onlyIfAbsent 为 false);(冲突,判断key是否相等,相等则替换) +- 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;(判断是否红黑树) +- 如果不是树型节点,创建普通 Node 加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树;(判断是否转成红黑树) +- 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。(扩容) + + + +### get + +1. 判断:是否为空,为空,返回null +2. 不为空,判断第一个位置是否为查询key,是,返回value +3. 不是,下一个节点继续判断是否为红黑树,是,按树查找 +4. 不是,按链表查找 + + + +### 扩容 + +> 先说1.7吧 + +```java +for (HashMapEntry e : table) { + // 如果这个数组位置上有元素且存在哈希冲突的链表结构则继续遍历链表 + while (null != e) { + //取当前数组索引位上单向链表的下一个元素 + HashMapEntry next = e.next; + //重新依据hash值计算元素在扩容后数组中的索引位置 + int i = indexFor(e.hash, newCapacity); + e.next = newTable[i]; // 这一步和下一步就是头插法了,并且这两步出现线程不安全死循环问题 + newTable[i] = e; + e = next; // 遍历链表 + } +} +``` + +> 1.8 +> HashMap的扩容使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。也就是说省略了重新计算hash值的时间,而且新增的1位是0还是1机会是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。如果在新表的数组索引位置相同,则链表元素不会倒置。 + + + +### 并发问题 + +- HashMap扩容的时候会调用resize()方法,就是这里的并发操作容易在一个桶上形成环形链表 +- 这样当获取一个不存在的key时,计算出的index正好是环形链表的下标就会出现死循环。 +- **但是1.7的头插法造成的问题,1.8改变了插入顺序,就解决了这个问题,但是为了内存可见性等安全性,还是需要ConCurrentHashMap** +- HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个数组,粒度比较大 +- Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现 +- ConcurrentHashMap 使用分段锁,降低了锁粒度,让并发度大大提高。(jdk1.8 CAS+ synchronized) + + + +## ConcurrentHashMap + +### 1.7 + +#### segment + +- 唯一的区别(和HashMap)就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。 +- ConcurrentHashMap 采用了分段锁技术(相对hashTable降低锁的粒度),其中 Segment 继承于 `ReentrantLock`(可能还会扯AQS)。 +- 不会像HashTable那样不管是put还是get操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。 +- **每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。** + +#### put + +- 虽然HashEntry中的value是用volatile关键字修饰的,但是并不能保证并发的原子性,所以put操作仍然需要加锁处理。 +- 首先第一步的时候会尝试获取锁,如果获取失败肯定就是其他线程存在竞争,则利用 `scanAndLockForPut()` **自旋获取锁**。 + - 尝试获取自旋锁 + - 如果**重试的次数**达到了`MAX_SCAN_RETRIES` 则改为阻塞锁获取,保证能获取成功。 + +总的来说: + +- 将当前的Segment中的table通过key的hashcode定位到HashEntry +- 遍历该HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value +- 不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容 +- 最后会解除在1中所获取当前Segment的锁。 + +#### get + +- 只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。 +- 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。 +- ConcurrentHashMap 的 get 方法是非常高效的,**因为整个过程都不需要加锁。** + +#### size + +在 JDK1.7 中,第一种方案他会**使用不加锁的模式**去尝试多次计算 ConcurrentHashMap 的 size,最多**三次**,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的。 第二种方案是如果第一种方案不符合,他就会给**每个 Segment 加上锁**,然后计算 ConcurrentHashMap 的 size 返回 + +### 1.8 + +1.7 查询遍历链表效率太低(种种原因)。其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性(会扯1.6对synchronized的优化) + +#### put + +- 根据key计算出hashcode +- 判断是否需要进行初始化 +- 如果f为null,说明table中这个位置第一次插入元素,利用Unsafe.compareAndSwapObject方法插入Node节点。 + - 如果CAS成功,说明Node节点已经插入,随后addCount(1L, binCount)方法会检查当前容量是否需要进行扩容。 + - 如果CAS失败,说明有其它线程提前插入了节点,自旋重新尝试在这个位置插入节点。 +- 如果f的hash值为-1,说明当前f是ForwardingNode节点,意味有其它线程正在扩容,则一起进行扩容操作。 +- 如果都不满足,则利用`synchronized`锁写入数据 +- 如果数量大于TREEIFY_THRESHOLD 则要转换为红黑树。 + +#### get + +- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 +- 如果是红黑树那就按照树的方式获取值。 +- 就不满足那就按照链表的方式遍历获取值。 + +#### size + +ConcurrentHashMap 提供了 baseCount、counterCells 两个辅助变量和一个 CounterCell 辅助内部类。sumCount() 就是迭代 counterCells 来统计 sum 的过程。 put 操作时,肯定会影响 size(),在 put() 方法最后会调用 **addCount()** 方法。 + +在addCount()方法中: + +- 如果 counterCells == null, 则对 baseCount 做 CAS 自增操作。 +- 如果并发导致 baseCount CAS 失败了使用 counterCells。 +- 如果counterCells CAS 失败了,在 fullAddCount 方法中,会继续死循环操作,直到成功。 +- CounterCell使用了 @sun.misc.Contended 标记的类 + +> 缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,**如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享**。 + +实际上: + +- JDK1.8 size 是通过对 baseCount 和 counterCell 进行 CAS 计算,最终通过 baseCount 和 遍历 CounterCell 数组得出 size。 +- JDK 8 推荐使用mappingCount 方法,因为这个方法的返回值是 long 类型,不会因为 size 方法是 int 类型限制最大值。 + +[https://zhuanlan.zhihu.com/p/40627259](https://zhuanlan.zhihu.com/p/40627259) + + + +## HashSet底层 + +HashSet中不允许有重复元素,这是因为HashSet是基于HashMap实现的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个`private static final Object PRESENT = new Object();`。 HashSet跟HashMap一样,都是一个存放链表的数组。这样遇到重复元素就可以返回object了(意味着不是null),如果value是null的话,发现重复,那么就返回上一个value值null,那么不符合源码: + +```java +private static final Object PRESENT = new Object(); +public boolean add(E e) { + return map.put(e, PRESENT)==null; +} +System.out.println(map.put(1, o)); // null +System.out.println(map.put(1, o));// java.lang.Object@610455d6 +// 所以重复就false +``` + + + +## TreeSet底层 + +TreeSet底层则采用NavigableMap这个接口来保存TreeSet集合,而实际上NavigableMap只是一个接口,实际上TreeSet还是用TreeMap来保存set元素。 + +```java +TreeSet(NavigableMap m) { + this.m = m; +} + + public TreeSet() { + this(new TreeMap()); + + } +``` + +TreeMap采用一种被称为“红黑树”的排序二叉树来保存Map中的的每个Entry——每个Entry都被当做红黑树的一个节点来对待;TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。 + +## LinkedHashMap + +[可以参考](https://crossoverjie.top/2018/02/06/LinkedHashMap/) + +一般说完这个,可能让你手撸LRU,你可以撸个伪代码即可。 + +```java +// 双向链表+HashMap,Java中的LinkedHashMap就实现了该算法。 +// get +public int get(int key) { + if (map.containsKey(key)) { + Node n = map.get(key); // 获取内存中存在的值,比如A + remove(n); //使用链表的方法,移除该节点 + setHead(n); //依然使用链表的方法,将该节点放入头部 + return n.value; + } + return -1; +} +// put +public void set(int key, int value) { + if (map.containsKey(key)) { + Node old = map.get(key); + old.value = value; + remove(old); // 移除旧节点 + setHead(old); // 放到队头 + } else { + Node created = new Node(key, value); + if (map.size() >= capacity) { + map.remove(end.key); // clear该key + remove(end); //链表也是依次 + setHead(created); // 将created放入队头 + } else { + setHead(created); // 如果没满,直接放入队头 + } + map.put(key,created); + } +} +``` + +```java +//lc: 146. LRU缓存机制 +class LRUCache { + private int cap; + private Map map = new LinkedHashMap<>(); + public LRUCache(int capacity) { + this.cap = capacity; + } + + public int get(int key) { + if (map.containsKey(key)) { + int value = map.get(key); + // 查一次,就将查到到仍在队尾 + map.remove(key); + map.put(key,value); + return value; + } + return -1; + } + + public void put(int key, int value) { + if (map.containsKey(key)) { + map.remove(key); + } else if (map.size() == cap) { + // 满了 + Iterator> iterator = map.entrySet().iterator(); + iterator.next(); + iterator.remove(); + } + map.put(key, value); + } +} +``` + diff --git "a/Java/classify/dis/CAP\345\222\214BASE.md" "b/Java/classify/dis/CAP\345\222\214BASE.md" new file mode 100644 index 00000000..c6f3b126 --- /dev/null +++ "b/Java/classify/dis/CAP\345\222\214BASE.md" @@ -0,0 +1,14 @@ + + +## CAP +- C(一致性):对某个指定的客户端来说,读操作能返回最新的写操作。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。 +- A(可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。 +- P(分区容错性):当出现网络分区后,系统能够继续工作。打个比方,集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。 + +- CP:对于CP来说,放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致。 +- AP:对于AP来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的BASE也是根据AP来扩展。 + +## BASE +BASE是BasicallyAvailable(基本可用)、Softstate(软状态)和Eventuallyconsistent(最终一致性)三个短语的缩写。是对CAP中AP的一个扩展-基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。 +- 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。 +- 最终一致:**最终一致是指经过一段时间后,所有节点数据都将会达到一致**。BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和ACID是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。 \ No newline at end of file diff --git a/Java/classify/dis/Dubbo.md b/Java/classify/dis/Dubbo.md new file mode 100644 index 00000000..64041f3d --- /dev/null +++ b/Java/classify/dis/Dubbo.md @@ -0,0 +1,133 @@ +[toc] + +## Dubbo是什么? + +Dubbo是一款**高性能**、**轻量级**的开源JavaRPC框架,它提供了三大核心能力:**面向接口的远程方法调用**,**智能容错和负载均衡**,以及**服务自动注册和发现**。简单来说Dubbo是一个**分布式服务框架**,致力于提供**高性能和透明化的RPC远程服务调用方案**,以及**SOA服务治理方案。** + +- **负载均衡**——同一个服务部署在不同的机器时该调用那一台机器上的服务。 +- **服务调用链路生成**——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo可以为我们解决服务之间互相是如何调用的。 +- **服务访问压力以及时长统计、资源调度和治理**——基于访问压力实时管理集群容量,提高集群利用率。 +- **服务降级**——某个服务挂掉之后调用备用服务。 + +## Dubbo的图解? +![dubbo架构-qvfIHl](https://gitee.com/dreamcater/blog-img/raw/master/uPic/dubbo架构-qvfIHl.png) + +- **Provider:**暴露服务的服务提供方 +- **Consumer:**调用远程服务的服务消费方 +- **Registry:**服务注册与发现的注册中心 +- **Monitor:**统计服务的调用次数和调用时间的监控中心 +- **Container:**服务运行容器 + +**调用关系说明**: + +- 服务容器负责启动,加载,运行服务提供者。 +- 服务提供者在启动时,向注册中心注册自己提供的服务。 +- 服务消费者在启动时,向注册中心订阅自己所需的服务。 +- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 + +**各个组件总结**: + +- **注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小** +- **监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示** +- **注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外** +- **注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者** +- **注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表** +- **注册中心和监控中心都是可选的,服务消费者可以直连服务提供者** +- **服务提供者无状态,任意一台宕掉后,不影响使用** +- **服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复** + +## Dubbo和SpringCloud的区别? +- **底层**:`Dubbo`底层是使用Netty的NIO框架,基于TCP协议传输,使用Hession序列化完成RPC通信;`SpringCloud`是基于HTTP协议+REST接口调用远程过程的通信,HTTP请求会有更大的报文,占的带宽也会更多。但是REST相比RPC更为灵活,不存在代码级别的强依赖。 +- **集成**:springcloud相关组件多,有自己的注册中心网关等,集成方便,Dubbo需要自己额外去集成。Dubbo是SOA时代的产物,它的关注点主要在于**服务的调用,流量分发、流量监控和熔断**。而SpringCloud诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了Spring、SpringBoot的优势之上,两个框架在开始目标就不一致,Dubbo定位服务治理、SpirngCloud是一个生态。 + + +## Dubbo的容错机制 +1. Failover Cluster失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数 +2. Failfast Cluster快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 +3. Failsafe Cluster失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 +4. Failback Cluster失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 +5. Forking Cluster并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 +6. 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息 + +## Dubbo的限流是怎么实现的? +Dubbo默认是**令牌桶**算法实现限流。在某段时间内,以某种速度向桶里面只能放n个令牌,然后来一个请求就减少一个令牌,如果桶内的令牌没有了,则不能继续执行请求。 + +限流主要是通过TPSLimitFilter实现。 + +## 常见的限流算法有哪些? +- 计数算法 +- 滑动窗口,解决计数算法同一时刻进入很多请求 +- 令牌桶算法 +- 漏桶算法 + + +## 什么是dubbo服务降级? +- dubbo在服务调用时,可能由于服务器宕机、网络超时、并发数太高等,导致调用失败。**服务降级**就是指在非业务异常导致的服务不可用时,可以返回默认值,避免影响主业务的处理。 +- dubbo可以通过mock配置实现服务降级。 + +## Dubbo的注册中心挂了,还可以继续通信吗? +可以。因为在开始初试化的时候,消费者会将提供者的地址等信息拉取到**本地缓存**中。 + +## 负载均衡 +个人理解: +> 比如我们的系统中的某个服务的**访问量特别大**,我们将这个服务部署在了**多台服务器**上,当客户端发起请求的时候,**多台服务器都可以处理这个请求**。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。 + +1. RandomLoadBalance:随机负载均衡。随机的选择一个。是Dubbo的默认负载均衡策略。 +2. RoundRobinLoadBalance:轮询负载均衡。轮询选择一个。 +3. LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机。活跃数指调用前后计数差。使慢的 Provider 收到更少请求,因为越慢的 Provider 的调用前后计数差会越大。 +4. ConsistentHashLoadBalance:一致性哈希负载均衡。相同参数的请求总是落在同一台机器上。 + +[http://dubbo.apache.org/zh-cn/blog/dubbo-loadbalance.html](http://dubbo.apache.org/zh-cn/blog/dubbo-loadbalance.html) + +## 为什么使用zk当dubbo的注册中心 +Zookeeper的数据模型很简单,有一系列被称为ZNode的数据节点组成,与传统的磁盘文件系统不同的是,zk将全量数据存储在内存中,可谓是高性能,而且支持集群,可谓高可用,另外支持事件监听。这些特点决定了zk特别适合作为注册中心(数据发布/订阅)。不过要注意网络闪断引发的节点摘除问题。 + +## 几个服务注册与发现的对比 +[https://zhuanlan.zhihu.com/p/145296163](https://zhuanlan.zhihu.com/p/145296163) + +## SPI源码(过程) +先说一下Java的SPI机制 +Java的SPI机制利用ServiceLoader的load方法传递个接口,就会得到该接口的所有的实现类 +要在指定的META-INF的services下 +但是有一说一,只能通过iterator来遍历判断想要的实现类 +而Dubbo和Spring的SPI比Java的灵活一些,可以通过key来获取对应的实例 + +直接说Dubbo的SPI源码过程 +先说一下Dubbo的SPI机制,不仅支持有着Java的SPI,还有着AOP的功能,同时有着DI功能 +1. 通过getExtensionLoader得到该接口的load,不过获取之间会对一些type检查,同时有缓存机制。 +2. 然后通过load调用getExtension,也是一系列检查和缓存,最关键的就是createExtension +3. 其中getExtensionClasses,这个方法返回对应name的接口的实例对象,接着来到injectExtension注入属性 +4. 如果有wrapper包装,就是通过接口的实例类有木有构造器,如果有,最后injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));无限遍历AOP,也就是构造器注入,最后返回带包装的接口的实例对象。 +5. 以上是没有讲依赖注入的过程,官网上有。 + +## 服务导出与暴漏(源码流程) +源码省略,针对于面试就说源码流程即可 +总结一波吧: +1. 当我们看源码知道,导出和暴漏在IoC初始化的最后一步的finishRefresh中的ServiceBean中。 +2. 其中在onApplicationEvent执行export->doExport,在doExport中首先检查provider呀,register呀,monitor呀等,最后来到关键的一步doExportUrls(); +3. 在这一步当中,实际上,就是对注册的url和导出的url拼接,并且将导出的url远程注册到注册中心,最后暴漏一下自己的url,具体的话就第四步。 +4. doExportUrlsFor1Protocol 包括:1. exportLocal,默认本地导出,2. 远程导出:proxyFactory.getInvoker,然后得到wrapperInvoker,最后就是这个关键了protocol.export(wrapperInvoker),然后会有个子流程去构造buildInvokerChain,调用链。这个是服务调用链路 +5. 实际上找Protocol.class接口的实例代理类,默认是dubbo协议,因此调用的dubbo的实例代理类的export方法,继续使用dubbo协议的url,一步一步绑定nettyClient客户端,最后导出自己的调用链。 + + +## 服务引入与目录(源码过程) + +肯定是ReferenceBean + +1. 当我们看源码知道,首先进来的ReferenceBean的get方法->ReferenceConfig的init方法内部 +2. checkDefault检查消费端的全局配置,接着通过SPI获取消费服务的实现类,经过一些列检查又进入了HashMap的缓存当中 +3. init方法中的最后一步createProxy中,这个方法就是将要引入订阅注册中心的服务端的目录,首先是refprotocol.refer方法从注册中心引入和订阅,该方法是核心。 +4. 首先通过RegistryProtocol的refer中,如果是zk协议,那么就启动zk客户端去连接,接着进入doRefer方法中,先在注册中心,注册消费端服务,接着开始通过subscribe订阅注册中心的目录,category、providers、configurators和routers,然后进入notify,调用listener.notify(categoryList),通知categoryList +5. 这时候来到了协议Dubbo的refer中,开始构造路由链,首先buildInvokerChain调用链,Dubbo启动的是netty客户端哦,debug时候看出来的,获取的是netty的client,最后构建成功就返回。 +6. 最后将所有的目录添加到cluster中,并返回invoker,其实该invoker是MockClusterInvoker,ref是它的代理实现类最后初始化完毕。 + +总感觉处处invoker(执行)类似于发送请求一样。 + +## 服务调用、服务降级和集群容错 +先说一下invorker,在服务引入那里最终返回的是MockClusterInvoker的代理实现类,意思就是说,首先进入Java的动态代理,InvokerInvocationHandler,然后调用invork,进入MockClusterInvoker,然后调用invoke进入默认的FailoverClusterInvoker的invoker。每个invoker就是InvokerDelegate委托实现类 +1. 根据我上面说的,其实从服务目录获取所有的提供者Invokers,在经过MockClusterInvoker的时候,如果配置了服务降级,服务降级就是通过mock机制而已,那么如果调用失败,先走Mock的服务降级策略,如果没有配置,然后开始初始化负载均衡策略, +2. 就进入了容错策略的Invoker类,然后通过负载均衡选择一个invoker,开始调用过滤链,最后才会执行我们的Dubbo协议上的客户端,应该是netty吧,去执行invoker +3. 服务那边开始被触发事件之后,也会执行自己的过滤链,然后最后执行自己的InvokerDelegate服务实现委托类,将结果先返回给自己,然后在通过负责处理请求的控制器传给消费端。 +4. 以上是一次调用过程粗略的经过。 diff --git a/Java/classify/dis/RocketMQ.md b/Java/classify/dis/RocketMQ.md new file mode 100644 index 00000000..2181e150 --- /dev/null +++ b/Java/classify/dis/RocketMQ.md @@ -0,0 +1,169 @@ + + +> 注意:先去了解消息队列:https://www.zhihu.com/question/34243607 + + +## 为什么选择RocketMQ作消息队列 +- ActiveMQ 的社区算是比较成熟,但是较目前来说,**ActiveMQ 的性能比较差,而且版本迭代很慢**,不推荐使用。 +- RabbitMQ 在**吞吐量方面虽然稍逊于 Kafka 和 RocketMQ** ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做erlang源码级别的研究和定制。如果**业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选**。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 +- RocketMQ 阿里出品,**Java 系开源项目**,源代码我们可以直接阅读,然后可以**定制自己公司的MQ**,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。其次具有分布式事务消息的功能,可以达到消息的最终一致性。 +- kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。**kafka 唯一的一点劣势是有可能消息重复消费**,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 + +## RocketMQ组件 +- Producer:消息发布的角色,主要负责把消息发送到Broker,支持分布式集群方式部署。 +- Consumer:消息消费者的角色,主要负责从Broker订阅消息消费,支持分布式集群方式部署。 +- Broker:消息存储的角色,主要负责消息的存储、投递和查询,以及服务高可用的保证,支持分布式集群方式部署。 +- NameServer:是一个非常简单的Topic路由注册中心,其角色类似于Dubbo中依赖的Zookeeper,支持Broker动态注册和发现。 + - 服务注册:NameServer接收Broker集群注册的信息,保存下来作为路由信息的基本数据,并提供心跳检测机制,检查Broker是否存活。 + - 路由信息管理:NameServer保存了Broker集群的路由信息,用于提供给客户端查询Broker的队列信息。Producer和Consumer通过NameServer可以知道Broker集群的路由信息,从而进行消息的投递和消费。 + +## MQ在高并发情况下,假设队列满了如何防止消息丢失? +- 生产者可以采用重试机制。因为消费者会不停的消费消息,可以重试将消息放入队列。 +- 死信队列,可以理解为备胎(推荐) + - 即在消息过期,队列满了,消息被拒绝的时候,都可以扔给死信队列。 + - 如果出现死信队列和普通队列都满的情况,此时考虑消费者消费能力不足,可以对消费者开多线程进行处理。 + +## RocketMQ事务消息原理 + +![mq事务消息-NaWGxH](https://gitee.com/dreamcater/blog-img/raw/master/uPic/mq事务消息-NaWGxH.png) + +想了想,mq也可以 +1. 先给Brock发送一条消息:我要下单了,注意哈 +2. Brock给本地事务回馈消息:ack,好的,我知道了(半投递状态,消费端看不到) +3. 本地事务开始执行业务逻辑,这里首先(校验场次id的座位是否重复,如果没有,直接执行下单:这两个业务非原子,上个锁,要不然可能会出现同样的座位)。下单成功,则返回commit给Brock。消费者此时就可以看到这条消息了。 +4. 如果下单不成功,则返回rollback,Brock一般三天自动删除该无效的消息,消费者也看不到。 +5. 消费者看到了这条消息,调用绑定座位服务,如果失败了,则重试。(消费端不能失败,要不然不能保持一致,如果还是一直失败,则人工处理。) 注意:幂等性 + +## 谈谈死信队列 +**死信队列用于处理无法被正常消费的消息,即死信消息**。 + +当一条消息初次消费失败,**消息队列 RocketMQ 会自动进行消息重试**;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该**消费者对应的特殊队列中**,该特殊队列称为**死信队列**。 + +**死信消息的特点**: + +- 不会再被消费者正常消费。 +- 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。 + +**死信队列的特点**: + +- 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。 +- 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。 +- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。 + +消息队列 RocketMQ 控制台提供对死信消息的查询、导出和重发的功能。 + +## 使用异步消息时如何保证数据的一致性 + +- **借助数据库的事务**:这需要在数据库中创建一个**本地消息表**,这样可以通过**一个事务来控制本地业务逻辑更新**和**本地消息表的写入在同一个事务中**,一旦消息落库失败,则直接全部回滚。如果消息落库成功,后续就可以根据情况基于本地数据库中的消息数据对消息进行重投了。关于本地消息表和消息队列中状态如何保持一致,可以采用 2PC 的方式。在发消息之前落库,然后发消息,在得到同步结果或者消息回调的时候更新本地数据库表中消息状态。然后只需要通过**定时轮询**的方式对状态未已记录但是未发送的消息重新投递就行了。但是这种方案有个前提,就是要求消息的消费者**做好幂等控制**,这个其实异步消息的消费者一般都需要考虑的。 +- 除了使用数据库以外,还可以使用 **Redis** 等缓存。这样就是无法利用关系型数据库自带的事务回滚了。 + +## RockMQ不适用Zookeeper作为注册中心的原因,以及自制的NameServer优缺点? +- ZooKeeper 作为支持**顺序一致性**的中间件,在某些情况下,它为了满足一致性,会丢失一定时间内的**可用性**,RocketMQ 需要注册中心只是为了**发现组件地址**,在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,这同时也是 **NameServer 的缺点,因为 NameServer 集群间互不通信,它们之间的注册信息可能会不一致**。 +- 另外,当有新的服务器加入时,**NameServer 并不会立马通知到 Produer**,而是由 **Produer 定时去请求 NameServer 获取最新的 Broker/Consumer 信息**(这种情况是通过 Producer 发送消息时,负载均衡解决) +- 包括组件通信间使用 Netty 的自定义协议 +- 消息重试负载均衡策略(具体参考 Dubbo 负载均衡策略) +- 消息过滤器(Producer 发送消息到 Broker,Broker 存储消息信息,Consumer 消费时请求 Broker 端从磁盘文件查询消息文件时,在 Broker 端就使用过滤服务器进行过滤) +- Broker 同步双写和异步双写中 Master 和 Slave 的交互 + +## 实现延迟队列 +rocketmq发送延时消息时先把消息按照延迟时间段发送到指定的队列中(rocketmq把每种延迟时间段的消息都存放到同一个队列中)然后通过一个定时器进行轮训这些队列,查看消息是否到期,如果到期就把这个消息发送到指定topic的队列中,这样的好处是同一队列中的消息延时时间是一致的,还有一个好处是这个队列中的消息时按照消息到期时间进行递增排序的,说的简单直白就是队列中消息越靠前的到期时间越早 + +缺点:定时器采用了timer,timer是单线程运行,如果延迟消息数量很大的情况下,可能单线程处理不过来,造成消息到期后也没有发送出去的情况 + +改进点:可以在每个延迟队列上各采用一个timer,或者使用timer进行扫描,加一个线程池对消息进行处理,这样可以提供效率 + +## 消息队列如何保证顺序消费 +生产者中把 orderId 进行取模,把相同模的数据放到 messagequeue 里面,消费者消费同一个 messagequeue,只要消费者这边有序消费,那么可以保证数据被顺序消费。 + +RocketMQ:顺序消息一般使用集群模式,是指对消息消费者内的线程池中的线程对消息消费队列只能串行消费。并并发消息消费最本质的区别是消息消费时必须成功锁定消息消费队列,在Broker端会存储消息消费队列的锁占用情况。 +[https://blog.csdn.net/AAA821/article/details/86650471](https://blog.csdn.net/AAA821/article/details/86650471) + +## RocketMQ消息失败策略 +[https://developer.aliyun.com/article/717340](https://developer.aliyun.com/article/717340) + +## RocketMQ顺序消息消费 +[https://juejin.im/post/6844903982805024782](https://juejin.im/post/6844903982805024782) + +[https://www.cnblogs.com/hzmark/p/orderly_message.html](https://www.cnblogs.com/hzmark/p/orderly_message.html) + +## 零拷贝 +都知道内核和用户态了,不必多说了 +1. 从磁盘复制数据到内核态内存; +2. 从内核态内存复制到用户态内存; +3. 然后从用户态内存复制到网络驱动的内核态内存; +4. 最后是从网络驱动的内核态内存复制到网卡中进行传输。 + +但,通过使用mmap的方式,可以省去向用户态的内存复制,提高速度。这种机制在Java中是通过MappedByteBuffer实现的(零拷贝) + +[https://zhuanlan.zhihu.com/p/83398714](https://zhuanlan.zhihu.com/p/83398714) + +## 消息存储 +### 存储过程 +1. 消息生产者发送消息 +2. MQ收到消息,将消息进行持久化,在存储中新增一条记录 +3. 返回ACK给生产者 +4. MQ push 消息给对应的消费者,然后等待消费者返回ACK +5. 如果消息消费者在指定时间内成功返回ack,那么MQ认为消息消费成功,在存储中删除消息,即执行第6步;如果MQ在指定时间内没有收到ACK,则认为消息消费失败,会尝试重新push消息,重复执行4、5、6步骤 +6. MQ删除消息 + +想说一点,activeMQ的存储介质DB,这就影响了存储效率,其他几位MQ采用的文件系统,并且依照顺序写,极大跟随了SSD的步伐。 + + +### 存储结构 +RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。 + +- CommitLog:存储消息的元数据 +- ConsumerQueue:存储消息在CommitLog的索引 +- IndexFile:为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程 + +## 刷盘机制 +**同步机制**:在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘, 然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。 +1. 封装刷盘请求 +2. 提交刷盘请求 +3. 线程阻塞5秒,等待刷盘结束 + +服务那边: +1. 加锁 +2. 遍历requestsRead +3. 刷盘 +4. 唤醒发送消息客户端 +5. 更新刷盘监测点 + +**异步机制**:在返回写成功状态时,消息**可能**只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。 + +在消息追加到内存后,立即返回给消息发送端。如果开启transientStorePoolEnable,RocketMQ会单独申请一个与目标物理文件(commitLog)同样大小的堆外内存,该堆外内存将使用内存锁定,确保不会被置换到虚拟内存中去,消息首先追加到堆外内存,然后提交到物理文件的内存映射中,然后刷写到磁盘。如果未开启transientStorePoolEnable,消息直接追加到物理文件直接映射文件中,然后刷写到磁盘中。 + +开启transientStorePoolEnable后异步刷盘步骤: + +1. 将消息直接追加到ByteBuffer(堆外内存) +2. CommitRealTimeService线程每隔200ms将ByteBuffer新追加内容提交到MappedByteBuffer中 +3. MappedByteBuffer在内存中追加提交的内容,wrotePosition指针向后移动 +4. commit操作成功返回,将committedPosition位置恢复 +5. FlushRealTimeService线程默认每500ms将MappedByteBuffer中新追加的内存刷写到磁盘 + +## 路由管理 +### 心跳机制 +- RocketMQ路由注册是通过Broker与NameServer的心跳功能实现的。 +- Broker启动时向集群中所有的NameServer发送心跳信息,每隔30s向集群中所有NameServer发送心跳包,NameServer收到心跳包时会更新brokerLiveTable缓存中BrokerLiveInfo的lastUpdataTimeStamp信息,然后NameServer每隔10s扫描brokerLiveTable,如果连续120S没有收到心跳包,NameServer将移除Broker的路由信息同时关闭Socket连接。 + +### 删除路由 +- `Broker`每隔30s向`NameServer`发送一个心跳包,心跳包包含`BrokerId`,`Broker`地址,`Broker`名称,`Broker`所属集群名称、`Broker`关联的`FilterServer`列表。但是如果`Broker`宕机,`NameServer`无法收到心跳包,此时`NameServer`如何来剔除这些失效的`Broker`呢? +- `NameServer`会每隔10s扫描`brokerLiveTable`状态表,如果`BrokerLive`的**lastUpdateTimestamp**的时间戳距当前时间超过120s,则认为`Broker`失效,移除该`Broker`,关闭与`Broker`连接,同时更新`topicQueueTable`、`brokerAddrTable`、`brokerLiveTable`、`filterServerTable`。 + +### 路由发现 +RocketMQ路由发现是非实时的,当Topic路由出现变化后,NameServer不会主动推送给客户端,而是由客户端定时拉取主题最新的路由。 + +其实就是一张图 +![](https://imgkr.cn-bj.ufileos.com/6ac077ee-bc03-4785-9200-426457563858.png) + + + + +## 长轮训 + +拉取长轮询分析:源码上实际上还是个监听器 +- RocketMQ未真正实现消息推模式,而是消费者主动向消息服务器拉取消息,RocketMQ推模式是循环向消息服务端发起消息拉取请求,如果消息消费者向RocketMQ拉取消息时,消息未到达消费队列时,如果不启用长轮询机制,则会在服务端等待shortPollingTimeMills时间后(挂起)再去判断消息是否已经到达指定消息队列,如果消息仍未到达则提示拉取消息客户端PULL—NOT—FOUND(消息不存在); +- 如果开启长轮询模式,RocketMQ一方面会每隔5s轮询检查一次消息是否可达,同时一有消息达到后立马通知挂起线程再次验证消息是否是自己感兴趣的消息,如果是则从CommitLog文件中提取消息返回给消息拉取客户端,否则直到挂起超时,超时时间由消息拉取方在消息拉取是封装在请求参数中,PUSH模式为15s,PULL模式通过DefaultMQPullConsumer#setBrokerSuspendMaxTimeMillis设置。RocketMQ通过在Broker客户端配置longPollingEnable为true来开启长轮询模式。 + + +说白了, push模式只不过在pull模式下加了个监控......哈哈哈哈 \ No newline at end of file diff --git a/Java/classify/dis/Sentinel.md b/Java/classify/dis/Sentinel.md new file mode 100644 index 00000000..26b077e9 --- /dev/null +++ b/Java/classify/dis/Sentinel.md @@ -0,0 +1,35 @@ + +## 为什么选择Sentinel? +Sentinel是一个面向分布式架构的轻量级服务保护框架,主要以流量控制、熔断降级、系统负载保护等多个维度。 + +隔离策略:信号量隔离(并发线程数限流) + +熔断策略: +1. 基于响应时间 +2. 异常比率 +3. 异常数 + +限流:基于QPS限流 + +控制台:查看秒级监控、机器发现等。 + +## 服务限流 +当**系统资源不够,不足以应对大量请求**,对系统按照预设的规则进行流量限制或功能限制 + +## 服务熔断 +当**调用目标服务的请求和调用大量超时或失败,服务调用方为避免造成长时间的阻塞造成影响其他服务**,后续对该服务接口的调用不再经过进行请求,直接执行本地的默认方法 + +## 服务降级 +**为了保证核心业务在大量请求下能正常运行,根据实际业务情况及流量,对部分服务降低优先级**,有策略的不处理或用简单的方式处理 + +## 为什么熔断降级 +系统承载的访问量是有限的,如果不做流量控制,会导致系统资源占满,服务超时,从而所有用户无法使用,通过服务限流控制请求的量,服务降级省掉非核心业务对系统资源的占用,最大化利用系统资源,尽可能服务更多用户 + +## 和Hystrix对比 +![sentinel和hystrix-qb3wFi](https://gitee.com/dreamcater/blog-img/raw/master/uPic/sentinel和hystrix-qb3wFi.png) + +**值得补充的是**:相比 Hystrix 基于线程池隔离进行限流,这种方案**虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响**。 + +Sentinel 并发线程数限流不负责创建和管理线程池,而是**简单统计当前请求上下文的线程数目,如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离**。 + +[官网补充](http://dubbo.apache.org/zh-cn/blog/sentinel-introduction-for-dubbo.html) \ No newline at end of file diff --git a/Java/classify/dis/zookeeper.md b/Java/classify/dis/zookeeper.md new file mode 100644 index 00000000..81e87ca1 --- /dev/null +++ b/Java/classify/dis/zookeeper.md @@ -0,0 +1,40 @@ + +## 什么是Zookeeper? +ZooKeeper 是一个开源的分布式协调服务 + +## Zookeeper使用场景? +1. 数据发布/订阅、 +2. 负载均衡、 +3. 命名服务、 +4. 分布式协调/通知、 +5. 集群管理、 +6. Master 选举、 +7. 分布式锁和分布式队列等功能。 + +## Zookeeper的特点 +- 顺序一致性: 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。 +- 原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。 +- 单一系统映像 : 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。 +- 可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。 + +## Zookeeper的原理? +ZAB 协议&Paxos算法 +ZAB协议包括两种基本的模式:**崩溃恢复**和**消息广播**。当整个 Zookeeper 集群刚刚启动或**者Leader服务器宕机**、**重启**或者网络故障导致**少于过半的服务器与 Leader 服务器保持正常通信**时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步。当集群中超过**半数机器与该 Leader 服务器完成数据同步**之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。 + +## 选择算法和流程 + + +先说: +> 每次投票会包含所推举的服务器的 myid 和 ZXID、epoch,使用(myid, ZXID,epoch)来表示 +> zxid,也就是事务 id,为了保证事务的顺序一致性,zookeeper 采用了递增的事务 id 号(zxid)来标识事务。 +> PK 1. 优先检查 ZXID。ZXID 比较大的服务器优先作为Leader 2. 如果 ZXID 相同,那么就比较 myid。myid 较大的服务器作为 Leader 服务器。 + +目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下: + +1. 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。 +2. 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。 +3. 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为leader,服务器1,2成为follower。 +4. 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为follower。 +5. 服务器5启动,后面的逻辑同服务器4成为follower。 + +[https://www.cnblogs.com/wuzhenzhao/p/9983231.html](https://www.cnblogs.com/wuzhenzhao/p/9983231.html) \ No newline at end of file diff --git "a/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\256\227\346\263\225.md" "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\256\227\346\263\225.md" new file mode 100644 index 00000000..e13c1969 --- /dev/null +++ "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\256\227\346\263\225.md" @@ -0,0 +1,39 @@ + +## paxos算法 +> 要讲这个算法,还要先扯背景:在常见的分布式系统中,总会发生诸如机器宕机或网络异常(等情况。Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。 + +> 其实在整个提议和投票过程当中,主要的角色就是“提议者”和“接受者” + +该算法大致流程:其实分为两个阶段 + +1. 因为存在多个“提议者”Proposer,如果都提意见,那么“接受者”Acceptor不就炸掉了嘛?到底接受谁啊?所以,要先明确哪个“提议者”是领袖,最厉害的那个,先把这个给挑出来。尽早的让意见统一,并且早点形成多数派。 +2. 由上阶段选出的意见领袖提出提议,“接受者”反馈意见。如果多数“接受者”接受了一个提议,那么这个提议就通过了。 + +[例子](https://ocavue.com/paxos.html#%E8%8A%82%E7%82%B9%E6%95%85%E9%9A%9C%E7%9A%84%E4%BE%8B%E5%AD%90) + +## ZAB +- ZAB协议包括两种基本的模式:**崩溃恢复**和**消息广播**。 +- 当整个 Zookeeper 集群刚刚启动或**者Leader服务器宕机**、**重启**或者网络故障导致**少于过半的服务器与 Leader 服务器保持正常通信**时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步。 +- 当集群中超过**半数机器与该 Leader 服务器完成数据同步**之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。 + +## zk的leader选举算法和流程 +目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下: + +1. 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。 +2. 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。 +3. 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为leader,服务器1,2成为follower。 +4. 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为follower。 +5. 服务器5启动,后面的逻辑同服务器4成为follower。 + +[https://www.cnblogs.com/wuzhenzhao/p/9983231.html](https://www.cnblogs.com/wuzhenzhao/p/9983231.html) + + +## raft + +[https://zhuanlan.zhihu.com/p/66441389](https://zhuanlan.zhihu.com/p/66441389) + + + +## 分布式一致性哈希 + +[https://zhuanlan.zhihu.com/p/24440059](https://zhuanlan.zhihu.com/p/24440059) \ No newline at end of file diff --git "a/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" new file mode 100644 index 00000000..54053faa --- /dev/null +++ "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" @@ -0,0 +1,74 @@ + + +> 无非就是在分布式或者集群的环境下,业务上存在很多上游调用下游链,所以存在是了事务或者数据不一致的情况。 + +## 2PC +![2pc1-DMjtyZ](https://gitee.com/dreamcater/blog-img/raw/master/uPic/2pc1-DMjtyZ.jpg) + +--- +第一阶段: +- 协调者 向所有的 参与者 发送事务预处理请求,称之为Prepare,并开始等待各 参与者 的响应。 +- 各个 参与者 节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向 协调者 报告说:“我这边可以处理了/我这边不能处理”。. +- 如果 参与者 成功执行了事务操作,那么就反馈给协调者 Yes 响应,表示事务可以执行,如果没有 参与者 成功执行事务,那么就反馈给协调者 No 响应,表示事务不可以执行。 + +图就不放了,很简单 +第二阶段:成功 +- 协调者 向 所有参与者 节点发出Commit请求. +- 参与者 收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源。 + +缺点: +- 性能问题:无论是在第一阶段的过程中,还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有当所有节点准备完毕,事务 协调者 才会通知进行全局提交,参与者 进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。 +- 单节点故障:由于协调者的重要性,一旦 协调者 发生故障。参与者 会一直阻塞下去。尤其在第二阶段,协调者 发生故障,那么所有的 参与者 还都处于锁定事务资源的状态中,而无法继续完成事务操作。(虽然协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题) + +2PC出现单点问题的三种情况: +- 协调者正常,参与者宕机:由于 协调者 无法收集到所有 参与者 的反馈,会陷入阻塞情况。解决方案:引入超时机制,如果协调者在超过指定的时间还没有收到参与者的反馈,事务就失败,向所有节点发送终止事务请求。 +- 协调者宕机,参与者正常:无论处于哪个阶段,由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交状态的参与者都会陷入阻塞情况.解决方案:引入协调者备份,同时协调者需记录操作日志.当检测到协调者宕机一段时间后,协调者备份取代协调者,并读取操作日志,向所有参与者询问状态。 +- 协调者和参与者都宕机 + + +## 3PC +对2pc的优化 + +- 引入超时机制 +- 在第一阶段和第二阶段中插入一个准备阶段,尝试获取数据库锁。如果可以就yes + +## TCC +Try-Confirm-Cancel +- 先是服务调用链路依次执行 Try 逻辑。 +- 如果都正常的话,TCC 分布式事务框架推进执行 Confirm 逻辑,完成整个事务。 +- 如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执行各个服务的 Cancel 逻辑,撤销之前执行的各种操作。 + +这就是所谓的 TCC 分布式事务。TCC 分布式事务的核心思想,说白了,就是当遇到下面这些情况时: +- 某个服务的数据库宕机了。 +- 某个服务自己挂了。 +- 那个服务的 Redis、Elasticsearch、MQ 等基础设施故障了。 +- 某些资源不足了,比如说库存不够这些。 + + +先来 Try 一下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。 + +如果 Try 都 OK,也就是说,底层的数据库、Redis、Elasticsearch、MQ 都是可以写入数据的,并且你保留好了需要使用的一些资源(比如冻结了一部分库存)。 + +接着,再执行各个服务的 Confirm 逻辑,基本上 Confirm 就可以很大概率保证一个分布式事务的完成了。 + +那如果 Try 阶段某个服务就失败了,比如说底层的数据库挂了,或者 Redis 挂了,等等。 + +此时就自动执行各个服务的 Cancel 逻辑,把之前的 Try 逻辑都回滚,所有服务都不要执行任何设计的业务逻辑。保证大家要么一起成功,要么一起失败。 + +等一等,你有没有想到一个问题?如果有一些意外的情况发生了,比如说订单服务突然挂了,然后再次重启,TCC 分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢? + +所以,TCC 事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。 + +问题还没完,万一某个服务的 Cancel 或者 Confirm 逻辑执行一直失败怎么办呢? + +那也很简单,TCC 事务框架会通过活动日志记录各个服务的状态。举个例子,比如发现某个服务的 Cancel 或者 Confirm 一直没成功,会不停的重试调用它的 Cancel 或者 Confirm 逻辑,务必要它成功! + +当然了,如果你的代码没有写什么 Bug,有充足的测试,而且 Try 阶段都基本尝试了一下,那么其实一般 Confirm、Cancel 都是可以成功的! + +## 可靠消息最终一致性 +在上面的通用方案设计里,完全依赖可靠消息服务的各种自检机制来确保: + +- 如果上游服务的数据库操作没成功,下游服务是不会收到任何通知。 +- 如果上游服务的数据库操作成功了,**可靠消息服务死活都会确保将一个调用消息投递给下游服务,而且一定会确保下游服务务必成功处理这条消息**。 + +通过这套机制,保证了基于 MQ 的异步调用/通知的服务间的分布式事务保障。其实阿里开源的 RocketMQ,就实现了可靠消息服务的所有功能,核心思想跟上面类似。 \ No newline at end of file diff --git "a/Java/classify/dis/\345\210\206\345\270\203\345\274\217\346\246\202\345\277\265.md" "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\346\246\202\345\277\265.md" new file mode 100644 index 00000000..492bc810 --- /dev/null +++ "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\346\246\202\345\277\265.md" @@ -0,0 +1,9 @@ +## 集群、分布式和微服务的概念 +微服务是一种架构风格,一个大型复杂软件应用由**一个或多个微服务组成**。系统中的**各个微服务可被独立部署,各个微服务之间是松耦合的**。**每个微服务仅关注于完成一件任务并很好地完成该任务**。在所有情况下,每个任务代表着一个小的业务能力。 + +- 分布式将一个大的系统划分为多个业务模块,**业务模块分别部署到不同的机器上**,各个业务模块之间通过接口进行数据交互。区别**分布式的方式是根据不同机器不同业务**。 +- 集群模式是**不同服务器部署同一套服务对外访问,实现服务的负载均衡**。区别集群的方式是根据部署多台服务器业务是否相同。 +- 微服务的设计是为了不因为某个模块的升级和BUG影响现有的系统业务。微服务与分布式的细微差别是,**微服务的应用不一定是分散在多个服务器上,他也可以是同一个服务器**。 + +## 什么是RPC? +RPC(RemoteProcedureCall)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务A、B部署在两台不同的机器上,那么服务A如果想要调用服务B中的某个方法该怎么办呢?使用HTTP请求当然可以,但是可能会比较慢而且一些优化做的并不好。RPC的出现就是为了解决这个问题。 \ No newline at end of file diff --git "a/Java/classify/dis/\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" "b/Java/classify/dis/\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" new file mode 100644 index 00000000..e228756e --- /dev/null +++ "b/Java/classify/dis/\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" @@ -0,0 +1,7 @@ +## 布隆过滤器原理 +- 布隆过滤器可以检查值是 “可能在集合中” 还是 “绝对不在集合中”。 +- 布隆过滤器(Bloom Filter)本质上是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0 +- 为了将数据项添加到布隆过滤器中,我们会提供 K 个不同的哈希函数,并将结果位置上对应位的值置为 “1” + +总结:当我们搜索一个值的时候,若该值经过 K 个哈希函数运算后的任何一个索引位为 ”0“,那么该值肯定不在集合中。但如果所有哈希索引值均为 ”1“,则只能说该搜索的值可能存在集合中。 +[5分钟搞懂布隆过滤器,亿级数据过滤算法你值得拥有!](https://juejin.im/post/5de1e37c5188256e8e43adfc) \ No newline at end of file diff --git "a/Java/classify/dis/\351\231\220\346\265\201\347\256\227\346\263\225.md" "b/Java/classify/dis/\351\231\220\346\265\201\347\256\227\346\263\225.md" new file mode 100644 index 00000000..0accc1cb --- /dev/null +++ "b/Java/classify/dis/\351\231\220\346\265\201\347\256\227\346\263\225.md" @@ -0,0 +1,34 @@ + +> 在大型电商等系统中,我们回努力提升API的吞吐量和QPS(Query Per Second 每秒查询量),但总归有上限.为了应对巨大流量的瞬间提交,我们会做对应的限流处理. + +常见的限流算法有计数器,漏桶,令牌桶. + +## 计数器 + +计数器限流方式比较粗暴,**一次访问设置一次计数**,在系统内设置每秒的访问量,**超过访问量的访问直接丢弃**,实现访问限流.这种算法的弊端就是,在开始的时间,访问量被使用完后,1S内会有长时间的真空期是处于接口不可用的状态的. + +实现方式和拓展方式很多.比如可以使用redis进行1S的100次访问计数,来一个流量100-1当数量到达0时,拒绝后续的访问.也可以不拒绝而是将请求放入缓存队列,根据实际业务情况选择不同的实现方式. + +## 漏斗 + +在计数器算法中我们看到,当使用了所有的访问量后,接口会完全处于不可用状态.有些系统不喜欢这样的处理方式,可以选择漏斗算法进行限流. 漏斗算法的原理就像名字,是一个漏斗,**访问量从漏斗的大口进入,从漏斗的小口进入系统**.这样不管是多大的访问量进入漏斗,最后进入系统的访问量都是固定的.漏斗的好处就是,大批量访问进入时,漏斗有容量,不超过容量(容量的设计=固定处理的访问量*可接受等待时长)的数据都可以排队等待处理,超过的才会丢弃. + +实现方式可以使用队列,队列设置容量,访问可以大批量塞入队列,满队列后丢弃后续访问量.队列的出口以固定速率拿去访问量处理. + +这种方案由于出口速率是固定的,那么当就无法应对短时间的突发流量. + +## 令牌桶 + +令牌桶算法算是漏斗算法的改进版,为了处理短时间的突发流量而做了优化,令牌桶算法主要由三部分组成令牌流、数据流、令牌桶. + +- 令牌流:流通令牌的管道,用于生成的令牌的流通,放入令牌桶中. +- 数据流:进入系统的数据流量 +- 令牌桶:保存令牌的区域,可以理解为一个缓存区.令牌保存在这里用于使用. + +令牌桶算法会**按照一定的速率生成令牌放入令牌桶,访问要进入系统时,需要从令牌桶获取令牌,有令牌的可以进入,没有的被抛弃**.**由于令牌桶的令牌是源源不断生成的,当访问量小时,可以留存令牌达到令牌桶的上限,这样当短时间的突发访问量来时,积累的令牌数可以处理这个问题.当访问量持续大量流入时,由于生成令牌的速率是固定的,最后也就变成了类似漏斗算法的固定流量处理.** + +实现方式和漏斗也比较类似,可以使用一个队列保存令牌,一个定时任务用等速率生成令牌放入队列,访问量进入系统时,从队列获取令牌再进入系统. + +Google开源的guava包中RateLimiter类实现了令牌桶算法,不过这是单机的.集群可以按照上面的实现方式实现,队列使用中间件MQ实现,配合负载均衡算法,考虑集群各个服务器的承压情况做对应服务器的队列是比较建议的做法. + +[参考](https://zhuanlan.zhihu.com/p/95066428) \ No newline at end of file diff --git "a/Java/classify/jvm/JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" "b/Java/classify/jvm/JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" new file mode 100644 index 00000000..00aa7025 --- /dev/null +++ "b/Java/classify/jvm/JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" @@ -0,0 +1,71 @@ + +# JVM内存区域 +> JVM这一块,经常还是经常被问到的 + +面试官:讲一讲JVM内存区域 + +我:行,先放两张图 + +![JVM内存模型-1.8之前](https://gitee.com/dreamcater/blog-img/raw/master/uPic/JVM内存模型-1.8之前-EKvhGB.png) + +![JVM内存模型-1.8](https://gitee.com/dreamcater/blog-img/raw/master/uPic/JVM内存模型-1.8-y9XNlT.png) + +总体来说,粗略的分为**堆和栈**,那么**栈是线程私有的**,而**堆是线程共享的**。那么**栈**又问分为**程序计数器**,**虚拟机栈**,**本地方法栈**。堆稍后再说,当然还有**方法区**,稍后单独说。 + +## 程序计数器 + +- **字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制**,如:顺序执行、选择、循环、异常处理。 +- 在多线程的情况下,**程序计数器用于记录当前线程执行的位置**,**从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了**。 +- **程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域**,它的生命周期随着线程的创建而创建,随着线程的结束而死亡 + +## 虚拟机栈 + +- 说白了,通俗的讲,主要是**对象中的方法产生的各种"材料"**。 +- 因此,虚拟机栈存放的是**局部变量表**、**操作数栈**、**动态链接**、**方法出口**。 +- 局部变量表存8**大基本数据类型以及引用类型**。 +- 当然,栈也会非常error: + - **StackOverFlowError**: 若 J**ava 虚拟机栈的内存大小不允许动态扩展**,**那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候**,就抛出 StackOverFlowError 异常。 + - **OutOfMemoryError**:若 **Java 虚拟机栈的内存大小允许动态扩展**,**且当线程请求栈时内存用完了**,**无法再动态扩展了**,此时抛出 OutOfMemoryError 异常。 + +## 本地方法栈 + +虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而**本地方法栈则为虚拟机使用到的 Native 方法服务**,JDK源码中很多本地方法哦。 + +## 方法区 + +这里单独说一下**方法区**: + +**方法区与 Java 堆一样,是各个线程共享的内存区域**,它用于存储已被虚拟机加载的**类信息**、**常量**、**静态变量**、即时编译器编译后的代码等数据。不过随着版本的变化,会发生变化。 + +- 1.6:运行时常量池在Perm Gen区(也就是方法区)中;字符串常量池在**运行时常量池**中。 +- 1.7:运行时常量池依然在Perm Gen区(也就是方法区)中在JDK7版本中,永久代的转移工作就已经开始了,将譬如**符号引用转移到了native heap**;**字面量转移到了java heap**;**类的静态变量转移到了java heap**。但是运行时常量池依然还存在,只是很多内容被转移,其只存着这些被转移的引用;字符串常量池被分配到了**Java堆的主要部分**。也就是字符串常量池从运行时常量池分离出来了。 +- 1.8:JVM已经将**运行时常量池从方法区中移了出来**,在**Java 堆(Heap)中开辟了一块区域存放运行时常量池**。同时永久代被移除,**以元空间代替**。**元空间并不在虚拟机中,而是使用本地内存**;**字符串常量池存在于Java堆中**。 + +方法区,依然会发生error,因为在之前的版本中,**当一个类启动的时候,也会加载很多class文件**,那么也会充满整个方法区,当满的时候,也会error的,当然,在以前的版本中,字符串常量池在方法区中,而**使用String.intern()方法,依然会占满空间并error**。 + +## 直接内存 + +**直接内存**并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现(如**DirectByteBuffer**)。本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。 + +## 堆 + +**堆**:此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 + +- 分为四区,分别为eden区、s0("From)区、s1("To")和tentired +- 在初始阶段,新创建的对象被分配到Eden区,survivor的两块空间都为空。 +- 当Eden区满了的时候,minor GC触发 +- 经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收 +- 在下一次的Minor GC中,Eden区的情况和上面一致,没有引用的对象被回收,存活的对象被复制到survivor区。然而在survivor区,S0的所有的数据都被复制到S1,需要注意的是,在上次minor GC过程中移动到S0中的相同存活的对象在复制到S1后其年龄要加1。此时Eden区S0区被清空,所有存活的数据都复制到了S1区,并且S1区存在着年龄不一样的对象(重点) +- 再下一次MinorGC则重复这个过程,这一次survivor的两个区对换,存活的对象被复制到S0,存活的对象年龄加1,Eden区和另一个survivor区被清空。 + +## Minor GC和Full GC + +**Minor GC和Full GC触发条件**: + +- Minor GC触发条件:当Eden区满时,触发Minor GC。 +- Full GC触发条件: + 1. 调用System.gc时,系统建议执行Full GC,但是不必然执行 + 2. 老年代空间不足 + 3. **方法区**空间不足 + 4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存 + 5. 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小 \ No newline at end of file diff --git "a/Java/classify/jvm/\345\236\203\345\234\276\345\233\236\346\224\266.md" "b/Java/classify/jvm/\345\236\203\345\234\276\345\233\236\346\224\266.md" new file mode 100644 index 00000000..f70f83ca --- /dev/null +++ "b/Java/classify/jvm/\345\236\203\345\234\276\345\233\236\346\224\266.md" @@ -0,0 +1,136 @@ +# 垃圾回收 + +> 这里可以主要讲CMS和G1,简单谈一些其他的 + +## JVM垃圾回收 + +面试官:知道JVM垃圾回收机制吗? + +我:知道,首先说一下垃圾回收算法,因为其中的一个分代收集,所以让堆又分为年轻代和老年代。 + +## 垃圾回收算法 + +垃圾回收算法: + +1. **标记-清除** + +该算法分为“标记”和“清除”阶段:首先**标记出所有需要回收的对象**,在标记完成后**统一回收所有被标记的对象**。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题: + +- **效率问题** +- **空间问题(标记清除后会产生大量不连续的碎片)** + +2. **标记-整理** + +根据**老年代的特点**提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是**让所有存活的对象向一端移动**,然后直接清理掉端边界以外的内存。(老年代一般存入的是大对象,时间比较久的对象) + +3. **复制** + +为了**解决效率**问题,“复制”收集算法出现了。它可以将**内存分为大小相同的两块**,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。**这样就使每次的内存回收都是对内存区间的一半进行回收**。(堆的年轻代又分为Eden、s0和s1) + +4. **分代** + +**比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。** + +一般情况: + +- 大多数情况下,**对象在新生代中 eden 区分配**。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC. +- **大对象直接进入老年代**,大对象就是需要**大量连续内存空间的对象**(比如:字符串、数组)。频繁复制降低性能。 +- 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1. 对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被**晋升到老年代**中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 + +## 如何判断死亡 + +面试官:如何判断对象死亡? + +我:有两种策略,其一为**引用计数法**,其二为**可达性分析**。 + +1. **引用计数法** + +给对象中添加一个引用计数器,每当有一个地方**引用它**,**计数器就加 1**;**当引用失效**,**计数器就减 1**;任何时候**计数器为 0 的对象就是不可能再被使用的**。 + +**这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。** + +2. **可达性分析** + +这个算法的基本思想就是通过一系列的称为 **“GC Roots”** 的对象作为起点,**从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的**。 + +哪些可以作为GC Roots的根: + +- 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中应用的对象。 +- 本地方法栈中JNI(native方法)引用的对象 +- 方法区中的类静态属性引用的对象 +- 方法区中常量引用的对象 + +面试官:如何枚举根节点? + +我:以上引用作用GC Roots的根,如果**方法区和大,要逐个检查这里面的引用,那么必然会消耗很多时间**,而且枚举根节点需要停顿的。在HotSpot的实现中,是使用一组称为**OopMap的数据结构**来达到这个目的的,在类加载完成的时候,**HotSpot就把对象内什么偏移量是什么类型的数据计算出来**,在JIT编译过程中,**也会在特定的位置记录下栈和寄存器中哪些位置是引用**。这样,GC在扫描时就可以直接得这心信息了。 + +但一个很现实的问题:**可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那么会需要大量的额外空间,这样GC成本很高,安全点由此而来**。 + +实际上,在JIT编译过程中,在**特定的位置记录下栈和寄存器哪些位置是引用**,实际上这些位置就是**安全点**,意思就是说,**程序执行时并非在所有地方都能停顿下来开始GC,只有在达到安全点时才能暂停**。 + +Safepoint机制保证了程序执行时,在**不太长的时间内就会遇到可进入GC的Safepoint**,但**如果线程处于Sleep或者Blocked状态**,这时候线程**无法响应JVM的中断请求**,JVM也显然不太可能等待线程重新被分配CPU时间,这种情况就需要**安全域**来解决。**安全域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。这时候安全点就被扩展到了Safe Region**。 + +## 垃圾回收器 + +面试官:给我讲讲垃圾收集器吧 + +我:当然没问题,有一张有趣的图 + +![垃圾回收器-DSVRAO](https://gitee.com/dreamcater/blog-img/raw/master/uPic/垃圾回收器-DSVRAO.png) + +小插曲:咱们知道,堆分为新生代和老年代,那么从这张图可以看出,新生代有Serial、ParNew和Parallel Scavenge而老年代自然也有Serial Old和Parallel Old,新生代和老年代都有串并行收集器,能互相搭配,但看CMS就很特殊,它是老年代的收集器,能从图中可以看出来,它不稳定呀,居然用Serial Old当备胎,而且为了能搭配CMS的并行垃圾收集器,就给它造了一个ParNew,哈哈哈(开个玩笑)。G1暂且不说,横跨新生和老年。在它这一块不分家,一把抓。 + +我就简单说一下串并行垃圾收集器,太古老了,面试官也不想听。 + +你像Serial和ParNew呀,其实在STW的时候,一个是**单线程**,一个是**多线程**回收垃圾。而ParNew和Parallel Scavenge的区别仅仅是**吞吐量**,后者重在吞吐量上(高效率利用CPU)。所以,**Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。而ParNew是在Server 模式下的虚拟机的首要选择之一。以上垃圾收集器新生代采用复制,而老年代采用标记-整理。** + +**CMS垃圾收集器**: + +**CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。** + +**CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。** + +从名字中的**Mark Sweep**这两个词可以看出,CMS 收集器是一种 **“标记-清除”算法**实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤: + +- 初始标记:暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; +- 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 +- 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 +- 并发清除:开启用户线程,同时 GC 线程开始对为标记的区域做清扫。 + + +从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:**并发收集、低停顿**。但是它有下面三个明显的缺点: + +- **对 CPU 资源敏感;** +- **无法处理浮动垃圾;** +- **它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。** + +因此,为了解决以上缺点,**G1**就出现了: + +- **将整个Java堆划分为多个大小相等的独立区域(Region)**,虽然还保留新生代和老年代的概念,但**新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合** + +- 并行与并发: G1 能充分利用 **CPU、多核**环境下的硬件优势,使用多个 CPU来缩短 Stop-The-World 停顿时间。 +- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 +- 空间整合:G1 从整体来看是基于**“标记整理”**算法实现的收集器;从局部上来看是基于**“复制”算法**实现的。 +- 可预测停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型。G1跟踪**各个Region里面的垃圾堆积的价值大小**(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个**优先列表**,每次根据允许的收集时间,**优先回收价值最大的Region**。 + +**G1的跨代引用**: + +在G1收集器中,**Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用**,虚拟机都是使用**Remembered Set(RS)**来避免全堆扫描的。**G1中每个Region都有一个与之对应的RS**,虚拟机发现程序**对Reference类型的数据进行写操作**时,会产生**一个Write Barrier暂时中断操作**,**检查Reference引用的对象是否处于不同的Region之间**(在分代的例子中就是检查是否老年代中的对象引用了新生代中方的对象)如果是,便**通过CardTable(每个Region块又细分了2000多个卡表,记录一波我引用了哪个对象)把相关引用信息记录到被引用对象所属的Region的RS之中**。当进行内存回收时,**在GC根节点的枚举范围中加入RS即可保证不对全堆扫描,也不会又遗漏**。 + +当然G1有也大致的四个过程: + +- 初始标记:仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改**TAMS(Nest Top Mark Start)**的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要**停顿线程**,但耗时很短。 +- 并发标记:从GC Root 开始对堆中对象进行**可达性分析**,找到存活对象,此阶段耗时较长,但**可与用户程序并发执行**。 +- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在**线程的Remembered Set Logs**里面,最终标记阶段需要**把Remembered Set Logs的数据合并到Remembered Set中**,这阶段需要**停顿线程**,但是**可并行执行**。 +- 筛选回收:首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 + +在这里,简单做一个CMS和G1的比较: + +1. CMS收集器是**获取最短回收停顿时间**为目标的收集器,因为CMS工作时,GC工作线程与用户线程可以并发执行,以此来达到降低收集停顿时间的目的(只有初始标记和重新标记会STW)。但**是CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降**。 +2. CMS仅作用于老年代,是基于**标记清除算法**,所以清理的过程中**会有大量的空间碎片**。 +3. CMS收集器**无法处理浮动垃圾**,**由于CMS并发清理阶段用户线程还在运行**,伴随程序的运行自然会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留在下一次GC时将其清理掉。 +4. G1是一款面向服务端应用的垃圾收集器,**适用于多核处理器、大内存容量的服务端系统**。G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU或核心来缩短STW的停顿时间,它满足短时间停顿的同时达到一个高的吞吐量。 +5. **从JDK 9开始,G1成为默认的垃圾回收器**。当应用有以下任何一种特性时非常适合用G1:Full GC持续时间太长或者太频繁;对象的创建速率和存活率变动很大;应用不希望停顿时间长(长于0.5s甚至1s)。 +6. G1将空间划分成很多块(Region),然后他们各自进行回收。堆比较大的时候可以采用,采用复制算法,碎片化问题不严重。整体上看属于标记整理算法,局部(region之间)属于复制算法。 +7. G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 **G1 中维护记忆集的成本较高**,带来了更高的执行负载,影响效率。**所以 CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB**。 + diff --git "a/Java/classify/jvm/\345\257\271\350\261\241\347\232\204\345\210\233\345\273\272\350\277\207\347\250\213.md" "b/Java/classify/jvm/\345\257\271\350\261\241\347\232\204\345\210\233\345\273\272\350\277\207\347\250\213.md" new file mode 100644 index 00000000..524d01cf --- /dev/null +++ "b/Java/classify/jvm/\345\257\271\350\261\241\347\232\204\345\210\233\345\273\272\350\277\207\347\250\213.md" @@ -0,0 +1,79 @@ +# 对象的创建过程 + +面试官:类加载过程,你之前给我讲过,那么创建对象的过程你知道吗? + +我:我似乎知道。 + +![Java创建对象的过程](https://gitee.com/dreamcater/blog-img/raw/master/uPic/Java创建对象的过程-fC0wRb.png) + +## 类加载检查 + +虚拟机遇到一条 **new** 指令时,首先将去检查这个指令的参数**是否能在常量池中定位到这个类的符号引用**,并且检查这个符号引用代表的**类是否已被加载过**、**解析和初始化过**。如果没有,那必须先执行相应的类加载过程。 + +## 分配内存 + +在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。 + +- **指针碰撞** + - **堆规整(没有内存碎片)** + - 复制算法 + - GC:Serial、ParNew +- **空闲列表** + - **堆内存不规整的情况下** + - 虚拟机会维护一个**列表**,该列表会**记录哪些内存块是可用的**,在分配的时候,找一块儿足够大的内存块来划分给对象实例,最后更新列表激励 + - GC:CMS + +- **并发问题** + - **CAS+失败重试:** CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。**虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。** + - **TLAB:** 为**每一个线程预先在 Eden 区分配一块儿内存**,JVM 在给线程中的对象分配内存时,**首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配** + +## 初始化零值 + +内存分配完成后,虚拟机需要将分配到的内存空间都**初始化为零值(不包括对象头)**,这一步操作保证了对象的实例字段在 Java 代码中**可以不赋初始值就直接使用**,程序能访问到这些字段的数据类型所对应的零值。 + +## 设置对象头 + +初始化零值完成之后,**虚拟机要对对象进行必要的设置**,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 **这些信息存放在对象头中。** 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 + +## 指向init方法 + +在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,**方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行**方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 + +## 内存布局 + +面试官:刚才说了内存布局,给我讲一下 + +我:好的,内存布局分别为**对象头,实例数据,对其填充** + +1. **对象头** + +**Hotspot 虚拟机的对象头包括两部分信息**,**第一部分用于存储对象自身的自身运行时数据**(哈**希码、GC 分代年龄、锁状态标志**等等),**另一部分是类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 + +2. **实例数据** + +**实例数据部分是对象真正存储的有效信息**,也是在程序中所定义的各种类型的字段内容。 + +3. **对齐填充** + +**对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。** 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 + +## 句柄的访问方式 + +面试官:给我讲讲对象的访问方式 + +我:明白,两种:使用句柄和使用直接指针 + +1. **句柄**: + +![使用句柄-f4Cw9x](https://gitee.com/dreamcater/blog-img/raw/master/uPic/使用句柄-f4Cw9x.png) + +如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了**对象实例数据**与**类型数据**各自的具体地址信息; + +2. **直接指针**: + +![直接指针-8m3HHz](https://gitee.com/dreamcater/blog-img/raw/master/uPic/直接指针-8m3HHz.png) + +如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 + +话说:这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是**稳定**的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 **reference 本身不需要修改**。使用直接指针访问方式最大的好处就是**速度快**,它节省了一次指针定位的时间开销。 + diff --git "a/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" "b/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" new file mode 100644 index 00000000..3ebaa2b5 --- /dev/null +++ "b/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" @@ -0,0 +1,81 @@ + +## 类加载器 + +面试官:谈谈类加载器吧 + +我:行,那还不得介绍三个类加载器? + +- BootstrapClassLoader(启动类加载器):最顶层的加载类,由C++实现,负责加载 `%JAVA_HOME%/lib`目录下的jar包和类或者或被 `-Xbootclasspath`参数指定的路径中的所有类。 +- ExtensionClassLoader(扩展类加载器):主要负责加载目录 `%JRE_HOME%/lib/ext` 目录下的jar包和类,或被 `java.ext.dirs` 系统变量所指定的路径下的jar包。 +- AppClassLoader(应用程序类加载器) + +## 双亲委派 + +我可能直接扯双亲委派了 + +每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 **双亲委派模型** 。即在类加载的时候,**系统会首先判断当前类是否被加载过**。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求**委派该父类加载器**的 `loadClass()` 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 `BootstrapClassLoader` 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 `BootstrapClassLoader` 作为父类加载器。 + +![类加载器](https://gitee.com/dreamcater/blog-img/raw/master/uPic/类加载器-Z1WdFt.png) + +可以按图说话 + +我可能主动的扯好处了 + +双亲委派模型保证了**Java程序的稳定运行,可以避免类的重复加载**,也保证了 **Java 的核心 API 不被篡改**。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 `java.lang.Object` 类的话,那么程序运行的时候,系统就会出现多个不同的 `Object` 类。 + +面试官:什么情况下需要开始类加载过程的第一个阶段加载 + +我: + +1. 遇到**new**、**getstatic**、**putstatic**或**invokestatic**这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是: +2. 使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。 +3. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 +4. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 +5. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 + +面试官:如何打破双亲委派模型 + +我:需要重写ClassLoader类的loadClass()方法: + +```java +// 其实重写该方法就行,但是打破可能会报错,系统找不到路径 +// 父类的加载(Object)也会交由我们自自定义的类加载器加载。而很明显在我们自定义的加载目录下是不会有Object.class这个文件的。 +protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + synchronized (getClassLoadingLock(name)) { + // First, check if the class has already been loaded + Class c = findLoadedClass(name); + if (c == null) { + long t0 = System.nanoTime(); + try { + if (parent != null) { + c = parent.loadClass(name, false); // 调用父类的加载器 递归 + } else { + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { + // ClassNotFoundException thrown if class not found + // from the non-null parent class loader + } + + if (c == null) { + // If still not found, then invoke findClass in order + // to find the class. + long t1 = System.nanoTime(); + c = findClass(name); + + // this is the defining class loader; record the stats + sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); + sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); + sun.misc.PerfCounter.getFindClasses().increment(); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } + } +``` + diff --git "a/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" "b/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" new file mode 100644 index 00000000..e5523bff --- /dev/null +++ "b/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" @@ -0,0 +1,38 @@ +# 类加载过程 + +面试官:谈一谈类加载过程 + +我:加载->验证->准备->解析->初始化 + +![类加载过程](https://gitee.com/dreamcater/blog-img/raw/master/uPic/类加载过程-5A2XIO.png) + +## 加载 + +类加载过程的第一步,主要完成下面3件事情: + +- 通过**全类名**获取定义此类的**二进制字节流** +- 将字节流所代表的**静态存储结构**转换为方法区的**运行时数据结构** +- 在内存中生成一个代表该类的 **Class 对象**,作为**方法区这些数据的访问入口** + +## 验证 + +- 文件格式验证:主要验证Class文件**是否规范**等。 +- 元数据验证:对字节码描述的信息**语义分析**等。 +- 字节码验证:确保语义是ok的。 +- 符号引用验证:确保解析动作能执行。 + +## 准备 + +**准备阶段是正式为类变量分配内存并设置类变量初始值的阶段**,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: + +- 这时候进行内存分配的仅包括**类变量**(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。 +- 这里所设置的初始值"通常情况"下是数据类型默认的**零值**(如0、0L、null、false等),比如我们定义了`public static int value=111` ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会复制)。特殊情况:比如给 value 变量加上了 **fianl 关键字**`public static final int value=111` ,那么准备阶段 value 的值就被复制为 111。 + +## 解析 + +解析阶段是虚拟机将常量池内的**符号引用替换为直接引用**的过程,也就是得到**类或者字段、方法在内存中的指针或者偏移量。** + +## 初始化 + +初始化是类加载的最后一步,也是真正执行类中定义的 **Java 程序代码**(字节码),初始化阶段是执行**类构造器** ` ()`方法的过程。 + diff --git "a/Java/classify/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" "b/Java/classify/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" new file mode 100644 index 00000000..0dc445ff --- /dev/null +++ "b/Java/classify/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" @@ -0,0 +1,66 @@ + + +> 这里简要放一些重要的 + +## 类文件结构 + +```java +ClassFile { + u4 magic; //Class 文件的标志 + u2 minor_version;//Class 的小版本号 + u2 major_version;//Class 的大版本号 + u2 constant_pool_count;//常量池的数量 + cp_info constant_pool[constant_pool_count-1];//常量池 + u2 access_flags;//Class 的访问标记 + u2 this_class;//当前类 + u2 super_class;//父类 + u2 interfaces_count;//接口 + u2 interfaces[interfaces_count];//一个类可以实现多个接口 + u2 fields_count;//Class 文件的字段属性 + field_info fields[fields_count];//一个类会可以有个字段 + u2 methods_count;//Class 文件的方法数量 + method_info methods[methods_count];//一个类可以有个多个方法 + u2 attributes_count;//此类的属性表中的属性数 + attribute_info attributes[attributes_count];//属性表集合 +} +``` + +## 静态常量池 + +- 字面量 +- 符号引用 + - 类和接口的全限定名 + - 字段的名称和描述符 + - 方法的名称和描述符 +- 好处:**常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享**。 + +## 运行时常量池 + +当Class文件被加载完成后,java虚拟机会将静态常量池里的内容转移到运行时常量池里,在静态常量池的**符号引用有一部分是会被转变为直接引用**的,比如说类的**静态方法或私有方法,实例构造方法,父类方法**,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的**一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的**。 + +## 字符串常量池 + +**字符串常量池的存在使JVM提高了性能和减少了内存开销**。 + +- 每当我们使用字面量(String s=“1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。 +- 每当我们使用关键字new(String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s。 + +## 版本变化 + +### 1.6 + +- 静态常量池在Class文件中。 +- 运行时常量池在Perm Gen区(也就是方法区)中。 +- 字符串常量池在运行时常量池中。 + +### 1.7 + +- 静态常量池在Class文件中。 +- 运行时常量池依然在Perm Gen区(也就是方法区)中。在JDK7版本中,永久代的转移工作就已经开始了,将譬如符号引用转移到了native heap;字面量转移到了java heap;类的静态变量转移到了java heap。但是运行时常量池依然还存在,只是很多内容被转移,其只存着这些被转移的引用。 +- 字符串常量池被分配到了Java堆的主要部分。也就是字符串常量池从运行时常量池分离出来了。 + +### 1.8 + +- 静态常量池在Class文件中。 +- JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。同时永久代被移除,以元空间代替。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。其主要用于存放一些元数据。 +- 字符串常量池存在于Java堆中。 \ No newline at end of file diff --git "a/Java/classify/jvm/\351\200\203\351\200\270\345\210\206\346\236\220.md" "b/Java/classify/jvm/\351\200\203\351\200\270\345\210\206\346\236\220.md" new file mode 100644 index 00000000..3f313e0d --- /dev/null +++ "b/Java/classify/jvm/\351\200\203\351\200\270\345\210\206\346\236\220.md" @@ -0,0 +1,19 @@ +# 逃逸分析 + +> 这一块知识还是要知道的呀,它是Java虚拟机中比较前沿优化的技术。 + +面试官:你了解逃逸分析吗? + +我:算是了解。逃逸分析的基本行为就是分析**对象动态作用域**:当一个对**象在方法中被定义后,它可能被外部方法引用,例如作为调用参数传递到其他方法中,称为方法逃逸**。甚至还有可能**被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸**。如果能证明**一个对象不会逃逸到方法或线程之外**,也就是**别的方法或线程无法通过任何途径访问到这个对象**,则可能为这个变量进行一些高效的优化: + +1. 栈上分配 + +Java虚拟机中,**如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存**将会是一个很不错的主意,**对象所占用的内存空间就可以随栈帧出栈而销毁**。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那**大量的对象就会随着方法的结束而自动销毁了**,垃圾收集系统的压力将会小很多。 + +2. 同步消除 + +**线程同步本身是一个相对耗时的过程**,**如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问**,那这个变量的**读写肯定就不会有竞争**,对这个变量实施的同步措施也就可以消除。 + +3. 标量替换 + +标量是指一**个数据已经无法再分解成更小的数据来表示了**,Java虚拟机的原始数据类型都不能再进一步分解,它们就可以称为标量。如果逃逸分析证明**一个对象不会被外部访问**,并且**这个对象可以被拆散的话**,那程序真正执行的时候将**可能不创建这个对象**,而改**为直接创建它的若干个被这个方法使用的成员变量来代替**。除了可以让对象的成员变量在栈上(栈上存储的数据,有很大的概率会被虚拟机分配到物理机器高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。 \ No newline at end of file diff --git a/Java/classify/mysql/ACID.md b/Java/classify/mysql/ACID.md new file mode 100644 index 00000000..b94138eb --- /dev/null +++ b/Java/classify/mysql/ACID.md @@ -0,0 +1,75 @@ + +## ACID + +> 这个问题经常问,但是问我的比较少,其实MySQL这一块,问的最多还是优化问题 + +面试官:聊聊ACID是什么 + +我:分别是:**原子性(Atomicity)**、**一致性(Consistency)**、**隔离性(Isolation)**和**持久性(Durability)**。 + +### 原子性 + +根据定义,原子性是指一个事务是一个不可分割的工作单位,**其中的操作要么都做,要么都不做**。即要么转账成功,要么转账失败,是不存在中间的状态!比如:如果不保证原子性,OK,就会出现数据不一致的情形,A账户减去50元,而B账户增加50元操作失败。系统将无故丢失50元~。可能会聊undolog + +### 一致性 + +根据定义,一致性是指事务执行前后,数据处于一种合法的状态,这种状态是语义上的而不是语法上的。 那什么是合法的数据状态呢? oK,这个状态是满足预定的约束就叫做合法的状态,再通俗一点,这状态是由你自己来定义的。**满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!**比如:A给B转100,B不能因为通过这个事务增加了150吧?或者A扣了150吧? + +### 隔离性 + +根据定义,隔离性是指**多个事务并发执行的时候,事务内部的操作与其他事务是隔离的**,并发执行的各个事务之间不能互相干扰。 + +### 持久性 + +根据定义,**持久性是指事务一旦提交,它对数据库的改变就应该是永久性的**。接下来的其他操作或故障不应该对其有任何影响。这里可能会让你聊redolog + +## 并发事务带来的问题 + +面试官:并发事务带来的问题都有哪些 + +我:**脏读、不可重复读和幻读(实际上还有一个丢弃修改)** + +### 脏读 + +第一个事务首先读取变量为50,接着准备更新为100的时,并未提交,第二个事务已经读取为100,此时第一个事务做了回滚。最终第二个事务读取的变量和数据库的不一样。 + +### 丢弃修改 + +T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,**T2 的修改覆盖了 T1 的修改**。例如:事务1读取某表中的数据A=50,事务2也读取A=50,事务1修改A=A+50,事务2也修改A=A+50,最终结果A=100,事务1的修改被丢失。 + +### 不可重复读 + +T2 读取一个数据,T1 对该数据做了修改并提交。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 + +### 幻读 + +T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和第一次读取的结果不同。(和不可重复读的区别:一个是变量变化,一个是范围变化) + +## 数据库的隔离级别 + +面试官:数据库的隔离级别? + +我: + +首先,MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ**(可重读) + +其次,**这里需要注意的是**:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别 下使用的是**Next-Key Lock 锁**算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以 说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要 求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。 + +因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)并不会有任何性能损失**。 + +### 未提交读 + +事务中发生了修改,即使没有提交,其他事务也是可见的,比如对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100.**可能会导致脏读、幻读或不可重复读** + +### 提交读 + +对于一个事务从开始直到提交之前,所做的任何修改是其他事务不可见的,举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取的A是50,刚读取完,A就被修改成100,这个时候另一个事务再进行读取发现A就突然变成100了;**可以阻止脏读,但是幻读或不可重复读仍有可能发生** + +### 可重复读 + +就是对一个记录读取多次的记录是相同的,比如对于一个数A读取的话一直是A,前后两次读取的A是一致的;**可以阻止脏读和不可重复读,但幻读仍有可能发生** + +### 可串行读 + +在并发情况下,和串行化的读取的结果是一致的,没有什么不同,比如不会发生脏读和幻读;**该级别可以防止脏读、不可重复读以及幻读** + diff --git "a/Java/classify/mysql/Innodb\344\270\216MyISAM.md" "b/Java/classify/mysql/Innodb\344\270\216MyISAM.md" new file mode 100644 index 00000000..281419a4 --- /dev/null +++ "b/Java/classify/mysql/Innodb\344\270\216MyISAM.md" @@ -0,0 +1,72 @@ + + +> 这个问题简单回答一下即可 + +## MySQL的引擎 + +面试官:MySQL的引擎都有哪些? + +我:我知道,MySQL内部可以分为服务层和存储引擎层两部分:**服务层包括连接器、查询缓存、分析器、优化器、执行器等;存储引擎层负责数据的存储和提取**。我就说一下自己了解的InnoDB和MyISAM引擎 + +## InnoDB + +- 是 MySQL 默认的**事务型存储引擎**,只有在需要它不支持的特性时,才考虑使用其它存储引擎。 +- 实现了四个标准的隔离级别,默认级别是**可重复读(REPEATABLE READ)**。在可重复读隔离级别下,通过**多版本并发控制**(MVCC)+ (Next-Key Locking)**防止幻影读**。 +- 主索引是**聚簇索引**,在**索引中保存了数据**,从而避免直接读取磁盘,因此对查询性能有很大的提升。 +- 内部做了很多优化,包括从磁盘读取数据时采用的**可预测性读**、能够加快读操作并且自动创建的**自适应哈希索引**、能够加速插入操作的**插入缓冲区**等。 +- 支持真正的**在线热备份**。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 + +## MyISAM + +- 设计简单,数据以**紧密格式存储**。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。 +- 提供了大量的特性,包括**压缩表、空间数据索引**等。 +- **不支持事务**。 +- **不支持行级锁,只能对整张表加锁**,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 + + +## 索引文件 +我一般还会回答一个**索引文件**上的区别 + +### MyISAM + +1. MyISAM**索引文件和数据文件是分离**的,**索引文件仅保存数据记录的地址**,同样使用B+Tree作为索引结构,叶节点的**data域存放的是数据记录的地址** +2. 在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复 +3. MyISAM中索引检索的算法为**首先按照B+Tree搜索算法搜索索引**,**如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录** + +### InnoDB + +1. **InnoDB的数据文件本身就是索引文件**,这棵树的叶节点**data域保存了完整的数据记录**(聚集索引) +2. InnoDB的**辅助索引data域存储相应记录主键的值而不是地址** +3. **聚集索引这种实现方式使得按主键的搜索十分高效**,**但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录**。 + +## 分页查询 + +其实个人还知道一点,分页查询的时候还有一点区别,这点区别也是根据索引文件的区别来的。 + +咱们知道,使用limit分页查询,offset越大,性能越差,比如: + +```sql +-- 以真实的生产环境的6万条数据的一张表为例,比较一下优化前后的查询耗时: +-- 传统limit,文件扫描 +select * from table order by id limit 50000,2; +受影响的行: 0 +时间: 0.171s + +-- 子查询方式,索引扫描 +select * from table +where id >= (select id from table order by id limit 50000 , 1) +limit 2; +受影响的行: 0 +时间: 0.035s + +-- JOIN分页方式 +select * from table as t1 +join (select id from table order by id limit 50000, 1) as t2 +where t1.id <= t2.id order by t1.id limit 2; +受影响的行: 0 +时间: 0.036s +``` + +原因:因为 MySQL 并非是跳过偏移量直接去取后面的数据,而是先把偏移量+要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回的。比如上面的(50000,2),每次取2条,还要经过回表,发现不是想要的,舍弃。那肯定非常耗时间,而通过子查询通过id索引,只查询id,使用到了innodb的索引覆盖, 在内存缓冲区中进行检索,没有回表查询. 然后再用id >= 条件,进一步的缩小查询范围.这样就大大提高了效率。 + +而MyISAM,是直接索引是分离的,通过索引文件查到的数据记录地址,不需要回表,直接对应数据记录,效率也很高。 \ No newline at end of file diff --git "a/Java/classify/mysql/MySQL\346\225\260\346\215\256\345\272\223\347\273\223\346\236\204\344\274\230\345\214\226.md" "b/Java/classify/mysql/MySQL\346\225\260\346\215\256\345\272\223\347\273\223\346\236\204\344\274\230\345\214\226.md" new file mode 100644 index 00000000..5354f2a7 --- /dev/null +++ "b/Java/classify/mysql/MySQL\346\225\260\346\215\256\345\272\223\347\273\223\346\236\204\344\274\230\345\214\226.md" @@ -0,0 +1,94 @@ +[toc] + +> 一个是索引优化,一个是结构优化。。。。 + +## 三范式 + +面试官:先来个**三范式** + +我:好的 + +1. 第一范式:第一范式就是原子性,字段不可再分割; + +- 数据库表中的所有字段都具有单一属性(即字段不能再分割了) +- 单一属性的列是由基本数据类型所构成的 +- 设计出来的表都是简单的二维表 + +2. 第二范式:第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言) +3. 第三范式:所有非主键字段和主键字段之间不能产生传递依赖 + +[例子](https://www.jianshu.com/p/3e97c2a1687b) + +**反范式设计**:反范式化就是为了性能和读取效率的考虑而适当的对数据库设计范式的要求进行违反,**而允许存在少量的数据冗余,也就是使用空间来换取(查询)时间**;比如举个例子:**比如订单表中应该保留当前购买商品的价格、商品的名称(商品的价格是会变动的,这很危险)** + +因此,简单说一下**范式化设计的优缺点**: + +优点: + +- 可以尽量的减少数据冗余 +- 范式化的更新操作比反范式化更快 +- 范式化的表通常比反范式化更小 + +缺点: + +- 对于查询需要关联多个表 +- 更难进行索引优化 + +**反范式化设计的优缺点**: + +优点: + +- 可以减少表的关联 +- 可以更好的进行索引优化 + +缺点: + +- 存在数据冗余和数据维护异常 +- 对数据修改需要更多的成本 + +**如何选择varchar和char类型**: + +- varchar用于存储变长字符串,只占用的必要的存储空间。 +- char类型是定长的,char类型的最大宽度为255 +- 场景:varchar适用于存储很少被更新的字符串列;char适合存储长度近似的值,适合存储短字符串,适合存储经常更新的字符串 + +## 分库分表 + +1. **垂直分表** + +也就是“大表拆小表”,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到“扩展表“。 一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。 + +2. **垂直分库** + +垂直分库针对的是一个系统中的不同业务进行拆分,比如用户User一个库,商品Producet一个库,订单Order一个库。 切分后,要放在多个服务器上,而不是一个服务器上。为什么? 我们想象一下,一个购物网站对外提供服务,会有用户,商品,订单等的CRUD。没拆分之前, 全部都是落到单一的库上的,这会让数据库的单库处理能力成为瓶颈。按垂直分库后,如果还是放在一个数据库服务器上, 随着用户量增大,这会让单个数据库的处理能力成为瓶颈,还有单个服务器的磁盘空间,内存,tps等非常吃紧。所以我们要拆分到多个服务器上,这样上面的问题都解决了,以后也不会面对单机资源问题。垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈。 + +3. **水平分表** + +针对数据量巨大的单张表(比如订单表),按照某种规则(RANGE,HASH取模等),切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。不建议采用。 + +切分规则: + +- RANGE:从0到10000一个表,10001到20000一个表; +- HASH取模:一个商场系统,一般都是将用户,订单作为主表,然后将和它们相关的作为附表,这样不会造成跨库事务之类的问题。 取用户id,然后hash取模,分配到不同的数据库上。 +- 地理区域:比如按照华东,华南,华北这样来区分业务,七牛云应该就是如此。 +- 时间:按照时间切分,就是将6个月前,甚至一年前的数据切出去放到另外的一张表,因为随着时间流逝,这些表的数据 被查询的概率变小,所以没必要和“热数据”放在一起,这个也是“冷热数据分离”。 + +4. **水平分库分表** + +将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。 + +**分库分表带来的问题:** + +1. **事务支持** + +分库分表后,就成了分布式事务了。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价; 如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。 + +2. **join** + +TODO 分库分表后表之间的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表, 结果原本一次查询能够完成的业务,可能需要多次查询才能完成。 粗略的解决方法: 全局表:基础数据,所有库都拷贝一份。 字段冗余:这样有些字段就不用join去查询了。 系统层组装:分别查询出所有,然后组装起来,较复杂。 + +**分库分表中间件:** + +Mycat 和 ShardingSphere(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar 3 款产品)。 + +[对比](https://my.oschina.net/u/4318872/blog/4281049) \ No newline at end of file diff --git "a/Java/classify/mysql/MySQL\346\227\245\345\277\227\346\226\207\344\273\266.md" "b/Java/classify/mysql/MySQL\346\227\245\345\277\227\346\226\207\344\273\266.md" new file mode 100644 index 00000000..26ccd5ed --- /dev/null +++ "b/Java/classify/mysql/MySQL\346\227\245\345\277\227\346\226\207\344\273\266.md" @@ -0,0 +1,34 @@ +# MySQL的日志文件 + +面试官:分别讲一下MySQL的几大文件,你懂的 + +我:我不懂,ok,好的。 + +- undoLog 也就是我们常说的**回滚日志文件** 主要用于事务中执行失败,进行回滚,以及MVCC中对于数据历史版本的查看。由**引擎层的InnoDB引擎实现**,是**逻辑日志**,记录数据修改被修改前的值,比如"把id='B' 修改为id = 'B2' ,那么undo日志就会用来存放id ='B'的记录”。当一条数据需要更新前,会先把修改前的记录存储在undolog中,如果这个修改出现异常,则会使用undo日志来实现回滚操作,保证事务的一致性。当事务提交之后,**undo log并不能立马被删除,而是会被放到待清理链表中,待判断没有事务用到该版本的信息时才可以清理相应undolog**。它保存了事务发生之前的数据的一个版本,用于回滚,**同时可以提供多版本并发控制下的读(MVCC)**,也即非锁定读。 +- redoLog 是重做日志文件是**记录数据修改之后的值**,**用于持久化到磁盘中**。redo log包括两部分:**一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的**;二是**磁盘上的重做日志文件(redo log file)**,该部分日志是持久的。由引**擎层的InnoDB引擎实现**,是**物理日志**,记录的是物理数据页修改的信息,比如“某个数据页上内容发生了哪些改动”。当一条数据需要更新时,InnoDB会先将数据更新,然后记录redoLog 在内存中,然后找个时间将redoLog的操作执行到磁盘上的文件上。不管是否提交成功我都记录,你要是回滚了,那我连回滚的修改也记录。它确保了事务的持久性。[参考](https://juejin.im/post/6844903573910716430) +- binlog由**Mysql的Server层实现**,是**逻辑日志**,记录的是sql语句的原始逻辑,比如"把id='B' 修改为id = ‘B2’。binlog会写入指定大小的物理文件中,是追加写入的,当前文件写满则会创建新的文件写入。 产生:**事务提交的时候,一次性将事务中的sql语句,按照一定的格式记录到binlog中。用于复制和恢复在主从复制中,从库利用主库上的binlog进行重播(执行日志中记录的修改逻辑),实现主从同步。业务数据不一致或者错了,用binlog恢复**。 +- MVCC多版本并发控制是MySQL中基于**乐观锁理论实现隔离级别**的方式,用于**读已提交和可重复读**取隔离级别的实现。在MySQL中,会在表中每一条数据后面添加两个字段:**最近修改该行数据的事务ID**,**指向该行(undolog表中)回滚段的指针**。**Read View判断行的可见性,创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表**。 + +**binlog和redolog的区别**: + +1. redolog是在**InnoDB存储引擎层产生**,而**binlog是MySQL数据库的上层服务层产生**的。 +2. 两种日志记录的内容形式不同。MySQL的**binlog是逻辑日志**,其记录是**对应的SQL语句**。而**innodb存储引擎层面的重做日志是物理日志**。 +3. 两种日志与记录写入磁盘的时间点不同,**binlog日志只在事务提交完成后进行一次写入**。而**innodb存储引擎的重做日志在事务进行中不断地被写入,并日志不是随事务提交的顺序进行写入的**。 +4. **binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redolog是循环使用**。 +5. **binlog可以作为恢复数据使用,主从复制搭建**,**redolog作为异常宕机或者介质故障后的数据恢复使用**。 + +**MVCC的缺点:** + +MVCC在大多数情况下代替了行锁,实现了对读的非阻塞,读不加锁,读写不冲突。缺点是每行记录**都需要额外的存储空间,需要做更多的行维护和检查工作**。 要知道的,MVCC机制下,会在更新前建立undo log,根据各种策略读取时非阻塞就是MVCC,undo log中的行就是MVCC中的多版本。 而undo log这个关键的东西,**记载的内容是串行化的结果,记录了多个事务的过程,不属于多版本共存**。 这么一看,似乎mysql的mvcc也并没有所谓的多版本共存 + +**读写分离原理**: + +主库(master)将变更写**binlog**日志,然后从库(slave)连接到主库之后,从库有一个**IO线程**,将主库的binlog日志**拷贝到自己本地**,写入一个中继日志中。接着从库中有一个SQL线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再次执行一遍SQL,这样就可以保证自己跟主库的数据是一样的。 + +这里有一个非常重要的一点,就是从库同步主库数据的过程是**串行化**的,也就是说**主库上并行**的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行SQL的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。 + +而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。 + +所以mysql实际上在这一块有两个机制,一个是**半同步复制**,用来解决主库数据丢失问题;一个是**并行复制**,用来解决主从同步延时问题。 + +所谓并行复制,指的是从库**开启多个线程,并行读取relay log中不同库的日志**,然后并行重放不同库的日志,这是库级别的并行。 \ No newline at end of file diff --git "a/Java/classify/mysql/MySQL\346\230\257\345\246\202\344\275\225\346\211\247\350\241\214\344\270\200\346\235\241SQL\347\232\204.md" "b/Java/classify/mysql/MySQL\346\230\257\345\246\202\344\275\225\346\211\247\350\241\214\344\270\200\346\235\241SQL\347\232\204.md" new file mode 100644 index 00000000..3021ee41 --- /dev/null +++ "b/Java/classify/mysql/MySQL\346\230\257\345\246\202\344\275\225\346\211\247\350\241\214\344\270\200\346\235\241SQL\347\232\204.md" @@ -0,0 +1,20 @@ + +## SQL执行顺序 +SQL的执行顺序:from---where--group by---having---select---order by + +## MySQL是如何执行一条SQL的 +![SQL执行的全部过程-MlS1d5](https://gitee.com/dreamcater/blog-img/raw/master/uPic/SQL执行的全部过程-MlS1d5.png) + +**MySQL内部可以分为服务层和存储引擎层两部分:** + +1. **服务层包括连接器、查询缓存、分析器、优化器、执行器等**,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。 +2. **存储引擎层负责数据的存储和提取**,其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认的存储引擎。 + +**Server层按顺序执行sql的步骤为**: +客户端请求: +- **连接器**(验证用户身份,给予权限) +- **查询缓存**(存在缓存则直接返回,不存在则执行后续操作) +- **分析器**(对SQL进行词法分析和语法分析操作) +- **优化器**(主要对执行的sql优化选择最优的执行方案方法) +- **执行器**(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口) +- **去引擎层获取数据返回**(如果开启查询缓存则会缓存查询结果) \ No newline at end of file diff --git "a/Java/classify/mysql/MySQL\347\232\204\351\224\201.md" "b/Java/classify/mysql/MySQL\347\232\204\351\224\201.md" new file mode 100644 index 00000000..406db529 --- /dev/null +++ "b/Java/classify/mysql/MySQL\347\232\204\351\224\201.md" @@ -0,0 +1,69 @@ +# MySQL的锁 + +> MySQL的锁,其实跟Java差不了,一个思想。 + +面试官:MySQL的锁,介绍一下 + +我: + +MyISAM:MyISAM只有表锁,其中又分为共享读锁和独占写锁。 + +- MyISAM表的读操作,不会阻塞其他用户对同一个表的读请求,但会阻塞对同一个表的写请求。 +- MyISAM表的写操作,会阻塞其他用户对同一个表的读和写操作。 +- MyISAM表的读、写操作之间、以及写操作之间是串行的。 + +Innodb行锁:共享锁,排他锁 + +- 对于UPDATE、DELETE、INSERT语句,Innodb会自动给涉及的数据集加排他锁(X);对于普通SELECT语句,Innodb不会加任何锁。 + +```sql +//显示共享锁(S) : + SELECT * FROM table_name WHERE .... LOCK IN SHARE MODE + //显示排他锁(X): + SELECT * FROM table_name WHERE .... FOR UPDATE. +``` + +- 记录锁(Record Locks):记录锁是封锁记录,记录锁也叫行锁,注意:行锁是针对索引的,如果表中没有索引,那么就会锁整张表 +- 间隙锁(GAP)对于键值在条件范围内但并不存在的记录,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。 +- 临键锁(Next-Key Lock):(Record Locks+GAP),锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。 + +面试官:给你张表怎么用cas实现高并发下的update操作 + +我: + +第一种: + +```xml +// cas, 期望值和数据表中的旧值一致,才更新。 +# newStock = oldStock-desStock; + + UPDATE t_order SET stock=#{newStock} WHERE id=#{orderId} AND stock=#{oldStock} + +``` + +```java +// orderId:订单id +// getStock:库存:旧值 +// desStock:可以是期望值,但这里预减值 +int result = orderManager.desStockByCas(orderId, orderDo.getStock(), desStock); +``` + +第二种: + +```xml +// 用版本号 +// 我期望的版本号, 和旧版本号一致才更新,并且版本号累加... + + UPDATE t_order SET stock=stock-#{desStock}, version=version+1 + WHERE id=#{orderId} AND version=#{oldVersion} + +``` + +如 + +```java +// orderId:订单id +// getVersion:获取数据库版本号,旧版本 +// desStock:可以是期望值,但这里预减值 +int result = orderManager.desStockByOptimistic(orderId, orderDo.getVersion(), desStock); +``` \ No newline at end of file diff --git "a/Java/classify/mysql/\347\264\242\345\274\225.md" "b/Java/classify/mysql/\347\264\242\345\274\225.md" new file mode 100644 index 00000000..7f730414 --- /dev/null +++ "b/Java/classify/mysql/\347\264\242\345\274\225.md" @@ -0,0 +1,167 @@ + + +> 这一块,最好能知道怎么优化 + +面试官:索引介绍一下 + +## 索引类型 + +我:ok,先说一下**索引类型**: + +- FULLTEXT:即为全文索引,目前只有MyISAM引擎支持。其可以在CREATE TABLE ,ALTER TABLE ,CREATE INDEX 使用,不过目前只有 CHAR、VARCHAR ,TEXT 列上可以创建全文索引。 +- HASH:由于HASH的唯一及类似键值对的形式,很适合作为索引。 HASH索引可以一次定位,不需要像树形索引那样逐层查找,因此具有极高的效率。但是,这种高效是有条件的,即只在“=”和“in”条件下高效,对于范围查询、排序及组合索引仍然效率不高。 +- BTREE:BTREE索引就是一种将索引值按一定的算法,存入一个树形的数据结构中(二叉树),每次查询都是从树的入口root开始,依次遍历node,获取leaf。这是MySQL里默认和最常用的索引类型。 +- RTREE:RTREE在MySQL很少使用,仅支持geometry数据类型,支持该类型的存储引擎只有MyISAM、BDb、InnoDb、NDb、Archive几种。 相对于BTREE,RTREE的优势在于范围查找。 + +## 索引种类 + +再说一下**索引种类**: + +- 普通索引:仅加速查询 +- 唯一索引:加速查询 + 列值唯一(可以有null) +- 主键索引:加速查询 + 列值唯一(不可以有null)+ 表中只有一个 +- 组合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并 +- 全文索引:对文本的内容进行分词,进行搜索 +- 索引合并:使用多个单列索引组合搜索 +- 覆盖索引:select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖 +- 聚簇索引:表数据是和主键一起存储的,主键索引的叶结点存储行数据(包含了主键值),二级索引的叶结点存储行的主键值。使用的是B+树作为索引的存储结构,非叶子节点都是索引关键字,但非叶子节点中的关键字中不存储对应记录的具体内容或内容地址。叶子节点上的数据是主键与具体记录(数据内容) + +## 索引结构 + +其次说**索引结构**: + +**MyISAM**: + +1. MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址,同样使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址 +2. 在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复 +3. MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录 + +**InnoDB**: + +1. InnoDB的数据文件本身就是索引文件,这棵树的叶节点data域保存了完整的数据记录(聚集索引) +2. InnoDB的辅助索引data域存储相应记录主键的值而不是地址 +3. 聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。 + +补充一下**为什么InnoDB索引是B+**: + +- Hash索引:Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。所以,哈希索引只适用于等值查询的场景。而B+ 树是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描 +- 二叉查找树:解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表。 +- 平衡二叉树:通过旋转解决了平衡的问题,但是旋转操作效率太低。 +- 红黑树:通过舍弃严格的平衡和引入红黑节点,解决了 AVL旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO次数太多。 +- B+树:在B树的基础上,**B+树相对于B树能够有更多的分支,使得这棵树更加矮胖,查询时做的IO操作次数也更少**;此外将叶节点使用指针连接成链表,范围查询更加高效。B+树的**非叶子节点不保存数据**,只保存**子树的临界值**(最大或者最小),所以同样大小的节点。 +- [B+树的高度](https://www.jianshu.com/p/544e97672deb) + +补充B树: + +B树(英语: B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让**查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成**。B树,概括来说是一个**一般化的二叉查找树**(binary search tree),可以拥有最多2个子节点。与自平衡二叉查找树不同,B树适用于读写相对大的数据块的存储系统,例如磁盘。 + +- 关键字集合分布在整颗树中; +- 任何一个关键字出现且只出现在一个结点中; +- 搜索有可能在非叶子结点结束; +- 其搜索性能等价于在关键字全集内做一次二分查找; + +## 索引失效 + +查询什么时候不走**索引**: + +1. **模糊查询 %like** +2. **索引列参与计算,使用了函数** +3. **非最左前缀顺序** +4. **where对null判断** +5. **where不等于** +6. or操作有至少一个字段没有索引 +7. 需要回表的查询结果集过大(超过配置的范围) +8. **将打算加索引的列设置为NOT NULL,否则将导致引擎放弃使用索引而进行全表扫描** + +## 索引最左 + +**索引最左原则:** + +**举例子**: +如果索引列分别为A,B,C,顺序也是A,B,C: + +- 那么查询的时候,如果查询【A】【A,B】 【A,B,C】,那么可以通过索引查询 +- 如果查询的时候,采用【A,C】,那么C这个虽然是索引,但是由于中间缺失了B,因此C这个索引是用不到的,只能用到A索引 +- 如果查询的时候,采用【B】 【B,C】 【C】,由于没有用到第一列索引,不是最左前缀,那么后面的索引也是用不到了 +- 如果查询的时候,采用范围查询,并且是最左前缀,也就是第一列索引,那么可以用到索引,但是范围后面的列无法用到索引(比如,a>= 3 and b = 4 and c = 5; A走索引,bc不走)(比如,a = 3 and b >= 4 and c = 5; a和b走,c不走) + +**组合索引的底层其实按照第一个索引排序,从排序里面查第二个索引,以此类推。如果第一个索引失效,或者没有经过第一个索引,后面没发在前面的基础上查询。** + +## 为什么使用索引 + +- 通过创建唯一性索引,可以保证数据库表中每一行数据的**唯一性**。 +- 可以大大加快数据的**检索速度**,这也是创建索引的最主要的原因。 +- 帮助服务器**避免排序和临时表**。 +- 将**随机IO变为顺序IO**。 +- 可以**加速表和表之间的连接**,特别是在实现数据的参考完整性方面特别有意义。 + +但是使用索引要看一条准则--- 那就是读写比例,我们知道索引的缺点: + +- 当对表中的数据进行增加、删除和修改的时候,**索引也要动态的维护**,这样就降低了数据的维护速度。 +- 索引需要**占物理空间**,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立簇索引,那么需要的空间就会更大。 +- **创建索引和维护索引要耗费时间**,这种时间随着数据量的增加而增加 + +**你想,如果某个场景,发送10条请求,9条写,1条读。 加索引岂不是在浪费效率和空间?** + +## explain + +面试官:聊聊**explain** + +我:好的,不过这一块内容好多,我只说几个关键的吧 + +1. id : 表示SQL执行的顺序的标识,SQL从大到小的执行 +2. select_type:表示查询中每个select子句的类型 +3. table:显示这一行的数据是关于哪张表的,有时不是真实的表名字 +4. type:表示MySQL在表中找到所需行的方式,又称“访问类型”。常用的类型有: ALL, index, range, ref, eq_ref, const, system, NULL(从左到右,性能从差到好) +5. possible_keys:指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 +6. Key:key列显示MySQL实际决定使用的键(索引),如果没有选择索引,键是NULL。 +7. key_len:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的) +8. ref:表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 +9. rows: 表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数,理论上行数越少,查询性能越好 +10. Extra:该列包含MySQL解决查询的详细信息 + +## 慢查询优化 + +面试官:慢查询优化 + +我:我试试 + +打开慢查询日志 + +1. 先运行看看是否真的很慢,注意设置SQL_NO_CACHE +2. where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高 +3. explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询) +4. order by limit 形式的sql语句让排序的表优先查(这里要注意如果数据量大,要注意了) +5. 了解业务方使用场景 +6. 加索引时参照建索引的几大原则 +7. 观察结果,不符合预期继续从0分析 + + + +咱们知道,使用limit分页查询,offset越大,性能越差,比如: + +```sql +-- 以真实的生产环境的6万条数据的一张表为例,比较一下优化前后的查询耗时: +-- 传统limit,文件扫描 +select * from table order by id limit 50000,2; +受影响的行: 0 +时间: 0.171s + +-- 子查询方式,索引扫描 +select * from table +where id >= (select id from table order by id limit 50000 , 1) +limit 2; +受影响的行: 0 +时间: 0.035s + +-- JOIN分页方式 +select * from table as t1 +join (select id from table order by id limit 50000, 1) as t2 +where t1.id <= t2.id order by t1.id limit 2; +受影响的行: 0 +时间: 0.036s +``` + +原因:因为 MySQL 并非是跳过偏移量直接去取后面的数据,而是先把偏移量+要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回的。比如上面的(50000,2),每次取2条,还要经过回表,发现不是想要的,舍弃。那肯定非常耗时间,而通过子查询通过id索引,只查询id,使用到了innodb的索引覆盖, 在内存缓冲区中进行检索,没有回表查询. 然后再用id >= 条件,进一步的缩小查询范围.这样就大大提高了效率。 + +而MyISAM,是直接索引是分离的,通过索引文件查到的数据记录地址,不需要回表,直接对应数据记录,效率也很高。 \ No newline at end of file diff --git a/Java/classify/net/DNS.md b/Java/classify/net/DNS.md new file mode 100644 index 00000000..6ced16f3 --- /dev/null +++ b/Java/classify/net/DNS.md @@ -0,0 +1,42 @@ + +## DNS + +### DNS是什么 + +**官方解释**:DNS(Domain Name System,域名系统),因特网上作为**域名和IP地址相互映射**的一个**分布式数据库**,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)。 + +**通俗的讲**,我们更习惯于记住一个网站的名字,比如www.baidu.com,而不是记住它的ip地址,比如:167.23.10.2。 + +### 谈谈DNS解析过程 +![DNS解析过程-eiVd6a](https://gitee.com/dreamcater/blog-img/raw/master/uPic/DNS解析过程-eiVd6a.png) + +- 请求一旦发起,若是chrome浏览器,先在浏览器找之前**有没有缓存过的域名所对应的ip地址**,有的话,直接跳过dns解析了,若是没有,就会**找硬盘的hosts文件**,看看有没有,有的话,直接找到hosts文件里面的ip + +[字节问了修改hosts,浏览器会变吗?](https://blog.csdn.net/woshizhangliang999/article/details/51457864) + +- 如果本地的hosts文件没有能的到对应的ip地址,浏览器会发出一个**dns请求到本地dns服务器**,**本地dns服务器一般都是你的网络接入服务器商提供**,比如中国电信,中国移动等。 +- 查询你输入的网址的DNS请求到达本地DNS服务器之后,**本地DNS服务器会首先查询它的缓存记录**,如果缓存中有此条记录,就可以直接返回结果,此过程是**递归的方式进行查询**。如果没有,本地DNS服务器还要向**DNS根服务器**进行查询。 +- 本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。 +- 最后,本地DNS服务器向**域名的解析服务器**发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。 + +### DNS查询方式 + +#### 递归解析 + +当局部DNS服务器自己不能回答客户机的DNS查询时,它就需要向其他DNS服务器进行查询。此时有两种方式。**局部DNS服务器自己负责向其他DNS服务器进行查询,一般是先向该域名的根域服务器查询,再由根域名服务器一级级向下查询**。最后得到的查询结果返回给局部DNS服务器,再由局部DNS服务器返回给客户端。 + +#### 迭代解析 + +当局部DNS服务器自己不能回答客户机的DNS查询时,也可以通过迭代查询的方式进行解析。局部DNS服务器不是自己向其他DNS服务器进行查询,**而是把能解析该域名的其他DNS服务器的IP地址返回给客户端DNS程序**,客户端DNS程序再继续向这些DNS服务器进行查询,直到得到查询结果为止。也就是说,迭代解析只是帮你找到相关的服务器而已,而不会帮你去查。比如说:baidu.com的服务器ip地址在192.168.4.5这里,你自己去查吧,本人比较忙,只能帮你到这里了。 + +### DNS负载均衡 + +当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。处理办法就是用DNS负载均衡技术,它的原理是在**DNS服务器中为同一个主机名配置多个IP地址,在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器**,从而达到负载均衡的目的。例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等。 + +### 为什么域名解析用UDP协议? + +因为UDP快啊!UDP的DNS协议只要一个请求、一个应答就好了。而使用基于TCP的DNS协议要三次握手、发送数据以及应答、四次挥手。但是UDP协议传输内容不能超过512字节。不过客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。 + +### 为什么区域传送用TCP协议? + +因为TCP协议可靠性好啊!你要从主DNS上复制内容啊,你用不可靠的UDP? 因为TCP协议传输的内容大啊,你用最大只能传512字节的UDP协议?万一同步的数据大于512字节,你怎么办? \ No newline at end of file diff --git "a/Java/classify/net/HTTP\345\222\214HTTPS.md" "b/Java/classify/net/HTTP\345\222\214HTTPS.md" new file mode 100644 index 00000000..9e0aa524 --- /dev/null +++ "b/Java/classify/net/HTTP\345\222\214HTTPS.md" @@ -0,0 +1,212 @@ + +## HTTP + +### GET和POST的区别? + +1. GET使用URL或Cookie传参,而POST将数据放在BODY中 +2. GET方式提交的数据有长度限制,则POST的数据则可以非常大 +3. POST比GET安全,因为数据在地址栏上不可见,没毛病 +4. **本质区别**:GET请求是幂等性的,POST请求不是。 + +> 这里的幂等性:幂等性是指一次和多次请求某一个资源应该具有同样的副作用。简单来说意味着对同一URL的多个请求应该返回同样的结果。 + +正因为它们有这样的区别,所以不应该且**不能用get请求做数据的增删改这些有副作用的操作**。因为get请求是幂等的,**在网络不好的隧道中会尝试重试**。如果用get请求增数据,会有**重复操作**的风险,而这种重复操作可能会导致副作用(浏览器和操作系统并不知道你会用get请求去做增操作)。 + +### 响应码 + +#### 1xx 信息 + +**100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 + +#### 2xx 成功 + +- **200 OK** +- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 +- **206 Partial Content** :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 + +#### 3xx 重定向 + +- **301 Moved Permanently** :永久性重定向 +- **302 Found** :临时性重定向 +- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。 +- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 +- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 + +#### 4xx 客户端错误 + +- **400 Bad Request** :请求报文中存在语法错误。 +- **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 +- **403 Forbidden** :请求被拒绝。没有权限 +- **404 Not Found**:路由不存在,或者没找到 + +#### 5xx 服务器错误 + +- **500 Internal Server Error** :服务器正在执行请求时发生错误。 +- **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 + + +### HTTP首部 + +> 这块有点多,可参考[http首部](https://github.com/DreamCats/JavaBooks/blob/master/Interview/network/计算机网络原理-http那些事儿.md#http首部) + +### Cookies + +HTTP 协议是**无状态**的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 + +Cookie 是**服务器发送到用户浏览器并保存在本地的一小块数据**,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 + +用途 + +- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) +- 个性化设置(如用户自定义设置、主题等) +- 浏览器行为跟踪(如跟踪分析用户行为等) + + +### Session + +除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 + +Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 + +### Cookie和Session的选择 + +- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session; +- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; +- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 + +### JWT + +JWT(json web token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。 + +cookie+session这种模式通常是保存在**内存**中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。而JWT不是这样的,**只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可**。 + +**JWT的构成**: + +第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。详情请见[官网](https://jwt.io/introduction/) + +**JWT总结**: + +1. 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。 +2. payload部分,JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。 +3. 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。它不需要在服务端保存会话信息, 所以它易于应用的扩展。 + +[jwt优缺点](https://snailclimb.gitee.io/javaguide/#/docs/system-design/authority-certification/JWT-advantages-and-disadvantages) + +### 浏览器在与服务器建立了一个 TCP 连接后是否会在一个 HTTP 请求完成后断开?什么情况下会断开? + +在 HTTP/1.0 中,一个服务器在发送完一个 HTTP 响应后,会断开 TCP 链接。但是这样每次请求都会重新建立和断开 TCP 连接,代价过大。所以虽然标准中没有设定,**某些服务器对 Connection: keep-alive 的 Header 进行了支持**。 + +**持久连接**:既然维持 TCP 连接好处这么多,HTTP/1.1 就把 Connection 头写进标准,并且默认开启持久连接,除非请求中写明 Connection: close,那么浏览器和服务器之间是会维持一段时间的 TCP 连接,不会一个请求结束就断掉。 + + +### 一个TCP连接可以对应几个HTTP请求? + +如果维持连接,一个 TCP 连接是可以发送多个 HTTP 请求的。 + +### 一个 TCP 连接中 HTTP 请求发送可以一起发送么(比如一起发三个请求,再三个响应一起接收)? + +HTTP/1.1 存在一个问题,单个 TCP 连接在同一时刻只能处理一个请求,意思是说:两个请求的生命周期不能重叠,任意两个 HTTP 请求从开始到结束的时间在同一个 TCP 连接里不能重叠。 + +在 HTTP/1.1 存在 Pipelining 技术可以完成这个多个请求同时发送,但是由于浏览器默认关闭,所以可以认为这是不可行的。在 HTTP2 中由于 Multiplexing 特点的存在,多个 HTTP 请求可以在同一个 TCP 连接中并行进行。 + +那么在 HTTP/1.1 时代,浏览器是如何提高页面加载效率的呢?主要有下面两点: + +- 维持和服务器已经建立的 TCP 连接,在同一连接上顺序处理多个请求。 +- 和服务器建立多个 TCP 连接。 + +### 为什么有的时候刷新页面不需要重新建立 SSL 连接? + +TCP 连接有的时候会被浏览器和服务端维持一段时间。TCP 不需要重新建立,SSL 自然也会用之前的。 + +### 浏览器对同一 Host 建立 TCP 连接到数量有没有限制? + +**有。Chrome 最多允许对同一个 Host 建立六个 TCP 连接。不同的浏览器有一些区别。** + + +### 在浏览器中输入url地址后显示主页的过程? + +> - 根据域名,进行DNS域名解析; +> - 拿到解析的IP地址,建立TCP连接; +> - 向IP地址,发送HTTP请求; +> - 服务器处理请求; +> - 返回响应结果; +> - 关闭TCP连接; +> - 浏览器解析HTML; +> - 浏览器布局渲染; + + +### HTTP1.x的缺点 +1. HTTP/1.0 一次只允许在一个TCP连接上发起一个请求,HTTP/1.1使用的流水线技术也只能部分处理请求分析,仍然会存在队列头阻塞问题,因此客户端在需要发起多次请求时,通常会采用建立多连接来减少延迟 +2. 单向请求,只能由客户端发起 +3. 请求报文与响应报文首部信息冗余量大。 +4. 数据未压缩,导致数据的传输量大。 + +### HTTP2.0有哪些改动 +1. 多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。 +2. 二进制分帧:应用层(HTTP/2)和传输层(TCP or UDP)之间增加一个二进制分帧层。 +3. 首部压缩(Header Compression) +4. 服务端推送(Server Push) + + +## HTTPS + +### HTTPS是什么 + +HTTPS 并不是新协议,而是让 **HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信**。通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 + +### HTTP的缺点 + +- 使用明文进行通信,内容可能会被窃听; +- 不验证通信方的身份,通信方的身份有可能遭遇伪装; +- 无法证明报文的完整性,报文有可能遭篡改。 + +### 对称密钥加密 + +对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。 + +- 优点:运算速度快 +- 缺点:无法安全地将密钥传输给通信方 + +### 非对称密钥加密 + +非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。 + +公开密钥所有人都可以获得,**通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密**,**接收方收到通信内容后使用私有密钥解密**。 + +非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。 + +- 优点:可以更安全地将公开密钥传输给通信发送方; +- 缺点:运算速度慢。 + +### HTTPS采用的加密方式 + +HTTPS 采用混合的加密机制,使用**非对称密钥加密用于传输对称密钥来保证传输过程的安全性**,之后使用**对称密钥加密进行通信来保证通信过程的效率**。DES+RSA + +![rsa原理-rWg4oK](https://gitee.com/dreamcater/blog-img/raw/master/uPic/rsa原理-rWg4oK.png) + +确保传输安全过程(其实就是rsa原理): + +1. Client给出**协议版本号**、一个客户端生成的**随机数**(Client random),以及客户端支持的**加密方法**。 +2. Server确认双方使用的**加密方法**,并给出**数字证书**、以及一个服务器生成的**随机数**(Server random)。 +3. Client确认**数字证书有效**,然后生成一个新的**随机数**(Premaster secret),并使用**数字证书中的公钥,加密这个随机数**,发给Server。 +4. Server使用自己的**私钥,获取Client发来的随机数**(Premaster secret)。 +5. Client和Server根据约定的加密方法,使用前面的**三个随机数,生成”对话密钥”**(session key),用来加密接下来的整个对话过程。 + +### 认证 + +通过使用 **证书** 来对通信方进行认证。 + +数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 + +服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 + +进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。 + +[加密全套流程](https://www.cnblogs.com/handsomeBoys/p/6556336.html) + +[https://www.cnblogs.com/xdyixia/p/11610102.html](https://www.cnblogs.com/xdyixia/p/11610102.html) + +### HTTPS的缺点 + +- 因为需要进行加密解密等过程,因此速度会更慢; +- 需要支付证书授权的高额费用。 \ No newline at end of file diff --git "a/Java/classify/net/TCP\345\222\214UDP.md" "b/Java/classify/net/TCP\345\222\214UDP.md" new file mode 100644 index 00000000..56eb065d --- /dev/null +++ "b/Java/classify/net/TCP\345\222\214UDP.md" @@ -0,0 +1,152 @@ + +## TCP/UDP + +### TCP + +#### TCP是什么? + +`TCP(Transmission Control Protocol 传输控制协议)`是一种面向连接的、可靠的、基于字节流的传输层通信协议。 + +#### TCP头部报文 + +##### source port 和 destination port + +> 两者分别为「源端口号」和「目的端口号」。源端口号就是指本地端口,目的端口就是远程端口。 + +可以这么理解,我们有很多软件,每个软件都对应一个端口,假如,你想和我数据交互,咱们得互相知道你我的端口号。 + +再来一个很官方的: + +> 扩展:应用程序的端口号和应用程序所在主机的 IP 地址统称为 socket(套接字),IP:端口号, 在互联网上 socket 唯一标识每一个应用程序,源端口+源IP+目的端口+目的IP称为”套接字对“,一对套接字就是一个连接,一个客户端与服务器之间的连接。 + +##### Sequence Number + +> 称为「序列号」。用于 TCP 通信过程中某一传输方向上字节流的每个字节的编号,为了确保数据通信的有序性,避免网络中乱序的问题。接收端根据这个编号进行确认,保证分割的数据段在原始数据包的位置。初始序列号由自己定,而后绪的序列号由对端的 ACK 决定:SN_x = ACK_y (x 的序列号 = y 发给 x 的 ACK)。 + +说白了,类似于身份证一样,而且还得发送此时此刻的所在的位置,就相当于身份证上的地址一样。 + +##### Acknowledge Number + +> 称为「确认序列号」。确认序列号是接收确认端所期望收到的下一序列号。确认序号应当是上次已成功收到数据字节序号加1,只有当标志位中的 ACK 标志为 1 时该确认序列号的字段才有效。主要用来解决不丢包的问题。 + +##### TCP Flag + +`TCP` 首部中有 6 个标志比特,它们中的多个可同时被设置为 `1`,主要是用于操控 `TCP` 的状态机的,依次为`URG,ACK,PSH,RST,SYN,FIN`。 + +当然只介绍三个: + +1. **ACK**:这个标识可以理解为发送端发送数据到接收端,发送的时候 ACK 为 0,标识接收端还未应答,一旦接收端接收数据之后,就将 ACK 置为 1,发送端接收到之后,就知道了接收端已经接收了数据。 +2. **SYN**:表示「同步序列号」,是 TCP 握手的发送的第一个数据包。用来建立 TCP 的连接。SYN 标志位和 ACK 标志位搭配使用,当连接请求的时候,SYN=1,ACK=0连接被响应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送一个只有 SYN 的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口。 +3. **FIN**:表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志位的 TCP 数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。发送端只剩最后的一段数据了,同时要告诉接收端后边没有数据可以接受了,所以用FIN标识一下,接收端看到这个FIN之后,哦!这是接受的最后的数据,接受完就关闭了;**TCP四次分手必然问**。 + +##### Window size + +> 称为滑动窗口大小。所说的滑动窗口,用来进行流量控制。 + +### TCP三次握手 + +![TCP三次握手-2ujCx2](https://gitee.com/dreamcater/blog-img/raw/master/uPic/TCP三次握手-2ujCx2.svg) + +- **初始状态**:客户端处于 `closed(关闭)`状态,服务器处于 `listen(监听)` 状态。 +- **第一次握手**:客户端发送请求报文将 `SYN = 1`同步序列号和初始化序列号`seq = x`发送给服务端,发送完之后客户端处于`SYN_Send`状态。 +- **第二次握手**:服务端收到 `SYN` 请求报文之后,如果同意连接,会以自己的同步序列号`SYN(服务端) = 1`、初始化序列号 `seq = y`和确认序列号(期望下次收到的数据包)`ack = x + 1` 以及确认号`ACK = 1`报文作为应答,服务器为`SYN_Receive`状态。(问题来了,两次握手之后,所以老哥,你需要给我三次握手来传个话告诉我一声。你要是不告诉我,万一我认为你跑了,然后我可能出于安全性的考虑继续给你发一次,看看你回不回我。) +- **第三次握手**: 客户端接收到服务端的 `SYN + ACK`之后,知道可以下次可以发送了下一序列的数据包了,然后发送同步序列号 `ack = y + 1`和数据包的序列号 `seq = x + 1`以及确认号`ACK = 1`确认包作为应答,客户端转为`established`状态。(分别站在双方的角度上思考,各自ok) + +1. 你吃饭了嘛?(seq=x),收到请回答(SYN=1) +2. 收到(ACK=1),吃饭了(ack=x+1),你吃饭了吗?(seq=y),收到请回答(SYN=1) +3. 收到(ACK=1),吃饭了(ack=y+1),那么我们聊一下接下里的事情(established) + +### TCP四次分手 + +![TCP四次分手-Nt8NUx](https://gitee.com/dreamcater/blog-img/raw/master/uPic/TCP四次分手-Nt8NUx.png) + +- **初始化状态**:客户端和服务端都在连接状态,接下来开始进行四次分手断开连接操作。 +- **第一次分手**:第一次分手无论是客户端还是服务端都可以发起,因为 TCP 是全双工的。 + +> 假如客户端发送的数据已经发送完毕,发送FIN = 1 **告诉服务端,客户端所有数据已经全发完了**,**服务端你可以关闭接收了**,但是如果你们服务端有数据要发给客户端,客户端照样可以接收的。此时客户端处于FIN = 1等待服务端确认释放连接状态。 + +- **第二次分手**:服务端接收到客户端的释放请求连接之后,**知道客户端没有数据要发给自己了**,**然后服务端发送ACK = 1告诉客户端收到你发给我的信息**,此时服务端处于 CLOSE_WAIT 等待关闭状态。(服务端先回应给客户端一声,我知道了,但服务端的发送数据能力即将等待关闭,于是接下来第三次就来了。) +- **第三次分手**:此时服务端向客户端把所有的数据发送完了,然后发送一个FIN = 1,**用于告诉客户端,服务端的所有数据发送完毕**,**客户端你也可以关闭接收数据连接了**。此时服务端状态处于LAST_ACK状态,来等待确认客户端是否收到了自己的请求。(服务端等客户端回复是否收到呢,不收到的话,服务端不知道客户端是不是挂掉了还是咋回事呢) +- **第四次分手**:此时如果客户端收到了服务端发送完的信息之后,就发送ACK = 1,告诉服务端,客户端已经收到了你的信息。**有一个 2 MSL 的延迟等待**。 + +#### 为什么要有2MSL等待延迟? + +对应这样一种情况,最后客户端发送的ACK = 1给服务端的**过程中丢失**了,服务端没收到,服务端怎么认为的?我已经发送完数据了,怎么客户端没回应我?是不是中途丢失了?然后服务端再次发起断开连接的请求,一个来回就是2MSL。 + +客户端给服务端发送的ACK = 1丢失,**服务端等待 1MSL没收到**,**然后重新发送消息需要1MSL**。如果再次接收到服务端的消息,则**重启2MSL计时器**,**发送确认请求**。客户端只需等待2MSL,如果没有再次收到服务端的消息,就说明服务端已经接收到自己确认消息;此时双方都关闭的连接,TCP 四次分手完毕 + +#### 为什么四次分手? + +任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。 + +### TCP粘包 + +**TCP粘包**是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。 + + +个人觉得:应用层的报文在以流的形式传输时,每一个报文的报头紧接着上一个报文的报文尾部,这就是所谓的“粘包”问题。 + +- 由TCP**连接复用**造成的粘包问题。 +- 因为TCP默认会使用**Nagle算法**,此算法会导致粘包问题。 + - 只有上一个分组得到确认,才会发送下一个分组; + - 收集多个小分组,在一个确认到来时一起发送。 +- **数据包过大**造成的粘包问题。 +- 流量控制,**拥塞控制**也可能导致粘包。 +- **接收方不及时接收缓冲区的包,造成多个包接收** + +**解决**: + +1. **Nagle算法**问题导致的,需要结合应用场景适当关闭该算法 +2. 尾部标记序列。通过特殊标识符表示数据包的边界,例如\n\r,\t,或者一些隐藏字符。 +3. 头部标记分步接收。在TCP报文的头部加上表示数据长度。 +4. 应用层发送数据时**定长**发送。 + +[https://blog.csdn.net/xp178171640/article/details/104746379/](https://blog.csdn.net/xp178171640/article/details/104746379/) + +[https://blog.csdn.net/songchuwang1868/article/details/87707127](https://blog.csdn.net/songchuwang1868/article/details/87707127) + +### TCP 协议如何保证可靠传输? + +- **确认和重传**:接收方收到报文就会确认,发送方发送一段时间后没有收到确认就会重传。 +- **数据校验**:TCP报文头有校验和,用于校验报文是否损坏。 +- **数据合理分片和排序**:tcp会按最大传输单元(MTU)合理分片,接收方会缓存未按序到达的数据,重新排序后交给应用层。而UDP:IP数据报大于1500字节,大于MTU。这个时候发送方的IP层就需要分片,把数据报分成若干片,是的每一片都小于MTU。而接收方IP层则需要进行数据报的重组。由于UDP的特性,某一片数据丢失时,接收方便无法重组数据报,导致丢弃整个UDP数据报。 +- **流量控制**:当接收方来不及处理发送方的数据,能通过滑动窗口,提示发送方降低发送的速率,防止包丢失。 +- **拥塞控制**:当网络拥塞时,通过拥塞窗口,减少数据的发送,防止包丢失。 + +### TCP 利用滑动窗口实现流量控制的机制? + +> 流量控制是为了控制发送方发送速率,保证接收方来得及接收。TCP 利用滑动窗口实现流量控制。 + +TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着**接收方还有多大的缓冲区可以用于接收数据**。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据。 + +> 例如,允许用户终止在远端机上的运行进程。另一种情况是发送方可以发送一个 1 字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。 + +### TCP拥塞控制的机制以及算法? + +> 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。 + +TCP 发送方要维持一个 **拥塞窗口(cwnd) 的状态变量**。拥塞控制窗口的大小**取决于网络的拥塞程度**,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。TCP的拥塞控制采用了四种算法,即 **慢开始** 、 **拥塞避免** 、**快重传** 和 **快恢复**。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。 + +### TCP的长连接和短连接 +[https://www.cnblogs.com/chinaops/p/9303041.html](https://www.cnblogs.com/chinaops/p/9303041.html) + +### UDP + +提供**无连接**的,尽最大努力的数据传输服务(**不保证数据传输的可靠性**)。 + +#### UDP的特点 + +- UDP是**无连接的**; +- UDP使用**尽最大努力交付**,即不保证可靠交付,因此主机不需要维持复杂的链接状态(这里面有许多参数); +- UDP是**面向报文**的; +- UDP**没有拥塞控制**,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等); +- UDP**支持一对一、一对多、多对一和多对多**的交互通信; +- UDP的**首部开销小**,只有8个字节,比TCP的20个字节的首部要短。 + +那么,再说一次TCP的特点: + +- **TCP是面向连接的**。(就好像打电话一样,通话前需要先拨号建立连接,通话结束后要挂机释放连接); +- 每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的(**一对一**); +- TCP**提供可靠交付的服务**。通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达; +- TCP**提供全双工通信**。TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双方通信的数据; +- **面向字节流**。TCP中的“流”(stream)指的是流入进程或从进程流出的字节序列。“面向字节流”的含义是:虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。 \ No newline at end of file diff --git "a/Java/classify/net/\347\275\221\347\273\234\346\250\241\345\236\213.md" "b/Java/classify/net/\347\275\221\347\273\234\346\250\241\345\236\213.md" new file mode 100644 index 00000000..da907eb8 --- /dev/null +++ "b/Java/classify/net/\347\275\221\347\273\234\346\250\241\345\236\213.md" @@ -0,0 +1,49 @@ + +## 网络模型 + +![分层模型-EiHhGW](https://gitee.com/dreamcater/blog-img/raw/master/uPic/分层模型-EiHhGW.png) + +### 简要概括 + +- 物理层:底层数据传输,如网线;网卡标准。 + +- 数据链路层:定义数据的基本格式,如何传输,如何标识;如网卡MAC地址。 + +- 网络层:定义IP编址,定义路由功能;如不同设备的数据转发。 + +- 传输层:端到端传输数据的基本功能;如 TCP、UDP。 + +- 会话层:控制应用程序之间会话能力;如不同软件数据分发给不同软件。 + +- 标识层:数据格式标识,基本压缩加密功能。 + +- 应用层:各种应用软件,包括 Web 应用。 + +### 流程 + +比如,计算机 A 和 计算机 B 要进行信息交互,比如 A 上开发了一个网页,需要 B 去访问。B 发出一个请求给 A,那么请求数据从 B 的 **应用层开始向下传到表示层、再从表示层传到会话层直到物理层,通过物理层传递到 A,A 的物理层接到请求后将请求向上传递到自己的应用层,应用层再将要请求的数据向自己的物理层方向传递然后 B 接到数据传递数据到自己的应用层**。 + +说明: + +- 在四层,既传输层数据被称作**段**(Segments); +- 三层网络层数据被称做**包**(Packages); +- 二层数据链路层时数据被称为**帧**(Frames); +- 一层物理层时数据被称为**比特流**(Bits)。 + +### 常见的端口号和协议号 + +![常见端口号-PemUq1](https://gitee.com/dreamcater/blog-img/raw/master/uPic/常见端口号-PemUq1.png) + +### 总结 + +- 网络七层模型是一个标准,而非实现。 +- 网络四层模型是一个实现的应用模型。 +- 网络四层模型由七层模型简化合并而来。 + +### ping命令基于哪一层协议的原理是什么? + +ping命令基于网络层的命令,是基于ICMP协议工作的。 + +### ARP +ARP是一种解决地址问题的协议。以目标IP地址为线索,用来定位下一个应该接收数据分包的网络设备对应的MAC地址。 +起初要通过广播发送一个ARP请求包,这个包里存放了其MAC地址的主机IP地址,由于广播的包可以被同一个链路上所有的主机或路由器接收,因此ARP的请求包也就会被这同一个链路上所有的主机和路由器进行解析。如果ARP请求包中的目标IP地址与自己的IP地址的一致,那么这个节点就将自己的MAC地址塞入ARP响应包返回给主机A。 \ No newline at end of file diff --git "a/Java/classify/redis/Redis\345\210\206\345\270\203\345\274\217\351\224\201.md" "b/Java/classify/redis/Redis\345\210\206\345\270\203\345\274\217\351\224\201.md" new file mode 100644 index 00000000..5a4b7400 --- /dev/null +++ "b/Java/classify/redis/Redis\345\210\206\345\270\203\345\274\217\351\224\201.md" @@ -0,0 +1,32 @@ +# 分布式锁 + +> 毕竟判断和绑定座位(或者下单)非原子性,为了降低锁的粒度,可以将判断和绑定座位锁在一个事务里。集群:Redisson + +- Key为xx_座位号,过期时间为随机1-5s(用setex的命令,该命令是key和过期时间是原子性的) +- 每次先Redis中判断该key存在不存在,如果存在,要么阻塞,要么就返回给用户,座位已被选择。 +- 如果不存在,先上锁,然后再判断和绑定座位(或者下单)。其实这里有个隐藏的问题。如果绑定座位非常耗时,超过了过期时间1-5s,就凉凉了。其实这里设置过期时间,就是防止一直因为某种原因阻塞而不释放锁 +- 前三步,少了个签证value,如果不设置,那么当锁过期了,业务逻辑才走完,准备删除的时候,B客户端获取到了该锁,但是A把B的key锁删除了,然而B还不知道。 +- 因此,要解决这个问题,可以设置value签证,结束的时候判断一次,该value是不是自己的value,这样就不会误删。 + +## RedLock算法流程 + +首先有这样的问题: + +1. 客户端 A 从 Master 上获取锁。 +2. 在锁未被复制到某 Slave 节点的时候,Master 节点 Down 掉了。 +3. 某 Slave 节点成为新的 Master。 +4. 客户端 B 可从新 Master 上获取锁。 + +假设有5个实例 + +1. 比如过期时间为TTL:10min +2. 记录当前时间:比如T1 = 12:00 +3. 客户端分别向5个实例获取锁,比如申请锁的时间依次为:12:01...12:05,最后获取实例的锁为T2:12:05(获取锁的超时时间要远远小于过期时间,防止死等。) +4. 如果获取锁的实例大于3个(过半机制),那么就相当于获取到锁了,该锁的真正的有效时间为TTL-(T2-T1) = 5min +5. 如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁 + +[https://juejin.im/post/5cc165816fb9a03202221dd5](https://juejin.im/post/5cc165816fb9a03202221dd5) + +## zk实现分布式锁 + +[补充-Zookeeper锁的实现](https://juejin.im/post/5c01532ef265da61362232ed) \ No newline at end of file diff --git "a/Java/classify/redis/Redis\346\214\201\344\271\205\345\214\226.md" "b/Java/classify/redis/Redis\346\214\201\344\271\205\345\214\226.md" new file mode 100644 index 00000000..44ce106a --- /dev/null +++ "b/Java/classify/redis/Redis\346\214\201\344\271\205\345\214\226.md" @@ -0,0 +1,46 @@ + +面试官:Redis的持久化了解嘛? + +我:了解,Redis的持久化分为两种:**RDB和AOF** + +## RDB + +**RDB**是一种**快照存储持久化**方式,具体就是将`Redis`某一时刻的内存数据保存到硬盘的文件当中,默认保存的文件名为`dump.rdb`。在`Redis`服务器启动时,会重新加载`dump.rdb`文件的数据到内存当中恢复数据。 + +优点: + +1. RDB会生成多个数据文件,**每个数据文件都代表了某一个时刻中redis的数据**,这种多个数据文件的方式,非常适合**做冷备**。 +2. RDB对redis对外提供读写服务的时候,影像非常小,因为redis 主进程只需要fork一个子进程出来,让子进程对磁盘io来进行rdb持久化 +3. **RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快**。 + +缺点: + +1. **如果redis要故障时要尽可能少的丢失数据,RDB没有AOF好**,例如1:00进行的快照,在1:10又要进行快照的时候宕机了,这个时候就会丢失10分钟的数据。 +2. RDB每次fork出子进程来执行RDB快照生成文件时,如果文件特别大,可能会导致客户端提供服务暂停数毫秒或者几秒 + +![rdb-uc03rm](https://gitee.com/dreamcater/blog-img/raw/master/uPic/rdb-uc03rm.png) + +## AOF + +**AOF**:把所有的**对Redis的服务器进行修改的命令都存到一个文件里,命令的集合**。 使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中。aof的默认策略是每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说,AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略,AOF的速度可能会慢于RDB。 Redis默认是快照RDB的持久化方式。对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。 + +优点: + +1. **AOF可以更好的保护数据不丢失,一般AOF会以每隔1秒**,通过后台的一个线程去执行一次fsync操作,如果redis进程挂掉,最多丢失1秒的数据。 +2. **AOF以appen-only的模式写入,所以没有任何磁盘寻址的开销,写入性能非常高**。 +3. AOF日志文件的命令通过非常可读的方式进行记录,这个非常适合做灾难性的误删除紧急恢复,如果某人不小心用flushall命令清空了所有数据,只要这个时候还没有执行rewrite,那么就可以将日志文件中的flushall删除,进行恢复。 + +缺点: + +1. 对于同一份文件**AOF文件比RDB数据快照要大**。 +2. AOF开启后支持写的QPS会比RDB支持的写的QPS低,因为AOF一般会配置成每秒fsync操作,每秒的fsync操作还是很高的 +3. **数据恢复比较慢,不适合做冷备**。 + +![aof-ElLyYr](https://gitee.com/dreamcater/blog-img/raw/master/uPic/aof-ElLyYr.png) + +**如何选择:** + +如何选择: + +综合AOF和RDB两种持久化方式,**用AOF来保证数据不丢失,作为恢复数据的第一选择**;**用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,可以使用RDB进行快速的数据恢复**。 + diff --git "a/Java/classify/redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/Java/classify/redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204.md" new file mode 100644 index 00000000..79d4ea81 --- /dev/null +++ "b/Java/classify/redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -0,0 +1,51 @@ + +## String + +String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用; **常规计数:微博数,粉丝数**等。 + +## Hash + +Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来**存储用户信息,商品信息**等等。 + +简单说一下结构 + +- 字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。 +- Redis中的字典使用哈希表作为底层结构实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。 +- Redis使用MurmurHash2算法来计算键的哈希值。 +- 哈希表使用链地址法来解决键冲突。 + +注意:这里和Java的HashMap不同的rehash过程 + +1. Redis的rehash过程是扩展和收缩,而且还是渐进式的rehash +2. Redis的字典有两个哈希表ht[0]和ht[1] +3. 为字典的ht[1]哈希表分配空间,如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used *2的2^n;如果执行的是收缩操作,那么ht[1]的大小第一个大于等于ht[0].used的2^n。(举个例子,ht[0]的长度为10,那么扩展就是2^5的32,如果是压缩的话2^4=16) +4. 如果ht[0]的键值非常多的话,一次性转移过去,是一个非常耗时的操作哦,因此并非一次性,采取渐进式rehash转移。 + +## List + +list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如**微博的关注列表,粉丝列表, 消息列表**等功能都可以用Redis的 list 结构来实现。 + +Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。 + +另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。 + +## Set + +set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。 + +当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在 一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。 + +比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常 方便的实现如**共同关注、共同粉丝、共同喜好**等功能。这个过程也就是求交集的过程,具体命令如下:`sinterstore key1 key2 key3`将交集存在key1内 + + +## Zset + +和set相比,sorted set增加了一个**权重参数score**,使得集合中的元素能够按score进行**有序**排列。 + +举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种**礼物排行榜**,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。 + +跳跃表,暂时先放一个链接[https://zhuanlan.zhihu.com/p/53975333](https://zhuanlan.zhihu.com/p/53975333) + +- 简单来说跳跃表是一种有序数据结构,它通过在**每个节点中维持多个指向其他节点的指针**,从而达到快速访问节点的目的。 +- 跳跃表平均O(longN),最坏O(N)复杂度的节点查找 +- 跳跃表有个层的概念:层带有两个属性:**前进指针和跨度**,前进指针用于**访问位于表尾方向的其他节点**,而跨度则记录了**前进指针所指向节点和当前节点的距离**。一般情况下,层越多,查找效率越高。 \ No newline at end of file diff --git "a/Java/classify/redis/Redis\346\250\241\345\236\213.md" "b/Java/classify/redis/Redis\346\250\241\345\236\213.md" new file mode 100644 index 00000000..ee0616c1 --- /dev/null +++ "b/Java/classify/redis/Redis\346\250\241\345\236\213.md" @@ -0,0 +1,75 @@ + +### Redis模型 + +面试官:Redis为什么快? + +我:内心:不知道为什么一直问这个问题。 + +1. 纯内存操作 +2. 单线程操作,避免了**频繁的上下文切换** +3. 合理高效的数据结构 +4. 采用了**非阻塞I/O多路复用**机制 + +实际上Redis服务器是一个事件驱动程序,分为**文件事件**和**时间事件**,就主要讲一下文件事件。 + +Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器 + +- 文件事件处理器使用**I/O多路复用程序来同时监听多个套接字**,并根据套接字目前执行的任务来为套接字**关联不同的事件处理器** +- 当被监听的套接字准备好执行连接**应答、读取、写入、关闭**等操作时,与操作相对于的文件事件就会产生,这时**文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件**。 +- 简单点:**就是一堆套接字请求,被一个叫做I/O多路复用程序监听,通过文件事件分派器一个一个和事件处理器绑定在一起去处理**。 + +I/O多路复用程序是有常见的select、epoll等系统调用所实现的。有个小故事,自行理解BIO、NIO、select、poll、epoll等 + +故事情节为:**老李去买火车票,三天后买到一张退票。参演人员(老李,黄牛,售票员,快递员),往返车站耗费1小时。** + +**往返车站可以看成系统调用,调用一次一小时** + +### 1. 阻塞I/O模型 + +老李去火车站买票,排队三天买到一张退票。 + +耗费:在车站吃喝拉撒睡 3天,其他事一件没干。 + +### 2. 非阻塞I/O模型 + +老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。 + +耗费:往返车站6次,路上6小时,其他时间做了好多事。 + +2比1多了个自己轮训调用 + +### 3. I/O复用模型 + +1. select/poll + +老李去火车站买票,委托黄牛,然后每隔6小时电话**黄牛**询问,黄牛三天内买到票,然后老李去火车站交钱领票。 + +耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次 + +实际上,就是自己不断调select(像个船一样,装了很多描述符)询问哪些描述符可读可写,比如又一个可读了,咱就调用可读系统调用就ok了 + +2. epoll + +老李去火车站买票,委托黄牛,**黄牛买到后即通知老李去领**,然后老李去火车站交钱领票。 +耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话 + +实际上,自己不用管了,当有可读的时候,直接中断你,然后你自己去读 + +### 4. 信号驱动I/O模型 + +老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。 + +耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话 + +不要黄牛了,省了这个单线程,系统通知你,你收到以后自己去读 + +### 5. 异步I/O模型 + +老李去火车站,告诉售票员要买票,售票员买到票之后,打电话通知老李把票放在某某储物箱,老李根据储物箱地址自己去取票。 + +耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话 + +只需要注册一次,得到消息之后,就去另外一个地址上取走票 + +黄牛是多路复用,他不仅可以帮你买票,还可以其他人买票,还可以买飞机票,高铁票等。 + diff --git "a/Java/classify/redis/\345\206\205\345\255\230\346\267\230\346\261\260\346\234\272\345\210\266.md" "b/Java/classify/redis/\345\206\205\345\255\230\346\267\230\346\261\260\346\234\272\345\210\266.md" new file mode 100644 index 00000000..0be06449 --- /dev/null +++ "b/Java/classify/redis/\345\206\205\345\255\230\346\267\230\346\261\260\346\234\272\345\210\266.md" @@ -0,0 +1,23 @@ +# 内存淘汰机制 + +面试官:先讲一下Redis的过期时间 + +我: + +定期删除+惰性删除 + +- 定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载! +- 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈! + +面试官:如果定期删除漏掉了很多过期 key,然后你也没及时去查, 也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? + +我:**redis 内存淘汰机制。** + +redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。redis 提供 6种数据淘汰策略: + +- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 +- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰 +- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰 +- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰 +- allkeys-random:从数据集中任意选择数据淘汰 +- no-enviction(驱逐):禁止驱逐数据 \ No newline at end of file diff --git "a/Java/classify/redis/\347\274\223\345\255\230\347\251\277\351\200\217\345\222\214\347\274\223\345\255\230\351\233\252\345\264\251.md" "b/Java/classify/redis/\347\274\223\345\255\230\347\251\277\351\200\217\345\222\214\347\274\223\345\255\230\351\233\252\345\264\251.md" new file mode 100644 index 00000000..e3e362d2 --- /dev/null +++ "b/Java/classify/redis/\347\274\223\345\255\230\347\251\277\351\200\217\345\222\214\347\274\223\345\255\230\351\233\252\345\264\251.md" @@ -0,0 +1,21 @@ +> 感觉这个被问烂了 + +面试官:聊聊什么是缓存穿透和雪崩 + +我:ok + +**缓存穿透**: + +一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。 + +1. 在接口做校验 +2. 存null值(缓存击穿加锁) +3. 布隆过滤器拦截: 将所有可能的查询key先映射到布隆过滤器中,查询时先判断key是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回。布隆过滤器将值进行多次哈希bit存储,布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在。 + +**缓存雪崩:** + +缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。 + +1. 使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉 +2. 缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效 +3. 限流降级策略:有一定的备案,比如个性推荐服务不可用了,换成热点数据推荐服务 \ No newline at end of file diff --git a/Java/classify/spring/Bean.md b/Java/classify/spring/Bean.md new file mode 100644 index 00000000..08cb05c7 --- /dev/null +++ b/Java/classify/spring/Bean.md @@ -0,0 +1,49 @@ + +## bean的作用域 +- singleton 作用域:表示在 Spring 容器中只有一个 Bean 实例,以单例的形式存在,是默认的 Bean 作用域。 +- prototype 作用域:原型作用域,每次调用 Bean 时都会创建一个新实例,也就是说每次调用 getBean() 方法时,相当于执行了 new Bean()。 +- request 作用域:每次 Http 请求时都会创建一个新的 Bean,该作用域仅适应于 WebApplicationContext 环境。 +- session 作用域:同一个 Http Session 共享一个 Bean 对象,不同的 Session 拥有不同的 Bean 对象,仅适用于 WebApplicationContext 环境。 +- application 作用域:全局的 Web 作用域,类似于 Servlet 中的 Application。 + +## Spring的单例有线程安全问题吗 +大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 + +常见的有两种解决办法: + +- 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 +- 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。 + +## 什么是三级缓存 +1. 第一级缓存:单例缓存池singletonObjects。 +2. 第二级缓存:早期提前暴露的对象缓存earlySingletonObjects。(属性还没有值对象也没有被初始化) +3. 第三级缓存:singletonFactories单例对象工厂缓存。 + + +## 创建Bean的整个过程 +1. 首先finishBeanFactoryInitialization->preInstantiateSingletons->getBean->doGetBean; +2. 在doGetBean中,transformedBeanName:主要负责判断一下有木有别名;getSingleton:从一级缓存singletonObjects拿bean,在getSingleton方法中,有一个判断条件就是isSingletonCurrentlyInCreation,判断为false,因为他是第一次进来,并且还没有正在创建该bean;dependsOn:依赖,暂时先不说他。 +3. 再来一次getSingleton:再一次的从singketonObjects缓存拿,依然没有的。接着有个重点beforeSingletonCreation:它把bean添加到临时的singletonsCurrentlyInCreation,这就意味着,下次再碰见它,那可就是true了。接着singletonFactory.getObject(),这里getObject调用的是传递的接口createBean方法。 +4. 在createBean方法中:有个doCreateBean->createBeanInstance方法:它就是直接实例化,实际上构造器有反应了(区分JVM创建对象和Spring创建对象),但是没有赋值(初始化);earlySingletonExposure:提前暴漏该bean。但要知道三个变量,为什么他是true:isSingleton(),是否单例,那肯定是哦;(这里解释了这里是单例才能提前曝漏,意味着才能存三级缓存)allowCircularReferences,默认变量为true,写死了;isSingletonCurrentlyInCreation,这里可就为true了,因为步骤3,已经将它设置为true了。那么会进来这个方法:addSingletonFactory +5. addSingletonFactory在这个方法中:将该bean放入到三级缓存singletonFactories中。(解决循环依赖) +6. 接下来,就是它了,populateBean:实际上就是属性赋值。(如果这里要有A依赖B,又发现三级缓存中没有B,那么它就会再次执行一次(递归开始)getBean->doGetBean->createBeanInstance(把B给实例化一下),同样的道理,这里会将B也会放入三级缓存中,B开始populateBean,那么它发现B依赖A,此时三级缓存中有A(精髓,牛逼),然后把A放到二级缓存中,同时从三级缓存中移除,接着得到A之后直接赋值,最后完成了初始化,然后来到addSingleton,将B仍到了一级缓存,同时将B从三级缓存仍出去)返回B,递归结束,得到B之后将B的赋值给A了。 +7. 最后将二级缓存的A删除,仍到一级缓存中。 + + + + +## bean的生命周期 +![bean的生命周期-GbJvfY](https://gitee.com/dreamcater/blog-img/raw/master/uPic/bean的生命周期-GbJvfY.png) + +- Bean 容器找到配置文件中 Spring Bean 的定义。 +- Bean 容器利用 Java Reflection API 创建一个Bean的实例。 +- 如果涉及到一些属性值 利用 `set()`方法设置一些属性值。 +- 如果 Bean 实现了 `BeanNameAware` 接口,调用 `setBeanName()`方法,传入Bean的名字。 +- 如果 Bean 实现了 `BeanClassLoaderAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoader`对象的实例。 +- 与上面的类似,如果实现了其他 `*.Aware`接口,就调用相应的方法。 +- 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessBeforeInitialization()` 方法 +- 如果Bean实现了`InitializingBean`接口,执行`afterPropertiesSet()`方法。 +- 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。 +- 如果有和加载这个 Bean的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessAfterInitialization()` 方法 +- 当要销毁 Bean 的时候,如果 Bean 实现了 `DisposableBean` 接口,执行 `destroy()` 方法。 +- 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。 \ No newline at end of file diff --git "a/Java/classify/spring/Ioc\345\222\214AOP.md" "b/Java/classify/spring/Ioc\345\222\214AOP.md" new file mode 100644 index 00000000..23d016f9 --- /dev/null +++ "b/Java/classify/spring/Ioc\345\222\214AOP.md" @@ -0,0 +1,59 @@ + +## IoC +IoC(Inverse of Control:控制反转)是一种**设计思想**,就是 **将原本在程序中手动创建对象的控制权,交由Spring框架来管理。IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。** + +将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 **IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。** + +### 初始化流程 + +- Resource资源定位:这个Resouce指的是BeanDefinition的资源定位。这个过程就是容器找数据的过程,就像水桶装水需要先找到水一样。 +- BeanDefinition的载入和解析:这个载入过程是把用户定义好的Bean表示成Ioc容器内部的数据结构,而这个容器内部的数据结构就是BeanDefition。 +- BeanDefinition注册 +- prepareRefresh():预备一下, 标记启动时间,上下文环境,我要的材料(beanDefinition)准备好了嘛? +- obtainFreshBeanFactory(): + - 如果已经有了BeanFactory就销毁它里面的单例Bean并关闭这个BeanFactory。 + - 创建一个新的BeanFactory。 + - 对这个BeanFactory进行定制(customize),如allowBeanDefinitionOverriding等参数 + - 转载BeanDefinitions(读取配置文件,将xml转换成对应得BeanDefinition) + - 检查是否同时启动了两个BeanFactory。 +- prepareBeanFactory(beanFactory):设置beanFactory的类加载器,材料(BeanDefinition)解析器等 +- postProcessBeanFactory(beanFactory): + - 设置beanFactory的后置处理器 + - 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类或做点什么事 +- invokeBeanFactoryPostProcessors(beanFactory): + - 调用beanFactory的后置处理器(BeanDefinitionRegisterPostProcessor和BeanFactoryPostProcessor) + - 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法 +- registerBeanPostProcessors(beanFactory): + - 注册 BeanPostProcessor 的实现类(bean的后置处理器) + - 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization 两个方法分别在 Bean 初始化之前和初始化之后得到执行。注意,到这里 Bean 还没初始化 +- initMessageSource():对上下文中的消息源进行初始化 +- initApplicationEventMulticaster():初始化上下文的事件广播器 +- onRefresh():- 模版方法,具体的子类可以在这里初始化一些特殊的 Bean(在初始化 singleton beans 之前) +- registerListeners():注册事件监听器 +- finishBeanFactoryInitialization(beanFactory):初始化所有的 singleton beans +- finishRefresh():最后,广播事件,ApplicationContext 初始化完成 + +## AOP + +AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,**却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来**,便于**减少系统的重复代码**,**降低模块间的耦合度**,并**有利于未来的可拓展性和可维护性**。 + +- **Spring AOP就是基于动态代理的** +- 如果要代理的对象,实现了某个接口,那么Spring AOP会使用**JDK Proxy**, +- 而对于没有实现接口的对象,这时候Spring AOP会使用 **Cglib** 生成一个被代理对象的子类来作为代理。 + + +![](https://user-gold-cdn.xitu.io/2018/9/14/165d631e56799a5c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +### 初始化流程 +registerAspectJAnnotationAutoProxyCreatorIfNecessary + +- 第一句,注册一个AnnotationAwareAspectJAutoProxyCreator(称它为自动代理器),这个Creator是AOP的操作核心,也是扫描Bean,代理Bean的操作所在。 +- 第二句,解析配置元素,决定代理的模式。其中有JDK动态代理,还有CGLIB代理,这部分后续会再细讲。 +- 第三句,作为系统组件,把Creator这个Bean,放到Spring容器中。让Spring实例化,启动这个Creator。 + +总结: +- Spring加载自动代理器AnnotationAwareAspectJAutoProxyCreator,当作一个系统组件。 +- 当一个bean加载到Spring中时,会触发自动代理器中的bean后置处理,然后会先扫描bean中所有的Advisor +- 然后用这些Adviosr和其他参数构建ProxyFactory +- ProxyFactory会根据配置和目标对象的类型寻找代理的方式(JDK动态代理或CGLIG代理) +- 然后代理出来的对象放回context中,完成Spring AOP代理 diff --git a/Java/classify/spring/SpringBoot.md b/Java/classify/spring/SpringBoot.md new file mode 100644 index 00000000..dd49e06e --- /dev/null +++ b/Java/classify/spring/SpringBoot.md @@ -0,0 +1,49 @@ + + +## Springboot自动装配原理 + +SpringBootApplication的注解 +[https://www.jianshu.com/p/943650ab7dfd](https://www.jianshu.com/p/943650ab7dfd) + +- @SpringBootConfiguration:允许在上下文中注册额外的bean或导入其他配置类 +- @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制 +- @ComponentScan: 扫描常用的注解 + +其中 @EnableAutoConfiguration 是实现自动配置的入口,该注解又通过 @Import 注解导入了AutoConfigurationImportSelector,在该类中加载 META-INF/spring.factories 的配置信息。然后筛选出以 EnableAutoConfiguration 为 key 的数据,加载到 IOC 容器中,实现自动配置功能! + + +## @Resource和@Autowired区别 + +### 共同点 +两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。 + +```java +public class TestServiceImpl { + // 下面两种@Autowired只要使用一种即可 + @Autowired + private UserDao userDao; // 用于字段上 + + @Autowired + public void setUserDao(UserDao userDao) { // 用于属性的方法上 + this.userDao = userDao; + } +} +``` + +### 不同点 + +1. @Autowired + +@Autowired注解是**按照类型**(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用**按照名称**(byName)来装配,可以结合**@Qualifier注解**一起使用。(通过类型匹配找到多个candidate,在没有@Qualifier、@Primary注解的情况下,会使用对象名作为最后的fallback匹配)如下: + +```java +public class TestServiceImpl { + @Autowired + @Qualifier("userDao") + private UserDao userDao; +} +``` + +2. @Resource + +@Resource默认按照ByName自动注入。@Resource有两个重要的属性:**name和type**,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。**所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略** \ No newline at end of file diff --git a/Java/classify/spring/SpringMVC.md b/Java/classify/spring/SpringMVC.md new file mode 100644 index 00000000..8886d18f --- /dev/null +++ b/Java/classify/spring/SpringMVC.md @@ -0,0 +1,13 @@ + +# SpringMVC + +![springmvc工作原理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/49790288.jpg) + +1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 +2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 +3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 +4. `HandlerAdapter` 会根据 `Handler`来调用真正的处理器开处理请求,并处理相应的业务逻辑。 +5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 +6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 +7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 +8. 把 `View` 返回给请求者(浏览器) \ No newline at end of file diff --git "a/Java/classify/spring/Spring\344\272\213\345\212\241.md" "b/Java/classify/spring/Spring\344\272\213\345\212\241.md" new file mode 100644 index 00000000..72925428 --- /dev/null +++ "b/Java/classify/spring/Spring\344\272\213\345\212\241.md" @@ -0,0 +1,48 @@ + +## Spring事务 +### 隔离级别 +- **TransactionDefinition.ISOLATION_DEFAULT:** 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别. +- **TransactionDefinition.ISOLATION_READ_UNCOMMITTED:** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读** +- **TransactionDefinition.ISOLATION_READ_COMMITTED:** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生** +- **TransactionDefinition.ISOLATION_REPEATABLE_READ:** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生。** +- **TransactionDefinition.ISOLATION_SERIALIZABLE:** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 + +### @Transactional(rollbackFor = Exception.class)注解了解吗 +1. @Transactional注解只能应用到public修饰符上,其它修饰符不起作用,但不报错。 +2. 默认情况下此注解会对unchecked异常进行回滚,对checked异常不回滚。 + +> checked异常:表示无效,不是程序中可以预测的。比如无效的用户输入,文件不存在,网络或者数据库链接错误。这些都是外在的原因,都不是程序内部可以控制的。unchecked异常:表示错误,程序的逻辑错误。是RuntimeException的子类,比如IllegalArgumentException, NullPointerException和IllegalStateException。 + +不回滚解决方案: +1. 检查方法是不是public +2. 检查异常是不是unchecked异常 +3. 如果是checked异常也想回滚的话,注解上写明异常类型即可@Transactional(rollbackFor=Exception.class) + +事务失效的8大原因: +1. 数据库引擎不支持事务 +2. 没有被 Spring 管理 +3. 方法不是 public 的 +4. 自身调用问题 +5. 数据源没有配置事务管理器 +6. 不支持事务(传播机制) +7. 异常被吃了(捕获异常) +8. 异常类型错误(checked异常失效) + +### Spring的的事务传播机制 +1. **required**(默认):支持使用当前事务,如果当前事务不存在,创建一个新事务。 +2. **requires_new**:创建一个新事务,如果当前事务存在,把当前事务挂起。 +3. **supports**:支持使用当前事务,如果当前事务不存在,则不使用事务。 +4. **not_supported**:无事务执行,如果当前事务存在,把当前事务挂起。 +5. **mandatory**:强制,支持使用当前事务,如果当前事务不存在,则抛出Exception。 +6. **never**:无事务执行,如果当前有事务则抛出Exception。 +7. **nested**:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟REQUIRED一样。 + +### 事务源码 +- 开启@EnableTransactionManagement +- 利用TransactionManagementConfigurationSelector给容器中会导入组件 + - AutoProxyRegistrar + - 给容器中注册一个 InfrastructureAdvisorAutoProxyCreator 组件 + - 利用后置处理器机制在对象创建以后,包装对象,返回一个代理对象(增强器),代理对象执行方法利用拦截器链进行调用; + - ProxyTransactionManagementConfiguration(给容器中注册事务增强器) + - 事务增强器要用事务注解的信息,AnnotationTransactionAttributeSource解析事务注解 + - 事务拦截器 \ No newline at end of file diff --git "a/Java/classify/sys/\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\347\256\241\347\220\206\346\226\271\345\274\217.md" "b/Java/classify/sys/\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\347\256\241\347\220\206\346\226\271\345\274\217.md" new file mode 100644 index 00000000..26548dc4 --- /dev/null +++ "b/Java/classify/sys/\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\347\256\241\347\220\206\346\226\271\345\274\217.md" @@ -0,0 +1,22 @@ + +## 分页管理 + +分页存储管理是将一个进程的逻辑地址空间分成若干个大小相等的片,称为页面或页,并为各页加以编号,从0开始,如第0页、第1页等。相应地,也把内存空间分成与页面相同大小的若干个存储块,称为(物理)块或页框(frame),也同样为它们加以编号,如0#块、1#块等等。在为进程分配内存时,以块为单位将进程中的若干个页分别装入到多个可以不相邻接的物理块中。由于进程的最后一页经常装不满一块而形成了不可利用的碎片,称之为“页内碎片”。 + +**优缺点**:**没有外部碎片,内存利用率高。但各页中内容没有关联,不利于编程和共享**。 + +## 分段管理 + +程序通过分段(segmentation)划分为多个模块,如代码段、数据段、共享段。内存每段的大小都匹配程序段,不会产生内部碎片。 +**优缺点**: 可以针对不同类型的段采取不同的保护。 可以按段为单位来进行共享,包括通过动态链接进行代码共享。 **不会产生内部碎片,但会产生外部碎片,内存利用率比分页低**。 + +## 段页式管理 + +一个进程中所包含的具有独立逻辑功能的程序或数据仍被划分为段,并有各自的段号s。这反映相继承了段式管理的特征。其次,对于段s中的程序或数据,则按照一定的大小将其划分为不同的页。和页式系统一样,最后不足一页的部分仍占一页。这反映了段页式管理中的页式特征。从而,段页式管理时的进程的虚拟地址空间中的虚拟地址由三部分组成:即段号s,页号P和页内相对地址d。虚拟空间的最小单位是页而不是段,从而内存可用区也就被划分成为若干个大小相等的页面,且每段所拥有的程序和数据在内存中可以分开存放。分段的大小也不再受内存可用区的限制。 +**优缺点:**既有具有独立逻辑功能的段,又以大小相同的页为内存分配单位进而不会产生外部碎片。但仍会有内部碎片。 + +[操作系统如管理内存](https://blog.csdn.net/hguisu/article/details/5713164) + +## 内存分配过程 + +[https://blog.csdn.net/edonlii/article/details/22601043](https://blog.csdn.net/edonlii/article/details/22601043) \ No newline at end of file diff --git "a/Java/classify/sys/\346\255\273\351\224\201.md" "b/Java/classify/sys/\346\255\273\351\224\201.md" new file mode 100644 index 00000000..30fbc5e5 --- /dev/null +++ "b/Java/classify/sys/\346\255\273\351\224\201.md" @@ -0,0 +1,20 @@ +## 死锁的条件 + +所以:死锁的条件? + +- **互斥条件**:该资源任意一个时刻只由一个线程占用。(同一时刻,这个碗是我的,你不能碰) +- **请求与保持条件**:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(我拿着这个碗一直不放) +- **不剥夺条件**:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。(我碗中的饭没吃完,你不能抢,释放权是我自己的,我想什么时候放就什么时候放) +- **循环等待条件**:若干进程之间形成一种头尾相接的循环等待资源关系。(我拿了A碗,你拿了B碗,但是我还想要你的B碗,你还想我的A碗)比喻成棒棒糖也阔以。 + +面试官:所以解决死锁的办法是? + +我:好的,没毛病 + +- 预防死锁: + - **资源一次性分配**:破坏请求和保持条件。 + - **可剥夺资源**:当进程新申请的资源不满足时,释放已经分配的资源。破坏不可剥夺条件 + - **资源有序分配**:系统给进程编号,按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 +- 避免死锁:银行家算法:分配资源前先评估风险,会不会在分配后导致死锁。 即分配给一个进程资源的时候,该进程能否全部返还占用的资源。 +- 检测死锁:建立资源分配表和进程等待表。 +- 解除死锁:可以直接撤销死锁进程,或撤销代价最小的进程。 \ No newline at end of file diff --git "a/Java/classify/sys/\347\263\273\347\273\237\350\277\233\347\250\213\350\260\203\345\272\246.md" "b/Java/classify/sys/\347\263\273\347\273\237\350\277\233\347\250\213\350\260\203\345\272\246.md" new file mode 100644 index 00000000..88d08eee --- /dev/null +++ "b/Java/classify/sys/\347\263\273\347\273\237\350\277\233\347\250\213\350\260\203\345\272\246.md" @@ -0,0 +1,8 @@ + + +- FCFS(先来先服务,队列实现,非抢占的):先请求CPU的进程先分配到CPU +- SJF(最短作业优先调度算法):平均等待时间最短,但难以知道下一个CPU区间长度 +- 优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;解决方案:老化 +- 时间片轮转调度算法(可抢占的):队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。 +- 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。 +- 多级反馈队列调度算法:与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。 \ No newline at end of file diff --git "a/Java/classify/sys/\350\231\232\346\213\237\345\206\205\345\255\230.md" "b/Java/classify/sys/\350\231\232\346\213\237\345\206\205\345\255\230.md" new file mode 100644 index 00000000..042f78da --- /dev/null +++ "b/Java/classify/sys/\350\231\232\346\213\237\345\206\205\345\255\230.md" @@ -0,0 +1,13 @@ + +> 为什么操作系统引进虚拟内存?说白了,直接让一个进程全部放在主内存当中,太占用内存啦,以前的计算机的内存可没有那么大,并且程序也很多。所以在进程和主内存之间存在虚拟内存。可以[参考](https://draveness.me/whys-the-design-os-virtual-memory/) + + +虚拟内存允许执行进程不必完全在内存中。虚拟内存的基本思想是: +- 每个进程拥有独立的地址空间,这个空间被分为大小相等的多个块,称为页(Page),每个页都是一段连续的地址。 +- 这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。 +- 当程序引用到一部分在物理内存中的地址空间时,由硬件立刻进行必要的映射;当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的命令。这样,对于进程而言,逻辑上似乎有很大的内存空间,实际上其中一部分对应物理内存上的一块(称为帧,通常页和帧大小相等),还有一些没加载在内存中的对应在硬盘上。 + +举个小例子: +吃饭。 + +比如你和同学去聚餐,根据菜谱点了一堆的菜,有汤,有肉,有素材等。 大可不必一下子全做完一起送过来(如果桌子不给力,比较小,并且时间成本,饭菜都凉了等),比如,同学想吃肉哈?不想先喝汤,那就把肉类端上来,对吧?等同学又想喝汤了,就把汤做一下端过来。 diff --git "a/Java/classify/sys/\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/Java/classify/sys/\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" new file mode 100644 index 00000000..cb636b53 --- /dev/null +++ "b/Java/classify/sys/\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -0,0 +1,54 @@ +# 线程与进程的区别 +面试官:说一下线程与进程的区别 + +我:好的,如下: + +- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位 + +- 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 + +- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的 + +- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。 + +- 与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程 + +举例子:比如,main函数启动了JVM进程,同时main就是其中的线程,并且启动了JVM进程,那么还有垃圾回收等线程。 + +或者这样的例子:做个简单的比喻:进程=火车,线程=车厢 + +- 线程在进程下行进(单纯的车厢无法运行) +- 一个进程可以包含多个线程(一辆火车可以有多个车厢) +- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘) +- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易) +- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源) +- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢) +- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上) +- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁" +- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量” + + + +--- + +## 协程 + +协程: + +协程是一种用户态的轻量级线程, 我们的server的发展如下:IO密集型应用:多进程 -> 多线程 ->事件驱动 ->协程 + +协程拥有自己的寄存器上下文和栈. 协程调度切换时,将寄存器上下文和栈保存到其他地方,然后去做其他工作,当你的IO解除之后切回原来的状态,恢复先前保存的寄存器上下文和栈。 + + +优点: + +- 跨平台 +- 无需线程上下文切换的开销 +- 无需原子操作锁定及同步的开销 +- 方便切换控制流,简化编程模型 +- 高并发+高扩展行+低成本: 一个CPU支持上万的协程都不是问题,所以很适合用于高并发处理 + +缺点: + +- 无法利用多核资源:协程的本质是一个单线程,它不能同时将单个CPU的多个核作用上,协程需要和进程配合才能运行在多CPU上. +- 进行阻塞(Blocking)操作会阻塞到整个程序; 这一点和事件驱动一样,可以使用异步IO操作来解决. \ No newline at end of file diff --git "a/Java/classify/sys/\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/Java/classify/sys/\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" new file mode 100644 index 00000000..9ea43cef --- /dev/null +++ "b/Java/classify/sys/\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -0,0 +1,57 @@ + +# 进程间通信 + +> 首先要知道进程之间为什么要通信。 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源(例如打开的文件描述符)。 但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程之间的通信。 + +## 进程通信的目的 + +- 数据传输:一个进程需要将它的数据发送给另一个进程。 +- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 +- 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。 +- 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。 + + +## 通信方式 + +### 管道 + +- 普通管道:通常有两种限制,一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用. +- 流管道:去除了第一种限制,为半双工,只能在父子或兄弟进程间使用,可以双向传输. +- 命名管道:去除了第二种限制,可以在许多并不相关的进程之间进行通讯. + +### 信号量 + +- 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。 +- 它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。 +- 因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 + +### 消息队列 + +消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 + +### 信号 + +- 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 + +### 共享内存 + +共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。 + +#### 共享内存的实现(mmap) +mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。 + +#### 系统调用mmap共享内存的两种方式 +- 使用普通文件提供的内存映射:适用于任何进程之间; +- 使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间; 由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。 + +### socket + +这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。 + +## 线程之间的通信 + +1. 锁机制:包括互斥锁、条件变量、读写锁 +2. 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量 +3. 信号机制(Signal):类似进程间的信号处理 + +> 线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。 \ No newline at end of file diff --git "a/Java/classify/sys/\351\241\265\351\235\242\347\275\256\346\215\242\347\256\227\346\263\225.md" "b/Java/classify/sys/\351\241\265\351\235\242\347\275\256\346\215\242\347\256\227\346\263\225.md" new file mode 100644 index 00000000..d5650418 --- /dev/null +++ "b/Java/classify/sys/\351\241\265\351\235\242\347\275\256\346\215\242\347\256\227\346\263\225.md" @@ -0,0 +1,7 @@ +> 地址映射过程中,若在页面中发现所要访问的页面不再内存中,则产生缺页中断。当发生缺页中断时操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。常见的置换算法有: + +- 最佳置换算法(OPT):所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。 +- 先进先出置换算法(FIFO):选择换出的页面是最先进入的页面。该算法会将那些经常被访问的页面换出,导致缺页率升高。 +- 第二次机会算法:当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。 +- 最近最久未使用(LRU)算法:为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。 + diff --git a/Java/classify/thread/AQS.md b/Java/classify/thread/AQS.md new file mode 100644 index 00000000..497f219d --- /dev/null +++ b/Java/classify/thread/AQS.md @@ -0,0 +1,69 @@ +# AQS + +> 这个先留着,这个内容有点多,待我这周结束之后来写。 + +面试官:AQS是什么? + +## 原理 +我:AbstractQueuedSynchronizer抽象同步队列。说白了,就是个FIFO双向队列。其内部通过节点head和tail记录对首和队尾元素。 + +**state**:在AQS中维持了一个单一的状态信息state,可以通过**getState**、**setState**、**compareAndSetState**函数修改其值。 + +- **ReentrantLock**:state可以用来表示当前线程获取锁的可重入次数。 + +- **ReentrantReadWriteLock**:state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数。 +- **semaphore**:state用来表示当前可用信号的个数。 +- **CountDownlatch**:state用来表示计数器当前的值。 + +对于AQS来讲,线程同步的关键是对状态值**state**进行操作。 + +## 方法 + +在独占方式下获取和释放资源使用的方法: + +```java +void acquire(int arg); +void acquireInterruptibly(int arg); +boolean release(int arg); +``` + +在共享方式获取和释放资源使用的方法: + +```java +void acquireShared(int arg); +void acquireSharedInterruptibly(int arg); +boolean releaseShared(int arg); +``` + +使用独占方式获取的资源是与**具体线程绑定**的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会**发现当前该资源不是自己持有的**,就会在**获取失败后被阻塞**。(比如ReentrantLock) + +对应的共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的资源,则当前线程只需要使用CAS方式进行获取即可。(比如semaphore) + +看一下**acquire**方法: + +```java +// tryAcquire 具体的子类去实现,并维护state的状态 +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果失败标记状态,入队 + selfInterrupt(); +} +``` + +看一下**release**方法: + +```java +// tryRelease 具体的子类是实现,并设置state的状态 +public final boolean release(int arg) { + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); // 调用unpark唤醒队列的线程,并调用tryAcquire尝试,看是否需要,如果不需要,继续挂起 + return true; + } + return false; +} +``` + +**acquireShared和releaseShared**和上面的方法的思想差不多 + diff --git "a/Java/classify/thread/BlockingQueue\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/classify/thread/BlockingQueue\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" new file mode 100644 index 00000000..d8970013 --- /dev/null +++ "b/Java/classify/thread/BlockingQueue\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" @@ -0,0 +1,158 @@ +# BlockingQueue +看一下官方1.8的官方文档 +![已知实现类](https://imgkr.cn-bj.ufileos.com/1c08311d-77a7-4f60-a4dd-2e293f43c8d0.png) + +官方文档告诉我们它们有这些特点: +- BlockingQueue实现被设计为主要用于生产者 - 消费者队列 +- BlockingQueue实现是线程安全的。 所有排队方法使用内部锁或其他形式的并发控制在原子上实现其效果。 +- BlockingQueue方法有四种形式,具有不同的操作方式,不能立即满足,但可能在将来的某个时间点满足: + - 一个抛出异常 + - 返回一个特殊值( null或false ,具体取决于操作) + - 第三个程序将无限期地阻止当前线程,直到操作成功为止 + - 在放弃之前只有给定的最大时限。 + +## 看源码之旅 + +### ArrayBlockingQueue +> 看一下官方文档的解释->一个有限的blocking queue由数组支持。 这个队列排列元素FIFO(先进先出)。 队列的头部是队列中最长的元素。 队列的尾部是队列中最短时间的元素。 新元素插入队列的尾部,队列检索操作获取队列头部的元素。这是一个经典的“有界缓冲区”,其中固定大小的数组保存由生产者插入的元素并由消费者提取。 创建后,容量无法更改。 尝试put成满的队列的元件将导致在操作阻挡; 尝试take从空队列的元件将类似地阻塞。此类支持可选的公平策略,用于订购等待的生产者和消费者线程。 默认情况下,此订单不能保证。 然而,以公平设置为true的队列以FIFO顺序授予线程访问权限。 公平性通常会降低吞吐量,但会降低变异性并避免饥饿。 + +#### 常见变量 +```java +int count; // 队列元素数量 +final ReentrantLock lock; // 内部线程安全性用了ReentrantLock +private final Condition notEmpty; // takes方法的等待条件 +private final Condition notFull; // puts方法的等待条件 +``` +#### 构造方法 +```java +public ArrayBlockingQueue(int capacity, boolean fair) { + if (capacity <= 0) + throw new IllegalArgumentException(); // 这里就不说了 + this.items = new Object[capacity]; // 初始容量 + lock = new ReentrantLock(fair); // fair参数决定是否公平锁 + notEmpty = lock.newCondition(); // 上面已提到 + notFull = lock.newCondition(); // 上面已提到 +} +``` +#### add +```java +public boolean add(E e) { + return super.add(e); // 该方法添加失败抛出异常 +} +// AbstractQueue +public boolean add(E e) { + if (offer(e)) + return true; + else + throw new IllegalStateException("Queue full"); // 在这 +} +``` +**remove就不分析了** + +#### offer +```java +public boolean offer(E e) { + checkNotNull(e); + final ReentrantLock lock = this.lock; // 上面的add其实内部也调用了offer,当时我还觉得奇怪,add没上锁?。 原来offer上了锁的 + lock.lock(); + try { + if (count == items.length) + return false; // 满了,就false + else { + enqueue(e); // 否则,添加即可 + return true; // 返回true,并不会抛出异常 + } + } finally { + lock.unlock(); + } +} +``` +**poll不分析了** + +#### put +```java +public void put(E e) throws InterruptedException { + checkNotNull(e); // 检查是否为空,为空就抛出空异常 + final ReentrantLock lock = this.lock; // 上锁哦 + lock.lockInterruptibly(); // 锁中断 + try { + while (count == items.length) // 满了,挂起阻塞 + notFull.await(); + enqueue(e); // 否则添加 + } finally { + lock.unlock(); + } +} +``` + +#### take +```java +public E take() throws InterruptedException { + final ReentrantLock lock = this.lock; // 上锁 + lock.lockInterruptibly(); // 锁中断 + try { + while (count == 0) + notEmpty.await(); // 没有元素,挂起 + return dequeue(); + } finally { + lock.unlock(); + } +} +``` + +#### 带时间的offer和poll +> 其实就是用了`long nanos = unit.toNanos(timeout);` + + +### LinkedBlockingQueue +> 四个方法和ArrayBlockingQueue的差不多,就不多分析了。它的特点之一在于,如果不指定容量,那么默认是等于Integer.MAX_VALUE。可以看一下源码 + +#### 常见的参数 +```java +/** The capacity bound, or Integer.MAX_VALUE if none */ +private final int capacity; // 一看注释,就晓得了 +// 还有一些常见的和上面的差不多 +``` + +#### 构造方法 +```java +public LinkedBlockingQueue() { + this(Integer.MAX_VALUE); // 在这里, 嘿嘿嘿。 +} +``` + +### LinkedTransferQueue +> 基于链接节点的无界TransferQueue 。 这个队列相对于任何给定的生产者订购元素FIFO(先进先出)。 队列的头部是那些已经排队的元素是一些生产者的最长时间。 队列的尾部是那些已经在队列上的元素是一些生产者的最短时间。 + +**四个方法就暂时不提了,大部分操作用了cas并且要关注transfer方法** + +先介绍几个标志参数: +```java +private static final int NOW = 0; // for untimed poll, tryTransfer +private static final int ASYNC = 1; // for offer, put, add +private static final int SYNC = 2; // for transfer, take +private static final int TIMED = 3; // for timed poll, tryTransfer +``` + +```java +// 如果可能,立即将元素转移到等待的消费者。 +public boolean tryTransfer(E e) { + return xfer(e, true, NOW, 0) == null; // xfer的now参数 +} +// 还有一个重载参数的,带时间的。 +``` + +```java +// 将元素传输到消费者,必要时等待。 +public void transfer(E e) throws InterruptedException { + if (xfer(e, true, SYNC, 0) != null) { // SYNC + Thread.interrupted(); // failure possible only due to interrupt + throw new InterruptedException(); + } +} +``` +### PriorityBlockingQueue +> 一看名字就是优先级阻塞队列,它的优先级是由堆实现的,所以该类中有很多堆的方法。源码暂时就不看了,很多都是差不多的。 + +### SynchronousQueue +> 官方文档:每个插入操作必须等待另一个线程相应的删除操作,反之亦然。 同步队列没有任何内部容量,甚至没有一个容量。 个人感觉是生产者生产一个元素,消费者必须消费,生产者才能继续生产。内部也维护了一个TransferQueue,其中部分操作是利用cas。源码就不贴了。有兴趣的可以进去看看哦。 diff --git a/Java/classify/thread/CAS.md b/Java/classify/thread/CAS.md new file mode 100644 index 00000000..9b10034f --- /dev/null +++ b/Java/classify/thread/CAS.md @@ -0,0 +1,98 @@ +# CAS + +面试官:了解CAS嘛? + +我:了解,**我们在读Concurrent包下的类的源码时,发现无论是**ReenterLock内部的AQS,还是各种Atomic开头的原子类,内部都应用到了`CAS`,并且在调用getAndAddInt方法中,会有compareAndSwapInt方法。其实都是调用unsafe.compareAndSwap()方法。 + +## compareAndSwapInt + +细说compareAndSwapInt方法,那岂不是要看源码: + +```cpp +UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) + UnsafeWrapper("Unsafe_CompareAndSwapInt"); + oop p = JNIHandles::resolve(obj); // oop 安全点 引用 + jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); // 计算偏移地址 + return (jint)(Atomic::cmpxchg(x, addr, e)) == e; // 原子操作,比较和更新 +UNSAFE_END +``` + +Java中的`compareAndSwapInt(obj, offset, expect, update)`比较清楚,**意思就是如果`obj`内的`value`和`expect`相等,就证明没有其他线程改变过这个变量,那么就更新它为`update`,如果这一步的`CAS`没有成功,那就采用自旋的方式继续进行`CAS`操作**,取出乍一看这也是两个步骤了啊,其实在`JNI`里是借助于一个`CPU`指令完成的。所以还是原子操作。 + +原子操作:可理解为要么这些行为都执行,不被打扰,要不都不执行。实在不行,就理解为串行操作, + +再细说这个cpu指令,那就要看该源码了: + +```hpp +inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { + int mp = os::is_MP(); // 返回处理器(判断是多核还是单核,单核省略lock前缀) + __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" // 比较 + : "=a" (exchange_value) + : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) + : "cc", "memory"); + return exchange_value; +} +``` + + + +调用了`Atomic::cmpxchg(x, addr, e)`, 其中参数**x是即将更新的值**,参数**e是原内存的值**。代码中能看到cmpxchg有基于各个平台的实现。 + +## ABA + +面试官:知道ABA问题嘛? + +我:知道,描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程**也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A** (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时**变量 x 的值还是 A**,所以 compareAndSet 操作是成功。 + +解决方法:**目前在JDK的atomic包里提供了一个类`AtomicStampedReference`来解决ABA问题。**说白了,就是版本号 + +举一个例子: + +```java +public class ABADemo { + static AtomicInteger atomicInteger = new AtomicInteger(100); + static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1); + + public static void main(String[] args) { + System.out.println("=====ABA的问题产生====="); + new Thread(() -> { + atomicInteger.compareAndSet(100, 101); + atomicInteger.compareAndSet(101, 100); + }, "t1").start(); + + new Thread(() -> { + // 保证线程1完成一次ABA问题 + try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } + System.out.println(atomicInteger.compareAndSet(100, 2020) + " " + atomicInteger.get()); + }, "t2").start(); + try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } + + System.out.println("=====解决ABA的问题====="); + new Thread(() -> { + int stamp = atomicStampedReference.getStamp(); // 第一次获取版本号 + System.out.println(Thread.currentThread().getName() + " 第1次版本号" + stamp); + try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } + atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); + System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp()); + atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); + System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp()); + }, "t3").start(); + + new Thread(() -> { + int stamp = atomicStampedReference.getStamp(); + System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp); + try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } + boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, stamp + 1); + System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp()); + System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference()); + }, "t4").start(); + + } +} +``` + +## CAS设计思想 + +面试官:了解CAS的设计思想吗? + +我:思想?我理解的是乐观思想,你听我娓娓道来,何为乐观,乐观就是在我角度上认为"哈士奇"不会去偷吃我的菜,那么我就不用同步加个房间锁住门防止"哈士奇"吃。那设计的时候,cpu咋知道"哈士奇"吃过没,那就看这道菜是不是少了点,或者量少了一半等。那么主人回来吃的时候肯定不会开吃了,是吧?所以主人吃就跟CAS的更新值一样,这道菜没被"哈士奇"吃过是CAS的期望值。 那么cpu的这个cmpxchg就是干这个事的。 \ No newline at end of file diff --git a/Java/classify/thread/CountDownLatch.md b/Java/classify/thread/CountDownLatch.md new file mode 100644 index 00000000..2f49a51a --- /dev/null +++ b/Java/classify/thread/CountDownLatch.md @@ -0,0 +1,53 @@ +# CountDownLatch + +> 我们经常在主线程中开启多个线程去并行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。知道你们要说join,但是join不够灵活。 + +面试官:讲讲CountDownLatch原理 + +我:我试试 + +## 原理 + +首先状态变量state:**state用来表示计数器当前的值**,当线程调用CountDownLatch对象的await方法后, 当前线程会被阻塞,直到下面的情况之一发生才返回:当所有线程都调用了CountDownLatch对象的countDown方法后,也就是**计数器的值为0**时:其他线程调用了**当前线程的interrupt()方法中断了当前线程**,当前线程就会抛出InterruptedException异常。 + +## 方法 + +所以,我们看一下await方法: + +```java +public final void acquireSharedInterruptibly(int arg) + throws InterruptedException { + if (Thread.interrupted()) // 线程可中断 + throw new InterruptedException(); + if (tryAcquireShared(arg) < 0) // 如果等于-1, 说明还在挂起 + doAcquireSharedInterruptibly(arg); +} +protected int tryAcquireShared(int acquires) { + return (getState() == 0) ? 1 : -1; // 如果为0,则返回1,不为0,则返回-1 +} +``` + +看一下countDown方法: + +```java +public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) { + doReleaseShared(); // 激活被阻塞的线程 + return true; + } + return false; +} +protected boolean tryReleaseShared(int releases) { // 尝试释放锁 + // Decrement count; signal when transition to zero + for (;;) { + int c = getState(); + if (c == 0) + return false; // 如果等于0, 返回false,不用释放 + int nextc = c-1; + if (compareAndSetState(c, nextc)) // 更新state + return nextc == 0; // nextc如果等于0了,说明资源释放成功,但是不管成功与否,都会退出循环 + // 并且去激活被await阻塞的线程 + } +} +``` + diff --git "a/Java/classify/thread/JMM\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/Java/classify/thread/JMM\345\206\205\345\255\230\346\250\241\345\236\213.md" new file mode 100755 index 00000000..eeda039d --- /dev/null +++ "b/Java/classify/thread/JMM\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -0,0 +1,3 @@ +![volatile保证内存可见性和避免重排-WDKMZd](https://gitee.com/dreamcater/blog-img/raw/master/uPic/volatile保证内存可见性和避免重排-WDKMZd.png) + + diff --git "a/Multithread/Java\351\224\201\346\234\272\345\210\266.md" "b/Java/classify/thread/Java\351\224\201\347\232\204\344\273\213\347\273\215.md" similarity index 79% rename from "Multithread/Java\351\224\201\346\234\272\345\210\266.md" rename to "Java/classify/thread/Java\351\224\201\347\232\204\344\273\213\347\273\215.md" index ea7a9da9..e8c3f0a3 100644 --- "a/Multithread/Java\351\224\201\346\234\272\345\210\266.md" +++ "b/Java/classify/thread/Java\351\224\201\347\232\204\344\273\213\347\273\215.md" @@ -1,36 +1,9 @@ ---- -title: Java锁机制 -author: DreamCat -id: 1 -date: 2020-02-10 01:22:20 -tags: Java -categories: Java - ---- - -## 引言 - -> Java常见的几种锁,比如: -> -> - 公平锁/非公平锁 -> - 可重入锁 -> - 独享锁/共享锁 -> - 互斥锁/读写锁 -> - 乐观锁/悲观锁 -> - 分段锁 -> - 偏向锁/轻量级锁/重量级锁 -> - 自旋锁 - - - +# 各种锁 ## 公平锁/非公平锁 - **公平锁指多个线程按照申请锁的顺序来获取锁。非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象(很长时间都没获取到锁-非洲人...),ReentrantLock,了解一下。** ## 可重入锁 - **可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,典型的synchronized,了解一下** - ```java synchronized void setA() throws Exception { Thread.sleep(1000); @@ -42,12 +15,10 @@ synchronized void setB() throws Exception { ``` ## 独享锁/共享锁 - - 独享锁:是指该锁一次只能被一个线程所持有。 - 共享锁:是该锁可被多个线程所持有。 ## 互斥锁/读写锁 - **上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是其具体的实现** ## 乐观锁/悲观锁 @@ -61,11 +32,8 @@ synchronized void setB() throws Exception { ## 分段锁 1. **分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来哦实现高效的并发操作。** - -2. ** 以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是ReentrantLock(Segment继承了ReentrantLock)** - +2. **以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是ReentrantLock(Segment继承了ReentrantLock)** 3. **当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。** - 4. **分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。** ## 偏向锁/轻量级锁/重量级锁 @@ -82,13 +50,49 @@ synchronized void setB() throws Exception { 3. **自旋锁尽可能的减少线程的阻塞,适用于锁的竞争不激烈,且占用锁时间非常短的代码来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。** 4. **但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适用使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu的线程又不能获取到cpu,造成cpu的浪费。** +### 手写自旋锁的例子 +```java +public class SpinLock { + + // 原子引用线程 + AtomicReference atomicReference = new AtomicReference<>(); + + public void mylock() { + Thread thread = Thread.currentThread(); + System.out.println(Thread.currentThread().getName() + " como in..."); + while (!atomicReference.compareAndSet(null, thread)) { +// System.out.println("不爽,重新获取一次值瞧瞧..."); + } + } + + public void myUnlock() { + Thread thread = Thread.currentThread(); + atomicReference.compareAndSet(thread, null); + System.out.println(Thread.currentThread().getName() + " invoke myUnLock..."); + } + + public static void main(String[] args) { + SpinLock spinLock = new SpinLock(); + new Thread(() -> { + spinLock.mylock(); + try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } + spinLock.myUnlock(); + }, "t1").start(); + try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } + + new Thread(() -> { + spinLock.mylock(); + spinLock.myUnlock(); + }, "t2").start(); + } + +} +``` + ## Java锁总结 **Java锁机制可归为Sychornized锁和Lock锁两类。Synchronized是基于JVM来保证数据同步的,而Lock则是硬件层面,依赖特殊的CPU指令来实现数据同步的。** - Synchronized是一个非公平、悲观、独享、互斥、可重入的重量级锁。 - ReentrantLock是一个默认非公平但可实现公平的、悲观、独享、互斥、可重入、重量级锁。 -- ReentrantReadWriteLock是一个默认非公平但可实现公平的、悲观、写独享、读共享、读写、可重入、重量级锁。 - - - +- ReentrantReadWriteLock是一个默认非公平但可实现公平的、悲观、写独享、读共享、读写、可重入、重量级锁。 \ No newline at end of file diff --git a/Java/classify/thread/ReentrantLock.md b/Java/classify/thread/ReentrantLock.md new file mode 100644 index 00000000..31191b6e --- /dev/null +++ b/Java/classify/thread/ReentrantLock.md @@ -0,0 +1,113 @@ +# ReentrantLock + +> 经常拿synchronized和ReentrantLock做比较,那就来看看这个锁的一些重点吧 + +面试官:了解ReentrantLock嘛? + +## 原理 + +我:当然,它实现了Lock接口,同时调用内部类sync继承的AQS,先说一下**state**:它代表获取该锁的**可重入次数**,在默认下,state的值为0表示**当前锁没有被任何线程持有**。当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,并且记录该锁的持有者为当前线程。若该线程第二次获取该锁后,状态值被设置2。 + + +## 方法 + +我们来看一下lock方法: + +```java +public void lock() { + sync.lock(); // 委托给sync了 +} +``` + +而且它具有非公平锁还是公平锁的特性。比如,我们可以看构造方法 + +```java +// 这不,由fair来决定是公平的还是非公平的 +public ReentrantLock(boolean fair) { + sync = fair ? new FairSync() : new NonfairSync(); +} +``` + +- 非公平锁: + +```java +final void lock() { + if (compareAndSetState(0, 1)) // cas设置state的状态,0->1 + setExclusiveOwnerThread(Thread.currentThread()); // 设置独占 + else + acquire(1); // 否则尝试,如果还是当前线程,就state累加,若不是,则挂起 +} +// 看看 acquire +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +// 看看重写的tryAcquire +protected final boolean tryAcquire(int acquires) { + return nonfairTryAcquire(acquires); +} +// 重点来了 +final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { // 0->acquires + if (compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } + else if (current == getExclusiveOwnerThread()) { // 否则判断该资源是否被该线程持有 + int nextc = c + acquires; // 持有,则+acquires + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; +} +``` + +- 公平锁和上面的差不多,就多了一个这样的判断: + +```java +if (c == 0) { + if (!hasQueuedPredecessors() && // 队列中是否轮到该线程了 + compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } +} +``` + +我们来看一下unlock()方法: + +```java +public void unlock() { + sync.release(1); +} +// release看看 +public final boolean release(int arg) { + if (tryRelease(arg)) { // 依然调 + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} +// 看看sync重写的 +protected final boolean tryRelease(int releases) { + int c = getState() - releases;// 并不会设置为0,而是减releases + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + if (c == 0) { // 如果状态为0了,则free为true + free = true; + setExclusiveOwnerThread(null); // 并且将持有该资源的线程设置为null + } + setState(c); // cas操作 + return free; +} +``` + diff --git a/Java/classify/thread/ReentrantReadWriteLock.md b/Java/classify/thread/ReentrantReadWriteLock.md new file mode 100644 index 00000000..4ee834a9 --- /dev/null +++ b/Java/classify/thread/ReentrantReadWriteLock.md @@ -0,0 +1,190 @@ +# ReentrantReadWriteLock + +> 聊一聊读写锁 + +面试官:了解AQS的读写锁嘛,知道其原理嘛? + +我:刚好了解,我们知道,在一些读多写少的场景中,若是用ReentrantLock效率显然不高,于是ReentrantReadWriteLock问世。 + +## 原理 + +老规矩: + +```java +// 维护了readlock和writelock +private final ReentrantReadWriteLock.ReadLock readerLock; +/** Inner class providing writelock */ +private final ReentrantReadWriteLock.WriteLock writerLock; +/** Performs all synchronization mechanics */ +final Sync sync; // 同样的是sync 继承aqs + +// 可惜的是state依然是一个, 但是不慌 +// 高16位表示读状态,低16位表示获取到写锁的线程的可重入锁 +static final int SHARED_SHIFT = 16; +// 共享锁的读锁的状态单位值65536 +static final int SHARED_UNIT = (1 << SHARED_SHIFT); +// 共享锁线程最大个数65536 +static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; +// 排它锁写锁掩码,二进制 15个1 +static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; +// 返回读锁线程数 +static int sharedCount(int c) { return c >>> SHARED_SHIFT; } +// 返回写锁可重入个数 +static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } +``` + +## 方法 + +直奔主题:写锁的获取与释放 + +**lock:** + +```java +// lock +public void lock() { + sync.acquire(1); +} +// acquire +public final void acquire(int arg) { + if (!tryAcquire(arg) && // 老规矩了 + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +// tryAcquire +protected final boolean tryAcquire(int acquires) { + Thread current = Thread.currentThread(); + int c = getState(); + int w = exclusiveCount(c); // 得到低16位的值 + // 如果c!=0, 说明资源已经被其他读锁或者写锁的线程所获取 + if (c != 0) { + // w==0 代表低16位位0,那么高16位不为0,那么获取了读锁 + if (w == 0 || current != getExclusiveOwnerThread()) + return false; + // 到这一步,已经是写锁了,那么判断可重入次数 + if (w + exclusiveCount(acquires) > MAX_COUNT) + throw new Error("Maximum lock count exceeded"); + // Reentrant acquire + // 更新state + setState(c + acquires); + return true; + } + // 是否第一个写锁获取线程 + if (writerShouldBlock() || + !compareAndSetState(c, c + acquires)) + return false; + setExclusiveOwnerThread(current); + return true; +} +``` + +**unlock:** + +```java +// unlock +public void unlock() { + sync.release(1); +} +// 不提了 +public final boolean release(int arg) { + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} +// 重点 +protected final boolean tryRelease(int releases) { + if (!isHeldExclusively()) + throw new IllegalMonitorStateException(); + int nextc = getState() - releases; // 依然是减 + boolean free = exclusiveCount(nextc) == 0; // 判断是否为0 + if (free) // 移除该写锁持有的线程 + setExclusiveOwnerThread(null); + setState(nextc); // 更新 + return free; +} +``` + +读锁的获取和释放: + +**lock:** + +```java +protected final int tryAcquireShared(int unused) { + Thread current = Thread.currentThread(); + int c = getState(); + // 判断是否被写锁占用 + if (exclusiveCount(c) != 0 && + getExclusiveOwnerThread() != current) + return -1; + // 获取读锁的数量 + int r = sharedCount(c); + // 尝试获取锁,多个读线程只有一个会成功,不成功的进入fullTryAcqureShared进行重试 + if (!readerShouldBlock() && + r < MAX_COUNT && + compareAndSetState(c, c + SHARED_UNIT)) { + // 第一个线程获取读锁 + if (r == 0) { + firstReader = current; + firstReaderHoldCount = 1; + // 如果当前线程是第一个获取读锁的线程 + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { + // 记录最后一个获取读锁的线程的线程或记录其他线程读锁的可重入数 + HoldCounter rh = cachedHoldCounter; + if (rh == null || rh.tid != getThreadId(current)) + cachedHoldCounter = rh = readHolds.get(); + else if (rh.count == 0) + readHolds.set(rh); + rh.count++; + } + return 1; + } + // 自旋获取 + return fullTryAcquireShared(current); +} +``` + +**unlock:** + +```java +public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) { + doReleaseShared(); // 释放所有 + return true; + } + return false; +} +protected final boolean tryReleaseShared(int unused) { + Thread current = Thread.currentThread(); + if (firstReader == current) { + // assert firstReaderHoldCount > 0; + if (firstReaderHoldCount == 1) + firstReader = null; + else + firstReaderHoldCount--; + } else { + HoldCounter rh = cachedHoldCounter; + if (rh == null || rh.tid != getThreadId(current)) + rh = readHolds.get(); + int count = rh.count; + if (count <= 1) { + readHolds.remove(); + if (count <= 0) + throw unmatchedUnlockException(); + } + --rh.count; + } + // 循环直到自己的读计数-1.CAS更新成功 + for (;;) { + int c = getState(); + int nextc = c - SHARED_UNIT; + if (compareAndSetState(c, nextc)) + return nextc == 0; + } +} +``` + diff --git a/Java/classify/thread/ThreadLocal.md b/Java/classify/thread/ThreadLocal.md new file mode 100644 index 00000000..a088c863 --- /dev/null +++ b/Java/classify/thread/ThreadLocal.md @@ -0,0 +1,75 @@ +# ThreadLocal + +面试官:了解ThreadLocal嘛?用过ThreadLocal嘛? + +我:了解过:它是这样的,假如想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。 + +## 底层原理 + +面试官:你知道底层原理嘛? + +我:知道 + +首先看:结构 + +```java +public class Thread implements Runnable { + ...... +//与此线程有关的ThreadLocal值。由ThreadLocal类维护 +ThreadLocal.ThreadLocalMap threadLocals = null; + ...... +} +// 由源码可见,ThreadLocal存储的变量存在Thread的ThreadLocalMap中 +// 可理解为ThreadLocalMap是专门定制的一种Map +``` + +其次看set和get方法 + +```java +// set +public void set(T value) { + // 获取当前线程 + Thread t = Thread.currentThread(); + // 拿到定制的ThreadLocalMap的map + ThreadLocalMap map = getMap(t); + if (map != null) + //不为空,则绑定 + map.set(this, value); + else + // 否则创建 + createMap(t, value); +} +// getMap 返回的就是threadLocals +ThreadLocalMap getMap(Thread t) { + return t.threadLocals; +} + +// get +public T get() { + // 依然获取当前线程 + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) { + // 获取当前Entry + ThreadLocalMap.Entry e = map.getEntry(this); + if (e != null) { + @SuppressWarnings("unchecked") + // 如果不为空,则返回 + T result = (T)e.value; + return result; + } + } + return setInitialValue(); +} +``` + +**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。** + +**每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。这也就解释了ThreadLocal声明的变量为什么在每一个线程都有自己的专属本地变量。 + +## 内存泄漏 + +面试官:知道内存泄露嘛? + +我:知道,`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法。 + diff --git a/Java/classify/thread/synchronized.md b/Java/classify/thread/synchronized.md new file mode 100644 index 00000000..91875ae6 --- /dev/null +++ b/Java/classify/thread/synchronized.md @@ -0,0 +1,81 @@ +# synchronized和lock的区别 + +> 很多面试官爱问synchronized和lock的区别 + +面试官:区别?哈哈哈 + +我:面有表情,好的。 + +- **两者都是可重入锁**:两者都是可重入锁。“可重入锁”概念是:**自己可以再次获取自己的内部锁**。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 +- **synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API**:synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 +- **ReenTrantLock 比 synchronized 增加了一些高级功能** + 1. **等待可中断**:过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 + 2. **可实现公平锁** + 3. **可实现选择性通知(锁可以绑定多个条件)**:线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” + 4. **性能已不是选择标准**:在jdk1.6之前synchronized 关键字吞吐量随线程数的增加,下降得非常严重。1.6之后,**synchronized 和 ReenTrantLock 的性能基本是持平了。** + +## synchronized修饰范围 + +面试官:先扯synchronized吧,修饰范围?可否了解? + +我:当然了解。 + +- 实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 +- 静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 +- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁 + +这里可能让谈对象头里面都有什么哦,简单的说:**Hotspot 虚拟机的对象头包括两部分信息**,**第一部分用于存储对象自身的自身运行时数据**(哈希码、GC 分代年龄、锁状态标志等等),**另一部分是类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 + +## 底层原理 + +面试官:聊聊底层原理 + +我:好的,代码块和方法有些区别。 + +- 代码块 + +**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** + +- 方法 + +**synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。** + +面试官:1.6对synchronized的一些优化都有哪些? + +我:JDK1.6对锁的实现引入了大量的优化,如**自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等**技术来减少锁操作的开销。 + +面试官:简单的说一下偏向锁、轻量级锁等。 + +- 偏向锁 + +引入偏向锁的目的和引入轻量级锁的目的很像,**他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗**。但是不同是:轻量级锁在无竞争的情况下**使用 CAS 操作去代替使用互斥量**。而偏向锁在**无竞争的情况下会把整个同步都消除掉**。 + +- 轻量级锁 + +轻量级锁不是为了代替重量级锁,**它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量**。另外,轻量级锁的加锁和解锁都用到了**CAS**操作。 + +- 锁消除 + +锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,**如果检测到那些共享数据不可能存在竞争,那么就执行锁消除**。锁消除可以节省毫无意义的请求锁的时间。 + +- 自旋锁和自适应锁 + +**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。 + +**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定**。 + +面试官:简要说一下偏向锁->轻量级锁->重量级锁的升级过程 + +我:好的 + +- 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁 +- 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1 +- 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。 +- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁 +- 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 +- 如果自旋成功则依然处于轻量级状态。 +- 如果自旋失败,则升级为重量级锁。 + + + +> 个人觉得够了...能一口气扯完这些,足以了。AQS就够受了。 \ No newline at end of file diff --git a/Java/classify/thread/volatile.md b/Java/classify/thread/volatile.md new file mode 100644 index 00000000..709ecf5b --- /dev/null +++ b/Java/classify/thread/volatile.md @@ -0,0 +1,80 @@ +# volatile的特性 + +面试官:了解volatile嘛?有啥子特性 + +我:了解,两个特性:**内存可见性、禁止重排序** + +禁止重排序:**不管是编译器还是JVM还是CPU,都会对一些指令进行重排序,目的是为了提高程序运行的速度,提高程序的性能,毫无疑问,在单线程下没毛病,多线程就似乎生病了。** + +给你稍微举举例子:禁止重排->单例模式 + +```java +// 来一波双重校验 +public class Test { + private volatile static Test instance = null; + private Test(){} + + private static Test getInstance() { + if (instance == null) { + synchronized (Test.class) { + if (instance == null) { + instance = new Test(); + } + } + } + return instance; + } +} +// instance类变量前面修饰的volatile?是吧? +``` + +问题在于:上面的代码是一个很常见的单例模式实现方式,但是上述代码在多线程环境下是有问题的。为什么呢,问题出在instance对象的初始化上,因为`instance = new Singleton();`这个初始化操作并不是原子的,在JVM上会对应下面的几条指令: + +```c +memory =allocate(); //1. 分配对象的内存空间 +ctorInstance(memory); //2. 初始化对象 +instance = memory; //3. 设置instance指向刚分配的内存地址 +``` + +上面三个指令中,步骤2依赖步骤1,但是步骤3不依赖步骤2,所以JVM可能针对他们进行指令重拍序优化,重排后的指令如下: + +```c +memory =allocate(); //1. 分配对象的内存空间 +instance = memory; //3. 设置instance指向刚分配的内存地址 +ctorInstance(memory); //2. 初始化对象 +``` + +这样优化之后,内存的初始化被放到了instance分配内存地址的后面,这样的话当线程1执行步骤3这段赋值指令后,刚好有另外一个线程2进入getInstance方法判断instance不为null,这个时候线程2拿到的instance对应的内存其实还未初始化,这个时候拿去使用就会导致出错。 + +这里多说多说一点:为什么在synchronized上面多加了一次判断 + +**还不是因为synchronized比较笨重,锁了代码块嘛,多线程不能每次都要进来块中,岂不是都要发生阻塞等这class的锁呀,直接给他上面判断一下不为空就直接跳出去了。提高了性能哇。** + +其实这里也能体现出volatile的内存可见性,让其他线程对这个实例可见。 + +我们继续说volatile的内存可见性 + +扯一波JMM内存模型 + +![volatile保证内存可见性和避免重排-ouQfjW](https://gitee.com/dreamcater/blog-img/raw/master/uPic/volatile保证内存可见性和避免重排-ouQfjW.png) + +根据这个图如何回答总结JMM内存模型,看各位的造化了,理解讲出来即可。结合例子讲也可以 + +1. 先说结构 +2. 再说为什么是这样的结构,原因是什么? +3. 然后扯流程 +4. 撒花结束 + +## 底层结构 + +面试官:知道底层结构嘛? + +我:禁止重排是利用内存屏障来解决的,其实最根本的还是cpu的一个**lock**指令:**它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。** + +- 锁住内存 +- 任何读必须在写完成之后再执行 +- 使其它线程这个值的栈缓存失效 + +## 不能保证原子性 + +![volatile不能保证数据一致性](https://gitee.com/dreamcater/blog-img/raw/master/uPic/volatile不能保证数据一致性-iGuPEi.png) diff --git "a/Java/classify/thread/\346\255\273\351\224\201.md" "b/Java/classify/thread/\346\255\273\351\224\201.md" new file mode 100644 index 00000000..6452f806 --- /dev/null +++ "b/Java/classify/thread/\346\255\273\351\224\201.md" @@ -0,0 +1,69 @@ +# 死锁 + +面试官:知道死锁嘛? + +我:知道,那我就谈一下Java的死锁吧,其实原理都一样。 + +先看个例子: + +```java +public class Test { + private static Object res1 = new Object(); + private static Object res2 = new Object(); + + public static void main(String[] args) { + new Thread(() -> { + synchronized (res1) { + System.out.println(Thread.currentThread().getName() + " res1"); + // 延迟一下, 确保B拿到了res2 + try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } + synchronized (res2) { + System.out.println(Thread.currentThread().getName() + " res2"); + } + } + }, "ThreadA").start(); + + new Thread(() -> { + synchronized (res2) { + System.out.println(Thread.currentThread().getName() + " res2"); + // 延迟一下,确保A拿到了res1 + synchronized (res1) { + System.out.println(Thread.currentThread().getName() + " res1"); + } + } + }, "ThreadB").start(); + } +} +``` + +## 死锁的条件 + +所以:死锁的条件? + +- **互斥条件**:该资源任意一个时刻只由一个线程占用。(同一时刻,这个碗是我的,你不能碰) +- **请求与保持条件**:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(我拿着这个碗一直不放) +- **不剥夺条件**:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。(我碗中的饭没吃完,你不能抢,释放权是我自己的,我想什么时候放就什么时候放) +- **循环等待条件**:若干进程之间形成一种头尾相接的循环等待资源关系。(我拿了A碗,你拿了B碗,但是我还想要你的B碗,你还想我的A碗)比喻成棒棒糖也阔以。 + +面试官:所以解决死锁的办法是? + +我:好的,没毛病 + +- 预防死锁: + - **资源一次性分配**:破坏请求和保持条件。 + - **可剥夺资源**:当进程新申请的资源不满足时,释放已经分配的资源。破坏不可剥夺条件 + - **资源有序分配**:系统给进程编号,按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 +- 避免死锁:银行家算法:分配资源前先评估风险,会不会在分配后导致死锁。 即分配给一个进程资源的时候,该进程能否全部返还占用的资源。 +- 检测死锁:建立资源分配表和进程等待表。 +- 解除死锁:可以直接撤销死锁进程,或撤销代价最小的进程。 + +## 找死锁的步骤 + +所以:找死锁的步骤: + +1. 我们通过jps确定当前执行任务的进程号 + +2. 然后执行jstack命令查看当前进程堆栈信息 + +3. 然后将会看到`Found a total of 1 deadlock` + diff --git "a/Java/classify/thread/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205.md" "b/Java/classify/thread/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205.md" new file mode 100644 index 00000000..5afc3457 --- /dev/null +++ "b/Java/classify/thread/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205.md" @@ -0,0 +1,161 @@ + +### synchronized + +```java +public class Test { + + private final LinkedList lists = new LinkedList<>(); + + public synchronized void put(String s) { + while (lists.size() != 0) { // 用while怕有存在虚拟唤醒线程 + // 满了, 不生产了 + try { + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + lists.add(s); + System.out.println(Thread.currentThread().getName() + " " + lists.peekFirst()); + this.notifyAll(); // 这里可是通知所有被挂起的线程,包括其他的生产者线程 + } + + public synchronized void get() { + while (lists.size() == 0) { + try { + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + System.out.println(Thread.currentThread().getName() + " " + lists.removeFirst()); + this.notifyAll(); // 通知所有被wait挂起的线程 用notify可能就死锁了。 + } + + public static void main(String[] args) { + Test test = new Test(); + + // 启动消费者线程 + for (int i = 0; i < 5; i++) { + new Thread(test::get, "ConsA" + i).start(); + } + + // 启动生产者线程 + for (int i = 0; i < 5; i++) { + int tempI = i; + new Thread(() -> { + test.put("" + tempI); + }, "ProdA" + i).start(); + } + } +} +``` + +这里讲一下为什么用while,不用if? + +假如,此时队列元素为空,那么消费者肯定都挂起来了哈。在挂起前通知了生产者线程去生产,那么,生产者产了一个之后唤醒消费者,所有消费者醒了以后,就一个消费者抢到锁,开始消费,当消费过后释放锁,其他消费者线程的某一个抢到锁之后,从唤醒处走代码,如果是if,往下走取元素发现队列空的,直接抛异常。如果是while的话,还会继续判断队列是否为空,空就挂起。不会抛异常。 + +### ReentrantLock + +```java +public class Test { + + private LinkedList lists = new LinkedList<>(); + private Lock lock = new ReentrantLock(); + private Condition prod = lock.newCondition(); + private Condition cons = lock.newCondition(); + + public void put(String s) { + lock.lock(); + try { + // 1. 判断 + while (lists.size() != 0) { + // 只要队列有元素,就不生产了,就停会儿 + prod.await(); + } + // 2.干活 + lists.add(s); + System.out.println(Thread.currentThread().getName() + " " + lists.peekFirst()); + // 3. 通知 + cons.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public void get() { + lock.lock(); + try { + // 1. 判断 + while (lists.size() == 0) { + // 队列为空,消费者肯定等待呀 + cons.await(); + } + // 2.干活 + System.out.println(Thread.currentThread().getName() + " " + lists.removeFirst()); + // 3. 通知 + prod.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public static void main(String[] args) { + Test test = new Test(); + for (int i = 0; i < 5; i++) { + int tempI = i; + new Thread(() -> { + test.put(tempI + ""); + }, "ProdA" + i).start(); + } + for (int i = 0; i < 5; i++) { + new Thread(test::get, "ConsA" + i).start(); + } + } +} +``` + +### BlockingQueue + +```java +public class Test { + public static void main(String[] args) { + ArrayBlockingQueue queue = new ArrayBlockingQueue<>(10); + // 生产者 + Runnable product = () -> { + while (true) { + try { + String s = "生产者:" + Thread.currentThread().getName() + " "+ new Object(); + System.out.println(s); + queue.put(s); + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + new Thread(product, "p1").start(); + new Thread(product, "p2").start(); + // 消费者 + Runnable consume = () -> { + while (true) { + try { + Object o = queue.take(); + System.out.println("消费者:" + Thread.currentThread().getName() + " " + o); + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + new Thread(consume, "c1").start(); + new Thread(consume, "c2").start(); + } +} +``` + +利用 BlockingQueue 实现生产者消费者模式的代码。虽然代码非常简单,但实际上 ArrayBlockingQueue 已经在背后完成了很多工作,比如队列满了就去阻塞生产者线程,队列有空就去唤醒生产者线程等。 \ No newline at end of file diff --git "a/Java/classify/thread/\347\272\277\347\250\213\346\261\240.md" "b/Java/classify/thread/\347\272\277\347\250\213\346\261\240.md" new file mode 100644 index 00000000..508f0e80 --- /dev/null +++ "b/Java/classify/thread/\347\272\277\347\250\213\346\261\240.md" @@ -0,0 +1,213 @@ +# 线程池 + +> 面试必问高频 + +面试官:了解线程池嘛? + +我:了解 + +面试官:为啥子采用线程池?有啥子优点? + +我:我大概分为3点 + +- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 +- **提高响应速度**。当任务到达时,任务可以不需要要等到线程创建就能立即执行。 +- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +面试官:都了解哪些线程池? + +我:我暂时知道的一些的如:**newFixedThreadPool**(固定线程池)、**newSingleThreadExecutor**(单个线程的线程池)、**newCachedThreadPool**(缓存线程的线程池)、**newScheduledThreadPool**(带定时器的线程池),还有几个就不说了。 + +我就举点源码吧 + +**newFixedThreadPool**: + +```java +// core和max是一样的 +// blockQueue是无界阻塞队列 +// 嗯, 不好不好!!! +public static ExecutorService newFixedThreadPool(int nThreads) { + return new ThreadPoolExecutor(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); +} +``` + +**newSingleThreadExecutor** + +```java +// core和max无非都是1而已 +// blockQueue是无界阻塞队列 +// 嗯, 不好不好!!! +public static ExecutorService newSingleThreadExecutor() { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue())); +} +``` + +**newCachedThreadPool** + +```java +// core 0 +// max有点狠,不怕暴栈? +// 队列还是SynchronousQueue,还真怕 +public static ExecutorService newCachedThreadPool() { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue()); +} +``` + +## 线程池参数 + +面试官:讲一下线程池的参数? + +我:没问题,ThreadPoolExecutor源码走起: + +```java +public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) +``` + +- corePoolSize:核心线程数线程数定义了最小可以同时运行的线程数量 +- maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数 +- keepAliveTime:当线程数大于核心线程数时,多余的空闲线程存活的最长时间 +- TimeUnit:时间单位 +- BlockingQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中 +- ThreadFactory:线程工厂,用来创建线程,一般默认即可 +- RejectedExecutionHandler:拒绝策略 + +![线程池参数关系](https://gitee.com/dreamcater/blog-img/raw/master/uPic/线程池参数关系-JgjlWU.png) + +面试官:讲一下都有哪些拒绝策略 + +我:还好我提前准备 + +- AbortPolicy:抛出 `RejectedExecutionException`来拒绝新任务的处理。 +- CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。(说白了,谁管理任务的,谁就负责帮忙) +- DiscardPolicy:不处理新任务,直接丢弃掉。 +- DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。 + +面试官:线程池的线程数量怎么确定 + +我:分情况,一般来说... + +1. 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。 +2. 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。 +3. 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 + +## shutdown和shutdownNow + +面试官:shutdown和shutdownNow的区别 + +我:上源码: + +```java +// 等待所有线程执行任务完毕之后退出 +public void shutdown() { + final ReentrantLock mainLock = this.mainLock; + // 获取锁 + mainLock.lock(); + try { + // 检查 + checkShutdownAccess(); + // 设置状态 + advanceRunState(SHUTDOWN); + interruptIdleWorkers(); + onShutdown(); // hook for ScheduledThreadPoolExecutor + } finally { + mainLock.unlock(); + } + // 主要在于这里,根据状态来是否立马停止还是等线程执行完毕过后停止 + tryTerminate(); // 这里就不贴了 +} +// 和上面的差不多,立马中断所有线程,关闭线程池 +public List shutdownNow() { + List tasks; + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + checkShutdownAccess(); + // 设置状态 + advanceRunState(STOP); + interruptWorkers(); + tasks = drainQueue(); + } finally { + mainLock.unlock(); + } + tryTerminate(); + return tasks; +} +``` + +## execute和submit的区别 + + +面试官:execute和submit的区别 + +我:心累 + +```java +// execute +public void execute(Runnable command) { + if (command == null) + throw new NullPointerException(); + /* + * Proceed in 3 steps: + * + * 1. If fewer than corePoolSize threads are running, try to + * start a new thread with the given command as its first + * task. The call to addWorker atomically checks runState and + * workerCount, and so prevents false alarms that would add + * threads when it shouldn't, by returning false. + * + * 2. If a task can be successfully queued, then we still need + * to double-check whether we should have added a thread + * (because existing ones died since last checking) or that + * the pool shut down since entry into this method. So we + * recheck state and if necessary roll back the enqueuing if + * stopped, or start a new thread if there are none. + * + * 3. If we cannot queue task, then we try to add a new + * thread. If it fails, we know we are shut down or saturated + * and so reject the task. + */ + // 按还是翻译了一下: + // 1.如果正在运行的线程少于corePoolSize线程,请尝试使用给定命令作为其第一个任务来启动新线程。 + // 对addWorker的调用从原子上检查runState和workerCount,从而通过返回false来防止在不应该添加线程的情况下发出虚假警报。 + // 2.如果一个任务可以成功排队,那么我们仍然需要仔细检查是否应该添加一个线程(因为现有线程自上次检查后就死掉了)或该池自进入该方法后就关闭了。 + // 因此,我们重新检查状态,并在必要时回滚排队(如果已停止),或者在没有线程时启动一个新线程。 + // 3.如果我们无法将任务排队,则尝试添加一个新线程。 + // 如果失败,我们知道我们已关闭或已饱和,因此拒绝该任务。 + // 总结:说白了,就是上面的流程图 + int c = ctl.get(); + if (workerCountOf(c) < corePoolSize) { + if (addWorker(command, true)) + return; + c = ctl.get(); + } + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + if (! isRunning(recheck) && remove(command)) + reject(command); + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + else if (!addWorker(command, false)) + reject(command); +} + +//不过传递的参数,Runnable,那么就意味着没有返回值 +// 简单看一下submit吧。 +// 不必多说了 + Future submit(Callable task); +Future submit(Runnable task); +``` + diff --git "a/Java/classify/thread/\347\272\277\347\250\213\347\232\204\345\210\233\345\273\272\346\226\271\345\274\217.md" "b/Java/classify/thread/\347\272\277\347\250\213\347\232\204\345\210\233\345\273\272\346\226\271\345\274\217.md" new file mode 100644 index 00000000..d57d40fc --- /dev/null +++ "b/Java/classify/thread/\347\272\277\347\250\213\347\232\204\345\210\233\345\273\272\346\226\271\345\274\217.md" @@ -0,0 +1,173 @@ + +# 线程的创建方式 +面试官:线程的创建方式有哪些? + +我:我目前知道4种:分别如下: + +- Thread + + +```java +public class Test extents Thread { + public void run() { + // 重写Thread的run方法 + System.out.println("dream"); + } + + public static void main(String[] args) { + new Test().start(); + } +} +``` + +- Runnable + +```java +public class Test { + public static void main(String[] args) { + new Thread(() -> { + System.out.println("dream"); + }).start(); + } +} +``` + +- Callable + +```java +public class Test { + public static void main(String[] args) { + // FutureTask 构造方法包装了Callable和Runnable。 + FutureTask task = new FutureTask<>(() -> { + System.out.println("dream"); + return 0; + }); + new Thread(task).start(); + } +} + +``` + +- 线程池 + +```java + +public class Test { + public static void main(String[] args) { + ExecutorService threadPool = Executors.newFixedThreadPool(1); + threadPool.submit(() -> { + System.out.println("dream"); + }); + threadPool.shutdown(); + } +} +``` + +面试官:Runnable和Callable有啥区别? + +我:那得先看源码咯 + +请看 + +```java +@FunctionalInterface +public interface Runnable { + /** + * 被线程执行,没有返回值也无法抛出异常 + */ + public abstract void run(); +} + +@FunctionalInterface +public interface Callable { + /** + * 计算结果,或在无法这样做时抛出异常。 + * @return 计算得出的结果 + * @throws 如果无法计算结果,则抛出异常 + */ + V call() throws Exception; +} +``` + +1. Runnable没有返回值并且无法抛出异常 +2. 不巧,我Callable可以做到你不能做到的 + +线程池的源码后边讲,讲东西,要结合源码和例子讲!!! + + + +> 补充一下主线程等待子线程的两种方式 + +# 主线程等待子线程的两种方式 + +## sleep +> 这个不常用,但是简单一些 +```java +public class Test { + void m() { + System.out.println(Thread.currentThread().getName()); + } + + public static void main(String[] args) { + Test t1 = new Test(); + for (int i = 0; i < 5; i++) { + new Thread(t1::m, "Thread " + i).start(); + } + try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } + System.out.println("main thread"); + } +} +``` + +## join + +```java +public class Test { + void m() { + System.out.println(Thread.currentThread().getName()); + } + + public static void main(String[] args) { + Test t1 = new Test(); + ArrayList threads = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + threads.add(new Thread(t1::m, "Thread " + i)); + } + threads.forEach(o -> o.start()); + threads.forEach(o -> { + try { + o.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + System.out.println("main thread"); + } +} +``` + +## CountDownLatch +```java +public class Test { + private CountDownLatch latch; + + public Test(CountDownLatch latch) { + this.latch = latch; + } + + void m() { + System.out.println(Thread.currentThread().getName()); + latch.countDown(); + } + + public static void main(String[] args) throws InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(5); + Test t1 = new Test(countDownLatch); + for (int i = 0; i < 5; i++) { + new Thread(t1::m, "Thread " + i).start(); + } + countDownLatch.await(); + System.out.println("main thread"); + } +} +``` diff --git "a/Java/classify/thread/\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213.md" "b/Java/classify/thread/\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213.md" new file mode 100644 index 00000000..132a3272 --- /dev/null +++ "b/Java/classify/thread/\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213.md" @@ -0,0 +1,122 @@ +# 进程和线程相关问题 + +## 什么是线程安全? +> 我的理解是:多个线程交替执行,本身是没有问题的,但是如果访问共享资源,结果可能会出现问题,于是就出现了线程不安全的问题。 + +1. 访问共享变量或资源 +2. 依赖时序的操作 +3. 不同数据之间存在绑定关系 +4. 对方没有声明自己是线程安全的 + +## 上下文切换 + +多线程编程中一般**线程的个数都大于 CPU 核心的个数**,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配**时间片并轮转**的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 + +实际上就是**任务从保存到再加载的过程就是一次上下文切换**。 + +## 并行与并发 + +- 并行:**单位时间内**,多个任务同时执行。 +- 并发:**同一时间段**,多个任务都在执行 (单位时间内不一定同时执行); + +## 进程与线程 + +> 面试官挺喜欢问这个问题的,他能引出来他想要问你的知识点。 + +面试官:说一下线程与进程的区别 + +我:好的,如下: + +- **进程是程序的一次执行过程,是系统运行程序的基本单位** + +- **线程是一个比进程更小的执行单位** + +- 一个进程在其执行的过程中可以产生**多个线程** + +- 与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程 + +举例子:比如,main函数启动了JVM进程,同时main就是其中的线程,并且启动了JVM进程,那么还有垃圾回收等线程。 + +或者这样的例子:做个简单的比喻:进程=火车,线程=车厢 + +- 线程在进程下行进(单纯的车厢无法运行) +- 一个进程可以包含多个线程(一辆火车可以有多个车厢) +- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘) +- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易) +- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源) +- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢) +- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上) +- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁" +- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量” + +面试官:直接调用Thread的run方法不行吗? + +我:肯定不行的,通过run方法启动线程其实就是调用一个类中的方法,**当作普通的方法的方式调用**。并没有创建一个线程,**程序中依旧只有一个主线程**,必须等到run()方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有达到写线程的目的。如果是start方法,效果就不一样了。 + +## start源码 + +首先看一下start源码: + +```java +public synchronized void start() { + // 等于0意味着可以是线程的新建状态 + if (threadStatus != 0) + throw new IllegalThreadStateException(); + // 将该线程加入线程组 + group.add(this); + boolean started = false; + try { + start0(); // 核心, 本地方法,新建线程被。 + started = true; + } finally { + try { + if (!started) { + group.threadStartFailed(this); + } + } catch (Throwable ignore) { + } + } +} +``` + +当得到CPU的时间片后就会执行其中的**run()方法**。这个run()方法包含了要执行的这个线程的内容,run()方法运行结束,此线程也就终止了。 + +```java +@Override +public void run() { + if (target != null) { + target.run(); // target是:private Runnable target; Runnable接口 + } +} +// Runnable: +public abstract void run();//抽象方法 +``` + +面试官:线程的生命周期,讲一下。 + +我:ok,看图说话 + +![](https://www.pdai.tech/_images/pics/ace830df-9919-48ca-91b5-60b193f593d2.png) + +- 线程创建之后它将处于`New`(新建)状态,调用 `start()` 方法后开始运行,线程这时候处于 `READY`(可运行,也叫做就绪) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 `RUNNING`(运行) 状态。 +- 当线程执行 `wait()`方法之后,线程进入 `WAITING`(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 `TIME_WAITING`(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 `TIMED WAITING` 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。 +- 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 `BLOCKED`(阻塞)状态。 +- 线程在执行 Runnable 的` run() `方法结束之后将会进入到 `TERMINATED`(终止) 状态。 + +## wait/notify和sleep + +面试官:wait/notify 和 sleep 方法的异同? + +我:ok + +相同点: + +1. 它们都可以让**线程阻塞**。 +2. 它们都可以响应 **interrupt** 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。 + +不同点: + +1. wait 方法必须在 **synchronized** 保护的代码中使用,而 sleep 方法并没有这个要求。 +2. 在同步代码中**执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁**。 +3. sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。 +4. **wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法**。 diff --git a/Java/crazy/Dubbo.md b/Java/crazy/Dubbo.md new file mode 100644 index 00000000..191790c3 --- /dev/null +++ b/Java/crazy/Dubbo.md @@ -0,0 +1,210 @@ +> 微服务和分布式算是一种潮流和趋势,项目中要到了微服务还是准备准备面试的问题吧... + +## 大纲图 + +![Dubbo常见面试](http://media.dreamcat.ink/uPic/Dubbo常见面试.png) + +### Dubbo和SpringCloud的区别 + +- **底层**:`Dubbo`底层是使用Netty的NIO框架,基于TCP协议传输,使用Hession序列化完成RPC通信;`SpringCloud`是基于HTTP协议+REST接口调用远程过程的通信,HTTP请求会有更大的报文,占的带宽也会更多。但是REST相比RPC更为灵活,不存在代码级别的强依赖。 +- **集成**:springcloud相关组件多,有自己得注册中心网关等,集成方便,Dubbo需要自己额外去集成。 +- **定位**:Dubbo是SOA时代的产物,它的关注点主要在于**服务的调用,流量分发、流量监控和熔断**。而SpringCloud诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了Spirng、SpirngBoot 的优势之上,两个框架在开始目标就不一致,Dubbo定位服务治理、SpirngCloud是一个生态。因此可以大胆地判断,Dubbo未来会在服务治理方面更为出色,而Spring Cloud在微服务治理上面无人能敌。 + +### 什么是Dubbo? + +Dubbo是一款**高性能**、**轻量级**的开源Java RPC 框架,它提供了三大核心能力:**面向接口的远程方法调用**,**智能容错和负载均衡**,以及**服务自动注册和发现**。简单来说 Dubbo 是一个**分布式服务框架**,致力于提供**高性能和透明化的RPC远程服务调用方案**,以及**SOA服务治理方案。** + +### 什么是RPC?原理是什么? + +#### RPC + +RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务 A、B 部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP请求 当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC 的出现就是为了解决这个问题。 + +#### RPC原理 + +![RPC原理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/37345851.jpg) + +1. 服务消费方(client)调用以本地调用方式调用服务; +2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体; +3. client stub找到服务地址,并将消息发送到服务端; +4. server stub收到消息后进行解码; +5. server stub根据解码结果调用本地的服务; +6. 本地服务执行并将结果返回给server stub; +7. server stub将返回结果打包成消息并发送至消费方; +8. client stub接收到消息,并进行解码; +9. 服务消费方得到最终结果。 + +![RPC时序图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/32527396.jpg) + +### 为什么要用Dubbo? + +Dubbo 的诞生和 **SOA 分布式架构**的流行有着莫大的关系。**SOA 面向服务的架构**(Service Oriented Architecture),也就是把工程按照业务逻辑拆分成**服务层、表现层**两个工程。**服务层中包含业务逻辑,只需要对外提供服务即可**。**表现层只需要处理和页面的交互**,业务逻辑都是调用服务层的服务来实现。SOA架构中有两个主要角色:**服务提供者**(Provider)**和服务使用者**(Consumer)。 + +我觉得主要可以从 Dubbo 提供的下面四点特性来说为什么要用 Dubbo: + +1. **负载均衡**——同一个服务部署在不同的机器时该调用那一台机器上的服务。 +2. **服务调用链路生成**——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。 +3. **服务访问压力以及时长统计、资源调度和治理**——基于访问压力实时管理集群容量,提高集群利用率。 +4. **服务降级**——某个服务挂掉之后调用备用服务。 + +另外,Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。不过,由于 Spring Cloud 在微服务中应用更加广泛,所以,我觉得一般我们提 Dubbo 的话,大部分是分布式系统的情况。 + +### 什么是分布式? + +分布式或者说 SOA 分布式重要的就是**面向服务**,说简单的分布式就是我们把**整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能**。比如电商系统可以简单地拆分成订单系统、商品系统、登录系统等等,拆分之后的每个服务可以**部署在不同的机器上**,如果某一个服务的访问量比较大的话也可以将这个服务同时部署在多台机器上。 + +### 为什么要分布式? + +- 从开发角度来讲**单体应用的代码都集中在一起**,而**分布式系统的代码根据业务被拆分**。所以,每个团队可以负责一个服务的开发,这样提升了开发效率。另外,代码根据业务拆分之后更加便于维护和扩展。 +- 系统拆分成分布式之后不光便于系统扩展和维护,更能提高整个系统的性能。把整个系统拆分成不同的服务/系统,然后每个服务/系统 单独部署在一台服务器上,是不是很大程度上提高了系统性能呢? + +### Dubbo的架构图解 + +![dubbo架构图解](http://media.dreamcat.ink/uPic/dubbo架构图解.png) + +- **Provider:** 暴露服务的服务提供方 +- **Consumer:** 调用远程服务的服务消费方 +- **Registry:** 服务注册与发现的注册中心 +- **Monitor:** 统计服务的调用次数和调用时间的监控中心 +- **Container:** 服务运行容器 + +**调用关系说明**: + +1. 服务容器负责启动,加载,运行服务提供者。 +2. 服务提供者在启动时,向注册中心注册自己提供的服务。 +3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 +4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 + +**各个组件总结**: + +- **注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小** +- **监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示** +- **注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外** +- **注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者** +- **注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表** +- **注册中心和监控中心都是可选的,服务消费者可以直连服务提供者** +- **服务提供者无状态,任意一台宕掉后,不影响使用** +- **服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复** + +### Dubbo工作原理 + +![参考-JavaGuide-工作原理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-26/64702923.jpg) + +图中从下至上分为十层,各层均为**单向依赖**,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。 + +- 第一层:**service层**,接口层,给服务提供者和消费者来实现的 +- 第二层:**config层**,配置层,主要是对dubbo进行各种配置的 +- 第三层:**proxy层**,服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton +- 第四层:**registry层**,服务注册层,负责服务的注册与发现 +- 第五层:**cluster层**,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务 +- 第六层:**monitor层**,监控层,对rpc接口的调用次数和调用时间进行监控 +- 第七层:**protocol层**,远程调用层,封装rpc调用 +- 第八层:**exchange层**,信息交换层,封装请求响应模式,同步转异步 +- 第九层:**transport层**,网络传输层,抽象mina和netty为统一接口 +- 第十层:**serialize层**,数据序列化层,网络传输需要 + +### Dubbo的负载均衡 + +#### 什么是负载均衡 + +> 负载均衡改善了跨多个计算资源(例如计算机,计算机集群,网络链接,中央处理单元或磁盘驱动的的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间,并避免任何单个资源的过载。使用具有负载平衡而不是单个组件的多个组件可以通过冗余提高可靠性和可用性。负载平衡通常涉及专用软件或硬件。------ 够强硬 + +比如我们的系统中的某个服务的**访问量特别大**,我们将这个服务部署在了**多台服务器**上,当客户端发起请求的时候,**多台服务器都可以处理这个请求**。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。 + +#### Dubbo的负载均衡 + +在集群负载均衡时,Dubbo 提供了多种均衡策略,默认为 `random` 随机调用。可以自行扩展负载均衡策略 + +##### Random LoadBalance(默认,基于权重的随机负载均衡机制) + +- **随机,按权重设置随机概率。** +- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 + +![参考-JavaGuide-Random LoadBalance](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-7/77722327.jpg) + +##### RoundRobin LoadBalance(不推荐,基于权重的轮询负载均衡机制) + +- 轮循,按公约后的权重设置轮循比率。 +- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 + +![参考-JavaGuide-RoundRobin](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-7/97933247.jpg) + +##### LeastAcive LoadBalace + +- 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。 +- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 + +##### ConsistentHash LodaBalance + +- **一致性 Hash,相同参数的请求总是发到同一提供者。(如果你需要的不是随机负载均衡,是要一类请求都到一个节点,那就走这个一致性hash策略。)** +- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 + +### Dubbo配置方式 + +#### xml配置方式 + +**服务端服务级别**: + +```xml + + +``` + +**客户端服务级别**: + +```xml + + +``` + +**服务端方法级别**: + +```xml + + + + +``` + +**客户端方法级别**: + +```xml + + + +``` + +#### 注解配置方式: + +服务级别配置: + +```java +@Service +.... +``` + +消费注解配置: + +```java +@Reference(loadbalance = "roundrobin") +``` + +### zookeeper宕机与dubbo直连的情况 + +在实际生产中,假如zookeeper注册中心宕掉,一段时间内服务消费方还是能够调用提供方的服务的,实际上它使用的本地缓存进行通讯,这只是dubbo健壮性的一种体现。 + +**dubbo的健壮性表现:** + +1. 监控中心宕掉不影响使用,只是丢失部分采样数据 +2. 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务 +3. 注册中心对等集群,任意一台宕掉后,将自动切换到另一台 +4. 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯 +5. 服务提供者无状态,任意一台宕掉后,不影响使用 +6. 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复 + +我们前面提到过:注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。所以,我们可以完全可以绕过注册中心——采用 **dubbo 直连** ,即在服务消费方配置服务提供方的位置信息。 + +> 创作不易哇,觉得有帮助的话,给个小小的star呗。[https://github.com/DreamCats/JavaBooks](https://github.com/DreamCats/JavaBooks)😁😁😁 +> [开源的微服务班车预约系统](https://github.com/DreamCats/school-bus) \ No newline at end of file diff --git a/Java/crazy/JVM.md b/Java/crazy/JVM.md new file mode 100644 index 00000000..759b730c --- /dev/null +++ b/Java/crazy/JVM.md @@ -0,0 +1,590 @@ +> 个人感觉JVM这一块,了解和背的知识点挺多,代码并不是特别多,主要是后期调优,需要大量的经验罢了。不过JVM这一块一定要深刻理解。 + +## 大纲图 + +![JVM面试](http://media.dreamcat.ink/uPic/JVM面试.png) + +### 类文件结构 + +#### 概述 + +在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 + +**可以说`.class`文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。** + +#### 类文件总结构 + +```java +ClassFile { + u4 magic; // 魔数 Class 文件的标志 这都没话可说的 + u2 minor_version;//Class 的小版本号 + u2 major_version;//Class 的大版本号 + u2 constant_pool_count;//常量池的数量 + cp_info constant_pool[constant_pool_count-1];//常量池 待会说 + u2 access_flags;//Class 的访问标记 + u2 this_class;//当前类 + u2 super_class;//父类 + u2 interfaces_count;//接口 + u2 interfaces[interfaces_count];//一个类可以实现多个接口 + u2 fields_count;//Class 文件的字段属性 + field_info fields[fields_count];//一个类会可以有个字段 描述接口或类中声明的变量 但不包括在方法内部声明的局部变量. + u2 methods_count;//Class 文件的方法数量 + method_info methods[methods_count];//一个类可以有个多个方法 和字段表性质一样 + u2 attributes_count;//此类的属性表中的属性数 + attribute_info attributes[attributes_count];//属性表集合 +} +``` + +#### 静态常量池 + +常量池主要存放两大常量:**字面量和符号引用**。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量: + +- 类和接口的全限定名 +- 字段的名称和描述符 +- 方法的名称和描述符 + +常量池的好处:常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。 + +举个例子:字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。 + +1. 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。 +2. 节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。 + +静态常量池用于存放编译期生成的各种**字面量和符号引用**,这部分内容将在类加载后存放到方法区的**运行时常量池**中。其中符号引用其实引用的就是常量池里面的字符串,但**符号引用不是直接存储字符串,而是存储字符串在常量池里的索引**。 + +当Class文件被加载完成后,java虚拟机会将静态常量池里的内容转移到运行时常量池里,在静态常量池的**符号引用有一部分是会被转变为直接引用**的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。 + +#### 运行时常量池 + +运行时常量池(Runtime Constant Pool)是**方法区**的一部分。对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。 + +运行时常量池还有个更重要的的特征:**动态性**。Java要求,编译期的常量池的内容可以进入运行时常量池,运行时产生的常量也可以放入池中。常用的是String类的intern()方法。 + +既然运行时常量池是方法区的一部分自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出**OutOfMemoryError**异常。 + +#### 字符串常量池 + +字符串常量池存在运行时常量池之中(在JDK7之前存在运行时常量池之中,在JDK7已经将其转移到堆中)。 + +字符串常量池的存在使JVM提高了性能和减少了内存开销。 + +1. 每当我们使用字面量(String s=“1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。 +2. 每当我们使用关键字new(String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s。 + +#### 三者关系 + +JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。 + +静态常量池用于存放编译期生成的各种字面量和符号引用,而当类加载到内存中后,jvm就会将静态常量池中的内容存放到运行时常量池中。而字符串常量池存的是引用值,其存在于运行时常量池之中。 + +#### 版本变化 + +##### 1.6 + +- 静态常量池在Class文件中。 + +- 运行时常量池在Perm Gen区(也就是方法区)中。(所谓的方法区是在Java堆的一个逻辑部分,为了与Java堆区别开来,也称其为非堆(Non-Heap),那么Perm Gen(永久代)区也被视为方法区的一种实现。) + +- 字符串常量池在运行时常量池中。 + +##### 1.7 + +- 静态常量池在Class文件中。 +- 运行时常量池依然在Perm Gen区(也就是方法区)中。在JDK7版本中,永久代的转移工作就已经开始了,将譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。但是运行时常量池依然还存在,只是很多内容被转移,其只存着这些被转移的引用。网上流传的一些测试运行时常量池转移的方式或者代码,其实是对字符串常量池转移的测试。 +- 字符串常量池被分配到了Java堆的主要部分(known as the young and old generations)。也就是字符串常量池从运行时常量池分离出来了。 + +##### 1.8 + +- 静态常量池在Class文件中。 +- JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。同时永久代被移除,以元空间代替。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。其主要用于存放一些元数据。 +- 字符串常量池存在于Java堆中。 + +### 类加载过程 + +系统加载 Class 类型的文件主要三步:**加载->连接->初始化**。连接过程又可分为三步:**验证->准备->解析**。 + +![类加载过程](http://media.dreamcat.ink/uPic/类加载过程.png) + +#### 加载 + +类加载过程的第一步,主要完成下面3件事情: + +1. 通过全类名获取定义此类的二进制字节流 +2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构 +3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口 + +加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。 + +#### 验证 + +- 文件格式验证:主要验证Class文件是否规范等。 +- 元数据验证:对字节码描述的信息语义分析等。 +- 字节码验证:确保语义是ok的。 +- 符号引用验证:确保解析动作能执行。 + +#### 准备 + +**准备阶段是正式为类变量分配内存并设置类变量初始值的阶段**,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: + +1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。 +2. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了`public static int value=111` ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会复制)。特殊情况:比如给 value 变量加上了 fianl 关键字`public static final int value=111` ,那么准备阶段 value 的值就被复制为 111。 + +#### 解析 + +解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。 + +符号引用就是一组符号来描述目标,可以是任何字面量。**直接引用**就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方发表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。 + +综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。 + +#### 初始化 + +初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 ` ()`方法的过程。 + +对于`()` 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 `()` 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。 + +对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化: + +1. 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 +2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化。 +3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。 +4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。 +5. 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化。 + +### 类加载器 + +JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自`java.lang.ClassLoader`: + +1. **BootstrapClassLoader(启动类加载器)** :最顶层的加载类,由C++实现,负责加载 `%JAVA_HOME%/lib`目录下的jar包和类或者或被 `-Xbootclasspath`参数指定的路径中的所有类。 +2. **ExtensionClassLoader(扩展类加载器)** :主要负责加载目录 `%JRE_HOME%/lib/ext` 目录下的jar包和类,或被 `java.ext.dirs` 系统变量所指定的路径下的jar包。 +3. **AppClassLoader(应用程序类加载器)** :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。 + +#### 双亲委派 + +每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 **双亲委派模型** 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 `loadClass()` 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 `BootstrapClassLoader` 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 `BootstrapClassLoader` 作为父类加载器。 + +![ClassLoader](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/classloader_WPS图片.png) + +```java +public class ClassLoaderDemo { + public static void main(String[] args) { + System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader()); + System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent()); + System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent()); + } +} +// ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2 +// The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586 +// The GrandParent of ClassLodarDemo's ClassLoader is null +``` + +`AppClassLoader`的父类加载器为`ExtClassLoader` +`ExtClassLoader`的父类加载器为null,**null并不代表`ExtClassLoader`没有父类加载器,而是 `BootstrapClassLoader`** 。 + +其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 Mother ClassLoader 和一个 Father ClassLoader 。另外,类加载器之间的“父子”关系也不是通过继承来体现的,是由“优先级”来决定。 + +**双亲委派的好处**: + +双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 `java.lang.Object` 类的话,那么程序运行的时候,系统就会出现多个不同的 `Object` 类。 + +#### 自定义类加载器 + +除了 `BootstrapClassLoader` 其他类加载器均由 Java 实现且全部继承自`java.lang.ClassLoader`。如果我们要自定义自己的类加载器,很明显需要继承 `ClassLoader`。 + +### jvm内存(运行时区域) + +#### 1.8之前 + +![内存区域](http://media.dreamcat.ink/uPic/JVM内存模型-1.8之前.png) + +#### 1.8之后 + +![参考-JavaGuide-内存区域](http://media.dreamcat.ink/uPic/JVM内存模型-1.8.png) + +按照1.8进行总结 + +**线程私有的:** + +- 程序计数器 +- 虚拟机栈 +- 本地方法栈 + +**线程共享的:** + +- 堆 +- 直接内存 (非运行时数据区的一部分) + +#### 程序计数器 + +说白了就是 + +1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 +2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 + +**注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。** + +#### Java虚拟机栈 + +与程序计数器一样,Java 虚拟机栈也是线程**私有**的,它的生命周期和线程相同,描述的是 Java **方法执行的内存模型**,每次方法调用的数据都是通过栈传递的。 + +**Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。** + +- 局部变量表 +- 操作数栈 + - 8大基本类型 + - 对象引用:可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置 +- 动态链接 +- 方法出口 + +**Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。** + +- **StackOverFlowError:** 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。 +- **OutOfMemoryError:** 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。 + +Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。 + +Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。 + +#### 本地方法栈 + +和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 其他和Java虚拟机差不多的 + +#### 堆 + +Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。** + +Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。** + +![](http://media.dreamcat.ink/uPic/堆内存分区.png) + +上图所示的 eden 区、s0("From") 区、s1("To") 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1("To"),并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。经过这次GC后,Eden区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到年老代中。 + +#### 方法区 + +方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 **Non-Heap(非堆)**,目的应该是与 Java 堆区分开来。 + +JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。 + +#### 运行时常量池 + +运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)以及符号引用替换为直接引用。JDK1.8 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。 + +#### 直接内存 + +直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。 + +#### 对象创建 + +![对象创建的过程](http://media.dreamcat.ink/uPic/Java创建对象的过程.png) + +1. 类加载检查,虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 + +2. 分配内存,在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。 + + 1. **内存分配的两种方式:(补充内容,需要掌握)** + + - 选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的 + - ![参考-JavaGuide-内存分配](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E7%9A%84%E4%B8%A4%E7%A7%8D%E6%96%B9%E5%BC%8F.png) + + 2. **内存分配并发问题(补充内容,需要掌握)** + - **CAS+失败重试:** CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。**虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。** + - **TLAB:** 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配 + +3. 初始化零值,内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 + +4. 设置对象头,初始化零值完成之后,**虚拟机要对对象进行必要的设置**,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 **这些信息存放在对象头中。** 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 + +5. 执行init方法,在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,`` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 + +##### 内存布局 + +在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:**对象头**、**实例数据**和**对齐填充**。 + +**Hotspot 虚拟机的对象头包括两部分信息**,**第一部分用于存储对象自身的自身运行时数据**(哈希码、GC 分代年龄、锁状态标志等等),**另一部分是类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 + +**实例数据部分是对象真正存储的有效信息**,也是在程序中所定义的各种类型的字段内容。 + +**对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。** 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 + +##### 对象的访问方式 + +建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有**①使用句柄**和**②直接指针**两种: + +###### 使用句柄 + +如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; + +![参考-JavaGuide-句柄](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/%E5%AF%B9%E8%B1%A1%E7%9A%84%E8%AE%BF%E9%97%AE%E5%AE%9A%E4%BD%8D-%E4%BD%BF%E7%94%A8%E5%8F%A5%E6%9F%84.png) + +###### 直接指针 + +如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 + +![参考-JavaGuide-直接指针](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/%E5%AF%B9%E8%B1%A1%E7%9A%84%E8%AE%BF%E9%97%AE%E5%AE%9A%E4%BD%8D-%E7%9B%B4%E6%8E%A5%E6%8C%87%E9%92%88.png) + +这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是**稳定**的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 **reference 本身不需要修改**。使用直接指针访问方式最大的好处就是**速度快**,它节省了一次指针定位的时间开销。 + +### 垃圾回收 + +Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。** + +#### 对象优先在Eden区分配 + +目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 + +大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.下面我们来进行实际测试以下。 + +在测试之前我们先来看看 **Minor GC 和 Full GC 有什么不同呢?** + +- **新生代 GC(Minor GC)**:指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。 +- **老年代 GC(Major GC/Full GC)**:指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。 + +#### 大对象直接进入老年代 + +大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。 + +**为什么要这样呢?**为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。 + +#### 长期存活的对象进入老年代 + +如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 + +#### 如何判断对象死亡 + +![参考-JavaGuide-对象死亡](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/11034259.jpg) + +##### 引用计数法 + +给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。 + +**这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。** 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。 + +```java +public class ReferenceCountingGc { + Object instance = null; + public static void main(String[] args) { + ReferenceCountingGc objA = new ReferenceCountingGc(); + ReferenceCountingGc objB = new ReferenceCountingGc(); + objA.instance = objB; // 循环引用 + objB.instance = objA; // 循环引用 + objA = null; + objB = null; + } +} +``` + +##### 可达性分析 + +这个算法的基本思想就是通过一系列的称为 **“GC Roots”** 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。 + +![参考-JavaGuide-可达性分析算法 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/72762049.jpg) + +哪些可以作为GC Roots的根 + +- 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中应用的对象。 +- 方法区中的类静态属性引用的对象 +- 方法区中常量引用的对象 +- 本地方法栈中JNI(native方法)引用的对象 + +在这里就聊一下引用 + +##### 四大引用 + +**1.强引用(StrongReference)** + +以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于**必不可少的生活用品**,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 + +**2.软引用(SoftReference)** + +如果一个对象只具有软引用,那就类似于**可有可无的生活用品**。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 + +软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。 + +**3.弱引用(WeakReference)** + +如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 + +弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 + +**4.虚引用(PhantomReference)** + +"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 + +**虚引用主要用来跟踪对象被垃圾回收的活动**。 + +**虚引用与软引用和弱引用的一个区别在于:** 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 + +##### 不可达的对象并非“非死不可” + +即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。 + +被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。 + +##### 如何判断一个常量是废弃常量 + +运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢? + +假如在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池。 + +##### 如何判断一个类是无用的类 + +判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 **“无用的类”** : + +- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。 +- 加载该类的 ClassLoader 已经被回收。 +- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 + +虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。 + +#### 垃圾回收算法 + +##### 标记-清除算法 + +该算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题: + +1. 效率问题 +2. 空间问题(标记清除后会产生大量不连续的碎片) + +公众号 + +##### 标记-整理算法 + +根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。 + +![标记-整理](https://www.pdai.tech/_images/pics/902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg) + +##### 复制算法 + +为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。 + +公众号 + +#### 分代收集算法 + +**比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。** + +### 垃圾收集器 + +- Serial收集器 +- ParNew收集器 +- Parallel Scavenge收集器 +- CMS收集器 +- G1收集器 + +#### Serial收集器 + +Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 **“单线程”** 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( **"Stop The World"** ),直到它收集结束。 + +**新生代采用复制算法,老年代采用标记-整理算法。** + +![ Serial 收集器 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/46873026.jpg) + +虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。 + +但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它**简单而高效(与其他收集器的单线程相比)**。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。 + +#### ParNew收集器 + +**ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。** + + **新生代采用复制算法,老年代采用标记-整理算法。** + +![ParNew 收集器 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/22018368.jpg) + +它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。 + +**并行和并发概念补充:** + +- **并行(Parallel)** :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 +- **并发(Concurrent)**:指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。 + +#### Parallel Scavenge收集器 + +Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它看上去几乎和ParNew都一样。 + +**Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。** Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在困难的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。 + +**新生代采用复制算法,老年代采用标记-整理算法。** + +#### Serial Old 收集器 + +**Serial 收集器的老年代版本**,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。 + +#### Parallel Old 收集器 + + **Parallel Scavenge 收集器的老年代版本**。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。 + +#### CMS收集器 + +**CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。** + +**CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。** + +从名字中的**Mark Sweep**这两个词可以看出,CMS 收集器是一种 **“标记-清除”算法**实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤: + +- **初始标记:** 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; +- **并发标记:** 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 +- **重新标记:** 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 +- **并发清除:** 开启用户线程,同时 GC 线程开始对为标记的区域做清扫。 + +![CMS 垃圾收集器 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/82825079.jpg) + +从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:**并发收集、低停顿**。但是它有下面三个明显的缺点: + +- **对 CPU 资源敏感;** +- **无法处理浮动垃圾;** +- **它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。** + +#### G1收集器 + +**G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.** + +被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点: + +- **并行与并发**:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。 +- **分代收集**:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 +- **空间整合**:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。 +- **可预测的停顿**:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。 + +G1 收集器的运作大致分为以下几个步骤: + +- **初始标记** +- **并发标记** +- **最终标记** +- **筛选回收** + +**G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)**。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 GF 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。 + +#### 如何选择垃圾回收器 + +- 单CPU或小内存,单机内存 + + -XX:+UseSerialGC + +- 多CPU,需要最大吞吐量,如后台计算型应用 + + -XX:+UseParallelGC -XX:+UseParallelOldGC + +- 多CPU,最求低停顿时间,需快速相应,如互联网应用 + + -XX:+ParNewGC -XX:+UseConcMarkSweepGC + +| 参数 | 新生代垃圾收集器 | 新生代算法 | 老年代垃圾收集器 | 老年代算法 | +| --------------------------------- | ------------------ | ------------------ | ------------------------------------------------------------ | ---------- | +| UseSerialGC | SerialGC | 复制 | SerialOldGC | 标整 | +| UseParNewGC | ParNew | 复制 | SerialOldGC | 标整 | +| UseParallelGC
UseParallelOldGC | Parallel[Scavenge] | 复制 | Parallel Old | 标整 | +| UseConcMarkSweepGC | ParNew | 复制 | CMS+Serial Old的收集器组合(Serial Old 作为CMS出错的后备收集器) | 标清 | +| UseG1GC | G1整体上采用标整 | 局部是通过复制算法 | | | + +### JVM调优参数 + +> 这里只介绍一些常用的,还有更多的后续讲解... + +```shell +-Xms128m -Xmx4096m -Xss1024K -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC +``` + + + +![解释](http://media.dreamcat.ink/uPic/iShot2020-05-26上午09.58.16.png) \ No newline at end of file diff --git "a/Java/crazy/Java\345\237\272\347\241\200.md" "b/Java/crazy/Java\345\237\272\347\241\200.md" new file mode 100644 index 00000000..0b594f25 --- /dev/null +++ "b/Java/crazy/Java\345\237\272\347\241\200.md" @@ -0,0 +1,936 @@ +## 大纲图 + +![基础面试](http://media.dreamcat.ink/uPic/基础面试.png) + +### 简述线程、程序、进程的基本概念。以及他们之间关系是什么? + +**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 + +**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 + +**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。 + +### Java 语言有哪些特点? + +1. 简单易学; +2. 面向对象(封装,继承,多态); +3. 平台无关性( Java 虚拟机实现平台无关性); +4. 可靠性; +5. 安全性; +6. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持); +7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便); +8. 编译与解释并存; + +### 面向对象和面向过程的区别 + +- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。** +- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。 + +> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是因为它是面向对象语言,而是Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码。 +> +> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比Java好。 + +### ==、hashcode和equals + +#### **==** + +它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。 + +#### **equals()** + + 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: + +- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 +- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 + +```java +public class test1 { + public static void main(String[] args) { + String a = new String("ab"); // a 为一个引用 + String b = new String("ab"); // b为另一个引用,对象的内容一样 + String aa = "ab"; // 放在常量池中 + String bb = "ab"; // 从常量池中查找 + if (aa == bb) // true + System.out.println("aa==bb"); + if (a == b) // false,非同一对象 + System.out.println("a==b"); + if (a.equals(b)) // true + System.out.println("aEQb"); + if (42 == 42.0) { // true + System.out.println("true"); + } + } +} +``` + +- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 +- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 + +#### hashcode + +**hashcode:**hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。 + +散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) + +#### 为什么要有hashcode + +**我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 + +通过我们可以看出:`hashCode()` 的作用就是**获取哈希码**,也称为散列码;它实际上是返回一个int整数。这个**哈希码的作用**是确定该对象在哈希表中的索引位置。**`hashCode() `在散列表中才有用,在其它情况下没用**。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。 + +#### hashcode和equals的相关规定 + +1. 如果两个对象相等,则hashcode一定也是相同的 +2. 两个对象相等,对两个对象分别调用equals方法都返回true +3. 两个对象有相同的hashcode值,它们也不一定是相等的 +4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** +5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) + +### JVM JDK 和 JRE 最详细通俗的解答 + +#### JVM + +Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 + +> 在 Java 中,JVM可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。 + +#### JDK和JRE + +**JDK**是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。 + +**JRE** 是 Java运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。但是,它不能用于创建新程序。 + +如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装JDK了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,如果要使用JSP部署Web应用程序,那么从技术上讲,您只是在应用程序服务器中运行Java程序。那你为什么需要JDK呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 + +### Java和C++的区别? + +- 都是面向对象的语言,都支持封装、继承和多态 +- Java 不提供指针来直接访问内存,程序内存更加安全 +- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 +- Java 有自动内存管理机制,不需要程序员手动释放无用内存 + +### 基本类型 + +#### 字符型常量和字符串常量的区别? + +1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符 +2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置) +3. 占内存大小 字符常量只占2个字节; 字符串常量占若干个字节(至少一个字符结束标志) (**注意: char在Java中占两个字节**) + +![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) + +#### 自动装箱与拆箱 + +- **装箱**:将基本类型用它们对应的引用类型包装起来; +- **拆箱**:将包装类型转换为基本数据类型; + +#### 说说&和&&的区别 + +- &和&&都可以用作逻辑与的运算符,表示逻辑与(and) +- 当运算符两边的表达式的结果都为 true 时, 整个运算结果才为 true,否则,只要有一方为 false,则结果为 false。 +- &&还具有短路的功能,即如果第一个表达式为 false,则不再计算第二个表达式 +- &还可以用作位运算符,当&操作符两边的表达式不是 boolean 类型时,&表示按位与操作,我们通常 使用 0x0f 来与一个整数进行&运算,来获取该整数的最低 4 个 bit 位 + +#### short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 += 1; 有什么错? + +- 对于 short s1 = 1; s1 = s1 + 1; 由于 s1+1 运算时会自动提升表达式的类型,所以结果是 int 型,再赋值 给 short 类型 s1 时,**编译器将报告需要强制转换类型的错误。** + +- 对于 short s1 = 1; s1 += 1;由于 **+= 是 java 语言规定的运算符,java 编译器会对它进行特殊处理**,因此 可以正确编译。 + +- ```java + public class TypeConvert { + public static void main(String[] args) { + // 字面量属于 double 类型 + // 不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型 + // Java 不能隐式执行向下转型,因为这会使得精度降低。 + // float f = 1.1; + float f = 1.1f; + + // 因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。 + short s1 = 1; + // s1 = s1 + 1; + + // 但是使用 += 运算符可以执行隐式类型转换。 + s1 += 1; + // 上面的语句相当于将 s1 + 1 的计算结果进行了向下转型: + s1 = (short) (s1 + 1); + + } + } + ``` + +#### char 型变量中能不能存贮一个中文汉字?为什么? + +- char 型变量是用来存储 Unicode 编码的字符的,unicode 编码字符集中包含了汉字,所以,char 型变量 中当然可以存储汉字啦。 +- 如果某个特殊的汉字没有被包含在 unicode 编码字符集中,那么,这个 char 型变量中就不能存储这个特殊汉字。 +- unicode 编码占用两个字节,所以,char 类型的变量也是占 用两个字节。 + +#### 整形包装类缓存池 + +```java +public class IntegerPackDemo { + public static void main(String[] args) { + Integer x = 3; // 装箱 + int z = x; // 拆箱 + Integer y = 3; + System.out.println(x == y); // true + // ------------------------- + Integer a = new Integer(3); + Integer b = new Integer(3); + System.out.println(a == b); // false 老生常谈了,就不说为什么了 + System.out.println(a.equals.(b)); // true // 这里是用重写了equals方法,比较的是值,而不是对象的地址 + // ------------------------ + // 缓存池 + Integer aa = Integer.valueOf(123); + Integer bb = Integer.valueOf(123); + System.out.println(aa == bb); // true + /** + * valueOf的源码 + * public static Integer valueOf(int i) { + * // 判断是否在Integer的范围内 + * if (i >= IntegerCache.low && i <= IntegerCache.high) + * return IntegerCache.cache[i + (-IntegerCache.low)]; + * return new Integer(i); + * } + */ + } +} +``` + +**当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。所以上述代码中,x和y引用的是相同的Integer对象。** + +### 面向对象 + +#### Java 面向对象编程三大特性: 封装 继承 多态 + +##### 封装 + +封装把一个对象的**属性私有化**,同时提供一些可以**被外界访问的属性的方法**,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 + +##### 继承 + +继承是使用**已存在的类**的定义作为基础建立新类的技术,新类的定义可以增加**新的数据**或**新的功能**,也可以用**父类的功能**,但不**能选择性地继承父类**。通过使用继承我们能够非常方便地复用以前的代码。 + +**关于继承如下 3 点请记住:** + +1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 +2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 +3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 + +##### 多态 + +所谓多态就是指程序中定义的**引用变量所指向的具体类型**和**通过该引用变量发出的方法**调用在编程时**并不确定**,而是在程序**运行期间才确定**,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 + +在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 + +#### 构造器 Constructor 是否可被 override? + +在讲继承的时候我们就知道父类的私有属性和构造方法并不能被继承,所以 Constructor 也就不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 + +#### 构造方法有哪些特性 + +1. 名字与类名相同。 +2. 没有返回值,但不能用void声明构造函数。 +3. 生成类的对象时自动执行,无需调用。 + +#### 接口和抽象类的区别是什么? + +1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 +2. 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。 +3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过implement关键字扩展多个接口。 +4. 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。 +5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。 + +备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。 + +#### 成员变量与局部变量的区别有哪些? + +1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 +2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 +3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 +4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 + +#### 重载和重写的区别 + +##### 重载 + +发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 + +##### 重写 + +重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。** + +#### 创建一个对象用什么运算符?对象实体与对象引用有何不同? + +new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。 + +#### 对象的相等与指向他们的引用相等,两者有什么不同? + +对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。 + +### Java值传递 + +**按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。** + +接下来三个例子瞧一瞧: + +基本类型传递 + +```java +public static void main(String[] args) { + int num1 = 10; + int num2 = 20; + + swap(num1, num2); + + System.out.println("num1 = " + num1); + System.out.println("num2 = " + num2); +} + +public static void swap(int a, int b) { + int temp = a; + a = b; + b = temp; + + System.out.println("a = " + a); + System.out.println("b = " + b); +} +// a = 20 +// b = 10 +// num1 = 10 +// num2 = 20 +``` + +![基本类型传递](http://media.dreamcat.ink/uPic/基本类型传递%20.png) + +在 swap 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。 + +接下来看数组: + +```java + public static void main(String[] args) { + int[] arr = { 1, 2, 3, 4, 5 }; + System.out.println(arr[0]); + change(arr); + System.out.println(arr[0]); + // 法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。 + } + + private static void change(int[] array) { + // 修改数组中的一个元素 + array[0] = 0; + } +// 1 +// 0 +``` + +![数组类型传递](http://media.dreamcat.ink/uPic/数组类型传递.png) + +array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。 + +**通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。** + +再看对象引用例子: + +```java + public static void main(String[] args) { + // 有些程序员(甚至本书的作者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。 + Student s1 = new Student("Mai"); + Student s2 = new Student("Feng"); + swap2(s1, s2); + System.out.println("s1:" + s1.getName()); + System.out.println("s2:" + s2.getName()); + // 方法并没有改变存储在变量 s1 和 s2 中的对象引用。 + // swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝 + } + + private static void swap2(Student x, Student y) { + Student temp = x; + x = y; + y = temp; + System.out.println("x:" + x.getName()); + System.out.println("y:" + y.getName()); + } +// x:Feng +// y:Mai +// s1:Mai +// s2:Feng +``` + +![对象引用传递](http://media.dreamcat.ink/uPic/对象引用传递.png) + +**方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝** + +下面再总结一下 Java 中方法参数的使用情况: + +- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。 +- 一个方法可以改变一个对象参数的状态。 +- 一个方法不能让对象参数引用一个新的对象。 + +### String + +#### String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? + +##### **可变性** + +简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。 + +##### **线程安全性** + +String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 + +##### **性能** + +每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 + +##### **对于三者使用的总结:** + +1. 操作少量的数据: 适用String +2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder +3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer + +##### 代码例子 + +```java + public static void main(String[] args) { + // String + String str = "hello"; + long start = System.currentTimeMillis(); + for (int i = 0; i < 100000; i++) { + str += i; // 创建多少个对象,, + } + System.out.println("String: " + (System.currentTimeMillis() - start)); + + // StringBuffer + StringBuffer sb = new StringBuffer("hello"); + long start1 = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + sb.append(i); + } + System.out.println("StringBuffer: " + (System.currentTimeMillis() - start1)); + + + // StringBuilder + StringBuilder stringBuilder = new StringBuilder("hello"); + long start2 = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + stringBuilder.append(i); + } + System.out.println("StringBuilder: " + (System.currentTimeMillis() - start2)); + } + +``` + +#### String A = "123"; String B = new String("123");生成几个对象? + +如果常量池中,原来没有“123”那么就是生成了2个对象,如果常量池中有“123”那么只要1个对象生成 + +#### 聊一聊String.intern()这个方法 + +```java +public class StringTest { + public static void main(String[] args) { + String str1 = "todo"; + String str2 = "todo"; + String str3 = "to"; + String str4 = "do"; + String str5 = str3 + str4; + String str6 = new String(str1); + + System.out.println("------普通String测试结果------"); + System.out.print("str1 == str2 ? "); + System.out.println( str1 == str2); + System.out.print("str1 == str5 ? "); + System.out.println(str1 == str5); + System.out.print("str1 == str6 ? "); + System.out.print(str1 == str6); + System.out.println(); + + System.out.println("---------intern测试结果---------"); + System.out.print("str1.intern() == str2.intern() ? "); + System.out.println(str1.intern() == str2.intern()); + System.out.print("str1.intern() == str5.intern() ? "); + System.out.println(str1.intern() == str5.intern()); + System.out.print("str1.intern() == str6.intern() ? "); + System.out.println(str1.intern() == str6.intern()); + System.out.print("str1 == str6.intern() ? "); + System.out.println(str1 == str6.intern()); + } +} +``` + +运行结果: + +```java +------普通String测试结果------ +str1 == str2 ? true +str1 == str5 ? false +str1 == str6 ? false +---------intern测试结果--------- +str1.intern() == str2.intern() ? true +str1.intern() == str5.intern() ? true +str1.intern() == str6.intern() ? true +str1 == str6.intern() ? true +``` + +普通String代码结果分析: + +Java语言会使用常量池保存那些在编译期就已确定的已编译的class文件中的一份数据。主要有类、接口、方法中的常量,以及一些以文本形式出现的符号引用,如类和接口的全限定名、字段的名称和描述符、方法和名称和描述符等。因此在编译完Intern类后,生成的class文件中会在常量池中**保存“todo”、“to”和“do”三个String常量**。**变量str1和str2均保存的是常量池中“todo”的引用,所以str1==str2成立**;在执行 str5 = str3 + str4这句时,JVM会先创建一个**StringBuilder对象**,通过StringBuilder.append()方法将str3与str4的值拼接,然后通过StringBuilder.toString()返回一个**堆中的String对象的引用**,赋值给str5,因此str1和str5指向的不是同一个String对象,str1 == str5不成立;String str6 = new String(str1)一句显式创建了一个新的String对象,因此str1 == str6不成立便是显而易见的事了。 + +intern代码结果分析: + +String.intern()是一个Native方法,底层调用C++的 StringTable::intern方法实现。当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String,**若存在则直接返回常量池中相应Strnig的引用**;**若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用**。因此,只要是等值的String对象,使用intern()方法返回的都是常量池中同一个String引用,所以,这些等值的String对象通过intern()后使用==是可以匹配的。由此就可以理解上面代码中------intern------部分的结果了。因为str1、str5和str6是三个等值的String,所以通过intern()方法,他们均会指向常量池中的同一个String引用,因此str1.intern() == str5.intern() == str6.intern()均为true。 + +##### jdk6 + +Jdk6中常量池位于PermGen(永久代)中,PermGen是一块主要用于存放已加载的类信息和字符串池的大小固定的区域。执行intern()方法时,**若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串,然后返回该字符串的引用**。除此以外,JVM 会自动在常量池中保存一份之前已使用过的字符串集合。Jdk6中使用intern()方法的主要问题就在于常量池被保存在PermGen中:首先,PermGen是一块大小固定的区域,一般不同的平台PermGen的默认大小也不相同,大致在32M到96M之间。所以不能对不受控制的运行时字符串(如用户输入信息等)使用intern()方法,否则很有可能会引发PermGen内存溢出;其次String对象保存在Java堆区,Java堆区与PermGen是物理隔离的,因此如果对多个不等值的字符串对象执行intern操作,则会导致内存中存在许多重复的字符串,会造成性能损失。 + +##### jdk7 + +Jdk7**将常量池从PermGen区移到了Java堆区**,执行intern操作时,**如果常量池已经存在该字符串,则直接返回字符串引用**,否则**复制该字符串对象的引用**到常量池中并返回。堆区的大小一般不受限,所以将常量池从PremGen区移到堆区使得常量池的使用不再受限于固定大小。除此之外,位于堆区的常量池中的对象可以被垃圾回收。当常量池中的字符串不再存在指向它的引用时,JVM就会回收该字符串。可以使用 -XX:StringTableSize 虚拟机参数设置字符串池的map大小。字符串池内部实现为一个HashMap,所以当能够确定程序中需要intern的字符串数目时,可以将该map的size设置为所需数目*2(减少hash冲突),这样就可以使得String.intern()每次都只需要常量时间和相当小的内存就能够将一个String存入字符串池中。 + +##### 适用场景 + +Jdk6中常量池位于PermGen区,大小受限,所以不建议适用intern()方法,当需要字符串池时,需要自己使用HashMap实现。Jdk7、8中,常量池由PermGen区移到了堆区,还可以通过-XX:StringTableSize参数设置StringTable的大小,常量池的使用不再受限,由此可以重新考虑使用intern()方法。intern()方法优点:执行速度非常快,直接使用==进行比较要比使用equals()方法快很多;内存占用少。虽然intern()方法的优点看上去很诱人,但若不是在恰当的场合中使用该方法的话,便非但不能获得如此好处,反而还可能会有性能损失。 + +### 关键字 + +#### final + +final关键字主要用在三个地方:变量、方法、类。 + +1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 +2. 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。 +3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 + +final修饰有啥好处 + +- final的关键字提高了性能,JVM和java应用会缓存final变量; +- final变量可以在多线程环境下保持线程安全; +- 使用final的关键字提高了性能,JVM会对方法变量类进行优化; + +#### static + +1. **修饰成员变量和成员方法:** 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:`类名.静态变量名` `类名.静态方法名()` +2. **静态代码块:** 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. +3. **静态内部类(static修饰类的话只能修饰内部类):** 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。 +4. **静态导包(用来导入类中的静态资源,1.5之后的新特性):** 格式为:`import static` 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 + +#### this + +```java +class Manager { + Employees[] employees; + + void manageEmployees() { + int totalEmp = this.employees.length; + System.out.println("Total employees: " + totalEmp); + this.report(); + } + + void report() { } +} +``` + +在上面的示例中,this关键字用于两个地方: + +- this.employees.length:访问类Manager的当前实例的变量。 +- this.report():调用类Manager的当前实例的方法。 + +此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。 + +#### super + +```java +public class Super { + protected int number; + + protected showNumber() { + System.out.println("number = " + number); + } +} + +public class Sub extends Super { + void bar() { + super.number = 10; + super.showNumber(); + } +} +``` + +在上面的例子中,Sub 类访问父类成员变量 number 并调用其其父类 Super 的 `showNumber()` 方法。 + +**使用 this 和 super 要注意的问题:** + +- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 +- this、super不能用在static方法中。 + +**简单解释一下:** + +被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, **this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西**。 + +#### final, finally, finalize 的区别 + +- final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。 内部类要访问局部变量,局部变量必须定义成 final 类型 +- finally 是异常处理语句结构的一部分,表示总是执行。 +- finalize 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法, 可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。JVM 不保证此方法总被调用 + +#### 请说出作用域 public,private,protected + +| 作用域 | 当前类 | 同package | 子孙类 | 其他package | +| :-------: | :----: | :-------: | :----: | :---------: | +| public | √ | √ | √ | √ | +| protected | √ | √ | √ | × | +| friednly | √ | √ | × | × | +| private | √ | × | × | × | + +### 异常处理 + +在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 **Throwable类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。 + +#### Error + +**是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。 + +这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。 + +#### Exception + +**是程序本身可以处理的异常**。Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由Java虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以0时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。 + +#### 处理 + +- **try 块:** 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。 +- **catch 块:** 用于处理try捕获到的异常。 +- **finally 块:** 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return 语句时,finally语句块将在方法返回之前被执行。 + +**在以下4种特殊情况下,finally块不会被执行:** + +1. 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行 +2. 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 ;若该语句在异常语句之后,finally会执行 +3. 程序所在的线程死亡。 +4. 关闭CPU。 + +**注意:** 当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会覆盖原始的返回值。如下: + +```java + public static int f(int value) { + try { + return value * value; + } finally { + if (value == 2) { + return 0; + } + } + } +``` + +如果调用 `f(2)`,返回值将是0,因为finally语句的返回值覆盖了try语句块的返回值。 + +#### 最常见到的 5 个 runtime exception + +- RuntimeException +- NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException。 + +#### Throwable图解 + +![异常分类](http://media.dreamcat.ink/uPic/异常分类.png) + +### IO + +#### 获取用键盘输入常用的两种方法 + +方法1:通过 Scanner + +```java +Scanner input = new Scanner(System.in); +String s = input.nextLine(); +input.close(); +``` + +方法2:通过 BufferedReader + +```java +BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); +String s = input.readLine(); +``` + +#### Java 中 IO 流分为几种? + +- 按照流的流向分,可以分为输入流和输出流; +- 按照操作单元划分,可以划分为字节流和字符流; +- 按照流的角色划分为节点流和处理流。 + +Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。 + +- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 +- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + +#### 既然有了字节流,为什么还要有字符流? + +问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** + +回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 + +#### BIO,NIO,AIO 有什么区别? + +- **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 +- **NIO (New I/O):** NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 +- **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 + +### 反射 + +#### 反射式什么? + +**Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能成为java语言的反射机制。** + +#### 静态编译和动态编译 + +- 静态编译:在编译时确定类型,绑定对象 +- 动态编译:运行时确定类型,绑定对象 + +#### 反射机制优缺点 + +- 优点:运行期间类型的判断,动态加载类,提高代码的灵活度。 +- 缺点:性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的java代码要慢很多。 + +#### 反射的应用场景 + +在我们平时的项目开发过程中,基本上很少会直接使用的反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如**模块化**的开发,通过反射去调用对应的字节码;动态代理设计模型也采用了反射机制,还有我们日常使用的**Spring / Hibernate**等框架也大量使用到了反射机制。 + +- 我们在使用JDBC连接数据库时使用`Class.forName()`通过反射加载数据看的驱动程序; +- Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring通过XML配置模式装载Bean的过程; + - 将程序内所有XML或Properties配置文件加载入内存中; + - Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; + - 使用反射机制,根据这个字符串获得某个类的Class实例 + - 动态配置实例的属性 + +#### 反射得到的Class对象的三种方式 + +```java +public class ReflectDemo { + public static void main(String[] args) throws ClassNotFoundException { + // 第一种方式获取Class对象 + Student student = new Student(); // 这一new 产生一个Student对象,一个Class对象。 + Class studentClass = student.getClass(); + System.out.println(studentClass.getName()); // com.reflect.Student + + // 第二种方式获取Class对象 + Class studentClass2 = Student.class; + System.out.println(studentClass == studentClass2); //判断第一种方式获取的Class对象和第二种方式获取的是否是同一个 + + //第三种方式获取Class对象 + Class studentClass3 = Class.forName("com.reflect.Student"); //注意此字符串必须是真实路径,就是带包名的类路径,包名.类名 + System.out.println(studentClass3 == studentClass2); // //判断三种方式是否获取的是同一个Class对象 + + // 三种方式常用第三种,第一种对象都有了还要反射干什么。 + // 第二种需要导入类的包,依赖太强,不导包就抛编译错误。 + // 一般都第三种,一个字符串可以传入也可写在配置文件中等多种方法。 + } +} +``` + +#### 反射访问并调用构造方法 + +```java +public class ConstructorsDemo { + public static void main(String[] args) throws Exception { + // 1. 加载Class对象 + Class clazz = Class.forName("com.reflect.Student"); + + // 2. 获取所有公有构造方法 + System.out.println("**********************所有公有构造方法*********************************"); + Constructor[] constructors = clazz.getConstructors(); + for (Constructor constructor : constructors) { + System.out.println(constructor); + } + + // 3. + System.out.println("************所有的构造方法(包括:私有、受保护、默认、公有)***************"); + Constructor[] declaredConstructors = clazz.getDeclaredConstructors(); + for (Constructor declaredConstructor : declaredConstructors) { + System.out.println(declaredConstructor); + } + + // 4. + System.out.println("*****************获取公有、无参的构造方法*******************************"); + Constructor constructor = clazz.getConstructor(); + System.out.println(constructor); + + // 调用构造方法 + Object object = constructor.newInstance(); + System.out.println(object); + + // + System.out.println("******************获取私有构造方法,并调用*******************************"); + Constructor constructor1 = clazz.getDeclaredConstructor(char.class); + System.out.println(constructor1); + // 调用构造方法 + constructor1.setAccessible(true); // 暴力访问 + Object object2 = constructor1.newInstance('买'); + System.out.println(object2); + } +} +``` + +#### 反射访问并调用成员变量 + +```java +public class FieldDemo { + public static void main(String[] args) throws Exception { + // 1. 获取class对象 + Class clazz = Class.forName("com.reflect.Student"); + // 2. 获取所有字段 + System.out.println("************获取所有公有的字段********************"); + Field[] fields = clazz.getFields(); + for (Field field : fields) { + System.out.println(field); + } + // + System.out.println("************获取所有的字段(包括私有、受保护、默认的)********************"); + Field[] fields1 = clazz.getDeclaredFields(); + for (Field field : fields1) { + System.out.println(field); + } + // + System.out.println("*************获取公有字段**并调用***********************************"); + Field gender = clazz.getField("gender"); + System.out.println(gender); + // 获取一个对象 + Object o = clazz.getConstructor().newInstance(); + gender.set(o, "男"); + Student stu = (Student) o; + System.out.println("验证性别:" + stu.getGender()); + // + System.out.println("*************获取公有字段**并调用***********************************"); + Field name = clazz.getDeclaredField("name"); + System.out.println(name); + name.setAccessible(true); //暴力反射,解除私有限定 + name.set(o, "买"); + System.out.println("验证姓名:" + stu); + } +} +``` + +#### 反射访问并调用成员方法 + +```java +public class MethodDemo { + public static void main(String[] args) throws Exception { + // 1. 获取对象 + Class clazz = Class.forName("com.reflect.Student"); + // 2. 获取所有公有方法 + System.out.println("***************获取所有的”公有“方法*******************"); + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + System.out.println(method); + } + System.out.println("***************获取所有的方法,包括私有的*******************"); + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method declaredMethod : declaredMethods) { + System.out.println(declaredMethod); + } + System.out.println("***************获取公有的show1()方法*******************"); + Method m = clazz.getMethod("show1", String.class); + System.out.println(m); + // 实例化对象 + Object o = clazz.getConstructor().newInstance(); + m.invoke(o, "买"); + + System.out.println("***************获取私有的show4()方法******************"); + m = clazz.getDeclaredMethod("show4", int.class); + System.out.println(m); + m.setAccessible(true); // 暴力解除 私有 + Object result = m.invoke(o, 20);//需要两个参数,一个是要调用的对象(获取有反射),一个是实参 + System.out.println("返回值:" + result); + } +} +``` + +### 深拷贝VS浅拷贝 + +1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 + +2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 + + ![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg) + +#### 浅拷贝例子 + +```java +public class ShallowCopyDemo { + public static void main(String[] args) throws CloneNotSupportedException { + // 原始对象 + Student student = new Student(new Subject("买"), "峰"); + System.out.println("原始对象: " + student.getName() + " - " + student.getSubject().getName()); + + // 拷贝对象 + Student cloneStu = (Student) student.clone(); + System.out.println("拷贝对象: " + cloneStu.getName() + " - " + cloneStu.getSubject().getName()); + + // 原始对象和拷贝对象是否一样: + System.out.println("原始对象和拷贝对象是否一样: " + (student == cloneStu)); + + // 原始对象和拷贝对象的name属性是否一样 + System.out.println("原始对象和拷贝对象的name属性是否一样: " + (student.getName() == cloneStu.getName())); + + // 原始对象和拷贝对象的subj属性是否一样 + System.out.println("原始对象和拷贝对象的subj属性是否一样: " + (student.getSubject() == cloneStu.getSubject())); + + student.setName("小"); + student.getSubject().setName("疯"); + System.out.println("更新后的原始对象: " + student.getName() + " - " + student.getSubject().getName()); + System.out.println("更新原始对象后的克隆对象: " + cloneStu.getName() + " - " + cloneStu.getSubject().getName()); + // 在这个例子中,让要拷贝的类Student实现了Clonable接口并重写Object类的clone()方法,然后在方法内部调用super.clone()方法。 + // 从输出结果中我们可以看到,对原始对象stud的"name"属性所做的改变并没有影响到拷贝对象clonedStud; + // 但是对引用对象subj的"name"属性所做的改变影响到了拷贝对象clonedStud。 + } +} +``` + +结果如下: + +``` +原始对象: 峰 - 买 +拷贝对象: 峰 - 买 +原始对象和拷贝对象是否一样: false +原始对象和拷贝对象的name属性是否一样: true +原始对象和拷贝对象的subj属性是否一样: true +更新后的原始对象: 小 - 疯 +更新原始对象后的克隆对象: 峰 - 疯 +``` + +**可以看到,浅拷贝中的引用类型并没有拷贝一份,都指向同一个对象**。 + +深拷贝例子 + +```java +// 重写 student的clone方法,例子还是如上。 + @Override + protected Object clone() throws CloneNotSupportedException { + // 浅拷贝 +// return super.clone(); + + // 深拷贝 + Student student = new Student(new Subject(subject.getName()), name); + return student; + // 因为它是深拷贝,所以你需要创建拷贝类的一个对象。 + // 因为在Student类中有对象引用,所以需要在Student类中实现Cloneable接口并且重写clone方法。 + } +``` + +结果如下: + +``` +原始对象: 峰 - 买 +拷贝对象: 峰 - 买 +原始对象和拷贝对象是否一样: false +原始对象和拷贝对象的name属性是否一样: true +原始对象和拷贝对象的subj属性是否一样: false +更新后的原始对象: 小 - 疯 +更新原始对象后的克隆对象: 峰 - 买 +``` + +**可以看到,浅拷贝中的引用类型创建一份新对象**。 + +> 创作不易哇,觉得有帮助的话,给个小小的star呗。[github地址](https://github.com/DreamCats/JavaBooks)😁😁😁 \ No newline at end of file diff --git "a/Java/crazy/Java\345\244\232\347\272\277\347\250\213.md" "b/Java/crazy/Java\345\244\232\347\272\277\347\250\213.md" new file mode 100644 index 00000000..828efb83 --- /dev/null +++ "b/Java/crazy/Java\345\244\232\347\272\277\347\250\213.md" @@ -0,0 +1,1328 @@ +--- +title: 个人吐血系列-总结Java多线程 +date: 2020-03-25 23:07:33 +tags: 多线程 +categories: java-interview +--- +## 大纲图 + +![多线程](http://media.dreamcat.ink/uPic/多线程.png) + +### 什么是线程和进程? + +#### 进程 + +进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 + +比如:当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 + +#### 线程 + +- 线程是一个比进程更小的执行单位 +- 一个进程在其执行的过程中可以产生**多个线程** +- 与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程 + +### 并发和并行的区别? + +#### **并发:同一时间段,多个任务都在执行 (单位时间内不一定同时执行);** + +#### **并行:单位时间内,多个任务同时执行。** + +### 为什么使用多线程? + +- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 +- **从当代互联网发展趋势来说:**现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 + +### 使用多线程可能会带来什么问题? + +可能会带来**内存泄漏**、**上下文切换**、**死锁**有受限于硬件和软件的资源闲置问题。 + +### 说说线程的生命周期? + +![](https://www.pdai.tech/_images/pics/ace830df-9919-48ca-91b5-60b193f593d2.png) + +线程创建之后它将处于`New`(新建)状态,调用 `start()` 方法后开始运行,线程这时候处于 `READY`(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 `RUNNING`(运行) 状态。 + +当线程执行 `wait()`方法之后,线程进入 `WAITING`(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 `TIME_WAITING`(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 `TIMED WAITING` 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 `BLOCKED`(阻塞)状态。线程在执行 Runnable 的` run() `方法之后将会进入到 `TERMINATED`(终止) 状态。 + +### 什么是上下文切换? + +多线程编程中一般**线程的个数都大于 CPU 核心的个数**,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配**时间片并轮转**的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 + +实际上就是**任务从保存到再加载的过程就是一次上下文切换**。 + +上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 + +Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 + +### 什么是死锁?如何避免死锁? + +如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 + +![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4%E6%AD%BB%E9%94%811.png) + +学过操作系统的朋友都知道产生死锁必须具备以下四个条件: + +- 互斥条件:该资源任意一个时刻只由一个线程占用。(同一时刻,这个碗是我的,你不能碰) +- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(我拿着这个碗一直不放) +- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。(我碗中的饭没吃完,你不能抢,释放权是我自己的,我想什么时候放就什么时候放) +- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(我拿了A碗,你拿了B碗,但是我还想要你的B碗,你还想我的A碗) + +```java +public class DeadLockDemo { + private static Object resource1 = new Object();//资源 1 + private static Object resource2 = new Object();//资源 2 + + public static void main(String[] args) { + new Thread(() -> { + synchronized (resource1) { + System.out.println(Thread.currentThread() + "get resource1"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread() + "waiting get resource2"); + synchronized (resource2) { + System.out.println(Thread.currentThread() + "get resource2"); + } + } + }, "线程 1").start(); + + new Thread(() -> { + synchronized (resource2) { + System.out.println(Thread.currentThread() + "get resource2"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread() + "waiting get resource1"); + synchronized (resource1) { + System.out.println(Thread.currentThread() + "get resource1"); + } + } + }, "线程 2").start(); + } +} +Thread[线程 1,5,main]get resource1 +Thread[线程 2,5,main]get resource2 +Thread[线程 1,5,main]waiting get resource2 +Thread[线程 2,5,main]waiting get resource1 +``` + +线程 A 通过 `synchronized (resource1)` 获得 resource1 的监视器锁,然后通过` Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。 + +#### 如何找到死锁 + +**通过jdk常用的命令jsp和jstack,jsp查看java程序的id,jstack查看方法的栈信息等。** + +### 说说Sleep和Wait方法的区别 + +- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。 +- 两者都可以暂停线程的执行。 +- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 +- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。 + +### Synchronzed + +#### 使用方式 + +- **修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁** +- **修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁** 。 +- **修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。** + +#### 单例 + +```java +public class Singleton { + + private volatile static Singleton uniqueInstance; // 第一步 + + private Singleton() { // 第二步,私有 + } + + public static Singleton getUniqueInstance() { + //先判断对象是否已经实例过,没有实例化过才进入加锁代码 + if (uniqueInstance == null) { // 双重校验 + //类对象加锁 + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } +} +``` + +注意:**uniqueInstance 采用 volatile 关键字修饰也是很有必要** + +uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: + +1. 为 uniqueInstance 分配内存空间 +2. 初始化 uniqueInstance +3. 将 uniqueInstance 指向分配的内存地址 + +但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 + +#### Synchronized 和 ReenTrantLock 的对比 + +1. **两者都是可重入锁**:两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 +2. **Synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API**:synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 +3. **ReenTrantLock 比 Synchronized 增加了一些高级功能** + 1. **等待可中断**:过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 + 2. **可实现公平锁** + 3. **可实现选择性通知(锁可以绑定多个条件)**:线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” + 4. **性能已不是选择标准**:在jdk1.6之前synchronized 关键字吞吐量随线程数的增加,下降得非常严重。1.6之后,**synchronized 和 ReenTrantLock 的性能基本是持平了。** + +#### 底层原理 + +**synchronized 关键字底层原理属于 JVM 层面。** + +1. **synchronized 同步语句块的情况** + + **synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 + +2. **synchronized 修饰方法的的情况** + + synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 + +#### 1.6版本的优化 + +在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 **Mutex Lock** 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 + +锁主要存在四中状态,依次是:**无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态**,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 + +1. **偏向锁** + + **引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。 + + 偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步! + + 但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 + + 升级过程: + + 1. 访问Mark Word中偏向锁的标识是否设置成1,锁标识位是否为01,确认偏向状态 + 2. 如果为可偏向状态,则判断当前线程ID是否为偏向线程 + 3. 如果偏向线程未当前线程,则通过cas操作竞争锁,如果竞争成功则操作Mark Word中线程ID设置为当前线程ID + 4. 如果cas偏向锁获取失败,则挂起当前偏向锁线程,偏向锁升级为轻量级锁。 + +2. **轻量级锁** + + 倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** + + **轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!** + + 升级过程: + + 1. 线程由偏向锁升级为轻量级锁时,会先把锁的对象头MarkWord复制一份到线程的栈帧中,建立一个名为锁记录空间(Lock Record),用于存储当前Mark Word的拷贝。 + 2. 虚拟机使用cas操作尝试将对象的Mark Word指向Lock Record的指针,并将Lock record里的owner指针指对象的Mark Word。 + 3. 如果cas操作成功,则该线程拥有了对象的轻量级锁。第二个线程cas自旋锁等待锁线程释放锁。 + 4. 如果多个线程竞争锁,轻量级锁要膨胀为重量级锁,Mark Word中存储的就是指向重量级锁(互斥量)的指针。其他等待线程进入阻塞状态。 + +3. **自旋锁和自适应自旋** + + 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。 + + 互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。 + + **一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。 + + **在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。 + +4. **锁消除** + + 锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。 + +5. **锁粗化** + + 原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。 + +6. 总结升级过程: + 1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁 + 2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1 + 3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。 + 4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁 + 5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 + 6. 如果自旋成功则依然处于轻量级状态。 + 7. 如果自旋失败,则升级为重量级锁。 + +### Volatile + +#### Volatile的特性 + +1. **可见性** + + **volatile的可见性是指当一个变量被volatile修饰后,这个变量就对所有线程均可见。白话点就是说当一个线程修改了一个volatile修饰的变量后,其他线程可以立刻得知这个变量的修改,拿到最这个变量最新的值。** + + ```java + public class VolatileVisibleDemo { + + // private boolean isReady = true; + private volatile boolean isReady = true; + + void m() { + System.out.println(Thread.currentThread().getName() + " m start..."); + while (isReady) { + } + System.out.println(Thread.currentThread().getName() + " m end..."); + } + + public static void main(String[] args) { + VolatileVisibleDemo demo = new VolatileVisibleDemo(); + new Thread(() -> demo.m(), "t1").start(); + try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();} + demo.isReady = false; // 刚才一秒过后开始执行 + } + } + ``` + + **分析:一开始isReady为true,m方法中的while会一直循环,而主线程开启开线程之后会延迟1s将isReady赋值为false,若不加volatile修饰,则程序一直在运行,若加了volatile修饰,则程序最后会输出t1 m end...** + +2. **有序性** + + **有序性是指程序代码的执行是按照代码的实现顺序来按序执行的;volatile的有序性特性则是指禁止JVM指令重排优化。** + + ```java + public class Singleton { + private static Singleton instance = null; // valotile + //private static volatile Singleton instance = null; + private Singleton() { } // 私有 + + public static Singleton getInstance() { // 双重校验 + //第一次判断 + if(instance == null) { + synchronized (Singleton.class) { // 加锁 + if(instance == null) { + //初始化,并非原子操作 + instance = new Singleton(); // 这一行代码展开其实分三步走 + } + } + } + return instance; + } + } + ``` + + **上面的代码是一个很常见的单例模式实现方式,但是上述代码在多线程环境下是有问题的。为什么呢,问题出在instance对象的初始化上,因为`instance = new Singleton();`这个初始化操作并不是原子的,在JVM上会对应下面的几条指令:** + + ``` + memory =allocate(); //1. 分配对象的内存空间 + ctorInstance(memory); //2. 初始化对象 + instance =memory; //3. 设置instance指向刚分配的内存地址 + ``` + + **上面三个指令中,步骤2依赖步骤1,但是步骤3不依赖步骤2,所以JVM可能针对他们进行指令重拍序优化,重排后的指令如下:** + + ``` + memory =allocate(); //1. 分配对象的内存空间 + instance =memory; //3. 设置instance指向刚分配的内存地址 + ctorInstance(memory); //2. 初始化对象 + ``` + + **这样优化之后,内存的初始化被放到了instance分配内存地址的后面,这样的话当线程1执行步骤3这段赋值指令后,刚好有另外一个线程2进入getInstance方法判断instance不为null,这个时候线程2拿到的instance对应的内存其实还未初始化,这个时候拿去使用就会导致出错。** + + **所以我们在用这种方式实现单例模式时,会使用volatile关键字修饰instance变量,这是因为volatile关键字除了可以保证变量可见性之外,还具有防止指令重排序的作用。当用volatile修饰instance之后,JVM执行时就不会对上面提到的初始化指令进行重排序优化,这样也就不会出现多线程安全问题了。** + +3. **不能保证原子性** + + **volatile关键字能保证变量的可见性和代码的有序性,但是不能保证变量的原子性,下面我再举一个volatile与原子性的例子:** + + ```java + public class VolatileAtomicDemo { + public static volatile int count = 0; + + public static void increase() { + count++; + } + + public static void main(String[] args) { + Thread[] threads = new Thread[20]; + for(int i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + for(int j = 0; j < 1000; j++) { + increase(); + } + }); + threads[i].start(); + } + //等待所有累加线程结束 + while (Thread.activeCount() > 1) { + Thread.yield(); + } + System.out.println(count); + } + } + ``` + + **上面这段代码创建了20个线程,每个线程对变量count进行1000次自增操作,如果这段代码并发正常的话,结果应该是20000,但实际运行过程中经常会出现小于20000的结果,因为count++这个自增操作不是原子操作。**[看图](https://www.processon.com/view/link/5e130e51e4b07db4cfac9d2c) + + ##### 内存屏障 + + **Java的Volatile的特征是任何读都能读到最新值,本质上是JVM通过内存屏障来实现的;为了实现volatile内存语义,JMM会分别限制重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:** + + | 是否能重排序 | 第二个操作 | | | +| :----------: | :--------: | :--------: | :--------: | + | 第一个操作 | 普通读/写 | volatile读 | volatile写 | + | 普通读/写 | | | no | + | volatile读 | no | no | no | + | volatile写 | | no | no | + + **从上表我们可以看出:** + + - 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。 + - 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。 + - 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。 + + **为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:** + + - 在每个volatile写操作的前面插入一个StoreStore屏障。 + - 在每个volatile写操作的后面插入一个StoreLoad屏障。 + - 在每个volatile读操作的后面插入一个LoadLoad屏障。 + - 在每个volatile读操作的后面插入一个LoadStore屏障。 + + **volatile写插入内存指令图:** + + ![volatile](https://image-static.segmentfault.com/416/041/416041851-5ac871370fec9_articlex) + +**上图中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。** + +**这里比较有意思的是volatile写后面的StoreLoad屏障。这个屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面,是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确实现volatile的内存语义,JMM在这里采取了保守策略:在每个volatile写的后面或在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里我们可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率。** + +**volatile读插入内存指令图:** + +![volatile读](https://image-static.segmentfault.com/288/764/2887649856-5ac871c442f52_articlex) + +**上图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。** + +**上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面我们通过具体的示例代码来说明:** + +```java +class VolatileBarrierExample { + int a; + volatile int v1 = 1; + volatile int v2 = 2; + + void readAndWrite() { + int i = v1; //第一个volatile读 + int j = v2; // 第二个volatile读 + a = i + j; //普通写 + v1 = i + 1; // 第一个volatile写 + v2 = j * 2; //第二个 volatile写 + } +} +``` + +**针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:** + +![readAndWrite](https://image-static.segmentfault.com/178/456/1784565222-5ac871e5e6dec_articlex) + +**注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器常常会在这里插入一个StoreLoad屏障。** + +#### volatile汇编 + +```java +0x000000011214bb49: mov %rdi,%rax + 0x000000011214bb4c: dec %eax + 0x000000011214bb4e: mov %eax,0x10(%rsi) + 0x000000011214bb51: lock addl $0x0,(%rsp) ;*putfield v1 + ; - com.earnfish.VolatileBarrierExample::readAndWrite@21 (line 35) + + 0x000000011214bb56: imul %edi,%ebx + 0x000000011214bb59: mov %ebx,0x14(%rsi) + 0x000000011214bb5c: lock addl $0x0,(%rsp) ;*putfield v2 + ; - com.earnfish.VolatileBarrierExample::readAndWrite@28 (line 36) +``` + +```java +v1 = i - 1; // 第一个volatile写 +v2 = j * i; // 第二个volatile写 +``` + +**可见其本质是通过一个lock指令来实现的。那么lock是什么意思呢?** + +**它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。** + +- 锁住内存 +- 任何读必须在写完成之后再执行 +- 使其它线程这个值的栈缓存失效 + +### CAS + +**我们在读Concurrent包下的类的源码时,发现无论是**ReenterLock内部的AQS,还是各种Atomic开头的原子类,内部都应用到了`CAS` + +```java +public class Test { + + public AtomicInteger i; + + public void add() { + i.getAndIncrement(); + } +} +``` + +**我们来看`getAndIncrement`的内部:** + +```java +public final int getAndIncrement() { + return unsafe.getAndAddInt(this, valueOffset, 1); +} +``` + +**再深入到`getAndAddInt`():** + +```java +public final int getAndAddInt(Object var1, long var2, int var4) { + int var5; + do { + var5 = this.getIntVolatile(var1, var2); + } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); + + return var5; +} +``` + +**现在重点来了,`compareAndSwapInt(var1, var2, var5, var5 + var4)`其实换成`compareAndSwapInt(obj, offset, expect, update)`比较清楚,意思就是如果`obj`内的`value`和`expect`相等,就证明没有其他线程改变过这个变量,那么就更新它为`update`,如果这一步的`CAS`没有成功,那就采用自旋的方式继续进行`CAS`操作,取出乍一看这也是两个步骤了啊,其实在`JNI`里是借助于一个`CPU`指令完成的。所以还是原子操作。** + +#### CAS底层 + +```c +UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) + UnsafeWrapper("Unsafe_CompareAndSwapInt"); + oop p = JNIHandles::resolve(obj); + jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); + return (jint)(Atomic::cmpxchg(x, addr, e)) == e; +UNSAFE_END +``` + +**p是取出的对象,addr是p中offset处的地址,最后调用了`Atomic::cmpxchg(x, addr, e)`, 其中参数x是即将更新的值,参数e是原内存的值。代码中能看到cmpxchg有基于各个平台的实现。** + +#### ABA问题 + +描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。 + +**目前在JDK的atomic包里提供了一个类`AtomicStampedReference`来解决ABA问题。** + +```java +public class ABADemo { + static AtomicInteger atomicInteger = new AtomicInteger(100); + static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1); + + public static void main(String[] args) { + System.out.println("=====ABA的问题产生====="); + new Thread(() -> { + atomicInteger.compareAndSet(100, 101); + atomicInteger.compareAndSet(101, 100); + }, "t1").start(); + + new Thread(() -> { + // 保证线程1完成一次ABA问题 + try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } + System.out.println(atomicInteger.compareAndSet(100, 2020) + " " + atomicInteger.get()); + }, "t2").start(); + try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } + + System.out.println("=====解决ABA的问题====="); + new Thread(() -> { + int stamp = atomicStampedReference.getStamp(); // 第一次获取版本号 + System.out.println(Thread.currentThread().getName() + " 第1次版本号" + stamp); + try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } + atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); + System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp()); + atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); + System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp()); + }, "t3").start(); + + new Thread(() -> { + int stamp = atomicStampedReference.getStamp(); + System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp); + try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } + boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, stamp + 1); + System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp()); + System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference()); + }, "t4").start(); + + } +} +``` + +### ThreadLocal + +**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。****如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** + +#### 原理 + +```java +public class Thread implements Runnable { + ...... +//与此线程有关的ThreadLocal值。由ThreadLocal类维护 +ThreadLocal.ThreadLocalMap threadLocals = null; + +//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 +ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; + ...... +} +``` + +从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 + +`ThreadLocal`类的`set()`方法 + +```java + public void set(T value) { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); + } + ThreadLocalMap getMap(Thread t) { + return t.threadLocals; + } +``` + +**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。** + +**每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。这也就解释了ThreadLocal声明的变量为什么在每一个线程都有自己的专属本地变量。 + +#### 内存泄露 + +`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 + +### 并发集合容器 + +#### 为什么说ArrayList线程不安全? + +**看add方法的源码** + +```java +public boolean add(E e) { + + /** + * 添加一个元素时,做了如下两步操作 + * 1.判断列表的capacity容量是否足够,是否需要扩容 + * 2.真正将元素放在列表的元素数组里面 + */ + ensureCapacityInternal(size + 1); // Increments modCount!! // 可能因为该操作,导致下一步发生数组越界 + elementData[size++] = e; // 可能null值 + return true; +} +``` + +**数组越界** + +1. 列表大小为9,即size=9 +2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。 +3. 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。 +4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。 +5. 线程B也发现需求大小为10,也可以容纳,返回。 +6. 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。 +7. 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException. + +**null值情况** + +**elementData[size++] = e不是一个原子操作**: + +1. elementData[size] = e; +2. size = size + 1; + +逻辑: + +1. 列表大小为0,即size=0 +2. 线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。 +3. 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。 +4. 线程A开始将size的值增加为1 +5. 线程B开始将size的值增加为2 + +**这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。** + +#### 解决非安全集合的并发都有哪些? + +[ArrayList->Vector->SynchronizedList->CopyOnWriteArrayList](https://github.com/DreamCats/JavaBooks/blob/master/Multithread/src/com/juc/collectiontest/ContainerNotSafeDemo.java) + +[ArraySet->SynchronizedSet->CopyOnWriteArraySet](https://github.com/DreamCats/JavaBooks/blob/master/Multithread/src/com/juc/collectiontest/HashSetTest.java) + +[HashMap->SynchronizedMap->ConcurrentHashMap](https://github.com/DreamCats/JavaBooks/blob/master/Multithread/src/com/juc/collectiontest/MapSafe.java) + +### 并发同步容器 + +#### AQS原理 + +AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。 + +```java +private volatile int state;//共享变量,使用volatile修饰保证线程可见性 +``` + +状态信息通过 protected 类型的`getState`,`setState`,`compareAndSetState`进行操作 + +```java +//返回同步状态的当前值 +protected final int getState() { + return state; +} + // 设置同步状态的值 +protected final void setState(int newState) { + state = newState; +} +//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) +protected final boolean compareAndSetState(int expect, int update) { + return unsafe.compareAndSwapInt(this, stateOffset, expect, update); +} +``` + +**AQS 定义两种资源共享方式** + +1. **Exclusive**(独占)只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁。 + + 总结:公平锁和非公平锁只有两处不同: + + 1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。 + 2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。 + +2. **Share**(共享)多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、 CyclicBarrier、ReadWriteLock 。 + +**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:** + +```java +isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 +tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 +tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 +tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 +tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 +``` + +#### CountDownLatch + +CountDownLatch是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用countDown方法时,其实使用了`tryReleaseShared`方法以CAS的操作来减少state,直至state为0就代表所有的线程都调用了countDown方法。当调用await方法的时候,如果state不为0,就代表仍然有线程没有调用countDown方法,那么就把已经调用过countDown的线程都放入阻塞队列Park,并自旋CAS判断state == 0,直至最后一个线程调用了countDown,使得state == 0,于是阻塞的线程便判断成功,全部往下执行。 + +三种用法: + +1. 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :`new CountDownLatch(n)`,每当一个任务线程执行完毕,就将计数器减 1 `countdownlatch.countDown()`,当计数器的值变为 0 时,在`CountDownLatch上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 +2. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 :`new CountDownLatch(1)`,多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。 +3. 死锁检测:一个非常方便的使用场景是,你可以使用 n 个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。 + +```java +public class CountDownLatchDemo { + + public static void main(String[] args) throws InterruptedException { + countDownLatchTest(); +// general(); + } + + public static void general() { + for (int i = 0; i < 6; i++) { + new Thread(() -> { + System.out.println(Thread.currentThread().getName() + " 上完自习,离开教师"); + }, "Thread --> " + i).start(); + } + while (Thread.activeCount() > 2) { + try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } + System.out.println(Thread.currentThread().getName() + " ====班长最后走人"); + } + } + + public static void countDownLatchTest() throws InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(6); + for (int i = 0; i < 6; i++) { + new Thread(() -> { + System.out.println(Thread.currentThread().getName() + " 上完自习,离开教师"); + countDownLatch.countDown(); + }, "Thread --> " + i).start(); + } + countDownLatch.await(); + System.out.println(Thread.currentThread().getName() + " ====班长最后走人"); + } +} +``` + +#### CyclicBarrier + +CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 + +```java +public class CyclicBarrierDemo { + public static void main(String[] args) { + cyclicBarrierTest(); + } + + public static void cyclicBarrierTest() { + CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { + System.out.println("====召唤神龙===="); + }); + for (int i = 0; i < 7; i++) { + final int tempInt = i; + new Thread(() -> { + System.out.println(Thread.currentThread().getName() + " 收集到第" + tempInt + "颗龙珠"); + try { + cyclicBarrier.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (BrokenBarrierException e) { + e.printStackTrace(); + } + }, "" + i).start(); + } + } +} +``` + +当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是`dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。 + +```java + // 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。 + private int count; + /** + * Main barrier code, covering the various policies. + */ + private int dowait(boolean timed, long nanos) + throws InterruptedException, BrokenBarrierException, + TimeoutException { + final ReentrantLock lock = this.lock; + // 锁住 + lock.lock(); + try { + final Generation g = generation; + + if (g.broken) + throw new BrokenBarrierException(); + + // 如果线程中断了,抛出异常 + if (Thread.interrupted()) { + breakBarrier(); + throw new InterruptedException(); + } + // cout减1 + int index = --count; + // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件 + if (index == 0) { // tripped + boolean ranAction = false; + try { + final Runnable command = barrierCommand; + if (command != null) + command.run(); + ranAction = true; + // 将 count 重置为 parties 属性的初始化值 + // 唤醒之前等待的线程 + // 下一波执行开始 + nextGeneration(); + return 0; + } finally { + if (!ranAction) + breakBarrier(); + } + } + + // loop until tripped, broken, interrupted, or timed out + for (;;) { + try { + if (!timed) + trip.await(); + else if (nanos > 0L) + nanos = trip.awaitNanos(nanos); + } catch (InterruptedException ie) { + if (g == generation && ! g.broken) { + breakBarrier(); + throw ie; + } else { + // We're about to finish waiting even if we had not + // been interrupted, so this interrupt is deemed to + // "belong" to subsequent execution. + Thread.currentThread().interrupt(); + } + } + + if (g.broken) + throw new BrokenBarrierException(); + + if (g != generation) + return index; + + if (timed && nanos <= 0L) { + breakBarrier(); + throw new TimeoutException(); + } + } + } finally { + lock.unlock(); + } + } + +``` + +总结:`CyclicBarrier` 内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 + +#### Semaphore + +**synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。** + +```java +public class SemaphoreDemo { + public static void main(String[] args) { + Semaphore semaphore = new Semaphore(3);// 模拟三个停车位 + for (int i = 0; i < 6; i++) { // 模拟6部汽车 + new Thread(() -> { + try { + semaphore.acquire(); + System.out.println(Thread.currentThread().getName() + " 抢到车位"); + // 停车3s + try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } + System.out.println(Thread.currentThread().getName() + " 停车3s后离开车位"); + } catch (Exception e) { + e.printStackTrace(); + } finally { + semaphore.release(); + } + }, "Car " + i).start(); + } + } +} +``` + +### 阻塞队列 + +- ArrayBlockingQueue:由数组结构组成的有界阻塞队列. +- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值Integer>MAX_VALUE)阻塞队列. +- PriorityBlockingQueue:支持优先级排序的无界阻塞队列. +- DelayQueue:使用优先级队列实现的延迟无界阻塞队列. +- SynchronousQueue:不存储元素的阻塞队列,也即是单个元素的队列. +- LinkedTransferQueue:由链表结构组成的无界阻塞队列. +- LinkedBlockingDuque:由链表结构组成的双向阻塞队列. +- 抛出异常方法:add remove +- 不抛异常:offer poll +- 阻塞 put take +- 带时间 offer poll + +#### 生产者消费者 + +synchronized版本的生产者和消费者,比较繁琐 + +```java +public class ProdConsumerSynchronized { + + private final LinkedList lists = new LinkedList<>(); + + public synchronized void put(String s) { + while (lists.size() != 0) { // 用while怕有存在虚拟唤醒线程 + // 满了, 不生产了 + try { + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + lists.add(s); + System.out.println(Thread.currentThread().getName() + " " + lists.peekFirst()); + this.notifyAll(); // 这里可是通知所有被挂起的线程,包括其他的生产者线程 + } + + public synchronized void get() { + while (lists.size() == 0) { + try { + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + System.out.println(Thread.currentThread().getName() + " " + lists.removeFirst()); + this.notifyAll(); // 通知所有被wait挂起的线程 用notify可能就死锁了。 + } + + public static void main(String[] args) { + ProdConsumerSynchronized prodConsumerSynchronized = new ProdConsumerSynchronized(); + + // 启动消费者线程 + for (int i = 0; i < 5; i++) { + new Thread(prodConsumerSynchronized::get, "ConsA" + i).start(); + } + + // 启动生产者线程 + for (int i = 0; i < 5; i++) { + int tempI = i; + new Thread(() -> { + prodConsumerSynchronized.put("" + tempI); + }, "ProdA" + i).start(); + } + } +} +``` + +ReentrantLock + +```java +public class ProdConsumerReentrantLock { + + private LinkedList lists = new LinkedList<>(); + + private Lock lock = new ReentrantLock(); + + private Condition prod = lock.newCondition(); + + private Condition cons = lock.newCondition(); + + public void put(String s) { + lock.lock(); + try { + // 1. 判断 + while (lists.size() != 0) { + // 等待不能生产 + prod.await(); + } + // 2.干活 + lists.add(s); + System.out.println(Thread.currentThread().getName() + " " + lists.peekFirst()); + // 3. 通知 + cons.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public void get() { + lock.lock(); + try { + // 1. 判断 + while (lists.size() == 0) { + // 等待不能消费 + cons.await(); + } + // 2.干活 + System.out.println(Thread.currentThread().getName() + " " + lists.removeFirst()); + // 3. 通知 + prod.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public static void main(String[] args) { + ProdConsumerReentrantLock prodConsumerReentrantLock = new ProdConsumerReentrantLock(); + for (int i = 0; i < 5; i++) { + int tempI = i; + new Thread(() -> { + prodConsumerReentrantLock.put(tempI + ""); + }, "ProdA" + i).start(); + } + for (int i = 0; i < 5; i++) { + new Thread(prodConsumerReentrantLock::get, "ConsA" + i).start(); + } + } +} +``` + +BlockingQueue + +```java +public class ProdConsumerBlockingQueue { + + private volatile boolean flag = true; + + private AtomicInteger atomicInteger = new AtomicInteger(); + + BlockingQueue blockingQueue = null; + + public ProdConsumerBlockingQueue(BlockingQueue blockingQueue) { + this.blockingQueue = blockingQueue; + } + + public void myProd() throws Exception { + String data = null; + boolean retValue; + while (flag) { + data = atomicInteger.incrementAndGet() + ""; + retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS); + if (retValue) { + System.out.println(Thread.currentThread().getName() + " 插入队列" + data + " 成功"); + } else { + System.out.println(Thread.currentThread().getName() + " 插入队列" + data + " 失败"); + } + TimeUnit.SECONDS.sleep(1); + } + System.out.println(Thread.currentThread().getName() + " 大老板叫停了,flag=false,生产结束"); + } + + public void myConsumer() throws Exception { + String result = null; + while (flag) { + result = blockingQueue.poll(2, TimeUnit.SECONDS); + if (null == result || result.equalsIgnoreCase("")) { + flag = false; + System.out.println(Thread.currentThread().getName() + " 超过2s没有取到蛋糕,消费退出"); + return; + } + System.out.println(Thread.currentThread().getName() + " 消费队列" + result + "成功"); + } + } + + public void stop() { + flag = false; + } + + public static void main(String[] args) { + ProdConsumerBlockingQueue prodConsumerBlockingQueue = new ProdConsumerBlockingQueue(new ArrayBlockingQueue<>(10)); + new Thread(() -> { + System.out.println(Thread.currentThread().getName() + " 生产线程启动"); + try { + prodConsumerBlockingQueue.myProd(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "Prod").start(); + + new Thread(() -> { + System.out.println(Thread.currentThread().getName() + " 消费线程启动"); + try { + prodConsumerBlockingQueue.myConsumer(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "Consumer").start(); + + try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } + System.out.println("5s后main叫停,线程结束"); + prodConsumerBlockingQueue.stop(); + } +} +``` + +### 线程池 + +#### 线程池的好处 + +- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 +- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 +- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +#### FixedThreadPool + +`FixedThreadPool` 被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现: + +```java + /** + * 创建一个可重用固定数量线程的线程池 + */ + public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { + return new ThreadPoolExecutor(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + threadFactory); + } +``` + +**从上面源代码可以看出新创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。** + +1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务; +2. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 `LinkedBlockingQueue`; +3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 `LinkedBlockingQueue` 中获取任务来执行; + +不推荐使用 + +**`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :** + +1. 当线程池中的线程数达到 `corePoolSize` 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize; +2. 由于使用无界队列时 `maximumPoolSize` 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 `FixedThreadPool`的源码可以看出创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 被设置为同一个值。 +3. 由于 1 和 2,使用无界队列时 `keepAliveTime` 将是一个无效参数; +4. 运行中的 `FixedThreadPool`(未执行 `shutdown()`或 `shutdownNow()`)不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。 + +#### SingleThreadExecutor + +```java + /** + *返回只有一个线程的线程池 + */ + public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + threadFactory)); + } +``` + +和上面一个差不多,只不过core和max都被设置为1 + +#### CachedThreadPool + +```java + /** + * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。 + */ + public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue(), + threadFactory); + } +``` + +`CachedThreadPool` 的` corePoolSize` 被设置为空(0),`maximumPoolSize `被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 `maximumPool` 中线程处理任务的速度时,`CachedThreadPool` 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。 + +1. 首先执行 `SynchronousQueue.offer(Runnable task)` 提交任务到任务队列。如果当前 `maximumPool` 中有闲线程正在执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`,那么主线程执行 offer 操作与空闲线程执行的 `poll` 操作配对成功,主线程把任务交给空闲线程执行,`execute()`方法执行完成,否则执行下面的步骤 2; +2. 当初始 `maximumPool` 为空,或者 `maximumPool` 中没有空闲线程时,将没有线程执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`。这种情况下,步骤 1 将失败,此时 `CachedThreadPool` 会创建新线程执行任务,execute 方法执行完成; + +#### ScheduledThreadPoolExecutor省略,基本不会用 + +#### ThreadPoolExecutor(重点) + +```java + /** + * 用给定的初始参数创建一个新的ThreadPoolExecutor。 + */ + public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 + int maximumPoolSize,//线程池的最大线程数 + long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 + TimeUnit unit,//时间单位 + BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 + ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 + RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 + ) { + if (corePoolSize < 0 || + maximumPoolSize <= 0 || + maximumPoolSize < corePoolSize || + keepAliveTime < 0) + throw new IllegalArgumentException(); + if (workQueue == null || threadFactory == null || handler == null) + throw new NullPointerException(); + this.corePoolSize = corePoolSize; + this.maximumPoolSize = maximumPoolSize; + this.workQueue = workQueue; + this.keepAliveTime = unit.toNanos(keepAliveTime); + this.threadFactory = threadFactory; + this.handler = handler; + } +``` + +**`ThreadPoolExecutor` 3 个最重要的参数:** + +- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 +- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 +- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。 + +`ThreadPoolExecutor`其他常见参数: + +- **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; +- **`unit`** : `keepAliveTime` 参数的时间单位。 +- **`threadFactory`** :executor 创建新线程的时候会用到。 +- **`handler`** :饱和策略。关于饱和策略下面单独介绍一下. + +![线程池各个参数的关系](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/线程池各个参数的关系.jpg) + +如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: + +- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 +- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 +- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 +- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 + +> Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。) + +> Executors 返回线程池对象的弊端如下: +> +> - **`FixedThreadPool` 和 `SingleThreadExecutor`** : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。 +> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 + +```java +public class ThreadPoolExecutorDemo { + public static void main(String[] args) { + ExecutorService threadpools = new ThreadPoolExecutor( + 3, + 5, + 1l, + TimeUnit.SECONDS, + new LinkedBlockingDeque<>(3), + Executors.defaultThreadFactory(), + new ThreadPoolExecutor.AbortPolicy()); +//new ThreadPoolExecutor.AbortPolicy(); +//new ThreadPoolExecutor.CallerRunsPolicy(); +//new ThreadPoolExecutor.DiscardOldestPolicy(); +//new ThreadPoolExecutor.DiscardPolicy(); + try { + for (int i = 0; i < 8; i++) { + threadpools.execute(() -> { + System.out.println(Thread.currentThread().getName() + "\t办理业务"); + }); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + threadpools.shutdown(); + } + } +} +``` + +### Java锁机制 + +#### 公平锁/非公平锁 + +**公平锁指多个线程按照申请锁的顺序来获取锁。非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象(很长时间都没获取到锁-非洲人...),ReentrantLock,了解一下。** + +#### 可重入锁 + +**可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,典型的synchronized,了解一下** + +```java +synchronized void setA() throws Exception { + Thread.sleep(1000); + setB(); // 因为获取了setA()的锁,此时调用setB()将会自动获取setB()的锁,如果不自动获取的话方法B将不会执行 +} +synchronized void setB() throws Exception { + Thread.sleep(1000); +} +``` + +#### 独享锁/共享锁 + +- 独享锁:是指该锁一次只能被一个线程所持有。 +- 共享锁:是该锁可被多个线程所持有。 + +#### 互斥锁/读写锁 + +**上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是其具体的实现** + +#### 乐观锁/悲观锁 + +1. **乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待兵法同步的角度。** +2. **悲观锁认为对于同一个人数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出现问题。** +3. **乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作时没有事情的。** +4. **悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁带来大量的性能提升。** +5. **悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子类操作的更新。重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁** + +#### 分段锁 + +1. **分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来哦实现高效的并发操作。** +2. **以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是ReentrantLock(Segment继承了ReentrantLock)** +3. **当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。** +4. **分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。** + +#### 偏向锁/轻量级锁/重量级锁 + +1. **这三种锁是锁的状态,并且是针对Synchronized。在Java5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。** +2. **偏向锁的适用场景:始终只有一个线程在执行代码块,在它没有执行完释放锁之前,没有其它线程去执行同步快,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;在有锁竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向锁的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。** +3. **轻量级锁是指当锁是偏向锁的时候,被另一个线程锁访问,偏向锁就会升级为轻量级锁,其他线程会通过自选的形式尝试获取锁,不会阻塞,提高性能。** +4. **重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。** + +#### 自旋锁 + +1. **在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。** +2. **自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。** +3. **自旋锁尽可能的减少线程的阻塞,适用于锁的竞争不激烈,且占用锁时间非常短的代码来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。** +4. **但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适用使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu的线程又不能获取到cpu,造成cpu的浪费。** + +#### Java锁总结 + +**Java锁机制可归为Sychornized锁和Lock锁两类。Synchronized是基于JVM来保证数据同步的,而Lock则是硬件层面,依赖特殊的CPU指令来实现数据同步的。** + +- Synchronized是一个非公平、悲观、独享、互斥、可重入的重量级锁。 +- ReentrantLock是一个默认非公平但可实现公平的、悲观、独享、互斥、可重入、重量级锁。 +- ReentrantReadWriteLock是一个默认非公平但可实现公平的、悲观、写独享、读共享、读写、可重入、重量级锁。 + diff --git "a/Java/crazy/Java\351\233\206\345\220\210.md" "b/Java/crazy/Java\351\233\206\345\220\210.md" new file mode 100644 index 00000000..e7d9f323 --- /dev/null +++ "b/Java/crazy/Java\351\233\206\345\220\210.md" @@ -0,0 +1,1041 @@ +> 个人感觉掌握常用的集合类,看其中的源码即可,有很多其实都差不多的,把个别不同的源码多看看,其实就是增删查 +> +> 比如,常见的ArrayList、LinkedList、HashMap和ConcurrentHashMap经常被问到的多准备准备。 +> +> 这一块就是看源码分析,没别的 + +### ArrayList + +#### 概述 + +- *ArrayList*实现了*List*接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入`null`元素,底层通过**数组实现**。 +- 除该类未实现同步外,其余跟*Vector*大致相同。 +- 每个*ArrayList*都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。 +- 当向容器中添加元素时,如果容量不足,**容器会自动增大底层数组的大小**。 +- 前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。 + +![](https://www.pdai.tech/_images/collection/ArrayList_base.png) + +- size(), isEmpty(), get(), set()方法均能在**常数时间**内完成,add()方法的时间开销跟插入位置有关,addAll()方法的时间开销跟添加元素的个数成正比。其余方法大都是线性时间。 +- 为追求效率,ArrayList没有实现同步(**synchronized**),如果需要多个线程并发访问,用户可以手动同步,也可使用Vector替代。 + +#### 实现 + +##### 底层数据结构 + +```java +transient Object[] elementData; // Object 数组 +private int size; // 大小 +``` + +##### 构造函数 + +```java + // 参数为容量的构造参数 + public ArrayList(int initialCapacity) { + if (initialCapacity > 0) { + this.elementData = new Object[initialCapacity]; + } else if (initialCapacity == 0) { + this.elementData = EMPTY_ELEMENTDATA; // 默认 + } else { + throw new IllegalArgumentException("Illegal Capacity: "+ + initialCapacity); + } + } + // 无参的构造参数 + public ArrayList() { + this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 默认容量 + } + + public ArrayList(Collection c) { + elementData = c.toArray(); + if ((size = elementData.length) != 0) { + // c.toArray might (incorrectly) not return Object[] (see 6260652) + if (elementData.getClass() != Object[].class) + elementData = Arrays.copyOf(elementData, size, Object[].class); + } else { + // replace with empty array. + this.elementData = EMPTY_ELEMENTDATA; + } + } +``` + +##### 自动扩容 + +```java + public void ensureCapacity(int minCapacity) { + int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) + // any size if not default element table + ? 0 + // larger than default for default empty table. It's already + // supposed to be at default size. + : DEFAULT_CAPACITY; + + if (minCapacity > minExpand) { + ensureExplicitCapacity(minCapacity); + } + } + + private void ensureCapacityInternal(int minCapacity) { + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); + } + + ensureExplicitCapacity(minCapacity); + } + + private void ensureExplicitCapacity(int minCapacity) { + modCount++; + + // overflow-conscious code + if (minCapacity - elementData.length > 0) + grow(minCapacity); + } + + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + + private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); + } + + private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) // overflow + throw new OutOfMemoryError(); + return (minCapacity > MAX_ARRAY_SIZE) ? + Integer.MAX_VALUE : + MAX_ARRAY_SIZE; + } +``` + + + +- 每当向数组中添加元素时,都要去检查添加后**元素的个数是否会超出当前数组的长度**,如果超出,**数组将会进行扩容**,以满足添加数据的需求。 +- 数组扩容通过一个公开的方法`ensureCapacity(int minCapacity)`来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。 +- 数组进行扩容时,会将老数组中的元素重新**拷贝**一份到新的数组中,每次数组容量的增长大约是其原容量的**1.5倍**。 +- 这种操作的代价是很高的,因此在实际使用时,我们应该**尽量避免数组容量的扩张**。 +- 当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就**指定其容量**,以避免数组扩容的发生。 +- 或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。 + +![扩容](https://www.pdai.tech/_images/collection/ArrayList_grow.png) + +##### add() + +是向容器中添加新元素,这可能会导致*capacity*不足,因此在添加元素之前,都需要进行剩余空间检查,如果需要则自动扩容。扩容操作最终是通过`grow()`方法完成的。 + +```java + public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! // 多线程容易出问题 + elementData[size++] = e; // 这里也是 + return true; + } + + public void add(int index, E element) { + rangeCheckForAdd(index); + + ensureCapacityInternal(size + 1); // Increments modCount!! + System.arraycopy(elementData, index, elementData, index + 1, + size - index); + elementData[index] = element; + size++; + } +``` + +##### get() + +`get()`方法同样很简单,唯一要注意的是由于底层数组是Object[],得到元素后需要进行类型转换。 + +```java +public E get(int index) { + rangeCheck(index); + return (E) elementData[index];//注意类型转换 +} +``` + +##### remove() + +`remove()`方法也有两个版本,一个是`remove(int index)`删除指定位置的元素,另一个是`remove(Object o)`删除第一个满足`o.equals(elementData[index])`的元素。删除操作是`add()`操作的逆过程,需要将删除点之后的元素向前移动一个位置。需要注意的是为了让**GC**起作用,必须显式的为最后一个位置赋`null`值。 + +```java +public E remove(int index) { + rangeCheck(index); + modCount++; + E oldValue = elementData(index); + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, numMoved); + elementData[--size] = null; //清除该位置的引用,让GC起作用 + return oldValue; +} +``` + +##### indexOf() + +**循环遍历用equals** + +```java + public int indexOf(Object o) { + if (o == null) { + for (int i = 0; i < size; i++) + if (elementData[i]==null) + return i; + } else { + for (int i = 0; i < size; i++) + if (o.equals(elementData[i])) + return i; + } + return -1; + } +``` + + + +##### Fail-Fast机制 + +ArrayList也采用了**快速失败的机制**,**通过记录modCount参数来实现**。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。 + +##### 多线程问题 + +[请参考->个人吐血系列-总结java多线程](http://dreamcat.ink/2020/03/25/ge-ren-tu-xie-xi-lie-zong-jie-java-duo-xian-cheng/#toc-heading-14) + +### LinkedList + +#### 概述 + +**LinkedList**同时实现了**list**接口和**Deque**接口,也就是说它既可以看作一个**顺序容器**,又可以看作**一个队列(Queue)**,同时又可以看作**一个栈(Stack)**。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(它是个接口名字)。**关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能**。 + +![LinkedList](https://www.pdai.tech/_images/collection/LinkedList_base.png) + +LinkedList的实现方式决定了所有跟**下标相关的操作都是线性时间**,而在**首段或者末尾删除元素只需要常数时间**。为追求效率LinkedList没有实现同步(synchronized),如果需要多个线程并发访问,可以先采用`Collections.synchronizedList()`方法对其进行包装。 + +#### 实现 + +##### 底层数据接口 + +```java + transient int size = 0; + transient Node first; // 经常用到 + transient Node last; // 也经常用到 + // Node是私有的内部类 + private static class Node { + E item; + Node next; + Node prev; + + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } + } +``` + +LinkedList底层**通过双向链表实现**,本节将着重讲解插入和删除元素时双向链表的维护过程,也即是之间解跟List接口相关的函数。双向链表的每个节点用内部类Node表示。LinkedList通过`first`和`last`引用分别指向链表的第一个和最后一个元素。注意这里没有所谓的哑元,当链表为空的时候`first`和`last`都指向`null`。 + +##### 构造函数 + +```java + public LinkedList() { + } + + public LinkedList(Collection c) { + this(); + addAll(c); + } +``` + +##### getFirst(),getLast() + +**本身在数据结构中,维护了first和last的变量,因此其实挺简单的**。 + +```java + public E getFirst() { + final Node f = first; // 获取第一个元素 + if (f == null) + throw new NoSuchElementException(); + return f.item; + } + + public E getLast() { + final Node l = last; // 获取最后一个元素 + if (l == null) + throw new NoSuchElementException(); + return l.item; + } +``` + +##### removeFirst(),removeLast(),remove(e),remove(index) + +![remove](https://www.pdai.tech/_images/collection/LinkedList_remove.png) + +**删除元素** - 指的是删除第一次出现的这个元素, 如果没有这个元素,则返回false;判读的依据是`equals`方法, 如果equals,则直接unlink这个node;由于LinkedList可存放null元素,故也可以删除第一次出现null的元素; + +```java + public boolean remove(Object o) { + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) { // 循环遍历 用equals判断 + unlink(x); + return true; + } + } + } + return false; + } + E unlink(Node x) { + // assert x != null; + final E element = x.item; // 当前元素 + final Node next = x.next; // 指向下一个节点 + final Node prev = x.prev; // 上一个节点 + + if (prev == null) {// 第一个元素,如果该节点的上节点为空,那么就把该节点的下个节点放在第一个位置 + first = next; + } else { + prev.next = next; // 不为空,则把上个节点指向该节点的下个节点 + x.prev = null; + } + + if (next == null) {// 最后一个元素 + last = prev; + } else { + next.prev = prev; + x.next = null; + } + + x.item = null; // GC + size--; + modCount++; + return element; + } +``` + +`remove(int index)`使用的是下标计数, 只需要判断该index是否有元素即可,如果有则直接unlink这个node。 + +```java + public E remove(int index) { + checkElementIndex(index); + return unlink(node(index)); + } +``` + +`removeFirst()`其实挺简单的 + +```java + public E removeFirst() { + final Node f = first; // 拿到firs直接unlink + if (f == null) + throw new NoSuchElementException(); + return unlinkFirst(f); + } + + private E unlinkFirst(Node f) { + // assert f == first && f != null; + final E element = f.item; // first e + final Node next = f.next; // first 没有 pre , 只有next + f.item = null; + f.next = null; // help GC + first = next; // 让first指向next + if (next == null) // 如果next为空,则当前元素已经是最后一个元素了,那么last自然为空 + last = null; + else + next.prev = null; // 如果不为空,next的上个节点指向为空 + size--; + modCount++; + return element; + } +``` + +`removLast()`其实挺简单的,和上面差不多 + +```java + public E removeLast() { + final Node l = last; + if (l == null) + throw new NoSuchElementException(); + return unlinkLast(l); + } + + private E unlinkLast(Node l) { + // assert l == last && l != null; + final E element = l.item; + final Node prev = l.prev; + l.item = null; + l.prev = null; // help GC + last = prev; + if (prev == null) + first = null; + else + prev.next = null; + size--; + modCount++; + return element; + } +``` + +##### add() + +```java + public boolean add(E e) { + linkLast(e); // 在链表末尾插入元素,所以常数时间 + return true; + } + + void linkLast(E e) { // 其实就是最后面修改引用 + final Node l = last; + final Node newNode = new Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; + } +``` + +`add(int index, E element)`, 当index==size时,等同于add(E e); 如果不是,则分两步:1.先根据index找到要插入的位置,即node(index)方法;2.修改引用,完成插入操作,其实想就是遍历插入。 + +##### indexOf() + +循环遍历equals,找到对应的下标 + +```java + public int indexOf(Object o) { + int index = 0; // 维护index + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) + return index; + index++; + } + } else { + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) // 用equals + return index; + index++; + } + } + return -1; + } +``` + +### HashMap(面试常问) + +众所周知,HashMap的底层结构是**数组和链表**组成的,不过在jdk1.7和jdk1.8中具体实现略有不同。 + +![底层结构](https://i.loli.net/2019/05/08/5cd1d2be77958.jpg) + +#### 1.7的实现 + +##### 成员变量 + +这里,就不贴1.7版本的源码了,因此贴图。 + +![](https://i.loli.net/2019/05/08/5cd1d2bfd6aba.jpg) + +介绍成员变量: + +1. **初始化桶大小**,因为底层是数组,所以这是数组默认的大小。 +2. **桶最大值**。 +3. 默认的**负载因子**(0.75) +4. table真正存放数据的数组。 +5. map存放数量的大小 +6. 桶大小,可在构造函数时显式指定。 +7. 负载因子,可在构造函数时显式指定。 + +##### 负载因子 + +```java +public HashMap() { + this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); // 桶和负载因子 +} +public HashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + + initialCapacity); + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; // 只能获取默认的最大值 + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + + loadFactor); + this.loadFactor = loadFactor; + threshold = initialCapacity; + in +``` + +- **给定的默认容量为16,负载因子为0.75**. +- Map在使用过程中不断的往里面存放数据,当数量达到了`16 * 0.75 = 12`就需要将当前16的容量进行扩容,而扩容这个过程涉及到`rehash`(重新哈希)、复制数据等操作,所有非常消耗性能。 +- 因此通常建议能提前预估HashMap的大小最好,尽量的减少扩容带来的额外性能损耗。 +- 关于这部分后期专门出一篇文章进行讲解。 + +##### Entry + +![Entry](https://i.loli.net/2019/05/08/5cd1d2c08e693.jpg) + +Entry是**Hashmap中的一个内部类**,从他的成员变量很容易看出: + +- key就是写入时的键 +- value自然就是值 +- 开始的时候就提到HashMap是由数组和链表组成,所以这个next就是用于实现链表结构 +- hash存放的是当前key的hashcode + +##### put(重点来了) + +```java +public V put(K key, V value) { + if (table == EMPTY_TABLE) { + inflateTable(threshold); // 判断数组是否需要初始化 + } + if (key == null) + return putForNullKey(value); // 判断key是否为空 + int hash = hash(key); // 计算hashcode + int i = indexFor(hash, table.length); // 计算桶 + for (Entry e = table[i]; e != null; e = e.next) { + Object k; + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 遍历判断链表中的key和hashcode是否相等,等就替换 + V oldValue = e.value; + e.value = value; + e.recordAccess(this); + return oldValue; + } + } + modCount++; + addEntry(hash, key, value, i); // 没有就添加新的呗 + return null; +} +``` + +- 判断当前数组是否需要初始化 +- 如果key为空,则put一个空值进去 +- 根据key计算hashcode +- 根据计算的hashcode定位index的桶 +- 如果桶是一个链表,则需要遍历判断里面的hashcode、key是否和传入的key相等,如果相等则进行覆盖,并返回原来的值 +- 如果桶是空的,说明当前位置没有数据存入,此时新增一个Entry对象写入当前位置。 + +```java +void addEntry(int hash, K key, V value, int bucketIndex) { + if ((size >= threshold) && (null != table[bucketIndex])) {// 是否扩容 + resize(2 * table.length); // 两倍扩容 重新哈希 + hash = (null != key) ? hash(key) : 0; + bucketIndex = indexFor(hash, table.length); + } + createEntry(hash, key, value, bucketIndex); +} +void createEntry(int hash, K key, V value, int bucketIndex) { + Entry e = table[bucketIndex]; + table[bucketIndex] = new Entry<>(hash, key, value, e); + size++; +} +``` + +- 当调用addEntry写入Entry时需要判断是否需要扩容 +- 如果需要就进行**两倍扩充**,并将当前的key重新hash并定位。 +- 而在createEntry中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。 + +##### get + +```java +public V get(Object key) { + if (key == null) // 判断key是否为空 + return getForNullKey(); // 为空,就返回空值 + Entry entry = getEntry(key); // get entry + return null == entry ? null : entry.getValue(); +} +final Entry getEntry(Object key) { + if (size == 0) { + return null; + } + int hash = (key == null) ? 0 : hash(key); //根据key和hashcode + for (Entry e = table[indexFor(hash, table.length)]; //循环遍历equals key拿值 + e != null; + e = e.next) { + Object k; + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } + return null; +} +``` + +- 首先根据key计算hashcode,然后定位具体的桶 +- 判断该位置是否为链表 +- 不是链接就根据key和hashcode是否相等来返回值 +- 为链表则需要遍历直到key和hashcode相等就返回值 +- 啥都没得,就返回null + +#### 1.8的实现 + +不知道 1.7 的实现大家看出需要优化的点没有? + +其实一个很明显的地方就是链表 + +**当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 `O(N)`。** + +![](https://i.loli.net/2019/05/08/5cd1d2c1c1cd7.jpg) + +##### 成员变量 + +```java +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +/** + * The maximum capacity, used if a higher value is implicitly specified + * by either of the constructors with arguments. + * MUST be a power of two <= 1<<30. + */ +static final int MAXIMUM_CAPACITY = 1 << 30; +/** + * The load factor used when none specified in constructor. + */ +static final float DEFAULT_LOAD_FACTOR = 0.75f; +static final int TREEIFY_THRESHOLD = 8; +transient Node[] table; +/** + * Holds cached entrySet(). Note that AbstractMap fields are used + * for keySet() and values(). + */ +transient Set> entrySet; +/** + * The number of key-value mappings contained in this map. + */ +transient int size; +``` + +- `TREEIFY_THRESHOLD` 用于判断是否需要将链表转换为红黑树的阈值。 +- HashEntry 修改为 Node。 +- Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 `key value hashcode next` 等数据。 + +##### put + +```java + /** + * Implements Map.put and related methods + * + * @param hash hash for key + * @param key the key + * @param value the value to put + * @param onlyIfAbsent if true, don't change existing value + * @param evict if false, the table is in creation mode. + * @return previous value, or null if none + */ + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + if ((tab = table) == null || (n = tab.length) == 0) // 1. 判断当前桶是否为空,空的就需要初始化(resize中会判断是否进行初始化) + n = (tab = resize()).length; + if ((p = tab[i = (n - 1) & hash]) == null) // 2. 根据当前key的hashcode定位到具体的桶中并判断是否为空,为空则表明没有Hash冲突,就直接在当前位置创建一个新桶 + tab[i] = newNode(hash, key, value, null); + else { + Node e; K k; + if (p.hash == hash && // 3. 如果当前桶有值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,在第8步的时候会统一进行赋值及返回 + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + else if (p instanceof TreeNode) // 4. 如果当前桶为红黑树,那就要按照红黑树的方式写入数据 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { // 5. 如果是个链表,就需要将当前的key、value封装称一个新节点写入到当前桶的后面形成链表。 + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 6. 接着判断当前链表的大小是否大于预设的阈值,大于就要转换成为红黑树 +- + treeifyBin(tab, hash); + break; + } + if (e.hash == hash && // 7. 如果在遍历过程中找到key相同时直接退出遍历。 + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + if (e != null) { // existing mapping for key 8. 如果`e != null`就相当于存在相同的key,那就需要将值覆盖。 + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; + if (++size > threshold) // 9. 最后判断是否需要进行扩容。 + resize(); + afterNodeInsertion(evict); + return null; + } +``` + +- 判断当前桶是否为空,空的就需要初始化(resize中会判断是否进行初始化) +- 根据当前key的hashcode定位到具体的桶中并判断是否为空,为空则表明没有Hash冲突,就直接在当前位置创建一个新桶 +- 如果当前桶有值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,在第8步的时候会统一进行赋值及返回 +- 如果当前桶为**红黑树**,那就要按照红黑树的方式写入数据 +- 如果是个链表,就需要将当前的key、value封装称一个新节点写入到当前桶的后面形成链表。 +- 接着判断当前链表的大小是否**大于预设的阈值**,大于就要转换成为**红黑树** +- 如果在遍历过程中找到key相同时直接退出遍历。 +- 如果`e != null`就相当于存在相同的key,那就需要将值覆盖。 +- 最后判断是否需要进行扩容。 + +##### get + +```java +public V get(Object key) { + Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} +final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + if ((e = first.next) != null) { + if (first instanceof TreeNode) + return ((TreeNode)first).getTreeNode(hash, key); + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +``` + +- 首先将key hash之后取得所定位的桶 +- 如果桶为空,则直接返回null +- 否则判断桶的第一个位置(有可能是链表、红黑树)的key是否为查询的key,是就直接返回value +- 如果第一个不匹配,则判断它的下一个是红黑树还是链表 +- 红黑树就按照树的查找方式返回值 +- 不然就按照链表的方式遍历匹配返回值 + +**从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 `O(logn)`。** + +#### 问题 + +但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现**死循环**。 + +```java +final HashMap map = new HashMap(); +for (int i = 0; i < 1000; i++) { + new Thread(new Runnable() { + @Override + public void run() { + map.put(UUID.randomUUID().toString(), ""); + } + }).start(); +} +``` + +- HashMap扩容的时候会调用resize()方法,就是这里的并发操作容易在一个桶上形成环形链表 +- 这样当获取一个不存在的key时,计算出的index正好是环形链表的下标就会出现死循环。 +- **但是1.7的头插法造成的问题,1.8改变了插入顺序,就解决了这个问题,但是为了内存可见性等安全性,还是需要ConCurrentHashMap** + +> 参考:hashMap死循环分析 +> +> [hashMap死循环分析](https://zhuanlan.zhihu.com/p/67915754) + +还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种: + +```java +Iterator> entryIterator = map.entrySet().iterator(); + while (entryIterator.hasNext()) { + Map.Entry next = entryIterator.next(); + System.out.println("key=" + next.getKey() + " value=" + next.getValue()); + } + +Iterator iterator = map.keySet().iterator(); + while (iterator.hasNext()){ + String key = iterator.next(); + System.out.println("key=" + key + " value=" + map.get(key)); + } +``` + +- **建议使用第一种,同时可以把key value取出**。 +- 第二种还需要通过key取一次key,效率较低。 + +### ConcurrentHashMap + +#### 1.7 + +![](https://i.loli.net/2019/05/08/5cd1d2c5ce95c.jpg) + +- Segment数组 +- HashEntry组成 +- 和HashMap一样,仍然是数组加链表 + +```java +/** + * Segment 数组,存放数据时首先需要定位到具体的 Segment 中。 + */ +final Segment[] segments; +transient Set keySet; +transient Set> entrySet; +``` + +Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下: + +```java +static final class Segment extends ReentrantLock implements Serializable { + private static final long serialVersionUID = 2249069246763182397L; + + // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶 + transient volatile HashEntry[] table; + transient int count; + transient int modCount; + transient int threshold; + final float loadFactor; + +} +``` + +![](https://i.loli.net/2019/05/08/5cd1d2c635c69.jpg) + +- 唯一的区别就是其中的核心数据如 value ,以及链表都是 `volatile` 修饰的,保证了获取时的可见性。 +- ConcurrentHashMap 采用了**分段锁**技术,其中 Segment 继承于 `ReentrantLock`。 +- 不会像HashTable那样不管是put还是get操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。 +- **每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment**。 + +##### put + +```java +public V put(K key, V value) { + Segment s; + if (value == null) + throw new NullPointerException(); + int hash = hash(key); + int j = (hash >>> segmentShift) & segmentMask; + if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck + (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment + s = ensureSegment(j); + return s.put(key, hash, value, false); +} +``` + +通过key定位到Segment,之后在对应的Segment中进行具体的put + +```java +final V put(K key, int hash, V value, boolean onlyIfAbsent) { + HashEntry node = tryLock() ? null : + scanAndLockForPut(key, hash, value); // 1. 加锁处理 + V oldValue; + try { + HashEntry[] tab = table; + int index = (tab.length - 1) & hash; + HashEntry first = entryAt(tab, index); + for (HashEntry e = first;;) { + if (e != null) { + K k; + if ((k = e.key) == key || // 2. 遍历该HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value + (e.hash == hash && key.equals(k))) { + oldValue = e.value; + if (!onlyIfAbsent) { + e.value = value; + ++modCount; + } + break; + } + e = e.next; + } + else { // 3. 不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容 + if (node != null) + node.setNext(first); + else + node = new HashEntry(hash, key, value, first); + int c = count + 1; + if (c > threshold && tab.length < MAXIMUM_CAPACITY) + rehash(node); + else + setEntryAt(tab, index, node); + ++modCount; + count = c; + oldValue = null; + break; + } + } + } finally { + unlock(); // 4. 解锁 + } + return oldValue; +} +``` + +- **虽然HashEntry中的value是用volatile关键字修饰的,但是并不能保证并发的原子性,所以put操作仍然需要加锁处理**。 + +- **首先第一步的时候会尝试获取锁,如果获取失败肯定就是其他线程存在竞争,则利用 `scanAndLockForPut()` 自旋获取锁**。 + + 1. 尝试获取自旋锁 + + 2. 如果重试的次数达到了`MAX_SCAN_RETRIES` 则改为**阻塞锁获取**,保证能获取成功。 + +总的来说: + +- 将当前的Segment中的table通过key的hashcode定位到HashEntry +- 遍历该HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value +- 不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容 +- 最后会解除在1中所获取当前Segment的锁。 + +##### get + +```java +public V get(Object key) { + Segment s; // manually integrate access methods to reduce overhead + HashEntry[] tab; + int h = hash(key); + long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; + if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && + (tab = s.table) != null) { + for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile + (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); + e != null; e = e.next) { + K k; + if ((k = e.key) == key || (e.hash == h && key.equals(k))) + return e.value; + } + } + return null; +} +``` + +- 只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。 +- 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。 +- ConcurrentHashMap 的 get 方法是非常高效的,**因为整个过程都不需要加锁**。 + +#### 1.8 + +**那就是查询遍历链表效率太低。** + +![](https://i.loli.net/2019/05/08/5cd1d2ce33795.jpg) + +**其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性** + +##### put + +```java + final V putVal(K key, V value, boolean onlyIfAbsent) { + if (key == null || value == null) throw new NullPointerException(); + int hash = spread(key.hashCode()); + int binCount = 0; + for (Node[] tab = table;;) { // 1. 根据key计算出hashcode + Node f; int n, i, fh; + if (tab == null || (n = tab.length) == 0) // 2. 判断是否需要进行初始化 + tab = initTable(); + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 3. f即为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功。 + if (casTabAt(tab, i, null, + new Node(hash, key, value, null))) + break; // no lock when adding to empty bin + } + else if ((fh = f.hash) == MOVED) // 4. 如果当前位置的`hashcode == MOVED == -1`,则需要进行扩容 + tab = helpTransfer(tab, f); + else { + V oldVal = null; + synchronized (f) { // 5. 如果都不满足,则利用synchronized锁写入数据 + if (tabAt(tab, i) == f) { + if (fh >= 0) { + binCount = 1; + for (Node e = f;; ++binCount) { + K ek; + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + oldVal = e.val; + if (!onlyIfAbsent) + e.val = value; + break; + } + Node pred = e; + if ((e = e.next) == null) { + pred.next = new Node(hash, key, + value, null); + break; + } + } + } + else if (f instanceof TreeBin) { + Node p; + binCount = 2; + if ((p = ((TreeBin)f).putTreeVal(hash, key, + value)) != null) { + oldVal = p.val; + if (!onlyIfAbsent) + p.val = value; + } + } + } + } + if (binCount != 0) { + if (binCount >= TREEIFY_THRESHOLD) // 6. 如果数量大于`TREEIFY_THRESHOLD` 则要转换为红黑树。 + treeifyBin(tab, i); + if (oldVal != null) + return oldVal; + break; + } + } + } + addCount(1L, binCount); + return null; + } +``` + +- 根据key计算出hashcode +- 判断是否需要进行初始化 +- f即为当前key定位出的Node,如果为空表示当前位置可以写入数据,**利用CAS尝试写入,失败则自旋保证成功**。 +- 如果当前位置的`hashcode == MOVED == -1`,则需要进行扩容 +- **如果都不满足,则利用synchronized锁写入数据** +- 如果数量大于`TREEIFY_THRESHOLD` 则要转换为**红黑树**。 + +##### get + +```java + public V get(Object key) { + Node[] tab; Node e, p; int n, eh; K ek; + int h = spread(key.hashCode()); + if ((tab = table) != null && (n = tab.length) > 0 && + (e = tabAt(tab, (n - 1) & h)) != null) { + if ((eh = e.hash) == h) { + if ((ek = e.key) == key || (ek != null && key.equals(ek))) + return e.val; + } + else if (eh < 0) + return (p = e.find(h, key)) != null ? p.val : null; + while ((e = e.next) != null) { + if (e.hash == h && + ((ek = e.key) == key || (ek != null && key.equals(ek)))) + return e.val; + } + } + return null; + } +``` + +- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 +- 如果是红黑树那就按照树的方式获取值。 +- 就不满足那就按照链表的方式遍历获取值。 + +1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(`O(logn)`),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。 + +### 总结 + +套路: + +- 谈谈你理解的 HashMap,讲讲其中的 get put 过程。 +- 1.8 做了什么优化? +- 是线程安全的嘛? +- 不安全会导致哪些问题? +- 如何解决?有没有线程安全的并发容器? +- ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做? + +> 创作不易哇,觉得有帮助的话,给个小小的star呗。github地址😁😁😁 \ No newline at end of file diff --git a/Java/crazy/MySQL.md b/Java/crazy/MySQL.md new file mode 100644 index 00000000..33017d7e --- /dev/null +++ b/Java/crazy/MySQL.md @@ -0,0 +1,354 @@ +--- + +title: 个人吐血系列-总结Mysql +date: 2020-03-30 15:13:08 +tags: db +categories: java-interview +--- + +> MySQL这一块的知识还是挺多的,问深度的话, 一般都是如何调优的,当然少不了MySQL的基础等知识。 + +![MySQL面试常见问题](http://media.dreamcat.ink/uPic/Mysql面试常见问题.png) + +### 数据库引擎innodb与myisam的区别 + +#### InnoDB + +是 MySQL 默认的**事务型**存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。 + +实现了四个标准的隔离级别,默认级别是**可重复读**(REPEATABLE READ)。在可重复读隔离级别下,**通过多版本并发控制(MVCC)+ 间隙锁(Next-Key Locking)防止幻影读。** + +主索引是**聚簇索引**,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。 + +内部做了很多优化,包括从磁盘读取数据时采用的**可预测性读**、能够加快读操作并且自动创建的**自适应哈希索引**、能够加速插入操作的插入缓冲区等。 + +支持真正的**在线热备份**。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 + +#### MyISAM + +设计简单,数据以**紧密格式存储**。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。 + +提供了大量的特性,包括**压缩表**、**空间数据索引**等。 + +**不支持事务**。 + +**不支持行级锁**,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 + +可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 + +如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 + +#### 比较 + +- **事务**: InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。 +- **并发**: MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 +- **外键**: InnoDB 支持外键。 +- **备份**: InnoDB 支持在线热备份。 +- **崩溃恢复**: MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 +- **其它特性**: MyISAM 支持压缩表和空间数据索引。 + +### MySQL是如何执行一条SQL的 + +![SQL执行的全部过程](http://media.dreamcat.ink/uPic/SQL执行的全部过程.png) + +**MySQL内部可以分为服务层和存储引擎层两部分:** + +1. **服务层包括连接器、查询缓存、分析器、优化器、执行器等**,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。 +2. **存储引擎层负责数据的存储和提取。**其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认的存储引擎。 + +**Server层按顺序执行sql的步骤为:** + +客户端请求->连接器(验证用户身份,给予权限) -> 查询缓存(存在缓存则直接返回,不存在则执行后续操作)->分析器(对SQL进行词法分析和语法分析操作) -> 优化器(主要对执行的sql优化选择最优的执行方案方法) -> 执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口)->去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果) + +**简单概括**: + +- **连接器**:管理连接、权限验证; +- **查询缓存**:命中缓存则直接返回结果; +- **分析器**:对SQL进行词法分析、语法分析;(判断查询的SQL字段是否存在也是在这步) +- **优化器**:执行计划生成、选择索引; +- **执行器**:操作引擎、返回结果; +- **存储引擎**:存储数据、提供读写接口。 + +### mysql的acid原理 + +**ACID嘛,原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)!** + +我们以从A账户转账50元到B账户为例进行说明一下ACID,四大特性。 + +#### 原子性 + +根据定义,原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。即要么转账成功,要么转账失败,是不存在中间的状态! + +**如果无法保证原子性会怎么样?** + +OK,就会出现**数据不一致**的情形,A账户减去50元,而B账户增加50元操作失败。系统将无故丢失50元~ + +#### 隔离性 + +根据定义,隔离性是指多个事务并发执行的时候,**事务内部的操作与其他事务是隔离的**,并发执行的各个事务之间不能互相干扰。 + +**如果无法保证隔离性会怎么样?** + +OK,假设A账户有200元,B账户0元。A账户往B账户转账两次,金额为50元,分别在两个事务中执行。如果无法保证隔离性,会出现下面的情形 + +![事务隔离性](http://media.dreamcat.ink/uPic/事务隔离性.png) + +如图所示,如果不保证隔离性,A扣款两次,而B只加款一次,凭空消失了50元,依然出现了**数据不一致**的情形! + +#### 持久性 + +根据定义,**持久性是指事务一旦提交,它对数据库的改变就应该是永久性的**。接下来的其他操作或故障不应该对其有任何影响。 + +**如果无法保证持久性会怎么样?** + +在MySQL中,为了解决CPU和磁盘速度不一致问题,MySQL是将磁盘上的数据加载到内存,对内存进行操作,然后再回写磁盘。好,假设此时宕机了,在内存中修改的数据全部丢失了,持久性就无法保证。 + +设想一下,系统提示你转账成功。但是你发现金额没有发生任何改变,此时数据出现了不合法的数据状态,我们将这种状态认为是**数据不一致**的情形。 + +#### 一致性 + +根据定义,一致性是指事务执行前后,数据处于一种合法的状态,这种状态是语义上的而不是语法上的。 那什么是合法的数据状态呢? oK,这个状态是满足预定的约束就叫做合法的状态,再通俗一点,这状态是由你自己来定义的。**满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的**! + +**如果无法保证一致性会怎么样?** + +- 例一:A账户有200元,转账300元出去,此时A账户余额为-100元。你自然就发现了此时数据是不一致的,为什么呢?因为你定义了一个状态,余额这列必须大于0。 +- 例二:A账户200元,转账50元给B账户,A账户的钱扣了,但是B账户因为各种意外,余额并没有增加。你也知道此时数据是不一致的,为什么呢?因为你定义了一个状态,要求A+B的余额必须不变。 + +#### mysql怎么保证一致性? + +OK,这个问题分为两个层面来说。 **从数据库层面**,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现AID三大特性,才有可能实现一致性。例如,原子性无法保证,显然一致性也无法保证。 + +但是,如果你在事务里故意写出违反约束的代码,一致性还是无法保证的。例如,你在转账的例子中,你的代码里故意不给B账户加钱,那一致性还是无法保证。因此,还必须从应用层角度考虑。 + +**从应用层面**,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据! + +#### mysql怎么保证原子性 + +OK,是利用Innodb的`undo log`。 `undo log`名为回滚日志,是实现原子性的关键,当事务回滚时能够**撤销所有已经成功执行的sql语句**,他需要记录你要回滚的相应日志信息。 例如 + +- (1)当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据 +- (2)当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作 +- (3)当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作 + +`undo log`记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。 + +#### mysql怎么保证持久性的 + +OK,是利用Innodb的`redo log`。 正如之前说的,Mysql是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。 *怎么解决这个问题?* 简单啊,事务提交前直接把数据写入磁盘就行啊。 *这么做有什么问题?* + +- 只修改一个页面里的一个字节,就要将整个页面刷入磁盘,太浪费资源了。毕竟一个页面16kb大小,你只改其中一点点东西,就要将16kb的内容刷入磁盘,听着也不合理。 +- 毕竟一个事务里的SQL可能牵涉到多个数据页的修改,而这些数据页可能不是相邻的,也就是属于随机IO。显然操作随机IO,速度会比较慢。 + +于是,决定采用`redo log`解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在`redo log`中记录这次操作。当事务提交的时候,会将`redo log`日志进行刷盘(`redo log`一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将`redo log`中的内容恢复到数据库中,再根据`undo log`和`binlog`内容决定回滚数据还是提交数据。 + +**采用redo log的好处?** + +其实好处就是将`redo log`进行刷盘比对数据页刷盘效率高,具体表现如下 + +- `redo log`体积小,毕竟只记录了哪一页修改了啥,因此体积小,刷盘快。 +- `redo log`是一直往末尾进行追加,属于顺序IO。效率显然比随机IO来的快。 + +#### mysql怎么保证隔离性 + +利用的是锁和MVCC机制。 + +### 并发事务带来的问题 + +#### 脏读 + +![脏读](https://www.pdai.tech/_images/pics/dd782132-d830-4c55-9884-cfac0a541b8e.png) + +#### 丢弃修改 + +T1 和 T2 两个事务都对一个数据进行修改,**T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改**。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。 + +![丢弃修改](https://www.pdai.tech/_images/pics/88ff46b3-028a-4dbb-a572-1f062b8b96d3.png) + +#### 不可重复读 + +**T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同**。 + +![不可重复读](https://www.pdai.tech/_images/pics/c8d18ca9-0b09-441a-9a0c-fb063630d708.png) + +#### 幻读 + +**T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同**。 + +![幻读](https://www.pdai.tech/_images/pics/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png) + +#### 不可重复读和幻读区别 + +**不可重复读的重点是修改,幻读的重点在于新增或者删除**。 + +例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操 作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。 + +例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所 有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记 录就变为了5条,这样就导致了幻读。 + +### 数据库的隔离级别 + +1. 未提交读,事务中发生了修改,即使没有提交,其他事务也是可见的,比如对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100.**可能会导致脏读、幻读或不可重复读** +2. 提交读,对于一个事务从开始直到提交之前,所做的任何修改是其他事务不可见的,举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取的A是50,刚读取完,A就被修改成100,这个时候另一个事务再进行读取发现A就突然变成100了;**可以阻止脏读,但是幻读或不可重复读仍有可能发生** +3. 可重复读,就是对一个记录读取多次的记录是相同的,比如对于一个数A读取的话一直是A,前后两次读取的A是一致的;**可以阻止脏读和不可重复读,但幻读仍有可能发生。** +4. 可串行化读,在并发情况下,和串行化的读取的结果是一致的,没有什么不同,比如不会发生脏读和幻读;**该级别可以防止脏读、不可重复读以及幻读。** + +| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | +| ---------------- | ---- | ---------- | ------ | +| READ-UNCOMMITTED | √ | √ | √ | +| READ-COMMITTED | × | √ | √ | +| REPEATABLE-READ | × | × | √ | +| SERIALIZABLE | × | × | × | + +MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。 + +**这里需要注意的是**:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别 下使用的是**Next-Key Lock 锁**算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以 说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要 求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。 + +因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内 容):,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)并不会有任何性能损失**。 + +InnoDB 存储引擎在分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。 + +### 为什么使用索引 + +- 通过创建**唯一性索引**,可以保证数据库表中每一行数据的唯一性。 +- 可以大大**加快数据的检索速度**,这也是创建索引的最主要的原因。 +- 帮助服务器**避免排序和临时表** +- 将**随机IO变为顺序IO**。 +- 可以加速**表和表之间的连接**,特别是在实现数据的参考完整性方面特别有意义。 + +### 索引这么多优点,为什么不对表总的每一列创建一个索引 + +- 当对表中的数据进行增加、删除和修改的时候,**索引也要动态的维护**,这样就降低了数据的维护速度。 +- **索引需要占物理空间**,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立簇索引,那么需要的空间就会更大。 +- **创建索引和维护索引要耗费时间**,这种时间随着数据量的增加而增加 + +### 索引如何提高查询速度的 + +将无序的数据变成相对有序的数据(就像查有目的一样) + +### 使用索引的注意事项 + +- 在经常需要搜索的列上,可以加快搜索的速度; +- 在经常使用在where子句中的列上面创建索引,加快条件的判断速度。 +- 在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间 +- 在中到大型表索引都是非常有效的,但是特大型表的维护开销会很大,不适合建索引 +- 在经常用到连续的列上,这些列主要是由一些外键,可以加快连接的速度 +- 避免where子句中对字段施加函数,这会造成无法命中索引 +- 在使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。 +- **将打算加索引的列设置为NOT NULL,否则将导致引擎放弃使用索引而进行全表扫描** +- 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 +- 在使用limit offset查询缓存时,可以借助索引来提高性能。 + +### MySQL索引主要使用的两种数据结构 + +- **哈希索引**,对于哈希索引来说,底层的数据结构肯定是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引 +- **BTree索引**,Mysql的BTree索引使用的是B树中的B+Tree但对于主要的两种存储引擎(MyISAM和InnoDB)的实现方式是不同的。 + +### myisam和innodb实现btree索引方式的区别 + +- MyISAM,**B+Tree叶节点的data域存放的是数据记录的地址**,在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的key存在,则取出其data域的值,然后以data域的值为地址读区相应的数据记录,这被称为“非聚簇索引” +- InnoDB,其数据文件本身就是索引文件,相比MyISAM,**索引文件和数据文件是分离的**,**其表数据文件本身就是按B+Tree组织的一个索引结构,树的节点data域保存了完整的数据记录**,这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引”或者聚集索引,而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方,在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,在走一遍主索引。因此,在设计表的时候,不建议使用过长的字段为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。 + +### 数据库结构优化 + +- **范式优化**: 比如消除冗余(节省空间。。) +- **反范式优化**:比如适当加冗余等(减少join) +- **限定数据的范围**: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内。 +- **读/写分离**: 经典的数据库拆分方案,主库负责写,从库负责读; +- **拆分表**:分区将数据在物理上分隔开,不同分区的数据可以制定保存在处于不同磁盘上的数据文件里。这样,当对这个表进行查询时,只需要在表分区中进行扫描,而不必进行全表扫描,明显缩短了查询时间,另外处于不同磁盘的分区也将对这个表的数据传输分散在不同的磁盘I/O,一个精心设置的分区可以将数据传输对磁盘I/O竞争均匀地分散开。对数据量大的时时表可采取此方法。可按月自动建表分区。 + +**拆分其实又分垂直拆分和水平拆分:** + +- 案例: 简单购物系统暂设涉及如下表: +- 1.产品表(数据量10w,稳定) +- 2.订单表(数据量200w,且有增长趋势) +- 3.用户表 (数据量100w,且有增长趋势) +- 以mysql为例讲述下水平拆分和垂直拆分,mysql能容忍的数量级在百万静态数据可以到千万 +- **垂直拆分:** + - 解决问题:表与表之间的io竞争 + - 不解决问题:单表中数据量增长出现的压力 + - 方案: 把产品表和用户表放到一个server上 订单表单独放到一个server上 +- **水平拆分:** + - 解决问题:单表中数据量增长出现的压力 + - 不解决问题:表与表之间的io争夺 +- 方案:**用户表** 通过性别拆分为男用户表和女用户表,**订单表** 通过已完成和完成中拆分为已完成订单和未完成订单,**产品表** 未完成订单放一个server上,已完成订单表盒男用户表放一个server上,女用户表放一个server上(女的爱购物 哈哈)。 + +### 主键超键候选键外键是什么 + +- **超键**:在关系中能唯一标识**元组的属性集**称为关系模式的超键 + +- **候选键**:不含有**多余属性的超键**称为候选键。也就是在候选键中,若再删除属性,就不是键了! + +- **主键**:**用户选作元组标识的一个候选键程序主键** + +- **外键**:如果关系模式**R中属性K是其它模式的主键**,那么**k在模式R中称为外键**。 + +**举例**: + +| 学号 | 姓名 | 性别 | 年龄 | 系别 | 专业 | +| -------- | ------ | ---- | ---- | ------ | -------- | +| 20020612 | 李辉 | 男 | 20 | 计算机 | 软件开发 | +| 20060613 | 张明 | 男 | 18 | 计算机 | 软件开发 | +| 20060614 | 王小玉 | 女 | 19 | 物理 | 力学 | +| 20060615 | 李淑华 | 女 | 17 | 生物 | 动物学 | +| 20060616 | 赵静 | 男 | 21 | 化学 | 食品化学 | +| 20060617 | 赵静 | 女 | 20 | 生物 | 植物学 | + +1. 超键:于是我们从例子中可以发现 学号是标识学生实体的唯一标识。那么该元组的超键就为学号。除此之外我们还可以把它跟其他属性组合起来,比如:(`学号`,`性别`),(`学号`,`年龄`) +2. 候选键:根据例子可知,学号是一个可以唯一标识元组的唯一标识,因此学号是一个候选键,实际上,候选键是超键的子集,比如 (学号,年龄)是超键,但是它不是候选键。因为它还有了额外的属性。 +3. 主键:简单的说,例子中的元组的候选键为学号,但是我们选定他作为该元组的唯一标识,那么学号就为主键。 +4. 外键是相对于主键的,比如在学生记录里,主键为学号,在成绩单表中也有学号字段,因此学号为成绩单表的外键,为学生表的主键。 + +**主键为候选键的子集,候选键为超键的子集,而外键的确定是相对于主键的。** + +### drop,delete与truncate的区别 + +- drop直接删掉表; +- truncate删除表中数据,再插入时自增长id又从1开始 ; +- delete删除表中数据,可以加where字句。 + +1. DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把单独的删除操作记录记入日志保存,删除行是不能恢复的。并且在删除的过程中不会激活与表有关的删除触发器。执行速度快。 +2. 表和索引所占空间。当表被TRUNCATE 后,这个表和索引所占用的空间会恢复到初始大小,而DELETE操作不会减少表或索引所占用的空间。drop语句将表所占用的空间全释放掉。 +3. 一般而言,drop > truncate > delete +4. 应用范围。TRUNCATE 只能对TABLE;DELETE可以是table和view +5. TRUNCATE 和DELETE只删除数据,而DROP则删除整个表(结构和数据)。 +6. truncate与不带where的delete :只删除数据,而不删除表的结构(定义)drop语句将删除表的结构被依赖的约束(constrain),触发器(trigger)索引(index);依赖于该表的存储过程/函数将被保留,但其状态会变为:invalid。 +7. delete语句为DML(Data Manipulation Language),这个操作会被放到 rollback segment中,事务提交后才生效。如果有相应的 tigger,执行的时候将被触发。 +8. truncate、drop是DDL(Data Define Language),操作立即生效,原数据不放到 rollback segment中,不能回滚 +9. 在没有备份情况下,谨慎使用 drop 与 truncate。要删除部分数据行采用delete且注意结合where来约束影响范围。回滚段要足够大。要删除表用drop;若想保留表而将表中数据删除,如果于事务无关,用truncate即可实现。如果和事务有关,或老是想触发trigger,还是用delete。 +10. Truncate table 表名 速度快,而且效率高,因为: truncate table 在功能上与不带 WHERE 子句的 DELETE 语句相同:二者均删除表中的全部行。但 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少。DELETE 语句每次删除一行,并在事务日志中为所删除的每行记录一项。TRUNCATE TABLE 通过释放存储表数据所用的数据页来删除数据,并且只在事务日志中记录页的释放。 +11. TRUNCATE TABLE 删除表中的所有行,但表结构及其列、约束、索引等保持不变。新行标识所用的计数值重置为该列的种子。如果想保留标识计数值,请改用 DELETE。如果要删除表定义及其数据,请使用 DROP TABLE 语句。 +12. 对于由 FOREIGN KEY 约束引用的表,不能使用 TRUNCATE TABLE,而应使用不带 WHERE 子句的 DELETE 语句。由于 TRUNCATE TABLE 不记录在日志中,所以它不能激活触发器。 + +### 视图的作用,视图可以更改吗 + +视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的sql操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。 + +视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by 则对视图再次order by将被覆盖。 + +创建视图:`create view xxx as xxxx` + +对于某些视图比如未使用联结子查询分组聚集函数Distinct Union等,是可以对其更新的,对视图的更新将对基表进行更新;但是视图主要用于简化检索,保护数据,并不用于更新,而且大部分视图都不可以更新。 + +### 数据库范式 + +#### 第一范式 + +在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。 所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,**第一范式就是无重复的列**。 + +#### 第二范式 + +第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主关键字或主键、主码。 第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,**第二范式就是非主属性非部分依赖于主关键字**。 + +#### 第三范式 + +满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。例如,**存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。**简而言之,第三范式就是属性不依赖于其它非主属性。(我的理解是消除冗余) + +### 什么是覆盖索引 + +如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称 之为“覆盖索引”。我们知道在InnoDB存储引 擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就 会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! + +### 各种树 + +> 这里就不多介绍了,有兴趣可以在这里看[各种树](http://dreamcat.ink/2019/11/08/shu-ju-jie-gou-shu-ji-chu/) + +> 创作不易哇,觉得有帮助的话,给个小小的star呗。github地址😁😁😁 + diff --git a/Java/crazy/Mybatis.md b/Java/crazy/Mybatis.md new file mode 100644 index 00000000..e00ace35 --- /dev/null +++ b/Java/crazy/Mybatis.md @@ -0,0 +1,210 @@ +> 个人感觉,这部分源码的重要基础之一就是反射,不过这里就不贴源码,好好学习Java的反射吧。 + + ## 大纲图 + + ![MyBatis面试常见问题](http://media.dreamcat.ink/uPic/MyBatis面试常见问题.png) + + ### 什么是数据持久化? + + 数据持久化是将**内存**中的**数据**模型转换为**存储**模型,以及将存储模型转换为内存中的数据模型的统称。例如,文件的存储、数据的读取等都是数据持久化操作。数据模型可以是任何**数据结构或对象的模型、XML、二进制流**等。 + 当我们编写应用程序操作数据库,对表数据进行**增删改查**的操作的时候就是数据持久化的操作。 + + ### Mybatis框架简介 + + - **MyBatis框架是一个开源的数据持久层框架。** + - **它的内部封装了通过JDBC访问数据库的操作,支持普通的SQL查询、存储过程和高级映射,几乎消除了所有的JDBC代码和参数的手工设置以及结果集的检索。** + - **MyBatis作为持久层框架,其主要思想是将程序中的大量SQL语句剥离出来,配置在配置文件当中,实现SQL的灵活配置。** + - **这样做的好处是将SQL与程序代码分离,可以在不修改代码的情况下,直接在配置文件当中修改SQL。** + + ### 什么是ORM? + + ORM(Object/Relational Mapping)即**对象关系映射**,是一种数据持久化技术。它在**对象模型和关系型数据库直接建立起对应关系**,并且提供一种机制,**通过JavaBean对象去操作数据库表的数据**。 + MyBatis通过简单的**XML**或者**注解**的方式进行配置和原始映射,将实体类和SQL语句之间建立映射关系,是一种**半自动**(之所以说是半自动,因为我们要自己写SQL)的ORM实现。 + + ### MyBatis框架的优缺点及其适用的场合 + + #### 优点 + + 1. 与JDBC相比,减少了50%以上的代码量。 + 2. MyBatis是易学的持久层框架,小巧并且简单易学。 + 3. MyBatis相当灵活,不会对应用程序或者数据库的现有设计强加任何影响,SQL写在XML文件里,从程序代码中彻底分离,降低耦合度,便于统一的管理和优化,并可重用。 + 4. 提供XML标签,支持编写动态的SQL,满足不同的业务需求。 + 5. 提供映射标签,支持对象与数据库的ORM字段关系映射。 + + #### 缺点 + + 1. SQL语句的编写工作量较大,对开发人员编写SQL的能力有一定的要求。 + 2. SQL语句依赖于数据库,导致数据库不具有好的移植性,不可以随便更换数据库。 + + #### 适用场景 + + MyBatis专注于SQL自身,是一个足够灵活的DAO层解决方案。对性能的要求很高,或者需求变化较多的项目,例如Web项目,那么MyBatis是不二的选择。 + + ### MyBatis与Hibernate有哪些不同? + + 1. Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。 + 2. Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。 + 3. Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。 + + ### #{}和${}的区别是什么? + + 1. `#{}` 是预编译处理,`${}`是字符串替换。 + 2. Mybatis在处理`#{}`时,会将sql中的`#{}`替换为?号,调用PreparedStatement的set方法来赋值; + 3. Mybatis在处理`${}`时,就是把`${}`替换成变量的值。 + 4. 使用`#{}`可以有效的防止SQL注入,提高系统安全性。 + + ### 当实体类中的属性名和表中的字段名不一样,怎么办? + + 1. 第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。 + 2. 第2种: 通过 ` 创作不易哇,觉得有帮助的话,给个小小的star呗。[github地址](https://github.com/DreamCats/JavaBooks)😁😁😁 \ No newline at end of file diff --git a/Java/crazy/README.md b/Java/crazy/README.md new file mode 100644 index 00000000..00d079a5 --- /dev/null +++ b/Java/crazy/README.md @@ -0,0 +1,12 @@ +- **个人吐血系列-总结Java基础**: [本地阅读](Java基础.md)->[掘金阅读](https://juejin.im/post/5e7e0615f265da795568754b) +- **个人吐血系列-总结Java集合**: [本地阅读](Java集合.md)->[掘金阅读](https://juejin.im/post/5e801e29e51d45470b4fce1c) +- **个人吐血系列-总结Java多线程**: [本地阅读](Java多线程)->[掘金阅读-1](https://juejin.im/post/5e7e0e4ce51d4546cd2fcc7c) [掘金阅读-2](https://juejin.im/post/5e7e0e4ce51d4546cd2fcc7c) +- **个人吐血系列-总结JVM**: [本地阅读](JVM.md)->[掘金阅读](https://juejin.im/post/5e8344486fb9a03c786ef885) +- **个人吐血系列-总结Spring**: [本地阅读](Spring.md)->[掘金阅读](https://juejin.im/post/5e846a4a6fb9a03c42378bc1) +- **个人吐血系列-总结Mybatis**: [本地阅读](Mybatis.md)->[掘金阅读](https://juejin.im/post/5e889b196fb9a03c875c8f50) +- **个人吐血系列-总结MySQL**: [本地阅读](MySQL.md)->[掘金阅读](https://juejin.im/post/5e94116551882573b86f970f) +- **个人吐血系列-总结Redis**: [本地阅读](Redis.md)->[掘金阅读](https://juejin.im/post/5e9d6a9ff265da47e34c0e8a) +- **个人吐血系列-总结计算机网络**: [本地阅读](计算机网络.md)->[掘金阅读](https://juejin.im/post/5ea383c251882573716ab496) +- **个人吐血系列-Dubbo**: [本地阅读](Dubbo.md)->[掘金阅读](https://juejin.im/post/5eb11127f265da7bb46bce26) +- **个人吐血系列-RocketMQ**: [本地阅读](RocketMQ)-> [掘金阅读](https://juejin.im/post/5ecf1f716fb9a047f338b972) + diff --git "a/Interview/redis/Redis-\351\235\242\350\257\225\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" b/Java/crazy/Redis.md similarity index 67% rename from "Interview/redis/Redis-\351\235\242\350\257\225\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" rename to Java/crazy/Redis.md index 3abef819..bd06253a 100644 --- "a/Interview/redis/Redis-\351\235\242\350\257\225\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" +++ b/Java/crazy/Redis.md @@ -1,51 +1,41 @@ ---- -title: Redis-面试常见的问题 -author: DreamCat -id: 1 -date: 2019-11-23 16:02:32 -tags: Redis -categories: 知识 ---- -## 引言 +> 毕竟Redis还是挺热门的缓存中间件,得好好学习一下子,在高并发的场景当中,也用的比较多,面试也是常问的点。 -> Redis在大型项目中也是经常用到的非关系型数据库,面试官也挺喜欢问的。 +## 大纲图 - +![Redis常见面试](http://media.dreamcat.ink/uPic/Redis常见面试.png) -## 常见问题 +### Redis是什么 -### redis是什么玩意? +简单来说redis就是一个**数据库**,不过与传统数据库不同的是redis的数据库是存在**内存**中,所以**读写速度非常快**,因此redis被广泛应用于**缓存**方向。另外,redis也经常用来做**分布式锁**,redis提供了多种数据类型来支持不同的业务场景。除此之外,**redis 支持事务** 、**持久化**、**LUA脚本**、**LRU驱动事件**、**多种集群**方案。 -简单来说redis就是一个数据库,不过与传统数据库不同的是redis的数据库是存在内存中,所以读写速度非常快,因此redis被广泛应用于缓存方向。另外,redis也经常用来做分布式锁,redis提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。 +### 为什么要用Redis -### 为什么要用redis/为什么要用缓存? +1. **高性能**:假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从**硬盘上读取**的。将该用户访问的**数据存在缓存**中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操**作缓存就是直接操作内存,所以速度相当快**。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可! +2. **高并发**:**直接操作缓存能够承受的请求是远远大于直接访问数据库的**,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。 -1. **高性能**:假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可! -2. **高并发**:直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。 +### 使用Redis有哪些好处 -### 使用Redis有哪些好处? - -1. 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1) -2. 支持丰富数据类型,支持string,list,set,sorted set,hash -3. 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行 -4. 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除 +1. **速度快**,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1) +2. 支持丰富数据类型,支持**string,list,set,sorted set,hash** +3. **支持事务**,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行 +4. 丰富的特性:**可用于缓存,消息,按key设置过期时间,过期后将会自动删除** 5. 等等... -### 为什么要用 redis 而不用 map/guava 做缓存? +### 为什么要用Redis而不用map/guava做缓存 -缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是 轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓 存不具有一致性。 +缓存分为**本地缓存**和**分布式缓存**。以 Java 为例,使用自带的 **map 或者 guava 实现的是本地缓存**,最主要的特点是**轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓 存不具有一致性。** -使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致 性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。 +使用 **redis 或 memcached 之类的称为分布式缓存**,在多实例的情况下,各实例共用一份缓存数据,**缓存具有一致 性**。缺点是需要保持 **redis 或 memcached服务的高可用**,整个程序架构上较为复杂。 -### redis相比memcached有哪些优势? +### Redis相比Memcached有哪些优势 -1. memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型 -2. redis的速度比memcached快很多 -3. redis可以持久化其数据 +1. memcached所有的值**均是简单的字符串**,redis作为其替代者,支持更为**丰富的数据类型** +2. redis的速度比memcached**快**很多 +3. redis可以**持久化**其数据 -### redis的线程模型? +### Redis的线程模型 -redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。 +redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是**单线程**的,所以 redis 才叫做**单线程的模型**。它采用 **IO 多路复用机制**同时监听多个 socket,根据 socket 上的事件来**选择对应的事件处理器**进行处理。 文件事件处理器的结构包含 4 个部分: @@ -54,9 +44,9 @@ redis 内部使用文件事件处理器 `file event handler`,这个文件事 - 文件事件分派器 - 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) -多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。 +多个 socket 可能会**并发产生不同的操作**,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的**事件放入队列中排队**,事件分派器每次从队列中取出一个事件,把该事件交给对应的**事件处理器**进行处理。 -### redis常见性能问题和解决方案 +### Redis常见性能问题和解决方案 1. Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件 2. 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次 @@ -64,7 +54,7 @@ redis 内部使用文件事件处理器 `file event handler`,这个文件事 4. 尽量避免在压力很大的主库上增加从库 5. 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3... -### redis 常见数据结构以及使用场景分析 +### Redis常见数据结构以及使用场景分析 #### String @@ -76,12 +66,7 @@ String数据结构是简单的key-value类型,value其实不仅可以是String > 常用命令: hget,hset,hgetall 等。 -Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅 仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息: - -```json -key=JavaUser293847 -value={ “id”: 1, “name”: “SnailClimb”, “age”: 22, “location”: “Wuhan, Hubei” } -``` +Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅 仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。 #### List @@ -111,13 +96,13 @@ set 对外提供的功能与list类似是一个列表的功能,特殊之处在 举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维 度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。 -### redis 设置过期时间 +### Redis设置过期时间 Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库, 这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统 的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。 我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的 时间。 -**定期删除+惰性删除。** +#### 定期删除+惰性删除 - 定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删 除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所 有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载! - 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这 就是所谓的惰性删除,也是够懒的哈! @@ -126,7 +111,7 @@ Redis中有个设置时间过期的功能,即对存储在 redis 数据库中 **redis 内存淘汰机制。** -### MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据 +### Mysql有2000万数据,redis只存20万,如何保证redis中的数据都是热点数据 redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。redis 提供 6种数据淘汰策略: @@ -137,7 +122,7 @@ redis 内存数据集大小上升到一定大小的时候,就会施行数据 - allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 - no-enviction(驱逐):禁止驱逐数据 -### Memcache与Redis的区别都有哪些? +### Memcache与Redis的区别都有哪些 1. 存储方式 @@ -160,19 +145,19 @@ redis 内存数据集大小上升到一定大小的时候,就会施行数据 5. Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。 -### redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以 进行恢复) +### Redis持久化机制 很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机 器、机器故障之后回复数据),或者是为了防止系统故障而将数据备份到一个远程位置。 -Redis不同于Memcached的很重一点就是,**Redis支持持久化**,而且支持两种不同的持久化操作。Redis的一种持久化方式叫**快照(snapshotting,RDB)**,另一种方式是**只追加文件(append-only file,AOF)**.这两种方法各有千 秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。 +Redis不同于Memcached的很重一点就是,**Redis支持持久化**,而且支持两种不同的持久化操作。Redis的一种持久化方式叫**快照(snapshotting,RDB)**,另一种方式是**只追加文件(append-only file,AOF)**.这两种方法各有千 秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方。 -**快照(snapshotting)持久化(RDB)** +#### 快照(snapshotting)持久化(RDB) Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行 备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性 能),还可以将快照留在原地以便重启服务器的时候使用。 快照持久化是Redis默认采用的持久化方式,在redis.conf配置文件中默认有此下配置: -```yaml +```shell save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令 创建快照。 @@ -181,15 +166,15 @@ save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Re save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 ``` -**AOF(append-only file)持久化** +#### AOF(append-only file)持久化 -与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启 AOF(append only file)方式的持久化,可以通过appendonly参数开启:`appendonly yes` +与快照持久化相比,AOF**持久化的实时性更好**,因此已成为主流的持久化方案。默认情况下Redis没有开启 AOF(append only file)方式的持久化,可以通过appendonly参数开启:`appendonly yes` 开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的 保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。 在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是: -```yaml +```shell appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 appendfsync no #让操作系统决定何时进行同步 @@ -203,38 +188,38 @@ Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通 如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。 -### **AOF 重写** +### AOF重写 -AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。 +AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,**但体积更小**。 -AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任伺读 入、分析或者写入操作。 +AOF重写是一个有歧义的名字,该功能是通过读取数据库中的**键值**对来实现的,程序无须对现有AOF文件进行任伺读 入、分析或者写入操作。 -在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期 间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容 追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的 AOF文件,以此来完成AOF文件重写操作 +在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF **重写缓冲区**,该缓冲区会在子进程创建新AOF文件期 间,记录服务器执行的所有写命令。**当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容 追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致**。最后,服务器用新的AOF文件替换旧的 AOF文件,以此来完成AOF文件重写操作。 -### Redis 事务 +### Redis事务 -Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然 后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令 请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。 +Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种**将多个命令请求打包,然 后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令 请求,它会将事务中的所有命令都执行完毕**,然后才去处理其他客户端的命令请求。 -在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性 (Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务 也具有持久性(Durability)。 +在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务**总是**具有原子性 (Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务 也具有持久性(Durability)。 -### Redis 常见的性能问题都有哪些?如何解决? +### Redis常见的性能问题都有哪些?如何解决? 1. Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。 2. Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。 3. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。 4. Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内 -### Redis的同步机制了解么? +### Redis的同步机制了解吗? -主从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。 +主从同步。第一次同步时,**主节点做一次bgsave**,并同时将后续修改操作记录到**内存buffer**,待完成后**将rdb文件全量同步到复制节点**,复制节点接受完成后**将rdb镜像加载到内存**。加载完成后,再通知主节点**将期间修改的操作记录同步到复制节点进行重放**就完成了同步过程。 -### 是否使用过Redis集群,集群的原理是什么? +### 是否使用Redis集群,集群的原理是什么 Redis Sentinel着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。 Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。 -### 缓存雪崩和缓存穿透问题解决方案 +### 缓存雪崩和缓存问题解决方案 #### 缓存雪崩 @@ -248,9 +233,9 @@ Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster 一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量 请求而崩掉。 -解决办法: 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈 希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压 力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存 在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 +解决办法: 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈 希到一个足够大的**bitmap**中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压 力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个**查询返回的数据为空**(不管是数据不存 在,还是系统故障),我们仍然把这个空结果进行缓存,但它的**过期时间会很短**,最长不超过五分钟。 -### 如何解决 Redis 的并发竞争 Key 问题 +### 如何解决Redis的并发竞争Key问题 所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺 序不同,这样也就导致了结果的不同! @@ -260,7 +245,7 @@ Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster 在实践中,当然是从以可靠性为主。所以首推Zookeeper。 -### 如何保证缓存与数据库双写时的数据一致性? +### 如何保证缓存与数据库双写时的数据一致性 你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如 何解决一致性问题? diff --git a/Java/crazy/RocketMQ.md b/Java/crazy/RocketMQ.md new file mode 100644 index 00000000..49befc64 --- /dev/null +++ b/Java/crazy/RocketMQ.md @@ -0,0 +1,83 @@ +> 一般面试问消息队列,都是结合自己的项目进行回答的...最好有个项目有消息队列的中间件.本项目使用了RocketMQ + +### 什么是消息队列?消息队列的主要作用是什么? + +我们可以把消息队列比作是一个**存放消息的容器**,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过**异步处理提高系统性能和削峰、降低系统耦合性**。 + +- **异步处理**:非核心流程异步化,提高系统响应性能 +- **应用解耦**: + - 系统不是强耦合,消息接受者可以随意增加,而不需要修改消息发送者的代码。消息发送者的成功不依赖消息接受者(比如有些银行接口不稳定,但调用方并不需要依赖这些接口) + - 消息发送者的成功不依赖消息接受者(比如有些银行接口不稳定,但调用方并不需要依赖这些接口) +- **最终一致性**:最终一致性不是消息队列的必备特性,但确实可以依靠消息队列来做最终一致性的事情。 + - 先写消息再操作,确保操作完成后再修改消息状态。定时任务补偿机制实现消息可靠发送接收、业务操作的可靠执行,要注意消息重复与幂等设计 + - 所有不保证100%不丢消息的消息队列,理论上无法实现最终一致性。 +- **广播**:只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情 +- **流量削峰与监控**:当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的“漏斗”。在下游有能力处理的时候,再进行分发。 +- **日志处理**:将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题 +- **消息通讯**:消息队列一般都内置了高效的通信机制,因此也可以用于单纯的消息通讯,如实现点对点消息队列或者聊天室等。 + +**推荐浅显易懂的讲解**: + +- [《吊打面试官》系列-消息队列基础](https://mp.weixin.qq.com/s/qGzMRvWwworit4lWp0EL3w) +- [面试官问你什么是消息队列?把这篇甩给他!](https://mp.weixin.qq.com/s/wHaaipxYx0CUwpYVRJnV1A) + +### kafka、activemq、rabbitmq、rocketmq都有什么区别? + +- ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。 +- RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做erlang源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 +- RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ 挺好的 +- kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 + +### MQ在高并发情况下,假设队列满了如何防止消息丢失? + +![](https://bbsmax.ikafan.com/static/L3Byb3h5L2h0dHBzL2ltZzIwMTguY25ibG9ncy5jb20vYmxvZy85OTgzNDIvMjAxOTAyLzk5ODM0Mi0yMDE5MDIxNjEyMTEzNzM1Mi04MzE0MzYzMDkucG5n.jpg) + +1. 生产者可以采用重试机制。因为消费者会不停的消费消息,可以重试将消息放入队列。 +2. 死信队列,可以理解为备胎(推荐) + - 即在消息过期,队列满了,消息被拒绝的时候,都可以扔给死信队列。 + - 如果出现死信队列和普通队列都满的情况,此时考虑消费者消费能力不足,可以对消费者开多线程进行处理。 + +### 谈谈死信队列 + +**死信队列用于处理无法被正常消费的消息,即死信消息**。 + +当一条消息初次消费失败,**消息队列 RocketMQ 版会自动进行消息重试**;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 版不会立刻将消息丢弃,而是将其发送到该**消费者对应的特殊队列中**,该特殊队列称为**死信队列**。 + +**死信消息的特点**: + +- 不会再被消费者正常消费。 +- 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。 + +**死信队列的特点**: + +- 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。 +- 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 版不会为其创建相应的死信队列。 +- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。 + +消息队列 RocketMQ 版控制台提供对死信消息的查询、导出和重发的功能。 + +### 消费者消费消息,如何保证MQ幂等性? + +#### 幂等性 + +消费者在消费mq中的消息时,mq已把消息发送给消费者,消费者在给mq返回ack时网络中断,故mq未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息; + +#### 解决方案 + +- MQ消费者的幂等行的解决一般使用全局ID 或者写个唯一标识比如时间戳 或者UUID 或者订单 +- 也可利用mq的该id来判断,或者可按自己的规则生成一个全局唯一id,每次消费消息时用该id先判断该消息是否已消费过。 +- 给消息分配一个全局id,只要消费过该消息,将 < id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。 + +### 使用异步消息时如何保证数据的一致性 + +1. **借助数据库的事务**:使用异步消息怎么还能借助到数据库事务?这需要在数据库中创建一个**本地消息表**,这样可以通过**一个事务来控制本地业务逻辑更新**和本**地消息表的写入在同一个事务中**,一旦消息落库失败,则直接全部回滚。如果消息落库成功,后续就可以根据情况基于本地数据库中的消息数据对消息进行重投了。关于本地消息表和消息队列中状态如何保持一致,可以采用 2PC 的方式。在发消息之前落库,然后发消息,在得到同步结果或者消息回调的时候更新本地数据库表中消息状态。然后只需要通过**定时轮询**的方式对状态未已记录但是未发送的消息重新投递就行了。但是这种方案有个前提,就是要求消息的消费者**做好幂等控制**,这个其实异步消息的消费者一般都需要考虑的。 +2. 除了使用数据库以外,还可以使用 **Redis** 等缓存。这样就是无法利用关系型数据库自带的事务回滚了。 + +### RockMQ不适用Zookeeper作为注册中心的原因,以及自制的NameServer优缺点? + +1. ZooKeeper 作为支持**顺序一致性**的中间件,在某些情况下,它为了满足一致性,会丢失一定时间内的**可用性**,RocketMQ 需要注册中心只是为了**发现组件地址**,在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,这同时也是 **NameServer 的缺点,因为 NameServer 集群间互不通信,它们之间的注册信息可能会不一致**。 +2. 另外,当有新的服务器加入时,**NameServer 并不会立马通知到 Produer**,而是由 **Produer 定时去请求 NameServer 获取最新的 Broker/Consumer 信息**(这种情况是通过 Producer 发送消息时,负载均衡解决) +3. 包括组件通信间使用 Netty 的自定义协议 +4. 消息重试负载均衡策略(具体参考 Dubbo 负载均衡策略) +5. 消息过滤器(Producer 发送消息到 Broker,Broker 存储消息信息,Consumer 消费时请求 Broker 端从磁盘文件查询消息文件时,在 Broker 端就使用过滤服务器进行过滤) +6. Broker 同步双写和异步双写中 Master 和 Slave 的交互 \ No newline at end of file diff --git a/Java/crazy/Spring.md b/Java/crazy/Spring.md new file mode 100644 index 00000000..c027f5ee --- /dev/null +++ b/Java/crazy/Spring.md @@ -0,0 +1,463 @@ +--- +title: 个人吐血系列-总结Spring +date: 2020-03-29 15:45:57 +tags: spring +categories: java-interview +--- + +> 个人感觉,Spring这一块,不仅会用,还得知道底层的源码或者是说初始化加载等过程吧,这篇就不介绍如何配置以及各个注解的使用,在这里不是重点。 + +## 大纲图 + +![Spring常见问题](http://media.dreamcat.ink/uPic/Spring常见问题.png) + +### 什么是Spring框架 + +我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。比如:Core Container 中的 Core 组件是Spring 所有组件的核心,Beans 组件和 Context 组件是实现IOC和依赖注入的基础,AOP组件用来实现面向切面编程。 + +Spring 官网列出的 Spring 的 6 个特征: + +- **核心技术** :依赖注入(DI),AOP,事件(events),资源,i18n,验证,数据绑定,类型转换,SpEL。 +- **测试** :模拟对象,TestContext框架,Spring MVC 测试,WebTestClient。 +- **数据访问** :事务,DAO支持,JDBC,ORM,编组XML。 +- **Web支持** : Spring MVC和Spring WebFlux Web框架。 +- **集成** :远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 +- **语言** :Kotlin,Groovy,动态语言。 + +### 列举一些重要的Spring模块 + +- **Spring Core:** 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IoC 依赖注入功能。 +- **Spring Aspects** : 该模块为与AspectJ的集成提供支持。 +- **Spring AOP** :提供了面向切面的编程实现。 +- **Spring JDBC** : Java数据库连接。 +- **Spring JMS** :Java消息服务。 +- **Spring ORM** : 用于支持Hibernate等ORM工具。 +- **Spring Web** : 为创建Web应用程序提供支持。 +- **Spring Test** : 提供了对 JUnit 和 TestNG 测试的支持。 + +### @RestController VS Controller + +#### Controller + +**`Controller` 返回一个页面** + +单独使用 `@Controller` 不加 `@ResponseBody`的话一般使用在要返回一个视图的情况,这种情况属于比较传统的Spring MVC 的应用,对应于前后端不分离的情况。 + +#### RestController + +**`@RestController` 返回JSON 或 XML 形式数据** + +但`@RestController`只返回对象,对象数据直接以 JSON 或 XML 形式写入 HTTP 响应(Response)中,这种情况属于 RESTful Web服务,这也是目前日常开发所接触的最常用的情况(前后端分离)。 + +**`@Controller +@ResponseBody` 返回JSON 或 XML 形式数据** + +如果你需要在Spring4之前开发 RESTful Web服务的话,你需要使用`@Controller` 并结合`@ResponseBody`注解,也就是说`@Controller` +`@ResponseBody`= `@RestController`(Spring 4 之后新加的注解)。 + +### 谈谈SpringIOC和AOP + +#### IOC + +IoC(Inverse of Control:控制反转)是一种**设计思想**,就是 **将原本在程序中手动创建对象的控制权,交由Spring框架来管理。** IoC 在其他语言中也有应用,并非 Spring 特有。 **IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。** + +将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 **IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。** 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。 + +Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。 + +推荐阅读:https://www.zhihu.com/question/23277575/answer/169698662 + +##### ioc容器初始化流程 + +**可以看到有很多PostProcessors的后置处理器** + +```java +@Override +public void refresh() throws BeansException, IllegalStateException { + // 来个锁,不然 refresh() 还没结束,你又来个启动或销毁容器的操作,那不就乱套了嘛 + synchronized (this.startupShutdownMonitor) { + + // 准备工作,记录下容器的启动时间、标记“已启动”状态、处理配置文件中的占位符 + prepareRefresh(); + // Tell the subclass to refresh the internal bean factory. + //获得容器ApplicationContext的子类BeanFactory。步骤如下: + //1.如果已经有了BeanFactory就销毁它里面的单例Bean并关闭这个BeanFactory。 + //2.创建一个新的BeanFactory。 + //3.对这个BeanFactory进行定制(customize),如allowBeanDefinitionOverriding等参数 + //4.转载BeanDefinitions(读取配置文件,将xml转换成对应得BeanDefinition) + //5.检查是否同时启动了两个BeanFactory。 + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + + // 设置 BeanFactory 的类加载器,添加几个 BeanPostProcessor,手动注册几个特殊的 bean + // 这块待会会展开说 + prepareBeanFactory(beanFactory); + + try { + // 设置beanFactory的后置处理器 + // 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类或做点什么事 + postProcessBeanFactory(beanFactory); + // 调用beanFactory的后置处理器 + // 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法 + invokeBeanFactoryPostProcessors(beanFactory); + + // 注册 BeanPostProcessor 的实现类(bean的后置处理器) + // 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization + // 两个方法分别在 Bean 初始化之前和初始化之后得到执行。注意,到这里 Bean 还没初始化 + registerBeanPostProcessors(beanFactory); + + // 初始化当前 ApplicationContext 的 MessageSource,国际化这里就不展开说了,不然没完没了了 + initMessageSource(); + + // 初始化当前 ApplicationContext 的事件广播器,这里也不展开了 + initApplicationEventMulticaster(); + + // 模板方法(钩子方法), + // 具体的子类可以在这里初始化一些特殊的 Bean(在初始化 singleton beans 之前) + onRefresh(); + + // 注册事件监听器,监听器需要实现 ApplicationListener 接口。这也不是我们的重点,过 + registerListeners(); + + // 重点,重点,重点 + // 初始化所有的 singleton beans + //(lazy-init 的除外) + finishBeanFactoryInitialization(beanFactory); + + // 最后,广播事件,ApplicationContext 初始化完成 + finishRefresh(); + } + + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + + // Destroy already created singletons to avoid dangling resources. + // 销毁已经初始化的 singleton 的 Beans,以免有些 bean 会一直占用资源 + destroyBeans(); + + // Reset 'active' flag. + cancelRefresh(ex); + + // 把异常往外抛 + throw ex; + } + + finally { + // Reset common introspection caches in Spring's core, since we + // might not ever need metadata for singleton beans anymore... + resetCommonCaches(); + } + } +} +``` + +##### 大概总结一下 + +1. Spring容器在启动的时候,先会保存所有注册进来的Bean的定义信息; + 1. xml注册bean; + 2. 注解注册Bean;@Service、@Component、@Bean、xxx + 2. Spring容器会合适的时机创建这些Bean + 1. 用到这个bean的时候;利用getBean创建bean;创建好以后保存在容器中; + 2. 统一创建剩下所有的bean的时候;finishBeanFactoryInitialization(); + 3. 后置处理器;BeanPostProcessor + 1. 每一个bean创建完成,都会使用各种后置处理器进行处理;来增强bean的功能;比如 + 1. AutowiredAnnotationBeanPostProcessor:处理自动注入 + 2. AnnotationAwareAspectJAutoProxyCreator:来做AOP功能; + 3. xxx + 4. 事件驱动模型; + 1. ApplicationListener;事件监听; + 2. ApplicationEventMulticaster;事件派发: + +更详细的源码可看 + +1. [http://dreamcat.ink/2020/01/31/spring-springaop-yuan-ma-fen-xi/ ](http://dreamcat.ink/2020/01/31/spring-springaop-yuan-ma-fen-xi/ ) +2. [ https://javadoop.com/post/spring-ioc](https://javadoop.com/post/spring-ioc]) + +#### AOP + +AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,**却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来**,便于**减少系统的重复代码**,**降低模块间的耦合度**,并**有利于未来的可拓展性和可维护性**。 +**Spring AOP就是基于动态代理的**,如果要代理的对象,实现了某个接口,那么Spring AOP会使用**JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用**Cglib** ,这时候Spring AOP会使用 **Cglib** 生成一个被代理对象的子类来作为代理。 +当然你也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。 +使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。 + +##### Spring AOP 和 AspectJ AOP 有什么区别 + +**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation) +Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单, +如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。 + +##### 源码总结 + +![AnnotationAwareAspectJAutoProxyCreator](http://media.dreamcat.ink//20200201150935.png) + +1. @EnableAspectJAutoProxy 开启AOP功能 +2. @EnableAspectJAutoProxy 会给容器中注册一个组件 AnnotationAwareAspectJAutoProxyCreator +3. AnnotationAwareAspectJAutoProxyCreator是一个后置处理器; +4. 容器的创建流程: + 1. registerBeanPostProcessors()注册后置处理器;创建AnnotationAwareAspectJAutoProxyCreator对象(**Spring源码**) + 2. finishBeanFactoryInitialization()初始化剩下的单实例bean(**Spring源码**) + 1. 创建业务逻辑组件和切面组件 + 2. AnnotationAwareAspectJAutoProxyCreator拦截组件的创建过程 + 3. 组件创建完之后,判断组件是否需要增强;是->切面的通知方法,包装成增强器(Advisor);给业务逻辑组件创建一个代理对象(cglib); +5. 执行目标方法: + 1. 代理对象执行目标方法 + 2. CglibAopProxy.intercept(); + 1. 得到目标方法的拦截器链(增强器包装成拦截器MethodInterceptor) + 2. 利用拦截器的链式机制,依次进入每一个拦截器进行执行; + 3. 效果: + 1. 正常执行:前置通知-》目标方法-》后置通知-》返回通知 + 2. 出现异常:前置通知-》目标方法-》后置通知-》异常通知 + +### Spring Bean + +#### 作用域 + +- singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 +- prototype : 每次请求都会创建一个新的 bean 实例。 +- request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 +- session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。 +- global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话 + +#### Spring的单例有线程安全问题吗 + +大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 + +常见的有两种解决办法: + +1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 +2. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。 + +#### Component和Bean的区别 + +1. 作用对象不同: `@Component` 注解作用于类,而`@Bean`注解作用于方法。 +2. `@Component`通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 `@ComponentScan` 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。`@Bean` 注解通常是我们在标有该注解的方法中定义产生这个 bean,`@Bean`告诉了Spring这是某个类的示例,当我需要用它的时候还给我。 +3. `@Bean` 注解比 `Component` 注解的自定义性更强,而且很多地方我们只能通过 `@Bean` 注解来注册bean。比如当我们引用第三方库中的类需要装配到 `Spring`容器时,则只能通过 `@Bean`来实现。 + +#### 将类声明为bean有哪些 + +我们一般使用 `@Autowired` 注解自动装配 bean,要想把类标识成可用于 `@Autowired` 注解自动装配的 bean 的类,采用以下注解可实现: + +- `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个Bean不知道属于哪个层,可以使用`@Component` 注解标注。 +- `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 +- `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。 +- `@Controller` : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。 + +#### Bean的声明周期 + +![bean周期](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/5496407.jpg) + +- Bean 容器找到配置文件中 Spring Bean 的定义。 +- Bean 容器利用 Java Reflection API 创建一个Bean的实例。 +- 如果涉及到一些属性值 利用 `set()`方法设置一些属性值。 +- 如果 Bean 实现了 `BeanNameAware` 接口,调用 `setBeanName()`方法,传入Bean的名字。 +- 如果 Bean 实现了 `BeanClassLoaderAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoader`对象的实例。 +- 与上面的类似,如果实现了其他 `*.Aware`接口,就调用相应的方法。 +- 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessBeforeInitialization()` 方法 +- 如果Bean实现了`InitializingBean`接口,执行`afterPropertiesSet()`方法。 +- 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。 +- 如果有和加载这个 Bean的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessAfterInitialization()` 方法 +- 当要销毁 Bean 的时候,如果 Bean 实现了 `DisposableBean` 接口,执行 `destroy()` 方法。 +- 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。 + +### SpringMVC + +MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的Web层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service层(处理业务)、Dao层(数据库操作)、Entity层(实体类)、Controller层(控制层,返回数据给前台页面)。 + +![springmvc](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/60679444.jpg) + +#### 工作原理 + +![springmvc工作原理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/49790288.jpg) + +**流程说明(重要):** + +1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 +2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 +3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 +4. `HandlerAdapter` 会根据 `Handler`来调用真正的处理器开处理请求,并处理相应的业务逻辑。 +5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 +6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 +7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 +8. 把 `View` 返回给请求者(浏览器) + +### Spring都用到了哪些设计模式 + +- **工厂设计模式** : Spring使用工厂模式通过 `BeanFactory`、`ApplicationContext` 创建 bean 对象。 +- **代理设计模式** : Spring AOP 功能的实现。 +- **单例设计模式** : Spring 中的 Bean 默认都是单例的。 +- **模板方法模式** : Spring 中 `jdbcTemplate`、`hibernateTemplate` 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 +- **包装器设计模式** : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 +- **观察者模式:** Spring 事件驱动模型就是观察者模式很经典的一个应用。 +- **适配器模式** :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 +- ...... + +### Spring事务 + +#### 管理事物有几种 + +- 编程式事务,在代码中硬编码。 +- 声明式事务,在配置文件中配置 + +**声明式事务又分为两种**: + +1. 基于XML的声明式事务 +2. 基于注解的声明式事务 + +#### 隔离级别 + +**隔离级别就跟mysql几乎差不多** + +#### 源码分析 + +1. 开启@EnableTransactionManagement +2. 利用TransactionManagementConfigurationSelector给容器中会导入组件 + 1. AutoProxyRegistrar + 2. ProxyTransactionManagementConfiguration +3. AutoProxyRegistrar: + 1. 给容器中注册一个 InfrastructureAdvisorAutoProxyCreator 组件; + 2. 利用后置处理器机制在对象创建以后,包装对象,返回一个代理对象(增强器),代理对象执行方法利用拦截器链进行调用; +4. ProxyTransactionManagementConfiguration 做了什么? + 1. 给容器中注册事务增强器; + 1. 事务增强器要用事务注解的信息,AnnotationTransactionAttributeSource解析事务注解 + 2. 事务拦截器: + 1. TransactionInterceptor;保存了事务属性信息,事务管理器; + 2. 他是一个 MethodInterceptor;在目标方法执行的时候; + 1. 先获取事务相关的属性 + 2. 再获取PlatformTransactionManager,如果事先没有添加指定任何transactionmanger,最终会从容器中按照类型获取一个PlatformTransactionManager; + 3. 执行目标方法 + 1. 如果异常,获取到事务管理器,利用事务管理回滚操作; + 2. 如果正常,利用事务管理器,提交事务 + +### Springboot启动流程 + +#### SpringApplication实例 + +```java +public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) { + this.sources = new LinkedHashSet(); // 1. + this.bannerMode = Mode.CONSOLE; + this.logStartupInfo = true; + this.addCommandLineProperties = true; + this.headless = true; + this.registerShutdownHook = true; + this.additionalProfiles = new HashSet(); + this.resourceLoader = resourceLoader; + Assert.notNull(primarySources, "PrimarySources must not be null"); + this.primarySources = new LinkedHashSet(Arrays.asList(primarySources)); + this.webApplicationType = this.deduceWebApplicationType(); + this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class)); // 3. + this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class)); // 4. + this.mainApplicationClass = this.deduceMainApplicationClass(); // 5. + } +``` + +- `com.example.helloworld.HelloworldApplication`放入到Set的集合中 +- 判断是否为Web环境:存在(javax.servlet.Servlet && org.springframework.web.context.ConfigurableWebApplicationContext )类 +- 创建并初始化ApplicationInitializer列表 (spring.factories) +- 创建并初始化ApplicationListener列表 (spring.factories) +- 初始化主类mainApplicatioClass (DemoApplication) +- **总结:上面就是SpringApplication初始化的代码,new SpringApplication()没做啥事情 ,主要加载了META-INF/spring.factories 下面定义的事件监听器接口实现类** + +#### ConfigurableApplicationContext的run方法 + +```java +public ConfigurableApplicationContext run(String... args) { + StopWatch stopWatch = new StopWatch(); // 1. 创建计时器StopWatch + stopWatch.start(); + ConfigurableApplicationContext context = null; + Collection exceptionReporters = new ArrayList(); + this.configureHeadlessProperty(); + SpringApplicationRunListeners listeners = this.getRunListeners(args); // 2. 获取SpringApplicationRunListeners并启动 + listeners.starting(); // + + Collection exceptionReporters; + try { + ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 创建ApplicationArguments + ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments); // 创建并初始化ConfigurableEnvironment + this.configureIgnoreBeanInfo(environment); // + Banner printedBanner = this.printBanner(environment); // 打印Banner + context = this.createApplicationContext(); // 创建ConfigurableApplicationContext + exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context); + this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);// 准备ConfigurableApplicationContext + this.refreshContext(context); // 刷新ConfigurableApplicationContext,这个refreshContext()加载了bean,还启动了内置web容器,需要细细的去看看 + this.afterRefresh(context, applicationArguments); // 容器刷新后动作,啥都没做 + stopWatch.stop();// 计时器停止计时 + if (this.logStartupInfo) { + (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch); + } + + listeners.started(context); + this.callRunners(context, applicationArguments); + } catch (Throwable var10) { + this.handleRunFailure(context, var10, exceptionReporters, listeners); + throw new IllegalStateException(var10); + } + + try { + listeners.running(context); + return context; + } catch (Throwable var9) { + this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null); + throw new IllegalStateException(var9); + } + } +``` + +- 创建计时器StopWatch +- 获取SpringApplicationRunListeners并启动 +- 创建ApplicationArguments +- 创建并初始化ConfigurableEnvironment +- 打印Banner +- 创建ConfigurableApplicationContext +- 准备ConfigurableApplicationContext +- 刷新ConfigurableApplicationContext,**这个refreshContext()加载了bean,还启动了内置web容器,需要细细的去看看** +- 容器刷新后动作,啥都没做 +- 计时器停止计时 + +#### refreshContext() + +**该源码中其实就是Spring源码的refresh()的源码** + +- **不过这里的refresh()是在`AbstractApplicationContext`抽象类上** +- **其他就不提了,关注点在onrefresh()方法上,但是个空方法,毕竟是抽象类,去找其子类继承的它** +- **debug调试可以找到ServletWebServerApplicationContext** + +#### ServletWebServerApplicationContext + +![](http://media.dreamcat.ink//20200203211202.png) + +- `onRefresh()`->`createWebServer()`->`getWebServerFactory()`,此时已经加载了个web容器 +- 可以返回刚才的`createWebServer()`,然后看`factory.getWebServer` + +```java +public WebServer getWebServer(ServletContextInitializer... initializers) { + //tomcat这位大哥出现了 + Tomcat tomcat = new Tomcat(); + File baseDir = (this.baseDirectory != null ? this.baseDirectory + : createTempDir("tomcat")); + tomcat.setBaseDir(baseDir.getAbsolutePath()); + Connector connector = new Connector(this.protocol); + tomcat.getService().addConnector(connector); + customizeConnector(connector); + tomcat.setConnector(connector); + tomcat.getHost().setAutoDeploy(false); + configureEngine(tomcat.getEngine()); + for (Connector additionalConnector : this.additionalTomcatConnectors) { + tomcat.getService().addConnector(additionalConnector); + } + prepareContext(tomcat.getHost(), initializers); + return getTomcatWebServer(tomcat); + } +``` + +- 内置的Tomcat就出现了 +- **总结:run() 方法主要调用了spring容器启动方法扫描配置,加载bean到spring容器中;启动的内置Web容器** + +#### SpringBootApplication的注解 + +**主要是三个注解** + +- @SpringBootConfiguration:允许在上下文中注册额外的bean或导入其他配置类。 +- @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制 +- @ComponentScan: 扫描常用的注解 + diff --git "a/Java/crazy/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" "b/Java/crazy/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" new file mode 100644 index 00000000..3c5d998c --- /dev/null +++ "b/Java/crazy/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" @@ -0,0 +1,520 @@ +> 计算机网络,那是必问环节咯,而且问的也都很固定,也多看看以及理解理解。 + +## 大纲 + +![计算机网络](http://media.dreamcat.ink/uPic/计算机网络.png) + +### 网络模型 + +![分层模型](http://media.dreamcat.ink/uPic/分层模型.png) + +#### 简要概括 + +- 物理层:底层数据传输,如网线;网卡标准。 + +- 数据链路层:定义数据的基本格式,如何传输,如何标识;如网卡MAC地址。 + +- 网络层:定义IP编址,定义路由功能;如不同设备的数据转发。 + +- 传输层:端到端传输数据的基本功能;如 TCP、UDP。 + +- 会话层:控制应用程序之间会话能力;如不同软件数据分发给不同软件。 + +- 标识层:数据格式标识,基本压缩加密功能。 + +- 应用层:各种应用软件,包括 Web 应用。 + +#### 流程 + +比如,计算机 A 和 计算机 B 要进行信息交互,比如 A 上开发了一个网页,需要 B 去访问。B 发出一个请求给 A,那么请求数据从 B 的 **应用层开始向下传到表示层、再从表示层传到会话层直到物理层,通过物理层传递到 A,A 的物理层接到请求后将请求向上传递到自己的应用层,应用层再将要请求的数据向自己的物理层方向传递然后 B 接到数据传递数据到自己的应用层**。 + +说明: + +- 在四层,既传输层数据被称作**段**(Segments); +- 三层网络层数据被称做**包**(Packages); +- 二层数据链路层时数据被称为**帧**(Frames); +- 一层物理层时数据被称为**比特流**(Bits)。 + +#### 常见的端口号和协议号 + +![协议端口号](http://media.dreamcat.ink/uPic/EuHz2s.png) + +#### 总结 + +- 网络七层模型是一个标准,而非实现。 +- 网络四层模型是一个实现的应用模型。 +- 网络四层模型由七层模型简化合并而来。 + +#### ping命令基于哪一层协议的原理是什么? + +ping命令基于网络层的命令,是基于ICMP协议工作的。 + +### DNS + +#### DNS是什么 + +**官方解释**:DNS(Domain Name System,域名系统),因特网上作为**域名和IP地址相互映射**的一个**分布式数据库**,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)。 + +**通俗的讲**,我们更习惯于记住一个网站的名字,比如www.baidu.com,而不是记住它的ip地址,比如:167.23.10.2。 + +#### 谈谈DNS解析过程 + +![DNS解析过程](http://media.dreamcat.ink/uPic/DNS解析过程.png) + +- 请求一旦发起,若是chrome浏览器,先在浏览器找之前**有没有缓存过的域名所对应的ip地址**,有的话,直接跳过dns解析了,若是没有,就会**找硬盘的hosts文件**,看看有没有,有的话,直接找到hosts文件里面的ip +- 如果本地的hosts文件没有能的到对应的ip地址,浏览器会发出一个**dns请求到本地dns服务器**,**本地dns服务器一般都是你的网络接入服务器商提供**,比如中国电信,中国移动等。 +- 查询你输入的网址的DNS请求到达本地DNS服务器之后,**本地DNS服务器会首先查询它的缓存记录**,如果缓存中有此条记录,就可以直接返回结果,此过程是**递归的方式进行查询**。如果没有,本地DNS服务器还要向**DNS根服务器**进行查询。 +- 本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。 +- 最后,本地DNS服务器向**域名的解析服务器**发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。 + +#### DNS查询方式 + +##### 递归解析 + +当局部DNS服务器自己不能回答客户机的DNS查询时,它就需要向其他DNS服务器进行查询。此时有两种方式。**局部DNS服务器自己负责向其他DNS服务器进行查询,一般是先向该域名的根域服务器查询,再由根域名服务器一级级向下查询**。最后得到的查询结果返回给局部DNS服务器,再由局部DNS服务器返回给客户端。 + +##### 迭代解析 + +当局部DNS服务器自己不能回答客户机的DNS查询时,也可以通过迭代查询的方式进行解析。局部DNS服务器不是自己向其他DNS服务器进行查询,**而是把能解析该域名的其他DNS服务器的IP地址返回给客户端DNS程序**,客户端DNS程序再继续向这些DNS服务器进行查询,直到得到查询结果为止。也就是说,迭代解析只是帮你找到相关的服务器而已,而不会帮你去查。比如说:baidu.com的服务器ip地址在192.168.4.5这里,你自己去查吧,本人比较忙,只能帮你到这里了。 + +#### DNS负载均衡 + +当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。处理办法就是用DNS负载均衡技术,它的原理是在**DNS服务器中为同一个主机名配置多个IP地址,在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器**,从而达到负载均衡的目的。例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等。 + +#### 为什么域名解析用UDP协议? + +因为UDP快啊!UDP的DNS协议只要一个请求、一个应答就好了。而使用基于TCP的DNS协议要三次握手、发送数据以及应答、四次挥手。但是UDP协议传输内容不能超过512字节。不过客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。 + +#### 为什么区域传送用TCP协议? + +因为TCP协议可靠性好啊!你要从主DNS上复制内容啊,你用不可靠的UDP? 因为TCP协议传输的内容大啊,你用最大只能传512字节的UDP协议?万一同步的数据大于512字节,你怎么办? + +### HTTP + +#### 请求和相应报文 + +##### 请求报文 + +简单来说: + +- 请求行:Request Line +- 请求头:Request Headers +- 请求体:Request Body + +##### 响应报文 + +简单来说: + +- 状态行:Status Line +- 响应头:Response Headers +- 响应体:Response Body + +#### HTTP请求方法 + +##### GET + +> 获取资源 + +当前网络请求中,绝大部分使用的是 GET 方法。 + +##### HEAD + +> 获取报文头部 + +和 GET 方法类似,但是不返回报文实体主体部分。 + +主要用于确认 URL 的有效性以及资源更新的日期时间等。 + +##### POST + +> 传输实体主体 + +POST 主要用来传输数据,而 GET 主要用来获取资源。 + +更多 POST 与 GET 的比较见后 + +##### PUT + +> 上传文件 + +由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。 + +##### PATCH + +> 对资源进行部分修改 + +PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。 + +##### DELETE + +> 删除文件 + +与 PUT 功能相反,并且同样不带验证机制。 + +##### OPTINONS + +> 查询支持的方法 + +查询指定的 URL 能够支持的方法。 + +会返回 `Allow: GET, POST, HEAD, OPTIONS` 这样的内容。 + +##### CONNECT + +> 要求在与代理服务器通信时建立隧道 + +使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。 + +#### GET和POST的区别? + +1. GET使用URL或Cookie传参,而POST将数据放在BODY中 +2. GET方式提交的数据有长度限制,则POST的数据则可以非常大 +3. POST比GET安全,因为数据在地址栏上不可见,没毛病 +4. **本质区别**:GET请求是幂等性的,POST请求不是。 + +> 这里的幂等性:幂等性是指一次和多次请求某一个资源应该具有同样的副作用。简单来说意味着对同一URL的多个请求应该返回同样的结果。 + +正因为它们有这样的区别,所以不应该且**不能用get请求做数据的增删改这些有副作用的操作**。因为get请求是幂等的,**在网络不好的隧道中会尝试重试**。如果用get请求增数据,会有**重复操作**的风险,而这种重复操作可能会导致副作用(浏览器和操作系统并不知道你会用get请求去做增操作)。 + +#### HTTP状态码 + +| 状态码 | 类别 | 含义 | +| ------ | -------------------------------- | -------------------------- | +| 1XX | Informational(信息性状态码) | 接收的请求正在处理 | +| 2XX | Success(成功状态码) | 请求正常处理完毕 | +| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 | +| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 | +| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出 | + +##### 1xx 信息 + +**100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 + +##### 2xx 成功 + +- **200 OK** +- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 +- **206 Partial Content** :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 + +##### 3xx 重定向 + +- **301 Moved Permanently** :永久性重定向 +- **302 Found** :临时性重定向 +- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。 +- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 +- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 + +##### 4xx 客户端错误 + +- **400 Bad Request** :请求报文中存在语法错误。 +- **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 +- **403 Forbidden** :请求被拒绝。 +- **404 Not Found** + +##### 5xx 服务器错误 + +- **500 Internal Server Error** :服务器正在执行请求时发生错误。 +- **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 + +#### HTTP首部 + +> 这块有点多,可参考[http首部](https://github.com/DreamCats/JavaBooks/blob/master/Interview/network/计算机网络原理-http那些事儿.md#http首部) + +#### Cookies + +HTTP 协议是**无状态**的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 + +Cookie 是**服务器发送到用户浏览器并保存在本地的一小块数据**,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 + +Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。 + +##### 用途 + +- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) +- 个性化设置(如用户自定义设置、主题等) +- 浏览器行为跟踪(如跟踪分析用户行为等) + +#### Session + +除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 + +Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 + +使用 Session 维护用户登录状态的过程如下: + +- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; +- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; +- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; +- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 + +**注意**:Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 + +#### Cookie和Session的选择 + +- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session; +- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; +- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 + +#### JWT + +JWT(json web token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。 + +cookie+session这种模式通常是保存在**内存**中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。而JWT不是这样的,**只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可**。 + +**JWT的构成**: + +第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。详情请见[官网](https://jwt.io/introduction/) + +**JWT总结**: + +1. 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。 +2. payload部分,JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。 +3. 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。它不需要在服务端保存会话信息, 所以它易于应用的扩展。 + +#### 浏览器在与服务器建立了一个 TCP 连接后是否会在一个 HTTP 请求完成后断开?什么情况下会断开? + +在 HTTP/1.0 中,一个服务器在发送完一个 HTTP 响应后,会断开 TCP 链接。但是这样每次请求都会重新建立和断开 TCP 连接,代价过大。所以虽然标准中没有设定,**某些服务器对 Connection: keep-alive 的 Header 进行了支持**。意思是说,完成这个 HTTP 请求之后,不要断开 HTTP 请求使用的 TCP 连接。这样的好处是连接可以被重新使用,之后发送 HTTP 请求的时候不需要重新建立 TCP 连接,以及如果维持连接,那么 SSL 的开销也可以避免。 + +**持久连接**:既然维持 TCP 连接好处这么多,HTTP/1.1 就把 Connection 头写进标准,并且默认开启持久连接,除非请求中写明 Connection: close,那么浏览器和服务器之间是会维持一段时间的 TCP 连接,不会一个请求结束就断掉。 + +默认情况下建立 TCP 连接不会断开,只有在请求报头中声明 Connection: close 才会在请求完成后关闭连接。 + +#### 一个TCP连接可以对应几个HTTP请求? + +如果维持连接,一个 TCP 连接是可以发送多个 HTTP 请求的。 + +#### 一个 TCP 连接中 HTTP 请求发送可以一起发送么(比如一起发三个请求,再三个响应一起接收)? + +HTTP/1.1 存在一个问题,单个 TCP 连接在同一时刻只能处理一个请求,意思是说:两个请求的生命周期不能重叠,任意两个 HTTP 请求从开始到结束的时间在同一个 TCP 连接里不能重叠。 + +在 HTTP/1.1 存在 Pipelining 技术可以完成这个多个请求同时发送,但是由于浏览器默认关闭,所以可以认为这是不可行的。在 HTTP2 中由于 Multiplexing 特点的存在,多个 HTTP 请求可以在同一个 TCP 连接中并行进行。 + +那么在 HTTP/1.1 时代,浏览器是如何提高页面加载效率的呢?主要有下面两点: + +- 维持和服务器已经建立的 TCP 连接,在同一连接上顺序处理多个请求。 +- 和服务器建立多个 TCP 连接。 + +#### 为什么有的时候刷新页面不需要重新建立 SSL 连接? + +TCP 连接有的时候会被浏览器和服务端维持一段时间。TCP 不需要重新建立,SSL 自然也会用之前的。 + +#### 浏览器对同一 Host 建立 TCP 连接到数量有没有限制? + +假设我们还处在 HTTP/1.1 时代,那个时候没有多路传输,当浏览器拿到一个有几十张图片的网页该怎么办呢?肯定不能只开一个 TCP 连接顺序下载,那样用户肯定等的很难受,但是如果每个图片都开一个 TCP 连接发 HTTP 请求,那电脑或者服务器都可能受不了,要是有 1000 张图片的话总不能开 1000 个TCP 连接吧,你的电脑同意 NAT 也不一定会同意。 + +**有。Chrome 最多允许对同一个 Host 建立六个 TCP 连接。不同的浏览器有一些区别。** + +如果图片都是 HTTPS 连接并且在同一个域名下,那么浏览器在 SSL 握手之后会和服务器商量能不能用 HTTP2,如果能的话就使用 Multiplexing 功能在这个连接上进行多路传输。不过也未必会所有挂在这个域名的资源都会使用一个 TCP 连接去获取,但是可以确定的是 Multiplexing 很可能会被用到。 + +如果发现用不了 HTTP2 呢?或者用不了 HTTPS(现实中的 HTTP2 都是在 HTTPS 上实现的,所以也就是只能使用 HTTP/1.1)。那浏览器就会在一个 HOST 上建立多个 TCP 连接,连接数量的最大限制取决于浏览器设置,这些连接会在空闲的时候被浏览器用来发送新的请求,如果所有的连接都正在发送请求呢?那其他的请求就只能等等了。 + +#### 在浏览器中输入url地址后显示主页的过程? + +> - 根据域名,进行DNS域名解析; +> - 拿到解析的IP地址,建立TCP连接; +> - 向IP地址,发送HTTP请求; +> - 服务器处理请求; +> - 返回响应结果; +> - 关闭TCP连接; +> - 浏览器解析HTML; +> - 浏览器布局渲染; + +### HTTPS + +#### HTTPS是什么 + +HTTPS 并不是新协议,而是让 **HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信**。通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 + +#### HTTP的缺点 + +- 使用明文进行通信,内容可能会被窃听; +- 不验证通信方的身份,通信方的身份有可能遭遇伪装; +- 无法证明报文的完整性,报文有可能遭篡改。 + +#### 对称密钥加密 + +对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。 + +- 优点:运算速度快 +- 缺点:无法安全地将密钥传输给通信方 + +#### 非对称密钥加密 + +非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。 + +公开密钥所有人都可以获得,**通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密**,**接收方收到通信内容后使用私有密钥解密**。 + +非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。 + +- 优点:可以更安全地将公开密钥传输给通信发送方; +- 缺点:运算速度慢。 + +#### HTTPS采用的加密方式 + +HTTPS 采用混合的加密机制,使用**非对称密钥加密用于传输对称密钥来保证传输过程的安全性**,之后使用**对称密钥加密进行通信来保证通信过程的效率**。 + +![rsa原理](http://media.dreamcat.ink/uPic/rsa原理.png) + +确保传输安全过程(其实就是rsa原理): + +1. Client给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。 +2. Server确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。 +3. Client确认数字证书有效,然后生成呀一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给Server。 +4. Server使用自己的私钥,获取Client发来的随机数(Premaster secret)。 +5. Client和Server根据约定的加密方法,使用前面的三个随机数,生成”对话密钥”(session key),用来加密接下来的整个对话过程。 + +#### 认证 + +通过使用 **证书** 来对通信方进行认证。 + +数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 + +服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 + +进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。 + +#### HTTP的缺点 + +- 因为需要进行加密解密等过程,因此速度会更慢; +- 需要支付证书授权的高额费用。 + +### TCP/UDP + +#### TCP + +##### TCP是什么? + +`TCP(Transmission Control Protocol 传输控制协议)`是一种面向连接的、可靠的、基于字节流的传输层通信协议。 + +##### TCP头部报文 + +###### source port 和 destination port + +> 两者分别为「源端口号」和「目的端口号」。源端口号就是指本地端口,目的端口就是远程端口。 + +可以这么理解,我们有很多软件,每个软件都对应一个端口,假如,你想和我数据交互,咱们得互相知道你我的端口号。 + +再来一个很官方的: + +> 扩展:应用程序的端口号和应用程序所在主机的 IP 地址统称为 socket(套接字),IP:端口号, 在互联网上 socket 唯一标识每一个应用程序,源端口+源IP+目的端口+目的IP称为”套接字对“,一对套接字就是一个连接,一个客户端与服务器之间的连接。 + +###### Sequence Number + +> 称为「序列号」。用于 TCP 通信过程中某一传输方向上字节流的每个字节的编号,为了确保数据通信的有序性,避免网络中乱序的问题。接收端根据这个编号进行确认,保证分割的数据段在原始数据包的位置。初始序列号由自己定,而后绪的序列号由对端的 ACK 决定:SN_x = ACK_y (x 的序列号 = y 发给 x 的 ACK)。 + +说白了,类似于身份证一样,而且还得发送此时此刻的所在的位置,就相当于身份证上的地址一样。 + +###### Acknowledge Number + +> 称为「确认序列号」。确认序列号是接收确认端所期望收到的下一序列号。确认序号应当是上次已成功收到数据字节序号加1,只有当标志位中的 ACK 标志为 1 时该确认序列号的字段才有效。主要用来解决不丢包的问题。 + +###### TCP Flag + +`TCP` 首部中有 6 个标志比特,它们中的多个可同时被设置为 `1`,主要是用于操控 `TCP` 的状态机的,依次为`URG,ACK,PSH,RST,SYN,FIN`。 + +当然只介绍三个: + +1. **ACK**:这个标识可以理解为发送端发送数据到接收端,发送的时候 ACK 为 0,标识接收端还未应答,一旦接收端接收数据之后,就将 ACK 置为 1,发送端接收到之后,就知道了接收端已经接收了数据。 +2. **SYN**:表示「同步序列号」,是 TCP 握手的发送的第一个数据包。用来建立 TCP 的连接。SYN 标志位和 ACK 标志位搭配使用,当连接请求的时候,SYN=1,ACK=0连接被响应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送一个只有 SYN 的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口。 +3. **FIN**:表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志位的 TCP 数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。发送端只剩最后的一段数据了,同时要告诉接收端后边没有数据可以接受了,所以用FIN标识一下,接收端看到这个FIN之后,哦!这是接受的最后的数据,接受完就关闭了;**TCP四次分手必然问**。 + +###### Window size + +> 称为滑动窗口大小。所说的滑动窗口,用来进行流量控制。 + +#### TCP三次握手 + +![TCP三次握手](http://media.dreamcat.ink/uPic/TCP三次握手.svg) + +- **初始状态**:客户端处于 `closed(关闭)`状态,服务器处于 `listen(监听)` 状态。 +- **第一次握手**:客户端发送请求报文将 `SYN = 1`同步序列号和初始化序列号`seq = x`发送给服务端,发送完之后客户端处于`SYN_Send`状态。(验证了客户端的发送能力和服务端的接收能力) +- **第二次握手**:服务端受到 `SYN` 请求报文之后,如果同意连接,会以自己的同步序列号`SYN(服务端) = 1`、初始化序列号 `seq = y`和确认序列号(期望下次收到的数据包)`ack = x+ 1` 以及确认号`ACK = 1`报文作为应答,服务器为`SYN_Receive`状态。(问题来了,两次握手之后,站在客户端角度上思考:我发送和接收都ok,服务端的发送和接收也都ok。但是站在服务端的角度思考:哎呀,我服务端接收ok,但是我不清楚我的发送ok不ok呀,而且我还不知道你接受能力如何呢?所以老哥,你需要给我三次握手来传个话告诉我一声。你要是不告诉我,万一我认为你跑了,然后我可能出于安全性的考虑继续给你发一次,看看你回不回我。) +- **第三次握手**: 客户端接收到服务端的 `SYN + ACK`之后,知道可以下次可以发送了下一序列的数据包了,然后发送同步序列号 `ack = y + 1`和数据包的序列号 `seq = x + 1`以及确认号`ACK = 1`确认包作为应答,客户端转为`established`状态。(分别站在双方的角度上思考,各自ok) + +#### TCP四次分手 + +![TCP四次分手](http://media.dreamcat.ink/uPic/TCP四次分手.png) + +- **初始化状态**:客户端和服务端都在连接状态,接下来开始进行四次分手断开连接操作。 +- **第一次分手**:第一次分手无论是客户端还是服务端都可以发起,因为 TCP 是全双工的。 + +> 假如客户端发送的数据已经发送完毕,发送FIN = 1 **告诉服务端,客户端所有数据已经全发完了**,**服务端你可以关闭接收了**,但是如果你们服务端有数据要发给客户端,客户端照样可以接收的。此时客户端处于FIN = 1等待服务端确认释放连接状态。 + +- **第二次分手**:服务端接收到客户端的释放请求连接之后,**知道客户端没有数据要发给自己了**,**然后服务端发送ACK = 1告诉客户端收到你发给我的信息**,此时服务端处于 CLOSE_WAIT 等待关闭状态。(服务端先回应给客户端一声,我知道了,但服务端的发送数据能力即将等待关闭,于是接下来第三次就来了。) +- **第三次分手**:此时服务端向客户端把所有的数据发送完了,然后发送一个FIN = 1,**用于告诉客户端,服务端的所有数据发送完毕**,**客户端你也可以关闭接收数据连接了**。此时服务端状态处于LAST_ACK状态,来等待确认客户端是否收到了自己的请求。(服务端等客户端回复是否收到呢,不收到的话,服务端不知道客户端是不是挂掉了还是咋回事呢,所以服务端不敢关闭自己的接收能力,于是第四次就来了。) +- **第四次分手**:此时如果客户端收到了服务端发送完的信息之后,就发送ACK = 1,告诉服务端,客户端已经收到了你的信息。**有一个 2 MSL 的延迟等待**。 + +##### 为什么要有2MSL等待延迟? + +对应这样一种情况,最后客户端发送的ACK = 1给服务端的**过程中丢失**了,服务端没收到,服务端怎么认为的?我已经发送完数据了,怎么客户端没回应我?是不是中途丢失了?然后服务端再次发起断开连接的请求,一个来回就是2MSL。 + +客户端给服务端发送的ACK = 1丢失,**服务端等待 1MSL没收到**,**然后重新发送消息需要1MSL**。如果再次接收到服务端的消息,则**重启2MSL计时器**,**发送确认请求**。客户端只需等待2MSL,如果没有再次收到服务端的消息,就说明服务端已经接收到自己确认消息;此时双方都关闭的连接,TCP 四次分手完毕 + +##### 为什么四次分手? + +任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。 + +#### TCP粘包 + +**TCP粘包**是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。 + +- 由TCP**连接复用**造成的粘包问题。 +- 因为TCP默认会使用**Nagle算法**,此算法会导致粘包问题。 + - 只有上一个分组得到确认,才会发送下一个分组; + - 收集多个小分组,在一个确认到来时一起发送。 +- **数据包过大**造成的粘包问题。 +- 流量控制,**拥塞控制**也可能导致粘包。 +- **接收方不及时接收缓冲区的包,造成多个包接收** + +**解决**: + +1. **Nagle算法**问题导致的,需要结合应用场景适当关闭该算法 +2. 尾部标记序列。通过特殊标识符表示数据包的边界,例如\n\r,\t,或者一些隐藏字符。 +3. 头部标记分步接收。在TCP报文的头部加上表示数据长度。 +4. 应用层发送数据时**定长**发送。 + +#### TCP 协议如何保证可靠传输? + +- **确认和重传**:接收方收到报文就会确认,发送方发送一段时间后没有收到确认就会重传。 +- **数据校验**:TCP报文头有校验和,用于校验报文是否损坏。 +- **数据合理分片和排序**:tcp会按最大传输单元(MTU)合理分片,接收方会缓存未按序到达的数据,重新排序后交给应用层。而UDP:IP数据报大于1500字节,大于MTU。这个时候发送方的IP层就需要分片,把数据报分成若干片,是的每一片都小于MTU。而接收方IP层则需要进行数据报的重组。由于UDP的特性,某一片数据丢失时,接收方便无法重组数据报,导致丢弃整个UDP数据报。 +- **流量控制**:当接收方来不及处理发送方的数据,能通过滑动窗口,提示发送方降低发送的速率,防止包丢失。 +- **拥塞控制**:当网络拥塞时,通过拥塞窗口,减少数据的发送,防止包丢失。 + +#### TCP 利用滑动窗口实现流量控制的机制? + +> 流量控制是为了控制发送方发送速率,保证接收方来得及接收。TCP 利用滑动窗口实现流量控制。 + +TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着**接收方还有多大的缓冲区可以用于接收数据**。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据。 + +> 例如,允许用户终止在远端机上的运行进程。另一种情况是发送方可以发送一个 1 字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。 + +#### TCP拥塞控制的机制以及算法? + +> 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。 + +TCP 发送方要维持一个 **拥塞窗口(cwnd) 的状态变量**。拥塞控制窗口的大小**取决于网络的拥塞程度**,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。TCP的拥塞控制采用了四种算法,即 **慢开始** 、 **拥塞避免** 、**快重传** 和 **快恢复**。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。 + +#### UDP + +提供**无连接**的,尽最大努力的数据传输服务(**不保证数据传输的可靠性**)。 + +##### UDP的特点 + +- UDP是**无连接的**; +- UDP使用**尽最大努力交付**,即不保证可靠交付,因此主机不需要维持复杂的链接状态(这里面有许多参数); +- UDP是**面向报文**的; +- UDP**没有拥塞控制**,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等); +- UDP**支持一对一、一对多、多对一和多对多**的交互通信; +- UDP的**首部开销小**,只有8个字节,比TCP的20个字节的首部要短。 + +那么,再说一次TCP的特点: + +- **TCP是面向连接的**。(就好像打电话一样,通话前需要先拨号建立连接,通话结束后要挂机释放连接); +- 每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的(**一对一**); +- TCP**提供可靠交付的服务**。通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达; +- TCP**提供全双工通信**。TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双方通信的数据; +- **面向字节流**。TCP中的“流”(stream)指的是流入进程或从进程流出的字节序列。“面向字节流”的含义是:虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。 + + + diff --git "a/Collections/Java\351\235\242\347\273\217-ArrayList\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/ArrayList\346\272\220\347\240\201.md" similarity index 98% rename from "Collections/Java\351\235\242\347\273\217-ArrayList\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/ArrayList\346\272\220\347\240\201.md" index bad69c7e..d3015437 100644 --- "a/Collections/Java\351\235\242\347\273\217-ArrayList\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/Java/jdk/ArrayList\346\272\220\347\240\201.md" @@ -1,17 +1,7 @@ ---- -title: Java面经-ArrayList源码解析 -author: DreamCat -id: 2 -date: 2019-10-29 23:51:46 -tags: Java -categories: Java ---- - ## 引言 **Java的集合框架,ArrayList源码解析等...** - ## 概述 diff --git "a/Java/jdk/Class\346\272\220\347\240\201.md" "b/Java/jdk/Class\346\272\220\347\240\201.md" new file mode 100644 index 00000000..3f2495ec --- /dev/null +++ "b/Java/jdk/Class\346\272\220\347\240\201.md" @@ -0,0 +1,151 @@ +## 类 + +```java +// final 修饰 +public final class Class implements java.io.Serializable, + GenericDeclaration, + Type, + AnnotatedElement +``` + +## 方法 + +### registerNatives() + +```java +// 本地方法,之间在object看到 +private static native void registerNatives(); +static { + registerNatives(); +} +``` + +### 构造方法 + +```java +private Class(ClassLoader loader) { + // Initialize final field for classLoader. The initialization value of non-null + // prevents future JIT optimizations from assuming this final field is null. + // 注入类加载器 + classLoader = loader; +} +``` + +### forName() + +```java +// 说白了,返回类对象 +className: 类的权限定名(包名+类名) +initialize: 是否初始化 +ClassLoader:类加载器 +caller:类装入器,没有就null + +@CallerSensitive +public static Class forName(String className) + throws ClassNotFoundException { + Class caller = Reflection.getCallerClass(); + return forName0(className, true, ClassLoader.getClassLoader(caller), caller); +} + +private static native Class forName0(String name, boolean initialize, + ClassLoader loader, + Class caller) + throws ClassNotFoundException; +``` + +### newInstance() + +```java +// 我见的比较少,一般步骤都是先拿class的构造器,接着new对象,如果私有,就暴力 +// 其实这里面还是调用构造器 +@CallerSensitive +public T newInstance() + throws InstantiationException, IllegalAccessException +{ + if (System.getSecurityManager() != null) { + checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false); + } + // 有意思啊,居然不是严格正确 + // NOTE: the following code may not be strictly correct under + // the current Java memory model.// 当前的JMM + + // Constructor lookup // 构造器检查 + if (cachedConstructor == null) { + if (this == Class.class) { + throw new IllegalAccessException( + "Can not call newInstance() on the Class for java.lang.Class" + ); + } + try { + Class[] empty = {}; + final Constructor c = getConstructor0(empty, Member.DECLARED); + // Disable accessibility checks on the constructor + // since we have to do the security check here anyway + // (the stack depth is wrong for the Constructor's + // security check to work) + java.security.AccessController.doPrivileged( + new java.security.PrivilegedAction() { + public Void run() { + c.setAccessible(true); // 暴力解除 + return null; + } + }); + cachedConstructor = c; + } catch (NoSuchMethodException e) { + throw (InstantiationException) + new InstantiationException(getName()).initCause(e); + } + } + Constructor tmpConstructor = cachedConstructor; // 拿到构造器 + // Security check (same as in java.lang.reflect.Constructor) + int modifiers = tmpConstructor.getModifiers(); + if (!Reflection.quickCheckMemberAccess(this, modifiers)) { + Class caller = Reflection.getCallerClass(); + if (newInstanceCallerCache != caller) { + Reflection.ensureMemberAccess(caller, this, null, modifiers); + newInstanceCallerCache = caller; + } + } + // Run constructor + try { + return tmpConstructor.newInstance((Object[])null); // new实例 + } catch (InvocationTargetException e) { + Unsafe.getUnsafe().throwException(e.getTargetException()); + // Not reached + return null; + } +} +``` + +### getFields() + +```java +@CallerSensitive +public Field[] getFields() throws SecurityException { + // 检查, 然后获取 + checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true); + return copyFields(privateGetPublicFields(null)); +} +``` + +### getMethods() + +```java +@CallerSensitive +public Method[] getMethods() throws SecurityException { + // 和上面一样 + checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true); + return copyMethods(privateGetPublicMethods()); +} +``` + +### getConstructors() + +```java +@CallerSensitive +public Constructor[] getConstructors() throws SecurityException { + checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true); + return copyConstructors(privateGetDeclaredConstructors(true)); +} +``` + diff --git "a/Collections/Java\351\235\242\347\273\217-HashSet-HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/HashSet-HashMap\346\272\220\347\240\201.md" similarity index 97% rename from "Collections/Java\351\235\242\347\273\217-HashSet-HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/HashSet-HashMap\346\272\220\347\240\201.md" index d6ff68c1..456a6995 100644 --- "a/Collections/Java\351\235\242\347\273\217-HashSet-HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/Java/jdk/HashSet-HashMap\346\272\220\347\240\201.md" @@ -1,12 +1,3 @@ ---- -title: Java面经- HashSet & HashMap源码解析 -author: DreamCat -id: 1 -date: 2019-10-31 09:38:23 -tags: Java -categories: Java ---- - ## 引言 **Java的集合框架,HashSet HashMap源码解析等...** diff --git "a/Java/jdk/Integer\346\272\220\347\240\201.md" "b/Java/jdk/Integer\346\272\220\347\240\201.md" new file mode 100644 index 00000000..0d3f17d2 --- /dev/null +++ "b/Java/jdk/Integer\346\272\220\347\240\201.md" @@ -0,0 +1,176 @@ +## 类 + +```java +// 看了不少,至少很多工具类,并不希望你们去继承,因此加了final方法 +public final class Integer extends Number implements Comparable +``` + +## 变量 + +```java +have, -231. // 官方注释哈,自然懂 +@Native public static final int MIN_VALUE = 0x80000000; +have, 231-1. +@Native public static final int MAX_VALUE = 0x7fffffff; +private final int value; +@Native public static final int SIZE = 32; + +// 下面方法会用到该静态变量 +final static char[] digits = { + '0' , '1' , '2' , '3' , '4' , '5' , + '6' , '7' , '8' , '9' , 'a' , 'b' , + 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , + 'i' , 'j' , 'k' , 'l' , 'm' , 'n' , + 'o' , 'p' , 'q' , 'r' , 's' , 't' , + 'u' , 'v' , 'w' , 'x' , 'y' , 'z' +}; +``` + +## 方法 + +### parseInt(String s, int radix) + +```java +// 你说说,lc的字符串转数字的题目,是不是? +public static int parseInt(String s, int radix) + throws NumberFormatException +{ + /* + * WARNING: This method may be invoked early during VM initialization + * before IntegerCache is initialized. Care must be taken to not use + * the valueOf method. + */ + + if (s == null) { + throw new NumberFormatException("null"); + } + + if (radix < Character.MIN_RADIX) { + throw new NumberFormatException("radix " + radix + + " less than Character.MIN_RADIX"); + } + + if (radix > Character.MAX_RADIX) { + throw new NumberFormatException("radix " + radix + + " greater than Character.MAX_RADIX"); + } + + int result = 0; // 返回结果值 + boolean negative = false; // 很熟悉的味道 + int i = 0, len = s.length(); // 干嘛?双指针? + int limit = -Integer.MAX_VALUE; // 限制一下哈 + int multmin; + int digit; + + if (len > 0) { + char firstChar = s.charAt(0); // 判断第一个字符是否"+" or "-" + if (firstChar < '0') { // Possible leading "+" or "-" + if (firstChar == '-') { + negative = true; // 负数标志 + limit = Integer.MIN_VALUE; + } else if (firstChar != '+') + throw NumberFormatException.forInputString(s); + + if (len == 1) // Cannot have lone "+" or "-" + throw NumberFormatException.forInputString(s); + i++; + } + multmin = limit / radix; // 这是算啥子?范围? + while (i < len) { + // Accumulating negatively avoids surprises near MAX_VALUE + digit = Character.digit(s.charAt(i++),radix); // digit我还是贴一下吧 + if (digit < 0) { + throw NumberFormatException.forInputString(s); + } + if (result < multmin) { + throw NumberFormatException.forInputString(s); + } + result *= radix; // 关键在这 + if (result < limit + digit) { + throw NumberFormatException.forInputString(s); + } + result -= digit; // 还有这 // 实际上这是负的,因为后面结果有个判断 + // 还有一种是是result = result * 10 + digit 也是可以, 根据正负判断标志 + } + } else { + throw NumberFormatException.forInputString(s); + } + return negative ? result : -result; +} +``` + +### valueOf(String s) + +```java +public static Integer valueOf(String s) throws NumberFormatException { + // 还是调用了一下parseInt + // 不过该方法进行了缓存,缓存范围是-128-127 + return Integer.valueOf(parseInt(s, 10)); +} + +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +### hashCode() + +```java +public int hashCode() { + return Integer.hashCode(value); +} +// 直接就是value哇 +public static int hashCode(int value) { + return value; +} +``` + +### equals() + +```java +public boolean equals(Object obj) { + if (obj instanceof Integer) { + // 比较的是基本类型的value + return value == ((Integer)obj).intValue(); + } + return false; +} +``` + +### bitCount(int i) + +```java +// 返回二进制1的个数,我可写不出来这么神奇的代码 +public static int bitCount(int i) { + // HD, Figure 5-2 + i = i - ((i >>> 1) & 0x55555555); + i = (i & 0x33333333) + ((i >>> 2) & 0x33333333); + i = (i + (i >>> 4)) & 0x0f0f0f0f; + i = i + (i >>> 8); + i = i + (i >>> 16); + return i & 0x3f; +} +``` + +### reverse(int i) + +```java +// 就连翻转都写这么难懂的嘛? +public static int reverse(int i) { + // HD, Figure 7-1 + i = (i & 0x55555555) << 1 | (i >>> 1) & 0x55555555; + i = (i & 0x33333333) << 2 | (i >>> 2) & 0x33333333; + i = (i & 0x0f0f0f0f) << 4 | (i >>> 4) & 0x0f0f0f0f; + i = (i << 24) | ((i & 0xff00) << 8) | + ((i >>> 8) & 0xff00) | (i >>> 24); + return i; +} +``` + + + +注意: + +其他几个包装类就不介绍了 \ No newline at end of file diff --git "a/Java/jdk/InvocationHandler\346\272\220\347\240\201.md" "b/Java/jdk/InvocationHandler\346\272\220\347\240\201.md" new file mode 100644 index 00000000..e4d6e48e --- /dev/null +++ "b/Java/jdk/InvocationHandler\346\272\220\347\240\201.md" @@ -0,0 +1,21 @@ +> 代理经常用到,回调方法,很多框架也的到。 +> +> 不过这仅仅是动态代理 + +## 类 + +```java +// 接口 +public interface InvocationHandler +``` + +## invoke + +```java +// proxy:调用方法的代理实例 +// method:代理实例的接口方法 +// args:参数 +public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable; +``` + diff --git "a/Collections/Java\351\235\242\347\273\217-LinkedHashSet-Map\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/LinkedHashSet\346\272\220\347\240\201.md" similarity index 98% rename from "Collections/Java\351\235\242\347\273\217-LinkedHashSet-Map\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/LinkedHashSet\346\272\220\347\240\201.md" index 387119c5..10430fa4 100644 --- "a/Collections/Java\351\235\242\347\273\217-LinkedHashSet-Map\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/Java/jdk/LinkedHashSet\346\272\220\347\240\201.md" @@ -1,12 +1,3 @@ ---- -title: Java面经- LinkedHashSet & Map源码解析 -author: DreamCat -id: 3 -date: 2019-10-31 18:48:58 -tags: Java -categories: Java ---- - ## 引言 **Java的集合框架,TreeSet & TreeMap源码解析等...** diff --git "a/Collections/Java\351\235\242\347\273\217-LinkedList\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/LinkedList\346\272\220\347\240\201.md" similarity index 99% rename from "Collections/Java\351\235\242\347\273\217-LinkedList\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/LinkedList\346\272\220\347\240\201.md" index 63fb84a8..4be8f76e 100644 --- "a/Collections/Java\351\235\242\347\273\217-LinkedList\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/Java/jdk/LinkedList\346\272\220\347\240\201.md" @@ -1,12 +1,3 @@ ---- -title: Java面经-LinkedList源码解析 -author: DreamCat -id: 1 -date: 2019-10-30 09:00:27 -tags: Java -categories: Java ---- - ## 引言 **Java的集合框架,LinkedList源码解析等...** diff --git "a/Java/jdk/Object\346\272\220\347\240\201.md" "b/Java/jdk/Object\346\272\220\347\240\201.md" new file mode 100644 index 00000000..c6e4146e --- /dev/null +++ "b/Java/jdk/Object\346\272\220\347\240\201.md" @@ -0,0 +1,108 @@ +## 类 + +```java +public class Object // 顶层所有父类,不需要实现和继承。。 造娃第一人 +``` + +## 方法 + +### registerNatives + +```java +static { + registerNatives(); // 打开试试,看名字知道是注册本地方法的 +} + +// 打开是 +private static native void registerNatives(); // 额 +``` + +### getClass() + +```java +public final native Class getClass(); // 本地方法,返回runtime的类 +``` + +### hashcode() + +```java +public native int hashCode(); // 返回对象的哈希码值。 +``` + +注意:对象的哈希码,意味着,只要对象内存地址不一样,这玩意就不一样 + +### equals() + +```java +public boolean equals(Object obj) { + // 还是用了==, ==在对象的时候,比较地址值 + return (this == obj); +} +``` + +### clone() + +```java +// 创建并返回此对象的副本。 +protected native Object clone() throws CloneNotSupportedException; +``` + +### toString() + +```java +public String toString() { + // 哈希码的16进制 + return getClass().getName() + "@" + Integer.toHexString(hashCode()); +} +``` + +### notify() + +```java +// 本地方法,和wait结合使用,很香 +public final native void notify() +``` + +### notifyAll() + +```java +// 很上面一样,不过区别在于,这个能唤醒所有wait的线程 +public final native void notifyAll(); +``` + +### wait() + +```java +public final native void wait(long timeout) throws InterruptedException; + +public final void wait(long timeout, int nanos) throws InterruptedException { + if (timeout < 0) { + throw new IllegalArgumentException("timeout value is negative"); + } + + if (nanos < 0 || nanos > 999999) { + throw new IllegalArgumentException( + "nanosecond timeout value out of range"); + } + + if (nanos > 0) { + timeout++; + } + + wait(timeout); +} + +public final void wait() throws InterruptedException { + wait(0); +} +// 其实还是调用wait(long timeout) + +``` + +### Finalize() + +```java +// jvm gc 你们懂得 回收该对象 +protected void finalize() throws Throwable { } +``` + diff --git "a/Collections/Java\351\235\242\347\273\217-PriorityQueue\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/PriorityQueue\346\272\220\347\240\201.md" similarity index 98% rename from "Collections/Java\351\235\242\347\273\217-PriorityQueue\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/PriorityQueue\346\272\220\347\240\201.md" index bf412cd0..9e3f150f 100644 --- "a/Collections/Java\351\235\242\347\273\217-PriorityQueue\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/Java/jdk/PriorityQueue\346\272\220\347\240\201.md" @@ -1,12 +1,3 @@ ---- -title: Java面经- PriorityQueue源码解析 -author: DreamCat -id: 3 -date: 2019-10-30 16:06:18 -tags: Java -categories: Java ---- - ## 引言 **Java的集合框架,PriorityQueue源码解析等...** diff --git "a/Java/jdk/Proxy\346\272\220\347\240\201.md" "b/Java/jdk/Proxy\346\272\220\347\240\201.md" new file mode 100644 index 00000000..1369afc9 --- /dev/null +++ "b/Java/jdk/Proxy\346\272\220\347\240\201.md" @@ -0,0 +1,70 @@ +> 动态代理经常用到该类的一个方法 + +## newProxyInstance + +```java +1. ClassLoader:类加载器 +2. interfaces:被代理类所实现的接口 +3. InvocationHandler:Invoker回调接口 +@CallerSensitive +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h) + throws IllegalArgumentException +{ + // 回调不能是空 + Objects.requireNonNull(h); + // 拷贝 + final Class[] intfs = interfaces.clone(); + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + // 检查loader有木有问题吧 + checkProxyAccess(Reflection.getCallerClass(), loader, intfs); + } + + /* + * Look up or generate the designated proxy class. + */ + // 一看0,就知道JNT,注释上写的是为该接口生成代理类 + Class cl = getProxyClass0(loader, intfs); + + /* + * Invoke its constructor with the designated invocation handler. + */ + // 用指定的调用处理程序调用它的构造函数。 + try { + if (sm != null) { + checkNewProxyPermission(Reflection.getCallerClass(), cl); + } + // 获取代理类的构造器 + final Constructor cons = cl.getConstructor(constructorParams); + // 拿到参数的invoker + final InvocationHandler ih = h; + // 如果不是public, 直接暴力 + if (!Modifier.isPublic(cl.getModifiers())) { + AccessController.doPrivileged(new PrivilegedAction() { + // 暴力解除 + public Void run() { + cons.setAccessible(true); + return null; + } + }); + } + // 直接根据代理的构造器通过反射new一个实例,接着调用回调invoker + // 当用户通过代理类调用方法的时候,实际上会调用invorker,也就是参数的h + return cons.newInstance(new Object[]{h}); + } catch (IllegalAccessException|InstantiationException e) { + throw new InternalError(e.toString(), e); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else { + throw new InternalError(t.toString(), t); + } + } catch (NoSuchMethodException e) { + throw new InternalError(e.toString(), e); + } +} +``` + diff --git a/Java/jdk/README.md b/Java/jdk/README.md new file mode 100644 index 00000000..c6d78c0c --- /dev/null +++ b/Java/jdk/README.md @@ -0,0 +1,21 @@ +> 说明:JDK1.8版本,我只看了一些重要且部分的源码,目前害没有看完,JUC部分的源码一定要看。看源码能带来很多思维上的好处,闲的时候多看看,能发现其中的原理,不分前后,都挺重要的。 + +- [Object](Object源码.md) +- [Integer](Integer源码.md) +- [String](String源码.md) +- [StringBuilder](StringBuilder源码.md) +- [Thread](Thread源码.md) +- [ThreadLocal](ThreadLocal源码.md) +- [WeakReference](WeakReference源码.md) +- [Proxy](Proxy源码.md) +- [Class](Class源码.md) +- [InvocationHandler](InvocationHandler源码.md) +- [ArrayList](ArrayList源码.md) +- [LinkedList](LinkedList源码.md) +- [HashSet-HashMap](HashSet-HashMap源码.md) +- [LinkedHashSet](LinkedHashSet源码.md) +- [Stack-Queue](Stack-Queue源码.md) +- [PriorityQueue](PriorityQueue源码.md) +- [TreeSet-TreeMap](TreeSet-TreeMap源码.md) + + diff --git "a/Java/jdk/SoftReference\346\272\220\347\240\201.md" "b/Java/jdk/SoftReference\346\272\220\347\240\201.md" new file mode 100644 index 00000000..9cd7a229 --- /dev/null +++ "b/Java/jdk/SoftReference\346\272\220\347\240\201.md" @@ -0,0 +1,31 @@ +## 类 + +```java +// 继承了Reference +public class SoftReference extends Reference +``` + +## 参数 + +```java +// 时间戳时钟,由垃圾收集器更新 +static private long clock; +// 通过每次调用get方法更新时间戳。当选择要清除的软引用时,VM可以使用这个字段,但这不是必需的。 +private long timestamp; +``` + +## 方法 + +### get() + +```java +// 返回该引用对象的引用。 +// 如果该引用对象已经被程序或垃圾收集器清除,则该方法返回null。 +public T get() { + T o = super.get(); + if (o != null && this.timestamp != clock) + this.timestamp = clock; + return o; +} +``` + diff --git "a/Collections/Java\351\235\242\347\273\217-Stack-Queue\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/Stack-Queue\346\272\220\347\240\201.md" similarity index 98% rename from "Collections/Java\351\235\242\347\273\217-Stack-Queue\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/Stack-Queue\346\272\220\347\240\201.md" index 0cff415f..4e28a982 100644 --- "a/Collections/Java\351\235\242\347\273\217-Stack-Queue\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/Java/jdk/Stack-Queue\346\272\220\347\240\201.md" @@ -1,12 +1,3 @@ ---- -title: Java面经-Stack & Queue源码解析 -author: DreamCat -id: 2 -date: 2019-10-30 10:15:02 -tags: Java -categories: Java ---- - ## 引言 **Java的集合框架,Stack & Queue源码解析等...** diff --git "a/Java/jdk/StringBuilder\346\272\220\347\240\201.md" "b/Java/jdk/StringBuilder\346\272\220\347\240\201.md" new file mode 100644 index 00000000..1019d01f --- /dev/null +++ "b/Java/jdk/StringBuilder\346\272\220\347\240\201.md" @@ -0,0 +1,101 @@ +## 类 + +```java +// 虽然该类上加了final +// 但是一会看看变量char[] +// 继承了ASB +// StringBuilder没有char,但是他的抽象父类有 +public final class StringBuilder + extends AbstractStringBuilder + implements java.io.Serializable, CharSequence +``` + +## 变量 + +```java +// 没有被final修饰,因此可变 +char[] value; +// 计数 +int count; +``` + +## 方法 + +### append(char[] str) + +大家最常用的 + +```java +// 毕竟是char数组类型,需要判断扩容 +// 其次是count++,跟ArrayList差不多的,size++ +@Override +public AbstractStringBuilder append(char c) { + ensureCapacityInternal(count + 1); + value[count++] = c; + return this; +} +``` + +### charAt(int index) + +也是大家常用的 + +```java +// 很多都差不多的,看多了,也就那样了 +@Override +public char charAt(int index) { + // 判断是否索引越界 + if ((index < 0) || (index >= count)) + throw new StringIndexOutOfBoundsException(index); + // 按照索引取对应数组上的值 + return value[index]; +} +``` + +### length() + +```java +// 直接返回维护的count变量 +@Override +public int length() { + return count; +} +``` + +### reverse() + +```java +// 看名字就知道翻转 +public AbstractStringBuilder reverse() { + // 维护一个 变量? 是否代理? + boolean hasSurrogates = false; + int n = count - 1; + // 双指针交换位置?而且还是从中间 + // 相当于 abcde j在b, k在d + // j-- k++ + for (int j = (n-1) >> 1; j >= 0; j--) { + int k = n - j; + // 前 + char cj = value[j]; + // 后 + char ck = value[k]; + value[j] = ck; + value[k] = cj; + if (Character.isSurrogate(cj) || + Character.isSurrogate(ck)) { + hasSurrogates = true; + } + } + if (hasSurrogates) { + reverseAllValidSurrogatePairs(); + } + return this; +} +``` + + + +注意: + +StringBuffer跟StringBuilder差不多是一样的,只不过前者在很多方法上加了synchronized同步锁,因此它是线程安全的,而后者不是。 + diff --git "a/Java/jdk/String\346\272\220\347\240\201.md" "b/Java/jdk/String\346\272\220\347\240\201.md" new file mode 100644 index 00000000..b11262a2 --- /dev/null +++ "b/Java/jdk/String\346\272\220\347\240\201.md" @@ -0,0 +1,179 @@ +> 主要针对于面试需要看的参数和方法 + +## 类 + +```java +public final class String implements java.io.Serializable, Comparable, CharSequence +``` + +可以看出该String被final修饰了,因此该类的所有方法都被隐式的加了final + +## 参数 + +```java +// 该值用于字符存储。 +private final char value[]; +// 缓存字符串的哈希代码 +private int hash; +``` + +## 方法 + +### equals + +```java +public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + // 判断是否属于String类 + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; // 获取长度 + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + // 一个一个判断 + while (n-- != 0) { + // 如果不相等,就直接false + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; + } +``` + +### hashcode + +```java +public int hashCode() { + int h = hash; + if (h == 0 && value.length > 0) { + char val[] = value; + + for (int i = 0; i < value.length; i++) { + h = 31 * h + val[i]; // 还是取决于val + } + hash = h; // 缓存hash + } + return h; +} +``` + +### length() + +```java +public int length() { + // 取value数组的长度 + return value.length; +} +``` + +### isEmpty() + +```java +public boolean isEmpty() { + // 长度是否为0 + return value.length == 0; +} +``` + +### toCharArray() + +```java +public char[] toCharArray() { + // Cannot use Arrays.copyOf because of class initialization order issues + // 没有调用Arrays的方法,而是直接调用本地方法,防止类初始化顺序问题 + char result[] = new char[value.length]; + System.arraycopy(value, 0, result, 0, value.length); + return result; +} +``` + +### trim() + +```java +public String trim() { + int len = value.length; + int st = 0; + char[] val = value; /* avoid getfield opcode */ + // 去除前面的空格 + while ((st < len) && (val[st] <= ' ')) { + st++; + } + // 去除后面的空格 + while ((st < len) && (val[len - 1] <= ' ')) { + len--; + } + return ((st > 0) || (len < value.length)) ? substring(st, len) : this; +} +``` + +### replace() + +```java +public String replace(char oldChar, char newChar) { + // 先判断新字符和旧字符是否一样 + if (oldChar != newChar) { + // 取该String的长度 + int len = value.length; + // 初始化个指针 + int i = -1; + // val和value指向同一个数组 + char[] val = value; /* avoid getfield opcode */ + // 先找到旧字符的索引 + while (++i < len) { + if (val[i] == oldChar) { + // 找到旧退出 + break; + } + } + if (i < len) { + // 先new一个缓存,长度len + char buf[] = new char[len]; + // 遍历0,i 把不需要替换的,放进去 + for (int j = 0; j < i; j++) { + buf[j] = val[j]; + } + // 替换新字符的过程 + while (i < len) { + // 先取出旧字符 + char c = val[i]; + // 判断是不是你传来的, 是就替换,不是,就不替换 + buf[i] = (c == oldChar) ? newChar : c; + // 可能重复替换 + i++; + } + return new String(buf, true); + } + } + return this; +} +``` + +### substring() + +```java +public String substring(int beginIndex, int endIndex) { + // 以下都是防止越界 + if (beginIndex < 0) { + throw new StringIndexOutOfBoundsException(beginIndex); + } + if (endIndex > value.length) { + throw new StringIndexOutOfBoundsException(endIndex); + } + int subLen = endIndex - beginIndex; + if (subLen < 0) { + throw new StringIndexOutOfBoundsException(subLen); + } + // new一个String, 这个重载方法,就不看了,底层还是调用了system.copyarray的本地方法 + return ((beginIndex == 0) && (endIndex == value.length)) ? this + : new String(value, beginIndex, subLen); +} +``` + diff --git "a/Java/jdk/ThreadLocal\346\272\220\347\240\201.md" "b/Java/jdk/ThreadLocal\346\272\220\347\240\201.md" new file mode 100644 index 00000000..1348f8d7 --- /dev/null +++ "b/Java/jdk/ThreadLocal\346\272\220\347\240\201.md" @@ -0,0 +1,75 @@ +## 类 + +```java +// 什么也没有,就有个泛型 +public class ThreadLocal +``` + +但是Entry + +```java +// +static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); // key super了, 弱引用 + value = v; + } +} +``` + + + +## 变量 + +```java +// hashcode 是指定一个固定值,开始自增 +private static AtomicInteger nextHashCode = + new AtomicInteger(); +// 连续生成的哈希码之间的区别——将隐式顺序线程本地id转换为接近最优扩散的倍增哈希值,用于两级幂的表。 +private static final int HASH_INCREMENT = 0x61c88647; +``` + +## 方法 + +### get() + +```java +// 依托Thread +public T get() { + // 获取线程 + Thread t = Thread.currentThread(); + // 得到定制的map + ThreadLocalMap map = getMap(t); + if (map != null) { + ThreadLocalMap.Entry e = map.getEntry(this); + if (e != null) { + @SuppressWarnings("unchecked") + T result = (T)e.value; + return result; + } + } + return setInitialValue(); +} +``` + +### set() + +```java +// 依然是map +public void set(T value) { + // 获取线程 + Thread t = Thread.currentThread(); + // 得到map + ThreadLocalMap map = getMap(t); + if (map != null) + // set + map.set(this, value); + else + // 没有就创建 + createMap(t, value); +} +``` + diff --git "a/Java/jdk/Thread\346\272\220\347\240\201.md" "b/Java/jdk/Thread\346\272\220\347\240\201.md" new file mode 100644 index 00000000..f61500dc --- /dev/null +++ "b/Java/jdk/Thread\346\272\220\347\240\201.md" @@ -0,0 +1,105 @@ +## 类 + +```java +// 实现了Runnable +public class Thread implements Runnable +``` + +## 变量 + +```java +// 默认不是守护线程 +private boolean daemon = false; +// run的时候要用到 +private Runnable target; +// 线程组 +private ThreadGroup group; + +// 这里很重要,TL的变量在Thread里面自定义的一种Map +ThreadLocal.ThreadLocalMap threadLocals = null; +``` + +## 方法 + +### start() + +```java +public synchronized void start() { + /** + * This method is not invoked for the main method thread or "system" + * group threads created/set up by the VM. Any new functionality added + * to this method in the future may have to also be added to the VM. + * + * A zero status value corresponds to state "NEW". + */ + if (threadStatus != 0) // 不为0,就抛出异常了 + throw new IllegalThreadStateException(); + + /* Notify the group that this thread is about to be started + * so that it can be added to the group's list of threads + * and the group's unstarted count can be decremented. */ + group.add(this); // 添加到线程组 + + boolean started = false; + try { + start0(); // 本地方法 + started = true; + } finally { + try { + if (!started) { + group.threadStartFailed(this); + } + } catch (Throwable ignore) { + /* do nothing. If start0 threw a Throwable then + it will be passed up the call stack */ + } + } +} + +private native void start0(); +``` + +### run() + +```java +// 实际上走的Runnable接口的run +@Override +public void run() { + if (target != null) { + target.run(); + } +} +``` + +### join(long millis) + +```java +public final synchronized void join(long millis) + throws InterruptedException { + long base = System.currentTimeMillis(); + long now = 0; + + if (millis < 0) { + throw new IllegalArgumentException("timeout value is negative"); + } + + if (millis == 0) { // 调用join,没有传参数,默认是0,那么会来到这里 + while (isAlive()) { // 当run方法走完,才能为false + wait(0); + } + } else { + while (isAlive()) { + long delay = millis - now; + if (delay <= 0) { + break; + } + wait(delay); + now = System.currentTimeMillis() - base; + } + } +} + +// 测试此线程是否处于活动状态。如果一个线程已经启动并且还没有死,那么它就是活的。 +public final native boolean isAlive(); +``` + diff --git "a/Collections/Java\351\235\242\347\273\217-TreeSet-TreeMap\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/TreeSet-TreeMap\346\272\220\347\240\201.md" similarity index 99% rename from "Collections/Java\351\235\242\347\273\217-TreeSet-TreeMap\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/TreeSet-TreeMap\346\272\220\347\240\201.md" index 21bf87bd..4a0b4570 100644 --- "a/Collections/Java\351\235\242\347\273\217-TreeSet-TreeMap\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/Java/jdk/TreeSet-TreeMap\346\272\220\347\240\201.md" @@ -1,12 +1,3 @@ ---- -title: Java面经- TreeSet & TreeMap源码解析 -author: DreamCat -id: 2 -date: 2019-10-31 09:53:49 -tags: Java -categories: Java ---- - ## 引言 **Java的集合框架,TreeSet & TreeMap源码解析等...** diff --git "a/Java/jdk/WeakReference\346\272\220\347\240\201.md" "b/Java/jdk/WeakReference\346\272\220\347\240\201.md" new file mode 100644 index 00000000..c37b8f31 --- /dev/null +++ "b/Java/jdk/WeakReference\346\272\220\347\240\201.md" @@ -0,0 +1,9 @@ +## 类 + +```java + +// 弱引用对象,它不阻止将其引用变为可结束、结束,然后再回收。 +// 弱引用最常用于实现规范化映射。 +public class WeakReference extends Reference +``` + diff --git a/Java/mianjing/README.md b/Java/mianjing/README.md new file mode 100644 index 00000000..b9c3c03c --- /dev/null +++ b/Java/mianjing/README.md @@ -0,0 +1,36 @@ +个人建议: + +> 首先自己先有个知识点体系,不管是思维导图也好,还是大纲也好。 +> 那么其次看大量的面经,可以将每个面经当作一次面试,查看自己是否能讲出来,查漏补缺! +> 最后,不断沉淀即可。祝好运!!! + +文档说明: + +> 秋招后期,有些面试我没有总结,大同小异,仅供参考,可能自己想偷懒了,还有一些有2面3面的,我也懒的总结了,工作量不小,不过大概差不多,面经就是需要复盘,查漏补缺,面试官问的大部分跟你的简历有关系,从简历问的又深又广,不仅仅局限于简历哦,提示:这里的面经是关于Java后端的。 + +## 牛客网面经 + +- [拼多多所有问题汇总](拼多多所有问题汇总.md) +- [京东所有问题汇总](京东所有问题汇总.md) +- [美团所有问题汇总](美团所有问题汇总.md) +- [阿里所有问题汇总](阿里所有问题汇总.md) +- [百度所有问题汇总](百度所有问题汇总.md) +- [字节所有问题汇总](字节所有问题汇总.md) +- [招银所有问题汇总](招银所有问题汇总.md) +- [网易所有问题汇总](网易所有问题汇总.md) +- [远景所有问题汇总](远景所有问题汇总.md) +- [猿辅导所有问题汇总](猿辅导所有问题汇总.md) + +## 自己 + +- [shein](myshein.md) +- [京东](my京东.md) +- [作业帮](my作业帮.md) +- [字节](my字节.md) +- [招银](my招银.md) +- [猿辅导](my猿辅导.md) +- [用友](my用友.md) +- [百度](my百度.md) +- [网易](my网易.md) +- [腾讯](my腾讯.md) +- [贝壳](my贝壳.md) \ No newline at end of file diff --git a/Java/mianjing/myshein.md b/Java/mianjing/myshein.md new file mode 100644 index 00000000..9123cfa5 --- /dev/null +++ b/Java/mianjing/myshein.md @@ -0,0 +1,26 @@ +一面 +时间:8.14 14:30 时长:40 + +1. 自我介绍 +2. 介绍项目(20min) +3. 为什么用Dubbo +4. 为什么用JWT +5. 为什么用Redis +6. 为什么用RocketMQ +7. RocketMQ事务模型 +7. 为什么用分布式锁 +8. MyISAM和InnoDB的区别 +9. B和B+树的区别 +10. 什么会锁整张表 +11. Explain都有啥玩意 +12. 如果是你,如何慢查询优化 + +二面 +时间:8.24 19:50 时长:25min + +1. 自我介绍 +2. 简单说一下微服务 +3. 为什么用索引 +4. 什么情况下用索引 +5. 家庭情况 +6. 反问:如何看待消息队列产生 \ No newline at end of file diff --git "a/Java/mianjing/my\344\272\254\344\270\234.md" "b/Java/mianjing/my\344\272\254\344\270\234.md" new file mode 100644 index 00000000..fe667979 --- /dev/null +++ "b/Java/mianjing/my\344\272\254\344\270\234.md" @@ -0,0 +1,56 @@ + +第一个offer,虽然我记录的比较晚。 +因为我在等字节的意向书 + +时间:7月14日 10:30 时长:40min + +小插曲:和面试官聊了北京和成都的自我感受 + +1. String、StringBuilder、StringBuffer的区别 +2. String不可变的优点 +3. 装箱和拆箱的区别 +4. Integer和int有哪些不同 +5. 重载和重写的区别 +6. 接口和抽象类的区别 +7. ArrayList和LinkedList的区别 +8. HashTable和HashMap的区别 +9. 有哪些同步方法,讲一下 +10. 线程的创建方式 +11. 线程池的原理 +12. 简单说一下Spring +13. 简单说一下Dubbo +14. MQ的特点 +15. 为什么用RocketMQ +16. 说一下Redis的分布式锁 +17. 讲一下RedLock算法 +18. 解决消息幂等的方案 +19. SSO是什么? +20. 说一下Token和Redis交互的过程 +21. Dubbo指定特定的客户端方案 +22. Dubbo的负载均衡都有哪些 +23. 实现一下其中一个负载均衡 + + +时间:7月16日 10:00 时长:24min + +1. 自我介绍 +2. HashTable和HashMap的区别 +3. 项目主要做什么 +4. 班车到点更改状态的业务逻辑 +5. 未来发展 +6. 反问 + +时间:7月21日 14:20 时长:20min + +hr + +1. 自我介绍 +2. 你觉得自己有什么吸引人的地方 +3. 手里还有其他的offer嘛? +4. 对成都和北京有想法? +5. 对自己将来有什么打算? +6. 反问 + +时间:7月31日 17:30 + +已获offer \ No newline at end of file diff --git "a/Java/mianjing/my\344\275\234\344\270\232\345\270\256.md" "b/Java/mianjing/my\344\275\234\344\270\232\345\270\256.md" new file mode 100644 index 00000000..89b4cee5 --- /dev/null +++ "b/Java/mianjing/my\344\275\234\344\270\232\345\270\256.md" @@ -0,0 +1,24 @@ +一面 +时间:8.27 19:00 时长:45min + +1. 自我介绍 +2. 谈一谈Java的面向对象的特性 +3. 谈一谈多态 +4. 讲一讲并发的同步方法 +5. 讲一下synchroniezd和lock的区别 +6. 什么是乐观锁和悲观锁 +7. juc都有哪些? +8. AQS的底层原理 +9. MySQL的索引是什么? +10. 如果优化慢查询 +11. 如何优化数据库 +12. jdbc的线程池 +13. MySQL,Mongodb和ES各自的场景 +14. 讲一下缓存穿透,缓存雪崩以及方案 +15. 谈一谈SpringIOC和AOP +16. 控制反转前后的优缺点 +17. 谈一谈微服务 +18. 谈一谈中间件 +19. MQ如何的特性 +20. 什么是幂等性,如何解决 +21. 反问 \ No newline at end of file diff --git "a/Java/mianjing/my\345\255\227\350\212\202.md" "b/Java/mianjing/my\345\255\227\350\212\202.md" new file mode 100644 index 00000000..2cca2cae --- /dev/null +++ "b/Java/mianjing/my\345\255\227\350\212\202.md" @@ -0,0 +1,174 @@ + +已获意向书 + +时间:7.15 14:00 时长:70min + +一面: + +还是挺紧张的,怕的是他的算法题,不怕其他的。 + +上来还是问了我的买姓的由来。 + +1. 聊项目(问的极其深) +2. 简单介绍一下微服务班车预约系统 +3. 为什么用Dubbo? +4. 为什么分为四个服务? +5. 什么是SSO? +6. 什么是JWT? +7. SSO、JWT和Redis登陆的过程 +8. Redis的5大结构 +9. Zset的底层结构 +10. 订单自动取消业务逻辑(key过期和延迟队列) +11. Dubbo的原理 +12. Dubbo的负载均衡 +13. Dubbo的SPI源码 +14. Redis的分布式锁 +15. RedLock算法 +16. RocketMQ的事务最终一致性 +17. RocketMQ事务模型 +18. 为什么用Sentinel +19. 限流都有哪些算法 +20. 给我讲一下令牌桶 +21. 讲一下Dubbo的服务降级源码 +22. 做题:31.下一个排列 +23. 做题:全排列和全排列II +24. 写SQL:1。反正写分组查询结合条件 +25. 写SQL:2。在上题基础上加个子查询 +26. MySQL索引类型 +27. B和B+的区别 +28. 反问,好奇为什么不问我基础 + + +时间:7.15 16:00 时长:65 + +二面 + +依然追着项目扣细节 + +1. Redis的Token有没有存在安全问题?如何解决? +2. 也不知道怎么扯到https了,讲了https +3. TCP三次握手,各个细节 +4. TCP四次分手,各个细节 +5. TCP三次握手,哪个握手容易遭受到攻击 +6. TCP分手,为什么有2msl +7. 给我讲一下滑动窗口机制 +9. 给我讲一下拥塞控制机制 +10. 给我讲一下Nagle算法 +11. 哈希冲突哪有哪些解决? +12. Redis呢? +13. 写题:1,股票,你们懂得 +14. 写题:2,股票,重复购买 +15. 写题:3,股票,有交易费用 +16. 写题:4,SQL,忘题目了,分组+having+子查询一类的 +17. 平时遇到困难,如何解决? (我扯了10分钟) +18. 平时如何学习?(我扯了10分钟,你们信吗?) +19. 反问 + + + + +等面完,再放新一轮的字节个人面经 + +时间:7月23日:14:00 60min +1. 自我介绍 +2. 介绍项目(40min) +3. 我讲的很细,不知道为啥,二面我没讲那么细,可能累了 +4. TCP所有(10min) +5. HTTPS所有(5min) +6. 写题:链表倒数第k个结点 +7. 反问 + +二面,自我感觉凉了,有几个不会 + +时间:7月27日:14:00 45 min +1. 自我介绍 +2. 介绍项目(5min) +3. 谈谈多态 +4. 说一下多态的底层 +5. 说一下方法表都存什么? +6. 说一下每个类都有一张方法表吗?(这里卡壳了) +7. ConcurrentHashMap和HashMap的区别 +8. 介绍一下Java的锁 +9. 谈谈synchonized优化 +10. 讲一下锁升级 +11. 谈一下异常 +12. 讲一下erorr和exception的区别 +13. 讲一下受检异常和非受检异常 +14. 说一下RuntimeException都有哪些 +15. 说一下如何处理异常 +16. 说一下栈溢出 +17. 说一下堆溢出 +18. 为什么无限递归方法会爆栈? +19. 说一下栈都有什么? +20. 说一下局部变量表都有什么? +21. 谈一下JVM结构 +22. 方法区存什么? +23. 方法区会溢出不? +24. 类元数据哪些会溢出? +25. 进程和线程的区别 +26. 进程通信方式 +27. 内存共享的实现原理 +28. 写题:迭代二叉树的深度 +29. 反问 + + +最后一轮字节 + +一面 + +时间有点长了, 记不清楚了 +1. 自我介绍 +2. 简要介绍一下项目 +3. RocketMQ事务模型 +4. NameServer如何知道broker挂掉了 +5. 怎么防止消息丢失? +6. 谈一谈原子性 +7. 谈一谈Java的锁 +8. HTTP和HTTPS的区别 +9. HTTPS的连接过程 +10. 中间人是什么? +11. 什么是对称加密和非对称加密? +12. CA认证过程? +13. DNS解析过程? +14. 先判断hosts?还是先判断chrome的缓存?更改hosts,chrome缓存失效不? +15. 写题:0~n-1中缺失的数字(两种方法) +16. 反问 + +时间:7月29日:16:30 60 min + +二面 + +1. 自我介绍 +2. 简要介绍一下项目 +3. 多态的原理? +4. JVM的内存模型? +5. 讲一讲volatile? +6. CAS的底层原理 +7. TCP的三次握手 +8. TCP的四次分手 +9. DNS解析过程 +10. URL请求到渲染的过程 +11. 进程调度都有哪些 +12. 进程和线程的区别 +13. Android、React Native和Flutter +14. 写题:二叉搜索树的后序遍历序列 +15. 反问 + +时间:7月31日:14:00 65 min + +三面 + +1. 自我介绍 +2. 介绍项目(40min)害,没忍住 +3. 说一下HashMap(说了很久,7 8分钟的样子) +4. JVM内存模型 +5. 多态的原理 +6. Vue、Android、React Native和flutter +7. 写题:25. K 个一组翻转链表 +8. 反问 + +有些不记得了 + +时间:8月6日:15:30 + +已获意向书 \ No newline at end of file diff --git "a/Java/mianjing/my\346\213\233\351\223\266.md" "b/Java/mianjing/my\346\213\233\351\223\266.md" new file mode 100644 index 00000000..f43d19c3 --- /dev/null +++ "b/Java/mianjing/my\346\213\233\351\223\266.md" @@ -0,0 +1,63 @@ +时间:6月29日 16:32 时长:20min + +1. Mybatis的依赖pom包 +2. maven如何解决循环pom依赖 +3. Java的8大基础类型和所占字节 +4. 类加载器 +5. Redis的缓存穿透和缓存雪崩 +6. Redis的key键过期事件 +7. Redis的延迟队列 +8. RocketMQ的消息最终一致性 +9. 分布式事务(cap,2pc,3pc,tcc) +10. 平时如何学习技术 +11. 未来发展 + + + +时间:7月8日 15.55 时长:45min左右 + +1. 介绍集合 +2. list的并发问题 +3. set是否有序?去重原理?如何有序? +4. 异常机制 +5. 线程的5大状态 +6. 线程的创建方式 +7. 线程池原理 +8. 谈谈双亲委派机制 +9. 垃圾回收算法介绍一下 +10. 谈谈Spring +11. 前端发来一条请求,Spring是怎么处理的(我讲了计算机网络的各个步骤和SpringMVC,讲很细) +12. Redis的OOM溢出原因和解决方案 +13. Redis的缓存穿透 +14. 为什么用RocketMQ,为什么不用其他的MQ? +15. RocketMQ的分布式事务模型 +16. Dubbo的原理 +17. 反问 + +时间问题,没让我手撕代码(手撕代码紧张的一批 + + + +时间:7月14日 17.40 时长:40min + +女面试官,我还有这种运气?碰见女面试官!!!! ,简直不要太爽 + +1. 讲一波买姓的由来(居然对我的姓氏这么感兴趣。。。 阔以),,卡卡,讲了10分钟,从元朝baba的讲到现代 +2. 项目的由来? +3. 为什么要做微服务? +4. 为什么用Dubbo? +5. 为什么分为四个服务? +6. 谈谈Spring cloud +7. 为什么项目里面自己没有用多线程? +8. 哪些用了多线程? +9. 在你遇到什么困难的情况下,为什么用了Redis? +10. 为什么用RocketMQ? +11. 为什么不用2pc、3pc、tcc? +12. 你1个多月做这个项目,代码量不少吧?而且头一次做,怎么做完的?(我熬夜做完的。。,。。。。。。。。。) +13. 项目上线了吗?用什么方案部署上的?了解热部署吗? +14. 为什么没有实习? +15. 项目不问了, 来, 给我讲一下GC的所有垃圾回收器 +16. G1什么版本的时候出来的。。。。。。。。。。 +17. 给我讲讲Redis的五大结构的底层结构,尤其是跳跃表 +18. 有什么想问的?(我今天表现如何?) + diff --git "a/Java/mianjing/my\347\214\277\350\276\205\345\257\274.md" "b/Java/mianjing/my\347\214\277\350\276\205\345\257\274.md" new file mode 100644 index 00000000..b25fb388 --- /dev/null +++ "b/Java/mianjing/my\347\214\277\350\276\205\345\257\274.md" @@ -0,0 +1,17 @@ +一面: +时间 8.28:15:00 时长50min + +1. 自我介绍 +2. 介绍一下项目 +3. MySQL的索引 +4. 慢查询优化 +5. Redis的缓存穿透和雪崩 +6. Dubbo的负载均衡策略 +7. 分布式一致性哈希的原理 +8. zk和redis实现分布式锁的原理 +9. MQ的特性 +10. RocketMQ事务消息的模型 +11. 如何维持幂等性 +12. final的关键字的作用 +13. final关键字修饰引用类型,那么在GC有什么特点 +14. 写题:非递归的对称二叉树 diff --git "a/Java/mianjing/my\347\224\250\345\217\213.md" "b/Java/mianjing/my\347\224\250\345\217\213.md" new file mode 100644 index 00000000..cfad3280 --- /dev/null +++ "b/Java/mianjing/my\347\224\250\345\217\213.md" @@ -0,0 +1,15 @@ + +时间:7月21日 11:00 时长:35min + +1. 自我介绍 +2. 介绍一下项目 +3. 讲一下为什么写这个项目,初衷? +4. 为什么用dubbo +5. 为什么用MQ +6. 讲一下分布式事务 +7. 如果下游服务宕机了怎么办? +8. 讲一下分布式锁 +9. 讲一下RedLock算法 +10. 讲一下zk的分布式锁的实现 +11. 反问 + diff --git "a/Java/mianjing/my\347\231\276\345\272\246.md" "b/Java/mianjing/my\347\231\276\345\272\246.md" new file mode 100644 index 00000000..bc27e2ae --- /dev/null +++ "b/Java/mianjing/my\347\231\276\345\272\246.md" @@ -0,0 +1,144 @@ + +一面 +时间:8.12 12:00 时长:110分钟 + + +1. 自我介绍 +2. 项目介绍(20min) +3. 为什么采用微服务架构? +4. 为什么用Dubbo? +5. 为什么用Redis? +6. 谈一谈Redis的持久化 +7. Redis与MySQL双写一致性方案 +8. 谈一谈zk? +9. zk如何实现分布式锁? +10. zk的leader选举过程 +11. zk的zab算法讲一下 +12. ArrayList和LinkedList的区别 +13. ArrayList的add源码 +14. 讲一下生产者消费者模型(讲了三种方案) +15. 生产者为什么用while? +16. 说一下synchronized的修饰范围以及锁的部门? +17. 了解volatile吗?都有哪些特性?分别举例子 +18. JMM内存模型 +19. JVM运行时区域 +20. 堆、方法区和虚拟机栈分别是什么?存了什么? +21. 方法区的版本变化以及内部常量池的变化? +22. 堆为什么要分成新生代和老年代 +23. 新生代为什么又分为Eden、s0和s1 +24. 标记算法? +25. 可达性分析工作原理?哪些可以作为GC Roots根 +26. 虚拟机栈局部变量表存什么? +27. class加载过程讲一下? +28. new对象,发生了什么过程? 讲一下 +29. 写一道SQL题,不记得了,大致还要用到join和分组求和。 +30. left join right join inner join的区别 +31. TCP三次握手 +32. 写一道题:求一个数组的所有递增子序列。 +33. 反问 + +总结:我倾其所有,想睡个午觉。 + +```java +import java.util.ArrayList; +import java.util.List; + +public class Main { + + static List> sub = new ArrayList<>(); + static List> ret = new ArrayList<>(); + + public static void main(String[] args) { + int[] nums = {4, 6, 9, 8, 8}; + subsets(nums); + isInc(); + for (List list : ret) { + System.out.println(list.toString()); + } + } + + public static void isInc() { + for (List list : sub) { + for (int i = 1; i < list.size(); i++) { + if (list.get(i) < list.get(i - 1)) + break; + if (i == list.size() - 1) + ret.add(new ArrayList<>(list)); + } + } + } + + public static List> subsets(int[] nums) { +// Arrays.sort(nums); + for (int i = 2; i <= nums.length; i++) { + dfs(0, nums, new ArrayList(), i, new boolean[nums.length]); + } + return sub; + } + + private static void dfs(int start, int[] nums, ArrayList list, int size, boolean[] marked) { + if (list.size() == size) { + sub.add(new ArrayList<>(list)); + return; + } + for (int i = start; i < nums.length; i++) { + if (i != 0 && nums[i] == nums[i - 1] && !marked[i - 1]) + continue; + marked[i] = true; + list.add(nums[i]); + dfs(i + 1, nums, list, size, marked); + list.remove(list.size() - 1); + marked[i] = false; + } + } +} +``` + +```java +import java.util.*; + +public class Main { + + static Set> sub = new LinkedHashSet<>(); + static List> ret = new ArrayList<>(); + + public static void main(String[] args) { + int[] nums = {4, 6, 9, 8, 8}; + subsets(nums); + isInc(); + for (List list : ret) { + System.out.println(list.toString()); + } + } + + public static void isInc() { + for (List list : sub) { + for (int i = 1; i < list.size(); i++) { + if (list.get(i) < list.get(i - 1)) + break; + if (i == list.size() - 1) + ret.add(new ArrayList<>(list)); + } + } + } + + public static Set> subsets(int[] nums) { + for (int i = 2; i <= nums.length; i++) { + dfs(0, nums, new ArrayList(), i); + } + return sub; + } + + private static void dfs(int start, int[] nums, ArrayList list, int size) { + if (list.size() == size) { + sub.add(new ArrayList<>(list)); + return; + } + for (int i = start; i < nums.length; i++) { + list.add(nums[i]); + dfs(i + 1, nums, list, size); + list.remove(list.size() - 1); + } + } +} +``` \ No newline at end of file diff --git "a/Java/mianjing/my\347\275\221\346\230\223.md" "b/Java/mianjing/my\347\275\221\346\230\223.md" new file mode 100644 index 00000000..a1550598 --- /dev/null +++ "b/Java/mianjing/my\347\275\221\346\230\223.md" @@ -0,0 +1,45 @@ +一面 +时间:8.13 14:50 时长:35min + + +1. 自我介绍 +2. Redis的集群如何管理,如何选举 +3. G1垃圾收集器如何查找块的 +4. Using Index和Using Where的区别 +5. 如何分库分表? +6. int[10]和int[11]的区别 +7. int[10]和int[10][10]的区别 +8. newarray和anewarray的区别 +9. RocketMQ的消息事务模型原理 +10. RocketMQ的存储为什么那么块? +11. 熟悉分布式事务嘛?(熟悉这个词语吓到我了,直接说了解) +12. 谈一谈分布式如何优化 +13. 限流算法都有哪些? +14. 平时如何学习的? + + +二面 +时间:8.14 19:20 时长:35min + +1. 自我介绍 +2. 谈一谈深度学习 +3. 简单聊以下项目 +4. 为什么用分布式事务 +5. 为何选择Java +6. 如何学习Java +7. Java都看过哪些源码 +8. 谈一谈HashMap +9. 谈一谈ConcurrentHashMap +10. 为何选择网易 +11. JavaSPI机制 +12. Spring SPI机制 +13. 如何是你,你会怎么设计SPI +14. 怎么学习Dubbo和RocketMQ的 +15. 从源码上讲以下Spring bean的初始化前后所有过程(我没讲循环依赖,可惜了) +16. AOP源码 +17. Spring事务原理,以及失效 +18. 如何也让受检异常,事务不失效 +19. for和iterator的区别,并且for会出什么错误 +20. 有没有看过iterator的源码 +21. 反问 + diff --git "a/Java/mianjing/my\350\205\276\350\256\257.md" "b/Java/mianjing/my\350\205\276\350\256\257.md" new file mode 100644 index 00000000..cf5b6a9e --- /dev/null +++ "b/Java/mianjing/my\350\205\276\350\256\257.md" @@ -0,0 +1,44 @@ +一面 +时间:8.20 16:00 时长:130min + +当时问我熟悉不熟悉计算机网络和操作系统之类的,我说的是了解。。。 +那边技术栈不用java的,基本问的java不是特别深入 + + +1. hashmap的底层结构 +2. hashmap的一些参数介绍 +3. hashmap在1.8版改进的地方 +4. hashmap什么情况下查询复杂度高 +5. 红黑树的特点 +6. 红黑树的复杂度 +7. 并发安全的map类 +8. 都有哪些排序算法 +9. 介绍快排 +10. 介绍堆排 +11. 手写单例 +12. 谈一谈volatile +13. 谈一谈synchroinzed +14. mutex如何实现的?过程是什么? +15. 如果是你,你如何实现mutex? +16. 谈一谈gc +17. JVM如何调优 +18. cap是什么 +19. zab算法 +20. 实现paxos算法的工程还有哪个?raft... +21. 哪些中间件用了raft? +22. 为什么zk不用raft? +23. paxos的有哪些缺点 +24. mongodb的底层结构是什么 +25. mysql宕机了,数据怎么办? +26. 两个客户端去修改同一个id的字段,mysql会发生什么? +27. 写一道题:股票 +28. select和epoll的区别 +29. Redis实现分布式锁的过程 +30. 写一道题:翻转序列,求前n项和(我用了个简单方法,但复杂度高,后来面试管提示我用数学方法,他挺好的,就是我太笨了。) +31. 线程和协程的区别是什么? +32. 如果是你,你如何设计协程 +33. 反问 + +总结:算法题写的不好,估计是凉了,我都不好意思耽误面试官那么长时间。 + +大概率凉凉 \ No newline at end of file diff --git "a/Java/mianjing/my\350\264\235\345\243\263.md" "b/Java/mianjing/my\350\264\235\345\243\263.md" new file mode 100644 index 00000000..0793ea02 --- /dev/null +++ "b/Java/mianjing/my\350\264\235\345\243\263.md" @@ -0,0 +1,33 @@ +一面 +时间:8.15 12:00 时长:40min + +1. 写题:数组中的第K个最大元素 +2. Object所有的方法以及作用 +3. CMS收集器的原理和过程及优缺点 +4. HashMap和ConcurrentHashMap的区别(扯细一点) +5. 项目(20min,我太熟悉了) +6. 反问 + + +二面 +时间:8.15 14:00 时长:40min + +1. 自我介绍 +2. 项目(30min,这里我讲的很熟悉,把所有为什么都讲出来了,可以看我git) +3. 数据库结构如何优化 +4. Spring IOC流程 +5. 如何解决循环依赖(按照源码讲) +6. 反问 + + +hr +时间:8.15 15:00 时长:25min +1. 自我介绍 +2. 怎么学习的 +3. 最近看的哪一本书,感受如何 +4. 别人对你如何评价的 +5. 老家在哪 +6. 如何选城市的? +7. 面了几家公司,都有哪些offer(这里,永远都不知道如何回答) +8. 你是怎么看待薪资的 + diff --git "a/Java/mianjing/\344\272\254\344\270\234\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\344\272\254\344\270\234\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..242be943 --- /dev/null +++ "b/Java/mianjing/\344\272\254\344\270\234\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,133 @@ +# 京东所有问题汇总 + +## Java + +- Java虚拟机:2 +- Java虚拟机发生fullGC会造成什么?:2 +- Hashmap了解吗,说一下:1 +- SpringMVC处理请求的流程:5 +- 序列化及使用场景:3 +- 常用的集合类:1 +- ArrayList和LinkedList的区别:3 +- 动态代理如何实现,cglibd动态?如何实现AOP?:7 +- SpringIoC:4 +- Spring的事务管理怎么实现的:3 +- SpringMVC的controller怎么传对象参数,你对这些注解的底层了解有多少:1 +- 抽象类和接口的区别:2 +- 多线程的实现方式:4 +- 异常处理中return的执行顺序:1 +- 代码块的初始化执行顺序:1 +- spring的特性:1 +- hashmap,hashtable和concurrenthashmap区别:8 +- 线程的五个状态:2 +- 线程池:7 +- Jvm运行时数据区:5 +- 内存分配:2 +- 垃圾回收:4 +- 写一个单例模式:2 +- String StringBuilder stringbuffer:2 +- 线程与进程:1 +- oom怎么出现呢,内存泄漏:2 +- spring bean的生命周期:2 +- Spring bean 单例:1 +- HashSet:1 +- 四大引用:1 +- CMS:5 +- G1:4 +- volatile:3 +- sleep 和 wait 的区别:1 +- CAS:2 +- 哪些对象是可能会被 GC 回收的?:1 +- 类加载机制:2 +- springboot优点:1 +- jmm模型:1 +- 八种数据类型:1 +- integer缓冲机制:1 +- autowired和resource的区别:1 +- 说一下nio:1 +- synchronized:1 +- ReentrantLock:1 +- AQS:1 +- 工厂模式:1 +- ThreadLocal:1 +- happens-before:1 + + + + + +## 分布式 +- 分布式锁了解吗?不了解,说了分布式系统设计原则:1 +- 分布式事务:1 + +## MySQL +- 数据库了解吗,聊一下?:1 +- 知道MVCC吗,聊一下:4 +- 数据库引擎相关:5 +- 索引类型:3 +- 乐观锁和悲观锁:1 +- 数据库优化:2 +- 索引的最左匹配原则:3 +- 事务的隔离级别,mysql默认是重复读:6 +- 数据库连接池种类:1 +- MySQL的事务:2 +- mysql最大连接数有了解过吗:1 +- mysql cpu过高怎么排查呀:1(error log) +- binlog存的什么:1 +- 谈谈 explain 结果中⽐较关键的指标,以及指标的含义:1 +- 聚簇索引和非聚簇索引:1 +- B和B+树:3 + + + + +## Redis +- 为什么用Redis:1 +- Redis为什么快?:1 +- Redis的常用数据结构?:3 +- Redis的事务:1 +- redis主从复制怎么完成的:1 +- 跳跃表:1 +- redis的淘汰机制:1 +- Redis原子性原理:1 + + + +## 算法 +- 怎么判断一个数是不是2的n次幂?n&(n-1) +- 怎么求根号2?(二分):1 +- 快排的实现原理:1 +- 如何链表翻转,如何判断链表有环:2 +- 3sum:1 +- 讲讲红黑树:1 +- 删除链表多个值:1 +- 包含min的栈:1 +- 爬楼梯:1 + + +## ES +- 为什么用ES,ES为什么搜索快?:1 +- 为什么数据不全部存储在ES里面?:1 +- 合并两个有序数组:1 + +## MyBatis +- mybatise中#和$的区别:7 +- mybatis分页:1 +- mybatis缓存机制:1 + +## 计算机网络 +- TCP为什么要三次握手:4 +- Tcp,udp区别:1 +- Get,post区别:1 +- 四次分手:2 +- http和https的区别:3 +- TCP/IP 如何保证可靠性:1 +- http常见请求头:1 + +## 其他 +- gbk和utf-8:2 + +## Dubbo和Zk +- 大致说了一下zookeeper的leader过程;:1 +- 关于dubbo和zookeeper的服务架构,这个画出dubbo官网的服务注册订购调用的结构图;:1 +- 服务在多台主机怎么处理:负载均衡,例如轮训和hash等;:1 \ No newline at end of file diff --git "a/Java/mianjing/\345\255\227\350\212\202\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\345\255\227\350\212\202\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..dfcb2070 --- /dev/null +++ "b/Java/mianjing/\345\255\227\350\212\202\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,140 @@ +# Java的哈 + +## 算法(好多树) +- 每K个节点翻转链表(链表k个一旋转):2 +- 二叉搜索树转链表:1 +- 负载均衡算法:1 +- 排序算法哪些是稳定的:1 +- 二叉树求和:1 +- 序列化和反序列化二叉树:1 +- 如何判断一颗树是否是完全二叉树:1 +- 求数组的极值点:1 +- 最大连续子序列:1 +- 回文链表:1 +- 链表反转:1 +- 对称二叉树:1 +- 二叉树右视图:1 +- 移动零:1 +- 给定链表,确定中间数:1 +- 链表奇数位升序、偶数位降序:1 +- 二叉树的左视图:1 +- 一致性hash:1 +- 旋转数组的最小值:1 +- 判断两个链表是否交叉?:1 +- 三数之和:1 +- 两个链表相加:1 +- 给一个序列判断是不是二叉搜索树的后序遍历:1 +- 单词翻转 +- 二叉树层序遍历:1 +- 最长连续子串:1 +- 岛屿:1 +- 无重复字符的最长字串:1 +- 设计一个数据结构 list: rpush rpop lpush lpop index 五种方法的时间复杂度均为 O(1),数据量上限是100w(我回答使用双端队列+hashMap, 面试官说可以用两个数组实现) +- 求集合的子集:1 +- 字符串全排列:1 + +## Java(所有谈谈的,老哥我最喜欢了,狂吹) +- 常见的GC回收器,越详细越好:1 +- SpringMVC的请求过程:1 +- 常见的GC回收器,越详细越好:1 +- 线程池(所有你知道的),原理尽量详细些:2 +- HashMap底层实现:1 +- concurrenthashmap:1 +- ConcurrentHashMap的扩容机制:1 +- LinkedHashMap 底层数据结构?使用场景? :1 +- Spring AOP怎么实现,围绕bean生命周期去讲:1 +- 三大特性:1 +- 谈谈多态:2 +- 接口和抽象类的区别:1 +- 谈谈集合:1 +- Arraylist和LinkedList的区别:1 +- Hashmap底层:1 +- ==跟equals的区别:1 +- 有界无界队列:1 +- 线程的创建方法:1 +- 深拷贝、浅拷贝:1 +- sychronized:1 +- GC算法:1 +- JVM内存结构:1 +- 谈谈cas:1 +- 谈谈JVM:1 +- JDK动态代理:1 +- 类加载的过程:1 +- 说说Object类,作用,有什么方法:1 +- Treeset Treemap的底层实现:1 +- volatile:1 +- 谈谈反射:1 + +> 字节问的Java没啥难度,简单的一批 + +## 计算机网络 +- https通信过程:1 +- https加密过程,怎么判断证书的有效性:1 +- tcp、udp区别 +- tcp(所有):1 +- TCP拥塞控制:1 +- TCP滑动窗口:1 +- http 头部有哪些key:1 +- http状态码有哪些:1 +- DNS服务器怎么处理域名请求的,了解原理吗:1 +- GET、POST:1 +- HTTP2.0有哪些改动:1 +- 路由器怎么工作的:1 +- 七层协议讲一下:1 +- http是否无状态?如何有状态?session和Cookies的区别:1 + + +## MySQL +- 聚簇索引和非聚簇索引底层实现:1 +- 隔离级别:2 +- mysql在业务中怎么实现乐观锁:1(MVCC各种吹) +- MVCC原理,和for update有什么区别:1 +- Innodb\myisam区别:1 +- 谈谈B+树前世今生以及所有:1 +- ACID:1 +- 联合索引底层结构:1 +- SQL里where having分别是做什么的,有什么区别,什么时候用:1 +- MySQL索引类别:1 +- 左连接和内连接的区别:1 +- 谈谈binlog和redolog :1 + + +题: +- 获取所有课程得分均大于80分的学生的平均得分:2 + +## Redis +- 分布式锁怎么实现,Redis加锁过程:1 +- Redis的setnx有哪些注意点,比如宕机时会发生什么:1 +- zset底层原理:(吹它的跳跃表和压缩列表):3 +- Redis中的哨兵:1 +- 谈谈Redis集群 Redis Cluster,以及主从复制原理:1 +- redis的hashmap和java的hashmap有何异同:1 +- 持久化策略:1 +- 利用redis的可重入锁如何设计:1 +- redis分布式锁是否公平锁? + +## 操作系统 + +- 进程间通信有哪些,各个优缺点:2 +- select/poll/epoll:2 +- 用户态、内核态:1 +- 信号量 同步 互斥的区别:1 +- 页面的置换算法:1 +- 进程间的同步方式用过哪些:1 + +## linux +- linux如何查找CPU故障 + +## RocketMQ +- RocketMQ有哪些组件:1 + + +## Mybatis +- mybatis的缓存:1 + +## 项目 +- Jmeter压测的时候为什么会丢包 + + + +> 感觉字节基本没有Spring,可惜了,问的Java也比较基础!! 如果有中间件的话,多熟悉熟悉。 其次就是计算机网络和操作系统的知识多熟悉熟悉,最后就是大家都知道的算法!!! \ No newline at end of file diff --git "a/Java/mianjing/\346\213\233\351\223\266\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\346\213\233\351\223\266\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..2031ec63 --- /dev/null +++ "b/Java/mianjing/\346\213\233\351\223\266\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,134 @@ +# 招银所有问题汇总 + +## Java +- countdownlatch:1 +- submit/execute:1 +- Java多态实现原理:3 +- 线程池:3 +- CAS:2 +- synchronized:3 +- lock:4 +- 多个线程轮流生产a-z :1 +- 生产者-消费者模型:1 +- SpringIoc:2 +- SpringAOP:2 +- Spring的bean生命周期:1 +- Spring的事务如何控制:3 +- Spring的事务传播机制:3 +- 深浅拷贝:1 +- HashMap:6 +- concurrenthashmap:2 +- HashSet:1 +- java的命名规则:1 +- java异常:2 +- 举例9个运行时异常:2 +- Spring中的设计模式:1 +- 重载和重写有什么区别:2 +- SpringMVC:1 +- 抽象类和接口的区别:3 +- Java8新特性:1 +- ==、equlas和hashcode:2 +- Java反射:2 +- 线程的同步方法:3 +- 几种方法创建线程:3 +- 线程的几种状态:1 +- 线程如何从运行到挂起:1 +- 动态代理:2 +- Java类加载如何进行:1 +- java类加载的几种状态:1 +- java jvm中GC如何进行线程是否需要回收的判断:1 +- 可达性分析如何做:1 +- 什么代码能够使jvm进行gc:1 +- Java泛型:2 +- 封装、继承和多态:3 +- JVM模型:2 +- static:1 +- String:1 +- Java集合有哪些:1 +- ArrayList与LinkedList使用场景的区别:1 +- ArrayList动态扩容:1 +- Spring 和Spring Boot的区别:1 +- ThreadLocal:1 +- 生产者-消费者:1 +- io与nio的区别:1 +- nio的实现原理:1 +- nio中selector的状态有哪些:1 +- 手写匿名类:3 +- java内存溢出有哪些情况,如何排查:1 +- 工厂模式 + + +## MySQL +- 数据库存储引擎MyISAM和Innodb的区别:3 +- Innodb什么时候是行锁,什么时候是表锁:1 +- ACID:2 +- 隔离级别:3 +- 数据库删除数据需要的方法,哪种最快,快在哪里:1 +- mysql怎么实现分页:1 +- 数据库有哪些数据类型?:1 +- varchar和char的区别?:1 +- 数据库的索引有哪些?:1 +- b+树索引的叶子结点放的什么?:1 +- 联合索引的最左前缀原则怎么实现的?:1 +- 如何判断一个SQL语句走不走索引?explain:1 +- 遇到慢查询如何处理:1 +- 数据库的三大范式:1 +- b树与b+树的区别:1 +- 持久化:2 +- 数据库锁:3 +- 如何防止sql注入:1 +- MVCC:1 +- MySQL主从复制:1 + + + +## Redis +- 持久化:2 +- 模型: 1 +- 分布式锁的实现:2 +- 缓存和数据库一致如何保证:1 +- redis集群至少需要几个节点:1 +- 为什么需要Redis:1 +- 数据结构:2 +- 缓存击穿、缓存穿透和缓存雪崩:1 + + +## 计算机网络 +- osi七层和tcp/ip四层:1 +- tcp在哪一层:1 +- tcp三次握手,为什么需要第三次握手:1 +- http和https的区别?:2 +- https协议怎么实现?:1 +- 为什么要先用非对称算法加密,后面又用对称算法?:1 +- http请求头有哪些:1 +- get和post的区别:1 + + +## 操作系统 +- 操作系统有哪几个部分组成:1 +- 内存管理:1 +- malloc方法的过程:1 +- 虚拟内存:1 +- 进程通信:1 + +## 分布式事务 +- emmmm 错过了啥 + +## zookeeper +- 分布式锁的实现:1 + +## mybatis +- mybatis比单纯jdbc好在哪里:1 +- mybatis一级二级缓存 +- `#`和`$`的区别 + +## 数据结构与算法 +- 两个队列实现栈 +- 单例 +- 链表环 +- 怎么实现手机号中间四位用星号表示? + +## 微服务 +- 微服务架构的服务粒度怎么确定,服务怎么通信? + + diff --git "a/Java/mianjing/\346\213\274\345\244\232\345\244\232\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\346\213\274\345\244\232\345\244\232\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..ce647f18 --- /dev/null +++ "b/Java/mianjing/\346\213\274\345\244\232\345\244\232\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,118 @@ +# 拼多多所有问题汇总 + +## java +- Java特性:1 +- HashMap:5 +- ConcurrentHashMap:4 +- 乐观锁和悲观锁:1 +- CAS原理:4 +- 动态代理:2 +- 静态代理:1 +- 线程池的原理和运用:3 +- AQS:1 +- spring优化的自动配置:1 +- Spring的AOP:3 +- SpringBoot和SpringMVC区别在哪:1 +- SpringMVC原理:1 +- 单例模式:2 +- 工厂模式:1 +- StringBuilder和Stringbuffer:2 +- ArrayList和LinkedList:1 +- wait()和sleep()的区别:1 +- NIO和BIO的区别:1 +- java的异常:1 +- jvm垃圾回收:2 +- 可重入锁:1 +- Jvm内存区域划分:2 +- 程序计数器的作用:1 +- 本地方法栈和虚拟机栈的区别:1 +- Gc全流程:1 +- Gc算法:1 +- 一致性hash了解吗?:1 +- 同步和异步的区别:1 +- 非阻塞io和阻塞式io的区别:1 +- Volatile:1 +- 死锁:1 +- `A a =new A()`的过程:1 +- 类加载机制:1 +- OSGI加载:1 + +## MySQL +- 组合索引的字段顺序:2 +- MySQL的MVCC:1 +- B+树和B树:2 +- 最左原则:1 +- 数据库事务:1 +- 数据库分库分表:1 +- 数据库索引:1 +- MySQL一条语句的执行过程,解析过程?查询缓存?怎么判断是否命中?:1 +- 数据库两种存储引擎的区别:1 +- 事务隔离:1 +- 索引:1 +- 数据库连接池用的哪个? 数据库连接是线程安全的吗?:1 + + +## Redis +- 缓存穿透、缓存击穿、缓存雪崩都指什么?三者有什么区别,和缓存失效有什么区别:1 +- Redis分布式锁的实现:3 +- redis的数据类型, 使用场景:1 +- 怎么保证Redis的高可用:1 +- redis的ttl指令底层实现,redis过期策略:1 +- redis数据类型,跳表实现:1 +- Setnx加锁的原理:1 +- 怎么解除分布式锁?:1 +- 布隆过滤器了解吗?:1 +- 怎么实现lru?: +- Redis 线程安全,Redis为什么更快:1 + +## 操作系统 +- 线程和进程的区别:4 +- 分段和分页内存管理:1 +- 用户态和内核态的区别:1 + +## 分布式事务 +- 自己实现一个二阶段提交, 如何设计:1 + + + +## 计算机网络 +- 三次握手:2 +- 四次分手:2 +- TCP拥塞控制算法:1 +- TCP和UDP的区别:1 +- HTTPS的实现:2 +- cookie和session:1 +- 网络7层模型:1 +- Ip寻址流程:1 +- Tcp和udp的区别:1 +- http如何保持连接:1 +- 如果不用http,如何保持连接:1 +- cookie和token的区别:1 +- rpc、http、消息队列三者区别:1 +- 访问一个网站的过程:1 + + +## 算法 +- 加密算法:1 +- 找出一个字符串的最大合法十进制数:1 +- 环形打印数组:1 +- 实现一个可以解决冲突的HashMap,用的是比较简陋的hash bucket + 双链表:1 +- 判断单链表有没有环,找到环入口点:1 +- 字符串反转:1 +- 如何选择第k大的数:1 +- 任务调度:1 +- 用给定的数组实现队列,包含push和put方法,要求考虑多线程情况:1 +- 堆排序:1 +- 归并排序: +- 二叉树非递归前序遍历:2 +- 三个数的最大乘积:1 +- 一亿个数找出top 100:1 + + + +## MQ +- 消息队列有什么好处:1 + + +## linux +- linux内存回收策略:1 \ No newline at end of file diff --git "a/Java/mianjing/\347\214\277\350\276\205\345\257\274\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\347\214\277\350\276\205\345\257\274\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..ed4904b9 --- /dev/null +++ "b/Java/mianjing/\347\214\277\350\276\205\345\257\274\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,147 @@ + + +## 算法 + +- 括号匹配(lc:22):1 +- 对角线遍历(lc:498) +- 累加数(lc:306) +- 二叉树中序遍历的后继节点 +- 判断—棵二叉树是否是对称的 +- 有若干[30,50,100]优惠券,求给定一个数值求最优的方案。例如∶价格是40=>[30]80 =>[30,50].110=>[30,30,50] +- 搜索旋转数组 +- 带精度的sqrt +- 矩阵中的最长递增路(lc:329) +- 删除单向链表中重复的数字 +- 给一个以字符串表示的非负整数num,移除这个数中的k为数字,使得剩下的数字最小(单调栈) +- 给定一个二叉搜索树,并给出他的前序序列,输出中序序列,时间复杂度O(n),并给出证明 +- 非递归中序遍历 +- 非递归后序遍历 +- 单链表代表一个大整数,给它低位加上一个数,返回加上后的链表 +- 链表压缩,a->a->a->d->d->b->z->z->null,转换为3a->2d->1b->2z->null (写了一个生成新的链表,又要求重写,在原链表上修改) +- 给一棵树,当一个节点的左节点下的两个节点都是1,或者是右节点下的两个节点都是1时,这个节点满足条件。返回所有满足查找条件的节点 +- 求树的直径 +- 判断一棵树是否为二叉平衡树 +- 大数相乘 +- 两个降序链表的第k大元素 +- 二叉树的深度 +- 两个字符串的最长公共子串 +- 给一个队列,借助两个栈,将队列中的元素排序 +- 字符串排序 首先按照长度排序,然后按照字符串排序。I am a an student.------->I a am an student +- 两个字符串是否由相同字符构成 +- N皇后 +- 升序链表转成二叉平衡搜索树 +- 给定一个字符矩阵,里边只有O和X,把所有被X包裹的O变为X +- 一个单向链表,根据value的奇偶性拆成2个链表,并且返回的两个链表是升序的 +- 数组求每一个数的下一个比它大的数字(先写了O(n2),写完想起来了O(n)的栈) +- 给一个数,求出树中祖先节点和其后代节点差值的最大值(思路对了,手撕没过) +- 整数数组拼接成最小数 +- 顺时针递增打印数组 +- 最长回文串 +- 最大正方形面积 +- 合并k个有序链表 +- 区间列表的交集(lc:986) +- 判断两个字符串是否是相似字符串,相似的含义是只允许交换一次字符串的两个位置,使得和第二个字符串相等,那么他们就是相似的。 +```java +ab, ba => true +abax, abax => true +abc, abc => false +aac, abc => false +1 字符串长度是否相等 +2 排序后字符串是否相等 +3 双指针判断相同位置不同字符的对数是否等于2 +4 不同字符为0时时,是否有重复字符 +``` +- 删除倒数第K个节点 +- Z字打印二叉树 +- 非严格递增数组1,2,3,3,3,4,5,找到目标数字所在的第一个位置,无则输出-1,如3,输出2,要求O(log n);(变种二分) +- 从l到r反转链表 +- 最小路径和 +- 输入两个数组,从两个数组中各取一个值,输出两个值的和为0的所有组合并需要去重。 +```java +输入: nums1={1 ,-1,0,0},nums2={-1,-1,0,1} ,输出:{{1,-1},{0,0}} +``` +- topK +- 求一个长度为n的无序数组中,第m大的数 topk? +- 找二叉查找树的两个节点相差的最小值(非递归中序+对比相邻节点的差) +- 二叉树剪枝 +- 二叉树指定层的节点个数 +- 双向链表奇偶位置节点拆分 +- 反转链表,迭代+递归 +- 字符串相加 +- 整数拆分 +- 给一个字符串数组,把由相同字母组成的字符串分入同组 +- 同样给了一个整型数组 [100,2,1,4,3,3,101,200] 从这个数组找到连续元素的长度,比如1,2,3,4 最长输出4。 +- 分割数组(lc:915) +- LeetCode求两个链表和,从头到尾相加即可 +- 一个链表,给定一个目标值,比目标值大的节点去到链表后面,要求不改变相对顺序 比如1 3 2 1 3 2 1 给定目标值2,返回链表1 2 1 2 1 3 33. +- 最长公共子序列 +- 判断二叉树是不是BST +- 求一个数的平方根,精确到0.01。 +- 重排链表(lc:143) +- 利用循环链表写一个队列 + + + +## Java +- 介绍一下垃圾收集器 +- g1和cms有什么区别 +- 为什么g1可以在保证用户吞吐量的情况下进行垃圾收集 +- Java中实现加锁的方式 +- sychronized及锁升级过程 +- 底层AQS实现原理 +- ConcurrentHashMap +- 线程状态 +- 线程池 +- 深拷贝,浅拷贝 +- 说说线程安全 +- Lock和Synchronized的区别 +- 逃逸分析 +- JVM内存区域,这些区域会内存溢出吗,栈什么情况下会溢出,堆呢 +- 堆区的垃圾回收机制,为什要用分代策略? +- 为什么老年代不采用复制算法? +- SpringBoot自动装配的原理 +- Spring中Aop是如何实现的? +- Cglib和JDK的动态代理有什么区别? +- Spring是怎么实现Ioc的? +- Java的反射机制了解吗? +- 反射机制中获得实例的方式有几种?分别是什么? +- getClass和forName得到的Class实例是同一个实例还是不同的实例? +- 讲一下ClassLoader? +- String,StringBuilder,StringBuffer +- HashMap + +## mysql +- mysql建立索引的原则 +- 聚簇索引和非聚簇索引区别 +- B+树结构、为什么不用B树或二叉树 +- 事务四大特性 +- 事务隔离级别 +- 用mysql实现一个分布式锁 +- MVCC具体原理 + +## 计网 +- http讲一下 +- get和post的区别 +- get是否可以有请求体 +- 手写http request报文和 http response报文 +- 四次挥手过程 +- close_wait作用 +- TCP有什么特点? +- TCP具体的可靠性指什么?怎么确保数据的正确性? +- 流量控制和阻塞控制的区别是什么? +- Socket跟其他的方式有什么不一样? +- OSI七层网络模型 +- 网络层有哪些协议? +- ICMP解释一下,有什么作用?除了判断是否能正常通信还有什么作用? + +## mq +- 如果有几百万消息堆积,如何处理 +- 如何保证消息消费的可靠性,顺序性 +- 消息队列方式有什么优缺点? +- 消息队列为什么比共享内存慢?(需要额外的复制) +- 共享内存有什么缺点?它怎么保证同步操作? + + + +## 其他 +- 令牌桶 diff --git "a/Java/mianjing/\347\231\276\345\272\246\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\347\231\276\345\272\246\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..60655d15 --- /dev/null +++ "b/Java/mianjing/\347\231\276\345\272\246\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,205 @@ + +1. 自我介绍 +2. 聊项目(亮点,难点,解决方法以及优缺点) + +## Java + +- String StringBuffer StringBuilder: 3 +- ArrayList和LinkedList的区别:2 +- HashMap、HashTable和ConcurrentHashMap:13 +- HashSet和TreeSet底层有什么区别?:1 +- LinkedHashMap:2 +- == 、 hashcode、equals:5 +- int和Integer的区别:1 +- voatile:6 +- synchronized:10 +- ReentranLock:10 +- JVM内存区域:9 +- GC垃圾回收:2 +- 举几个垃圾回收器 (CMS,G1):4 +- spring aop:4 +- spring事务:5 +- jdk代理和cglib的区别:1 +- Java反射:2 +- Java的异常:3 +- Java泛型:1 +- CAS:4 +- 线程池:5 +- JVM 如何排查OOM:1 +- java对象结构:1 +- 对象头中存储哪些内容:1 +- ThreadLocal的原理:5 +- 创建线程的方式:2 +- 公平锁和非公平锁的区别:1 +- SpringBoot:3 +- SpringMVC:1 +- SpringBoot的starter,导入pom的时候是把所有的pom引入吗:1 +- 类加载的过程:4 +- JDK、JRE、JVM:2 +- Java有哪些数据类型,分别占多少个字节?:2 +- 抽象类与接口之间的区别:3 +- 聊一下集合类:1 +- class类加载和forname加载:1 +- native:1 +- 加载类的方式有哪些;:1 +- class.forName加载类与classloader加载类有什么区别:1 +- 类加载器有哪些:1 +- 死锁:4 +- sleep与wait方法什么区别:2 +- jvm的一些参数:1 +- finalize方法的作用:1 +- 对象锁和类锁的区别?:1 +- 线程的几种状态?:1 +- 深浅拷贝区别:1 +- 静态内部类和内部类的区别:1 +- 类的加载机制:1 +- 并行并发的区别:1 +- Spring Bean构造器原理:1 +- Spring注解实现原理:2 +- Spring的IOC怎么实现:3 +- 循环依赖是怎么解决的:1 +- 同一个类中的无事务的方法A调用有事务的方法B可以生效吗,为什么。:1 +- TLB是什么。:1 +- 多线程之间的通信。:1 +- Thread里面的run和start:1 +- 序列化和反序列化:1 +- Spring中Bean的作用域:1 +- 栈溢出和内存溢出:1 +- spring的三级缓存:1 + + +## 算法 +- 红黑树的性质:1 +- 二分查找:2 +- 给定字符串判断括号是否匹配 (用栈秒了):1 +- B+树的时间复杂度:1 +- B+数:1 +- 单链表排序:1 +- lc287寻找重复数:1 +- 快排:5 +- 堆排:3 +- 一致性hash:1 +- 手撸死锁:2 +- 链表的倒数第n个:1 +- 用多线程实现互斥同步打印123,10次:1 +- 判断回文数,不用字符数组和数字求余求除法:1 +- 给若干个正整数,求出拼接出的最大数(字典序排序):1 +- 手写LRU:3 +- 两个有序链表排序:1 +- 两个字符串的最大公共子串的个数:1 +- 手撸生产者和消费者模型:1 +- 循环链表入口结点;:1 +- 两个交叉链表交叉节点;:2 +- 旋转数组求最小值:1 +- 红黑树,B-树,B+树:1 +- 反转链表:2 +- 两个文件,包含上亿个URL,只有4G内存,如何找出两个文件相同的URL:1 +- 从一个数组中选出数量超过数组长度个数一半的数:1 +- 二叉树右视图:1 +- 找最长无重复字母的子串:1 +- 三数之和:1 +- 数组中的第K个最大元素:1 +- 跳台阶问题:1 +- 判断平衡二叉树:1 +- 两个string表示的大数相乘:1 +- 两个乱序数组求交集:1 +- 两个有序数组,找合并后的中位数:1 +- 搜索旋转排序数组:1 +- 几百万数据,,怎样拿到最大的50个数:1 +- 字符串中最长回文子串的长度:1 +- 最长公共子串:1 + +## 设计模式 +- 动态代理:1 +- 单例:7 +- 观察者:1 +- 装饰模式:1 + +## 计算机网络 +- cookie和session:1 +- HTTP请求报文 +- header(举几个常见的):1 +- REST和RPC概念区别:1 +- HTTP和HTTPS的区别:2 +- HTTPS是怎么建立连接的:4 +- HTTP的状态码:1 +- TCP三次握手:7 +- TCP四次分手:5 +- tcp的拥塞控制,什么时候慢开始。:1 +- dns解析过程,劫持,污染:1 +- 七层模型:2 +- HTTP3为什么用UDP:1 +- TCP和UDP的区别:1 +- User-Agent:1 +- xss攻击:1 +- www.baidu.com哪个是根域名,顶级域名,权威域名:1 +- 说一下arp协议的过程:1 +- 知道arp欺骗吗?:1 +- get post区别:1 +- restful接口和普通http接口有什么区别?:1 + + +## redis +- redis基本数据类型:5 +- redis集群如何保证节点数据安全 (答了哨兵):1 +- redis的分布式锁:1 +- Redis的缓存雪崩,缓存穿透:2 +- redis的持久化:1 +- Redis集群如何进行数据同步;:1 +- redis单线程为什么效率还很高:1 +- redis的模型:2 +- 如何发现热点 key,删除大key的影响分析及解决:1 +- Redis 定期清理数据的策略:1 + + +## MySQL +- 数据库隔离级别:4 +- 数据库引擎:2 +- MySQL的所有锁:2 +- 分页查询语句:1 +- 500页数据查第400页很慢,原因,解决方案:1 +- 数据库的悲观锁、乐观锁:1 +- B树和B+树的区别:1 +- MyIsam与Innodb的区别;:1 +- 为何不建议使用limit;:1 +- 写一个sql语句实现查询所有成绩都大于80的学生的姓名:1 + `select sname from student a join (select course, max(score) from student group by course) b on a.course = b.course and a.score = b.score;` +- 数据库索引,聚簇索引,非聚簇索引:1 +- 三范式:1 +- ACID:1 +- 关于大表分页,如果需要查很后面的数据,怎么优化:1 +- 数据库的主从复制的优缺点;:1 + + +## Linux +- linux修改文件权限命令:1 +- 文件权限3个数字分别代表什么:1 +- linux查看进程,停止进程:1 +- 查看端口号被谁占用:1 + + +## nginx +- nginx处理http请求的过程 :1 +- nginx配置负载均衡 :1 + + +## 操作系统 +- 进程和线程区别:4 +- 子进程和父进程,继承了哪些东西:1 +- BIO、NIO、Select、POll、Epoll:2 +- 多进程之间的通信(IPC)。:1 +- CPU调度算法:1 + +## zk +- zookeeper用来干什么:1 + + +## dubbo +- dubbo的客户端服务端的数据处理过程:1 +- dubbo 的注册中心 producer 和consumer怎样去注册和发现:1 + +## MyBatis +- `#`和`$`的区别:2 + +## 智力题 +- 3/5/8升的桶,8升装满水,怎么倒出四升的水?:1 \ No newline at end of file diff --git "a/Java/mianjing/\347\275\221\346\230\223\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\347\275\221\346\230\223\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..b3d781f3 --- /dev/null +++ "b/Java/mianjing/\347\275\221\346\230\223\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,126 @@ +## Java + +- HashMap所有:4 +- HashTable:3 +- ConcurrentHashMap:4 +- 四种修饰符的限制范围:1 +- Object类中的方法:1 +- 接口和抽象类的区别:1 +- 动态代理的两种方式:2 +- Java序列化的方式:1 +- 传值和传引用的区别:1 +- 一个ArrayList在循环过程中删除,会出现什么问题:1 +- 反射(执行某个方法):1 +- 多态:1 +- HashSet底层实现原理:2 + + +## Spring +- @transactional注解在什么情况下会失效:2 +- spring中对查询功能是否需要开启事务,是否有效:1 +- Spring的beanFactory和factoryBean的区别:2 +- Spring AOP的原理:2 +- Spring整个启动的初始化流程介绍一下:1 +- Spring除了单例模式还有什么其他模式吗?:1 +- 讲一讲Spring的事务:2 +- bean作用域:1 +- IOC怎么实现:1 +- Bean的生命周期是什么:1 + + + +## JVM +- JVM的内存结构:2 +- JVM方法栈的工作过程,方法栈和本地方法栈有什么区别:1 +- JVM的栈中引用如何和堆中的对象产生关联:1 +- GC的常见算法,CMS以及G1的垃圾回收过程,CMS的各个阶段哪两个是Stop the world的,CMS会不会产生碎片,G1的优势:3 +- 标记清除和标记整理算法的理解以及优缺点:1 +- eden survivor区的比例,为什么是这个比例,eden survivor的工作过程:1 +- JVM如何判断一个对象是否该被GC,可以视为root的都有哪几种类型:1 +- 强软弱虚引用的区别以及GC对他们执行怎样的操作:1 +- Java是否可以GC直接内存:1 +- Java类加载的过程:3 +- 双亲委派模型的过程以及优势:2 +- 破坏双亲委派的场景。加载同一个类怎么做:1 +- 常用的JVM调优参数:1 +- dump文件的分析:1 + +## 多线程 +- 多进程和多线程的区别?:1 +- Java实现多线程有哪几种方式:1 +- Callable和Future的了解:1 +- 线程池的参数有哪些,在线程池创建一个线程的过程:5 +- volitile关键字的作用,原理:4 +- synchronized关键字的用法,优缺点:3 +- Lock接口有哪些实现类,使用场景是什么:1 +- 可重入锁的用处及实现原理,写时复制的过程,读写锁,分段锁(ConcurrentHashMap中的segment):1 +- 悲观锁,乐观锁,优缺点,CAS有什么缺陷,该如何解决:1 +- ABC三个线程如何保证顺序执行:1 +- 线程的状态都有哪些:1 +- sleep和wait的区别:1 +- notify和notifyAll的区别:1 +- ThreadLocal的了解,实现原理:1 +- CAS和ABA问题:3 +- Java内存模型:2 +- 遇到了死锁,死循环该怎么办:1 + +## MySQL +- MySQL的索引:1 +- 常见的数据库优化手段:1 +- 数据库连接池:1 +- mvvc:1 +- 事务:2 +- select 加锁问题:1 +- MySQL数据库索引有哪几种:1 +- MySQL默认的隔离级别?:1 +- B+树:1 +- 聚簇索引和非聚簇索引有什么区别?:1 +- MySQL乐观锁与悲观锁的实现:1 +- 最左前缀:1 + + + +## 计算机网络 +- TCP,UDP区别:2 +- 三次握手,四次挥手,为什么要四次挥手:1 +- 长连接和短连接:1 +- 连接池适合长连接还是短连接:1 +- 从网站输入一个http的网址会发生什么:2 +- http缓存问题:1 +- 你应该如何优化一个网站 :1 +- 如果有大量的有很多time_wait怎么办?:1 + +## 设计模式 +- 观察者模式 +- 代理模式 +- 单例模式,有五种写法,可以参考文章单例模式的五种实现方式 +- 可以考Spring中使用了哪些设计模式 + +## Redis +- redis和memcached的区别:1 +- redis支持哪些数据结构:1 +- redis是单线程的么,所有的工作都是单线程么:1 +- redis如何存储一个String的:1 +- redis的部署方式,主从,集群:1 +- redis的哨兵模式,一个key值如何在redis集群中找到存储在哪里:1 +- redis持久化策略:1 +- 缓存穿透和缓存雪崩:1 +- 分布式锁用过吗?用Redis怎么实现:1 + + +## MyBatis +- mybatis的加载原理:1 + + +## 数据结构和算法 +- B+树:1 +- 快速排序:3 +- 堆排序:1 +- 插入排序:1 +- 一致性Hash算法,一致性Hash算法的应用:1 + + +## 分布式 +- Paxos算法稍微讲一下:1 +- ZooKeeper最少要几个节点,为什么?:1 +- CAP知道吗?大概讲一讲:1 \ No newline at end of file diff --git "a/Java/mianjing/\347\276\216\345\233\242\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\347\276\216\345\233\242\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..a3fbc848 --- /dev/null +++ "b/Java/mianjing/\347\276\216\345\233\242\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,159 @@ +# 美团所有问题汇总 + +## Java +- 解释一下包装类、装箱与拆箱:2 +- 垃圾回收:6 +- 线程池了解么:12 +- 拒绝策略:3 +- 集合有哪些:1 +- arraylist和linkedlist区别:3 +- hashmap怎么扩容(多线程扩容为什么会死循环),各种过程:9 +- hashset底层:1 +- 如何解决hash冲突:1 +- concurrentHashMap 1.7和1.8:9 +- 接口和抽象类区别:2 +- 新生代:1 +- JVM调优:1 +- 如何判断对象是否应该被回收:5 +- root根包括哪些:5 +- CMS回收过程,优缺点:3 +- G1回收过程:3 +- GC有哪些收集器:3 +- 类加载过程(加载,验证,准备,解析,初始化):1 +- 双亲委派优点:2 +- 线程和进程概念:3 +- synchronized和Lock的区别:3 +- synchronized底层实现:2 +- volatile:6 +- HashMap什么时候用到了红黑树:1 +- 红黑树的特点:1 +- synchronized:1 +- AQS:1 +- SpringIoc和AOP:6 +- Spring用到了什么设计模式:2 +- 单例模式:3 +- 对反射的理解:2 +- 乐观锁和悲观锁:2 +- 偏向锁、轻量级锁和重量级锁:1 +- NIO是什么:2 +- CAS原理:2 +- Springboot对Spring优化的原理:1 +- String里的bean线程安全问题:1 +- String里的bean是单例嘛?:1 +- String里的bean的生命周期:1 +- Java基本类型:2 +- Java运行原理:1 +- JVM内存模型:6 +- Spring里autowired注解和resource注解:1 +- 代码中的异常的处理过程:1 +- String和StringBuffer区别,存储位置是否相同:1 +- hashmap、hashtable 实现:1 +- 公平锁和非公平锁如何实现:1 +- yield sleep结合状态转换讲一下:1 +- TreeMap:1 +- 死锁的四个条件:1 +- Java的四种特性吗?:1 +- ArrayList为什么是快速失败的?:1 +- String、StringBuffer与StringBuilder的区别?分别用在什么场景?:1 +- 多线程的wait和sleep的区别:2 +- 可重入锁:1 +- ThreadLocal了解吗,怎么实现的:2 +- OOM如何分析(通过jmap,jstack等命令):1 +- CopyOnWrite 思想和实现:1 +- 类加载器有哪些:1 +- countdownlatch的底层:1 + + +## MySQL +- ACID:1 +- 聊一下事务:6 +- 事务的隔离:7 +- 索引数据结构:2 +- 为什么用B+树而不用hash和B-Tree:7 +- InooDB和MyISAM的区别:1 +- 回表,联合索引查询:1 +- 最左匹配:3 +- 独占所,共享锁,乐观锁:1 +- mysql分库分表:1 +- sql优化:1 +- 聚集索引和非聚集索引:2 +- 数据库范式:1 +- MySQL慢查询怎么看?:1 +- InnoDB的索引是什么形式的?:1 +- SQL语句的优化器什么的?:1 +- SQL的explain,type什么的?:2 +- Count(*)、count(1)与count(列属性)三者的区别?:1 +- sql查询某个人的所有成绩,两个表,学生表和成绩表:1 +- 问MySQL的innodb的行锁是什么?锁的是什么?:1 +- 一个学生表,选出成绩排名第三的学生和成绩分数第三的学生,怎么写sql?:1 +- 姓名课程成绩,找出没有不及格课程的学生(group by having min >= 60) + +## Redis +- Redis使用场景:1 +- Redis线程模型?多路复用讲一下,为什么redis很快:1 +- 分布式锁有哪些?:1 +- Redis的五种数据类型:2 +- Redis跳表:1 +- 集群:2 +- 持久化:2 +- setnx指令:1 +- 数据库和缓存的读一致性?:1 +- 数据库和缓存的写一致性?:1 +- Redis都说是单线程的,具体是怎么处理请求的:1 +- Redis实现过分布式锁,那有别的方案可以实现分布式锁吗:1 + + +## 网络 +- 三次握手:3 +- 滑动控制:1 +- 七层模型:1 +- 四次挥手:3 +- 为什么TCP能保证不丢失(滑动窗口,拥塞控制):1 +- HTTP和HTTPS的区别:1 +- GET和POST区别:2 +- 一次url请求的过程:3 +- cookie和session区别,怎么用:1 +- https实现过程:2 +- 对称加密和非对称加密?:1 +- TCP和UDP的区别:1 +- DNS解析过程 + + +## 其他 +- 操作系统里进程状态及转换:1 +- 4G内存读取10G日志,统计其中出现频次最高的十个word,如何实现,如何使得速度更快:1 +- 5个赛道25匹马,选出最快的5个:1 + +## 代码 +- 二分查找:4 +- 反转链表:2 +- 反转字符串:1 +- 顺时针填充数组:1 +- 字符串去重:3 +- 查找二进制数据里出现1的次数:1 +- 常用的排序算法哪些,最优时间复杂度,最差时间复杂度,平均时间复杂度,空间复杂度:1 +- 二叉树的两个最远节点的距离:1 +- 字符串转为数字:2 +- 链表环路:1 +- 爬楼梯:1 +- 两个链表的第一个公共节点:1 +- 实现一个阻塞队列:1 +- 两个栈实现队列:1 +- 约瑟夫环问题:1 +- BlockingQueue的生产者和消费者模型:1 +- 一个数组找出出现次数最多的一个数,如果多个数出现的次数相同则输出第一个:1 +- 栈里的数降序排序,额外空间只能再用一个栈:1 +- 链表相加:1 + + +## RocketMQ +- 数据存在哪里 +- 会不会丢失数据 +- 幂等操作 +- 生产者提交数据的流程 +- 如何实现高并发消费,分片 + +## MyBatis +- `#`和`$`符号有什么区别 +- ORM框架的作用 + diff --git "a/Java/mianjing/\350\205\276\350\256\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\350\205\276\350\256\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..1f9323d1 --- /dev/null +++ "b/Java/mianjing/\350\205\276\350\256\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,189 @@ + + + +## 算法 + +- 判断树是否对称:1 +- 在一个大数组里求第100大的数字:1 +- 找出A[1000]、B[1000]、C[1000]中的重合的数字:1 +- 给出25亿个QQ号,找出其中重复的?如果不用bitmap且只有一台机器怎么做?:1 +- 你了解哪些排序?:1 +- 快排什么时候复杂度不好?:1 +- 红黑树的特点?如果给红黑树插入数据,红黑树是怎么做的?:1 +- 写一个栈:1 +- 两个特别大的数相乘:1 +- 求两个已排序数组合并后的中位数:1 +- 设计栈,使得getMin()和getMax()的时间复杂度为O(1):1 +- 一个数组中只有一个数字出现了奇数次,其他数字出现了偶数次,找到出现了奇数次的那个数:1 +- 100层楼和2个玻璃杯,怎样用最少的次数找出杯子在哪一层会碎:1 +- 哈希表是如何实现的?:1 +- 1TB的数据如何进行排序:1 +- 对100TB的数据进行排序?(拆分多个数据段进行排序,然后归并):1 +- 判断链表有环,找环入口:2 +- 100万个数找最小的10个(大顶堆):1 +- 旋转数组找出最小值:1 +- 找链表中间节点:1 +- 找链表倒数第k节点:1 +- 微信发红包 m块钱发给n个人 你怎么设计算法:1 +- 很多数中有俩数重复了咋判断:1 +- 单调栈问题:1 +- 分组反转单向链表:1 +- 非递归实现后序遍历:1 +- 把数组排成最小的数:1 +- 找第k大元素:1 +- 链表循环右移动k位置:1 +- 如何自己实现一个kv结构的,你有哪几种方式:1 +- 用hash表有什么问题,和解决了什么问题?:1 +- 用树解决了什么问题,红黑树和hash分别是有序还是无序的呢?:1 +- 为什么是有序还是无序?具体怎么实现的呢?:1 + +## MySQl + +- mysql慢查询如何优化?:2 +- 优化器是什么时候起作用的?:1 +- MVCC的原理?:1 +- InnoDB和myISAM的区别?:2 +- InnoDB的聚集索引和MyISAM的非聚集索引的区别?:1 +- B+树、B树、红黑树的区别:2 +- 辅助索引的叶子上有什么内容?辅助索引和主键索引性能差距在哪里?:1 +- 数据库事务性质,并发一致性问题:1 +- 数据库存储过程,视图,函数的使用,几种连接方式:1 +- 索引:2 +- 事务的ACID:2 +- 数据库的join操作实际上是MySQL底层是怎么做的呢:1 +- limit a,b是什么意思,会有什么性能上的问题:1(limit之前的数据先查出来、a代表起点、b代表数量,如果a很大的话,那么MySQL需要先去遍历前a条数据而不是直接定位,所以这里存在性能问题) +- 数据库容灾方面问题,数据库挂了怎么办:1 +- 数据库集群怎么实现数据一致性:1 + + +## java +- 从java代码到.class文件,中间经历实现你了哪些过程?:1 +- Java数据类型,大小:1 +- instanceof和getClass:1 +- JMM:2 +- 介绍项目中的SpringIOC DI MVC AOP:1 +- JVM数据区:1 +- GC相关:1 +- OOM情况:1 +- 多态的实现:1 +- Java类的分类:1 +- 普通类,接口,抽象类的区别:1 +- Java创建线程的方式:1 +- 线程的状态:1 +- 单例模式:1 +- 类加载过程:1 +- 双亲委派模型:1 +- Java会出现内存泄露吗:1 +- 如何定位和解决OOM:1 +- Java的GC中什么场景下使用CMS和G1:1 +- hashmap:1 +- JVM参数调优:1 +- Minor GC和Full GC:1 +- 线程池:1 +- spring bean的什么周期:2 +- spring 单例的bean是否线程安全:1 +- 有状态的bean和无状态的bean区别:1 +- spring事务了解么:1 +- 如何实现HashMap取出的顺序和放入顺序一致?:1 +- HashMap的扩容的时间复杂度,如何优化?:1 +- JDK8添加了哪些特性?:1 +- java为什么说它即是解释型语言,又是编译型语言:1 +- 面向对象和面向过程的区别?:1 +- java的类和c++的类有什么区别:1 +- java语言的三大特性:1 +- 怎么拼接多个string:1 +- 讲讲异常:1 +- 深拷贝和浅拷贝:1 +- java的包装类的了解?为啥要有包装类:1 +- Java的集合:1 +- 乐观锁和悲观锁:1 +- synchronize和Lock区别:1 +- synchronized的底层是怎么实现的?:1 +- ReentrantLock的区别:1 +- ThreadLocal的原理是什么:1 +- CountdownLatch:1 + +## redis + +- redis在你项目中怎么用的?防止重复提交是怎么做到的?:1 +- Redis五种数据类型:1 +- Hash底层:1 +- redis跳表的优势:1 +- reactor模式:1 +- redis持久化:1 + + +## 计网 + +- HTTP还是HTTPS请求?有什么区别?:3 +- HTTP过程的四次挥手?:2 +- TIME_WAIT的作用?:2 +- cookie的作用?:1 +- 腾讯和百度两个网页能获取对方的cookie吗?:1 +- 在百度里搜索abc的过程?:1 +- 搜索的时候,数据包是怎么交给网卡的?(7层 5层网络模型)层层封包都加的是什么内容?:1 +- 网卡怎么知道数据是发送给百度服务器的,怎么找到服务器的?:1 +- 计算机网络分层,各层的协议:1 +- http流量控制,拥塞避免如何实现:1 +- 多少种请求方式,get和post区别:1 +- Https端口443以及怎么的实现流程:1 +- Session和Cookie区别:1 +- TCP和UDP的区别:1 +- TCP三次握手:1 +- CLOSE_WAIT:1 +- DNS解析的过程:1 +- HTTP长连接和短连接:1 +- TCP拥塞控制:1 +- 了解粘包吗,怎么设置不粘包:1 +- sql注入如何防范:1 +- xss如何防范:1 +- udp的最大包长度,为什么这么大?:1 +- 傻瓜窗口了解吗?怎么解决?:1 +- socket去写的时候你会怎么写?考虑什么?:1 +- http常见头部信息有哪些:1 +- 你知道https的非对称加密和对称加密使用了哪些算法么:1 + +## os +- 内核态和用户态的区别?:1 +- 用户态通过什么样的接口调用内核? +- 进程在内存中是如何分配的?(段页式及其细节、数据段、栈段、代码段):1 +- 操作系统进程同步方式:1 +- 进程怎样通信:2 +- 套接字:1 +- 线程通信机制:1 +- CPU密集型任务适合多进程还是多线程?:1 +- 进程的同步机制,你在什么时候用:1 +- 向一个进程发出kill信号接下来发生什么?:1 +- 共享内存的坏处:1 +- 操作系统中中断的分类:1 +- 操作系统页置换算法:1 +- 僵尸进程问题:1 +- 页式和段式的区别,优缺点,应用场景。:1 +- 讲一下虚拟内存、页表:1 +- 为什么顺序io比随机io快?:1 +- 随机io的过程是什么?:1 +- 用户态和内核态的区别?如何切换?:1 +- 原子操作的意义:1 +- i++为什么不是原子操作,如何去保证是原子操作?:1 + +## 分布式 + +- 高并发系统大量请求如何优化:1 +- 分布式系统CAP:1 +- 秒杀系统解决超卖问题,(数据库排它锁和乐观锁CAS版本号机制):1 +- base理论:1 +- 两阶段提交:1 +- redis分布式锁的注意事项,实现过程?:1 + +## 亮点 +- Netty,NIO通信框架:1 +- BIO、NIO、AIO:1 +- NIO又可以分为三种,基于轮询、基于多路复用、基于事件回调:1 +- 如何指定使用哪种方式:1 +- 知道他底层怎么实现的吗:1 +- Netty底层Buffer的实现:1 +- 日志清理如何解决:1 +- 日志合并如何解决:1 +- reactor模式:1 +- 讲讲netty的io模型:1 +- 讲讲多路复用机制,你觉得什么时候多路复用性能会比较好? \ No newline at end of file diff --git "a/Java/mianjing/\350\277\234\346\231\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\350\277\234\346\231\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..6021baf1 --- /dev/null +++ "b/Java/mianjing/\350\277\234\346\231\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,25 @@ +## java +- springboot了解多少:2 +- Spring MVC:2 +- bean生命周期:1 +- 为什么用MQ:1 +- JVM内存区域:2 +- 双亲委派模型:1 +- sychronized:1 +- volatile:1 +- ArrayList和LinkedList的区别 + + +## Redis +- Redis的数据结构:1 +- 缓存穿透:1 +- 缓存雪崩:1 +- Redis为什么快:1 +- IO多路复用:1 + + + +## 计算机网络 +- 三次握手:1 +- TCP和UDP:2 +- nginx负载均衡的几种方式:1 \ No newline at end of file diff --git "a/Java/mianjing/\351\230\277\351\207\214\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\351\230\277\351\207\214\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..aad72cbd --- /dev/null +++ "b/Java/mianjing/\351\230\277\351\207\214\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,154 @@ + +## Java + +- JVM分区 +- 垃圾收集算法 +- synchronized原理 +- 双亲委派机制 +- 线程池参数 +- newFixedTheadPool底层,优缺点 +- springmvc +- @Autowired原理 +- springboot原理 +- @OnConditionalClass实现原理 +- @OnMissingBean没有bean会怎么样,会classNotFound +- HashMap 底层数据结构 +- HashMap 什么时候扩容?装载因子和临界值默认是多少?扩容成多大?为什么容量是2的幂次方? +- 线程安全的Map?分段锁是如何实现的?JDK 1.8之后有哪些优化? +- Lock和Synchronized区别 +- AQS实现 +- 锁优化 +- String StringBuilder StringBuffer区别,特点,场景 +- ConcurrentMap 源码 +- CMS +- 线程生命周期(状态) +- ReentrantLock与AQS同步框架 +- CAS原理是什么? +- synchronize原理是什么? +- springboot 和 spring 的区别是什么? +- spring中bean的初始化过程 +- 谈一谈异常机制 +- Spring IOC和AOP +- Arrays.sort()底层原理 + +## 场景题 +- 一个8G的服务器,堆的大小应该设置成多少 +- 海量数据求频率最多的100个 +- spring一个事务中调用另外一个事务,另一个- 事务发生异常会怎么样 +- 一个父类加载器能不能加载一个子类加载器,为什么 +- select * from A where id in (select id from B)怎么优化 +- 一个16G的内存堆分配多少,采用什么垃圾收集器,为什么用cms不用g1,为什么(面试官一直问为什么使用cms或者使用g1,回答了这两个的优缺点之后还是不满意) +- 多线程解析一个超大文件怎么处理,如果文件切分的时候关键信息被分到了不同的解析线程中怎么办 +- hashset是如何判断两个对象相等的 +- 如何要两个对象相等equals和hashcode这两个方法要怎么重写 +- hash算法(最开始讲hash冲突算法,面试官说不是这个,我又说对hash值对质数取余,面试官也说不是这个,不知道他要我回答啥。。。) +- 不使用任何优化,直接访问数据库,如何优化 (提示 redo、undo log的开销)(这里应该还可以答:批处理、连接池等) +- 行锁锁的是索引,如果一个表没有主键怎么加锁?(锁表) +- hashmap 为什么不直接用红黑树,还要用链表? +- 红黑树的特性?各种操作的时间复杂度?最多旋转几次达到平衡? +- finally 中 return 会发生什么? +- 如何破坏双亲委派 +- 浏览器中用户信息和密码的安全有没有考虑过 +- 两个线程如何交替打印A和B +- 100万个数,数据类型是int,给你8核CPU,8G内存,如何求数组的和? +- 很多个不重复的数,假设用的数组来装,我希望你实现一个方法,每次调用这个方法给我随机返回100个不重复,记住是随机的。 +- 四个引用的具体应用场景 +- 垃圾回收器具体应用场景 +- 可达性分析算法具体应用场景 +- 垃圾回收器参数调优(给具体场景 大概就是并发低延时) +- 高并发多连接jvm 线程池参数调优(顺着来) +- 高并发系统调优(再顺一下) +- 队列与栈的 应用场景 +- 点赞高并发怎么办? +- 消息中间件用过么?怎么自己设计一个消息中间件? +- 假设catch,final,catch中有return,那么final还会不会执行 +- 假设1亿的11位的手机号,运行空间128M,如果要进行排序,那么要怎么设计 +- jvm的设计角度,gc你怎么优化? +- 你怎么设计md5函数? +- cpu一直被占满,怎么排查? +- 让你自己设计一个垃圾回收器,尽可能提高回收效率,你会怎么设计?(ZGC和香浓多,整就完事了) +- MD5加密算法,不使用hashmap和映射,你自己来设计一个同类型的,给定字符串可以生成64位随机数的,你怎么设计 +- 利用java现有的东西,让你设计一个对象,实现类似synchronize的功能,使得多个线程不冲突,你如何设计?(ThreadLocal玩起来) +- synchronize锁定.class和锁定一个实例有什么区别? +- explain中的key字段的值等于ref时,有没有触发索引? +- 如何实现单点登录,如何实现权限控制,用户密码泄露后如何保证安全性 +- spring中循环依赖如何解决?如果让你来实现你怎么做? +- 如果我的服务器ip地址变了,客户端如何感知到呢? +- 轮询的负载均衡的缺点是什么?如何改进? +- 让你来实现真正的负载均衡,你如何做?(我回答记录每台服务器在最近的一段时间内接收的请求的数量,每次都把请求发送给数量最小的服务器,后来面试官提醒我还应当考虑每个请求耗费的时间) +- 秒杀项目中静态资源CDN怎么做? +- css文件能放到CDN上吗? +- 秒杀缓存如何与数据库的数据保持一致性? +- 通过广播的方式去更新每个节点的缓存,如果某个节点的缓存更新失败,那么如何排查是哪个节点呢? +- 消费者消费失败是怎么处理的 +- 如何保证消息消费的顺序性 +- 如何保证消息不被重复消费 +- 项目中要进行分布式扩展应该怎么做 +- 缓存和mysql数据一致性如何保证 +- 微信步数排行,假设百万级用户,怎么排序,实时更新(我说排序就是海量数据排序的方式分组排序再归并,然后实时更新的话也是组内更新组内排序,但是每组的分度值可能不一样,比如0-2000可能一组,但2000-4000可能要分五组之类的,因为步数特别少和特别多的都是少数。) +- 发生了OOM,应该怎么去分析解决?jvm调优 +- 什么时候发生线程的上下文切换? +- CAS是硬件实现还是软件实现 +- 除了wait和notifyall,还有什么办法实现类似的功能 +- 微信抢红包设计(只讲了类似多线程抢、Semphore,缓存)、海量文件找重复次数最多的个数(分治) + +## MySQL +- 索引怎么优化 +- 为什么用B+树 +- Innodb 和 Myisam 的区别 +- 聚集索引和非聚集索引 创建以后的文件大小 +- 为什么不用哈希索引? +- 有几种事务隔离级别? +- Innodb 行锁是如何实现的? +- mysql怎么分页 +- 索引为什么查找快 +- 慢查询如何优化 +- mvcc原理 + +## Redis +- redis基本数据类型 +- redis集群?切片 +- 为什么用Redis,Redis宕机了怎么办,数据呢?怎么保持热点数据?过期机制,淘汰机制? +- redis是单线程么?为什么快?io模型什么怎么样的,具体是怎么个流程? +- 缓存穿透、雪崩,redis持久化机制 + +## 计网 +- tcp三次握手 +- HTTP 和RPC区别?还有呢? +- JWT流程,安全?Session? +- 盐有单独保存起来么 +- HTTP 和 HTTPS的区别 +- https的加密证书怎么获取 +- 加密后客户端保留的是公钥还是私钥 +- 讲一下ftp +- cookies +- http以及https 以及加密手段 +- 浏览器输入url后的一系列操作 +- HTTP状态码 +- 为什么用Jwt,cookie,session有什么好处? +- TCP怎么确保可靠性的?丢包和串包怎么办? +- DNS说一下? + +## 操作系统 +- 上下文切换 +- 内核态用户态 +- 多路复用IO模型,select,poll,epoll + +## 分布式 +- 怎么实现分布式锁 +- redis分布式锁有什么缺点,怎么解决 +- 单体应用和微服务区别,为什么要有微服务?好处?还有呢? +- 微服务流程? +- 分布式缓存? +- 分布式session原理 +- SpringCloud及其组件你了解哪些? + + +## 算法 +- n个线程顺序循环打印0-100 +- 手写LinkedList的数据结构并写出add和remove算法 +- 微信红包算法实现 +- 链表如何找环 +- 有序链表合并 +- 共计9个苹果,有2只猴子,一个猴子每次拿2个苹果,一个猴子每次拿3个苹果,如果剩余的苹果不够猴子每次拿的数量,则2只猴子停止拿苹果,请用java多线程模拟上面的描述,要求性能尽可能高效(这个题开始是用可重入锁写的,结束之后自己本地测试发现程序不会自动结束,后来改成用AtomicInteger和cas来实现了) +- 设计一个多线程打印程序,第i个线程只打印i-1数字,比如第1个线程打印数字0,第2个线程只打印数字1,依次类推。任意给定一个数字序列,比如3382019835830,能够使用该程序打印出来。 diff --git a/Java/mind/README.md b/Java/mind/README.md new file mode 100644 index 00000000..49880ea5 --- /dev/null +++ b/Java/mind/README.md @@ -0,0 +1,13 @@ +- [总体架构](https://www.processon.com/view/link/5e170217e4b0bcfb733ce553) **这边就不放图了,放图的字体小,放大可能模糊。该图还在持续总结中...** +- [Java常见基础问题](https://www.processon.com/view/link/5e457c32e4b05d0fd4e94cad) **常见的基础问题,这是必须要掌握。** +- [Java常见集合问题]() **还没总结,后续总结...** +- [Java常见多线程问题](https://www.processon.com/view/link/5e4ab92de4b0996b2ba505bf) **常见的多线程问题,也是必须掌握...** +- [JVM常见问题](https://www.processon.com/view/link/5e4c0704e4b00aefb7e74f44) **常见的JVM要掌握的点...** +- [Spring常见问题](https://www.processon.com/view/link/5e846de9e4b07b16dcdb63f0) **常见的Spring面试的问题...** +- [Mybatis常见问题](https://www.processon.com/view/link/5e4e3b7ae4b0369b916b2e71) **常见的Mybatis面试的问题...** +- [MySQL常见问题](https://www.processon.com/view/link/5e9b0cb15653bb1a686e17ea) **常见的MySQL面试的问题...** +- [Redis常见问题](https://www.processon.com/view/link/5ea2da5907912948b0d89a0a) **常见的Redis面试的问题...** +- [计算机网络常见问题](https://www.processon.com/view/link/5eb8c93be401fd16f42b5f77) **常见的计算机网络面试的问题...** +- [Dubbo常见问题](https://www.processon.com/view/link/5eb8c9715653bb6f2aff7c11) **常见的Dubbo的问题...** +- [RocketMQ常见问题](https://www.processon.com/view/link/5ecf208f7d9c08156c6c37e3) **常见的RocketMQ的问题...** +- [微服务班车在线预约系统](https://github.com/DreamCats/SchoolBus) 个人撸的项目是基于微服务架构的班车预约系统,采用**springboot+mybatis+dubbo+rocketmq+mysql+redis等**。当然,该项目也是前后端分离,前端采用比较流行的vue框架。 \ No newline at end of file diff --git a/Java/mode/README.md b/Java/mode/README.md new file mode 100644 index 00000000..633732eb --- /dev/null +++ b/Java/mode/README.md @@ -0,0 +1,6 @@ +- [单例](mode/单例模式.md) +- [工厂](mode/工厂模式.md) +- [代理](mode/代理模式.md) +- [模板方法](mode/模板方法模式.md) +- [观察者](mode/观察者模式.md) +- [装饰器](mode/装饰器模式.md) \ No newline at end of file diff --git a/DesignPattern/src/ProxyMode.java "b/Java/mode/\344\273\243\347\220\206\346\250\241\345\274\217.md" similarity index 52% rename from DesignPattern/src/ProxyMode.java rename to "Java/mode/\344\273\243\347\220\206\346\250\241\345\274\217.md" index aa969691..afaa720e 100644 --- a/DesignPattern/src/ProxyMode.java +++ "b/Java/mode/\344\273\243\347\220\206\346\250\241\345\274\217.md" @@ -1,39 +1,31 @@ -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; +## 代理模式 -/** - * @program JavaBooks - * @description: 代理模式 - * @author: mf - * @create: 2019/10/02 10:04 - */ +> 所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。 + 一般是因为客户端不想直接访问实际的对象,或者访问实际的对象存在困难,因此通过一个代理对象来完成间接的访问。 +### 静态代理 -/* -所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。 -一般是因为客户端不想直接访问实际的对象,或者访问实际的对象存在困难,因此通过一个代理对象来完成间接的访问。 - */ +> 定义主题 -/* -静态代理 - */ - -// 主题 +```java interface Subject { void visit(); } +``` -// 实现subject的两个类 -class RealSubject implements Subject { +> 实现subject的两个类 - private String name = "feng zi"; +```java +class RealSubject implements Subject { + private String name = "dreamcat"; @Override public void visit() { System.out.println(name); } } - +``` +> 代理类 +```java class ProxySubject implements Subject { private Subject subject; @@ -47,17 +39,18 @@ public void visit() { subject.visit(); } } +``` +缺点: +> 代理类接受一个Subject接口的对象,任何实现该接口的对象,都可以通过代理类进行代理,增加了通用性。 + 每一个代理类都必须实现一遍委托类(也就是realsubject)的接口,如果接口增加方法,则代理类也必须跟着修改。 + 代理类每一个接口对象对应一个委托对象,如果委托对象非常多,则静态代理类就非常臃肿,难以胜任。 + +### 动态代理 +> 动态代理是实现方式,是通过反射来实现的,借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。 -/* -代理类接受一个Subject接口的对象,任何实现该接口的对象,都可以通过代理类进行代理,增加了通用性。 -每一个代理类都必须实现一遍委托类(也就是realsubject)的接口,如果接口增加方法,则代理类也必须跟着修改。 -代理类每一个接口对象对应一个委托对象,如果委托对象非常多,则静态代理类就非常臃肿,难以胜任。 - */ +> 动态代理 -/* -动态代理 -动态代理是实现方式,是通过反射来实现的,借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。 - */ +```java class DynamicProxy implements InvocationHandler { private Object object; @@ -72,7 +65,10 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return result; } } +``` +### 测试 +```java public class ProxyMode { public static void main(String[] args) { // 静态代理 @@ -87,3 +83,4 @@ public static void main(String[] args) { subject.visit(); } } +``` diff --git "a/Java/mode/\345\215\225\344\276\213\346\250\241\345\274\217.md" "b/Java/mode/\345\215\225\344\276\213\346\250\241\345\274\217.md" new file mode 100644 index 00000000..d74b210b --- /dev/null +++ "b/Java/mode/\345\215\225\344\276\213\346\250\241\345\274\217.md" @@ -0,0 +1,87 @@ +## 单例模式 +> 单例就不多说了 + +### 饿汉 + +```java +public class Singleton { + private static Singleton instance = new Singleton(); + private Singleton(){} + + public static Singleton getInstance() { + return instance; + } +} +``` +### 饿汉变种 + +```java +class Singleton { + private static Singleton instance = null; + static { + instance = new Singleton(); + } + private Singleton() {} + + public static Singleton getInstance() { + return instance; + } +} +``` + +### 懒汉(线程不安全) + +```java +class Singleton { + private static Singleton instance = null; + private Singleton(){} + + public static Singleton getInstance() { + if (instance == null){ + instance = new Singleton(); + } + return instance; + + } +} +``` + +### 懒汉(线程安全,但消耗资源较为严重) + +```java +class Singleton { + private static Singleton instance = null; + + private Singleton() { + } + + public static synchronized Singleton getInstance() { + if (instance == null) { + instance = new Singleton(); + } + return instance; + } +} +``` + +### 懒汉(线程安全,双重校验) + +```java +class Singleton { + private static volatile Singleton instance = null; + + private Singleton() { + } + + public static Singleton getInstance() { + if (instance == null) { + synchronized (Singleton.class) { + if (instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } +} +``` \ No newline at end of file diff --git a/DesignPattern/src/FactoryMode.java "b/Java/mode/\345\267\245\345\216\202\346\250\241\345\274\217.md" similarity index 74% rename from DesignPattern/src/FactoryMode.java rename to "Java/mode/\345\267\245\345\216\202\346\250\241\345\274\217.md" index ff57d6a3..3ef26dba 100644 --- a/DesignPattern/src/FactoryMode.java +++ "b/Java/mode/\345\267\245\345\216\202\346\250\241\345\274\217.md" @@ -1,28 +1,22 @@ -/** - * @program JavaBooks - * @description: 工厂模式 - * @author: mf - * @create: 2019/09/30 09:59 - */ +## 工厂模式 +> 大概意思就不要说了,直接举个例子,看例子讲解就知道是什么意思了。 -/* -简单工厂模式 -工厂模版模式 -抽象工厂模式 +### 举例子 -说白了, 就是为解耦。。。 - */ +> 定义一个面条抽象类 - -// 举个例子 +```java abstract class INoodles { /** * 描述每种面条长什么样的... */ public abstract void desc(); } +``` + +> 定义一份兰州拉面(具体产品) -// 先来一份兰州拉面(具体产品类) +```java class LzNoodles extends INoodles { @Override @@ -30,8 +24,11 @@ public void desc() { System.out.println("兰州拉面,成都的好贵 家里的才5-6块钱一碗"); } } +``` + +> 定义一份泡面(程序员挺喜欢的) -// 程序员加班也的来一份泡面哈 (具体产品类) +```java class PaoNoodles extends INoodles { @Override @@ -39,8 +36,11 @@ public void desc() { System.out.println("泡面可还行..."); } } +``` -// 家乡的杂酱面,那才叫好吃撒... +> 不得不说家乡的杂酱面了,好吃得不得了 + +```java class ZaNoodles extends INoodles { @Override @@ -48,8 +48,11 @@ public void desc() { System.out.println("杂酱面,嗯? 真香..."); } } +``` + +> 重头戏,开面条馆了。(工厂) -// 开个面馆吧... 做生意 +```java class SimpleNoodlesFactory { public static final int TYPE_LZ = 1; // 兰州拉面 public static final int TYPE_PAO = 2; // 泡面撒 @@ -64,10 +67,15 @@ public static INoodles createNoodles(int type) { } } } +``` + +> 测试 +```java public class FactoryMode { public static void main(String[] args) { INoodles noodles = SimpleNoodlesFactory.createNoodles(SimpleNoodlesFactory.TYPE_ZA); noodles.desc(); } } +``` \ No newline at end of file diff --git "a/Java/mode/\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" "b/Java/mode/\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" new file mode 100644 index 00000000..cb19467b --- /dev/null +++ "b/Java/mode/\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" @@ -0,0 +1,63 @@ +## 模板方法模式 + +> 定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的 + +例如: +> 去银行办业务,银行给我们提供了一个模板就是:先取号,排对,办理业务(核心部分我们子类完成),给客服人员评分,完毕。 + 这里办理业务是属于子类来完成的,其他的取号,排队,评分则是一个模板。 + +再例如: +> 去餐厅吃饭,餐厅给提供的一套模板就是:先点餐,等待,吃饭(核心部分我们子类完成),买单 + 这里吃饭是属于子类来完成的,其他的点餐,买单则是餐厅提供给我们客户的一个模板。 + +所以: +> 实现一些操作时,整体步骤很固定,但是呢。就是其中一小部分容易变,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。 + + +### 举例子 +> 银行办理业务抽象类 + +```java +abstract class BankTemplateMethod { + // 1. 取号排队 + public void takeNumber() { + System.out.println("取号排队..."); + } + + // 2. 每个子类不同的业务实现,各由子类来实现 + abstract void transact(); + + // 3. 评价 + public void evaluate() { + System.out.println("反馈评价..."); + } + + public void process() { + takeNumber(); + transact(); + evaluate(); + } +} +``` + +> 具体的业务,比如取钱 +```java +class DrawMoney extends BankTemplateMethod { + + @Override + void transact() { + System.out.println("我要取款..."); + } +} +``` + +### 测试 + +```java +public class TemplateMode { + public static void main(String[] args) { + BankTemplateMethod drawMoney = new DrawMoney(); + drawMoney.process(); + } +} +``` \ No newline at end of file diff --git a/DesignPattern/src/DecoratorMode.java "b/Java/mode/\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217.md" similarity index 59% rename from DesignPattern/src/DecoratorMode.java rename to "Java/mode/\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217.md" index 2cb8f5e7..1513ee9d 100644 --- a/DesignPattern/src/DecoratorMode.java +++ "b/Java/mode/\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217.md" @@ -1,26 +1,17 @@ -/** - * @program JavaBooks - * @description: 装饰器模式 - * @author: mf - * @create: 2019/10/05 16:46 - */ - -/* -按照单一职责原则,某一个对象只专注于干一件事,而如果要扩展其职能的话,不如想办法分离出一个类来“包装”这个对象,而这个扩展出的类则专注于实现扩展功能。 -装饰器模式就可以将新功能动态地附加于现有对象而不改变现有对象的功能。 - -代理模式专注于对被代理对象的访问; -装饰器模式专注于对被装饰对象附加额外功能。 - */ - -/* -假如有这样的一个需求 -假设我去买咖啡,首先服务员给我冲了一杯原味咖啡,我希望服务员给我加些牛奶和白糖混合入原味咖啡中。使用装饰器模式就可以解决这个问题。 - */ - -/** - * 咖啡 - */ +## 装饰器模式 + +> 按照单一职责原则,某一个对象只专注于干一件事,而如果要扩展其职能的话,不如想办法分离出一个类来“包装”这个对象,而这个扩展出的类则专注于实现扩展功能。 + 装饰器模式就可以将新功能动态地附加于现有对象而不改变现有对象的功能。 + +> 代理模式专注于对被代理对象的访问; + 装饰器模式专注于对被装饰对象附加额外功能。 + +### 举例子 +> 假设我去买咖啡,首先服务员给我冲了一杯原味咖啡,我希望服务员给我加些牛奶和白糖混合入原味咖啡中。使用装饰器模式就可以解决这个问题。 + +> 咖啡接口 + +```java interface Coffee { // 获取价格 double getCost(); @@ -28,12 +19,11 @@ interface Coffee { // 获取配料 String getIngredients(); } +``` + +> 原味咖啡 -/** - * 原味咖啡 - * cost:1 - * 配料:只有咖啡 - */ +```java class SimpleCoffee implements Coffee { @Override @@ -46,11 +36,9 @@ public String getIngredients() { return "Coffee"; } } - -/** - * 咖啡对象的装饰器类 - * 定义一个Coffe对象的引用,在构造器中进行初始化。并且将getCost()和getIntegredients()方法转发给被装饰对象。 - */ +``` +> 咖啡对象的装饰器类 +```java abstract class CoffeeDecorator implements Coffee { protected final Coffee decoratedCoffee; @@ -73,11 +61,16 @@ public String getIngredients() { return decoratedCoffee.getIngredients(); } } -// 具体的装饰器类,负责往咖啡中“添加”牛奶,注意看getCost()方法和getIngredients()方法,可以在转发请求之前或者之后,增加功能。 -// 如果是代理模式,这里的结构就有所不同,通常代理模式根据运行时的条件来判断是否转发请求。 -/** - * 此装饰类混合"牛奶"到原味咖啡中 - */ +``` + +注意: +> 具体的装饰器类,负责往咖啡中“添加”牛奶,注意看getCost()方法和getIngredients()方法,可以在转发请求之前或者之后,增加功能。 + 如果是代理模式,这里的结构就有所不同,通常代理模式根据运行时的条件来判断是否转发请求。 + + +> 混合牛奶 + +```java class WithMilk extends CoffeeDecorator { public WithMilk(Coffee coffee) { @@ -96,7 +89,9 @@ public String getIngredients() { return super.getIngredients() + ", " + additionalIngredient; } } +``` +```java class WithSugar extends CoffeeDecorator { public WithSugar(Coffee coffee) { @@ -113,6 +108,10 @@ public String getIngredients() { return super.getIngredients() + ", Sugar"; } } +``` + +### 测试 +```java public class DecoratorMode { static void print(Coffee c) { System.out.println("花费了: " + c.getCost()); @@ -134,3 +133,4 @@ public static void main(String[] args) { print(c); } } +``` \ No newline at end of file diff --git a/DesignPattern/src/ObserverMode.java "b/Java/mode/\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" similarity index 66% rename from DesignPattern/src/ObserverMode.java rename to "Java/mode/\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" index b3ef048b..4e209295 100644 --- a/DesignPattern/src/ObserverMode.java +++ "b/Java/mode/\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" @@ -1,47 +1,35 @@ -/** - * @program JavaBooks - * @description: 观察者模式 - * @author: mf - * @create: 2019/10/04 10:46 - */ - -/* -在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新。 -其实就是发布订阅模式,发布者发布信息,订阅者获取信息,订阅了就能收到信息,没订阅就收不到信息。 -四个角色 - - 抽象被观察者角色 - - 抽象观察者角色 - - 具体被观察者角色 - - 具体观察者角色 - -场景:比如微信公众号... - */ - - -import java.util.ArrayList; -import java.util.List; - -/** - * 定义一个抽象被观察者接口 - * 声明了添加、删除、通知观察者方法 - */ +## 观察者模式 +> 在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新。 + 其实就是发布订阅模式,发布者发布信息,订阅者获取信息,订阅了就能收到信息,没订阅就收不到信息。 +> 四个角色 +> - 抽象被观察者角色 +> - 抽象观察者角色 +> - 具体被观察者角色 +> - 具体观察者角色 + +### 举例子(如:微信公众号) + +> 定义一个抽象被观察者接口,声明了添加、删除、通知观察者方法。 + +```java interface Observerable { void registerObserver(Observer o); void removeObserver(Observer o); void notifyObserver(); } +``` + +> 定义一个抽象观察者接口,定义了一个update()方法,当被观察者调用notifyObservers()方法时,观察者的update()方法会被回调。 -/** - * 抽象观察者接口 - * 定义了一个update()方法,当被观察者调用notifyObservers()方法时,观察者的update()方法会被回调。 - */ +```java interface Observer { void update(String message); } +``` -/** - * 定义被观察者,实现三个接口,并且list保存注册的Observer - */ +> 定义被观察者,实现三个接口,并且list保存注册的Observer + +```java class WechatObserver implements Observerable { private List list; @@ -75,10 +63,10 @@ public void setMessage(String message) { notifyObserver(); } } +``` +> 定义具体观察者,User -/** - * 定义具体观察者,User - */ +```java class User implements Observer { private String name; @@ -99,6 +87,11 @@ void read() { System.out.println(name+ " 收到推送消息:" + message); } } +``` + +### 测试 + +```java public class ObserverMode { public static void main(String[] args) { WechatObserver wechatObserver = new WechatObserver(); @@ -117,3 +110,4 @@ public static void main(String[] args) { wechatObserver.setMessage("JAVA是世界上最好的语言..."); } } +``` \ No newline at end of file diff --git "a/Java/spring-books/MyBatis\346\241\206\346\236\266\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/Java/spring-books/MyBatis\346\241\206\346\236\266\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230.md" new file mode 100644 index 00000000..717ee102 --- /dev/null +++ "b/Java/spring-books/MyBatis\346\241\206\346\236\266\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230.md" @@ -0,0 +1,154 @@ +## 引言 +> 项目中,经常用到MyBatis,面试也需要掌握滴! + + +## 基本概念 + +### 数据持久化 + +**数据持久化是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称。例如,文件的存储、数据的读取等都是数据持久化操作。数据模型可以是任何数据结构或对象的模型、XML、二进制流等。** + +**当我们编写应用程序操作数据库,对表数据进行增删改查的操作的时候就是数据持久化的操作。** + +### Mybatis框架简介 + +- **MyBatis框架是一个开源的数据持久层框架。** +- **它的内部封装了通过JDBC访问数据库的操作,支持普通的SQL查询、存储过程和高级映射,几乎消除了所有的JDBC代码和参数的手工设置以及结果集的检索。** +- **MyBatis作为持久层框架,其主要思想是将程序中的大量SQL语句剥离出来,配置在配置文件当中,实现SQL的灵活配置。** +- **这样做的好处是将SQL与程序代码分离,可以在不修改代码的情况下,直接在配置文件当中修改SQL。** + +### ORM + +**ORM(Object/Relational Mapping)即对象关系映射,是一种数据持久化技术。它在对象模型和关系型数据库直接建立起对应关系,并且提供一种机制,通过JavaBean对象去操作数据库表的数据。** + +**MyBatis通过简单的XML或者注解的方式进行配置和原始映射,将实体类和SQL语句之间建立映射关系,是一种半自动(之所以说是半自动,因为我们要自己写SQL)的ORM实现。** + +## MyBatis框架的优缺点及其适用的场合 + +### 优点 + +1. 与JDBC相比,减少了50%以上的代码量。 +2. MyBatis是嘴加单的持久层框架,小巧并且简单易学。 +3. MyBatis相当灵活,不会对应用程序或者数据库的现有设计强加任何影响,SQL写在XML文件里,从程序代码中彻底分离,降低耦合度,便于统一的管理和优化,并可重用。 +4. 提供XML标签,支持编写动态的SQL,满足不同的业务需求。 +5. 提供映射标签,支持对象与数据库的ORM字段关系映射。 + +### 缺点 + +1. SQL语句的编写工作量较大,对开发人员编写SQL的能力有一定的要求。 +2. SQL语句依赖于数据库,导致数据库不具有好的移植性,不可以随便更换数据库。 + +### 适用场合 + +**MyBatis专注于SQL自身,是一个足够灵活的DAO层解决方案。对性能的要求很高,或者需求变化较多的项目,例如Web项目,那么MyBatis是不二的选择。** + +## MyBatis与Hibernate有哪些不同? + +1. Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。 +2. Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。 +3. Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。 + +## **#{}和${}的区别是什么?** + +1. \#{} 是预编译处理,${}是字符串替换。 +2. Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; +3. Mybatis在处理\${}时,就是把\${}替换成变量的值。 + +4. 使用#{}可以有效的防止SQL注入,提高系统安全性。 + +## 当实体类中的属性名和表中的字段名不一样 ,怎么办 ? + +1. 第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。 +2. 第2种: 通过 `` 来映射字段名和实体类属性名的一一对应的关系。 + +## 模糊查询like语句该怎么写? + +1. 第1种:在Java代码中添加sql通配符。 +2. 第2种:在sql语句中拼接通配符,会引起sql注入 + +## 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗? + +**Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。** +**Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中每`