From 601de423eefa7e58cd6f5bae81bbc76fc87aa1cf Mon Sep 17 00:00:00 2001 From: RyoJerryYu Date: Sun, 14 Apr 2024 19:22:18 +0000 Subject: [PATCH] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@=20RyoJerry?= =?UTF-8?q?Yu/blog-next@7d9966d601c44e3d838fba8d1aa74012b0e09d09=20?= =?UTF-8?q?=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 404.html | 2 +- .../articles.json | 2 +- .../articles/Building-this-blog.json | 0 .../articles/Handy-heap-cheat-sheet.json | 1 + .../articles/Sort-algorithm.json | 0 .../articles/The-beauty-of-design-parten.json | 0 .../articles/create-blog-cicd-by-github.json | 0 .../articles/graph-for-economics-1.json | 0 .../articles/graph-for-economics-2.json | 0 .../articles/hello-world.json | 0 .../articles/init-a-new-hexo-project.json | 0 .../articles/introduction-for-k8s-2.json | 0 .../articles/introduction-for-k8s.json | 0 .../articles/python-dict.json | 0 .../articles/the-using-in-cpp.json | 0 .../use-paste-image-and-vscode-memo.json | 0 .../articles/why-homogeneous.json | 0 .../clips.json | 0 .../ideas.json | 0 .../ideas/blog-in-next.json | 0 .../ideas/blog-syntax.json | 0 .../ideas/first-idea.json | 0 .../ideas/newest.json | 0 .../ideas/using-chart-js.json | 0 .../index.json | 0 .../tags.json | 0 .../tags/aws.json | 0 .../tags/blog.json | 0 .../tags/c++.json | 0 .../tags/ci-cd.json | 0 .../tags/cloud-computing.json | 0 .../tags/cloud-native.json | 0 .../tags/devops.json | 0 .../tags/docker.json | 0 .../tags/github.json | 0 .../tags/hexo.json | 0 .../tags/iac.json | 0 .../tags/javascript.json | 0 .../tags/kubernetes.json | 0 .../tags/nextjs.json | 0 .../tags/python.json | 0 .../tags/vscode.json | 0 .../tags/\346\216\222\345\272\217.json" | 0 ...\346\215\256\347\273\223\346\236\204.json" | 1 + .../tags/\346\235\202\346\212\200.json" | 0 .../tags/\346\235\202\350\260\210.json" | 0 .../tags/\347\254\224\350\256\260.json" | 0 .../tags/\347\256\227\346\263\225.json" | 1 + ...\346\263\225\347\253\236\350\265\233.json" | 1 + ...\350\256\241\346\250\241\345\274\217.json" | 0 .../articles/Handy-heap-cheat-sheet.json | 1 - ...\346\215\256\347\273\223\346\236\204.json" | 1 - .../tags/\347\256\227\346\263\225.json" | 1 - ...\346\263\225\347\253\236\350\265\233.json" | 1 - .../_buildManifest.js | 0 .../_ssgManifest.js | 0 articles.html | 2 +- articles/Building-this-blog.html | 4 +- articles/Handy-heap-cheat-sheet.html | 699 ++++++++---------- articles/Sort-algorithm.html | 4 +- articles/The-beauty-of-design-parten.html | 4 +- articles/create-blog-cicd-by-github.html | 4 +- articles/graph-for-economics-1.html | 4 +- articles/graph-for-economics-2.html | 4 +- articles/hello-world.html | 4 +- articles/init-a-new-hexo-project.html | 4 +- articles/introduction-for-k8s-2.html | 4 +- articles/introduction-for-k8s.html | 4 +- articles/python-dict.html | 4 +- articles/the-using-in-cpp.html | 4 +- articles/use-paste-image-and-vscode-memo.html | 4 +- articles/why-homogeneous.html | 4 +- clips.html | 2 +- .../2021-03-21-Handy-heap-cheat-sheet.md | 18 +- ideas.html | 2 +- ideas/blog-in-next.html | 4 +- ideas/blog-syntax.html | 4 +- ideas/first-idea.html | 4 +- ideas/newest.html | 4 +- ideas/using-chart-js.html | 4 +- index.html | 2 +- sitemap.xml | 98 +-- tags.html | 2 +- tags/aws.html | 2 +- tags/blog.html | 2 +- tags/c++.html | 2 +- tags/ci-cd.html | 2 +- tags/cloud-computing.html | 2 +- tags/cloud-native.html | 2 +- tags/devops.html | 2 +- tags/docker.html | 2 +- tags/github.html | 2 +- tags/hexo.html | 2 +- tags/iac.html | 2 +- tags/javascript.html | 2 +- tags/kubernetes.html | 2 +- tags/nextjs.html | 2 +- tags/python.html | 2 +- tags/vscode.html | 2 +- "tags/\346\216\222\345\272\217.html" | 2 +- ...\346\215\256\347\273\223\346\236\204.html" | 2 +- "tags/\346\235\202\346\212\200.html" | 2 +- "tags/\346\235\202\350\260\210.html" | 2 +- "tags/\347\254\224\350\256\260.html" | 2 +- "tags/\347\256\227\346\263\225.html" | 2 +- ...\346\263\225\347\253\236\350\265\233.html" | 2 +- ...\350\256\241\346\250\241\345\274\217.html" | 2 +- 107 files changed, 444 insertions(+), 517 deletions(-) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles.json (70%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/Building-this-blog.json (100%) create mode 100644 _next/data/06cVAX8N9Z8KmESdU4cc2/articles/Handy-heap-cheat-sheet.json rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/Sort-algorithm.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/The-beauty-of-design-parten.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/create-blog-cicd-by-github.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/graph-for-economics-1.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/graph-for-economics-2.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/hello-world.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/init-a-new-hexo-project.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/introduction-for-k8s-2.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/introduction-for-k8s.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/python-dict.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/the-using-in-cpp.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/use-paste-image-and-vscode-memo.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/articles/why-homogeneous.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/clips.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/ideas.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/ideas/blog-in-next.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/ideas/blog-syntax.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/ideas/first-idea.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/ideas/newest.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/ideas/using-chart-js.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/index.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/aws.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/blog.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/c++.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/ci-cd.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/cloud-computing.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/cloud-native.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/devops.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/docker.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/github.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/hexo.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/iac.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/javascript.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/kubernetes.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/nextjs.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/python.json (100%) rename _next/data/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/tags/vscode.json (100%) rename "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\216\222\345\272\217.json" => "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\216\222\345\272\217.json" (100%) create mode 100644 "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\225\260\346\215\256\347\273\223\346\236\204.json" rename "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\235\202\346\212\200.json" => "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\235\202\346\212\200.json" (100%) rename "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\235\202\350\260\210.json" => "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\235\202\350\260\210.json" (100%) rename "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\254\224\350\256\260.json" => "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\254\224\350\256\260.json" (100%) create mode 100644 "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\256\227\346\263\225.json" create mode 100644 "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\256\227\346\263\225\347\253\236\350\265\233.json" rename "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\350\256\276\350\256\241\346\250\241\345\274\217.json" => "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\350\256\276\350\256\241\346\250\241\345\274\217.json" (100%) delete mode 100644 _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/Handy-heap-cheat-sheet.json delete mode 100644 "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\225\260\346\215\256\347\273\223\346\236\204.json" delete mode 100644 "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\256\227\346\263\225.json" delete mode 100644 "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\256\227\346\263\225\347\253\236\350\265\233.json" rename _next/static/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/_buildManifest.js (100%) rename _next/static/{PqrPNWMG-77f9Y8Ei6sRo => 06cVAX8N9Z8KmESdU4cc2}/_ssgManifest.js (100%) diff --git a/404.html b/404.html index 56aa02e2..edb5eaf6 100644 --- a/404.html +++ b/404.html @@ -1 +1 @@ -404: This page could not be found

404

This page could not be found.

\ No newline at end of file +404: This page could not be found

404

This page could not be found.

\ No newline at end of file diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles.json similarity index 70% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles.json index e13e51e9..c979939c 100644 --- a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles.json +++ b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles.json @@ -1 +1 @@ -{"pageProps":{"posts":[{"slug":"introduction-for-k8s-2","file":"public/content/articles/2022-08-20-introduction-for-k8s-2.md","mediaDir":"content/articles/2022-08-20-introduction-for-k8s-2","path":"/articles/introduction-for-k8s-2","meta":{"content":"\n我们之前说的都是用于部署 Pod 的资源,我们接下来介绍与创建 Pod 不相关的资源:储存与网络。\n\n# 储存\n\n其实我们之前已经接触过储存相关的内容了:在讲 Stateful Set 时我们提过 Stateful Set 创建出来的 Pod 都会有相互独立的储存;而讲 Daemon Set 时我们提到 K8s 推荐只在 Daemon Set 的 Pod 中访问宿主机磁盘。但独立的储存具体指什么?除了访问宿主机磁盘以外还有什么其他的储存?\n\n在 Docker 中,我们可以把宿主机磁盘上的一个路径作为一个 Volume 来给容器绑定,或者直接使用 Docker Engine 管理的 Volume 来提供持久化存储或是容器间共享文件。在 K8s 里面也沿用了 Volume 这个概念,可以通过 Mount 绑定到容器内的路径,并通过实现 CSI 的各种引擎来提供更多样的存储。\n\n> CSI: Container Storage Interface ,容器储存接口标准,是 K8s 提出的一种规范。不管是哪种储存引擎,只要编写一个对应的插件实现 CSI ,都可以在 K8s 中使用。\n\n### K8s 中使用 Volume 与可用的 Volume 类型\n\n其实 K8s 中使用 Volume 的例子我们一开始就已经接触过了。还记得一开始介绍 Pod 时的 Nginx 例子吗?\n\n```yaml\nmetadata:\n name: simple-webapp\nspec:\n containers:\n - name: main-application\n image: nginx\n volumeMounts:\n - name: shared-logs\n mountPath: /var/log/nginx\n - name: sidecar-container\n image: busybox\n command: [\"sh\",\"-c\",\"while true; do cat /var/log/nginx/access.log; sleep 30; done\"]\n volumeMounts:\n - name: shared-logs\n mountPath: /var/log/nginx\n volumes:\n - name: shared-logs\n emptyDir: {}\n```\n\n这个 Pod 描述中声明了一个种类为 `emptyDir` 的,名为 `shared-logs` 的 Volume ,然后 Pod 中的两个容器都分别 Mount 了这个 Volume 。\n\nK8s 中默认提供了几种 Volume ,比如:\n\n- emptyDir :一个简单的空目录,一般用于储存临时数据或是 Pod 的容器之间共享数据。\n- hostPath :绑定到节点宿主机文件系统上的路径,一般在 Daemon Set 中使用。\n- gitRepo :这种 Volume 其实相当于 emptyDir ,不过在 Pod 启动时会从 Git 仓库 clone 一份内容作为默认数据。\n- configMap 、 secret :一般用于配置文件加载,需要与 configMap 、 secret 这两种资源一同使用。会将 configMap 、 secret 中对应的内容拷贝一份作为 Volume 绑到容器。(下一节中会展开讨论)\n- nfs 、 glusterfs 、 ……:可以通过各种网络存储协议直接挂载一个网络存储\n- (deprecated!) gcePersistentDisk 、 awsElasticBlockStore ……:可以调用各个云平台的 API ,创建一个块储存硬件挂载到宿主机上,再将那个硬件挂载到容器中。\n- persistentVolumeClaim :持久卷声明,用于把实际储存方式抽象化,使得 Pod 不需要关心具体的储存类型。这种类型会在下面详细介绍。\n\n我们可以注意到, Volume 的声明是 Pod 的一个属性,而不是一种单独的资源。 Volume 是 Pod 的一部分,因此不同的 Pod 之间永远不可能共享同一个 Volume 。\n\n> 但是 Volume 所指向的位置可以相同,比如 HostPath 类型的 Volume 就可以两个 Pod 可以绑定到宿主机上同一个路径,因此 Volume 里的数据还是能通过一定方式在 Pod 间共享。但当然 K8s 不推荐这么做。\n\n另外,由于 Volume 是 Pod 的一部分, Volume 的生命周期也是跟随 Pod 的,当一个 Pod 被销毁时, Volume 也会被销毁,因此最主要还是用于 Pod 内容器间的文件共享。如果需要持久化储存,需要使用 Persistent Volume 。\n\n> Volume 会被销毁不代表 Volume 指向的内容会被销毁。比如 hostPath 、 NFS 等类型 Volume 中的内容就会继续保留在宿主机或是 NAS 上。下面提到的 Presistent Volume Claim 也是,拥有 `persistentVolumeClaim` 类型 Volume 的 Pod 被删除后对应的 PVC 不一定会被删除。\n\n### Presistent Volume 、 Presistent Volume Claim 、 Storage Class\n\n如果需要在 Pod 声明中直接指定 NFS 、 awsElasticBlockStore 之类的信息,就需要应用的开发人员对真实可用的储存结构有所理解,违背了 K8s 的理念。因此 K8s 就弄出了小标题中的三种资源来将储存抽象化。\n\n一个 Persistent Volume (PV) 对应云平台提供的一个块存储,或是 NAS 上的一个路径。可以简单地理解为 **PV 直接描述了一块可用的物理存储** 。因为 PV 直接对应到硬件,因此 PV 跟节点一样,是名称空间无关的。\n\n而一个 **Persistent Volume Claim (PVC) 则是描述了怎样去使用储存** :使用多少空间、只读还是读写等。一个 PVC 被创建后会且只会对应到一个 PV 。 PVC 从属于一个名称空间,并能被该名称空间下的 Pod 指定为一个 Volume 。\n\nPV 与 PVC 这两种抽象是很必要的。试想一下用自己的物理机搭建一个 K8s 集群的场景。你会提前给物理机插上许多个储存硬件,这时你就需要用 PV 来描述这些硬件,之后才能在 K8s 里利用这些硬件的储存。而实际将应用部署到 K8s 中时,你才需要用 PVC 来描述 Pod 中需要怎么样的储存卷,然后 K8s 就会自动挑一个合适 PV 给这个 PVC 绑定上。这样实际部署应用的时候就不用再特意跑去机房给物理机插硬件了。\n\n但是现在都云原生时代了,各供应商都有提供 API 可以直接创建一个块储存,还要想办法提前准备 PV 实在是太蠢了。于是便需要 Storage Class 这种资源。\n\n使用 Storage Class 前需要先安装各种云供应商提供的插件(当然使用云服务提供的 K8s 的话一般已经准备好了),然后再创建一个 Storage Class 类型的资源(当然一般也已经准备好了)。下面是 AWS 上的 EKS 服务中默认自带的 Storage Class :\n\n```yaml\napiVersion: storage.k8s.io/v1\nkind: StorageClass\nmetadata:\n annotations:\n storageclass.kubernetes.io/is-default-class: \"true\"\n name: gp2\nprovisioner: kubernetes.io/aws-ebs\nparameters:\n fsType: ext4\n type: gp2\n# 当 PVC 被删除时会同时删除 PV\nreclaimPolicy: Delete\n# 只有当 PVC 被绑定为一个 Pod 的 Volume 时才会创建一个 PV\nvolumeBindingMode: WaitForFirstConsumer\n```\n\n可以看到 EKS 自带的 gp2 提供了一些默认的选项,我们也可以类似地去定义自己的 Storage Class 。有了 gp2 这个 Storage Class ,我们创建一个 PVC 后 K8s 就会调用 AWS 的 API ,创建一个块储存接到我们的节点上,然后 K8s 再自动创建一个 PV 并绑定到 PVC 上。\n\n例如,我们部署 Kafka 时会创建一个这样的 PVC :\n\n```yaml\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: data-kafka-0\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 10Gi\n storageClassName: gp2\n```\n\nK8s 就会自动为我们创建出一个对应的 PV :\n\n```sh\n# `pvc-` 开头这个是 AWS 自动给我们起的名字。它虽然是 `pvc-` 开头,但他其实是一个 PV 。\nNAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE\npvc-3614c15f-5697-4d66-a13c-6ddf7eb89998 10Gi RWO Delete Bound kafka/data-kafka-0 gp2 152d\n```\n\n要是打开 AWS Console 还会发现, K8s 调用了 AWS 的 API ,自动为我们创建了一个 EBS 块储存并绑定到了我们对应的宿主机上。\n\n可以用下面这张图来表示 Pod 中的 Volume 、 PVC 、 PV 之间的关系:\n\n```mermaid\nflowchart TD\n\nsubgraph Pod[Pod: Kafka-0]\nsubgraph Container[Container: docker.io/bitnami/kafka:3.1.0]\nvm[VolumeMount: /bitnami/kafka]\nend\nvolume[(Volume: data)]\nvm --> volume\nend\n\npvc[pvc: data-kafka-0]\npv[pv: pvc-3614c15f-5697-4d66-a13c-6ddf7eb89998]\nebs[ebs: AWS 为我们创建的块储存硬件]\n\nvolume --> pvc\npvc --> pv\npv --> ebs\n```\n\n而 Storage Class 在上图中则负责读取我们提交的 PVC ,然后创建 PV 与 EBS 。\n\n### 再说回 Stateful Set\n\n之前我们提到 Stateful Set 时说到 Stateful Set 创建的 Pod 拥有固定的储存,到底是什么意思呢?跟 Deployment 的储存又有什么区别呢?\n\n我们先来看看,如果要给 Deployment 创建出来的 Pod 挂载 PVC 需要怎么做。下面是一个部署 Nginx 的 Deployment 清单,其中 html 目录下的静态文件存放在 NFS 里,通过 PVC 挂载到 Pod 中:\n\n```yaml\n# 这里省略了 Service 相关的内容\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: nginx-dpl-with-nfs-pvc\nspec:\n replicas: 3\n selector:\n matchLabels:\n app: nginx\n template:\n metadata:\n labels:\n app: nginx\n spec:\n containers:\n - name: nginx\n image: nginx:alpine\n ports:\n - containerPort: 80\n name: web\n volumeMounts: #挂载容器中的目录到 pvc nfs 中的目录\n - name: www\n mountPath: /usr/share/nginx/html\n volumes:\n - name: www\n persistentVolumeClaim: #指定pvc\n claimName: nfs-pvc-for-nginx\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: nfs-pvc-for-nginx\n namespace: default\nspec:\n storageclassname: \"\" # 指定使用现有 PV ,不使用 StorageClass 创建 PV\n accessModes:\n - ReadWriteMany\n resources:\n requests:\n storage: 1Gi\n---\n# 这个例子中需要挂载 NFS 上的特定路径,所以手动定义了一个 PV\n# 一般情况下我们不会手动创建 PV,而是使用 StorageClass 自动创建\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n name: nfs-pv-for-nginx\nspec:\n capacity: \n storage: 1Gi\n accessModes:\n - ReadWriteMany\n persistentVolumeReclaimPolicy: Retain\n nfs:\n path: /nfs/sharefolder/nginx\n server: 81.70.4.171\n```\n\n这份清单我们主要关注前两个资源,我们可以看到除了一个 Deployment 资源以外我们还单独定义了一个 PVC 资源。然后在 Deployment 的 Pod 模板中声明并绑定了这个 PVC 。\n\n可这样 apply 了之后会发生什么情况呢?因为我们只声明了一份 PVC ,当然我们只会拥有一个 PVC 资源。但这个 Deployment 的副本数是 3 ,因此我们会有 3 个相同的 Pod 去绑定同一个 PVC 。也就是最终会在 3 个容器里访问同一个 NFS 的同一个目录。如果我们在其中一个容器里对这个目录作修改,也会影响到另外两个容器。\n\n> 注:这一现象不一定在任何情况下都适用。比如 AWS 的 EBS 卷只支持单个 AZ 内的绑定。如果 Pod 因为 Node Affinity 等设定被部署到了多个区,没法绑定同一个 EBS 卷,就会在 Scedule 的阶段报错。\n\n很多时候我们都不希望多个 Pod 绑定到同一 PVC 。比如我们部署一个 DB 集群的时候,如果好不容易部署出来的多个实例居然用的是同一份储存,就会显得很呆。 Stateful Set 就是为了解决这种情况,会为其管理下的每个 Pod 都部署一个专用的 PVC 。\n\n下面是给 Stateful Set 创建出来的 Pod 挂载 PVC 的一份清单:\n\n```yaml\n# 这里省略了 Service 相关的内容\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: web\nspec:\n serviceName: \"nginx\"\n replicas: 2\n selector:\n matchLabels:\n app: nginx\n template:\n metadata:\n labels:\n app: nginx\n spec:\n containers:\n - name: nginx\n image: k8s.gcr.io/nginx-slim:0.8\n ports:\n - containerPort: 80\n name: web\n volumeMounts:\n - name: www\n mountPath: /usr/share/nginx/html\n volumeClaimTemplates:\n - metadata:\n name: www\n spec:\n accessModes: [ \"ReadWriteOnce\" ]\n resources:\n requests:\n storage: 1Gi\n```\n\n我们可以看到,部署 Stateful Set 时我们不能另外单独定义一份 PVC 了,只能作为 Stateful Set 定义的一部分,在 volumeClaimTemplates 字段中定义 PVC 的模板。这样一来, Stateful Set 会根据这个模板,为每个 Pod 创建一个对应的 PVC ,并作为 Pod 的 Volume 绑定上:\n\n```bash\n# Stateful Set 创建出来的 Pod ,名字都是按顺序的\n$ kubectl get pods -l app=nginx\nNAME READY STATUS RESTARTS AGE\nweb-0 1/1 Running 0 1m\nweb-1 1/1 Running 0 1m\n\n# Stateful Set 创建出来的 PVC ,名字与 Pod 的名字一一对应\n$ kubectl get pvc -l app=nginx\nNAME STATUS VOLUME CAPACITY ACCESSMODES AGE\nwww-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s\nwww-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s\n```\n\n这样, Stateful Set 的多个 Pod 就会拥有自己的储存,不会相互打架了。另外,如果我们事先定义了 StorageClass ,还能根据 Stateful Set 的副本数动态配置 PV 。\n\n### ConfigMap 与 Secret 挂载作为特殊的卷\n\n有时候我们需要使用配置文件来配置应用(比如 Nginx 的配置文件),而且我们有时候会需要不重启 Pod 就热更新配置。如果用 PVC 来加载配置文件略微麻烦,这时候可以使用 Config Map 。\n\n下面是 K8s 官网上 Config Map 的一个例子:\n\n```yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: game-demo\ndata:\n # 一个 Key 可以对应一个值\n player_initial_lives: \"3\"\n ui_properties_file_name: \"user-interface.properties\"\n\n # 一个 Key 也可以对应一个文件的内容\n game.properties: |\n enemy.types=aliens,monsters\n player.maximum-lives=5 \n user-interface.properties: |\n color.good=purple\n color.bad=yellow\n allow.textmode=true \n---\napiVersion: v1\nkind: Pod\nmetadata:\n name: configmap-demo-pod\nspec:\n containers:\n - name: demo\n image: alpine\n command: [\"sleep\", \"3600\"]\n env:\n # ConfigMap 的 Key 可以作为环境变量引用\n - name: PLAYER_INITIAL_LIVES\n valueFrom:\n configMapKeyRef:\n name: game-demo # 从这个 Config Map 里\n key: player_initial_lives # 拿到这个 key 的值\n - name: UI_PROPERTIES_FILE_NAME\n valueFrom:\n configMapKeyRef:\n name: game-demo\n key: ui_properties_file_name\n volumeMounts:\n - name: config\n mountPath: \"/config\"\n readOnly: true\n volumes:\n # 定义 Pod 的 Volume ,种类为 configMap\n - name: config\n configMap:\n name: game-demo # ConfigMap的名字\n # 需要作为文件放入 Volume 的 Key\n items:\n - key: \"game.properties\"\n path: \"game.properties\"\n - key: \"user-interface.properties\"\n path: \"user-interface.properties\"\n```\n\n我们可以看到 ConfigMap 里的 Key 可以作为文件或是环境变量加载到 Pod 中。另外,作为环境变量加载后其实还能作为命令行参数传入应用,实现各种配置方式。如果修改 Config map 的内容,也可以自动更新 Pod 中的文件。\n\n然而, Config Map 的热更新有一些不太灵活的地方:\n\n1. 作为环境变量加载的 Config Map 数据不会被热更新。想要更新这一部分数据需要重启 Pod。(当然,命令行参数也不能热更新)\n2. 由于 Kubelet 会先将 Config Map 内容加载到本地作为缓存,因此修改 Config Map 后新的内容不会第一时间加载到 Pod 中。而且在旧版本的 K8s 中, Config Map 被更新直到缓存被刷新的时间间隔还会很长,新版本的 K8s 这一部分有了优化,可以设定刷新时间,但会导致 API Server 的负担加重。(这其实是一个 Known Issue ,被诟病多年: https://github.com/kubernetes/kubernetes/issues/22368 )\n\n除 Config Map 以外, K8s 还提供了一种叫 Secret 的资源,用法和 Config Map 几乎一样。对比 Config Map ,Secret 有以下几个特点:\n\n1. 在 Pod 里, Secret 只会被加载到内存中,而永远不会被写到磁盘上。\n2. 用 `kubectl get` 之类的命令显示的 Secret 内容会被用 base64 编码。(不过, well ,众所周知 base64 可不算是什么加密)\n3. 可以通过 K8s 的 Service Account 等 RBAC 相关的资源来控制 Secret 的访问权限。\n\n不过,由于 Secret 也是以明文的形式被存储在 K8s 的主节点中的,因此需要保证 K8s 主节点的安全。\n\n> **Downward API 挂载作为特殊的卷**\n> \n> 还有另外一种叫 Downward API 的东西,可以作为 Volume 或是环境变量被加载到 Pod 中。有一些参数我们很难事先在 Manifest 中定义( e.g. Deployment 生成的 Pod 的名字),因此可以通过 Downward API 来实现。\n> \n> ```yaml\n> apiVersion: v1\n> kind: Pod\n> metadata:\n> name: test-volume-pod\n> namespace: kube-system\n> labels:\n> k8s-app: test-volume\n> node-env: test\n> spec:\n> containers:\n> - name: test-volume-pod-container\n> image: busybox:latest\n> env:\n> - name: POD_NAME # 将 Pod 的名字作为环境变量 POD_NAME 加载到 Pod 中\n> valueFrom:\n> fieldRef:\n> fieldPath: metadata.name\n> command: [\"sh\", \"-c\"]\n> args:\n> - while true; do\n> cat /etc/podinfo/labels | echo;\n> env | sort | echo;\n> sleep 3600;\n> done;\n> volumeMounts:\n> - name: podinfo\n> mountPath: /etc/podinfo\n> volumes:\n> - name: podinfo\n> downwardAPI: # Downward API 类型的卷\n> items:\n> - path: \"labels\" # 将 Pod 的标签作为 labels 文件挂载到 Pod 中\n> fieldRef:\n> fieldPath: metadata.labels\n> ```\n\n\n\n# 网络\n\n其实 Pod 只要部署好了,就会被分配到一个集群内部的 IP 地址,流量就可以通过 IP 地址来访问 Pod 了。然而通过可能会有很大问题: **Pod 随时会被杀死。** 虽然通过用 Deployment 等资源可以在挂掉后重新创建一个 Pod ,但那毕竟是不同的 Pod , IP 已经改变。\n\n另外, Deployment 等资源的就是为了能更方便的做到多副本部署及任意缩容扩容而存在的。如果在 K8s 中访问 Pod 还需要小心翼翼地去找到 Pod 的 IP 地址,或是去寻找 Pod 是否部署了新副本, Deployment 等资源就几乎没有存在价值了。\n\n> 其实 Pod 部署好后不止会被分配 IP 地址,还会被分配到一个类似 `..pod.cluster.local` 的 DNS 记录。例如一个位于 default 名字空间,IP 地址为 172.17.0.3 的 Pod ,对应 DNS 记录为 `172-17-0-3.default.pod.cluster.local` 。\n\n### Service\n\n在古代,人们是通过注册中心、服务发现、负载均衡等中间件来解决上面这些问题的,但这样很不云原生。于是 K8s 引入了 Service 这种资源,来实现简易的服务发现、 DNS 功能。\n\n下面是一个经典的例子,部署了一个 Service 和一个 Deployment:\n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: auth-service\n labels:\n app: auth\nspec:\n type: ClusterIP\n selector:\n app: auth # 指向 Deployment 创建的 Pod\n ports:\n - port: 80 # Service 暴露的端口\n targetPort: 8080 # Pod 的端口\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: auth\n labels:\n app: auth\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: auth\n template:\n metadata:\n name: auth\n labels:\n app: auth\n spec:\n containers:\n - name: auth\n image: xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/auth:xxxxx\n ports:\n - containerPort: 8080\n```\n\n根据前面的知识我们知道,这份文件会部署 Deployment 会创建 2 个相同的 Pod 副本。另外还会部署一个名为 auth-service 的 Service 资源。这个 Service 暴露了一个 80 端口,并且指向那两个 Pod 的 8080 端口。\n\n而这份文件部署后, Service 资源就会在集群中注册一个 DNS A 记录(或 AAAA 记录),集群内其他 Pod (为了辨别我们叫它 Client )就可以通过相同的 DNS 名称来访问 Deployment 部署的这 2 个 Pod :\n\n```sh\ncurl http://auth-service..svc.cluster.local:80\n# 或者省略掉后面的一大串\ncurl http://auth-service.:80\n# 如果 Client 和 Service 在同一个 Namespace 中,还可以:\ncurl http://auth-service:80\n```\n\n像这样 Client 通过 Service 来访问时,会随机访问到其中一个 Pod ,这样一来无论 Deployment 到底创建了多少个副本,只要副本的标签相同,就能通过同一个 DNS 名称来访问,还能自动实现一些简单的负载均衡。\n\n> **为什么 DNS 名称可以简化?**\n> \n> Pod 被部署时, kubelet 会为每个 Pod 注入一个类似如下的 `/etc/resolv.conf` 文件:\n> \n> ```\n> nameserver 10.32.0.10\n> search .svc.cluster.local svc.cluster.local cluster.local\n> options ndots:5\n> ```\n> \n> Pod 中进行 DNS 查询时,默认会先读取这个文件,然后按照 `search` 选项中的内容展开 DNS 。例如,在 test 名称空间中的 Pod ,访问 data 时的查询可能被展开为 data.test.svc.cluster.local 。\n> 更多关于 `/etc/resolv.conf` 文件的内容可参考 https://www.man7.org/linux/man-pages/man5/resolv.conf.5.html\n\n### Service 的种类\n\n我们上面的例子中,可以看到 Service 资源有个字段 `type:ClusterIP` 。其实 Service 资源有以下几个种类:\n\n| 种类 | 作用 |\n| :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ClusterIP` | 这个类型的 Service 会在集群内创建一条 DNS A 记录并通过一定方法将流量代理到其指向的 Pod 上。这种 Service 不会暴露到集群外。这是最基础的 Service 种类。 |\n| `NodePort` | 这种 Service 会在 ClusterIP 的基础上,在所有节点上各暴露一个端口,并把端口的流量也代理到指向的 Pod 上。可以通过这种方法从集群外访问集群内的资源。 |\n| `LoadBalancer` | 这种 Service 会在 ClusterIP 的基础上,在所有节点上各暴露一个端口,并在集群外创建一个负载均衡器来将外部流量路由到暴露的端口,再把流量代理到指向的 Pod 上。这种 Service 一般需要调用云服务提供的 API 或是额外安装的插件。如果什么插件都没安装的话,这种 Service 部署后会与 `NodePort` 的表现一样。 |\n| `ExternalName` | 这种 Service 不需要 selector 字段指定后端,而是用 externalName 字段指定一个外部 DNS 记录,然后将流量全部指向外部服务。如果打算将集群内的服务迁移到集群外、或是集群外迁移到集群内,这种类型的 Service 可以实现无缝迁移。 |\n\n### 虚拟 IP 与 Headless Service\n\n如果你在集群内尝试对 Service 对应的 DNS 记录进行域名解析,会发现返回来的 IP 地址与 Service 指向的任何一个 Pod 对应的 IP 地址都不相同。如果你还尝试了去 Ping 这个 IP 地址,会发现不能 Ping 通。为什么会这样呢?\n\n原来,每个 Service 被部署后, K8s 都会给他分配一个集群内部的 IP 地址,也就是 Cluster IP (这也是最基础的 Service 种类会起名叫 Cluster IP 的原因)。\n\n但是这个 Cluster IP 不会绑定任何的网卡,是一个虚拟 IP 。然后 K8s 中有一个叫 kube-proxy 的组件(这里叫他做组件,是因为 kube-proxy 与 Service 、 Deployment 等不一样,不是一种资源而是 K8s 的一部分), kube-proxy 通过修改 iptables ,将虚拟 IP 的流量经过一定的负载均衡规则后代理到 Pod 上。\n\n![K8s 官网上的虚拟 IP 图](https://d33wubrfki0l68.cloudfront.net/27b2978647a8d7bdc2a96b213f0c0d3242ef9ce0/e8c9b/images/docs/services-iptables-overview.svg)\n\n> **为什么不使用 DNS 轮询?**\n> \n> 为什么 K8s 不配置多条 DNS A 记录,然后通过轮询名称来解析?为什么需要搞出虚拟 IP 这么复杂的东西?这个问题 K8s 官网上也有特别提到原因:\n> \n> - DNS 实现的历史由来已久,它不遵守记录 TTL,并且在名称查找结果到期后对其进行缓存。\n> - 有些应用程序仅执行一次 DNS 查找,并无限期地缓存结果。\n> - 即使应用和库进行了适当的重新解析,DNS 记录上的 TTL 值低或为零也可能会给 DNS 带来高负载,从而使管理变得困难。\n\n有些时候(比如想使用自己的服务发现机制或是自己的负载均衡机制时)我们确实也会想越过虚拟 IP ,直接获取背后 Pod 的 IP 地址。这时候我们可以将 Service 的 `spec.clusterIP` 字段指定为 `None` ,这样 K8s 就不会给这个 Service 分配一个 Cluster IP 。这样的 Service 被称为 **Headless Service** 。\n\nHeadless Service 资源会创建一组 A 记录直接指向背后的 Pod ,可以通过 DNS 轮询等方式直接获得其中一个 Pod 的 IP 地址。另外更重要的一点, Headless Service 还会创建一组 SRV 记录,包含了指向各个 Pod 的 DNS 记录,可以通过 SRV 记录来发现所有 Pod 。\n\n我们可以在集群里用 nsloopup 或 dig 命令去验证一下:\n\n```sh\n# 在集群的 Pod 内部运行\n$ nslookup kafka-headless.kafka.svc.cluster.local\nServer: 10.96.0.10\nAddress: 10.96.0.10#53\n\nName: kafka-headless.kafka.svc.cluster.local\nAddress: 172.17.0.6\nName: kafka-headless.kafka.svc.cluster.local\nAddress: 172.17.0.5\nName: kafka-headless.kafka.svc.cluster.local\nAddress: 172.17.0.4\n\n$ dig SRV kafka-headless.kafka.svc.cluster.local\n# .....\n;; ANSWER SECTION:\nkafka-headless.kafka.svc.cluster.local. 30 IN SRV 0 20 9092 kafka-0.kafka-headless.kafka.svc.cluster.local.\nkafka-headless.kafka.svc.cluster.local. 30 IN SRV 0 20 9092 kafka-1.kafka-headless.kafka.svc.cluster.local.\nkakfa-headless.kafka.svc.cluster.local. 30 IN SRV 0 20 9092 kafka-2.kafka-headless.kafka.svc.cluster.local.\n\n;; ADDITIONAL SECTION:\nkafka-0.kafka-headless.kafka.svc.cluster.local. 30 IN A 172.17.0.6\nkafka-1.kafka-headless.kafka.svc.cluster.local. 30 IN A 172.17.0.5\nkafka-2.kafka-headless.kafka.svc.cluster.local. 30 IN A 172.17.0.4\n```\n\n> 拥有 Cluster IP 的 Service 其实也有 SRV 记录。但这种情况的 SRV 记录中对应的 Target 仍为 Service 自己的 FQDN 。\n\n### 第三次回到 Stateful Set\n\n在上面 Headless Service 的例子中,我们看到,各个 Pod 对应的 DNS A 记录格式为 `...svc.cluster.local` 。不对啊,之前的小知识里不是说过 Pod 被分配的 DNS A 记录格式应该是 `172-17-0-3.default.pod.cluster.local` 的吗?\n\n其实 Headless Service 还有一个众所周知的隐藏功能。 Pod 这种资源本身的参数中有 `subdomain` 字段和 `hostname` 字段,如果设置了这两个字段,这个 Pod 就拥有了形如 `...svc.cluster.local` 的 FQDN (全限定域名)。如果这时刚好在同一名称空间下有与 `subdomain` 同名的 Headless Service , DNS 就会用为这个 Pod 用它的 FQDN 来创建一条 DNS A 记录。\n\n比如 Pod1 在 `kafka` 名称空间中, `hostname` 为 `kafka-1` , `subdomain` 为 `kafka-headless` ,那么 Pod1 的 FQDN 就是 `kafka-1.kafka-headless.kakfa.svc.cluster.local` 。而同样在 `kafka` 名称空间中,刚好又有一个 `kafka-headless` 的 Headless Service ,那么 DNS 就会创建一条 A 记录,就可以通过 `kafka-1.kafka-headless.kafka.svc.cluster.local` 来访问 Pod1 了。当然,由于 DNS 展开,也可以用 `kafka-1.kafka-headless.kafka` 甚至是 `kafka-1.kafka-headless` 来访问这个 Pod 。\n\n其实这些 Pod 是用 Stateful Set 来部署的,这一部分其实是 Stateful Set 相关的功能。之前我们说到 Stateful Set 有唯一稳定的网络标识。我们现在就来详细讲讲,这“唯一稳定的网络标识”到底是在指什么。\n\n我们来看一下这个 kafka Stateful Set 到底是怎么部署的:\n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: kafka-headless\nspec:\n clusterIP: None # 这是一个 headless service\n ports:\n - name: tcp-client\n port: 9092\n protocol: TCP\n targetPort: kafka-client\n selector:\n select-label: kafka-label\n type: ClusterIP\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: kafka\nspec:\n replicas: 3\n serviceName: kafka-headless # 注意到这里有 serviceName 字段\n selector:\n matchLabels:\n select-label: kafka-label\n template:\n metadata:\n labels:\n select-label: kafka-label\n spec:\n containers:\n - name: kafka\n image: docker.io/bitnami/kafka:3.1.0-debian-10-r52\n # 接下来 Pod 相关部分省略\n # 下面 Volume 相关部分也省略\n```\n\n我们看到, Stateful Set 的定义中必须要用 `spec.serviceName` 字段指定一个 Headless Service 。 Stateful Set 创建 Pod 时,会自动给 Pod 指定 `hostname` 和 `subdomain` 字段。这样一来,每个 Pod 才有了唯一固定的 hostname ,唯一固定的 FQDN ,以及通过与 Headless Service 共同部署而获得唯一固定的 A 记录。(此外,其实当 Pod 因为版本升级等原因被重新创建时,相同序号的 Pod 还会被分配到相同固定的集群内 IP 。)\n\n> **关于 Stateful Set 中 `serviceName` 字段的争议**\n> \n> Stateful Set 中的 serviceName 字段是必填字段。这个字段唯一的作用其实就是给 Pod 指定 subdomain 。其实这样会有一些问题:\n> \n> 1. Stateful Set 部署时不会检查是否真的存在这么一个 Headless Service 。如果 serviceName 乱填一个值,会导致虽然 Pod 的 `hostname` 和 `subdomain` 都指定了却没有创建 A 记录的情况。\n> 2. 有时 Stateful Set 的 Pod 不需要接收流量,也不需要相互发现,这时候还强行需要指定一个 serviceName 显得有点多余。\n> \n> 在 GitHub 上有关于这个问题的 Issue : https://github.com/kubernetes/kubernetes/issues/69608\n\n### 从集群外部访问\n\n在 K8s 集群里把应用部署好了,可是如何让集群外部的客户端访问我们集群中的应用呢?这可能是大家最关心的问题。\n\n不过有认真听的同学估计已经有这个问题的答案了。之前我们讲过 NodePort 和 LoadBalancer 这两种 Service 类型。\n\n其中 NodePort Service 只是简单地在节点机器上各开一个端口,而如何路由、如何负载均衡等则一概不管。\n\n而 LoadBalancer Service 则是在 NodePort 的基础上再加一个一个负载均衡器,然后把节点暴露的端口注册到这个负载均衡器上。这样一来,集群外部的客户端就可以通过同一个 IP 来访问集群中的应用。但是要使用 LoadBalancer Service ,一般需要先安装云供应商提供的 Controller ,或是安装其他第三方的 Controller (比如 Nginx Controller )。\n\n在 Service 之外还另有一种资源类型叫 Ingress ,也可以用来实现集群外部访问集群内部应用的功能。 Ingress 其实也会在集群外创建一个负载均衡器,因此也需要预先安装云供应商的 Controller 。但 Ingress 与 Service 不同的是,它还会管理一定的路由逻辑,接收流量后可以根据路由来分配给不同的 Service 。\n\n| 类型 | OSI 模型工作层数 | 依赖于云平台或其他插件 |\n| :------------------- | :--------------- | :--------------------- |\n| NodePort Service | 第四层 | 否 |\n| LoadBalancer Service | 第四层 | 是 |\n| Ingress | 第七层 | 是 |\n\n特别再详细说一下 Ingress 这种资源。 Ingress 本身不会在集群内的 DNS 上创建记录,一般也不会主动去路由集群内的流量(除非你在集群内强行访问 Ingress 的负载均衡器…… 不过一般也没什么理由要这样做对吧)。但 Ingress 可以根据 HTTP 的 hostname 和 path 来路由流量,把流量分发到不同的 Service 上。 Ingress 也是 K8s 的原生资源里唯一能看到 OSI 第七层的资源。\n\n下面是 AWS 的 EKS 服务中部署的一个 Ingress 的例子(集群中已安装 AWS Load Balancer Controller ):\n\n```yaml\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n annotations:\n kubernetes.io/ingress.class: alb\n alb.ingress.kubernetes.io/scheme: internet-facing\n alb.ingress.kubernetes.io/target-type: ip\n alb.ingress.kubernetes.io/backend-protocol-version: GRPC\n alb.ingress.kubernetes.io/listen-ports: '[{\"HTTPS\":443}]'\n alb.ingress.kubernetes.io/healthcheck-path: /grpc.health.v1.Health/Check\n alb.ingress.kubernetes.io/healthcheck-protocol: HTTP\n alb.ingress.kubernetes.io/success-codes: 0,12\n alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:xxxxxxxxxx:certificate/xxxxxxxxxx\n\n external-dns.alpha.kubernetes.io/hostname: sample.example.com\n \n name: gateway-ingress\nspec:\n rules:\n - host: sample.example.com\n http:\n paths:\n - path: /grpc.health.v1.Health\n pathType: Prefix\n backend:\n service:\n name: health-service\n port:\n number: 50051\n - path: /proto.sample.v1.Sample\n pathType: Prefix\n backend:\n service:\n name: sample-service\n port:\n number: 50051\n```\n\n可以看到, Ingress 资源可以通过 `spec.rules` 字段中定义各条规则,通过 hostname 或是 path 等第七层的信息来进行路由。 Ingress 部署下去后, AWS Load Balancer Controller 会读取会根据的配置,并在云上创建一个 AWS Application Load Balancer (ALB),而 `spec.rules` 会应用到 ALB 上,由 ALB 来负责流量的路由。\n\n我们也会注意到,怎么 `metadata.annotations` 里有这么多奇奇怪怪的字段! Ingress 本身的功能都是 AWS Load Balancer Controller 调用 AWS 的 API 创建 ALB 来实现的。但 AWS 的 ALB 能实现的功能可不止 Ingress 字段定义的这些,比如安装 TLS 证书、 health check 等 spec 字段中描述不下的功能,就只能是通过 annotation 的形式来定义了。\n\n> 小彩蛋:可以看到例子中的 Ingress 资源 annotation 字段里还有一行 `external-dns.alpha.kubernetes.io/hostname: sample.example.com` 。其实这个 K8s 集群中还安装了 external-dns 这个应用,它可以根据 annotation 来在外部 DNS 上直接创建 DNS 记录!有了这个插件我们可不用再慢慢打开公共 DNS 管理页面,再小心翼翼地记下 IP 地址去添加 A 记录了。\n\n# 更高级的部署方式(一)\n\n一路说道这里, K8s 中最基础的资源大部分都已经介绍了。但是,这么多资源之间又需要相互配合,只部署一种资源基本没什么生产能力。\n\n比如只部署 Deployment 的话,我们确实是能在一组多副本的 Pod 里跑起可执行程序,但这组 Pod 却几乎没办法接受集群里其他 Pod 的流量(只能通过制定 Pod 的 IP 来访问,但 Pod 的 IP 是会变的)。因此一般来说一个 Deployment 都会搭配一个 Service 来使用。这还是最简单的一种搭配了。\n\n假若我们现在要在自己的 K8s 里安装一个别人提供的应用。当然由于 K8s 是基于容器的,只要别人提供了他应用的 yaml 清单,我们只用把清单用 `kubectl apply -f` 提交给 K8s ,然后让 K8s 把清单中的镜像拉下来就能跑了。可如果我们需要根据环境来改一些参数呢?\n\n如果别人提供的 yaml 文件比较简单还好说,改改对应的字段就好了。如果别人的应用比较复杂,那改 yaml 文件可就是一个大难题了。比如 AWS 的 Load Balancer Controller ,它的 yaml 清单文件可是多达 939 行!\n\n[[aws-elb-controller-lines.png]]\n\n在这种复杂的场景下,我们就需要一些更高级的部署方式了。\n\n### Helm\n\n首先来介绍的是 Helm 。 Helm 是一个包管理工具,可以类比一下 CentOS 中的 yum 工具。它可以把一组 K8s 资源发布成一个 Chart ,然后我们可以用 Helm 来安装这个 Chart ,并且可以通过参数设值来改变 Chart 中的部分资源。利用 Helm 安装 Chart 后还可以管理 Chart 的升级、回滚、卸载。\n\n使用别人提供的 Helm Chart 前,需要先 add 一下 Chart 的仓库,然后再安装仓库里提供的 Chart 。比如我们要安装 bitnami 提供的 Kafka Chart 时:\n\n```bash\n# 添加 https://charts.bitnami.com/bitnami 这个仓库,命名为 bitnami\nhelm repo add bitnami https://charts.bitnami.com/bitnami\n\n# 在 kafka 名称空间里安装 bitnami 仓库里的 kafka Chart ,并通过参数设置为 3 个副本,并同时安装一个 3 副本的 Zookeeper\nhelm install kafka -n kafka \\\n --set replicaCount=3 \\\n --set zookeeper.enabled=true \\\n --set zookeeper.replicaCount=3 \\\n bitnami/kafka\n```\n\n命令执行后, helm 就会根据参数与 Chart 的内容,在 K8s 里安装 StatefulSet 、 Service 、 ConfigMap 等一切所需要的资源。\n\n```sh\n$ k -n kafka get all,cm\nNAME READY STATUS RESTARTS AGE\npod/kafka-0 1/1 Running 1 46d\npod/kafka-1 1/1 Running 3 46d\npod/kafka-2 1/1 Running 3 46d\npod/kafka-zookeeper-0 1/1 Running 0 46d\npod/kafka-zookeeper-1 1/1 Running 0 46d\npod/kafka-zookeeper-2 1/1 Running 0 46d\n\nNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\nservice/kafka ClusterIP 172.20.1.196 9092/TCP 164d\nservice/kafka-headless ClusterIP None 9092/TCP,9093/TCP 164d\nservice/kafka-zookeeper ClusterIP 172.20.227.236 2181/TCP,2888/TCP,3888/TCP 164d\nservice/kafka-zookeeper-headless ClusterIP None 2181/TCP,2888/TCP,3888/TCP 164d\n\nNAME READY AGE\nstatefulset.apps/kafka 3/3 164d\nstatefulset.apps/kafka-zookeeper 3/3 164d\n\nNAME DATA AGE\nconfigmap/kafka-scripts 2 164d\nconfigmap/kafka-zookeeper-scripts 2 164d\nconfigmap/kube-root-ca.crt 1 165d\n```\n\n甚至, Helm 可以通过模板生成的 Pod 环境变量,来预先设置好 Kafka 的配置,让他找得到 Zookeeper 服务:\n\n```yaml\napiVersion: v1\nkind: Pod\n# 略去无关信息\nspec:\n containers:\n - name: kafka\n command:\n - /scripts/setup.sh\n env:\n - name: KAFKA_CFG_ZOOKEEPER_CONNECT\n value: kafka-zookeeper\n # ...\n```\n\n通过设置 `KAFKA_CFG_ZOOKEEPER_CONNECT` 这个环境变量,指定了 Kafka Broker 可以通过访问 `kafka-zookeeper` 来找到 zookeeper 服务。(还记得 zookeeper 的 Service 名字是 `kafka-zookeeper` 吗? zookeeper 与 kafka 部署在同一个名称空间里,因此可以直接通过 Service 名访问。)\n\n如果我们打开这个 helm chart 对应的[代码仓库](https://github.com/bitnami/charts/tree/master/bitnami/kafka),会发现原来有一组 go template 文件,以及一个 `values.yaml` 文件和 `Chart.yaml` 文件:\n\n```sh\n.\n├── Chart.lock\n├── Chart.yaml\n├── README.md\n├── templates\n│ ├── NOTES.txt # 这里定义的是 helm 工具的命令行信息\n│ ├── _helpers.tpl # 这里面是一些定义好的 go template 代码块可以供其他模板使用\n│ ├── configmap.yaml\n│ ├── statefulset.yaml\n│ ├── svc-headless.yaml\n│ ├── svc.yaml\n│ └── # 以下省略若干模板文件\n└── values.yaml\n```\n\n- `Chart.yaml` 中定义了这个 Chart 的基本信息,包括名称、版本、描述、依赖等。\n- `values.yaml` 中定义了这个 Chart 的默认参数,包括各种资源的默认配置、副本数量、镜像版本等。其中的值都可以通过 `helm install` 命令的 `--set` 参数来覆盖。\n- `templates/` 文件夹下的都是 go template 的模板文件。\n\n`helm install` 就是通过用 `values.yaml` 中预定义的参数,渲染 `templates/` 文件夹下的 go template 文件,生成最终的 yaml 文件,然后再通过 kubectl apply -f 的方式,将 yaml 文件里的资源部署到 K8s 里。然后通过忘资源里注入一些特殊 annotation 的方式来记住自己部署了那些资源,进而提供 `update` 、 `uninstall` 等功能。\n\n关于更多 Helm 的内容,可以参考[官方文档](https://helm.sh/docs/)。\n\n### Kustomize\n\n另一个部署工具是 Kustomize 。之前提到 Config Map 时的例子中,将配置文件的内容直接写进了 yaml 清单的一个字段里:\n\n```yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: game-demo\ndata:\n # 一个 Key 可以对应一个值\n player_initial_lives: \"3\"\n ui_properties_file_name: \"user-interface.properties\"\n\n # 一个 Key 也可以对应一个文件的内容\n game.properties: |\n enemy.types=aliens,monsters\n player.maximum-lives=5 \n user-interface.properties: |\n color.good=purple\n color.bad=yellow\n allow.textmode=true \n```\n\n其实这样很不好,先不说这样写没办法在 IDE 里用配置文件自己的语法检查,每行还需要一定的缩进,如果配置文件有好几百行,你甚至会忘了这一行到底是哪个配置文件!此时我们就会自然而然的想把每个配置文件以单独文件的形式保存。\n\nKustomize 就是这样一个工具,它可以帮助我们把每个配置文件以单独文件的形式保存,然后再通过一个 `kustomization.yaml` 文件,将这些配置文件组合起来,生成最终的 yaml 文件。\n\n```yaml\napiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n # 其他资源也可以单独使用一个文件定义\n - deployment.yaml\n\n# 用 configMapGenerator 从文件中生成 ConfigMap\nconfigMapGenerator:\n - name: game-demo\n literals:\n - \"ui_properties_file_name=user-interface.properties\"\n - \"player_initial_lives=3\"\n # 从文件中读取内容\n files:\n - game.properties\n - user-interface.properties\n# 有多个 configMap 时,可以通过统一的 generatorOptions 来设置一些通用的选项\ngeneratorOptions:\n disableNameSuffixHash: true\n```\n\n然后两个配置文件的内容可以单独用文件定义,此时可以结合 IDE 的语法检查,以及代码补全功能,来编写配置文件。\n\n```properties\n# user-interface.properties\ncolor.good=purple\ncolor.bad=yellow\nallow.textmode=true \n```\n\n然后将 `kustomization.yaml` 和其他所需的文件都放在同一个目录下:\n\n```bash\n.\n├── kustomization.yaml\n├── deployment.yaml\n├── game.properties\n└── user-interface.properties\n```\n\n然后就可以通过 `kubectl apply -k ./` 来将整个 kustomize 文件夹转换为 yaml 清单直接部署到 K8s 中。\n(没错,现在 Kustomize 已经成为 kubectl 中的内置功能!可以不用先 `kustomize build` 生成 yaml 文件再 `kubectl apply` 两步走了!)\n\n值得提醒的是,虽然 `kustomization.yaml` 有 `apiVersion` 和 `kind` 字段,长得很像一个资源清单,但其实 K8s 的 API server 并不认识他。 Kustomize 的工作原理其实是先根据 `kustomization.yaml` 生成 K8s 认识的 yaml 资源清单,然后再通过 `kubectl apply` 来部署。\n\n除了可以直接将 ConfigMap 与 Secret 中的文件字段内容用单独的文件定义外, Kustomize 还有其他比如为部署的资源添加统一的名称前缀、添加统一字段等功能。这些大家可以阅读 Kustomize 的[官方文档](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/)来了解。\n\n### 各种工具的优缺点\n\n我们目前已经知道有三种在 K8s 中部署资源的方式: `kubectl apply`、Helm 和 Kustomize 。\n\n其中 `kubectl apply` 的优缺点很明确,优点是最简单直接,缺点是会导致要么 yaml 清单过长,要么需要分多文件多次部署,使集群中产生中间状态。\n\n而 Helm 与 Kustomize 我们上面也分析过,其实都是基于 `kubectl apply` 的。 Helm 是通过 go template 先生成 yaml 文件再 `kubectl apply` ,而 Kustomize 是通过 `kustomization.yaml` 中的定义用自己的一套逻辑生成 yaml 文件,然后再 `kubectl apply` 。\n\nHelm 的优点是 Helm Chart 安装时可以直接使用别人 Helm 仓库中已经上传好的 Chart ,只需要设置参数就可以使用。这也是 Kustomize 的缺点:如果想要使用别人提供的 Kustomization 而只修改其中的一些配置,必须要先把放 `kustomization.yaml` 的整个文件夹下载下来才能做修改。\n\n而 Helm 的缺点也是明显的, Helm 依赖于往资源里注入特殊的 annotation 来管理 Chart 生成的资源,这可能会很难与集群中现有的一些系统(比如 Service Mesh 或是 GitOps 系统等)放一起管理。而 Kustomize 生成的 yaml 清单就是很干净的 K8s 资源,原先的 K8s 资源该是什么表现就是什么表现,与现有的系统兼容一般会比较好。\n\n而另外,由于 Helm 与 Kustomize 都是基于 `kubectl apply` 的,因此他们有共同的缺点,就是不能做 `kubectl apply` 不能做的事情。\n\n什么叫 `kubectl apply` 不能做的事情呢?比如说我们要在 K8s 中部署 Redis 集群。聪明的你可能就想到要用 Stateful Set 、 PVC 、 Headless Service 来一套组合拳。这确实可以部署一个多节点、有状态的 Redis Cluster 。可是如果我们要往 Redis Cluster 里加一个节点呢?\n\n你当然可以把 Stateful Set 中的 `Replicas` 字段加个 1 然后用 `kubectl apply` 部署,可是这实际上只能增加一个一个 Redis 实例 —— 然后什么都没发生。其他节点不认识这个新的节点,访问这个新节点也不能拿到正确的数据。要知道往 Redis Cluster 里加节点,是要先让集群发现这个新节点,然后还要迁移 slot 的! `kubectl apply` 可不会做这些事。\n\n> Well, 其实这些也是可以通过增加 initContainer 、修改镜像增加启动脚本等方式,实现用 `kubectl apply` 部署的。可是,这会让整个 Pod 资源变得很难理解,也不好维护。而且,如果不是因为做不到,谁会想去修改别人的镜像呢?\n\n我们接下来会介绍 K8s 的核心架构,来理解我们之前讲的这些资源到底是怎么工作的。最后会引出一组新的概念: Operator 与自定义资源( Custom Resource Definition ,简称 CRD )。通过 Operator 与 CRD ,我们可以做到 `kubectl apply` 所不能做到的事,包括 Redis Cluster 的扩容。\n\n> DIO: `kubectl apply` 的能力是有限的……\n> 越是部署复杂的应用,就越会发现 `kubectl apply` 的能力是有极限的……除非超越 `kubectl apply` 。\n> \n> JOJO: 你到底想说什么?\n> \n> DIO: 我不用 `kubectl apply` 了! JOJO !\n> (其实还是要用的)\n\n","title":"Kubernetes 入门 (2)","abstract":"我们之前说的都是用于部署 Pod 的资源,我们接下来介绍与创建 Pod 不相关的资源:储存与网络。\n其实我们之前已经接触过储存相关的内容了:在讲 Stateful Set 时我们提过 Stateful Set 创建出来的 Pod 都会有相互独立的储存;而讲 Daemon Set 时我们提到 K8s 推荐只在 Daemon Set 的 Pod 中访问宿主机磁盘。但独立的储存具体指什么?除了访问宿主机磁盘以外还有什么其他的储存?\n在 Docker 中,我们可以把宿主机磁盘上的一个路径作为一个 Volume 来给容器绑定,或者直接使用 Docker Engine 管理的 Volume 来提供持久化存储或是容器间共享文件。在 K8s 里面也沿用了 Volume 这个概念,可以通过 Mount 绑定到容器内的路径,并通过实现 CSI 的各种引擎来提供更多样的存储。","length":875,"created_at":"2022-08-20T21:56:52.000Z","updated_at":"2022-08-20T14:02:18.000Z","tags":["Kubernetes","DevOps","Docker","Cloud Native"],"license":true}},{"slug":"introduction-for-k8s","file":"public/content/articles/2022-08-13-introduction-for-k8s.md","mediaDir":"content/articles/2022-08-13-introduction-for-k8s","path":"/articles/introduction-for-k8s","meta":{"content":"\n# 容器, Docker 与 K8s\n\n我们知道 K8s 利用了容器虚拟化技术。而说到容器虚拟化就要说 Docker 。可是,容器到底是什么? Docker 又为我们做了些什么?我们又为什么要用 K8s ?\n\n### 关于容器虚拟化\n\n> 要把一个不知道打过多少个升级补丁,不知道经历了多少任管理员的系统迁移到其他机器上,毫无疑问会是一场灾难。 —— Chad Fowler 《Trash Your Servers and Burn Your Code》\n\n\"Write once, run anywhere\" 是 Java 曾经的口号。 Java 企图通过 JVM 虚拟机来实现一个可执行程序在多平台间的移植性。但我们现在知道, Java 语言并没能实现他的目标,会在操作系统调用、第三方依赖丢失、两个程序间依赖的冲突等各方面出现问题。\n\n要保证程序拉下来就能跑,最好的方法就是把程序和依赖打包到一起,然后将外部环境隔离起来。容器虚拟化技术就是为了解决这个。\n\n与常说的虚拟机不同, Docker 等各类容器是用隔离名称空间的方式进行资源隔离的。 Linux 系统的内核直接提供了名称空间隔离的能力,是针对进程设计的访问隔离机制,可以进行一些资源封装。\n\n| 名称空间 | 隔离内容 | 内核版本 |\n| :----------- | :---------------------------- | :------- |\n| Mount | 文件系统与路径等 | 2.4.19 |\n| UTS | 主机的Hostname、Domain names | 2.6.19 |\n| IPC | 进程间通信管道 | 2.6.19 |\n| PID | 独立的进程编号空间 | 2.6.24 |\n| Network | 网卡、IP 地址、端口等网络资源 | 2.6.29 |\n| User | 进程独立的用户和用户组 | 3.8 |\n| Cgroup | CPU 时间片,内存分页等 | 4.6 |\n| Time \\<- New! | 进程独立的系统时间 | 5.6 |\n\n值得注目的是, Linux 系统提供了 Cgroup 名称空间隔离的支持。通过隔离 Cgroup ,可以给单独一个进程分配 CPU 占用比率、内存大小、外设 I/O 访问权限等。再配合 IPC 、 PID 等的隔离,可以让被隔离的进程看不到同一实体机中其他进程的信息,就像是独享一整台机器一样。\n\n由于容器虚拟化技术直接利用了宿主机操作系统内核,因此远远要比虚拟机更轻量,也更适合用来给单个程序进行隔离。但也同样由于依赖了宿主机内核,在不同的架构、不同种类的操作系统间容器可能不能移植。\n\n### 关于 Docker\n\n在介绍 K8s 之前,我们要先搞清楚 Docker 是什么。或者说,我们平时说的“ Docker ”是什么?\n\n我们平时说的 Docker ,可能是以下几个东西:\n\n- Docker Engine: 在宿主机上跑的一个进程,专门用来管理各个容器的生命周期、网络连接等,还暴露出一些 API 供外部调用。有时会被称为 Docker Daemon 或是 dockerd 。\n- Docker Client: 命令行中的 `docker` 命令,其实只会跟 Docker Server 通信,不会直接创建销毁一个容器进程。\n- Docker Container: 宿主机上运行的一组被资源隔离的进程,在容器中看来像是独占了一台虚拟的机器,不需要考虑外部依赖。\n- Docker Image: 是一个打包好的文件系统,可以从一个 Image 运行出复数个 Container 。 Image 内部包含了程序运行所需的所有文件、库依赖,以及运行时的环境变量等。\n- Docker 容器运行时: 是 Docker Engine 中专门管理容器状态、生命周期等的那个组件,原来名为 libcontainer 。[《开放容器交互标准》](https://en.wikipedia.org/wiki/Open_Container_Initiative)制定后, Docker 公司将此部分重构为 [runC 项目](https://github.com/opencontainers/runc),交给 Linux 基金会管理。而 Docker Engine 中与运行时进行交互的部分则抽象出来成为 [containerd 项目](https://containerd.io/),捐献给了 CNCF 。\n\n我们平时在 linux 机上运行 `yum install docker` 之类的命令,安装的其实是 Docker Engine + Docker Client 。(而在 Windows 或 MacOS 上安装的 Docker Desktop 其实是一个定制过的 linux 虚拟机。)下面说的 Docker 的功能其实都是指 Docker Engine 的功能。\n\n而 Docker 提供给我们的功能,除了最基础的运行和销毁容器外,还包括了一些容器网络编排、重启策略、文件路径映射、端口映射等功能。\n\n而我认为 Docker 最大的贡献,还是容器的镜像与镜像仓库。有了镜像与镜像仓库,人们就可以把自己的程序与执行环境直接打包成镜像发布,也可以直接拿打包好的镜像来运行容器进行部署,而不需要额外下载或是安装一些东西,也不需要担心程序会与已经跑起来的其他程序冲突。\n\n### 为什么要用 K8s ?\n\n其实 Docker 有一个很强大的工具叫 docker-compose ,可以通过一个 manifest 对多个容器组成的网络进行编排。那为什么我们还需要 K8s 呢?换句话说,有什么事是 Docker 不能做的?而 K8s 设计出来的目标是为了解决什么问题?\n\n首先, Docker 做不到以下的功能:\n\n1. **Docker 不能做跨多主机的容器编排。** docker-compose 再方便,他也只能编排单台主机上的容器。对跨主机的集群编排无能为力。(实际上,用了 Docker-Swarm 后是可以多主机编排的,但一来 Docker-Swarm 出现的比 K8s 晚,而来 Docker-Swarm 功能不如 K8s ,因此用的人很少,我们下面就默认 Docker-Swarm 不存在了。)\n2. **Docker 提供的容器部署管理功能不够丰富。** Docker 有一些简单的容器重启策略,但也只是简单的失败后重启之类的,没有完整的应用状态检查等功能。同时,版本升级、缩扩容等策略选择的余地也不多。\n3. **Docker 缺乏高级网络功能。** 要让 Docker 的容器间进行网络通信,也只能是说把容器放到同一个网络下,然后再通过各自的 Hostname 来找到对方。但实际上,我们更会想要一些负载均衡、自定义域名、选择某些容器端口不暴露之类的功能。\n\nand more...\n\n总的来说, Docker 更关注单台主机上容器怎么跑,而对部署管理的功能则支持不多。而最大的痛点,就是 Docker 对多主机的集群部署支持的实再很差。然而,为了实现多区可用、负载均衡等功能,多主机集群的容器编排又是必不可少的。\n\nK8s 的出现,主要就是为了解决多主机集群上的容器编排问题。\n\n1. **K8s 可以进行多主机调度。** 用户只需要描述自己需要运行怎样的应用, K8s 就可以自己选择一个合适的节点进行部署,用户不需要关心自己的应用部署到哪个节点上。\n2. **K8s 中一切皆资源。** K8s 有完善的抽象资源机制,用户几乎不需要知道磁盘、网络等任何硬件信息,只需要对着统一的抽象资源进行操作。\n3. **K8s 能保证较强的可用性。** 除了能跨多主机调度实现多区可用外, K8s 还提供了很完善的缩扩容机制、健康检查机制以及自动恢复机制。\n\n可以说, K8s 是容器编排工具的主流选择。\n\n### K8s 与 Docker 的关系\n\nK8s 与 Docker 关系很复杂,是一个逐渐变化的过程。\n\n一开始 K8s 是完全依赖于 Docker Engine 进行容器启动与销毁的。后来[容器运行时接口(CRI)](https://kubernetes.io/blog/2016/12/container-runtime-interface-cri-in-kubernetes/)、 [CRI-O 标准](https://github.com/cri-o/cri-o)、开放容器交互标准(OCI)等标准逐渐建立,可替代 Docker Engine 的工具越来越多, K8s 中已经完全可以不使用 Docker Engine 了。\n\n[《凤凰架构》](http://icyfenix.cn/)一书中有下面这样一张图来描述 K8s 与 Docker Engine 的关系:\n\n![K8s 与 Docker Engine 的关系](http://icyfenix.cn/assets/img/kubernetes.495f9eae.png)\n\n《凤凰架构》书中[这一章节](http://icyfenix.cn/immutable-infrastructure/container/history.html#%E5%B0%81%E8%A3%85%E9%9B%86%E7%BE%A4%EF%BC%9Akubernetes)详细介绍了 K8s 与 Docker 的历史,我这里就不再赘述。\n\n# 部署一个 Pod\n\n上面说了一堆概念,我们接下来实际上会怎样应用 K8s 。\n\n### Pod 示例\n\n> Pod 是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元。\n> Pod 是一组容器;Pod 中的内容总是一同调度,在共享的上下文中运行。 Pod 中包含一个或多个应用容器,这些容器相对紧密地耦合在一起。在非云环境中,在相同的物理机或虚拟机上运行的应用类似于在同一逻辑主机上运行的云应用。\n> —— Kubernetes 官方文档\n\nPod 是 K8s 的最小部署单位。\n\n因为 K8s 将硬件资源都抽象化了,用户不需要知道自己的应用部署到哪台机上。但是有些场景下两个主进程之间又必须相互协作才能完成任务,如果两个进程不确定会不会部署到同一个节点上会变得很麻烦。因此才需要 Pod 这种资源。\n\n下面是一个 Nginx Pod 的示例(这是 K8s manifest 文件,可以用 `kubectl apply -f ` 进行部署):\n\n```yaml\nmetadata:\n name: simple-webapp\nspec:\n containers:\n - name: main-application\n image: nginx\n volumeMounts:\n - name: shared-logs\n mountPath: /var/log/nginx\n - name: sidecar-container\n image: busybox\n command: [\"sh\",\"-c\",\"while true; do cat /var/log/nginx/access.log; sleep 30; done\"]\n volumeMounts:\n - name: shared-logs\n mountPath: /var/log/nginx\n volumes:\n - name: shared-logs\n emptyDir: {}\n```\n\n可以看到, Pod 中可以包含多个容器,这组容器总是以一定的逻辑一起部署,且总是部署在同一个节点。对 K8s 操作时,不能说只部署 Pod 中一个特定的容器,也不能说把 Pod 中一个容器部署在这个节点,另一个容器部署在另一个节点上。\n\n在上面这个例子中,我们看到 Pod 中除了 Nginx 容器以外还有一个 Sidecar 容器负责将 Nginx 的 access.log 日志输出到控制台。两个容器可以通过 mount 同一个路径来实现文件共享。这种场景下,单独跑一个 Sidecar 容器没有意义,而我们也不会希望两个容器部署在不同的节点上。 **两个容器同生共死** ,这样的模式被称为 **Sidecar 模式** 。 Jaeger Agent ,或是 Service Mesh 中常见的 Envoy Sidecar 都可以通过这种模式部署,这样业务容器中就可以不考虑 tracing 或是流量控制相关的问题。\n\n此外,由于同一个 Pod 中的容器默认共享了相同的 network 和 UTS 名称空间,不管是在 Pod 的内部还是外部来看,他们一定程度上就像是真的部署在同一主机上一样,有相同的 Hostname 与 ip 地址,在一个容器中也可以通过 localhost 来访问零一个容器的端口。\n\n另外 Pod 中可以定义若干个 initContainer ,这些容器会比 `spec.containers` 中的容器先运行,并且是顺序运行。下面是通过安装 bitnami 的 Kafka Helm Chart 得到的一个 Kafka Broker Pod (有所简化):\n\n```yaml\napiVersion: v1\nkind: Pod\nmetadata:\n name: kafka-0\n namespace: kafka\nspec:\n containers:\n - name: kafka\n image: docker.io/bitnami/kafka:3.1.0-debian-10-r52\n command:\n - /scripts/setup.sh\n volumeMounts:\n - name: scripts\n mountPath: /scripts/setup.sh\n subPath: setup.sh\n - name: shared\n mountPath: /shared\n initContainers:\n - name: auto-discovery\n image: docker.io/bitnami/kubectl:1.23.5-debian-10-r1\n command:\n - /scripts/auto-discovery.sh\n volumeMounts:\n - name: shared\n mountPath: /shared\n - name: scripts\n mountPath: /scripts/auto-discovery.sh\n subPath: auto-discovery.sh\n volumes:\n - name: scripts\n configMap:\n defaultMode: 493\n name: kafka-scripts\n - name: shared\n emptyDir: {}\n```\n\n可以看到,在 `kafka` pod 启动前会先启动一个名为 `auto-discovery` 的 initContainer ,负责获得集群信息等准备工作。准备工作完成后,会将信息写入 `/shared` 目录下,然后再启动 `kafka` 容器 Mount 同一目录,就可以获取准备好的信息。\n\n**这样运行容器进行 Pod 初始化就叫 initContainer 模式** 。每个 initContainer 会运行到成功退出为止,如果有一个 initContainer 启动失败,则整个 Pod 启动失败,触发 K8s 的 Pod 重启策略。\n\n\n# 部署更多 Pod\n\n### Replica Set\n\n可是上面说了这么多,还只是单个 Pod 的部署,但我们希望能做多副本部署。\n\n其实,只要把 Pod 的 manifest 改一下 `metadata.name` 再部署一次,就能得到一模一样的两个 Pod ,就是一个简单的多副本部署了。(必须改 `metadata.name` ,不然 K8s 会以为你是想修改同一个 Pod )\n\n可是这样做会有很多问题:\n\n- 要复制一下还要改名字多麻烦啊,我想用同一份模板,只定义一下副本数就能得到对应数量的 Pod 。\n- 缩容扩容还要对着 Pod 操作很危险,我想直接修改副本数就能缩容扩容。\n- 如果其中一些 Pod 挂掉了不能重启,现在是什么都不会做。我希望能自动建一些新的 Pod 顶上,来保证副本数不变。\n\n为了实现这些需求,就出现了 Replica Set 这种资源。下面是实际应用中一个 Replica Set 的例子:\n\n```yaml\napiVersion: apps/v1\nkind: ReplicaSet\nmetadata:\n labels:\n app: gateway\n name: gateway-9dc546658\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: gateway\n template:\n metadata:\n labels:\n app: gateway\n name: gateway\n spec:\n containers:\n name: gateway\n image: xxxxxxxx.amazonaws.com/gateway:xxxxxxx\n ports:\n - containerPort: 50051\n protocol: TCP\n readinessProbe:\n initialDelaySeconds: 5\n tcpSocket:\n port: 50051\n startupProbe:\n failureThreshold: 60\n tcpSocket:\n port: 50051\n affinity:\n podAntiAffinity:\n preferredDuringSchedulingIgnoredDuringExecution:\n - podAffinityTerm:\n labelSelector:\n matchLabels:\n app: gateway\n topologyKey: topology.kubernetes.io/zone\n weight: 80\n```\n\n我们可以看到, `spec.template` 中就是我们要的 Pod 的模板,在 metadata 里带上了 `app:gateway` 标签。而在 `spec.replicas` 中定义了我们需要的 Pod 数量, `spec.selector` 中描述了我们要对带 `app:gateway` 标签的 Pod 进行控制。把这份 manifest 部署后,我们就会得到除名字以外几乎一摸一样的两个 Pod :\n\n```yaml\napiVersion: v1\nkind: Pod\nmetadata:\n generateName: gateway-9dc546658-\n labels:\n app: gateway\n pod-template-hash: 9dc546658\n name: gateway-9dc546658-6c9qs\n ownerReferences:\n - apiVersion: apps/v1\n blockOwnerDeletion: true\n controller: true\n kind: ReplicaSet\n name: gateway-9dc546658\n uid: 6633f89c-377c-4c90-bd08-3be5bc7b21bd\n resourceVersion: \"49793842\"\n uid: f927db88-a39a-4623-852d-4f150a6d853b\nspec:\n containers:\n name: gateway\n image: xxxxxxxx.amazonaws.com/gateway:xxxxxxx\n ports:\n # 后续省略\n\n---\napiVersion: v1\nkind: Pod\nmetadata:\n annotations:\n kubernetes.io/psp: eks.privileged\n creationTimestamp: \"2022-08-09T08:51:25Z\"\n generateName: gateway-9dc546658-\n labels:\n app: gateway\n pod-template-hash: 9dc546658\n name: gateway-9dc546658-8trcs\n ownerReferences:\n - apiVersion: apps/v1\n blockOwnerDeletion: true\n controller: true\n kind: ReplicaSet\n name: gateway-9dc546658\n uid: 6633f89c-377c-4c90-bd08-3be5bc7b21bd\n resourceVersion: \"49793745\"\n uid: 0918e3ed-2965-4237-8828-421a7831c9ed\nspec:\n containers:\n image: xxxxxxxx.amazonaws.com/gateway:xxxxxxx\n name: gateway\n ports:\n # 后续省略\n```\n\n可以看到,创建出来的 Pod 自动生成了两个后缀( `6c9qs` 与 `8trcs` ),带上了 Replica Set 的信息(在 `metadata.ownerReferences` ),其他部分基本一模一样。如果其中一个 Pod 挂掉了, K8s 会帮我们从模板中重新创建一个 Pod 。而且由于我们在 Pod 模板定义了 affinity , K8s 还会按照我们的要求自动筛选合适的节点。例如在上面 Replica Set 的例子中,创建出来的 Pod 就会尽量部署在不同的节点上。\n\n> **K8s 中对 Pod 的生存状态检查机制**\n> \n> 除了线程直接错误退出以外,还有出现死锁等等各种可能性使得容器中的应用不能正常工作。这些情况下虽然是不健康状态,但容器却不一定会挂掉。因此 K8s 提供了一些探针检查的机制来判断 Pod 是否健康。\n> K8s 主要提供了三种探针:\n> 1. **存活探针( liveness probe )** : Pod 运行时 K8s 会循环执行 liveness probe 检查容器是否健康。如果检查失败, K8s 会认为这个容器不健康,就会尝试重启容器。\n> 2. **就绪探针( readiness probe )** : 程序可能会有一段时间不能提供服务(比如正在加载数据等)。这时可能既不想杀死应用,也不想给它发送请求,这时就需要 readiness probe 。如果 readiness probe 检查失败, K8s 就会将这个 Pod 从 Service 上摘下来,直到 readiness probe 成功重新加入 Service 。\n> 3. **启动探针( startup probe )** : 有些程序会有非常长的启动时间,会有较长时间不能提供服务。这时如果 liveness probe 失败了导致重启毫无必要,此时就需要 startup probe 。 startup probe 只会在容器启动时检查直到第一次成功。直到 startup probe 成功为止, liveness probe 与 readiness probe 都不会开始执行检查。\n> \n> 而检测方式主要有:\n> 1. httpGet: 对指定的端口路径执行 HTTP GET 请求,如果返回 2xx 或 3xx 就是成功。\n> 2. tcpSocket: 尝试与容器的端口建立连接,如果不能成功建立连接就是失败。\n> 3. exec: 在容器内执行一段命令,如果退出时状态码不为 0 就是失败。\n> 4. grpc (New!): K8s 1.24 新出的检查方式,直接用 [GRPC Health Checking Protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) 对 GRPC Server 进行检查。\n\n此外, Replica Set 还提供了简易的缩容扩容功能。 kubectl 中提供了 scale 命令:\n\n```bash\nkubectl scale replicaset gateway --replicas=10\n```\n\n执行上述命令,就可以将名为 gateway 的 Replica Set 对应的副本数扩容到 10 份。当然,你也可以直接修改 Replica Set 的 `spec.replicas` 字段来实现缩容扩容。\n\n然而, Replica Set 的功能还是有限的。实际上, Replica Set 只关心跟它的 selector 匹配的 Pod 的数量。而至于匹配的 Pod 是否真的是跟 template 字段中描述的一样, Replica Set 就不关心了。因此如果单用 Replica Set ,更新 Pod 就会变得究极麻烦。\n\n### Deployment\n\n为了解决 Pod 的更新问题,我们需要有 Deployment 这种资源。实际上, Replica Set 的主要用途是提供给 Deployment 作为控制 Pod 数量,以及创建、删除 Pod 的一种机制。我们一般不会直接使用 Replica Set 。\n\n下面是实际应用中一个 Deployment 的例子:\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n labels:\n app: gateway\n name: gateway\nspec:\n replicas: 2\n revisionHistoryLimit: 10\n selector:\n matchLabels:\n app: gateway\n template:\n metadata:\n labels:\n app: gateway\n name: gateway\n spec:\n containers:\n name: gateway\n image: xxxxxxxx.amazonaws.com/gateway:xxxxxxx\n ports:\n # 下略\n```\n\n可以看到 Deployment 的 manifest 跟 Replica Set 很像。但实际上, Deployment 不会直接创建 Pod ,而是创建出一个 Replica Set ,再由 Replica Set 来创建 Pod :\n\n\n```mermaid\nflowchart TB\n\nDeployment1[Deployment]\nReplicaSet11[Replica Set]\nPod11[Pod1]\nPod12[Pod2]\nDeployment1 --> ReplicaSet11\nReplicaSet11 --> Pod11\nReplicaSet11 --> Pod12\n```\n\n比如在上面的例子中,名为 gateway 的 Deployment 创建后,就会有如下 ReplicaSet 和 Pod :\n\n```sh\n# Replica Set:\n$ kubectl get rc -l app=gateway\nNAME DESIRED CURRENT READY AGE\ngateway-9dc546658 2 2 2 5d3h\n\n# Pod:\n$ kubectl get po -l app=gateway\nNAME READY STATUS RESTARTS AGE\ngateway-9dc546658-6c9qs 1/1 Running 0 5d3h\ngateway-9dc546658-8trcs 1/1 Running 0 5d3h\n```\n\n可以看到,gateway Deployment 创建了一个 Replica Set ,然后随机给了它一个 `9dc546658` 后缀。然后 gateway-9dc546658 这个 Replica Set 又根据 template 中创建了两个 Pod ,再在自己名字的基础上加上两个后缀 `6c9qs` 与 `8trcs` 。\n\n接下来就是 Deployment 的重点了: Replica Set 只会根据 template 创建出 Pod ,而不管匹配的 Pod 到底是不是跟 template 中描述的一样。而 **Deployment 则会专门关注 template 的内容变更。**\n\n假如我们现在更新了 Deployment 的 template 中的内容提交给 K8s , Deployment 就会感知到 template 被修改了, Pod 需要更新。\n感知到更新之后, Deployment 就会创建一个新的 Replica Set 。然后逐渐将旧的 Replica Set 缩容到 0 ,并同时将新的 Replica Set 扩容到目标值。最后,所有旧版本的 Pod 将会被更新成新版本的 Pod 。如下图所示:\n\n```mermaid\nflowchart TB\n\nsubgraph A\ndirection TB\nDeployment1[Deployment]\nReplicaSet11[Replica Set]\nReplicaSet12[New Replica Set]\nPod11[Pod1]\nPod12[Pod2]\nDeployment1 --> ReplicaSet11\nDeployment1 --> ReplicaSet12\nReplicaSet11 --> Pod11\nReplicaSet11 --> Pod12\nend\n\nsubgraph B\ndirection TB\nDeployment2[Deployment]\nReplicaSet21[Replica Set]\nReplicaSet22[New Replica Set]\nPod21[New Pod1]\nPod22[Pod2]\nDeployment2 --> ReplicaSet21\nDeployment2 --> ReplicaSet22\nReplicaSet21 --> Pod22\nReplicaSet22 --> Pod21\nend\n\nsubgraph C\ndirection TB\nDeployment3[Deployment]\nReplicaSet31[Replica Set]\nReplicaSet32[New Replica Set]\nPod31[New Pod1]\nPod32[New Pod2]\nDeployment3 --> ReplicaSet31\nDeployment3 --> ReplicaSet32\nReplicaSet32 --> Pod31\nReplicaSet32 --> Pod32\nend\n\nA --> B --> C\n```\n\n整个过程完成后, Deployment 还不会将旧的 Replica Set 删除掉。我们注意到 Deployment 的声明中有这么一个字段: `revisionHistoryLimit: 10` ,表示 Deployment 会保留历史中 最近的 10 个 Replica Set ,这样在必要的时候可以立刻将 Deployment 回滚到上个版本。而超出 10 个的 Replica Set 才会被从 K8s 中删除。\n\n```sh\n# 实际中被 scale 到 0 但还没被删除的 Replica Set\n$ kubectl get rs -l app=gateway\nNAME DESIRED CURRENT READY AGE\ngateway-5c4cdf957d 0 0 0 5d4h\ngateway-5c56f6d487 0 0 0 17d\ngateway-65857cfc78 0 0 0 10d\ngateway-6bddbdd85f 0 0 0 16d\ngateway-6cc9bb5b4c 0 0 0 13d\ngateway-6f4664bc65 0 0 0 17d\ngateway-7bd667cb79 0 0 0 9d\ngateway-7d658d57f5 0 0 0 13d\ngateway-84df97d4c8 0 0 0 6d4h\ngateway-9998f4689 0 0 0 13d\ngateway-9dc546658 2 2 2 5d4h\n```\n\n### Stateful Set\n\nDeployment 中默认了我们不关心自己访问的是哪个 Pod ,因为各个 Pod 的功能是一样的,访问哪个没有差别。\n\n实际上这也符合大多数情况:试想一个 HTTP Server ,如果其所有数据都存放到同一个的数据库中,那这个 HTTP Server 不管部署在哪台主机、不管有多少个实例、不管你访问的是哪个实例,都察觉不出有什么差别。而有了这种默认,我们就能更放心地对 Pod 进行负载均衡、缩扩容等操作。\n\n但实际上我们总会遇到需要保存自己状态的 Pod 。比如我们在 K8s 里部署一个 Kafka 集群,每个 Kafka broker 都需要保存自己的分区数据,而且还要往 Zookeeper 里写入自己的名字来实现选举等功能。如果简单地用 Deployment 来部署, broker 之间可能就会分不清到底哪块是自己的分区,而且由 Deployment 生成出来的 Pod 名字是随机的,升级后 Pod 的名字会变,导致 Kafka 升级后名字与 Zookeeper 里的名字不一致,被以为是一个新的 broker 。\n\nStateful Set 就是为了解决有状态应用的部署而出现的。下面是 用 bitnami 的 Kafka Helm Chart 部署的一个 Kafka Stateful Set 的例子:\n\n```yaml\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n labels:\n app.kubernetes.io/name: kafka\n name: kafka\nspec:\n replicas: 3\n selector:\n matchLabels:\n app.kubernetes.io/name: kafka\n serviceName: kafka-headless\n template:\n metadata:\n labels:\n app.kubernetes.io/name: kafka\n spec:\n containers:\n - name: kafka\n image: docker.io/bitnami/kafka:3.1.0-debian-10-r52\n command:\n - /scripts/setup.sh\n ports:\n - containerPort: 9092\n name: kafka-client\n protocol: TCP\n volumeMounts:\n - mountPath: /bitnami/kafka\n name: data\n volumeClaimTemplates:\n - apiVersion: v1\n kind: PersistentVolumeClaim\n metadata:\n name: data\n spec:\n resources:\n requests:\n storage: 10Gi\n storageClassName: gp2\n```\n\n可以看到其实 Stateful Set 类似 Deployment ,也可以通过 replicas 字段定义实例数,如果更新 template 部分, Stateful Set 也会以一定的策略对 Pod 进行更新。\n\n而其创建出来的 Pod 如下所示:\n```sh\n$ kubectl get po -l app.kubernetes.io/name=kafka\nNAME READY STATUS RESTARTS AGE\nkafka-0 1/1 Running 1 26d\nkafka-1 1/1 Running 3 26d\nkafka-2 1/1 Running 3 26d\n```\n\n与 Replica Set 创建出来的 Pod 相比名字上会有很大差别。 Stateful Set 创建出来的 Pod 会固定的以 `-0` 、 `-1` 、 `-2` 结尾而不是随机生成:\n\n```mermaid\nflowchart TB\nrs[Replica Set A]\nrs --> A-qwert\nrs --> A-asdfg\nrs --> A-zxcvb\n\nss[Stateful Set A]\nss --> A-0\nss --> A-1\nss --> A-2\n```\n\n这样一来,更新时将 Pod 更换之后,新的 Pod 仍能够跟旧的 Pod 保持相同的名字。此外,与 Deployment 相比, Stateful Set 更新后同名的 Pod 仍能保持原来的 IP ,拿到同一个持久化卷,而且不同的 Pod 还能通过独立的 DNS 记录相互区分。这些内容后面还会详细介绍。\n\n> **宠物与牛( Cattle vs Pets )的比喻**\n> \n> Deployment 更倾向于将 Pod 看作是牛:我们不会去关心每一个 Pod 个体,如果有一个 Pod 出现了问题,我们只需要把他杀掉并替换成新的 Pod 就好。\n> \n> 但 Stateful Set 更倾向于将 Pod 看作是宠物:弄来一直完全一模一样的宠物并不是容易的事,我们对待这些宠物必须小心翼翼。我们要给他们各自一个专属的名字,替换掉一只宠物时,必须要保证它的花色、名字、行为举止都与之前那只宠物一模一样。\n\n### Daemon Set\n\n不管是 Deployment 还是 Stateful Set ,一般都不会在意自己的 Pod 部署到哪个节点。而假如你不在意自己 Pod 的数量,但需要保证每个节点上都运行一个 Pod 时,就需要 Daemon Set 了。\n\n需要保证每个节点上有且只有一个 Pod 在运行这种情况,经常会在基础结构相关的操作中出现。比如我需要在集群中部署 fluentd 采集 log ,一般来说需要在 Pod 里直接挂载节点磁盘上的文件路径。这种时候如果有一个节点上没有运行 Pod ,那个节点的 log 就采集不到;另一方面,一个节点上运行多个 Pod 毫无意义,而且可能还会导致 log 重复等冲突。\n\n这种需求下简单地使用 Replica Set 或是 Stateful Set 都是不能达到要求的,这两种资源都只能通过亲和性达到“尽量不部署在同一个节点”,做不到绝对。而且当节点数有变更时还需要手动更改设置。\n\n下面是一个用 fluent-bit helm chart 部署的 fluent-bit Daemon Set 的例子:\n\n```yaml\napiVersion: apps/v1\nkind: DaemonSet\nmetadata:\n labels:\n app.kubernetes.io/instance: fluent-bit\n app.kubernetes.io/name: fluent-bit\n name: fluent-bit\n namespace: fluent-bit\nspec:\n selector:\n matchLabels:\n app.kubernetes.io/instance: fluent-bit\n app.kubernetes.io/name: fluent-bit\n template:\n metadata:\n labels:\n app.kubernetes.io/instance: fluent-bit\n app.kubernetes.io/name: fluent-bit\n spec:\n containers:\n - image: cr.fluentbit.io/fluent/fluent-bit:1.9.5\n volumeMounts:\n - name: varlibdockercontainers\n mountPath: /var/lib/docker/containers\n readOnly: true\n - name: etcmachineid\n mountPath: /etc/machine-id\n readOnly: true\n volumes:\n - name: varlibdockercontainers\n hostPath:\n path: /var/lib/docker/containers\n type: \"\"\n - name: etcmachineid\n hostPath:\n path: /etc/machine-id\n type: File\n```\n\nSelector 之类的都是一样的了,而 Daemon Set 不能指定 replicas 。另外可以看到一个比较刺激的地方: Volume 里使用了 `hostPath` 这种 Volume ,在 Pod 里直接指定了宿主机磁盘上的路径。\n\nK8s 认为经过抽象后, Pod 不应该去关心自己在哪台宿主机上,一般来说是不推荐在 Pod 里直接访问宿主机路径的(不过也没有强制禁止)。不过 Daemon Set 是个特例,由于 Daemon Set 生成的 Pod 与节点强相关, K8s 十分推荐在且仅在 Daemon Set 的 Pod 中访问宿主机路径。\n\n### Job 与 CronJob\n\nReplica Set , Stateful Set , Daemon Set 的 Pod 中运行的一般是持续运行的程序,因此这些 Pod 运行终止后会有相应的机制重启这些 Pod 。而 Job 与 Cron Job 这两种资源则专门负责调度不会持续运行的程序。\n\n下面是 《Kubernetes in Action》 书中的一个例子:\n\n```yaml\napiVersion: batch/v1\nkind: Job\nmetadata:\n name: pi\nspec:\n completions: 5\n parallelism: 2\n template:\n spec:\n containers:\n - name: pi\n image: perl:5.34.0\n command: [\"perl\", \"-Mbignum=bpi\", \"-wle\", \"print bpi(2000)\"]\n restartPolicy: Never\n```\n\n可以看到,这个 Job 描述了一个会输出 PI 小数点后 2000 位的 Pod 模板。这个 Job 部署后,一共会以这个模板跑完 5 个 Pod ,其中最多并行跑 2 个,并在其中一个成功终止后再跑剩下的 Pod 。可以通过调整 `completions` 与 `parallelism` 字段调整并行与穿行数量。\n\n顺带一提,在 Job 定义中一般不会出现 selector ,但其实 Job 有 selector 字段,一般会由 K8s 为每个 Job 生成一个 uuid 作为 selector 。\n\n另外,可以通过部署 CronJob 这种资源来定时执行 Job 。下面是 《Kubernetes in Action》 书中关于 CronJob 的例子:\n\n```yaml\napiVersion: batch/v1beta1\nkind: CronJob\nmetadata:\n name: pi\nspec:\n schedule: \"0 0 * * *\"\n jobTemplate:\n spec:\n template:\n spec:\n containers:\n - name: pi\n image: perl:5.34.0\n command: [\"perl\", \"-Mbignum=bpi\", \"-wle\", \"print bpi(2000)\"]\n restartPolicy: Never\n```\n\n这个例子中, CronJob 会在每天的 0 点创建一个只运行一个 Pod 的 Job 。 CronJob 不会直接创建 Pod ,而是创建一个 Job ,再由 Job 创建 Pod (就像 Deployment 与 Replica Set 的关系)。另外, CronJob 创建的 Job 会限制 `completions` 与 `parallelism` 都只能等于 1 。\n\n> 关于资源的名称空间\n> \n> 在 K8s 中,各资源都是不能重名的。不能部署两个都叫 `gateway` 的 Pod ,资源之间有可能因为名字冲突而导致部署不成功。(部署一个叫 `gateway` 的 Pod 和一个叫 `gateway` 的 Deployment 倒是可以,因为 `gateway` 不是他们两个的全名,他们的全名分别叫 `pod/gateway` 及 `deployment/gateway` 。)\n> 另外我们已经知道 Deployment 等资源一般会通过标签等来管理自己创建的资源,那两份不相关的应用完全有可能会撞标签,这时候部署逻辑就有可能会出问题。\n> \n> K8s 中提供了名称空间这种资源,用于进行资源隔离。K8s 中大部分资源都从属于一个且仅从属于一个名称空间, Deployment 等资源一般只能控制在同一名称空间下的资源,而不会影响其他名称空间。\n> \n> 另外,也有一些资源是名称空间无关的,比如节点 `Node` 。\n\n\n","title":"Kubernetes 入门 (1)","abstract":"我们知道 K8s 利用了容器虚拟化技术。而说到容器虚拟化就要说 Docker 。可是,容器到底是什么? Docker 又为我们做了些什么?我们又为什么要用 K8s ?\n> 要把一个不知道打过多少个升级补丁,不知道经历了多少任管理员的系统迁移到其他机器上,毫无疑问会是一场灾难。 —— Chad Fowler 《Trash Your Servers and Burn Your Code》\n\"Write once, run anywhere\" 是 Java 曾经的口号。 Java 企图通过 JVM 虚拟机来实现一个可执行程序在多平台间的移植性。但我们现在知道, Java 语言并没能实现他的目标,会在操作系统调用、第三方依赖丢失、两个程序间依赖的冲突等各方面出现问题。","length":644,"created_at":"2022-08-13T17:45:31.000Z","updated_at":"2022-08-20T14:02:18.000Z","tags":["Kubernetes","DevOps","Docker","Cloud Native"],"license":true}},{"slug":"why-homogeneous","file":"public/content/articles/2022-07-31-why-homogeneous.md","mediaDir":"content/articles/2022-07-31-why-homogeneous","path":"/articles/why-homogeneous","meta":{"content":"## 首先,什么是线性变换?\n\n简化了一万倍来说,线性变换主要是在描述符合这两种性质的变换:一是要可加,二是要能数乘。\n也就是说,对于空间中所有向量 $$\\vec{v_1}, \\vec{v_2}$$ ,以及任意数量 $$k_1, k_2$$ ,如果有:\n$$\nA(k_1 \\vec{v_1} + k_2 \\vec{v_2}) = k_1 A(\\vec{v_1}) + k_2 A(\\vec{v_2})\n$$\n符合这种规律的 A 就叫线性变换。而一次矩阵乘法正好可以代表一次线性变换。\n\n为什么叫“线性”变换呢?感性地来说,因为它很“线”。\n\n我们可以直观地从下面这张图看出原因:\n\n![[OnOneLineWillStillOneLine_ManimCE_v0.16.0.post0.gif]]\n\n我们可以看到,在同一直线上的点,经过同一线性变换后还在同一直线上。所以它很“线”。\n\n另一方面,我们可以找一找最简单的线性变换:\n\n考虑函数:\n$$\nf(x) = k_0 x\n$$\n我们都知道这是一条过原点的直线。\n\n而从另一方面想,其实这个函数对于任意一维向量(实数) $$x_1, x_2$$ , 与任意数量(实数) $$k_1, k_2$$ , 都有:\n$$\nf(k_1 x_1 + k_2 x_2) = k_1 k_0 x_1 + k_2 k_0 x_2 = k_1 f(x_1) + k_2 f(x_2) \\\\\n$$\n\n即, xy 平面上过原点的直线(正比例函数)本身就是一种从 x 轴到 y 轴的线性变换。\n\n关于线性变换, [3blue1Brown](https://www.3blue1brown.com/topics/linear-algebra) 上有更详细更感性的介绍,大家感兴趣可以前往观看。\n\n## 为什么普通的线性变换不能表示点平移?\n\n从上面的感性介绍来看,我们知道线性变换的性质就是可加和数乘,写成等式就是:\n\n$$\nA(k_1 \\vec{v_1} + k_2 \\vec{v_2}) = k_1 A(\\vec{v_1}) + k_2 A(\\vec{v_2})\n$$\n\n而当两个向量都为零向量时,等式就会简化成:\n\n$$\nA(\\vec{0}) = A(\\vec{0}) + A(\\vec{0})\n$$\n\n解一下方程,就可以知道,对任意线性变换 A,都会有:\n\n$$\nA(\\vec{0}) = 0\n$$\n\n也就是说,不管是哪个线性变换 A ,原点经过变换后都必须只能是在原点不变。如果变换后原点的位置变了,那它就一定不是线性变换。\n\n我们从下图也可以看出,对于切变 $$\\begin{pmatrix}1 & 1 \\\\ 0 & 1\\end{pmatrix}$$ 、伸缩 $$ \\begin{pmatrix}2 & 0 \\\\ 0 & \\frac{1}{2}\\end{pmatrix} $$、旋转 $$ \\begin{pmatrix}\n \\frac{\\sqrt{3}}{2} & -\\frac{1}{2} \\\\ \\frac{1}{2} & \\frac{\\sqrt{3}}{2}\n\\end{pmatrix} $$ 这些经典的线性变换,变换后原点都不会变。\n\n![[SliceScaleRotateForOrigin_ManimCE_v0.16.0.post0.gif]]\n\n但是平移这种变换不一样。原点经过平移后,是一定不会还留在原点的。因此平移不是一种线性变换,自然也不能用矩阵来表示。\n\n## 为什么基于齐次坐标下的线性变换就可以表示平移?\n\n我们先来看一下齐次坐标做了些什么。\n\n在上面传统的线性变换中,我们不会考虑向量与点的区别。一个二维坐标 $$(x, y)$$ 既能代表那个坐标上的点,也能代表从原点到 $$(x, y)$$ 的向量。这时,点与向量是一一对应的。\n\n但如果要考虑平移,点与向量就不能再一一对应了,因为对向量平移没有意义(不考虑物理中力矩的场景)。\n所以在齐次坐标下,我们需要区分这个坐标代表的是点还是向量。\n\n以二维空间为例,齐次坐标就是在二维空间上加了第三个维度 w 轴,二维空间里的点在 w 轴上的值为 1 ,而二维向量在 w 轴上的值对应为 0 :\n\n$$\n\\begin{align}\n P &= \\begin{pmatrix}x & y & 1\\end{pmatrix} \\\\\n \\vec{v} &= \\begin{pmatrix}v_x & v_y & 0\\end{pmatrix}\n\\end{align}\n$$\n\n从字面上看可能还是不太明显,让我们试着把二维空间齐次坐标强行转化为三维空间坐标看看:\n\n![[HomogeneousTransform_ManimCE_v0.16.0.post0.gif]]\n\n我们发现,原来二维空间中的点,被投射到三维空间中 w = 1 的平面上了!\n\n这样一来,二维空间齐次坐标下的平移矩阵也很好理解了:\n\n$$\n将平面沿向量 (x, y) 平移:\n\\begin{pmatrix}\n 1 & 0 & x \\\\\n 0 & 1 & y \\\\\n 0 & 0 & 1\n\\end{pmatrix}\n$$\n\n这不就是三维空间中在 w 轴上做切变时的变换矩阵嘛!\n\n我们可以重点关注一下 $$\\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix}$$ 这个向量。\n从齐次坐标的定义来看,这个向量对应着二维空间中的原点 $$P_{Origin} = \\begin{pmatrix} 0 \\\\ 0 \\end{pmatrix}$$ 。而由矩阵乘法计算可知,经过 $$ A = \\begin{pmatrix} 1 & 0 & x \\\\ 0 & 1 & y \\\\ 0 & 0 & 1 \\end{pmatrix} $$ 对应的线性变换后, $$ \\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix} $$ 这个向量会被映射到 $$ \\begin{pmatrix}x\\\\y\\\\1\\end{pmatrix} $$ 上。也就是说,二维空间原点 $$ P_{Origin} = \\begin{pmatrix}0\\\\0\\end{pmatrix}$$ 经过变换后会变为 $$ P_{Origin}' = A(P_{Origin}) = \\begin{pmatrix}x\\\\y\\end{pmatrix}$$ 。\n\n而对于二维空间中的向量 $$\\vec{v}=\\begin{pmatrix}v_x\\\\v_y\\end{pmatrix}$$ ,其齐次坐标下 w 轴方向分量为 0 ,因此 w 轴方向上的切变并不会影响二维空间中的向量。即 $$ \\vec{v'} = A(\\vec{v}) = \\vec{v} $$ 。\n\n而对于原来二维空间中的其他点的坐标:\n$$\nP = \\begin{pmatrix}x_0\\\\y_0\\end{pmatrix}\n$$ \n其实可以理解为原点坐标再加上一个偏移向量:\n$$\nP = \\begin{pmatrix}0\\\\0\\end{pmatrix} + \\begin{pmatrix}x_0\\\\y_0\\end{pmatrix} = P_{Origin} + \\vec{v}_{x,y}\n$$\n\n而在齐次坐标下,点坐标 = 原点坐标 + 偏移向量 这一等式仍然成立:\n$$\nP = \\begin{pmatrix}x_0\\\\y_0\\\\1\\end{pmatrix} = \\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix} + \\begin{pmatrix}x_0\\\\y_0\\\\0\\end{pmatrix} = P_{Origin} + \\vec{v}_{x,y}\n$$\n\n而由于切变是线性变换,因此有:\n\n$$\n\\begin{align}\nP' &= A(P) \\\\\n&= A(P_{Origin} + \\vec{v}_{x,y}) \\\\\n&= A(P_{Origin}) + A(\\vec{v}_{x,y}) \\\\\n&= P_{Origin}' + \\vec{v}_{x,y} \\\\\n\\end{align}\n$$\n\n因为切变前后偏移向量没有发生变化,因此二维空间上的点经变换后相对于原点的方向、距离都没有发生变化。由此也可得出,原先由二维空间中的点组成的图案,经齐次坐标下 w 轴的切变后,其大小、形状、方向都不会发生变化。\n\n![[SliceOnHomogeneousWithGraph_ManimCE_v0.16.0.post0.gif]]\n\n而这种大小、形状、方向都不变化,只有整体位置发生了变化的变换,正是我们一般所说的“平移”。因此在齐次坐标下,我们能通过线性变换(aka 矩阵乘法)表示平移。\n\n> 其实 $$\\begin{pmatrix}1 & 0 & x \\\\0 & 1 & y \\\\0 & 0 & 1\\end{pmatrix}$$ 对应切变作用后各点坐标如何变化这个过程, 3Blue1Brown 的[这个视频](https://www.3blue1brown.com/lessons/matrix-multiplication) 有更直观明了的解释,大家可以参考。\n\n## 总结一下\n\nQ: 为什么普通的矩阵乘法不能表示平移?\nA: 因为矩阵乘法只能表示线性变换。平移不是线性变换。\n\nQ: 为什么在齐次坐标下的矩阵乘法又能表示平移?\nA: 因为齐次坐标增加了一个维度。平移变换矩阵其实是在新增的这个维度上做切变(一种线性变换)。切变后的结果正好就是原坐标中的平移变换。\n\n\n","title":"为什么使用在齐次坐标下矩阵乘法能表示点平移?","abstract":"简化了一万倍来说,线性变换主要是在描述符合这两种性质的变换:一是要可加,二是要能数乘。\n也就是说,对于空间中所有向量 $$\\vec{v_1}, \\vec{v_2}$$ ,以及任意数量 $$k_1, k_2$$ ,如果有:\n$$","length":149,"created_at":"2022-07-31T15:35:17.000Z","updated_at":"2022-08-05T17:45:09.000Z","tags":[],"license":true}},{"slug":"graph-for-economics-2","file":"public/content/articles/2022-07-19-graph-for-economics-2.md","mediaDir":"content/articles/2022-07-19-graph-for-economics-2","path":"/articles/graph-for-economics-2","meta":{"content":"\n> 1. 这篇文章参考了曼昆的《经济学原理》与北京大学王辉老师的《微观经济学》课程,内容上会有部分相似。\n> 2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。\n\n上一篇讲供给,这一篇讲需求。\n其实供给与需求有很多相似的地方,有时只需要套一下上一篇中给出的模型就能求解。因此各位如果还没有看过上一篇,可以先看完再回来看这一篇内容也不迟。\n\n讲需求曲线的时候,我们会先假设消费者是一个理性人,做决策时会将成本与收益作比较。比如在买冰酒这个场景,我们就会比较冰酒的价格与我们喝冰酒爽到的满足感(意愿支付价格),如果冰酒价格低于意愿支付价格我们就会去买这瓶冰酒。\n\n而假如我们是葡萄庄园主,我们当然也会去比较各种收益与成本,来决定是否制作冰酒拿出去卖。\n\n当我们选择制作冰酒拿出去卖,那收益自然就是卖冰酒所能拿到的钱,也就是冰酒的价格。\n\n但选择制作冰酒拿出去卖的成本呢?\n\n# 机会成本\n\n有一般会计常识的人可能会很快答出:成本不就是买种子、种葡萄、酿酒等过程中花掉的钱嘛!\n\n但实际上问题没有这么简单。因为我们的决策是“制作冰酒拿出去卖”,所以我们必须计算做出决策和不做出决策两种情况之间的差别。\n如果决定不制作冰酒拿出去卖,那我们可以省下一大笔时间与资金。我们可以拿着这些时间与资金去做其他事情,比如可以去种西瓜卖,可以去投资,甚至可以去打工当码农。这些活动都可以获得收益。\n而如果决定去制作冰酒拿出去卖,就代表你为了获得卖冰酒的收益,要选择放弃上面这些活动中获得的收益。“放弃获得这些收益的机会”也是你卖冰酒的成本。\n\n为了与常识中所说的成本做区别,我们把这种算上放弃收益机会的成本称为**机会成本**。经济学中常说的成本的也是机会成本。\n\n> **机会成本**( Opportunity cost ):是指为了得到某种东西所必须放弃的东西。 _ 曼昆《经济学原理:微观经济学分册》:Page 52\n\n## 生产者的理性人决策模型\n\n还记得理性人决策的模型吗?不记得的话可以先看看上一篇。\n引入了机会成本这一概念,我们套模型的三个要素就都准备好了:\n- 做决策 = 生产冰酒拿出去卖\n- 收益 = 冰酒价格(卖冰酒拿到的钱)\n- 成本 = 机会成本\n\n那么我们就有:\n- if 冰酒价格 > 机会成本 : 生产冰酒拿出去卖\n- if 冰酒价格 < 机会成本 : 不生产冰酒\n\n[[图片:机会成本柱,价格线,线高于柱做决策,线低于柱不做决策]]\n\n假设我们是葡萄庄园主,而且已经能预知冰酒市场价稳定在每瓶 50 元。如果我们荒废掉庄园拿钱去投资,就算减去掉投资风险,赚的钱都比卖冰酒赚的钱多得多,那我们就会毫不犹豫地荒废掉庄园选择躺着赚钱。这种其实就是机会成本高于交易收益的情况。\n\n## 生产者剩余\n\n如果收益高于机会成本,那我们就会毫不犹豫地选择生产冰酒拿出去卖。为啥?因为能赚钱呀!赚钱嘛,不寒掺。\n\n“赚钱”,可能就是生产者剩余最贴切地解释了。因为卖冰酒获得的收益(冰酒价格),比制作冰酒的成本(制作冰酒所放弃的其他收益加上制作冰酒耗费的金钱,也就是机会成本)更多,我们称在卖出冰酒的过程中我们获得了生产者剩余。\n\n> **生产者剩余**( producer surplus ):卖者出售一种商品得到的量减去其生产成本。Page 140\n\n与消费者剩余类似,生产者剩余计算上表示为:\n$$\n生产者剩余 = 卖出商品得到的量 - 卖出商品所支付的金钱\n$$\n表示为图的话就是商品价格与成本之间的部分:\n[[图片:生产者剩余柱,价格线,线与柱之间的部分]]\n\n## 供给曲线\n\n与上一篇里消费者情况类似,市场中的生产者也不会只有我们一个。我们把市场中(可能)卖冰酒的人全部抓过来审问一遍,统计一下他们卖冰酒的机会成本,从低到高排个序后就得到下面的图:\n\n[[图:机会成本柱状图,从低到高连成曲线,冰酒价格横线,线高于柱做决策,线低于柱不做决策]]\n\n与上一篇需求曲线过程类似,把各人机会成本连成曲线,我们就得到了冰酒市场中的供给曲线。\n\n供给曲线与冰酒价格交点的左边,由于这些生产者机会成本小于商品价格,能获得生产者剩余,他们就会选择制作并卖出冰酒(进入市场)。假设他们全部都能卖出冰酒,那他们卖出冰酒的量就是并就的交易量,他们的生产者剩余总和,也就是需求曲线以上价格线以下的部分,就是冰酒市场中总的生产者剩余。\n\n[[图:需求曲线与价格的交点,纵坐标横坐标解释,价格变动后,纵坐标与横坐标变化解释,交点连续变为曲线]]\n\n与上一篇同理,由于机会成本比较高,处于供给曲线与价格线交点右方的那些人不会选择制作冰酒,因此他们并不会在冰酒市场获得或失去生产者剩余。\n\n# 均衡\n\n上面分析供给曲线,包括上一篇中分析需求曲线时,我们都是先假设先有一个价格,然后再分析如果价格高了会怎么样,如果价格低了会怎么样。\n\n可是这个价格是谁来定的?\n\n## 完全竞争市场\n\n为了分析这个问题,我们需要引入除理性人假设外第二个假设:完全竞争市场假设:\n\n我们假设市场是完全竞争的,这样的市场必须具有三个特征:\n1. 消费者能自由选择购买任一生产者的商品\n2. 市场中的商品都是完全相同的\n3. 买卖双方都人数众多\n\n在这种假设下,市场中所有商品价格都相等,且没有任何一个消费者或生产者能够影响市场价格。因为如果有一个生产者的商品价格高于市场价,消费者们就会到别的地方购买;而由于他们都是理性人,没有生产者会打算以低于市场价的价格出售商品。消费者角度也同理:没有理由用高价买商品,而低价将买不到商品。\n\n这时,我们就可以把需求曲线与供给曲线放在一起分析,由于完全竞争市场中:\n$$\n任意消费者购买商品的价格 = 任意生产者出售商品的价格\n$$\n因此市场中商品价格是固定值,是水平于供给量/需求量的横线。\n[[图:需求曲线与供给曲线,价格横线只有一条,高于交点时与低于交点时]]\n\n另外,我们看到供给曲线与需求曲线之间有一个交点。接下来我们就要针对这个交点,解决完全竞争市场中价格由谁来定的问题。\n\n## 市场趋向于均衡\n\n首先我们考虑如果价格高于交点时的情况。\n\n前面我们说过,只有价格线与需求曲线交点左边的消费者会购买商品,而只有价格线与供给曲线交点左边的生产者会生产并出售商品。因此我们可以直观地知道,这时商品供给量比需求量要多。那多出来的那一部分商品一定会卖不出去。\n\n卖不出去咋办呀?那就只能降价。\n之前我们说完全竞争市场中生产者没有低价出售商品的理由,那是建立在商品都能卖出去前提下的。商品都能卖出去时没有道理自损利益,但现在商品卖不出去就只能降价吸引客流了。(其实提升商品质量也增加售出量是一种好方法,但我们这里假设了是完全竞争市场,所有商品都完全相同)\n\n消费者们也都是理性人,既然商品完全相同,自然就会选择购买更低价的商品。原本还凑合着能卖出去的那部分商品反而因为未降价变得卖不出去了,自然他们也会选择降价,最终市场中商品的价格整体降低。\n\n市场价降低,使得一部分生产者的机会成本高过了收益,这一部分生产者就会选择离开,使得供给量下降。另一方面,降价使得价格低过了一部分潜在消费者的意愿支付价格,这一部分人就会选择购买商品,使得需求量上升。\n\n[[图:均衡P81(Eng P77),价格高于均衡的情况]]\n\n而另一种情况,也相类似。如果价格低于交点,市场中商品的需求量就会大于供给量。这时必然会有一部分人想买但是买不到商品,他们就会逐渐选择用更高的价格来购买,最终拉高整个市场中的商品价格。市场价升高,使得供给量上升,需求量下降。\n\n[[图:价格低于均衡的情况]]\n\n价格高于交点时会趋于降价,而价格低于交点时会趋于涨价。在充分选择的情况下,最终市场价会等于交点处商品价格。这时市场达到均衡,生产者生产出的所有商品都能卖出,所有消费者都能购买到他们所需的商品。\n\n## 市场效率与福利\n\n之前我们提过消费者剩余与生产者剩余的概念。\n\n消费者剩余 = 意愿支付价格 - 商品价格,如果一个消费者在一场交易中消费者剩余越大,他就感觉越赚,他就对这场交易越满意。对于市场中所有消费者都是如此,因此市场中所有的消费者剩余,也就是需求曲线以下价格线以上的面积,代表了市场中所有消费者对市场交易的满意度。\n\n同样的,生产者剩余 = 商品价格 - 机会成本。市场中所有的生产者剩余,就是价格曲线以下供给曲线以上的部分,代表了市场中所有生产者对市场交易的满意度。\n\n因此,市场中总的生产者剩余加上总的消费者剩余,代表了市场中所有人对市场交易的满意度。我们称这就是市场中的总剩余。\n\n在市场均衡情况下,我们很容易地就能知道总剩余是多少。由于市场均衡时市场中所有的生产者与所有的消费者都能达成交易,而交易价格就是均衡价格。因此我们很容易地就能在图中找到代表生产者剩余、消费者剩余与总剩余的面积。\n\n[[图]]\n\n而如果市场没有达到均衡,会出现有的消费者没能买到商品、或是生产者的商品没能卖出去的情况。这些时候,没能买到商品的消费者与没能卖出商品的生产者自然不会对交易满意(因为没能达成交易),自然也不计算剩余。而市场中商品的交易量取决于需求量与供给量中更小的一方。无论如何,总剩余总会小于均衡时的总剩余。\n\n[[图]]\n\n由此可以看出,只有当市场达到均衡的时候,加入市场的所有人对市场交易的满意度最大。而市场均衡是完全竞争的自由市场中会自发达到的状态。\n\n因此,从经济学的观点来看,在所有人都是理性人、市场是完全竞争市场的假设下,不需要任何外加的制度或政策,市场就会自发地达到人们满意度最高的状态。也正因如此,亚当·斯密才会说市场是一个看不见的手。\n\n# 总结一下\n\n\n\n\n\n\n\nneeded:\n- [ ] svg character\n- [ ] bar graph\n- [ ] function graph\n- [ ] manim command for output path and input path https://docs.manim.community/en/stable/tutorials/configuration.html\n- [ ] want mp4 as picture\n- [ ] interactive manim https://github.com/3b1b/3Blue1Brown.com/tree/main/public/content/lessons/2021/newtons-fractal\n\n\n# 税收,污染权与外部性,国际贸易\n\n※\n# 比较优势\n# 弹性与均衡移动,收益分析 \n# 生产成本,垄断,寡头\n# 生产要素市场\n\n宏观:\n重点:\n1. 三个指标:GDP,价格水平,就业 =》 促使政府调节经济\n2. 两种政策:\n 1. 货币政策 =》 名义利率,量化宽松,前瞻指引,汇率决定制度等\n 2. 财政政策 =》 加息、采购\n3. 两种政策对经济影响(总供给总需求模型),以及国际经济与贸易影响\n\n# 三个指标与两种政策\n## 三个指标\n## 经济增长的原因 —— 全要素生产率\n\n## 经济波动\n## 凯恩斯主义 —— 促进政府调节经济\n## 调节经济两种政策\n# 财政政策\n# 货币政策\n\n## 两种政策对经济影响 —— 总供给总需求模型\n## 国际经济\n\n","title":"图解经济学原理(2)","abstract":"> 1. 这篇文章参考了曼昆的《经济学原理》与北京大学王辉老师的《微观经济学》课程,内容上会有部分相似。\n> 2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。\n上一篇讲供给,这一篇讲需求。","length":188,"created_at":"2022-07-19T23:12:48.000Z","updated_at":"2022-08-13T09:53:03.000Z","tags":[],"license":true}},{"slug":"graph-for-economics-1","file":"public/content/articles/2022-06-28-graph-for-economics-1.md","mediaDir":"content/articles/2022-06-28-graph-for-economics-1","path":"/articles/graph-for-economics-1","meta":{"content":"\n> 1. 这篇文章参考了曼昆的《经济学原理》与北京大学王辉老师的《微观经济学》课程,内容上会有部分相似。\n> 2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。\n\n我们先不讲课,先来带个货。\n\n德国和加拿大有一种特别的葡萄酒,叫冰酒。这种葡萄酒制作工艺比较特殊,必须要在严冬葡萄被霜冻在藤曼上时采摘下来,再经过发酵、压榨酿造而成。\n在冰酒压榨过程中,大量的冰被去除,使得葡萄的成分得到浓缩,因此冰酒口感偏甜。但加工过程对温度要求十分苛刻,温度过高、过低、变化太过剧烈等都会对口感产生影响。由于冰酒工艺特别,主要只有德国、加拿大等少数地区生产。\n\n好了,现在来考虑一个消费场景:\n假如你到加拿大去旅游,回国时在机场看到有礼品店在卖冰酒,考虑买一瓶冰酒回国后自己消费饮用。这瓶冰酒容量大概为 400ml ,并且这瓶冰酒只是一个普通牌子,不是奢侈品或高档品牌。\n在这个场景下,请大家考虑两个问题:\n1. 假设商店中这瓶冰酒标价换算为人民币是 ¥100 ,你是否愿意以这个价格购买一瓶冰酒?\n2. 假设你还不知道这瓶冰酒的价格,你会选择购买的最高价格是一瓶冰酒多少元?\n\n在问题 2 中,你愿意支付的最高价格就是你的心理价位,如果商店价格高于心理价位,你就不会购买这件商品。而如果价格低于心理价位,你就会购买这件商品。\n\n# 意愿支付\n\n上面场景中所说的心理价位,在经济学中又叫**意愿支付**。\n\n> **意愿支付**( willingness to pay ):是消费者愿意为获得某种物品所支付的最高代价。 —— 曼昆《经济学原理:微观经济学分册》: Page 134\n\n如果这瓶冰酒定价是 100 元,你会觉得太贵了不买,就说明你的意愿支付价格低于 100 元。\n然后我们再假设这瓶冰酒 50 元,你觉得很好很便宜,选择买了,就说明你的意愿支付价格在 100 元到 50 元之间。\n再把范围收窄一点, 80 元选择不买, 60 元选择买。范围逐渐收敛,买与不买之间的意向会变得越来越模糊。我们假设最终收敛到 70 元,你变得非常犹豫,感觉买与不买没有差别,那 70 元就是你的意愿支付价格。\n\n[[图片:意愿支付价格逼近。价格100时,不买,价格50时,买,80,60,最后70时会犹豫]]\n\n## 意愿支付价格的经济学解释\n\n经济学上有理性人这一概念,实际上是在假设你在决定是否做决策时,会将成本与收益做比较:\n- if 收益 > 成本 : 做出决策\n- if 收益 < 成本 : 不做出决策\n\n在购买冰酒的场景中,做决策就是指“买冰酒”这个行为。而成本就是冰酒的价格,收益就是你喝下冰酒感觉“爽到”。\n而我们不是机器人,我们“爽到”的感觉是很难与价格这种数字相比较的。因此我们要找一个办法把我们的爽到量化为价格。\n而意愿支付价格就是这个办法。在上面的例子中,买与不买的价格范围不断逼近,最终到 70 元时你觉得买还是不买都没什么区别。也就是说你喝冰酒爽到,就相当于得到了 70 元。\n\n> **意愿支付价格**:商品消费行为给消费者带来的效用的货币度量。\n\n\n有了意愿支付价格,买冰酒这件事就很容易模型化了。我们可以直接套回理性人决策的模型:\n- 做决策 = 买冰酒\n- 收益 = 意愿支付价格\n- 成本 = 冰酒价格\n\n则有:\n- if 意愿支付价格 > 冰酒价格 : 买冰酒\n- if 意愿支付价格 < 冰酒价格 : 不买冰酒\n\n[[图片:意愿支付价格=收益柱=70元,价格=成本=线,线高于柱=不决策,线低于柱=决策]]\n\n在买冰酒这一决策中,你的收益就是 70 元(喝冰酒爽到)。要你花 100 元(冰酒价格)来换 70 元,你肯定是不干的。而要你花 50 元来换 70 元,你就会爽快答应了。\n\n## 消费者剩余\n\n在上面模型中,如果你的意愿支付价格是 70 元,而冰酒只卖 50 元,你就一定会买买买,因为只要花 50 元就能买到 70 元的“爽到”呀!买到就是赚到。\n\n70 元的“爽到”只要花 50 元就能买到,这中间就差了 20 元呢,你就会觉得买冰酒的这笔钱花得真值,赚到 20 元。经济学上就称这是得到了 20 元的消费者剩余。\n\n> **消费者剩余**( consumer surplus ):买者原意为一种物品支付的量减去其为此实际支付的量。 —— 曼昆《经济学原理:微观经济学分册》: Page 135\n\n计算上:\n$$\n消费者剩余 = 意愿支付价格 - 商品价格 \n$$\n而实际上,消费者剩余是你买商品时赚到的感觉,是这种感觉的量化。\n你感觉买这瓶冰酒赚飞了,量化后表现为这次交易你获得的消费者剩余多;你感觉这次交易一般般,有点小贵(但还是愿意买),量化后就是这次交易你获得的消费者剩余少。你获得多少消费者剩余,就代表你在这场交易中赚到了多少(感觉上)。\n\n现在考虑另一种情况:你的意愿支付价格为 70 元,而冰酒价格为 100 元时。你没有选择交易,因此在这种情况,你没有获得消费者剩余,当然也没有失去消费者剩余。\n从另一个角度来说,你觉得交易成立后你会得到负的消费者剩余,因此机制的你决定不交易,防止了这次损失。\n\n话又说回来,实际情况中人的决策是不可能这么理性地去比较成本与收益,甚至有可能根本得不出一个意愿支付价格。因此上述讨论都是建立在假设上的——假设理性人模型成立。\n在实际情况中,这一假设可能根本不成立,因此这些讨论在现实中可能根本不适用。可这又有什么关系呢?就算相对论是正确的,牛顿定理仍然有他价值不是吗?\n\n# 需求曲线\n\n好了,上面说了一大堆,其实都是单个消费者(你)进行消费的情况。可实际上,这冰酒总不可能只有一个人买呀!\n\n而实际上,每个人对冰酒的爱好、口感要求、奢侈品需求等都是不同的。这就导致了每个人对冰酒这一商品的意愿支付价格可能都不一样!\n\n## 意愿支付价格统计\n\n我们假设,今天其实有包括你在内的 100 个客人都来过这家冰酒店。我把这 100 个客人全部逮住,按顺序每个人都审问了一遍意愿支付价格。于是得到了这样一幅意愿支付价格统计的图:\n\n[[图:意愿支付价格柱状图,乱序,横坐标是到店时间,纵坐标价格,横线为冰酒价格,上下浮动,意愿支付价格超过冰酒价格就会购买]]\n\n如果冰酒价格为 80 元,那所有意愿支付价格超过 80 元的客人都会选择买冰酒,而意愿支付价格低于 80 元的人都不会选择买。而如果冰酒价格为 60 元,那意愿支付价格超过 60 元的那部分客人也会开始选择买。冰酒价格越低,选择买冰酒的客人就越多。\n\n可是这图有点乱:\n1. 看不出客人意愿支付价格的分布\n2. 如果有 200 个客人到店,对应价格的冰酒又会有多少人买?\n\n## 需求曲线\n\n为了处理上面提出的两个问题,我把客人按照意愿支付价格从高到低来了个快速排序,然后把柱状图连成了一条曲线:\n\n[[图:快速排序,意愿支付价格从高到低,然后连成曲线,最后还是有冰酒价格横线]]\n\n我们能看到,代表冰酒价格(市场价格)的横线与曲线形成了一个交点。交点左边的客人都会选择买冰酒,而右边的人都会选择不买。\n冰酒价格下降,交点右移,选择购买冰酒的客人就会变多;冰酒价格上升,交点左移,购买的人就会变少。因此交点的横坐标就是购买冰酒的人数,也就是冰酒交易量。\n\n假设每个客人只会买一瓶冰酒,那么交点的横坐标同时也就是冰酒的需求量(实际上有客人不止买一瓶冰酒也没关系,我们可以当是来了两个客人)。而交点的纵坐标当然就是冰酒的价格。\n冰酒价格变化,交点位置也会变化,对应需求量也跟随发生变化。这条曲线描绘的就是冰酒需求量随冰酒价格变化的关系。\n\n[[图:需求曲线与价格的交点,纵坐标横坐标解释,价格变动后,纵坐标与横坐标变化解释,交点连续变为曲线]]\n\n我们称这条曲线为冰酒的需求曲线。\n\n> **需求曲线**( Demand Curve ):表示一种物品价格与需求量之间关系的图形 —— 曼昆《经济学原理:微观经济学分册》: Page 68\n\n像这样用需求曲线表示价格与需求量的关系,可以解决上面的两个问题:\n1. 客人意愿支付价格的分布就是需求曲线的形状(虽然我们为了简化只画直线,但其实曲线形状也是可以上凸下凹,甚至是S形的)\n2. 如果客人数量翻倍,我们一般认为新来的 100 人意愿支付价格分布跟原先 100 人的分布几乎相同,因此需求曲线形状不变,横坐标轴缩短一半(或者说图形横向拉伸一倍)就是我们要的结果了。\n\n值得一提的是,数学中我们常把横坐标当作自变量,而纵坐标表示因变量。但需求曲线中正相反,纵坐标的价格是自变量,需求量才是因变量。\n我记得高中老师一般都会说这是因为经济学家不懂数学,然后草草带过。但实际上,消费者意愿支付多少钱容易统计,而不同价格下到底有多少人会想买难以统计。通过统计意愿支付价格并排序生成需求曲线时,将价格放在纵轴是一种很合理的选择。马歇尔当初也是在这一框架下推导出需求曲线的,曼昆也在他的[这篇博客](http://gregmankiw.blogspot.com/2006/09/who-invented-supply-and-demand.html)中对此有过讨论。\n(实际上,马歇尔是从效用理论推导出需求曲线的,与我们上面推导的过程不一样,但总的来说还是在同一框架下。说马歇尔不懂数学,就像是在说薛定谔不懂数学——怎么可能嘛。)\n\n\n\n## 市场上所有的消费者剩余\n\n店里来了这么多人,每个人意愿支付价格都不一样,那每个人买到同样价格的冰酒,感觉赚到的程度肯定是不一样的。\n\n[[图:柱状图,展示个人的消费者剩余,然后到线图,展示面积,即市场中的消费者剩余]]\n\n对于单个人来说,他的消费者剩余就是意愿支付价格减去商品价格,也就是柱子在价格线以上标橙色的部分。\n\n可现在到店里的不止一个人啊,我要算所有消费者一共感觉赚到了多少。那我就要把所有橙色部分加起来,也就是做了一个积分,积分的结果就是到店里所有人通过买冰酒这件事一共能赚多少了。大家别看到积分就怕,其实意思就是求需求曲线以下,价格线以上这一三角形的面积。\n(每个人买冰酒价格肯定是固定的。总不能对不同的人以不同的价格出售吧)\n\n值得一提的是,在价格线与需求曲线交点右边的这些人,是不算消费者剩余的,也不会使总的消费者剩余减少。因为他们嫌冰酒太贵(高于意愿支付价格),根本就没有买冰酒(达成交易)。\n\n\n# 总结一下\n\n","title":"图解经济学原理(1)","abstract":"> 1. 这篇文章参考了曼昆的《经济学原理》与北京大学王辉老师的《微观经济学》课程,内容上会有部分相似。\n> 2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。\n我们先不讲课,先来带个货。","length":139,"created_at":"2022-06-28T00:59:41.000Z","updated_at":"2022-06-28T14:24:43.000Z","tags":[],"license":true}},{"slug":"use-paste-image-and-vscode-memo","file":"public/content/articles/2022-04-03-use-paste-image-and-vscode-memo.md","mediaDir":"content/articles/2022-04-03-use-paste-image-and-vscode-memo","path":"/articles/use-paste-image-and-vscode-memo","meta":{"content":"\n我平时使用 [vscode-memo](https://github.com/svsool/vscode-memo) 插件写笔记,其中插入图片使用 `![[]]` 语法,显示简短,也有较好的预览支持,体验极佳。希望这种特性也能在写 hexo 博客的时候使用。\n\n# 关于 vscode-memo\n\n可能有很多人不熟悉 vscode-memo 这个插件,我先来简单介绍一下。\n\nvscode-memo 定位是一个 knowledge base ,对标的是 [Obsidian.md](https://obsidian.md/) 等软件。其功能包括且不限于:\n\n1. 使用独有的短链接语法 `[[]]` 连接到其他文档与图片。\n2. 修改文件名时自动同步更新链接,反向查找当前文档被那些文档链接。\n3. 鼠标悬停时能预览链接与图片。\n\n同时,由于 vscode-memo 是个 vscode 插件,可以跟 vscode 的其他众多插件合作使用。比如 [vscode-memo 官方文档](https://github.com/svsool/vscode-memo/blob/master/help/How%20to/Pasting%20images%20from%20clipboard.md)里就推荐将 vscode-memo 与 vscode-past-image 插件配合,粘贴图片。\n\n这篇文章主要的目的,也是利用这两个插件,达到把图片粘贴为短链接,并被 Hexo 正常渲染为网页。\n\n# Image Paste 与 Hexo 的配置\n\n这一步其实很简单。\n\n在 Hexo 的文章中,一般需要使用从根目录起的相对链接。如有文件结构:\n\n```tree\nsource\n├───img\n│ └───in-post\n│ ├───heap-cheat-sheet.jpg\n│ └───post-js-version.jpg\n├───playground\n└───_posts\n ├───2022-03-26-create-blog-cicd-by-github.md\n └───2022-04-03-use-paste-image-and-vscode-memo.md\n```\n\n在 `2022-03-26-create-blog-cicd-by-github.md` 中引用 `heap-cheat-sheet.jpg` 这个图片,就需要 `![](/img/in-post/heap-cheat-sheet.jpg)` 这样的链接。\n\n但如果在配置里把 `post_asset_folder` 设为 `true` ,就可以在 Markdown 文件的同级位置的同名目录中直接找到图片。如:\n\n```tree\nsource\n├───img\n├───playground\n└───_posts\n ├───2022-03-26-create-blog-cicd-by-github\n │ ├───heap-cheat-sheet.jpg\n │ └───post-js-version.jpg\n ├───2022-03-26-create-blog-cicd-by-github.md\n └───2022-04-03-use-paste-image-and-vscode-memo.md\n```\n\n然后在 `2022-03-26-create-blog-cicd-by-github.md` 中可以直接 `![](heap-cheat-sheet.jpg)` 引用图片。为了图片文件管理方便,我们打开这个配置项。\n\n为了能让 Image Paste 粘贴的图片能放到这个同名文件夹下,我们需要修改 Image Paste 配置,在 VSCode 的 Workspace Setting 中,添加如下设置:\n\n```json\n{\n \"pasteImage.path\": \"${currentFileDir}/${currentFileNameWithoutExt}/\"\n}\n```\n\n# Image Paste 粘贴为 vscode-memo 短链接格式\n\n这一步也很简单。 Image Paste 可以设定粘贴后的格式。我们在 Workspace Setting 中添加如下设置即可:\n\n```json\n{\n \"pasteImage.insertPattern\": \"![[${imageFileName}]]\",\n}\n```\n\n这样我们粘贴后的图片就能有预览功能了。\n\n# 让 Hexo 正确渲染 vscode-memo 的短链接\n\n这一步其实是最难的。 Hexo 当然不认识 vscode-memo 的短链接,而经过调查,现在还没有现成的方案让 Hexo 与 vscode-memo 集成。虽然我们提倡尽量不要重复造轮子,但这里我们也是除了造轮子没有其他办法了。\n\n我们采用的方案是让 Hexo 在渲染 Markdown 前,先把 Markdown 中形如 `![[]]` 的短链接,替换为 `![]()` 的正常 Markdown 图片链接。\n\n假设我们项目 `source` 文件夹如下:\n\n```tree\nsource\n├───playground\n└───_posts\n ├───2022-03-26-create-blog-cicd-by-github\n │ ├───heap-cheat-sheet.jpg\n │ └───post-js-version.jpg\n ├───2022-03-26-create-blog-cicd-by-github.md\n └───2022-04-03-use-paste-image-and-vscode-memo.md\n```\n\n如在渲染 `2022-03-26-create-blog-cicd-by-github.md` 前,需要将其中的 `![[heat-cheat-sheet.jpg]]` 替换为 `![](heap-cheat-sheet.jpg)` 。我们知道 Hexo 在生成静态文件前会先把项目根目录下 `scripts` 目录下的所有脚本执行一遍。我们可以在这里注册一个 filter ,专门做这个替换。代码如下:\n\n```js\n'use-strict';\n\nhexo.extend.filter.register('before_post_render', function (data) {\n const isToHandle = (data) => {\n var source = data.source;\n var ext = source.substring(source.lastIndexOf('.') + 1, source.length).toLowerCase();\n return ['md'].indexOf(ext) > -1;\n }\n\n if (!isToHandle(data)) {\n return data;\n }\n\n const reg = /(\\s+)\\!\\[\\[(.+)\\]\\](\\s+)/g;\n\n data.content = data.content\n .replace(reg, function (raw, start, content, end) {\n var nameAndTitle = content.split('|');\n if (nameAndTitle.length == 1) {\n return `${start}![](${content})${end}`;\n }\n return `${start}![${nameAndTitle[1]}](${nameAndTitle[0]})${end}`;\n });\n return data;\n\n})\n```\n\n# 测试一下\n\n文章中如下内容:\n\n![[这部分内容会被转换为图片.png]]\n\n\n而你看到上面的内容是一张图片,表示这个转换已经成功了。\n\n# 不足之处\n\n这一段代码仍有以下待改进的地方:\n1. 如果图片短链接的内容写在 Code Block 里,也一样会被转换。实际上我们一般不希望 Code Block 里的内容被转换,需要过滤一下。\n2. 形如 `![[文件|图片描述]]` 的内容会正常转换为 `![图片描述](文件)` 。然而我现在用的这个主题不支持图片描述。以后可能需要更换主题。\n\n# 补充\n\n如果希望网站图片放在 `img` 之类的文件夹下统一管理,不把 `post_asset_folder` 设为 `true` ,也是没问题的,可以通过修改代码,在返回 `${content}` 前添加统一前缀。\n\n而如果希望图片放在 `img` 下,又要按文章分文件夹管理,如下情况:\n\n```tree\nsource\n├───img\n│ ├───2022-03-26-create-blog-cicd-by-github\n│ │ ├───heap-cheat-sheet.jpg\n│ │ └───post-js-version.jpg\n│ └───2022-04-03-use-paste-image-and-vscode-memo\n├───playground\n└───_posts\n ├───2022-03-26-create-blog-cicd-by-github.md\n └───2022-04-03-use-paste-image-and-vscode-memo.md\n```\n\n可以通过在代码中引用 `data.source` 解决。","title":"完善 Hexo 编写环境,改善文章中使用图片的体验","abstract":"我平时使用 [vscode-memo](https://github.com/svsool/vscode-memo) 插件写笔记,其中插入图片使用 `![[]]` 语法,显示简短,也有较好的预览支持,体验极佳。希望这种特性也能在写 hexo 博客的时候使用。\n可能有很多人不熟悉 vscode-memo 这个插件,我先来简单介绍一下。\nvscode-memo 定位是一个 knowledge base ,对标的是 [Obsidian.md](https://obsidian.md/) 等软件。其功能包括且不限于:","length":158,"created_at":"2022-04-03T21:03:03.000Z","updated_at":"2022-04-03T17:47:52.000Z","tags":["Blog","VSCode","Hexo","JavaScript"],"license":false}},{"slug":"create-blog-cicd-by-github","file":"public/content/articles/2022-03-26-create-blog-cicd-by-github.md","mediaDir":"content/articles/2022-03-26-create-blog-cicd-by-github","path":"/articles/create-blog-cicd-by-github","meta":{"content":"\nGitHub Action 自动化构建发布到 GitHub Pages 大家都见得多了,甚至 Hexo 官方自己都有相关的文档。\n但我今天要做的不是发布到 GitHub 这么简单,而是要同时发布到 GitHub 和自己的域名下。\n\n# 这篇文章的目标\n\n我们需要构建一个 CI/CD 过程。这个过程需要做到以下目标:\n1. 将文章 push 到 GitHub 的 master branch 后,自动触发。\n2. 我们博客使用 Hexo 引擎,需要先构建静态文件。\n3. 需要将静态文件部署到 GitHub Page 。\n4. 需要将静态文件部署到自己域名下。\n 这里我们使用 AWS 的 S3 服务与 CloudFront 服务直接部署到 CDN 上。 CloudFront 直接通过 OAI 访问 S3 ,不允许用户直接通过 S3 访问。\n5. 博客在 GitHub Page 与 S3 需要处于不同的路径下。\n 为了延续以往的情况,博客在 GitHub Page 需要部署在 `/blog/` 下。\n 而在 AWS 上我则希望直接部署在根目录下,这就导致需要两份配置文件。\n 当然弄两份配置文件我是不乐意的,于是就需要从模板自动生成配置文件...\n\n其中,一二三点都很好解决,而第四点会是一个比较难又比较坑爹的地方。\n\n# 先做简单的 —— CI/CD 构建并发布到 GitHub Pages\n\n这一步其实没什么难的, Hexo 官网上就有[这篇文章](https://hexo.io/docs/github-pages.html)写的十分详细了,可以作为参考。\n\n```yaml\nname: Pages\n\non:\n push:\n branches:\n - master # default branch\n\njobs:\n pages:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2\n - name: Use Node.js 16.x\n uses: actions/setup-node@v2\n with:\n node-version: '16'\n - name: Cache NPM dependencies\n uses: actions/cache@v2\n with:\n path: node_modules\n key: ${{ runner.OS }}-npm-cache\n restore-keys: |\n ${{ runner.OS }}-npm-cache\n - name: Install Dependencies\n run: npm install\n - name: Build\n run: npm run build\n - name: Deploy\n uses: peaceiris/actions-gh-pages@v3\n with:\n github_token: ${{ secrets.GITHUB_TOKEN }}\n publish_dir: ./public\n publish_branch: gh-pages # deploying branch\n```\n\n这个 yaml 就是 GitHub Action 的 workflow 文件,在这个 workflow 里:\n1. 先用 `npm run build` 把静态文件生成到 `./public` 下\n2. 用 `peaceiris/actions-gh-pages@v3` 这个 action 把 `./public` 的文件放到 `gh-pages` 分支下。\n\n把上面这个 yaml 文件复制到 `.github/workflows/build.yml` 中,这样 master 分支上发生任何提交都会触发构建流程了。按照 Hexo 官网上的文档跑一边就能成功发布到 GitHub Pages 上了。\n\n不过我需要部署到 `/blog/` 下,这叫 Project Page ,因此我走的是 Hexo 文档的 Project Page 这一小节的流程,需要把 `_config.yml` 里做如下设置:\n\n```yaml\nurl: https://ryojerryyu.github.io/blog # 这个其实不是很重要,现在用的主题没有用到这个字段\nroot: /blog/ # 这个比较重要,这个不设定好,整个页面的超链接都会歪掉\n```\n\n当然, “没什么难” 的前提是你首先要对 Hexo 和 GitHub Action 有一个了解...\n\n# 难一点 —— 搭建 AWS 基础设施\n\n我为什么不止用 GitHub Pages 还要配一套 AWS 呢?其实主要还是想以后可能会做一下 Backend ,而且放 AWS 上还能利用 AWS 的服务做一下流量分析之类的。没这么些需求的小伙伴可以不用继续看了...\n\n我们打算使用 AWS 的 S3 与 CloudFront 服务, CloudFront 直接通过 OAI 访问 S3 。\n\n## S3\n\nS3 是 AWS 的对象储存服务,简单来说就是可以当网盘用,往里面放文件。\nS3 有静态网站托管服务,把静态文件放到 S3 里,配置一番就直接可以通过 HTTP 访问了,还能用自己的域名。\n但我们不打算使用 S3 的静态网站托管,因为我打算直接上 CDN ,又不想用户可以直接通过 S3 来访问我们的静态文件。\n\n## CloudFront\n\nCloudFront 是 AWS 的内容分发服务,简单来说就是 CDN 。其实它不只有 CDN 的功能,它还能加速动态调用,还能通过 CloudFront 连接 Web Socket ... 不过我们这次主要是用 CDN 功能。\nCloudFront 访问 S3 的方式还是有好几种的。中文教程最常见的是让你先打开 S3 静态网站托管,然后将 CloudFront 的源设为 S3 的域名。\n这个方法是最早支持的,因此推广的也比较开。但其实我觉得这个方法有些问题:\n\n1. S3 不做另外配置的话是可以直接访问的,比较 low\n2. S3 自己的 HTTP Endpoint 不能上 TLS ,所以 CloudFront 到 S3 这一段是裸奔的\n\n因此我打算使用 AWS 最近推荐的 OAI 方式访问 S3 。这种方式不走 HTTP Endpoint 而是 S3 自己的 S3 Endpoint ,可以通过 AWS 的 IAM 机制统一管理。\nOAI 是 Origin Access Identity ,简单来说就是给 CloudFront 一个 AWS IAM Policy 的 Principal 身份, S3 可以通过如下 Bucket Policy 限制外部只能通过这个 Principal 访问:\n```json\n{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Effect\": \"Allow\",\n \"Principal\": {\n \"AWS\": \"arn:aws:iam::cloudfront:user/\"\n },\n \"Action\": \"s3:GetObject\",\n \"Resource\": \"arn:aws:s3:::/*\"\n }\n ]\n}\n```\n上面这一段看不懂的同学,可以去补习一下 AWS IAM 权限管理机制,关键就是 Principal —— 主体 、 Action —— 动词 、 Resource —— 受体 的一个主谓宾模式。\n\n## 其他 AWS 服务\n\n当然,仅有 S3 和 CloudFront 是不足以实现全部功能的,我们还需要 Route53 来管理路由, ACM 来获取免费证书。\n但这些我都不打算细讲,因为内容真的很多-_-,而且大部分都是 AWS 的细节,搬到别的云上不一定适用...而且手动操作麻烦死了...\n\n## Pulumi\n\n综上嘛,我们需要:\n1. 建一个 Route53 Hosted Zone ,把域名交给 Route53 管理\n2. 用 ACM 给域名申请一个 us-east-1 Region 的免费证书(CloudFront 的证书必须在 us-east-1 )\n3. 建一个 S3 储存桶,把 Public Access Block 配置一下\n4. 建一个 CloudFront Distribution ,通过 OAI 来访问 S3 ,还要指定一下证书\n5. 给 S3 配一个 Bucket Policy ,允许 CloudFront 访问\n6. 把 Route53 里的域名弄个 DNS 记录指向 CloudFront\n\n手动操作麻烦死了,于是我打算用 IaC (Infrastructure-as-Code) 来解决。我把这些基础设施定义用 Pulumi 写成的代码放在[这里](https://github.com/RyoJerryYu/aws-blog-infra/tree/c97f0fe41b5c0306d5343ddfc22f4a3775d79b88/website)了,大家可以参考一下(做了模块化,跟我其他基础设施放一起了)。\n\n当然,用 Pulumi 没什么特别原因,纯粹是因为我最近在写 Pulumi... 你完全可以用其他 IaC 工具(Ansible、Terraform、CloudFormation)来做。而且 Pulumi 太新了,用起来挺多 Bug 的...(也许是我不会用)\n\n## 测试一下\n\nS3 桶啥的都建好之后,本地把文件 build 一下,用 `aws s3 cp ./public/ s3:/// --recursive` 之类的命令上传到 S3 ,给 CloudFront 创建一个 Invalidation 刷新一下 CloudFront 缓存,访问域名看看,有返回个 HTML 我们的基础设施就算是跑通了。此时可能会出现以下情况,都属正常:\n1. 访问返回 307 :\n 是 S3 储存桶 Region 不在 us-east-1 导致的。\n CloudFront 是通过 s3 的 global endpoint 访问 s3 的,但不在 us-east-1 的 s3 刚新建时还不能通过 global endpoint 访问。\n 参考 so 的[这个问题](https://stackoverflow.com/questions/38706424/aws-cloudfront-returns-http-307-when-origin-is-s3-bucket):\n\n > All buckets have at least two REST endpoint hostnames. In eu-west-1, they are example-bucket.s3-eu-west-1.amazonaws.com and example-bucket.s3.amazonaws.com. The first one will be immedately valid when the bucket is created. The second one -- sometimes referred to as the \"global endpoint\" -- which is the one CloudFront uses -- will not, unless the bucket is in us-east-1. Over a period of seconds to minutes, variable by location and other factors, it becomes globally accesible as well. Before that, the 307 redirect is returned. Hence, the bucket was not ready.\n \n 这时候只要等个十几分钟就好了。\n2. 本地 build 的时候没配置好的话,js 之类的静态文件可能返回不了,但问题不大,我们接下来再处理。\n\n\n# 搭建 S3 的 workflow\n\n基础设施搭好了,我们就要像 deploy 到 GitHub Pages 一样,造一个自动管线发布到 S3 了。\n整理一下,我们的 workflow 里要包括:\n\n1. 从模板生成配置文件\n 别忘了,我需要的是静态文件部署在 GitHub Pages 和自己域名下的不同路径上。 Hexo 生成静态文件前配置文件必须要改的。\n2. 把原先 s3 上的文件删除,并上传新的文件到 s3\n3. 给 CloudFront 创建一个 Invalidation 刷新缓存\n\n## 生成配置文件\n\n这一步其实方案很多,甚至 bash 直接全文替换都可以...\n不过怕以后要改的东西变多,这里还是选择一些模板生成工具。有如下选择:\n\n1. 屠龙刀 Ansible\n2. Python Jinja2\n3. Go Template\n\n这里用 Ansible 确实是大材小用了,而且 Ansible 不能在 Windows 下用还是有点不方便,只能弃选。而 Python 和 Go 里我选了 Go Template ,原因是... 不想写 Python...\n这里其实确实是装逼了,这种小型脚本应该 Python 比 Go 合适的多。不过还好 Go run 可以不先 go mod 就能运行,不算是个太差的选择。不过以后还是大概率要改回 Python 。\n\n写 golang 脚本没有难度,大致如下:\n\ngolang template 的 name 要是 file name\n```golang\nname := path.Base(*tmpl)\nt := template.Must(template.New(name).ParseFiles(*tmpl))\nerr = t.Execute(os.Stdout, config)\nif err != nil {\n log.Fatal(err)\n}\n```\ngithub workflow 如下\n```yaml \n- name: Use Go 1.16\n uses: actions/setup-go@v1\n with:\n go-version: '1.16.1'\n\n- name: generate config\n run: go run ./genconfig/main.go --env=gh-pages > _config.yml\n\n```\nwindows 玩家可能要注意一下,windows 下编码有问题, `go run ./genconfig/main.go --env=gh-pages > _config.yml` 这段命令直接在 PowerShell 下跑生成出来的文件不能被 Hexo 识别。不过没什么关系,反正这段到时候是在 GitHub Action Runner 上跑的,只不过是不能本地生成用来测试而已。\n\n[参考代码](https://raw.githubusercontent.com/RyoJerryYu/blog/2f407cb6ee723d0e17c97af1289bd2231bb265ab/genconfig/main.go)\n\n## 上传 s3 与刷新 CloudFront\n\n后两步搜一下发现其实有很多现成的 GitHub Action 可以用。\n不过我没有采用,原因是——真的没必要啊...就几个命令的事,又不是不会敲...\n\nworkflows yaml 如下:\n```yaml\n- name: Configure AWS\n uses: aws-actions/configure-aws-credentials@v1\n with:\n aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n aws-region: ap-northeast-1\n- name: Deploy\n env:\n S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}\n DISTRIBUTION_ID: ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }}\n run: |\n aws s3 rm s3://$S3_BUCKET/* --recursive\n aws s3 cp ./public s3://$S3_BUCKET/ --recursive\n aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths '/*' --region=us-east-1\n```\n\n[完整 yaml 参考代码](https://raw.githubusercontent.com/RyoJerryYu/blog/f0affb812f2de437943d9cf2a4f8a5fe690d1efd/.github/workflows/clouds.yml)\n\n由于改为了生成配置文件, deploy 到 Github Pages 的 yaml 也要做相应改动,这里就不多说。\n\n# CloudFront 的一点小问题(不太小)\n\n这样我们的整个流程是不是跑完了?我们的博客已经部署到自己的域名下了?\n浏览器打开自己的域名看看,完美显示!\n\n等等,别高兴的太早,点进去一篇文章... 403 了...\n\n403 的原因:\n1. hexo 生成出来的 page 连接是 `/` 结尾的,如 `/2022/03/26/create-blog-cicd-by-github/` ,然后通过 HTTP 服务器的自动转义指向 `/2022/03/26/create-blog-cicd-by-github/index.html` 文件。\n2. CloudFront 可以定义默认根对象,没有为每个子路径都自动转义的功能。\n3. S3 的 HTTP endpoint 可以配置索引文档,为每个子路径自动转义,但 CloudFront 通过 OAI 访问 S3 时通过 REST endpoint 访问,不会触发自动转义。\n\n一大波参考阅读:\n\n[Specifying a default root object](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DefaultRootObject.html)\n> Here's an example of how a default root object works. Suppose the following request points to the object image.jpg:\n> ```\n> https://d111111abcdef8.cloudfront.net/image.jpg\n> ```\n> In contrast, the following request points to the root URL of the same distribution instead of to a specific object, as in the first example:\n> ```\n> https://d111111abcdef8.cloudfront.net/\n> ```\n> When you define a default root object, an end-user request that calls the root of your distribution returns the default root object. For example, if you designate the file index.html as your default root object, a request for:\n> ```\n> https://d111111abcdef8.cloudfront.net/\n> ```\n> Returns:\n> ```\n> https://d111111abcdef8.cloudfront.net/index.html\n> ```\n> However, if you define a default root object, an end-user request for a subdirectory of your distribution does not return the default root object. For example, suppose index.html is your default root object and that CloudFront receives an end-user request for the install directory under your CloudFront distribution:\n> ```\n> https://d111111abcdef8.cloudfront.net/install/\n> ```\n> CloudFront does not return the default root object even if a copy of index.html appears in the install directory.\n> \n> If you configure your distribution to allow all of the HTTP methods that CloudFront supports, the default root object applies to all methods. For example, if your default root object is index.php and you write your application to submit a POST request to the root of your domain (http://example.com), CloudFront sends the request to http://example.com/index.php.\n> \n> The behavior of CloudFront default root objects is different from the behavior of Amazon S3 index documents. When you configure an Amazon S3 bucket as a website and specify the index document, Amazon S3 returns the index document even if a user requests a subdirectory in the bucket. (A copy of the index document must appear in every subdirectory.) For more information about configuring Amazon S3 buckets as websites and about index documents, see the Hosting Websites on Amazon S3 chapter in the Amazon Simple Storage Service User Guide.\n\n[Configuring an index document](https://docs.aws.amazon.com/AmazonS3/latest/userguide/IndexDocumentSupport.html)\n> In Amazon S3, a bucket is a flat container of objects. It does not provide any hierarchical organization as the file system on your computer does. However, you can create a logical hierarchy by using object key names that imply a folder structure.\n> \n> For example, consider a bucket with three objects that have the following key names. Although these are stored with no physical hierarchical organization, you can infer the following logical folder structure from the key names:\n> - sample1.jpg — Object is at the root of the bucket.\n> - photos/2006/Jan/sample2.jpg — Object is in the photos/2006/Jan subfolder.\n> - photos/2006/Feb/sample3.jpg — Object is in the photos/2006/Feb subfolder.\n> \n> In the Amazon S3 console, you can also create a folder in a bucket. For example, you can create a folder named photos. You can upload objects to the bucket or to the photos folder within the bucket. If you add the object sample.jpg to the bucket, the key name is sample.jpg. If you upload the object to the photos folder, the object key name is photos/sample.jpg.\n> \n> If you create a folder structure in your bucket, you must have an index document at each level. In each folder, the index document must have the same name, for example, index.html. When a user specifies a URL that resembles a folder lookup, the presence or absence of a trailing slash determines the behavior of the website. For example, the following URL, with a trailing slash, returns the photos/index.html index document.\n> ```\n> http://bucket-name.s3-website.Region.amazonaws.com/photos/\n> ```\n> \n> However, if you exclude the trailing slash from the preceding URL, Amazon S3 first looks for an object photos in the bucket. If the photos object is not found, it searches for an index document, photos/index.html. If that document is found, Amazon S3 returns a 302 Found message and points to the photos/ key. For subsequent requests to photos/, Amazon S3 returns photos/index.html. If the index document is not found, Amazon S3 returns an error.\n\n[Implementing Default Directory Indexes in Amazon S3-backed Amazon CloudFront Origins Using Lambda@Edge](https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/)\n> If you implement CloudFront in front of S3, you can achieve this by using an OAI. However, in order to do this, you cannot use the HTTP endpoint that is exposed by S3’s static website hosting feature. Instead, CloudFront must use the S3 REST endpoint to fetch content from your origin so that the request can be authenticated using the OAI. This presents some challenges in that the REST endpoint does not support redirection to a default index page.\n\n> CloudFront does allow you to specify a default root object (index.html), but it only works on the root of the website (such as http://www.example.com > http://www.example.com/index.html). It does not work on any subdirectory (such as http://www.example.com/about/). If you were to attempt to request this URL through CloudFront, CloudFront would do a S3 GetObject API call against a key that does not exist.\n\n\n\n那么,我们要怎么解决这个问题呢?我觉得,这个问题有三种解决方法:\n\n1. 不使用 OAI ,让 CloudFront 直接指向 S3 的域名,让 CloudFront 使用 S3 HTTP Endpoint 的特性\n2. 调整 Hexo 配置,更改生成文件路径或连接路径\n3. 使用 AWS 推荐的 Lambda@Edge 功能,在 CloudFront 上修改路径\n\n其中第二种方案是最下策,我们不能在还有其他方案的情况下,因为基础设施的一个性质就去修改我们的产品。况且我们的产品在大多数场景下都是适用的。\n第一种方案是中策,也许实行起来也是最简单的。但我不想用,原因上面也说过了。\n第三种方案是实施起来难度最大的,我们要引入 Lambda 这一新概念。但反正折腾嘛,试试就试试,反正失败了再变回第一种方案就是。\n\n## 创建 Lambda\n\n[Implementing Default Directory Indexes in Amazon S3-backed Amazon CloudFront Origins Using Lambda@Edge](https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/)\n\n参考上面的文档,我们直接在 Console 创建一个 Lambda 函数,内容如下:\n\n```javascript\n'use strict';\nexports.handler = (event, context, callback) => {\n \n // Extract the request from the CloudFront event that is sent to Lambda@Edge \n var request = event.Records[0].cf.request;\n\n // Extract the URI from the request\n var olduri = request.uri;\n\n // Match any '/' that occurs at the end of a URI. Replace it with a default index\n var newuri = olduri.replace(/\\/$/, '\\/index.html');\n \n // Log the URI as received by CloudFront and the new URI to be used to fetch from origin\n console.log(\"Old URI: \" + olduri);\n console.log(\"New URI: \" + newuri);\n \n // Replace the received URI with the URI that includes the index page\n request.uri = newuri;\n \n // Return to CloudFront\n return callback(null, request);\n\n};\n```\n这一段代码主要作用是把接收到每个以 `/` 结尾的请求,都转换为以 `/index.html` 结尾的请求。\n\nDeploy 之后,为 Lambda 添加 Trigger ,选择 CloudFront 作为 Trigger , Event 选择 On Request 。按照界面的提示为 Lambda 创建专用的 Role 。\n提交后,我们就可以通过 Url 访问,发现 `/` 结尾的 URL 也会正常显示了。\n\n# 之后的事\n\n这个过程仍有以下问题:\n- 对 Lambda 的认识仍有不足,今后需继续学习运用\n- Lambda@Edge 还没有结合到 IaC 中\n- 配置文件生成过程仍有改进空间\n\n留下这些问题,今后再修改。\n","title":"用 GitHub Action 自动化构建 Hexo 并发布到 S3","abstract":"GitHub Action 自动化构建发布到 GitHub Pages 大家都见得多了,甚至 Hexo 官方自己都有相关的文档。\n但我今天要做的不是发布到 GitHub 这么简单,而是要同时发布到 GitHub 和自己的域名下。\n我们需要构建一个 CI/CD 过程。这个过程需要做到以下目标:","length":342,"created_at":"2022-03-26T23:55:08.000Z","updated_at":"2022-03-27T13:31:04.000Z","tags":["Blog","GitHub","AWS","CI/CD","IaC","DevOps"],"license":false}},{"slug":"init-a-new-hexo-project","file":"public/content/articles/2021-12-12-init-a-new-hexo-project.md","mediaDir":"content/articles/2021-12-12-init-a-new-hexo-project","path":"/articles/init-a-new-hexo-project","meta":{"content":"\n## 使用 hexo 搭建博客\n\n最近使用 hexo 搭建了一个博客,并打算挂载在 github page 上。\n对之前的那个博客进行替代,并将之前的文章逐渐搬移过来。\n\n使用的[这个主题](https://github.com/Yue-plus/hexo-theme-arknights)功能还是比较完善的。\n\n我们可以尝试一下代码块高亮:\n\n```python\ndef func_echo(s: str):\n print(s)\n\n\nclass HelloPrinter:\n printer: Callable[[str]]\n\n def __init__(self, printer: Callable[[str]]):\n self.printer = printer\n \n def call(self, s: str):\n self.printer(s)\n\n\np = HelloPrinter(func_echo)\np.call(\"hello world!\")\n```\n\n试试下标语法吧:\n\n这是一句话。[^sub]\n\n没想到还支持下标语法,还是比较惊艳的。\n\n来几句 mermaid 吧\n\n```mermaid\ngraph LR\n\nohmy-->coll\n\n```\n\n原本是不能渲染的, 这个主题渲染代码块时把 mermaid 代码当作普通代码,往里面里插换行符号了。\n使用了 hexo-filter-mermaid-diagrams 插件,添加 mermaid 过滤器,解决问题。\n\n\n来几句 LaTeX:\n\n$$\n\\begin{aligned}\nf(x) &= \\sum_{i=2}^{\\infty}{\\Join} \\\\\n&= \\sum_{i=2}^{\\infty}{\\frac{1}{i}}\n\\end{aligned}\n$$\n\n原本是不能渲染的,因为与 hexo 的渲染器有冲突,需要转义。\n我因为需要从以前的博客把文章转移过来觉得比较麻烦...\n于是魔改了一下主题,用上 mathjax 插件,能渲染了,感觉挺不错的。\n再改善一下推个 PR 吧。\n\n\n\n\n[^sub]: 这是脚注","title":"init-a-new-hexo-project","abstract":"最近使用 hexo 搭建了一个博客,并打算挂载在 github page 上。\n对之前的那个博客进行替代,并将之前的文章逐渐搬移过来。\n使用的[这个主题](https://github.com/Yue-plus/hexo-theme-arknights)功能还是比较完善的。","length":66,"created_at":"2021-12-12T20:09:13.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["Blog"],"license":false}},{"slug":"Handy-heap-cheat-sheet","file":"public/content/articles/2021-03-21-Handy-heap-cheat-sheet.md","mediaDir":"content/articles/2021-03-21-Handy-heap-cheat-sheet","path":"/articles/Handy-heap-cheat-sheet","meta":{"content":"\n# 如何手撕一个堆\n\n# 写在前面\n\n在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\n\n# 首先要理解,然后才能实现\n\n就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\n\n## 先抓住重点:堆是一种树结构\n\n首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\n\n再进一步,在堆这种树结构中,最重要的约束就是:**对于树中的每个节点,总有父节点大于两个子节点**(以大顶堆为例,下同)。\n\n如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\n\n而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\n\n1. 堆并不关注左右子树之间的大小情况,那么**要维护一个堆,基本只需要做交换父节点与子节点的操作**,而不需要像二叉查找树那样做各种旋转操作。\n2. 因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,**可以使用数组进行实现**。\n3. 因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:**堆的增删操作最坏时间复杂度为O(logn)**。\n\n## 再抓基本操作:上浮与下沉\n\n上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点**过大**,就跟他的父节点**向上交换**;若一个节点**过小**,就跟他的子节点**向下交换**。\n\n假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\n\n![p与g交换](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png)\n\n由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\n\n1. p > g > p2\n2. g > 原p > c1与c2\n\n则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\n\n同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\n\n为了简化,我们把前面那种递归地向上交换称为**上浮操作**,把后面这种递归地向下交换称为**下沉操作**。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\n\n# 各种接口的逻辑\n\n## 插入元素——入堆\n\n把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\n\n虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\n\n## 删除堆顶元素——出堆\n\n我们从堆中删除元素时,一般只会删除堆顶元素。\n\n删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\n\n这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\n\n## 堆的初始化——建堆\n\n建立一个堆,我们有两种思路:\n\n1. 将元素一个一个插入,即对每个元素都做一次入堆操作。\n2. 当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\n\n下面我们来分析这两种建堆策略。\n\n### 元素逐个入堆\n\n上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\n\n插入第i个元素时,堆的大小为$i$(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\n\n$$T(i) = logi$$\n\n那么把所有元素上浮,则总时间复杂度为:\n\n$$\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logi\\\\\n&= 1\\times0 + 2\\times1 + ... + 2^{logn}\\times{logn} \\\\\n&=O(nlogn)\n\\end{aligned}$$\n\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\n\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\n\n### 堆合并\n\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\n\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\n\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\n\n$$T(i) = logn-logi$$\n\n那么把所有元素下沉,则总时间复杂度为:\n\n$$\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logn-logi \\\\\n&= \\frac{n}{2^{logn}}\\times{logn}+ ... + \\frac{n}{4}\\times2+\\frac{n}{2}\\times1 \\\\\n&= O(n)\n\\end{aligned}$$\n\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\n\n### 两种策略的比较与理解\n\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\n\n1. 从元素移动路径的角度\n\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\n\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\n\n2. 从元素移动数量与移动距离的角度\n\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\n\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\n\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\n\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\n\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\n\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\n\n# 代码实现\n\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\n\n```python\nT = TypeVar(\"T\")\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n```\n\n## 实现树结构\n\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\n\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\n\n```python\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n```\n\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\n\n## 实现基本操作——上浮与下沉\n\n### 上浮\n\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\n\n```python\ndef floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n```\n\n### 下沉\n\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\n\n```python\ndef size(self):\n '''返回堆大小\n '''\n return len(self.A)\n\ndef sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n```\n\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\n\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\n\n## 实现各种借口——读、增、删、初始化\n\n### 读取堆顶\n\n堆一般只允许读取堆顶,即全堆最大元素。\n\n```python\ndef top(self):\n '''返回堆顶\n '''\n return self.A[0]\n```\n\n### 入堆\n\n入堆时,把元素加到堆尾,再做上浮操作。\n\n```python\ndef insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n```\n\n### 出堆\n\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\n\n```python\ndef pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n注意入堆与出堆操作都要保证堆的大小会相应变化。\n\n### 堆初始化\n\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\n\n```python\ndef __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n```\n\n## 整体代码\n\n### 堆的整体实现\n\n综上,堆的整体代码实现如下:\n\n```python\nfrom typing import Any, Callable, Generic, List, TypeVar\n\nT = TypeVar(\"T\")\n\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n def __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n \n def size(self):\n '''返回堆大小\n '''\n return len(self.A)\n \n def top(self):\n '''返回堆顶\n '''\n return self.A[0]\n \n def sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n\n def floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n \n def insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n\n def pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n### 单元测试\n\n入堆、出堆等操作的简单单元测试如下:\n\n```python\nimport pytest\nimport heap\n\n@pytest.fixture\ndef initHeap():\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \n lambda a,b:a>b)\n\nclass Test_TestHeap:\n def test_init_notNull(self, initHeap:heap.Heap):\n assert initHeap.size() == 10\n assert initHeap.top() == 9\n \n def test_insert_notTop(self, initHeap:heap.Heap):\n initHeap.insert(6)\n assert initHeap.size() == 11\n assert initHeap.top() == 9\n \n def test_insert_top(self, initHeap:heap.Heap):\n initHeap.insert(10)\n assert initHeap.size() == 11\n assert initHeap.top() == 10\n \n def test_pop(self, initHeap:heap.Heap):\n p = initHeap.pop()\n assert p == 9\n assert initHeap.size() == 9\n assert initHeap.top() == 8\n```\n\n# 关于堆排序\n\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\n\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\n\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\n\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。","title":"如何手撕一个堆","abstract":"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。","length":477,"created_at":"2021-08-28T23:09:14.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["数据结构","算法","算法竞赛"],"license":false}},{"slug":"The-beauty-of-design-parten","file":"public/content/articles/2021-08-21-The-beauty-of-design-parten.md","mediaDir":"content/articles/2021-08-21-The-beauty-of-design-parten","path":"/articles/The-beauty-of-design-parten","meta":{"content":"\n# 导读\n\n## 02:如何评价代码好坏?\n\n从7个方面评价代码的好坏:\n\n1. 易维护性:根本\n2. 可读性:最重要\n3. 易扩展性:对修改关闭,对扩展开放\n4. 灵活性\n5. 简洁性:KISS\n6. 可复用性:DRY\n7. 可测试性:TDD,单元测试,控制反转与依赖注入\n\n## 03:编程方法论\n\n设计模式之美这一课程不单止讲设计模式,而会讲包括设计模式在内的指导我们进行代码设计的方法论。包括以下5个方面:\n\n1. 面向对象:封装,抽象,继承,多态\n2. 设计原则:SOLID(单一职责,开闭原则,里氏替换,接口隔离,依赖倒置),DRY,KISS,YAGNI,LOD\n3. 设计模式\n4. 编程规范:可读性,命名规范\n5. 重构技巧:(目的,对象,时机,方法),保证手段(单元测试与可测性),两种规模\n\n整个课程会以编程方法论为纵轴,以代码好坏的评价为横轴,来讲提高代码质量的方法以及采用这种方法的原因。\n\n\n# 面向对象\n\n使用封装,抽象,继承,多态,作为代码设计和实现的基石。\n\n1. 面向对象分析(做什么),设计(怎么做),编程\n\n## 05:封装,抽象,继承,多态\n\n| | 是什么 | 怎么做 | 为什么 |\n| ---- | ---------------------- | ---------------------- | -------------------------------------------------------------- |\n| 封装 | 信息隐藏、数据访问保护 | 访问控制关键字 | 减少不可控因素、统一修改方式、保证可读性与可维护性、提高易用性 |\n| 抽象 | 隐藏实现方法 | 函数、接口类、抽象类 | 提高可扩展性与维护性、过滤非必要信息 |\n| 继承 | is-a关系 | 继承机制 | 代码复用、反映真实世界关系 |\n| 多态 | 子类替代父类 | 继承、接口类、鸭子类型 | 提高扩展性与复用性 |\n\n- 继承不应过度使用,会导致层次过深,导致低可读性与低可维护性\n- 在我看来,多态的本质与其说是子类替代父类,更应说是用同一个过程方法能适应多种不同类型的对象。\n- 有些观点认为,多态除了表中这三种实现方式以外,还有泛型的实现方式,被称为连接时多态。\n\n## 06,07:面向过程与面向对象\n\n1. 面向过程是:数据与方法分离\n2. 面向对象优势:适应大规模开发,代码组织更清晰;易复用、易扩展、易维护;人性化;\n3. 看似面向对象实际面向过程:滥用getter、setter破坏封装;滥用全局变量与全局方法,Constants类与Utils类;数据与方法分离,贫血模型;\n4. 为什么容易面向过程:略\n5. 面向过程的用处:略\n\n## 08:接口与抽象类\n\n1. 接口类与抽象类语法特性:略\n2. 抽象类表示is-a,为了解决代码复用。接口表示能做什么,为了解耦,隔离接口与实现。\n3. 应用场景区别:\n - 抽象类:代表is-a关系,解决代码复用问题\n - 接口类:解决抽象、解耦问题","title":"设计模式之美读书笔记","abstract":"从7个方面评价代码的好坏:\n1. 易维护性:根本\n2. 可读性:最重要","length":62,"created_at":"2021-08-21T08:53:27.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["设计模式","笔记"],"license":false}},{"slug":"Sort-algorithm","file":"public/content/articles/2021-01-11-Sort-algorithm.md","mediaDir":"content/articles/2021-01-11-Sort-algorithm","path":"/articles/Sort-algorithm","meta":{"content":"\n# 序言\n\n我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。\n\n你会发现堆排序被当成是选择排序的一种优化——因为我认为堆排序主要在于使用了堆这种数据结构,而总体思想与选择排序相比没有太大变化。\n\n你还会找到其他与别的文章不一样的地方。因为这篇文章是我按照自己的理解来写的,我脑子里是这样想的,那文章里就是这样写的。我会按照我的理解,从纵向与横向两个维度,来理清楚各个排序算法的特性与异同。\n\n# 整篇文章的要点\n\n整篇文章以纵向——算法分类、以及横向——算法评价两个维度来进行组织。\n\n排序算法可以按照以下方式来进行分类:\n\n- 基于比较的排序算法\n - 基于分治思想\n - 快速排序\n - 归并排序\n - 基于有序区域扩展\n - 插入排序\n - 选择排序\n- 不基于比较的排序算法\n - 计数排序\n - 桶排序\n - 基数排序\n\n文章中还会讲一讲为什么会这么分,每种分类有什么共性,分类之间有什么差异。此外,在最后还会稍微提一提外部排序与适用于并行运算的排序等。\n\n而对于纵向分类中的每一个端点,我们又会从以下五个方面,来对各个算法进行一个总体评价:\n\n- 时间复杂度(最坏,最好,平均※)\n- 空间复杂度\n- 是否原地排序\n- 是否稳定排序\n- 能否用于链表排序\n\n而由于复杂度主要只关注数量级,因此在这篇文章里会在不影响计算结果的前提下对复杂度计算进行适当的近似与简化。\n\n# 快排\n\n## 思想\n\n分治法:先把序列分为小的部分和大的部分,再将两部分分别排序。即:复杂分割,简单合并,主要操作在于分割。\n\n## 要点\n\n### 时间复杂度\n\n推导式:T(n) = T(找) + T(左) + T(右) = O(n) + 两个子问题时间复杂度。\n\n- 最好时间复杂度为每次都正好找到最中间的一个数时时间复杂度为O(nlogn)。证明略。\n- 最坏时间复杂度为每次都正好找到最旁边的数时时间复杂度为O(n^2)。证明略。\n- 平均时间复杂度为O(nlogn),推导式如下:\n\n 快速排序每一步中,将元素分为左右两边需要遍历整个列表,耗时T(n)。假设最后定位的元素为最终第i个元素,则两个子问题复杂度分别为T(i)和T(n-i-1)。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{\\sum_{i=0}^{n-1}{T(i)+T(n-i-1)}}{n} \\\\\n &=n + \\frac{2}{n}\\times\\sum_{i=0}^{n-1}{T(i)} \\\\\n \\end{aligned}\n $$\n\n 令 $$\\sum_{i=0}^{n}T(i) = Sum(n)$$ ,即有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{2}{n} \\times Sum(n-1) \\\\\n Sum(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1)\n \\end{aligned}\n $$\n\n 错位相减:\n\n $$\n \\begin{aligned}\n Sum(n) - Sum(n-1) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1) - \\frac{n}{2}T(n) + \\frac{n}{2}(n) \n \\\\\n T(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n}{2}T(n) - \\frac{2n+1}{2} \n \\\\\n \\frac{T(n+1)}{n+2} &= \\frac{T(n)}{n+1}+\\frac{2n+1}{(n+1)(n+2)} \n \\\\\n &= \\frac{T(n)}{n+1}+\\frac{1}{n}+\\frac{1}{n+1} \n \\\\\n &=...\n \\\\\n &= \\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+\\frac{1}{3}+...+\\frac{1}{n})+\\frac{1}{n+1}\n \\\\\n \\\\\n \\frac{T(n)}{n+1}&=\\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+...+\\frac{1}{n-1})+\\frac{1}{n}\n \\\\\n &= O(1)+O(1)+O(logn)+O(\\frac{1}{n})\n \\\\\n &= O(logn)\n \\end{aligned}\n $$\n\n 其中由于 $$\\frac{1}{x}=\\frac{d(logx)}{dx}$$ ,因此 $$\\frac{1}{2}+...+\\frac{1}{n-1}=O(logn)$$ 。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n)&=(n+1)\\times O(logn)\\\\\n &=O(n)\\times O(logn)\\\\\n &=O(nlogn)\n \\end{aligned}\n $$\n\n### 额外空间复杂度\n\n考虑栈深度,额外空间复杂度为O(logn)。由于快速排序主要步骤在于分,因此必须自上而下的进行递归,无法避免栈深度。\n\n### 原地排序\n\n虽然快速排序有额外空间复杂度,但并不妨碍它是一个原地排序。\n\n### 不稳定\n\n堆排序在分操作时将元素左右交换,会破坏稳定性。\n\n### 链表形式特点\n\n- 时间复杂度不变\n- 空间复杂度不变\n- 变为稳定排序※\n\n## 手写时的易错点\n\n- 分成左右子序列时最好完全分开(一边用`<=`一边用`>`),不然容易造成死循环。\n- 分左右子序列时仔细考虑最初下标位置与最终下标位置,以及对应位置的值的大小。\n- 不要忘记递归的结束条件。\n\n# 归并排序\n\n## 思想\n\n分治法:先把两个子序列各自排好序,然后再合并两个子序列。即:简单分割,复杂合并。主要步骤在于合并。\n\n## 要点\n\n- 时间复杂度推导式:T(n) = 2T(n/2) + T(合)\n- 平均、最好、最坏时间复杂度都是O(nlogn),推导过程略。\n- 额外空间复杂度为O(n),合并时必须准备额外空间。但由于主要步骤在于合并,可以自下而上地进行迭代合并,可以不使用栈。\n- 非原地排序\n- 稳定排序\n\n### 链表形式\n\n- 时间复杂度不变\n- 额外空间复杂度变为O(1)※\n- 稳定排序\n- 只能使用迭代形式,不能使用递归形式\n\n## 易错点\n\n- 合并时一边结束时另一边还未结束,需要把那一边也放入合并后序列中\n- 保持稳定排序:合并时左序列等于右序列时也采用左序列\n- 不要忘记递归结束条件\n- 不要忘记循环递进条件\n\n## 进阶\n\n- 原地归并排序:时间复杂度为O(log^2n),牺牲合并的时间复杂度进行原地排序。\n- 多路归并排序:使用竞标树,多路归并,用于磁盘IO。\n\n# 插入排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将无序序列当前元素插入有序序列,复杂度取决于有序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:基本有序情况下,O(n)\n- 额外空间复杂度:O(1)\n- 原地排序\n- 稳定排序\n- 每次插入时都要移动序列,写次数较多\n- 若查找插入位置时使用二分法查找,则可加快时间。(但不足以对时间复杂度造成影响,且最好时间复杂度也会上升为O(nlogn))\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 稳定排序\n- 插入时不再需要移动序列,但也不能使用二分法查找\n\n## 进阶——希尔排序\n\n- 要点:\n\n 插入排序的优点在于当序列基本有序时,时间复杂度可逼近为O(n)。\n\n 但插入时移动有序序列中元素所耗时间较多,而每次只移动一步。但实际上当序列分布均匀时,有序序列中排靠后的元素在整个序列中也会排靠后。\n\n 可以把序列分为几个大步长序列,在最初的几次插入放开移动步长,让大的元素直接移动到较后位置。再往后慢慢缩小步长,此时序列基本有序,可以利用基本有序时插入排序的优势。\n\n- 时间复杂度\n 1. 当步长为2^i时,不能使时间复杂度缩短为O(nlogn)。因为一个子序列所有元素有可能比另一个子序列最大元素都要大,这时插入排序仍需进行约n^2次操作\n 2. 当步长为2^i时效率较低,因为当步长为4已经有序时,步长为2再比较是无用比较。但由于1.的问题,不能节省比较时间。\n 3. 当步长之间最小公约数较少,甚至互质时,无用比较次数会降低。\n 4. 最坏时间复杂度下限为 $$O(nlog^2n)$$ (当步长采用 $$2^i3^j$$ 时),但一般希尔排序平均时间复杂度都为 $$O(n^{\\frac{3}{2}})$$\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序:希尔排序步长较大时会发生前后跳转。\n- 不能写为链表形式\n\n# 选择排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将选无序序列中最小元素加到有序序列末尾,复杂度取决于无序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:O(n2),如能保证无序部分的最小元素所在位置一定(堆排序),能降低时间复杂度\n- 额外空间复杂度:O(1)\n- 原地排序\n- 不稳定排序(采用元素交换策略时)\n- 每次找到最小元素后,只需交换一次位置即可,写次数较少。\n- 若找到最小元素后,不直接交换而是进行数组移动,则可进行稳定排序,但写次数变多,与插入排序相比没有优势,也不能使用二分查找进行简化。\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 变为稳定排序※(因为链表不需要数组移动,稳定排序方式的缺点得以消除)\n\n## 进阶——堆排序\n\n- 要点:\n\n 选择排序中耗时最多的是取出无序序列中最小值的时间,需要遍历整个无序序列。\n\n 但实际上我们只关心无序序列中的最小值,而不关心其他值的位置。通过将无序序列建为堆,减少选择时间,降低总的时间复杂度。\n\n- 时间复杂度O(nlogn)\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序\n- 操作时间复杂度:每次向下比较关注一个节点与其左右子堆顶元素,每次向上比较只关注节点与其父元素(大顶堆,堆大小为n)\n - 下沉:向下比较,若顶元素不是最大,将顶元素与较大的子堆堆顶元素交换。递归处理该子堆顶元素,直到向下比较顶元素最大。\n\n 最好时间复杂度O(1),最坏时间复杂度为O(h)=O(logn),平均O(logn)\n\n - 上浮:向上比较,若元素比其上层要大,交换该元素与其上层元素。递归处理其上层元素,直到向上比较不比上层要大。\n\n 最好时间复杂度O(1),最坏时间复杂度O(h)=O(logn),平均O(logn)\n\n - 入堆:堆扩容一位,将新元素插到尾部,将该元素上浮,最坏、平均时间复杂度O(logn)\n - 出堆:取出堆顶元素,将尾部放到堆顶,将该元素下沉,最坏、平均时间复杂度O(logn)\n - 缺点:通常堆尾元素较小,出堆时将堆尾元素放到堆顶再下沉基本要沉到堆底,无用比较较多\n- 建堆时间复杂度O(n)\n - 策略1:从头开始建堆,逐个元素插入,时间复杂度取决于最后一层,时间复杂度为O(nlogn)\n - 每次将堆扩容一位,将末尾元素上浮。\n - 时间复杂度推导:\n\n 每次插入时间:\n\n $$\n \\begin{aligned} T(i) &= h\\\\\n &= logi\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n}logi\\\\\n &= 1\\times1 + 2\\times2+ 3\\times4 + ...+h\\times \\frac{n}{2} \\\\\n \\\\\n 2T(n) &= 1\\times2 + 2\\times4+ 3\\times8 + ...+h\\times n \\\\\n \\\\\n 2T(n)-T(n) &= h\\times n - (1+2+4+...+2^{h-1}) \\\\\n &= h\\times n - O(2^h) \\\\\n \\\\\n T(n) &= nlogn - O(2^h) \\\\\n &= O(nlogn)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(nlogn)$$\n\n - 策略2:从后开始建堆,小堆合并(逐个元素下沉),时间复杂度为O(n)\n - 每次堆合并时,有三部分:左子堆,右子堆,顶元素。下沉顶元素。\n - 时间复杂度推导:\n\n 第i个元素合并时,时间为:\n\n $$\n \\begin{aligned}\n T(i) &= h_{子堆}\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n} (h_{子堆}) \\\\\n &= h + (h-1)\\times2 + ... + 2 \\times \\frac{n}{4} + 1 \\times\\frac{n}{2}\\\\\n \\\\\n 2T(n) &= h \\times 2 + (h-1) \\times 4 + ... + 2 \\times \\frac{n}{2} + n \\\\\n \\\\\n 2T(n) - T(n) &= n + \\frac{n}{2} + ... + 2 - h \\\\\n \\\\\n T(n) &= O(n) - h \\\\ \n &= O(n)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(n)$$ \n\n - 虽说每步是做一个小堆合并,但实际上从堆尾到堆头遍历,相当于仅关注元素没有稳定,相当于可以直接使用下沉操作。\n\n# 基于比较的排序算法时间复杂度下限:逆序对思想\n\n基于比较的排序算法可以看作序列逆序对的消除。完全随机序列逆序对数量为O(n^2),若一次元操作只消除一个逆序对,则时间复杂度不会低于O(n^2)。降低时间复杂度关键在于一次消除多个逆序对。\n\n1. 希尔排序通过增大最初的步长来企图一次消除多个逆序对。\n2. 归并排序消除逆序对最主要在于归并步骤。最后几次合并每个子步骤用O(1)时间消除O(n)个逆序对。\n3. 快速排序消除逆序对最主要在于划分步骤。每个划分步骤用O(n)时间消除O(n)+O(左长度×右长度)个逆序对。\n4. 堆排序逆序对消除方式比较Tricky,但可以看出消除逆序对大致在于出堆步骤,通过O(logn)时间复杂度消除O(n)个逆序对。(左小右大排序时需要建立左大右小的大顶堆,建堆时基本没有消除逆序对)\n\n# 最后\n\n这篇文章我们主要关注了排序算法中的大头——基于比较的排序算法。在下篇文章,我们再来看一下不基于比较的排序算法,以及外排序与并行排序。","title":"排序算法","abstract":"我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。","length":342,"created_at":"2021-01-11T22:57:10.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","排序"],"license":false}},{"slug":"python-dict","file":"public/content/articles/2020-08-02-python-dict.md","mediaDir":"content/articles/2020-08-02-python-dict","path":"/articles/python-dict","meta":{"content":"\n> CPython从3.6开始,字典(dict)不再是无序的了——字典的修改了原先的底层实现,变得能按字典插入的顺序进行遍历。而Python从3.7开始将字典的有序性写入语言特性,不管是Jython、IronPython还是其他Python实现,从3.7开始大家的字典都是有序的了。\n\n# 前言\n\n以前参加Python相关的面试时,面试官经常都会问一个问题:Python里的字典(dict)是有序的吗?\n\n这自然难不倒我,我也照本宣科地讲:Python的字典底层是用哈希表实现的,在不发生冲突时读写的时间复杂度是O(1),比读写时间复杂度为O(logn)的红黑树要更快。但红黑树可以按下标的大小顺序进行遍历,而Dict遍历时是无序的。\n\n我讲的时候没感觉到任何的违和感,估计面试官们也没觉得任何的不对。直到有一天,我查Python各个版本的新特性时,发现Python 3.6的What's New里有[这么一条](https://docs.python.org/3/whatsnew/3.6.html#new-dict-implementation):\n\n> New dict implementation\n> \n> The dict type now uses a “compact” representation based on a proposal by Raymond Hettinger which was first implemented by PyPy. The memory usage of the new dict() is between 20% and 25% smaller compared to Python 3.5.\n> \n> The order-preserving aspect of this new implementation is considered an implementation detail and should not be relied upon (this may change in the future, but it is desired to have this new dict implementation in the language for a few releases before changing the language spec to mandate order-preserving semantics for all current and future Python implementations; this also helps preserve backwards-compatibility with older versions of the language where random iteration order is still in effect, e.g. Python 3.5).\n\n啥情况?CPython的dict竟然优化了内存,还变有序了!?\n\n# Python 3.5 以前dict的实现\n\n先不着急看Python 3.6 里的dict,我们先来看看Python 3.5之前的dict是怎么实现的,再拿3.6来做对比。\n\n在Python 3.5以前,dict是用Hash表来实现的,而且Key和Value直接储存在Hash表上。想通过Key获取Value,只需通过Python内部的Hash函数计算出Key对应的Hash值,再映射到Hash表上对应的地址,访问该地址即可获取Key对应的Value。如下图所示:\n\n我们知道,Hash表读写时间复杂度在不发生冲突的情况下都是O(1)。\n\n为什么呢?我们可以把Hash表读写的步骤分开来看:\n\n1. 首先用Hash函数计算key的Hash值,Hash函数一般来说时间复杂度都是O(1)的。\n2. 计算出Hash值后,映射到Hash表内的数组下标,一般用取余数或是取二进制后几位的方式实现,时间复杂度也是O(1)。\n3. 然后用数组下标读取数组中实际储存的键值,数组的下标读取时间复杂度也是O(1)。\n\n这三个步骤串起来后复杂度并没有提升,总的时间复杂度自然也是O(1)的。\n\n而内部储存空间,Python字典中称为entries。entries相当于一个数组,是一段连续的内存空间,每个位置储存一个(Hash值,指向Key的指针,指向Value的指针)三元组。\n\n当然,由于抽屉原理,我们知道Hash表不可避免的会出现Hash冲突,Python的dict也不例外。\n\n而解决Hash冲突的方法有很多,比如C++的unordered_map和Go的map就用链地址法来解决冲突,用链表储存发生冲突的值。而Java更进一步,当链表长度超过8时就转换成红黑树,将链表O(n)的查找复杂度降为O(logn)。C#的HashTable则是用再散列法,内部有多个Hash函数,一次冲突了就换一个函数再算,直到不冲突为止。\n\n而Python的dict则是利用开放寻址法。当插入数据发生冲突时,就会从那个位置往后找,直到找到有空位的地址为止。要查的时候,也是把下标值映射到到地址后,先对比一下下标值相不相等,若不相等则往后继续对比。\n\n这也造成个问题,dict中的元素不能直接从entries中清理掉,不然往后寻找的查找链就会断掉了。只能是先标记住删除,等到一定时机再一并清理。\n\n此外我们也知道,当冲突过发生得过多,dict读写所需的时间也会变多,时间复杂度不再是O(1),这也是Hash表的通病了。\n\nPython中dict初始化时,内部储存空间entries容量为8。当内部储存空间占用到一定程度(entries容量×装填因子,Python的dict中装填因子是2/3)后,就会进行倍增扩容。每次扩容都要遍历原先的元素,时间复杂度为O(n),但基本上插入O(n)次之后才会进行一次扩容,所以扩容的均摊时间复杂度为O(1)。而扩容时会重新进行Hash值到entries位置的映射,此时就是把标记删除但仍留在entries中的元素清理掉的最佳时机。\n\nPython3.5之前这种dict的实现就有两个毛病:\n\n1. 元素的顺序不被记录。两个Key值通过Hash函数的出来的Hash值不一定能保证原来的大小关系,由于Hash冲突、扩容等影响元素的顺序也会变化。当然这种无序性也是Hash表通用的特点了。\n2. 占用了太多了无用空间。上面说到entries中每个位置储存一个(Hash值,指向Key的指针,指向Value的指针)三元组,没用到或是标记删除的位置占用了大量的空间。\n\n于是,Raymond Hettinger就提出了一种新的dict实现方式。在CPython3.6中就使用了这种新的实现方式。\n\n# CPython3.6中dict的实现\n\n当要实现一个如下的dict时:\n\n```python\nd = {\n 'timmy': 'red', \n 'barry': 'green', \n 'guido': 'blue'\n}\n```\n\n如在上一节中所讲,在Python3.5以前,在内存储存的形式可以表示成这样子:\n\n```python\nentries = [['--', '--', '--'],\n [-8522787127447073495, 'barry', 'green'],\n ['--', '--', '--'],\n ['--', '--', '--'],\n ['--', '--', '--'],\n [-9092791511155847987, 'timmy', 'red'],\n ['--', '--', '--'],\n [-6480567542315338377, 'guido', 'blue']]\n```\n\n而CPython3.6以后,是以这种形式储存在内存中的:\n\n```python\nindices = [None, 1, None, None, None, 0, None, 2]\nentries = [[-9092791511155847987, 'timmy', 'red'],\n [-8522787127447073495, 'barry', 'green'],\n [-6480567542315338377, 'guido', 'blue']]\n```\n\n改变了什么?\n\n1. dict内部的entries改为按插入顺序存储,新增了一个indices用于储存元素在entries中的下标。dict整体仍是Hash表结构,但Hash值映射到indices中,而不是直接映射到entries中。\n2. 由于entries改为了按插入顺序存储,使得申请entries容量时只要申请Hash表长度的2/3即可,省去了Hash表中的无用空间,储存更紧凑。\n3. dict读写步骤从原先的3步变为4步:计算key的Hash值,映射到indices内存空间,从indices读取entries的下标值,用下标从entries中读写数据。读写时间复杂度仍保持为O(1),冲突、删除标记等Hash表的特性也仍然存在。indices的扩容策略也仍然是倍增扩容,但因为填充因子仍然为2/3,entries每次扩容时只需申请indices长度的2/3即可。\n\n有什么好处?\n\n1. 压缩空间:原先Hash映射是直接映射到entries上,会有大量的空隙。现在Hash映射到indices上,而entries中可更紧凑地存储元素。而indices中储存的entries下标占用内存可以比entries元素要小得多——当entries长度足够短时每个下标只需占一个字节。indices中确实也还仍有空隙,但占用空间总要比旧的dict实现要小得多了。\n2. 更快的遍历:以前的实现遍历dict要遍历整个Hash表,需要挨个位置读取一下,判断它是空闲位置还是实际存在的元素。而现在只需要对变得更紧凑的entries遍历就行了。这也带来一个新的特性:entries是按照元素插入的顺序存储的,遍历entries自然也会按元素插入的顺序输出。这就给dict带来了有序性。\n3. 扩容时关注的内存块更少。原先的entries扩容时所有数据都要重新映射到内存上,cache利用率不好。现在扩容时基本可以整个entries直接复制(当然,有删除标记的数据这时要忽略)。\n\n综上,CPython3.6以后通过增加了一个indices增加了空间利用率,在维持读写时间复杂度不变的情况下增加了遍历与扩容效率。至于dict遍历变得有序,倒是有点次要的特性了。\n\n# 我们是否应利用新dict的有序性?\n\n既然Python中dict变得有序了,那我们是否应该主动去利用它呢?我是这么认为的:\n\n1. 在Python3.6中,我们不推荐利用dict的有序性。3.6时dict的有序性还只是CPython的一个实现细节,并不是Python的语言特性。当我们的代码不是在CPython环境下运行,dict的有序性就不起作用,就容易出莫名其妙的BUG了。\n2. 在Python3.7后,dict按插入顺序进行遍历的性质被写入Python语言特性中。这时确实在代码中利用dict有序性也没什么大问题。但dict这种数据结构,最主要的特性还是表现在Key映射到Value的这种关系,以及O(1)的读写时间复杂度。当我们的代码中需要关注到dict的遍历顺序时,我们就要先质问一下自己:是否应该改为用队列或是其他数据结构来实现?\n\n\n# 参考文献\n\n- [Are dictionaries ordered in Python 3.6+?](https://stackoverflow.com/questions/39980323/are-dictionaries-ordered-in-python-3-6)\n- [[Python-Dev] Python 3.6 dict becomes compact and gets a private version; and keywords become ordered](https://mail.python.org/pipermail/python-dev/2016-September/146327.html)\n- [[Python-Dev] More compact dictionaries with faster iteration](https://mail.python.org/pipermail/python-dev/2012-December/123028.html)\n- [关于python3.6中dict如何保证有序](https://zhuanlan.zhihu.com/p/36167600)\n- [python3.7源码分析-字典_小屋子大侠的博客-CSDN博客_python 字典源码](https://blog.csdn.net/qq_33339479/article/details/90446988)\n- [《深度剖析CPython解释器》9. 解密Python中字典和集合的底层实现,深度分析哈希表](https://www.cnblogs.com/traditional/p/13503114.html)\n- [CPython 源码阅读 - dict](http://blog.dreamfever.me/2018/03/12/cpython-yuan-ma-yue-du-dict/)","title":"Python字典的实现原理","abstract":"> CPython从3.6开始,字典(dict)不再是无序的了——字典的修改了原先的底层实现,变得能按字典插入的顺序进行遍历。而Python从3.7开始将字典的有序性写入语言特性,不管是Jython、IronPython还是其他Python实现,从3.7开始大家的字典都是有序的了。\n以前参加Python相关的面试时,面试官经常都会问一个问题:Python里的字典(dict)是有序的吗?\n这自然难不倒我,我也照本宣科地讲:Python的字典底层是用哈希表实现的,在不发生冲突时读写的时间复杂度是O(1),比读写时间复杂度为O(logn)的红黑树要更快。但红黑树可以按下标的大小顺序进行遍历,而Dict遍历时是无序的。","length":121,"created_at":"2020-08-02T00:10:10.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["Python","数据结构"],"license":false}},{"slug":"the-using-in-cpp","file":"public/content/articles/2020-01-28-the-using-in-cpp.md","mediaDir":"content/articles/2020-01-28-the-using-in-cpp","path":"/articles/the-using-in-cpp","meta":{"content":"\n## using的用法\n#### using与命名空间\n\n1. 引入整个命名空间中的成员\n \n 不引入命名空间时,使用其中变量需要使用`<命名空间名>::<变量名>`的方式使用。\n ```C++\n using namespace foo;\n ```\n 如此会将命名空间foo下所有的成员名称引入,可在直接以 `<变量名>` 的形式使用。但如此做有可能会使得命名空间foo中部分变量与当前定义的变量名冲突,违反命名空间隔离编译时名称冲突的初衷,因此不建议如此使用。\n\n2. 引入命名空间中的部分成员\n \n 可通过仅引入命名空间中部分的成员,避免命名冲突。\n ```C++\n using foo::bar;\n ```\n 这种方法仅会引入在语句中明确声明的名称。如using一个枚举类时,不会连其定义的枚举常量也一同引入。\n\n#### using与基类成员\n\n1. 子类中引入基类名称\n \n ```C++\n class Base {\n public:\n std::size_t size() const { return n; }\n protected:\n std::size_t n;\n };\n\n class Derived : private Base {\n public:\n using Base::size;\n protected:\n using Base::n;\n // ...\n };\n ```\n 例中子类private继承基类,由于private继承使得`Base::size`与`Base::n`可视性变为private。而使用`using Base::size`、`using Base::n`后,可分别使其变为public与protected。\n\n2. 子类成员函数与基类同名时保留基类函数用以重载\n \n ```C++\n class Base\n {\n public:\n int Func(){return 0;}\n };\n class Derived : Base\n {\n public:\n using Base::Func;\n int Func(int);\n };\n ```\n 子类中定义的成员函数与基类中重名时,即使函数原型不同,子类函数也会覆盖基类函数。\n \n 如果基类中定义了一个函数的多个重载,而子类中又重写或重定义了其中某些版本,或是定义了一个新的重载,则基类中该函数的所有重载均被隐藏。\n\n 此时可以在子类中使用`using Base::Func`,令基类中所有重载版本在子类中可见,再重定义需要更改的版本。\n\n又如cppreference中的[例子](https://en.cppreference.com/w/cpp/language/using_declaration#In_class_definition):\n```C++\n#include \nstruct B {\n virtual void f(int) { std::cout << \"B::f\\n\"; }\n void g(char) { std::cout << \"B::g\\n\"; }\n void h(int) { std::cout << \"B::h\\n\"; }\nprotected:\n int m; // B::m is protected\n typedef int value_type;\n};\n\nstruct D : B {\n using B::m; // D::m is public\n using B::value_type; // D::value_type is public\n\n using B::f;\n void f(int) { std::cout << \"D::f\\n\"; } // D::f(int) overrides B::f(int)\n using B::g;\n void g(int) { std::cout << \"D::g\\n\"; } // both g(int) and g(char) are visible\n // as members of D\n using B::h;\n void h(int) { std::cout << \"D::h\\n\"; } // D::h(int) hides B::h(int)\n};\n\nint main()\n{\n D d;\n B& b = d;\n\n// b.m = 2; // error, B::m is protected\n d.m = 1; // protected B::m is accessible as public D::m\n b.f(1); // calls derived f()\n d.f(1); // calls derived f()\n d.g(1); // calls derived g(int)\n d.g('a'); // calls base g(char)\n b.h(1); // calls base h()\n d.h(1); // calls derived h()\n}\n```\n`using`语句可以改变基类成员的可访问性,也能在子类中重载(Overload)、重写(Override)基类的函数,或是通过重定义隐藏(Hide)对应的基类函数。\n\n\n#### using与别名\n\nusing在C++11开始,可用于别名的声明。用法如下:\n```C++\nusing UPtrMapSS = std::unique_ptr>;//普通别名\nusing FP = void (*) (int, const std::string&);//函数指针别名\n\ntemplate \nusing Vec = MyVector>;//模板别名\nVec vec;//模板别名的使用\n```\n\n## using关键字与typedef关键字定义别名的不同\n\n在STL容器或是其他泛型中若是再接受一个容器类型,类型名称就会写得很长。使用typedef或using定义别名会变得比较方便:\n```C++\ntypedef std::unique_ptr> UPtrMapSS;\n\nusing UPtrMapSS = std::unique_ptr>;\n```\n\n对于函数指针,使用using语句可以把函数原型与别名强制分到左右两边,比使用typedef易读得多:\n```C++\ntypedef void (*FP) (int, const std::string&);\n\nusing FP = void (*) (int, const std::string&);\n```\n\n---\n\n在C++中,若试图使用typedef定义一个模板:\n```C++\ntemplate \ntypedef MyVector> Vec;\n\n// usage\nVec vec;\n```\n编译就会报错,提示:\n> error: a typedef cannot be a template\n\n在一些STL中,通过如下方式包装一层来使用:\n```C++\ntemplate \nstruct Vec\n{\n typedef MyVector> type;\n};\n\n// usage\nVec::type vec;\n```\n\n如此显得十分不美观,且要是在模板类中或参数传递时使用typename强制这为类型,而不是其他如静态成员等语法:\n```C++\ntemplate \nclass Widget\n{\n typename Vec::type vec;\n};\n```\n\n而using关键字可定义模板别名,则一切都会显得十分自然:\n```C++\ntemplate \nusing Vec = MyVector>;\n\n// usage\nVec vec;\n\n// in a class template\ntemplate \nclass Widget\n{\n Vec vec;\n};\n```\n\n---\n\n能做到类似别名功能的,还有宏#define。但#define运行在编译前的宏处理阶段,对代码进行字符串替换。没有类型检查或其他编译、链接阶段才能进行的检查,不具备安全性。在C++11中不提倡使用#define。\n\n\n\n \n ","title":"C++中using关键字的使用","abstract":"1. 引入整个命名空间中的成员\n 不引入命名空间时,使用其中变量需要使用`<命名空间名>::<变量名>`的方式使用。\n ```C++","length":192,"created_at":"2020-01-28T18:00:00.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["C++","杂技"],"license":false}},{"slug":"Building-this-blog","file":"public/content/articles/2020-01-27-Building-this-blog.md","mediaDir":"content/articles/2020-01-27-Building-this-blog","path":"/articles/Building-this-blog","meta":{"content":"\n> “Stop Trying to Reinvent the Wheel.”\n\n## 博客构建\n\n\n#### 把仓库clone到本地\n\n参考[BruceZhao][BruceZhao]提供的中文翻译:[README.zh.md][READMEzh],先将[Huxpro][Huxpro]提供的[博客模板仓库][origin_repo]fork出来,`git clone`到本地。\n\n整个网站文件夹大致结构如下:\n\n```\n├── _config.yml\n|\n├── _posts/\n| ├── 2007-10-29-awsome-file-name.md\n| └── 2009-04-26-stupid-file-name.md\n├── img/\n| ├── in-post/\n| ├── awsome-bg.jpg\n| ├── avatar-ryo.png\n| ├── favicon.ico\n| └── icon_wechat.jpg\n├── other_awsome_directory/\n| └── awsomefiles\n|\n|\n├── 404.html\n├── about.html\n├── index.html\n└── other_awsome_files\n```\n\n博客的文章上传到`_posts`文件夹中,网站中用到的图片上传到`img`文件夹中,网站的全局设置在`_config.yml`中进行。\n\n\n\n#### 修改**_config.yml**文件\n\n修改根目录下的`_config.yml`文件,将其中的内容更改为自己的信息。\n\n```yml\n# Site settings\ntitle: Ryo's Blog\nSEOTitle: 阿亮仔的博客 | 亮のブログ | Ryo's Blog\nheader-img: img/home-bg.jpg\nemail: qq250707340@163.com\ndescription: \"君の夢が 叶うのは 誰かのおかげじゃないぜ。\"\nkeyword: \"Ryo, Blog, 阿亮仔, りょう, 博客, ブログ, Algorithm, Unity, Python, C-Sharp\"\nurl: \"http://RyoJerryYu.github.io\" # your host, for absolute URL\nbaseurl: \"\" # for example, '/blog' if your blog hosted on 'host/blog'\ngithub_repo: \"https://github.com/RyoJerryYu/RyoJerryYu.github.io.git\" # you code repository\n```\n- `SEOTitle`: ``标签,即显示在浏览器标题中的文字。\n- `header-img`: 首页显示的图像,可以把路径更改为自己的图片。\n- `description`: `<meta name=\"description\">`中的内容。\n- `keyword`: `<meta name=\"keyword\">`中的内容。\n- `url`, `baseurl`: 分别为博客域名地址与其下路径。如不想将博客直接架在根路径下,需要对`baseurl`进行设置。\n- `github_repo`: 博客所在的GitHub仓库。\n\n---\n\n\n```yml\n# SNS settings\nRSS: false\n# weibo_username: huxpro\n# zhihu_username: huxpro\ngithub_username: RyoJerryYu\ntwitter_username: ryo_okami\n# facebook_username: huxpro\n```\n分别为各个社交网站上的账号信息,以供在侧边栏中直接跳转到对应的页面。可通过在行首添加或删除`#`进行注释或取消注释。\n\n从[原仓库][origin_repo]中直接fork出来时,社交网站的图标可能会有[无法显示的问题](https://github.com/Huxpro/huxblog-boilerplate/issues/17),其解决方法在[后面](#FixSNS)介绍。\n\n---\n\n\n```yml\n# Disqus settings\n#disqus_username: _your_disqus_short_name_\n\n# Duoshuo settings\n# duoshuo_username: huxblog\n# Share component is depend on Comment so we can NOT use share only.\n# duoshuo_share: true # set to false if you want to use Comment without Sharing\n\n# Gitalk\ngitalk:\n enable: false #是否开启Gitalk评论\n clientID: f2c84e7629bb1446c1a4 #生成的clientID\n clientSecret: ca6d6139d1e1b8c43f8b2e19492ddcac8b322d0d #生成的clientSecret\n repo: qiubaiying.github.io #仓库名称\n owner: qiubaiying #github用户名\n admin: qiubaiying\n distractionFreeMode: true #是否启用类似FB的阴影遮罩 \n```\n分别为各种评论系统。均未开启。\n\n---\n\n\n```yml\n# Analytics settings\n# Baidu Analytics\n# ba_track_id: 4cc1f2d8f3067386cc5cdb626a202900\n# Google Analytics\nga_track_id: 'UA-156933256-1' # Format: UA-xxxxxx-xx\nga_domain: auto\n```\n分别为百度与谷歌的网站统计。我只启用了Google Analytics。可先到[Google Marketing Platform](https://marketingplatform.google.com/about/)注册,开启Google Analytics。在`设置`->`媒体资源设置`中获得Track ID,并填入`ga_track_id`中。\n\n---\n\n\n```yml\n# Sidebar settings\nsidebar: true # whether or not using Sidebar.\nsidebar-about-description: \"记录平时遇到的问题,以及对应的解决方法。偶尔上传些许宅活或是娱乐方面的记录。\"\nsidebar-avatar: /img/avatar-ryo.png # use absolute URL, seeing it's used in both `/` and `/about/`\n```\n`sidebar`: 是否开启侧边栏,为`true`或`false`。\n`sidebar-about-description`: 显示在侧边栏中的个人简介。\n`sidebar-avatar`: 显示在侧边栏中的头像。\n\n---\n\n\n```yml\n# Featured Tags\nfeatured-tags: true # whether or not using Feature-Tags\nfeatured-condition-size: 2 # A tag will be featured if the size of it is more than this condition value\n```\n是否开启tag功能,以及最少要达到多少篇文章才能使tag显示在首页上。\n\n\n\n#### 修改主页等信息\n\n修改`index.html`、`404.html`、`about.html`、`tags.html`等文件,将其中的内容更改为自己的信息。\n\n- 在`index`中,修改`description`对应的内容,亦即主页中标题下方的描述。\n- 在`404`、`tags`、`about`中,修改`description`的内容,亦即404页面中的描述信息。如有需要,也可以修改`header-img`,即404页面的图片地址。\n- 在`about`中,还有修改自我介绍对应的内容。\n\n\n\n#### 修改图片信息\n\n修改`img/`下的图片,替换为自己的图片。要记得替换以下图片:\n- `avatar-ryo.png`\n- `favicon.ico`\n- `icon_wechat.png`\n\n\n\n#### 修改README.md\n\nREADME.md为Github仓库的介绍,可以在README.md中写上这个博客主要的内容,让别人了解这个博客。\n\n\n\n#### 完成\n\n将`_posts`中的博文全部删除后,将本地文件全部push到GitHub仓库中。稍等后用浏览器浏览`<用户名>.github.io`(或是你在`_config.yml`中设定的路径)。若发现网页已更新,即博客搭建成功,可以开始写博文了。\n\n*然而,并没有成功。*\n\n\n\n## Fix Bug\n\n<p id = \"FixReadmeCh\"></p>\n\n#### 修复README.zh.md引发的错误\n\n按上述步骤搭建完毕后,网页并没有正常显示。此时GitHub账号所关联的邮箱中收到标题为**Page build failure**的邮件,内容如下:\n> The page build failed for the `master` branch with the following error:\n> The tag `if` on line 235 in `README.zh.md` was not properly closed.\n\n如[原仓库][origin_repo]中的[issue#11](https://github.com/Huxpro/huxblog-boilerplate/issues/11)所示,在`README.zh.md`中存在`if`语句,会触发错误。\n\n因并无其他特别的需求,此处采用暴力删除`README.zh.md`的方法解决。\n\n对应commit:[删除README.zh.md,尝试修复因...](https://github.com/RyoJerryYu/RyoJerryYu.GitHub.io/commit/098d710160775df9b6d2cf04d7d4eec526a67bf4)\n\n\n<p id = \"FixSNS\"></p>\n\n#### 修复SNS链接不正常显示\n\n修复上述错误后,稍等即可正常打开网页。但是,我们在`_config.yml`中设置好的SNS链接并没有在侧边栏以及网页底部正常显示。如原仓库中的[issue#17](https://github.com/Huxpro/huxblog-boilerplate/issues/17)所示,原因是gitpage必须通过https访问bootcss.com等的cdn。\n\n此处采用原仓库[pull request#21](https://github.com/Huxpro/huxblog-boilerplate/pull/21)的方法,修改`_includes/head.html`, `_includes/footer.html`, `_layouts/keynote.html`, `_layouts/post.html`文件,将其中`http`修改为`https`。\n\n对应commit:[fix: change http into https](https://github.com/RyoJerryYu/RyoJerryYu.GitHub.io/commit/ec954c380472f30f09efdfadd074cb7967c2fa11)\n\n\n\n## 上传文章\n\n文章主要放在_posts文件夹中,用`git push`的方式推送到GitHub仓库,即可完成文章上传。\n\n文章正文以**markdown**语法书写,在文本头部增加如下格式的信息:\n```\n---\nlayout: post\ntitle: \"Welcome to Ryo's Blog!\"\nsubtitle: \" \\\"Hello World, Hello Blog\\\"\"\ndate: 2020-01-27 12:00:00\nauthor: \"Ryo\"\nheader-img: \"img/post-bg-default.jpg\"\ntags:\n - 杂技\n - 杂谈\n---\n```\n其中:\n- `layout`为文章所用的模板,可选`post`或`keynote`,也可自己写一个模板html放在`_layouts`文件夹下。\n- `title`为文章标题,`subtitle`为文章副标题。\n- `date`为博客中显示的文章发表时间。\n- `author`为博客中显示的作者。\n- `header-img`为文章顶部显示的封面。\n- `tags`为文章的标签,我们的博客网站可以通过标签来快速寻找文章。\n\n把文章的文件名命名为时间+标题的形式,后缀名使用markdown文本的通用后缀名`md`,如`2020-01-27-hello-world.md`。完成后将此文本文件放到`_posts/`文件夹下。文章中使用到的图片建议放到`img/in-post/`文件夹下。\n\n完成后,使用`git push`推送到GitHub仓库,稍等后刷新博客网页即可看见刚才上传的文章。文章的url一般为:`<博客地址>/<文章文件名中的年>/<月>/<日>/<文件名中剩余部分>`。\n\n\n\n\n## 祝你开始愉快的博客生活。\n\n\n#### 感谢\n\n- [Huxpro][Huxpro]提供的博客模板:[huxblog-boilerplate][origin_repo]\n- [BruceZhao][BruceZhao]提供的中文翻译:[README.zh.md][READMEzh]\n- [Luo Yifan(罗一凡)](https://github.com/iVanlIsh)提供的Bug解决方案。\n\n\n\n\n[Huxpro]: https://github.com/huxpro\n[BruceZhao]: https://github.com/BruceZhaoR\n[origin_repo]: https://github.com/Huxpro/huxblog-boilerplate\n[READMEzh]: https://github.com/Huxpro/huxpro.github.io/blob/master/README.zh.md","title":"搭建博客的过程","abstract":"> “Stop Trying to Reinvent the Wheel.”\n参考[BruceZhao][BruceZhao]提供的中文翻译:[README.zh.md][READMEzh],先将[Huxpro][Huxpro]提供的[博客模板仓库][origin_repo]fork出来,`git clone`到本地。\n整个网站文件夹大致结构如下:","length":250,"created_at":"2020-01-27T14:00:00.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["杂技","Blog"],"license":false}},{"slug":"hello-world","file":"public/content/articles/2020-01-27-hello-world.md","mediaDir":"content/articles/2020-01-27-hello-world","path":"/articles/hello-world","meta":{"content":"\n> “Hello World!”\n\n## 这是我的第一篇博文\n\n自己盲人摸象折腾了一两天,终于利用GitHub Pages,把自己的博客搭好了。\n\n感谢[Huxpro][Huxpro]提供的博客模板,以及[BruceZhao][BruceZhao]编写的中文ReadMe。\n\n这个博客的使用流程:\n- 写作时利用**Markdown**语法书写,与日常编写GitHub上的文档相同。\n- 使用**Git Workflow**进行博客的更新。\n- 利用**GitHub Pages**提供的域名与免费空间,以及其支持的**Jekyll**进行网站搭建。\n\n我以后会利用这个博客,记录些许编程中遇到的问题。同时还有记录一下生活娱乐上的琐事。\n\n这第一篇博文主要用于测试一下博客是否运行成功,不打算写太多东西。今后有时间的话会记录一下搭建博客的过程。\n\n\n#### 感谢\n\n- [Huxpro][Huxpro]提供的博客模板:[huxblog-boilerplate](https://github.com/Huxpro/huxblog-boilerplate)\n- [BruceZhao][BruceZhao]提供的中文翻译:[README.zh.md](https://github.com/Huxpro/huxpro.github.io/blob/master/README.zh.md)\n\n\n\n\n[Huxpro]: https://github.com/huxpro\n[BruceZhao]: https://github.com/BruceZhaoR","title":"Welcome to Ryo's Blog!","abstract":"> “Hello World!”\n自己盲人摸象折腾了一两天,终于利用GitHub Pages,把自己的博客搭好了。\n感谢[Huxpro][Huxpro]提供的博客模板,以及[BruceZhao][BruceZhao]编写的中文ReadMe。","length":29,"created_at":"2020-01-27T12:00:00.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["杂技","杂谈"],"license":false}}],"allTagsList":[{"tag":"Kubernetes","slug":"kubernetes","path":"/tags/kubernetes","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"DevOps","slug":"devops","path":"/tags/devops","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Docker","slug":"docker","path":"/tags/docker","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Cloud Native","slug":"cloud-native","path":"/tags/cloud-native","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Blog","slug":"blog","path":"/tags/blog","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"init-a-new-hexo-project","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"VSCode","slug":"vscode","path":"/tags/vscode","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"Hexo","slug":"hexo","path":"/tags/hexo","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"JavaScript","slug":"javascript","path":"/tags/javascript","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"GitHub","slug":"github","path":"/tags/github","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"AWS","slug":"aws","path":"/tags/aws","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"CI/CD","slug":"ci-cd","path":"/tags/ci-cd","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"IaC","slug":"iac","path":"/tags/iac","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]},{"tag":"设计模式","slug":"设计模式","path":"/tags/设计模式","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"笔记","slug":"笔记","path":"/tags/笔记","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"排序","slug":"排序","path":"/tags/排序","postSlugs":[{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"Python","slug":"python","path":"/tags/python","postSlugs":[{"postSlug":"python-dict","postType":"article"}]},{"tag":"C++","slug":"c++","path":"/tags/c++","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"}]},{"tag":"杂技","slug":"杂技","path":"/tags/杂技","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"hello-world","postType":"article"}]},{"tag":"杂谈","slug":"杂谈","path":"/tags/杂谈","postSlugs":[{"postSlug":"hello-world","postType":"article"}]},{"tag":"Nextjs","slug":"nextjs","path":"/tags/nextjs","postSlugs":[{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"Cloud Computing","slug":"cloud-computing","path":"/tags/cloud-computing","postSlugs":[{"postSlug":"newest","postType":"idea"}]}]},"__N_SSG":true} \ No newline at end of file +{"pageProps":{"posts":[{"slug":"introduction-for-k8s-2","file":"public/content/articles/2022-08-20-introduction-for-k8s-2.md","mediaDir":"content/articles/2022-08-20-introduction-for-k8s-2","path":"/articles/introduction-for-k8s-2","meta":{"content":"\n我们之前说的都是用于部署 Pod 的资源,我们接下来介绍与创建 Pod 不相关的资源:储存与网络。\n\n# 储存\n\n其实我们之前已经接触过储存相关的内容了:在讲 Stateful Set 时我们提过 Stateful Set 创建出来的 Pod 都会有相互独立的储存;而讲 Daemon Set 时我们提到 K8s 推荐只在 Daemon Set 的 Pod 中访问宿主机磁盘。但独立的储存具体指什么?除了访问宿主机磁盘以外还有什么其他的储存?\n\n在 Docker 中,我们可以把宿主机磁盘上的一个路径作为一个 Volume 来给容器绑定,或者直接使用 Docker Engine 管理的 Volume 来提供持久化存储或是容器间共享文件。在 K8s 里面也沿用了 Volume 这个概念,可以通过 Mount 绑定到容器内的路径,并通过实现 CSI 的各种引擎来提供更多样的存储。\n\n> CSI: Container Storage Interface ,容器储存接口标准,是 K8s 提出的一种规范。不管是哪种储存引擎,只要编写一个对应的插件实现 CSI ,都可以在 K8s 中使用。\n\n### K8s 中使用 Volume 与可用的 Volume 类型\n\n其实 K8s 中使用 Volume 的例子我们一开始就已经接触过了。还记得一开始介绍 Pod 时的 Nginx 例子吗?\n\n```yaml\nmetadata:\n name: simple-webapp\nspec:\n containers:\n - name: main-application\n image: nginx\n volumeMounts:\n - name: shared-logs\n mountPath: /var/log/nginx\n - name: sidecar-container\n image: busybox\n command: [\"sh\",\"-c\",\"while true; do cat /var/log/nginx/access.log; sleep 30; done\"]\n volumeMounts:\n - name: shared-logs\n mountPath: /var/log/nginx\n volumes:\n - name: shared-logs\n emptyDir: {}\n```\n\n这个 Pod 描述中声明了一个种类为 `emptyDir` 的,名为 `shared-logs` 的 Volume ,然后 Pod 中的两个容器都分别 Mount 了这个 Volume 。\n\nK8s 中默认提供了几种 Volume ,比如:\n\n- emptyDir :一个简单的空目录,一般用于储存临时数据或是 Pod 的容器之间共享数据。\n- hostPath :绑定到节点宿主机文件系统上的路径,一般在 Daemon Set 中使用。\n- gitRepo :这种 Volume 其实相当于 emptyDir ,不过在 Pod 启动时会从 Git 仓库 clone 一份内容作为默认数据。\n- configMap 、 secret :一般用于配置文件加载,需要与 configMap 、 secret 这两种资源一同使用。会将 configMap 、 secret 中对应的内容拷贝一份作为 Volume 绑到容器。(下一节中会展开讨论)\n- nfs 、 glusterfs 、 ……:可以通过各种网络存储协议直接挂载一个网络存储\n- (deprecated!) gcePersistentDisk 、 awsElasticBlockStore ……:可以调用各个云平台的 API ,创建一个块储存硬件挂载到宿主机上,再将那个硬件挂载到容器中。\n- persistentVolumeClaim :持久卷声明,用于把实际储存方式抽象化,使得 Pod 不需要关心具体的储存类型。这种类型会在下面详细介绍。\n\n我们可以注意到, Volume 的声明是 Pod 的一个属性,而不是一种单独的资源。 Volume 是 Pod 的一部分,因此不同的 Pod 之间永远不可能共享同一个 Volume 。\n\n> 但是 Volume 所指向的位置可以相同,比如 HostPath 类型的 Volume 就可以两个 Pod 可以绑定到宿主机上同一个路径,因此 Volume 里的数据还是能通过一定方式在 Pod 间共享。但当然 K8s 不推荐这么做。\n\n另外,由于 Volume 是 Pod 的一部分, Volume 的生命周期也是跟随 Pod 的,当一个 Pod 被销毁时, Volume 也会被销毁,因此最主要还是用于 Pod 内容器间的文件共享。如果需要持久化储存,需要使用 Persistent Volume 。\n\n> Volume 会被销毁不代表 Volume 指向的内容会被销毁。比如 hostPath 、 NFS 等类型 Volume 中的内容就会继续保留在宿主机或是 NAS 上。下面提到的 Presistent Volume Claim 也是,拥有 `persistentVolumeClaim` 类型 Volume 的 Pod 被删除后对应的 PVC 不一定会被删除。\n\n### Presistent Volume 、 Presistent Volume Claim 、 Storage Class\n\n如果需要在 Pod 声明中直接指定 NFS 、 awsElasticBlockStore 之类的信息,就需要应用的开发人员对真实可用的储存结构有所理解,违背了 K8s 的理念。因此 K8s 就弄出了小标题中的三种资源来将储存抽象化。\n\n一个 Persistent Volume (PV) 对应云平台提供的一个块存储,或是 NAS 上的一个路径。可以简单地理解为 **PV 直接描述了一块可用的物理存储** 。因为 PV 直接对应到硬件,因此 PV 跟节点一样,是名称空间无关的。\n\n而一个 **Persistent Volume Claim (PVC) 则是描述了怎样去使用储存** :使用多少空间、只读还是读写等。一个 PVC 被创建后会且只会对应到一个 PV 。 PVC 从属于一个名称空间,并能被该名称空间下的 Pod 指定为一个 Volume 。\n\nPV 与 PVC 这两种抽象是很必要的。试想一下用自己的物理机搭建一个 K8s 集群的场景。你会提前给物理机插上许多个储存硬件,这时你就需要用 PV 来描述这些硬件,之后才能在 K8s 里利用这些硬件的储存。而实际将应用部署到 K8s 中时,你才需要用 PVC 来描述 Pod 中需要怎么样的储存卷,然后 K8s 就会自动挑一个合适 PV 给这个 PVC 绑定上。这样实际部署应用的时候就不用再特意跑去机房给物理机插硬件了。\n\n但是现在都云原生时代了,各供应商都有提供 API 可以直接创建一个块储存,还要想办法提前准备 PV 实在是太蠢了。于是便需要 Storage Class 这种资源。\n\n使用 Storage Class 前需要先安装各种云供应商提供的插件(当然使用云服务提供的 K8s 的话一般已经准备好了),然后再创建一个 Storage Class 类型的资源(当然一般也已经准备好了)。下面是 AWS 上的 EKS 服务中默认自带的 Storage Class :\n\n```yaml\napiVersion: storage.k8s.io/v1\nkind: StorageClass\nmetadata:\n annotations:\n storageclass.kubernetes.io/is-default-class: \"true\"\n name: gp2\nprovisioner: kubernetes.io/aws-ebs\nparameters:\n fsType: ext4\n type: gp2\n# 当 PVC 被删除时会同时删除 PV\nreclaimPolicy: Delete\n# 只有当 PVC 被绑定为一个 Pod 的 Volume 时才会创建一个 PV\nvolumeBindingMode: WaitForFirstConsumer\n```\n\n可以看到 EKS 自带的 gp2 提供了一些默认的选项,我们也可以类似地去定义自己的 Storage Class 。有了 gp2 这个 Storage Class ,我们创建一个 PVC 后 K8s 就会调用 AWS 的 API ,创建一个块储存接到我们的节点上,然后 K8s 再自动创建一个 PV 并绑定到 PVC 上。\n\n例如,我们部署 Kafka 时会创建一个这样的 PVC :\n\n```yaml\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: data-kafka-0\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 10Gi\n storageClassName: gp2\n```\n\nK8s 就会自动为我们创建出一个对应的 PV :\n\n```sh\n# `pvc-` 开头这个是 AWS 自动给我们起的名字。它虽然是 `pvc-` 开头,但他其实是一个 PV 。\nNAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE\npvc-3614c15f-5697-4d66-a13c-6ddf7eb89998 10Gi RWO Delete Bound kafka/data-kafka-0 gp2 152d\n```\n\n要是打开 AWS Console 还会发现, K8s 调用了 AWS 的 API ,自动为我们创建了一个 EBS 块储存并绑定到了我们对应的宿主机上。\n\n可以用下面这张图来表示 Pod 中的 Volume 、 PVC 、 PV 之间的关系:\n\n```mermaid\nflowchart TD\n\nsubgraph Pod[Pod: Kafka-0]\nsubgraph Container[Container: docker.io/bitnami/kafka:3.1.0]\nvm[VolumeMount: /bitnami/kafka]\nend\nvolume[(Volume: data)]\nvm --> volume\nend\n\npvc[pvc: data-kafka-0]\npv[pv: pvc-3614c15f-5697-4d66-a13c-6ddf7eb89998]\nebs[ebs: AWS 为我们创建的块储存硬件]\n\nvolume --> pvc\npvc --> pv\npv --> ebs\n```\n\n而 Storage Class 在上图中则负责读取我们提交的 PVC ,然后创建 PV 与 EBS 。\n\n### 再说回 Stateful Set\n\n之前我们提到 Stateful Set 时说到 Stateful Set 创建的 Pod 拥有固定的储存,到底是什么意思呢?跟 Deployment 的储存又有什么区别呢?\n\n我们先来看看,如果要给 Deployment 创建出来的 Pod 挂载 PVC 需要怎么做。下面是一个部署 Nginx 的 Deployment 清单,其中 html 目录下的静态文件存放在 NFS 里,通过 PVC 挂载到 Pod 中:\n\n```yaml\n# 这里省略了 Service 相关的内容\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: nginx-dpl-with-nfs-pvc\nspec:\n replicas: 3\n selector:\n matchLabels:\n app: nginx\n template:\n metadata:\n labels:\n app: nginx\n spec:\n containers:\n - name: nginx\n image: nginx:alpine\n ports:\n - containerPort: 80\n name: web\n volumeMounts: #挂载容器中的目录到 pvc nfs 中的目录\n - name: www\n mountPath: /usr/share/nginx/html\n volumes:\n - name: www\n persistentVolumeClaim: #指定pvc\n claimName: nfs-pvc-for-nginx\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: nfs-pvc-for-nginx\n namespace: default\nspec:\n storageclassname: \"\" # 指定使用现有 PV ,不使用 StorageClass 创建 PV\n accessModes:\n - ReadWriteMany\n resources:\n requests:\n storage: 1Gi\n---\n# 这个例子中需要挂载 NFS 上的特定路径,所以手动定义了一个 PV\n# 一般情况下我们不会手动创建 PV,而是使用 StorageClass 自动创建\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n name: nfs-pv-for-nginx\nspec:\n capacity: \n storage: 1Gi\n accessModes:\n - ReadWriteMany\n persistentVolumeReclaimPolicy: Retain\n nfs:\n path: /nfs/sharefolder/nginx\n server: 81.70.4.171\n```\n\n这份清单我们主要关注前两个资源,我们可以看到除了一个 Deployment 资源以外我们还单独定义了一个 PVC 资源。然后在 Deployment 的 Pod 模板中声明并绑定了这个 PVC 。\n\n可这样 apply 了之后会发生什么情况呢?因为我们只声明了一份 PVC ,当然我们只会拥有一个 PVC 资源。但这个 Deployment 的副本数是 3 ,因此我们会有 3 个相同的 Pod 去绑定同一个 PVC 。也就是最终会在 3 个容器里访问同一个 NFS 的同一个目录。如果我们在其中一个容器里对这个目录作修改,也会影响到另外两个容器。\n\n> 注:这一现象不一定在任何情况下都适用。比如 AWS 的 EBS 卷只支持单个 AZ 内的绑定。如果 Pod 因为 Node Affinity 等设定被部署到了多个区,没法绑定同一个 EBS 卷,就会在 Scedule 的阶段报错。\n\n很多时候我们都不希望多个 Pod 绑定到同一 PVC 。比如我们部署一个 DB 集群的时候,如果好不容易部署出来的多个实例居然用的是同一份储存,就会显得很呆。 Stateful Set 就是为了解决这种情况,会为其管理下的每个 Pod 都部署一个专用的 PVC 。\n\n下面是给 Stateful Set 创建出来的 Pod 挂载 PVC 的一份清单:\n\n```yaml\n# 这里省略了 Service 相关的内容\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: web\nspec:\n serviceName: \"nginx\"\n replicas: 2\n selector:\n matchLabels:\n app: nginx\n template:\n metadata:\n labels:\n app: nginx\n spec:\n containers:\n - name: nginx\n image: k8s.gcr.io/nginx-slim:0.8\n ports:\n - containerPort: 80\n name: web\n volumeMounts:\n - name: www\n mountPath: /usr/share/nginx/html\n volumeClaimTemplates:\n - metadata:\n name: www\n spec:\n accessModes: [ \"ReadWriteOnce\" ]\n resources:\n requests:\n storage: 1Gi\n```\n\n我们可以看到,部署 Stateful Set 时我们不能另外单独定义一份 PVC 了,只能作为 Stateful Set 定义的一部分,在 volumeClaimTemplates 字段中定义 PVC 的模板。这样一来, Stateful Set 会根据这个模板,为每个 Pod 创建一个对应的 PVC ,并作为 Pod 的 Volume 绑定上:\n\n```bash\n# Stateful Set 创建出来的 Pod ,名字都是按顺序的\n$ kubectl get pods -l app=nginx\nNAME READY STATUS RESTARTS AGE\nweb-0 1/1 Running 0 1m\nweb-1 1/1 Running 0 1m\n\n# Stateful Set 创建出来的 PVC ,名字与 Pod 的名字一一对应\n$ kubectl get pvc -l app=nginx\nNAME STATUS VOLUME CAPACITY ACCESSMODES AGE\nwww-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s\nwww-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s\n```\n\n这样, Stateful Set 的多个 Pod 就会拥有自己的储存,不会相互打架了。另外,如果我们事先定义了 StorageClass ,还能根据 Stateful Set 的副本数动态配置 PV 。\n\n### ConfigMap 与 Secret 挂载作为特殊的卷\n\n有时候我们需要使用配置文件来配置应用(比如 Nginx 的配置文件),而且我们有时候会需要不重启 Pod 就热更新配置。如果用 PVC 来加载配置文件略微麻烦,这时候可以使用 Config Map 。\n\n下面是 K8s 官网上 Config Map 的一个例子:\n\n```yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: game-demo\ndata:\n # 一个 Key 可以对应一个值\n player_initial_lives: \"3\"\n ui_properties_file_name: \"user-interface.properties\"\n\n # 一个 Key 也可以对应一个文件的内容\n game.properties: |\n enemy.types=aliens,monsters\n player.maximum-lives=5 \n user-interface.properties: |\n color.good=purple\n color.bad=yellow\n allow.textmode=true \n---\napiVersion: v1\nkind: Pod\nmetadata:\n name: configmap-demo-pod\nspec:\n containers:\n - name: demo\n image: alpine\n command: [\"sleep\", \"3600\"]\n env:\n # ConfigMap 的 Key 可以作为环境变量引用\n - name: PLAYER_INITIAL_LIVES\n valueFrom:\n configMapKeyRef:\n name: game-demo # 从这个 Config Map 里\n key: player_initial_lives # 拿到这个 key 的值\n - name: UI_PROPERTIES_FILE_NAME\n valueFrom:\n configMapKeyRef:\n name: game-demo\n key: ui_properties_file_name\n volumeMounts:\n - name: config\n mountPath: \"/config\"\n readOnly: true\n volumes:\n # 定义 Pod 的 Volume ,种类为 configMap\n - name: config\n configMap:\n name: game-demo # ConfigMap的名字\n # 需要作为文件放入 Volume 的 Key\n items:\n - key: \"game.properties\"\n path: \"game.properties\"\n - key: \"user-interface.properties\"\n path: \"user-interface.properties\"\n```\n\n我们可以看到 ConfigMap 里的 Key 可以作为文件或是环境变量加载到 Pod 中。另外,作为环境变量加载后其实还能作为命令行参数传入应用,实现各种配置方式。如果修改 Config map 的内容,也可以自动更新 Pod 中的文件。\n\n然而, Config Map 的热更新有一些不太灵活的地方:\n\n1. 作为环境变量加载的 Config Map 数据不会被热更新。想要更新这一部分数据需要重启 Pod。(当然,命令行参数也不能热更新)\n2. 由于 Kubelet 会先将 Config Map 内容加载到本地作为缓存,因此修改 Config Map 后新的内容不会第一时间加载到 Pod 中。而且在旧版本的 K8s 中, Config Map 被更新直到缓存被刷新的时间间隔还会很长,新版本的 K8s 这一部分有了优化,可以设定刷新时间,但会导致 API Server 的负担加重。(这其实是一个 Known Issue ,被诟病多年: https://github.com/kubernetes/kubernetes/issues/22368 )\n\n除 Config Map 以外, K8s 还提供了一种叫 Secret 的资源,用法和 Config Map 几乎一样。对比 Config Map ,Secret 有以下几个特点:\n\n1. 在 Pod 里, Secret 只会被加载到内存中,而永远不会被写到磁盘上。\n2. 用 `kubectl get` 之类的命令显示的 Secret 内容会被用 base64 编码。(不过, well ,众所周知 base64 可不算是什么加密)\n3. 可以通过 K8s 的 Service Account 等 RBAC 相关的资源来控制 Secret 的访问权限。\n\n不过,由于 Secret 也是以明文的形式被存储在 K8s 的主节点中的,因此需要保证 K8s 主节点的安全。\n\n> **Downward API 挂载作为特殊的卷**\n> \n> 还有另外一种叫 Downward API 的东西,可以作为 Volume 或是环境变量被加载到 Pod 中。有一些参数我们很难事先在 Manifest 中定义( e.g. Deployment 生成的 Pod 的名字),因此可以通过 Downward API 来实现。\n> \n> ```yaml\n> apiVersion: v1\n> kind: Pod\n> metadata:\n> name: test-volume-pod\n> namespace: kube-system\n> labels:\n> k8s-app: test-volume\n> node-env: test\n> spec:\n> containers:\n> - name: test-volume-pod-container\n> image: busybox:latest\n> env:\n> - name: POD_NAME # 将 Pod 的名字作为环境变量 POD_NAME 加载到 Pod 中\n> valueFrom:\n> fieldRef:\n> fieldPath: metadata.name\n> command: [\"sh\", \"-c\"]\n> args:\n> - while true; do\n> cat /etc/podinfo/labels | echo;\n> env | sort | echo;\n> sleep 3600;\n> done;\n> volumeMounts:\n> - name: podinfo\n> mountPath: /etc/podinfo\n> volumes:\n> - name: podinfo\n> downwardAPI: # Downward API 类型的卷\n> items:\n> - path: \"labels\" # 将 Pod 的标签作为 labels 文件挂载到 Pod 中\n> fieldRef:\n> fieldPath: metadata.labels\n> ```\n\n\n\n# 网络\n\n其实 Pod 只要部署好了,就会被分配到一个集群内部的 IP 地址,流量就可以通过 IP 地址来访问 Pod 了。然而通过可能会有很大问题: **Pod 随时会被杀死。** 虽然通过用 Deployment 等资源可以在挂掉后重新创建一个 Pod ,但那毕竟是不同的 Pod , IP 已经改变。\n\n另外, Deployment 等资源的就是为了能更方便的做到多副本部署及任意缩容扩容而存在的。如果在 K8s 中访问 Pod 还需要小心翼翼地去找到 Pod 的 IP 地址,或是去寻找 Pod 是否部署了新副本, Deployment 等资源就几乎没有存在价值了。\n\n> 其实 Pod 部署好后不止会被分配 IP 地址,还会被分配到一个类似 `<pod-ip>.<namespace>.pod.cluster.local` 的 DNS 记录。例如一个位于 default 名字空间,IP 地址为 172.17.0.3 的 Pod ,对应 DNS 记录为 `172-17-0-3.default.pod.cluster.local` 。\n\n### Service\n\n在古代,人们是通过注册中心、服务发现、负载均衡等中间件来解决上面这些问题的,但这样很不云原生。于是 K8s 引入了 Service 这种资源,来实现简易的服务发现、 DNS 功能。\n\n下面是一个经典的例子,部署了一个 Service 和一个 Deployment:\n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: auth-service\n labels:\n app: auth\nspec:\n type: ClusterIP\n selector:\n app: auth # 指向 Deployment 创建的 Pod\n ports:\n - port: 80 # Service 暴露的端口\n targetPort: 8080 # Pod 的端口\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: auth\n labels:\n app: auth\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: auth\n template:\n metadata:\n name: auth\n labels:\n app: auth\n spec:\n containers:\n - name: auth\n image: xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/auth:xxxxx\n ports:\n - containerPort: 8080\n```\n\n根据前面的知识我们知道,这份文件会部署 Deployment 会创建 2 个相同的 Pod 副本。另外还会部署一个名为 auth-service 的 Service 资源。这个 Service 暴露了一个 80 端口,并且指向那两个 Pod 的 8080 端口。\n\n而这份文件部署后, Service 资源就会在集群中注册一个 DNS A 记录(或 AAAA 记录),集群内其他 Pod (为了辨别我们叫它 Client )就可以通过相同的 DNS 名称来访问 Deployment 部署的这 2 个 Pod :\n\n```sh\ncurl http://auth-service.<namespace>.svc.cluster.local:80\n# 或者省略掉后面的一大串\ncurl http://auth-service.<namespace>:80\n# 如果 Client 和 Service 在同一个 Namespace 中,还可以:\ncurl http://auth-service:80\n```\n\n像这样 Client 通过 Service 来访问时,会随机访问到其中一个 Pod ,这样一来无论 Deployment 到底创建了多少个副本,只要副本的标签相同,就能通过同一个 DNS 名称来访问,还能自动实现一些简单的负载均衡。\n\n> **为什么 DNS 名称可以简化?**\n> \n> Pod 被部署时, kubelet 会为每个 Pod 注入一个类似如下的 `/etc/resolv.conf` 文件:\n> \n> ```\n> nameserver 10.32.0.10\n> search <namespace>.svc.cluster.local svc.cluster.local cluster.local\n> options ndots:5\n> ```\n> \n> Pod 中进行 DNS 查询时,默认会先读取这个文件,然后按照 `search` 选项中的内容展开 DNS 。例如,在 test 名称空间中的 Pod ,访问 data 时的查询可能被展开为 data.test.svc.cluster.local 。\n> 更多关于 `/etc/resolv.conf` 文件的内容可参考 https://www.man7.org/linux/man-pages/man5/resolv.conf.5.html\n\n### Service 的种类\n\n我们上面的例子中,可以看到 Service 资源有个字段 `type:ClusterIP` 。其实 Service 资源有以下几个种类:\n\n| 种类 | 作用 |\n| :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ClusterIP` | 这个类型的 Service 会在集群内创建一条 DNS A 记录并通过一定方法将流量代理到其指向的 Pod 上。这种 Service 不会暴露到集群外。这是最基础的 Service 种类。 |\n| `NodePort` | 这种 Service 会在 ClusterIP 的基础上,在所有节点上各暴露一个端口,并把端口的流量也代理到指向的 Pod 上。可以通过这种方法从集群外访问集群内的资源。 |\n| `LoadBalancer` | 这种 Service 会在 ClusterIP 的基础上,在所有节点上各暴露一个端口,并在集群外创建一个负载均衡器来将外部流量路由到暴露的端口,再把流量代理到指向的 Pod 上。这种 Service 一般需要调用云服务提供的 API 或是额外安装的插件。如果什么插件都没安装的话,这种 Service 部署后会与 `NodePort` 的表现一样。 |\n| `ExternalName` | 这种 Service 不需要 selector 字段指定后端,而是用 externalName 字段指定一个外部 DNS 记录,然后将流量全部指向外部服务。如果打算将集群内的服务迁移到集群外、或是集群外迁移到集群内,这种类型的 Service 可以实现无缝迁移。 |\n\n### 虚拟 IP 与 Headless Service\n\n如果你在集群内尝试对 Service 对应的 DNS 记录进行域名解析,会发现返回来的 IP 地址与 Service 指向的任何一个 Pod 对应的 IP 地址都不相同。如果你还尝试了去 Ping 这个 IP 地址,会发现不能 Ping 通。为什么会这样呢?\n\n原来,每个 Service 被部署后, K8s 都会给他分配一个集群内部的 IP 地址,也就是 Cluster IP (这也是最基础的 Service 种类会起名叫 Cluster IP 的原因)。\n\n但是这个 Cluster IP 不会绑定任何的网卡,是一个虚拟 IP 。然后 K8s 中有一个叫 kube-proxy 的组件(这里叫他做组件,是因为 kube-proxy 与 Service 、 Deployment 等不一样,不是一种资源而是 K8s 的一部分), kube-proxy 通过修改 iptables ,将虚拟 IP 的流量经过一定的负载均衡规则后代理到 Pod 上。\n\n![K8s 官网上的虚拟 IP 图](https://d33wubrfki0l68.cloudfront.net/27b2978647a8d7bdc2a96b213f0c0d3242ef9ce0/e8c9b/images/docs/services-iptables-overview.svg)\n\n> **为什么不使用 DNS 轮询?**\n> \n> 为什么 K8s 不配置多条 DNS A 记录,然后通过轮询名称来解析?为什么需要搞出虚拟 IP 这么复杂的东西?这个问题 K8s 官网上也有特别提到原因:\n> \n> - DNS 实现的历史由来已久,它不遵守记录 TTL,并且在名称查找结果到期后对其进行缓存。\n> - 有些应用程序仅执行一次 DNS 查找,并无限期地缓存结果。\n> - 即使应用和库进行了适当的重新解析,DNS 记录上的 TTL 值低或为零也可能会给 DNS 带来高负载,从而使管理变得困难。\n\n有些时候(比如想使用自己的服务发现机制或是自己的负载均衡机制时)我们确实也会想越过虚拟 IP ,直接获取背后 Pod 的 IP 地址。这时候我们可以将 Service 的 `spec.clusterIP` 字段指定为 `None` ,这样 K8s 就不会给这个 Service 分配一个 Cluster IP 。这样的 Service 被称为 **Headless Service** 。\n\nHeadless Service 资源会创建一组 A 记录直接指向背后的 Pod ,可以通过 DNS 轮询等方式直接获得其中一个 Pod 的 IP 地址。另外更重要的一点, Headless Service 还会创建一组 SRV 记录,包含了指向各个 Pod 的 DNS 记录,可以通过 SRV 记录来发现所有 Pod 。\n\n我们可以在集群里用 nsloopup 或 dig 命令去验证一下:\n\n```sh\n# 在集群的 Pod 内部运行\n$ nslookup kafka-headless.kafka.svc.cluster.local\nServer: 10.96.0.10\nAddress: 10.96.0.10#53\n\nName: kafka-headless.kafka.svc.cluster.local\nAddress: 172.17.0.6\nName: kafka-headless.kafka.svc.cluster.local\nAddress: 172.17.0.5\nName: kafka-headless.kafka.svc.cluster.local\nAddress: 172.17.0.4\n\n$ dig SRV kafka-headless.kafka.svc.cluster.local\n# .....\n;; ANSWER SECTION:\nkafka-headless.kafka.svc.cluster.local. 30 IN SRV 0 20 9092 kafka-0.kafka-headless.kafka.svc.cluster.local.\nkafka-headless.kafka.svc.cluster.local. 30 IN SRV 0 20 9092 kafka-1.kafka-headless.kafka.svc.cluster.local.\nkakfa-headless.kafka.svc.cluster.local. 30 IN SRV 0 20 9092 kafka-2.kafka-headless.kafka.svc.cluster.local.\n\n;; ADDITIONAL SECTION:\nkafka-0.kafka-headless.kafka.svc.cluster.local. 30 IN A 172.17.0.6\nkafka-1.kafka-headless.kafka.svc.cluster.local. 30 IN A 172.17.0.5\nkafka-2.kafka-headless.kafka.svc.cluster.local. 30 IN A 172.17.0.4\n```\n\n> 拥有 Cluster IP 的 Service 其实也有 SRV 记录。但这种情况的 SRV 记录中对应的 Target 仍为 Service 自己的 FQDN 。\n\n### 第三次回到 Stateful Set\n\n在上面 Headless Service 的例子中,我们看到,各个 Pod 对应的 DNS A 记录格式为 `<pod_name>.<svc_name>.<namespace>.svc.cluster.local` 。不对啊,之前的小知识里不是说过 Pod 被分配的 DNS A 记录格式应该是 `172-17-0-3.default.pod.cluster.local` 的吗?\n\n其实 Headless Service 还有一个众所周知的隐藏功能。 Pod 这种资源本身的参数中有 `subdomain` 字段和 `hostname` 字段,如果设置了这两个字段,这个 Pod 就拥有了形如 `<hostname>.<subdomain>.<namespace>.svc.cluster.local` 的 FQDN (全限定域名)。如果这时刚好在同一名称空间下有与 `subdomain` 同名的 Headless Service , DNS 就会用为这个 Pod 用它的 FQDN 来创建一条 DNS A 记录。\n\n比如 Pod1 在 `kafka` 名称空间中, `hostname` 为 `kafka-1` , `subdomain` 为 `kafka-headless` ,那么 Pod1 的 FQDN 就是 `kafka-1.kafka-headless.kakfa.svc.cluster.local` 。而同样在 `kafka` 名称空间中,刚好又有一个 `kafka-headless` 的 Headless Service ,那么 DNS 就会创建一条 A 记录,就可以通过 `kafka-1.kafka-headless.kafka.svc.cluster.local` 来访问 Pod1 了。当然,由于 DNS 展开,也可以用 `kafka-1.kafka-headless.kafka` 甚至是 `kafka-1.kafka-headless` 来访问这个 Pod 。\n\n其实这些 Pod 是用 Stateful Set 来部署的,这一部分其实是 Stateful Set 相关的功能。之前我们说到 Stateful Set 有唯一稳定的网络标识。我们现在就来详细讲讲,这“唯一稳定的网络标识”到底是在指什么。\n\n我们来看一下这个 kafka Stateful Set 到底是怎么部署的:\n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: kafka-headless\nspec:\n clusterIP: None # 这是一个 headless service\n ports:\n - name: tcp-client\n port: 9092\n protocol: TCP\n targetPort: kafka-client\n selector:\n select-label: kafka-label\n type: ClusterIP\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: kafka\nspec:\n replicas: 3\n serviceName: kafka-headless # 注意到这里有 serviceName 字段\n selector:\n matchLabels:\n select-label: kafka-label\n template:\n metadata:\n labels:\n select-label: kafka-label\n spec:\n containers:\n - name: kafka\n image: docker.io/bitnami/kafka:3.1.0-debian-10-r52\n # 接下来 Pod 相关部分省略\n # 下面 Volume 相关部分也省略\n```\n\n我们看到, Stateful Set 的定义中必须要用 `spec.serviceName` 字段指定一个 Headless Service 。 Stateful Set 创建 Pod 时,会自动给 Pod 指定 `hostname` 和 `subdomain` 字段。这样一来,每个 Pod 才有了唯一固定的 hostname ,唯一固定的 FQDN ,以及通过与 Headless Service 共同部署而获得唯一固定的 A 记录。(此外,其实当 Pod 因为版本升级等原因被重新创建时,相同序号的 Pod 还会被分配到相同固定的集群内 IP 。)\n\n> **关于 Stateful Set 中 `serviceName` 字段的争议**\n> \n> Stateful Set 中的 serviceName 字段是必填字段。这个字段唯一的作用其实就是给 Pod 指定 subdomain 。其实这样会有一些问题:\n> \n> 1. Stateful Set 部署时不会检查是否真的存在这么一个 Headless Service 。如果 serviceName 乱填一个值,会导致虽然 Pod 的 `hostname` 和 `subdomain` 都指定了却没有创建 A 记录的情况。\n> 2. 有时 Stateful Set 的 Pod 不需要接收流量,也不需要相互发现,这时候还强行需要指定一个 serviceName 显得有点多余。\n> \n> 在 GitHub 上有关于这个问题的 Issue : https://github.com/kubernetes/kubernetes/issues/69608\n\n### 从集群外部访问\n\n在 K8s 集群里把应用部署好了,可是如何让集群外部的客户端访问我们集群中的应用呢?这可能是大家最关心的问题。\n\n不过有认真听的同学估计已经有这个问题的答案了。之前我们讲过 NodePort 和 LoadBalancer 这两种 Service 类型。\n\n其中 NodePort Service 只是简单地在节点机器上各开一个端口,而如何路由、如何负载均衡等则一概不管。\n\n而 LoadBalancer Service 则是在 NodePort 的基础上再加一个一个负载均衡器,然后把节点暴露的端口注册到这个负载均衡器上。这样一来,集群外部的客户端就可以通过同一个 IP 来访问集群中的应用。但是要使用 LoadBalancer Service ,一般需要先安装云供应商提供的 Controller ,或是安装其他第三方的 Controller (比如 Nginx Controller )。\n\n在 Service 之外还另有一种资源类型叫 Ingress ,也可以用来实现集群外部访问集群内部应用的功能。 Ingress 其实也会在集群外创建一个负载均衡器,因此也需要预先安装云供应商的 Controller 。但 Ingress 与 Service 不同的是,它还会管理一定的路由逻辑,接收流量后可以根据路由来分配给不同的 Service 。\n\n| 类型 | OSI 模型工作层数 | 依赖于云平台或其他插件 |\n| :------------------- | :--------------- | :--------------------- |\n| NodePort Service | 第四层 | 否 |\n| LoadBalancer Service | 第四层 | 是 |\n| Ingress | 第七层 | 是 |\n\n特别再详细说一下 Ingress 这种资源。 Ingress 本身不会在集群内的 DNS 上创建记录,一般也不会主动去路由集群内的流量(除非你在集群内强行访问 Ingress 的负载均衡器…… 不过一般也没什么理由要这样做对吧)。但 Ingress 可以根据 HTTP 的 hostname 和 path 来路由流量,把流量分发到不同的 Service 上。 Ingress 也是 K8s 的原生资源里唯一能看到 OSI 第七层的资源。\n\n下面是 AWS 的 EKS 服务中部署的一个 Ingress 的例子(集群中已安装 AWS Load Balancer Controller ):\n\n```yaml\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n annotations:\n kubernetes.io/ingress.class: alb\n alb.ingress.kubernetes.io/scheme: internet-facing\n alb.ingress.kubernetes.io/target-type: ip\n alb.ingress.kubernetes.io/backend-protocol-version: GRPC\n alb.ingress.kubernetes.io/listen-ports: '[{\"HTTPS\":443}]'\n alb.ingress.kubernetes.io/healthcheck-path: /grpc.health.v1.Health/Check\n alb.ingress.kubernetes.io/healthcheck-protocol: HTTP\n alb.ingress.kubernetes.io/success-codes: 0,12\n alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:xxxxxxxxxx:certificate/xxxxxxxxxx\n\n external-dns.alpha.kubernetes.io/hostname: sample.example.com\n \n name: gateway-ingress\nspec:\n rules:\n - host: sample.example.com\n http:\n paths:\n - path: /grpc.health.v1.Health\n pathType: Prefix\n backend:\n service:\n name: health-service\n port:\n number: 50051\n - path: /proto.sample.v1.Sample\n pathType: Prefix\n backend:\n service:\n name: sample-service\n port:\n number: 50051\n```\n\n可以看到, Ingress 资源可以通过 `spec.rules` 字段中定义各条规则,通过 hostname 或是 path 等第七层的信息来进行路由。 Ingress 部署下去后, AWS Load Balancer Controller 会读取会根据的配置,并在云上创建一个 AWS Application Load Balancer (ALB),而 `spec.rules` 会应用到 ALB 上,由 ALB 来负责流量的路由。\n\n我们也会注意到,怎么 `metadata.annotations` 里有这么多奇奇怪怪的字段! Ingress 本身的功能都是 AWS Load Balancer Controller 调用 AWS 的 API 创建 ALB 来实现的。但 AWS 的 ALB 能实现的功能可不止 Ingress 字段定义的这些,比如安装 TLS 证书、 health check 等 spec 字段中描述不下的功能,就只能是通过 annotation 的形式来定义了。\n\n> 小彩蛋:可以看到例子中的 Ingress 资源 annotation 字段里还有一行 `external-dns.alpha.kubernetes.io/hostname: sample.example.com` 。其实这个 K8s 集群中还安装了 external-dns 这个应用,它可以根据 annotation 来在外部 DNS 上直接创建 DNS 记录!有了这个插件我们可不用再慢慢打开公共 DNS 管理页面,再小心翼翼地记下 IP 地址去添加 A 记录了。\n\n# 更高级的部署方式(一)\n\n一路说道这里, K8s 中最基础的资源大部分都已经介绍了。但是,这么多资源之间又需要相互配合,只部署一种资源基本没什么生产能力。\n\n比如只部署 Deployment 的话,我们确实是能在一组多副本的 Pod 里跑起可执行程序,但这组 Pod 却几乎没办法接受集群里其他 Pod 的流量(只能通过制定 Pod 的 IP 来访问,但 Pod 的 IP 是会变的)。因此一般来说一个 Deployment 都会搭配一个 Service 来使用。这还是最简单的一种搭配了。\n\n假若我们现在要在自己的 K8s 里安装一个别人提供的应用。当然由于 K8s 是基于容器的,只要别人提供了他应用的 yaml 清单,我们只用把清单用 `kubectl apply -f` 提交给 K8s ,然后让 K8s 把清单中的镜像拉下来就能跑了。可如果我们需要根据环境来改一些参数呢?\n\n如果别人提供的 yaml 文件比较简单还好说,改改对应的字段就好了。如果别人的应用比较复杂,那改 yaml 文件可就是一个大难题了。比如 AWS 的 Load Balancer Controller ,它的 yaml 清单文件可是多达 939 行!\n\n[[aws-elb-controller-lines.png]]\n\n在这种复杂的场景下,我们就需要一些更高级的部署方式了。\n\n### Helm\n\n首先来介绍的是 Helm 。 Helm 是一个包管理工具,可以类比一下 CentOS 中的 yum 工具。它可以把一组 K8s 资源发布成一个 Chart ,然后我们可以用 Helm 来安装这个 Chart ,并且可以通过参数设值来改变 Chart 中的部分资源。利用 Helm 安装 Chart 后还可以管理 Chart 的升级、回滚、卸载。\n\n使用别人提供的 Helm Chart 前,需要先 add 一下 Chart 的仓库,然后再安装仓库里提供的 Chart 。比如我们要安装 bitnami 提供的 Kafka Chart 时:\n\n```bash\n# 添加 https://charts.bitnami.com/bitnami 这个仓库,命名为 bitnami\nhelm repo add bitnami https://charts.bitnami.com/bitnami\n\n# 在 kafka 名称空间里安装 bitnami 仓库里的 kafka Chart ,并通过参数设置为 3 个副本,并同时安装一个 3 副本的 Zookeeper\nhelm install kafka -n kafka \\\n --set replicaCount=3 \\\n --set zookeeper.enabled=true \\\n --set zookeeper.replicaCount=3 \\\n bitnami/kafka\n```\n\n命令执行后, helm 就会根据参数与 Chart 的内容,在 K8s 里安装 StatefulSet 、 Service 、 ConfigMap 等一切所需要的资源。\n\n```sh\n$ k -n kafka get all,cm\nNAME READY STATUS RESTARTS AGE\npod/kafka-0 1/1 Running 1 46d\npod/kafka-1 1/1 Running 3 46d\npod/kafka-2 1/1 Running 3 46d\npod/kafka-zookeeper-0 1/1 Running 0 46d\npod/kafka-zookeeper-1 1/1 Running 0 46d\npod/kafka-zookeeper-2 1/1 Running 0 46d\n\nNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\nservice/kafka ClusterIP 172.20.1.196 <none> 9092/TCP 164d\nservice/kafka-headless ClusterIP None <none> 9092/TCP,9093/TCP 164d\nservice/kafka-zookeeper ClusterIP 172.20.227.236 <none> 2181/TCP,2888/TCP,3888/TCP 164d\nservice/kafka-zookeeper-headless ClusterIP None <none> 2181/TCP,2888/TCP,3888/TCP 164d\n\nNAME READY AGE\nstatefulset.apps/kafka 3/3 164d\nstatefulset.apps/kafka-zookeeper 3/3 164d\n\nNAME DATA AGE\nconfigmap/kafka-scripts 2 164d\nconfigmap/kafka-zookeeper-scripts 2 164d\nconfigmap/kube-root-ca.crt 1 165d\n```\n\n甚至, Helm 可以通过模板生成的 Pod 环境变量,来预先设置好 Kafka 的配置,让他找得到 Zookeeper 服务:\n\n```yaml\napiVersion: v1\nkind: Pod\n# 略去无关信息\nspec:\n containers:\n - name: kafka\n command:\n - /scripts/setup.sh\n env:\n - name: KAFKA_CFG_ZOOKEEPER_CONNECT\n value: kafka-zookeeper\n # ...\n```\n\n通过设置 `KAFKA_CFG_ZOOKEEPER_CONNECT` 这个环境变量,指定了 Kafka Broker 可以通过访问 `kafka-zookeeper` 来找到 zookeeper 服务。(还记得 zookeeper 的 Service 名字是 `kafka-zookeeper` 吗? zookeeper 与 kafka 部署在同一个名称空间里,因此可以直接通过 Service 名访问。)\n\n如果我们打开这个 helm chart 对应的[代码仓库](https://github.com/bitnami/charts/tree/master/bitnami/kafka),会发现原来有一组 go template 文件,以及一个 `values.yaml` 文件和 `Chart.yaml` 文件:\n\n```sh\n.\n├── Chart.lock\n├── Chart.yaml\n├── README.md\n├── templates\n│ ├── NOTES.txt # 这里定义的是 helm 工具的命令行信息\n│ ├── _helpers.tpl # 这里面是一些定义好的 go template 代码块可以供其他模板使用\n│ ├── configmap.yaml\n│ ├── statefulset.yaml\n│ ├── svc-headless.yaml\n│ ├── svc.yaml\n│ └── # 以下省略若干模板文件\n└── values.yaml\n```\n\n- `Chart.yaml` 中定义了这个 Chart 的基本信息,包括名称、版本、描述、依赖等。\n- `values.yaml` 中定义了这个 Chart 的默认参数,包括各种资源的默认配置、副本数量、镜像版本等。其中的值都可以通过 `helm install` 命令的 `--set` 参数来覆盖。\n- `templates/` 文件夹下的都是 go template 的模板文件。\n\n`helm install` 就是通过用 `values.yaml` 中预定义的参数,渲染 `templates/` 文件夹下的 go template 文件,生成最终的 yaml 文件,然后再通过 kubectl apply -f 的方式,将 yaml 文件里的资源部署到 K8s 里。然后通过忘资源里注入一些特殊 annotation 的方式来记住自己部署了那些资源,进而提供 `update` 、 `uninstall` 等功能。\n\n关于更多 Helm 的内容,可以参考[官方文档](https://helm.sh/docs/)。\n\n### Kustomize\n\n另一个部署工具是 Kustomize 。之前提到 Config Map 时的例子中,将配置文件的内容直接写进了 yaml 清单的一个字段里:\n\n```yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: game-demo\ndata:\n # 一个 Key 可以对应一个值\n player_initial_lives: \"3\"\n ui_properties_file_name: \"user-interface.properties\"\n\n # 一个 Key 也可以对应一个文件的内容\n game.properties: |\n enemy.types=aliens,monsters\n player.maximum-lives=5 \n user-interface.properties: |\n color.good=purple\n color.bad=yellow\n allow.textmode=true \n```\n\n其实这样很不好,先不说这样写没办法在 IDE 里用配置文件自己的语法检查,每行还需要一定的缩进,如果配置文件有好几百行,你甚至会忘了这一行到底是哪个配置文件!此时我们就会自然而然的想把每个配置文件以单独文件的形式保存。\n\nKustomize 就是这样一个工具,它可以帮助我们把每个配置文件以单独文件的形式保存,然后再通过一个 `kustomization.yaml` 文件,将这些配置文件组合起来,生成最终的 yaml 文件。\n\n```yaml\napiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n # 其他资源也可以单独使用一个文件定义\n - deployment.yaml\n\n# 用 configMapGenerator 从文件中生成 ConfigMap\nconfigMapGenerator:\n - name: game-demo\n literals:\n - \"ui_properties_file_name=user-interface.properties\"\n - \"player_initial_lives=3\"\n # 从文件中读取内容\n files:\n - game.properties\n - user-interface.properties\n# 有多个 configMap 时,可以通过统一的 generatorOptions 来设置一些通用的选项\ngeneratorOptions:\n disableNameSuffixHash: true\n```\n\n然后两个配置文件的内容可以单独用文件定义,此时可以结合 IDE 的语法检查,以及代码补全功能,来编写配置文件。\n\n```properties\n# user-interface.properties\ncolor.good=purple\ncolor.bad=yellow\nallow.textmode=true \n```\n\n然后将 `kustomization.yaml` 和其他所需的文件都放在同一个目录下:\n\n```bash\n.\n├── kustomization.yaml\n├── deployment.yaml\n├── game.properties\n└── user-interface.properties\n```\n\n然后就可以通过 `kubectl apply -k ./` 来将整个 kustomize 文件夹转换为 yaml 清单直接部署到 K8s 中。\n(没错,现在 Kustomize 已经成为 kubectl 中的内置功能!可以不用先 `kustomize build` 生成 yaml 文件再 `kubectl apply` 两步走了!)\n\n值得提醒的是,虽然 `kustomization.yaml` 有 `apiVersion` 和 `kind` 字段,长得很像一个资源清单,但其实 K8s 的 API server 并不认识他。 Kustomize 的工作原理其实是先根据 `kustomization.yaml` 生成 K8s 认识的 yaml 资源清单,然后再通过 `kubectl apply` 来部署。\n\n除了可以直接将 ConfigMap 与 Secret 中的文件字段内容用单独的文件定义外, Kustomize 还有其他比如为部署的资源添加统一的名称前缀、添加统一字段等功能。这些大家可以阅读 Kustomize 的[官方文档](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/)来了解。\n\n### 各种工具的优缺点\n\n我们目前已经知道有三种在 K8s 中部署资源的方式: `kubectl apply`、Helm 和 Kustomize 。\n\n其中 `kubectl apply` 的优缺点很明确,优点是最简单直接,缺点是会导致要么 yaml 清单过长,要么需要分多文件多次部署,使集群中产生中间状态。\n\n而 Helm 与 Kustomize 我们上面也分析过,其实都是基于 `kubectl apply` 的。 Helm 是通过 go template 先生成 yaml 文件再 `kubectl apply` ,而 Kustomize 是通过 `kustomization.yaml` 中的定义用自己的一套逻辑生成 yaml 文件,然后再 `kubectl apply` 。\n\nHelm 的优点是 Helm Chart 安装时可以直接使用别人 Helm 仓库中已经上传好的 Chart ,只需要设置参数就可以使用。这也是 Kustomize 的缺点:如果想要使用别人提供的 Kustomization 而只修改其中的一些配置,必须要先把放 `kustomization.yaml` 的整个文件夹下载下来才能做修改。\n\n而 Helm 的缺点也是明显的, Helm 依赖于往资源里注入特殊的 annotation 来管理 Chart 生成的资源,这可能会很难与集群中现有的一些系统(比如 Service Mesh 或是 GitOps 系统等)放一起管理。而 Kustomize 生成的 yaml 清单就是很干净的 K8s 资源,原先的 K8s 资源该是什么表现就是什么表现,与现有的系统兼容一般会比较好。\n\n而另外,由于 Helm 与 Kustomize 都是基于 `kubectl apply` 的,因此他们有共同的缺点,就是不能做 `kubectl apply` 不能做的事情。\n\n什么叫 `kubectl apply` 不能做的事情呢?比如说我们要在 K8s 中部署 Redis 集群。聪明的你可能就想到要用 Stateful Set 、 PVC 、 Headless Service 来一套组合拳。这确实可以部署一个多节点、有状态的 Redis Cluster 。可是如果我们要往 Redis Cluster 里加一个节点呢?\n\n你当然可以把 Stateful Set 中的 `Replicas` 字段加个 1 然后用 `kubectl apply` 部署,可是这实际上只能增加一个一个 Redis 实例 —— 然后什么都没发生。其他节点不认识这个新的节点,访问这个新节点也不能拿到正确的数据。要知道往 Redis Cluster 里加节点,是要先让集群发现这个新节点,然后还要迁移 slot 的! `kubectl apply` 可不会做这些事。\n\n> Well, 其实这些也是可以通过增加 initContainer 、修改镜像增加启动脚本等方式,实现用 `kubectl apply` 部署的。可是,这会让整个 Pod 资源变得很难理解,也不好维护。而且,如果不是因为做不到,谁会想去修改别人的镜像呢?\n\n我们接下来会介绍 K8s 的核心架构,来理解我们之前讲的这些资源到底是怎么工作的。最后会引出一组新的概念: Operator 与自定义资源( Custom Resource Definition ,简称 CRD )。通过 Operator 与 CRD ,我们可以做到 `kubectl apply` 所不能做到的事,包括 Redis Cluster 的扩容。\n\n> DIO: `kubectl apply` 的能力是有限的……\n> 越是部署复杂的应用,就越会发现 `kubectl apply` 的能力是有极限的……除非超越 `kubectl apply` 。\n> \n> JOJO: 你到底想说什么?\n> \n> DIO: 我不用 `kubectl apply` 了! JOJO !\n> (其实还是要用的)\n\n","title":"Kubernetes 入门 (2)","abstract":"我们之前说的都是用于部署 Pod 的资源,我们接下来介绍与创建 Pod 不相关的资源:储存与网络。\n其实我们之前已经接触过储存相关的内容了:在讲 Stateful Set 时我们提过 Stateful Set 创建出来的 Pod 都会有相互独立的储存;而讲 Daemon Set 时我们提到 K8s 推荐只在 Daemon Set 的 Pod 中访问宿主机磁盘。但独立的储存具体指什么?除了访问宿主机磁盘以外还有什么其他的储存?\n在 Docker 中,我们可以把宿主机磁盘上的一个路径作为一个 Volume 来给容器绑定,或者直接使用 Docker Engine 管理的 Volume 来提供持久化存储或是容器间共享文件。在 K8s 里面也沿用了 Volume 这个概念,可以通过 Mount 绑定到容器内的路径,并通过实现 CSI 的各种引擎来提供更多样的存储。","length":875,"created_at":"2022-08-20T21:56:52.000Z","updated_at":"2022-08-20T14:02:18.000Z","tags":["Kubernetes","DevOps","Docker","Cloud Native"],"license":true}},{"slug":"introduction-for-k8s","file":"public/content/articles/2022-08-13-introduction-for-k8s.md","mediaDir":"content/articles/2022-08-13-introduction-for-k8s","path":"/articles/introduction-for-k8s","meta":{"content":"\n# 容器, Docker 与 K8s\n\n我们知道 K8s 利用了容器虚拟化技术。而说到容器虚拟化就要说 Docker 。可是,容器到底是什么? Docker 又为我们做了些什么?我们又为什么要用 K8s ?\n\n### 关于容器虚拟化\n\n> 要把一个不知道打过多少个升级补丁,不知道经历了多少任管理员的系统迁移到其他机器上,毫无疑问会是一场灾难。 —— Chad Fowler 《Trash Your Servers and Burn Your Code》\n\n\"Write once, run anywhere\" 是 Java 曾经的口号。 Java 企图通过 JVM 虚拟机来实现一个可执行程序在多平台间的移植性。但我们现在知道, Java 语言并没能实现他的目标,会在操作系统调用、第三方依赖丢失、两个程序间依赖的冲突等各方面出现问题。\n\n要保证程序拉下来就能跑,最好的方法就是把程序和依赖打包到一起,然后将外部环境隔离起来。容器虚拟化技术就是为了解决这个。\n\n与常说的虚拟机不同, Docker 等各类容器是用隔离名称空间的方式进行资源隔离的。 Linux 系统的内核直接提供了名称空间隔离的能力,是针对进程设计的访问隔离机制,可以进行一些资源封装。\n\n| 名称空间 | 隔离内容 | 内核版本 |\n| :----------- | :---------------------------- | :------- |\n| Mount | 文件系统与路径等 | 2.4.19 |\n| UTS | 主机的Hostname、Domain names | 2.6.19 |\n| IPC | 进程间通信管道 | 2.6.19 |\n| PID | 独立的进程编号空间 | 2.6.24 |\n| Network | 网卡、IP 地址、端口等网络资源 | 2.6.29 |\n| User | 进程独立的用户和用户组 | 3.8 |\n| Cgroup | CPU 时间片,内存分页等 | 4.6 |\n| Time \\<- New! | 进程独立的系统时间 | 5.6 |\n\n值得注目的是, Linux 系统提供了 Cgroup 名称空间隔离的支持。通过隔离 Cgroup ,可以给单独一个进程分配 CPU 占用比率、内存大小、外设 I/O 访问权限等。再配合 IPC 、 PID 等的隔离,可以让被隔离的进程看不到同一实体机中其他进程的信息,就像是独享一整台机器一样。\n\n由于容器虚拟化技术直接利用了宿主机操作系统内核,因此远远要比虚拟机更轻量,也更适合用来给单个程序进行隔离。但也同样由于依赖了宿主机内核,在不同的架构、不同种类的操作系统间容器可能不能移植。\n\n### 关于 Docker\n\n在介绍 K8s 之前,我们要先搞清楚 Docker 是什么。或者说,我们平时说的“ Docker ”是什么?\n\n我们平时说的 Docker ,可能是以下几个东西:\n\n- Docker Engine: 在宿主机上跑的一个进程,专门用来管理各个容器的生命周期、网络连接等,还暴露出一些 API 供外部调用。有时会被称为 Docker Daemon 或是 dockerd 。\n- Docker Client: 命令行中的 `docker` 命令,其实只会跟 Docker Server 通信,不会直接创建销毁一个容器进程。\n- Docker Container: 宿主机上运行的一组被资源隔离的进程,在容器中看来像是独占了一台虚拟的机器,不需要考虑外部依赖。\n- Docker Image: 是一个打包好的文件系统,可以从一个 Image 运行出复数个 Container 。 Image 内部包含了程序运行所需的所有文件、库依赖,以及运行时的环境变量等。\n- Docker 容器运行时: 是 Docker Engine 中专门管理容器状态、生命周期等的那个组件,原来名为 libcontainer 。[《开放容器交互标准》](https://en.wikipedia.org/wiki/Open_Container_Initiative)制定后, Docker 公司将此部分重构为 [runC 项目](https://github.com/opencontainers/runc),交给 Linux 基金会管理。而 Docker Engine 中与运行时进行交互的部分则抽象出来成为 [containerd 项目](https://containerd.io/),捐献给了 CNCF 。\n\n我们平时在 linux 机上运行 `yum install docker` 之类的命令,安装的其实是 Docker Engine + Docker Client 。(而在 Windows 或 MacOS 上安装的 Docker Desktop 其实是一个定制过的 linux 虚拟机。)下面说的 Docker 的功能其实都是指 Docker Engine 的功能。\n\n而 Docker 提供给我们的功能,除了最基础的运行和销毁容器外,还包括了一些容器网络编排、重启策略、文件路径映射、端口映射等功能。\n\n而我认为 Docker 最大的贡献,还是容器的镜像与镜像仓库。有了镜像与镜像仓库,人们就可以把自己的程序与执行环境直接打包成镜像发布,也可以直接拿打包好的镜像来运行容器进行部署,而不需要额外下载或是安装一些东西,也不需要担心程序会与已经跑起来的其他程序冲突。\n\n### 为什么要用 K8s ?\n\n其实 Docker 有一个很强大的工具叫 docker-compose ,可以通过一个 manifest 对多个容器组成的网络进行编排。那为什么我们还需要 K8s 呢?换句话说,有什么事是 Docker 不能做的?而 K8s 设计出来的目标是为了解决什么问题?\n\n首先, Docker 做不到以下的功能:\n\n1. **Docker 不能做跨多主机的容器编排。** docker-compose 再方便,他也只能编排单台主机上的容器。对跨主机的集群编排无能为力。(实际上,用了 Docker-Swarm 后是可以多主机编排的,但一来 Docker-Swarm 出现的比 K8s 晚,而来 Docker-Swarm 功能不如 K8s ,因此用的人很少,我们下面就默认 Docker-Swarm 不存在了。)\n2. **Docker 提供的容器部署管理功能不够丰富。** Docker 有一些简单的容器重启策略,但也只是简单的失败后重启之类的,没有完整的应用状态检查等功能。同时,版本升级、缩扩容等策略选择的余地也不多。\n3. **Docker 缺乏高级网络功能。** 要让 Docker 的容器间进行网络通信,也只能是说把容器放到同一个网络下,然后再通过各自的 Hostname 来找到对方。但实际上,我们更会想要一些负载均衡、自定义域名、选择某些容器端口不暴露之类的功能。\n\nand more...\n\n总的来说, Docker 更关注单台主机上容器怎么跑,而对部署管理的功能则支持不多。而最大的痛点,就是 Docker 对多主机的集群部署支持的实再很差。然而,为了实现多区可用、负载均衡等功能,多主机集群的容器编排又是必不可少的。\n\nK8s 的出现,主要就是为了解决多主机集群上的容器编排问题。\n\n1. **K8s 可以进行多主机调度。** 用户只需要描述自己需要运行怎样的应用, K8s 就可以自己选择一个合适的节点进行部署,用户不需要关心自己的应用部署到哪个节点上。\n2. **K8s 中一切皆资源。** K8s 有完善的抽象资源机制,用户几乎不需要知道磁盘、网络等任何硬件信息,只需要对着统一的抽象资源进行操作。\n3. **K8s 能保证较强的可用性。** 除了能跨多主机调度实现多区可用外, K8s 还提供了很完善的缩扩容机制、健康检查机制以及自动恢复机制。\n\n可以说, K8s 是容器编排工具的主流选择。\n\n### K8s 与 Docker 的关系\n\nK8s 与 Docker 关系很复杂,是一个逐渐变化的过程。\n\n一开始 K8s 是完全依赖于 Docker Engine 进行容器启动与销毁的。后来[容器运行时接口(CRI)](https://kubernetes.io/blog/2016/12/container-runtime-interface-cri-in-kubernetes/)、 [CRI-O 标准](https://github.com/cri-o/cri-o)、开放容器交互标准(OCI)等标准逐渐建立,可替代 Docker Engine 的工具越来越多, K8s 中已经完全可以不使用 Docker Engine 了。\n\n[《凤凰架构》](http://icyfenix.cn/)一书中有下面这样一张图来描述 K8s 与 Docker Engine 的关系:\n\n![K8s 与 Docker Engine 的关系](http://icyfenix.cn/assets/img/kubernetes.495f9eae.png)\n\n《凤凰架构》书中[这一章节](http://icyfenix.cn/immutable-infrastructure/container/history.html#%E5%B0%81%E8%A3%85%E9%9B%86%E7%BE%A4%EF%BC%9Akubernetes)详细介绍了 K8s 与 Docker 的历史,我这里就不再赘述。\n\n# 部署一个 Pod\n\n上面说了一堆概念,我们接下来实际上会怎样应用 K8s 。\n\n### Pod 示例\n\n> Pod 是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元。\n> Pod 是一组容器;Pod 中的内容总是一同调度,在共享的上下文中运行。 Pod 中包含一个或多个应用容器,这些容器相对紧密地耦合在一起。在非云环境中,在相同的物理机或虚拟机上运行的应用类似于在同一逻辑主机上运行的云应用。\n> —— Kubernetes 官方文档\n\nPod 是 K8s 的最小部署单位。\n\n因为 K8s 将硬件资源都抽象化了,用户不需要知道自己的应用部署到哪台机上。但是有些场景下两个主进程之间又必须相互协作才能完成任务,如果两个进程不确定会不会部署到同一个节点上会变得很麻烦。因此才需要 Pod 这种资源。\n\n下面是一个 Nginx Pod 的示例(这是 K8s manifest 文件,可以用 `kubectl apply -f <filepath>` 进行部署):\n\n```yaml\nmetadata:\n name: simple-webapp\nspec:\n containers:\n - name: main-application\n image: nginx\n volumeMounts:\n - name: shared-logs\n mountPath: /var/log/nginx\n - name: sidecar-container\n image: busybox\n command: [\"sh\",\"-c\",\"while true; do cat /var/log/nginx/access.log; sleep 30; done\"]\n volumeMounts:\n - name: shared-logs\n mountPath: /var/log/nginx\n volumes:\n - name: shared-logs\n emptyDir: {}\n```\n\n可以看到, Pod 中可以包含多个容器,这组容器总是以一定的逻辑一起部署,且总是部署在同一个节点。对 K8s 操作时,不能说只部署 Pod 中一个特定的容器,也不能说把 Pod 中一个容器部署在这个节点,另一个容器部署在另一个节点上。\n\n在上面这个例子中,我们看到 Pod 中除了 Nginx 容器以外还有一个 Sidecar 容器负责将 Nginx 的 access.log 日志输出到控制台。两个容器可以通过 mount 同一个路径来实现文件共享。这种场景下,单独跑一个 Sidecar 容器没有意义,而我们也不会希望两个容器部署在不同的节点上。 **两个容器同生共死** ,这样的模式被称为 **Sidecar 模式** 。 Jaeger Agent ,或是 Service Mesh 中常见的 Envoy Sidecar 都可以通过这种模式部署,这样业务容器中就可以不考虑 tracing 或是流量控制相关的问题。\n\n此外,由于同一个 Pod 中的容器默认共享了相同的 network 和 UTS 名称空间,不管是在 Pod 的内部还是外部来看,他们一定程度上就像是真的部署在同一主机上一样,有相同的 Hostname 与 ip 地址,在一个容器中也可以通过 localhost 来访问零一个容器的端口。\n\n另外 Pod 中可以定义若干个 initContainer ,这些容器会比 `spec.containers` 中的容器先运行,并且是顺序运行。下面是通过安装 bitnami 的 Kafka Helm Chart 得到的一个 Kafka Broker Pod (有所简化):\n\n```yaml\napiVersion: v1\nkind: Pod\nmetadata:\n name: kafka-0\n namespace: kafka\nspec:\n containers:\n - name: kafka\n image: docker.io/bitnami/kafka:3.1.0-debian-10-r52\n command:\n - /scripts/setup.sh\n volumeMounts:\n - name: scripts\n mountPath: /scripts/setup.sh\n subPath: setup.sh\n - name: shared\n mountPath: /shared\n initContainers:\n - name: auto-discovery\n image: docker.io/bitnami/kubectl:1.23.5-debian-10-r1\n command:\n - /scripts/auto-discovery.sh\n volumeMounts:\n - name: shared\n mountPath: /shared\n - name: scripts\n mountPath: /scripts/auto-discovery.sh\n subPath: auto-discovery.sh\n volumes:\n - name: scripts\n configMap:\n defaultMode: 493\n name: kafka-scripts\n - name: shared\n emptyDir: {}\n```\n\n可以看到,在 `kafka` pod 启动前会先启动一个名为 `auto-discovery` 的 initContainer ,负责获得集群信息等准备工作。准备工作完成后,会将信息写入 `/shared` 目录下,然后再启动 `kafka` 容器 Mount 同一目录,就可以获取准备好的信息。\n\n**这样运行容器进行 Pod 初始化就叫 initContainer 模式** 。每个 initContainer 会运行到成功退出为止,如果有一个 initContainer 启动失败,则整个 Pod 启动失败,触发 K8s 的 Pod 重启策略。\n\n\n# 部署更多 Pod\n\n### Replica Set\n\n可是上面说了这么多,还只是单个 Pod 的部署,但我们希望能做多副本部署。\n\n其实,只要把 Pod 的 manifest 改一下 `metadata.name` 再部署一次,就能得到一模一样的两个 Pod ,就是一个简单的多副本部署了。(必须改 `metadata.name` ,不然 K8s 会以为你是想修改同一个 Pod )\n\n可是这样做会有很多问题:\n\n- 要复制一下还要改名字多麻烦啊,我想用同一份模板,只定义一下副本数就能得到对应数量的 Pod 。\n- 缩容扩容还要对着 Pod 操作很危险,我想直接修改副本数就能缩容扩容。\n- 如果其中一些 Pod 挂掉了不能重启,现在是什么都不会做。我希望能自动建一些新的 Pod 顶上,来保证副本数不变。\n\n为了实现这些需求,就出现了 Replica Set 这种资源。下面是实际应用中一个 Replica Set 的例子:\n\n```yaml\napiVersion: apps/v1\nkind: ReplicaSet\nmetadata:\n labels:\n app: gateway\n name: gateway-9dc546658\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: gateway\n template:\n metadata:\n labels:\n app: gateway\n name: gateway\n spec:\n containers:\n name: gateway\n image: xxxxxxxx.amazonaws.com/gateway:xxxxxxx\n ports:\n - containerPort: 50051\n protocol: TCP\n readinessProbe:\n initialDelaySeconds: 5\n tcpSocket:\n port: 50051\n startupProbe:\n failureThreshold: 60\n tcpSocket:\n port: 50051\n affinity:\n podAntiAffinity:\n preferredDuringSchedulingIgnoredDuringExecution:\n - podAffinityTerm:\n labelSelector:\n matchLabels:\n app: gateway\n topologyKey: topology.kubernetes.io/zone\n weight: 80\n```\n\n我们可以看到, `spec.template` 中就是我们要的 Pod 的模板,在 metadata 里带上了 `app:gateway` 标签。而在 `spec.replicas` 中定义了我们需要的 Pod 数量, `spec.selector` 中描述了我们要对带 `app:gateway` 标签的 Pod 进行控制。把这份 manifest 部署后,我们就会得到除名字以外几乎一摸一样的两个 Pod :\n\n```yaml\napiVersion: v1\nkind: Pod\nmetadata:\n generateName: gateway-9dc546658-\n labels:\n app: gateway\n pod-template-hash: 9dc546658\n name: gateway-9dc546658-6c9qs\n ownerReferences:\n - apiVersion: apps/v1\n blockOwnerDeletion: true\n controller: true\n kind: ReplicaSet\n name: gateway-9dc546658\n uid: 6633f89c-377c-4c90-bd08-3be5bc7b21bd\n resourceVersion: \"49793842\"\n uid: f927db88-a39a-4623-852d-4f150a6d853b\nspec:\n containers:\n name: gateway\n image: xxxxxxxx.amazonaws.com/gateway:xxxxxxx\n ports:\n # 后续省略\n\n---\napiVersion: v1\nkind: Pod\nmetadata:\n annotations:\n kubernetes.io/psp: eks.privileged\n creationTimestamp: \"2022-08-09T08:51:25Z\"\n generateName: gateway-9dc546658-\n labels:\n app: gateway\n pod-template-hash: 9dc546658\n name: gateway-9dc546658-8trcs\n ownerReferences:\n - apiVersion: apps/v1\n blockOwnerDeletion: true\n controller: true\n kind: ReplicaSet\n name: gateway-9dc546658\n uid: 6633f89c-377c-4c90-bd08-3be5bc7b21bd\n resourceVersion: \"49793745\"\n uid: 0918e3ed-2965-4237-8828-421a7831c9ed\nspec:\n containers:\n image: xxxxxxxx.amazonaws.com/gateway:xxxxxxx\n name: gateway\n ports:\n # 后续省略\n```\n\n可以看到,创建出来的 Pod 自动生成了两个后缀( `6c9qs` 与 `8trcs` ),带上了 Replica Set 的信息(在 `metadata.ownerReferences` ),其他部分基本一模一样。如果其中一个 Pod 挂掉了, K8s 会帮我们从模板中重新创建一个 Pod 。而且由于我们在 Pod 模板定义了 affinity , K8s 还会按照我们的要求自动筛选合适的节点。例如在上面 Replica Set 的例子中,创建出来的 Pod 就会尽量部署在不同的节点上。\n\n> **K8s 中对 Pod 的生存状态检查机制**\n> \n> 除了线程直接错误退出以外,还有出现死锁等等各种可能性使得容器中的应用不能正常工作。这些情况下虽然是不健康状态,但容器却不一定会挂掉。因此 K8s 提供了一些探针检查的机制来判断 Pod 是否健康。\n> K8s 主要提供了三种探针:\n> 1. **存活探针( liveness probe )** : Pod 运行时 K8s 会循环执行 liveness probe 检查容器是否健康。如果检查失败, K8s 会认为这个容器不健康,就会尝试重启容器。\n> 2. **就绪探针( readiness probe )** : 程序可能会有一段时间不能提供服务(比如正在加载数据等)。这时可能既不想杀死应用,也不想给它发送请求,这时就需要 readiness probe 。如果 readiness probe 检查失败, K8s 就会将这个 Pod 从 Service 上摘下来,直到 readiness probe 成功重新加入 Service 。\n> 3. **启动探针( startup probe )** : 有些程序会有非常长的启动时间,会有较长时间不能提供服务。这时如果 liveness probe 失败了导致重启毫无必要,此时就需要 startup probe 。 startup probe 只会在容器启动时检查直到第一次成功。直到 startup probe 成功为止, liveness probe 与 readiness probe 都不会开始执行检查。\n> \n> 而检测方式主要有:\n> 1. httpGet: 对指定的端口路径执行 HTTP GET 请求,如果返回 2xx 或 3xx 就是成功。\n> 2. tcpSocket: 尝试与容器的端口建立连接,如果不能成功建立连接就是失败。\n> 3. exec: 在容器内执行一段命令,如果退出时状态码不为 0 就是失败。\n> 4. grpc (New!): K8s 1.24 新出的检查方式,直接用 [GRPC Health Checking Protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) 对 GRPC Server 进行检查。\n\n此外, Replica Set 还提供了简易的缩容扩容功能。 kubectl 中提供了 scale 命令:\n\n```bash\nkubectl scale replicaset gateway --replicas=10\n```\n\n执行上述命令,就可以将名为 gateway 的 Replica Set 对应的副本数扩容到 10 份。当然,你也可以直接修改 Replica Set 的 `spec.replicas` 字段来实现缩容扩容。\n\n然而, Replica Set 的功能还是有限的。实际上, Replica Set 只关心跟它的 selector 匹配的 Pod 的数量。而至于匹配的 Pod 是否真的是跟 template 字段中描述的一样, Replica Set 就不关心了。因此如果单用 Replica Set ,更新 Pod 就会变得究极麻烦。\n\n### Deployment\n\n为了解决 Pod 的更新问题,我们需要有 Deployment 这种资源。实际上, Replica Set 的主要用途是提供给 Deployment 作为控制 Pod 数量,以及创建、删除 Pod 的一种机制。我们一般不会直接使用 Replica Set 。\n\n下面是实际应用中一个 Deployment 的例子:\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n labels:\n app: gateway\n name: gateway\nspec:\n replicas: 2\n revisionHistoryLimit: 10\n selector:\n matchLabels:\n app: gateway\n template:\n metadata:\n labels:\n app: gateway\n name: gateway\n spec:\n containers:\n name: gateway\n image: xxxxxxxx.amazonaws.com/gateway:xxxxxxx\n ports:\n # 下略\n```\n\n可以看到 Deployment 的 manifest 跟 Replica Set 很像。但实际上, Deployment 不会直接创建 Pod ,而是创建出一个 Replica Set ,再由 Replica Set 来创建 Pod :\n\n\n```mermaid\nflowchart TB\n\nDeployment1[Deployment]\nReplicaSet11[Replica Set]\nPod11[Pod1]\nPod12[Pod2]\nDeployment1 --> ReplicaSet11\nReplicaSet11 --> Pod11\nReplicaSet11 --> Pod12\n```\n\n比如在上面的例子中,名为 gateway 的 Deployment 创建后,就会有如下 ReplicaSet 和 Pod :\n\n```sh\n# Replica Set:\n$ kubectl get rc -l app=gateway\nNAME DESIRED CURRENT READY AGE\ngateway-9dc546658 2 2 2 5d3h\n\n# Pod:\n$ kubectl get po -l app=gateway\nNAME READY STATUS RESTARTS AGE\ngateway-9dc546658-6c9qs 1/1 Running 0 5d3h\ngateway-9dc546658-8trcs 1/1 Running 0 5d3h\n```\n\n可以看到,gateway Deployment 创建了一个 Replica Set ,然后随机给了它一个 `9dc546658` 后缀。然后 gateway-9dc546658 这个 Replica Set 又根据 template 中创建了两个 Pod ,再在自己名字的基础上加上两个后缀 `6c9qs` 与 `8trcs` 。\n\n接下来就是 Deployment 的重点了: Replica Set 只会根据 template 创建出 Pod ,而不管匹配的 Pod 到底是不是跟 template 中描述的一样。而 **Deployment 则会专门关注 template 的内容变更。**\n\n假如我们现在更新了 Deployment 的 template 中的内容提交给 K8s , Deployment 就会感知到 template 被修改了, Pod 需要更新。\n感知到更新之后, Deployment 就会创建一个新的 Replica Set 。然后逐渐将旧的 Replica Set 缩容到 0 ,并同时将新的 Replica Set 扩容到目标值。最后,所有旧版本的 Pod 将会被更新成新版本的 Pod 。如下图所示:\n\n```mermaid\nflowchart TB\n\nsubgraph A\ndirection TB\nDeployment1[Deployment]\nReplicaSet11[Replica Set]\nReplicaSet12[New Replica Set]\nPod11[Pod1]\nPod12[Pod2]\nDeployment1 --> ReplicaSet11\nDeployment1 --> ReplicaSet12\nReplicaSet11 --> Pod11\nReplicaSet11 --> Pod12\nend\n\nsubgraph B\ndirection TB\nDeployment2[Deployment]\nReplicaSet21[Replica Set]\nReplicaSet22[New Replica Set]\nPod21[New Pod1]\nPod22[Pod2]\nDeployment2 --> ReplicaSet21\nDeployment2 --> ReplicaSet22\nReplicaSet21 --> Pod22\nReplicaSet22 --> Pod21\nend\n\nsubgraph C\ndirection TB\nDeployment3[Deployment]\nReplicaSet31[Replica Set]\nReplicaSet32[New Replica Set]\nPod31[New Pod1]\nPod32[New Pod2]\nDeployment3 --> ReplicaSet31\nDeployment3 --> ReplicaSet32\nReplicaSet32 --> Pod31\nReplicaSet32 --> Pod32\nend\n\nA --> B --> C\n```\n\n整个过程完成后, Deployment 还不会将旧的 Replica Set 删除掉。我们注意到 Deployment 的声明中有这么一个字段: `revisionHistoryLimit: 10` ,表示 Deployment 会保留历史中 最近的 10 个 Replica Set ,这样在必要的时候可以立刻将 Deployment 回滚到上个版本。而超出 10 个的 Replica Set 才会被从 K8s 中删除。\n\n```sh\n# 实际中被 scale 到 0 但还没被删除的 Replica Set\n$ kubectl get rs -l app=gateway\nNAME DESIRED CURRENT READY AGE\ngateway-5c4cdf957d 0 0 0 5d4h\ngateway-5c56f6d487 0 0 0 17d\ngateway-65857cfc78 0 0 0 10d\ngateway-6bddbdd85f 0 0 0 16d\ngateway-6cc9bb5b4c 0 0 0 13d\ngateway-6f4664bc65 0 0 0 17d\ngateway-7bd667cb79 0 0 0 9d\ngateway-7d658d57f5 0 0 0 13d\ngateway-84df97d4c8 0 0 0 6d4h\ngateway-9998f4689 0 0 0 13d\ngateway-9dc546658 2 2 2 5d4h\n```\n\n### Stateful Set\n\nDeployment 中默认了我们不关心自己访问的是哪个 Pod ,因为各个 Pod 的功能是一样的,访问哪个没有差别。\n\n实际上这也符合大多数情况:试想一个 HTTP Server ,如果其所有数据都存放到同一个的数据库中,那这个 HTTP Server 不管部署在哪台主机、不管有多少个实例、不管你访问的是哪个实例,都察觉不出有什么差别。而有了这种默认,我们就能更放心地对 Pod 进行负载均衡、缩扩容等操作。\n\n但实际上我们总会遇到需要保存自己状态的 Pod 。比如我们在 K8s 里部署一个 Kafka 集群,每个 Kafka broker 都需要保存自己的分区数据,而且还要往 Zookeeper 里写入自己的名字来实现选举等功能。如果简单地用 Deployment 来部署, broker 之间可能就会分不清到底哪块是自己的分区,而且由 Deployment 生成出来的 Pod 名字是随机的,升级后 Pod 的名字会变,导致 Kafka 升级后名字与 Zookeeper 里的名字不一致,被以为是一个新的 broker 。\n\nStateful Set 就是为了解决有状态应用的部署而出现的。下面是 用 bitnami 的 Kafka Helm Chart 部署的一个 Kafka Stateful Set 的例子:\n\n```yaml\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n labels:\n app.kubernetes.io/name: kafka\n name: kafka\nspec:\n replicas: 3\n selector:\n matchLabels:\n app.kubernetes.io/name: kafka\n serviceName: kafka-headless\n template:\n metadata:\n labels:\n app.kubernetes.io/name: kafka\n spec:\n containers:\n - name: kafka\n image: docker.io/bitnami/kafka:3.1.0-debian-10-r52\n command:\n - /scripts/setup.sh\n ports:\n - containerPort: 9092\n name: kafka-client\n protocol: TCP\n volumeMounts:\n - mountPath: /bitnami/kafka\n name: data\n volumeClaimTemplates:\n - apiVersion: v1\n kind: PersistentVolumeClaim\n metadata:\n name: data\n spec:\n resources:\n requests:\n storage: 10Gi\n storageClassName: gp2\n```\n\n可以看到其实 Stateful Set 类似 Deployment ,也可以通过 replicas 字段定义实例数,如果更新 template 部分, Stateful Set 也会以一定的策略对 Pod 进行更新。\n\n而其创建出来的 Pod 如下所示:\n```sh\n$ kubectl get po -l app.kubernetes.io/name=kafka\nNAME READY STATUS RESTARTS AGE\nkafka-0 1/1 Running 1 26d\nkafka-1 1/1 Running 3 26d\nkafka-2 1/1 Running 3 26d\n```\n\n与 Replica Set 创建出来的 Pod 相比名字上会有很大差别。 Stateful Set 创建出来的 Pod 会固定的以 `-0` 、 `-1` 、 `-2` 结尾而不是随机生成:\n\n```mermaid\nflowchart TB\nrs[Replica Set A]\nrs --> A-qwert\nrs --> A-asdfg\nrs --> A-zxcvb\n\nss[Stateful Set A]\nss --> A-0\nss --> A-1\nss --> A-2\n```\n\n这样一来,更新时将 Pod 更换之后,新的 Pod 仍能够跟旧的 Pod 保持相同的名字。此外,与 Deployment 相比, Stateful Set 更新后同名的 Pod 仍能保持原来的 IP ,拿到同一个持久化卷,而且不同的 Pod 还能通过独立的 DNS 记录相互区分。这些内容后面还会详细介绍。\n\n> **宠物与牛( Cattle vs Pets )的比喻**\n> \n> Deployment 更倾向于将 Pod 看作是牛:我们不会去关心每一个 Pod 个体,如果有一个 Pod 出现了问题,我们只需要把他杀掉并替换成新的 Pod 就好。\n> \n> 但 Stateful Set 更倾向于将 Pod 看作是宠物:弄来一直完全一模一样的宠物并不是容易的事,我们对待这些宠物必须小心翼翼。我们要给他们各自一个专属的名字,替换掉一只宠物时,必须要保证它的花色、名字、行为举止都与之前那只宠物一模一样。\n\n### Daemon Set\n\n不管是 Deployment 还是 Stateful Set ,一般都不会在意自己的 Pod 部署到哪个节点。而假如你不在意自己 Pod 的数量,但需要保证每个节点上都运行一个 Pod 时,就需要 Daemon Set 了。\n\n需要保证每个节点上有且只有一个 Pod 在运行这种情况,经常会在基础结构相关的操作中出现。比如我需要在集群中部署 fluentd 采集 log ,一般来说需要在 Pod 里直接挂载节点磁盘上的文件路径。这种时候如果有一个节点上没有运行 Pod ,那个节点的 log 就采集不到;另一方面,一个节点上运行多个 Pod 毫无意义,而且可能还会导致 log 重复等冲突。\n\n这种需求下简单地使用 Replica Set 或是 Stateful Set 都是不能达到要求的,这两种资源都只能通过亲和性达到“尽量不部署在同一个节点”,做不到绝对。而且当节点数有变更时还需要手动更改设置。\n\n下面是一个用 fluent-bit helm chart 部署的 fluent-bit Daemon Set 的例子:\n\n```yaml\napiVersion: apps/v1\nkind: DaemonSet\nmetadata:\n labels:\n app.kubernetes.io/instance: fluent-bit\n app.kubernetes.io/name: fluent-bit\n name: fluent-bit\n namespace: fluent-bit\nspec:\n selector:\n matchLabels:\n app.kubernetes.io/instance: fluent-bit\n app.kubernetes.io/name: fluent-bit\n template:\n metadata:\n labels:\n app.kubernetes.io/instance: fluent-bit\n app.kubernetes.io/name: fluent-bit\n spec:\n containers:\n - image: cr.fluentbit.io/fluent/fluent-bit:1.9.5\n volumeMounts:\n - name: varlibdockercontainers\n mountPath: /var/lib/docker/containers\n readOnly: true\n - name: etcmachineid\n mountPath: /etc/machine-id\n readOnly: true\n volumes:\n - name: varlibdockercontainers\n hostPath:\n path: /var/lib/docker/containers\n type: \"\"\n - name: etcmachineid\n hostPath:\n path: /etc/machine-id\n type: File\n```\n\nSelector 之类的都是一样的了,而 Daemon Set 不能指定 replicas 。另外可以看到一个比较刺激的地方: Volume 里使用了 `hostPath` 这种 Volume ,在 Pod 里直接指定了宿主机磁盘上的路径。\n\nK8s 认为经过抽象后, Pod 不应该去关心自己在哪台宿主机上,一般来说是不推荐在 Pod 里直接访问宿主机路径的(不过也没有强制禁止)。不过 Daemon Set 是个特例,由于 Daemon Set 生成的 Pod 与节点强相关, K8s 十分推荐在且仅在 Daemon Set 的 Pod 中访问宿主机路径。\n\n### Job 与 CronJob\n\nReplica Set , Stateful Set , Daemon Set 的 Pod 中运行的一般是持续运行的程序,因此这些 Pod 运行终止后会有相应的机制重启这些 Pod 。而 Job 与 Cron Job 这两种资源则专门负责调度不会持续运行的程序。\n\n下面是 《Kubernetes in Action》 书中的一个例子:\n\n```yaml\napiVersion: batch/v1\nkind: Job\nmetadata:\n name: pi\nspec:\n completions: 5\n parallelism: 2\n template:\n spec:\n containers:\n - name: pi\n image: perl:5.34.0\n command: [\"perl\", \"-Mbignum=bpi\", \"-wle\", \"print bpi(2000)\"]\n restartPolicy: Never\n```\n\n可以看到,这个 Job 描述了一个会输出 PI 小数点后 2000 位的 Pod 模板。这个 Job 部署后,一共会以这个模板跑完 5 个 Pod ,其中最多并行跑 2 个,并在其中一个成功终止后再跑剩下的 Pod 。可以通过调整 `completions` 与 `parallelism` 字段调整并行与穿行数量。\n\n顺带一提,在 Job 定义中一般不会出现 selector ,但其实 Job 有 selector 字段,一般会由 K8s 为每个 Job 生成一个 uuid 作为 selector 。\n\n另外,可以通过部署 CronJob 这种资源来定时执行 Job 。下面是 《Kubernetes in Action》 书中关于 CronJob 的例子:\n\n```yaml\napiVersion: batch/v1beta1\nkind: CronJob\nmetadata:\n name: pi\nspec:\n schedule: \"0 0 * * *\"\n jobTemplate:\n spec:\n template:\n spec:\n containers:\n - name: pi\n image: perl:5.34.0\n command: [\"perl\", \"-Mbignum=bpi\", \"-wle\", \"print bpi(2000)\"]\n restartPolicy: Never\n```\n\n这个例子中, CronJob 会在每天的 0 点创建一个只运行一个 Pod 的 Job 。 CronJob 不会直接创建 Pod ,而是创建一个 Job ,再由 Job 创建 Pod (就像 Deployment 与 Replica Set 的关系)。另外, CronJob 创建的 Job 会限制 `completions` 与 `parallelism` 都只能等于 1 。\n\n> 关于资源的名称空间\n> \n> 在 K8s 中,各资源都是不能重名的。不能部署两个都叫 `gateway` 的 Pod ,资源之间有可能因为名字冲突而导致部署不成功。(部署一个叫 `gateway` 的 Pod 和一个叫 `gateway` 的 Deployment 倒是可以,因为 `gateway` 不是他们两个的全名,他们的全名分别叫 `pod/gateway` 及 `deployment/gateway` 。)\n> 另外我们已经知道 Deployment 等资源一般会通过标签等来管理自己创建的资源,那两份不相关的应用完全有可能会撞标签,这时候部署逻辑就有可能会出问题。\n> \n> K8s 中提供了名称空间这种资源,用于进行资源隔离。K8s 中大部分资源都从属于一个且仅从属于一个名称空间, Deployment 等资源一般只能控制在同一名称空间下的资源,而不会影响其他名称空间。\n> \n> 另外,也有一些资源是名称空间无关的,比如节点 `Node` 。\n\n\n","title":"Kubernetes 入门 (1)","abstract":"我们知道 K8s 利用了容器虚拟化技术。而说到容器虚拟化就要说 Docker 。可是,容器到底是什么? Docker 又为我们做了些什么?我们又为什么要用 K8s ?\n> 要把一个不知道打过多少个升级补丁,不知道经历了多少任管理员的系统迁移到其他机器上,毫无疑问会是一场灾难。 —— Chad Fowler 《Trash Your Servers and Burn Your Code》\n\"Write once, run anywhere\" 是 Java 曾经的口号。 Java 企图通过 JVM 虚拟机来实现一个可执行程序在多平台间的移植性。但我们现在知道, Java 语言并没能实现他的目标,会在操作系统调用、第三方依赖丢失、两个程序间依赖的冲突等各方面出现问题。","length":644,"created_at":"2022-08-13T17:45:31.000Z","updated_at":"2022-08-20T14:02:18.000Z","tags":["Kubernetes","DevOps","Docker","Cloud Native"],"license":true}},{"slug":"why-homogeneous","file":"public/content/articles/2022-07-31-why-homogeneous.md","mediaDir":"content/articles/2022-07-31-why-homogeneous","path":"/articles/why-homogeneous","meta":{"content":"## 首先,什么是线性变换?\n\n简化了一万倍来说,线性变换主要是在描述符合这两种性质的变换:一是要可加,二是要能数乘。\n也就是说,对于空间中所有向量 $$\\vec{v_1}, \\vec{v_2}$$ ,以及任意数量 $$k_1, k_2$$ ,如果有:\n$$\nA(k_1 \\vec{v_1} + k_2 \\vec{v_2}) = k_1 A(\\vec{v_1}) + k_2 A(\\vec{v_2})\n$$\n符合这种规律的 A 就叫线性变换。而一次矩阵乘法正好可以代表一次线性变换。\n\n为什么叫“线性”变换呢?感性地来说,因为它很“线”。\n\n我们可以直观地从下面这张图看出原因:\n\n![[OnOneLineWillStillOneLine_ManimCE_v0.16.0.post0.gif]]\n\n我们可以看到,在同一直线上的点,经过同一线性变换后还在同一直线上。所以它很“线”。\n\n另一方面,我们可以找一找最简单的线性变换:\n\n考虑函数:\n$$\nf(x) = k_0 x\n$$\n我们都知道这是一条过原点的直线。\n\n而从另一方面想,其实这个函数对于任意一维向量(实数) $$x_1, x_2$$ , 与任意数量(实数) $$k_1, k_2$$ , 都有:\n$$\nf(k_1 x_1 + k_2 x_2) = k_1 k_0 x_1 + k_2 k_0 x_2 = k_1 f(x_1) + k_2 f(x_2) \\\\\n$$\n\n即, xy 平面上过原点的直线(正比例函数)本身就是一种从 x 轴到 y 轴的线性变换。\n\n关于线性变换, [3blue1Brown](https://www.3blue1brown.com/topics/linear-algebra) 上有更详细更感性的介绍,大家感兴趣可以前往观看。\n\n## 为什么普通的线性变换不能表示点平移?\n\n从上面的感性介绍来看,我们知道线性变换的性质就是可加和数乘,写成等式就是:\n\n$$\nA(k_1 \\vec{v_1} + k_2 \\vec{v_2}) = k_1 A(\\vec{v_1}) + k_2 A(\\vec{v_2})\n$$\n\n而当两个向量都为零向量时,等式就会简化成:\n\n$$\nA(\\vec{0}) = A(\\vec{0}) + A(\\vec{0})\n$$\n\n解一下方程,就可以知道,对任意线性变换 A,都会有:\n\n$$\nA(\\vec{0}) = 0\n$$\n\n也就是说,不管是哪个线性变换 A ,原点经过变换后都必须只能是在原点不变。如果变换后原点的位置变了,那它就一定不是线性变换。\n\n我们从下图也可以看出,对于切变 $$\\begin{pmatrix}1 & 1 \\\\ 0 & 1\\end{pmatrix}$$ 、伸缩 $$ \\begin{pmatrix}2 & 0 \\\\ 0 & \\frac{1}{2}\\end{pmatrix} $$、旋转 $$ \\begin{pmatrix}\n \\frac{\\sqrt{3}}{2} & -\\frac{1}{2} \\\\ \\frac{1}{2} & \\frac{\\sqrt{3}}{2}\n\\end{pmatrix} $$ 这些经典的线性变换,变换后原点都不会变。\n\n![[SliceScaleRotateForOrigin_ManimCE_v0.16.0.post0.gif]]\n\n但是平移这种变换不一样。原点经过平移后,是一定不会还留在原点的。因此平移不是一种线性变换,自然也不能用矩阵来表示。\n\n## 为什么基于齐次坐标下的线性变换就可以表示平移?\n\n我们先来看一下齐次坐标做了些什么。\n\n在上面传统的线性变换中,我们不会考虑向量与点的区别。一个二维坐标 $$(x, y)$$ 既能代表那个坐标上的点,也能代表从原点到 $$(x, y)$$ 的向量。这时,点与向量是一一对应的。\n\n但如果要考虑平移,点与向量就不能再一一对应了,因为对向量平移没有意义(不考虑物理中力矩的场景)。\n所以在齐次坐标下,我们需要区分这个坐标代表的是点还是向量。\n\n以二维空间为例,齐次坐标就是在二维空间上加了第三个维度 w 轴,二维空间里的点在 w 轴上的值为 1 ,而二维向量在 w 轴上的值对应为 0 :\n\n$$\n\\begin{align}\n P &= \\begin{pmatrix}x & y & 1\\end{pmatrix} \\\\\n \\vec{v} &= \\begin{pmatrix}v_x & v_y & 0\\end{pmatrix}\n\\end{align}\n$$\n\n从字面上看可能还是不太明显,让我们试着把二维空间齐次坐标强行转化为三维空间坐标看看:\n\n![[HomogeneousTransform_ManimCE_v0.16.0.post0.gif]]\n\n我们发现,原来二维空间中的点,被投射到三维空间中 w = 1 的平面上了!\n\n这样一来,二维空间齐次坐标下的平移矩阵也很好理解了:\n\n$$\n将平面沿向量 (x, y) 平移:\n\\begin{pmatrix}\n 1 & 0 & x \\\\\n 0 & 1 & y \\\\\n 0 & 0 & 1\n\\end{pmatrix}\n$$\n\n这不就是三维空间中在 w 轴上做切变时的变换矩阵嘛!\n\n我们可以重点关注一下 $$\\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix}$$ 这个向量。\n从齐次坐标的定义来看,这个向量对应着二维空间中的原点 $$P_{Origin} = \\begin{pmatrix} 0 \\\\ 0 \\end{pmatrix}$$ 。而由矩阵乘法计算可知,经过 $$ A = \\begin{pmatrix} 1 & 0 & x \\\\ 0 & 1 & y \\\\ 0 & 0 & 1 \\end{pmatrix} $$ 对应的线性变换后, $$ \\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix} $$ 这个向量会被映射到 $$ \\begin{pmatrix}x\\\\y\\\\1\\end{pmatrix} $$ 上。也就是说,二维空间原点 $$ P_{Origin} = \\begin{pmatrix}0\\\\0\\end{pmatrix}$$ 经过变换后会变为 $$ P_{Origin}' = A(P_{Origin}) = \\begin{pmatrix}x\\\\y\\end{pmatrix}$$ 。\n\n而对于二维空间中的向量 $$\\vec{v}=\\begin{pmatrix}v_x\\\\v_y\\end{pmatrix}$$ ,其齐次坐标下 w 轴方向分量为 0 ,因此 w 轴方向上的切变并不会影响二维空间中的向量。即 $$ \\vec{v'} = A(\\vec{v}) = \\vec{v} $$ 。\n\n而对于原来二维空间中的其他点的坐标:\n$$\nP = \\begin{pmatrix}x_0\\\\y_0\\end{pmatrix}\n$$ \n其实可以理解为原点坐标再加上一个偏移向量:\n$$\nP = \\begin{pmatrix}0\\\\0\\end{pmatrix} + \\begin{pmatrix}x_0\\\\y_0\\end{pmatrix} = P_{Origin} + \\vec{v}_{x,y}\n$$\n\n而在齐次坐标下,点坐标 = 原点坐标 + 偏移向量 这一等式仍然成立:\n$$\nP = \\begin{pmatrix}x_0\\\\y_0\\\\1\\end{pmatrix} = \\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix} + \\begin{pmatrix}x_0\\\\y_0\\\\0\\end{pmatrix} = P_{Origin} + \\vec{v}_{x,y}\n$$\n\n而由于切变是线性变换,因此有:\n\n$$\n\\begin{align}\nP' &= A(P) \\\\\n&= A(P_{Origin} + \\vec{v}_{x,y}) \\\\\n&= A(P_{Origin}) + A(\\vec{v}_{x,y}) \\\\\n&= P_{Origin}' + \\vec{v}_{x,y} \\\\\n\\end{align}\n$$\n\n因为切变前后偏移向量没有发生变化,因此二维空间上的点经变换后相对于原点的方向、距离都没有发生变化。由此也可得出,原先由二维空间中的点组成的图案,经齐次坐标下 w 轴的切变后,其大小、形状、方向都不会发生变化。\n\n![[SliceOnHomogeneousWithGraph_ManimCE_v0.16.0.post0.gif]]\n\n而这种大小、形状、方向都不变化,只有整体位置发生了变化的变换,正是我们一般所说的“平移”。因此在齐次坐标下,我们能通过线性变换(aka 矩阵乘法)表示平移。\n\n> 其实 $$\\begin{pmatrix}1 & 0 & x \\\\0 & 1 & y \\\\0 & 0 & 1\\end{pmatrix}$$ 对应切变作用后各点坐标如何变化这个过程, 3Blue1Brown 的[这个视频](https://www.3blue1brown.com/lessons/matrix-multiplication) 有更直观明了的解释,大家可以参考。\n\n## 总结一下\n\nQ: 为什么普通的矩阵乘法不能表示平移?\nA: 因为矩阵乘法只能表示线性变换。平移不是线性变换。\n\nQ: 为什么在齐次坐标下的矩阵乘法又能表示平移?\nA: 因为齐次坐标增加了一个维度。平移变换矩阵其实是在新增的这个维度上做切变(一种线性变换)。切变后的结果正好就是原坐标中的平移变换。\n\n\n","title":"为什么使用在齐次坐标下矩阵乘法能表示点平移?","abstract":"简化了一万倍来说,线性变换主要是在描述符合这两种性质的变换:一是要可加,二是要能数乘。\n也就是说,对于空间中所有向量 $$\\vec{v_1}, \\vec{v_2}$$ ,以及任意数量 $$k_1, k_2$$ ,如果有:\n$$","length":149,"created_at":"2022-07-31T15:35:17.000Z","updated_at":"2022-08-05T17:45:09.000Z","tags":[],"license":true}},{"slug":"graph-for-economics-2","file":"public/content/articles/2022-07-19-graph-for-economics-2.md","mediaDir":"content/articles/2022-07-19-graph-for-economics-2","path":"/articles/graph-for-economics-2","meta":{"content":"\n> 1. 这篇文章参考了曼昆的《经济学原理》与北京大学王辉老师的《微观经济学》课程,内容上会有部分相似。\n> 2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。\n\n上一篇讲供给,这一篇讲需求。\n其实供给与需求有很多相似的地方,有时只需要套一下上一篇中给出的模型就能求解。因此各位如果还没有看过上一篇,可以先看完再回来看这一篇内容也不迟。\n\n讲需求曲线的时候,我们会先假设消费者是一个理性人,做决策时会将成本与收益作比较。比如在买冰酒这个场景,我们就会比较冰酒的价格与我们喝冰酒爽到的满足感(意愿支付价格),如果冰酒价格低于意愿支付价格我们就会去买这瓶冰酒。\n\n而假如我们是葡萄庄园主,我们当然也会去比较各种收益与成本,来决定是否制作冰酒拿出去卖。\n\n当我们选择制作冰酒拿出去卖,那收益自然就是卖冰酒所能拿到的钱,也就是冰酒的价格。\n\n但选择制作冰酒拿出去卖的成本呢?\n\n# 机会成本\n\n有一般会计常识的人可能会很快答出:成本不就是买种子、种葡萄、酿酒等过程中花掉的钱嘛!\n\n但实际上问题没有这么简单。因为我们的决策是“制作冰酒拿出去卖”,所以我们必须计算做出决策和不做出决策两种情况之间的差别。\n如果决定不制作冰酒拿出去卖,那我们可以省下一大笔时间与资金。我们可以拿着这些时间与资金去做其他事情,比如可以去种西瓜卖,可以去投资,甚至可以去打工当码农。这些活动都可以获得收益。\n而如果决定去制作冰酒拿出去卖,就代表你为了获得卖冰酒的收益,要选择放弃上面这些活动中获得的收益。“放弃获得这些收益的机会”也是你卖冰酒的成本。\n\n为了与常识中所说的成本做区别,我们把这种算上放弃收益机会的成本称为**机会成本**。经济学中常说的成本的也是机会成本。\n\n> **机会成本**( Opportunity cost ):是指为了得到某种东西所必须放弃的东西。 _ 曼昆《经济学原理:微观经济学分册》:Page 52\n\n## 生产者的理性人决策模型\n\n还记得理性人决策的模型吗?不记得的话可以先看看上一篇。\n引入了机会成本这一概念,我们套模型的三个要素就都准备好了:\n- 做决策 = 生产冰酒拿出去卖\n- 收益 = 冰酒价格(卖冰酒拿到的钱)\n- 成本 = 机会成本\n\n那么我们就有:\n- if 冰酒价格 > 机会成本 : 生产冰酒拿出去卖\n- if 冰酒价格 < 机会成本 : 不生产冰酒\n\n[[图片:机会成本柱,价格线,线高于柱做决策,线低于柱不做决策]]\n\n假设我们是葡萄庄园主,而且已经能预知冰酒市场价稳定在每瓶 50 元。如果我们荒废掉庄园拿钱去投资,就算减去掉投资风险,赚的钱都比卖冰酒赚的钱多得多,那我们就会毫不犹豫地荒废掉庄园选择躺着赚钱。这种其实就是机会成本高于交易收益的情况。\n\n## 生产者剩余\n\n如果收益高于机会成本,那我们就会毫不犹豫地选择生产冰酒拿出去卖。为啥?因为能赚钱呀!赚钱嘛,不寒掺。\n\n“赚钱”,可能就是生产者剩余最贴切地解释了。因为卖冰酒获得的收益(冰酒价格),比制作冰酒的成本(制作冰酒所放弃的其他收益加上制作冰酒耗费的金钱,也就是机会成本)更多,我们称在卖出冰酒的过程中我们获得了生产者剩余。\n\n> **生产者剩余**( producer surplus ):卖者出售一种商品得到的量减去其生产成本。Page 140\n\n与消费者剩余类似,生产者剩余计算上表示为:\n$$\n生产者剩余 = 卖出商品得到的量 - 卖出商品所支付的金钱\n$$\n表示为图的话就是商品价格与成本之间的部分:\n[[图片:生产者剩余柱,价格线,线与柱之间的部分]]\n\n## 供给曲线\n\n与上一篇里消费者情况类似,市场中的生产者也不会只有我们一个。我们把市场中(可能)卖冰酒的人全部抓过来审问一遍,统计一下他们卖冰酒的机会成本,从低到高排个序后就得到下面的图:\n\n[[图:机会成本柱状图,从低到高连成曲线,冰酒价格横线,线高于柱做决策,线低于柱不做决策]]\n\n与上一篇需求曲线过程类似,把各人机会成本连成曲线,我们就得到了冰酒市场中的供给曲线。\n\n供给曲线与冰酒价格交点的左边,由于这些生产者机会成本小于商品价格,能获得生产者剩余,他们就会选择制作并卖出冰酒(进入市场)。假设他们全部都能卖出冰酒,那他们卖出冰酒的量就是并就的交易量,他们的生产者剩余总和,也就是需求曲线以上价格线以下的部分,就是冰酒市场中总的生产者剩余。\n\n[[图:需求曲线与价格的交点,纵坐标横坐标解释,价格变动后,纵坐标与横坐标变化解释,交点连续变为曲线]]\n\n与上一篇同理,由于机会成本比较高,处于供给曲线与价格线交点右方的那些人不会选择制作冰酒,因此他们并不会在冰酒市场获得或失去生产者剩余。\n\n# 均衡\n\n上面分析供给曲线,包括上一篇中分析需求曲线时,我们都是先假设先有一个价格,然后再分析如果价格高了会怎么样,如果价格低了会怎么样。\n\n可是这个价格是谁来定的?\n\n## 完全竞争市场\n\n为了分析这个问题,我们需要引入除理性人假设外第二个假设:完全竞争市场假设:\n\n我们假设市场是完全竞争的,这样的市场必须具有三个特征:\n1. 消费者能自由选择购买任一生产者的商品\n2. 市场中的商品都是完全相同的\n3. 买卖双方都人数众多\n\n在这种假设下,市场中所有商品价格都相等,且没有任何一个消费者或生产者能够影响市场价格。因为如果有一个生产者的商品价格高于市场价,消费者们就会到别的地方购买;而由于他们都是理性人,没有生产者会打算以低于市场价的价格出售商品。消费者角度也同理:没有理由用高价买商品,而低价将买不到商品。\n\n这时,我们就可以把需求曲线与供给曲线放在一起分析,由于完全竞争市场中:\n$$\n任意消费者购买商品的价格 = 任意生产者出售商品的价格\n$$\n因此市场中商品价格是固定值,是水平于供给量/需求量的横线。\n[[图:需求曲线与供给曲线,价格横线只有一条,高于交点时与低于交点时]]\n\n另外,我们看到供给曲线与需求曲线之间有一个交点。接下来我们就要针对这个交点,解决完全竞争市场中价格由谁来定的问题。\n\n## 市场趋向于均衡\n\n首先我们考虑如果价格高于交点时的情况。\n\n前面我们说过,只有价格线与需求曲线交点左边的消费者会购买商品,而只有价格线与供给曲线交点左边的生产者会生产并出售商品。因此我们可以直观地知道,这时商品供给量比需求量要多。那多出来的那一部分商品一定会卖不出去。\n\n卖不出去咋办呀?那就只能降价。\n之前我们说完全竞争市场中生产者没有低价出售商品的理由,那是建立在商品都能卖出去前提下的。商品都能卖出去时没有道理自损利益,但现在商品卖不出去就只能降价吸引客流了。(其实提升商品质量也增加售出量是一种好方法,但我们这里假设了是完全竞争市场,所有商品都完全相同)\n\n消费者们也都是理性人,既然商品完全相同,自然就会选择购买更低价的商品。原本还凑合着能卖出去的那部分商品反而因为未降价变得卖不出去了,自然他们也会选择降价,最终市场中商品的价格整体降低。\n\n市场价降低,使得一部分生产者的机会成本高过了收益,这一部分生产者就会选择离开,使得供给量下降。另一方面,降价使得价格低过了一部分潜在消费者的意愿支付价格,这一部分人就会选择购买商品,使得需求量上升。\n\n[[图:均衡P81(Eng P77),价格高于均衡的情况]]\n\n而另一种情况,也相类似。如果价格低于交点,市场中商品的需求量就会大于供给量。这时必然会有一部分人想买但是买不到商品,他们就会逐渐选择用更高的价格来购买,最终拉高整个市场中的商品价格。市场价升高,使得供给量上升,需求量下降。\n\n[[图:价格低于均衡的情况]]\n\n价格高于交点时会趋于降价,而价格低于交点时会趋于涨价。在充分选择的情况下,最终市场价会等于交点处商品价格。这时市场达到均衡,生产者生产出的所有商品都能卖出,所有消费者都能购买到他们所需的商品。\n\n## 市场效率与福利\n\n之前我们提过消费者剩余与生产者剩余的概念。\n\n消费者剩余 = 意愿支付价格 - 商品价格,如果一个消费者在一场交易中消费者剩余越大,他就感觉越赚,他就对这场交易越满意。对于市场中所有消费者都是如此,因此市场中所有的消费者剩余,也就是需求曲线以下价格线以上的面积,代表了市场中所有消费者对市场交易的满意度。\n\n同样的,生产者剩余 = 商品价格 - 机会成本。市场中所有的生产者剩余,就是价格曲线以下供给曲线以上的部分,代表了市场中所有生产者对市场交易的满意度。\n\n因此,市场中总的生产者剩余加上总的消费者剩余,代表了市场中所有人对市场交易的满意度。我们称这就是市场中的总剩余。\n\n在市场均衡情况下,我们很容易地就能知道总剩余是多少。由于市场均衡时市场中所有的生产者与所有的消费者都能达成交易,而交易价格就是均衡价格。因此我们很容易地就能在图中找到代表生产者剩余、消费者剩余与总剩余的面积。\n\n[[图]]\n\n而如果市场没有达到均衡,会出现有的消费者没能买到商品、或是生产者的商品没能卖出去的情况。这些时候,没能买到商品的消费者与没能卖出商品的生产者自然不会对交易满意(因为没能达成交易),自然也不计算剩余。而市场中商品的交易量取决于需求量与供给量中更小的一方。无论如何,总剩余总会小于均衡时的总剩余。\n\n[[图]]\n\n由此可以看出,只有当市场达到均衡的时候,加入市场的所有人对市场交易的满意度最大。而市场均衡是完全竞争的自由市场中会自发达到的状态。\n\n因此,从经济学的观点来看,在所有人都是理性人、市场是完全竞争市场的假设下,不需要任何外加的制度或政策,市场就会自发地达到人们满意度最高的状态。也正因如此,亚当·斯密才会说市场是一个看不见的手。\n\n# 总结一下\n\n\n\n\n\n\n\nneeded:\n- [ ] svg character\n- [ ] bar graph\n- [ ] function graph\n- [ ] manim command for output path and input path https://docs.manim.community/en/stable/tutorials/configuration.html\n- [ ] want mp4 as picture\n- [ ] interactive manim https://github.com/3b1b/3Blue1Brown.com/tree/main/public/content/lessons/2021/newtons-fractal\n\n\n# 税收,污染权与外部性,国际贸易\n\n※\n# 比较优势\n# 弹性与均衡移动,收益分析 \n# 生产成本,垄断,寡头\n# 生产要素市场\n\n宏观:\n重点:\n1. 三个指标:GDP,价格水平,就业 =》 促使政府调节经济\n2. 两种政策:\n 1. 货币政策 =》 名义利率,量化宽松,前瞻指引,汇率决定制度等\n 2. 财政政策 =》 加息、采购\n3. 两种政策对经济影响(总供给总需求模型),以及国际经济与贸易影响\n\n# 三个指标与两种政策\n## 三个指标\n## 经济增长的原因 —— 全要素生产率\n\n## 经济波动\n## 凯恩斯主义 —— 促进政府调节经济\n## 调节经济两种政策\n# 财政政策\n# 货币政策\n\n## 两种政策对经济影响 —— 总供给总需求模型\n## 国际经济\n\n","title":"图解经济学原理(2)","abstract":"> 1. 这篇文章参考了曼昆的《经济学原理》与北京大学王辉老师的《微观经济学》课程,内容上会有部分相似。\n> 2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。\n上一篇讲供给,这一篇讲需求。","length":188,"created_at":"2022-07-19T23:12:48.000Z","updated_at":"2022-08-13T09:53:03.000Z","tags":[],"license":true}},{"slug":"graph-for-economics-1","file":"public/content/articles/2022-06-28-graph-for-economics-1.md","mediaDir":"content/articles/2022-06-28-graph-for-economics-1","path":"/articles/graph-for-economics-1","meta":{"content":"\n> 1. 这篇文章参考了曼昆的《经济学原理》与北京大学王辉老师的《微观经济学》课程,内容上会有部分相似。\n> 2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。\n\n我们先不讲课,先来带个货。\n\n德国和加拿大有一种特别的葡萄酒,叫冰酒。这种葡萄酒制作工艺比较特殊,必须要在严冬葡萄被霜冻在藤曼上时采摘下来,再经过发酵、压榨酿造而成。\n在冰酒压榨过程中,大量的冰被去除,使得葡萄的成分得到浓缩,因此冰酒口感偏甜。但加工过程对温度要求十分苛刻,温度过高、过低、变化太过剧烈等都会对口感产生影响。由于冰酒工艺特别,主要只有德国、加拿大等少数地区生产。\n\n好了,现在来考虑一个消费场景:\n假如你到加拿大去旅游,回国时在机场看到有礼品店在卖冰酒,考虑买一瓶冰酒回国后自己消费饮用。这瓶冰酒容量大概为 400ml ,并且这瓶冰酒只是一个普通牌子,不是奢侈品或高档品牌。\n在这个场景下,请大家考虑两个问题:\n1. 假设商店中这瓶冰酒标价换算为人民币是 ¥100 ,你是否愿意以这个价格购买一瓶冰酒?\n2. 假设你还不知道这瓶冰酒的价格,你会选择购买的最高价格是一瓶冰酒多少元?\n\n在问题 2 中,你愿意支付的最高价格就是你的心理价位,如果商店价格高于心理价位,你就不会购买这件商品。而如果价格低于心理价位,你就会购买这件商品。\n\n# 意愿支付\n\n上面场景中所说的心理价位,在经济学中又叫**意愿支付**。\n\n> **意愿支付**( willingness to pay ):是消费者愿意为获得某种物品所支付的最高代价。 —— 曼昆《经济学原理:微观经济学分册》: Page 134\n\n如果这瓶冰酒定价是 100 元,你会觉得太贵了不买,就说明你的意愿支付价格低于 100 元。\n然后我们再假设这瓶冰酒 50 元,你觉得很好很便宜,选择买了,就说明你的意愿支付价格在 100 元到 50 元之间。\n再把范围收窄一点, 80 元选择不买, 60 元选择买。范围逐渐收敛,买与不买之间的意向会变得越来越模糊。我们假设最终收敛到 70 元,你变得非常犹豫,感觉买与不买没有差别,那 70 元就是你的意愿支付价格。\n\n[[图片:意愿支付价格逼近。价格100时,不买,价格50时,买,80,60,最后70时会犹豫]]\n\n## 意愿支付价格的经济学解释\n\n经济学上有理性人这一概念,实际上是在假设你在决定是否做决策时,会将成本与收益做比较:\n- if 收益 > 成本 : 做出决策\n- if 收益 < 成本 : 不做出决策\n\n在购买冰酒的场景中,做决策就是指“买冰酒”这个行为。而成本就是冰酒的价格,收益就是你喝下冰酒感觉“爽到”。\n而我们不是机器人,我们“爽到”的感觉是很难与价格这种数字相比较的。因此我们要找一个办法把我们的爽到量化为价格。\n而意愿支付价格就是这个办法。在上面的例子中,买与不买的价格范围不断逼近,最终到 70 元时你觉得买还是不买都没什么区别。也就是说你喝冰酒爽到,就相当于得到了 70 元。\n\n> **意愿支付价格**:商品消费行为给消费者带来的效用的货币度量。\n\n\n有了意愿支付价格,买冰酒这件事就很容易模型化了。我们可以直接套回理性人决策的模型:\n- 做决策 = 买冰酒\n- 收益 = 意愿支付价格\n- 成本 = 冰酒价格\n\n则有:\n- if 意愿支付价格 > 冰酒价格 : 买冰酒\n- if 意愿支付价格 < 冰酒价格 : 不买冰酒\n\n[[图片:意愿支付价格=收益柱=70元,价格=成本=线,线高于柱=不决策,线低于柱=决策]]\n\n在买冰酒这一决策中,你的收益就是 70 元(喝冰酒爽到)。要你花 100 元(冰酒价格)来换 70 元,你肯定是不干的。而要你花 50 元来换 70 元,你就会爽快答应了。\n\n## 消费者剩余\n\n在上面模型中,如果你的意愿支付价格是 70 元,而冰酒只卖 50 元,你就一定会买买买,因为只要花 50 元就能买到 70 元的“爽到”呀!买到就是赚到。\n\n70 元的“爽到”只要花 50 元就能买到,这中间就差了 20 元呢,你就会觉得买冰酒的这笔钱花得真值,赚到 20 元。经济学上就称这是得到了 20 元的消费者剩余。\n\n> **消费者剩余**( consumer surplus ):买者原意为一种物品支付的量减去其为此实际支付的量。 —— 曼昆《经济学原理:微观经济学分册》: Page 135\n\n计算上:\n$$\n消费者剩余 = 意愿支付价格 - 商品价格 \n$$\n而实际上,消费者剩余是你买商品时赚到的感觉,是这种感觉的量化。\n你感觉买这瓶冰酒赚飞了,量化后表现为这次交易你获得的消费者剩余多;你感觉这次交易一般般,有点小贵(但还是愿意买),量化后就是这次交易你获得的消费者剩余少。你获得多少消费者剩余,就代表你在这场交易中赚到了多少(感觉上)。\n\n现在考虑另一种情况:你的意愿支付价格为 70 元,而冰酒价格为 100 元时。你没有选择交易,因此在这种情况,你没有获得消费者剩余,当然也没有失去消费者剩余。\n从另一个角度来说,你觉得交易成立后你会得到负的消费者剩余,因此机制的你决定不交易,防止了这次损失。\n\n话又说回来,实际情况中人的决策是不可能这么理性地去比较成本与收益,甚至有可能根本得不出一个意愿支付价格。因此上述讨论都是建立在假设上的——假设理性人模型成立。\n在实际情况中,这一假设可能根本不成立,因此这些讨论在现实中可能根本不适用。可这又有什么关系呢?就算相对论是正确的,牛顿定理仍然有他价值不是吗?\n\n# 需求曲线\n\n好了,上面说了一大堆,其实都是单个消费者(你)进行消费的情况。可实际上,这冰酒总不可能只有一个人买呀!\n\n而实际上,每个人对冰酒的爱好、口感要求、奢侈品需求等都是不同的。这就导致了每个人对冰酒这一商品的意愿支付价格可能都不一样!\n\n## 意愿支付价格统计\n\n我们假设,今天其实有包括你在内的 100 个客人都来过这家冰酒店。我把这 100 个客人全部逮住,按顺序每个人都审问了一遍意愿支付价格。于是得到了这样一幅意愿支付价格统计的图:\n\n[[图:意愿支付价格柱状图,乱序,横坐标是到店时间,纵坐标价格,横线为冰酒价格,上下浮动,意愿支付价格超过冰酒价格就会购买]]\n\n如果冰酒价格为 80 元,那所有意愿支付价格超过 80 元的客人都会选择买冰酒,而意愿支付价格低于 80 元的人都不会选择买。而如果冰酒价格为 60 元,那意愿支付价格超过 60 元的那部分客人也会开始选择买。冰酒价格越低,选择买冰酒的客人就越多。\n\n可是这图有点乱:\n1. 看不出客人意愿支付价格的分布\n2. 如果有 200 个客人到店,对应价格的冰酒又会有多少人买?\n\n## 需求曲线\n\n为了处理上面提出的两个问题,我把客人按照意愿支付价格从高到低来了个快速排序,然后把柱状图连成了一条曲线:\n\n[[图:快速排序,意愿支付价格从高到低,然后连成曲线,最后还是有冰酒价格横线]]\n\n我们能看到,代表冰酒价格(市场价格)的横线与曲线形成了一个交点。交点左边的客人都会选择买冰酒,而右边的人都会选择不买。\n冰酒价格下降,交点右移,选择购买冰酒的客人就会变多;冰酒价格上升,交点左移,购买的人就会变少。因此交点的横坐标就是购买冰酒的人数,也就是冰酒交易量。\n\n假设每个客人只会买一瓶冰酒,那么交点的横坐标同时也就是冰酒的需求量(实际上有客人不止买一瓶冰酒也没关系,我们可以当是来了两个客人)。而交点的纵坐标当然就是冰酒的价格。\n冰酒价格变化,交点位置也会变化,对应需求量也跟随发生变化。这条曲线描绘的就是冰酒需求量随冰酒价格变化的关系。\n\n[[图:需求曲线与价格的交点,纵坐标横坐标解释,价格变动后,纵坐标与横坐标变化解释,交点连续变为曲线]]\n\n我们称这条曲线为冰酒的需求曲线。\n\n> **需求曲线**( Demand Curve ):表示一种物品价格与需求量之间关系的图形 —— 曼昆《经济学原理:微观经济学分册》: Page 68\n\n像这样用需求曲线表示价格与需求量的关系,可以解决上面的两个问题:\n1. 客人意愿支付价格的分布就是需求曲线的形状(虽然我们为了简化只画直线,但其实曲线形状也是可以上凸下凹,甚至是S形的)\n2. 如果客人数量翻倍,我们一般认为新来的 100 人意愿支付价格分布跟原先 100 人的分布几乎相同,因此需求曲线形状不变,横坐标轴缩短一半(或者说图形横向拉伸一倍)就是我们要的结果了。\n\n值得一提的是,数学中我们常把横坐标当作自变量,而纵坐标表示因变量。但需求曲线中正相反,纵坐标的价格是自变量,需求量才是因变量。\n我记得高中老师一般都会说这是因为经济学家不懂数学,然后草草带过。但实际上,消费者意愿支付多少钱容易统计,而不同价格下到底有多少人会想买难以统计。通过统计意愿支付价格并排序生成需求曲线时,将价格放在纵轴是一种很合理的选择。马歇尔当初也是在这一框架下推导出需求曲线的,曼昆也在他的[这篇博客](http://gregmankiw.blogspot.com/2006/09/who-invented-supply-and-demand.html)中对此有过讨论。\n(实际上,马歇尔是从效用理论推导出需求曲线的,与我们上面推导的过程不一样,但总的来说还是在同一框架下。说马歇尔不懂数学,就像是在说薛定谔不懂数学——怎么可能嘛。)\n\n\n\n## 市场上所有的消费者剩余\n\n店里来了这么多人,每个人意愿支付价格都不一样,那每个人买到同样价格的冰酒,感觉赚到的程度肯定是不一样的。\n\n[[图:柱状图,展示个人的消费者剩余,然后到线图,展示面积,即市场中的消费者剩余]]\n\n对于单个人来说,他的消费者剩余就是意愿支付价格减去商品价格,也就是柱子在价格线以上标橙色的部分。\n\n可现在到店里的不止一个人啊,我要算所有消费者一共感觉赚到了多少。那我就要把所有橙色部分加起来,也就是做了一个积分,积分的结果就是到店里所有人通过买冰酒这件事一共能赚多少了。大家别看到积分就怕,其实意思就是求需求曲线以下,价格线以上这一三角形的面积。\n(每个人买冰酒价格肯定是固定的。总不能对不同的人以不同的价格出售吧)\n\n值得一提的是,在价格线与需求曲线交点右边的这些人,是不算消费者剩余的,也不会使总的消费者剩余减少。因为他们嫌冰酒太贵(高于意愿支付价格),根本就没有买冰酒(达成交易)。\n\n\n# 总结一下\n\n","title":"图解经济学原理(1)","abstract":"> 1. 这篇文章参考了曼昆的《经济学原理》与北京大学王辉老师的《微观经济学》课程,内容上会有部分相似。\n> 2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。\n我们先不讲课,先来带个货。","length":139,"created_at":"2022-06-28T00:59:41.000Z","updated_at":"2022-06-28T14:24:43.000Z","tags":[],"license":true}},{"slug":"use-paste-image-and-vscode-memo","file":"public/content/articles/2022-04-03-use-paste-image-and-vscode-memo.md","mediaDir":"content/articles/2022-04-03-use-paste-image-and-vscode-memo","path":"/articles/use-paste-image-and-vscode-memo","meta":{"content":"\n我平时使用 [vscode-memo](https://github.com/svsool/vscode-memo) 插件写笔记,其中插入图片使用 `![[]]` 语法,显示简短,也有较好的预览支持,体验极佳。希望这种特性也能在写 hexo 博客的时候使用。\n\n# 关于 vscode-memo\n\n可能有很多人不熟悉 vscode-memo 这个插件,我先来简单介绍一下。\n\nvscode-memo 定位是一个 knowledge base ,对标的是 [Obsidian.md](https://obsidian.md/) 等软件。其功能包括且不限于:\n\n1. 使用独有的短链接语法 `[[]]` 连接到其他文档与图片。\n2. 修改文件名时自动同步更新链接,反向查找当前文档被那些文档链接。\n3. 鼠标悬停时能预览链接与图片。\n\n同时,由于 vscode-memo 是个 vscode 插件,可以跟 vscode 的其他众多插件合作使用。比如 [vscode-memo 官方文档](https://github.com/svsool/vscode-memo/blob/master/help/How%20to/Pasting%20images%20from%20clipboard.md)里就推荐将 vscode-memo 与 vscode-past-image 插件配合,粘贴图片。\n\n这篇文章主要的目的,也是利用这两个插件,达到把图片粘贴为短链接,并被 Hexo 正常渲染为网页。\n\n# Image Paste 与 Hexo 的配置\n\n这一步其实很简单。\n\n在 Hexo 的文章中,一般需要使用从根目录起的相对链接。如有文件结构:\n\n```tree\nsource\n├───img\n│ └───in-post\n│ ├───heap-cheat-sheet.jpg\n│ └───post-js-version.jpg\n├───playground\n└───_posts\n ├───2022-03-26-create-blog-cicd-by-github.md\n └───2022-04-03-use-paste-image-and-vscode-memo.md\n```\n\n在 `2022-03-26-create-blog-cicd-by-github.md` 中引用 `heap-cheat-sheet.jpg` 这个图片,就需要 `![](/img/in-post/heap-cheat-sheet.jpg)` 这样的链接。\n\n但如果在配置里把 `post_asset_folder` 设为 `true` ,就可以在 Markdown 文件的同级位置的同名目录中直接找到图片。如:\n\n```tree\nsource\n├───img\n├───playground\n└───_posts\n ├───2022-03-26-create-blog-cicd-by-github\n │ ├───heap-cheat-sheet.jpg\n │ └───post-js-version.jpg\n ├───2022-03-26-create-blog-cicd-by-github.md\n └───2022-04-03-use-paste-image-and-vscode-memo.md\n```\n\n然后在 `2022-03-26-create-blog-cicd-by-github.md` 中可以直接 `![](heap-cheat-sheet.jpg)` 引用图片。为了图片文件管理方便,我们打开这个配置项。\n\n为了能让 Image Paste 粘贴的图片能放到这个同名文件夹下,我们需要修改 Image Paste 配置,在 VSCode 的 Workspace Setting 中,添加如下设置:\n\n```json\n{\n \"pasteImage.path\": \"${currentFileDir}/${currentFileNameWithoutExt}/\"\n}\n```\n\n# Image Paste 粘贴为 vscode-memo 短链接格式\n\n这一步也很简单。 Image Paste 可以设定粘贴后的格式。我们在 Workspace Setting 中添加如下设置即可:\n\n```json\n{\n \"pasteImage.insertPattern\": \"![[${imageFileName}]]\",\n}\n```\n\n这样我们粘贴后的图片就能有预览功能了。\n\n# 让 Hexo 正确渲染 vscode-memo 的短链接\n\n这一步其实是最难的。 Hexo 当然不认识 vscode-memo 的短链接,而经过调查,现在还没有现成的方案让 Hexo 与 vscode-memo 集成。虽然我们提倡尽量不要重复造轮子,但这里我们也是除了造轮子没有其他办法了。\n\n我们采用的方案是让 Hexo 在渲染 Markdown 前,先把 Markdown 中形如 `![[]]` 的短链接,替换为 `![]()` 的正常 Markdown 图片链接。\n\n假设我们项目 `source` 文件夹如下:\n\n```tree\nsource\n├───playground\n└───_posts\n ├───2022-03-26-create-blog-cicd-by-github\n │ ├───heap-cheat-sheet.jpg\n │ └───post-js-version.jpg\n ├───2022-03-26-create-blog-cicd-by-github.md\n └───2022-04-03-use-paste-image-and-vscode-memo.md\n```\n\n如在渲染 `2022-03-26-create-blog-cicd-by-github.md` 前,需要将其中的 `![[heat-cheat-sheet.jpg]]` 替换为 `![](heap-cheat-sheet.jpg)` 。我们知道 Hexo 在生成静态文件前会先把项目根目录下 `scripts` 目录下的所有脚本执行一遍。我们可以在这里注册一个 filter ,专门做这个替换。代码如下:\n\n```js\n'use-strict';\n\nhexo.extend.filter.register('before_post_render', function (data) {\n const isToHandle = (data) => {\n var source = data.source;\n var ext = source.substring(source.lastIndexOf('.') + 1, source.length).toLowerCase();\n return ['md'].indexOf(ext) > -1;\n }\n\n if (!isToHandle(data)) {\n return data;\n }\n\n const reg = /(\\s+)\\!\\[\\[(.+)\\]\\](\\s+)/g;\n\n data.content = data.content\n .replace(reg, function (raw, start, content, end) {\n var nameAndTitle = content.split('|');\n if (nameAndTitle.length == 1) {\n return `${start}![](${content})${end}`;\n }\n return `${start}![${nameAndTitle[1]}](${nameAndTitle[0]})${end}`;\n });\n return data;\n\n})\n```\n\n# 测试一下\n\n文章中如下内容:\n\n![[这部分内容会被转换为图片.png]]\n\n\n而你看到上面的内容是一张图片,表示这个转换已经成功了。\n\n# 不足之处\n\n这一段代码仍有以下待改进的地方:\n1. 如果图片短链接的内容写在 Code Block 里,也一样会被转换。实际上我们一般不希望 Code Block 里的内容被转换,需要过滤一下。\n2. 形如 `![[文件|图片描述]]` 的内容会正常转换为 `![图片描述](文件)` 。然而我现在用的这个主题不支持图片描述。以后可能需要更换主题。\n\n# 补充\n\n如果希望网站图片放在 `img` 之类的文件夹下统一管理,不把 `post_asset_folder` 设为 `true` ,也是没问题的,可以通过修改代码,在返回 `${content}` 前添加统一前缀。\n\n而如果希望图片放在 `img` 下,又要按文章分文件夹管理,如下情况:\n\n```tree\nsource\n├───img\n│ ├───2022-03-26-create-blog-cicd-by-github\n│ │ ├───heap-cheat-sheet.jpg\n│ │ └───post-js-version.jpg\n│ └───2022-04-03-use-paste-image-and-vscode-memo\n├───playground\n└───_posts\n ├───2022-03-26-create-blog-cicd-by-github.md\n └───2022-04-03-use-paste-image-and-vscode-memo.md\n```\n\n可以通过在代码中引用 `data.source` 解决。","title":"完善 Hexo 编写环境,改善文章中使用图片的体验","abstract":"我平时使用 [vscode-memo](https://github.com/svsool/vscode-memo) 插件写笔记,其中插入图片使用 `![[]]` 语法,显示简短,也有较好的预览支持,体验极佳。希望这种特性也能在写 hexo 博客的时候使用。\n可能有很多人不熟悉 vscode-memo 这个插件,我先来简单介绍一下。\nvscode-memo 定位是一个 knowledge base ,对标的是 [Obsidian.md](https://obsidian.md/) 等软件。其功能包括且不限于:","length":158,"created_at":"2022-04-03T21:03:03.000Z","updated_at":"2022-04-03T17:47:52.000Z","tags":["Blog","VSCode","Hexo","JavaScript"],"license":false}},{"slug":"create-blog-cicd-by-github","file":"public/content/articles/2022-03-26-create-blog-cicd-by-github.md","mediaDir":"content/articles/2022-03-26-create-blog-cicd-by-github","path":"/articles/create-blog-cicd-by-github","meta":{"content":"\nGitHub Action 自动化构建发布到 GitHub Pages 大家都见得多了,甚至 Hexo 官方自己都有相关的文档。\n但我今天要做的不是发布到 GitHub 这么简单,而是要同时发布到 GitHub 和自己的域名下。\n\n# 这篇文章的目标\n\n我们需要构建一个 CI/CD 过程。这个过程需要做到以下目标:\n1. 将文章 push 到 GitHub 的 master branch 后,自动触发。\n2. 我们博客使用 Hexo 引擎,需要先构建静态文件。\n3. 需要将静态文件部署到 GitHub Page 。\n4. 需要将静态文件部署到自己域名下。\n 这里我们使用 AWS 的 S3 服务与 CloudFront 服务直接部署到 CDN 上。 CloudFront 直接通过 OAI 访问 S3 ,不允许用户直接通过 S3 访问。\n5. 博客在 GitHub Page 与 S3 需要处于不同的路径下。\n 为了延续以往的情况,博客在 GitHub Page 需要部署在 `/blog/` 下。\n 而在 AWS 上我则希望直接部署在根目录下,这就导致需要两份配置文件。\n 当然弄两份配置文件我是不乐意的,于是就需要从模板自动生成配置文件...\n\n其中,一二三点都很好解决,而第四点会是一个比较难又比较坑爹的地方。\n\n# 先做简单的 —— CI/CD 构建并发布到 GitHub Pages\n\n这一步其实没什么难的, Hexo 官网上就有[这篇文章](https://hexo.io/docs/github-pages.html)写的十分详细了,可以作为参考。\n\n```yaml\nname: Pages\n\non:\n push:\n branches:\n - master # default branch\n\njobs:\n pages:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2\n - name: Use Node.js 16.x\n uses: actions/setup-node@v2\n with:\n node-version: '16'\n - name: Cache NPM dependencies\n uses: actions/cache@v2\n with:\n path: node_modules\n key: ${{ runner.OS }}-npm-cache\n restore-keys: |\n ${{ runner.OS }}-npm-cache\n - name: Install Dependencies\n run: npm install\n - name: Build\n run: npm run build\n - name: Deploy\n uses: peaceiris/actions-gh-pages@v3\n with:\n github_token: ${{ secrets.GITHUB_TOKEN }}\n publish_dir: ./public\n publish_branch: gh-pages # deploying branch\n```\n\n这个 yaml 就是 GitHub Action 的 workflow 文件,在这个 workflow 里:\n1. 先用 `npm run build` 把静态文件生成到 `./public` 下\n2. 用 `peaceiris/actions-gh-pages@v3` 这个 action 把 `./public` 的文件放到 `gh-pages` 分支下。\n\n把上面这个 yaml 文件复制到 `.github/workflows/build.yml` 中,这样 master 分支上发生任何提交都会触发构建流程了。按照 Hexo 官网上的文档跑一边就能成功发布到 GitHub Pages 上了。\n\n不过我需要部署到 `/blog/` 下,这叫 Project Page ,因此我走的是 Hexo 文档的 Project Page 这一小节的流程,需要把 `_config.yml` 里做如下设置:\n\n```yaml\nurl: https://ryojerryyu.github.io/blog # 这个其实不是很重要,现在用的主题没有用到这个字段\nroot: /blog/ # 这个比较重要,这个不设定好,整个页面的超链接都会歪掉\n```\n\n当然, “没什么难” 的前提是你首先要对 Hexo 和 GitHub Action 有一个了解...\n\n# 难一点 —— 搭建 AWS 基础设施\n\n我为什么不止用 GitHub Pages 还要配一套 AWS 呢?其实主要还是想以后可能会做一下 Backend ,而且放 AWS 上还能利用 AWS 的服务做一下流量分析之类的。没这么些需求的小伙伴可以不用继续看了...\n\n我们打算使用 AWS 的 S3 与 CloudFront 服务, CloudFront 直接通过 OAI 访问 S3 。\n\n## S3\n\nS3 是 AWS 的对象储存服务,简单来说就是可以当网盘用,往里面放文件。\nS3 有静态网站托管服务,把静态文件放到 S3 里,配置一番就直接可以通过 HTTP 访问了,还能用自己的域名。\n但我们不打算使用 S3 的静态网站托管,因为我打算直接上 CDN ,又不想用户可以直接通过 S3 来访问我们的静态文件。\n\n## CloudFront\n\nCloudFront 是 AWS 的内容分发服务,简单来说就是 CDN 。其实它不只有 CDN 的功能,它还能加速动态调用,还能通过 CloudFront 连接 Web Socket ... 不过我们这次主要是用 CDN 功能。\nCloudFront 访问 S3 的方式还是有好几种的。中文教程最常见的是让你先打开 S3 静态网站托管,然后将 CloudFront 的源设为 S3 的域名。\n这个方法是最早支持的,因此推广的也比较开。但其实我觉得这个方法有些问题:\n\n1. S3 不做另外配置的话是可以直接访问的,比较 low\n2. S3 自己的 HTTP Endpoint 不能上 TLS ,所以 CloudFront 到 S3 这一段是裸奔的\n\n因此我打算使用 AWS 最近推荐的 OAI 方式访问 S3 。这种方式不走 HTTP Endpoint 而是 S3 自己的 S3 Endpoint ,可以通过 AWS 的 IAM 机制统一管理。\nOAI 是 Origin Access Identity ,简单来说就是给 CloudFront 一个 AWS IAM Policy 的 Principal 身份, S3 可以通过如下 Bucket Policy 限制外部只能通过这个 Principal 访问:\n```json\n{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Effect\": \"Allow\",\n \"Principal\": {\n \"AWS\": \"arn:aws:iam::cloudfront:user/<CloudFront Origin Access Identity ID>\"\n },\n \"Action\": \"s3:GetObject\",\n \"Resource\": \"arn:aws:s3:::<bucket name>/*\"\n }\n ]\n}\n```\n上面这一段看不懂的同学,可以去补习一下 AWS IAM 权限管理机制,关键就是 Principal —— 主体 、 Action —— 动词 、 Resource —— 受体 的一个主谓宾模式。\n\n## 其他 AWS 服务\n\n当然,仅有 S3 和 CloudFront 是不足以实现全部功能的,我们还需要 Route53 来管理路由, ACM 来获取免费证书。\n但这些我都不打算细讲,因为内容真的很多-_-,而且大部分都是 AWS 的细节,搬到别的云上不一定适用...而且手动操作麻烦死了...\n\n## Pulumi\n\n综上嘛,我们需要:\n1. 建一个 Route53 Hosted Zone ,把域名交给 Route53 管理\n2. 用 ACM 给域名申请一个 us-east-1 Region 的免费证书(CloudFront 的证书必须在 us-east-1 )\n3. 建一个 S3 储存桶,把 Public Access Block 配置一下\n4. 建一个 CloudFront Distribution ,通过 OAI 来访问 S3 ,还要指定一下证书\n5. 给 S3 配一个 Bucket Policy ,允许 CloudFront 访问\n6. 把 Route53 里的域名弄个 DNS 记录指向 CloudFront\n\n手动操作麻烦死了,于是我打算用 IaC (Infrastructure-as-Code) 来解决。我把这些基础设施定义用 Pulumi 写成的代码放在[这里](https://github.com/RyoJerryYu/aws-blog-infra/tree/c97f0fe41b5c0306d5343ddfc22f4a3775d79b88/website)了,大家可以参考一下(做了模块化,跟我其他基础设施放一起了)。\n\n当然,用 Pulumi 没什么特别原因,纯粹是因为我最近在写 Pulumi... 你完全可以用其他 IaC 工具(Ansible、Terraform、CloudFormation)来做。而且 Pulumi 太新了,用起来挺多 Bug 的...(也许是我不会用)\n\n## 测试一下\n\nS3 桶啥的都建好之后,本地把文件 build 一下,用 `aws s3 cp ./public/ s3://<bucket>/ --recursive` 之类的命令上传到 S3 ,给 CloudFront 创建一个 Invalidation 刷新一下 CloudFront 缓存,访问域名看看,有返回个 HTML 我们的基础设施就算是跑通了。此时可能会出现以下情况,都属正常:\n1. 访问返回 307 :\n 是 S3 储存桶 Region 不在 us-east-1 导致的。\n CloudFront 是通过 s3 的 global endpoint 访问 s3 的,但不在 us-east-1 的 s3 刚新建时还不能通过 global endpoint 访问。\n 参考 so 的[这个问题](https://stackoverflow.com/questions/38706424/aws-cloudfront-returns-http-307-when-origin-is-s3-bucket):\n\n > All buckets have at least two REST endpoint hostnames. In eu-west-1, they are example-bucket.s3-eu-west-1.amazonaws.com and example-bucket.s3.amazonaws.com. The first one will be immedately valid when the bucket is created. The second one -- sometimes referred to as the \"global endpoint\" -- which is the one CloudFront uses -- will not, unless the bucket is in us-east-1. Over a period of seconds to minutes, variable by location and other factors, it becomes globally accesible as well. Before that, the 307 redirect is returned. Hence, the bucket was not ready.\n \n 这时候只要等个十几分钟就好了。\n2. 本地 build 的时候没配置好的话,js 之类的静态文件可能返回不了,但问题不大,我们接下来再处理。\n\n\n# 搭建 S3 的 workflow\n\n基础设施搭好了,我们就要像 deploy 到 GitHub Pages 一样,造一个自动管线发布到 S3 了。\n整理一下,我们的 workflow 里要包括:\n\n1. 从模板生成配置文件\n 别忘了,我需要的是静态文件部署在 GitHub Pages 和自己域名下的不同路径上。 Hexo 生成静态文件前配置文件必须要改的。\n2. 把原先 s3 上的文件删除,并上传新的文件到 s3\n3. 给 CloudFront 创建一个 Invalidation 刷新缓存\n\n## 生成配置文件\n\n这一步其实方案很多,甚至 bash 直接全文替换都可以...\n不过怕以后要改的东西变多,这里还是选择一些模板生成工具。有如下选择:\n\n1. 屠龙刀 Ansible\n2. Python Jinja2\n3. Go Template\n\n这里用 Ansible 确实是大材小用了,而且 Ansible 不能在 Windows 下用还是有点不方便,只能弃选。而 Python 和 Go 里我选了 Go Template ,原因是... 不想写 Python...\n这里其实确实是装逼了,这种小型脚本应该 Python 比 Go 合适的多。不过还好 Go run 可以不先 go mod 就能运行,不算是个太差的选择。不过以后还是大概率要改回 Python 。\n\n写 golang 脚本没有难度,大致如下:\n\ngolang template 的 name 要是 file name\n```golang\nname := path.Base(*tmpl)\nt := template.Must(template.New(name).ParseFiles(*tmpl))\nerr = t.Execute(os.Stdout, config)\nif err != nil {\n log.Fatal(err)\n}\n```\ngithub workflow 如下\n```yaml \n- name: Use Go 1.16\n uses: actions/setup-go@v1\n with:\n go-version: '1.16.1'\n\n- name: generate config\n run: go run ./genconfig/main.go --env=gh-pages > _config.yml\n\n```\nwindows 玩家可能要注意一下,windows 下编码有问题, `go run ./genconfig/main.go --env=gh-pages > _config.yml` 这段命令直接在 PowerShell 下跑生成出来的文件不能被 Hexo 识别。不过没什么关系,反正这段到时候是在 GitHub Action Runner 上跑的,只不过是不能本地生成用来测试而已。\n\n[参考代码](https://raw.githubusercontent.com/RyoJerryYu/blog/2f407cb6ee723d0e17c97af1289bd2231bb265ab/genconfig/main.go)\n\n## 上传 s3 与刷新 CloudFront\n\n后两步搜一下发现其实有很多现成的 GitHub Action 可以用。\n不过我没有采用,原因是——真的没必要啊...就几个命令的事,又不是不会敲...\n\nworkflows yaml 如下:\n```yaml\n- name: Configure AWS\n uses: aws-actions/configure-aws-credentials@v1\n with:\n aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n aws-region: ap-northeast-1\n- name: Deploy\n env:\n S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}\n DISTRIBUTION_ID: ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }}\n run: |\n aws s3 rm s3://$S3_BUCKET/* --recursive\n aws s3 cp ./public s3://$S3_BUCKET/ --recursive\n aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths '/*' --region=us-east-1\n```\n\n[完整 yaml 参考代码](https://raw.githubusercontent.com/RyoJerryYu/blog/f0affb812f2de437943d9cf2a4f8a5fe690d1efd/.github/workflows/clouds.yml)\n\n由于改为了生成配置文件, deploy 到 Github Pages 的 yaml 也要做相应改动,这里就不多说。\n\n# CloudFront 的一点小问题(不太小)\n\n这样我们的整个流程是不是跑完了?我们的博客已经部署到自己的域名下了?\n浏览器打开自己的域名看看,完美显示!\n\n等等,别高兴的太早,点进去一篇文章... 403 了...\n\n403 的原因:\n1. hexo 生成出来的 page 连接是 `/` 结尾的,如 `/2022/03/26/create-blog-cicd-by-github/` ,然后通过 HTTP 服务器的自动转义指向 `/2022/03/26/create-blog-cicd-by-github/index.html` 文件。\n2. CloudFront 可以定义默认根对象,没有为每个子路径都自动转义的功能。\n3. S3 的 HTTP endpoint 可以配置索引文档,为每个子路径自动转义,但 CloudFront 通过 OAI 访问 S3 时通过 REST endpoint 访问,不会触发自动转义。\n\n一大波参考阅读:\n\n[Specifying a default root object](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DefaultRootObject.html)\n> Here's an example of how a default root object works. Suppose the following request points to the object image.jpg:\n> ```\n> https://d111111abcdef8.cloudfront.net/image.jpg\n> ```\n> In contrast, the following request points to the root URL of the same distribution instead of to a specific object, as in the first example:\n> ```\n> https://d111111abcdef8.cloudfront.net/\n> ```\n> When you define a default root object, an end-user request that calls the root of your distribution returns the default root object. For example, if you designate the file index.html as your default root object, a request for:\n> ```\n> https://d111111abcdef8.cloudfront.net/\n> ```\n> Returns:\n> ```\n> https://d111111abcdef8.cloudfront.net/index.html\n> ```\n> However, if you define a default root object, an end-user request for a subdirectory of your distribution does not return the default root object. For example, suppose index.html is your default root object and that CloudFront receives an end-user request for the install directory under your CloudFront distribution:\n> ```\n> https://d111111abcdef8.cloudfront.net/install/\n> ```\n> CloudFront does not return the default root object even if a copy of index.html appears in the install directory.\n> \n> If you configure your distribution to allow all of the HTTP methods that CloudFront supports, the default root object applies to all methods. For example, if your default root object is index.php and you write your application to submit a POST request to the root of your domain (http://example.com), CloudFront sends the request to http://example.com/index.php.\n> \n> The behavior of CloudFront default root objects is different from the behavior of Amazon S3 index documents. When you configure an Amazon S3 bucket as a website and specify the index document, Amazon S3 returns the index document even if a user requests a subdirectory in the bucket. (A copy of the index document must appear in every subdirectory.) For more information about configuring Amazon S3 buckets as websites and about index documents, see the Hosting Websites on Amazon S3 chapter in the Amazon Simple Storage Service User Guide.\n\n[Configuring an index document](https://docs.aws.amazon.com/AmazonS3/latest/userguide/IndexDocumentSupport.html)\n> In Amazon S3, a bucket is a flat container of objects. It does not provide any hierarchical organization as the file system on your computer does. However, you can create a logical hierarchy by using object key names that imply a folder structure.\n> \n> For example, consider a bucket with three objects that have the following key names. Although these are stored with no physical hierarchical organization, you can infer the following logical folder structure from the key names:\n> - sample1.jpg — Object is at the root of the bucket.\n> - photos/2006/Jan/sample2.jpg — Object is in the photos/2006/Jan subfolder.\n> - photos/2006/Feb/sample3.jpg — Object is in the photos/2006/Feb subfolder.\n> \n> In the Amazon S3 console, you can also create a folder in a bucket. For example, you can create a folder named photos. You can upload objects to the bucket or to the photos folder within the bucket. If you add the object sample.jpg to the bucket, the key name is sample.jpg. If you upload the object to the photos folder, the object key name is photos/sample.jpg.\n> \n> If you create a folder structure in your bucket, you must have an index document at each level. In each folder, the index document must have the same name, for example, index.html. When a user specifies a URL that resembles a folder lookup, the presence or absence of a trailing slash determines the behavior of the website. For example, the following URL, with a trailing slash, returns the photos/index.html index document.\n> ```\n> http://bucket-name.s3-website.Region.amazonaws.com/photos/\n> ```\n> \n> However, if you exclude the trailing slash from the preceding URL, Amazon S3 first looks for an object photos in the bucket. If the photos object is not found, it searches for an index document, photos/index.html. If that document is found, Amazon S3 returns a 302 Found message and points to the photos/ key. For subsequent requests to photos/, Amazon S3 returns photos/index.html. If the index document is not found, Amazon S3 returns an error.\n\n[Implementing Default Directory Indexes in Amazon S3-backed Amazon CloudFront Origins Using Lambda@Edge](https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/)\n> If you implement CloudFront in front of S3, you can achieve this by using an OAI. However, in order to do this, you cannot use the HTTP endpoint that is exposed by S3’s static website hosting feature. Instead, CloudFront must use the S3 REST endpoint to fetch content from your origin so that the request can be authenticated using the OAI. This presents some challenges in that the REST endpoint does not support redirection to a default index page.\n\n> CloudFront does allow you to specify a default root object (index.html), but it only works on the root of the website (such as http://www.example.com > http://www.example.com/index.html). It does not work on any subdirectory (such as http://www.example.com/about/). If you were to attempt to request this URL through CloudFront, CloudFront would do a S3 GetObject API call against a key that does not exist.\n\n\n\n那么,我们要怎么解决这个问题呢?我觉得,这个问题有三种解决方法:\n\n1. 不使用 OAI ,让 CloudFront 直接指向 S3 的域名,让 CloudFront 使用 S3 HTTP Endpoint 的特性\n2. 调整 Hexo 配置,更改生成文件路径或连接路径\n3. 使用 AWS 推荐的 Lambda@Edge 功能,在 CloudFront 上修改路径\n\n其中第二种方案是最下策,我们不能在还有其他方案的情况下,因为基础设施的一个性质就去修改我们的产品。况且我们的产品在大多数场景下都是适用的。\n第一种方案是中策,也许实行起来也是最简单的。但我不想用,原因上面也说过了。\n第三种方案是实施起来难度最大的,我们要引入 Lambda 这一新概念。但反正折腾嘛,试试就试试,反正失败了再变回第一种方案就是。\n\n## 创建 Lambda\n\n[Implementing Default Directory Indexes in Amazon S3-backed Amazon CloudFront Origins Using Lambda@Edge](https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/)\n\n参考上面的文档,我们直接在 Console 创建一个 Lambda 函数,内容如下:\n\n```javascript\n'use strict';\nexports.handler = (event, context, callback) => {\n \n // Extract the request from the CloudFront event that is sent to Lambda@Edge \n var request = event.Records[0].cf.request;\n\n // Extract the URI from the request\n var olduri = request.uri;\n\n // Match any '/' that occurs at the end of a URI. Replace it with a default index\n var newuri = olduri.replace(/\\/$/, '\\/index.html');\n \n // Log the URI as received by CloudFront and the new URI to be used to fetch from origin\n console.log(\"Old URI: \" + olduri);\n console.log(\"New URI: \" + newuri);\n \n // Replace the received URI with the URI that includes the index page\n request.uri = newuri;\n \n // Return to CloudFront\n return callback(null, request);\n\n};\n```\n这一段代码主要作用是把接收到每个以 `/` 结尾的请求,都转换为以 `/index.html` 结尾的请求。\n\nDeploy 之后,为 Lambda 添加 Trigger ,选择 CloudFront 作为 Trigger , Event 选择 On Request 。按照界面的提示为 Lambda 创建专用的 Role 。\n提交后,我们就可以通过 Url 访问,发现 `/` 结尾的 URL 也会正常显示了。\n\n# 之后的事\n\n这个过程仍有以下问题:\n- 对 Lambda 的认识仍有不足,今后需继续学习运用\n- Lambda@Edge 还没有结合到 IaC 中\n- 配置文件生成过程仍有改进空间\n\n留下这些问题,今后再修改。\n","title":"用 GitHub Action 自动化构建 Hexo 并发布到 S3","abstract":"GitHub Action 自动化构建发布到 GitHub Pages 大家都见得多了,甚至 Hexo 官方自己都有相关的文档。\n但我今天要做的不是发布到 GitHub 这么简单,而是要同时发布到 GitHub 和自己的域名下。\n我们需要构建一个 CI/CD 过程。这个过程需要做到以下目标:","length":342,"created_at":"2022-03-26T23:55:08.000Z","updated_at":"2022-03-27T13:31:04.000Z","tags":["Blog","GitHub","AWS","CI/CD","IaC","DevOps"],"license":false}},{"slug":"init-a-new-hexo-project","file":"public/content/articles/2021-12-12-init-a-new-hexo-project.md","mediaDir":"content/articles/2021-12-12-init-a-new-hexo-project","path":"/articles/init-a-new-hexo-project","meta":{"content":"\n## 使用 hexo 搭建博客\n\n最近使用 hexo 搭建了一个博客,并打算挂载在 github page 上。\n对之前的那个博客进行替代,并将之前的文章逐渐搬移过来。\n\n使用的[这个主题](https://github.com/Yue-plus/hexo-theme-arknights)功能还是比较完善的。\n\n我们可以尝试一下代码块高亮:\n\n```python\ndef func_echo(s: str):\n print(s)\n\n\nclass HelloPrinter:\n printer: Callable[[str]]\n\n def __init__(self, printer: Callable[[str]]):\n self.printer = printer\n \n def call(self, s: str):\n self.printer(s)\n\n\np = HelloPrinter(func_echo)\np.call(\"hello world!\")\n```\n\n试试下标语法吧:\n\n这是一句话。[^sub]\n\n没想到还支持下标语法,还是比较惊艳的。\n\n来几句 mermaid 吧\n\n```mermaid\ngraph LR\n\nohmy-->coll\n\n```\n\n原本是不能渲染的, 这个主题渲染代码块时把 mermaid 代码当作普通代码,往里面里插换行符号了。\n使用了 hexo-filter-mermaid-diagrams 插件,添加 mermaid 过滤器,解决问题。\n\n\n来几句 LaTeX:\n\n$$\n\\begin{aligned}\nf(x) &= \\sum_{i=2}^{\\infty}{\\Join} \\\\\n&= \\sum_{i=2}^{\\infty}{\\frac{1}{i}}\n\\end{aligned}\n$$\n\n原本是不能渲染的,因为与 hexo 的渲染器有冲突,需要转义。\n我因为需要从以前的博客把文章转移过来觉得比较麻烦...\n于是魔改了一下主题,用上 mathjax 插件,能渲染了,感觉挺不错的。\n再改善一下推个 PR 吧。\n\n\n\n\n[^sub]: 这是脚注","title":"init-a-new-hexo-project","abstract":"最近使用 hexo 搭建了一个博客,并打算挂载在 github page 上。\n对之前的那个博客进行替代,并将之前的文章逐渐搬移过来。\n使用的[这个主题](https://github.com/Yue-plus/hexo-theme-arknights)功能还是比较完善的。","length":66,"created_at":"2021-12-12T20:09:13.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["Blog"],"license":false}},{"slug":"Handy-heap-cheat-sheet","file":"public/content/articles/2021-03-21-Handy-heap-cheat-sheet.md","mediaDir":"content/articles/2021-03-21-Handy-heap-cheat-sheet","path":"/articles/Handy-heap-cheat-sheet","meta":{"content":"\n# 如何手撕一个堆\n\n# 写在前面\n\n在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\n\n# 首先要理解,然后才能实现\n\n就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\n\n## 先抓住重点:堆是一种树结构\n\n首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\n\n再进一步,在堆这种树结构中,最重要的约束就是:**对于树中的每个节点,总有父节点大于两个子节点**(以大顶堆为例,下同)。\n\n如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\n\n而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\n\n1. 堆并不关注左右子树之间的大小情况,那么**要维护一个堆,基本只需要做交换父节点与子节点的操作**,而不需要像二叉查找树那样做各种旋转操作。\n2. 因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,**可以使用数组进行实现**。\n3. 因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:**堆的增删操作最坏时间复杂度为O(logn)**。\n\n## 再抓基本操作:上浮与下沉\n\n上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点**过大**,就跟他的父节点**向上交换**;若一个节点**过小**,就跟他的子节点**向下交换**。\n\n假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\n\n![p与g交换](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png)\n\n由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\n\n1. p > g > p2\n2. g > 原p > c1与c2\n\n则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\n\n同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\n\n为了简化,我们把前面那种递归地向上交换称为**上浮操作**,把后面这种递归地向下交换称为**下沉操作**。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\n\n# 各种接口的逻辑\n\n## 插入元素——入堆\n\n把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\n\n虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\n\n## 删除堆顶元素——出堆\n\n我们从堆中删除元素时,一般只会删除堆顶元素。\n\n删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\n\n这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\n\n## 堆的初始化——建堆\n\n建立一个堆,我们有两种思路:\n\n1. 将元素一个一个插入,即对每个元素都做一次入堆操作。\n2. 当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\n\n下面我们来分析这两种建堆策略。\n\n### 元素逐个入堆\n\n上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\n\n插入第i个元素时,堆的大小为$i$(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\n\n$$T(i) = logi$$\n\n那么把所有元素上浮,则总时间复杂度为:\n\n$$\n\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logi\\\\\n&= 1\\times0 + 2\\times1 + ... + 2^{logn}\\times{logn} \\\\\n&=O(nlogn)\n\\end{aligned}\n$$\n\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\n\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\n\n### 堆合并\n\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\n\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\n\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\n\n$$\nT(i) = logn-logi\n$$\n\n那么把所有元素下沉,则总时间复杂度为:\n\n$$\n\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logn-logi \\\\\n&= \\frac{n}{2^{logn}}\\times{logn}+ ... + \\frac{n}{4}\\times2+\\frac{n}{2}\\times1 \\\\\n&= O(n)\n\\end{aligned}\n$$\n\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\n\n### 两种策略的比较与理解\n\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\n\n1. 从元素移动路径的角度\n\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\n\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\n\n2. 从元素移动数量与移动距离的角度\n\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\n\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\n\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\n\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\n\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\n\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\n\n# 代码实现\n\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\n\n```python\nT = TypeVar(\"T\")\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n```\n\n## 实现树结构\n\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\n\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\n\n```python\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n```\n\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\n\n## 实现基本操作——上浮与下沉\n\n### 上浮\n\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\n\n```python\ndef floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n```\n\n### 下沉\n\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\n\n```python\ndef size(self):\n '''返回堆大小\n '''\n return len(self.A)\n\ndef sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n```\n\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\n\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\n\n## 实现各种借口——读、增、删、初始化\n\n### 读取堆顶\n\n堆一般只允许读取堆顶,即全堆最大元素。\n\n```python\ndef top(self):\n '''返回堆顶\n '''\n return self.A[0]\n```\n\n### 入堆\n\n入堆时,把元素加到堆尾,再做上浮操作。\n\n```python\ndef insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n```\n\n### 出堆\n\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\n\n```python\ndef pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n注意入堆与出堆操作都要保证堆的大小会相应变化。\n\n### 堆初始化\n\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\n\n```python\ndef __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n```\n\n## 整体代码\n\n### 堆的整体实现\n\n综上,堆的整体代码实现如下:\n\n```python\nfrom typing import Any, Callable, Generic, List, TypeVar\n\nT = TypeVar(\"T\")\n\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n def __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n \n def size(self):\n '''返回堆大小\n '''\n return len(self.A)\n \n def top(self):\n '''返回堆顶\n '''\n return self.A[0]\n \n def sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n\n def floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n \n def insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n\n def pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n### 单元测试\n\n入堆、出堆等操作的简单单元测试如下:\n\n```python\nimport pytest\nimport heap\n\n@pytest.fixture\ndef initHeap():\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \n lambda a,b:a>b)\n\nclass Test_TestHeap:\n def test_init_notNull(self, initHeap:heap.Heap):\n assert initHeap.size() == 10\n assert initHeap.top() == 9\n \n def test_insert_notTop(self, initHeap:heap.Heap):\n initHeap.insert(6)\n assert initHeap.size() == 11\n assert initHeap.top() == 9\n \n def test_insert_top(self, initHeap:heap.Heap):\n initHeap.insert(10)\n assert initHeap.size() == 11\n assert initHeap.top() == 10\n \n def test_pop(self, initHeap:heap.Heap):\n p = initHeap.pop()\n assert p == 9\n assert initHeap.size() == 9\n assert initHeap.top() == 8\n```\n\n# 关于堆排序\n\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\n\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\n\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\n\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。","title":"如何手撕一个堆","abstract":"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。","length":483,"created_at":"2021-08-28T23:09:14.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","算法竞赛"],"license":false}},{"slug":"The-beauty-of-design-parten","file":"public/content/articles/2021-08-21-The-beauty-of-design-parten.md","mediaDir":"content/articles/2021-08-21-The-beauty-of-design-parten","path":"/articles/The-beauty-of-design-parten","meta":{"content":"\n# 导读\n\n## 02:如何评价代码好坏?\n\n从7个方面评价代码的好坏:\n\n1. 易维护性:根本\n2. 可读性:最重要\n3. 易扩展性:对修改关闭,对扩展开放\n4. 灵活性\n5. 简洁性:KISS\n6. 可复用性:DRY\n7. 可测试性:TDD,单元测试,控制反转与依赖注入\n\n## 03:编程方法论\n\n设计模式之美这一课程不单止讲设计模式,而会讲包括设计模式在内的指导我们进行代码设计的方法论。包括以下5个方面:\n\n1. 面向对象:封装,抽象,继承,多态\n2. 设计原则:SOLID(单一职责,开闭原则,里氏替换,接口隔离,依赖倒置),DRY,KISS,YAGNI,LOD\n3. 设计模式\n4. 编程规范:可读性,命名规范\n5. 重构技巧:(目的,对象,时机,方法),保证手段(单元测试与可测性),两种规模\n\n整个课程会以编程方法论为纵轴,以代码好坏的评价为横轴,来讲提高代码质量的方法以及采用这种方法的原因。\n\n\n# 面向对象\n\n使用封装,抽象,继承,多态,作为代码设计和实现的基石。\n\n1. 面向对象分析(做什么),设计(怎么做),编程\n\n## 05:封装,抽象,继承,多态\n\n| | 是什么 | 怎么做 | 为什么 |\n| ---- | ---------------------- | ---------------------- | -------------------------------------------------------------- |\n| 封装 | 信息隐藏、数据访问保护 | 访问控制关键字 | 减少不可控因素、统一修改方式、保证可读性与可维护性、提高易用性 |\n| 抽象 | 隐藏实现方法 | 函数、接口类、抽象类 | 提高可扩展性与维护性、过滤非必要信息 |\n| 继承 | is-a关系 | 继承机制 | 代码复用、反映真实世界关系 |\n| 多态 | 子类替代父类 | 继承、接口类、鸭子类型 | 提高扩展性与复用性 |\n\n- 继承不应过度使用,会导致层次过深,导致低可读性与低可维护性\n- 在我看来,多态的本质与其说是子类替代父类,更应说是用同一个过程方法能适应多种不同类型的对象。\n- 有些观点认为,多态除了表中这三种实现方式以外,还有泛型的实现方式,被称为连接时多态。\n\n## 06,07:面向过程与面向对象\n\n1. 面向过程是:数据与方法分离\n2. 面向对象优势:适应大规模开发,代码组织更清晰;易复用、易扩展、易维护;人性化;\n3. 看似面向对象实际面向过程:滥用getter、setter破坏封装;滥用全局变量与全局方法,Constants类与Utils类;数据与方法分离,贫血模型;\n4. 为什么容易面向过程:略\n5. 面向过程的用处:略\n\n## 08:接口与抽象类\n\n1. 接口类与抽象类语法特性:略\n2. 抽象类表示is-a,为了解决代码复用。接口表示能做什么,为了解耦,隔离接口与实现。\n3. 应用场景区别:\n - 抽象类:代表is-a关系,解决代码复用问题\n - 接口类:解决抽象、解耦问题","title":"设计模式之美读书笔记","abstract":"从7个方面评价代码的好坏:\n1. 易维护性:根本\n2. 可读性:最重要","length":62,"created_at":"2021-08-21T08:53:27.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["设计模式","笔记"],"license":false}},{"slug":"Sort-algorithm","file":"public/content/articles/2021-01-11-Sort-algorithm.md","mediaDir":"content/articles/2021-01-11-Sort-algorithm","path":"/articles/Sort-algorithm","meta":{"content":"\n# 序言\n\n我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。\n\n你会发现堆排序被当成是选择排序的一种优化——因为我认为堆排序主要在于使用了堆这种数据结构,而总体思想与选择排序相比没有太大变化。\n\n你还会找到其他与别的文章不一样的地方。因为这篇文章是我按照自己的理解来写的,我脑子里是这样想的,那文章里就是这样写的。我会按照我的理解,从纵向与横向两个维度,来理清楚各个排序算法的特性与异同。\n\n# 整篇文章的要点\n\n整篇文章以纵向——算法分类、以及横向——算法评价两个维度来进行组织。\n\n排序算法可以按照以下方式来进行分类:\n\n- 基于比较的排序算法\n - 基于分治思想\n - 快速排序\n - 归并排序\n - 基于有序区域扩展\n - 插入排序\n - 选择排序\n- 不基于比较的排序算法\n - 计数排序\n - 桶排序\n - 基数排序\n\n文章中还会讲一讲为什么会这么分,每种分类有什么共性,分类之间有什么差异。此外,在最后还会稍微提一提外部排序与适用于并行运算的排序等。\n\n而对于纵向分类中的每一个端点,我们又会从以下五个方面,来对各个算法进行一个总体评价:\n\n- 时间复杂度(最坏,最好,平均※)\n- 空间复杂度\n- 是否原地排序\n- 是否稳定排序\n- 能否用于链表排序\n\n而由于复杂度主要只关注数量级,因此在这篇文章里会在不影响计算结果的前提下对复杂度计算进行适当的近似与简化。\n\n# 快排\n\n## 思想\n\n分治法:先把序列分为小的部分和大的部分,再将两部分分别排序。即:复杂分割,简单合并,主要操作在于分割。\n\n## 要点\n\n### 时间复杂度\n\n推导式:T(n) = T(找) + T(左) + T(右) = O(n) + 两个子问题时间复杂度。\n\n- 最好时间复杂度为每次都正好找到最中间的一个数时时间复杂度为O(nlogn)。证明略。\n- 最坏时间复杂度为每次都正好找到最旁边的数时时间复杂度为O(n^2)。证明略。\n- 平均时间复杂度为O(nlogn),推导式如下:\n\n 快速排序每一步中,将元素分为左右两边需要遍历整个列表,耗时T(n)。假设最后定位的元素为最终第i个元素,则两个子问题复杂度分别为T(i)和T(n-i-1)。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{\\sum_{i=0}^{n-1}{T(i)+T(n-i-1)}}{n} \\\\\n &=n + \\frac{2}{n}\\times\\sum_{i=0}^{n-1}{T(i)} \\\\\n \\end{aligned}\n $$\n\n 令 $$\\sum_{i=0}^{n}T(i) = Sum(n)$$ ,即有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{2}{n} \\times Sum(n-1) \\\\\n Sum(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1)\n \\end{aligned}\n $$\n\n 错位相减:\n\n $$\n \\begin{aligned}\n Sum(n) - Sum(n-1) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1) - \\frac{n}{2}T(n) + \\frac{n}{2}(n) \n \\\\\n T(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n}{2}T(n) - \\frac{2n+1}{2} \n \\\\\n \\frac{T(n+1)}{n+2} &= \\frac{T(n)}{n+1}+\\frac{2n+1}{(n+1)(n+2)} \n \\\\\n &= \\frac{T(n)}{n+1}+\\frac{1}{n}+\\frac{1}{n+1} \n \\\\\n &=...\n \\\\\n &= \\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+\\frac{1}{3}+...+\\frac{1}{n})+\\frac{1}{n+1}\n \\\\\n \\\\\n \\frac{T(n)}{n+1}&=\\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+...+\\frac{1}{n-1})+\\frac{1}{n}\n \\\\\n &= O(1)+O(1)+O(logn)+O(\\frac{1}{n})\n \\\\\n &= O(logn)\n \\end{aligned}\n $$\n\n 其中由于 $$\\frac{1}{x}=\\frac{d(logx)}{dx}$$ ,因此 $$\\frac{1}{2}+...+\\frac{1}{n-1}=O(logn)$$ 。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n)&=(n+1)\\times O(logn)\\\\\n &=O(n)\\times O(logn)\\\\\n &=O(nlogn)\n \\end{aligned}\n $$\n\n### 额外空间复杂度\n\n考虑栈深度,额外空间复杂度为O(logn)。由于快速排序主要步骤在于分,因此必须自上而下的进行递归,无法避免栈深度。\n\n### 原地排序\n\n虽然快速排序有额外空间复杂度,但并不妨碍它是一个原地排序。\n\n### 不稳定\n\n堆排序在分操作时将元素左右交换,会破坏稳定性。\n\n### 链表形式特点\n\n- 时间复杂度不变\n- 空间复杂度不变\n- 变为稳定排序※\n\n## 手写时的易错点\n\n- 分成左右子序列时最好完全分开(一边用`<=`一边用`>`),不然容易造成死循环。\n- 分左右子序列时仔细考虑最初下标位置与最终下标位置,以及对应位置的值的大小。\n- 不要忘记递归的结束条件。\n\n# 归并排序\n\n## 思想\n\n分治法:先把两个子序列各自排好序,然后再合并两个子序列。即:简单分割,复杂合并。主要步骤在于合并。\n\n## 要点\n\n- 时间复杂度推导式:T(n) = 2T(n/2) + T(合)\n- 平均、最好、最坏时间复杂度都是O(nlogn),推导过程略。\n- 额外空间复杂度为O(n),合并时必须准备额外空间。但由于主要步骤在于合并,可以自下而上地进行迭代合并,可以不使用栈。\n- 非原地排序\n- 稳定排序\n\n### 链表形式\n\n- 时间复杂度不变\n- 额外空间复杂度变为O(1)※\n- 稳定排序\n- 只能使用迭代形式,不能使用递归形式\n\n## 易错点\n\n- 合并时一边结束时另一边还未结束,需要把那一边也放入合并后序列中\n- 保持稳定排序:合并时左序列等于右序列时也采用左序列\n- 不要忘记递归结束条件\n- 不要忘记循环递进条件\n\n## 进阶\n\n- 原地归并排序:时间复杂度为O(log^2n),牺牲合并的时间复杂度进行原地排序。\n- 多路归并排序:使用竞标树,多路归并,用于磁盘IO。\n\n# 插入排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将无序序列当前元素插入有序序列,复杂度取决于有序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:基本有序情况下,O(n)\n- 额外空间复杂度:O(1)\n- 原地排序\n- 稳定排序\n- 每次插入时都要移动序列,写次数较多\n- 若查找插入位置时使用二分法查找,则可加快时间。(但不足以对时间复杂度造成影响,且最好时间复杂度也会上升为O(nlogn))\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 稳定排序\n- 插入时不再需要移动序列,但也不能使用二分法查找\n\n## 进阶——希尔排序\n\n- 要点:\n\n 插入排序的优点在于当序列基本有序时,时间复杂度可逼近为O(n)。\n\n 但插入时移动有序序列中元素所耗时间较多,而每次只移动一步。但实际上当序列分布均匀时,有序序列中排靠后的元素在整个序列中也会排靠后。\n\n 可以把序列分为几个大步长序列,在最初的几次插入放开移动步长,让大的元素直接移动到较后位置。再往后慢慢缩小步长,此时序列基本有序,可以利用基本有序时插入排序的优势。\n\n- 时间复杂度\n 1. 当步长为2^i时,不能使时间复杂度缩短为O(nlogn)。因为一个子序列所有元素有可能比另一个子序列最大元素都要大,这时插入排序仍需进行约n^2次操作\n 2. 当步长为2^i时效率较低,因为当步长为4已经有序时,步长为2再比较是无用比较。但由于1.的问题,不能节省比较时间。\n 3. 当步长之间最小公约数较少,甚至互质时,无用比较次数会降低。\n 4. 最坏时间复杂度下限为 $$O(nlog^2n)$$ (当步长采用 $$2^i3^j$$ 时),但一般希尔排序平均时间复杂度都为 $$O(n^{\\frac{3}{2}})$$\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序:希尔排序步长较大时会发生前后跳转。\n- 不能写为链表形式\n\n# 选择排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将选无序序列中最小元素加到有序序列末尾,复杂度取决于无序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:O(n2),如能保证无序部分的最小元素所在位置一定(堆排序),能降低时间复杂度\n- 额外空间复杂度:O(1)\n- 原地排序\n- 不稳定排序(采用元素交换策略时)\n- 每次找到最小元素后,只需交换一次位置即可,写次数较少。\n- 若找到最小元素后,不直接交换而是进行数组移动,则可进行稳定排序,但写次数变多,与插入排序相比没有优势,也不能使用二分查找进行简化。\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 变为稳定排序※(因为链表不需要数组移动,稳定排序方式的缺点得以消除)\n\n## 进阶——堆排序\n\n- 要点:\n\n 选择排序中耗时最多的是取出无序序列中最小值的时间,需要遍历整个无序序列。\n\n 但实际上我们只关心无序序列中的最小值,而不关心其他值的位置。通过将无序序列建为堆,减少选择时间,降低总的时间复杂度。\n\n- 时间复杂度O(nlogn)\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序\n- 操作时间复杂度:每次向下比较关注一个节点与其左右子堆顶元素,每次向上比较只关注节点与其父元素(大顶堆,堆大小为n)\n - 下沉:向下比较,若顶元素不是最大,将顶元素与较大的子堆堆顶元素交换。递归处理该子堆顶元素,直到向下比较顶元素最大。\n\n 最好时间复杂度O(1),最坏时间复杂度为O(h)=O(logn),平均O(logn)\n\n - 上浮:向上比较,若元素比其上层要大,交换该元素与其上层元素。递归处理其上层元素,直到向上比较不比上层要大。\n\n 最好时间复杂度O(1),最坏时间复杂度O(h)=O(logn),平均O(logn)\n\n - 入堆:堆扩容一位,将新元素插到尾部,将该元素上浮,最坏、平均时间复杂度O(logn)\n - 出堆:取出堆顶元素,将尾部放到堆顶,将该元素下沉,最坏、平均时间复杂度O(logn)\n - 缺点:通常堆尾元素较小,出堆时将堆尾元素放到堆顶再下沉基本要沉到堆底,无用比较较多\n- 建堆时间复杂度O(n)\n - 策略1:从头开始建堆,逐个元素插入,时间复杂度取决于最后一层,时间复杂度为O(nlogn)\n - 每次将堆扩容一位,将末尾元素上浮。\n - 时间复杂度推导:\n\n 每次插入时间:\n\n $$\n \\begin{aligned} T(i) &= h\\\\\n &= logi\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n}logi\\\\\n &= 1\\times1 + 2\\times2+ 3\\times4 + ...+h\\times \\frac{n}{2} \\\\\n \\\\\n 2T(n) &= 1\\times2 + 2\\times4+ 3\\times8 + ...+h\\times n \\\\\n \\\\\n 2T(n)-T(n) &= h\\times n - (1+2+4+...+2^{h-1}) \\\\\n &= h\\times n - O(2^h) \\\\\n \\\\\n T(n) &= nlogn - O(2^h) \\\\\n &= O(nlogn)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(nlogn)$$\n\n - 策略2:从后开始建堆,小堆合并(逐个元素下沉),时间复杂度为O(n)\n - 每次堆合并时,有三部分:左子堆,右子堆,顶元素。下沉顶元素。\n - 时间复杂度推导:\n\n 第i个元素合并时,时间为:\n\n $$\n \\begin{aligned}\n T(i) &= h_{子堆}\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n} (h_{子堆}) \\\\\n &= h + (h-1)\\times2 + ... + 2 \\times \\frac{n}{4} + 1 \\times\\frac{n}{2}\\\\\n \\\\\n 2T(n) &= h \\times 2 + (h-1) \\times 4 + ... + 2 \\times \\frac{n}{2} + n \\\\\n \\\\\n 2T(n) - T(n) &= n + \\frac{n}{2} + ... + 2 - h \\\\\n \\\\\n T(n) &= O(n) - h \\\\ \n &= O(n)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(n)$$ \n\n - 虽说每步是做一个小堆合并,但实际上从堆尾到堆头遍历,相当于仅关注元素没有稳定,相当于可以直接使用下沉操作。\n\n# 基于比较的排序算法时间复杂度下限:逆序对思想\n\n基于比较的排序算法可以看作序列逆序对的消除。完全随机序列逆序对数量为O(n^2),若一次元操作只消除一个逆序对,则时间复杂度不会低于O(n^2)。降低时间复杂度关键在于一次消除多个逆序对。\n\n1. 希尔排序通过增大最初的步长来企图一次消除多个逆序对。\n2. 归并排序消除逆序对最主要在于归并步骤。最后几次合并每个子步骤用O(1)时间消除O(n)个逆序对。\n3. 快速排序消除逆序对最主要在于划分步骤。每个划分步骤用O(n)时间消除O(n)+O(左长度×右长度)个逆序对。\n4. 堆排序逆序对消除方式比较Tricky,但可以看出消除逆序对大致在于出堆步骤,通过O(logn)时间复杂度消除O(n)个逆序对。(左小右大排序时需要建立左大右小的大顶堆,建堆时基本没有消除逆序对)\n\n# 最后\n\n这篇文章我们主要关注了排序算法中的大头——基于比较的排序算法。在下篇文章,我们再来看一下不基于比较的排序算法,以及外排序与并行排序。","title":"排序算法","abstract":"我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。","length":342,"created_at":"2021-01-11T22:57:10.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","排序"],"license":false}},{"slug":"python-dict","file":"public/content/articles/2020-08-02-python-dict.md","mediaDir":"content/articles/2020-08-02-python-dict","path":"/articles/python-dict","meta":{"content":"\n> CPython从3.6开始,字典(dict)不再是无序的了——字典的修改了原先的底层实现,变得能按字典插入的顺序进行遍历。而Python从3.7开始将字典的有序性写入语言特性,不管是Jython、IronPython还是其他Python实现,从3.7开始大家的字典都是有序的了。\n\n# 前言\n\n以前参加Python相关的面试时,面试官经常都会问一个问题:Python里的字典(dict)是有序的吗?\n\n这自然难不倒我,我也照本宣科地讲:Python的字典底层是用哈希表实现的,在不发生冲突时读写的时间复杂度是O(1),比读写时间复杂度为O(logn)的红黑树要更快。但红黑树可以按下标的大小顺序进行遍历,而Dict遍历时是无序的。\n\n我讲的时候没感觉到任何的违和感,估计面试官们也没觉得任何的不对。直到有一天,我查Python各个版本的新特性时,发现Python 3.6的What's New里有[这么一条](https://docs.python.org/3/whatsnew/3.6.html#new-dict-implementation):\n\n> New dict implementation\n> \n> The dict type now uses a “compact” representation based on a proposal by Raymond Hettinger which was first implemented by PyPy. The memory usage of the new dict() is between 20% and 25% smaller compared to Python 3.5.\n> \n> The order-preserving aspect of this new implementation is considered an implementation detail and should not be relied upon (this may change in the future, but it is desired to have this new dict implementation in the language for a few releases before changing the language spec to mandate order-preserving semantics for all current and future Python implementations; this also helps preserve backwards-compatibility with older versions of the language where random iteration order is still in effect, e.g. Python 3.5).\n\n啥情况?CPython的dict竟然优化了内存,还变有序了!?\n\n# Python 3.5 以前dict的实现\n\n先不着急看Python 3.6 里的dict,我们先来看看Python 3.5之前的dict是怎么实现的,再拿3.6来做对比。\n\n在Python 3.5以前,dict是用Hash表来实现的,而且Key和Value直接储存在Hash表上。想通过Key获取Value,只需通过Python内部的Hash函数计算出Key对应的Hash值,再映射到Hash表上对应的地址,访问该地址即可获取Key对应的Value。如下图所示:\n\n我们知道,Hash表读写时间复杂度在不发生冲突的情况下都是O(1)。\n\n为什么呢?我们可以把Hash表读写的步骤分开来看:\n\n1. 首先用Hash函数计算key的Hash值,Hash函数一般来说时间复杂度都是O(1)的。\n2. 计算出Hash值后,映射到Hash表内的数组下标,一般用取余数或是取二进制后几位的方式实现,时间复杂度也是O(1)。\n3. 然后用数组下标读取数组中实际储存的键值,数组的下标读取时间复杂度也是O(1)。\n\n这三个步骤串起来后复杂度并没有提升,总的时间复杂度自然也是O(1)的。\n\n而内部储存空间,Python字典中称为entries。entries相当于一个数组,是一段连续的内存空间,每个位置储存一个(Hash值,指向Key的指针,指向Value的指针)三元组。\n\n当然,由于抽屉原理,我们知道Hash表不可避免的会出现Hash冲突,Python的dict也不例外。\n\n而解决Hash冲突的方法有很多,比如C++的unordered_map和Go的map就用链地址法来解决冲突,用链表储存发生冲突的值。而Java更进一步,当链表长度超过8时就转换成红黑树,将链表O(n)的查找复杂度降为O(logn)。C#的HashTable则是用再散列法,内部有多个Hash函数,一次冲突了就换一个函数再算,直到不冲突为止。\n\n而Python的dict则是利用开放寻址法。当插入数据发生冲突时,就会从那个位置往后找,直到找到有空位的地址为止。要查的时候,也是把下标值映射到到地址后,先对比一下下标值相不相等,若不相等则往后继续对比。\n\n这也造成个问题,dict中的元素不能直接从entries中清理掉,不然往后寻找的查找链就会断掉了。只能是先标记住删除,等到一定时机再一并清理。\n\n此外我们也知道,当冲突过发生得过多,dict读写所需的时间也会变多,时间复杂度不再是O(1),这也是Hash表的通病了。\n\nPython中dict初始化时,内部储存空间entries容量为8。当内部储存空间占用到一定程度(entries容量×装填因子,Python的dict中装填因子是2/3)后,就会进行倍增扩容。每次扩容都要遍历原先的元素,时间复杂度为O(n),但基本上插入O(n)次之后才会进行一次扩容,所以扩容的均摊时间复杂度为O(1)。而扩容时会重新进行Hash值到entries位置的映射,此时就是把标记删除但仍留在entries中的元素清理掉的最佳时机。\n\nPython3.5之前这种dict的实现就有两个毛病:\n\n1. 元素的顺序不被记录。两个Key值通过Hash函数的出来的Hash值不一定能保证原来的大小关系,由于Hash冲突、扩容等影响元素的顺序也会变化。当然这种无序性也是Hash表通用的特点了。\n2. 占用了太多了无用空间。上面说到entries中每个位置储存一个(Hash值,指向Key的指针,指向Value的指针)三元组,没用到或是标记删除的位置占用了大量的空间。\n\n于是,Raymond Hettinger就提出了一种新的dict实现方式。在CPython3.6中就使用了这种新的实现方式。\n\n# CPython3.6中dict的实现\n\n当要实现一个如下的dict时:\n\n```python\nd = {\n 'timmy': 'red', \n 'barry': 'green', \n 'guido': 'blue'\n}\n```\n\n如在上一节中所讲,在Python3.5以前,在内存储存的形式可以表示成这样子:\n\n```python\nentries = [['--', '--', '--'],\n [-8522787127447073495, 'barry', 'green'],\n ['--', '--', '--'],\n ['--', '--', '--'],\n ['--', '--', '--'],\n [-9092791511155847987, 'timmy', 'red'],\n ['--', '--', '--'],\n [-6480567542315338377, 'guido', 'blue']]\n```\n\n而CPython3.6以后,是以这种形式储存在内存中的:\n\n```python\nindices = [None, 1, None, None, None, 0, None, 2]\nentries = [[-9092791511155847987, 'timmy', 'red'],\n [-8522787127447073495, 'barry', 'green'],\n [-6480567542315338377, 'guido', 'blue']]\n```\n\n改变了什么?\n\n1. dict内部的entries改为按插入顺序存储,新增了一个indices用于储存元素在entries中的下标。dict整体仍是Hash表结构,但Hash值映射到indices中,而不是直接映射到entries中。\n2. 由于entries改为了按插入顺序存储,使得申请entries容量时只要申请Hash表长度的2/3即可,省去了Hash表中的无用空间,储存更紧凑。\n3. dict读写步骤从原先的3步变为4步:计算key的Hash值,映射到indices内存空间,从indices读取entries的下标值,用下标从entries中读写数据。读写时间复杂度仍保持为O(1),冲突、删除标记等Hash表的特性也仍然存在。indices的扩容策略也仍然是倍增扩容,但因为填充因子仍然为2/3,entries每次扩容时只需申请indices长度的2/3即可。\n\n有什么好处?\n\n1. 压缩空间:原先Hash映射是直接映射到entries上,会有大量的空隙。现在Hash映射到indices上,而entries中可更紧凑地存储元素。而indices中储存的entries下标占用内存可以比entries元素要小得多——当entries长度足够短时每个下标只需占一个字节。indices中确实也还仍有空隙,但占用空间总要比旧的dict实现要小得多了。\n2. 更快的遍历:以前的实现遍历dict要遍历整个Hash表,需要挨个位置读取一下,判断它是空闲位置还是实际存在的元素。而现在只需要对变得更紧凑的entries遍历就行了。这也带来一个新的特性:entries是按照元素插入的顺序存储的,遍历entries自然也会按元素插入的顺序输出。这就给dict带来了有序性。\n3. 扩容时关注的内存块更少。原先的entries扩容时所有数据都要重新映射到内存上,cache利用率不好。现在扩容时基本可以整个entries直接复制(当然,有删除标记的数据这时要忽略)。\n\n综上,CPython3.6以后通过增加了一个indices增加了空间利用率,在维持读写时间复杂度不变的情况下增加了遍历与扩容效率。至于dict遍历变得有序,倒是有点次要的特性了。\n\n# 我们是否应利用新dict的有序性?\n\n既然Python中dict变得有序了,那我们是否应该主动去利用它呢?我是这么认为的:\n\n1. 在Python3.6中,我们不推荐利用dict的有序性。3.6时dict的有序性还只是CPython的一个实现细节,并不是Python的语言特性。当我们的代码不是在CPython环境下运行,dict的有序性就不起作用,就容易出莫名其妙的BUG了。\n2. 在Python3.7后,dict按插入顺序进行遍历的性质被写入Python语言特性中。这时确实在代码中利用dict有序性也没什么大问题。但dict这种数据结构,最主要的特性还是表现在Key映射到Value的这种关系,以及O(1)的读写时间复杂度。当我们的代码中需要关注到dict的遍历顺序时,我们就要先质问一下自己:是否应该改为用队列或是其他数据结构来实现?\n\n\n# 参考文献\n\n- [Are dictionaries ordered in Python 3.6+?](https://stackoverflow.com/questions/39980323/are-dictionaries-ordered-in-python-3-6)\n- [[Python-Dev] Python 3.6 dict becomes compact and gets a private version; and keywords become ordered](https://mail.python.org/pipermail/python-dev/2016-September/146327.html)\n- [[Python-Dev] More compact dictionaries with faster iteration](https://mail.python.org/pipermail/python-dev/2012-December/123028.html)\n- [关于python3.6中dict如何保证有序](https://zhuanlan.zhihu.com/p/36167600)\n- [python3.7源码分析-字典_小屋子大侠的博客-CSDN博客_python 字典源码](https://blog.csdn.net/qq_33339479/article/details/90446988)\n- [《深度剖析CPython解释器》9. 解密Python中字典和集合的底层实现,深度分析哈希表](https://www.cnblogs.com/traditional/p/13503114.html)\n- [CPython 源码阅读 - dict](http://blog.dreamfever.me/2018/03/12/cpython-yuan-ma-yue-du-dict/)","title":"Python字典的实现原理","abstract":"> CPython从3.6开始,字典(dict)不再是无序的了——字典的修改了原先的底层实现,变得能按字典插入的顺序进行遍历。而Python从3.7开始将字典的有序性写入语言特性,不管是Jython、IronPython还是其他Python实现,从3.7开始大家的字典都是有序的了。\n以前参加Python相关的面试时,面试官经常都会问一个问题:Python里的字典(dict)是有序的吗?\n这自然难不倒我,我也照本宣科地讲:Python的字典底层是用哈希表实现的,在不发生冲突时读写的时间复杂度是O(1),比读写时间复杂度为O(logn)的红黑树要更快。但红黑树可以按下标的大小顺序进行遍历,而Dict遍历时是无序的。","length":121,"created_at":"2020-08-02T00:10:10.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["Python","数据结构"],"license":false}},{"slug":"the-using-in-cpp","file":"public/content/articles/2020-01-28-the-using-in-cpp.md","mediaDir":"content/articles/2020-01-28-the-using-in-cpp","path":"/articles/the-using-in-cpp","meta":{"content":"\n## using的用法\n#### using与命名空间\n\n1. 引入整个命名空间中的成员\n \n 不引入命名空间时,使用其中变量需要使用`<命名空间名>::<变量名>`的方式使用。\n ```C++\n using namespace foo;\n ```\n 如此会将命名空间foo下所有的成员名称引入,可在直接以 `<变量名>` 的形式使用。但如此做有可能会使得命名空间foo中部分变量与当前定义的变量名冲突,违反命名空间隔离编译时名称冲突的初衷,因此不建议如此使用。\n\n2. 引入命名空间中的部分成员\n \n 可通过仅引入命名空间中部分的成员,避免命名冲突。\n ```C++\n using foo::bar;\n ```\n 这种方法仅会引入在语句中明确声明的名称。如using一个枚举类时,不会连其定义的枚举常量也一同引入。\n\n#### using与基类成员\n\n1. 子类中引入基类名称\n \n ```C++\n class Base {\n public:\n std::size_t size() const { return n; }\n protected:\n std::size_t n;\n };\n\n class Derived : private Base {\n public:\n using Base::size;\n protected:\n using Base::n;\n // ...\n };\n ```\n 例中子类private继承基类,由于private继承使得`Base::size`与`Base::n`可视性变为private。而使用`using Base::size`、`using Base::n`后,可分别使其变为public与protected。\n\n2. 子类成员函数与基类同名时保留基类函数用以重载\n \n ```C++\n class Base\n {\n public:\n int Func(){return 0;}\n };\n class Derived : Base\n {\n public:\n using Base::Func;\n int Func(int);\n };\n ```\n 子类中定义的成员函数与基类中重名时,即使函数原型不同,子类函数也会覆盖基类函数。\n \n 如果基类中定义了一个函数的多个重载,而子类中又重写或重定义了其中某些版本,或是定义了一个新的重载,则基类中该函数的所有重载均被隐藏。\n\n 此时可以在子类中使用`using Base::Func`,令基类中所有重载版本在子类中可见,再重定义需要更改的版本。\n\n又如cppreference中的[例子](https://en.cppreference.com/w/cpp/language/using_declaration#In_class_definition):\n```C++\n#include <iostream>\nstruct B {\n virtual void f(int) { std::cout << \"B::f\\n\"; }\n void g(char) { std::cout << \"B::g\\n\"; }\n void h(int) { std::cout << \"B::h\\n\"; }\nprotected:\n int m; // B::m is protected\n typedef int value_type;\n};\n\nstruct D : B {\n using B::m; // D::m is public\n using B::value_type; // D::value_type is public\n\n using B::f;\n void f(int) { std::cout << \"D::f\\n\"; } // D::f(int) overrides B::f(int)\n using B::g;\n void g(int) { std::cout << \"D::g\\n\"; } // both g(int) and g(char) are visible\n // as members of D\n using B::h;\n void h(int) { std::cout << \"D::h\\n\"; } // D::h(int) hides B::h(int)\n};\n\nint main()\n{\n D d;\n B& b = d;\n\n// b.m = 2; // error, B::m is protected\n d.m = 1; // protected B::m is accessible as public D::m\n b.f(1); // calls derived f()\n d.f(1); // calls derived f()\n d.g(1); // calls derived g(int)\n d.g('a'); // calls base g(char)\n b.h(1); // calls base h()\n d.h(1); // calls derived h()\n}\n```\n`using`语句可以改变基类成员的可访问性,也能在子类中重载(Overload)、重写(Override)基类的函数,或是通过重定义隐藏(Hide)对应的基类函数。\n\n\n#### using与别名\n\nusing在C++11开始,可用于别名的声明。用法如下:\n```C++\nusing UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;//普通别名\nusing FP = void (*) (int, const std::string&);//函数指针别名\n\ntemplate <typename T>\nusing Vec = MyVector<T, MyAlloc<T>>;//模板别名\nVec<int> vec;//模板别名的使用\n```\n\n## using关键字与typedef关键字定义别名的不同\n\n在STL容器或是其他泛型中若是再接受一个容器类型,类型名称就会写得很长。使用typedef或using定义别名会变得比较方便:\n```C++\ntypedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;\n\nusing UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;\n```\n\n对于函数指针,使用using语句可以把函数原型与别名强制分到左右两边,比使用typedef易读得多:\n```C++\ntypedef void (*FP) (int, const std::string&);\n\nusing FP = void (*) (int, const std::string&);\n```\n\n---\n\n在C++中,若试图使用typedef定义一个模板:\n```C++\ntemplate <typename T>\ntypedef MyVector<T, MyAlloc<T>> Vec;\n\n// usage\nVec<int> vec;\n```\n编译就会报错,提示:\n> error: a typedef cannot be a template\n\n在一些STL中,通过如下方式包装一层来使用:\n```C++\ntemplate <typename T>\nstruct Vec\n{\n typedef MyVector<T, MyAlloc<T>> type;\n};\n\n// usage\nVec<int>::type vec;\n```\n\n如此显得十分不美观,且要是在模板类中或参数传递时使用typename强制这为类型,而不是其他如静态成员等语法:\n```C++\ntemplate <typename T>\nclass Widget\n{\n typename Vec<T>::type vec;\n};\n```\n\n而using关键字可定义模板别名,则一切都会显得十分自然:\n```C++\ntemplate <typename T>\nusing Vec = MyVector<T, MyAlloc<T>>;\n\n// usage\nVec<int> vec;\n\n// in a class template\ntemplate <typename T>\nclass Widget\n{\n Vec<T> vec;\n};\n```\n\n---\n\n能做到类似别名功能的,还有宏#define。但#define运行在编译前的宏处理阶段,对代码进行字符串替换。没有类型检查或其他编译、链接阶段才能进行的检查,不具备安全性。在C++11中不提倡使用#define。\n\n\n\n \n ","title":"C++中using关键字的使用","abstract":"1. 引入整个命名空间中的成员\n 不引入命名空间时,使用其中变量需要使用`<命名空间名>::<变量名>`的方式使用。\n ```C++","length":192,"created_at":"2020-01-28T18:00:00.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["C++","杂技"],"license":false}},{"slug":"Building-this-blog","file":"public/content/articles/2020-01-27-Building-this-blog.md","mediaDir":"content/articles/2020-01-27-Building-this-blog","path":"/articles/Building-this-blog","meta":{"content":"\n> “Stop Trying to Reinvent the Wheel.”\n\n## 博客构建\n\n\n#### 把仓库clone到本地\n\n参考[BruceZhao][BruceZhao]提供的中文翻译:[README.zh.md][READMEzh],先将[Huxpro][Huxpro]提供的[博客模板仓库][origin_repo]fork出来,`git clone`到本地。\n\n整个网站文件夹大致结构如下:\n\n```\n├── _config.yml\n|\n├── _posts/\n| ├── 2007-10-29-awsome-file-name.md\n| └── 2009-04-26-stupid-file-name.md\n├── img/\n| ├── in-post/\n| ├── awsome-bg.jpg\n| ├── avatar-ryo.png\n| ├── favicon.ico\n| └── icon_wechat.jpg\n├── other_awsome_directory/\n| └── awsomefiles\n|\n|\n├── 404.html\n├── about.html\n├── index.html\n└── other_awsome_files\n```\n\n博客的文章上传到`_posts`文件夹中,网站中用到的图片上传到`img`文件夹中,网站的全局设置在`_config.yml`中进行。\n\n\n\n#### 修改**_config.yml**文件\n\n修改根目录下的`_config.yml`文件,将其中的内容更改为自己的信息。\n\n```yml\n# Site settings\ntitle: Ryo's Blog\nSEOTitle: 阿亮仔的博客 | 亮のブログ | Ryo's Blog\nheader-img: img/home-bg.jpg\nemail: qq250707340@163.com\ndescription: \"君の夢が 叶うのは 誰かのおかげじゃないぜ。\"\nkeyword: \"Ryo, Blog, 阿亮仔, りょう, 博客, ブログ, Algorithm, Unity, Python, C-Sharp\"\nurl: \"http://RyoJerryYu.github.io\" # your host, for absolute URL\nbaseurl: \"\" # for example, '/blog' if your blog hosted on 'host/blog'\ngithub_repo: \"https://github.com/RyoJerryYu/RyoJerryYu.github.io.git\" # you code repository\n```\n- `SEOTitle`: `<title>`标签,即显示在浏览器标题中的文字。\n- `header-img`: 首页显示的图像,可以把路径更改为自己的图片。\n- `description`: `<meta name=\"description\">`中的内容。\n- `keyword`: `<meta name=\"keyword\">`中的内容。\n- `url`, `baseurl`: 分别为博客域名地址与其下路径。如不想将博客直接架在根路径下,需要对`baseurl`进行设置。\n- `github_repo`: 博客所在的GitHub仓库。\n\n---\n\n\n```yml\n# SNS settings\nRSS: false\n# weibo_username: huxpro\n# zhihu_username: huxpro\ngithub_username: RyoJerryYu\ntwitter_username: ryo_okami\n# facebook_username: huxpro\n```\n分别为各个社交网站上的账号信息,以供在侧边栏中直接跳转到对应的页面。可通过在行首添加或删除`#`进行注释或取消注释。\n\n从[原仓库][origin_repo]中直接fork出来时,社交网站的图标可能会有[无法显示的问题](https://github.com/Huxpro/huxblog-boilerplate/issues/17),其解决方法在[后面](#FixSNS)介绍。\n\n---\n\n\n```yml\n# Disqus settings\n#disqus_username: _your_disqus_short_name_\n\n# Duoshuo settings\n# duoshuo_username: huxblog\n# Share component is depend on Comment so we can NOT use share only.\n# duoshuo_share: true # set to false if you want to use Comment without Sharing\n\n# Gitalk\ngitalk:\n enable: false #是否开启Gitalk评论\n clientID: f2c84e7629bb1446c1a4 #生成的clientID\n clientSecret: ca6d6139d1e1b8c43f8b2e19492ddcac8b322d0d #生成的clientSecret\n repo: qiubaiying.github.io #仓库名称\n owner: qiubaiying #github用户名\n admin: qiubaiying\n distractionFreeMode: true #是否启用类似FB的阴影遮罩 \n```\n分别为各种评论系统。均未开启。\n\n---\n\n\n```yml\n# Analytics settings\n# Baidu Analytics\n# ba_track_id: 4cc1f2d8f3067386cc5cdb626a202900\n# Google Analytics\nga_track_id: 'UA-156933256-1' # Format: UA-xxxxxx-xx\nga_domain: auto\n```\n分别为百度与谷歌的网站统计。我只启用了Google Analytics。可先到[Google Marketing Platform](https://marketingplatform.google.com/about/)注册,开启Google Analytics。在`设置`->`媒体资源设置`中获得Track ID,并填入`ga_track_id`中。\n\n---\n\n\n```yml\n# Sidebar settings\nsidebar: true # whether or not using Sidebar.\nsidebar-about-description: \"记录平时遇到的问题,以及对应的解决方法。偶尔上传些许宅活或是娱乐方面的记录。\"\nsidebar-avatar: /img/avatar-ryo.png # use absolute URL, seeing it's used in both `/` and `/about/`\n```\n`sidebar`: 是否开启侧边栏,为`true`或`false`。\n`sidebar-about-description`: 显示在侧边栏中的个人简介。\n`sidebar-avatar`: 显示在侧边栏中的头像。\n\n---\n\n\n```yml\n# Featured Tags\nfeatured-tags: true # whether or not using Feature-Tags\nfeatured-condition-size: 2 # A tag will be featured if the size of it is more than this condition value\n```\n是否开启tag功能,以及最少要达到多少篇文章才能使tag显示在首页上。\n\n\n\n#### 修改主页等信息\n\n修改`index.html`、`404.html`、`about.html`、`tags.html`等文件,将其中的内容更改为自己的信息。\n\n- 在`index`中,修改`description`对应的内容,亦即主页中标题下方的描述。\n- 在`404`、`tags`、`about`中,修改`description`的内容,亦即404页面中的描述信息。如有需要,也可以修改`header-img`,即404页面的图片地址。\n- 在`about`中,还有修改自我介绍对应的内容。\n\n\n\n#### 修改图片信息\n\n修改`img/`下的图片,替换为自己的图片。要记得替换以下图片:\n- `avatar-ryo.png`\n- `favicon.ico`\n- `icon_wechat.png`\n\n\n\n#### 修改README.md\n\nREADME.md为Github仓库的介绍,可以在README.md中写上这个博客主要的内容,让别人了解这个博客。\n\n\n\n#### 完成\n\n将`_posts`中的博文全部删除后,将本地文件全部push到GitHub仓库中。稍等后用浏览器浏览`<用户名>.github.io`(或是你在`_config.yml`中设定的路径)。若发现网页已更新,即博客搭建成功,可以开始写博文了。\n\n*然而,并没有成功。*\n\n\n\n## Fix Bug\n\n<p id = \"FixReadmeCh\"></p>\n\n#### 修复README.zh.md引发的错误\n\n按上述步骤搭建完毕后,网页并没有正常显示。此时GitHub账号所关联的邮箱中收到标题为**Page build failure**的邮件,内容如下:\n> The page build failed for the `master` branch with the following error:\n> The tag `if` on line 235 in `README.zh.md` was not properly closed.\n\n如[原仓库][origin_repo]中的[issue#11](https://github.com/Huxpro/huxblog-boilerplate/issues/11)所示,在`README.zh.md`中存在`if`语句,会触发错误。\n\n因并无其他特别的需求,此处采用暴力删除`README.zh.md`的方法解决。\n\n对应commit:[删除README.zh.md,尝试修复因...](https://github.com/RyoJerryYu/RyoJerryYu.GitHub.io/commit/098d710160775df9b6d2cf04d7d4eec526a67bf4)\n\n\n<p id = \"FixSNS\"></p>\n\n#### 修复SNS链接不正常显示\n\n修复上述错误后,稍等即可正常打开网页。但是,我们在`_config.yml`中设置好的SNS链接并没有在侧边栏以及网页底部正常显示。如原仓库中的[issue#17](https://github.com/Huxpro/huxblog-boilerplate/issues/17)所示,原因是gitpage必须通过https访问bootcss.com等的cdn。\n\n此处采用原仓库[pull request#21](https://github.com/Huxpro/huxblog-boilerplate/pull/21)的方法,修改`_includes/head.html`, `_includes/footer.html`, `_layouts/keynote.html`, `_layouts/post.html`文件,将其中`http`修改为`https`。\n\n对应commit:[fix: change http into https](https://github.com/RyoJerryYu/RyoJerryYu.GitHub.io/commit/ec954c380472f30f09efdfadd074cb7967c2fa11)\n\n\n\n## 上传文章\n\n文章主要放在_posts文件夹中,用`git push`的方式推送到GitHub仓库,即可完成文章上传。\n\n文章正文以**markdown**语法书写,在文本头部增加如下格式的信息:\n```\n---\nlayout: post\ntitle: \"Welcome to Ryo's Blog!\"\nsubtitle: \" \\\"Hello World, Hello Blog\\\"\"\ndate: 2020-01-27 12:00:00\nauthor: \"Ryo\"\nheader-img: \"img/post-bg-default.jpg\"\ntags:\n - 杂技\n - 杂谈\n---\n```\n其中:\n- `layout`为文章所用的模板,可选`post`或`keynote`,也可自己写一个模板html放在`_layouts`文件夹下。\n- `title`为文章标题,`subtitle`为文章副标题。\n- `date`为博客中显示的文章发表时间。\n- `author`为博客中显示的作者。\n- `header-img`为文章顶部显示的封面。\n- `tags`为文章的标签,我们的博客网站可以通过标签来快速寻找文章。\n\n把文章的文件名命名为时间+标题的形式,后缀名使用markdown文本的通用后缀名`md`,如`2020-01-27-hello-world.md`。完成后将此文本文件放到`_posts/`文件夹下。文章中使用到的图片建议放到`img/in-post/`文件夹下。\n\n完成后,使用`git push`推送到GitHub仓库,稍等后刷新博客网页即可看见刚才上传的文章。文章的url一般为:`<博客地址>/<文章文件名中的年>/<月>/<日>/<文件名中剩余部分>`。\n\n\n\n\n## 祝你开始愉快的博客生活。\n\n\n#### 感谢\n\n- [Huxpro][Huxpro]提供的博客模板:[huxblog-boilerplate][origin_repo]\n- [BruceZhao][BruceZhao]提供的中文翻译:[README.zh.md][READMEzh]\n- [Luo Yifan(罗一凡)](https://github.com/iVanlIsh)提供的Bug解决方案。\n\n\n\n\n[Huxpro]: https://github.com/huxpro\n[BruceZhao]: https://github.com/BruceZhaoR\n[origin_repo]: https://github.com/Huxpro/huxblog-boilerplate\n[READMEzh]: https://github.com/Huxpro/huxpro.github.io/blob/master/README.zh.md","title":"搭建博客的过程","abstract":"> “Stop Trying to Reinvent the Wheel.”\n参考[BruceZhao][BruceZhao]提供的中文翻译:[README.zh.md][READMEzh],先将[Huxpro][Huxpro]提供的[博客模板仓库][origin_repo]fork出来,`git clone`到本地。\n整个网站文件夹大致结构如下:","length":250,"created_at":"2020-01-27T14:00:00.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["杂技","Blog"],"license":false}},{"slug":"hello-world","file":"public/content/articles/2020-01-27-hello-world.md","mediaDir":"content/articles/2020-01-27-hello-world","path":"/articles/hello-world","meta":{"content":"\n> “Hello World!”\n\n## 这是我的第一篇博文\n\n自己盲人摸象折腾了一两天,终于利用GitHub Pages,把自己的博客搭好了。\n\n感谢[Huxpro][Huxpro]提供的博客模板,以及[BruceZhao][BruceZhao]编写的中文ReadMe。\n\n这个博客的使用流程:\n- 写作时利用**Markdown**语法书写,与日常编写GitHub上的文档相同。\n- 使用**Git Workflow**进行博客的更新。\n- 利用**GitHub Pages**提供的域名与免费空间,以及其支持的**Jekyll**进行网站搭建。\n\n我以后会利用这个博客,记录些许编程中遇到的问题。同时还有记录一下生活娱乐上的琐事。\n\n这第一篇博文主要用于测试一下博客是否运行成功,不打算写太多东西。今后有时间的话会记录一下搭建博客的过程。\n\n\n#### 感谢\n\n- [Huxpro][Huxpro]提供的博客模板:[huxblog-boilerplate](https://github.com/Huxpro/huxblog-boilerplate)\n- [BruceZhao][BruceZhao]提供的中文翻译:[README.zh.md](https://github.com/Huxpro/huxpro.github.io/blob/master/README.zh.md)\n\n\n\n\n[Huxpro]: https://github.com/huxpro\n[BruceZhao]: https://github.com/BruceZhaoR","title":"Welcome to Ryo's Blog!","abstract":"> “Hello World!”\n自己盲人摸象折腾了一两天,终于利用GitHub Pages,把自己的博客搭好了。\n感谢[Huxpro][Huxpro]提供的博客模板,以及[BruceZhao][BruceZhao]编写的中文ReadMe。","length":29,"created_at":"2020-01-27T12:00:00.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["杂技","杂谈"],"license":false}}],"allTagsList":[{"tag":"Kubernetes","slug":"kubernetes","path":"/tags/kubernetes","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"DevOps","slug":"devops","path":"/tags/devops","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Docker","slug":"docker","path":"/tags/docker","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Cloud Native","slug":"cloud-native","path":"/tags/cloud-native","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Blog","slug":"blog","path":"/tags/blog","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"init-a-new-hexo-project","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"VSCode","slug":"vscode","path":"/tags/vscode","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"Hexo","slug":"hexo","path":"/tags/hexo","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"JavaScript","slug":"javascript","path":"/tags/javascript","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"GitHub","slug":"github","path":"/tags/github","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"AWS","slug":"aws","path":"/tags/aws","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"CI/CD","slug":"ci-cd","path":"/tags/ci-cd","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"IaC","slug":"iac","path":"/tags/iac","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]},{"tag":"设计模式","slug":"设计模式","path":"/tags/设计模式","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"笔记","slug":"笔记","path":"/tags/笔记","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"排序","slug":"排序","path":"/tags/排序","postSlugs":[{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"Python","slug":"python","path":"/tags/python","postSlugs":[{"postSlug":"python-dict","postType":"article"}]},{"tag":"C++","slug":"c++","path":"/tags/c++","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"}]},{"tag":"杂技","slug":"杂技","path":"/tags/杂技","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"hello-world","postType":"article"}]},{"tag":"杂谈","slug":"杂谈","path":"/tags/杂谈","postSlugs":[{"postSlug":"hello-world","postType":"article"}]},{"tag":"Nextjs","slug":"nextjs","path":"/tags/nextjs","postSlugs":[{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"Cloud Computing","slug":"cloud-computing","path":"/tags/cloud-computing","postSlugs":[{"postSlug":"newest","postType":"idea"}]}]},"__N_SSG":true} \ No newline at end of file diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/Building-this-blog.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/Building-this-blog.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/Building-this-blog.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/Building-this-blog.json diff --git a/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/Handy-heap-cheat-sheet.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/Handy-heap-cheat-sheet.json new file mode 100644 index 00000000..1a84f949 --- /dev/null +++ b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/Handy-heap-cheat-sheet.json @@ -0,0 +1 @@ +{"pageProps":{"slug":"Handy-heap-cheat-sheet","tags":[{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]}],"source":{"compiledSource":"/*@jsxRuntime automatic @jsxImportSource react*/\nconst {Fragment: _Fragment, jsx: _jsx, jsxs: _jsxs} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = Object.assign({\n h1: \"h1\",\n a: \"a\",\n p: \"p\",\n h2: \"h2\",\n strong: \"strong\",\n ol: \"ol\",\n li: \"li\",\n img: \"img\",\n h3: \"h3\",\n span: \"span\",\n math: \"math\",\n semantics: \"semantics\",\n mrow: \"mrow\",\n mi: \"mi\",\n annotation: \"annotation\",\n mo: \"mo\",\n div: \"div\",\n mtable: \"mtable\",\n mtr: \"mtr\",\n mtd: \"mtd\",\n mstyle: \"mstyle\",\n munderover: \"munderover\",\n mn: \"mn\",\n msup: \"msup\",\n mfrac: \"mfrac\",\n pre: \"pre\",\n code: \"code\"\n }, _provideComponents(), props.components);\n return _jsxs(_Fragment, {\n children: [_jsx(_components.h1, {\n id: \"如何手撕一个堆\",\n children: _jsx(_components.a, {\n href: \"#如何手撕一个堆\",\n children: \"如何手撕一个堆\"\n })\n }), \"\\n\", _jsx(_components.h1, {\n id: \"写在前面\",\n children: _jsx(_components.a, {\n href: \"#写在前面\",\n children: \"写在前面\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\"\n }), \"\\n\", _jsx(_components.h1, {\n id: \"首先要理解然后才能实现\",\n children: _jsx(_components.a, {\n href: \"#首先要理解然后才能实现\",\n children: \"首先要理解,然后才能实现\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\"\n }), \"\\n\", _jsx(_components.h2, {\n id: \"先抓住重点堆是一种树结构\",\n children: _jsx(_components.a, {\n href: \"#先抓住重点堆是一种树结构\",\n children: \"先抓住重点:堆是一种树结构\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\"\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"再进一步,在堆这种树结构中,最重要的约束就是:\", _jsx(_components.strong, {\n children: \"对于树中的每个节点,总有父节点大于两个子节点\"\n }), \"(以大顶堆为例,下同)。\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\"\n }), \"\\n\", _jsxs(_components.ol, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [\"堆并不关注左右子树之间的大小情况,那么\", _jsx(_components.strong, {\n children: \"要维护一个堆,基本只需要做交换父节点与子节点的操作\"\n }), \",而不需要像二叉查找树那样做各种旋转操作。\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,\", _jsx(_components.strong, {\n children: \"可以使用数组进行实现\"\n }), \"。\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:\", _jsx(_components.strong, {\n children: \"堆的增删操作最坏时间复杂度为O(logn)\"\n }), \"。\"]\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.h2, {\n id: \"再抓基本操作上浮与下沉\",\n children: _jsx(_components.a, {\n href: \"#再抓基本操作上浮与下沉\",\n children: \"再抓基本操作:上浮与下沉\"\n })\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点\", _jsx(_components.strong, {\n children: \"过大\"\n }), \",就跟他的父节点\", _jsx(_components.strong, {\n children: \"向上交换\"\n }), \";若一个节点\", _jsx(_components.strong, {\n children: \"过小\"\n }), \",就跟他的子节点\", _jsx(_components.strong, {\n children: \"向下交换\"\n }), \"。\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\"\n }), \"\\n\", _jsx(_components.img, {\n src: \"/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png\",\n alt: \"p与g交换\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\"\n }), \"\\n\", _jsxs(_components.ol, {\n children: [\"\\n\", _jsx(_components.li, {\n children: \"p > g > p2\"\n }), \"\\n\", _jsx(_components.li, {\n children: \"g > 原p > c1与c2\"\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\"\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"为了简化,我们把前面那种递归地向上交换称为\", _jsx(_components.strong, {\n children: \"上浮操作\"\n }), \",把后面这种递归地向下交换称为\", _jsx(_components.strong, {\n children: \"下沉操作\"\n }), \"。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\"]\n }), \"\\n\", _jsx(_components.h1, {\n id: \"各种接口的逻辑\",\n children: _jsx(_components.a, {\n href: \"#各种接口的逻辑\",\n children: \"各种接口的逻辑\"\n })\n }), \"\\n\", _jsx(_components.h2, {\n id: \"插入元素入堆\",\n children: _jsx(_components.a, {\n href: \"#插入元素入堆\",\n children: \"插入元素——入堆\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\"\n }), \"\\n\", _jsx(_components.h2, {\n id: \"删除堆顶元素出堆\",\n children: _jsx(_components.a, {\n href: \"#删除堆顶元素出堆\",\n children: \"删除堆顶元素——出堆\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"我们从堆中删除元素时,一般只会删除堆顶元素。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\"\n }), \"\\n\", _jsx(_components.h2, {\n id: \"堆的初始化建堆\",\n children: _jsx(_components.a, {\n href: \"#堆的初始化建堆\",\n children: \"堆的初始化——建堆\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"建立一个堆,我们有两种思路:\"\n }), \"\\n\", _jsxs(_components.ol, {\n children: [\"\\n\", _jsx(_components.li, {\n children: \"将元素一个一个插入,即对每个元素都做一次入堆操作。\"\n }), \"\\n\", _jsx(_components.li, {\n children: \"当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\"\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"下面我们来分析这两种建堆策略。\"\n }), \"\\n\", _jsx(_components.h3, {\n id: \"元素逐个入堆\",\n children: _jsx(_components.a, {\n href: \"#元素逐个入堆\",\n children: \"元素逐个入堆\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\"\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"插入第i个元素时,堆的大小为\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsx(_components.mrow, {\n children: _jsx(_components.mi, {\n children: \"i\"\n })\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"i\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"0.6595em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n })]\n })\n })]\n })\n }), \"(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\"]\n }), \"\\n\", _jsx(_components.p, {\n children: _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"T\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"i\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n }), _jsx(_components.mo, {\n children: \"=\"\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"i\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"T(i) = logi\"\n })]\n })\n })\n }), _jsxs(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: [_jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.13889em\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mrel\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n })]\n }), _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"0.8889em\",\n verticalAlign: \"-0.1944em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n })]\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"那么把所有元素上浮,则总时间复杂度为:\"\n }), \"\\n\", _jsx(_components.div, {\n className: \"math math-display\",\n children: _jsx(_components.span, {\n className: \"katex-display\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n display: \"block\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mtable, {\n rowspacing: \"0.25em\",\n columnalign: \"right left\",\n columnspacing: \"0em\",\n children: [_jsxs(_components.mtr, {\n children: [_jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"T\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n })\n })\n }), _jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsxs(_components.mrow, {\n children: [_jsx(_components.mrow, {}), _jsx(_components.mo, {\n children: \"=\"\n }), _jsxs(_components.munderover, {\n children: [_jsx(_components.mo, {\n children: \"∑\"\n }), _jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"i\"\n }), _jsx(_components.mo, {\n children: \"=\"\n }), _jsx(_components.mn, {\n children: \"1\"\n })]\n }), _jsx(_components.mi, {\n children: \"n\"\n })]\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"i\"\n })]\n })\n })\n })]\n }), _jsxs(_components.mtr, {\n children: [_jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsx(_components.mrow, {})\n })\n }), _jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsxs(_components.mrow, {\n children: [_jsx(_components.mrow, {}), _jsx(_components.mo, {\n children: \"=\"\n }), _jsx(_components.mn, {\n children: \"1\"\n }), _jsx(_components.mo, {\n children: \"×\"\n }), _jsx(_components.mn, {\n children: \"0\"\n }), _jsx(_components.mo, {\n children: \"+\"\n }), _jsx(_components.mn, {\n children: \"2\"\n }), _jsx(_components.mo, {\n children: \"×\"\n }), _jsx(_components.mn, {\n children: \"1\"\n }), _jsx(_components.mo, {\n children: \"+\"\n }), _jsx(_components.mi, {\n mathvariant: \"normal\",\n children: \".\"\n }), _jsx(_components.mi, {\n mathvariant: \"normal\",\n children: \".\"\n }), _jsx(_components.mi, {\n mathvariant: \"normal\",\n children: \".\"\n }), _jsx(_components.mo, {\n children: \"+\"\n }), _jsxs(_components.msup, {\n children: [_jsx(_components.mn, {\n children: \"2\"\n }), _jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n })]\n })]\n }), _jsx(_components.mo, {\n children: \"×\"\n }), _jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n })]\n })]\n })\n })\n })]\n }), _jsxs(_components.mtr, {\n children: [_jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsx(_components.mrow, {})\n })\n }), _jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsxs(_components.mrow, {\n children: [_jsx(_components.mrow, {}), _jsx(_components.mo, {\n children: \"=\"\n }), _jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n })\n })\n })]\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"\\\\begin{aligned}\\nT(n) &= \\\\sum_{i=1}^{n}logi\\\\\\\\\\n&= 1\\\\times0 + 2\\\\times1 + ... + 2^{logn}\\\\times{logn} \\\\\\\\\\n&=O(nlogn)\\n\\\\end{aligned}\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"6.2882em\",\n verticalAlign: \"-2.8941em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: _jsxs(_components.span, {\n className: \"mtable\",\n children: [_jsx(_components.span, {\n className: \"col-align-r\",\n children: _jsxs(_components.span, {\n className: \"vlist-t vlist-t2\",\n children: [_jsxs(_components.span, {\n className: \"vlist-r\",\n children: [_jsxs(_components.span, {\n className: \"vlist\",\n style: {\n height: \"3.3941em\"\n },\n children: [_jsxs(_components.span, {\n style: {\n top: \"-5.3941em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.13889em\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-2.9173em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\"\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-1.4173em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\"\n })]\n })]\n }), _jsx(_components.span, {\n className: \"vlist-s\",\n children: \"​\"\n })]\n }), _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"2.8941em\"\n },\n children: _jsx(_components.span, {})\n })\n })]\n })\n }), _jsx(_components.span, {\n className: \"col-align-l\",\n children: _jsxs(_components.span, {\n className: \"vlist-t vlist-t2\",\n children: [_jsxs(_components.span, {\n className: \"vlist-r\",\n children: [_jsxs(_components.span, {\n className: \"vlist\",\n style: {\n height: \"3.3941em\"\n },\n children: [_jsxs(_components.span, {\n style: {\n top: \"-5.3941em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mrel\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mop op-limits\",\n children: _jsxs(_components.span, {\n className: \"vlist-t vlist-t2\",\n children: [_jsxs(_components.span, {\n className: \"vlist-r\",\n children: [_jsxs(_components.span, {\n className: \"vlist\",\n style: {\n height: \"1.6514em\"\n },\n children: [_jsxs(_components.span, {\n style: {\n top: \"-1.8723em\",\n marginLeft: \"0em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.05em\"\n }\n }), _jsx(_components.span, {\n className: \"sizing reset-size6 size3 mtight\",\n children: _jsxs(_components.span, {\n className: \"mord mtight\",\n children: [_jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n children: \"i\"\n }), _jsx(_components.span, {\n className: \"mrel mtight\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mord mtight\",\n children: \"1\"\n })]\n })\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-3.05em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.05em\"\n }\n }), _jsx(_components.span, {\n children: _jsx(_components.span, {\n className: \"mop op-symbol large-op\",\n children: \"∑\"\n })\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-4.3em\",\n marginLeft: \"0em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.05em\"\n }\n }), _jsx(_components.span, {\n className: \"sizing reset-size6 size3 mtight\",\n children: _jsx(_components.span, {\n className: \"mord mtight\",\n children: _jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n children: \"n\"\n })\n })\n })]\n })]\n }), _jsx(_components.span, {\n className: \"vlist-s\",\n children: \"​\"\n })]\n }), _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"1.2777em\"\n },\n children: _jsx(_components.span, {})\n })\n })]\n })\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.1667em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n })]\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-2.9173em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mrel\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: \"1\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"×\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: \"0\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"+\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: \"2\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"×\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: \"1\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"+\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: \"...\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"+\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord\",\n children: \"2\"\n }), _jsx(_components.span, {\n className: \"msupsub\",\n children: _jsx(_components.span, {\n className: \"vlist-t\",\n children: _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"0.8991em\"\n },\n children: _jsxs(_components.span, {\n style: {\n top: \"-3.113em\",\n marginRight: \"0.05em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"2.7em\"\n }\n }), _jsx(_components.span, {\n className: \"sizing reset-size6 size3 mtight\",\n children: _jsxs(_components.span, {\n className: \"mord mtight\",\n children: [_jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n children: \"n\"\n })]\n })\n })]\n })\n })\n })\n })\n })]\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"×\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n })]\n })]\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-1.4173em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mrel\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })]\n })]\n }), _jsx(_components.span, {\n className: \"vlist-s\",\n children: \"​\"\n })]\n }), _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"2.8941em\"\n },\n children: _jsx(_components.span, {})\n })\n })]\n })\n })]\n })\n })]\n })\n })]\n })\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\"\n }), \"\\n\", _jsx(_components.img, {\n src: \"/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png\",\n alt: \"\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\"\n }), \"\\n\", _jsx(_components.h3, {\n id: \"堆合并\",\n children: _jsx(_components.a, {\n href: \"#堆合并\",\n children: \"堆合并\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\"\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n children: \"−\"\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"i\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"logn-logi\"\n })]\n })\n })\n }), _jsxs(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: [_jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"0.8889em\",\n verticalAlign: \"-0.1944em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"−\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n })]\n }), _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"0.8889em\",\n verticalAlign: \"-0.1944em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n })]\n })]\n })]\n })\n }), \",则有下沉时间复杂度为:\"]\n }), \"\\n\", _jsx(_components.div, {\n className: \"math math-display\",\n children: _jsx(_components.span, {\n className: \"katex-display\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n display: \"block\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"T\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"i\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n }), _jsx(_components.mo, {\n children: \"=\"\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n children: \"−\"\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"i\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"T(i) = logn-logi\"\n })]\n })\n })\n }), _jsxs(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: [_jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.13889em\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mrel\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n })]\n }), _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"0.8889em\",\n verticalAlign: \"-0.1944em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"−\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n })]\n }), _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"0.8889em\",\n verticalAlign: \"-0.1944em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n })]\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"那么把所有元素下沉,则总时间复杂度为:\"\n }), \"\\n\", _jsx(_components.div, {\n className: \"math math-display\",\n children: _jsx(_components.span, {\n className: \"katex-display\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n display: \"block\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mtable, {\n rowspacing: \"0.25em\",\n columnalign: \"right left\",\n columnspacing: \"0em\",\n children: [_jsxs(_components.mtr, {\n children: [_jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"T\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n })\n })\n }), _jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsxs(_components.mrow, {\n children: [_jsx(_components.mrow, {}), _jsx(_components.mo, {\n children: \"=\"\n }), _jsxs(_components.munderover, {\n children: [_jsx(_components.mo, {\n children: \"∑\"\n }), _jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"i\"\n }), _jsx(_components.mo, {\n children: \"=\"\n }), _jsx(_components.mn, {\n children: \"1\"\n })]\n }), _jsx(_components.mi, {\n children: \"n\"\n })]\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n children: \"−\"\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"i\"\n })]\n })\n })\n })]\n }), _jsxs(_components.mtr, {\n children: [_jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsx(_components.mrow, {})\n })\n }), _jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsxs(_components.mrow, {\n children: [_jsx(_components.mrow, {}), _jsx(_components.mo, {\n children: \"=\"\n }), _jsxs(_components.mfrac, {\n children: [_jsx(_components.mi, {\n children: \"n\"\n }), _jsxs(_components.msup, {\n children: [_jsx(_components.mn, {\n children: \"2\"\n }), _jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n })]\n })]\n })]\n }), _jsx(_components.mo, {\n children: \"×\"\n }), _jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n })]\n }), _jsx(_components.mo, {\n children: \"+\"\n }), _jsx(_components.mi, {\n mathvariant: \"normal\",\n children: \".\"\n }), _jsx(_components.mi, {\n mathvariant: \"normal\",\n children: \".\"\n }), _jsx(_components.mi, {\n mathvariant: \"normal\",\n children: \".\"\n }), _jsx(_components.mo, {\n children: \"+\"\n }), _jsxs(_components.mfrac, {\n children: [_jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mn, {\n children: \"4\"\n })]\n }), _jsx(_components.mo, {\n children: \"×\"\n }), _jsx(_components.mn, {\n children: \"2\"\n }), _jsx(_components.mo, {\n children: \"+\"\n }), _jsxs(_components.mfrac, {\n children: [_jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mn, {\n children: \"2\"\n })]\n }), _jsx(_components.mo, {\n children: \"×\"\n }), _jsx(_components.mn, {\n children: \"1\"\n })]\n })\n })\n })]\n }), _jsxs(_components.mtr, {\n children: [_jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsx(_components.mrow, {})\n })\n }), _jsx(_components.mtd, {\n children: _jsx(_components.mstyle, {\n scriptlevel: \"0\",\n displaystyle: \"true\",\n children: _jsxs(_components.mrow, {\n children: [_jsx(_components.mrow, {}), _jsx(_components.mo, {\n children: \"=\"\n }), _jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n })\n })\n })]\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"\\\\begin{aligned}\\nT(n) &= \\\\sum_{i=1}^{n}logn-logi \\\\\\\\\\n&= \\\\frac{n}{2^{logn}}\\\\times{logn}+ ... + \\\\frac{n}{4}\\\\times2+\\\\frac{n}{2}\\\\times1 \\\\\\\\\\n&= O(n)\\n\\\\end{aligned}\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"6.8226em\",\n verticalAlign: \"-3.1613em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: _jsxs(_components.span, {\n className: \"mtable\",\n children: [_jsx(_components.span, {\n className: \"col-align-r\",\n children: _jsxs(_components.span, {\n className: \"vlist-t vlist-t2\",\n children: [_jsxs(_components.span, {\n className: \"vlist-r\",\n children: [_jsxs(_components.span, {\n className: \"vlist\",\n style: {\n height: \"3.6613em\"\n },\n children: [_jsxs(_components.span, {\n style: {\n top: \"-5.6613em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.13889em\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-2.9761em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\"\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-1.1501em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\"\n })]\n })]\n }), _jsx(_components.span, {\n className: \"vlist-s\",\n children: \"​\"\n })]\n }), _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"3.1613em\"\n },\n children: _jsx(_components.span, {})\n })\n })]\n })\n }), _jsx(_components.span, {\n className: \"col-align-l\",\n children: _jsxs(_components.span, {\n className: \"vlist-t vlist-t2\",\n children: [_jsxs(_components.span, {\n className: \"vlist-r\",\n children: [_jsxs(_components.span, {\n className: \"vlist\",\n style: {\n height: \"3.6613em\"\n },\n children: [_jsxs(_components.span, {\n style: {\n top: \"-5.6613em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mrel\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mop op-limits\",\n children: _jsxs(_components.span, {\n className: \"vlist-t vlist-t2\",\n children: [_jsxs(_components.span, {\n className: \"vlist-r\",\n children: [_jsxs(_components.span, {\n className: \"vlist\",\n style: {\n height: \"1.6514em\"\n },\n children: [_jsxs(_components.span, {\n style: {\n top: \"-1.8723em\",\n marginLeft: \"0em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.05em\"\n }\n }), _jsx(_components.span, {\n className: \"sizing reset-size6 size3 mtight\",\n children: _jsxs(_components.span, {\n className: \"mord mtight\",\n children: [_jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n children: \"i\"\n }), _jsx(_components.span, {\n className: \"mrel mtight\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mord mtight\",\n children: \"1\"\n })]\n })\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-3.05em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.05em\"\n }\n }), _jsx(_components.span, {\n children: _jsx(_components.span, {\n className: \"mop op-symbol large-op\",\n children: \"∑\"\n })\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-4.3em\",\n marginLeft: \"0em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.05em\"\n }\n }), _jsx(_components.span, {\n className: \"sizing reset-size6 size3 mtight\",\n children: _jsx(_components.span, {\n className: \"mord mtight\",\n children: _jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n children: \"n\"\n })\n })\n })]\n })]\n }), _jsx(_components.span, {\n className: \"vlist-s\",\n children: \"​\"\n })]\n }), _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"1.2777em\"\n },\n children: _jsx(_components.span, {})\n })\n })]\n })\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.1667em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"−\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n })]\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-2.9761em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mrel\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mopen nulldelimiter\"\n }), _jsx(_components.span, {\n className: \"mfrac\",\n children: _jsxs(_components.span, {\n className: \"vlist-t vlist-t2\",\n children: [_jsxs(_components.span, {\n className: \"vlist-r\",\n children: [_jsxs(_components.span, {\n className: \"vlist\",\n style: {\n height: \"1.1076em\"\n },\n children: [_jsxs(_components.span, {\n style: {\n top: \"-2.314em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord\",\n children: \"2\"\n }), _jsx(_components.span, {\n className: \"msupsub\",\n children: _jsx(_components.span, {\n className: \"vlist-t\",\n children: _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"0.7751em\"\n },\n children: _jsxs(_components.span, {\n style: {\n top: \"-2.989em\",\n marginRight: \"0.05em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"2.7em\"\n }\n }), _jsx(_components.span, {\n className: \"sizing reset-size6 size3 mtight\",\n children: _jsxs(_components.span, {\n className: \"mord mtight\",\n children: [_jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal mtight\",\n children: \"n\"\n })]\n })\n })]\n })\n })\n })\n })\n })]\n })\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-3.23em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3em\"\n }\n }), _jsx(_components.span, {\n className: \"frac-line\",\n style: {\n borderBottomWidth: \"0.04em\"\n }\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-3.677em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n })\n })]\n })]\n }), _jsx(_components.span, {\n className: \"vlist-s\",\n children: \"​\"\n })]\n }), _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"0.686em\"\n },\n children: _jsx(_components.span, {})\n })\n })]\n })\n }), _jsx(_components.span, {\n className: \"mclose nulldelimiter\"\n })]\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"×\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n })]\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"+\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: \"...\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"+\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mopen nulldelimiter\"\n }), _jsx(_components.span, {\n className: \"mfrac\",\n children: _jsxs(_components.span, {\n className: \"vlist-t vlist-t2\",\n children: [_jsxs(_components.span, {\n className: \"vlist-r\",\n children: [_jsxs(_components.span, {\n className: \"vlist\",\n style: {\n height: \"1.1076em\"\n },\n children: [_jsxs(_components.span, {\n style: {\n top: \"-2.314em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: _jsx(_components.span, {\n className: \"mord\",\n children: \"4\"\n })\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-3.23em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3em\"\n }\n }), _jsx(_components.span, {\n className: \"frac-line\",\n style: {\n borderBottomWidth: \"0.04em\"\n }\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-3.677em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n })\n })]\n })]\n }), _jsx(_components.span, {\n className: \"vlist-s\",\n children: \"​\"\n })]\n }), _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"0.686em\"\n },\n children: _jsx(_components.span, {})\n })\n })]\n })\n }), _jsx(_components.span, {\n className: \"mclose nulldelimiter\"\n })]\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"×\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: \"2\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"+\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mopen nulldelimiter\"\n }), _jsx(_components.span, {\n className: \"mfrac\",\n children: _jsxs(_components.span, {\n className: \"vlist-t vlist-t2\",\n children: [_jsxs(_components.span, {\n className: \"vlist-r\",\n children: [_jsxs(_components.span, {\n className: \"vlist\",\n style: {\n height: \"1.1076em\"\n },\n children: [_jsxs(_components.span, {\n style: {\n top: \"-2.314em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: _jsx(_components.span, {\n className: \"mord\",\n children: \"2\"\n })\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-3.23em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3em\"\n }\n }), _jsx(_components.span, {\n className: \"frac-line\",\n style: {\n borderBottomWidth: \"0.04em\"\n }\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-3.677em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n })\n })]\n })]\n }), _jsx(_components.span, {\n className: \"vlist-s\",\n children: \"​\"\n })]\n }), _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"0.686em\"\n },\n children: _jsx(_components.span, {})\n })\n })]\n })\n }), _jsx(_components.span, {\n className: \"mclose nulldelimiter\"\n })]\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mbin\",\n children: \"×\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2222em\"\n }\n }), _jsx(_components.span, {\n className: \"mord\",\n children: \"1\"\n })]\n })]\n }), _jsxs(_components.span, {\n style: {\n top: \"-1.1501em\"\n },\n children: [_jsx(_components.span, {\n className: \"pstrut\",\n style: {\n height: \"3.6514em\"\n }\n }), _jsxs(_components.span, {\n className: \"mord\",\n children: [_jsx(_components.span, {\n className: \"mord\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mrel\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })]\n })]\n }), _jsx(_components.span, {\n className: \"vlist-s\",\n children: \"​\"\n })]\n }), _jsx(_components.span, {\n className: \"vlist-r\",\n children: _jsx(_components.span, {\n className: \"vlist\",\n style: {\n height: \"3.1613em\"\n },\n children: _jsx(_components.span, {})\n })\n })]\n })\n })]\n })\n })]\n })\n })]\n })\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\"\n }), \"\\n\", _jsx(_components.img, {\n src: \"/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png\",\n alt: \"\"\n }), \"\\n\", _jsx(_components.h3, {\n id: \"两种策略的比较与理解\",\n children: _jsx(_components.a, {\n href: \"#两种策略的比较与理解\",\n children: \"两种策略的比较与理解\"\n })\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"逐个元素入堆的策略时间复杂度为\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"O(logn)\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })\n })]\n })\n }), \",堆合并策略的时间复杂度为\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"O(n)\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })\n })]\n })\n }), \",为什么会出现差异呢?我们可以从两个角度来理解:\"]\n }), \"\\n\", _jsxs(_components.ol, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [\"\\n\", _jsx(_components.p, {\n children: \"从元素移动路径的角度\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\"\n }), \"\\n\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"\\n\", _jsx(_components.p, {\n children: \"从元素移动数量与移动距离的角度\"\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此\", _jsx(_components.strong, {\n children: \"建堆的时间复杂度主要取决于底层元素\"\n }), \"的移动距离。\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"O(n)\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })\n })]\n })\n }), \"个元素需要移动\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"O(logn)\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })\n })]\n })\n }), \"的距离,因此时间复杂度较高。\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"O(n)\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })\n })]\n })\n }), \"。\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\"\n }), \"\\n\", _jsx(_components.img, {\n src: \"/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png\",\n alt: \"\"\n }), \"\\n\"]\n }), \"\\n\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"O(n)\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })\n })]\n })\n }), \"。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\"]\n }), \"\\n\", _jsx(_components.h1, {\n id: \"代码实现\",\n children: _jsx(_components.a, {\n href: \"#代码实现\",\n children: \"代码实现\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\"\n }), \"\\n\", _jsx(_components.div, {\n \"data-rehype-pretty-code-fragment\": \"\",\n children: _jsx(_components.pre, {\n style: {\n backgroundColor: \"#232136\"\n },\n tabIndex: \"0\",\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: _jsxs(_components.code, {\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: [_jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" TypeVar\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"\\\"T\\\"\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"class\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"Heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"Generic\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''堆结构\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 有两个成员:\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" self.fCompare: Callable[[T,T],bool] # 比较函数\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 下面假设堆为大顶堆\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 即有self.fCompare = lambda a,b: a>b\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n })]\n })\n })\n }), \"\\n\", _jsx(_components.h2, {\n id: \"实现树结构\",\n children: _jsx(_components.a, {\n href: \"#实现树结构\",\n children: \"实现树结构\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\"\n }), \"\\n\", _jsx(_components.div, {\n \"data-rehype-pretty-code-fragment\": \"\",\n children: _jsx(_components.pre, {\n style: {\n backgroundColor: \"#232136\"\n },\n tabIndex: \"0\",\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: _jsxs(_components.code, {\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: [_jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"lfChildOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"int\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"+\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"<<\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"-\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"rtChildOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"int\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"+\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"<<\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"parentOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"int\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"-\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \">>\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\"\n }), \"\\n\", _jsx(_components.img, {\n src: \"/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png\",\n alt: \"\"\n }), \"\\n\", _jsx(_components.h2, {\n id: \"实现基本操作上浮与下沉\",\n children: _jsx(_components.a, {\n href: \"#实现基本操作上浮与下沉\",\n children: \"实现基本操作——上浮与下沉\"\n })\n }), \"\\n\", _jsx(_components.h3, {\n id: \"上浮\",\n children: _jsx(_components.a, {\n href: \"#上浮\",\n children: \"上浮\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\"\n }), \"\\n\", _jsx(_components.div, {\n \"data-rehype-pretty-code-fragment\": \"\",\n children: _jsx(_components.pre, {\n style: {\n backgroundColor: \"#232136\"\n },\n tabIndex: \"0\",\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: _jsxs(_components.code, {\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: [_jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"floatUp\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"int\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''上浮操作\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 对下标为i的元素递归地进行上浮操作\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 直到该元素小于其父节点或该元素上浮到根节点\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 元素i上浮到根节点时结束递归\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"if\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" i \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"<=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 当元素i小于其父节点时符合堆结构,结束递归\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" pr \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" parentOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"if\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"fCompare\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pr\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 元素i大于其父节点,交换i与其父节点并继续上浮\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pr\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pr\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"floatUp\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pr\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.h3, {\n id: \"下沉\",\n children: _jsx(_components.a, {\n href: \"#下沉\",\n children: \"下沉\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\"\n }), \"\\n\", _jsx(_components.div, {\n \"data-rehype-pretty-code-fragment\": \"\",\n children: _jsx(_components.pre, {\n style: {\n backgroundColor: \"#232136\"\n },\n tabIndex: \"0\",\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: _jsxs(_components.code, {\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: [_jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"size\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''返回堆大小\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"len\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"sinkDown\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"int\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''下沉操作\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 对下标为i的元素递归地进行下沉操作\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 直到该元素大于其两个子节点或该元素下沉到叶子节点\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" lc \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" lfChildOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" rc \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" rtChildOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" larger \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" i\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"if\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" lc \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"<\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"size\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"and\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"fCompare\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"lc\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"larger\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" larger \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" lc\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"if\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" rc \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"<\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"size\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"and\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"fCompare\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"rc\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"larger\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" larger \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" rc\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 当元素i大于其两个子节点时符合堆结构,结束递归\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"if\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" larger \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"larger\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"larger\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"sinkDown\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"larger\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n })]\n })\n })\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mn, {\n children: \"1\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"O(1)\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord\",\n children: \"1\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })\n })]\n })\n }), \"。\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mn, {\n children: \"1\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"O(1)\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord\",\n children: \"1\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })\n })]\n })\n }), \"。要改成迭代实现并不困难,还请大家尝试自己实现。\"]\n }), \"\\n\", _jsx(_components.h2, {\n id: \"实现各种借口读增删初始化\",\n children: _jsx(_components.a, {\n href: \"#实现各种借口读增删初始化\",\n children: \"实现各种借口——读、增、删、初始化\"\n })\n }), \"\\n\", _jsx(_components.h3, {\n id: \"读取堆顶\",\n children: _jsx(_components.a, {\n href: \"#读取堆顶\",\n children: \"读取堆顶\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"堆一般只允许读取堆顶,即全堆最大元素。\"\n }), \"\\n\", _jsx(_components.div, {\n \"data-rehype-pretty-code-fragment\": \"\",\n children: _jsx(_components.pre, {\n style: {\n backgroundColor: \"#232136\"\n },\n tabIndex: \"0\",\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: _jsxs(_components.code, {\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: [_jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"top\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''返回堆顶\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.h3, {\n id: \"入堆\",\n children: _jsx(_components.a, {\n href: \"#入堆\",\n children: \"入堆\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"入堆时,把元素加到堆尾,再做上浮操作。\"\n }), \"\\n\", _jsx(_components.div, {\n \"data-rehype-pretty-code-fragment\": \"\",\n children: _jsx(_components.pre, {\n style: {\n backgroundColor: \"#232136\"\n },\n tabIndex: \"0\",\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: _jsxs(_components.code, {\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: [_jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"insert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"v\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''入堆\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 将元素加到堆尾并做上浮操作\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"append\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"v\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"floatUp\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"len\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"-\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.h3, {\n id: \"出堆\",\n children: _jsx(_components.a, {\n href: \"#出堆\",\n children: \"出堆\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\"\n }), \"\\n\", _jsx(_components.div, {\n \"data-rehype-pretty-code-fragment\": \"\",\n children: _jsx(_components.pre, {\n style: {\n backgroundColor: \"#232136\"\n },\n tabIndex: \"0\",\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: _jsxs(_components.code, {\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: [_jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"pop\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")->\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''出堆\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 取出堆顶元素\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" res \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 将堆尾元素填到堆顶并做下沉操作\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"len\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"-\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pop\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"sinkDown\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" res\"\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"注意入堆与出堆操作都要保证堆的大小会相应变化。\"\n }), \"\\n\", _jsx(_components.h3, {\n id: \"堆初始化\",\n children: _jsx(_components.a, {\n href: \"#堆初始化\",\n children: \"堆初始化\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\"\n }), \"\\n\", _jsx(_components.div, {\n \"data-rehype-pretty-code-fragment\": \"\",\n children: _jsx(_components.pre, {\n style: {\n backgroundColor: \"#232136\"\n },\n tabIndex: \"0\",\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: _jsxs(_components.code, {\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: [_jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"__init__\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"List\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"fCompare\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"Callable\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"bool\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=lambda\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"a\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"b\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"a\"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \">\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"b\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"->\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"None\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''堆初始化\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" :param A: 在数组A上进行初始化\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" A\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"fCompare \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" fCompare\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"for\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" i \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"in\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"reversed\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"range\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"len\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"))):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"sinkDown\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.h2, {\n id: \"整体代码\",\n children: _jsx(_components.a, {\n href: \"#整体代码\",\n children: \"整体代码\"\n })\n }), \"\\n\", _jsx(_components.h3, {\n id: \"堆的整体实现\",\n children: _jsx(_components.a, {\n href: \"#堆的整体实现\",\n children: \"堆的整体实现\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"综上,堆的整体代码实现如下:\"\n }), \"\\n\", _jsx(_components.div, {\n \"data-rehype-pretty-code-fragment\": \"\",\n children: _jsx(_components.pre, {\n style: {\n backgroundColor: \"#232136\"\n },\n tabIndex: \"0\",\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: _jsxs(_components.code, {\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: [_jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"from\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" typing \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"import\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" Any\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" Callable\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" Generic\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" List\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" TypeVar\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" TypeVar\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"\\\"T\\\"\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"lfChildOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"int\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"+\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"<<\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"-\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"rtChildOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"int\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"+\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"<<\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"parentOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"int\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"-\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \">>\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"class\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"Heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"Generic\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''堆结构\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 有两个成员:\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" self.fCompare: Callable[[T,T],bool] # 比较函数\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 下面假设堆为大顶堆\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 即有self.fCompare = lambda a,b: a>b\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"__init__\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"List\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"fCompare\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"Callable\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"bool\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=lambda\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"a\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"b\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"a\"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \">\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"b\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"->\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"None\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''堆初始化\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" :param A: 在数组A上进行初始化\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" A\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"fCompare \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" fCompare\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"for\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" i \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"in\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"reversed\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"range\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"len\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"))):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"sinkDown\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"size\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''返回堆大小\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"len\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"top\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''返回堆顶\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"sinkDown\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"int\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''下沉操作\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 对下标为i的元素递归地进行下沉操作\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 直到该元素大于其两个子节点或该元素下沉到叶子节点\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" lc \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" lfChildOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" rc \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" rtChildOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" larger \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" i\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"if\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" lc \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"<\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"size\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"and\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"fCompare\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"lc\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"larger\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" larger \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" lc\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"if\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" rc \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"<\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"size\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"and\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"fCompare\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"rc\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"larger\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" larger \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" rc\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 当元素i大于其两个子节点时符合堆结构,结束递归\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"if\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" larger \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"larger\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"larger\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"sinkDown\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"larger\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"floatUp\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"int\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''上浮操作\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 对下标为i的元素递归地进行上浮操作\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" 直到该元素小于其父节点或该元素上浮到根节点\"\n })\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 元素i上浮到根节点时结束递归\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"if\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" i \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"<=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 当元素i小于其父节点时符合堆结构,结束递归\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" pr \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" parentOf\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"if\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"fCompare\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pr\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 元素i大于其父节点,交换i与其父节点并继续上浮\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pr\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"i\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pr\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"floatUp\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pr\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"insert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"v\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''入堆\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 将元素加到堆尾并做上浮操作\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"append\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"v\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"floatUp\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"len\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"-\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"pop\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")->\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \"'''出堆\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#F6C177\"\n },\n children: \" '''\"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 取出堆顶元素\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" res \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\",\n fontStyle: \"italic\"\n },\n children: \"#\"\n }), _jsx(_components.span, {\n style: {\n color: \"#6E6A86\",\n fontStyle: \"italic\"\n },\n children: \" 将堆尾元素填到堆顶并做下沉操作\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"[\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EB6F92\",\n fontStyle: \"italic\"\n },\n children: \"len\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"-\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"]\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"A\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pop\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"sinkDown\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" res\"\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.h3, {\n id: \"单元测试\",\n children: _jsx(_components.a, {\n href: \"#单元测试\",\n children: \"单元测试\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"入堆、出堆等操作的简单单元测试如下:\"\n }), \"\\n\", _jsx(_components.div, {\n \"data-rehype-pretty-code-fragment\": \"\",\n children: _jsx(_components.pre, {\n style: {\n backgroundColor: \"#232136\"\n },\n tabIndex: \"0\",\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: _jsxs(_components.code, {\n \"data-language\": \"python\",\n \"data-theme\": \"default\",\n children: [_jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"import\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" pytest\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"import\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" heap\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"@\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"pytest\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"fixture\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"():\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"return\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"Heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"([\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"1\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"3\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"4\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"7\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"2\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"6\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"5\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"9\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"0\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"8\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"],\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"lambda\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"a\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"b\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"a\"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \">\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"b\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: \" \"\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"class\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#9CCFD8\"\n },\n children: \"Test_TestHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"test_init_notNull\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"Heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"assert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"size\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"10\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"assert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"top\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"9\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"test_insert_notTop\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"Heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"insert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"6\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"assert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"size\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"11\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"assert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"top\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"9\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"test_insert_top\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"Heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"insert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"10\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \")\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"assert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"size\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"11\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"assert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"top\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"10\"\n })]\n }), \"\\n\", _jsx(_components.span, {\n className: \"line\",\n children: _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n })\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"def\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"test_pop\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"(\"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"self\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \",\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#C4A7E7\",\n fontStyle: \"italic\"\n },\n children: \"initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \":\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"Heap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"):\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" p \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"=\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"pop\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"assert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" p \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"9\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"assert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"size\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"9\"\n })]\n }), \"\\n\", _jsxs(_components.span, {\n className: \"line\",\n children: [_jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"assert\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" initHeap\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \".\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \"top\"\n }), _jsx(_components.span, {\n style: {\n color: \"#908CAA\"\n },\n children: \"()\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#3E8FB0\"\n },\n children: \"==\"\n }), _jsx(_components.span, {\n style: {\n color: \"#E0DEF4\"\n },\n children: \" \"\n }), _jsx(_components.span, {\n style: {\n color: \"#EA9A97\"\n },\n children: \"8\"\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.h1, {\n id: \"关于堆排序\",\n children: _jsx(_components.a, {\n href: \"#关于堆排序\",\n children: \"关于堆排序\"\n })\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"O\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"n\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"O(nlogn)\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.02778em\"\n },\n children: \"O\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"n\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n })]\n })\n })]\n })\n }), \"等优秀的性质,是比较常用的一个排序算法。\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\"\n }), \"\\n\", _jsxs(_components.ol, {\n children: [\"\\n\", _jsx(_components.li, {\n children: \"堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\"\n }), \"\\n\", _jsx(_components.li, {\n children: \"堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\"\n }), \"\\n\", _jsx(_components.li, {\n children: \"堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\"\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。\"\n })]\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = Object.assign({}, _provideComponents(), props.components);\n return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, {\n children: _jsx(_createMdxContent, props)\n })) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"meta":{"content":"\n# 如何手撕一个堆\n\n# 写在前面\n\n在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\n\n# 首先要理解,然后才能实现\n\n就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\n\n## 先抓住重点:堆是一种树结构\n\n首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\n\n再进一步,在堆这种树结构中,最重要的约束就是:**对于树中的每个节点,总有父节点大于两个子节点**(以大顶堆为例,下同)。\n\n如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\n\n而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\n\n1. 堆并不关注左右子树之间的大小情况,那么**要维护一个堆,基本只需要做交换父节点与子节点的操作**,而不需要像二叉查找树那样做各种旋转操作。\n2. 因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,**可以使用数组进行实现**。\n3. 因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:**堆的增删操作最坏时间复杂度为O(logn)**。\n\n## 再抓基本操作:上浮与下沉\n\n上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点**过大**,就跟他的父节点**向上交换**;若一个节点**过小**,就跟他的子节点**向下交换**。\n\n假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\n\n![p与g交换](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png)\n\n由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\n\n1. p > g > p2\n2. g > 原p > c1与c2\n\n则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\n\n同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\n\n为了简化,我们把前面那种递归地向上交换称为**上浮操作**,把后面这种递归地向下交换称为**下沉操作**。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\n\n# 各种接口的逻辑\n\n## 插入元素——入堆\n\n把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\n\n虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\n\n## 删除堆顶元素——出堆\n\n我们从堆中删除元素时,一般只会删除堆顶元素。\n\n删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\n\n这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\n\n## 堆的初始化——建堆\n\n建立一个堆,我们有两种思路:\n\n1. 将元素一个一个插入,即对每个元素都做一次入堆操作。\n2. 当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\n\n下面我们来分析这两种建堆策略。\n\n### 元素逐个入堆\n\n上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\n\n插入第i个元素时,堆的大小为$i$(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\n\n$$T(i) = logi$$\n\n那么把所有元素上浮,则总时间复杂度为:\n\n$$\n\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logi\\\\\n&= 1\\times0 + 2\\times1 + ... + 2^{logn}\\times{logn} \\\\\n&=O(nlogn)\n\\end{aligned}\n$$\n\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\n\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\n\n### 堆合并\n\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\n\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\n\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\n\n$$\nT(i) = logn-logi\n$$\n\n那么把所有元素下沉,则总时间复杂度为:\n\n$$\n\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logn-logi \\\\\n&= \\frac{n}{2^{logn}}\\times{logn}+ ... + \\frac{n}{4}\\times2+\\frac{n}{2}\\times1 \\\\\n&= O(n)\n\\end{aligned}\n$$\n\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\n\n### 两种策略的比较与理解\n\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\n\n1. 从元素移动路径的角度\n\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\n\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\n\n2. 从元素移动数量与移动距离的角度\n\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\n\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\n\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\n\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\n\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\n\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\n\n# 代码实现\n\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\n\n```python\nT = TypeVar(\"T\")\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n```\n\n## 实现树结构\n\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\n\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\n\n```python\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n```\n\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\n\n## 实现基本操作——上浮与下沉\n\n### 上浮\n\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\n\n```python\ndef floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n```\n\n### 下沉\n\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\n\n```python\ndef size(self):\n '''返回堆大小\n '''\n return len(self.A)\n\ndef sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n```\n\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\n\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\n\n## 实现各种借口——读、增、删、初始化\n\n### 读取堆顶\n\n堆一般只允许读取堆顶,即全堆最大元素。\n\n```python\ndef top(self):\n '''返回堆顶\n '''\n return self.A[0]\n```\n\n### 入堆\n\n入堆时,把元素加到堆尾,再做上浮操作。\n\n```python\ndef insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n```\n\n### 出堆\n\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\n\n```python\ndef pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n注意入堆与出堆操作都要保证堆的大小会相应变化。\n\n### 堆初始化\n\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\n\n```python\ndef __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n```\n\n## 整体代码\n\n### 堆的整体实现\n\n综上,堆的整体代码实现如下:\n\n```python\nfrom typing import Any, Callable, Generic, List, TypeVar\n\nT = TypeVar(\"T\")\n\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n def __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n \n def size(self):\n '''返回堆大小\n '''\n return len(self.A)\n \n def top(self):\n '''返回堆顶\n '''\n return self.A[0]\n \n def sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n\n def floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n \n def insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n\n def pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n### 单元测试\n\n入堆、出堆等操作的简单单元测试如下:\n\n```python\nimport pytest\nimport heap\n\n@pytest.fixture\ndef initHeap():\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \n lambda a,b:a>b)\n\nclass Test_TestHeap:\n def test_init_notNull(self, initHeap:heap.Heap):\n assert initHeap.size() == 10\n assert initHeap.top() == 9\n \n def test_insert_notTop(self, initHeap:heap.Heap):\n initHeap.insert(6)\n assert initHeap.size() == 11\n assert initHeap.top() == 9\n \n def test_insert_top(self, initHeap:heap.Heap):\n initHeap.insert(10)\n assert initHeap.size() == 11\n assert initHeap.top() == 10\n \n def test_pop(self, initHeap:heap.Heap):\n p = initHeap.pop()\n assert p == 9\n assert initHeap.size() == 9\n assert initHeap.top() == 8\n```\n\n# 关于堆排序\n\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\n\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\n\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\n\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。","title":"如何手撕一个堆","abstract":"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。","length":483,"created_at":"2021-08-28T23:09:14.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","算法竞赛"],"license":false},"prevNextInfo":{"prevInfo":{"slug":"init-a-new-hexo-project","title":"init-a-new-hexo-project","path":"/articles/init-a-new-hexo-project"},"nextInfo":{"slug":"The-beauty-of-design-parten","title":"设计模式之美读书笔记","path":"/articles/The-beauty-of-design-parten"}}},"__N_SSG":true} \ No newline at end of file diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/Sort-algorithm.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/Sort-algorithm.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/Sort-algorithm.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/Sort-algorithm.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/The-beauty-of-design-parten.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/The-beauty-of-design-parten.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/The-beauty-of-design-parten.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/The-beauty-of-design-parten.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/create-blog-cicd-by-github.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/create-blog-cicd-by-github.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/create-blog-cicd-by-github.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/create-blog-cicd-by-github.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/graph-for-economics-1.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/graph-for-economics-1.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/graph-for-economics-1.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/graph-for-economics-1.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/graph-for-economics-2.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/graph-for-economics-2.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/graph-for-economics-2.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/graph-for-economics-2.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/hello-world.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/hello-world.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/hello-world.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/hello-world.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/init-a-new-hexo-project.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/init-a-new-hexo-project.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/init-a-new-hexo-project.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/init-a-new-hexo-project.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/introduction-for-k8s-2.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/introduction-for-k8s-2.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/introduction-for-k8s-2.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/introduction-for-k8s-2.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/introduction-for-k8s.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/introduction-for-k8s.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/introduction-for-k8s.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/introduction-for-k8s.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/python-dict.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/python-dict.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/python-dict.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/python-dict.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/the-using-in-cpp.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/the-using-in-cpp.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/the-using-in-cpp.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/the-using-in-cpp.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/use-paste-image-and-vscode-memo.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/use-paste-image-and-vscode-memo.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/use-paste-image-and-vscode-memo.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/use-paste-image-and-vscode-memo.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/why-homogeneous.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/articles/why-homogeneous.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/why-homogeneous.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/articles/why-homogeneous.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/clips.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/clips.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/clips.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/clips.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/ideas.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/ideas.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas/blog-in-next.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/ideas/blog-in-next.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas/blog-in-next.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/ideas/blog-in-next.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas/blog-syntax.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/ideas/blog-syntax.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas/blog-syntax.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/ideas/blog-syntax.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas/first-idea.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/ideas/first-idea.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas/first-idea.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/ideas/first-idea.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas/newest.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/ideas/newest.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas/newest.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/ideas/newest.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas/using-chart-js.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/ideas/using-chart-js.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/ideas/using-chart-js.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/ideas/using-chart-js.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/index.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/index.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/index.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/index.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/aws.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/aws.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/aws.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/aws.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/blog.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/blog.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/blog.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/blog.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/c++.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/c++.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/c++.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/c++.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/ci-cd.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/ci-cd.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/ci-cd.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/ci-cd.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/cloud-computing.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/cloud-computing.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/cloud-computing.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/cloud-computing.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/cloud-native.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/cloud-native.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/cloud-native.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/cloud-native.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/devops.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/devops.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/devops.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/devops.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/docker.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/docker.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/docker.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/docker.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/github.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/github.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/github.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/github.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/hexo.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/hexo.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/hexo.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/hexo.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/iac.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/iac.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/iac.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/iac.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/javascript.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/javascript.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/javascript.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/javascript.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/kubernetes.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/kubernetes.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/kubernetes.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/kubernetes.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/nextjs.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/nextjs.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/nextjs.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/nextjs.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/python.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/python.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/python.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/python.json diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/vscode.json b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/vscode.json similarity index 100% rename from _next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/vscode.json rename to _next/data/06cVAX8N9Z8KmESdU4cc2/tags/vscode.json diff --git "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\216\222\345\272\217.json" "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\216\222\345\272\217.json" similarity index 100% rename from "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\216\222\345\272\217.json" rename to "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\216\222\345\272\217.json" diff --git "a/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\225\260\346\215\256\347\273\223\346\236\204.json" "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\225\260\346\215\256\347\273\223\346\236\204.json" new file mode 100644 index 00000000..d4cec927 --- /dev/null +++ "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\225\260\346\215\256\347\273\223\346\236\204.json" @@ -0,0 +1 @@ +{"pageProps":{"allTagInfos":[{"tag":"Kubernetes","slug":"kubernetes","path":"/tags/kubernetes","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"DevOps","slug":"devops","path":"/tags/devops","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Docker","slug":"docker","path":"/tags/docker","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Cloud Native","slug":"cloud-native","path":"/tags/cloud-native","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Blog","slug":"blog","path":"/tags/blog","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"init-a-new-hexo-project","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"VSCode","slug":"vscode","path":"/tags/vscode","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"Hexo","slug":"hexo","path":"/tags/hexo","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"JavaScript","slug":"javascript","path":"/tags/javascript","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"GitHub","slug":"github","path":"/tags/github","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"AWS","slug":"aws","path":"/tags/aws","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"CI/CD","slug":"ci-cd","path":"/tags/ci-cd","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"IaC","slug":"iac","path":"/tags/iac","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]},{"tag":"设计模式","slug":"设计模式","path":"/tags/设计模式","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"笔记","slug":"笔记","path":"/tags/笔记","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"排序","slug":"排序","path":"/tags/排序","postSlugs":[{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"Python","slug":"python","path":"/tags/python","postSlugs":[{"postSlug":"python-dict","postType":"article"}]},{"tag":"C++","slug":"c++","path":"/tags/c++","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"}]},{"tag":"杂技","slug":"杂技","path":"/tags/杂技","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"hello-world","postType":"article"}]},{"tag":"杂谈","slug":"杂谈","path":"/tags/杂谈","postSlugs":[{"postSlug":"hello-world","postType":"article"}]},{"tag":"Nextjs","slug":"nextjs","path":"/tags/nextjs","postSlugs":[{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"Cloud Computing","slug":"cloud-computing","path":"/tags/cloud-computing","postSlugs":[{"postSlug":"newest","postType":"idea"}]}],"selectedTagInfo":{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},"posts":[{"slug":"Handy-heap-cheat-sheet","file":"public/content/articles/2021-03-21-Handy-heap-cheat-sheet.md","mediaDir":"content/articles/2021-03-21-Handy-heap-cheat-sheet","path":"/articles/Handy-heap-cheat-sheet","meta":{"content":"\n# 如何手撕一个堆\n\n# 写在前面\n\n在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\n\n# 首先要理解,然后才能实现\n\n就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\n\n## 先抓住重点:堆是一种树结构\n\n首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\n\n再进一步,在堆这种树结构中,最重要的约束就是:**对于树中的每个节点,总有父节点大于两个子节点**(以大顶堆为例,下同)。\n\n如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\n\n而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\n\n1. 堆并不关注左右子树之间的大小情况,那么**要维护一个堆,基本只需要做交换父节点与子节点的操作**,而不需要像二叉查找树那样做各种旋转操作。\n2. 因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,**可以使用数组进行实现**。\n3. 因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:**堆的增删操作最坏时间复杂度为O(logn)**。\n\n## 再抓基本操作:上浮与下沉\n\n上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点**过大**,就跟他的父节点**向上交换**;若一个节点**过小**,就跟他的子节点**向下交换**。\n\n假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\n\n![p与g交换](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png)\n\n由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\n\n1. p > g > p2\n2. g > 原p > c1与c2\n\n则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\n\n同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\n\n为了简化,我们把前面那种递归地向上交换称为**上浮操作**,把后面这种递归地向下交换称为**下沉操作**。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\n\n# 各种接口的逻辑\n\n## 插入元素——入堆\n\n把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\n\n虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\n\n## 删除堆顶元素——出堆\n\n我们从堆中删除元素时,一般只会删除堆顶元素。\n\n删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\n\n这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\n\n## 堆的初始化——建堆\n\n建立一个堆,我们有两种思路:\n\n1. 将元素一个一个插入,即对每个元素都做一次入堆操作。\n2. 当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\n\n下面我们来分析这两种建堆策略。\n\n### 元素逐个入堆\n\n上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\n\n插入第i个元素时,堆的大小为$i$(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\n\n$$T(i) = logi$$\n\n那么把所有元素上浮,则总时间复杂度为:\n\n$$\n\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logi\\\\\n&= 1\\times0 + 2\\times1 + ... + 2^{logn}\\times{logn} \\\\\n&=O(nlogn)\n\\end{aligned}\n$$\n\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\n\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\n\n### 堆合并\n\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\n\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\n\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\n\n$$\nT(i) = logn-logi\n$$\n\n那么把所有元素下沉,则总时间复杂度为:\n\n$$\n\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logn-logi \\\\\n&= \\frac{n}{2^{logn}}\\times{logn}+ ... + \\frac{n}{4}\\times2+\\frac{n}{2}\\times1 \\\\\n&= O(n)\n\\end{aligned}\n$$\n\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\n\n### 两种策略的比较与理解\n\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\n\n1. 从元素移动路径的角度\n\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\n\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\n\n2. 从元素移动数量与移动距离的角度\n\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\n\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\n\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\n\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\n\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\n\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\n\n# 代码实现\n\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\n\n```python\nT = TypeVar(\"T\")\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n```\n\n## 实现树结构\n\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\n\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\n\n```python\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n```\n\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\n\n## 实现基本操作——上浮与下沉\n\n### 上浮\n\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\n\n```python\ndef floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n```\n\n### 下沉\n\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\n\n```python\ndef size(self):\n '''返回堆大小\n '''\n return len(self.A)\n\ndef sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n```\n\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\n\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\n\n## 实现各种借口——读、增、删、初始化\n\n### 读取堆顶\n\n堆一般只允许读取堆顶,即全堆最大元素。\n\n```python\ndef top(self):\n '''返回堆顶\n '''\n return self.A[0]\n```\n\n### 入堆\n\n入堆时,把元素加到堆尾,再做上浮操作。\n\n```python\ndef insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n```\n\n### 出堆\n\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\n\n```python\ndef pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n注意入堆与出堆操作都要保证堆的大小会相应变化。\n\n### 堆初始化\n\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\n\n```python\ndef __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n```\n\n## 整体代码\n\n### 堆的整体实现\n\n综上,堆的整体代码实现如下:\n\n```python\nfrom typing import Any, Callable, Generic, List, TypeVar\n\nT = TypeVar(\"T\")\n\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n def __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n \n def size(self):\n '''返回堆大小\n '''\n return len(self.A)\n \n def top(self):\n '''返回堆顶\n '''\n return self.A[0]\n \n def sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n\n def floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n \n def insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n\n def pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n### 单元测试\n\n入堆、出堆等操作的简单单元测试如下:\n\n```python\nimport pytest\nimport heap\n\n@pytest.fixture\ndef initHeap():\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \n lambda a,b:a>b)\n\nclass Test_TestHeap:\n def test_init_notNull(self, initHeap:heap.Heap):\n assert initHeap.size() == 10\n assert initHeap.top() == 9\n \n def test_insert_notTop(self, initHeap:heap.Heap):\n initHeap.insert(6)\n assert initHeap.size() == 11\n assert initHeap.top() == 9\n \n def test_insert_top(self, initHeap:heap.Heap):\n initHeap.insert(10)\n assert initHeap.size() == 11\n assert initHeap.top() == 10\n \n def test_pop(self, initHeap:heap.Heap):\n p = initHeap.pop()\n assert p == 9\n assert initHeap.size() == 9\n assert initHeap.top() == 8\n```\n\n# 关于堆排序\n\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\n\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\n\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\n\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。","title":"如何手撕一个堆","abstract":"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。","length":483,"created_at":"2021-08-28T23:09:14.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","算法竞赛"],"license":false}},{"slug":"Sort-algorithm","file":"public/content/articles/2021-01-11-Sort-algorithm.md","mediaDir":"content/articles/2021-01-11-Sort-algorithm","path":"/articles/Sort-algorithm","meta":{"content":"\n# 序言\n\n我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。\n\n你会发现堆排序被当成是选择排序的一种优化——因为我认为堆排序主要在于使用了堆这种数据结构,而总体思想与选择排序相比没有太大变化。\n\n你还会找到其他与别的文章不一样的地方。因为这篇文章是我按照自己的理解来写的,我脑子里是这样想的,那文章里就是这样写的。我会按照我的理解,从纵向与横向两个维度,来理清楚各个排序算法的特性与异同。\n\n# 整篇文章的要点\n\n整篇文章以纵向——算法分类、以及横向——算法评价两个维度来进行组织。\n\n排序算法可以按照以下方式来进行分类:\n\n- 基于比较的排序算法\n - 基于分治思想\n - 快速排序\n - 归并排序\n - 基于有序区域扩展\n - 插入排序\n - 选择排序\n- 不基于比较的排序算法\n - 计数排序\n - 桶排序\n - 基数排序\n\n文章中还会讲一讲为什么会这么分,每种分类有什么共性,分类之间有什么差异。此外,在最后还会稍微提一提外部排序与适用于并行运算的排序等。\n\n而对于纵向分类中的每一个端点,我们又会从以下五个方面,来对各个算法进行一个总体评价:\n\n- 时间复杂度(最坏,最好,平均※)\n- 空间复杂度\n- 是否原地排序\n- 是否稳定排序\n- 能否用于链表排序\n\n而由于复杂度主要只关注数量级,因此在这篇文章里会在不影响计算结果的前提下对复杂度计算进行适当的近似与简化。\n\n# 快排\n\n## 思想\n\n分治法:先把序列分为小的部分和大的部分,再将两部分分别排序。即:复杂分割,简单合并,主要操作在于分割。\n\n## 要点\n\n### 时间复杂度\n\n推导式:T(n) = T(找) + T(左) + T(右) = O(n) + 两个子问题时间复杂度。\n\n- 最好时间复杂度为每次都正好找到最中间的一个数时时间复杂度为O(nlogn)。证明略。\n- 最坏时间复杂度为每次都正好找到最旁边的数时时间复杂度为O(n^2)。证明略。\n- 平均时间复杂度为O(nlogn),推导式如下:\n\n 快速排序每一步中,将元素分为左右两边需要遍历整个列表,耗时T(n)。假设最后定位的元素为最终第i个元素,则两个子问题复杂度分别为T(i)和T(n-i-1)。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{\\sum_{i=0}^{n-1}{T(i)+T(n-i-1)}}{n} \\\\\n &=n + \\frac{2}{n}\\times\\sum_{i=0}^{n-1}{T(i)} \\\\\n \\end{aligned}\n $$\n\n 令 $$\\sum_{i=0}^{n}T(i) = Sum(n)$$ ,即有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{2}{n} \\times Sum(n-1) \\\\\n Sum(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1)\n \\end{aligned}\n $$\n\n 错位相减:\n\n $$\n \\begin{aligned}\n Sum(n) - Sum(n-1) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1) - \\frac{n}{2}T(n) + \\frac{n}{2}(n) \n \\\\\n T(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n}{2}T(n) - \\frac{2n+1}{2} \n \\\\\n \\frac{T(n+1)}{n+2} &= \\frac{T(n)}{n+1}+\\frac{2n+1}{(n+1)(n+2)} \n \\\\\n &= \\frac{T(n)}{n+1}+\\frac{1}{n}+\\frac{1}{n+1} \n \\\\\n &=...\n \\\\\n &= \\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+\\frac{1}{3}+...+\\frac{1}{n})+\\frac{1}{n+1}\n \\\\\n \\\\\n \\frac{T(n)}{n+1}&=\\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+...+\\frac{1}{n-1})+\\frac{1}{n}\n \\\\\n &= O(1)+O(1)+O(logn)+O(\\frac{1}{n})\n \\\\\n &= O(logn)\n \\end{aligned}\n $$\n\n 其中由于 $$\\frac{1}{x}=\\frac{d(logx)}{dx}$$ ,因此 $$\\frac{1}{2}+...+\\frac{1}{n-1}=O(logn)$$ 。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n)&=(n+1)\\times O(logn)\\\\\n &=O(n)\\times O(logn)\\\\\n &=O(nlogn)\n \\end{aligned}\n $$\n\n### 额外空间复杂度\n\n考虑栈深度,额外空间复杂度为O(logn)。由于快速排序主要步骤在于分,因此必须自上而下的进行递归,无法避免栈深度。\n\n### 原地排序\n\n虽然快速排序有额外空间复杂度,但并不妨碍它是一个原地排序。\n\n### 不稳定\n\n堆排序在分操作时将元素左右交换,会破坏稳定性。\n\n### 链表形式特点\n\n- 时间复杂度不变\n- 空间复杂度不变\n- 变为稳定排序※\n\n## 手写时的易错点\n\n- 分成左右子序列时最好完全分开(一边用`<=`一边用`>`),不然容易造成死循环。\n- 分左右子序列时仔细考虑最初下标位置与最终下标位置,以及对应位置的值的大小。\n- 不要忘记递归的结束条件。\n\n# 归并排序\n\n## 思想\n\n分治法:先把两个子序列各自排好序,然后再合并两个子序列。即:简单分割,复杂合并。主要步骤在于合并。\n\n## 要点\n\n- 时间复杂度推导式:T(n) = 2T(n/2) + T(合)\n- 平均、最好、最坏时间复杂度都是O(nlogn),推导过程略。\n- 额外空间复杂度为O(n),合并时必须准备额外空间。但由于主要步骤在于合并,可以自下而上地进行迭代合并,可以不使用栈。\n- 非原地排序\n- 稳定排序\n\n### 链表形式\n\n- 时间复杂度不变\n- 额外空间复杂度变为O(1)※\n- 稳定排序\n- 只能使用迭代形式,不能使用递归形式\n\n## 易错点\n\n- 合并时一边结束时另一边还未结束,需要把那一边也放入合并后序列中\n- 保持稳定排序:合并时左序列等于右序列时也采用左序列\n- 不要忘记递归结束条件\n- 不要忘记循环递进条件\n\n## 进阶\n\n- 原地归并排序:时间复杂度为O(log^2n),牺牲合并的时间复杂度进行原地排序。\n- 多路归并排序:使用竞标树,多路归并,用于磁盘IO。\n\n# 插入排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将无序序列当前元素插入有序序列,复杂度取决于有序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:基本有序情况下,O(n)\n- 额外空间复杂度:O(1)\n- 原地排序\n- 稳定排序\n- 每次插入时都要移动序列,写次数较多\n- 若查找插入位置时使用二分法查找,则可加快时间。(但不足以对时间复杂度造成影响,且最好时间复杂度也会上升为O(nlogn))\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 稳定排序\n- 插入时不再需要移动序列,但也不能使用二分法查找\n\n## 进阶——希尔排序\n\n- 要点:\n\n 插入排序的优点在于当序列基本有序时,时间复杂度可逼近为O(n)。\n\n 但插入时移动有序序列中元素所耗时间较多,而每次只移动一步。但实际上当序列分布均匀时,有序序列中排靠后的元素在整个序列中也会排靠后。\n\n 可以把序列分为几个大步长序列,在最初的几次插入放开移动步长,让大的元素直接移动到较后位置。再往后慢慢缩小步长,此时序列基本有序,可以利用基本有序时插入排序的优势。\n\n- 时间复杂度\n 1. 当步长为2^i时,不能使时间复杂度缩短为O(nlogn)。因为一个子序列所有元素有可能比另一个子序列最大元素都要大,这时插入排序仍需进行约n^2次操作\n 2. 当步长为2^i时效率较低,因为当步长为4已经有序时,步长为2再比较是无用比较。但由于1.的问题,不能节省比较时间。\n 3. 当步长之间最小公约数较少,甚至互质时,无用比较次数会降低。\n 4. 最坏时间复杂度下限为 $$O(nlog^2n)$$ (当步长采用 $$2^i3^j$$ 时),但一般希尔排序平均时间复杂度都为 $$O(n^{\\frac{3}{2}})$$\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序:希尔排序步长较大时会发生前后跳转。\n- 不能写为链表形式\n\n# 选择排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将选无序序列中最小元素加到有序序列末尾,复杂度取决于无序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:O(n2),如能保证无序部分的最小元素所在位置一定(堆排序),能降低时间复杂度\n- 额外空间复杂度:O(1)\n- 原地排序\n- 不稳定排序(采用元素交换策略时)\n- 每次找到最小元素后,只需交换一次位置即可,写次数较少。\n- 若找到最小元素后,不直接交换而是进行数组移动,则可进行稳定排序,但写次数变多,与插入排序相比没有优势,也不能使用二分查找进行简化。\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 变为稳定排序※(因为链表不需要数组移动,稳定排序方式的缺点得以消除)\n\n## 进阶——堆排序\n\n- 要点:\n\n 选择排序中耗时最多的是取出无序序列中最小值的时间,需要遍历整个无序序列。\n\n 但实际上我们只关心无序序列中的最小值,而不关心其他值的位置。通过将无序序列建为堆,减少选择时间,降低总的时间复杂度。\n\n- 时间复杂度O(nlogn)\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序\n- 操作时间复杂度:每次向下比较关注一个节点与其左右子堆顶元素,每次向上比较只关注节点与其父元素(大顶堆,堆大小为n)\n - 下沉:向下比较,若顶元素不是最大,将顶元素与较大的子堆堆顶元素交换。递归处理该子堆顶元素,直到向下比较顶元素最大。\n\n 最好时间复杂度O(1),最坏时间复杂度为O(h)=O(logn),平均O(logn)\n\n - 上浮:向上比较,若元素比其上层要大,交换该元素与其上层元素。递归处理其上层元素,直到向上比较不比上层要大。\n\n 最好时间复杂度O(1),最坏时间复杂度O(h)=O(logn),平均O(logn)\n\n - 入堆:堆扩容一位,将新元素插到尾部,将该元素上浮,最坏、平均时间复杂度O(logn)\n - 出堆:取出堆顶元素,将尾部放到堆顶,将该元素下沉,最坏、平均时间复杂度O(logn)\n - 缺点:通常堆尾元素较小,出堆时将堆尾元素放到堆顶再下沉基本要沉到堆底,无用比较较多\n- 建堆时间复杂度O(n)\n - 策略1:从头开始建堆,逐个元素插入,时间复杂度取决于最后一层,时间复杂度为O(nlogn)\n - 每次将堆扩容一位,将末尾元素上浮。\n - 时间复杂度推导:\n\n 每次插入时间:\n\n $$\n \\begin{aligned} T(i) &= h\\\\\n &= logi\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n}logi\\\\\n &= 1\\times1 + 2\\times2+ 3\\times4 + ...+h\\times \\frac{n}{2} \\\\\n \\\\\n 2T(n) &= 1\\times2 + 2\\times4+ 3\\times8 + ...+h\\times n \\\\\n \\\\\n 2T(n)-T(n) &= h\\times n - (1+2+4+...+2^{h-1}) \\\\\n &= h\\times n - O(2^h) \\\\\n \\\\\n T(n) &= nlogn - O(2^h) \\\\\n &= O(nlogn)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(nlogn)$$\n\n - 策略2:从后开始建堆,小堆合并(逐个元素下沉),时间复杂度为O(n)\n - 每次堆合并时,有三部分:左子堆,右子堆,顶元素。下沉顶元素。\n - 时间复杂度推导:\n\n 第i个元素合并时,时间为:\n\n $$\n \\begin{aligned}\n T(i) &= h_{子堆}\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n} (h_{子堆}) \\\\\n &= h + (h-1)\\times2 + ... + 2 \\times \\frac{n}{4} + 1 \\times\\frac{n}{2}\\\\\n \\\\\n 2T(n) &= h \\times 2 + (h-1) \\times 4 + ... + 2 \\times \\frac{n}{2} + n \\\\\n \\\\\n 2T(n) - T(n) &= n + \\frac{n}{2} + ... + 2 - h \\\\\n \\\\\n T(n) &= O(n) - h \\\\ \n &= O(n)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(n)$$ \n\n - 虽说每步是做一个小堆合并,但实际上从堆尾到堆头遍历,相当于仅关注元素没有稳定,相当于可以直接使用下沉操作。\n\n# 基于比较的排序算法时间复杂度下限:逆序对思想\n\n基于比较的排序算法可以看作序列逆序对的消除。完全随机序列逆序对数量为O(n^2),若一次元操作只消除一个逆序对,则时间复杂度不会低于O(n^2)。降低时间复杂度关键在于一次消除多个逆序对。\n\n1. 希尔排序通过增大最初的步长来企图一次消除多个逆序对。\n2. 归并排序消除逆序对最主要在于归并步骤。最后几次合并每个子步骤用O(1)时间消除O(n)个逆序对。\n3. 快速排序消除逆序对最主要在于划分步骤。每个划分步骤用O(n)时间消除O(n)+O(左长度×右长度)个逆序对。\n4. 堆排序逆序对消除方式比较Tricky,但可以看出消除逆序对大致在于出堆步骤,通过O(logn)时间复杂度消除O(n)个逆序对。(左小右大排序时需要建立左大右小的大顶堆,建堆时基本没有消除逆序对)\n\n# 最后\n\n这篇文章我们主要关注了排序算法中的大头——基于比较的排序算法。在下篇文章,我们再来看一下不基于比较的排序算法,以及外排序与并行排序。","title":"排序算法","abstract":"我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。","length":342,"created_at":"2021-01-11T22:57:10.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","排序"],"license":false}},{"slug":"python-dict","file":"public/content/articles/2020-08-02-python-dict.md","mediaDir":"content/articles/2020-08-02-python-dict","path":"/articles/python-dict","meta":{"content":"\n> CPython从3.6开始,字典(dict)不再是无序的了——字典的修改了原先的底层实现,变得能按字典插入的顺序进行遍历。而Python从3.7开始将字典的有序性写入语言特性,不管是Jython、IronPython还是其他Python实现,从3.7开始大家的字典都是有序的了。\n\n# 前言\n\n以前参加Python相关的面试时,面试官经常都会问一个问题:Python里的字典(dict)是有序的吗?\n\n这自然难不倒我,我也照本宣科地讲:Python的字典底层是用哈希表实现的,在不发生冲突时读写的时间复杂度是O(1),比读写时间复杂度为O(logn)的红黑树要更快。但红黑树可以按下标的大小顺序进行遍历,而Dict遍历时是无序的。\n\n我讲的时候没感觉到任何的违和感,估计面试官们也没觉得任何的不对。直到有一天,我查Python各个版本的新特性时,发现Python 3.6的What's New里有[这么一条](https://docs.python.org/3/whatsnew/3.6.html#new-dict-implementation):\n\n> New dict implementation\n> \n> The dict type now uses a “compact” representation based on a proposal by Raymond Hettinger which was first implemented by PyPy. The memory usage of the new dict() is between 20% and 25% smaller compared to Python 3.5.\n> \n> The order-preserving aspect of this new implementation is considered an implementation detail and should not be relied upon (this may change in the future, but it is desired to have this new dict implementation in the language for a few releases before changing the language spec to mandate order-preserving semantics for all current and future Python implementations; this also helps preserve backwards-compatibility with older versions of the language where random iteration order is still in effect, e.g. Python 3.5).\n\n啥情况?CPython的dict竟然优化了内存,还变有序了!?\n\n# Python 3.5 以前dict的实现\n\n先不着急看Python 3.6 里的dict,我们先来看看Python 3.5之前的dict是怎么实现的,再拿3.6来做对比。\n\n在Python 3.5以前,dict是用Hash表来实现的,而且Key和Value直接储存在Hash表上。想通过Key获取Value,只需通过Python内部的Hash函数计算出Key对应的Hash值,再映射到Hash表上对应的地址,访问该地址即可获取Key对应的Value。如下图所示:\n\n我们知道,Hash表读写时间复杂度在不发生冲突的情况下都是O(1)。\n\n为什么呢?我们可以把Hash表读写的步骤分开来看:\n\n1. 首先用Hash函数计算key的Hash值,Hash函数一般来说时间复杂度都是O(1)的。\n2. 计算出Hash值后,映射到Hash表内的数组下标,一般用取余数或是取二进制后几位的方式实现,时间复杂度也是O(1)。\n3. 然后用数组下标读取数组中实际储存的键值,数组的下标读取时间复杂度也是O(1)。\n\n这三个步骤串起来后复杂度并没有提升,总的时间复杂度自然也是O(1)的。\n\n而内部储存空间,Python字典中称为entries。entries相当于一个数组,是一段连续的内存空间,每个位置储存一个(Hash值,指向Key的指针,指向Value的指针)三元组。\n\n当然,由于抽屉原理,我们知道Hash表不可避免的会出现Hash冲突,Python的dict也不例外。\n\n而解决Hash冲突的方法有很多,比如C++的unordered_map和Go的map就用链地址法来解决冲突,用链表储存发生冲突的值。而Java更进一步,当链表长度超过8时就转换成红黑树,将链表O(n)的查找复杂度降为O(logn)。C#的HashTable则是用再散列法,内部有多个Hash函数,一次冲突了就换一个函数再算,直到不冲突为止。\n\n而Python的dict则是利用开放寻址法。当插入数据发生冲突时,就会从那个位置往后找,直到找到有空位的地址为止。要查的时候,也是把下标值映射到到地址后,先对比一下下标值相不相等,若不相等则往后继续对比。\n\n这也造成个问题,dict中的元素不能直接从entries中清理掉,不然往后寻找的查找链就会断掉了。只能是先标记住删除,等到一定时机再一并清理。\n\n此外我们也知道,当冲突过发生得过多,dict读写所需的时间也会变多,时间复杂度不再是O(1),这也是Hash表的通病了。\n\nPython中dict初始化时,内部储存空间entries容量为8。当内部储存空间占用到一定程度(entries容量×装填因子,Python的dict中装填因子是2/3)后,就会进行倍增扩容。每次扩容都要遍历原先的元素,时间复杂度为O(n),但基本上插入O(n)次之后才会进行一次扩容,所以扩容的均摊时间复杂度为O(1)。而扩容时会重新进行Hash值到entries位置的映射,此时就是把标记删除但仍留在entries中的元素清理掉的最佳时机。\n\nPython3.5之前这种dict的实现就有两个毛病:\n\n1. 元素的顺序不被记录。两个Key值通过Hash函数的出来的Hash值不一定能保证原来的大小关系,由于Hash冲突、扩容等影响元素的顺序也会变化。当然这种无序性也是Hash表通用的特点了。\n2. 占用了太多了无用空间。上面说到entries中每个位置储存一个(Hash值,指向Key的指针,指向Value的指针)三元组,没用到或是标记删除的位置占用了大量的空间。\n\n于是,Raymond Hettinger就提出了一种新的dict实现方式。在CPython3.6中就使用了这种新的实现方式。\n\n# CPython3.6中dict的实现\n\n当要实现一个如下的dict时:\n\n```python\nd = {\n 'timmy': 'red', \n 'barry': 'green', \n 'guido': 'blue'\n}\n```\n\n如在上一节中所讲,在Python3.5以前,在内存储存的形式可以表示成这样子:\n\n```python\nentries = [['--', '--', '--'],\n [-8522787127447073495, 'barry', 'green'],\n ['--', '--', '--'],\n ['--', '--', '--'],\n ['--', '--', '--'],\n [-9092791511155847987, 'timmy', 'red'],\n ['--', '--', '--'],\n [-6480567542315338377, 'guido', 'blue']]\n```\n\n而CPython3.6以后,是以这种形式储存在内存中的:\n\n```python\nindices = [None, 1, None, None, None, 0, None, 2]\nentries = [[-9092791511155847987, 'timmy', 'red'],\n [-8522787127447073495, 'barry', 'green'],\n [-6480567542315338377, 'guido', 'blue']]\n```\n\n改变了什么?\n\n1. dict内部的entries改为按插入顺序存储,新增了一个indices用于储存元素在entries中的下标。dict整体仍是Hash表结构,但Hash值映射到indices中,而不是直接映射到entries中。\n2. 由于entries改为了按插入顺序存储,使得申请entries容量时只要申请Hash表长度的2/3即可,省去了Hash表中的无用空间,储存更紧凑。\n3. dict读写步骤从原先的3步变为4步:计算key的Hash值,映射到indices内存空间,从indices读取entries的下标值,用下标从entries中读写数据。读写时间复杂度仍保持为O(1),冲突、删除标记等Hash表的特性也仍然存在。indices的扩容策略也仍然是倍增扩容,但因为填充因子仍然为2/3,entries每次扩容时只需申请indices长度的2/3即可。\n\n有什么好处?\n\n1. 压缩空间:原先Hash映射是直接映射到entries上,会有大量的空隙。现在Hash映射到indices上,而entries中可更紧凑地存储元素。而indices中储存的entries下标占用内存可以比entries元素要小得多——当entries长度足够短时每个下标只需占一个字节。indices中确实也还仍有空隙,但占用空间总要比旧的dict实现要小得多了。\n2. 更快的遍历:以前的实现遍历dict要遍历整个Hash表,需要挨个位置读取一下,判断它是空闲位置还是实际存在的元素。而现在只需要对变得更紧凑的entries遍历就行了。这也带来一个新的特性:entries是按照元素插入的顺序存储的,遍历entries自然也会按元素插入的顺序输出。这就给dict带来了有序性。\n3. 扩容时关注的内存块更少。原先的entries扩容时所有数据都要重新映射到内存上,cache利用率不好。现在扩容时基本可以整个entries直接复制(当然,有删除标记的数据这时要忽略)。\n\n综上,CPython3.6以后通过增加了一个indices增加了空间利用率,在维持读写时间复杂度不变的情况下增加了遍历与扩容效率。至于dict遍历变得有序,倒是有点次要的特性了。\n\n# 我们是否应利用新dict的有序性?\n\n既然Python中dict变得有序了,那我们是否应该主动去利用它呢?我是这么认为的:\n\n1. 在Python3.6中,我们不推荐利用dict的有序性。3.6时dict的有序性还只是CPython的一个实现细节,并不是Python的语言特性。当我们的代码不是在CPython环境下运行,dict的有序性就不起作用,就容易出莫名其妙的BUG了。\n2. 在Python3.7后,dict按插入顺序进行遍历的性质被写入Python语言特性中。这时确实在代码中利用dict有序性也没什么大问题。但dict这种数据结构,最主要的特性还是表现在Key映射到Value的这种关系,以及O(1)的读写时间复杂度。当我们的代码中需要关注到dict的遍历顺序时,我们就要先质问一下自己:是否应该改为用队列或是其他数据结构来实现?\n\n\n# 参考文献\n\n- [Are dictionaries ordered in Python 3.6+?](https://stackoverflow.com/questions/39980323/are-dictionaries-ordered-in-python-3-6)\n- [[Python-Dev] Python 3.6 dict becomes compact and gets a private version; and keywords become ordered](https://mail.python.org/pipermail/python-dev/2016-September/146327.html)\n- [[Python-Dev] More compact dictionaries with faster iteration](https://mail.python.org/pipermail/python-dev/2012-December/123028.html)\n- [关于python3.6中dict如何保证有序](https://zhuanlan.zhihu.com/p/36167600)\n- [python3.7源码分析-字典_小屋子大侠的博客-CSDN博客_python 字典源码](https://blog.csdn.net/qq_33339479/article/details/90446988)\n- [《深度剖析CPython解释器》9. 解密Python中字典和集合的底层实现,深度分析哈希表](https://www.cnblogs.com/traditional/p/13503114.html)\n- [CPython 源码阅读 - dict](http://blog.dreamfever.me/2018/03/12/cpython-yuan-ma-yue-du-dict/)","title":"Python字典的实现原理","abstract":"> CPython从3.6开始,字典(dict)不再是无序的了——字典的修改了原先的底层实现,变得能按字典插入的顺序进行遍历。而Python从3.7开始将字典的有序性写入语言特性,不管是Jython、IronPython还是其他Python实现,从3.7开始大家的字典都是有序的了。\n以前参加Python相关的面试时,面试官经常都会问一个问题:Python里的字典(dict)是有序的吗?\n这自然难不倒我,我也照本宣科地讲:Python的字典底层是用哈希表实现的,在不发生冲突时读写的时间复杂度是O(1),比读写时间复杂度为O(logn)的红黑树要更快。但红黑树可以按下标的大小顺序进行遍历,而Dict遍历时是无序的。","length":121,"created_at":"2020-08-02T00:10:10.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["Python","数据结构"],"license":false}}]},"__N_SSG":true} \ No newline at end of file diff --git "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\235\202\346\212\200.json" "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\235\202\346\212\200.json" similarity index 100% rename from "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\235\202\346\212\200.json" rename to "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\235\202\346\212\200.json" diff --git "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\235\202\350\260\210.json" "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\235\202\350\260\210.json" similarity index 100% rename from "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\235\202\350\260\210.json" rename to "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\346\235\202\350\260\210.json" diff --git "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\254\224\350\256\260.json" "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\254\224\350\256\260.json" similarity index 100% rename from "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\254\224\350\256\260.json" rename to "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\254\224\350\256\260.json" diff --git "a/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\256\227\346\263\225.json" "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\256\227\346\263\225.json" new file mode 100644 index 00000000..6a594476 --- /dev/null +++ "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\256\227\346\263\225.json" @@ -0,0 +1 @@ +{"pageProps":{"allTagInfos":[{"tag":"Kubernetes","slug":"kubernetes","path":"/tags/kubernetes","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"DevOps","slug":"devops","path":"/tags/devops","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Docker","slug":"docker","path":"/tags/docker","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Cloud Native","slug":"cloud-native","path":"/tags/cloud-native","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Blog","slug":"blog","path":"/tags/blog","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"init-a-new-hexo-project","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"VSCode","slug":"vscode","path":"/tags/vscode","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"Hexo","slug":"hexo","path":"/tags/hexo","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"JavaScript","slug":"javascript","path":"/tags/javascript","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"GitHub","slug":"github","path":"/tags/github","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"AWS","slug":"aws","path":"/tags/aws","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"CI/CD","slug":"ci-cd","path":"/tags/ci-cd","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"IaC","slug":"iac","path":"/tags/iac","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]},{"tag":"设计模式","slug":"设计模式","path":"/tags/设计模式","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"笔记","slug":"笔记","path":"/tags/笔记","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"排序","slug":"排序","path":"/tags/排序","postSlugs":[{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"Python","slug":"python","path":"/tags/python","postSlugs":[{"postSlug":"python-dict","postType":"article"}]},{"tag":"C++","slug":"c++","path":"/tags/c++","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"}]},{"tag":"杂技","slug":"杂技","path":"/tags/杂技","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"hello-world","postType":"article"}]},{"tag":"杂谈","slug":"杂谈","path":"/tags/杂谈","postSlugs":[{"postSlug":"hello-world","postType":"article"}]},{"tag":"Nextjs","slug":"nextjs","path":"/tags/nextjs","postSlugs":[{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"Cloud Computing","slug":"cloud-computing","path":"/tags/cloud-computing","postSlugs":[{"postSlug":"newest","postType":"idea"}]}],"selectedTagInfo":{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},"posts":[{"slug":"Handy-heap-cheat-sheet","file":"public/content/articles/2021-03-21-Handy-heap-cheat-sheet.md","mediaDir":"content/articles/2021-03-21-Handy-heap-cheat-sheet","path":"/articles/Handy-heap-cheat-sheet","meta":{"content":"\n# 如何手撕一个堆\n\n# 写在前面\n\n在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\n\n# 首先要理解,然后才能实现\n\n就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\n\n## 先抓住重点:堆是一种树结构\n\n首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\n\n再进一步,在堆这种树结构中,最重要的约束就是:**对于树中的每个节点,总有父节点大于两个子节点**(以大顶堆为例,下同)。\n\n如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\n\n而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\n\n1. 堆并不关注左右子树之间的大小情况,那么**要维护一个堆,基本只需要做交换父节点与子节点的操作**,而不需要像二叉查找树那样做各种旋转操作。\n2. 因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,**可以使用数组进行实现**。\n3. 因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:**堆的增删操作最坏时间复杂度为O(logn)**。\n\n## 再抓基本操作:上浮与下沉\n\n上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点**过大**,就跟他的父节点**向上交换**;若一个节点**过小**,就跟他的子节点**向下交换**。\n\n假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\n\n![p与g交换](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png)\n\n由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\n\n1. p > g > p2\n2. g > 原p > c1与c2\n\n则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\n\n同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\n\n为了简化,我们把前面那种递归地向上交换称为**上浮操作**,把后面这种递归地向下交换称为**下沉操作**。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\n\n# 各种接口的逻辑\n\n## 插入元素——入堆\n\n把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\n\n虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\n\n## 删除堆顶元素——出堆\n\n我们从堆中删除元素时,一般只会删除堆顶元素。\n\n删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\n\n这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\n\n## 堆的初始化——建堆\n\n建立一个堆,我们有两种思路:\n\n1. 将元素一个一个插入,即对每个元素都做一次入堆操作。\n2. 当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\n\n下面我们来分析这两种建堆策略。\n\n### 元素逐个入堆\n\n上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\n\n插入第i个元素时,堆的大小为$i$(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\n\n$$T(i) = logi$$\n\n那么把所有元素上浮,则总时间复杂度为:\n\n$$\n\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logi\\\\\n&= 1\\times0 + 2\\times1 + ... + 2^{logn}\\times{logn} \\\\\n&=O(nlogn)\n\\end{aligned}\n$$\n\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\n\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\n\n### 堆合并\n\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\n\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\n\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\n\n$$\nT(i) = logn-logi\n$$\n\n那么把所有元素下沉,则总时间复杂度为:\n\n$$\n\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logn-logi \\\\\n&= \\frac{n}{2^{logn}}\\times{logn}+ ... + \\frac{n}{4}\\times2+\\frac{n}{2}\\times1 \\\\\n&= O(n)\n\\end{aligned}\n$$\n\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\n\n### 两种策略的比较与理解\n\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\n\n1. 从元素移动路径的角度\n\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\n\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\n\n2. 从元素移动数量与移动距离的角度\n\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\n\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\n\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\n\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\n\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\n\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\n\n# 代码实现\n\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\n\n```python\nT = TypeVar(\"T\")\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n```\n\n## 实现树结构\n\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\n\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\n\n```python\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n```\n\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\n\n## 实现基本操作——上浮与下沉\n\n### 上浮\n\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\n\n```python\ndef floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n```\n\n### 下沉\n\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\n\n```python\ndef size(self):\n '''返回堆大小\n '''\n return len(self.A)\n\ndef sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n```\n\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\n\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\n\n## 实现各种借口——读、增、删、初始化\n\n### 读取堆顶\n\n堆一般只允许读取堆顶,即全堆最大元素。\n\n```python\ndef top(self):\n '''返回堆顶\n '''\n return self.A[0]\n```\n\n### 入堆\n\n入堆时,把元素加到堆尾,再做上浮操作。\n\n```python\ndef insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n```\n\n### 出堆\n\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\n\n```python\ndef pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n注意入堆与出堆操作都要保证堆的大小会相应变化。\n\n### 堆初始化\n\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\n\n```python\ndef __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n```\n\n## 整体代码\n\n### 堆的整体实现\n\n综上,堆的整体代码实现如下:\n\n```python\nfrom typing import Any, Callable, Generic, List, TypeVar\n\nT = TypeVar(\"T\")\n\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n def __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n \n def size(self):\n '''返回堆大小\n '''\n return len(self.A)\n \n def top(self):\n '''返回堆顶\n '''\n return self.A[0]\n \n def sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n\n def floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n \n def insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n\n def pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n### 单元测试\n\n入堆、出堆等操作的简单单元测试如下:\n\n```python\nimport pytest\nimport heap\n\n@pytest.fixture\ndef initHeap():\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \n lambda a,b:a>b)\n\nclass Test_TestHeap:\n def test_init_notNull(self, initHeap:heap.Heap):\n assert initHeap.size() == 10\n assert initHeap.top() == 9\n \n def test_insert_notTop(self, initHeap:heap.Heap):\n initHeap.insert(6)\n assert initHeap.size() == 11\n assert initHeap.top() == 9\n \n def test_insert_top(self, initHeap:heap.Heap):\n initHeap.insert(10)\n assert initHeap.size() == 11\n assert initHeap.top() == 10\n \n def test_pop(self, initHeap:heap.Heap):\n p = initHeap.pop()\n assert p == 9\n assert initHeap.size() == 9\n assert initHeap.top() == 8\n```\n\n# 关于堆排序\n\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\n\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\n\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\n\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。","title":"如何手撕一个堆","abstract":"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。","length":483,"created_at":"2021-08-28T23:09:14.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","算法竞赛"],"license":false}},{"slug":"Sort-algorithm","file":"public/content/articles/2021-01-11-Sort-algorithm.md","mediaDir":"content/articles/2021-01-11-Sort-algorithm","path":"/articles/Sort-algorithm","meta":{"content":"\n# 序言\n\n我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。\n\n你会发现堆排序被当成是选择排序的一种优化——因为我认为堆排序主要在于使用了堆这种数据结构,而总体思想与选择排序相比没有太大变化。\n\n你还会找到其他与别的文章不一样的地方。因为这篇文章是我按照自己的理解来写的,我脑子里是这样想的,那文章里就是这样写的。我会按照我的理解,从纵向与横向两个维度,来理清楚各个排序算法的特性与异同。\n\n# 整篇文章的要点\n\n整篇文章以纵向——算法分类、以及横向——算法评价两个维度来进行组织。\n\n排序算法可以按照以下方式来进行分类:\n\n- 基于比较的排序算法\n - 基于分治思想\n - 快速排序\n - 归并排序\n - 基于有序区域扩展\n - 插入排序\n - 选择排序\n- 不基于比较的排序算法\n - 计数排序\n - 桶排序\n - 基数排序\n\n文章中还会讲一讲为什么会这么分,每种分类有什么共性,分类之间有什么差异。此外,在最后还会稍微提一提外部排序与适用于并行运算的排序等。\n\n而对于纵向分类中的每一个端点,我们又会从以下五个方面,来对各个算法进行一个总体评价:\n\n- 时间复杂度(最坏,最好,平均※)\n- 空间复杂度\n- 是否原地排序\n- 是否稳定排序\n- 能否用于链表排序\n\n而由于复杂度主要只关注数量级,因此在这篇文章里会在不影响计算结果的前提下对复杂度计算进行适当的近似与简化。\n\n# 快排\n\n## 思想\n\n分治法:先把序列分为小的部分和大的部分,再将两部分分别排序。即:复杂分割,简单合并,主要操作在于分割。\n\n## 要点\n\n### 时间复杂度\n\n推导式:T(n) = T(找) + T(左) + T(右) = O(n) + 两个子问题时间复杂度。\n\n- 最好时间复杂度为每次都正好找到最中间的一个数时时间复杂度为O(nlogn)。证明略。\n- 最坏时间复杂度为每次都正好找到最旁边的数时时间复杂度为O(n^2)。证明略。\n- 平均时间复杂度为O(nlogn),推导式如下:\n\n 快速排序每一步中,将元素分为左右两边需要遍历整个列表,耗时T(n)。假设最后定位的元素为最终第i个元素,则两个子问题复杂度分别为T(i)和T(n-i-1)。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{\\sum_{i=0}^{n-1}{T(i)+T(n-i-1)}}{n} \\\\\n &=n + \\frac{2}{n}\\times\\sum_{i=0}^{n-1}{T(i)} \\\\\n \\end{aligned}\n $$\n\n 令 $$\\sum_{i=0}^{n}T(i) = Sum(n)$$ ,即有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{2}{n} \\times Sum(n-1) \\\\\n Sum(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1)\n \\end{aligned}\n $$\n\n 错位相减:\n\n $$\n \\begin{aligned}\n Sum(n) - Sum(n-1) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1) - \\frac{n}{2}T(n) + \\frac{n}{2}(n) \n \\\\\n T(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n}{2}T(n) - \\frac{2n+1}{2} \n \\\\\n \\frac{T(n+1)}{n+2} &= \\frac{T(n)}{n+1}+\\frac{2n+1}{(n+1)(n+2)} \n \\\\\n &= \\frac{T(n)}{n+1}+\\frac{1}{n}+\\frac{1}{n+1} \n \\\\\n &=...\n \\\\\n &= \\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+\\frac{1}{3}+...+\\frac{1}{n})+\\frac{1}{n+1}\n \\\\\n \\\\\n \\frac{T(n)}{n+1}&=\\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+...+\\frac{1}{n-1})+\\frac{1}{n}\n \\\\\n &= O(1)+O(1)+O(logn)+O(\\frac{1}{n})\n \\\\\n &= O(logn)\n \\end{aligned}\n $$\n\n 其中由于 $$\\frac{1}{x}=\\frac{d(logx)}{dx}$$ ,因此 $$\\frac{1}{2}+...+\\frac{1}{n-1}=O(logn)$$ 。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n)&=(n+1)\\times O(logn)\\\\\n &=O(n)\\times O(logn)\\\\\n &=O(nlogn)\n \\end{aligned}\n $$\n\n### 额外空间复杂度\n\n考虑栈深度,额外空间复杂度为O(logn)。由于快速排序主要步骤在于分,因此必须自上而下的进行递归,无法避免栈深度。\n\n### 原地排序\n\n虽然快速排序有额外空间复杂度,但并不妨碍它是一个原地排序。\n\n### 不稳定\n\n堆排序在分操作时将元素左右交换,会破坏稳定性。\n\n### 链表形式特点\n\n- 时间复杂度不变\n- 空间复杂度不变\n- 变为稳定排序※\n\n## 手写时的易错点\n\n- 分成左右子序列时最好完全分开(一边用`<=`一边用`>`),不然容易造成死循环。\n- 分左右子序列时仔细考虑最初下标位置与最终下标位置,以及对应位置的值的大小。\n- 不要忘记递归的结束条件。\n\n# 归并排序\n\n## 思想\n\n分治法:先把两个子序列各自排好序,然后再合并两个子序列。即:简单分割,复杂合并。主要步骤在于合并。\n\n## 要点\n\n- 时间复杂度推导式:T(n) = 2T(n/2) + T(合)\n- 平均、最好、最坏时间复杂度都是O(nlogn),推导过程略。\n- 额外空间复杂度为O(n),合并时必须准备额外空间。但由于主要步骤在于合并,可以自下而上地进行迭代合并,可以不使用栈。\n- 非原地排序\n- 稳定排序\n\n### 链表形式\n\n- 时间复杂度不变\n- 额外空间复杂度变为O(1)※\n- 稳定排序\n- 只能使用迭代形式,不能使用递归形式\n\n## 易错点\n\n- 合并时一边结束时另一边还未结束,需要把那一边也放入合并后序列中\n- 保持稳定排序:合并时左序列等于右序列时也采用左序列\n- 不要忘记递归结束条件\n- 不要忘记循环递进条件\n\n## 进阶\n\n- 原地归并排序:时间复杂度为O(log^2n),牺牲合并的时间复杂度进行原地排序。\n- 多路归并排序:使用竞标树,多路归并,用于磁盘IO。\n\n# 插入排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将无序序列当前元素插入有序序列,复杂度取决于有序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:基本有序情况下,O(n)\n- 额外空间复杂度:O(1)\n- 原地排序\n- 稳定排序\n- 每次插入时都要移动序列,写次数较多\n- 若查找插入位置时使用二分法查找,则可加快时间。(但不足以对时间复杂度造成影响,且最好时间复杂度也会上升为O(nlogn))\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 稳定排序\n- 插入时不再需要移动序列,但也不能使用二分法查找\n\n## 进阶——希尔排序\n\n- 要点:\n\n 插入排序的优点在于当序列基本有序时,时间复杂度可逼近为O(n)。\n\n 但插入时移动有序序列中元素所耗时间较多,而每次只移动一步。但实际上当序列分布均匀时,有序序列中排靠后的元素在整个序列中也会排靠后。\n\n 可以把序列分为几个大步长序列,在最初的几次插入放开移动步长,让大的元素直接移动到较后位置。再往后慢慢缩小步长,此时序列基本有序,可以利用基本有序时插入排序的优势。\n\n- 时间复杂度\n 1. 当步长为2^i时,不能使时间复杂度缩短为O(nlogn)。因为一个子序列所有元素有可能比另一个子序列最大元素都要大,这时插入排序仍需进行约n^2次操作\n 2. 当步长为2^i时效率较低,因为当步长为4已经有序时,步长为2再比较是无用比较。但由于1.的问题,不能节省比较时间。\n 3. 当步长之间最小公约数较少,甚至互质时,无用比较次数会降低。\n 4. 最坏时间复杂度下限为 $$O(nlog^2n)$$ (当步长采用 $$2^i3^j$$ 时),但一般希尔排序平均时间复杂度都为 $$O(n^{\\frac{3}{2}})$$\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序:希尔排序步长较大时会发生前后跳转。\n- 不能写为链表形式\n\n# 选择排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将选无序序列中最小元素加到有序序列末尾,复杂度取决于无序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:O(n2),如能保证无序部分的最小元素所在位置一定(堆排序),能降低时间复杂度\n- 额外空间复杂度:O(1)\n- 原地排序\n- 不稳定排序(采用元素交换策略时)\n- 每次找到最小元素后,只需交换一次位置即可,写次数较少。\n- 若找到最小元素后,不直接交换而是进行数组移动,则可进行稳定排序,但写次数变多,与插入排序相比没有优势,也不能使用二分查找进行简化。\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 变为稳定排序※(因为链表不需要数组移动,稳定排序方式的缺点得以消除)\n\n## 进阶——堆排序\n\n- 要点:\n\n 选择排序中耗时最多的是取出无序序列中最小值的时间,需要遍历整个无序序列。\n\n 但实际上我们只关心无序序列中的最小值,而不关心其他值的位置。通过将无序序列建为堆,减少选择时间,降低总的时间复杂度。\n\n- 时间复杂度O(nlogn)\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序\n- 操作时间复杂度:每次向下比较关注一个节点与其左右子堆顶元素,每次向上比较只关注节点与其父元素(大顶堆,堆大小为n)\n - 下沉:向下比较,若顶元素不是最大,将顶元素与较大的子堆堆顶元素交换。递归处理该子堆顶元素,直到向下比较顶元素最大。\n\n 最好时间复杂度O(1),最坏时间复杂度为O(h)=O(logn),平均O(logn)\n\n - 上浮:向上比较,若元素比其上层要大,交换该元素与其上层元素。递归处理其上层元素,直到向上比较不比上层要大。\n\n 最好时间复杂度O(1),最坏时间复杂度O(h)=O(logn),平均O(logn)\n\n - 入堆:堆扩容一位,将新元素插到尾部,将该元素上浮,最坏、平均时间复杂度O(logn)\n - 出堆:取出堆顶元素,将尾部放到堆顶,将该元素下沉,最坏、平均时间复杂度O(logn)\n - 缺点:通常堆尾元素较小,出堆时将堆尾元素放到堆顶再下沉基本要沉到堆底,无用比较较多\n- 建堆时间复杂度O(n)\n - 策略1:从头开始建堆,逐个元素插入,时间复杂度取决于最后一层,时间复杂度为O(nlogn)\n - 每次将堆扩容一位,将末尾元素上浮。\n - 时间复杂度推导:\n\n 每次插入时间:\n\n $$\n \\begin{aligned} T(i) &= h\\\\\n &= logi\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n}logi\\\\\n &= 1\\times1 + 2\\times2+ 3\\times4 + ...+h\\times \\frac{n}{2} \\\\\n \\\\\n 2T(n) &= 1\\times2 + 2\\times4+ 3\\times8 + ...+h\\times n \\\\\n \\\\\n 2T(n)-T(n) &= h\\times n - (1+2+4+...+2^{h-1}) \\\\\n &= h\\times n - O(2^h) \\\\\n \\\\\n T(n) &= nlogn - O(2^h) \\\\\n &= O(nlogn)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(nlogn)$$\n\n - 策略2:从后开始建堆,小堆合并(逐个元素下沉),时间复杂度为O(n)\n - 每次堆合并时,有三部分:左子堆,右子堆,顶元素。下沉顶元素。\n - 时间复杂度推导:\n\n 第i个元素合并时,时间为:\n\n $$\n \\begin{aligned}\n T(i) &= h_{子堆}\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n} (h_{子堆}) \\\\\n &= h + (h-1)\\times2 + ... + 2 \\times \\frac{n}{4} + 1 \\times\\frac{n}{2}\\\\\n \\\\\n 2T(n) &= h \\times 2 + (h-1) \\times 4 + ... + 2 \\times \\frac{n}{2} + n \\\\\n \\\\\n 2T(n) - T(n) &= n + \\frac{n}{2} + ... + 2 - h \\\\\n \\\\\n T(n) &= O(n) - h \\\\ \n &= O(n)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(n)$$ \n\n - 虽说每步是做一个小堆合并,但实际上从堆尾到堆头遍历,相当于仅关注元素没有稳定,相当于可以直接使用下沉操作。\n\n# 基于比较的排序算法时间复杂度下限:逆序对思想\n\n基于比较的排序算法可以看作序列逆序对的消除。完全随机序列逆序对数量为O(n^2),若一次元操作只消除一个逆序对,则时间复杂度不会低于O(n^2)。降低时间复杂度关键在于一次消除多个逆序对。\n\n1. 希尔排序通过增大最初的步长来企图一次消除多个逆序对。\n2. 归并排序消除逆序对最主要在于归并步骤。最后几次合并每个子步骤用O(1)时间消除O(n)个逆序对。\n3. 快速排序消除逆序对最主要在于划分步骤。每个划分步骤用O(n)时间消除O(n)+O(左长度×右长度)个逆序对。\n4. 堆排序逆序对消除方式比较Tricky,但可以看出消除逆序对大致在于出堆步骤,通过O(logn)时间复杂度消除O(n)个逆序对。(左小右大排序时需要建立左大右小的大顶堆,建堆时基本没有消除逆序对)\n\n# 最后\n\n这篇文章我们主要关注了排序算法中的大头——基于比较的排序算法。在下篇文章,我们再来看一下不基于比较的排序算法,以及外排序与并行排序。","title":"排序算法","abstract":"我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。","length":342,"created_at":"2021-01-11T22:57:10.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","排序"],"license":false}}]},"__N_SSG":true} \ No newline at end of file diff --git "a/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\256\227\346\263\225\347\253\236\350\265\233.json" "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\256\227\346\263\225\347\253\236\350\265\233.json" new file mode 100644 index 00000000..072b6c47 --- /dev/null +++ "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\347\256\227\346\263\225\347\253\236\350\265\233.json" @@ -0,0 +1 @@ +{"pageProps":{"allTagInfos":[{"tag":"Kubernetes","slug":"kubernetes","path":"/tags/kubernetes","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"DevOps","slug":"devops","path":"/tags/devops","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Docker","slug":"docker","path":"/tags/docker","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Cloud Native","slug":"cloud-native","path":"/tags/cloud-native","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Blog","slug":"blog","path":"/tags/blog","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"init-a-new-hexo-project","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"VSCode","slug":"vscode","path":"/tags/vscode","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"Hexo","slug":"hexo","path":"/tags/hexo","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"JavaScript","slug":"javascript","path":"/tags/javascript","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"GitHub","slug":"github","path":"/tags/github","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"AWS","slug":"aws","path":"/tags/aws","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"CI/CD","slug":"ci-cd","path":"/tags/ci-cd","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"IaC","slug":"iac","path":"/tags/iac","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]},{"tag":"设计模式","slug":"设计模式","path":"/tags/设计模式","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"笔记","slug":"笔记","path":"/tags/笔记","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"排序","slug":"排序","path":"/tags/排序","postSlugs":[{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"Python","slug":"python","path":"/tags/python","postSlugs":[{"postSlug":"python-dict","postType":"article"}]},{"tag":"C++","slug":"c++","path":"/tags/c++","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"}]},{"tag":"杂技","slug":"杂技","path":"/tags/杂技","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"hello-world","postType":"article"}]},{"tag":"杂谈","slug":"杂谈","path":"/tags/杂谈","postSlugs":[{"postSlug":"hello-world","postType":"article"}]},{"tag":"Nextjs","slug":"nextjs","path":"/tags/nextjs","postSlugs":[{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"Cloud Computing","slug":"cloud-computing","path":"/tags/cloud-computing","postSlugs":[{"postSlug":"newest","postType":"idea"}]}],"selectedTagInfo":{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]},"posts":[{"slug":"Handy-heap-cheat-sheet","file":"public/content/articles/2021-03-21-Handy-heap-cheat-sheet.md","mediaDir":"content/articles/2021-03-21-Handy-heap-cheat-sheet","path":"/articles/Handy-heap-cheat-sheet","meta":{"content":"\n# 如何手撕一个堆\n\n# 写在前面\n\n在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\n\n# 首先要理解,然后才能实现\n\n就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\n\n## 先抓住重点:堆是一种树结构\n\n首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\n\n再进一步,在堆这种树结构中,最重要的约束就是:**对于树中的每个节点,总有父节点大于两个子节点**(以大顶堆为例,下同)。\n\n如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\n\n而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\n\n1. 堆并不关注左右子树之间的大小情况,那么**要维护一个堆,基本只需要做交换父节点与子节点的操作**,而不需要像二叉查找树那样做各种旋转操作。\n2. 因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,**可以使用数组进行实现**。\n3. 因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:**堆的增删操作最坏时间复杂度为O(logn)**。\n\n## 再抓基本操作:上浮与下沉\n\n上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点**过大**,就跟他的父节点**向上交换**;若一个节点**过小**,就跟他的子节点**向下交换**。\n\n假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\n\n![p与g交换](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png)\n\n由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\n\n1. p > g > p2\n2. g > 原p > c1与c2\n\n则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\n\n同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\n\n为了简化,我们把前面那种递归地向上交换称为**上浮操作**,把后面这种递归地向下交换称为**下沉操作**。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\n\n# 各种接口的逻辑\n\n## 插入元素——入堆\n\n把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\n\n虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\n\n## 删除堆顶元素——出堆\n\n我们从堆中删除元素时,一般只会删除堆顶元素。\n\n删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\n\n这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\n\n## 堆的初始化——建堆\n\n建立一个堆,我们有两种思路:\n\n1. 将元素一个一个插入,即对每个元素都做一次入堆操作。\n2. 当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\n\n下面我们来分析这两种建堆策略。\n\n### 元素逐个入堆\n\n上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\n\n插入第i个元素时,堆的大小为$i$(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\n\n$$T(i) = logi$$\n\n那么把所有元素上浮,则总时间复杂度为:\n\n$$\n\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logi\\\\\n&= 1\\times0 + 2\\times1 + ... + 2^{logn}\\times{logn} \\\\\n&=O(nlogn)\n\\end{aligned}\n$$\n\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\n\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\n\n### 堆合并\n\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\n\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\n\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\n\n$$\nT(i) = logn-logi\n$$\n\n那么把所有元素下沉,则总时间复杂度为:\n\n$$\n\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logn-logi \\\\\n&= \\frac{n}{2^{logn}}\\times{logn}+ ... + \\frac{n}{4}\\times2+\\frac{n}{2}\\times1 \\\\\n&= O(n)\n\\end{aligned}\n$$\n\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\n\n### 两种策略的比较与理解\n\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\n\n1. 从元素移动路径的角度\n\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\n\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\n\n2. 从元素移动数量与移动距离的角度\n\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\n\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\n\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\n\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\n\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\n\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\n\n# 代码实现\n\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\n\n```python\nT = TypeVar(\"T\")\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n```\n\n## 实现树结构\n\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\n\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\n\n```python\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n```\n\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\n\n## 实现基本操作——上浮与下沉\n\n### 上浮\n\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\n\n```python\ndef floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n```\n\n### 下沉\n\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\n\n```python\ndef size(self):\n '''返回堆大小\n '''\n return len(self.A)\n\ndef sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n```\n\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\n\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\n\n## 实现各种借口——读、增、删、初始化\n\n### 读取堆顶\n\n堆一般只允许读取堆顶,即全堆最大元素。\n\n```python\ndef top(self):\n '''返回堆顶\n '''\n return self.A[0]\n```\n\n### 入堆\n\n入堆时,把元素加到堆尾,再做上浮操作。\n\n```python\ndef insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n```\n\n### 出堆\n\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\n\n```python\ndef pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n注意入堆与出堆操作都要保证堆的大小会相应变化。\n\n### 堆初始化\n\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\n\n```python\ndef __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n```\n\n## 整体代码\n\n### 堆的整体实现\n\n综上,堆的整体代码实现如下:\n\n```python\nfrom typing import Any, Callable, Generic, List, TypeVar\n\nT = TypeVar(\"T\")\n\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n def __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n \n def size(self):\n '''返回堆大小\n '''\n return len(self.A)\n \n def top(self):\n '''返回堆顶\n '''\n return self.A[0]\n \n def sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n\n def floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n \n def insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n\n def pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n### 单元测试\n\n入堆、出堆等操作的简单单元测试如下:\n\n```python\nimport pytest\nimport heap\n\n@pytest.fixture\ndef initHeap():\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \n lambda a,b:a>b)\n\nclass Test_TestHeap:\n def test_init_notNull(self, initHeap:heap.Heap):\n assert initHeap.size() == 10\n assert initHeap.top() == 9\n \n def test_insert_notTop(self, initHeap:heap.Heap):\n initHeap.insert(6)\n assert initHeap.size() == 11\n assert initHeap.top() == 9\n \n def test_insert_top(self, initHeap:heap.Heap):\n initHeap.insert(10)\n assert initHeap.size() == 11\n assert initHeap.top() == 10\n \n def test_pop(self, initHeap:heap.Heap):\n p = initHeap.pop()\n assert p == 9\n assert initHeap.size() == 9\n assert initHeap.top() == 8\n```\n\n# 关于堆排序\n\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\n\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\n\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\n\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。","title":"如何手撕一个堆","abstract":"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。","length":483,"created_at":"2021-08-28T23:09:14.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","算法竞赛"],"license":false}}]},"__N_SSG":true} \ No newline at end of file diff --git "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\350\256\276\350\256\241\346\250\241\345\274\217.json" "b/_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\350\256\276\350\256\241\346\250\241\345\274\217.json" similarity index 100% rename from "_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\350\256\276\350\256\241\346\250\241\345\274\217.json" rename to "_next/data/06cVAX8N9Z8KmESdU4cc2/tags/\350\256\276\350\256\241\346\250\241\345\274\217.json" diff --git a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/Handy-heap-cheat-sheet.json b/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/Handy-heap-cheat-sheet.json deleted file mode 100644 index 90c87082..00000000 --- a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/articles/Handy-heap-cheat-sheet.json +++ /dev/null @@ -1 +0,0 @@ -{"pageProps":{"slug":"Handy-heap-cheat-sheet","tags":[{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]}],"source":{"compiledSource":"/*@jsxRuntime automatic @jsxImportSource react*/\nconst {Fragment: _Fragment, jsx: _jsx, jsxs: _jsxs} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = Object.assign({\n h1: \"h1\",\n a: \"a\",\n p: \"p\",\n h2: \"h2\",\n strong: \"strong\",\n ol: \"ol\",\n li: \"li\",\n img: \"img\",\n h3: \"h3\",\n span: \"span\",\n math: \"math\",\n semantics: \"semantics\",\n mrow: \"mrow\",\n mi: \"mi\",\n annotation: \"annotation\",\n mo: \"mo\",\n div: \"div\"\n }, _provideComponents(), props.components);\n return _jsxs(_Fragment, {\n children: [_jsx(_components.h1, {\n id: \"如何手撕一个堆\",\n children: _jsx(_components.a, {\n href: \"#如何手撕一个堆\",\n children: \"如何手撕一个堆\"\n })\n }), \"\\n\", _jsx(_components.h1, {\n id: \"写在前面\",\n children: _jsx(_components.a, {\n href: \"#写在前面\",\n children: \"写在前面\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\"\n }), \"\\n\", _jsx(_components.h1, {\n id: \"首先要理解然后才能实现\",\n children: _jsx(_components.a, {\n href: \"#首先要理解然后才能实现\",\n children: \"首先要理解,然后才能实现\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\"\n }), \"\\n\", _jsx(_components.h2, {\n id: \"先抓住重点堆是一种树结构\",\n children: _jsx(_components.a, {\n href: \"#先抓住重点堆是一种树结构\",\n children: \"先抓住重点:堆是一种树结构\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\"\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"再进一步,在堆这种树结构中,最重要的约束就是:\", _jsx(_components.strong, {\n children: \"对于树中的每个节点,总有父节点大于两个子节点\"\n }), \"(以大顶堆为例,下同)。\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\"\n }), \"\\n\", _jsxs(_components.ol, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [\"堆并不关注左右子树之间的大小情况,那么\", _jsx(_components.strong, {\n children: \"要维护一个堆,基本只需要做交换父节点与子节点的操作\"\n }), \",而不需要像二叉查找树那样做各种旋转操作。\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,\", _jsx(_components.strong, {\n children: \"可以使用数组进行实现\"\n }), \"。\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:\", _jsx(_components.strong, {\n children: \"堆的增删操作最坏时间复杂度为O(logn)\"\n }), \"。\"]\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.h2, {\n id: \"再抓基本操作上浮与下沉\",\n children: _jsx(_components.a, {\n href: \"#再抓基本操作上浮与下沉\",\n children: \"再抓基本操作:上浮与下沉\"\n })\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点\", _jsx(_components.strong, {\n children: \"过大\"\n }), \",就跟他的父节点\", _jsx(_components.strong, {\n children: \"向上交换\"\n }), \";若一个节点\", _jsx(_components.strong, {\n children: \"过小\"\n }), \",就跟他的子节点\", _jsx(_components.strong, {\n children: \"向下交换\"\n }), \"。\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\"\n }), \"\\n\", _jsx(_components.img, {\n src: \"/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png\",\n alt: \"p与g交换\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\"\n }), \"\\n\", _jsxs(_components.ol, {\n children: [\"\\n\", _jsx(_components.li, {\n children: \"p > g > p2\"\n }), \"\\n\", _jsx(_components.li, {\n children: \"g > 原p > c1与c2\"\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\"\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"为了简化,我们把前面那种递归地向上交换称为\", _jsx(_components.strong, {\n children: \"上浮操作\"\n }), \",把后面这种递归地向下交换称为\", _jsx(_components.strong, {\n children: \"下沉操作\"\n }), \"。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\"]\n }), \"\\n\", _jsx(_components.h1, {\n id: \"各种接口的逻辑\",\n children: _jsx(_components.a, {\n href: \"#各种接口的逻辑\",\n children: \"各种接口的逻辑\"\n })\n }), \"\\n\", _jsx(_components.h2, {\n id: \"插入元素入堆\",\n children: _jsx(_components.a, {\n href: \"#插入元素入堆\",\n children: \"插入元素——入堆\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\"\n }), \"\\n\", _jsx(_components.h2, {\n id: \"删除堆顶元素出堆\",\n children: _jsx(_components.a, {\n href: \"#删除堆顶元素出堆\",\n children: \"删除堆顶元素——出堆\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"我们从堆中删除元素时,一般只会删除堆顶元素。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\"\n }), \"\\n\", _jsx(_components.p, {\n children: \"这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\"\n }), \"\\n\", _jsx(_components.h2, {\n id: \"堆的初始化建堆\",\n children: _jsx(_components.a, {\n href: \"#堆的初始化建堆\",\n children: \"堆的初始化——建堆\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"建立一个堆,我们有两种思路:\"\n }), \"\\n\", _jsxs(_components.ol, {\n children: [\"\\n\", _jsx(_components.li, {\n children: \"将元素一个一个插入,即对每个元素都做一次入堆操作。\"\n }), \"\\n\", _jsx(_components.li, {\n children: \"当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\"\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"下面我们来分析这两种建堆策略。\"\n }), \"\\n\", _jsx(_components.h3, {\n id: \"元素逐个入堆\",\n children: _jsx(_components.a, {\n href: \"#元素逐个入堆\",\n children: \"元素逐个入堆\"\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\"\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"插入第i个元素时,堆的大小为\", _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsx(_components.mrow, {\n children: _jsx(_components.mi, {\n children: \"i\"\n })\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"i\"\n })]\n })\n })\n }), _jsx(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"0.6595em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n })]\n })\n })]\n })\n }), \"(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\"]\n }), \"\\n\", _jsx(_components.p, {\n children: _jsx(_components.span, {\n className: \"math math-inline\",\n children: _jsxs(_components.span, {\n className: \"katex\",\n children: [_jsx(_components.span, {\n className: \"katex-mathml\",\n children: _jsx(_components.math, {\n xmlns: \"http://www.w3.org/1998/Math/MathML\",\n children: _jsxs(_components.semantics, {\n children: [_jsxs(_components.mrow, {\n children: [_jsx(_components.mi, {\n children: \"T\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \"(\"\n }), _jsx(_components.mi, {\n children: \"i\"\n }), _jsx(_components.mo, {\n stretchy: \"false\",\n children: \")\"\n }), _jsx(_components.mo, {\n children: \"=\"\n }), _jsx(_components.mi, {\n children: \"l\"\n }), _jsx(_components.mi, {\n children: \"o\"\n }), _jsx(_components.mi, {\n children: \"g\"\n }), _jsx(_components.mi, {\n children: \"i\"\n })]\n }), _jsx(_components.annotation, {\n encoding: \"application/x-tex\",\n children: \"T(i) = logi\"\n })]\n })\n })\n }), _jsxs(_components.span, {\n className: \"katex-html\",\n \"aria-hidden\": \"true\",\n children: [_jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"1em\",\n verticalAlign: \"-0.25em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.13889em\"\n },\n children: \"T\"\n }), _jsx(_components.span, {\n className: \"mopen\",\n children: \"(\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n }), _jsx(_components.span, {\n className: \"mclose\",\n children: \")\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n }), _jsx(_components.span, {\n className: \"mrel\",\n children: \"=\"\n }), _jsx(_components.span, {\n className: \"mspace\",\n style: {\n marginRight: \"0.2778em\"\n }\n })]\n }), _jsxs(_components.span, {\n className: \"base\",\n children: [_jsx(_components.span, {\n className: \"strut\",\n style: {\n height: \"0.8889em\",\n verticalAlign: \"-0.1944em\"\n }\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.01968em\"\n },\n children: \"l\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"o\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n style: {\n marginRight: \"0.03588em\"\n },\n children: \"g\"\n }), _jsx(_components.span, {\n className: \"mord mathnormal\",\n children: \"i\"\n })]\n })]\n })]\n })\n })\n }), \"\\n\", _jsx(_components.p, {\n children: \"那么把所有元素上浮,则总时间复杂度为:\"\n }), \"\\n\", _jsx(_components.div, {\n className: \"math math-display\",\n children: _jsx(_components.span, {\n className: \"katex-error\",\n title: \"ParseError: KaTeX parse error: Expected 'EOF', got '&' at position 6: T(n) &̲= \\\\sum_{i=1}^{n…\",\n style: {\n color: \"#cc0000\"\n },\n children: \"T(n) &= \\\\sum_{i=1}^{n}logi\\\\\\\\\\n&= 1\\\\times0 + 2\\\\times1 + ... + 2^{logn}\\\\times{logn} \\\\\\\\\\n&=O(nlogn)\\n\\\\end{aligned}$$\\n\\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\\n\\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\\n\\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\\n\\n### 堆合并\\n\\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\\n\\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\\n\\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\\n\\n$$T(i) = logn-logi$$\\n\\n那么把所有元素下沉,则总时间复杂度为:\\n\\n$$\\\\begin{aligned}\\nT(n) &= \\\\sum_{i=1}^{n}logn-logi \\\\\\\\\\n&= \\\\frac{n}{2^{logn}}\\\\times{logn}+ ... + \\\\frac{n}{4}\\\\times2+\\\\frac{n}{2}\\\\times1 \\\\\\\\\\n&= O(n)\\n\\\\end{aligned}$$\\n\\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\\n\\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\\n\\n### 两种策略的比较与理解\\n\\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\\n\\n1. 从元素移动路径的角度\\n\\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\\n\\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\\n\\n2. 从元素移动数量与移动距离的角度\\n\\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\\n\\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\\n\\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\\n\\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\\n\\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\\n\\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\\n\\n# 代码实现\\n\\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\\n\\n```python\\nT = TypeVar(\\\"T\\\")\\nclass Heap(Generic[T]):\\n '''堆结构\\n\\n 有两个成员:\\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\\n self.fCompare: Callable[[T,T],bool] # 比较函数\\n \\n 下面假设堆为大顶堆\\n 即有self.fCompare = lambda a,b: a>b\\n '''\\n```\\n\\n## 实现树结构\\n\\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\\n\\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\\n\\n```python\\ndef lfChildOf(i:int):\\n return (i + 1) << 1 - 1\\n\\ndef rtChildOf(i:int):\\n return (i + 1) << 1\\n\\ndef parentOf(i:int):\\n return (i - 1) >> 1\\n```\\n\\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\\n\\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\\n\\n## 实现基本操作——上浮与下沉\\n\\n### 上浮\\n\\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\\n\\n```python\\ndef floatUp(self, i:int):\\n '''上浮操作\\n\\n 对下标为i的元素递归地进行上浮操作\\n 直到该元素小于其父节点或该元素上浮到根节点\\n '''\\n # 元素i上浮到根节点时结束递归\\n if i <= 0:\\n return\\n \\n # 当元素i小于其父节点时符合堆结构,结束递归\\n pr = parentOf(i)\\n if self.fCompare(self.A[pr], self.A[i]):\\n return\\n \\n # 元素i大于其父节点,交换i与其父节点并继续上浮\\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\\n self.floatUp(pr)\\n```\\n\\n### 下沉\\n\\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\\n\\n```python\\ndef size(self):\\n '''返回堆大小\\n '''\\n return len(self.A)\\n\\ndef sinkDown(self, i:int):\\n '''下沉操作\\n\\n 对下标为i的元素递归地进行下沉操作\\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\\n '''\\n lc = lfChildOf(i)\\n rc = rtChildOf(i)\\n\\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\\n larger = i\\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\\n larger = lc\\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\\n larger = rc\\n \\n # 当元素i大于其两个子节点时符合堆结构,结束递归\\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\\n if larger == i:\\n return\\n \\n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\\n self.sinkDown(larger)\\n```\\n\\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\\n\\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\\n\\n## 实现各种借口——读、增、删、初始化\\n\\n### 读取堆顶\\n\\n堆一般只允许读取堆顶,即全堆最大元素。\\n\\n```python\\ndef top(self):\\n '''返回堆顶\\n '''\\n return self.A[0]\\n```\\n\\n### 入堆\\n\\n入堆时,把元素加到堆尾,再做上浮操作。\\n\\n```python\\ndef insert(self, v:T):\\n '''入堆\\n '''\\n # 将元素加到堆尾并做上浮操作\\n self.A.append(v)\\n self.floatUp(len(self.A) - 1)\\n```\\n\\n### 出堆\\n\\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\\n\\n```python\\ndef pop(self)->T:\\n '''出堆\\n '''\\n # 取出堆顶元素\\n res = self.A[0]\\n\\n # 将堆尾元素填到堆顶并做下沉操作\\n self.A[0] = self.A[len(self.A) - 1]\\n self.A.pop()\\n self.sinkDown(0)\\n\\n return res\\n```\\n\\n注意入堆与出堆操作都要保证堆的大小会相应变化。\\n\\n### 堆初始化\\n\\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\\n\\n```python\\ndef __init__(self, A:List[T]=[], \\n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\\n ) -> None:\\n '''堆初始化\\n\\n :param A: 在数组A上进行初始化\\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\\n '''\\n self.A = A\\n self.fCompare = fCompare\\n for i in reversed(range(len(A))):\\n self.sinkDown(i)\\n```\\n\\n## 整体代码\\n\\n### 堆的整体实现\\n\\n综上,堆的整体代码实现如下:\\n\\n```python\\nfrom typing import Any, Callable, Generic, List, TypeVar\\n\\nT = TypeVar(\\\"T\\\")\\n\\ndef lfChildOf(i:int):\\n return (i + 1) << 1 - 1\\n\\ndef rtChildOf(i:int):\\n return (i + 1) << 1\\n\\ndef parentOf(i:int):\\n return (i - 1) >> 1\\n\\nclass Heap(Generic[T]):\\n '''堆结构\\n\\n 有两个成员:\\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\\n self.fCompare: Callable[[T,T],bool] # 比较函数\\n \\n 下面假设堆为大顶堆\\n 即有self.fCompare = lambda a,b: a>b\\n '''\\n def __init__(self, A:List[T]=[], \\n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\\n ) -> None:\\n '''堆初始化\\n\\n :param A: 在数组A上进行初始化\\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\\n '''\\n self.A = A\\n self.fCompare = fCompare\\n for i in reversed(range(len(A))):\\n self.sinkDown(i)\\n \\n def size(self):\\n '''返回堆大小\\n '''\\n return len(self.A)\\n \\n def top(self):\\n '''返回堆顶\\n '''\\n return self.A[0]\\n \\n def sinkDown(self, i:int):\\n '''下沉操作\\n\\n 对下标为i的元素递归地进行下沉操作\\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\\n '''\\n lc = lfChildOf(i)\\n rc = rtChildOf(i)\\n\\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\\n larger = i\\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\\n larger = lc\\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\\n larger = rc\\n \\n # 当元素i大于其两个子节点时符合堆结构,结束递归\\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\\n if larger == i:\\n return\\n \\n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\\n self.sinkDown(larger)\\n\\n def floatUp(self, i:int):\\n '''上浮操作\\n\\n 对下标为i的元素递归地进行上浮操作\\n 直到该元素小于其父节点或该元素上浮到根节点\\n '''\\n # 元素i上浮到根节点时结束递归\\n if i <= 0:\\n return\\n \\n # 当元素i小于其父节点时符合堆结构,结束递归\\n pr = parentOf(i)\\n if self.fCompare(self.A[pr], self.A[i]):\\n return\\n \\n # 元素i大于其父节点,交换i与其父节点并继续上浮\\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\\n self.floatUp(pr)\\n \\n def insert(self, v:T):\\n '''入堆\\n '''\\n # 将元素加到堆尾并做上浮操作\\n self.A.append(v)\\n self.floatUp(len(self.A) - 1)\\n\\n def pop(self)->T:\\n '''出堆\\n '''\\n # 取出堆顶元素\\n res = self.A[0]\\n\\n # 将堆尾元素填到堆顶并做下沉操作\\n self.A[0] = self.A[len(self.A) - 1]\\n self.A.pop()\\n self.sinkDown(0)\\n\\n return res\\n```\\n\\n### 单元测试\\n\\n入堆、出堆等操作的简单单元测试如下:\\n\\n```python\\nimport pytest\\nimport heap\\n\\n@pytest.fixture\\ndef initHeap():\\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \\n lambda a,b:a>b)\\n\\nclass Test_TestHeap:\\n def test_init_notNull(self, initHeap:heap.Heap):\\n assert initHeap.size() == 10\\n assert initHeap.top() == 9\\n \\n def test_insert_notTop(self, initHeap:heap.Heap):\\n initHeap.insert(6)\\n assert initHeap.size() == 11\\n assert initHeap.top() == 9\\n \\n def test_insert_top(self, initHeap:heap.Heap):\\n initHeap.insert(10)\\n assert initHeap.size() == 11\\n assert initHeap.top() == 10\\n \\n def test_pop(self, initHeap:heap.Heap):\\n p = initHeap.pop()\\n assert p == 9\\n assert initHeap.size() == 9\\n assert initHeap.top() == 8\\n```\\n\\n# 关于堆排序\\n\\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\\n\\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\\n\\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\\n\\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。\"\n })\n })]\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = Object.assign({}, _provideComponents(), props.components);\n return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, {\n children: _jsx(_createMdxContent, props)\n })) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"meta":{"content":"\n# 如何手撕一个堆\n\n# 写在前面\n\n在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\n\n# 首先要理解,然后才能实现\n\n就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\n\n## 先抓住重点:堆是一种树结构\n\n首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\n\n再进一步,在堆这种树结构中,最重要的约束就是:**对于树中的每个节点,总有父节点大于两个子节点**(以大顶堆为例,下同)。\n\n如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\n\n而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\n\n1. 堆并不关注左右子树之间的大小情况,那么**要维护一个堆,基本只需要做交换父节点与子节点的操作**,而不需要像二叉查找树那样做各种旋转操作。\n2. 因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,**可以使用数组进行实现**。\n3. 因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:**堆的增删操作最坏时间复杂度为O(logn)**。\n\n## 再抓基本操作:上浮与下沉\n\n上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点**过大**,就跟他的父节点**向上交换**;若一个节点**过小**,就跟他的子节点**向下交换**。\n\n假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\n\n![p与g交换](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png)\n\n由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\n\n1. p > g > p2\n2. g > 原p > c1与c2\n\n则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\n\n同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\n\n为了简化,我们把前面那种递归地向上交换称为**上浮操作**,把后面这种递归地向下交换称为**下沉操作**。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\n\n# 各种接口的逻辑\n\n## 插入元素——入堆\n\n把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\n\n虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\n\n## 删除堆顶元素——出堆\n\n我们从堆中删除元素时,一般只会删除堆顶元素。\n\n删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\n\n这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\n\n## 堆的初始化——建堆\n\n建立一个堆,我们有两种思路:\n\n1. 将元素一个一个插入,即对每个元素都做一次入堆操作。\n2. 当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\n\n下面我们来分析这两种建堆策略。\n\n### 元素逐个入堆\n\n上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\n\n插入第i个元素时,堆的大小为$i$(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\n\n$$T(i) = logi$$\n\n那么把所有元素上浮,则总时间复杂度为:\n\n$$\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logi\\\\\n&= 1\\times0 + 2\\times1 + ... + 2^{logn}\\times{logn} \\\\\n&=O(nlogn)\n\\end{aligned}$$\n\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\n\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\n\n### 堆合并\n\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\n\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\n\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\n\n$$T(i) = logn-logi$$\n\n那么把所有元素下沉,则总时间复杂度为:\n\n$$\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logn-logi \\\\\n&= \\frac{n}{2^{logn}}\\times{logn}+ ... + \\frac{n}{4}\\times2+\\frac{n}{2}\\times1 \\\\\n&= O(n)\n\\end{aligned}$$\n\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\n\n### 两种策略的比较与理解\n\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\n\n1. 从元素移动路径的角度\n\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\n\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\n\n2. 从元素移动数量与移动距离的角度\n\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\n\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\n\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\n\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\n\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\n\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\n\n# 代码实现\n\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\n\n```python\nT = TypeVar(\"T\")\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n```\n\n## 实现树结构\n\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\n\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\n\n```python\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n```\n\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\n\n## 实现基本操作——上浮与下沉\n\n### 上浮\n\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\n\n```python\ndef floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n```\n\n### 下沉\n\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\n\n```python\ndef size(self):\n '''返回堆大小\n '''\n return len(self.A)\n\ndef sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n```\n\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\n\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\n\n## 实现各种借口——读、增、删、初始化\n\n### 读取堆顶\n\n堆一般只允许读取堆顶,即全堆最大元素。\n\n```python\ndef top(self):\n '''返回堆顶\n '''\n return self.A[0]\n```\n\n### 入堆\n\n入堆时,把元素加到堆尾,再做上浮操作。\n\n```python\ndef insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n```\n\n### 出堆\n\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\n\n```python\ndef pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n注意入堆与出堆操作都要保证堆的大小会相应变化。\n\n### 堆初始化\n\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\n\n```python\ndef __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n```\n\n## 整体代码\n\n### 堆的整体实现\n\n综上,堆的整体代码实现如下:\n\n```python\nfrom typing import Any, Callable, Generic, List, TypeVar\n\nT = TypeVar(\"T\")\n\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n def __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n \n def size(self):\n '''返回堆大小\n '''\n return len(self.A)\n \n def top(self):\n '''返回堆顶\n '''\n return self.A[0]\n \n def sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n\n def floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n \n def insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n\n def pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n### 单元测试\n\n入堆、出堆等操作的简单单元测试如下:\n\n```python\nimport pytest\nimport heap\n\n@pytest.fixture\ndef initHeap():\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \n lambda a,b:a>b)\n\nclass Test_TestHeap:\n def test_init_notNull(self, initHeap:heap.Heap):\n assert initHeap.size() == 10\n assert initHeap.top() == 9\n \n def test_insert_notTop(self, initHeap:heap.Heap):\n initHeap.insert(6)\n assert initHeap.size() == 11\n assert initHeap.top() == 9\n \n def test_insert_top(self, initHeap:heap.Heap):\n initHeap.insert(10)\n assert initHeap.size() == 11\n assert initHeap.top() == 10\n \n def test_pop(self, initHeap:heap.Heap):\n p = initHeap.pop()\n assert p == 9\n assert initHeap.size() == 9\n assert initHeap.top() == 8\n```\n\n# 关于堆排序\n\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\n\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\n\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\n\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。","title":"如何手撕一个堆","abstract":"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。","length":477,"created_at":"2021-08-28T23:09:14.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["数据结构","算法","算法竞赛"],"license":false},"prevNextInfo":{"prevInfo":{"slug":"init-a-new-hexo-project","title":"init-a-new-hexo-project","path":"/articles/init-a-new-hexo-project"},"nextInfo":{"slug":"The-beauty-of-design-parten","title":"设计模式之美读书笔记","path":"/articles/The-beauty-of-design-parten"}}},"__N_SSG":true} \ No newline at end of file diff --git "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\225\260\346\215\256\347\273\223\346\236\204.json" "b/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\225\260\346\215\256\347\273\223\346\236\204.json" deleted file mode 100644 index 032dec59..00000000 --- "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\346\225\260\346\215\256\347\273\223\346\236\204.json" +++ /dev/null @@ -1 +0,0 @@ -{"pageProps":{"allTagInfos":[{"tag":"Kubernetes","slug":"kubernetes","path":"/tags/kubernetes","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"DevOps","slug":"devops","path":"/tags/devops","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Docker","slug":"docker","path":"/tags/docker","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Cloud Native","slug":"cloud-native","path":"/tags/cloud-native","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Blog","slug":"blog","path":"/tags/blog","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"init-a-new-hexo-project","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"VSCode","slug":"vscode","path":"/tags/vscode","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"Hexo","slug":"hexo","path":"/tags/hexo","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"JavaScript","slug":"javascript","path":"/tags/javascript","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"GitHub","slug":"github","path":"/tags/github","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"AWS","slug":"aws","path":"/tags/aws","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"CI/CD","slug":"ci-cd","path":"/tags/ci-cd","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"IaC","slug":"iac","path":"/tags/iac","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]},{"tag":"设计模式","slug":"设计模式","path":"/tags/设计模式","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"笔记","slug":"笔记","path":"/tags/笔记","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"排序","slug":"排序","path":"/tags/排序","postSlugs":[{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"Python","slug":"python","path":"/tags/python","postSlugs":[{"postSlug":"python-dict","postType":"article"}]},{"tag":"C++","slug":"c++","path":"/tags/c++","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"}]},{"tag":"杂技","slug":"杂技","path":"/tags/杂技","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"hello-world","postType":"article"}]},{"tag":"杂谈","slug":"杂谈","path":"/tags/杂谈","postSlugs":[{"postSlug":"hello-world","postType":"article"}]},{"tag":"Nextjs","slug":"nextjs","path":"/tags/nextjs","postSlugs":[{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"Cloud Computing","slug":"cloud-computing","path":"/tags/cloud-computing","postSlugs":[{"postSlug":"newest","postType":"idea"}]}],"selectedTagInfo":{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},"posts":[{"slug":"Handy-heap-cheat-sheet","file":"public/content/articles/2021-03-21-Handy-heap-cheat-sheet.md","mediaDir":"content/articles/2021-03-21-Handy-heap-cheat-sheet","path":"/articles/Handy-heap-cheat-sheet","meta":{"content":"\n# 如何手撕一个堆\n\n# 写在前面\n\n在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\n\n# 首先要理解,然后才能实现\n\n就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\n\n## 先抓住重点:堆是一种树结构\n\n首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\n\n再进一步,在堆这种树结构中,最重要的约束就是:**对于树中的每个节点,总有父节点大于两个子节点**(以大顶堆为例,下同)。\n\n如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\n\n而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\n\n1. 堆并不关注左右子树之间的大小情况,那么**要维护一个堆,基本只需要做交换父节点与子节点的操作**,而不需要像二叉查找树那样做各种旋转操作。\n2. 因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,**可以使用数组进行实现**。\n3. 因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:**堆的增删操作最坏时间复杂度为O(logn)**。\n\n## 再抓基本操作:上浮与下沉\n\n上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点**过大**,就跟他的父节点**向上交换**;若一个节点**过小**,就跟他的子节点**向下交换**。\n\n假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\n\n![p与g交换](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png)\n\n由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\n\n1. p > g > p2\n2. g > 原p > c1与c2\n\n则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\n\n同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\n\n为了简化,我们把前面那种递归地向上交换称为**上浮操作**,把后面这种递归地向下交换称为**下沉操作**。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\n\n# 各种接口的逻辑\n\n## 插入元素——入堆\n\n把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\n\n虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\n\n## 删除堆顶元素——出堆\n\n我们从堆中删除元素时,一般只会删除堆顶元素。\n\n删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\n\n这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\n\n## 堆的初始化——建堆\n\n建立一个堆,我们有两种思路:\n\n1. 将元素一个一个插入,即对每个元素都做一次入堆操作。\n2. 当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\n\n下面我们来分析这两种建堆策略。\n\n### 元素逐个入堆\n\n上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\n\n插入第i个元素时,堆的大小为$i$(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\n\n$$T(i) = logi$$\n\n那么把所有元素上浮,则总时间复杂度为:\n\n$$\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logi\\\\\n&= 1\\times0 + 2\\times1 + ... + 2^{logn}\\times{logn} \\\\\n&=O(nlogn)\n\\end{aligned}$$\n\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\n\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\n\n### 堆合并\n\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\n\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\n\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\n\n$$T(i) = logn-logi$$\n\n那么把所有元素下沉,则总时间复杂度为:\n\n$$\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logn-logi \\\\\n&= \\frac{n}{2^{logn}}\\times{logn}+ ... + \\frac{n}{4}\\times2+\\frac{n}{2}\\times1 \\\\\n&= O(n)\n\\end{aligned}$$\n\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\n\n### 两种策略的比较与理解\n\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\n\n1. 从元素移动路径的角度\n\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\n\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\n\n2. 从元素移动数量与移动距离的角度\n\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\n\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\n\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\n\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\n\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\n\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\n\n# 代码实现\n\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\n\n```python\nT = TypeVar(\"T\")\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n```\n\n## 实现树结构\n\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\n\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\n\n```python\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n```\n\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\n\n## 实现基本操作——上浮与下沉\n\n### 上浮\n\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\n\n```python\ndef floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n```\n\n### 下沉\n\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\n\n```python\ndef size(self):\n '''返回堆大小\n '''\n return len(self.A)\n\ndef sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n```\n\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\n\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\n\n## 实现各种借口——读、增、删、初始化\n\n### 读取堆顶\n\n堆一般只允许读取堆顶,即全堆最大元素。\n\n```python\ndef top(self):\n '''返回堆顶\n '''\n return self.A[0]\n```\n\n### 入堆\n\n入堆时,把元素加到堆尾,再做上浮操作。\n\n```python\ndef insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n```\n\n### 出堆\n\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\n\n```python\ndef pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n注意入堆与出堆操作都要保证堆的大小会相应变化。\n\n### 堆初始化\n\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\n\n```python\ndef __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n```\n\n## 整体代码\n\n### 堆的整体实现\n\n综上,堆的整体代码实现如下:\n\n```python\nfrom typing import Any, Callable, Generic, List, TypeVar\n\nT = TypeVar(\"T\")\n\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n def __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n \n def size(self):\n '''返回堆大小\n '''\n return len(self.A)\n \n def top(self):\n '''返回堆顶\n '''\n return self.A[0]\n \n def sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n\n def floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n \n def insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n\n def pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n### 单元测试\n\n入堆、出堆等操作的简单单元测试如下:\n\n```python\nimport pytest\nimport heap\n\n@pytest.fixture\ndef initHeap():\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \n lambda a,b:a>b)\n\nclass Test_TestHeap:\n def test_init_notNull(self, initHeap:heap.Heap):\n assert initHeap.size() == 10\n assert initHeap.top() == 9\n \n def test_insert_notTop(self, initHeap:heap.Heap):\n initHeap.insert(6)\n assert initHeap.size() == 11\n assert initHeap.top() == 9\n \n def test_insert_top(self, initHeap:heap.Heap):\n initHeap.insert(10)\n assert initHeap.size() == 11\n assert initHeap.top() == 10\n \n def test_pop(self, initHeap:heap.Heap):\n p = initHeap.pop()\n assert p == 9\n assert initHeap.size() == 9\n assert initHeap.top() == 8\n```\n\n# 关于堆排序\n\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\n\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\n\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\n\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。","title":"如何手撕一个堆","abstract":"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。","length":477,"created_at":"2021-08-28T23:09:14.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["数据结构","算法","算法竞赛"],"license":false}},{"slug":"Sort-algorithm","file":"public/content/articles/2021-01-11-Sort-algorithm.md","mediaDir":"content/articles/2021-01-11-Sort-algorithm","path":"/articles/Sort-algorithm","meta":{"content":"\n# 序言\n\n我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。\n\n你会发现堆排序被当成是选择排序的一种优化——因为我认为堆排序主要在于使用了堆这种数据结构,而总体思想与选择排序相比没有太大变化。\n\n你还会找到其他与别的文章不一样的地方。因为这篇文章是我按照自己的理解来写的,我脑子里是这样想的,那文章里就是这样写的。我会按照我的理解,从纵向与横向两个维度,来理清楚各个排序算法的特性与异同。\n\n# 整篇文章的要点\n\n整篇文章以纵向——算法分类、以及横向——算法评价两个维度来进行组织。\n\n排序算法可以按照以下方式来进行分类:\n\n- 基于比较的排序算法\n - 基于分治思想\n - 快速排序\n - 归并排序\n - 基于有序区域扩展\n - 插入排序\n - 选择排序\n- 不基于比较的排序算法\n - 计数排序\n - 桶排序\n - 基数排序\n\n文章中还会讲一讲为什么会这么分,每种分类有什么共性,分类之间有什么差异。此外,在最后还会稍微提一提外部排序与适用于并行运算的排序等。\n\n而对于纵向分类中的每一个端点,我们又会从以下五个方面,来对各个算法进行一个总体评价:\n\n- 时间复杂度(最坏,最好,平均※)\n- 空间复杂度\n- 是否原地排序\n- 是否稳定排序\n- 能否用于链表排序\n\n而由于复杂度主要只关注数量级,因此在这篇文章里会在不影响计算结果的前提下对复杂度计算进行适当的近似与简化。\n\n# 快排\n\n## 思想\n\n分治法:先把序列分为小的部分和大的部分,再将两部分分别排序。即:复杂分割,简单合并,主要操作在于分割。\n\n## 要点\n\n### 时间复杂度\n\n推导式:T(n) = T(找) + T(左) + T(右) = O(n) + 两个子问题时间复杂度。\n\n- 最好时间复杂度为每次都正好找到最中间的一个数时时间复杂度为O(nlogn)。证明略。\n- 最坏时间复杂度为每次都正好找到最旁边的数时时间复杂度为O(n^2)。证明略。\n- 平均时间复杂度为O(nlogn),推导式如下:\n\n 快速排序每一步中,将元素分为左右两边需要遍历整个列表,耗时T(n)。假设最后定位的元素为最终第i个元素,则两个子问题复杂度分别为T(i)和T(n-i-1)。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{\\sum_{i=0}^{n-1}{T(i)+T(n-i-1)}}{n} \\\\\n &=n + \\frac{2}{n}\\times\\sum_{i=0}^{n-1}{T(i)} \\\\\n \\end{aligned}\n $$\n\n 令 $$\\sum_{i=0}^{n}T(i) = Sum(n)$$ ,即有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{2}{n} \\times Sum(n-1) \\\\\n Sum(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1)\n \\end{aligned}\n $$\n\n 错位相减:\n\n $$\n \\begin{aligned}\n Sum(n) - Sum(n-1) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1) - \\frac{n}{2}T(n) + \\frac{n}{2}(n) \n \\\\\n T(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n}{2}T(n) - \\frac{2n+1}{2} \n \\\\\n \\frac{T(n+1)}{n+2} &= \\frac{T(n)}{n+1}+\\frac{2n+1}{(n+1)(n+2)} \n \\\\\n &= \\frac{T(n)}{n+1}+\\frac{1}{n}+\\frac{1}{n+1} \n \\\\\n &=...\n \\\\\n &= \\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+\\frac{1}{3}+...+\\frac{1}{n})+\\frac{1}{n+1}\n \\\\\n \\\\\n \\frac{T(n)}{n+1}&=\\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+...+\\frac{1}{n-1})+\\frac{1}{n}\n \\\\\n &= O(1)+O(1)+O(logn)+O(\\frac{1}{n})\n \\\\\n &= O(logn)\n \\end{aligned}\n $$\n\n 其中由于 $$\\frac{1}{x}=\\frac{d(logx)}{dx}$$ ,因此 $$\\frac{1}{2}+...+\\frac{1}{n-1}=O(logn)$$ 。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n)&=(n+1)\\times O(logn)\\\\\n &=O(n)\\times O(logn)\\\\\n &=O(nlogn)\n \\end{aligned}\n $$\n\n### 额外空间复杂度\n\n考虑栈深度,额外空间复杂度为O(logn)。由于快速排序主要步骤在于分,因此必须自上而下的进行递归,无法避免栈深度。\n\n### 原地排序\n\n虽然快速排序有额外空间复杂度,但并不妨碍它是一个原地排序。\n\n### 不稳定\n\n堆排序在分操作时将元素左右交换,会破坏稳定性。\n\n### 链表形式特点\n\n- 时间复杂度不变\n- 空间复杂度不变\n- 变为稳定排序※\n\n## 手写时的易错点\n\n- 分成左右子序列时最好完全分开(一边用`<=`一边用`>`),不然容易造成死循环。\n- 分左右子序列时仔细考虑最初下标位置与最终下标位置,以及对应位置的值的大小。\n- 不要忘记递归的结束条件。\n\n# 归并排序\n\n## 思想\n\n分治法:先把两个子序列各自排好序,然后再合并两个子序列。即:简单分割,复杂合并。主要步骤在于合并。\n\n## 要点\n\n- 时间复杂度推导式:T(n) = 2T(n/2) + T(合)\n- 平均、最好、最坏时间复杂度都是O(nlogn),推导过程略。\n- 额外空间复杂度为O(n),合并时必须准备额外空间。但由于主要步骤在于合并,可以自下而上地进行迭代合并,可以不使用栈。\n- 非原地排序\n- 稳定排序\n\n### 链表形式\n\n- 时间复杂度不变\n- 额外空间复杂度变为O(1)※\n- 稳定排序\n- 只能使用迭代形式,不能使用递归形式\n\n## 易错点\n\n- 合并时一边结束时另一边还未结束,需要把那一边也放入合并后序列中\n- 保持稳定排序:合并时左序列等于右序列时也采用左序列\n- 不要忘记递归结束条件\n- 不要忘记循环递进条件\n\n## 进阶\n\n- 原地归并排序:时间复杂度为O(log^2n),牺牲合并的时间复杂度进行原地排序。\n- 多路归并排序:使用竞标树,多路归并,用于磁盘IO。\n\n# 插入排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将无序序列当前元素插入有序序列,复杂度取决于有序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:基本有序情况下,O(n)\n- 额外空间复杂度:O(1)\n- 原地排序\n- 稳定排序\n- 每次插入时都要移动序列,写次数较多\n- 若查找插入位置时使用二分法查找,则可加快时间。(但不足以对时间复杂度造成影响,且最好时间复杂度也会上升为O(nlogn))\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 稳定排序\n- 插入时不再需要移动序列,但也不能使用二分法查找\n\n## 进阶——希尔排序\n\n- 要点:\n\n 插入排序的优点在于当序列基本有序时,时间复杂度可逼近为O(n)。\n\n 但插入时移动有序序列中元素所耗时间较多,而每次只移动一步。但实际上当序列分布均匀时,有序序列中排靠后的元素在整个序列中也会排靠后。\n\n 可以把序列分为几个大步长序列,在最初的几次插入放开移动步长,让大的元素直接移动到较后位置。再往后慢慢缩小步长,此时序列基本有序,可以利用基本有序时插入排序的优势。\n\n- 时间复杂度\n 1. 当步长为2^i时,不能使时间复杂度缩短为O(nlogn)。因为一个子序列所有元素有可能比另一个子序列最大元素都要大,这时插入排序仍需进行约n^2次操作\n 2. 当步长为2^i时效率较低,因为当步长为4已经有序时,步长为2再比较是无用比较。但由于1.的问题,不能节省比较时间。\n 3. 当步长之间最小公约数较少,甚至互质时,无用比较次数会降低。\n 4. 最坏时间复杂度下限为 $$O(nlog^2n)$$ (当步长采用 $$2^i3^j$$ 时),但一般希尔排序平均时间复杂度都为 $$O(n^{\\frac{3}{2}})$$\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序:希尔排序步长较大时会发生前后跳转。\n- 不能写为链表形式\n\n# 选择排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将选无序序列中最小元素加到有序序列末尾,复杂度取决于无序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:O(n2),如能保证无序部分的最小元素所在位置一定(堆排序),能降低时间复杂度\n- 额外空间复杂度:O(1)\n- 原地排序\n- 不稳定排序(采用元素交换策略时)\n- 每次找到最小元素后,只需交换一次位置即可,写次数较少。\n- 若找到最小元素后,不直接交换而是进行数组移动,则可进行稳定排序,但写次数变多,与插入排序相比没有优势,也不能使用二分查找进行简化。\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 变为稳定排序※(因为链表不需要数组移动,稳定排序方式的缺点得以消除)\n\n## 进阶——堆排序\n\n- 要点:\n\n 选择排序中耗时最多的是取出无序序列中最小值的时间,需要遍历整个无序序列。\n\n 但实际上我们只关心无序序列中的最小值,而不关心其他值的位置。通过将无序序列建为堆,减少选择时间,降低总的时间复杂度。\n\n- 时间复杂度O(nlogn)\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序\n- 操作时间复杂度:每次向下比较关注一个节点与其左右子堆顶元素,每次向上比较只关注节点与其父元素(大顶堆,堆大小为n)\n - 下沉:向下比较,若顶元素不是最大,将顶元素与较大的子堆堆顶元素交换。递归处理该子堆顶元素,直到向下比较顶元素最大。\n\n 最好时间复杂度O(1),最坏时间复杂度为O(h)=O(logn),平均O(logn)\n\n - 上浮:向上比较,若元素比其上层要大,交换该元素与其上层元素。递归处理其上层元素,直到向上比较不比上层要大。\n\n 最好时间复杂度O(1),最坏时间复杂度O(h)=O(logn),平均O(logn)\n\n - 入堆:堆扩容一位,将新元素插到尾部,将该元素上浮,最坏、平均时间复杂度O(logn)\n - 出堆:取出堆顶元素,将尾部放到堆顶,将该元素下沉,最坏、平均时间复杂度O(logn)\n - 缺点:通常堆尾元素较小,出堆时将堆尾元素放到堆顶再下沉基本要沉到堆底,无用比较较多\n- 建堆时间复杂度O(n)\n - 策略1:从头开始建堆,逐个元素插入,时间复杂度取决于最后一层,时间复杂度为O(nlogn)\n - 每次将堆扩容一位,将末尾元素上浮。\n - 时间复杂度推导:\n\n 每次插入时间:\n\n $$\n \\begin{aligned} T(i) &= h\\\\\n &= logi\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n}logi\\\\\n &= 1\\times1 + 2\\times2+ 3\\times4 + ...+h\\times \\frac{n}{2} \\\\\n \\\\\n 2T(n) &= 1\\times2 + 2\\times4+ 3\\times8 + ...+h\\times n \\\\\n \\\\\n 2T(n)-T(n) &= h\\times n - (1+2+4+...+2^{h-1}) \\\\\n &= h\\times n - O(2^h) \\\\\n \\\\\n T(n) &= nlogn - O(2^h) \\\\\n &= O(nlogn)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(nlogn)$$\n\n - 策略2:从后开始建堆,小堆合并(逐个元素下沉),时间复杂度为O(n)\n - 每次堆合并时,有三部分:左子堆,右子堆,顶元素。下沉顶元素。\n - 时间复杂度推导:\n\n 第i个元素合并时,时间为:\n\n $$\n \\begin{aligned}\n T(i) &= h_{子堆}\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n} (h_{子堆}) \\\\\n &= h + (h-1)\\times2 + ... + 2 \\times \\frac{n}{4} + 1 \\times\\frac{n}{2}\\\\\n \\\\\n 2T(n) &= h \\times 2 + (h-1) \\times 4 + ... + 2 \\times \\frac{n}{2} + n \\\\\n \\\\\n 2T(n) - T(n) &= n + \\frac{n}{2} + ... + 2 - h \\\\\n \\\\\n T(n) &= O(n) - h \\\\ \n &= O(n)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(n)$$ \n\n - 虽说每步是做一个小堆合并,但实际上从堆尾到堆头遍历,相当于仅关注元素没有稳定,相当于可以直接使用下沉操作。\n\n# 基于比较的排序算法时间复杂度下限:逆序对思想\n\n基于比较的排序算法可以看作序列逆序对的消除。完全随机序列逆序对数量为O(n^2),若一次元操作只消除一个逆序对,则时间复杂度不会低于O(n^2)。降低时间复杂度关键在于一次消除多个逆序对。\n\n1. 希尔排序通过增大最初的步长来企图一次消除多个逆序对。\n2. 归并排序消除逆序对最主要在于归并步骤。最后几次合并每个子步骤用O(1)时间消除O(n)个逆序对。\n3. 快速排序消除逆序对最主要在于划分步骤。每个划分步骤用O(n)时间消除O(n)+O(左长度×右长度)个逆序对。\n4. 堆排序逆序对消除方式比较Tricky,但可以看出消除逆序对大致在于出堆步骤,通过O(logn)时间复杂度消除O(n)个逆序对。(左小右大排序时需要建立左大右小的大顶堆,建堆时基本没有消除逆序对)\n\n# 最后\n\n这篇文章我们主要关注了排序算法中的大头——基于比较的排序算法。在下篇文章,我们再来看一下不基于比较的排序算法,以及外排序与并行排序。","title":"排序算法","abstract":"我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。","length":342,"created_at":"2021-01-11T22:57:10.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","排序"],"license":false}},{"slug":"python-dict","file":"public/content/articles/2020-08-02-python-dict.md","mediaDir":"content/articles/2020-08-02-python-dict","path":"/articles/python-dict","meta":{"content":"\n> CPython从3.6开始,字典(dict)不再是无序的了——字典的修改了原先的底层实现,变得能按字典插入的顺序进行遍历。而Python从3.7开始将字典的有序性写入语言特性,不管是Jython、IronPython还是其他Python实现,从3.7开始大家的字典都是有序的了。\n\n# 前言\n\n以前参加Python相关的面试时,面试官经常都会问一个问题:Python里的字典(dict)是有序的吗?\n\n这自然难不倒我,我也照本宣科地讲:Python的字典底层是用哈希表实现的,在不发生冲突时读写的时间复杂度是O(1),比读写时间复杂度为O(logn)的红黑树要更快。但红黑树可以按下标的大小顺序进行遍历,而Dict遍历时是无序的。\n\n我讲的时候没感觉到任何的违和感,估计面试官们也没觉得任何的不对。直到有一天,我查Python各个版本的新特性时,发现Python 3.6的What's New里有[这么一条](https://docs.python.org/3/whatsnew/3.6.html#new-dict-implementation):\n\n> New dict implementation\n> \n> The dict type now uses a “compact” representation based on a proposal by Raymond Hettinger which was first implemented by PyPy. The memory usage of the new dict() is between 20% and 25% smaller compared to Python 3.5.\n> \n> The order-preserving aspect of this new implementation is considered an implementation detail and should not be relied upon (this may change in the future, but it is desired to have this new dict implementation in the language for a few releases before changing the language spec to mandate order-preserving semantics for all current and future Python implementations; this also helps preserve backwards-compatibility with older versions of the language where random iteration order is still in effect, e.g. Python 3.5).\n\n啥情况?CPython的dict竟然优化了内存,还变有序了!?\n\n# Python 3.5 以前dict的实现\n\n先不着急看Python 3.6 里的dict,我们先来看看Python 3.5之前的dict是怎么实现的,再拿3.6来做对比。\n\n在Python 3.5以前,dict是用Hash表来实现的,而且Key和Value直接储存在Hash表上。想通过Key获取Value,只需通过Python内部的Hash函数计算出Key对应的Hash值,再映射到Hash表上对应的地址,访问该地址即可获取Key对应的Value。如下图所示:\n\n我们知道,Hash表读写时间复杂度在不发生冲突的情况下都是O(1)。\n\n为什么呢?我们可以把Hash表读写的步骤分开来看:\n\n1. 首先用Hash函数计算key的Hash值,Hash函数一般来说时间复杂度都是O(1)的。\n2. 计算出Hash值后,映射到Hash表内的数组下标,一般用取余数或是取二进制后几位的方式实现,时间复杂度也是O(1)。\n3. 然后用数组下标读取数组中实际储存的键值,数组的下标读取时间复杂度也是O(1)。\n\n这三个步骤串起来后复杂度并没有提升,总的时间复杂度自然也是O(1)的。\n\n而内部储存空间,Python字典中称为entries。entries相当于一个数组,是一段连续的内存空间,每个位置储存一个(Hash值,指向Key的指针,指向Value的指针)三元组。\n\n当然,由于抽屉原理,我们知道Hash表不可避免的会出现Hash冲突,Python的dict也不例外。\n\n而解决Hash冲突的方法有很多,比如C++的unordered_map和Go的map就用链地址法来解决冲突,用链表储存发生冲突的值。而Java更进一步,当链表长度超过8时就转换成红黑树,将链表O(n)的查找复杂度降为O(logn)。C#的HashTable则是用再散列法,内部有多个Hash函数,一次冲突了就换一个函数再算,直到不冲突为止。\n\n而Python的dict则是利用开放寻址法。当插入数据发生冲突时,就会从那个位置往后找,直到找到有空位的地址为止。要查的时候,也是把下标值映射到到地址后,先对比一下下标值相不相等,若不相等则往后继续对比。\n\n这也造成个问题,dict中的元素不能直接从entries中清理掉,不然往后寻找的查找链就会断掉了。只能是先标记住删除,等到一定时机再一并清理。\n\n此外我们也知道,当冲突过发生得过多,dict读写所需的时间也会变多,时间复杂度不再是O(1),这也是Hash表的通病了。\n\nPython中dict初始化时,内部储存空间entries容量为8。当内部储存空间占用到一定程度(entries容量×装填因子,Python的dict中装填因子是2/3)后,就会进行倍增扩容。每次扩容都要遍历原先的元素,时间复杂度为O(n),但基本上插入O(n)次之后才会进行一次扩容,所以扩容的均摊时间复杂度为O(1)。而扩容时会重新进行Hash值到entries位置的映射,此时就是把标记删除但仍留在entries中的元素清理掉的最佳时机。\n\nPython3.5之前这种dict的实现就有两个毛病:\n\n1. 元素的顺序不被记录。两个Key值通过Hash函数的出来的Hash值不一定能保证原来的大小关系,由于Hash冲突、扩容等影响元素的顺序也会变化。当然这种无序性也是Hash表通用的特点了。\n2. 占用了太多了无用空间。上面说到entries中每个位置储存一个(Hash值,指向Key的指针,指向Value的指针)三元组,没用到或是标记删除的位置占用了大量的空间。\n\n于是,Raymond Hettinger就提出了一种新的dict实现方式。在CPython3.6中就使用了这种新的实现方式。\n\n# CPython3.6中dict的实现\n\n当要实现一个如下的dict时:\n\n```python\nd = {\n 'timmy': 'red', \n 'barry': 'green', \n 'guido': 'blue'\n}\n```\n\n如在上一节中所讲,在Python3.5以前,在内存储存的形式可以表示成这样子:\n\n```python\nentries = [['--', '--', '--'],\n [-8522787127447073495, 'barry', 'green'],\n ['--', '--', '--'],\n ['--', '--', '--'],\n ['--', '--', '--'],\n [-9092791511155847987, 'timmy', 'red'],\n ['--', '--', '--'],\n [-6480567542315338377, 'guido', 'blue']]\n```\n\n而CPython3.6以后,是以这种形式储存在内存中的:\n\n```python\nindices = [None, 1, None, None, None, 0, None, 2]\nentries = [[-9092791511155847987, 'timmy', 'red'],\n [-8522787127447073495, 'barry', 'green'],\n [-6480567542315338377, 'guido', 'blue']]\n```\n\n改变了什么?\n\n1. dict内部的entries改为按插入顺序存储,新增了一个indices用于储存元素在entries中的下标。dict整体仍是Hash表结构,但Hash值映射到indices中,而不是直接映射到entries中。\n2. 由于entries改为了按插入顺序存储,使得申请entries容量时只要申请Hash表长度的2/3即可,省去了Hash表中的无用空间,储存更紧凑。\n3. dict读写步骤从原先的3步变为4步:计算key的Hash值,映射到indices内存空间,从indices读取entries的下标值,用下标从entries中读写数据。读写时间复杂度仍保持为O(1),冲突、删除标记等Hash表的特性也仍然存在。indices的扩容策略也仍然是倍增扩容,但因为填充因子仍然为2/3,entries每次扩容时只需申请indices长度的2/3即可。\n\n有什么好处?\n\n1. 压缩空间:原先Hash映射是直接映射到entries上,会有大量的空隙。现在Hash映射到indices上,而entries中可更紧凑地存储元素。而indices中储存的entries下标占用内存可以比entries元素要小得多——当entries长度足够短时每个下标只需占一个字节。indices中确实也还仍有空隙,但占用空间总要比旧的dict实现要小得多了。\n2. 更快的遍历:以前的实现遍历dict要遍历整个Hash表,需要挨个位置读取一下,判断它是空闲位置还是实际存在的元素。而现在只需要对变得更紧凑的entries遍历就行了。这也带来一个新的特性:entries是按照元素插入的顺序存储的,遍历entries自然也会按元素插入的顺序输出。这就给dict带来了有序性。\n3. 扩容时关注的内存块更少。原先的entries扩容时所有数据都要重新映射到内存上,cache利用率不好。现在扩容时基本可以整个entries直接复制(当然,有删除标记的数据这时要忽略)。\n\n综上,CPython3.6以后通过增加了一个indices增加了空间利用率,在维持读写时间复杂度不变的情况下增加了遍历与扩容效率。至于dict遍历变得有序,倒是有点次要的特性了。\n\n# 我们是否应利用新dict的有序性?\n\n既然Python中dict变得有序了,那我们是否应该主动去利用它呢?我是这么认为的:\n\n1. 在Python3.6中,我们不推荐利用dict的有序性。3.6时dict的有序性还只是CPython的一个实现细节,并不是Python的语言特性。当我们的代码不是在CPython环境下运行,dict的有序性就不起作用,就容易出莫名其妙的BUG了。\n2. 在Python3.7后,dict按插入顺序进行遍历的性质被写入Python语言特性中。这时确实在代码中利用dict有序性也没什么大问题。但dict这种数据结构,最主要的特性还是表现在Key映射到Value的这种关系,以及O(1)的读写时间复杂度。当我们的代码中需要关注到dict的遍历顺序时,我们就要先质问一下自己:是否应该改为用队列或是其他数据结构来实现?\n\n\n# 参考文献\n\n- [Are dictionaries ordered in Python 3.6+?](https://stackoverflow.com/questions/39980323/are-dictionaries-ordered-in-python-3-6)\n- [[Python-Dev] Python 3.6 dict becomes compact and gets a private version; and keywords become ordered](https://mail.python.org/pipermail/python-dev/2016-September/146327.html)\n- [[Python-Dev] More compact dictionaries with faster iteration](https://mail.python.org/pipermail/python-dev/2012-December/123028.html)\n- [关于python3.6中dict如何保证有序](https://zhuanlan.zhihu.com/p/36167600)\n- [python3.7源码分析-字典_小屋子大侠的博客-CSDN博客_python 字典源码](https://blog.csdn.net/qq_33339479/article/details/90446988)\n- [《深度剖析CPython解释器》9. 解密Python中字典和集合的底层实现,深度分析哈希表](https://www.cnblogs.com/traditional/p/13503114.html)\n- [CPython 源码阅读 - dict](http://blog.dreamfever.me/2018/03/12/cpython-yuan-ma-yue-du-dict/)","title":"Python字典的实现原理","abstract":"> CPython从3.6开始,字典(dict)不再是无序的了——字典的修改了原先的底层实现,变得能按字典插入的顺序进行遍历。而Python从3.7开始将字典的有序性写入语言特性,不管是Jython、IronPython还是其他Python实现,从3.7开始大家的字典都是有序的了。\n以前参加Python相关的面试时,面试官经常都会问一个问题:Python里的字典(dict)是有序的吗?\n这自然难不倒我,我也照本宣科地讲:Python的字典底层是用哈希表实现的,在不发生冲突时读写的时间复杂度是O(1),比读写时间复杂度为O(logn)的红黑树要更快。但红黑树可以按下标的大小顺序进行遍历,而Dict遍历时是无序的。","length":121,"created_at":"2020-08-02T00:10:10.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["Python","数据结构"],"license":false}}]},"__N_SSG":true} \ No newline at end of file diff --git "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\256\227\346\263\225.json" "b/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\256\227\346\263\225.json" deleted file mode 100644 index 1a707e41..00000000 --- "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\256\227\346\263\225.json" +++ /dev/null @@ -1 +0,0 @@ -{"pageProps":{"allTagInfos":[{"tag":"Kubernetes","slug":"kubernetes","path":"/tags/kubernetes","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"DevOps","slug":"devops","path":"/tags/devops","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Docker","slug":"docker","path":"/tags/docker","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Cloud Native","slug":"cloud-native","path":"/tags/cloud-native","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Blog","slug":"blog","path":"/tags/blog","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"init-a-new-hexo-project","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"VSCode","slug":"vscode","path":"/tags/vscode","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"Hexo","slug":"hexo","path":"/tags/hexo","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"JavaScript","slug":"javascript","path":"/tags/javascript","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"GitHub","slug":"github","path":"/tags/github","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"AWS","slug":"aws","path":"/tags/aws","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"CI/CD","slug":"ci-cd","path":"/tags/ci-cd","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"IaC","slug":"iac","path":"/tags/iac","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]},{"tag":"设计模式","slug":"设计模式","path":"/tags/设计模式","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"笔记","slug":"笔记","path":"/tags/笔记","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"排序","slug":"排序","path":"/tags/排序","postSlugs":[{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"Python","slug":"python","path":"/tags/python","postSlugs":[{"postSlug":"python-dict","postType":"article"}]},{"tag":"C++","slug":"c++","path":"/tags/c++","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"}]},{"tag":"杂技","slug":"杂技","path":"/tags/杂技","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"hello-world","postType":"article"}]},{"tag":"杂谈","slug":"杂谈","path":"/tags/杂谈","postSlugs":[{"postSlug":"hello-world","postType":"article"}]},{"tag":"Nextjs","slug":"nextjs","path":"/tags/nextjs","postSlugs":[{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"Cloud Computing","slug":"cloud-computing","path":"/tags/cloud-computing","postSlugs":[{"postSlug":"newest","postType":"idea"}]}],"selectedTagInfo":{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},"posts":[{"slug":"Handy-heap-cheat-sheet","file":"public/content/articles/2021-03-21-Handy-heap-cheat-sheet.md","mediaDir":"content/articles/2021-03-21-Handy-heap-cheat-sheet","path":"/articles/Handy-heap-cheat-sheet","meta":{"content":"\n# 如何手撕一个堆\n\n# 写在前面\n\n在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\n\n# 首先要理解,然后才能实现\n\n就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\n\n## 先抓住重点:堆是一种树结构\n\n首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\n\n再进一步,在堆这种树结构中,最重要的约束就是:**对于树中的每个节点,总有父节点大于两个子节点**(以大顶堆为例,下同)。\n\n如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\n\n而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\n\n1. 堆并不关注左右子树之间的大小情况,那么**要维护一个堆,基本只需要做交换父节点与子节点的操作**,而不需要像二叉查找树那样做各种旋转操作。\n2. 因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,**可以使用数组进行实现**。\n3. 因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:**堆的增删操作最坏时间复杂度为O(logn)**。\n\n## 再抓基本操作:上浮与下沉\n\n上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点**过大**,就跟他的父节点**向上交换**;若一个节点**过小**,就跟他的子节点**向下交换**。\n\n假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\n\n![p与g交换](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png)\n\n由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\n\n1. p > g > p2\n2. g > 原p > c1与c2\n\n则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\n\n同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\n\n为了简化,我们把前面那种递归地向上交换称为**上浮操作**,把后面这种递归地向下交换称为**下沉操作**。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\n\n# 各种接口的逻辑\n\n## 插入元素——入堆\n\n把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\n\n虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\n\n## 删除堆顶元素——出堆\n\n我们从堆中删除元素时,一般只会删除堆顶元素。\n\n删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\n\n这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\n\n## 堆的初始化——建堆\n\n建立一个堆,我们有两种思路:\n\n1. 将元素一个一个插入,即对每个元素都做一次入堆操作。\n2. 当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\n\n下面我们来分析这两种建堆策略。\n\n### 元素逐个入堆\n\n上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\n\n插入第i个元素时,堆的大小为$i$(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\n\n$$T(i) = logi$$\n\n那么把所有元素上浮,则总时间复杂度为:\n\n$$\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logi\\\\\n&= 1\\times0 + 2\\times1 + ... + 2^{logn}\\times{logn} \\\\\n&=O(nlogn)\n\\end{aligned}$$\n\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\n\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\n\n### 堆合并\n\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\n\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\n\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\n\n$$T(i) = logn-logi$$\n\n那么把所有元素下沉,则总时间复杂度为:\n\n$$\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logn-logi \\\\\n&= \\frac{n}{2^{logn}}\\times{logn}+ ... + \\frac{n}{4}\\times2+\\frac{n}{2}\\times1 \\\\\n&= O(n)\n\\end{aligned}$$\n\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\n\n### 两种策略的比较与理解\n\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\n\n1. 从元素移动路径的角度\n\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\n\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\n\n2. 从元素移动数量与移动距离的角度\n\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\n\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\n\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\n\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\n\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\n\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\n\n# 代码实现\n\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\n\n```python\nT = TypeVar(\"T\")\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n```\n\n## 实现树结构\n\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\n\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\n\n```python\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n```\n\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\n\n## 实现基本操作——上浮与下沉\n\n### 上浮\n\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\n\n```python\ndef floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n```\n\n### 下沉\n\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\n\n```python\ndef size(self):\n '''返回堆大小\n '''\n return len(self.A)\n\ndef sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n```\n\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\n\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\n\n## 实现各种借口——读、增、删、初始化\n\n### 读取堆顶\n\n堆一般只允许读取堆顶,即全堆最大元素。\n\n```python\ndef top(self):\n '''返回堆顶\n '''\n return self.A[0]\n```\n\n### 入堆\n\n入堆时,把元素加到堆尾,再做上浮操作。\n\n```python\ndef insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n```\n\n### 出堆\n\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\n\n```python\ndef pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n注意入堆与出堆操作都要保证堆的大小会相应变化。\n\n### 堆初始化\n\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\n\n```python\ndef __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n```\n\n## 整体代码\n\n### 堆的整体实现\n\n综上,堆的整体代码实现如下:\n\n```python\nfrom typing import Any, Callable, Generic, List, TypeVar\n\nT = TypeVar(\"T\")\n\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n def __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n \n def size(self):\n '''返回堆大小\n '''\n return len(self.A)\n \n def top(self):\n '''返回堆顶\n '''\n return self.A[0]\n \n def sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n\n def floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n \n def insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n\n def pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n### 单元测试\n\n入堆、出堆等操作的简单单元测试如下:\n\n```python\nimport pytest\nimport heap\n\n@pytest.fixture\ndef initHeap():\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \n lambda a,b:a>b)\n\nclass Test_TestHeap:\n def test_init_notNull(self, initHeap:heap.Heap):\n assert initHeap.size() == 10\n assert initHeap.top() == 9\n \n def test_insert_notTop(self, initHeap:heap.Heap):\n initHeap.insert(6)\n assert initHeap.size() == 11\n assert initHeap.top() == 9\n \n def test_insert_top(self, initHeap:heap.Heap):\n initHeap.insert(10)\n assert initHeap.size() == 11\n assert initHeap.top() == 10\n \n def test_pop(self, initHeap:heap.Heap):\n p = initHeap.pop()\n assert p == 9\n assert initHeap.size() == 9\n assert initHeap.top() == 8\n```\n\n# 关于堆排序\n\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\n\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\n\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\n\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。","title":"如何手撕一个堆","abstract":"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。","length":477,"created_at":"2021-08-28T23:09:14.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["数据结构","算法","算法竞赛"],"license":false}},{"slug":"Sort-algorithm","file":"public/content/articles/2021-01-11-Sort-algorithm.md","mediaDir":"content/articles/2021-01-11-Sort-algorithm","path":"/articles/Sort-algorithm","meta":{"content":"\n# 序言\n\n我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。\n\n你会发现堆排序被当成是选择排序的一种优化——因为我认为堆排序主要在于使用了堆这种数据结构,而总体思想与选择排序相比没有太大变化。\n\n你还会找到其他与别的文章不一样的地方。因为这篇文章是我按照自己的理解来写的,我脑子里是这样想的,那文章里就是这样写的。我会按照我的理解,从纵向与横向两个维度,来理清楚各个排序算法的特性与异同。\n\n# 整篇文章的要点\n\n整篇文章以纵向——算法分类、以及横向——算法评价两个维度来进行组织。\n\n排序算法可以按照以下方式来进行分类:\n\n- 基于比较的排序算法\n - 基于分治思想\n - 快速排序\n - 归并排序\n - 基于有序区域扩展\n - 插入排序\n - 选择排序\n- 不基于比较的排序算法\n - 计数排序\n - 桶排序\n - 基数排序\n\n文章中还会讲一讲为什么会这么分,每种分类有什么共性,分类之间有什么差异。此外,在最后还会稍微提一提外部排序与适用于并行运算的排序等。\n\n而对于纵向分类中的每一个端点,我们又会从以下五个方面,来对各个算法进行一个总体评价:\n\n- 时间复杂度(最坏,最好,平均※)\n- 空间复杂度\n- 是否原地排序\n- 是否稳定排序\n- 能否用于链表排序\n\n而由于复杂度主要只关注数量级,因此在这篇文章里会在不影响计算结果的前提下对复杂度计算进行适当的近似与简化。\n\n# 快排\n\n## 思想\n\n分治法:先把序列分为小的部分和大的部分,再将两部分分别排序。即:复杂分割,简单合并,主要操作在于分割。\n\n## 要点\n\n### 时间复杂度\n\n推导式:T(n) = T(找) + T(左) + T(右) = O(n) + 两个子问题时间复杂度。\n\n- 最好时间复杂度为每次都正好找到最中间的一个数时时间复杂度为O(nlogn)。证明略。\n- 最坏时间复杂度为每次都正好找到最旁边的数时时间复杂度为O(n^2)。证明略。\n- 平均时间复杂度为O(nlogn),推导式如下:\n\n 快速排序每一步中,将元素分为左右两边需要遍历整个列表,耗时T(n)。假设最后定位的元素为最终第i个元素,则两个子问题复杂度分别为T(i)和T(n-i-1)。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{\\sum_{i=0}^{n-1}{T(i)+T(n-i-1)}}{n} \\\\\n &=n + \\frac{2}{n}\\times\\sum_{i=0}^{n-1}{T(i)} \\\\\n \\end{aligned}\n $$\n\n 令 $$\\sum_{i=0}^{n}T(i) = Sum(n)$$ ,即有:\n\n $$\n \\begin{aligned}\n T(n) &= n + \\frac{2}{n} \\times Sum(n-1) \\\\\n Sum(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1)\n \\end{aligned}\n $$\n\n 错位相减:\n\n $$\n \\begin{aligned}\n Sum(n) - Sum(n-1) &= \\frac{n+1}{2}T(n+1) - \\frac{n+1}{2}(n+1) - \\frac{n}{2}T(n) + \\frac{n}{2}(n) \n \\\\\n T(n) &= \\frac{n+1}{2}T(n+1) - \\frac{n}{2}T(n) - \\frac{2n+1}{2} \n \\\\\n \\frac{T(n+1)}{n+2} &= \\frac{T(n)}{n+1}+\\frac{2n+1}{(n+1)(n+2)} \n \\\\\n &= \\frac{T(n)}{n+1}+\\frac{1}{n}+\\frac{1}{n+1} \n \\\\\n &=...\n \\\\\n &= \\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+\\frac{1}{3}+...+\\frac{1}{n})+\\frac{1}{n+1}\n \\\\\n \\\\\n \\frac{T(n)}{n+1}&=\\frac{T(1)}{2}+1+2\\times(\\frac{1}{2}+...+\\frac{1}{n-1})+\\frac{1}{n}\n \\\\\n &= O(1)+O(1)+O(logn)+O(\\frac{1}{n})\n \\\\\n &= O(logn)\n \\end{aligned}\n $$\n\n 其中由于 $$\\frac{1}{x}=\\frac{d(logx)}{dx}$$ ,因此 $$\\frac{1}{2}+...+\\frac{1}{n-1}=O(logn)$$ 。\n\n 则有:\n\n $$\n \\begin{aligned}\n T(n)&=(n+1)\\times O(logn)\\\\\n &=O(n)\\times O(logn)\\\\\n &=O(nlogn)\n \\end{aligned}\n $$\n\n### 额外空间复杂度\n\n考虑栈深度,额外空间复杂度为O(logn)。由于快速排序主要步骤在于分,因此必须自上而下的进行递归,无法避免栈深度。\n\n### 原地排序\n\n虽然快速排序有额外空间复杂度,但并不妨碍它是一个原地排序。\n\n### 不稳定\n\n堆排序在分操作时将元素左右交换,会破坏稳定性。\n\n### 链表形式特点\n\n- 时间复杂度不变\n- 空间复杂度不变\n- 变为稳定排序※\n\n## 手写时的易错点\n\n- 分成左右子序列时最好完全分开(一边用`<=`一边用`>`),不然容易造成死循环。\n- 分左右子序列时仔细考虑最初下标位置与最终下标位置,以及对应位置的值的大小。\n- 不要忘记递归的结束条件。\n\n# 归并排序\n\n## 思想\n\n分治法:先把两个子序列各自排好序,然后再合并两个子序列。即:简单分割,复杂合并。主要步骤在于合并。\n\n## 要点\n\n- 时间复杂度推导式:T(n) = 2T(n/2) + T(合)\n- 平均、最好、最坏时间复杂度都是O(nlogn),推导过程略。\n- 额外空间复杂度为O(n),合并时必须准备额外空间。但由于主要步骤在于合并,可以自下而上地进行迭代合并,可以不使用栈。\n- 非原地排序\n- 稳定排序\n\n### 链表形式\n\n- 时间复杂度不变\n- 额外空间复杂度变为O(1)※\n- 稳定排序\n- 只能使用迭代形式,不能使用递归形式\n\n## 易错点\n\n- 合并时一边结束时另一边还未结束,需要把那一边也放入合并后序列中\n- 保持稳定排序:合并时左序列等于右序列时也采用左序列\n- 不要忘记递归结束条件\n- 不要忘记循环递进条件\n\n## 进阶\n\n- 原地归并排序:时间复杂度为O(log^2n),牺牲合并的时间复杂度进行原地排序。\n- 多路归并排序:使用竞标树,多路归并,用于磁盘IO。\n\n# 插入排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将无序序列当前元素插入有序序列,复杂度取决于有序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:基本有序情况下,O(n)\n- 额外空间复杂度:O(1)\n- 原地排序\n- 稳定排序\n- 每次插入时都要移动序列,写次数较多\n- 若查找插入位置时使用二分法查找,则可加快时间。(但不足以对时间复杂度造成影响,且最好时间复杂度也会上升为O(nlogn))\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 稳定排序\n- 插入时不再需要移动序列,但也不能使用二分法查找\n\n## 进阶——希尔排序\n\n- 要点:\n\n 插入排序的优点在于当序列基本有序时,时间复杂度可逼近为O(n)。\n\n 但插入时移动有序序列中元素所耗时间较多,而每次只移动一步。但实际上当序列分布均匀时,有序序列中排靠后的元素在整个序列中也会排靠后。\n\n 可以把序列分为几个大步长序列,在最初的几次插入放开移动步长,让大的元素直接移动到较后位置。再往后慢慢缩小步长,此时序列基本有序,可以利用基本有序时插入排序的优势。\n\n- 时间复杂度\n 1. 当步长为2^i时,不能使时间复杂度缩短为O(nlogn)。因为一个子序列所有元素有可能比另一个子序列最大元素都要大,这时插入排序仍需进行约n^2次操作\n 2. 当步长为2^i时效率较低,因为当步长为4已经有序时,步长为2再比较是无用比较。但由于1.的问题,不能节省比较时间。\n 3. 当步长之间最小公约数较少,甚至互质时,无用比较次数会降低。\n 4. 最坏时间复杂度下限为 $$O(nlog^2n)$$ (当步长采用 $$2^i3^j$$ 时),但一般希尔排序平均时间复杂度都为 $$O(n^{\\frac{3}{2}})$$\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序:希尔排序步长较大时会发生前后跳转。\n- 不能写为链表形式\n\n# 选择排序\n\n## 思想\n\n有序序列不断扩张,每次从无序序列中取出元素加入有序序列,直至长度为N则完成排序。\n\n每次将选无序序列中最小元素加到有序序列末尾,复杂度取决于无序序列。\n\n## 要点\n\n- 最坏、平均时间复杂度:O(n2)\n- 最好时间复杂度:O(n2),如能保证无序部分的最小元素所在位置一定(堆排序),能降低时间复杂度\n- 额外空间复杂度:O(1)\n- 原地排序\n- 不稳定排序(采用元素交换策略时)\n- 每次找到最小元素后,只需交换一次位置即可,写次数较少。\n- 若找到最小元素后,不直接交换而是进行数组移动,则可进行稳定排序,但写次数变多,与插入排序相比没有优势,也不能使用二分查找进行简化。\n\n### 链表形式\n\n- 最好,最坏,平均时间复杂度不变\n- 额外空间复杂度不变\n- 变为稳定排序※(因为链表不需要数组移动,稳定排序方式的缺点得以消除)\n\n## 进阶——堆排序\n\n- 要点:\n\n 选择排序中耗时最多的是取出无序序列中最小值的时间,需要遍历整个无序序列。\n\n 但实际上我们只关心无序序列中的最小值,而不关心其他值的位置。通过将无序序列建为堆,减少选择时间,降低总的时间复杂度。\n\n- 时间复杂度O(nlogn)\n- 额外空间复杂度O(1)\n- 原地排序\n- 不稳定排序\n- 操作时间复杂度:每次向下比较关注一个节点与其左右子堆顶元素,每次向上比较只关注节点与其父元素(大顶堆,堆大小为n)\n - 下沉:向下比较,若顶元素不是最大,将顶元素与较大的子堆堆顶元素交换。递归处理该子堆顶元素,直到向下比较顶元素最大。\n\n 最好时间复杂度O(1),最坏时间复杂度为O(h)=O(logn),平均O(logn)\n\n - 上浮:向上比较,若元素比其上层要大,交换该元素与其上层元素。递归处理其上层元素,直到向上比较不比上层要大。\n\n 最好时间复杂度O(1),最坏时间复杂度O(h)=O(logn),平均O(logn)\n\n - 入堆:堆扩容一位,将新元素插到尾部,将该元素上浮,最坏、平均时间复杂度O(logn)\n - 出堆:取出堆顶元素,将尾部放到堆顶,将该元素下沉,最坏、平均时间复杂度O(logn)\n - 缺点:通常堆尾元素较小,出堆时将堆尾元素放到堆顶再下沉基本要沉到堆底,无用比较较多\n- 建堆时间复杂度O(n)\n - 策略1:从头开始建堆,逐个元素插入,时间复杂度取决于最后一层,时间复杂度为O(nlogn)\n - 每次将堆扩容一位,将末尾元素上浮。\n - 时间复杂度推导:\n\n 每次插入时间:\n\n $$\n \\begin{aligned} T(i) &= h\\\\\n &= logi\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n}logi\\\\\n &= 1\\times1 + 2\\times2+ 3\\times4 + ...+h\\times \\frac{n}{2} \\\\\n \\\\\n 2T(n) &= 1\\times2 + 2\\times4+ 3\\times8 + ...+h\\times n \\\\\n \\\\\n 2T(n)-T(n) &= h\\times n - (1+2+4+...+2^{h-1}) \\\\\n &= h\\times n - O(2^h) \\\\\n \\\\\n T(n) &= nlogn - O(2^h) \\\\\n &= O(nlogn)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(nlogn)$$\n\n - 策略2:从后开始建堆,小堆合并(逐个元素下沉),时间复杂度为O(n)\n - 每次堆合并时,有三部分:左子堆,右子堆,顶元素。下沉顶元素。\n - 时间复杂度推导:\n\n 第i个元素合并时,时间为:\n\n $$\n \\begin{aligned}\n T(i) &= h_{子堆}\n \\end{aligned}\n $$\n\n 则有总时间:\n\n $$\n \\begin{aligned}\n T(n) &= \\sum_{i=0}^{n} (h_{子堆}) \\\\\n &= h + (h-1)\\times2 + ... + 2 \\times \\frac{n}{4} + 1 \\times\\frac{n}{2}\\\\\n \\\\\n 2T(n) &= h \\times 2 + (h-1) \\times 4 + ... + 2 \\times \\frac{n}{2} + n \\\\\n \\\\\n 2T(n) - T(n) &= n + \\frac{n}{2} + ... + 2 - h \\\\\n \\\\\n T(n) &= O(n) - h \\\\ \n &= O(n)\n \\end{aligned}\n $$\n\n 即时间复杂度为 $$O(n)$$ \n\n - 虽说每步是做一个小堆合并,但实际上从堆尾到堆头遍历,相当于仅关注元素没有稳定,相当于可以直接使用下沉操作。\n\n# 基于比较的排序算法时间复杂度下限:逆序对思想\n\n基于比较的排序算法可以看作序列逆序对的消除。完全随机序列逆序对数量为O(n^2),若一次元操作只消除一个逆序对,则时间复杂度不会低于O(n^2)。降低时间复杂度关键在于一次消除多个逆序对。\n\n1. 希尔排序通过增大最初的步长来企图一次消除多个逆序对。\n2. 归并排序消除逆序对最主要在于归并步骤。最后几次合并每个子步骤用O(1)时间消除O(n)个逆序对。\n3. 快速排序消除逆序对最主要在于划分步骤。每个划分步骤用O(n)时间消除O(n)+O(左长度×右长度)个逆序对。\n4. 堆排序逆序对消除方式比较Tricky,但可以看出消除逆序对大致在于出堆步骤,通过O(logn)时间复杂度消除O(n)个逆序对。(左小右大排序时需要建立左大右小的大顶堆,建堆时基本没有消除逆序对)\n\n# 最后\n\n这篇文章我们主要关注了排序算法中的大头——基于比较的排序算法。在下篇文章,我们再来看一下不基于比较的排序算法,以及外排序与并行排序。","title":"排序算法","abstract":"我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。\n当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:\n在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。","length":342,"created_at":"2021-01-11T22:57:10.000Z","updated_at":"2024-04-14T13:30:33.000Z","tags":["数据结构","算法","排序"],"license":false}}]},"__N_SSG":true} \ No newline at end of file diff --git "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\256\227\346\263\225\347\253\236\350\265\233.json" "b/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\256\227\346\263\225\347\253\236\350\265\233.json" deleted file mode 100644 index 72dfb875..00000000 --- "a/_next/data/PqrPNWMG-77f9Y8Ei6sRo/tags/\347\256\227\346\263\225\347\253\236\350\265\233.json" +++ /dev/null @@ -1 +0,0 @@ -{"pageProps":{"allTagInfos":[{"tag":"Kubernetes","slug":"kubernetes","path":"/tags/kubernetes","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"DevOps","slug":"devops","path":"/tags/devops","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Docker","slug":"docker","path":"/tags/docker","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Cloud Native","slug":"cloud-native","path":"/tags/cloud-native","postSlugs":[{"postSlug":"introduction-for-k8s-2","postType":"article"},{"postSlug":"introduction-for-k8s","postType":"article"},{"postSlug":"newest","postType":"idea"}]},{"tag":"Blog","slug":"blog","path":"/tags/blog","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"},{"postSlug":"create-blog-cicd-by-github","postType":"article"},{"postSlug":"init-a-new-hexo-project","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"VSCode","slug":"vscode","path":"/tags/vscode","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"Hexo","slug":"hexo","path":"/tags/hexo","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"JavaScript","slug":"javascript","path":"/tags/javascript","postSlugs":[{"postSlug":"use-paste-image-and-vscode-memo","postType":"article"}]},{"tag":"GitHub","slug":"github","path":"/tags/github","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"AWS","slug":"aws","path":"/tags/aws","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"CI/CD","slug":"ci-cd","path":"/tags/ci-cd","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"IaC","slug":"iac","path":"/tags/iac","postSlugs":[{"postSlug":"create-blog-cicd-by-github","postType":"article"}]},{"tag":"数据结构","slug":"数据结构","path":"/tags/数据结构","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"},{"postSlug":"python-dict","postType":"article"}]},{"tag":"算法","slug":"算法","path":"/tags/算法","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"},{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]},{"tag":"设计模式","slug":"设计模式","path":"/tags/设计模式","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"笔记","slug":"笔记","path":"/tags/笔记","postSlugs":[{"postSlug":"The-beauty-of-design-parten","postType":"article"}]},{"tag":"排序","slug":"排序","path":"/tags/排序","postSlugs":[{"postSlug":"Sort-algorithm","postType":"article"}]},{"tag":"Python","slug":"python","path":"/tags/python","postSlugs":[{"postSlug":"python-dict","postType":"article"}]},{"tag":"C++","slug":"c++","path":"/tags/c++","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"}]},{"tag":"杂技","slug":"杂技","path":"/tags/杂技","postSlugs":[{"postSlug":"the-using-in-cpp","postType":"article"},{"postSlug":"Building-this-blog","postType":"article"},{"postSlug":"hello-world","postType":"article"}]},{"tag":"杂谈","slug":"杂谈","path":"/tags/杂谈","postSlugs":[{"postSlug":"hello-world","postType":"article"}]},{"tag":"Nextjs","slug":"nextjs","path":"/tags/nextjs","postSlugs":[{"postSlug":"blog-syntax","postType":"idea"},{"postSlug":"blog-in-next","postType":"idea"}]},{"tag":"Cloud Computing","slug":"cloud-computing","path":"/tags/cloud-computing","postSlugs":[{"postSlug":"newest","postType":"idea"}]}],"selectedTagInfo":{"tag":"算法竞赛","slug":"算法竞赛","path":"/tags/算法竞赛","postSlugs":[{"postSlug":"Handy-heap-cheat-sheet","postType":"article"}]},"posts":[{"slug":"Handy-heap-cheat-sheet","file":"public/content/articles/2021-03-21-Handy-heap-cheat-sheet.md","mediaDir":"content/articles/2021-03-21-Handy-heap-cheat-sheet","path":"/articles/Handy-heap-cheat-sheet","meta":{"content":"\n# 如何手撕一个堆\n\n# 写在前面\n\n在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。\n\n# 首先要理解,然后才能实现\n\n就像人总不会忘记自行车怎么骑一样,只要理解了数据结构的原理,身体就会自动来帮我们记忆,总不会忘。那要怎么理解一个堆呢?\n\n## 先抓住重点:堆是一种树结构\n\n首先最重要的,要理解堆是一种树结构。不管实际是基于数组实现还是别的什么实现,逻辑结构是树结构没变的。\n\n再进一步,在堆这种树结构中,最重要的约束就是:**对于树中的每个节点,总有父节点大于两个子节点**(以大顶堆为例,下同)。\n\n如此一来,大小关系在树中层层传递,最终可得树的根节点(堆顶)就是整个堆的最大节点,读取堆中最大值的时间复杂度为O(1)。而我们使用堆也一般是为了利用这种堆顶元素就是最大值的特点,读取、删除操作一般会限制为只允许读取、删除堆顶元素。\n\n而且我们可以注意到,与二叉查找树比起来,堆的约束十分之弱:堆只约束父节点与子节点的大小关系,而不需要管左右子树的大小关系,甚至不需要管左右两个子节点之间谁大谁小。这样一来堆就有很多很好的性质了:\n\n1. 堆并不关注左右子树之间的大小情况,那么**要维护一个堆,基本只需要做交换父节点与子节点的操作**,而不需要像二叉查找树那样做各种旋转操作。\n2. 因为维护一个堆不需要做旋转操作,那么几乎不需要花任何代价,就可以把堆的树结构维持在完全二叉树状态。因此堆的物理结构可以设计得很紧凑,**可以使用数组进行实现**。\n3. 因为堆可以维持在完全二叉树状态,那么堆的树结构的高度就可以控制为O(logn)范围内。而如上所述,要维护一个堆我们不需要关注左右子树的关系。因此我们要在堆上做增删操作,都只需要上下交换若干次父子节点。而交换次数最多时,也只是从树根一直交换到树叶,或是从树叶一直交换到树根,最多交换logn次。那么我们可得:**堆的增删操作最坏时间复杂度为O(logn)**。\n\n## 再抓基本操作:上浮与下沉\n\n上面也提到,要维护一个堆,我们只需要上下交换若干次父子节点即可。若一个节点**过大**,就跟他的父节点**向上交换**;若一个节点**过小**,就跟他的子节点**向下交换**。\n\n假设p节点过大破坏了堆结构,即p节点比其父节点g还要大,向上交换如下图:\n\n![p与g交换](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/change.png)\n\n由于除了p过大破坏堆结构以外,其他节点都符合堆结构,则有:\n\n1. p > g > p2\n2. g > 原p > c1与c2\n\n则向上交换后有只有一种破坏堆结构的可能性:p节点过大,比gg节点还要大。而解决方法也很简单,就是递归地进行向上交换,最坏情况下一直交换到堆根节点为止。\n\n同理可得,p节点过小,小于他的子节点时,向下交换后有可能需要递归地向下交换,最坏情况下一直交换到叶子节点为止。要注意向下交换时需要先比较一下两个子节点的大小,再跟较大的子节点交换,才能交换后的大小关系符合堆的要求。\n\n为了简化,我们把前面那种递归地向上交换称为**上浮操作**,把后面这种递归地向下交换称为**下沉操作**。所有需要维护堆结构的操作:增、删、建堆,都可以拆分为上浮操作或是下沉操作的组合。\n\n# 各种接口的逻辑\n\n## 插入元素——入堆\n\n把一个元素p加入堆中,我们可以先把p加到堆尾,然后对p做上浮操作。\n\n虽然堆是一个树结构,但由于堆可以用数组实现,那我们只要用O(1)的时间就可以找到堆尾。而如上面所述上浮操作最多交换到根节点 。由于用数组实现的堆是完全二叉树,交换到根节点时间复杂度为O(logn)。因此我们可得入堆的最坏时间复杂度为O(logn)。\n\n## 删除堆顶元素——出堆\n\n我们从堆中删除元素时,一般只会删除堆顶元素。\n\n删除堆顶元素时,我们可以摘出堆尾元素p填到堆顶的空缺中,再对p做下沉操作。找到堆尾元素需要O(1)时间,下沉操作最多交换到叶子节点,时间复杂度为O(logn)。因此出堆最坏时间复杂度为O(logn)。\n\n这里加点餐:出堆时把堆尾元素p放到堆顶后下沉,而p原先在堆中的最下层,一般在整个堆中都算较小的元素。因此下沉p时有较大概率需要一直把p下沉到最下层或是倒数第二层,即出堆时最坏情况出现概率较高。\n\n## 堆的初始化——建堆\n\n建立一个堆,我们有两种思路:\n\n1. 将元素一个一个插入,即对每个元素都做一次入堆操作。\n2. 当节点p左子树和右子树都各自为一个堆时,只要把p下沉就可以把左右两个堆合并成一个更大的堆。即不断地进行堆合并操作。\n\n下面我们来分析这两种建堆策略。\n\n### 元素逐个入堆\n\n上面说到,入堆就是把元素加到堆尾,再做上浮操作。把元素逐个入堆,就是把元素逐个上浮。\n\n插入第i个元素时,堆的大小为$i$(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:\n\n$$T(i) = logi$$\n\n那么把所有元素上浮,则总时间复杂度为:\n\n$$\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logi\\\\\n&= 1\\times0 + 2\\times1 + ... + 2^{logn}\\times{logn} \\\\\n&=O(nlogn)\n\\end{aligned}$$\n\n通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png)\n\n(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)\n\n### 堆合并\n\n我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。\n\n当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。\n\n下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为:\n\n$$T(i) = logn-logi$$\n\n那么把所有元素下沉,则总时间复杂度为:\n\n$$\\begin{aligned}\nT(n) &= \\sum_{i=1}^{n}logn-logi \\\\\n&= \\frac{n}{2^{logn}}\\times{logn}+ ... + \\frac{n}{4}\\times2+\\frac{n}{2}\\times1 \\\\\n&= O(n)\n\\end{aligned}$$\n\n同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png)\n\n### 两种策略的比较与理解\n\n逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解:\n\n1. 从元素移动路径的角度\n\n 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。\n\n 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。\n\n2. 从元素移动数量与移动距离的角度\n\n 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。\n\n 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。\n\n 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。\n\n 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。\n\n ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png)\n\n综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。\n\n# 代码实现\n\n其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。\n\n```python\nT = TypeVar(\"T\")\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n```\n\n## 实现树结构\n\n堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。\n\n对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:\n\n```python\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n```\n\n至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。\n\n![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png)\n\n## 实现基本操作——上浮与下沉\n\n### 上浮\n\n上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。\n\n```python\ndef floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n```\n\n### 下沉\n\n而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。\n\n```python\ndef size(self):\n '''返回堆大小\n '''\n return len(self.A)\n\ndef sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n```\n\n注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。\n\n但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。\n\n## 实现各种借口——读、增、删、初始化\n\n### 读取堆顶\n\n堆一般只允许读取堆顶,即全堆最大元素。\n\n```python\ndef top(self):\n '''返回堆顶\n '''\n return self.A[0]\n```\n\n### 入堆\n\n入堆时,把元素加到堆尾,再做上浮操作。\n\n```python\ndef insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n```\n\n### 出堆\n\n出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。\n\n```python\ndef pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n注意入堆与出堆操作都要保证堆的大小会相应变化。\n\n### 堆初始化\n\n堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。\n\n```python\ndef __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n```\n\n## 整体代码\n\n### 堆的整体实现\n\n综上,堆的整体代码实现如下:\n\n```python\nfrom typing import Any, Callable, Generic, List, TypeVar\n\nT = TypeVar(\"T\")\n\ndef lfChildOf(i:int):\n return (i + 1) << 1 - 1\n\ndef rtChildOf(i:int):\n return (i + 1) << 1\n\ndef parentOf(i:int):\n return (i - 1) >> 1\n\nclass Heap(Generic[T]):\n '''堆结构\n\n 有两个成员:\n self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组\n self.fCompare: Callable[[T,T],bool] # 比较函数\n \n 下面假设堆为大顶堆\n 即有self.fCompare = lambda a,b: a>b\n '''\n def __init__(self, A:List[T]=[], \n fCompare:Callable[[T,T],bool]=lambda a,b:a>b\n ) -> None:\n '''堆初始化\n\n :param A: 在数组A上进行初始化\n :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True\n '''\n self.A = A\n self.fCompare = fCompare\n for i in reversed(range(len(A))):\n self.sinkDown(i)\n \n def size(self):\n '''返回堆大小\n '''\n return len(self.A)\n \n def top(self):\n '''返回堆顶\n '''\n return self.A[0]\n \n def sinkDown(self, i:int):\n '''下沉操作\n\n 对下标为i的元素递归地进行下沉操作\n 直到该元素大于其两个子节点或该元素下沉到叶子节点\n '''\n lc = lfChildOf(i)\n rc = rtChildOf(i)\n\n # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素\n larger = i\n if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):\n larger = lc\n if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):\n larger = rc\n \n # 当元素i大于其两个子节点时符合堆结构,结束递归\n # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归\n if larger == i:\n return\n \n # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉\n self.A[larger], self.A[i] = self.A[i], self.A[larger]\n self.sinkDown(larger)\n\n def floatUp(self, i:int):\n '''上浮操作\n\n 对下标为i的元素递归地进行上浮操作\n 直到该元素小于其父节点或该元素上浮到根节点\n '''\n # 元素i上浮到根节点时结束递归\n if i <= 0:\n return\n \n # 当元素i小于其父节点时符合堆结构,结束递归\n pr = parentOf(i)\n if self.fCompare(self.A[pr], self.A[i]):\n return\n \n # 元素i大于其父节点,交换i与其父节点并继续上浮\n self.A[pr], self.A[i] = self.A[i], self.A[pr]\n self.floatUp(pr)\n \n def insert(self, v:T):\n '''入堆\n '''\n # 将元素加到堆尾并做上浮操作\n self.A.append(v)\n self.floatUp(len(self.A) - 1)\n\n def pop(self)->T:\n '''出堆\n '''\n # 取出堆顶元素\n res = self.A[0]\n\n # 将堆尾元素填到堆顶并做下沉操作\n self.A[0] = self.A[len(self.A) - 1]\n self.A.pop()\n self.sinkDown(0)\n\n return res\n```\n\n### 单元测试\n\n入堆、出堆等操作的简单单元测试如下:\n\n```python\nimport pytest\nimport heap\n\n@pytest.fixture\ndef initHeap():\n return heap.Heap([1,3,4,7,2,6,5,9,0,8], \n lambda a,b:a>b)\n\nclass Test_TestHeap:\n def test_init_notNull(self, initHeap:heap.Heap):\n assert initHeap.size() == 10\n assert initHeap.top() == 9\n \n def test_insert_notTop(self, initHeap:heap.Heap):\n initHeap.insert(6)\n assert initHeap.size() == 11\n assert initHeap.top() == 9\n \n def test_insert_top(self, initHeap:heap.Heap):\n initHeap.insert(10)\n assert initHeap.size() == 11\n assert initHeap.top() == 10\n \n def test_pop(self, initHeap:heap.Heap):\n p = initHeap.pop()\n assert p == 9\n assert initHeap.size() == 9\n assert initHeap.top() == 8\n```\n\n# 关于堆排序\n\n算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。\n\n然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:\n\n1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。\n2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。\n3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。\n\n关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。","title":"如何手撕一个堆","abstract":"在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。\n当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。\n但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。","length":477,"created_at":"2021-08-28T23:09:14.000Z","updated_at":"2022-03-27T13:30:33.000Z","tags":["数据结构","算法","算法竞赛"],"license":false}}]},"__N_SSG":true} \ No newline at end of file diff --git a/_next/static/PqrPNWMG-77f9Y8Ei6sRo/_buildManifest.js b/_next/static/06cVAX8N9Z8KmESdU4cc2/_buildManifest.js similarity index 100% rename from _next/static/PqrPNWMG-77f9Y8Ei6sRo/_buildManifest.js rename to _next/static/06cVAX8N9Z8KmESdU4cc2/_buildManifest.js diff --git a/_next/static/PqrPNWMG-77f9Y8Ei6sRo/_ssgManifest.js b/_next/static/06cVAX8N9Z8KmESdU4cc2/_ssgManifest.js similarity index 100% rename from _next/static/PqrPNWMG-77f9Y8Ei6sRo/_ssgManifest.js rename to _next/static/06cVAX8N9Z8KmESdU4cc2/_ssgManifest.js diff --git a/articles.html b/articles.html index 0fcfc147..a3e0fdc0 100644 --- a/articles.html +++ b/articles.html @@ -1 +1 @@ -<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="description" content="The blog owned by Ryo, about Programing, Painting, and Gaming."/><meta property="og:description" content="The blog owned by Ryo, about Programing, Painting, and Gaming."/><meta name="twitter:description" content="The blog owned by Ryo, about Programing, Painting, and Gaming."/><meta property="og:image" content="https://ryojerryyu.github.io/blog-next/img/home-bg-kasumi-hanabi.jpg"/><meta name="twitter:image" content="https://ryojerryyu.github.io/blog-next/img/home-bg-kasumi-hanabi.jpg"/><meta property="og:type" content="website"/><meta property="og:url" content="https://blog.ryo-okami.xyz/articles"/><meta name="twitter:card" content="summary_large_image"/><meta name="twitter:site" content="@ryo_okami"/><meta name="twitter:creator" content="@ryo_okami"/><link rel="icon" href="/blog-next/favicon.ico"/><meta name="viewport" content="width=device-width, initial-scale=1"/><meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"/><title>Articles | Ryo's Blog
© 2023 Ryo Jerry Yu. All rights reserved.
\ No newline at end of file +Articles | Ryo's Blog
© 2023 Ryo Jerry Yu. All rights reserved.
\ No newline at end of file diff --git a/articles/Building-this-blog.html b/articles/Building-this-blog.html index 22adcc73..1a9d0626 100644 --- a/articles/Building-this-blog.html +++ b/articles/Building-this-blog.html @@ -4,7 +4,7 @@ 参考[BruceZhao][BruceZhao]提供的中文翻译:[README.zh.md][READMEzh],先将[Huxpro][Huxpro]提供的[博客模板仓库][origin_repo]fork出来,`git clone`到本地。 整个网站文件夹大致结构如下:"/>

搭建博客的过程

+整个网站文件夹大致结构如下:"/>
Loading comments...
© 2023 Ryo Jerry Yu. All rights reserved.
\ No newline at end of file +

Loading comments...
© 2023 Ryo Jerry Yu. All rights reserved.
\ No newline at end of file diff --git a/articles/Handy-heap-cheat-sheet.html b/articles/Handy-heap-cheat-sheet.html index da1394a8..c76ec564 100644 --- a/articles/Handy-heap-cheat-sheet.html +++ b/articles/Handy-heap-cheat-sheet.html @@ -4,7 +4,7 @@ 当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。 但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。"/>

如何手撕一个堆

如何手撕一个堆

+但是,如果哪一天你把编程语言的类库全忘光了,又遇到一题需要频繁求最值的题目——你明知这里要用堆,却又忘记该调用的类名了,咋办?我还真遇到过这问题:三年没刷算法,只能对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候,只能自己实现一个堆出来了。"/>

如何手撕一个堆

如何手撕一个堆

写在前面

在参加如AtCoder等算法竞技,或是刷Leetcode等算法题时,我们总是不可避免地遇到堆这种数据结构。

当然,一般来说我们只要理解堆,知道堆的性质,知道怎么样用堆就足够了。在做题时只需要调用系统类库即可——在参加AtCoder时你甚至不会有时间去自己实现一个堆。

@@ -53,396 +53,317 @@

元素逐个入堆

插入第i个元素时,堆的大小为ii(在不影响计算情况下的近似,下同),则有堆的高度为,则上浮时间复杂度为:

T(i)=logiT(i) = logi

那么把所有元素上浮,则总时间复杂度为:

-
T(n) &= \sum_{i=1}^{n}logi\\ +
T(n)=i=1nlogi=1×0+2×1+...+2logn×logn=O(nlogn)\begin{aligned} +T(n) &= \sum_{i=1}^{n}logi\\ &= 1\times0 + 2\times1 + ... + 2^{logn}\times{logn} \\ &=O(nlogn) -\end{aligned}$$ - -通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示: - -![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/insert-length.png) - -(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度) - -### 堆合并 - -我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。 - -当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。 - -下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为: - -$$T(i) = logn-logi$$ - -那么把所有元素下沉,则总时间复杂度为: - -$$\begin{aligned} +\end{aligned}
+

通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示:

+ +

(每条红线的长度就是插入该元素所需的时间,红线的总长度就是建堆所需的总时间复杂度)

+

堆合并

+

我们就可以从树结构的最底层出发不断进行堆合并,小堆合并成大堆,最后合并到根节点就建成整个堆结构。

+

当节点的左右两个子树都是堆时,只需要对该节点进行下沉操作就可以合并左右两个堆。 不断进行堆合并,就是从下层开始把元素逐个下沉。

+

下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为lognlogilogn-logi,则有下沉时间复杂度为:

+
T(i)=lognlogiT(i) = logn-logi
+

那么把所有元素下沉,则总时间复杂度为:

+
T(n)=i=1nlognlogi=n2logn×logn+...+n4×2+n2×1=O(n)\begin{aligned} T(n) &= \sum_{i=1}^{n}logn-logi \\ &= \frac{n}{2^{logn}}\times{logn}+ ... + \frac{n}{4}\times2+\frac{n}{2}\times1 \\ &= O(n) -\end{aligned}$$ - -同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意: - -![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/merge-length.png) - -### 两种策略的比较与理解 - -逐个元素入堆的策略时间复杂度为$O(logn)$,堆合并策略的时间复杂度为$O(n)$,为什么会出现差异呢?我们可以从两个角度来理解: - -1. 从元素移动路径的角度 - - 我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。 - - 这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。 - -2. 从元素移动数量与移动距离的角度 - - 我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此**建堆的时间复杂度主要取决于底层元素**的移动距离。 - - 用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,$O(n)$个元素需要移动$O(logn)$的距离,因此时间复杂度较高。 - - 而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有$O(n)$。 - - 如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。 - - ![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/move-length.png) - -综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需$O(n)$。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。 - -# 代码实现 - -其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。 - -```python -T = TypeVar("T") -class Heap(Generic[T]): - '''堆结构 - - 有两个成员: - self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组 - self.fCompare: Callable[[T,T],bool] # 比较函数 - - 下面假设堆为大顶堆 - 即有self.fCompare = lambda a,b: a>b - ''' -``` - -## 实现树结构 - -堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。 - -对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示: - -```python -def lfChildOf(i:int): - return (i + 1) << 1 - 1 - -def rtChildOf(i:int): - return (i + 1) << 1 - -def parentOf(i:int): - return (i - 1) >> 1 -``` - -至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。 - -![](/img/in-post/2021-03-21-Handly-heap-cheat-sheet/tree-struct-function.png) - -## 实现基本操作——上浮与下沉 - -### 上浮 - -上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。 - -```python -def floatUp(self, i:int): - '''上浮操作 - - 对下标为i的元素递归地进行上浮操作 - 直到该元素小于其父节点或该元素上浮到根节点 - ''' - # 元素i上浮到根节点时结束递归 - if i <= 0: - return - - # 当元素i小于其父节点时符合堆结构,结束递归 - pr = parentOf(i) - if self.fCompare(self.A[pr], self.A[i]): - return - - # 元素i大于其父节点,交换i与其父节点并继续上浮 - self.A[pr], self.A[i] = self.A[i], self.A[pr] - self.floatUp(pr) -``` - -### 下沉 - -而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。 - -```python -def size(self): - '''返回堆大小 - ''' - return len(self.A) - -def sinkDown(self, i:int): - '''下沉操作 - - 对下标为i的元素递归地进行下沉操作 - 直到该元素大于其两个子节点或该元素下沉到叶子节点 - ''' - lc = lfChildOf(i) - rc = rtChildOf(i) - - # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素 - larger = i - if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]): - larger = lc - if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]): - larger = rc - - # 当元素i大于其两个子节点时符合堆结构,结束递归 - # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归 - if larger == i: - return - - # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉 - self.A[larger], self.A[i] = self.A[i], self.A[larger] - self.sinkDown(larger) -``` - -注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是$O(1)$。 - -但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为$O(1)$。要改成迭代实现并不困难,还请大家尝试自己实现。 - -## 实现各种借口——读、增、删、初始化 - -### 读取堆顶 - -堆一般只允许读取堆顶,即全堆最大元素。 - -```python -def top(self): - '''返回堆顶 - ''' - return self.A[0] -``` - -### 入堆 - -入堆时,把元素加到堆尾,再做上浮操作。 - -```python -def insert(self, v:T): - '''入堆 - ''' - # 将元素加到堆尾并做上浮操作 - self.A.append(v) - self.floatUp(len(self.A) - 1) -``` - -### 出堆 - -出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。 - -```python -def pop(self)->T: - '''出堆 - ''' - # 取出堆顶元素 - res = self.A[0] - - # 将堆尾元素填到堆顶并做下沉操作 - self.A[0] = self.A[len(self.A) - 1] - self.A.pop() - self.sinkDown(0) - - return res -``` - -注意入堆与出堆操作都要保证堆的大小会相应变化。 - -### 堆初始化 - -堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。 - -```python -def __init__(self, A:List[T]=[], - fCompare:Callable[[T,T],bool]=lambda a,b:a>b - ) -> None: - '''堆初始化 - - :param A: 在数组A上进行初始化 - :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True - ''' - self.A = A - self.fCompare = fCompare - for i in reversed(range(len(A))): - self.sinkDown(i) -``` - -## 整体代码 - -### 堆的整体实现 - -综上,堆的整体代码实现如下: - -```python -from typing import Any, Callable, Generic, List, TypeVar - -T = TypeVar("T") - -def lfChildOf(i:int): - return (i + 1) << 1 - 1 - -def rtChildOf(i:int): - return (i + 1) << 1 - -def parentOf(i:int): - return (i - 1) >> 1 - -class Heap(Generic[T]): - '''堆结构 - - 有两个成员: - self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组 - self.fCompare: Callable[[T,T],bool] # 比较函数 - - 下面假设堆为大顶堆 - 即有self.fCompare = lambda a,b: a>b - ''' - def __init__(self, A:List[T]=[], - fCompare:Callable[[T,T],bool]=lambda a,b:a>b - ) -> None: - '''堆初始化 - - :param A: 在数组A上进行初始化 - :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True - ''' - self.A = A - self.fCompare = fCompare - for i in reversed(range(len(A))): - self.sinkDown(i) - - def size(self): - '''返回堆大小 - ''' - return len(self.A) - - def top(self): - '''返回堆顶 - ''' - return self.A[0] - - def sinkDown(self, i:int): - '''下沉操作 - - 对下标为i的元素递归地进行下沉操作 - 直到该元素大于其两个子节点或该元素下沉到叶子节点 - ''' - lc = lfChildOf(i) - rc = rtChildOf(i) - - # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素 - larger = i - if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]): - larger = lc - if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]): - larger = rc - - # 当元素i大于其两个子节点时符合堆结构,结束递归 - # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归 - if larger == i: - return - - # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉 - self.A[larger], self.A[i] = self.A[i], self.A[larger] - self.sinkDown(larger) - - def floatUp(self, i:int): - '''上浮操作 - - 对下标为i的元素递归地进行上浮操作 - 直到该元素小于其父节点或该元素上浮到根节点 - ''' - # 元素i上浮到根节点时结束递归 - if i <= 0: - return - - # 当元素i小于其父节点时符合堆结构,结束递归 - pr = parentOf(i) - if self.fCompare(self.A[pr], self.A[i]): - return - - # 元素i大于其父节点,交换i与其父节点并继续上浮 - self.A[pr], self.A[i] = self.A[i], self.A[pr] - self.floatUp(pr) - - def insert(self, v:T): - '''入堆 - ''' - # 将元素加到堆尾并做上浮操作 - self.A.append(v) - self.floatUp(len(self.A) - 1) - - def pop(self)->T: - '''出堆 - ''' - # 取出堆顶元素 - res = self.A[0] - - # 将堆尾元素填到堆顶并做下沉操作 - self.A[0] = self.A[len(self.A) - 1] - self.A.pop() - self.sinkDown(0) - - return res -``` - -### 单元测试 - -入堆、出堆等操作的简单单元测试如下: - -```python -import pytest -import heap - -@pytest.fixture -def initHeap(): - return heap.Heap([1,3,4,7,2,6,5,9,0,8], - lambda a,b:a>b) - -class Test_TestHeap: - def test_init_notNull(self, initHeap:heap.Heap): - assert initHeap.size() == 10 - assert initHeap.top() == 9 - - def test_insert_notTop(self, initHeap:heap.Heap): - initHeap.insert(6) - assert initHeap.size() == 11 - assert initHeap.top() == 9 - - def test_insert_top(self, initHeap:heap.Heap): - initHeap.insert(10) - assert initHeap.size() == 11 - assert initHeap.top() == 10 - - def test_pop(self, initHeap:heap.Heap): - p = initHeap.pop() - assert p == 9 - assert initHeap.size() == 9 - assert initHeap.top() == 8 -``` - -# 关于堆排序 - -算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为$O(nlogn)$等优秀的性质,是比较常用的一个排序算法。 - -然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下: - -1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。 -2. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。 -3. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。 - -关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。

Loading comments...
© 2023 Ryo Jerry Yu. All rights reserved.
\ No newline at end of file +\end{aligned}
+

同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意:

+ +

两种策略的比较与理解

+

逐个元素入堆的策略时间复杂度为O(logn)O(logn),堆合并策略的时间复杂度为O(n)O(n),为什么会出现差异呢?我们可以从两个角度来理解:

+
    +
  1. +

    从元素移动路径的角度

    +

    我们从前一小节的两幅图中可发现,元素入堆策略的图中根节点附近红线十分密集。而堆合并策略的红线则整体来说比较稀疏。

    +

    这说明元素入堆策略中,在根节点附近元素做了较多重复无效的移动——也就是说插入一个元素时上浮到了根节点附近,然后又被其他后来的元素顶替下来。一上一下自然消耗了多余的时间,而这种消耗在元素入堆策略中出现频率高,无可忽视。

    +
  2. +
  3. +

    从元素移动数量与移动距离的角度

    +

    我们知道一般来说树的越下层节点数量越多。特别是用数组实现的堆是个完全二叉树,最下层节点数量占了总数的一半。 因此建堆的时间复杂度主要取决于底层元素的移动距离。

    +

    用元素入堆策略需要每个元素进行上浮操作,而偏偏元素数量最多的底层移动距离最长,O(n)O(n)个元素需要移动O(logn)O(logn)的距离,因此时间复杂度较高。

    +

    而堆合并策略则反过来,需要每个元素进行下沉操作。移动距离最长的只有一个根元素,底层元素几乎不需要移动,因此时间复杂度加起来只有O(n)O(n)

    +

    如图所示,颜色越深代表移动距离越长。颜色深度对面积的积分即为建堆时间复杂度。

    + +
  4. +
+

综上分析我们可以得出,通过堆合并策略建堆较优,时间复杂度只需O(n)O(n)。因此我们建堆一般采用堆合并策略,从下往上逐个元素下沉。

+

代码实现

+

其实理解了上面这些,要写一个堆出来也已经是水到渠成了。但正如Linus所说,Talk is cheap, show me the code。我们还是要亲手写一段,才能知道堆到底长啥样。

+
T = TypeVar("T")
+class Heap(Generic[T]):
+    '''堆结构
+ 
+    有两个成员:
+    self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组
+    self.fCompare: Callable[[T,T],bool] # 比较函数
+    
+    下面假设堆为大顶堆
+    即有self.fCompare = lambda a,b: a>b
+    '''
+

实现树结构

+

堆可以实现为基于数组的完全二叉树,以下标为零的节点为树根节点。

+

对于下标为i的节点,其左子节点、右子节点、父节点的下标分别如下所示:

+
def lfChildOf(i:int):
+    return (i + 1) << 1 - 1
+ 
+def rtChildOf(i:int):
+    return (i + 1) << 1
+ 
+def parentOf(i:int):
+    return (i - 1) >> 1
+

至于为什么是这样,是因为完全二叉树与数组的对应规则如下图所示。这三个函数也没必要记住,到时候纸上画一画就记起来了。

+ +

实现基本操作——上浮与下沉

+

上浮

+

上浮就是递归地进行向上交换,下沉就是递归地进行向下交换。

+
def floatUp(self, i:int):
+    '''上浮操作
+ 
+    对下标为i的元素递归地进行上浮操作
+    直到该元素小于其父节点或该元素上浮到根节点
+    '''
+    # 元素i上浮到根节点时结束递归
+    if i <= 0:
+        return
+    
+    # 当元素i小于其父节点时符合堆结构,结束递归
+    pr = parentOf(i)
+    if self.fCompare(self.A[pr], self.A[i]):
+        return
+    
+    # 元素i大于其父节点,交换i与其父节点并继续上浮
+    self.A[pr], self.A[i] = self.A[i], self.A[pr]
+    self.floatUp(pr)
+

下沉

+

而下沉要稍微比上浮复杂。向下交换时,需要先找出较大的子节点,再跟较大的子节点进行交互。还要考虑左右子节点不存在的情况:当子节点下标超出堆大小时,子节点不存在。

+
def size(self):
+    '''返回堆大小
+    '''
+    return len(self.A)
+ 
+def sinkDown(self, i:int):
+    '''下沉操作
+ 
+    对下标为i的元素递归地进行下沉操作
+    直到该元素大于其两个子节点或该元素下沉到叶子节点
+    '''
+    lc = lfChildOf(i)
+    rc = rtChildOf(i)
+ 
+    # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素
+    larger = i
+    if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):
+        larger = lc
+    if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):
+        larger = rc
+    
+    # 当元素i大于其两个子节点时符合堆结构,结束递归
+    # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归
+    if larger == i:
+        return
+    
+    # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉
+    self.A[larger], self.A[i] = self.A[i], self.A[larger]
+    self.sinkDown(larger)
+

注意这里上浮和下沉操作使用了递归,会占用递归栈空间,因此额外空间复杂度并不是O(1)O(1)

+

但上浮和下沉都可以改为循环迭代实现,迭代实现时额外空间复杂度为O(1)O(1)。要改成迭代实现并不困难,还请大家尝试自己实现。

+

实现各种借口——读、增、删、初始化

+

读取堆顶

+

堆一般只允许读取堆顶,即全堆最大元素。

+
def top(self):
+    '''返回堆顶
+    '''
+    return self.A[0]
+

入堆

+

入堆时,把元素加到堆尾,再做上浮操作。

+
def insert(self, v:T):
+    '''入堆
+    '''
+    # 将元素加到堆尾并做上浮操作
+    self.A.append(v)
+    self.floatUp(len(self.A) - 1)
+

出堆

+

出堆时,取出堆顶,把堆尾元素填到堆顶后,再做下沉操作。

+
def pop(self)->T:
+    '''出堆
+    '''
+    # 取出堆顶元素
+    res = self.A[0]
+ 
+    # 将堆尾元素填到堆顶并做下沉操作
+    self.A[0] = self.A[len(self.A) - 1]
+    self.A.pop()
+    self.sinkDown(0)
+ 
+    return res
+

注意入堆与出堆操作都要保证堆的大小会相应变化。

+

堆初始化

+

堆的初始化采用堆合并策略,从堆尾到堆顶逐个元素做下沉操作。

+
def __init__(self, A:List[T]=[], 
+             fCompare:Callable[[T,T],bool]=lambda a,b:a>b
+             ) -> None:
+    '''堆初始化
+ 
+    :param A: 在数组A上进行初始化
+    :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True
+    '''
+    self.A = A
+    self.fCompare = fCompare
+    for i in reversed(range(len(A))):
+        self.sinkDown(i)
+

整体代码

+

堆的整体实现

+

综上,堆的整体代码实现如下:

+
from typing import Any, Callable, Generic, List, TypeVar
+ 
+T = TypeVar("T")
+ 
+def lfChildOf(i:int):
+    return (i + 1) << 1 - 1
+ 
+def rtChildOf(i:int):
+    return (i + 1) << 1
+ 
+def parentOf(i:int):
+    return (i - 1) >> 1
+ 
+class Heap(Generic[T]):
+    '''堆结构
+ 
+    有两个成员:
+    self.A: List[T] # 堆内元素集合,元素类型为T,储存为数组
+    self.fCompare: Callable[[T,T],bool] # 比较函数
+    
+    下面假设堆为大顶堆
+    即有self.fCompare = lambda a,b: a>b
+    '''
+    def __init__(self, A:List[T]=[], 
+                 fCompare:Callable[[T,T],bool]=lambda a,b:a>b
+                 ) -> None:
+        '''堆初始化
+ 
+        :param A: 在数组A上进行初始化
+        :param fCompare: 比较函数,对堆中节点p与子节点c,有fCompare(p,c)==True
+        '''
+        self.A = A
+        self.fCompare = fCompare
+        for i in reversed(range(len(A))):
+            self.sinkDown(i)
+    
+    def size(self):
+        '''返回堆大小
+        '''
+        return len(self.A)
+    
+    def top(self):
+        '''返回堆顶
+        '''
+        return self.A[0]
+    
+    def sinkDown(self, i:int):
+        '''下沉操作
+ 
+        对下标为i的元素递归地进行下沉操作
+        直到该元素大于其两个子节点或该元素下沉到叶子节点
+        '''
+        lc = lfChildOf(i)
+        rc = rtChildOf(i)
+ 
+        # 比较元素i与其两个子节点,获取三个元素中存在且最大的元素
+        larger = i
+        if lc < self.size() and self.fCompare(self.A[lc], self.A[larger]):
+            larger = lc
+        if rc < self.size() and self.fCompare(self.A[rc], self.A[larger]):
+            larger = rc
+        
+        # 当元素i大于其两个子节点时符合堆结构,结束递归
+        # 当元素i下沉到叶子节点时,左右子节点不存在,也会在此结束递归
+        if larger == i:
+            return
+        
+        # 元素i小于其中一个子节点,交换i与较大子节点并继续下沉
+        self.A[larger], self.A[i] = self.A[i], self.A[larger]
+        self.sinkDown(larger)
+ 
+    def floatUp(self, i:int):
+        '''上浮操作
+ 
+        对下标为i的元素递归地进行上浮操作
+        直到该元素小于其父节点或该元素上浮到根节点
+        '''
+        # 元素i上浮到根节点时结束递归
+        if i <= 0:
+            return
+        
+        # 当元素i小于其父节点时符合堆结构,结束递归
+        pr = parentOf(i)
+        if self.fCompare(self.A[pr], self.A[i]):
+            return
+        
+        # 元素i大于其父节点,交换i与其父节点并继续上浮
+        self.A[pr], self.A[i] = self.A[i], self.A[pr]
+        self.floatUp(pr)
+    
+    def insert(self, v:T):
+        '''入堆
+        '''
+        # 将元素加到堆尾并做上浮操作
+        self.A.append(v)
+        self.floatUp(len(self.A) - 1)
+ 
+    def pop(self)->T:
+        '''出堆
+        '''
+        # 取出堆顶元素
+        res = self.A[0]
+ 
+        # 将堆尾元素填到堆顶并做下沉操作
+        self.A[0] = self.A[len(self.A) - 1]
+        self.A.pop()
+        self.sinkDown(0)
+ 
+        return res
+

单元测试

+

入堆、出堆等操作的简单单元测试如下:

+
import pytest
+import heap
+ 
+@pytest.fixture
+def initHeap():
+    return heap.Heap([1,3,4,7,2,6,5,9,0,8], 
+                     lambda a,b:a>b)
+ 
+class Test_TestHeap:
+    def test_init_notNull(self, initHeap:heap.Heap):
+        assert initHeap.size() == 10
+        assert initHeap.top() == 9
+    
+    def test_insert_notTop(self, initHeap:heap.Heap):
+        initHeap.insert(6)
+        assert initHeap.size() == 11
+        assert initHeap.top() == 9
+    
+    def test_insert_top(self, initHeap:heap.Heap):
+        initHeap.insert(10)
+        assert initHeap.size() == 11
+        assert initHeap.top() == 10
+    
+    def test_pop(self, initHeap:heap.Heap):
+        p = initHeap.pop()
+        assert p == 9
+        assert initHeap.size() == 9
+        assert initHeap.top() == 8
+

关于堆排序

+

算法竞赛中除了原生使用堆结构以外,还有一个使用到堆的地方——堆排序。堆排序有原地排序、最坏时间复杂度为O(nlogn)O(nlogn)等优秀的性质,是比较常用的一个排序算法。

+

然而,手写堆排序要注意的地方与手写堆结构有比较大的不同。堆排序时要注意的点如下:

+
    +
  1. 堆排序时一般要求在给入数组上原地排序,不需要内部维护一个数组结构,反之,需要记录堆结构的大小。
  2. +
  3. 堆结构一般占用数组前端,因此从小到大排序时,有序部分从数组末尾开始扩张,建立的堆为大顶堆。
  4. +
  5. 堆排序只需要建堆与出堆操作,因此只需要实现下沉操作。
  6. +
+

关于堆排序的具体讨论,有机会的话我会另外写一篇来讲解。


Loading comments...
© 2023 Ryo Jerry Yu. All rights reserved.
\ No newline at end of file diff --git a/articles/Sort-algorithm.html b/articles/Sort-algorithm.html index 9eb37240..95203483 100644 --- a/articles/Sort-algorithm.html +++ b/articles/Sort-algorithm.html @@ -4,7 +4,7 @@ 当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同: 在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。"/>

排序算法

序言

+在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。"/>

排序算法

序言

我们知道排序是算法入门基本功,排序算法有多重要想必也不需要我在这里说明了。因此这一篇就按着我的理解,聊一聊排序算法。

当然我不打算随便弄个什么十大排序算法或是经典排序总结之类响当当的名头,各个算法走马看花一样拉出来遛一遍,最后变得跟网上搜索到的其他讲排序的文章一样换汤不换药。你会发现这篇文章的结构跟在网上搜索到的任何讲排序的文章都有所不同:

在这篇文章里,你会发现你找不到冒泡排序——因为我认为冒泡排序只不过是一种低效率的选择排序。

@@ -339,4 +339,4 @@

堆排序逆序对消除方式比较Tricky,但可以看出消除逆序对大致在于出堆步骤,通过O(logn)时间复杂度消除O(n)个逆序对。(左小右大排序时需要建立左大右小的大顶堆,建堆时基本没有消除逆序对)

最后

-

这篇文章我们主要关注了排序算法中的大头——基于比较的排序算法。在下篇文章,我们再来看一下不基于比较的排序算法,以及外排序与并行排序。


Loading comments...
© 2023 Ryo Jerry Yu. All rights reserved.
\ No newline at end of file +

这篇文章我们主要关注了排序算法中的大头——基于比较的排序算法。在下篇文章,我们再来看一下不基于比较的排序算法,以及外排序与并行排序。


Loading comments...
© 2023 Ryo Jerry Yu. All rights reserved.
\ No newline at end of file diff --git a/articles/The-beauty-of-design-parten.html b/articles/The-beauty-of-design-parten.html index bf23d922..dfb0abc7 100644 --- a/articles/The-beauty-of-design-parten.html +++ b/articles/The-beauty-of-design-parten.html @@ -4,7 +4,7 @@ 1. 易维护性:根本 2. 可读性:最重要"/>

设计模式之美读书笔记

导读

+2. 可读性:最重要"/>
Loading comments...
© 2023 Ryo Jerry Yu. All rights reserved.
\ No newline at end of file +

Loading comments...
© 2023 Ryo Jerry Yu. All rights reserved.
\ No newline at end of file diff --git a/articles/create-blog-cicd-by-github.html b/articles/create-blog-cicd-by-github.html index 3a8aa980..f3777697 100644 --- a/articles/create-blog-cicd-by-github.html +++ b/articles/create-blog-cicd-by-github.html @@ -4,7 +4,7 @@ 但我今天要做的不是发布到 GitHub 这么简单,而是要同时发布到 GitHub 和自己的域名下。 我们需要构建一个 CI/CD 过程。这个过程需要做到以下目标:"/>

用 GitHub Action 自动化构建 Hexo 并发布到 S3

GitHub Action 自动化构建发布到 GitHub Pages 大家都见得多了,甚至 Hexo 官方自己都有相关的文档。 +我们需要构建一个 CI/CD 过程。这个过程需要做到以下目标:"/>

用 GitHub Action 自动化构建 Hexo 并发布到 S3

GitHub Action 自动化构建发布到 GitHub Pages 大家都见得多了,甚至 Hexo 官方自己都有相关的文档。 但我今天要做的不是发布到 GitHub 这么简单,而是要同时发布到 GitHub 和自己的域名下。

这篇文章的目标

我们需要构建一个 CI/CD 过程。这个过程需要做到以下目标:

@@ -285,4 +285,4 @@

之后的事

  • Lambda@Edge 还没有结合到 IaC 中
  • 配置文件生成过程仍有改进空间
  • -

    留下这些问题,今后再修改。


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    留下这些问题,今后再修改。


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/articles/graph-for-economics-1.html b/articles/graph-for-economics-1.html index e3c398c3..149ec1ef 100644 --- a/articles/graph-for-economics-1.html +++ b/articles/graph-for-economics-1.html @@ -4,7 +4,7 @@ > 2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。 我们先不讲课,先来带个货。"/>

    图解经济学原理(1)

    +我们先不讲课,先来带个货。"/>

    图解经济学原理(1)

    1. 这篇文章参考了曼昆的《经济学原理》与北京大学王辉老师的《微观经济学》课程,内容上会有部分相似。
    2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。
    3. @@ -108,4 +108,4 @@

      总结一下


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    总结一下


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/articles/graph-for-economics-2.html b/articles/graph-for-economics-2.html index 31d9121c..256e2c3f 100644 --- a/articles/graph-for-economics-2.html +++ b/articles/graph-for-economics-2.html @@ -4,7 +4,7 @@ > 2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。 上一篇讲供给,这一篇讲需求。"/>

    图解经济学原理(2)

    +上一篇讲供给,这一篇讲需求。"/>

    图解经济学原理(2)

    1. 这篇文章参考了曼昆的《经济学原理》与北京大学王辉老师的《微观经济学》课程,内容上会有部分相似。
    2. 这篇文章中的图使用 3Blue1Brown 的动画生成工具 manim 的 Community Edition 制作,源代码之后会上传到 GitHub 。
    3. @@ -133,4 +133,4 @@

      调节经

      财政政策

      货币政策

      两种政策对经济影响 —— 总供给总需求模型

      -

      国际经济


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    国际经济


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/articles/hello-world.html b/articles/hello-world.html index d9cdbdd5..d998f222 100644 --- a/articles/hello-world.html +++ b/articles/hello-world.html @@ -4,7 +4,7 @@ 自己盲人摸象折腾了一两天,终于利用GitHub Pages,把自己的博客搭好了。 感谢[Huxpro][Huxpro]提供的博客模板,以及[BruceZhao][BruceZhao]编写的中文ReadMe。"/>

    Welcome to Ryo's Blog!

    +感谢[Huxpro][Huxpro]提供的博客模板,以及[BruceZhao][BruceZhao]编写的中文ReadMe。"/>
    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/articles/init-a-new-hexo-project.html b/articles/init-a-new-hexo-project.html index 99dfbeb3..4799561e 100644 --- a/articles/init-a-new-hexo-project.html +++ b/articles/init-a-new-hexo-project.html @@ -4,7 +4,7 @@ 对之前的那个博客进行替代,并将之前的文章逐渐搬移过来。 使用的[这个主题](https://github.com/Yue-plus/hexo-theme-arknights)功能还是比较完善的。"/>

    init-a-new-hexo-project

    使用 hexo 搭建博客

    +使用的[这个主题](https://github.com/Yue-plus/hexo-theme-arknights)功能还是比较完善的。"/>
    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/articles/introduction-for-k8s-2.html b/articles/introduction-for-k8s-2.html index e75c0caa..0bc7af75 100644 --- a/articles/introduction-for-k8s-2.html +++ b/articles/introduction-for-k8s-2.html @@ -4,7 +4,7 @@ 其实我们之前已经接触过储存相关的内容了:在讲 Stateful Set 时我们提过 Stateful Set 创建出来的 Pod 都会有相互独立的储存;而讲 Daemon Set 时我们提到 K8s 推荐只在 Daemon Set 的 Pod 中访问宿主机磁盘。但独立的储存具体指什么?除了访问宿主机磁盘以外还有什么其他的储存? 在 Docker 中,我们可以把宿主机磁盘上的一个路径作为一个 Volume 来给容器绑定,或者直接使用 Docker Engine 管理的 Volume 来提供持久化存储或是容器间共享文件。在 K8s 里面也沿用了 Volume 这个概念,可以通过 Mount 绑定到容器内的路径,并通过实现 CSI 的各种引擎来提供更多样的存储。"/>

    Kubernetes 入门 (2)

    我们之前说的都是用于部署 Pod 的资源,我们接下来介绍与创建 Pod 不相关的资源:储存与网络。

    +在 Docker 中,我们可以把宿主机磁盘上的一个路径作为一个 Volume 来给容器绑定,或者直接使用 Docker Engine 管理的 Volume 来提供持久化存储或是容器间共享文件。在 K8s 里面也沿用了 Volume 这个概念,可以通过 Mount 绑定到容器内的路径,并通过实现 CSI 的各种引擎来提供更多样的存储。"/>

    Kubernetes 入门 (2)

    我们之前说的都是用于部署 Pod 的资源,我们接下来介绍与创建 Pod 不相关的资源:储存与网络。

    储存

    其实我们之前已经接触过储存相关的内容了:在讲 Stateful Set 时我们提过 Stateful Set 创建出来的 Pod 都会有相互独立的储存;而讲 Daemon Set 时我们提到 K8s 推荐只在 Daemon Set 的 Pod 中访问宿主机磁盘。但独立的储存具体指什么?除了访问宿主机磁盘以外还有什么其他的储存?

    在 Docker 中,我们可以把宿主机磁盘上的一个路径作为一个 Volume 来给容器绑定,或者直接使用 Docker Engine 管理的 Volume 来提供持久化存储或是容器间共享文件。在 K8s 里面也沿用了 Volume 这个概念,可以通过 Mount 绑定到容器内的路径,并通过实现 CSI 的各种引擎来提供更多样的存储。

    @@ -677,4 +677,4 @@

    各种工

    JOJO: 你到底想说什么?

    DIO: 我不用 kubectl apply 了! JOJO ! (其实还是要用的)

    -


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/articles/introduction-for-k8s.html b/articles/introduction-for-k8s.html index 4fc85ff3..118f89e6 100644 --- a/articles/introduction-for-k8s.html +++ b/articles/introduction-for-k8s.html @@ -4,7 +4,7 @@ > 要把一个不知道打过多少个升级补丁,不知道经历了多少任管理员的系统迁移到其他机器上,毫无疑问会是一场灾难。 —— Chad Fowler 《Trash Your Servers and Burn Your Code》 "Write once, run anywhere" 是 Java 曾经的口号。 Java 企图通过 JVM 虚拟机来实现一个可执行程序在多平台间的移植性。但我们现在知道, Java 语言并没能实现他的目标,会在操作系统调用、第三方依赖丢失、两个程序间依赖的冲突等各方面出现问题。"/>

    Kubernetes 入门 (1)

    容器, Docker 与 K8s

    +"Write once, run anywhere" 是 Java 曾经的口号。 Java 企图通过 JVM 虚拟机来实现一个可执行程序在多平台间的移植性。但我们现在知道, Java 语言并没能实现他的目标,会在操作系统调用、第三方依赖丢失、两个程序间依赖的冲突等各方面出现问题。"/>

    Kubernetes 入门 (1)

    容器, Docker 与 K8s

    我们知道 K8s 利用了容器虚拟化技术。而说到容器虚拟化就要说 Docker 。可是,容器到底是什么? Docker 又为我们做了些什么?我们又为什么要用 K8s ?

    关于容器虚拟化

    @@ -451,4 +451,4 @@

    Job 与 CronJob

    另外我们已经知道 Deployment 等资源一般会通过标签等来管理自己创建的资源,那两份不相关的应用完全有可能会撞标签,这时候部署逻辑就有可能会出问题。

    K8s 中提供了名称空间这种资源,用于进行资源隔离。K8s 中大部分资源都从属于一个且仅从属于一个名称空间, Deployment 等资源一般只能控制在同一名称空间下的资源,而不会影响其他名称空间。

    另外,也有一些资源是名称空间无关的,比如节点 Node

    -

    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/articles/python-dict.html b/articles/python-dict.html index 0996ad25..a40c7722 100644 --- a/articles/python-dict.html +++ b/articles/python-dict.html @@ -4,7 +4,7 @@ 以前参加Python相关的面试时,面试官经常都会问一个问题:Python里的字典(dict)是有序的吗? 这自然难不倒我,我也照本宣科地讲:Python的字典底层是用哈希表实现的,在不发生冲突时读写的时间复杂度是O(1),比读写时间复杂度为O(logn)的红黑树要更快。但红黑树可以按下标的大小顺序进行遍历,而Dict遍历时是无序的。"/>

    Python字典的实现原理

    +这自然难不倒我,我也照本宣科地讲:Python的字典底层是用哈希表实现的,在不发生冲突时读写的时间复杂度是O(1),比读写时间复杂度为O(logn)的红黑树要更快。但红黑树可以按下标的大小顺序进行遍历,而Dict遍历时是无序的。"/>

    Python字典的实现原理

    CPython从3.6开始,字典(dict)不再是无序的了——字典的修改了原先的底层实现,变得能按字典插入的顺序进行遍历。而Python从3.7开始将字典的有序性写入语言特性,不管是Jython、IronPython还是其他Python实现,从3.7开始大家的字典都是有序的了。

    前言

    @@ -90,4 +90,4 @@

    参考文献

  • python3.7源码分析-字典_小屋子大侠的博客-CSDN博客_python 字典源码
  • 《深度剖析CPython解释器》9. 解密Python中字典和集合的底层实现,深度分析哈希表
  • CPython 源码阅读 - dict
  • -

    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/articles/the-using-in-cpp.html b/articles/the-using-in-cpp.html index 21f49b7f..a866b3a5 100644 --- a/articles/the-using-in-cpp.html +++ b/articles/the-using-in-cpp.html @@ -4,7 +4,7 @@ 不引入命名空间时,使用其中变量需要使用`<命名空间名>::<变量名>`的方式使用。 ```C++"/> \ No newline at end of file +

    能做到类似别名功能的,还有宏#define。但#define运行在编译前的宏处理阶段,对代码进行字符串替换。没有类型检查或其他编译、链接阶段才能进行的检查,不具备安全性。在C++11中不提倡使用#define。


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/articles/use-paste-image-and-vscode-memo.html b/articles/use-paste-image-and-vscode-memo.html index c01cab66..34b6fc3e 100644 --- a/articles/use-paste-image-and-vscode-memo.html +++ b/articles/use-paste-image-and-vscode-memo.html @@ -4,7 +4,7 @@ 可能有很多人不熟悉 vscode-memo 这个插件,我先来简单介绍一下。 vscode-memo 定位是一个 knowledge base ,对标的是 [Obsidian.md](https://obsidian.md/) 等软件。其功能包括且不限于:"/>

    完善 Hexo 编写环境,改善文章中使用图片的体验

    我平时使用 vscode-memo 插件写笔记,其中插入图片使用 ![[]] 语法,显示简短,也有较好的预览支持,体验极佳。希望这种特性也能在写 hexo 博客的时候使用。

    +vscode-memo 定位是一个 knowledge base ,对标的是 [Obsidian.md](https://obsidian.md/) 等软件。其功能包括且不限于:"/>

    完善 Hexo 编写环境,改善文章中使用图片的体验

    我平时使用 vscode-memo 插件写笔记,其中插入图片使用 ![[]] 语法,显示简短,也有较好的预览支持,体验极佳。希望这种特性也能在写 hexo 博客的时候使用。

    关于 vscode-memo

    可能有很多人不熟悉 vscode-memo 这个插件,我先来简单介绍一下。

    vscode-memo 定位是一个 knowledge base ,对标的是 Obsidian.md 等软件。其功能包括且不限于:

    @@ -111,4 +111,4 @@

    补充

    └───_posts ├───2022-03-26-create-blog-cicd-by-github.md └───2022-04-03-use-paste-image-and-vscode-memo.md
    -

    可以通过在代码中引用 data.source 解决。


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    可以通过在代码中引用 data.source 解决。


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/articles/why-homogeneous.html b/articles/why-homogeneous.html index 2fdb5748..b1d6a6ed 100644 --- a/articles/why-homogeneous.html +++ b/articles/why-homogeneous.html @@ -4,7 +4,7 @@ 也就是说,对于空间中所有向量 $$\vec{v_1}, \vec{v_2}$$ ,以及任意数量 $$k_1, k_2$$ ,如果有: $$"/>

    为什么使用在齐次坐标下矩阵乘法能表示点平移?

    首先,什么是线性变换?

    +$$"/>

    为什么使用在齐次坐标下矩阵乘法能表示点平移?

    首先,什么是线性变换?

    简化了一万倍来说,线性变换主要是在描述符合这两种性质的变换:一是要可加,二是要能数乘。 也就是说,对于空间中所有向量 v1,v2\vec{v_1}, \vec{v_2}

    Q: 为什么普通的矩阵乘法不能表示平移? A: 因为矩阵乘法只能表示线性变换。平移不是线性变换。

    Q: 为什么在齐次坐标下的矩阵乘法又能表示平移? -A: 因为齐次坐标增加了一个维度。平移变换矩阵其实是在新增的这个维度上做切变(一种线性变换)。切变后的结果正好就是原坐标中的平移变换。


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +A: 因为齐次坐标增加了一个维度。平移变换矩阵其实是在新增的这个维度上做切变(一种线性变换)。切变后的结果正好就是原坐标中的平移变换。


    Loading comments...
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/clips.html b/clips.html index fd8e2847..36fe7578 100644 --- a/clips.html +++ b/clips.html @@ -1 +1 @@ -Clips | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +Clips | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/content/articles/2021-03-21-Handy-heap-cheat-sheet.md b/content/articles/2021-03-21-Handy-heap-cheat-sheet.md index 7da59503..1f68bd7f 100644 --- a/content/articles/2021-03-21-Handy-heap-cheat-sheet.md +++ b/content/articles/2021-03-21-Handy-heap-cheat-sheet.md @@ -1,6 +1,6 @@ --- created_at: 2021-08-28 23:09:14 -updated_at: 2022-03-27 21:30:33+08:00 +updated_at: 2024-04-14 21:30:33+08:00 layout: post title: "如何手撕一个堆" subtitle: "如果哪一天你把编程语言的类库全忘光了,又遇到一题明知到要用堆的题目,咋办?对着一道自己明显会的题干着急,愣是想不起PriorityQueue的名字。这时候只能自己实现一个堆出来了。" @@ -95,11 +95,13 @@ $$T(i) = logi$$ 那么把所有元素上浮,则总时间复杂度为: -$$\begin{aligned} +$$ +\begin{aligned} T(n) &= \sum_{i=1}^{n}logi\\ &= 1\times0 + 2\times1 + ... + 2^{logn}\times{logn} \\ &=O(nlogn) -\end{aligned}$$ +\end{aligned} +$$ 通过把元素逐个入堆来建堆时,元素的时间复杂度可以用下图直观显示: @@ -115,15 +117,19 @@ T(n) &= \sum_{i=1}^{n}logi\\ 下沉第i个元素(从顶到底数)时,以其为顶点的树高度约为$logn-logi$,则有下沉时间复杂度为: -$$T(i) = logn-logi$$ +$$ +T(i) = logn-logi +$$ 那么把所有元素下沉,则总时间复杂度为: -$$\begin{aligned} +$$ +\begin{aligned} T(n) &= \sum_{i=1}^{n}logn-logi \\ &= \frac{n}{2^{logn}}\times{logn}+ ... + \frac{n}{4}\times2+\frac{n}{2}\times1 \\ &= O(n) -\end{aligned}$$ +\end{aligned} +$$ 同样的,我们也可以把逐个元素下沉所耗费的时间用下图来示意: diff --git a/ideas.html b/ideas.html index 447f3150..6447e1b5 100644 --- a/ideas.html +++ b/ideas.html @@ -1 +1 @@ -Ideas | Ryo's Blog \ No newline at end of file +Ideas | Ryo's Blog \ No newline at end of file diff --git a/ideas/blog-in-next.html b/ideas/blog-in-next.html index 4ec5eddb..e1967007 100644 --- a/ideas/blog-in-next.html +++ b/ideas/blog-in-next.html @@ -4,7 +4,7 @@ - [x] remark-math - [x] rehype-katex"/>

    用 Next.js 重构 blog ,TODO list

    blog todo

    + - [x] rehype-katex"/>
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/ideas/blog-syntax.html b/ideas/blog-syntax.html index fd862265..d1be48b4 100644 --- a/ideas/blog-syntax.html +++ b/ideas/blog-syntax.html @@ -4,7 +4,7 @@ *斜体* ***加粗斜体***"/>

    博客语法渲染测试

    一级标题

    +***加粗斜体***"/>
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/ideas/first-idea.html b/ideas/first-idea.html index 8e809b4c..e6c13ffa 100644 --- a/ideas/first-idea.html +++ b/ideas/first-idea.html @@ -1,2 +1,2 @@ -<No Title> | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +<No Title> | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/ideas/newest.html b/ideas/newest.html index 46ea8ed8..2d68bf53 100644 --- a/ideas/newest.html +++ b/ideas/newest.html @@ -4,7 +4,7 @@ 然后这里是第二行。 这里是一些内容。"/>

    Kubernetes 入门 (1)

    这里是第一行, +这里是一些内容。"/>

    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +

    new lines!


    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/ideas/using-chart-js.html b/ideas/using-chart-js.html index 5e549533..52a43bc2 100644 --- a/ideas/using-chart-js.html +++ b/ideas/using-chart-js.html @@ -4,7 +4,7 @@ Introduce: - Chart.js: https://github.com/chartjs/Chart.js"/>

    About Chart.js

    testing for using chart js

    +- Chart.js: https://github.com/chartjs/Chart.js"/>

    About Chart.js

    testing for using chart js

    Use Chart.js in React.

    Introduce:

      @@ -71,4 +71,4 @@

      remark-mdx-chartjs data: [4,2,11,8,6,1,4] backgroundColor: "rgba(53, 162, 235, 0.5)"

    But it's not written in TypeScript, and raising a TS7016 error. -So raise an issue, and wait for the type definition.


    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +So raise an issue, and wait for the type definition.


    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/index.html b/index.html index 604c016a..55250720 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -Ryo's Blog
    Ryo's Blog
    About Tech, Paint, and Games.
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +Ryo's Blog
    Ryo's Blog
    About Tech, Paint, and Games.
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index f71dfa9b..1988528e 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1,52 +1,52 @@ -https://ryojerryyu.github.io/blog-next2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/clips2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/ideas2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/introduction-for-k8s-22024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/introduction-for-k8s2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/why-homogeneous2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/graph-for-economics-22024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/graph-for-economics-12024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/use-paste-image-and-vscode-memo2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/create-blog-cicd-by-github2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/init-a-new-hexo-project2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/Handy-heap-cheat-sheet2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/The-beauty-of-design-parten2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/Sort-algorithm2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/python-dict2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/the-using-in-cpp2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/Building-this-blog2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/articles/hello-world2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/ideas/blog-syntax2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/ideas/using-chart-js2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/ideas/blog-in-next2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/ideas/first-idea2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/ideas/newest2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/kubernetes2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/devops2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/docker2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/cloud-native2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/blog2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/vscode2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/hexo2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/javascript2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/github2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/aws2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/ci-cd2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/iac2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/数据结构2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/算法2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/算法竞赛2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/设计模式2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/笔记2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/排序2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/python2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/c++2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/杂技2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/杂谈2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/nextjs2024-04-14T19:11:53.357Zdaily0.7 -https://ryojerryyu.github.io/blog-next/tags/cloud-computing2024-04-14T19:11:53.357Zdaily0.7 +https://ryojerryyu.github.io/blog-next2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/clips2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/ideas2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/introduction-for-k8s-22024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/introduction-for-k8s2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/why-homogeneous2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/graph-for-economics-22024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/graph-for-economics-12024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/use-paste-image-and-vscode-memo2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/create-blog-cicd-by-github2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/init-a-new-hexo-project2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/Handy-heap-cheat-sheet2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/The-beauty-of-design-parten2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/Sort-algorithm2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/python-dict2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/the-using-in-cpp2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/Building-this-blog2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/articles/hello-world2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/ideas/blog-syntax2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/ideas/using-chart-js2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/ideas/blog-in-next2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/ideas/first-idea2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/ideas/newest2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/kubernetes2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/devops2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/docker2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/cloud-native2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/blog2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/vscode2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/hexo2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/javascript2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/github2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/aws2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/ci-cd2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/iac2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/数据结构2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/算法2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/算法竞赛2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/设计模式2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/笔记2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/排序2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/python2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/c++2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/杂技2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/杂谈2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/nextjs2024-04-14T19:21:28.461Zdaily0.7 +https://ryojerryyu.github.io/blog-next/tags/cloud-computing2024-04-14T19:21:28.461Zdaily0.7 \ No newline at end of file diff --git a/tags.html b/tags.html index 199f5269..7016c889 100644 --- a/tags.html +++ b/tags.html @@ -1 +1 @@ -Tags | Ryo's Blog \ No newline at end of file +Tags | Ryo's Blog \ No newline at end of file diff --git a/tags/aws.html b/tags/aws.html index 654c7e05..c094ba57 100644 --- a/tags/aws.html +++ b/tags/aws.html @@ -1 +1 @@ -AWS | Ryo's Blog \ No newline at end of file +AWS | Ryo's Blog \ No newline at end of file diff --git a/tags/blog.html b/tags/blog.html index 42db650e..5bdaeae4 100644 --- a/tags/blog.html +++ b/tags/blog.html @@ -1 +1 @@ -Blog | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +Blog | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/tags/c++.html b/tags/c++.html index 96087f76..060d7f2b 100644 --- a/tags/c++.html +++ b/tags/c++.html @@ -1 +1 @@ -C++ | Ryo's Blog \ No newline at end of file +C++ | Ryo's Blog \ No newline at end of file diff --git a/tags/ci-cd.html b/tags/ci-cd.html index 3e8853c2..e9932ad1 100644 --- a/tags/ci-cd.html +++ b/tags/ci-cd.html @@ -1 +1 @@ -CI/CD | Ryo's Blog \ No newline at end of file +CI/CD | Ryo's Blog \ No newline at end of file diff --git a/tags/cloud-computing.html b/tags/cloud-computing.html index 4904b397..c6e5c7c5 100644 --- a/tags/cloud-computing.html +++ b/tags/cloud-computing.html @@ -1 +1 @@ -Cloud Computing | Ryo's Blog \ No newline at end of file +Cloud Computing | Ryo's Blog \ No newline at end of file diff --git a/tags/cloud-native.html b/tags/cloud-native.html index 404bcd71..a5ac30ef 100644 --- a/tags/cloud-native.html +++ b/tags/cloud-native.html @@ -1 +1 @@ -Cloud Native | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +Cloud Native | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/tags/devops.html b/tags/devops.html index 16b88d16..a1b42c96 100644 --- a/tags/devops.html +++ b/tags/devops.html @@ -1 +1 @@ -DevOps | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +DevOps | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/tags/docker.html b/tags/docker.html index 83a8ba65..c37aa983 100644 --- a/tags/docker.html +++ b/tags/docker.html @@ -1 +1 @@ -Docker | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +Docker | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/tags/github.html b/tags/github.html index 6d2760f4..7db470a8 100644 --- a/tags/github.html +++ b/tags/github.html @@ -1 +1 @@ -GitHub | Ryo's Blog \ No newline at end of file +GitHub | Ryo's Blog \ No newline at end of file diff --git a/tags/hexo.html b/tags/hexo.html index 87357bd6..f4935e5e 100644 --- a/tags/hexo.html +++ b/tags/hexo.html @@ -1 +1 @@ -Hexo | Ryo's Blog \ No newline at end of file +Hexo | Ryo's Blog \ No newline at end of file diff --git a/tags/iac.html b/tags/iac.html index b385ca2a..0b815bfb 100644 --- a/tags/iac.html +++ b/tags/iac.html @@ -1 +1 @@ -IaC | Ryo's Blog \ No newline at end of file +IaC | Ryo's Blog \ No newline at end of file diff --git a/tags/javascript.html b/tags/javascript.html index b9595419..d6058cae 100644 --- a/tags/javascript.html +++ b/tags/javascript.html @@ -1 +1 @@ -JavaScript | Ryo's Blog \ No newline at end of file +JavaScript | Ryo's Blog \ No newline at end of file diff --git a/tags/kubernetes.html b/tags/kubernetes.html index 31ca277e..0657add6 100644 --- a/tags/kubernetes.html +++ b/tags/kubernetes.html @@ -1 +1 @@ -Kubernetes | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +Kubernetes | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git a/tags/nextjs.html b/tags/nextjs.html index 73c129d8..fc88acb3 100644 --- a/tags/nextjs.html +++ b/tags/nextjs.html @@ -1 +1 @@ -Nextjs | Ryo's Blog \ No newline at end of file +Nextjs | Ryo's Blog \ No newline at end of file diff --git a/tags/python.html b/tags/python.html index 057bc1af..085c259b 100644 --- a/tags/python.html +++ b/tags/python.html @@ -1 +1 @@ -Python | Ryo's Blog \ No newline at end of file +Python | Ryo's Blog \ No newline at end of file diff --git a/tags/vscode.html b/tags/vscode.html index b1b8caa6..864e5f10 100644 --- a/tags/vscode.html +++ b/tags/vscode.html @@ -1 +1 @@ -VSCode | Ryo's Blog \ No newline at end of file +VSCode | Ryo's Blog \ No newline at end of file diff --git "a/tags/\346\216\222\345\272\217.html" "b/tags/\346\216\222\345\272\217.html" index 6564cf4c..c3409ccf 100644 --- "a/tags/\346\216\222\345\272\217.html" +++ "b/tags/\346\216\222\345\272\217.html" @@ -1 +1 @@ -排序 | Ryo's Blog \ No newline at end of file +排序 | Ryo's Blog \ No newline at end of file diff --git "a/tags/\346\225\260\346\215\256\347\273\223\346\236\204.html" "b/tags/\346\225\260\346\215\256\347\273\223\346\236\204.html" index 23804276..f6d7caca 100644 --- "a/tags/\346\225\260\346\215\256\347\273\223\346\236\204.html" +++ "b/tags/\346\225\260\346\215\256\347\273\223\346\236\204.html" @@ -1 +1 @@ -数据结构 | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +数据结构 | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git "a/tags/\346\235\202\346\212\200.html" "b/tags/\346\235\202\346\212\200.html" index 19695bdc..bcb7a201 100644 --- "a/tags/\346\235\202\346\212\200.html" +++ "b/tags/\346\235\202\346\212\200.html" @@ -1 +1 @@ -杂技 | Ryo's Blog \ No newline at end of file +杂技 | Ryo's Blog \ No newline at end of file diff --git "a/tags/\346\235\202\350\260\210.html" "b/tags/\346\235\202\350\260\210.html" index cae20b59..41a856d8 100644 --- "a/tags/\346\235\202\350\260\210.html" +++ "b/tags/\346\235\202\350\260\210.html" @@ -1 +1 @@ -杂谈 | Ryo's Blog \ No newline at end of file +杂谈 | Ryo's Blog \ No newline at end of file diff --git "a/tags/\347\254\224\350\256\260.html" "b/tags/\347\254\224\350\256\260.html" index 75e81931..c097cd88 100644 --- "a/tags/\347\254\224\350\256\260.html" +++ "b/tags/\347\254\224\350\256\260.html" @@ -1 +1 @@ -笔记 | Ryo's Blog \ No newline at end of file +笔记 | Ryo's Blog \ No newline at end of file diff --git "a/tags/\347\256\227\346\263\225.html" "b/tags/\347\256\227\346\263\225.html" index 30cb0a52..e759f880 100644 --- "a/tags/\347\256\227\346\263\225.html" +++ "b/tags/\347\256\227\346\263\225.html" @@ -1 +1 @@ -算法 | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file +算法 | Ryo's Blog
    © 2023 Ryo Jerry Yu. All rights reserved.
    \ No newline at end of file diff --git "a/tags/\347\256\227\346\263\225\347\253\236\350\265\233.html" "b/tags/\347\256\227\346\263\225\347\253\236\350\265\233.html" index 30c2e629..055ad31d 100644 --- "a/tags/\347\256\227\346\263\225\347\253\236\350\265\233.html" +++ "b/tags/\347\256\227\346\263\225\347\253\236\350\265\233.html" @@ -1 +1 @@ -算法竞赛 | Ryo's Blog \ No newline at end of file +算法竞赛 | Ryo's Blog \ No newline at end of file diff --git "a/tags/\350\256\276\350\256\241\346\250\241\345\274\217.html" "b/tags/\350\256\276\350\256\241\346\250\241\345\274\217.html" index e91698ae..995d2337 100644 --- "a/tags/\350\256\276\350\256\241\346\250\241\345\274\217.html" +++ "b/tags/\350\256\276\350\256\241\346\250\241\345\274\217.html" @@ -1 +1 @@ -设计模式 | Ryo's Blog \ No newline at end of file +设计模式 | Ryo's Blog \ No newline at end of file