diff --git a/AdavancedPart/AOP.md b/AdavancedPart/AOP.md new file mode 100644 index 00000000..53c518aa --- /dev/null +++ b/AdavancedPart/AOP.md @@ -0,0 +1,51 @@ +AOP +--- + + +AOP(Aspect Oriented Programing),面向切面编程。 +是OOP(Object Oriented Programing)面向对象编程的延续。 + +在OOP思想中,我们会把问题划分为各个模块,如语言、表情等。 +在划分这些模块的过程中,也会出现一些共同特征(如埋点)。它的逻辑被分散到了各个模块,导致了代码复杂度提高,可复用性降低。 + +而AOP,就是将各个模块中的通用逻辑抽离出来。 +我们将这些逻辑视为Aspect(切面),然后动态地把代码插入到类的指定方法、指定位置中。 + +一句话概括: 在运行时,动态的将代码切入到类的指定方法、指定位置上的编程思想就是面相切面的编程。 + + +一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。 + + +### AOP的实现方式 + +#### 静态AOP + +在编译器,切面直接以字节码的形式编译到目标字节码文件中。 + +1. AspectJ +AspectJ属于静态AOP,它是在编译时进行增强,会在编译时期将AOP逻辑织入到代码中。 + +由于是在编译器织入,所以它的优点是不影响运行时性能,缺点是不够灵活。 + +2. AbstractProcessor +自定义一个AbstractProcessor,在编译期去解析编译的类,并且根据需求生成一个实现了特定接口的子类(代理类) + +#### 动态AOP +1. JDK动态代理 +通过实现InvocationHandler接口,可以实现对一个类的动态代理,通过动态代理可以生成代理类,从而在代理类方法中,在执行被代理类方法前后,添加自己的实现内容,从而实现AOP。 + +2. 动态字节码生成 +在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中,没有接口也可以织入,但扩展类的实例方法为final时,则无法进行织入。比如Cglib + +CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。 + +3. 自定义类加载器 +在运行期,目标加载前,将切面逻辑加到目标字节码里。如:Javassist + +Javassist是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。 + +4. ASM +ASM可以在编译期直接修改编译出的字节码文件,也可以像Javassit一样,在运行期,类文件加载前,去修改字节码。 + + diff --git "a/AdavancedPart/ART\344\270\216Dalvik.md" "b/AdavancedPart/ART\344\270\216Dalvik.md" deleted file mode 100644 index 84fa0fea..00000000 --- "a/AdavancedPart/ART\344\270\216Dalvik.md" +++ /dev/null @@ -1,48 +0,0 @@ -ART与Dalvik -=== - - - -Dalvik ---- - -`Dalvik`是`Google`公司自己设计用于`Android`平台的`Java`虚拟机。它可以支持已转换为`.dex`(即`Dalvik Executable`)格式的`Java`应用程序的运行, -`.dex`格式是专为`Dalvik`设计的一种压缩格式,适合内存和处理器速度有限的系统。`Dalvik`经过优化,允许在有限的内存中同时运行多个虚拟机的实例, -并且每一个`Dalvik`应用作为一个独立的`Linux`进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。 -很长时间以来,`Dalvik`虚拟机一直被用户指责为拖慢安卓系统运行速度不如`IOS`的根源。 -`2014`年`6`月`25`日,`Android L`正式亮相于召开的谷歌`I/O`大会,`Android L`改动幅度较大,谷歌将直接删除`Dalvik`,代替它的是传闻已久的`ART`。 - - -ART ---- - -`Android 4.4`提供了一种与`Dalvik`截然不同的运行环境`ART`支持,`ART`源于`google`收购的`Flexycore`的公司。 -`ART`模式与`Dalvik`模式最大的不同在于,启用`ART`模式后,系统在安装应用的时候会进行一次预编译,将字节码转换为机器语言存储在本地, -这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。 - -`ART`使用`AOT(Ahead Of Time)`(静态编译)而`Dalvik`使用`JIT(Just In Time)`(动态编译) -`JIT`方式会在程序执行时将`Dex bytecode`(`java`字节码)转换为处理器可以理解的本地代码,这种方式会将编译时间计入程序的执行时间,程序执行会显得慢一些。`AOT`方式会在程序执行之前(一般是安装时)就编译好本地代码,因此程序执行时少了编译的过程会显得快一些,但占用更多存储空间,安装时也会更慢。但没针对ART优化的程序反而会运行得更慢,随着`Android L`的普及这个问题迟早会解决。`ART`拥有改进的`GC`(垃圾回收)机制:`GC`时更少的暂停时间、`GC`时并行处理、某些时候`Collector`所需时间更短、减少内存不足时触发GC的次数、减少后台内存占用。 -在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。总体的理念就是空间换时间。 - - -`AOT`的编译器分两种模式: -- 在开发机上编译预装应用; -- 在设备上编译新安装的应用,在应用安装时将`dex`字节码翻译成本地机器码。 - -ART优点: -- 系统性能的显著提升。 -- 应用启动更快、运行更快、体验更流畅、触感反馈更及时。 -- 更长的电池续航能力。 -- 支持更低的硬件。 - -ART缺点: -- 更大的存储空间占用,可能会增加10%-20%。 -- 更长的应用安装时间。 - - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git a/AdavancedPart/ARouter.md b/AdavancedPart/ARouter.md new file mode 100644 index 00000000..f9d9cc9f --- /dev/null +++ b/AdavancedPart/ARouter.md @@ -0,0 +1,71 @@ +ARouter +--- + + +[ARouter](https://github.com/alibaba/ARouter) +一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦 + + + + +```java +Intent intent = new Intent(mContext, XxxActivity.class); +intent.putExtra("key","value"); +startActivity(intent); + +Intent intent = new Intent(mContext, XxxActivity.class); +intent.putExtra("key","value"); +startActivityForResult(intent, 666); +``` +在未使用ARouter路由框架之前的原生页面跳转方式。 + + +## 原生路由方案的缺点 + +1. 显式: 直接的类依赖,耦合严重 +2. 隐式: 会在Manifest文件中进行集中管理,写作困难 + + +## ARouter的优势 + +1. 使用注解,实现了映射关系自动注册与分布式路由管理 +2. 编译期间处理注解,并生成映射文件,没有使用反射,不影响运行时性能 +3. 映射关系按组分类,分级管理,按需初始化。 +4. 灵活的降级策略,每次跳转都会回调跳转结果,避免startActivity()一旦失败会抛出异常 +5. 自定义拦截器,自定义拦截顺序,可以对路由进行拦截,比如登录判断和埋点处理 +6. 支持依赖注入,可单独作为依赖注入框架使用,从而实现跨模块API调用 +7. 支持直接解析标准url进行跳转,并自动注入参数到目标页面中 +8. 支持多模块使用,支持组件化开发 + + + +## 基本使用 + +添加依赖并初始化后的基本使用: + +```java +// 在支持路由的页面上添加注解(必选) +// 这里的路径需要注意的是至少需要有两级,/xx/xx +@Route(path = "/test/activity") +public class YourActivity extend Activity { + ... +} + +// 1. 应用内简单的跳转(通过URL跳转在'进阶用法'中) +ARouter.getInstance().build("/test/activity").navigation(); + +// 2. 跳转并携带参数 +ARouter.getInstance().build("/test/1") + .withLong("key1", 666L) + .withString("key3", "888") + .withObject("key4", new Test("Jack", "Rose")) + .navigation(); +``` + + + + + + + + diff --git "a/AdavancedPart/AndroidRuntime_ART\344\270\216Dalvik.md" "b/AdavancedPart/AndroidRuntime_ART\344\270\216Dalvik.md" new file mode 100644 index 00000000..7efca459 --- /dev/null +++ "b/AdavancedPart/AndroidRuntime_ART\344\270\216Dalvik.md" @@ -0,0 +1,161 @@ +AndroidRuntime_ART与Dalvik +=== +在说Android Runtime之前,我们需要了解什么是运行时环境,还需要了解一些基本知识,即JVM和Dalvik VM的功能。 + +## Runtime + +用最简单的术语来说,它是操作系统使用的系统,它负责将您用Java之类的高级语言编写的代码转换为CPU/处理器能理解机器代码。 + +运行时包含在程序运行时执行的软件指令,即使它们实际上并不是该软件代码的一部分也是如此。 +CPU或更笼统的术语我们的计算机仅理解机器语言(二进制代码),因此要使其在CPU上运行,必须将代码转换为机器代码,这由翻译器完成。 +因此,以下是按顺序生成翻译器的过程: +- Assemblers + 它可以直接将汇编代码转换为机器代码,因此速度非常快。 +- Compilers + 它将代码转换为汇编代码,然后使用汇编程序将代码转换为二进制。使用此编译速度很慢,但是执行速度很快。但是编译器的最大问题是所生成的机器代码取决于平台。换句话说,在一台计算机上运行的代码可能不会在另一台计算机上运行。 +- Interpreters + 它在执行代码时翻译代码。由于翻译是在运行时进行的,因此执行速度很慢。 +### JVM +为了维持代码的平台独立性,JAVA开发了JVM,即Java虚拟机。它针对每个平台开发了JVM,这意味着JVM依赖于该平台。Java编译器将.java文件转换为.class文件,这称为字节码。该字节码被提供给JVM,该JVM将其转换为机器码。 + +### Android Runtime + +当我们构建应用程序并生成APK时,该APK的一部分是.dex文件。这些文件包含我们应用程序的源代码,包括我们在为软件解释器设计的低级代码(字节码)中使用的所有库。 + +当用户运行我们的应用程序时,写入的.dex文件中的字节码将由Android Runtime转换为机器码—一组指令,机器可以直接理解并由CPU处理。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dex_local_code.png) + +Android Runtime同样也回管理内存及垃圾回收。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_dvm.png) + + + +可以使用各种策略将字节码编译为机器代码,所有这些策略都有其取舍。为了了解Android Runtime的工作原理,需要首先了解Dalvik。 + + + +## Android Runtime的发展 + +### Dalvik(<= Android K) + +`Dalvik`是`Google`公司自己设计用于`Android`平台的`Java`虚拟机。它可以支持已转换为`.dex`(即`Dalvik Executable`)格式的`Java`应用程序的运行, +`.dex`格式是专为`Dalvik`设计的一种压缩格式,适合内存和处理器速度有限的系统。`Dalvik`经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个`Dalvik`应用作为一个独立的`Linux`进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。 +很长时间以来,`Dalvik`虚拟机一直被用户指责为拖慢安卓系统运行速度不如`IOS`的根源。 +`2014`年`6`月`25`日,`Android L`正式亮相于召开的谷歌`I/O`大会,`Android L`改动幅度较大,谷歌将直接删除`Dalvik`,代替它的是传闻已久的`ART`。 + + + +早期,Android智能手机并不像现在那么强大。大多数手机的RAM很少,有些甚至只有200MB。 +难怪第一个被称为Dalvik的Android Runtime的实现正是为了优化此参数:RAM的使用。 + +因此,它没有在运行它之前将整个应用程序编译为机器代码,而是使用了称为Just In Time编译(简称JIT)的策略。 + +在这种策略下,编译器可以充当解释器。它在应用程序执行期间(在运行时)编译一小段代码。 + +而且由于Dalvik仅编译所需的代码并在运行时执行它,因此可以节省大量RAM。 + +使用Dalvik JIT编译器,每次运行该应用程序时,它会将Dalvik字节码的一部分动态转换为机器代码。随着执行的进行,更多的字节码将被编译和缓存。由于JIT仅编译部分代码,因此它具有较小的内存占用空间,并且在设备上使用的物理空间更少。 + +但是此策略有一个严重的缺点-因为所有这些都在运行时发生,因此显然会对运行时性能产生负面影响。 + +最终,引入了一些优化以使Dalvik更具性能。一些经常使用的已编译代码段已被缓存,不再重新编译。但这是非常有限的,因为在最初的日子里内存非常稀缺。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dalvik_jit.png) + +几年来运行良好,但与此同时,手机的性能越来越高,RAM也越来越多。而且由于应用程序也越来越大,因此JIT性能影响变得越来越成问题。 + +这就是为什么在Android L中引入了新的Android Runtime:ART。 + + + +### ART(Android L) + +`Android 4.4`提供了一种与`Dalvik`截然不同的运行环境`ART`支持,`ART`源于`google`收购的`Flexycore`的公司。 +`ART`模式与`Dalvik`模式最大的不同在于,启用`ART`模式后,系统在安装应用的时候会进行一次预编译,将字节码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。 + +`ART`使用`AOT(Ahead Of Time)`(静态编译)而`Dalvik`使用`JIT(Just In Time)`(动态编译) +`JIT`方式会在程序执行时将`Dex bytecode`(`java`字节码)转换为处理器可以理解的本地代码,这种方式会将编译时间计入程序的执行时间,程序执行会显得慢一些。`AOT`方式会在程序执行之前(一般是安装时)就编译好本地代码,因此程序执行时少了编译的过程会显得快一些,但占用更多存储空间,安装时也会更慢。但没针对ART优化的程序反而会运行得更慢,随着`Android L`的普及这个问题迟早会解决。`ART`拥有改进的`GC`(垃圾回收)机制:`GC`时更少的暂停时间、`GC`时并行处理、某些时候`Collector`所需时间更短、减少内存不足时触发GC的次数、减少后台内存占用。 +在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。总体的理念就是空间换时间。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dvm_art.png) + +`AOT`的编译器分两种模式: + +- 在开发机上编译预装应用; +- 在设备上编译新安装的应用,在应用安装时将`dex`字节码翻译成本地机器码。 + + + +这种方法极大地提高了运行时性能,因为运行本机机器代码甚至比即时编译快20倍。 + +ART优点: + +- 系统性能的显著提升。 +- 应用启动更快、运行更快、体验更流畅、触感反馈更及时。 +- 更长的电池续航能力。 +- 支持更低的硬件。 + +ART缺点: + +- 更大的存储空间占用,可能会增加10%-20%。字节码预先编译成机器码并存储到本地,机器码需要的存储空间更大。 +- 更长的应用安装时间,因为下载APK后,整个应用程序都需要转换为机器代码,而且由于所有应用程序都需要重新优化,因此执行系统更新还需要更长的时间。 +- Android L中的ART使用的内存比Dalvik多得多。 + + + +对于应用程序中经常运行的部分来说,对其进行预编译显然是有回报的,但现实是,用户很少打开应用程序的大多数部分,而对整个应用程序进行预编译几乎也没有回报。 + +这就是为什么在Android N中,Just In Time编译与称为配置文件引导编译(profile-guided complication)一起被引入到Android Runtime的原因。 + +### Profile-guided compilation(Android N) + +配置文件引导编译是一种策略,可以在运行Android应用程序时不断提高其性能。默认情况下,应用程序使用即时编译策略进行编译,但是当ART检测到某些热点功能时,这意味着它们经常运行,ART可以预编译并缓存这些方法以获得最佳性能。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/art_profile_guide.png) + +该策略可为应用程序的关键部分提供最佳性能,同时减少RAM使用量。由于事实证明,对于大多数应用程序来说,通常仅使用10%到20%的代码。 + +更改ART之后,不再影响应用程序安装和系统更新的速度。应用的关键部分的预编译仅在设备空闲和充电时进行,以最大程度地减少对设备电池的影响。 + + + +这种方法的唯一缺点是,为了获取配置文件数据并预编译常用的方法和类,用户必须实际使用应用程序。这意味着该应用程序的一些首次使用可能会有点慢,因为在这种情况下,将仅使用即时编译。 + +这就是为什么要改善在Android P中的初始用户体验的原因,Google在云中引入了个人资料。 + +### Profiles in the cloud(Android P) + +云中的配置文件背后的主要思想是,大多数人以非常相似的方式使用该应用程序。因此,为了在安装后立即提高性能,我们可以从已经使用过此应用程序的人那里收集配置文件数据。此汇总的配置文件数据用于为应用程序创建一个称为通用核心配置文件的文件。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/profiles_in_cloud.png) + +因此,当新用户安装该应用程序时,该文件将与该应用程序一起下载。 ART使用它来预编译大多数用户经常运行的类和方法。这样,新用户在下载应用程序后即可获得更好的性能。 + +这并不意味着不再使用旧策略。用户运行应用程序后,ART将收集用户特定的配置文件数据并重新编译设备闲置时该特定用户经常使用的代码。 + +而最好的部分是,我们开发应用程序的开发人员无需执行任何操作即可启用此功能。这一切都发生在Android Runtime中。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_runtime_change.png) + + + +- 最初Android使用Dalvik,它使用即时编译(JIT)来优化RAM(那时内存是非常紧缺的)的使用。 +- 为了提高Android L中的性能,引入了使用Ahead of time编译的ART。这样可以实现更好的运行时性能,但会导致更长的安装时间和更多的RAM使用率。 +- 因此,在Android N中,JIT被引入到ART中,并且配置文件引导的编译允许为经常运行的部分代码提供更好的性能。 +- 为了让用户在Android P中安装应用后立即获得最佳性能,Google在云端引入了配置文件,它通过添加随APK下载的通用核心配置文件来补充以前的优化,并允许ART预编译部分代码最常由以前的应用程序用户运行。 + + + + + +[Android Runtime-How Dalvik and ART works?](https://www.youtube.com/watch?v=0J1bm585UCc&t=27s) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" index 25d0e1be..5008840f 100644 --- "a/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" +++ "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -29,17 +29,37 @@ Android启动模式详解 - 如果设置了`singleTask`启动模式的`Activity`不是在新的任务中启动时,它会在已有的任务中查看是否已经存在相应的`Activity`实例,如果存在,就会把位于这个`Activity`实例上面的`Activity`全部结束掉, 即最终这个Activity实例会位于任务的堆栈顶端中。以`A`启动`B`来说,当`A`和`B`的`taskAffinity`不同时:第一次创建`B`的实例时,会启动新的`task`,然后将`B`添加到新建的`task`中;否则,将`B`所在`task`中位于`B`之上的全部`Activity`都删除,然后跳转到`B`中。 - `singleInstance` + 顾名思义,是单一实例的意思,即任意时刻只允许存在唯一的`Activity`实例,而且该`Activity`所在的`task`不能容纳除该`Activity`之外的其他`Activity`实例! -它与`singleTask`有相同之处,也有不同之处。 -相同之处:任意时刻,最多只允许存在一个实例。 -不同之处: - - `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。 - - `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。 - - 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时, - 不需要删除其他`Activity`,因为它所在的`task`只有该`Activity`唯一一个`Activity`实例。 +它与`singleTask`有相同之处,也有不同之处。 +相同之处: 任意时刻,最多只允许存在一个实例。 +不同之处: + +- `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。 +- `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。 +- 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时,不需要删除其他`Activity`,因为它所在的`task`只有该`Activity`唯一一个`Activity`实例。 + + +假设我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,应该如何实现呢? + +使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。 +而使用singleInstance模式就可以解决这个问题,在这种模式下,会有一个单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,也就解决了共享Activity实例的问题。 + +假设现在有FirstActivity、SecondActivity、ThirdActivity三个Activity, SecondActivity的启动模式是SingleInstance。 +现在FirstActivity 启动SecondActivity,SecondActivity再启动ThirdActivity。 + +然后我们按下Back键进行返回,你会发现ThirdActivity竟然直接返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序,这是为什么呢?其实原理很简单,由于FirstActivity和ThirdActivity是存放在同一个返回栈里的,当在ThirdActivity的界面按下Back键时,ThirdActivity会从返回栈中出栈,那么FirstActivity就成为了栈顶Activity显示在界面上,因此也就出现了从ThirdActivity直接返回到FirstActivity的情况。然后在FirstActivity界面再次按下Back键,这时当前的返回栈已经空了,于是就显示了另一个返回栈的栈顶Activity,即SecondActivity。最后再次按下Back键,这时所有返回栈都已经空了,也就自然退出了程序。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/activity_launch_mode_singleinstance.png?raw=true) + + + + + --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git a/AdavancedPart/ApplicationId vs PackageName.md b/AdavancedPart/ApplicationId vs PackageName.md index 62829a21..35dc2f0a 100644 --- a/AdavancedPart/ApplicationId vs PackageName.md +++ b/AdavancedPart/ApplicationId vs PackageName.md @@ -7,7 +7,7 @@ ApplicationId vs PackageName 在`Android`官方文档中有一句是这样描述`applicationId`的:`applicationId : the effective packageName`,真是言简意赅,那既然`applicationId`是有效的包明了,`packageName`算啥? -所有`Android`应用都有一个包名。包名在设备上能唯一的标示一个应用,它在`Google Play`应用商店中也是唯一的。这就意味着一旦你使用一个包名发布应用后,你就永 远不能改变它的包名;如果你改了包名就会导致你的应用被认为是一个新的应用,并且已经使用你之前应用的用户将不会看到作为更新的新应用包。 +所有`Android`应用都有一个包名。包名在设备上能唯一的标识一个应用,它在`Google Play`应用商店中也是唯一的。这就意味着一旦你使用一个包名发布应用后,你就永 远不能改变它的包名;如果你改了包名就会导致你的应用被认为是一个新的应用,并且已经使用你之前应用的用户将不会看到作为更新的新应用包。 之前的`Android Gradle`构建系统中,应用的包名是由你的`manifest`文件中的根元素中的`package`属性定义的: @@ -86,4 +86,4 @@ buildTypes { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" "b/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" index 1485641a..0a6a7f1a 100644 --- "a/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" +++ "b/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" @@ -5,7 +5,7 @@ BroadcastReceiver安全问题 - 保证发送的广播要发送给指定的对象 当应用程序发送某个广播时系统会将发送的`Intent`与系统中所有注册的`BroadcastReceiver`的`IntentFilter`进行匹配,若匹配成功则执行相应的`onReceive`函数。可以通过类似`sendBroadcast(Intent, String)`的接口在发送广播时指定接收者必须具备的`permission`或通过`Intent.setPackage`设置广播仅对某个程序有效。 -- 保证我接收到的广播室指定对象发送过来的 +- 保证我接收到的广播是指定对象发送过来的 当应用程序注册了某个广播时,即便设置了`IntentFilter`还是会接收到来自其他应用程序的广播进行匹配判断。对于动态注册的广播可以通过类似`registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)`的接口指定发送者必须具备的`permission`,对于静态注册的广播可以通过`android:exported="false"`属性表示接收者对外部应用程序不可用,即不接受来自外部的广播。 `android.support.v4.content.LocalBroadcastManager`工具类,可以实现在自己的进程内进行局部广播发送与注册,使用它比直接通过sendBroadcast(Intent)发送系统全局广播有以下几个好处: @@ -46,4 +46,4 @@ protected void onDestroy() { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/AdavancedPart/Componse\344\275\277\347\224\250.md" "b/AdavancedPart/Componse\344\275\277\347\224\250.md" new file mode 100644 index 00000000..6a2f0449 --- /dev/null +++ "b/AdavancedPart/Componse\344\275\277\347\224\250.md" @@ -0,0 +1,130 @@ +Compose使用 +=== + +传统 Android UI 开发采用命令式模型,即通过命令驱动视图变化:findViewById 查找控件、设置属性、处理交互逻辑。代码经常伴随着多个职责耦合在一起,结构混乱,易错难测。 + +Compose 则采用声明式模型:界面即状态的函数表达。当状态改变时,对应的 Composable 自动重新组合(Recompose)并刷新界面。这种模式更贴近现代前端(如 React/Vue)的理念。 + +无需关心视图更新逻辑,只要状态变化,界面自然重绘,大幅降低 UI 层复杂度。 + + +1.3 开发效率提升点 + +代码量平均减少 40%-60% + +无需 ViewHolder、Adapter 逻辑 + +状态与 UI 同步更新,避免 UI 状态丢失 + +支持实时预览(@Preview)、热重载、即时调试 + + +我们在公司项目中构建了性能对比 Benchmark(测试设备为 Pixel 6、Android 13): + +2.1 滚动列表对比:RecyclerView vs LazyColumn + +指标 RecyclerView LazyColumn (Compose) 差异 +平均帧率 +48 fps +58 fps ++20% +内存占用 +28 MB +22 MB +-21% +首次绘制耗时 +320 ms +210 ms +-34% + + +2.2 原因解析:Compose 更快的秘密 + +SlotTable:结构快照树 +Compose 编译器会将 Composable 函数转换为组装 SlotTable 的代码。SlotTable 是一种高效的数据结构,存储了 Composable 树的结构快照。当状态发生变化时,Compose 通过对比 SlotTable 的版本,精确地定位变化范围,从而进行最小代价的重组操作(recomposition)。这一过程通过 Composer 对 Slot 表的操作实现,避免了冗余 UI 节点更新。 + +重组与 Group 管理机制 +Compose 使用 Group(startGroup/endGroup)对 Composable 调用进行打包与标识,每个重组区域会通过重新执行对应的 Group 来进行更新,确保仅变更部分被执行。此机制在 RecomposeScopeImpl 中有体现,它能追踪每个状态依赖的作用域,从而提升重组精度。 + +无需 ViewHolder 回收 +传统 RecyclerView 需要手动管理视图缓存与回收,而 Compose 自动处理 Composition 节点生命周期。Compose Compiler 会生成高效的 Slot 操作指令,通过“skip、reuse”策略对 UI 层进行精准控制,避免重复创建与销毁。 + +Skia 图形引擎与 RenderNode +Compose 绘制层基于 Skia 引擎,使用 DrawModifier 直接对 Canvas 进行渲染。它不会像传统 View 那样层层嵌套测量布局与绘制流程,而是采用测量(MeasurePass)-> 布局(LayoutPass)-> 绘制(DrawPass)的管线逻辑,通过 LayoutNode 驱动 Compose UI 树的变化。同时 Compose Layout 使用 SubcomposeLayout 实现异步测量能力,提高复杂嵌套组件的性能表现。 + +渲染流程对比 +阶段 View System Compose +布局树管理 +View/ViewGroup 层级 +LayoutNode 节点 +渲染方式 +Choreographer + RenderThread +FrameClock + Skia 渲染 +状态追踪 +手动触发 invalidate +Snapshot 自动追踪 + Diff Patch +更新路径 +requestLayout → measure/layout +Recomposer + SlotTable 重组 + +注意:Compose 并非所有场景都一定更快,特别是复杂嵌套、过度组合场景仍需谨慎使用。 + +### 简介 + +Jetpack Compose 是用于构建 Android 界面的新款工具包。 + + +Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发,打造生动而精彩的应用。它可让您更快速、更轻松地构建 Android 界面。 + + +编写更少的代码会影响到所有开发阶段:作为代码撰写者,需要测试和调试的代码会更少,出现 bug 的可能性也更小,您就可以专注于解决手头的问题;作为审核人员或维护人员,您需要阅读、理解、审核和维护的代码就更少。 + +与使用 Android View 系统(按钮、列表或动画)相比,Compose 可让您使用更少的代码实现更多的功能。无论您需要构建什么内容,现在需要编写的代码都更少了。以下是我们的一些合作伙伴的感想: + +“对于相同的 Button 类,代码的体量要小 10 倍。”(Twitter) +“使用 RecyclerView 构建的任何屏幕(我们的大部分屏幕都使用它构建)的大小也显著减小。”(Monzo) +““只需要很少几行代码就可以在应用中创建列表或动画,这一点令我们非常满意。对于每项功能,我们编写的代码行更少了,这让我们能够将更多精力放在为客户提供价值上。”(Cuvva) +编写代码只需要采用 Kotlin,而不必拆分成 Kotlin 和 XML 部分:“当所有代码都使用同一种语言编写并且通常位于同一文件中(而不是在 Kotlin 和 XML 语言之间来回切换)时,跟踪变得更容易”(Monzo) + +无论您要构建什么,使用 Compose 编写的代码都很简洁且易于维护。“Compose 的布局系统在概念上更简单,因此可以更轻松地推断。查看复杂组件的代码也更轻松。”(Square) + + + +Compose 使用声明性 API,这意味着您只需描述界面,Compose 会负责完成其余工作。这类 API 十分直观 - 易于探索和使用:“我们的主题层更加直观,也更加清晰。我们能够在单个 Kotlin 文件中完成之前需要在多个 XML 文件中完成的任务,这些 XML 文件负责通过多个分层主题叠加层定义和分配属性。”(Twitter) + + +Compose 与您所有的现有代码兼容:您可以从 View 调用 Compose 代码,也可以从 Compose 调用 View。大多数常用库(如 Navigation、ViewModel 和 Kotlin 协程)都适用于 Compose,因此您可以随时随地开始采用。“我们集成 Compose 的初衷是实现互操作性,我们发现这件事情已经‘水到渠成’。我们不必考虑浅色模式和深色模式等问题,整个体验无比顺畅。”(Cuvva) + + +为现有应用设置 Compose + +首先,使用 Compose Compiler Gradle 插件配置 Compose 编译器。 + +然后,将以下定义添加到应用的 build.gradle 文件中: +``` +android { + buildFeatures { + compose = true + } +} +``` +在 Android BuildFeatures 代码块内将 compose 标志设置为 true 会在 Android Studio 中启用 Compose 功能。 + + +Compose vs HarmonyOS ArkUI 对比分析 + + +Jetpack Compose 和 HarmonyOS ArkUI 均采用声明式 UI 编程范式,面向多设备场景的响应式 UI 构建,二者在理念相通的同时,在架构设计、状态模型、渲染机制等方面有显著区别。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/compose_vs_arkui.png?raw=true) + + +Jetpack Compose 是围绕可组合函数构建的。这些函数可让您以程序化方式定义应用的界面,只需描述应用界面的外观并提供数据依赖项,而不必关注界面的构建过程(初始化元素、将其附加到父项等)。如需创建可组合函数,只需将 @Composable 注解添加到函数名称中即可。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" "b/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" index d765862d..f46d2dc2 100644 --- "a/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" +++ "b/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" @@ -224,15 +224,10 @@ implementation 'com.android.support.constraint:constraint-layout:1.1.0' 它有点类似于`RelativeLayout`,但远比`RelativeLayout`要更强大。 `ConstraintLayout`在测量/布局阶段的性能比 `RelativeLayout`大约高`40%`。 - - - - - - [Build a Responsive UI with ConstraintLayout](https://developer.android.com/training/constraint-layout/index.html) - [ConstraintLayout文档](https://developer.android.com/reference/android/support/constraint/package-summary.html) --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" "b/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" new file mode 100644 index 00000000..54dcdcfa --- /dev/null +++ "b/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" @@ -0,0 +1,127 @@ +# crash分析 + + +## Java Crash流程 + +1、首先发生crash所在进程,在创建之初便准备好了defaultUncaughtHandler,用来处理Uncaught Exception,并输出当前crash的基本信息; + +2、调用当前进程中的AMP.handleApplicationCrash;经过binder ipc机制,传递到system_server进程; + +3、接下来,进入system_server进程,调用binder服务端执行AMS.handleApplicationCrash; + +4、从mProcessNames查找到目标进程的ProcessRecord对象;并将进程crash信息输出到目录/data/system/dropbox; + +5、执行makeAppCrashingLocked: + +创建当前用户下的crash应用的error receiver,并忽略当前应用的广播; +停止当前进程中所有activity中的WMS的冻结屏幕消息,并执行相关一些屏幕相关操作; + +6、再执行handleAppCrashLocked方法: + +当1分钟内同一进程连续crash两次时,且非persistent进程,则直接结束该应用所有activity,并杀死该进程以及同一个进程组下的所有进程。然后再恢复栈顶第一个非finishing状态的activity; +当1分钟内同一进程连续crash两次时,且persistent进程,则只执行恢复栈顶第一个非finishing状态的activity; +当1分钟内同一进程未发生连续crash两次时,则执行结束栈顶正在运行activity的流程。 + +7、通过mUiHandler发送消息SHOW_ERROR_MSG,弹出crash对话框; + +8、到此,system_server进程执行完成。回到crash进程开始执行杀掉当前进程的操作; + +9、当crash进程被杀,通过binder死亡通知,告知system_server进程来执行appDiedLocked(); + +10、最后,执行清理应用相关的四大组件信息。 + + + +- 剩余内存: /proc/meminfo,当系统可用内存小于MemTotal的10%时,非常容易发生OOM和大量GC。 +- PSS和RSS通过/proc/self/smap +- 虚拟内存: 获取大小/proc/self/status,获取具体的分布/proc/self/maps。 + +如果应用堆内存和设备内存比较充足,但还出现内存分配失败,则可能跟资源泄露有关。 +- 获取fd的限制数量:/proc/self/limits。一般单个进程允许打开的最大句柄个数为1024,如果超过800需将所有fd和文件名输出日志进行排查。 +- 获取线程数大小:/proc/self/status一个线程一般占2MB的虚拟内存,线程数超过400个比较危险,需要将所有tid和线程名输出到日志进行排查。 + + + +Native Crash: + +- 崩溃过程:native crash 时操作系统会向进程发送信号,崩溃信息会写入到 data/tombstones 下,并在 logcat 输出崩溃日志 + +- 定位:so 库剥离调试信息的话,只有相对位置没有具体行号,可以使用 NDK 提供的 addr2line 或 ndk-stack 来定位 + +- addr2line:根据有调试信息的 so 和相对位置定位实际的代码处 + +- ndk-stack:可以分析 tombstone 文件,得到实际的代码调用栈 + + + +# ANR分析 + +Application Not Responding,字面意思就是应用无响应,稍加解释就是用户的一些操作无法从应用中获取反馈 + + +Android系统中的应用被Activity Manager及Window Manager两个系统服务监控着,Android系统会在如下情况展示出ANR的对话框: +- Service Timeout:比如前台服务在20s内未执行完成;后台服务超过200没有执行 +- BroadcastQueue Timeout:比如前台广播在10s内未执行完成,后台60s +- ContentProvider Timeout:内容提供者,在publish过超时10s +- InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。 + + + +ANR信息输出到traces.txt文件中 + +traces.txt文件是一个ANR记录文件,用于开发人员调试,目录位于/data/anr中,无需root权限即可通过pull命令获取,下面的命令可以将traces.txt文件拷贝到当前目录下 +adb pull /data/anr . + +ANR排查流程 +1、Log获取 +1、抓取bugreport +adb shell bugreport > bugreport.txt +2、直接导出/data/anr/traces.txt文件 +adb pull /data/anr/traces.txt trace.txt +2、搜索“ANR in”处log关键点解读 + + +发生时间(可能会延时10-20s) + + +pid:当pid=0,说明在ANR之前,进程就被LMK杀死或出现了Crash,所以无法接受到系统的广播或者按键消息,因此会出现ANR + + +cpu负载Load: 7.58 / 6.21 / 4.83 +代表此时一分钟有平均有7.58个进程在等待 +1、5、15分钟内系统的平均负荷 +当系统负荷持续大于1.0,必须将值降下来 +当系统负荷达到5.0,表面系统有很严重的问题 + + +cpu使用率 +CPU usage from 18101ms to 0ms ago +28% 2085/system_server: 18% user + 10% kernel / faults: 8689 minor 24 major +11% 752/android.hardware.sensors@1.0-service: 4% user + 6.9% kernel / faults: 2 minor +9.8% 780/surfaceflinger: 6.2% user + 3.5% kernel / faults: 143 minor 4 major + + +上述表示Top进程的cpu占用情况。 +注意 +如果CPU使用量很少,说明主线程可能阻塞。 +3、在bugreport.txt中根据pid和发生时间搜索到阻塞的log处 +----- pid 10494 at 2019-11-18 15:28:29 ----- +4、往下翻找到“main”线程则可看到对应的阻塞log +"main" prio=5 tid=1 Sleeping +| group="main" sCount=1 dsCount=0 flags=1 obj=0x746bf7f0 self=0xe7c8f000 +| sysTid=10494 nice=-4 cgrp=default sched=0/0 handle=0xeb6784a4 +| state=S schedstat=( 5119636327 325064933 4204 ) utm=460 stm=51 core=4 HZ=100 +| stack=0xff575000-0xff577000 stackSize=8MB +| held mutexes= +上述关键字段的含义如下所示: + +tid:线程号 +sysTid:主进程线程号和进程号相同 +Waiting/Sleeping:各种线程状态 +nice:nice值越小,则优先级越高,-17~16 +schedstat:Running、Runable时间(ns)与Switch次数 +utm:该线程在用户态的执行时间(jiffies) +stm:该线程在内核态的执行时间(jiffies) +sCount:该线程被挂起的次数 +dsCount:该线程被调试器挂起的次数 +self:线程本身的地址 diff --git "a/AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" "b/AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" deleted file mode 100644 index d3ea4562..00000000 --- "a/AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" +++ /dev/null @@ -1,105 +0,0 @@ -Handler导致内存泄露分析 -=== - -有关内存泄露请猛戳[内存泄露][1] - -```java -Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // do something. - } -} -``` -当我们这样创建`Handler`的时候`Android Lint`会提示我们这样一个`warning: In Android, Handler classes should be static or leaks might occur.`。 - -一直以来没有仔细的去分析泄露的原因,先把主要原因列一下: -- `Android`程序第一次创建的时候,默认会创建一个`Looper`对象,`Looper`去处理`Message Queue`中的每个`Message`,主线程的`Looper`存在整个应用程序的生命周期. -- `Hanlder`在主线程创建时会关联到`Looper`的`Message Queue`,`Message`添加到消息队列中的时候`Message(排队的Message)`会持有当前`Handler`引用, -当`Looper`处理到当前消息的时候,会调用`Handler#handleMessage(Message)`.就是说在`Looper`处理这个`Message`之前, -会有一条链`MessageQueue -> Message -> Handler -> Activity`,由于它的引用导致你的`Activity`被持有引用而无法被回收 -- **在java中,no-static的内部类会隐式的持有当前类的一个引用。static的内部类则没有。** - -## 具体分析 -```java -public class SampleActivity extends Activity { - - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // do something - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // 发送一个10分钟后执行的一个消息 - mHandler.postDelayed(new Runnable() { - @Override - public void run() { } - }, 600000); - - // 结束当前的Activity - finish(); -} -``` -在`finish()`的时候,该`Message`还没有被处理,`Message`持有`Handler`,`Handler`持有`Activity`,这样会导致该`Activity`不会被回收,就发生了内存泄露. - -## 解决方法 -- 通过程序逻辑来进行保护。 - - 如果`Handler`中执行的是耗时的操作,在关闭`Activity`的时候停掉你的后台线程。线程停掉了,就相当于切断了`Handler`和外部连接的线, - `Activity`自然会在合适的时候被回收。 - - 如果`Handler`是被`delay`的`Message`持有了引用,那么在`Activity`的`onDestroy()`方法要调用`Handler`的`remove*`方法,把消息对象从消息队列移除就行了。 - - 关于`Handler.remove*`方法 - - `removeCallbacks(Runnable r)` ——清除r匹配上的Message。 - - `removeC4allbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 - - `removeCallbacksAndMessages(Object token)` ——清除token匹配上的Message。 - - `removeMessages(int what)` ——按what来匹配 - - `removeMessages(int what, Object object)` ——按what来匹配 - 我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; -- 将`Handler`声明为静态类。 - 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 - -```java -public class MyActivity extends Activity { - private MyHandler mHandler; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mHandler = new MyHandler(this); - } - - @Override - protected void onDestroy() { - // Remove all Runnable and Message. - mHandler.removeCallbacksAndMessages(null); - super.onDestroy(); - } - - static class MyHandler extends Handler { - // WeakReference to the outer class's instance. - private WeakReference mOuter; - - public MyHandler(MyActivity activity) { - mOuter = new WeakReference(activity); - } - - @Override - public void handleMessage(Message msg) { - MyActivity outer = mOuter.get(); - if (outer != null) { - // Do something with outer as your wish. - } - } - } -} -``` - -[1]:(https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" b/AdavancedPart/Java similarity index 100% rename from "VideoDevelopment/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" rename to AdavancedPart/Java diff --git "a/AdavancedPart/KMP\345\274\200\345\217\221.md" "b/AdavancedPart/KMP\345\274\200\345\217\221.md" new file mode 100644 index 00000000..d4faa6f6 --- /dev/null +++ "b/AdavancedPart/KMP\345\274\200\345\217\221.md" @@ -0,0 +1,30 @@ +KMP开发 +=== + +Kotlin Multiplatform(简称 KMP)是 JetBrains 推出的开源跨平台开发框架。 + +它可以通过共享 Kotlin 编写的业务逻辑代码实现多平台复用。 + + +从应用场景来看,KMP 不仅局限于移动端,它支持 iOS、Android、Web、桌面端(Windows/macOS/Linux)以及服务器端的代码共享。这种扩展性使得开发者能够用同一套代码库构建全平台应用,大幅提升开发效率。 + + + +KMP 有三大编译目标,分别是: Kotlin/JVM、Kotlin/Native、Kotlin/JS。通过编译不同的目标文件实现各端的跨平台能力。除此之外,KMP 还实验性地支持 WebAssembly(Kotlin/Wasm)编译目标,不过目前实际应用场景相对较少。 + + +### KMP编译器 + + +我们知道,一个语言的编译需要经过词法分析和语法分析,解析成抽象语法树 AST。 +而 KMP 为了将 Kotlin 源代码编译成不同的目标平台代码,就需要将 Kotlin 的编译产物进一步向不同的平台转化。 +Kotlin 语言的编译,与向不同的平台转化,明显是不同的职责,需要解耦,所以 KMP 的编译器必然有两个部分,也就是编译器前端(Frontend)与编译器后端(Backend)。 +Frontend 会将 AST 进一步转换为 Kotlin IR(Kotlin Intermediate Representation),是 Kotlin 源代码的中间表示形式,Kotlin IR 是编译器前端的输出,也是编译器后端的输入。 +Backend 则会吧 Kotlin IR 转换为不同平台的中间表示形式,最终生成目标代码。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" "b/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" deleted file mode 100644 index c6f3788d..00000000 --- "a/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" +++ /dev/null @@ -1,591 +0,0 @@ -MaterialDesign使用 -=== - -- `Material Design`是`Google`在`2014`年的`I/O`大会上推出的全新设计语言。 -- `Material Design`是基于`Android 5.0``(API level 21)`的,兼容5.0以下的设备时需要使用版本号`v21.0.0`以上的 -`support v7`包中的`appcpmpat`,不过遗憾的是`support`包只支持`Material Design`的部分特性。 -使用`eclipse`或`Android Studio`进行开发时,直接在`Android SDK Manager`中将`Extras->Android Support Library` -升级至最新版即可。 - -下面我就简单讲解一下如何通过`support v7`包来使用`Material Design`进行开发。 - -Material Design Theme ---- - -`Material`主题: - -- @android:style/Theme.Material (dark version) -- Theme.AppCompat -- @android:style/Theme.Material.Light (light version) -- Theme.AppCompat.Light -- @android:style/Theme.Material.Light.DarkActionBar -- Theme.AppCompat.Light.DarkActionBar - -对应的效果分别如下: - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Material_theme.png?raw=true) - -使用ToolBar ---- - -- 禁止Action Bar - 可以通过使用`Material theme`来让应用使用`Material Design`。想要使用`ToolBar`需要先禁用`ActionBar`。 - 可以通过自定义`theme`继承`Theme.AppCompat.Light.NoActionBar`或者在`theme`中通过以下配置来进行。 - ```xml - false - true - ``` - - 下面我通过第二种方式来看一下具体的实现: - - 在`style.xml`中自定义`AppTheme`: - - ```xml - - - ``` - - 配置的这几种颜色分别如下图所示: - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/material_color.png?raw=true) - 里面没有`colorAccent`的颜色,这个颜色是设置`Checkbox`等控件选中时的颜色。 - - 在`values-v21`中的`style.xml`中同样自定义`AppTheme`主题: - ```xml - - ``` - -- 在`Manifest`文件中设置`AppTheme`主题: - - ```xml - - - - - ``` - 这里说一下为什么要在`values-v21`中也自定义个主题,这是为了能让在`21`以上的版本能更好的使用`Material Design`, -在21以上的版本中会有更多的动画、特效等。 - -- 让Activity继承AppCompatActivity - - ```java - public class MainActivity extends AppCompatActivity { - ... - } - ``` - -- 在布局文件中进行声明 - - 声明`toolbar.xml`,我们把他单独放到一个文件中,方便多布局使用: - ```xml - - ``` - 在`Activity`的布局中使用`ToolBar`: - - ```xml - - - - - - - - - - ``` - -- 在Activity中设置ToolBar - ```java - public class MainActivity extends AppCompatActivity{ - private Context mContext; - private Toolbar mToolbar; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mContext = this; - mToolbar = (Toolbar) findViewById(R.id.toolbar); - mToolbar.setTitle(R.string.app_name); - // 将ToolBar设置为ActionBar,这样一设置后他就能像ActionBar一样直接显示menu目录中的菜单资源 - // 如果不用该方法,那ToolBar就只是一个普通的View,对menu要用inflateMenu去加载布局。 - setSupportActionBar(mToolbar); - getSupportActionBar().setDisplayShowHomeEnabled(true); - } - } - ``` - -到这里运行项目就可以了,就可以看到一个简单的`ToolBar`实现。 - -接下来我们看一下`ToolBar`中具体有哪些内容: - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ToolBar_content.jpg?raw=true) - -我们可以通过对应的方法来修改他们的属性: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/toolbarCode.png?raw=true) - -对于`ToolBar`中的`Menu`部分我们可以通过一下方法来设置: -```java -toolbar.inflateMenu(R.menu.menu_main); -toolbar.setOnMenuItemClickListener(); -``` -或者也可以直接在`Activity`的`onCreateOptionsMenu`及`onOptionsItemSelected`来处理: -```java -@Override -public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; -} - -@Override -public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - if (id == R.id.action_search) { - Toast.makeText(getApplicationContext(), "Search action is selected!", Toast.LENGTH_SHORT).show(); - return true; - } - return super.onOptionsItemSelected(item); -} -``` -`menu`的实现如下: -```xml - - - - - - - -``` - -如果想要对`NavigationIcon`添加点击实现: -```java -toolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onBackPressed(); - } -}); -``` - -运行后发现我们强大的`Activity`切换动画怎么在`5.0`一下系统上实现呢?`support v7`包也帮我们考虑到了。使用`ActivityOptionsCompat` -及`ActivityCompat.startActivity`,但是悲剧了,他对4.0一下基本都无效,而且就算在4.0上很多动画也不行,具体还是用其他 -大神在`github`写的开源项目吧。 - - -- 动态取色Palette - - `Palette`这个类中可以提取一下集中颜色:   - - - Vibrant (有活力) - - Vibrant dark(有活力 暗色) - - Vibrant light(有活力 亮色) - - Muted (柔和) - - Muted dark(柔和 暗色) - - Muted light(柔和 亮色) - - ```java - //目标bitmap,代码片段 - Bitmap bm = BitmapFactory.decodeResource(getResources(), - R.drawable.kale); - Palette palette = Palette.generate(bm); - if (palette.getLightVibrantSwatch() != null) { - //得到不同的样本,设置给imageview进行显示 - iv.setBackgroundColor(palette.getLightVibrantSwatch().getRgb()); - iv1.setBackgroundColor(palette.getDarkVibrantSwatch().getRgb()); - iv2.setBackgroundColor(palette.getLightMutedSwatch().getRgb()); - iv3.setBackgroundColor(palette.getDarkMutedSwatch().getRgb()); - } - ``` - -使用DrawerLayout ---- - -- 布局中的使用 - -```xml - - - - - - - - - - - - - - - - - -``` - -使用DrawerLayout后可以实现类似SlidingMenu的效果。但是怎么将DrawerLayout与ToolBar结合起来呢? 还有再结合Navigation Tabs -以及ViewPager。下面我就直接上代码了。 - -先看布局: activity_main.xml -```xml - - - - - - - - - - - - - - - - - -``` - -MainActivity的代码: -```java -public class MainActivity extends AppCompatActivity { - - private Context mContext; - - private Toolbar mToolbar; - private PagerSlidingTabStrip mScrollingTabs; - private ViewPager mViewPager; - private MainPagerAdapter mPagerAdapter; - private ActionBarDrawerToggle mDrawerToggle; - private DrawerLayout mDrawerLayout; - - private List mTitles; - private List mFragments; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mContext = this; - - findView(); - setToolBar(); - initView(); - initDrawerFragment(); - } - - private void findView() { - mToolbar = (Toolbar) findViewById(R.id.toolbar); - mScrollingTabs = (PagerSlidingTabStrip) findViewById(R.id.psts_main); - mViewPager = (ViewPager) findViewById(R.id.vp_main); - mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); - } - - - private void setToolBar() { - mToolbar.setTitle(R.string.app_name); - setSupportActionBar(mToolbar); - getSupportActionBar().setDisplayShowHomeEnabled(true); - } - - private void initView() { - mFragments = new ArrayList<>(); - for (int xxx = 0; xxx < 5; xxx++) { - mFragments.add(new FriendsFragment()); - } - - mTitles = new ArrayList<>(); - for (int xxx = 0; xxx < 5; xxx++) { - mTitles.add("Tab : " + xxx); - } - - mPagerAdapter = new MainPagerAdapter(getSupportFragmentManager(), mFragments, mTitles); - mViewPager.setAdapter(mPagerAdapter); - mScrollingTabs.setDividerColor(Color.TRANSPARENT); - mScrollingTabs.setIndicatorHeight(10); - mScrollingTabs.setUnderlineHeight(0); - mScrollingTabs.setTextSize(50); - mScrollingTabs.setTextColor(Color.BLACK); - mScrollingTabs.setSelectedTextColor(Color.WHITE); - mScrollingTabs.setViewPager(mViewPager); - - } - - private void initDrawerFragment() { - mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.drawer_open, R.string.drawer_close) { - @Override - public void onDrawerOpened(View drawerView) { - super.onDrawerOpened(drawerView); - MainActivity.this.invalidateOptionsMenu(); - } - - @Override - public void onDrawerClosed(View drawerView) { - super.onDrawerClosed(drawerView); - MainActivity.this.invalidateOptionsMenu(); - } - - @Override - public void onDrawerSlide(View drawerView, float slideOffset) { - super.onDrawerSlide(drawerView, slideOffset); - mToolbar.setAlpha(1 - slideOffset / 2); - } - }; - - mDrawerLayout.setDrawerListener(mDrawerToggle); - mDrawerToggle.syncState(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - if (id == R.id.action_search) { - Toast.makeText(getApplicationContext(), "Search action is selected!", Toast.LENGTH_SHORT).show(); - return true; - } - return super.onOptionsItemSelected(item); - } - -} -``` -最后再看一下`DrawerFragment`的代码: -```java -public class DrawerFragment extends Fragment { - private Context mContext; - private RecyclerView mRecyclerView; - private NavigationDrawerAdapter mAdapter; - private static String[] titles = null; - - public DrawerFragment() { - - } - - public static List getData() { - List data = new ArrayList<>(); - - // preparing navigation drawer items - for (int i = 0; i < titles.length; i++) { - NavDrawerItem navItem = new NavDrawerItem(); - navItem.setTitle(titles[i]); - data.add(navItem); - } - return data; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mContext = getActivity(); - // drawer labels - titles = getActivity().getResources().getStringArray(R.array.nav_drawer_labels); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflating view layout - View layout = inflater.inflate(R.layout.fragment_navigation_drawer, container, false); - mRecyclerView = (RecyclerView) layout.findViewById(R.id.drawerList); - mRecyclerView.setHasFixedSize(true); - mAdapter = new NavigationDrawerAdapter(getActivity(), getData()); - mRecyclerView.setAdapter(mAdapter); - mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - mAdapter.setOnRecyclerViewListener(new NavigationDrawerAdapter.OnRecyclerViewListener() { - @Override - public void onItemClick(int position) { - Toast.makeText(mContext, getData().get(position).getTitle(), Toast.LENGTH_SHORT).show(); - startActivity(new Intent(getActivity(), FriendsActivity.class)); - } - - @Override - public boolean onItemLongClick(int position) { - return false; - } - }); - return layout; - } - -} -``` -上面的`PagerSlidingTabStrip`是开源项目,我改了下,添加了一个选中时的文字颜色改变。 - -[Demo地址](https://github.com/CharonChui/MaterialLibrary) - - -Ripple效果 ---- - -个人非常喜欢的效果。相当于给点击事件加上了动态的赶脚。。。 - - -假设现在有一个`Button`的`selector`,我们想给这个`Button`加上`Ripple`效果,肿么办? -新建一个`xml`文件,用`ripple`包裹`selector`,然后在`Button`的`backgroud`直接引用这个`xml`就好了。 -```xml - - - - - - - - -``` -但是很遗憾,`ripple`是5.0才有的,而且`support`包中没有实现该功能的扩展。 -`5.0`的这些效果还是无法在低版本上实现,包括一些`TextView`等样式,现在可以用大神的开源项目 -[MaterialDesignLibrary](https://github.com/navasmdc/MaterialDesignLibrary) - - -RecyclerView ---- - -`ListView`的升级版,还有什么理由不去用呢? 同样他也在`support v7`包中。 -``` -compile 'com.android.support:recyclerview-v7:21.+' -``` -通过`mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); `设置为`LinearLayoutManager`来实现水平或者竖直 -方向的`ListView`。 - - -阴影 ---- - -通过对`View`设置`backgroud`后再添加`android:elevation="2dp"`来实现背景大小。 - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" index 793904c7..e7dcaaa0 100644 --- "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -5,6 +5,19 @@ OOM问题分析 OOM(OutOfMemoryError),最近线上版本出现了大量线程OOM的crash,尤其是华为Android 9.0系统的手机,占总OOM量的85%左右。 + + +## 内存指标概念 + + + +- USS(Unique Set Size): 物理内存,进程独占的内存 +- PSS(Proportional Set Size): 物理内存,PSS = USS + 按比例包含共享库 +- RSS(Resident Set Size): 物理内存,RSS = USS + 包含共享库 +- VSS(Virtual Set Size): 虚拟内存,VSS = RSS + 未分配实际物理内存 + + + ### OOM分类 #### [XXXClassName] of length XXX would overflow“是系统限制String/Array的长度所致,这种情况比较少。 @@ -761,7 +774,7 @@ nonvoluntary_ctxt_switches: 328 ``` 当线程数(可以在/proc/pid/status 中的threads项实时查看)超过/proc/sys/kernel/threads-max 中规定的上限时产生 OOM 崩溃。 ``` - + ## 定位验证方法: Thread.UncaughtExceptionHandler捕获到OutOfMemoryError时记录/proc/pid目录下的如下信息: diff --git "a/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" "b/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" index a64d0fe2..e268531a 100644 --- "a/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" +++ "b/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" @@ -557,4 +557,4 @@ public void restoreLayoutParams(ViewGroup.LayoutParams params) { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" "b/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" index b5c34641..68dd2232 100644 --- "a/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" +++ "b/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" @@ -1,8 +1,34 @@ 布局优化 === +布局优化的核心问题就是要解决因布局渲染性能不佳而导致应用卡顿的问题。 + + + +## 绘制原理 + +Android的绘制主要是借助CPU和GPU结合刷新机制来共同完成。 + +- CPU负责计算显示内容,包括Measure、Layout等操作,在UI绘制上的缺陷在于容易显示重复的视图组件,这样不仅带来重复的计算操作,而且会占用额外的GPU资源。 +- GPU负责光栅化,将UI元素绘制到屏幕上。 + +例如,文字首先要经过CPU换算成纹理,然后再传递给GPU进行渲染。而图片是先经过CPU计算,然后加载到内存中,最后再传给GPU进行渲染。 + + +## 耗时原因 +分析完布局的加载流程之后,我们发现有如下四点可能会导致布局卡顿: + +1. 首先,系统会将我们的Xml文件通过IO的方式映射的方式加载到我们的内存当中,而IO的过程可能会导致卡顿。 +2. 其次,布局加载的过程是一个反射的过程,而反射的过程也会可能会导致卡顿。 +3. 同时,这个布局的层级如果比较深,那么进行布局遍历的过程就会比较耗时。 +4. 最后,不合理的嵌套RelativeLayout布局也会导致重绘的次数过多。 + + +## 优化方式 + - 去除不必要的嵌套和节点 这是最基本的一条,但也是最不好做到的一条,往往不注意的时候难免会一些嵌套等。 + - 首次不需要的节点设置为`GONE`或使用`ViewStud`. - 使用`Relativelayout`代替`LinearLayout`. 平时写布局的时候要多注意,写完后可以通过`Hierarchy Viewer`或在手机上通过开发者选项中的显示布局边界来查看是否有不必要的嵌套。 @@ -146,7 +172,26 @@ - 减少不必要的`Inflate` 如上一步中`stub.infalte()`后将该`View`进行记录或者是`ListView`中`item inflate`的时候。 - + +- 使用ConstraintLayout降低布局嵌套层级 + + - 实现几乎完全扁平化的布局 + - 构建复杂布局性能更高 + - 具有RelativeLayout和LinearLayout的特性 + +- 使用AsyncLayoutInflater异步加载对应的布局 + + - 工作线程加载布局 + - 回调主线程 + - 节省主线程时间 + + AsyncLayoutInflater是通过侧面缓解的方式去缓解布局加载过程中的卡顿,但是它依然存在一些问题: + + - 1、不能设置LayoutInflater.Factory,需要通过自定义AsyncLayoutInflater的方式解决,由于它是一个final,所以需要将代码直接拷处进行修改。 + - 2、因为是异步加载,所以需要注意在布局加载过程中不能有依赖于主线程的操作。 + + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/Algorithm/1.\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/Algorithm/1.\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..2dc0dda4 --- /dev/null +++ "b/Algorithm/1.\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,85 @@ +1.合并两个有序数组 +=== + + +### 题目 + +给你两个按`非递减顺序`排列的整数数组`nums1`和`nums2`,另有两个整数`m`和`n`,分别表示`nums1`和`nums2`中的元素数目。 + +请你`合并`nums2到nums1中,使合并后的数组同样按非递减顺序排列。 + +注意:最终,合并后数组不应由函数返回,而是存储在数组nums1中。为了应对这种情况,nums1的初始长度为`m + n`,其中前m个元素表示应合并的元素,后n个元素为0,应忽略。nums2的长度为n。 + + + +示例 1: + +输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 +输出:[1,2,2,3,5,6] +解释:需要合并 [1,2,3] 和 [2,5,6] 。 +合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。 + +示例 2: + +输入:nums1 = [1], m = 1, nums2 = [], n = 0 +输出:[1] +解释:需要合并 [1] 和 [] 。 +合并结果是 [1] 。 + +示例 3: + +输入:nums1 = [0], m = 0, nums2 = [1], n = 1 +输出:[1] +解释:需要合并的数组是 [] 和 [1] 。 +合并结果是 [1] 。 +注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。 + + + +### 思路 + +逆向双指针: +- nums1中是非递减的数组。 +- nums2中也是非递减数组。 +- 所以我们要做的就是把nums1中前m个元素与nums2中的元素进行倒序比较,将大的值倒序放到nums1中的后面。 +- 因为nums1中的数据已经是非递减的了,所以等nums2中的内容都放置完就可以结束。 + +```python +class Solution: + def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None: + index = m + n - 1 + p1 = m - 1 + p2 = n - 1 + while p2 >= 0: + if p1 >= 0 and nums1[p1] >= nums2[p2]: + nums1[index] = nums1[p1] + index += 1 + p1 -= 1 + else: + nums1[index] = nums2[p2] + index += 1 + p2 -= 1 +``` + +```kotlin +class Solution { + fun merge(nums1: IntArray, m: Int, nums2: IntArray, n: Int): Unit { + var index = nums1.lastIndex + var r1 = m - 1 + var r2 = n - 1 + while(r2 >= 0) { + if (r1 >= 0 && nums1[r1] >= nums2[r2]) { + nums1[index --] = nums1[r1--] + } else { + nums1[index --] = nums2[r2 --] + } + } + } +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" "b/Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" new file mode 100644 index 00000000..0e95d5fa --- /dev/null +++ "b/Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" @@ -0,0 +1,65 @@ +10.跳跃游戏II +=== + + +### 题目 + +给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 + +每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处: + +- 0 <= j <= nums[i] +- i + j < n +返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。 + + + +示例 1: + +- 输入: nums = [2,3,1,1,4] +- 输出: 2 +- 解释: 跳到最后一个位置的最小跳跃数是 2。 + - 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 + +示例 2: + +- 输入: nums = [2,3,0,1,4] +- 输出: 2 + + +### 思路 + + +贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。 + +所以真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数! + +这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。 + +- 如下图,开始的位置是 2,可跳的范围是橙色的。然后因为 3 可以跳的更远,所以跳到 3 的位置。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jump_2_1.png?raw=true) +- 然后现在的位置就是 3 了,能跳的范围是橙色的,然后因为 4 可以跳的更远,所以下次跳到 4 的位置。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jump_2_2.png?raw=true) + + +```python + +class Solution: + def jump(self, nums: List[int]) -> int: + end = 0 + maxPosition = 0 + steps = 0 + for i in range(len(nums) - 1): + maxPosition = max(maxPosition, i + nums[i]) + if i == end: + steps += 1 + end = maxPosition + + return steps +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/11.H\346\214\207\346\225\260.md" "b/Algorithm/11.H\346\214\207\346\225\260.md" new file mode 100644 index 00000000..20afa38a --- /dev/null +++ "b/Algorithm/11.H\346\214\207\346\225\260.md" @@ -0,0 +1,232 @@ +11.H指数 +=== + + +### 题目 + +给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返回该研究者的 h 指数。 + +根据维基百科上 h 指数的定义:h 代表“高引用次数” ,一名科研人员的 h 指数 是指他(她)至少发表了 h 篇论文,并且 至少 有 h 篇论文被引用次数大于等于 h 。如果 h 有多种可能的值,h 指数 是其中最大的那个。 + + + +示例 1: + +- 输入:citations = [3,0,6,1,5] +- 输出:3 +- 解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。 + - 由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。 + +示例 2: + +- 输入:citations = [1,3,1] +- 输出:1 + +### 思路 + +翻译: 数组中有h个不小于h的值,求最大的h + + +至少有h篇论文,每一篇至少被引用次数为h,听起来很绕,但是以用例[0, 1, 3, 5, 6]来分析: + +- h指数无非就是5或4或3或2或1或0 + +- 那么如果数组中最小的引用次数都大于等于5,那么其他的不用看了,h就是取最大值5 + +- 如果最小值不满足,那么如果数组中倒数第二小值如果大于等于4,那么一定有4个是大于等于4的 + +- 这就是h指数 + + + + + +##### 方法一:排序 + +h肯定不会超过数组的长度。 + +[0, 1, 3, 5, 6] + +- 从小到大排序。 +- 排序后从头开始遍历,如果最小的值,都大于数组的size,那就是size +- 如果上面不满足,那第二小的值如果大于数组的size - 1,那就是size - 1 +- 所以遍历条件就是 for (int i = 0; i < size; i ++ ) { if (nums[i] >= size - 1)} +- 遍历排序后的数组,如果数组中该位置的值>h,那就h++然后继续遍历下一个 + +```java +public int hIndex(int[] citations) { + Arrays.sort(citations); + for(int i = 0; i < citations.length; i++){ + if( citations[i] >= (citations.length -i)){ + return citations.length -i; + } + } + return 0; +} +``` + + +最终的时间复杂度与排序算法的时间复杂度有关 + +复杂度分析: + +- 时间复杂度:O(nlogn),其中 n 为数组 citations 的长度。即为排序的时间复杂度。 + +- 空间复杂度:O(logn),其中 n 为数组 citations 的长度。即为排序的空间复杂度。 + + +##### 方法二:计数排序 + +根据上述解法我们发现,最终的时间复杂度与排序算法的时间复杂度有关。 + +所以我们可以使用计数排序算法,新建并维护一个数组 counter 用来记录当前引用次数的论文有几篇。它的值可以是0 ~ n,所以数组的长度是n + 1 + +[0, 1, 3, 5, 6] + + +- 我们遍历数组 citations,将引用次数大于 n 的论文都当作引用次数为 n 的论文,然后将每篇论文的引用次数作为下标,将 cnt 中对应的元素值加 1。这样我们就统计出了每个引用次数对应的论文篇数。 + +``` +counter[0] = 1 +counter[1] = 1 +counter[2] = 0 +counter[3] = 1 +// 无值,默认0 +counter[4] = 0 +// 引用次数为5的论文有2篇 +counter[5] = 2 +``` + +- 最后我们可以从后向前遍历数组 counter,因为要找最大的H值,所以这个时候要倒序从n到0遍历遍历,找出从后往前遍历时第一个满足引用次数>i的值,就是最大的h值。 + +- 注意要用累加,因为引用次数为5的2篇,一定也能满足引用次数>=3的条件 + +```java +public class Solution { + public int hIndex(int[] citations) { + int n = citations.length, tot = 0; + int[] counter = new int[n + 1]; + for (int i = 0; i < n; i++) { + if (citations[i] >= n) { + counter[n]++; + } else { + counter[citations[i]]++; + } + } + for (int i = n; i >= 0; i--) { + tot += counter[i]; + if (tot >= i) { + return i; + } + } + return 0; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 为数组 citations 的长度。需要遍历数组 citations 一次,以及遍历长度为 n+1 的数组 counter 一次。 + +- 空间复杂度:O(n),其中 n 为数组 citations 的长度。需要创建长度为 n+1 的数组 counter。 + + +##### 方法三:二分法 + +所谓的 h 指数是指一个具体的数值,该数值为“最大”的满足「至少发表了 x 篇论文,且每篇论文至少被引用 x 次」定义的合法数,重点是“最大”。 + +给定所有论文的引用次数情况为[3,0,6,1,5],可统计满足定义的数值有哪些: + +``` +h=0,含义为「至少发表了 0 篇,且这 0 篇论文至少被引用 0 次」,空集即满足,恒成立; + +h=1,含义为「至少发表了 1 篇,且这 1 篇论文至少被引用 1 次」,可以找到这样的组合,如 [3],成立; + +h=2,含义为「至少发表了 2 篇,且这 2 篇论文至少被引用 2 次」,可以找到这样的组合,如 [3, 6],成立; + +h=3,含义为「至少发表了 3 篇,且这 3 篇论文至少被引用 3 次」,可以找到这样的组合,如 [3, 6, 5],成立; + +h=4,含义为「至少发表了 4 篇,且这 4 篇论文至少被引用 4 次」,找不到这样的组合,不成立; + +h=5,含义为「至少发表了 5 篇,且这 5 篇论文至少被引用 5 次」,找不到这样的组合,不成立; +``` +实际上,当遇到第一个无法满足的数时,更大的数值就没必要找了。 + + +基于此分析,我们发现对于任意的 citations数组(论文总数量为该数组长度 n),都必然对应了一个最大的 h 值,且小于等于该 h 值的情况均满足,大于该 h 值的均不满足。 + +那么,在以最大 h 值为分割点的数轴上具有「二段性」,可通过「二分」求解该分割点(答案)。 + +最后考虑在什么值域范围内进行二分? + +一个合格的二分范围,仅需确保答案在此范围内即可。 + +再回看我们关于 h 的定义「至少发表了 x 篇论文,且每篇论文至少被引用 x 次」 +综上,我们只需要在 [0,n] 范围进行二分即可。 + + +设查找范围的初始左边界 left 为 0,初始右边界 right 为 n。每次在查找范围内取中点 mid,同时扫描整个数组,判断是否至少有 mid 个数大于 mid。如果有,说明要寻找的 h 在搜索区间的右边,反之则在左边。 + +二分的本质:前半部分符合要求、后半部分不符合要求,找出符合要求的最大索引 + + +```java +class Solution { + public int hIndex(int[] cs) { + int n = cs.length; + int l = 0, r = n; + while (l < r) { + // 加1 是为了防止死循环 + int mid = (l + r + 1) >> 1; + if (check(cs, mid)) l = mid; + else r = mid - 1; + } + return r; + } + // 判断是否存在至少mid篇论文的引用次数至少为mid。 + boolean check(int[] cs, int mid) { + int ans = 0; + for (int i : cs) if (i >= mid) ans++; + return ans >= mid; + } +} +``` + + + +###### 上面为什么要加1 + +- 这是因为整数除法的向下取整特性。 +- 在计算mid时,如果使用(l+r)/2,当l和r相邻时(例如l=2, r=3),则mid=(2+3)/2=2(整数除法向下取整)。 +- 如果此时check(mid)为真,那么我们会执行l=mid,即l=2,然后循环继续,再次计算mid=(2+3)/2=2,这样就进入了死循环。 + +为了避免这种死循环,我们使用(l + r + 1) / 2,使得当l和r相邻时,mid会等于r(因为(2+3+1)/2=6/2=3),然后无论走哪个分支,循环都会结束(因为执行l=3后l等于r,或执行r=mid-1=2后l等于r)。 +所以,加1是为了避免在只剩两个数时陷入死循环,因为我们要找的是右边界(最大值)。 + + + +###### 例子: + +- citations = [3,0,6,1,5],用上述二分法: +- 初始化:l=0, r=5 +- mid = (0+5+1)/2 = 3,检查check(3): 统计>=3的个数,为3(3,5,6)-> 满足,所以l=3 +- 接下来:l=3, r=5,mid=(3+5+1)/2=9/2=4(整数除法取整为4),检查check(4):统计>=4的个数,有2篇(5,6)不满足4(因为需要至少4篇) +- 所以r=3循环结束,返回3。 + + + + +复杂度分析: + +- 时间复杂度:O(nlogn),其中 n 为数组 citations 的长度。需要进行 logn 次二分搜索,每次二分搜索需要遍历数组 citations 一次。 +- 空间复杂度:O(1),只需要常数个变量来进行二分搜索。 + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" "b/Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" new file mode 100644 index 00000000..68b25643 --- /dev/null +++ "b/Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" @@ -0,0 +1,105 @@ +12.O(1) 时间插入、删除和获取随机元素 +=== + + +### 题目 + +实现RandomizedSet类: + +- RandomizedSet() 初始化 RandomizedSet 对象 +- bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。 +- bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。 +- int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。 + +你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。 + + + +示例: + +- 输入["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"] +- [[], [1], [2], [2], [], [1], [2], []] +- 输出[null, true, false, true, 2, true, false, 2] + +解释: +``` +RandomizedSet randomizedSet = new RandomizedSet(); +randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。 +randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。 +randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。 +randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。 +randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。 +randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。 +randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。 +``` + +提示: + +- -231 <= val <= 231 - 1 +- 最多调用 insert、remove 和 getRandom 函数 2 * 105 次 +- 在调用 getRandom 方法时,数据结构中 至少存在一个 元素。 + +### 思路 + + +题目要求插入、删除、随机的时间复杂度为 O(1) 。 + +- 变长数组可以在 O(1) 的时间内完成获取随机元素操作,但是由于无法在 O(1) 的时间内判断元素是否存在,因此不能在 O(1) 的时间内完成插入和删除操作。 +- 哈希表可以在 O(1) 的时间内完成插入和删除操作,但是由于无法根据下标定位到特定元素,因此不能在 O(1) 的时间内完成获取随机元素操作。 +- 为了满足插入、删除和获取随机元素操作的时间复杂度都是 O(1),需要将变长数组和哈希表结合,变长数组中存储元素,哈希表中存储每个元素在变长数组中的下标。 + +```java +class RandomizedSet { + List nums; + Map indices; + Random random; + + public RandomizedSet() { + nums = new ArrayList(); + indices = new HashMap(); + random = new Random(); + } + + public boolean insert(int val) { + if (indices.containsKey(val)) { + return false; + } + int index = nums.size(); + nums.add(val); + indices.put(val, index); + return true; + } + + public boolean remove(int val) { + if (!indices.containsKey(val)) { + return false; + } + int index = indices.get(val); + int last = nums.get(nums.size() - 1); + nums.set(index, last); + indices.put(last, index); + nums.remove(nums.size() - 1); + indices.remove(val); + return true; + } + + public int getRandom() { + int randomIndex = random.nextInt(nums.size()); + return nums.get(randomIndex); + } +} +``` + + +复杂度分析: + +- 时间复杂度:初始化和各项操作的时间复杂度都是 O(1)。 + +- 空间复杂度:O(n),其中 n 是集合中的元素个数。存储元素的数组和哈希表需要 O(n) 的空间。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" "b/Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" new file mode 100644 index 00000000..abff28d0 --- /dev/null +++ "b/Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" @@ -0,0 +1,128 @@ +13.除自身以外数组的乘积 +=== + + +### 题目 + +给你一个整数数组nums,返回数组answer,其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。 + +题目数据保证数组nums之中任意元素的全部前缀元素和后缀的乘积都在32位整数范围内。 + +请***不要使用除法***,且在O(n)时间复杂度内完成此题。 + + + +示例 1: + +- 输入: nums = [1,2,3,4] +- 输出: [24,12,8,6] + +示例 2: + +- 输入: nums = [-1,1,0,-3,3] +- 输出: [0,0,9,0,0] + + +提示: + +- 2 <= nums.length <= 105 +- -30 <= nums[i] <= 30 +- 输入 保证 数组 answer[i] 在 32 位 整数范围内 + +### 思路 + + +先计算数组中所有元素的乘积,然后将总的乘积除以数组的中每个元素x就是除自身值以外数组的乘积。 + +但是这样有个问题,如果数组中有一个元素是0,那这个方法就失效了,而且题目中说了不能使用除法运算。 + +我们可以分解为两部分: + +- 得到索引左侧所有数字的乘积L +- 得到索引右侧所有数字的乘积R +- 两部分相乘 + + +```java + +class Solution { + public int[] productExceptSelf(int[] nums) { + int length = nums.length; + + // L 和 R 分别表示左右两侧的乘积列表 + int[] L = new int[length]; + int[] R = new int[length]; + + int[] answer = new int[length]; + + // L[i] 为索引 i 左侧所有元素的乘积 + // 对于索引为 '0' 的元素,因为左侧没有元素,所以 L[0] = 1 + L[0] = 1; + for (int i = 1; i < length; i++) { + L[i] = nums[i - 1] * L[i - 1]; + } + + // R[i] 为索引 i 右侧所有元素的乘积 + // 对于索引为 'length-1' 的元素,因为右侧没有元素,所以 R[length-1] = 1 + R[length - 1] = 1; + for (int i = length - 2; i >= 0; i--) { + R[i] = nums[i + 1] * R[i + 1]; + } + + // 对于索引 i,除 nums[i] 之外其余各元素的乘积就是左侧所有元素的乘积乘以右侧所有元素的乘积 + for (int i = 0; i < length; i++) { + answer[i] = L[i] * R[i]; + } + + return answer; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(N),其中 N 指的是数组 nums 的大小。预处理 L 和 R 数组以及最后的遍历计算都是 O(N) 的时间复杂度。 +- 空间复杂度:O(N),其中 N 指的是数组 nums 的大小。使用了 L 和 R 数组去构造答案,L 和 R 数组的长度为数组 nums 的大小。 + + +尽管上面的方法已经能够很好的解决这个问题,但是空间复杂度并不为常数。 + +- 由于输出数组不算在空间复杂度内,那么我们可以将 L 或 R 数组用输出数组来计算。也就是不再新申请L和R数组,而只是用一个变量记录索引右侧元素的乘积之和。 +- 第一遍遍历的时候将answer数组的值都填充为索引左侧元素的值(也就是上面方法中L的值) +- 再一次后续遍历的时候取answer中的值和数组索引右侧元素乘积的和值相乘并赋值到answer中。这个时候answer中的值就已经是乘积的和值了。 + + +```java +class Solution { + public int[] productExceptSelf(int[] nums) { + int n = nums.length; + int[] ans = new int[n]; + //初始化ans都为1 + for (int i = 0; i < n; i++) { + ans[i] = 1; + } + //左侧 + int L = 1; + for(int i = 0; i < n; i++){ + ans[i] *= L; + L *= nums[i]; + } + //右侧 + int R = 1; + for(int i = n - 1; i >= 0; i--){ + ans[i] *= R; + R *= nums[i]; + } + return ans; + } +} +``` + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/14.\345\212\240\346\262\271\347\253\231.md" "b/Algorithm/14.\345\212\240\346\262\271\347\253\231.md" new file mode 100644 index 00000000..8debb624 --- /dev/null +++ "b/Algorithm/14.\345\212\240\346\262\271\347\253\231.md" @@ -0,0 +1,143 @@ +14.加油站 +=== + + +### 题目 + +在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 + +你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。 + +给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。 + + + +示例 1: + +- 输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2] +- 输出: 3 +- 解释: + - 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 + - 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 + - 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 + - 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 + - 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 + - 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 + +因此,3 可为起始索引。 + +示例 2: + +- 输入: gas = [2,3,4], cost = [3,4,3] +- 输出: -1 +- 解释: + - 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 + - 我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 + - 开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 + - 开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 + - 你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 + +因此,无论怎样,你都不可能绕环路行驶一周。 + + +提示: + +- n == gas.length == cost.length +- 1 <= n <= 105 +- 0 <= gas[i], cost[i] <= 104 +- 输入保证答案唯一。 + +### 思路 + +- 题目有一点很重要: 如果存在解,则 保证 它是 唯一 的。 +- 能跑一圈的前提有两个: + - 每一站的油量都能达到下一站 + - 一圈下来剩下的油量是大于等于0的 + +- 我们首先检查第 0 个加油站,并试图判断能否环绕一周;如果不能,就从第一个无法到达的加油站开始继续检查。 + + +```python +class Solution: + def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int: + n = len(gas) + start = 0 + while start < n: + cur = 0 + step = 0 + while step < n: + nex = (start + step) % n + cur += gas[nex] - cost[nex] + # 走到这一步的时候当前的油量< 0了。需要更新起始的站,重新从下一站开始来计算了 + if cur < 0: + break + # 走下一步 + step += 1 + # 走了n步,也就是一圈了 + if step == n: + return start + else: + # 没走了,一圈,从当前已经走的步数继续遍历找 + start = start + step + 1 + return -1 +``` + + +##### 注意: 为什么使用 start += step + 1 而不是 start += 1? + +​如果从起点 start 出发,在走了 step 步后失败(无法到达第 step+1 个加油站),那么从 start 到 start+step 之间的任意一个加油站作为新起点都一定会失败。 +对于任意k在[start + 1, start + step]: + +- 如果从 k 出发,我们失去了 start → k 这段路的油量积累(由问题性质决定)。 +- 由于 start → k 这段的油量积累 ​必然是非负的​(因为如果这段是负的,那在 k 之前就已经失败了)。 +- 因此,当从 k 开始时,油量比从 start 开始更少,不可能完成接下来的路程。 + +使用 start += step + 1 是基于数学性质的优化,避免了无效的重复检查。 +改为 start += 1 虽然逻辑正确,但会退化为 O(n²) 的暴力解法,在大型数据集上效率极低。 + + + +复杂度分析: + +- 时间复杂度:O(N),其中 N 为数组的长度。我们对数组进行了单次遍历。 + +- 空间复杂度:O(1)。 + + +##### 方法二 + + + +允许油量为负,但是总剩余油量应该大于等于0,否则不存在解的。 + +存在解的情况下,利用贪心法的思想,找到最低点,它的下一个点出发的话,可以保证前期得到剩余油量最大,所以可以跑完全程。 + +```java +public int canCompleteCircuit2(int[] gas, int[] cost) { + // 一圈下来的总剩余油量 + int totalNum = 0; + // 从某一站开始时每一站剩余的油量 + int curNum = 0; + int idx = 0; + + for (int i = 0; i < gas.length; i++) { + curNum += gas[i] - cost[i]; + totalNum += gas[i] - cost[i]; + if (curNum < 0) { + idx = (i+1) % gas.length; + curNum = 0; + } + + } + + if(totalNum < 0) return -1; + return idx; +} +``` + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" "b/Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" new file mode 100644 index 00000000..b74a91ed --- /dev/null +++ "b/Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" @@ -0,0 +1,174 @@ +15.分发糖果 +=== + + +### 题目 + +n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 + +你需要按照以下要求,给这些孩子分发糖果: + +- 每个孩子至少分配到 1 个糖果。 +- 相邻两个孩子评分更高的孩子会获得更多的糖果。 +- 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。 + + + +示例 1: + +- 输入:ratings = [1,0,2] +- 输出:5 +- 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。 + +示例 2: + +- 输入:ratings = [1,2,2] +- 输出:4 +- 解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。 + 第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。 + + +提示: + +- n == ratings.length +- 1 <= n <= 2 * 104 +- 0 <= ratings[i] <= 2 * 104 + +### 思路 + +注意题目要求的是最少的糖果数目,且每个孩子至少分配1个糖果。 + +我们默认把所有小孩的糖果都是1。 + +那么本题我采用了两次贪心的策略: + + +- 第一次从左到右遍历,如果当前孩子评分高于左边孩子,那么当前孩子比左边多一个糖果;否则不处理,只保留一个糖果。 + - 左规则:当`ratings[i−1]ratings[i+1]`时,i号学生的糖果数量将比i+1号孩子的糖果数量多。 + + +我们遍历该数组两次,处理出每一个学生分别满足左规则或右规则时,最少需要被分得的糖果数量。每个人最终分得的糖果数量即为这两个数量的最大值。 + +在实际代码中,我们先计算出左规则left数组,在计算右规则的时候只需要用单个变量记录当前位置的右规则,同时计算答案即可。 + +```java +class Solution { + public int candy(int[] ratings) { + int n = ratings.length; + int[] left = new int[n]; + for (int i = 0; i < n; i++) { + if (i > 0 && ratings[i] > ratings[i - 1]) { + left[i] = left[i - 1] + 1; + } else { + left[i] = 1; + } + } + int right = 0, ret = 0; + for (int i = n - 1; i >= 0; i--) { + if (i < n - 1 && ratings[i] > ratings[i + 1]) { + right++; + } else { + right = 1; + } + ret += Math.max(left[i], right); + } + return ret; + } +} +``` + +```python +class Solution: + def candy(self, ratings: List[int]) -> int: + n = len(ratings) + candies = [1] * n # 每个孩子至少一个糖果 + + # 从左到右:右边评分高,则右边糖果 = 左边糖果 + 1 + for i in range(1, n): + if ratings[i] > ratings[i - 1]: + candies[i] = candies[i - 1] + 1 + + # 从右到左:左边评分高,则左边糖果 = max(当前左边糖果, 右边糖果 + 1) + for i in range(n - 2, -1, -1): + if ratings[i] > ratings[i + 1]: + candies[i] = max(candies[i], candies[i + 1] + 1) + + return sum(candies) # 返回总糖果数 +``` + + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是孩子的数量。我们需要遍历两次数组以分别计算满足左规则或右规则的最少糖果数量。 + +- 空间复杂度:O(n),其中 n 是孩子的数量。我们需要保存所有的左规则对应的糖果数量。 + +##### 方法二: 一次遍历 + + + +在上面的方法中,我们只考虑了递增段的处理,没有考虑如何处理递减段,所以才必须使用两次遍历,那该如何将递减段的处理一同加入呢? + +分析一下,如果当前孩子的ratings[i]比左侧孩子的ratings[i-1]更小,到底该分配多大的值?这取决于这个递减段的长度。 + +- 显然,如果i是最右侧的孩子,他可以只分配1颗糖果; +- 如果i是倒数第二个孩子,右侧还有一个递减,那么他最少分配2颗糖果。 + +核心思路:遍历数组时,跟踪当前是上升趋势、下降趋势,还是平;对于连续下降段,不立即分糖果,而是等下降结束后一次性累加。 +观察下降的规律,因为不处理的话就是默认分配1。如果下降长度为k,则需要补上1+2+....+k,即k * (k + 1) / 2颗糖果。 + +举个例子,假设ratings=[1,3,4,1],从左往右处理时,得到cadies=[1,2,3]。我们发现,下降段是[4,1],长度为1,所以补上1颗糖果,将数组变为 +candies=[1,2,3,1]。 + +特别的,如果下降长度 >= 上升长度,需要为“山峰”孩子多加一个糖果。 + +举个例子,假设ratings=[1,2,4,3,2,1],从左往右处理时,得到cadies=[1,2,3]。 +我们发现,下降段的长度为3,比上升段还长。 + +如果仅按照之前的处理,得到candies=[1,2,3,3,2,1]。实际上,第一个3应该变为4,也即山峰更高才对,正确的结果是candies=[1,2,4,3,2,1]。 + +代码如下: + + +```java +class Solution { + public int candy(int[] ratings) { + int n = ratings.length; + int total = 1; // 第一个孩子至少一个糖果 + int up = 0; // 上升序列长度 + int down = 0; // 下降序列长度 + int peak = 0; // 当前上升段的长度峰值 + + for (int i = 1; i < n; i++) { + if (ratings[i] > ratings[i - 1]) { // 上升 + up++; + peak = up; + down = 0; + total += 1 + up; // 当前孩子比前一个多一颗糖果 + } else if (ratings[i] == ratings[i - 1]) { // 平 + up = down = peak = 0; + total += 1; // 持平,默认为1 + } else { // 下降 + up = 0; + down++; + // 如果下降长度超过了之前的上升峰值,需要额外补偿 + total += 1 + down - (peak >= down ? 1 : 0); + } + } + + return total; + } +} +``` + +- 时间复杂度: O(n),其中 n为孩子总数 +- 空间复杂度: O(1),仅使用常数个额外变量 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/16.\346\216\245\351\233\250\346\260\264.md" "b/Algorithm/16.\346\216\245\351\233\250\346\260\264.md" new file mode 100644 index 00000000..0a86815c --- /dev/null +++ "b/Algorithm/16.\346\216\245\351\233\250\346\260\264.md" @@ -0,0 +1,149 @@ +16.接雨水 +=== + + +### 题目 + +给定n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 + +示例 1: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rainwatertrap_1.png?raw=true) + +- 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] +- 输出:6 +- 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 + +示例 2: + +- 输入:height = [4,2,0,3,2,5] +- 输出:9 + + +### 思路 + +##### 方法一:动态规划 + +- 对于下标i,下雨后水能到达的***最大高度***等于下标i两边的最大高度的最小值, +- 下标i处能接的雨水量等于下标i处的水能到达的***最大高度***减去height[i]。 +- 朴素的做法是对于数组height中的每个元素,分别向左和向右扫描并记录左边和右边的最大高度,然后计算每个下标位置能接的雨水量。 + + +上述做法的时间复杂度较高是因为需要对每个下标位置都向两边扫描。如果已经知道每个位置两边的最大高度,则可以在 O(n) 的时间内得到能接的雨水总量。使用动态规划的方法,可以在 O(n) 的时间内预处理得到每个位置两边的最大高度。 + +- 创建两个长度为n的数组leftMax(存储每个位置左侧的最高柱子高度)和rightMax(存储每个位置右侧的最高柱子高度)。 +- 对于leftMax[i]表示下标i及其左边的位置中,柱子的最大高度 +- rightMax[i]表示下标i及其右边的位置中,柱子的最大高度。 +- 显然,leftMax[0]=height[0],rightMax[n−1]=height[n−1]。 +- 两个数组的其余元素的计算如下: + - 从左到右遍历,保障每次leftMax[i-1]是截止当前左边最大的。当1≤i≤n−1时,leftMax[i]=max(leftMax[i−1],height[i]); + - 从右到左遍历,保障每次rightMax[i+1]是截止当前右边最大的。当0≤i≤n−2时,rightMax[i]=max(rightMax[i+1],height[i])。 + +- 在得到数组leftMax和rightMax的每个元素值之后,对于下标i处能接的雨水量等于`min(leftMax[i],rightMax[i])−height[i]`。遍历累加每个下标位置即可得到能接的雨水总量。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rainbow_2.png?raw=true) + + +```java +class Solution { + public int trap(int[] height) { + int n = height.length; + if (n == 0) { + return 0; + } + + int[] leftMax = new int[n]; + leftMax[0] = height[0]; + for (int i = 1; i < n; i++) { + leftMax[i] = Math.max(leftMax[i - 1], height[i]); + } + + int[] rightMax = new int[n]; + rightMax[n - 1] = height[n - 1]; + for (int i = n - 2; i >= 0; i--) { + rightMax[i] = Math.max(rightMax[i + 1], height[i]); + } + + int ans = 0; + for (int i = 0; i < n; i++) { + // 左右最小边界决定能存水的高度 + int minBound = Math.min(leftMax[i], rightMax[i]); + // 当前柱子能存的水高度 = 最小边界 - 当前高度 + ans += minBound - height[i]; + } + return ans; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n),其中n是数组height的长度。计算数组leftMax和rightMax的元素值各需要遍历数组height一次,计算能接的雨水总量还需要遍历一次。 +- 空间复杂度:O(n),其中n是数组height的长度。需要创建两个长度为n的数组leftMax和rightMax。 + + + +##### 方法二 + +动态规划的做法中,需要维护两个数组leftMax和rightMax,因此空间复杂度是O(n)。是否可以将空间复杂度降到O(1)? + +注意到下标i处能接的雨水量由leftMax[i]和rightMax[i]中的最小值决定。 + +- 由于数组leftMax是从左往右计算,数组rightMax是从右往左计算,因此可以使用双指针和两个变量代替两个数组。 + +- 维护两个指针left和right,以及两个变量leftMax(左侧已扫描的最大高度)和rightMax(右侧已扫描的最大高度),初始时left=0,right=n−1,leftMax=0,rightMax=0。 +- 指针left只会向右移动,指针right只会向左移动,在移动指针的过程中维护两个变量leftMax和rightMax的值。 + +当两个指针没有相遇时,进行如下操作: + +- 使用height[left]和height[right]的值更新leftMax和rightMax的值 + +- 如果`height[left] symbolValues = new HashMap() {{ + put('I', 1); + put('V', 5); + put('X', 10); + put('L', 50); + put('C', 100); + put('D', 500); + put('M', 1000); + }}; + + public int romanToInt(String s) { + int ans = 0; + int n = s.length(); + for (int i = 0; i < n; ++i) { + int value = symbolValues.get(s.charAt(i)); + if (i < n - 1 && value < symbolValues.get(s.charAt(i + 1))) { + ans -= value; + } else { + ans += value; + } + } + return ans; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是字符串 s 的长度。 + +- 空间复杂度:O(1)。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/18.\346\225\264\346\225\260\350\275\254\347\275\227\351\251\254\346\225\260\345\255\227.md" "b/Algorithm/18.\346\225\264\346\225\260\350\275\254\347\275\227\351\251\254\346\225\260\345\255\227.md" new file mode 100644 index 00000000..cce5ae83 --- /dev/null +++ "b/Algorithm/18.\346\225\264\346\225\260\350\275\254\347\275\227\351\251\254\346\225\260\345\255\227.md" @@ -0,0 +1,114 @@ +18.整数转罗马数字 +=== + + +### 题目 + +七个不同的符号代表罗马数字,其值如下: +``` +符号 值 +I 1 +V 5 +X 10 +L 50 +C 100 +D 500 +M 1000 +``` +罗马数字是通过添加从最高到最低的小数位值的转换而形成的。将小数位值转换为罗马数字有以下规则: + +- 如果该值不是以 4 或 9 开头,请选择可以从输入中减去的最大值的符号,将该符号附加到结果,减去其值,然后将其余部分转换为罗马数字。 +- 如果该值以 4 或 9 开头,使用 减法形式,表示从以下符号中减去一个符号,例如 4 是 5 (V) 减 1 (I): IV ,9 是 10 (X) 减 1 (I):IX。仅使用以下减法形式:4 (IV),9 (IX),40 (XL),90 (XC),400 (CD) 和 900 (CM)。 +- 只有 10 的次方(I, X, C, M)最多可以连续附加 3 次以代表 10 的倍数。你不能多次附加 5 (V),50 (L) 或 500 (D)。如果需要将符号附加4次,请使用 减法形式。 +- 给定一个整数,将其转换为罗马数字。 + + + +示例 1: + +- 输入:num = 3749 + +- 输出: "MMMDCCXLIX" + +- 解释: + + - 3000 = MMM 由于 1000 (M) + 1000 (M) + 1000 (M) + - 700 = DCC 由于 500 (D) + 100 (C) + 100 (C) + - 40 = XL 由于 50 (L) 减 10 (X) + - 9 = IX 由于 10 (X) 减 1 (I) +注意:49 不是 50 (L) 减 1 (I) 因为转换是基于小数位 + +示例 2: + +- 输入:num = 58 + +- 输出:"LVIII" + +解释: + + - 50 = L + - 8 = VIII + +示例 3: + +- 输入:num = 1994 + +- 输出:"MCMXCIV" + +- 解释: + + - 1000 = M + - 900 = CM + - 90 = XC + - 4 = IV + + +提示: + +- 1 <= num <= 3999 + +### 思路 + +题目中说了num <= 3999 + +所以: + +- 千位数只能由M表示,分别为 M,MM,MMM +- 百位数只能由C、CC、CCC、CD、D、DC、DCC、DCCC、CM表示 +- 十位数只能由X、XX、XXX、XL、L、LX、LXX、LXXX、XC表示 +- 个位数只能由I、II、III、IV、V、VI、VII、VIII、IV表示 + + +所以可以利用模运算和除法运算,得到num每个位上的数字,然后去取对应的罗马数字就可以了。 + +```java + +class Solution { + String[] thousands = {"", "M", "MM", "MMM"}; + String[] hundreds = {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}; + String[] tens = {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}; + String[] ones = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}; + + public String intToRoman(int num) { + StringBuffer roman = new StringBuffer(); + roman.append(thousands[num / 1000]); + roman.append(hundreds[num % 1000 / 100]); + roman.append(tens[num % 100 / 10]); + roman.append(ones[num % 10]); + return roman.toString(); + } +} +``` + +复杂度分析: + +- 时间复杂度:O(1)。计算量与输入数字的大小无关。 + +- 空间复杂度:O(1)。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/19.\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/Algorithm/19.\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..95e2c32f --- /dev/null +++ "b/Algorithm/19.\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,78 @@ +19.最后一个单词的长度 +=== + + +### 题目 + +给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。 + +单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。 + + + +示例 1: + +- 输入:s = "Hello World" +- 输出:5 +- 解释:最后一个单词是“World”,长度为 5。 + +示例 2: + +- 输入:s = " fly me to the moon " +- 输出:4 +- 解释:最后一个单词是“moon”,长度为 4。 + +示例 3: + +- 输入:s = "luffy is still joyboy" +- 输出:6 +- 解释:最后一个单词是长度为 6 的“joyboy”。 + + +提示: + +- 1 <= s.length <= 104 +- s 仅有英文字母和空格 ' ' 组成 +- s 中至少存在一个单词 + +### 思路 + +从最后一个字母开始往前遍历,并开始计数,找到第一个空格的时候停止。要注意没有空格的情况,例如"ab",应该返回2。 + +```java +class Solution { + public int lengthOfLastWord(String s) { + int end = s.length() - 1; + while(end >= 0 && s.charAt(end) == ' ') end--; + if(end < 0) return 0; + int start = end; + while(start >= 0 && s.charAt(start) != ' ') start--; + return end - start; + } +} +``` + + +```python +class Solution: + def lengthOfLastWord(self, s: str) -> int: + length = 0 + for i in reversed(range(len(s.rstrip()))): + if s.rstrip()[i] == " ": + return length + else: + length += 1 + return length +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是字符串的长度。最多需要反向遍历字符串一次。 + +- 空间复杂度:O(1)。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/2.\347\247\273\351\231\244\345\205\203\347\264\240.md" "b/Algorithm/2.\347\247\273\351\231\244\345\205\203\347\264\240.md" new file mode 100644 index 00000000..422431ad --- /dev/null +++ "b/Algorithm/2.\347\247\273\351\231\244\345\205\203\347\264\240.md" @@ -0,0 +1,118 @@ +2.移除元素 +=== + + +### 题目 + +给你一个数组`nums`和一个值`val`,你需要`原地`移除所有数值等于`val`的元素。元素的顺序可能发生改变。然后返回`nums`中与`val`不同的元素的数量。 + +假设`nums`中不等于`val`的元素数量为`k`,要通过此题,您需要执行以下操作: + +更改`nums`数组,使`nums`的前`k`个元素包含不等于`val`的元素。`nums`的其余元素和`nums`的大小并不重要。 +返回`k`。 + + +示例 1: + +输入:nums = [3,2,2,3], val = 3 +输出:2, nums = [2,2,_,_] +解释:你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。 +你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。 +示例 2: + +输入:nums = [0,1,2,2,3,0,4,2], val = 2 +输出:5, nums = [0,1,4,0,3,_,_,_] +解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。 +注意这五个元素可以任意顺序返回。 +你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。 + + +### 思路 + +##### 双指针: + +- 用一个变量k记录不等于val的元素数量,从0开始 +- 从头开始遍历nums中的元素,与val进行比较,如果不等于val,那就将nums[k]的值设置为nums中当前的元素,同时后移k + +```python +class Solution: + def removeElement(self, nums: List[int], val: int) -> int: + k = 0 + for num in nums: + if num != val: + nums[k] = num + k += 1 + return k +``` + + +```kotlin +class Solution { + fun removeElement(nums: IntArray, `val`: Int): Int { + var k = 0 + for (num in nums) { + if (num != `val`) { + nums[k] = num + k++ + } + } + return k + } +} +``` + +##### 双指针优化 + +上面的方案存在一个问题,就是例如数组为[1, 2, 3, 4, 5],而val为1时。我们需要把每一个元素都左移一位。 + +注意到题目上说:元素的顺序可以改变。 + +实际上我们只需要将最后一个元素5移动到序列开头,取代元素1,得到序列[5, 2, 3, 4]就可以。 + + +思路: +- 还是用双指针,一个从前往后left,一个从后往前right +- 从前往后的指针left的判断方式还是如同上面的思路 +- 如果left上的值等于val,那就把left位置的值换成right位置的值,同时移动right继续循环 +- 直到left > right相等,那就都遍历完了 + +```python +class Solution: + def removeElement(self, nums: List[int], val: int) -> int: + left = 0 + right = len(nums) - 1 + while left <= right: + if (nums[left] == val): + nums[left] = nums[right] + right -= 1 + else: + left += 1 + return left +``` + +```Kotlin +class Solution { + fun removeElement(nums: IntArray, `val`: Int): Int { + var left = 0 + var right = nums.size - 1 + while (left <= right) { + if (nums[left] == `val`) { + nums[left] = nums[right] + right -- + } else { + left ++ + } + } + + return left + } +} +``` + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/20.\346\234\200\351\225\277\345\205\254\345\205\261\345\211\215\347\274\200.md" "b/Algorithm/20.\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..ce72e6a5 --- /dev/null +++ "b/Algorithm/20.\346\234\200\351\225\277\345\205\254\345\205\261\345\211\215\347\274\200.md" @@ -0,0 +1,68 @@ +20.最长公共前缀 +=== + + +### 题目 + +编写一个函数来查找字符串数组中的最长公共前缀。 + +如果不存在公共前缀,返回空字符串 ""。 + + + +示例 1: + +- 输入:strs = ["flower","flow","flight"] +- 输出:"fl" + +示例 2: + +- 输入:strs = ["dog","racecar","car"] +- 输出:"" +- 解释:输入不存在公共前缀。 + + +提示: + +- 1 <= strs.length <= 200 +- 0 <= strs[i].length <= 200 +- strs[i] 如果非空,则仅由小写英文字母组成 + +### 思路 + + +###### 遍历每次找重合的前缀部分 + +横向扫描,依次遍历每个字符串,更新最长公共前缀 + +```python +class Solution: + def longestCommonPrefix(self, strs: List[str]) -> str: + ans = strs[0] + def compare(s1, s2) -> str: + result = "" + for i in range(min(len(s1), len(s2))): + if s1[i] == s2[i]: + result += s1[i] + else: + break + return result + + for i in range(1, len(strs)): + ans = compare(ans, strs[i]) + return ans +``` + + +复杂度分析: + +- 时间复杂度:O(mn),其中 m 是字符串数组中的字符串的平均长度,n 是字符串的数量。最坏情况下,字符串数组中的每个字符串的每个字符都会被比较一次。 + +- 空间复杂度:O(1)。使用的额外空间复杂度为常数。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/21.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\215\225\350\257\215.md" "b/Algorithm/21.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\215\225\350\257\215.md" new file mode 100644 index 00000000..1a5754c5 --- /dev/null +++ "b/Algorithm/21.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\215\225\350\257\215.md" @@ -0,0 +1,84 @@ +21.反转字符串中的单词 +=== + + +### 题目 + + +给你一个字符串 s ,请你反转字符串中 单词 的顺序。 + +单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。 + +返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。 + +注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。 + + + +示例 1: + +- 输入:s = "the sky is blue" +- 输出:"blue is sky the" + +示例 2: + +- 输入:s = " hello world " +- 输出:"world hello" +- 解释:反转后的字符串中不能存在前导空格和尾随空格。 + +示例 3: + +- 输入:s = "a good example" +- 输出:"example good a" +- 解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。 + + +提示: + +- 1 <= s.length <= 104 +- s 包含英文大小写字母、数字和空格 ' ' +- s 中 至少存在一个 单词 + + +### 思路 + +- 倒序遍历,记录每个单词的起始和结束长度 +- 结果中累加每个单词,注意在加下一个单词的时候需要提前加上空格 +- 遍历到最后一个空格或者第一个元素截止 +- 注意如果是第一个元素截止的时候需要考虑第一个元素是不是空格" ",例如: " asdasd df f" + - 如果是空格,需要从i + 1开始 + - 如果不是空格,需要从0开始 + +```python +class Solution: + def reverseWords(self, s: str) -> str: + length = len(s) + result = "" + last = -1 + + for i in reversed(range(length)): + if last == -1 and s[i] != " ": + last = i + if last > -1 and (s[i] == " " or i == 0): + if result != "": + result += " " + if i == 0 and s[i] != " ": + result += s[0: last + 1] + else: + result += s[i + 1: last + 1] + last = -1 + + return result +``` + + +复杂度分析: + +- 时间复杂度 O(N) : 其中 N 为字符串 s 的长度,线性遍历字符串。 +- 空间复杂度 O(N) : 新建的 list(Python) 或 StringBuilder(Java) 中的字符串总长度 ≤N ,占用 O(N) 大小的额外空间。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/22.Z\345\255\227\345\275\242\350\275\254\346\215\242.md" "b/Algorithm/22.Z\345\255\227\345\275\242\350\275\254\346\215\242.md" new file mode 100644 index 00000000..d4f3e661 --- /dev/null +++ "b/Algorithm/22.Z\345\255\227\345\275\242\350\275\254\346\215\242.md" @@ -0,0 +1,106 @@ +22.Z字形转换 +=== + + +### 题目 + +将一个给定字符串 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); + + +示例 1: + +- 输入:s = "PAYPALISHIRING", numRows = 3 +- 输出:"PAHNAPLSIIGYIR" + +示例 2: +- 输入:s = "PAYPALISHIRING", numRows = 4 +- 输出:"PINALSIGYAHRPI" +- 解释: +``` +P I N +A L S I G +Y A H R +P I +``` +示例 3: + +- 输入:s = "A", numRows = 1 +- 输出:"A" + + +提示: + +- 1 <= s.length <= 1000 +- s 由英文字母(小写和大写)、',' 和 '.' 组成 +- 1 <= numRows <= 1000 + +### 思路 + +找规律,假设s = "PAYPALISHIRING", numRows = 3 + +- 从前往后遍历s +- s[0] : 第一行 +- s[1] : 第二行 +- s[2] : 第三行, 大于等于numRows了,行要开始递减了 +- s[3] : 第二行, +- s[4] : 第一行, 到最小行了,行要开始递加了 +- s[5] : 第二行 +- ... + + +```python + +class Solution: + def convert(self, s: str, numRows: int) -> str: + if len(s) < 3 or numRows < 2: + return s + row = 1 + add = True + # 用一个数组记录每一行的字符内容 + rowArray = [""] * numRows + for i in s: + rowArray[row - 1] += i + + if add: + row += 1 + else: + row -= 1 + if row > numRows: + add = False + # 本来要减1因为上面刚加了1,所以这里要减2 + row -= 2 + elif row < 1: + add = True + row += 2 + + result = "" + for i in rowArray: + result += i + return result +``` + + + + +复杂度分析: + +- 时间复杂度 O(N) :遍历一遍字符串 s; +- 空间复杂度 O(N) :各行字符串共占用 O(N) 额外空间。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/23.\346\211\276\345\207\272\345\255\227\347\254\246\344\270\262\344\270\255\347\254\254\344\270\200\344\270\252\345\214\271\351\205\215\351\241\271\347\232\204\344\270\213\346\240\207.md" "b/Algorithm/23.\346\211\276\345\207\272\345\255\227\347\254\246\344\270\262\344\270\255\347\254\254\344\270\200\344\270\252\345\214\271\351\205\215\351\241\271\347\232\204\344\270\213\346\240\207.md" new file mode 100644 index 00000000..b3f118f9 --- /dev/null +++ "b/Algorithm/23.\346\211\276\345\207\272\345\255\227\347\254\246\344\270\262\344\270\255\347\254\254\344\270\200\344\270\252\345\214\271\351\205\215\351\241\271\347\232\204\344\270\213\346\240\207.md" @@ -0,0 +1,324 @@ +23.找出字符串中第一个匹配项的下标 +=== + + +### 题目 + +给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。 + + + +示例 1: + +- 输入:haystack = "sadbutsad", needle = "sad" +- 输出:0 +- 解释:"sad" 在下标 0 和 6 处匹配。 +- 第一个匹配项的下标是 0 ,所以返回 0 。 + +示例 2: + +- 输入:haystack = "leetcode", needle = "leeto" +- 输出:-1 +- 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。 + + +提示: + +- 1 <= haystack.length, needle.length <= 104 +- haystack 和 needle 仅由小写英文字符组成 + +### 思路 + +##### 方法一: 普通对比 + +```python + +class Solution: + def strStr(self, haystack: str, needle: str) -> int: + length = len(needle) + for i in range(len(haystack)): + if haystack[i] == needle[0]: + if haystack[i: i+length] == needle: + return i + return -1 +``` + + +复杂度分析 + +- 时间复杂度:O(n×m),其中 n 是字符串 haystack 的长度,m 是字符串 needle 的长度。最坏情况下我们需要将字符串 needle 与字符串 haystack 的所有长度为 m 的子串均匹配一次。 + +- 空间复杂度:O(1)。我们只需要常数的空间保存若干变量。 + + +##### 方法二: KMP + + + +上述的朴素解法,不考虑剪枝的话复杂度是 O(m∗n) 的,而 KMP 算法的复杂度为 O(m+n)。 + +KMP算法是一种字符串匹配算法,可以在 O(n+m) 的时间复杂度内实现两个字符串的匹配。 + +KMP 之所以能够在 O(m+n) 复杂度内完成查找,是因为其能在「非完全匹配」的过程中提取到有效信息进行复用,以减少「重复匹配」的消耗。 + + +KMP算法的核心,是一个被称为部分匹配表(Partial Match Table)的数组。 + +###### 算法原理 + +从主字符串的第一个字符开始:KMP算法从主字符串的第一个字符开始,将其与子字符串的第一个字符进行比较。 相等与不等的情况:如果字符相等,则继续比较后续字符;如果不等,则根据部分匹配表(即next数组)的值,将子字符串向右移动若干个字符,然后再次进行比较。 + + +###### 算法效率 + +- 时间复杂度:KMP算法的时间复杂度为O(m+n),其中m和n分别是模式串和主串的长度。相较于O(n^2)的暴力匹配算法,KMP算法具有较高的效率。 + + + +###### KMP算法的本质 + +理解计算next数组是核心。 + + +next数组是匹配串的一个查找表,它的定义可以用下面一句话来解释。就是kmp算法的本质: + +***next数组的每个元素表示匹配串中从起始到以当前字符结尾的子串中以当前字符结尾的连续重复最长串长度。*** + +字符串abcdabe,len是每个子串以最后一个字符结尾的连续重复最长串长度: + +- next[0] = 0 // 子串'a'中没有包含以'a'结尾的连续重复子串,len = 0 +- next[1] = 0 // 子串'ab'中没有包含以'b'结尾的连续重复子串,len = 0 +- next[2] = 0 // 子串'abc'中没有包含以'c'结尾的连续重复子串,len = 0 +- next[3] = 0 // 子串'abcd'中没有包含以'd'结尾的连续重复子串,len = 0 +- next[4] = 1 // 子串'abcda'中包含以'a'结尾的连续重复子串是'a',len = 1 +- next[5] = 2 // 子串'abcdab'中包含以'b'结尾的连续重复子串'ab',len = 2 +- next[6] = 0 // 子串'abcdabe'中没有包含以'e'结尾的连续重复子串,len = 0 + +匹配过程与计算next的思路相似,如果当前字符不匹配,就往回跳,跳多少呢,就是前面已比较串的以最后一个字符结尾的连续最长重复长度,最长也就一半,不用跳到开头,即next[j-1]的长度,不用再重头比较, + + + + + +所谓字符串匹配,是这样一种问题: 字符串P是否为字符串S的子串?如果是,它出现在S的哪个位置。 + +- S称为主串。 +- P称为模式串。 + + +最简单的方法就是上面的方法一,不断的去遍历查找。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force.png?raw=true) + + +这就是Brute-Force 算法。现在,我们需要对它的时间复杂度做一点讨论,它的时间复杂度是O(n×m)。 + +我们很难降低字符串比较的复杂度(因为比较两个字符串,真的只能逐个比较字符)。 + +因此,我们考虑降低比较的趟数。如果比较的趟数能降到足够低,那么总的复杂度也将会下降很多。 + + +要优化一个算法,首先要回答的问题是“我手上有什么信息?” 我们手上的信息是否足够、是否有效,决定了我们能把算法优化到何种程度。请记住:尽可能利用残余的信息,是KMP算法的思想所在。 + + +在Brute-Force中,如果从S[i]开始的那一趟比较失败了,算法会直接开始尝试从S[i+1]开始比较。 + +这种行为,属于典型的“没有从之前的错误中学到东西”。 + +我们应当注意到,一次失败的匹配,会给我们提供宝贵的信息: + +- 如果 S[i : i+len(P)] 与 P 的匹配是在第 r 个位置失败的,那么从 S[i] 开始的 (r-1) 个连续字符,一定与 P 的前 (r-1) 个字符一模一样! +- 为什么呢? 因为不然得话你在r之前就失败了。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_2.png?raw=true) + + +需要实现的任务是“字符串匹配”,而每一次失败都会给我们换来一些信息——能告诉我们,主串的某一个子串等于模式串的某一个前缀。但是这又有什么用呢? + +有些趟字符串比较是有可能会成功的;有些则毫无可能。 + +我们刚刚提到过,优化 Brute-Force的路线是“尽量减少比较的趟数”,而如果我们跳过那些绝不可能成功的字符串比较,则可以希望复杂度降低到能接受的范围。   + +那么,哪些字符串比较是不可能成功的?来看一个例子。已知信息如下: + +- 模式串 P = "abcabd". +- 和主串从S[0]开始匹配时,在 P[5] 处失配。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_3.png?raw=true) + +- 既然是在 P[5] 失配的,那么说明 S[0:5] 等于 P[0:5],即"abcab". + +- 现在我们来考虑:从 S[1]、S[2]、S[3] 开始的匹配尝试,有没有可能成功?   + +- 从 S[1] 开始肯定没办法成功,因为 S[1] = P[1] = 'b',和 P[0] 并不相等。 + +- 从 S[2] 开始也是没戏的,因为 S[2] = P[2] = 'c',并不等于P[0]. + +- 但是从 S[3] 开始是有可能成功的——至少按照已知的信息,我们推不出矛盾。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_4.png?raw=true) + + +也就是说当主串的某个字符c发生不匹配时,如果主串回退,最终还是会重新匹配到字符c上。 +那干脆不回退,岂不美哉! +也就是说主串一直遍历不回退。 +主串不回退,那模式串必须回退尽可能少,并且模式串回退位置的前面那段已经和主串匹配,这样主串才能不用回退。 + +如何找到模式串回退的位置呢? + +在不匹配发生时,前面匹配的那一小段字符对于主串和模式串都是相同的(如果不相同,在这之前会就匹配失败了)。 +那既然这一小段是主串和模式串相同的。那我就用这个串的头部去匹配这个串的尾部,最长的那段就是答案,也就是模式串改回退到的位置。 + + +***带着“跳过不可能成功的尝试”的思想,我们来看next数组。*** + +那就假设模式串在其所有位置上都发生了不匹配,模式串在和主串匹配前把其所有位置的最长匹配都算出来(算个长度就行),生成一张表,之后我俩发生不匹配时直接查这张表就行。这就是next数组。 + +###### next数组 + +next数组是对于***模式串***而言的。 + +P 的 next 数组定义为:next[i] 表示 P[0] ~ P[i] 这一个子串,使得***前k个字符恰等于后k个字符的最大的k***. 特别地,k不能取i+1(因为这个子串一共才 i+1 个字符,自己肯定与自己相等,就没有意义了)。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_5.png?raw=true) + +上图给出了一个例子。P="abcabd"时,next[4]=2,这是因为P[0] ~ P[4] 这个子串是"abcab",前两个字符与后两个字符相等,因此next[4]取2. + +而next[5]=0,是因为"abcabd"找不到前缀与后缀相同,因此只能取0. + +如果把模式串视为一把标尺,在主串上移动,那么 Brute-Force 就是每次失配之后只右移一位;改进算法则是每次失配之后,移很多位,跳过那些不可能匹配成功的位置。但是该如何确定要移多少位呢? + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_6.png?raw=true) + + +- 在 S[0] 尝试匹配,失配于 S[3] != P[3] 之后,我们直接把模式串往右移了两位,让 S[3] 对准 P[1]. + +- 接着继续匹配,失配于 S[8] != P[6], 接下来我们把 P 往右平移了三位,把 S[8] 对准 P[3]. + +- 此后继续匹配直到成功。   + + +我们应该如何移动这把标尺?很明显,如图中蓝色箭头所示,旧的后缀要与新的前缀一致(如果不一致,那就肯定没法匹配上了)!   + + +--- +回忆next数组的性质:P[0] 到 P[i] 这一段子串中,前next[i]个字符与后next[i]个字符一模一样。 + +既然如此,如果失配在`P[r]`, 那么`P[0]~P[r-1]`这一段里面,前`next[r-1]`个字符恰好和后`next[r-1]`个字符相等——也就是说,我们可以拿长度为`next[r-1]`的那一段前缀,来顶替当前后缀的位置,让匹配继续下去。 + + +您可以验证一下上面的匹配例子:P[3]失配后,把P[next[3-1]]也就是P[1]对准了主串刚刚失配的那一位;P[6]失配后,把P[next[6-1]]也就是P[3]对准了主串刚刚失配的那一位。 + +--- + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_7.png?raw=true) + + + +如上图所示,绿色部分是成功匹配,失配于红色部分。深绿色手绘线条标出了相等的前缀和后缀,其长度为next[右端]. 由于手绘线条部分的字符是一样的,所以直接把前面那条移到后面那条的位置。因此说,next数组为我们如何移动标尺提供了依据。接下来,我们实现这个优化的算法。 + +了解了利用next数组加速字符串匹配的原理,我们接下来代码实现之。分为两个部分: + +- 建立next数组 + +- 利用next数组进行匹配。 + + +首先是建立next数组。我们暂且用最朴素的做法,以后再回来优化: + +```python + +def getNxt(x): + for i in range(x, 0, -1): + if p[0: 1] == p[x-i+1:x+1]: + return i + return 0 + +nxt = [getNxt(x) for x in range(len(p))] +``` + +如上图代码所示,直接根据next数组的定义来建立next数组。不难发现它的复杂度是 的。 +  接下来,实现利用next数组加速字符串匹配。代码如下: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_8.png?raw=true) + + + +###### 快速求next数组 + + +终于来到了我们最后一个问题——如何快速构建next数组。   + +- 首先说一句:快速构建next数组,是KMP算法的精髓所在,核心思想是“P自己与自己做匹配”。  + +为什么这样说呢?回顾next数组的完整定义: + +- 定义 “k-前缀” 为一个字符串的前 k 个字符; “k-后缀” 为一个字符串的后 k 个字符。k 必须小于字符串长度。 + +- next[x]定义为:`P[0]~P[x]`这一段字符串,使得k-前缀恰等于k-后缀的最大的k.   + +这个定义中,不知不觉地就包含了一个匹配——前缀和后缀相等。 + +接下来,我们考虑采用递推的方式求出next数组。如果next[0], next[1], ... next[x-1]均已知,那么如何求出 next[x] 呢?   + +来分情况讨论。首先,已经知道了 next[x-1](以下记为now),如果 P[x] 与 P[now] 一样,那最长相等前后缀的长度就可以扩展一位,很明显 next[x] = now + 1. 图示如下。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_9.png?raw=true) + +刚刚解决了 P[x] = P[now] 的情况。那如果 P[x] 与 P[now] 不一样,又该怎么办? + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_10.png?raw=true) + +如图。 + +长度为now的子串A和子串B是`P[0]~P[x-1]`中最长的公共前后缀。 + +可惜 A 右边的字符和 B 右边的那个字符不相等,next[x]不能改成now+1了。 + +因此,我们应该缩短这个now,把它改成小一点的值,再来试试 P[x] 是否等于 P[now].   + +now该缩小到多少呢?显然,我们不想让now缩小太多。因此我们决定,在保持`P[0]~P[x-1]`的now-前缀仍然等于now-后缀”的前提下,让这个新的now尽可能大一点。 + + +`P[0]~P[x-1]`的公共前后缀,前缀一定落在串A里面、后缀一定落在串B里面。 + +换句话讲:接下来now应该改成:使得 A的k-前缀等于B的k-后缀 的最大的k.   + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_14.png?raw=true) + +也就是说:假设这个时候最大的前缀和后缀是字符串X,那将这个前缀和后缀去掉一个字符(去掉c)后,得到的两个新的串也必然是相等的。 +也就是只可能是现有最大串的子串。也就是a的前缀和b的后缀(而这个时候a和b是一样的,所以就变成找A中的前缀和后缀一样的子串)。 + +您应该已经注意到了一个非常强的性质——串A和串B是相同的!B的后缀等于A的后缀!因此,使得A的k-前缀等于B的k-后缀的最大的k,其实就是串A的最长公共前后缀的长度 —— next[now-1]! + +简单说就是: 次大匹配必定在最大匹配中 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_11.png?raw=true) + + +来看上面的例子。当P[now]与P[x]不相等的时候,我们需要缩小now——把now变成next[now-1],直到P[now]=P[x]为止。P[now]=P[x]时,就可以直接向右扩展了。 + + +代码实现如下: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_12.png?raw=true) + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_13.png?raw=true) + + + +https://www.zhihu.com/question/21923021/answer/281346746 + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" "b/Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" new file mode 100644 index 00000000..ffde6522 --- /dev/null +++ "b/Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" @@ -0,0 +1,160 @@ +24.文本左右对齐 +=== + + +### 题目 + +给定一个单词数组 words 和一个长度 maxWidth ,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。 + +你应该使用 “贪心算法” 来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 ' ' 填充,使得每行恰好有 maxWidth 个字符。 + +要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。 + +文本的最后一行应为左对齐,且单词之间不插入额外的空格。 + +注意: + +- 单词是指由非空格字符组成的字符序列。 +- 每个单词的长度大于 0,小于等于 maxWidth。 +- 输入单词数组 words 至少包含一个单词。 + + +示例 1: + +- 输入: words = ["This", "is", "an", "example", "of", "text", "justification."], maxWidth = 16 +- 输出: +``` +[ + "This is an", + "example of text", + "justification. " +] +``` +示例 2: + +- 输入:words = ["What","must","be","acknowledgment","shall","be"], maxWidth = 16 +- 输出: +``` +[ + "What must be", + "acknowledgment ", + "shall be " +] +``` +- 解释: + - 注意最后一行的格式应为 "shall be " 而不是 "shall be", + - 因为最后一行应为左对齐,而不是左右两端对齐。 + - 第二行同样为左对齐,这是因为这行只包含一个单词。 + +示例 3: + +- 输入:words = ["Science","is","what","we","understand","well","enough","to","explain","to","a","computer.","Art","is","everything","else","we","do"],maxWidth = 20 +- 输出: +``` +[ + "Science is what we", + "understand well", + "enough to explain to", + "a computer. Art is", + "everything else we", + "do " +] +``` + +提示: + +- 1 <= words.length <= 300 +- 1 <= words[i].length <= 20 +- words[i] 由小写英文字母和符号组成 +- 1 <= maxWidth <= 100 +- words[i].length <= maxWidth + +### 思路 + +字符串大模拟,分情况讨论即可: + +- 如果当前行只有一个单词,特殊处理为左对齐; +- 如果当前行为最后一行,特殊处理为左对齐; +- 其余为一般情况,分别计算「当前行单词总长度」、「当前行空格总长度」和「往下取整后的单位空格长度」,然后依次进行拼接。当空格无法均分时,每次往靠左的间隙多添加一个空格,直到剩余的空格能够被后面的间隙所均分。 + +```java + +class Solution { + public List fullJustify(String[] words, int maxWidth) { + List ans = new ArrayList<>(); + int n = words.length; + List list = new ArrayList<>(); + for (int i = 0; i < n; ) { + // list 装载当前行的所有 word + list.clear(); + list.add(words[i]); + int cur = words[i++].length(); + while (i < n && cur + 1 + words[i].length() <= maxWidth) { + cur += 1 + words[i].length(); + list.add(words[i++]); + } + + // 当前行为最后一行,特殊处理为左对齐 + if (i == n) { + StringBuilder sb = new StringBuilder(list.get(0)); + for (int k = 1; k < list.size(); k++) { + sb.append(" ").append(list.get(k)); + } + while (sb.length() < maxWidth) sb.append(" "); + ans.add(sb.toString()); + break; + } + + // 如果当前行只有一个 word,特殊处理为左对齐 + int cnt = list.size(); + if (cnt == 1) { + String str = list.get(0); + while (str.length() != maxWidth) str += " "; + ans.add(str); + continue; + } + + /** + * 其余为一般情况 + * wordWidth : 当前行单词总长度; + * spaceWidth : 当前行空格总长度; + * spaceItem : 往下取整后的单位空格长度 + */ + int wordWidth = cur - (cnt - 1); + int spaceWidth = maxWidth - wordWidth; + int spaceItemWidth = spaceWidth / (cnt - 1); + String spaceItem = ""; + for (int k = 0; k < spaceItemWidth; k++) spaceItem += " "; + StringBuilder sb = new StringBuilder(); + for (int k = 0, sum = 0; k < cnt; k++) { + String item = list.get(k); + sb.append(item); + if (k == cnt - 1) break; + sb.append(spaceItem); + sum += spaceItemWidth; + // 剩余的间隙数量(可填入空格的次数) + int remain = cnt - k - 1 - 1; + // 剩余间隙数量 * 最小单位空格长度 + 当前空格长度 < 单词总长度,则在当前间隙多补充一个空格 + if (remain * spaceItemWidth + sum < spaceWidth) { + sb.append(" "); + sum++; + } + } + ans.add(sb.toString()); + } + return ans; + } +} +``` + +复杂度分析: + +- 时间复杂度:会对 words 做线性扫描,最坏情况下每个 words[i] 独占一行,此时所有字符串的长度为 n∗maxWidth。复杂度为 O(n∗maxWidth) +- 空间复杂度:最坏情况下每个 words[i] 独占一行,复杂度为 O(n∗maxWidth) + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" "b/Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" new file mode 100644 index 00000000..b8720d78 --- /dev/null +++ "b/Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" @@ -0,0 +1,104 @@ +25.验证回文串 +=== + + +### 题目 + +如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。 + +字母和数字都属于字母数字字符。 + +给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。 + + + +示例 1: + +- 输入: s = "A man, a plan, a canal: Panama" +- 输出:true +- 解释:"amanaplanacanalpanama" 是回文串。 + +示例 2: + +- 输入:s = "race a car" +- 输出:false +- 解释:"raceacar" 不是回文串。 +示例 3: + +- 输入:s = " " +- 输出:true +- 解释:在移除非字母数字字符之后,s 是一个空字符串 "" 。 +- 由于空字符串正着反着读都一样,所以是回文串。 + + +提示: + +- 1 <= s.length <= 2 * 105 +- s 仅由可打印的 ASCII 字符组成 + + +### 思路 + +##### 方法一 + +最简单的方法是对字符串 s 进行一次遍历,并将其中的字母和数字字符进行保留,放在另一个字符串 sgood 中。这样我们只需要判断 sgood 是否是一个普通的回文串即可。 + +判断的方法有两种: + +- 第一种是使用语言中的字符串翻转 API 得到 sgood 的逆序字符串 sgood_rev,只要这两个字符串相同,那么 sgood 就是回文串。 +- 第二种是使用双指针。初始时,左右指针分别指向 sgood 的两侧,随后我们不断地将这两个指针相向移动,每次移动一步,并判断这两个指针指向的字符是否相同。当这两个指针相遇时,就说明 sgood 时回文串。 + +```java +class Solution { + public boolean isPalindrome(String s) { + StringBuffer sgood = new StringBuffer(); + int length = s.length(); + for (int i = 0; i < length; i++) { + char ch = s.charAt(i); + if (Character.isLetterOrDigit(ch)) { + sgood.append(Character.toLowerCase(ch)); + } + } + int n = sgood.length(); + int left = 0, right = n - 1; + while (left < right) { + if (Character.toLowerCase(sgood.charAt(left)) != Character.toLowerCase(sgood.charAt(right))) { + return false; + } + ++left; + --right; + } + return true; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n),其中n是字符串s的长度。 + +- 空间复杂度:O(n)。由于我们需要将所有的字母和数字字符存放在另一个字符串中,在最坏情况下,新的字符串sgood与原字符串s完全相同,因此需要使用 O(n) 的空间。 + + +##### 方法二:在原字符串上直接判断 + +我们可以对方法一中第二种判断回文串的方法进行优化,就可以得到只使用 O(1) 空间的算法。 + +我们直接在原字符串 s 上使用双指针。在移动任意一个指针时,需要不断地向另一指针的方向移动,直到遇到一个字母或数字字符,或者两指针重合为止。 + +也就是说,我们每次将指针移到下一个字母字符或数字字符,再判断这两个指针指向的字符是否相同。 + + + + +复杂度分析: + +- 时间复杂度:O(n),其中n是字符串s的长度。 + +- 空间复杂度:O(1)。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/26.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" "b/Algorithm/26.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" new file mode 100644 index 00000000..77ea7063 --- /dev/null +++ "b/Algorithm/26.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" @@ -0,0 +1,129 @@ +26.判断子序列 +=== + + +### 题目 + +给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 + +字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。 + +进阶: + +如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码? + + +示例 1: + +- 输入:s = "abc", t = "ahbgdc" +- 输出:true + +示例 2: + +- 输入:s = "axc", t = "ahbgdc" +- 输出:false + + +提示: + +- 0 <= s.length <= 100 +- 0 <= t.length <= 10^4 +- 两个字符串都只由小写字符组成。 + +### 思路 + + +首先,如果 s 是空串,直接返回 true,因为空串是任何字符串的子序列。 +设置双指针 i , j 分别指向字符串 s , t 的首个字符,遍历字符串 t: + +- 当 s[i] == t[j] 时,代表匹配成功,此时同时 i++ , j++ ; + - 进而,若 i 已走过 s 尾部,代表 s 是 t 的子序列,此时应提前返回 true ; +- 当 s[i] != t[j] 时,代表匹配失败,此时仅 j++ ; + +若遍历完字符串 t 后,字符串 s 仍未遍历完,代表 s 不是 t 的子序列,此时返回 false 。 + +```java + +class Solution { + public boolean isSubsequence(String s, String t) { + int i = 0, j = 0; + while (i < s.length() && j < t.length()) { + if (s.charAt(i) == t.charAt(j)) { + i++; + } + j++; + } + return i == s.length(); + } +} +``` + +复杂度分析: + +- 时间复杂度 O(N) : 其中 N 为字符串 t 的长度。最差情况下需完整遍历 t 。 +- 空间复杂度 O(1) : i , j 变量使用常数大小空间。 + + +##### 进阶问题解法 + +如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码? + + +这种类似对同一个长字符串做很多次匹配的 ,可以像 KMP 算法一样,先用一些时间将长字符串中的数据 提取出来,磨刀不误砍柴功。有了提取好的数据,就可以快速的进行匹配。 + +因为S非常多,所以可以通过一次性对T进行简化处理,这样来减少后续每一次S匹配T的遍历时间: + +- 这里需要的数据就是匹配到某一点时,待匹配的字符在长字符串中 下一次 出现的位置。 + +- 所以我们前期多做一点工作,将长字符串研究透彻,假如长字符串的长度为 n,建立一个 n∗26 大小的矩阵,表示每个位置上26个字符下一次出现的位置。实现如下: + +- 对于要匹配的短字符串,遍历每一个字符,不断地寻找该字符在长字符串中的位置,然后将位置更新,寻找下一个字符,相当于在长字符串上“跳跃”。 + +- 如果下一个位置为 -1,表示长字符串再没有该字符了,返回 false 即可。 + +- 如果能正常遍历完毕,则表示可行,返回 true + +- 需要注意的一点 + + - 对于 "abc" 在 "ahbgdc" 上匹配的时候,由于长字符串第一个 a 的下一个出现 a 的位置为 -1(不出现),会导致一个 bug。 + + - 所以在生成数组时在长字符串前插入一个空字符即可。 + +```java + +//进阶问题的解决 +public boolean isSubsequence(String s, String t) { + + //考虑到 对第一个字符的处理 ,在t 之前一个空字符 + t=' '+t; + + //对t长字符串 做预处理 + int[][] dp = new int[t.length()][26];//存储每一个位置上 a--z的下一个字符出现的位置 + for (char c = 'a'; c <= 'z'; c++) {//依次对每个字符作处理 + int nextPos = -1;//表示接下来不会在出现该字符 + + for (int i = t.length() - 1; i >= 0; i--) {//从最后一位开始处理 + dp[i][c - 'a'] = nextPos;//dp[i][c-'a'] 加上外层循环 就是对每一个位置的a---z字符的处理了 + if (t.charAt(i) == c) {//表示当前位置有该字符 那么指向下一个该字符出现的位置就要被更新 为i + nextPos = i; + } + } + } + + //数据的利用 ,开始匹配 + int index=0; + for (char c:s.toCharArray()){ + index=dp[index][c-'a'];//因为加了' ',所以之后在处理第一个字符的时候 如果是在第一行,就会去第一行,不影响之后字符的判断 + if(index==-1){ + return false; + } + } + return true; +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/27.\344\270\244\346\225\260\344\271\213\345\222\214 II - \350\276\223\345\205\245\346\234\211\345\272\217\346\225\260\347\273\204.md" "b/Algorithm/27.\344\270\244\346\225\260\344\271\213\345\222\214 II - \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..b879cd8b --- /dev/null +++ "b/Algorithm/27.\344\270\244\346\225\260\344\271\213\345\222\214 II - \350\276\223\345\205\245\346\234\211\345\272\217\346\225\260\347\273\204.md" @@ -0,0 +1,97 @@ +27.两数之和 II - 输入有序数组 +=== + + +### 题目 + +给你一个下标从1开始的整数数组numbers,该数组已按非递减顺序排列,请你从数组中找出满足相加之和等于目标数target的两个数。 + +如果设这两个数分别是numbers[index1]和numbers[index2],则`1 <= index1 < index2 <= numbers.length`。 + +以长度为`2`的整数数组`[index1, index2]`的形式返回这两个整数的下标`index1`和`index2`。 + +你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。 + +你所设计的解决方案必须只使用常量级的额外空间。 + + +示例 1: + +- 输入:numbers = [2,7,11,15], target = 9 +- 输出:[1,2] +- 解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。 + +示例 2: + +- 输入:numbers = [2,3,4], target = 6 +- 输出:[1,3] +- 解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。 + +示例 3: + +- 输入:numbers = [-1,0], target = -1 +- 输出:[1,2] +- 解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。 + + +提示: + +- 2 <= numbers.length <= 3 * 104 +- -1000 <= numbers[i] <= 1000 +- numbers 按 非递减顺序 排列 +- -1000 <= target <= 1000 +- 仅存在一个有效答案 + +### 思路 + +注意题目说仅存在一个有效答案 + + +初始时两个指针分别指向第一个元素位置和最后一个元素的位置。每次计算两个指针指向的两个元素之和,并和目标值比较。如果两个元素之和等于目标值,则发现了唯一解。如果两个元素之和小于目标值,则将左侧指针右移一位。如果两个元素之和大于目标值,则将右侧指针左移一位。移动指针之后,重复上述操作,直到找到答案。 + +使用双指针的实质是缩小查找范围。那么会不会把可能的解过滤掉?答案是不会。 + +假设`numbers[i]+numbers[j]=target`是唯一解,其中`0≤itarget`,因此一定是右指针左移,左指针不可能移到 i 的右侧。 + +- 如果右指针先到达下标 j 的位置,此时左指针还在下标 i 的左侧,`sum List[int]: + left = 0 + right = len(numbers) - 1 + result = [-1]*2 + while left < right: + sum = numbers[left] + numbers[right] + if sum < target: + left += 1 + elif sum > target: + right -= 1 + else: + return [left+1, right+1] + + return [-1, -1] +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是数组的长度。两个指针移动的总次数最多为 n 次。 + +- 空间复杂度:O(1)。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/28.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.md" "b/Algorithm/28.\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..d8f7ea6e --- /dev/null +++ "b/Algorithm/28.\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,73 @@ +28.盛最多水的容器 +=== + + +### 题目 + +给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 + +找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 + +返回容器可以储存的最大水量。 + +说明:你不能倾斜容器。 + + + +示例 1: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_28_1.png?raw=true) + +- 输入:[1,8,6,2,5,4,8,3,7] +- 输出:49 +- 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。 + +示例 2: + +- 输入:height = [1,1] +- 输出:1 + + +提示: + +- n == height.length +- 2 <= n <= 105 +- 0 <= height[i] <= 104 + +### 思路 + +- 求出两个index1和index2,其中index < index2,使得min(height[index1], height[index2]) * (index2 - index1)最大 +- 核心就是:优先移动短板指针,因为面积是由短板的高度决定。 + +在每个状态下,无论长板或短板向中间收窄一格,都会导致水槽 底边宽度 −1​ 变短: + +- 若向内 移动短板 ,水槽的短板 min(h[i],h[j]) 可能变大,因此下个水槽的面积 可能增大 。 +- 若向内 移动长板 ,水槽的短板 min(h[i],h[j])​ 不变或变小,因此下个水槽的面积 一定变小 。 + +因此,初始化双指针分列水槽左右两端,循环每轮将短板向内移动一格,并更新面积最大值,直到两指针相遇时跳出;即可获得最大面积。 + + +```python +class Solution: + def maxArea(self, height: List[int]) -> int: + i, j, res = 0, len(height) - 1, 0 + while i < j: + if height[i] < height[j]: + res = max(res, height[i] * (j - i)) + i += 1 + else: + res = max(res, height[j] * (j - i)) + j -= 1 + return res +``` + +复杂度分析: + +- 时间复杂度 O(N)​ : 双指针遍历一次底边宽度 N​​ 。 +- 空间复杂度 O(1)​ : 变量 i , j , res 使用常数额外空间。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/29.\344\270\211\346\225\260\344\271\213\345\222\214.md" "b/Algorithm/29.\344\270\211\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 00000000..fae1fd31 --- /dev/null +++ "b/Algorithm/29.\344\270\211\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,109 @@ +29.三数之和 +=== + + +### 题目 + + +给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。 + +注意:答案中不可以包含重复的三元组。 + + + + + +示例 1: + +- 输入:nums = [-1,0,1,2,-1,-4] +- 输出:[[-1,-1,2],[-1,0,1]] +- 解释: + - nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 + - nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 + - nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 +- 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 +注意,输出的顺序和三元组的顺序并不重要。 + +示例 2: + +- 输入:nums = [0,1,1] +- 输出:[] +- 解释:唯一可能的三元组和不为 0 。 + +示例 3: + +- 输入:nums = [0,0,0] +- 输出:[[0,0,0]] +- 解释:唯一可能的三元组和为 0 。 + + +提示: + +- 3 <= nums.length <= 3000 +- -105 <= nums[i] <= 105 + + +### 思路 + + +- 特判,对于数组长度 n,如果数组为 null 或者数组长度小于 3,返回 []。 +- 对数组进行从小到大的排序。 +- 从0到nums.length - 2(因为一共三个数,后面还有有两个数)遍历排序后数组: + - 若 nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于 0,直接返回结果。 + - 对于重复元素:跳过,避免出现重复解 + - 令左指针 L=i+1,右指针 R=n−1,当 L List[List[int]]: + res = [] + if not nums or len(nums) < 3: + return res + nums = sorted(nums) + for i in range(len(nums) - 2): + if nums[i] > 0: + return res + if i > 0 and nums[i] == nums[i - 1]: + continue + + left = i + 1 + right = len(nums) - 1 + while left < right: + result = nums[i] + nums[left] + nums[right] + if result > 0: + # 需要right减小 + right -= 1 + elif result < 0: + left += 1 + else: + res.append([nums[i], nums[left], nums[right]]) + # 去重复,判断左边或右边的元素是否与当前的相同,相同就夸脱 + while left < right and nums[left] == nums[left + 1]: + left += 1 + + while left < right and nums[right] == nums[right - 1]: + right -= 1 + # 移动指针 + left += 1 + right -= 1 + return res + +``` + + +复杂度分析: + +- 时间复杂度:O(n²),数组排序 O(NlogN),遍历数组 O(n),双指针遍历 O(n),总体 O(NlogN)+O(n)∗O(n),O(n²) +- 空间复杂度:O(1) + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/3.\345\210\240\351\231\244\346\234\211\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/Algorithm/3.\345\210\240\351\231\244\346\234\211\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..8465bf39 --- /dev/null +++ "b/Algorithm/3.\345\210\240\351\231\244\346\234\211\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,78 @@ +3.删除有序数组中的重复项 +=== + + +### 题目 + +给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 + +考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过: + +更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。 +返回 k 。 + + + +示例 1: + +输入:nums = [1,1,2] +输出:2, nums = [1,2,_] +解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。 +示例 2: + +输入:nums = [0,0,1,1,1,2,2,3,3,4] +输出:5, nums = [0,1,2,3,4] +解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。 + + + +### 思路 + +双指针 +- 既然非严格递增队列,那么重复的元素一定会相邻,那我们可以从0开始遍历,将没有重复的数值替换放到数组的开头 + + +```python +class Solution: + def removeDuplicates(self, nums: List[int]) -> int: + # 第一个位置的不用判断 + k = 1 + for i in range(len(nums)): + # 当前的数值与已放置的数值不同就重新放置 + if nums[i] != nums[k - 1]: + nums[k] = nums[i] + k += 1 + return k +``` + + +```kotlin +class Solution { + fun removeDuplicates(nums: IntArray): Int { + var k = 1 + for(i in nums){ + if (i != nums[k - 1]) { + nums[k] = i + k ++ + } + } + return k + } +} +``` + +优化: + +假如数组是[0, 1, 2, 3, 4, 5] +此时数组中没有重复元素,按照上面的方法,每次比较时nums[i]都不等于nums[k - 1],因此就会将k指向的元素原地复制一遍,这个操作其实是不必要的。 + +因此我们可以添加一个小判断,当 i - k > 1 时,才进行复制。 + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/30.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" "b/Algorithm/30.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" new file mode 100644 index 00000000..c04f8f5d --- /dev/null +++ "b/Algorithm/30.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" @@ -0,0 +1,102 @@ +30.长度最小的子数组 +=== + + +### 题目 + +给定一个含有 n 个正整数的数组和一个正整数 target 。 + +找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。 + + + +示例 1: + +- 输入:target = 7, nums = [2,3,1,2,4,3] +- 输出:2 +- 解释:子数组 [4,3] 是该条件下的长度最小的子数组。 + +示例 2: + +- 输入:target = 4, nums = [1,4,4] +- 输出:1 + +示例 3: + +- 输入:target = 11, nums = [1,1,1,1,1,1,1,1] +- 输出:0 + + +提示: + +- 1 <= target <= 109 +- 1 <= nums.length <= 105 +- 1 <= nums[i] <= 104 + + +进阶: + +如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。 + + + +### 思路 + +注意: +子数组 是数组中连续的 非空 元素序列。 + +所以下面排序的方法是错误的。 因为不连续 +```python +class Solution: + def minSubArrayLen(self, target, nums) -> int: + nums = sorted(nums, reverse=True) + result = 0 + total = 0 + for i in nums: + print(i) + total += i + result += 1 + print(total) + if total >= target: + print(result) + return result + return result +``` + + +2.使用队列相加(实际上我们也可以把它称作是滑动窗口,这里的队列其实就相当于一个窗口) + +我们把数组中的元素不停的入队,直到总和大于等于 s 为止,接着记录下队列中元素的个数,然后再不停的出队,直到队列中元素的和小于 s 为止(如果不小于 s,也要记录下队列中元素的个数,这个个数其实就是不小于 s 的连续子数组长度,我们要记录最小的即可)。接着再把数组中的元素添加到队列中……重复上面的操作,直到数组中的元素全部使用完为止。 + +```java +class Solution { + public int minSubArrayLen(int s, int[] nums) { + int leftIndex = 0; + int rightIndex = 0; + int sum = 0; + int min = Integer.MAX_VALUE; + while (rightIndex < nums.length) { + sum += nums[rightIndex++]; + while (sum >= s) { + min = Math.min(min, rightIndex - leftIndex); + sum -= nums[leftIndex++]; + } + } + return min == Integer.MAX_VALUE ? 0 : min; + } +} +``` + + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是数组的长度。指针 start 和 end 最多各移动 n 次。 + +- 空间复杂度:O(1)。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/31.\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/Algorithm/31.\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..32e4a899 --- /dev/null +++ "b/Algorithm/31.\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,107 @@ +31.无重复字符的最长子串 +=== + + +### 题目 + +给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。 + + + +示例 1: + +- 输入: s = "abcabcbb" +- 输出: 3 +- 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 + +示例 2: + +- 输入: s = "bbbbb" +- 输出: 1 +- 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 + +示例 3: + +- 输入: s = "pwwkew" +- 输出: 3 +- 解释: + - 因为无重复字符的最长子串是 "wke",所以其长度为 3。 + - 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 + + +提示: + +- 0 <= s.length <= 5 * 104 +- s 由英文字母、数字、符号和空格组成 + + +### 思路 + +这道题主要用到思路是:滑动窗口 + +什么是滑动窗口? + +其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列! + +如何移动? + +我们只要把队列的左边的元素移出就行了,直到满足题目要求! + +一直维持这样的队列,找出队列出现最长的长度时候,求出解! + + + +```python +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + leftIndex = 0 + rightIndex = 0 + + # pwwkew + # "abcabcbb" + result = 0 + while rightIndex < len(s): + index = s[leftIndex: rightIndex].find(s[rightIndex]) + if index >= 0: + leftIndex += index + 1 + rightIndex += 1 + result = max(result, rightIndex - leftIndex) + return result +``` + +这个实现的时间复杂度是O(n²),显然不满足。 + +可以使用Set或者HashMap进行优化: + + + +```python +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + leftIndex = 0 + rightIndex = 0 + result = 0 + data = set() + while rightIndex < len(s): + while s[rightIndex] in data: + data.remove(s[leftIndex]) + leftIndex += 1 + data.add(s[rightIndex]) + rightIndex += 1 + result = max(result, rightIndex - leftIndex) + return result +``` + + + +- 时间复杂度:O(n) + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" "b/Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" new file mode 100644 index 00000000..73f0d47d --- /dev/null +++ "b/Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" @@ -0,0 +1,218 @@ +32.串联所有单词的子串 +=== + + +### 题目 + +给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。 + +s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。 + +例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd","cdabef", "cdefab","efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。 + +返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。 + + + +示例 1: + +- 输入:s = "barfoothefoobarman", words = ["foo","bar"] +- 输出:[0,9] +- 解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。 +- 子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。 +- 子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。 +- 输出顺序无关紧要。返回 [9,0] 也是可以的。 + +示例 2: + +- 输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"] +- 输出:[] +- 解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。 +- s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。 +- 所以我们返回一个空数组。 + +示例 3: + +- 输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"] +- 输出:[6,9,12] +- 解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。 +- 子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。 +- 子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。 +- 子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。 + + +提示: + +- 1 <= s.length <= 104 +- 1 <= words.length <= 5000 +- 1 <= words[i].length <= 30 +- words[i] 和 s 由小写英文字母组成 + +### 思路 + +##### 方法一,从头到尾遍历s + +- words中每个字符的长度是m、words整个数组的长度是n +- 那我们每次可以从s中从头开始去遍历,每次是取 `m*n` 个字符(窗口) +- 然后把这个`m*n`个字符按照words中每个字符的长度m进行分割,然后把分割后的单词与words中的单词进行比较 +- 如果和words中的都一样,那当前的索引就是。否则不是 +- 在与words中的单词进行比较的时候,这个时候是不用关心顺序的,所以我们可以用一个哈希表来表示单词以及频次 + - 当窗口中出现一个单词时,我们就把该单词加到哈希表中,如果已经存在,那就在后面的值+1 + - 然后用哈希表中的内容与words中的单词进行对比,如果words中出现一个单词且也在哈希表中,那就将哈希表中该单词的频次减1,如果是0了就一次该单词 + - 等到最后都遍历完,如果哈希表的长度是0,那就说明完全与words重点额一样 + +```python + +class Solution: + + def findSubstring(self, s: str, words: List[str]) -> List[int]: + res = [] + if len(s) == 0 or len(words) == 0 or len(words[0]) == 0: + return[] + + wordsLength = len(words[0]) + slideLength = wordsLength * len(words) + wordsCount = len(words) + + wordsMap = dict() + for word in words: + wordsMap[word] = wordsMap.get(word, 0) + 1 + + tempMap = dict() + for index in range(len(s) - slideLength + 1): + currentString = s[index: index+slideLength] + print(currentString) + for currentStringIndex in range(wordsCount): + currentWord = currentString[currentStringIndex * wordsLength: (currentStringIndex + 1) * wordsLength] + tempMap[currentWord] = tempMap.get(currentWord, 0) + 1 + + print(tempMap) + print(wordsMap) + if tempMap == wordsMap: + res.append(index) + tempMap.clear() + + + return res +``` + + + +时间复杂度:O(n×m×k),n为s的长度,m为每个words中单词的长度,也就是len(words[0]),k为words的长度,也就是len(words) + + +空间复杂度:每次循环都新建一个字典tempMap,但循环结束就清除,所以空间为O(m)(m是words中不同单词的个数)?实际上每次循环都重新统计,所以空间上是O(m),但要注意,我们每次循环都重新统计整个子串,所以没有利用滑动窗口的特性。 + + +而下面方法二的时间复杂度是: +- O(n×k): n为s的长度,k为words中单词的个数 + + + +##### 方法二 + + +上面方法一种当仅使用一个从0开始的滑动窗口时,为了避免漏掉一些子串,在缩短窗口时,每次只能缩小一格,而且这还会导致窗口内所维护的单词计数无效。那么,希望有一个方法,可以保证: +- 不遗漏子串 +- 缩小窗口时,不会使窗口内的单词计数无效 + +那么,在使用滑动窗口时,每次都移动 sz = len(words[0]) 个字符,那么我们就可以按照单词的纬度进行统计。 +但是,这样会导致某一些子串没有枚举到(即[1, sz-1]为起点的子串都被忽略了),所以为了保证不遗漏子串,可以枚举以[0, sz-1]为起点的所有滑动窗口,并且每一个滑窗都是互相独立的。 + +--- + +- 建立滑动窗口 + + - 如何建立:计算窗口长度为:words中所有串拼接后的长度len,第一个窗口为[0,len-1]。 + - 如果窗口中的字符串和words中所有串拼接后相等,则说明满足要求。 + - 如何判断: + - 将words中的所有word放入hashmap。key为word,value为个数。 + - 对窗口进行substr操作,每隔d,substr一次。d为words中word的长度。将substr的结果作为key放入另一个hashmap,个数为value。 + - 当窗口中没有剩余字符时,对两个map进行判断,如果相等。说明满足要求。此时记录窗口的起点。 + +- 建立多起点的滑动窗口。 + + - 何为多起点: + - 一般理解滑动窗口从0或者某个数值开始,向右滑动不断滑动一个步长。 + - 此处需要建立多个滑动窗口,数量为d。起点分别为0,1,2...d; + + - 为何需要多起点: + - 如果只建立一个滑动窗口,那么每次就只能滑动一格,因为需要找到所有的可能。但是这样的操作意味着之前建立的map需要重新构建 + - 例如:foobarfoobar [foo][bar]。当foobar完成匹配后,向右滑动一格,oobarf。这时候需要重新构建map。插入oob ,arf。时间复杂度很高,而滑动窗口应该是线性时间复杂度, + - 理想状态是,滑动d格,删去左侧foo,加入右侧foo。这个时候不需要重新构建map,map中原本的foo的计数先-1再+1即可,然后判断即可。 + + 那么如果按照上面的方法,每次都滑动d,又会漏检。例如afoobarfoobar [foo][bar] + + 因此,需要多起点,afoobar,foobar。。个数为d个。这样每个窗口每次都是滑动d格。map的效率最高 + + - 如何建立多起点 + + - 从0开始初始化d个滑动窗口。每个滑动窗口每次都是滑动d格。建立相应的map。 + + - 最后得到vector>; + +- 滑动 + + - 由于已经建立了多起点的滑动窗口,所以不会存在漏检的情况。同时map的效率最高。 + + + + + + +```java + +class Solution { + public List findSubstring(String s, String[] words) { + // 记录所有满足的结果索引 + List res = new ArrayList<>(); + if (s == null || s.length() == 0 || words == null || words.length == 0) { + return res; + } + + HashMap map = new HashMap<>(); + // 每个单词的长度是固定的 + int one_word = words[0].length(); + int word_num = words.length; + // 窗口长度 + int all_len = one_word * word_num; + // 把words中的内容和次数都记录到HashMap中 + for (String word : words) { + map.put(word, map.getOrDefault(word, 0) + 1); + } + + for (int i = 0; i < one_word; i++) { + int left = i, right = i, count = 0; + HashMap tmp_map = new HashMap<>(); + while (right + one_word <= s.length()) { + String w = s.substring(right, right + one_word); + right += one_word; + if (!map.containsKey(w)) { + count = 0; + left = right; + tmp_map.clear(); + } else { + tmp_map.put(w, tmp_map.getOrDefault(w, 0) + 1); + count++; + while (tmp_map.getOrDefault(w, 0) > map.getOrDefault(w, 0)) { + String t_w = s.substring(left, left + one_word); + count--; + tmp_map.put(t_w, tmp_map.getOrDefault(t_w, 0) - 1); + left += one_word; + } + if (count == word_num) res.add(left); + } + } + } + return res; + } +} +``` + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/33.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.md" "b/Algorithm/33.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.md" new file mode 100644 index 00000000..c4f31bed --- /dev/null +++ "b/Algorithm/33.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.md" @@ -0,0 +1,109 @@ +33.最小覆盖子串 +=== + + +### 题目 + +给你一个字符串s、一个字符串t。返回s中涵盖t所有字符的最小子串。如果s中不存在涵盖t所有字符的子串,则返回空字符串 "" 。 + + + +注意: + +- 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。 +- 如果 s 中存在这样的子串,我们保证它是唯一的答案。 + + +示例 1: + +- 输入:s = "ADOBECODEBANC", t = "ABC" +- 输出:"BANC" +- 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。 + +示例 2: + +- 输入:s = "a", t = "a" +- 输出:"a" +- 解释:整个字符串 s 是最小覆盖子串。 + +示例 3: + +- 输入: s = "a", t = "aa" +- 输出: "" +- 解释: t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。 + + +提示: + +- m == s.length +- n == t.length +- 1 <= m, n <= 105 +- s 和 t 由英文字母组成 + + +进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗? + +### 思路 + +题目中说如果 s 中存在这样的子串,我们保证它是唯一的答案。 + +双指针 + +- 初始化ansLeft=−1, ansRight=m,用来记录最短子串的左右端点,其中m是s的长度。 +- 用一个哈希表(或者数组)cntT 统计 t 中每个字母的出现次数。 +- 初始化 left=0,以及一个空哈希表(或者数组)cntS,用来统计 s 子串中每个字母的出现次数。 +- 遍历 s,设当前枚举的子串右端点为 right,把 s[right] 的出现次数加一。 +- 遍历 cntS 中的每个字母及其出现次数,如果出现次数都大于等于 cntT 中的字母出现次数: + - 如果`right−left cnt = new HashMap<>(); + for (char c : t.toCharArray()) { + cnt.put(c, cnt.getOrDefault(c, 0) + 1); + } + int ans_l = -1; + int ans_r = s.length(); + int l = 0; + int count = cnt.size(); + for (int r = 0; r < s.length(); r++) { + char c = s.charAt(r); + if (cnt.containsKey(c)) { + cnt.put(c, cnt.get(c) - 1); + if (cnt.get(c) == 0) { + count--; + } + } + while (count == 0) { + if (ans_r - ans_l > r - l) { + ans_l = l; + ans_r = r; + } + char ch = s.charAt(l); + if (cnt.containsKey(ch)) { + if (cnt.get(ch) == 0) { + count++; + } + cnt.put(ch, cnt.get(ch) + 1); + } + l++; + } + } + return ans_l == -1 ? "" : s.substring(ans_l, ans_r+1); + } +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/34.\346\234\211\346\225\210\347\232\204\346\225\260\347\213\254.md" "b/Algorithm/34.\346\234\211\346\225\210\347\232\204\346\225\260\347\213\254.md" new file mode 100644 index 00000000..e1c6fefa --- /dev/null +++ "b/Algorithm/34.\346\234\211\346\225\210\347\232\204\346\225\260\347\213\254.md" @@ -0,0 +1,142 @@ +34.有效的数独 +=== + + +### 题目 + +请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。 + +数字 1-9 在每一行只能出现一次。 +数字 1-9 在每一列只能出现一次。 +数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图) + + +注意: + +- 一个有效的数独(部分已被填充)不一定是可解的。 +- 只需要根据以上规则,验证已经填入的数字是否有效即可。 +- 空白格用 '.' 表示。 + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/isValidSudoku.png?raw=true) + + +``` +输入:board = +[["5","3",".",".","7",".",".",".","."] +,["6",".",".","1","9","5",".",".","."] +,[".","9","8",".",".",".",".","6","."] +,["8",".",".",".","6",".",".",".","3"] +,["4",".",".","8",".","3",".",".","1"] +,["7",".",".",".","2",".",".",".","6"] +,[".","6",".",".",".",".","2","8","."] +,[".",".",".","4","1","9",".",".","5"] +,[".",".",".",".","8",".",".","7","9"]] +``` +输出:true + +``` +输入:board = +[["8","3",".",".","7",".",".",".","."] +,["6",".",".","1","9","5",".",".","."] +,[".","9","8",".",".",".",".","6","."] +,["8",".",".",".","6",".",".",".","3"] +,["4",".",".","8",".","3",".",".","1"] +,["7",".",".",".","2",".",".",".","6"] +,[".","6",".",".",".",".","2","8","."] +,[".",".",".","4","1","9",".",".","5"] +,[".",".",".",".","8",".",".","7","9"]] +``` +输出:false +解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。 + + +提示: + +- board.length == 9 +- board[i].length == 9 +- board[i][j] 是一位数字(1-9)或者 '.' + + +### 思路 + +有效的数独满足以下三个条件: + +- 同一个数字在每一行只能出现一次; + +- 同一个数字在每一列只能出现一次; + +- 同一个数字在每一个小九宫格只能出现一次。 + +--- + +- i是行标 +- j是列标 +- 数字从char直接转换成int,会变成对应的ASCII数字码,而不是原来的数值。解决方法就是当前char数字减'0':用两个char数字的ASCII码相减,差值就是原来char数值直接对应的int数值。 + - 为什么不是减'0',而是减'1'? + - 若运行了大佬的代码你会发现,减'0'的话会出现`ArrayIndexOutOfBoundsException`的异常。因为后面的代码将这个`num`作为了数组的下标。本身数组设置的就是9个位置,下标范围是`[0~8]`,那么遇到数字9作为下标的时候,不就越位了吗,所以就要减1,char转换成int的同时还能解决后面越位的情况。 +- boolean数组的巧妙建立 + - 第一个[]存放第?行/列/块 + - 第二个[]存放 相应数字 + - 结合起来解释就是:第?行/列/块 是否 出现过相应数字 + +- 行标决定一组block的起始位置(因为block为3行,所以除3取整得到组号,又因为每组block为3个,所以需要乘3),列标再细分出是哪个block(因为block是3列,所以除3取整) + +``` +blockIndex = i / 3 * 3 + j / 3的原因: +[0, 0, 0, 1, 1, 1, 2, 2, 2] +[0, 0, 0, 1, 1, 1, 2, 2, 2] +[0, 0, 0, 1, 1, 1, 2, 2, 2] +[3, 3, 3, 4, 4, 4, 5, 5, 5] +[3, 3, 3, 4, 4, 4, 5, 5, 5] +[3, 3, 3, 4, 4, 4, 5, 5, 5] +[6, 6, 6, 7, 7, 7, 8, 8, 8] +[6, 6, 6, 7, 7, 7, 8, 8, 8] +[6, 6, 6, 7, 7, 7, 8, 8, 8] +``` + +- blockIndex的规律探寻 + - 微观`9x9` -> 宏观`3x3` + - (1)`i/3`为行号,`j/3`为列号 + - (2)二维数组思路:`行号*列数+列号`,即位置 + + + + +```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 blockIndex = i / 3 * 3 + j / 3; + if (row[i][num] || col[j][num] || block[blockIndex][num]) { + return false; + } else { + row[i][num] = true; + col[j][num] = true; + block[blockIndex][num] = true; + } + } + } + } + return true; + } +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/35.\350\236\272\346\227\213\347\237\251\351\230\265.md" "b/Algorithm/35.\350\236\272\346\227\213\347\237\251\351\230\265.md" new file mode 100644 index 00000000..cdde4f92 --- /dev/null +++ "b/Algorithm/35.\350\236\272\346\227\213\347\237\251\351\230\265.md" @@ -0,0 +1,99 @@ +35.螺旋矩阵 +=== + + +### 题目 + + +给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。 + + +示例 1: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/spiralOrder_1.png?raw=true) + +- 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] +- 输出:[1,2,3,6,9,8,7,4,5] + +示例 2: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/spiralOrder_2.png?raw=true) + +- 输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] +- 输出:[1,2,3,4,8,12,11,10,9,5,6,7] + + +提示: + +- m == matrix.length +- n == matrix[i].length +- 1 <= m, n <= 10 +- -100 <= matrix[i][j] <= 100 + + +### 思路 + +- 对于这种螺旋遍历的方法,重要的是要确定上下左右四条边的位置 +- 初始化的时候,上边up就是0,下边down就是m-1,左边left是0,右边right是n-1。 +- 然后我们进行while循环 + - 先遍历上边第一行up ,将所有元素加入结果res,然后上边下移一位,如果此时上边大于下边,说明此时已经遍历完成了,直接break。 + + +```java +class Solution { + public List spiralOrder(int[][] matrix) { + List res=new ArrayList(); + + if(matrix.length==0 || matrix[0].length==0) { + return res; + } + + int m = matrix.length; + int n =matrix[0].length; + + int up = 0; + int down = m-1; + int left = 0; + int right = n-1; + + while(true) { + for(int i=left; i<=right; i++) { + res.add(matrix[up][i]); + } + + if (++up > down) { + break; + } + + for (int i = up; i <= down; i++) { + res.add(matrix[i][right]); + } + + if (--right < left) { + break; + } + + for (int i = right; i >= left; i--) { + res.add(matrix[down][i]); + } + + if (--down < up) { + break; + } + + for (int i = down; i >= up; i--) { + res.add(matrix[i][left]); + } + + if (++left > right) { + break; + } + } + return res; + } +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/36.\346\227\213\350\275\254\345\233\276\345\203\217.md" "b/Algorithm/36.\346\227\213\350\275\254\345\233\276\345\203\217.md" new file mode 100644 index 00000000..ddda4601 --- /dev/null +++ "b/Algorithm/36.\346\227\213\350\275\254\345\233\276\345\203\217.md" @@ -0,0 +1,252 @@ +36.旋转图像 +=== + + +### 题目 + + +给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 + +你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 + +示例1: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_rotate_1.png?raw=true) + +- 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] +- 输出:[[7,4,1],[8,5,2],[9,6,3]] + +示例2: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_rotate_2.png?raw=true) + +- 输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]] +- 输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] + + +提示: + +- n == matrix.length == matrix[i].length +- 1 <= n <= 20 +- -1000 <= matrix[i][j] <= 1000 + + + + +### 思路 + +##### 方法一: 辅助矩阵 + +如下图所示,矩阵顺时针旋转 90º 后,可找到以下规律: + +- 「第 i 行」元素旋转到「第 n−1−i 列」元素; +- 「第 j 列」元素旋转到「第 j 行」元素; + +因此,对于矩阵任意第 i 行、第 j 列元素 matrix[i][j] ,矩阵旋转 90º 后「元素位置旋转公式」为: + +``` +matrix[i][j] → matrix[j][n−1−i] +原索引位置 →旋转后索引位置 +​``` + + + +根据以上「元素旋转公式」,考虑遍历矩阵,将各元素依次写入到旋转后的索引位置。但仍存在问题:在写入一个元素 matrix[i][j]→matrix[j][n−1−i] 后,原矩阵元素 matrix[j][n−1−i] 就会被覆盖(即丢失),而此丢失的元素就无法被写入到旋转后的索引位置了。 + +为解决此问题,考虑借助一个「辅助矩阵」暂存原矩阵,通过遍历辅助矩阵所有元素,将各元素填入「原矩阵」旋转后的新索引位置即可。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_rotate_3.png?raw=true) + +```python3 +class Solution: + def rotate(self, matrix: List[List[int]]) -> None: + n = len(matrix) + # 深拷贝 matrix -> tmp + tmp = copy.deepcopy(matrix) + # 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素 + for i in range(n): + for j in range(n): + matrix[j][n - 1 - i] = tmp[i][j] +``` + +复杂度分析: +​ +- 遍历矩阵所有元素的时间复杂度为O(N²) +- 由于借助了一个辅助矩阵,空间复杂度为O(N²)。 + + +##### 方法二:原地修改 + + +考虑不借助辅助矩阵,通过在原矩阵中直接「原地修改」,实现空间复杂度 O(1) 的解法。 + +以位于矩阵四个角点的元素为例,设矩阵左上角元素A、右上角元素B、右下角元素C、左下角元素D。 + +矩阵旋转90º后,相当于依次先后执行D→A,C→D,B→C,A→B修改元素,即如下「首尾相接」的元素旋转操作: +``` +A←D←C←B←A +``` + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_rotate_4.png?raw=true) + + +如上图所示: + +- 由于第1步D→A已经将A覆盖(导致A丢失),此丢失导致最后第4步A→B无法赋值。为解决此问题,考虑借助一个「辅助变量 tmp 」预先存储 A ,此时的旋转操作变为: + +``` +暂存 tmp=A +A←D←C←B←tmp +``` + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_rotate_5.png?raw=true) + + + +如上图所示,一轮可以完成矩阵 4 个元素的旋转。因而,只要分别以矩阵左上角 1/4 的各元素为起始点执行以上旋转操作,即可完整实现矩阵旋转。 + +- 当矩阵大小 n 为偶数时,取前½ * n行、前 ½ * n 列的元素为起始点; + +- 当矩阵大小 n 为奇数时,取前 ½ * n 行、前 ½ * (n + 1)列的元素为起始点。 + +令 matrix[i][j]=A ,根据文章开头的元素旋转公式,可推导得适用于任意起始点的元素旋转操作: + +``` +暂存tmp=matrix[i][j] +matrix[i][j]←matrix[n−1−j][i]←matrix[n−1−i][n−1−j]←matrix[j][n−1−i]←tmp +``` + + + +```python3 +class Solution: + def rotate(self, matrix: List[List[int]]) -> None: + n = len(matrix) + for i in range(n // 2): + for j in range((n + 1) // 2): + tmp = matrix[i][j] + matrix[i][j] = matrix[n - 1 - j][i] + matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j] + matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i] + matrix[j][n - 1 - i] = tmp +``` + + +```java +class Solution { + public void rotate(int[][] matrix) { + // 设矩阵行列数为 n + int n = matrix.length; + // 起始点范围为 0 <= i < n / 2 , 0 <= j < (n + 1) / 2 + // 其中 '/' 为整数除法 + for (int i = 0; i < n / 2; i++) { + for (int j = 0; j < (n + 1) / 2; j++) { + // 暂存 A 至 tmp + int tmp = matrix[i][j]; + // 元素旋转操作 A <- D <- C <- B <- tmp + matrix[i][j] = matrix[n - 1 - j][i]; + matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]; + matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i]; + matrix[j][n - 1 - i] = tmp; + } + } + } +} +``` + +复杂度分析: + +- 时间复杂度 O(N²): 其中 N 为输入矩阵的行(列)数。需要将矩阵中每个元素旋转到新的位置,即对矩阵所有元素操作一次,使用O(N²)时间。 +- 空间复杂度 O(1): 临时变量 tmp 使用常数大小的额外空间。值得注意,当循环中进入下轮迭代,上轮迭代初始化的 tmp 占用的内存就会被自动释放,因此无累计使用空间。 + + +##### 方法三: 先转置再左右翻转 + +``` +很明显可以发现,对二位矩阵顺时针旋转90度,等价于先对二位矩阵进行转置操作,再对转置后的数组进行左右翻转。 +如: +原矩阵: +1 2 3 +4 5 6 +7 8 9 + +转置后: +1 4 7 +2 5 8 +3 6 9 + +左右翻转: +7 4 1 +8 5 2 +9 6 3 + +原矩阵: +1 2 3 +4 5 6 +7 8 9 + +顺时针旋转90度: +7 4 1 +8 5 2 +9 6 3 + +可以发现,顺时针旋转90度后的矩阵,于先转置再左右翻转的矩阵相同 +``` + +1、矩阵转置: + +矩阵的转置是将矩阵的行和列互换,即将元素 matrix[i][j] 和 matrix[j][i] 交换。 +性质: +- 转置操作不会改变主对角线上的元素(即 i=j 时的元素)。 +- 转置完成后,矩阵变为关于主对角线对称。 + +本质:将矩阵对角线两边的元素互换 + + +2、逐行翻转: + +- 对转置后的矩阵,每一行进行翻转,即从左到右交换元素,得到最终旋转后的结果。 + +性质: +转置只是调整了行列的关系,但要实现顺时针旋转 90 度,还需要将转置后的行元素顺序反转。 + + +全部代码: + +```python3 +class Solution: + def transpose(self, matrix:List[List[int]]) -> None: + length = len(matrix) + for i in range(0, length): + for j in range(0, i): + temp = matrix[i][j] + matrix[i][j] = matrix[j][i] + matrix[j][i] = temp + + def overturn(self, matrix:List[List[int]]) -> None: + length = len(matrix) + for i in range(0, length//2): + for j in range(0, length): + temp = matrix[j][i] + matrix[j][i] = matrix[j][length - 1 - i] + matrix[j][length - 1 - i] = temp + + def rotate(self, matrix: List[List[int]]) -> None: + """ + Do not return anything, modify matrix in-place instead. + """ + self.transpose(matrix) + self.overturn(matrix) +``` + + +- 时间复杂度: O(N²) +- 空间复杂度: O(1) + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/37.\347\237\251\351\230\265\347\275\256\351\233\266.md" "b/Algorithm/37.\347\237\251\351\230\265\347\275\256\351\233\266.md" new file mode 100644 index 00000000..774d552c --- /dev/null +++ "b/Algorithm/37.\347\237\251\351\230\265\347\275\256\351\233\266.md" @@ -0,0 +1,165 @@ +37.矩阵置零 +=== + + + +### 题目 + +给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 + + +示例: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_setzeros_1.png?raw=true) + +- 输入: matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]] +- 输出: [[0,0,0,0],[0,4,5,0],[0,3,1,0]] + +提示: + +- m == matrix.length +- n == matrix[0].length +- 1 <= m, n <= 200 +- -231 <= matrix[i][j] <= 231 - 1 + + +进阶: + +- 一个直观的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。 +- 一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。 + +你能想出一个仅使用常量空间的解决方案吗? + + +### 思路 + + +思路一: 用 O(m+n)额外空间 + +- 两遍扫matrix,第一遍用集合记录哪些行,哪些列有0;第二遍置0 + +```java +class Solution { + public void setZeroes(int[][] matrix) { + Set row_zero = new HashSet<>(); + Set col_zero = new HashSet<>(); + int row = matrix.length; + int col = matrix[0].length; + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + if (matrix[i][j] == 0) { + row_zero.add(i); + col_zero.add(j); + } + } + } + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + if (row_zero.contains(i) || col_zero.contains(j)) matrix[i][j] = 0; + } + } + } +} +``` + +思路二: 用O(1)空间 + +关键思想: 用matrix第一行和第一列记录该行该列是否有0,作为标志位 + +但是对于第一行,和第一列要设置一个标志位,为了防止自己这一行(一列)也有0的情况.注释写在代码里,直接看代码很好理解! + +思路:第一次循环 +1、0行数组的每个元素临时标识该元素所在列是否有0,0列数组的每个元素临时标识该元素所在行是否有0。 +2、判断每行的第0列是否为0,有则赋值额外的标识字段,该字段的作用是用于后续对称遍历的时候,赋值所有行的0列是否应该赋值0使用 + +``` +比如原始数组为 +[ + [2,1,2,3], + [2,1,2,3], + [3,0,5,2], + [1,3,0,5] +] + + +// 如果当前遍历位置为0,则该行0列设置标识,以便后续使用 +// 如果当前遍历位置为0,则0行该列设置标识,以便后续使用 +[ + [2,0,0,3], + [2,1,2,3], + [0,0,5,2], + [0,3,0,5] +] +// 第二次循环-从右下角遍历处理:行从最后一行遍历(直到0行),列从最后一列遍历(直到第一列) +// 使用0行数组、0列数组判断当前遍历位置是否应该置0 +// 注意需要额外使用标识字段判断该行0列是否应该置0 +// 处理完成之后 +[ + [2,0,0,3], + [2,0,0,3], + [0,0,0,0], + [0,0,0,0] +] + +``` + + +```java + +class Solution { + public void setZeroes(int[][] matrix) { + int row = matrix.length; + int col = matrix[0].length; + boolean row0_flag = false; + boolean col0_flag = false; + // 第一行是否有零 + for (int j = 0; j < col; j++) { + if (matrix[0][j] == 0) { + row0_flag = true; + break; + } + } + // 第一列是否有零 + for (int i = 0; i < row; i++) { + if (matrix[i][0] == 0) { + col0_flag = true; + break; + } + } + // 把第一行第一列作为标志位 + for (int i = 1; i < row; i++) { + for (int j = 1; j < col; j++) { + if (matrix[i][j] == 0) { + matrix[i][0] = matrix[0][j] = 0; + } + } + } + // 置0 + for (int i = 1; i < row; i++) { + for (int j = 1; j < col; j++) { + if (matrix[i][0] == 0 || matrix[0][j] == 0) { + matrix[i][j] = 0; + } + } + } + if (row0_flag) { + for (int j = 0; j < col; j++) { + matrix[0][j] = 0; + } + } + if (col0_flag) { + for (int i = 0; i < row; i++) { + matrix[i][0] = 0; + } + } + } +} +``` + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/38.\347\224\237\345\221\275\346\270\270\346\210\217.md" "b/Algorithm/38.\347\224\237\345\221\275\346\270\270\346\210\217.md" new file mode 100644 index 00000000..f2e79bb5 --- /dev/null +++ "b/Algorithm/38.\347\224\237\345\221\275\346\270\270\346\210\217.md" @@ -0,0 +1,137 @@ +38.生命游戏 +=== + + +### 题目 + + +生命游戏,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。 + +给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 即为 活细胞 (live),或 0 即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律: + +- 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡; +- 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活; +- 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡; +- 如果死细胞周围正好有三个活细胞,则该位置死细胞复活; + +下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是 同时 发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。 + +给定当前 board 的状态,更新 board 到下一个状态。 + +注意 你不需要返回任何东西。 + + +示例1: + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_gameOfLife_1.jpg?raw=true) + +- 输入:board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]] +- 输出:[[0,0,0],[1,0,1],[0,1,1],[0,1,0]] + + +示例2: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_gameOfLife_1.jpg?raw=true) + +- 输入:board = [[1,1],[1,0]] +- 输出:[[1,1],[1,1]] + + +提示: + +- m == board.length +- n == board[i].length +- 1 <= m, n <= 25 +- board[i][j] 为 0 或 1 + + +进阶: + +你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。 +本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题? + + +### 思路 + +简化规则: +1. 原来是活的,周围有2-3个活的,成为活的 +2. 原来是死的,周围有3个活的,成为活的 +3. 其他都是死了 + +这道题主要就是模拟,遍历每一个格子,然后统计其周围八个格子的活细胞个数,来看这个格子的状态是否改变。 +但难点在于:如果这个格子的状态改变,不能直接改变。这样会影响后面格子的统计。即题目中说的:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。 + +因此我们需要使用特殊值去标记发生改变的格子,从而根据特殊值可以知道这个格子原状态是什么,要更新的状态是什么。 + + +- 可以使用 2表示活细胞变成死细胞,3表示死细胞变成活细胞。【这样的好处是最终是死细胞的都是偶数,活细胞的都是奇数,模2即结果;】 + +也可以用下面的方式: + +- 由于每个位置的细胞的状态是取决于当前四周其他状态的,而且每个细胞的状态是同时变化的,所以不能一个一个地更新,只能在一个新的数组里创建新的状态。 + +- 当然上面所说的也不是绝对的,因为这道题目的输入是int[][],矩阵是 int 类型的,有 32 位,而状态只有 0,1 两种,只需一位且只有最低位用上了,我们用其他位存储下一个状态即可,相当于用原矩阵当一个复制的矩阵。 + +- 所以可以,原有的最低位存储的是当前状态,那倒数第二低位存储下一个状态就行了。 + + +```java +class Solution { + public void gameOfLife(int[][] board) { + int m = board.length; // 行数 + int n = board[0].length; // 列数 + int count = 0; // 统计每个格子周围八个位置的活细胞数 + for(int i = 0; i < m; i++){ + for(int j = 0; j < n; j++){ + count = 0; // 每个格子计数重置为0 + for(int x = -1; x <= 1; x++){ // -1 0 1 分别代表当前位置左边的格子、当前格子、当前位置右边的格子 + for(int y = -1; y <= 1; y++){ // -1 0 1 分别代表当前位置下边的格子、当前格子、当前位置上边的格子 + // 枚举周围八个位置,其中去掉本身(x = y = 0)和越界(靠近边缘)的情况 + if((x == 0 && y == 0) || i + x < 0 || i + x >= m || j + y < 0 || j + y >= n)continue; + // 如果周围格子是活细胞(1)或者是活细胞变死细胞(2)的,都算一个活细胞 + if(board[i + x][j + y] == 1 || board[i + x][j + y] == 2) { + count++; + } + } + } + + if(board[i][j] == 1 && (count < 2 || count > 3)) { + board[i][j] = 2; // 格子本身是活细胞,周围满足变成死细胞的条件,标记为2 + } + + if(board[i][j] == 0 && count == 3) { + board[i][j] = 3; // 格子本身是死细胞,周围满足复活条件,标记为3 + } + } + } + + for(int i = 0; i < m; i++){ + for(int j = 0; j < n; j++){ + // 死细胞为0,活细胞变成死细胞为2,都为偶数,模2为0,刚好是死细胞 + // 活细胞为1,死细胞变成活细胞为3,都为奇数,模2为1,刚好是活细胞 + board[i][j] %= 2; + } + } + } +} +``` + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/39.\350\265\216\351\207\221\344\277\241.md" "b/Algorithm/39.\350\265\216\351\207\221\344\277\241.md" new file mode 100644 index 00000000..63cff051 --- /dev/null +++ "b/Algorithm/39.\350\265\216\351\207\221\344\277\241.md" @@ -0,0 +1,97 @@ +39.赎金信 +=== + + +### 题目 + + +给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。 + +如果可以,返回 true ;否则返回 false 。 + +magazine 中的每个字符只能在 ransomNote 中使用一次。 + + + +示例 1: + +- 输入:ransomNote = "a", magazine = "b" +- 输出:false + +示例 2: + +- 输入:ransomNote = "aa", magazine = "ab" +- 输出:false + +示例 3: + +- 输入:ransomNote = "aa", magazine = "aab" +- 输出:true + + +提示: + +- 1 <= ransomNote.length, magazine.length <= 105 +- ransomNote 和 magazine 由小写英文字母组成 + + +### 思路 + +用hash表还是一个大小为26的数组,记录26个单词每一个出现的次数 + +```java +class Solution { + public boolean canConstruct(String ransomNote, String magazine) { + //记录杂志字符串出现的次数 + int[] arr = new int[26]; + int temp; + for (int i = 0; i < magazine.length(); i++) { + temp = magazine.charAt(i) - 'a'; + arr[temp]++; + } + for (int i = 0; i < ransomNote.length(); i++) { + temp = ransomNote.charAt(i) - 'a'; + //对于金信中的每一个字符都在数组中查找 + //找到相应位减一,否则找不到返回false + if (arr[temp] > 0) { + arr[temp]--; + } else { + return false; + } + } + return true; + } +} +``` + + +```python +class Solution: + def canConstruct(self, ransomNote: str, magazine: str) -> bool: + if len(ransomNote) > len(magazine): + return False + + arr = [0] * 26 + for i in magazine: + arr[ord(i) -ord('a')] += 1 + + for i in ransomNote: + index = ord(i) - ord('a') + arr[index] -= 1 + if arr[index] < 0: + return False + + return True +``` + + +复杂度: + +- 时间复杂度: O(N) +- 空间复杂度: O(1) + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/4.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271II.md" "b/Algorithm/4.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271II.md" new file mode 100644 index 00000000..549b9a20 --- /dev/null +++ "b/Algorithm/4.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271II.md" @@ -0,0 +1,80 @@ +4.删除有序数组中的重复项II +=== + + +### 题目 + +给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。 + +不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 + + + +示例 1: + +输入:nums = [1,1,1,2,2,3] +输出:5, nums = [1,1,2,2,3] +解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 不需要考虑数组中超出新长度后面的元素。 +示例 2: + +输入:nums = [0,0,1,1,1,1,2,3,3] +输出:7, nums = [0,0,1,1,2,3,3] +解释:函数应返回新长度 length = 7, 并且原数组的前七个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素。 + + + +### 思路 + +双指针 + +- 因为给定数组是有序的,所以相同元素必然连续。 +- 我们可以使用双指针解决本题,遍历数组检查每一个元素是否应该被保留,如果应该被保留,就将其移动到指定位置。具体地,我们定义两个指针 slow 和 fast 分别为慢指针和快指针,其中慢指针表示处理出的数组的长度,快指针表示已经检查过的数组的长度,即 nums[fast] 表示待检查的第一个元素,nums[slow−1] 为上一个应该被保留的元素所移动到的指定位置。 + +- 因为本题要求相同元素最多出现两次而非一次,所以我们需要检查上上个应该被保留的元素 nums[slow−2] 是否和当前待检查元素 nums[fast] 相同。 + +- 当且仅当 nums[slow−2]=nums[fast] 时,当前待检查元素 nums[fast] 不应该被保留(因为此时必然有 nums[slow−2]=nums[slow−1]=nums[fast])。最后,slow 即为处理好的数组的长度。 + +特别地,数组的前两个数必然可以被保留,因此对于长度不超过 2 的数组,我们无需进行任何处理,对于长度超过 2 的数组,我们直接将双指针的初始值设为 2 即可。 + + +```python + +class Solution: + def removeDuplicates(self, nums: List[int]) -> int: + if len(nums) < 2: + return len(nums) + k = 2 + for i in range(2, len(nums)): + if nums[i] == nums[k - 2] : + continue + else: + nums[k] = nums[i] + k += 1 + return k +``` + + +```kotlin +class Solution { + fun removeDuplicates(nums: IntArray): Int { + if (nums.size < 2) { + return nums.size + } + var k = 2 + for (i in 2..nums.size - 1) { + if (nums[i] != nums[k - 2]) { + nums[k] = nums[i] + k ++ + } + } + return k + } +} +``` + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/40.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" "b/Algorithm/40.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" new file mode 100644 index 00000000..aa801292 --- /dev/null +++ "b/Algorithm/40.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,98 @@ +40.同构字符串 +=== + + +### 题目 + +给定两个字符串 s 和 t ,判断它们是否是同构的。 + +如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。 + +每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。 + + + +示例 1: + +- 输入:s = "egg", t = "add" +- 输出:true + +示例 2: + +- 输入:s = "foo", t = "bar" +- 输出:false + +示例 3: + +- 输入:s = "paper", t = "title" +- 输出:true + + +提示: + +- 1 <= s.length <= 5 * 104 +- t.length == s.length +- s 和 t 由任意有效的 ASCII 字符组成 + +### 思路 + + + + +- “每个出现的字符都应当映射到另一个字符”。代表字符集合 s , t 之间是「满射」。 +- “相同字符只能映射到同一个字符上,不同字符不能映射到同一个字符上”。代表字符集合 s , t 之间是「单射」。 +- 因此, s 和 t 之间是「双射」,满足一一对应。考虑遍历字符串,使用哈希表 s2t , t2s 分别记录 s→t , t→s 的映射,当发现任意「一对多」的关系时返回 false 即可。 + + +```java + +class Solution { + public boolean isIsomorphic(String s, String t) { + Map s2t = new HashMap<>(), t2s = new HashMap<>(); + for (int i = 0; i < s.length(); i++) { + char a = s.charAt(i), b = t.charAt(i); + // 对于已有映射 a -> s2t[a],若和当前字符映射 a -> b 不匹配, + // 说明有一对多的映射关系,则返回 false ; + // 对于映射 b -> a 也同理 + if (s2t.containsKey(a) && s2t.get(a) != b || + t2s.containsKey(b) && t2s.get(b) != a) { + return false; + } + s2t.put(a, b); + t2s.put(b, a); + } + return true; + } +} +``` + + +```python +class Solution: + def isIsomorphic(self, s: str, t: str) -> bool: + s2t = {} + t2s = {} + + for i in range(len(s)): + a = s[i] + b = t[i] + + if (a in s2t and s2t.get(a) != b) or (b in t2s and t2s.get(b) != a): + return False + s2t[a] = b + t2s[b] = a + + return True +``` + +复杂度分析: + +- 时间复杂度 O(N) : 其中 N 为字符串 s , t 的长度。遍历字符串 s , t 使用线性时间,hashmap 查询操作使用 O(1) 时间。 +- 空间复杂度 O(1) : 题目说明 s 和 t 由任意有效的 ASCII 字符组成。由于 ASCII 字符共 128 个,因此 hashmap s2t , t2s 使用 O(128)=O(1) 空间。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/41.\345\215\225\350\257\215\350\247\204\345\276\213.md" "b/Algorithm/41.\345\215\225\350\257\215\350\247\204\345\276\213.md" new file mode 100644 index 00000000..33cae539 --- /dev/null +++ "b/Algorithm/41.\345\215\225\350\257\215\350\247\204\345\276\213.md" @@ -0,0 +1,114 @@ +41.单词规律 +=== + + +### 题目 + +给定一种规律pattern和一个字符串s,判断s是否遵循相同的规律。 + +这里的遵循指完全匹配,例如,pattern里的每个字母和字符串s中的每个非空单词之间存在着双向连接的对应规律。 + + + +示例1: + +- 输入: pattern = "abba", s = "dog cat cat dog" +- 输出: true + +示例 2: + +- 输入:pattern = "abba", s = "dog cat cat fish" +- 输出: false + +示例 3: + +- 输入: pattern = "aaaa", s = "dog cat cat dog" +- 输出: false + + +提示: + +- 1 <= pattern.length <= 300 +- pattern 只包含小写英文字母 +- 1 <= s.length <= 3000 +- s 只包含小写英文字母和 ' ' +- s 不包含 任何前导或尾随对空格 +- s 中每个单词都被 单个空格 分隔 + + +### 思路 + +和上一题的思路完全一样,由字符之间的一一映射升级成了字符与字符串之间的一一映射。 +首先本质是一样的,要实现一一映射,就要用到两个哈希表分别记录字符到字符串的映射和字符串到字符的映射。 +其次,我们要对s中的单词进行提取,比较单词数量和pattern中的数量是否一致,如果数量上不一致,二者一定不匹配; + + + +```java + +class Solution { + public boolean wordPattern(String pattern, String s) { + Map p2s = new HashMap<>(); // pattern中的字符到s中的字符子串的映射表 + Map s2p = new HashMap<>(); // s中的字符字串到pattern中的字符的映射表 + String[] words = s.split(" "); // 根据空格,提取s中的单词 + int n = pattern.length(); + int m = words.length; + if(n != m){ + return false; // 字符数和单词数不一致,一定不匹配 + } + char ch; + String word; + for(int i = 0; i < n; i++){ + ch = pattern.charAt(i); + word = words[i]; + if((p2s.containsKey(ch) && !p2s.get(ch).equals(word)) || (s2p.containsKey(word) && s2p.get(word) != ch)){ + // 字符与单词没有一一映射:即字符记录的映射不是当前单词或单词记录的映射不是当前字符 + return false; + } + // 更新映射,已存在的映射更新后仍然是不变的;不存在的映射将被加入 + p2s.put(ch, word); + s2p.put(word, ch); + } + return true; + } +} +``` + + +```python + +class Solution: + def wordPattern(self, pattern: str, s: str) -> bool: + words = s.split(' ') + length = len(words) + if length != len(pattern): + return False + + p2s = {} + s2p = {} + + for i in range(length): + word = words[i] + p = pattern[i] + + if (word in s2p and s2p[word] != p) or (p in p2s and p2s[p] != word): + return False + + p2s[p] = word + s2p[word] = p + + return True +``` + +复杂度分析: + +- 时间复杂度:O(n+m),其中 n 为 pattern 的长度,m 为 str 的长度。插入和查询哈希表的均摊时间复杂度均为 O(n+m)。每一个字符至多只被遍历一次。 + +- 空间复杂度:O(n+m),其中 n 为 pattern 的长度,m 为 str 的长度。最坏情况下,我们需要存储 pattern 中的每一个字符和 str 中的每一个字符串。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/42.\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/Algorithm/42.\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..b72a4c92 --- /dev/null +++ "b/Algorithm/42.\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,194 @@ +42.有效的字母异位词 +=== + + +### 题目 + +给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的 字母异位词。 + + + +示例 1: + +- 输入: s = "anagram", t = "nagaram" +- 输出: true + +示例 2: + +- 输入: s = "rat", t = "car" +- 输出: false + + +提示: + +- 1 <= s.length, t.length <= 5 * 104 +- s 和 t 仅包含小写字母 + + +进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况? + + +### 思路 + +设两字符串 s +1 +​ + , s +2 +​ + ,则两者互为重排的「充要条件」为:两字符串 s +1 +​ + , s +2 +​ + 包含的字符是一致的,即 s +1 +​ + , s +2 +​ + 所有对应字符数量都相同,仅排列顺序不同。 + +根据以上分析,可借助「哈希表」分别统计 s +1 +​ + , s +2 +​ + 中各字符数量 key: 字符, value: 数量 ,分为以下情况: + +若 s +1 +​ + , s +2 +​ + 字符串长度不相等,则「不互为重排」; +若 s +1 +​ + , s +2 +​ + 某对应字符数量不同,则「不互为重排」; +否则,若 s +1 +​ + , s +2 +​ + 所有对应字符数量都相同,则「互为重排」; +具体上看,我们可以统计 s +1 +​ + 各字符时执行 +1 ,统计 s +2 +​ + 各字符时 −1 。若两字符串互为重排,则最终哈希表中所有字符统计数值都应为 0 。 + +作者:Krahets +链接:https://leetcode.cn/problems/valid-anagram/solutions/2362065/242-you-xiao-de-zi-mu-yi-wei-ci-ha-xi-bi-cch7/ +来源:力扣(LeetCode) +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + +```java +class Solution { + public boolean isAnagram(String s, String t) { + int len1 = s.length(), len2 = t.length(); + if (len1 != len2) + return false; + HashMap dic = new HashMap<>(); + for (int i = 0; i < len1; i++) { + dic.put(s.charAt(i) , dic.getOrDefault(s.charAt(i), 0) + 1); + } + for (int i = 0; i < len2; i++) { + dic.put(t.charAt(i) , dic.getOrDefault(t.charAt(i), 0) - 1); + } + for (int val : dic.values()) { + if (val != 0) + return false; + } + return true; + } +} +``` + + +```python +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + if len(s) != len(t): + return False + dic = defaultdict(int) + for c in s: + dic[c] += 1 + for c in t: + dic[c] -= 1 + for val in dic.values(): + if val != 0: + return False + return True +``` + + +复杂度分析: + +- 时间复杂度 O(M+N) : 其 M , N 分别为字符串 s1, s2长度。当 s1, s2无相同字符时,三轮循环的总迭代次数最多为 2M+2N ,使用 O(M+N) 线性时间。 + +- 空间复杂度 O(1) : 由于字符种类是有限的(常量),一般 ASCII 码共包含 128 个字符,因此可假设使用 O(1) 大小的额外空间。 + + +对于进阶问题,Unicode 是为了解决传统字符编码的局限性而产生的方案,它为每个语言中的字符规定了一个唯一的二进制编码。而 Unicode 中可能存在一个字符对应多个字节的问题,为了让计算机知道多少字节表示一个字符,面向传输的编码方式的 UTF−8 和 UTF−16 也随之诞生逐渐广泛使用,具体相关的知识读者可以继续查阅相关资料拓展视野,这里不再展开。 + +回到本题,进阶问题的核心点在于「字符是离散未知的」,因此我们用哈希表维护对应字符的频次即可。同时读者需要注意 Unicode 一个字符可能对应多个字节的问题,不同语言对于字符串读取处理的方式是不同的。 + + + +```java + +class Solution { + public boolean isAnagram(String s, String t) { + if (s.length() != t.length()) { + return false; + } + Map table = new HashMap(); + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + table.put(ch, table.getOrDefault(ch, 0) + 1); + } + for (int i = 0; i < t.length(); i++) { + char ch = t.charAt(i); + table.put(ch, table.getOrDefault(ch, 0) - 1); + if (table.get(ch) < 0) { + return false; + } + } + return true; + } +} + +作者:力扣官方题解 +链接:https://leetcode.cn/problems/valid-anagram/solutions/493231/you-xiao-de-zi-mu-yi-wei-ci-by-leetcode-solution/ +来源:力扣(LeetCode) +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 +``` + +复杂度分析 + +时间复杂度:O(n),其中 n 为 s 的长度。 + +空间复杂度:O(S),其中 S 为字符集大小,此处 S=26。 + + + +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + return Counter(s) == Counter(t) + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/43.\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215\345\210\206\347\273\204.md" "b/Algorithm/43.\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215\345\210\206\347\273\204.md" new file mode 100644 index 00000000..69eee016 --- /dev/null +++ "b/Algorithm/43.\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215\345\210\206\347\273\204.md" @@ -0,0 +1,118 @@ +43.字母异位词分组 +=== + + +### 题目 + +给你一个字符串数组,请你将 字母异位词(字母异位词是通过重新排列不同单词或短语的字母而形成的单词或短语,并使用所有原字母一次) 组合在一起。可以按任意顺序返回结果列表。 + + + +示例 1: + +输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"] + +输出: [["bat"],["nat","tan"],["ate","eat","tea"]] + +解释: + +- 在 strs 中没有字符串可以通过重新排列来形成 "bat"。 +- 字符串 "nat" 和 "tan" 是字母异位词,因为它们可以重新排列以形成彼此。 +- 字符串 "ate" ,"eat" 和 "tea" 是字母异位词,因为它们可以重新排列以形成彼此。 + +示例 2: + +- 输入: strs = [""] + +- 输出: [[""]] + +示例 3: + +- 输入: strs = ["a"] + +- 输出: [["a"]] + + + +提示: + +- 1 <= strs.length <= 104 +- 0 <= strs[i].length <= 100 +- strs[i] 仅包含小写字母 + +### 思路 + +##### 方法一: 排序 + +由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。 + +```java +class Solution { + public List> groupAnagrams(String[] strs) { + Map> map = new HashMap>(); + for (String str : strs) { + char[] array = str.toCharArray(); + Arrays.sort(array); + String key = new String(array); + List list = map.getOrDefault(key, new ArrayList()); + list.add(str); + map.put(key, list); + } + return new ArrayList>(map.values()); + } +} +``` + +复杂度分析 + +- 时间复杂度:O(nklogk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要遍历 n 个字符串,对于每个字符串,需要 O(klogk) 的时间进行排序以及 O(1) 的时间更新哈希表,因此总时间复杂度是 O(nklogk)。 + +- 空间复杂度:O(nk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要用哈希表存储全部字符串。 + +##### 方法二: 计数 + +- 由于互为字母异位词的两个字符串包含的字母相同,因此两个字符串中的相同字母出现的次数一定是相同的 +- 所以可以将每个字母出现的次数使用字符串表示,作为哈希表的键 + +```java + +class Solution { + public List> groupAnagrams(String[] strs) { + Map> map = new HashMap>(); + for (String str : strs) { + int[] counts = new int[26]; + int length = str.length(); + for (int i = 0; i < length; i++) { + counts[str.charAt(i) - 'a']++; + } + // 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串,作为哈希表的键 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 26; i++) { + if (counts[i] != 0) { + sb.append((char) ('a' + i)); + sb.append(counts[i]); + } + } + String key = sb.toString(); + List list = map.getOrDefault(key, new ArrayList()); + list.add(str); + map.put(key, list); + } + return new ArrayList>(map.values()); + } +} +``` + + +复杂度分析: + +- 时间复杂度:O(nk)),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度. + +- 空间复杂度:O(nk)),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的最大长度. + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/44.\344\270\244\346\225\260\344\271\213\345\222\214.md" "b/Algorithm/44.\344\270\244\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 00000000..9e1fba5d --- /dev/null +++ "b/Algorithm/44.\344\270\244\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,121 @@ +44.两数之和 +=== + + +### 题目 + +给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 + +你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。 + +你可以按任意顺序返回答案。 + + + +示例 1: + +- 输入:nums = [2,7,11,15], target = 9 +- 输出:[0,1] +- 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 + +示例 2: + +- 输入:nums = [3,2,4], target = 6 +- 输出:[1,2] + +示例 3: + +- 输入:nums = [3,3], target = 6 +- 输出:[0,1] + + +提示: + +- 2 <= nums.length <= 104 +- -109 <= nums[i] <= 109 +- -109 <= target <= 109 +- 只会存在一个有效答案 + + +进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗? + + + +### 思路 + +这道题本身如果通过暴力遍历的话也是很容易解决的,时间复杂度在 O(n²) + +##### 方法一:暴力枚举 + + +最容易想到的方法是枚举数组中的每一个数 x,寻找数组中是否存在 target - x。 + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + int n = nums.length; + for (int i = 0; i < n; ++i) { + for (int j = i + 1; j < n; ++j) { + if (nums[i] + nums[j] == target) { + return new int[]{i, j}; + } + } + } + return new int[0]; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(N²),其中 N 是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。 + +- 空间复杂度:O(1)。 + + +##### 方法二: 哈希表 + + +- 注意到方法一的时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。 +- 因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。 +- 使用哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N) 降低到 O(1)。 +- 这样我们创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。 + + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + Map map = new HashMap<>(); + for(int i = 0; i< nums.length; i++) { + if(map.containsKey(target - nums[i])) { + return new int[] {map.get(target-nums[i]),i}; + } + map.put(nums[i], i); + } + throw new IllegalArgumentException("No two sum solution"); + } +} +``` + +```python +class Solution: + def twoSum(self, nums: List[int], target: int) -> List[int]: + hashTable = dict() + for i, num in enumerate(nums): + if target - num in hashTable: + return [hashTable[target-num], i] + hashTable[nums[i]] = i + return [] +``` + +复杂度分析: + +- 时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。 + +- 空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/45.\345\277\253\344\271\220\346\225\260.md" "b/Algorithm/45.\345\277\253\344\271\220\346\225\260.md" new file mode 100644 index 00000000..bd557384 --- /dev/null +++ "b/Algorithm/45.\345\277\253\344\271\220\346\225\260.md" @@ -0,0 +1,198 @@ +45.快乐数 +=== + + +### 题目 + +编写一个算法来判断一个数 n 是不是快乐数。 + +「快乐数」 定义为: + +- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。 +- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。 +- 如果这个过程 结果为 1,那么这个数就是快乐数。 +- 如果 n 是 快乐数 就返回 true ;不是,则返回 false 。 + + + +示例 1: + +- 输入:n = 19 +- 输出:true + +- 解释: + - 1² + 9² = 82 + - 8² + 2² = 68 + - 6² + 8² = 100 + - 1² + 0² + 0² = 1 + +示例 2: + +- 输入:n = 2 +- 输出:false + + +提示: + +- 1 <= n <= 231 - 1 + + +### 思路 + +- 快乐数的定义是基于一个计算过程,即对一个正整数,不断将其替换为它各个位上数字的平方和,如果最终这个过程能够收敛到1,则这个数被称为快乐数。 + +- 相反,如果在这个过程中形成了一个不包含1的循环,则该数不是快乐数。 + +- 对于非快乐数,它们的平方和序列会进入一个固定的循环,例如4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_happy_1.png?raw=true) + +根据我们的探索,我们猜测会有以下三种可能: + +- 最终会得到 1。 +- 最终会进入循环。 +- 值会越来越大,最后接近无穷大(不会发生,因为假设9999999999999,那他每个位置的数值平方相加后的值是1053,再往后循环只会越来越小)。 + + +第三个情况比较难以检测和处理。我们怎么知道它会继续变大,而不是最终得到 1 呢?我们可以仔细想一想,每一位数的最大数字的下一位数是多少。 + +| 位数 | 最大值 | Next | +|--------|----------------|------| +| 1 | 9 | 81 | +| 2 | 99 | 162 | +| 3 | 999 | 243 | +| 4 | 9999 | 324 | +| 13 | 9999999999999 | 1053 | + + +- 对于 3 位数的数字,它不可能大于 243。这意味着它要么被困在 243 以下的循环内,要么跌到 1。 + +- 4 位或 4 位以上的数字在每一步都会丢失一位,直到降到 3 位为止。 + +- 所以我们知道,最坏的情况下,算法可能会在 243 以下的所有数字上循环,然后回到它已经到过的一个循环或者回到 1。但它不会无限期地进行下去,所以我们排除第三种选择。 + + + +##### 方法一:哈希表检测 + +所以这道题的解法主要是两部分: + +- 按照题目的要求做数位分离,求平方和。 + +- 使用哈希集合完成。每次生成链中的下一个数字时,我们都会检查它是否已经在哈希集合中。 + + - 如果它不在哈希集合中,我们应该添加它。 + - 如果它在哈希集合中,这意味着我们处于一个循环中,因此应该返回 false。 + + + +```java +class Solution { + private int getNext(int n) { + int totalSum = 0; + while (n > 0) { + int d = n % 10; + n = n / 10; + totalSum += d * d; + } + return totalSum; + } + + public boolean isHappy(int n) { + Set seen = new HashSet<>(); + while (n != 1 && !seen.contains(n)) { + seen.add(n); + n = getNext(n); + } + return n == 1; + } +} +``` + + +- 时间复杂度: O(Logn) +- 空间复杂度: O(Logn) + +##### 方法二:快慢指针法 + +通过反复调用 getNext(n) 得到的链是一个隐式的链表。隐式意味着我们没有实际的链表节点和指针,但数据仍然形成链表结构。起始数字是链表的头 “节点”,链中的所有其他数字都是节点。next 指针是通过调用 getNext(n) 函数获得。 + +意识到我们实际有个链表,那么这个问题就可以转换为检测一个链表是否有环。因此我们在这里可以使用弗洛伊德循环查找算法。这个算法是两个奔跑选手,一个跑的快,一个跑得慢。在龟兔赛跑的寓言中,跑的慢的称为 “乌龟”,跑得快的称为 “兔子”。 + +不管乌龟和兔子在循环中从哪里开始,它们最终都会相遇。这是因为兔子每走一步就向乌龟靠近一个节点(在它们的移动方向上)。 + +```java +class Solution { + + public int getNext(int n) { + int totalSum = 0; + while (n > 0) { + int d = n % 10; + n = n / 10; + totalSum += d * d; + } + return totalSum; + } + + public boolean isHappy(int n) { + int slowRunner = n; + int fastRunner = getNext(n); + while (fastRunner != 1 && slowRunner != fastRunner) { + slowRunner = getNext(slowRunner); + fastRunner = getNext(getNext(fastRunner)); + } + return fastRunner == 1; + } +} + +``` + +复杂度分析: + +- 时间复杂度:O(logn)。该分析建立在对前一种方法的分析的基础上,但是这次我们需要跟踪两个指针而不是一个指针来分析,以及在它们相遇前需要绕着这个循环走多少次。 +如果没有循环,那么快跑者将先到达 1,慢跑者将到达链表中的一半。我们知道最坏的情况下,成本是 O(2⋅logn)=O(logn)。 +一旦两个指针都在循环中,在每个循环中,快跑者将离慢跑者更近一步。一旦快跑者落后慢跑者一步,他们就会在下一步相遇。假设循环中有 k 个数字。如果他们的起点是相隔 k−1 的位置(这是他们可以开始的最远的距离),那么快跑者需要 k−1 步才能到达慢跑者,这对于我们的目的来说也是不变的。因此,主操作仍然在计算起始 n 的下一个值,即 O(logn)。 +- 空间复杂度:O(1),对于这种方法,我们不需要哈希集来检测循环。指针需要常数的额外空间。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/46.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\240 II.md" "b/Algorithm/46.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\240 II.md" new file mode 100644 index 00000000..2051ee00 --- /dev/null +++ "b/Algorithm/46.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\240 II.md" @@ -0,0 +1,112 @@ +46.存在重复元素 II +=== + + +### 题目 + +给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。 + + + +示例 1: + +- 输入:nums = [1,2,3,1], k = 3 +- 输出:true + +示例 2: + +- 输入:nums = [1,0,1,1], k = 1 +- 输出:true + +示例 3: + +- 输入:nums = [1,2,3,1,2,3], k = 2 +- 输出:false + + + + +提示: + +- 1 <= nums.length <= 105 +- -109 <= nums[i] <= 109 +- 0 <= k <= 105 + + +### 思路 + +##### 方法一: 哈希表 + +```python +class Solution: + def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool: + map = defaultdict() + for i, num in enumerate(nums): + if num in map: + m = map[num] + if i - m > k: + return True + map[num] = i + return False +``` + +复杂度分析: + +- 时间复杂度:O(n) +- 空间复杂度:O(n) + + +##### 方法二: 定长滑动窗口 + + +整理题意:是否存在长度不超过的 k+1 窗口,窗口内有相同元素。 + + +我们可以从前往后遍历 nums,同时使用 Set 记录遍历当前滑窗内出现过的元素。 + +假设当前遍历的元素为 nums[i]: + +- 下标小于等于 k(起始滑窗长度还不足 k+1):直接往滑窗加数,即将当前元素加入 Set 中; +- 下标大于 k:将上一滑窗的左端点元素 nums[i−k−1] 移除,判断当前滑窗的右端点元素 nums[i] 是否存在 Set 中,若存在,返回 True,否则将当前元素 nums[i] 加入 Set 中。 +- 重复上述过程,若整个 nums 处理完后仍未找到,返回 False。 + +```python +class Solution: + def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool: + n = len(nums) + s = set() + for i in range(n): + if i > k: + s.remove(nums[i - k - 1]) + if nums[i] in s: + return True + s.add(nums[i]) + return False +``` + +```java +class Solution { + public boolean containsNearbyDuplicate(int[] nums, int k) { + int n = nums.length; + Set set = new HashSet<>(); + for (int i = 0; i < n; i++) { + if (i > k) set.remove(nums[i - k - 1]); + if (set.contains(nums[i])) return true; + set.add(nums[i]); + } + return false; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n) +- 空间复杂度:O(k) + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/47.\346\234\200\351\225\277\350\277\236\347\273\255\345\272\217\345\210\227.md" "b/Algorithm/47.\346\234\200\351\225\277\350\277\236\347\273\255\345\272\217\345\210\227.md" new file mode 100644 index 00000000..0dd4b5f5 --- /dev/null +++ "b/Algorithm/47.\346\234\200\351\225\277\350\277\236\347\273\255\345\272\217\345\210\227.md" @@ -0,0 +1,133 @@ +47.最长连续序列 +=== + + +### 题目 + +给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。 + +请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 + + + +示例 1: + +- 输入:nums = [100,4,200,1,3,2] +- 输出:4 +- 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。 + +示例 2: + +- 输入:nums = [0,3,7,2,5,8,4,6,0,1] +- 输出:9 + +示例 3: + +- 输入:nums = [1,0,1,2] +- 输出:3 + + +提示: + +- 0 <= nums.length <= 105 +- -109 <= nums[i] <= 109 + + +### 思路 + +首先,本题是不能排序的,因为排序的时间复杂度是 O(nlogn),不符合题目 O(n) 的要求。 + + +题解说的比较复杂,不太容易懂,简单来说就是每个数都判断一次这个数是不是连续序列的开头那个数。 + +怎么判断呢,就是用哈希表查找这个数前面一个数是否存在,即num-1在序列中是否存在。 + +为了做到 O(n) 的时间复杂度,需要两个关键优化: + +- 把 nums 中的数都放入一个哈希集合中,这样可以 O(1) 判断数字是否在 nums 中。 +- 存在那这个数肯定不是开头,直接跳过。 +- 因此只需要对每个开头的数进行循环,直到这个序列不再连续,因此复杂度是O(n)。 + +以题解中的序列举例: +[100,4,200,1,3,4,2] +去重后的哈希序列为: +[100,4,200,1,3,2] + +按照上面逻辑进行判断: + +- 元素100是开头,因为没有99,且以100开头的序列长度为1 +- 元素4不是开头,因为有3存在,过 +- 元素200是开头,因为没有199,且以200开头的序列长度为1 +- 元素1是开头,因为没有0,且以1开头的序列长度为4,因为依次累加,2,3,4都存在。 +- 元素3不是开头,因为2存在,过, +- 元素2不是开头,因为1存在,过。 + + +```java +class Solution { + public int longestConsecutive(int[] nums) { + Set num_set = new HashSet(); + for (int num : nums) { + num_set.add(num); + } + + int longestStreak = 0; + + for (int num : num_set) { + if (!num_set.contains(num - 1)) { + int currentNum = num; + int currentStreak = 1; + + while (num_set.contains(currentNum + 1)) { + currentNum += 1; + currentStreak += 1; + } + + longestStreak = Math.max(longestStreak, currentStreak); + } + } + + return longestStreak; + } +} +``` + +```python +class Solution: + def longestConsecutive(self, nums: List[int]) -> int: + longest = 0 + numSet = set(nums) + + for num in numSet: + if num -1 not in numSet: + # 这就是开头,可以开始计数 + currentNum = num + currentStreak = 1 + + while currentNum + 1 in numSet: + currentNum += 1 + currentStreak += 1 + + + longest = max(longest, currentStreak) + + return longest +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 为数组的长度。具体分析已在上面正文中给出。 + +- 空间复杂度:O(n)。哈希表存储数组中所有的数需要 O(n) 的空间。 + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/48.\346\261\207\346\200\273\345\214\272\351\227\264.md" "b/Algorithm/48.\346\261\207\346\200\273\345\214\272\351\227\264.md" new file mode 100644 index 00000000..60983af9 --- /dev/null +++ "b/Algorithm/48.\346\261\207\346\200\273\345\214\272\351\227\264.md" @@ -0,0 +1,134 @@ +48.汇总区间 +=== + + +### 题目 + + +给定一个 无重复元素 的 有序 整数数组 nums 。 + +区间 [a,b] 是从 a 到 b(包含)的所有整数的集合。 + +返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个区间但不属于 nums 的数字 x 。 + +列表中的每个区间范围 [a,b] 应该按如下格式输出: + +- "a->b" ,如果 a != b +- "a" ,如果 a == b + + +示例 1: + +- 输入:nums = [0,1,2,4,5,7] +- 输出:["0->2","4->5","7"] + +解释:区间范围是: +- [0,2] --> "0->2" +- [4,5] --> "4->5" +- [7,7] --> "7" + +示例 2: + +- 输入:nums = [0,2,3,4,6,8,9] +- 输出:["0","2->4","6","8->9"] + +解释:区间范围是: + +- [0,0] --> "0" +- [2,4] --> "2->4" +- [6,6] --> "6" +- [8,9] --> "8->9" + + +提示: + +- 0 <= nums.length <= 20 +- -231 <= nums[i] <= 231 - 1 +- nums 中的所有值都 互不相同 +- nums 按升序排列 + + +### 思路 + + +- 我们从数组的位置 0 出发,向右遍历。 +- 每次遇到相邻元素之间的差值大于 1 时,我们就找到了一个区间。遍历完数组之后,就能得到一系列的区间的列表。 +- 在遍历过程中,维护下标low和high分别记录区间的起点和终点,对于任何区间都有`low≤high`。当得到一个区间时,根据`low`和`high`的值生成区间的字符串表示。 +- 当`low summaryRanges(int[] nums) { + List ret = new ArrayList(); + int i = 0; + int n = nums.length; + while (i < n) { + int low = i; + i++; + while (i < n && nums[i] == nums[i - 1] + 1) { + i++; + } + int high = i - 1; + StringBuffer temp = new StringBuffer(Integer.toString(nums[low])); + if (low < high) { + temp.append("->"); + temp.append(Integer.toString(nums[high])); + } + ret.add(temp.toString()); + } + return ret; + } +} +``` + + +```python +class Solution: + def summaryRanges(self, nums: List[int]) -> List[str]: + def f(i: int, j: int) -> str: + return str(nums[i]) if i == j else f'{nums[i]}->{nums[j]}' + + i = 0 + n = len(nums) + ans = [] + while i < n: + j = i + while j + 1 < n and nums[j + 1] == nums[j] + 1: + j += 1 + ans.append(f(i, j)) + i = j + 1 + return ans +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 为数组的长度。 + +- 空间复杂度:O(1)。除了用于输出的空间外,额外使用的空间为常数。 + + + + + + + + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/49.\345\220\210\345\271\266\345\214\272\351\227\264.md" "b/Algorithm/49.\345\220\210\345\271\266\345\214\272\351\227\264.md" new file mode 100644 index 00000000..8040592e --- /dev/null +++ "b/Algorithm/49.\345\220\210\345\271\266\345\214\272\351\227\264.md" @@ -0,0 +1,82 @@ +49.合并区间 +=== + + +### 题目 + +以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。 + + + +示例 1: + +- 输入:intervals = [[2,6],[1,3],[8,10],[15,18]] +- 输出:[[1,6],[8,10],[15,18]] +- 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. + +示例 2: + +- 输入:intervals = [[1,4],[4,5]] +- 输出:[[1,5]] +- 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。 + + +提示: + +- 1 <= intervals.length <= 104 +- intervals[i].length == 2 +- 0 <= starti <= endi <= 104 + + +### 思路 + +如果我们按照区间的左端点排序,那么在排完序的列表中,可以合并的区间一定是连续的。如下图所示,标记为蓝色、黄色和绿色的区间分别可以合并成一个大区间,它们在排完序的列表中是连续的: + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_merge_qujian_1.png?raw=true) + +- 我们用数组 merged 存储最终的答案。 + +- 首先,我们将列表中的区间按照左端点升序排序。然后我们将第一个区间加入 merged 数组中,并按顺序依次考虑之后的每个区间: + +- 如果当前区间的左端点在数组 merged 中最后一个区间的右端点之后,那么它们不会重合,我们可以直接将这个区间加入数组 merged 的末尾; + +- 否则,它们重合,我们需要用当前区间的右端点更新数组 merged 中最后一个区间的右端点,将其置为二者的较大值。 + +```java +class Solution { + public int[][] merge(int[][] intervals) { + if (intervals.length == 0) { + return new int[0][2]; + } + Arrays.sort(intervals, new Comparator() { + public int compare(int[] interval1, int[] interval2) { + return interval1[0] - interval2[0]; + } + }); + List merged = new ArrayList(); + for (int i = 0; i < intervals.length; ++i) { + int L = intervals[i][0], R = intervals[i][1]; + if (merged.size() == 0 || merged.get(merged.size() - 1)[1] < L) { + merged.add(new int[]{L, R}); + } else { + merged.get(merged.size() - 1)[1] = Math.max(merged.get(merged.size() - 1)[1], R); + } + } + return merged.toArray(new int[merged.size()][]); + } +} +``` + +复杂度分析: + +- 时间复杂度:O(nlogn),其中 n 为区间的数量。除去排序的开销,我们只需要一次线性扫描,所以主要的时间开销是排序的 O(nlogn)。 + +- 空间复杂度:O(logn),其中 n 为区间的数量。这里计算的是存储答案之外,使用的额外空间。O(logn) 即为排序所需要的空间复杂度。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" "b/Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" new file mode 100644 index 00000000..f99c600a --- /dev/null +++ "b/Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" @@ -0,0 +1,87 @@ +5.多数元素 +=== + + +### 题目 + +给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 + +你可以假设数组是非空的,并且给定的数组总是存在多数元素。 + + +数组中出现次数超过一半的数字被称为众数。 + +示例 1: + +- 输入:nums = [3,2,3] +- 输出:3 + +示例 2: + +- 输入:nums = [2,2,1,1,1,2,2] +- 输出:2 + + + +### 思路 + +##### 哈希表 + +遍历数组nums,用HashMap统计各数字的数量,即可找出众数。 +此方法时间和空间复杂度均为O(N)。 + + +##### 排序法 + +将数组nums排序,数组中心点的元素,一定是众数 + +##### 摩尔投票法 + +核心理念是票数正负抵消。此方法的时间和空间复杂度分别为O(N)和O(1)。 +为本题最佳解法。 + +若记 众数 的票数为 +1 ,非众数 的票数为 −1 ,则一定有所有数字的 票数和 >0 。 + +```python + +class Solution: + def majorityElement(self, nums: List[int]) -> int: + votes = 0 + for num in nums: + if votes == 0: + x = num + if num == x: + votes += 1 + else: + votes -= 1 + return x +``` + + +```kotlin +class Solution { + fun majorityElement(nums: IntArray): Int { + var vote = 0 + var k = 0 + for (num in nums) { + if (vote == 0) { + k = num + } + if (num == k) { + vote ++ + } else { + vote -- + } + } + return k + } +} +``` + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/50.\346\217\222\345\205\245\345\214\272\351\227\264.md" "b/Algorithm/50.\346\217\222\345\205\245\345\214\272\351\227\264.md" new file mode 100644 index 00000000..6fbe536e --- /dev/null +++ "b/Algorithm/50.\346\217\222\345\205\245\345\214\272\351\227\264.md" @@ -0,0 +1,102 @@ +50.插入区间 +=== + + +### 题目 + +给你一个 无重叠的 ,按照区间起始端点排序的区间列表 intervals,其中 intervals[i] = [starti, endi] 表示第 i 个区间的开始和结束,并且 intervals 按照 starti 升序排列。同样给定一个区间 newInterval = [start, end] 表示另一个区间的开始和结束。 + +在 intervals 中插入区间 newInterval,使得 intervals 依然按照 starti 升序排列,且区间之间不重叠(如果有必要的话,可以合并区间)。 + +返回插入之后的 intervals。 + +注意 你不需要原地修改 intervals。你可以创建一个新数组然后返回它。 + + + +示例 1: + +- 输入:intervals = [[1,3],[6,9]], newInterval = [2,5] +- 输出:[[1,5],[6,9]] + +示例 2: + +- 输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] +- 输出:[[1,2],[3,10],[12,16]] +- 解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。 + + +提示: + +- 0 <= intervals.length <= 104 +- intervals[i].length == 2 +- 0 <= starti <= endi <= 105 +- intervals 根据 starti 按 升序 排列 +- newInterval.length == 2 +- 0 <= start <= end <= 105 + +### 思路 + +- 题目说了是无重叠的按照区间起始端点排序的,所以不用排序了。 +- 用指针去扫 intervals,最多可能有三个阶段: + + - 不重叠的绿区间,在蓝区间的左边 + - 有重叠的绿区间 + - 不重叠的绿区间,在蓝区间的右边 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_insert_qujian_1.png?raw=true) + +- 逐个分析 + + - 不重叠,需满足:绿区间的右端,位于蓝区间的左端的左边,如 [1,2]。 + + - 则当前绿区间,推入 res 数组,指针 +1,考察下一个绿区间。 + - 循环结束时,当前绿区间的屁股,就没落在蓝区间之前,有重叠了,如 [3,5]。 + - 现在看重叠的。我们反过来想,没重叠,就要满足:绿区间的左端,落在蓝区间的屁股的后面,反之就有重叠:绿区间的左端 <= 蓝区间的右端,极端的例子就是 [8,10]。 + + - 和蓝有重叠的区间,会合并成一个区间:左端取蓝绿左端的较小者,右端取蓝绿右端的较大者,不断更新给蓝区间。 + - 循环结束时,将蓝区间(它是合并后的新区间)推入 res 数组。 + - 剩下的,都在蓝区间右边,不重叠。不用额外判断,依次推入 res 数组。 + + + + +```java +class Solution { + public int[][] insert(int[][] intervals, int[] newInterval) { + ArrayList res = new ArrayList<>(); + int len = intervals.length; + int i = 0; + // 判断左边不重合 + while (i < len && intervals[i][1] < newInterval[0]) { + res.add(intervals[i]); + i++; + } + // 判断重合 + while (i < len && intervals[i][0] <= newInterval[1]) { + newInterval[0] = Math.min(intervals[i][0], newInterval[0]); + newInterval[1] = Math.max(intervals[i][1], newInterval[1]); + i++; + } + res.add(newInterval); + // 判断右边不重合 + while (i < len && intervals[i][0] > newInterval[1]) { + res.add(intervals[i]); + i++; + } + return res.toArray(new int[0][]); + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是数组 intervals 的长度,即给定的区间个数。 + +- 空间复杂度:O(n)。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/51.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" "b/Algorithm/51.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" new file mode 100644 index 00000000..a6fcbec6 --- /dev/null +++ "b/Algorithm/51.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" @@ -0,0 +1,110 @@ +51.用最少数量的箭引爆气球 +=== + + +### 题目 + +有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。 + +一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。 + +给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。 + + +示例 1: + +- 输入:points = [[10,16],[2,8],[1,6],[7,12]] +- 输出:2 +- 解释:气球可以用2支箭来爆破: + - 在x = 6处射出箭,击破气球[2,8]和[1,6]。 + - 在x = 11处发射箭,击破气球[10,16]和[7,12]。 + +示例 2: + +- 输入:points = [[1,2],[3,4],[5,6],[7,8]] +- 输出:4 +- 解释:每个气球需要射出一支箭,总共需要4支箭。 + +示例 3: + +- 输入:points = [[1,2],[2,3],[3,4],[4,5]] +- 输出:2 +- 解释:气球可以用2支箭来爆破: + - 在x = 2处发射箭,击破气球[1,2]和[2,3]。 + - 在x = 4处射出箭,击破气球[3,4]和[4,5]。 + + +提示: + +- 1 <= points.length <= 105 +- points[i].length == 2 +- -231 <= xstart < xend <= 231 - 1 + + +### 思路 + +##### 方法一:区间合并 + +```java +//思路:区间重叠则合并,合并为交集 +//先排序 +class Solution { + public int findMinArrowShots(int[][] points) { + //防止相减式的比较造成溢出 + Arrays.sort(points,(o1,o2)->Integer.compare(o1[0],o2[0])); + int n = points.length; // 合并一次,n-1 + for(int i=1;i a[1] > b[1] ? 1 : -1); + //获取排序后第一个气球右边界的位置,我们可以认为是箭射入的位置 + int last = points[0][1]; + //统计箭的数量 + int count = 1; + for (int i = 1; i < points.length; i++) { + //如果箭射入的位置小于下标为i这个气球的左边位置,说明这支箭不能 + //击爆下标为i的这个气球,需要再拿出一支箭,并且要更新这支箭射入的 + //位置 + if (last < points[i][0]) { + last = points[i][1]; + count++; + } + } + return count; +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/52.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" "b/Algorithm/52.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" new file mode 100644 index 00000000..a9518cdc --- /dev/null +++ "b/Algorithm/52.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" @@ -0,0 +1,140 @@ +52.有效的括号 +=== + + +### 题目 + +给定一个只包括 '(',')','{','}','[',']' 的字符串s,判断字符串是否有效。 + +有效字符串需满足: + +- 左括号必须用相同类型的右括号闭合。 +- 左括号必须以正确的顺序闭合。 +- 每个右括号都有一个对应的相同类型的左括号。 + + +示例 1: + +- 输入:s = "()" + +- 输出:true + +示例 2: + +- 输入:s = "()[]{}" + +- 输出:true + +示例 3: + +- 输入:s = "(]" + +- 输出:false + +示例 4: + +- 输入:s = "([])" + +- 输出:true + + + +提示: + +- 1 <= s.length <= 104 +- s 仅由括号`()[]{}`组成 + + +### 思路 + +要求是“以正确的顺序闭合”,所以`([)]`这种是错误的。 + +要判断一个仅包含括号字符`()[]{}`的字符串是否有效(即括号是否按正确顺序闭合),可通过 ​栈`Stack`这一数据结构高效解决。 + + +括号的闭合需满足 ​​“后进先出”​​ 的嵌套关系: 最后出现的左括号需优先匹配右括号,栈的`LIFO后进先出`特性完美契合此需求。 + + + +​- 遍历字符​: 逐个处理字符串中的字符。 +​- 左括号入栈​: 遇到`(, {, [`时压入栈。 +​- 右括号匹配​: 遇到`), }, ]`时: + - 若栈为空 `→` 无效(右括号多余)。 + - 若栈顶左括号与当前右括号不匹配 `→` 无效。 + - 匹配则弹出栈顶元素。 + +- 最终校验​: 遍历结束后栈必须为空(左括号全被匹配)。 + +使用`ArrayDeque`替代传统`Stack`类,因前者在`push/pop`操作上效率更高(无同步开销)。 + + +​```java +import java.util.ArrayDeque; +import java.util.Deque; +public class Solution { + public boolean isValid(String s) { + // 边界处理:空字符串有效,null 或奇数长度直接无效 + if (s == null) { + return false; + } + if (s.isEmpty()) { + return true; + } + if (s.length() % 2 != 0) { + // 奇数长度不可能完全匹配 + return false; + } + + // 使用双端队列模拟栈(高效) + Deque stack = new ArrayDeque<>(); + + for (char c : s.toCharArray()) { + // 左括号:入栈 + if (c == '(' || c == '{' || c == '[') { + stack.push(c); + } + // 右括号:检查匹配 + else { + if (stack.isEmpty()) return false; // 栈空说明右括号多余 + char top = stack.pop(); // 弹出栈顶左括号 + // 检查括号类型是否匹配 + if ((c == ')' && top != '(') || + (c == '}' && top != '{') || + (c == ']' && top != '[')) { + return false; + } + } + } + return stack.isEmpty(); // 栈空说明所有左括号均被匹配 + } +} +``` + + + + +```python +# 映射表优化:用字典 bracket_map 存储括号对应关系,避免冗长的 if-else 分支 +def isValid(s: str) -> bool: + stack = [] + bracket_map = {')': '(', ']': '[', '}': '{'} # 右括号到左括号的映射 + for char in s: + if char in bracket_map.values(): # 左括号入栈 + stack.append(char) + elif char in bracket_map: # 右括号匹配 + if not stack or stack.pop() != bracket_map[char]: + return False + return not stack +``` + +复杂度分析: + +- 时间复杂度: O(N) +- 空间复杂度: O(N) + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/53.\347\256\200\345\214\226\350\267\257\345\276\204.md" "b/Algorithm/53.\347\256\200\345\214\226\350\267\257\345\276\204.md" new file mode 100644 index 00000000..a9ace618 --- /dev/null +++ "b/Algorithm/53.\347\256\200\345\214\226\350\267\257\345\276\204.md" @@ -0,0 +1,127 @@ +53.简化路径 +=== + + +### 题目 + +给你一个字符串path,表示指向某一文件或目录的 Unix 风格 绝对路径(以 '/' 开头),请你将其转化为 更加简洁的规范路径。 + +在Unix风格的文件系统中规则如下: + +- 一个点 '.' 表示当前目录本身。 +- 此外,两个点 '..' 表示将目录切换到上一级(指向父目录)。 +- 任意多个连续的斜杠(即,'//' 或 '///')都被视为单个斜杠 '/'。 +- 任何其他格式的点(例如,'...' 或 '....')均被视为有效的文件/目录名称。 + +返回的 简化路径 必须遵循下述格式: + +- 始终以斜杠 '/' 开头。 +- 两个目录名之间必须只有一个斜杠 '/' 。 +- 最后一个目录名(如果存在)不能 以 '/' 结尾。 +- 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 '.' 或 '..')。 + +返回简化后得到的 规范路径 。 + + + +示例 1: + +- 输入:path = "/home/" + +- 输出:"/home" + +解释: + +- 应删除尾随斜杠。 + +示例 2: + +- 输入:path = "/home//foo/" + +- 输出:"/home/foo" + +解释: + +- 多个连续的斜杠被单个斜杠替换。 + +示例 3: + +- 输入:path = "/home/user/Documents/../Pictures" + +- 输出:"/home/user/Pictures" + +解释: + +- 两个点 ".." 表示上一级目录(父目录)。 + +示例 4: + +- 输入:path = "/../" + +- 输出:"/" + +解释: + +- 不可能从根目录上升一级目录。 + +示例 5: + +- 输入:path = "/.../a/../b/c/../d/./" + +- 输出:"/.../b/d" + +解释: + +- "..." 在这个问题中是一个合法的目录名。 + + + +提示: + +- 1 <= path.length <= 3000 +- path 由英文字母,数字,'.','/' 或 '_' 组成。 +- path 是一个有效的 Unix 风格绝对路径。 + +### 思路 + + +根据题意,使用栈进行模拟即可。 + +具体的,从前往后处理 path,每次以 item 为单位进行处理(有效的文件名),根据 item 为何值进行分情况讨论: + +- item 为有效值 : 存入栈中 +- item 为 .. : 弹出栈顶元素(若存在) +- item 为 . : 不作处理 + +```java +public String simplifyPath(String path) { + Deque deque = new ArrayDeque<>(); + int n = path.length(); + for (int i = 1; i < n; i++) { + if (path.charAt(i) == '/') continue; // 找到下一个不是"/"的位置 + int j = i + 1; // j指向下一个位置,双指针! + while (j < n && path.charAt(j) != '/') j++; // 直到j指向的位置是"/" + String temp = path.substring(i, j); // 左闭右开,[i.j) + if (temp.equals("..")) { //..的时候,栈内有元素才删 + if (!deque.isEmpty()) { + deque.pollLast(); + } + } else if (!(temp.equals("."))) { // .的时候就continue,这里省略了,不是.的时候就进栈 + deque.addLast(temp); + } + i = j; //j是指向"/"的,令i=j开始下一轮循环 + } + StringBuilder ans = new StringBuilder(); + while (!deque.isEmpty()) { // 还原栈内的路径 + ans.append("/"); + ans.append(deque.pollFirst()); + } + return ans.length() == 0 ? "/" : ans.toString(); // 栈为空就直接返回"/",否则返回ans.toString() +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/54.\346\234\200\345\260\217\346\240\210.md" "b/Algorithm/54.\346\234\200\345\260\217\346\240\210.md" new file mode 100644 index 00000000..11a8c8c2 --- /dev/null +++ "b/Algorithm/54.\346\234\200\345\260\217\346\240\210.md" @@ -0,0 +1,103 @@ +54.最小栈 +=== + + +### 题目 + +设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 + +实现 MinStack 类: + +- MinStack() 初始化堆栈对象。 +- void push(int val) 将元素val推入堆栈。 +- void pop() 删除堆栈顶部的元素。 +- int top() 获取堆栈顶部的元素。 +- int getMin() 获取堆栈中的最小元素。 + + +示例 1: + +- 输入: +``` +["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. +``` + +提示: + +- -231 <= val <= 231 - 1 +- pop、top 和 getMin 操作总是在 非空栈 上调用 +- push, pop, top, and getMin最多被调用 3 * 104 次 + +### 思路 + + +解题思路: + +借用一个辅助栈min_stack,用于存获取stack中最小值。 + +算法流程: + +- push()方法: 每当push()新值进来时,如果小于等于min_stack栈顶值,则一起push()到min_stack,即更新了栈顶最小值 +- pop()方法: 判断将pop()出去的元素值是否是min_stack栈顶元素值(即最小值),如果是则将min_stack栈顶元素一起pop(),这样可以保证min_stack栈顶元素始终是stack中的最小值 +- getMin()方法: 返回min_stack栈顶即可 + +min_stack作用分析: + +min_stack等价于遍历stack所有元素,把升序的数字都删除掉,留下一个从栈底到栈顶降序的栈。 +相当于给stack中的降序元素做了标记,每当pop()这些降序元素,min_stack会将相应的栈顶元素pop()出去,保证其栈顶元素始终是stack中的最小元素。 +复杂度分析: + +- 时间复杂度 O(1) : 压栈,出栈,获取最小值的时间复杂度都为 O(1) 。 +- 空间复杂度 O(N) : 包含 N 个元素辅助栈占用线性大小的额外空间。 + +```java +class MinStack { + private Stack stack; + private Stack min_stack; + public MinStack() { + stack = new Stack<>(); + min_stack = new Stack<>(); + } + public void push(int x) { + stack.push(x); + if(min_stack.isEmpty() || x <= min_stack.peek()) { + min_stack.push(x); + } + } + public void pop() { + if(stack.pop().equals(min_stack.peek())) { + min_stack.pop(); + } + } + + public int top() { + return stack.peek(); + } + + public int getMin() { + return min_stack.peek(); + } +} +``` + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/55.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" "b/Algorithm/55.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" new file mode 100644 index 00000000..11250060 --- /dev/null +++ "b/Algorithm/55.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" @@ -0,0 +1,137 @@ +55.逆波兰表达式求值 +=== + + +### 题目 + +给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。 + +请你计算该表达式。返回一个表示表达式值的整数。 + +注意: + +- 有效的算符为 '+'、'-'、'*' 和 '/' 。 +- 每个操作数(运算对象)都可以是一个整数或者另一个表达式。 +- 两个整数之间的除法总是 向零截断 。 +- 表达式中不含除零运算。 +- 输入是一个根据逆波兰表示法表示的算术表达式。 +- 答案及所有中间计算结果可以用 32 位 整数表示。 + + +示例 1: + +- 输入:tokens = ["2","1","+","3","*"] +- 输出:9 +- 解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9 + +示例 2: + +- 输入:tokens = ["4","13","5","/","+"] +- 输出:6 +- 解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6 + +示例 3: + +- 输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"] +- 输出:22 +- 解释:该算式转化为常见的中缀算术表达式为: +``` + ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 += ((10 * (6 / (12 * -11))) + 17) + 5 += ((10 * (6 / -132)) + 17) + 5 += ((10 * 0) + 17) + 5 += (0 + 17) + 5 += 17 + 5 += 22 +``` + +提示: + +- 1 <= tokens.length <= 104 +- tokens[i]是一个算符(`+`、`-`、`*`或`/`),或是在范围`[-200, 200]`内的一个整数 + + +逆波兰表达式: + +- 逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。 + +- 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。 +- 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。 + +逆波兰表达式主要有以下两个优点: + +- 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。 +- 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中 + +### 思路 + +逆波兰表达式,也叫做后缀表达式。 + +我们平时见到的运算表达式是中缀表达式,即 "操作数① 运算符② 操作数③" 的顺序,运算符在两个操作数中间。 +但是后缀表达式是 "操作数① 操作数③ 运算符②" 的顺序,运算符在两个操作数之后。 + + +逆波兰表达式严格遵循「从左到右」的运算。计算逆波兰表达式的值时,使用一个栈存储操作数,从左到右遍历逆波兰表达式,进行如下操作: + +- 如果遇到操作数,则将操作数入栈; + +- 如果遇到运算符,则将两个操作数出栈,其中先出栈的是右操作数,后出栈的是左操作数,使用运算符对两个操作数进行运算,将运算得到的新操作数入栈。 + +整个逆波兰表达式遍历完毕之后,栈内只有一个元素,该元素即为逆波兰表达式的值。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_evalRPN1.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_evalRPN2.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_evalRPN3.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_evalRPN4.png?raw=true) + +```java +class Solution { + public int evalRPN(String[] tokens) { + Deque stack = new LinkedList(); + int n = tokens.length; + for (int i = 0; i < n; i++) { + String token = tokens[i]; + if (isNumber(token)) { + stack.push(Integer.parseInt(token)); + } else { + int num2 = stack.pop(); + int num1 = stack.pop(); + switch (token) { + case "+": + stack.push(num1 + num2); + break; + case "-": + stack.push(num1 - num2); + break; + case "*": + stack.push(num1 * num2); + break; + case "/": + stack.push(num1 / num2); + break; + default: + } + } + } + return stack.pop(); + } + + public boolean isNumber(String token) { + return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token)); + } +} +``` + +复杂度分析: + +- 时间复杂度: O(n),其中 n 是数组 tokens 的长度。需要遍历数组 tokens 一次,计算逆波兰表达式的值。 + +- 空间复杂度: O(n),其中 n 是数组 tokens 的长度。使用栈存储计算过程中的数,栈内元素个数不会超过逆波兰表达式的长度。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/56.\345\237\272\346\234\254\350\256\241\347\256\227\345\231\250.md" "b/Algorithm/56.\345\237\272\346\234\254\350\256\241\347\256\227\345\231\250.md" new file mode 100644 index 00000000..10656199 --- /dev/null +++ "b/Algorithm/56.\345\237\272\346\234\254\350\256\241\347\256\227\345\231\250.md" @@ -0,0 +1,163 @@ +56.基本计算器 +=== + + +### 题目 + +给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。 + +注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() 。 + + + +示例 1: + +- 输入:s = "1 + 1" +- 输出:2 + +示例 2: + +- 输入:s = " 2-1 + 2 " +- 输出:3 + +示例 3: + +- 输入:s = "(1+(4+5+2)-3)+(6+8)" +- 输出:23 + + +提示: + +- 1 <= s.length <= 3 * 105 +- s 由数字、'+'、'-'、'('、')'、和 ' ' 组成 +- s 表示一个有效的表达式 +- '+' 不能用作一元运算(例如, "+1" 和 "+(2 + 3)" 无效) +- '-' 可以用作一元运算(即 "-1" 和 "-(2 + 3)" 是有效的) +- 输入中不存在两个连续的操作符 +- 每个数字和运行的计算将适合于一个有符号的 32位 整数 + +### 思路 + + +【进阶补充】双栈解决通用「表达式计算」问题 + + +宫水三叶 +76872 +2021.03.10 +发布于 上海市 +栈 +数学 +C++ +Java +双栈解法 + +我们可以使用两个栈 nums 和 ops 。 + +nums : 存放所有的数字 +ops :存放所有的数字以外的操作,+/- 也看做是一种操作 +然后从前往后做,对遍历到的字符做分情况讨论: + +空格 : 跳过 +( : 直接加入 ops 中,等待与之匹配的 ) +) : 使用现有的 nums 和 ops 进行计算,直到遇到左边最近的一个左括号为止,计算结果放到 nums +数字 : 从当前位置开始继续往后取,将整一个连续数字整体取出,加入 nums ++/- : 需要将操作放入 ops 中。在放入之前先把栈内可以算的都算掉,使用现有的 nums 和 ops 进行计算,直到没有操作或者遇到左括号,计算结果放到 nums +一些细节: + +由于第一个数可能是负数,为了减少边界判断。一个小技巧是先往 nums 添加一个 0 +为防止 () 内出现的首个字符为运算符,将所有的空格去掉,并将 (- 替换为 (0-,(+ 替换为 (0+(当然也可以不进行这样的预处理,将这个处理逻辑放到循环里去做) + + + +class Solution { + public int calculate(String s) { + // 存放所有的数字 + Deque nums = new ArrayDeque<>(); + // 为了防止第一个数为负数,先往 nums 加个 0 + nums.addLast(0); + // 将所有的空格去掉 + s = s.replaceAll(" ", ""); + // 存放所有的操作,包括 +/- + Deque ops = new ArrayDeque<>(); + int n = s.length(); + char[] cs = s.toCharArray(); + for (int i = 0; i < n; i++) { + char c = cs[i]; + if (c == '(') { + ops.addLast(c); + } else if (c == ')') { + // 计算到最近一个左括号为止 + while (!ops.isEmpty()) { + char op = ops.peekLast(); + if (op != '(') { + calc(nums, ops); + } else { + ops.pollLast(); + break; + } + } + } else { + if (isNum(c)) { + int u = 0; + int j = i; + // 将从 i 位置开始后面的连续数字整体取出,加入 nums + while (j < n && isNum(cs[j])) u = u * 10 + (int)(cs[j++] - '0'); + nums.addLast(u); + i = j - 1; + } else { + if (i > 0 && (cs[i - 1] == '(' || cs[i - 1] == '+' || cs[i - 1] == '-')) { + nums.addLast(0); + } + // 有一个新操作要入栈时,先把栈内可以算的都算了 + while (!ops.isEmpty() && ops.peekLast() != '(') calc(nums, ops); + ops.addLast(c); + } + } + } + while (!ops.isEmpty()) calc(nums, ops); + return nums.peekLast(); + } + void calc(Deque nums, Deque ops) { + if (nums.isEmpty() || nums.size() < 2) return; + if (ops.isEmpty()) return; + int b = nums.pollLast(), a = nums.pollLast(); + char op = ops.pollLast(); + nums.addLast(op == '+' ? a + b : a - b); + } + boolean isNum(char c) { + return Character.isDigit(c); + } +} + +作者:宫水三叶 +链接:https://leetcode.cn/problems/basic-calculator/solutions/646865/shuang-zhan-jie-jue-tong-yong-biao-da-sh-olym/ +来源:力扣(LeetCode) +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + + + + + + + + + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/57..md b/Algorithm/57..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/57..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/58..md b/Algorithm/58..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/58..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/59..md b/Algorithm/59..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/59..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" "b/Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" new file mode 100644 index 00000000..4f9e6d02 --- /dev/null +++ "b/Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" @@ -0,0 +1,88 @@ +6.轮转数组 +=== + + +### 题目 + +给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。 + + +示例 1: + +- 输入: nums = [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] + +示例 2: + +- 输入:nums = [-1,-100,3,99], k = 2 +- 输出:[3,99,-1,-100] + +解释: + +- 向右轮转 1 步: [99,-1,-100,3] +- 向右轮转 2 步: [3,99,-1,-100] + + + +### 思路 + + +##### 双层循环 + +外层循环k此,不断把最后一位移到第一位,然后内层循环每个元素后移一位 + +```kotlin +class Solution { + fun rotate(nums: IntArray, k: Int): Unit { + for (i in 0.. None: + def reverse(i: int, j: int) -> None: + while i < j: + nums[i], nums[j] = nums[j], nums[i] + i += 1 + j -= 1 + + n = len(nums) + k %= n # 轮转 k 次等于轮转 k % n 次 + reverse(0, n - 1) + reverse(0, k - 1) + reverse(k, n - 1) +``` + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/60..md b/Algorithm/60..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/60..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/61..md b/Algorithm/61..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/61..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/62..md b/Algorithm/62..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/62..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/7.\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/Algorithm/7.\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..18fa2d74 --- /dev/null +++ "b/Algorithm/7.\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,76 @@ +7.买卖股票的最佳时机 +=== + + +### 题目 + +给定一个数组prices,它的第i个元素prices[i]表示一支给定股票第i天的价格。 + +你只能选择`某一天`买入这只股票,并选择在`未来的某一个不同的日子`卖出该股票。设计一个算法来计算你所能获取的最大利润。 + +返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0。 + + + +示例 1: + +- 输入:[7,1,5,3,6,4] +- 输出:5 +- 解释:在第2天(股票价格 = 1)的时候买入,在第5天(股票价格=6)的时候卖出,最大利润= `6-1 = 5` 。 + 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 +示例 2: + +- 输入:prices = [7,6,4,3,1] +- 输出:0 +- 解释:在这种情况下, 没有交易完成, 所以最大利润为 0。 + + +### 思路 + + +##### 暴力遍历 + +先考虑最简单的「暴力遍历」,即枚举出所有情况,并从中选择最大利润。设数组 prices 的长度为 n ,由于只能先买入后卖出,因此第 1 天买可在未来 n−1 天卖出,第 2 天买可在未来 n−2 天卖出……以此类推,要思考更优解法。 + +然而,暴力法会产生许多冗余计算。例如,若第 1 天价格低于第 2 天价格,即第 1 天成本更低,那么我们一定不会选择在第 2 天买入。进一步的,若在前 i 天选择买入,若想达到最高利润,则一定选择价格最低的交易日买入。 + +##### 贪心思想 + +考虑根据此贪心思想,遍历价格列表 prices 并执行两步: + + +- 更新前 i 天的最低价格,即最低买入成本 cost; +- 更新前 i 天的最高利润 profit ,即选择「前 i−1 天最高利润 profit 」和「第 i 天卖出的最高利润 price - cost 」中的最大值 ; + + +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + minPrice = prices[0] + cost = 0 + for i in prices: + if i < minPrice: + minPrice = i + elif i - minPrice > cost: + cost = i - minPrice + return cost +``` + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/8.\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\272II.md" "b/Algorithm/8.\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\272II.md" new file mode 100644 index 00000000..00e0fa54 --- /dev/null +++ "b/Algorithm/8.\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\272II.md" @@ -0,0 +1,107 @@ +8.买卖股票的最佳时机II +=== + + +### 题目 + +给你一个整数数组prices,其中prices[i]表示某支股票第i天的价格。 + +在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有一股股票。你也可以先购买,然后在同一天出售。 + +返回 你能获得的 最大 利润 。 + + + +示例 1: + +- 输入:prices = [7,1,5,3,6,4] +- 输出:7 +- 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 +- 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。 +- 最大总利润为 4 + 3 = 7 。 + +示例 2: + +- 输入:prices = [1,2,3,4,5] +- 输出:4 +- 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 +- 最大总利润为 4。 + +示例 3: + +- 输入:prices = [7,6,4,3,1] +- 输出:0 +- 解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。 + + +### 思路 + +这个的改动点时可以交易多次,怎么能保证利益最大? +下面两种方式其实一样 + +##### 方式一 + +只要第二天价格比第一天价格高就卖 + +```kotlin +class Solution { + fun maxProfit(prices: IntArray): Int { + var buyPirce = prices[0] + var totalIncome = 0 + for (i in prices) { + if (i > buyPirce) { + totalIncome += (i - buyPirce) + buyPirce = i + } else if (i < buyPrice) { + buyPirce = i + } + } + return totalIncome + } +} +``` +##### 方式二 + +- 记录当前可卖的最大收入及买入的价格 +- 如果最新的价格低于买入的价格或者当前已经有收入了且当前的价格小于最大收入时的卖价(i - buyPrice < income),这个时候需要卖了再买 + + +```python3 +class Solution: + def maxProfit(self, prices: List[int]) -> int: + buyPrice = prices[0] + income = 0 + totalIncome = 0 + for i in prices: + + if i < buyPrice or i - buyPrice < income: # 有更低的价格,或者说现在的价格比目前收益最大时可卖出的价格更低 + # 有更低的购买价格了,这个时候要判断当前有没有利润,有就卖,没有就使用该价格买 + if (income > 0): + totalIncome += income + income = 0 + buyPrice = i + elif (i - buyPrice > income): + income = i - buyPrice + + totalIncome += income + return totalIncome +``` + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" "b/Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" new file mode 100644 index 00000000..3bb87d3a --- /dev/null +++ "b/Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" @@ -0,0 +1,91 @@ +9.跳跃游戏 +=== + + +### 题目 + +给你一个非负整数数组nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。 + +判断你是否能够到达最后一个下标,如果可以,返回true;否则,返回false。 + + + +示例 1: + +- 输入:nums = [2,3,1,1,4] +- 输出:true +- 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 + +示例 2: + +- 输入:nums = [3,2,1,0,4] +- 输出:false +- 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。 + + + +### 思路 + +##### 方式一 + +题目中说:数组中的每个元素代表你在该位置可以跳跃的最大长度。 +所以我们只要保证能够跳跃的最大长度超过了数组的长度就可以。 + + +- 每走一步就记录最远可以到哪里 +- 最远可达位置够不够你继续往前走 + + +```python +class Solution: + def canJump(self, nums: List[int]) -> bool: + rightmost = 0 + for i in range(len(nums)): + # 在可走的最大步数范围内走 + if i <= rightmost: + # 每走一步更新一下可继续走的最大步 + rightmost = max(rightmost, i + nums[i]) + if rightmost >= len(nums) - 1: + return True + return False +``` + + + + +##### 方式二 + + +- 假设在一个格子上,每个格子有对应的能量值。 +- 每前进一步需要消耗一个能量; +- 而到达一个格子后, 如果当前格子储存的能量值较大,则更新为较大的能量值; +- 如果当前格子能量值小于现有的能量值,则无需更新; +- 如果出现能量值正好消耗完,那就没能量继续走了,最远的步数就是这里了 +- 判断最远的步数与数组的长度一样不一样 + +```python +class Solution: + def canJump(self, nums: List[int]) -> bool: + cur = nums[0] + if len(nums) == 1: + return True + + for i in range(len(nums)): + cur -= 1 + if cur < nums[i]: + cur = nums[i] + if cur <= 0: + break + return i == (len(nums) - 1) +``` + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/AppPublish/Zipalign\344\274\230\345\214\226.md" "b/AppPublish/Zipalign\344\274\230\345\214\226.md" index f79fa2cc..c69dedfa 100644 --- "a/AppPublish/Zipalign\344\274\230\345\214\226.md" +++ "b/AppPublish/Zipalign\344\274\230\345\214\226.md" @@ -22,7 +22,7 @@ And any files added to an "aligned" archive will not be aligned. ``` 大意就是它提供了一个灰常重要滴功能来确保所有未压缩的数据都从文件的开始位置以指定的4字节对齐方式排列,例如图片或者 -`raw`文件。当然好处也是大大的,就是能够减少内存的资源消耗。最后他还特意提醒了你一下就是已经在对`apk`签完名之后再用`zipalign` +`raw`文件。当然好处也是大大的,就是能够减少内存的资源消耗。最后他还特意提醒了你一下就是一定在对`apk`签完名之后再用`zipalign` 优化,如果你在之前用,那无效。 废多看用法: diff --git "a/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" "b/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" new file mode 100644 index 00000000..6fa7327a --- /dev/null +++ "b/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" @@ -0,0 +1,108 @@ +1.系统架构 +=== + +#### 什么是系统架构 + +关于系统架构,维基百科给出了一个非常好的定义。 +A system architecture is the conceptual model that defines the structure, behavior, and more views of a system.[ +(系统架构是概念模型,定义了系统的结构、行为和更多的视图) + +* 系统架构是一个概念模型。 +* 系统架构定义了系统的结构、行为以及更多的视图。 +关于这个定义,这里给出了另外一种解读,供大家参考。 +* 静。首先,从静止的角度,描述系统如何组成,以及系统的功能在这些组成部分之间是如何划分的。这就是系统的“结构”。一般要描述的是:系统包含哪些子系统,每个子系统有什么功能。在做这些描述时,应感觉自己是一名导游,带着游客在系统的子系统间参观。 +* 动。然后,从动态的角度,描述各子系统之间是如何联动的,它们是如何相互配合完成系统预定的任务或流程的。这就是系统的“行为”。在做这个描述时,应感觉自己是一名电影导演,将系统的各种运行情况通过一个个短片展现出来。 +* 细。最后,在以上两种描述的基础上,从不通的角度,更详细的刻画出系统的细节和全貌。这就是“更多的视图”。 + + + + +好代码的特性: +1. 鲁棒(Solid and Robust) +2. 高效(Fast) +3. 简洁(Maintainable and Simple) +4. 简短(Small) +5. 可测试(Testable) +6. 共享(Re-Usable) +7. 可移植(Portable) +8. 可观测(Observvable)/可监控(Monitorable) +9. 可运维(Operational): 可运维重点关注成本、效率和稳定性三个方面 +10. 可扩展(Scalable and Extensible) + + +工程能力的定义: +使用系统化的方法,在保证质量的前提下,更高效率的为客户/用户持续交付有价值的软件或服务的能力。 + +在《软件开发的201个原则》一书中,将“质量第一”列为全书的第一个原则,可见其重要性。 +Edward Yourdon建议,当你被要求加快测试、忽视剩余的少量Bug、在设计或需求达成一致前就开始编码时,要直接说“不”。 +开发前期的设计文档、技术评审3天以上100%。代码规范,缺乏认真的代码评审。 +降低质量要求,事实上不会降低研发成本,反而会增加整体的研发成本。在研发阶段通过降低质量所“节省”的研发成本,会在软件维护阶段加倍偿还。 + +在研发前期(需求分析和系统设计)多投入资源,相对于把资源都投入在研发后期(编码、测试等),其收益更大。 + + +### 架构三要素 + +#### 构件 + +构件在软件领域是指可复用的模块,它可以是被封装的对象类、类树、一些功能模块、软件框架(framework)、软件架构(或体系结构Architectural)、文档、分析件、设计模式(Pattern)。但是,操作集合、过程、函数即使可以复用也不能成为一个构件。 + +##### 构件的属性: +1. 有用性(Usefulness):构件必须提供有用的功能。 +2. 可用性(Usability):构件必须易于理解和使用,可以正常运行。 +3. 质量(Quality):构件及其变形必须能正确工作,质量好坏与可用性相互补充。 +4. 适应性(Adaptability):构件应该易于通过参数化等方式再不同环境中进行配置,比较高端一点的复用性,接收外界各种入参,产生不同的结果,健壮性比较高。 +5. 可移植性(Portability):构件应能在不同的硬件运行平台和软件环境中工作,可移植性比较好,跨平台。 + + +#### 模式(Pattern) + +其实就是解决某一类问题的方法论,是生产经验和生活经验中经过抽象和升华提炼出来的核心知识体系。 模式就是一个完整的流程闭环,能够解决一些问题的通用方法(比如资本运作、玩家不同的需求等),软件中的模式大多源于生活,是人类智慧的结晶。 + +#### 规划 + +规划是系统架构中最重要的组成部分,是个人或者组织制定的比较全面长远的发展计划,是对未来整体性、长期性、基本性问题的思考和考量。设计未来整套行动的方案。很早就有规划这个概念了,例如:国家的十一五规划等。当然软件开发也和生活紧密联系,一个大型的系统也需要良好的规划,规划可以说是基石,是系统架构的前提。 + + +系统架构虽然是软件系统的结构,行为,属性的高级抽象,但其根本就是在需求分析的基础行为下,制定技术框架,对需求的技术实现。 + + +### 系统设计的原则和方法: + +* 单一目的 +* 对外关系清晰 +* 重视资源约束 +* 根据需求做决策 +* 基于模型思考 + + +#### 单一职责原则 + + + +#### 开放-封闭原则 +无论模块是多么的‘封闭’,都会存在一些无法对之封闭的变化。既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化 + +开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用、灵活性好。开发人员应该仅对程序中呈现出频繁变化的那些部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要 + + +#### 依赖倒转原则 + + + +#### 迪米特法则 +迪米特法则首先强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限,也就是说,一个类包装好自己的private状态,不需要让别的类知道的字段或行为就不要公开 + +我们在程序设计时,类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。也就是说,信息的隐藏促进了软件的复用。” + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Architect/2.UML\347\256\200\344\273\213.md" "b/Architect/2.UML\347\256\200\344\273\213.md" new file mode 100644 index 00000000..2bcf58de --- /dev/null +++ "b/Architect/2.UML\347\256\200\344\273\213.md" @@ -0,0 +1,26 @@ +2.UML简介 + +推荐使用[Visual Paradigm](https://www.visual-paradigm.com/cn/),如果是非商业用途可以下载社区版,免费使用 + + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/uml_demo.png?raw=true) + + +类图分三层,第一层显示类的名称,如果是抽象类,则就用斜体显示。第二层是类的特性,通常就是字段和属性。第三层是类的操作,通常是方法或行为。注意前面的符号,‘+’表示public,‘-’表示private,‘#’表示protected。” + +继承关系用空心三角形+实线来表示。 +接口用空心三角形+虚线来表示。 +关联关系用实线箭头来表示。 +聚合表示一种弱的‘拥有’关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。聚合关系用空心的菱形+实线箭头来表示。 +合成(Composition,也有翻译成‘组合’的)是一种强的‘拥有’关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样.合成关系用实心的菱形+实线箭头来表示 +依赖关系(Dependency),用虚线箭头来表示。 + + + + +## 类图 + +## 时序图 + + diff --git "a/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" "b/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" deleted file mode 100644 index 396665c9..00000000 --- "a/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" +++ /dev/null @@ -1,164 +0,0 @@ -2.集成(二) -=== - - -首先在`Project`目录中的`build.gradle`中添加`google()`仓库(大部分项目可能都已经有了): - -``` -allprojects { - repositories { - jcenter() - google() - } -} -``` - -然后在`app`的`build.gradle`中添加对应的依赖,如: - -``` -implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" -``` -如果想要使用`kotlin`开发的话,可以在后面加上`-ktx`后缀就可以了,如下: -``` -implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" -``` - -`Lifecycle`依赖: ---- - -`Lifecycle`依赖包括`LiveData`和`ViewModel` - -``` -dependencies { - def lifecycle_version = "1.1.1" - - // ViewModel and LiveData - implementation "android.arch.lifecycle:extensions:$lifecycle_version" - // alternatively - just ViewModel - implementation "android.arch.lifecycle:viewmodel:$lifecycle_version" // use -ktx for Kotlin - // alternatively - just LiveData - implementation "android.arch.lifecycle:livedata:$lifecycle_version" - // alternatively - Lifecycles only (no ViewModel or LiveData). - // Support library depends on this lightweight import - implementation "android.arch.lifecycle:runtime:$lifecycle_version" - - annotationProcessor "android.arch.lifecycle:compiler:$lifecycle_version" - // alternately - if using Java8, use the following instead of compiler - implementation "android.arch.lifecycle:common-java8:$lifecycle_version" - - // optional - ReactiveStreams support for LiveData - implementation "android.arch.lifecycle:reactivestreams:$lifecycle_version" - - // optional - Test helpers for LiveData - testImplementation "android.arch.core:core-testing:$lifecycle_version" -} -``` - - -`Room`依赖: ---- - -`Room`的依赖包括`testing Room migrations`和`Room RxJava` - -``` -dependencies { - def room_version = "1.1.1" - - implementation "android.arch.persistence.room:runtime:$room_version" - annotationProcessor "android.arch.persistence.room:compiler:$room_version" - - // optional - RxJava support for Room - implementation "android.arch.persistence.room:rxjava2:$room_version" - - // optional - Guava support for Room, including Optional and ListenableFuture - implementation "android.arch.persistence.room:guava:$room_version" - - // Test helpers - testImplementation "android.arch.persistence.room:testing:$room_version" -} - -``` - -`Paging`依赖 ---- - -``` -dependencies { - def paging_version = "1.0.0" - - implementation "android.arch.paging:runtime:$paging_version" - - // alternatively - without Android dependencies for testing - testImplementation "android.arch.paging:common:$paging_version" - - // optional - RxJava support, currently in release candidate - implementation "android.arch.paging:rxjava2:1.0.0-rc1" -} -``` - -`Navigation`依赖 ---- - -> Navigation classes are already in the androidx.navigation package, but currently depend on Support Library 27.1.1, and associated Arch component versions. Version of Navigation with AndroidX dependencies will be released in the future. - -``` -dependencies { - def nav_version = "1.0.0-alpha02" - - implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin - implementation "android.arch.navigation:navigation-ui:$nav_version" // use -ktx for Kotlin - - // optional - Test helpers - androidTestImplementation "android.arch.navigation:navigation-testing:$nav_version" // use -ktx for Kotlin -} -``` - - -`Safe args`依赖 ---- - -想要使用`Safe args`,需要在`Project`顶层的`build.gradle`中配置以下路径: -``` -buildscript { - repositories { - google() - } - dependencies { - classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha02" - } -} -``` -并且在`app`或`module`中`build.gradle`中: -``` -apply plugin: "androidx.navigation.safeargs" -``` - -`WorkManager`依赖 ---- - -> WorkManager classes are already in the androidx.work package, but currently depend on Support Library 27.1, and associated Arch component versions. Version of WorkManager with AndroidX dependencies will be released in the future. - - -``` -dependencies { - def work_version = "1.0.0-alpha03" - - implementation "android.arch.work:work-runtime:$work_version" // use -ktx for Kotlin - - // optional - Firebase JobDispatcher support - implementation "android.arch.work:work-firebase:$work_version" - - // optional - Test helpers - androidTestImplementation "android.arch.work:work-testing:$work_version" -} -``` - - -[上一篇: 1.简介(一)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/1.%E7%AE%80%E4%BB%8B(%E4%B8%80).md) -[下一篇: 3.Lifecycle(三)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/3.Lifecycle(%E4%B8%89).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/ArchitectureComponents/3.Lifecycle(\344\270\211).md" "b/ArchitectureComponents/3.Lifecycle(\344\270\211).md" deleted file mode 100644 index b034723b..00000000 --- "a/ArchitectureComponents/3.Lifecycle(\344\270\211).md" +++ /dev/null @@ -1,175 +0,0 @@ -3.Lifecycle(三) -=== - - -`Android`开发中,经常需要管理生命周期。举个栗子,我们需要获取用户的地址位置,当这个`Activity`在显示的时候,我们开启定位功能,然后实时获取到定位信息,当页面被销毁的时候,需要关闭定位功能。 -```java -class MyLocationListener { - public MyLocationListener(Context context, Callback callback) { - // ... - } - - void start() { - // connect to system location service - } - - void stop() { - // disconnect from system location service - } -} - - -class MyActivity extends AppCompatActivity { - private MyLocationListener myLocationListener; - - @Override - public void onCreate(...) { - myLocationListener = new MyLocationListener(this, (location) -> { - // update UI - }); - } - - @Override - public void onStart() { - super.onStart(); - myLocationListener.start(); - // manage other components that need to respond - // to the activity lifecycle - } - - @Override - public void onStop() { - super.onStop(); - myLocationListener.stop(); - // manage other components that need to respond - // to the activity lifecycle - } -} -``` - -上面的代码看起来还挺简单,但是当定位功能需要满足一些条件下才开启,那么会变得复杂多了。可能在执行`Activity`的`stop`方法时,定位的`start`方法才刚刚开始执行,比如如下代码,这样生命周期管理就变得很麻烦了。 -```java -class MyActivity extends AppCompatActivity { - private MyLocationListener myLocationListener; - - public void onCreate(...) { - myLocationListener = new MyLocationListener(this, location -> { - // update UI - }); - } - - @Override - public void onStart() { - super.onStart(); - Util.checkUserStatus(result -> { - // what if this callback is invoked AFTER activity is stopped? - if (result) { - myLocationListener.start(); - } - }); - } - - @Override - public void onStop() { - super.onStop(); - myLocationListener.stop(); - } -} -``` - -`android.arch.lifecycle`包提供的类和接口可帮助您用简单和独立的方式解决这些问题。 - -`Lifecycle`类是一个持有组件(`activity`或`fragment`)生命周期信息的类,其他对象可以观察该状态。`Lifecycle`使用两个重要的枚举部分来管理对应组件的生命周期的状态: - -- `Event`:生命周期事件由系统来分发,这些事件对应于`Activity`和`Fragment`的生命周期函数。 - -- `State`:`Lifecycle`对象所追踪的组件的当前状态 - - - - -```kotlin -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - // lifecycle是LifecycleOwner接口的getLifecycle()方法得到的,从com.android.support:appcompat-v7:26.1.0开始activity和fragment都实现了该接口 - lifecycle.addObserver(MyObserver()) - } -} -``` - -```kotlin -class MyObserver : LifecycleObserver{ - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - fun connectListener() { - Log.e("@@@", "connect") - } - - @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) - fun disconnectListener() { - Log.e("@@@", "disconnect") - } -} -``` -上面的`lifecycle.addObserver(MyObserver()) `的完整写法应该是`aLifecycleOwner.getLifecycle().addObserver(new MyObserver())`而`aLifecycleOwner`一般是实现了`LifecycleOwner`的类,比如`Activity/Fragment` - - - -`LifecycleOwner` ---- - -那什么是`LifecycleOwner`呢?实现`LifecycleOwner`接口就表示这是个有生命周期的类,他有一个`getLifecycle ()`方法是必须实现的。 - -对于前面提到的监听位置的例子。可以把`MyLocationListener`实现`LifecycleObserver`,然后在`Lifecycle(Activity/Fragment)`的`onCreate`方法中初始化。这样`MyLocationListener`就能自行处理生命周期带来的问题。 - - - -从`Support Library 26.1.0`开始`Activity/Fragment`已经实现了`LifecycleOwner`接口。 -如果想在自定义的类中实现`LifecyclerOwner`,就需要用到[LifecycleRegistry](https://developer.android.com/reference/android/arch/lifecycle/LifecycleRegistry)类,并且需要自行发送`Event`: - -```java -public class MyActivity extends Activity implements LifecycleOwner { - private LifecycleRegistry mLifecycleRegistry; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mLifecycleRegistry = new LifecycleRegistry(this); - mLifecycleRegistry.markState(Lifecycle.State.CREATED); - } - - @Override - public void onStart() { - super.onStart(); - mLifecycleRegistry.markState(Lifecycle.State.STARTED); - } - - @NonNull - @Override - public Lifecycle getLifecycle() { - return mLifecycleRegistry; - } -} -``` - - -`Lifecycles`的最佳建议: - -- 保持`UI Controllers(Activity/Fragment)`中代码足够简洁。一定不能包含如何获取数据的代码,要通过`ViewModel`获取`LiveData`形式的数据。 -- 用数据驱动`UI`,`UI`的职责就是根据数据改变显示的内容,并且把用户操作`UI`的行为传递给`ViewModel`。 -- 把业务逻辑相关的代码放到`ViewModel`中,把`ViewModel`看成是链接`UI`和`App`其他部分的纽带。但`ViewModel`不能直接获取数据,要通过调用其他类来获取数据。 -- 使用`DataBinding`来简化`View`(布局文件)和`UI Controllers(Activity/Fragment)`之间的代码 -- 如果布局本身太过复杂,可以考虑创建一个`Presenter`类来处理UI相关的改变。虽然这么做会多写很多代码,但是对于保持`UI`的简介和可测试性是有帮助的。 -- 不要在`ViewModel`中持有任何`View/Activity`的`context`。否则会造成内存泄露。 - - -[上一篇: 2.集成(二)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/2.%E9%9B%86%E6%88%90(%E4%BA%8C).md) -[下一篇: 4.LiveData(四)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/4.LiveData(%E5%9B%9B).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/ArchitectureComponents/4.LiveData(\345\233\233).md" "b/ArchitectureComponents/4.LiveData(\345\233\233).md" deleted file mode 100644 index 143fdc3f..00000000 --- "a/ArchitectureComponents/4.LiveData(\345\233\233).md" +++ /dev/null @@ -1,315 +0,0 @@ -4.LiveData(四) -=== - -> LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state. - - -`LiveData`是一种持有可被观察数据的类。和其他可被观察的类不同的是,`LiveData`是有生命周期感知能力的,这意味着它可以在`activities`,`fragments`,或者`services`生命周期是活跃状态时更新这些组件。那么什么是活跃状态呢?上篇文章中提到的`STARTED`和`RESUMED`就是活跃状态,只有在这两个状态下`LiveData`是会通知数据变化的。 - -要想使用`LiveData`(或者这种有可被观察数据能力的类)就必须配合实现了`LifecycleOwner`的对象使用。在这种情况下,当对应的生命周期对象`DESTROYED`时,才能移除观察者。这对`Activity`或者`Fragment`来说显得尤为重要,因为他们可以在生命周期结束的时候立刻解除对数据的订阅,从而避免内存泄漏等问题。 - - - - -使用`LiveData`的优点: - -- `UI`和实时数据保持一致 因为`LiveData`采用的是观察者模式,这样一来就可以在数据发生改变时获得通知,更新`UI`。 -- 避免内存泄漏,观察者被绑定到组件的生命周期上,当被绑定的组件销毁(`destory`)时,观察者会立刻自动清理自身的数据。 -- 不会再产生由于`Activity`处于`stop`状态而引起的崩溃 例如:当`Activity`处于后台状态时,是不会收到`LiveData`的任何事件的。 -- 不需要再解决生命周期带来的问题`LiveData`可以感知被绑定的组件的生命周期,只有在活跃状态才会通知数据变化。 -- 实时数据刷新,当组件处于活跃状态或者从不活跃状态到活跃状态时总是能收到最新的数据 -- 解决`Configuration Change`问题,在屏幕发生旋转或者被回收再次启动,立刻就能收到最新的数据。 -- 数据共享,如果对应的`LiveData`是单例的话,就能在`app`的组件间分享数据。 - - - -使用`LiveData`: - -- 创建一个持有某种数据类型的`LiveData`(通常是在`ViewModel`中) -- 创建一个定义了`onChange()`方法的观察者。这个方法是控制`LiveData`中数据发生变化时,采取什么措施 (比如更新界面)。通常是在`UI Controller`(`Activity/Fragment`)中创建这个观察者。 -- 通过`observe()`方法连接观察者和`LiveData`。`observe()`方法需要携带一个`LifecycleOwner`类。这样就可以让观察者订阅`LiveData`中的数据,实现实时更新。 - - -创建`LiveData`对象 ---- - -`LiveData`是一个数据的包装。具体的包装对象可以是任何数据,包括集合(比如`List`)。`LiveData`通常在`ViewModel`中创建,然后通过`getter`方法获取。具体可以看一下代码: -```java -public class NameViewModel extends ViewModel { - -// Create a LiveData with a String -private MutableLiveData mCurrentName; - - public MutableLiveData getCurrentName() { - if (mCurrentName == null) { - mCurrentName = new MutableLiveData(); - } - return mCurrentName; - } - -// Rest of the ViewModel... -} -``` - -观察`LiveData`中的数据 ---- - - -通常情况下都是在组件的`onCreate()`方法中开始观察数据,原因有以下两点: - -- 系统会多次调用`onResume()`方法 -- 确保`Activity/Fragment`在处于活跃状态时立刻可以展示数据。 - -```java -public class NameActivity extends AppCompatActivity { - - private NameViewModel mModel; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Other code to setup the activity... - - // Get the ViewModel. - mModel = ViewModelProviders.of(this).get(NameViewModel.class); - - - // Create the observer which updates the UI. - final Observer nameObserver = new Observer() { - @Override - public void onChanged(@Nullable final String newName) { - // Update the UI, in this case, a TextView. - mNameTextView.setText(newName); - } - }; - - // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer. - mModel.getCurrentName().observe(this, nameObserver); - } -} -``` - -更新`LiveData`对象 ---- - - -如果想要在`UI Controller`中改变`LiveData`中的值呢?(比如点击某个`Button`把性别从男设置成女)。`LiveData`并没有提供这样的功能,但是`Architecture Component`提供了`MutableLiveData`这样一个类,可以通过`setValue(T)`和`postValue(T)`方法来修改存储在`LiveData`中的数据。`MutableLiveData`是`LiveData`的一个子类,从名称上也能看出这个类的作用。举个直观点的例子: - -```java -mButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - String anotherName = "John Doe"; - mModel.getCurrentName().setValue(anotherName); - } -}) -``` -调用`setValue()`方法就可以把`LiveData`中的值改为`John Doe`。同样通过这种方法修改`LiveData`中的值同样会触发所有对这个数据感兴趣的类。那么`setValue()`和`postValue()`有什么不同呢?区别就是`setValue()`只能在主线程中调用,而`postValue()`可以在子线程中调用。 - - -`Room`和`LiveData`配合使用 ---- - -`Room`可以返回`LiveData`的数据类型。这样对数据库中的任何改动都会被传递出去。这样修改完数据库就能获取最新的数据,减少了主动获取数据的代码。 - -继承`LiveData`扩展功能 ---- - -`LiveData`的活跃状态包括:`STARTED`或者`RESUMED`两种状态。那么如何在活跃状态下把数据传递出去呢?下面是示例代码: - -```java -public class StockLiveData extends LiveData { - private StockManager mStockManager; - - private SimplePriceListener mListener = new SimplePriceListener() { - @Override - public void onPriceChanged(BigDecimal price) { - setValue(price); - } - }; - - public StockLiveData(String symbol) { - mStockManager = new StockManager(symbol); - } - - @Override - protected void onActive() { - mStockManager.requestPriceUpdates(mListener); - } - - @Override - protected void onInactive() { - mStockManager.removeUpdates(mListener); - } -} -``` - -上面有三个重要的方法: - -- The onActive() method is called when the LiveData object has an active observer. This means you need to start observing the stock price updates from this method. -- The onInactive() method is called when the LiveData object doesn't have any active observers. Since no observers are listening, there is no reason to stay connected to the StockManager service. -- The setValue(T) method updates the value of the LiveData instance and notifies any active observers about the change. - -可以像下面这样使用`StockLiveData`: -```java -public class MyFragment extends Fragment { - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - LiveData myPriceListener = ...; - myPriceListener.observe(this, price -> { - // Update the UI. - }); - } -} -``` -上面`observe()`方法中的第一个参数传递的是`fragment`的实例,该`fragment`实现了`LifecycleOwner`接口。这样做是为了将`observer`和`Lifecycle`对象绑定到一起,这意味着: -- 如果当前的`Lifecycle`对象不是出于活跃期,就算`value`值有改变也不会回调到`observer`中 -- 在`Lifecycle`对象销毁后哦,`observer`对象也会自动移除 - -实际上`LiveData`对象是适应生命周期也就意味着你需要在多个`activities`,`fragments`和`services`中进行共享,所以通常我们会将`LiveData`的示例设计成单例的: -```java -public class StockLiveData extends LiveData { - private static StockLiveData sInstance; - private StockManager mStockManager; - - private SimplePriceListener mListener = new SimplePriceListener() { - @Override - public void onPriceChanged(BigDecimal price) { - setValue(price); - } - }; - - @MainThread - public static StockLiveData get(String symbol) { - if (sInstance == null) { - sInstance = new StockLiveData(symbol); - } - return sInstance; - } - - private StockLiveData(String symbol) { - mStockManager = new StockManager(symbol); - } - - @Override - protected void onActive() { - mStockManager.requestPriceUpdates(mListener); - } - - @Override - protected void onInactive() { - mStockManager.removeUpdates(mListener); - } -} -``` -这样就可以在`fragment`中像如下这样使用: -```java -public class MyFragment extends Fragment { - @Override - public void onActivityCreated(Bundle savedInstanceState) { - StockLiveData.get(getActivity()).observe(this, price -> { - // Update the UI. - }); - } -} -``` - -转换LiveData ---- - -你可能有时会在`LiveData`分发给`observers`之前想要修改一下存储在`LiveData`中的值,或者你想根据当前的值进行修改返回另一个值。`Lifecycle`提供了`Transformations`类来通过里面的`helper`方法解决这种问题。 - -- `Transformations.map()` - -可以将`LiveData`中的数据进行改变。 - - -```java -LiveData userLiveData = ...; -LiveData userName = Transformations.map(userLiveData, user -> { - user.name + " " + user.lastName -}); -``` -将`LiveData`中的`User`数据转换成`String` - - -- `Transformations.switchMap()` - -```java -private LiveData getUser(String id) { - ...; -} - -LiveData userId = ...; -LiveData user = Transformations.switchMap(userId, id -> getUser(id) ); -``` - - -和上面的`map()`方法很像。区别在于传递给`switchMap()`的函数必须返回`LiveData`对象。 -和`LiveData`一样,`Transformation`也可以在观察者的整个生命周期中存在。只有在观察者处于观察`LiveData`状态时,`Transformation`才会运算。`Transformation`是延迟运算的(`calculated lazily`),而生命周期感知的能力确保不会因为延迟发生任何问题。 - -如果在`ViewModel`对象的内部需要一个`Lifecycle`对象,那么使用`Transformation`是一个不错的方法。举个例子:假如有个`UI`组件接受输入的地址,返回对应的邮政编码。那么可以 实现一个`ViewModel`和这个组件绑定: -```java -class MyViewModel extends ViewModel { - private final PostalCodeRepository repository; - public MyViewModel(PostalCodeRepository repository) { - this.repository = repository; - } - - private LiveData getPostalCode(String address) { - // DON'T DO THIS - return repository.getPostCode(address); - } -} - -``` - -看代码中的注释,有个`// DON'T DO THIS`(不要这么干),这是为什么?有一种情况是如果`UI`组件被回收后又被重新创建,那么又会触发一次`repository.getPostCode(address)`询,而不是重用上次已经获取到的查询。那么应该怎样避免这个问题呢?看一下下面的代码: - -```java -class MyViewModel extends ViewModel { - private final PostalCodeRepository repository; - private final MutableLiveData addressInput = new MutableLiveData(); - public final LiveData postalCode = - Transformations.switchMap(addressInput, (address) -> { - return repository.getPostCode(address); - }); - - public MyViewModel(PostalCodeRepository repository) { - this.repository = repository - } - - private void setInput(String address) { - addressInput.setValue(address); - } -} -``` - -`postalCode`变量的修饰符是`public`和`final`,因为这个变量的是不会改变的。哎?不会改变?那我输入不同的地址还总返回相同邮编?先打住,`postalCode`这个变量存在的作用是把输入的`addressInput`转换成邮编,那么只有在输入变化时才会调用`repository.getPostCode()`方法。这就好比你用`final`来修饰一个数组,虽然这个变量不能再指向其他数组,但是数组里面的内容是可以被修改的。绕来绕去就一点:当输入是相同的情况下,用了`switchMap()`可以减少没有必要的请求。并且同样,只有在观察者处于活跃状态时才会运算并将结果通知观察者。 - - - - -合并多个`LiveData`中的数据 ---- - -`MediatorLiveData`是`LiveData`的子类,可以通过`MediatorLiveData`合并多个`LiveData`来源的数据。同样任意一个来源的`LiveData`数据发生变化,`MediatorLiveData`都会通知观察他的对象。说的有点抽象,举个例子。比如`UI`接收来自本地数据库和网络数据,并更新相应的`UI`。可以把下面两个`LiveData`加入到`MeidatorLiveData`中: - -- 关联数据库的`LiveData` -- 关联联网请求的`LiveData` -相应的`UI`只需要关注`MediatorLiveData`就可以在任意数据来源更新时收到通知。 - - - -[上一篇: 3.Lifecycle(三)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/3.Lifecycle(%E4%B8%89).md) -[下一篇: 5.ViewModel(五)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/5.ViewModel(%E4%BA%94).md) - - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/ArchitectureComponents/5.ViewModel(\344\272\224).md" "b/ArchitectureComponents/5.ViewModel(\344\272\224).md" deleted file mode 100644 index 98ea2f37..00000000 --- "a/ArchitectureComponents/5.ViewModel(\344\272\224).md" +++ /dev/null @@ -1,138 +0,0 @@ -5.ViewModel(五) -=== - -`ViewModel`是用来存储`UI`层的数据,以及管理对应的数据,当数据修改的时候,可以马上刷新`UI`。 - -`Android`系统提供控件,比如`Activity`和`Fragment`,这些控件都是具有生命周期方法,这些生命周期方法被系统调用。 - -当这些控件被销毁或者被重建的时候,如果数据保存在这些对象中,那么数据就会丢失。比如在一个界面,保存了一些用户信息,当界面重新创建的时候,就需要重新去获取数据。当然了也可以使用控件自动再带的方法,在`onSaveInstanceState`方法中保存数据,在`onCreate`中重新获得数据,但这仅仅在数据量比较小的情况下。如果数据量很大,这种方法就不能适用了。 - -另外一个问题就是,经常需要在`Activity`中加载数据,这些数据可能是异步的,因为获取数据需要花费很长的时间。那么`Activity`就需要管理这些数据调用,否则很有可能会产生内存泄露问题。最后需要做很多额外的操作,来保证程序的正常运行。 - -同时`Activity`不仅仅只是用来加载数据的,还要加载其他资源,做其他的操作,最后`Activity`类变大,就是我们常讲的上帝类。也有不少架构是把一些操作放到单独的类中,比如`MVP`就是这样,创建相同类似于生命周期的函数做代理,这样可以减少`Activity`的代码量,但是这样就会变得很复杂,同时也难以测试。 - -`AAC`中提供`ViewModel`可以很方便的用来管理数据。我们可以利用它来管理`UI`组件与数据的绑定关系。`ViewModel`提供自动绑定的形式,当数据源有更新的时候,可以自动立即的更新`UI`。 - - -实现`ViewModel` ---- - -```java -public class MyViewModel extends ViewModel { - private MutableLiveData> users; - public LiveData> getUsers() { - if (users == null) { - users = new MutableLiveData>(); - loadUsers(); - } - return users; - } - - private void loadUsers() { - // Do an asynchronous operation to fetch users. - } -} -``` - -然后可以再`activity`像如下这样获取数据: -```java -public class MyActivity extends AppCompatActivity { - public void onCreate(Bundle savedInstanceState) { - // Create a ViewModel the first time the system calls an activity's onCreate() method. - // Re-created activities receive the same MyViewModel instance created by the first activity. - - MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class); - model.getUsers().observe(this, users -> { - // update UI - }); - } -} -``` - -在`activity`重建后,它会收到在第一个`activity`中创建的同一个`MyViewModel`实例,当所属的`activity`销毁后,`framework`会调用`ViewModel`对象的`onCleared()` -方法来清除资源。 - - -`ViewModel`的生命周期 ---- - -`ViewModel`在获取`ViewModel`对象时会通过`ViewModelProvider`的传递来绑定对应的声明周期。 -`ViewModel`只有在`Activity finish`或者`Fragment detach`之后才会销毁。 - - - - - - -在`Fragments`间分享数据 ---- - -有时候一个`Activity`中的两个或多个`Fragment`需要分享数据或者相互通信,这样就会带来很多问题,比如数据获取,相互确定生命周期。 - -使用`ViewModel`可以很好的解决这个问题。假设有这样两个`Fragment`,一个`Fragment`提供一个列表,另一个`Fragment`提供点击每个`item`现实的详细信息。 - - -```java -public class SharedViewModel extends ViewModel { - private final MutableLiveData selected = new MutableLiveData(); - - public void select(Item item) { - selected.setValue(item); - } - - public LiveData getSelected() { - return selected; - } -} - -public class MasterFragment extends Fragment { - private SharedViewModel model; - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class); - itemSelector.setOnClickListener(item -> { - model.select(item); - }); - } -} - -public class DetailFragment extends LifecycleFragment { - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class); - model.getSelected().observe(this, { item -> - // update UI - }); - } -} -``` - -两个`Fragment`都是通过`getActivity()`来获取`ViewModelProvider`。这意味着两个`Activity`都是获取的属于同一个`Activity`的同一个`ShareViewModel`实例。 -这样做优点如下: - -- `Activity`不需要写任何额外的代码,也不需要关心`Fragment`之间的通信。 -- `Fragment`不需要处理除`SharedViewModel`以外其他的代码。这两个`Fragment`不需要知道对方是否存在。 -- `Fragment`的生命周期不会相互影响 - - - - -`ViewModel`和`SavedInstanceState`对比 ---- - -`ViewModel`使得在`configuration change`(旋转屏幕等)保存数据变的十分方便,但是这不能用于应用被系统杀死时持久化数据。举个简单的例子,有一个界面展示国家信息。 -不应该把整个国家信息放到`SavedInstanceState`里,而是把国家对应的`id`放到`SavedInstanceState`,等到界面恢复时,再通过`id`去获取详细的信息。这些详细的信息应该被存放在数据库中。 - - - - - - -[上一篇: 4.LiveData(四)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/4.LiveData(%E5%9B%9B).md) -[下一篇: 6.Room(六)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/6.Room(%E5%85%AD).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/BasicKnowledge/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" "b/BasicKnowledge/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" index f5a864c6..319f5ef8 100644 --- "a/BasicKnowledge/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" +++ "b/BasicKnowledge/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" @@ -1,7 +1,7 @@ Android基础面试题 === -没有删这套题,虽然都是网上找的,在刚开始找工作的时候这套题帮了我很多,那时候`Android`刚起步,很多家都是这一套面试题,我都是直接去了不看题画画一顿就写完了,哈哈 +没有删这套题,虽然都是网上找的,在刚开始找工作的时候这套题帮了我很多,那时候`Android`刚起步,很多家都是这一套面试题,我都是直接去了不看题哗哗一顿就写完了,哈哈 现在估计没有公司会用这种笔试题了。还是留下来吧,回忆一下。 1. 下列哪些语句关于内存回收的说明是正确的? (b) diff --git "a/BasicKnowledge/Bitmap\344\274\230\345\214\226.md" "b/BasicKnowledge/Bitmap\344\274\230\345\214\226.md" index c127bf68..42b7954b 100644 --- "a/BasicKnowledge/Bitmap\344\274\230\345\214\226.md" +++ "b/BasicKnowledge/Bitmap\344\274\230\345\214\226.md" @@ -3,9 +3,9 @@ Bitmap优化 1. 一个进程的内存可以由2个部分组成:`native和dalvik` `dalvik`就是我们平常说的`java`堆,我们创建的对象是在这里面分配的,而`bitmap`是直接在`native`上分配的。 - 一旦内存分配给`Java`后,以后这块内存即使释放后,也只能给`Java`的使用,所以如果`Java`突然占用了一个大块内存, + 一旦内存分配给`Java`后,以后这块内存即使释放后,也只能给`Java`使用,所以如果`Java`突然占用了一个大块内存, 即使很快释放了,`C`能用的内存也是16M减去`Java`最大占用的内存数。 - 而`Bitmap`的生成是通过`malloc`进行内存分配的,占用的是`C`的内存,这个也就说明了,上述的`4MBitmap`无法生成的原因, + 而`Bitmap`的生成是通过`malloc`进行内存分配的,占用的是`C`的内存,这个也就说明了,有时候`4MBitmap`无法生成的原因, 因为在`13M`被`Java`用过后,剩下`C`能用的只有`3M`了。 2. 在`Android`应用里,最耗费内存的就是图片资源。 @@ -58,7 +58,7 @@ Bitmap优化 // 打印出图片的宽和高 Log.d("example", opts.outWidth + "," + opts.outHeight); ``` - 在实际项目中,可以利用上面的代码,先获取图片真实的宽度和高度,然后判断是否需要跑缩小。如果不需要缩小,设置inSampleSize的值为1。如果需要缩小,则动态计算并设置inSampleSize的值,对图片进行缩小。需要注意的是,在下次使用BitmapFactory的decodeFile()等方法实例化Bitmap对象前,别忘记将opts.inJustDecodeBound设置回false。否则获取的bitmap对象还是null。 + 在实际项目中,可以利用上面的代码,先获取图片真实的宽度和高度,然后判断是否需要缩小。如果不需要缩小,设置inSampleSize的值为1。如果需要缩小,则动态计算并设置inSampleSize的值,对图片进行缩小。需要注意的是,在下次使用BitmapFactory的decodeFile()等方法实例化Bitmap对象前,别忘记将opts.inJustDecodeBound设置回false。否则获取的bitmap对象还是null。 以从Gallery获取一个图片为例讲解缩放: ```java @@ -134,4 +134,4 @@ Bitmap优化 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/BasicKnowledge/Fragment\344\270\223\351\242\230.md" "b/BasicKnowledge/Fragment\344\270\223\351\242\230.md" index d3d89ad8..81ceb228 100644 --- "a/BasicKnowledge/Fragment\344\270\223\351\242\230.md" +++ "b/BasicKnowledge/Fragment\344\270\223\351\242\230.md" @@ -334,4 +334,4 @@ public abstract class BaseFragment extends Fragment { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/BasicKnowledge/Parcelable\345\217\212Serializable.md" "b/BasicKnowledge/Parcelable\345\217\212Serializable.md" index 1a77bc21..de220416 100644 --- "a/BasicKnowledge/Parcelable\345\217\212Serializable.md" +++ "b/BasicKnowledge/Parcelable\345\217\212Serializable.md" @@ -1,6 +1,15 @@ Parcelable及Serializable === + + +### Serializable + +在Java中Serializable接口是一个允许将对象转换为字节流(序列化)然后重新构造回对象(反序列化)的标记接口。 + +它会使用反射,并且会创建许多临时对象,导致内存使用率升高,并可能产生性能问题。 + + `Serializable`的作用是为了保存对象的属性到本地文件、数据库、网络流、`rmi`以方便数据传输, 当然这种传输可以是程序内的也可以是两个程序间的。而`Parcelable`的设计初衷是因为`Serializable`效率过慢, 为了在程序内不同组件间以及不同`Android`程序间(`AIDL`)高效的传输数据而设计,这些数据仅在内存中存在,`Parcelable`是通过`IBinder`通信的消息的载体。 @@ -9,6 +18,60 @@ Parcelable及Serializable 如`activity`间传输数据,而`Serializable`可将数据持久化方便保存,所以在需要保存或网络传输数据时选择 `Serializable`,因为`android`不同版本`Parcelable`可能不同,所以不推荐使用`Parcelable`进行数据持久化。 +Parcelable不同于将对象进行序列化,Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的数据类型,这样也就实现传递对象的功能了。 + + +### Parcelable实现 + +```kotlin +data class Developer(val name: String, val age: Int) : Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readInt() + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(name) + parcel.writeInt(age) + } + + // code removed for brevity + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): Developer { + return Developer(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} + +``` +上面是实现Parcelable的代码,可以看到有很多重复的代码。 +为了避免写这些重复的代码,可以使用kotlin-parcelize插件,并在类上使用@Parcelize注解。 + +```kotlin +@Parcelize +data class Developer(val name: String, val age: Int) : Parcelable +``` + +当在一个类上声明`@Parcelize`注解后,就会自动生成对应的代码。 + + + + + +Parcelable不会使用反射,并且在序列化过程中会产生更少的临时对象,这样就会减少垃圾回收的压力: + +- Parcelable不会使用反射 +- Parcelable是Android平台特定的接口 + +所以Parcelable比Serializable更快。 + + 区别: - Parcelable is faster than serializable interface - Parcelable interface takes more time for implemetation compared to serializable interface @@ -18,4 +81,4 @@ Parcelable及Serializable ---- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/BasicKnowledge/Scroller\347\256\200\344\273\213.md" "b/BasicKnowledge/Scroller\347\256\200\344\273\213.md" index 8ed68bfa..bfa12941 100644 --- "a/BasicKnowledge/Scroller\347\256\200\344\273\213.md" +++ "b/BasicKnowledge/Scroller\347\256\200\344\273\213.md" @@ -29,8 +29,8 @@ Scroller简介 public void computeScroll() { } ``` - 通过注释我们可以看到该方法又父类调用根据滚动的值去更新`View`,在使用`Scroller`的时候通常都要实现该方法。来达到子`View`的滚动效果。 - 继续往下跟发现在`draw()`方法中回去调用`computeScroll()`,而`draw()`方法会在父布局调用`drawChild()`的时候使用。 + 通过注释我们可以看到该方法由父类调用根据滚动的值去更新`View`,在使用`Scroller`的时候通常都要实现该方法。来达到子`View`的滚动效果。 + 继续往下跟发现在`draw()`方法中会去调用`computeScroll()`,而`draw()`方法会在父布局调用`drawChild()`的时候使用。 3. 具体关联 通过上面两步大体能得到`Scroller`与`View`的移动要通过`computeScroll()`来完成,但是在究竟如何进行代码实现。 diff --git "a/BasicKnowledge/\347\237\245\350\257\206\345\244\247\346\235\202\347\203\251.md" "b/BasicKnowledge/\347\237\245\350\257\206\345\244\247\346\235\202\347\203\251.md" index f9a082f8..f2fc9fa7 100644 --- "a/BasicKnowledge/\347\237\245\350\257\206\345\244\247\346\235\202\347\203\251.md" +++ "b/BasicKnowledge/\347\237\245\350\257\206\345\244\247\346\235\202\347\203\251.md" @@ -407,6 +407,11 @@ protected void setFullscreen(boolean on) { ``` +21.apk签名信息获取 +--- + +/Users/xxx/Library/Android/sdk/build-tools/30.0.3/apksigner verify --print-certs xxx.apk + --- - 邮箱 :charon.chui@gmail.com diff --git "a/Gradle&Maven/Composing builds\347\256\200\344\273\213.md" "b/Gradle&Maven/Composing builds\347\256\200\344\273\213.md" new file mode 100644 index 00000000..0c6ea44b --- /dev/null +++ "b/Gradle&Maven/Composing builds\347\256\200\344\273\213.md" @@ -0,0 +1,369 @@ +Composing builds简介 +=== + +在Android Studio项目中,经常会引用多个Module,而且会有多人同时参与项目开发,在这种背景下,会时常遇到版本冲突问题,出现不同的compileSdkVersion或者出现同一个库的多个不同版本等,导致我们的包体变大,项目运行时间变长,所以将依赖版本统一是一个项目优化的必经之路。 + +到目前为止Google为管理Gradle依赖提供了4种不同方法: + +- 手动管理 + + 在每个module中定义插件依赖库,每次升级依赖库时都需要手动更改(不建议使用)。 + +- ext的方式管理 + + 这是Google推荐管理依赖的方法 [Android官方文档](https://developer.android.com/studio/build/gradle-tips#configure-project-wide-properties)。但是无法跟踪依赖关系,可读性差,不便维护。 + +- Kotlin + buildSrc + + 支持自动补全和单击跳转,依赖更新时将重新构建整个项目。 + +- Composing builds + + 自动补全和单击跳转,依赖更新时不会重新构建整个项目。 + + + +## Groovy ext扩展函数的替代方式 + +我们在使用Groovy语言构建项目的时候,抽取config.gradle作为全局的变量控制,使用ext扩展函数来统一配置依赖,如下: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/config_gradle.png?raw=true) + +```groovy +ext { + android = [ + compileSdkVersion: 29, + buildToolsVersion: "29", + minSdkVersion : 17, + targetSdkVersion : 26, + versionCode : 102, + versionName : "1.0.2" + ] + + version = [ + appcompatVersion : "1.1.0", + coreKtxVersion : "1.2.0", + supportLibraryVersion : "28.0.0", + androidTestVersion : "3.0.1", + junitVersion : "4.12", + glideVersion : "4.11.0", + okhttpVersion : "3.11.0", + retrofitVersion : "2.3.0", + constraintLayoutVersion: "1.1.3", + gsonVersion : "2.7", + rxjavaVersion : "2.2.2", + rxandroidVersion : "2.1.0", + ..........省略........... + ] + + dependencies = [ + //base + "constraintLayout" : "androidx.constraintlayout:constraintlayout:${version["constraintLayoutVersion"]}", + "appcompat" : "androidx.appcompat:appcompat:${version["appcompatVersion"]}", + "coreKtx" : "androidx.core:core-ktx:${version["coreKtxVersion"]}", + "material" : "com.google.android.material:material:1.2.1", + + //multidex + "multidex" : "com.android.support:multidex:${version["multidexVersion"]}", + + //okhttp + "okhttp" : "com.squareup.okhttp3:okhttp:${version["okhttpVersion"]}", + "logging-interceptor" : "com.squareup.okhttp3:logging-interceptor:${version["okhttpVersion"]}", + + //retrofit + "retrofit" : "com.squareup.retrofit2:retrofit:${version["retrofitVersion"]}", + "converter-gson" : "com.squareup.retrofit2:converter-gson:${version["retrofitVersion"]}", + "adapter-rxjava2" : "com.squareup.retrofit2:adapter-rxjava2:${version["retrofitVersion"]}", + "converter-scalars" : "com.squareup.retrofit2:converter-scalars:${version["retrofitVersion"]}", + ..........省略........... + ] +} +``` + +依赖写完之后,在root路径下的build.gradle添加以下代码: + +```groovy +apply from: "config.gradle" +``` + +然后在需要依赖的module下的build.gradle中: + +```groovy +dependencies { + ... + // Retrofit + okhttp 相关的依赖包 + api rootProject.ext.dependencies["retrofit"] + ... +} +``` + +以上就是Groovy ext扩展函数的依赖管理方式,此方式可以做到版本依赖,但是最大的缺点就是无法跟踪代码,想要找到上面示例代码中的rootProject.ext.dependencies["retrofit"]这个依赖,需要手动切到config.gradle去搜索查找,可读性很差。 + + + +## buildSrc+kotlin + +Android Gradle插件4.0支持在Gradle构建配置中使用Kotlin脚本 (KTS),用于替代Groovy(过去在Gradle配置文件中使用的编程语言)。 + +将来,KTS会比Groovy更适合用于编写Gradle脚本,因为采用Kotlin编写的代码可读性更高,并且Kotlin提供了更好的编译时检查和IDE支持。 + +虽然与Groovy相比,KTS当前能更好地在Android Studio的代码编辑器中集成,但采用KTS的构建速度往往比采用Groovy慢,因此在迁移到KTS时应考虑构建性能。 + +KTS:是指Kotlin脚本,这是Gradle在构建配置文件中使用的一种[Kotlin语言形式](https://kotlinlang.org/docs/tutorials/command-line.html#run-scripts)。Kotlin脚本是[可从命令行运行](https://kotlinlang.org/docs/tutorials/command-line.html#using-the-command-line-to-run-scripts)的Kotlin代码。 + +Kotlin DSL:主要是指[Android Gradle插件Kotlin DSL](https://developer.android.com/reference/tools/gradle-api),有时也指[底层Gradle Kotlin DSL](https://guides.gradle.org/migrating-build-logic-from-groovy-to-kotlin/)。 + + + +摘自 [Gradle 文档](https://docs.gradle.org/current/userguide/organizing_gradle_projects.html#sec:build_sources):当运行Gradle时会检查根项目中是否存在一个名为buildSrc的目录,该目录包含了项目build相关的逻辑。然后Gradle会自动编译并测试这段代码,并将其放入构建脚本的类路径中, 对于多项目构建,只能有一个buildSrc目录,该目录必须位于根项目目录中,buildSrc是Gradle项目根目录下的一个目录,它可以包含我们的构建逻辑,与脚本插件相比,buildSrc应该是首选,因为它更易于维护、重构和测试代码。 + +buildSrc的方式,是最近几年特别流行的版本依赖管理方式。它有以下几个优点: + +- 支持双向跟踪 +- buildSrc是Android默认插件,全局只有这一个地方可以修改 +- 支持Android Studio的代码补全 + +使用方式可参考:[Kotlin + buildSrc for Better Gradle Dependency Management](https://handstandsam.com/2018/02/11/kotlin-buildsrc-for-better-gradle-dependency-management/) + +- 在项目根目录下新建一个名为buildSrc的文件夹( 名字必须是buildSrc,因为运行Gradle时会检查项目中是否存在一个名为buildSrc的目录 ) +- 在buildSrc文件夹里创建名为build.gradle.kts的文件,添加以下内容: + +```kotlin +plugins { + `kotlin-dsl` +} +repositories{ + jcenter +} +``` + +- 在buildSrc/src/main/java/包名/目录下新建Deps.kt文件,添加以下内容: + +```groovy +object Versions { + val appcompat = "1.1.0" +} + +object Deps { + val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}" +} +``` + +- 重启Android Studio,项目里就会多出一个名为buildSrc的module。 + + + + +缺点: + +- buildSrc 依赖更新将重新构建整个项目,项目越大,重新构建的时间就越长,造成不必要的时间浪费。 + + + +### 脚本文件命名 + +- 用Groovy编写的Gradle build文件使用.gradle文件扩展名。 +- 用Kotlin编写的Gradle build文件使用.gradle.kts文件扩展名。 + +### 常见误区 + +**用于定义字符串的双引号**。Groovy允许使用单引号来定义字符串,而Kotlin则要求使用双引号。 + +**基于句点表达式的字符串插值。**在Groovy中,您可以使用`$`前缀来表示基于句点表达式的字符串插值,例如以下代码段中的$project.rootDir: + +``` +myRootDirectory = "$project.rootDir/tools/proguard-rules-debug.pro" +``` + +但在Kotlin中,上述代码将对 `project`(而非 `project.rootDir`)调用 `toString()`。如需获取根目录的值,请使用大括号括住整个变量: + +- `myRootDirectory = "${project.rootDir}/tools/proguard-rules-debug.pro"` +- **变量分配**。一些在Groovy中适用的分配方式现在会被视作setter(或者,对于列表、集合等,则适用“addX”),因为属性在Kotlin中是只读的。 + +### 显式和隐式buildTypes + +在Kotlin DSL中,某些buildTypes(如debug和release)是隐式提供的。但是,其他buildTypes则必须手动创建。 + +例如,在Groovy中,您可能有debug、release和staging` `buildTypes: + +**Groovy** + +```groovy +buildTypes debug { ... } release { ... } staging { ... } +``` + +在KTS中,仅debug和release` `buildTypes是隐式提供的,而staging则必须由您手动创建: + +**KTS** + +``` +buildTypes getByName("debug") { ... } getByName("release") { ... } create("staging") { ... } +``` + +### 使用plugins代码块 + +如果您在build文件中使用plugins代码块,IDE将能够获知相关上下文信息,即使在构建失败时也是如此。IDE可使用这些信息执行代码补全并提供其他实用建议,从而帮助您解决KTS文件中存在的问题。 + +在您的代码中,将命令式apply plugin替换为声明式plugins代码块。Groovy中的以下代码: + +```groovy +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'androidx.navigation.safeargs.kotlin' +``` + +…在 KTS 中变为以下代码: + +```kotlin +plugins { + id("com.android.application") + id("kotlin-android") + id("kotlin-kapt") + id("androidx.navigation.safeargs.kotlin") +} +``` + +如需详细了解plugins代码块,请参阅 [Gradle 的迁移指南](https://docs.gradle.org/nightly/userguide/migrating_from_groovy_to_kotlin_dsl.html#applying_plugins)。 + + + + + +## Composing builds + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/composite_build.jpeg?raw=true) + + + +Composing builds:A composite build is simply a build that includes other builds. In many ways a composite build is similar to a Gradle multi-project build, except that instead of including single projects, complete builds are included. +复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于Gradle多项目构建,不同之处在于,它包括完整的builds,而不是包含单个projects: + +- 组合通常独立开发的构建,例如,在应用程序使用的库中尝试错误修复时 +- 将大型的多项目构建分解为更小,更孤立的块,可以根据需要独立或一起工作 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/buildsrc_composingbuild.png?raw=true) + +**使用方式** + +1. 新建module,名为versionPlugin(自起) +2. 在该module下的build.gradle文件中,添加如下代码: + +```groovy +buildscript { + ext.kotlin_version = "1.5.10" + repositories { + mavenCentral() + } + dependencies { + // 因为使用的Kotlin需要需要添加Kotlin插件 + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'kotlin' +apply plugin: 'java-gradle-plugin' + +repositories { + mavenCentral() +} + +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +gradlePlugin { + plugins { + version { + // 在app模块需要通过id引用这个插件 + id = 'com.xx.xx.plugin' + // 实现这个插件的类的路径 + implementationClass = 'com.xx.xx.versionplugin.Deps' + } + } +} +``` + +3. 在versionPlugin/src/main/java/包名/目录下新建Deps.kt文件,添加你的依赖配置,如: + +```kotlin +package com.xx.xx.versionplugin + +class Deps : Plugin { + override fun apply(project: Project) { + // Possibly common dependencies or can stay empty + } + + companion object { + val appcompat = "androidx.appcompat:appcompat:1.1.0" + } +} +``` + +或者也可以按依赖类型用不同的类配置,例如 + +```kotlin +object CustomLibs { + ... + object Glide { + private const val glideVersion = "4.11.0" + const val glide = "com.github.bumptech.glide:glide:$glideVersion" + const val glideCompiler = "com.github.bumptech.glide:compiler:$glideVersion" + } + + object Retrofit { + private const val retrofitVersion = "2.9.0" + const val retrofit = "com.squareup.retrofit2:retrofit:$retrofitVersion" + const val converter_gson = "com.squareup.retrofit2:converter-gson:$retrofitVersion" + } +} +``` + + + +4. 在settings.gradle文件内添加`includeBuild 'versionPlugin'`,注意是includeBuild哦~,Rebuild项目 + +5. 后面就可以在需要使用的gradle文件中使用了,在app或其他module下的build.gradle,在首行添加以下内容: + +```groovy +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' + // 通过id来使用该plugin,这个id就是在versionPlugin文件夹下build.gradle文件内定义的id + id 'com.xx.xx.plugin' +} +``` + +使用如下: + +```groovy +dependencies { + implementation CustomLibs.Glide.glide + kapt CustomLibs.Glide.glideCompiler +} +``` + + + + + +# 参考 + +- [Kotlin + buildSrc for Better Gradle Dependency Management](https://handstandsam.com/2018/02/11/kotlin-buildsrc-for-better-gradle-dependency-management/) +- [How to use Composite builds as a replacement of buildSrc in Gradle](https://medium.com/bumble-tech/how-to-use-composite-builds-as-a-replacement-of-buildsrc-in-gradle-64ff99344b58) +- [Stop using Gradle buildSrc. Use composite builds instead](https://proandroiddev.com/stop-using-gradle-buildsrc-use-composite-builds-instead-3c38ac7a2ab3) +- [再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度](https://juejin.cn/post/6844904176250519565) +- [composite_builds](https://docs.gradle.org/current/userguide/composite_builds.html) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" index 9be34a2a..253a8c33 100644 --- "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" +++ "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" @@ -1,26 +1,178 @@ Gradle专题 === -随着`Google`对`Eclipse`的无情抛弃以及`Studio`的不断壮大,`Android`开发者逐渐拜倒在`Studio`的石榴裙下。 -而作为`Studio`的默认编译方式,`Gradle`已逐渐普及。我最开始是被它的多渠道打包所吸引。关于多渠道打包,请看之前我写的文章[AndroidStudio使用教程(第七弹)][1] +作用 +--- + +[Gradle](https://docs.gradle.org/7.3.3/userguide/what_is_gradle.html)是一个开源的自动化构建工具。现在Android项目构建编译都是通过Gradle进行的。 + +Gradle的版本在`gradle/wrapper/gradle-wrapper.properties`下: +![image](https://github.com/CharonChui/Pictures/blob/master/gradle_version.png?raw=true) + +当前Gradle版本为6.7.1。当我们执行assembleDebug/assembleRelease编译命令的时候,Gradle就会开始进行编译构建流程。 + + +gradle-wrapper是对Gradle的一层包装,便于在团队开发过程中统一Gradle构建的版本号,这样大家都可以使用统一的Gradle版本进行构建。 +里面的distributionUrl属性是用于配置Gradle发行版压缩包的下载地址。 + +Gradle 是一个 运行在 JVM 的通用构建工具,其核心模型是一个由 Task 组成的有向无环图(Directed Acyclic Graphs). + + +![image](https://github.com/CharonChui/Pictures/blob/master/gradle_task_1.png?raw=true) -接下来我们就系统的学习一下`Gradle`。 简介 --- -`Gradle`是以`Groovy`语言为基础,面向`Java`应用为主。基于`DSL(Domain Specific Language)`语法的自动化构建工具。 +[Gradle](https://gradle.org/releases/)是以`Groovy`语言为基础,面向`Java`应用为主。 +基于`DSL(Domain Specific Language)`语法的自动化构建工具。 `Gradle`集合了`Ant`的灵活性和强大功能,同时也集合了`Maven`的依赖管理和约定,从而创造了一个更有效的构建方式。凭借`Groovy`的`DSL`和创新打包方式,`Gradle`提供了一个可声明的方式,并在合理默认值的基础上描述所有类型的构建。 `Gradle`目前已被选作许多开源项目的构建系统。 +[Groovy](http://www.groovy-lang.org/api.html)基于Java并拓展了Java。 +Java程序员可以无缝切换到使用Groovy开发程序。Groovy说白了就是把写Java程序变得像写脚本一样简单。写完就可以执行,Groovy内部会将其编译成Javaclass然后启动虚拟机来执行。 +当然,这些底层的渣活不需要你管。实际上,由于Groovy Code在真正执行的时候已经变成了Java字节码,所以JVM根本不知道自己运行的是Groovy代码。 + + 因为`Gradle`是基于`DSL`语法的,如果想看到`build.gradle`文件中全部可以选项的配置,可以看这里 [DSL Reference](http://google.github.io/android-gradle-dsl/current/) + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_build_process.png?raw=true) + + +说起来我们一直在使用Gradle,但仔细想想我们在项目中其实没有用gradle命令,而一般是使用gradlew命令,同时如下图所示,找遍整个项目,与gradle有关的就这两个文件夹,却只发现gradle-wrapper.jar。 + + +那么问题来了,gradlew是什么,gradle-wrapper.jar又是什么? + +wrapper的意思:包装。 + +那么可想而已,这是gradle的包装。其实是这样的,因为gradle处于快速迭代阶段,经常发布新版本,如果我们的项目直接去引用,那么更改版本等会变得无比麻烦。而且每个项目又有可能用不一样的gradle版本,这样去手动配置每一个项目对应的gradle版本就会变得麻烦,gradle的引入本来就是想让大家构建项目变得轻松,如果这样的话,岂不是又增加了新的麻烦? + +所以android想到了包装,引入gradle-wrapper,通过读取配置文件中gradle的版本,为每个项目自动的下载和配置gradle,就是这么简单。我们便不用关心如何去下载gradle,如何配置到项目中。 + +至于gradlew也是一样的道理,它共有两个文件,gradlew是在linux,mac下使用的,gradlew.bat是在window下使用的,提供在命令行下执行gradle命令的功能 + +至于为什么不直接执行Gradle,而是执行Gradlew命令呢? + +因为就像wrapper本身的意义,gradle命令行也是善变的,所以wrapper对命令行也进行了一层封装,使用同一的gradlew命令,wrapper会自动去执行具体版本对应的gradle命令。 + +同时如果我们配置了全局的gradle命令,在项目中如果也用gradle容易造成混淆,而gradlew明确就是项目中指定的gradle版本,更加清晰与明确 + + +### Gradle的生命周期 + +1. Initialization:初始化阶段 + - 解析整个工程中所有的Project,构建所有Project对应的project对象。 + - 初始化阶段执行项目目录下的settings.gradle脚本,用于判断哪些项目需要被构建,并且为对应项目创建Project对象。 + +2. Configuration配置阶段 + - 解析所有的project对象中的Task,构建所有Task的括扑图 + - 配置阶段的任务是执行各module下的build.gradle脚本,从而完成Project的配置,并且构建Task任务依赖关系图以便在执行阶段按照依赖关系执行Task。 + - 这个阶段Gradle会拉取remote repo的依赖(如果本地之前没有下载过依赖的话) +3. Execution执行阶段 + 执行具体的task以及其依赖的task +4. Build Finished + + + + + +Gradle与Android Studio的关系 +--- + +Gradle跟Android Studio其实没有关系,但是Gradle官方还是很看重Android开发的,Google在推出AS的时候选中了Gradle作为构建工具,为了支持Gradle +能在AS上使用,Google做了个AS的插件叫Android Gradle Plugin,所以我们能在AS上使用Gradle完全是因为这个插件的原因。 + +### AGP +AGP即Android Gradle Plugin,主要用于管理Android编译相关的Gradle插件集合,包括javac,kotlinc,aapt打包资源,D8/R8等都在AGP中。 + +AGP的版本是在根目录的build.gradle中配置的: +``` +dependencies { + classpath 'com.android.tools.build:gradle:4.2.1' +... +} +``` + +### AGP与Gradle的区别与联系 + +Gradle是构建工具,而AGP是管理Android构建的插件。可以理解为AGP是Gradle构建流程中重要的一环。 + +虽然AGP和Gradle不是一个纬度的事情,但是两者也在一定程度上有所关联:两者的版本号必须匹配上: https://developer.android.com/build/releases/gradle-plugin?hl=zh-cn#updating-gradle + + +![image](https://github.com/CharonChui/Pictures/blob/master/agp_gradle_version.png?raw=true) + + +既然Android编译是通过AGP实现的,AGP就是Gradle的插件,那么这个插件是什么时候被apply的内? +因为一个插件如果没有apply的话,那么压根不会执行的。 +``` +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' +} +``` +这就是AGP被apply的地方,也是区分一个module究竟是被打包成app还是一个library的地址。 + + + + + 基本的项目设置 --- 一个`Gradle`项目通过一个在项目根目录中的`build.gradle`文件来描述它的构建。 +# Gradle Java 构建入门 + +## Java 插件 + +如你所见,Gradle 是一个通用工具。它可以通过脚本构建任何你想要实现的东西,真正实现开箱即用。但前提是你需要在脚本中编写好代码才行。 + +大部分 Java 项目基本流程都是相似的:编译源文件,进行单元测试,创建 jar 包。使用 Gradle 做这些工作不必为每个工程都编写代码。Gradle 已经提供了完美的插件来解决这些问题。插件就是 Gradle 的扩展,简而言之就是为你添加一些非常有用的默认配置。Gradle 自带了很多插件,并且你也可以很容易的编写和分享自己的插件。Java plugin 作为其中之一,为你提供了诸如编译,测试,打包等一些功能。 + +Java 插件为工程定义了许多默认值,如Java源文件位置。如果你遵循这些默认规则,那么你无需在你的脚本文件中书写太多代码。当然,Gradle 也允许你自定义项目中的一些规则,实际上,由于对 Java 工程的构建是基于插件的,那么你也可以完全不用插件自己编写代码来进行构建。 + +后面的章节我们通过许多深入的例子介绍了如何使用 Java 插件来进行以来管理和多项目构建等。但在这个章节我们需要先了解 Java 插件的基本用法。 + +### 一个基本 Java 项目 + +来看一下下面这个小例子,想用 Java 插件,只需增加如下代码到你的脚本里。 + +### 采用 Java 插件 + +``` +build.gradle +apply plugin: 'java' +``` + +备注:示例代码可以在 Gralde 发行包中的 samples/java/quickstart 下找到。 + +定义一个 Java 项目只需如此而已。这将会为你添加 Java 插件及其一些内置任务。 + +> 添加了哪些任务? +> +> 你可以运行 gradle tasks 列出任务列表。这样便可以看到 Java 插件为你添加了哪些任务。 + +标准目录结构如下: + +``` +project + +build + +src/main/java + +src/main/resources + +src/test/java + +src/test/resources +``` + +Gradle 默认会从 `src/main/java` 搜寻打包源码,在 `src/test/java` 下搜寻测试源码。并且 `src/main/resources` 下的所有文件按都会被打包,所有 `src/test/resources` 下的文件 都会被添加到类路径用以执行测试。所有文件都输出到 build 下,打包的文件输出到 build/libs 下。 + + + ### 简单的`Build`文件 最简单的`Android`应用中的`build.gradle`都会包含以下几个配置: @@ -40,7 +192,7 @@ buildscript { } ``` `Module`中的`build.gradle`: - + ``` apply plugin: 'com.android.application' @@ -56,9 +208,8 @@ android { - `apply plugin : com.android.application`,声明使用`com.androdi.application`插件。这是构建`Android`应用所需要的插件。 - `android{...}`配置了所有`Android`构建时的参数。默认情况下,只有编译的目标版本以及编译工具的版本是需要的。 -重要: 这里只能使用`com.android.application`插件。如果使用`java`插件将会报错。 +重要: 这里只能使用`com.android.application`插件。如果使用`java`插件将会报错。目录结构 -### 目录结构 `module/src/main`下的目录结构,因为有时候很多人把`so`放到`libs`目录就会报错: - java/ @@ -93,6 +244,16 @@ android { 就像有些人就是要把`so`放到`libs`目录中(这类人有点犟),那就需要这样进行修改。 注意:因为在旧的项目结构中所有的源文件(`Java`,`AIDL`和`RenderScript`)都放到同一个目录中,我们需要将`sourceSet`中的这些新部件都设置给`src`目录。 + + +projects 和 tasks是 Gradle 中最重要的两个概念。 + +任何一个 Gradle 构建都是由一个或多个 projects 组成。每个 project 包括许多可构建组成部分。 这完全取决于你要构建些什么。举个例子,每个 project 或许是一个 jar 包或者一个 web 应用,它也可以是一个由许多其他项目中产生的 jar 构成的 zip 压缩包。一个 project 不必描述它只能进行构建操作。它也可以部署你的应用或搭建你的环境。不要担心它像听上去的那样庞大。 Gradle 的 build-by-convention 可以让您来具体定义一个 project 到底该做什么。 + +每个 project 都由多个 tasks 组成。每个 task 都代表了构建执行过程中的一个原子性操作。如编译,打包,生成 javadoc,发布到某个仓库等操作。 + +到目前为止,可以发现我们可以在一个 project 中定义一些简单任务,后续章节将会阐述多项目构建和多项目多任务的内容。 + Build Tasks --- @@ -108,6 +269,7 @@ Build Tasks - `assembleDebug` - `assembleRelease` + 提示:`Gradle`支持通过命令行执行任务首字母缩写的方式。例如: 在没有其他任务符合`aR`的前提下,`gradle aR`与`gradle assembleRelease`是相同的。 @@ -123,7 +285,7 @@ Build Tasks ### 基本的`Build`定制 `Android`插件提供了一些列的`DSL`来让直接从构建系统中做大部分的定制。 - + ##### `Manifest`整体部分 `DSL`提供了很多重要的配置`manifest`文件的参数,例如: @@ -185,9 +347,9 @@ android { - 设置了它的`applicationId`。这样`debug`模式就能与`release`模式的`apk`同时安装在同一手机上。 - 创建了一个新的`jnidebug`的`Build Type`,并且把它设置为`debug`的拷贝。 - 通过允许`JNI`组件的`debug`和增加一个新的包名后缀来继续定制该`Build Type`。 - + 不管使用`initWith()`还是使用其他的代码块,创建一个新的`Build Types`都是非常简单的在`buildTypes`代码块中创建一个新的元素就可以了。 - + ##### 签名配置 为应用签名需要使用如下几个部分: @@ -290,7 +452,188 @@ dependencies { } ``` -##### 多项目设置 + + +Gradle支持三种不同的仓库,分别是:Maven和Ivy以及文件夹。依赖包会在你执行build构建的时候从这些远程仓库下载,当然Gradle会为你在本地保留缓存,所以一个特定版本的依赖包只需要下载一次。 + +一个依赖需要定义三个元素:group,name和version。group意味着创建该library的组织名,通常这会是包名,name是该library的唯一标示。version是该library的版本号,我们来看看如何申明依赖: + +``` +dependencies { + compile 'com.google.code.gson:gson:2.3' + compile 'com.squareup.retrofit:retrofit:1.9.0' +} +``` + +上述的代码是基于groovy语法的,所以其完整的表述应该是这样的: + +``` +dependencies { + compile group: 'com.google.code.gson', name: 'gson', version:'2.3' + compile group: 'com.squareup.retrofit', name: 'retrofit' + version: '1.9.0' + } + +``` + +### 为你的仓库预定义 + +为了方便,Gradle会默认预定义三个maven仓库:Jcenter和mavenCentral以及本地maven仓库。你可以同时申明它们: + +``` +repositories { + mavenCentral() + jcenter() + mavenLocal() + } + +``` + +Maven和Jcenter仓库是很出名的两大仓库。我们没必要同时使用他们,在这里我建议你们使用jcenter,jcenter是maven中心库的一个分支,这样你可以任意去切换这两个仓库。当然jcenter也支持了https,而maven仓库并没有。 + +本地maven库是你曾使用过的所有依赖包的集合,当然你也可以添加自己的依赖包。默认情况下,你可以在你的home文件下找到.m2的文件夹。除了这些仓库外,你还可以使用其他的公有的甚至是私有仓库。 + + + +### 远程仓库 + +有些组织,创建了一些有意思的插件或者library,他们更愿意把这些放在自己的maven库,而不是maven中心库或jcenter。那么当你需要是要这些仓库的时候,你只需要在maven方法中加入url地址就好: + +``` +repositories { + maven { + url "http://repo.acmecorp.com/maven2" + } +} +``` + +同样的,Ivy仓库也可以这么做。Apache Ivy在ant世界里是一个很出名的依赖管理工具。如果你的公司有自己的仓库,如果他们需要权限才能访问,你可以这么编写: + +``` +repositories { + maven { + url "http://repo.acmecorp.com/maven2" + credentials { + username 'user' + password 'secretpassword' + } + } + } +``` + +> 注意:这不是一个好主意,最好的方式是把这些验证放在Gradle properties文件里,这些我们已经介绍过在第二章。 + +## 本地依赖 + +可能有些情况,你需要手动下载jar包,或者你想创建自己的library,这样你就可以复用在不同的项目,而不必将该library publish到公有或者私有库。在上述情况下,可能你不需要网络资源,接下来我将介绍如何是使用这些jar依赖,以及如何导入so包,如何为你的项目添加依赖项目。 + +### 文件依赖 + +如果你想为你的工程添加jar文件作为依赖,你可以这样: + +``` +dependencies { + compile files('libs/domoarigato.jar') +} +``` + +如果你这么做,那会很愚蠢,因为当你有很多这样的jar包时,你可以改写为: + +``` +dependencies { + compile fileTree('libs') + } + +``` + +默认情况下,新建的Android项目会有一个lib文件夹,并且会在依赖中这么定义(即添加所有在libs文件夹中的jar): + +``` +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) +} +``` + +这也意味着,在任何一个Android项目中,你都可以把一个jar文件放在到libs文件夹下,其会自动的将其添加到编译路径以及最后的APK文件。 + +### native包(so包) + +用c或者c++写的library会被叫做so包,Android插件默认情况下支持native包,你需要把.so文件放在对应的文件夹中: + +``` +app + ├── AndroidManifest.xml + └── jniLibs + ├── armeabi + │ └── nativelib.so + ├── armeabi-v7a + │ └── nativelib.so + ├── mips + │ └── nativelib.so + └── x86 + └── nativelib.so + +``` + +## aar文件 + +如果你想分享一个library,该依赖包使用了Android api,或者包含了Android 资源文件,那么aar文件适合你。依赖库和应用工程是一样的,你可以使用相同的tasks来构建和[测试](http://lib.csdn.net/base/softwaretest)你的依赖工程,当然他们也可以有不同的构建版本。应用工程和依赖工程的区别在于输出文件,应用工程会生成APK文件,并且其可以安装在Android设备上,而依赖工程会生成.aar文件。该文件可以被Android应用工程当做依赖来使用。 + +### 创建和使用依赖工程模块 + +不同的是,你需要加不同的插件: + +``` + apply plugin: 'com.android.library' + +``` + +我们有两种方式去使用一个依赖工程。一个就是在你的工程里面,直接将其作为一个模块,另外一个就是创建一个aar文件,这样其他的应用也就可以复用了。 + +如果你把其作为模块,那你需要在settings.gradle文件中添加其为模块: + +``` + include ':app', ':library' + +``` + +在这里,我们就把它叫做library吧,如果你想使用该模块,你需要在你的依赖里面添加它,就像这样: + +``` + dependencies { + compile project(':library') + } +``` + +### 使用aar文件 + +如果你想复用你的library,那么你就可以创建一个aar文件,并将其作为你的工程依赖。当你构建你的library项目,aar文件将会在 build/output/aar/下生成。把该文件作为你的依赖包,你需要创建一个文件夹来放置它,我们就叫它aars文件夹吧,然后把它拷贝到该文件夹里面,然后添加该文件夹作为依赖库: + +``` +repositories { + flatDir { + dirs 'aars' + } +} +``` + +这样你就可以把该文件夹下的所有aar文件作为依赖,同时你可以这么干: + +``` + dependencies { + compile(name:'libraryname', ext:'aar') +} +``` + +这个会告诉Gradle,在aars文件夹下,添加一个叫做libraryname的文件,且其后缀是aar的作为依赖。 + + + + + + + +##### 多项目设置 `Gradle`项目通常使用多项目设置来依赖其他的`gradle`项目。例如: @@ -300,6 +643,7 @@ dependencies { - lib1/ - lib2/ + `Gradle`会通过下面的名字来引用他们: `:app` `:libraries:lib1` @@ -317,7 +661,7 @@ dependencies { - build.gradle + lib2/ - build.gradle - + `setting.gradle`文件中的内容非常简单。它指定了哪个目录是`Gralde`项目: ``` @@ -366,7 +710,7 @@ android { `Library`项目的主要输出我`.aar`包。它结合了代码(例如`jar`包或者本地`.so`文件)和资源(`manifest`,`res`,`assets`)。每个`library`也可以单独设置`Build Type`等来指定生成不同版本的`aar`。 ### `Lint Support` - + 你可以通过指定对应的变量来设置`lint`的运行。可以通过添加`lintOptions`来进行配置: ``` @@ -461,7 +805,7 @@ android { - `minSdkVersion`: 14 - `versionCode`: 10 - + 通常,`Build Type`配置会覆盖其他的配置。例如,`Build Type`的`applicationIdSuffix`会添加到`Product Flavor`的`applicationId`上。 @@ -518,7 +862,7 @@ android { } } ``` - + ### `Tasks`控制 基本的`Java`项目有一系列的`tasks`一起制作输出文件。 @@ -553,7 +897,7 @@ android { ### `Resource Shrinking` `Gradle`构建系统支持资源清理:对构建的应用会自动移除无用的资源。不仅会移除项目中未使用的资源,而且还会移除项目所以来的类库中的资源。注意,资源清理只能在与代码清理结合使用(例如`ProGuad`)。这就是为什么它能移除所依赖类库的无用资源。通常,类库中的所有资源都是使用的,只有类库中无用代码被移除后这些资源才会变成没有代码引用的无用资源。 - + ``` android { ... @@ -568,10 +912,1221 @@ android { } ``` -[1]: https://github.com/CharonChui/AndroidNote/blob/master/AndroidStudioCourse/AndroidStudio%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B(%E7%AC%AC%E4%B8%83%E5%BC%B9).md "AndroidStudio使用教程(第七弹)" - + +## Task + +在Gradle中有一个原子性的操作叫做task,简单理解为task是Gradle脚本中最小可执行单元。 + +在build.gradle里可以通过task关键字来创建Task。 + +例如,可以在build.gradle中创建2个task: + +```groovy +task helloWorld { + println "Hello World" +} +task myTask2 { + println "configure task2" +} +``` + +在命令行里执行命令: gradle helloWorld: + +``` +Hello World +configure task2 +``` + +我们会发现当我们执行helloWorld时,task2的代码也被执行了。括号内部的代码我们称之为配置代码,在gradle脚本的配置阶段都会执行,也就是说不管执行脚本里的哪个任务,所有task里的配置代码都会执行。 + +这与我们期望的不一致,通常我们写程序时调用一个方法,这个方法里的代码才会执行,那么我们执行一个task时,这个task里的代码才会被执行才对。显然Gradle里的不一样,这个问题就设计到Task Action的概念。 + + + +## Task Action + +一个Task由一系列Action组成,当运行一个Task的时候,这个Task里的Action序列会按顺序依次执行。 + +前面例子中括号里的代码只是配置代码,它们并不是Action,Task里的Action只会在该Task真正运行时执行,Gradle里通过doFirst、doLast来为Task增加Action。 + +- doFirst: task执行时最先执行的操作 +- doLast: task执行时最后执行的操作 + +``` +task myTask1 { + println "configure task1" +} +task myTask2 { + println "configure task2" +} +myTask1.doFirst { + println "task1 doFirst" +} +myTask1.doLast { + println "task1 doLast" +} +myTask2.doLast { + println "task2 doLast" +} +``` + +同样运行gradle myTask1,来执行myTask1,这次的结果如下: + +``` +configure task1 +configure task2 + +task1 doFirst +task1 doLast +``` + +可以看到所有Task的配置代码都会运行,而Task Action则只有该Task运行时才会执行。 + +doLast有一种等价操作叫做leftShift,leftShift可以缩写为<<,下面几种写法效果是一模一样的: + +``` +myTask1.doLast { + println "task1 doLast" +} +myTask1 << { + println "task1 doLast <<" +} +myTask1.leftShift { + println "task1 doLast leftShift" +} +``` + +<<操作符只是一种Gradle里的语法糖 + +## Extension + +先来看一段Android应用的Gradle配置代码: + +```groovy +android { + compileSdkVersion 26 + defaultConfig { + applicationId "xxx" + minSdkVersion 19 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} +``` + +上面这个android打包配置就是Gradle的Extension,翻译成中文的意思就是扩展。它的作用就是通过实现自定义的Extension,可以在Gradle脚本中增加类似android这样命名空间的配置,Gradle可以识别这种配置,并读取里面的配置内容。 + +每个Extension实际上都会与某个类相关联,在build.gradle中通过DSL来定义,Gradle会识别解析并生成一个对象实例,通过该类可以获取我们所配置的信息。 + +```groovy +outer { + outerName "outer" + msg "this is a outer message." + + inner { + innerName "inner" + msg "This is a inner message." + } +} +``` + +形式上就是外面的Extension里面定义了另一个Extension,这种叫做nested Extension,也就是嵌套的 Extension。 + +## Project详解 + +每一个build.gradle脚本文件被Gradle加载解析后,都会生成一个对应的Project对象,在脚本中的配置方法其实都对应着Project中的API,如果想详细了解这些脚本的配置含义,有必要对Project类进行深入的了解。 + + + +### Project类图 + +当构建进程启动后,Gradle基于build.gradle中的配置实例化org.gradle.api.Project类: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/build_gradle_project.png?raw=true) + +#### 构建脚本配置 + +##### buildscript + +配置该Project的构建脚本的classpath,在Android Studio中的root project中可以看到: + +```groovy +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.0.1' + } +} +``` + + + +##### apply + +```groovy +apply(options: Map) +``` + +我们通过该方法使用插件或者是其他脚本,options里主要选项有: + +- from: 使用其他脚本,值可以为Project.uri(Object) 支持的路径 +- plugin:使用其他插件,值可以为插件id或者是插件的具体实现类 + +例如: + +```groovy +//使用插件,com.android.application 就是插件id +apply plugin: 'com.android.application' +//使用插件,MyPluginImpl 就是一个Plugin接口的实现类 +apply plugin: MyPluginImpl + +//引用其他gradle脚本,push.gradle就是另外一个gradle脚本文件 +apply from: './push.gradle' +``` + +### Gradle属性 + + + +在与build.gradle文件同级目录下,定义一个名为gradle.properties的文件,里面定义的键值对,可以在Project中直接访问: + +```groovy +// gradle.properties +username="xxx" +password="yyy" +``` + +在build.gradle文件里可以直接访问: + +```groovy +println "username = ${username}" +println "password = ${password}" +``` + +### 扩展属性 + + + +在一个build.gradle里,可以通过变量定义来实现相关字符串的替换,比如: + + apply plugin: 'com.android.application' + + def mCompileSdkVersion = 28 + def libAndroidAppcompat = 'com.android.support:appcompat-v7:28.0.0' + android { + compileSdkVersion mCompileSdkVersion + } + + dependencies { + implementation libAndroidAppcompat + } + +gradle支持扩展属性,通过扩展属性也可以达到上述的目的: + + apply plugin: 'com.android.application' + + // 扩展属性 + ext { + compileSdkVersion = 28 + libAndroidAppcompat = 'com.android.support:appcompat-v7:28.0.0' + } + + android { + compileSdkVersion this.compileSdkVersion + } + + dependencies { + implementation this.libAndroidAppcompat + } + +为每个子工程配置扩展属性,在根build.gradle中添加如下代码,子工程相关配置删除。 + + subprojects { + ext { + compileSdkVersion = 28 + libAndroidAppcompat = 'com.android.support:appcompat-v7:28.0.0' + } + } + +上述方法相当于每个子project定义了扩展属性,如果想定义一份,需要把根build.gradle改成如下: + + ext { + compileSdkVersion = 28 + libAndroidAppcompat = 'com.android.support:appcompat-v7:28.0.0' + } + +子工程build.gradle改成如下: + + apply plugin: 'com.android.application' + + android { + compileSdkVersion this.rootProject.compileSdkVersion + } + + dependencies { + implementation this.rootProject.libAndroidAppcompat + } + +如果把rootProject去掉,也是可以的 + + apply plugin: 'com.android.application' + + android { + compileSdkVersion this.compileSdkVersion + } + + dependencies { + implementation this.libAndroidAppcompat + } + +gradle规定,父project所有的属性都会被根project继承,所以可以直接在子project使用父project的属性。 +可以把所有的扩展属性定义到一个独立的gradle文件中,在需要使用的build.gradle文件中使用apply from进行引入。 + +``` +apply from : file('common.gradle') +``` + +修改根目录build.gradle文件如下: + +```groovy +println "-----root file config-----" + +//配置 app 项目 +project(":app") { + ext { + appParam = "test app" + } +} + +//配置所有的项目 +allprojects { + ext { + allParam = "test all project" + } +} + +//配置子项目 +subprojects { + ext { + subParam = "test sub project" + } +} + +println "allParam = ${allParam}" +``` + + + + + +还可以通过ext命名空间来定义属性,我们称之为扩展属性。 + +```groovy +ext { + username = "hjy" + age = 30 +} + +println username +println ext.age +println project.username +println project.ext.age +``` + +必须注意,默认的扩展属性,只能定义在 ext 命名空间下面。对扩展属性的访问方式,以上几种都支持。 + + + +如果你有新建一个kotlin项目的经历,那么你将看到Google推荐的方案 + +``` +buildscript { + ext.kotlin_version = '1.1.51' + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.0.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} +``` + +在rootProject的build.gradle中使用**ext**来定义版本号全局变量。这样我们就可以在module的build.gradle中直接引用这些定义的变量。引用方式如下: + +``` +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" +} +``` + +你可以将这些变量理解为java的静态变量。通过这种方式能够达到不同module中的配置统一,但局限性是,一但配置项过多,所有的配置都将写到rootProject项目的build.gradle中,导致build.gradle臃肿。这不符合我们的所提倡的模块开发,所以应该想办法将ext的配置单独分离出来。 + +这个时候我就要用到之前的文章[Android Gradle系列-原理篇](https://mp.weixin.qq.com/s?__biz=MzIzNTc5NDY4Nw==&mid=2247483834&idx=1&sn=55264aaad1f018b55280beec93ed4cac&chksm=e8e0f82adf97713c5a43c67b67fbabd659578328a22a406c5a01bd69ccf550e88bf645b15457&token=330677494&lang=zh_CN#rd)中所介绍的apply函数。之前的文章我们只使用了apply三种情况之一的plugin(应用一个插件,通过id或者class名),只使用在子项目的build.gradle中。 + +``` +apply plugin: 'com.android.application' +``` + +这次我们需要使用它的**from**,它主要是的作用是**应用一个脚本文件**。作用接下来我们需要做的是将ext配置单独放到一个gradle脚本文件中。 + +首先我们在rootProject目录下创建一个gradle脚本文件,我这里取名为version.gradle。 + +然后我们在version.gradle文件中使用ext来定义变量。例如之前的kotlin版本号就可以使用如下方式实现 + +``` +ext.deps = [:] + +def versions = [:] +versions.support = "26.1.0" +versions.kotlin = "1.2.51" +versions.gradle = '3.2.1' + +def support = [:] +support.app_compat = "com.android.support:appcompat-v7:$versions.support" +support.recyclerview = "com.android.support:recyclerview-v7:$versions.support" +deps.support = support + +def kotlin = [:] +kotlin.kotlin_stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin" +kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" +deps.kotlin = kotlin + +deps.gradle_plugin = "com.android.tools.build:gradle:$versions.gradle" + +ext.deps = deps + +def build_versions = [:] +build_versions.target_sdk = 26 +build_versions.min_sdk = 16 +build_versions.build_tools = "28.0.3" +ext.build_versions = build_versions + +def addRepos(RepositoryHandler handler) { + handler.google() + handler.jcenter() + handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } +} +ext.addRepos = this.&addRepos +``` + +> 因为gradle使用的是groovy语言,所以以上都是groovy语法 + +例如kotlin版本控制,上面代码的意思就是将有个kotlin相关的版本依赖放到deps的kotlin变量中,同时deps放到了ext中。其它的亦是如此。 + +既然定义好了,现在我们开始引入到项目中,为了让所有的子项目都能够访问到,我们使用**apply from**将其引入到rootProject的build.gradle中 + +``` +buildscript { + apply from: 'versions.gradle' + addRepos(repositories) + dependencies { + classpath deps.gradle_plugin + classpath deps.kotlin.plugin + } +} +``` + +这时build.gradle中就默认有了ext所声明的变量,使用方式就如dependencies中的引用一样。 + +我们再看上面的addRepos方法,在关于Gradle原理的文章中已经分析了repositories会通过RepositoryHandler来执行,所以这里我们直接定义一个方法来统一调用RepositoryHandler。这样我们在build.gradle中就无需使用如下方式,直接调用addRepos方法即可 + +``` + //之前调用 + repositories { + google() + jcenter() + } + //现在调用 + addRepos(repositories) +``` + +另一方面,如果有多个module,例如有module1,现在就可以直接在module1中的build.gradle中使用定义好的配置 + +``` +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + // support + implementation deps.support.app_compat + //kotlin + implementation deps.kotlin.kotlin_stdlib +} +``` + +上面我们还定义了sdk与tools版本,所以也可以一起统一使用,效果如下 + +``` +android { + compileSdkVersion build_versions.target_sdk + buildToolsVersion build_versions.build_tools + defaultConfig { + applicationId "com.idisfkj.androidapianalysis" + minSdkVersion build_versions.min_sdk + targetSdkVersion build_versions.target_sdk + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + ... +} +``` + +一旦实现了统一配置,那么之后我们要修改相关的版本就只需在我们定义的version.gradle中修改即可。无需再对所用的module进行逐一修改与统一配置。 + + + +扩展属性也可以定义在gradle.properties中,在这个文件中只能定义key-value形式的扩展属性,而不能使用类似Map方式的定义,在使用上有一定的限制。 +下面通过在gradle.properties定义开关控制一个模块是否引入项目中,在gradle.properties中定义: + +isLoadTest = false + +setting.gradle: + + include ':app', ':module1', ':module2' + if(hasProperty('isLoadTest') ? isLoadTest.toBoolean() : false) { + include 'test' + } + +所以gradle.properties也可以定义扩展属性,在使用的时候转换成对应的类型。 +例如在gradle.properties定义: + +mCompileSdkVersion = 28 + +使用的时候: + +compileSdkVersion mCompileSdkVersion.toInteger() + +在gradle.properties定义的属性不能和build.gradle已经存在的方法同名,否则编译的时候不报错,但是在使用时会提示属性找不到。 + + + +### 多模块构建的结构 + +通常情况下,一个工程包含多模块,这些模块会在一个父目录文件夹下。为了告诉gradle,该项目的结构以及哪一个子文件夹包含模块,你需要提供一个settings.gradle文件。每个模块可以提供其独立的build.gradle文件。我们已经学习了关于setting.gradle和build.gradle如何正常工作,现在我们只需要学习如何使用它们。 + +这是多模块项目的结构图: + +``` + project + ├─── setting.gradle + ├─── build.gradle + ├─── app + │ └─── build.gradle + └─── library + └─── build.gradle + +``` + +这是最简单最直接的方式来创建你的多模块项目了。setting.gradle文件申明了该项目下的所有模块,它应该是这样: + +``` +include ':app', ':library' +``` + +这保证了app和library模块都会包含在构建配置中。你需要做的仅仅只是为你的模块添加子文件夹。 + +为了在你的app模块中添加library模块做为其依赖包,你需要在app的build.gradle文件中添加以下内容: + +``` +dependencies { + compile project(':library') +} +``` + +为了给app添加一个模块作为依赖,你需要使用project()方法,该方法的参数为模块路径。 + +如果在你的模块中还包含了子模块,gradle可以满足你得要求。举个栗子,你可以把你的目录结构定义为这样: + +``` +project +├─── setting.gradle +├─── build.grade +├─── app +│ └─── build.gradle +└─── libraries + ├─── library1 + │ └─── build.gradle + └─── library2 + └─── build.gradle + +``` + +该app模块依然位于根目录,但是现在项目有2个不同的依赖包。这些依赖模块不位于项目的根目录,而是在特定的依赖文件夹内。根据这一结构,你需要在settings.xml中这么定义: + +``` +include ':app', ':libraries:library1', ':libraries:library2' +``` + +你会注意到在子目录下申明模块也非常的容易。所有的路径都是围绕着根目录,即当你添加一个位于子文件夹下的模块作为另外一个模块的依赖包得实惠,你应该将路径定为根目录。这意味着如果在上例中app模块想要依赖library1,build.gradle文件需要这么申明: + +``` +dependencies { + compile project(':libraries:library1') +} +``` + +如果你在子目录下申明了依赖,所有的路径都应该与根目录相关。这是因为gradle是根据你的项目的根目录来定义你的依赖包的。 + + + +## 构建的三个构建阶段 + +1. Initialization:配置构建环境以及有哪些Project会参与构建(解析settings.build) +2. Configuration:生成参与构建的Task的有向无环图以及执行属于配置阶段的代码(解析build.gradle) +3. Execution:按序执行所有Task + +在第一步骤中,即初始化阶段,gradle会寻找到settings.grade文件。如果该文件不存在,那么gradle就会假定你只有一个单独的构建模块。如果你有多个模块,settings.gradle文件定义了这些模块的位置。如果这些子目录包含了其自己的build.gradle文件,gradle将会运行它们,并且将他们合并到构建任务中。这就解释了为什么你需要申明在一个模块中申明的依赖是相对于根目录。 + +一旦你理解了构建任务是如何将所有的模块聚合在一起的时候,那关于几种不同的构建多模块策略就会变得简单易懂。你可以配置所有的模块在根目录下的build.gradle。这让你能够简单的浏览到整个项目的配置,但是这将会变得一团乱麻,特别是当你的模块需要不同的插件的时候。另外一种方式是将每个模块的配置分隔开,这一策略保证了每个模块之间的互不干扰。这也让你跟踪构建的改变变得容易,因为你不需要指出哪个改变导致了哪个模块出现错误等。 + + + +#### 为你的项目添加模块 + + + +#### 添加Java依赖库 + +当你新建了一个Java模块,build.grade文件会是这样: + +``` +apply plugin: 'java' + dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) +} + +``` + +Java模块使用了Java插件,这意味着很多Android特性在这儿不能使用,因为你不需要。 + +build文件也有基本的库管理,你可以添加jar文件在libs文件夹下。你可以添加更多的依赖库,根据第三章的内容。 + +给你的app模块添加Java模块,这很简单,不是吗? + +``` +dependencies { + compile project(':javalib') +} +``` + +这告诉了gradle去引入一个叫做javelin的模块吧,如果你为你的app模块添加了这个依赖,那么javalib模块将会总是在你的app模块构建之前构建。 + +#### 添加Android依赖库 + +同样的,我们利用Android studio的图形化界面创建Android模块,然后其构建文件如下: + +``` +apply plugin: 'com.android.library' +``` + +记住:Android依赖库不仅仅包含了Java代码,同样也会包含Android资源,像manifest和strings,layout文件,在你引入该模块后,你可以使用该模块的所有类和资源文件。 + + + + + +## Groovy + +在Java中,打印一天String应该是这样的: + +``` +System.out.println("Hello, world!"); +``` + +在Groovy中,你可以这么写: + +``` +println 'Hello, world!' +``` + +你应该主要到几点不同之处: + +- 没有了System.out +- 没有了方括号 +- 列结尾没有了; + +这个例子同样使用了单引号,你可以使用双引号或者单引号,但是他们有不同的用法。双引号可以包含插入语句。插入是计算一个字符串包含placeholders的过程,并将placeholders的值替换,这些placeholder可以是变量甚至是方法。Placeholders必须包含一个方法或者变量,并且其被{}包围,且其前面有$修饰。如果其只有一个单一的变量,可以只需要$。下面是一些基本的用法: + +``` +def name = 'Andy' +def greeting = "Hello, $name!" +def name_size "Your name is ${name.size()} characters long." +``` + +greeting应该是“ Hello,Andy”,并且 name_size 为 Your name is 4 characters long.string的插入可以让你更好的动态执行代码。比如 + +``` + def method = 'toString' + new Date()."$method"() +``` + +这在Java中看起来很奇怪,但是这在groovy里是合法的。 + + + +Groovy里面创建类和Java类似,举个例子: + +``` +class MyGroovyClass { + String greeting + String getGreeting() { + return 'Hello!' + } +} +``` + +注意到不论是类名还是成员变量都没有修饰符。其默认的修饰符是类和方法为public,成员变量为private。 + +当你想使用MyGroovyClass,你可以这样实例化: + +``` +def instance = new MyGroovyClass() +instance.setGreeting 'Hello, Groovy!' +instance.getGreeting() +``` + +你可以利用def去创建变量,一旦你为你的类创建了实例,你就可以操作其成员变量了。get/set方法groovy默认为你添加 。你甚至可以覆写它。 + +如果你想直接使用一个成员变量,你可以这么干: + +``` + println instance.getGreeting() + println instance.greeting + +``` + +而这二种方式都是可行的。 + +### 方法 + +和变量一样,你不必定义为你的方法定义返回类型。举个例子,先看java: + +``` +public int square(int num) { + return num * num; +} +square(2); +``` + +你需要将该方法定义为public,需要定义返回类型,以及入参,最后你需要返回值。 + +我们再看下Groovy的写法: + +``` + def square(def num) { + num * num + } + square 4 + +``` + +没有了返回类型,没有了入参的定义。def代替了修饰符,方法体内没有了return关键字。然而我还是建议你使用return关键字。当你调用该方法时,你不需要括号和分号。 + +我们设置可以写的更简单点: + +``` +def square = { num -> + num * num +} +square 8 +``` + +下面我将通过code的形式,列出几点 + +- 当调用的方法有参数时,可以不用(),看下面的例子 + + + + + +``` + 1def printAge(String name, int age) { + 2 print("$name is $age years old") + 3} + 4 + 5def printEmptyLine() { + 6 println() + 7} + 8 + 9def callClosure(Closure closure) { +10 closure() +11} +12 +13printAge "John", 24 //输出John is 24 years old +14printEmptyLine() //输出空行 +15callClosure { println("From closure") } //输出From closure +``` + + + +- 如果最后的参数是闭包,可以将它写在括号的外面 + + + +``` +1def callWithParam(String param, Closure closure) { +2 closure(param) +3} +4 +5callWithParam("param", { println it }) //输出param +6callWithParam("param") { println it } //输出param +7callWithParam "param", { println it } //输出param +``` + + + +- 调用方法时可以指定参数名进行传参,有指定的会转化到Map对象中,没有的将按正常传参 + + + +``` + 1def printPersonInfo(Map person) { + 2 println("${person.name} is ${person.age} years old") + 3} + 4 + 5def printJobInfo(Map job, String employeeName) { + 6 println("${employeeName} works as ${job.name} at ${job.company}") + 7} + 8 + 9printPersonInfo name: "Jake", age: 29 +10printJobInfo "Payne", name: "Android Engineer", company: "Google" +``` + +你会发现他们的调用都不需要括号,同时printJobInfo的调用参数的顺序不受影响。 + + + + + +### 闭包 + +闭包是一段匿名的方法体,其可以接受参数和返回值。它们可以定义变量或者可以将参数传给方法。 + +你可以简单的使用方括号来定义闭包,如果你想详细点,你也可以这么定义: + +``` +Closure square = { + it * it +} +square 16 +``` + +添加了Closure,让其更加清晰。注意,当你没有显式的为闭包添加一个参数,groovy会默认为你添加一个叫做it。你可以在所有的闭包中使用it,如果调用者没有定义任何参数,那么it将会是null,这会使得你的代码更加简洁。 + +在grade中,我们经常使用闭包,例如Android代码体和dependencies也是。 + +### Collections + +在groovy中,有二个重要的容器分别是lists和maps。 + +创建一个list很容易,我们不必初始化: + +``` +List list = [1, 2, 3, 4, 5] +``` + +为list迭代也很简单,你可以使用each方法: + +``` +list.each() { element -> + println element +} +``` + +你甚至可以使得你的代码更加简洁,使用it: + +``` +list.each() { + println it +} +``` + +map和list差不多: + +``` +Map pizzaPrices = [margherita:10, pepperoni:12] +``` + +如果你想取出map中的元素,可以使用get方法: + +``` +pizzaPrices.get('pepperoni') +pizzaPrices['pepperoni'] +``` + +同样的groovy有更简单的方式: + +``` +pizzaPrices.pepperoni +``` + + + + + + +1.Groovy概述 +Groovy是Apache 旗下的一种基于JVM的面向对象编程语言,既可以用于面向对象编程,也可以用作纯粹的脚本语言。在语言的设计上它吸纳了Python、Ruby 和 Smalltalk 语言的优秀特性,比如动态类型转换、闭包和元编程支持。 +Groovy与 Java可以很好的互相调用并结合编程 ,比如在写 Groovy 的时候忘记了语法可以直接按Java的语法继续写,也可以在 Java 中调用 Groovy 脚本。比起Java,Groovy语法更加的灵活和简洁,可以用更少的代码来实现Java实现的同样功能。 + +2.Groovy编写和调试 +Groovy的代码可以在Android Studio和IntelliJ IDEA等IDE中进行编写和调试,缺点是需要配置环境,这里推荐在文本中编写代码并结合命令行进行调试(文本推荐使用Sublime Text)。关于命令行请查看Gradle入门前奏这篇文章。 +具体的操作步骤就是:在一个目录中新建build.gradle文件,在build.gradle中新建一个task,在task中编写Groovy代码,用命令行进入这个build.gradle文件所在的目录,运行gradle task名称 等命令行对代码进行调试,本文中的例子都是这样编写和调试的。 + +3.变量 +Groovy中用def关键字来定义变量,可以不指定变量的类型,默认访问修饰符是public。 + +def a = 1; +def int b = 1; +def c = "hello world"; +4.方法 +方法使用返回类型或def关键字定义,方法可以接收任意数量的参数,这些参数可以不申明类型,如果不提供可见性修饰符,则该方法为public。 +用def关键字定义方法。 + +task method <<{ + add (1,2) + minus 1,2 //1 +} +def add(int a,int b) { + println a+b //3 +} +def minus(a,b) {//2 + println a-b +} +如果指定了方法返回类型,可以不需要def关键字来定义方法。 + +task method <<{ + def number=minus 1,2 + println number +} +int minus(a,b) { + return a-b +} +如果不使用return ,方法的返回值为最后一行代码的执行结果。 + +int minus(a,b) { + a-b //4 +} +从上面两段代码中可以发现Groovy中有很多省略的地方: + +语句后面的分号可以省略。 + +方法的括号可以省略,比如注释1和注释3处。 + +参数类型可以省略,比如注释2处。 + +return可以省略掉,比如注释4处。 + +5.类 +Groovy类非常类似于Java类。 + +task method <<{ + def p = new Person() + p.increaseAge 5 + println p.age +} +class Person { + String name + Integer age =10 + def increaseAge(Integer years) { + this.age += years + } +} +运行 gradle method打印结果为: +15 + +Groovy类与Java类有以下的区别: + +默认类的修饰符为public。 + +没有可见性修饰符的字段会自动生成对应的setter和getter方法。 + +类不需要与它的源文件有相同的名称,但还是建议采用相同的名称。 + +6.语句 +6.1 断言 +Groovy断言和Java断言不同,它一直处于开启状态,是进行单元测试的首选方式。 + +task method <<{ + assert 1+2 == 6 +} +输出结果为: + +Execution failed for task ':method'. +> assert 1+2 == 6 + | | + 3 false +当断言的条件为false时,程序会抛出异常,不再执行下面的代码,从输出可以很清晰的看到发生错误的地方。 + +6.2 for循环 +Groovy支持Java的for(int i=0;i ] statements } +闭包分为两个部分,分别是参数列表部分[closureParameters -> ]和语句部分 statements 。 +参数列表部分是可选的,如果闭包只有一个参数,参数名是可选的,Groovy会隐式指定it作为参数名,如下所示。 + +{ println it } //使用隐式参数it的闭包 +当需要指定参数列表时,需要->将参数列表和闭包体相分离。 + +{ it -> println it } //it是一个显示参数 +{ String a, String b -> + println "${a} is a ${b}" +} +闭包是groovy.lang.Cloush类的一个实例,这使得闭包可以赋值给变量或字段,如下所示。 + +//将闭包赋值给一个变量 +def println ={ it -> println it } +assert println instanceof Closure +//将闭包赋值给Closure类型变量 +Closure do= { println 'do!' } +调用闭包 +闭包既可以当做方法来调用,也可以显示调用call方法。 + +def code = { 123 } +assert code() == 123 //闭包当做方法调用 +assert code.call() == 123 //显示调用call方法 +def isOddNumber = { int i -> i%2 != 0 } +assert isOddNumber(3) == true //调用带参数的闭包 +8. I/O 操作 +Groovy的 I/O 操作要比Java的更为的简洁。 + +8.1 文件读取 +我们可以在PC上新建一个name.txt,在里面输入一些内容,然后用Groovy来读取该文件的内容: + +def filePath = "D:/Android/name.txt" +def file = new File(filePath) ; +file.eachLine { + println it +} +可以看出Groovy的文件读取是很简洁的,还可以更简洁些: + +def filePath = "D:/Android/name.txt" +def file = new File(filePath) ; +println file.text +8.2 文件写入 +文件写入同样十分简洁: + +def filePath = "D:/Android/name.txt" +def file = new File(filePath); + +file.withPrintWriter { + it.println("三井寿") + it.println("仙道彰") +} +9. 其他 +9.1 asType +asType可以用于数据类型转换: + +String a = '23' +int b = a as int +def c = a.asType(Integer) +assert c instanceof java.lang.Integer +9.2 判断是否为真 +if (name != null && name.length > 0) {} +可以替换为 + +if (name) {} +9.3 安全取值 +在Java中,要安全获取某个对象的值可能需要大量的if语句来判空: + +if (school != null) { + if (school.getStudent() != null) { + if (school.getStudent().getName() != null) { + System.out.println(school.getStudent().getName()); + } + } +} +Groovy中可以使用?.来安全的取值: + +println school?.student?.name +9.4 with操作符 +对同一个对象的属性进行赋值时,可以这么做: + +task method <<{ +Person p = new Person() +p.name = "杨影枫" +p.age = 19 +p.sex = "男" +println p.name +} +class Person { + String name + Integer age + String sex +} +使用with来进行简化: + +Person p = new Person() +p.with { + name = "杨影枫" + age= 19 + sex= "男" + } +println p.name + + + + + + + + +[1]: https://github.com/CharonChui/AndroidNote/blob/master/AndroidStudioCourse/AndroidStudio%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B(%E7%AC%AC%E4%B8%83%E5%BC%B9).md "AndroidStudio使用教程(第七弹)" + + + + + + + + + + + + --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/Gradle&Maven/duplicate class\345\206\262\347\252\201\350\247\243\345\206\263.md" "b/Gradle&Maven/duplicate class\345\206\262\347\252\201\350\247\243\345\206\263.md" new file mode 100644 index 00000000..58008579 --- /dev/null +++ "b/Gradle&Maven/duplicate class\345\206\262\347\252\201\350\247\243\345\206\263.md" @@ -0,0 +1,82 @@ +duplicate class冲突解决 +=== + +``` +Duplicate class com.x.util.Base64Encoder found in modules jetified-b64encode-1.0.8-runtime (com.x.x.x.x:b64encode:1.0.8) and jetified-b64encode_v2_0 (b64encode_v2_0.jar) +``` +今天在开发过程中遇到了这个错误。提示Base64Encoder在com.x.x.x.x:b64encode:1.0.8和b64encode_v2_0.jar里面重复了,一个是1.0.8版本一个是2.0版本。 +那么这里要做的就是exclude一个就可以。 + +1. 首先需要找到是哪个依赖中有该依赖: +通常情况下我们会直接查找一下该类就能看到有那几个库中包含,但是这里只能查到jar包的类,所以需要用另一种方式。 +Terminal执行: +``` +./gradlew app:dependencies +``` +执行后会将所有的依赖列出: +``` +| \--- androidx.annotation:annotation:1.1.0 -> 1.2.0 ++--- com.google.android.gms:play-services-tasks:17.2.1 +| \--- com.google.android.gms:play-services-basement:17.6.0 +| +--- androidx.collection:collection:1.0.0 -> 1.1.0 +| | \--- androidx.annotation:annotation:1.1.0 -> 1.2.0 +| +--- androidx.core:core:1.2.0 -> 1.5.0 +| | +--- androidx.annotation:annotation:1.2.0 +| | +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.3.1 +| | | +--- androidx.arch.core:core-runtime:2.1.0 +| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.2.0 +| | | | \--- androidx.arch.core:core-common:2.1.0 +| | | | \--- androidx.annotation:annotation:1.1.0 -> 1.2.0 +| | | +--- androidx.lifecycle:lifecycle-common:2.3.1 (*) +| | | +--- androidx.arch.core:core-common:2.1.0 (*) +| | | \--- androidx.annotation:annotation:1.1.0 -> 1.2.0 +| | +--- androidx.versionedparcelable:versionedparcelable:1.1.1 +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.2.0 +| | | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*) +| | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*) +| \--- androidx.fragment:fragment:1.0.0 -> 1.3.4 +| +--- androidx.annotation:annotation:1.1.0 -> 1.2.0 +| +--- androidx.core:core:1.2.0 -> 1.5.0 (*) +| +--- androidx.collection:collection:1.1.0 (*) +``` + +2. 接下来就是在该列表中搜索,找到后直接在`build.gradle`中使用exclude: +``` +implementation("com.xxx.xxx:pass-sdk-core-router:${PASS_VERSION}") { + // 去除扫码相关功能 + exclude group: "com.xxx.passport", module: "pass-module-qrcode" + // 去除人脸登录相关功能 + exclude group: "com.xxx.passport", module: "pass-module-face" +} +``` + +3. 但是有时候会发现有很多个库中都会有该依赖,一个一个的去添加不太适合,这时可以在app的buid.gradle中统一配置: +``` +android { + compileSdkVersion rootProject.android.extCompileSdkVersion + buildToolsVersion rootProject.android.extBuildToolsVersion + useLibrary 'org.apache.http.legacy' + compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_1_8 + } + configurations { + implementation.exclude group: 'com.xxx.xxx.common.toolbox' , module:'b64encode' + } +} +``` + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git a/Gradle&Maven/kts.md b/Gradle&Maven/kts.md new file mode 100644 index 00000000..c6a2e26c --- /dev/null +++ b/Gradle&Maven/kts.md @@ -0,0 +1,136 @@ +Gradle 支持使用 Groovy DSL 或 Kotlin DSL 来编写脚本。所以在学习具体怎么写脚本时,我们肯定会考虑到底是使用 Kotlin 来写还是 Groovy 来写。 + +不一定说你是 Kotlin Android 开发者就一定要用 Kotlin 来写 Gradle,我们得判断哪种写法更适合项目、更适合开发团队人群(学习成本)。 + +所以下面来学习一下这两种语言的差异。 + +1. Groovy 和 Kotlin 的差异 + +1.1 语言差异 + +Groovy是一种基于 JVM 的面向对象的编程语言,它可以作为常规编程语言,但主要是作为脚本的语言(为了解决 Java 在写脚本时过于死板)。 + +它是一个动态语言,可以不指定变量类型。它的特性是支持闭包,闭包的本质很简单,简单的说就是定义一个匿名作用域,这个作用域内部可以封装函数和变量,外部不可以访问这个作用域内部的东西,但是可以通过调用这个作用域来完成一些任务。 + +Kotlin则是Java的优化版,在解决Kotlin很多痛点的情况下,不引入过多的新概念。它具有强大的类型推断系统,使得语言有良好的动态性,其次是其语言招牌 —— 语法糖,Kotlin 的代码可以写的非常简洁。这使得 Kotlin 不仅做为常规编程语言能大放异彩,作为脚本语言也深受很多开发者喜爱。 + +它们共同特点就是基于JVM,可以和 Java 互操作。Gradle 能提供的东西, Kotlin 也能通过提供(闭包)。在功能上,两者能做的事情都是一样的。此外一些简单的差异有: + +groovy 字符串可以使用单引号,而 kotlin 则必须为双引号。 +groovy 在方法调用时可以省略扩号,而 kotlin 不可省略。 +groovy 分配属性时可以省略 = 赋值运算符,而 kotlin 不可省略。 +groovy 是动态语言,不用导包,而 kotlin 则需要。 + + + +为什么要用Kotlin DSL写gradle脚本? + +撇开其他方面,就单从提高程序员生产效率方面就有很多优点: + +- 脚本代码自动补全 +- 跳转查看源码 +- 动态显示注释 +- 支持重构(Refactoring) +… +怎么样,要是你经历过groovy那令人蛋疼的体验,kotlin会让你爽的起飞,接下来让我们开始吧。 + +从Groovy到Kotlin + +让我们使用Android Studio 新建一个Android项目,AS默认会为我们生成3个gradle脚本文件。 + +- settings.gradle (属于 project) +- build.gradle (属于 project) +- build.gradle (属于 module) + +我们的目的就是转换这3个文件 + +- 第一步: 修改groovy语法到严格格式 + +groovy既支持双引号""也支持单引号'',而kotlin只支持双引号,所以首先将所有的单引号改为双引号。 + +例如 include ':app' -> include ":app" + +groovy方法调用可以不使用() 但是kotlin方法调用必须使用(),所以将所有方法调用改为()方式。 + +例如: + +```java +implementation "androidx.appcompat:appcompat:1.0.2" +改为 + + implementation ("androidx.appcompat:appcompat:1.0.2") +groovy 属性赋值可以不使用=,但是kotlin属性赋值需要使用=,所以将所有属性赋值添加=。 +``` + +例如: +``` +applicationId "com.ss007.gradlewithkotlin" +改为 + +applicationId = "com.ss007.gradlewithkotlin" +``` + +完成以上几步,准备工作就完成了。 + + + + +1.2 文件差异 +两者编写 Gradle 的文件是有差异的: + +用 Groovy 写的 Gradle 文件是 .gradle 后缀 +用 Kotlin 写的 Gradle 文件是 .gradle.kts 为后缀 +两者的主要区别是: + +代码提示和编译检查 +.kts 内所有都是基于kotlin代码规范的,所以强类型语言的好处就是编译没通过的情况下根本无法运行。此外,IDE 集成后可以提供自动补全代码的能力 +.gradle 则不会有代码提示和编译检查 +源代码、文档查看 +.gradle 被编译后是 JVM 字节码,有时候无法查看其源码 +.kts 的 DSL 是通过扩展函数实现的(可以看这篇:Kotlin DSL 学习),IDE 支持下可以导航到源代码、文档或重构部分 +对于写脚本的人来说,两者的差异不大,因为 Gradle 的 DSL 是 Groovy 提供的,后来的 Kotlin 并没有另起炉灶,而是写了一套 Kotlin 版的。所以两者在代码上也就只有所用语言的差异了,概念啥的都是一样的。 + +作为一名 Kotlin Android 开发者,我之后基本上是使用 Kotlin DSL 来学习写 Gradle 脚本,但是就跟我上面说的一样,了解其中一个后,要搞懂另外一个成本是很低的。 + + +2. 基本命令 +2.1 Project 、 Task 和 Action 介绍 +Gradle 主要是围绕着 Project(项目)、Task(任务)、Action(行为)这几个概念进行的。它们的作用分别是: + +project:每次 build 可以由一个或多个 project 组成。Gradle 为每个 build.gradle 创建一个相应的 project 领域对象,在编写Gradle脚本时,我们实际上是在操作诸如 project 这样的 Gradle 领域对象。 +若要创建多 project 的项目,我们需要在 根工程(root目录)下面新建 settings.gradle 文件,将所有的子 project 都写进去(include)。在 Android 中,每个 Module 都是一个子 project。 +task:每个 project 可以由一个或多个 task 组成。它代表更加细化的构建任务,例如:签名、编译一些java文件等。 +action:每个 task 可以由一个或多个 action 组成,它有 doFirst{} 和 doLast{} 两种类型 +———————————————— + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ImageLoaderLibrary/Coil\347\256\200\344\273\213.md" "b/ImageLoaderLibrary/Coil\347\256\200\344\273\213.md" new file mode 100644 index 00000000..940d93b0 --- /dev/null +++ "b/ImageLoaderLibrary/Coil\347\256\200\344\273\213.md" @@ -0,0 +1,62 @@ +# Coil简介 + +Coil作为图片加载库的新秀,和Glide、Picasso这些老牌图片库相比,它们的优缺点是什么以及Coil未来的展望?先来了解一下什么是Coil。 + +Coil是基于Kotlin开发的首个图片加载库,来自Instacart团队,来看看官网对它的最新的介绍。 + +- R8:Coil下完全兼容R8,所以不需要添加任何与Coil相关的混淆器规则。 +- Fast:Coil进行了许多优化,包括内存和磁盘缓存,对内存中的图片进行采样,重新使用位图,自动暂停/取消请求等等。 +- Lightweight:Coil为你的APK增加了2000个方法(对于已经使用了OkHttp和协程的应用程序),这与Picasso相当,明显少于Glide和Fresco。 +- Easy to use:Coil利用了Kotlin的语言特性减少了样版代码。 +- Modern:使用了大量的高级特性,例如协程、OkHttp、和androidX lifecycle跟踪生命周期状态的,Coil是目前唯一支持androidX lifecycle的库。 + +Coil作为图片库的新秀,越来越受欢迎了,但是为什么会引起这么多人的关注?在当今主流的图片加载库环境中Coil是一股清流,它是轻量级的,因为它使用了许多Android开发者已经在他们的项目中包含的其他库(协程、Okhttp)。 + +当我第一次看到这个库时,我认为这些都是很好的改进,但是我很想知道和其他主流的图片加载库相比,这个库能带来哪些好处,这篇文章的主要目的是分析一下Coil的性能,接下来我们来对比一下Coil、Glide和Picasso。 + +作者从以下场景对Coil、Glide、Picasso做了全面的测试。 + +- 当缓存为空时,从网络中下载图片的平均时间。 + + - 从网络中下载图片所用的时间。 + + 结果:Glide最快Picasso和Coil几乎相同。 + + - 加载完整的图片列表所用的时间,以及平均时间。 + + 结果:Glide是最快的,其次是Picasso,Coil是最慢的。 + +- 当缓存不为空时,从缓存中加载图片的平均时间。 + + - 从缓存中加载图片所用的时间。 + + 结果:Glide最快,Coil其次,Picasso最慢。 + + - 加载完整的图片列表所用的时间,以及平均时间。 + + 结果:Glide和Coil几乎相同,Picasso是最慢的。 + +图片加载库的选择是我们应用程序中最重要的部分之一,根据以上结果,如果你的应用程序中没有大量使用图片的时候,我认为使用Coil更好,原因有以下几点: + +- 与Glide和Fresco类似,Coil支持位图池,位图池是一种重新使用不再使用的位图对象的技术,这可以显著提高内存性能(特别是在oreo之前的设备上),但是它会造成一些API限制。 +- Coil是基于Kotlin开发的,为Kotlin使用而设计的,所以代码通常更简洁更干净。 +- Kotlin作为Android首选语言,Coil是为Kotlin而设计的,Coil在未来肯定会大方光彩。 +- 从Glide、Picasso迁移到Coil是非常的容易,API非常的相似。 +- Coil支持androidX lifecycle跟踪生命周期状态,也是是目前唯一支持androidX lifecycle的网络图片加载库。 +- Coil支持动态图片采样,假设本地有一个500x500的图片,当从磁盘读取500x500的映像时,我们将使用100x100的映像作为占位符。 + +如果你的是图片类型的应用,应用程序中包含了大量的图片,图片加载的速度是整个应用的核心指标之一,那么现在还不适合使用Coil。 + +Coil涵盖了Glide、Picasso等等图片加载库所支持的功能,除此之外Coil还有一个功能动态图片采样。 + +### 动态图片采样 + +更多关于图片采样信息可以访问[Coil](https://coil-kt.github.io/coil/getting_started/) ,这里简单的说明一下,假设本地有一个500x500的图片,当从磁盘读取500x500的图片时,将使用100x100的映像作为占位符,等待加载完成之后才会完全显示。 + + + + + +# 参考: + +- [Coil vs Picasso vs Glide: Get Ready… Go!](https://proandroiddev.com/coil-vs-picasso-vs-glide-get-ready-go-774add8cfd40) diff --git "a/ImageLoaderLibrary/Glide\347\256\200\344\273\213(\344\270\213).md" "b/ImageLoaderLibrary/Glide\347\256\200\344\273\213(\344\270\213).md" index 5456759f..7c246365 100644 --- "a/ImageLoaderLibrary/Glide\347\256\200\344\273\213(\344\270\213).md" +++ "b/ImageLoaderLibrary/Glide\347\256\200\344\273\213(\344\270\213).md" @@ -420,6 +420,33 @@ builder.setDiskCache( - `Disk cache needs to implement: DiskCache` + + +Let's take Glide as an example. To optimize memory usage and use less memory, Glide does downsampling. + +Downsampling means scaling the bitmap(image) to a smaller size which is actually required by the view. + +Assume that we have an image of size 2000*2000, but the view size is 400*400. So why load an image of 2000*2000, Glide down-samples the bitmap to 400*400, and then show it into the view. + +We use Glide like this: +``` +Glide.with(fragment) + .load(url) + .into(imageView); +``` +As we are passing the imageView as a parameter to the Glide, it knows the dimension of the imageView. + +Glide down-samples the image without loading the whole image into the memory. + +This way, the bitmap takes less memory, and the out-of-memory error is solved. Similarly, other Image Loading libraries like Fresco also do it. + +This was all about how the Android Image Loading library optimizes memory usage. + + + + + + 参考 === diff --git "a/JavaKnowledge/Base64\345\212\240\345\257\206.md" "b/JavaKnowledge/Base64\345\212\240\345\257\206.md" index 81a600f9..1dbe14c9 100644 --- "a/JavaKnowledge/Base64\345\212\240\345\257\206.md" +++ "b/JavaKnowledge/Base64\345\212\240\345\257\206.md" @@ -29,9 +29,7 @@ Base64加密 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/base64_man.png?raw=true) -正式因为这,所以 -想要转换成`Base64`最少要三个字节才可以,转换出来的`Base64`最少是4个字节,但是如果我要转换的字节不够3个怎么办?比如我想对字符`A`进行`Base64` -加密。,`A`对应的第二个`Base64`的二进制位只有两个,把后边的四个补0就是了。 +正是因为这,所以想要转换成`Base64`最少要三个字节才可以,转换出来的`Base64`最少是4个字节,但是如果我要转换的字节不够3个怎么办?比如我想对字符`A`进行`Base64`加密。,`A`对应的第二个`Base64`的二进制位只有两个,把后边的四个补0就是了。 所以`A`对应的`Base64`字符就是QQ。上边已经说过了,原则是`Base64`字符的最小单位是四个字符一组,那这才两个字符,后边补两个"="吧。 其实不用"="也不耽误解码,之所以用"=",可能是考虑到多段编码后的Base64字符串拼起来也不会引起混淆。 由此可见 Base64字符串只可能最后出现一个或两个"=",中间是不可能出现"="的。下图中字符"BC"的编码过程也是一样的。 @@ -50,4 +48,4 @@ Base64加密 - 邮箱 :charon.chui@gmail.com - Good Luck! - \ No newline at end of file + diff --git "a/JavaKnowledge/Git\347\256\200\344\273\213.md" "b/JavaKnowledge/Git\347\256\200\344\273\213.md" index c1022154..2d427891 100644 --- "a/JavaKnowledge/Git\347\256\200\344\273\213.md" +++ "b/JavaKnowledge/Git\347\256\200\344\273\213.md" @@ -1,46 +1,54 @@ Git简介 -=== - -`Git`和其他版本控制系统(包括`Subversion`及其他相似的工具)的主要差别在于`Git`对待数据的方法。概念上来区分,其它大部分系统以文件变更列表的方式存储信息。 -这类系统(`CVS、Subversion、Perforce、Bazaar`等等)将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。存储每个文件与初始版本的差异。 - - -`Git`不按照以上方式对待或保存数据。反之,`Git`更像是把数据看作是对小型文件系统的一组快照。每次你提交更新,或在`Git`中保存项目状态时, -它主要对当时的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,`Git`不再重新存储该文件,而是只保留一个链接指向之前存储的文件。`Git`对待数据更像是一个快照流。 - -`Git`是分布式版本控制系统,集中式和分布式版本控制有什么区别呢? - -- 集中式版本控制系统 - 版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快,可如果在互联网上,遇到网速慢的话,可能提交一个10M的文件就需要5分钟,这还不得把人给憋死啊。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_jizhong.jpeg) - -- 分布式版本控制系统 - 分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。 - 和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个 人 的电脑坏掉了不要紧,随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。 - 在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_fenbu.jpeg) +======= + +`Git`和其他版本控制系统(包括`Subversion`及其他相似的工具)的主要差别在于`Git`对待数据的方法。概念上来区分,其它大部分系统以文件 +变更列表的方式存储信息。 +这类系统(`CVS、Subversion、Perforce、Bazaar`等等)将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。 +存储每个文件与初始版本的差异。 + +`Git`不按照以上方式对待或保存数据。反之,`Git`更像是把数据看作是对小型文件系统的一组快照。每次你提交更新,或在`Git`中保存项目 +状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,`Git`不再重新存储该文件,而是只保留一个 +链接指向之前存储的文件。`Git`对待数据更像是一个快照流。 + +`Git`是分布式版本控制系统,集中式和分布式版本控制有什么区别呢? + +- 集中式版本控制系统 + 版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了, + 再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了, + 再放回图书馆。集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快,可如果在互联网上, + 遇到网速慢的话,可能提交一个10M的文件就需要5分钟,这还不得把人给憋死啊。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_jizhong.jpeg) +- 分布式版本控制系统 + 分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在 + 你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上 + 改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。 + 和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑坏掉了不要紧, + 随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。 + 在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相 + 访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器 + 的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。 + + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_fenbu.jpeg) 版本库 ---- +------ -什么是版本库呢?版本库又名仓库,英文名`repository`,你可以简单理解成一个目录,这个目录里面的所有文件都可以被`Git`管理起来,每个文件的修改、删除,`Git`都能跟踪, -以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。 +什么是版本库呢?版本库又名仓库,英文名`repository`,你可以简单理解成一个目录,这个目录里面的所有文件都可以被`Git`管理起来, +每个文件的修改、删除,`Git`都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。 -所以,创建一个版本库非常简单: +所以,创建一个版本库非常简单: - 创建一个空目录 -- 通过`git init`命令把这个目录变成`Git`可以管理的仓库: +- 通过`git init`命令把这个目录变成`Git`可以管理的仓库 瞬间`Git`就把仓库建好了,而且告诉你是一个空的仓库`(empty Git repository)`,细心的读者可以发现当前目录下多了一个`.git`的目录, 这个目录是`Git`来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把`Git`仓库给破坏了。 - 使用命令`git add `,注意,可反复多次使用,添加多个文件; - 使用命令`git commit`,完成。 +Git的五种状态 +------------- -五种状态 ---- - - -`Git`有五种状态,你的文件可能处于其中之一: +`Git`有五种状态,你的文件可能处于其中之一: - 未修改`(origin)` - 已修改`(modified)` @@ -48,41 +56,34 @@ Git简介 - 已提交`(committed)` - 已推送`(pushed)` - -已提交表示数据已经安全的保存在本地数据库中。 已修改表示修改了文件,但还没保存到数据库中。 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。 - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_list.png) - +已提交表示数据已经安全的保存在本地数据库中。 +已修改表示修改了文件,但还没保存到数据库中。 +已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。 `Git`仓库目录是`Git`用来保存项目的元数据和对象数据库的地方。这是`Git`中最重要的部分,从其它计算机克隆仓库时,拷贝的就是这里的数据。 工作目录是对项目的某个版本独立提取出来的内容。这些从`Git`仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。 暂存区域是一个文件,保存了下次将提交的文件列表信息,一般在`Git`仓库目录中。 有时候也被称作‘索引’,不过一般说法还是叫暂存区域。 -基本的`Git`工作流程如下: +基本的`Git`工作流程如下: - 在工作目录中修改文件。 - 暂存文件,将文件的快照放入暂存区域。 - 提交更新,找到暂存区域的文件,将快照永久性存储到`Git`仓库目录。 - 四个区 ---- +------ -`Git`主要分为四个区: +`Git`主要分为四个区: - 工作区`(Working Area)` - 暂存区`(Stage或Index Area)` - 本地仓库`(Local Repository)` - 远程仓库`(Remote Repository)` +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_buzhou.jpg) - -上面说了`git add`和`git commit`的惭怍,总体分为了三个部分,其实更加详细的来分析,还需要一个`git push`的过程,也就是把更改`push`到远程仓库中。 - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_buzhou.jpg) - -正常情况下,我们的工作流程就是三个步骤,分别对应上图中的三个箭头线: +正常情况下,我们的工作流程就是三个步骤,分别对应上图中的三个箭头线: ```shell git add . // 把所有文件放入暂存区 @@ -90,388 +91,701 @@ git commit -m "comment" // 把所有文件从暂存区提交进本地仓库 git push // 把所有文件从本地仓库推送进远程仓库 ``` -先上一张图 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.jpg) -图中的`index`部分就是暂存区 +先上一张图 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.png) + +图中的`index`部分就是暂存区 + +## 三棵树 + +Git作为一个系统,是以它的一般操作来管理并操纵这三棵树的: + +| 树 | 用途 | +| :---------------- | :----------------------------------- | +| HEAD | 上一次提交的快照,下一次提交的父结点 | +| Index | 预期的下一次提交的快照 | +| Working Directory | 沙盒 | + +#### HEAD + +HEAD是当前分支引用的指针,它总是指向该分支上的最后一次提交。这表示HEAD将是下一次提交的父结点。通常,理解HEAD的最简方式, +就是将它看做**该分支上的最后一次提交**的快照。 + +#### 索引 + +索引是你的**预期的下一次提交**。我们也会将这个概念引用为Git的“暂存区”,这就是当你运行`git commit`时Git看起来的样子。 + +#### 工作目录 -- 安装好git后我们要先配置一下。以便`git`跟踪。 +最后,你就有了自己的**工作目录**(通常也叫**工作区**)。 另外两棵树以一种高效但并不直观的方式,将它们的内容存储在`.git`文件夹中。 +工作目录会将它们解包为实际的文件以便编辑。你可以把工作目录当做**沙盒**。在你将修改提交到暂存区并记录到历史之前,可以随意更改。 - ``` - git config --global user.name "xxx" +#### Git目录下文件的状态: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_file_lifecycle.png?raw=true) +你工作目录下的每一个文件都不外乎这两种状态: + +- 已跟踪(Tracked) + 已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有他们的记录,在工作一段时间后,它们的状态可能是未修改,已修改或 + 已放入暂存区。简而言之,已跟踪的文件就是Git已经知道的文件 +- 未跟踪(Untracked) + 工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们即不存在与上次快照的记录中,也没有被放入暂存区。 + +## 常用命令 + +### git config + +安装好git后我们要先配置一下。以便`git`跟踪。 +``` + git config --global user.name "xxx" git config --global user.email "xxx@xxx.com" - ``` - 上面修改后可以使用`cat ~/.gitconfig`查看 - 如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 - `git config --list`来查看所有的配置。 - -- 新建仓库 - ``` - mkdir gitDemo - cd gitDemo - git init - ``` - 这样就创建完了。 - -- `clone`仓库 - 在某一目录下执行. - `git clone [git path]` - 只是后`Git`会自动把当地仓库的`master`分支和远程仓库的`master`分支对应起来,远程仓库默认的名称是`origin`。 - -- `git add`提交文件更改(修改和新增),把当前的修改添加到暂存区 - `git add xxx.txt`添加某一个文件 - `git add .`添加当前目录所有的文件 - -- `git commit`提交,把修改由暂存区提交到仓库中 - `git commit`提交,然后在出来的提示框内查看当前提交的内容以及输入注释。 - 或者也可以用`git commit -m "xxx"` 提交到本地仓库并且注释是xxx - - `git commit`是很小的一件事情,但是往往小的事情往往引不起大家的关注,不妨打开公司的任一个`repo`,查看`commit log`,满篇的`update`和`fix`, - 完全不知道这些`commit`是要做啥。在提交`commit`的时候尽量保证这个`commit`只做一件事情,比如实现某个功能或者修改了配置文件。注意是保证每个`commit` - 只做一件事,而不是让你做了一件事`commit`后就`push`,那样就有点过分了。 - -- `git cherry-pick` - `git cherry-pick`可以选择某一个分支中的一个或几个`commit(s)`来进行操作。例如,假设我们有个稳定版本的分支,叫`v2.0`,另外还有个开发版本的分支`v3.0`,我们不能直接把两个分支合并,这样会导致稳定版本混乱,但是又想增加一个`v3.0`中的功能到`v2.0`中,这里就可以使用`cherry-pick`了。 - 就是对已经存在的`commit`进行 再次提交; - 简单用法: - `git cherry-pick ` - - -- `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 - -- `git diff` - `git diff`直接查看所有的区别 - `git diff HEAD -- xx.txt`查看工作区与版本库最新版的差别。 - - - 首先如果我们只是本地修改了一个文件,但是还没有执行`git add .`之前,该如何查看有那些修改。这种情况下直接执行`git diff`就可以了。 - - 那如果我们执行了`git add .`操作,然后你再执行`git diff`这时就会发现没有任何结果,这时因为`git diff`这个命令只是检查工作区和暂存区之间的差异。 - 如果我们要查看暂存区和本地仓库之间的差异就需要加一个参数使用`--staged`参数或者`--cached`,`git diff --cached`。这样再执行就可以看到暂存区和本地仓库之间的差异。 - - 现在如果我们把修改使用`git commit`从暂存区提交到本地仓库,再看一下差异。这时候再执行`git diff --cached`就会发现没有任何差异。 - 如果我们行查看本地仓库和远程仓库的差异,就要换另一个参数,执行`git diff master origin/master`这样就可以看到差异了。 这里面`master`是本地的仓库,而`origin/master`是 - 远程仓库,因为默认都是在主分支上工作,所以两边都是`master`而`origin`代表远程。 - -- `git push` 提交到远程仓库 - 可以直接调用`git push`推送到当前分支 - 或者`git push origin master`推送到远程`master`分支 - `git push origin devBranch`推送到远程`devBranch`分支 - -- `git log`查看当前分支下的提交记录 - 用`git log`可以查看提交历史,以便确定要回退到哪个版本。 - 如果已经使用`git log`查出版本`commit id`后`reset`到某一次提交后,又要重返回来, - 用`git reflog`查看命令历史,以便确定要回到未来的哪个版本。 - ``` - git log -p -2 // -p 是仅显示最近的x次提交 - git log --stat // stat简略的显示每次提交的内容梗概,如哪些文件变更,多少删除,多少添加 - git log --oneline --graph - ``` - 下面是常用的参数: - - `–author=“Alex Kras”` ——只显示某个用户的提交任务 - - `–name-only` ——只显示变更文件的名称 - - `–oneline`——将提交信息压缩到一行显示 - - `–graph` ——显示所有提交的依赖树 - - `–reverse` ——按照逆序显示提交记录(最先提交的在最前面) - - `–after` ——显示某个日期之后发生的提交 - - `–before` ——显示发生某个日期之前的提交 - - -- `git reflog` - 可以查看所有操作记录包括`commit`和`reset`操作以及删除的`commit`记录 - -- `git reset` - `git reset`命令用于将当前HEAD复位到指定状态。一般用于撤消之前的一些操作(如:`git add`,`git commit`等)。 - 在`git`的一般使用中,如果发现错误的将不想暂存的文件被`git add`进入索引之后,想回退取消,则可以使用命令:`git reset HEAD `, - 同时`git add`完毕之后,`git`也会做相应的提示,比如: - ```shell - # Changes to be committed: - # (use "git reset HEAD ..." to unstage) - # - # new file: test.py - ``` - `git reset [--hard|soft|mixed|merge|keep] [或HEAD]`:将当前的分支重设`(reset)`到指定的``或者`HEAD`(默认,如果不显示指定``,默认是`HEAD`,即最新的一次提交),并且根据`[mode]`有可能更新索引和工作目录。`mode`的取值可以是`hard、soft、mixed、merged、keep`。下面来详细说明每种模式的意义和效果: - - `--hard`:彻底回退到某一个版本,本地的源码也会变为上一个版本的内容。重删除工作空间改动代码,撤销commit,撤销git add .。所有变更集都会被丢弃。 - - `--mixed`:默认方式,它回退到某个版本,只保留源码,不删除工作空间改动代码,撤销commit,并且撤销git add . 。所有变更集都放在工作区。 - - `--soft`: 回退到某个版本,不删除工作空间改动代码,撤销commit,不撤销git add . ,所有变更集都放在暂存区,如果还要提交直接重新commit即可。 - - 下面是具体一个例子,假设有三个`commit`,执行`git status`结果如下: - ``` - commit3: add test3.c - commit2: add test2.c - commit1: add test1.c - ``` - 执行`git reset --hard HEAD~1`命令后, - 显示:`HEAD is now at commit2`,运行`git log`,如下所示: - ``` - commit2: add test2.c - commit1: add test1.c - ``` - - - 回滚最近一次提交 - - ``` - $ git commit -a -m "这是提交的备注信息" - $ git reset --soft HEAD^ #(1) - $ edit code #(2) 编辑代码操作 - $ git commit -a -c ORIG_HEAD #(3) - ``` - - - `Git`中用`HEAD`表示当前版本,上一版本就是`HEAD^`,上上一版本就是`HEAD^^`.如果往前一千个版本呢? 那就是`HEAD~1000`. - `git reset —-hard HEAD^` - `git reset —-hard commit_id` - `git reset HEAD fileName`可以把用`git add`之后但是还没有`commit`之前暂存区中的修改撤销。 - 说到这里就说一个问题,如果你reset到某一个版本之后,发现弄错了,还想返回去,这时候用`git log`已经找不到之前的`commit id`了。那怎么办?这时候可以使用下面的命令来找。 - -- `git checkout`撤销修改或者切换分支 - `git checkout -- xx.txt`意思就是将`xx.txt`文件在工作区的修改全部撤销。可能会有两种情况: - - - 修改后还没有调用`git add`添加到暂存区,现在撤销后就会和版本库一样的状态。 - - 修改后已经调用`git add`添加到暂存区后又做了修改,这时候撤销就会回到暂存区的状态。 - - 总的来说`git checkout`就是让这个文件回到最近一次`git commit`或者`git add`的状态。 - 这里还有一个问题就是我胡乱修改了某个文件内容然后调用了`git add`添加到缓存区中,这时候想丢弃修改该怎么办?也是要分两步: - - 使用`git reset HEAD file`命令,将暂存区中的内容回退,这样修改的内容会从暂存区回到工作区。 - - 使用`git checkout --file`直接丢弃工作区的修改。 - - `git checkout`把当前目录所有修改的文件从`HEAD`都撤销修改。 - 为什么分支的地方也是用`git checkout`这里撤销还是用它呢?他们的区别在于`--`,如果没有`--`那就是检出分支了。 - `git checkout origin/developer` // 切换到orgin/developer分支 - - -上面介绍了`git reset`和`git checkout`,这里就总结一下如何来对修改进行撤销操作: - -- 已经修改,但是并未执行`git add .`进行暂存 - 如果只是修改了本地文件,但是还没有执行`git add .`这时候我们的修改还是再工作区,并未进入暂存区,我们可以使用:`git checkouot .`或者`git reset --hard`来进行 - 撤销操作。 - - `git add .`的反义词是`git checkout .`做完修改后,如果想要向前一步,让修改进入暂存区执行`git add .`如果想退后一步,撤销修改就执行`git checkout .`。 - -- 已暂存,未提交 - 如果已经执行了`git add .`但是还没有执行`git commit -m "comment"`这时候你意识到了错误,想要撤销,可以执行: - - ``` - git reset // git reset 只是把修改退回到了git add .之前的状态,也就是让文件还处于已修改未暂存的状态 - git checkout . // 上面让文件处于已修改未暂存的状态,还要执行git checkout .来撤销工作区的状态 - ``` - 或`git reset --hard` +``` +上面修改后可以使用`cat ~/.gitconfig`查看 +如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 +`git config --list`来查看所有的配置。 +如果需要查看当前的user.name和user.email的值可以通过`git config user.name` + +### git init + +新建仓库 +``` +mkdir gitDemo +cd gitDemo +git init +``` +这样就创建完了。 + +### git clone仓库 + +在某一目录下执行. +`git clone [git path]` +执行后`Git`会自动把当地仓库的`master`分支和远程仓库的`master`分支对应起来,远程仓库默认的名称是`origin`。 + +### git add提交文件更改(修改和新增),把当前的修改添加到暂存区 + +`git add xxx.txt`添加某一个文件 +`git add .`添加当前目录所有的文件 + +### `git commit`提交,把修改由暂存区提交到仓库中 + +`git commit`提交,然后在出来的提示框内查看当前提交的内容以及输入注释。 +或者也可以用`git commit -m "xxx"` 提交到本地仓库并且注释是xxx + +`git commit`是很小的一件事情,但是往往小的事情往往引不起大家的关注,不妨打开公司的任一个`repo`,查看`commit log`, +满篇的`update`和`fix`,完全不知道这些`commit`是要做啥。在提交`commit`的时候尽量保证这个`commit`只做一件事情, +比如实现某个功能或者修改了配置文件。注意是保证每个`commit`只做一件事,而不是让你做了一件事`commit`后就`push`, +那样就有点过分了。 + +### `git cherry-pick` + +`git cherry-pick`可以选择某一个分支中的一个或几个`commit(s)`来进行操作。例如,假设我们有个稳定版本的分支,叫`v2.0`, +另外还有个开发版本的分支`v3.0`,我们不能直接把两个分支合并,这样会导致稳定版本混乱,但是又想增加一个`v3.0`中的功能到`v2.0`中, +这里就可以使用`cherry-pick`了。 +就是对已经存在的`commit`进行 再次提交; +简单用法: +`git cherry-pick ` + +`git rebase`命令基本是是一个自动化的`cherry-pick`命令。它计算出一系列的提交,然后再以它们在其他地方以同样的顺序一个一个的`cherry-picks`出它们。 + +### `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 + +### `git diff` + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_diff.webp?raw=true) +`git diff`直接查看当前修改未add(暂存staged)的差别 +`git diff --staged`查看已add(到暂存区)的差别`git diff HEAD -- xx.txt`查看工作区与版本库最新版的差别。 + +- 首先如果我们只是本地修改了一个文件,但是还没有执行`git add .`之前,该如何查看有那些修改。这种情况下直接执行`git diff`就可以了。 +- 那如果我们执行了`git add .`操作,然后你再执行`git diff`这时就会发现没有任何结果,这时因为`git diff`这个命令只是检查工作区和暂存区之间的差异。 + 如果我们要查看暂存区和本地仓库之间的差异就需要加一个参数使用`--staged`参数或者`--cached`,`git diff --cached`。这样再执行就可以看到暂存区和本地仓库之间的差异。 +- 现在如果我们把修改使用`git commit`从暂存区提交到本地仓库,再看一下差异。这时候再执行`git diff --cached`就会发现没有任何差异。 + 如果我们行查看本地仓库和远程仓库的差异,就要换另一个参数,执行`git diff master origin/master`这样就可以看到差异了。 这里面`master`是本地的仓库,而`origin/master`是远程仓库,因为默认都是在主分支上工作,所以两边都是`master`而`origin`代表远程。 + +### `git push` 提交到远程仓库 + +可以直接调用`git push`推送到当前分支 +或者`git push origin master`推送到远程`master`分支 +`git push origin devBranch`推送到远程`devBranch`分支 +### `git log`查看当前分支下的提交记录 + +用`git log`可以查看提交历史,以便确定要回退到哪个版本。 +如果已经使用`git log`查出版本`commit id`后`reset`到某一次提交后,又要重返回来,用`git reflog`查看命令历史,以便确定要回到未来的哪个版本。 + +``` +git log -p -2 // -p 是仅显示最近的x次提交 +git log --stat // stat简略的显示每次提交的内容梗概,如哪些文件变更,多少删除,多少添加 +git log --oneline --graph +git log --grep="1" +``` +下面是常用的参数: + +- `–-author=“Alex Kras”` ——只显示某个用户的提交任务 +- `–-name-only` ——只显示变更文件的名称 +- `–-oneline`——将提交信息压缩到一行显示 +- `–-graph` ——显示所有提交的依赖树 +- `–-reverse` ——按照逆序显示提交记录(最先提交的在最前面) +- `–-after` ——显示某个日期之后发生的提交 +- `–-before` ——显示发生某个日期之前的提交 +- `--grep` ——过滤内容 + +#### Git日志搜索 + +如果你想知道某一个东西是什么时候存在或者引入的。git log命令有许多强大的工具可以通过提交信息甚至是diff的内容来找到某个特定的提交。 +例如,如果我们想找到ZLIB_BUF_MAX常量是什么时候引入的,我们可以使用-S选项来显示新增和删除该字符串的提交: + +```shell +git log -S ZLIB_BUF_MAX --oneline +``` +### `git reflog` + +可以查看所有操作记录包括`commit`和`reset`操作以及删除的`commit`记录 + +### `git reset` + +`git reset`命令用于将当前HEAD复位到指定状态。一般用于撤消之前的一些操作(如:`git add`,`git commit`等)。 +在`git`的一般使用中,如果发现错误的将不想暂存的文件被`git add`进入索引之后,想回退取消,则可以使用命令:`git reset HEAD `, +同时`git add`完毕之后,`git`也会做相应的提示,比如: + +```shell +# Changes to be committed: +# (use "git reset HEAD ..." to unstage) +# +# new file: test.py +``` +`git reset [--hard|soft|mixed|merge|keep] [或HEAD]`:将当前的分支重设`(reset)`到指定的``或者`HEAD`(默认,如果不显示指定``,默认是`HEAD`,即最新的一次提交),并且根据`[mode]`有可能更新索引和工作目录。`mode`的取值可以是`hard、soft、mixed、merged、keep`。下面来详细说明每种模式的意义和效果: + +- `--hard`:彻底回退到某一个版本,本地的源码也会变为上一个版本的内容。重删除工作空间改动代码,撤销commit,撤销git add .。所有变更集都会被丢弃。 +- `--mixed`:默认方式,它回退到某个版本,只保留源码,不删除工作空间改动代码,撤销commit,并且撤销git add . 。所有变更集都放在工作区。 +- `--soft`: 回退到某个版本,不删除工作空间改动代码,撤销commit,不撤销git add . ,所有变更集都放在暂存区,如果还要提交直接重新commit即可。 + +#### 示例 + +假设我们进入到一个新目录,其中有一个文件。 我们称其为该文件的v1版本,将它标记为蓝色。 +现在运行git init,这会创建一个Git仓库,其中的HEAD引用指向未创建的master分支。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex1.png?raw=true) +此时,只有工作目录有内容。 +现在我们想要提交这个文件,所以用git add来获取工作目录中的内容,并将其复制到索引中。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex2.png?raw=true) +接着运行git commit,它会取得索引中的内容并将它保存为一个永久的快照,然后创建一个指向该快照的提交对象,最后更新master来指向本次提交。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex3.png?raw=true) +此时如果我们运行 git status,会发现没有任何改动,因为现在三棵树完全相同。 + +现在我们想要对文件进行修改然后提交它。 我们将会经历同样的过程;首先在工作目录中修改文件。 我们称其为该文件的v2版本,并将它标记为红色。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex4.png?raw=true) +如果现在运行git status,我们会看到文件显示在 “Changes not staged for commit” 下面并被标记为红色,因为该条目在索引与工作目录之间存在不同。 +接着我们运行git add来将它暂存到索引中。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex5.png?raw=true) +此时,由于索引和HEAD不同,若运行git status的话就会看到“Changes to be committed” 下的该文件变为绿色 ——也就是说,现在预期的下一次提交 +与上一次提交不同。 最后,我们运行git commit来完成提交。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex6.png?raw=true) +现在运行git status会没有输出,因为三棵树又变得相同了。 + +切换分支或克隆的过程也类似。当检出一个分支时,它会修改HEAD指向新的分支引用,将索引填充为该次提交的快照, +然后将索引的内容复制到工作目录中。 + +#### 重置的作用 + +在以下情景中观察reset命令会更有意义。 + +为了演示这些例子,假设我们再次修改了file.txt 文件并第三次提交它。 现在的历史看起来是这样的: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-start.png?raw=true) +让我们跟着reset看看它都做了什么。它以一种简单可预见的方式直接操纵这三棵树。它做了三个基本操作。 + +##### 第 1 步:移动 HEAD + +reset 做的第一件事是移动 HEAD 的指向。 这与改变 HEAD 自身不同(checkout 所做的);reset 移动 HEAD 指向的分支。 这意味着如果 HEAD 设置为 master 分支(例如,你正在 master 分支上), 运行 git reset 9e5e6a4 将会使 master 指向 9e5e6a4。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-soft.png?raw=true) +无论你调用了何种形式的带有一个提交的 reset,它首先都会尝试这样做。 使用 reset --soft,它将仅仅停在那儿。 + +现在看一眼上图,理解一下发生的事情:它本质上是撤销了上一次 git commit 命令。 当你在运行 git commit 时,Git 会创建一个新的提交,并移动 HEAD 所指向的分支来使其指向该提交。 当你将它 reset 回 HEAD~(HEAD 的父结点)时,其实就是把该分支移动回原来的位置,而不会改变索引和工作目录。 现在你可以更新索引并再次运行 git commit 来完成 git commit --amend 所要做的事情了 + +##### 第 2 步:更新索引(--mixed) + +注意,如果你现在运行 git status 的话,就会看到新的 HEAD 和以绿色标出的它和索引之间的区别。 + +接下来,reset 会用 HEAD 指向的当前快照的内容来更新索引。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-mixed.png?raw=true) +如果指定 --mixed 选项,reset 将会在这时停止。 这也是默认行为,所以如果没有指定任何选项(在本例中只是 git reset HEAD~),这就是命令将会停止的地方。 - 上面两个例子中都使用了`git reset --hard`这个命令也可以完成,这个命令可以一步到位的把你的修改完全恢复到本地仓库的未修改的状态。 +现在再看一眼上图,理解一下发生的事情:它依然会撤销一上次 提交,但还会 取消暂存 所有的东西。 于是,我们回滚到了所有 git add 和 git commit 的命令执行之前。 -- 已提交,未推送 - 如果执行了`git add .`又执行了`git commit -m "comment"`提交了代码,这时候代码已经进入到了本地仓库,然而你发现问题了,想要撤销,怎么办? +##### 第 3 步:更新工作目录(--hard) + +reset 要做的的第三件事情就是让工作目录看起来像索引。 如果使用 --hard 选项,它将会继续这一步。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-hard.png?raw=true) +现在让我们回想一下刚才发生的事情。 你撤销了最后的提交、git add 和 git commit 命令 以及 工作目录中的所有工作。 + +必须注意,--hard 标记是 reset 命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的 reset 调用都可以轻松撤消,但是 --hard 选项不能,因为它强制覆盖了工作目录中的文件。 在这种特殊情况下,我们的 Git 数据库中的一个提交内还留有该文件的 v3 版本, 我们可以通过 reflog 来找回它。但是若该文件还未提交,Git 仍会覆盖它从而导致无法恢复。 + +回顾reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止: + +- 移动 HEAD 分支的指向 (若指定了 --soft,则到此停止) +- 使索引看起来像 HEAD (若未指定 --hard,则到此停止) +- 使工作目录看起来像索引 + +##### 通过路径来重置 + +前面讲述了 reset 基本形式的行为,不过你还可以给它提供一个作用路径。 若指定了一个路径,reset 将会跳过第 1 步,并且将它的作用范围限定为指定的文件或文件集合。 这样做自然有它的道理,因为 HEAD 只是一个指针,你无法让它同时指向两个提交中各自的一部分。 不过索引和工作目录 可以部分更新,所以重置会继续进行第 2、3 步。 + +现在,假如我们运行 git reset file.txt (这其实是 git reset --mixed HEAD file.txt 的简写形式,因为你既没有指定一个提交的 SHA-1 或分支,也没有指定 --soft 或 --hard),它会: + +- 移动 HEAD 分支的指向 (已跳过) +- 让索引看起来像 HEAD (到此处停止) + +所以它本质上只是将 file.txt 从 HEAD 复制到索引中。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path1.png?raw=true) +它还有 取消暂存文件 的实际效果。 如果我们查看该命令的示意图,然后再想想 git add 所做的事,就会发现它们正好相反。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path2.png?raw=true) +这就是为什么 git status 命令的输出会建议运行此命令来取消暂存一个文件。我们可以不让 Git 从 HEAD 拉取数据,而是通过具体指定一个提交来拉取该文件的对应版本。 我们只需运行类似于 git reset eb43bf file.txt 的命令即可。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path3.png?raw=true) +它其实做了同样的事情,也就是把工作目录中的文件恢复到 v1 版本,运行 git add 添加它, 然后再将它恢复到 v3 版本(只是不用真的过一遍这些步骤)。 如果我们现在运行 git commit,它就会记录一条“将该文件恢复到 v1 版本”的更改, 尽管我们并未在工作目录中真正地再次拥有它。 + +还有一点同 git add 一样,就是 reset 命令也可以接受一个 --patch 选项来一块一块地取消暂存的内容。 这样你就可以根据选择来取消暂存或恢复内容了。 + +### `git checkout`撤销修改或者切换分支 + +`git checkout -- xx.txt`意思就是将`xx.txt`文件在工作区的修改全部撤销。可能会有两种情况: + +- 修改后还没有调用`git add`添加到暂存区,现在撤销后就会和版本库一样的状态。 +- 修改后已经调用`git add`添加到暂存区后又做了修改,这时候撤销就会回到暂存区的状态。 + +总的来说`git checkout`就是让这个文件回到最近一次`git commit`或者`git add`的状态。 +这里还有一个问题就是我胡乱修改了某个文件内容然后调用了`git add`添加到缓存区中,这时候想丢弃修改该怎么办?也是要分两步: + +- 使用`git reset HEAD file`命令,将暂存区中的内容回退,这样修改的内容会从暂存区回到工作区。 +- 使用`git checkout --file`直接丢弃工作区的修改。 + +`git checkout`把当前目录所有修改的文件从`HEAD`都撤销修改。 +为什么分支的地方也是用`git checkout`这里撤销还是用它呢?他们的区别在于`--`,如果没有`--`那就是检出分支了。 +`git checkout origin/developer` // 切换到orgin/developer分支 + +上面介绍了两个回退操作`git reset`和`git checkout`,这里就总结一下如何来对修改进行撤销操作: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_reset_checkout.png?raw=true) + +- 已经修改,但是并未执行`git add .`进行暂存 + 如果只是修改了本地文件,但是还没有执行`git add .`这时候我们的修改还是在工作区,并未进入暂存区,我们可以使用:`git checkouot .`或者`git reset --hard`来进行撤销操作。 + + `git add .`的反义词是`git checkout .`做完修改后,如果想要向前一步,让修改进入暂存区执行`git add .`如果想退后一步,撤销修改就执行`git checkout .`。 +- 已暂存,未提交 + 如果已经执行了`git add .`但是还没有执行`git commit -m "comment"`这时候你意识到了错误,想要撤销,可以执行: + + ``` + git reset // git reset 只是把修改退回到了git add .之前的状态,也就是让文件还处于已修改未暂存的状态 + git checkout . // 上面让文件处于已修改未暂存的状态,还要执行git checkout .来撤销工作区的状态 + ``` + 或`git reset --hard` + + 上面两个例子中都使用了`git reset --hard`这个命令也可以完成,这个命令可以一步到位的把你的修改完全恢复到本地仓库的未修改的状态。 +- 已提交,未推送 + 如果执行了`git add .`又执行了`git commit -m "comment"`提交了代码,这时候代码已经进入到了本地仓库,然而你发现问题了,想要撤销,怎么办? 执行`git reset --hard origin/master`还是`git reset --hard`命令,只不过这次多了一个参数`origin/master`,这代表远程仓库,既然本地仓库已经有了 - 你提交的脏代码,那么就从远程仓库中把代码恢复把。 + 你提交的脏代码,那么就从远程仓库中把代码恢复把。 - 但是上面这样会导致你之前修改的代码都没有了,如果我只是想撤回提交,还想要我之前修改的东西重新回到本地仓库呢? + 但是上面这样会导致你之前修改的代码都没有了,如果我只是想撤回提交,还想要我之前修改的东西重新回到本地仓库呢? `git reset --soft HEAD^`,这样就成功的撤销了你的commit。注意,仅仅是撤回commit操作,您写的代码仍然保留。 +- 已推送到远程仓库 + 如果你执行`git add .`后又`commit`又执行了`git push`操作了,这时候你的代码已经进入到了远程仓库中,如果你发现你提交的代码又问题想恢复的话,那你只能先把本地仓库的代码恢复,然后再强制执行`git push`仓做,`push`到远程仓库就可以了。 + + ``` + git reset --hard HEAD^ // HEAD^代表最新提交的前一次 + git push -f // 强制推送 + ``` + +### reset checkout的区别 + +你大概还想知道 checkout 和 reset 之间的区别。 和 reset 一样,checkout 也操纵三棵树,不过它有一点不同,这取决于你是否传给该命令一个文件路径。 + +不带路径运行 git checkout [branch] 与运行 git reset --hard [branch] 非常相似,它会更新所有三棵树使其看起来像 [branch],不过有两点重要的区别。 -- 已推送到远程仓库 - 如果你执行`git add .`后又`commit`又执行了`git push`操作了,这时候你的代码已经进入到了远程仓库中,如果你发现你提交的代码又问题想恢复的话,那你只能先把本地仓库的 - 代码恢复,然后再强制执行`git push`仓做,`push`到远程仓库就可以了。 - - ``` - git reset --hard HEAD^ // HEAD^代表最新提交的前一次 - git push -f // 强制推送 - ``` - - -- `git revert`撤销提交 - `git revert`在撤销一个提交的同时会创建一个新的提交,这是一个安全的方法,因为它不会重写提交历史。 - - - `git revert`是生成一个新的提交来撤销某次提交,此次提交之前的`commit`都会被保留 - - `git reset`是回到某次提交,提交及之前的`commit`都会被保留,但是此次之后的修改都会被退回到暂存区 - - 相比`git reset`它不会改变现在得提交历史。`git reset`是直接删除制定的`commit` - 并把`HEAD`向后移动了一下。而`git revert`是一次新的特殊的`commit`,`HEAD`继续前进,本质和普通`add commit`一样,仅仅是`commit`内容很特殊。内容是与前面普通`commit`变化的反操作。 - 比如前面普通`commit`是增加一行`a`,那么`revert`内容就是删除一行`a`。 - - -- `git rm`删除文件 - 该文件就不再纳入版本管理了。如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f(译注:即 force 的首字母),以防误删除文件后丢失修改的内容。 - 另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。换句话说,仅是从跟踪清单中删除。比如一些大型日志文件或者一堆 .a 编译文件,不小心纳入仓库后,要移除跟踪但不删除文件,以便稍后在 .gitignore 文件中补上,用 --cached 选项即可:`git rm --cached readme.txt` - - -- 分支 - `git`分支的创建和合并都是非常快的,因为增加一个分支其实就是增加一个指针,合并其实就是让某个分支的指针指向某一个位置。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_master_branch.png?raw=true) - -- 创建分支 - - `git branch devBranch`创建名为`devBranch`的分支。 - `git checkout devBranch`切换到`devBranch`分支。 - `git checkout -b devBranch`创建+切换到分支`devBranch`。 - `git branch`查看当前仓库中的分支。 - `git branch -r`查看远程仓库的分支。 - `git branch -d devBranch`删除`devBranch`分支。 - ``` - origin/HEAD -> origin/master - origin/developer - origin/developer_sg - origin/master - origin/master_sg - origin/offline - ``` - `git branch -d devBranch`删除`devBranch`分支。 - 当时如果在新建了一个分支后进行修改但是还没有合并到其他分支的时候就去使用`git branch -d xxx`删除的时候系统会手提示说这个分支没有被合并,删除失败。 - 这时如果你要强行删除的话可以使用命令`git branch -D xxx`. - 如何删除远程分支呢? - ``` - git branch -r -d origin/developer - git push origin :developer - ``` - 如何本地创建分支并推送给远程仓库? - ``` - // 本地创建分支 - git checkout master //进入master分支 - git checkout -b frommaster //以master为源创建分支frommaster - // 推送到远程仓库 - git push origin frommaster// 推送到远程仓库所要使用的名字 - ``` - - 如何切到到远程仓库分支进行开发呢? - `git checkout -b frommaster origin/frommaster` - // 本地新建frommaster分支并且与远程仓库的frommaster分支想关联 - 提交更改的话就用 - `git push origin frommaster` - - // 重命名分支 - `git branch -m new_branch wchar_support` - - -- `git merge`合并指定分支到当前分支 - `git merge devBranch`将`devBranch`分支合并到`master`。 - -- 打`tag` - `git tag v1.0`来进行打`tag`,默认为`HEAD` - `git tag`查看所有`tag` - 如果我想在之前提交的某次`commit`上打`tag`,`git tag v1.0 commitID` - 当然也可以在打`tag`时带上参数 `git tag v1.0 -m "version 1.0 released" commitID` - `git tag -d xxx`删除xxx - - `git show tagName`来查看某`tag`的详细信息。 -- 打完`tag`后怎么推送到远程仓库 - `git push origin tagName` - -- 删除`tag` - `git tag -d tagName` - -- 删除完`tag`后怎么推送到远程仓库,这个写法有点复杂 - `git push origin:refs/tags/tagName` - -- 忽略文件 +首先不同于 reset --hard,checkout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢。 其实它还更聪明一些。它会在工作目录中先试着 +简单合并一下,这样所有还未修改过的 文件都会被更新。而 reset --hard 则会不做检查就全面地替换所有东西。 + +第二个重要的区别是 checkout 如何更新 HEAD。 reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。 + +例如,假设我们有 master 和 develop 分支,它们分别指向不同的提交;我们现在在 develop 上(所以 HEAD 指向它)。 +如果我们运行 git reset master,那么 develop 自身现在会和 master 指向同一个提交。 而如果我们运行 git checkout master 的话, +develop 不会移动,HEAD 自身会移动。 现在 HEAD 将会指向 master。 + +所以,虽然在这两种情况下我们都移动 HEAD 使其指向了提交 A,但做法是非常不同的。 reset 会移动 HEAD 分支的指向,而checkout则移动HEAD自身。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-checkout.png?raw=true) + +#### 带路径 + +运行 checkout 的另一种方式就是指定一个文件路径,这会像 reset 一样不会移动 HEAD。 它就像 git reset [branch] file 那样用该次提交中的那个文件来更新索引,但是它也会覆盖工作目录中对应的文件。 它就像是 git reset --hard [branch] file(如果 reset 允许你这样运行的话), 这样对工作目录并不安全,它也不会移动 HEAD。 + +此外,同 git reset 和 git add 一样,checkout 也接受一个 --patch 选项,允许你根据选择一块一块地恢复文件内容。 + +### `git revert`撤销提交 + +`git revert`在撤销一个提交的同时会创建一个新的提交,这是一个安全的方法,因为它不会重写提交历史。 + +- `git revert`是生成一个新的提交来撤销某次提交,此次提交之前的`commit`都会被保留 +- `git reset`是回到某次提交,提交及之前的`commit`都会被保留,但是此次之后的修改都会被退回到暂存区 + +相比`git reset`它不会改变现在得提交历史。`git reset`是直接删除指定的`commit`并把`HEAD`向后移动了一下。而`git revert`是一次新的特殊的`commit`,`HEAD`继续前进,本质和普通`add commit`一样,仅仅是`commit`内容很特殊。内容是与前面普通`commit`变化的反操作。 +比如前面普通`commit`是增加一行`a`,那么`revert`内容就是删除一行`a`。 +在 Git 开发中通常会控制主干分支的质量,但有时还是会把错误的代码合入到远程主干。虽然可以直接回滚远程分支,但有时新的代码也已经合入, +直接回滚后最近的提交都要重新操作。 那么有没有只移除某些Commit的方式呢?可以用一次revert操作来完成。 + +考虑这个例子,我们提交了 6 个版本,其中 3-4 包含了错误的代码需要被回滚掉。 同时希望不影响到后续的 5-6。 + +```shell +* 982d4f6 (HEAD -> master) version 6 +* 54cc9dc version 5 +* 551c408 version 4, harttle screwed it up again +* 7e345c9 version 3, harttle screwed it up +* f7742cd version 2 +* 6c4db3f version 1 +``` +这种情况在团队协作的开发中会很常见:可能是流程或认为原因不小心合入了错误的代码,也可能是合入一段时间后才发现存在问题。 +总之已经存在后续提交,使得直接回滚不太现实。 + +下面的部分就开始介绍具体操作了,同时我们假设远程分支是受保护的(不允许 Force Push)。思路是从产生一个新的 Commit 撤销之前的错误提交。 + +使用 git revert 可以撤销指定的提交, 要撤销一串提交可以用 .. 语法。 注意这是一个前开后闭区间, +即不包括commit1,但包括commit2。 + +```shell +git revert --no-commit f7742cd..551c408 +git commit -a -m 'This reverts commit 7e345c9 and 551c408' +``` +其中 f7742cd 是 version 2,551c408 是 version 4,这样被移除的是 version 3 和 version 4。 +注意 revert 命令会对每个撤销的 commit 进行一次提交,--no-commit 后可以最后一起手动提交。 + +此时 Git 记录是这样的: + +```shell +* 8fef80a (HEAD -> master) This reverts commit 7e345c9 and 551c408 +* 982d4f6 version 6 +* 54cc9dc version 5 +* 551c408 version 4, harttle screwed it up again +* 7e345c9 version 3, harttle screwed it up +* f7742cd version 2 +* 6c4db3f version 1 +``` +现在的 HEAD(8fef80a)就是我们想要的版本,把它 Push 到远程即可。 + +git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中,本质上就是撤销或者倒转。 + +### `git rm`删除文件 + +该文件就不再纳入版本管理了。如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f(译注:即 force 的首字母),以防误删除文件后丢失修改的内容。 +另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。换句话说,仅是从跟踪清单中删除。 +比如一些大型日志文件或者一堆 .a 编译文件,不小心纳入仓库后,要移除跟踪但不删除文件,以便稍后在 .gitignore 文件中补上, +用 --cached 选项即可:`git rm --cached readme.txt` + +### 分支 + +`git`分支的创建和合并都是非常快的,因为增加一个分支其实就是增加一个指针,合并其实就是让某个分支的指针指向某一个位置。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_master_branch.png?raw=true) + +#### 创建分支 + +`git branch devBranch`创建名为`devBranch`的分支。 +`git checkout devBranch`切换到`devBranch`分支。 +`git checkout -b devBranch`创建+切换到分支`devBranch`。 +`git branch`查看当前仓库中的分支。 +`git branch -r`查看远程仓库的分支。 +`git branch -d devBranch`删除`devBranch`分支。 + +``` +origin/HEAD -> origin/master +origin/developer +origin/developer_sg +origin/master +origin/master_sg +origin/offline +``` +`git branch -d devBranch`删除`devBranch`分支。 +当时如果在新建了一个分支后进行修改但是还没有合并到其他分支的时候就去使用`git branch -d xxx`删除的时候系统会手提示说这个分支没有被合并,删除失败。 +这时如果你要强行删除的话可以使用命令`git branch -D xxx`. +如何删除远程分支呢? + +``` +git branch -r -d origin/developer +git push origin :developer +``` +如何本地创建分支并推送给远程仓库? + +``` +// 本地创建分支 +git checkout master //进入master分支 +git checkout -b frommaster //以master为源创建分支frommaster +// 推送到远程仓库 +git push origin frommaster// 推送到远程仓库所要使用的名字 +``` +如何切到到远程仓库分支进行开发呢? +`git checkout -b frommaster origin/frommaster` +// 本地新建frommaster分支并且与远程仓库的frommaster分支想关联 +提交更改的话就用 +`git push origin frommaster` + +// 重命名分支 +`git branch -m new_branch wchar_support` +// 查看每一个分支的最后一次提交 +`git branch -v` + +#### `git merge`合并指定分支到当前分支 + +`git merge devBranch`将`devBranch`分支合并到`master`。 + +### 打`tag` + +`git tag v1.0`来进行打`tag`,默认为`HEAD` +`git tag`查看所有`tag` +如果我想在之前提交的某次`commit`上打`tag`,`git tag v1.0 commitID` +当然也可以在打`tag`时带上参数 `git tag v1.0 -m "version 1.0 released" commitID` +`git tag -d xxx`删除xxx + +`git show tagName`来查看某`tag`的详细信息。 + +- 打完`tag`后怎么推送到远程仓库 + `git push origin tagName` +- 删除`tag` + `git tag -d tagName` +- 删除完`tag`后怎么推送到远程仓库,这个写法有点复杂 + `git push origin:refs/tags/tagName` +- 忽略文件 在`git`根目录下创建一个特殊的`.gitignore`文件,把想要忽略的文件名填进去就可以了,匹配模式最后跟斜杠(/)说明要忽略的是目录,#是注释 。 - 其实不用一个个的去写,具体可以根据项目参考[https://github.com/github/gitignore](https://github.com/github/gitignore) - 当然不要忘了把该文件提交上去 - 在用`linux`的时候会自动生成一些以`~`结尾的备份文件,如果ignore掉呢?[https://github.com/github/gitignore/blob/master/Global/Linux.gitignore](https://github.com/github/gitignore/blob/master/Global/Linux.gitignore) - -- 撤销最后一次提交 - 有时候我们提交完了才发现漏掉了几个文件没有加或者提交信息写错了,想要撤销刚才的的提交操作。可以修改后重新git add 然后使用`--amend`选项重新提交:`git commit --amend`,然后再执行`git push`操作。 - - - -- 查看远程仓库克隆地址 - `git remote -v` - -关于`git`的工作区、缓存区可以看下图`index`标记部分的区域就是暂存区 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_stage.jpg?raw=true) - -从这个图中能看到缓存区的存在,这就是为什么我们新加或者修改之后都要调用`git add`方法后再调用`git commit`。 -其实我一直有点分不开`reset`和`checkout`的区别,从这个图里能明显看出来了: - -- 当执行`git reset HEAD`命令时,暂存区的目录树会被重写,会被`master`分支指向的目录树所替换,但是工作区不受影响。 -- 当执行`git checkout .`或`git checkout -- file`命令是,会用暂存区全部的文件或指定的文件替换工作区的文件。这个操作很危险,会清楚工作区中未添加到暂存区的改动。 -命令时,会用`HEAD`指向的`master`分支中的全部或部分文件替换暂存区和工作区中的文件。这个命令也是极度危险的。因为不但会清楚工作区中未提交的改动,也会清楚暂存区中未提交的改动。 -- `git reset HEAD ` 是在添加到暂存区后,撤出暂存区使用,他只会把文件撤出暂存区,但是你的修改还在,仍然在工作区。当然如果使用`git reset --hard HEAD`这样就完了,工作区所有的内容都会被远程仓库最新代码覆盖。 -- `git checkout -- xxx.txt`是用于修改后未添加到暂存区时使用(如果修改后添加到暂存区后就没效果了,必须要先`reset`撤销暂存区后再使用`checkout`),这时候会把之前的修改覆盖掉。所以是危险的。 - - -- 隐藏操作 - - 假设您正在为产品新的功能编写/实现代码,当正在编写代码时,突然出现软件客户端升级。这时,您必须将新编写的功能代码保留几个小时然后去处理升级的问题。在这段时间内不能提交代码,也不能丢弃您的代码更改。 所以需要一些临时等待一段时间,您可以存储部分更改,然后再提交它。 - - 在`Git`中,隐藏操作将使您能够修改跟踪文件,阶段更改,并将其保存在一系列未完成的更改中,并可以随时重新应用。 - - 假设你现在在`a`分支上开发新版本内容,已经开发了一部分,但是还没有达到可以提交的程度。你需要切换到`b`分支进行另一个升级的开发。那么可以 - 把当前工作的改变隐藏起来,要将一个新的存根推到堆栈上,运行`git stash`命令。 - - ```shell - $ git stash - Saved working directory and index state WIP on master: ef07ab5 synchronized with the remote repository - HEAD is now at ef07ab5 synchronized with the remote repository - ``` - 现在,工作目录是干净的,所有更改都保存在堆栈中。 现在使用`git status`命令来查看当前工作区状态: - ```shell - $ git status - On branch master - Your branch is up-to-date with 'origin/master'. - - nothing to commit, working directory clean - ``` - - 现在,可以安全地切换分支并在其他地方工作。通过使用`git stash list`命令来查看已存在更改的列表。 - ```shell - $ git stash list - stash@{0}: WIP on master: ef07ab5 synchronized with the remote repository - ``` - - 假设您已经解决了客户升级问题,想要重新开始新的功能的代码编写,查找上次没有写完成的代码, - 只需执行`git stash pop`命令即可从堆栈中删除更改并将其放置在当前工作目录中。 - - 这样你之前隐藏的内容就会重新出现了,你可以继续开发了。 - -- Rebase操作 - - 多人在同一个分支上协作时,很容易出现冲突,即使没有冲突,在`push`代码之前也要先`pull`,在本地合并后再`push`,所以就经常会出现这样的分支: -```git -$ git log --graph --pretty=oneline --abbrev-commit -* d1be385 (HEAD -> master, origin/master) init hello -* e5e69f1 Merge branch 'dev' -|\ -| * 57c53ab (origin/dev, dev) fix env conflict -| |\ -| | * 7a5e5dd add env -| * | 7bd91f1 add new env -| |/ -* | 12a631b merged bug fix 101 -|\ \ -| * | 4c805e2 fix bug 101 -|/ / -* | e1e9c68 merge with no-ff -|\ \ -| |/ -| * f52c633 add merge -|/ -* cf810e4 conflict fixed + +### amend修改最后一次提交 + +有时候我们提交完了才发现漏掉了几个文件没有加或者提交信息写错了,想要撤销刚才的的提交操作。可以修改后重新git add 然后使用`--amend`选项重新提交:`git commit --amend`,然后再执行`git push`操作。 + +修补后的提交可能需要修补提交信息,当你在修补一次提交时,可以同时修改提交信息和提交内容。 如果你修补了提交的内容,那么几乎肯定要更新提交消息以反映修改后的内容。 + +另一方面,如果你的修补是琐碎的(如修改了一个笔误或添加了一个忘记暂存的文件), 那么之前的提交信息不必修改,你只需作出更改,暂存它们,然后通过以下命令避免不必要的编辑器环节即可: + +$ git commit --amend --no-edit + +#### 修改多个提交信息 + +为了修改在提交历史中较远的提交,必须使用更复杂的工具。 Git 没有一个改变历史工具,但是可以使用变基工具来变基一系列提交,基于它们原来的 HEAD 而不是将其移动到另一个新的上面。 通过交互式变基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情。 可以通过给 git rebase 增加 -i 选项来交互式地运行变基。 必须指定想要重写多久远的历史,这可以通过告诉命令将要变基到的提交来做到。 + +例如,如果想要修改最近三次提交信息,或者那组提交中的任意一个提交信息, 将想要修改的最近一次提交的父提交作为参数传递给 git rebase -i 命令,即 HEAD~2^ 或 HEAD~3。 记住 ~3 可能比较容易,因为你正尝试修改最后三次提交;但是注意实际上指定了以前的四次提交,即想要修改提交的父提交: + +$ git rebase -i HEAD~3 +再次记住这是一个变基命令——在 HEAD~3..HEAD 范围内的每一个修改了提交信息的提交及其 所有后裔 都会被重写。 +不要涉及任何已经推送到中央服务器的提交——这样做会产生一次变更的两个版本,因而使他人困惑。 +输入 + +$ git commit --amend +修改提交信息,然后退出编辑器。 然后,运行 + +$ git rebase --continue +这个命令将会自动地应用另外两个提交,然后就完成了。 如果需要将不止一处的 pick 改为 edit,需要在每一个修改为 edit 的提交上重复这些步骤。 每一次,Git 将会停止,让你修正提交,然后继续直到完成。 + +### 查看远程仓库克隆地址 + +`git remote -v` + +关于`git`的工作区、缓存区可以看下图`index`标记部分的区域就是暂存区 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_stage.jpg?raw=true) + +从这个图中能看到缓存区的存在,这就是为什么我们新加或者修改之后都要调用`git add`方法后再调用`git commit`。 + +### stash(贮藏) + +stash会处理工作目录的脏的文件--即跟踪文件的修改与暂存的改动--然后将未完成的修改保存到一个栈上,而你可以在任何时候重新应用这些改动(甚至在不同的分支上)。 +假设你现在在`a`分支上开发新版本内容,已经开发了一部分,但是还没有达到可以提交的程度。你需要切换到`b`分支进行另一个升级的开发。那么可以把当前工作的改变隐藏起来,要将一个新的存根推到堆栈上,运行`git stash`命令。 + +```shell +$ git stash +Saved working directory and index state WIP on master: ef07ab5 synchronized with the remote repository +HEAD is now at ef07ab5 synchronized with the remote repository ``` -看上去会很乱,有些强迫症的人会问:为什么`Git`的提交历史不能是一条干净的直线? -`rebase`操作就是解决这个问题的,它可以把分叉的提交历史整理变成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。 +现在,工作目录是干净的,所有更改都保存在堆栈中。 现在使用`git status`命令来查看当前工作区状态: -也就是说`gie merge`和`git rebase`做的事情其实是一样的。它们都被设计来将一个分支的更改并入到另一个分支中。 +```shell +$ git status +On branch master +Your branch is up-to-date with 'origin/master'. + +nothing to commit, working directory clean +``` +现在,可以安全地切换分支并在其他地方工作。通过使用`git stash list`命令来查看已存在更改的列表。 -- git fetch与git pull的区别 +```shell +$ git stash list +stash@{0}: WIP on master: ef07ab5 synchronized with the remote repository +``` +这个命令所储藏的修改可以使用`git stash list`列出,使用`git stash show`进行检查,并使用`git stash apply` +或`git stash apply stash@{2}`恢复(可能在不同的提交之上)。或者可以用git stash pop将最近的一次stash恢复。调用没有任何参数 +的`git stash`相当于`git stash save`。在17年10月下旬Git讨论废弃了git stash save命令,代之以现有的git stash push命令。 +可以使用git stash drop加上要移除的贮藏的名字来移除它。 - `git`中`fetch`命令是将远程分支的最新内容拉到了本地,但是`fecth`后是看不到变化的,如果查看当前的分支,会发现此时本地多了一个`FETCH_HEAD`的指针,`checkout`到该指针后才可以查看远程分支的最新内容。 +### 区间提交 - 而`git pull`的作用相当于`fetch`和`merge`的组合,会自动合并: +你想要查看 experiment 分支中还有哪些提交尚未被合并入 master 分支。 你可以使用 master..experiment 来让 Git 显示这些提交。也就是“在 experiment 分支中而不在 master 分支中的提交”。 - ```git - git fetch origin master - git merge FETCH_HEAD - ``` +```shell +git log master..experiment +``` +#### 三点 -- git pull 与git pull --rebase的使用 +这个语法可以选择出被两个引用之一包含但又不被两者同时包含的提交。 再看看之前双点例子中的提交历史。 如果你想看 master 或者 experiment 中包含的但不是两者共有的提交,你可以执行: - 使用下面的关系区别这两个操作: +```shell +git log master...experiment +``` +### Rebase操作 - ```git - git pull = git fetch + git merge - git pull --rebase = git fetch + git rebase - ``` - `git rebase`的过程中,有时会有`conflit`这时`Git`会停止`rebase`并让用户去解决冲突,解决完冲突后,用`git add`命令去更新这些内容,然后不用执行`git commit`,直接执行`git rebase --continue`这样`git`会继续`apply`余下的补丁。 +官网中将rebase翻译为变基,我感觉理解成改变基点,重新实现更容易理解一些。 +假设目前除master分支之外还有一个experiment分支: ---- +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-1.png?raw=true) +我们现在想要把master分支merge一下experiment分支的最新代码。整合分支最容易的方法是merge命令。它会把这两个分支的最新快照(C3和C4)以及两者 +最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(C5)并提交。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-2.png?raw=true) +其实,还有一种方法:你可以提取在C4中引入的补丁和修改,然后在C3的基础上应用一次。在Git中,这种操作就叫做变基(rebase)。 +你可以使用rebase命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。 +在这个例子中,你可以检出experiment分支,然后将它变基到master分支上: + +```shell +git checkout experiment +git rebase master +``` +它的原理是首先找到这两个分支(即当前分支experiment、变基操作的目标基底分支master)的最近共同祖先C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底C3,最后以此将之前另存为临时文件的修改依序引用。 +![将C4中的修改变基到C3上](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-3.png?raw=true) +现在回到master分支,进行一次快进合并。 + +```shell +git checkout master +git merge experiment +``` +![master分支的快进合并](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-4.png?raw=true) +此时C4'指向的快照就和上面直接用merge中的C5指向的快照一模一样了。这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 +你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。 +一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁--例如向某个他人维护的项目贡献代码时。在这种情况下,你首先在自己 +的分支里进行开发,当开发完成时你需要先将你的代码变基到orgin/master上,然后再向主项目提交修改。这样的话,该项目的维护者就不再需要 +进行整合工作,只需要快进合并即可。 +请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。变基是将一系列提交按照原有 +次序依次应用到另一分支上,而合并是把最终结果合在一起。 + +##### 更有趣的变基例子 + +在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用。假如你创建了一个主题分支server, +为服务端添加了一些功能,提交了C3和C4.然后从C3上创建了主题分支client,为客户端添加了一些功能,提交了C8和C9.最后,你回到server分支, +又提交了C10. +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-1.png?raw=true) +假设你希望将client中的修改合并到主分支并发布,但暂时并不想合并server中的修改,因为他们还需要经过更全面的测试。这时,你就可以使用 +git rebase命令的--onto选项,选中在client分支里但不在server分支里的修改(即C8和C9), +将它们在master分支上重放: `git rebase --onto master server client` +以上命令的意思是:取出client分支,找出它从server分支分歧之后的补丁,然后把这些补丁在master分支上重放一遍,让client看起来像直接 +基于master修改一样。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-2.png?raw=true) +现在可以快速合并master分支了(快速合并master分支,使之包含来自client分支的修改): + +```shell +git checkout master +git merge client +``` +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-3.png?raw=true) +接下来你决定将server分支中的修改也整合进来,使用git rebase 命令可以直接将主题分支(即这里的server)变基到基分支(即这里的master)上。这样做能省去你先切换到server分支,再对其进行变基命令的多个步骤。 +`git rebase master server` +如下图,将server中的修改变基到master上所示,server中的代码被“续”到了master后面。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-4.png?raw=true) +然后就可以快进合并主分支master了: + +```shell +git checkout master +git merge server +``` +至此,client和server分支中的修改都已经整合到主分支里了,你可以删除这两个分支,最终提交历史会变成下图的样子: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-5.png?raw=true) + +##### 变基的风险 -- 邮箱 :charon.chui@gmail.com -- Good Luck! +奇妙的变基也并非完美无缺,要用它得遵守一条准则: +如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。 +变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。如果你已经将提交推送至某个仓库,而其他人也已经从该 +仓库拉取提交并进行了后续工作,此时,如果你用git rebase命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的 +提交进行整合,如果接下来你还要拉去并整合他们修改过的提交,事情就会变的一团糟。 +让我们来看一个在公开仓库上执行变基操作所带来的问题。假设你从一个中央服务器克隆然后在它的基础上进行了一些开发。你的提交历史如下图: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-1.png?raw=true) +然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交 +记录就会变成这样: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-2.png?raw=true) +接下来,这个人又决定把合并操作回滚,改用变基。继而又用git push --force命令覆盖了服务器上的提交历史。之后你从服务器抓取更新,会发现 +多出来一些新的提交: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-3.png?raw=true) +结果就是你们两个人的处境都十分尴尬。如果你执行git pull命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库也会变成: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-4.png?raw=true) +这相当于是你将相同的内容又合并了一次,生成了一个新的提交。此时如果你执行git log命令,你会发现有两个调的作者、日期、日志居然是一样的, +这会令人感到混乱。此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,这会令人感到更加混乱。 +很明显对方并不想在提交历史中看到C4和C6,因为之前就是他把这两个提交丢弃的。 +##### 用变基解决变基 +如果你真的遭遇了类似的处境,Git 还有一些高级魔法可以帮到你。 如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。 - +实际上,Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和——即 “patch-id”。 + +如果你拉取被覆盖过的更新并将你手头的工作基于此进行变基的话,一般情况下 Git 都能成功分辨出哪些是你的修改,并把它们应用到新分支上。 + +举个例子,如果遇到前面提到的 有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交 那种情境,如果我们不是执行合并,而是执行 git rebase teamone/master, Git 将会: + +- 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7) +- 检查其中哪些提交不是合并操作的结果(C2,C3,C4) +- 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4') +- 把查到的这些提交应用在 teamone/master 上面 + +从而我们将得到与 你将相同的内容又合并了一次,生成了一个新的提交 中不同的结果,如图 在一个被变基然后强制推送的分支上再次执行变基 所示。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-5.png?raw=true) +要想上述方案有效,还需要对方在变基时确保 C4' 和 C4 是几乎一样的。 否则变基操作将无法识别,并新建另一个类似 C4 的补丁(而这个补丁很可能无法整洁的整合入历史,因为补丁中的修改已经存在于某个地方了)。 + +在本例中另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull。 又或者你可以自己手动完成这个过程,先 git fetch,再 git rebase teamone/master。 + +如果你只对不会离开你电脑的提交执行变基,那就不会有事。 如果你对已经推送过的提交执行变基,但别人没有基于它的提交,那么也不会有事。 如果你对已经推送至共用仓库的提交上执行变基命令,并因此丢失了一些别人的开发所基于的提交,那你就有大麻烦了,你的同事也会因此鄙视你。 + +如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行 git pull --rebase 命令,这样尽管不能避免伤痛,但能有所缓解。 + +#### 变基 vs. 合并 + +至此,你已在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。 在回答这个问题之前,让我们退后一步,想讨论一下提交历史 +到底意味着什么。 + +有一种观点认为,仓库的提交历史即是记录实际发生过什么。它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是 +一种亵渎,你使用谎言掩盖了实际发生过的事情。如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来, +让后人能够查阅。 + +另一种观点则正好相反,他们认为提交历史是项目过程中发生的事。没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 +持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。 + +现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对 +提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。 + +总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。 + +### git fetch与git pull的区别 + +`git`中`fetch`命令是将远程分支的最新内容拉到了本地,但是`fecth`后是看不到变化的,如果查看当前的分支,会发现此时本地多了 +一个`FETCH_HEAD`的指针,`checkout`到该指针后才可以查看远程分支的最新内容。 + +而`git pull`的作用相当于`fetch`和`merge`的组合,会自动合并: + +```git +git fetch origin master +git merge FETCH_HEAD +``` +### git pull 与git pull --rebase的使用 + +使用下面的关系区别这两个操作: + +```git +git pull = git fetch + git merge +git pull --rebase = git fetch + git rebase +``` +`git rebase`的过程中,有时会有`conflit`这时`Git`会停止`rebase`并让用户去解决冲突,解决完冲突后, +用`git add`命令去更新这些内容,然后不用执行`git commit`,直接执行`git rebase --continue`这样`git`会继续`apply`余下的补丁。 + +## 参考 + +[Git官方文档](https://git-scm.com/book/zh/v2) + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" index 5ebc846c..02f9c51c 100644 --- "a/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" +++ "b/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" @@ -1,55 +1,107 @@ HashMap实现原理分析 -=== - -HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null建和null值,因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。 - +=== +HashMap是Map接口的实现,元素以键值对的方式存储,并且允许使用null建和null值,因为key不允许重复,因此只能有一个键为null。 +HashMap被认为是Hashtable的增强版,HashMap是一个非线程安全的容器,如果想构造线程安全的Map考虑使用ConcurrentHashMap。 +HashMap是无序的,因为HashMap无法保证内部存储的键值对的有序性。 + +### 重要属性 + +- 初始容量 + HashMap的默认初始容量是由DEFAULT_INITIAL_CAPACITY属性管理的。 + `static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + HashMap的默认初始容量是 1 << 4 = 16, << 是一个左移操作,它相当于是: + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_1.webp) + +- 最大容量 + HashMap的最大容量是 + `static final int MAXIMUM_CAPACITY = 1 << 30;` + 这里是不是有个疑问?int 占用四个字节,按说最大容量应该是左移 31 位,为什么 HashMap 最大容量是左移 30 位呢?因为在数值计算中,最高位也就是最左位的位 是代表着符号为,0 -> 正数,1 -> 负数,容量不可能是负数,所以 HashMap 最高位只能移位到 2 ^ 30 次幂。 +- 默认负载因子 + HashMap的默认负载因子是 + `static final float DEFAULT_LOAD_FACTOR = 0.75f;` + float 类型所以用 .f 为单位,负载因子是和扩容机制有关,这里大致提一下,后面会细说。扩容机制的原则是当 HashMap 中存储的数量 > HashMap 容量 * 负载因子时,就会把 HashMap 的容量扩大为原来的二倍。 + HashMap 的第一次扩容就在 DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12 时进行。 +- 树化阈值 + HashMap 的树化阈值是 + `static final int TREEIFY_THRESHOLD = 8;` + + 在进行添加元素时,当一个桶中存储元素的数量 > 8 时,会自动转换为红黑树(JDK1.8 特性)。 +- 链表阈值 + HashMap 的链表阈值是 + `static final int UNTREEIFY_THRESHOLD = 6;` 在进行删除元素时,如果一个桶中存储元素数量 < 6 后,会自动转换为链表 - 扩容临界值 `static final int MIN_TREEIFY_CAPACITY = 64;` + 这个值表示的是当桶数组容量小于该值时,优先进行扩容,而不是树化 ## 原理 其底层数据结构是数组称之为哈希桶,每个桶(bucket)里面放的是链表,链表中的每个节点,就是哈希表中的每个元素。 -通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K / V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量 (超过 Load Facotr则resize为原来的2倍)。获取对象时,我们 K传给get,它调用hashCodeO()计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在JDK8中,如果一个bucket中碰撞冲突的元素超过8哥,则使用红黑树来替换链表,从而提高速度。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_hash.webp) +哈希表中哈希函数的设计是相当重要的,这也是建哈希表过程中的关键问题之一。 +建立一个哈希表之前需要解决两个主要问题: + +- 构造一个合适的哈希函数,均匀性 H(key)的值均匀分布在哈希表中 +- 冲突的处理 + +冲突:在哈希表中,不同的关键字值对应到同一个存储位置的现象。 + +当一个值中要存储到HashMap中的时候会根据Key的值来计算出他的hash,通过hash值来确认存放到数组中的位置,如果发生hash冲突就以链表的形式存储,当链表过长的话,HashMap会把这个链表转换成红黑树来存储通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K / V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量 (超过Load Factor则resize为原来的2倍)。 +获取对象时,我们将K传给get()方法,它调用hashCode()计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。 + +扩容前后,哈希桶的长度一定会是2的次方。这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作,更加高效。 + +而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。 -因其底层哈希桶的数据结构是数组,所以也会涉及到扩容的问题。当HashMap的容量达到threshold域值时,就会触发扩容。扩容前后,哈希桶的长度一定会是2的次方。这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作,更加高效。而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。 但就算原本的hashCode()取得很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。 即,碰撞率会增大。扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量如果追加节点后,链表数量》=8,则转化为红黑树由迭代器的实现可以看出,遍历HashMap时,顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。 +因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。 +但就算原本的hashCode()取的很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。 +因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。即碰撞率会增大。 +扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。 + +扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 + +high位= low位+原哈希桶容量,如果追加节点后,链表数量>=8,且只有数组长度大于64才处理,则转化为红黑树由迭代器的实现可以看出,遍历HashMap时, +顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。 + +数组的特点:查询效率高,插入删除效率低。 +链表的特点:查询效率低,插入删除效率高。 + +在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。 ## JDK1.7 -HashMap在JDK1.8中发生了改变,下面的部分是基于JDK1.7的分析。HashMap主要是用数组来存储数据的,我们都知道它会对key进行哈希运算,哈希运算会有重复的哈希值,对于哈希值的冲突,HashMap采用链表来解决的。 +HashMap在JDK1.8中发生了改变,下面的部分是基于JDK1.7的分析。HashMap主要是用数组来存储数据的,我们都知道它会对key进行哈希运算, +哈希运算会有重复的哈希值,对于哈希值的冲突,HashMap采用链表来解决的。 在HashMap里有这样的一句属性声明: ```java transient Entry[] table; ``` -可以看到Map是通过数组的方式来储存Entry那Entry是神马呢?就是HashMap存储数据所用的类,它拥有的属性如下: +可以看到Map是通过数组的方式来储存Entry,那Entry是神马呢?就是HashMap存储数据所用的类,它拥有的属性如下: ```java static class Entry implements Map.Entry { - final K key; - V value; - Entry next; - final int hash; - ...//More code goes here + final K key; + V value; + Entry next; + final int hash; + ...//More code goes here } ``` -看到next了吗?next就是为了哈希冲突而存在的。比如通过哈希运算,一个新元素应该在数组的第10个位置,但是第10个位置已经有Entry,那么好吧,将新加的元素也放到第10个位置,将第10个位置的原有Entry赋值给当前新加的Entry的next属性。数组存储的是链表,链表是为了解决哈希冲突的,这一点要注意。 +看到next了吗?next就是为了哈希冲突而存在的。比如通过哈希运算,一个新元素应该在数组的第10个位置,但是第10个位置已经有Entry, +那么好吧,将新加的元素也放到第10个位置,将第10个位置的原有Entry赋值给当前新加的Entry的next属性。 +数组存储的是链表,链表是为了解决哈希冲突的,这一点要注意。 好了,总结一下: - HashMap中有一个叫table的Entry数组。这个数组存储了Entry类的对象。Entry类包含了key-value作为实例变量。 - - table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。 - -- 每当往Hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的Entry数组table中。根据key的hashcode()方法计算出来的hash值来决定在Entry数组的索引(所在的桶)。 - +- 每当往Hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的Entry数组table中。 根据key的hashcode()方法计算出来的hash值来决定在Entry数组的索引(所在的桶)。 - 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。 - - key的equals()方法用来确保key的唯一性。 - 接下来看一下put方法: @@ -70,33 +122,33 @@ static class Entry implements Map.Entry { */ public V put(K key, V value) { // 对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。 - if (key == null) - return putForNullKey(value); - // 计算key的hash值,hash值用来找到存储Entry对象的数组的索引。有时候hash函数可能写的很不好,所以JDK的设计者添加了另一个叫做hash()的方法,它接收刚才计算的hash值作为参数 - int hash = hash(key.hashCode()); - // indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引 - int i = indexFor(hash, table.length); - - for (Entry e = table[i]; e != null; e = e.next) { - // 如果这个位置已经有了(也就是hash值一样)就用链表来存了。 开始迭代链表 - Object k; - // 直到Entry->next为null,就把当前的Entry对象变成链表的下一个节点。 - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { - // 如果我们再次放入同样的key会怎样呢?它会替换老的value。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)), - // 如果这个方法返回true,它就会用当前Entry的value来替换之前的value。 - V oldValue = e.value; - e.value = value; - e.recordAccess(this); - return oldValue; - } - } - // 如果计算出来的索引位置没有元素,就直接把Entry对象放到那个索引上。 - modCount++; - addEntry(hash, key, value, i); - return null; + if (key == null) + return putForNullKey(value); + // 计算key的hash值,hash值用来找到存储Entry对象的数组的索引。有时候hash函数可能写的很不好,所以JDK的设计者添加了另一个叫做hash()的方法,它接收刚才计算的hash值作为参数 + int hash = hash(key.hashCode()); + // indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引 + int i = indexFor(hash, table.length); + + for (Entry e = table[i]; e != null; e = e.next) { + // 如果这个位置已经有了(也就是hash值一样)就用链表来存了。 开始迭代链表 + Object k; + // 直到Entry->next为null,就把当前的Entry对象变成链表的下一个节点。 + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { + // 如果我们再次放入同样的key会怎样呢?它会替换老的value。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)), + // 如果这个方法返回true,它就会用当前Entry的value来替换之前的value。 + V oldValue = e.value; + e.value = value; + e.recordAccess(this); + return oldValue; + } + } + // 如果计算出来的索引位置没有元素,就直接把Entry对象放到那个索引上。 + modCount++; + addEntry(hash, key, value, i); + return null; } ``` -再看一下get方法:     +再看一下get方法: ```java /** * Returns the value to which the specified key is mapped, or {@code null} @@ -118,31 +170,32 @@ public V put(K key, V value) { */ public V get(Object key) { // 如果key是null,table[0]这个位置的元素将被返回。 - if (key == null) - return getForNullKey(); - // 计算hash值 - int hash = hash(key.hashCode()); - // indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。 - for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { - Object k; - // 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。 - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) - return e.value; - } - return null; + if (key == null) + return getForNullKey(); + // 计算hash值 + int hash = hash(key.hashCode()); + // indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。 + for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { + Object k; + // 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。 + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) + return e.value; + } + return null; } ``` +## JDK1.8 +在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变的,只是在一些地方做了优化,下面来看一下这些改变的地方,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,且只有数组长度大于64才处理,将链表转换为红黑树。 -## JDK1.8 +利用红黑树快速增删改查的特点来提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。 -在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变得,只是在一些地方做了优化,下面来看一下这些改变的地方,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树。利用红黑树快速增删改查的特点来提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。HashMap中,如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话,getKey方法的时间复杂度就是O(1),如果Hash算法技术的结果碰撞非常多,假如Hash算极其差,所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中,或者在一个链表中,或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn)。 +HashMap中,如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话,getKey方法的时间复杂度就是O(1),如果Hash算法技术的结果碰撞非常多,假如Hash算法极其差,所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中,或者在一个链表中,或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn)。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_data_structure.jpg) - 既然发生了变化,那这里就以1.8的源码基础上再做一下分析: 1. 首先看一下对应的两个Node类: @@ -167,7 +220,7 @@ public V get(Object key) { public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } - // 每一个节点的hashcode值,是将key和value的hashCode值异或得到的 + // 每一个节点的hashcode值,是将key和value的hashCode值异或得到的 public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } @@ -217,7 +270,7 @@ public V get(Object key) { * @return root of tree */ final void treeify(Node[] tab) { - ... + ... } /** @@ -229,7 +282,7 @@ public V get(Object key) { } /** - * 红黑树的插入 + * 红黑树的插入 * Tree version of putVal. */ final TreeNode putTreeVal(HashMap map, Node[] tab, @@ -285,7 +338,7 @@ public V get(Object key) { ```java public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { - // 默认的初始化容量大小,必须是2的倍数,默认是16,为啥必须是2的倍数,后面会说 + // 默认的初始化容量大小,必须是2的倍数,默认是16,为啥必须是2的倍数,后面会说 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 在构造函数中未指定时使用的负载因子。 static final float DEFAULT_LOAD_FACTOR = 0.75f; @@ -329,7 +382,10 @@ public V get(Object key) { ### 确定哈希桶数组索引位置 -不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。先看看源码的实现: +不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合, +所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的 +时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的 +离散性能。先看看源码的实现: ```java static final int hash(Object key) { @@ -338,18 +394,22 @@ static final int hash(Object key) { } ``` -hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。为什么要这样设计呢? 主要有两个原因 : +hash函数是先拿到通过key的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。为什么要这样设计呢? +主要有两个原因 : 1. 一定要尽可能降低hash碰撞,越分散越好。 2. 算法一定要尽可能高效,因为这是高频操作,因此采用位运算。 -因为hashcode是32位的int值int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比%运算要快。 - - +因为hashcode是32位的int值int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散, +一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算, +得到的余数才能用来访问数组下标。源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比%运算要快。 - -对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用所计算得到的Hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的,在HashMap中是这样做的:调用 (table.length -1) & hash来计算该对象应该保存在table数组的哪个索引处。这个方法非常巧妙,它通过 (table.length -1) & hash来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,当length总是2的n次方时, (table.length -1) & hash运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。 +对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用所计算得到的Hash值总是相同的。我们首先想到的就是把hash值对数组长度 +取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的, +在HashMap中是这样做的:调用 (table.length -1) & hash来计算该对象应该保存在table数组的哪个索引处。 +这个方法非常巧妙,它通过 (table.length -1) & hash来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方, +当length总是2的n次方时, (table.length -1) & hash运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。 当length总是2的倍数时,h & (length-1) 将是一个非常巧妙的设计: @@ -359,16 +419,16 @@ hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashc - 但是当h=16时 , length=16 时,那么h & length - 1将得到0了; - 当h=17时, length=16时,那么h & length - 1将得到1了。 -这样保证计算得到的索引值总是位于 table 数组的索引之内。 - +这样保证计算得到的索引值总是位于table数组的索引之内。 -在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。 +在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16), +主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中, +同时不会有太大的开销。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_hash.bmp) - ### put ```java @@ -389,7 +449,7 @@ hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashc // 3. 如果tab[索引]的值为null,那就说明这个索引没有数组桶,直接新建一个数组桶 tab[i] = newNode(hash, key, value, null); else { - // 4. 否则就是目前已经存在该索引的数组桶了,要继续判断是链表还是红黑树。 + // 4. 否则就是目前已经存在该索引的数组桶了,要继续判断是链表还是红黑树。 Node e; K k; // 5. 判断table[索引]处的收个元素是否和key一样,如果首个就是那就直接覆盖value,不用再找了 if (p.hash == hash && @@ -400,7 +460,7 @@ hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashc // 7. 数组桶首个元素不是,并且链表是红黑树,红黑树直接插入键值对 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else { - // 8. 目前为链表,开始遍历链表准备插入 + // 8. 目前为链表,开始遍历链表准备插入 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 添加到目前的节点的next上 @@ -475,27 +535,22 @@ final void treeifyBin(Node[] tab, int hash) { resize()方法用于初始化数组或数组扩容,每次扩容后容量为原来的2倍,并进行数据迁移。 - -例如我们从 16 扩展为 32 时,具体的变化如下所示: - +例如我们从16扩展为32时,具体的变化如下所示: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize1.bmp) - - - -因此元素在重新计算hash之后,因为n变为 2 倍,那 n-1的mask范围在高位多1bit (红色),因此新的 index就会发生这样的变化: - +因此元素在重新计算hash之后,因为n变为2倍,那n-1的mask范围在高位多1bit (红色),因此新的index就会发生这样的变化: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize2.bmp) - -因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成 “原索引 + oldCap”。可以看看下图为16扩充为32的resize示意图: - +因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了, +是0的话索引没变,是1的话索引变成 “原索引 + oldCap”。可以看看下图为16扩充为32的resize示意图: ![resize](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize3.bmp) +这里有一个需要注意的点就是在JDK1.8 HashMap扩容阶段重新映射元素时不需要像1.7版本那样重新去一个个计算元素的hash值,而是通过hash & oldCap的值来判断,若为0则索引位置不变,不为0则新索引=原索引+旧数组长度,为什么呢?具体原因如下: +因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap - -这个设计确实非常的巧妙,既省去了重新计算 hash 值的时间,而且同时,**由于新增的 1bit 是 0 还是 1 可以认为是随机的,因此 resize 的过程,均匀的把之前的冲突的节点分散到新的 bucket 了**。 +这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,**由于新增的1bit是0还是1可以认为是随机的,因此resize的过程, +均匀的把之前的冲突的节点分散到新的bucket了**。 ```java final Node[] resize() { @@ -602,7 +657,8 @@ final Node[] resize() { } ``` -再看一下往哈希表里插入一个节点的putVal函数,如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。如果evict是false。那么表示是在初始化时调用的 +再看一下往哈希表里插入一个节点的putVal函数,如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。 +如果evict是false,那么表示是在初始化时调用的。 ```java final V putVal(int hash, K key, V value, boolean onlyIfAbsent, @@ -673,11 +729,11 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, * 运算尽量都用位运算代替,更高效。 * 对于扩容导致需要新建数组存放更多元素时,除了要将老数组中的元素迁移过来,也记得将老数组中的引用置null,以便GC -* 取下标 是用 哈希值 与运算 (桶的长度-1) i = (n - 1) & hash。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高 +* 取下标 是用哈希值与运算 (桶的长度-1) i = (n - 1) & hash。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高 * 扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。 * 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量 * 利用哈希值 与运算 旧的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点 -* 如果追加节点后,链表数量》=8,则转化为红黑树 +* 如果追加节点后,链表数量 >= 8,则转化为红黑树 * 插入节点操作时,有一些空实现的函数,用作LinkedHashMap重写使用。 @@ -713,39 +769,51 @@ final Node getNode(int hash, Object key) { ``` +## JDK 7与JDK 8中关于HashMap的对比 - - -## JDK 7 与 JDK 8 中关于 HashMap的对比 - -1. JDK8为红黑树 + 链表 + 数组的形式,当桶内元素大于8时,便会树化。 +1. JDK8为红黑树 + 链表 + 数组的形式,当桶内元素大于8时,且只有数组长度大于64才处理,便会树化。 2. hash值的计算方式不同 (jdk 8 简化)。 3. JDK7中table在创建hashmap时分配空间,而8中在put的时候分配。 4. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;因为头插法会使链表发生反转,多线程环境下会产生环; 5. 在resize操作中,7 需要重新进行index的计算,而8不需要,通过判断相应的位是0还是1,要么依旧是原index,要么是oldCap + 原 index。 6. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容; +把链表转换成红黑树,树化需要满足以下两个条件: +- 链表长度大于等于8 +- table数组长度大于等于64 +为什么table数组容量大于等于64才树化? +因为当table数组容量比较小时,键值对节点 hash 的碰撞率可能会比较高,进而导致链表长度较长。这个时候应该优先扩容,而不是立马树化。 ## 问题 1. 为什么capcity是2的幂? - 因为算index时用的是(n-1) & hash,这样就能保证n-1是全为1的二进制数,如果不全为1的话,存在某一位为 0,那么0,1与0与的结果都是0,这样便有可能将两个hash不同的值最终装入同一个桶中,造成冲突。所以必须是2的幂。这是为了服务key映射到index的Hash算法的,公式index=hashcode(key)&(length-1),初始长度(16-1),二进制为1111&hashcode结果为hashcode最后四位,能最大程度保持平均,二的幂数保证二进制为1,保持hashcode最后四位。这种算法在保持分布均匀之外,效率也非常高。 + + 因为算index时用的是(n-1) & hash,这样就能保证n-1是全为1的二进制数,如果不全为1的话,存在某一位为0,那么0,1与0与的结果都是0, + 这样便有可能将两个hash不同的值最终装入同一个桶中,造成冲突。所以必须是2的幂。这是为了服务key映射到index的Hash算法的, + 公式index=hashcode(key)&(length-1),初始长度(16-1),二进制为1111&hashcode结果为hashcode最后四位,能最大程度保持平均,二的幂数保证二进制为1,保持hashcode最后四位。这种算法在保持分布均匀之外,效率也非常高。 2. 为什么需要使用加载因子,为什么需要扩容呢? - 因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率。HashMap 本来是以空间换时间,所以填充比没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。 + 因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多), + 扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率。HashMap本来是以空间换时间,所以填充比没必要太大。 + 但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。 3. 为什么 HashMap 是线程不安全的,实际会如何体现? - - 如果多个线程同时使用put方法添加元素,假设正好存在两个put的key发生了碰撞 (hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。 -- 如果多个线程同时检测到元素个数超过数组大小 * loadFactor。这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。且会引起死循环的错误。 + 如果多个线程同时使用put方法添加元素,假设正好存在两个put的key发生了碰撞 (hash值一样),那么根据HashMap的实现, + 这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。 + 如果多个线程同时检测到元素个数超过数组大小 * loadFactor。这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置 + 以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。 + 且会引起死循环的错误。 4. 与HashTable的区别 - Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,它的并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。 + Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable, + 它的并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换, + 需要线程安全的场合可以用ConcurrentHashMap替换。 - 与之相比HashTable是线程安全的,且不允许key、value是null。 - HashTable默认容量是11。 @@ -754,15 +822,17 @@ final Node getNode(int hash, Object key) { - 扩容时,新容量是原来的2倍+1。int newCapacity = (oldCapacity << 1) + 1; - Hashtable是Dictionary的子类同时也实现了Map接口,HashMap是Map接口的一个实现类 -5. 扩容的时候为什么1.8 不用重新hash就可以直接定位原节点在新数据的位置呢? +5. 扩容的时候为什么1.8不用重新hash就可以直接定位原节点在新数据的位置呢? 这是由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,举个例子: + 扩容前长度为16,用于计算 (n-1) & hash 的二进制n - 1为0000 1111, + 扩容后为32后的二进制就高位多了1,============>为0001 1111。因为是&运算,1和任何数&都是它本身,那就分二种情况, + 原数据hashcode高位第4位为0和高位为1的情况;第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)。 - 扩容前长度为16,用于计算 (n-1) & hash 的二进制n - 1为0000 1111, +6. HashMap和HashSet的区别: - 扩容后为32后的二进制就高位多了1,============>为0001 1111。因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,如下图:原数据hashcode高位第4位为0和高位为1的情况;第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)。 - - +HashSet继承于AbstractSet接口,实现了Set、Cloneable、java.io.Serializable接口。HashSet不允许集合中出现重复的值。HashSet底层其实 +就是HashMap,所有对HashSet的操作其实就是对HashMap的操作。所以HashSet也不保证集合的顺序。 参考: @@ -773,11 +843,6 @@ final Node getNode(int hash, Object key) { - - - --- - 邮箱 :charon.chui@gmail.com - Good Luck! - - diff --git "a/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" "b/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" index 90346bfb..e24c20ec 100644 --- "a/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" +++ "b/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" @@ -9,8 +9,14 @@ HTTP与HTTPS的区别 - 1997年发布HTTP/1.1: 持久连接(长连接)、节约带宽、HOST域、管道机制、分块传输编码。 + 长连接是指的TCP连接,也就是说复用的是TCP连接。即长连接情况下,多个HTTP请求可以复用同一个TCP连接,这就节省了很多TCP连接建立和断开的消耗。 + + 此外,长连接并不是永久连接的。如果一段时间内(具体的时间长短,是可以在header当中进行设置的,也就是所谓的超时时间),这个连接没有HTTP请求发出的话,那么这个长连接就会被断掉。 + - 2015年发布HTTP/2:多路复用、服务器推送、头信息压缩、二进制协议等。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/http1.1vs2.jpg) 多路复用:通过单一的HTTP/2连接请求发起多重的请求-响应消息,多个请求stream共享一个TCP连接,实现多留并行而不是依赖建立多个TCP连接。 @@ -27,6 +33,25 @@ HTTP与HTTPS的区别 - 传输速度快 + + +### HTTP请求响应过程 + +你是不是很好奇,当你在浏览器中输入网址后,到底发生了什么事情?你想要的内容是如何展现出来的?让我们通过一个例子来探讨一下,我们假设访问的 URL 地址为 `http://www.someSchool.edu/someDepartment/home.index`,当我们输入网址并点击回车时,浏览器内部会进行如下操作 + +- DNS服务器会首先进行域名的映射,找到访问`www.someSchool.edu`所在的地址,然后HTTP客户端进程在80端口发起一个到服务器`www.someSchool.edu`的TCP连接(80端口是HTTP的默认端口)。在客户和服务器进程中都会有一个`套接字`与其相连。 +- HTTP客户端通过它的套接字向服务器发送一个HTTP请求报文。该报文中包含了路径`someDepartment/home.index`的资源,我们后面会详细讨论HTTP请求报文。 +- HTTP服务器通过它的套接字接受该报文,进行请求的解析工作,并从其`存储器(RAM 或磁盘)`中检索出对象 [www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到](http://www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到)HTTP响应报文中,并通过套接字向客户进行发送。 +- HTTP服务器随即通知TCP断开TCP连接,实际上是需要等到客户接受完响应报文后才会断开TCP连接。 +- HTTP客户端接受完响应报文后,TCP连接会关闭。HTTP客户端从响应中提取出报文中是一个HTML响应文件,并检查该HTML文件,然后循环检查报文中其他内部对象。 +- 检查完成后,HTTP客户端会把对应的资源通过显示器呈现给用户。 + +至此,键入网址再按下回车的全过程就结束了。上述过程描述的是一种简单的`请求-响应`全过程,真实的请求-响应情况可能要比上面描述的过程复杂很多。 + + + + + ## HTTPS Https并非是应用层的一种新协议。只是http通信接口部分用SSL(安全套接字层)和TLS(安全传输层协议)代替而已。即添加了加密及认证机制的HTTP称为HTTPS(HTTP Secure). @@ -135,7 +160,21 @@ HTTP + 加密 + 认证 + 完整性保护 = HTTPS +### Https 请求慢的解决办法 + +1. 不通过DNS解析,直接访问IP + +2. 解决连接无法复用 + + http/1.0协议头里可以设置Connection:Keep-Alive或者Connection:Close,选择是否允许在一定时间内复用连接(时间可由服务器控制)。但是这对App端的请求成效不大,因为App端的请求比较分散且时间跨度相对较大。 + + 方案1.基于tcp的长连接 (主要) 移动端建立一条自己的长链接通道,通道的实现是基于tcp协议。基于tcp的socket编程技术难度相对复杂很多,而且需要自己定制协议。但信息的上报和推送变得更及时,请求量爆发的时间点还能减轻服务器压力(避免频繁创建和销毁连接) + + 方案2.http long-polling 客户端在初始状态发送一个polling请求到服务器,服务器并不会马上返回业务数据,而是等待有新的业务数据产生的时候再返回,所以链接会一直被保持。一但结束当前连接,马上又会发送一个新的polling请求,如此反复,保证一个连接被保持。 存在问题: 1)增加了服务器的压力 2)网络环境复杂场景下,需要考虑怎么重建健康的连接通道 3)polling的方式稳定性不好 4)polling的response可能被中间代理cache住 …… + + 方案3.http streaming 和long-polling不同的是,streaming方式通过再server response的头部增加“Transfer Encoding:chuncked”来告诉客户端后续还有新的数据到来 存在问题: 1)有些代理服务器会等待服务器的response结束之后才将结果推送给请求客户端。streaming不会结束response 2)业务数据无法按照请求分割 …… + 方案4.web socket 和传统的tcp socket相似,基于tcp协议,提供双向的数据通道。它的优势是提供了message的概念,比基于字节流的tcp socket使用更简单。技术较新,不是所有浏览器都提供了支持。 @@ -151,4 +190,4 @@ HTTP + 加密 + 认证 + 完整性保护 = HTTPS - 邮箱 :charon.chui@gmail.com - Good Luck! - \ No newline at end of file + \ No newline at end of file diff --git "a/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" "b/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" index 1b1dfb51..6e1a10a4 100644 --- "a/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" +++ "b/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" @@ -1,74 +1,84 @@ # JVM垃圾回收机制 +当前主流的商用程序语言(Java、C#)的内存管理子系统都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。 -## JVM内存模式 +这个算法的基本思路就是通过一系列被称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所做过的路径称为"引用链"(Reference Chain), +如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。 -JVM内存模型可以分为两个部分,如下图所示,**堆和方法区是所有线程共有的**,而虚拟机栈,本地方法栈和程序计数器则是线程私有的。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm.png) - -### 堆(Heap) +如下图: 对象object5、object6、object7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。 -Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。 -**堆空间的基本结构:** +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reachability_analysis.png) -![](http://raw.githubusercontent.com/CharonChui/Pictures/master/java_heap.png) -在我们垃圾回收的时候,我们往往将堆内存分成**新生代和老生代(大小比例1:2)**,新生代中由Eden和Survivor0,Survivor1组成,**三者的比例是8:1:1**,新生代的回收机制采用**复制算法**,大部分情况,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入s0或者s1,并且对象的年龄还会加 1(Eden区->Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。老生代采用的回收算法是**标记整理算法。** +在Java技术体系里面,固定可作为GC Roots的对象包括以下几种: +- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。 +- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 -#### 对象优先在eden区分配 +- 在方法区中常量引用的对象,譬如字符串常量池(String Table)中的引用。 -目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 +- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。 -大多数情况下,对象在新生代中eden区分配。当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC. +- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器。 -这里说一下Minor GC 和 Full GC 有什么不同呢? +- 所有被同步锁(synchronized关键字)持有的对象。 -- 新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。 -- 老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。 +- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。 -#### 大对象直接进入老年代 -大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。这样做主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。 +除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。譬如后文将会提到的分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集)​,必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不可见的)​,更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。 -#### 长期存活的对象将进入老年代 -既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 -如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(一般都会说默认为 15 岁,其实默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6.),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 +[Java内存模型](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.md)如下图所示,**堆和方法区是所有线程共有的**,而虚拟机栈,本地方法栈和程序计数器则是线程私有的。 -### 方法区(Method Area) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm.png) -方法区也称”永久代“,它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB(64位JVM由于指针膨胀,默认是85M),可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。它是一片连续的堆空间,永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。不过,一个明显的问题是,当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,应用将会报OOM的错误。参数是通过-XX:PermSize和-XX:MaxPermSize来设定的。 +### 堆(Heap) +Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。Java的自动内存管理主要是针对对象内存的回收和对象 +内存的分配。同时,Java自动内存管理最核心的功能是堆内存中对象的分配与回收。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法, +所以Java堆还可以细分为:新生代和老年代,再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收 +内存,或者更快地分配内存。 +**堆空间的基本结构:** -### 虚拟机栈(JVM Stack) +![](http://raw.githubusercontent.com/CharonChui/Pictures/master/java_heap.png) -描述的是java方法执行的内存模型:每个方法被执行的时候都会创建一个”栈帧”,用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程**。**声明周期与线程相同,是线程私有的。栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区被组织为以一个字长为单位、从0开始计数的数组,和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的,可以看作为临时数据的存储区域。除了局部变量区和操作数栈外,java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在java栈帧的帧数据区中。 +在垃圾回收的时候,我们往往将堆内存分成**新生代和老生代(大小比例1:2)**,新生代中由Eden和Survivor0,Survivor1组成,**三者的比例是8:1:1**, +新生代的回收机制采用**复制算法**,大部分情况,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入s0或者s1,并且对象的 +年龄还会加1(Eden区->Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄 +阈值,可以通过参数`-XX:MaxTenuringThreshold`来设置。老生代采用的回收算法是**标记整理算法。** -局部变量表: 存放了编译器可知的各种基本数据类型、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间**在**编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。 +#### 对象优先在eden区分配 +目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 +大多数情况下,对象在新生代中eden区分配。当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC. -### 本地方法栈(Native Stack) +这里说一下Minor GC和Full GC有什么不同呢? -与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务(栈的空间大小远远小于堆)。 +- 新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。 +- 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。 -### 程序计数器(PC Register) +#### 大对象直接进入老年代 -最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令**,**分支**、**循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。 +大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。这样做主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。 -### 直接内存 +#### 长期存活的对象将进入老年代 -直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小. +既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对 +象一个对象年龄(Age)计数器。 +如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在 +Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(一般都会说默认为 15 岁,其实默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6), +就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数`-XX:MaxTenuringThreshold`来设置。 ## 如何判断对象是垃圾 @@ -93,9 +103,6 @@ ObjB.obj - ObjA ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/yinyongjishu.jpg) - - - ### 根搜索算法 根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图, @@ -113,7 +120,7 @@ ObjB.obj - ObjA 垃圾回收算法 --- -而手机后的垃圾是通过什么算法来回收的呢: +而收集后的垃圾是通过什么算法来回收的呢: - 标记-清除算法 - 复制算法 @@ -138,7 +145,9 @@ ObjB.obj - ObjA ### 标记复制算法(mark-copy) ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/fuzhisuanfa.png) -原理:思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。避免了内存碎片问题,但是内存浪费很严重,相当于只能使用 50% 的内存。。从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉 +原理:思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。避免了内存碎片 +问题,但是内存浪费很严重,相当于只能使用50%的内存。从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存 +(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉 适用场合: @@ -154,7 +163,9 @@ ObjB.obj - ObjA ### 标记-整理算法(Mark-Compact) ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_zhengli.png) -原理:从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象)(可以发现前边这些就是标记-清除算法的原理),清除完之后,将所有的存活对象左移到一起。 避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理),保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低 GC 的效率。 +原理:从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象)(可以发现前边这些就是标记-清除算法的原理), +清除完之后,将所有的存活对象左移到一起。避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理), +保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低GC的效率。 适用场合: @@ -171,75 +182,84 @@ ObjB.obj - ObjA ### 分代收集算法(Generational Collection) -上述三种算法,每种都有各自的优缺点,都不完美。在现代 JVM 中,往往是综合使用的,经过大量实际分析,发现内存中的对象,大致可以分为两类:有些生命周期很短,比如一些局部变量 / 临时对象,而另一些则会存活很久,典型的比如 websocket 长连接中的 connection 对象,如下图: +上述三种算法,每种都有各自的优缺点,都不完美。在现代JVM中,往往是综合使用的,经过大量实际分析,发现内存中的对象,大致可以分为两类: +有些生命周期很短,比如一些局部变量/临时对象,而另一些则会存活很久,典型的比如websocket长连接中的connection对象,如下图: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/62226d097ac54148804119b4c239c802.png) -纵向 y 轴可以理解分配内存的字节数,横向 x 轴理解为随着时间流逝(伴随着 GC),可以发现大部分对象其实相当短命,很少有对象能在 GC 后活下来。因此诞生了分代的思想,以 Hotspot 为例(JDK 7): +纵向y轴可以理解分配内存的字节数,横向x轴理解为随着时间流逝(伴随着 GC),可以发现大部分对象其实相当短命,很少有对象能在GC后活下来。 +因此诞生了分代的思想,以Hotspot为例(JDK 7): ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/e4ff361d409b6939e6da49a06b6dc677.png) -将内存分成了三大块:年青代(Young Genaration),老年代(Old Generation), 永久代(Permanent Generation),其中 Young Genaration 更是又细为分 eden,S0,S1 三个区。 +将内存分成了三大块:年青代(Young Genaration),老年代(Old Generation), 永久代(Permanent Generation),其中Young Genaration更是又细分为eden, +S0,S1三个区。 -结合我们经常使用的一些 jvm 调优参数后,一些参数能影响的各区域内存大小值,示意图如下: +结合我们经常使用的一些jvm调优参数后,一些参数能影响的各区域内存大小值,示意图如下: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/d4e6583cd0ecfa60622526e7a4f13634.png) -注:jdk8 开始,用 MetaSpace 区取代了 Perm 区(永久代),所以相应的 jvm 参数变成 -XX:MetaspaceSize 及 -XX:MaxMetaspaceSize。 +注:jdk8开始,用MetaSpace区取代了Perm区(永久代),所以相应的jvm参数变成-XX:MetaspaceSize及-XX:MaxMetaspaceSize。 -以 Hotspot 为例,我们来分析下 GC 的主要过程: +以Hotspot为例,我们来分析下GC的主要过程: -刚开始时,对象分配在 eden 区,s0(即:from)及 s1(即:to)区,几乎是空着。 +刚开始时,对象分配在eden区,s0(即:from)及s1(即:to)区,几乎是空着。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/8dc1423cc9f85658a946e204dc85ec18.png) -随着应用的运行,越来越多的对象被分配到 eden 区。 +随着应用的运行,越来越多的对象被分配到eden区。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/c9138916377192a8a2590aca3e888049.png) -当 eden 区放不下时,就会发生 minor GC(也被称为 young GC),第 1 步当然是要先标识出不可达垃圾对象(即:下图中的黄色块),然后将可达对象,移动到 s0 区(即:4 个淡蓝色的方块挪到 s0 区),然后将黄色的垃圾块清理掉,这一轮过后,eden 区就成空的了。 +当eden区放不下时,就会发生minor GC(也被称为young GC),第1步当然是要先标识出不可达垃圾对象(即:下图中的黄色块),然后将可达对象, +移动到s0区(即:4个淡蓝色的方块挪到s0区),然后将黄色的垃圾块清理掉,这一轮过后,eden区就成空的了。 注:这里其实已经综合运用了“【标记 - 清理 eden】 + 【标记 - 复制 eden->s0】”算法。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/adc376cad0670c6993da8f32f3a88aba.png) -随着时间推移,eden 如果又满了,再次触发 minor GC,同样还是先做标记,这时 eden 和 s0 区可能都有垃圾对象了(下图中的黄色块),注意:这时 s1(即:to)区是空的,s0 区和 eden 区的存活对象,将直接搬到 s1 区。然后将 eden 和 s0 区的垃圾清理掉,这一轮 minor GC 后,eden 和 s0 区就变成了空的了。 +随着时间推移,eden如果又满了,再次触发minor GC,同样还是先做标记,这时eden和s0区可能都有垃圾对象了(下图中的黄色块),注意:这时s1(即:to)区是空的, +s0区和eden区的存活对象,将直接搬到s1区。然后将eden和s0区的垃圾清理掉,这一轮minor GC后,eden和s0区就变成了空的了。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f21d18a0736d0976c4ecf14e82236cb2.png) -继续,随着对象的不断分配,eden 空可能又满了,这时会重复刚才的 minor GC 过程,不过要注意的是,这时候 s0 是空的,所以 s0 与 s1 的角色其实会互换,即:存活的对象,会从 eden 和 s1 区,向 s0 区移动。然后再把 eden 和 s1 区中的垃圾清除,这一轮完成后,eden 与 s1 区变成空的,如下图。 +继续,随着对象的不断分配,eden区可能又满了,这时会重复刚才的minor GC过程,不过要注意的是,这时候s0是空的,所以s0与s1的角色其实会互换, +即:存活的对象,会从eden和s1区,向s0区移动。然后再把eden和s1区中的垃圾清除,这一轮完成后,eden与s1区变成空的,如下图。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f765eb0f46635ae093516f35fb1eee66.png) -对于那些比较“长寿”的对象一直在 s0 与 s1 中挪来挪去,一来很占地方,而且也会造成一定开销,降低 gc 效率,于是有了“代龄 (age)”及“晋升”。 +对于那些比较“长寿”的对象一直在s0与s1中挪来挪去,一来很占地方,而且也会造成一定开销,降低gc效率,于是有了“代龄 (age)”及“晋升”。 -对象在年青代的 3 个区 (edge,s0,s1) 之间,每次从 1 个区移到另 1 区,年龄 +1,在 young 区达到一定的年龄阈值后,将晋升到老年代。下图中是 8,即:挪动 8 次后,如果还活着,下次 minor GC 时,将移动到 Tenured 区。 +对象在年青代的3个区 (edge,s0,s1) 之间,每次从1个区移到另1区,年龄+1,在young区达到一定的年龄阈值后,将晋升到老年代。下图中是8,即:挪动8次后, +如果还活着,下次minor GC时,将移动到Tenured区。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/2705a4ba41ed37bd535adab5b91ffb8f.png) -下图是晋升的主要过程:对象先分配在年青代,经过多次 Young GC 后,如果对象还活着,晋升到老年代。 +下图是晋升的主要过程:对象先分配在年青代,经过多次Young GC后,如果对象还活着,晋升到老年代。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/6d24d96eb137f805c867736a750c2bd9.png) -如果老年代,最终也放满了,就会发生 major GC(即 Full GC),由于老年代的的对象通常会比较多,因为标记 - 清理 - 整理(压缩)的耗时通常会比较长,会让应用出现卡顿的现象,这也是为什么很多应用要优化,尽量避免或减少 Full GC 的原因。 +如果老年代,最终也放满了,就会发生major GC(即 Full GC),由于老年代的的对象通常会比较多,因为标记 - 清理 - 整理(压缩)的耗时通常会比较长, +会让应用出现卡顿的现象,这也是为什么很多应用要优化,尽量避免或减少Full GC的原因。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/ebad989a70f78a40c561df9943234441.png) -注:上面的过程主要来自 oracle 官网的资料,但是有一个细节官网没有提到,如果分配的新对象比较大,eden 区放不下,但是 old 区可以放下时,会直接分配到 old 区(即没有晋升这一过程,直接到老年代了)。 +注:上面的过程主要来自oracle官网的资料,但是有一个细节官网没有提到,如果分配的新对象比较大,eden区放不下,但是old区可以放下时,会直接分配到 +old区(即没有晋升这一过程,直接到老年代了)。 下图引自阿里出品的《码出高效 -Java 开发手册》一书,梳理了 GC 的主要过程。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/e663bd3043c6b3465edc1e7313671d69.png) - ## 垃圾回收器 -不算最新出现的神器 ZGC,历史上出现过 7 种经典的垃圾回收器。 +不算最新出现的神器ZGC,历史上出现过7种经典的垃圾回收器。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/ab1c7be2fa4b5d180ffa1f43bf9dfce7.png) -这些回收器都是基于分代的,把 G1 除外,按回收的分代划分,横线以上的 3 种:Serial ,ParNew, Parellel Scavenge 都是回收年青代的,横线以下的 3 种:CMS,Serial Old, Parallel Old 都是回收老年代的。 +这些回收器都是基于分代的,把G1除外,按回收的分代划分,横线以上的3种:Serial ,ParNew, Parellel Scavenge都是回收年青代的, +横线以下的3种:CMS,Serial Old, Parallel Old都是回收老年代的。 @@ -255,66 +275,90 @@ ObjB.obj - ObjA ParNew收集器是Serial收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会stop the world,只是相比较Serial收集器而言它会运行多条进程进行垃圾回收。 -ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百的保证能超越Serial收集器。当然,随着可以使用的CPU的数量增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。 +ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不 +能百分之百的保证能超越Serial收集器。当然,随着可以使用的CPU的数量增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数 +量相同,在CPU非常多(譬如32个,现在CPU动辄4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数 +来限制垃圾收集的线程数。 ### Parallel Scavenge -Parallel是采用复制算法的多线程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的目标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 +Parallel是采用复制算法的多线程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的 +目标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。 +停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务, +主要适合在后台运算而不需要太多交互的任务。 ### CMS -全称:Concurrent Mark Sweep,从名字上看,就能猜出它是并发多线程的。这是 JDK 7 中广泛使用的收集器,有必要多说一下,借一张网友的图说话: +全称:Concurrent Mark Sweep,从名字上看,就能猜出它是并发多线程的。这是JDK 7中广泛使用的收集器,有必要多说一下,借一张网友的图说话: ![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/e8/4e/e8b152cf510b06544c2a13bfb4fc564e.png) -相对Serial Old 收集器或Parallel Old 收集器而言,这个明显要复杂多了,从名字(Mark Swep)就可以看出,CMS收集器是基于标记清除算法实现的。它的收集过程分为四个步骤: +相对Serial Old收集器或Parallel Old 收集器而言,这个明显要复杂多了,从名字(Mark Swep)就可以看出,CMS收集器是基于标记清除算法实现的。 +它的收集过程分为四个步骤: 1. 初始标记(initial mark) 2. 并发标记(concurrent mark) 3. 重新标记(remark) 4. 并发清除(concurrent sweep) -分为 4 个阶段: - -1)Inital Mark 初始标记:主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。 +分为4个阶段: -2)Concurrent Mark 并发标记:根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。 +- Inital Mark 初始标记 + 主要是标记GC Root开始的下级(注:仅下一级)对象,这个过程会STW,但是跟GC Root直接关联的下级对象不会很多,因此这个过程其实很快。 -3)Remark 再标志:为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。 +- Concurrent Mark 并发标记 + 根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有STW。 -试想下,高铁上的垃圾清理员,从车厢一头开始吆喝“有需要扔垃圾的乘客,请把垃圾扔一下”,一边工作一边向前走,等走到车厢另一头时,刚才走过的位置上,可能又有乘客产生了新的空瓶垃圾。所以,要完全把这个车厢清理干净的话,她应该喊一下:所有乘客不要再扔垃圾了(STW),然后把新产生的垃圾收走。当然,因为刚才已经把收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW 时间不会很长)。 +- Remark 再标志 + 为啥还要再标记一次?因为第2步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。 + 试想下,高铁上的垃圾清理员,从车厢一头开始吆喝“有需要扔垃圾的乘客,请把垃圾扔一下”,一边工作一边向前走,等走到车厢另一头时,刚才走过的位置上, + 可能又有乘客产生了新的空瓶垃圾。所以,要完全把这个车厢清理干净的话,她应该喊一下:所有乘客不要再扔垃圾了(STW),然后把新产生的垃圾收走。 + 当然,因为刚才已经把收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW 时间不会很长)。 -4)Concurrent Sweep:并行清理,这里使用多线程以“Mark Sweep- 标记清理”算法,把垃圾清掉,其它工作线程仍然能继续支行,不会造成卡顿。 +- Concurrent Sweep + 并行清理,这里使用多线程以“Mark Sweep- 标记清理”算法,把垃圾清掉,其它工作线程仍然能继续支行,不会造成卡顿。 -等等,刚才我们不是提到过“标记清理”法,会留下很多内存碎片吗?确实,但是也没办法,如果换成“Mark Compact 标记 - 整理”法,把垃圾清理后,剩下的对象也顺便排整理,会导致这些对象的内存地址发生变化,别忘了,此时其它线程还在工作,如果引用的对象地址变了,就天下大乱了。 +等等,刚才我们不是提到过“标记清理”法,会留下很多内存碎片吗?确实,但是也没办法,如果换成“Mark Compact标记 - 整理”法,把垃圾清理后, +剩下的对象也顺便排整理,会导致这些对象的内存地址发生变化,别忘了,此时其它线程还在工作,如果引用的对象地址变了,就天下大乱了。 -另外,由于这一步是并行处理,并不阻塞其它线程,所以还有一个副使用,在清理的过程中,仍然可能会有新垃圾对象产生,只能等到下一轮 GC,才会被清理掉。不过由于CMS收集器是基于标记清除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前开启一次Full GC。 +另外,由于这一步是并行处理,并不阻塞其它线程,所以还有一个副使用,在清理的过程中,仍然可能会有新垃圾对象产生,只能等到下一轮GC,才会被清理掉。 +不过由于CMS收集器是基于标记清除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会出现老年代还有很大的空间剩余,但是无法找 +到足够大的连续空间来分配当前对象,不得不提前开启一次Full GC。 -为了解决这个问题,CMS收集器默认提供了一个-XX:+UseCMSCompactAtFullCollection收集开关参数(默认就是开启的),用于在CMS收集器进行FullGC完开启内存碎片的合并整理过程,内存整理的过程是无法并发的,这样内存碎片问题倒是没有了,不过停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction参数用于设置执行多少次不压缩的FULL GC后跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。 +为了解决这个问题,CMS收集器默认提供了一个-XX:+UseCMSCompactAtFullCollection收集开关参数(默认就是开启的),用于在CMS收集器进行Full GC完 +开启内存碎片的合并整理过程,内存整理的过程是无法并发的,这样内存碎片问题倒是没有了,不过停顿时间不得不变长。虚拟机设计者还提供了另外一个参 +数-XX:CMSFullGCsBeforeCompaction参数用于设置执行多少次不压缩的FULL GC后跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。 -不幸的是,它作为老年代的收集器,却无法与jdk1.4中已经存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms来收集老年代的时候,新生代只能选择ParNew或Serial收集器中的一个。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项启用CMS收集器之后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。 +不幸的是,它作为老年代的收集器,却无法与jdk1.4中已经存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms来收集老年代的时候, +新生代只能选择ParNew或Serial收集器中的一个。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项启用CMS收集器之后的默认新生代收集器,也可以使用 +-XX:+UseParNewGC选项来强制指定它。 -虽然仍不完美,但是从这 4 步的处理过程来看,以往收集器中最让人诟病的长时间 STW,通过上述设计,被分解成二次短暂的 STW,所以从总体效果上看,应用在 GC 期间卡顿的情况会大大改善,这也是 CMS 一度十分流行的重要原因。 +虽然仍不完美,但是从这4步的处理过程来看,以往收集器中最让人诟病的长时间STW,通过上述设计,被分解成二次短暂的STW,所以从总体效果上看,应用在GC期间 +卡顿的情况会大大改善,这也是CMS一度十分流行的重要原因。 ### G1 -G1 的全称是 Garbage-First,为什么叫这个名字,呆会儿会详细说明。鉴于 CMS 的一些不足之外,比如: 老年代内存碎片化,STW 时间虽然已经改善了很多,但是仍然有提升空间。G1 就横空出世了,它对于 heap 区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。G1收集器是一款面向服务端应用的垃圾收集器。HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点: +G1的全称是Garbage-First,为什么叫这个名字,呆会儿会详细说明。鉴于CMS的一些不足之外,比如:老年代内存碎片化,STW时间虽然已经改善了很多, +但是仍然有提升空间。G1就横空出世了,它对于heap区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。G1收集器是一款面向服务端应用的垃圾收集器。 +HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点: 1. 并行与并发:G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。 2. 分代收集:和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾回收器的配合就可以独自管理整个GC堆。 3. 空间整合:G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。 -4. 可预测的非停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。 +4. 可预测的非停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内, + 消耗在垃圾收集上的时间不得超过N毫秒。 -在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。 +在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生 +代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。 虽然G1看起来有很多优点,实际上CMS还是主流。 +如下图,G1将heap内存区,划分为一个个大小相等(1-32M,2的n次方)、内存连续的Region区域,每个region都对应Eden、Survivor、Old、Humongous +四种角色之一,但是region与region之间不要求连续。 -如下图,G1 将 heap 内存区,划分为一个个大小相等(1-32M,2 的 n 次方)、内存连续的 Region 区域,每个 region 都对应 Eden、Survivor 、Old、Humongous 四种角色之一,但是 region 与 region 之间不要求连续。 +注:Humongous,简称H区是专用于存放超大对象的区域,通常>= 1/2 Region Size,且只有Full GC阶段,才会回收H区,避免了频繁扫描、复制 / 移动大对象。 -注:Humongous,简称 H 区是专用于存放超大对象的区域,通常 >= 1/2 Region Size,且只有 Full GC 阶段,才会回收 H 区,避免了频繁扫描、复制 / 移动大对象。 - -所有的垃圾回收,都是基于 1 个个 region 的。JVM 内部知道,哪些 region 的对象最少(即:该区域最空),总是会优先收集这些 region(因为对象少,内存相对较空,肯定快),这也是 Garbage-First 得名的由来,G 即是 Garbage 的缩写, 1 即 First。 +所有的垃圾回收,都是基于1个个region的。JVM内部知道,哪些region的对象最少(即:该区域最空),总是会优先收集这些 region(因为对象少,内存相对较空,肯定快),这也是 Garbage-First 得名的由来,G 即是 Garbage 的缩写, 1 即 First。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f1a1bdecf4e56a4440707ce073d73f61.png) @@ -422,25 +466,24 @@ NUMA 是一种多核服务器的架构,简单来讲,一个多核服务器( - - - - # 与GC相关的常用参数 -​ 除了上面提及的一些参数,下面补充一些和GC相关的常用参数: +除了上面提及的一些参数,下面补充一些和GC相关的常用参数: -- ​ -Xmx: 设置堆内存的最大值。 -- ​ -Xms: 设置堆内存的初始值。 -- ​ -Xmn: 设置新生代的大小。 -- ​ -Xss: 设置栈的大小。 -- ​ -PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。 -- ​ -MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个参数值时就进入老年代。 -- ​ -UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。 -- ​ -SurvivorRattio: 新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Suvivor= 8: 1。 -- ​ -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。 -- ​ -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。 -- ​ -XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。 +- -Xmx: 设置堆内存的最大值。 +- -Xms: 设置堆内存的初始值。 +- -Xmn: 设置新生代的大小。 +- -Xss: 设置栈的大小。 +- -PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。 +- -MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个参数值时就进入老年代。 +- -UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden和survivor的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、 + 吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和 + 停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。 +- -SurvivorRattio: 新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Suvivor= 8: 1。 +- -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等。但在CPU数量比较多的情况下,设置相对较小的数值也是合理的。 +- -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于0的整数。收集器在工作时,会调整Java堆大小或者其他一些参数,尽可能地把 + 停顿时间控制在MaxGCPauseMills以内。 +- -XX:GCTimeRatio:设置吞吐量大小,它的值是一个0-100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。 @@ -453,4 +496,4 @@ NUMA 是一种多核服务器的架构,简单来讲,一个多核服务器( --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/JavaKnowledge/JVM\346\236\266\346\236\204.md" "b/JavaKnowledge/JVM\346\236\266\346\236\204.md" new file mode 100644 index 00000000..34304137 --- /dev/null +++ "b/JavaKnowledge/JVM\346\236\266\346\236\204.md" @@ -0,0 +1,264 @@ +# JVM架构 + +Java的主要优势在于,它被设计为可在具有WORA(write once, run anywhere)概念的各种平台上运行-“一次编写,可在任何地方运行”。 + +Java源代码会被JDK内置的Java编译器(javac)编译成称为字节码(即.class文件)的中间状态, + +这些字节码是带有操作码操作数的十六进制格式,并且JVM可以将这些指令(无需进一步重新编译)解释为操作系统和底层硬件平台可以理解的本地语言。 +因此,字节码充当独立于平台的中间状态,该状态可在任何JVM之间移植,而与底层操作系统和硬件体系结构无关。 + +但是,由于JVM是为运行基础硬件和OS结构并与之通信而开发的,因此我们需要为我们的OS版本(Windows,Linux,Mac)和处理器体系结构(x86,x64) +选择适当的JVM版本。 + +我们大多数人都知道Java的上述故事,这里的问题是该过程中最重要的组成部分-JVM被当作一个黑匣子教给我们,它可以神奇地解释字节码并执行许多运行时活动, +例如JIT(程序执行期间进行实时)编译和GC(垃圾收集)。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/JVM_Architecture.png) + +## Class Loader子系统 + +JVM驻留在内存(RAM)上。在执行过程中,会使用Class Loader子系统将class文件加载到内存中。这称为Java的动态类加载(dynamic class loading)功能。 +当它在运行时(而非编译时)首次引用类时,它将加载、链接和初始化类文件(.class)。 + +- 加载 + + Class Loader的主要任务就是把编译后的.class文件加载到内存中。通常,class加载过程会从加载main类(有声明static main()的类)。所有后续的 + 类加载尝试都是根据已运行的类中的类引用完成的,如以下情况所述: + + - 当字节码静态引用一个类时 + - 当字节码创建一个类对象时(例如: Person person = new Person("John")) + + 有3种类型的类加载器: + + - Bootstrap Class Loader + + 加载来自rt.jar的标准JDK类,例如引导路径中存在的核心Java API类-$ JAVA_HOME/jre/lib目录(例如java.lang。*包类)。它以C / C ++ + 之类的本地语言实现,并是Java中所有类加载器的父级。 + + - Extension Class Loader + + 将类加载请求委托给其父类Bootstrap,如果不成功,则从扩展路径中的扩展目录(例如,安全扩展功能)中加载-JAVA_HOME/jre/ext或java.ext.dirs + 系统指定的任何其他目录的类。该类加载器由Java中的sun.misc.Launcher$ExtClassLoader类实现。 + + - System/Application Class Loader + + 从系统类路径加载应用程序特定的类,可以使用-cp或-classpath命令来在程序执行时动态设置。它在内部使用映射到java.class.path的环境变量。 + 该类加载器由sun.misc.Launcher$AppClassLoader类用Java实现。 + + 除了上面讨论的3个主要的类加载器,程序员还可以直接在代码本身上创建用户定义的类加载器。这通过类加载器委托模型保证了应用程序的独立性。这种方法 + 用于Tomcat之类的Web应用程序服务器中,以使Web应用程序和企业解决方案独立运行。 + + 每个类加载器都有自己的名称空间来保存已加载的类。当类加载器加载类时,它将基于存储在名称空间中的完全合格的类名称(FQCN:Fully Qualified Class Name) + 搜索该类,以检查该类是否已被加载。即使该类具有相同的FQCN但具有不同的名称空间,也将其视为不同的类。不同的名称空间意味着该类已由另一个类加载器加载。 + + 它们遵循4个主要原则: + + - 可见性原则 + + 该原则指出子类加载器可以看到父类加载器加载的类,但是父类加载器找不到子类加载器加载的类。 + + - 唯一性原则 + + 该原则指出,父类加载的类不应再由子类加载器加载,并确保不会发生重复的类加载。 + + - 委托层次结构原则 + + 为了满足上述2个原则,JVM遵循一个委托层次结构来为每个请求装入的类选择类加载器。首先从最低的子级别开始,Application Class Loader将 + 接收到的类加载请求委托给Extension Class Loader,然后Extension Class Loader将该请求委托给Bootstrap Class Loader。如果在 + Bootstrap路径中找到了所请求的类,则将加载该类。否则,该请求将再次被转移回Extension Class Loader加载器中从该Loader的扩展路径或 + 自定义指定的路径中查找类。如果它也失败,则请求将返回到Application Class Loader中从该Loader的System类路径中查找该类,并且如果 + Application Class Loader也未能加载所请求的类,则将获得运行时异常— java.lang.ClassNotFoundException。 + + - 无法卸载原则 + + 即使类加载器可以加载类,但是无法卸载已加载的类。替代卸载功能的是可以删除当前的类加载器,并创建一个新的类加载器。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_class_loaders.png) + +- 链接 + + 在遵循以下属性的同时,链接涉及验证和准备已加载的类或接口,其直接父类和实现的接口以及其元素类型。 + + - 在链接一个类或接口之前,必须将其完全加载。 + - 在初始化类或接口之前,必须对其进行完全验证和准备(在下一步中)。 + - 如果在链接过程中发生错误,则会将其抛出到程序中的某个位置,在该位置,程序将采取某些操作,这些操作可能直接或间接地需要链接到错误所涉及的类或接口。 + + 链接分为以下三个阶段: + + - Verification + + 确保.class文件的正确性(代码是否根据Java语言规范正确编写?它是由有效的编译器根据JVM规范生成的吗?)。这是类加载过程中最复杂的测试过程, + 并且耗时最长。即使链接减慢了类加载过程的速度,它也避免了在执行字节码时多次执行这些检查的需要,从而使整体执行高效而有效。如果验证失败, + 则会引发运行时错误(java.lang.VerifyError)。例如,执行以下检查: + + ```java + - consistent and correctly formatted symbol table + - final methods / classes not overridden + - methods respect access control keywords + - methods have correct number and type of parameters + - bytecode doesn’t manipulate stack incorrectly + - variables are initialized before being read + - variables are a value of the correct type + ``` + - Preparation + + 为静态存储和JVM使用的任何数据结构(例如方法表)分配内存。静态字段已创建并初始化为其默认值,但是,在此阶段不执行任何初始化程序或代码, + 因为这是初始化的一部分。 + + - Resolution + + 用直接引用替换类型中的符号引用。通过搜索方法区域以找到引用的实体来完成此操作。 + +- 初始化 + + 在这里,将执行每个加载的类或接口的初始化逻辑(例如,调用类的构造函数)。由于JVM是多线程的,因此应在适当同步的情况下非常仔细地进行类或接口的 + 初始化,以避免其他线程尝试同时初始化同一类或接口(即使其成为线程安全的)。 + + 这是类加载的最后阶段,所有静态变量都分配有代码中定义的原始值,并且将执行静态块(如果有)。这是在类中从上到下,从类层次结构中的父级到子级逐行执行的。 + +### 对象的创建 + Java对象的创建过程: +1. 类加载检查 + 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析 + 和初始化过。如果没有,那必须先执行相应的类加载过程。 +2. 分配内存 + 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内 + 存从Java堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 +3. 初始化零值 + 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 +4. 设置对象头 + 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 + 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 +5. 执行init方法 + 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行, + 所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 + + +### 对象的内存布局 +在Hotspot虚拟机中,对象在内存中的布局可以分为3快区域:对象头、实例数据和对齐填充。 +Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希吗、GC分代年龄、锁状态标志等等),另一部分是类型指针,即对象 +指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 +实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。 +对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍, +换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 + + +### 对象的访问定位 +建立对象就是为了使用对象,我们的Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种: +1. 句柄 + 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_refe_jubing.png) + +2. 直接指针 + 如果使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_refe_direct.png) + +这两种对象访问方式各有优势。使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针, +而reference本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。 + +## Runtime Data Area + +运行时数据区是当JVM程序在OS上运行时分配的存储区。除了读取.class文件之外,Class Loader子系统还会生成相应的二进制数据,并将以下信息分别保存在每个类的Method区域中: + +- 加载的类及其直接父类的全限定名称 +- .class文件是否与Class / Interface / Enum相关 +- 修饰符,静态变量和方法信息等。 + +然后,对于每个已加载的.class文件,它都会按照java.lang包中的定义,恰好创建一个Class对象来表示堆内存中的文件。稍后,在我们的代码中,可以使用此 +Class对象读取类级别的信息(类名称,父名称,方法,变量信息,静态变量等)。 + +[具体可参考Java内存模型](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.md) + + + +## Execution Engine + +字节码的实际执行在这里进行。执行引擎通过读取分配给上述运行时数据区域的数据逐行执行字节码中的指令。 + +### Interpreter + +解释器解释字节码并一对一执行指令。因此,它可以快速解释一个字节码行,但是执行解释后的结果是一项较慢的任务。缺点是,当多次调用一个方法时,每次都需要新的解释和较慢的执行速度。 + +### Just-In-Time (JIT) Compiler + +如果只有解释器可用,则当多次调用一个方法时,每次也会进行解释,如果有效处理,这将是多余的操作。使用JIT编译器已经可以做到这一点。首先,它将整个字 +节码编译为本地代码(机器代码)。然后,对于重复的方法调用,它直接提供了本机代码,使用本机代码的执行比单步解释指令要快得多。本机代码存储在缓存中, +因此可以更快地执行编译后的代码。 + +但是,即使对于JIT编译器,编译所花费的时间也要比解释器所花费的时间更多。对于仅执行一次的代码段,最好对其进行解释而不是进行编译。同样,本机代码也 +存储在高速缓存中,这是一种昂贵的资源。在这种情况下,JIT编译器会在内部检查每个方法调用的频率,并仅在所选方法发生超过特定时间级别时才决定编译每个 +方法。自适应编译的想法已在Oracle Hotspot VM中使用。 + +当JVM供应商引入性能优化时,执行引擎有资格成为关键子系统。在这些工作中,以下4个组件可以大大提高其性能: + +- 中间代码生成器生成中间代码。 +- 代码优化器负责优化上面生成的中间代码。 +- 目标代码生成器负责生成本机代码(即机器代码)。 +- Profiler是一个特殊的组件,负责查找性能热点(例如,多次调用一种方法的实例) + +### 供应商的优化方法 + +- Oracle Hotspot VMs + + Oracle通过流行的JIT编译器模型Hotspot Compiler实现了其标准Java VM的2种实现。通过分析,它可以确定最需要JIT编译的热点,然后将代码的那些 + 性能关键部分编译为本机代码。随着时间的流逝,如果不再频繁调用这种已编译的方法,它将把该方法标识为不再是热点,并迅速从缓存中删除本机代码,并开 + 始在解释器模式下运行。这种方法可以提高性能,同时避免不必要地编译很少使用的代码。此外,Hotspot Compiler可以即时确定使用lining等技术来优化 + 已编译代码的最佳方式。编译器执行的运行时分析使它可以消除在确定哪些优化将产生最大性能收益方面的猜测。 + 这些虚拟机使用相同的运行时(解释器,内存,线程),但是将自定义构建JIT编译器的实现,如下所述。 + + Oracle Java Hotspot Client VM是Oracle JDK和JRE的默认VM技术。它通过减少应用程序启动时间和内存占用量而在客户端环境中运行应用程序时进行了优化,以实现最佳性能。 + Oracle Java Hotspot Server VM旨在为在服务器环境中运行的应用程序提供最高的程序执行速度。此处使用的JIT编译器称为“高级动态优化编译器”, + 它使用更复杂和多样化的性能优化技术。通过使用服务器命令行选项(例如Java服务器MyApp)来调用Java HotSpot Server VM。 + + Oracle的Java Hotspot技术以其快速的内存分配,快速高效的GC以及易于在大型共享内存多处理器服务器中扩展的线程处理能力而闻名。 + +- IBM AOT (Ahead-Of-Time) Compiling + + 这里的特长是这些JVM共享通过共享缓存编译的本机代码,因此已经通过AOT编译器编译的代码可以由另一个JVM使用,而无需编译。另外,IBM JVM通过使用 + AOT编译器将代码预编译为JXE(Java可执行文件)文件格式,提供了一种快速的执行方式。 + + + +### Garbage Collector (GC) + +只要引用了一个对象,JVM就会认为它是活动的。一旦不再引用对象,因此应用程序代码无法访问该对象,则垃圾收集器将其删除并回收未使用的内存。通常,垃圾 +回收是在后台进行的,但是我们可以通过调用System.gc()方法来触发垃圾回收(同样,无法保证执行。因此,请调用Thread.sleep(1000)并等待GC完成)。 +[具体内存回收部分请看JVM垃圾回收机制](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.md) + + + +### JVM线程 + +我们讨论了如何执行Java程序,但没有具体提及执行程序。实际上,为了执行我们前面讨论的每个任务,JVM并发运行多个线程。这些线程中的一些带有编程逻辑, +并且是由程序(应用程序线程)创建的,而其余的则是由JVM本身创建的,以承担系统中的后台任务(系统线程)。 + +主应用程序线程是作为调用公共静态void main(String [])的一部分而创建的主线程,而所有其他应用程序线程都是由该主线程创建的。 +应用程序线程执行诸如执行以main()方法开头的指令,在Heap区域中创建对象(如果它在任何方法逻辑中找到新关键字)之类的任务等。 + +主要的系统线程有: + +- Compiler threads + + 在运行时,这些线程将字节码编译为本地代码。 + +- GC threads + + 所有与GC相关的活动均由这些线程执行。 + +- Periodic task thread + + 计划周期性操作执行的计时器事件(即中断)由该线程执行。 + +- Signal dispatcher thread + + 该线程接收发送到JVM进程的信号,并通过调用适当的JVM方法在JVM内部对其进行处理。 + +- VM thread + + 前提条件是,某些操作需要JVM到达安全点才能执行,在该点不再进行对Heap区域的修改。这种情况的示例是“世界停止”垃圾回收,线程堆栈转储,线程挂起 + 和有偏向的锁吊销。这些操作可以在称为VM线程的特殊线程上执行。 + + + diff --git "a/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" new file mode 100644 index 00000000..8e75224f --- /dev/null +++ "b/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -0,0 +1,239 @@ +Java内存模型 +=== + +Java虚拟机(Java Virtual Machine)在执行Java程序的过程中,会把它管理的内存划分为五个不同的数据区域(Heap Memory、 +Stack Memory、Method Area、PC、Native Stack Memory),这些区域都有各自的用途、创建时间、销毁时间: +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_memory_model.jpeg) + +## Program Counter Register + +程序计数器是一块较小的内存空间,严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹 +不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照 +被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储, +我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。可以看作是当前线程所执行的字节码的行号指示器。 + +从上面的介绍中我们知道程序计数器主要有两个作用: + +- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 +- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 + +注意:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。 + + + +## Heap Memory + +所有的对象实例和数组都存放到Heap内存中。Heap内存也称为共享内存。多线程可以共享这里面的数据。 + +- 堆内存在JVM启动的时候被加载(初始大小: -Xms) +- 堆内存在程序运行时会增加或减少 +- 最小值: -Xmx +- 从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden空间、From Survivor空间(s0)、To Survivor空间(s1)。 所有新生成的对象首先都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_heap_memory.png) + +在并发编程中,多个线程之间采取什么机制进行通信(信息交换),什么机制进行数据的同步?在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的。 + +## Stack Memory + +栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的线程栈,线程stack中包含有关线程调用了哪些方法以达到当前执行点的信息,也可以称之为调用栈,只要线程执行代码,调用栈就会发生改变。在这个栈中又会包含多个栈帧(Stack Frame),栈是由一个个栈帧组成,这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量表、操作栈和方法返回值等信息。 + +局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 + +每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。 + +线程stack中同样会包含该线程调用栈中所有方法执行所需要的本地变量,一个线程只能获取到它自己对应的线程stack。一个线程创建的本地变量对于其他任何线程都是不可见的。即使两个线程执行完全相同的代码,这两个线程仍然会在各自自己对应的线程stack中创建自己的本地变量。 +所有基础类型的局部变量(boolean,byte,short,char,int,long,float,double)都被保存在自己的线程stack中。一个线程可以将一个基础变量的副本传递给另一个线程,但是它不能共享原始局部变量本身。 + +堆内存包含在Java应用程序中创建的所有对象,而不管创建该对象的线程是什么。这包括基本类型的对象版本(例如Byte,Integer,Long等)。创建对象并将其分配给局部变量,或者将其创建为另一个对象的成员变量都没有关系,该对象仍存储在堆中。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java-memory-model-1.png) + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/stack_heap.png) + +- 局部变量可以是原始类型,在这种情况下,它完全保留在线程堆栈中。 +- 局部变量也可以是对对象的引用。在这种情况下,引用(局部变量)存储在线程堆栈中,但是对象本身(如果有)存储在堆中。 +- 一个对象可能包含方法,而这些方法可能包含局部变量。即使该方法所属的对象存储在堆中,这些局部变量也存储在线程堆栈中。 +- 对象的成员变量与对象本身一起存储在堆中。不管成员变量是原始类型时,以及它是对对象的引用时,都是如此。 +- 静态类变量也与类定义一起存储在堆中。 +- 引用对象的所有线程都可以访问堆上的对象。当线程可以访问对象时,它也可以访问该对象的成员变量。如果两个线程同时在同一个对象上调用一个方法,则它们都将有权访问该对象的成员变量,但是每个线程将拥有自己的局部变量副本。 + +Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError: + +- StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。 +- OutOfMemoryError: 若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。 + +## Method Area + +方法区也称”永久代“,它用于存储虚拟机加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的属性(Field)信息(属性信息包括属性名、属性类型、属性修饰符(public, private, protected,static,final volatile,transient的某个子集))、类中的方法信息(方法的相关信息包括:方法名, 方法的返回类型(或 void), 方法参数的数量和类型(有序的),方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)),以及即时编译器编译后的代码(每个方法:字节码,操作数堆栈大小,局部变量大小,局部变量表,异常表;异常表中的每个异常处理程序:起点,终点,处理程序代码的PC偏移,捕获到的异常类的常量池索引),当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。 + +方法区默认最小值为16MB,最大值为64MB(64位JVM由于指针膨胀,默认是85M),可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。它是一片连续的堆空间,永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集 + +方法区是被Java线程锁共享的,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。方法区也是堆中的一部分,就是我们通常所说的Java堆中的永久区 Permanet Generation,大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。 + +方法区与堆有很多共性:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。 + +正因为如此相像,Java虚拟机规范把方法区描述为堆的一个逻辑部分,但目前实际上是与Java堆分开的(Non-Heap)。Non-Heap主要包括的内容如下: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_no_heap.webp) + +方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是回收确实是有必要的。 + +### Constant Pool + +常量池也称为运行时常量池(Runtime Constant Pool),用于存放编译期生成的各种字面量和符号引用。 + +常量池本身是方法区中的一个数据结构。既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。 + +常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。一般分为两类:字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。 + +JVM会加载、链接、初始化class文件。一个class文件会把其所有所有符号引用都保留在一个位置,即常量池中。 + +每个class文件都会有一个对应constant pool。但是class文件中的常量池显然是不够的,因为需要再JVM上执行。这种情况下,需要runtime constant pool来服务JVM的运行。 + +Java虚拟机加载的每个类或接口都有其常量池的内部版本,称为运行时常量池(runtime constant pool)。运行时常量池是特定于实现的数据结构,它与类文件中的常量池是一一对应映射的。因此,在最初加载类型之后,该类型的所有符号引用都驻留在该类型的运行时常量池中。包括字符串常量,类和接口名称,字段名称以及类中引用的其他常量。 + +#### String Constant Pool + +在Constant Pool中还有一个单独存放字符串的String Constant Pool。 + +```java +String str1 = "abcd"; +String str2 = new String("abcd"); +System.out.println(str1==str2);//false +``` +这两种不同的创建方法是有差别的,第一种方式是在常量池中拿对象,第二种方式是直接在堆内存空间创建一个新的对象。 +```java +String str1 = "str"; +String str2 = "ing"; + +String str3 = "str" + "ing";//常量池中的对象 +String str4 = str1 + str2; //在堆上创建的新的对象 +String str5 = "string";//常量池中的对象 +System.out.println(str3 == str4);//false +System.out.println(str3 == str5);//true +System.out.println(str4 == str5);//false +``` +尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用StringBuilder或者StringBuffer。 + +Java 基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean;这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 +两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。 +```java +Integer i1 = 33; +Integer i2 = 33; +System.out.println(i1 == i2);// 输出true +Integer i11 = 333; +Integer i22 = 333; +System.out.println(i11 == i22);// 输出false +Double i3 = 1.2; +Double i4 = 1.2; +System.out.println(i3 == i4);// 输出false +``` + +### Method Area & Constant Pool改动 + +很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。 + +在JDK1.7中,将字符串常量池和静态变量从方法区域中移到堆中,其余的运行时常量池仍在方法区域中,即hotspot中的永久生成。 所有的被intern的String被存储在PermGen区.PermGen区使用-XX:MaxPermSize=N来设置最大大小,但是由于应用程序string.intern通常是不可预测和不可控的,因此不好设置这个大小。设置不好的话,常常会引起java.lang.OutOfMemoryError: PermGen space。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java7_method_constant_pool.jpg) + +在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。string constant pool仍然还是在Heap内存中。runtime constant pool也仍然在方法区。但是方法区的实现已经改成使用Metaspace。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java8_memory_method.jpg) + +为什么要移除永久代,改为元空间呢? + +> Metaspace与PermGen之间最大的区别在于:Metaspace并不在虚拟机中,而是使用本机内存。如果没有使用-XX:MaxMetaspaceSize来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。 +> 但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进而可能出现swap内存被耗尽;最终导致进程直接被系统直接kill掉。 +> +> 如果类元数据的空间占用达到MaxMetaspaceSize设置的值,将会触发对象和类加载器的垃圾回收。java.lang.OutOfMemoryError: Metaspace space + +从用户角度来看,主要的区别是,默认情况下,Metaspace自动增加其大小(达到基础操作系统所提供的大小),而PermGen始终具有固定的最大大小。您可以使用JVM参数为Metaspace设置固定的最大值,但不能使PermGen自动增加。 + +在很大程度上,这只是名称的更改。早在引入PermGen时,就没有Java EE或动态类的加载(取消加载),因此,一旦加载了一个类,该类便一直停留在内存中,直到JVM关闭为止,从而实现了永久生成。如今,可以在JVM的生命周期内加载和卸载类,因此对于保留元数据的区域,Metaspace更有意义。 + +## Native Method Stack + +本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。如Java使用c或c++编写的接口服务时,代码在此区运行,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/RDA.png) +先看一张图,这张图能很清晰的说明JVM内存结构的布局和相应的控制参数: +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_memory_archi_param.jpeg) + +## 主内存和工作内存: + +Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程里面的变量有所不同步,它包含了实例字段、静态字段和构成数组对象的元素,但不包含局部变量和方法参数,因为后者是线程私有的,不会共享,当然不存在数据竞争问题(如果局部变量是一个reference引用类型,它引用的对象在Java堆中可被各个线程共享,但是reference引用本身在Java栈的局部变量表中,是线程私有的)。为了获得较高的执行效能,Java内存模型并没有限制执行引起使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。 + +JMM(Java Memory Model)规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。 + +由上述对JVM内存结构的描述中,我们知道了堆和方法区是线程共享的。而局部变量,方法定义参数和异常处理器参数就不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。 + +Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 + +假设线程A与线程B之间如要通信的话,必须要经历下面2个步骤: + +1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去。 +2. 线程B到主内存中去读取线程A之前已更新过的共享变量。 + +## 重排 + +在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。 + +这里说的重排序可以发生在好几个地方:编译器、运行时、JIT等,比如编译器会觉得把一个变量的写操作放在最后会更有效率,编译后,这个指令就在最后了(前提是只要不改变程序的语义,编译器、执行器就可以这样自由的随意优化),一旦编译器对某个变量的写操作进行优化(放到最后),那么在执行之前,另一个线程将不会看到这个执行结果。 + +当然了,写入动作可能被移到后面,那也有可能被挪到了前面,这样的“优化”有什么影响呢?这种情况下,其它线程可能会在程序实现“发生”之前,看到这个写入动作(这里怎么理解,指令已经执行了,但是在代码层面还没执行到)。通过内存屏障的功能,我们可以禁止一些不必要、或者会带来负面影响的重排序优化,在内存模型的范围内,实现更高的性能,同时保证程序的正确性。 + +下面我们来看一个重排序的例子: +```java +Class Reordering { + int x = 0, y = 0; + public void writer() { + x = 1; + y = 2; + } + public void reader() { + int r1 = y; + int r2 = x; + } +} +``` +假设这段代码有2个线程并发执行,线程A执行writer方法,线程B执行reader方法,线程B看到y的值为2,因为把y设置成2发生在变量x的写入之后(代码层面),所以能断定线程B这时看到的x就是1吗? + +当然不行!因为在writer方法中,可能发生了重排序,y的写入动作可能发在x写入之前,这种情况下,线程B就有可能看到x的值还是0。 + +在Java内存模型中,描述了在多线程代码中,哪些行为是正确的、合法的,以及多线程之间如何进行通信,代码中变量的读写行为如何反应到内存、CPU缓存的底层细节。 + + +## 问题: 匿名内部类访问局部变量时,为什么这个局部变量必须用final修饰? +这个问题并不是很严谨,严格来说应该是Java 1.8之前,匿名内部类访问局部变量时,才需要用final修饰。 +我们平时经常会用匿名内部类访问局部变量的情况,编译器都会提示我们要对这个局部变量加final修饰,但是我们却并没有去仔细考虑过这是为什么? + +上面说到类和成员变量保存到堆内存中。而局部变量则保存在栈内存中。 +假设在main()方法中有一个局部变量a,然后main()方法里面又去创建了一个匿名内部类使用该局部变量a。 +a是在栈内存中的,当main()方法执行结束,a就被清理了。但是你创建的内部类中的方法却可能在main()方法执行完成后再去执行,但是这时候局部变量已经不存在了,那怎么解决这个问题呢? +因此实际上是在访问它的副本,而不是访问原始的局部变量。 + +在Java的参数传递中,当基本类型作为参数传递时,传递的是值的拷贝,无论你怎么改变这个拷贝,原值是不会改变的;当对象作为参数传递时,传递的是对象的引用的拷贝,无论你怎么改变这个新的引用的指向,原来的引用是不会改变的(当然如果你通过这个引用改变了对象的内容,那么改变实实在在发生了)。而当final修饰基本类型变量时,不可更改其值,当final修饰引用变量时,不可更改其指向,只能更改其对象的内容。 + +在Java中内部类会持有外部类的引用和方法中参数的引用,当反编译class文件后,内部类的class文件的构造函数参数中会传入外部类的对象以及方法内局部变量,不管是基本数据类型还是引用变量,如果重新赋值了,会导致内外指向的对象不一致,所以java就暴力的规定使用final,不能重新赋值。 +所以用final修饰实际上就是为了变量值(数据)的一致性。 这里所说的数据一致性,对引用变量来说是引用地址的一致性,对基本类型来说就是值的一致性。 + + +当然在JDK 1.8及以后,看起来似乎编译器取消了这种限制,没有被声明为final的变量或参数也可以在匿名内部类内部被访问了。但实际上是因为Java 8引入了effectively final的概念(A variable or parameter whose value is never changed after it is initialized is effectively final)。对于effectively final(事实上的final),可以省略final关键字,本质不变,所以,实际上是诸如effectively final的变量或参数被Java默认为final类型,所以才不会报错,而上述的根本原因没有任何变化。 + + + It's about the scope of variables , Because anonymous inner classes appear inside a method , If it wants to access the parameters of the method or the variables defined in the method , Then these parameters and variables must be modified to final. Because although anonymous inner classes are inside methods , But when it's actually compiled , Inner classes are compiled into Outer.Inner, This means that the inner class is at the same level as the method in the outer class , A variable or parameter in a method in an external class is just a local variable of the method , The scope of these variables or parameters is only valid inside this method . Because internal classes and methods are at the same level when compiling , So the variables or parameters in the method are only final, Internal classes can be referenced . + +--- + +参考 +--- +- [Java (JVM) Memory Types](https://javapapers.com/core-java/java-jvm-memory-types/) +- [Java 8: From PermGen to Metaspace](https://dzone.com/articles/java-8-permgen-metaspace) +- [Interview question Series Part 5: JDK runtime constant pool, string constant pool, static constant pool, are you stupid and confused?](https://javamana.com/2020/11/20201113132526144q.html) +- [Where Has the Java PermGen Gone?](https://www.infoq.com/articles/Java-PERMGEN-Removed/) + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" "b/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" index 1f3a021f..f90b23af 100644 --- "a/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" +++ "b/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" @@ -1,13 +1,10 @@ # Java并发编程之原子性、可见性以及有序性 - - - 缓存导致的可见性问题 - 线程切换带来的原子性问题 - 编译优化带来的有序性问题 - ## 原子性(Atomicity) 众所周知,原子是构成物质的基本单位,所以原子代表着不可分。 @@ -23,14 +20,19 @@ - 计算`a+b` - 将计算结果写入内存 如果有两个线程`t1`,`t2`在进行这样的操作。`t1`在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是`t2`开始执行,`t2`执行完毕后`t1`又把没有完成的第三步做完。这个时候就出现了错误, - 相当于`t2`的计算结果被无视掉了。所以上面的买碘片例子在同步`add`方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。 + 相当于`t2`的计算结果被无视掉了。所以上面的片例子在同步`add`方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。 类似的,像`a++`这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。 ## 可见性(Visibility) 可见性指的是当一个线程修改了共享变量后,其他线程能够立即得知这个修改。 -在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时,多个处理器会将变量从主内存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。(这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数);同样在单核处理器中这样由于备份造成的问题同样存在!这样的优化带来的问题之一是变量可见性——如果线程`t1`与线程`t2`分别被安排在了不同的处理器上面,那么`t1`与`t2`对于变量`A`的修改时相互不可见,如果`t1`给`A`赋值,然后`t2`又赋新值,那么`t2`的操作就将`t1`的操作覆盖掉了,这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。 +在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化, +当线程要处理该变量时,多个处理器会将变量从主内存复制一份分别存储在自己的存储器中,等到进行完操作后,再赋值回主存。 +(这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数); +同样在单核处理器中这样由于备份造成的问题同样存在!这样的优化带来的问题之一是变量可见性——如果线程`t1`与线程`t2`分别被安排在了不同 +的处理器上面,那么`t1`与`t2`对于变量`A`的修改时相互不可见,如果`t1`给`A`赋值,然后`t2`又赋新值,那么`t2`的操作就将`t1`的操作 +覆盖掉了,这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。 volatile、synchronized、final都可以解决可见性问题。 @@ -38,7 +40,7 @@ volatile、synchronized、final都可以解决可见性问题。 有序性:即程序执行的顺序按照代码的先后顺序执行。 -有序性简单来说就是程序代码执行的顺序是否按照我们编写代码的顺序执行,一般来说,为了提高性能,编译器和处理器会对指令做重排序,重排序分3类 +有序性简单来说就是程序代码执行的顺序是否按照我们编写代码的顺序执行,一般来说,为了提高性能,编译器和处理器会对指令做重排序, 重排序分3类 - 编译器优化重排序,在不改变单线程程序语义的前提下,改变代码的执行顺序 - 指令集并行的重排序,对于不存在数据依赖的指令,处理器可以改变语句对应指令的执行顺序来充分利用CPU资源 @@ -77,52 +79,53 @@ public class Singleton{ } } } - return instance; + return instance; } } ``` 我们先看 instance=newSingleton() 的未被编译器优化的操作 -- 指令 1:分配一块内存 M; -- 指令 2:在内存 M 上初始化 Singleton 对象; -- 指令 3:然后 M 的地址赋值给 instance 变量。 +- 指令 1:分配一块内存M; +- 指令 2:在内存M上初始化Singleton对象; +- 指令 3:然后M的地址赋值给instance变量。 编译器优化后的操作指令 -- 指令 1:分配一块内存 M; -- 指令 2:将 M 的地址赋值给 instance 变量; -- 指令 3:然后在内存 M 上初始化 Singleton 对象。 +- 指令 1:分配一块内存M; +- 指令 2:将M的地址赋值给instance变量; +- 指令 3:然后在内存M上初始化Singleton对象。 -现在有A,B两个线程,我们假设线程A先执行getInstance()方法,当执行编译器优化后的操作指令2时(此时候未完成对象的初始化),这时候发生了线程切换,那么线程B进入,刚好执行到第一次判断instance==nul会发现 instance不等于null了,所以直接返回instance,而此时的 instance 是没有初始化过的。 +现在有A,B两个线程,我们假设线程A先执行getInstance()方法,当执行编译器优化后的操作指令2时(此时候未完成对象的初始化), +这时候发生了线程切换,那么线程B进入,刚好执行到第一次判断instance==nul会发现instance不等于null了,所以直接返回instance, +而此时的instance是没有初始化过的。 - - -Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。 +Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义, +而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块 +只能串行地进入。 ## **先行发生原则:** -如果Java内存模型中所有的有序性都只靠volatile和synchronized来完成,那么有一些操作将会变得很啰嗦,但是我们在编写Java并发代码的时候并没有感觉到这一点,这是因为Java语言中有一个“先行发生”(Happen-Before)的原则。这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依赖。 +如果Java内存模型中所有的有序性都只靠volatile和synchronized来完成,那么有一些操作将会变的很啰嗦,但是我们在编写Java并发代码的 +时候并没有感觉到这一点,这是因为Java语言中有一个“先行发生”(Happen-Before)的原则。这个原则非常重要,它是判断数据是否存在竞争, +线程是否安全的主要依赖。 -先行发生原则是指Java内存模型中定义的两项操作之间的依序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。下面是Java内存模型下一些”天然的“先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。 +先行发生原则是指Java内存模型中定义的两项操作之间的依序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能 +被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。下面是Java内存模型下一些”天然的“先行发生关系, +这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话, +它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。 - 程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。 - - 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。 - - volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。 - - 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。 - - 线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。 - - 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。 - - 对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。 - - 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。 -一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生“呢?也是不成立的,一个典型的例子就是指令重排序。所以时间上的先后顺序与先生发生原则之间基本没有什么关系,所以衡量并发安全问题一切必须以先行发生原则为准。 +一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生“呢?也是不成立的, +一个典型的例子就是指令重排序。所以时间上的先后顺序与先生发生原则之间基本没有什么关系,所以衡量并发安全问题一切必须以先行发生原则为准。 ```java int i = 0; @@ -130,9 +133,13 @@ boolean flag = false; i = 1; //语句1 flag = true; //语句2 ``` -上面代码定义了一个`int`型变量,定义了一个`boolean`类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么`JVM`在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? +上面代码定义了一个`int`型变量,定义了一个`boolean`类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的, +那么`JVM`在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 不一定,为什么呢?这里可能会发生指令重排序`(Instruction Reorder)`。 -下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢? +下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序 +同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上面的代码中,语句1和语句2谁先执行对最终的程序结果 +并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会 +和代码顺序执行结果相同,那么它靠什么保证的呢? 再看下面一个例子: ```java @@ -158,9 +165,10 @@ while(!inited ){ } doSomethingwithconfig(context); ``` -上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为初始化工作已经完成, -那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。 -从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。 +上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为 +初始化工作已经完成, 那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化, +就会导致程序出错。 从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执 +行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。 @@ -168,4 +176,4 @@ doSomethingwithconfig(context); - 邮箱 :charon.chui@gmail.com - Good Luck! - + diff --git "a/JavaKnowledge/MD5\345\212\240\345\257\206.md" "b/JavaKnowledge/MD5\345\212\240\345\257\206.md" index 0d2b6bce..62a8dd01 100644 --- "a/JavaKnowledge/MD5\345\212\240\345\257\206.md" +++ "b/JavaKnowledge/MD5\345\212\240\345\257\206.md" @@ -1,11 +1,11 @@ MD5加密 -=== +======= -`MD5`是一种不可逆的加密算法只能将原文加密,不能讲密文再还原去,原来把加密后将这个数组通过`Base64`给变成字符串, +`MD5`是一种不可逆的加密算法只能将原文加密,不能将密文再还原回去,原来把加密后将这个数组通过`Base64`给变成字符串, 这样是不严格的业界标准的做法是对其加密之后用每个字节`&15`然后就能得到一个`int`型的值,再将这个`int`型的值变成16进制的字符串.虽然MD5不可逆, 但是网上出现了将常用的数字用`md5`加密之后通过数据库查询,所以`MD5`简单的情况下仍然可以查出来,一般可以对其多加密几次或者`&15`之后再和别的数运算等, 这称之为*加盐*. - + ```java public class MD5Utils { /** @@ -35,6 +35,7 @@ public class MD5Utils { } ``` ----- -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" "b/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" index 428ac290..02a3ad83 100644 --- "a/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" +++ "b/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" @@ -15,10 +15,10 @@ MVC 分层同时也简化了分组开发。不同的开发人员可同时开发 - 优点 - 耦合性低 - - 重用性高 - - 可维护性高 - - 有利软件工程化管理 - + - 重用性高 + - 可维护性高 + - 有利软件工程化管理 + - 缺点 - 没有明确的定义 - 视图与控制器间的过于紧密的连接 @@ -34,7 +34,7 @@ MVP 所有的交互都发生在`Presenter`内部,而在`MVC`中`View`会直接从`Model`中读取数据而不是通过`Controller`。 在`MVC`里,`View`是可以直接访问`Model`的!从而,`View`里会包含`Model`信息,不可避免的还要包括一些业务逻辑。 在`MVC`模型里,更关注的`Model`的不变,而同时有多个对`Model`的不同显示及`View`。所以,在`MVC`模型里,`Model`不依赖于`View`,但是`View`是依赖于`Model`的。 -不仅如此,因为有一些业务逻辑在`View`里实现了,导致要更改`View`也是比较困难的,至少那些业务逻辑是无法重用的。 +不仅如此,因为有一些业务逻辑在`View`里实现了,导致要更改`View`也是比较困难的,至少那些业务逻辑是无法重用的。 ![image](https://github.com/CharonChui/Pictures/blob/master/is-activity-god-the-mvp-architecture-10-638.jpg?raw=true) @@ -56,8 +56,8 @@ MVP - 缺点 由于对视图的渲染放在了`Presenter`中,所以视图和`Presenter`的交互会过于频繁。还有一点需要明白,如果`Presenter`过多地渲染了视图, - 往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么`Presenter`也需要变更了。 - 比如说,原本用来呈现`Html`的`Presenter`现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。 + 往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么`Presenter`也需要变更了。 + 比如说,原本用来呈现`Html`的`Presenter`现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。 MVVM --- @@ -66,9 +66,11 @@ MVVM是Model-View-ViewModel的简写。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/MVVM.png) -MVVM模式将Presener改名为View Model,基本上与MVP模式完全一致,同样是以VM为核心,但是不同于MVP,MVVM采用了数据双向绑定的方案,替代了繁琐复杂的DOM操作。该模型中,View与VM保持同步,View绑定到VM的属性上,如果VM数据发生变化,通过数据绑定的方式,View会自动更新视图;VM同样也暴露出Model中的数据。 +MVVM模式将Presener改名为View Model,基本上与MVP模式完全一致,同样是以VM为核心,但是不同于MVP,MVVM采用了数据双向绑定的方案,替代了繁琐复杂的DOM操作。 +该模型中,View与VM保持同步,View绑定到VM的属性上,如果VM数据发生变化,通过数据绑定的方式,View会自动更新视图;VM同样也暴露出Model中的数据。 -看起来MVVM很好的解决了MVC和MVP的不足,但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源,有可能数据问题导致,也有可能业务逻辑中对视图属性的修改导致。如果项目中打算用MVVM的话可以考虑使用官方的架构组件ViewModel、LiveData、DataBinding去实现MVVM +看起来MVVM很好的解决了MVC和MVP的不足,但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源,有可能数据问题导致,也有可能业务逻辑中对视图 +属性的修改导致。如果项目中打算用MVVM的话可以考虑使用官方的架构组件ViewModel、LiveData、DataBinding去实现MVVM @@ -83,4 +85,4 @@ MVVM模式将Presener改名为View Model,基本上与MVP模式完全一致, - 邮箱 :charon.chui@gmail.com - Good Luck! - + diff --git "a/JavaKnowledge/Top-K\351\227\256\351\242\230.md" "b/JavaKnowledge/Top-K\351\227\256\351\242\230.md" index 4acb0c67..d716c354 100644 --- "a/JavaKnowledge/Top-K\351\227\256\351\242\230.md" +++ "b/JavaKnowledge/Top-K\351\227\256\351\242\230.md" @@ -21,19 +21,28 @@ Top-K问题 - 局部淘汰法 - 该方法与排序方法类似,用一个容器保存前`10000`个数,然后将剩余的所有数字逐一与容器内的最小数字相比,如果所有后续的元素都比容器内的`10000`个数还小,那么容器内这个`10000`个数就是最大`10000`个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这`1`亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为`O(n+m^2)`,其中`m`为容器的大小,即`10000`。 + 该方法与排序方法类似,用一个容器保存前`10000`个数,然后将剩余的所有数字逐一与容器内的最小数字相比,如果所有后续的元素都比容器内的`10000`个数还小, + 那么容器内这个`10000`个数就是最大`10000`个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这`1` + 亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为`O(n+m^2)`,其中`m`为容器的大小,即`10000`。 - 分治法 - 将`1`亿个数据分成`100`份,每份`100`万个数据,找到每份数据中最大的`10000`个,最后在剩下的`100*10000`个数据里面找出最大的`10000`个。如果`100`万数据选择足够理想,那么可以过滤掉`1`亿数据里面`99%`的数据。`100`万个数据里面查找最大的`10000`个数据的方法如下:用快速排序的方法,将数据分为`2`堆,如果大的那堆个数`N`大于`10000`个,继续对大堆快速排序一次分成`2`堆,如果大的那堆个数`N`大于`10000`个,继续对大堆快速排序一次分成`2`堆,如果大堆个数`N`小于`10000`个,就在小的那堆里面快速排序一次,找第`10000-n`大的数字;递归以上过程,就可以找到第`10000`大的数。一共需要101次这样的比较。 + 将`1`亿个数据分成`100`份,每份`100`万个数据,找到每份数据中最大的`10000`个,最后在剩下的`100*10000`个数据里面找出最大的`10000`个。 + 如果`100`万数据选择足够理想,那么可以过滤掉`1`亿数据里面`99%`的数据。`100`万个数据里面查找最大的`10000`个数据的方法如下: + 用快速排序的方法,将数据分为`2`堆,如果大的那堆个数`N`大于`10000`个,继续对大堆快速排序一次分成`2`堆,如果大的那堆个数`N`大于`10000`个, + 继续对大堆快速排序一次分成`2`堆,如果大堆个数`N`小于`10000`个,就在小的那堆里面快速排序一次,找第`10000-n`大的数字;递归以上过程,就可 + 以找到第`10000`大的数。一共需要101次这样的比较。 - `Hash`法 - 如果这`1`亿个书里面有很多重复的数,先通过`Hash`法,把这`1`亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的`10000`个数。 + 如果这`1`亿个数里面有很多重复的数,先通过`Hash`法,把这`1`亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间, + 然后通过分治法或最小堆法查找最大的`10000`个数。 - 最小堆 - 首先读入前`10000`个数来创建大小为`10000`的最小堆,建堆的时间复杂度为`O(mlogm)`(`m`为数组的大小即为`10000`),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至`1`亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有`10000`个数字。该算法的时间复杂度为`O(nmlogm)`,空间复杂度是10000(常数)。 + 首先读入前`10000`个数来创建大小为`10000`的最小堆,建堆的时间复杂度为`O(mlogm)`(`m`为数组的大小即为`10000`),然后遍历后续的数字, + 并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至`1`亿 + 个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有`10000`个数字。该算法的时间复杂度为`O(nmlogm)`,空间复杂度是10000(常数)。 解决`Top K`问题有两种思路: diff --git "a/JavaKnowledge/UML\347\261\273\345\233\276.pdf" "b/JavaKnowledge/UML\347\261\273\345\233\276.pdf" new file mode 100644 index 00000000..d7f9f7f7 Binary files /dev/null and "b/JavaKnowledge/UML\347\261\273\345\233\276.pdf" differ diff --git "a/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" "b/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" index 71132805..3c2cffc6 100644 --- "a/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" +++ "b/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" @@ -16,10 +16,28 @@ Vim使用教程 --- - `i` 进入`insert`模式 -- `a` 在光标后进行插入,直接进入`Insert`模式 -- `o` 在当前行后插入一个新行,直接进入`Insert`模式 +- `a(append)` 在光标后进行插入,直接进入`insert`模式 +- `o(open a line below)` 在当前行后插入一个新行,直接进入`insert`模式 +- `O` 大写的O是在光标所在的行下面插入一个新航,直接进入编译模式 +- `I` 从该行的最前面开始编辑 +- `A` 从该行的最后面开始编辑 +- `s` 删除光标后的字符,从光标当前位置插入 +- `S` 删除光标所在当前行,从行首插入 - `cw` 替换从光标位置开始到该单词结束位置的所有字符,直接进入`Insert`模式 +在VIM中,有相当一部分命令可以扩展为3部分: + +- 开头的部分是一个数字,代表重复次数; +- 中间的部分是命令; +- 最后的部分,代表命令的对象。 + +比如,命令3de中,3表示执行3次,d是删除命令,e表示从当前位置到单词的末尾。整条命令的意思就是,从当前位置向后,删除3个单词。类似的,命令3ce表示从当前位置向后,删除三个单词,然后进入编辑模式。 + +可以看到,命令组合的前两个部分比较简单,但第三个部分也就是命令对象,技巧就比较多了。所以接下来,我就与你详细介绍下到底有哪些命令对象可以使用。 + +其实,对命令对象并没有一个完整的分类。但我根据经验,将其总结为光标移动命令和文本对象两种。 + +第一种是光标移动命令。比如,$命令是移动光标到本行末尾,那么d$就表示删除到本行末尾;再比如,4}表示向下移动4个由空行隔开的段落,那么d4}就是删除这4个段落。 移动光标 --- @@ -34,14 +52,17 @@ Vim使用教程 - `$` $光标移动当前行尾 - `0` 数字0光标移动当前行首 -- `e` 移动到单词结束位置 +- `e` 向右移动一个单词 +- `w` 向右移动一个单词,与e的区别是w是把光标放到下一个单词的开头,而e是把光标放到这一个单词的结尾。 - `b` 移动到单词开始位置 - `:59` 移动到59行 -- `#l` 移动光标到改行第#个字的位置,如`5l` +- `#l` 移动光标到该行第#个字的位置,如`5l` - `ctrl+g` 列出当前光标所在行的行号等信息 - `: #` 如输入: 15会跳到文章的第15行 -- `ctrl + d`向下翻页 -- `ctrl + u`向上翻页 +- `ctrl+b`:向上滚动一屏 +- `ctrl+f`:向下滚动一屏 +- `ctrl+u`:向上滚动半屏 +- `ctrl+d`:向下滚动半屏 删除文字 --- @@ -52,6 +73,7 @@ Vim使用教程 - `#X` 删除光标所在位置前的#个字符 - `dd` 删除当前行,并把删除的行存到剪贴板中 - `#dd` 从光标所在行开始删除#行。如`5dd`就是删除5行 +- `v/ctrl+v`: 使用h、j、k、l移动选择内容,然后按d删除。其中v是非列模式,ctrl+v是列模式 复制粘贴 --- @@ -63,7 +85,9 @@ Vim使用教程 - `y$` 拷贝光标至本行结束位置 - `y` 拷贝选中部分,在`Normal`模式下按`v`会进入到可视化模式,这时候可以上下移动进行选中某一部分,然后按`y`就可以复制了。 -- `p` 粘贴 +- `p` 在光标所在的位置向下开辟一行,粘贴 +- `P` 在光标所在的位置向上开辟一行,粘贴 +- 剪切: 按dd或者ndd删除,将删除的行保存到剪贴板中,然后按p/P就可以粘贴了。 替换 @@ -72,6 +96,9 @@ Vim使用教程 - `r` 替换光标所在处的字符 - `R` 替换光标所到之处的字符,直到按下`esc`键为止 - `:%s/old/new/g` 用`new`替换文件中所有的`old` +- `:%s/old/new/gc`,同上,但是每次替换需要用户确认 +- `:s/old/new/g` 光标所在行的所有old替换为new +- `:s/old/new/` 光标所在行的第一个old替换为new 撤销 --- @@ -92,7 +119,7 @@ Vim使用教程 - `>>` 当前行缩进 - `#>>` 当前光标下n行缩进 - `<<` 当前行缩出 -- `>>` 当前光标下n行缩出 +- `#<<` 当前光标下n行缩出 - `: set nu` 会在文件每一行前面显示行号 - `:wq` 保存并退出 @@ -100,7 +127,8 @@ Vim使用教程 - `:q!` 退出不保存 - `:saveas ` 另存为 - `:e filename` 打开文件 -- `:sav filename` 保存为某文件名 +- `:sav filename 保存为某文件名 +- ZZ: 命令模式使用大写ZZ直接保存并退出 diff --git "a/JavaKnowledge/hashCode\344\270\216equals.md" "b/JavaKnowledge/hashCode\344\270\216equals.md" index 75bf9067..c73130a0 100644 --- "a/JavaKnowledge/hashCode\344\270\216equals.md" +++ "b/JavaKnowledge/hashCode\344\270\216equals.md" @@ -1,11 +1,12 @@ hashCode与equals === -`HashSet`和`HashMap`一直都是`JDK`中最常用的两个类,`HashSet`要求不能存储相同的对象,`HashMap`要求不能存储相同的键。 那么`Java`运行时环境是如何判断`HashSet` -中相同对象、`HashMap`中相同键的呢?当存储了相同的东西之后`Java`运行时环境又将如何来维护呢? -在研究这个问题之前,首先说明一下`JDK`对`equals(Object obj)`和`hashcode()`这两个方法的定义和规范:在`Java`中任何一个对象都具备`equals(Object obj)` -和`hashcode()`这两个方法,因为他们是在`Object`类中定义的。`equals(Object obj)`方法用来判断两个对象是否“相同”,如果“相同”则返回`true`,否则返回`false`。 -`hashcode()`方法返回一个`int`数,在`Object`类中的默认实现是“将该对象的内部地址转换成一个整数返回”。 +`HashSet`和`HashMap`一直都是`JDK`中最常用的两个类,`HashSet`要求不能存储相同的对象,`HashMap`要求不能存储相同的键。 那么`Java`运行时 +环境是如何判断`HashSet`中相同对象、`HashMap`中相同键的呢?当存储了相同的东西之后`Java`运行时环境又将如何来维护呢? +在研究这个问题之前,首先说明一下`JDK`对`equals(Object obj)`和`hashcode()`这两个方法的定义和规范: +在`Java`中任何一个对象都具备`equals(Object obj)`和`hashcode()`这两个方法,因为他们是在`Object`类中定义的: +- `equals(Object obj)`方法用来判断两个对象是否“相同”,如果“相同”则返回`true`,否则返回`false`。 +- `hashcode()`方法返回一个`int`数,在`Object`类中的默认实现是“将该对象的内部地址转换成一个整数返回”。 接下来有两个个关于这两个方法的重要规范: - 若重写`equals(Object obj)`方法,有必要重写`hashcode()`方法,确保通过`equals(Object obj)`方法判断结果为`true`的两个对象具备相等的`hashcode()`返回值。 diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" new file mode 100644 index 00000000..35233217 --- /dev/null +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -0,0 +1,1246 @@ +# python3入门 + +## 变量 + +```python +message = "Hello World" +print(message) +``` + +常量: python中一版约定名字全为大写的就是常量。 + +## bool + + +bool类型只有True和False,用于真假判断。 + +python3中,bool是int的子类,True和False可以和数字相加。 + +True == 1、False == 0 会返回True + +is运算符用于比较两个对象的身份(即它们是否是同一个对象,是否在内存中占据相同的问题),而不是比较它们的值。 + +```python3 +print(True == 1) # True +print(False == 0) # True + +print(True is 1) # False +print(False is 0) # False + +``` + + +在python中,能够解释为假的值不只有False,还有: + +- None +- 0 +- 0.0 +- False +- 所有的空容器(空列表、空元组、空字典、空集合、空字符串) + +## 字符串 + +字符串就是一系列字符。在Python中,用引号括起来的都是字符串,其中的引号可以是单引号,也可以是双引号。例如: +```python +message = 'Hello World' +print(message) +message = "Hello World" +print(message) +``` + +### 大小写 + +修改单词中的大小写: + +- title()方法是以首字母大写的方式显示每个单词,即将每个单词的首字母都改为大写。 +- upper()方法将字符串改为全部大写。 +- lower()方法将字符串改为全部小写。 + +```python +message = 'hello world' +print(message.title()) +# 输出结果为Hello World +``` + +注意上面方法lower、upper、title等不会修改存储在变量message中的值。 + +### 拼接 + +合并: Python使用加号(+)来合并字符串。这种合并字符串的方法称为拼接。 +```python +first_name = "ada" +last_name = "lovelace" +full_name = first_name + " " + last_name +print(full_name) +# 输出: ada lovelace +``` + +### 空格 + +rstrip(): 去除字符串末尾空白。注意去除只是暂时的,等你再次访问该变量时,你会发现这个字符串仍然包含末尾空白。 +要永久删除这个字符串中的空白,必须将删除的结果存回到变量中: + + +first_name = "ada " +print(first_name) +first_name = first_name.rstrip() +print(first_name) + +- lstrip(): 去除字符串开头空白 +- strip(): 同时去除字符串两端的空白 + + +## 整数 + + +可对整数执行加(+)、减(-)、乘(`*`)、除(/)运算。 + +Python使用两个乘号表示乘方运算。 + + +## 浮点数 + +在Python中将带小数点的数字都称为浮点数。 +但是要注意的是,结果包含小数位数可能是不确定的,例如: +```python +>>> 0.2 + 0.1 +0.30000000000000004 +``` + +命令行执行python后执行exit()或quit()可退出。 + +## 类型转换 + +```python +age = 23 +message = "Happy" + age + "Birthday" +print(message) +``` + +执行时会报错: +```python +message = "Happy" + age + "Birthday" + ~~~~~~~~^~~~~ +TypeError: can only concatenate str (not "int") to str +``` + +类型错误,这个时候需要显式的指定希望Python将这个整数用作字符串。 +为此,可调用函数str(),它让Python将非字符串值表示为字符串: + +```python +age = 23 +message = "Happy" + str(age) + "Birthday" +print(message) +``` + +### 自动类型转换(隐式转换) + +对两种不同类型的数据进行运算,较小的数据类型(整数)就会转换为较大的数据类型(浮点数)以免数据丢失,计算结果为浮点型: + +```python3 +num1 = 2 +num2 = 3.0 + +print(num1 + num2) # 5.0 + + + +num1 = 9 +num2 = 1 +print(num1 / num2) #9.0 +``` +注意: 特别的,两个整形进行除法运算时结果也是浮点型。 + + + + + + + + + + + +## 注释 + +使用井号(#)标识注释 + + +## 列表 + +在Python中,用方括号[]来表示列表,并用逗号来分割其中的元素。 +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +print(bicycles) +print(bicycles[0]) +print(bicycles[-1]) +# 设置最后一个元素 +bicycles[-1] = 'honda' +# 在列表最后添加元素 +bicycles.append('ducati') +# 在某个位置添加元素 +bicycles.insert(0, 'a') +print(bicycles) +# 删除某个位置的元素 +del bicycles[0] +# 删除最后一个元素,并返回删除的值 +popValue = bicycles.pop() +print(popValue) +# 通过值删除元素,如果列表中有多个重复的元素,那该方法只会删除第一个指定的值 +bicycles.remove('trek') +# 排序 +bicycles.sort() +# 反序排序 +bicycles.sort(reverse=True) +``` +Python为访问最后一个列表元素提供了一种特殊的语法,通过将索引指定为-1,可让Python返回最后一个列表元素。 +这是为了方便在不知道列表长度的情况下访问最后的元素。 +这种约定也适用于其他负数索引,例如,索引-2返回倒数第二个列表元素,索引-3返回倒数第三个列表元素,以此类推。 + +### 临时排序 + +要保留列表元素原来的排列顺序,同时以特定的顺序呈现它们,可使用函数sorted()。 +函数sorted()让你能够按特定顺序显示列表元素,同时不影响它们在列表中的原始排列顺序。 + +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +bicycles.sort(reverse=True) +# ['trek', 'specialized', 'redline', 'cannondale'] +print(bicycles) +# ['cannondale', 'redline', 'specialized', 'trek'] +print(sorted(bicycles)) +# ['trek', 'specialized', 'redline', 'cannondale'] +print(bicycles) +``` + + +### 反转列表 + +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +bicycles.reverse() +print(bicycles) +# 列表长度 +size = len(bicycles) +print(size) +``` + +### 遍历 + +**Python根据缩进来判断代码行与前一个代码行的关系,在较长的Python程序中,你将看到缩进程度各不相同的代码块,这让你对程序的组织结构有大致的认识** + + +注意: + +- for in 后有个冒号`:`,for语句末尾的冒号告诉Python,下一行是循环的第一行。 +- 前面缩进的代码才是for循环中的部分 +- 没有缩进的是for循环之外的 + + +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +for bcy in bicycles: + print(bcy) + print(bcy.title()) +print('end') +``` + +#### 使用enumerate()可以同时获取列表的下标和元素。 + +```python +lst = [1, 2, 3] + +for i, value in enumerate(lst): + print(i, value) +``` + + +### 切片 + +你还可以处理列表的部分元素——Python称之为切片。 +要创建切片,可指定要使用的第一个元素和最后一个元素的索引。 +你可以生成列表的任何子集,例如,如果你要提取列表的第2~4个元素,可将起始索引指定为1,并将终止索引指定为4: + +```python +players = ['charles','martina','michael','florence','eli'] +print(players[1:4]) +``` + +例如,如果要提取从第3个元素到列表末尾的所有元素,可将起始索引指定为2,并省略终止索引 + + +### range函数 + +range()函数能够生成一系列的数字,例如range(1, 5)会生成1 2 3 4。 +要创建数字列表,可使用函数list()将range()的结果直接转换为列表: +```ptyhon +numbers = list(range(1, 5)) +print(numbers) + +# +print(min(numbers)) +print(max(numbers)) +print(sum(numbers)) + +``` + + +## 元组(不可变的列表) + +有时候你需要创建一系列不可修改的元素,元组可以满足这种需求。Python将不能修改的值称为不可变的,而不可变的列表被称为元组。 +元组看起来犹如列表,但使用圆括号而不是方括号来标识。定义元组后,就可以使用索引来访问其元素,就像访问列表元素一样。 +元组中的元素不能修改,访问和遍历都和列表一样。 + + +相对于列表,元组是更简单的数据结构。 +如果需要存储的一组值在程序的整个生命周期内都不变,可使用元组。 + + + +## if语句 + +```python +cars = ['audi', 'bmd', 'toyota'] +for car in cars: + if car == 'toyota': + print(car.upper()) + else: + print(car.title()) +``` +每条if语句的核心都是一个值为True或False的表达式,这种表达式被称为条件测试。 + + +```python +age = 12 +if age < 4: + print("cost $0") +elif age < 18: + print("cost $5") +else: + print("cost $10") +``` + +### match case语句 + +```python3 +match x: + case a: + xxxx + case b: + xxxx + case _: + xxx +``` +在python3.10版本新增了match case的条件判断方式,match后的对象会依次与case后的内容匹配,匹配成功则执行相应的语句,否则跳过,其中_可以匹配一切。 + + +### 相等判断 +```python +car = 'Audi' +# False +car == 'audi' +``` + +Python中使用两个等号(==)检测值是否相等,在Python中检测是否相等时区分大小写。 + +判断两个值不相等,可结合使用惊叹号和等号(!=)来判断。 + +条件语句中可包含各种数学比较,如小于<、小于等于<=、大于>、大于等于>=。 + +### 多条件检测 + +有时候需要判断两个条件都为True或只要求一个条件为True时就执行相应的操作。 + +在这些情况下,可以使用关键字and和or。 + +### 值是否包含在列表中 + +要判断特定的值是否已包含在列表中,可使用关键字in。 +```python +cars = ['audi', 'bmd', 'toyota'] +# True +print('audi' in cars) + +# False +print('audi' not in cars) +``` + + +## 字典(Key-Value) + +在Python中,字典是一系列键-值对。字典用放在花括号{}中的一些列键-值对表示。 +每个键都与一个值相关联,你可以使用键来访问与之相关联的值。 +与键相关联的值可以是数字、字符串、列表乃至字典。事实上,可将任何Python对象用作字典中的值。 + +```python +alien = {'color': 'blue', 'points': 5} +# 取值 +print(alien['color']) +print(alien['points']) +# 添加值 +alien['xPos'] = 10 +alien['yPos'] = 20 +print(alien['xPos']) +del alien['points'] +``` + +字典的其他写法: + +```python3 +dict4 = dict(name="Bob", age=20, gender="female") +dict5 = dict([("name", "Tom"), ("age", 22), ("gender", "male")]) +``` + + +### 遍历字典 + +- keys()方法返回所有的键列表。 +- items()方法返回一个键-值对列表。 +- values()方法返回一个值列表。 + +```ptyhon +alien = {'color': 'blue', 'points': 5} + +for key,value in alien.items(): + print("key: " + key) + print("value: " + str(value)) + + +for key in alien.keys(): + print("key: " + key) +``` +字典总是明确地记录键和值之间的关联关系,但获取字典的元素时,获取顺序是不可预测的。 + +这不是问题,因为通常你想要的只是获取与键相关联的正确的值。 + +要以特定的顺序返回元素,一种方法是在for循环中对返回的键进行排序。。 + +为此,可使用函数sorted()来获得特定顺序排列的键列表的副本: +```python +favorite_languages = { + 'jen':'python', + 'sarah':'c', + 'edward':'ruby', + 'phil':'python', + } +for name in sorted(favorite_languages.keys()): + print(name.title()+",thank you for taking the poll.") +``` + +```python +favorite_languages = { + 'jen':'python', + 'sarah':'c', + 'edward':'ruby', + 'phil':'python', + } +print("The following languages have been mentioned:") +for language in favorite_languages.values(): + print(language.title()) +``` + +这种做法提取字典中所有的值,而没有考虑是否重复。涉及的值很少时,这也许不是问题,但如果被调查者很多,最终的列表可能包含大量的重复项。为剔除重复项,可使用集合(set)。集合类似于列表,但每个元素都必须是独一无二的: +```python +favorite_languages = { + 'jen':'python', + 'sarah':'c', + 'edward':'ruby', + 'phil':'python', + } +print("The following languages have been mentioned:") +for language in set(favorite_languages.values()):❶ + print(language.title()) +``` +通过对包含重复元素的列表调用set(),可让Python找出列表中独一无二的元素,并使用这些元素来创建一个集合。 + +## 用户输入 + +函数input()让程序暂停运行,等待用户输入一些文本。 + +获取用户输入后,Python将其存在一个变量中,以方便你使用。 +```python3 +age = input("How old are you?") +print(age) +``` +用户输入的是数字21,但我们请求Python提供age的值时,它返回的是`'21'`(用户输入的数值的字符串表示)。 + +为了解决这个问题,可以使用函数int():将数字的字符串表示转换为数值表示,如: +```python +age = input("Please Input") +print(age) +# print(age >= 18) 报错 +age = int(age) +print(age >= 18) +``` + +## while + +```python +current_number = 1 +while current_number <= 5: + print(current_number) + current_number+= 1 +``` + +同样while中也可以结合使用break、continue等,和Java基本一样。 + +## 缩进 + +在python3中,代码块的结束不像其他一些编程语言(如c、java等)使用大括号{}来明确界定,而是通过缩进来表示。PEP8建议每级缩进都使用四个空格。 + + +## 函数 + +使用关键字def来定义一个函数,定义以冒号结尾。 +跟在def xxx:后面的所有缩进行构成了函数体。 + +```python +def greet_user(username): + """函数功能的注释""" + # 返回值 + return 'Hello ' + username + +# 调用函数 +print(greet_user('jack')) +print(greet_user(username='lili')) +``` +同样,在Python中函数也支持参数的默认值。 + +上面三个引号的部分是文档字符串格式,用于简要的阐述其功能的注释。 + +### 函数的参数传递 + +- 不可变类型 + 类似c++的值传递,如整数、字符串、元祖。如fun(a),传递的只是a的值,没有影响a对象本身,比如在fun(a)内部修改a的值,只是修改了另一个复制的对象,不会影响a本身。 +- 可变类型 + 类似c++的引用传递,如列表、字典。如fun(la),则是将la真正的传过去,修改后fun外部的la也会受影响 + + +python中一切都是对象,严格意义上我们不能说值传递还是引用传递,我们应该说传可变对象和不可变对象。 + + + +### 函数列表参数副本 + +将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量的数据。 + +但是有些时候我们并不想让函数修改原始的列表。 + +为解决这个问题,可向函数传递列表的副本而不是原件;这样函数所做的任何修改都只影响副本,而丝毫不影响原件。 + +要将列表的副本传递给函数,可以像下面这样做: +``` +function_name(list_name[:]) +``` + +切片表示法[:]创建列表的副本。 + +### 函数不定参数 + +有时候,你预先不知道函数需要接受多少个实参,Python允许函数从调用语句中收集任意数量的实参。 +```python +def make_pizza(*toppings): + """打印顾客点的所有配料""" + print(toppings) +make_pizza('pepperoni') +make_pizza('mushrooms','green peppers','extra cheese') +``` +形参名`*toppings`中的星号让Python创建一个名为toppings的空元组,并将收到的所有值都封装到这个元组中。 + +现在,我们可以将这条print语句替换为一个循环,对配料列表进行遍历,并对顾客点的比萨进行描述: + +```python +def make_pizza(*toppings): + """概述要制作的比萨""" + print("\nMaking a pizza with the following toppings:") + for topping in toppings: + print("- "+topping) +make_pizza('pepperoni') +make_pizza('mushrooms','green peppers','extra cheese') +``` + + +简单来说,Python中所有的函数参数传递,统统都是基于传递对象的引用进行的。这是因为,在Python中,一切皆对象。而传对象,实质上传的是对象的内存地址,而地址即引用。 + + +看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 + + +### 解包传参 + +若函数的形参是定长参数,可以通过`*`和`**`对列表、元组、字典等解包传参。 + +```python3 +def func(a, b, c): + return a + b + c + +tuple1 = (1, 2, 3) + +print(func(*tuple)) +dict1 = {"a":1, "b":2, "c":3} + +print(func(**dict1)) +``` + + + +### 将函数存储在模块中 + +函数的优点之一是,使用它们可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。你还可以更进一步,将函数存储在被称为模块的独立文件中,再将模块导入到主程序中。import语句允许在当前运行的程序文件中使用模块中的代码。 + + +要让函数是可导入的,得先创建模块。模块是扩展名为.py的文件,包含要导入到程序中的代码。下面来创建一个包含函数make_pizza()的模块。为此,我们将文件pizza.py中除函数make_pizza()之外的其他代码都删除: + +```python +# pizza.py + +def make_pizza(size,*toppings): + """概述要制作的比萨""" + print("\nMaking a "+str(size)+ + "-inch pizza with the following toppings:") + for topping in toppings: + print("- "+topping) +``` + +接下来,我们在pizza.py所在的目录中创建另一个名为making_pizzas.py的文件,这个文件导入刚创建的模块,再调用make_pizza()两次: + +```python +# making_pizzas.py +import pizza +pizza.make_pizza(16,'pepperoni') ❶ +pizza.make_pizza(12,'mushrooms','green peppers','extra cheese') + +``` +Python读取这个文件时,代码行import pizza让Python打开文件pizza.py,并将其中的所有函数都复制到这个程序中。你看不到复制的代码,因为这个程序运行时,Python在幕后复制这些代码。你只需知道,在making_pizzas.py中,可以使用pizza.py中定义的所有函数。 + +你还可以导入模块中的特定函数,这种导入方法的语法如下: +`from modulname import funcxx` + +```python +from pizza import make_pizza +make_pizza(16,'pepperoni') +make_pizza(12,'mushrooms','green peppers','extra cheese') +``` +若使用这种语法,调用函数时就无需使用句点。由于我们在import语句中显式地导入了函数make_pizza(),因此调用它时只需指定其名称。 + +还可以使用星号(`*`)导入模块中的所有函数: + +```python +from pizza import * +make_pizza(16,'pepperoni') +make_pizza(12,'mushrooms','green peppers','extra cheese') +``` +`import *`会将模块pizza中的每个函数都复制到这个程序文件中。 +由于导入了每个函数,可通过名称来调用每个函数,而无需使用句点表示法。 +然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法: 如果模块中有函数的名称与你的项目中使用的名称相同,可能导致意想不到的结果,因为Python可能遇到多个名称相同的函数或变量,进而覆盖函数,而不是分别导入所有的函数。 + + +### 别名 + +如果要导入的函数的名称与现有的名称冲突,或者函数的名称太长,可以通过别名的方式进行指定。 + +```python +from pizza import make_pizza as mp +mp(16,'pepperoni') +mp(12,'mushrooms','green peppers','extra cheese') +``` + +同样也可以通过as给模块指定别名: + +```python +import pizza as p + +p.make_pizza(16,'pepperoni') +p.make_pizza(12,'mushrooms','green peppers','extra cheese') +``` + +### __name__ + +在python中,__name__是一个特殊的内置变量: + +- 当一个python文件被直接运行时,该文件的__name__属性为"__main__" +- 当一个python文件作为模块被导入时,__name__属性会被设置为该模块的名称(即文件名,不包括.py后缀) + +例如有时我们在模块中写一些测试代码,当模块被其他文件导入时这些测试代码会被执行。 +例如我们在main.py中导入my_add.py中,会发现my_add中的测试代码被执行了。这个时候使用__name__ == "__main__"判断就能避免测试代码被执行。 + + + + + + +### 全局变量和局部变量 + + +定义在函数内部的变量拥有一个局部作用域,定义在函数外的变量拥有全局作用域,局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。 + +#### global关键字 + +使用global关键字修改全局变量 +```python +def function_a(): + global var1 + var1 = 200 + print("var1:", var1) + +var1 = 100 + +``` +当全局变量为可变类型时,函数内部使用global声明,也可以对其进行修改。 +```python +def function_a(): + list1[0] = 1000 + +list1 = [1, 2, 3] +``` +在函数中不使用global声明全局变量时不能修改全局变量的本质是不能修改全局变量的指向,即不能将全局变量指向新的数据。 + +- 不可变类型的全局变量其指向的数据不能修改,所以部使用global无法修改全局变量。 +- 可变类型的全局变量其指向的数据可以修改,所以不使用global也可修改全局变量。 + +#### nonlocal关键字 + +nonlocal也用作内部作用域修改外部作用域的变量的场景,不过此时外部作用域不是全局作用域,而是嵌套作用域 + +```python +def function_outer(): + var = 1 + print(var) + + def function_inner(): + nonlocal var + var = 200 + function_inner() + print(var) + + +function_outer() +``` + + + +## 类 + +在Python中,首字母大写的名称指的是类。 +根据类来创建对象叫实例化。 + +dog.py +```python +class Dog: + home = "xx" # 类属性 + + def __init__(self, name, age): + self.name = name # 实例属性 + self.age = age + + def sit(self): + print(self.name + " Sitting") + + def roll(self): + print(self.name + " Rolling") +``` + +__init__()是一个特殊的方法(构造方法),每当你根据Dog类创建新实例时,Python都会自动运行它(__init__()方法的调用时机在实例(通过__new__()被创建之后,返回调用者之前调用,一般用于初始化一些数据))。在这个方法中形参self必不可少,还必须位于其他形参的前面。 + +为什么必须在方法定义中包含形参self呢?因为Python调用这个__init__()方法来创建Dog实例时,将自动传入实参self。 + +每个与类相关联的方法调用都自动传入实参self,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。 + + + +```python +import dog + +dog = dog.Dog("xiaohei", 1) +# 属性 +print(dog.name) +# 方法 +dog.sit() +``` + +类属性可以直接通过类名.类属性调用也可以通过实例对象.类属性调用,如`Dog.home或dog.home` + +```python +class Person: + home = "class home" + + def __init__(self, name) -> None: + self.name = name + + +person = Person("name") +print(person.name) +print(person.home) +person.home = "object home" +print(person.home) +print(Person.home) +Person.home = "new class home" +print(Person.home) + +# 结果 +name +class home +object home +class home +new class home + +``` +可以看到所有类的实力都会共享同一个类属性,如果通过对象实例.类属性调用则会直接创建一个实例属性(并不是修改了类属性的值) + +#### 动态添加属性和方法 + +```python +class Person: + def __init__(self, name: str) -> None: + self.name = name + +p = Person("John") +p.age = 20 +print(p.name) +print(p.age) + +``` + + + +### 方法 + +Python的类中有三种方法: 实例方法、静态方法、类方法。 + +- 实例方法在类中定义,第一个参数是self,代表实例本身 +- 实例方法只能被实例对象调用 +- 可以访问实例属性、类属性、类方法 + +#### 类方法 + +- 类方法通过@classmethod标注,方法的第一个参数是cls +- 类方法可以通过类来调用也可以通过实例来调用 +- 可以访问类属性 +- 类方法可以在不创建实例的情况下调用,通过类名直接调用,非常方便,适合一些和类整体相关的操作 + + +```python +class Dog: + + @classmethod + def wangwang(cls): + print"wangwang" + + +``` + +#### 静态方法 + +- 静态方法通过@staticmethod定义 +- 不访问实例属性或类属性,只依赖传入的参数 +- 可以通过类名或实例调用,但它不会访问类或实例的内部信息,更像一个工具函数 + + +#### 特殊方法 + + +方法名中有两个前缀下划线和两个后缀下划线的方法为特殊方法,也叫魔法方法。 +上面的__init__()就是一个特殊方法,这些方法会在进行特定的操作时自动被调用。 + +几个常见的特殊方法: + +- __new__(): 对象实例化时第一个调用的方法 +- __init__(): 类的初始化方法 +- __del__(): 对象的销毁器,定义了当对象被垃圾回收时的行为,使用del xxx时不会主动调用__del__(),除非此时引用计数=0 +- __str__(): 定义了对类的实例调用str()时的行为 +- __repr__(): 定义了对类的实例调用repr()时的行为,str()和repr()最主要的差别在于目标用户。repr()的作用是产生机器可读的输出,而str()则产生人类可读的输出。 +- __getattribute__():属性访问拦截器,定义了属性被访问前的操作。 + + + + + + +### 继承 + +一个类继承另一个类时,它将自动获得另一个类的所有属性和方法。 + +创建子类实例时,Python首先需要完成的任务是给父类的所有属性赋值。因此,子类的__init__()方法需要调用父类的方法。 + +```python +class Car: + def __init__(self, make, model, year): + self.make = make + self.model = model + self.year = year + + def get_des(self): + name = str(self.year) + str(self.make) + str(self.model) + return name.title() + + +class ElectricCar(Car): + def __init__(self, make, model, year, battery): + super().__init__(make, model, year) + self.battery = battery + + # 重写父类方法 + def get_des(self): + name = str(self.year) + str(self.make) + str(self.model) + str(self.battery) + return name.title() + def get_battery(self): + print("battery : " + str(self.battery)) + + +tesla = ElectricCar('Tesla', 'Model S', 2021, 80) +print(tesla.get_des()) +tesla.get_battery() +``` + +- 创建子类时,父类必须包含在当前文件中,且位于子类前面。 +- 定义子类时,必须在括号内指定父类的名称 +- 方法__init__()接受创建子类实例所需的信息 + +super()是一个特殊函数,帮助Python将父类和子类关联起来。这行代码让子类包含父类的所有属性。 + + +多继承: 调用方法时先在子类中查找,若不存在则从左到右依次查找父类中是否包含方法。 + +#### 私有属性和方法 + +有时为了限制属性和方法只能在类内访问,外部无法访问。或父类中某些属性和方法不希望被子类继承,可以将其私有化: + +- 单下划线: 非公开API + + 大多数python代码都遵循这样一个约定,有一个前缀下划线的变量或方法应被视为非公开的API,例如_var1。但这种约定不具有强制力。 + +- 双下划线: 名称改写 + 有两个前缀下划线,并至多一个后缀下划线的标识符,例如__x,会被改写为_类名__x,只有在类内部可以通过__x访问,其他地方无法访问或只能通过_类名__x访问。 + + +#### property装饰器 + +可通过@property装饰器将一个方法转换为属性来调用。转换后可直接使用.方法名来使用,而无需使用.方法名()。 + +注意: @property修饰的方法不要和变量重名,否则可能导致无限递归 +#### 只读属性 + +```python +class Person: + def __init__(self, name: str) -> None: + self.__name = name + + @property + def name(self): + return self.__name + +p = Person("zhangsan") +print(p.name) + +.name = "lisi" # 报错 +``` + +#### 读写属性 + +使用属性名.setter装饰 + +```python +class Person: + def __init__(self, name: str) -> None: + self.__name = name + + @property + def name(self): + return self.__name + + @name.setter + def name(self, name: str) -> None: + self.__name = name + +p = Person("zhangsan") +print(p.name) + +p.name = "lisi" +print(p.name) +``` + + + + +## 标准库 + +Python标准库是一组模块,安装的Python都包含它。 +类名应采用驼峰命名法,即将类名中的每个单词的首字母都大写,而不使用下划线。实例名和模块名都采用小写格式,并在单词之间加上下划线。 +每个模块也都应包含一个文档字符串,对其中的类可用于做什么进行描述。 + + +### 读取文件 + +一次性读取整个文件: +```python +with open('test.txt') as file_object: + contents = file_object.read() + print(contents) +``` + +要以每次一行的方式检查文件,可对文件对象使用for循环: + +```python +filename = 'test.txt' +with open(filename) as file_object: + for line in file_object: + print(line) +``` +这里使用了关键字with,让Python负责妥善地打开和关闭文件。 +使用关键字with时,open()返回的文件对象只在with代码块内可用。 + + +要将文本写入文件,你在调用open()时需要提供另一个实参,告诉Python你要写入打开的文件。 + +```python +filename = 'test.txt' + +with open(filename, 'w') as fo: + fo.write("Hello World") +``` +调用open()时提供了两个实参: + +- 第一个实参是要打开的文件的名称 +- 第二个实参('w')告诉Python,我们要以写入模式打开这个文件。 + +打开文件时,可指定读取模式('r')、写入模式('w')、附加模式('a')或让你能够读取和写入文件的模式('r+')。如果你省略了模式实参,Python将以默认的只读模式打开文件。 +如果你要写入的文件不存在,函数open()将自动创建它。然而,以写入('w')模式打开文件时千万要小心,因为如果指定的文件已经存在,Python将在返回文件对象前清空该文件。 +注意 Python只能将字符串写入文本文件。要将数值数据存储到文本文件中,必须先使用函数str()将其转换为字符串格式。 + +如果你要给文件添加内容,而不是覆盖原有的内容,可以附加模式打开文件。你以附加模式打开文件时,Python不会在返回文件对象前清空文件,而你写入到文件的行都将添加到文件末尾。如果指定的文件不存在,Python将为你创建一个空文件。 + + +### 异常 + +异常是使用try-except代码块处理的。try-except代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办。使用了try-except代码块时,即便出现异常,程序也将继续运行:显示你编写的友好的错误消息,而不是令用户迷惑的traceback。 + +```python +try: + 可能发生异常的代码 +except 异常类型1 as 变量名1: + 异常1处理代码 +except 异常类型2 as 变量名2: + 异常2处理代码 +else: + 没有异常时执行的代码 +finally: + 无论是否发生异常都会执行的代码 +``` + +#### 抛出异常 + +当想在代码中明确表示发生了错误或异常时,可以使用raise来抛出异常。 +```python +def int_add(a, b): + if isinstance(a, int) and isinstance(b, int): + return a + b + else: + raise TypeError("参数类型错误") +``` + +### with关键字 + + +python中的with语句用于异常处理,封装了try except finally编码范式,提供了一种简洁的方式来确保资源的正确获取和释放,同时处理可能发生的异常,提高了易用性,使代码更清晰、更具可读性,简化了文件流等公共资源的管理 + +```python +with expression as variable: + # 代码块 +``` + +- expression: 通常是一个对象或函数调用,该对象需要一个上下文管理器,即实现了__enter__和__exit__方法 + +- variable: 是可选的,用于存储expression的__enter__方法的返回值。 + +工作原理: + +- 使用with关键字系统会自动调用f.close()方法,with的作用等效于try finally语句。 - 当执行with语句时,会调用expression对象的__enter__方法 +- __enter__方法的返回值可以被存储在variable中(如果有),以供with代码块中使用 +- 然后执行with语句内部的代码块 +- 无论在代码块中是否发生异常,都会调用expression对象的__exit__方法,以确保资源的释放或清理工作,这类似于try - except -finally中的finally语句。 + + + + + +### 分割字符串 + +方法split()以空格为分隔符将字符串分拆成多个部分,并将这些部分都存储到一个列表中。 + +```python +title = "Alice in Wonderland" +title.split() +``` + +['Alice','in','Wonderland'] + + +### pass + +Python有一个pass语句,可在代码块中使用它来让Python什么都不要做。 + +```python +def donothing(): + pass #空语句 + +``` + + +pass语句如果啥事都不做,那要它有何用呢?实际上,pass可以作为一个占位符来用——视为未成熟代码的“预留地”。比如说,如果我们还没想好函数的内部实现,就可以先放一个pass,让代码先跑起来。 + +### json + +函数json.dump()接受两个实参:要存储的数据以及可用于存储数据的文件对象。下面演示了如何使用json.dump()来存储数字列表: + +```python +import json +numbers = [2,3,5,7,11,13] +filename = 'numbers.json' +with open(filename,'w') as f_obj: + json.dump(numbers,f_obj) +``` + +使用json.load()将这个列表读取到内存中: +```python +import json +filename = 'numbers.json' +with open(filename) as f_obj: + numbers = json.load(f_obj) +print(numbers) +``` + +### 网络请求 + +Web API是网站的一部分,用于与使用非常具体的URL请求特定信息的程序交互。这种请求称为API调用。请求的数据将以易于处理的格式(如JSON或CSV)返回。依赖于外部数据源的大多数应用程序都依赖于API调用,如集成社交媒体网站的应用程序。 + + + +requests包让Python程序能够轻松地向网站请求信息以及检查返回的响应。要安装requests,请执行类似于下面的命令: + +$pip install --user requests +或者直接在ide中点击修复安装就可以: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/python_install_requests.png) + +```python +import requests +# 执行API调用并存储响应 +url = 'https://api.github.com/search/repositories?q=language:python&sort=stars' +r = requests.get(url) +print("Status code:",r.status_code) +# 将API响应存储在一个变量中 +response_dict = r.json() +# 处理结果 +print(response_dict.keys()) +``` + +执行结果为: +``` +Status code: 200 +dict_keys(['total_count', 'incomplete_results', 'items']) +``` + +这个API返回JSON格式的信息,因此我们使用方法json()将这些信息转换为一个Python字典。我们将转换得到的字典存储在response_dict中。 +最后,我们打印response_dict中的键。 + + + + +在Python中,还可以定义可变参数。可变参数也称不定长参数,即传入函数中的实际参数可以是零个、一个、两个到任意个。 + +Python中的可变参数的表现形式为,在形参前添加一个星号(*),意为函数传递过来的实参个数不定,可能为0个、1个,也可能为n个(n≥2)。需要注意的是,不管可变参数有多少个,在函数内部,它们都被“收集”起来并统一存放在以形参名为某个特定标识符的元组之中。因此,可变参数也被称为“收集参数”。 + + +除了用单个星号(*)表示可变参数,其实还有另一种标定可变参数的形式,即用两个星号(**)来标定。通过前文的介绍,我们知道,一个星号(*)将多个参数打包为一个元组,而两个星号(**)的作用是什么呢?它的作用就是把可变参数打包成字典模样。 + +这时调用函数则需要采用如“arg1=value1,arg2=value2”这样的形式。等号左边的参数好比字典中的键(key)[插图],等号右边的数值好比字典中的值(value)​, + +```python +def varFun(**x): + if len(x) == 0: + print("None") + else: + print(x) + + +varFun(a = 1, b = 3) + + +``` + +定义可变参数时,主要有两种形式,一种是*parameter,另一种是**parameter。下面分别进行介绍。 +1.*parameter +这种形式表示接收任意多个实际参数并将其放到一个元组中。 +2.**parameter +这种形式表示接收任意多个类似关键字参数一样显式赋值的实际参数,并将其放到一个字典中。 + + +除了用等号给可变关键字参数赋值,事实上,我们还可以直接用字典给可变关键字参数赋值: + +```python +def some_kwargs(name, age, sex): + print(f"name: ${name}") + +kdic = {'name' : 'Alice', 'age' : 11, 'sex' : 'nv'} +some_kwargs(**kdic) + +``` + + +在Python中,可以通过@property(装饰器)将一个方法转换为属性,从而实现用于计算的属性。将方法转换为属性后,可以直接通过方法名来访问方法,而不需要再添加一对小括号“()”,这样可以让代码更加简洁。 + + +#### lambda + +Python使用lambda来定义匿名函数,所谓匿名,指其不用def的标准形式定义函数。 + + +Lambda 表达式的基本语法是:`lambda 参数列表: 表达式`。 +关键字与参数:以 lambda关键字开头,后跟参数列表(如 x或 x, y),参数之间用逗号分隔。这些参数是函数的输入。 +表达式与返回值:冒号后面是一个单一的表达式。这个表达式会被求值,其结果就是 lambda 函数的返回值。注意,lambda 函数体内只能有一个表达式,不能包含复杂的语句或多行代码块。 +Lambda 函数是匿名的,也就是说,它们没有像用 def定义的函数那样的名称。它们的优势在于简洁,特别适合定义那些逻辑简单、可能只使用一次的短小函数。 + + +### var1 *= 2与var1 = var1 * 2的区别 + + +- var1 *= 2使用原地址 +- var1 = var1 * 2开辟了新的空间 +- 同样的对于类似,var1 += 2和var1 = var1 + 2也是同理 + +```python +def test(var1): + print("函数内var1 id:", id(var1)) + var1 *= 2 + print("var1 *= 2后函数内var1 id:", id(var1)) + var1 = var1 * 2 + print("var1 = var1 * 2后函数内var1 id:", id(var1)) + +函数内var1 id: 2302584035712 +var1 *= 2后,函数内var1 id: 2302584035712 +var1 = var1 * 2后,函数内var1 id: 2302584033664 +``` + + + + + +---- + + + + + + +计算机高级运行方式: + +- 编译型: c、c++ +- 解释型: python、JavaScript +- 先编译再解释: java + + +其实python也是先编译再解释,但是python的编译时机和java的不一样。 +- java是在代码解释执行前,先进行预编译,编译后生成字节码文件存在硬盘中。 +- python是在对代码进行逐行解释执行的时候进行的编译操作,默认生成的字节码文件存在内存中。 + + diff --git a/JavaKnowledge/shell.md b/JavaKnowledge/shell.md new file mode 100644 index 00000000..1c2fb339 --- /dev/null +++ b/JavaKnowledge/shell.md @@ -0,0 +1,101 @@ +shell +--- + + + + +### ls: + +ls x* y*: 过滤文件中x和y开头的文件 +ls -F: 区分文件还是目录 +ls -l: 显示详细信息 + +### touch: 创建或修改文件时间 + + +### cp: 复制文件 + +cp sourceFile destinationFile,但是如果源文件存在会直接被覆盖,也不会提醒, +如果想要提醒需要加上-i, 例如cp -i 1.txt 2.txt + + +### mv移动重新命名 + +用法和cp一致 + + +### rm删除 + +rm -i xxx: -i会询问是否真的要删除 +rm -f xxx: 强制删除,不会询问 + +注意 对于rm命令,-r选项和-R选项的效果是一样的,都可以递归地删除目录中的文件。shell命令很少会对相同的功能使用大小写不同的选项。 +一口气删除目录树的最终解决方案是使用rm -rf命令。该命令不声不响,能够直接删除指定目录及其所有内容。当然,这肯定是一个非常危险的命令,所以务必谨慎使用,并再三检查你要进行的操作是否符合预期。 + + +### mkdir 创建目录 + +mkdir命令的-p选项可以根据需要创建缺失的父目录。父目录是包含目录树中下一级目录的目录。 + +### rmdir 删除空目录 + +### file + +file命令是一个方便的小工具,能够探测文件的内部并判断文件类型: +$ file .bashrc +.bashrc: ASCII text + + +### cat 显示文本文件 + +cat fileName + +-n 加上行号 + +### more +cat的缺点是其开始运行后无法控制后续的操作,为了解决这个问题,有了more命令。 + +more命令会显示文件内容,但会在显示每页数据之后暂停下来。 + +### ps 显示当前用户进程 + +ps -ef显示系统中运行的所有进程 + +### top 实时监测进程 + +ps命令虽然在收集系统中运行进程的信息时非常有用,但也存在不足之处:只能显示某个特定时间点的信息。如果想观察那些被频繁换入和换出内存的进程,ps命令就不太方便了。这正是top命令的用武之地。与ps命令相似,top命令也可以显示进程信息,但采用的是实时方式。 + + + +### kill pid, 通过pid发送信号 + +### pkill pname: pkill命令可以使用程序名代替PID来终止进程。除此之外,pkill命令也允许使用通配符。 + + +### grep数据搜索 + +经常需要在大文件中搜索 + +### gzip压缩 + +gzip: 压缩 +gzcat: 查看压缩过的文本文件内容 +gunzip:解压 + +### tar归档 + +ar命令最开始是用于将文件写入磁带设备以作归档,但它也可以将输出写入文件,这种用法成了在Linux中归档数据的普遍做法。tar命令的格式如下 + +// 该命令创建了一个名为test.tar的归档文件,包含目录test和test2的内容。 +tar -cvf test.tar test/ test2/ + + +### 切换到root用户 + + +`sudo su -` + + - sudo : super user doing + - su : switch user + - `-` : root用户 + diff --git "a/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" "b/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" index 9dd6ce0a..8894decc 100644 --- "a/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" +++ "b/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" @@ -10,12 +10,10 @@ 这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。 -那么,在有了多级缓存之后,程序的执行就变成了: +那么,在有了多级缓存之后,程序的执行就变成了: 当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。 - - > 这就像一家创业公司,刚开始,创始人和员工之间工作关系其乐融融,但是随着创始人的能力和野心越来越大,逐渐和员工之间出现了差距,普通员工原来越跟不上CEO的脚步。老板的每一个命令,传到到基层员工之后,由于基层员工的理解能力、执行能力的欠缺,就会耗费很多时间。这也就无形中拖慢了整家公司的工作效率。 > > 之后,这家公司开始设立中层管理人员,管理人员直接归CEO领导,领导有什么指示,直接告诉管理人员,然后就可以去做自己的事情了。管理人员负责去协调底层员工的工作。因为管理人员是了解手下的人员以及自己负责的事情的。所以,大多数时候,公司的各种决策,通知等,CEO只要和管理人员之间沟通就够了。 @@ -28,15 +26,15 @@ > > 多核CPU就像一家公司是由多个合伙人共同创办的,那么,就需要给每个合伙人都设立一套供自己直接领导的高层管理人员,多个合伙人共享使用的是公司的底层员工。 > -> 还有的公司,不断壮大,开始差分出各个子公司。各个子公司就是多个CPU了,互相使用没有共用的资源。互不影响。 +> 还有的公司,不断壮大,开始拆分出各个子公司。各个子公司就是多个CPU了,互相使用没有共用的资源。互不影响。 随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。 -**单线程。**cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。 +**单线程**:cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。 -**单核CPU,多线程。**进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。 +**单核CPU,多线程**:进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。 -**多核CPU,多线程。**每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 +**多核CPU,多线程**:每个核都至少有一个L1缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的cache中保留一份共享内存的缓存。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 在CPU和主存之间增加缓存,在多线程场景下就可能存在**缓存一致性问题**,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。 @@ -50,7 +48,7 @@ ### 处理器优化和指令重排 -上面提到在在CPU和主存之间增加缓存,在多线程场景下会存在**缓存一致性问题**。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是**处理器优化**。 +上面提到在CPU和主存之间增加缓存,在多线程场景下会存在**缓存一致性问题**。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是**处理器优化**。 除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做**指令重排**。 @@ -76,7 +74,7 @@ 所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。 -**为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。**通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。 +**为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范**。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。 内存模型解决并发问题主要采用两种方式:**限制处理器优化**和**使用内存屏障**。 @@ -86,31 +84,20 @@ Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存(每个线程都分配有单独的处理器缓存,用这些处理器缓存去缓存一些数据,就可以不用再次访问主内存去获取相应的数据,这样就可以提高效率)。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。对于共享普通变量来说,约定了变量在工作内存中发生变化了之后,必须要回写到工作内存(**迟早要回写但并非马上回写**),*但对于volatile变量则要求工作内存中发生变化之后,必须马上回写到工作内存,而线程读取volatile变量的时候,必须马上到工作内存中去取最新值而不是读取本地工作内存的副本*,此规则保证了前面所说的“当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动”。 - - - - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_mm.png) 这里面提到的主内存和工作内存,读者可以简单的类比成计算机内存模型中的主存和缓存的概念。特别需要注意的是,主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分,无法直接类比。《深入理解Java虚拟机》中认为,如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。 **所以,再来总结下,JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。** - - ## volatile - - 作用:使变量在多个线程间可见(可见性),能够保证volatile变量的可见性,但不能保证volatile变量复合操作的原子性。 `Java`语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而在这个过程中,变量的新值对其他线程是不可见的.而且只当线程进入或者离开同步代码块时才与共享成员变量 -的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。也就是说每个线程都有一个自己的本地内存空间,在线程执行时,先把变量从主内存读取到线程自己的本地内存空间,然后再对该变量进行操作,当对该变量操作完后,在某个时间再把变量刷新回主内存。 - - +的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。也就是说每个线程都有一个自己的本地内存空间,在线程执行时,先把变量从主内存读取到线程自己的本地内存空间,然后再对该变量进行操作,当对该变量操作完后,在某个时间再把变量刷新回主内存。 - volatile如何实现内存可见性。深入来说:通过加入**内存屏障**和**禁止重排序优化**来实现的 - - 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令 - 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令 @@ -119,27 +106,20 @@ Java内存模型规定了所有的变量都存储在主内存中,每条线程 1. **更新主内存。** 2. **向CPU总线发送一个修改信号。** - - **这时监听CPU总线的处理器会收到这个修改信号后,如果发现修改的数据自己缓存了,就把自己缓存的数据失效掉。这样其它线程访问到这段缓存时知道缓存数据失效了,需要从主内存中获取。这样所有线程中的共享变量i就达到了一致性。** ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_mm_1.jpg) **所以volatile也可以看作线程间通信的一种廉价方式。** - - - ### volatile关键字的非原子性 所谓原子性,就是某系列的操作步骤要么全部执行,要么都不执行。 -比如,变量的自增操作 i++,分三个步骤: +比如,变量的自增操作 i++,分三个步骤: - 从内存中读取出变量 i 的值 - - 将i的值加1 - - 将加1后的值写回内存 这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行。 @@ -152,9 +132,7 @@ volatile修饰的变量并不保证对它的操作(自增)具有原子性。 **综上,仅靠volatile不能保证线程的安全性。(原子性)** - - -### volatile禁止指令重排序 +### volatile禁止指令重排序 代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化 @@ -170,9 +148,9 @@ volatile禁止指令重排序,典型的应用是单例模式中的双重检查 public class DoubleCheckSingleton { private volatile static DoubleCheckSingleton singleton; - + private DoubleCheckSingleton() {} - + public static DoubleCheckSingleton getInstance() { if(singleton == null) { synchronized (singleton) { @@ -183,7 +161,7 @@ public class DoubleCheckSingleton { } return singleton; } - + } ``` @@ -192,14 +170,14 @@ public class DoubleCheckSingleton { ```java memory = allocate(); //1.分配对象的内存空间 ctorInstance(memory); //2.初始化对象 -instance = memory; //3.设置instance指向刚分配的内存地址 +instance = memory; //3.设置instance指向刚分配的内存地址 ``` 上面3行伪代码中的2和3之间可能会发生重排序,排序后的执行顺序如下: ```java memory = allocate(); //1.分配对象的内存空间 -instance = memory; //3.设置instance指向刚分配的内存地址,此时对象还没有初始化,但instance == null 判断为false +instance = memory; //3.设置instance指向刚分配的内存地址,此时对象还没有初始化,但instance == null 判断为false ctorInstance(memory); //2.初始化对象 ``` @@ -207,6 +185,10 @@ ctorInstance(memory); //2.初始化对象 +那为何说它禁止指令重排序呢?从硬件架构上讲,指令重排序是指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。但并不是说指令任意重排,处理器必须能正确处理指令依赖情况保障程序能得出正确的执行结果。譬如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值减去3,这时指令1和指令2是有依赖的,它们之间的顺序不能重排——(A+10)*2与A*2+10显然不相等,但指令3可以重排到指令1、2之前或者中间,只要保证处理器执行后面依赖到A、B值的操作时能获取正确的A和B值即可。所以在同一个处理器中,重排序过的代码看起来依然是有序的。因此,lock addl$0x0,(%esp)指令把修改同步到内存时,意味着所有之前的操作都已经执行完成,这样便形成了“指令重排序无法越过内存屏障”的效果。 + + + ## synchronized **synchronized实现同步的基础是:Java中的每个对象都可作为锁。所以synchronized锁的都对象,只不过不同形式下锁的对象不一样。** @@ -222,14 +204,10 @@ ctorInstance(memory); //2.初始化对象 - 然而,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,另一个线程仍然可以访问该`object`中的非`synchronized(this)`同步代码块。 - 尤其关键的是,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,其他线程对`object`中所有其它`synchronized(this)`同步代码块的访问将被阻塞。 - - **在JVM规范中规定了synchronized是通过Monitor对象来实现方法和代码块的同步,但两者实现细节有点一不样。代码块同步是使用monitorenter和monitorexit指令,方法同步是使用另外一种方法实现,细节JVM规范并没有详细说明。但是,方法的同步同样可以使用这两指令来实现。** **monitorenter指令是编译后插入到同步代码块的开始位置,而monitorexit指令是插入到方法结束处和异常处。JVM保证了每个monitorenter都有对应的monitorexit。任何一个对象都有一个monitor与之关联,当且一个monitor被持有后,对象将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象对应monitor的所有权,即尝试获得对象的锁。** - - ### synchronized缺点 #### 有性能损耗 @@ -242,8 +220,6 @@ Monitor其实是一种同步工具,也可以说是一种同步机制,它通 > > 通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。 - - #### 产生阻塞 基于Monitor对象,当多个线程同时访问一段同步代码时,首先会进入Entry Set,当有一个线程获取到对象的锁之后,才能进行The Owner区域,其他线程还会继续在Entry Set等待。并且当某个线程调用了wait方法后,会释放锁并进入Wait Set等待。 @@ -252,20 +228,13 @@ Monitor其实是一种同步工具,也可以说是一种同步机制,它通 所以,synchronize实现的锁本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。 - - ## volatile与Synchronized的区别: - - - synchronized通过加锁的方式,使得其在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操作都能使用synchronized来完成。 - - volatile通过在volatile变量的操作前后插入内存屏障的方式,保证了变量在并发场景下的可见性和有序性。 - - volatile关键字是无法保证原子性的,而synchronized通过monitorenter和monitorexit两个指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,即可保证不会出现CPU时间片在多个线程间切换,即可保证原子性 - - volatile是变量修饰符,而synchronized则作用于一段代码或方法。 -- volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。 +- volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。 一句话,那什么时候才能用volatile关键字呢?(千万记住了,重要事情说三遍,感觉这句话过时了) @@ -277,23 +246,12 @@ Monitor其实是一种同步工具,也可以说是一种同步机制,它通 比如上面 count++ ,是获取-计算-写入三步操作,也就是依赖当前值的,所以不能靠volatile 解决问题。 - - - - -参考: +参考: - [再有人问你Java内存模型是什么,就把这篇文章发给他](https://www.hollischuang.com/archives/2550) - [原子性、可见性以及有序性](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%8E%9F%E5%AD%90%E6%80%A7%E3%80%81%E5%8F%AF%E8%A7%81%E6%80%A7%E4%BB%A5%E5%8F%8A%E6%9C%89%E5%BA%8F%E6%80%A7.md) - - --- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - - - - +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" "b/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" index e8c1277f..a010cb92 100644 --- "a/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" +++ "b/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" @@ -3,16 +3,11 @@ 有关代理模式已经动态代理和静态代理的区别请查看另一篇文章[设计模式](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) -刚毕业的时候在学习`android`时看到过[张孝祥老师的Java高新技术](http://yun.itheima.com/course/5.html),里面 -讲到了动态代理,当时看完后感觉懂了.但是现在全部都忘了。 -因为动态代理我们平时用的其实并不多,但是作为`Android`开发,你肯定知道`Retrofit`,而`Retrofit`就是基于动态代理实现。 - - +动态代理我们平时用的其实并不多,但是作为`Android`开发,你肯定知道`Retrofit`,而`Retrofit`就是基于动态代理实现。 动态代理的类和接口 --- - - `Proxy`:动态代理机制的主类,提供一组静态方法为一组接口动态的生成对象和代理类。 ```java @@ -32,7 +27,7 @@ public static Object newProxyInstance(ClassLoader loader, ``` -- `InvocationHandler`:调用处理器接口,自定义invokle方法,用于实现对于真正委托类的代理访问。 +- `InvocationHandler`:调用处理器接口,自定义invoke方法,用于实现对于真正委托类的代理访问。 ```java /** diff --git "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" index 53efd6b1..8609744e 100644 --- "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" +++ "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" @@ -87,7 +87,7 @@ - `rmdir` - 该命令的功能是删除空目录,一个目录被删除之前必须是空的。删除某目录时也必须具有对父目录的写权限。 + 该命令的功能是删除空目录,一个目录被删除之前必须是空的。删除某目录时也必须具有对父目录的写权限。由于只能删除空目录,一般都是使用rm -f - `mv` @@ -467,6 +467,7 @@ chmod [who] [+ | - | =] [mode] 文件名 ``` chmod 751 file ``` +如果是当前root用户执行,前面需要加 sudo chmod 751 file 性能监控和优化命令 @@ -511,9 +512,82 @@ ifconfig [网络设备] [参数] +- file命令是一个方便的小工具,能够探测文件的内部并判断文件类型: +$ file .bashrc +.bashrc: ASCII text + +- pkill命令可以使用程序名代替PID来终止进程,这就方便多了。除此之外,pkill命令也允许使用通配符,当系统出问题时,这是一个非常有用的工具: +# pkill http* +# +该命令将“杀死”所有名称以http起始的进程,比如Apahce Web Server的httpd服务。 +警告 以root身份使用pkill命令时要格外小心。命令中的通配符很容易意外地将系统的重要进程终止。这可能会导致文件系统损坏。 +### Shell脚本 + +Shell的作用就是解释执行用户的命令,用户输入一条命令,Shell就解释执行一条,这种方式称为交互式,Shell还有一种执行命令的方式称为批处理,用户事先写一个Shell脚本,其中有很多条命令,让Shell一次把这些命令执行完,而不是一条一条的敲命令。 +Shell脚本和编程语言很类似,也有变量和流程控制语句,但Shell脚本是解释执行的,不需要编译,Shell程序从脚本中一行一行读取并执行这些命令,相当于一个用户把脚本中的命令一行一行敲到Shell提示符下执行。 + + +```shell +#!/bin/bash +# This line is a comment +echo "Hello World" +``` +一个shell脚本永远以#!开头,这个是一个脚本开始的标记,它是告诉系统执行这个文件需要使用某个解释器,后面的/bin/bash指明了解释器的具体位置。 +第二行 # 这里表示是一个注解 +第三行是输出Hello World + +#### 运行脚本 +脚本的运行有好几种方式: +- 在该脚本所在的目录中直接bash这个脚本,直接bash一个文件就是指定了使用Bash Shell来解释脚本内容。 +- 给该脚本加上可执行权限,然后使用./来运行,它代表运行的是当前目录下的Hello World.sh脚本, 如果采用这种方式而脚本没有可执行权限则会报错。 + ```shell + sudu chmod 777 HelloWorld.sh + ./HelloWorld.sh + ``` +- 如果不想修改权限可以使用.来运行脚本 + ```shell + . ./HelloWorld.sh + ``` +###### 声明变量: declare、typeset +这两个命令都是用来声明变量的,作用完全相同。 +很多语法严谨的语言对变量的声明都是有严格要求的,变量的使用原则是必须在使用前声明、声明时必须说明变量类型,而shell脚本中对变量声明的要求并不高,因为shell弱化了变量的类概念,所有shell又称为弱类型编程语言,声明变量时并不需要指明类型。 + + +###### 函数 +```shell +function NAME() { + ..... + return x; +} +``` +function关键字可以省略。 + +###### 命令代换 + +```shell +DATE=`date` +echo $DATE +``` +由“`”反引号括起来的也是一条命令,Shell先执行该命令,然后将输出结果立刻代换到当前命令行中,也可以用$()表示: +```shell +DATE = $(date) +``` + + +####### 输入输出 +echo: 显示文本行或变量,或者把字符串输入到文件 + +###### tee +tee 命令把结果输出到标准输出,另一个副本输出到相应文件中。 + + + + + + ---- - 邮箱 :charon.chui@gmail.com diff --git "a/JavaKnowledge/\345\270\270\350\247\201\347\256\227\346\263\225.md" "b/JavaKnowledge/\345\270\270\350\247\201\347\256\227\346\263\225.md" deleted file mode 100644 index 81add14e..00000000 --- "a/JavaKnowledge/\345\270\270\350\247\201\347\256\227\346\263\225.md" +++ /dev/null @@ -1,117 +0,0 @@ -常见算法 -=== - -算法(Algorithm)是一系列解决问题的清晰指令,也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题, -执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。 -算法可以理解为有基本运算及规定的运算顺序所构成的完整的解题步骤。或者看成按照要求设计好的有限的确切的计算序列,并且这样的步骤和序列可以解决一类问题。 -```java -/** - * 1到100所有素数的和 - * 素数就是只能够被1和自身整除的数 - */ -public class dd { - public static void main(String[] args) { - int i = 2; // i 即为所求素数 - System.out.println("i= " + i); - for (i = 3; i <= 100; i = i + 2) { - boolean f = true; - Label: for (int j = 2; j < i; j++) { - if (i % j == 0) { // if(true)时,i为非素数 - f = false; - break Label; // 加了Label貌似只是起到提高效率 - } - } - if (f) {// 当f=true时,i为素数。 - System.out.println("i= " + i); - } - } - } -} - -/** - *古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死, - *问每个月的兔子总数为多少? - * - */ -public class aa{ - public static void main(String[] args) { - int i = 0; - for (i = 1; i < 20; i++) { - System.out.println(f(i)); - } - } - - public static int f(int x) { - if(x==1||x==2) { - return 1; - }else { - return f(x-1)+f(x-2); - } - } -} -``` - -3.有两个有序数组`a`,`b`,现需要将其合并成一个新的有序数组。 - -简单的思路就是先放到一个新的数组中,再排序。但是这样的没体现任何算法,这里考的不是快速排序等排序算法。关键应该是如何利用`有序` 已知这个条件。可以这样想,假设两个源数组的长度不一样,那么假设其中短的数组用完了,即全部放入到新数组中去了,那么长数组中剩下的那一段就可以直接拿来放入到新数组中去了。 - -其中用到的思想是:归并排序思想 - -```java -public static int[] merge(int[] a, int[] b) { - int lengthA = a.length; - int lengthB = b.length; - int[] result = new int[lengthA + lengthB]; - - //aIndex:用于标示a数组 bIndex:用来标示b数组 resultIndex:用来标示传入的数组 - int aIndex = 0, bIndex = 0, resultIndex = 0; - - while (aIndex < lengthA && bIndex < lengthB) { - if (a[aIndex] < b[bIndex]) { - result[resultIndex++] = a[aIndex]; - aIndex++; - } else { - result[resultIndex++] = b[bIndex]; - bIndex++; - } - } - - // a有剩余 - while (aIndex < lengthA) { - result[resultIndex++] = a[aIndex]; - aIndex++; - } - - // b有剩余 - while (bIndex < lengthB) { - result[resultIndex++] = b[bIndex]; - bIndex++; - } - return result; -} -```` - - - - - - - - - - - - - - - - - - - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git "a/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" "b/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" index 1f4346c1..c656ea7f 100644 --- "a/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" +++ "b/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" @@ -16,23 +16,20 @@ - 强引用(Strong Reference) - - 你懂的,不要胡乱持有着不放,不然内存泄露、oom有你好看,就像是老板(OOM)的亲儿子一样,在公司可以什么事都不干,但是千万不要老是占用公司的资源为他自己做事,记得用完公司的妹子之后,要让她们去工作(资源要懂得释放) 不然公司很可能会垮掉的。 + + 你懂的,不要胡乱持有着不放,不然内存泄露、oom有你好看,就像是老板(OOM)的亲儿子一样,在公司可以什么事都不干,但是千万不要老是占用公司的资源为他自己做事,记得用完公司的妹子之后,要让她们去工作(资源要懂得释放) 不然公司很可能会垮掉的。 平时我们编程的时候例如:`Object object=new Object()`;那`object`就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!记住是存活着,不可能是你new一个对象就永远不会被GC回收。 - + - 软引用(SoftReference) - - 描述一些还有用,但并非必需的对象。如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存,但是system.gc对其无效,有点像老板(OOM)的亲戚,在公司表现不好有可能会被开除,即使你投诉他(调用GC)上班看片,但是只要不被老板看到(被JVM检测到)就不会被开除(被虚拟机回收)。 - -**软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用, - 如果软引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个软引用加入到与之关联的引用队列中。 - + + 描述一些还有用,但并非必需的对象。如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存,但是system.gc对其无效,有点像老板(OOM)的亲戚,在公司表现不好有可能会被开除,即使你投诉他(调用GC)上班看片,但是只要不被老板看到(被JVM检测到)就不会被开除(被虚拟机回收)。**软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用,如果软引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个软引用加入到与之关联的引用队列中。 + - 弱引用(WeakReference) + + 同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。所以弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的声明周期。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 - 同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 - - 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了***只具***有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。 + 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了***只具***有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。 常见的一个例子就是WeakHashMap,在HashMap中,键被置为null,唤醒gc后,不会垃圾回收键为null的键值对。但是在WeakHashMap中,键被置为null,唤醒gc后,键为null的键值对会被回收。 @@ -60,21 +57,29 @@ ``` - 虚引用(PhantomReference) - + "虚引用"顾名思义,就是形同虚设,也成为幽灵引用或幻影引用,它是最弱的一种引用关系。就只是一个标识,对象的生命周期不受期影响,这货估计就是个临时工把,遇到事情的时候想到了你,没有事情的时候,秒秒钟拿出去顶锅,开除。 一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一的用处:能在对象被GC时收到系统通知,主要用于跟踪对象何时被回收,比如防止资源泄漏等。 - 虚引用必须和引用队列 `(ReferenceQueue)`联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null。 - - + 虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/reference_compare.jpg) +无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。在JDK 1.2版之前,Java里面的引用是很传统的定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。 + + +在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。 + + +- 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 +- 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。 +- 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。 +- 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。 --- diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204.md" deleted file mode 100644 index b3f671cc..00000000 --- "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ /dev/null @@ -1,310 +0,0 @@ -数据结构 -=== - -常见的数据结构: - -- 数组`(Array)` -- 栈`(Stack)` -- 队列`(Queue)` -- 链表`(LinkedList)` -- 树`(Tree)` -- 哈希表`(Hash)` -- 堆`(Heap)` -- 图`(Graph)` - - -数组`(Array)` ---- - -数组是一种大小固定的数据结构,对线性表的所有操作都可以通过数组来实现。数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。虽然数组一旦创建之后,它的大小就无法改变了,但是当数组不能再存储线性表中的新元素时,我们可以创建一个新的大的数组来替换当前数组。这样就可以使用数组实现动态的数据结构。 - -```java -int[] arr = new int[10]; -``` - -数组是最常用的数据结构了。这里就不说了。 - -优点: - -- 可以通过下标来访问或者修改元素,比较高效 - -缺点: - -- 增删慢,插入和删除的花费开销比较大,比如当在第一个位置前插入一个元素,那么首先要把所有的元素往后移动一个位置 -- 大小固定,只能存储单一元素, - - - -栈`(Stack)` ---- - - -> The Stack class represents a last-in-first-out (LIFO) stack of objects. It extends class Vector with five operations that allow a vector to be treated as a stack. The usual push and pop operations are provided, as well as a method to peek at the top item on the stack, a method to test for whether the stack is empty, and a method to search the stack for an item and discover how far it is from the top. -When a stack is first created, it contains no items. - -栈是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫作栈顶,数据称为压栈,移除数据称为弹栈(就像子弹弹夹装弹和取弹一样)。 -对栈的基本操作有`push`(进栈)和`pop`(出栈),前者相当于插入,后者相当于删除最后一个元素。栈有时又叫作`LIFO(Last In First Out)`表, -即后进先出。简单暴力的理解就是吃进去吐出来 - -优点: - -- 提供了先进后出的存取方式 - -缺点: - -- 存取其他项很慢 - - -队列`(Queue)` ---- - - -队列是一种特殊的线性表,特殊之处在于它只允许在表的前端`(front)`进行删除操作,而在表的后端`(rear)`进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。先进先出,简单暴力的理解就是吃进去拉出来 - - -优点: - -- 提供了先进先出的存取方式 - -缺点: - -- 存取其他项很慢 - - - - - - - -链表`(LinkedList)` ---- - -链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 -链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。 -每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,链表比较方便插入和删除操作。 - -用一组地址任意的存储单元存放线性表中的数据元素。以元素(数据元素的映象)+指针(指示后继元素存储位置) = 结点。 -以“结点的序列”表示线性表,称作线性链表(单链表)。单链表是一种顺序存取的结构,为找第`i`个数据元素,必须先找到第`i-1`个数据元素。 - -链表的结点结构: -``` - ┌──┬──┐──┐ - │data│next│ - └──┴──┘──┘ -``` -`data`域:存放结点值的数据域    -`next`域:存放结点的直接后继的地址(位置)的指针域(链域)。 - -注意: - -- 链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。    -- 每个结点只有一个链域的链表称为单链表`(Single Linked List)` - -所谓的链表就好像火车车厢一样,从火车头开始,每一节车厢之后都连着后一节车厢。 - -优点: - -- 和数组相比,链表的优势在于长度不受限制,也不需要连续的内存空间。 -- 在进行插入和删除操作时,不需要移动数据项,故尽管某些操作的时间复杂度与数组想同,实际效率上还是比数组要高很多,所以插入快,删除快 - -缺点: - -- 劣势在于随机访问,无法像数组那样直接通过下标找到特定的数据项 -- 查找慢 -- 相对数组只存储元素,链表的元素还要存储其他元素地址,内存开销相对增大 - - -树`(Tree)` ---- - - -树是由`n(n>=1)`个有限节点组成一个具有层次关系的集合。 -它具有以下特点:每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树。 - - - -#### 二叉树 - -二叉树`(binary tree)`是一棵树,每一个节点都不能有多于两个的子节点。 -通常子树被称作“左子树”和“右子树”。二叉树常被用于实现二叉查找树和二叉堆。 - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binary_tree.jpg?raw=true) - - -#### 满二叉树和完全二叉树 - -满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上。 - -完全二叉树:若设二叉树的深度为`h`,除第`h`层外,其它各层`(1~(h-1))`层的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。 - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/wanquan_binary_tree.jpg?raw=true) - - -完全二叉树是效率很高的数据结构,堆是一种完全二叉树或者近似完全二叉树,所以效率极高,像十分常用的排序算法、Dijkstra算法、Prim算法等都要用堆才能优化,二叉排序树的效率也要借助平衡性来提高,而平衡性基于完全二叉树。 - - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/man_binary_tree.png?raw=true) - - - -我们知道一颗基本的二叉排序树他们都需要满足一个基本性质:即树中的任何节点的值大于它的左子节点,且小于它的右子节点。 - -按照这个基本性质使得树的检索效率大大提高。我们知道在生成二叉排序树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右/左子树),这样势必会导致二叉树的检索效率大大降低`(O(n))`,所以为了维持二叉排序树的平衡,大牛们提出了各种平衡二叉树的实现算法,在平衡二叉搜索树中,其高度一般都良好地维持在`O(log2n)`,大大降低了操作的时间复杂度。如:`AVL`,`SBT`,伸展树,`TREAP` ,红黑树等等。 - - -#### 平衡二叉树 - -平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个子节点,其左右子树的高度都相近。下面给出平衡二叉树的示意图: - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/pingheng_binary_tree.jpg?raw=true) - - - -#### 红黑树 - -红黑树顾名思义就是结点是红色或者是黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由`Rudolf Bayer`发明的,他称之为"对称二叉B树",它现代的名字是在`Leo J. Guibas`和`Robert Sedgewick`于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在`O(log n)`时间内做查找,插入和删除,这里的`n`是树中元素的数目。 - -对于一棵有效的红黑树而言我们必须增加如下规则,这也是红黑树最重要的5点规则: - -- 每个结点都只能是红色或者黑色中的一种。 -- 根结点是黑色的。 -- 每个叶结点(NIL节点,空节点)是黑色的。 -- 如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。 -- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结 - -这些约束强制了红黑树的关键性质: 从根到叶子最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。 - - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hongheishu.jpg?raw=true) - -黑红树节点的`java`表示结构: - -```java -private static final boolean RED = true; -private static final boolean BLACK = false; -private Node root;//二叉查找树的根节点 - -//结点数据结构 -private class Node{ - private Key key;//键 - private Value value;//值 - private Node left, right;//指向子树的链接:左子树和右子树. - private int N;//以该节点为根的子树中的结点总数 - boolean color;//由其父结点指向它的链接的颜色也就是结点颜色. - - public Node(Key key, Value value, int N, boolean color) { - this.key = key; - this.value = value; - this.N = N; - this.color = color; - } -} - -/** - * 获取整个二叉查找树的大小 - * @return - */ -public int size(){ - return size(root); -} -/** - * 获取某一个结点为根结点的二叉查找树的大小 - * @param x - * @return - */ -private int size(Node x){ - if(x == null){ - return 0; - } else { - return x.N; - } -} -private boolean isRed(Node x){ - if(x == null){ - return false; - } - return x.color == RED; -} -``` - -哈希表`(Hash)` ---- - -哈希表就是一种以 键-值`(key-indexed)`存储数据的结构,我们只要输入待查找的值即`key`,即可查找到其对应的值。 - -优点: - -- 如果关键字已知则存取极快 -- 插入、查找、删除的时间级为`O(1)` -- 数据项占哈希表长的一半,或者三分之二时,哈希表的性能最好。 - -缺点: - -- 删除慢,如果不知道关键字存取慢,对存储空间使用不充分 -- 基于数组,数组创建后难于扩展,某些哈希表被基本填满时性能下降的非常严重; -- 没有一种简单的方法可以以任何一种顺序(如从小到大)遍历整个数据项; - -堆`(Heap)` ---- - -这里所说的堆是数据结构中的堆,而不是内存模型中的堆。堆通常是一个可以被看做一棵树,它满足下列性质: - -- 堆中任意节点的值总是不大于(不小于)其子节点的值; -- 堆是完全二叉树 -- 常常用数组实现 - - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/heap_1.png?raw=true) - -二叉堆是完全二元树或者是近似完全二元树,它分为两种:最大堆和最小堆。 -最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。 - - -优点: - -- 插入、删除快,对最大数据项存取快 - -缺点: - -- 对其他数据项存取慢 - - -图`(Graph)` ---- - - -图是一种较线性表和树更为复杂的数据结构,在线性表中,数据元素之间仅有线性关系,在树形结构中,数据元素之间有着明显的层次关系,而在图形结构中,节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。图的应用相当广泛,特别是近年来的迅速发展,已经渗入到诸如语言学、逻辑学、物理、化学、电讯工程、计算机科学以及数学的其他分支中。 - - -优点: - -- 对现实世界建模 - -缺点: - -- 有些算法慢且复杂 - - - - - - - - - - - - - - - - - - - - ---- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 00000000..b5d71dba --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,172 @@ +1. LeetCode_两数之和 +=== + +给定一个整数数组nums和一个整数目标值target,请你在该数组中找出和为目标值target的那两个整数,并返回它们的数组下标。 + +你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 + +你可以按任意顺序返回答案。 + + +--- + +示例 1 + +输入:nums = [2,7,11,15], target = 9 +输出:[0,1] +解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 + + +示例 2: + +输入:nums = [3,2,4], target = 6 +输出:[1,2] + +示例 3: + +输入:nums = [3,3], target = 6 +输出:[0,1] + + + +提示: + +只会存在一个有效答案 + + +进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗? + + +### 方法一:暴力枚举 + + +最简单的方法就是对数组中的每一个数x,都去遍历找后面是否存在target - x的值是否存在。 + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + int size = nums.length; + + for (int i = 0; i < size; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) { + return new int[]{i, j}; + } + } + } + return new int[0]; + } +} +``` + +```c++ +#include +using namespace std; + + +class Solution { +public: + vector twoSum(vector &nums, int target) { + int size = nums.size(); + + for (int i = 0; i < size; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) { + return {i, j}; + } + } + } + return {}; + } +}; +``` + +复杂度分析: + +- 时间复杂度:O(N²),其中N是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。 + +- 空间复杂度:O(1)。 + + +### 方法二: 哈希表 + +注意到方法一的时间复杂度较高的原因是寻找target - x的时间复杂度过高。 + +因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。 + +使用哈希表,可以将寻找target - x的时间复杂度降低到从O(N)降低到O(1)。 + + +--- + +如果想要快速确定某个元素是否在nums数组中,并且可以快速的获取所在下表index。 +我们的第一反应就是将数组维护成一个Map结构: + +- key: 存储数组里的值 +- value: 存储数组的下标index + +这样我们只需要通过target - nums[i]的值去Map中查找即可。 +但是这样存在一个问题,就是你需要先把数组中的值都放到Map中,需要多一步循环。 + + +--- + +其实我们可以先创建一个Map,在Map的初始化过程中什么元素都不放。 + +对于每一个x,我们首先查询哈希表中是否存在target-x,如果已存在就返回,如果不存在那再将x插入到哈希表中,即可保证不会让x和自己匹配。 + +这样只需要一次循环就可以了,而且Map数组不用提前初始化,在性能和内存占用率都比较低。 + + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + Map hashtable = new HashMap(); + for (int i = 0; i < nums.length; ++i) { + if (hashtable.containsKey(target - nums[i])) { + return new int[]{hashtable.get(target - nums[i]), i}; + } + hashtable.put(nums[i], i); + } + return new int[0]; + } +} + +``` + +```c++ +#include +#include + +using namespace std; + +class Solution { +public: + vector twoSum(vector &nums, int target) { + unordered_map map; + for (int i = 0; i < nums.size(); i ++) { + auto it = map.find(target - nums[i]); + if (it != map.end()) { + return {i, it->second}; + } + + map[nums[i]] = i; + } + return {}; + } +}; +``` + + +复杂度分析: + +- 时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。 + +- 空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" new file mode 100644 index 00000000..03581f19 --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" @@ -0,0 +1,210 @@ +2. LeetCode_两数相加 +=== + +给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。 + +请你将两个数相加,并以相同形式返回一个表示和的链表。 + +你可以假设除了数字0之外,这两个数都不会以0开头。 + + +输入:l1 = [2,4,3], l2 = [5,6,4] +输出:[7,0,8] +解释:342 + 465 = 807. + + + +输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] +输出:[8,9,9,9,0,0,0,1] + + +提示: + +- 每个链表中的节点数在范围[1, 100]内 +- 0 <= Node.val <= 9 +- 题目数据保证列表表示的数字不含前导零 + + + +想法: + +我的想法是先把两个链表转成两个整数相加,然后把这个整数转成字符串再次生成链表。 + +这种方案是不可行的,因为链表的长度可能会很长,整数是操作不了的,例如: + + +方法: + +- 由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。 + +- 我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。具体而言,如果当前两个链表处相应位置的数字为 n1,n2,进位值为 carry,则它们的和为 n1+n2+carry; +其中,答案链表处相应位置的数字为 (n1+n2+carry)mod10,而新的进位值为 [(n1+n2+carry) / 10] + +- 如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 0 。 + +- 此外,如果链表遍历结束后,有carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry。 + + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_2_twoaddsum.png?raw=true) + + +- 将链表反过来看,头结点在右侧 +- 横线上的数字为进位 +- 2 + 5 + 0(第一个进位默认为0) = 7 + + - 7 % 10得到新节点中的元素为7 + - 7 / 10得到下一个进位为0 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_2_twoaddsum_2.png?raw=true) + + +```java +class ListNode { + int val; + ListNode next; + + ListNode() { + this.val = 0; + this.next = null; + } + + ListNode(int val) { + this.val = val; + this.next = null; + } + + ListNode(int val, ListNode next) { + this.val = val; + this.next = next; + } +} + +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + ListNode head = null; + ListNode tail = null; + + int carry = 0; + + while(l1 != null || l2 != null) { + int n1 = l1 != null ? l1.val : 0; + int n2 = l2 != null ? l2.val : 0; + + int sum = n1 + n2 + carry; + + if (head == null) { + head = tail = new ListNode(sum % 10); + } else { + tail.next = new ListNode(sum % 10); + tail = tail.next; + } + + carry = sum / 10; + + if (l1 != null) { + l1 = l1.next; + } + + if (l2 != null) { + l2 = l2.next; + } + } + + if (carry > 0) { + tail.next = new ListNode(carry); + } + return head; + } +} +``` + + + +复杂度分析: + +- 时间复杂度: O(max(m,n)),其中m和n分别为两个链表的长度。我们要遍历两个链表的全部位置,而处理每个位置只需要O(1)的时间。 + +- 空间复杂度: O(1)。注意返回值不计入空间复杂度。 + + +### 改进: 递归 + +```java +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + return add(l1, l2, 0); + } +} + +public ListNode add(ListNode l1, ListNode l2, int carry) { + if (l1 == null && l2 == null && carry == 0) { + return null; + } + int val = carry; + if (l1 != null) { + val += l1.val; + l1 = l1.next; + } + if (l2 != null) { + val += l2.val; + l2 = l2.next; + } + ListNode node = new ListNode(val % 10); + node.next = add(l1, l2, val / 10); + return node; + } +} +``` + +使用C++实现: + +```c++ +struct ListNode { + int val; + ListNode *next; + + ListNode() : val(0), next(nullptr) { + } + + ListNode(int x) : val(x), next(nullptr) { + } + + ListNode(int x, ListNode *next) : val(x), next(next) { + } +}; + +class Solution { +public: + ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) { + int carry = 0; + + ListNode *answer = new ListNode(); + ListNode *head = answer; + + while (l1 || l2) { + int number = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + carry; + carry = number / 10; + number %= 10; + head->next = new ListNode(number); + head = head->next; + + l1 ? l1 = l1->next : 0; + l2 ? l2 = l2->next : 0; + } + if (carry) { + head->next = new ListNode(carry); + } + return answer->next; + } +}; +``` + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/3. LeetCode_\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/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/3. LeetCode_\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..748e420e --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/3. LeetCode_\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,190 @@ +3. LeetCode_无重复字符的最长子串 +=== + +给定一个字符串s,请你找出其中不含有重复字符的 最长子串 的长度。 + + +示例 1: + +输入: s = "abcabcbb" +输出: 3 +解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 +示例 2: + +输入: s = "bbbbb" +输出: 1 +解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 +示例 3: + +输入: s = "pwwkew" +输出: 3 +解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 + 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 + + +提示: + +0 <= s.length <= 5 * 104 +s 由英文字母、数字、符号和空格组成 + + + +方式一: + +暴力破解,通过多个循环以穷举的方式 找出答案。简单且易想到的方案,包括之前我们刷的两道算法题也可以使用暴力破解来解决,缺点就是虽然简单,但是效率非常低,并非是最优方案。 + + +具体思路: + +- 将字符串中每个字符作为一个循环,和子串的组合进行比较,从而获取最长字串长度。 + +如字符串abc,则第一轮比较为: 字符a和子串a、ab、abc比较,第二轮则为字符b和子串b、bc比较,以此类推,最后获取不重复的子串长度。 + +```java +public class Solution { + public static int lengthOfLongestSubstring(String s) { + int maxLength = 0; + + for (int i = 0; i < s.length(); i++) { + for (int j = i + 1; j < s.length(); j++) { + if (!judgeCharacterExist(i, j, s)) { + maxLength = Math.max(maxLength, j - i); + } + } + } + return maxLength; + } + + + public static boolean judgeCharacterExist(int start, int end, String param) { + HashSet hashSet = new HashSet<>(); + for (int i = start; i < end; i++) { + if (hashSet.contains(param.charAt(i))) { + return true; + } + hashSet.add(param.charAt(i)); + } + + return false; + } +} +``` + +该方法效率低,时间福再度为O(n³)。 + + +方法二: 滑动窗口法 + +滑动窗口: 是数组和字符串中一个抽象的概念,可分为滑动和窗口两个概念理解。 + +窗口:即表示一个范围,通常是字符串和数组从开始到结束两个索引范围中间包含的一系列元素集合。如字符串abcd,如果开始索引和结束索引分别为0、2的话,这个窗口包含的字符则为: abc。 + +滑动:它表示窗口的开始和结束索引是可以往某个方向移动的。 +如上面的例子开始索引和结束索引分别为0、2的话,当开始索引和结束索引都往右移动一位时,它们的索引值分别为1、3,这个窗口包含的字符为:bcd。 + +示例: + +- abcdef +- 字符串开始索引: 0 +- 字符串结束索引: 5 +- 开始和结束索引范围包含的字符(abcdef)就可以看作是一个窗口 + +思路: + +- 使用一个HashSet来实现滑动窗口,用来检查重复字符 +- 维护开始和结束两个索引,默认都是从0开始 +- 随着循环 向右移动结束索引 + + - 遇到不是重复的字符则放入窗口里 + - 遇到重复字符则向右移动开始索引 + +最终得到结果。 + +```java +public class Solution { + public static Integer lengthOfLongestSubstring(String s) { + int maxLength = 0; + int leftPoint = 0; + int rightPoint = 0; + + Set set = new HashSet<>(); + + while(leftPoint < s.length() && rightPoint < s.length()) { + if (!set.contains(s.charAt(rightPoint))) { + set.add(s.charAt(rightPoint++)); + maxLength = Math.max(maxLength, rightPoint - leftPoint); + } else { + set.remove(s.charAt(leftPoint++)); + } + } + return maxLength; + } +} +``` + +滑动窗口算法的时间复杂度为O(n),相比于暴力破解,它的效率大大提高了。 + +方式三: + +虽然方式使用了滑动窗口时间复杂度只有O(n),但是如果存在重复字符还需要移动开始索引,因此我们可以考虑借助之前其他算法谈到的“空间换时间”的想法,通过借助HashMap建立字符和索引映射,避免手动移动索引。 + + +```java +public class Solution { + public static Integer lengthOfLongestSubstring(String s) { + char[] charArry = s.toCharArray(); + + HashMap keyMap = new HashMap<>(); + int maxLength = 0; + + int leftPoint = 0; + for (int i = 0; i < charArry.length; i++) { + if (keyMap.containsKey(charArry[i])) { + // 存在重复数据则获取索引值最大的 + leftPoint = Math.max(leftPoint, keyMap.get(charArry[i])); + } + // 值重复就覆盖,不重复就添加 + keyMap.put(charArry[i], i + 1); + maxLength = Math.max(maxLength, i + 1 - leftPoint); + } + return maxLength; + } +} +``` + +时间复杂度为O(n),虽然和上一种方案的时间复杂度是一样的,但是效率还是有一定的提高(思考问题时要思考是否还有其他有优质的方案,培养发散思维)。 + + +方式四: + + +上面题目分析的时候就提到了要注意题目中提到的:【字符串英文字母、数字、符号和空格组成】,这些字符是可以使用ASCII表示(如字符a的ASCII值为97,想具体了解的可以百度下),那么我们就可以建立字符与ASCII的映射关系,从而实现重复字符的排除。 + + + +```java +public class Solution { + public static Integer lengthOfLongestSubstring(String s) { + int[] index = new int[128]; + int maxLength = 0; + int length = s.length(); + int i = 0; + for (int j = 0; j < length; j++) { + i = Math.max(index[s.charAt(j)], i); + index[s.charAt(j)] = j + 1; + maxLength = Math.max(maxLength, j + 1 - i); + } + return maxLength; + } +} +``` + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" similarity index 100% rename from "JavaKnowledge/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" rename to "JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" new file mode 100644 index 00000000..1a75ec60 --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -0,0 +1,443 @@ +数据结构 +=== + +结构,简单的理解就是关系,比如分子结构,就是说组成分子的原子之间的排列方式。严格点说,结构是指各个组成部分相互搭配和排列的方式。在现实世界中, +不同数据元素之间不是独立的,而是存在特定的关系,我们将这些关系称为结构。那数据结构是什么? + +数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。 + +在计算机中,数据元素并不是孤立、杂乱无序的,而是具有内在联系的数据集合。数据元素之间存在的一种或多种特定关系,也就是数据的组织形式。 + +按照视点的不同,我们把数据结构分为逻辑结构和物理结构。 + +数据结构(data structure)是组织和存储数据的方式,涵盖数据内容、数据之间关系和数据操作方法,它具有以下设计目标。 + +- 空间占用尽量少,以节省计算机内存。 +- 数据操作尽可能快速,涵盖数据访问、添加、删除、更新等。 +- 提供简洁的数据表示和逻辑信息,以便算法高效运行。 +- 数据结构设计是一个充满权衡的过程。如果想在某方面取得提升,往往需要在另一方面作出妥协。 + +下面举两个例子: + +- 链表相较于数组,在数据添加和删除操作上更加便捷,但牺牲了数据访问速度。 +- 图相较于链表,提供了更丰富的逻辑信息,但需要占用更大的内存空间。 + + + +#### 逻辑结构 + +逻辑结构:是指数据对象中数据元素之间的相互关系。其实这也是我们今后最需要关注的问题。逻辑结构分为以下四种: + +- 集合结构 + 集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。各个数据元素是“平等”的,它们的共同属性是"同属于一个集合"。 + 数据结构中的集合关系就类似于数学中的集合 +- 线性结构 + 线性结构:线性结构中的数据元素之间是一对一的关系 +- 树形结构 + 树形结构:树形结构中的数据元素之间存在一种一对多的层次关系 +- 图形结构 + 图形结构:图形结构的数据元素是多对多的关系 + +#### 物理结构 + +物理结构(也称为存储结构):是指数据的逻辑结构在计算机中的存储形式。 + +- 顺序存储结构 + 顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的 + 这种存储结构其实很简单,说白了,就是排队占位。大家都按顺序排好,每个人占一小段空间,大家谁也别插谁的队。数组就是这 + 样的顺序存储结构。当你告诉计算机,你要建立一个有9个整型数据的数组时,计算机就在内存中找了片空地,按照一个整型所占位置的大小乘以9,开辟一段 + 连续的空间,于是第一个数组数据就放在第一个位置,第二个数据放在第二个,这样依次摆放。 +- 链式存储结构 + 链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此 + 需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置 + +常见的数据结构: + +- 数组`(Array)` +- 栈`(Stack)` +- 队列`(Queue)` +- 链表`(LinkedList)` +- 树`(Tree)` +- 哈希表`(Hash)` +- 堆`(Heap)` +- 图`(Graph)` + + +## 线性表 + +零个或多个数据元素的有限序列。除第一个元素外,每一个元素有且只有一个直接前驱元素,除了最后一个元素外,每一个元素有且只有一个直接后继元素。 +数据元素之间的关系是一对一的关系。 + +- 序列 + 也就是说元素之间是有顺序的。 +- 有限 + 元素的个数是有限的。 + +线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1); +而插入或删除时,时间复杂度都是O(n)。 +这就说明,它比较适合元素个数不太变化,而更多是存取数据的应用。当然,它的优缺点还不止这些。 + +由于线性表的顺序存储结构在插入和删除的时候需要移动大量元素,为了解决这个问题,有了线性表的链式存储结构。 +又分为单链表和多链表。 + +### 数组`(Array)` + +数组是一种大小固定的数据结构,对线性表的所有操作都可以通过数组来实现。数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋, +有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。虽然数组一旦创建之后,它的大小就无法改变了,但是当数组不能再存储 +线性表中的新元素时,我们可以创建一个新的大的数组来替换当前数组。这样就可以使用数组实现动态的数据结构。 + +```java +int[] arr = new int[10]; +``` + +数组是最常用的数据结构了。这里就不说了。 + +优点: + +- 可以通过下标来访问或者修改元素,比较高效 + +缺点: + +- 增删慢,插入和删除的花费开销比较大,比如当在第一个位置前插入一个元素,那么首先要把所有的元素往后移动一个位置 +- 大小固定,只能存储单一元素 + +### 链表 + +链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 +链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。 +每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。相比于线性表顺序结构,链表比较方便插入和删除操作。 + +用一组地址任意的存储单元存放线性表中的数据元素。以元素(数据元素的映象)+指针(指示后继元素存储位置) = 结点。 +以“结点的序列”表示线性表,称作线性链表(单链表)。单链表是一种顺序存取的结构,为找第`i`个数据元素,必须先找到第`i-1`个数据元素。 + +链表的结点结构: +``` + ┌──┬──┐──┐ + │data│next│ + └──┴──┘──┘ +``` +`data`域:存放结点值的数据域 +`next`域:存放结点的直接后继的地址(位置)的指针域(链域)。 + +链表又分为很多种:静态链表、循环链表、单链表、双向链表 + +注意: + +- 链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。 +- 每个结点只有一个链域的链表称为单链表`(Single Linked List)` + +所谓的链表就好像火车车厢一样,从火车头开始,每一节车厢之后都连着后一节车厢。 + +单链表插入和删除算法,它们其实都是由两部分组成: +第一部分就是遍历查找第i个元素; +第二部分就是插入和删除元素。 +从整个算法来说,我们很容易推导出:它们的时间复杂度都是O(n)。 +如果在我们不知道第i个元素的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。 +但如果,我们希望从第i个位置,插入10个元素,对于顺序存储结构意味着,每一次插入都需要移动n-i个元素,每次都是O(n)。 +而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。 +显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。 + + +优点: + +- 和数组相比,链表的优势在于长度不受限制,也不需要连续的内存空间。 +- 在进行插入和删除操作时,不需要移动数据项,故尽管某些操作的时间复杂度与数组相同,实际效率上还是比数组要高很多,所以插入快,删除快 + +缺点: + +- 劣势在于随机访问,无法像数组那样直接通过下标找到特定的数据项 +- 查找慢 +- 相对数组只存储元素,链表的元素还要存储其他元素地址,内存开销相对增大 + +### 栈`(Stack)` + +栈是限定仅在表尾进行插入和删除操作的线性表。 +我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。 + +> The Stack class represents a last-in-first-out (LIFO) stack of objects. It extends class Vector with five operations +> that allow a vector to be treated as a stack. The usual push and pop operations are provided, as well as a method to +> peek at the top item on the stack, a method to test for whether the stack is empty, and a method to search the stack +> for an item and discover how far it is from the top. +> When a stack is first created, it contains no items. + +栈是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫作栈顶,数据称为压栈,移除数据称为弹栈(就像子弹弹夹装弹和取弹一样)。 +对栈的基本操作有`push`(进栈)和`pop`(出栈),前者相当于插入,后者相当于删除最后一个元素。栈有时又叫作`LIFO(Last In First Out)`表, +即后进先出。简单暴力的理解就是吃进去吐出来 + +优点: + +- 提供了先进后出的存取方式 + +缺点: + +- 存取其他项很慢 + + +### 链栈 + +栈的顺序存储结构,我们现在来看看栈的链式存储结构,简称为链栈。 +对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间,如果真的发生,那此时的计算机操作系统已经面临死机崩溃的情况,而不是这个链栈是 +否溢出的问题。 +对比一下顺序栈与链栈,它们在时间复杂度上是一样的,均为O(1)。对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题, +但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。所以它们的区别和线性表中讨论的 +一样,如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。 + + +#### 栈的应用-递归 + +把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。 + +当然,写递归程序最怕的就是陷入永不结束的无穷递归中,所以,每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。 + +递归(recursion)是一种算法策略,通过函数调用自身来解决问题。它主要包含两个阶段: + +- 递:程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。 +- 归:触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。 + +而从实现的角度看,递归代码主要包含三个要素: + +- 终止条件:用于决定什么时候由“递”转“归”。 +- 递归调用:对应“递”,函数调用自身,通常输入更小或更简化的参数。 +- 返回结果:对应“归”,将当前递归层级的结果返回至上一层。 + + +那么我们讲了这么多递归的内容,和栈有什么关系呢?这得从计算机系统的内部说起。前面我们已经看到递归是如何执行它的前行和退回阶段的。 +递归过程退回的顺序是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。这种存储某些数据, +并在后面又以存储的逆序恢复这些数据,以提供之后使用的需求,显然很符合栈这样的数据结构,因此,编译器使用栈实现递归就没什么好惊讶的了。 +简单的说,就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出, +用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。 + + +### 队列`(Queue)` + +队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。 +队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。 + +队列是一种特殊的线性表,特殊之处在于它只允许在表的前端`(front)`进行删除操作,而在表的后端`(rear)`进行插入操作,和栈一样,队列是一种操作受限制 +的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。先进先出,简单暴力的理解就是吃进去拉出来 + + +优点: + +- 提供了先进先出的存取方式 + +缺点: + +- 存取其他项很慢 + +### 链队列 +队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。 + + +### 串 +串(string)是由零个或多个字符组成的有限序列,又名叫字符串。 + + + +### 树`(Tree)` + +树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中: +- 有且仅有一个特定的称为根(Root)的结点; +- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。 + + +树是由`n(n>=1)`个有限节点组成一个具有层次关系的集合。 +它具有以下特点:每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树。 + +树中结点的最大层次称为树的深度(Depth)。如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。 + + +#### 二叉树 + +二叉树`(binary tree)`是一棵树,每一个节点都不能有多于两个的子节点。 +通常子树被称作“左子树”和“右子树”。二叉树常被用于实现二叉查找树和二叉堆。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binary_tree.jpg?raw=true) + + +二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/alg_ercha_list.png?raw=true) +其中data是数据域,lchild和rchild都是指针域,分别存放只想左孩子和右孩子的指针。 +如果有需要,还可以再增加一个指向其双亲的指针域,那样就称之为三叉链表 + +二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。 +##### 二叉树遍历方法 +- 前序遍历 + 规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。 +- 中序遍历 + 规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。 +- 后序遍历 + 规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。 +- 层序遍历 + 规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。 + + + +#### 满二叉树和完全二叉树 + +满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上。 + +完全二叉树:若设二叉树的深度为`h`,除第`h`层外,其它各层`(1~(h-1))`层的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/wanquan_binary_tree.jpg?raw=true) + + +完全二叉树是效率很高的数据结构,堆是一种完全二叉树或者近似完全二叉树,所以效率极高,像十分常用的排序算法、Dijkstra算法、Prim算法等都要用堆才 +能优化,二叉排序树的效率也要借助平衡性来提高,而平衡性基于完全二叉树。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/man_binary_tree.png?raw=true) + + + +我们知道一颗基本的二叉排序树他们都需要满足一个基本性质:即树中的任何节点的值大于它的左子节点,且小于它的右子节点。 + +按照这个基本性质使得树的检索效率大大提高。我们知道在生成二叉排序树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右/左子树), +这样势必会导致二叉树的检索效率大大降低`(O(n))`,所以为了维持二叉排序树的平衡,大牛们提出了各种平衡二叉树的实现算法,在平衡二叉搜索树中, +其高度一般都良好地维持在`O(log2n)`,大大降低了操作的时间复杂度。如:`AVL`,`SBT`,伸展树,`TREAP` ,红黑树等等。 + + +#### 平衡二叉树 + +平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一 +个子节点,其左右子树的高度都相近。下面给出平衡二叉树的示意图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/pingheng_binary_tree.jpg?raw=true) + + +#### 红黑树 + +红黑树顾名思义就是结点是红色或者是黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。红黑树是一种自平衡二叉查找树,是在计算机科学中用到的 +一种数据结构,典型的用途是实现关联数组。它是在1972年由`Rudolf Bayer`发明的,他称之为"对称二叉B树",它现代的名字是在`Leo J. Guibas`和 +`Robert Sedgewick`于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在`O(log n)` +时间内做查找,插入和删除,这里的`n`是树中元素的数目。 + +对于一棵有效的红黑树而言我们必须增加如下规则,这也是红黑树最重要的5点规则: + +- 每个结点都只能是红色或者黑色中的一种。 +- 根结点是黑色的。 +- 每个叶结点(NIL节点,空节点)是黑色的。 +- 如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。 +- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结 + +这些约束强制了红黑树的关键性质: 从根到叶子最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hongheishu.jpg?raw=true) + +黑红树节点的`java`表示结构: + +```java +private static final boolean RED = true; +private static final boolean BLACK = false; +private Node root;//二叉查找树的根节点 + +//结点数据结构 +private class Node{ + private Key key;//键 + private Value value;//值 + private Node left, right;//指向子树的链接:左子树和右子树. + private int N;//以该节点为根的子树中的结点总数 + boolean color;//由其父结点指向它的链接的颜色也就是结点颜色. + + public Node(Key key, Value value, int N, boolean color) { + this.key = key; + this.value = value; + this.N = N; + this.color = color; + } +} + +/** + * 获取整个二叉查找树的大小 + * @return + */ +public int size(){ + return size(root); +} +/** + * 获取某一个结点为根结点的二叉查找树的大小 + * @param x + * @return + */ +private int size(Node x){ + if(x == null){ + return 0; + } else { + return x.N; + } +} +private boolean isRed(Node x){ + if(x == null){ + return false; + } + return x.color == RED; +} +``` + +哈希表`(Hash)` +--- + +哈希表就是一种以 键-值`(key-indexed)`存储数据的结构,我们只要输入待查找的值即`key`,即可查找到其对应的值。 + +优点: + +- 如果关键字已知则存取极快 +- 插入、查找、删除的时间级为`O(1)` +- 数据项占哈希表长的一半,或者三分之二时,哈希表的性能最好。 + +缺点: + +- 删除慢,如果不知道关键字存取慢,对存储空间使用不充分 +- 基于数组,数组创建后难于扩展,某些哈希表被基本填满时性能下降的非常严重; +- 没有一种简单的方法可以以任何一种顺序(如从小到大)遍历整个数据项; + +堆`(Heap)` +--- + +这里所说的堆是数据结构中的堆,而不是内存模型中的堆。堆通常是一个可以被看做一棵树,它满足下列性质: + +- 堆中任意节点的值总是不大于(不小于)其子节点的值; +- 堆是完全二叉树 +- 常常用数组实现 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/heap_1.png?raw=true) + +二叉堆是完全二元树或者是近似完全二元树,它分为两种:最大堆和最小堆。 +最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。 + +优点: + +- 插入、删除快,对最大数据项存取快 + +缺点: + +- 对其他数据项存取慢 + + +图`(Graph)` +--- + +图是一种较线性表和树更为复杂的数据结构,在线性表中,数据元素之间仅有线性关系,在树形结构中,数据元素之间有着明显的层次关系,而在图形结构中, +节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。图的应用相当广泛,特别是近年来的迅速发展,已经渗入到诸如语言学、逻辑学、物理、 +化学、电讯工程、计算机科学以及数学的其他分支中。 +图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。 + + + +优点: + +- 对现实世界建模 + +缺点: + +- 有些算法慢且复杂 + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" new file mode 100644 index 00000000..b33352bd --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" @@ -0,0 +1,333 @@ +算法 +=== + + +算法(algorithm)是在有限时间内解决特定问题的一组指令或操作步骤,它具有以下特性: + +- 问题是明确的,包含清晰的输入和输出定义。 +- 具有可行性,能够在有限步骤、时间和内存空间下完成。 +- 各步骤都有确定的含义,在相同的输入和运行条件下,输出始终相同。 + + +算法具有五个基本特性: +- 输入 + 算法具有零个或多个输入 +- 输出 + 算法至少有一个或多个输出 +- 有穷性 + 指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。 +- 确定性 + 算法的每一步骤都具有确定的含义,不会出现二义性 +- 可行性 + 算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成 + + +在算法设计中,我们先后追求以下两个层面的目标。 + +- 找到问题解法:算法需要在规定的输入范围内可靠地求得问题的正确解。 +- 寻求最优解法:同一个问题可能存在多种解法,我们希望找到尽可能高效的算法。 + +也就是说,在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标,它包括以下两个维度: + +- 时间效率:算法运行时间的长短。 +- 空间效率:算法占用内存空间的大小。 + +简而言之,我们的目标是设计“既快又省”的数据结构与算法。而有效地评估算法效率至关重要,因为只有这样,我们才能将各种算法进行对比,进而指导算法设计与优化过程。 + + + + + +### 算法时间复杂度 + +一个算法的时间复杂度(Time Complexity)是指算法运行从开始到结束所需要的时间。这个时间就是该算法中每条语句的执行时间之和,而每条语句的执行时间是该语句执行次数(也称为频度)与执行该语句所需时间的乘积。但是,当算法转换为程序之后,一条语句执行一次所需的时间与机器的性能及编译程序生成目标代码的质量有关,是很难确定的。为此,假设执行每条语句所需的时间均为单位时间。在这一假设下,一个算法所花费的时间就等于算法中所有语句的频度之和。这样就可以脱离机器的硬、软件环境而独立地分析算法所消耗的时间。 + +在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。 + +这样用大写O( )来体现算法时间复杂度的记法,我们称之为大O记法。 +一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法。 +显然,由此算法时间复杂度的定义可知,我们的三个求和算法的时间复杂度分别为O(n),O(1),O(n²)。 +我们分别给它们取了非官方的名称,O(1)叫常数阶、O(n)叫线性阶、O(n²)叫平方阶。 + +- O(1) : 操作所需的时间是常数(n个元素所需的时间与一个元素所需的时间相同) +- O(n) : n个元素的时间是一个元素时间乘以n +- O(n²): n个元素的时间是一个元素时间乘以n² + +#### 推导大O阶方法 + +推导大O阶: +1. 用常数1取代运行时间中的所有加法常数。 +2. 在修改后的运行次数函数中,只保留最高阶项。 +3. 如果最高阶项存在且不是1,则去除与这个项相乘的常数。 +得到的结果就是大O阶。 + +##### 常数阶 + +顺序结构的时间复杂度。 + +下面这个算法,也就是刚才的第二种算法(高斯算法),为什么时间复杂度不是O(3),而是O(1)。 +``` +int sum = 0, n = 100; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +printf("%d", sum); // 执行一次 +``` +这个算法的运行次数函数是f(n)=3。 +根据我们推导大O阶的方法,第一步就是把常数项3改为1。 +在保留最高阶项时发现,它根本没有最高阶项,所以这个算法的时间复杂度为O(1)。 +那假设是这样: + +``` +int sum = 0, n = 100; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +printf("%d", sum); // 执行一次 +``` +事实上无论n为多少,上面的两段代码就是3次和12次执行的差异。这种与问题的大小无关(n的多少),执行时间恒定的算法,我们称之为具有O(1)的时间复杂度,又叫常数阶。注意:不管这个常数是多少,我们都记作O(1),而不能是O(3)、O(12)等其他任何数字,这是初学者常常犯的错误。 + + +##### 线性阶 + +循环结构的时间复杂度。 + +下面这段代码,它的循环的时间复杂度为O(n),因为循环体中的代码须要执行n次。 +```java +int i; +for (i = 0; i < n; i++) { + // 下面省略时间复杂度为O(1)的程序步骤 + ... +} + +``` +##### 对数阶 +下面的这段代码,时间复杂度是多少呢? +```java +int count = 1; +while (count < n) { + count = count * 2; + // 下面省略时间复杂度为O(1)的程序步骤 + ... +} +``` +由于每次count乘以2之后,就距离n更近了一分。 +也就是说,有多少个2相乘后大于n,则会退出循环。 +由2^x = n得到 x = ㏒(2)N。所以这个循环的时间复杂度为O(㏒n) + + +##### 平方阶 + +下面的例子是一个循环嵌套,它的内循环上面已经分析过,时间复杂度为O(n)。 +```java +int i, j; +for (i = 0; i < n; i ++ ) { + for (j = 0; j < n; j ++) { + // 下面省略时间复杂度为O(1)的程序步骤 + ... + } +} +``` + +而对于外层的循环,不过是内部这个时间复杂度为O(n)的语句,再循环n次。所以这段代码的时间复杂度为O(n2)。 + +如果外循环的循环次数改为了m,时间复杂度就变为O(m×n),如下: +```java +int i, j; +for (i = 0; i < m; i ++ ) { + for (j = 0; j < n; j ++) { + // 下面省略时间复杂度为O(1)的程序步骤 + ... + } +} +``` +所以我们可以总结得出,循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。那么下面这个循环嵌套,它的时间复杂度是多少呢? +```java +int i, j; +for(i = 0; i < n; i ++) { + for (j = i; j < n; j ++) { // 注意j = i 而不是0 + // 下面省略时间复杂度为O(1)的程序步骤 + ... + } +} +``` +由于当i=0时,内循环执行了n次,当i=1时,执行了n-1次,……当i=n-1时,执行了1次。所以总的执行次数为: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/alg_1.png?raw=true) +用我们推导大O阶的方法: +- 第一条,没有加法常数不予考虑 +- 第二条,只保留最高阶项,因此保留n2/2 +- 第三条,去除这个项相乘的常数,也就是去除1/2 +最终这段代码的时间复杂度为O(n2)。 + +##### 常见的时间复杂度 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/alg_2.png?raw=true) + + + +### 算法空间复杂度 + +我们在写代码时,完全可以用空间来换取时间,比如说,要判断某某年是不是闰年,你可能会花一点心思写了一个算法,而且由于是一个算法,也就意味着,每次给一个年份,都是要通过计算得到是否是闰年的结果。还有另一个办法就是,事先建立一个有2050个元素的数组(年数略比现实多一点),然后把所有的年份按下标的数字对应,如果是闰年,此数组项的值就是1,如果不是值为0。这样,所谓的判断某一年是否是闰年,就变成了查找这个数组的某一项的值是多少的问题。此时,我们的运算是最小化了,但是硬盘上或者内存中需要存储这2050个0和1。这是通过一笔空间上的开销来换取计算时间的小技巧。到底哪一个好,其实要看你用在什么地方。 + +算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。 + +一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1)。 + + +算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。算法的空间复杂度`S(n)`定义为该算法所耗费空间的数量级。 +`S(n)=O(f(n))`若算法执行时所需要的辅助空间相对于输入数据量`n`而言是一个常数,则称这个算法的辅助空间为`O(1)`; +递归算法的空间复杂度:递归深度`N*`每次递归所要的辅助空间,如果每次递归所需的辅助空间是常数,则递归的空间复杂度是`O(N)`. + +空间复杂度的分析方法: + +- 一个算法的空间复杂度`S(n)`定义为该算法所耗费的存储空间,它也是问题规模`n`的函数。空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。 +- 一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。 +- 一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小,它包括为参数表中形参变量分配的存储空间和为在函数体中定义的局部变量分配的存储空间两个部分。 + +  算法的空间复杂度一般也以数量级的形式给出。如当一个算法的空间复杂度为一个常量,即不随被处理数据量`n`的大小而改变时,可表示为`O(1)`; +  当一个算法的空间复杂度与以2为底的`n`的对数成正比时,可表示为`O(log2n)`; +  当一个算法的空间复杂度与`n`成线性比例关系时,可表示为`O(n)`。若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。 + +空间复杂度补充: + +一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。 +一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。 +程序执行时所需存储空间包括以下两部分:    + +- 固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。 +- 可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。一个算法所需的存储空间用`f(n)`表示。`S(n)=O(f(n))`其中`n`为问题的规模,`S(n)`表示空间复杂度。 + + +时间与空间复杂度比较 +--- + +对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差, +即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。 +另外,算法的所有性能之间都存在着或多或少的相互影响。因此,当设计一个算法(特别是大型算法)时,要综合考虑算法的各项性能,算法的使用频率,算法处理的数据量的大小,算法描述语言的特性,算法运行的机器系统环境等各方面因素,才能够设计出比较好的算法。 + + +通常,我们都使用“时间复杂度”来指运行时间的需求,使用“空间复杂度”指空间需求。 + +当不用限定词地使用“复杂度”时,通常都是指时间复杂度。 + + + + +## 顺序查找算法 + +顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。 + +算法(Algorithm)是一系列解决问题的清晰指令,也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题, +执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。 +算法可以理解为有基本运算及规定的运算顺序所构成的完整的解题步骤。或者看成按照要求设计好的有限的确切的计算序列,并且这样的步骤和序列可以解决一类问题。 +```java +/** + * 1到100所有素数的和 + * 素数就是只能够被1和自身整除的数 + */ +public class dd { + public static void main(String[] args) { + int i = 2; // i 即为所求素数 + System.out.println("i= " + i); + for (i = 3; i <= 100; i = i + 2) { + boolean f = true; + Label: for (int j = 2; j < i; j++) { + if (i % j == 0) { // if(true)时,i为非素数 + f = false; + break Label; // 加了Label貌似只是起到提高效率 + } + } + if (f) {// 当f=true时,i为素数。 + System.out.println("i= " + i); + } + } + } +} + +/** + *古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死, + *问每个月的兔子总数为多少? + * + */ +public class aa{ + public static void main(String[] args) { + int i = 0; + for (i = 1; i < 20; i++) { + System.out.println(f(i)); + } + } + + public static int f(int x) { + if(x==1||x==2) { + return 1; + }else { + return f(x-1)+f(x-2); + } + } +} +``` + +3.有两个有序数组`a`,`b`,现需要将其合并成一个新的有序数组。 + +简单的思路就是先放到一个新的数组中,再排序。但是这样的没体现任何算法,这里考的不是快速排序等排序算法。关键应该是如何利用`有序` 已知这个条件。可以这样想,假设两个源数组的长度不一样,那么假设其中短的数组用完了,即全部放入到新数组中去了,那么长数组中剩下的那一段就可以直接拿来放入到新数组中去了。 + +其中用到的思想是:归并排序思想 + +```java +public static int[] merge(int[] a, int[] b) { + int lengthA = a.length; + int lengthB = b.length; + int[] result = new int[lengthA + lengthB]; + + //aIndex:用于标示a数组 bIndex:用来标示b数组 resultIndex:用来标示传入的数组 + int aIndex = 0, bIndex = 0, resultIndex = 0; + + while (aIndex < lengthA && bIndex < lengthB) { + if (a[aIndex] < b[bIndex]) { + result[resultIndex++] = a[aIndex]; + aIndex++; + } else { + result[resultIndex++] = b[bIndex]; + bIndex++; + } + } + + // a有剩余 + while (aIndex < lengthA) { + result[resultIndex++] = a[aIndex]; + aIndex++; + } + + // b有剩余 + while (bIndex < lengthB) { + result[resultIndex++] = b[bIndex]; + bIndex++; + } + return result; +} +``` + + +数据结构与算法高度相关、紧密结合,具体表现在以下三个方面: + +- 数据结构是算法的基石。数据结构为算法提供了结构化存储的数据,以及操作数据的方法。 +- 算法是数据结构发挥作用的舞台。数据结构本身仅存储数据信息,结合算法才能解决特定问题。 +- 算法通常可以基于不同的数据结构实现,但执行效率可能相差很大,选择合适的数据结构是关键。 + + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/algorithm_datasturcture.jpg?raw=true) + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\347\256\227\346\263\225\347\232\204\345\244\215\346\235\202\345\272\246.md" "b/JavaKnowledge/\347\256\227\346\263\225\347\232\204\345\244\215\346\235\202\345\272\246.md" deleted file mode 100644 index 341bdff2..00000000 --- "a/JavaKnowledge/\347\256\227\346\263\225\347\232\204\345\244\215\346\235\202\345\272\246.md" +++ /dev/null @@ -1,179 +0,0 @@ -算法的复杂度 -=== - - -何为算法 ---- - -算法是对特定问题求解步骤的一种描述,是独立存在的一种解决问题的方法和思想。它是指令的有限序列,其中每一条指令表示一个或多个操作; - -此外,成为一个算法需要满足以下条件或特性: - -- 有穷性。一个算法必须总是在执行有穷步之后结束,且每一步都可在有穷时间内完成。 -- 确定性。算法中每一条指令必须有确切的含义读者理解时不会产生二义性。并且,在任何条件下,算法只有唯一的一条执行路径,即对于相同的输入只能得出相同的输出。 -- 可行性。一个算法是能行的,即算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现的。 -- 输入。零个或多个的输入。 -- 输出。一个或多个的输出。 - - -算法的设计要求 ---- - -通常设计一个好的算法应考虑达到以下目标: - -- 正确性:对于合法输入能够得到满足的结果;算法能够处理非法处理,并得到合理结果;算法对于边界数据和压力数据都能得到满足的结果。 -- 可读性。算法要方便阅读,理解和交流,只有自己能看得懂,其它人都看不懂,谈和好算法。 -- 健壮性。算法不应该产生莫名其妙的结果,一会儿正确,一会儿又是其它结果。 -- 高性价比,效率与低存储量需求。利用最少的时间和资源得到满足要求的结果,可以通过(时间复杂度和空间复杂度来判定) - - - -算法复杂度分为: - -- 时间复杂度:指执行算法所需要的计算工作量 -- 空间复杂度:指执行这个算法所需要的内存空间 - -简单来说,时间复杂度指的是语句执行次数,空间复杂度指的是算法所占的存储空间。 - - -时间复杂度 ---- - -什么是时间复杂度,算法中某个函数有n次基本操作重复执行,用T(n)表示,现在有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。通俗一点讲,其实所谓的时间复杂度,就是找了一个同样曲线类型的函数f(n)来表示这个算法的在n不断变大时的趋势 。当输入量n逐渐加大时,时间复杂性的极限情形称为算法的“渐近时间复杂性”。 - -- 时间频度 - - 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为`T(n)`。(算法中的基本操作一般指算法中最深层循环内的语句) - -- 时间复杂度 - - 在刚才提到的时间频度中,`n`称为问题的规模,当`n`不断变化时,时间频度`T(n)`也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度的概念。 - - - -在进行算法分析时,语句总的执行次数`T(n)`是关于问题规模`n`的函数,进而分析`T(n)`随`n`的变化情况并确定`T(n)`的数量级。 -算法的时间复杂度,也就是算法的时间量度,记作`T(n) = O(f(n))`。它表示随问题规模`n`的增大,算法执行时间的增长率和`f(n)`的增长率相同, -称为算法的渐近时间复杂度,简称为时间复杂度。其中`f(n)`是规模`n`的某个函数。 - - -大`O`表示法 ---- - -像前面用`O()`来体现算法时间复杂度的记法,我们称之为大O表示法。用`O()`来体现算法时间复杂度的记法,叫作大`O`记法。 - - -按数量级递增排列,常见的时间复杂度有: -常数阶`O(1)`,对数阶`O(log2n)`(以2为底`n`的对数,下同),线性阶`O(n)`, -线性对数阶`O(nlog2n)`,平方阶`O(n^2)`,立方阶`O(n^3)`,..., -`k`次方阶`O(n^k)`,指数阶`O(2^n)`。随着问题规模`n`的不断增大,上述时间复杂度不断增大,算法的执行效率越低。 - - -##### 推导大`O`阶方法 - -- 用常数1取代运行时间中的所有加法常数。 -- 在修改后的运行次数函数中,只保留最高阶项。 -- 如果最高阶项存在且不是1,则去除与这个项相乘的常数。 - - -##### 常数阶 - -`O(1)`的算法是一些运算次数为常数的算法。例如: -```java -temp=a;a=b;b=temp; -``` - -上面语句共三条操作,单条操作的频度为1,即使他有成千上万条操作,也只是个较大常数,这一类的时间复杂度为`O(1)`。 - -##### 线性阶 - -`O(n)`的算法是一些线性算法。例如: -```java -sum=0; -for(i=0;i实例。在您的Kotlin文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将DataStore保留为单例。 + +``` +// At the top level of your kotlin file +val Context.dataStore: DataStore by preferencesDataStore(name = "settings") +``` + +### 从Preferences DataStore读取内容 + +由于Preferences DataStore不使用预定义的架构,因此您必须使用相应的键类型函数为需要存储在DataStore实例中的每个值定义一个键。例如,如需为int 值定义一个键,请使用intPreferencesKey()](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/package-summary#intPreferencesKey(kotlin.String))。然后,使用[DataStore.data](https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore#data) 属性,通过Flow 提供适当的存储值。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +```kotlin +val EXAMPLE_COUNTER = intPreferencesKey("example_counter")val exampleCounterFlow: Flow = context.dataStore.data .map { preferences -> // No type safety. preferences[EXAMPLE_COUNTER] ?: 0} +``` + +### 将内容写入Preferences DataStore + +Preferences DataStore提供了一个[edit()](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/package-summary#edit) 函数,用于以事务方式更新DataStore中的数据。该函数的transform参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +suspend fun incrementCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0 settings[EXAMPLE_COUNTER] = currentCounterValue + 1 }} +``` + +## 使用Proto DataStore存储类型化的对象 + +既生Preference DataStore何生Proto DataStore,它们之间有什么区别? + +- Preference DataStore主要是为了解决SharedPreferences所带来的性能问题 +- Proto DataStore比Preference DataStore 更加灵活,支持更多的类型 + - Preference DataStore支持Int、Long、Boolean、Float、String + - protocol buffers支持的类型,Proto DataStore都支持 +- Preference DataStore以XML的形式存储key-value数据,可读性很好 +- Proto DataStore使用了二进制编码压缩,体积更小,速度比XML更快 + + + +## 序列化 + +序列化:将一个对象转换成可存储或可传输的状态,数据可能存储在本地或者在蓝牙、网络间进行传输。序列化大概分为对象序列化、数据序列化。 + +### 对象的序列化 + +**Java对象序列化** 将一个存储在内存中的对象转化为可传输的字节序列,便于在蓝牙、网络间进行传输或者存储在本地。把字节序列还原为存储在内存中的Java对象的过程称为**反序列化**。 + +在Android中可以通过Serializable和Parcelable两种方式实现对象序列化。 + +**Serializable** + +Serializable是Java原生序列化的方式,主要通过ObjectInputStream和ObjectOutputStream来实现对象序列化和反序列化,但是在整个过程中用到了大量的反射和临时变量,会频繁的触发GC,序列化的性能会非常差,但是实现方式非常简单,来看一下ObjectInputStream和ObjectOutputStream源码里有很多反射的地方。 + +``` +ObjectOutputStream.java +private void writeObject0(Object obj, boolean unshared) + throws IOException{ + ...... + Class cl = obj.getClass(); + ...... +} + +ObjectInputStream.java +void readFields() throws IOException { + ...... + ObjectStreamField[] fields = desc.getFields(false); + for (int i = 0; i < objVals.length; i++) { + objVals[i] = + readObject0(fields[numPrimFields + i].isUnshared()); + objHandles[i] = passHandle; + } + ...... +} +复制代码 +``` + +在Android中存在大量跨进程通信,由于Serializable性能差的原因,所以Android需要更加轻量且高效的对象序列化和反序列化机制,因此Parcelable出现了。 + +**Parcelable** + +Parcelable的出现解决了Android中跨进程通信性能差的问题,而且Parcelable比Serializable要快很多,因为写入和读取的时候都是采用自定义序列化存储的方式,通过writeToParcel()方法和describeContents()方法来实现,不需要使用反射来推断它,因此性能得到提升,但是使用起来比Serializable要复杂很多。 + +为了解决复杂性问题, AndroidStudio也有对应插件简化使用过程,如果是Java语言可以使用`android parcelable code generator` 插件, 如果Kotlin语言的话可以使用 @Parcelize注解,快速的实现Parcelable序列化。 + +用一张表格汇总一下Serializable和Parcelable的区别: + +![img](https:////p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e273a80bbe564bd684b3a68f39b114cf~tplv-k3u1fbpfcp-zoom-1.image) + +### 数据序列化 + +对象序列化记录了很多信息,包括Class信息、继承关系信息、变量信息等等,但是数据序列化相比于对象序列化就没有这么多沉余信息,数据序列化常用的方式有JSON、Protocol Buffers、FlatBuffers。 + +- JSON:是一种轻量级的数据交互格式,支持跨平台、跨语言,被广泛用在网络间传输,JSON的可读性很强,但是序列化和反序列化性能却是最差的,解析过程中,要产生大量的临时变量,会频繁的触发GC,为了保证可读性,并没有进行二进制压缩,当数据量很大的时候,性能上会差一点。 + +- Protocol Buffers:它是Google开源的跨语言编码协议,可以应用到C++、C#、Dart 、Go 、Java、Python等等语言,Google内部几乎所有RPC都在使用这个协议,使用了二进制编码压缩,体积更小,速度比JSON更快,但是缺点是牺牲了可读性 + + RPC指的是跨进程远程调用,即一个进程调用另外一个进程的方法。 + +- FlatBuffers:同Protocol Buffers一样是Google开源的跨平台数据序列化库,可以应用到C++、C#,Go、Java、JavaScript、PHP、Python 等等语言,空间和时间复杂度上比其他的方式都要好,在使用过程中,不需要额外的内存,几乎接近原始数据在内存中的大小,但是缺点是牺牲了可读性 + +最后我们用一张图来分析一下JSON、Protocol Buffers、FlatBuffers 它们序列化和反序列的性能,数据来源于[JSON vs Protocol Buffers vs FlatBuffers](https://codeburst.io/json-vs-protocol-buffers-vs-flatbuffers-a4247f8bda6f) + +![img](https:////p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef8b9fdbd95e463abf64bea9a2a39588~tplv-k3u1fbpfcp-zoom-1.image) + +FlatBuffers和Protocol Buffers无论是序列化还是反序列都完胜JSON,FlatBuffers 最初是Google为游戏或者其他对性能要求很高的应用开发的,接下来我们来看一下今天主角Protocol Buffer。 + +### Protocol Buffer + +Protocol Buffer(简称Protobuf) 它是Google开源的跨语言编码协议,可以应用到C++、C#、Dart、Go、Java、Python等等语言,Google内部几乎所有RPC都在使用这个协议,使用了二进制编码压缩,体积更小,速度比JSON更快。 + +> 从[Proto3.0.0 Release Note](https://github.com/protocolbuffers/protobuf/releases/tag/v3.0.0-alpha-1) 得知:protocol buffers最初开源时,它实现了Protocol Buffers语言版本2(称为 proto2), 这也是为什么版本数从v2.0.0开始,从v3.0.0开始, 引入新的语言版本(proto3),而旧的版本(proto2)继续被支持。所以到目前为止Protobuf共两个版本proto2和proto3。 + +**proto2和proto3应该学习那个版本?** + +proto3简化了proto2的语法,提高了开发的效率,因此也带来了版本不兼容的问题,因为2019年的时候才发布proto3稳定版本,所以在这之前使用Protocol Buffer的公司,大部分项目都是使用proto2的版本,从上文的源码分析部分可知,在DataStore中使用了proto2语法,所以proto2和proto3这两种语法都同时在使用。 + +对于初学者而言直接学习proto3语法就可以了,为了适应技术迭代的变化,当掌握proto3语法之后,可以顺带了解一下proto2语法以及proto3和proto2语法的区别,这样可以更好的理解其他的开源项目。 + +为了避免混淆proto3和proto2语法,在本文仅仅分析proto3语法,当我们了解完这些基本概念之后,我们开始分析如何在项目中使用Proto DataStore。 + + + + + +Proto DataStore实现使用DataStore和[协议缓冲区](https://developers.google.com/protocol-buffers)将类型化的对象保留在磁盘上。 + +### 定义架构 + +Proto DataStore 要求在 `app/src/main/proto/` 目录的 proto 文件中保存预定义的架构。此架构用于定义您在 Proto DataStore 中保存的对象的类型。如需详细了解如何定义 proto 架构,请参阅 [protobuf 语言指南](https://developers.google.com/protocol-buffers/docs/proto3)。 + +``` +syntax = "proto3"; option java_package = "com.example.application"; option java_multiple_files = true; message Settings { int32 example_counter = 1; } +``` + +**注意**:您的存储对象的类在编译时由 proto 文件中定义的 `message` 生成。请务必重新构建您的项目。 + +### 创建 Proto DataStore + +创建 Proto DataStore 来存储类型化对象涉及两个步骤: + +1. 定义一个实现 `Serializer` 的类,其中 `T` 是 proto 文件中定义的类型。此序列化器类会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。 +2. 使用由 `dataStore` 创建的属性委托来创建 `DataStore` 的实例,其中 `T` 是在 proto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。`filename` 参数会告知 DataStore 使用哪个文件存储数据,而 `serializer` 参数会告知 DataStore 第 1 步中定义的序列化器类的名称。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +object SettingsSerializer : Serializer { override val defaultValue: Settings = Settings.getDefaultInstance() override suspend fun readFrom(input: InputStream): Settings { try { return Settings.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } override suspend fun writeTo( t: Settings, output: OutputStream) = t.writeTo(output)}val Context.settingsDataStore: DataStore by dataStore( fileName = "settings.pb", serializer = SettingsSerializer) +``` + +### 从 Proto DataStore 读取内容 + +使用 `DataStore.data` 显示所存储对象中相应属性的 `Flow`。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +val exampleCounterFlow: Flow = context.settingsDataStore.data .map { settings -> // The exampleCounter property is generated from the proto schema. settings.exampleCounter } +``` + +### 将内容写入 Proto DataStore + +Proto DataStore 提供了一个 [`updateData()`](https://developer.android.com/reference/kotlin/androidx/datastore/DataStore#updatedata) 函数,用于以事务方式更新存储的对象。`updateData()` 为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setExampleCounter(currentSettings.exampleCounter + 1) .build() }} +``` + +## 在同步代码中使用 DataStore + +**注意**:请尽可能避免在 DataStore 数据读取时阻塞线程。阻塞界面线程可能会导致 [ANR](https://developer.android.com/topic/performance/vitals/anr) 或界面卡顿,而阻塞其他线程可能会导致[死锁](https://en.wikipedia.org/wiki/Deadlock)。 + +DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。如果您使用的现有代码库采用同步磁盘 I/O,或者您的依赖项不提供异步 API,就可能出现这种情况。 + +Kotlin 协程提供 [`runBlocking()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) 协程构建器,以帮助消除同步与异步代码之间的差异。您可以使用 `runBlocking()` 从 DataStore 同步读取数据。RxJava 在 `Flowable` 上提供阻塞方法。以下代码会阻塞发起调用的线程,直到 DataStore 返回数据: + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +val exampleData = runBlocking { context.dataStore.data.first() } +``` + +对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题: + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { context.dataStore.data.first() // You should also handle IOExceptions here. }} +``` + +这样,DataStore 可以异步读取数据并将其缓存在内存中。以后使用 `runBlocking()` 进行同步读取的速度可能会更快,或者如果初始读取已经完成,可能也可以完全避免磁盘 I/O 操作。 + + + + + + + + + + + +https://developer.android.com/topic/libraries/architecture/datastore#typed-datastore + +https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html + + +https://blog.csdn.net/zzw0221/article/details/109274610 + +SharedPreferences 有着许多缺陷: 看起来可以在 UI 线程安全调用的同步 API 其实并不安全、没有提示错误的机制、缺少事务 API 等等。DataStore 是 SharedPreferences 的替代方案,它解决了 Shared Preferences 的绝大部分问题。DataStore 包含使用 Kotlin 协程和 Flow 实现的完全异步 API,可以处理数据迁移、保证数据一致性,并且可以处理数据损坏。 + +https://blog.csdn.net/weixin_42324979/article/details/112650189?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control + + + +缺点: + +- 目前Jetpack Security没有支持DataStore,所以不能像SharedPreference一样支持加密 +- 不能安全的进行IPC,这点相对于SharedPreferences没有提升,有较强IPC需求的话首选MMKV +- 使用PB进行序列化时需要额外定义IDL,这会产生一定工作量 + + + + + + + +最后用一张表格来对比一下 MMKV、DataStore、SharedPreferences 的不同之处,如果发现错误,或者有其他不同之处,期待你来一起完善。 + +![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8903ca5079314ff7a03b33f60922c1bf~tplv-k3u1fbpfcp-zoom-1.image) + + + +目前为止,MMKV 功能比 DataStore 强大,毕竟 MMKV 已经经历好几个年头了, DataStore 才只是 alpha01 版本,但是 DataStore 作为 Jetpack 的成员,背靠 Google 的支持,加上全球的开发者一起来完善,从未来角度 DataStore 功能会逐渐完善。DataStore 的版本还是挺优势的,例如:类型安全检查,DataStore 编译的时候,就会告诉开发者类型不对,相比于运行时错误,编译时错误提示更好 + + + + + + + +- [上一篇:9.App Startup简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/9.App%20Startup%E7%AE%80%E4%BB%8B.md) +- [下一篇:11.Hilt简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/11.Hilt%E7%AE%80%E4%BB%8B.md) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" "b/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" new file mode 100644 index 00000000..2d3164db --- /dev/null +++ "b/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" @@ -0,0 +1,327 @@ +# 11.Hilt简介 + +## 依赖注入 + + + +## Dagger + +说到依赖注入,做Android的人都会想到一个库:Dagger。 + +说到Dagger,大家想到的都是牛逼、高端,又难学又难用。很多使用者用着用着就会掉进了自己亲手用Dagger搭建的迷宫中,越陷越深。 + +## Hilt + +所以Android团队在Jetpack中增加了Hilt,它是一个专门针对Android平台的依赖注入库。它并不是提供了依赖注入的能力,而是提供了一种依赖注入的简单实现方式。Hilt 是在 Dagger 基础上进行开发的,减少了在项目中进行手动依赖,Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,同时 Hilt 也继承了 Dagger 优点,编译时正确性、运行时性能、并且得到了 Android Studio 的支持。 + +Hilt做的优化包括: +1. 无需编写大量的Component代码 +2. Scope也会与Component自动绑定 +3. 预定义绑定,例如Application和Activity +4. 预定义的限定符,例如@ApplicationContext和@ActivityContext + + +## Koin + +Koin - a smart Kotlin injection library to keep you focused on your app, not on your tools + +[Koin](https://insert-koin.io/)是为Kotlin开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射。 + + + + + +Hilt、Dagger、Koin等等都是依赖注入库,Google也在努力不断的完善依赖注入库从Dagger到Dagger2在到现在的Hilt,因为依赖注入是面向对象设计中最好的架构模式之一,使用依赖注入库有以下优点: + +- 依赖注入库会自动释放不再使用的对象,减少资源的过度使用。 +- 在配置scopes范围内,可重用依赖项和创建的实例,提高代码的可重用性,减少了很多模板代码。 +- 代码变得更具可读性。 +- 易于构建对象。 +- 编写低耦合代码,更容易测试。 + + + +## 添加依赖项 + +首先,将 `hilt-android-gradle-plugin` 插件添加到项目的根级 `build.gradle` 文件中: + +```groovy +buildscript { + ... + dependencies { + ... + classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha' + } +} +``` + +然后,应用 Gradle 插件并在 `app/build.gradle` 文件中添加以下依赖项: + +```groovy +... +apply plugin: 'kotlin-kapt' +apply plugin: 'dagger.hilt.android.plugin' + +android { + ... +} + +dependencies { + implementation "com.google.dagger:hilt-android:2.28-alpha" + kapt "com.google.dagger:hilt-android-compiler:2.28-alpha" +} +``` + +**注意**:同时使用Hilt和[数据绑定](https://developer.android.com/topic/libraries/data-binding)的项目需要Android Studio 4.0或更高版本。 + +Hilt使用 [Java 8 功能](https://developer.android.com/studio/write/java8-support)。如需在项目中启用Java 8,请将以下代码添加到`app/build.gradle`文件中: + +```groovy +android { + ... + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} +``` + + + + + +## Hilt常用注解的含义 + +Hilt常用注解包含@HiltAndroidApp、@AndroidEntryPoint、@Inject、@Module、@InstallIn、@Provides、@EntryPoint等等。 + +### @HiltAndroidApp + +1. 所有使用Hilt的App必须包含一个使用@HiltAndroidApp注解的Application。它会替代Dagger中的AppComponent。 +2. @HiltAndroidApp注解将会触发Hilt代码的生成,作为应用程序依赖项容器的基类。 +3. 生成的Hilt组件依附于Application的生命周期,它也是App的父组件,提供其他组件访问的依赖。 +4. 在Application中设置好@HiltAndroidApp之后,就可以使用Hilt提供的组件了,组件包含Application、Activity、Fragment、View、Service、BroadcastReceiver 等等。 + +### @AndroidEntryPoint + +Hilt提供的@AndroidEntryPoint注解用于提供Android类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)。 + +- Activity:仅仅支持ComponentActivity的子类例如FragmentActivity、AppCompatActivity等等。 +- Fragment:仅仅支持继承androidx.Fragment的Fragment +- View +- Service +- BroadcastReceiver + +如果您使用@AndroidEntryPoint为某个Android类添加注释,则还必须为依赖于该类的Android类添加注释。例如,如果您为某个Fragment添加注释,则还必须为使用该Fragment的所有Activity添加注释。 + +### @Inject + +Hilt需要知道如何从相应的组件中提供必要依赖的实例。使用@Inject注解来告诉Hilt如何提供该类的实例,它常用于构造函数、非私有字段、方法中。 + +**注意:在构建时,Hilt为Android类生成Dagger组件。然后Dagger遍历您的代码并执行以下步骤:** + +- 构建并验证依赖关系,确保没有未满足的依赖关系。 +- 生成它在运行时用于创建实际对象及其依赖项的类。 + +```kotlin +@AndroidEntryPoint +class ExampleActivity : AppCompatActivity() { + + @Inject lateinit var analytics: AnalyticsAdapter + ... +} +``` + +**注意**:由Hilt注入的字段不能为私有字段。尝试使用Hilt注入私有字段会导致编译错误。 + +### @Module + +有时,类型不能通过构造函数注入。发生这种情况可能有多种原因。例如,您不能通过构造函数注入接口。此外,您也不能通过构造函数注入不归您所有的类型,如来自外部库的类。在这些情况下,您可以使用Hilt模块向Hilt提供绑定信息。 + +Hilt模块是一个带有@Module注释的类。与[Dagger 模块](https://developer.android.com/training/dependency-injection/dagger-android#dagger-modules)一样,它会告知Hilt如何提供某些类型的实例。与Dagger 模块不同的是,您必须使用@InstallIn为Hilt模块添加注释,以告知Hilt每个模块将用在或安装在哪个Android类中。 + +常用于创建依赖类的对象(例如第三方库 OkHttp、Retrofit等等),使用@Module注解的类,需要使用@InstallIn注解指定module的范围。 + +```kotlin +@Module +@InstallIn(ApplicationComponent::class) +// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。 +object NetworkModule { +} +``` + +### @InstallIn + +使用@Module注入的类,需要使用@InstallIn注解指定module的范围,例如使用 @InstallIn(ActivityComponent::class) 注解的module会绑定到activity的生命周期上。 + +Hilt提供了以下组件来绑定依赖与对应的Android类的活动范围。 + +| Hilt 提供的组件 | 对应的 Android 类的活动范围 | +| ------------------------- | ----------------------------------------- | +| ApplicationComponent | Application | +| ActivityRetainedComponent | ViewModel | +| ActivityComponent | Activity | +| FragmentComponent | Fragment | +| ViewComponent | View | +| ViewWithFragmentComponent | View annotated with @WithFragmentBindings | +| ServiceComponent | Service | + +**注意:Hilt没有为broadcast receivers提供组件,因为Hilt直接从ApplicationComponent注入broadcast receivers。** + +Hilt会根据相应的Android类生命周期自动创建和销毁生成的组件类的实例,它们的对应关系如下表格所示。 + +| Hilt 提供的组件 | 创建对应的生命周期 | 销毁对应的生命周期 | +| ------------------------- | ---------------------- | ----------------------- | +| ApplicationComponent | Application#onCreate() | Application#onDestroy() | +| ActivityRetainedComponent | Activity#onCreate() | Activity#onDestroy() | +| ActivityComponent | Activity#onCreate() | Activity#onDestroy() | +| FragmentComponent | Fragment#onAttach() | Fragment#onDestroy() | +| ViewComponent | View#super() | View destroyed | +| ViewWithFragmentComponent | View#super() | View destroyed | +| ServiceComponent | Service#onCreate() | Service#onDestroy() | + +### @Provides + +它常用于被@Module注解标记类的内部的方法,并提供依赖项对象。 + +```kotlin +@Module +@InstallIn(ApplicationComponent::class) +// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。 +object NetworkModule { + + /** + * @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。 + * @Singleton 提供单例 + */ + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder() + .build() + } +} +``` + +### @EntryPoint + +Hilt支持最常见的Android类Application、Activity、Fragment、View、Service、BroadcastReceiver等等,但是您可能需要在Hilt不支持的类中执行依赖注入,在这种情况下可以使用@EntryPoint注解进行创建,Hilt会提供相应的依赖。 + + + +## 如何使用 Hilt 进行依赖注入 + +我们先来看一个简单的例子,注入HiltSimple并在Application中调用它的doSomething方法。 + +```kotlin +class HiltSimple @Inject constructor() { + fun doSomething() { + Log.e(TAG, "----doSomething----") + } +} + +@HiltAndroidApp +class HiltApplication : Application() { + @Inject + lateinit var mHiltSimple: HiltSimple + + override fun onCreate() { + super.onCreate() + mHiltSimple.doSomething() + } +} +``` + +Hilt需要知道如何从相应的组件中提供必要依赖的实例。使用@Inject注解来告诉Hilt如何提供该类的实例,@Inject常用于构造函数、非私有字段、方法中。 + +## Hilt vs Koin + +我们总共从以下几个方面对Hilt和Koin进行全方面的分析: + +- AndroidStudio支持Hilt在关联代码间进行导航,支持在@Inject修饰的构造器、@Binds或者@Provides修饰的方法、限定符之间进行跳转。 + +- 项目结构:完成Hilt的依赖注入需要的文件往往多于Koin。 + +- 代码行数:使用 [Statistic](https://plugins.jetbrains.com/plugin/4509-statistic) 工具来进行代码统计,反复对比了项目编译前和编译后,Hilt生成的代码多于Koin,随着项目越来越复杂,生成的代码量会越来越多。 + + | 代码行数 | Hilt | Koin | + | -------- | ------ | ------ | + | 编译之前 | 2414 | 2414 | + | 编译之后 | 149608 | 138405 | + +- 编译时间:Hilt编译时间总是大于Koin,这个结果告诉我们,如果是在一个非常大型的项目,这个代价是非常昂贵。 + + ``` + Hilt: + BUILD SUCCESSFUL in 35s + 27 actionable tasks: 27 executed + + Koin: + BUILD SUCCESSFUL in 18s + 27 actionable tasks: 27 executed + ``` + +- 使用上对比:Hilt使用起来要比Koin麻烦很多,其入门门槛高于Koin,在阅读Hilt文档的时候花了好几天时间才消化,而Koin只需要花很短的时间,依赖注入部分的代码Hilt多于Koin,在一个更大更复杂的项目中所需要的代码也更多,也越来越复杂。 + + | 依赖注入框架 | Hilt | Koin | + | ------------ | ---- | ---- | + | 代码行数 | 122 | 42 | + +**为什么Hilt编译时间总是大于Koin?** + +因为在Koin中不需要使用注解,也不需要kapt,这意味着没有额外的代码生成,所有的代码都是Kotlin原始代码,所以说 Hilt编译时间总是大于Koin,从这个角度上同时也解释了,为什么会说Koin仅使用功能解析,无额外代码生成。 + +**为什么Koin不需要用到反射?** + +因为Koin基于kotlin基础上进行开发的,使用了kotlin强大的语法糖(例如 Inline、Reified 等等)和函数式编程,来看一个简单的例子。 + +```kotlin +inline fun Module.viewModel( + qualifier: Qualifier? = null, + override: Boolean = false, + noinline definition: Definition +): BeanDefinition { + val beanDefinition = factory(qualifier, override, definition) + beanDefinition.setIsViewModel() + return beanDefinition +} +``` + +内联函数支持具体化的类型参数,使用reified修饰符来限定类型参数,可以在函数内部访问它,由于函数是内联的,所以不需要反射。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hilt_koin.png?raw=true) + +If you need **compile-time** generating library – use **Hilt** over Dagger, due to simplifications provided. Also consider the best documentation and plenty of examples from the box and in the internet. + +In case of **run-time** generation – use **Koin** over Kodein because it is simpler for using, has better support with docs and examples. + + + +# 参考 + + + +- [Why would I use Android Hilt (Dagger2) when Koin is available](https://stackoverflow.com/questions/64824349/why-would-i-use-android-hilt-dagger2-when-koin-is-available) +- [Android 中的依赖项注入](https://developer.android.com/training/dependency-injection) +- [How Dagger, Hilt and Koin differ under the hood?](https://proandroiddev.com/how-dagger-hilt-and-koin-differ-under-the-hood-c3be1a2959d7) +- [Finally, a loveable dependency injection for Android: Hilt & Koin](https://medium.com/@genc.tasbasi/finally-a-loveable-dependency-injection-for-android-hilt-koin-6cdfc08a6401) +- [Koin — Dependency Injection for Android](https://medium.com/@genc.tasbasi/koin-dependency-injection-for-android-ab521da7e6f8) +- [**Differences and Advantages of Hilt, Dagger2, Koin — 1**](https://medium.com/huawei-developers/differences-and-advantages-of-hilt-dagger2-koin-1-3af44e9a0722) +- [Comparing Three Dependency Injection Solutions](https://androidessence.com/comparing-three-dependency-injection-solutions) +- [Hilt, Koin, Kodein](https://smartbright.blog/?p=86) +- [Change DI Library from Koin to Dagger-Hilt](https://proandroiddev.com/change-di-library-from-koin-to-dagger-hilt-53a4fb3e8dd0) +- [Jetpack新成员,一篇文章带你玩转Hilt和依赖注入](https://blog.csdn.net/guolin_blog/article/details/109787732) + + + + + + + +- [上一篇:10.DataStore简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/10.DataStore%E7%AE%80%E4%BB%8B.md) +- [下一篇:12.Navigation简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/12.Navigation%E7%AE%80%E4%BB%8B.md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! ` diff --git "a/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" "b/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" new file mode 100644 index 00000000..38c42a53 --- /dev/null +++ "b/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" @@ -0,0 +1,509 @@ +# 12.Navigation简介 + +单个Activity嵌套多个Fragment的UI架构模式,已经被大多数Android工程师所接受和采用。但是,对Fragment的管理一直是一件比较麻烦的事情。工程师需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。页面的切换通常还包括对应用程序App bar的管理、Fragment间的切换动画,以及Fragment间的参数传递。纯代码的方式使用起来不是特别友好,并且Fragment和App bar在管理和使用的过程中显得很混乱。 + +为此,Jetpack提供了一个名为Navigation的组件,旨在方便我们管理页面和App bar。 + + +Navigation 是一个框架,用于在 Android 应用中的“目的地”之间导航,该框架提供一致的 API,无论目的地是作为 fragment、activity 还是其他组件实现。 + + + +它具有以下优势: + +- 可视化的页面导航图,类似于Apple Xcode中的StoryBoard,便于我们理清页面间的关系。 +- 通过destination和action完成页面间的导航。 +- 方便添加页面切换动画。 +- 页面间类型安全的参数传递。 +- 通过NavigationUI类,对菜单、底部导航、抽屉菜单导航进行统一的管理。 +- 支持深层链接DeepLink。 + +Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 `NavHostFragment`。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。 + + +在使用过程中,我们感受到如下的优点。 + +- 页面跳转性能更好,在单 Activity 的架构下,都是 fragment 的切换,每次 fragment 被压栈之后,View 被销毁,相比之前 Activity 跳转,更加轻量,需要的内存更少。 +- 通过 Viewmodel 进行数据共享更便捷,不需要页面之间来回传数据。 +- 统一的 Navigation API 来更精细的控制跳转逻辑。 + + + + +## 依赖 + +如果想要使用Navigation,需要现在build.gradle文件中添加以下依赖: + +``` +dependencies { + def nav_version = "2.3.5" + + // Java language implementation + implementation "androidx.navigation:navigation-fragment:$nav_version" + implementation "androidx.navigation:navigation-ui:$nav_version" + + // Kotlin + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + + // Feature module Support + implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" + + // Testing Navigation + androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" + + // Jetpack Compose Integration + implementation "androidx.navigation:navigation-compose:1.0.0-alpha10" +} + +``` + + + + + +## Navigation的主要元素 + +导航组件由以下三个关键部分组成: + +1. Navigation Graph : 图表 + + 一种数据结果,用于定义应用中的所有导航目的地以及它们如何连接在一起。 + 在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为*目标*)以及用户可以通过应用获取的可能路径。 + + +2. NavHost : 主机 + + 显示导航图中目标的空白容器。导航组件包含一个默认NavHost实现 (NavHostFragment),可显示Fragment目标。 + +3. NavController : 控制器 + + 在NavHost中管理应用导航的对象。当用户在整个应用中移动时,NavController会安排NavHost中目标内容的交换。 + 该控制器提供了一些方法,可用于在目的地之间导航、处理深层链接、管理返回堆栈等。 + +在应用中导航时,您告诉NavController,您想沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController便会在NavHost中显示相应目标。 + + + +## Navigation Graph(导航图) + +导航图是一种资源文件,其中包含您的所有目的地和操作。该图表会显示应用的所有导航路径。 + + + +如需向项目添加导航图,请执行以下操作: + +1. 在“Project”窗口中,右键点击 `res` 目录,然后依次选择 **New > Android Resource File**。此时系统会显示 **New Resource File** 对话框。 +2. 在 **File name** 字段中输入名称,例如“nav_graph”。 +3. 从 **Resource type** 下拉列表中选择 **Navigation**,然后点击 **OK**。 + +``` + + + +``` + +`` 元素是导航图的根元素。当您向图表添加目的地和连接操作时,可以看到相应的 `` 和 `` 元素在此处显示为子元素。如果您有[嵌套图表](https://developer.android.com/guide/navigation/navigation-nested-graphs),它们将显示为子 `` 元素。 + +## 向 Activity 添加 NavHost + +导航宿主是 Navigation 组件的核心部分之一。导航宿主是一个空容器,用户在您的应用中导航时,目的地会在该容器中交换进出。 + +导航宿主必须派生于 [`NavHost`](https://developer.android.com/reference/androidx/navigation/NavHost)。Navigation 组件的默认 `NavHost` 实现 ([`NavHostFragment`](https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment)) 负责处理 Fragment 目的地的交换。 + + + +### 通过 XML 添加 NavHostFragment + +以下 XML 示例显示了作为应用主 Activity 一部分的 `NavHostFragment`: + +```xml + + + + + + + + + + +``` + +NavHostFragment是一个特殊的Fragment,我们需要将其添加到Activity的布局文件中,作为其他Fragment的容器。 + +请注意以下几点: + +- `android:name` 属性包含 `NavHost` 实现的类名称。 +- `app:navGraph` 属性将 `NavHostFragment` 与导航图相关联。导航图会在此 `NavHostFragment` 中指定用户可以导航到的所有目的地。 +- `app:defaultNavHost="true"` 属性确保您的 `NavHostFragment` 会自动处理系统返回键,即当用户按下手机的返回按钮时,系统能自动将当前所展示的Fragment退出。请注意,只能有一个默认 `NavHost`。如果同一布局(例如,双窗格布局)中有多个宿主,请务必仅指定一个默认 `NavHost`。 + + + +```xml + + // 起始fragment + + +``` + +### Action + +```xml + + + + + + + + +``` + +在导航图中,操作由 `` 元素表示。操作至少应包含自己的 ID 和用户应转到的目的地的 ID。 + +## 导航到目的地 + +导航到目的地是使用 [`NavController`](https://developer.android.com/reference/androidx/navigation/NavController) 完成的,它是一个在 `NavHost` 中管理应用导航的对象。每个 `NavHost` 均有自己的相应 `NavController`。您可以使用以下方法之一检索 `NavController`: + +**Kotlin**: + +- [`Fragment.findNavController()`](https://developer.android.com/reference/kotlin/androidx/navigation/fragment/package-summary#findnavcontroller) +- [`View.findNavController()`](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#(android.view.View).findNavController()) +- [`Activity.findNavController(viewId: Int)`](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#findnavcontroller) + +**Java**: + +- [`NavHostFragment.findNavController(Fragment)`](https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment#findNavController(android.support.v4.app.Fragment)) +- [`Navigation.findNavController(Activity, @IdRes int viewId)`](https://developer.android.com/reference/androidx/navigation/Navigation#findNavController(android.app.Activity, int)) +- [`Navigation.findNavController(View)`](https://developer.android.com/reference/androidx/navigation/Navigation#findNavController(android.view.View)) + +使用 `FragmentContainerView` 创建 `NavHostFragment`,或通过 `FragmentTransaction` 手动将 `NavHostFragment` 添加到您的 Activity 时,尝试通过 `Navigation.findNavController(Activity, @IdRes int)` 检索 Activity 的 `onCreate()` 中的 `NavController` 将失败。您应改为直接从 `NavHostFragment` 检索 `NavController`。 + +```kotlin +val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment +val navController = navHostFragment.navController +navController.navigate(R.id.action_blankFragment_to_blankFragment2) +``` + +对于按钮,您还可以使用 [`Navigation`](https://developer.android.com/reference/androidx/navigation/Navigation) 类的 [`createNavigateOnClickListener()`](https://developer.android.com/reference/androidx/navigation/Navigation#createNavigateOnClickListener(int)) 便捷方法导航到目的地,如下例所示: + +```kotlin +button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_fragment, null)) + +``` + + + +## 使用 DeepLinkRequest 导航 + +您可以使用 [`navigate(NavDeepLinkRequest)`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(androidx.navigation.NavDeepLinkRequest)) 直接导航到[隐式深层链接目的地](https://developer.android.com/guide/navigation/navigation-deep-link#implicit),如下例所示: + +```kotlin +val request = NavDeepLinkRequest.Builder + .fromUri("android-app://androidx.navigation.app/profile".toUri()) + .build() +findNavController().navigate(request) + +``` + +## 导航和返回堆栈 + +Android 会维护一个[返回堆栈](https://developer.android.com/guide/components/activities/tasks-and-back-stack),其中包含您之前访问过的目的地。当用户打开您的应用时,应用的第一个目的地就放置在堆栈中。每次调用 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 方法都会将另一目的地放置到堆栈的顶部。点按**向上**或**返回**会分别调用 [`NavController.navigateUp()`](https://developer.android.com/reference/androidx/navigation/NavController#navigateUp()) 和 [`NavController.popBackStack()`](https://developer.android.com/reference/androidx/navigation/NavController#popBackStack()) 方法,用于移除(或弹出)堆栈顶部的目的地。 + +`NavController.popBackStack()` 会返回一个布尔值,表明它是否已成功返回到另一个目的地。当返回 `false` 时,最常见的情况是手动弹出图的起始目的地。 + +如果该方法返回 `false`,则 `NavController.getCurrentDestination()` 会返回 `null`。您应负责导航到新目的地,或通过对 Activity 调用 `finish()` 来处理弹出情况,如下例所示: + +```kotlin + +if (!navController.popBackStack()) { + // Call finish() on your Activity + finish() +} + +``` + +## popUpTo 和 popUpToInclusive + +使用操作进行导航时,您可以选择从返回堆栈上弹出其他目的地。例如,如果您的应用具有初始登录流程,那么在用户登录后,您应将所有与登录相关的目的地从返回堆栈上弹出,这样返回按钮就不会将用户带回登录流程。 + +如需在从一个目的地导航到另一个目的地时弹出目的地,请在关联的 `` 元素中添加 `app:popUpTo` 属性。`app:popUpTo` 会告知 Navigation 库在调用 `navigate()` 的过程中从返回堆栈上弹出一些目的地。属性值是应保留在堆栈中的最新目的地的 ID。 + +您还可以添加 `app:popUpToInclusive="true"`,以表明在 `app:popUpTo` 中指定的目的地也应从返回堆栈中移除。 + +## 通过 引用其他导航图 + +在导航图中,您可以使用 `include` 引用其他图。虽然这在功能上与使用嵌套图相同,但 `include` 可让您使用其他项目模块或库项目中的图,如以下示例所示: + +```xml + + + + + + + + + + + ... + +``` + +```xml + + + + + + +``` + + + +## 创建全局操作 + +您可以使用全局操作来创建可由多个目的地共用的通用操作。例如,您可能想要不同目的地中的多个按钮导航到同一应用主屏幕。 + +```xml + + + + ... + + + + +``` + +如需在代码中使用某个全局操作,请将该全局操作的资源 ID 传递到每个界面元素的 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 方法,如以下示例所示: + +```kotlin +viewTransactionButton.setOnClickListener { view -> + view.findNavController().navigate(R.id.action_global_mainFragment) +} +``` + +## 使用 Safe Args 实现类型安全的导航 + +如需在目的地之间导航,建议使用 Safe Args Gradle 插件。此插件可生成简单的对象和构建器类,以便在目的地之间实现类型安全的导航。我们强烈建议您在导航以及[在目的地之间传递数据](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args)时使用 Safe Args。 + + + +如需将 [Safe Args](https://developer.android.com/topic/libraries/architecture/navigation/navigation-pass-data#Safe-args) 添加到您的项目中,请在顶层 `build.gradle` 文件中包含以下 `classpath`: + +```xml +buildscript { + repositories { + google() + } + dependencies { + def nav_version = "2.3.5" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + } +} +``` + +您还必须应用以下两个可用插件之一。 + +如需生成适用于 Java 模块或 Java 和 Kotlin 混合模块的 Java 语言代码,请将以下行添加到**应用或模块**的 `build.gradle` 文件中: + +`apply plugin: "androidx.navigation.safeargs"` + +此外,如需生成适用于 Kotlin 独有的模块的 Kotlin 代码,请添加以下行: + +`apply plugin: "androidx.navigation.safeargs.kotlin"` + +根据[迁移到 AndroidX](https://developer.android.com/jetpack/androidx/migrate#migrate)) 文档,您的 [`gradle.properties` 文件](https://developer.android.com/studio/build#properties-files)中必须具有 `android.useAndroidX=true`。 + +启用 Safe Args 后,生成的代码会包含已定义的每个操作的类和方法,以及与每个发送目的地和接收目的地相对应的类。 + +Safe Args 为生成操作的每个目的地生成一个类。生成的类名称会在源目的地类名称的基础上添加“Directions”。例如,如果源目的地的名称为 `SpecifyAmountFragment`,则生成的类的名称为 `SpecifyAmountFragmentDirections`。 + +生成的类为源目的地中定义的每个操作提供了一个静态方法。该方法接受任何定义的[操作参数](https://developer.android.com/guide/navigation/navigation-pass-data)为参数,并返回可直接传递到 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController.html?skip_cache=true#navigate(androidx.navigation.NavDirections)) 的 [`NavDirections`](https://developer.android.com/reference/androidx/navigation/NavDirections.html?skip_cache=true) 对象。 + + + +### Safe Args 示例 + +例如,假设我们的导航图包含一个操作,该操作将两个目的地 `SpecifyAmountFragment` 和 `ConfirmationFragment` 连接起来。`ConfirmationFragment` 接受您作为操作的一部分提供的单个 `float` 参数。 + +Safe Args 会生成一个 `SpecifyAmountFragmentDirections` 类,其中只包含一个 `actionSpecifyAmountFragmentToConfirmationFragment()` 方法和一个名为 `ActionSpecifyAmountFragmentToConfirmationFragment` 的内部类。这个内部类派生自 `NavDirections` 并存储了关联的操作 ID 和 `float` 参数。然后,您可以将返回的 `NavDirections` 对象直接传递到 `navigate()`,如下例所示: + +```kotlin +override fun onClick(v: View) { + val amount: Float = ... + val action = + SpecifyAmountFragmentDirections + .actionSpecifyAmountFragmentToConfirmationFragment(amount) + v.findNavController().navigate(action) +} +``` + +## 传递参数 + +Navigation 支持您通过定义目的地参数将数据附加到导航操作。例如,用户个人资料目的地可能会根据用户 ID 参数来确定要显示哪个用户。 + +通常情况下,强烈建议您仅在目的地之间传递最少量的数据。例如,您应该传递键来检索对象而不是传递对象本身,因为在 Android 上用于保存所有状态的总空间是有限的。如果您需要传递大量数据,不妨考虑使用 [`ViewModel`](https://developer.android.com/reference/androidx/lifecycle/ViewModel)(如[在 Fragment 之间共享数据](https://developer.android.com/topic/libraries/architecture/viewmodel#sharing)中所述)。 + +```xml + + + +``` + +通过声明argement节点来指定参数。 + +启用 Safe Args 后,生成的代码会为每个操作包含以下类型安全的类和方法,以及每个发送和接收目的地。 + +- 为生成操作的每一个目的地创建一个类。该类的名称是在源目的地的名称后面加上“Directions”。例如,如果源目的地是名为 `SpecifyAmountFragment` 的 Fragment,则生成的类的名称为 `SpecifyAmountFragmentDirections`。 + + 该类会为源目的地中定义的每个操作提供一个方法。 + +- 对于用于传递参数的每个操作,都会创建一个 inner 类,该类的名称根据操作的名称确定。例如,如果操作名称为 `confirmationAction,`,则类名称为 `ConfirmationAction`。如果您的操作包含不带 `defaultValue` 的参数,则您可以使用关联的 action 类来设置参数值。 + +- 为接收目的地创建一个类。该类的名称是在目的地的名称后面加上“Args”。例如,如果目的地 Fragment 的名称为 `ConfirmationFragment,`,则生成的类的名称为 `ConfirmationFragmentArgs`。可以使用该类的 `fromBundle()` 方法检索参数。 + +``` +override fun onClick(v: View) { val amountTv: EditText = view!!.findViewById(R.id.editTextAmount) val amount = amountTv.text.toString().toInt() val action = SpecifyAmountFragmentDirections.confirmationAction(amount) v.findNavController().navigate(action)} +``` + +在接收目的地的代码中,请使用 [`getArguments()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#getArguments()) 方法来检索 bundle 并使用其内容。使用 `-ktx` 依赖项时,Kotlin 用户还可以使用 `by navArgs()` 属性委托来访问参数。 + +```kotlin +val args: ConfirmationFragmentArgs by navArgs() + +override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val tv: TextView = view.findViewById(R.id.textViewAmount) + val amount = args.amount + tv.text = amount.toString() +} +``` + +## 使用 Bundle 对象在目的地之间传递参数 + +如果您不使用 Gradle,仍然可以使用 `Bundle` 对象在目的地之间传递参数。创建 `Bundle` 对象并使用 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 将它传递给目的地,如下所示: + +``` +val bundle = bundleOf("amount" to amount)view.findNavController().navigate(R.id.confirmationAction, bundle) +``` + +在接收目的地的代码中,请使用 [`getArguments()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#getArguments()) 方法来检索 `Bundle` 并使用其内容: + +```kotlin +val tv = view.findViewById(R.id.textViewAmount) +tv.text = arguments?.getString("amount") +``` + + + +## NavigationUI + +导航图是Navigation组件中很重要的一部分,它可以帮助我们快速了解页面之间的关系,再通过NavController便可以完成页面的切换工作。而在页面的切换过程中,通常还伴随着App bar中menu菜单的变化。对于不同的页面,App bar中的menu菜单很可能是不一样的。App bar中的各种按钮和菜单,同样承担着页面切换的工作。例如,当ActionBar左边的返回按钮被单击时,我们需要响应该事件,返回到上一个页面。既然Navigation和App bar都需要处理页面切换事件,那么,为了方便管理,Jetpack引入了NavigationUI组件,使App bar中的按钮和菜单能够与导航图中的页面关联起来。 + +`NavigationUI` 支持以下顶部应用栏类型: + +- [`Toolbar`](https://developer.android.com/reference/android/widget/Toolbar) +- [`CollapsingToolbarLayout`](https://developer.android.com/reference/com/google/android/material/appbar/CollapsingToolbarLayout) +- [`ActionBar`](https://developer.android.com/reference/androidx/appcompat/app/ActionBar) + +```kotlin +override fun onCreate(savedInstanceState: Bundle?) { + ... + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val navController = navHostFragment.navController + val appBarConfiguration = AppBarConfiguration( + topLevelDestinationIds = setOf(), + fallbackOnNavigateUpListener = ::onSupportNavigateUp + ) + findViewById(R.id.toolbar) + .setupWithNavController(navController, appBarConfiguration) +} + +``` + +### 参考 + +https://mp.weixin.qq.com/s?src=11×tamp=1712714064&ver=5191&signature=JTMgHGLtMGW*NoSWSrLNVuGzs-KEEDznO-ja7*X*KumZMFAuIRl7WbPYT1gG7AX810nUx6Ftb6nm6Ao92M*GzojPfqBUo1wOFc0gMs1mseTLkUWZ9Q*BIW69MM7ULPDV&new=1 + + +- [上一篇:11.Hilt简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/11.Hilt%E7%AE%80%E4%BB%8B.md) +- [下一篇:13.Jetpack MVVM简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/13.Jetpack%20MVVM%E7%AE%80%E4%BB%8B.md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Jetpack/architecture/13.Jetpack MVVM\347\256\200\344\273\213.md" "b/Jetpack/architecture/13.Jetpack MVVM\347\256\200\344\273\213.md" new file mode 100644 index 00000000..50a25fa1 --- /dev/null +++ "b/Jetpack/architecture/13.Jetpack MVVM\347\256\200\344\273\213.md" @@ -0,0 +1,50 @@ +# 13.Jetpack MVVM简介 + +项目地址:[android-architecture](https://github.com/googlesamples/android-architecture) +`Google`将该项目命名为`Android`的架构蓝图,我想从名字上已可以看穿一切。 + +在它的官方介绍中是这样说的: + +> The Android framework offers a lot of flexibility when it comes to defining how to organize and architect an Android app. This freedom, whilst very valuable, can also result in apps with large classes, inconsistent naming and architectures (or lack of) that can make testing, maintaining and extending difficult. + +> Android Architecture Blueprints is meant to demonstrate possible ways to help with these common problems. In this project we offer the same application implemented using different architectural concepts and tools. + +> You can use these samples as a reference or as a starting point for creating your own apps. The focus here is on code structure, architecture, testing and maintainability. However, bear in mind that there are many ways to build apps with these architectures and tools, depending on your priorities, so these shouldn't be considered canonical examples. The UI is deliberately kept simple. + +Jetpack MVVM 是 MVVM 模式在 Android 开发中的一个具体实现,是 Android中 Google 官方提供并推荐的 MVVM实现方式。 +不仅通过数据驱动完成彻底解耦,还兼顾了 Android 页面开发中其他不可预期的错误,例如Lifecycle 能在妥善处理 页面生命周期 避免view空指针问题,ViewModel使得UI发生重建时 无需重新向后台请求数据,节省了开销,让视图重建时更快展示数据。 +首先,请查看下图,该图显示了所有模块应如何彼此交互: + +各模块对应MVVM架构: + +View层:Activity/Fragment +ViewModel层:Jetpack ViewModel + Jetpack LivaData +Model层:Repository仓库,包含 本地持久性数据 和 服务端数据 + +View层 包含了我们平时写的Activity/Fragment/布局文件等与界面相关的东西。 +ViewModel层 用于持有和UI元素相关的数据,以保证这些数据在屏幕旋转时不会丢失,并且还要提供接口给View层调用以及和仓库层进行通信。 +仓库层 要做的主要工作是判断调用方请求的数据应该是从本地数据源中获取还是从网络数据源中获取,并将获取到的数据返回给调用方。本地数据源可以使用数据库、SharedPreferences等持久化技术来实现,而网络数据源则通常使用Retrofit访问服务器提供的Webservice接口来实现。 +另外,图中所有的箭头都是单向的,例如View层指向了ViewModel层,表示View层会持有ViewModel层的引用,但是反过来ViewModel层却不能持有View层的引用。除此之外,引用也不能跨层持有,比如View层不能持有仓库层的引用,谨记每一层的组件都只能与它相邻层的组件进行交互。 +这种设计打造了一致且愉快的用户体验。无论用户上次使用应用是在几分钟前还是几天之前,现在回到应用时都会立即看到应用在本地保留的数据。如果此数据已过期,则应用的Repository将开始在后台更新数据。 + +有人可能会有疑惑:怎么完全没有提 DataBinding、双向绑定? +实际上,这也是我之前的疑惑。 没有提 是因为: + +我不想让读者 一提到 MVVM 就和DataBinding联系起来 +我想让读者 抓住 MVVM 数据驱动 的本质。 +而DataBinding提供的双向绑定,是用来完善Jetpack MVVM 的工具,其本身在业界又非常具有争议性。 +掌握本篇内容,已经是Google推荐的开发架构,就已经实现 MVVM 模式。在Google官方的 应用架构指南 中 也同样丝毫没有提到 DataBinding。 + + +## 参考 +- [“终于懂了“系列:Jetpack AAC完整解析(四)MVVM - Android架构探索!](https://juejin.cn/post/6921321173661777933) + + +- [上一篇:12.Navigation简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/12.Navigation%E7%AE%80%E4%BB%8B.md) +- [下一篇:14.findViewById的过去及未来](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/14.findViewById%E7%9A%84%E8%BF%87%E5%8E%BB%E5%8F%8A%E6%9C%AA%E6%9D%A5.md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! ` \ No newline at end of file diff --git "a/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" "b/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" new file mode 100644 index 00000000..fe29ca19 --- /dev/null +++ "b/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" @@ -0,0 +1,28 @@ +# 14.findViewById的过去及未来 + +We have lots of alternatives for this, and you may wonder why do we need another solution. Let’s compare the different solutions based on these criteria: null-safety, compile-time safety, and speed. + +| Column 1 | **[ButterKnife](https://github.com/JakeWharton/butterknife)** | [**Kotlin Synthetics**](https://developer.android.com/kotlin/ktx) | [**Data Binding**](https://developer.android.com/topic/libraries/data-binding) | [**findViewById**](https://developer.android.com/reference/android/app/Activity#findViewById(int)) | [View Binding](https://developer.android.com/topic/libraries/view-binding) | +| --------------------- | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | +| **Fast** | ❌ * | ✅ | ❌ * | ✅ | ✅ | +| **Null-safe** | ❌ | ❌ | ✅ | ❌ | ✅ | +| **Compile-time safe** | ❌ | ❌ | ✅ | ✅ ** | ✅ | + +\* ButterKnife and Data Binding solutions are slower because they use an annotation-based approach + ** `findViewById()` is compile-time safe since API 26 because we don’t need to cast the type of view anymore. + +https://juejin.cn/post/6905942568467759111 + +https://medium.com/mobile-app-development-publication/how-android-access-view-item-the-past-to-the-future-bb003ae84527 + + + +## 参考 +- [Kotlin 插件的落幕,ViewBinding 的崛起](https://juejin.cn/post/6905942568467759111) +- [How Android Access View Item: The Past to the Future](https://medium.com/mobile-app-development-publication/how-android-access-view-item-the-past-to-the-future-bb003ae84527) + + + +- [上一篇:13.Jetpack MVVM简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/13.Jetpack%20MVVM%E7%AE%80%E4%BB%8B.md) + + diff --git "a/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" "b/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" new file mode 100644 index 00000000..4718b3f9 --- /dev/null +++ "b/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" @@ -0,0 +1,426 @@ +# 2.ViewBinding简介 + +ViewBinding是Google在2019年I/O大会上公布的一款Android视图绑定工具,在Android Studio 3.6中添加的一个新功能,更准确的说,它是DataBinding的一个更轻量变体,为什么要使用View Binding呢?答案是性能。许多开发者使用Data Binding库来引用Layout XML中的视图,而忽略它的其他强大功能。相比来说,自动生成代码ViewBinding其实比DataBinding性能更好。但是传统的方式使用View Binding却不是很好,因为会有很多样板代码(垃圾代码)。 + +通过ViewBinding,你可以更轻松的编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个XML布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有ID的所有视图的直接引用。在大多数情况下,视图绑定会替代findViewById。 + +## 使用方法 + +### 1.build.gradle中开启 +在build.gradle文件中的android节点添加如下代码: +``` +android { + ... + buildFeatures { + viewBinding true + } +} +``` +重新编译后系统会为每个布局文件生成对应的Binding类,该类中包含对应布局中具有id的所有视图的直接饮用。生成类的目录在app/build/generated/data_binding_base_class_source_out中。 +如果项目中存在多个模块,则需要在每个模块的build.gradle文件中都加上该配置。 +假设某个布局文件的名称为result_profile.xml: + +```xml + + + +