diff --git a/.gitignore b/.gitignore index 71986661..c647db94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /.project /.DS_Store -/.DS_Store +/.idea diff --git "a/AdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" "b/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" similarity index 99% rename from "AdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" rename to "AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" index 15246225..bf052c89 100644 --- "a/AdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" +++ "b/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" @@ -1,5 +1,5 @@ -热修复实现 +热修复实现(一) === diff --git "a/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" "b/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" new file mode 100644 index 00000000..7d02b934 --- /dev/null +++ "b/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" @@ -0,0 +1,97 @@ + +热修复实现(二) +=== + +之前也分析过`InstantRun`的源码,前面也写了一篇热修复实现原理的文章。 + +But,最近遇到困难了,所在项目要做插件化,同事在开发过程中遇到了一个在5.0以下手机崩溃的问题,想着一起找找原因修复下。 +但是这个bug已经折腾了我两天了,只找到原因,并没有找出任何解决方案。 + +深深的感觉到之前的那些都是皮毛,没有真正的去做,真正的去处理一些细节,那些都是没任何意义的。 + +所以这里系统学习一下,既然学习,就找一个做的最好的来学。 + + +前面的文章也介绍了,目前存在的几个开源框架: + +- 手机淘宝基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java Method Hook技术-Dexposed。 +但这个方案犹豫底层Dalvik结构过于依赖,最终无法兼容Android 5.0以后的ART虚拟机。 + +- 支付宝提出了新的方案Andfix,它同样是一种底层结构替换的方案,也达到了运行时生效即时修复的效果,而且做到了Dalvik和ART全版本的兼容。然而它也 +是由局限性的,且不说其底层固定结构的替换方案稳定性不好,其使用范文也存在着诸多限制,虽然可以通过代码改造来绕过限制达到同样的 +修复目的,但是这种方式即不优雅也不方便。而且更大的问题是Andfix只提供了代码层面的修复,对于资源和so的修复都还未实现。 + +- 其他的就是微信Tinker、饿了么的Amigo、美团的Robust,不过他们都各自有各自的局限性,或者不够稳定、或者补丁过大、或者效率低下 +,或者使用起来太繁琐,大部分技术上看起来似乎可行,但是实际体验并不好。 + +我们学习的就是阿里巴巴的新一代非侵入式Android热修复方案-Sophix。 +它各个方面都比较优秀,使用也比较方便,唯一不支持的就是四大组件的修复。这是因为如果修复四大组件,必须在AndroidManifest里面预先插入代码组件,并且尽可能声明所有权限,这样就会给 +原先的app添加很多臃肿的代码,对app运行流程的侵入性很强。 + + +在Sophix中,唯一需要的就是初始化和请求补丁两行代码,甚至连入口Application类我们都不需要做任何修改。 + +这样就给了开发者最大的透明度和自由度。我们甚至重新开发了打包工具,使的补丁工具操作图形界面化,这种所见即所得的补丁生成 +方式也是阿里热修复独家的,因此,Sophix的接入成本也是目前市面上所有方案里最低的。 + + + + +代码修复 +--- + +代码修复有两大主要方案: + +- 阿里系的底层替换方案:底层替换方案限制颇多,但是时效性最好,加载轻快,立即见效。 + + 底层替换方案是在已经加载了的类中直接替换掉原有的方法,是在原来类的基础上进行修改的。因而无法实现对原有类进行方法和字段的增减,因为这样将破坏原有类的结构。 + 一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个dex的方法数的变化,方法数的变化伴随着方法索引的变化,这样在 + 访问方法时就无法正常的索引到正确的方法了。如果字段发生了增加或减少,和方法变化的情况一样,所有字段的索引都会发生变化。 + 而新方法中使用到这些老的示例对象时,访问新增字段就会会产生不可预期的结果。 + + 这是该类方案的固有限制,而底层替换方案最为人逅病的地方,在于底层替换的不稳定性。因为Hook方案,都是直接依赖修改虚拟机方法实体的具体字段。因为Art虚拟机和Dalvik虚拟机的不同,每个版本的虚拟机都要适配,而且Android系统是开源的,各个厂商都可以对代码进行改造,如果某个厂商进行了修改,那么这种通用性的替换机制就会出问题。这便是不稳定的根源。 + + + +- 腾讯系的类加载方案:类加载方案时效性差,需要重新冷启动才能见效,但修复范围广、限制少。 + +类加载方案的原理是在app重新启动后让classloader去加载新的类。因为在app运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。如果不重启,原来的类还在虚拟机中,就无法加载新类,因此只有在下次重启的时候, +在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类的时候,就会用新类,从而达到热修复的目的。 + + +为什么sophix更好? +--- + +既然底层替换方案和类加载方案各有优点,把他们联合起来就是最好的选择。Sophix就是同时涵盖了这两种方案,可以实现优势互补、完全兼顾的作用,可以灵活的根据实际情况自动切换。 + + + + + + +但是[Sophix](https://help.aliyun.com/document_detail/57064.html?spm=a2c4g.11186623.6.543.SPhMhO)有一个缺点就是,他不是开源的,而且是收费的。但是确实强大。 + + + + +- [Google Instant app](https://developer.android.com/topic/instant-apps/index.html) +- [微信热补丁实现](https://github.com/WeMobileDev/article/blob/master/%E5%BE%AE%E4%BF%A1Android%E7%83%AD%E8%A1%A5%E4%B8%81%E5%AE%9E%E8%B7%B5%E6%BC%94%E8%BF%9B%E4%B9%8B%E8%B7%AF.md#rd) +- [多dex分拆](http://my.oschina.net/853294317/blog/308583) +- [QQ空间热修复方案](https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a) +- [Android dex分包方案](http://my.oschina.net/853294317/blog/308583) +- [类加载器DexClassLoader](http://www.maplejaw.com/2016/05/24/Android%E6%8F%92%E4%BB%B6%E5%8C%96%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%80%EF%BC%89%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8DexClassLoader/) +- [基于cydia Hook在线热修复补丁方案](http://blog.csdn.net/xwl198937/article/details/49801975) +- [Android 热补丁动态修复框架小结](http://blog.csdn.net/lmj623565791/article/details/49883661) +- [美团Android DEX自动拆包及动态加载简介](http://tech.meituan.com/mt-android-auto-split-dex.html) +- [插件化从放弃到捡起](http://kymjs.com/column/plugin.html) +- [无需Root也能使用Xposed!](http://weishu.me/) +- [当你准备开发一个热修复框架的时候,你需要了解的一切](http://www.zjutkz.net/2016/05/23/%E5%BD%93%E4%BD%A0%E5%87%86%E5%A4%87%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E7%83%AD%E4%BF%AE%E5%A4%8D%E6%A1%86%E6%9E%B6%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E4%BD%A0%E9%9C%80%E8%A6%81%E4%BA%86%E8%A7%A3%E7%9A%84%E4%B8%80%E5%88%87/) + + +[1]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/InstantRun%E8%AF%A6%E8%A7%A3.md "InstantRun详解" + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" "b/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" new file mode 100644 index 00000000..4da9dd5c --- /dev/null +++ "b/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" @@ -0,0 +1,395 @@ +在做热修复功能时Java层通过反射调用addAssetPath在Android5.0及以上系统没有问题,在Android 4.x版本找不到资源。 + +addAssetPath方法: +```java +/** + * Add an additional set of assets to the asset manager. This can be + * either a directory or ZIP file. Not for use by applications. Returns + * the cookie of the added asset, or 0 on failure. + * {@hide} + */ +public final int addAssetPath(String path) { + int res = addAssetPathNative(path); + return res; +} +private native final int addAssetPathNative(String path); +``` + +addAssetPath具体的实现方法是native层的。 + +[AssetManager.cpp源码](https://android.googlesource.com/platform/frameworks/base/+/android-4.4_r1.0.1/libs/androidfw/AssetManager.cpp) + +4.4及以前的版本调用addAssetPath方法时,只是把补丁包的路径添加到mAssetPath中,不会去重新解析,真正解析的代码是在app第一次执行AssetManager.getResTable()`方法的时候。 +一旦解析完一次后,mResource对象就不为nil,以后就会直接return掉,不会重新解析。 + +```c +bool AssetManager::addAssetPath(const String8& path, void** cookie) +{ + AutoMutex _l(mLock); + asset_path ap; + String8 realPath(path); + if (kAppZipName) { + realPath.appendPath(kAppZipName); + } + ap.type = ::getFileType(realPath.string()); + if (ap.type == kFileTypeRegular) { + ap.path = realPath; + } else { + ap.path = path; + ap.type = ::getFileType(path.string()); + if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) { + ALOGW("Asset path %s is neither a directory nor file (type=%d).", + path.string(), (int)ap.type); + return false; + } + } + // Skip if we have it already. + for (size_t i=0; i(this)->loadFileNameCacheLocked(); + const size_t N = mAssetPaths.size(); + // 真正解析package的地方 + for (size_t i=0; i(this)-> + mZipSet.getZipResourceTable(ap.path); + } + if (sharedRes == NULL) { + ass = const_cast(this)-> + mZipSet.getZipResourceTableAsset(ap.path); + if (ass == NULL) { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + if (ass != NULL && ass != kExcludedAsset) { + ass = const_cast(this)-> + mZipSet.setZipResourceTableAsset(ap.path, ass); + } + } + + if (i == 0 && ass != NULL) { + // If this is the first resource table in the asset + // manager, then we are going to cache it so that we + // can quickly copy it out for others. + ALOGV("Creating shared resources for %s", ap.path.string()); + sharedRes = new ResTable(); + sharedRes->add(ass, (void*)(i+1), false, idmap); + sharedRes = const_cast(this)-> + mZipSet.setZipResourceTable(ap.path, sharedRes); + } + } + } else { + ALOGV("loading resource table %s\n", ap.path.string()); + Asset* ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + shared = false; + } + if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) { + if (rt == NULL) { + mResources = rt = new ResTable(); + updateResourceParamsLocked(); + } + ALOGV("Installing resource asset %p in to table %p\n", ass, mResources); + if (sharedRes != NULL) { + ALOGV("Copying existing resources for %s", ap.path.string()); + rt->add(sharedRes); + } else { + ALOGV("Parsing resources for %s", ap.path.string()); + rt->add(ass, (void*)(i+1), !shared, idmap); + } + if (!shared) { + delete ass; + } + } + if (idmap != NULL) { + delete idmap; + } + MY_TRACE_END(); + } + if (required && !rt) ALOGW("Unable to find resources file resources.arsc"); + if (!rt) { + mResources = rt = new ResTable(); + } + return rt; +} + + +const ResTable& AssetManager::getResources(bool required) const +{ + const ResTable* rt = getResTable(required); + return *rt; +} +``` +而当我们执行加载补丁的代码的时候,getResTable已经执行过多次了,Android Framework里面的代码会多次调用该方法。所以即使是使用addAssetPath,也只是添加到了mAssetPath,并不会发生解析,所以补丁包里面的资源就是完全不生效的。 + +而在android 5.0及以上的代码中: + +[Android 5.0 AssetManager.cpp源码](https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r7/libs/androidfw/AssetManager.cpp) + +```c +bool AssetManager::addAssetPath(const String8& path, int32_t* cookie) +{ + AutoMutex _l(mLock); + asset_path ap; + String8 realPath(path); + if (kAppZipName) { + realPath.appendPath(kAppZipName); + } + ap.type = ::getFileType(realPath.string()); + if (ap.type == kFileTypeRegular) { + ap.path = realPath; + } else { + ap.path = path; + ap.type = ::getFileType(path.string()); + if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) { + ALOGW("Asset path %s is neither a directory nor file (type=%d).", + path.string(), (int)ap.type); + return false; + } + } + // Skip if we have it already. + for (size_t i=0; i(i+1); + } + return true; + } + } + ALOGV("In %p Asset %s path: %s", this, + ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string()); + // Check that the path has an AndroidManifest.xml + Asset* manifestAsset = const_cast(this)->openNonAssetInPathLocked( + kAndroidManifest, Asset::ACCESS_BUFFER, ap); + if (manifestAsset == NULL) { + // This asset path does not contain any resources. + delete manifestAsset; + return false; + } + delete manifestAsset; + mAssetPaths.add(ap); + // new paths are always added at the end + if (cookie) { + *cookie = static_cast(mAssetPaths.size()); + } +#ifdef HAVE_ANDROID_OS + // Load overlays, if any + asset_path oap; + for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) { + mAssetPaths.add(oap); + } +#endif + if (mResources != NULL) { + // 重新调用该方法去解析package + appendPathToResTable(ap); + } + return true; +} +``` + +```c +bool AssetManager::appendPathToResTable(const asset_path& ap) const { + Asset* ass = NULL; + ResTable* sharedRes = NULL; + bool shared = true; + bool onlyEmptyResources = true; + MY_TRACE_BEGIN(ap.path.string()); + Asset* idmap = openIdmapLocked(ap); + size_t nextEntryIdx = mResources->getTableCount(); + ALOGV("Looking for resource asset in '%s'\n", ap.path.string()); + if (ap.type != kFileTypeDirectory) { + if (nextEntryIdx == 0) { + // The first item is typically the framework resources, + // which we want to avoid parsing every time. + sharedRes = const_cast(this)-> + mZipSet.getZipResourceTable(ap.path); + if (sharedRes != NULL) { + // skip ahead the number of system overlay packages preloaded + nextEntryIdx = sharedRes->getTableCount(); + } + } + if (sharedRes == NULL) { + ass = const_cast(this)-> + mZipSet.getZipResourceTableAsset(ap.path); + if (ass == NULL) { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + if (ass != NULL && ass != kExcludedAsset) { + ass = const_cast(this)-> + mZipSet.setZipResourceTableAsset(ap.path, ass); + } + } + + if (nextEntryIdx == 0 && ass != NULL) { + // If this is the first resource table in the asset + // manager, then we are going to cache it so that we + // can quickly copy it out for others. + ALOGV("Creating shared resources for %s", ap.path.string()); + sharedRes = new ResTable(); + sharedRes->add(ass, idmap, nextEntryIdx + 1, false); +#ifdef HAVE_ANDROID_OS + const char* data = getenv("ANDROID_DATA"); + LOG_ALWAYS_FATAL_IF(data == NULL, "ANDROID_DATA not set"); + String8 overlaysListPath(data); + overlaysListPath.appendPath(kResourceCache); + overlaysListPath.appendPath("overlays.list"); + addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx); +#endif + sharedRes = const_cast(this)-> + mZipSet.setZipResourceTable(ap.path, sharedRes); + } + } + } else { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + shared = false; + } + if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) { + ALOGV("Installing resource asset %p in to table %p\n", ass, mResources); + if (sharedRes != NULL) { + ALOGV("Copying existing resources for %s", ap.path.string()); + mResources->add(sharedRes); + } else { + ALOGV("Parsing resources for %s", ap.path.string()); + mResources->add(ass, idmap, nextEntryIdx + 1, !shared); + } + onlyEmptyResources = false; + if (!shared) { + delete ass; + } + } else { + ALOGV("Installing empty resources in to table %p\n", mResources); + mResources->addEmpty(nextEntryIdx + 1); + } + if (idmap != NULL) { + delete idmap; + } + MY_TRACE_END(); + return onlyEmptyResources; +} +const ResTable* AssetManager::getResTable(bool required) const +{ + ResTable* rt = mResources; + if (rt) { + return rt; + } + // Iterate through all asset packages, collecting resources from each. + AutoMutex _l(mLock); + if (mResources != NULL) { + return mResources; + } + if (required) { + LOG_FATAL_IF(mAssetPaths.size() == 0, "No assets added to AssetManager"); + } + if (mCacheMode != CACHE_OFF && !mCacheValid) { + const_cast(this)->loadFileNameCacheLocked(); + } + mResources = new ResTable(); + updateResourceParamsLocked(); + bool onlyEmptyResources = true; + const size_t N = mAssetPaths.size(); + // 也是调用appendPathToResTable去解析 + for (size_t i=0; i 布局(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/AdavancedPart/Java b/AdavancedPart/Java new file mode 100644 index 00000000..e69de29b 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" new file mode 100644 index 00000000..e7dcaaa0 --- /dev/null +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -0,0 +1,941 @@ +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的长度所致,这种情况比较少。 + +#### java.lang.OutOfMemoryError: "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM"; + +通常情况下是因为`java`堆内存不足导致的,即`Runtime.getRuntime().maxMemory()`获取到的最大内存无法满足要申请的内存大小时,这种情况比较好模拟,例如我们可以通过`new byte[]`的方式来申请超过`maxMemory()`的内存,但是也有一些情况是堆内存充裕,而且设备内存也充裕的情况下发生的。 +#### java.lang.OutOfMemoryError: Could not allocate JNI Env(代号JNIEnv) + +- cat /proc/pid/limits 描述linux系统对对应进程的限制: +``` +Limit Soft Limit Hard Limit Units +Max cpu time unlimited unlimited seconds +Max file size unlimited unlimited bytes +Max data size unlimited unlimited bytes +Max stack size 8388608 unlimited bytes // 整个系统的 +Max core file size 0 unlimited bytes +Max resident set unlimited unlimited bytes +Max processes 17235 17235 processes // 整个系统的最大进程数,底层只有进程,线程也是通过进程实现的 +Max open files 32768 32768 files // 每个进程最大打开文件的数量 +Max locked memory 67108864 67108864 bytes // 线程创建过程中分配线程私有stack使用的mmap调用没有设置MAP_LOCKED,所以这个限制与线程创建过程无关 +Max address space unlimited unlimited bytes +Max file locks unlimited unlimited locks +Max pending signals 17235 17235 signals // c层信号个数阈值,与线程创建过程无关 +Max msgqueue size 819200 819200 bytes +Max nice priority 40 40 +Max realtime priority 0 0 +Max realtime timeout unlimited unlimited us +``` +里面`Max open files`表示每个进程最大打开文件的数目,进程每打开一个文件就会产生一个文件描述符`fd`(记录在/proc/pid/fd中) + +验证: 触发大量的网络连接或者打开文件,每个连接处于独立的线程中并报出,每打开一个socket都会增加一个fd +```java + private Runnable increaseFDRunnable = new Runnable() { + @Override + public void run() { + try { + for (int i = 0; i < 1000; i++) { + new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/status")); + } + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + }; +``` +#### java.lang.OutOfMemoryError: pthread_create(1040KB statck) failed: Out of memory(代号1040) + +根据OOM的crash日志,我们发现都是从`Thread.start()`方法开始的, +```java + public synchronized void start() { + /** + * This method is not invoked for the main method thread or "system" + * group threads created/set up by the VM. Any new functionality added + * to this method in the future may have to also be added to the VM. + * + * A zero status value corresponds to state "NEW". + */ + // Android-changed: throw if 'started' is true + if (threadStatus != 0 || started) + throw new IllegalThreadStateException(); + + /* Notify the group that this thread is about to be started + * so that it can be added to the group's list of threads + * and the group's unstarted count can be decremented. */ + group.add(this); + + started = false; + try { + nativeCreate(this, stackSize, daemon); + started = true; + } finally { + try { + if (!started) { + group.threadStartFailed(this); + } + } catch (Throwable ignore) { + /* do nothing. If start0 threw a Throwable then + it will be passed up the call stack */ + } + } + } +``` +上面的核心方法是`nativeCreate(this, stackSize, daemon);` +这个方法有三个参数: +- this : Thread对象自身 +- stackSize : the desired stack size for the new thread, or zero to indicate that this parameter is to be ignored.该新创建的线程的栈的大小,单位是byte,一般默认情况下都是0,我们全局搜这个变量复制的地方,可以看到Thread有一个构造函数可以设置这个值 +``` +public Thread(ThreadGroup group, Runnable target, String name, + long stackSize) { + init(group, target, name, stackSize); +} +``` +- daemon : Whether or not the thread is a daemon thread. + +而nativeCreate方法的native实现,是在art/runtime/native/java_lang_thread.cc中,因为OOM主要是在Android 9.0系统发生,所以这里基于9.0系统的源码分析:`https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/native/java_lang_Thread.cc`。 +```java +static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size, + jboolean daemon) { + // There are sections in the zygote that forbid thread creation. + Runtime* runtime = Runtime::Current(); + if (runtime->IsZygote() && runtime->IsZygoteNoThreadSection()) { + jclass internal_error = env->FindClass("java/lang/InternalError"); + CHECK(internal_error != nullptr); + env->ThrowNew(internal_error, "Cannot create threads in zygote"); + return; + } + Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE); +} +``` +里面又会调用到[art/runtime/thread.cc](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/thread.cc)中的Thread::CreateNativeThread方法: +```java +void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) { + CHECK(java_peer != nullptr); + Thread* self = static_cast(env)->GetSelf(); + if (VLOG_IS_ON(threads)) { + ScopedObjectAccess soa(env); + ArtField* f = jni::DecodeArtField(WellKnownClasses::java_lang_Thread_name); + ObjPtr java_name = + f->GetObject(soa.Decode(java_peer))->AsString(); + std::string thread_name; + if (java_name != nullptr) { + thread_name = java_name->ToModifiedUtf8(); + } else { + thread_name = "(Unnamed)"; + } + VLOG(threads) << "Creating native thread for " << thread_name; + self->Dump(LOG_STREAM(INFO)); + } + Runtime* runtime = Runtime::Current(); + // Atomically start the birth of the thread ensuring the runtime isn't shutting down. + bool thread_start_during_shutdown = false; + { + MutexLock mu(self, *Locks::runtime_shutdown_lock_); + if (runtime->IsShuttingDownLocked()) { + thread_start_during_shutdown = true; + } else { + runtime->StartThreadBirth(); + } + } + if (thread_start_during_shutdown) { + ScopedLocalRef error_class(env, env->FindClass("java/lang/InternalError")); + env->ThrowNew(error_class.get(), "Thread starting during runtime shutdown"); + return; + } + + // 1. native层创建thread + Thread* child_thread = new Thread(is_daemon); + // Use global JNI ref to hold peer live while child thread starts. + child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer); + // 2. FixStackSize方法里面会返回具体的Stack内存的大小 + stack_size = FixStackSize(stack_size); + // Thread.start is synchronized, so we know that nativePeer is 0, and know that we're not racing + // to assign it. + env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, + reinterpret_cast(child_thread)); + + // 3. java中的每一个线程都都应一个JNIEnv结构,这里的JNIEnvExt就是ART中的JNIEnv。下面的注释说明的很明白,这里可能会有oom + // Try to allocate a JNIEnvExt for the thread. We do this here as we might be out of memory and + // do not have a good way to report this on the child's side. + std::string error_msg; + std::unique_ptr child_jni_env_ext( + JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM(), &error_msg)); + int pthread_create_result = 0; + if (child_jni_env_ext.get() != nullptr) { + // 4. child_jni_env_ext.get() != nullptr 才会继续 + pthread_t new_pthread; + pthread_attr_t attr; + child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get(); + CHECK_PTHREAD_CALL(pthread_attr_init, (&attr), "new thread"); + CHECK_PTHREAD_CALL(pthread_attr_setdetachstate, (&attr, PTHREAD_CREATE_DETACHED), + "PTHREAD_CREATE_DETACHED"); + CHECK_PTHREAD_CALL(pthread_attr_setstacksize, (&attr, stack_size), stack_size); + // 5. 调用pthread_create创建线程,并返回结果 + pthread_create_result = pthread_create(&new_pthread, + &attr, + Thread::CreateCallback, + child_thread); + CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread"); + if (pthread_create_result == 0) { + // 6. 结果为0才是创建成功 + // pthread_create started the new thread. The child is now responsible for managing the + // JNIEnvExt we created. + // Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization + // between the threads. + child_jni_env_ext.release(); + return; + } + } + // Either JNIEnvExt::Create or pthread_create(3) failed, so clean up. + { + MutexLock mu(self, *Locks::runtime_shutdown_lock_); + runtime->EndThreadBirth(); + } + // Manually delete the global reference since Thread::Init will not have been run. + env->DeleteGlobalRef(child_thread->tlsPtr_.jpeer); + child_thread->tlsPtr_.jpeer = nullptr; + delete child_thread; + child_thread = nullptr; + // TODO: remove from thread group? + env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0); + { + std::string msg(child_jni_env_ext.get() == nullptr ? + StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) : + // 具体的错误信息由pthread_create_result的返回的错误码给出。 + StringPrintf("pthread_create (%s stack) failed: %s", + PrettySize(stack_size).c_str(), strerror(pthread_create_result))); + ScopedObjectAccess soa(env); + soa.Self()->ThrowOutOfMemoryError(msg.c_str()); + } +} +``` +上面代码太多,分了四个部分: +1. native层创建thread +2. FixStackSize方法里面会返回具体的Stack内存的大小 +``` +static size_t FixStackSize(size_t stack_size) { + // A stack size of zero means "use the default". + if (stack_size == 0) { + stack_size = Runtime::Current()->GetDefaultStackSize(); + } + // Dalvik used the bionic pthread default stack size for native threads, + // so include that here to support apps that expect large native stacks. + stack_size += 1 * MB; + // It's not possible to request a stack smaller than the system-defined PTHREAD_STACK_MIN. + if (stack_size < PTHREAD_STACK_MIN) { + stack_size = PTHREAD_STACK_MIN; + } + if (Runtime::Current()->ExplicitStackOverflowChecks()) { + // It's likely that callers are trying to ensure they have at least a certain amount of + // stack space, so we should add our reserved space on top of what they requested, rather + // than implicitly take it away from them. + // 8k + stack_size += GetStackOverflowReservedBytes(kRuntimeISA); + } else { + // If we are going to use implicit stack checks, allocate space for the protected + // region at the bottom of the stack. + // 8k 8k + stack_size += Thread::kStackOverflowImplicitCheckSize + + GetStackOverflowReservedBytes(kRuntimeISA); + } + // Some systems require the stack size to be a multiple of the system page size, so round up. + stack_size = RoundUp(stack_size, kPageSize); + return stack_size; +} +``` + +// static const size_t kStackOverflowImplicitCheckSize = 8 * KB; +上面kStackOverflowImplicitCheckSize的值是8k,而前面是1m,1024k+8k+8k=1040k,这就是为什么crash信息里面java.lang.OutOfMemoryError pthread_create (1040KB stack) failed: Out of memory是1040kb的原因。 + +3. java中的每一个线程都都应一个JNIEnv结构,这里的JNIEnvExt就是ART中的JNIEnv。下面的注释说明的很明白,这里可能会有oom,这里具体要看[JNIEnvExt::Create()](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/jni_env_ext.cc)方法 +``` +const JNINativeInterface* JNIEnvExt::table_override_ = nullptr; + +JNIEnvExt* JNIEnvExt::Create(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg) { + // 调用new JNIEnvExt构造函数 + std::unique_ptr ret(new JNIEnvExt(self_in, vm_in, error_msg)); + if (CheckLocalsValid(ret.get())) { + return ret.release(); + } + return nullptr; +} +``` +代码中发现特确实会返回nullptr,只有在CheckLocalsValid(ret.get())返回false的时候才会 +``` +bool JNIEnvExt::CheckLocalsValid(JNIEnvExt* in) NO_THREAD_SAFETY_ANALYSIS { + if (in == nullptr) { + return false; + } + return in->locals_.IsValid(); +} +``` +从代码上看,基本排除是传入的参数nullptr导致的,所以根本原因是locals.IsValid返回了false,而locals是JNIEnvExt的一个成员变量,在JNIEnvExt构造的时候通过成员列表方式初始化 +``` +JNIEnvExt::JNIEnvExt(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg) + : self_(self_in), + vm_(vm_in), + local_ref_cookie_(kIRTFirstSegment), + locals_(kLocalsInitial, kLocal, IndirectReferenceTable::ResizableCapacity::kYes, error_msg), + monitors_("monitors", kMonitorsInitial, kMonitorsMax), + critical_(0), + check_jni_(false), + runtime_deleted_(false) { + MutexLock mu(Thread::Current(), *Locks::jni_function_table_lock_); + check_jni_ = vm_in->IsCheckJniEnabled(); + functions = GetFunctionTable(check_jni_); + unchecked_functions_ = GetJniNativeInterface(); +} +``` + +而[locals_.isValid](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/indirect_reference_table.cc)的方法的源码为: +``` +bool IndirectReferenceTable::IsValid() const { + return table_mem_map_.get() != nullptr; +} +``` +所以只可能是table_men_map为nullptr导致的。 +而[IndirectReferenceTable](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/indirect_reference_table.cc)类,他的构造函数为 +``` +IndirectReferenceTable::IndirectReferenceTable(size_t max_count, + IndirectRefKind desired_kind, + ResizableCapacity resizable, + std::string* error_msg) + : segment_state_(kIRTFirstSegment), + kind_(desired_kind), + max_entries_(max_count), + current_num_holes_(0), + resizable_(resizable) { + CHECK(error_msg != nullptr); + CHECK_NE(desired_kind, kHandleScopeOrInvalid); + // Overflow and maximum check. + CHECK_LE(max_count, kMaxTableSizeInBytes / sizeof(IrtEntry)); + // max_count是常量512,而sizeof(IrtEntry)是8,所以table_bytes = 512 * 8 = 4k + const size_t table_bytes = max_count * sizeof(IrtEntry); + table_mem_map_.reset(MemMap::MapAnonymous("indirect ref table", nullptr, table_bytes, + PROT_READ | PROT_WRITE, false, false, error_msg)); + if (table_mem_map_.get() == nullptr && error_msg->empty()) { + *error_msg = "Unable to map memory for indirect ref table"; + } + if (table_mem_map_.get() != nullptr) { + table_ = reinterpret_cast(table_mem_map_->Begin()); + } else { + table_ = nullptr; + } + segment_state_ = kIRTFirstSegment; + last_known_previous_state_ = kIRTFirstSegment; +} +``` + + +如果上面失败的话,那就只有一种情况就是 MemMap::MapAnonymous 失败了,而MemMap::MapAnonymous的作用是为JNIEnv结构体中的Indirect_Reference_table(C层用于存储JNI局部/全局变量)申请内存,我们继续看[MemMap](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/mem_map.cc) +``` +MemMap* MemMap::MapAnonymous(const char* name, + uint8_t* expected_ptr, + size_t byte_count, + int prot, + bool low_4gb, + bool reuse, + std::string* error_msg, + bool use_ashmem) { +#ifndef __LP64__ + UNUSED(low_4gb); +#endif + use_ashmem = use_ashmem && !kIsTargetLinux; + if (byte_count == 0) { + return new MemMap(name, nullptr, 0, nullptr, 0, prot, false); + } + size_t page_aligned_byte_count = RoundUp(byte_count, kPageSize); + int flags = MAP_PRIVATE | MAP_ANONYMOUS; + if (reuse) { + // reuse means it is okay that it overlaps an existing page mapping. + // Only use this if you actually made the page reservation yourself. + CHECK(expected_ptr != nullptr); + DCHECK(ContainedWithinExistingMap(expected_ptr, byte_count, error_msg)) << *error_msg; + flags |= MAP_FIXED; + } + if (use_ashmem) { + if (!kIsTargetBuild) { + // When not on Android (either host or assuming a linux target) ashmem is faked using + // files in /tmp. Ensure that such files won't fail due to ulimit restrictions. If they + // will then use a regular mmap. + struct rlimit rlimit_fsize; + CHECK_EQ(getrlimit(RLIMIT_FSIZE, &rlimit_fsize), 0); + use_ashmem = (rlimit_fsize.rlim_cur == RLIM_INFINITY) || + (page_aligned_byte_count < rlimit_fsize.rlim_cur); + } + } + unique_fd fd; + if (use_ashmem) { + // android_os_Debug.cpp read_mapinfo assumes all ashmem regions associated with the VM are + // prefixed "dalvik-". + std::string debug_friendly_name("dalvik-"); + debug_friendly_name += name; + // 1. 创建 + fd.reset(ashmem_create_region(debug_friendly_name.c_str(), page_aligned_byte_count)); + // == -1 就说明是fd超过了系统限制的最大fd量,错误信息中会有Too many open files的提示 + if (fd.get() == -1) { + // We failed to create the ashmem region. Print a warning, but continue + // anyway by creating a true anonymous mmap with an fd of -1. It is + // better to use an unlabelled anonymous map than to fail to create a + // map at all. + PLOG(WARNING) << "ashmem_create_region failed for '" << name << "'"; + } else { + // We succeeded in creating the ashmem region. Use the created ashmem + // region as backing for the mmap. + flags &= ~MAP_ANONYMOUS; + } + } + // We need to store and potentially set an error number for pretty printing of errors + int saved_errno = 0; + // 2. 调用mmap映射到用户态内存地址空间 + void* actual = MapInternal(expected_ptr, + page_aligned_byte_count, + prot, + flags, + fd.get(), + 0, + low_4gb); + saved_errno = errno; + if (actual == MAP_FAILED) { + if (error_msg != nullptr) { + if (kIsDebugBuild || VLOG_IS_ON(oat)) { + PrintFileToLog("/proc/self/maps", LogSeverity::WARNING); + } + *error_msg = StringPrintf("Failed anonymous mmap(%p, %zd, 0x%x, 0x%x, %d, 0): %s. " + "See process maps in the log.", + expected_ptr, + page_aligned_byte_count, + prot, + flags, + fd.get(), + strerror(saved_errno)); + } + return nullptr; + } + if (!CheckMapRequest(expected_ptr, actual, page_aligned_byte_count, error_msg)) { + return nullptr; + } + return new MemMap(name, reinterpret_cast(actual), byte_count, actual, + page_aligned_byte_count, prot, reuse); +} +``` +这里面又用到了`ashmem_create_region()`方法,该方法的作用就是创建一块匿名共享内存(Anonymous Shared Memory-Ashmem),并返回一个文件描述符,我们看一下[ashmem_create_region](https://android.googlesource.com/platform/system/core/+/4f6e8d7a00cbeda1e70cc15be9c4af1018bdad53/libcutils/ashmem-dev.c)的源码: +``` +/* + * ashmem_create_region - creates a new ashmem region and returns the file + * descriptor, or <0 on error + * + * `name' is an optional label to give the region (visible in /proc/pid/maps) + * `size' is the size of the region, in page-aligned bytes + */ +int ashmem_create_region(const char *name, size_t size) +{ + int fd, ret; + // 打开一个fd + fd = open(ASHMEM_DEVICE, O_RDWR); + if (fd < 0) + return fd; + if (name) { + char buf[ASHMEM_NAME_LEN]; + strlcpy(buf, name, sizeof(buf)); + ret = ioctl(fd, ASHMEM_SET_NAME, buf); + if (ret < 0) + goto error; + } + ret = ioctl(fd, ASHMEM_SET_SIZE, size); + if (ret < 0) + goto error; + return fd; +error: + close(fd); + return ret; +} +``` + + +上面的两个步骤中,不论第一个步骤执行成功与否,都会执行第二步,但是执行的行为不同 + +- 如果第一步执行成功,就会通过Andorid的匿名共享内存(Anonymous Shared Memory)分配4KB(一个page)内核态内存,然后再通过Linux的mmap调用映射到用户态虚拟内存地址空间。 +- 如果第一步执行失败,第二步就会通过Linux的mmap调用创建一段虚拟内存。 + +而上面失败的情况主要有: +- 第一步失败的情况一般是内核分配内存失败,这种情况下,整个设备OS的内存应该都处于非常紧张的状态。但是我们从crash的信息里面看用户的内存还是挺充足的,所以排除这种情况。 +- 第二步失败的情况一般是进程虚拟内存地址空间耗尽。而且会打印Failed anonymous mmap的错误 + +所以这里child_jni_env_ext.get() == nullptr 通常是因为第二步失败,也就是进程虚拟内存地址空间耗尽。所以这就是代号JNIEnv OOM的原因。 + +``` +__BIONIC_ERRDEF( ENOMEM , 12, "Out of memory" ) +__BIONIC_ERRDEF( EMFILE , 24, "Too many open files" ) +``` +当然代号JNIEnv还有一种原因就是FD打开太多,到达了最大限制。 + +4. child_jni_env_ext.get() != nullptr 才会继续 +5. 调用pthread_create创建线程,并返回结果 +看一下[pthread_create](https://android.googlesource.com/platform/bionic.git/+/refs/tags/android-9.0.0_r41/libc/bionic/pthread_create.cpp)的代码 +``` +__BIONIC_WEAK_FOR_NATIVE_BRIDGE +int pthread_create(pthread_t* thread_out, pthread_attr_t const* attr, + void* (*start_routine)(void*), void* arg) { + ErrnoRestorer errno_restorer; + pthread_attr_t thread_attr; + if (attr == NULL) { + pthread_attr_init(&thread_attr); + } else { + thread_attr = *attr; + attr = NULL; // Prevent misuse below. + } + pthread_internal_t* thread = NULL; + void* child_stack = NULL; + // 1. 分配该线程对应的栈内存空间,如果返回result != 0 就直接返回result就说明失败了 + int result = __allocate_thread(&thread_attr, &thread, &child_stack); + if (result != 0) { + return result; + } + // Create a lock for the thread to wait on once it starts so we can keep + // it from doing anything until after we notify the debugger about it + // + // This also provides the memory barrier we need to ensure that all + // memory accesses previously performed by this thread are visible to + // the new thread. + thread->startup_handshake_lock.init(false); + thread->startup_handshake_lock.lock(); + thread->start_routine = start_routine; + thread->start_routine_arg = arg; + thread->set_cached_pid(getpid()); + int flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM | + CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID; + void* tls = reinterpret_cast(thread->tls); +#if defined(__i386__) + // On x86 (but not x86-64), CLONE_SETTLS takes a pointer to a struct user_desc rather than + // a pointer to the TLS itself. + user_desc tls_descriptor; + __init_user_desc(&tls_descriptor, false, tls); + tls = &tls_descriptor; +#endif + // 2. linux系统调用clone,执行真正的创建动作,而这个clone是创建新进程,Unix里面其实只有进程,而线程是POSIX标准定义的,因此这里的clone只是实现线程的一种手段。 clone后父进程和子进程共享内存, 因此当两个进程的内存共享之后,完全就符合“线程”的定义了。 + int rc = clone(__pthread_start, child_stack, flags, thread, &(thread->tid), tls, &(thread->tid)); + if (rc == -1) { + int clone_errno = errno; + // We don't have to unlock the mutex at all because clone(2) failed so there's no child waiting to + // be unblocked, but we're about to unmap the memory the mutex is stored in, so this serves as a + // reminder that you can't rewrite this function to use a ScopedPthreadMutexLocker. + thread->startup_handshake_lock.unlock(); + if (thread->mmap_size != 0) { + munmap(thread->attr.stack_base, thread->mmap_size); + } + // clone失败就会报出clone failed的错误 + async_safe_format_log(ANDROID_LOG_WARN, "libc", "pthread_create failed: clone failed: %s", + strerror(clone_errno)); + return clone_errno; + } + int init_errno = __init_thread(thread); + if (init_errno != 0) { + // Mark the thread detached and replace its start_routine with a no-op. + // Letting the thread run is the easiest way to clean up its resources. + atomic_store(&thread->join_state, THREAD_DETACHED); + __pthread_internal_add(thread); + thread->start_routine = __do_nothing; + thread->startup_handshake_lock.unlock(); + return init_errno; + } + // Publish the pthread_t and unlock the mutex to let the new thread start running. + *thread_out = __pthread_internal_add(thread); + thread->startup_handshake_lock.unlock(); + return 0; +} +``` +上面第一步中__allocate_thread方法的源码为: +``` +static int __allocate_thread(pthread_attr_t* attr, pthread_internal_t** threadp, void** child_stack) { + size_t mmap_size; + uint8_t* stack_top; + if (attr->stack_base == NULL) { + // The caller didn't provide a stack, so allocate one. + // Make sure the stack size and guard size are multiples of PAGE_SIZE. + if (__builtin_add_overflow(attr->stack_size, attr->guard_size, &mmap_size)) return EAGAIN; + if (__builtin_add_overflow(mmap_size, sizeof(pthread_internal_t), &mmap_size)) return EAGAIN; + mmap_size = __BIONIC_ALIGN(mmap_size, PAGE_SIZE); + attr->guard_size = __BIONIC_ALIGN(attr->guard_size, PAGE_SIZE); + // 调用mmap分配栈内存,而mmap分配的内存赋值给了stack_base, stack_base不光是线程执行的栈,其中还存储了线程的其他信息(线程名、ThreadLocal变量等,这些信息都定义在pthread_internal_t结构体中),而这个具体的大小就是前面我们分析的 1M + 8K + 8K = 1040K + attr->stack_base = __create_thread_mapped_space(mmap_size, attr->guard_size); + if (attr->stack_base == NULL) { + return EAGAIN; + } + stack_top = reinterpret_cast(attr->stack_base) + mmap_size; + } else { + // Remember the mmap size is zero and we don't need to free it. + mmap_size = 0; + stack_top = reinterpret_cast(attr->stack_base) + attr->stack_size; + } + // Mapped space(or user allocated stack) is used for: + // pthread_internal_t + // thread stack (including guard) + // To safely access the pthread_internal_t and thread stack, we need to find a 16-byte aligned boundary. + stack_top = reinterpret_cast( + (reinterpret_cast(stack_top) - sizeof(pthread_internal_t)) & ~0xf); + pthread_internal_t* thread = reinterpret_cast(stack_top); + if (mmap_size == 0) { + // If thread was not allocated by mmap(), it may not have been cleared to zero. + // So assume the worst and zero it. + memset(thread, 0, sizeof(pthread_internal_t)); + } + attr->stack_size = stack_top - reinterpret_cast(attr->stack_base); + thread->mmap_size = mmap_size; + thread->attr = *attr; + if (!__init_tls(thread)) { + if (thread->mmap_size != 0) munmap(thread->attr.stack_base, thread->mmap_size); + return EAGAIN; + } + __init_thread_stack_guard(thread); + *threadp = thread; + *child_stack = stack_top; + return 0; +} +``` +而__create_thread_mapped_space方法的源码为 +``` +static void* __create_thread_mapped_space(size_t mmap_size, size_t stack_guard_size) { + // Create a new private anonymous map. + int prot = PROT_READ | PROT_WRITE; + // MAP_ANONYMOUS即匿名内存映射是在Linux中分配大块内存的常用方式。其分配的是虚拟内存,对应页的物理内存并不会立即分配,而是在用到的时候,触发内核的缺页中断,然后中断处理函数再分配物理内存。 + int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE; + void* space = mmap(NULL, mmap_size, prot, flags, -1, 0); + if (space == MAP_FAILED) { + async_safe_format_log(ANDROID_LOG_WARN, + "libc", + "pthread_create failed: couldn't allocate %zu-bytes mapped space: %s", + mmap_size, strerror(errno)); + return NULL; + } + // Stack is at the lower end of mapped space, stack guard region is at the lower end of stack. + // Set the stack guard region to PROT_NONE, so we can detect thread stack overflow. + if (mprotect(space, stack_guard_size, PROT_NONE) == -1) { + async_safe_format_log(ANDROID_LOG_WARN, "libc", + "pthread_create failed: couldn't mprotect PROT_NONE %zu-byte stack guard region: %s", + stack_guard_size, strerror(errno)); + munmap(space, mmap_size); + return NULL; + } + return space; +} +``` + +而对于结果不为0的情况,那就只能是这里mmap分配虚拟内存失败。 +所以代号1040的OOM也是因为虚拟内存分配失败导致的。 + +6. 结果为0才是创建成功 +7. 调用ThrowOutOfMemoryError报出错误信息 +如果child_jni_env_ext.get() == nullptr则报"Could not allocate JNI Env: %s", error_msg.c_str()的错误 +否则如果pthread_create_result != 0则报"pthread_create (%s stack) failed: %s" + + ``` + void Thread::ThrowOutOfMemoryError(const char* msg) { + LOG(ERROR) << StringPrintf("Throwing OutOfMemoryError \"%s\"%s", + msg, (throwing_OutOfMemoryError_ ? " (recursive case)" : "")); + ThrowLocation throw_location = GetCurrentLocationForThrow(); + if (!throwing_OutOfMemoryError_) { + throwing_OutOfMemoryError_ = true; + ThrowNewException(throw_location, "Ljava/lang/OutOfMemoryError;", msg); + throwing_OutOfMemoryError_ = false; + } else { + Dump(LOG(ERROR)); // The pre-allocated OOME has no stack, so help out and log one. + SetException(throw_location, Runtime::Current()->GetPreAllocatedOutOfMemoryError()); + } + } + ``` + + + +最后总结一下: 不管是代号JNIEnv还是1040的OOM都是因为进程内虚拟内存地址空间耗尽导致的。 +在一个32位系统中,如果是4G的内存空间,系统内核将使用最上层的1G虚拟空间,用户空间的内存就只剩下3G或者更少,而创建一个进程需要1040k的虚拟内存,所以假设创建一个线程什么都不干,那最多也只能最大能创建3000个线程。当逻辑地址空间不足(已用逻辑空间地址可以查看 /proc/pid/status中的VmPeak/VmSize查看),就会报出创建线程的OOM问题,`W/libc: pthread_create failed: couldn't allocate 1069056-bytes mapped space: Out of memory +W/art: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Try again"` + + + +而我在项目中遇到的问题是第二个也就是线程过多导致的,Android系统基于linux,所以linux的限制对Android同样实用, + +- cat /proc/sys/kernel/threads-max 规定了每个进程创建线程数量的上限 + +验证:创建大量空线程,不做任何事情,直接sleep. +```java +private Runnable emptyRunnable = new Runnable() { + @Override + public void run() { + try { + for (int i = 0; i < 3000 ; i++) { + Thread.sleep(Long.MAX_VALUE); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +}; +``` + + +连接手机 +``` +adb shell +ps -A -l +``` + +``` +1 S 0 15304 2 0 19 0 - 0 0 ? 00:00:00 kworker/7:2 +5 S 1000 15701 1001 0 19 0 - 1092845 0 ? 00:00:00 ndroid.keychain +5 S 10458 15834 1001 0 29 -10 - 1100514 0 ? 00:00:02 com.example.oom +5 S 10014 15956 1001 0 19 0 - 1106511 0 ? 00:00:01 com.android.mms +1 S 0 16275 2 0 19 0 - 0 0 ? 00:00:00 kworker/1:2 +1 S 0 16542 2 0 19 0 - 0 0 ? 00:00:00 kworker/3:2 +1 S 0 16603 2 0 19 0 - 0 0 ? 00:00:00 kworker/2:2 +``` + +pid为15834 +``` +cat proc/15834/status +``` + +``` +PD1806:/ $ cat proc/15834/status +Name: com.example.oom +Umask: 0077 +State: S (sleeping) +Tgid: 15834 // 进程组的ID +Ngid: 0 +Pid: 15834 // 进程ID +PPid: 1001 // 当前进程的父进程 +TracerPid: 0 // 跟踪当前进程的进程ID,如果是0表示没有跟踪 +Uid: 10458 10458 10458 10458 +Gid: 10458 10458 10458 10458 +FDSize: 128 // 当前分配的文件描述符,这个值不是当前进程使用文件描述符的上线 +Groups: 9997 20458 50458 +VmPeak: 4403108 kB // 当前进程运行过程中所占用内存的峰值 +VmSize: 4402056 kB // 已用逻辑空间地址,虚拟内存大小。整个进程使用虚拟内存大小,是VmLib, VmExe, VmData, 和 VmStk的总和。 +VmLck: 0 kB +VmPin: 0 kB +VmHWM: 49108 kB // 程序得到分配到物理内存的峰值 +VmRSS: 48920 kB // 程序现在正在使用的物理内存 +RssAnon: 9268 kB +RssFile: 39540 kB +RssShmem: 112 kB +VmData: 1737808 kB // 所占用的虚拟内存 +VmStk: 8192 kB // 任务在用户态的栈的大小 (stack_vm) +VmExe: 20 kB // 程序所拥有的可执行虚拟内存的大小,代码段,不包括任务使用的库 (end_code-start_code) +VmLib: 163804 kB // 被映像到任务的虚拟内存空间的库的大小 (exec_lib) +VmPTE: 1000 kB // 该进程的所有页表的大小,单位:kb +VmPMD: 32 kB +VmSwap: 15776 kB +Threads: 17 // 当前的线程数 +SigQ: 0/21568 +SigPnd: 0000000000000000 +ShdPnd: 0000000000000000 +SigBlk: 0000000000001204 +SigIgn: 0000000000000000 +SigCgt: 00000006400084f8 +CapInh: 0000000000000000 +CapPrm: 0000000000000000 +CapEff: 0000000000000000 +CapBnd: 0000000000000000 +CapAmb: 0000000000000000 +Seccomp: 2 +Cpus_allowed: ff +Cpus_allowed_list: 0-7 +Mems_allowed: 1 +Mems_allowed_list: 0 +voluntary_ctxt_switches: 2132 +nonvoluntary_ctxt_switches: 328 + +``` + +``` +当线程数(可以在/proc/pid/status 中的threads项实时查看)超过/proc/sys/kernel/threads-max 中规定的上限时产生 OOM 崩溃。 +``` + +## 定位验证方法: + +Thread.UncaughtExceptionHandler捕获到OutOfMemoryError时记录/proc/pid目录下的如下信息: + +- /proc/pid/fd目录下文件数(fd数) +- /proc/pid/status中threads项(当前线程数目) +- 当前设备的内存信息 +- OOM的日志信息(出了堆栈信息还包含其他的一些warning信息 +- 在灰度版本中通过一个定时器10分钟dump出应用所有的线程,当线程数超过一定阈值时,将当前的线程上报并预警,通过对这种异常情况的捕捉 + + + +## 什么情况下虚拟内存地址空间才会耗尽 + + +说面分析了那么多,结论就是因为虚拟内存空间耗尽导致的,但是究竟什么情况才会出现耗尽的情况? + +内存是程序运行时的存储地址空间,可分为虚拟地址空间和物理地址空间。虚拟地址空间是相对进程而言的,每个进程都有独立的地址空间(如32位程序都有4GB的虚拟地址空间)。物理地址空间就是由硬件(内存条)提供的存储空间,物理地址空间被所有进程共享。 + +Linux采用虚拟内存管理技术,每个进程都有各自独立的进程地址空间(即4G的线性虚拟空间),无法直接访问物理内存。这样起到保护操作系统,并且让用户程序可使用比实际物理内存更大的地址空间。 + +4G进程地址空间被划分两部分,内核空间和用户空间。用户空间(包括代码、数据、堆、共享库以及栈)从0到3G,内核空间(包括内核中的代码和数据结构)从3G到4G; + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/vm_linux.png?raw=true) + +用户进程通常情况只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等情况可访问到内核空间; +用户空间对应进程,所以当进程切换,用户空间也会跟着变化; +内核空间是由内核负责映射,不会跟着进程变化;内核空间地址有自己对应的页表,用户进程各自有不同额页表。 + +从程序角度看,我们谈到的地址空间一般是虚拟地址空间,通过malloc或new分配的内存都虚拟地址空间的内存。虚拟地址空间与物理地址空间的都是以page为最小管理单元,page的大小因系统而异,一般都是4KB。虚拟地址空间有到物理地址空间的映射,如果要访问的虚拟地址空间没有映射到物理地址空间,操作系统会产生缺页中断,将虚拟地址空间映射到物理地址空间。 + +因此,程度的虚拟地址空间比物理的地址空间要大的多。在较多进程同时运行时,物理地址空间有可能不够,操作系统会将一部物理地址空间的内容交换到磁盘,从而腾挪出一部分物理地址空间来。磁盘上的交换区,在linux上叫swap area,windows时叫page file。 + +android底层基于linux,不过android是没有交换区的(为什么没有?),所以android系统的内存资源就更加宝贵。为更合理、充分利用有限内存资源,android引入一个low-memory-killer机制,在内存不足,根据规则回收一部分低优先级的进程,从而释放他们占有的内存。 + +进程的内存空间只是虚拟内存,而程序运行需要的是物理内存(ram),在必要时,操作系统会将程序运行中申请的虚拟内存映射到ram,让进程能够使用物理内存。进程所操作的空间都是虚拟地址空间,无法直接操作ram。java程序发生OOM并不表示ram不足,如果ram真的不足,android的memory killer就会发挥作用,它会杀死一些优先级比较低的进程来释放物理内存,让高优先级程序得到更多的内存。 + + +Android系统给每个进程分配了一定的虚拟地址空间大小,进程使用的虚拟空间如果超过阈值,就会触发OOM。所以只可能是线程太多,消耗了大部分虚拟内存地址空间,从而引发了当前进程空间不足。 + +`adb shell dumpsys meminfo packagename` 可以查看占用的内存信息 +``` +PD1806:/system/bin $ dumpsys meminfo com.example.oom +Applications Memory Usage (in Kilobytes): +Uptime: 31807223 Realtime: 31807223 + +** MEMINFO in pid 11380 [com.example.oom] ** + Pss Private Private SwapPss Heap Heap Heap + Total Dirty Clean Dirty Size Alloc Free + ------ ------ ------ ------ ------ ------ ------ + Native Heap 64227 64176 0 28 77824 72483 5340 + Dalvik Heap 2158 2124 0 24 3590 2693 897 + Dalvik Other 20804 20804 0 0 + Stack 92 92 0 0 + Ashmem 2 0 0 0 + Gfx dev 892 892 0 0 + Other dev 12 0 12 0 + .so mmap 8595 212 6180 16 + .apk mmap 2388 1964 60 0 + .ttf mmap 105 0 0 0 + .dex mmap 2375 176 552 0 + .oat mmap 176 0 112 0 + .art mmap 6796 6356 120 0 + Other mmap 60 4 4 0 + EGL mtrack 29808 29808 0 0 + GL mtrack 3000 3000 0 0 + Unknown 44350 44332 0 1 + TOTAL 185909 173940 7040 69 81414 75176 6237 + + App Summary + Pss(KB) + ------ + Java Heap: 8600 + Native Heap: 64176 + Code: 9256 + Stack: 92 + Graphics: 33700 + Private Other: 65156 + System: 4929 + + TOTAL: 185909 TOTAL SWAP PSS: 69 + + Objects + Views: 27 ViewRootImpl: 1 + AppContexts: 5 Activities: 1 + Assets: 7 AssetManagers: 0 + Local Binders: 14 Proxy Binders: 31 + Parcel memory: 4 Parcel count: 20 + Death Recipients: 1 OpenSSL Sockets: 0 + WebViews: 0 + + SQL + MEMORY_USED: 0 + PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0 +``` + +查看当前手机的内存信息可以通过`cat /proc/meminfo`来查看 +``` +1|PD1806:/ $ cat /proc/meminfo +MemTotal: 5772000 kB +MemFree: 129500 kB +MemAvailable: 2594764 kB +Buffers: 3968 kB +Cached: 2330100 kB +SwapCached: 12780 kB +Active: 2678740 kB +Inactive: 759120 kB +Active(anon): 804284 kB +Inactive(anon): 303532 kB +Active(file): 1874456 kB +Inactive(file): 455588 kB +Unevictable: 3500 kB +Mlocked: 3500 kB +SwapTotal: 2097148 kB +SwapFree: 488020 kB +Dirty: 60 kB +Writeback: 0 kB +AnonPages: 1102064 kB +Mapped: 743796 kB 映射文件大小 +Shmem: 1416 kB +Slab: 548448 kB +SReclaimable: 241428 kB +SUnreclaim: 307020 kB +KernelStack: 171856 kB +PageTables: 108432 kB +NFS_Unstable: 0 kB +Bounce: 0 kB +WritebackTmp: 0 kB +CommitLimit: 4983148 kB // 请的内存总数超过这个阈值就算overcommit,CommitLimit 就是overcommit的阈值,申请的内存总数超过CommitLimit的话就算是overcommit。 +Committed_AS: 131533804 kB // 表示所有进程已经申请的内存总大小,(注意是已经申请的,不是已经分配的),如果 Committed_AS 超过 CommitLimit 就表示发生了 overcommit,超出越多表示 overcommit 越严重。Committed_AS 的含义换一种说法就是,如果要绝对保证不发生OOM (out of memory) 需要多少物理内存。 +VmallocTotal: 263061440 kB +VmallocUsed: 0 kB +VmallocChunk: 0 kB +CmaTotal: 217088 kB +CmaFree: 1740 kB +NR_KMALLOC: 23312 kB +NR_VMALLOC: 33844 kB +NR_DMA_NOR: 0 kB +NR_DMA_CMA: 58348 kB +NR_ION: 268600 kB +free_ion: 121060 kB +free_ion_pool: 121060 kB +free_ion_heap: 0 kB +NR_GPU: 267812 kB +free_gpu: 154260 kB +zram_size: 609440 kB +zcache_size: 0 kB +pcppages: 6944 kB +ALL_MEM: 5675448 kB +``` + + +- [Virtual Memory and Linux](https://events.linuxfoundation.org/sites/events/files/slides/elc_2016_mem.pdf) +- [Android进程的内存管理分析](https://blog.csdn.net/gemmem/article/details/8920039) +- [Android系统匿名共享内存(Anonymous Shared Memory)C++调用接口分析](https://blog.csdn.net/luoshengyang/article/details/6939890) +- [Android系统匿名共享内存Ashmem(Anonymous Shared Memory)驱动程序源代码分析](https://blog.csdn.net/luoshengyang/article/details/6664554) +- [Android系统匿名共享内存Ashmem(Anonymous Shared Memory)简要介绍和学习计划](https://blog.csdn.net/luoshengyang/article/details/6651971) +- [虚拟内存那点事](https://sylvanassun.github.io/2017/10/29/2017-10-29-virtual_memory/ ) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + 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 44c7cb32..00000000 --- "a/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" +++ /dev/null @@ -1,159 +0,0 @@ -集成(一) -=== - - -首先在`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" -} -``` - ---- - -- 邮箱 :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 205df86a..00000000 --- "a/ArchitectureComponents/3.Lifecycle(\344\270\211).md" +++ /dev/null @@ -1,173 +0,0 @@ -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`。否则会造成内存泄露。 - - - - ---- - -- 邮箱 :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 a1f97f42..00000000 --- "a/ArchitectureComponents/4.LiveData(\345\233\233).md" +++ /dev/null @@ -1,323 +0,0 @@ -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`就可以在任意数据来源更新时收到通知。 - - - - - - - - - - - - - - - - - ---- - -- 邮箱 :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 a7bf84f5..00000000 --- "a/ArchitectureComponents/5.ViewModel(\344\272\224).md" +++ /dev/null @@ -1,134 +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`去获取详细的信息。这些详细的信息应该被存放在数据库中。 - - - - - - ---- - -- 邮箱 :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/\345\217\215\347\274\226\350\257\221.md" "b/BasicKnowledge/\345\217\215\347\274\226\350\257\221.md" new file mode 100644 index 00000000..51a0aeee --- /dev/null +++ "b/BasicKnowledge/\345\217\215\347\274\226\350\257\221.md" @@ -0,0 +1,88 @@ +反编译 +=== + +- [资源文件获取Apktool](https://ibotpeaches.github.io/Apktool/install/) + 按照官网的指示配置完成后,执行apktool命令 + + ``` + apktool d xxx.apk + // 如果提示-bash: /usr/local/bin/apktool: Permission denied + cd /usr/local/bin + sudo chmod +x apktool + sudo chmod +x apktool.jar + ``` + 执行完成后会在apktool.jar的目录下升级一个文件,里面可以看到xml的信息 + ``` + cd /usr/loca/bin + open . + ``` +- [源码文件获取dex2jar](https://github.com/pxb1988/dex2jar) + 将apk文件解压后获取class.dex文件,然后将dex文件拷贝到dex2jar目录中 + ``` + sh d2j-dex2jar.sh classes.dex + d2j-dex2jar.sh: line 36: ./d2j_invoke.sh: Permission denied + // chmod一下 + sudo chmod +x d2j_invoke.sh + sh d2j-dex2jar.sh classes.dex + ``` + 执行完成后会在当前目录中生成一个 classes-dex2jar.jar + +- [jar包源码查看工具jd-gui](https://github.com/java-decompiler/jd-gui) + 里面有osx的版本,安装后直接打开上面用dex2jar编译出来的.jar文件就可以查看源码了 + + +### 反编译后的源码修改 + +修改代码及资源,最好的方式是修改apktool反编译后的资源级smali代码。JD-GUI查看的java代码不适宜修改,因为修改后还需要重新转换成smali,才能重新编译打包会apk。 +至于smali的修改,则要学习smali语言的语法了,smali是一种类似汇编语言的语言,具体语法可自行上网学习。 +在smali文件夹中找到与具体类对应的smali文件,然后进行修改 +可以用到[java2smali](https://plugins.jetbrains.com/plugin/7385-java2smali)将java代码转换成smali代码 + +### 重新打包 + +``` +B0000000134553m:bin xuchuanren$ apktool b xxxfilename +I: Using Apktool 2.4.0 +I: Checking whether sources has changed... +I: Smaling smali folder into classes.dex... +I: Checking whether resources has changed... +I: Building resources... +S: WARNING: Could not write to ( instead... +S: Please be aware this is a volatile directory and frameworks could go missing, please utilize --frame-path if the default storage directory is unavailable +I: Copying libs... (/lib) +I: Building apk file... +I: Copying unknown files/dir... +I: Built apk... + +``` +生成的apk在该文件xxxfilename中的dist目录中 + +### 重新签名 + +重新签名用的是jarsigner命令 +``` +jarsigner -verbose -keystore [your_key_store_path] -signedjar [signed_apk_name] [usigned_apk_name] [your_key_store_alias] -digestalg SHA1 -sigalg MD5withRSA +``` + +- [your_key_store_path]:密钥所在位置的绝对路径 +- [signed_apk_name]:签名后安装包名称 +- [usigned_apk_name]:未签名的安装包名称 +- [your_key_store_alias]:密钥的别名 + +如: + +``` +jarsigner -verbose -keystore /development/key.keystore -signedjar signed_apk.apk xxx.apk charon -digestalg SHA1 -sigalg MD5withRSA +Enter Passphrase for keystore: + adding: META-INF/MANIFEST.MF + adding: META-INF/CHARON.SF + adding: META-INF/CHARON.RSA + +``` +执行完后会在当前目录下生成签名后的apk文件 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! 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/.DS_Store b/JavaKnowledge/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/JavaKnowledge/.DS_Store and /dev/null differ 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 3e20b446..2d427891 100644 --- "a/JavaKnowledge/Git\347\256\200\344\273\213.md" +++ "b/JavaKnowledge/Git\347\256\200\344\273\213.md" @@ -1,47 +1,54 @@ Git简介 -=== - -`Git`和其他版本控制系统(包括`Subvversion`及其他相似的工具)的主要差别在于`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)` @@ -49,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)` +- 暂存区`(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 . // 把所有文件放入暂存区 @@ -91,333 +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`:重设`(reset)`索引和工作目录,自从``以来在工作目录中的任何改变都被丢弃,并把`HEAD`指向``。会将其之后的修改全部撤回,并且会影响到工作区 - - `--mixed`改变分支和暂存区,不影响工作区 - - `soft`只改变分支的提交 - - 下面是具体一个例子,假设有三个`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"`这时候你意识到了错误,想要撤销,可以执行: - - ``` - + + + + + + + + + + ... + +``` + +```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 + + + +