diff --git a/.gitignore b/.gitignore index 77473d34f..ffb0690b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ # MacOS Desktop Services Store .DS_Store +.cache/ # Images *.png *.jpg *.gif -*.pdf \ No newline at end of file +*.pdf diff --git a/docs/chapter_graph/graph_operations.md b/docs/chapter_graph/graph_operations.md index 44ae93b85..9371ea441 100644 --- a/docs/chapter_graph/graph_operations.md +++ b/docs/chapter_graph/graph_operations.md @@ -15,19 +15,19 @@ comments: true - **删除顶点**:在邻接矩阵中删除一行一列。当删除首行首列时达到最差情况,需要将 $(n-1)^2$ 个元素“向左上移动”,从而使用 $O(n^2)$ 时间。 - **初始化**:传入 $n$ 个顶点,初始化长度为 $n$ 的顶点列表 `vertices` ,使用 $O(n)$ 时间;初始化 $n \times n$ 大小的邻接矩阵 `adjMat` ,使用 $O(n^2)$ 时间。 -=== "初始化邻接矩阵" +=== "<1>" ![邻接矩阵的初始化、增删边、增删顶点](graph_operations.assets/adjacency_matrix_step1_initialization.png){ class="animation-figure" } -=== "添加边" +=== "<2>" ![adjacency_matrix_add_edge](graph_operations.assets/adjacency_matrix_step2_add_edge.png){ class="animation-figure" } -=== "删除边" +=== "<3>" ![adjacency_matrix_remove_edge](graph_operations.assets/adjacency_matrix_step3_remove_edge.png){ class="animation-figure" } -=== "添加顶点" +=== "<4>" ![adjacency_matrix_add_vertex](graph_operations.assets/adjacency_matrix_step4_add_vertex.png){ class="animation-figure" } -=== "删除顶点" +=== "<5>" ![adjacency_matrix_remove_vertex](graph_operations.assets/adjacency_matrix_step5_remove_vertex.png){ class="animation-figure" }

图 9-7   邻接矩阵的初始化、增删边、增删顶点

@@ -1221,19 +1221,19 @@ comments: true - **删除顶点**:需遍历整个邻接表,删除包含指定顶点的所有边,使用 $O(n + m)$ 时间。 - **初始化**:在邻接表中创建 $n$ 个顶点和 $2m$ 条边,使用 $O(n + m)$ 时间。 -=== "初始化邻接表" +=== "<1>" ![邻接表的初始化、增删边、增删顶点](graph_operations.assets/adjacency_list_step1_initialization.png){ class="animation-figure" } -=== "添加边" +=== "<2>" ![adjacency_list_add_edge](graph_operations.assets/adjacency_list_step2_add_edge.png){ class="animation-figure" } -=== "删除边" +=== "<3>" ![adjacency_list_remove_edge](graph_operations.assets/adjacency_list_step3_remove_edge.png){ class="animation-figure" } -=== "添加顶点" +=== "<4>" ![adjacency_list_add_vertex](graph_operations.assets/adjacency_list_step4_add_vertex.png){ class="animation-figure" } -=== "删除顶点" +=== "<5>" ![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_step5_remove_vertex.png){ class="animation-figure" }

图 9-8   邻接表的初始化、增删边、增删顶点

diff --git a/docs/chapter_preface/about_the_book.md b/docs/chapter_preface/about_the_book.md index 6d0f38ca9..0c11c9324 100644 --- a/docs/chapter_preface/about_the_book.md +++ b/docs/chapter_preface/about_the_book.md @@ -36,13 +36,13 @@ comments: true ## 0.1.3   致谢 -本书在开源社区众多贡献者的共同努力下不断完善。感谢每一位投入时间与精力的撰稿人,他们是(按照 GitHub 自动生成的顺序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、IsChristina、xBLACKICEx、guowei-gong、Cathay-Chen、pengchzn、QiLOL、magentaqin、hello-ikun、JoseHung、qualifier1024、thomasq0、sunshinesDL、L-Super、Guanngxu、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、Shyam-Chen、theNefelibatas、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、MolDuM、Nigh、Dr-XYZ、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、beatrix-chan、DullSword、xjr7670、jiaxianhua、qq909244296、iStig、boloboloda、hts0000、gledfish、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、llql1211、fbigm、echo1937、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、GaochaoZhu、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Allen-Scai、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、zhongfq、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Keynman、KeiichiKasai 和 KawaiiAsh。 +本书在开源社区众多贡献者的共同努力下不断完善。感谢每一位投入时间与精力的撰稿人,他们是(按照 GitHub 自动生成的顺序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、pengchzn、QiLOL、Cathay-Chen、guowei-gong、xBLACKICEx、IsChristina、JoseHung、qualifier1024、hello-ikun、magentaqin、Guanngxu、thomasq0、sunshinesDL、L-Super、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、theNefelibatas、Shyam-Chen、sangxiaai、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、Nigh、Dr-XYZ、MolDuM、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、xjr7670、beatrix-chan、DullSword、qq909244296、iStig、boloboloda、hts0000、gledfish、fbigm、echo1937、jiaxianhua、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、JTCPOWI、KawaiiAsh、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、llql1211、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Senrian、Allen-Scai、19santosh99、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、codetypess、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Kunchen-Luo、Keynman 和 KeiichiKasai。 本书的代码审阅工作由 coderonion、curtishd、Gonglja、gvenusleo、hpstory、justin-tse、khoaxuantu、krahets、night-cruise、nuomi1、Reanon 和 rongyi 完成(按照首字母顺序排列)。感谢他们付出的时间与精力,正是他们确保了各语言代码的规范与统一。 -本书的繁体中文版由 Shyam-Chen 和 Dr-XYZ 审阅,英文版由 yuelinxin、K3v123、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn、thomasq0 和 magentaqin 审阅,日文版由 eltociear 审阅。正是因为他们的持续贡献,这本书才能够服务于更广泛的读者群体,感谢他们。 +本书的英文版由 yuelinxin、K3v123、magentaqin、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn 和 thomasq0 审阅;日文版由 eltociear 审阅;俄文版由 И. А. Шевкун 和 Yuyan Huang 审阅;繁体中文版由 Shyam-Chen 和 Dr-XYZ 审阅。正是因为他们的贡献,这本书才能够服务于更广泛的读者群体,感谢他们。 -本书的 ePub 电子书生成工具由 zhongfq 开发。感谢他的贡献,为读者提供了更加自由的阅读方式。 +本书的 ePub 电子书生成工具由 zhongfq 开发。感谢他的贡献,为读者提供了更灵活的阅读方式。 在本书的创作过程中,我得到了许多人的帮助。 @@ -51,7 +51,7 @@ comments: true - 感谢腾宝、琦宝、飞宝为本书起了一个富有创意的名字,唤起大家写下第一行代码“Hello World!”的美好回忆; - 感谢校铨在知识产权方面提供的专业帮助,这对本开源书的完善起到了重要作用; - 感谢苏潼为本书设计了精美的封面和 logo ,并在我的强迫症的驱使下多次耐心修改; -- 感谢 @squidfunk 提供的排版建议,以及他开发的开源文档主题 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 。 +- 感谢 @squidfunk 提供的排版建议,以及他开发的开源文档主题 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material) 。 在写作过程中,我阅读了许多关于数据结构与算法的教材和文章。这些作品为本书提供了优秀的范本,确保了本书内容的准确性与品质。在此感谢所有老师和前辈的杰出贡献! diff --git a/docs/chapter_preface/suggestions.md b/docs/chapter_preface/suggestions.md index cdf44cb29..36a723c4c 100644 --- a/docs/chapter_preface/suggestions.md +++ b/docs/chapter_preface/suggestions.md @@ -141,10 +141,8 @@ comments: true // 内容注释,用于详解代码 - /** - * 多行 - * 注释 - */ + // 多行 + // 注释 ``` === "C" diff --git a/docs/chapter_stack_and_queue/deque.md b/docs/chapter_stack_and_queue/deque.md index 26cd57cfd..d7987eb6f 100644 --- a/docs/chapter_stack_and_queue/deque.md +++ b/docs/chapter_stack_and_queue/deque.md @@ -410,19 +410,19 @@ comments: true 如图 5-8 所示,我们将双向链表的头节点和尾节点视为双向队列的队首和队尾,同时实现在两端添加和删除节点的功能。 -=== "LinkedListDeque" +=== "<1>" ![基于链表实现双向队列的入队出队操作](deque.assets/linkedlist_deque_step1.png){ class="animation-figure" } -=== "push_last()" +=== "<2>" ![linkedlist_deque_push_last](deque.assets/linkedlist_deque_step2_push_last.png){ class="animation-figure" } -=== "push_first()" +=== "<3>" ![linkedlist_deque_push_first](deque.assets/linkedlist_deque_step3_push_first.png){ class="animation-figure" } -=== "pop_last()" +=== "<4>" ![linkedlist_deque_pop_last](deque.assets/linkedlist_deque_step4_pop_last.png){ class="animation-figure" } -=== "pop_first()" +=== "<5>" ![linkedlist_deque_pop_first](deque.assets/linkedlist_deque_step5_pop_first.png){ class="animation-figure" }

图 5-8   基于链表实现双向队列的入队出队操作

@@ -2158,19 +2158,19 @@ comments: true 如图 5-9 所示,与基于数组实现队列类似,我们也可以使用环形数组来实现双向队列。 -=== "ArrayDeque" +=== "<1>" ![基于数组实现双向队列的入队出队操作](deque.assets/array_deque_step1.png){ class="animation-figure" } -=== "push_last()" +=== "<2>" ![array_deque_push_last](deque.assets/array_deque_step2_push_last.png){ class="animation-figure" } -=== "push_first()" +=== "<3>" ![array_deque_push_first](deque.assets/array_deque_step3_push_first.png){ class="animation-figure" } -=== "pop_last()" +=== "<4>" ![array_deque_pop_last](deque.assets/array_deque_step4_pop_last.png){ class="animation-figure" } -=== "pop_first()" +=== "<5>" ![array_deque_pop_first](deque.assets/array_deque_step5_pop_first.png){ class="animation-figure" }

图 5-9   基于数组实现双向队列的入队出队操作

diff --git a/docs/chapter_stack_and_queue/queue.md b/docs/chapter_stack_and_queue/queue.md index 6698b9650..e5638fe0e 100755 --- a/docs/chapter_stack_and_queue/queue.md +++ b/docs/chapter_stack_and_queue/queue.md @@ -379,13 +379,13 @@ comments: true 如图 5-5 所示,我们可以将链表的“头节点”和“尾节点”分别视为“队首”和“队尾”,规定队尾仅可添加节点,队首仅可删除节点。 -=== "LinkedListQueue" +=== "<1>" ![基于链表实现队列的入队出队操作](queue.assets/linkedlist_queue_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![linkedlist_queue_push](queue.assets/linkedlist_queue_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![linkedlist_queue_pop](queue.assets/linkedlist_queue_step3_pop.png){ class="animation-figure" }

图 5-5   基于链表实现队列的入队出队操作

@@ -1329,13 +1329,13 @@ comments: true 可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 $O(1)$ 。 -=== "ArrayQueue" +=== "<1>" ![基于数组实现队列的入队出队操作](queue.assets/array_queue_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![array_queue_push](queue.assets/array_queue_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![array_queue_pop](queue.assets/array_queue_step3_pop.png){ class="animation-figure" }

图 5-6   基于数组实现队列的入队出队操作

@@ -2072,7 +2072,7 @@ comments: true typedef struct { int *nums; // 用于存储队列元素的数组 int front; // 队首指针,指向队首元素 - int queSize; // 尾指针,指向队尾 + 1 + int queSize; // 当前队列的元素数量 int queCapacity; // 队列容量 } ArrayQueue; diff --git a/docs/chapter_stack_and_queue/stack.md b/docs/chapter_stack_and_queue/stack.md index b866b70a3..c1566eda9 100755 --- a/docs/chapter_stack_and_queue/stack.md +++ b/docs/chapter_stack_and_queue/stack.md @@ -376,13 +376,13 @@ comments: true 如图 5-2 所示,对于入栈操作,我们只需将元素插入链表头部,这种节点插入方法被称为“头插法”。而对于出栈操作,只需将头节点从链表中删除即可。 -=== "LinkedListStack" +=== "<1>" ![基于链表实现栈的入栈出栈操作](stack.assets/linkedlist_stack_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![linkedlist_stack_push](stack.assets/linkedlist_stack_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![linkedlist_stack_pop](stack.assets/linkedlist_stack_step3_pop.png){ class="animation-figure" }

图 5-2   基于链表实现栈的入栈出栈操作

@@ -1169,13 +1169,13 @@ comments: true 使用数组实现栈时,我们可以将数组的尾部作为栈顶。如图 5-3 所示,入栈与出栈操作分别对应在数组尾部添加元素与删除元素,时间复杂度都为 $O(1)$ 。 -=== "ArrayStack" +=== "<1>" ![基于数组实现栈的入栈出栈操作](stack.assets/array_stack_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![array_stack_push](stack.assets/array_stack_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![array_stack_pop](stack.assets/array_stack_step3_pop.png){ class="animation-figure" }

图 5-3   基于数组实现栈的入栈出栈操作

diff --git a/docs/index.assets/animation.mp4 b/docs/index.assets/animation.mp4 new file mode 100644 index 000000000..cf3838e79 Binary files /dev/null and b/docs/index.assets/animation.mp4 differ diff --git a/docs/index.assets/comment.mp4 b/docs/index.assets/comment.mp4 new file mode 100644 index 000000000..3f9ea340c Binary files /dev/null and b/docs/index.assets/comment.mp4 differ diff --git a/docs/index.assets/running_code.mp4 b/docs/index.assets/running_code.mp4 new file mode 100644 index 000000000..c946fbd68 Binary files /dev/null and b/docs/index.assets/running_code.mp4 differ diff --git a/docs/index.html b/docs/index.html index 11283b43e..0991626d1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -84,7 +84,7 @@ - 代码仓库 + GitHub @@ -97,7 +97,7 @@ -
+
@@ -147,7 +147,7 @@
-
+

推荐语

@@ -164,7 +164,7 @@
-
+
@@ -179,11 +179,11 @@

"A picture is worth a thousand words."
“一图胜千言”

- Animation example +
- Running code example +
@@ -211,14 +211,14 @@

"Learning by teaching."
“教学相长”

- Comments example +
-
+

鸣谢 @@ -234,7 +234,7 @@

-
+
@@ -251,7 +251,7 @@
-

代码译者

+

代码审阅者

本书多语言代码版本由以下开发者协力完成,感谢他们的投入与贡献!

diff --git a/docs/index.md b/docs/index.md index bcabc4880..818a368dd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,5 @@ --- comments: false -glightbox: false search: exclude: true hide: diff --git a/en/docs/chapter_appendix/contribution.md b/en/docs/chapter_appendix/contribution.md index 339629089..e68c52ed3 100644 --- a/en/docs/chapter_appendix/contribution.md +++ b/en/docs/chapter_appendix/contribution.md @@ -20,7 +20,7 @@ As shown in Figure 16-3, there is an "edit icon" in the top-right corner of each 1. Click the "edit icon". If you encounter a prompt asking you to "Fork this repository", please approve the operation. 2. Modify the content of the Markdown source file, verify the correctness of the content, and maintain consistent formatting as much as possible. -3. Fill in a description of your changes at the bottom of the page, then click the "Propose file change" button. After the page transitions, click the "Create pull request" button to submit your pull request. +3. Fill in a description of your changes at the bottom of the page, then click the "Propose file change" button. After the new page loads, click the "Create pull request" button to submit your pull request. ![Page edit button](contribution.assets/edit_markdown.png){ class="animation-figure" } @@ -33,14 +33,14 @@ Images cannot be directly modified. Please describe the issue by creating a new If you are interested in contributing to this open source project, including translating code into other programming languages or expanding article content, you will need to follow the Pull Request workflow below. 1. Log in to GitHub and Fork the book's [code repository](https://github.com/krahets/hello-algo) to your personal account. -2. Enter your forked repository webpage and use the `git clone` command to clone the repository to your local machine. +2. Go to your forked repository page and use the `git clone` command to clone the repository to your local machine. 3. Create content locally and conduct comprehensive tests to verify code correctness. 4. Commit your local changes and push them to the remote repository. 5. Refresh the repository webpage and click the "Create pull request" button to submit your pull request. ### 3.   Docker Deployment -From the root directory of `hello-algo`, run the following Docker script to access the project at `http://localhost:8000`: +From the root directory of `hello-algo`, run the following Docker command to access the project at `http://localhost:8000`: ```shell docker-compose up -d diff --git a/en/docs/chapter_appendix/index.md b/en/docs/chapter_appendix/index.md index fdc1e490c..e84386b96 100644 --- a/en/docs/chapter_appendix/index.md +++ b/en/docs/chapter_appendix/index.md @@ -11,4 +11,4 @@ icon: material/help-circle-outline - [16.1   Programming Environment Installation](installation.md) - [16.2   Contributing Together](contribution.md) -- [16.3   Terminology Table](terminology.md) +- [16.3   Glossary](terminology.md) diff --git a/en/docs/chapter_appendix/installation.md b/en/docs/chapter_appendix/installation.md index be665333d..079e5988e 100644 --- a/en/docs/chapter_appendix/installation.md +++ b/en/docs/chapter_appendix/installation.md @@ -4,7 +4,7 @@ comments: true # 16.1   Programming Environment Installation -## 16.1.1   Installing Ide +## 16.1.1   Installing IDE We recommend using the open-source and lightweight VS Code as the local integrated development environment (IDE). Visit the [VS Code official website](https://code.visualstudio.com/), and download and install the appropriate version of VS Code according to your operating system. @@ -22,11 +22,11 @@ VS Code has a powerful ecosystem of extensions that supports running and debuggi ### 1.   Python Environment -1. Download and install [Miniconda3](https://docs.conda.io/en/latest/miniconda.html), which requires Python 3.10 or newer. +1. Download and install [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) with Python 3.10 or later. 2. Search for `python` in the VS Code extension marketplace and install the Python Extension Pack. 3. (Optional) Enter `pip install black` on the command line to install the code formatter. -### 2.   C/c++ Environment +### 2.   C/C++ Environment 1. Windows systems need to install [MinGW](https://sourceforge.net/projects/mingw-w64/files/) ([configuration tutorial](https://blog.csdn.net/qq_33698226/article/details/129031241)); macOS comes with Clang built-in and does not require installation. 2. Search for `c++` in the VS Code extension marketplace and install the C/C++ Extension Pack. @@ -34,12 +34,12 @@ VS Code has a powerful ecosystem of extensions that supports running and debuggi ### 3.   Java Environment -1. Download and install [OpenJDK](https://jdk.java.net/18/) (version must be > JDK 9). +1. Download and install [OpenJDK](https://jdk.java.net/18/) (version 10 or later). 2. Search for `java` in the VS Code extension marketplace and install the Extension Pack for Java. ### 4.   C# Environment -1. Download and install [.Net 8.0](https://dotnet.microsoft.com/en-us/download). +1. Download and install [.NET 8.0](https://dotnet.microsoft.com/en-us/download). 2. Search for `C# Dev Kit` in the VS Code extension marketplace and install C# Dev Kit ([configuration tutorial](https://code.visualstudio.com/docs/csharp/get-started)). 3. You can also use Visual Studio ([installation tutorial](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022)). @@ -54,12 +54,12 @@ VS Code has a powerful ecosystem of extensions that supports running and debuggi 1. Download and install [Swift](https://www.swift.org/download/). 2. Search for `swift` in the VS Code extension marketplace and install [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang). -### 7.   Javascript Environment +### 7.   JavaScript Environment 1. Download and install [Node.js](https://nodejs.org/en/). 2. (Optional) Search for `Prettier` in the VS Code extension marketplace and install the code formatter. -### 8.   Typescript Environment +### 8.   TypeScript Environment 1. Follow the same installation steps as the JavaScript environment. 2. Install [TypeScript Execute (tsx)](https://github.com/privatenumber/tsx?tab=readme-ov-file#global-installation). diff --git a/en/docs/chapter_appendix/terminology.md b/en/docs/chapter_appendix/terminology.md index 085304a38..cab433d79 100644 --- a/en/docs/chapter_appendix/terminology.md +++ b/en/docs/chapter_appendix/terminology.md @@ -2,11 +2,11 @@ comments: true --- -# 16.3   Terminology Table +# 16.3   Glossary The following table lists important terms that appear in this book. It is worth noting the following points: -- We recommend remembering the English names of terms to help with reading English literature. +- We recommend remembering the English terms to help with reading English-language literature. - Some terms have different names in Simplified Chinese and Traditional Chinese.

Table 16-1   Important Terms in Data Structures and Algorithms

diff --git a/en/docs/chapter_array_and_linkedlist/array.md b/en/docs/chapter_array_and_linkedlist/array.md index 569536196..6783391fc 100755 --- a/en/docs/chapter_array_and_linkedlist/array.md +++ b/en/docs/chapter_array_and_linkedlist/array.md @@ -14,7 +14,7 @@ An array is a linear data structure that stores elements of the same type ### 1.   Initializing Arrays -We can choose between two array initialization methods based on our needs: without initial values or with given initial values. When no initial values are specified, most programming languages will initialize array elements to $0$: +We can choose between two array initialization methods based on our needs: with or without initial values. When no initial values are specified, most programming languages initialize array elements to $0$: === "Python" @@ -320,13 +320,13 @@ Accessing elements in an array is highly efficient; we can randomly access any e ### 3.   Inserting Elements -Array elements are stored "tightly adjacent" in memory, with no space between them to store any additional data. As shown in Figure 4-3, if we want to insert an element in the middle of an array, we need to shift all elements after that position backward by one position, and then assign the value to that index. +Array elements are packed tightly together in memory, with no extra space between them for additional data. As shown in Figure 4-3, if we want to insert an element in the middle of an array, we need to shift all subsequent elements one position to the right and then assign the value at that index. ![Example of inserting an element into an array](array.assets/array_insert_element.png){ class="animation-figure" }

Figure 4-3   Example of inserting an element into an array

-It is worth noting that since the length of an array is fixed, inserting an element will inevitably cause the element at the end of the array to be "lost". We will leave the solution to this problem for discussion in the "List" chapter. +It is worth noting that since the length of an array is fixed, inserting an element will inevitably push the last element out of the array. We will leave the solution to this problem for discussion in the "List" chapter. === "Python" @@ -517,7 +517,7 @@ Similarly, as shown in Figure 4-4, to delete the element at index $i$, we need t

Figure 4-4   Example of removing an element from an array

-Note that after the deletion is complete, the original last element becomes "meaningless", so we do not need to specifically modify it. +Note that after the deletion is complete, the original last element is no longer meaningful, so we do not need to modify it explicitly. === "Python" @@ -678,7 +678,7 @@ Overall, array insertion and deletion operations have the following drawbacks: - **High time complexity**: The average time complexity for both insertion and deletion in arrays is $O(n)$, where $n$ is the length of the array. - **Loss of elements**: Since the length of an array is immutable, after inserting an element, elements that exceed the array's length will be lost. -- **Memory waste**: We can initialize a relatively long array and only use the front portion, so that when inserting data, the lost elements at the end are "meaningless", but this causes some memory space to be wasted. +- **Memory waste**: We can initialize a relatively long array and use only the front portion, so that any overwritten tail elements are merely unused placeholders, but this wastes some memory space. ### 5.   Traversing Arrays diff --git a/en/docs/chapter_array_and_linkedlist/index.md b/en/docs/chapter_array_and_linkedlist/index.md index 906b5521e..2dbde69e4 100644 --- a/en/docs/chapter_array_and_linkedlist/index.md +++ b/en/docs/chapter_array_and_linkedlist/index.md @@ -3,20 +3,20 @@ comments: true icon: material/view-list-outline --- -# Chapter 4.   Array and Linked List +# Chapter 4.   Arrays and Linked Lists -![Array and Linked List](../assets/covers/chapter_array_and_linkedlist.jpg){ class="cover-image" } +![Arrays and Linked Lists](../assets/covers/chapter_array_and_linkedlist.jpg){ class="cover-image" } !!! abstract The world of data structures is like a solid brick wall. - Array bricks are neatly arranged, tightly packed one by one. Linked list bricks are scattered everywhere, with connecting vines freely weaving through the gaps between bricks. + The bricks of an array are neatly aligned, each pressed tightly against the next. The bricks of a linked list are scattered about, with connecting vines weaving freely through the gaps between them. ## Chapter contents - [4.1   Array](array.md) - [4.2   Linked List](linked_list.md) - [4.3   List](list.md) -- [4.4   Memory and Cache *](ram_and_cache.md) +- [4.4   Random-Access Memory and Cache *](ram_and_cache.md) - [4.5   Summary](summary.md) diff --git a/en/docs/chapter_array_and_linkedlist/linked_list.md b/en/docs/chapter_array_and_linkedlist/linked_list.md index 91283010e..ef8f5d38c 100755 --- a/en/docs/chapter_array_and_linkedlist/linked_list.md +++ b/en/docs/chapter_array_and_linkedlist/linked_list.md @@ -4,11 +4,11 @@ comments: true # 4.2   Linked List -Memory space is a shared resource for all programs. In a complex system runtime environment, available memory space may be scattered throughout the memory. We know that the memory space for storing an array must be contiguous, and when the array is very large, the memory may not be able to provide such a large contiguous space. This is where the flexibility advantage of linked lists becomes apparent. +Memory is a shared resource for all programs. In a complex runtime environment, free memory may be scattered throughout the address space. We know that arrays require contiguous memory, and when an array is very large, the system may not be able to provide such a large contiguous block. This is where the flexibility of linked lists becomes apparent. A linked list is a linear data structure in which each element is a node object, and the nodes are connected through "references". A reference records the memory address of the next node, through which the next node can be accessed from the current node. -The design of linked lists allows nodes to be stored scattered throughout the memory, and their memory addresses do not need to be contiguous. +This design allows linked-list nodes to be stored in different locations in memory, and their addresses do not need to be contiguous. ![Linked list definition and storage method](linked_list.assets/linkedlist_definition.png){ class="animation-figure" } @@ -424,7 +424,7 @@ Building a linked list involves two steps: first, initializing each node object;
-An array is a single variable; for example, an array `nums` contains elements `nums[0]`, `nums[1]`, etc. A linked list, however, is composed of multiple independent node objects. **We typically use the head node as the reference to the linked list**; for example, the linked list in the above code can be referred to as linked list `n0`. +An array is a single variable; for example, an array `nums` contains elements `nums[0]`, `nums[1]`, and so on. A linked list, by contrast, is composed of multiple independent node objects. **We usually use the head node as a stand-in for the entire linked list**; for example, the linked list in the above code can be referred to as linked list `n0`. ### 2.   Inserting a Node @@ -1221,7 +1221,7 @@ Table 4-1 summarizes the characteristics of arrays and linked lists and compares As shown in Figure 4-8, there are three common types of linked lists: -- **Singly linked list**: This is the ordinary linked list introduced earlier. The nodes of a singly linked list contain a value and a reference to the next node. We call the first node the head node and the last node the tail node, which points to null `None`. +- **Singly linked list**: This is the ordinary linked list introduced earlier. The nodes of a singly linked list contain a value and a reference to the next node. We call the first node the head node and the last node the tail node; the tail node points to `None`. - **Circular linked list**: If we make the tail node of a singly linked list point to the head node (connecting the tail to the head), we get a circular linked list. In a circular linked list, any node can be viewed as the head node. - **Doubly linked list**: Compared to a singly linked list, a doubly linked list records references in both directions. The node definition of a doubly linked list includes references to both the successor node (next node) and the predecessor node (previous node). Compared to a singly linked list, a doubly linked list is more flexible and can traverse the linked list in both directions, but it also requires more memory space. diff --git a/en/docs/chapter_array_and_linkedlist/list.md b/en/docs/chapter_array_and_linkedlist/list.md index ac64cbfef..ffb99c4c5 100755 --- a/en/docs/chapter_array_and_linkedlist/list.md +++ b/en/docs/chapter_array_and_linkedlist/list.md @@ -4,22 +4,22 @@ comments: true # 4.3   List -A list is an abstract data structure concept that represents an ordered collection of elements, supporting operations such as element access, modification, insertion, deletion, and traversal, without requiring users to consider capacity limitations. Lists can be implemented based on linked lists or arrays. +A list is an abstract data structure that represents an ordered collection of elements. It supports operations such as element access, modification, insertion, deletion, and traversal, without requiring users to worry about capacity limits. Lists can be implemented using linked lists or arrays. -- A linked list can naturally be viewed as a list, supporting element insertion, deletion, search, and modification operations, and can flexibly expand dynamically. -- An array also supports element insertion, deletion, search, and modification, but since its length is immutable, it can only be viewed as a list with length limitations. +- A linked list can naturally be viewed as a list: it supports insertion, deletion, search, and update, and can grow flexibly as needed. +- An array also supports insertion, deletion, search, and update, but because its length is fixed, it can only be regarded as a list with a capacity limit. -When implementing lists using arrays, **the immutable length property reduces the practicality of the list**. This is because we usually cannot determine in advance how much data we need to store, making it difficult to choose an appropriate list length. If the length is too small, it may fail to meet usage requirements; if the length is too large, it will waste memory space. +When a list is implemented with an array, **its fixed length makes it less practical**. This is because we usually cannot determine in advance how much data we need to store, making it difficult to choose an appropriate capacity. If the capacity is too small, it may fail to meet our needs; if it is too large, memory space will be wasted. -To solve this problem, we can use a dynamic array to implement a list. It inherits all the advantages of arrays and can dynamically expand during program execution. +To solve this problem, we can use a dynamic array to implement a list. It inherits all the advantages of arrays while supporting dynamic resizing during program execution. -In fact, **the lists provided in the standard libraries of many programming languages are implemented based on dynamic arrays**, such as `list` in Python, `ArrayList` in Java, `vector` in C++, and `List` in C#. In the following discussion, we will treat "list" and "dynamic array" as equivalent concepts. +In fact, **the list types provided by the standard libraries of many programming languages are implemented with dynamic arrays**, such as `list` in Python, `ArrayList` in Java, `vector` in C++, and `List` in C#. In the following discussion, we will treat "list" and "dynamic array" as equivalent concepts. ## 4.3.1   Common List Operations ### 1.   Initialize a List -We typically use two initialization methods: "without initial values" and "with initial values": +We typically initialize a list in one of two ways: empty or with predefined values: === "Python" diff --git a/en/docs/chapter_array_and_linkedlist/ram_and_cache.md b/en/docs/chapter_array_and_linkedlist/ram_and_cache.md index 50be50078..3a09c26bc 100644 --- a/en/docs/chapter_array_and_linkedlist/ram_and_cache.md +++ b/en/docs/chapter_array_and_linkedlist/ram_and_cache.md @@ -4,7 +4,7 @@ comments: true # 4.4   Random-Access Memory and Cache * -In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures that represent "contiguous storage" and "distributed storage" as two physical structures, respectively. +In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures that represent two physical layouts: "contiguous storage" and "distributed storage", respectively. In fact, **physical structure largely determines the efficiency with which programs utilize memory and cache**, which in turn affects the overall performance of algorithmic programs. @@ -22,11 +22,11 @@ Computers include three types of storage devices: hard disk, random-ac | Volatility | Data is not lost after power-off | Data is lost after power-off | Data is lost after power-off | | Capacity | Large, on the order of terabytes (TB) | Small, on the order of gigabytes (GB) | Very small, on the order of megabytes (MB) | | Speed | Slow, hundreds to thousands of MB/s | Fast, tens of GB/s | Very fast, tens to hundreds of GB/s | -| Cost (USD/GB) | Inexpensive, fractions of a dollar to a few dollars per GB | Expensive, tens to hundreds of dollars per GB | Very expensive, priced as part of the CPU package | +| Cost (CNY/GB) | Inexpensive, from a few tenths of a yuan to a few yuan per GB | Expensive, from tens to hundreds of yuan per GB | Very expensive, effectively bundled with the CPU package |
-We can imagine the computer storage system as a pyramid structure as shown in the diagram below. Storage devices closer to the top of the pyramid are faster, have smaller capacity, and are more expensive. This multi-layered design is not by accident, but rather the result of careful consideration by computer scientists and engineers. +We can imagine the computer storage system as a pyramid, as shown in the diagram below. Storage devices closer to the top are faster, have smaller capacity, and are more expensive. This multi-layered design is deliberate, the result of careful consideration by computer scientists and engineers. - **Hard disk cannot be easily replaced by RAM**. First, data in memory is lost after power-off, making it unsuitable for long-term data storage. Second, memory is tens of times more expensive than hard disk, which makes it difficult to popularize in the consumer market. - **Cache cannot simultaneously achieve large capacity and high speed**. As the capacity of L1, L2, and L3 caches increases, their physical size becomes larger, and the physical distance between them and the CPU core increases, resulting in longer data transmission time and higher element access latency. With current technology, the multi-layered cache structure represents the best balance point between capacity, speed, and cost. @@ -39,9 +39,9 @@ We can imagine the computer storage system as a pyramid structure as shown in th The storage hierarchy of computers embodies a delicate balance among speed, capacity, and cost. In fact, such trade-offs are common across all industrial fields, requiring us to find the optimal balance point between different advantages and constraints. -In summary, **hard disk is used for long-term storage of large amounts of data, RAM is used for temporary storage of data being processed during program execution, and cache is used for storage of frequently accessed data and instructions**, to improve program execution efficiency. The three work together to ensure efficient operation of the computer system. +In summary, **hard disks are used for long-term storage of large amounts of data, RAM is used to temporarily store the data being processed during program execution, and cache is used to store frequently accessed data and instructions**, thereby improving program execution efficiency. The three work together to keep the computer system running efficiently. -As shown in the diagram below, during program execution, data is read from the hard disk into RAM for CPU computation. Cache can be viewed as part of the CPU, **it intelligently loads data from RAM**, providing the CPU with high-speed data reading, thereby significantly improving program execution efficiency and reducing reliance on slower RAM. +As shown in the diagram below, during program execution, data is read from the hard disk into RAM for CPU computation. Cache can be viewed as part of the CPU. **By intelligently loading data from RAM**, it provides the CPU with high-speed access to data, significantly improving program execution efficiency and reducing reliance on slower RAM. ![Data Flow Among Hard Disk, RAM, and Cache](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } @@ -68,9 +68,9 @@ To achieve the highest efficiency possible, cache employs the following data loa - **Spatial locality**: If a piece of data is accessed, nearby data may also be accessed in the near future. Therefore, when the cache loads a particular piece of data, it also loads nearby data to improve hit rate. - **Temporal locality**: If a piece of data is accessed, it is likely to be accessed again in the near future. Cache leverages this principle by retaining recently accessed data to improve hit rate. -In fact, **arrays and linked lists have different efficiencies in utilizing cache**, manifested in the following aspects. +In fact, **arrays and linked lists differ in how efficiently they utilize cache**, mainly in the following respects. -- **Space occupied**: Linked list elements occupy more space than array elements, resulting in fewer effective data in the cache. +- **Space occupied**: Linked-list elements occupy more space than array elements, so less useful data can fit in the cache. - **Cache lines**: Linked list data are scattered throughout memory, while cache loads "by lines," so the proportion of invalid data loaded is higher. - **Prefetching mechanism**: Arrays have more "predictable" data access patterns than linked lists, making it easier for the system to guess which data will be loaded next. - **Spatial locality**: Arrays are stored in centralized memory space, so data near loaded data is more likely to be accessed soon. diff --git a/en/docs/chapter_array_and_linkedlist/summary.md b/en/docs/chapter_array_and_linkedlist/summary.md index ecdbb6350..1effe3c68 100644 --- a/en/docs/chapter_array_and_linkedlist/summary.md +++ b/en/docs/chapter_array_and_linkedlist/summary.md @@ -6,11 +6,11 @@ comments: true ### 1.   Key Review -- Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous memory storage and scattered memory storage. The characteristics of the two complement each other. +- Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous storage and scattered storage. Their strengths and weaknesses complement each other. - Arrays support random access and use less memory; however, inserting and deleting elements is inefficient, and the length is immutable after initialization. - Linked lists achieve efficient insertion and deletion of nodes by modifying references (pointers), and can flexibly adjust length; however, node access is inefficient and memory consumption is higher. Common linked list types include singly linked lists, circular linked lists, and doubly linked lists. - A list is an ordered collection of elements that supports insertion, deletion, search, and modification, typically implemented based on dynamic arrays. It retains the advantages of arrays while allowing flexible adjustment of length. -- The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space. +- The emergence of lists has greatly improved the practicality of arrays, but it may also waste some memory space. - During program execution, data is primarily stored in memory. Arrays provide higher memory space efficiency, while linked lists offer greater flexibility in memory usage. - Caches provide fast data access to the CPU through mechanisms such as cache lines, prefetching, and spatial and temporal locality, significantly improving program execution efficiency. - Because arrays have higher cache hit rates, they are generally more efficient than linked lists. When choosing a data structure, appropriate selection should be made based on specific requirements and scenarios. @@ -29,17 +29,17 @@ Arrays stored on the stack and on the heap are both stored in contiguous memory Linked lists are composed of nodes, with nodes connected through references (pointers), and each node can store different types of data, such as `int`, `double`, `string`, `object`, etc. -In contrast, array elements must be of the same type, so that the corresponding element position can be obtained by calculating the offset. For example, if an array contains both `int` and `long` types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different "element lengths". +In contrast, array elements must be of the same type so that their positions can be determined by calculating offsets. For example, if an array contains both `int` and `long` types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different "element sizes". ```shell -# Element Memory Address = Array Memory Address (first Element Memory address) + Element Length * Element Index +# element address = array base address (address of the first element) + element size * element index ``` **Q**: After deleting node `P`, do we need to set `P.next` to `None`? It is not necessary to modify `P.next`. From the perspective of the linked list, traversing from the head node to the tail node will no longer encounter `P`. This means that node `P` has been removed from the linked list, and it doesn't matter where node `P` points to at this time—it won't affect the linked list. -From a data structures and algorithms perspective (problem-solving), not disconnecting the pointer doesn't matter as long as the program logic is correct. From the perspective of standard libraries, disconnecting is safer and the logic is clearer. If not disconnected, assuming the deleted node is not properly reclaimed, it may affect the memory reclamation of its successor nodes. +From an algorithms-and-problem-solving perspective, leaving the pointer connected is fine as long as the program logic is correct. From a standard-library implementation perspective, explicitly disconnecting it is safer and clearer. If it is not disconnected and the deleted node is not reclaimed properly, it may affect the reclamation of successor nodes. **Q**: In a linked list, the time complexity of insertion and deletion operations is $O(1)$. However, both insertion and deletion require $O(n)$ time to find the element; why isn't the time complexity $O(n)$? @@ -83,8 +83,8 @@ If we want each `[0]` in the 2D list to be independent, we can use `res = [[0] f **Q**: Does the operation `res = [0] * n` create a list where each integer 0 is independent? -In this list, all integer 0s are references to the same object. This is because Python uses a caching mechanism for small integers (typically -5 to 256) to maximize object reuse and improve performance. +In this list, all the integer zeros reference the same object. This is because Python uses a caching mechanism for small integers (typically -5 to 256) to maximize object reuse and improve performance. -Although they point to the same object, we can still independently modify each element in the list. This is because Python integers are "immutable objects". When we modify an element, we are actually switching to a reference of another object, rather than changing the original object itself. +Although they all reference the same object, we can still modify each element in the list independently. This is because Python integers are "immutable objects". When we modify an element, we actually switch that element to reference a different object, rather than changing the original object itself. However, when list elements are "mutable objects" (such as lists, dictionaries, or class instances), modifying an element directly changes the object itself, and all elements referencing that object will have the same change. diff --git a/en/docs/chapter_backtracking/backtracking_algorithm.md b/en/docs/chapter_backtracking/backtracking_algorithm.md index 522e66bc6..2dff560db 100644 --- a/en/docs/chapter_backtracking/backtracking_algorithm.md +++ b/en/docs/chapter_backtracking/backtracking_algorithm.md @@ -240,7 +240,7 @@ For this problem, we perform a preorder traversal of the tree and check whether **The reason it is called a backtracking algorithm is that it employs "attempt" and "backtrack" strategies when searching the solution space**. When the algorithm encounters a state where it cannot continue forward or cannot find a solution that satisfies the constraints, it will undo the previous choice, return to a previous state, and try other possible choices. -For Example 1, visiting each node represents an "attempt", while skipping over a leaf node or a function `return` from the parent node represents a "backtrack". +For Example 1, visiting each node represents an "attempt", while skipping over a leaf node or the `return` that brings the traversal back to the parent node represents a "backtrack". It is worth noting that **backtracking is not limited to function returns alone**. To illustrate this, let's extend Example 1 slightly. @@ -248,7 +248,7 @@ It is worth noting that **backtracking is not limited to function returns alone* In a binary tree, search all nodes with value $7$, **and return the paths from the root node to these nodes**. -Based on the code from Example 1, we need to use a list `path` to record the visited node path. When we reach a node with value $7$, we copy `path` and add it to the result list `res`. After traversal is complete, `res` contains all the solutions. The code is as follows: +Based on the code from Example 1, we need to use a list `path` to record the path of visited nodes. When we reach a node with value $7$, we copy `path` and add it to the result list `res`. After traversal is complete, `res` contains all the solutions. The code is as follows: === "Python" @@ -895,7 +895,7 @@ To satisfy the above constraints, **we need to add pruning operations**: during ## 13.1.3   Framework Code -Next, we attempt to extract the main framework of backtracking's "attempt, backtrack, and pruning", to improve code generality. +Next, we attempt to extract a general framework centered on backtracking's "attempt, backtrack, and pruning" to improve code generality. In the following framework code, `state` represents the current state of the problem, and `choices` represents the choices available in the current state: @@ -1915,7 +1915,7 @@ As per the problem statement, we should continue searching after finding a node

Figure 13-4   Comparison of search process with and without return statement

-Compared to code based on preorder traversal, code based on the backtracking algorithm framework appears more verbose, but has better generality. In fact, **many backtracking problems can be solved within this framework**. We only need to define `state` and `choices` for the specific problem and implement each method in the framework. +Compared to code based on preorder traversal, code based on the backtracking algorithm framework appears more verbose, but is more general. In fact, **many backtracking problems can be solved within this framework**. We only need to define `state` and `choices` for the specific problem and implement each method in the framework. ## 13.1.4   Common Terminology @@ -1938,7 +1938,7 @@ To analyze algorithmic problems more clearly, we summarize the meanings of commo !!! tip - The concepts of problem, solution, state, etc. are universal and are involved in divide-and-conquer, backtracking, dynamic programming, greedy and other algorithms. + The concepts of problem, solution, state, etc. are universal and appear in divide-and-conquer, backtracking, dynamic programming, greedy algorithms, and others. ## 13.1.5   Advantages and Limitations @@ -1946,7 +1946,7 @@ The backtracking algorithm is essentially a depth-first search algorithm that tr However, when dealing with large-scale or complex problems, **the running efficiency of the backtracking algorithm may be unacceptable**. -- **Time**: The backtracking algorithm usually needs to traverse all possibilities in the solution space, and the time complexity can reach exponential or factorial order. +- **Time**: The backtracking algorithm usually needs to traverse all possibilities in the state space, and the time complexity can reach exponential or factorial order. - **Space**: During recursive calls, the current state needs to be saved (such as paths, auxiliary variables used for pruning, etc.), and when the depth is large, the space requirement can become very large. Nevertheless, **the backtracking algorithm is still the best solution for certain search problems and constraint satisfaction problems**. For these problems, since we cannot predict which choices will generate valid solutions, we must traverse all possible choices. In this case, **the key is how to optimize efficiency**. There are two common efficiency optimization methods. diff --git a/en/docs/chapter_backtracking/n_queens_problem.md b/en/docs/chapter_backtracking/n_queens_problem.md index 75b12ef73..834008d4a 100644 --- a/en/docs/chapter_backtracking/n_queens_problem.md +++ b/en/docs/chapter_backtracking/n_queens_problem.md @@ -6,7 +6,7 @@ comments: true !!! question - According to the rules of chess, a queen can attack pieces that share the same row, column, or diagonal line. Given $n$ queens and an $n \times n$ chessboard, find a placement scheme such that no two queens can attack each other. + According to the rules of chess, a queen can attack any piece in the same row, column, or diagonal. Given $n$ queens and an $n \times n$ chessboard, find an arrangement such that no two queens can attack each other. As shown in Figure 13-15, when $n = 4$, there are two solutions that can be found. From the perspective of the backtracking algorithm, an $n \times n$ chessboard has $n^2$ squares, which provide all the choices `choices`. During the process of placing queens one by one, the chessboard state changes continuously, and the chessboard at each moment represents the state `state`. @@ -22,11 +22,11 @@ Figure 13-16 illustrates the three constraints of this problem: **multiple queen ### 1.   Row-By-Row Placement Strategy -Since both the number of queens and the number of rows on the chessboard are $n$, we can easily derive a conclusion: **each row of the chessboard allows and only allows exactly one queen to be placed**. +Since both the number of queens and the number of rows on the chessboard are $n$, we can easily derive a conclusion: **each row of the chessboard allows one and only one queen to be placed**. This means we can adopt a row-by-row placement strategy: starting from the first row, place one queen in each row until the last row is completed. -Figure 13-17 shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that do not satisfy the column constraint and diagonal constraints are pruned. +Figure 13-17 shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that violate the column or diagonal constraints are pruned. ![Row-by-row placement strategy](n_queens_problem.assets/n_queens_placing.png){ class="animation-figure" } @@ -54,7 +54,7 @@ Similarly, **for all squares on an anti-diagonal, the sum $row + col$ is a const ### 3.   Code Implementation -Please note that in an $n$-dimensional square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Therefore, the number of both main diagonals and anti-diagonals is $2n - 1$, meaning the length of both arrays `diags1` and `diags2` is $2n - 1$. +Please note that in an $n \times n$ square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Therefore, the number of both main diagonals and anti-diagonals is $2n - 1$, meaning the length of both arrays `diags1` and `diags2` is $2n - 1$. === "Python" diff --git a/en/docs/chapter_backtracking/permutations_problem.md b/en/docs/chapter_backtracking/permutations_problem.md index f594c84ec..a581bda3c 100644 --- a/en/docs/chapter_backtracking/permutations_problem.md +++ b/en/docs/chapter_backtracking/permutations_problem.md @@ -40,7 +40,7 @@ As shown in Figure 13-5, we can unfold the search process into a recursion tree, To ensure that each element is chosen only once, we consider introducing a boolean array `selected`, where `selected[i]` indicates whether `choices[i]` has been chosen. We implement the following pruning operation based on it. -- After making a choice `choice[i]`, we set `selected[i]` to $\text{True}$, indicating that it has been chosen. +- After making a choice `choices[i]`, we set `selected[i]` to $\text{True}$, indicating that it has been chosen. - When traversing the candidate list `choices`, we skip all nodes that have been chosen, which is pruning. As shown in Figure 13-6, suppose we choose $1$ in the first round, $3$ in the second round, and $2$ in the third round. Then we need to prune the branch of element $1$ in the second round and prune the branches of elements $1$ and $3$ in the third round. @@ -546,7 +546,7 @@ After understanding the above information, we can fill in the blanks in the temp Suppose the input array is $[1, 1, 2]$. To distinguish the two duplicate elements $1$, we denote the second $1$ as $\hat{1}$. -As shown in Figure 13-7, the method described above generates permutations where half are duplicates. +As shown in Figure 13-7, half of the permutations generated by the above method are duplicates. ![Duplicate permutations](permutations_problem.assets/permutations_ii.png){ class="animation-figure" } @@ -554,7 +554,7 @@ As shown in Figure 13-7, the method described above generates permutations where So how do we remove duplicate permutations? The most direct approach is to use a hash set to directly deduplicate the permutation results. However, this is not elegant because **the search branches that generate duplicate permutations are unnecessary and should be identified and pruned early**, which can further improve algorithm efficiency. -### 1.   Pruning Duplicate Elements +### 1.   Pruning Equal Elements Observe Figure 13-8. In the first round, choosing $1$ or choosing $\hat{1}$ is equivalent. All permutations generated under these two choices are duplicates. Therefore, we should prune $\hat{1}$. @@ -568,7 +568,7 @@ Essentially, **our goal is to ensure that multiple equal elements are chosen onl ### 2.   Code Implementation -Building on the code from the previous problem, we consider opening a hash set `duplicated` in each round of choices to record which elements have been tried in this round, and prune duplicate elements: +Building on the code from the previous problem, we initialize a hash set `duplicated` in each round of choices to record which elements have already been tried in that round, and prune equal elements: === "Python" @@ -1089,7 +1089,7 @@ The maximum recursion depth is $n$, using $O(n)$ stack frame space. `selected` u Note that although both `selected` and `duplicated` are used for pruning, they have different objectives. - **Pruning duplicate choices**: There is only one `selected` throughout the entire search process. It records which elements are included in the current state, and its purpose is to prevent an element from appearing repeatedly in `state`. -- **Pruning duplicate elements**: Each round of choices (each `backtrack` function call) contains a `duplicated` set. It records which elements have been chosen in this round's iteration (the `for` loop), and its purpose is to ensure that equal elements are chosen only once. +- **Pruning equal elements**: Each round of choices (each `backtrack` function call) contains a `duplicated` set. It records which elements have been chosen in this round's iteration (the `for` loop), and its purpose is to ensure that equal elements are chosen only once. Figure 13-9 shows the effective scope of the two pruning conditions. Note that each node in the tree represents a choice, and the nodes on the path from the root to a leaf node form a permutation. diff --git a/en/docs/chapter_backtracking/subset_sum_problem.md b/en/docs/chapter_backtracking/subset_sum_problem.md index 4fe4bf7ac..a0b348fd0 100644 --- a/en/docs/chapter_backtracking/subset_sum_problem.md +++ b/en/docs/chapter_backtracking/subset_sum_problem.md @@ -15,11 +15,11 @@ For example, given the set $\{3, 4, 5\}$ and target integer $9$, the solutions a - Elements in the input set can be selected repeatedly without limit. - Subsets do not distinguish element order; for example, $\{4, 5\}$ and $\{5, 4\}$ are the same subset. -### 1.   Reference to Full Permutation Solution +### 1.   Using the Permutation Solution as a Reference -Similar to the full permutation problem, we can imagine the process of generating subsets as a series of choices, and update the "sum of elements" in real-time during the selection process. When the sum equals `target`, we record the subset to the result list. +Similar to the permutation problem, we can view the process of generating subsets as the result of a series of choices and update the running sum during the selection process. When the sum equals `target`, we record the subset in the result list. -Unlike the full permutation problem, **elements in this problem's set can be selected unlimited times**, so we do not need to use a `selected` boolean list to track whether an element has been selected. We can make minor modifications to the full permutation code and initially obtain the solution: +Unlike the permutation problem, **elements in this problem can be selected any number of times**, so we do not need to use a `selected` boolean list to track whether an element has already been selected. With a few small changes to the permutation code, we obtain an initial solution: === "Python" @@ -501,7 +501,7 @@ Unlike the full permutation problem, **elements in this problem's set can be sel end ``` -When we input array $[3, 4, 5]$ and target element $9$ to the above code, the output is $[3, 3, 3], [4, 5], [5, 4]$. **Although we successfully find all subsets that sum to $9$, there are duplicate subsets $[4, 5]$ and $[5, 4]$**. +Running the above code on array $[3, 4, 5]$ with target value $9$ produces $[3, 3, 3], [4, 5], [5, 4]$. **Although we successfully found all subsets that sum to $9$, there are duplicate subsets $[4, 5]$ and $[5, 4]$**. This is because the search process distinguishes the order of selections, but subsets do not distinguish selection order. As shown in Figure 13-10, selecting 4 first and then 5 versus selecting 5 first and then 4 are different branches, but they correspond to the same subset. @@ -519,13 +519,13 @@ To eliminate duplicate subsets, **one straightforward idea is to deduplicate the **We consider deduplication through pruning during the search process**. Observing Figure 13-11, duplicate subsets occur when array elements are selected in different orders, as in the following cases: 1. When the first and second rounds select $3$ and $4$ respectively, all subsets containing these two elements are generated, denoted as $[3, 4, \dots]$. -2. Afterward, when the first round selects $4$, **the second round should skip $3$**, because the subset $[4, 3, \dots]$ generated by this choice is completely duplicate with the subset generated in step `1.` +2. Afterward, when the first round selects $4$, **the second round should skip $3$**, because the subset $[4, 3, \dots]$ generated by this choice is an exact duplicate of the subset generated in step `1.` In the search process, each level's choices are tried from left to right, so the rightmost branches are pruned more. 1. The first two rounds select $3$ and $5$, generating subset $[3, 5, \dots]$. 2. The first two rounds select $4$ and $5$, generating subset $[4, 5, \dots]$. -3. If the first round selects $5$, **the second round should skip $3$ and $4$**, because subsets $[5, 3, \dots]$ and $[5, 4, \dots]$ are completely duplicate with the subsets described in steps `1.` and `2.` +3. If the first round selects $5$, **the second round should skip $3$ and $4$**, because subsets $[5, 3, \dots]$ and $[5, 4, \dots]$ are exact duplicates of the subsets described in steps `1.` and `2.` ![Different selection orders leading to duplicate subsets](subset_sum_problem.assets/subset_sum_i_pruning.png){ class="animation-figure" } @@ -1057,7 +1057,7 @@ In addition, we have made the following two optimizations to the code: end ``` -Figure 13-12 shows the complete backtracking process when array $[3, 4, 5]$ and target element $9$ are input to the above code. +Figure 13-12 shows the complete backtracking process produced by running the above code on array $[3, 4, 5]$ with target value $9$. ![Subset-sum I backtracking process](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" } @@ -1069,7 +1069,7 @@ Figure 13-12 shows the complete backtracking process when array $[3, 4, 5]$ and Given a positive integer array `nums` and a target positive integer `target`, find all possible combinations where the sum of elements in the combination equals `target`. **The given array may contain duplicate elements, and each element can be selected at most once**. Return these combinations in list form, where the list should not contain duplicate combinations. -Compared to the previous problem, **the input array in this problem may contain duplicate elements**, which introduces new challenges. For example, given array $[4, \hat{4}, 5]$ and target element $9$, the output of the existing code is $[4, 5], [\hat{4}, 5]$, which contains duplicate subsets. +Compared to the previous problem, **the input array in this problem may contain duplicate elements**, which introduces a new issue. For example, given array $[4, \hat{4}, 5]$ and target value $9$, the output of the existing code is $[4, 5], [\hat{4}, 5]$, which contains duplicate subsets. **The reason for this duplication is that equal elements are selected multiple times in a certain round**. In Figure 13-13, the first round has three choices, two of which are $4$, creating two duplicate search branches that output duplicate subsets. Similarly, the two $4$'s in the second round also produce duplicate subsets. @@ -1079,7 +1079,7 @@ Compared to the previous problem, **the input array in this problem may contain ### 1.   Pruning Equal Elements -To solve this problem, **we need to limit equal elements to be selected only once in each round**. The implementation is quite clever: since the array is already sorted, equal elements are adjacent. This means that in a certain round of selection, if the current element equals the element to its left, it means this element has already been selected, so we skip the current element directly. +To solve this problem, **we need to limit equal elements to be selected only once in each round**. The implementation is quite clever: since the array is already sorted, equal elements are adjacent. This means that in a given round of selection, if the current element equals the element to its left, then the same value has already been chosen in this round, so we skip the current element directly. At the same time, **this problem specifies that each array element can only be selected once**. Fortunately, we can also use the variable `start` to satisfy this constraint: after making choice $x_{i}$, set the next round to start traversal from index $i + 1$ onwards. This both eliminates duplicate subsets and avoids selecting elements multiple times. @@ -1663,7 +1663,7 @@ At the same time, **this problem specifies that each array element can only be s end ``` -Figure 13-14 shows the backtracking process for array $[4, 4, 5]$ and target element $9$, which includes four types of pruning operations. Combine the illustration with the code comments to understand the entire search process and how each pruning operation works. +Figure 13-14 shows the backtracking process for array $[4, 4, 5]$ with target value $9$, which includes four types of pruning operations. Combine the illustration with the code comments to understand the entire search process and how each pruning operation works. ![Subset-sum II backtracking process](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" } diff --git a/en/docs/chapter_backtracking/summary.md b/en/docs/chapter_backtracking/summary.md index 738d765ff..542f95f5c 100644 --- a/en/docs/chapter_backtracking/summary.md +++ b/en/docs/chapter_backtracking/summary.md @@ -13,15 +13,15 @@ comments: true - The permutation problem aims to find all possible permutations of elements in a given set. We use an array to record whether each element has been selected, thereby pruning search branches that attempt to select the same element repeatedly, ensuring each element is selected exactly once. - In the permutation problem, if the set contains duplicate elements, the final result will contain duplicate permutations. We need to impose a constraint so that equal elements can only be selected once per round, which is typically achieved using a hash set. - The subset-sum problem aims to find all subsets of a given set that sum to a target value. Since the set is unordered but the search process outputs results in all orders, duplicate subsets are generated. We sort the data before backtracking and use a variable to indicate the starting point of each round's traversal, thereby pruning search branches that generate duplicate subsets. -- For the subset-sum problem, equal elements in the array produce duplicate sets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round. +- For the subset-sum problem, equal elements in the array produce duplicate subsets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round. - The $n$ queens problem aims to find placements of $n$ queens on an $n \times n$ chessboard such that no two queens can attack each other. The constraints of this problem include row constraints, column constraints, and main and anti-diagonal constraints. To satisfy row constraints, we adopt a row-by-row placement strategy, ensuring exactly one queen is placed in each row. - The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether each column has a queen, thereby indicating whether a selected cell is valid. For diagonal constraints, we use two arrays to separately record whether queens exist on each main or anti-diagonal. The challenge lies in finding the row-column index pattern that characterizes cells on the same main (anti-)diagonal. ### 2.   Q & A -**Q**: How should we understand the relationship between backtracking and recursion? +**Q**: How can we understand the relationship between backtracking and recursion? -Overall, backtracking is an "algorithm strategy", while recursion is more like a "tool". +Overall, backtracking is an algorithmic strategy, while recursion is better viewed as a tool. -- The backtracking algorithm is typically implemented based on recursion. However, backtracking is one application scenario of recursion and represents the application of recursion in search problems. -- The structure of recursion embodies the "subproblem decomposition" problem-solving paradigm, commonly used to solve problems involving divide-and-conquer, backtracking, and dynamic programming (memoized recursion). +- Backtracking is typically implemented with recursion. However, backtracking is only one application of recursion, specifically its use in search problems. +- The structure of recursion reflects a problem-solving paradigm based on decomposing a problem into subproblems, and it is commonly used in divide-and-conquer, backtracking, and dynamic programming (memoized recursion). diff --git a/en/docs/chapter_computational_complexity/iteration_and_recursion.md b/en/docs/chapter_computational_complexity/iteration_and_recursion.md index 4424aa8ca..3c2a708d4 100644 --- a/en/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/en/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -14,7 +14,7 @@ In algorithms, repeatedly executing a task is very common and closely related to The `for` loop is one of the most common forms of iteration, **suitable for use when the number of iterations is known in advance**. -The following function implements the summation $1 + 2 + \dots + n$ based on a `for` loop, with the sum result recorded using the variable `res`. Note that in Python, `range(a, b)` corresponds to a "left-closed, right-open" interval, with the traversal range being $a, a + 1, \dots, b-1$: +The following function implements the summation $1 + 2 + \dots + n$ using a `for` loop, with the result stored in the variable `res`. Note that in Python, `range(a, b)` corresponds to a "left-closed, right-open" interval, with the traversal range being $a, a + 1, \dots, b-1$: === "Python" @@ -901,7 +901,7 @@ Figure 2-2 shows the flowchart of this nested loop. In this case, the number of operations of the function is proportional to $n^2$, or the algorithm's running time has a "quadratic relationship" with the input data size $n$. -We can continue adding nested loops, where each nesting is a "dimension increase", raising the time complexity to "cubic relationship", "quartic relationship", and so on. +We can continue adding nested loops, where each additional level of nesting can be viewed as an increase in dimensionality, raising the time complexity to a "cubic relationship", a "quartic relationship", and so on. ## 2.2.2   Recursion @@ -1129,7 +1129,7 @@ Taking the above summation function as an example, let the problem be $f(n) = 1 ### 1.   Call Stack -Each time a recursive function calls itself, the system allocates memory for the newly opened function to store local variables, call addresses, and other information. This leads to two consequences. +Each time a recursive function calls itself, the system allocates memory for the newly invoked function to store local variables, call addresses, and other information. This leads to two consequences. - The function's context data is stored in a memory area called "stack frame space", which is not released until the function returns. Therefore, **recursion usually consumes more memory space than iteration**. - Recursive function calls incur additional overhead. **Therefore, recursion is usually less time-efficient than loops**. @@ -1144,7 +1144,7 @@ In practice, the recursion depth allowed by programming languages is usually lim ### 2.   Tail Recursion -Interestingly, **if a function makes the recursive call as the very last step before returning**, the function can be optimized by the compiler or interpreter to have space efficiency comparable to iteration. This case is called tail recursion. +Interestingly, **if a function makes the recursive call as the very last step before returning**, the compiler or interpreter may optimize it so that its space efficiency is comparable to iteration. This case is called tail recursion. - **Regular recursion**: When a function returns to the previous level, it needs to continue executing code, so the system needs to save the context of the previous layer's call. - **Tail recursion**: The recursive call is the last operation before the function returns, meaning that after returning to the previous level, there is no need to continue executing other operations, so the system does not need to save the context of the previous layer's function. @@ -1319,7 +1319,7 @@ Taking the calculation of $1 + 2 + \dots + n$ as an example, we can set the resu end ``` -The execution process of tail recursion is shown in Figure 2-5. Comparing regular recursion and tail recursion, the execution point of the summation operation is different. +The execution process of tail recursion is shown in Figure 2-5. Comparing regular recursion and tail recursion, the summation operation is performed at different points. - **Regular recursion**: The summation operation is performed during the "ascending" process, requiring an additional summation operation after each layer returns. - **Tail recursion**: The summation operation is performed during the "descending" process; the "ascending" process only needs to return layer by layer. @@ -1540,7 +1540,7 @@ Following the recurrence relation to make recursive calls, with the first two nu end ``` -Observing the above code, we recursively call two functions within the function, **meaning that one call produces two call branches**. As shown in Figure 2-6, such continuous recursive calling will eventually produce a recursion tree with $n$ levels. +Observing the above code, we make two recursive calls within the function, **meaning that one call produces two call branches**. As shown in Figure 2-6, this repeated recursive calling eventually produces a recursion tree with $n$ levels. ![Recursion tree of the Fibonacci sequence](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" } diff --git a/en/docs/chapter_computational_complexity/performance_evaluation.md b/en/docs/chapter_computational_complexity/performance_evaluation.md index 9f612b366..b6dbb4d88 100644 --- a/en/docs/chapter_computational_complexity/performance_evaluation.md +++ b/en/docs/chapter_computational_complexity/performance_evaluation.md @@ -20,17 +20,17 @@ Efficiency evaluation methods are mainly divided into two types: actual testing ## 2.1.1   Actual Testing -Suppose we now have algorithm `A` and algorithm `B`, both of which can solve the same problem, and we need to compare the efficiency of these two algorithms. The most direct method is to find a computer, run these two algorithms, and monitor and record their running time and memory usage. This evaluation approach can reflect the real situation, but it also has considerable limitations. +Suppose we now have algorithm `A` and algorithm `B`, both of which can solve the same problem, and we need to compare their efficiency. The most direct method is to run them on a computer and measure their running time and memory usage. This evaluation approach can reflect real-world behavior, but it also has considerable limitations. -On one hand, **it is difficult to eliminate interference factors from the testing environment**. Hardware configuration affects the performance of algorithms. For example, if an algorithm has a high degree of parallelism, it is more suitable for running on multi-core CPUs; if an algorithm has intensive memory operations, it will perform better on high-performance memory. In other words, the test results of an algorithm on different machines may be inconsistent. This means we need to test on various machines and calculate average efficiency, which is impractical. +On one hand, **it is difficult to eliminate interference factors from the testing environment**. Hardware configuration affects algorithmic performance. For example, if an algorithm has a high degree of parallelism, it is more suitable for running on multi-core CPUs; if an algorithm performs memory-intensive operations, it will benefit more from high-performance memory. In other words, the test results of an algorithm on different machines may be inconsistent. This means we need to test on various machines and calculate average efficiency, which is impractical. On the other hand, **conducting complete testing is very resource-intensive**. As the input data volume changes, the algorithm will exhibit different efficiencies. For example, when the input data volume is small, the running time of algorithm `A` is shorter than algorithm `B`; but when the input data volume is large, the test results may be exactly the opposite. Therefore, to obtain convincing conclusions, we need to test input data of various scales, which requires a large amount of computational resources. ## 2.1.2   Theoretical Estimation -Since actual testing has considerable limitations, we can consider evaluating algorithm efficiency through calculations alone. This estimation method is called asymptotic complexity analysis, or complexity analysis for short. +Since actual testing has considerable limitations, we can consider evaluating algorithm efficiency through theoretical calculation. This estimation method is called asymptotic complexity analysis, or complexity analysis for short. -Complexity analysis can reflect the relationship between the time and space resources required for algorithm execution and the input data scale. **It describes the growth trend of the time and space required for algorithm execution as the input data scale increases**. This definition is somewhat convoluted, so we can break it down into three key points to understand. +Complexity analysis can reflect the relationship between the time and space resources required for algorithm execution and the input data scale. **It describes the growth trend of the time and space required for algorithm execution as the input data scale increases**. This definition is a bit cumbersome, so we can break it down into three key points to understand. - "Time and space resources" correspond to time complexity and space complexity, respectively. - "As the input data scale increases" means that complexity reflects the relationship between algorithm running efficiency and input data scale. @@ -48,6 +48,6 @@ Complexity analysis can reflect the relationship between the time and space reso Complexity analysis provides us with a "ruler" for evaluating algorithm efficiency, allowing us to measure the time and space resources required to execute a certain algorithm and compare the efficiency between different algorithms. -Complexity is a mathematical concept that may be relatively abstract for beginners, with a relatively high learning difficulty. From this perspective, complexity analysis may not be very suitable as the first content to be introduced. However, when we discuss the characteristics of a certain data structure or algorithm, it is difficult to avoid analyzing its running speed and space usage. +Complexity is a mathematical concept that may feel abstract and challenging for beginners. From this perspective, complexity analysis may not be the most suitable topic to introduce first. However, when we discuss the characteristics of a certain data structure or algorithm, it is difficult to avoid analyzing its running speed and space usage. -In summary, it is recommended that before diving deep into data structures and algorithms, **you first establish a preliminary understanding of complexity analysis so that you can complete complexity analysis of simple algorithms**. +In summary, it is recommended that before diving deep into data structures and algorithms, **you first establish a preliminary understanding of complexity analysis so that you can analyze the complexity of simple algorithms**. diff --git a/en/docs/chapter_computational_complexity/space_complexity.md b/en/docs/chapter_computational_complexity/space_complexity.md index 7a79c445e..b9ec99bac 100644 --- a/en/docs/chapter_computational_complexity/space_complexity.md +++ b/en/docs/chapter_computational_complexity/space_complexity.md @@ -22,7 +22,7 @@ Temporary space can be further divided into three parts. - **Stack frame space**: Used to save the context data of called functions. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns. - **Instruction space**: Used to save compiled program instructions, which are usually ignored in actual statistics. -When analyzing the space complexity of a program, **we usually count three parts: temporary data, stack frame space, and output data**, as shown in the following figure. +When analyzing the space complexity of a program, **we usually consider three parts: temporary data, stack frame space, and output data**, as shown in the following figure. ![Algorithm-related space](space_complexity.assets/space_types.png){ class="animation-figure" } @@ -369,11 +369,11 @@ The related code is as follows: ## 2.4.2   Calculation Method -The calculation method for space complexity is roughly the same as for time complexity, except that the statistical object is changed from "number of operations" to "size of space used". +The calculation method for space complexity is roughly the same as for time complexity, except that what we measure changes from the "number of operations" to the "amount of space used". Unlike time complexity, **we usually only focus on the worst-case space complexity**. This is because memory space is a hard requirement, and we must ensure that sufficient memory space is reserved for all input data. -Observe the following code. The "worst case" in worst-case space complexity has two meanings. +Observe the following code. Here, "worst case" in worst-case space complexity has two meanings. 1. **Based on the worst input data**: When $n < 10$, the space complexity is $O(1)$; but when $n > 10$, the initialized array `nums` occupies $O(n)$ space, so the worst-case space complexity is $O(n)$. 2. **Based on the peak memory during algorithm execution**: For example, before executing the last line, the program occupies $O(1)$ space; when initializing the array `nums`, the program occupies $O(n)$ space, so the worst-case space complexity is $O(n)$. @@ -823,7 +823,7 @@ $$ ### 1.   Constant Order $O(1)$ {data-toc-label="1.   Constant Order"} -Constant order is common in constants, variables, and objects whose quantity is independent of the input data size $n$. +Constant order is common for constants, variables, and objects whose number is independent of the input data size $n$. It should be noted that memory occupied by initializing variables or calling functions in a loop is released when entering the next iteration, so it does not accumulate space, and the space complexity remains $O(1)$: @@ -2212,6 +2212,6 @@ Another example is converting a number to a string. Given a positive integer $n$ Ideally, we hope that both the time complexity and space complexity of an algorithm can reach optimal. However, in practice, optimizing both time complexity and space complexity simultaneously is usually very difficult. -**Reducing time complexity usually comes at the cost of increasing space complexity, and vice versa**. The approach of sacrificing memory space to improve algorithm execution speed is called "trading space for time"; conversely, it is called "trading time for space". +**Reducing time complexity usually comes at the cost of increasing space complexity, and vice versa**. Sacrificing memory space to improve execution speed is called "trading space for time"; the reverse is called "trading time for space". The choice of which approach depends on which aspect we value more. In most cases, time is more precious than space, so "trading space for time" is usually the more common strategy. Of course, when the data volume is very large, controlling space complexity is also very important. diff --git a/en/docs/chapter_computational_complexity/summary.md b/en/docs/chapter_computational_complexity/summary.md index 5d4f0c24d..a793f3bb1 100644 --- a/en/docs/chapter_computational_complexity/summary.md +++ b/en/docs/chapter_computational_complexity/summary.md @@ -10,16 +10,16 @@ comments: true - Time efficiency and space efficiency are the two primary evaluation metrics for measuring algorithm performance. - We can evaluate algorithm efficiency through actual testing, but it is difficult to eliminate the influence of the testing environment, and it consumes substantial computational resources. -- Complexity analysis can eliminate the drawbacks of actual testing, with results applicable to all running platforms, and it can reveal algorithm efficiency under different data scales. +- Complexity analysis can overcome the limitations of actual testing. Its results apply across running platforms, and it can reveal algorithm efficiency under different data scales. **Time Complexity** -- Time complexity is used to measure the trend of algorithm runtime as data volume increases. It can effectively evaluate algorithm efficiency, but may fail in certain situations, such as when the input data volume is small or when time complexities are identical, making it impossible to precisely compare algorithm efficiency. +- Time complexity is used to measure the trend of algorithm runtime as data volume increases. It can effectively evaluate algorithm efficiency, but it may be less informative in certain situations, such as when the input data volume is small or when time complexities are identical, making it impossible to precisely compare algorithm efficiency. - Worst-case time complexity is represented using Big $O$ notation, corresponding to the asymptotic upper bound of a function, reflecting the growth level of the number of operations $T(n)$ as $n$ approaches positive infinity. - Deriving time complexity involves two steps: first, counting the number of operations, then determining the asymptotic upper bound. - Common time complexities arranged from low to high include $O(1)$, $O(\log n)$, $O(n)$, $O(n \log n)$, $O(n^2)$, $O(2^n)$, and $O(n!)$. - The time complexity of some algorithms is not fixed, but rather depends on the distribution of input data. Time complexity is divided into worst-case, best-case, and average-case time complexity. Best-case time complexity is rarely used because input data generally needs to satisfy strict conditions to achieve the best case. -- Average time complexity reflects the algorithm's runtime efficiency under random data input, and is closest to the algorithm's performance in practical applications. Calculating average time complexity requires statistical analysis of input data distribution and the combined mathematical expectation. +- Average time complexity reflects the algorithm's runtime efficiency under random data input, and is closest to the algorithm's performance in practical applications. Calculating average time complexity requires analyzing the input data distribution and the resulting mathematical expectation. **Space Complexity** @@ -36,7 +36,7 @@ Theoretically, the space complexity of tail recursive functions can be optimized **Q**: What is the difference between the terms function and method? -A function can be executed independently, with all parameters passed explicitly. A method is associated with an object, is implicitly passed to the object that invokes it, and can operate on data contained in class instances. +A function can be executed independently, with all parameters passed explicitly. A method is associated with an object, is implicitly bound to the object that invokes it, and can operate on data contained in class instances. The following examples use several common programming languages for illustration. @@ -50,7 +50,7 @@ No, the diagram shows space complexity, which reflects growth trends rather than Assuming $n = 8$, you might find that the values of each curve do not correspond to the functions. This is because each curve contains a constant term used to compress the value range into a visually comfortable range. -In practice, because we generally do not know what the "constant term" complexity of each method is, we usually cannot select the optimal solution for $n = 8$ based on complexity alone. But for $n = 8^5$, the choice is straightforward, as the growth trend already dominates. +In practice, because we generally do not know the "constant-term" cost of each method, we usually cannot choose the optimal solution for cases like $n = 8$ based on complexity alone. But for $n = 8^5$, the choice is straightforward, because the growth trend already dominates. **Q**: Are there situations where algorithms are designed to sacrifice time (or space) based on actual use cases? diff --git a/en/docs/chapter_computational_complexity/time_complexity.md b/en/docs/chapter_computational_complexity/time_complexity.md index cd1a91c79..d505b2005 100644 --- a/en/docs/chapter_computational_complexity/time_complexity.md +++ b/en/docs/chapter_computational_complexity/time_complexity.md @@ -211,7 +211,7 @@ $$ 1 + 1 + 10 + (1 + 5) \times n = 6n + 12 $$ -In reality, however, **counting an algorithm's runtime is neither reasonable nor realistic**. First, we do not want to tie the estimated time to the running platform, because algorithms need to run on various different platforms. Second, it is difficult to know the runtime of each type of operation, which brings great difficulty to the estimation process. +In reality, however, **trying to count an algorithm's exact runtime is neither practical nor realistic**. First, we do not want to tie the estimated time to the running platform, because algorithms need to run on many different platforms. Second, it is difficult to know the runtime of each type of operation, which makes the estimation process extremely difficult. ## 2.3.1   Counting Time Growth Trends @@ -501,7 +501,7 @@ Figure 2-7 shows the time complexity of the above three algorithm functions. Compared to directly counting the algorithm's runtime, what are the characteristics of time complexity analysis? - **Time complexity can effectively evaluate algorithm efficiency**. For example, the runtime of algorithm `B` grows linearly; when $n > 1$ it is slower than algorithm `A`, and when $n > 1000000$ it is slower than algorithm `C`. In fact, as long as the input data size $n$ is sufficiently large, an algorithm with "constant order" complexity will always be superior to one with "linear order" complexity, which is precisely the meaning of time growth trend. -- **The derivation method for time complexity is simpler**. Obviously, the running platform and the types of computational operations are both unrelated to the growth trend of the algorithm's runtime. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same "unit time", thus simplifying "counting computational operation runtime" to "counting the number of computational operations", which greatly reduces the difficulty of estimation. +- **The derivation method for time complexity is simpler**. Obviously, the running platform and the types of computational operations are both unrelated to the growth trend of the algorithm's runtime. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same "unit time", reducing "tracking the runtime of each operation" to "counting the number of operations", which greatly reduces the difficulty of estimation. - **Time complexity also has certain limitations**. For example, although algorithms `A` and `C` have the same time complexity, their actual runtimes differ significantly. Similarly, although algorithm `B` has a higher time complexity than `C`, when the input data size $n$ is small, algorithm `B` is clearly superior to algorithm `C`. In such cases, it is often difficult to judge the efficiency of algorithms based solely on time complexity. Of course, despite the above issues, complexity analysis remains the most effective and commonly used method for evaluating algorithm efficiency. ## 2.3.2   Asymptotic Upper Bound of Functions @@ -713,7 +713,7 @@ As shown in Figure 2-8, calculating the asymptotic upper bound is to find a func ## 2.3.3   Derivation Method -The asymptotic upper bound has a bit of mathematical flavor. If you feel you haven't fully understood it, don't worry. We can first master the derivation method, and gradually grasp its mathematical meaning through continuous practice. +The idea of an asymptotic upper bound is somewhat mathematical. If you feel you haven't fully understood it, don't worry. We can first master the derivation method, and gradually grasp its mathematical meaning through continuous practice. According to the definition, after determining $f(n)$, we can obtain the time complexity $O(f(n))$. So how do we determine the asymptotic upper bound $f(n)$? Overall, it is divided into two steps: first count the number of operations, then determine the asymptotic upper bound. @@ -1005,7 +1005,7 @@ Let the input data size be $n$. Common time complexity types are shown in Figure $$ \begin{aligned} O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline -\text{Constant order} < \text{Logarithmic order} < \text{Linear order} < \text{Linearithmic order} < \text{Quadratic order} < \text{Exponential order} < \text{Factorial order} +\text{Constant} < \text{Logarithmic} < \text{Linear} < \text{Linearithmic} < \text{Quadratic} < \text{Exponential} < \text{Factorial} \end{aligned} $$ @@ -1017,7 +1017,7 @@ $$ The number of operations in constant order is independent of the input data size $n$, meaning it does not change as $n$ changes. -In the following function, although the number of operations `size` may be large, since it is independent of the input data size $n$, the time complexity remains $O(1)$: +In the following function, although the value of `size` may be large, it is independent of the input data size $n$, so the time complexity remains $O(1)$: === "Python" @@ -2784,7 +2784,7 @@ Like exponential order, logarithmic order also commonly appears in recursive fun end ``` -Logarithmic order commonly appears in algorithms based on the divide-and-conquer strategy, embodying the algorithmic thinking of "dividing into many" and "simplifying complexity". It grows slowly and is the ideal time complexity second only to constant order. +Logarithmic order commonly appears in algorithms based on the divide-and-conquer strategy, reflecting the idea of repeatedly splitting a problem and simplifying it. It grows slowly and is the ideal time complexity second only to constant order. !!! tip "What is the base of $O(\log n)$?" @@ -3605,7 +3605,7 @@ The "worst-case time complexity" corresponds to the function's asymptotic upper It is worth noting that we rarely use best-case time complexity in practice, because it can usually only be achieved with a very small probability and may be somewhat misleading. **The worst-case time complexity is more practical because it gives a safety value for efficiency**, allowing us to use the algorithm with confidence. -From the above example, we can see that both worst-case and best-case time complexities only occur under "special data distributions", which may have a very small probability of occurrence and may not truly reflect the algorithm's running efficiency. In contrast, **average time complexity can reflect the algorithm's running efficiency under random input data**, denoted using the $\Theta$ notation. +From the above example, we can see that both worst-case and best-case time complexities arise only under particular input distributions, which may occur with very low probability and may not truly reflect the algorithm's running efficiency. In contrast, **average time complexity can reflect the algorithm's running efficiency under random input data**, denoted using the $\Theta$ notation. For some algorithms, we can simply derive the average case under random data distribution. For example, in the above example, since the input array is shuffled, the probability of element $1$ appearing at any index is equal, so the algorithm's average number of loops is half the array length $n / 2$, giving an average time complexity of $\Theta(n / 2) = \Theta(n)$. diff --git a/en/docs/chapter_data_structure/basic_data_types.md b/en/docs/chapter_data_structure/basic_data_types.md index 25fde05bb..675a92791 100644 --- a/en/docs/chapter_data_structure/basic_data_types.md +++ b/en/docs/chapter_data_structure/basic_data_types.md @@ -4,7 +4,7 @@ comments: true # 3.2   Basic Data Types -When we talk about data in computers, we think of various forms such as text, images, videos, audio, 3D models, and more. Although these data are organized in different ways, they are all composed of various basic data types. +When we talk about data stored in computers, we think of various forms such as text, images, videos, audio, 3D models, and more. Although these kinds of data are organized in different ways, they are all composed of various basic data types. **Basic data types are types that the CPU can directly operate on**, and they are directly used in algorithms, mainly including the following. @@ -13,7 +13,7 @@ When we talk about data in computers, we think of various forms such as text, im - Character type `char`, used to represent letters, punctuation marks, and even emojis in various languages. - Boolean type `bool`, used to represent "yes" and "no" judgments. -**Basic data types are stored in binary form in computers**. One binary bit is $1$ bit. In most modern operating systems, $1$ byte consists of $8$ bits. +**Basic data types are stored in binary form in computers**. A binary digit is one bit. In most modern operating systems, $1$ byte consists of $8$ bits. The range of values for basic data types depends on the size of the space they occupy. Below is an example using Java. @@ -39,16 +39,16 @@ The following table lists the space occupied, value ranges, and default values o
-Please note that the above table is specific to Java's basic data types. Each programming language has its own data type definitions, and their space occupied, value ranges, and default values may vary. +Please note that Table 3-1 applies specifically to Java's basic data types. Each programming language has its own type definitions, and their space usage, value ranges, and default values may vary. - In Python, the integer type `int` can be of any size, limited only by available memory; the floating-point type `float` is double-precision 64-bit; there is no `char` type, a single character is actually a string `str` of length 1. - C and C++ do not explicitly specify the size of basic data types, which varies by implementation and platform. The above table follows the LP64 [data model](https://en.cppreference.com/w/cpp/language/types#Properties), which is used in Unix 64-bit operating systems including Linux and macOS. - The size of character `char` is 1 byte in C and C++, and in most programming languages it depends on the specific character encoding method, as detailed in the "Character Encoding" section. - Even though representing a boolean value requires only 1 bit ($0$ or $1$), it is usually stored as 1 byte in memory. This is because modern computer CPUs typically use 1 byte as the minimum addressable memory unit. -So, what is the relationship between basic data types and data structures? We know that data structures are ways of organizing and storing data in computers. The subject of this statement is "structure", not "data". +So, what is the relationship between basic data types and data structures? We know that data structures are ways of organizing and storing data in computers. Here, the emphasis is on the "structure", not the "data". -If we want to represent "a row of numbers", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and order relationships of numbers, but the content stored—whether integer `int`, floating-point `float`, or character `char`—is unrelated to the "data structure". +If we want to represent "a row of numbers", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and order relationships of numbers, but whether the stored content is integer `int`, floating-point `float`, or character `char` is unrelated to the "data structure". In other words, **basic data types provide the "content type" of data, while data structures provide the "organization method" of data**. For example, in the following code, we use the same data structure (array) to store and represent different basic data types, including `int`, `float`, `char`, `bool`, etc. diff --git a/en/docs/chapter_data_structure/character_encoding.md b/en/docs/chapter_data_structure/character_encoding.md index cc1aa0046..b89d26463 100644 --- a/en/docs/chapter_data_structure/character_encoding.md +++ b/en/docs/chapter_data_structure/character_encoding.md @@ -20,7 +20,7 @@ Worldwide, a batch of EASCII character sets suitable for different regions have ## 3.4.2   Gbk Character Set -Later, people found that **EASCII code still cannot meet the character quantity requirements of many languages**. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used daily. In 1980, the China National Standardization Administration released the GB2312 character set, which included 6,763 Chinese characters, basically meeting the needs for computer processing of Chinese characters. +Later, people found that **EASCII still could not provide enough characters for many languages**. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used in everyday life. In 1980, the China National Standardization Administration released the GB2312 character set, which included 6,763 Chinese characters, basically meeting the needs of computer processing for Chinese. However, GB2312 cannot handle some rare characters and traditional Chinese characters. The GBK character set is an extension based on GB2312, which includes a total of 21,886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented using one byte, and Chinese characters are represented using two bytes. @@ -28,11 +28,11 @@ However, GB2312 cannot handle some rare characters and traditional Chinese chara With the vigorous development of computer technology, character sets and encoding standards flourished, which brought many problems. On the one hand, these character sets generally only define characters for specific languages and cannot work normally in multilingual environments. On the other hand, multiple character set standards exist for the same language, and if two computers use different encoding standards, garbled characters will appear during information transmission. -Researchers of that era thought: **If a sufficiently complete character set is released that includes all languages and symbols in the world, wouldn't it be possible to solve cross-language environment and garbled character problems**? Driven by this idea, a large and comprehensive character set, Unicode, was born. +Researchers of that era thought: **If a sufficiently complete character set were released to include all languages and symbols in the world, wouldn't that solve problems in cross-language environments and eliminate garbled text**? Driven by this idea, a large and comprehensive character set, Unicode, was born. Unicode is called "统一码" (Unified Code) in Chinese and can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards. -Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages. In the vast Unicode character set, commonly used characters occupy 2 bytes, and some rare characters occupy 3 bytes or even 4 bytes. +Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages. In practical storage and encoding schemes for this vast character set, commonly used characters often occupy 2 bytes, while some rare characters occupy 3 bytes or even 4 bytes. Unicode is a universal character set that essentially assigns a number (called a "code point") to each character, **but it does not specify how to store these character code points in computers**. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters? @@ -53,7 +53,7 @@ The encoding rules of UTF-8 are not complicated and can be divided into the foll - For 1-byte characters, set the highest bit to $0$, and set the remaining 7 bits to the Unicode code point. It is worth noting that ASCII characters occupy the first 128 code points in the Unicode character set. That is to say, **UTF-8 encoding is backward compatible with ASCII code**. This means we can use UTF-8 to parse very old ASCII code text. - For characters with a length of $n$ bytes (where $n > 1$), set the highest $n$ bits of the first byte to $1$, and set the $(n + 1)$-th bit to $0$; starting from the second byte, set the highest 2 bits of each byte to $10$; use all remaining bits to fill in the Unicode code point of the character. -Figure 3-8 shows the UTF-8 encoding corresponding to "Hello算法". It can be observed that since the highest $n$ bits are all set to $1$, the system can parse the length of the character as $n$ by reading the number of highest bits that are $1$. +Figure 3-8 shows the UTF-8 encoding corresponding to "Hello算法". It can be observed that since the highest $n$ bits are all set to $1$, the system can determine that the character length is $n$ by counting the leading $1$ bits. But why set the highest 2 bits of all other bytes to $10$? In fact, this $10$ can serve as a check symbol. Assuming the system starts parsing text from an incorrect byte, the $10$ at the beginning of the byte can help the system quickly determine an anomaly. @@ -74,7 +74,7 @@ From a compatibility perspective, UTF-8 has the best universality, and many tool ## 3.4.5   Character Encoding in Programming Languages -For most past programming languages, strings during program execution use fixed-length encodings such as UTF-16 or UTF-32. Under fixed-length encoding, we can treat strings as arrays for processing, and this approach has the following advantages. +For many programming languages in the past, strings during program execution used internal encodings such as UTF-16 or UTF-32. Under these representations, we can often treat strings like arrays during processing, and this approach has the following advantages. - **Random access**: UTF-16 encoded strings can be easily accessed randomly. UTF-8 is a variable-length encoding. To find the $i$-th character, we need to traverse from the beginning of the string to the $i$-th character, which requires $O(n)$ time. - **Character counting**: Similar to random access, calculating the length of a UTF-16 encoded string is also an $O(1)$ operation. However, calculating the length of a UTF-8 encoded string requires traversing the entire string. diff --git a/en/docs/chapter_data_structure/classification_of_data_structure.md b/en/docs/chapter_data_structure/classification_of_data_structure.md index 01ce2c320..34e972770 100644 --- a/en/docs/chapter_data_structure/classification_of_data_structure.md +++ b/en/docs/chapter_data_structure/classification_of_data_structure.md @@ -8,7 +8,7 @@ Common data structures include arrays, linked lists, stacks, queues, hash tables ## 3.1.1   Logical Structure: Linear and Non-Linear -**Logical structure reveals the logical relationships between data elements**. In arrays and linked lists, data is arranged in a certain order, embodying the linear relationship between data; while in trees, data is arranged hierarchically from top to bottom, showing the derived relationship between "ancestors" and "descendants"; graphs are composed of nodes and edges, reflecting complex network relationships. +**Logical structure reveals the logical relationships between data elements**. In arrays and linked lists, data is arranged in a certain order, embodying linear relationships between elements; while in trees, data is arranged hierarchically from top to bottom, showing parent-descendant relationships; graphs are composed of nodes and edges, reflecting complex network relationships. As shown in Figure 3-1, logical structures can be divided into two major categories: "linear" and "non-linear". Linear structures are more intuitive, indicating that data is linearly arranged in logical relationships; non-linear structures are the opposite, arranged non-linearly. @@ -36,11 +36,11 @@ Non-linear data structures can be further divided into tree structures and netwo !!! tip - It is worth noting that comparing memory to an Excel spreadsheet is a simplified analogy. The actual working mechanism of memory is quite complex, involving concepts such as address space, memory management, cache mechanisms, virtual memory, and physical memory. + It should be noted that comparing memory to an Excel spreadsheet is only a simplified analogy. The actual workings of memory are much more complex, involving concepts such as address space, memory management, cache mechanisms, virtual memory, and physical memory. Memory is a shared resource for all programs. When a block of memory is occupied by a program, it usually cannot be used by other programs at the same time. **Therefore, in the design of data structures and algorithms, memory resources are an important consideration**. For example, the peak memory occupied by an algorithm should not exceed the remaining free memory of the system; if there is a lack of contiguous large memory blocks, then the data structure chosen must be able to be stored in dispersed memory spaces. -As shown in Figure 3-3, **physical structure reflects the way data is stored in computer memory**, and can be divided into contiguous space storage (arrays) and dispersed space storage (linked lists). The two physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency. +As shown in Figure 3-3, **physical structure reflects the way data is stored in computer memory**. It can be divided into contiguous-space storage (arrays) and dispersed-space storage (linked lists). At a low level, physical structure determines how data is accessed, updated, inserted, and deleted. These two physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency. ![Contiguous space storage and dispersed space storage](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" } @@ -51,7 +51,7 @@ It is worth noting that **all data structures are implemented based on arrays, l - **Can be implemented based on arrays**: Stacks, queues, hash tables, trees, heaps, graphs, matrices, tensors (arrays with dimensions $\geq 3$), etc. - **Can be implemented based on linked lists**: Stacks, queues, hash tables, trees, heaps, graphs, etc. -After initialization, linked lists can still adjust their length during program execution, so they are also called "dynamic data structures". After initialization, the length of arrays cannot be changed, so they are also called "static data structures". It is worth noting that arrays can achieve length changes by reallocating memory, thus possessing a certain degree of "dynamism". +After initialization, linked lists can still adjust their length during program execution, so they are also called "dynamic data structures". After initialization, the length of arrays cannot be changed, so they are also called "static data structures". It is worth noting that arrays can change length by reallocating memory, thus retaining a limited degree of flexibility. !!! tip diff --git a/en/docs/chapter_data_structure/index.md b/en/docs/chapter_data_structure/index.md index 84de2ec5f..40177aaa6 100644 --- a/en/docs/chapter_data_structure/index.md +++ b/en/docs/chapter_data_structure/index.md @@ -9,7 +9,7 @@ icon: material/shape-outline !!! abstract - Data structure is like a sturdy and diverse framework. + Data structures are like a sturdy and diverse framework. It provides a blueprint for the orderly organization of data, upon which algorithms come to life. diff --git a/en/docs/chapter_data_structure/number_encoding.md b/en/docs/chapter_data_structure/number_encoding.md index bfa282694..38d5e098b 100644 --- a/en/docs/chapter_data_structure/number_encoding.md +++ b/en/docs/chapter_data_structure/number_encoding.md @@ -10,7 +10,7 @@ comments: true ## 3.3.1   Sign-Magnitude, 1's Complement, and 2's Complement -In the table from the previous section, we found that all integer types can represent one more negative number than positive numbers. For example, the `byte` range is $[-128, 127]$. This phenomenon is counterintuitive, and its underlying reason involves knowledge of sign-magnitude, 1's complement, and 2's complement. +In the table from the previous section, we found that all integer types can represent one more negative number than positive numbers. For example, the `byte` range is $[-128, 127]$. This phenomenon is counterintuitive, and its underlying cause lies in sign-magnitude, 1's complement, and 2's complement representations. First, it should be noted that **numbers are stored in computers in the form of "2's complement"**. Before analyzing the reasons for this, let's first define these three concepts. @@ -69,7 +69,7 @@ $$ Adding $1$ to the 1's complement of negative zero produces a carry, but since the `byte` type has a length of only 8 bits, the $1$ that overflows to the 9th bit is discarded. That is to say, **the 2's complement of negative zero is $0000 \; 0000$, which is the same as the 2's complement of positive zero**. This means that in 2's complement representation, there is only one zero, and the positive and negative zero ambiguity is thus resolved. -One last question remains: the range of the `byte` type is $[-128, 127]$, and how is the extra negative number $-128$ obtained? We notice that all integers in the interval $[-127, +127]$ have corresponding sign-magnitude, 1's complement, and 2's complement, and sign-magnitude and 2's complement can be converted to each other. +One last question remains: the range of the `byte` type is $[-128, 127]$, so where does the extra negative number $-128$ come from? We notice that all integers in the interval $[-127, +127]$ have corresponding sign-magnitude, 1's complement, and 2's complement, and sign-magnitude and 2's complement can be converted to each other. However, **the 2's complement $1000 \; 0000$ is an exception, and it does not have a corresponding sign-magnitude**. According to the conversion method, we get that the sign-magnitude of this 2's complement is $0000 \; 0000$. This is clearly contradictory because this sign-magnitude represents the number $0$, and its 2's complement should be itself. The computer specifies that this special 2's complement $1000 \; 0000$ represents $-128$. In fact, the result of calculating $(-1) + (-127)$ in 2's complement is $-128$. @@ -88,7 +88,7 @@ You may have noticed that all the above calculations are addition operations. Th Please note that this does not mean that computers can only perform addition. **By combining addition with some basic logical operations, computers can implement various other mathematical operations**. For example, calculating the subtraction $a - b$ can be converted to calculating the addition $a + (-b)$; calculating multiplication and division can be converted to calculating multiple additions or subtractions. -Now we can summarize the reasons why computers use 2's complement: based on 2's complement representation, computers can use the same circuits and operations to handle the addition of positive and negative numbers, without the need to design special hardware circuits to handle subtraction, and without the need to specially handle the ambiguity problem of positive and negative zero. This greatly simplifies hardware design and improves operational efficiency. +We can now summarize why computers use 2's complement: with 2's complement representation, computers can use the same circuits and operations to handle the addition of positive and negative numbers, without designing special hardware circuits for subtraction or separately handling the ambiguity of positive and negative zero. This greatly simplifies hardware design and improves efficiency. The design of 2's complement is very ingenious. Due to space limitations, we will stop here. Interested readers are encouraged to explore further. diff --git a/en/docs/chapter_data_structure/summary.md b/en/docs/chapter_data_structure/summary.md index 644c5423a..f5ece4b6f 100644 --- a/en/docs/chapter_data_structure/summary.md +++ b/en/docs/chapter_data_structure/summary.md @@ -7,15 +7,15 @@ comments: true ### 1.   Key Review - Data structures can be classified from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data elements, while physical structure describes how data is stored in computer memory. -- Common logical structures include linear, tree, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures. +- Common logical structures include linear, tree-like, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures. - When a program runs, data is stored in computer memory. Each memory space has a corresponding memory address, and the program accesses data through these memory addresses. - Physical structures are primarily divided into contiguous space storage (arrays) and dispersed space storage (linked lists). All data structures are implemented using arrays, linked lists, or a combination of both. - Basic data types in computers include integers `byte`, `short`, `int`, `long`, floating-point numbers `float`, `double`, characters `char`, and booleans `bool`. Their value ranges depend on the size of space they occupy and their representation method. - Sign-magnitude, 1's complement, and 2's complement are three methods for encoding numbers in computers, and they can be converted into each other. The most significant bit of sign-magnitude is the sign bit, and the remaining bits represent the value of the number. - Integers are stored in computers in 2's complement form. Under 2's complement representation, computers can treat the addition of positive and negative numbers uniformly, without needing to design special hardware circuits for subtraction, and there is no ambiguity of positive and negative zero. - The encoding of floating-point numbers consists of 1 sign bit, 8 exponent bits, and 23 fraction bits. Due to the exponent bits, the range of floating-point numbers is much larger than that of integers, at the cost of sacrificing precision. -- ASCII is the earliest English character set, with a length of 1 byte, containing a total of 127 characters. GBK is a commonly used Chinese character set, containing over 20,000 Chinese characters. Unicode is committed to providing a complete character set standard, collecting characters from various languages around the world, thereby solving the garbled text problem caused by inconsistent character encoding methods. -- UTF-8 is the most popular Unicode encoding method, with excellent universality. It is a variable-length encoding method with good scalability, effectively improving storage space efficiency. UTF-16 and UTF-32 are fixed-length encoding methods. When encoding Chinese characters, UTF-16 occupies less space than UTF-8. Programming languages such as Java and C# use UTF-16 encoding by default. +- ASCII is the earliest English character set, with a length of 1 byte, containing a total of 128 characters. GBK is a commonly used Chinese character set, containing over 20,000 Chinese characters. Unicode is committed to providing a complete character set standard, collecting characters from various languages around the world, thereby solving the garbled text problem caused by inconsistent character encoding methods. +- UTF-8 is the most popular Unicode encoding method and has excellent compatibility. It is a variable-length encoding method with good scalability, effectively improving storage space efficiency. UTF-16 and UTF-32 are common Unicode encoding methods. When encoding Chinese characters, UTF-16 occupies less space than UTF-8. Programming languages such as Java and C# use UTF-16 encoding by default. ### 2.   Q & A @@ -35,7 +35,7 @@ Stacks can indeed implement dynamic data operations, but the data structure is s **Q**: When constructing a stack (queue), its size is not specified. Why are they "static data structures"? -In high-level programming languages, we do not need to manually specify the initial capacity of a stack (queue); this work is automatically completed within the class. For example, the initial capacity of Java's `ArrayList` is typically 10. Additionally, the expansion operation is also automatically implemented. See the subsequent "List" section for details. +In high-level programming languages, we do not need to manually specify the initial capacity of a stack (queue); the class handles this automatically. For example, the initial capacity of Java's `ArrayList` is typically 10. Additionally, the expansion operation is also automatically implemented. See the subsequent "List" section for details. **Q**: The method of converting sign-magnitude to 2's complement is "first negate then add 1". So converting 2's complement to sign-magnitude should be the inverse operation "first subtract 1 then negate". However, 2's complement can also be converted to sign-magnitude through "first negate then add 1". Why is this? diff --git a/en/docs/chapter_divide_and_conquer/binary_search_recur.md b/en/docs/chapter_divide_and_conquer/binary_search_recur.md index 31192cd6b..9efd6af11 100644 --- a/en/docs/chapter_divide_and_conquer/binary_search_recur.md +++ b/en/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -7,7 +7,7 @@ comments: true We have already learned that search algorithms are divided into two major categories. - **Brute-force search**: Implemented by traversing the data structure, with a time complexity of $O(n)$. -- **Adaptive search**: Utilizes unique data organization forms or prior information, with time complexity reaching $O(\log n)$ or even $O(1)$. +- **Adaptive search**: Leverages specific data organization or prior information, with time complexity reaching $O(\log n)$ or even $O(1)$. In fact, **search algorithms with time complexity of $O(\log n)$ are typically implemented based on the divide and conquer strategy**, such as binary search and trees. @@ -28,7 +28,7 @@ In previous sections, binary search was implemented based on iteration. Now we i !!! question - Given a sorted array `nums` of length $n$, where all elements are unique, find the element `target`. + Given a sorted array `nums` of length $n$, where all elements are unique, find `target`. From a divide and conquer perspective, we denote the subproblem corresponding to the search interval $[i, j]$ as $f(i, j)$. @@ -36,7 +36,7 @@ Starting from the original problem $f(0, n-1)$, perform binary search through th 1. Calculate the midpoint $m$ of the search interval $[i, j]$, and use it to eliminate half of the search interval. 2. Recursively solve the subproblem reduced by half in size, which could be $f(i, m-1)$ or $f(m+1, j)$. -3. Repeat steps `1.` and `2.` until `target` is found or the interval is empty and return. +3. Repeat steps `1.` and `2.` until `target` is found, or return when the interval is empty. Figure 12-4 shows the divide and conquer process of binary search for element $6$ in an array. diff --git a/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md index ff25ffea9..fb863b42a 100644 --- a/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -41,7 +41,7 @@ Using the data from the figure above as an example, we can obtain the division r ### 3.   Describing Subtree Intervals Based on Variables -Based on the above division method, **we have obtained the index intervals of the root node, left subtree, and right subtree in `preorder` and `inorder`**. To describe these index intervals, we need to use several pointer variables. +Based on the above division method, **we have obtained the index intervals of the root node, left subtree, and right subtree in `preorder` and `inorder`**. To describe these index intervals, we need to use several index variables. - Denote the index of the current tree's root node in `preorder` as $i$. - Denote the index of the current tree's root node in `inorder` as $m$. diff --git a/en/docs/chapter_divide_and_conquer/divide_and_conquer.md b/en/docs/chapter_divide_and_conquer/divide_and_conquer.md index c51d9b154..a84ed317e 100644 --- a/en/docs/chapter_divide_and_conquer/divide_and_conquer.md +++ b/en/docs/chapter_divide_and_conquer/divide_and_conquer.md @@ -4,7 +4,7 @@ comments: true # 12.1   Divide and Conquer Algorithms -Divide and conquer is a very important and common algorithm strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: "divide" and "conquer". +Divide and conquer is a very important and common algorithmic strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: "divide" and "conquer". 1. **Divide (partition phase)**: Recursively divide the original problem into two or more subproblems until the smallest subproblem is reached. 2. **Conquer (merge phase)**: Starting from the smallest subproblems with known solutions, merge the solutions of subproblems from bottom to top to construct the solution to the original problem. @@ -34,13 +34,13 @@ Clearly, merge sort satisfies these three criteria. ## 12.1.2   Improving Efficiency Through Divide and Conquer -**Divide and conquer can not only effectively solve algorithmic problems but often also improve algorithm efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy. +**Divide and conquer can not only effectively solve algorithmic problems, but can often also improve algorithmic efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy. This raises the question: **Why can divide and conquer improve algorithm efficiency, and what is the underlying logic**? In other words, why is dividing a large problem into multiple subproblems, solving the subproblems, and merging their solutions more efficient than directly solving the original problem? This question can be discussed from two aspects: operation count and parallel computation. ### 1.   Operation Count Optimization -Taking "bubble sort" as an example, processing an array of length $n$ requires $O(n^2)$ time. Suppose we divide the array into two subarrays from the midpoint as shown in Figure 12-2, the division requires $O(n)$ time, sorting each subarray requires $O((n / 2)^2)$ time, and merging the two subarrays requires $O(n)$ time, resulting in an overall time complexity of: +Taking "bubble sort" as an example, processing an array of length $n$ requires $O(n^2)$ time. Suppose we divide the array at the midpoint into two subarrays, as shown in Figure 12-2. The division requires $O(n)$ time, sorting each subarray requires $O((n / 2)^2)$ time, and merging the two subarrays requires $O(n)$ time, resulting in an overall time complexity of: $$ O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n) @@ -68,7 +68,7 @@ Thinking further, **what if we set multiple division points** and evenly divide ### 2.   Parallel Computation Optimization -We know that the subproblems generated by divide and conquer are independent of each other, **so they can typically be solved in parallel**. This means divide and conquer can not only reduce the time complexity of algorithms, **but also benefits from parallel optimization by operating systems**. +We know that the subproblems generated by divide and conquer are independent of each other, **so they can typically be solved in parallel**. This means divide and conquer can not only reduce the time complexity of algorithms, **but is also amenable to parallel optimization by the operating system**. Parallel optimization is particularly effective in multi-core or multi-processor environments, as the system can simultaneously handle multiple subproblems, making fuller use of computing resources and significantly reducing overall runtime. @@ -80,7 +80,7 @@ For example, in the "bucket sort" shown in Figure 12-3, we evenly distribute mas ## 12.1.3   Common Applications of Divide and Conquer -On one hand, divide and conquer can be used to solve many classic algorithmic problems. +On the one hand, divide and conquer can be used to solve many classic algorithmic problems. - **Finding the closest pair of points**: This algorithm first divides the point set into two parts, then finds the closest pair of points in each part separately, and finally finds the closest pair of points that spans both parts. - **Large integer multiplication**: For example, the Karatsuba algorithm, which decomposes large integer multiplication into several smaller integer multiplications and additions. @@ -90,12 +90,12 @@ On one hand, divide and conquer can be used to solve many classic algorithmic pr On the other hand, divide and conquer is widely applied in the design of algorithms and data structures. -- **Binary search**: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary operation on the remaining interval. +- **Binary search**: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary-search step on the remaining interval. - **Merge sort**: Already introduced at the beginning of this section, no further elaboration needed. - **Quick sort**: Quick sort selects a pivot value, then divides the array into two subarrays, one with elements smaller than the pivot and the other with elements larger than the pivot, then performs the same division operation on these two parts until the subarrays have only one element. - **Bucket sort**: The basic idea of bucket sort is to scatter data into multiple buckets, then sort the elements within each bucket, and finally extract the elements from each bucket in sequence to obtain a sorted array. - **Trees**: For example, binary search trees, AVL trees, red-black trees, B-trees, B+ trees, etc. Their search, insertion, and deletion operations can all be viewed as applications of the divide and conquer strategy. - **Heaps**: A heap is a special complete binary tree, and its various operations, such as insertion, deletion, and heapify, actually imply the divide and conquer idea. -- **Hash tables**: Although hash tables do not directly apply divide and conquer, some hash collision resolution solutions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve query efficiency. +- **Hash tables**: Although hash tables do not directly apply divide and conquer, some methods for resolving hash collisions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve lookup efficiency. -It can be seen that **divide and conquer is a "subtly pervasive" algorithmic idea**, embedded in various algorithms and data structures. +It can be seen that **divide and conquer is a "quietly pervasive" algorithmic idea**, embedded in various algorithms and data structures. diff --git a/en/docs/chapter_divide_and_conquer/hanota_problem.md b/en/docs/chapter_divide_and_conquer/hanota_problem.md index 70818ef0f..de5abee46 100644 --- a/en/docs/chapter_divide_and_conquer/hanota_problem.md +++ b/en/docs/chapter_divide_and_conquer/hanota_problem.md @@ -552,4 +552,4 @@ As shown in Figure 12-15, the hanota problem forms a recursion tree of height $n The hanota problem originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and $64$ golden discs of different sizes. The monks continuously moved the discs, believing that when the last disc was correctly placed, the world would come to an end. - However, even if the monks moved one disc per second, it would take approximately $2^{64} \approx 1.84×10^{19}$ seconds, which is about $5850$ billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world. + However, even if the monks moved one disc per second, it would take approximately $2^{64} \approx 1.84×10^{19}$ seconds, which is about $585$ billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world. diff --git a/en/docs/chapter_divide_and_conquer/index.md b/en/docs/chapter_divide_and_conquer/index.md index c00a8daa1..4071a8578 100644 --- a/en/docs/chapter_divide_and_conquer/index.md +++ b/en/docs/chapter_divide_and_conquer/index.md @@ -11,12 +11,12 @@ icon: material/set-split Difficult problems are decomposed layer by layer, with each decomposition making them simpler. - Divide and conquer reveals an important truth: start with simplicity, and nothing remains complex. + Divide and conquer reveals an important truth: start with what is simple, and nothing remains complex. ## Chapter contents - [12.1   Divide and Conquer Algorithms](divide_and_conquer.md) - [12.2   Divide and Conquer Search Strategy](binary_search_recur.md) - [12.3   Building a Binary Tree Problem](build_binary_tree_problem.md) -- [12.4   Hanoi Tower Problem](hanota_problem.md) +- [12.4   Hanota Problem](hanota_problem.md) - [12.5   Summary](summary.md) diff --git a/en/docs/chapter_divide_and_conquer/summary.md b/en/docs/chapter_divide_and_conquer/summary.md index 9f562aaea..0a73511ec 100644 --- a/en/docs/chapter_divide_and_conquer/summary.md +++ b/en/docs/chapter_divide_and_conquer/summary.md @@ -6,11 +6,11 @@ comments: true ### 1.   Key Review -- Divide and conquer is a common algorithm design strategy, consisting of two phases: divide (partition) and conquer (merge), typically implemented based on recursion. +- Divide and conquer is a common algorithm design strategy consisting of two phases, divide (partition) and conquer (merge), and is typically implemented recursively. - The criteria for determining whether a problem is a divide and conquer problem include: whether the problem can be decomposed, whether subproblems are independent, and whether subproblems can be merged. - Merge sort is a typical application of the divide and conquer strategy. It recursively divides an array into two equal-length subarrays until only one element remains, then merges them layer by layer to complete the sorting. -- Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, the divide and conquer strategy reduces the number of operations; on the other hand, it facilitates parallel optimization of the system after division. -- Divide and conquer can both solve many algorithmic problems and is widely applied in data structure and algorithm design, appearing everywhere. +- Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, it reduces the number of operations; on the other hand, it makes parallel optimization by the system easier. +- Divide and conquer can solve many algorithmic problems and is also widely used in data structures and algorithm design, making it ubiquitous. - Compared to brute-force search, adaptive search is more efficient. Search algorithms with time complexity of $O(\log n)$ are typically implemented based on the divide and conquer strategy. - Binary search is another typical application of divide and conquer. It does not include the step of merging solutions of subproblems. We can implement binary search through recursive divide and conquer. - In the problem of building a binary tree, building the tree (original problem) can be divided into building the left subtree and right subtree (subproblems), which can be achieved by dividing the index intervals of the preorder and inorder traversals. diff --git a/en/docs/chapter_dynamic_programming/dp_problem_features.md b/en/docs/chapter_dynamic_programming/dp_problem_features.md index 562401fee..3195dd10a 100644 --- a/en/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/en/docs/chapter_dynamic_programming/dp_problem_features.md @@ -18,7 +18,7 @@ We make a slight modification to the stair climbing problem to make it more suit !!! question "Climbing stairs with minimum cost" - Given a staircase, where you can climb $1$ or $2$ steps at a time, and each step has a non-negative integer representing the cost you need to pay at that step. Given a non-negative integer array $cost$, where $cost[i]$ represents the cost at the $i$-th step, and $cost[0]$ is the ground (starting point). What is the minimum cost required to reach the top? + Given a staircase, you can climb $1$ or $2$ steps at a time, and each step is labeled with a non-negative integer representing the cost of stepping on it. Given a non-negative integer array $cost$, where $cost[i]$ represents the cost of the $i$-th step and $cost[0]$ is the ground (starting point), what is the minimum cost required to reach the top? As shown in Figure 14-6, if the costs of the $1$st, $2$nd, and $3$rd steps are $1$, $10$, and $1$ respectively, then climbing from the ground to the $3$rd step requires a minimum cost of $2$. @@ -588,7 +588,7 @@ However, if we add a constraint to the stair climbing problem, the situation cha Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time, **but you cannot jump $1$ step in two consecutive rounds**. How many ways are there to climb to the top? -As shown in Figure 14-8, there are only $2$ feasible ways to climb to the $3$rd step. The way of jumping $1$ step three consecutive times does not satisfy the constraint and is therefore discarded. +As shown in Figure 14-8, there are only $2$ feasible ways to climb to the $3$rd step. The path with three consecutive $1$-step jumps does not satisfy the constraint and is therefore discarded. ![Number of ways to climb to the 3rd step with constraint](dp_problem_features.assets/climbing_stairs_constraint_example.png){ class="animation-figure" } @@ -600,8 +600,8 @@ It is not difficult to see that this problem no longer satisfies no aftereffects For this reason, we need to expand the state definition: **state $[i, j]$ represents being on the $i$-th step with the previous round having jumped $j$ steps**, where $j \in \{1, 2\}$. This state definition effectively distinguishes whether the previous round was a jump of $1$ step or $2$ steps, allowing us to determine where the current state came from. -- When the previous round jumped $1$ step, the round before that could only choose to jump $2$ steps, i.e., $dp[i, 1]$ can only be transferred from $dp[i-1, 2]$. -- When the previous round jumped $2$ steps, the round before that could choose to jump $1$ step or $2$ steps, i.e., $dp[i, 2]$ can be transferred from $dp[i-2, 1]$ or $dp[i-2, 2]$. +- When the previous round jumped $1$ step, the round before that could only choose to jump $2$ steps, i.e., $dp[i, 1]$ can only transition from $dp[i-1, 2]$. +- When the previous round jumped $2$ steps, the round before that could choose to jump $1$ step or $2$ steps, i.e., $dp[i, 2]$ can transition from $dp[i-2, 1]$ or $dp[i-2, 2]$. As shown in Figure 14-9, under this definition, $dp[i, j]$ represents the number of ways for state $[i, j]$. The state transition equation is then: @@ -936,8 +936,8 @@ In the above case, since we only need to consider one more preceding state, we c !!! question "Climbing stairs with obstacle generation" - Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time. **It is stipulated that when climbing to the $i$-th step, the system will automatically place an obstacle on the $2i$-th step, and thereafter no round is allowed to jump to the $2i$-th step**. For example, if the first two rounds jump to the $2$nd and $3$rd steps, then afterwards you cannot jump to the $4$th and $6$th steps. How many ways are there to climb to the top? + Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time. **Whenever you reach the $i$-th step, the system automatically places an obstacle on the $2i$-th step, and no subsequent round is allowed to jump to the $2i$-th step**. For example, if the first two rounds jump to the $2$nd and $3$rd steps, then afterwards you cannot jump to the $4$th and $6$th steps. How many ways are there to climb to the top? In this problem, the next jump depends on all past states, because each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming is often difficult to solve. -In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually choose to use other methods, such as heuristic search, genetic algorithms, reinforcement learning, etc., to obtain usable local optimal solutions within a limited time. +In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually use other methods, such as heuristic search, genetic algorithms, and reinforcement learning, to obtain usable locally optimal solutions within a limited time. diff --git a/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md index 0e3823d4e..135b237f3 100644 --- a/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -9,7 +9,7 @@ The previous two sections introduced the main characteristics of dynamic program 1. How to determine whether a problem is a dynamic programming problem? 2. What is the complete process for solving a dynamic programming problem, and where should we start? -## 14.3.1   Problem Determination +## 14.3.1   Problem Identification Generally speaking, if a problem contains overlapping subproblems, optimal substructure, and satisfies no aftereffects, then it is usually suitable for solving with dynamic programming. However, it is difficult to directly extract these characteristics from the problem description. Therefore, we usually relax the conditions and **first observe whether the problem is suitable for solving with backtracking (exhaustive search)**. @@ -17,17 +17,17 @@ Generally speaking, if a problem contains overlapping subproblems, optimal subst In other words, if a problem contains an explicit concept of decisions, and the solution is generated through a series of decisions, then it satisfies the decision tree model and can usually be solved using backtracking. -On this basis, dynamic programming problems also have some "bonus points" for determination. +On this basis, dynamic programming problems also have some positive indicators. - The problem contains descriptions such as maximum (minimum) or most (least), indicating optimization. - The problem's state can be represented using a list, multi-dimensional matrix, or tree, and a state has a recurrence relation with its surrounding states. -Correspondingly, there are also some "penalty points". +Correspondingly, there are also some negative indicators. - The goal of the problem is to find all possible solutions, rather than finding the optimal solution. - The problem description has obvious permutation and combination characteristics, requiring the return of specific multiple solutions. -If a problem satisfies the decision tree model and has relatively obvious "bonus points", we can assume it is a dynamic programming problem and verify it during the solving process. +If a problem satisfies the decision tree model and has relatively obvious positive indicators, we can assume it is a dynamic programming problem and verify that assumption during the solving process. ## 14.3.2   Problem-Solving Steps @@ -37,7 +37,7 @@ To illustrate the problem-solving steps more vividly, we use a classic problem " !!! question - Given an $n \times m$ two-dimensional grid `grid`, where each cell in the grid contains a non-negative integer representing the cost of that cell. A robot starts from the top-left cell and can only move down or right at each step until reaching the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right. + Given an $n \times m$ two-dimensional grid `grid` in which each cell contains a non-negative integer representing its cost, a robot starts from the top-left cell and can only move down or right at each step until reaching the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right. Figure 14-10 shows an example where the minimum path sum for the given grid is $13$. @@ -65,7 +65,7 @@ From this, we obtain the two-dimensional $dp$ matrix shown in Figure 14-11, whos **Step 2: Identify the optimal substructure, and then derive the state transition equation** -For state $[i, j]$, it can only be transferred from the cell above $[i-1, j]$ or the cell to the left $[i, j-1]$. Therefore, the optimal substructure is: the minimum path sum to reach $[i, j]$ is determined by the smaller of the minimum path sums of $[i, j-1]$ and $[i-1, j]$. +For state $[i, j]$, it can only transition from the cell above $[i-1, j]$ or the cell to the left $[i, j-1]$. Therefore, the optimal substructure is: the minimum path sum to reach $[i, j]$ is determined by the smaller of the minimum path sums of $[i, j-1]$ and $[i-1, j]$. Based on the above analysis, the state transition equation shown in Figure 14-12 can be derived: @@ -87,7 +87,7 @@ $$ In this problem, states in the first row can only come from the state to their left, and states in the first column can only come from the state above them. Therefore, the first row $i = 0$ and first column $j = 0$ are boundary conditions. -As shown in Figure 14-13, since each cell is transferred from the cell to its left and the cell above it, we use loops to traverse the matrix, with the outer loop traversing rows and the inner loop traversing columns. +As shown in Figure 14-13, since each cell transitions from the cell to its left and the cell above it, we use loops to traverse the matrix, with the outer loop traversing rows and the inner loop traversing columns. ![Boundary conditions and state transition order](dp_solution_pipeline.assets/min_path_sum_solution_initial_state.png){ class="animation-figure" } @@ -95,7 +95,7 @@ As shown in Figure 14-13, since each cell is transferred from the cell to its le !!! note - Boundary conditions in dynamic programming are used to initialize the $dp$ table, and in search are used for pruning. + Boundary conditions in dynamic programming are used to initialize the $dp$ table, while in search they are used for pruning. The core of state transition order is to ensure that when computing the solution to the current problem, all the smaller subproblems it depends on have already been computed correctly. @@ -103,7 +103,7 @@ Based on the above analysis, we can directly write the dynamic programming code. ### 1.   Method 1: Brute Force Search -Starting from state $[i, j]$, continuously decompose into smaller states $[i-1, j]$ and $[i, j-1]$. The recursive function includes the following elements. +Starting from state $[i, j]$, we continuously decompose it into smaller states $[i-1, j]$ and $[i, j-1]$. The recursive function includes the following elements. - **Recursive parameters**: state $[i, j]$. - **Return value**: minimum path sum from $[0, 0]$ to $[i, j]$, which is $dp[i, j]$. diff --git a/en/docs/chapter_dynamic_programming/edit_distance_problem.md b/en/docs/chapter_dynamic_programming/edit_distance_problem.md index b61048fa6..8846a269b 100644 --- a/en/docs/chapter_dynamic_programming/edit_distance_problem.md +++ b/en/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -18,7 +18,7 @@ As shown in Figure 14-27, transforming `kitten` into `sitting` requires 3 edits,

Figure 14-27   Example data for edit distance

-**The edit distance problem can be naturally explained using the decision tree model**. Strings correspond to tree nodes, and a round of decision (one edit operation) corresponds to an edge of the tree. +**The edit distance problem can be naturally explained using the decision tree model**. Strings correspond to tree nodes, and each edit operation corresponds to an edge in the tree. As shown in Figure 14-28, without restricting operations, each node can branch into many edges, with each edge corresponding to one operation, meaning there are many possible paths to transform `hello` into `algo`. @@ -34,7 +34,7 @@ From the perspective of the decision tree, the goal of this problem is to find t Each round of decision involves performing one edit operation on string $s$. -We want the problem scale to gradually decrease during the editing process, which allows us to construct subproblems. Let the lengths of strings $s$ and $t$ be $n$ and $m$ respectively. We first consider the tail characters of the two strings, $s[n-1]$ and $t[m-1]$. +We want the problem size to gradually decrease during the editing process so that we can construct subproblems. Let the lengths of strings $s$ and $t$ be $n$ and $m$ respectively. We first consider the tail characters of the two strings, $s[n-1]$ and $t[m-1]$. - If $s[n-1]$ and $t[m-1]$ are the same, we can skip them and directly consider $s[n-2]$ and $t[m-2]$. - If $s[n-1]$ and $t[m-1]$ are different, we need to perform one edit on $s$ (insert, delete, or replace) to make the tail characters of the two strings the same, allowing us to skip them and consider a smaller-scale problem. @@ -57,7 +57,7 @@ Consider subproblem $dp[i, j]$, where the tail characters of the corresponding t

Figure 14-29   State transition for edit distance

-Based on the above analysis, the optimal substructure can be obtained: the minimum number of edits for $dp[i, j]$ equals the minimum among the minimum edit steps of $dp[i, j-1]$, $dp[i-1, j]$, and $dp[i-1, j-1]$, plus the edit step $1$ for this time. The corresponding state transition equation is: +Based on the above analysis, we obtain the optimal substructure: the minimum number of edits for $dp[i, j]$ equals the minimum of $dp[i, j-1]$, $dp[i-1, j]$, and $dp[i-1, j-1]$, plus the current edit cost of $1$. The corresponding state transition equation is: $$ dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 @@ -477,7 +477,7 @@ Observing the state transition equation, the solution $dp[i, j]$ depends on solu end ``` -As shown in Figure 14-30, the state transition process for the edit distance problem is very similar to the knapsack problem and can both be viewed as the process of filling a two-dimensional grid. +As shown in Figure 14-30, the state transition process for the edit distance problem is very similar to that of the knapsack problem; both can be viewed as the process of filling a two-dimensional grid. === "<1>" ![Dynamic programming process for edit distance](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" } @@ -528,9 +528,9 @@ As shown in Figure 14-30, the state transition process for the edit distance pro ### 3.   Space Optimization -Since $dp[i, j]$ is transferred from the solutions above $dp[i-1, j]$, to the left $dp[i, j-1]$, and to the upper-left $dp[i-1, j-1]$, forward traversal will lose the upper-left solution $dp[i-1, j-1]$, and reverse traversal cannot build $dp[i, j-1]$ in advance, so neither traversal order is feasible. +Since $dp[i, j]$ depends on the states above $dp[i-1, j]$, to the left $dp[i, j-1]$, and at the upper-left $dp[i-1, j-1]$, forward traversal will lose the upper-left state $dp[i-1, j-1]$, while reverse traversal cannot construct $dp[i, j-1]$ in advance, so neither traversal order is suitable. -For this reason, we can use a variable `leftup` to temporarily store the upper-left solution $dp[i-1, j-1]$, so we only need to consider the solutions to the left and above. This situation is the same as the unbounded knapsack problem, allowing for forward traversal. The code is as follows: +For this reason, we can use a variable `leftup` to temporarily store the upper-left solution $dp[i-1, j-1]$, so we only need to consider the solutions to the left and above. This situation is the same as in the unbounded knapsack problem, so we can use forward traversal. The code is as follows: === "Python" diff --git a/en/docs/chapter_dynamic_programming/index.md b/en/docs/chapter_dynamic_programming/index.md index cf051aea8..8381a5348 100644 --- a/en/docs/chapter_dynamic_programming/index.md +++ b/en/docs/chapter_dynamic_programming/index.md @@ -9,9 +9,9 @@ icon: material/table-pivot !!! abstract - Streams converge into rivers, rivers converge into the sea. + Streams flow into rivers, rivers flow into the sea. - Dynamic programming gathers solutions to small problems into answers to large problems, step by step guiding us to the shore of problem-solving. + Dynamic programming combines solutions to small problems into the answer to a large problem, leading us step by step to the other shore of problem-solving. ## Chapter contents diff --git a/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md index 9d4d1900c..224e20abd 100644 --- a/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -18,7 +18,7 @@ As shown in Figure 14-1, for a $3$-step staircase, there are $3$ different ways

Figure 14-1   Number of ways to reach the 3rd step

-The goal of this problem is to find the number of ways, **we can consider using backtracking to enumerate all possibilities**. Specifically, imagine climbing stairs as a multi-round selection process: starting from the ground, choosing to go up $1$ or $2$ steps in each round, incrementing the count by $1$ whenever the top of the stairs is reached, and pruning when exceeding the top. The code is as follows: +The goal of this problem is to determine the number of ways, so **we can consider using backtracking to enumerate all possibilities**. Specifically, imagine climbing stairs as a multi-round selection process: starting from the ground, choosing to go up $1$ or $2$ steps in each round, incrementing the count by $1$ whenever the top of the stairs is reached, and pruning when exceeding the top. The code is as follows: === "Python" @@ -438,7 +438,7 @@ $$ dp[i] = dp[i-1] + dp[i-2] $$ -This means that in the stair climbing problem, there exists a recurrence relation among the subproblems, **the solution to the original problem can be constructed from the solutions to the subproblems**. Figure 14-2 illustrates this recurrence relation. +This means that in the stair climbing problem, there exists a recurrence relation among the subproblems, and **the solution to the original problem can be constructed from the solutions to the subproblems**. Figure 14-2 illustrates this recurrence relation. ![Recurrence relation for the number of ways](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png){ class="animation-figure" } @@ -446,7 +446,7 @@ This means that in the stair climbing problem, there exists a recurrence relatio We can obtain a brute force search solution based on the recurrence formula. Starting from $dp[n]$, **recursively decompose a larger problem into the sum of two smaller problems**, until reaching the smallest subproblems $dp[1]$ and $dp[2]$ and returning. Among them, the solutions to the smallest subproblems are known, namely $dp[1] = 1$ and $dp[2] = 2$, representing $1$ and $2$ ways to climb to the $1$st and $2$nd steps, respectively. -Observe the following code, which, like standard backtracking code, belongs to depth-first search but is more concise: +Observe the following code: like standard backtracking code, it also uses depth-first search but is more concise: === "Python" @@ -690,7 +690,7 @@ Observe the following code, which, like standard backtracking code, belongs to d end ``` -Figure 14-3 shows the recursion tree formed by brute force search. For the problem $dp[n]$, the depth of its recursion tree is $n$, with a time complexity of $O(2^n)$. Exponential order represents explosive growth; if we input a relatively large $n$, we will fall into a long wait. +Figure 14-3 shows the recursion tree formed by brute force search. For the problem $dp[n]$, the depth of its recursion tree is $n$, with a time complexity of $O(2^n)$. Exponential growth is explosive; if we input a relatively large $n$, the wait can be very long. ![Recursion tree for climbing stairs](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png){ class="animation-figure" } @@ -1052,7 +1052,7 @@ The code is as follows: end ``` -Observe Figure 14-4, **after memoization, all overlapping subproblems only need to be computed once, optimizing the time complexity to $O(n)$**, which is a tremendous leap. +Observe Figure 14-4: **after memoization, all overlapping subproblems need to be computed only once, reducing the time complexity to $O(n)$**, which is a tremendous leap. ![Recursion tree with memoization](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png){ class="animation-figure" } @@ -1339,7 +1339,7 @@ Based on the above content, we can summarize the commonly used terminology in dy ## 14.1.4   Space Optimization -Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i-1]$ and $dp[i-2]$, we do not need to use an array `dp` to store the solutions to all subproblems**, but can simply use two variables to roll forward. The code is as follows: +Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i-1]$ and $dp[i-2]$, we do not need to use an array `dp` to store the solutions to all subproblems**, and can instead use two variables that roll forward. The code is as follows: === "Python" @@ -1555,6 +1555,6 @@ Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i end ``` -Observing the above code, since the space occupied by the array `dp` is saved, the space complexity is reduced from $O(n)$ to $O(1)$. +As the above code shows, by eliminating the space occupied by the array `dp`, the space complexity is reduced from $O(n)$ to $O(1)$. In dynamic programming problems, the current state often depends only on a limited number of preceding states, allowing us to retain only the necessary states and save memory space through "dimension reduction". **This space optimization technique is called "rolling variable" or "rolling array"**. diff --git a/en/docs/chapter_dynamic_programming/knapsack_problem.md b/en/docs/chapter_dynamic_programming/knapsack_problem.md index 9a378de94..f9d3f9a7f 100644 --- a/en/docs/chapter_dynamic_programming/knapsack_problem.md +++ b/en/docs/chapter_dynamic_programming/knapsack_problem.md @@ -10,7 +10,7 @@ In this section, we will first solve the most common 0-1 knapsack problem. !!! question - Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. Each item can only be selected once. What is the maximum value that can be placed in the knapsack within the capacity limit? + Given $n$ items and a knapsack with capacity $cap$, where the weight and value of the $i$-th item are $wgt[i-1]$ and $val[i-1]$, respectively. Each item can be selected at most once. What is the maximum value that can fit in the knapsack under the capacity limit? Observe Figure 14-17. Since item number $i$ starts counting from $1$ and array indices start from $0$, item $i$ corresponds to weight $wgt[i-1]$ and value $val[i-1]$. @@ -37,7 +37,7 @@ After making the decision for item $i$, what remains is the subproblem of the fi - **Not putting item $i$**: The knapsack capacity remains unchanged, and the state changes to $[i-1, c]$. - **Putting item $i$**: The knapsack capacity decreases by $wgt[i-1]$, the value increases by $val[i-1]$, and the state changes to $[i-1, c-wgt[i-1]]$. -The above analysis reveals the optimal substructure of this problem: **the maximum value $dp[i, c]$ equals the larger value between not putting item $i$ and putting item $i$**. From this, the state transition equation can be derived: +The above analysis reveals the optimal substructure of this problem: **the maximum value $dp[i, c]$ equals the greater of the values obtained by not putting item $i$ into the knapsack and by putting it into the knapsack**. From this, the state transition equation can be derived: $$ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) @@ -49,7 +49,7 @@ Note that if the weight of the current item $wgt[i - 1]$ exceeds the remaining k When there are no items or the knapsack capacity is $0$, the maximum value is $0$, i.e., the first column $dp[i, 0]$ and the first row $dp[0, c]$ are both equal to $0$. -The current state $[i, c]$ is transferred from the state above $[i-1, c]$ and the state in the upper-left $[i-1, c-wgt[i-1]]$, so the entire $dp$ table is traversed in order through two nested loops. +The current state $[i, c]$ transitions from the state above $[i-1, c]$ and the upper-left state $[i-1, c-wgt[i-1]]$, so we can traverse the entire $dp$ table in forward order using two nested loops. Based on the above analysis, we will next implement the brute force search, memoization, and dynamic programming solutions in order. @@ -59,7 +59,7 @@ The search code includes the following elements. - **Recursive parameters**: state $[i, c]$. - **Return value**: solution to the subproblem $dp[i, c]$. -- **Termination condition**: when the item number is out of bounds $i = 0$ or the remaining knapsack capacity is $0$, terminate recursion and return value $0$. +- **Termination condition**: when there are no items left ($i = 0$) or the remaining knapsack capacity is $0$, terminate the recursion and return value $0$. - **Pruning**: if the weight of the current item exceeds the remaining knapsack capacity, only the option of not putting it in is available. === "Python" @@ -338,7 +338,7 @@ The search code includes the following elements. end ``` -As shown in Figure 14-18, since each item generates two search branches of not selecting and selecting, the time complexity is $O(2^n)$. +As shown in Figure 14-18, since each item generates two search branches, excluding it and including it, the time complexity is $O(2^n)$. Observing the recursion tree, it is easy to see overlapping subproblems, such as $dp[1, 10]$. When there are many items, large knapsack capacity, and especially many items with the same weight, the number of overlapping subproblems will increase significantly. diff --git a/en/docs/chapter_dynamic_programming/summary.md b/en/docs/chapter_dynamic_programming/summary.md index f7bded759..14e8e2445 100644 --- a/en/docs/chapter_dynamic_programming/summary.md +++ b/en/docs/chapter_dynamic_programming/summary.md @@ -4,7 +4,7 @@ comments: true # 14.7   Summary -### 1.   Key Review +### 1.   Key Points - Dynamic programming decomposes problems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving computational efficiency. - Without considering time constraints, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree contains a large number of overlapping subproblems, resulting in extremely low efficiency. By introducing a memo list, we can store the solutions to all computed subproblems, ensuring that overlapping subproblems are only computed once. @@ -12,12 +12,12 @@ comments: true - Subproblem decomposition is a general algorithmic approach, with different properties in divide and conquer, dynamic programming, and backtracking. - Dynamic programming problems have three major characteristics: overlapping subproblems, optimal substructure, and no aftereffects. - If the optimal solution to the original problem can be constructed from the optimal solutions to the subproblems, then it has optimal substructure. -- No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not have no aftereffects and cannot be quickly solved using dynamic programming. +- No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not satisfy this property and cannot be solved efficiently using dynamic programming. **Knapsack problem** - The knapsack problem is one of the most typical dynamic programming problems, with variants such as the 0-1 knapsack, unbounded knapsack, and multiple knapsack. -- The state definition for the 0-1 knapsack is the maximum value among the first $i$ items in a knapsack of capacity $c$. Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state. +- The state definition for the 0-1 knapsack is the maximum value achievable using the first $i$ items with a knapsack capacity of $c$. Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state. - The unbounded knapsack problem has no limit on the selection quantity of each type of item, so the state transition for choosing to put in an item differs from the 0-1 knapsack problem. Since the state depends on the state directly above and directly to the left, space optimization should use forward traversal. - The coin change problem is a variant of the unbounded knapsack problem. It changes from seeking the "maximum" value to seeking the "minimum" number of coins, so $\max()$ in the state transition equation should be changed to $\min()$. It changes from seeking "not exceeding" the knapsack capacity to seeking "exactly" making up the target amount, so $amt + 1$ is used to represent the invalid solution of "unable to make up the target amount". - Coin change problem II changes from seeking the "minimum number of coins" to seeking the "number of coin combinations", so the state transition equation correspondingly changes from $\min()$ to a summation operator. diff --git a/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md index 559237fbd..842d75f02 100644 --- a/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -751,7 +751,7 @@ The two-dimensional $dp$ table has size $(n+1) \times (amt+1)$. This problem differs from the unbounded knapsack problem in the following two aspects regarding the state transition equation. - This problem seeks the minimum value, so the operator $\max()$ needs to be changed to $\min()$. -- The optimization target is the number of coins rather than item value, so when a coin is selected, simply execute $+1$. +- The optimization target is the number of coins rather than item value, so when a coin is selected, simply add $1$. $$ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) @@ -765,7 +765,7 @@ When there are no coins, **it is impossible to make up any amount $> 0$**, which ### 2.   Code Implementation -Most programming languages do not provide a $+ \infty$ variable, and can only use the maximum value of integer type `int` as a substitute. However, this can lead to large number overflow: the $+ 1$ operation in the state transition equation may cause overflow. +Most programming languages do not provide a $+ \infty$ variable, and can only use the maximum value of integer type `int` as a substitute. However, this can lead to integer overflow: the $+ 1$ operation in the state transition equation may cause overflow. For this reason, we use the number $amt + 1$ to represent invalid solutions, because the maximum number of coins needed to make up $amt$ is at most $amt$. Before returning, check whether $dp[n, amt]$ equals $amt + 1$; if so, return $-1$, indicating that the target amount cannot be made up. The code is as follows: @@ -1572,7 +1572,7 @@ The space optimization for the coin change problem is handled in the same way as end ``` -## 14.5.3   Coin Change Problem Ii +## 14.5.3   Coin Change Problem II !!! question diff --git a/en/docs/chapter_graph/graph.md b/en/docs/chapter_graph/graph.md index 7f3c7cb19..9d6f06657 100644 --- a/en/docs/chapter_graph/graph.md +++ b/en/docs/chapter_graph/graph.md @@ -14,7 +14,7 @@ G & = \{ V, E \} \newline \end{aligned} $$ -If we view vertices as nodes and edges as references (pointers) connecting the nodes, we can see graphs as a data structure extended from linked lists. As shown in Figure 9-1, **compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex**. +If we view vertices as nodes and edges as references (pointers) connecting them, we can regard a graph as an extension of the linked list data structure. As shown in Figure 9-1, **compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex**. ![Relationships among linked lists, trees, and graphs](graph.assets/linkedlist_tree_graph.png){ class="animation-figure" } @@ -24,8 +24,8 @@ If we view vertices as nodes and edges as references (pointers) connecting the n Graphs can be divided into undirected graphs and directed graphs based on whether edges have direction, as shown in Figure 9-2. -- In undirected graphs, edges represent a "bidirectional" connection between two vertices, such as the "friend relationship" on WeChat or QQ. -- In directed graphs, edges have directionality, meaning edges $A \rightarrow B$ and $A \leftarrow B$ are independent of each other, such as the "follow" and "be followed" relationships on Weibo or TikTok. +- In undirected graphs, edges represent a "bidirectional" connection between two vertices, such as friendships on WeChat or QQ. +- In directed graphs, edges have directionality, meaning edges $A \rightarrow B$ and $A \leftarrow B$ are independent of each other, such as following and follower relationships on Weibo or TikTok. ![Directed and undirected graphs](graph.assets/directed_graph.png){ class="animation-figure" } @@ -40,7 +40,7 @@ Graphs can be divided into connected graphs and disconnected graphs Figure 9-3   Connected and disconnected graphs

-We can also add a "weight" variable to edges, resulting in weighted graphs as shown in Figure 9-4. For example, in mobile games like "Honor of Kings", the system calculates the "intimacy" between players based on their shared game time, and such intimacy networks can be represented using weighted graphs. +We can also add a "weight" variable to edges, resulting in weighted graphs as shown in Figure 9-4. For example, in mobile games like "Honor of Kings", the system calculates the "intimacy" between players based on how long they have played together, and such intimacy networks can be represented using weighted graphs. ![Weighted and unweighted graphs](graph.assets/weighted_graph.png){ class="animation-figure" } @@ -50,7 +50,7 @@ Graph data structures include the following commonly used terms. - Adjacency: When two vertices are connected by an edge, these two vertices are said to be "adjacent". In Figure 9-4, the adjacent vertices of vertex 1 are vertices 2, 3, and 5. - Path: The sequence of edges from vertex A to vertex B is called a "path" from A to B. In Figure 9-4, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4. -- Degree: The number of edges a vertex has. For directed graphs, in-degree indicates how many edges point to the vertex, and out-degree indicates how many edges point out from the vertex. +- Degree: The number of edges a vertex has. For directed graphs, in-degree indicates how many edges point to the vertex, and out-degree indicates how many edges leave the vertex. ## 9.1.2   Representation of Graphs @@ -70,7 +70,7 @@ Adjacency matrices have the following properties. - In simple graphs, vertices cannot connect to themselves, so the elements on the main diagonal of the adjacency matrix are meaningless. - For undirected graphs, edges in both directions are equivalent, so the adjacency matrix is symmetric about the main diagonal. -- Replacing the elements of the adjacency matrix from $1$ and $0$ to weights allows representation of weighted graphs. +- Replacing the $1$ and $0$ entries in the adjacency matrix with weights allows it to represent weighted graphs. When using adjacency matrices to represent graphs, we can directly access matrix elements to obtain edges, resulting in highly efficient addition, deletion, lookup, and modification operations, all with a time complexity of $O(1)$. However, the space complexity of the matrix is $O(n^2)$, which consumes significant memory. @@ -82,9 +82,9 @@ An adjacency list uses $n$ linked lists to represent a graph, with linked

Figure 9-6   Adjacency list representation of a graph

-Adjacency lists only store edges that actually exist, and the total number of edges is typically much less than $n^2$, making them more space-efficient. However, finding edges in an adjacency list requires traversing the linked list, so its time efficiency is inferior to that of adjacency matrices. +Adjacency lists only store edges that actually exist, and the total number of edges is typically much less than $n^2$, making them more space-efficient. However, finding edges in an adjacency list requires traversing the linked list, so it is less time-efficient than an adjacency matrix. -Observing Figure 9-6, **the structure of adjacency lists is very similar to "chaining" in hash tables, so we can adopt similar methods to optimize efficiency**. For example, when linked lists are long, they can be converted to AVL trees or red-black trees, thereby optimizing time efficiency from $O(n)$ to $O(\log n)$; linked lists can also be converted to hash tables, thereby reducing time complexity to $O(1)$. +As shown in Figure 9-6, **the structure of adjacency lists is very similar to separate chaining in hash tables, so we can use similar methods to improve efficiency**. For example, when a linked list becomes long, it can be converted into an AVL tree or red-black tree, improving the time complexity from $O(n)$ to $O(\log n)$; it can also be converted into a hash table, reducing the time complexity to $O(1)$. ## 9.1.3   Common Applications of Graphs diff --git a/en/docs/chapter_graph/graph_operations.md b/en/docs/chapter_graph/graph_operations.md index c0e53bbe1..3cf26af89 100644 --- a/en/docs/chapter_graph/graph_operations.md +++ b/en/docs/chapter_graph/graph_operations.md @@ -4,7 +4,7 @@ comments: true # 9.2   Basic Operations on Graphs -Basic operations on graphs can be divided into operations on "edges" and operations on "vertices". Under the two representation methods of "adjacency matrix" and "adjacency list", the implementation methods differ. +Basic operations on graphs can be divided into operations on "edges" and operations on "vertices". Their implementations differ depending on whether the graph is represented as an "adjacency matrix" or an "adjacency list". ## 9.2.1   Implementation Based on Adjacency Matrix @@ -13,21 +13,21 @@ Given an undirected graph with $n$ vertices, the various operations are implemen - **Adding or removing an edge**: Directly modify the specified edge in the adjacency matrix, using $O(1)$ time. Since it is an undirected graph, both directions of the edge need to be updated simultaneously. - **Adding a vertex**: Add a row and a column at the end of the adjacency matrix and fill them all with $0$s, using $O(n)$ time. - **Removing a vertex**: Delete a row and a column in the adjacency matrix. The worst case occurs when removing the first row and column, requiring $(n-1)^2$ elements to be "moved up and to the left", thus using $O(n^2)$ time. -- **Initialization**: Pass in $n$ vertices, initialize a vertex list `vertices` of length $n$, using $O(n)$ time; initialize an adjacency matrix `adjMat` of size $n \times n$, using $O(n^2)$ time. +- **Initialization**: Given $n$ vertices, initialize a vertex list `vertices` of length $n$, using $O(n)$ time; initialize an adjacency matrix `adjMat` of size $n \times n$, using $O(n^2)$ time. -=== "Initialize adjacency matrix" +=== "<1>" ![Initialization, adding and removing edges, adding and removing vertices in adjacency matrix](graph_operations.assets/adjacency_matrix_step1_initialization.png){ class="animation-figure" } -=== "Add an edge" +=== "<2>" ![adjacency_matrix_add_edge](graph_operations.assets/adjacency_matrix_step2_add_edge.png){ class="animation-figure" } -=== "Remove an edge" +=== "<3>" ![adjacency_matrix_remove_edge](graph_operations.assets/adjacency_matrix_step3_remove_edge.png){ class="animation-figure" } -=== "Add a vertex" +=== "<4>" ![adjacency_matrix_add_vertex](graph_operations.assets/adjacency_matrix_step4_add_vertex.png){ class="animation-figure" } -=== "Remove a vertex" +=== "<5>" ![adjacency_matrix_remove_vertex](graph_operations.assets/adjacency_matrix_step5_remove_vertex.png){ class="animation-figure" }

Figure 9-7   Initialization, adding and removing edges, adding and removing vertices in adjacency matrix

@@ -1212,33 +1212,33 @@ Given an undirected graph with a total of $n$ vertices and $m$ edges, the variou - **Adding an edge**: Add the edge at the end of the corresponding vertex's linked list, using $O(1)$ time. Since it is an undirected graph, edges in both directions need to be added simultaneously. - **Removing an edge**: Find and remove the specified edge in the corresponding vertex's linked list, using $O(m)$ time. In an undirected graph, edges in both directions need to be removed simultaneously. -- **Adding a vertex**: Add a linked list in the adjacency list and set the new vertex as the head node of the list, using $O(1)$ time. +- **Adding a vertex**: Add a linked list to the adjacency list, with the new vertex as the head node, using $O(1)$ time. - **Removing a vertex**: Traverse the entire adjacency list and remove all edges containing the specified vertex, using $O(n + m)$ time. - **Initialization**: Create $n$ vertices and $2m$ edges in the adjacency list, using $O(n + m)$ time. -=== "Initialize adjacency list" +=== "<1>" ![Initialization, adding and removing edges, adding and removing vertices in adjacency list](graph_operations.assets/adjacency_list_step1_initialization.png){ class="animation-figure" } -=== "Add an edge" +=== "<2>" ![adjacency_list_add_edge](graph_operations.assets/adjacency_list_step2_add_edge.png){ class="animation-figure" } -=== "Remove an edge" +=== "<3>" ![adjacency_list_remove_edge](graph_operations.assets/adjacency_list_step3_remove_edge.png){ class="animation-figure" } -=== "Add a vertex" +=== "<4>" ![adjacency_list_add_vertex](graph_operations.assets/adjacency_list_step4_add_vertex.png){ class="animation-figure" } -=== "Remove a vertex" +=== "<5>" ![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_step5_remove_vertex.png){ class="animation-figure" }

Figure 9-8   Initialization, adding and removing edges, adding and removing vertices in adjacency list

-The following is the adjacency list code implementation. Compared to Figure 9-8, the actual code has the following differences. +The following code shows the adjacency list implementation. Compared with Figure 9-8, the actual code differs in the following ways. - For convenience in adding and removing vertices, and to simplify the code, we use lists (dynamic arrays) instead of linked lists. - A hash table is used to store the adjacency list, where `key` is the vertex instance and `value` is the list (linked list) of adjacent vertices for that vertex. -Additionally, we use the `Vertex` class to represent vertices in the adjacency list. The reason for this is: if we used list indices to distinguish different vertices as with adjacency matrices, then to delete the vertex at index $i$, we would need to traverse the entire adjacency list and decrement all indices greater than $i$ by $1$, which is very inefficient. However, if each vertex is a unique `Vertex` instance, deleting a vertex does not require modifying other vertices. +Additionally, we use the `Vertex` class to represent vertices in the adjacency list for the following reason: if we used list indices to distinguish different vertices, as with adjacency matrices, then to delete the vertex at index $i$, we would need to traverse the entire adjacency list and decrement all indices greater than $i$ by $1$, which is very inefficient. However, if each vertex is a unique `Vertex` instance, deleting one vertex does not require modifying the others. === "Python" @@ -2361,7 +2361,7 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l ## 9.2.3   Efficiency Comparison -Assuming the graph has $n$ vertices and $m$ edges, Table 9-2 compares the time efficiency and space efficiency of adjacency matrices and adjacency lists. Note that the adjacency list (linked list) corresponds to the implementation in this text, while the adjacency list (hash table) refers specifically to the implementation where all linked lists are replaced with hash tables. +Assuming the graph has $n$ vertices and $m$ edges, Table 9-2 compares the time efficiency and space efficiency of adjacency matrices and adjacency lists. Note that the adjacency list (linked list) corresponds to the implementation used in this section, while the adjacency list (hash table) refers specifically to the implementation where all linked lists are replaced with hash tables.

Table 9-2   Comparison of adjacency matrix and adjacency list

diff --git a/en/docs/chapter_graph/graph_traversal.md b/en/docs/chapter_graph/graph_traversal.md index 4b9c4c698..c67e218ee 100644 --- a/en/docs/chapter_graph/graph_traversal.md +++ b/en/docs/chapter_graph/graph_traversal.md @@ -10,7 +10,7 @@ Both graphs and trees require the application of search algorithms to implement ## 9.3.1   Breadth-First Search -**Breadth-first search is a near-to-far traversal method that, starting from a certain node, always prioritizes visiting the nearest vertices and expands outward layer by layer**. As shown in Figure 9-9, starting from the top-left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited. +**Breadth-first search proceeds from near to far: starting from a given node, it always visits the nearest vertices first and expands outward layer by layer**. As shown in Figure 9-9, starting from the top-left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited. ![Breadth-first search of a graph](graph_traversal.assets/graph_bfs.png){ class="animation-figure" } @@ -28,7 +28,7 @@ To prevent revisiting vertices, we use a hash set `visited` to record which node !!! tip - A hash set can be viewed as a hash table that stores only `key` without storing `value`. It can perform addition, deletion, lookup, and modification operations on `key` in $O(1)$ time complexity. Based on the uniqueness of `key`, hash sets are typically used for data deduplication and similar scenarios. + A hash set can be viewed as a hash table that stores only `key` without storing `value`. It supports insertion, deletion, lookup, and update operations on `key` in $O(1)$ time. Based on the uniqueness of `key`, hash sets are typically used for data deduplication and similar scenarios. === "Python" @@ -933,9 +933,9 @@ This "go as far as possible then return" algorithm paradigm is typically impleme The algorithm flow of depth-first search is shown in Figure 9-12. - **Straight dashed lines represent downward recursion**, indicating that a new recursive method has been initiated to visit a new vertex. -- **Curved dashed lines represent upward backtracking**, indicating that this recursive method has returned to the position where it was initiated. +- **Curved dashed lines represent upward backtracking**, indicating that this recursive call has returned to the point where it was made. -To deepen understanding, it is recommended to combine Figure 9-12 with the code to mentally simulate (or draw out) the entire DFS process, including when each recursive method is initiated and when it returns. +To deepen understanding, it is recommended to combine Figure 9-12 with the code to mentally simulate (or draw out) the entire DFS process, including when each recursive call begins and when it returns. === "<1>" ![Steps of depth-first search of a graph](graph_traversal.assets/graph_dfs_step1.png){ class="animation-figure" } @@ -974,7 +974,7 @@ To deepen understanding, it is recommended to combine Figure 9-12 with the code !!! question "Is the depth-first traversal sequence unique?" - Similar to breadth-first search, the order of depth-first traversal sequences is also not unique. Given a certain vertex, exploring in any direction first is valid, meaning the order of adjacent vertices can be arbitrarily shuffled, all being depth-first search. + Similar to breadth-first search, depth-first traversal sequences are also not unique. Given a vertex, any exploration direction may be chosen first; that is, the order of adjacent vertices can be arbitrarily rearranged and still constitute depth-first search. Taking tree traversal as an example, "root $\rightarrow$ left $\rightarrow$ right", "left $\rightarrow$ root $\rightarrow$ right", and "left $\rightarrow$ right $\rightarrow$ root" correspond to pre-order, in-order, and post-order traversals, respectively. They represent three different traversal priorities, yet all three belong to depth-first search. diff --git a/en/docs/chapter_graph/summary.md b/en/docs/chapter_graph/summary.md index ab9272a28..bed1cada4 100644 --- a/en/docs/chapter_graph/summary.md +++ b/en/docs/chapter_graph/summary.md @@ -7,16 +7,16 @@ comments: true ### 1.   Key Review - Graphs consist of vertices and edges and can be represented as a set of vertices and a set of edges. -- Compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex. -- Directed graphs have edges with directionality, connected graphs have all vertices reachable from any vertex, and weighted graphs have edges that each contain a weight variable. +- Compared with the linear relationships modeled by linked lists and the divide-and-conquer relationships modeled by trees, the network relationships modeled by graphs offer much greater flexibility and are therefore more complex. +- In directed graphs, edges have direction; in connected graphs, every vertex is reachable from any other vertex; and in weighted graphs, each edge carries a weight. - Adjacency matrices use matrices to represent graphs, where each row (column) represents a vertex, and matrix elements represent edges, using $1$ or $0$ to indicate whether two vertices have an edge or not. Adjacency matrices are highly efficient for addition, deletion, lookup, and modification operations, but consume significant space. -- Adjacency lists use multiple linked lists to represent graphs, where the $i$-th linked list corresponds to vertex $i$ and stores all adjacent vertices of that vertex. Adjacency lists are more space-efficient than adjacency matrices, but have lower time efficiency because they require traversing linked lists to find edges. +- Adjacency lists use multiple linked lists to represent a graph: the $i$-th linked list corresponds to vertex $i$ and stores all vertices adjacent to it. Compared with adjacency matrices, adjacency lists use less space, but edge lookups are less efficient because the linked list must be traversed. - When linked lists in adjacency lists become too long, they can be converted to red-black trees or hash tables, thereby improving lookup efficiency. - From an algorithmic perspective, adjacency matrices embody "trading space for time", while adjacency lists embody "trading time for space". - Graphs can be used to model various real-world systems, such as social networks and subway lines. - Trees are a special case of graphs, and tree traversal is a special case of graph traversal. -- Breadth-first search of graphs is a near-to-far, layer-by-layer expansion search method, typically implemented using a queue. -- Depth-first search of graphs is a search method that prioritizes going as far as possible and backtracks when no path remains, commonly implemented using recursion. +- Breadth-first search in graphs explores from near to far, expanding layer by layer, and is typically implemented with a queue. +- Depth-first search in graphs follows a path as deep as possible and backtracks when it can go no farther, and is commonly implemented with recursion. ### 2.   Q & A @@ -28,8 +28,8 @@ In this text, a path is viewed as a sequence of edges, not a sequence of vertice **Q**: In a disconnected graph, will there be unreachable vertices? -In a disconnected graph, starting from a certain vertex, at least one vertex cannot be reached. Traversing a disconnected graph requires setting multiple starting points to traverse all connected components of the graph. +In a disconnected graph, if you start from one vertex, at least one other vertex will be unreachable. To traverse a disconnected graph, you need multiple starting points so that all connected components are covered. -**Q**: In an adjacency list, is there a requirement for the order of "all vertices connected to that vertex"? +**Q**: In an adjacency list, is there any required ordering for the vertices adjacent to a given vertex? -It can be in any order. However, in practical applications, it may be necessary to sort according to specified rules, such as the order in which vertices were added, or the order of vertex values, which helps quickly find vertices "with certain extreme values". +They can appear in any order. In practice, however, they may need to be sorted according to specific rules, such as the order in which vertices were added or the order of vertex values, which helps when quickly finding a vertex with some extreme value. diff --git a/en/docs/chapter_greedy/fractional_knapsack_problem.md b/en/docs/chapter_greedy/fractional_knapsack_problem.md index 0970aa26d..dad1a5add 100644 --- a/en/docs/chapter_greedy/fractional_knapsack_problem.md +++ b/en/docs/chapter_greedy/fractional_knapsack_problem.md @@ -6,7 +6,7 @@ comments: true !!! question - Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. Each item can be selected only once, **but a portion of an item can be selected, with the value calculated based on the proportion of weight selected**, what is the maximum value of items in the knapsack under the limited capacity? An example is shown in Figure 15-3. + Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. Each item can be selected only once, **but a fraction of an item may be selected, with its value proportional to the selected weight**. What is the maximum total value that can be placed in the knapsack under the capacity constraint? An example is shown in Figure 15-3. ![Example data for the fractional knapsack problem](fractional_knapsack_problem.assets/fractional_knapsack_example.png){ class="animation-figure" } @@ -14,7 +14,7 @@ comments: true The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, with states including the current item $i$ and capacity $c$, and the goal being to maximize value under the limited knapsack capacity. -The difference is that this problem allows selecting only a portion of an item. As shown in Figure 15-4, **we can arbitrarily split items and calculate the corresponding value based on the weight proportion**. +The difference is that this problem allows selecting only a fraction of an item. As shown in Figure 15-4, **we can split an item arbitrarily and compute its value in proportion to the selected weight**. 1. For item $i$, its value per unit weight is $val[i-1] / wgt[i-1]$, referred to as unit value. 2. Suppose we put a portion of item $i$ with weight $w$ into the knapsack, then the value added to the knapsack is $w \times val[i-1] / wgt[i-1]$. @@ -25,7 +25,7 @@ The difference is that this problem allows selecting only a portion of an item. ### 1.   Greedy Strategy Determination -Maximizing the total value of items in the knapsack **is essentially maximizing the value per unit weight of items**. From this, we can derive the greedy strategy shown in Figure 15-5. +Maximizing the total value in the knapsack **essentially means prioritizing items with higher value per unit weight**. From this observation, we can derive the greedy strategy shown in Figure 15-5. 1. Sort items by unit value from high to low. 2. Iterate through all items, **greedily selecting the item with the highest unit value in each round**. @@ -37,7 +37,7 @@ Maximizing the total value of items in the knapsack **is essentially maximizing ### 2.   Code Implementation -We created an `Item` class to facilitate sorting items by unit value. We loop to make greedy selections, breaking when the knapsack is full and returning the solution: +We define an `Item` class so that items can be sorted by unit value. We then iterate through the sorted items greedily, stopping once the knapsack is full and returning the result: === "Python" @@ -531,7 +531,7 @@ We created an `Item` class to facilitate sorting items by unit value. We loop to end ``` -The time complexity of built-in sorting algorithms is usually $O(\log n)$, and the space complexity is usually $O(\log n)$ or $O(n)$, depending on the specific implementation of the programming language. +Built-in sorting algorithms usually take $O(n \log n)$ time, and their space complexity is usually $O(\log n)$ or $O(n)$, depending on the specific implementation of the programming language. Apart from sorting, in the worst case the entire item list needs to be traversed, **therefore the time complexity is $O(n)$**, where $n$ is the number of items. @@ -539,13 +539,13 @@ Since an `Item` object list is initialized, **the space complexity is $O(n)$**. ### 3.   Correctness Proof -Using proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm yields a maximum value of `res`, but this solution does not include item $x$. +We use proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm produces an optimal value `res`, but the resulting solution does not include item $x$. -Now remove a unit weight of any item from the knapsack and replace it with a unit weight of item $x$. Since item $x$ has the highest unit value, the total value after replacement will definitely be greater than `res`. **This contradicts the assumption that `res` is the optimal solution, proving that the optimal solution must include item $x$**. +Now remove one unit of weight from any item in the knapsack and replace it with one unit of weight from item $x$. Since item $x$ has the highest unit value, the total value after the replacement must be greater than `res`. **This contradicts the assumption that `res` is optimal, proving that any optimal solution must include item $x$**. -For other items in this solution, we can also construct the above contradiction. In summary, **items with greater unit value are always better choices**, which proves that the greedy strategy is effective. +We can construct the same contradiction for the other items in the solution as well. In summary, **items with higher unit value are always the better choice**, which proves that the greedy strategy is effective. -As shown in Figure 15-6, if we view item weight and item unit value as the horizontal and vertical axes of a two-dimensional chart respectively, then the fractional knapsack problem can be transformed into "finding the maximum area enclosed within a limited horizontal axis range". This analogy can help us understand the effectiveness of the greedy strategy from a geometric perspective. +As shown in Figure 15-6, if we treat item weight and unit value as the horizontal and vertical axes of a two-dimensional chart, then the fractional knapsack problem can be viewed as "finding the maximum area enclosed within a bounded interval on the horizontal axis." This analogy helps explain the effectiveness of the greedy strategy from a geometric perspective. ![Geometric representation of the fractional knapsack problem](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png){ class="animation-figure" } diff --git a/en/docs/chapter_greedy/greedy_algorithm.md b/en/docs/chapter_greedy/greedy_algorithm.md index b04c2d537..f73b7f64f 100644 --- a/en/docs/chapter_greedy/greedy_algorithm.md +++ b/en/docs/chapter_greedy/greedy_algorithm.md @@ -4,20 +4,20 @@ comments: true # 15.1   Greedy Algorithm -Greedy algorithm is a common algorithm for solving optimization problems. Its basic idea is to make the seemingly best choice at each decision stage of the problem, that is, to greedily make locally optimal decisions in hopes of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely applied in many practical problems. +Greedy algorithm is a common approach to solving optimization problems. Its basic idea is to choose the option that appears best at each decision stage, that is, to greedily make locally optimal decisions in the hope of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely used in many practical problems. Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as both relying on the optimal substructure property, but they work differently. - Dynamic programming considers all previous decisions when making the current decision, and uses solutions to past subproblems to construct the solution to the current subproblem. - Greedy algorithms do not consider past decisions, but instead make greedy choices moving forward, continually reducing the problem size until the problem is solved. -We will first understand how greedy algorithms work through the example problem "coin change". This problem has already been introduced in the "Complete Knapsack Problem" chapter, so I believe you are not unfamiliar with it. +We will first understand how greedy algorithms work through the example problem "coin change." This problem was already introduced in the "Complete Knapsack Problem" chapter, so it should already be familiar to you. !!! question - Given $n$ types of coins, where the denomination of the $i$-th type of coin is $coins[i - 1]$, and the target amount is $amt$, with each type of coin available for repeated selection, what is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return $-1$. + Given $n$ types of coins, where the denomination of the $i$-th type is $coins[i - 1]$, a target amount $amt$, and an unlimited number of coins of each type, what is the minimum number of coins needed to make up the target amount? If the target amount cannot be made up, return $-1$. -The greedy strategy adopted for this problem is shown in Figure 15-1. Given a target amount, **we greedily select the coin that is not greater than and closest to it**, and continuously repeat this step until the target amount is reached. +The greedy strategy for this problem is shown in Figure 15-1. Given a target amount, **we greedily choose the coin that does not exceed it and is closest to it**, repeating this step until the target amount is made up. ![Greedy strategy for coin change](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" } @@ -330,28 +330,28 @@ The implementation code is as follows: end ``` -You might exclaim: So clean! The greedy algorithm solves the coin change problem in about ten lines of code. +You may find yourself exclaiming, "So clean!" The greedy algorithm solves the coin change problem in only about ten lines of code. ## 15.1.1   Advantages and Limitations of Greedy Algorithms -**Greedy algorithms are not only straightforward and simple to implement, but are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy choice loops at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude smaller than the time complexity of the dynamic programming solution $O(n \times amt)$. +**Greedy algorithms are not only straightforward to apply and easy to implement, but are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy selection loop runs at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude lower than the time complexity of the dynamic programming solution, $O(n \times amt)$. -However, **for certain coin denomination combinations, greedy algorithms cannot find the optimal solution**. Figure 15-2 provides two examples. +However, **for some coin denomination sets, greedy algorithms cannot find the optimal solution**. Figure 15-2 shows two examples. -- **Positive example $coins = [1, 5, 10, 20, 50, 100]$**: With this coin combination, given any $amt$, the greedy algorithm can find the optimal solution. -- **Negative example $coins = [1, 20, 50]$**: Suppose $amt = 60$, the greedy algorithm can only find the combination $50 + 1 \times 10$, totaling $11$ coins, but dynamic programming can find the optimal solution $20 + 20 + 20$, requiring only $3$ coins. -- **Negative example $coins = [1, 49, 50]$**: Suppose $amt = 98$, the greedy algorithm can only find the combination $50 + 1 \times 48$, totaling $49$ coins, but dynamic programming can find the optimal solution $49 + 49$, requiring only $2$ coins. +- **Positive example $coins = [1, 5, 10, 20, 50, 100]$**: With this coin set, the greedy algorithm can find the optimal solution for any $amt$. +- **Counterexample $coins = [1, 20, 50]$**: Suppose $amt = 60$. The greedy algorithm can only find the combination $50 + 1 \times 10$, using $11$ coins in total, whereas dynamic programming can find the optimal solution $20 + 20 + 20$ using only $3$ coins. +- **Counterexample $coins = [1, 49, 50]$**: Suppose $amt = 98$. The greedy algorithm can only find the combination $50 + 1 \times 48$, using $49$ coins in total, whereas dynamic programming can find the optimal solution $49 + 49$ using only $2$ coins. ![Examples where greedy algorithms cannot find the optimal solution](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" }

Figure 15-2   Examples where greedy algorithms cannot find the optimal solution

-In other words, for the coin change problem, greedy algorithms cannot guarantee finding the global optimal solution, and may even find very poor solutions. It is better suited for solving with dynamic programming. +In other words, for the coin change problem, greedy algorithms cannot guarantee a globally optimal solution and may even produce very poor results. This problem is better solved with dynamic programming. -Generally, the applicability of greedy algorithms falls into the following two situations. +In general, greedy algorithms are applicable in the following two situations. -1. **Can guarantee finding the optimal solution**: In this situation, greedy algorithms are often the best choice, because they tend to be more efficient than backtracking and dynamic programming. -2. **Can find an approximate optimal solution**: Greedy algorithms are also applicable in this situation. For many complex problems, finding the global optimal solution is very difficult, and being able to find a suboptimal solution with high efficiency is also very good. +1. **The optimal solution can be guaranteed**: In this case, greedy algorithms are often the best choice because they tend to be more efficient than backtracking and dynamic programming. +2. **An approximately optimal solution can be found**: Greedy algorithms are also useful in this case. For many complex problems, finding the global optimal solution is very difficult, so efficiently finding a suboptimal solution is already a very good outcome. ## 15.1.2   Characteristics of Greedy Algorithms @@ -366,30 +366,30 @@ Optimal substructure has already been introduced in the "Dynamic Programming" ch We mainly explore methods for determining the greedy choice property. Although its description seems relatively simple, **in practice, for many problems, proving the greedy choice property is not easy**. -For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving it is quite difficult. If asked: **what conditions must a coin combination satisfy to be solvable using a greedy algorithm**? We often can only rely on intuition or examples to give an ambiguous answer, and find it difficult to provide a rigorous mathematical proof. +For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving that it holds is much harder. If asked, **under what conditions can a coin set be solved using a greedy algorithm**? We often can only rely on intuition or examples to give a vague answer, and it is difficult to provide a rigorous mathematical proof. !!! quote - There is a paper that presents an algorithm with $O(n^3)$ time complexity for determining whether a coin combination can use a greedy algorithm to find the optimal solution for any amount. + There is a paper that presents an $O(n^3)$ algorithm for determining whether a coin set can be solved optimally by a greedy algorithm for any amount. Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234. ## 15.1.3   Steps for Solving Problems with Greedy Algorithms -The problem-solving process for greedy problems can generally be divided into the following three steps. +The general process for solving greedy problems can be divided into the following three steps. -1. **Problem analysis**: Sort out and understand the problem characteristics, including state definition, optimization objectives, and constraints, etc. This step is also involved in backtracking and dynamic programming. -2. **Determine the greedy strategy**: Determine how to make greedy choices at each step. This strategy should be able to reduce the problem size at each step, ultimately solving the entire problem. -3. **Correctness proof**: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical proofs, such as mathematical induction or proof by contradiction. +1. **Problem analysis**: Sort out and understand the characteristics of the problem, including state definitions, optimization objectives, and constraints. This step also appears in backtracking and dynamic programming. +2. **Determine the greedy strategy**: Decide how to make a greedy choice at each step. This strategy should reduce the problem size step by step and ultimately solve the entire problem. +3. **Correctness proof**: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical tools such as induction or proof by contradiction. -Determining the greedy strategy is the core step in solving the problem, but it may not be easy to implement, mainly for the following reasons. +Determining the greedy strategy is the core step in solving such problems, but it may not be easy in practice, mainly for the following reasons. -- **Greedy strategies differ greatly between different problems**. For many problems, the greedy strategy is relatively straightforward, and we can derive it through some general thinking and attempts. However, for some complex problems, the greedy strategy may be very elusive, which really tests one's problem-solving experience and algorithmic ability. -- **Some greedy strategies are highly misleading**. When we confidently design a greedy strategy, write the solution code and submit it for testing, we may find that some test cases cannot pass. This is because the designed greedy strategy is only "partially correct", as exemplified by the coin change problem discussed above. +- **Greedy strategies vary greatly from problem to problem**. For many problems, the greedy strategy is fairly intuitive and can be derived through rough reasoning and experimentation. For some complex problems, however, the greedy strategy may be deeply hidden, which strongly tests one's problem-solving experience and algorithmic ability. +- **Some greedy strategies are highly deceptive**. We may confidently design a greedy strategy, write the solution code, and submit it, only to find that some test cases fail. This is because the designed greedy strategy is only "partially correct," as exemplified by the coin change problem discussed above. -To ensure correctness, we should rigorously mathematically prove the greedy strategy, **usually using proof by contradiction or mathematical induction**. +To ensure correctness, we should give a rigorous mathematical proof of the greedy strategy, **usually using proof by contradiction or mathematical induction**. -However, correctness proofs may also not be easy. If we have no clue, we usually choose to debug the code based on test cases, step by step modifying and verifying the greedy strategy. +However, correctness proofs can also be difficult. If we have no clear direction, we usually resort to debugging against test cases, revising and validating the greedy strategy step by step. ## 15.1.4   Typical Problems Solved by Greedy Algorithms diff --git a/en/docs/chapter_greedy/index.md b/en/docs/chapter_greedy/index.md index 44fdd7f33..95dac47ac 100644 --- a/en/docs/chapter_greedy/index.md +++ b/en/docs/chapter_greedy/index.md @@ -9,9 +9,9 @@ icon: material/head-heart-outline !!! abstract - Sunflowers turn toward the sun, constantly pursuing the maximum potential for their own growth. + Sunflowers turn toward the sun, always seeking the fullest growth possible. - Through rounds of simple choices, greedy strategies gradually lead to the best answer. + Through successive simple choices, greedy strategies gradually lead to the optimal solution. ## Chapter contents diff --git a/en/docs/chapter_greedy/max_capacity_problem.md b/en/docs/chapter_greedy/max_capacity_problem.md index 66c4ce9dd..e3c7a25cb 100644 --- a/en/docs/chapter_greedy/max_capacity_problem.md +++ b/en/docs/chapter_greedy/max_capacity_problem.md @@ -6,56 +6,56 @@ comments: true !!! question - Input an array $ht$, where each element represents the height of a vertical partition. Any two partitions in the array, along with the space between them, can form a container. + Given an array $ht$, where each element represents the height of a vertical partition. Any two partitions in the array, together with the space between them, can form a container. - The capacity of the container equals the product of height and width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions. + The capacity of the container equals the product of its height and width (that is, its area), where the height is determined by the shorter partition and the width is the difference between the array indices of the two partitions. - Please select two partitions in the array such that the capacity of the formed container is maximized, and return the maximum capacity. An example is shown in Figure 15-7. + Select two partitions in the array such that the capacity of the resulting container is maximized, and return that maximum capacity. An example is shown in Figure 15-7. ![Example data for the max capacity problem](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" }

Figure 15-7   Example data for the max capacity problem

-The container is formed by any two partitions, **therefore the state of this problem is the indices of two partitions, denoted as $[i, j]$**. +The container is formed by any two partitions, **so the state of this problem is the indices of the two partitions, denoted by $[i, j]$**. -According to the problem description, capacity equals height multiplied by width, where height is determined by the shorter partition, and width is the difference in array indices between the two partitions. Let the capacity be $cap[i, j]$, then the calculation formula is: +According to the problem statement, capacity equals height multiplied by width, where the height is determined by the shorter partition and the width is the difference between the array indices of the two partitions. Let the capacity be $cap[i, j]$; then we obtain the following formula: $$ cap[i, j] = \min(ht[i], ht[j]) \times (j - i) $$ -Let the array length be $n$, then the number of combinations of two partitions (total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. Most directly, **we can exhaustively enumerate all states** to find the maximum capacity, with time complexity $O(n^2)$. +Let the array length be $n$. Then the number of ways to choose two partitions (that is, the total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. The most straightforward approach is to **exhaustively enumerate all states** to find the maximum capacity, which has a time complexity of $O(n^2)$. ### 1.   Greedy Strategy Determination -This problem has a more efficient solution. As shown in Figure 15-8, select a state $[i, j]$ where index $i < j$ and height $ht[i] < ht[j]$, meaning $i$ is the short partition and $j$ is the long partition. +This problem has a more efficient solution. As shown in Figure 15-8, consider a state $[i, j]$ where $i < j$ and $ht[i] < ht[j]$. In this case, $i$ is the shorter partition and $j$ is the taller partition. ![Initial state](max_capacity_problem.assets/max_capacity_initial_state.png){ class="animation-figure" }

Figure 15-8   Initial state

-As shown in Figure 15-9, **if we now move the long partition $j$ closer to the short partition $i$, the capacity will definitely decrease**. +As shown in Figure 15-9, **if we now move the taller partition $j$ inward toward the shorter partition $i$, the capacity will definitely decrease**. -This is because after moving the long partition $j$, the width $j-i$ definitely decreases; and since height is determined by the short partition, the height can only remain unchanged ($i$ is still the short partition) or decrease (the moved $j$ becomes the short partition). +This is because after moving the taller partition $j$, the width $j-i$ definitely decreases. Since the height is determined by the shorter partition, the height can only stay the same ($i$ remains the shorter partition) or decrease ($j$ becomes the shorter partition after being moved). ![State after moving the long partition inward](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" }

Figure 15-9   State after moving the long partition inward

-Conversely, **we can only possibly increase capacity by contracting the short partition $i$ inward**. Because although width will definitely decrease, **height may increase** (the moved short partition $i$ may become taller). For example, in Figure 15-10, the area increases after moving the short partition. +Conversely, **only by moving the shorter partition $i$ inward can the capacity possibly increase**. Although the width will definitely decrease, **the height may increase** (the moved partition at $i$ may be taller). For example, in Figure 15-10, the area increases after moving the shorter partition. ![State after moving the short partition inward](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" }

Figure 15-10   State after moving the short partition inward

-From this we can derive the greedy strategy for this problem: initialize two pointers at both ends of the container, and in each round contract the pointer corresponding to the short partition inward, until the two pointers meet. +From this, we can derive the greedy strategy for this problem: initialize two pointers at the two ends, and in each round move the pointer corresponding to the shorter partition inward until the two pointers meet. Figure 15-11 shows the execution process of the greedy strategy. 1. In the initial state, pointers $i$ and $j$ are at both ends of the array. 2. Calculate the capacity of the current state $cap[i, j]$, and update the maximum capacity. -3. Compare the heights of partition $i$ and partition $j$, and move the short partition inward by one position. -4. Loop through steps `2.` and `3.` until $i$ and $j$ meet. +3. Compare the heights of partitions $i$ and $j$, and move the pointer corresponding to the shorter partition inward by one position. +4. Repeat steps `2.` and `3.` until $i$ and $j$ meet. === "<1>" ![Greedy process for the max capacity problem](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" } @@ -88,9 +88,9 @@ Figure 15-11 shows the execution process of the greedy strategy. ### 2.   Code Implementation -The code loops at most $n$ rounds, **therefore the time complexity is $O(n)$**. +The code runs for at most $n$ rounds, **so the time complexity is $O(n)$**. -Variables $i$, $j$, and $res$ use a constant amount of extra space, **therefore the space complexity is $O(1)$**. +Variables $i$, $j$, and $res$ use only a constant amount of extra space, **so the space complexity is $O(1)$**. === "Python" @@ -425,7 +425,7 @@ Variables $i$, $j$, and $res$ use a constant amount of extra space, **therefore The reason greedy is faster than exhaustive enumeration is that each round of greedy selection "skips" some states. -For example, in state $cap[i, j]$ where $i$ is the short partition and $j$ is the long partition, if we greedily move the short partition $i$ inward by one position, the states shown in Figure 15-12 will be "skipped". **This means that the capacities of these states cannot be verified later**. +For example, in state $cap[i, j]$, suppose $i$ is the shorter partition and $j$ is the taller partition. If we greedily move the shorter partition $i$ inward by one position, the states shown in Figure 15-12 will be "skipped." **This means that their capacities can no longer be checked later**. $$ cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1] @@ -435,6 +435,6 @@ $$

Figure 15-12   States skipped by moving the short partition

-Observing carefully, **these skipped states are actually all the states obtained by moving the long partition $j$ inward**. We have already proven that moving the long partition inward will definitely decrease capacity. That is, the skipped states cannot possibly be the optimal solution, **skipping them will not cause us to miss the optimal solution**. +A closer look shows that **these skipped states are exactly the states obtained by moving the taller partition $j$ inward**. We have already proven that moving the taller partition inward will definitely decrease the capacity. Therefore, none of the skipped states can be the optimal solution, **so skipping them does not cause us to miss the optimum**. -The above analysis shows that the operation of moving the short partition is "safe", and the greedy strategy is effective. +The above analysis shows that moving the shorter partition is a "safe" operation, and that the greedy strategy is effective. diff --git a/en/docs/chapter_greedy/max_product_cutting_problem.md b/en/docs/chapter_greedy/max_product_cutting_problem.md index 404c41e8e..7af72b647 100644 --- a/en/docs/chapter_greedy/max_product_cutting_problem.md +++ b/en/docs/chapter_greedy/max_product_cutting_problem.md @@ -2,11 +2,11 @@ comments: true --- -# 15.4   Max Product Cutting Problem +# 15.4   Maximum Product Cutting Problem !!! question - Given a positive integer $n$, split it into the sum of at least two positive integers, and find the maximum product of all integers after splitting, as shown in Figure 15-13. + Given a positive integer $n$, split it into the sum of at least two positive integers and find the maximum product of the resulting integers, as shown in Figure 15-13. ![Problem definition of max product cutting](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" } @@ -24,11 +24,11 @@ $$ \max(\prod_{i=1}^{m}n_i) $$ -We need to think about: how large should the splitting count $m$ be, and what should each $n_i$ be? +We need to determine how many parts $m$ there should be and what each $n_i$ should be. -### 1.   Greedy Strategy Determination +### 1.   Determining the Greedy Strategy -Based on experience, the product of two integers is often greater than their sum. Suppose we split out a factor of $2$ from $n$, then their product is $2(n-2)$. We compare this product with $n$: +As a rule of thumb, the product of two integers is often greater than their sum. Suppose we split off a factor of $2$ from $n$; the resulting product is $2(n-2)$. We compare this product with $n$: $$ \begin{aligned} @@ -40,7 +40,7 @@ $$ As shown in Figure 15-14, when $n \geq 4$, splitting out a $2$ will increase the product, **which indicates that integers greater than or equal to $4$ should all be split**. -**Greedy strategy one**: If the splitting scheme includes factors $\geq 4$, then they should continue to be split. The final splitting scheme should only contain factors $1$, $2$, and $3$. +**Greedy strategy one**: If the splitting scheme contains a factor $\geq 4$, it should be split further. The final splitting scheme should contain only the factors $1$, $2$, and $3$. ![Splitting causes product to increase](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" } @@ -50,7 +50,7 @@ Next, consider which factor is optimal. Among the three factors $1$, $2$, and $3 As shown in Figure 15-15, when $n = 6$, we have $3 \times 3 > 2 \times 2 \times 2$. **This means that splitting out $3$ is better than splitting out $2$**. -**Greedy strategy two**: In the splitting scheme, there should be at most two $2$s. Because three $2$s can always be replaced by two $3$s to obtain a larger product. +**Greedy strategy two**: In the splitting scheme, there should be at most two $2$s, because three $2$s can always be replaced by two $3$s to obtain a larger product. ![Optimal splitting factor](max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png){ class="animation-figure" } @@ -60,12 +60,12 @@ In summary, the following greedy strategies can be derived. 1. Input integer $n$, continuously split out factor $3$ until the remainder is $0$, $1$, or $2$. 2. When the remainder is $0$, it means $n$ is a multiple of $3$, so no further action is needed. -3. When the remainder is $2$, do not continue splitting, keep it. -4. When the remainder is $1$, since $2 \times 2 > 1 \times 3$, the last $3$ should be replaced with $2$. +3. When the remainder is $2$, do not split it further; keep it as is. +4. When the remainder is $1$, since $2 \times 2 > 1 \times 3$, replace the final $3$ and the remaining $1$ with two $2$s. ### 2.   Code Implementation -As shown in Figure 15-16, we don't need to use loops to split the integer, but can use integer division to get the count of $3$s as $a$, and modulo operation to get the remainder as $b$, at which point we have: +As shown in Figure 15-16, we do not need loops to split the integer. Instead, we use integer division to obtain the number of $3$s, denoted by $a$, and the modulo operation to obtain the remainder $b$, giving: $$ n = 3 a + b @@ -390,7 +390,7 @@ Please note that for the edge case of $n \leq 3$, a $1$ must be split out, with

Figure 15-16   Calculation method for max product cutting

-**The time complexity depends on the implementation of the exponentiation operation in the programming language**. Taking Python as an example, there are three commonly used power calculation functions. +**The time complexity depends on how exponentiation is implemented in the programming language**. Taking Python as an example, there are three commonly used ways to compute powers. - Both the operator `**` and the function `pow()` have time complexity $O(\log⁡ a)$. - The function `math.pow()` internally calls the C library's `pow()` function, which performs floating-point exponentiation, with time complexity $O(1)$. @@ -399,8 +399,8 @@ Variables $a$ and $b$ use a constant amount of extra space, **therefore the spac ### 3.   Correctness Proof -Using proof by contradiction, only analyzing the case where $n \geq 4$. +We use proof by contradiction and consider only the case where $n \geq 4$. -1. **All factors $\leq 3$**: Suppose the optimal splitting scheme includes a factor $x \geq 4$, then it can definitely continue to be split into $2(x-2)$ to obtain a larger (or equal) product. This contradicts the assumption. -2. **The splitting scheme does not contain $1$**: Suppose the optimal splitting scheme includes a factor of $1$, then it can definitely be merged into another factor to obtain a larger product. This contradicts the assumption. -3. **The splitting scheme contains at most two $2$s**: Suppose the optimal splitting scheme includes three $2$s, then they can definitely be replaced by two $3$s for a larger product. This contradicts the assumption. +1. **All factors $\leq 3$**: Suppose the optimal splitting scheme includes a factor $x \geq 4$. Then it can be further split into $2(x-2)$ to obtain a larger (or equal) product. This contradicts the assumption. +2. **The splitting scheme does not contain $1$**: Suppose the optimal splitting scheme includes a factor of $1$. Then it can be merged into another factor to obtain a larger product. This contradicts the assumption. +3. **The splitting scheme contains at most two $2$s**: Suppose the optimal splitting scheme includes three $2$s. Then they can be replaced by two $3$s, yielding a larger product. This contradicts the assumption. diff --git a/en/docs/chapter_greedy/summary.md b/en/docs/chapter_greedy/summary.md index 511f02e78..4821ee080 100644 --- a/en/docs/chapter_greedy/summary.md +++ b/en/docs/chapter_greedy/summary.md @@ -12,7 +12,7 @@ comments: true - In the coin change problem, for certain coin combinations, greedy algorithms can guarantee finding the optimal solution; for other coin combinations, however, greedy algorithms may find very poor solutions. - Problems suitable for solving with greedy algorithms have two major properties: greedy choice property and optimal substructure. The greedy choice property represents the effectiveness of the greedy strategy. - For some complex problems, proving the greedy choice property is not simple. Relatively speaking, disproving it is easier, such as in the coin change problem. -- Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the difficult point. -- The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting a portion of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction. -- The max capacity problem can be solved using exhaustive enumeration with time complexity $O(n^2)$. By designing a greedy strategy to move the short partition inward in each round, the time complexity can be optimized to $O(n)$. +- Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the main difficulty. +- The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting fractions of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction. +- The max capacity problem can be solved using exhaustive enumeration with time complexity $O(n^2)$. By designing a greedy strategy to move the shorter side inward in each round, the time complexity can be optimized to $O(n)$. - In the max product cutting problem, we successively derive two greedy strategies: integers $\geq 4$ should all continue to be split, and the optimal splitting factor is $3$. The code includes exponentiation operations, and the time complexity depends on the implementation method of exponentiation, typically being $O(1)$ or $O(\log n)$. diff --git a/en/docs/chapter_hashing/hash_algorithm.md b/en/docs/chapter_hashing/hash_algorithm.md index cafd626c5..3dc1a47cc 100644 --- a/en/docs/chapter_hashing/hash_algorithm.md +++ b/en/docs/chapter_hashing/hash_algorithm.md @@ -12,7 +12,7 @@ If hash collisions occur too frequently, the performance of the hash table will

Figure 6-8   Ideal and worst cases of hash collisions

-**The distribution of key-value pairs is determined by the hash function**. Recalling the calculation steps of the hash function, first compute the hash value, then take the modulo by the array length: +**The distribution of key-value pairs is determined by the hash function**. Recall the steps of the hash function: first compute the hash value, then take it modulo the array length: ```shell index = hash(key) % capacity @@ -24,7 +24,7 @@ This means that, to reduce the probability of hash collisions, we should focus o ## 6.3.1   Goals of Hash Algorithms -To achieve a "fast and stable" hash table data structure, hash algorithms should have the following characteristics: +To build a hash table that is both fast and robust, a hash algorithm should have the following properties: - **Determinism**: For the same input, the hash algorithm should always produce the same output. Only then can the hash table be reliable. - **High efficiency**: The process of computing the hash value should be fast enough. The smaller the computational overhead, the more practical the hash table. @@ -35,7 +35,7 @@ In fact, hash algorithms are not only used to implement hash tables but are also - **Password storage**: To protect the security of user passwords, systems usually do not store the plaintext passwords but rather the hash values of the passwords. When a user enters a password, the system calculates the hash value of the input and compares it with the stored hash value. If they match, the password is considered correct. - **Data integrity check**: The data sender can calculate the hash value of the data and send it along; the receiver can recalculate the hash value of the received data and compare it with the received hash value. If they match, the data is considered intact. -For cryptographic applications, to prevent reverse engineering such as deducing the original password from the hash value, hash algorithms need higher-level security features. +For cryptographic applications, hash algorithms need stronger security properties to prevent reverse engineering, such as inferring the original password from a hash value. - **Unidirectionality**: It should be impossible to deduce any information about the input data from the hash value. - **Collision resistance**: It should be extremely difficult to find two different inputs that produce the same hash value. @@ -48,7 +48,7 @@ Note that **"uniform distribution" and "collision resistance" are two independen The design of hash algorithms is a complex issue that requires consideration of many factors. However, for some less demanding scenarios, we can also design some simple hash algorithms. - **Additive hash**: Add up the ASCII codes of each character in the input and use the total sum as the hash value. -- **Multiplicative hash**: Utilize the non-correlation of multiplication, multiplying each round by a constant, accumulating the ASCII codes of each character into the hash value. +- **Multiplicative hash**: Leverage the low correlation introduced by multiplication: multiply by a constant at each step and accumulate the ASCII codes of the characters into the hash value. - **XOR hash**: Accumulate the hash value by XORing each element of the input data. - **Rotating hash**: Accumulate the ASCII code of each character into a hash value, performing a rotation operation on the hash value before each accumulation. @@ -642,9 +642,9 @@ The design of hash algorithms is a complex issue that requires consideration of end ``` -It is observed that the last step of each hash algorithm is to take the modulus of the large prime number $1000000007$ to ensure that the hash value is within an appropriate range. It is worth pondering why emphasis is placed on modulo a prime number, or what are the disadvantages of modulo a composite number? This is an interesting question. +We can observe that the final step of each hash algorithm is to take the result modulo the large prime $1000000007$, ensuring that the hash value stays within a suitable range. This naturally raises a question: why emphasize using a prime modulus, and what are the drawbacks of using a composite modulus? -To conclude: **Using a large prime number as the modulus can maximize the uniform distribution of hash values**. Since a prime number does not share common factors with other numbers, it can reduce the periodic patterns caused by the modulo operation, thus avoiding hash collisions. +In short: **using a large prime as the modulus helps maximize the uniformity of hash values**. Because a prime shares no common factors with other numbers, it can reduce periodic patterns introduced by the modulo operation and thus mitigate hash collisions. For example, suppose we choose the composite number $9$ as the modulus, which can be divided by $3$, then all `key` divisible by $3$ will be mapped to hash values $0$, $3$, $6$. @@ -656,7 +656,7 @@ $$ \end{aligned} $$ -If the input `key` happens to have this kind of arithmetic sequence distribution, then the hash values will cluster, thereby exacerbating hash collisions. Now, suppose we replace `modulus` with the prime number $13$, since there are no common factors between `key` and `modulus`, the uniformity of the output hash values will be significantly improved. +If the input `key` values happen to follow this kind of arithmetic progression, the hash values will cluster, worsening hash collisions. Now suppose we replace `modulus` with the prime number $13$. Because `key` and `modulus` share no common factors, the output hash values become much more evenly distributed. $$ \begin{aligned} @@ -672,7 +672,7 @@ In summary, we usually choose a prime number as the modulus, and this prime numb ## 6.3.3   Common Hash Algorithms -It is not hard to see that the simple hash algorithms mentioned above are quite "fragile" and far from reaching the design goals of hash algorithms. For example, since addition and XOR obey the commutative law, additive hash and XOR hash cannot distinguish strings with the same content but in different order, which may exacerbate hash collisions and cause security issues. +It is easy to see that the simple hash algorithms introduced above are fairly "fragile" and fall far short of the design goals of hash algorithms. For example, because addition and XOR are commutative, additive hash and XOR hash cannot distinguish strings with the same characters in a different order, which may worsen hash collisions and introduce security risks. In practice, we usually use some standard hash algorithms, such as MD5, SHA-1, SHA-2, and SHA-3. They can map input data of any length to a fixed-length hash value. @@ -696,14 +696,14 @@ Over the past century, hash algorithms have been in a continuous process of upgr
-# Hash Values in Data Structures +## 6.3.4   Hash Values in Data Structures -We know that the keys in a hash table can be of various data types such as integers, decimals, or strings. Programming languages usually provide built-in hash algorithms for these data types to calculate the bucket indices in the hash table. Taking Python as an example, we can use the `hash()` function to compute the hash values for various data types. +We know that hash table keys can be integers, floating-point numbers, strings, and other data types. Programming languages usually provide built-in hash algorithms for these types to compute bucket indices in a hash table. Taking Python as an example, we can call the `hash()` function to compute hash values for various data types. - The hash values of integers and booleans are their own values. - The calculation of hash values for floating-point numbers and strings is more complex, and interested readers are encouraged to study this on their own. -- The hash value of a tuple is a combination of the hash values of each of its elements, resulting in a single hash value. -- The hash value of an object is generated based on its memory address. By overriding the hash method of an object, hash values can be generated based on content. +- The hash value of a tuple is obtained by hashing each of its elements and combining those results into a single hash value. +- An object's hash value is typically generated from its memory address. By overriding the object's hash method, it can instead be generated from the object's contents. !!! tip diff --git a/en/docs/chapter_hashing/hash_collision.md b/en/docs/chapter_hashing/hash_collision.md index fb3a41b6c..5806a6d35 100644 --- a/en/docs/chapter_hashing/hash_collision.md +++ b/en/docs/chapter_hashing/hash_collision.md @@ -11,21 +11,21 @@ Hash collisions can lead to incorrect query results, severely impacting the usab 1. Improve the hash table data structure so that **the hash table can function normally when hash collisions occur**. 2. Only expand when necessary, that is, only when hash collisions are severe. -The main methods for improving the structure of hash tables include "separate chaining" and "open addressing". +The main approaches to improving a hash table's structure are separate chaining and open addressing. ## 6.2.1   Separate Chaining -In the original hash table, each bucket can store only one key-value pair. Separate chaining converts a single element into a linked list, treating key-value pairs as linked list nodes and storing all colliding key-value pairs in the same linked list. Figure 6-5 shows an example of a separate chaining hash table. +In the original hash table, each bucket can store only one key-value pair. Separate chaining replaces the single element in each bucket with a linked list, treating each key-value pair as a node and storing all colliding key-value pairs in the same list. Figure 6-5 shows an example of a separate chaining hash table. ![Separate chaining hash table](hash_collision.assets/hash_table_chaining.png){ class="animation-figure" }

Figure 6-5   Separate chaining hash table

-The operations of a hash table implemented with separate chaining have changed as follows: +In a hash table implemented with separate chaining, the basic operations work as follows: -- **Querying elements**: Input `key`, obtain the bucket index through the hash function, then access the head node of the linked list, then traverse the linked list and compare `key` to find the target key-value pair. -- **Adding elements**: First access the linked list head node through the hash function, then append the node (key-value pair) to the linked list. -- **Deleting elements**: Access the head of the linked list based on the result of the hash function, then traverse the linked list to find the target node and delete it. +- **Querying elements**: Input `key`, compute the bucket index using the hash function, access the head of the corresponding linked list, and traverse the list while comparing keys until the target key-value pair is found. +- **Adding elements**: First use the hash function to locate the corresponding linked list, then insert the node (key-value pair) into the list. +- **Deleting elements**: Use the hash function to locate the corresponding linked list, then traverse it to find and delete the target node. Separate chaining has the following limitations: @@ -1517,40 +1517,40 @@ The code below provides a simple implementation of a separate chaining hash tabl end ``` -It's worth noting that when the linked list is very long, the query efficiency $O(n)$ is poor. **In this case, the list can be converted to an "AVL tree" or "Red-Black tree"** to optimize the time complexity of the query operation to $O(\log n)$. +It's worth noting that when the linked list becomes very long, the query time $O(n)$ is poor. **In this case, the linked list can be converted into an AVL tree or a red-black tree**, reducing the time complexity of lookups to $O(\log n)$. ## 6.2.2   Open Addressing -Open addressing does not introduce additional data structures but instead handles hash collisions through "multiple probes". The probing methods mainly include linear probing, quadratic probing, and double hashing. +Open addressing does not introduce additional data structures. Instead, it handles hash collisions through repeated probing. Common probing strategies include linear probing, quadratic probing, and multiple hashing. Let's use linear probing as an example to introduce the mechanism of open addressing hash tables. ### 1.   Linear Probing -Linear probing uses a fixed-step linear search for probing, and its operation method differs from ordinary hash tables. +Linear probing uses a fixed step size to probe sequentially, so its operations differ somewhat from those of an ordinary hash table. -- **Inserting elements**: Calculate the bucket index using the hash function. If the bucket already contains an element, linearly traverse forward from the conflict position (usually with a step size of $1$) until an empty bucket is found, then insert the element. -- **Searching for elements**: If a hash collision is encountered, use the same step size to linearly traverse forward until the corresponding element is found and return `value`; if an empty bucket is encountered, it means the target element is not in the hash table, so return `None`. +- **Inserting elements**: Compute the bucket index using the hash function. If the bucket is already occupied, continue probing forward from the collision position with a fixed step size (usually $1$) until an empty bucket is found, then insert the element there. +- **Searching for elements**: If a collision occurs, continue probing forward with the same step size until the corresponding element is found and return its `value`; if an empty bucket is encountered, the target element is not in the hash table, so return `None`. -Figure 6-6 shows the distribution of key-value pairs in an open addressing (linear probing) hash table. According to this hash function, keys with the same last two digits will be mapped to the same bucket. Through linear probing, they are stored sequentially in that bucket and the buckets below it. +Figure 6-6 shows the distribution of key-value pairs in an open-addressing hash table that uses linear probing. Under this hash function, keys with the same last two digits are mapped to the same bucket. Linear probing then places them in that bucket and the subsequent buckets. ![Distribution of key-value pairs in open addressing (linear probing) hash table](hash_collision.assets/hash_table_linear_probing.png){ class="animation-figure" }

Figure 6-6   Distribution of key-value pairs in open addressing (linear probing) hash table

-However, **linear probing is prone to create "clustering"**. Specifically, the longer the continuously occupied positions in the array, the greater the probability of hash collisions occurring in these continuous positions, further promoting clustering growth at that position, forming a vicious cycle, and ultimately leading to degraded efficiency of insertion, deletion, query, and update operations. +However, **linear probing is prone to clustering**. Specifically, the longer a contiguous occupied region in the array becomes, the more likely new collisions are to occur within that region. This in turn makes the cluster grow even further, creating a vicious cycle that gradually degrades the efficiency of insertion, deletion, lookup, and update operations. -It's important to note that **we cannot directly delete elements in an open addressing hash table**. Deleting an element creates an empty bucket `None` in the array. When searching for elements, if linear probing encounters this empty bucket, it will return, making the elements below this empty bucket inaccessible. The program may incorrectly assume these elements do not exist, as shown in Figure 6-7. +It's important to note that **we cannot directly delete elements from an open-addressing hash table**. Deleting an element creates an empty bucket `None` in the array. During lookup, once linear probing reaches that empty bucket, it stops, which means any elements stored farther along the probe sequence become unreachable. As a result, the program may incorrectly conclude that those elements do not exist, as shown in Figure 6-7. ![Query issues caused by deletion in open addressing](hash_collision.assets/hash_table_open_addressing_deletion.png){ class="animation-figure" }

Figure 6-7   Query issues caused by deletion in open addressing

-To solve this problem, we can adopt the lazy deletion mechanism: instead of directly removing elements from the hash table, **use a constant `TOMBSTONE` to mark the bucket**. In this mechanism, both `None` and `TOMBSTONE` represent empty buckets and can hold key-value pairs. However, when linear probing encounters `TOMBSTONE`, it should continue traversing since there may still be key-value pairs below it. +To solve this problem, we can adopt lazy deletion: instead of directly removing an element from the hash table, **use a constant `TOMBSTONE` to mark the bucket**. Under this mechanism, both `None` and `TOMBSTONE` denote buckets that can accept key-value pairs. The difference is that when linear probing encounters `TOMBSTONE`, it must continue probing, because key-value pairs may still exist farther along the sequence. -However, **lazy deletion may accelerate the performance degradation of the hash table**. Every deletion operation produces a deletion mark, and as `TOMBSTONE` increases, the search time will also increase because linear probing may need to skip multiple `TOMBSTONE` to find the target element. +However, **lazy deletion may accelerate hash-table performance degradation**. Each deletion leaves behind a marker, and as the number of `TOMBSTONE` entries grows, search time increases as well, because linear probing may need to skip over multiple tombstones before finding the target element. -To address this, consider recording the index of the first encountered `TOMBSTONE` during linear probing and swapping the searched target element with that `TOMBSTONE`. The benefit of doing this is that each time an element is queried or added, the element will be moved to a bucket closer to its ideal position (the starting point of probing), thereby optimizing query efficiency. +To address this, we can record the index of the first `TOMBSTONE` encountered during linear probing and swap the found target element into that position. The benefit is that each query or insertion can move elements closer to their ideal positions, that is, closer to where probing begins, which improves lookup efficiency. The code below implements an open addressing (linear probing) hash table with lazy deletion. To make better use of the hash table space, we treat the hash table as a "circular array". When going beyond the end of the array, we return to the beginning and continue traversing. @@ -3291,18 +3291,18 @@ However, quadratic probing is not perfect: - Clustering still exists, i.e., some positions are more likely to be occupied than others. - Due to the growth of squares, quadratic probing may not probe the entire hash table, meaning that even if there are empty buckets in the hash table, quadratic probing may not be able to access them. -### 3.   Double Hashing +### 3.   Multiple Hashing -As the name suggests, the double hashing method uses multiple hash functions $f_1(x)$, $f_2(x)$, $f_3(x)$, $\dots$ for probing. +As the name suggests, multiple hashing uses multiple hash functions $f_1(x)$, $f_2(x)$, $f_3(x)$, $\dots$ for probing. - **Inserting elements**: If hash function $f_1(x)$ encounters a conflict, try $f_2(x)$, and so on, until an empty position is found and the element is inserted. - **Searching for elements**: Search in the same order of hash functions until the target element is found and return it; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return `None`. -Compared to linear probing, the double hashing method is less prone to clustering, but multiple hash functions introduce additional computational overhead. +Compared with linear probing, multiple hashing is less prone to clustering, but using multiple hash functions introduces additional computational overhead. !!! tip - Please note that open addressing (linear probing, quadratic probing, and double hashing) hash tables all have the problem of "cannot directly delete elements". + Please note that hash tables based on open addressing, including linear probing, quadratic probing, and multiple hashing, all have the problem that elements cannot be deleted directly. ## 6.2.3   Choice of Programming Languages diff --git a/en/docs/chapter_hashing/hash_map.md b/en/docs/chapter_hashing/hash_map.md index acc10f989..a9cba10ca 100755 --- a/en/docs/chapter_hashing/hash_map.md +++ b/en/docs/chapter_hashing/hash_map.md @@ -4,9 +4,9 @@ comments: true # 6.1   Hash Table -A hash table, also known as a hash map, establishes a mapping between keys `key` and values `value`, enabling efficient element retrieval. Specifically, when we input a key `key` into a hash table, we can retrieve the corresponding value `value` in $O(1)$ time. +A hash table, also known as a hash map, stores mappings from keys `key` to values `value`, enabling efficient lookups. Specifically, given a key `key`, we can retrieve the corresponding value `value` from a hash table in $O(1)$ time. -As shown in Figure 6-1, given $n$ students, each with two pieces of data: "name" and "student ID". If we want to implement a query function that "inputs a student ID and returns the corresponding name", we can use the hash table shown below. +As shown below, suppose we have $n$ students, each with two pieces of information: a name and a student ID. If we want to support the query "given a student ID, return the corresponding name," we can use the hash table shown below. ![Abstract representation of a hash table](hash_map.assets/hash_table_lookup.png){ class="animation-figure" } @@ -30,7 +30,7 @@ In addition to hash tables, arrays and linked lists can also implement query fun
-As observed, **the time complexity for insertion, deletion, search, and modification operations in a hash table is $O(1)$**, which is very efficient. +As we can see, **insertion, deletion, lookup, and update operations in a hash table all have time complexity $O(1)$**, making hash tables highly efficient. ## 6.1.1   Common Hash Table Operations @@ -259,7 +259,7 @@ Common operations on hash tables include: initialization, query operations, addi map.insert(12836, "XiaoHa".to_string()); map.insert(15937, "XiaoLuo".to_string()); map.insert(16750, "XiaoSuan".to_string()); - map.insert(13279, "XiaoFa".to_string()); + map.insert(13276, "XiaoFa".to_string()); map.insert(10583, "XiaoYa".to_string()); /* Query operation */ @@ -550,22 +550,22 @@ There are three common ways to traverse a hash table: traversing key-value pairs ## 6.1.2   Simple Hash Table Implementation -Let's first consider the simplest case: **implementing a hash table using only an array**. In a hash table, each empty position in the array is called a bucket, and each bucket can store a key-value pair. Therefore, the query operation is to find the bucket corresponding to `key` and retrieve the `value` from the bucket. +Let's start with the simplest case: **implementing a hash table with just an array**. In a hash table, each empty slot in the array is called a bucket, and each bucket can store one key-value pair. A lookup therefore consists of finding the bucket for `key` and reading the `value` stored there. -So how do we locate the corresponding bucket based on `key`? This is achieved through a hash function. The role of the hash function is to map a larger input space to a smaller output space. In a hash table, the input space is all `key`s, and the output space is all buckets (array indices). In other words, given a `key`, **we can use the hash function to obtain the storage location of the key-value pair corresponding to that `key` in the array**. +So how do we find the right bucket for a given `key`? We do this with a hash function. A hash function maps a larger input space to a smaller output space. In a hash table, the input space is the set of all `key`s, and the output space is the set of all buckets (array indices). In other words, given a `key`, **the hash function tells us where the corresponding key-value pair should be stored in the array**. -When inputting a `key`, the hash function's calculation process consists of the following two steps: +Given a `key`, computing the bucket index involves the following two steps: -1. Calculate the hash value through a hash algorithm `hash()`. -2. Take the modulo of the hash value by the number of buckets (array length) `capacity` to obtain the bucket (array index) `index` corresponding to that `key`. +1. Use a hash algorithm `hash()` to compute a hash value. +2. Take that hash value modulo the number of buckets (array length), `capacity`, to obtain the bucket (array index) `index` corresponding to the `key`. ```shell index = hash(key) % capacity ``` -Subsequently, we can use `index` to access the corresponding bucket in the hash table and retrieve the `value`. +We can then use `index` to access the corresponding bucket in the hash table and retrieve the `value`. -Assuming the array length is `capacity = 100` and the hash algorithm is `hash(key) = key`, the hash function becomes `key % 100`. Figure 6-2 shows the working principle of the hash function using `key` as student ID and `value` as name. +Suppose the array length is `capacity = 100` and the hash algorithm is `hash(key) = key`. Then the hash function is `key % 100`. Figure 6-2 illustrates how this hash function works, using student ID as `key` and name as `value`. ![Working principle of hash function](hash_map.assets/hash_function.png){ class="animation-figure" } @@ -1753,7 +1753,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an ## 6.1.3   Hash Collision and Resizing -Fundamentally, the role of a hash function is to map the input space consisting of all `key`s to the output space consisting of all array indices, and the input space is often much larger than the output space. Therefore, **theoretically there must be cases where "multiple inputs correspond to the same output"**. +Fundamentally, a hash function maps the input space consisting of all `key`s to the output space consisting of all array indices, and the input space is often much larger than the output space. Therefore, **in theory, different inputs must sometimes map to the same output**. For the hash function in the above example, when the input `key`s have the same last two digits, the hash function produces the same output. For example, when querying two students with IDs 12836 and 20336, we get: @@ -1762,7 +1762,7 @@ For the hash function in the above example, when the input `key`s have the same 20336 % 100 = 36 ``` -As shown in Figure 6-3, two student IDs point to the same name, which is obviously incorrect. We call this situation where multiple inputs correspond to the same output a hash collision. +As shown below, two student IDs now point to the same name, which is clearly incorrect. We call this situation, where multiple inputs map to the same output, a hash collision. ![Hash collision example](hash_map.assets/hash_collision.png){ class="animation-figure" } @@ -1776,6 +1776,6 @@ As shown in Figure 6-4, before expansion, the key-value pairs `(136, A)` and `(2

Figure 6-4   Hash table resizing

-Similar to array expansion, hash table expansion requires migrating all key-value pairs from the original hash table to the new hash table, which is very time-consuming. Moreover, since the hash table capacity `capacity` changes, we need to recalculate the storage locations of all key-value pairs through the hash function, further increasing the computational overhead of the expansion process. For this reason, programming languages typically reserve a sufficiently large hash table capacity to prevent frequent expansion. +Like resizing an array, resizing a hash table requires migrating all key-value pairs from the original table to the new table, which is expensive. In addition, because the hash table capacity `capacity` changes, we must recompute the storage location of every key-value pair using the hash function, which further increases the cost of resizing. For this reason, programming languages typically reserve a sufficiently large hash table capacity to avoid frequent resizing. -The load factor is an important concept for hash tables. It is defined as the number of elements in the hash table divided by the number of buckets, and is used to measure the severity of hash collisions. **It is also commonly used as a trigger condition for hash table expansion**. For example, in Java, when the load factor exceeds $0.75$, the system will expand the hash table to $2$ times its original size. +The load factor is an important concept in hash tables. It is defined as the number of elements in the hash table divided by the number of buckets and is used to measure the severity of hash collisions. **It is also commonly used as a threshold for triggering hash table resizing**. For example, in Java, when the load factor exceeds $0.75$, the system expands the hash table to twice its original size. diff --git a/en/docs/chapter_hashing/index.md b/en/docs/chapter_hashing/index.md index 50cc48b95..8e8f79931 100644 --- a/en/docs/chapter_hashing/index.md +++ b/en/docs/chapter_hashing/index.md @@ -3,15 +3,15 @@ comments: true icon: material/table-search --- -# Chapter 6.   Hashing +# Chapter 6.   Hash Table -![Hashing](../assets/covers/chapter_hashing.jpg){ class="cover-image" } +![Hash Table](../assets/covers/chapter_hashing.jpg){ class="cover-image" } !!! abstract In the world of computing, a hash table is like a clever librarian. - They know how to calculate call numbers, enabling them to quickly locate the target book. + It knows how to compute call numbers, allowing it to quickly locate the desired book. ## Chapter contents diff --git a/en/docs/chapter_hashing/summary.md b/en/docs/chapter_hashing/summary.md index 2d3f8154f..4441a4f0d 100644 --- a/en/docs/chapter_hashing/summary.md +++ b/en/docs/chapter_hashing/summary.md @@ -12,7 +12,7 @@ comments: true - Two different keys may end up with the same array index after hashing, leading to erroneous query results. This phenomenon is known as hash collision. - The larger the capacity of the hash table, the lower the probability of hash collisions. Therefore, hash table expansion can mitigate hash collisions. Similar to array expansion, hash table expansion is costly. - The load factor, defined as the number of elements divided by the number of buckets, reflects the severity of hash collisions and is often used as a condition to trigger hash table expansion. -- Separate chaining addresses hash collisions by converting each element into a linked list, storing all colliding elements in the same linked list. However, excessively long linked lists can reduce query efficiency, which can be improved by converting the linked lists into red-black trees. +- Separate chaining addresses hash collisions by storing all colliding elements in the same linked list. However, excessively long linked lists can reduce query efficiency, which can be improved by further converting the linked lists into red-black trees. - Open addressing handles hash collisions through multiple probing. Linear probing uses a fixed step size but cannot delete elements and is prone to clustering. Double hashing uses multiple hash functions for probing, which reduces clustering compared to linear probing but increases computational overhead. - Different programming languages adopt various hash table implementations. For example, Java's `HashMap` uses separate chaining, while Python's `dict` employs open addressing. - In hash tables, we desire hash algorithms with determinism, high efficiency, and uniform distribution. In cryptography, hash algorithms should also possess collision resistance and the avalanche effect. @@ -49,3 +49,7 @@ During the search process, the hash function points to the corresponding bucket **Q**: Why can expanding a hash table alleviate hash collisions? The last step of a hash function often involves taking the modulo of the array length $n$, to keep the output within the array index range. When expanding, the array length $n$ changes, and the indices corresponding to the keys may also change. Keys that were previously mapped to the same bucket might be distributed across multiple buckets after expansion, thereby mitigating hash collisions. + +**Q**: If the goal is efficient access, why not just use an array directly? + +When the `key` values are continuous integers within a small range, an array is indeed a simple and efficient choice. But when the `key` is of another type, such as a string, we need a hash function to map the `key` to an array index and then store the element in a bucket array. That structure is precisely what a hash table is. diff --git a/en/docs/chapter_heap/build_heap.md b/en/docs/chapter_heap/build_heap.md index f1f7b837e..63b096956 100644 --- a/en/docs/chapter_heap/build_heap.md +++ b/en/docs/chapter_heap/build_heap.md @@ -8,7 +8,7 @@ In some cases, we want to build a heap using all elements of a list, and this pr ## 8.2.1   Implementing with Element Insertion -We first create an empty heap, then iterate through the list, performing the "element insertion operation" on each element in sequence. This means adding the element to the bottom of the heap and then performing "bottom-to-top" heapify on that element. +We first create an empty heap, then iterate through the list, performing the "element insertion operation" on each element in sequence. This means appending the element to the end of the heap and then performing "bottom-to-top" heapify on that element. Each time an element is inserted into the heap, the heap's length increases by one. Since nodes are added to the binary tree sequentially from top to bottom, the heap is constructed "from top to bottom." @@ -23,9 +23,9 @@ In fact, we can implement a more efficient heap construction method in two steps **After heapifying a node, the subtree rooted at that node becomes a valid sub-heap**. Since we traverse in reverse order, the heap is constructed "from bottom to top." -The reason for choosing reverse order traversal is that it ensures the subtree below the current node is already a valid sub-heap, making the heapification of the current node effective. +The reason for choosing reverse-order traversal is that it ensures the subtrees beneath the current node are already valid sub-heaps, so heapifying the current node is effective. -It's worth noting that **since leaf nodes have no children, they are naturally valid sub-heaps and do not require heapification**. As shown in the code below, the last non-leaf node is the parent of the last node; we start from it and traverse in reverse order to perform heapification: +It's worth noting that **since leaf nodes have no children, they are naturally valid sub-heaps and do not require heapification**. As shown in the code below, the last non-leaf node is the parent of the last node; we start from that node and heapify while traversing in reverse order: === "Python" @@ -327,23 +327,23 @@ It's worth noting that **since leaf nodes have no children, they are naturally v Next, let's attempt to derive the time complexity of this second heap construction method. - Assuming the complete binary tree has $n$ nodes, then the number of leaf nodes is $(n + 1) / 2$, where $/$ is floor division. Therefore, the number of nodes that need heapification is $(n - 1) / 2$. -- In the top-to-bottom heapify process, each node is heapified at most to the leaf nodes, so the maximum number of iterations is the binary tree height $\log n$. +- In the top-to-bottom heapify process, each node can sink at most to a leaf node, so the maximum number of iterations is the height of the binary tree, $\log n$. Multiplying these two together, we get a time complexity of $O(n \log n)$ for the heap construction process. **However, this estimate is not accurate because it doesn't account for the property that binary trees have far more nodes at lower levels than at upper levels**. -Let's perform a more accurate calculation. To reduce calculation difficulty, assume a "perfect binary tree" with $n$ nodes and height $h$; this assumption does not affect the correctness of the result. +Let's perform a more accurate calculation. To simplify the analysis, assume a "perfect binary tree" with $n$ nodes and height $h$; this assumption does not affect the correctness of the result. ![Node count at each level of a perfect binary tree](build_heap.assets/heapify_operations_count.png){ class="animation-figure" }

Figure 8-5   Node count at each level of a perfect binary tree

-As shown in Figure 8-5, the maximum number of iterations for a node's "top-to-bottom heapify" equals the distance from that node to the leaf nodes, which is precisely the "node height." Therefore, we can sum the "number of nodes $\times$ node height" at each level to **obtain the total number of heapify iterations for all nodes**. +As shown in Figure 8-5, the maximum number of iterations for a node's "top-to-bottom heapify" equals the distance from that node to a leaf node, which is precisely the node's height. Therefore, we can sum the "number of nodes $\times$ node height" at each level to **obtain the total number of heapify iterations for all nodes**. $$ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1 $$ -To simplify the above expression, we need to use sequence knowledge from high school. First, multiply $T(h)$ by $2$ to get: +Simplifying the expression above requires some high-school sequence algebra. First, multiply $T(h)$ by $2$ to get: $$ \begin{aligned} @@ -352,7 +352,7 @@ T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline \end{aligned} $$ -Using the method of differences, subtract the first equation $T(h)$ from the second equation $2 T(h)$ to get: +Using subtraction of shifted sums, subtract the first equation $T(h)$ from the second equation $2 T(h)$ to get: $$ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h diff --git a/en/docs/chapter_heap/heap.md b/en/docs/chapter_heap/heap.md index edf3e5dd7..f1faff2be 100644 --- a/en/docs/chapter_heap/heap.md +++ b/en/docs/chapter_heap/heap.md @@ -21,7 +21,7 @@ As a special case of a complete binary tree, heaps have the following characteri ## 8.1.1   Common Heap Operations -It should be noted that many programming languages provide a priority queue, which is an abstract data structure defined as a queue with priority sorting. +It should be noted that many programming languages provide a priority queue, an abstract data structure defined as a queue whose elements are ordered by priority. In fact, **heaps are typically used to implement priority queues, with max heaps corresponding to priority queues where elements are dequeued in descending order**. From a usage perspective, we can regard "priority queue" and "heap" as equivalent data structures. Therefore, this book does not make a special distinction between the two and uniformly refers to them as "heap." @@ -425,13 +425,13 @@ Similar to "ascending order" and "descending order" in sorting algorithms, we ca ## 8.1.2   Implementation of the Heap -The following implementation is of a max heap. To convert it to a min heap, simply invert all size logic comparisons (for example, replace $\geq$ with $\leq$). Interested readers are encouraged to implement this on their own. +The following implementation is for a max heap. To convert it to a min heap, simply reverse all comparison logic related to ordering (for example, replace $\geq$ with $\leq$). Interested readers are encouraged to implement this on their own. ### 1.   Heap Storage and Representation As mentioned in the "Binary Tree" chapter, complete binary trees are well-suited for array representation. Since heaps are a type of complete binary tree, **we will use arrays to store heaps**. -When representing a binary tree with an array, elements represent node values, and indexes represent node positions in the binary tree. **Node pointers are implemented through index mapping formulas**. +When representing a binary tree with an array, elements represent node values, and indexes represent node positions in the binary tree. **Parent-child relationships are represented through index-mapping formulas**. As shown in Figure 8-2, given an index $i$, the index of its left child is $2i + 1$, the index of its right child is $2i + 2$, and the index of its parent is $(i - 1) / 2$ (floor division). When an index is out of bounds, it indicates a null node or that the node does not exist. @@ -808,9 +808,9 @@ The heap top element is the root node of the binary tree, which is also the firs ### 3.   Inserting an Element Into the Heap -Given an element `val`, we first add it to the bottom of the heap. After addition, since `val` may be larger than other elements in the heap, the heap's property may be violated. **Therefore, it's necessary to repair the path from the inserted node to the root node**. This operation is called heapify. +Given an element `val`, we first add it to the bottom of the heap. After insertion, because `val` may be larger than other elements in the heap, the heap property may be violated. **Therefore, we need to restore the heap property along the path from the inserted node to the root**. This operation is called heapify. -Starting from the inserted node, **perform heapify from bottom to top**. As shown in Figure 8-3, we compare the inserted node with its parent node, and if the inserted node is larger, swap them. Then continue this operation, repairing nodes in the heap from bottom to top until we pass the root node or encounter a node that does not need swapping. +Starting from the inserted node, **perform heapify from bottom to top**. As shown in Figure 8-3, we compare the inserted node with its parent, and if the inserted node is larger, we swap them. We continue this process from bottom to top until we move past the root or reach a node that no longer needs to be swapped. === "<1>" ![Steps of inserting an element into the heap](heap.assets/heap_push_step1.png){ class="animation-figure" } @@ -1760,6 +1760,6 @@ Similar to the element insertion operation, the time complexity of the heap top ## 8.1.3   Common Applications of Heaps -- **Priority queue**: Heaps are typically the preferred data structure for implementing priority queues, with both enqueue and dequeue operations having a time complexity of $O(\log n)$, and the heap construction operation having $O(n)$, all of which are highly efficient. +- **Priority queue**: Heaps are typically the preferred data structure for implementing priority queues. The time complexity of both enqueue and dequeue operations is $O(\log n)$, and heap construction has a time complexity of $O(n)$, making these operations highly efficient. - **Heap sort**: Given a set of data, we can build a heap with them and then continuously perform element removal operations to obtain sorted data. However, we usually use a more elegant approach to implement heap sort, as detailed in the "Heap Sort" chapter. -- **Getting the largest $k$ elements**: This is a classic algorithm problem and also a typical application, such as selecting the top 10 trending news for Weibo hot search, selecting the top 10 best-selling products, etc. +- **Getting the largest $k$ elements**: This is a classic algorithm problem and also a typical application, such as selecting the top 10 trending news items for Weibo Hot Search or the top 10 best-selling products. diff --git a/en/docs/chapter_heap/index.md b/en/docs/chapter_heap/index.md index 91242dc74..39988253d 100644 --- a/en/docs/chapter_heap/index.md +++ b/en/docs/chapter_heap/index.md @@ -9,13 +9,13 @@ icon: material/family-tree !!! abstract - Heaps are like mountain peaks, layered and undulating, each with its unique form. + Heaps are like mountain peaks, rising layer upon layer, each with a distinct shape. The peaks rise and fall at varying heights, yet the tallest peak always catches the eye first. ## Chapter contents - [8.1   Heap](heap.md) -- [8.2   Building a Heap](build_heap.md) -- [8.3   Top-K Problem](top_k.md) +- [8.2   Heap Construction Operation](build_heap.md) +- [8.3   Top-k Problem](top_k.md) - [8.4   Summary](summary.md) diff --git a/en/docs/chapter_heap/summary.md b/en/docs/chapter_heap/summary.md index 34ce94457..80ad39810 100644 --- a/en/docs/chapter_heap/summary.md +++ b/en/docs/chapter_heap/summary.md @@ -6,16 +6,16 @@ comments: true ### 1.   Key Review -- A heap is a complete binary tree that can be categorized as a max heap or min heap based on its property. The heap top element of a max heap (min heap) is the largest (smallest). -- A priority queue is defined as a queue with priority sorting, typically implemented using heaps. -- Common heap operations and their corresponding time complexities include: element insertion $O(\log n)$, heap top element removal $O(\log n)$, and accessing the heap top element $O(1)$. +- A heap is a complete binary tree. Depending on the property it satisfies, it can be classified as either a max heap or a min heap. The top element of a max heap (min heap) is the largest (smallest) element. +- A priority queue is a queue in which elements are dequeued according to priority, and it is typically implemented using a heap. +- Common heap operations and their corresponding time complexities include inserting an element $O(\log n)$, removing the top element $O(\log n)$, and accessing the top element $O(1)$. - Complete binary trees are well-suited for array representation, so we typically use arrays to store heaps. - Heapify operations are used to maintain the heap property and are employed in both element insertion and removal operations. -- The time complexity of building a heap with $n$ input elements can be optimized to $O(n)$, which is highly efficient. -- Top-k is a classic algorithm problem that can be efficiently solved using the heap data structure, with a time complexity of $O(n \log k)$. +- Building a heap from $n$ input elements can be optimized to $O(n)$, which is highly efficient. +- Top-k is a classic algorithmic problem that can be solved efficiently using a heap, with a time complexity of $O(n \log k)$. ### 2.   Q & A -**Q**: Are the "heap" in data structures and the "heap" in memory management the same concept? +**Q**: Does the term "heap" in data structures mean the same thing as "heap" in memory management? -The two are not the same concept; they just happen to share the name "heap." The heap in computer system memory is part of dynamic memory allocation, where programs can use it to store data during runtime. Programs can request a certain amount of heap memory to store complex structures such as objects and arrays. When this data is no longer needed, the program needs to release this memory to prevent memory leaks. Compared to stack memory, heap memory management and usage require more caution, as improper use can lead to issues such as memory leaks and dangling pointers. +They are not the same concept; they simply share the same name. In computer systems, the heap is part of dynamic memory allocation, and programs can use it to store data at runtime. A program can request a certain amount of heap memory to store complex structures such as objects and arrays. When the data is no longer needed, the program must release that memory to prevent memory leaks. Compared with stack memory, heap memory requires more careful management and use; improper handling can lead to problems such as memory leaks and dangling pointers. diff --git a/en/docs/chapter_heap/top_k.md b/en/docs/chapter_heap/top_k.md index 48eb5ca3e..135135351 100644 --- a/en/docs/chapter_heap/top_k.md +++ b/en/docs/chapter_heap/top_k.md @@ -2,19 +2,19 @@ comments: true --- -# 8.3   Top-K Problem +# 8.3   Top-k Problem !!! question Given an unordered array `nums` of length $n$, return the largest $k$ elements in the array. -For this problem, we'll first introduce two solutions with relatively straightforward approaches, then introduce a more efficient heap-based solution. +For this problem, we will first introduce two relatively straightforward solutions, followed by a more efficient heap-based solution. ## 8.3.1   Method 1: Iterative Selection We can perform $k$ rounds of traversal as shown in Figure 8-6, extracting the $1^{st}$, $2^{nd}$, $\dots$, $k^{th}$ largest elements in each round, with a time complexity of $O(nk)$. -This method is only suitable when $k \ll n$, because when $k$ is close to $n$, the time complexity approaches $O(n^2)$, which is very time-consuming. +This method is only suitable when $k \ll n$, because when $k$ is close to $n$, the time complexity approaches $O(n^2)$, making it very inefficient. ![Traversing to find the largest k elements](top_k.assets/top_k_traversal.png){ class="animation-figure" } @@ -28,7 +28,7 @@ This method is only suitable when $k \ll n$, because when $k$ is close to $n$, t As shown in Figure 8-7, we can first sort the array `nums`, then return the rightmost $k$ elements, with a time complexity of $O(n \log n)$. -Clearly, this method "overachieves" the task, as we only need to find the largest $k$ elements, without needing to sort the other elements. +Clearly, this method does more work than necessary, because we only need to find the largest $k$ elements rather than sort the other elements. ![Sorting to find the largest k elements](top_k.assets/top_k_sorting.png){ class="animation-figure" } @@ -36,7 +36,7 @@ Clearly, this method "overachieves" the task, as we only need to find the larges ## 8.3.3   Method 3: Heap -We can solve the Top-k problem more efficiently using heaps, with the process shown in Figure 8-8. +We can solve the Top-k problem more efficiently with a heap, as shown in Figure 8-8. 1. Initialize a min heap, where the heap top element is the smallest. 2. First, insert the first $k$ elements of the array into the heap in sequence. @@ -463,4 +463,4 @@ Example code is as follows: A total of $n$ rounds of heap insertions and removals are performed, with the heap's maximum length being $k$, so the time complexity is $O(n \log k)$. This method is very efficient; when $k$ is small, the time complexity approaches $O(n)$; when $k$ is large, the time complexity does not exceed $O(n \log n)$. -Additionally, this method is suitable for dynamic data stream scenarios. By continuously adding data, we can maintain the elements in the heap, thus achieving dynamic updates of the largest $k$ elements. +Additionally, this method is well suited to dynamic data streams. As new data arrives, we can continuously maintain the elements in the heap, enabling dynamic updates to the largest $k$ elements. diff --git a/en/docs/chapter_hello_algo/index.md b/en/docs/chapter_hello_algo/index.md index 7d398ed55..e86f62e1a 100644 --- a/en/docs/chapter_hello_algo/index.md +++ b/en/docs/chapter_hello_algo/index.md @@ -3,17 +3,17 @@ comments: true icon: material/rocket-launch-outline --- -# Before Starting +# Preface A few years ago, I shared the "Sword for Offer" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most frequently asked question I encountered was "how to get started with algorithms." Gradually, I developed a keen interest in this question. Diving straight into problem-solving seems to be the most popular approach—it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-learning abilities can successfully defuse the mines one by one, while those with insufficient foundations may end up bruised and battered, retreating step by step in frustration. Reading through textbooks is also a common practice, but for job seekers, graduation theses, resume submissions, and preparations for written tests and interviews have already consumed most of their energy, making working through thick books an arduous challenge. -If you're facing similar struggles, then it's fortunate that this book has "found" you. This book is my answer to this question—even if it may not be the optimal solution, it is at least a positive attempt. While this book alone won't directly land you a job offer, it will guide you to explore the "knowledge map" of data structures and algorithms, help you understand the shapes, sizes, and distributions of different "mines," and enable you to master various "mine-clearing methods." With these skills, I believe you can tackle problems and read technical literature more confidently, gradually building a complete knowledge system. +If you're facing similar struggles, then it's fortunate that this book has "found" you. This book is my answer to this question—even if it may not be the optimal solution, it is at least a positive attempt. While this book alone won't directly land you a job offer, it will guide you through the "landscape" of data structures and algorithms, help you understand the shapes, sizes, and distributions of different "mines," and enable you to master various "mine-clearing methods." With these skills, I believe you can tackle problems and read technical literature more confidently, gradually building a complete knowledge system. I deeply agree with Professor Feynman's words: "Knowledge isn't free. You have to pay attention." In this sense, this book is not entirely "free." In order to live up to the precious "attention" you invest in this book, I will do my utmost and devote my greatest "attention" to completing this work. -I'm acutely aware of my limited knowledge and shallow expertise. Although the content of this book has been refined over a period of time, there are certainly still many errors, and I sincerely welcome critiques and corrections from teachers and fellow students. +I'm keenly aware of the limits of my knowledge and experience. Although the content of this book has been refined over a period of time, there are certainly still many errors, and I sincerely welcome critiques and corrections from teachers and fellow students. ![Hello Algorithms](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" } @@ -21,10 +21,10 @@ I'm acutely aware of my limited knowledge and shallow expertise. Although the co

Hello, Algorithms!

-The advent of computers has brought tremendous changes to the world. With their high-speed computing capabilities and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics in video games, the intelligent decision-making in autonomous driving, AlphaGo's brilliant Go matches, or ChatGPT's natural interactions, these applications are all exquisite interpretations of algorithms on computers. +The advent of computers has brought tremendous changes to the world. With their high-speed computing capabilities and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics in video games, the intelligent decision-making in autonomous driving, AlphaGo's brilliant Go matches, or ChatGPT's natural interactions, these applications are all striking demonstrations of algorithms at work on computers. In fact, before the advent of computers, algorithms and data structures already existed in every corner of the world. Early algorithms were relatively simple, such as ancient counting methods and tool-making procedures. As civilization progressed, algorithms gradually became more refined and complex. From the ingenious craftsmanship of master artisans, to industrial products that liberate productive forces, to the scientific laws governing the operation of the universe, behind almost every ordinary or astonishing thing lies ingenious algorithmic thinking. Similarly, data structures are everywhere: from large-scale social networks to small subway systems, many systems can be modeled as "graphs"; from a nation to a family, the primary organizational forms of society exhibit characteristics of "trees"; winter clothing is like a "stack," where the first item put on is the last to be taken off; a badminton tube is like a "queue," with items inserted at one end and retrieved from the other; a dictionary is like a "hash table," enabling quick lookup of target entries. -This book aims to help readers understand the core concepts of algorithms and data structures through clear and accessible animated illustrations and runnable code examples, and to implement them through programming. Building on this foundation, the book endeavors to reveal the vivid manifestations of algorithms in the complex world and showcase the beauty of algorithms. I hope this book can be of help to you! +This book aims to help readers understand the core concepts of algorithms and data structures through clear and accessible animated illustrations and runnable code examples, and to implement them in code. Building on this foundation, the book endeavors to reveal the vivid manifestations of algorithms in the complex world and showcase the beauty of algorithms. I hope this book can be of help to you! diff --git a/en/docs/chapter_introduction/algorithms_are_everywhere.md b/en/docs/chapter_introduction/algorithms_are_everywhere.md index 84fbb4c13..c7583826f 100644 --- a/en/docs/chapter_introduction/algorithms_are_everywhere.md +++ b/en/docs/chapter_introduction/algorithms_are_everywhere.md @@ -6,11 +6,11 @@ comments: true When we hear the term "algorithm," we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives. -Before we start discussing about algorithms officially, there's an interesting fact worth sharing: **you've learned many algorithms unconsciously and are used to applying them in your daily life**. Here, I will give a few specific examples to prove this point. +Before we formally explore algorithms, here's an interesting fact worth sharing: **you have already learned many algorithms without realizing it, and you are used to applying them in daily life**. Let me give a few specific examples to illustrate this point. **Example 1: Looking Up a Dictionary**. In an English dictionary, words are listed alphabetically. Assuming we're searching for a word that starts with the letter $r$, this is typically done in the following way: -1. Open the dictionary to about halfway and check the first vocabulary of the page, let's say the letter starts with $m$. +1. Open the dictionary to about halfway and check the first word on that page; suppose it starts with the letter $m$. 2. Since $r$ comes after $m$ in the alphabet, the first half can be ignored and the search space is narrowed down to the second half. 3. Repeat steps `1.` and `2.` until you find the page where the word starts with $r$. @@ -33,7 +33,7 @@ Before we start discussing about algorithms officially, there's an interesting f Looking up a dictionary, an essential skill for elementary school students is actually the famous "Binary Search" algorithm. From a data structure perspective, we can consider the dictionary as a sorted "array"; from an algorithmic perspective, the series of actions taken to look up a word in the dictionary can be viewed as the algorithm "Binary Search." -**Example 2: Organizing Card Deck**. When playing cards, we need to arrange the cards in our hands in ascending order, as shown in the following process. +**Example 2: Organizing Playing Cards**. When playing cards, we need to arrange the cards in our hands in ascending order, as shown in the following process. 1. Divide the playing cards into "ordered" and "unordered" sections, assuming initially the leftmost card is already in order. 2. Take out a card from the unordered section and insert it into the correct position in the ordered section; after this, the leftmost two cards are in order. @@ -43,11 +43,11 @@ Looking up a dictionary, an essential skill for elementary school students is ac

Figure 1-2   Process of sorting a deck of cards

-The above method of organizing playing cards is practically the "Insertion Sort" algorithm, which is very efficient for small datasets. Many programming languages' sorting functions include the insertion sort. +The above method of organizing playing cards is essentially the "Insertion Sort" algorithm, which is very efficient for small datasets. Many programming languages' built-in sorting implementations use insertion sort internally. **Example 3: Making Change**. Assume making a purchase of $69$ at a supermarket. If you give the cashier $100$, they will need to provide you with $31$ in change. This process can be clearly understood as illustrated in Figure 1-3. -1. The options are currencies valued below $31$, including $1$, $5$, $10$, and $20$. +1. The available denominations smaller than $31$ are $1$, $5$, $10$, and $20$. 2. Take out the largest $20$ from the options, leaving $31 - 20 = 11$. 3. Take out the largest $10$ from the remaining options, leaving $11 - 10 = 1$. 4. Take out the largest $1$ from the remaining options, leaving $1 - 1 = 0$. @@ -57,10 +57,10 @@ The above method of organizing playing cards is practically the "Insertion Sort"

Figure 1-3   Process of making change

-In the steps described, we choose the best option at each stage by utilizing the largest denomination available, which leads to an effective change-making strategy. From a data structures and algorithms perspective, this approach is known as a "Greedy" algorithm. +In the steps above, we choose what seems to be the best option at each stage by using the largest denomination available, which leads to an effective way to make change. From a data structures and algorithms perspective, this approach is known as a "Greedy" algorithm. From cooking a meal to interstellar travel, almost all problem-solving involves algorithms. The advent of computers allows us to store data structures in memory and write code to call the CPU and GPU to execute algorithms. In this way, we can transfer real-life problems to computers and solve various complex issues in a more efficient way. !!! tip - If you are still confused about concepts like data structures, algorithms, arrays, and binary searches, I encourage you to keep reading. This book will gently guide you into the realm of understanding data structures and algorithms. + If concepts such as data structures, algorithms, arrays, and binary search still feel only half-familiar, keep reading. This book will guide you into the world of data structures and algorithms. diff --git a/en/docs/chapter_introduction/summary.md b/en/docs/chapter_introduction/summary.md index ea82bae5b..af550b900 100644 --- a/en/docs/chapter_introduction/summary.md +++ b/en/docs/chapter_introduction/summary.md @@ -6,13 +6,13 @@ comments: true ### 1.   Key Review -- Algorithms are ubiquitous in daily life and are not distant, esoteric knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life. +- Algorithms are ubiquitous in daily life and are not some distant, esoteric body of knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life. - The principle of looking up a dictionary is consistent with the binary search algorithm. Binary search embodies the important algorithmic idea of divide and conquer. - The process of organizing playing cards is very similar to the insertion sort algorithm. Insertion sort is suitable for sorting small datasets. - The steps of making change are essentially a greedy algorithm, where the best choice is made at each step based on the current situation. -- An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time, while a data structure is the way computers organize and store data. +- An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time, while a data structure is a way of organizing and storing data in a computer. - Data structures and algorithms are closely connected. Data structures are the foundation of algorithms, and algorithms breathe life into data structures. -- We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the shape and connection method of the blocks represent the data structure, and the steps to assemble the blocks correspond to the algorithm. +- We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the way they are shaped and connected represents the data structure, and the steps used to assemble them correspond to the algorithm. ### 2.   Q & A @@ -20,9 +20,9 @@ comments: true If we compare specific work skills to "techniques" in martial arts, then fundamental subjects should be more like "internal skills". -I believe the significance of learning algorithms (and other fundamental subjects) is not to implement them from scratch at work, but rather to be able to make professional reactions and judgments when solving problems based on the knowledge learned, thereby improving the overall quality of work. Here is a simple example. Every programming language has a built-in sorting function: +I believe the significance of learning algorithms (and other fundamental subjects) is not that you will need to implement them from scratch at work, but that the knowledge you gain enables you to make sound professional judgments when solving problems, thereby improving the overall quality of your work. Here is a simple example. Every programming language has a built-in sorting function: - If we have not studied data structures and algorithms, we might simply feed any given data to this sorting function. It runs smoothly with good performance, and there doesn't seem to be any problem. - But if we have studied algorithms, we would know that the time complexity of the built-in sorting function is $O(n \log n)$. However, if the given data consists of integers with a fixed number of digits (such as student IDs), we can use the more efficient "radix sort", reducing the time complexity to $O(nk)$, where $k$ is the number of digits. When the data volume is very large, the saved running time can create significant value (reduced costs, improved experience, etc.). -In the field of engineering, a large number of problems are difficult to reach optimal solutions, and many problems are only solved "approximately". The difficulty of a problem depends on one hand on the nature of the problem itself, and on the other hand on the knowledge reserve of the person observing the problem. The more complete a person's knowledge and the more experience they have, the deeper their analysis of the problem will be, and the more elegantly the problem can be solved. +In engineering, many problems are difficult to solve optimally, and many others are only solved "well enough." The difficulty of a problem depends, on the one hand, on the nature of the problem itself and, on the other hand, on the knowledge of the person examining it. The more complete a person's knowledge and the more experience they have, the deeper their analysis will be, and the more elegantly the problem can be solved. diff --git a/en/docs/chapter_introduction/what_is_dsa.md b/en/docs/chapter_introduction/what_is_dsa.md index 5d7439946..4d309afb2 100644 --- a/en/docs/chapter_introduction/what_is_dsa.md +++ b/en/docs/chapter_introduction/what_is_dsa.md @@ -9,12 +9,12 @@ comments: true An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time. It has the following characteristics. - The problem is well-defined, with clear input and output definitions. -- It is feasible and can be completed within a finite number of steps, time, and memory space. +- It is feasible and can be completed with finite steps, time, and memory. - Each step has a definite meaning, and under the same input and operating conditions, the output is always the same. ## 1.2.2   Data Structure Definition -A data structure is a way of organizing and storing data, covering the data content, relationships between data, and methods for data operations. It has the following design objectives. +A data structure is a way of organizing and storing data, including the data itself, the relationships between data elements, and the methods used to operate on them. It has the following design objectives. - Occupy as little space as possible to save computer memory. - Data operations should be as fast as possible, covering data access, addition, deletion, update, etc. @@ -58,7 +58,7 @@ The detailed correspondence between the two is shown in Table 1-1. -It is worth noting that data structures and algorithms are independent of programming languages. For this reason, this book is able to provide implementations based on multiple programming languages. +It is worth noting that data structures and algorithms are independent of programming languages. That is why this book can provide implementations in multiple programming languages. !!! tip "Conventional abbreviation" diff --git a/en/docs/chapter_preface/about_the_book.md b/en/docs/chapter_preface/about_the_book.md index 635cb17a6..deff3a39a 100644 --- a/en/docs/chapter_preface/about_the_book.md +++ b/en/docs/chapter_preface/about_the_book.md @@ -6,28 +6,28 @@ comments: true This project aims to create an open-source, free, beginner-friendly introductory tutorial on data structures and algorithms. -- The entire book uses animated illustrations, with clear and easy-to-understand content and a smooth learning curve, guiding beginners to explore the knowledge map of data structures and algorithms. +- The entire book uses animated illustrations, with clear and easy-to-understand content and a smooth learning curve, guiding beginners through the landscape of data structures and algorithms. - The source code can be run with one click, helping readers improve their programming skills through practice and understand how algorithms work and the underlying implementation of data structures. - We encourage readers to learn from each other, and everyone is welcome to ask questions and share insights in the comments section, making progress together through discussion and exchange. ## 0.1.1   Target Audience -If you are an algorithm beginner who has never been exposed to algorithms, or if you already have some problem-solving experience and have a vague understanding of data structures and algorithms, oscillating between knowing and not knowing, then this book is tailor-made for you! +If you are an algorithm beginner who has never studied algorithms, or if you already have some problem-solving experience but only a hazy understanding of data structures and algorithms, then this book is tailor-made for you! If you have already accumulated a certain amount of problem-solving experience and are familiar with most question types, this book can help you review and organize your algorithm knowledge system, and the repository's source code can be used as a "problem-solving toolkit" or "algorithm dictionary." -If you are an algorithm "expert," we look forward to receiving your valuable suggestions, or [participating in creation together](https://www.hello-algo.com/chapter_appendix/contribution/). +If you are an algorithm "expert," we look forward to receiving your valuable suggestions, or [joining us as a contributor](https://www.hello-algo.com/chapter_appendix/contribution/). !!! success "Prerequisites" - You need to have at least a programming foundation in any language, and be able to read and write simple code. + You need basic programming knowledge in at least one language and the ability to read and write simple code. ## 0.1.2   Content Structure The main content of this book is shown in Figure 0-1. - **Complexity analysis**: Evaluation dimensions and methods for data structures and algorithms. Methods for calculating time complexity and space complexity, common types, examples, etc. -- **Data structures**: Classification methods for basic data types and data structures. The definition, advantages and disadvantages, common operations, common types, typical applications, implementation methods, etc. of data structures such as arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. +- **Data structures**: Classification methods for basic data types and data structures. Definitions, advantages and disadvantages, common operations, common types, typical applications, implementation methods, and more for data structures such as arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. - **Algorithms**: The definition, advantages and disadvantages, efficiency, application scenarios, problem-solving steps, and example problems of algorithms such as searching, sorting, divide and conquer, backtracking, dynamic programming, and greedy algorithms. ![Main content of this book](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" } @@ -36,25 +36,25 @@ The main content of this book is shown in Figure 0-1. ## 0.1.3   Acknowledgements -This book has been continuously improved through the joint efforts of many contributors in the open-source community. Thanks to every contributor who invested time and effort, they are (in the order automatically generated by GitHub): krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, IsChristina, xBLACKICEx, guowei-gong, Cathay-Chen, pengchzn, QiLOL, magentaqin, hello-ikun, JoseHung, qualifier1024, thomasq0, sunshinesDL, L-Super, Guanngxu, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, Shyam-Chen, theNefelibatas, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, MolDuM, Nigh, Dr-XYZ, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, beatrix-chan, DullSword, xjr7670, jiaxianhua, qq909244296, iStig, boloboloda, hts0000, gledfish, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, llql1211, fbigm, echo1937, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, GaochaoZhu, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Allen-Scai, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, zhongfq, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Keynman, KeiichiKasai, and KawaiiAsh. +This book has been continuously improved through the joint efforts of many contributors in the open-source community. Thanks to every contributor who invested time and effort, they are (in the order automatically generated by GitHub): krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, pengchzn, QiLOL, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, hello-ikun, magentaqin, Guanngxu, thomasq0, sunshinesDL, L-Super, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, Shyam-Chen, sangxiaai, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, Nigh, Dr-XYZ, MolDuM, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, xjr7670, beatrix-chan, DullSword, qq909244296, iStig, boloboloda, hts0000, gledfish, fbigm, echo1937, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, JTCPOWI, KawaiiAsh, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, llql1211, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Senrian, Allen-Scai, 19santosh99, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, codetypess, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Kunchen-Luo, Keynman, and KeiichiKasai. -The code review work for this book was completed by coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (in alphabetical order). Thanks to them for the time and effort they put in, it is they who ensure the standardization and unity of code in various languages. +The code review work for this book was completed by coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (in alphabetical order). Thanks to them for the time and effort they put in; they helped keep the code consistent and standardized across the different language versions. -The Traditional Chinese version of this book was reviewed by Shyam-Chen and Dr-XYZ, the English version was reviewed by yuelinxin, K3v123, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn, thomasq0 and magentaqin, and the Japanese edition was reviewed by eltociear. It is because of their continuous contributions that this book can serve a wider readership, and we thank them. +The English version of this book was reviewed by yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn and thomasq0; the Japanese version was reviewed by eltociear; the Russian version was reviewed by И. А. Шевкун and Yuyan Huang; and the Traditional Chinese version was reviewed by Shyam-Chen and Dr-XYZ. Thanks to their contributions, this book is able to serve a broader readership, and we are deeply grateful to them. The ePub ebook generation tool for this book was developed by zhongfq. We thank him for his contribution, which provides readers with a more flexible way to read. During the creation of this book, I received help from many people. - Thanks to my mentor at the company, Dr. Li Xi, who encouraged me to "take action quickly" during a conversation, strengthening my determination to write this book; -- Thanks to my girlfriend Bubble as the first reader of this book, who provided many valuable suggestions from the perspective of an algorithm beginner, making this book more suitable for novices to read; +- Thanks to my girlfriend Bubble, the first reader of this book, who provided many valuable suggestions from the perspective of an algorithm beginner, making this book more approachable for beginners; - Thanks to Tengbao, Qibao, and Feibao for coming up with a creative name for this book, evoking everyone's fond memories of writing their first line of code "Hello World!"; - Thanks to Xiaoquan for providing professional help in intellectual property rights, which played an important role in the improvement of this open-source book; -- Thanks to Sutong for designing the beautiful cover and logo for this book, and for patiently making revisions multiple times driven by my obsessive-compulsive disorder; -- Thanks to @squidfunk for the typesetting suggestions, as well as for developing the open-source documentation theme [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master). +- Thanks to Sutong for designing the beautiful cover and logo for this book, and for patiently revising them many times at my perfectionist insistence; +- Thanks to @squidfunk for the typesetting suggestions, as well as for developing the open-source documentation theme [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material). -During the writing process, I read many textbooks and articles on data structures and algorithms. These works provided excellent examples for this book and ensured the accuracy and quality of the book's content. I would like to thank all the teachers and predecessors for their outstanding contributions! +During the writing process, I read many textbooks and articles on data structures and algorithms. These works served as excellent models for this book and helped ensure the accuracy and quality of its content. I would like to thank all the teachers and predecessors for their outstanding contributions! -This book advocates a learning method that combines hands and brain, and in this regard I was deeply inspired by [Dive into Deep Learning](https://github.com/d2l-ai/d2l-zh). I highly recommend this excellent work to all readers. +This book advocates a hands-on approach to learning, and in this respect I was deeply inspired by [Dive into Deep Learning](https://github.com/d2l-ai/d2l-zh). I highly recommend this excellent work to all readers. -**Heartfelt thanks to my parents, it is your support and encouragement that has given me the opportunity to do this interesting thing**. +**Heartfelt thanks to my parents. It is your support and encouragement that gave me the opportunity to pursue this enjoyable project**. diff --git a/en/docs/chapter_preface/suggestions.md b/en/docs/chapter_preface/suggestions.md index 0e8062163..cde095aba 100644 --- a/en/docs/chapter_preface/suggestions.md +++ b/en/docs/chapter_preface/suggestions.md @@ -10,12 +10,12 @@ comments: true ## 0.2.1   Writing Style Conventions -- Titles marked with `*` are optional sections with relatively difficult content. If you have limited time, you can skip them first. -- Technical terms will be in bold (in paper and PDF versions) or underlined (in web versions), such as array. It is recommended to memorize them for reading literature. +- Sections marked with `*` after the title are optional and somewhat more challenging. If you're short on time, you can skip them on your first pass. +- Technical terms are shown in bold (in the print and PDF editions) or underlined (in the web edition), such as array. They are worth remembering, as they will help when reading technical literature. - Key content and summary statements will be **bolded**, and such text deserves special attention. - Words and phrases with specific meanings will be marked with "quotation marks" to avoid ambiguity. -- When it comes to nouns that are inconsistent between programming languages, this book uses Python as the standard, for example, using `None` to represent "null". -- This book partially abandons the comment conventions of programming languages in favor of more compact content layout. Comments are mainly divided into three types: title comments, content comments, and multi-line comments. +- When terminology differs across programming languages, this book follows Python conventions; for example, it uses `None` to represent "null". +- This book partially relaxes conventional programming-language comment styles in favor of a more compact layout. Comments are mainly divided into three types: title comments, content comments, and multi-line comments. === "Python" @@ -141,10 +141,8 @@ comments: true // Content comment, used to explain code in detail - /** - * Multi-line - * comment - */ + // Multi-line + // comment ``` === "C" @@ -186,9 +184,9 @@ comments: true ## 0.2.2   Learning Efficiently with Animated Illustrations -Compared to text, videos and images have higher information density and structural organization, making them easier to understand. In this book, **key and difficult knowledge will mainly be presented in the form of animated illustrations**, with text serving as explanation and supplement. +Compared with plain text, videos and images have higher information density and a clearer structure, making them easier to understand. In this book, **key concepts and challenging topics are presented mainly through animated illustrations**, with text serving as explanation and supplement. -If you find that a section of content provides animated illustrations as shown in Figure 0-2 while reading this book, **please focus on the illustrations first, with text as a supplement**, and combine the two to understand the content. +If, while reading this book, you encounter an animated illustration like the one shown below, **treat the illustration as primary and the text as supplementary**, and use both together to understand the content. ![Example of animated illustrations](../index.assets/animation.gif){ class="animation-figure" } @@ -200,15 +198,15 @@ The accompanying code for this book is hosted in the [GitHub repository](https:/ If time permits, **it is recommended that you type out the code yourself**. If you have limited study time, please at least read through and run all the code. -Compared to reading code, the process of writing code often brings more rewards. **Learning by doing is the real learning**. +Compared with simply reading code, writing it yourself often brings greater rewards. **Hands-on practice is where real learning happens**. ![Example of running code](../index.assets/running_code.gif){ class="animation-figure" }

Figure 0-3   Example of running code

-The preliminary work for running code is mainly divided into three steps. +Getting the code running mainly involves three preliminary steps. -**Step 1: Install the local programming environment**. Please follow the [tutorial](https://www.hello-algo.com/chapter_appendix/installation/) shown in the appendix for installation. If already installed, you can skip this step. +**Step 1: Install the local programming environment**. Please follow the [tutorial](https://www.hello-algo.com/chapter_appendix/installation/) in the appendix. If it is already installed, you can skip this step. **Step 2: Clone or download the code repository**. Visit the [GitHub repository](https://github.com/krahets/hello-algo). If you have already installed [Git](https://git-scm.com/downloads), you can clone this repository with the following command: @@ -216,7 +214,7 @@ The preliminary work for running code is mainly divided into three steps. git clone https://github.com/krahets/hello-algo.git ``` -Of course, you can also click the "Download ZIP" button at the location shown in Figure 0-4 to directly download the code compressed package, and then extract it locally. +Alternatively, you can click the "Download ZIP" button shown below to download a ZIP archive of the repository directly and then extract it locally. ![Clone repository and download code](suggestions.assets/download_code.png){ class="animation-figure" } @@ -228,7 +226,7 @@ Of course, you can also click the "Download ZIP" button at the location shown in

Figure 0-5   Code blocks and corresponding source code files

-In addition to running code locally, **the web version also supports visual running of Python code** (implemented based on [pythontutor](https://pythontutor.com/)). As shown in Figure 0-6, you can click "Visual Run" below the code block to expand the view and observe the execution process of the algorithm code; you can also click "Full Screen View" for a better viewing experience. +In addition to running code locally, **the web version also supports visual execution of Python code** (implemented based on [pythontutor](https://pythontutor.com/)). As shown in Figure 0-6, you can click "Visual Run" below the code block to expand the view and observe the execution process of the algorithm code; you can also click "Full Screen View" for a better viewing experience. ![Visual running of Python code](suggestions.assets/pythontutor_example.png){ class="animation-figure" } @@ -236,9 +234,9 @@ In addition to running code locally, **the web version also supports visual runn ## 0.2.4   Growing Together Through Questions and Discussions -When reading this book, please do not easily skip knowledge points that you have not learned well. **Feel free to ask your questions in the comments section**, and my friends and I will do our best to answer you, and generally reply within two days. +When reading this book, please do not skip over points that you still do not fully understand. **Feel free to ask your questions in the comments section**, and my friends and I will do our best to answer them, usually within two days. -As shown in Figure 0-7, the web version has a comments section at the bottom of each chapter. I hope you will pay more attention to the content of the comments section. On the one hand, you can learn about the problems that everyone encounters, thus checking for omissions and stimulating deeper thinking. On the other hand, I hope you can generously answer other friends' questions, share your insights, and help others progress. +As shown in Figure 0-7, the web version has a comments section at the bottom of each chapter. I encourage you to pay close attention to the discussions there. On the one hand, you can learn about the problems that others encounter, thereby filling gaps in your own understanding and prompting deeper thought. On the other hand, I hope you will generously answer other readers' questions, share your insights, and help others improve. ![Example of comments section](../index.assets/comment.gif){ class="animation-figure" } @@ -246,10 +244,10 @@ As shown in Figure 0-7, the web version has a comments section at the bottom of ## 0.2.5   Algorithm Learning Roadmap -From an overall perspective, we can divide the process of learning data structures and algorithms into three stages. +Overall, we can divide the process of learning data structures and algorithms into three stages. 1. **Stage 1: Algorithm introduction**. We need to familiarize ourselves with the characteristics and usage of various data structures, and learn the principles, processes, uses, and efficiency of different algorithms. -2. **Stage 2: Practice algorithm problems**. It is recommended to start with popular problems, and accumulate at least 100 problems first, to familiarize yourself with mainstream algorithm problems. When first practicing problems, "knowledge forgetting" may be a challenge, but rest assured, this is very normal. We can review problems according to the "Ebbinghaus forgetting curve", and usually after 3-5 rounds of repetition, we can firmly remember them. For recommended problem lists and practice plans, please see this [GitHub repository](https://github.com/krahets/LeetCode-Book). +2. **Stage 2: Practice algorithm problems**. It is recommended to start with popular problems and solve at least 100 of them first, so that you become familiar with mainstream algorithm questions. When you first begin practicing problems, "knowledge forgetting" may feel like a challenge, but rest assured, this is very normal. We can review problems according to the "Ebbinghaus forgetting curve", and after 3-5 rounds of repetition, they usually stick firmly in memory. For recommended problem lists and practice plans, please see this [GitHub repository](https://github.com/krahets/LeetCode-Book). 3. **Stage 3: Building a knowledge system**. In terms of learning, we can read algorithm column articles, problem-solving frameworks, and algorithm textbooks to continuously enrich our knowledge system. In terms of practicing problems, we can try advanced problem-solving strategies, such as categorization by topic, one problem multiple solutions, one solution multiple problems, etc. Related problem-solving insights can be found in various communities. As shown in Figure 0-8, the content of this book mainly covers "Stage 1", aiming to help you more efficiently carry out Stage 2 and Stage 3 learning. diff --git a/en/docs/chapter_preface/summary.md b/en/docs/chapter_preface/summary.md index 47dd82f78..bf7f4cdfa 100644 --- a/en/docs/chapter_preface/summary.md +++ b/en/docs/chapter_preface/summary.md @@ -6,9 +6,9 @@ comments: true ### 1.   Key Review -- The main audience of this book is algorithm beginners. If you already have a certain foundation, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a "problem-solving toolkit." +- The main audience of this book is algorithm beginners. If you already have some background, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a "problem-solving toolkit." - The content of the book mainly includes three parts: complexity analysis, data structures, and algorithms, covering most topics in this field. - For algorithm novices, reading an introductory book during the initial learning stage is crucial, as it can help you avoid many detours. -- The animated illustrations in the book are usually used to introduce key and difficult knowledge. When reading this book, you should pay more attention to these contents. +- The animated illustrations in the book are usually used to introduce key concepts and challenging topics. When reading this book, you should pay more attention to these topics. - Practice is the best way to learn programming. It is strongly recommended to run the source code and type the code yourself. - The web version of this book has a comments section for each chapter, where you are welcome to share your questions and insights at any time. diff --git a/en/docs/chapter_reference/index.md b/en/docs/chapter_reference/index.md index 2c015705f..1f9dfc05f 100644 --- a/en/docs/chapter_reference/index.md +++ b/en/docs/chapter_reference/index.md @@ -16,7 +16,7 @@ icon: material/bookshelf [6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition). -[7] Cheng Jie. Conversational Data Structures. +[7] Cheng Jie. Data Structures in Plain Language. [8] Wang Zheng. The Beauty of Data Structures and Algorithms. diff --git a/en/docs/chapter_searching/binary_search.md b/en/docs/chapter_searching/binary_search.md index 9990d1067..d1d10f194 100644 --- a/en/docs/chapter_searching/binary_search.md +++ b/en/docs/chapter_searching/binary_search.md @@ -4,7 +4,7 @@ comments: true # 10.1   Binary Search -Binary search is an efficient searching algorithm based on the divide-and-conquer strategy. It leverages the orderliness of data to reduce the search range by half in each round until the target element is found or the search interval becomes empty. +Binary search is an efficient search algorithm based on the divide-and-conquer strategy. It leverages the sorted order of the data to reduce the search range by half in each round until the target element is found or the search interval becomes empty. !!! question @@ -24,7 +24,7 @@ Next, perform the following two steps in a loop: 2. When `nums[m] > target`, it indicates that `target` is in the interval $[i, m - 1]$, so execute $j = m - 1$. 3. When `nums[m] = target`, it indicates that `target` has been found, so return index $m$. -If the array does not contain the target element, the search interval will eventually shrink to empty. In this case, return $-1$. +If the array does not contain the target element, the search interval will eventually become empty. In this case, return $-1$. === "<1>" ![Binary search process](binary_search.assets/binary_search_step1.png){ class="animation-figure" } @@ -49,7 +49,7 @@ If the array does not contain the target element, the search interval will event

Figure 10-2   Binary search process

-It's worth noting that since both $i$ and $j$ are of `int` type, **$i + j$ may exceed the range of the `int` type**. To avoid large number overflow, we typically use the formula $m = \lfloor {i + (j - i) / 2} \rfloor$ to calculate the midpoint. +It's worth noting that since both $i$ and $j$ are of `int` type, **$i + j$ may exceed the range of the `int` type**. To avoid integer overflow, we typically use the formula $m = \lfloor {i + (j - i) / 2} \rfloor$ to calculate the midpoint. The code is shown below: @@ -362,13 +362,13 @@ The code is shown below: end ``` -**Time complexity is $O(\log n)$**: In the binary loop, the interval is reduced by half each round, so the number of loops is $\log_2 n$. +**Time complexity is $O(\log n)$**: In the binary search loop, the interval is reduced by half each round, so the number of iterations is $\log_2 n$. **Space complexity is $O(1)$**: Pointers $i$ and $j$ use constant-size space. ## 10.1.1   Interval Representation Methods -In addition to the closed interval mentioned above, another common interval representation is the "left-closed right-open" interval, defined as $[0, n)$, meaning the left boundary includes itself while the right boundary does not. Under this representation, the interval $[i, j)$ is empty when $i = j$. +In addition to the closed interval mentioned above, another common interval representation is the "left-closed right-open" interval, defined as $[0, n)$, meaning that the left boundary is inclusive while the right boundary is exclusive. Under this representation, the interval $[i, j)$ is empty when $i = j$. We can implement a binary search algorithm with the same functionality based on this representation: @@ -691,13 +691,13 @@ Since both the left and right boundaries in the "closed interval" representation ## 10.1.2   Advantages and Limitations -Binary search performs well in both time and space aspects. +Binary search offers good performance in both time and space. -- Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size $n = 2^{20}$, linear search requires $2^{20} = 1048576$ loop rounds, while binary search only needs $\log_2 2^{20} = 20$ rounds. +- Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size $n = 2^{20}$, linear search requires $2^{20} = 1048576$ iterations, while binary search only needs $\log_2 2^{20} = 20$ iterations. - Binary search requires no extra space. Compared to searching algorithms that require additional space (such as hash-based search), binary search is more space-efficient. However, binary search is not suitable for all situations, mainly for the following reasons: -- Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of $O(n \log n)$, which is higher than both linear search and binary search. For scenarios with frequent element insertions, maintaining array orderliness requires inserting elements at specific positions with a time complexity of $O(n)$, which is also very expensive. -- Binary search is only applicable to arrays. Binary search requires jump-style (non-contiguous) element access, and jump-style access has low efficiency in linked lists, making it unsuitable for linked lists or data structures based on linked list implementations. +- Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of $O(n \log n)$, which is higher than both linear search and binary search. For scenarios with frequent element insertions, keeping the array sorted requires inserting elements at specific positions with a time complexity of $O(n)$, which is also very expensive. +- Binary search is only applicable to arrays. Binary search requires non-contiguous, jump-style access to elements, and this kind of access is inefficient in linked lists, making it unsuitable for linked lists or linked-list-based data structures. - For small data volumes, linear search performs better. In linear search, each round requires only 1 comparison operation; while in binary search, it requires 1 addition, 1 division, 1-3 comparison operations, and 1 addition (subtraction), totaling 4-6 unit operations. Therefore, when the data volume $n$ is small, linear search is actually faster than binary search. diff --git a/en/docs/chapter_searching/binary_search_edge.md b/en/docs/chapter_searching/binary_search_edge.md index 669ebc9c3..33767854b 100644 --- a/en/docs/chapter_searching/binary_search_edge.md +++ b/en/docs/chapter_searching/binary_search_edge.md @@ -2,13 +2,13 @@ comments: true --- -# 10.3   Binary Search Edge Cases +# 10.3   Binary Search Boundaries ## 10.3.1   Finding the Left Boundary !!! question - Given a sorted array `nums` of length $n$ that may contain duplicate elements, return the index of the leftmost element `target` in the array. If the array does not contain the element, return $-1$. + Given a sorted array `nums` of length $n$ that may contain duplicate elements, return the index of the leftmost occurrence of `target`. If the array does not contain `target`, return $-1$. Recall the method for finding the insertion point with binary search. After the search completes, $i$ points to the leftmost `target`, **so finding the insertion point is essentially finding the index of the leftmost `target`**. @@ -232,9 +232,9 @@ Below we introduce two more clever methods. ### 1.   Reusing Left Boundary Search -In fact, we can use the function for finding the leftmost element to find the rightmost element. The specific method is: **Convert finding the rightmost `target` into finding the leftmost `target + 1`**. +In fact, we can use the function for finding the leftmost `target` to find the rightmost `target`. The specific method is: **convert finding the rightmost `target` into finding the leftmost `target + 1`**. -As shown in Figure 10-7, after the search completes, pointer $i$ points to the leftmost `target + 1` (if it exists), while $j$ points to the rightmost `target`, **so we can simply return $j$**. +As shown in Figure 10-7, after the search completes, the pointer $i$ points to the leftmost `target + 1` (if it exists), while $j$ points to the rightmost `target`, **so we can return $j$**. ![Converting right boundary search to left boundary search](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png){ class="animation-figure" } @@ -480,8 +480,8 @@ We know that when the array does not contain `target`, $i$ and $j$ will eventual Therefore, as shown in Figure 10-8, we can construct an element that does not exist in the array to find the left and right boundaries. -- Finding the leftmost `target`: Can be converted to finding `target - 0.5` and returning pointer $i$. -- Finding the rightmost `target`: Can be converted to finding `target + 0.5` and returning pointer $j$. +- Finding the leftmost `target`: This can be converted to finding `target - 0.5` and returning the pointer $i$. +- Finding the rightmost `target`: This can be converted to finding `target + 0.5` and returning the pointer $j$. ![Converting boundary search to element search](binary_search_edge.assets/binary_search_edge_by_element.png){ class="animation-figure" } @@ -489,5 +489,5 @@ Therefore, as shown in Figure 10-8, we can construct an element that does not ex The code is omitted here, but the following two points are worth noting: -- Since the given array does not contain decimals, we don't need to worry about how to handle equal cases. +- Since the given array does not contain decimal values, we do not need to worry about how to handle equality. - Because this method introduces decimals, the variable `target` in the function needs to be changed to a floating-point type (Python does not require this change). diff --git a/en/docs/chapter_searching/binary_search_insertion.md b/en/docs/chapter_searching/binary_search_insertion.md index 040cfccf3..90c218920 100644 --- a/en/docs/chapter_searching/binary_search_insertion.md +++ b/en/docs/chapter_searching/binary_search_insertion.md @@ -4,13 +4,13 @@ comments: true # 10.2   Binary Search Insertion Point -Binary search can not only be used to search for target elements but also to solve many variant problems, such as searching for the insertion position of a target element. +Binary search can be used not only to search for target elements, but also to solve many variant problems, such as finding the insertion position of a target element. ## 10.2.1   Case Without Duplicate Elements !!! question - Given a sorted array `nums` of length $n$ and an element `target`, where the array contains no duplicate elements. Insert `target` into the array `nums` while maintaining its sorted order. If the array already contains the element `target`, insert it to its left. Return the index of `target` in the array after insertion. An example is shown in Figure 10-4. + Given a sorted array `nums` of length $n$ and an element `target`, where the array contains no duplicate elements, insert `target` into `nums` while maintaining its sorted order. If `target` already exists in the array, insert it to its left. Return the index of `target` after insertion. An example is shown below. ![Binary search insertion point example data](binary_search_insertion.assets/binary_search_insertion_example.png){ class="animation-figure" } @@ -24,9 +24,9 @@ The problem requires inserting `target` to the left of equal elements, which mea **Question 2**: When the array does not contain `target`, what is the insertion point index? -Further consider the binary search process: When `nums[m] < target`, $i$ moves, which means pointer $i$ is approaching elements greater than or equal to `target`. Similarly, pointer $j$ is always approaching elements less than or equal to `target`. +To analyze this further, consider the binary search process: when `nums[m] < target`, $i$ moves, meaning that pointer $i$ is approaching elements greater than or equal to `target`. Similarly, pointer $j$ is always approaching elements less than or equal to `target`. -Therefore, when the binary search ends, we must have: $i$ points to the first element greater than `target`, and $j$ points to the first element less than `target`. **It's easy to see that when the array does not contain `target`, the insertion index is $i$**. The code is shown below: +Therefore, when the binary search ends, $i$ must point to the first element greater than `target`, and $j$ must point to the first element less than `target`. **It follows that when the array does not contain `target`, the insertion index is $i$**. The code is shown below: === "Python" @@ -323,7 +323,7 @@ Therefore, when the binary search ends, we must have: $i$ points to the first el Suppose there are multiple `target` elements in the array. Ordinary binary search can only return the index of one `target`, **and cannot determine how many `target` elements are to the left and right of that element**. -The problem requires inserting the target element at the leftmost position, **so we need to find the index of the leftmost `target` in the array**. Initially, consider implementing this through the steps shown in Figure 10-5: +The problem requires inserting the target element at the leftmost position, **so we need to find the index of the leftmost `target` in the array**. A straightforward initial approach is to follow the steps shown in Figure 10-5: 1. Perform binary search to obtain the index of any `target`, denoted as $k$. 2. Starting from index $k$, perform linear traversal to the left, and return when the leftmost `target` is found. @@ -334,10 +334,10 @@ The problem requires inserting the target element at the leftmost position, **so Although this method works, it includes linear search, resulting in a time complexity of $O(n)$. When the array contains many duplicate `target` elements, this method is very inefficient. -Now consider extending the binary search code. As shown in Figure 10-6, the overall process remains unchanged: calculate the midpoint index $m$ in each round, then compare `target` with `nums[m]`, divided into the following cases: +Now consider extending the binary search code. As shown in Figure 10-6, the overall process remains unchanged: in each iteration, we first compute the midpoint index $m$, then compare `target` with `nums[m]`, leading to the following cases: -- When `nums[m] < target` or `nums[m] > target`, it means `target` has not been found yet, so use the ordinary binary search interval narrowing operation to **make pointers $i$ and $j$ approach `target`**. -- When `nums[m] == target`, it means elements less than `target` are in the interval $[i, m - 1]$, so use $j = m - 1$ to narrow the interval, thereby **making pointer $j$ approach elements less than `target`**. +- When `nums[m] < target` or `nums[m] > target`, it means `target` has not been found yet, so use the standard interval-shrinking operation of binary search to **move pointers $i$ and $j$ closer to `target`**. +- When `nums[m] == target`, it means elements less than `target` are in the interval $[i, m - 1]$, so use $j = m - 1$ to shrink the interval, thereby **moving pointer $j$ closer to elements less than `target`**. After the loop completes, $i$ points to the leftmost `target`, and $j$ points to the first element less than `target`, **so index $i$ is the insertion point**. @@ -367,7 +367,7 @@ After the loop completes, $i$ points to the leftmost `target`, and $j$ points to

Figure 10-6   Steps for binary search insertion point of duplicate elements

-Observe the following code: the operations for branches `nums[m] > target` and `nums[m] == target` are the same, so the two can be merged. +Observe the following code: the branches `nums[m] > target` and `nums[m] == target` perform the same operation, so they can be merged. Even so, we can still keep the conditional branches expanded, as the logic is clearer and more readable. @@ -657,8 +657,8 @@ Even so, we can still keep the conditional branches expanded, as the logic is cl !!! tip - The code in this section all uses the "closed interval" approach. Interested readers can implement the "left-closed right-open" approach themselves. + The code in this section uses the "closed interval" approach throughout. Interested readers can implement the "left-closed, right-open" approach themselves. -Overall, binary search is simply about setting search targets for pointers $i$ and $j$ separately. The target could be a specific element (such as `target`) or a range of elements (such as elements less than `target`). +Overall, binary search is simply a matter of setting separate search targets for pointers $i$ and $j$. The target may be a specific element (such as `target`) or a range of elements (such as elements less than `target`). -Through continuous binary iterations, both pointers $i$ and $j$ gradually approach their preset targets. Ultimately, they either successfully find the answer or stop after crossing the boundaries. +With each iteration of binary search, pointers $i$ and $j$ gradually approach their preset targets. Ultimately, they either find the answer or stop after crossing the boundary. diff --git a/en/docs/chapter_searching/index.md b/en/docs/chapter_searching/index.md index 7dae9ebd4..4619ceddb 100644 --- a/en/docs/chapter_searching/index.md +++ b/en/docs/chapter_searching/index.md @@ -16,8 +16,8 @@ icon: material/text-search ## Chapter contents - [10.1   Binary Search](binary_search.md) -- [10.2   Binary Search Insertion](binary_search_insertion.md) -- [10.3   Binary Search Edge Cases](binary_search_edge.md) +- [10.2   Binary Search Insertion Point](binary_search_insertion.md) +- [10.3   Binary Search Boundaries](binary_search_edge.md) - [10.4   Hash Optimization Strategy](replace_linear_by_hashing.md) -- [10.5   Search Algorithms Revisited](searching_algorithm_revisited.md) +- [10.5   Searching Algorithms Revisited](searching_algorithm_revisited.md) - [10.6   Summary](summary.md) diff --git a/en/docs/chapter_searching/replace_linear_by_hashing.md b/en/docs/chapter_searching/replace_linear_by_hashing.md index fd76520ec..c7f06c37d 100644 --- a/en/docs/chapter_searching/replace_linear_by_hashing.md +++ b/en/docs/chapter_searching/replace_linear_by_hashing.md @@ -8,11 +8,11 @@ In algorithm problems, **we often reduce the time complexity of algorithms by re !!! question - Given an integer array `nums` and a target element `target`, search for two elements in the array whose "sum" equals `target`, and return their array indices. Any solution will do. + Given an integer array `nums` and a target value `target`, find two elements in the array whose sum is `target`, and return their indices. Any solution will do. ## 10.4.1   Linear Search: Trading Time for Space -Consider directly traversing all possible combinations. As shown in Figure 10-9, we open a two-layer loop and judge in each round whether the sum of two integers equals `target`. If so, return their indices. +Consider directly traversing all possible combinations. As shown in Figure 10-9, we use nested loops and check in each iteration whether the sum of two integers is `target`. If so, return their indices. ![Linear search solution for two sum](replace_linear_by_hashing.assets/two_sum_brute_force.png){ class="animation-figure" } @@ -241,11 +241,11 @@ The code is shown below: end ``` -This method has a time complexity of $O(n^2)$ and a space complexity of $O(1)$, which is very time-consuming with large data volumes. +This method has a time complexity of $O(n^2)$ and a space complexity of $O(1)$, making it very time-consuming on large inputs. ## 10.4.2   Hash-Based Search: Trading Space for Time -Consider using a hash table where key-value pairs are array elements and element indices respectively. Loop through the array, performing the steps shown in Figure 10-10 in each round: +Consider using a hash table whose keys are array elements and whose values are their indices. Traverse the array and perform the steps shown in Figure 10-10 in each iteration: 1. Check if the number `target - nums[i]` is in the hash table. If so, directly return the indices of these two elements. 2. Add the key-value pair `nums[i]` and index `i` to the hash table. @@ -261,7 +261,7 @@ Consider using a hash table where key-value pairs are array elements and element

Figure 10-10   Hash table solution for two sum

-The implementation code is shown below, requiring only a single loop: +The implementation is shown below and requires only a single loop: === "Python" @@ -533,4 +533,4 @@ The implementation code is shown below, requiring only a single loop: This method reduces the time complexity from $O(n^2)$ to $O(n)$ through hash-based search, greatly improving runtime efficiency. -Since an additional hash table needs to be maintained, the space complexity is $O(n)$. **Nevertheless, this method achieves a more balanced overall time-space efficiency, making it the optimal solution for this problem**. +Since an additional hash table needs to be maintained, the space complexity is $O(n)$. **Nevertheless, this method offers a more balanced overall time-space trade-off, making it the optimal solution to this problem**. diff --git a/en/docs/chapter_searching/searching_algorithm_revisited.md b/en/docs/chapter_searching/searching_algorithm_revisited.md index 140047713..b87086deb 100644 --- a/en/docs/chapter_searching/searching_algorithm_revisited.md +++ b/en/docs/chapter_searching/searching_algorithm_revisited.md @@ -9,9 +9,9 @@ comments: true Searching algorithms can be divided into the following two categories based on their implementation approach: - **Locating target elements by traversing the data structure**, such as traversing arrays, linked lists, trees, and graphs. -- **Achieving efficient element search by utilizing data organization structure or prior information contained in the data**, such as binary search, hash-based search, and binary search tree search. +- **Achieving efficient element lookup by leveraging the way data is organized or prior information about the data**, such as binary search, hash-based search, and binary search tree search. -It's not hard to see that these topics have all been covered in previous chapters, so searching algorithms are not unfamiliar to us. In this section, we will approach from a more systematic perspective and re-examine searching algorithms. +As these topics have already been introduced in earlier chapters, searching algorithms should already be familiar to us. In this section, we revisit them from a more systematic perspective. ## 10.5.1   Brute-Force Search @@ -26,11 +26,11 @@ However, **the time complexity of such algorithms is $O(n)$**, where $n$ is the ## 10.5.2   Adaptive Search -Adaptive search utilizes the unique properties of data (such as orderliness) to optimize the search process, thereby locating target elements more efficiently. +Adaptive search leverages properties of the data itself (such as sorted order) to optimize the search process and locate target elements more efficiently. - "Binary search" uses the orderliness of data to achieve efficient searching, applicable only to arrays. -- "Hash-based search" uses hash tables to establish key-value pair mappings between search data and target data, thereby achieving query operations. -- "Tree search" in specific tree structures (such as binary search trees), quickly eliminates nodes based on comparing node values to locate target elements. +- "Hash-based search" uses hash tables to store searchable data as key-value pairs, thereby enabling efficient queries. +- "Tree search" operates on specific tree structures (such as binary search trees), quickly ruling out nodes by comparing node values to locate the target element. The advantage of such algorithms is high efficiency, **with time complexity reaching $O(\log n)$ or even $O(1)$**. @@ -48,7 +48,7 @@ Given a dataset of size $n$, we can use linear search, binary search, tree searc

Figure 10-11   Multiple search strategies

-The operational efficiency and characteristics of the above methods are as follows: +The efficiency and characteristics of these methods are summarized in Table 10-1.

Table 10-1   Comparison of search algorithm efficiency

@@ -69,26 +69,26 @@ The choice of search algorithm also depends on data volume, search performance r **Linear search** -- Good generality, requiring no data preprocessing operations. If we only need to query the data once, the data preprocessing time for the other three methods would be longer than linear search. +- Good generality, requiring no data preprocessing operations. If we need to query the data only once, the preprocessing required by the other three methods can take longer than the linear search itself. - Suitable for small data volumes, where time complexity has less impact on efficiency. - Suitable for scenarios with high data update frequency, as this method does not require any additional data maintenance. **Binary search** -- Suitable for large data volumes with stable efficiency performance, worst-case time complexity of $O(\log n)$. +- Suitable for large datasets, with stable performance and a worst-case time complexity of $O(\log n)$. - Data volume cannot be too large, as storing arrays requires contiguous memory space. - Not suitable for scenarios with frequent data insertion and deletion, as maintaining a sorted array has high overhead. **Hash-based search** - Suitable for scenarios with high query performance requirements, with an average time complexity of $O(1)$. -- Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain data orderliness. +- Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain the data in sorted order. - High dependence on hash functions and hash collision handling strategies, with significant risk of performance degradation. - Not suitable for excessively large data volumes, as hash tables require extra space to minimize collisions and thus provide good query performance. **Tree search** -- Suitable for massive data, as tree nodes are stored dispersedly in memory. -- Suitable for scenarios requiring maintained ordered data or range searches. +- Suitable for massive datasets, as tree nodes are stored non-contiguously in memory. +- Suitable for scenarios that require maintaining ordered data or performing range searches. - During continuous node insertion and deletion, binary search trees may become skewed, degrading time complexity to $O(n)$. -- If using AVL trees or red-black trees, all operations can run stably at $O(\log n)$ efficiency, but operations to maintain tree balance add extra overhead. +- If AVL trees or red-black trees are used, all operations can consistently run in $O(\log n)$ time, though maintaining tree balance adds extra overhead. diff --git a/en/docs/chapter_searching/summary.md b/en/docs/chapter_searching/summary.md index 5cc10250c..32274cc4b 100644 --- a/en/docs/chapter_searching/summary.md +++ b/en/docs/chapter_searching/summary.md @@ -6,9 +6,9 @@ comments: true ### 1.   Key Review -- Binary search relies on data orderliness and progressively reduces the search interval by half through loops. It requires input data to be sorted and is only applicable to arrays or data structures based on array implementations. -- Brute-force search locates data by traversing the data structure. Linear search is applicable to arrays and linked lists, while breadth-first search and depth-first search are applicable to graphs and trees. Such algorithms have good generality and require no data preprocessing, but have a relatively high time complexity of $O(n)$. +- Binary search relies on ordered data and searches by repeatedly halving the search interval. It requires the input data to be sorted and applies only to arrays or array-based data structures. +- Brute-force search locates data by traversing the data structure. Linear search applies to arrays and linked lists, while breadth-first search and depth-first search apply to graphs and trees. These algorithms are broadly applicable and require no data preprocessing, but their relatively high time complexity is $O(n)$. - Hash-based search, tree search, and binary search are efficient search methods that can quickly locate target elements in specific data structures. Such algorithms are highly efficient with time complexity reaching $O(\log n)$ or even $O(1)$, but typically require additional data structures. - In practice, we need to analyze factors such as data scale, search performance requirements, and data query and update frequency to choose the appropriate search method. -- Linear search is suitable for small-scale or frequently updated data; binary search is suitable for large-scale, sorted data; hash-based search is suitable for data with high query efficiency requirements and no need for range queries; tree search is suitable for large-scale dynamic data that needs to maintain order and support range queries. +- Linear search is suitable for small datasets or data that is updated frequently; binary search is suitable for large sorted datasets; hash-based search is suitable when high query efficiency is required and range queries are unnecessary; tree search is suitable for large dynamic datasets that must maintain order and support range queries. - Replacing linear search with hash-based search is a commonly used strategy to optimize runtime, reducing time complexity from $O(n)$ to $O(1)$. diff --git a/en/docs/chapter_sorting/bubble_sort.md b/en/docs/chapter_sorting/bubble_sort.md index 75f2b557e..160a8d93d 100644 --- a/en/docs/chapter_sorting/bubble_sort.md +++ b/en/docs/chapter_sorting/bubble_sort.md @@ -4,12 +4,12 @@ comments: true # 11.3   Bubble Sort -Bubble sort (bubble sort) achieves sorting by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name bubble sort. +Bubble sort sorts an array by continuously comparing and swapping adjacent elements. This process resembles bubbles rising from the bottom to the top, hence the name bubble sort. -As shown in Figure 11-4, the bubbling process can be simulated using element swap operations: starting from the leftmost end of the array and traversing to the right, compare the size of adjacent elements, and if "left element > right element", swap them. After completing the traversal, the largest element will be moved to the rightmost end of the array. +As shown in Figure 11-4, the bubbling process can be simulated using element swaps: starting from the leftmost end of the array and traversing to the right, compare each pair of adjacent elements, and if "left element > right element", swap them. After the traversal is complete, the largest element is moved to the rightmost end of the array. === "<1>" - ![Simulating bubble using element swap operation](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" } + ![Simulating bubble sort using element swaps](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" } === "<2>" ![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png){ class="animation-figure" } @@ -29,7 +29,7 @@ As shown in Figure 11-4, the bubbling process can be simulated using element swa === "<7>" ![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png){ class="animation-figure" } -

Figure 11-4   Simulating bubble using element swap operation

+

Figure 11-4   Simulating bubble sort using element swaps

## 11.3.1   Algorithm Flow @@ -292,9 +292,9 @@ Example code is as follows: ## 11.3.2   Efficiency Optimization -We notice that if no swap operations are performed during a certain round of "bubbling", it means the array has already completed sorting and can directly return the result. Therefore, we can add a flag `flag` to monitor this situation and return immediately once it occurs. +We can observe that if no swaps occur during a round of "bubbling", the array is already sorted and the algorithm can return immediately. Therefore, we can add a flag `flag` to detect this situation and terminate as soon as it occurs. -After optimization, the worst-case time complexity and average time complexity of bubble sort remain $O(n^2)$; but when the input array is completely ordered, the best-case time complexity can reach $O(n)$. +After this optimization, the worst-case and average-case time complexities of bubble sort remain $O(n^2)$; however, when the input array is already sorted, the best-case time complexity becomes $O(n)$. === "Python" @@ -592,6 +592,6 @@ After optimization, the worst-case time complexity and average time complexity o ## 11.3.3   Algorithm Characteristics -- **Time complexity of $O(n^2)$, adaptive sorting**: The array lengths traversed in each round of "bubbling" are $n - 1$, $n - 2$, $\dots$, $2$, $1$, totaling $(n - 1) n / 2$. After introducing the `flag` optimization, the best-case time complexity can reach $O(n)$. +- **Time complexity is $O(n^2)$; adaptive**: In successive rounds of "bubbling", the traversed portion of the array has lengths $n - 1$, $n - 2$, $\dots$, $2$, $1$, for a total of $(n - 1) n / 2$. After introducing the `flag` optimization, the best-case time complexity can reach $O(n)$. - **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. -- **Stable sorting**: Since equal elements are not swapped during "bubbling". +- **Stable sorting**: Equal elements are not swapped during "bubbling". diff --git a/en/docs/chapter_sorting/bucket_sort.md b/en/docs/chapter_sorting/bucket_sort.md index fc287bc34..45020f9f8 100644 --- a/en/docs/chapter_sorting/bucket_sort.md +++ b/en/docs/chapter_sorting/bucket_sort.md @@ -4,9 +4,9 @@ comments: true # 11.8   Bucket Sort -The several sorting algorithms mentioned earlier all belong to "comparison-based sorting algorithms", which achieve sorting by comparing the size of elements. The time complexity of such sorting algorithms cannot exceed $O(n \log n)$. Next, we will explore several "non-comparison sorting algorithms", whose time complexity can reach linear order. +The sorting algorithms discussed earlier are all comparison-based sorting algorithms, which sort by comparing the relative order of elements. The time complexity of such algorithms cannot beat $O(n \log n)$. Next, we will explore several non-comparison sorting algorithms, whose time complexity can be linear. -Bucket sort (bucket sort) is a typical application of the divide-and-conquer strategy. It works by setting up buckets with size order, each bucket corresponding to a data range, evenly distributing data to each bucket; then, sorting within each bucket separately; finally, merging all data in the order of the buckets. +Bucket sort is a typical application of the divide-and-conquer strategy. It works by creating a sequence of ordered buckets, each corresponding to a data range, and distributing the data evenly among them. The elements within each bucket are then sorted separately. Finally, all buckets are merged in order. ## 11.8.1   Algorithm Flow @@ -439,25 +439,25 @@ The code is as follows: ## 11.8.2   Algorithm Characteristics -Bucket sort is suitable for processing very large data volumes. For example, if the input data contains 1 million elements and system memory cannot load all the data at once, the data can be divided into 1000 buckets, each bucket sorted separately, and then the results merged. +Bucket sort is suitable for processing very large datasets. For example, suppose the input contains 1 million elements, and limited memory prevents the system from loading all of them at once. In that case, the data can be divided into 1000 buckets, each bucket can be sorted separately, and the results can then be merged. -- **Time complexity of $O(n + k)$**: Assuming the elements are evenly distributed among the buckets, then the number of elements in each bucket is $\frac{n}{k}$. Assuming sorting a single bucket uses $O(\frac{n}{k} \log\frac{n}{k})$ time, then sorting all buckets uses $O(n \log\frac{n}{k})$ time. **When the number of buckets $k$ is relatively large, the time complexity approaches $O(n)$**. Merging results requires traversing all buckets and elements, taking $O(n + k)$ time. In the worst case, all data is distributed into one bucket, and sorting that bucket uses $O(n^2)$ time. -- **Space complexity of $O(n + k)$, non-in-place sorting**: Additional space is required for $k$ buckets and a total of $n$ elements. +- **Time complexity is $O(n + k)$**: Assuming the elements are evenly distributed across the buckets, each bucket contains $\frac{n}{k}$ elements. If sorting a single bucket takes $O(\frac{n}{k} \log\frac{n}{k})$ time, then sorting all buckets takes $O(n \log\frac{n}{k})$ time. **When the number of buckets $k$ is relatively large, the time complexity approaches $O(n)$**. Merging the results requires traversing all buckets and elements, which takes $O(n + k)$ time. In the worst case, all data is placed into a single bucket, and sorting that bucket takes $O(n^2)$ time. +- **Space complexity is $O(n + k)$, and bucket sort is not in-place**: It requires extra space for $k$ buckets and a total of $n$ elements. - Whether bucket sort is stable depends on whether the algorithm for sorting elements within buckets is stable. ## 11.8.3   How to Achieve Even Distribution -Theoretically, bucket sort can achieve $O(n)$ time complexity. **The key is to evenly distribute elements to each bucket**, because real data is often not evenly distributed. For example, if we want to evenly distribute all products on Taobao into 10 buckets by price range, there may be very many products below 100 yuan and very few above 1000 yuan. If the price intervals are evenly divided into 10, the difference in the number of products in each bucket will be very large. +In theory, bucket sort can achieve $O(n)$ time complexity. **The key is to distribute the elements evenly across the buckets**, because real-world data is often not uniformly distributed. For example, suppose we want to divide all products on Taobao evenly into 10 buckets by price range, but the price distribution is uneven: there are many products priced below 100 yuan and very few priced above 1000 yuan. If the price range is divided evenly into 10 intervals, the numbers of products in the buckets will differ greatly. -To achieve even distribution, we can first set an approximate dividing line to roughly divide the data into 3 buckets. **After distribution is complete, continue dividing buckets with more products into 3 buckets until the number of elements in all buckets is roughly equal**. +To achieve a more even distribution, we can first choose a rough boundary and partition the data into 3 buckets. **After that, buckets containing more products can be further divided into 3 buckets until the numbers of elements in all buckets are roughly equal**. -As shown in Figure 11-14, this method essentially creates a recursion tree, with the goal of making the values of leaf nodes as even as possible. Of course, it is not necessary to divide the data into 3 buckets every round; the specific division method can be flexibly chosen according to data characteristics. +As shown in Figure 11-14, this method essentially builds a recursion tree whose goal is to make the leaf nodes as balanced as possible. Of course, the data does not have to be split into 3 buckets in every round; the specific partitioning strategy can be chosen flexibly based on the characteristics of the data. ![Recursively dividing buckets](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" }

Figure 11-14   Recursively dividing buckets

-If we know the probability distribution of product prices in advance, **we can set the price dividing line for each bucket based on the data probability distribution**. It is worth noting that the data distribution does not necessarily need to be specifically calculated, but can also be approximated using a certain probability model based on data characteristics. +If we know the probability distribution of product prices in advance, **we can set the price boundaries for each bucket according to that distribution**. Notably, the data distribution does not need to be measured exactly; it can also be approximated with a probability model chosen to fit the characteristics of the data. As shown in Figure 11-15, we assume that product prices follow a normal distribution, which allows us to reasonably set price intervals to evenly distribute products to each bucket. diff --git a/en/docs/chapter_sorting/counting_sort.md b/en/docs/chapter_sorting/counting_sort.md index 8a7da9a8f..9918fb10f 100644 --- a/en/docs/chapter_sorting/counting_sort.md +++ b/en/docs/chapter_sorting/counting_sort.md @@ -4,15 +4,15 @@ comments: true # 11.9   Counting Sort -Counting sort (counting sort) achieves sorting by counting the number of elements, typically applied to integer arrays. +Counting sort sorts by counting the occurrences of elements and is typically applied to integer arrays. ## 11.9.1   Simple Implementation Let's start with a simple example. Given an array `nums` of length $n$, where the elements are all "non-negative integers", the overall flow of counting sort is shown in Figure 11-16. 1. Traverse the array to find the largest number, denoted as $m$, and then create an auxiliary array `counter` of length $m + 1$. -2. **Use `counter` to count the number of occurrences of each number in `nums`**, where `counter[num]` corresponds to the number of occurrences of the number `num`. The counting method is simple: just traverse `nums` (let the current number be `num`), and increase `counter[num]` by $1$ in each round. -3. **Since each index of `counter` is naturally ordered, this is equivalent to all numbers being sorted**. Next, we traverse `counter` and fill in `nums` in ascending order based on the number of occurrences of each number. +2. **Use `counter` to count how many times each number appears in `nums`**, where `counter[num]` stores the number of occurrences of `num`. This is simple: traverse `nums` (denote the current number by `num`) and increment `counter[num]` by $1$ each time. +3. **Because the indices of `counter` are naturally ordered, the numbers are effectively already sorted**. Next, traverse `counter` and write the numbers back into `nums` in ascending order according to their occurrence counts. ![Counting sort flow](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" } @@ -365,21 +365,21 @@ The code is as follows: !!! note "Connection between counting sort and bucket sort" - From the perspective of bucket sort, we can regard each index of the counting array `counter` in counting sort as a bucket, and the process of counting quantities as distributing each element to the corresponding bucket. Essentially, counting sort is a special case of bucket sort for integer data. + From the perspective of bucket sort, each index of the counting array `counter` can be viewed as a bucket, and the counting process can be seen as distributing elements into their corresponding buckets. Essentially, counting sort is a special case of bucket sort for integer data. ## 11.9.2   Complete Implementation -Observant readers may have noticed that **if the input data is objects, step `3.` above becomes invalid**. Suppose the input data is product objects, and we want to sort the products by price (a member variable of the class), but the above algorithm can only give the sorting result of prices. +Observant readers may have noticed that **if the input consists of objects, step `3.` above no longer works**. Suppose the input consists of product objects and we want to sort them by price (a member variable of the class); the above algorithm can only produce the sorted order of the prices themselves. -So how can we obtain the sorting result of the original data? We first calculate the "prefix sum" of `counter`. As the name suggests, the prefix sum at index `i`, `prefix[i]`, equals the sum of the first `i` elements of the array: +So how can we obtain the sorted order of the original data? We first compute the prefix sums of `counter`. As the name suggests, the prefix sum at index `i`, `prefix[i]`, equals the sum of the elements from index `0` through `i`: $$ \text{prefix}[i] = \sum_{j=0}^i \text{counter[j]} $$ -**The prefix sum has a clear meaning: `prefix[num] - 1` represents the index of the last occurrence of element `num` in the result array `res`**. This information is very critical because it tells us where each element should appear in the result array. Next, we traverse each element `num` of the original array `nums` in reverse order, performing the following two steps in each iteration. +**The prefix sum has a clear interpretation: `prefix[num] - 1` gives the index of the last occurrence of element `num` in the result array `res`**. This information is crucial because it tells us where each element should be placed in the result array. Next, we traverse the original array `nums` in reverse, and for each element `num`, perform the following two steps. -1. Fill `num` into the array `res` at index `prefix[num] - 1`. +1. Place `num` at index `prefix[num] - 1` of the array `res`. 2. Decrease the prefix sum `prefix[num]` by $1$ to get the index for the next placement of `num`. After the traversal is complete, the array `res` contains the sorted result, and finally `res` is used to overwrite the original array `nums`. The complete counting sort flow is shown in Figure 11-17. @@ -410,7 +410,7 @@ After the traversal is complete, the array `res` contains the sorted result, and

Figure 11-17   Counting sort steps

-The implementation code of counting sort is as follows: +The counting sort implementation is shown below: === "Python" @@ -876,14 +876,14 @@ The implementation code of counting sort is as follows: ## 11.9.3   Algorithm Characteristics -- **Time complexity of $O(n + m)$, non-adaptive sorting**: Involves traversing `nums` and traversing `counter`, both using linear time. Generally, $n \gg m$, and time complexity tends toward $O(n)$. +- **Time complexity is $O(n + m)$, and counting sort is non-adaptive**: Traversing `nums` and `counter` both takes linear time. In general, when $n \gg m$, the time complexity approaches $O(n)$. - **Space complexity of $O(n + m)$, non-in-place sorting**: Uses arrays `res` and `counter` of lengths $n$ and $m$ respectively. - **Stable sorting**: Since elements are filled into `res` in a "right-to-left" order, traversing `nums` in reverse can avoid changing the relative positions of equal elements, thereby achieving stable sorting. In fact, traversing `nums` in forward order can also yield correct sorting results, but the result would be unstable. ## 11.9.4   Limitations -By this point, you might think counting sort is very clever, as it can achieve efficient sorting just by counting quantities. However, the prerequisites for using counting sort are relatively strict. +At this point, you might think counting sort is quite ingenious because it achieves efficient sorting simply by counting occurrences. However, the prerequisites for using counting sort are fairly restrictive. -**Counting sort is only suitable for non-negative integers**. If you want to apply it to other types of data, you need to ensure that the data can be converted to non-negative integers without changing the relative size relationships between elements. For example, for an integer array containing negative numbers, you can first add a constant to all numbers to convert them all to positive numbers, and then convert them back after sorting is complete. +**Counting sort is only applicable to non-negative integers**. To apply it to other types of data, you must ensure that they can be converted to non-negative integers without changing the relative ordering of the elements. For example, for an integer array containing negative numbers, you can first add a constant to every number to shift them into the non-negative range, and then shift them back after sorting. -**Counting sort is suitable for situations where the data volume is large but the data range is small**. For example, in the above example, $m$ cannot be too large, otherwise it will occupy too much space. And when $n \ll m$, counting sort uses $O(m)$ time, which may be slower than $O(n \log n)$ sorting algorithms. +**Counting sort is well suited to cases with many elements but a small value range**. For example, in the above scenario, $m$ cannot be too large; otherwise, it consumes too much space. And when $n \ll m$, counting sort takes $O(m)$ time, which may be slower than sorting algorithms with $O(n \log n)$ time complexity. diff --git a/en/docs/chapter_sorting/heap_sort.md b/en/docs/chapter_sorting/heap_sort.md index e730ef503..718650520 100644 --- a/en/docs/chapter_sorting/heap_sort.md +++ b/en/docs/chapter_sorting/heap_sort.md @@ -8,10 +8,10 @@ comments: true Before reading this section, please ensure you have completed the "Heap" chapter. -Heap sort (heap sort) is an efficient sorting algorithm based on the heap data structure. We can use the "build heap operation" and "element out-heap operation" that we have already learned to implement heap sort. +Heap sort is an efficient sorting algorithm based on the heap data structure. We can implement heap sort using the heap construction and element removal operations introduced earlier. 1. Input the array and build a min-heap, at which point the smallest element is at the heap top. -2. Continuously perform the out-heap operation, record the out-heap elements in sequence, and an ascending sorted sequence can be obtained. +2. Continuously perform element removal operations and record the removed elements in order to obtain a sequence sorted in ascending order. Although the above method is feasible, it requires an additional array to save the popped elements, which is quite wasteful of space. In practice, we usually use a more elegant implementation method. @@ -21,12 +21,12 @@ Assume the array length is $n$. The flow of heap sort is shown in Figure 11-12. 1. Input the array and build a max-heap. After completion, the largest element is at the heap top. 2. Swap the heap top element (first element) with the heap bottom element (last element). After the swap is complete, reduce the heap length by $1$ and increase the count of sorted elements by $1$. -3. Starting from the heap top element, perform top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored. -4. Loop through steps `2.` and `3.` After looping $n - 1$ rounds, the array sorting can be completed. +3. Starting from the heap top element, perform a top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored. +4. Repeat steps `2.` and `3.` After $n - 1$ rounds, the array is sorted. !!! tip - In fact, the element out-heap operation also includes steps `2.` and `3.`, with just an additional step to pop the element. + In fact, the element removal operation also includes steps `2.` and `3.`, with the additional step of removing the element. === "<1>" ![Heap sort steps](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" } @@ -66,7 +66,7 @@ Assume the array length is $n$. The flow of heap sort is shown in Figure 11-12.

Figure 11-12   Heap sort steps

-In the code implementation, we use the same top-to-bottom heapify function `sift_down()` from the "Heap" chapter. It is worth noting that since the heap length will decrease as the largest element is extracted, we need to add a length parameter $n$ to the `sift_down()` function to specify the current effective length of the heap. The code is as follows: +In the code below, we use the same `sift_down()` function for top-to-bottom heapify as in the "Heap" chapter. It is worth noting that since the heap length decreases as the largest element is extracted, we need to add a length parameter $n$ to `sift_down()` to specify the current effective length of the heap. The code is as follows: === "Python" @@ -614,6 +614,6 @@ In the code implementation, we use the same top-to-bottom heapify function `sift ## 11.7.2   Algorithm Characteristics -- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The build heap operation uses $O(n)$ time. Extracting the largest element from the heap has a time complexity of $O(\log n)$, looping a total of $n - 1$ rounds. -- **Space complexity of $O(1)$, in-place sorting**: A few pointer variables use $O(1)$ space. Element swapping and heapify operations are both performed on the original array. -- **Non-stable sorting**: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change. +- **Time complexity is $O(n \log n)$; heap sort is non-adaptive**: Heap construction takes $O(n)$ time. Extracting the largest element from the heap takes $O(\log n)$ time, and this is repeated for a total of $n - 1$ rounds. +- **Space complexity is $O(1)$; heap sort is in-place**: A few pointer variables use $O(1)$ space. Element swapping and heapify are both performed on the original array. +- **Unstable sorting**: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change. diff --git a/en/docs/chapter_sorting/index.md b/en/docs/chapter_sorting/index.md index 71a4428a4..2735dbd3c 100644 --- a/en/docs/chapter_sorting/index.md +++ b/en/docs/chapter_sorting/index.md @@ -11,11 +11,11 @@ icon: material/sort-ascending Sorting is like a magic key that transforms chaos into order, enabling us to understand and process data more efficiently. - Whether it's simple ascending order or complex categorized arrangements, sorting demonstrates the harmonious beauty of data. + From simple ascending order to more complex classification schemes, sorting reveals the harmonious beauty of data. ## Chapter contents -- [11.1   Sorting Algorithms](sorting_algorithm.md) +- [11.1   Sorting Algorithm](sorting_algorithm.md) - [11.2   Selection Sort](selection_sort.md) - [11.3   Bubble Sort](bubble_sort.md) - [11.4   Insertion Sort](insertion_sort.md) diff --git a/en/docs/chapter_sorting/insertion_sort.md b/en/docs/chapter_sorting/insertion_sort.md index a208c08e5..3ce98b216 100644 --- a/en/docs/chapter_sorting/insertion_sort.md +++ b/en/docs/chapter_sorting/insertion_sort.md @@ -4,11 +4,11 @@ comments: true # 11.4   Insertion Sort -Insertion sort (insertion sort) is a simple sorting algorithm that works very similarly to the process of manually organizing a deck of cards. +Insertion sort is a simple sorting algorithm that works very similarly to the process of manually sorting a deck of cards. -Specifically, we select a base element from the unsorted interval, compare the element with elements in the sorted interval to its left one by one, and insert the element into the correct position. +Specifically, we select a base element from the unsorted portion, compare it one by one with the elements in the sorted portion to its left, and insert it into the correct position. -Figure 11-6 shows the operation flow of inserting an element into the array. Let the base element be `base`. We need to move all elements from the target index to `base` one position to the right, and then assign `base` to the target index. +Figure 11-6 illustrates how an element is inserted into an array. Let the base element be `base`. We need to shift all elements between the target index and `base` one position to the right, and then assign `base` to the target index. ![Single insertion operation](insertion_sort.assets/insertion_operation.png){ class="animation-figure" } @@ -18,7 +18,7 @@ Figure 11-6 shows the operation flow of inserting an element into the array. Let The overall flow of insertion sort is shown in Figure 11-7. -1. Initially, the first element of the array has completed sorting. +1. Initially, the first element of the array is already sorted. 2. Select the second element of the array as `base`, and after inserting it into the correct position, **the first 2 elements of the array are sorted**. 3. Select the third element as `base`, and after inserting it into the correct position, **the first 3 elements of the array are sorted**. 4. And so on. In the last round, select the last element as `base`, and after inserting it into the correct position, **all elements are sorted**. @@ -272,20 +272,20 @@ Example code is as follows: ## 11.4.2   Algorithm Characteristics -- **Time complexity of $O(n^2)$, adaptive sorting**: In the worst case, each insertion operation requires loops of $n - 1$, $n-2$, $\dots$, $2$, $1$, summing to $(n - 1) n / 2$, so the time complexity is $O(n^2)$. When encountering ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best-case time complexity of $O(n)$. +- **Time complexity of $O(n^2)$, adaptive sorting**: In the worst case, the insertion operations require $n - 1$, $n-2$, $\dots$, $2$, and $1$ iterations, respectively, summing to $(n - 1) n / 2$, so the time complexity is $O(n^2)$. When the data is already sorted, each insertion operation terminates early. When the input array is completely sorted, insertion sort achieves its best-case time complexity of $O(n)$. - **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. -- **Stable sorting**: During the insertion operation process, we insert elements to the right of equal elements, without changing their order. +- **Stable sorting**: During insertion, we place elements to the right of equal elements, so their relative order is unchanged. ## 11.4.3   Advantages of Insertion Sort -The time complexity of insertion sort is $O(n^2)$, while the time complexity of quick sort, which we will learn about next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **insertion sort is usually faster for smaller data volumes**. +The time complexity of insertion sort is $O(n^2)$, while the time complexity of quick sort, which we will learn about next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **it is usually faster on small datasets**. -This conclusion is similar to the applicable situations of linear search and binary search. Algorithms like quick sort with $O(n \log n)$ complexity are sorting algorithms based on divide-and-conquer strategy and often contain more unit computation operations. When the data volume is small, $n^2$ and $n \log n$ are numerically close, and complexity does not dominate; the number of unit operations per round plays a decisive role. +This conclusion is similar to the one about when linear search and binary search are applicable. Algorithms such as quick sort, with $O(n \log n)$ complexity, are divide-and-conquer sorting algorithms and often involve more primitive operations. When the dataset is small, the values of $n^2$ and $n \log n$ are relatively close, so asymptotic complexity does not dominate; instead, the number of primitive operations per round becomes the deciding factor. -In fact, the built-in sorting functions in many programming languages (such as Java) adopt insertion sort. The general approach is: for long arrays, use sorting algorithms based on divide-and-conquer strategy, such as quick sort; for short arrays, directly use insertion sort. +In fact, the built-in sorting functions of many programming languages (such as Java) use insertion sort. The general idea is: for large arrays, use divide-and-conquer sorting algorithms such as quick sort; for short arrays, use insertion sort directly. Although bubble sort, selection sort, and insertion sort all have a time complexity of $O(n^2)$, in actual situations, **insertion sort is used significantly more frequently than bubble sort and selection sort**, mainly for the following reasons. -- Bubble sort is based on element swapping, requiring the use of a temporary variable, involving 3 unit operations; insertion sort is based on element assignment, requiring only 1 unit operation. Therefore, **the computational overhead of bubble sort is usually higher than that of insertion sort**. +- Bubble sort is implemented through element swaps, which require a temporary variable and involve 3 primitive operations; insertion sort is implemented through element assignment and requires only 1 primitive operation. Therefore, **bubble sort usually has higher computational overhead than insertion sort**. - Selection sort has a time complexity of $O(n^2)$ in any case. **If given a set of partially ordered data, insertion sort is usually more efficient than selection sort**. - Selection sort is unstable and cannot be applied to multi-level sorting. diff --git a/en/docs/chapter_sorting/merge_sort.md b/en/docs/chapter_sorting/merge_sort.md index 6143108d5..47cb8909a 100644 --- a/en/docs/chapter_sorting/merge_sort.md +++ b/en/docs/chapter_sorting/merge_sort.md @@ -4,10 +4,10 @@ comments: true # 11.6   Merge Sort -Merge sort (merge sort) is a sorting algorithm based on the divide-and-conquer strategy, which includes the "divide" and "merge" phases shown in Figure 11-10. +Merge sort is a sorting algorithm based on a divide-and-conquer strategy, consisting of the "divide" and "merge" phases shown in Figure 11-10. -1. **Divide phase**: Recursively split the array from the midpoint, transforming the sorting problem of a long array into the sorting problems of shorter arrays. -2. **Merge phase**: When the sub-array length is 1, terminate the division and start merging, continuously merging two shorter sorted arrays into one longer sorted array until the process is complete. +1. **Divide phase**: Recursively split the array at the midpoint, reducing the problem of sorting a long array to the problem of sorting shorter arrays. +2. **Merge phase**: When a sub-array has length 1, stop dividing and start merging, continuously combining the shorter sorted sub-arrays on the left and right into a longer sorted array until the process is complete. ![Divide and merge phases of merge sort](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" } @@ -18,9 +18,9 @@ comments: true As shown in Figure 11-11, the "divide phase" recursively splits the array from the midpoint into two sub-arrays from top to bottom. 1. Calculate the array midpoint `mid`, recursively divide the left sub-array (interval `[left, mid]`) and right sub-array (interval `[mid + 1, right]`). -2. Recursively execute step `1.` until the sub-array interval length is 1, then terminate. +2. Repeat step `1.` recursively until a sub-array has length 1. -The "merge phase" merges the left sub-array and right sub-array into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, and each sub-array in the merge phase is sorted. +The "merge phase" merges the left and right sub-arrays into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, so every sub-array involved in this phase is already sorted. === "<1>" ![Merge sort steps](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" } @@ -54,7 +54,7 @@ The "merge phase" merges the left sub-array and right sub-array into a sorted ar

Figure 11-11   Merge sort steps

-It can be observed that the recursive order of merge sort is consistent with the post-order traversal of a binary tree. +The recursive order of merge sort is consistent with the post-order traversal of a binary tree. - **Post-order traversal**: First recursively traverse the left subtree, then recursively traverse the right subtree, and finally process the root node. - **Merge sort**: First recursively process the left sub-array, then recursively process the right sub-array, and finally perform the merge. @@ -681,15 +681,15 @@ The implementation of merge sort is shown in the code below. Note that the inter ## 11.6.2   Algorithm Characteristics -- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The division produces a recursion tree of height $\log n$, and the total number of merge operations at each level is $n$, so the overall time complexity is $O(n \log n)$. -- **Space complexity of $O(n)$, non-in-place sorting**: The recursion depth is $\log n$, using $O(\log n)$ size of stack frame space. The merge operation requires the aid of an auxiliary array, using $O(n)$ size of additional space. -- **Stable sorting**: In the merge process, the order of equal elements remains unchanged. +- **Time complexity is $O(n \log n)$; merge sort is non-adaptive**: The divide phase produces a recursion tree of height $\log n$, and the total number of operations performed during merging at each level is $n$, so the overall time complexity is $O(n \log n)$. +- **Space complexity is $O(n)$; merge sort is not in-place**: The recursion depth is $\log n$, which uses $O(\log n)$ stack-frame space. The merge operation requires an auxiliary array, which uses $O(n)$ additional space. +- **Stable sort**: During merging, the relative order of equal elements remains unchanged. ## 11.6.3   Linked List Sorting -For linked lists, merge sort has significant advantages over other sorting algorithms, **and can optimize the space complexity of linked list sorting tasks to $O(1)$**. +For linked lists, merge sort has significant advantages over other sorting algorithms, **and it can reduce the space complexity of the sorting task to $O(1)$**. -- **Divide phase**: "Iteration" can be used instead of "recursion" to implement linked list division work, thus saving the stack frame space used by recursion. -- **Merge phase**: In linked lists, node insertion and deletion operations can be achieved by just changing references (pointers), so there is no need to create additional linked lists during the merge phase (merging two short ordered linked lists into one long ordered linked list). +- **Divide phase**: Iteration can be used instead of recursion to split the linked list, thereby eliminating the stack-frame space used by recursion. +- **Merge phase**: In linked lists, node insertion and deletion require only pointer updates, so the merge phase (merging two short sorted linked lists into one longer sorted linked list) does not require creating an additional linked list. The specific implementation details are quite complex, and interested readers can consult related materials for learning. diff --git a/en/docs/chapter_sorting/quick_sort.md b/en/docs/chapter_sorting/quick_sort.md index 7c78d86ac..66020fe31 100644 --- a/en/docs/chapter_sorting/quick_sort.md +++ b/en/docs/chapter_sorting/quick_sort.md @@ -4,13 +4,13 @@ comments: true # 11.5   Quick Sort -Quick sort (quick sort) is a sorting algorithm based on the divide-and-conquer strategy, which operates efficiently and is widely applied. +Quick sort is an efficient and widely used sorting algorithm based on the divide-and-conquer strategy. -The core operation of quick sort is "sentinel partitioning", which aims to: select a certain element in the array as the "pivot", move all elements smaller than the pivot to its left, and move elements larger than the pivot to its right. Specifically, the flow of sentinel partitioning is shown in Figure 11-8. +The core operation of quick sort is "sentinel partitioning", whose goal is to select an element as the "pivot", move all elements smaller than the pivot to its left, and move all elements larger than the pivot to its right. Specifically, the process is shown in Figure 11-8. -1. Select the leftmost element of the array as the pivot, and initialize two pointers `i` and `j` pointing to the two ends of the array. -2. Set up a loop in which `i` (`j`) is used in each round to find the first element larger (smaller) than the pivot, and then swap these two elements. -3. Loop through step `2.` until `i` and `j` meet, and finally swap the pivot to the boundary line of the two sub-arrays. +1. Select the leftmost element as the pivot, and initialize two pointers `i` and `j` at the two ends of the array. +2. Enter a loop. In each round, use `i` (`j`) to find the first element larger (smaller) than the pivot, and then swap the two elements. +3. Repeat step `2.` until `i` and `j` meet, then swap the pivot into the boundary position between the two sub-arrays. === "<1>" ![Sentinel partitioning steps](quick_sort.assets/pivot_division_step1.png){ class="animation-figure" } @@ -41,7 +41,7 @@ The core operation of quick sort is "sentinel partitioning", which aims to: sele

Figure 11-8   Sentinel partitioning steps

-After sentinel partitioning is complete, the original array is divided into three parts: left sub-array, pivot, right sub-array, satisfying "any element in left sub-array $\leq$ pivot $\leq$ any element in right sub-array". Therefore, we next only need to sort these two sub-arrays. +After sentinel partitioning, the original array is divided into three parts: the left sub-array, the pivot, and the right sub-array, such that "any element in the left sub-array $\leq$ the pivot $\leq$ any element in the right sub-array". Therefore, we only need to sort the two sub-arrays next. !!! note "Divide-and-conquer strategy of quick sort" @@ -590,27 +590,27 @@ The overall flow of quick sort is shown in Figure 11-9. ## 11.5.2   Algorithm Characteristics -- **Time complexity of $O(n \log n)$, non-adaptive sorting**: In the average case, the number of recursive levels of sentinel partitioning is $\log n$, and the total number of loops at each level is $n$, using $O(n \log n)$ time overall. In the worst case, each round of sentinel partitioning divides an array of length $n$ into two sub-arrays of length $0$ and $n - 1$, at which point the number of recursive levels reaches $n$, the number of loops at each level is $n$, and the total time used is $O(n^2)$. +- **Time complexity of $O(n \log n)$, non-adaptive sorting**: On average, sentinel partitioning produces $\log n$ recursive levels, and the total number of loop iterations across each level is $n$, so the overall time complexity is $O(n \log n)$. In the worst case, each round of sentinel partitioning splits an array of length $n$ into sub-arrays of lengths $0$ and $n - 1$. The recursion depth then reaches $n$, with $n$ loop iterations at each level, yielding an overall time complexity of $O(n^2)$. - **Space complexity of $O(n)$, in-place sorting**: In the case where the input array is completely reversed, the worst recursive depth reaches $n$, using $O(n)$ stack frame space. The sorting operation is performed on the original array without the aid of an additional array. -- **Non-stable sorting**: In the last step of sentinel partitioning, the pivot may be swapped to the right of equal elements. +- **Unstable sorting**: In the last step of sentinel partitioning, the pivot may be swapped to the right of an equal element. ## 11.5.3   Why Is Quick Sort Fast -From the name, we can see that quick sort should have certain advantages in terms of efficiency. Although the average time complexity of quick sort is the same as "merge sort" and "heap sort", quick sort is usually more efficient, mainly for the following reasons. +As the name suggests, quick sort has clear efficiency advantages. Although its average time complexity is the same as that of "merge sort" and "heap sort", quick sort is usually faster in practice for the following reasons. -- **The probability of the worst case occurring is very low**: Although the worst-case time complexity of quick sort is $O(n^2)$, which is not as stable as merge sort, in the vast majority of cases, quick sort can run with a time complexity of $O(n \log n)$. -- **High cache utilization**: When performing sentinel partitioning operations, the system can load the entire sub-array into the cache, so element access efficiency is relatively high. Algorithms like "heap sort" require jump-style access to elements, thus lacking this characteristic. -- **Small constant coefficient of complexity**: Among the three algorithms mentioned above, quick sort has the smallest total number of operations such as comparisons, assignments, and swaps. This is similar to the reason why "insertion sort" is faster than "bubble sort". +- **The worst case is unlikely to occur**: Although the worst-case time complexity of quick sort is $O(n^2)$ and its performance is less predictable than that of merge sort, quick sort runs in $O(n \log n)$ time in the vast majority of cases. +- **High cache efficiency**: During sentinel partitioning, the system can load the entire sub-array into cache, so accessing elements is relatively efficient. By contrast, algorithms such as "heap sort" require non-contiguous access to elements and therefore do not enjoy this advantage. +- **Small constant factors**: Among the three algorithms above, quick sort performs the fewest comparisons, assignments, and swaps in total. This is similar to why "insertion sort" is faster than "bubble sort". ## 11.5.4   Pivot Optimization -**Quick sort may have reduced time efficiency for certain inputs**. Take an extreme example: suppose the input array is completely reversed. Since we select the leftmost element as the pivot, after sentinel partitioning is complete, the pivot is swapped to the rightmost end of the array, causing the left sub-array length to be $n - 1$ and the right sub-array length to be $0$. If we recurse down like this, each round of sentinel partitioning will have a sub-array length of $0$, the divide-and-conquer strategy fails, and quick sort degrades to a form approximate to "bubble sort". +**Quick sort can become less time-efficient for certain inputs**. Consider an extreme example in which the input array is in completely descending order. Because we choose the leftmost element as the pivot, once sentinel partitioning is complete, the pivot is swapped to the far right of the array, leaving a left sub-array of length $n - 1$ and a right sub-array of length $0$. If this continues recursively, each round of sentinel partitioning produces one sub-array of length $0$, the divide-and-conquer strategy breaks down, and quick sort degenerates into an approximation of "bubble sort". -To avoid this situation as much as possible, **we can optimize the pivot selection strategy in sentinel partitioning**. For example, we can randomly select an element as the pivot. However, if luck is not good and we select a non-ideal pivot every time, efficiency is still not satisfactory. +To reduce the chance of this happening, **we can optimize the pivot selection strategy used in sentinel partitioning**. For example, we can choose a pivot at random. However, if we are unlucky and repeatedly pick poor pivots, performance can still be unsatisfactory. -It should be noted that programming languages usually generate "pseudo-random numbers". If we construct a specific test case for a pseudo-random number sequence, the efficiency of quick sort may still degrade. +It should be noted that programming languages usually generate "pseudo-random numbers". If we construct a specific test case against a pseudo-random sequence, quick sort can still suffer degraded performance. -For further improvement, we can select three candidate elements in the array (usually the first, last, and middle elements of the array), **and use the median of these three candidate elements as the pivot**. In this way, the probability that the pivot is "neither too small nor too large" will be greatly increased. Of course, we can also select more candidate elements to further improve the robustness of the algorithm. After adopting this method, the probability of time complexity degrading to $O(n^2)$ is greatly reduced. +To improve further, we can choose three candidate elements from the array, usually the first, last, and middle elements, **and use the median of the three as the pivot**. This greatly increases the chance that the pivot is "neither too small nor too large". We can also choose more candidate elements to further improve the robustness of the algorithm. With this method, the probability that the time complexity degrades to $O(n^2)$ is significantly reduced. Example code is as follows: @@ -1074,9 +1074,9 @@ Example code is as follows: ## 11.5.5   Recursive Depth Optimization -**For certain inputs, quick sort may occupy more space**. Taking a completely ordered input array as an example, let the length of the sub-array in recursion be $m$. Each round of sentinel partitioning will produce a left sub-array of length $0$ and a right sub-array of length $m - 1$, which means that the problem scale reduced per recursive call is very small (only one element is reduced), and the height of the recursion tree will reach $n - 1$, at which point $O(n)$ size of stack frame space is required. +**Quick sort may also use more space for certain inputs**. Consider a fully sorted input array. Let the length of the current sub-array in the recursion be $m$. Each round of sentinel partitioning produces a left sub-array of length $0$ and a right sub-array of length $m - 1$, which means each recursive call reduces the problem size by only one element. The recursion tree can therefore reach a height of $n - 1$, requiring $O(n)$ stack-frame space. -To prevent the accumulation of stack frame space, we can compare the lengths of the two sub-arrays after each round of sentinel sorting is complete, **and only recurse on the shorter sub-array**. Since the length of the shorter sub-array will not exceed $n / 2$, this method can ensure that the recursion depth does not exceed $\log n$, thus optimizing the worst-case space complexity to $O(\log n)$. The code is as follows: +To prevent stack frames from accumulating, we can compare the lengths of the two sub-arrays after each round of sentinel partitioning, **and recurse only on the shorter one**. Because the shorter sub-array has length at most $n / 2$, this method ensures that the recursion depth does not exceed $\log n$, reducing the worst-case space complexity to $O(\log n)$. The code is shown below: === "Python" diff --git a/en/docs/chapter_sorting/radix_sort.md b/en/docs/chapter_sorting/radix_sort.md index e24c04bc4..ced50b98e 100644 --- a/en/docs/chapter_sorting/radix_sort.md +++ b/en/docs/chapter_sorting/radix_sort.md @@ -4,9 +4,9 @@ comments: true # 11.10   Radix Sort -The previous section introduced counting sort, which is suitable for situations where the data volume $n$ is large but the data range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, and the student ID is an 8-digit number, which means the data range $m = 10^8$ is very large. Using counting sort would require allocating a large amount of memory space, whereas radix sort can avoid this situation. +The previous section introduced counting sort, which is suitable when the number of items $n$ is large but the value range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, each of which is an 8-digit number. Then the value range $m = 10^8$ is very large. Using counting sort would require a large amount of memory, whereas radix sort avoids this problem. -Radix sort (radix sort) has a core idea consistent with counting sort, which also achieves sorting by counting quantities. Building on this, radix sort utilizes the progressive relationship between the digits of numbers, sorting each digit in turn to obtain the final sorting result. +Radix sort is based on the same core idea as counting sort: it also sorts by counting occurrences. Building on this, radix sort exploits the positional relationship among digits and sorts them one digit at a time to obtain the final result. ## 11.10.1   Algorithm Flow @@ -20,13 +20,13 @@ Taking student ID data as an example, assume the lowest digit is the $1$st digit

Figure 11-18   Radix sort algorithm flow

-Below we analyze the code implementation. For a $d$-base number $x$, to get its $k$th digit $x_k$, the following calculation formula can be used: +Next, let us look at the code. For a number $x$ in base $d$, its $k$th digit $x_k$ can be obtained with the following formula: $$ x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d $$ -Where $\lfloor a \rfloor$ denotes rounding down the floating-point number $a$, and $\bmod \: d$ denotes taking the modulo (remainder) with respect to $d$. For student ID data, $d = 10$ and $k \in [1, 8]$. +Here, $\lfloor a \rfloor$ denotes rounding the floating-point number $a$ down, and $\bmod \: d$ denotes taking the remainder modulo $d$. For student ID data, $d = 10$ and $k \in [1, 8]$. Additionally, we need to slightly modify the counting sort code to make it sort based on the $k$th digit of the number: @@ -718,12 +718,12 @@ Additionally, we need to slightly modify the counting sort code to make it sort !!! question "Why start sorting from the lowest digit?" - In successive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the first round result is $a < b$, while the second round result is $a > b$, then the second round's result will replace the first round's result. Since higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then sort the higher digits. + In successive sorting passes, a later pass overrides the result of an earlier one. For example, if the first pass yields $a < b$ but the second yields $a > b$, then the result of the second pass prevails. Because higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then the higher digits. ## 11.10.2   Algorithm Characteristics -Compared to counting sort, radix sort is suitable for larger numerical ranges, **but the prerequisite is that the data must be representable in a fixed number of digits, and the number of digits should not be too large**. For example, floating-point numbers are not suitable for radix sort because their number of digits $k$ may be too large, potentially leading to time complexity $O(nk) \gg O(n^2)$. +Compared with counting sort, radix sort is suitable for larger value ranges, **but only when the data can be represented with a fixed number of digits and that digit count is not too large**. For example, floating-point numbers are not well suited to radix sort because the digit count $k$ can be too large, potentially leading to time complexity $O(nk) \gg O(n^2)$. -- **Time complexity of $O(nk)$, non-adaptive sorting**: Let the data volume be $n$, the data be in base $d$, and the maximum number of digits be $k$. Then performing counting sort on a certain digit uses $O(n + d)$ time, and sorting all $k$ digits uses $O((n + d)k)$ time. Typically, both $d$ and $k$ are relatively small, and the time complexity approaches $O(n)$. +- **Time complexity of $O(nk)$, non-adaptive sorting**: Let the number of items be $n$, let the values be represented in base $d$, and let the maximum number of digits be $k$. Counting sort on one digit takes $O(n + d)$ time, so sorting all $k$ digits takes $O((n + d)k)$ time. In practice, $d$ and $k$ are usually relatively small, so the overall time complexity approaches $O(n)$. - **Space complexity of $O(n + d)$, non-in-place sorting**: Same as counting sort, radix sort requires auxiliary arrays `res` and `counter` of lengths $n$ and $d$. -- **Stable sorting**: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee obtaining correct sorting results. +- **Stable sort**: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee correct sorting results. diff --git a/en/docs/chapter_sorting/selection_sort.md b/en/docs/chapter_sorting/selection_sort.md index c62a1654e..dd13ac70f 100644 --- a/en/docs/chapter_sorting/selection_sort.md +++ b/en/docs/chapter_sorting/selection_sort.md @@ -4,15 +4,15 @@ comments: true # 11.2   Selection Sort -Selection sort (selection sort) works very simply: it opens a loop, and in each round, selects the smallest element from the unsorted interval and places it at the end of the sorted interval. +Selection sort works very simply: in each round, it selects the smallest element from the unsorted interval and places it at the end of the sorted interval. -Assume the array has length $n$. The algorithm flow of selection sort is shown in Figure 11-2. +Assume the array has length $n$. The procedure of selection sort is shown in Figure 11-2. 1. Initially, all elements are unsorted, i.e., the unsorted (index) interval is $[0, n-1]$. 2. Select the smallest element in the interval $[0, n-1]$ and swap it with the element at index $0$. After completion, the first element of the array is sorted. 3. Select the smallest element in the interval $[1, n-1]$ and swap it with the element at index $1$. After completion, the first 2 elements of the array are sorted. 4. And so on. After $n - 1$ rounds of selection and swapping, the first $n - 1$ elements of the array are sorted. -5. The only remaining element must be the largest element, requiring no sorting, so the array sorting is complete. +5. The only remaining element must be the largest, so no further sorting is needed and the array is sorted. === "<1>" ![Selection sort steps](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" } @@ -49,7 +49,7 @@ Assume the array has length $n$. The algorithm flow of selection sort is shown i

Figure 11-2   Selection sort steps

-In the code, we use $k$ to record the smallest element within the unsorted interval: +In the code, we use $k$ to track the smallest element within the unsorted interval: === "Python" @@ -326,9 +326,9 @@ In the code, we use $k$ to record the smallest element within the unsorted inter ## 11.2.1   Algorithm Characteristics -- **Time complexity of $O(n^2)$, non-adaptive sorting**: The outer loop has $n - 1$ rounds in total. The length of the unsorted interval in the first round is $n$, and the length of the unsorted interval in the last round is $2$. That is, each round of the outer loop contains $n$, $n - 1$, $\dots$, $3$, $2$ inner loop iterations, summing to $\frac{(n - 1)(n + 2)}{2}$. -- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. -- **Non-stable sorting**: As shown in Figure 11-3, element `nums[i]` may be swapped to the right of an element equal to it, causing a change in their relative order. +- **Time complexity $O(n^2)$, non-adaptive sorting**: The outer loop has $n - 1$ rounds in total. The length of the unsorted interval in the first round is $n$, and the length of the unsorted interval in the last round is $2$. That is, the rounds of the outer loop contain inner loops with $n$, $n - 1$, $\dots$, $3$, and $2$ iterations, summing to $\frac{(n - 1)(n + 2)}{2}$. +- **Space complexity $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. +- **Unstable sorting**: As shown in Figure 11-3, element `nums[i]` may be swapped to the right of an element equal to it, causing a change in their relative order. ![Selection sort non-stability example](selection_sort.assets/selection_sort_instability.png){ class="animation-figure" } diff --git a/en/docs/chapter_sorting/sorting_algorithm.md b/en/docs/chapter_sorting/sorting_algorithm.md index eabba69bf..604a894d5 100644 --- a/en/docs/chapter_sorting/sorting_algorithm.md +++ b/en/docs/chapter_sorting/sorting_algorithm.md @@ -4,9 +4,9 @@ comments: true # 11.1   Sorting Algorithm -Sorting algorithm (sorting algorithm) is used to arrange a group of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently. +A sorting algorithm arranges a set of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently. -As shown in Figure 11-1, data types in sorting algorithms can be integers, floating-point numbers, characters, or strings, etc. The sorting criterion can be set according to requirements, such as numerical size, character ASCII code order, or custom rules. +As shown in Figure 11-1, the data being sorted can be integers, floating-point numbers, characters, strings, and so on. The sorting rule can be defined as needed, such as numerical order, ASCII order, or a custom rule. ![Data type and criterion examples](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" } @@ -23,7 +23,7 @@ As shown in Figure 11-1, data types in sorting algorithms can be integers, float Stable sorting is a necessary condition for multi-level sorting scenarios. Suppose we have a table storing student information, where column 1 and column 2 are name and age, respectively. In this case, unstable sorting may cause the ordered nature of the input data to be lost: ```shell -# Input Data Is Sorted by Name +# The input data is sorted by name # (name, age) ('A', 19) ('B', 18) @@ -31,9 +31,9 @@ Stable sorting is a necessary condition for multi-level sorting scenarios. Suppo ('D', 19) ('E', 23) -# Assuming We Use an Unstable Sorting Algorithm to Sort the List by Age, -# In the Result, the Relative Positions of ('D', 19) and ('A', 19) Are Changed, -# And the Property That the Input Data Is Sorted by Name Is Lost +# Suppose we use an unstable sorting algorithm to sort the list by age. +# In the result, the relative positions of ('D', 19) and ('A', 19) change, +# so the property that the input data is sorted by name is lost. ('B', 18) ('D', 19) ('A', 19) @@ -43,10 +43,10 @@ Stable sorting is a necessary condition for multi-level sorting scenarios. Suppo **Adaptability**: Adaptive sorting can utilize the existing order information in the input data to reduce the amount of computation, achieving better time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than the average time complexity. -**Comparison-based or not**: Comparison-based sorting relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of $O(n \log n)$. Non-comparison sorting does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively limited. +**Comparison-based or non-comparison**: Comparison-based sorting relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of $O(n \log n)$. Non-comparison sorting does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively limited. ## 11.1.2   Ideal Sorting Algorithm -**Fast execution, in-place, stable, adaptive, good versatility**. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem. +**Fast, in-place, stable, adaptive, and broadly applicable**. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem. -Next, we will learn about various sorting algorithms together and analyze the advantages and disadvantages of each sorting algorithm based on the above evaluation dimensions. +Next, we will examine various sorting algorithms and analyze their advantages and disadvantages based on the evaluation dimensions above. diff --git a/en/docs/chapter_sorting/summary.md b/en/docs/chapter_sorting/summary.md index 2b531fa8b..bda0a5590 100644 --- a/en/docs/chapter_sorting/summary.md +++ b/en/docs/chapter_sorting/summary.md @@ -7,13 +7,13 @@ comments: true ### 1.   Key Review - Bubble sort achieves sorting by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to $O(n)$. -- Insertion sort completes sorting by inserting elements from the unsorted interval into the correct position in the sorted interval each round. Although the time complexity of insertion sort is $O(n^2)$, it is very popular in small data volume sorting tasks because it involves relatively few unit operations. -- Quick sort is implemented based on sentinel partitioning operations. In sentinel partitioning, it is possible to select the worst pivot every time, causing the time complexity to degrade to $O(n^2)$. Introducing median pivot or random pivot can reduce the probability of such degradation. By preferentially recursing on the shorter sub-interval, the recursion depth can be effectively reduced, optimizing the space complexity to $O(\log n)$. +- In each round, insertion sort inserts an element from the unsorted portion into its correct position in the sorted portion. Although insertion sort has a time complexity of $O(n^2)$, it remains very popular for small sorting tasks because each operation is relatively lightweight. +- Quick sort relies on sentinel partitioning. In sentinel partitioning, repeatedly choosing the worst possible pivot can degrade the time complexity to $O(n^2)$. Choosing a median-based pivot or a random pivot can reduce the probability of this degradation. By recursing on the shorter subarray first, we can effectively reduce the recursion depth and optimize the space complexity to $O(\log n)$. - Merge sort includes two phases: divide and merge, which typically embody the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, with a space complexity of $O(n)$; however, the space complexity of sorting a linked list can be optimized to $O(1)$. - Bucket sort consists of three steps: distributing data into buckets, sorting within buckets, and merging results. It also embodies the divide-and-conquer strategy and is suitable for very large data volumes. The key to bucket sort is distributing data evenly. - Counting sort is a special case of bucket sort, which achieves sorting by counting the number of occurrences of data. Counting sort is suitable for situations where the data volume is large but the data range is limited, and requires that data can be converted to positive integers. - Radix sort achieves data sorting by sorting digit by digit, requiring that data can be represented as fixed-digit numbers. -- Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive, with good versatility. However, just like other data structures and algorithms, no sorting algorithm has been found so far that simultaneously possesses all these characteristics. In practical applications, we need to select the appropriate sorting algorithm based on the specific characteristics of the data. +- Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive. However, as with other data structures and algorithms, no sorting algorithm can satisfy all of these criteria at the same time. In practice, we need to choose the appropriate sorting algorithm based on the characteristics of the data. - Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place property, and adaptability. ![Sorting algorithm comparison](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" } @@ -26,7 +26,7 @@ comments: true In reality, we may sort based on a certain attribute of objects. For example, students have two attributes: name and height. We want to implement multi-level sorting: first sort by name to get `(A, 180) (B, 185) (C, 170) (D, 170)`; then sort by height. Because the sorting algorithm is unstable, we may get `(D, 170) (C, 170) (A, 180) (B, 185)`. -It can be seen that the positions of students D and C have been swapped, and the orderliness of names has been disrupted, which is something we don't want to see. +We can see that students D and C have swapped positions, destroying the ordering by name, which is not what we want. **Q**: Can the order of "searching from right to left" and "searching from left to right" in sentinel partitioning be swapped? @@ -36,17 +36,17 @@ The last step of sentinel partitioning `partition()` is to swap `nums[left]` and For example, given the array `[0, 0, 0, 0, 1]`, if we first "search from left to right", the array after sentinel partitioning is `[1, 0, 0, 0, 0]`, which is incorrect. -Thinking deeper, if we select `nums[right]` as the pivot, then it's exactly the opposite - we must first "search from left to right". +By the same reasoning, if we select `nums[right]` as the pivot, the order is reversed: we must first "search from left to right". **Q**: Regarding the optimization of recursion depth in quick sort, why can selecting the shorter array ensure that the recursion depth does not exceed $\log n$? -The recursion depth is the number of currently unreturned recursive methods. Each round of sentinel partitioning divides the original array into two sub-arrays. After recursion depth optimization, the length of the sub-array to be recursively processed is at most half of the original array length. Assuming the worst case is always half the length, the final recursion depth will be $\log n$. +Recursion depth is the number of recursive calls that have not yet returned. Each round of sentinel partitioning divides the original array into two sub-arrays. After this optimization, the sub-array selected for further recursion is at most half the length of the original array. In the worst case, if it is always half as long, the final recursion depth is $\log n$. Reviewing the original quick sort, we may continuously recurse on the longer array. In the worst case, it would be $n$, $n - 1$, $\dots$, $2$, $1$, with a recursion depth of $n$. Recursion depth optimization can avoid this situation. **Q**: When all elements in the array are equal, is the time complexity of quick sort $O(n^2)$? How should this degenerate case be handled? -Yes. For this situation, consider partitioning the array into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. Only recursively process the less than and greater than parts. Under this method, an array where all input elements are equal can complete sorting in just one round of sentinel partitioning. +Yes. In this case, the array can be partitioned into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. We then recurse only on the less-than and greater-than parts. With this approach, an array whose elements are all equal can be sorted in just one round of sentinel partitioning. **Q**: Why is the worst-case time complexity of bucket sort $O(n^2)$? diff --git a/en/docs/chapter_stack_and_queue/deque.md b/en/docs/chapter_stack_and_queue/deque.md index 60fe34257..35299bb97 100644 --- a/en/docs/chapter_stack_and_queue/deque.md +++ b/en/docs/chapter_stack_and_queue/deque.md @@ -4,7 +4,7 @@ comments: true # 5.3   Deque -In a queue, we can only remove elements from the front or add elements at the rear. As shown in Figure 5-7, a double-ended queue (deque) provides greater flexibility, allowing the addition or removal of elements at both the front and rear. +In a queue, we can only remove elements from the front or add elements at the rear. As shown in Figure 5-7, a double-ended queue (deque) provides greater flexibility, allowing elements to be added or removed at both the front and the rear. ![Operations of deque](deque.assets/deque_operations.png){ class="animation-figure" } @@ -29,7 +29,7 @@ The common operations on a deque are shown in Table 5-3. The specific method nam -Similarly, we can directly use the deque classes already implemented in programming languages: +Similarly, we can directly use the deque classes provided by the programming language: === "Python" @@ -410,19 +410,19 @@ For a deque, both the front and rear can perform enqueue and dequeue operations. As shown in Figure 5-8, we treat the head and tail nodes of the doubly linked list as the front and rear of the deque, implementing functionality to add and remove nodes at both ends. -=== "LinkedListDeque" +=== "<1>" ![Enqueue and dequeue operations in linked list implementation of deque](deque.assets/linkedlist_deque_step1.png){ class="animation-figure" } -=== "push_last()" +=== "<2>" ![linkedlist_deque_push_last](deque.assets/linkedlist_deque_step2_push_last.png){ class="animation-figure" } -=== "push_first()" +=== "<3>" ![linkedlist_deque_push_first](deque.assets/linkedlist_deque_step3_push_first.png){ class="animation-figure" } -=== "pop_last()" +=== "<4>" ![linkedlist_deque_pop_last](deque.assets/linkedlist_deque_step4_pop_last.png){ class="animation-figure" } -=== "pop_first()" +=== "<5>" ![linkedlist_deque_pop_first](deque.assets/linkedlist_deque_step5_pop_first.png){ class="animation-figure" }

Figure 5-8   Enqueue and dequeue operations in linked list implementation of deque

@@ -2158,19 +2158,19 @@ The implementation code is shown below: As shown in Figure 5-9, similar to implementing a queue based on an array, we can also use a circular array to implement a deque. -=== "ArrayDeque" +=== "<1>" ![Enqueue and dequeue operations in array implementation of deque](deque.assets/array_deque_step1.png){ class="animation-figure" } -=== "push_last()" +=== "<2>" ![array_deque_push_last](deque.assets/array_deque_step2_push_last.png){ class="animation-figure" } -=== "push_first()" +=== "<3>" ![array_deque_push_first](deque.assets/array_deque_step3_push_first.png){ class="animation-figure" } -=== "pop_last()" +=== "<4>" ![array_deque_pop_last](deque.assets/array_deque_step4_pop_last.png){ class="animation-figure" } -=== "pop_first()" +=== "<5>" ![array_deque_pop_first](deque.assets/array_deque_step5_pop_first.png){ class="animation-figure" }

Figure 5-9   Enqueue and dequeue operations in array implementation of deque

diff --git a/en/docs/chapter_stack_and_queue/index.md b/en/docs/chapter_stack_and_queue/index.md index 23a17ab83..0e77245c4 100644 --- a/en/docs/chapter_stack_and_queue/index.md +++ b/en/docs/chapter_stack_and_queue/index.md @@ -3,19 +3,19 @@ comments: true icon: material/stack-overflow --- -# Chapter 5.   Stack and Queue +# Chapter 5.   Stacks and Queues -![Stack and Queue](../assets/covers/chapter_stack_and_queue.jpg){ class="cover-image" } +![Stacks and Queues](../assets/covers/chapter_stack_and_queue.jpg){ class="cover-image" } !!! abstract - Stacks are like stacking cats, while queues are like cats lining up. + A stack is like cats piled on top of one another, while a queue is like cats lining up. - They represent LIFO (Last In First Out) and FIFO (First In First Out) logic, respectively. + They represent the logical relationships of LIFO (Last In, First Out) and FIFO (First In, First Out), respectively. ## Chapter contents - [5.1   Stack](stack.md) - [5.2   Queue](queue.md) -- [5.3   Double-Ended Queue](deque.md) +- [5.3   Deque](deque.md) - [5.4   Summary](summary.md) diff --git a/en/docs/chapter_stack_and_queue/queue.md b/en/docs/chapter_stack_and_queue/queue.md index 44428bf59..0edc8d242 100755 --- a/en/docs/chapter_stack_and_queue/queue.md +++ b/en/docs/chapter_stack_and_queue/queue.md @@ -4,7 +4,7 @@ comments: true # 5.2   Queue -A queue is a linear data structure that follows the First In First Out (FIFO) rule. As the name suggests, a queue simulates the phenomenon of lining up, where newcomers continuously join the end of the queue, while people at the front of the queue leave one by one. +A queue is a linear data structure that follows the First In, First Out (FIFO) rule. As the name suggests, it models people lining up: newcomers continuously join the rear of the queue, while the people at the front leave one by one. As shown in Figure 5-4, we call the front of the queue the "front" and the end the "rear." The operation of adding an element to the rear is called "enqueue," and the operation of removing the front element is called "dequeue." @@ -14,7 +14,7 @@ As shown in Figure 5-4, we call the front of the queue the "front" and the end t ## 5.2.1   Common Queue Operations -The common operations on a queue are shown in Table 5-2. Note that method names may vary across different programming languages. We adopt the same naming convention as for stacks here. +The common operations on a queue are shown in Table 5-2. Note that method names may vary across programming languages. Here, we use the same naming convention as for stacks.

Table 5-2   Efficiency of Queue Operations

@@ -28,7 +28,7 @@ The common operations on a queue are shown in Table 5-2. Note that method names -We can directly use the ready-made queue classes in programming languages: +We can directly use the queue classes provided by the programming language: === "Python" @@ -379,13 +379,13 @@ To implement a queue, we need a data structure that allows adding elements at on As shown in Figure 5-5, we can treat the "head node" and "tail node" of a linked list as the "front" and "rear" of the queue, respectively, with the rule that nodes can only be added at the rear and removed from the front. -=== "LinkedListQueue" +=== "<1>" ![Enqueue and dequeue operations in linked list implementation of queue](queue.assets/linkedlist_queue_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![linkedlist_queue_push](queue.assets/linkedlist_queue_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![linkedlist_queue_pop](queue.assets/linkedlist_queue_step3_pop.png){ class="animation-figure" }

Figure 5-5   Enqueue and dequeue operations in linked list implementation of queue

@@ -1324,13 +1324,13 @@ Based on this design, **the valid interval containing elements in the array is ` As you can see, both enqueue and dequeue operations require only one operation, with a time complexity of $O(1)$. -=== "ArrayQueue" +=== "<1>" ![Enqueue and dequeue operations in array implementation of queue](queue.assets/array_queue_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![array_queue_push](queue.assets/array_queue_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![array_queue_pop](queue.assets/array_queue_step3_pop.png){ class="animation-figure" }

Figure 5-6   Enqueue and dequeue operations in array implementation of queue

@@ -2067,7 +2067,7 @@ For a circular array, we need to let `front` or `rear` wrap around to the beginn typedef struct { int *nums; // Array for storing queue elements int front; // Front pointer, points to the front of the queue element - int queSize; // Rear pointer, points to rear + 1 + int queSize; // Current number of elements in the queue int queCapacity; // Queue capacity } ArrayQueue; diff --git a/en/docs/chapter_stack_and_queue/stack.md b/en/docs/chapter_stack_and_queue/stack.md index f8cf1756b..48f0e61bf 100755 --- a/en/docs/chapter_stack_and_queue/stack.md +++ b/en/docs/chapter_stack_and_queue/stack.md @@ -4,11 +4,11 @@ comments: true # 5.1   Stack -A stack is a linear data structure that follows the Last In First Out (LIFO) logic. +A stack is a linear data structure that follows the Last In, First Out (LIFO) principle. We can compare a stack to a pile of plates on a table. If we specify that only one plate can be moved at a time, then to get the bottom plate, we must first remove the plates above it one by one. If we replace the plates with various types of elements (such as integers, characters, objects, etc.), we get the stack data structure. -As shown in Figure 5-1, we call the top of the stacked elements the "top" and the bottom the "base." The operation of adding an element to the top is called "push," and the operation of removing the top element is called "pop." +As shown in Figure 5-1, we call the top of the stacked elements the "top" and the bottom the "bottom." The operation of adding an element to the top is called "push," and the operation of removing the top element is called "pop." ![LIFO rule of stack](stack.assets/stack_operations.png){ class="animation-figure" } @@ -30,7 +30,7 @@ The common operations on a stack are shown in Table 5-1. The specific method nam -Typically, we can directly use the built-in stack class provided by the programming language. However, some languages may not provide a dedicated stack class. In these cases, we can use the language's "array" or "linked list" as a stack and ignore operations unrelated to the stack in the program logic. +Typically, we can directly use the built-in stack class provided by the programming language. However, some languages may not provide a dedicated stack class. In such cases, we can use the language's "array" or "linked list" as a stack and simply avoid using operations unrelated to stack behavior. === "Python" @@ -376,13 +376,13 @@ When implementing a stack using a linked list, we can treat the head node of the As shown in Figure 5-2, for the push operation, we simply insert an element at the head of the linked list. This node insertion method is called the "head insertion method." For the pop operation, we just need to remove the head node from the linked list. -=== "LinkedListStack" +=== "<1>" ![Push and pop operations in linked list implementation of stack](stack.assets/linkedlist_stack_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![linkedlist_stack_push](stack.assets/linkedlist_stack_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![linkedlist_stack_pop](stack.assets/linkedlist_stack_step3_pop.png){ class="animation-figure" }

Figure 5-2   Push and pop operations in linked list implementation of stack

@@ -1164,13 +1164,13 @@ Below is sample code for implementing a stack based on a linked list: When implementing a stack using an array, we can treat the end of the array as the top of the stack. As shown in Figure 5-3, push and pop operations correspond to adding and removing elements at the end of the array, both with a time complexity of $O(1)$. -=== "ArrayStack" +=== "<1>" ![Push and pop operations in array implementation of stack](stack.assets/array_stack_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![array_stack_push](stack.assets/array_stack_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![array_stack_pop](stack.assets/array_stack_step3_pop.png){ class="animation-figure" }

Figure 5-3   Push and pop operations in array implementation of stack

diff --git a/en/docs/chapter_stack_and_queue/summary.md b/en/docs/chapter_stack_and_queue/summary.md index 71a904ef9..7365ed835 100644 --- a/en/docs/chapter_stack_and_queue/summary.md +++ b/en/docs/chapter_stack_and_queue/summary.md @@ -7,7 +7,7 @@ comments: true ### 1.   Key Review - A stack is a data structure that follows the LIFO principle and can be implemented using arrays or linked lists. -- In terms of time efficiency, the array implementation of a stack has higher average efficiency, but during expansion, the time complexity of a single push operation degrades to $O(n)$. In contrast, the linked list implementation of a stack provides more stable efficiency performance. +- In terms of time efficiency, the array implementation of a stack has higher average efficiency, but during expansion, the time complexity of a single push operation degrades to $O(n)$. In contrast, the linked-list implementation of a stack offers more stable performance. - In terms of space efficiency, the array implementation of a stack may lead to some degree of space wastage. However, it should be noted that the memory space occupied by linked list nodes is larger than that of array elements. - A queue is a data structure that follows the FIFO principle and can also be implemented using arrays or linked lists. The conclusions regarding time efficiency and space efficiency comparisons for queues are similar to those for stacks mentioned above. - A deque is a queue with greater flexibility that allows adding and removing elements at both ends. @@ -16,7 +16,7 @@ comments: true **Q**: Is the browser's forward and backward functionality implemented with a doubly linked list? -The forward and backward functionality of a browser is essentially a manifestation of a "stack." When a user visits a new page, that page is added to the top of the stack; when the user clicks the back button, that page is popped from the top of the stack. Using a deque can conveniently implement some additional operations, as mentioned in the "Deque" section. +The browser's forward and backward behavior is essentially an application of a "stack." When a user visits a new page, that page is added to the top of the stack; when the user clicks the back button, that page is popped from the top of the stack. A deque can conveniently support some additional operations, as mentioned in the "Deque" section. **Q**: After popping from the stack, do we need to free the memory of the popped node? @@ -24,7 +24,7 @@ If the popped node will still be needed later, then memory does not need to be f **Q**: A deque seems like two stacks joined together. What is its purpose? -A deque is like a combination of a stack and a queue, or two stacks joined together. It exhibits the logic of both stack and queue, so it can implement all applications of stacks and queues, and is more flexible. +A deque is like a combination of a stack and a queue, or two stacks joined together. It combines the logic of both, so it can support all applications of stacks and queues while offering greater flexibility. **Q**: How are undo and redo specifically implemented? diff --git a/en/docs/chapter_tree/array_representation_of_tree.md b/en/docs/chapter_tree/array_representation_of_tree.md index de9800185..4c8b3760b 100644 --- a/en/docs/chapter_tree/array_representation_of_tree.md +++ b/en/docs/chapter_tree/array_representation_of_tree.md @@ -4,7 +4,7 @@ comments: true # 7.3   Array Representation of Binary Trees -Under the linked list representation, the storage unit of a binary tree is a node `TreeNode`, and nodes are connected by pointers. The previous section introduced the basic operations of binary trees under the linked list representation. +In the linked-list representation, the storage unit of a binary tree is a node `TreeNode`, and nodes are connected by pointers. The previous section introduced the basic operations of binary trees in this representation. So, can we use an array to represent a binary tree? The answer is yes. @@ -30,7 +30,7 @@ As shown in Figure 7-13, given a non-perfect binary tree, the above method of ar

Figure 7-13   Level-order traversal sequence corresponds to multiple binary tree possibilities

-To solve this problem, **we can consider explicitly writing out all `None` values in the level-order traversal sequence**. As shown in Figure 7-14, after this treatment, the level-order traversal sequence can uniquely represent a binary tree. Example code is as follows: +To solve this problem, **we can explicitly write out all `None` values in the level-order traversal sequence**. As shown in Figure 7-14, once we do this, the level-order traversal sequence can uniquely represent a binary tree. Example code is as follows: === "Python" @@ -136,9 +136,9 @@ To solve this problem, **we can consider explicitly writing out all `None` value tree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] ``` -![Array representation of any type of binary tree](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" } +![Array representation of an arbitrary binary tree](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" } -

Figure 7-14   Array representation of any type of binary tree

+

Figure 7-14   Array representation of an arbitrary binary tree

It's worth noting that **complete binary trees are very well-suited for array representation**. Recalling the definition of a complete binary tree, `None` only appears at the bottom level and towards the right, **meaning all `None` values must appear at the end of the level-order traversal sequence**. @@ -148,9 +148,9 @@ This means that when using an array to represent a complete binary tree, it's po

Figure 7-15   Array representation of a complete binary tree

-The following code implements a binary tree based on array representation, including the following operations: +The following code implements a binary tree using an array representation, including the following operations: -- Given a certain node, obtain its value, left (right) child node, and parent node. +- Given a node, obtain its value, left (right) child node, and parent node. - Obtain the preorder, inorder, postorder, and level-order traversal sequences. === "Python" diff --git a/en/docs/chapter_tree/avl_tree.md b/en/docs/chapter_tree/avl_tree.md index 7c13c52fc..2e2a2d391 100644 --- a/en/docs/chapter_tree/avl_tree.md +++ b/en/docs/chapter_tree/avl_tree.md @@ -2,7 +2,7 @@ comments: true --- -# 7.5   Avl Tree * +# 7.5   AVL Tree * In the "Binary Search Tree" section, we mentioned that after multiple insertion and removal operations, a binary search tree may degenerate into a linked list. In this case, the time complexity of all operations degrades from $O(\log n)$ to $O(n)$. @@ -18,9 +18,9 @@ For example, in the perfect binary tree shown in Figure 7-25, after inserting tw

Figure 7-25   Degradation of an AVL tree after inserting nodes

-In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the AVL tree in their paper "An algorithm for the organization of information". The paper described in detail a series of operations ensuring that after continuously adding and removing nodes, the AVL tree does not degenerate, thus keeping the time complexity of various operations at the $O(\log n)$ level. In other words, in scenarios requiring frequent insertions, deletions, searches, and modifications, the AVL tree can always maintain efficient data operation performance, making it very valuable in applications. +In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the AVL tree in their paper "An algorithm for the organization of information". The paper describes a series of operations that prevent an AVL tree from degenerating as nodes are inserted and removed, thereby keeping the time complexity of various operations at $O(\log n)$. In other words, in scenarios that require frequent insertion, deletion, lookup, and update operations, AVL trees can maintain consistently efficient performance and therefore have strong practical value. -## 7.5.1   Common Terminology in Avl Trees +## 7.5.1   Common Terminology in AVL Trees An AVL tree is both a binary search tree and a balanced binary tree, simultaneously satisfying all the properties of these two types of binary trees, hence it is a balanced binary search tree. @@ -236,7 +236,7 @@ Since the operations related to AVL trees require obtaining node heights, we nee end ``` -The "node height" refers to the distance from that node to its farthest leaf node, i.e., the number of "edges" passed. It is important to note that the height of a leaf node is $0$, and the height of a null node is $-1$. We will create two utility functions for getting and updating the height of a node: +The "node height" refers to the distance from that node to its farthest leaf node, i.e., the number of edges on the path. It is important to note that the height of a leaf node is $0$, and the height of a null node is $-1$. We will create two utility functions for getting and updating the height of a node: === "Python" @@ -650,11 +650,11 @@ The balance factor of a node is defined as the height of the node's left Let the balance factor be $f$, then the balance factor of any node in an AVL tree satisfies $-1 \le f \le 1$. -## 7.5.2   Rotations in Avl Trees +## 7.5.2   Rotations in AVL Trees The characteristic of AVL trees lies in the "rotation" operation, which can restore balance to unbalanced nodes without affecting the inorder traversal sequence of the binary tree. In other words, **rotation operations can both maintain the property of a "binary search tree" and make the tree return to a "balanced binary tree"**. -We call nodes with a balance factor absolute value $> 1$ "unbalanced nodes". Depending on the imbalance situation, rotation operations are divided into four types: right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. Below we describe these rotation operations in detail. +We call nodes with a balance factor absolute value $> 1$ "unbalanced nodes". Depending on the imbalance situation, rotation operations are divided into four types: right rotation, left rotation, right rotation then left rotation, and left rotation then right rotation. Below we describe these rotation operations in detail. ### 1.   Right Rotation @@ -1659,7 +1659,7 @@ For ease of use, we encapsulate the rotation operations into a function. **With end ``` -## 7.5.3   Common Operations in Avl Trees +## 7.5.3   Common Operations in AVL Trees ### 1.   Node Insertion @@ -2642,7 +2642,7 @@ Similarly, on the basis of the binary search tree's node removal method, rotatio The node search operation in AVL trees is consistent with that in binary search trees, and will not be elaborated here. -## 7.5.4   Typical Applications of Avl Trees +## 7.5.4   Typical Applications of AVL Trees - Organizing and storing large-scale data, suitable for scenarios with high-frequency searches and low-frequency insertions and deletions. - Used to build index systems in databases. diff --git a/en/docs/chapter_tree/binary_search_tree.md b/en/docs/chapter_tree/binary_search_tree.md index 2f3384b3a..0eeac1752 100755 --- a/en/docs/chapter_tree/binary_search_tree.md +++ b/en/docs/chapter_tree/binary_search_tree.md @@ -19,7 +19,7 @@ We encapsulate the binary search tree as a class `BinarySearchTree` and declare ### 1.   Searching for a Node -Given a target node value `num`, we can search according to the properties of the binary search tree. As shown in Figure 7-17, we declare a node `cur` and start from the binary tree's root node `root`, looping to compare the node value `cur.val` with `num`. +Given a target node value `num`, we can search according to the properties of the binary search tree. As shown in Figure 7-17, we declare a node `cur` and start from the binary search tree's root node `root`, looping to compare `cur.val` with `num`. - If `cur.val < num`, it means the target node is in `cur`'s right subtree, thus execute `cur = cur.right`. - If `cur.val > num`, it means the target node is in `cur`'s left subtree, thus execute `cur = cur.left`. @@ -39,7 +39,7 @@ Given a target node value `num`, we can search according to the properties of th

Figure 7-17   Example of searching for a node in a binary search tree

-The search operation in a binary search tree works on the same principle as the binary search algorithm, both eliminating half of the cases in each round. The number of loop iterations is at most the height of the binary tree. When the binary tree is balanced, it uses $O(\log n)$ time. The example code is as follows: +The search operation in a binary search tree follows the same principle as binary search: each round rules out half of the remaining cases. The number of loop iterations is at most the height of the tree. When the tree is balanced, the search takes $O(\log n)$ time. The example code is as follows: === "Python" @@ -343,7 +343,7 @@ The search operation in a binary search tree works on the same principle as the Given an element `num` to be inserted, in order to maintain the property of the binary search tree "left subtree < root node < right subtree," the insertion process is as shown in Figure 7-18. 1. **Finding the insertion position**: Similar to the search operation, start from the root node and loop downward searching according to the size relationship between the current node value and `num`, until passing the leaf node (traversing to `None`) and then exit the loop. -2. **Insert the node at that position**: Initialize node `num` and place it at the `None` position. +2. **Insert the node at that position**: Create a node for `num` and place it at the `None` position. ![Inserting a node into a binary search tree](binary_search_tree.assets/bst_insert.png){ class="animation-figure" } @@ -351,7 +351,7 @@ Given an element `num` to be inserted, in order to maintain the property of the In the code implementation, note the following two points: -- Binary search trees do not allow duplicate nodes; otherwise, it would violate its definition. Therefore, if the node to be inserted already exists in the tree, the insertion is not performed and it returns directly. +- Binary search trees do not allow duplicate nodes; otherwise, the tree would no longer satisfy its definition. Therefore, if the node to be inserted already exists in the tree, the insertion is skipped and the function returns directly. - To implement the node insertion, we need to use node `pre` to save the node from the previous loop iteration. This way, when traversing to `None`, we can obtain its parent node, thereby completing the node insertion operation. === "Python" @@ -801,7 +801,7 @@ Similar to searching for a node, inserting a node uses $O(\log n)$ time. ### 3.   Removing a Node -First, find the target node in the binary tree, then remove it. Similar to node insertion, we need to ensure that after the removal operation is completed, the binary search tree's property of "left subtree $<$ root node $<$ right subtree" is still maintained. Therefore, depending on the number of child nodes the target node has, we divide it into 0, 1, and 2 three cases, and execute the corresponding node removal operations. +First, find the target node in the binary search tree, then remove it. Similar to node insertion, we need to ensure that after the removal operation is completed, the binary search tree's property of "left subtree $<$ root node $<$ right subtree" is still maintained. Therefore, depending on the number of child nodes the target node has, we consider three cases: degree $0$, degree $1$, and degree $2$, and perform the corresponding removal operation. As shown in Figure 7-19, when the degree of the node to be removed is $0$, it means the node is a leaf node and can be directly removed. @@ -817,7 +817,7 @@ As shown in Figure 7-20, when the degree of the node to be removed is $1$, repla When the degree of the node to be removed is $2$, we cannot directly remove it; instead, we need to use a node to replace it. To maintain the binary search tree's property of "left subtree $<$ root node $<$ right subtree," **this node can be either the smallest node in the right subtree or the largest node in the left subtree**. -Assuming we choose the smallest node in the right subtree (the next node in the inorder traversal), the removal process is as shown in Figure 7-21. +Assuming we choose the smallest node in the right subtree, that is, the inorder successor, the removal process is as shown in Figure 7-21. 1. Find the next node of the node to be removed in the "inorder traversal sequence," denoted as `tmp`. 2. Replace the value of the node to be removed with the value of `tmp`, and recursively remove node `tmp` in the tree. @@ -1606,7 +1606,7 @@ Given a set of data, we consider using an array or a binary search tree for stor -In the ideal case, a binary search tree is "balanced," such that any node can be found within $\log n$ loop iterations. +In the ideal case, a binary search tree is balanced, so any node can be found within $O(\log n)$ loop iterations. However, if we continuously insert and remove nodes in a binary search tree, it may degenerate into a linked list as shown in Figure 7-23, where the time complexity of various operations also degrades to $O(n)$. diff --git a/en/docs/chapter_tree/binary_tree.md b/en/docs/chapter_tree/binary_tree.md index 31665ab0b..6fe690b05 100644 --- a/en/docs/chapter_tree/binary_tree.md +++ b/en/docs/chapter_tree/binary_tree.md @@ -4,7 +4,7 @@ comments: true # 7.1   Binary Tree -A binary tree is a non-linear data structure that represents the derivation relationship between "ancestors" and "descendants" and embodies the divide-and-conquer logic of "one divides into two". Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node. +A binary tree is a non-linear data structure that models the hierarchical relationship between "ancestors" and "descendants" and embodies a divide-and-conquer pattern in which each split branches into two. Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node. === "Python" @@ -207,7 +207,7 @@ A binary tree is a non-linear data structure that represents the derivati Each node has two references (pointers), pointing respectively to the left-child node and right-child node. This node is called the parent node of these two child nodes. When given a node of a binary tree, we call the tree formed by this node's left child and all nodes below it the left subtree of this node. Similarly, the right subtree can be defined. -**In a binary tree, except leaf nodes, all other nodes contain child nodes and non-empty subtrees.** As shown in Figure 7-1, if "Node 2" is regarded as a parent node, its left and right child nodes are "Node 4" and "Node 5" respectively. The left subtree is formed by "Node 4" and all nodes beneath it, while the right subtree is formed by "Node 5" and all nodes beneath it. +**In a binary tree, every non-leaf node has child nodes and therefore non-empty subtrees.** As shown in Figure 7-1, if "Node 2" is regarded as a parent node, its left and right child nodes are "Node 4" and "Node 5" respectively. The left subtree is formed by "Node 4" and all nodes beneath it, while the right subtree is formed by "Node 5" and all nodes beneath it. ![Parent Node, child Node, subtree](binary_tree.assets/binary_tree_definition.png){ class="animation-figure" } @@ -232,7 +232,7 @@ The commonly used terminology of binary trees is shown in Figure 7-2. !!! tip - Please note that we usually define "height" and "depth" as "the number of edges traversed", but some questions or textbooks may define them as "the number of nodes traversed". In this case, both height and depth need to be incremented by 1. + We usually define "height" and "depth" as the number of edges traversed, but some textbooks and problem statements define them as the number of nodes on the path. In that case, both values are larger by 1. ## 7.1.2   Basic Operations of Binary Trees @@ -629,13 +629,13 @@ Similar to a linked list, inserting and removing nodes in a binary tree can be a !!! tip - It should be noted that inserting nodes may change the original logical structure of the binary tree, while removing nodes typically involves removing the node and all its subtrees. Therefore, in a binary tree, insertion and removal are usually performed through a set of operations to achieve meaningful outcomes. + Keep in mind that inserting a node can alter the original logical structure of a binary tree, while deleting a node usually entails removing that node together with its entire subtree. In practice, insertion and deletion in binary trees are therefore typically implemented as coordinated sequences of operations to achieve a meaningful result. ## 7.1.3   Common Types of Binary Trees ### 1.   Perfect Binary Tree -As shown in Figure 7-4, a perfect binary tree has all levels completely filled with nodes. In a perfect binary tree, leaf nodes have a degree of $0$, while all other nodes have a degree of $2$. If the tree height is $h$, the total number of nodes is $2^{h+1} - 1$, exhibiting a standard exponential relationship that reflects the common phenomenon of cell division in nature. +As shown in Figure 7-4, a perfect binary tree has every level completely filled. In a perfect binary tree, leaf nodes have a degree of $0$, while all other nodes have a degree of $2$. If the tree height is $h$, the total number of nodes is $2^{h+1} - 1$, following a standard exponential pattern that mirrors the common phenomenon of cell division in nature. !!! tip @@ -671,9 +671,9 @@ As shown in Figure 7-7, in a balanced binary tree, the absolute differenc ## 7.1.4   Degeneration of Binary Trees -Figure 7-8 shows the ideal and degenerate structures of binary trees. When every level of a binary tree is filled, it reaches the "perfect binary tree" state; when all nodes are biased toward one side, the binary tree degenerates into a "linked list". +Figure 7-8 contrasts the ideal and degenerate structures of binary trees. When every level is filled, the tree becomes a "perfect binary tree"; when all nodes skew to one side, the binary tree degenerates into a "linked list". -- A perfect binary tree is the ideal case, fully leveraging the "divide and conquer" advantage of binary trees. +- A perfect binary tree is the ideal case, fully leveraging the divide-and-conquer advantages of binary trees. - A linked list represents the other extreme, where all operations become linear operations with time complexity degrading to $O(n)$. ![The Best and Worst Structures of Binary Trees](binary_tree.assets/binary_tree_best_worst_cases.png){ class="animation-figure" } diff --git a/en/docs/chapter_tree/binary_tree_traversal.md b/en/docs/chapter_tree/binary_tree_traversal.md index 2b63af245..1265f018b 100755 --- a/en/docs/chapter_tree/binary_tree_traversal.md +++ b/en/docs/chapter_tree/binary_tree_traversal.md @@ -12,7 +12,7 @@ The common traversal methods for binary trees include level-order traversal, pre As shown in Figure 7-9, level-order traversal traverses the binary tree from top to bottom, layer by layer. Within each level, it visits nodes from left to right. -Level-order traversal is essentially breadth-first traversal, also known as breadth-first search (BFS), which embodies a "expanding outward circle by circle" layer-by-layer traversal method. +Level-order traversal is essentially breadth-first traversal, also known as breadth-first search (BFS), which proceeds outward level by level. ![Level-order traversal of a binary tree](binary_tree_traversal.assets/binary_tree_bfs.png){ class="animation-figure" } @@ -341,7 +341,7 @@ Breadth-first traversal is typically implemented with the help of a "queue". The ## 7.2.2   Preorder, Inorder, and Postorder Traversal -Correspondingly, preorder, inorder, and postorder traversals all belong to depth-first traversal, also known as depth-first search (DFS), which embodies a "first go to the end, then backtrack and continue" traversal method. +Correspondingly, preorder, inorder, and postorder traversals all belong to depth-first traversal, also known as depth-first search (DFS), which goes as deep as possible before backtracking. Figure 7-10 shows how depth-first traversal works on a binary tree. **Depth-first traversal is like "walking" around the perimeter of the entire binary tree**, encountering three positions at each node, corresponding to preorder, inorder, and postorder traversal. @@ -816,12 +816,12 @@ Depth-first search is usually implemented based on recursion: !!! tip - Depth-first search can also be implemented based on iteration, interested readers can study this on their own. + Depth-first search can also be implemented iteratively, and interested readers can explore this on their own. -Figure 7-11 shows the recursive process of preorder traversal of a binary tree, which can be divided into two opposite parts: "recursion" and "return". +Figure 7-11 shows the recursive process of preorder traversal of a binary tree, which can be divided into two opposite phases: "descending" and "returning". -1. "Recursion" means opening a new method, where the program accesses the next node in this process. -2. "Return" means the function returns, indicating that the current node has been fully visited. +1. "Descending" means making a new recursive call, during which the program visits the next node. +2. "Returning" means the function call returns, indicating that the current node has been fully processed. === "<1>" ![The recursive process of preorder traversal](binary_tree_traversal.assets/preorder_step1.png){ class="animation-figure" } diff --git a/en/docs/chapter_tree/index.md b/en/docs/chapter_tree/index.md index e7f6b8cda..b03e60681 100644 --- a/en/docs/chapter_tree/index.md +++ b/en/docs/chapter_tree/index.md @@ -9,14 +9,15 @@ icon: material/graph-outline !!! abstract - Towering trees are full of vitality, with deep roots and lush leaves, spreading branches and flourishing. + Towering trees are full of vitality, with deep roots, lush foliage, and sprawling branches. + + They offer a vivid illustration of divide-and-conquer in data structures. - They show us the vivid form of divide and conquer in data. ## Chapter contents - [7.1   Binary Tree](binary_tree.md) - [7.2   Binary Tree Traversal](binary_tree_traversal.md) -- [7.3   Array Representation of Tree](array_representation_of_tree.md) +- [7.3   Array Representation of Binary Trees](array_representation_of_tree.md) - [7.4   Binary Search Tree](binary_search_tree.md) - [7.5   AVL Tree *](avl_tree.md) - [7.6   Summary](summary.md) diff --git a/en/docs/chapter_tree/summary.md b/en/docs/chapter_tree/summary.md index d1e55dd8b..cdb1f8420 100644 --- a/en/docs/chapter_tree/summary.md +++ b/en/docs/chapter_tree/summary.md @@ -6,23 +6,23 @@ comments: true ### 1.   Key Review -- A binary tree is a non-linear data structure that embodies the divide-and-conquer logic of "one divides into two". Each binary tree node contains a value and two pointers, which respectively point to its left and right child nodes. +- A binary tree is a non-linear data structure that embodies the divide-and-conquer logic of splitting into two. Each binary tree node contains a value and two pointers, which point to its left and right child nodes. - For a certain node in a binary tree, the tree formed by its left (right) child node and all nodes below is called the left (right) subtree of that node. - Related terminology of binary trees includes root node, leaf node, level, degree, edge, height, and depth. - The initialization, node insertion, and node removal operations of binary trees are similar to those of linked lists. -- Common types of binary trees include perfect binary trees, complete binary trees, full binary trees, and balanced binary trees. The perfect binary tree is the ideal state, while the linked list is the worst state after degradation. +- Common types of binary trees include perfect binary trees, complete binary trees, full binary trees, and balanced binary trees. A perfect binary tree is the ideal form, while a linked list represents the worst degenerate case. - A binary tree can be represented using an array by arranging node values and empty slots in level-order traversal sequence, and implementing pointers based on the index mapping relationship between parent and child nodes. -- Level-order traversal of a binary tree is a breadth-first search method, embodying a layer-by-layer traversal approach of "expanding outward circle by circle", typically implemented using a queue. -- Preorder, inorder, and postorder traversals all belong to depth-first search, embodying a traversal approach of "first go to the end, then backtrack and continue", typically implemented using recursion. +- Level-order traversal of a binary tree is a breadth-first search method that proceeds level by level, typically implemented using a queue. +- Preorder, inorder, and postorder traversals all belong to depth-first search, which proceeds by going as deep as possible before backtracking, typically using recursion. - A binary search tree is an efficient data structure for element searching, with search, insertion, and removal operations all having time complexity of $O(\log n)$. When a binary search tree degenerates into a linked list, all time complexities degrade to $O(n)$. - An AVL tree, also known as a balanced binary search tree, ensures the tree remains balanced after continuous node insertions and removals through rotation operations. -- Rotation operations in AVL trees include right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. After inserting or removing nodes, AVL trees perform rotation operations from bottom to top to restore the tree to balance. +- Rotation operations in AVL trees include right rotation, left rotation, right rotation followed by left rotation, and left rotation followed by right rotation. After inserting or removing nodes, AVL trees perform rotations from bottom to top to restore balance. ### 2.   Q & A **Q**: For a binary tree with only one node, are both the height of the tree and the depth of the root node $0$? -Yes, because height and depth are typically defined as "the number of edges passed." +Yes, because height and depth are typically defined as the number of edges on the path. **Q**: The insertion and removal in a binary tree are generally accomplished by a set of operations. What does "a set of operations" refer to here? Does it imply releasing the resources of the child nodes? diff --git a/en/docs/index.assets/animation.mp4 b/en/docs/index.assets/animation.mp4 new file mode 100644 index 000000000..cf3838e79 Binary files /dev/null and b/en/docs/index.assets/animation.mp4 differ diff --git a/en/docs/index.assets/comment.mp4 b/en/docs/index.assets/comment.mp4 new file mode 100644 index 000000000..3f9ea340c Binary files /dev/null and b/en/docs/index.assets/comment.mp4 differ diff --git a/en/docs/index.assets/running_code.mp4 b/en/docs/index.assets/running_code.mp4 new file mode 100644 index 000000000..c946fbd68 Binary files /dev/null and b/en/docs/index.assets/running_code.mp4 differ diff --git a/en/docs/index.html b/en/docs/index.html index 0e7e348e2..d4a719168 100644 --- a/en/docs/index.html +++ b/en/docs/index.html @@ -5,7 +5,7 @@
-
- -
-
- Preview -
- - - - - - - - - - - - - - -
-

500 animated illustrations, 14 programming languages, and 3000 community Q&As to help you dive into data structures and algorithms.

-
-
- -
+
+

500 animated illustrations, 14 programming languages, and 3,000 community Q&As to help you get started with data structures and algorithms.

-
+

Endorsements

-

“An easy-to-understand book on data structures and algorithms, which guides readers to learn by minds-on and hands-on. Strongly recommended for algorithm beginners!”

-

—— Junhui Deng, Professor, Department of computer science and technology, Tsinghua University

+

“An easy-to-understand introduction to data structures and algorithms that encourages readers to engage both mind and hands. Highly recommended for beginners!”

+

—— Junhui Deng, Professor, Department of Computer Science and Technology, Tsinghua University

“If I had 'Hello Algo' when I was learning data structures and algorithms, it would have been 10 times easier!”

-

—— Mu Li, Senior principal scientist, Amazon

+

—— Mu Li, Senior Principal Scientist, Amazon

-
+
@@ -196,26 +161,26 @@ -

Animated illustrations

+

Animated Illustrations

It’s crafted for a smooth learning experience.

"A picture is worth a thousand words."

- Animation example +
- Running code example +
-

Off-the-shelf code

+

One-Click Runnable Code

-

One click to run code in multiple languages.

+

Run code in multiple languages with one click.

"Talk is cheap. Show me the code."

@@ -228,23 +193,23 @@ -

Learning together

+

Learn Together

-

Don’t hesitate to ask or share your thoughts.

+

Ask questions, discuss ideas, and learn with others.

"Learning by teaching."

- Comments example +
-
+

- Special thanks + Special Thanks

-
+
@@ -272,9 +237,78 @@
+ +
+
-

Code reviewers

+

Code Reviewers

+

The multilingual code versions of this book were made possible by the following developers. We appreciate their efforts and contributions!

- - -

Contributors

@@ -439,4 +406,4 @@
-
\ No newline at end of file +
diff --git a/en/docs/index.md b/en/docs/index.md index bcabc4880..818a368dd 100644 --- a/en/docs/index.md +++ b/en/docs/index.md @@ -1,6 +1,5 @@ --- comments: false -glightbox: false search: exclude: true hide: diff --git a/ja/docs/chapter_appendix/contribution.md b/ja/docs/chapter_appendix/contribution.md index a791a6f95..ddfb4fc10 100644 --- a/ja/docs/chapter_appendix/contribution.md +++ b/ja/docs/chapter_appendix/contribution.md @@ -2,51 +2,51 @@ comments: true --- -# 16.2   コントリビューション +# 16.2   一緒に制作に参加しましょう -著者の能力に限りがあるため、本書にはいくつかの省略や誤りが避けられません。ご理解をお願いします。誤字、リンク切れ、内容の欠落、文章の曖昧さ、説明の不明確さ、または不合理な文章構造を発見された場合は、読者により良質な学習リソースを提供するため、修正にご協力ください。 +著者の力には限りがあるため、本書にはどうしても一部の漏れや誤りが含まれる可能性があります。ご了承ください。誤字、リンク切れ、内容の欠落、表現の曖昧さ、説明の不明瞭さ、文章構成の不適切さなどの問題を見つけた場合は、ぜひ修正にご協力ください。読者により良い学習リソースを提供できます。 -すべての[コントリビューター](https://github.com/krahets/hello-algo/graphs/contributors)のGitHub IDは、本書のリポジトリ、ウェブ、PDFバージョンのホームページに表示され、オープンソースコミュニティへの無私の貢献に感謝いたします。 +すべての[寄稿者](https://github.com/krahets/hello-algo/graphs/contributors)の GitHub ID は、本書のリポジトリ、Web 版、PDF 版のホームページに掲載され、オープンソースコミュニティへの惜しみない貢献に感謝を表します。 !!! success "オープンソースの魅力" - 紙の本の2つの印刷版の間隔はしばしば長く、内容の更新が非常に不便です。 - - しかし、このオープンソースの本では、内容の更新サイクルは数日、さらには数時間に短縮されます。 + 紙の書籍では、2 回の増刷の間隔が長くなりがちで、内容更新は非常に不便です。 + + 一方、このオープンソース書籍では、内容更新のサイクルは数日、場合によっては数時間にまで短縮されています。 ### 1.   内容の微調整 -下の図に示すように、各ページの右上角に「編集アイコン」があります。以下の手順に従ってテキストやコードを修正できます。 +以下の図のように、各ページの右上には「編集アイコン」があります。次の手順で本文やコードを修正できます。 -1. 「編集アイコン」をクリックします。「このリポジトリをフォークしますか」と促された場合は、同意してください。 -2. Markdownソースファイルの内容を修正し、内容の正確性を確認し、フォーマットの一貫性を保つようにしてください。 -3. ページの下部で修正説明を記入し、「Propose file change」ボタンをクリックします。ページがリダイレクトされた後、「Create pull request」ボタンをクリックしてプルリクエストを開始します。 +1. 「編集アイコン」をクリックし、「このリポジトリを Fork する必要があります」と表示された場合は、その操作を承認してください。 +2. Markdown のソースファイルを修正し、内容が正しいことを確認したうえで、できるだけ書式の統一を保ってください。 +3. ページ下部に修正内容の説明を入力し、その後「Propose file change」ボタンをクリックします。ページ遷移後、「Create pull request」ボタンをクリックするとプルリクエストを作成できます。 ![ページ編集ボタン](contribution.assets/edit_markdown.png){ class="animation-figure" }

図 16-3   ページ編集ボタン

-図は直接修正できないため、新しい[Issue](https://github.com/krahets/hello-algo/issues)を作成するか、問題を説明するコメントが必要です。できるだけ早く図を再描画して置き換えます。 +画像は直接修正できないため、新しい [Issue](https://github.com/krahets/hello-algo/issues) を作成するかコメントで問題を説明してください。できるだけ早く描き直して差し替えます。 -### 2.   内容の作成 +### 2.   コンテンツ制作 -このオープンソースプロジェクトへの参加に興味がある場合、コードを他のプログラミング言語に翻訳したり、記事の内容を拡張したりすることを含めて、以下のプルリクエストワークフローを実装する必要があります。 +コードを他のプログラミング言語へ翻訳することや、記事内容を拡充することなど、このオープンソースプロジェクトへの参加に興味がある場合は、以下の Pull Request ワークフローに従ってください。 -1. GitHubにログインし、本書の[コードリポジトリ](https://github.com/krahets/hello-algo)を個人アカウントにフォークします。 -2. フォークしたリポジトリのウェブページに移動し、`git clone`コマンドを使用してリポジトリをローカルマシンにクローンします。 -3. ローカルで内容を作成し、完全なテストを実行してコードの正確性を検証します。 -4. ローカルで行った変更をコミットし、リモートリポジトリにプッシュします。 -5. リポジトリのウェブページを更新し、「Create pull request」ボタンをクリックしてプルリクエストを開始します。 +1. GitHub にログインし、本書の[コードリポジトリ](https://github.com/krahets/hello-algo)を個人アカウントに Fork します。 +2. Fork したリポジトリのページに入り、`git clone` コマンドを使ってリポジトリをローカルにクローンします。 +3. ローカルでコンテンツを作成し、完全なテストを行ってコードの正しさを確認します。 +4. ローカルで行った変更を Commit し、その後リモートリポジトリへ Push します。 +5. リポジトリのページを更新し、「Create pull request」ボタンをクリックするとプルリクエストを作成できます。 -### 3.   Dockerデプロイメント +### 3.   Docker デプロイ -`hello-algo`ルートディレクトリで、以下のDockerスクリプトを実行して`http://localhost:8000`でプロジェクトにアクセスします: +`hello-algo` のルートディレクトリで以下の Docker スクリプトを実行すると、`http://localhost:8000` で本プロジェクトにアクセスできます。 ```shell docker-compose up -d ``` -以下のコマンドを使用してデプロイメントを削除します: +以下のコマンドでデプロイを削除できます。 ```shell docker-compose down diff --git a/ja/docs/chapter_appendix/index.md b/ja/docs/chapter_appendix/index.md index 492484c86..a65d205c4 100644 --- a/ja/docs/chapter_appendix/index.md +++ b/ja/docs/chapter_appendix/index.md @@ -10,5 +10,5 @@ icon: material/help-circle-outline ## 章の内容 - [16.1   プログラミング環境のインストール](installation.md) -- [16.2   一緒に創作に参加する](contribution.md) +- [16.2   一緒に制作に参加しましょう](contribution.md) - [16.3   用語集](terminology.md) diff --git a/ja/docs/chapter_appendix/installation.md b/ja/docs/chapter_appendix/installation.md index f063b961b..da878d419 100644 --- a/ja/docs/chapter_appendix/installation.md +++ b/ja/docs/chapter_appendix/installation.md @@ -2,75 +2,75 @@ comments: true --- -# 16.1   インストール +# 16.1   プログラミング環境のインストール -## 16.1.1   IDEのインストール +## 16.1.1   IDE のインストール -ローカルの統合開発環境(IDE)として、オープンソースで軽量なVS Codeを使用することをお勧めします。[VS Code公式ウェブサイト](https://code.visualstudio.com/)にアクセスし、お使いのオペレーティングシステムに適したVS Codeのバージョンを選択してダウンロードし、インストールしてください。 +オープンソースで軽量な VS Code をローカルの統合開発環境(IDE)として使用することを推奨します。[VS Code 公式サイト](https://code.visualstudio.com/) にアクセスし、使用している OS に応じたバージョンの VS Code をダウンロードしてインストールしてください。 -![公式ウェブサイトからVS Codeをダウンロード](installation.assets/vscode_installation.png){ class="animation-figure" } +![公式サイトから VS Code をダウンロード](installation.assets/vscode_installation.png){ class="animation-figure" } -

図 16-1   公式ウェブサイトからVS Codeをダウンロード

+

図 16-1   公式サイトから VS Code をダウンロード

-VS Codeには強力な拡張機能エコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。例えば、「Python Extension Pack」をインストールした後、Pythonコードをデバッグできます。インストール手順を下の図に示します。 +VS Code には強力な拡張機能のエコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。Python を例にすると、「Python Extension Pack」拡張機能をインストールした後、Python コードをデバッグできるようになります。インストール手順を以下に示します。 -![VS Code拡張機能パックのインストール](installation.assets/vscode_extension_installation.png){ class="animation-figure" } +![VS Code 拡張機能のインストール](installation.assets/vscode_extension_installation.png){ class="animation-figure" } -

図 16-2   VS Code拡張機能パックのインストール

+

図 16-2   VS Code 拡張機能のインストール

## 16.1.2   言語環境のインストール -### 1.   Python環境 +### 1.   Python 環境 -1. [Miniconda3](https://docs.conda.io/en/latest/miniconda.html)をダウンロードしてインストールします。Python 3.10以降が必要です。 -2. VS Code拡張機能マーケットプレイスで`python`を検索し、Python Extension Packをインストールします。 -3. (オプション)コマンドラインで`pip install black`を入力して、コードフォーマッティングツールをインストールします。 +1. [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) をダウンロードしてインストールします。Python 3.10 以降が必要です。 +2. VS Code の拡張機能マーケットプレイスで `python` を検索し、Python Extension Pack をインストールします。 +3. (任意)コマンドラインで `pip install black` を入力し、コード整形ツールをインストールします。 -### 2.   C/C++環境 +### 2.   C/C++ 環境 -1. Windowsシステムでは[MinGW](https://sourceforge.net/projects/mingw-w64/files/)をインストールする必要があります([設定チュートリアル](https://blog.csdn.net/qq_33698226/article/details/129031241))。MacOSにはClangが付属しているため、インストールは不要です。 -2. VS Code拡張機能マーケットプレイスで`c++`を検索し、C/C++ Extension Packをインストールします。 -3. (オプション)設定ページを開き、`Clang_format_fallback Style`コードフォーマッティングオプションを検索し、`{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }`に設定します。 +1. Windows システムでは [MinGW](https://sourceforge.net/projects/mingw-w64/files/) をインストールする必要があります([設定チュートリアル](https://blog.csdn.net/qq_33698226/article/details/129031241))。MacOS には Clang が標準搭載されているため、追加インストールは不要です。 +2. VS Code の拡張機能マーケットプレイスで `c++` を検索し、C/C++ Extension Pack をインストールします。 +3. (任意)Settings ページを開き、コード整形オプション `Clang_format_fallback Style` を検索して、`{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }` に設定します。 -### 3.   Java環境 +### 3.   Java 環境 -1. [OpenJDK](https://jdk.java.net/18/)をダウンロードしてインストールします(バージョンはJDK 9より新しい必要があります)。 -2. VS Code拡張機能マーケットプレイスで`java`を検索し、Extension Pack for Javaをインストールします。 +1. [OpenJDK](https://jdk.java.net/18/) をダウンロードしてインストールします(バージョンは JDK 9 より新しい必要があります)。 +2. VS Code の拡張機能マーケットプレイスで `java` を検索し、Extension Pack for Java をインストールします。 -### 4.   C#環境 +### 4.   C# 環境 -1. [.Net 8.0](https://dotnet.microsoft.com/en-us/download)をダウンロードしてインストールします。 -2. VS Code拡張機能マーケットプレイスで`C# Dev Kit`を検索し、C# Dev Kitをインストールします([設定チュートリアル](https://code.visualstudio.com/docs/csharp/get-started))。 -3. Visual Studioを使用することもできます([インストールチュートリアル](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022))。 +1. [.Net 8.0](https://dotnet.microsoft.com/en-us/download) をダウンロードしてインストールします。 +2. VS Code の拡張機能マーケットプレイスで `C# Dev Kit` を検索し、C# Dev Kit をインストールします([設定チュートリアル](https://code.visualstudio.com/docs/csharp/get-started))。 +3. Visual Studio を使用することもできます([インストール手順](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022))。 -### 5.   Go環境 +### 5.   Go 環境 -1. [go](https://go.dev/dl/)をダウンロードしてインストールします。 -2. VS Code拡張機能マーケットプレイスで`go`を検索し、Goをインストールします。 -3. `Ctrl + Shift + P`を押してコマンドバーを呼び出し、goと入力し、`Go: Install/Update Tools`を選択し、すべてを選択してインストールします。 +1. [go](https://go.dev/dl/) をダウンロードしてインストールします。 +2. VS Code の拡張機能マーケットプレイスで `go` を検索し、Go をインストールします。 +3. ショートカットキー `Ctrl + Shift + P` を押してコマンドパレットを開き、go と入力して `Go: Install/Update Tools` を選択し、すべてにチェックを入れてインストールします。 -### 6.   Swift環境 +### 6.   Swift 環境 -1. [Swift](https://www.swift.org/download/)をダウンロードしてインストールします。 -2. VS Code拡張機能マーケットプレイスで`swift`を検索し、[Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang)をインストールします。 +1. [Swift](https://www.swift.org/download/) をダウンロードしてインストールします。 +2. VS Code の拡張機能マーケットプレイスで `swift` を検索し、[Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang) をインストールします。 -### 7.   JavaScript環境 +### 7.   JavaScript 環境 -1. [Node.js](https://nodejs.org/en/)をダウンロードしてインストールします。 -2. (オプション)VS Code拡張機能マーケットプレイスで`Prettier`を検索し、コードフォーマッティングツールをインストールします。 +1. [Node.js](https://nodejs.org/en/) をダウンロードしてインストールします。 +2. (任意)VS Code の拡張機能マーケットプレイスで `Prettier` を検索し、コード整形ツールをインストールします。 -### 8.   TypeScript環境 +### 8.   TypeScript 環境 -1. JavaScript環境と同じインストール手順に従います。 -2. [TypeScript Execute (tsx)](https://github.com/privatenumber/tsx?tab=readme-ov-file#global-installation)をインストールします。 -3. VS Code拡張機能マーケットプレイスで`typescript`を検索し、[Pretty TypeScript Errors](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors)をインストールします。 +1. JavaScript 環境と同じ手順でインストールします。 +2. [TypeScript Execute (tsx)](https://github.com/privatenumber/tsx?tab=readme-ov-file#global-installation) をインストールします。 +3. VS Code の拡張機能マーケットプレイスで `typescript` を検索し、[Pretty TypeScript Errors](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) をインストールします。 -### 9.   Dart環境 +### 9.   Dart 環境 -1. [Dart](https://dart.dev/get-dart)をダウンロードしてインストールします。 -2. VS Code拡張機能マーケットプレイスで`dart`を検索し、[Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code)をインストールします。 +1. [Dart](https://dart.dev/get-dart) をダウンロードしてインストールします。 +2. VS Code の拡張機能マーケットプレイスで `dart` を検索し、[Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code) をインストールします。 -### 10.   Rust環境 +### 10.   Rust 環境 -1. [Rust](https://www.rust-lang.org/tools/install)をダウンロードしてインストールします。 -2. VS Code拡張機能マーケットプレイスで`rust`を検索し、[rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)をインストールします。 +1. [Rust](https://www.rust-lang.org/tools/install) をダウンロードしてインストールします。 +2. VS Code の拡張機能マーケットプレイスで `rust` を検索し、[rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) をインストールします。 diff --git a/ja/docs/chapter_appendix/terminology.md b/ja/docs/chapter_appendix/terminology.md index a19a814ac..bc69c2d35 100644 --- a/ja/docs/chapter_appendix/terminology.md +++ b/ja/docs/chapter_appendix/terminology.md @@ -4,142 +4,142 @@ comments: true # 16.3   用語集 -下の表は本書に登場する重要な用語をリストアップしており、以下の点に注意する価値があります。 +以下の表は、本書に登場する重要な用語を一覧にしたものです。特に次の点に注意してください。 -- 英語文献を読みやすくするため、用語の英語名を覚えることをお勧めします。 -- 一部の用語は簡体字中国語と繁体字中国語で異なる名前を持ちます。 +- 名詞の英語表現も覚えておくと、英語文献を読む際に役立ちます。 +- 一部の名詞は、簡体字中国語と繁体字中国語で呼び方が異なります。

表 16-1   データ構造とアルゴリズムの重要用語

-| English | 日本語 | 简体中文 | 繁体中文 | -| ------------------------------ | ---------------------- | -------------- | -------------- | -| algorithm | アルゴリズム | 算法 | 演算法 | -| data structure | データ構造 | 数据结构 | 資料結構 | -| code | コード | 代码 | 程式碼 | -| file | ファイル | 文件 | 檔案 | -| function | 関数 | 函数 | 函式 | -| method | メソッド | 方法 | 方法 | -| variable | 変数 | 变量 | 變數 | -| asymptotic complexity analysis | 漸近計算量解析 | 渐近复杂度分析 | 漸近複雜度分析 | -| time complexity | 時間計算量 | 时间复杂度 | 時間複雜度 | -| space complexity | 空間計算量 | 空间复杂度 | 空間複雜度 | -| loop | ループ | 循环 | 迴圈 | -| iteration | 反復 | 迭代 | 迭代 | -| recursion | 再帰 | 递归 | 遞迴 | -| tail recursion | 末尾再帰 | 尾递归 | 尾遞迴 | -| recursion tree | 再帰木 | 递归树 | 遞迴樹 | -| big-$O$ notation | ビッグO記法 | 大 $O$ 记号 | 大 $O$ 記號 | -| asymptotic upper bound | 漸近上界 | 渐近上界 | 漸近上界 | -| sign-magnitude | 符号と絶対値 | 原码 | 原碼 | -| 1's complement | 1の補数 | 反码 | 一補數 | -| 2's complement | 2の補数 | 补码 | 二補數 | -| array | 配列 | 数组 | 陣列 | -| index | インデックス | 索引 | 索引 | -| linked list | 連結リスト | 链表 | 鏈結串列 | -| linked list node, list node | 連結リストノード | 链表节点 | 鏈結串列節點 | -| head node | 先頭ノード | 头节点 | 頭節點 | -| tail node | 末尾ノード | 尾节点 | 尾節點 | -| list | リスト | 列表 | 串列 | -| dynamic array | 動的配列 | 动态数组 | 動態陣列 | -| hard disk | ハードディスク | 硬盘 | 硬碟 | -| random-access memory (RAM) | メモリ | 内存 | 記憶體 | -| cache memory | キャッシュメモリ | 缓存 | 快取 | -| cache miss | キャッシュミス | 缓存未命中 | 快取未命中 | -| cache hit rate | キャッシュヒット率 | 缓存命中率 | 快取命中率 | -| stack | スタック | 栈 | 堆疊 | -| top of the stack | スタックトップ | 栈顶 | 堆疊頂 | -| bottom of the stack | スタックボトム | 栈底 | 堆疊底 | -| queue | キュー | 队列 | 佇列 | -| double-ended queue | 両端キュー | 双向队列 | 雙向佇列 | -| front of the queue | キューの先頭 | 队首 | 佇列首 | -| rear of the queue | キューの末尾 | 队尾 | 佇列尾 | -| hash table | ハッシュテーブル | 哈希表 | 雜湊表 | -| hash set | ハッシュセット | 哈希集合 | 雜湊集合 | -| bucket | バケット | 桶 | 桶 | -| hash function | ハッシュ関数 | 哈希函数 | 雜湊函式 | -| hash collision | ハッシュ衝突 | 哈希冲突 | 雜湊衝突 | -| load factor | 負荷率 | 负载因子 | 負載因子 | -| separate chaining | チェイン法 | 链式地址 | 鏈結位址 | -| open addressing | オープンアドレス法 | 开放寻址 | 開放定址 | -| linear probing | 線形プローブ法 | 线性探测 | 線性探查 | -| lazy deletion | 遅延削除 | 懒删除 | 懶刪除 | -| binary tree | 二分木 | 二叉树 | 二元樹 | -| tree node | 木のノード | 树节点 | 樹節點 | -| left-child node | 左の子ノード | 左子节点 | 左子節點 | -| right-child node | 右の子ノード | 右子节点 | 右子節點 | -| parent node | 親ノード | 父节点 | 父節點 | -| left subtree | 左の部分木 | 左子树 | 左子樹 | -| right subtree | 右の部分木 | 右子树 | 右子樹 | -| root node | ルートノード | 根节点 | 根節點 | -| leaf node | 葉ノード | 叶节点 | 葉節點 | -| edge | エッジ | 边 | 邊 | -| level | レベル | 层 | 層 | -| degree | 次数 | 度 | 度 | -| height | 高さ | 高度 | 高度 | -| depth | 深さ | 深度 | 深度 | -| perfect binary tree | 完全二分木 | 完美二叉树 | 完美二元樹 | -| complete binary tree | 完全二分木 | 完全二叉树 | 完全二元樹 | -| full binary tree | 満二分木 | 完满二叉树 | 完滿二元樹 | -| balanced binary tree | 平衡二分木 | 平衡二叉树 | 平衡二元樹 | -| binary search tree | 二分探索木 | 二叉搜索树 | 二元搜尋樹 | -| AVL tree | AVL木 | AVL 树 | AVL 樹 | -| red-black tree | 赤黒木 | 红黑树 | 紅黑樹 | -| level-order traversal | レベル順走査 | 层序遍历 | 層序走訪 | -| breadth-first traversal | 幅優先走査 | 广度优先遍历 | 廣度優先走訪 | -| depth-first traversal | 深さ優先走査 | 深度优先遍历 | 深度優先走訪 | -| binary search tree | 二分探索木 | 二叉搜索树 | 二元搜尋樹 | -| balanced binary search tree | 平衡二分探索木 | 平衡二叉搜索树 | 平衡二元搜尋樹 | -| balance factor | 平衡因子 | 平衡因子 | 平衡因子 | -| heap | ヒープ | 堆 | 堆積 | -| max heap | 最大ヒープ | 大顶堆 | 大頂堆積 | -| min heap | 最小ヒープ | 小顶堆 | 小頂堆積 | -| priority queue | 優先度キュー | 优先队列 | 優先佇列 | -| heapify | ヒープ化 | 堆化 | 堆積化 | -| top-$k$ problem | Top-$k$ 問題 | Top-$k$ 问题 | Top-$k$ 問題 | -| graph | グラフ | 图 | 圖 | -| vertex | 頂点 | 顶点 | 頂點 | -| undirected graph | 無向グラフ | 无向图 | 無向圖 | -| directed graph | 有向グラフ | 有向图 | 有向圖 | -| connected graph | 連結グラフ | 连通图 | 連通圖 | -| disconnected graph | 非連結グラフ | 非连通图 | 非連通圖 | -| weighted graph | 重み付きグラフ | 有权图 | 有權圖 | -| adjacency | 隣接 | 邻接 | 鄰接 | -| path | パス | 路径 | 路徑 | -| in-degree | 入次数 | 入度 | 入度 | -| out-degree | 出次数 | 出度 | 出度 | -| adjacency matrix | 隣接行列 | 邻接矩阵 | 鄰接矩陣 | -| adjacency list | 隣接リスト | 邻接表 | 鄰接表 | -| breadth-first search | 幅優先探索 | 广度优先搜索 | 廣度優先搜尋 | -| depth-first search | 深さ優先探索 | 深度优先搜索 | 深度優先搜尋 | -| binary search | 二分探索 | 二分查找 | 二分搜尋 | -| searching algorithm | 探索アルゴリズム | 搜索算法 | 搜尋演算法 | -| sorting algorithm | ソートアルゴリズム | 排序算法 | 排序演算法 | -| selection sort | 選択ソート | 选择排序 | 選擇排序 | -| bubble sort | バブルソート | 冒泡排序 | 泡沫排序 | -| insertion sort | 挿入ソート | 插入排序 | 插入排序 | -| quick sort | クイックソート | 快速排序 | 快速排序 | -| merge sort | マージソート | 归并排序 | 合併排序 | -| heap sort | ヒープソート | 堆排序 | 堆積排序 | -| bucket sort | バケットソート | 桶排序 | 桶排序 | -| counting sort | 計数ソート | 计数排序 | 計數排序 | -| radix sort | 基数ソート | 基数排序 | 基數排序 | -| divide and conquer | 分割統治法 | 分治 | 分治 | -| hanota problem | ハノイの塔問題 | 汉诺塔问题 | 河內塔問題 | -| backtracking algorithm | バックトラッキング | 回溯算法 | 回溯演算法 | -| constraint | 制約 | 约束 | 約束 | -| solution | 解 | 解 | 解 | -| state | 状態 | 状态 | 狀態 | -| pruning | 枝刈り | 剪枝 | 剪枝 | -| permutations problem | 順列問題 | 全排列问题 | 全排列問題 | -| subset-sum problem | 部分集合和問題 | 子集和问题 | 子集合問題 | -| $n$-queens problem | $n$ クイーン問題 | $n$ 皇后问题 | $n$ 皇后問題 | -| dynamic programming | 動的プログラミング | 动态规划 | 動態規劃 | -| initial state | 初期状態 | 初始状态 | 初始狀態 | -| state-transition equation | 状態遷移方程式 | 状态转移方程 | 狀態轉移方程 | -| knapsack problem | ナップサック問題 | 背包问题 | 背包問題 | -| edit distance problem | 編集距離問題 | 编辑距离问题 | 編輯距離問題 | -| greedy algorithm | 貪欲アルゴリズム | 贪心算法 | 貪婪演算法 | +| English | 日本語 | 日本語 | +| ------------------------------ | -------------- | -------------- | +| algorithm | アルゴリズム | アルゴリズム | +| data structure | データ構造 | データ構造 | +| code | コード | コード | +| file | ファイル | ファイル | +| function | 関数 | 関数 | +| method | メソッド | メソッド | +| variable | 変数 | 変数 | +| asymptotic complexity analysis | 漸近計算量解析 | 漸近計算量解析 | +| time complexity | 時間計算量 | 時間計算量 | +| space complexity | 空間計算量 | 空間計算量 | +| loop | ループ | ループ | +| iteration | 反復 | 反復 | +| recursion | 再帰 | 再帰 | +| tail recursion | 末尾再帰 | 末尾再帰 | +| recursion tree | 再帰木 | 再帰木 | +| big-$O$ notation | ビッグオー記法 | ビッグオー記法 | +| asymptotic upper bound | 漸近上界 | 漸近上界 | +| sign-magnitude | 符号絶対値表現 | 符号絶対値表現 | +| 1’s complement | 1の補数 | 1の補数 | +| 2’s complement | 2の補数 | 2の補数 | +| array | 配列 | 配列 | +| index | インデックス | インデックス | +| linked list | 連結リスト | 連結リスト | +| linked list node, list node | 連結リストノード | 連結リストノード | +| head node | 先頭ノード | 先頭ノード | +| tail node | 末尾ノード | 末尾ノード | +| list | リスト | リスト | +| dynamic array | 動的配列 | 動的配列 | +| hard disk | ハードディスク | ハードディスク | +| random-access memory (RAM) | メモリ | メモリ | +| cache memory | キャッシュ | キャッシュ | +| cache miss | キャッシュミス | キャッシュミス | +| cache hit rate | キャッシュヒット率 | キャッシュヒット率 | +| stack | スタック | スタック | +| top of the stack | スタックトップ | スタックトップ | +| bottom of the stack | スタックボトム | スタックボトム | +| queue | キュー | キュー | +| double-ended queue | 両端キュー | 両端キュー | +| front of the queue | キュー先頭 | キュー先頭 | +| rear of the queue | キュー末尾 | キュー末尾 | +| hash table | ハッシュテーブル | ハッシュテーブル | +| hash set | ハッシュ集合 | ハッシュ集合 | +| bucket | バケット | バケット | +| hash function | ハッシュ関数 | ハッシュ関数 | +| hash collision | ハッシュ衝突 | ハッシュ衝突 | +| load factor | 負荷率 | 負荷率 | +| separate chaining | 連鎖アドレス法 | 連鎖アドレス法 | +| open addressing | オープンアドレス法 | オープンアドレス法 | +| linear probing | 線形探索 | 線形探索 | +| lazy deletion | 遅延削除 | 遅延削除 | +| binary tree | 二分木 | 二分木 | +| tree node | ノード | ノード | +| left-child node | 左子ノード | 左子ノード | +| right-child node | 右子ノード | 右子ノード | +| parent node | 親ノード | 親ノード | +| left subtree | 左部分木 | 左部分木 | +| right subtree | 右部分木 | 右部分木 | +| root node | 根ノード | 根ノード | +| leaf node | 葉ノード | 葉ノード | +| edge | 辺 | 辺 | +| level | レベル | レベル | +| degree | 次数 | 次数 | +| height | 高さ | 高さ | +| depth | 深さ | 深さ | +| perfect binary tree | 完備二分木 | 完備二分木 | +| complete binary tree | 完全二分木 | 完全二分木 | +| full binary tree | 満二分木 | 満二分木 | +| balanced binary tree | 平衡二分木 | 平衡二分木 | +| binary search tree | 二分探索木 | 二分探索木 | +| AVL tree | AVL 木 | AVL 木 | +| red-black tree | 赤黒木 | 赤黒木 | +| level-order traversal | レベル順走査 | レベル順走査 | +| breadth-first traversal | 幅優先走査 | 幅優先走査 | +| depth-first traversal | 深さ優先走査 | 深さ優先走査 | +| binary search tree | 二分探索木 | 二分探索木 | +| balanced binary search tree | 平衡二分探索木 | 平衡二分探索木 | +| balance factor | 平衡係数 | 平衡係数 | +| heap | ヒープ | ヒープ | +| max heap | 最大ヒープ | 最大ヒープ | +| min heap | 最小ヒープ | 最小ヒープ | +| priority queue | 優先度付きキュー | 優先度付きキュー | +| heapify | ヒープ化 | ヒープ化 | +| top-$k$ problem | Top-$k$ 問題 | Top-$k$ 問題 | +| graph | グラフ | グラフ | +| vertex | 頂点 | 頂点 | +| undirected graph | 無向グラフ | 無向グラフ | +| directed graph | 有向グラフ | 有向グラフ | +| connected graph | 連結グラフ | 連結グラフ | +| disconnected graph | 非連結グラフ | 非連結グラフ | +| weighted graph | 重み付きグラフ | 重み付きグラフ | +| adjacency | 隣接 | 隣接 | +| path | 経路 | 経路 | +| in-degree | 入次数 | 入次数 | +| out-degree | 出次数 | 出次数 | +| adjacency matrix | 隣接行列 | 隣接行列 | +| adjacency list | 隣接リスト | 隣接リスト | +| breadth-first search | 幅優先探索 | 幅優先探索 | +| depth-first search | 深さ優先探索 | 深さ優先探索 | +| binary search | 二分探索 | 二分探索 | +| searching algorithm | 探索アルゴリズム | 探索アルゴリズム | +| sorting algorithm | ソートアルゴリズム | ソートアルゴリズム | +| selection sort | 選択ソート | 選択ソート | +| bubble sort | バブルソート | バブルソート | +| insertion sort | 挿入ソート | 挿入ソート | +| quick sort | クイックソート | クイックソート | +| merge sort | マージソート | マージソート | +| heap sort | ヒープソート | ヒープソート | +| bucket sort | バケットソート | バケットソート | +| counting sort | 計数ソート | 計数ソート | +| radix sort | 基数ソート | 基数ソート | +| divide and conquer | 分割統治 | 分割統治 | +| hanota problem | ハノイの塔問題 | ハノイの塔問題 | +| backtracking algorithm | バックトラッキングアルゴリズム | バックトラッキングアルゴリズム | +| constraint | 制約 | 制約 | +| solution | 解 | 解 | +| state | 状態 | 状態 | +| pruning | 枝刈り | 枝刈り | +| permutations problem | 全順列問題 | 全順列問題 | +| subset-sum problem | 部分和問題 | 部分和問題 | +| $n$-queens problem | $n$ クイーン問題 | $n$ クイーン問題 | +| dynamic programming | 動的計画法 | 動的計画法 | +| initial state | 初期状態 | 初期状態 | +| state-transition equation | 状態遷移方程式 | 状態遷移方程式 | +| knapsack problem | ナップサック問題 | ナップサック問題 | +| edit distance problem | 編集距離問題 | 編集距離問題 | +| greedy algorithm | 貪欲法 | 貪欲法 |
diff --git a/ja/docs/chapter_array_and_linkedlist/array.md b/ja/docs/chapter_array_and_linkedlist/array.md index ad3563da8..4614ae99e 100644 --- a/ja/docs/chapter_array_and_linkedlist/array.md +++ b/ja/docs/chapter_array_and_linkedlist/array.md @@ -4,22 +4,22 @@ comments: true # 4.1   配列 -配列は線形データ構造で、同じような項目が並んでいるようなもので、コンピュータのメモリ内の連続した空間に一緒に格納されます。これは整理された格納を維持するシーケンスのようなものです。この並びの各項目には、インデックスとして知られる独自の「位置」があります。以下の図を参照して、配列の動作を観察し、これらの重要な用語を理解してください。 +配列(array)は線形データ構造の一種であり、同じ型の要素を連続したメモリ領域に格納します。要素が配列内にある位置を、その要素のインデックス(index)と呼びます。下図は、配列の主要な概念と格納方式を示しています。 -![配列の定義と格納方法](array.assets/array_definition.png){ class="animation-figure" } +![配列の定義と格納方式](array.assets/array_definition.png){ class="animation-figure" } -

図 4-1   配列の定義と格納方法

+

図 4-1   配列の定義と格納方式

## 4.1.1   配列の一般的な操作 ### 1.   配列の初期化 -配列は必要に応じて2つの方法で初期化できます:初期値なしまたは指定された初期値付きです。初期値が指定されていない場合、ほとんどのプログラミング言語は配列要素を$0$に設定します: +必要に応じて、配列の初期化方法として初期値なしと初期値ありの 2 種類を使い分けられます。初期値を指定しない場合、多くのプログラミング言語では配列要素は $0$ に初期化されます。 === "Python" ```python title="array.py" - # 配列を初期化 + # 配列を初期化する arr: list[int] = [0] * 5 # [ 0, 0, 0, 0, 0 ] nums: list[int] = [1, 3, 2, 5, 4] ``` @@ -27,11 +27,11 @@ comments: true === "C++" ```cpp title="array.cpp" - /* 配列を初期化 */ - // スタックに格納 + /* 配列を初期化する */ + // スタック上に格納 int arr[5]; int nums[5] = { 1, 3, 2, 5, 4 }; - // ヒープに格納(手動でのメモリ解放が必要) + // ヒープ上に格納(手動で領域を解放する必要がある) int* arr1 = new int[5]; int* nums1 = new int[5] { 1, 3, 2, 5, 4 }; ``` @@ -39,7 +39,7 @@ comments: true === "Java" ```java title="array.java" - /* 配列を初期化 */ + /* 配列を初期化する */ int[] arr = new int[5]; // { 0, 0, 0, 0, 0 } int[] nums = { 1, 3, 2, 5, 4 }; ``` @@ -47,7 +47,7 @@ comments: true === "C#" ```csharp title="array.cs" - /* 配列を初期化 */ + /* 配列を初期化する */ int[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ] int[] nums = [1, 3, 2, 5, 4]; ``` @@ -55,18 +55,18 @@ comments: true === "Go" ```go title="array.go" - /* 配列を初期化 */ + /* 配列を初期化する */ var arr [5]int - // Goでは、長さを指定([5]int)すると配列を示し、指定しない([]int)とスライスを示します。 - // Goの配列はコンパイル時に固定長を持つよう設計されているため、長さの指定には定数のみ使用できます。 - // extend()メソッドの実装の便宜上、ここではSliceを配列として扱います。 + // Go では、長さを指定する場合([5]int)は配列であり、長さを指定しない場合([]int)はスライス + // Go の配列はコンパイル時に長さが確定するよう設計されているため、長さの指定には定数しか使用できない + // 拡張 extend() メソッドを実装しやすくするため、以下ではスライス(Slice)を配列(Array)として扱う nums := []int{1, 3, 2, 5, 4} ``` === "Swift" ```swift title="array.swift" - /* 配列を初期化 */ + /* 配列を初期化する */ let arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0] let nums = [1, 3, 2, 5, 4] ``` @@ -74,7 +74,7 @@ comments: true === "JS" ```javascript title="array.js" - /* 配列を初期化 */ + /* 配列を初期化する */ var arr = new Array(5).fill(0); var nums = [1, 3, 2, 5, 4]; ``` @@ -82,7 +82,7 @@ comments: true === "TS" ```typescript title="array.ts" - /* 配列を初期化 */ + /* 配列を初期化する */ let arr: number[] = new Array(5).fill(0); let nums: number[] = [1, 3, 2, 5, 4]; ``` @@ -90,7 +90,7 @@ comments: true === "Dart" ```dart title="array.dart" - /* 配列を初期化 */ + /* 配列を初期化する */ List arr = List.filled(5, 0); // [0, 0, 0, 0, 0] List nums = [1, 3, 2, 5, 4]; ``` @@ -98,20 +98,20 @@ comments: true === "Rust" ```rust title="array.rs" - /* 配列を初期化 */ + /* 配列を初期化する */ let arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0] let slice: &[i32] = &[0; 5]; - // Rustでは、長さを指定([i32; 5])すると配列を示し、指定しない(&[i32])とスライスを示します。 - // Rustの配列はコンパイル時に固定長を持つよう設計されているため、長さの指定には定数のみ使用できます。 - // 一般的にRustでは動的配列としてVectorが使用されます。 - // extend()メソッドの実装の便宜上、ここではベクターを配列として扱います。 + // Rust では、長さを指定する場合([i32; 5])は配列であり、長さを指定しない場合(&[i32])はスライス + // Rust の配列はコンパイル時に長さが確定するよう設計されているため、長さの指定には定数しか使用できない + // Vector は Rust で一般に動的配列として使われる型 + // 拡張 extend() メソッドを実装しやすくするため、以下では vector を配列(array)として扱う let nums: Vec = vec![1, 3, 2, 5, 4]; ``` === "C" ```c title="array.c" - /* 配列を初期化 */ + /* 配列を初期化する */ int arr[5] = { 0 }; // { 0, 0, 0, 0, 0 } int nums[5] = { 1, 3, 2, 5, 4 }; ``` @@ -119,27 +119,41 @@ comments: true === "Kotlin" ```kotlin title="array.kt" - + /* 配列を初期化する */ + var arr = IntArray(5) // { 0, 0, 0, 0, 0 } + var nums = intArrayOf(1, 3, 2, 5, 4) ``` +=== "Ruby" + + ```ruby title="array.rb" + # 配列を初期化する + arr = Array.new(5, 0) + nums = [1, 3, 2, 5, 4] + ``` + +??? pythontutor "実行の可視化" + + https://pythontutor.com/render.html#code=%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0Aarr%20%3D%20%5B0%5D%20*%205%20%20%23%20%5B%200,%200,%200,%200,%200%20%5D%0Anums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + ### 2.   要素へのアクセス -配列内の要素は連続したメモリ空間に格納されるため、各要素のメモリアドレスを計算することが簡単になります。以下の図に示されている公式は、配列のメモリアドレス(特に、最初の要素のアドレス)と要素のインデックスを利用して、要素のメモリアドレスを決定するのに役立ちます。この計算により、目的の要素への直接アクセスが合理化されます。 +配列要素は連続したメモリ領域に格納されるため、要素のメモリアドレスの計算は非常に容易です。配列のメモリアドレス(先頭要素のメモリアドレス)とある要素のインデックスが与えられれば、下図の式を使ってその要素のメモリアドレスを計算でき、直接その要素にアクセスできます。 -![配列要素のメモリアドレス計算](array.assets/array_memory_location_calculation.png){ class="animation-figure" } +![配列要素のメモリアドレスの計算](array.assets/array_memory_location_calculation.png){ class="animation-figure" } -

図 4-2   配列要素のメモリアドレス計算

+

図 4-2   配列要素のメモリアドレスの計算

-上の図で観察されるように、配列のインデックスは慣例的に$0$から始まります。これは直感に反するように見えるかもしれません。数を数えるのは通常$1$から始まるためですが、アドレス計算公式内では、**インデックスは本質的にメモリアドレスからのオフセット**です。最初の要素のアドレスでは、このオフセットは$0$で、そのインデックスが$0$であることを検証しています。 +上図を見ると、配列の最初の要素のインデックスは $0$ であり、これは少し直感に反するように思えます。というのも、$1$ から数え始めるほうが自然だからです。しかし、アドレス計算式の観点では、**インデックスの本質はメモリアドレスのオフセット**です。先頭要素のアドレスのオフセットは $0$ であるため、そのインデックスが $0$ なのは妥当です。 -配列内の要素へのアクセスは非常に効率的で、$O(1)$時間で任意の要素にランダムアクセスできます。 +配列では要素へのアクセスは非常に効率的であり、$O(1)$ 時間で任意の要素にランダムアクセスできます。 === "Python" ```python title="array.py" def random_access(nums: list[int]) -> int: - """要素へのランダムアクセス""" - # 区間 [0, len(nums)-1] から数値をランダムに選択 + """要素へランダムアクセス""" + # 区間 [0, len(nums)-1] からランダムに数字を 1 つ選ぶ random_index = random.randint(0, len(nums) - 1) # ランダムな要素を取得して返す random_num = nums[random_index] @@ -149,11 +163,11 @@ comments: true === "C++" ```cpp title="array.cpp" - /* 要素への乱数アクセス */ + /* 要素へランダムアクセス */ int randomAccess(int *nums, int size) { - // [0, size)の範囲で乱数を選択 + // 区間 [0, size) からランダムに 1 つの数を選ぶ int randomIndex = rand() % size; - // 乱数要素を取得して返却 + // ランダムな要素を取得して返す int randomNum = nums[randomIndex]; return randomNum; } @@ -162,11 +176,11 @@ comments: true === "Java" ```java title="array.java" - /* 要素へのランダムアクセス */ + /* 要素へランダムアクセス */ int randomAccess(int[] nums) { - // 区間 [0, nums.length) からランダムに数を選択 + // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length); - // ランダム要素を取得して返す + // ランダムな要素を取得して返す int randomNum = nums[randomIndex]; return randomNum; } @@ -175,95 +189,171 @@ comments: true === "C#" ```csharp title="array.cs" - [class]{array}-[func]{RandomAccess} + /* 要素へランダムアクセス */ + int RandomAccess(int[] nums) { + Random random = new(); + // 区間 [0, nums.Length) からランダムに数字を 1 つ選ぶ + int randomIndex = random.Next(nums.Length); + // ランダムな要素を取得して返す + int randomNum = nums[randomIndex]; + return randomNum; + } ``` === "Go" ```go title="array.go" - [class]{}-[func]{randomAccess} + /* 要素へランダムアクセス */ + func randomAccess(nums []int) (randomNum int) { + // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ + randomIndex := rand.Intn(len(nums)) + // ランダムな要素を取得して返す + randomNum = nums[randomIndex] + return + } ``` === "Swift" ```swift title="array.swift" - [class]{}-[func]{randomAccess} + /* 要素へランダムアクセス */ + func randomAccess(nums: [Int]) -> Int { + // 区間 [0, nums.count) からランダムに数字を 1 つ選ぶ + let randomIndex = nums.indices.randomElement()! + // ランダムな要素を取得して返す + let randomNum = nums[randomIndex] + return randomNum + } ``` === "JS" ```javascript title="array.js" - [class]{}-[func]{randomAccess} + /* 要素へランダムアクセス */ + function randomAccess(nums) { + // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ + const random_index = Math.floor(Math.random() * nums.length); + // ランダムな要素を取得して返す + const random_num = nums[random_index]; + return random_num; + } ``` === "TS" ```typescript title="array.ts" - [class]{}-[func]{randomAccess} + /* 要素へランダムアクセス */ + function randomAccess(nums: number[]): number { + // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ + const random_index = Math.floor(Math.random() * nums.length); + // ランダムな要素を取得して返す + const random_num = nums[random_index]; + return random_num; + } ``` === "Dart" ```dart title="array.dart" - [class]{}-[func]{randomAccess} + /* 要素へランダムアクセス */ + int randomAccess(List nums) { + // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ + int randomIndex = Random().nextInt(nums.length); + // ランダムな要素を取得して返す + int randomNum = nums[randomIndex]; + return randomNum; + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{random_access} + /* 要素へランダムアクセス */ + fn random_access(nums: &[i32]) -> i32 { + // 区間 [0, nums.len()) からランダムに数字を 1 つ選ぶ + let random_index = rand::thread_rng().gen_range(0..nums.len()); + // ランダムな要素を取得して返す + let random_num = nums[random_index]; + random_num + } ``` === "C" ```c title="array.c" - [class]{}-[func]{randomAccess} + /* 要素へランダムアクセス */ + int randomAccess(int *nums, int size) { + // 区間 [0, size) からランダムに 1 つの数を選ぶ + int randomIndex = rand() % size; + // ランダムな要素を取得して返す + int randomNum = nums[randomIndex]; + return randomNum; + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{randomAccess} + /* 要素へランダムアクセス */ + fun randomAccess(nums: IntArray): Int { + // 区間 [0, nums.size) からランダムに数字を 1 つ選ぶ + val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size) + // ランダムな要素を取得して返す + val randomNum = nums[randomIndex] + return randomNum + } ``` === "Ruby" ```ruby title="array.rb" - [class]{}-[func]{random_access} + ### 要素にランダムアクセス ### + def random_access(nums) + # 区間 [0, nums.length) からランダムに 1 つの数を選ぶ + random_index = Random.rand(0...nums.length) + + # ランダムな要素を取得して返す + nums[random_index] + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 3.   要素の挿入 -配列要素はメモリ内で密に詰まっており、それらの間に追加データを収容するための空間はありません。以下の図に示すように、配列の中央に要素を挿入するには、後続のすべての要素を1つずつ後ろにシフトして、新しい要素のための空間を作る必要があります。 +配列要素はメモリ内で「ぴったり隣接して」おり、その間にほかのデータを格納する余地はありません。下図のように、配列の途中に要素を挿入したい場合は、その要素より後ろにあるすべての要素を 1 つずつ後ろへずらし、その後でそのインデックスに要素を代入する必要があります。 -![配列要素挿入の例](array.assets/array_insert_element.png){ class="animation-figure" } +![配列への要素挿入の例](array.assets/array_insert_element.png){ class="animation-figure" } -

図 4-3   配列要素挿入の例

+

図 4-3   配列への要素挿入の例

-配列の長さが固定されているため、要素を挿入すると必然的に配列の最後の要素が失われることに注意することが重要です。この問題を解決する方法は「リスト」の章で探求されます。 +注意すべき点として、配列の長さは固定であるため、要素を 1 つ挿入すると配列末尾の要素が必ず「失われ」ます。この問題の解決策は「リスト」の章で扱います。 === "Python" ```python title="array.py" def insert(nums: list[int], num: int, index: int): - """インデックス index に要素 num を挿入""" - # インデックス index より後のすべての要素を1つ後ろに移動 + """配列の index 番目に要素 num を挿入""" + # インデックス index 以降の全要素を 1 つ後ろへ移動する for i in range(len(nums) - 1, index, -1): nums[i] = nums[i - 1] - # num を index の位置の要素に代入 + # index の要素に num を代入する nums[index] = num ``` === "C++" ```cpp title="array.cpp" - /* `index`に要素numを挿入 */ + /* 配列の index 番目に要素 num を挿入 */ void insert(int *nums, int size, int num, int index) { - // `index`より後のすべての要素を1つ後ろに移動 + // インデックス index 以降の全要素を 1 つ後ろへ移動する for (int i = size - 1; i > index; i--) { nums[i] = nums[i - 1]; } - // indexの位置にnumを代入 + // index の要素に num を代入する nums[index] = num; } ``` @@ -271,13 +361,13 @@ comments: true === "Java" ```java title="array.java" - /* `index` に要素 num を挿入 */ + /* 配列の index 番目に要素 num を挿入 */ void insert(int[] nums, int num, int index) { - // `index` より後のすべての要素を1つ後ろに移動 + // インデックス index 以降の全要素を 1 つ後ろへ移動する for (int i = nums.length - 1; i > index; i--) { nums[i] = nums[i - 1]; } - // index の要素に num を代入 + // index の要素に num を代入する nums[index] = num; } ``` @@ -285,79 +375,165 @@ comments: true === "C#" ```csharp title="array.cs" - [class]{array}-[func]{Insert} + /* 配列の index 番目に要素 num を挿入 */ + void Insert(int[] nums, int num, int index) { + // インデックス index 以降の全要素を 1 つ後ろへ移動する + for (int i = nums.Length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // index の要素に num を代入する + nums[index] = num; + } ``` === "Go" ```go title="array.go" - [class]{}-[func]{insert} + /* 配列の index 番目に要素 num を挿入 */ + func insert(nums []int, num int, index int) { + // インデックス index 以降の全要素を 1 つ後ろへ移動する + for i := len(nums) - 1; i > index; i-- { + nums[i] = nums[i-1] + } + // index の要素に num を代入する + nums[index] = num + } ``` === "Swift" ```swift title="array.swift" - [class]{}-[func]{insert} + /* 配列の index 番目に要素 num を挿入 */ + func insert(nums: inout [Int], num: Int, index: Int) { + // インデックス index 以降の全要素を 1 つ後ろへ移動する + for i in nums.indices.dropFirst(index).reversed() { + nums[i] = nums[i - 1] + } + // index の要素に num を代入する + nums[index] = num + } ``` === "JS" ```javascript title="array.js" - [class]{}-[func]{insert} + /* 配列の index 番目に要素 num を挿入 */ + function insert(nums, num, index) { + // インデックス index 以降の全要素を 1 つ後ろへ移動する + for (let i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // index の要素に num を代入する + nums[index] = num; + } ``` === "TS" ```typescript title="array.ts" - [class]{}-[func]{insert} + /* 配列の index 番目に要素 num を挿入 */ + function insert(nums: number[], num: number, index: number): void { + // インデックス index 以降の全要素を 1 つ後ろへ移動する + for (let i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // index の要素に num を代入する + nums[index] = num; + } ``` === "Dart" ```dart title="array.dart" - [class]{}-[func]{insert} + /* 配列の添字 index に要素 _num を挿入 */ + void insert(List nums, int _num, int index) { + // インデックス index 以降の全要素を 1 つ後ろへ移動する + for (var i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // _num を index の位置の要素に代入 + nums[index] = _num; + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{insert} + /* 配列の index 番目に要素 num を挿入 */ + fn insert(nums: &mut [i32], num: i32, index: usize) { + // インデックス index 以降の全要素を 1 つ後ろへ移動する + for i in (index + 1..nums.len()).rev() { + nums[i] = nums[i - 1]; + } + // index の要素に num を代入する + nums[index] = num; + } ``` === "C" ```c title="array.c" - [class]{}-[func]{insert} + /* 配列の index 番目に要素 num を挿入 */ + void insert(int *nums, int size, int num, int index) { + // インデックス index 以降の全要素を 1 つ後ろへ移動する + for (int i = size - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // index の要素に num を代入する + nums[index] = num; + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{insert} + /* 配列の index 番目に要素 num を挿入 */ + fun insert(nums: IntArray, num: Int, index: Int) { + // インデックス index 以降の全要素を 1 つ後ろへ移動する + for (i in nums.size - 1 downTo index + 1) { + nums[i] = nums[i - 1] + } + // index の要素に num を代入する + nums[index] = num + } ``` === "Ruby" ```ruby title="array.rb" - [class]{}-[func]{insert} + ### 配列のインデックス index に要素 num を挿入 ### + def insert(nums, num, index) + # インデックス index 以降の全要素を 1 つ後ろへ移動する + for i in (nums.length - 1).downto(index + 1) + nums[i] = nums[i - 1] + end + + # index の要素に num を代入する + nums[index] = num + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 4.   要素の削除 -同様に、以下の図に示すように、インデックス$i$の要素を削除するには、インデックス$i$に続くすべての要素を1つずつ前に移動する必要があります。 +同様に、下図のように、インデックス $i$ の要素を削除したい場合は、インデックス $i$ より後ろの要素をすべて 1 つずつ前へずらす必要があります。 -![配列要素削除の例](array.assets/array_remove_element.png){ class="animation-figure" } +![配列からの要素削除の例](array.assets/array_remove_element.png){ class="animation-figure" } -

図 4-4   配列要素削除の例

+

図 4-4   配列からの要素削除の例

-削除後、元の最後の要素は「意味がない」ものになるため、特定の修正は必要ないことに注意してください。 +注意してください。要素の削除が完了すると、もともとの末尾要素は「意味を持たない」状態になるため、わざわざ変更する必要はありません。 === "Python" ```python title="array.py" def remove(nums: list[int], index: int): - """インデックス index の要素を削除""" - # インデックス index より後のすべての要素を1つ前に移動 + """index の要素を削除する""" + # インデックス index より後ろの全要素を 1 つ前へ移動する for i in range(index, len(nums) - 1): nums[i] = nums[i + 1] ``` @@ -365,9 +541,9 @@ comments: true === "C++" ```cpp title="array.cpp" - /* `index`の要素を削除 */ + /* index の要素を削除する */ void remove(int *nums, int size, int index) { - // `index`より後のすべての要素を1つ前に移動 + // インデックス index より後ろの全要素を 1 つ前へ移動する for (int i = index; i < size - 1; i++) { nums[i] = nums[i + 1]; } @@ -377,9 +553,9 @@ comments: true === "Java" ```java title="array.java" - /* `index` の要素を削除 */ + /* index の要素を削除する */ void remove(int[] nums, int index) { - // `index` より後のすべての要素を1つ前に移動 + // インデックス index より後ろの全要素を 1 つ前へ移動する for (int i = index; i < nums.length - 1; i++) { nums[i] = nums[i + 1]; } @@ -389,86 +565,152 @@ comments: true === "C#" ```csharp title="array.cs" - [class]{array}-[func]{Remove} + /* index の要素を削除する */ + void Remove(int[] nums, int index) { + // インデックス index より後ろの全要素を 1 つ前へ移動する + for (int i = index; i < nums.Length - 1; i++) { + nums[i] = nums[i + 1]; + } + } ``` === "Go" ```go title="array.go" - [class]{}-[func]{remove} + /* index の要素を削除する */ + func remove(nums []int, index int) { + // インデックス index より後ろの全要素を 1 つ前へ移動する + for i := index; i < len(nums)-1; i++ { + nums[i] = nums[i+1] + } + } ``` === "Swift" ```swift title="array.swift" - [class]{}-[func]{remove} + /* index の要素を削除する */ + func remove(nums: inout [Int], index: Int) { + // インデックス index より後ろの全要素を 1 つ前へ移動する + for i in nums.indices.dropFirst(index).dropLast() { + nums[i] = nums[i + 1] + } + } ``` === "JS" ```javascript title="array.js" - [class]{}-[func]{remove} + /* index の要素を削除する */ + function remove(nums, index) { + // インデックス index より後ろの全要素を 1 つ前へ移動する + for (let i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } ``` === "TS" ```typescript title="array.ts" - [class]{}-[func]{remove} + /* index の要素を削除する */ + function remove(nums: number[], index: number): void { + // インデックス index より後ろの全要素を 1 つ前へ移動する + for (let i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } ``` === "Dart" ```dart title="array.dart" - [class]{}-[func]{remove} + /* index の要素を削除する */ + void remove(List nums, int index) { + // インデックス index より後ろの全要素を 1 つ前へ移動する + for (var i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{remove} + /* index の要素を削除する */ + fn remove(nums: &mut [i32], index: usize) { + // インデックス index より後ろの全要素を 1 つ前へ移動する + for i in index..nums.len() - 1 { + nums[i] = nums[i + 1]; + } + } ``` === "C" ```c title="array.c" - [class]{}-[func]{removeItem} + /* index の要素を削除する */ + // 注意: stdio.h が remove 識別子を使用している + void removeItem(int *nums, int size, int index) { + // インデックス index より後ろの全要素を 1 つ前へ移動する + for (int i = index; i < size - 1; i++) { + nums[i] = nums[i + 1]; + } + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{remove} + /* index の要素を削除する */ + fun remove(nums: IntArray, index: Int) { + // インデックス index より後ろの全要素を 1 つ前へ移動する + for (i in index..
+ + +全体として見ると、配列の挿入と削除には次の欠点があります。 + +- **時間計算量が高い**:配列の挿入と削除の平均時間計算量はいずれも $O(n)$ であり、ここで $n$ は配列長です。 +- **要素が失われる**:配列の長さは不変であるため、要素を挿入すると配列長の範囲を超えた要素は失われます。 +- **メモリの浪費**:やや長めの配列を初期化して先頭部分だけを使うこともでき、この場合データ挿入時に失われる末尾要素はすべて「無意味」ですが、その代わり一部のメモリ領域が無駄になります。 ### 5.   配列の走査 -ほとんどのプログラミング言語では、インデックスを使用するか、各要素を直接反復することで配列を走査できます: +ほとんどのプログラミング言語では、インデックスを使って配列を走査することも、各要素を直接取り出しながら走査することもできます。 === "Python" ```python title="array.py" def traverse(nums: list[int]): - """配列の走査""" + """配列を走査""" count = 0 - # インデックスによる配列の走査 + # インデックスで配列を走査 for i in range(len(nums)): count += nums[i] - # 配列要素の走査 + # 配列要素を直接走査 for num in nums: count += num - # データのインデックスと要素の両方を走査 + # データのインデックスと要素を同時に走査する for i, num in enumerate(nums): count += nums[i] count += num @@ -477,10 +719,10 @@ comments: true === "C++" ```cpp title="array.cpp" - /* 配列の走査 */ + /* 配列を走査 */ void traverse(int *nums, int size) { int count = 0; - // インデックスによる配列の走査 + // インデックスで配列を走査 for (int i = 0; i < size; i++) { count += nums[i]; } @@ -493,11 +735,11 @@ comments: true /* 配列を走査 */ void traverse(int[] nums) { int count = 0; - // インデックスによる配列の走査 + // インデックスで配列を走査 for (int i = 0; i < nums.length; i++) { count += nums[i]; } - // 配列要素の走査 + // 配列要素を直接走査 for (int num : nums) { count += num; } @@ -507,74 +749,203 @@ comments: true === "C#" ```csharp title="array.cs" - [class]{array}-[func]{Traverse} + /* 配列を走査 */ + void Traverse(int[] nums) { + int count = 0; + // インデックスで配列を走査 + for (int i = 0; i < nums.Length; i++) { + count += nums[i]; + } + // 配列要素を直接走査 + foreach (int num in nums) { + count += num; + } + } ``` === "Go" ```go title="array.go" - [class]{}-[func]{traverse} + /* 配列を走査 */ + func traverse(nums []int) { + count := 0 + // インデックスで配列を走査 + for i := 0; i < len(nums); i++ { + count += nums[i] + } + count = 0 + // 配列要素を直接走査 + for _, num := range nums { + count += num + } + // データのインデックスと要素を同時に走査する + for i, num := range nums { + count += nums[i] + count += num + } + } ``` === "Swift" ```swift title="array.swift" - [class]{}-[func]{traverse} + /* 配列を走査 */ + func traverse(nums: [Int]) { + var count = 0 + // インデックスで配列を走査 + for i in nums.indices { + count += nums[i] + } + // 配列要素を直接走査 + for num in nums { + count += num + } + // データのインデックスと要素を同時に走査する + for (i, num) in nums.enumerated() { + count += nums[i] + count += num + } + } ``` === "JS" ```javascript title="array.js" - [class]{}-[func]{traverse} + /* 配列を走査 */ + function traverse(nums) { + let count = 0; + // インデックスで配列を走査 + for (let i = 0; i < nums.length; i++) { + count += nums[i]; + } + // 配列要素を直接走査 + for (const num of nums) { + count += num; + } + } ``` === "TS" ```typescript title="array.ts" - [class]{}-[func]{traverse} + /* 配列を走査 */ + function traverse(nums: number[]): void { + let count = 0; + // インデックスで配列を走査 + for (let i = 0; i < nums.length; i++) { + count += nums[i]; + } + // 配列要素を直接走査 + for (const num of nums) { + count += num; + } + } ``` === "Dart" ```dart title="array.dart" - [class]{}-[func]{traverse} + /* 配列要素を走査する */ + void traverse(List nums) { + int count = 0; + // インデックスで配列を走査 + for (var i = 0; i < nums.length; i++) { + count += nums[i]; + } + // 配列要素を直接走査 + for (int _num in nums) { + count += _num; + } + // forEach メソッドで配列を走査する + nums.forEach((_num) { + count += _num; + }); + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{traverse} + /* 配列を走査 */ + fn traverse(nums: &[i32]) { + let mut _count = 0; + // インデックスで配列を走査 + for i in 0..nums.len() { + _count += nums[i]; + } + // 配列要素を直接走査 + _count = 0; + for &num in nums { + _count += num; + } + } ``` === "C" ```c title="array.c" - [class]{}-[func]{traverse} + /* 配列を走査 */ + void traverse(int *nums, int size) { + int count = 0; + // インデックスで配列を走査 + for (int i = 0; i < size; i++) { + count += nums[i]; + } + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{traverse} + /* 配列を走査 */ + fun traverse(nums: IntArray) { + var count = 0 + // インデックスで配列を走査 + for (i in nums.indices) { + count += nums[i] + } + // 配列要素を直接走査 + for (j in nums) { + count += j + } + } ``` === "Ruby" ```ruby title="array.rb" - [class]{}-[func]{traverse} + ### 配列を走査 ### + def traverse(nums) + count = 0 + + # インデックスで配列を走査 + for i in 0...nums.length + count += nums[i] + end + + # 配列要素を直接走査 + for num in nums + count += num + end + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 6.   要素の検索 -配列内の特定の要素を見つけることは、配列を反復し、各要素をチェックして目的の値と一致するかどうかを決定することを含みます。 +配列内で指定した要素を探すには、配列を走査し、各反復で要素値が一致するかを判定し、一致したら対応するインデックスを出力します。 -配列は線形データ構造であるため、この操作は一般的に「線形探索」と呼ばれます。 +配列は線形データ構造であるため、上記の検索操作は「線形探索」と呼ばれます。 === "Python" ```python title="array.py" def find(nums: list[int], target: int) -> int: - """配列内の指定された要素を検索""" + """配列内で指定要素を探す""" for i in range(len(nums)): if nums[i] == target: return i @@ -584,7 +955,7 @@ comments: true === "C++" ```cpp title="array.cpp" - /* 配列内の指定要素を検索 */ + /* 配列内で指定要素を探す */ int find(int *nums, int size, int target) { for (int i = 0; i < size; i++) { if (nums[i] == target) @@ -597,7 +968,7 @@ comments: true === "Java" ```java title="array.java" - /* 配列内で指定された要素を検索 */ + /* 配列内で指定要素を探す */ int find(int[] nums, int target) { for (int i = 0; i < nums.length; i++) { if (nums[i] == target) @@ -610,77 +981,156 @@ comments: true === "C#" ```csharp title="array.cs" - [class]{array}-[func]{Find} + /* 配列内で指定要素を探す */ + int Find(int[] nums, int target) { + for (int i = 0; i < nums.Length; i++) { + if (nums[i] == target) + return i; + } + return -1; + } ``` === "Go" ```go title="array.go" - [class]{}-[func]{find} + /* 配列内で指定要素を探す */ + func find(nums []int, target int) (index int) { + index = -1 + for i := 0; i < len(nums); i++ { + if nums[i] == target { + index = i + break + } + } + return + } ``` === "Swift" ```swift title="array.swift" - [class]{}-[func]{find} + /* 配列内で指定要素を探す */ + func find(nums: [Int], target: Int) -> Int { + for i in nums.indices { + if nums[i] == target { + return i + } + } + return -1 + } ``` === "JS" ```javascript title="array.js" - [class]{}-[func]{find} + /* 配列内で指定要素を探す */ + function find(nums, target) { + for (let i = 0; i < nums.length; i++) { + if (nums[i] === target) return i; + } + return -1; + } ``` === "TS" ```typescript title="array.ts" - [class]{}-[func]{find} + /* 配列内で指定要素を探す */ + function find(nums: number[], target: number): number { + for (let i = 0; i < nums.length; i++) { + if (nums[i] === target) { + return i; + } + } + return -1; + } ``` === "Dart" ```dart title="array.dart" - [class]{}-[func]{find} + /* 配列内で指定要素を探す */ + int find(List nums, int target) { + for (var i = 0; i < nums.length; i++) { + if (nums[i] == target) return i; + } + return -1; + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{find} + /* 配列内で指定要素を探す */ + fn find(nums: &[i32], target: i32) -> Option { + for i in 0..nums.len() { + if nums[i] == target { + return Some(i); + } + } + None + } ``` === "C" ```c title="array.c" - [class]{}-[func]{find} + /* 配列内で指定要素を探す */ + int find(int *nums, int size, int target) { + for (int i = 0; i < size; i++) { + if (nums[i] == target) + return i; + } + return -1; + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{find} + /* 配列内で指定要素を探す */ + fun find(nums: IntArray, target: Int): Int { + for (i in nums.indices) { + if (nums[i] == target) + return i + } + return -1 + } ``` === "Ruby" ```ruby title="array.rb" - [class]{}-[func]{find} + ### 配列内の指定要素を検索 ### + def find(nums, target) + for i in 0...nums.length + return i if nums[i] == target + end + + -1 + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 7.   配列の拡張 -複雑なシステム環境では、安全な容量拡張のために配列の後にメモリ空間の可用性を確保することが困難になります。その結果、ほとんどのプログラミング言語では、**配列の長さは不変**です。 +複雑なシステム環境では、配列の後方にあるメモリ領域が利用可能であることをプログラム側で保証できず、そのため安全に配列容量を拡張できません。したがって、ほとんどのプログラミング言語では、**配列の長さは不変です**。 -配列を拡張するには、より大きな配列を作成し、元の配列から要素をコピーする必要があります。この操作の時間計算量は$O(n)$で、大きな配列では時間がかかる可能性があります。コードは以下の通りです: +配列を拡張したい場合は、より大きな新しい配列を作り、元の配列の要素を順に新配列へコピーする必要があります。これは $O(n)$ の操作であり、配列が大きい場合は非常に時間がかかります。コードは次のとおりです。 === "Python" ```python title="array.py" def extend(nums: list[int], enlarge: int) -> list[int]: - """配列の長さを拡張""" - # 拡張された長さの配列を初期化 + """配列長を拡張する""" + # 拡張後の長さを持つ配列を初期化する res = [0] * (len(nums) + enlarge) - # 元の配列のすべての要素を新しい配列にコピー + # 元の配列の全要素を新しい配列にコピー for i in range(len(nums)): res[i] = nums[i] # 拡張後の新しい配列を返す @@ -690,17 +1140,17 @@ comments: true === "C++" ```cpp title="array.cpp" - /* 配列長の拡張 */ + /* 配列長を拡張する */ int *extend(int *nums, int size, int enlarge) { - // 拡張された長さの配列を初期化 + // 拡張後の長さを持つ配列を初期化する int *res = new int[size + enlarge]; // 元の配列の全要素を新しい配列にコピー for (int i = 0; i < size; i++) { res[i] = nums[i]; } - // メモリを解放 + // メモリを解放する delete[] nums; - // 拡張後の新しい配列を返却 + // 拡張後の新しい配列を返す return res; } ``` @@ -708,11 +1158,11 @@ comments: true === "Java" ```java title="array.java" - /* 配列長の拡張 */ + /* 配列長を拡張する */ int[] extend(int[] nums, int enlarge) { - // 拡張された長さの配列を初期化 + // 拡張後の長さを持つ配列を初期化する int[] res = new int[nums.length + enlarge]; - // 元の配列のすべての要素を新しい配列にコピー + // 元の配列の全要素を新しい配列にコピー for (int i = 0; i < nums.length; i++) { res[i] = nums[i]; } @@ -724,83 +1174,199 @@ comments: true === "C#" ```csharp title="array.cs" - [class]{array}-[func]{Extend} + /* 配列長を拡張する */ + int[] Extend(int[] nums, int enlarge) { + // 拡張後の長さを持つ配列を初期化する + int[] res = new int[nums.Length + enlarge]; + // 元の配列の全要素を新しい配列にコピー + for (int i = 0; i < nums.Length; i++) { + res[i] = nums[i]; + } + // 拡張後の新しい配列を返す + return res; + } ``` === "Go" ```go title="array.go" - [class]{}-[func]{extend} + /* 配列長を拡張する */ + func extend(nums []int, enlarge int) []int { + // 拡張後の長さを持つ配列を初期化する + res := make([]int, len(nums)+enlarge) + // 元の配列の全要素を新しい配列にコピー + for i, num := range nums { + res[i] = num + } + // 拡張後の新しい配列を返す + return res + } ``` === "Swift" ```swift title="array.swift" - [class]{}-[func]{extend} + /* 配列長を拡張する */ + func extend(nums: [Int], enlarge: Int) -> [Int] { + // 拡張後の長さを持つ配列を初期化する + var res = Array(repeating: 0, count: nums.count + enlarge) + // 元の配列の全要素を新しい配列にコピー + for i in nums.indices { + res[i] = nums[i] + } + // 拡張後の新しい配列を返す + return res + } ``` === "JS" ```javascript title="array.js" - [class]{}-[func]{extend} + /* 配列長を拡張する */ + // JavaScript の Array は動的配列であり、直接拡張できます + // 学習しやすいよう、本関数では Array を長さ不変の配列として扱います + function extend(nums, enlarge) { + // 拡張後の長さを持つ配列を初期化する + const res = new Array(nums.length + enlarge).fill(0); + // 元の配列の全要素を新しい配列にコピー + for (let i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // 拡張後の新しい配列を返す + return res; + } ``` === "TS" ```typescript title="array.ts" - [class]{}-[func]{extend} + /* 配列長を拡張する */ + // TypeScript の Array は動的配列であり、直接拡張できます + // 学習しやすいよう、本関数では Array を長さ不変の配列として扱います + function extend(nums: number[], enlarge: number): number[] { + // 拡張後の長さを持つ配列を初期化する + const res = new Array(nums.length + enlarge).fill(0); + // 元の配列の全要素を新しい配列にコピー + for (let i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // 拡張後の新しい配列を返す + return res; + } ``` === "Dart" ```dart title="array.dart" - [class]{}-[func]{extend} + /* 配列長を拡張する */ + List extend(List nums, int enlarge) { + // 拡張後の長さを持つ配列を初期化する + List res = List.filled(nums.length + enlarge, 0); + // 元の配列の全要素を新しい配列にコピー + for (var i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // 拡張後の新しい配列を返す + return res; + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{extend} + /* 配列長を拡張する */ + fn extend(nums: &[i32], enlarge: usize) -> Vec { + // 拡張後の長さを持つ配列を初期化する + let mut res: Vec = vec![0; nums.len() + enlarge]; + // 元の配列の全要素を新しい配列にコピー + res[0..nums.len()].copy_from_slice(nums); + + // 拡張後の新しい配列を返す + res + } ``` === "C" ```c title="array.c" - [class]{}-[func]{extend} + /* 配列長を拡張する */ + int *extend(int *nums, int size, int enlarge) { + // 拡張後の長さを持つ配列を初期化する + int *res = (int *)malloc(sizeof(int) * (size + enlarge)); + // 元の配列の全要素を新しい配列にコピー + for (int i = 0; i < size; i++) { + res[i] = nums[i]; + } + // 拡張後の領域を初期化する + for (int i = size; i < size + enlarge; i++) { + res[i] = 0; + } + // 拡張後の新しい配列を返す + return res; + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{extend} + /* 配列長を拡張する */ + fun extend(nums: IntArray, enlarge: Int): IntArray { + // 拡張後の長さを持つ配列を初期化する + val res = IntArray(nums.size + enlarge) + // 元の配列の全要素を新しい配列にコピー + for (i in nums.indices) { + res[i] = nums[i] + } + // 拡張後の新しい配列を返す + return res + } ``` === "Ruby" ```ruby title="array.rb" - [class]{}-[func]{extend} + ### 配列長を拡張 ### + # Ruby の Array は動的配列であり、直接拡張できます + # 学習しやすいよう、本関数では Array を長さ不変の配列として扱います + def extend(nums, enlarge) + # 拡張後の長さを持つ配列を初期化する + res = Array.new(nums.length + enlarge, 0) + + # 元の配列の全要素を新しい配列にコピー + for i in 0...nums.length + res[i] = nums[i] + end + + # 拡張後の新しい配列を返す + res + end ``` -## 4.1.2   配列の利点と制限 +??? pythontutor "コードの可視化" -配列は連続したメモリ空間に格納され、同じ型の要素で構成されます。このアプローチは、システムがデータ構造操作の効率を最適化するために活用できる実質的な事前情報を提供します。 +
+ -- **高い空間効率**:配列はデータのための連続したメモリブロックを割り当て、追加の構造的オーバーヘッドの必要性を排除します。 -- **ランダムアクセスのサポート**:配列は任意の要素への$O(1)$時間アクセスを可能にします。 -- **キャッシュ局所性**:配列要素にアクセスするとき、コンピュータはそれらを読み込むだけでなく、周囲のデータもキャッシュし、高速キャッシュを利用して後続の操作速度を向上させます。 +## 4.1.2   配列の利点と限界 -しかし、連続空間格納は諸刃の剣で、以下の制限があります: +配列は連続したメモリ領域に格納され、要素の型も同一です。この方法には豊富な事前情報が含まれており、システムはそれらを利用してデータ構造の操作効率を最適化できます。 -- **挿入と削除の効率が低い**:配列に多くの要素が蓄積されると、要素の挿入や削除には大量の要素をシフトする必要があります。 -- **固定長**:配列の長さは初期化後に固定されます。配列を拡張するには、すべてのデータを新しい配列にコピーする必要があり、大きなコストがかかります。 -- **空間の無駄**:割り当てられた配列サイズが必要以上に大きい場合、余分な空間が無駄になります。 +- **空間効率が高い**:配列はデータに連続したメモリブロックを割り当てるため、追加の構造オーバーヘッドが不要です。 +- **ランダムアクセスをサポートする**:配列では任意の要素に $O(1)$ 時間でアクセスできます。 +- **キャッシュ局所性**:配列要素にアクセスする際、コンピュータはその要素だけでなく周囲のデータもキャッシュするため、高速キャッシュを利用して後続操作の実行速度を高められます。 + +連続領域への格納は諸刃の剣であり、次のような制約があります。 + +- **挿入と削除の効率が低い**:配列内の要素が多い場合、挿入や削除では大量の要素を移動する必要があります。 +- **長さが不変**:配列は初期化後に長さが固定され、拡張するにはすべてのデータを新しい配列へコピーする必要があり、コストが大きくなります。 +- **空間の浪費**:配列に割り当てたサイズが実際の必要量を上回る場合、余分な領域は無駄になります。 ## 4.1.3   配列の典型的な応用 -配列は基本的で広く使用されるデータ構造です。様々なアルゴリズムで頻繁に応用され、複雑なデータ構造の実装に役立ちます。 +配列は基礎的で一般的なデータ構造であり、さまざまなアルゴリズムで頻繁に使われるだけでなく、多様な複雑データ構造の実装にも利用できます。 -- **ランダムアクセス**:配列はランダムサンプリングが必要なときのデータ格納に理想的です。インデックスに基づいてランダムシーケンスを生成することで、効率的にランダムサンプリングを実現できます。 -- **ソートと検索**:配列はソートと検索アルゴリズムで最も一般的に使用されるデータ構造です。クイックソート、マージソート、二分探索などの技術は主に配列で動作します。 -- **ルックアップテーブル**:配列は迅速な要素や関係の取得のための効率的なルックアップテーブルとして機能します。例えば、文字をASCIIコードにマッピングすることは、ASCIIコード値をインデックスとして使用し、対応する要素を配列に格納することで簡単になります。 -- **機械学習**:ニューラルネットワークの領域では、配列はベクトル、行列、テンソルを含む重要な線形代数演算の実行において重要な役割を果たします。配列はニューラルネットワークプログラミングにおいて主要かつ最も広範囲に使用されるデータ構造として機能します。 -- **データ構造の実装**:配列は、スタック、キュー、ハッシュ表、ヒープ、グラフなど、様々なデータ構造を実装するための構成要素として機能します。例えば、グラフの隣接行列表現は本質的に二次元配列です。 +- **ランダムアクセス**:いくつかのサンプルをランダムに抽出したい場合、配列に格納してランダムな系列を生成し、インデックスに基づいてランダムサンプリングを行えます。 +- **ソートと探索**:配列はソートアルゴリズムと探索アルゴリズムで最もよく使われるデータ構造です。クイックソート、マージソート、二分探索などは主に配列上で行われます。 +- **ルックアップテーブル**:ある要素やその対応関係を高速に調べる必要がある場合、配列をルックアップテーブルとして使えます。たとえば文字から ASCII コードへの対応を実装したいなら、文字の ASCII コード値をインデックスとし、対応する要素を配列の対応位置に格納できます。 +- **機械学習**:ニューラルネットワークでは、ベクトル、行列、テンソル間の線形代数演算が大量に使われ、これらのデータはいずれも配列の形で構築されます。配列はニューラルネットワークプログラミングで最もよく使われるデータ構造です。 +- **データ構造の実装**:配列はスタック、キュー、ハッシュテーブル、ヒープ、グラフなどのデータ構造の実装に利用できます。たとえば、グラフの隣接行列表現は実際には 2 次元配列です。 diff --git a/ja/docs/chapter_array_and_linkedlist/index.md b/ja/docs/chapter_array_and_linkedlist/index.md index 1758a3c79..0f8bf9dd6 100644 --- a/ja/docs/chapter_array_and_linkedlist/index.md +++ b/ja/docs/chapter_array_and_linkedlist/index.md @@ -9,9 +9,9 @@ icon: material/view-list-outline !!! abstract - データ構造の世界は頑丈なレンガの壁に似ています。 + データ構造の世界は、まるで重厚なれんがの壁のようです。 - 配列では、レンガがぴったりと整列し、それぞれが次のものと継ぎ目なく隣り合って、統一された形成を作っている姿を想像してください。一方、連結リストでは、これらのレンガが自由に散らばり、それらの間を優雅に編み込む蔦に抱かれています。 + 配列のれんがは整然と並び、一つひとつがぴったりと接しています。連結リストのれんがはあちこちに分散し、それらをつなぐつるがれんがのすき間を自由に行き交います。 ## 章の内容 diff --git a/ja/docs/chapter_array_and_linkedlist/linked_list.md b/ja/docs/chapter_array_and_linkedlist/linked_list.md index 78d9b5785..747add3fb 100644 --- a/ja/docs/chapter_array_and_linkedlist/linked_list.md +++ b/ja/docs/chapter_array_and_linkedlist/linked_list.md @@ -4,23 +4,23 @@ comments: true # 4.2   連結リスト -メモリ空間は、すべてのプログラム間で共有されるリソースです。複雑なシステム環境では、使用可能なメモリがメモリ空間全体に分散している可能性があります。配列に割り当てられるメモリは連続している必要があることを理解していますが、非常に大きな配列の場合、十分な大きさの連続メモリ空間を見つけるのは困難な場合があります。ここで、連結リストの柔軟な利点が明らかになります。 +メモリ空間はすべてのプログラムに共通の資源であり、複雑なシステム実行環境では、空きメモリがメモリの各所に散在している可能性があります。配列を格納するメモリ空間は連続していなければなりませんが、配列が非常に大きい場合、メモリはそのような大きな連続領域を提供できないことがあります。このとき、連結リストの柔軟性という利点が現れます。 -連結リストは線形データ構造であり、各要素はノードオブジェクトで、ノードは「参照」を通じて相互接続されています。これらの参照は後続ノードのメモリアドレスを保持し、1つのノードから次のノードへのナビゲーションを可能にします。 +連結リスト(linked list)は線形データ構造の一種であり、各要素はノードオブジェクトです。各ノードは「参照」によって接続されます。参照には次のノードのメモリアドレスが記録されており、これによって現在のノードから次のノードへアクセスできます。 -連結リストの設計では、ノードを連続するメモリアドレスを必要とせずに、メモリ位置全体に分散配置することができます。 +連結リストの設計では、各ノードをメモリの各所に分散して格納でき、それらのメモリアドレスは連続している必要がありません。 -![連結リストの定義と格納方法](linked_list.assets/linkedlist_definition.png){ class="animation-figure" } +![連結リストの定義と格納方式](linked_list.assets/linkedlist_definition.png){ class="animation-figure" } -

図 4-5   連結リストの定義と格納方法

+

図 4-5   連結リストの定義と格納方式

-上図に示すように、連結リストの基本的な構成要素はノードオブジェクトです。各ノードは2つの主要なコンポーネントで構成されています:ノードの「値」と次のノードへの「参照」です。 +上図を見ると、連結リストの構成単位はノード(node)オブジェクトです。各ノードは 2 つのデータ、すなわちノードの「値」と次のノードを指す「参照」を含みます。 -- 連結リストの最初のノードは「ヘッドノード」、最後のノードは「テールノード」です。 -- テールノードは「null」を指し、Javaでは`null`、C++では`nullptr`、Pythonでは`None`として指定されます。 -- C、C++、Go、Rustなどのポインタをサポートする言語では、この「参照」は通常「ポインタ」として実装されます。 +- 連結リストの最初のノードを「先頭ノード」、最後のノードを「末尾ノード」と呼びます。 +- 末尾ノードが指す先は「空」であり、Java、C++、Python ではそれぞれ `null`、`nullptr`、`None` と表記します。 +- C、C++、Go、Rust などポインタをサポートする言語では、上記の「参照」は「ポインタ」に置き換えるべきです。 -以下のコードが示すように、連結リストの`ListNode`は値を保持するだけでなく、追加の参照(またはポインタ)も維持する必要があります。したがって、**連結リストは同じ量のデータを格納する場合、配列よりも多くのメモリ空間を占有します**。 +以下のコードが示すように、連結リストノード `ListNode` は値のほかに、追加で 1 つの参照(ポインタ)を保持する必要があります。そのため、同じデータ量であれば、**連結リストは配列より多くのメモリ空間を消費します**。 === "Python" @@ -28,7 +28,7 @@ comments: true class ListNode: """連結リストノードクラス""" def __init__(self, val: int): - self.val: int = val # ノード値 + self.val: int = val # ノードの値 self.next: ListNode | None = None # 次のノードへの参照 ``` @@ -37,7 +37,7 @@ comments: true ```cpp title="" /* 連結リストノード構造体 */ struct ListNode { - int val; // ノード値 + int val; // ノードの値 ListNode *next; // 次のノードへのポインタ ListNode(int x) : val(x), next(nullptr) {} // コンストラクタ }; @@ -48,7 +48,7 @@ comments: true ```java title="" /* 連結リストノードクラス */ class ListNode { - int val; // ノード値 + int val; // ノードの値 ListNode next; // 次のノードへの参照 ListNode(int x) { val = x; } // コンストラクタ } @@ -58,8 +58,8 @@ comments: true ```csharp title="" /* 連結リストノードクラス */ - class ListNode(int x) { // コンストラクタ - int val = x; // ノード値 + class ListNode(int x) { //コンストラクタ + int val = x; // ノードの値 ListNode? next; // 次のノードへの参照 } ``` @@ -69,11 +69,11 @@ comments: true ```go title="" /* 連結リストノード構造体 */ type ListNode struct { - Val int // ノード値 + Val int // ノードの値 Next *ListNode // 次のノードへのポインタ } - // NewListNode コンストラクタ、新しい連結リストを作成 + // NewListNode コンストラクタ。新しい連結リストを作成する func NewListNode(val int) *ListNode { return &ListNode{ Val: val, @@ -87,7 +87,7 @@ comments: true ```swift title="" /* 連結リストノードクラス */ class ListNode { - var val: Int // ノード値 + var val: Int // ノードの値 var next: ListNode? // 次のノードへの参照 init(x: Int) { // コンストラクタ @@ -102,7 +102,7 @@ comments: true /* 連結リストノードクラス */ class ListNode { constructor(val, next) { - this.val = (val === undefined ? 0 : val); // ノード値 + this.val = (val === undefined ? 0 : val); // ノードの値 this.next = (next === undefined ? null : next); // 次のノードへの参照 } } @@ -116,7 +116,7 @@ comments: true val: number; next: ListNode | null; constructor(val?: number, next?: ListNode | null) { - this.val = val === undefined ? 0 : val; // ノード値 + this.val = val === undefined ? 0 : val; // ノードの値 this.next = next === undefined ? null : next; // 次のノードへの参照 } } @@ -127,7 +127,7 @@ comments: true ```dart title="" /* 連結リストノードクラス */ class ListNode { - int val; // ノード値 + int val; // ノードの値 ListNode? next; // 次のノードへの参照 ListNode(this.val, [this.next]); // コンストラクタ } @@ -141,7 +141,7 @@ comments: true /* 連結リストノードクラス */ #[derive(Debug)] struct ListNode { - val: i32, // ノード値 + val: i32, // ノードの値 next: Option>>, // 次のノードへのポインタ } ``` @@ -151,7 +151,7 @@ comments: true ```c title="" /* 連結リストノード構造体 */ typedef struct ListNode { - int val; // ノード値 + int val; // ノードの値 struct ListNode *next; // 次のノードへのポインタ } ListNode; @@ -168,19 +168,39 @@ comments: true === "Kotlin" ```kotlin title="" - + /* 連結リストノードクラス */ + // コンストラクタ + class ListNode(x: Int) { + val _val: Int = x // ノードの値 + val next: ListNode? = null // 次のノードへの参照 + } ``` -## 4.2.1   連結リストの一般的な操作 +=== "Ruby" + + ```ruby title="" + # 連結リストノードクラス + class ListNode + attr_accessor :val # ノードの値 + attr_accessor :next # 次のノードへの参照 + + def initialize(val=0, next_node=nil) + @val = val + @next = next_node + end + end + ``` + +## 4.2.1   連結リストの基本操作 ### 1.   連結リストの初期化 -連結リストの構築は2段階のプロセスです:まず各ノードオブジェクトを初期化し、次にノード間の参照リンクを形成します。初期化後、ヘッドノードから`next`参照をたどってすべてのノードを順次巡回できます。 +連結リストの構築は 2 つの手順に分かれます。第 1 に各ノードオブジェクトを初期化し、第 2 にノード間の参照関係を構築します。初期化が完了したら、連結リストの先頭ノードから出発し、参照で `next` をたどってすべてのノードに順にアクセスできます。 === "Python" ```python title="linked_list.py" - # 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 + # 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 # 各ノードを初期化 n0 = ListNode(1) n1 = ListNode(3) @@ -197,7 +217,7 @@ comments: true === "C++" ```cpp title="linked_list.cpp" - /* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */ + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */ // 各ノードを初期化 ListNode* n0 = new ListNode(1); ListNode* n1 = new ListNode(3); @@ -214,7 +234,7 @@ comments: true === "Java" ```java title="linked_list.java" - /* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */ + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */ // 各ノードを初期化 ListNode n0 = new ListNode(1); ListNode n1 = new ListNode(3); @@ -231,7 +251,7 @@ comments: true === "C#" ```csharp title="linked_list.cs" - /* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */ + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */ // 各ノードを初期化 ListNode n0 = new(1); ListNode n1 = new(3); @@ -248,7 +268,7 @@ comments: true === "Go" ```go title="linked_list.go" - /* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */ + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */ // 各ノードを初期化 n0 := NewListNode(1) n1 := NewListNode(3) @@ -265,7 +285,7 @@ comments: true === "Swift" ```swift title="linked_list.swift" - /* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */ + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */ // 各ノードを初期化 let n0 = ListNode(x: 1) let n1 = ListNode(x: 3) @@ -282,7 +302,7 @@ comments: true === "JS" ```javascript title="linked_list.js" - /* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */ + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */ // 各ノードを初期化 const n0 = new ListNode(1); const n1 = new ListNode(3); @@ -299,7 +319,7 @@ comments: true === "TS" ```typescript title="linked_list.ts" - /* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */ + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */ // 各ノードを初期化 const n0 = new ListNode(1); const n1 = new ListNode(3); @@ -316,7 +336,7 @@ comments: true === "Dart" ```dart title="linked_list.dart" - /* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */ + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\ // 各ノードを初期化 ListNode n0 = ListNode(1); ListNode n1 = ListNode(3); @@ -333,7 +353,7 @@ comments: true === "Rust" ```rust title="linked_list.rs" - /* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */ + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */ // 各ノードを初期化 let n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None })); let n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None })); @@ -351,7 +371,7 @@ comments: true === "C" ```c title="linked_list.c" - /* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */ + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */ // 各ノードを初期化 ListNode* n0 = newListNode(1); ListNode* n1 = newListNode(3); @@ -368,26 +388,58 @@ comments: true === "Kotlin" ```kotlin title="linked_list.kt" - + /* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */ + // 各ノードを初期化 + val n0 = ListNode(1) + val n1 = ListNode(3) + val n2 = ListNode(2) + val n3 = ListNode(5) + val n4 = ListNode(4) + // ノード間の参照を構築 + n0.next = n1; + n1.next = n2; + n2.next = n3; + n3.next = n4; ``` -配列全体は1つの変数です。例えば、配列`nums`には`nums[0]`、`nums[1]`などの要素が含まれますが、連結リストは複数の異なるノードオブジェクトで構成されています。**通常、連結リストはそのヘッドノードで参照されます**。例えば、前のコードスニペットの連結リストは`n0`として参照されます。 +=== "Ruby" + + ```ruby title="linked_list.rb" + # 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 + # 各ノードを初期化 + n0 = ListNode.new(1) + n1 = ListNode.new(3) + n2 = ListNode.new(2) + n3 = ListNode.new(5) + n4 = ListNode.new(4) + # ノード間の参照を構築 + n0.next = n1 + n1.next = n2 + n2.next = n3 + n3.next = n4 + ``` + +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + +配列全体は 1 つの変数であり、たとえば配列 `nums` には `nums[0]` や `nums[1]` などの要素が含まれます。一方、連結リストは複数の独立したノードオブジェクトで構成されます。**通常、先頭ノードを連結リストの代名詞として扱います**。たとえば上記のコードの連結リストは `n0` と表せます。 ### 2.   ノードの挿入 -連結リストにノードを挿入するのは非常に簡単です。下図に示すように、隣接する2つのノード`n0`と`n1`の間に新しいノード`P`を挿入することを目指すとします。**これは2つのノード参照(ポインタ)を変更するだけで実現でき**、時間計算量は$O(1)$です。 +連結リストへのノード挿入は非常に簡単です。下図に示すように、隣り合う 2 つのノード `n0` と `n1` の間に新しいノード `P` を挿入したいとします。**このとき 2 つのノードの参照(ポインタ)を変更するだけでよく**、時間計算量は $O(1)$ です。 -比較すると、配列に要素を挿入する時間計算量は$O(n)$であり、大量のデータを扱う場合には効率が悪くなります。 +これに対して、配列に要素を挿入する時間計算量は $O(n)$ であり、データ量が大きい場合の効率は低くなります。 -![連結リストノード挿入の例](linked_list.assets/linkedlist_insert_node.png){ class="animation-figure" } +![連結リストへのノード挿入例](linked_list.assets/linkedlist_insert_node.png){ class="animation-figure" } -

図 4-6   連結リストノード挿入の例

+

図 4-6   連結リストへのノード挿入例

=== "Python" ```python title="linked_list.py" def insert(n0: ListNode, P: ListNode): - """連結リストのノード n0 の後にノード P を挿入""" + """連結リストでノード n0 の後ろにノード P を挿入する""" n1 = n0.next P.next = n1 n0.next = P @@ -396,7 +448,7 @@ comments: true === "C++" ```cpp title="linked_list.cpp" - /* 連結リストのノードn0の後にノードPを挿入 */ + /* 連結リストでノード n0 の後ろにノード P を挿入する */ void insert(ListNode *n0, ListNode *P) { ListNode *n1 = n0->next; P->next = n1; @@ -407,7 +459,7 @@ comments: true === "Java" ```java title="linked_list.java" - /* 連結リストでノード n0 の後にノード P を挿入 */ + /* 連結リストでノード n0 の後ろにノード P を挿入する */ void insert(ListNode n0, ListNode P) { ListNode n1 = n0.next; P.next = n1; @@ -418,78 +470,135 @@ comments: true === "C#" ```csharp title="linked_list.cs" - [class]{linked_list}-[func]{Insert} + /* 連結リストでノード n0 の後ろにノード P を挿入する */ + void Insert(ListNode n0, ListNode P) { + ListNode? n1 = n0.next; + P.next = n1; + n0.next = P; + } ``` === "Go" ```go title="linked_list.go" - [class]{}-[func]{insertNode} + /* 連結リストでノード n0 の後ろにノード P を挿入する */ + func insertNode(n0 *ListNode, P *ListNode) { + n1 := n0.Next + P.Next = n1 + n0.Next = P + } ``` === "Swift" ```swift title="linked_list.swift" - [class]{}-[func]{insert} + /* 連結リストでノード n0 の後ろにノード P を挿入する */ + func insert(n0: ListNode, P: ListNode) { + let n1 = n0.next + P.next = n1 + n0.next = P + } ``` === "JS" ```javascript title="linked_list.js" - [class]{}-[func]{insert} + /* 連結リストでノード n0 の後ろにノード P を挿入する */ + function insert(n0, P) { + const n1 = n0.next; + P.next = n1; + n0.next = P; + } ``` === "TS" ```typescript title="linked_list.ts" - [class]{}-[func]{insert} + /* 連結リストでノード n0 の後ろにノード P を挿入する */ + function insert(n0: ListNode, P: ListNode): void { + const n1 = n0.next; + P.next = n1; + n0.next = P; + } ``` === "Dart" ```dart title="linked_list.dart" - [class]{}-[func]{insert} + /* 連結リストでノード n0 の後ろにノード P を挿入する */ + void insert(ListNode n0, ListNode P) { + ListNode? n1 = n0.next; + P.next = n1; + n0.next = P; + } ``` === "Rust" ```rust title="linked_list.rs" - [class]{}-[func]{insert} + /* 連結リストでノード n0 の後ろにノード P を挿入する */ + #[allow(non_snake_case)] + pub fn insert(n0: &Rc>>, P: Rc>>) { + let n1 = n0.borrow_mut().next.take(); + P.borrow_mut().next = n1; + n0.borrow_mut().next = Some(P); + } ``` === "C" ```c title="linked_list.c" - [class]{}-[func]{insert} + /* 連結リストでノード n0 の後ろにノード P を挿入する */ + void insert(ListNode *n0, ListNode *P) { + ListNode *n1 = n0->next; + P->next = n1; + n0->next = P; + } ``` === "Kotlin" ```kotlin title="linked_list.kt" - [class]{}-[func]{insert} + /* 連結リストでノード n0 の後ろにノード P を挿入する */ + fun insert(n0: ListNode?, p: ListNode?) { + val n1 = n0?.next + p?.next = n1 + n0?.next = p + } ``` === "Ruby" ```ruby title="linked_list.rb" - [class]{}-[func]{insert} + ### 連結リストのノード n0 の後にノード _p を挿入 ### + # Ruby の `p` は組み込み関数で、`P` は定数なので、代わりに `_p` を使える + def insert(n0, _p) + n1 = n0.next + _p.next = n1 + n0.next = _p + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 3.   ノードの削除 -下図に示すように、連結リストからノードを削除することも非常に簡単で、**1つのノードの参照(ポインタ)を変更するだけです**。 +下図に示すように、連結リストでのノード削除も非常に簡単で、**1 つのノードの参照(ポインタ)を変更するだけで済みます**。 -重要な点は、ノード`P`が削除された後も`n1`を指し続けていることですが、連結リストの巡回中にはアクセスできなくなることです。これは事実上、`P`が連結リストの一部ではなくなったことを意味します。 +なお、削除操作が完了した後もノード `P` は依然として `n1` を指していますが、実際にはこの連結リストをたどっても `P` へはアクセスできません。つまり、`P` はすでにこの連結リストには属していません。 -![連結リストノードの削除](linked_list.assets/linkedlist_remove_node.png){ class="animation-figure" } +![連結リストのノード削除](linked_list.assets/linkedlist_remove_node.png){ class="animation-figure" } -

図 4-7   連結リストノードの削除

+

図 4-7   連結リストのノード削除

=== "Python" ```python title="linked_list.py" def remove(n0: ListNode): - """連結リストのノード n0 の後の最初のノードを削除""" + """連結リストでノード n0 の直後のノードを削除する""" if not n0.next: return # n0 -> P -> n1 @@ -501,7 +610,7 @@ comments: true === "C++" ```cpp title="linked_list.cpp" - /* 連結リストのノードn0の後の最初のノードを削除 */ + /* 連結リストでノード n0 の直後のノードを削除する */ void remove(ListNode *n0) { if (n0->next == nullptr) return; @@ -509,7 +618,7 @@ comments: true ListNode *P = n0->next; ListNode *n1 = P->next; n0->next = n1; - // メモリを解放 + // メモリを解放する delete P; } ``` @@ -517,7 +626,7 @@ comments: true === "Java" ```java title="linked_list.java" - /* 連結リストでノード n0 の後の最初のノードを削除 */ + /* 連結リストでノード n0 の直後のノードを削除する */ void remove(ListNode n0) { if (n0.next == null) return; @@ -531,72 +640,162 @@ comments: true === "C#" ```csharp title="linked_list.cs" - [class]{linked_list}-[func]{Remove} + /* 連結リストでノード n0 の直後のノードを削除する */ + void Remove(ListNode n0) { + if (n0.next == null) + return; + // n0 -> P -> n1 + ListNode P = n0.next; + ListNode? n1 = P.next; + n0.next = n1; + } ``` === "Go" ```go title="linked_list.go" - [class]{}-[func]{removeItem} + /* 連結リストでノード n0 の直後のノードを削除する */ + func removeItem(n0 *ListNode) { + if n0.Next == nil { + return + } + // n0 -> P -> n1 + P := n0.Next + n1 := P.Next + n0.Next = n1 + } ``` === "Swift" ```swift title="linked_list.swift" - [class]{}-[func]{remove} + /* 連結リストでノード n0 の直後のノードを削除する */ + func remove(n0: ListNode) { + if n0.next == nil { + return + } + // n0 -> P -> n1 + let P = n0.next + let n1 = P?.next + n0.next = n1 + } ``` === "JS" ```javascript title="linked_list.js" - [class]{}-[func]{remove} + /* 連結リストでノード n0 の直後のノードを削除する */ + function remove(n0) { + if (!n0.next) return; + // n0 -> P -> n1 + const P = n0.next; + const n1 = P.next; + n0.next = n1; + } ``` === "TS" ```typescript title="linked_list.ts" - [class]{}-[func]{remove} + /* 連結リストでノード n0 の直後のノードを削除する */ + function remove(n0: ListNode): void { + if (!n0.next) { + return; + } + // n0 -> P -> n1 + const P = n0.next; + const n1 = P.next; + n0.next = n1; + } ``` === "Dart" ```dart title="linked_list.dart" - [class]{}-[func]{remove} + /* 連結リストでノード n0 の直後のノードを削除する */ + void remove(ListNode n0) { + if (n0.next == null) return; + // n0 -> P -> n1 + ListNode P = n0.next!; + ListNode? n1 = P.next; + n0.next = n1; + } ``` === "Rust" ```rust title="linked_list.rs" - [class]{}-[func]{remove} + /* 連結リストでノード n0 の直後のノードを削除する */ + #[allow(non_snake_case)] + pub fn remove(n0: &Rc>>) { + // n0 -> P -> n1 + let P = n0.borrow_mut().next.take(); + if let Some(node) = P { + let n1 = node.borrow_mut().next.take(); + n0.borrow_mut().next = n1; + } + } ``` === "C" ```c title="linked_list.c" - [class]{}-[func]{removeItem} + /* 連結リストでノード n0 の直後のノードを削除する */ + // 注意: stdio.h が remove 識別子を使用している + void removeItem(ListNode *n0) { + if (!n0->next) + return; + // n0 -> P -> n1 + ListNode *P = n0->next; + ListNode *n1 = P->next; + n0->next = n1; + // メモリを解放する + free(P); + } ``` === "Kotlin" ```kotlin title="linked_list.kt" - [class]{}-[func]{remove} + /* 連結リストでノード n0 の直後のノードを削除する */ + fun remove(n0: ListNode?) { + if (n0?.next == null) + return + // n0 -> P -> n1 + val p = n0.next + val n1 = p?.next + n0.next = n1 + } ``` === "Ruby" ```ruby title="linked_list.rb" - [class]{}-[func]{remove} + ### 連結リストのノード n0 の直後のノードを削除 ### + def remove(n0) + return if n0.next.nil? + + # n0 -> remove_node -> n1 + remove_node = n0.next + n1 = remove_node.next + n0.next = n1 + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 4.   ノードへのアクセス -**連結リストでのノードへのアクセスは効率が悪いです**。前述したように、配列の任意の要素には$O(1)$時間でアクセスできます。対照的に、連結リストでは、プログラムはヘッドノードから開始して目的のノードが見つかるまで順次ノードを巡回する必要があります。つまり、連結リストの$i$番目のノードにアクセスするには、プログラムは$i - 1$個のノードを反復処理する必要があり、時間計算量は$O(n)$になります。 +**連結リストでノードにアクセスする効率は低い**です。前節で述べたように、配列では任意の要素へ $O(1)$ 時間でアクセスできます。これに対して連結リストでは、プログラムは先頭ノードから出発し、1 つずつ後ろへたどって目的のノードを見つける必要があります。つまり、連結リストの第 $i$ ノードにアクセスするには $i - 1$ 回のループが必要であり、時間計算量は $O(n)$ です。 === "Python" ```python title="linked_list.py" def access(head: ListNode, index: int) -> ListNode | None: - """連結リストのインデックス index のノードにアクセス""" + """連結リスト内で index 番目のノードにアクセス""" for _ in range(index): if not head: return None @@ -607,7 +806,7 @@ comments: true === "C++" ```cpp title="linked_list.cpp" - /* 連結リストの`index`番目のノードにアクセス */ + /* 連結リスト内で index 番目のノードにアクセス */ ListNode *access(ListNode *head, int index) { for (int i = 0; i < index; i++) { if (head == nullptr) @@ -621,7 +820,7 @@ comments: true === "Java" ```java title="linked_list.java" - /* 連結リストの `index` のノードにアクセス */ + /* 連結リスト内で index 番目のノードにアクセス */ ListNode access(ListNode head, int index) { for (int i = 0; i < index; i++) { if (head == null) @@ -635,72 +834,172 @@ comments: true === "C#" ```csharp title="linked_list.cs" - [class]{linked_list}-[func]{Access} + /* 連結リスト内で index 番目のノードにアクセス */ + ListNode? Access(ListNode? head, int index) { + for (int i = 0; i < index; i++) { + if (head == null) + return null; + head = head.next; + } + return head; + } ``` === "Go" ```go title="linked_list.go" - [class]{}-[func]{access} + /* 連結リスト内で index 番目のノードにアクセス */ + func access(head *ListNode, index int) *ListNode { + for i := 0; i < index; i++ { + if head == nil { + return nil + } + head = head.Next + } + return head + } ``` === "Swift" ```swift title="linked_list.swift" - [class]{}-[func]{access} + /* 連結リスト内で index 番目のノードにアクセス */ + func access(head: ListNode, index: Int) -> ListNode? { + var head: ListNode? = head + for _ in 0 ..< index { + if head == nil { + return nil + } + head = head?.next + } + return head + } ``` === "JS" ```javascript title="linked_list.js" - [class]{}-[func]{access} + /* 連結リスト内で index 番目のノードにアクセス */ + function access(head, index) { + for (let i = 0; i < index; i++) { + if (!head) { + return null; + } + head = head.next; + } + return head; + } ``` === "TS" ```typescript title="linked_list.ts" - [class]{}-[func]{access} + /* 連結リスト内で index 番目のノードにアクセス */ + function access(head: ListNode | null, index: number): ListNode | null { + for (let i = 0; i < index; i++) { + if (!head) { + return null; + } + head = head.next; + } + return head; + } ``` === "Dart" ```dart title="linked_list.dart" - [class]{}-[func]{access} + /* 連結リスト内で index 番目のノードにアクセス */ + ListNode? access(ListNode? head, int index) { + for (var i = 0; i < index; i++) { + if (head == null) return null; + head = head.next; + } + return head; + } ``` === "Rust" ```rust title="linked_list.rs" - [class]{}-[func]{access} + /* 連結リスト内で index 番目のノードにアクセス */ + pub fn access(head: Rc>>, index: i32) -> Option>>> { + fn dfs( + head: Option<&Rc>>>, + index: i32, + ) -> Option>>> { + if index <= 0 { + return head.cloned(); + } + + if let Some(node) = head { + dfs(node.borrow().next.as_ref(), index - 1) + } else { + None + } + } + + dfs(Some(head).as_ref(), index) + } ``` === "C" ```c title="linked_list.c" - [class]{}-[func]{access} + /* 連結リスト内で index 番目のノードにアクセス */ + ListNode *access(ListNode *head, int index) { + for (int i = 0; i < index; i++) { + if (head == NULL) + return NULL; + head = head->next; + } + return head; + } ``` === "Kotlin" ```kotlin title="linked_list.kt" - [class]{}-[func]{access} + /* 連結リスト内で index 番目のノードにアクセス */ + fun access(head: ListNode?, index: Int): ListNode? { + var h = head + for (i in 0.. + + +### 5.   ノードの探索 + +連結リストを走査し、その中から値が `target` のノードを探し、そのノードの連結リスト内でのインデックスを出力します。この処理も線形探索に属します。コードは次のとおりです。 === "Python" ```python title="linked_list.py" def find(head: ListNode, target: int) -> int: - """連結リストで値 target を持つ最初のノードを検索""" + """連結リストで値が target の最初のノードを探す""" index = 0 while head: if head.val == target: @@ -713,7 +1012,7 @@ comments: true === "C++" ```cpp title="linked_list.cpp" - /* 連結リストで値がtargetの最初のノードを検索 */ + /* 連結リストで値が target の最初のノードを探す */ int find(ListNode *head, int target) { int index = 0; while (head != nullptr) { @@ -729,7 +1028,7 @@ comments: true === "Java" ```java title="linked_list.java" - /* 連結リストで値 target を持つ最初のノードを検索 */ + /* 連結リストで値が target の最初のノードを探す */ int find(ListNode head, int target) { int index = 0; while (head != null) { @@ -745,89 +1044,205 @@ comments: true === "C#" ```csharp title="linked_list.cs" - [class]{linked_list}-[func]{Find} + /* 連結リストで値が target の最初のノードを探す */ + int Find(ListNode? head, int target) { + int index = 0; + while (head != null) { + if (head.val == target) + return index; + head = head.next; + index++; + } + return -1; + } ``` === "Go" ```go title="linked_list.go" - [class]{}-[func]{findNode} + /* 連結リストで値が target の最初のノードを探す */ + func findNode(head *ListNode, target int) int { + index := 0 + for head != nil { + if head.Val == target { + return index + } + head = head.Next + index++ + } + return -1 + } ``` === "Swift" ```swift title="linked_list.swift" - [class]{}-[func]{find} + /* 連結リストで値が target の最初のノードを探す */ + func find(head: ListNode, target: Int) -> Int { + var head: ListNode? = head + var index = 0 + while head != nil { + if head?.val == target { + return index + } + head = head?.next + index += 1 + } + return -1 + } ``` === "JS" ```javascript title="linked_list.js" - [class]{}-[func]{find} + /* 連結リストで値が target の最初のノードを探す */ + function find(head, target) { + let index = 0; + while (head !== null) { + if (head.val === target) { + return index; + } + head = head.next; + index += 1; + } + return -1; + } ``` === "TS" ```typescript title="linked_list.ts" - [class]{}-[func]{find} + /* 連結リストで値が target の最初のノードを探す */ + function find(head: ListNode | null, target: number): number { + let index = 0; + while (head !== null) { + if (head.val === target) { + return index; + } + head = head.next; + index += 1; + } + return -1; + } ``` === "Dart" ```dart title="linked_list.dart" - [class]{}-[func]{find} + /* 連結リストで値が target の最初のノードを探す */ + int find(ListNode? head, int target) { + int index = 0; + while (head != null) { + if (head.val == target) { + return index; + } + head = head.next; + index++; + } + return -1; + } ``` === "Rust" ```rust title="linked_list.rs" - [class]{}-[func]{find} + /* 連結リストで値が target の最初のノードを探す */ + pub fn find(head: Rc>>, target: T) -> i32 { + fn find(head: Option<&Rc>>>, target: T, idx: i32) -> i32 { + if let Some(node) = head { + if node.borrow().val == target { + return idx; + } + return find(node.borrow().next.as_ref(), target, idx + 1); + } else { + -1 + } + } + + find(Some(head).as_ref(), target, 0) + } ``` === "C" ```c title="linked_list.c" - [class]{}-[func]{find} + /* 連結リストで値が target の最初のノードを探す */ + int find(ListNode *head, int target) { + int index = 0; + while (head) { + if (head->val == target) + return index; + head = head->next; + index++; + } + return -1; + } ``` === "Kotlin" ```kotlin title="linked_list.kt" - [class]{}-[func]{find} + /* 連結リストで値が target の最初のノードを探す */ + fun find(head: ListNode?, target: Int): Int { + var index = 0 + var h = head + while (h != null) { + if (h._val == target) + return index + h = h.next + index++ + } + return -1 + } ``` === "Ruby" ```ruby title="linked_list.rb" - [class]{}-[func]{find} + ### 連結リストで値が target の最初のノードを探す ### + def find(head, target) + index = 0 + while head + return index if head.val == target + head = head.next + index += 1 + end + + -1 + end ``` +??? pythontutor "コードの可視化" + +
+ + ## 4.2.2   配列 vs. 連結リスト -下表は配列と連結リストの特性をまとめ、様々な操作における効率も比較しています。それぞれが対照的な格納戦略を使用するため、それぞれの特性と操作効率は明確に対比されています。 +次の表は、配列と連結リストの各種特徴と操作効率をまとめたものです。両者は互いに逆の格納戦略を採用しているため、各種性質や操作効率にも対照的な特徴が現れます。

表 4-1   配列と連結リストの効率比較

-| | 配列 | 連結リスト | -| ------------------ | ------------------------------------------------ | ----------------------- | -| 格納方式 | 連続メモリ空間 | 分散メモリ空間 | -| 容量拡張 | 固定長 | 柔軟な拡張 | -| メモリ効率 | 要素あたりのメモリ少、潜在的な空間の無駄 | 要素あたりのメモリ多 | -| 要素へのアクセス | $O(1)$ | $O(n)$ | -| 要素の追加 | $O(n)$ | $O(1)$ | -| 要素の削除 | $O(n)$ | $O(1)$ | +| | 配列 | 連結リスト | +| -------- | ------------------------------ | -------------- | +| 格納方式 | 連続したメモリ空間 | 分散したメモリ空間 | +| 容量拡張 | 長さは不変 | 柔軟に拡張可能 | +| メモリ効率 | 要素のメモリ消費は少ないが、空間を無駄にする可能性がある | 要素のメモリ消費が多い | +| 要素へのアクセス | $O(1)$ | $O(n)$ | +| 要素の追加 | $O(n)$ | $O(1)$ | +| 要素の削除 | $O(n)$ | $O(1)$ |
-## 4.2.3   連結リストの一般的な種類 +## 4.2.3   一般的な連結リストの種類 -下図に示すように、連結リストには3つの一般的な種類があります。 +下図に示すように、一般的な連結リストの種類は 3 つあります。 -- **単方向連結リスト**:これは前述した標準的な連結リストです。単方向連結リストのノードには値と次のノードへの参照が含まれます。最初のノードはヘッドノードと呼ばれ、null(`None`)を指す最後のノードはテールノードです。 -- **循環連結リスト**:これは単方向連結リストのテールノードがヘッドノードを指してループを作ることで形成されます。循環連結リストでは、任意のノードがヘッドノードとして機能できます。 -- **双方向連結リスト**:単方向連結リストとは対照的に、双方向連結リストは2つの方向で参照を維持します。各ノードには後続者(次のノード)と前任者(前のノード)の両方への参照(ポインタ)が含まれます。双方向連結リストはどちらの方向にも巡回できるより多くの柔軟性を提供しますが、より多くのメモリ空間も消費します。 +- **単方向連結リスト**:前述した通常の連結リストのことです。単方向連結リストのノードは、値と次のノードを指す参照の 2 つのデータを含みます。最初のノードを先頭ノード、最後のノードを末尾ノードと呼び、末尾ノードは空 `None` を指します。 +- **循環連結リスト**:単方向連結リストの末尾ノードを先頭ノードへ向けると(先頭と末尾をつなぐと)、循環連結リストが得られます。循環連結リストでは、任意のノードを先頭ノードとみなせます。 +- **双方向連結リスト**:単方向連結リストと比べて、双方向連結リストは 2 方向の参照を記録します。双方向連結リストのノード定義には、後続ノード(次のノード)と前駆ノード(前のノード)を指す参照(ポインタ)が含まれます。単方向連結リストより柔軟で、2 方向に連結リストを走査できますが、そのぶん多くのメモリ空間を必要とします。 === "Python" @@ -835,9 +1250,9 @@ comments: true class ListNode: """双方向連結リストノードクラス""" def __init__(self, val: int): - self.val: int = val # ノード値 + self.val: int = val # ノードの値 self.next: ListNode | None = None # 後続ノードへの参照 - self.prev: ListNode | None = None # 前任ノードへの参照 + self.prev: ListNode | None = None # 前駆ノードへの参照 ``` === "C++" @@ -845,9 +1260,9 @@ comments: true ```cpp title="" /* 双方向連結リストノード構造体 */ struct ListNode { - int val; // ノード値 + int val; // ノードの値 ListNode *next; // 後続ノードへのポインタ - ListNode *prev; // 前任ノードへのポインタ + ListNode *prev; // 前駆ノードへのポインタ ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // コンストラクタ }; ``` @@ -857,9 +1272,9 @@ comments: true ```java title="" /* 双方向連結リストノードクラス */ class ListNode { - int val; // ノード値 - ListNode next; // 次のノードへの参照 - ListNode prev; // 前任ノードへの参照 + int val; // ノードの値 + ListNode next; // 後続ノードへの参照 + ListNode prev; // 前駆ノードへの参照 ListNode(int x) { val = x; } // コンストラクタ } ``` @@ -869,9 +1284,9 @@ comments: true ```csharp title="" /* 双方向連結リストノードクラス */ class ListNode(int x) { // コンストラクタ - int val = x; // ノード値 - ListNode next; // 次のノードへの参照 - ListNode prev; // 前任ノードへの参照 + int val = x; // ノードの値 + ListNode next; // 後続ノードへの参照 + ListNode prev; // 前駆ノードへの参照 } ``` @@ -880,12 +1295,12 @@ comments: true ```go title="" /* 双方向連結リストノード構造体 */ type DoublyListNode struct { - Val int // ノード値 + Val int // ノードの値 Next *DoublyListNode // 後続ノードへのポインタ - Prev *DoublyListNode // 前任ノードへのポインタ + Prev *DoublyListNode // 前駆ノードへのポインタ } - // NewDoublyListNode 初期化 + // NewDoublyListNode の初期化 func NewDoublyListNode(val int) *DoublyListNode { return &DoublyListNode{ Val: val, @@ -900,9 +1315,9 @@ comments: true ```swift title="" /* 双方向連結リストノードクラス */ class ListNode { - var val: Int // ノード値 - var next: ListNode? // 次のノードへの参照 - var prev: ListNode? // 前任ノードへの参照 + var val: Int // ノードの値 + var next: ListNode? // 後続ノードへの参照 + var prev: ListNode? // 前駆ノードへの参照 init(x: Int) { // コンストラクタ val = x @@ -916,9 +1331,9 @@ comments: true /* 双方向連結リストノードクラス */ class ListNode { constructor(val, next, prev) { - this.val = val === undefined ? 0 : val; // ノード値 + this.val = val === undefined ? 0 : val; // ノードの値 this.next = next === undefined ? null : next; // 後続ノードへの参照 - this.prev = prev === undefined ? null : prev; // 前任ノードへの参照 + this.prev = prev === undefined ? null : prev; // 前駆ノードへの参照 } } ``` @@ -932,9 +1347,9 @@ comments: true next: ListNode | null; prev: ListNode | null; constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) { - this.val = val === undefined ? 0 : val; // ノード値 + this.val = val === undefined ? 0 : val; // ノードの値 this.next = next === undefined ? null : next; // 後続ノードへの参照 - this.prev = prev === undefined ? null : prev; // 前任ノードへの参照 + this.prev = prev === undefined ? null : prev; // 前駆ノードへの参照 } } ``` @@ -944,9 +1359,9 @@ comments: true ```dart title="" /* 双方向連結リストノードクラス */ class ListNode { - int val; // ノード値 - ListNode next; // 次のノードへの参照 - ListNode prev; // 前任ノードへの参照 + int val; // ノードの値 + ListNode? next; // 後続ノードへの参照 + ListNode? prev; // 前駆ノードへの参照 ListNode(this.val, [this.next, this.prev]); // コンストラクタ } ``` @@ -960,9 +1375,9 @@ comments: true /* 双方向連結リストノード型 */ #[derive(Debug)] struct ListNode { - val: i32, // ノード値 + val: i32, // ノードの値 next: Option>>, // 後続ノードへのポインタ - prev: Option>>, // 前任ノードへのポインタ + prev: Option>>, // 前駆ノードへのポインタ } /* コンストラクタ */ @@ -982,14 +1397,14 @@ comments: true ```c title="" /* 双方向連結リストノード構造体 */ typedef struct ListNode { - int val; // ノード値 + int val; // ノードの値 struct ListNode *next; // 後続ノードへのポインタ - struct ListNode *prev; // 前任ノードへのポインタ + struct ListNode *prev; // 前駆ノードへのポインタ } ListNode; /* コンストラクタ */ ListNode *newListNode(int val) { - ListNode *node, *next; + ListNode *node; node = (ListNode *) malloc(sizeof(ListNode)); node->val = val; node->next = NULL; @@ -1001,28 +1416,51 @@ comments: true === "Kotlin" ```kotlin title="" - + /* 双方向連結リストノードクラス */ + // コンストラクタ + class ListNode(x: Int) { + val _val: Int = x // ノードの値 + val next: ListNode? = null // 後続ノードへの参照 + val prev: ListNode? = null // 前駆ノードへの参照 + } ``` -![連結リストの一般的な種類](linked_list.assets/linkedlist_common_types.png){ class="animation-figure" } +=== "Ruby" -

図 4-8   連結リストの一般的な種類

+ ```ruby title="" + # 双方向連結リストノードクラス + class ListNode + attr_accessor :val # ノードの値 + attr_accessor :next # 後続ノードへの参照 + attr_accessor :prev # 前駆ノードへの参照 + + def initialize(val=0, next_node=nil, prev_node=nil) + @val = val + @next = next_node + @prev = prev_node + end + end + ``` + +![一般的な連結リストの種類](linked_list.assets/linkedlist_common_types.png){ class="animation-figure" } + +

図 4-8   一般的な連結リストの種類

## 4.2.4   連結リストの典型的な応用 -単方向連結リストは、スタック、キュー、ハッシュ表、グラフの実装によく使用されます。 +単方向連結リストは、スタック、キュー、ハッシュテーブル、グラフなどのデータ構造の実装によく用いられます。 -- **スタックとキュー**:単方向連結リストで、挿入と削除が同じ端で行われる場合、スタック(後入先出)のように動作します。逆に、挿入が一方の端で、削除がもう一方の端で行われる場合、キュー(先入先出)のように機能します。 -- **ハッシュ表**:連結リストは、ハッシュ衝突を解決する人気の方法である連鎖法で使用されます。ここでは、すべての衝突した要素が連結リストにグループ化されます。 -- **グラフ**:グラフ表現の標準的な方法である隣接リストは、各グラフ頂点を連結リストに関連付けます。このリストには、対応する頂点に接続された頂点を表す要素が含まれます。 +- **スタックとキュー**:挿入と削除の両方の操作を連結リストの一端で行うと、その性質は後入れ先出しとなり、スタックに対応します。挿入を連結リストの一端で行い、削除をもう一端で行うと、その性質は先入れ先出しとなり、キューに対応します。 +- **ハッシュテーブル**:連鎖アドレス法はハッシュ衝突を解決する主流の方式の 1 つであり、この方式では、衝突したすべての要素が 1 つの連結リストに格納されます。 +- **グラフ**:隣接リストはグラフを表現する一般的な方法の 1 つであり、グラフの各頂点は 1 つの連結リストに関連付けられます。連結リスト内の各要素は、その頂点に接続されたほかの頂点を表します。 -双方向連結リストは、前後の要素への高速アクセスが必要なシナリオに最適です。 +双方向連結リストは、前後の要素をすばやく見つける必要がある場面でよく用いられます。 -- **高度なデータ構造**:赤黒木やB木などの構造では、ノードの親へのアクセスが重要です。これは各ノードに親ノードへの参照を組み込むことで実現され、双方向連結リストに似ています。 -- **ブラウザ履歴**:Webブラウザでは、双方向連結リストにより、ユーザーが前進または後退ボタンをクリックしたときの訪問ページの履歴ナビゲーションが容易になります。 -- **LRUアルゴリズム**:双方向連結リストは、最近最少使用(LRU)キャッシュ削除アルゴリズムに適しており、最近最少使用データの迅速な識別と、高速なノード追加・削除を可能にします。 +- **高度なデータ構造**:たとえば赤黒木や B 木では、ノードの親ノードへアクセスする必要があります。これは、ノード内に親ノードを指す参照を保持することで実現でき、双方向連結リストに似ています。 +- **ブラウザ履歴**:Web ブラウザでユーザーが進むボタンや戻るボタンをクリックしたとき、ブラウザはユーザーが訪れた前後のページを知る必要があります。双方向連結リストの性質によって、この操作は簡単になります。 +- **LRU アルゴリズム**:キャッシュ淘汰(LRU)アルゴリズムでは、最近最も使用されていないデータをすばやく見つける必要があり、さらにノードの高速な追加と削除も必要です。そのため、双方向連結リストが非常に適しています。 -循環連結リストは、オペレーティングシステムでのリソーススケジューリングなど、周期的な操作が必要なアプリケーションに最適です。 +循環連結リストは、オペレーティングシステムのリソーススケジューリングのように、周期的な操作が必要な場面でよく用いられます。 -- **ラウンドロビンスケジューリングアルゴリズム**:オペレーティングシステムでは、ラウンドロビンスケジューリングアルゴリズムは一般的なCPUスケジューリング方法であり、プロセスのグループを循環する必要があります。各プロセスにはタイムスライスが割り当てられ、期限切れになるとCPUは次のプロセスに回転します。この循環操作は循環連結リストを使用して効率的に実現でき、すべてのプロセス間で公平かつ時分割システムを可能にします。 -- **データバッファ**:循環連結リストは、オーディオやビデオプレーヤーなどのデータバッファでも使用され、データストリームが複数のバッファブロックに分割され、シームレスな再生のために循環方式で配置されます。 +- **ラウンドロビン時間片スケジューリングアルゴリズム**:オペレーティングシステムにおいて、ラウンドロビン時間片スケジューリングは一般的な CPU スケジューリングアルゴリズムであり、一連のプロセスを循環的に処理する必要があります。各プロセスには 1 つの時間片が割り当てられ、その時間片を使い切ると、CPU は次のプロセスへ切り替わります。この循環操作は、循環連結リストで実現できます。 +- **データバッファ**:一部のデータバッファ実装でも、循環連結リストが使われることがあります。たとえば音声・動画プレーヤーでは、データストリームを複数のバッファブロックに分割して循環連結リストへ格納し、シームレス再生を実現できます。 diff --git a/ja/docs/chapter_array_and_linkedlist/list.md b/ja/docs/chapter_array_and_linkedlist/list.md index df293613d..75d2f079f 100644 --- a/ja/docs/chapter_array_and_linkedlist/list.md +++ b/ja/docs/chapter_array_and_linkedlist/list.md @@ -4,22 +4,22 @@ comments: true # 4.3   リスト -リストは、要素へのアクセス、変更、追加、削除、走査などの操作をサポートする、順序付けられた要素のコレクションを表す抽象的なデータ構造の概念であり、ユーザーが容量制限を考慮する必要がありません。リストは連結リストまたは配列に基づいて実装できます。 +リスト(list)は抽象的なデータ構造の概念であり、要素の順序付き集合を表す。要素のアクセス、更新、追加、削除、走査などの操作をサポートし、利用者は容量制限の問題を考慮する必要がない。リストは連結リストまたは配列に基づいて実装できる。 -- 連結リストは本質的にリストとして機能し、要素の追加、削除、検索、変更の操作をサポートし、サイズを動的に調整する柔軟性があります。 -- 配列もこれらの操作をサポートしますが、長さが不変であるため、長さ制限のあるリストと考えることができます。 +- 連結リストは本質的にリストと見なすことができ、要素の追加・削除・参照・更新をサポートし、柔軟に動的拡張できる。 +- 配列も要素の追加・削除・参照・更新をサポートするが、長さが不変であるため、長さ制限のあるリストとしか見なせない。 -配列を使用してリストを実装する場合、**長さの不変性によりリストの実用性が低下します**。これは、事前に格納するデータ量を予測することが困難な場合が多く、適切なリスト長を選択することが困難であるためです。長さが小さすぎると要件を満たさない可能性があり、大きすぎるとメモリ空間を無駄にする可能性があります。 +配列でリストを実装する場合、**長さが不変である性質によってリストの実用性が低下する**。これは、通常は事前にどれだけのデータを格納する必要があるかを決められず、適切なリスト長を選びにくいためである。長さが小さすぎると利用要件を満たせない可能性が高く、大きすぎるとメモリ空間の浪費を招く。 -この問題を解決するために、動的配列を使用してリストを実装できます。これは配列の利点を継承し、プログラム実行中に動的に拡張できます。 +この問題を解決するために、動的配列(dynamic array)を用いてリストを実装できる。これは配列の各種利点を引き継ぎつつ、プログラム実行中に動的な拡張を行える。 -実際、**多くのプログラミング言語の標準ライブラリは動的配列を使用してリストを実装しています**。例えば、Pythonの`list`、Javaの`ArrayList`、C++の`vector`、C#の`List`などです。以下の議論では、「リスト」と「動的配列」を同義の概念として扱います。 +実際には、**多くのプログラミング言語の標準ライブラリが提供するリストは動的配列に基づいて実装されている**。たとえば、Python の `list` 、Java の `ArrayList` 、C++ の `vector` 、C# の `List` などである。以降の議論では、「リスト」と「動的配列」を同じ概念として扱う。 -## 4.3.1   リストの一般的な操作 +## 4.3.1   リストの基本操作 ### 1.   リストの初期化 -通常、「初期値なし」と「初期値あり」の2つの初期化方法を使用します。 +通常は「初期値なし」と「初期値あり」の 2 つの初期化方法を用いる。 === "Python" @@ -35,7 +35,7 @@ comments: true ```cpp title="list.cpp" /* リストを初期化 */ - // 注意: C++では、vectorがここで説明されているnumsに相当します + // なお、C++ では vector が本稿でいう nums に相当する // 初期値なし vector nums1; // 初期値あり @@ -48,7 +48,7 @@ comments: true /* リストを初期化 */ // 初期値なし List nums1 = new ArrayList<>(); - // 初期値あり(要素型はint[]のラッパークラスInteger[]である必要があります) + // 初期値あり(配列の要素型は int[] のラッパークラスである Integer[] である必要があることに注意) Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 }; List nums = new ArrayList<>(Arrays.asList(numbers)); ``` @@ -127,133 +127,168 @@ comments: true === "C" ```c title="list.c" - // Cは組み込みの動的配列を提供していません + // C には組み込みの動的配列がない ``` === "Kotlin" ```kotlin title="list.kt" - + /* リストを初期化 */ + // 初期値なし + var nums1 = listOf() + // 初期値あり + var numbers = arrayOf(1, 3, 2, 5, 4) + var nums = numbers.toMutableList() ``` +=== "Ruby" + + ```ruby title="list.rb" + # リストを初期化 + # 初期値なし + nums1 = [] + # 初期値あり + nums = [1, 3, 2, 5, 4] + ``` + +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20%23%20%E6%97%A0%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + ### 2.   要素へのアクセス -リストは本質的に配列であるため、$O(1)$時間で要素にアクセスし更新することができ、非常に効率的です。 +リストの本質は配列であるため、要素へのアクセスと更新は $O(1)$ 時間で行え、効率が高い。 === "Python" ```python title="list.py" # 要素にアクセス - num: int = nums[1] # インデックス1の要素にアクセス + num: int = nums[1] # インデックス 1 の要素にアクセス # 要素を更新 - nums[1] = 0 # インデックス1の要素を0に更新 + nums[1] = 0 # インデックス 1 の要素を 0 に更新 ``` === "C++" ```cpp title="list.cpp" /* 要素にアクセス */ - int num = nums[1]; // インデックス1の要素にアクセス + int num = nums[1]; // インデックス 1 の要素にアクセス /* 要素を更新 */ - nums[1] = 0; // インデックス1の要素を0に更新 + nums[1] = 0; // インデックス 1 の要素を 0 に更新 ``` === "Java" ```java title="list.java" /* 要素にアクセス */ - int num = nums.get(1); // インデックス1の要素にアクセス + int num = nums.get(1); // インデックス 1 の要素にアクセス /* 要素を更新 */ - nums.set(1, 0); // インデックス1の要素を0に更新 + nums.set(1, 0); // インデックス 1 の要素を 0 に更新 ``` === "C#" ```csharp title="list.cs" /* 要素にアクセス */ - int num = nums[1]; // インデックス1の要素にアクセス + int num = nums[1]; // インデックス 1 の要素にアクセス /* 要素を更新 */ - nums[1] = 0; // インデックス1の要素を0に更新 + nums[1] = 0; // インデックス 1 の要素を 0 に更新 ``` === "Go" ```go title="list_test.go" /* 要素にアクセス */ - num := nums[1] // インデックス1の要素にアクセス + num := nums[1] // インデックス 1 の要素にアクセス /* 要素を更新 */ - nums[1] = 0 // インデックス1の要素を0に更新 + nums[1] = 0 // インデックス 1 の要素を 0 に更新 ``` === "Swift" ```swift title="list.swift" /* 要素にアクセス */ - let num = nums[1] // インデックス1の要素にアクセス + let num = nums[1] // インデックス 1 の要素にアクセス /* 要素を更新 */ - nums[1] = 0 // インデックス1の要素を0に更新 + nums[1] = 0 // インデックス 1 の要素を 0 に更新 ``` === "JS" ```javascript title="list.js" /* 要素にアクセス */ - const num = nums[1]; // インデックス1の要素にアクセス + const num = nums[1]; // インデックス 1 の要素にアクセス /* 要素を更新 */ - nums[1] = 0; // インデックス1の要素を0に更新 + nums[1] = 0; // インデックス 1 の要素を 0 に更新 ``` === "TS" ```typescript title="list.ts" /* 要素にアクセス */ - const num: number = nums[1]; // インデックス1の要素にアクセス + const num: number = nums[1]; // インデックス 1 の要素にアクセス /* 要素を更新 */ - nums[1] = 0; // インデックス1の要素を0に更新 + nums[1] = 0; // インデックス 1 の要素を 0 に更新 ``` === "Dart" ```dart title="list.dart" /* 要素にアクセス */ - int num = nums[1]; // インデックス1の要素にアクセス + int num = nums[1]; // インデックス 1 の要素にアクセス /* 要素を更新 */ - nums[1] = 0; // インデックス1の要素を0に更新 + nums[1] = 0; // インデックス 1 の要素を 0 に更新 ``` === "Rust" ```rust title="list.rs" /* 要素にアクセス */ - let num: i32 = nums[1]; // インデックス1の要素にアクセス + let num: i32 = nums[1]; // インデックス 1 の要素にアクセス /* 要素を更新 */ - nums[1] = 0; // インデックス1の要素を0に更新 + nums[1] = 0; // インデックス 1 の要素を 0 に更新 ``` === "C" ```c title="list.c" - // Cは組み込みの動的配列を提供していません + // C には組み込みの動的配列がない ``` === "Kotlin" ```kotlin title="list.kt" - + /* 要素にアクセス */ + val num = nums[1] // インデックス 1 の要素にアクセス + /* 要素を更新 */ + nums[1] = 0 // インデックス 1 の要素を 0 に更新 ``` +=== "Ruby" + + ```ruby title="list.rb" + # 要素にアクセス + num = nums[1] # インデックス 1 の要素にアクセス + # 要素を更新 + nums[1] = 0 # インデックス 1 の要素を 0 に更新 + ``` + +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%E8%AE%BF%E9%97%AE%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%0A%0A%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%E5%B0%86%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%E6%9B%B4%E6%96%B0%E4%B8%BA%200&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + ### 3.   要素の挿入と削除 -配列と比較して、リストは要素の追加と削除においてより柔軟性を提供します。リストの末尾への要素追加は$O(1)$操作ですが、リストの他の場所での要素の挿入と削除の効率は配列と同じままで、時間計算量は$O(n)$です。 +配列と比べて、リストでは要素を自由に追加・削除できる。リスト末尾への要素追加の時間計算量は $O(1)$ だが、要素の挿入と削除の効率は依然として配列と同じで、時間計算量は $O(n)$ である。 === "Python" @@ -268,11 +303,11 @@ comments: true nums.append(5) nums.append(4) - # 中間に要素を挿入 - nums.insert(3, 6) # インデックス3に数値6を挿入 + # 途中に要素を挿入 + nums.insert(3, 6) # インデックス 3 に数値 6 を挿入 # 要素を削除 - nums.pop(3) # インデックス3の要素を削除 + nums.pop(3) # インデックス 3 の要素を削除 ``` === "C++" @@ -288,11 +323,11 @@ comments: true nums.push_back(5); nums.push_back(4); - /* 中間に要素を挿入 */ - nums.insert(nums.begin() + 3, 6); // インデックス3に数値6を挿入 + /* 途中に要素を挿入 */ + nums.insert(nums.begin() + 3, 6); // インデックス 3 に数値 6 を挿入 /* 要素を削除 */ - nums.erase(nums.begin() + 3); // インデックス3の要素を削除 + nums.erase(nums.begin() + 3); // インデックス 3 の要素を削除 ``` === "Java" @@ -308,11 +343,11 @@ comments: true nums.add(5); nums.add(4); - /* 中間に要素を挿入 */ - nums.add(3, 6); // インデックス3に数値6を挿入 + /* 途中に要素を挿入 */ + nums.add(3, 6); // インデックス 3 に数値 6 を挿入 /* 要素を削除 */ - nums.remove(3); // インデックス3の要素を削除 + nums.remove(3); // インデックス 3 の要素を削除 ``` === "C#" @@ -328,11 +363,11 @@ comments: true nums.Add(5); nums.Add(4); - /* 中間に要素を挿入 */ - nums.Insert(3, 6); + /* 途中に要素を挿入 */ + nums.Insert(3, 6); // インデックス 3 に数値 6 を挿入 /* 要素を削除 */ - nums.RemoveAt(3); + nums.RemoveAt(3); // インデックス 3 の要素を削除 ``` === "Go" @@ -348,11 +383,11 @@ comments: true nums = append(nums, 5) nums = append(nums, 4) - /* 中間に要素を挿入 */ - nums = append(nums[:3], append([]int{6}, nums[3:]...)...) // インデックス3に数値6を挿入 + /* 途中に要素を挿入 */ + nums = append(nums[:3], append([]int{6}, nums[3:]...)...) // インデックス 3 に数値 6 を挿入 /* 要素を削除 */ - nums = append(nums[:3], nums[4:]...) // インデックス3の要素を削除 + nums = append(nums[:3], nums[4:]...) // インデックス 3 の要素を削除 ``` === "Swift" @@ -368,11 +403,11 @@ comments: true nums.append(5) nums.append(4) - /* 中間に要素を挿入 */ - nums.insert(6, at: 3) // インデックス3に数値6を挿入 + /* 途中に要素を挿入 */ + nums.insert(6, at: 3) // インデックス 3 に数値 6 を挿入 /* 要素を削除 */ - nums.remove(at: 3) // インデックス3の要素を削除 + nums.remove(at: 3) // インデックス 3 の要素を削除 ``` === "JS" @@ -388,11 +423,11 @@ comments: true nums.push(5); nums.push(4); - /* 中間に要素を挿入 */ - nums.splice(3, 0, 6); + /* 途中に要素を挿入 */ + nums.splice(3, 0, 6); // インデックス 3 に数値 6 を挿入 /* 要素を削除 */ - nums.splice(3, 1); + nums.splice(3, 1); // インデックス 3 の要素を削除 ``` === "TS" @@ -408,11 +443,11 @@ comments: true nums.push(5); nums.push(4); - /* 中間に要素を挿入 */ - nums.splice(3, 0, 6); + /* 途中に要素を挿入 */ + nums.splice(3, 0, 6); // インデックス 3 に数値 6 を挿入 /* 要素を削除 */ - nums.splice(3, 1); + nums.splice(3, 1); // インデックス 3 の要素を削除 ``` === "Dart" @@ -428,11 +463,11 @@ comments: true nums.add(5); nums.add(4); - /* 中間に要素を挿入 */ - nums.insert(3, 6); // インデックス3に数値6を挿入 + /* 途中に要素を挿入 */ + nums.insert(3, 6); // インデックス 3 に数値 6 を挿入 /* 要素を削除 */ - nums.removeAt(3); // インデックス3の要素を削除 + nums.removeAt(3); // インデックス 3 の要素を削除 ``` === "Rust" @@ -448,38 +483,76 @@ comments: true nums.push(5); nums.push(4); - /* 中間に要素を挿入 */ - nums.insert(3, 6); // インデックス3に数値6を挿入 + /* 途中に要素を挿入 */ + nums.insert(3, 6); // インデックス 3 に数値 6 を挿入 /* 要素を削除 */ - nums.remove(3); // インデックス3の要素を削除 + nums.remove(3); // インデックス 3 の要素を削除 ``` === "C" ```c title="list.c" - // Cは組み込みの動的配列を提供していません + // C には組み込みの動的配列がない ``` === "Kotlin" ```kotlin title="list.kt" + /* リストをクリア */ + nums.clear(); + /* 末尾に要素を追加 */ + nums.add(1); + nums.add(3); + nums.add(2); + nums.add(5); + nums.add(4); + + /* 途中に要素を挿入 */ + nums.add(3, 6); // インデックス 3 に数値 6 を挿入 + + /* 要素を削除 */ + nums.remove(3); // インデックス 3 の要素を削除 ``` -### 4.   リストの反復 +=== "Ruby" -配列と同様に、リストはインデックスを使用して反復することも、各要素を直接反復することもできます。 + ```ruby title="list.rb" + # リストをクリア + nums.clear + + # 末尾に要素を追加 + nums << 1 + nums << 3 + nums << 2 + nums << 5 + nums << 4 + + # 途中に要素を挿入 + nums.insert(3, 6) # インデックス 3 に数値 6 を挿入 + + # 要素を削除 + nums.delete_at(3) # インデックス 3 の要素を削除 + ``` + +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B8%85%E7%A9%BA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E5%B0%BE%E9%83%A8%E6%B7%BB%E5%8A%A0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.append%281%29%0A%20%20%20%20nums.append%283%29%0A%20%20%20%20nums.append%282%29%0A%20%20%20%20nums.append%285%29%0A%20%20%20%20nums.append%284%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.insert%283,%206%29%20%20%23%20%E5%9C%A8%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E6%8F%92%E5%85%A5%E6%95%B0%E5%AD%97%206%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + +### 4.   リストの走査 + +配列と同様に、リストもインデックスに基づいて走査することも、各要素を直接走査することもできる。 === "Python" ```python title="list.py" - # インデックスでリストを反復 + # インデックスでリストを走査 count = 0 for i in range(len(nums)): count += nums[i] - # リスト要素を直接反復 + # リストの要素を直接走査 for num in nums: count += num ``` @@ -487,13 +560,13 @@ comments: true === "C++" ```cpp title="list.cpp" - /* インデックスでリストを反復 */ + /* インデックスでリストを走査 */ int count = 0; for (int i = 0; i < nums.size(); i++) { count += nums[i]; } - /* リスト要素を直接反復 */ + /* リストの要素を直接走査 */ count = 0; for (int num : nums) { count += num; @@ -503,13 +576,13 @@ comments: true === "Java" ```java title="list.java" - /* インデックスでリストを反復 */ + /* インデックスでリストを走査 */ int count = 0; for (int i = 0; i < nums.size(); i++) { count += nums.get(i); } - /* リスト要素を直接反復 */ + /* リストの要素を直接走査 */ for (int num : nums) { count += num; } @@ -518,13 +591,13 @@ comments: true === "C#" ```csharp title="list.cs" - /* インデックスでリストを反復 */ + /* インデックスでリストを走査 */ int count = 0; for (int i = 0; i < nums.Count; i++) { count += nums[i]; } - /* リスト要素を直接反復 */ + /* リストの要素を直接走査 */ count = 0; foreach (int num in nums) { count += num; @@ -534,13 +607,13 @@ comments: true === "Go" ```go title="list_test.go" - /* インデックスでリストを反復 */ + /* インデックスでリストを走査 */ count := 0 for i := 0; i < len(nums); i++ { count += nums[i] } - /* リスト要素を直接反復 */ + /* リストの要素を直接走査 */ count = 0 for _, num := range nums { count += num @@ -550,13 +623,13 @@ comments: true === "Swift" ```swift title="list.swift" - /* インデックスでリストを反復 */ + /* インデックスでリストを走査 */ var count = 0 for i in nums.indices { count += nums[i] } - /* リスト要素を直接反復 */ + /* リストの要素を直接走査 */ count = 0 for num in nums { count += num @@ -566,13 +639,13 @@ comments: true === "JS" ```javascript title="list.js" - /* インデックスでリストを反復 */ + /* インデックスでリストを走査 */ let count = 0; for (let i = 0; i < nums.length; i++) { count += nums[i]; } - /* リスト要素を直接反復 */ + /* リストの要素を直接走査 */ count = 0; for (const num of nums) { count += num; @@ -582,13 +655,13 @@ comments: true === "TS" ```typescript title="list.ts" - /* インデックスでリストを反復 */ + /* インデックスでリストを走査 */ let count = 0; for (let i = 0; i < nums.length; i++) { count += nums[i]; } - /* リスト要素を直接反復 */ + /* リストの要素を直接走査 */ count = 0; for (const num of nums) { count += num; @@ -598,13 +671,13 @@ comments: true === "Dart" ```dart title="list.dart" - /* インデックスでリストを反復 */ + /* インデックスでリストを走査 */ int count = 0; for (var i = 0; i < nums.length; i++) { count += nums[i]; } - /* リスト要素を直接反復 */ + /* リストの要素を直接走査 */ count = 0; for (var num in nums) { count += num; @@ -614,13 +687,13 @@ comments: true === "Rust" ```rust title="list.rs" - // インデックスでリストを反復 + // インデックスでリストを走査 let mut _count = 0; for i in 0..nums.len() { _count += nums[i]; } - // リスト要素を直接反復 + // リストの要素を直接走査 _count = 0; for num in &nums { _count += num; @@ -630,96 +703,125 @@ comments: true === "C" ```c title="list.c" - // Cは組み込みの動的配列を提供していません + // C には組み込みの動的配列がない ``` === "Kotlin" ```kotlin title="list.kt" + /* インデックスでリストを走査 */ + var count = 0 + for (i in nums.indices) { + count += nums[i] + } + /* リストの要素を直接走査 */ + for (num in nums) { + count += num + } ``` +=== "Ruby" + + ```ruby title="list.rb" + # インデックスでリストを走査 + count = 0 + for i in 0...nums.length + count += nums[i] + end + + # リストの要素を直接走査 + count = 0 + for num in nums + count += num + end + ``` + +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%80%9A%E8%BF%87%E7%B4%A2%E5%BC%95%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%0A%20%20%20%20%23%20%E7%9B%B4%E6%8E%A5%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + ### 5.   リストの連結 -新しいリスト`nums1`が与えられたとき、それを元のリストの末尾に追加できます。 +新しいリスト `nums1` が与えられたとき、それを元のリストの末尾に連結できる。 === "Python" ```python title="list.py" - # 2つのリストを連結 + # 2 つのリストを連結 nums1: list[int] = [6, 8, 7, 10, 9] - nums += nums1 # nums1をnumsの末尾に連結 + nums += nums1 # リスト nums1 を nums の後ろに連結 ``` === "C++" ```cpp title="list.cpp" - /* 2つのリストを連結 */ + /* 2 つのリストを連結 */ vector nums1 = { 6, 8, 7, 10, 9 }; - // nums1をnumsの末尾に連結 + // リスト nums1 を nums の後ろに連結 nums.insert(nums.end(), nums1.begin(), nums1.end()); ``` === "Java" ```java title="list.java" - /* 2つのリストを連結 */ + /* 2 つのリストを連結 */ List nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 })); - nums.addAll(nums1); // nums1をnumsの末尾に連結 + nums.addAll(nums1); // リスト nums1 を nums の後ろに連結 ``` === "C#" ```csharp title="list.cs" - /* 2つのリストを連結 */ + /* 2 つのリストを連結 */ List nums1 = [6, 8, 7, 10, 9]; - nums.AddRange(nums1); // nums1をnumsの末尾に連結 + nums.AddRange(nums1); // リスト nums1 を nums の後ろに連結 ``` === "Go" ```go title="list_test.go" - /* 2つのリストを連結 */ + /* 2 つのリストを連結 */ nums1 := []int{6, 8, 7, 10, 9} - nums = append(nums, nums1...) // nums1をnumsの末尾に連結 + nums = append(nums, nums1...) // リスト nums1 を nums の後ろに連結 ``` === "Swift" ```swift title="list.swift" - /* 2つのリストを連結 */ + /* 2 つのリストを連結 */ let nums1 = [6, 8, 7, 10, 9] - nums.append(contentsOf: nums1) // nums1をnumsの末尾に連結 + nums.append(contentsOf: nums1) // リスト nums1 を nums の後ろに連結 ``` === "JS" ```javascript title="list.js" - /* 2つのリストを連結 */ + /* 2 つのリストを連結 */ const nums1 = [6, 8, 7, 10, 9]; - nums.push(...nums1); // nums1をnumsの末尾に連結 + nums.push(...nums1); // リスト nums1 を nums の後ろに連結 ``` === "TS" ```typescript title="list.ts" - /* 2つのリストを連結 */ + /* 2 つのリストを連結 */ const nums1: number[] = [6, 8, 7, 10, 9]; - nums.push(...nums1); // nums1をnumsの末尾に連結 + nums.push(...nums1); // リスト nums1 を nums の後ろに連結 ``` === "Dart" ```dart title="list.dart" - /* 2つのリストを連結 */ + /* 2 つのリストを連結 */ List nums1 = [6, 8, 7, 10, 9]; - nums.addAll(nums1); // nums1をnumsの末尾に連結 + nums.addAll(nums1); // リスト nums1 を nums の後ろに連結 ``` === "Rust" ```rust title="list.rs" - /* 2つのリストを連結 */ + /* 2 つのリストを連結 */ let nums1: Vec = vec![6, 8, 7, 10, 9]; nums.extend(nums1); ``` @@ -727,110 +829,136 @@ comments: true === "C" ```c title="list.c" - // Cは組み込みの動的配列を提供していません + // C には組み込みの動的配列がない ``` === "Kotlin" ```kotlin title="list.kt" - + /* 2 つのリストを連結 */ + val nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList() + nums.addAll(nums1) // リスト nums1 を nums の後ろに連結 ``` -### 6.   リストのソート +=== "Ruby" -リストがソートされると、「二分探索」や「双ポインタ」アルゴリズムなど、配列関連のアルゴリズム問題でよく使用されるアルゴリズムを使用できます。 + ```ruby title="list.rb" + # 2 つのリストを連結 + nums1 = [6, 8, 7, 10, 9] + nums += nums1 + ``` + +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8B%BC%E6%8E%A5%E4%B8%A4%E4%B8%AA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums1%20%3D%20%5B6,%208,%207,%2010,%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%20nums1%20%E6%8B%BC%E6%8E%A5%E5%88%B0%20nums%20%E4%B9%8B%E5%90%8E&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + +### 6.   リストをソート + +リストのソートが完了すると、配列系アルゴリズム問題でよく問われる「二分探索」や「両ポインタ」アルゴリズムを使えるようになる。 === "Python" ```python title="list.py" # リストをソート - nums.sort() # ソート後、リスト要素は昇順になります + nums.sort() # ソート後、リスト要素は小さい順に並ぶ ``` === "C++" ```cpp title="list.cpp" /* リストをソート */ - sort(nums.begin(), nums.end()); // ソート後、リスト要素は昇順になります + sort(nums.begin(), nums.end()); // ソート後、リスト要素は小さい順に並ぶ ``` === "Java" ```java title="list.java" /* リストをソート */ - Collections.sort(nums); // ソート後、リスト要素は昇順になります + Collections.sort(nums); // ソート後、リスト要素は小さい順に並ぶ ``` === "C#" ```csharp title="list.cs" /* リストをソート */ - nums.Sort(); // ソート後、リスト要素は昇順になります + nums.Sort(); // ソート後、リスト要素は小さい順に並ぶ ``` === "Go" ```go title="list_test.go" /* リストをソート */ - sort.Ints(nums) // ソート後、リスト要素は昇順になります + sort.Ints(nums) // ソート後、リスト要素は小さい順に並ぶ ``` === "Swift" ```swift title="list.swift" /* リストをソート */ - nums.sort() // ソート後、リスト要素は昇順になります + nums.sort() // ソート後、リスト要素は小さい順に並ぶ ``` === "JS" ```javascript title="list.js" /* リストをソート */ - nums.sort((a, b) => a - b); // ソート後、リスト要素は昇順になります + nums.sort((a, b) => a - b); // ソート後、リスト要素は小さい順に並ぶ ``` === "TS" ```typescript title="list.ts" /* リストをソート */ - nums.sort((a, b) => a - b); // ソート後、リスト要素は昇順になります + nums.sort((a, b) => a - b); // ソート後、リスト要素は小さい順に並ぶ ``` === "Dart" ```dart title="list.dart" /* リストをソート */ - nums.sort(); // ソート後、リスト要素は昇順になります + nums.sort(); // ソート後、リスト要素は小さい順に並ぶ ``` === "Rust" ```rust title="list.rs" /* リストをソート */ - nums.sort(); // ソート後、リスト要素は昇順になります + nums.sort(); // ソート後、リスト要素は小さい順に並ぶ ``` === "C" ```c title="list.c" - // Cは組み込みの動的配列を提供していません + // C には組み込みの動的配列がない ``` === "Kotlin" ```kotlin title="list.kt" - + /* リストをソート */ + nums.sort() // ソート後、リスト要素は小さい順に並ぶ ``` +=== "Ruby" + + ```ruby title="list.rb" + # リストをソート + nums = nums.sort { |a, b| a <=> b } # ソート後、リスト要素は小さい順に並ぶ + ``` + +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8E%92%E5%BA%8F%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.sort%28%29%20%20%23%20%E6%8E%92%E5%BA%8F%E5%90%8E%EF%BC%8C%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E4%BB%8E%E5%B0%8F%E5%88%B0%E5%A4%A7%E6%8E%92%E5%88%97&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + ## 4.3.2   リストの実装 -多くのプログラミング言語には、Java、C++、Pythonなどを含む組み込みリストが付属しています。それらの実装は、初期容量や拡張係数などの様々なパラメータを慎重に考慮した設定で、複雑になりがちです。興味のある読者は、さらなる学習のためにソースコードを調べることができます。 +多くのプログラミング言語にはリストが組み込まれており、たとえば Java、C++、Python などがある。それらの実装は比較的複雑で、初期容量や拡張倍率など各種パラメータの設定もよく考えられている。興味があればソースコードを参照して学べる。 -リストがどのように動作するかの理解を深めるために、3つの重要な設計側面に焦点を当てて、簡略化されたリストの実装を試みます: +リストの動作原理への理解を深めるため、ここでは簡易版のリストを実装し、以下の 3 つの設計ポイントを含める。 -- **初期容量**:配列に合理的な初期容量を選択します。この例では、初期容量として10を選択します。 -- **サイズ記録**:リスト内の現在の要素数を記録する変数`size`を宣言し、要素の挿入と削除でリアルタイムに更新します。この変数により、リストの末尾を特定し、拡張が必要かどうかを判断できます。 -- **拡張メカニズム**:要素挿入時にリストが満杯に達した場合、拡張プロセスが必要です。これには拡張係数に基づいてより大きな配列を作成し、現在の配列からすべての要素を新しい配列に転送することが含まれます。この例では、拡張のたびに配列サイズを2倍にすることを規定します。 +- **初期容量**:妥当な配列の初期容量を選ぶ。この例では 10 を初期容量として選ぶ。 +- **要素数の記録**:`size` という変数を宣言して、現在のリスト要素数を記録し、要素の挿入と削除に応じてリアルタイムに更新する。この変数により、リスト末尾の位置を特定し、拡張が必要かどうかを判断できる。 +- **拡張機構**:要素を挿入する時点でリスト容量がいっぱいなら、拡張が必要になる。まず拡張倍率に応じてより大きな配列を作成し、次に現在の配列の全要素を順に新しい配列へ移す。この例では、配列を毎回以前の 2 倍に拡張する。 === "Python" @@ -840,35 +968,35 @@ comments: true def __init__(self): """コンストラクタ""" - self._capacity: int = 10 # リストの容量 + self._capacity: int = 10 # リスト容量 self._arr: list[int] = [0] * self._capacity # 配列(リスト要素を格納) self._size: int = 0 # リストの長さ(現在の要素数) - self._extend_ratio: int = 2 # 各リスト拡張の倍数 + self._extend_ratio: int = 2 # リスト拡張時の増加倍率 def size(self) -> int: - """リストの長さ(現在の要素数)を取得""" + """リストの長さを取得(現在の要素数)""" return self._size def capacity(self) -> int: - """リストの容量を取得""" + """リスト容量を取得する""" return self._capacity def get(self, index: int) -> int: """要素にアクセス""" - # インデックスが範囲外の場合、以下のように例外をスロー + # インデックスが範囲外なら例外を送出する。以下同様 if index < 0 or index >= self._size: - raise IndexError("Index out of bounds") + raise IndexError("インデックスが範囲外です") return self._arr[index] def set(self, num: int, index: int): """要素を更新""" if index < 0 or index >= self._size: - raise IndexError("Index out of bounds") + raise IndexError("インデックスが範囲外です") self._arr[index] = num def add(self, num: int): """末尾に要素を追加""" - # 要素数が容量を超える場合、拡張メカニズムをトリガー + # 要素数が容量を超えると、拡張機構が発動する if self.size() == self.capacity(): self.extend_capacity() self._arr[self._size] = num @@ -877,11 +1005,11 @@ comments: true def insert(self, num: int, index: int): """中間に要素を挿入""" if index < 0 or index >= self._size: - raise IndexError("Index out of bounds") - # 要素数が容量を超える場合、拡張メカニズムをトリガー + raise IndexError("インデックスが範囲外です") + # 要素数が容量を超えると、拡張機構が発動する if self._size == self.capacity(): self.extend_capacity() - # インデックス index より後のすべての要素を1つ後ろに移動 + # index 以降の要素をすべて 1 つ後ろへずらす for j in range(self._size - 1, index - 1, -1): self._arr[j + 1] = self._arr[j] self._arr[index] = num @@ -891,9 +1019,9 @@ comments: true def remove(self, index: int) -> int: """要素を削除""" if index < 0 or index >= self._size: - raise IndexError("Index out of bounds") + raise IndexError("インデックスが範囲外です") num = self._arr[index] - # インデックス index より後のすべての要素を1つ前に移動 + # インデックス index より後の要素をすべて 1 つ前に移動する for j in range(index, self._size - 1): self._arr[j] = self._arr[j + 1] # 要素数を更新 @@ -902,14 +1030,14 @@ comments: true return num def extend_capacity(self): - """リストを拡張""" - # 元の配列の _extend_ratio 倍の長さの新しい配列を作成し、元の配列を新しい配列にコピー + """リストの拡張""" + # 元の配列の `_extend_ratio` 倍の長さを持つ新しい配列を作成し、元の配列を新しい配列にコピーする self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1) # リストの容量を更新 self._capacity = len(self._arr) def to_array(self) -> list[int]: - """有効な長さのリストを返す""" + """有効長のリストを返す""" return self._arr[: self._size] ``` @@ -920,9 +1048,9 @@ comments: true class MyList { private: int *arr; // 配列(リスト要素を格納) - int arrCapacity = 10; // リストの容量 + int arrCapacity = 10; // リスト容量 int arrSize = 0; // リストの長さ(現在の要素数) - int extendRatio = 2; // リスト拡張時の倍率 + int extendRatio = 2; // リスト拡張時の増加倍率 public: /* コンストラクタ */ @@ -930,39 +1058,39 @@ comments: true arr = new int[arrCapacity]; } - /* デストラクタ */ + /* デストラクタメソッド */ ~MyList() { delete[] arr; } - /* リストの長さを取得(現在の要素数)*/ + /* リストの長さを取得(現在の要素数) */ int size() { return arrSize; } - /* リストの容量を取得 */ + /* リスト容量を取得する */ int capacity() { return arrCapacity; } /* 要素にアクセス */ int get(int index) { - // インデックスが範囲外の場合、例外をスロー(以下同様) + // インデックスが範囲外なら例外を送出する。以下同様 if (index < 0 || index >= size()) - throw out_of_range("Index out of bounds"); + throw out_of_range("インデックスが範囲外"); return arr[index]; } /* 要素を更新 */ void set(int index, int num) { if (index < 0 || index >= size()) - throw out_of_range("Index out of bounds"); + throw out_of_range("インデックスが範囲外"); arr[index] = num; } /* 末尾に要素を追加 */ void add(int num) { - // 要素数が容量を超えた場合、拡張メカニズムをトリガー + // 要素数が容量を超えると、拡張機構が発動する if (size() == capacity()) extendCapacity(); arr[size()] = num; @@ -973,11 +1101,11 @@ comments: true /* 中間に要素を挿入 */ void insert(int index, int num) { if (index < 0 || index >= size()) - throw out_of_range("Index out of bounds"); - // 要素数が容量を超えた場合、拡張メカニズムをトリガー + throw out_of_range("インデックスが範囲外"); + // 要素数が容量を超えると、拡張機構が発動する if (size() == capacity()) extendCapacity(); - // `index`より後のすべての要素を1つ後ろに移動 + // index 以降の要素をすべて 1 つ後ろへずらす for (int j = size() - 1; j >= index; j--) { arr[j + 1] = arr[j]; } @@ -989,36 +1117,36 @@ comments: true /* 要素を削除 */ int remove(int index) { if (index < 0 || index >= size()) - throw out_of_range("Index out of bounds"); + throw out_of_range("インデックスが範囲外"); int num = arr[index]; - // `index`より後のすべての要素を1つ前に移動 + // インデックス index より後の要素をすべて 1 つ前に移動する for (int j = index; j < size() - 1; j++) { arr[j] = arr[j + 1]; } // 要素数を更新 arrSize--; - // 削除された要素を返却 + // 削除された要素を返す return num; } - /* リストを拡張 */ + /* リストの拡張 */ void extendCapacity() { - // 元の配列のextendRatio倍の長さで新しい配列を作成 + // 元の配列の `extendRatio` 倍の長さを持つ新しい配列を作成する int newCapacity = capacity() * extendRatio; int *tmp = arr; arr = new int[newCapacity]; - // 元の配列のすべての要素を新しい配列にコピー + // 元の配列の全要素を新しい配列にコピー for (int i = 0; i < size(); i++) { arr[i] = tmp[i]; } - // メモリを解放 + // メモリを解放する delete[] tmp; arrCapacity = newCapacity; } - /* リストをVectorに変換して印刷用に使用 */ + /* 出力用にリストを Vector に変換 */ vector toVector() { - // 有効な長さ範囲内の要素のみを変換 + // 有効長の範囲内のリスト要素のみを変換 vector vec(size()); for (int i = 0; i < size(); i++) { vec[i] = arr[i]; @@ -1035,33 +1163,33 @@ comments: true class MyList { private int[] arr; // 配列(リスト要素を格納) private int capacity = 10; // リスト容量 - private int size = 0; // リスト長(現在の要素数) - private int extendRatio = 2; // リストの各拡張倍率 + private int size = 0; // リストの長さ(現在の要素数) + private int extendRatio = 2; // リスト拡張時の増加倍率 /* コンストラクタ */ public MyList() { arr = new int[capacity]; } - /* リスト長を取得(現在の要素数) */ + /* リストの長さを取得(現在の要素数) */ public int size() { return size; } - /* リスト容量を取得 */ + /* リスト容量を取得する */ public int capacity() { return capacity; } - /* 要素へのアクセス */ + /* 要素にアクセス */ public int get(int index) { - // インデックスが範囲外の場合、以下のように例外をスロー + // インデックスが範囲外なら例外を送出する。以下同様 if (index < 0 || index >= size) throw new IndexOutOfBoundsException("インデックスが範囲外です"); return arr[index]; } - /* 要素の更新 */ + /* 要素を更新 */ public void set(int index, int num) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException("インデックスが範囲外です"); @@ -1070,7 +1198,7 @@ comments: true /* 末尾に要素を追加 */ public void add(int num) { - // 要素数が容量を超える場合、拡張メカニズムを実行 + // 要素数が容量を超えると、拡張機構が発動する if (size == capacity()) extendCapacity(); arr[size] = num; @@ -1082,10 +1210,10 @@ comments: true public void insert(int index, int num) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException("インデックスが範囲外です"); - // 要素数が容量を超える場合、拡張メカニズムを実行 + // 要素数が容量を超えると、拡張機構が発動する if (size == capacity()) extendCapacity(); - // `index` より後のすべての要素を1つ後ろに移動 + // index 以降の要素をすべて 1 つ後ろへずらす for (int j = size - 1; j >= index; j--) { arr[j + 1] = arr[j]; } @@ -1094,12 +1222,12 @@ comments: true size++; } - /* 要素の削除 */ + /* 要素を削除 */ public int remove(int index) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException("インデックスが範囲外です"); int num = arr[index]; - // `index` より後のすべての要素を1つ前に移動 + // インデックス index より後の要素をすべて 1 つ前に移動する for (int j = index; j < size - 1; j++) { arr[j] = arr[j + 1]; } @@ -1109,18 +1237,18 @@ comments: true return num; } - /* リストを拡張 */ + /* リストの拡張 */ public void extendCapacity() { - // 元の配列の長さを extendRatio 倍した新しい配列を作成し、元の配列を新しい配列にコピー + // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする arr = Arrays.copyOf(arr, capacity() * extendRatio); - // リスト容量を更新 + // リストの容量を更新 capacity = arr.length; } - /* リストを配列に変換 */ + /* リストを配列に変換する */ public int[] toArray() { int size = size(); - // 有効な長さ範囲内の要素のみを変換 + // 有効長の範囲内のリスト要素のみを変換 int[] arr = new int[size]; for (int i = 0; i < size; i++) { arr[i] = get(i); @@ -1133,59 +1261,1035 @@ comments: true === "C#" ```csharp title="my_list.cs" - [class]{MyList}-[func]{} + /* リストクラス */ + class MyList { + private int[] arr; // 配列(リスト要素を格納) + private int arrCapacity = 10; // リスト容量 + private int arrSize = 0; // リストの長さ(現在の要素数) + private readonly int extendRatio = 2; // リスト拡張時の増加倍率 + + /* コンストラクタ */ + public MyList() { + arr = new int[arrCapacity]; + } + + /* リストの長さを取得(現在の要素数) */ + public int Size() { + return arrSize; + } + + /* リスト容量を取得する */ + public int Capacity() { + return arrCapacity; + } + + /* 要素にアクセス */ + public int Get(int index) { + // インデックスが範囲外なら例外を送出する。以下同様 + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("インデックスが範囲外です"); + return arr[index]; + } + + /* 要素を更新 */ + public void Set(int index, int num) { + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("インデックスが範囲外です"); + arr[index] = num; + } + + /* 末尾に要素を追加 */ + public void Add(int num) { + // 要素数が容量を超えると、拡張機構が発動する + if (arrSize == arrCapacity) + ExtendCapacity(); + arr[arrSize] = num; + // 要素数を更新 + arrSize++; + } + + /* 中間に要素を挿入 */ + public void Insert(int index, int num) { + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("インデックスが範囲外です"); + // 要素数が容量を超えると、拡張機構が発動する + if (arrSize == arrCapacity) + ExtendCapacity(); + // index 以降の要素をすべて 1 つ後ろへずらす + for (int j = arrSize - 1; j >= index; j--) { + arr[j + 1] = arr[j]; + } + arr[index] = num; + // 要素数を更新 + arrSize++; + } + + /* 要素を削除 */ + public int Remove(int index) { + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("インデックスが範囲外です"); + int num = arr[index]; + // インデックス index より後の要素をすべて 1 つ前に移動する + for (int j = index; j < arrSize - 1; j++) { + arr[j] = arr[j + 1]; + } + // 要素数を更新 + arrSize--; + // 削除された要素を返す + return num; + } + + /* リストの拡張 */ + public void ExtendCapacity() { + // `arrCapacity * extendRatio` の長さを持つ配列を新規作成し、元の配列を新しい配列にコピーする + Array.Resize(ref arr, arrCapacity * extendRatio); + // リストの容量を更新 + arrCapacity = arr.Length; + } + + /* リストを配列に変換する */ + public int[] ToArray() { + // 有効長の範囲内のリスト要素のみを変換 + int[] arr = new int[arrSize]; + for (int i = 0; i < arrSize; i++) { + arr[i] = Get(i); + } + return arr; + } + } ``` === "Go" ```go title="my_list.go" - [class]{myList}-[func]{} + /* リストクラス */ + type myList struct { + arrCapacity int + arr []int + arrSize int + extendRatio int + } + + /* コンストラクタ */ + func newMyList() *myList { + return &myList{ + arrCapacity: 10, // リスト容量 + arr: make([]int, 10), // 配列(リスト要素を格納) + arrSize: 0, // リストの長さ(現在の要素数) + extendRatio: 2, // リスト拡張時の増加倍率 + } + } + + /* リストの長さを取得(現在の要素数) */ + func (l *myList) size() int { + return l.arrSize + } + + /* リスト容量を取得する */ + func (l *myList) capacity() int { + return l.arrCapacity + } + + /* 要素にアクセス */ + func (l *myList) get(index int) int { + // インデックスが範囲外なら例外を送出する。以下同様 + if index < 0 || index >= l.arrSize { + panic("インデックスが範囲外です") + } + return l.arr[index] + } + + /* 要素を更新 */ + func (l *myList) set(num, index int) { + if index < 0 || index >= l.arrSize { + panic("インデックスが範囲外です") + } + l.arr[index] = num + } + + /* 末尾に要素を追加 */ + func (l *myList) add(num int) { + // 要素数が容量を超えると、拡張機構が発動する + if l.arrSize == l.arrCapacity { + l.extendCapacity() + } + l.arr[l.arrSize] = num + // 要素数を更新 + l.arrSize++ + } + + /* 中間に要素を挿入 */ + func (l *myList) insert(num, index int) { + if index < 0 || index >= l.arrSize { + panic("インデックスが範囲外です") + } + // 要素数が容量を超えると、拡張機構が発動する + if l.arrSize == l.arrCapacity { + l.extendCapacity() + } + // index 以降の要素をすべて 1 つ後ろへずらす + for j := l.arrSize - 1; j >= index; j-- { + l.arr[j+1] = l.arr[j] + } + l.arr[index] = num + // 要素数を更新 + l.arrSize++ + } + + /* 要素を削除 */ + func (l *myList) remove(index int) int { + if index < 0 || index >= l.arrSize { + panic("インデックスが範囲外です") + } + num := l.arr[index] + // インデックス index より後の要素をすべて 1 つ前に移動する + for j := index; j < l.arrSize-1; j++ { + l.arr[j] = l.arr[j+1] + } + // 要素数を更新 + l.arrSize-- + // 削除された要素を返す + return num + } + + /* リストの拡張 */ + func (l *myList) extendCapacity() { + // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする + l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...) + // リストの容量を更新 + l.arrCapacity = len(l.arr) + } + + /* 有効長のリストを返す */ + func (l *myList) toArray() []int { + // 有効長の範囲内のリスト要素のみを変換 + return l.arr[:l.arrSize] + } ``` === "Swift" ```swift title="my_list.swift" - [class]{MyList}-[func]{} + /* リストクラス */ + class MyList { + private var arr: [Int] // 配列(リスト要素を格納) + private var _capacity: Int // リスト容量 + private var _size: Int // リストの長さ(現在の要素数) + private let extendRatio: Int // リスト拡張時の増加倍率 + + /* コンストラクタ */ + init() { + _capacity = 10 + _size = 0 + extendRatio = 2 + arr = Array(repeating: 0, count: _capacity) + } + + /* リストの長さを取得(現在の要素数) */ + func size() -> Int { + _size + } + + /* リスト容量を取得する */ + func capacity() -> Int { + _capacity + } + + /* 要素にアクセス */ + func get(index: Int) -> Int { + // インデックスが範囲外ならエラーを投げる。以下同様 + if index < 0 || index >= size() { + fatalError("インデックスが範囲外") + } + return arr[index] + } + + /* 要素を更新 */ + func set(index: Int, num: Int) { + if index < 0 || index >= size() { + fatalError("インデックスが範囲外") + } + arr[index] = num + } + + /* 末尾に要素を追加 */ + func add(num: Int) { + // 要素数が容量を超えると、拡張機構が発動する + if size() == capacity() { + extendCapacity() + } + arr[size()] = num + // 要素数を更新 + _size += 1 + } + + /* 中間に要素を挿入 */ + func insert(index: Int, num: Int) { + if index < 0 || index >= size() { + fatalError("インデックスが範囲外") + } + // 要素数が容量を超えると、拡張機構が発動する + if size() == capacity() { + extendCapacity() + } + // index 以降の要素をすべて 1 つ後ろへずらす + for j in (index ..< size()).reversed() { + arr[j + 1] = arr[j] + } + arr[index] = num + // 要素数を更新 + _size += 1 + } + + /* 要素を削除 */ + @discardableResult + func remove(index: Int) -> Int { + if index < 0 || index >= size() { + fatalError("インデックスが範囲外") + } + let num = arr[index] + // インデックス index より後の要素をすべて 1 つ前に移動する + for j in index ..< (size() - 1) { + arr[j] = arr[j + 1] + } + // 要素数を更新 + _size -= 1 + // 削除された要素を返す + return num + } + + /* リストの拡張 */ + func extendCapacity() { + // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする + arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1)) + // リストの容量を更新 + _capacity = arr.count + } + + /* リストを配列に変換する */ + func toArray() -> [Int] { + Array(arr.prefix(size())) + } + } ``` === "JS" ```javascript title="my_list.js" - [class]{MyList}-[func]{} + /* リストクラス */ + class MyList { + #arr = new Array(); // 配列(リスト要素を格納) + #capacity = 10; // リスト容量 + #size = 0; // リストの長さ(現在の要素数) + #extendRatio = 2; // リスト拡張時の増加倍率 + + /* コンストラクタ */ + constructor() { + this.#arr = new Array(this.#capacity); + } + + /* リストの長さを取得(現在の要素数) */ + size() { + return this.#size; + } + + /* リスト容量を取得する */ + capacity() { + return this.#capacity; + } + + /* 要素にアクセス */ + get(index) { + // インデックスが範囲外なら例外を送出する。以下同様 + if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です'); + return this.#arr[index]; + } + + /* 要素を更新 */ + set(index, num) { + if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です'); + this.#arr[index] = num; + } + + /* 末尾に要素を追加 */ + add(num) { + // 長さが容量に等しい場合は拡張が必要 + if (this.#size === this.#capacity) { + this.extendCapacity(); + } + // 新しい要素をリストの末尾に追加する + this.#arr[this.#size] = num; + this.#size++; + } + + /* 中間に要素を挿入 */ + insert(index, num) { + if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です'); + // 要素数が容量を超えると、拡張機構が発動する + if (this.#size === this.#capacity) { + this.extendCapacity(); + } + // index 以降の要素をすべて 1 つ後ろへずらす + for (let j = this.#size - 1; j >= index; j--) { + this.#arr[j + 1] = this.#arr[j]; + } + // 要素数を更新 + this.#arr[index] = num; + this.#size++; + } + + /* 要素を削除 */ + remove(index) { + if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です'); + let num = this.#arr[index]; + // インデックス index より後の要素をすべて 1 つ前に移動する + for (let j = index; j < this.#size - 1; j++) { + this.#arr[j] = this.#arr[j + 1]; + } + // 要素数を更新 + this.#size--; + // 削除された要素を返す + return num; + } + + /* リストの拡張 */ + extendCapacity() { + // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする + this.#arr = this.#arr.concat( + new Array(this.capacity() * (this.#extendRatio - 1)) + ); + // リストの容量を更新 + this.#capacity = this.#arr.length; + } + + /* リストを配列に変換する */ + toArray() { + let size = this.size(); + // 有効長の範囲内のリスト要素のみを変換 + const arr = new Array(size); + for (let i = 0; i < size; i++) { + arr[i] = this.get(i); + } + return arr; + } + } ``` === "TS" ```typescript title="my_list.ts" - [class]{MyList}-[func]{} + /* リストクラス */ + class MyList { + private arr: Array; // 配列(リスト要素を格納) + private _capacity: number = 10; // リスト容量 + private _size: number = 0; // リストの長さ(現在の要素数) + private extendRatio: number = 2; // リスト拡張時の増加倍率 + + /* コンストラクタ */ + constructor() { + this.arr = new Array(this._capacity); + } + + /* リストの長さを取得(現在の要素数) */ + public size(): number { + return this._size; + } + + /* リスト容量を取得する */ + public capacity(): number { + return this._capacity; + } + + /* 要素にアクセス */ + public get(index: number): number { + // インデックスが範囲外なら例外を送出する。以下同様 + if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です'); + return this.arr[index]; + } + + /* 要素を更新 */ + public set(index: number, num: number): void { + if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です'); + this.arr[index] = num; + } + + /* 末尾に要素を追加 */ + public add(num: number): void { + // 長さが容量に等しい場合は拡張が必要 + if (this._size === this._capacity) this.extendCapacity(); + // 新しい要素をリストの末尾に追加する + this.arr[this._size] = num; + this._size++; + } + + /* 中間に要素を挿入 */ + public insert(index: number, num: number): void { + if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です'); + // 要素数が容量を超えると、拡張機構が発動する + if (this._size === this._capacity) { + this.extendCapacity(); + } + // index 以降の要素をすべて 1 つ後ろへずらす + for (let j = this._size - 1; j >= index; j--) { + this.arr[j + 1] = this.arr[j]; + } + // 要素数を更新 + this.arr[index] = num; + this._size++; + } + + /* 要素を削除 */ + public remove(index: number): number { + if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です'); + let num = this.arr[index]; + // インデックス index より後の要素をすべて 1 つ前に移動する + for (let j = index; j < this._size - 1; j++) { + this.arr[j] = this.arr[j + 1]; + } + // 要素数を更新 + this._size--; + // 削除された要素を返す + return num; + } + + /* リストの拡張 */ + public extendCapacity(): void { + // `size` の長さを持つ配列を新規作成し、元の配列を新しい配列にコピーする + this.arr = this.arr.concat( + new Array(this.capacity() * (this.extendRatio - 1)) + ); + // リストの容量を更新 + this._capacity = this.arr.length; + } + + /* リストを配列に変換する */ + public toArray(): number[] { + let size = this.size(); + // 有効長の範囲内のリスト要素のみを変換 + const arr = new Array(size); + for (let i = 0; i < size; i++) { + arr[i] = this.get(i); + } + return arr; + } + } ``` === "Dart" ```dart title="my_list.dart" - [class]{MyList}-[func]{} + /* リストクラス */ + class MyList { + late List _arr; // 配列(リスト要素を格納) + int _capacity = 10; // リスト容量 + int _size = 0; // リストの長さ(現在の要素数) + int _extendRatio = 2; // リスト拡張時の増加倍率 + + /* コンストラクタ */ + MyList() { + _arr = List.filled(_capacity, 0); + } + + /* リストの長さを取得(現在の要素数) */ + int size() => _size; + + /* リスト容量を取得する */ + int capacity() => _capacity; + + /* 要素にアクセス */ + int get(int index) { + if (index >= _size) throw RangeError('インデックスが範囲外です'); + return _arr[index]; + } + + /* 要素を更新 */ + void set(int index, int _num) { + if (index >= _size) throw RangeError('インデックスが範囲外です'); + _arr[index] = _num; + } + + /* 末尾に要素を追加 */ + void add(int _num) { + // 要素数が容量を超えると、拡張機構が発動する + if (_size == _capacity) extendCapacity(); + _arr[_size] = _num; + // 要素数を更新 + _size++; + } + + /* 中間に要素を挿入 */ + void insert(int index, int _num) { + if (index >= _size) throw RangeError('インデックスが範囲外です'); + // 要素数が容量を超えると、拡張機構が発動する + if (_size == _capacity) extendCapacity(); + // index 以降の要素をすべて 1 つ後ろへずらす + for (var j = _size - 1; j >= index; j--) { + _arr[j + 1] = _arr[j]; + } + _arr[index] = _num; + // 要素数を更新 + _size++; + } + + /* 要素を削除 */ + int remove(int index) { + if (index >= _size) throw RangeError('インデックスが範囲外です'); + int _num = _arr[index]; + // インデックス index より後の要素をすべて 1 つ前に移動する + for (var j = index; j < _size - 1; j++) { + _arr[j] = _arr[j + 1]; + } + // 要素数を更新 + _size--; + // 削除された要素を返す + return _num; + } + + /* リストの拡張 */ + void extendCapacity() { + // 元の配列の `_extendRatio` 倍の長さを持つ新しい配列を作成する + final _newNums = List.filled(_capacity * _extendRatio, 0); + // 元の配列を新しい配列にコピー + List.copyRange(_newNums, 0, _arr); + // `_arr` の参照を更新 + _arr = _newNums; + // リストの容量を更新 + _capacity = _arr.length; + } + + /* リストを配列に変換する */ + List toArray() { + List arr = []; + for (var i = 0; i < _size; i++) { + arr.add(get(i)); + } + return arr; + } + } ``` === "Rust" ```rust title="my_list.rs" - [class]{MyList}-[func]{} + /* リストクラス */ + #[allow(dead_code)] + struct MyList { + arr: Vec, // 配列(リスト要素を格納) + capacity: usize, // リスト容量 + size: usize, // リストの長さ(現在の要素数) + extend_ratio: usize, // リスト拡張時の増加倍率 + } + + #[allow(unused, unused_comparisons)] + impl MyList { + /* コンストラクタ */ + pub fn new(capacity: usize) -> Self { + let mut vec = vec![0; capacity]; + Self { + arr: vec, + capacity, + size: 0, + extend_ratio: 2, + } + } + + /* リストの長さを取得(現在の要素数) */ + pub fn size(&self) -> usize { + return self.size; + } + + /* リスト容量を取得する */ + pub fn capacity(&self) -> usize { + return self.capacity; + } + + /* 要素にアクセス */ + pub fn get(&self, index: usize) -> i32 { + // インデックスが範囲外なら例外を送出する。以下同様 + if index >= self.size { + panic!("インデックスが範囲外です") + }; + return self.arr[index]; + } + + /* 要素を更新 */ + pub fn set(&mut self, index: usize, num: i32) { + if index >= self.size { + panic!("インデックスが範囲外です") + }; + self.arr[index] = num; + } + + /* 末尾に要素を追加 */ + pub fn add(&mut self, num: i32) { + // 要素数が容量を超えると、拡張機構が発動する + if self.size == self.capacity() { + self.extend_capacity(); + } + self.arr[self.size] = num; + // 要素数を更新 + self.size += 1; + } + + /* 中間に要素を挿入 */ + pub fn insert(&mut self, index: usize, num: i32) { + if index >= self.size() { + panic!("インデックスが範囲外です") + }; + // 要素数が容量を超えると、拡張機構が発動する + if self.size == self.capacity() { + self.extend_capacity(); + } + // index 以降の要素をすべて 1 つ後ろへずらす + for j in (index..self.size).rev() { + self.arr[j + 1] = self.arr[j]; + } + self.arr[index] = num; + // 要素数を更新 + self.size += 1; + } + + /* 要素を削除 */ + pub fn remove(&mut self, index: usize) -> i32 { + if index >= self.size() { + panic!("インデックスが範囲外です") + }; + let num = self.arr[index]; + // インデックス index より後の要素をすべて 1 つ前に移動する + for j in index..self.size - 1 { + self.arr[j] = self.arr[j + 1]; + } + // 要素数を更新 + self.size -= 1; + // 削除された要素を返す + return num; + } + + /* リストの拡張 */ + pub fn extend_capacity(&mut self) { + // 元の配列の extend_ratio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする + let new_capacity = self.capacity * self.extend_ratio; + self.arr.resize(new_capacity, 0); + // リストの容量を更新 + self.capacity = new_capacity; + } + + /* リストを配列に変換する */ + pub fn to_array(&self) -> Vec { + // 有効長の範囲内のリスト要素のみを変換 + let mut arr = Vec::new(); + for i in 0..self.size { + arr.push(self.get(i)); + } + arr + } + } ``` === "C" ```c title="my_list.c" - [class]{MyList}-[func]{} + /* リストクラス */ + typedef struct { + int *arr; // 配列(リスト要素を格納) + int capacity; // リスト容量 + int size; // リストのサイズ + int extendRatio; // リストが拡張されるたびの倍率 + } MyList; + + /* コンストラクタ */ + MyList *newMyList() { + MyList *nums = malloc(sizeof(MyList)); + nums->capacity = 10; + nums->arr = malloc(sizeof(int) * nums->capacity); + nums->size = 0; + nums->extendRatio = 2; + return nums; + } + + /* デストラクタ */ + void delMyList(MyList *nums) { + free(nums->arr); + free(nums); + } + + /* リストの長さを取得 */ + int size(MyList *nums) { + return nums->size; + } + + /* リスト容量を取得する */ + int capacity(MyList *nums) { + return nums->capacity; + } + + /* 要素にアクセス */ + int get(MyList *nums, int index) { + assert(index >= 0 && index < nums->size); + return nums->arr[index]; + } + + /* 要素を更新 */ + void set(MyList *nums, int index, int num) { + assert(index >= 0 && index < nums->size); + nums->arr[index] = num; + } + + /* 末尾に要素を追加 */ + void add(MyList *nums, int num) { + if (size(nums) == capacity(nums)) { + extendCapacity(nums); // 容量を拡張 + } + nums->arr[size(nums)] = num; + nums->size++; + } + + /* 中間に要素を挿入 */ + void insert(MyList *nums, int index, int num) { + assert(index >= 0 && index < size(nums)); + // 要素数が容量を超えると、拡張機構が発動する + if (size(nums) == capacity(nums)) { + extendCapacity(nums); // 容量を拡張 + } + for (int i = size(nums); i > index; --i) { + nums->arr[i] = nums->arr[i - 1]; + } + nums->arr[index] = num; + nums->size++; + } + + /* 要素を削除 */ + // 注意: stdio.h が remove 識別子を使用している + int removeItem(MyList *nums, int index) { + assert(index >= 0 && index < size(nums)); + int num = nums->arr[index]; + for (int i = index; i < size(nums) - 1; i++) { + nums->arr[i] = nums->arr[i + 1]; + } + nums->size--; + return num; + } + + /* リストの拡張 */ + void extendCapacity(MyList *nums) { + // 先に領域を確保する + int newCapacity = capacity(nums) * nums->extendRatio; + int *extend = (int *)malloc(sizeof(int) * newCapacity); + int *temp = nums->arr; + + // 古いデータを新しいデータにコピー + for (int i = 0; i < size(nums); i++) + extend[i] = nums->arr[i]; + + // 古いデータを解放する + free(temp); + + // 新しいデータに更新 + nums->arr = extend; + nums->capacity = newCapacity; + } + + /* 出力用にリストを Array に変換 */ + int *toArray(MyList *nums) { + return nums->arr; + } ``` === "Kotlin" ```kotlin title="my_list.kt" - [class]{MyList}-[func]{} + /* リストクラス */ + class MyList { + private var arr: IntArray = intArrayOf() // 配列(リスト要素を格納) + private var capacity: Int = 10 // リスト容量 + private var size: Int = 0 // リストの長さ(現在の要素数) + private var extendRatio: Int = 2 // リスト拡張時の増加倍率 + + /* コンストラクタ */ + init { + arr = IntArray(capacity) + } + + /* リストの長さを取得(現在の要素数) */ + fun size(): Int { + return size + } + + /* リスト容量を取得する */ + fun capacity(): Int { + return capacity + } + + /* 要素にアクセス */ + fun get(index: Int): Int { + // インデックスが範囲外なら例外を送出する。以下同様 + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("インデックスが範囲外") + return arr[index] + } + + /* 要素を更新 */ + fun set(index: Int, num: Int) { + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("インデックスが範囲外") + arr[index] = num + } + + /* 末尾に要素を追加 */ + fun add(num: Int) { + // 要素数が容量を超えると、拡張機構が発動する + if (size == capacity()) + extendCapacity() + arr[size] = num + // 要素数を更新 + size++ + } + + /* 中間に要素を挿入 */ + fun insert(index: Int, num: Int) { + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("インデックスが範囲外") + // 要素数が容量を超えると、拡張機構が発動する + if (size == capacity()) + extendCapacity() + // index 以降の要素をすべて 1 つ後ろへずらす + for (j in size - 1 downTo index) + arr[j + 1] = arr[j] + arr[index] = num + // 要素数を更新 + size++ + } + + /* 要素を削除 */ + fun remove(index: Int): Int { + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("インデックスが範囲外") + val num = arr[index] + // インデックス index より後の要素をすべて 1 つ前に移動する + for (j in index..= size + @arr[index] + end + + ### 要素にアクセス ### + def set(index, num) + raise IndexError, "インデックスが範囲外です" if index < 0 || index >= size + @arr[index] = num + end + + ### 末尾に要素を追加 ### + def add(num) + # 要素数が容量を超えると、拡張機構が発動する + extend_capacity if size == capacity + @arr[size] = num + + # 要素数を更新 + @size += 1 + end + + ### 途中に要素を挿入 ### + def insert(index, num) + raise IndexError, "インデックスが範囲外です" if index < 0 || index >= size + + # 要素数が容量を超えると、拡張機構が発動する + extend_capacity if size == capacity + + # index 以降の要素をすべて 1 つ後ろへずらす + for j in (size - 1).downto(index) + @arr[j + 1] = @arr[j] + end + @arr[index] = num + + # 要素数を更新 + @size += 1 + end + + ### 要素の削除 ### + def remove(index) + raise IndexError, "インデックスが範囲外です" if index < 0 || index >= size + num = @arr[index] + + # インデックス index より後の要素をすべて 1 つ前に移動する + for j in index...size + @arr[j] = @arr[j + 1] + end + + # 要素数を更新 + @size -= 1 + + # 削除された要素を返す + num + end + + ### リストの容量拡張 ### + def extend_capacity + # 元の配列の extend_ratio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする + arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1)) + # リストの容量を更新 + @capacity = arr.length + end + + ### リストを配列に変換 ### + def to_array + sz = size + # 有効長の範囲内のリスト要素のみを変換 + arr = Array.new(sz) + for i in 0...sz + arr[i] = get(i) + end + arr + end + end ``` + +??? pythontutor "コードの可視化" + +
+ diff --git a/ja/docs/chapter_array_and_linkedlist/ram_and_cache.md b/ja/docs/chapter_array_and_linkedlist/ram_and_cache.md index a876fd70d..1347d87aa 100644 --- a/ja/docs/chapter_array_and_linkedlist/ram_and_cache.md +++ b/ja/docs/chapter_array_and_linkedlist/ram_and_cache.md @@ -4,80 +4,80 @@ comments: true # 4.4   メモリとキャッシュ * -この章の最初の2つのセクションでは、「連続格納」と「分散格納」をそれぞれ表現する2つの基本的なデータ構造である配列と連結リストを探究しました。 +本章の前二節では、配列と連結リストという二つの基礎的かつ重要なデータ構造を扱いました。これらはそれぞれ「連続格納」と「分散格納」という二つの物理構造を表しています。 -実際、**物理構造はプログラムがメモリとキャッシュをどの程度効率的に利用するかを大きく決定し**、これがアルゴリズムの全体的なパフォーマンスに影響を与えます。 +実際には、**物理構造はプログラムにおけるメモリとキャッシュの利用効率を大きく左右し**、ひいてはアルゴリズムプログラム全体の性能に影響します。 -## 4.4.1   コンピュータ記憶装置 +## 4.4.1   コンピュータの記憶装置 -コンピュータには3種類の記憶装置があります:ハードディスクランダムアクセスメモリ(RAM)、およびキャッシュメモリです。以下の表は、コンピュータシステムにおけるそれぞれの役割とパフォーマンス特性を示しています。 +コンピュータには三種類の記憶装置があります。ハードディスク(hard disk)メモリ(random-access memory, RAM)キャッシュ(cache memory)です。以下の表は、これらがコンピュータシステムで担う役割と性能上の特徴を示しています。 -

表 4-2   コンピュータ記憶装置

+

表 4-2   コンピュータの記憶装置

-| | ハードディスク | メモリ | キャッシュ | -| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | -| 用途 | OS、プログラム、ファイルなどのデータの長期保存 | 現在実行中のプログラムと処理中のデータの一時保存 | 頻繁にアクセスされるデータと命令を保存し、CPUのメモリへのアクセス数を削減 | -| 揮発性 | 電源オフ後もデータは失われない | 電源オフ後にデータは失われる | 電源オフ後にデータは失われる | -| 容量 | より大きい、TBレベル | より小さい、GBレベル | 非常に小さい、MBレベル | -| 速度 | より遅い、数百から数千MB/s | より高速、数十GB/s | 非常に高速、数十から数百GB/s | -| 価格(USD) | より安価、数セント/GB | より高価、数ドル/GB | 非常に高価、CPUと一緒に価格設定 | +| | ハードディスク | メモリ | キャッシュ | +| -------------- | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- | +| 用途 | OS、プログラム、ファイルなどを長期保存 | 実行中のプログラムや処理中のデータを一時保存 | 頻繁にアクセスされるデータや命令を保存し、CPU のメモリアクセス回数を減らす | +| 揮発性 | 電源断後もデータは失われない | 電源断後にデータは失われる | 電源断後にデータは失われる | +| 容量 | 大きい、TB 級 | 小さい、GB 級 | 非常に小さい、MB 級 | +| 速度 | 遅い、数百〜数千 MB/s | 速い、数十 GB/s | 非常に速い、数十〜数百 GB/s | +| 価格(人民元) | 比較的安価、数角〜数元 / GB | 比較的高価、数十〜数百元 / GB | 非常に高価、CPU と一体で価格設定される |
-コンピュータ記憶システムは、下図に示すようにピラミッドとして視覚化できます。ピラミッドの上部にある記憶装置ほど高速で、容量が小さく、より高価です。このマルチレベル設計は偶然ではなく、コンピュータ科学者とエンジニアによる慎重な検討の結果です。 +コンピュータの記憶システムは、下図のようなピラミッド構造として捉えられます。ピラミッドの頂点に近い記憶装置ほど速度は速く、容量は小さく、コストは高くなります。この多層構造は偶然ではなく、コンピュータ科学者やエンジニアによる熟慮の末の設計です。 -- **ハードディスクをメモリに置き換えるのは困難です**。第一に、メモリ内のデータは電源オフ後に失われるため、長期データ保存には適していません。第二に、メモリはハードディスクよりも大幅に高価で、消費者市場での広範囲な使用の実現可能性を制限しています。 -- **キャッシュは大容量と高速のトレードオフに直面しています**。L1、L2、L3キャッシュの容量が増加するにつれて、その物理サイズが大きくなり、CPUコアからの距離が増加します。これによりデータ転送時間が長くなり、アクセス遅延が高くなります。現在の技術では、マルチレベルキャッシュ構造が容量、速度、コストの間の最適なバランスを提供します。 +- **ハードディスクはメモリで置き換えにくい**。まず、メモリ内のデータは電源断後に失われるため、長期保存には向きません。次に、メモリのコストはハードディスクの数十倍であり、消費者市場で広く普及しにくいという問題があります。 +- **キャッシュは大容量と高速性を両立しにくい**。L1、L2、L3 キャッシュの容量が段階的に増えるにつれて、物理サイズは大きくなり、CPU コアとの物理的距離も遠くなります。その結果、データ転送時間が増え、要素アクセスの遅延も大きくなります。現在の技術では、多層キャッシュ構造が容量、速度、コストの最適なバランスです。 -![コンピュータ記憶システム](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" } +![コンピュータの記憶システム](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" } -

図 4-9   コンピュータ記憶システム

+

図 4-9   コンピュータの記憶システム

!!! tip - コンピュータの記憶階層は、速度、容量、コストの間の慎重なバランスを反映しています。このタイプのトレードオフは様々な業界で一般的であり、利益と制限の間の最適なバランスを見つけることが重要です。 + コンピュータの記憶階層は、速度、容量、コストの三者間にある巧妙なバランスを体現しています。実際、このようなトレードオフはあらゆる工業分野に広く存在しており、異なる利点と制約のあいだで最適な均衡点を見つけることが求められます。 -全体的に、**ハードディスクは大量のデータの長期保存を提供し、メモリはプログラム実行中に処理されるデータの一時保存として機能し、キャッシュは頻繁にアクセスされるデータと命令を保存して実行効率を向上させます**。それらは一緒になってコンピュータシステムの効率的な動作を保証します。 +要するに、**ハードディスクは大量データの長期保存に、メモリはプログラム実行中に処理しているデータの一時保存に、キャッシュは頻繁にアクセスされるデータや命令の保存に用いられ**、プログラム実行効率を高めます。三者は協調して動作し、コンピュータシステムの高効率な運用を支えています。 -下図に示すように、プログラム実行中、データはハードディスクからメモリに読み込まれ、CPU計算が行われます。CPUの拡張として機能するキャッシュは、**メモリからインテリジェントにデータを先読み**し、CPUのより高速なデータアクセスを可能にします。これによりプログラム実行効率が大幅に向上し、低速なメモリへの依存が減少します。 +次の図に示すように、プログラム実行時にはデータがハードディスクからメモリへ読み込まれ、CPU の計算に使われます。キャッシュは CPU の一部と見なせ、**メモリからデータを賢く読み込むことで**、CPU に高速なデータ読み出しを提供し、プログラムの実行効率を大きく高め、低速なメモリへの依存を減らします。 -![ハードディスク、メモリ、キャッシュ間のデータフロー](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } +![ハードディスク、メモリ、キャッシュ間のデータの流れ](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } -

図 4-10   ハードディスク、メモリ、キャッシュ間のデータフロー

+

図 4-10   ハードディスク、メモリ、キャッシュ間のデータの流れ

## 4.4.2   データ構造のメモリ効率 -メモリ空間利用の観点から、配列と連結リストにはそれぞれ利点と制限があります。 +メモリ空間の利用という観点では、配列と連結リストにはそれぞれ利点と制約があります。 -一方で、**メモリは限られており、複数のプログラム間で共有できない**ため、データ構造での空間使用の最適化は重要です。配列は要素が密接にパックされており、連結リストのように参照(ポインタ)のための追加メモリを必要としないため、空間効率的です。しかし、配列は連続したメモリブロックを事前に割り当てる必要があり、割り当てられた空間が実際の必要量を超える場合、無駄につながる可能性があります。配列の拡張も追加の時間と空間のオーバーヘッドを伴います。対照的に、連結リストは各ノードに対してメモリを動的に割り当て・解放し、ポインタのための追加メモリのコストでより大きな柔軟性を提供します。 +一方で、**メモリは有限であり、同じメモリ領域を複数のプログラムで共有することはできません**。そのため、データ構造にはできるだけ効率よく空間を使うことが求められます。配列の要素は密に並んでおり、連結リストのノード間参照(ポインタ)を保持する追加領域が不要なため、空間効率は高くなります。しかし、配列は十分な連続メモリを一度に確保する必要があり、メモリ浪費を招くことがありますし、拡張時にも追加の時間と空間コストがかかります。これに対して連結リストは「ノード」単位で動的にメモリを割り当て・解放でき、より高い柔軟性を備えています。 -一方で、プログラム実行中、**繰り返されるメモリの割り当てと解放はメモリの断片化を増加させ**、メモリ利用効率を低下させます。配列は連続記憶方式により、メモリ断片化を引き起こす可能性が比較的低いです。対照的に、連結リストは要素を非連続の場所に保存し、頻繁な挿入と削除はメモリ断片化を悪化させる可能性があります。 +他方で、プログラムの実行中には、**メモリの確保と解放を繰り返すにつれて、空きメモリの断片化はますます進み**、メモリ利用効率の低下を招きます。配列は連続した格納方式を取るため、比較的メモリ断片化を起こしにくい構造です。反対に、連結リストの要素は分散して格納されるため、頻繁な挿入や削除を行うと、より断片化を招きやすくなります。 ## 4.4.3   データ構造のキャッシュ効率 -キャッシュはメモリよりも空間容量がはるかに小さいですが、はるかに高速で、プログラム実行速度において重要な役割を果たします。限られた容量のため、キャッシュは頻繁にアクセスされるデータのサブセットのみを保存できます。CPUがキャッシュに存在しないデータにアクセスしようとすると、キャッシュミスが発生し、CPUは低速なメモリから必要なデータを取得する必要があり、パフォーマンスに影響を与える可能性があります。 +キャッシュは容量こそメモリよりはるかに小さいものの、速度はメモリよりずっと速く、プログラム実行速度において極めて重要な役割を果たします。キャッシュ容量には限りがあり、頻繁にアクセスされる一部のデータしか保持できません。そのため、CPU がアクセスしようとするデータがキャッシュ内に存在しない場合、キャッシュミス(cache miss)が発生し、CPU は低速なメモリから必要なデータを読み込まなければなりません。 -明らかに、**キャッシュミスが少ないほど、CPUのデータ読み書き効率が高く**、プログラムパフォーマンスが向上します。CPUがキャッシュからデータを正常に取得する割合はキャッシュヒット率と呼ばれ、キャッシュ効率を測定するためによく使用される指標です。 +当然ながら、**「キャッシュミス」が少ないほど、CPU のデータ読み書き効率は高くなり**、プログラム性能も向上します。CPU がキャッシュからデータを正常に取得できた割合をキャッシュヒット率(cache hit rate)と呼び、この指標は通常、キャッシュ効率の評価に用いられます。 -より高い効率を達成するために、キャッシュは以下のデータロードメカニズムを採用します。 +できるだけ高い効率を実現するため、キャッシュは次のようなデータ読み込みの仕組みを採用しています。 -- **キャッシュライン**:キャッシュは個々のバイトではなく、キャッシュラインと呼ばれる単位でデータを保存・ロードして動作します。このアプローチは、一度により大きなデータブロックを転送することで効率を向上させます。 -- **先読みメカニズム**:プロセッサはデータアクセスパターン(例:連続または固定ストライドアクセス)を予測し、これらのパターンに基づいてデータをキャッシュに先読みして、キャッシュヒット率を向上させます。 -- **空間的局所性**:特定のデータがアクセスされると、近くのデータもまもなくアクセスされる可能性があります。これを活用するために、キャッシュは要求されたデータと一緒に隣接するデータをロードし、ヒット率を向上させます。 -- **時間的局所性**:データがアクセスされた場合、近い将来に再びアクセスされる可能性があります。キャッシュはこの原理を使用して、最近アクセスされたデータを保持してヒット率を向上させます。 +- **キャッシュライン**:キャッシュはデータを 1 バイト単位で保存・読み込みするのではなく、キャッシュライン単位で扱います。1 バイト単位の転送と比べて、キャッシュライン単位のほうが効率的です。 +- **プリフェッチ機構**:プロセッサはデータアクセスのパターン(たとえば順次アクセス、一定ステップ幅のスキップアクセスなど)を予測し、そのパターンに応じてデータをキャッシュへ読み込むことで、ヒット率を高めます。 +- **空間的局所性**:あるデータがアクセスされた場合、その近傍のデータも近いうちにアクセスされる可能性があります。そのため、キャッシュはあるデータを読み込む際に、その周辺のデータもあわせて読み込み、ヒット率を高めます。 +- **時間的局所性**:あるデータがアクセスされた場合、そのデータは近い将来に再びアクセスされる可能性が高いです。キャッシュはこの性質を利用し、最近アクセスしたデータを保持することでヒット率を高めます。 -実際、**配列と連結リストは異なるキャッシュ利用効率を持ち**、これは主に以下の側面に反映されます。 +実際には、**配列と連結リストではキャッシュの利用効率が異なり**、主に次の点に表れます。 -- **占有空間**:連結リスト要素は配列要素よりも多くの空間を占有するため、キャッシュに保持される有効データが少なくなります。 -- **キャッシュライン**:連結リストデータはメモリ全体に散在し、キャッシュは「行単位でロード」されるため、ロードされる無効データの割合が高くなります。 -- **先読みメカニズム**:配列のデータアクセスパターンは連結リストよりも「予測可能」で、つまりシステムがこれからロードされるデータを推測しやすいです。 -- **空間的局所性**:配列は連続したメモリ空間に保存されるため、ロードされているデータの近くのデータがまもなくアクセスされる可能性が高くなります。 +- **使用空間**:連結リストの要素は配列要素より多くの空間を占めるため、キャッシュに収まる有効データ量は少なくなります。 +- **キャッシュライン**:連結リストのデータはメモリの各所に分散しており、キャッシュは「ライン単位で読み込む」ため、無効データまで読み込む割合が高くなります。 +- **プリフェッチ機構**:配列のほうが連結リストよりもデータアクセスのパターンを「予測しやすく」、システムが次に読み込まれるデータを推測しやすくなります。 +- **空間的局所性**:配列はまとまったメモリ空間に格納されるため、読み込まれたデータの近くにあるデータも、まもなくアクセスされる可能性が高くなります。 -全体的に、**配列はより高いキャッシュヒット率を持ち、一般的に連結リストよりも操作効率が高いです**。これにより、配列に基づくデータ構造はアルゴリズム問題の解決において人気があります。 +全体として、**配列はより高いキャッシュヒット率を持つため、操作効率では通常、連結リストより優れています**。このため、アルゴリズム問題を解く際には、配列ベースで実装されたデータ構造のほうが好まれることが多くなります。 -**高いキャッシュ効率が配列が常に連結リストより優れているという意味ではない**ことに注意すべきです。データ構造の選択は特定のアプリケーション要件に依存すべきです。例えば、配列と連結リストの両方が「スタック」データ構造を実装できますが(次章で詳細説明)、それらは異なるシナリオに適しています。 +注意すべきなのは、**キャッシュ効率が高いからといって、配列があらゆる状況で連結リストより優れているとは限らない**という点です。実際にどのデータ構造を選ぶかは、具体的な要件に応じて決めるべきです。たとえば、配列と連結リストはいずれも「スタック」データ構造を実装できますが(次章で詳しく説明します)、適した場面は異なります。 -- アルゴリズム問題では、より高い操作効率とランダムアクセス機能を提供するため、配列に基づくスタックを選択する傾向があります。唯一のコストは配列に対して一定量のメモリ空間を事前に割り当てる必要があることです。 -- データ量が非常に大きく、高度に動的で、スタックの予想サイズを推定するのが困難な場合、連結リストに基づくスタックがより良い選択です。連結リストは大量のデータをメモリの異なる部分に分散でき、配列拡張の追加オーバーヘッドを回避できます。 +- アルゴリズム問題に取り組むときは、一般に配列ベースのスタックを選ぶ傾向があります。より高い操作効率とランダムアクセス能力を備えており、その代償は配列用に一定量のメモリを事前確保することだけです。 +- データ量が非常に大きく、動的性が高く、スタックの想定サイズを見積もりにくい場合は、連結リストベースのスタックのほうが適しています。連結リストなら大量のデータをメモリの異なる場所に分散して保存でき、配列拡張による追加コストも回避できます。 diff --git a/ja/docs/chapter_array_and_linkedlist/summary.md b/ja/docs/chapter_array_and_linkedlist/summary.md index 6abecc2b3..f95602a8a 100644 --- a/ja/docs/chapter_array_and_linkedlist/summary.md +++ b/ja/docs/chapter_array_and_linkedlist/summary.md @@ -4,82 +4,87 @@ comments: true # 4.5   まとめ -### 1.   重要な復習 +### 1.   要点の振り返り -- 配列と連結リストは2つの基本的なデータ構造であり、コンピュータメモリにおける2つの格納方法を表しています:連続空間格納と非連続空間格納です。それらの特性は互いに補完し合います。 -- 配列はランダムアクセスをサポートし、使用するメモリが少ない一方で、要素の挿入と削除は非効率的で、初期化後の長さが固定されています。 -- 連結リストは参照(ポインタ)の変更によって効率的なノードの挿入と削除を実装し、長さを柔軟に調整できますが、ノードアクセス効率が低く、より多くのメモリを消費します。 -- 連結リストの一般的な種類には、単方向連結リスト、循環連結リスト、双方向連結リストがあり、それぞれに独自の応用シナリオがあります。 -- リストは要素の順序付けられたコレクションで、追加、削除、変更をサポートし、通常は動的配列に基づいて実装され、配列の利点を保持しながら柔軟な長さ調整を可能にします。 -- リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります。 -- プログラム実行中、データは主にメモリに格納されます。配列はより高いメモリ空間効率を提供し、連結リストはメモリ使用においてより柔軟です。 -- キャッシュは、キャッシュライン、先読み、空間的局所性、時間的局所性などのメカニズムを通じてCPUに高速データアクセスを提供し、プログラム実行効率を大幅に向上させます。 -- より高いキャッシュヒット率により、配列は一般的に連結リストよりも効率的です。データ構造を選択する際は、特定のニーズとシナリオに基づいて適切な選択をすべきです。 +- 配列と連結リストは 2 種類の基本的なデータ構造であり、それぞれコンピュータメモリにおけるデータの 2 つの格納方式、すなわち連続領域への格納と分散領域への格納を表す。両者の特徴は相互補完的である。 +- 配列はランダムアクセスをサポートし、使用メモリも少ない。一方で、要素の挿入と削除の効率は低く、初期化後に長さを変更できない。 +- 連結リストは参照(ポインタ)を変更することでノードの挿入と削除を効率的に行え、長さも柔軟に調整できる。一方で、ノードへのアクセス効率は低く、メモリ使用量も多い。一般的な連結リストには単方向連結リスト、循環連結リスト、双方向連結リストがある。 +- リストは、追加・削除・検索・更新をサポートする順序付き要素集合であり、通常は動的配列に基づいて実装される。配列の利点を保ちながら、長さを柔軟に調整できる。 +- リストの登場により配列の実用性は大幅に高まったが、一部のメモリ領域が無駄になる可能性がある。 +- プログラムの実行時、データは主にメモリに格納される。配列はより高いメモリ空間効率を提供でき、連結リストはメモリ利用の面でより柔軟である。 +- キャッシュは、キャッシュライン、プリフェッチ機構、空間局所性と時間局所性といったデータ読み込み機構を通じて CPU に高速なデータアクセスを提供し、プログラムの実行効率を大きく向上させる。 +- 配列はキャッシュヒット率が高いため、通常は連結リストよりも高効率である。データ構造を選択する際は、具体的な要件や場面に応じて適切に選ぶべきである。 ### 2.   Q & A -**Q**:配列をスタックに格納するかヒープに格納するかは、時間と空間効率に影響しますか? +**Q**:配列をスタックに格納する場合とヒープに格納する場合では、時間効率と空間効率に影響がありますか? -スタックとヒープの両方に格納される配列は連続したメモリ空間に格納され、データ操作効率は本質的に同じです。しかし、スタックとヒープには独自の特性があり、以下の違いが生じます。 +スタック上とヒープ上の配列はいずれも連続したメモリ領域に格納されるため、データ操作の効率は基本的に同じである。ただし、スタックとヒープにはそれぞれ特徴があり、以下の違いが生じる。 -1. 割り当てと解放効率:スタックはより小さなメモリブロックで、コンパイラによって自動的に割り当てられます。ヒープメモリは比較的大きく、コードで動的に割り当てることができ、断片化しやすいです。したがって、ヒープでの割り当てと解放操作は一般的にスタックよりも遅くなります。 -2. サイズ制限:スタックメモリは比較的小さく、ヒープサイズは一般的に利用可能なメモリによって制限されます。したがって、ヒープは大きな配列の格納により適しています。 -3. 柔軟性:スタック上の配列のサイズはコンパイル時に決定される必要がありますが、ヒープ上の配列のサイズは実行時に動的に決定できます。 +1. 確保と解放の効率:スタックは比較的小さなメモリ領域で、確保はコンパイラによって自動的に行われる。一方、ヒープメモリは相対的に大きく、コード内で動的に確保できる反面、断片化しやすい。そのため、ヒープ上での確保と解放は通常スタック上より遅い。 +2. サイズ制限:スタックメモリは比較的小さく、ヒープのサイズは一般に利用可能メモリに制限される。そのため、ヒープは大きな配列の格納により適している。 +3. 柔軟性:スタック上の配列サイズはコンパイル時に確定している必要があるが、ヒープ上の配列サイズは実行時に動的に決定できる。 -**Q**:なぜ配列は同じ型の要素を必要とし、連結リストは同じ型の要素を強調しないのですか? +**Q**:なぜ配列では同じ型の要素が求められるのに、連結リストでは同じ型であることが強調されないのですか? -連結リストは参照(ポインタ)によって接続されたノードで構成され、各ノードはint、double、string、objectなど、異なる型のデータを格納できます。 +連結リストはノードで構成され、ノード同士は参照(ポインタ)で接続されている。各ノードには `int`、`double`、`string`、`object` など、異なる型のデータを格納できる。 -対照的に、配列要素は同じ型である必要があり、これにより対応する要素位置にアクセスするためのオフセットを計算できます。例えば、intとlong型の両方を含む配列で、単一要素がそれぞれ4バイトと8バイトを占有する場合、配列に2つの異なる長さの要素が含まれているため、以下の式を使用してオフセットを計算できません。 +これに対して、配列要素は同じ型でなければならない。そうでなければ、オフセットを計算して対応する要素位置を取得できないからである。たとえば、配列に `int` と `long` の 2 種類が同時に含まれていて、各要素がそれぞれ 4 バイトと 8 バイトを占める場合、配列内に 2 種類の「要素長」が存在するため、次の式ではオフセットを計算できない。 ```shell -# 要素メモリアドレス = 配列メモリアドレス + 要素長 * 要素インデックス +# 要素のメモリアドレス = 配列のメモリアドレス(先頭要素のメモリアドレス) + 要素長 * 要素インデックス ``` -**Q**:ノードを削除した後、`P.next`を`None`に設定する必要がありますか? +**Q**:ノード `P` を削除した後、`P.next` を `None` に設定する必要はありますか? -`P.next`を変更しなくても問題ありません。連結リストの観点から、ヘッドノードからテールノードまでの巡回で`P`に遭遇することはもうありません。これは、ノード`P`がリストから効果的に削除されたことを意味し、`P`が指す場所はもはやリストに影響しません。 +`P.next` を変更しなくてもよい。この連結リストの観点では、先頭ノードから末尾ノードまでたどっても、もはや `P` に出会うことはない。つまり、ノード `P` はすでに連結リストから削除されており、この時点で `P` がどこを指していても、この連結リストには影響しない。 -ガベージコレクションの観点から、Java、Python、Goなどの自動ガベージコレクションメカニズムを持つ言語では、ノード`P`が収集されるかどうかは、それを指す参照がまだあるかどうかに依存し、`P.next`の値には依存しません。CやC++などの言語では、ノードのメモリを手動で解放する必要があります。 +データ構造とアルゴリズム(問題を解くとき)の観点では、切り離さなくても問題はなく、プログラムのロジックが正しいことを保証すればよい。標準ライブラリの観点では、切り離したほうがより安全で、ロジックも明確である。切り離さない場合、削除されたノードが適切に回収されなかったとすると、後続ノードのメモリ回収に影響する可能性がある。 -**Q**:連結リストでは、挿入と削除操作の時間計算量は`O(1)`です。しかし、挿入や削除前の要素検索には`O(n)`時間がかかるので、なぜ時間計算量は`O(n)`ではないのですか? +**Q**:連結リストでの挿入と削除の時間計算量は $O(1)$ です。しかし、追加や削除の前には要素を探すのに $O(n)$ の時間が必要です。では、なぜ時間計算量は $O(n)$ ではないのですか? -要素を最初に検索してから削除する場合、時間計算量は確かに`O(n)`です。しかし、連結リストの挿入と削除における`O(1)`の利点は他のアプリケーションで実現できます。例えば、連結リストを使用した両端キューの実装では、常にヘッドとテールノードを指すポインタを維持し、各挿入と削除操作を`O(1)`にします。 +要素を先に探してから削除するのであれば、時間計算量が $O(n)$ であるのは確かである。しかし、連結リストの $O(1)$ での追加・削除という利点は、ほかの用途で生かせる。たとえば、両端キューは連結リストで実装するのに適しており、先頭ノードと末尾ノードを常に指すポインタ変数を維持すれば、各挿入・削除操作はどれも $O(1)$ になる。 -**Q**:「連結リストの定義と格納方法」の図で、薄青色の格納ノードは単一のメモリアドレスを占有しますか、それともノード値と半分を共有しますか? +**Q**:図「連結リストの定義と格納方式」で、薄青色のノードポインタ部分は 1 つのメモリアドレスを占めているのですか? それともノード値と半分ずつなのでしょうか? -図は単なる定性的な表現であり、定量的分析は特定の状況に依存します。 +この模式図は定性的な表現にすぎず、定量的な表現は具体的な状況に応じて分析する必要がある。 -- 異なる型のノード値は異なる量の空間を占有します。例えば、int、long、double、オブジェクトインスタンスです。 -- ポインタ変数によって占有されるメモリ空間は、使用されるオペレーティングシステムとコンパイル環境に依存し、通常8バイトまたは4バイトです。 +- ノード値が占める領域は型によって異なり、たとえば `int`、`long`、`double`、インスタンスオブジェクトなどがある。 +- ポインタ変数が占めるメモリ空間の大きさは、使用する OS やコンパイル環境によって異なり、多くは 8 バイトまたは 4 バイトである。 -**Q**:リストの末尾への要素追加は常に`O(1)`ですか? +**Q**:リストの末尾への要素追加は常に $O(1)$ ですか? -要素を追加することでリスト長を超える場合、リストは最初に拡張される必要があります。システムは新しいメモリブロックを要求し、元のリストのすべての要素を移動するため、この場合の時間計算量は`O(n)`になります。 +要素を追加する際にリスト長を超える場合は、先にリストを拡張してから追加する必要がある。システムは新しいメモリ領域を確保し、元のリストの全要素をそこへ移動するため、このとき時間計算量は $O(n)$ になる。 -**Q**:「リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります」という文は、容量、長さ、拡張係数などの追加変数によって占有されるメモリを指していますか? +**Q**:「リストの登場により配列の実用性は大きく向上したが、一部のメモリ空間が無駄になる可能性がある」というのは、容量、長さ、拡張倍率のような追加変数が占めるメモリのことですか? -ここでの空間の無駄は主に2つの側面を指します:一方で、リストは初期長で設定されますが、常に必要とは限りません。他方で、頻繁な拡張を防ぐため、拡張は通常$\times 1.5$などの係数で乗算されます。これにより多くの空きスロットが生まれ、通常は完全に埋めることができません。 +ここでいう空間の無駄には主に 2 つの意味がある。一方では、リストには初期長が設定されるが、必ずしもそれだけ必要とは限らない。もう一方では、頻繁な拡張を防ぐため、拡張時には通常ある係数、たとえば $\times 1.5$ を掛ける。このため、多くの空きスロットが生じ、通常それらを完全に埋めることはできない。 -**Q**:Pythonで`n = [1, 2, 3]`を初期化した後、これら3つの要素のアドレスは連続していますが、`m = [2, 1, 3]`を初期化すると、各要素の`id`は連続していないが`n`のものと同一です。これらの要素のアドレスが連続していない場合、`m`はまだ配列ですか? +**Q**:Python で `n = [1, 2, 3]` を初期化した後、この 3 つの要素のアドレスは連続しています。しかし `m = [2, 1, 3]` を初期化すると、各要素の id は連続しておらず、それぞれ `n` 内の同じ値と一致していることがわかります。これらの要素のアドレスが連続していないなら、`m` も配列なのですか? -リスト要素を連結リストノード`n = [n1, n2, n3, n4, n5]`に置き換える場合、これら5つのノードオブジェクトも通常メモリ全体に分散しています。しかし、リストインデックスが与えられれば、`O(1)`時間でノードのメモリアドレスにアクセスでき、対応するノードにアクセスできます。これは、配列がノード自体ではなく、ノードへの参照を格納するためです。 +仮にリスト要素を連結リストのノード `n = [n1, n2, n3, n4, n5]` に置き換えたとしても、通常この 5 つのノードオブジェクトもメモリ上の各所に分散して格納される。それでも、与えられたリストインデックスに対して、私たちは依然として $O(1)$ 時間でノードのメモリアドレスを取得し、対応するノードにアクセスできる。これは、配列に格納されているのがノードそのものではなく、ノードへの参照だからである。 -多くの言語とは異なり、Pythonでは数値もオブジェクトとしてラップされ、リストは数値自体ではなく、これらの数値への参照を格納します。したがって、2つの配列の同じ数値が同じ`id`を持ち、これらの数値のメモリアドレスは連続である必要がないことがわかります。 +多くの言語と異なり、Python では数値もオブジェクトとしてラップされており、リストに格納されているのは数値そのものではなく、数値への参照である。そのため、2 つの配列内の同じ数値が同一の id を持つことがあり、しかもそれらの数値のメモリアドレスは連続している必要がない。 -**Q**:C++ STLの`std::list`はすでに双方向連結リストを実装していますが、一部のアルゴリズム書籍では直接使用していないようです。何か制限がありますか? +**Q**:C++ STL の `std::list` はすでに双方向連結リストを実装していますが、アルゴリズム本ではあまり直接使われないようです。何か制約があるのでしょうか? -一方で、アルゴリズムを実装する際は配列を使用することを好み、必要な場合のみ連結リストを使用します。主に2つの理由があります。 +一方では、私たちは多くの場合、アルゴリズムの実装に配列を好み、必要なときにだけ連結リストを使う。その主な理由は 2 つある。 -- 空間オーバーヘッド:各要素に2つの追加ポインタ(前の要素用と次の要素用)が必要なため、`std::list`は通常`std::vector`よりも多くの空間を占有します。 -- キャッシュ非友好的:データが連続して格納されていないため、`std::list`はキャッシュ利用率が低くなります。一般的に、`std::vector`の方がパフォーマンスが優れています。 +- 空間オーバーヘッド:各要素には 2 つの追加ポインタ(前の要素用と次の要素用)が必要なため、`std::list` は通常 `std::vector` より多くの空間を消費する。 +- キャッシュ非効率:データが連続して格納されていないため、`std::list` はキャッシュの利用効率が低い。一般には、`std::vector` のほうが性能がよい。 -他方で、連結リストは主に二分木とグラフに必要です。スタックとキューは、連結リストではなく、プログラミング言語の`stack`と`queue`クラスを使用して実装されることが多いです。 +もう一方では、連結リストを使う必要がある代表的な場面は主に二分木とグラフである。スタックやキューについては、連結リストではなく、たいてい言語が提供する `stack` と `queue` を使う。 -**Q**:リスト`res = [0] * self.size()`を初期化すると、`res`の各要素は同じアドレスを参照しますか? +**Q**:`res = [[0]] * n` という操作で 2 次元リストを生成した場合、それぞれの `[0]` は独立していますか? -いいえ。しかし、この問題は二次元配列で発生します。例えば、二次元リスト`res = [[0]] * self.size()`を初期化すると、同じリスト`[0]`を複数回参照することになります。 +独立していない。この 2 次元リストでは、すべての `[0]` は実際には同一オブジェクトへの参照である。そのうちの 1 つを変更すると、対応するすべての要素が一緒に変化することがわかる。 -**Q**:ノードを削除する際、その後続ノードへの参照を断つ必要がありますか? +2 次元リスト内の各 `[0]` を独立させたい場合は、`res = [[0] for _ in range(n)]` を使って実現できる。この方式の原理は、独立した `[0]` リストオブジェクトを $n$ 個初期化していることにある。 -データ構造とアルゴリズム(問題解決)の観点から、プログラムのロジックが正しい限り、リンクを断たなくても問題ありません。標準ライブラリの観点から、リンクを断つ方が安全で論理的に明確です。リンクを断たず、削除されたノードが適切にリサイクルされない場合、後続ノードのメモリのリサイクルに影響を与える可能性があります。 +**Q**:`res = [0] * n` という操作で生成されたリストでは、それぞれの整数 0 は独立していますか? + +このリストでは、すべての整数 0 が同一オブジェクトへの参照である。これは、Python が小さな整数(通常は -5 から 256)に対してキャッシュプール機構を採用し、オブジェクトの再利用を最大化して性能を向上させているためである。 + +それらは同じオブジェクトを指しているが、それでもリスト内の各要素は独立して変更できる。これは、Python の整数が「イミュータブルオブジェクト」だからである。ある要素を変更するとき、実際には別のオブジェクトへの参照に切り替わるのであって、元のオブジェクトそのものを変更しているわけではない。 + +しかし、リスト要素が「ミュータブルオブジェクト」(たとえばリスト、辞書、クラスインスタンスなど)である場合は、ある要素を変更するとそのオブジェクト自体が直接変更され、そのオブジェクトを参照しているすべての要素に同じ変化が生じる。 diff --git a/ja/docs/chapter_backtracking/backtracking_algorithm.md b/ja/docs/chapter_backtracking/backtracking_algorithm.md index 8dd9848ef..20b4740d2 100644 --- a/ja/docs/chapter_backtracking/backtracking_algorithm.md +++ b/ja/docs/chapter_backtracking/backtracking_algorithm.md @@ -4,21 +4,21 @@ comments: true # 13.1   バックトラッキングアルゴリズム -バックトラッキングアルゴリズムは全数探索によって問題を解決する方法です。その核心概念は、初期状態から開始してすべての可能な解を総当たりで探索することです。アルゴリズムは正しいものを記録し、解が見つかるか、すべての可能な解が試されたが解が見つからないまで続けます。 +バックトラッキングアルゴリズム(backtracking algorithm)は、総当たりによって問題を解く手法です。その中核となる考え方は、初期状態から出発し、あり得るすべての解を力任せに探索し、正しい解に到達したらそれを記録し、解を見つけるか、考えられるすべての選択を試しても解が見つからなくなるまで続ける、というものです。 -バックトラッキングは通常「深さ優先探索」を使用して解空間を走査します。「二分木」の章で、前順、中順、後順走査はすべて深さ優先探索であることを述べました。次に、前順走査を使用してバックトラッキング問題を解決し、アルゴリズムの動作を段階的に理解していきます。 +バックトラッキングアルゴリズムでは、通常「深さ優先探索」を用いて解空間をたどります。「二分木」の章で述べたように、前順・中順・後順走査はいずれも深さ優先探索に属します。ここでは前順走査を使ってバックトラッキング問題を構成し、その仕組みを段階的に理解していきます。 -!!! question "例1" +!!! question "例題1" - 二分木が与えられた場合、値が $7$ のすべてのノードを検索して記録し、リストで返してください。 + 1 本の二分木が与えられたとき、値が $7$ のノードをすべて探索して記録し、そのノードのリストを返してください。 -この問題を解決するために、この木を前順で走査し、現在のノードの値が $7$ かどうかを確認します。そうであれば、ノードの値を結果リスト `res` に追加します。プロセスは以下の図に示されています: +この問題では、この木を前順走査し、現在のノードの値が $7$ かどうかを判定します。該当する場合は、そのノードの値を結果リスト `res` に追加します。関連する処理は下図と次のコードのとおりです。 === "Python" ```python title="preorder_traversal_i_compact.py" def pre_order(root: TreeNode): - """前順走査:例一""" + """前順走査:例題 1""" if root is None: return if root.val == 7: @@ -31,7 +31,7 @@ comments: true === "C++" ```cpp title="preorder_traversal_i_compact.cpp" - /* 前順走査:例1 */ + /* 前順走査:例題 1 */ void preOrder(TreeNode *root) { if (root == nullptr) { return; @@ -48,7 +48,7 @@ comments: true === "Java" ```java title="preorder_traversal_i_compact.java" - /* 前順走査:例1 */ + /* 前順走査:例題 1 */ void preOrder(TreeNode root) { if (root == null) { return; @@ -65,108 +65,223 @@ comments: true === "C#" ```csharp title="preorder_traversal_i_compact.cs" - [class]{preorder_traversal_i_compact}-[func]{PreOrder} + /* 前順走査:例題 1 */ + void PreOrder(TreeNode? root) { + if (root == null) { + return; + } + if (root.val == 7) { + // 解を記録 + res.Add(root); + } + PreOrder(root.left); + PreOrder(root.right); + } ``` === "Go" ```go title="preorder_traversal_i_compact.go" - [class]{}-[func]{preOrderI} + /* 前順走査:例題 1 */ + func preOrderI(root *TreeNode, res *[]*TreeNode) { + if root == nil { + return + } + if (root.Val).(int) == 7 { + // 解を記録 + *res = append(*res, root) + } + preOrderI(root.Left, res) + preOrderI(root.Right, res) + } ``` === "Swift" ```swift title="preorder_traversal_i_compact.swift" - [class]{}-[func]{preOrder} + /* 前順走査:例題 1 */ + func preOrder(root: TreeNode?) { + guard let root = root else { + return + } + if root.val == 7 { + // 解を記録 + res.append(root) + } + preOrder(root: root.left) + preOrder(root: root.right) + } ``` === "JS" ```javascript title="preorder_traversal_i_compact.js" - [class]{}-[func]{preOrder} + /* 前順走査:例題 1 */ + function preOrder(root, res) { + if (root === null) { + return; + } + if (root.val === 7) { + // 解を記録 + res.push(root); + } + preOrder(root.left, res); + preOrder(root.right, res); + } ``` === "TS" ```typescript title="preorder_traversal_i_compact.ts" - [class]{}-[func]{preOrder} + /* 前順走査:例題 1 */ + function preOrder(root: TreeNode | null, res: TreeNode[]): void { + if (root === null) { + return; + } + if (root.val === 7) { + // 解を記録 + res.push(root); + } + preOrder(root.left, res); + preOrder(root.right, res); + } ``` === "Dart" ```dart title="preorder_traversal_i_compact.dart" - [class]{}-[func]{preOrder} + /* 前順走査:例題 1 */ + void preOrder(TreeNode? root, List res) { + if (root == null) { + return; + } + if (root.val == 7) { + // 解を記録 + res.add(root); + } + preOrder(root.left, res); + preOrder(root.right, res); + } ``` === "Rust" ```rust title="preorder_traversal_i_compact.rs" - [class]{}-[func]{pre_order} + /* 前順走査:例題 1 */ + fn pre_order(res: &mut Vec>>, root: Option<&Rc>>) { + if root.is_none() { + return; + } + if let Some(node) = root { + if node.borrow().val == 7 { + // 解を記録 + res.push(node.clone()); + } + pre_order(res, node.borrow().left.as_ref()); + pre_order(res, node.borrow().right.as_ref()); + } + } ``` === "C" ```c title="preorder_traversal_i_compact.c" - [class]{}-[func]{preOrder} + /* 前順走査:例題 1 */ + void preOrder(TreeNode *root) { + if (root == NULL) { + return; + } + if (root->val == 7) { + // 解を記録 + res[resSize++] = root; + } + preOrder(root->left); + preOrder(root->right); + } ``` === "Kotlin" ```kotlin title="preorder_traversal_i_compact.kt" - [class]{}-[func]{preOrder} + /* 前順走査:例題 1 */ + fun preOrder(root: TreeNode?) { + if (root == null) { + return + } + if (root._val == 7) { + // 解を記録 + res!!.add(root) + } + preOrder(root.left) + preOrder(root.right) + } ``` === "Ruby" ```ruby title="preorder_traversal_i_compact.rb" - [class]{}-[func]{pre_order} + ### 前順走査:例題1 ### + def pre_order(root) + return unless root + + # 解を記録 + $res << root if root.val == 7 + + pre_order(root.left) + pre_order(root.right) + end ``` -![前順走査でのノード検索](backtracking_algorithm.assets/preorder_find_nodes.png){ class="animation-figure" } +??? pythontutor "コードの可視化" -

図 13-1   前順走査でのノード検索

+
+ -## 13.1.1   試行と後退 +![前順走査でノードを探索する](backtracking_algorithm.assets/preorder_find_nodes.png){ class="animation-figure" } -**解空間を探索する際に「試行」と「後退」戦略を使用するため、バックトラッキングアルゴリズムと呼ばれます**。探索中、満足のいく解を得るためにもはや進めない状態に遭遇するたびに、前の選択を取り消して前の状態に戻り、次の試行のために他の可能な選択を選択できるようにします。 +

図 13-1   前順走査でノードを探索する

-例1では、各ノードの訪問が「試行」を開始します。そして葉ノードを通過するか、`return` 文で親ノードに戻ることが「後退」を示唆します。 +## 13.1.1   試行と戻る -**後退は単に関数の戻り値ではないことに注意してください**。例1の問題を少し拡張して、それが何を意味するかを説明します。 +**バックトラッキングアルゴリズムと呼ばれるのは、解空間を探索する際に「試行」と「戻る」という戦略を取るためです**。探索中に、ある状態から先へ進めない、または条件を満たす解を得られないと分かった場合、アルゴリズムは直前の選択を取り消して前の状態へ戻り、別の選択肢を試します。 -!!! question "例2" +例題1では、各ノードへの訪問が 1 回の「試行」に対応し、葉ノードを越えるか親ノードへ戻る `return` は「戻る」を表します。 - 二分木で、値が $7$ のすべてのノードを検索し、すべてのマッチングノードについて、**ルートノードからそのノードまでのパスを返してください**。 +ここで強調しておきたいのは、**戻るとは関数の return だけを指すわけではない**という点です。これを説明するために、例題1を少し拡張します。 -例1のコードに基づいて、訪問したノードパスを記録するために `path` というリストを使用する必要があります。値が $7$ のノードに到達すると、`path` をコピーして結果リスト `res` に追加します。走査後、`res` にはすべての解が保持されます。コードは以下の通りです: +!!! question "例題2" + + 二分木の中で値が $7$ のノードをすべて探索し、**根ノードからそれらのノードまでの経路を返してください**。 + +例題1のコードを土台に、訪問済みノードの経路を記録するためのリスト `path` を導入します。値が $7$ のノードに到達したら、`path` をコピーして結果リスト `res` に追加します。走査が完了すると、`res` にはすべての解が保存されています。コードは次のとおりです。 === "Python" ```python title="preorder_traversal_ii_compact.py" def pre_order(root: TreeNode): - """前順走査:例二""" + """前順走査:例題 2""" if root is None: return - # 試行 + # 試す path.append(root) if root.val == 7: # 解を記録 res.append(list(path)) pre_order(root.left) pre_order(root.right) - # 撤回 + # バックトラック path.pop() ``` === "C++" ```cpp title="preorder_traversal_ii_compact.cpp" - /* 前順走査:例2 */ + /* 前順走査:例題 2 */ void preOrder(TreeNode *root) { if (root == nullptr) { return; } - // 試行 + // 試す path.push_back(root); if (root->val == 7) { // 解を記録 @@ -174,7 +289,7 @@ comments: true } preOrder(root->left); preOrder(root->right); - // 回退 + // バックトラック path.pop_back(); } ``` @@ -182,12 +297,12 @@ comments: true === "Java" ```java title="preorder_traversal_ii_compact.java" - /* 前順走査:例2 */ + /* 前順走査:例題 2 */ void preOrder(TreeNode root) { if (root == null) { return; } - // 試行 + // 試す path.add(root); if (root.val == 7) { // 解を記録 @@ -195,7 +310,7 @@ comments: true } preOrder(root.left); preOrder(root.right); - // 回退 + // バックトラック path.remove(path.size() - 1); } ``` @@ -203,69 +318,242 @@ comments: true === "C#" ```csharp title="preorder_traversal_ii_compact.cs" - [class]{preorder_traversal_ii_compact}-[func]{PreOrder} + /* 前順走査:例題 2 */ + void PreOrder(TreeNode? root) { + if (root == null) { + return; + } + // 試す + path.Add(root); + if (root.val == 7) { + // 解を記録 + res.Add(new List(path)); + } + PreOrder(root.left); + PreOrder(root.right); + // バックトラック + path.RemoveAt(path.Count - 1); + } ``` === "Go" ```go title="preorder_traversal_ii_compact.go" - [class]{}-[func]{preOrderII} + /* 前順走査:例題 2 */ + func preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) { + if root == nil { + return + } + // 試す + *path = append(*path, root) + if root.Val.(int) == 7 { + // 解を記録 + *res = append(*res, append([]*TreeNode{}, *path...)) + } + preOrderII(root.Left, res, path) + preOrderII(root.Right, res, path) + // バックトラック + *path = (*path)[:len(*path)-1] + } ``` === "Swift" ```swift title="preorder_traversal_ii_compact.swift" - [class]{}-[func]{preOrder} + /* 前順走査:例題 2 */ + func preOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 試す + path.append(root) + if root.val == 7 { + // 解を記録 + res.append(path) + } + preOrder(root: root.left) + preOrder(root: root.right) + // バックトラック + path.removeLast() + } ``` === "JS" ```javascript title="preorder_traversal_ii_compact.js" - [class]{}-[func]{preOrder} + /* 前順走査:例題 2 */ + function preOrder(root, path, res) { + if (root === null) { + return; + } + // 試す + path.push(root); + if (root.val === 7) { + // 解を記録 + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // バックトラック + path.pop(); + } ``` === "TS" ```typescript title="preorder_traversal_ii_compact.ts" - [class]{}-[func]{preOrder} + /* 前順走査:例題 2 */ + function preOrder( + root: TreeNode | null, + path: TreeNode[], + res: TreeNode[][] + ): void { + if (root === null) { + return; + } + // 試す + path.push(root); + if (root.val === 7) { + // 解を記録 + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // バックトラック + path.pop(); + } ``` === "Dart" ```dart title="preorder_traversal_ii_compact.dart" - [class]{}-[func]{preOrder} + /* 前順走査:例題 2 */ + void preOrder( + TreeNode? root, + List path, + List> res, + ) { + if (root == null) { + return; + } + + // 試す + path.add(root); + if (root.val == 7) { + // 解を記録 + res.add(List.from(path)); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // バックトラック + path.removeLast(); + } ``` === "Rust" ```rust title="preorder_traversal_ii_compact.rs" - [class]{}-[func]{pre_order} + /* 前順走査:例題 2 */ + fn pre_order( + res: &mut Vec>>>, + path: &mut Vec>>, + root: Option<&Rc>>, + ) { + if root.is_none() { + return; + } + if let Some(node) = root { + // 試す + path.push(node.clone()); + if node.borrow().val == 7 { + // 解を記録 + res.push(path.clone()); + } + pre_order(res, path, node.borrow().left.as_ref()); + pre_order(res, path, node.borrow().right.as_ref()); + // バックトラック + path.pop(); + } + } ``` === "C" ```c title="preorder_traversal_ii_compact.c" - [class]{}-[func]{preOrder} + /* 前順走査:例題 2 */ + void preOrder(TreeNode *root) { + if (root == NULL) { + return; + } + // 試す + path[pathSize++] = root; + if (root->val == 7) { + // 解を記録 + for (int i = 0; i < pathSize; ++i) { + res[resSize][i] = path[i]; + } + resSize++; + } + preOrder(root->left); + preOrder(root->right); + // バックトラック + pathSize--; + } ``` === "Kotlin" ```kotlin title="preorder_traversal_ii_compact.kt" - [class]{}-[func]{preOrder} + /* 前順走査:例題 2 */ + fun preOrder(root: TreeNode?) { + if (root == null) { + return + } + // 試す + path!!.add(root) + if (root._val == 7) { + // 解を記録 + res!!.add(path!!.toMutableList()) + } + preOrder(root.left) + preOrder(root.right) + // バックトラック + path!!.removeAt(path!!.size - 1) + } ``` === "Ruby" ```ruby title="preorder_traversal_ii_compact.rb" - [class]{}-[func]{pre_order} + ### 前順走査:例題2 ### + def pre_order(root) + return unless root + + # 試す + $path << root + + # 解を記録 + $res << $path.dup if root.val == 7 + + pre_order(root.left) + pre_order(root.right) + + # バックトラック + $path.pop + end ``` -各「試行」で、現在のノードを `path` に追加することでパスを記録します。「後退」が必要なときはいつでも、`path` からノードをポップして**この失敗した試行前の状態を復元します**。 +??? pythontutor "コードの可視化" -以下の図に示すプロセスを観察することで、**試行は「前進」のようで、後退は「元に戻す」のようです**。後者のペアは、対応するものに対する逆操作と見なすことができます。 +
+ + +各「試行」で現在のノードを `path` に追加して経路を記録し、「戻る」前にはそのノードを `path` から取り除き、**今回の試行前の状態を復元する**必要があります。 + +次の図に示す過程を見ると、**試行と戻るは「前進」と「取り消し」として理解できます**。この 2 つの操作は互いに逆向きです。 === "<1>" - ![試行と後退](backtracking_algorithm.assets/preorder_find_paths_step1.png){ class="animation-figure" } + ![試行と戻る](backtracking_algorithm.assets/preorder_find_paths_step1.png){ class="animation-figure" } === "<2>" ![preorder_find_paths_step2](backtracking_algorithm.assets/preorder_find_paths_step2.png){ class="animation-figure" } @@ -297,47 +585,47 @@ comments: true === "<11>" ![preorder_find_paths_step11](backtracking_algorithm.assets/preorder_find_paths_step11.png){ class="animation-figure" } -

図 13-2   試行と後退

+

図 13-2   試行と戻る

-## 13.1.2   剪定 +## 13.1.2   枝刈り -複雑なバックトラッキング問題は通常1つ以上の制約を含み、**これらは「剪定」によく使用されます**。 +複雑なバックトラッキング問題には、通常 1 つ以上の制約条件が含まれます。**制約条件は多くの場合「枝刈り」に利用できます**。 -!!! question "例3" +!!! question "例題3" - 二分木で、値が $7$ のすべてのノードを検索し、ルートからこれらのノードまでのパスを返してください。**ただし、パスには値が $3$ のノードを含まないという制限があります**。 + 二分木の中で値が $7$ のノードをすべて探索し、根ノードからそれらのノードまでの経路を返してください。**ただし、経路には値が $3$ のノードを含めてはいけません**。 -上記の制約を満たすために、**剪定操作を追加する必要があります**:検索プロセス中に、値が $3$ のノードに遭遇した場合、そのパスを通じてさらに検索することを即座に中止します。コードは以下の通りです: +上の制約条件を満たすために、**枝刈り操作を追加する必要があります**。探索中に値が $3$ のノードに出会った場合は、そこで早めに return し、それ以上探索を続けません。コードは次のとおりです。 === "Python" ```python title="preorder_traversal_iii_compact.py" def pre_order(root: TreeNode): - """前順走査:例三""" + """前順走査:例題 3""" # 枝刈り if root is None or root.val == 3: return - # 試行 + # 試す path.append(root) if root.val == 7: # 解を記録 res.append(list(path)) pre_order(root.left) pre_order(root.right) - # 撤回 + # バックトラック path.pop() ``` === "C++" ```cpp title="preorder_traversal_iii_compact.cpp" - /* 前順走査:例3 */ + /* 前順走査:例題 3 */ void preOrder(TreeNode *root) { - // 剪定 + // 枝刈り if (root == nullptr || root->val == 3) { return; } - // 試行 + // 試す path.push_back(root); if (root->val == 7) { // 解を記録 @@ -345,7 +633,7 @@ comments: true } preOrder(root->left); preOrder(root->right); - // 回退 + // バックトラック path.pop_back(); } ``` @@ -353,13 +641,13 @@ comments: true === "Java" ```java title="preorder_traversal_iii_compact.java" - /* 前順走査:例3 */ + /* 前順走査:例題 3 */ void preOrder(TreeNode root) { - // 剪定 + // 枝刈り if (root == null || root.val == 3) { return; } - // 試行 + // 試す path.add(root); if (root.val == 7) { // 解を記録 @@ -367,7 +655,7 @@ comments: true } preOrder(root.left); preOrder(root.right); - // 回退 + // バックトラック path.remove(path.size() - 1); } ``` @@ -375,117 +663,299 @@ comments: true === "C#" ```csharp title="preorder_traversal_iii_compact.cs" - [class]{preorder_traversal_iii_compact}-[func]{PreOrder} + /* 前順走査:例題 3 */ + void PreOrder(TreeNode? root) { + // 枝刈り + if (root == null || root.val == 3) { + return; + } + // 試す + path.Add(root); + if (root.val == 7) { + // 解を記録 + res.Add(new List(path)); + } + PreOrder(root.left); + PreOrder(root.right); + // バックトラック + path.RemoveAt(path.Count - 1); + } ``` === "Go" ```go title="preorder_traversal_iii_compact.go" - [class]{}-[func]{preOrderIII} + /* 前順走査:例題 3 */ + func preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) { + // 枝刈り + if root == nil || root.Val == 3 { + return + } + // 試す + *path = append(*path, root) + if root.Val.(int) == 7 { + // 解を記録 + *res = append(*res, append([]*TreeNode{}, *path...)) + } + preOrderIII(root.Left, res, path) + preOrderIII(root.Right, res, path) + // バックトラック + *path = (*path)[:len(*path)-1] + } ``` === "Swift" ```swift title="preorder_traversal_iii_compact.swift" - [class]{}-[func]{preOrder} + /* 前順走査:例題 3 */ + func preOrder(root: TreeNode?) { + // 枝刈り + guard let root = root, root.val != 3 else { + return + } + // 試す + path.append(root) + if root.val == 7 { + // 解を記録 + res.append(path) + } + preOrder(root: root.left) + preOrder(root: root.right) + // バックトラック + path.removeLast() + } ``` === "JS" ```javascript title="preorder_traversal_iii_compact.js" - [class]{}-[func]{preOrder} + /* 前順走査:例題 3 */ + function preOrder(root, path, res) { + // 枝刈り + if (root === null || root.val === 3) { + return; + } + // 試す + path.push(root); + if (root.val === 7) { + // 解を記録 + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // バックトラック + path.pop(); + } ``` === "TS" ```typescript title="preorder_traversal_iii_compact.ts" - [class]{}-[func]{preOrder} + /* 前順走査:例題 3 */ + function preOrder( + root: TreeNode | null, + path: TreeNode[], + res: TreeNode[][] + ): void { + // 枝刈り + if (root === null || root.val === 3) { + return; + } + // 試す + path.push(root); + if (root.val === 7) { + // 解を記録 + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // バックトラック + path.pop(); + } ``` === "Dart" ```dart title="preorder_traversal_iii_compact.dart" - [class]{}-[func]{preOrder} + /* 前順走査:例題 3 */ + void preOrder( + TreeNode? root, + List path, + List> res, + ) { + if (root == null || root.val == 3) { + return; + } + + // 試す + path.add(root); + if (root.val == 7) { + // 解を記録 + res.add(List.from(path)); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // バックトラック + path.removeLast(); + } ``` === "Rust" ```rust title="preorder_traversal_iii_compact.rs" - [class]{}-[func]{pre_order} + /* 前順走査:例題 3 */ + fn pre_order( + res: &mut Vec>>>, + path: &mut Vec>>, + root: Option<&Rc>>, + ) { + // 枝刈り + if root.is_none() || root.as_ref().unwrap().borrow().val == 3 { + return; + } + if let Some(node) = root { + // 試す + path.push(node.clone()); + if node.borrow().val == 7 { + // 解を記録 + res.push(path.clone()); + } + pre_order(res, path, node.borrow().left.as_ref()); + pre_order(res, path, node.borrow().right.as_ref()); + // バックトラック + path.pop(); + } + } ``` === "C" ```c title="preorder_traversal_iii_compact.c" - [class]{}-[func]{preOrder} + /* 前順走査:例題 3 */ + void preOrder(TreeNode *root) { + // 枝刈り + if (root == NULL || root->val == 3) { + return; + } + // 試す + path[pathSize++] = root; + if (root->val == 7) { + // 解を記録 + for (int i = 0; i < pathSize; i++) { + res[resSize][i] = path[i]; + } + resSize++; + } + preOrder(root->left); + preOrder(root->right); + // バックトラック + pathSize--; + } ``` === "Kotlin" ```kotlin title="preorder_traversal_iii_compact.kt" - [class]{}-[func]{preOrder} + /* 前順走査:例題 3 */ + fun preOrder(root: TreeNode?) { + // 枝刈り + if (root == null || root._val == 3) { + return + } + // 試す + path!!.add(root) + if (root._val == 7) { + // 解を記録 + res!!.add(path!!.toMutableList()) + } + preOrder(root.left) + preOrder(root.right) + // バックトラック + path!!.removeAt(path!!.size - 1) + } ``` === "Ruby" ```ruby title="preorder_traversal_iii_compact.rb" - [class]{}-[func]{pre_order} + ### 前順走査:例題3 ### + def pre_order(root) + # 枝刈り + return if !root || root.val == 3 + + # 試す + $path.append(root) + + # 解を記録 + $res << $path.dup if root.val == 7 + + pre_order(root.left) + pre_order(root.right) + + # バックトラック + $path.pop + end ``` -「剪定」は非常に生き生きとした名詞です。以下の図に示すように、検索プロセスで、**制約を満たさない検索分岐を「切り取り」ます**。さらなる不要な試行を避け、検索効率を向上させます。 +??? pythontutor "コードの可視化" -![制約に基づく剪定](backtracking_algorithm.assets/preorder_find_constrained_paths.png){ class="animation-figure" } +
+ -

図 13-3   制約に基づく剪定

+「枝刈り」は非常にイメージしやすい名称です。次の図のように、探索中に**制約条件を満たさない探索分岐を切り落とす**ことで、多くの無意味な試行を避け、探索効率を高められます。 + +![制約条件にもとづく枝刈り](backtracking_algorithm.assets/preorder_find_constrained_paths.png){ class="animation-figure" } + +

図 13-3   制約条件にもとづく枝刈り

## 13.1.3   フレームワークコード -今度は、バックトラッキングから「試行、後退、剪定」の主要なフレームワークを抽出して、コードの汎用性を向上させてみましょう。 +次に、バックトラッキングにおける「試行・戻る・枝刈り」の本体部分を抽出し、汎用性の高いコードフレームワークへまとめてみます。 -以下のフレームワークコードでは、`state` は問題の現在の状態を表し、`choices` は現在の状態で利用可能な選択肢を表します: +以下のフレームワークコードでは、`state` は問題の現在状態、`choices` はその状態で取り得る選択肢を表します。 === "Python" ```python title="" def backtrack(state: State, choices: list[choice], res: list[state]): - """バックトラッキングアルゴリズムフレームワーク""" - # 解かどうかを確認 + """バックトラッキングアルゴリズムのフレームワーク""" + # 解かどうかを判定 if is_solution(state): # 解を記録 record_solution(state, res) - # 検索を停止 + # これ以上探索しない return - # すべての選択肢を反復 + # すべての選択肢を走査 for choice in choices: - # 剪定:選択肢が有効かどうかを確認 + # 枝刈り: 選択が妥当かを判定 if is_valid(state, choice): - # 試行:選択を行い、状態を更新 + # 試行: 選択を行い、状態を更新 make_choice(state, choice) backtrack(state, choices, res) - # 後退:選択を取り消し、前の状態に戻す + # 戻る: 選択を取り消し、前の状態に戻す undo_choice(state, choice) ``` === "C++" ```cpp title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ void backtrack(State *state, vector &choices, vector &res) { - // 解かどうかを確認 + // 解かどうかを判定 if (isSolution(state)) { // 解を記録 recordSolution(state, res); - // 検索を停止 + // これ以上探索しない return; } - // すべての選択肢を反復 + // すべての選択肢を走査 for (Choice choice : choices) { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if (isValid(state, choice)) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state, choice); backtrack(state, choices, res); - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す undoChoice(state, choice); } } @@ -495,23 +965,23 @@ comments: true === "Java" ```java title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ void backtrack(State state, List choices, List res) { - // 解かどうかを確認 + // 解かどうかを判定 if (isSolution(state)) { // 解を記録 recordSolution(state, res); - // 検索を停止 + // これ以上探索しない return; } - // すべての選択肢を反復 + // すべての選択肢を走査 for (Choice choice : choices) { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if (isValid(state, choice)) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state, choice); backtrack(state, choices, res); - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す undoChoice(state, choice); } } @@ -521,23 +991,23 @@ comments: true === "C#" ```csharp title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ void Backtrack(State state, List choices, List res) { - // 解かどうかを確認 + // 解かどうかを判定 if (IsSolution(state)) { // 解を記録 RecordSolution(state, res); - // 検索を停止 + // これ以上探索しない return; } - // すべての選択肢を反復 + // すべての選択肢を走査 foreach (Choice choice in choices) { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if (IsValid(state, choice)) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 MakeChoice(state, choice); Backtrack(state, choices, res); - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す UndoChoice(state, choice); } } @@ -547,23 +1017,23 @@ comments: true === "Go" ```go title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ func backtrack(state *State, choices []Choice, res *[]State) { - // 解かどうかを確認 + // 解かどうかを判定 if isSolution(state) { // 解を記録 recordSolution(state, res) - // 検索を停止 + // これ以上探索しない return } - // すべての選択肢を反復 + // すべての選択肢を走査 for _, choice := range choices { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if isValid(state, choice) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state, choice) backtrack(state, choices, res) - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す undoChoice(state, choice) } } @@ -573,23 +1043,23 @@ comments: true === "Swift" ```swift title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ func backtrack(state: inout State, choices: [Choice], res: inout [State]) { - // 解かどうかを確認 + // 解かどうかを判定 if isSolution(state: state) { // 解を記録 recordSolution(state: state, res: &res) - // 検索を停止 + // これ以上探索しない return } - // すべての選択肢を反復 + // すべての選択肢を走査 for choice in choices { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if isValid(state: state, choice: choice) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state: &state, choice: choice) backtrack(state: &state, choices: choices, res: &res) - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す undoChoice(state: &state, choice: choice) } } @@ -599,23 +1069,23 @@ comments: true === "JS" ```javascript title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ function backtrack(state, choices, res) { - // 解かどうかを確認 + // 解かどうかを判定 if (isSolution(state)) { // 解を記録 recordSolution(state, res); - // 検索を停止 + // これ以上探索しない return; } - // すべての選択肢を反復 + // すべての選択肢を走査 for (let choice of choices) { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if (isValid(state, choice)) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state, choice); backtrack(state, choices, res); - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す undoChoice(state, choice); } } @@ -625,23 +1095,23 @@ comments: true === "TS" ```typescript title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ function backtrack(state: State, choices: Choice[], res: State[]): void { - // 解かどうかを確認 + // 解かどうかを判定 if (isSolution(state)) { // 解を記録 recordSolution(state, res); - // 検索を停止 + // これ以上探索しない return; } - // すべての選択肢を反復 + // すべての選択肢を走査 for (let choice of choices) { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if (isValid(state, choice)) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state, choice); backtrack(state, choices, res); - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す undoChoice(state, choice); } } @@ -651,23 +1121,23 @@ comments: true === "Dart" ```dart title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ void backtrack(State state, List, List res) { - // 解かどうかを確認 + // 解かどうかを判定 if (isSolution(state)) { // 解を記録 recordSolution(state, res); - // 検索を停止 + // これ以上探索しない return; } - // すべての選択肢を反復 + // すべての選択肢を走査 for (Choice choice in choices) { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if (isValid(state, choice)) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state, choice); backtrack(state, choices, res); - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す undoChoice(state, choice); } } @@ -677,23 +1147,23 @@ comments: true === "Rust" ```rust title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ fn backtrack(state: &mut State, choices: &Vec, res: &mut Vec) { - // 解かどうかを確認 + // 解かどうかを判定 if is_solution(state) { // 解を記録 record_solution(state, res); - // 検索を停止 + // これ以上探索しない return; } - // すべての選択肢を反復 + // すべての選択肢を走査 for choice in choices { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if is_valid(state, choice) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 make_choice(state, choice); backtrack(state, choices, res); - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す undo_choice(state, choice); } } @@ -703,23 +1173,23 @@ comments: true === "C" ```c title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ void backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) { - // 解かどうかを確認 + // 解かどうかを判定 if (isSolution(state)) { // 解を記録 recordSolution(state, res, numRes); - // 検索を停止 + // これ以上探索しない return; } - // すべての選択肢を反復 + // すべての選択肢を走査 for (int i = 0; i < numChoices; i++) { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if (isValid(state, &choices[i])) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state, &choices[i]); backtrack(state, choices, numChoices, res, numRes); - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す undoChoice(state, &choices[i]); } } @@ -729,23 +1199,23 @@ comments: true === "Kotlin" ```kotlin title="" - /* バックトラッキングアルゴリズムフレームワーク */ + /* バックトラッキングアルゴリズムのフレームワーク */ fun backtrack(state: State?, choices: List, res: List?) { - // 解かどうかを確認 + // 解かどうかを判定 if (isSolution(state)) { // 解を記録 recordSolution(state, res) - // 検索を停止 + // これ以上探索しない return } - // すべての選択肢を反復 + // すべての選択肢を走査 for (choice in choices) { - // 剪定:選択肢が有効かどうかを確認 + // 枝刈り: 選択が妥当かを判定 if (isValid(state, choice)) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state, choice) backtrack(state, choices, res) - // 後退:選択を取り消し、前の状態に戻す + // 戻る: 選択を取り消し、前の状態に戻す undoChoice(state, choice) } } @@ -755,30 +1225,30 @@ comments: true === "Ruby" ```ruby title="" - ### バックトラッキングアルゴリズムフレームワーク ### + ### バックトラッキングアルゴリズムのフレームワーク ### def backtrack(state, choices, res) - # 解かどうかを確認 + # 解かどうかを判定 if is_solution?(state) # 解を記録 record_solution(state, res) return end - # すべての選択肢を反復 + # すべての選択肢を走査 for choice in choices - # 剪定:選択肢が有効かどうかを確認 + # 枝刈り: 選択が妥当かを判定 if is_valid?(state, choice) - # 試行:選択を行い、状態を更新 + # 試行: 選択を行い、状態を更新 make_choice(state, choice) backtrack(state, choices, res) - # 後退:選択を取り消し、前の状態に戻す + # 戻る: 選択を取り消し、前の状態に戻す undo_choice(state, choice) end end end ``` -次に、フレームワークコードに基づいて例題 3 を解きます。状態 `state` はノードの走査経路を表し、選択肢 `choices` は現在ノードの左子ノードと右子ノード、結果 `res` は経路リストです: +次に、このフレームワークコードを用いて例題3を解きます。状態 `state` はノードの走査経路、選択肢 `choices` は現在のノードの左子ノードと右子ノード、結果 `res` は経路のリストです。 === "Python" @@ -792,7 +1262,7 @@ comments: true res.append(list(state)) def is_valid(state: list[TreeNode], choice: TreeNode) -> bool: - """現在の状態下で選択が合法かどうかを判定""" + """現在の状態で、この選択が有効かどうかを判定""" return choice is not None and choice.val != 3 def make_choice(state: list[TreeNode], choice: TreeNode): @@ -800,26 +1270,26 @@ comments: true state.append(choice) def undo_choice(state: list[TreeNode], choice: TreeNode): - """状態を復元""" + """状態を元に戻す""" state.pop() def backtrack( state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]] ): - """バックトラッキングアルゴリズム:例三""" - # 解かどうかをチェック + """バックトラッキング:例題 3""" + # 解かどうかを確認 if is_solution(state): # 解を記録 record_solution(state, res) # すべての選択肢を走査 for choice in choices: - # 枝刈り:選択が合法かどうかをチェック + # 枝刈り:選択が妥当かを確認する if is_valid(state, choice): - # 試行:選択を行い、状態を更新 + # 試行: 選択を行い、状態を更新 make_choice(state, choice) - # 次の選択ラウンドに進む + # 次の選択へ進む backtrack(state, [choice.left, choice.right], res) - # 撤回:選択を取り消し、前の状態に復元 + # バックトラック:選択を取り消し、前の状態に戻す undo_choice(state, choice) ``` @@ -836,7 +1306,7 @@ comments: true res.push_back(state); } - /* 現在の状態下で選択が合法かどうかを判定 */ + /* 現在の状態で、この選択が有効かどうかを判定 */ bool isValid(vector &state, TreeNode *choice) { return choice != nullptr && choice->val != 3; } @@ -846,28 +1316,28 @@ comments: true state.push_back(choice); } - /* 状態を復元 */ + /* 状態を元に戻す */ void undoChoice(vector &state, TreeNode *choice) { state.pop_back(); } - /* バックトラッキングアルゴリズム:例3 */ + /* バックトラッキング:例題 3 */ void backtrack(vector &state, vector &choices, vector> &res) { - // 解かどうかをチェック + // 解かどうかを確認 if (isSolution(state)) { // 解を記録 recordSolution(state, res); } // すべての選択肢を走査 for (TreeNode *choice : choices) { - // 剪定:選択が合法かどうかをチェック + // 枝刈り:選択が妥当かを確認する if (isValid(state, choice)) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state, choice); - // 次のラウンドの選択に進む + // 次の選択へ進む vector nextChoices{choice->left, choice->right}; backtrack(state, nextChoices, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す undoChoice(state, choice); } } @@ -887,7 +1357,7 @@ comments: true res.add(new ArrayList<>(state)); } - /* 現在の状態下で選択が合法かどうかを判定 */ + /* 現在の状態で、この選択が有効かどうかを判定 */ boolean isValid(List state, TreeNode choice) { return choice != null && choice.val != 3; } @@ -897,27 +1367,27 @@ comments: true state.add(choice); } - /* 状態を復元 */ + /* 状態を元に戻す */ void undoChoice(List state, TreeNode choice) { state.remove(state.size() - 1); } - /* バックトラッキングアルゴリズム:例3 */ + /* バックトラッキング:例題 3 */ void backtrack(List state, List choices, List> res) { - // 解かどうかをチェック + // 解かどうかを確認 if (isSolution(state)) { // 解を記録 recordSolution(state, res); } // すべての選択肢を走査 for (TreeNode choice : choices) { - // 剪定:選択が合法かどうかをチェック + // 枝刈り:選択が妥当かを確認する if (isValid(state, choice)) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 makeChoice(state, choice); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, Arrays.asList(choice.left, choice.right), res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す undoChoice(state, choice); } } @@ -927,232 +1397,607 @@ comments: true === "C#" ```csharp title="preorder_traversal_iii_template.cs" - [class]{preorder_traversal_iii_template}-[func]{IsSolution} + /* 現在の状態が解かどうかを判定 */ + bool IsSolution(List state) { + return state.Count != 0 && state[^1].val == 7; + } - [class]{preorder_traversal_iii_template}-[func]{RecordSolution} + /* 解を記録 */ + void RecordSolution(List state, List> res) { + res.Add(new List(state)); + } - [class]{preorder_traversal_iii_template}-[func]{IsValid} + /* 現在の状態で、この選択が有効かどうかを判定 */ + bool IsValid(List state, TreeNode choice) { + return choice != null && choice.val != 3; + } - [class]{preorder_traversal_iii_template}-[func]{MakeChoice} + /* 状態を更新 */ + void MakeChoice(List state, TreeNode choice) { + state.Add(choice); + } - [class]{preorder_traversal_iii_template}-[func]{UndoChoice} + /* 状態を元に戻す */ + void UndoChoice(List state, TreeNode choice) { + state.RemoveAt(state.Count - 1); + } - [class]{preorder_traversal_iii_template}-[func]{Backtrack} + /* バックトラッキング:例題 3 */ + void Backtrack(List state, List choices, List> res) { + // 解かどうかを確認 + if (IsSolution(state)) { + // 解を記録 + RecordSolution(state, res); + } + // すべての選択肢を走査 + foreach (TreeNode choice in choices) { + // 枝刈り:選択が妥当かを確認する + if (IsValid(state, choice)) { + // 試行: 選択を行い、状態を更新 + MakeChoice(state, choice); + // 次の選択へ進む + Backtrack(state, [choice.left!, choice.right!], res); + // バックトラック:選択を取り消し、前の状態に戻す + UndoChoice(state, choice); + } + } + } ``` === "Go" ```go title="preorder_traversal_iii_template.go" - [class]{}-[func]{isSolution} + /* 現在の状態が解かどうかを判定 */ + func isSolution(state *[]*TreeNode) bool { + return len(*state) != 0 && (*state)[len(*state)-1].Val == 7 + } - [class]{}-[func]{recordSolution} + /* 解を記録 */ + func recordSolution(state *[]*TreeNode, res *[][]*TreeNode) { + *res = append(*res, append([]*TreeNode{}, *state...)) + } - [class]{}-[func]{isValid} + /* 現在の状態で、この選択が有効かどうかを判定 */ + func isValid(state *[]*TreeNode, choice *TreeNode) bool { + return choice != nil && choice.Val != 3 + } - [class]{}-[func]{makeChoice} + /* 状態を更新 */ + func makeChoice(state *[]*TreeNode, choice *TreeNode) { + *state = append(*state, choice) + } - [class]{}-[func]{undoChoice} + /* 状態を元に戻す */ + func undoChoice(state *[]*TreeNode, choice *TreeNode) { + *state = (*state)[:len(*state)-1] + } - [class]{}-[func]{backtrackIII} + /* バックトラッキング:例題 3 */ + func backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) { + // 解かどうかを確認 + if isSolution(state) { + // 解を記録 + recordSolution(state, res) + } + // すべての選択肢を走査 + for _, choice := range *choices { + // 枝刈り:選択が妥当かを確認する + if isValid(state, choice) { + // 試行: 選択を行い、状態を更新 + makeChoice(state, choice) + // 次の選択へ進む + temp := make([]*TreeNode, 0) + temp = append(temp, choice.Left, choice.Right) + backtrackIII(state, &temp, res) + // バックトラック:選択を取り消し、前の状態に戻す + undoChoice(state, choice) + } + } + } ``` === "Swift" ```swift title="preorder_traversal_iii_template.swift" - [class]{}-[func]{isSolution} + /* 現在の状態が解かどうかを判定 */ + func isSolution(state: [TreeNode]) -> Bool { + !state.isEmpty && state.last!.val == 7 + } - [class]{}-[func]{recordSolution} + /* 解を記録 */ + func recordSolution(state: [TreeNode], res: inout [[TreeNode]]) { + res.append(state) + } - [class]{}-[func]{isValid} + /* 現在の状態で、この選択が有効かどうかを判定 */ + func isValid(state: [TreeNode], choice: TreeNode?) -> Bool { + choice != nil && choice!.val != 3 + } - [class]{}-[func]{makeChoice} + /* 状態を更新 */ + func makeChoice(state: inout [TreeNode], choice: TreeNode) { + state.append(choice) + } - [class]{}-[func]{undoChoice} + /* 状態を元に戻す */ + func undoChoice(state: inout [TreeNode], choice: TreeNode) { + state.removeLast() + } - [class]{}-[func]{backtrack} + /* バックトラッキング:例題 3 */ + func backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) { + // 解かどうかを確認 + if isSolution(state: state) { + recordSolution(state: state, res: &res) + } + // すべての選択肢を走査 + for choice in choices { + // 枝刈り:選択が妥当かを確認する + if isValid(state: state, choice: choice) { + // 試行: 選択を行い、状態を更新 + makeChoice(state: &state, choice: choice) + // 次の選択へ進む + backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res) + // バックトラック:選択を取り消し、前の状態に戻す + undoChoice(state: &state, choice: choice) + } + } + } ``` === "JS" ```javascript title="preorder_traversal_iii_template.js" - [class]{}-[func]{isSolution} + /* 現在の状態が解かどうかを判定 */ + function isSolution(state) { + return state && state[state.length - 1]?.val === 7; + } - [class]{}-[func]{recordSolution} + /* 解を記録 */ + function recordSolution(state, res) { + res.push([...state]); + } - [class]{}-[func]{isValid} + /* 現在の状態で、この選択が有効かどうかを判定 */ + function isValid(state, choice) { + return choice !== null && choice.val !== 3; + } - [class]{}-[func]{makeChoice} + /* 状態を更新 */ + function makeChoice(state, choice) { + state.push(choice); + } - [class]{}-[func]{undoChoice} + /* 状態を元に戻す */ + function undoChoice(state) { + state.pop(); + } - [class]{}-[func]{backtrack} + /* バックトラッキング:例題 3 */ + function backtrack(state, choices, res) { + // 解かどうかを確認 + if (isSolution(state)) { + // 解を記録 + recordSolution(state, res); + } + // すべての選択肢を走査 + for (const choice of choices) { + // 枝刈り:選択が妥当かを確認する + if (isValid(state, choice)) { + // 試行: 選択を行い、状態を更新 + makeChoice(state, choice); + // 次の選択へ進む + backtrack(state, [choice.left, choice.right], res); + // バックトラック:選択を取り消し、前の状態に戻す + undoChoice(state); + } + } + } ``` === "TS" ```typescript title="preorder_traversal_iii_template.ts" - [class]{}-[func]{isSolution} + /* 現在の状態が解かどうかを判定 */ + function isSolution(state: TreeNode[]): boolean { + return state && state[state.length - 1]?.val === 7; + } - [class]{}-[func]{recordSolution} + /* 解を記録 */ + function recordSolution(state: TreeNode[], res: TreeNode[][]): void { + res.push([...state]); + } - [class]{}-[func]{isValid} + /* 現在の状態で、この選択が有効かどうかを判定 */ + function isValid(state: TreeNode[], choice: TreeNode): boolean { + return choice !== null && choice.val !== 3; + } - [class]{}-[func]{makeChoice} + /* 状態を更新 */ + function makeChoice(state: TreeNode[], choice: TreeNode): void { + state.push(choice); + } - [class]{}-[func]{undoChoice} + /* 状態を元に戻す */ + function undoChoice(state: TreeNode[]): void { + state.pop(); + } - [class]{}-[func]{backtrack} + /* バックトラッキング:例題 3 */ + function backtrack( + state: TreeNode[], + choices: TreeNode[], + res: TreeNode[][] + ): void { + // 解かどうかを確認 + if (isSolution(state)) { + // 解を記録 + recordSolution(state, res); + } + // すべての選択肢を走査 + for (const choice of choices) { + // 枝刈り:選択が妥当かを確認する + if (isValid(state, choice)) { + // 試行: 選択を行い、状態を更新 + makeChoice(state, choice); + // 次の選択へ進む + backtrack(state, [choice.left, choice.right], res); + // バックトラック:選択を取り消し、前の状態に戻す + undoChoice(state); + } + } + } ``` === "Dart" ```dart title="preorder_traversal_iii_template.dart" - [class]{}-[func]{isSolution} + /* 現在の状態が解かどうかを判定 */ + bool isSolution(List state) { + return state.isNotEmpty && state.last.val == 7; + } - [class]{}-[func]{recordSolution} + /* 解を記録 */ + void recordSolution(List state, List> res) { + res.add(List.from(state)); + } - [class]{}-[func]{isValid} + /* 現在の状態で、この選択が有効かどうかを判定 */ + bool isValid(List state, TreeNode? choice) { + return choice != null && choice.val != 3; + } - [class]{}-[func]{makeChoice} + /* 状態を更新 */ + void makeChoice(List state, TreeNode? choice) { + state.add(choice!); + } - [class]{}-[func]{undoChoice} + /* 状態を元に戻す */ + void undoChoice(List state, TreeNode? choice) { + state.removeLast(); + } - [class]{}-[func]{backtrack} + /* バックトラッキング:例題 3 */ + void backtrack( + List state, + List choices, + List> res, + ) { + // 解かどうかを確認 + if (isSolution(state)) { + // 解を記録 + recordSolution(state, res); + } + // すべての選択肢を走査 + for (TreeNode? choice in choices) { + // 枝刈り:選択が妥当かを確認する + if (isValid(state, choice)) { + // 試行: 選択を行い、状態を更新 + makeChoice(state, choice); + // 次の選択へ進む + backtrack(state, [choice!.left, choice.right], res); + // バックトラック:選択を取り消し、前の状態に戻す + undoChoice(state, choice); + } + } + } ``` === "Rust" ```rust title="preorder_traversal_iii_template.rs" - [class]{}-[func]{is_solution} + /* 現在の状態が解かどうかを判定 */ + fn is_solution(state: &mut Vec>>) -> bool { + return !state.is_empty() && state.last().unwrap().borrow().val == 7; + } - [class]{}-[func]{record_solution} + /* 解を記録 */ + fn record_solution( + state: &mut Vec>>, + res: &mut Vec>>>, + ) { + res.push(state.clone()); + } - [class]{}-[func]{is_valid} + /* 現在の状態で、この選択が有効かどうかを判定 */ + fn is_valid(_: &mut Vec>>, choice: Option<&Rc>>) -> bool { + return choice.is_some() && choice.unwrap().borrow().val != 3; + } - [class]{}-[func]{make_choice} + /* 状態を更新 */ + fn make_choice(state: &mut Vec>>, choice: Rc>) { + state.push(choice); + } - [class]{}-[func]{undo_choice} + /* 状態を元に戻す */ + fn undo_choice(state: &mut Vec>>, _: Rc>) { + state.pop(); + } - [class]{}-[func]{backtrack} + /* バックトラッキング:例題 3 */ + fn backtrack( + state: &mut Vec>>, + choices: &Vec>>>, + res: &mut Vec>>>, + ) { + // 解かどうかを確認 + if is_solution(state) { + // 解を記録 + record_solution(state, res); + } + // すべての選択肢を走査 + for &choice in choices.iter() { + // 枝刈り:選択が妥当かを確認する + if is_valid(state, choice) { + // 試行: 選択を行い、状態を更新 + make_choice(state, choice.unwrap().clone()); + // 次の選択へ進む + backtrack( + state, + &vec![ + choice.unwrap().borrow().left.as_ref(), + choice.unwrap().borrow().right.as_ref(), + ], + res, + ); + // バックトラック:選択を取り消し、前の状態に戻す + undo_choice(state, choice.unwrap().clone()); + } + } + } ``` === "C" ```c title="preorder_traversal_iii_template.c" - [class]{}-[func]{isSolution} + /* 現在の状態が解かどうかを判定 */ + bool isSolution(void) { + return pathSize > 0 && path[pathSize - 1]->val == 7; + } - [class]{}-[func]{recordSolution} + /* 解を記録 */ + void recordSolution(void) { + for (int i = 0; i < pathSize; i++) { + res[resSize][i] = path[i]; + } + resSize++; + } - [class]{}-[func]{isValid} + /* 現在の状態で、この選択が有効かどうかを判定 */ + bool isValid(TreeNode *choice) { + return choice != NULL && choice->val != 3; + } - [class]{}-[func]{makeChoice} + /* 状態を更新 */ + void makeChoice(TreeNode *choice) { + path[pathSize++] = choice; + } - [class]{}-[func]{undoChoice} + /* 状態を元に戻す */ + void undoChoice(void) { + pathSize--; + } - [class]{}-[func]{backtrack} + /* バックトラッキング:例題 3 */ + void backtrack(TreeNode *choices[2]) { + // 解かどうかを確認 + if (isSolution()) { + // 解を記録 + recordSolution(); + } + // すべての選択肢を走査 + for (int i = 0; i < 2; i++) { + TreeNode *choice = choices[i]; + // 枝刈り:選択が妥当かを確認する + if (isValid(choice)) { + // 試行: 選択を行い、状態を更新 + makeChoice(choice); + // 次の選択へ進む + TreeNode *nextChoices[2] = {choice->left, choice->right}; + backtrack(nextChoices); + // バックトラック:選択を取り消し、前の状態に戻す + undoChoice(); + } + } + } ``` === "Kotlin" ```kotlin title="preorder_traversal_iii_template.kt" - [class]{}-[func]{isSolution} + /* 現在の状態が解かどうかを判定 */ + fun isSolution(state: MutableList): Boolean { + return state.isNotEmpty() && state[state.size - 1]?._val == 7 + } - [class]{}-[func]{recordSolution} + /* 解を記録 */ + fun recordSolution(state: MutableList?, res: MutableList?>) { + res.add(state!!.toMutableList()) + } - [class]{}-[func]{isValid} + /* 現在の状態で、この選択が有効かどうかを判定 */ + fun isValid(state: MutableList?, choice: TreeNode?): Boolean { + return choice != null && choice._val != 3 + } - [class]{}-[func]{makeChoice} + /* 状態を更新 */ + fun makeChoice(state: MutableList, choice: TreeNode?) { + state.add(choice) + } - [class]{}-[func]{undoChoice} + /* 状態を元に戻す */ + fun undoChoice(state: MutableList, choice: TreeNode?) { + state.removeLast() + } - [class]{}-[func]{backtrack} + /* バックトラッキング:例題 3 */ + fun backtrack( + state: MutableList, + choices: MutableList, + res: MutableList?> + ) { + // 解かどうかを確認 + if (isSolution(state)) { + // 解を記録 + recordSolution(state, res) + } + // すべての選択肢を走査 + for (choice in choices) { + // 枝刈り:選択が妥当かを確認する + if (isValid(state, choice)) { + // 試行: 選択を行い、状態を更新 + makeChoice(state, choice) + // 次の選択へ進む + backtrack(state, mutableListOf(choice!!.left, choice.right), res) + // バックトラック:選択を取り消し、前の状態に戻す + undoChoice(state, choice) + } + } + } ``` === "Ruby" ```ruby title="preorder_traversal_iii_template.rb" - [class]{}-[func]{is_solution} + ### 現在の状態が解かどうかを判定 ### + def is_solution?(state) + !state.empty? && state.last.val == 7 + end - [class]{}-[func]{record_solution} + ### 解を記録 ### + def record_solution(state, res) + res << state.dup + end - [class]{}-[func]{is_valid} + ### 現在の状態で、この選択が妥当かを判定 ### + def is_valid?(state, choice) + choice && choice.val != 3 + end - [class]{}-[func]{make_choice} + ### 状態を更新 ### + def make_choice(state, choice) + state << choice + end - [class]{}-[func]{undo_choice} + ### 状態を復元 ### + def undo_choice(state, choice) + state.pop + end - [class]{}-[func]{backtrack} + ### バックトラッキング法:例題3 ### + def backtrack(state, choices, res) + # 解かどうかを確認 + record_solution(state, res) if is_solution?(state) + + # すべての選択肢を走査 + for choice in choices + # 枝刈り:選択が妥当かを確認する + if is_valid?(state, choice) + # 試行: 選択を行い、状態を更新 + make_choice(state, choice) + # 次の選択へ進む + backtrack(state, [choice.left, choice.right], res) + # バックトラック:選択を取り消し、前の状態に戻す + undo_choice(state, choice) + end + end + end ``` -問題文の意味に従い、値が $7$ のノードを見つけた後も探索を続ける必要があります。**したがって、解を記録した後の `return` 文を削除する必要があります**。次の図は、`return` 文を保持する場合と削除する場合の探索過程の比較です。 +??? pythontutor "コードの可視化" -![returnを保持する場合と削除する場合の探索過程の比較](backtracking_algorithm.assets/backtrack_remove_return_or_not.png){ class="animation-figure" } +
+ -

図 13-4   returnを保持する場合と削除する場合の探索過程の比較

+問題の条件より、値が $7$ のノードを見つけた後も探索を続ける必要があります。**そのため、解を記録した後の `return` 文は削除しなければなりません**。次の図は、`return` 文を残す場合と削除する場合の探索過程を比較したものです。 -前順走査に基づくコード実装と比べると、バックトラッキングアルゴリズムのフレームワークに基づく実装はやや冗長に見えますが、汎用性はより高いです。実際、**多くのバックトラッキング問題はこのフレームワークの下で解くことができます**。具体的な問題に応じて `state` と `choices` を定義し、フレームワーク内の各メソッドを実装すればよいのです。 +![return を残す場合と削除する場合の探索過程の比較](backtracking_algorithm.assets/backtrack_remove_return_or_not.png){ class="animation-figure" } + +

図 13-4   return を残す場合と削除する場合の探索過程の比較

+ +前順走査にもとづく実装と比べると、バックトラッキングアルゴリズムのフレームワークにもとづく実装はやや冗長に見えますが、汎用性に優れています。実際、**多くのバックトラッキング問題はこのフレームワークで解けます**。具体的な問題に応じて `state` と `choices` を定義し、各メソッドを実装すれば十分です。 ## 13.1.4   よく使われる用語 -アルゴリズム問題をより明確に分析するために、バックトラッキングアルゴリズムでよく使われる用語の意味をまとめ、例題 3 の対応例を以下の表に示します。 +アルゴリズム問題をより明確に分析するために、バックトラッキングでよく使われる用語の意味を整理し、例題3に対応する例を次の表にまとめます。 -

表 13-1   バックトラッキングアルゴリズムでよく使われる用語

+

表 13-1   よく使われるバックトラッキング用語

-| 名称 | 定義 | 例題 3 | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | -| 解(solution) | 解は問題の特定条件を満たす答えであり、1 つまたは複数存在する可能性がある | 根ノードからノード $7$ までの制約条件を満たすすべての経路 | -| 制約条件(constraint) | 制約条件は、解の実現可能性を制限する条件であり、通常は枝刈りに使用される | 経路にノード $3$ を含まない | -| 状態(state) | 状態は、ある時点での問題の状況を表し、これまでに行った選択を含む | 現在訪問したノード経路、すなわち `path` ノードリスト | -| 試行(attempt) | 試行は、利用可能な選択肢に基づいて解空間を探索する過程であり、選択を行い、状態を更新し、解かどうかを確認する | 左(右)子ノードを再帰的に訪問し、ノードを `path` に追加し、ノードの値が $7$ かを確認する | -| バックトラック(backtracking) | 制約条件を満たさない状態に遭遇した場合、以前の選択を取り消して前の状態に戻ること | 葉ノードを越えたとき、探索終了、値が $3$ のノードに遭遇したとき探索を終了し、関数が戻る | -| 枝刈り(pruning) | 問題の特性や制約条件に基づき、無意味な探索経路を避ける方法であり、探索効率を向上させる | 値が $3$ のノードに遭遇した場合、それ以上探索しない | +| 用語 | 定義 | 例題3 | +| ---------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| 解(solution) | 問題の特定の条件を満たす答えであり、1 つまたは複数存在し得る | 根ノードからノード $7$ までの、制約条件を満たすすべての経路 | +| 制約条件(constraint) | 解の実現可能性を制限する条件であり、通常は枝刈りに用いられる | 経路にノード $3$ を含まないこと | +| 状態(state) | ある時点における問題の状況を表し、すでに行った選択を含む | 現在までに訪問したノードの経路、すなわち `path` ノードリスト | +| 試行(attempt) | 利用可能な選択肢にもとづいて解空間を探索する過程であり、選択、状態更新、解判定を含む | 左右の子ノードを再帰的に訪問し、ノードを `path` に追加し、値が $7$ か判定する | +| 戻る(backtracking) | 制約条件を満たさない状態に出会ったとき、それまでの選択を取り消して前の状態へ戻ること | 葉ノードを越えたとき、ノード訪問を終えたとき、値が $3$ のノードに出会ったときに探索を終了し、関数から戻る | +| 枝刈り(pruning) | 問題の性質や制約条件にもとづき、無意味な探索経路を避ける方法であり、探索効率を高める | 値が $3$ のノードに出会ったら、それ以上探索しない |
!!! tip - 問題、解、状態などの概念は一般的なものであり、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムにも関係します。 + 問題、解、状態などの概念は汎用的であり、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムにも共通して現れます。 -## 13.1.5   長所と限界 +## 13.1.5   利点と限界 -バックトラッキングアルゴリズムは本質的に深さ優先探索(DFS)アルゴリズムの一種であり、条件を満たす解を見つけるまであらゆる可能な解を試みます。この方法の利点は、すべての可能な解を見つけられる点であり、適切な枝刈りを行えば効率が高いことです。 +バックトラッキングアルゴリズムの本質は深さ優先探索です。条件を満たす解を見つけるまで、あり得るすべての解を試します。この方法の利点は、考えられるすべての解を見つけられることであり、適切な枝刈りを行えば高い効率を発揮します。 -しかし、大規模または複雑な問題を扱う場合、**バックトラッキングアルゴリズムの実行効率は許容できないほど低下する可能性があります**。 +しかし、大規模または複雑な問題を扱う場合、**バックトラッキングアルゴリズムの実行効率は受け入れがたいことがあります**。 -- **時間**:バックトラッキングアルゴリズムは通常、状態空間のすべての可能性を探索する必要があり、時間計算量は指数オーダーまたは階乗オーダーに達する可能性があります。 -- **空間**:再帰呼び出し中に現在の状態(例:経路、枝刈り用の補助変数など)を保存する必要があり、深さが大きい場合、空間の使用量が増加します。 +- **時間**:バックトラッキングアルゴリズムでは通常、状態空間のすべての可能性をたどる必要があり、時間計算量は指数時間や階乗時間に達することがあります。 +- **空間**:再帰呼び出しの過程では現在の状態(たとえば経路や枝刈り用の補助変数など)を保持する必要があり、深さが大きいと空間使用量も大きくなります。 -それでもなお、**バックトラッキングアルゴリズムは特定の探索問題や制約満足問題の最良の解法であることが多いです**。これらの問題では、どの選択が有効な解を生成するかを予測できないため、すべての可能な選択を試す必要があります。このような場合、**効率の最適化が鍵**となります。一般的な最適化手法は次の 2 つです。 +それでもなお、**バックトラッキングアルゴリズムは一部の探索問題や制約充足問題に対する最良の解法です**。この種の問題では、どの選択が有効な解を生むかを事前に予測できないため、可能な選択肢をすべてたどる必要があります。このときの鍵は**いかに効率を最適化するか**であり、代表的な方法は 2 つあります。 -- **枝刈り**:解を生成しないことが確実な経路を避けることで、時間と空間を節約します。 -- **ヒューリスティック探索**:探索中に戦略や評価値を導入し、有効な解を生成する可能性が高い経路を優先的に探索します。 +- **枝刈り**:解が生じないことが確実な経路を探索しないことで、時間と空間を節約する。 +- **ヒューリスティック探索**:探索中に何らかの戦略や推定値を導入し、有効な解を生みやすい経路を優先的に探索する。 -## 13.1.6   バックトラッキングの典型的な例題 +## 13.1.6   バックトラッキングの典型例題 -バックトラッキングアルゴリズムは、多くの探索問題、制約満足問題、組合せ最適化問題を解くのに使用できます。 +バックトラッキングアルゴリズムは、多くの探索問題、制約充足問題、組合せ最適化問題の解決に利用できます。 **探索問題**:この種の問題の目標は、特定の条件を満たす解を見つけることです。 -- 全順列問題:与えられた集合のすべての可能な順列を求める。 -- 部分和問題:与えられた集合と目標和に対して、和が目標値になるすべての部分集合を求める。 -- ハノイの塔:3 本の柱と異なるサイズの円盤があり、すべての円盤を 1 本の柱から別の柱に移す。1 回に 1 枚しか動かせず、大きな円盤を小さい円盤の上に置くことはできない。 +- 全順列問題:ある集合が与えられたとき、考えられるすべての順列を求める。 +- 部分和問題:ある集合と目標和が与えられたとき、和が目標値となるすべての部分集合を見つける。 +- ハノイの塔問題:3 本の柱と大きさの異なる複数の円盤が与えられたとき、すべての円盤を 1 本の柱から別の柱へ移動する。ただし 1 回に 1 枚しか動かせず、大きい円盤を小さい円盤の上に置いてはならない。 -**制約満足問題**:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。 +**制約充足問題**:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。 -- $n$ クイーン問題:$n imes n$ のチェス盤に $n$ 個のクイーンを配置し、互いに攻撃しないようにする。 -- 数独:$9 imes 9$ のグリッドに数字 $1$ \~ $9$ を入力し、各行、列、$3 imes 3$ のサブグリッドに重複がないようにする。 -- グラフ彩色問題:与えられた無向グラフに対し、隣接頂点が異なる色になるように最小限の色で彩色する。 +- $n$ クイーン問題:$n \times n$ の盤面に $n$ 個のクイーンを配置し、互いに攻撃し合わないようにする。 +- 数独:$9 \times 9$ のグリッドに数字 $1$ ~ $9$ を入れ、各行・各列・各 $3 \times 3$ の小区画で数字が重複しないようにする。 +- グラフ彩色問題:無向グラフが与えられたとき、隣接する頂点が同じ色にならないように、できるだけ少ない色で各頂点を彩色する。 -**組合せ最適化問題**:この種の問題の目標は、組合せ空間内で特定の条件を満たす最適解を見つけることです。 +**組合せ最適化問題**:この種の問題の目標は、組合せ空間の中で条件を満たす最適解を見つけることです。 -- 0-1 ナップサック問題:与えられた物品群とバックパックがあり、各物品には価値と重さが設定されている。バックパックの容量制限内で、総価値を最大化する物品の選択を求める。 -- 旅行セールスマン問題:グラフ上で、1 つの点から出発し、すべての他の点を 1 回ずつ訪問して出発点に戻る最短経路を求める。 -- 最大クリーク問題:与えられた無向グラフの中で、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。 +- 0-1 ナップサック問題:複数の品物とナップサックが与えられ、各品物には価値と重さがある。ナップサック容量の範囲内で総価値が最大になるように品物を選ぶ。 +- 巡回セールスマン問題:グラフ内のある頂点から出発し、他のすべての頂点をちょうど 1 回ずつ訪れて出発点へ戻るときの最短経路を求める。 +- 最大クリーク問題:無向グラフが与えられたとき、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。 -注意すべきは、多くの組合せ最適化問題に対して、バックトラッキングが最適解法ではないということです。 +多くの組合せ最適化問題では、バックトラッキングは最適な解法ではない点に注意してください。 -- 0-1 ナップサック問題は、時間効率を高めるために動的計画法がよく使用されます。 -- 旅行セールスマン問題は有名な NP-Hard 問題であり、遺伝的アルゴリズムやアントコロニーアルゴリズムなどの手法がよく使われます。 -- 最大クリーク問題はグラフ理論の古典的な問題であり、貪欲法などのヒューリスティックアルゴリズムで解くことができます。 +- 0-1 ナップサック問題は通常、より高い時間効率を得るために動的計画法で解く。 +- 巡回セールスマン問題は著名な NP-Hard 問題であり、よく用いられる解法には遺伝的アルゴリズムや蟻コロニー最適化などがある。 +- 最大クリーク問題はグラフ理論における古典的問題であり、貪欲法などのヒューリスティックで解ける。 diff --git a/ja/docs/chapter_backtracking/index.md b/ja/docs/chapter_backtracking/index.md index 387254e7e..491293e3a 100644 --- a/ja/docs/chapter_backtracking/index.md +++ b/ja/docs/chapter_backtracking/index.md @@ -9,14 +9,14 @@ icon: material/map-marker-path !!! abstract - 迷路の探検家のように、私たちは前進する道で障害に遭遇することがあります。 - - バックトラッキングの力は、私たちに新しく始めること、試し続けること、そして最終的に光への出口を見つけることを可能にします。 + 私たちは迷宮の探検者のように、前へ進む道で困難に出会うことがあります。 + + バックトラッキングの力によってやり直しができ、試行を重ね、最後には光へ通じる出口を見つけられます。 ## 章の内容 - [13.1   バックトラッキングアルゴリズム](backtracking_algorithm.md) - [13.2   全順列問題](permutations_problem.md) -- [13.3   部分集合和問題](subset_sum_problem.md) -- [13.4   Nクイーン問題](n_queens_problem.md) +- [13.3   部分和問題](subset_sum_problem.md) +- [13.4   n クイーン問題](n_queens_problem.md) - [13.5   まとめ](summary.md) diff --git a/ja/docs/chapter_backtracking/n_queens_problem.md b/ja/docs/chapter_backtracking/n_queens_problem.md index a1131d198..e55327cc1 100644 --- a/ja/docs/chapter_backtracking/n_queens_problem.md +++ b/ja/docs/chapter_backtracking/n_queens_problem.md @@ -2,59 +2,59 @@ comments: true --- -# 13.4   Nクイーン問題 +# 13.4   n クイーン問題 !!! question - チェスのルールによると、クイーンは同じ行、列、または対角線上の駒を攻撃できます。$n$ 個のクイーンと $n \times n$ のチェスボードが与えられた場合、2つのクイーンが互いに攻撃できない配置を見つけてください。 + チェスのルールによれば、クイーンは同じ行、同じ列、または同じ斜線上にある駒を攻撃できます。$n$ 個のクイーンと $n \times n$ サイズの盤面が与えられたとき、すべてのクイーンが互いに攻撃し合わない配置を求めます。 -以下の図に示すように、$n = 4$ の場合、2つの解があります。バックトラッキングアルゴリズムの観点から、$n \times n$ のチェスボードには $n^2$ 個のマスがあり、すべての可能な選択肢 `choices` を示しています。チェスボードの状態 `state` は、各クイーンが配置されるにつれて継続的に変化します。 +下図に示すように、$n = 4$ のとき、2 つの解を見つけることができます。バックトラッキングの観点から見ると、$n \times n$ サイズの盤面には合計 $n^2$ 個のマスがあり、これがすべての選択肢 `choices` を与えます。クイーンを 1 つずつ配置していく過程で、盤面の状態は絶えず変化し、その各時点の盤面が状態 `state` です。 -![4クイーン問題の解](n_queens_problem.assets/solution_4_queens.png){ class="animation-figure" } +![4 クイーン問題の解](n_queens_problem.assets/solution_4_queens.png){ class="animation-figure" } -

図 13-15   4クイーン問題の解

+

図 13-15   4 クイーン問題の解

-以下の図は、この問題の3つの制約を示しています:**複数のクイーンは同じ行、列、または対角線を占有できません**。対角線は主対角線 `\` と副対角線 `/` に分かれることに注意することが重要です。 +下図は本問題の 3 つの制約条件を示しています。**複数のクイーンは同じ行、同じ列、同じ対角線上に置けません**。なお、対角線には主対角線 `\` と副対角線 `/` の 2 種類があります。 -![Nクイーン問題の制約](n_queens_problem.assets/n_queens_constraints.png){ class="animation-figure" } +![n クイーン問題の制約条件](n_queens_problem.assets/n_queens_constraints.png){ class="animation-figure" } -

図 13-16   Nクイーン問題の制約

+

図 13-16   n クイーン問題の制約条件

### 1.   行ごとの配置戦略 -クイーンの数がチェスボードの行数と等しく、どちらも $n$ であるため、**チェスボードの各行には1つのクイーンのみが配置できることが**容易に結論付けられます。 +クイーンの数と盤面の行数はいずれも $n$ なので、次の推論を容易に得られます:**盤面の各行にはクイーンを 1 つだけ配置できます**。 -これは、行ごとの配置戦略を採用できることを意味します:最初の行から開始して、最後の行に到達するまで行ごとに1つのクイーンを配置します。 +つまり、行ごとの配置戦略を採用できます:最初の行から始めて、各行に 1 つのクイーンを配置し、最後の行まで進みます。 -以下の図は、4クイーン問題の行ごとの配置プロセスを示しています。スペースの制限により、図は最初の行の1つの検索分岐のみを展開し、列と対角線の制約を満たさない配置を剪定します。 +下図は 4 クイーン問題における行ごとの配置過程を示しています。図の大きさの都合上、下図では 1 行目における検索分岐の 1 つだけを展開し、列制約と対角線制約を満たさない案はすべて枝刈りしています。 ![行ごとの配置戦略](n_queens_problem.assets/n_queens_placing.png){ class="animation-figure" }

図 13-17   行ごとの配置戦略

-本質的に、**行ごとの配置戦略は剪定関数として機能し**、同じ行に複数のクイーンを配置するすべての検索分岐を除去します。 +本質的には、**行ごとの配置戦略は枝刈りとして機能します**。これにより、同じ行に複数のクイーンが現れるすべての探索分岐を回避できます。 -### 2.   列と対角線の剪定 +### 2.   列と対角線の枝刈り -列の制約を満たすために、長さ $n$ のブール配列 `cols` を使用して、各列にクイーンが占有されているかどうかを追跡できます。各配置決定の前に、`cols` を使用してすでにクイーンがある列を剪定し、バックトラッキング中に動的に更新されます。 +列制約を満たすために、長さ $n$ のブール配列 `cols` を用いて、各列にクイーンがあるかどうかを記録できます。配置を決めるたびに、`cols` を使って既存のクイーンがある列を枝刈りし、バックトラッキングの中で `cols` の状態を動的に更新します。 !!! tip - 行列の原点は左上隅にあり、行インデックスは上から下に増加し、列インデックスは左から右に増加することに注意してください。 + 注意として、行列の原点は左上にあり、行インデックスは上から下へ、列インデックスは左から右へ増加します。 -対角線の制約はどうでしょうか?チェスボード上の特定のセルの行と列のインデックスを $(row, col)$ とします。特定の主対角線を選択することで、その対角線上のすべてのセルで差 $row - col$ が同じであることに気付きます。**つまり、$row - col$ は主対角線上で定数値です**。 +では、対角線制約はどのように扱えばよいのでしょうか。盤面上のあるマスの行列インデックスを $(row, col)$ とし、行列内のある主対角線を選ぶと、その対角線上のすべてのマスで行インデックスから列インデックスを引いた値が等しいことが分かります。**つまり、主対角線上のすべてのマスでは $row - col$ が一定値になります**。 -言い換えると、2つのセルが $row_1 - col_1 = row_2 - col_2$ を満たす場合、それらは確実に同じ主対角線上にあります。このパターンを使用して、以下の図に示す配列 `diags1` を利用して、クイーンが主対角線上にあるかどうかを追跡できます。 +つまり、2 つのマスが $row_1 - col_1 = row_2 - col_2$ を満たすなら、それらは必ず同じ主対角線上にあります。この性質を利用して、下図の配列 `diags1` により、各主対角線にクイーンがあるかどうかを記録できます。 -同様に、**$row + col$ の和は副対角線上のすべてのセルで定数値です**。配列 `diags2` を使用して副対角線の制約も処理できます。 +同様に、**副対角線上のすべてのマスでは $row + col$ が一定値です**。副対角線制約も配列 `diags2` を使って処理できます。 -![列と対角線の制約の処理](n_queens_problem.assets/n_queens_cols_diagonals.png){ class="animation-figure" } +![列制約と対角線制約の処理](n_queens_problem.assets/n_queens_cols_diagonals.png){ class="animation-figure" } -

図 13-18   列と対角線の制約の処理

+

図 13-18   列制約と対角線制約の処理

### 3.   コード実装 -$n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で、$row + col$ の範囲は $[0, 2n - 2]$ であることに注意してください。したがって、主対角線と副対角線の数はどちらも $2n - 1$ で、配列 `diags1` と `diags2` の長さは $2n - 1$ です。 +注意として、$n$ 次正方行列では $row - col$ の範囲は $[-n + 1, n - 1]$ 、$row + col$ の範囲は $[0, 2n - 2]$ です。したがって、主対角線と副対角線の本数はいずれも $2n - 1$ であり、配列 `diags1` と `diags2` の長さもともに $2n - 1$ です。 === "Python" @@ -68,34 +68,34 @@ $n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で diags1: list[bool], diags2: list[bool], ): - """バックトラッキングアルゴリズム:n クイーン""" - # すべての行が配置されたら、解を記録 + """バックトラッキング:N クイーン""" + # すべての行への配置が完了したら、解を記録する if row == n: res.append([list(row) for row in state]) return # すべての列を走査 for col in range(n): - # セルに対応する主対角線と副対角線を計算 + # このマスに対応する主対角線と副対角線を計算 diag1 = row - col + n - 1 diag2 = row + col - # 枝刈り:セルの列、主対角線、副対角線にクイーンを配置しない + # 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない if not cols[col] and not diags1[diag1] and not diags2[diag2]: - # 試行:セルにクイーンを配置 + # 試行:そのマスにクイーンを置く state[row][col] = "Q" cols[col] = diags1[diag1] = diags2[diag2] = True - # 次の行を配置 + # 次の行に配置する backtrack(row + 1, n, state, res, cols, diags1, diags2) - # 撤回:セルを空のスポットに復元 + # 戻す:そのマスを空きマスに戻す state[row][col] = "#" cols[col] = diags1[diag1] = diags2[diag2] = False def n_queens(n: int) -> list[list[list[str]]]: - """n クイーンを解く""" - # n*n サイズのチェスボードを初期化、'Q' はクイーンを表し、'#' は空のスポットを表す + """N クイーンを解く""" + # n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す state = [["#" for _ in range(n)] for _ in range(n)] - cols = [False] * n # クイーンがある列を記録 - diags1 = [False] * (2 * n - 1) # クイーンがある主対角線を記録 - diags2 = [False] * (2 * n - 1) # クイーンがある副対角線を記録 + cols = [False] * n # 列にクイーンがあるか記録 + diags1 = [False] * (2 * n - 1) # 主対角線にクイーンがあるかを記録 + diags2 = [False] * (2 * n - 1) # 副対角線にクイーンがあるかを記録 res = [] backtrack(0, n, state, res, cols, diags1, diags2) @@ -105,40 +105,40 @@ $n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で === "C++" ```cpp title="n_queens.cpp" - /* バックトラッキングアルゴリズム:n クイーン */ + /* バックトラッキング:N クイーン */ void backtrack(int row, int n, vector> &state, vector>> &res, vector &cols, vector &diags1, vector &diags2) { - // すべての行が配置されたら、解を記録 + // すべての行への配置が完了したら、解を記録する if (row == n) { res.push_back(state); return; } // すべての列を走査 for (int col = 0; col < n; col++) { - // セルに対応する主対角線と副対角線を計算 + // このマスに対応する主対角線と副対角線を計算 int diag1 = row - col + n - 1; int diag2 = row + col; - // 剪定:セルの列、主対角線、副対角線にクイーンを配置することを許可しない + // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { - // 試行:セルにクイーンを配置 + // 試行:そのマスにクイーンを置く state[row][col] = "Q"; cols[col] = diags1[diag1] = diags2[diag2] = true; - // 次の行を配置 + // 次の行に配置する backtrack(row + 1, n, state, res, cols, diags1, diags2); - // 回退:セルを空のスポットに復元 + // 戻す:そのマスを空きマスに戻す state[row][col] = "#"; cols[col] = diags1[diag1] = diags2[diag2] = false; } } } - /* n クイーンを解く */ + /* N クイーンを解く */ vector>> nQueens(int n) { - // n*n サイズのチェスボードを初期化、'Q' はクイーンを表し、'#' は空のスポットを表す + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す vector> state(n, vector(n, "#")); - vector cols(n, false); // クイーンのある列を記録 - vector diags1(2 * n - 1, false); // クイーンのある主対角線を記録 - vector diags2(2 * n - 1, false); // クイーンのある副対角線を記録 + vector cols(n, false); // 列にクイーンがあるか記録 + vector diags1(2 * n - 1, false); // 主対角線にクイーンがあるかを記録 + vector diags2(2 * n - 1, false); // 副対角線にクイーンがあるかを記録 vector>> res; backtrack(0, n, state, res, cols, diags1, diags2); @@ -150,10 +150,10 @@ $n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で === "Java" ```java title="n_queens.java" - /* バックトラッキングアルゴリズム:n クイーン */ + /* バックトラッキング:N クイーン */ void backtrack(int row, int n, List> state, List>> res, boolean[] cols, boolean[] diags1, boolean[] diags2) { - // すべての行が配置されたら、解を記録 + // すべての行への配置が完了したら、解を記録する if (row == n) { List> copyState = new ArrayList<>(); for (List sRow : state) { @@ -164,26 +164,26 @@ $n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で } // すべての列を走査 for (int col = 0; col < n; col++) { - // セルに対応する主対角線と副対角線を計算 + // このマスに対応する主対角線と副対角線を計算 int diag1 = row - col + n - 1; int diag2 = row + col; - // 剪定:セルの列、主対角線、副対角線にクイーンを配置することを許可しない + // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { - // 試行:セルにクイーンを配置 + // 試行:そのマスにクイーンを置く state.get(row).set(col, "Q"); cols[col] = diags1[diag1] = diags2[diag2] = true; - // 次の行を配置 + // 次の行に配置する backtrack(row + 1, n, state, res, cols, diags1, diags2); - // 回退:セルを空のスポットに復元 + // 戻す:そのマスを空きマスに戻す state.get(row).set(col, "#"); cols[col] = diags1[diag1] = diags2[diag2] = false; } } } - /* n クイーンを解く */ + /* N クイーンを解く */ List>> nQueens(int n) { - // n*n サイズのチェスボードを初期化、'Q' はクイーンを表し、'#' は空のスポットを表す + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す List> state = new ArrayList<>(); for (int i = 0; i < n; i++) { List row = new ArrayList<>(); @@ -192,9 +192,9 @@ $n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で } state.add(row); } - boolean[] cols = new boolean[n]; // クイーンのある列を記録 - boolean[] diags1 = new boolean[2 * n - 1]; // クイーンのある主対角線を記録 - boolean[] diags2 = new boolean[2 * n - 1]; // クイーンのある副対角線を記録 + boolean[] cols = new boolean[n]; // 列にクイーンがあるか記録 + boolean[] diags1 = new boolean[2 * n - 1]; // 主対角線にクイーンがあるかを記録 + boolean[] diags2 = new boolean[2 * n - 1]; // 副対角線にクイーンがあるかを記録 List>> res = new ArrayList<>(); backtrack(0, n, state, res, cols, diags1, diags2); @@ -206,83 +206,549 @@ $n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で === "C#" ```csharp title="n_queens.cs" - [class]{n_queens}-[func]{Backtrack} + /* バックトラッキング:N クイーン */ + void Backtrack(int row, int n, List> state, List>> res, + bool[] cols, bool[] diags1, bool[] diags2) { + // すべての行への配置が完了したら、解を記録する + if (row == n) { + List> copyState = []; + foreach (List sRow in state) { + copyState.Add(new List(sRow)); + } + res.Add(copyState); + return; + } + // すべての列を走査 + for (int col = 0; col < n; col++) { + // このマスに対応する主対角線と副対角線を計算 + int diag1 = row - col + n - 1; + int diag2 = row + col; + // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // 試行:そのマスにクイーンを置く + state[row][col] = "Q"; + cols[col] = diags1[diag1] = diags2[diag2] = true; + // 次の行に配置する + Backtrack(row + 1, n, state, res, cols, diags1, diags2); + // 戻す:そのマスを空きマスに戻す + state[row][col] = "#"; + cols[col] = diags1[diag1] = diags2[diag2] = false; + } + } + } - [class]{n_queens}-[func]{NQueens} + /* N クイーンを解く */ + List>> NQueens(int n) { + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す + List> state = []; + for (int i = 0; i < n; i++) { + List row = []; + for (int j = 0; j < n; j++) { + row.Add("#"); + } + state.Add(row); + } + bool[] cols = new bool[n]; // 列にクイーンがあるか記録 + bool[] diags1 = new bool[2 * n - 1]; // 主対角線にクイーンがあるかを記録 + bool[] diags2 = new bool[2 * n - 1]; // 副対角線にクイーンがあるかを記録 + List>> res = []; + + Backtrack(0, n, state, res, cols, diags1, diags2); + + return res; + } ``` === "Go" ```go title="n_queens.go" - [class]{}-[func]{backtrack} + /* バックトラッキング:N クイーン */ + func backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) { + // すべての行への配置が完了したら、解を記録する + if row == n { + newState := make([][]string, len(*state)) + for i, _ := range newState { + newState[i] = make([]string, len((*state)[0])) + copy(newState[i], (*state)[i]) - [class]{}-[func]{nQueens} + } + *res = append(*res, newState) + return + } + // すべての列を走査 + for col := 0; col < n; col++ { + // このマスに対応する主対角線と副対角線を計算 + diag1 := row - col + n - 1 + diag2 := row + col + // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない + if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] { + // 試行:そのマスにクイーンを置く + (*state)[row][col] = "Q" + (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true + // 次の行に配置する + backtrack(row+1, n, state, res, cols, diags1, diags2) + // 戻す:そのマスを空きマスに戻す + (*state)[row][col] = "#" + (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false + } + } + } + + /* N クイーンを解く */ + func nQueens(n int) [][][]string { + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す + state := make([][]string, n) + for i := 0; i < n; i++ { + row := make([]string, n) + for i := 0; i < n; i++ { + row[i] = "#" + } + state[i] = row + } + // 列にクイーンがあるか記録 + cols := make([]bool, n) + diags1 := make([]bool, 2*n-1) + diags2 := make([]bool, 2*n-1) + res := make([][][]string, 0) + backtrack(0, n, &state, &res, &cols, &diags1, &diags2) + return res + } ``` === "Swift" ```swift title="n_queens.swift" - [class]{}-[func]{backtrack} + /* バックトラッキング:N クイーン */ + func backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) { + // すべての行への配置が完了したら、解を記録する + if row == n { + res.append(state) + return + } + // すべての列を走査 + for col in 0 ..< n { + // このマスに対応する主対角線と副対角線を計算 + let diag1 = row - col + n - 1 + let diag2 = row + col + // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない + if !cols[col] && !diags1[diag1] && !diags2[diag2] { + // 試行:そのマスにクイーンを置く + state[row][col] = "Q" + cols[col] = true + diags1[diag1] = true + diags2[diag2] = true + // 次の行に配置する + backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2) + // 戻す:そのマスを空きマスに戻す + state[row][col] = "#" + cols[col] = false + diags1[diag1] = false + diags2[diag2] = false + } + } + } - [class]{}-[func]{nQueens} + /* N クイーンを解く */ + func nQueens(n: Int) -> [[[String]]] { + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す + var state = Array(repeating: Array(repeating: "#", count: n), count: n) + var cols = Array(repeating: false, count: n) // 列にクイーンがあるか記録 + var diags1 = Array(repeating: false, count: 2 * n - 1) // 主対角線にクイーンがあるかを記録 + var diags2 = Array(repeating: false, count: 2 * n - 1) // 副対角線にクイーンがあるかを記録 + var res: [[[String]]] = [] + + backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2) + + return res + } ``` === "JS" ```javascript title="n_queens.js" - [class]{}-[func]{backtrack} + /* バックトラッキング:N クイーン */ + function backtrack(row, n, state, res, cols, diags1, diags2) { + // すべての行への配置が完了したら、解を記録する + if (row === n) { + res.push(state.map((row) => row.slice())); + return; + } + // すべての列を走査 + for (let col = 0; col < n; col++) { + // このマスに対応する主対角線と副対角線を計算 + const diag1 = row - col + n - 1; + const diag2 = row + col; + // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // 試行:そのマスにクイーンを置く + state[row][col] = 'Q'; + cols[col] = diags1[diag1] = diags2[diag2] = true; + // 次の行に配置する + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // 戻す:そのマスを空きマスに戻す + state[row][col] = '#'; + cols[col] = diags1[diag1] = diags2[diag2] = false; + } + } + } - [class]{}-[func]{nQueens} + /* N クイーンを解く */ + function nQueens(n) { + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す + const state = Array.from({ length: n }, () => Array(n).fill('#')); + const cols = Array(n).fill(false); // 列にクイーンがあるか記録 + const diags1 = Array(2 * n - 1).fill(false); // 主対角線にクイーンがあるかを記録 + const diags2 = Array(2 * n - 1).fill(false); // 副対角線にクイーンがあるかを記録 + const res = []; + + backtrack(0, n, state, res, cols, diags1, diags2); + return res; + } ``` === "TS" ```typescript title="n_queens.ts" - [class]{}-[func]{backtrack} + /* バックトラッキング:N クイーン */ + function backtrack( + row: number, + n: number, + state: string[][], + res: string[][][], + cols: boolean[], + diags1: boolean[], + diags2: boolean[] + ): void { + // すべての行への配置が完了したら、解を記録する + if (row === n) { + res.push(state.map((row) => row.slice())); + return; + } + // すべての列を走査 + for (let col = 0; col < n; col++) { + // このマスに対応する主対角線と副対角線を計算 + const diag1 = row - col + n - 1; + const diag2 = row + col; + // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // 試行:そのマスにクイーンを置く + state[row][col] = 'Q'; + cols[col] = diags1[diag1] = diags2[diag2] = true; + // 次の行に配置する + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // 戻す:そのマスを空きマスに戻す + state[row][col] = '#'; + cols[col] = diags1[diag1] = diags2[diag2] = false; + } + } + } - [class]{}-[func]{nQueens} + /* N クイーンを解く */ + function nQueens(n: number): string[][][] { + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す + const state = Array.from({ length: n }, () => Array(n).fill('#')); + const cols = Array(n).fill(false); // 列にクイーンがあるか記録 + const diags1 = Array(2 * n - 1).fill(false); // 主対角線にクイーンがあるかを記録 + const diags2 = Array(2 * n - 1).fill(false); // 副対角線にクイーンがあるかを記録 + const res: string[][][] = []; + + backtrack(0, n, state, res, cols, diags1, diags2); + return res; + } ``` === "Dart" ```dart title="n_queens.dart" - [class]{}-[func]{backtrack} + /* バックトラッキング:N クイーン */ + void backtrack( + int row, + int n, + List> state, + List>> res, + List cols, + List diags1, + List diags2, + ) { + // すべての行への配置が完了したら、解を記録する + if (row == n) { + List> copyState = []; + for (List sRow in state) { + copyState.add(List.from(sRow)); + } + res.add(copyState); + return; + } + // すべての列を走査 + for (int col = 0; col < n; col++) { + // このマスに対応する主対角線と副対角線を計算 + int diag1 = row - col + n - 1; + int diag2 = row + col; + // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // 試行:そのマスにクイーンを置く + state[row][col] = "Q"; + cols[col] = true; + diags1[diag1] = true; + diags2[diag2] = true; + // 次の行に配置する + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // 戻す:そのマスを空きマスに戻す + state[row][col] = "#"; + cols[col] = false; + diags1[diag1] = false; + diags2[diag2] = false; + } + } + } - [class]{}-[func]{nQueens} + /* N クイーンを解く */ + List>> nQueens(int n) { + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す + List> state = List.generate(n, (index) => List.filled(n, "#")); + List cols = List.filled(n, false); // 列にクイーンがあるか記録 + List diags1 = List.filled(2 * n - 1, false); // 主対角線にクイーンがあるかを記録 + List diags2 = List.filled(2 * n - 1, false); // 副対角線にクイーンがあるかを記録 + List>> res = []; + + backtrack(0, n, state, res, cols, diags1, diags2); + + return res; + } ``` === "Rust" ```rust title="n_queens.rs" - [class]{}-[func]{backtrack} + /* バックトラッキング:N クイーン */ + fn backtrack( + row: usize, + n: usize, + state: &mut Vec>, + res: &mut Vec>>, + cols: &mut [bool], + diags1: &mut [bool], + diags2: &mut [bool], + ) { + // すべての行への配置が完了したら、解を記録する + if row == n { + res.push(state.clone()); + return; + } + // すべての列を走査 + for col in 0..n { + // このマスに対応する主対角線と副対角線を計算 + let diag1 = row + n - 1 - col; + let diag2 = row + col; + // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない + if !cols[col] && !diags1[diag1] && !diags2[diag2] { + // 試行:そのマスにクイーンを置く + state[row][col] = "Q".into(); + (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true); + // 次の行に配置する + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // 戻す:そのマスを空きマスに戻す + state[row][col] = "#".into(); + (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false); + } + } + } - [class]{}-[func]{n_queens} + /* N クイーンを解く */ + fn n_queens(n: usize) -> Vec>> { + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す + let mut state: Vec> = vec![vec!["#".to_string(); n]; n]; + let mut cols = vec![false; n]; // 列にクイーンがあるか記録 + let mut diags1 = vec![false; 2 * n - 1]; // 主対角線にクイーンがあるかを記録 + let mut diags2 = vec![false; 2 * n - 1]; // 副対角線にクイーンがあるかを記録 + let mut res: Vec>> = Vec::new(); + + backtrack( + 0, + n, + &mut state, + &mut res, + &mut cols, + &mut diags1, + &mut diags2, + ); + + res + } ``` === "C" ```c title="n_queens.c" - [class]{}-[func]{backtrack} + /* バックトラッキング:N クイーン */ + void backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE], + bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) { + // すべての行への配置が完了したら、解を記録する + if (row == n) { + res[*resSize] = (char **)malloc(sizeof(char *) * n); + for (int i = 0; i < n; ++i) { + res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1)); + strcpy(res[*resSize][i], state[i]); + } + (*resSize)++; + return; + } + // すべての列を走査 + for (int col = 0; col < n; col++) { + // このマスに対応する主対角線と副対角線を計算 + int diag1 = row - col + n - 1; + int diag2 = row + col; + // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // 試行:そのマスにクイーンを置く + state[row][col] = 'Q'; + cols[col] = diags1[diag1] = diags2[diag2] = true; + // 次の行に配置する + backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2); + // 戻す:そのマスを空きマスに戻す + state[row][col] = '#'; + cols[col] = diags1[diag1] = diags2[diag2] = false; + } + } + } - [class]{}-[func]{nQueens} + /* N クイーンを解く */ + char ***nQueens(int n, int *returnSize) { + char state[MAX_SIZE][MAX_SIZE]; + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す + for (int i = 0; i < n; ++i) { + for (int j = 0; j < n; ++j) { + state[i][j] = '#'; + } + state[i][n] = '\0'; + } + bool cols[MAX_SIZE] = {false}; // 列にクイーンがあるか記録 + bool diags1[2 * MAX_SIZE - 1] = {false}; // 主対角線にクイーンがあるかを記録 + bool diags2[2 * MAX_SIZE - 1] = {false}; // 副対角線にクイーンがあるかを記録 + + char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE); + *returnSize = 0; + backtrack(0, n, state, res, returnSize, cols, diags1, diags2); + return res; + } ``` === "Kotlin" ```kotlin title="n_queens.kt" - [class]{}-[func]{backtrack} + /* バックトラッキング:N クイーン */ + fun backtrack( + row: Int, + n: Int, + state: MutableList>, + res: MutableList>?>, + cols: BooleanArray, + diags1: BooleanArray, + diags2: BooleanArray + ) { + // すべての行への配置が完了したら、解を記録する + if (row == n) { + val copyState = mutableListOf>() + for (sRow in state) { + copyState.add(sRow.toMutableList()) + } + res.add(copyState) + return + } + // すべての列を走査 + for (col in 0..>?> { + // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す + val state = mutableListOf>() + for (i in 0..() + for (j in 0..>?>() + + backtrack(0, n, state, res, cols, diags1, diags2) + + return res + } ``` === "Ruby" ```ruby title="n_queens.rb" - [class]{}-[func]{backtrack} + ### バックトラッキング法:Nクイーン ### + def backtrack(row, n, state, res, cols, diags1, diags2) + # すべての行への配置が完了したら、解を記録する + if row == n + res << state.map { |row| row.dup } + return + end - [class]{}-[func]{n_queens} + # すべての列を走査 + for col in 0...n + # このマスに対応する主対角線と副対角線を計算 + diag1 = row - col + n - 1 + diag2 = row + col + # 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない + if !cols[col] && !diags1[diag1] && !diags2[diag2] + # 試行:そのマスにクイーンを置く + state[row][col] = "Q" + cols[col] = diags1[diag1] = diags2[diag2] = true + # 次の行に配置する + backtrack(row + 1, n, state, res, cols, diags1, diags2) + # 戻す:そのマスを空きマスに戻す + state[row][col] = "#" + cols[col] = diags1[diag1] = diags2[diag2] = false + end + end + end + + ### Nクイーンを解く ### + def n_queens(n) + # n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す + state = Array.new(n) { Array.new(n, "#") } + cols = Array.new(n, false) # 列にクイーンがあるか記録 + diags1 = Array.new(2 * n - 1, false) # 主対角線にクイーンがあるかを記録 + diags2 = Array.new(2 * n - 1, false) # 副対角線にクイーンがあるかを記録 + res = [] + backtrack(0, n, state, res, cols, diags1, diags2) + + res + end ``` -$n$ 個のクイーンを行ごとに配置し、列の制約を考慮して、最初の行から最後の行まで、$n$、$n-1$、$\dots$、$2$、$1$ の選択肢があり、$O(n!)$ 時間を使用します。解を記録する際、行列 `state` をコピーして `res` に追加する必要があり、コピー操作は $O(n^2)$ 時間を使用します。したがって、**全体の時間計算量は $O(n! \cdot n^2)$ です**。実際には、対角線制約に基づく剪定により検索空間を大幅に削減できるため、多くの場合、検索効率は上記の時間計算量よりも優れています。 +??? pythontutor "コードの可視化" -配列 `state` は $O(n^2)$ 空間を使用し、配列 `cols`、`diags1`、`diags2` はそれぞれ $O(n)$ 空間を使用します。最大再帰深度は $n$ で、$O(n)$ のスタックフレーム空間を使用します。したがって、**空間計算量は $O(n^2)$ です**。 +
+ + +行ごとに $n$ 回配置し、列制約を考慮すると、1 行目から最終行までの選択肢はそれぞれ $n$、$n-1$、$\dots$、$2$、$1$ 個となるため、時間計算量は $O(n!)$ です。解を記録する際には、行列 `state` をコピーして `res` に追加する必要があり、このコピー操作には $O(n^2)$ 時間を要します。したがって、**全体の時間計算量は $O(n! \cdot n^2)$** です。実際には、対角線制約による枝刈りも探索空間を大きく縮小できるため、探索効率はしばしば上記の時間計算量より良くなります。 + +配列 `state` は $O(n^2)$ の空間を使用し、配列 `cols`、`diags1`、`diags2` はいずれも $O(n)$ の空間を使用します。最大再帰深さは $n$ で、スタックフレーム空間として $O(n)$ を使用します。したがって、**空間計算量は $O(n^2)$** です。 diff --git a/ja/docs/chapter_backtracking/permutations_problem.md b/ja/docs/chapter_backtracking/permutations_problem.md index c30c45fcc..0bef5d66f 100644 --- a/ja/docs/chapter_backtracking/permutations_problem.md +++ b/ja/docs/chapter_backtracking/permutations_problem.md @@ -2,58 +2,58 @@ comments: true --- -# 13.2   順列問題 +# 13.2   全順列問題 -順列問題は、バックトラッキングアルゴリズムの典型的な応用です。これは、配列や文字列などの与えられた集合から要素のすべての可能な配置(順列)を見つけることを含みます。 +全順列問題はバックトラッキングアルゴリズムの典型的な応用例です。これは、ある集合(配列や文字列など)が与えられたとき、その要素のあり得るすべての順列を求める問題です。 -以下の表は、入力配列とその対応する順列を含むいくつかの例を示しています。 +下表に、入力配列とそれに対応するすべての順列から成る例をいくつか示します。 -

表 13-2   順列の例

+

表 13-2   全順列の例

-| 入力配列 | 順列 | -| :----------- | :----------------------------------------------------------------- | -| $[1]$ | $[1]$ | -| $[1, 2]$ | $[1, 2], [2, 1]$ | -| $[1, 2, 3]$ | $[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]$ | +| 入力配列 | すべての順列 | +| :---------- | :----------------------------------------------------------------- | +| $[1]$ | $[1]$ | +| $[1, 2]$ | $[1, 2], [2, 1]$ | +| $[1, 2, 3]$ | $[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]$ |
-## 13.2.1   重複要素がない場合 +## 13.2.1   等しい要素がない場合 !!! question - 重複要素のない整数配列が与えられた場合、すべての可能な順列を返してください。 + 重複要素を含まない整数配列を入力として受け取り、あり得るすべての順列を返します。 -バックトラッキングの観点から、**順列を生成するプロセスを一連の選択として見ることができます。** 入力配列が $[1, 2, 3]$ だとします。最初に $1$ を選択し、次に $3$、最後に $2$ を選択すると、順列 $[1, 3, 2]$ が得られます。「バックトラッキング」は前の選択を取り消して、代替オプションを探索することを意味します。 +バックトラッキングアルゴリズムの観点から見ると、**順列生成の過程は一連の選択の結果として捉えられます**。入力配列が $[1, 2, 3]$ だとすると、最初に $1$ を選び、次に $3$ を選び、最後に $2$ を選べば、順列 $[1, 3, 2]$ が得られます。戻る操作は 1 つの選択を取り消し、その後で別の選択を試し続けることを表します。 -コーディングの観点から、候補集合 `choices` は入力配列のすべての要素で構成され、`state` はこれまでに選択された要素を保持します。各要素は一度だけ選択できるため、**`state` のすべての要素は一意である必要があります**。 +バックトラッキングコードの観点では、候補集合 `choices` は入力配列中のすべての要素であり、状態 `state` は現時点までに選ばれた要素です。各要素は 1 回しか選べないことに注意してください。**したがって `state` 内の要素はすべて一意でなければなりません**。 -以下の図に示すように、検索プロセスを再帰木に展開できます。各ノードは現在の `state` を表します。ルートノードから開始して、3回の選択の後、葉ノードに到達します—それぞれが順列に対応します。 +下図のように、探索過程は再帰木として展開できます。木の各ノードは現在の状態 `state` を表します。根ノードから始めて 3 ラウンドの選択を経て葉ノードに到達し、各葉ノードが 1 つの順列に対応します。 -![順列の再帰木](permutations_problem.assets/permutations_i.png){ class="animation-figure" } +![全順列の再帰木](permutations_problem.assets/permutations_i.png){ class="animation-figure" } -

図 13-5   順列の再帰木

+

図 13-5   全順列の再帰木

-### 1.   重複選択の剪定 +### 1.   重複選択の枝刈り -各要素が一度だけ選択されることを保証するために、ブール配列 `selected` を導入します。ここで `selected[i]` は `choices[i]` が選択されたかどうかを示します。次に、この配列に基づいて剪定ステップを実行します: +各要素が 1 回しか選ばれないようにするため、ブール配列 `selected` の導入を考えます。ここで `selected[i]` は `choices[i]` がすでに選ばれているかどうかを表し、これに基づいて次の枝刈りを行います。 -- `choice[i]` を選択した後、`selected[i]` を $\text{True}$ に設定して選択されたとマークします。 -- `choices` を反復処理する際、選択されたとマークされたすべての要素をスキップします(つまり、それらの分岐を剪定します)。 +- 選択 `choice[i]` を行った後、`selected[i]` を $\text{True}$ に設定し、その要素が選択済みであることを表します。 +- 選択肢リスト `choices` を走査するとき、すでに選ばれたノードはすべてスキップします。これが枝刈りです。 -以下の図に示すように、最初のラウンドで1を選択し、2番目のラウンドで3を選択し、最後のラウンドで2を選択するとします。2番目のラウンドで要素1の分岐と、3番目のラウンドで要素1と3の分岐を剪定する必要があります。 +下図のように、1 回目に 1、2 回目に 3、3 回目に 2 を選ぶとします。このとき 2 回目では要素 1 の分岐を、3 回目では要素 1 と要素 3 の分岐を刈り取る必要があります。 -![順列の剪定例](permutations_problem.assets/permutations_i_pruning.png){ class="animation-figure" } +![全順列の枝刈り例](permutations_problem.assets/permutations_i_pruning.png){ class="animation-figure" } -

図 13-6   順列の剪定例

+

図 13-6   全順列の枝刈り例

-図から、この剪定プロセスが検索空間を $O(n^n)$ から $O(n!)$ に削減することがわかります。 +上図から、この枝刈りにより探索空間の大きさは $O(n^n)$ から $O(n!)$ へ削減されることがわかります。 ### 2.   コード実装 -この理解により、フレームワークコードの「空欄を埋める」ことができます。全体のコードを簡潔に保つため、フレームワークの各部分を個別に実装せず、代わりに `backtrack()` 関数ですべてを展開します: +以上を整理できれば、フレームワークコードの「穴埋め」を行えます。全体のコードを短くするため、フレームワークコード中の各関数を個別には実装せず、これらを `backtrack()` 関数内に展開します。 === "Python" @@ -61,8 +61,8 @@ comments: true def backtrack( state: list[int], choices: list[int], selected: list[bool], res: list[list[int]] ): - """バックトラッキングアルゴリズム:順列 I""" - # 状態の長さが要素数と等しいとき、解を記録 + """バックトラッキング:順列 I""" + # 状態の長さが要素数に等しければ、解を記録 if len(state) == len(choices): res.append(list(state)) return @@ -70,17 +70,17 @@ comments: true for i, choice in enumerate(choices): # 枝刈り:要素の重複選択を許可しない if not selected[i]: - # 試行:選択を行い、状態を更新 + # 試行: 選択を行い、状態を更新 selected[i] = True state.append(choice) - # 次の選択ラウンドに進む + # 次の選択へ進む backtrack(state, choices, selected, res) - # 撤回:選択を取り消し、前の状態に復元 + # バックトラック:選択を取り消し、前の状態に戻す selected[i] = False state.pop() def permutations_i(nums: list[int]) -> list[list[int]]: - """順列 I""" + """全順列 I""" res = [] backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res) return res @@ -89,9 +89,9 @@ comments: true === "C++" ```cpp title="permutations_i.cpp" - /* バックトラッキングアルゴリズム:順列 I */ + /* バックトラッキング:順列 I */ void backtrack(vector &state, const vector &choices, vector &selected, vector> &res) { - // 状態の長さが要素数と等しくなったら、解を記録 + // 状態の長さが要素数に等しければ、解を記録 if (state.size() == choices.size()) { res.push_back(state); return; @@ -99,21 +99,21 @@ comments: true // すべての選択肢を走査 for (int i = 0; i < choices.size(); i++) { int choice = choices[i]; - // 剪定:要素の重複選択を許可しない + // 枝刈り:要素の重複選択を許可しない if (!selected[i]) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 selected[i] = true; state.push_back(choice); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, choices, selected, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す selected[i] = false; state.pop_back(); } } } - /* 順列 I */ + /* 全順列 I */ vector> permutationsI(vector nums) { vector state; vector selected(nums.size(), false); @@ -126,9 +126,9 @@ comments: true === "Java" ```java title="permutations_i.java" - /* バックトラッキングアルゴリズム:順列 I */ + /* バックトラッキング:順列 I */ void backtrack(List state, int[] choices, boolean[] selected, List> res) { - // 状態の長さが要素数と等しくなったら、解を記録 + // 状態の長さが要素数に等しければ、解を記録 if (state.size() == choices.length) { res.add(new ArrayList(state)); return; @@ -136,21 +136,21 @@ comments: true // すべての選択肢を走査 for (int i = 0; i < choices.length; i++) { int choice = choices[i]; - // 剪定:要素の重複選択を許可しない + // 枝刈り:要素の重複選択を許可しない if (!selected[i]) { - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 selected[i] = true; state.add(choice); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, choices, selected, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す selected[i] = false; state.remove(state.size() - 1); } } } - /* 順列 I */ + /* 全順列 I */ List> permutationsI(int[] nums) { List> res = new ArrayList>(); backtrack(new ArrayList(), nums, new boolean[nums.length], res); @@ -161,114 +161,419 @@ comments: true === "C#" ```csharp title="permutations_i.cs" - [class]{permutations_i}-[func]{Backtrack} + /* バックトラッキング:順列 I */ + void Backtrack(List state, int[] choices, bool[] selected, List> res) { + // 状態の長さが要素数に等しければ、解を記録 + if (state.Count == choices.Length) { + res.Add(new List(state)); + return; + } + // すべての選択肢を走査 + for (int i = 0; i < choices.Length; i++) { + int choice = choices[i]; + // 枝刈り:要素の重複選択を許可しない + if (!selected[i]) { + // 試行: 選択を行い、状態を更新 + selected[i] = true; + state.Add(choice); + // 次の選択へ進む + Backtrack(state, choices, selected, res); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + state.RemoveAt(state.Count - 1); + } + } + } - [class]{permutations_i}-[func]{PermutationsI} + /* 全順列 I */ + List> PermutationsI(int[] nums) { + List> res = []; + Backtrack([], nums, new bool[nums.Length], res); + return res; + } ``` === "Go" ```go title="permutations_i.go" - [class]{}-[func]{backtrackI} + /* バックトラッキング:順列 I */ + func backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) { + // 状態の長さが要素数に等しければ、解を記録 + if len(*state) == len(*choices) { + newState := append([]int{}, *state...) + *res = append(*res, newState) + } + // すべての選択肢を走査 + for i := 0; i < len(*choices); i++ { + choice := (*choices)[i] + // 枝刈り:要素の重複選択を許可しない + if !(*selected)[i] { + // 試行: 選択を行い、状態を更新 + (*selected)[i] = true + *state = append(*state, choice) + // 次の選択へ進む + backtrackI(state, choices, selected, res) + // バックトラック:選択を取り消し、前の状態に戻す + (*selected)[i] = false + *state = (*state)[:len(*state)-1] + } + } + } - [class]{}-[func]{permutationsI} + /* 全順列 I */ + func permutationsI(nums []int) [][]int { + res := make([][]int, 0) + state := make([]int, 0) + selected := make([]bool, len(nums)) + backtrackI(&state, &nums, &selected, &res) + return res + } ``` === "Swift" ```swift title="permutations_i.swift" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 I */ + func backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) { + // 状態の長さが要素数に等しければ、解を記録 + if state.count == choices.count { + res.append(state) + return + } + // すべての選択肢を走査 + for (i, choice) in choices.enumerated() { + // 枝刈り:要素の重複選択を許可しない + if !selected[i] { + // 試行: 選択を行い、状態を更新 + selected[i] = true + state.append(choice) + // 次の選択へ進む + backtrack(state: &state, choices: choices, selected: &selected, res: &res) + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false + state.removeLast() + } + } + } - [class]{}-[func]{permutationsI} + /* 全順列 I */ + func permutationsI(nums: [Int]) -> [[Int]] { + var state: [Int] = [] + var selected = Array(repeating: false, count: nums.count) + var res: [[Int]] = [] + backtrack(state: &state, choices: nums, selected: &selected, res: &res) + return res + } ``` === "JS" ```javascript title="permutations_i.js" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 I */ + function backtrack(state, choices, selected, res) { + // 状態の長さが要素数に等しければ、解を記録 + if (state.length === choices.length) { + res.push([...state]); + return; + } + // すべての選択肢を走査 + choices.forEach((choice, i) => { + // 枝刈り:要素の重複選択を許可しない + if (!selected[i]) { + // 試行: 選択を行い、状態を更新 + selected[i] = true; + state.push(choice); + // 次の選択へ進む + backtrack(state, choices, selected, res); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + state.pop(); + } + }); + } - [class]{}-[func]{permutationsI} + /* 全順列 I */ + function permutationsI(nums) { + const res = []; + backtrack([], nums, Array(nums.length).fill(false), res); + return res; + } ``` === "TS" ```typescript title="permutations_i.ts" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 I */ + function backtrack( + state: number[], + choices: number[], + selected: boolean[], + res: number[][] + ): void { + // 状態の長さが要素数に等しければ、解を記録 + if (state.length === choices.length) { + res.push([...state]); + return; + } + // すべての選択肢を走査 + choices.forEach((choice, i) => { + // 枝刈り:要素の重複選択を許可しない + if (!selected[i]) { + // 試行: 選択を行い、状態を更新 + selected[i] = true; + state.push(choice); + // 次の選択へ進む + backtrack(state, choices, selected, res); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + state.pop(); + } + }); + } - [class]{}-[func]{permutationsI} + /* 全順列 I */ + function permutationsI(nums: number[]): number[][] { + const res: number[][] = []; + backtrack([], nums, Array(nums.length).fill(false), res); + return res; + } ``` === "Dart" ```dart title="permutations_i.dart" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 I */ + void backtrack( + List state, + List choices, + List selected, + List> res, + ) { + // 状態の長さが要素数に等しければ、解を記録 + if (state.length == choices.length) { + res.add(List.from(state)); + return; + } + // すべての選択肢を走査 + for (int i = 0; i < choices.length; i++) { + int choice = choices[i]; + // 枝刈り:要素の重複選択を許可しない + if (!selected[i]) { + // 試行: 選択を行い、状態を更新 + selected[i] = true; + state.add(choice); + // 次の選択へ進む + backtrack(state, choices, selected, res); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + state.removeLast(); + } + } + } - [class]{}-[func]{permutationsI} + /* 全順列 I */ + List> permutationsI(List nums) { + List> res = []; + backtrack([], nums, List.filled(nums.length, false), res); + return res; + } ``` === "Rust" ```rust title="permutations_i.rs" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 I */ + fn backtrack(mut state: Vec, choices: &[i32], selected: &mut [bool], res: &mut Vec>) { + // 状態の長さが要素数に等しければ、解を記録 + if state.len() == choices.len() { + res.push(state); + return; + } + // すべての選択肢を走査 + for i in 0..choices.len() { + let choice = choices[i]; + // 枝刈り:要素の重複選択を許可しない + if !selected[i] { + // 試行: 選択を行い、状態を更新 + selected[i] = true; + state.push(choice); + // 次の選択へ進む + backtrack(state.clone(), choices, selected, res); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + state.pop(); + } + } + } - [class]{}-[func]{permutations_i} + /* 全順列 I */ + fn permutations_i(nums: &mut [i32]) -> Vec> { + let mut res = Vec::new(); // 状態(部分集合) + backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res); + res + } ``` === "C" ```c title="permutations_i.c" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 I */ + void backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) { + // 状態の長さが要素数に等しければ、解を記録 + if (stateSize == choicesSize) { + res[*resSize] = (int *)malloc(choicesSize * sizeof(int)); + for (int i = 0; i < choicesSize; i++) { + res[*resSize][i] = state[i]; + } + (*resSize)++; + return; + } + // すべての選択肢を走査 + for (int i = 0; i < choicesSize; i++) { + int choice = choices[i]; + // 枝刈り:要素の重複選択を許可しない + if (!selected[i]) { + // 試行: 選択を行い、状態を更新 + selected[i] = true; + state[stateSize] = choice; + // 次の選択へ進む + backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + } + } + } - [class]{}-[func]{permutationsI} + /* 全順列 I */ + int **permutationsI(int *nums, int numsSize, int *returnSize) { + int *state = (int *)malloc(numsSize * sizeof(int)); + bool *selected = (bool *)malloc(numsSize * sizeof(bool)); + for (int i = 0; i < numsSize; i++) { + selected[i] = false; + } + int **res = (int **)malloc(MAX_SIZE * sizeof(int *)); + *returnSize = 0; + + backtrack(state, 0, nums, numsSize, selected, res, returnSize); + + free(state); + free(selected); + + return res; + } ``` === "Kotlin" ```kotlin title="permutations_i.kt" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 I */ + fun backtrack( + state: MutableList, + choices: IntArray, + selected: BooleanArray, + res: MutableList?> + ) { + // 状態の長さが要素数に等しければ、解を記録 + if (state.size == choices.size) { + res.add(state.toMutableList()) + return + } + // すべての選択肢を走査 + for (i in choices.indices) { + val choice = choices[i] + // 枝刈り:要素の重複選択を許可しない + if (!selected[i]) { + // 試行: 選択を行い、状態を更新 + selected[i] = true + state.add(choice) + // 次の選択へ進む + backtrack(state, choices, selected, res) + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false + state.removeAt(state.size - 1) + } + } + } - [class]{}-[func]{permutationsI} + /* 全順列 I */ + fun permutationsI(nums: IntArray): MutableList?> { + val res = mutableListOf?>() + backtrack(mutableListOf(), nums, BooleanArray(nums.size), res) + return res + } ``` === "Ruby" ```ruby title="permutations_i.rb" - [class]{}-[func]{backtrack} + ### バックトラッキング法:全順列 I ### + def backtrack(state, choices, selected, res) + # 状態の長さが要素数に等しければ、解を記録 + if state.length == choices.length + res << state.dup + return + end - [class]{}-[func]{permutations_i} + # すべての選択肢を走査 + choices.each_with_index do |choice, i| + # 枝刈り:要素の重複選択を許可しない + unless selected[i] + # 試行: 選択を行い、状態を更新 + selected[i] = true + state << choice + # 次の選択へ進む + backtrack(state, choices, selected, res) + # バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false + state.pop + end + end + end + + ### 全順列 I ### + def permutations_i(nums) + res = [] + backtrack([], nums, Array.new(nums.length, false), res) + res + end ``` -## 13.2.2   重複要素を考慮する場合 +??? pythontutor "コードの可視化" + +
+ + +## 13.2.2   等しい要素を考慮する場合 !!! question - **重複要素を含む可能性のある**整数配列が与えられた場合、すべての一意の順列を返してください。 + 整数配列を入力として受け取り、**配列には重複要素が含まれる場合があります**。重複しない順列をすべて返します。 -入力配列が $[1, 1, 2]$ だとします。2つの同一要素 $1$ を区別するために、2番目を $\hat{1}$ とラベル付けします。 +入力配列が $[1, 1, 2]$ だと仮定します。2 つの重複する要素 $1$ を区別しやすくするため、2 つ目の $1$ を $\hat{1}$ と記します。 -以下の図に示すように、この方法で生成される順列の半分は重複です: +下図のように、上述の方法で生成される順列の半分は重複しています。 -![重複順列](permutations_problem.assets/permutations_ii.png){ class="animation-figure" } +![重複した順列](permutations_problem.assets/permutations_ii.png){ class="animation-figure" } -

図 13-7   重複順列

+

図 13-7   重複した順列

-では、これらの重複順列をどのように除去できるでしょうか?一つの直接的なアプローチは、すべての順列を生成した後にハッシュセットを使用して重複を除去することです。しかし、これはあまり優雅ではありません。**重複を生成する分岐は本来不要であり、事前に剪定されるべきだからです**、これによりアルゴリズムの効率が向上します。 +では、重複した順列をどのように取り除けばよいのでしょうか。最も直接的なのは、ハッシュ集合を用いて順列結果をそのまま重複排除する方法です。しかしこのやり方は十分に洗練されていません。**なぜなら、重複順列を生成する探索分岐はそもそも不要であり、事前に見つけて枝刈りすべきだからです**。そうすることで、アルゴリズム効率をさらに高められます。 -### 1.   等値要素の剪定 +### 1.   等しい要素の枝刈り -以下の図を見ると、最初のラウンドで $1$ または $\hat{1}$ を選択すると同じ順列につながるため、$\hat{1}$ を剪定します。 +下図を見ると、1 回目のラウンドでは $1$ を選ぶことと $\hat{1}$ を選ぶことは等価であり、これら 2 つの選択の下で生成される順列はすべて重複します。したがって $\hat{1}$ を枝刈りすべきです。 -同様に、最初のラウンドで $2$ を選択した後、2番目のラウンドで $1$ または $\hat{1}$ を選択しても重複分岐につながるため、その時も $\hat{1}$ を剪定します。 +同様に、1 回目で $2$ を選んだ後では、2 回目のラウンドにおける $1$ と $\hat{1}$ も重複分岐を生むため、2 回目の $\hat{1}$ も枝刈りすべきです。 -本質的に、**私たちの目標は、複数の同一要素が選択の各ラウンドで一度だけ選択されることを保証することです。** +本質的には、**各ラウンドの選択において、等しい複数の要素が 1 回しか選ばれないようにすることが目標です**。 -![重複順列の剪定](permutations_problem.assets/permutations_ii_pruning.png){ class="animation-figure" } +![重複順列の枝刈り](permutations_problem.assets/permutations_ii_pruning.png){ class="animation-figure" } -

図 13-8   重複順列の剪定

+

図 13-8   重複順列の枝刈り

### 2.   コード実装 -前の問題のコードに基づいて、各ラウンドでハッシュセット `duplicated` を導入します。このセットは、すでに試行した要素を追跡し、重複を剪定できるようにします: +前問のコードを土台として、各ラウンドの選択でハッシュ集合 `duplicated` を 1 つ用意し、そのラウンドですでに試した要素を記録して、重複要素を枝刈りすることを考えます。 === "Python" @@ -276,28 +581,28 @@ comments: true def backtrack( state: list[int], choices: list[int], selected: list[bool], res: list[list[int]] ): - """バックトラッキングアルゴリズム:順列 II""" - # 状態の長さが要素数と等しいとき、解を記録 + """バックトラッキング:順列 II""" + # 状態の長さが要素数に等しければ、解を記録 if len(state) == len(choices): res.append(list(state)) return # すべての選択肢を走査 duplicated = set[int]() for i, choice in enumerate(choices): - # 枝刈り:要素の重複選択を許可せず、等しい要素の重複選択も許可しない + # 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない if not selected[i] and choice not in duplicated: - # 試行:選択を行い、状態を更新 - duplicated.add(choice) # 選択された要素値を記録 + # 試行: 選択を行い、状態を更新 + duplicated.add(choice) # 選択済みの要素値を記録 selected[i] = True state.append(choice) - # 次の選択ラウンドに進む + # 次の選択へ進む backtrack(state, choices, selected, res) - # 撤回:選択を取り消し、前の状態に復元 + # バックトラック:選択を取り消し、前の状態に戻す selected[i] = False state.pop() def permutations_ii(nums: list[int]) -> list[list[int]]: - """順列 II""" + """全順列 II""" res = [] backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res) return res @@ -306,9 +611,9 @@ comments: true === "C++" ```cpp title="permutations_ii.cpp" - /* バックトラッキングアルゴリズム:順列 II */ + /* バックトラッキング:順列 II */ void backtrack(vector &state, const vector &choices, vector &selected, vector> &res) { - // 状態の長さが要素数と等しくなったら、解を記録 + // 状態の長さが要素数に等しければ、解を記録 if (state.size() == choices.size()) { res.push_back(state); return; @@ -317,22 +622,22 @@ comments: true unordered_set duplicated; for (int i = 0; i < choices.size(); i++) { int choice = choices[i]; - // 剪定:要素の重複選択を許可せず、等しい要素の重複選択も許可しない + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない if (!selected[i] && duplicated.find(choice) == duplicated.end()) { - // 試行:選択を行い、状態を更新 - duplicated.emplace(choice); // 選択された要素値を記録 + // 試行: 選択を行い、状態を更新 + duplicated.emplace(choice); // 選択済みの要素値を記録 selected[i] = true; state.push_back(choice); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, choices, selected, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す selected[i] = false; state.pop_back(); } } } - /* 順列 II */ + /* 全順列 II */ vector> permutationsII(vector nums) { vector state; vector selected(nums.size(), false); @@ -345,9 +650,9 @@ comments: true === "Java" ```java title="permutations_ii.java" - /* バックトラッキングアルゴリズム:順列 II */ + /* バックトラッキング:順列 II */ void backtrack(List state, int[] choices, boolean[] selected, List> res) { - // 状態の長さが要素数と等しくなったら、解を記録 + // 状態の長さが要素数に等しければ、解を記録 if (state.size() == choices.length) { res.add(new ArrayList(state)); return; @@ -356,22 +661,22 @@ comments: true Set duplicated = new HashSet(); for (int i = 0; i < choices.length; i++) { int choice = choices[i]; - // 剪定:要素の重複選択を許可せず、等しい要素の重複選択も許可しない + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない if (!selected[i] && !duplicated.contains(choice)) { - // 試行:選択を行い、状態を更新 - duplicated.add(choice); // 選択された要素値を記録 + // 試行: 選択を行い、状態を更新 + duplicated.add(choice); // 選択済みの要素値を記録 selected[i] = true; state.add(choice); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, choices, selected, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す selected[i] = false; state.remove(state.size() - 1); } } } - /* 順列 II */ + /* 全順列 II */ List> permutationsII(int[] nums) { List> res = new ArrayList>(); backtrack(new ArrayList(), nums, new boolean[nums.length], res); @@ -382,96 +687,422 @@ comments: true === "C#" ```csharp title="permutations_ii.cs" - [class]{permutations_ii}-[func]{Backtrack} + /* バックトラッキング:順列 II */ + void Backtrack(List state, int[] choices, bool[] selected, List> res) { + // 状態の長さが要素数に等しければ、解を記録 + if (state.Count == choices.Length) { + res.Add(new List(state)); + return; + } + // すべての選択肢を走査 + HashSet duplicated = []; + for (int i = 0; i < choices.Length; i++) { + int choice = choices[i]; + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない + if (!selected[i] && !duplicated.Contains(choice)) { + // 試行: 選択を行い、状態を更新 + duplicated.Add(choice); // 選択済みの要素値を記録 + selected[i] = true; + state.Add(choice); + // 次の選択へ進む + Backtrack(state, choices, selected, res); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + state.RemoveAt(state.Count - 1); + } + } + } - [class]{permutations_ii}-[func]{PermutationsII} + /* 全順列 II */ + List> PermutationsII(int[] nums) { + List> res = []; + Backtrack([], nums, new bool[nums.Length], res); + return res; + } ``` === "Go" ```go title="permutations_ii.go" - [class]{}-[func]{backtrackII} + /* バックトラッキング:順列 II */ + func backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) { + // 状態の長さが要素数に等しければ、解を記録 + if len(*state) == len(*choices) { + newState := append([]int{}, *state...) + *res = append(*res, newState) + } + // すべての選択肢を走査 + duplicated := make(map[int]struct{}, 0) + for i := 0; i < len(*choices); i++ { + choice := (*choices)[i] + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない + if _, ok := duplicated[choice]; !ok && !(*selected)[i] { + // 試す: 選択を行って状態を更新 + // 選択済みの要素値を記録 + duplicated[choice] = struct{}{} + (*selected)[i] = true + *state = append(*state, choice) + // 次の選択へ進む + backtrackII(state, choices, selected, res) + // バックトラック:選択を取り消し、前の状態に戻す + (*selected)[i] = false + *state = (*state)[:len(*state)-1] + } + } + } - [class]{}-[func]{permutationsII} + /* 全順列 II */ + func permutationsII(nums []int) [][]int { + res := make([][]int, 0) + state := make([]int, 0) + selected := make([]bool, len(nums)) + backtrackII(&state, &nums, &selected, &res) + return res + } ``` === "Swift" ```swift title="permutations_ii.swift" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 II */ + func backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) { + // 状態の長さが要素数に等しければ、解を記録 + if state.count == choices.count { + res.append(state) + return + } + // すべての選択肢を走査 + var duplicated: Set = [] + for (i, choice) in choices.enumerated() { + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない + if !selected[i], !duplicated.contains(choice) { + // 試行: 選択を行い、状態を更新 + duplicated.insert(choice) // 選択済みの要素値を記録 + selected[i] = true + state.append(choice) + // 次の選択へ進む + backtrack(state: &state, choices: choices, selected: &selected, res: &res) + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false + state.removeLast() + } + } + } - [class]{}-[func]{permutationsII} + /* 全順列 II */ + func permutationsII(nums: [Int]) -> [[Int]] { + var state: [Int] = [] + var selected = Array(repeating: false, count: nums.count) + var res: [[Int]] = [] + backtrack(state: &state, choices: nums, selected: &selected, res: &res) + return res + } ``` === "JS" ```javascript title="permutations_ii.js" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 II */ + function backtrack(state, choices, selected, res) { + // 状態の長さが要素数に等しければ、解を記録 + if (state.length === choices.length) { + res.push([...state]); + return; + } + // すべての選択肢を走査 + const duplicated = new Set(); + choices.forEach((choice, i) => { + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない + if (!selected[i] && !duplicated.has(choice)) { + // 試行: 選択を行い、状態を更新 + duplicated.add(choice); // 選択済みの要素値を記録 + selected[i] = true; + state.push(choice); + // 次の選択へ進む + backtrack(state, choices, selected, res); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + state.pop(); + } + }); + } - [class]{}-[func]{permutationsII} + /* 全順列 II */ + function permutationsII(nums) { + const res = []; + backtrack([], nums, Array(nums.length).fill(false), res); + return res; + } ``` === "TS" ```typescript title="permutations_ii.ts" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 II */ + function backtrack( + state: number[], + choices: number[], + selected: boolean[], + res: number[][] + ): void { + // 状態の長さが要素数に等しければ、解を記録 + if (state.length === choices.length) { + res.push([...state]); + return; + } + // すべての選択肢を走査 + const duplicated = new Set(); + choices.forEach((choice, i) => { + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない + if (!selected[i] && !duplicated.has(choice)) { + // 試行: 選択を行い、状態を更新 + duplicated.add(choice); // 選択済みの要素値を記録 + selected[i] = true; + state.push(choice); + // 次の選択へ進む + backtrack(state, choices, selected, res); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + state.pop(); + } + }); + } - [class]{}-[func]{permutationsII} + /* 全順列 II */ + function permutationsII(nums: number[]): number[][] { + const res: number[][] = []; + backtrack([], nums, Array(nums.length).fill(false), res); + return res; + } ``` === "Dart" ```dart title="permutations_ii.dart" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 II */ + void backtrack( + List state, + List choices, + List selected, + List> res, + ) { + // 状態の長さが要素数に等しければ、解を記録 + if (state.length == choices.length) { + res.add(List.from(state)); + return; + } + // すべての選択肢を走査 + Set duplicated = {}; + for (int i = 0; i < choices.length; i++) { + int choice = choices[i]; + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない + if (!selected[i] && !duplicated.contains(choice)) { + // 試行: 選択を行い、状態を更新 + duplicated.add(choice); // 選択済みの要素値を記録 + selected[i] = true; + state.add(choice); + // 次の選択へ進む + backtrack(state, choices, selected, res); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + state.removeLast(); + } + } + } - [class]{}-[func]{permutationsII} + /* 全順列 II */ + List> permutationsII(List nums) { + List> res = []; + backtrack([], nums, List.filled(nums.length, false), res); + return res; + } ``` === "Rust" ```rust title="permutations_ii.rs" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 II */ + fn backtrack(mut state: Vec, choices: &[i32], selected: &mut [bool], res: &mut Vec>) { + // 状態の長さが要素数に等しければ、解を記録 + if state.len() == choices.len() { + res.push(state); + return; + } + // すべての選択肢を走査 + let mut duplicated = HashSet::::new(); + for i in 0..choices.len() { + let choice = choices[i]; + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない + if !selected[i] && !duplicated.contains(&choice) { + // 試行: 選択を行い、状態を更新 + duplicated.insert(choice); // 選択済みの要素値を記録 + selected[i] = true; + state.push(choice); + // 次の選択へ進む + backtrack(state.clone(), choices, selected, res); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + state.pop(); + } + } + } - [class]{}-[func]{permutations_ii} + /* 全順列 II */ + fn permutations_ii(nums: &mut [i32]) -> Vec> { + let mut res = Vec::new(); + backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res); + res + } ``` === "C" ```c title="permutations_ii.c" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 II */ + void backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) { + // 状態の長さが要素数に等しければ、解を記録 + if (stateSize == choicesSize) { + res[*resSize] = (int *)malloc(choicesSize * sizeof(int)); + for (int i = 0; i < choicesSize; i++) { + res[*resSize][i] = state[i]; + } + (*resSize)++; + return; + } + // すべての選択肢を走査 + bool duplicated[MAX_SIZE] = {false}; + for (int i = 0; i < choicesSize; i++) { + int choice = choices[i]; + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない + if (!selected[i] && !duplicated[choice]) { + // 試行: 選択を行い、状態を更新 + duplicated[choice] = true; // 選択済みの要素値を記録 + selected[i] = true; + state[stateSize] = choice; + // 次の選択へ進む + backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize); + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false; + } + } + } - [class]{}-[func]{permutationsII} + /* 全順列 II */ + int **permutationsII(int *nums, int numsSize, int *returnSize) { + int *state = (int *)malloc(numsSize * sizeof(int)); + bool *selected = (bool *)malloc(numsSize * sizeof(bool)); + for (int i = 0; i < numsSize; i++) { + selected[i] = false; + } + int **res = (int **)malloc(MAX_SIZE * sizeof(int *)); + *returnSize = 0; + + backtrack(state, 0, nums, numsSize, selected, res, returnSize); + + free(state); + free(selected); + + return res; + } ``` === "Kotlin" ```kotlin title="permutations_ii.kt" - [class]{}-[func]{backtrack} + /* バックトラッキング:順列 II */ + fun backtrack( + state: MutableList, + choices: IntArray, + selected: BooleanArray, + res: MutableList?> + ) { + // 状態の長さが要素数に等しければ、解を記録 + if (state.size == choices.size) { + res.add(state.toMutableList()) + return + } + // すべての選択肢を走査 + val duplicated = HashSet() + for (i in choices.indices) { + val choice = choices[i] + // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない + if (!selected[i] && !duplicated.contains(choice)) { + // 試行: 選択を行い、状態を更新 + duplicated.add(choice) // 選択済みの要素値を記録 + selected[i] = true + state.add(choice) + // 次の選択へ進む + backtrack(state, choices, selected, res) + // バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false + state.removeAt(state.size - 1) + } + } + } - [class]{}-[func]{permutationsII} + /* 全順列 II */ + fun permutationsII(nums: IntArray): MutableList?> { + val res = mutableListOf?>() + backtrack(mutableListOf(), nums, BooleanArray(nums.size), res) + return res + } ``` === "Ruby" ```ruby title="permutations_ii.rb" - [class]{}-[func]{backtrack} + ### バックトラッキング法:全順列 II ### + def backtrack(state, choices, selected, res) + # 状態の長さが要素数に等しければ、解を記録 + if state.length == choices.length + res << state.dup + return + end - [class]{}-[func]{permutations_ii} + # すべての選択肢を走査 + duplicated = Set.new + choices.each_with_index do |choice, i| + # 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない + if !selected[i] && !duplicated.include?(choice) + # 試行: 選択を行い、状態を更新 + duplicated.add(choice) + selected[i] = true + state << choice + # 次の選択へ進む + backtrack(state, choices, selected, res) + # バックトラック:選択を取り消し、前の状態に戻す + selected[i] = false + state.pop + end + end + end + + ### 全順列 II ### + def permutations_ii(nums) + res = [] + backtrack([], nums, Array.new(nums.length, false), res) + res + end ``` -すべての要素が異なると仮定すると、$n$ 個の要素の順列は $n!$ (階乗)個あります。各結果を記録するには長さ $n$ のリストをコピーする必要があり、これには $O(n)$ 時間がかかります。**したがって、総時間計算量は $O(n!n)$ です。** +??? pythontutor "コードの可視化" -最大再帰深度は $n$ で、$O(n)$ のスタック空間を使用します。`selected` 配列も $O(n)$ 空間が必要です。一度に最大 $n$ 個の個別の `duplicated` セットが存在する可能性があるため、それらは集合的に $O(n^2)$ 空間を占有します。**したがって、空間計算量は $O(n^2)$ です。** +
+ -### 3.   2つの剪定方法の比較 +要素どうしがすべて互いに異なると仮定すると、$n$ 個の要素には全部で $n!$ 通りの順列(階乗)があります。結果を記録する際には、長さ $n$ のリストをコピーする必要があり、これに $O(n)$ 時間を要します。**したがって時間計算量は $O(n!n)$** です。 -`selected` と `duplicated` はどちらも剪定メカニズムとして機能しますが、異なる問題をターゲットにしています: +再帰の最大深さは $n$ であり、$O(n)$ のスタックフレーム空間を使います。`selected` は $O(n)$ 空間を使用します。同時刻に存在する `duplicated` は最大で $n$ 個であり、$O(n^2)$ 空間を要します。**したがって空間計算量は $O(n^2)$** です。 -- **重複選択の剪定**(`selected` 経由):検索全体に単一の `selected` 配列があり、現在の状態にすでにある要素を示します。これにより、同じ要素が `state` に複数回現れることを防ぎます。 -- **等値要素の剪定**(`duplicated` 経由):`backtrack` 関数の各呼び出しは独自の `duplicated` セットを使用し、その特定の反復(`for` ループ)ですでに選択された要素を記録します。これにより、等しい要素が選択の各ラウンドで一度だけ選択されることを保証します。 +### 3.   2 種類の枝刈りの比較 -以下の図は、これら2つの剪定戦略の範囲を示しています。木の各ノードは選択を表します。ルートから任意の葉への経路は、1つの完全な順列に対応します。 +`selected` と `duplicated` はどちらも枝刈りに用いられますが、目的は異なる点に注意してください。 -![2つの剪定条件の範囲](permutations_problem.assets/permutations_ii_pruning_summary.png){ class="animation-figure" } +- **重複選択の枝刈り**:探索全体を通して `selected` は 1 つだけです。これは現在の状態にどの要素が含まれているかを記録し、ある要素が `state` に重複して現れるのを防ぎます。 +- **等しい要素の枝刈り**:各ラウンドの選択、すなわち各回の `backtrack` 呼び出しには `duplicated` が含まれます。これはそのラウンドの走査(`for` ループ)でどの要素がすでに選ばれたかを記録し、等しい要素が 1 回しか選ばれないことを保証します。 -

図 13-9   2つの剪定条件の範囲

+下図は、2 つの枝刈り条件が有効になる範囲を示しています。木の各ノードは 1 つの選択を表し、根ノードから葉ノードまでの経路上の各ノードが 1 つの順列を構成することに注意してください。 + +![2 種類の枝刈り条件の作用範囲](permutations_problem.assets/permutations_ii_pruning_summary.png){ class="animation-figure" } + +

図 13-9   2 種類の枝刈り条件の作用範囲

diff --git a/ja/docs/chapter_backtracking/subset_sum_problem.md b/ja/docs/chapter_backtracking/subset_sum_problem.md index ca30de042..9747f7052 100644 --- a/ja/docs/chapter_backtracking/subset_sum_problem.md +++ b/ja/docs/chapter_backtracking/subset_sum_problem.md @@ -2,24 +2,24 @@ comments: true --- -# 13.3   部分集合和問題 +# 13.3   部分和問題 -## 13.3.1   重複要素がない場合 +## 13.3.1   重複しない要素の場合 !!! question - 正の整数の配列 `nums` とターゲット正整数 `target` が与えられた場合、組み合わせ内の要素の和が `target` に等しくなるようなすべての可能な組み合わせを見つけてください。与えられた配列には重複要素がなく、各要素は複数回選択できます。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。 + 正整数配列 `nums` と目標の正整数 `target` が与えられたとき、要素の和が `target` に等しくなるすべての組合せを見つけてください。配列に重複要素はなく、各要素は複数回選択できます。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。 -例えば、入力集合 $\{3, 4, 5\}$ とターゲット整数 $9$ の場合、解は $\{3, 3, 3\}, \{4, 5\}$ です。以下の2点に注意してください。 +例えば、入力集合 $\{3, 4, 5\}$ と目標整数 $9$ に対する解は $\{3, 3, 3\}, \{4, 5\}$ です。次の 2 点に注意してください。 -- 入力集合の要素は無制限に選択できます。 -- 部分集合は要素の順序を区別しません。例えば $\{4, 5\}$ と $\{5, 4\}$ は同じ部分集合です。 +- 入力集合内の要素は何度でも繰り返し選択できます。 +- 部分集合では要素の順序を区別しません。例えば $\{4, 5\}$ と $\{5, 4\}$ は同じ部分集合です。 -### 1.   順列解法の参考 +### 1.   全順列の解法を参考にする -順列問題と同様に、部分集合の生成を一連の選択として想像でき、選択プロセス中に「要素和」をリアルタイムで更新できます。要素和が `target` に等しくなったとき、部分集合を結果リストに記録します。 +全順列問題と同様に、部分集合の生成過程を一連の選択結果として捉え、選択の過程で「要素の和」を逐次更新できます。そして要素の和が `target` に等しくなった時点で、その部分集合を結果リストに記録します。 -順列問題とは異なり、**この問題では要素は無制限に選択できるため**、要素が選択されたかどうかを記録するための `selected` ブール配列を使用する必要がありません。順列コードに軽微な修正を加えて、最初に問題を解決できます: +ただし全順列問題と異なるのは、**この問題では集合内の要素を無制限に選択できる**点です。そのため、要素がすでに選択されたかどうかを記録する `selected` ブール配列は不要です。全順列のコードに少し修正を加えると、まず次の解法コードが得られます。 === "Python" @@ -31,28 +31,28 @@ comments: true choices: list[int], res: list[list[int]], ): - """バックトラッキングアルゴリズム:部分集合の和 I""" - # 部分集合の和が target と等しいとき、解を記録 + """バックトラッキング:部分和 I""" + # 部分集合の和が target に等しければ、解を記録 if total == target: res.append(list(state)) return # すべての選択肢を走査 for i in range(len(choices)): - # 枝刈り:部分集合の和が target を超える場合、その選択をスキップ + # 枝刈り:部分和が target を超える場合はその選択をスキップする if total + choices[i] > target: continue - # 試行:選択を行い、要素と total を更新 + # 試行:選択を行い、要素と total を更新する state.append(choices[i]) - # 次の選択ラウンドに進む + # 次の選択へ進む backtrack(state, target, total + choices[i], choices, res) - # 撤回:選択を取り消し、前の状態に復元 + # バックトラック:選択を取り消し、前の状態に戻す state.pop() def subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]: - """部分集合の和 I を解く(重複する部分集合を含む)""" + """部分和 I を解く(重複部分集合を含む)""" state = [] # 状態(部分集合) - total = 0 # 部分集合の和 - res = [] # 結果リスト(部分集合リスト) + total = 0 # 部分和 + res = [] # 結果リスト(部分集合のリスト) backtrack(state, target, total, nums, res) return res ``` @@ -60,33 +60,33 @@ comments: true === "C++" ```cpp title="subset_sum_i_naive.cpp" - /* バックトラッキングアルゴリズム:部分集合和 I */ + /* バックトラッキング:部分和 I */ void backtrack(vector &state, int target, int total, vector &choices, vector> &res) { - // 部分集合の和がtargetと等しいとき、解を記録 + // 部分集合の和が target に等しければ、解を記録 if (total == target) { res.push_back(state); return; } // すべての選択肢を走査 - for (int i = 0; i < choices.size(); i++) { - // 剪定:部分集合の和がtargetを超えた場合、その選択をスキップ + for (size_t i = 0; i < choices.size(); i++) { + // 枝刈り:部分和が target を超える場合はその選択をスキップする if (total + choices[i] > target) { continue; } - // 試行:選択を行い、要素とtotalを更新 + // 試行:選択を行い、要素と total を更新する state.push_back(choices[i]); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, target, total + choices[i], choices, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す state.pop_back(); } } - /* 部分集合和 I を解く(重複する部分集合を含む) */ - vector> subsetSumINaive(vector nums, int target) { + /* 部分和 I を解く(重複部分集合を含む) */ + vector> subsetSumINaive(vector &nums, int target) { vector state; // 状態(部分集合) - int total = 0; // 部分集合の和 - vector> res; // 結果リスト(部分集合リスト) + int total = 0; // 部分和 + vector> res; // 結果リスト(部分集合のリスト) backtrack(state, target, total, nums, res); return res; } @@ -95,33 +95,33 @@ comments: true === "Java" ```java title="subset_sum_i_naive.java" - /* バックトラッキングアルゴリズム:部分集合和 I */ + /* バックトラッキング:部分和 I */ void backtrack(List state, int target, int total, int[] choices, List> res) { - // 部分集合の和がtargetと等しいとき、解を記録 + // 部分集合の和が target に等しければ、解を記録 if (total == target) { res.add(new ArrayList<>(state)); return; } // すべての選択肢を走査 for (int i = 0; i < choices.length; i++) { - // 剪定:部分集合の和がtargetを超えた場合、その選択をスキップ + // 枝刈り:部分和が target を超える場合はその選択をスキップする if (total + choices[i] > target) { continue; } - // 試行:選択を行い、要素とtotalを更新 + // 試行:選択を行い、要素と total を更新する state.add(choices[i]); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, target, total + choices[i], choices, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す state.remove(state.size() - 1); } } - /* 部分集合和 I を解く(重複する部分集合を含む) */ + /* 部分和 I を解く(重複部分集合を含む) */ List> subsetSumINaive(int[] nums, int target) { List state = new ArrayList<>(); // 状態(部分集合) - int total = 0; // 部分集合の和 - List> res = new ArrayList<>(); // 結果リスト(部分集合リスト) + int total = 0; // 部分和 + List> res = new ArrayList<>(); // 結果リスト(部分集合のリスト) backtrack(state, target, total, nums, res); return res; } @@ -130,123 +130,443 @@ comments: true === "C#" ```csharp title="subset_sum_i_naive.cs" - [class]{subset_sum_i_naive}-[func]{Backtrack} + /* バックトラッキング:部分和 I */ + void Backtrack(List state, int target, int total, int[] choices, List> res) { + // 部分集合の和が target に等しければ、解を記録 + if (total == target) { + res.Add(new List(state)); + return; + } + // すべての選択肢を走査 + for (int i = 0; i < choices.Length; i++) { + // 枝刈り:部分和が target を超える場合はその選択をスキップする + if (total + choices[i] > target) { + continue; + } + // 試行:選択を行い、要素と total を更新する + state.Add(choices[i]); + // 次の選択へ進む + Backtrack(state, target, total + choices[i], choices, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.RemoveAt(state.Count - 1); + } + } - [class]{subset_sum_i_naive}-[func]{SubsetSumINaive} + /* 部分和 I を解く(重複部分集合を含む) */ + List> SubsetSumINaive(int[] nums, int target) { + List state = []; // 状態(部分集合) + int total = 0; // 部分和 + List> res = []; // 結果リスト(部分集合のリスト) + Backtrack(state, target, total, nums, res); + return res; + } ``` === "Go" ```go title="subset_sum_i_naive.go" - [class]{}-[func]{backtrackSubsetSumINaive} + /* バックトラッキング:部分和 I */ + func backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) { + // 部分集合の和が target に等しければ、解を記録 + if target == total { + newState := append([]int{}, *state...) + *res = append(*res, newState) + return + } + // すべての選択肢を走査 + for i := 0; i < len(*choices); i++ { + // 枝刈り:部分和が target を超える場合はその選択をスキップする + if total+(*choices)[i] > target { + continue + } + // 試行:選択を行い、要素と total を更新する + *state = append(*state, (*choices)[i]) + // 次の選択へ進む + backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res) + // バックトラック:選択を取り消し、前の状態に戻す + *state = (*state)[:len(*state)-1] + } + } - [class]{}-[func]{subsetSumINaive} + /* 部分和 I を解く(重複部分集合を含む) */ + func subsetSumINaive(nums []int, target int) [][]int { + state := make([]int, 0) // 状態(部分集合) + total := 0 // 部分和 + res := make([][]int, 0) // 結果リスト(部分集合のリスト) + backtrackSubsetSumINaive(total, target, &state, &nums, &res) + return res + } ``` === "Swift" ```swift title="subset_sum_i_naive.swift" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + func backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) { + // 部分集合の和が target に等しければ、解を記録 + if total == target { + res.append(state) + return + } + // すべての選択肢を走査 + for i in choices.indices { + // 枝刈り:部分和が target を超える場合はその選択をスキップする + if total + choices[i] > target { + continue + } + // 試行:選択を行い、要素と total を更新する + state.append(choices[i]) + // 次の選択へ進む + backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res) + // バックトラック:選択を取り消し、前の状態に戻す + state.removeLast() + } + } - [class]{}-[func]{subsetSumINaive} + /* 部分和 I を解く(重複部分集合を含む) */ + func subsetSumINaive(nums: [Int], target: Int) -> [[Int]] { + var state: [Int] = [] // 状態(部分集合) + let total = 0 // 部分和 + var res: [[Int]] = [] // 結果リスト(部分集合のリスト) + backtrack(state: &state, target: target, total: total, choices: nums, res: &res) + return res + } ``` === "JS" ```javascript title="subset_sum_i_naive.js" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + function backtrack(state, target, total, choices, res) { + // 部分集合の和が target に等しければ、解を記録 + if (total === target) { + res.push([...state]); + return; + } + // すべての選択肢を走査 + for (let i = 0; i < choices.length; i++) { + // 枝刈り:部分和が target を超える場合はその選択をスキップする + if (total + choices[i] > target) { + continue; + } + // 試行:選択を行い、要素と total を更新する + state.push(choices[i]); + // 次の選択へ進む + backtrack(state, target, total + choices[i], choices, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.pop(); + } + } - [class]{}-[func]{subsetSumINaive} + /* 部分和 I を解く(重複部分集合を含む) */ + function subsetSumINaive(nums, target) { + const state = []; // 状態(部分集合) + const total = 0; // 部分和 + const res = []; // 結果リスト(部分集合のリスト) + backtrack(state, target, total, nums, res); + return res; + } ``` === "TS" ```typescript title="subset_sum_i_naive.ts" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + function backtrack( + state: number[], + target: number, + total: number, + choices: number[], + res: number[][] + ): void { + // 部分集合の和が target に等しければ、解を記録 + if (total === target) { + res.push([...state]); + return; + } + // すべての選択肢を走査 + for (let i = 0; i < choices.length; i++) { + // 枝刈り:部分和が target を超える場合はその選択をスキップする + if (total + choices[i] > target) { + continue; + } + // 試行:選択を行い、要素と total を更新する + state.push(choices[i]); + // 次の選択へ進む + backtrack(state, target, total + choices[i], choices, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.pop(); + } + } - [class]{}-[func]{subsetSumINaive} + /* 部分和 I を解く(重複部分集合を含む) */ + function subsetSumINaive(nums: number[], target: number): number[][] { + const state = []; // 状態(部分集合) + const total = 0; // 部分和 + const res = []; // 結果リスト(部分集合のリスト) + backtrack(state, target, total, nums, res); + return res; + } ``` === "Dart" ```dart title="subset_sum_i_naive.dart" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + void backtrack( + List state, + int target, + int total, + List choices, + List> res, + ) { + // 部分集合の和が target に等しければ、解を記録 + if (total == target) { + res.add(List.from(state)); + return; + } + // すべての選択肢を走査 + for (int i = 0; i < choices.length; i++) { + // 枝刈り:部分和が target を超える場合はその選択をスキップする + if (total + choices[i] > target) { + continue; + } + // 試行:選択を行い、要素と total を更新する + state.add(choices[i]); + // 次の選択へ進む + backtrack(state, target, total + choices[i], choices, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.removeLast(); + } + } - [class]{}-[func]{subsetSumINaive} + /* 部分和 I を解く(重複部分集合を含む) */ + List> subsetSumINaive(List nums, int target) { + List state = []; // 状態(部分集合) + int total = 0; // 要素の合計 + List> res = []; // 結果リスト(部分集合のリスト) + backtrack(state, target, total, nums, res); + return res; + } ``` === "Rust" ```rust title="subset_sum_i_naive.rs" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + fn backtrack( + state: &mut Vec, + target: i32, + total: i32, + choices: &[i32], + res: &mut Vec>, + ) { + // 部分集合の和が target に等しければ、解を記録 + if total == target { + res.push(state.clone()); + return; + } + // すべての選択肢を走査 + for i in 0..choices.len() { + // 枝刈り:部分和が target を超える場合はその選択をスキップする + if total + choices[i] > target { + continue; + } + // 試行:選択を行い、要素と total を更新する + state.push(choices[i]); + // 次の選択へ進む + backtrack(state, target, total + choices[i], choices, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.pop(); + } + } - [class]{}-[func]{subset_sum_i_naive} + /* 部分和 I を解く(重複部分集合を含む) */ + fn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec> { + let mut state = Vec::new(); // 状態(部分集合) + let total = 0; // 部分和 + let mut res = Vec::new(); // 結果リスト(部分集合のリスト) + backtrack(&mut state, target, total, nums, &mut res); + res + } ``` === "C" ```c title="subset_sum_i_naive.c" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + void backtrack(int target, int total, int *choices, int choicesSize) { + // 部分集合の和が target に等しければ、解を記録 + if (total == target) { + for (int i = 0; i < stateSize; i++) { + res[resSize][i] = state[i]; + } + resColSizes[resSize++] = stateSize; + return; + } + // すべての選択肢を走査 + for (int i = 0; i < choicesSize; i++) { + // 枝刈り:部分和が target を超える場合はその選択をスキップする + if (total + choices[i] > target) { + continue; + } + // 試行:選択を行い、要素と total を更新する + state[stateSize++] = choices[i]; + // 次の選択へ進む + backtrack(target, total + choices[i], choices, choicesSize); + // バックトラック:選択を取り消し、前の状態に戻す + stateSize--; + } + } - [class]{}-[func]{subsetSumINaive} + /* 部分和 I を解く(重複部分集合を含む) */ + void subsetSumINaive(int *nums, int numsSize, int target) { + resSize = 0; // 解の個数を 0 に初期化する + backtrack(target, 0, nums, numsSize); + } ``` === "Kotlin" ```kotlin title="subset_sum_i_naive.kt" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + fun backtrack( + state: MutableList, + target: Int, + total: Int, + choices: IntArray, + res: MutableList?> + ) { + // 部分集合の和が target に等しければ、解を記録 + if (total == target) { + res.add(state.toMutableList()) + return + } + // すべての選択肢を走査 + for (i in choices.indices) { + // 枝刈り:部分和が target を超える場合はその選択をスキップする + if (total + choices[i] > target) { + continue + } + // 試行:選択を行い、要素と total を更新する + state.add(choices[i]) + // 次の選択へ進む + backtrack(state, target, total + choices[i], choices, res) + // バックトラック:選択を取り消し、前の状態に戻す + state.removeAt(state.size - 1) + } + } - [class]{}-[func]{subsetSumINaive} + /* 部分和 I を解く(重複部分集合を含む) */ + fun subsetSumINaive(nums: IntArray, target: Int): MutableList?> { + val state = mutableListOf() // 状態(部分集合) + val total = 0 // 部分和 + val res = mutableListOf?>() // 結果リスト(部分集合のリスト) + backtrack(state, target, total, nums, res) + return res + } ``` === "Ruby" ```ruby title="subset_sum_i_naive.rb" - [class]{}-[func]{backtrack} + ### バックトラッキング: 部分和 I ### + def backtrack(state, target, total, choices, res) + # 部分集合の和が target に等しければ、解を記録 + if total == target + res << state.dup + return + end - [class]{}-[func]{subset_sum_i_naive} + # すべての選択肢を走査 + for i in 0...choices.length + # 枝刈り:部分和が target を超える場合はその選択をスキップする + next if total + choices[i] > target + # 試行:選択を行い、要素と total を更新する + state << choices[i] + # 次の選択へ進む + backtrack(state, target, total + choices[i], choices, res) + # バックトラック:選択を取り消し、前の状態に戻す + state.pop + end + end + + ### バックトラッキング: 部分和 I ### + def backtrack(state, target, total, choices, res) + # 部分集合の和が target に等しければ、解を記録 + if total == target + res << state.dup + return + end + + # すべての選択肢を走査 + for i in 0...choices.length + # 枝刈り:部分和が target を超える場合はその選択をスキップする + next if total + choices[i] > target + # 試行:選択を行い、要素と total を更新する + state << choices[i] + # 次の選択へ進む + backtrack(state, target, total + choices[i], choices, res) + # バックトラック:選択を取り消し、前の状態に戻す + state.pop + end + end + + # ## 部分和 I を解く(重複する部分集合を含む)### + def subset_sum_i_naive(nums, target) + state = [] # 状態(部分集合) + total = 0 # 部分和 + res = [] # 結果リスト(部分集合のリスト) + backtrack(state, target, total, nums, res) + res + end ``` -配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力すると、結果 $[3, 3, 3], [4, 5], [5, 4]$ が得られます。**和が $9$ のすべての部分集合を正常に見つけましたが、重複する部分集合 $[4, 5]$ と $[5, 4]$ が含まれています**。 +??? pythontutor "コードの可視化" -これは、検索プロセスが選択の順序を区別するためですが、部分集合は選択順序を区別しません。以下の図に示すように、$5$ の前に $4$ を選択することと $4$ の前に $5$ を選択することは異なる分岐ですが、同じ部分集合に対応します。 +
+ -![部分集合の検索と境界外の剪定](subset_sum_problem.assets/subset_sum_i_naive.png){ class="animation-figure" } +上のコードに配列 $[3, 4, 5]$ と目標値 $9$ を入力すると、出力は $[3, 3, 3], [4, 5], [5, 4]$ となります。**和が $9$ となる部分集合はすべて見つかっていますが、重複する部分集合 $[4, 5]$ と $[5, 4]$ が含まれています**。 -

図 13-10   部分集合の検索と境界外の剪定

+これは、探索過程では選択順を区別する一方で、部分集合では選択順を区別しないためです。次の図のように、先に $4$ を選んでから $5$ を選ぶ場合と、先に $5$ を選んでから $4$ を選ぶ場合は別の分岐ですが、対応する部分集合は同じです。 -重複する部分集合を除去するために、**直接的なアイデアは結果リストを重複除去することです**。しかし、この方法は2つの理由で非常に非効率的です。 +![部分集合探索と境界超過の枝刈り](subset_sum_problem.assets/subset_sum_i_naive.png){ class="animation-figure" } -- 配列要素が多い場合、特に `target` が大きい場合、検索プロセスで大量の重複する部分集合が生成されます。 -- 部分集合(配列)の差異を比較することは非常に時間がかかり、まず配列をソートし、次に配列の各要素の差異を比較する必要があります。 +

図 13-10   部分集合探索と境界超過の枝刈り

-### 2.   重複部分集合の剪定 +重複する部分集合を取り除くために、**直接的な方法として結果リストの重複を除去する**ことが考えられます。しかし、この方法は効率が低く、その理由は次の 2 点です。 -**剪定を通じて検索プロセス中に重複除去を検討します**。以下の図を観察すると、異なる順序で配列要素を選択するときに重複する部分集合が生成されます。例えば、以下の状況です。 +- 配列要素が多い場合、特に `target` が大きい場合には、探索過程で大量の重複部分集合が生成されます。 +- 部分集合(配列)同士の違いを比較するのは非常に時間がかかり、まず配列をソートし、その後に各要素を比較する必要があります。 -1. 最初のラウンドで $3$ を選択し、2番目のラウンドで $4$ を選択すると、これら2つの要素を含むすべての部分集合が生成され、$[3, 4, \dots]$ と表記されます。 -2. 後で、最初のラウンドで $4$ が選択されたとき、**2番目のラウンドは $3$ をスキップすべきです**。この選択によって生成される部分集合 $[4, 3, \dots]$ はステップ `1.` の部分集合と完全に重複するからです。 +### 2.   重複部分集合の枝刈り -検索プロセスでは、各層の選択が左から右に一つずつ試行されるため、右側の分岐ほどより多く剪定されます。 +**探索過程で枝刈りを行って重複を除去する**ことを考えます。次の図を観察すると、重複部分集合は配列要素を異なる順序で選択したときに生じます。例えば次のような状況です。 -1. 最初の2ラウンドで $3$ と $5$ を選択し、部分集合 $[3, 5, \dots]$ を生成します。 -2. 最初の2ラウンドで $4$ と $5$ を選択し、部分集合 $[4, 5, \dots]$ を生成します。 -3. 最初のラウンドで $5$ が選択された場合、**2番目のラウンドは $3$ と $4$ をスキップすべきです**。部分集合 $[5, 3, \dots]$ と $[5, 4, \dots]$ はステップ `1.` と `2.` で記述された部分集合と完全に重複するからです。 +1. 1 回目と 2 回目でそれぞれ $3$ と $4$ を選ぶと、これら 2 要素を含むすべての部分集合、すなわち $[3, 4, \dots]$ が生成されます。 +2. その後、1 回目で $4$ を選んだ場合、**2 回目では $3$ をスキップすべき**です。というのも、この選択で生成される部分集合 $[4, 3, \dots]$ は、手順 `1.` で生成された部分集合と完全に重複するからです。 -![異なる選択順序による重複部分集合](subset_sum_problem.assets/subset_sum_i_pruning.png){ class="animation-figure" } +探索過程では、各階層の選択は左から右へ順に試されるため、右側にある分岐ほど多く枝刈りされます。 -

図 13-11   異なる選択順序による重複部分集合

+1. 最初の 2 回で $3$ と $5$ を選ぶと、部分集合 $[3, 5, \dots]$ が生成されます。 +2. 最初の 2 回で $4$ と $5$ を選ぶと、部分集合 $[4, 5, \dots]$ が生成されます。 +3. もし 1 回目で $5$ を選ぶなら、**2 回目では $3$ と $4$ をスキップすべき**です。なぜなら、部分集合 $[5, 3, \dots]$ と $[5, 4, \dots]$ は、手順 `1.` と手順 `2.` で述べた部分集合と完全に重複するからです。 -要約すると、入力配列 $[x_1, x_2, \dots, x_n]$ が与えられた場合、検索プロセスでの選択シーケンスは $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$ であるべきで、$i_1 \leq i_2 \leq \dots \leq i_m$ を満たす必要があります。**この条件を満たさない選択シーケンスは重複を引き起こし、剪定されるべきです**。 +![異なる選択順によって生じる重複部分集合](subset_sum_problem.assets/subset_sum_i_pruning.png){ class="animation-figure" } + +

図 13-11   異なる選択順によって生じる重複部分集合

+ +まとめると、入力配列 $[x_1, x_2, \dots, x_n]$ が与えられ、探索過程における選択列を $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$ とすると、この選択列は $i_1 \leq i_2 \leq \dots \leq i_m$ を満たす必要があります。**この条件を満たさない選択列は重複を生むため、枝刈りすべきです**。 ### 3.   コード実装 -この剪定を実装するために、変数 `start` を初期化し、これは走査の開始点を示します。**選択 $x_{i}$ を行った後、次のラウンドをインデックス $i$ から開始するように設定します**。これにより、選択シーケンスが $i_1 \leq i_2 \leq \dots \leq i_m$ を満たすことが保証され、部分集合の一意性が保証されます。 +この枝刈りを実現するために、走査の開始位置を示す変数 `start` を初期化します。**選択 $x_{i}$ を行った後、次のラウンドはインデックス $i$ から走査を開始する**ように設定します。これにより、選択列が $i_1 \leq i_2 \leq \dots \leq i_m$ を満たし、部分集合の一意性が保証されます。 -さらに、コードに以下の2つの最適化を行いました。 +これに加えて、コードには次の 2 つの最適化も施しています。 -- 検索を開始する前に、配列 `nums` をソートします。すべての選択の走査で、**部分集合和が `target` を超えたときにループを直接終了します**。後続の要素はより大きく、それらの部分集合和は確実に `target` を超えるからです。 -- 要素和変数 `total` を除去し、**`target` に対して減算を実行して要素和をカウントします**。`target` が $0$ に等しくなったとき、解を記録します。 +- 探索を始める前に、まず配列 `nums` をソートします。すべての選択肢を走査するとき、**部分集合の和が `target` を超えたら直ちにループを終了**します。後続の要素はさらに大きいため、その和も必ず `target` を超えるからです。 +- 要素和を保持する変数 `total` は省略し、**`target` から減算することで要素和を管理**します。`target` が $0$ になったときに解を記録します。 === "Python" @@ -254,31 +574,31 @@ comments: true def backtrack( state: list[int], target: int, choices: list[int], start: int, res: list[list[int]] ): - """バックトラッキングアルゴリズム:部分集合の和 I""" - # 部分集合の和が target と等しいとき、解を記録 + """バックトラッキング:部分和 I""" + # 部分集合の和が target に等しければ、解を記録 if target == 0: res.append(list(state)) return # すべての選択肢を走査 - # 枝刈り二:start から走査を開始して重複する部分集合の生成を避ける + # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける for i in range(start, len(choices)): - # 枝刈り一:部分集合の和が target を超える場合、直ちにループを終了 - # これは配列がソートされており、後の要素がより大きいため、部分集合の和は必ず target を超えるため + # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため if target - choices[i] < 0: break - # 試行:選択を行い、target、start を更新 + # 試す:選択を行い、target と start を更新 state.append(choices[i]) - # 次の選択ラウンドに進む + # 次の選択へ進む backtrack(state, target - choices[i], choices, i, res) - # 撤回:選択を取り消し、前の状態に復元 + # バックトラック:選択を取り消し、前の状態に戻す state.pop() def subset_sum_i(nums: list[int], target: int) -> list[list[int]]: - """部分集合の和 I を解く""" + """部分和 I を解く""" state = [] # 状態(部分集合) nums.sort() # nums をソート - start = 0 # 走査の開始点 - res = [] # 結果リスト(部分集合リスト) + start = 0 # 開始点を走査 + res = [] # 結果リスト(部分集合のリスト) backtrack(state, target, nums, start, res) return res ``` @@ -286,36 +606,36 @@ comments: true === "C++" ```cpp title="subset_sum_i.cpp" - /* バックトラッキングアルゴリズム:部分集合和 I */ + /* バックトラッキング:部分和 I */ void backtrack(vector &state, int target, vector &choices, int start, vector> &res) { - // 部分集合の和がtargetと等しいとき、解を記録 + // 部分集合の和が target に等しければ、解を記録 if (target == 0) { res.push_back(state); return; } // すべての選択肢を走査 - // 剪定二:startから走査を開始し、重複する部分集合の生成を回避 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける for (int i = start; i < choices.size(); i++) { - // 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了 - // 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため if (target - choices[i] < 0) { break; } - // 試行:選択を行い、target、startを更新 + // 試す:選択を行い、target と start を更新 state.push_back(choices[i]); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, target - choices[i], choices, i, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す state.pop_back(); } } - /* 部分集合和 I を解く */ - vector> subsetSumI(vector nums, int target) { + /* 部分和 I を解く */ + vector> subsetSumI(vector &nums, int target) { vector state; // 状態(部分集合) sort(nums.begin(), nums.end()); // nums をソート - int start = 0; // 走査の開始点 - vector> res; // 結果リスト(部分集合リスト) + int start = 0; // 開始点を走査 + vector> res; // 結果リスト(部分集合のリスト) backtrack(state, target, nums, start, res); return res; } @@ -324,36 +644,36 @@ comments: true === "Java" ```java title="subset_sum_i.java" - /* バックトラッキングアルゴリズム:部分集合和 I */ + /* バックトラッキング:部分和 I */ void backtrack(List state, int target, int[] choices, int start, List> res) { - // 部分集合の和がtargetと等しいとき、解を記録 + // 部分集合の和が target に等しければ、解を記録 if (target == 0) { res.add(new ArrayList<>(state)); return; } // すべての選択肢を走査 - // 剪定二:startから走査を開始し、重複する部分集合の生成を回避 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける for (int i = start; i < choices.length; i++) { - // 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了 - // 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため if (target - choices[i] < 0) { break; } - // 試行:選択を行い、target、startを更新 + // 試す:選択を行い、target と start を更新 state.add(choices[i]); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, target - choices[i], choices, i, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す state.remove(state.size() - 1); } } - /* 部分集合和 I を解く */ + /* 部分和 I を解く */ List> subsetSumI(int[] nums, int target) { List state = new ArrayList<>(); // 状態(部分集合) Arrays.sort(nums); // nums をソート - int start = 0; // 走査の開始点 - List> res = new ArrayList<>(); // 結果リスト(部分集合リスト) + int start = 0; // 開始点を走査 + List> res = new ArrayList<>(); // 結果リスト(部分集合のリスト) backtrack(state, target, nums, start, res); return res; } @@ -362,108 +682,437 @@ comments: true === "C#" ```csharp title="subset_sum_i.cs" - [class]{subset_sum_i}-[func]{Backtrack} + /* バックトラッキング:部分和 I */ + void Backtrack(List state, int target, int[] choices, int start, List> res) { + // 部分集合の和が target に等しければ、解を記録 + if (target == 0) { + res.Add(new List(state)); + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + for (int i = start; i < choices.Length; i++) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if (target - choices[i] < 0) { + break; + } + // 試す:選択を行い、target と start を更新 + state.Add(choices[i]); + // 次の選択へ進む + Backtrack(state, target - choices[i], choices, i, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.RemoveAt(state.Count - 1); + } + } - [class]{subset_sum_i}-[func]{SubsetSumI} + /* 部分和 I を解く */ + List> SubsetSumI(int[] nums, int target) { + List state = []; // 状態(部分集合) + Array.Sort(nums); // nums をソート + int start = 0; // 開始点を走査 + List> res = []; // 結果リスト(部分集合のリスト) + Backtrack(state, target, nums, start, res); + return res; + } ``` === "Go" ```go title="subset_sum_i.go" - [class]{}-[func]{backtrackSubsetSumI} + /* バックトラッキング:部分和 I */ + func backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) { + // 部分集合の和が target に等しければ、解を記録 + if target == 0 { + newState := append([]int{}, *state...) + *res = append(*res, newState) + return + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + for i := start; i < len(*choices); i++ { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if target-(*choices)[i] < 0 { + break + } + // 試す:選択を行い、target と start を更新 + *state = append(*state, (*choices)[i]) + // 次の選択へ進む + backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res) + // バックトラック:選択を取り消し、前の状態に戻す + *state = (*state)[:len(*state)-1] + } + } - [class]{}-[func]{subsetSumI} + /* 部分和 I を解く */ + func subsetSumI(nums []int, target int) [][]int { + state := make([]int, 0) // 状態(部分集合) + sort.Ints(nums) // nums をソート + start := 0 // 開始点を走査 + res := make([][]int, 0) // 結果リスト(部分集合のリスト) + backtrackSubsetSumI(start, target, &state, &nums, &res) + return res + } ``` === "Swift" ```swift title="subset_sum_i.swift" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + func backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) { + // 部分集合の和が target に等しければ、解を記録 + if target == 0 { + res.append(state) + return + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + for i in choices.indices.dropFirst(start) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if target - choices[i] < 0 { + break + } + // 試す:選択を行い、target と start を更新 + state.append(choices[i]) + // 次の選択へ進む + backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res) + // バックトラック:選択を取り消し、前の状態に戻す + state.removeLast() + } + } - [class]{}-[func]{subsetSumI} + /* 部分和 I を解く */ + func subsetSumI(nums: [Int], target: Int) -> [[Int]] { + var state: [Int] = [] // 状態(部分集合) + let nums = nums.sorted() // nums をソート + let start = 0 // 開始点を走査 + var res: [[Int]] = [] // 結果リスト(部分集合のリスト) + backtrack(state: &state, target: target, choices: nums, start: start, res: &res) + return res + } ``` === "JS" ```javascript title="subset_sum_i.js" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + function backtrack(state, target, choices, start, res) { + // 部分集合の和が target に等しければ、解を記録 + if (target === 0) { + res.push([...state]); + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + for (let i = start; i < choices.length; i++) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if (target - choices[i] < 0) { + break; + } + // 試す:選択を行い、target と start を更新 + state.push(choices[i]); + // 次の選択へ進む + backtrack(state, target - choices[i], choices, i, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.pop(); + } + } - [class]{}-[func]{subsetSumI} + /* 部分和 I を解く */ + function subsetSumI(nums, target) { + const state = []; // 状態(部分集合) + nums.sort((a, b) => a - b); // nums をソート + const start = 0; // 開始点を走査 + const res = []; // 結果リスト(部分集合のリスト) + backtrack(state, target, nums, start, res); + return res; + } ``` === "TS" ```typescript title="subset_sum_i.ts" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + function backtrack( + state: number[], + target: number, + choices: number[], + start: number, + res: number[][] + ): void { + // 部分集合の和が target に等しければ、解を記録 + if (target === 0) { + res.push([...state]); + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + for (let i = start; i < choices.length; i++) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if (target - choices[i] < 0) { + break; + } + // 試す:選択を行い、target と start を更新 + state.push(choices[i]); + // 次の選択へ進む + backtrack(state, target - choices[i], choices, i, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.pop(); + } + } - [class]{}-[func]{subsetSumI} + /* 部分和 I を解く */ + function subsetSumI(nums: number[], target: number): number[][] { + const state = []; // 状態(部分集合) + nums.sort((a, b) => a - b); // nums をソート + const start = 0; // 開始点を走査 + const res = []; // 結果リスト(部分集合のリスト) + backtrack(state, target, nums, start, res); + return res; + } ``` === "Dart" ```dart title="subset_sum_i.dart" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + void backtrack( + List state, + int target, + List choices, + int start, + List> res, + ) { + // 部分集合の和が target に等しければ、解を記録 + if (target == 0) { + res.add(List.from(state)); + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + for (int i = start; i < choices.length; i++) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if (target - choices[i] < 0) { + break; + } + // 試す:選択を行い、target と start を更新 + state.add(choices[i]); + // 次の選択へ進む + backtrack(state, target - choices[i], choices, i, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.removeLast(); + } + } - [class]{}-[func]{subsetSumI} + /* 部分和 I を解く */ + List> subsetSumI(List nums, int target) { + List state = []; // 状態(部分集合) + nums.sort(); // nums をソート + int start = 0; // 開始点を走査 + List> res = []; // 結果リスト(部分集合のリスト) + backtrack(state, target, nums, start, res); + return res; + } ``` === "Rust" ```rust title="subset_sum_i.rs" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + fn backtrack( + state: &mut Vec, + target: i32, + choices: &[i32], + start: usize, + res: &mut Vec>, + ) { + // 部分集合の和が target に等しければ、解を記録 + if target == 0 { + res.push(state.clone()); + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + for i in start..choices.len() { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if target - choices[i] < 0 { + break; + } + // 試す:選択を行い、target と start を更新 + state.push(choices[i]); + // 次の選択へ進む + backtrack(state, target - choices[i], choices, i, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.pop(); + } + } - [class]{}-[func]{subset_sum_i} + /* 部分和 I を解く */ + fn subset_sum_i(nums: &mut [i32], target: i32) -> Vec> { + let mut state = Vec::new(); // 状態(部分集合) + nums.sort(); // nums をソート + let start = 0; // 開始点を走査 + let mut res = Vec::new(); // 結果リスト(部分集合のリスト) + backtrack(&mut state, target, nums, start, &mut res); + res + } ``` === "C" ```c title="subset_sum_i.c" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + void backtrack(int target, int *choices, int choicesSize, int start) { + // 部分集合の和が target に等しければ、解を記録 + if (target == 0) { + for (int i = 0; i < stateSize; ++i) { + res[resSize][i] = state[i]; + } + resColSizes[resSize++] = stateSize; + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + for (int i = start; i < choicesSize; i++) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if (target - choices[i] < 0) { + break; + } + // 試す:選択を行い、target と start を更新 + state[stateSize] = choices[i]; + stateSize++; + // 次の選択へ進む + backtrack(target - choices[i], choices, choicesSize, i); + // バックトラック:選択を取り消し、前の状態に戻す + stateSize--; + } + } - [class]{}-[func]{subsetSumI} + /* 部分和 I を解く */ + void subsetSumI(int *nums, int numsSize, int target) { + qsort(nums, numsSize, sizeof(int), cmp); // nums をソート + int start = 0; // 開始点を走査 + backtrack(target, nums, numsSize, start); + } ``` === "Kotlin" ```kotlin title="subset_sum_i.kt" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 I */ + fun backtrack( + state: MutableList, + target: Int, + choices: IntArray, + start: Int, + res: MutableList?> + ) { + // 部分集合の和が target に等しければ、解を記録 + if (target == 0) { + res.add(state.toMutableList()) + return + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + for (i in start..?> { + val state = mutableListOf() // 状態(部分集合) + nums.sort() // nums をソート + val start = 0 // 開始点を走査 + val res = mutableListOf?>() // 結果リスト(部分集合のリスト) + backtrack(state, target, nums, start, res) + return res + } ``` === "Ruby" ```ruby title="subset_sum_i.rb" - [class]{}-[func]{backtrack} + ### バックトラッキング: 部分和 I ### + def backtrack(state, target, choices, start, res) + # 部分集合の和が target に等しければ、解を記録 + if target.zero? + res << state.dup + return + end + # すべての選択肢を走査 + # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + for i in start...choices.length + # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + break if target - choices[i] < 0 + # 試す:選択を行い、target と start を更新 + state << choices[i] + # 次の選択へ進む + backtrack(state, target - choices[i], choices, i, res) + # バックトラック:選択を取り消し、前の状態に戻す + state.pop + end + end - [class]{}-[func]{subset_sum_i} + ### 部分和 I を解く ### + def subset_sum_i(nums, target) + state = [] # 状態(部分集合) + nums.sort! # nums をソート + start = 0 # 開始点を走査 + res = [] # 結果リスト(部分集合のリスト) + backtrack(state, target, nums, start, res) + res + end ``` -以下の図は、配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力した後の全体的なバックトラッキングプロセスを示しています。 +??? pythontutor "コードの可視化" -![部分集合和 I のバックトラッキングプロセス](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" } +
+ -

図 13-12   部分集合和 I のバックトラッキングプロセス

+次の図は、配列 $[3, 4, 5]$ と目標値 $9$ を上のコードに入力したときの、全体のバックトラッキング過程を示しています。 -## 13.3.2   重複要素がある場合を考慮 +![部分和 I のバックトラッキング過程](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" } + +

図 13-12   部分和 I のバックトラッキング過程

+ +## 13.3.2   重複要素を考慮する場合 !!! question - 正の整数の配列 `nums` とターゲット正整数 `target` が与えられた場合、組み合わせ内の要素の和が `target` に等しくなるようなすべての可能な組み合わせを見つけてください。**与えられた配列には重複要素が含まれる可能性があり、各要素は一度だけ選択できます**。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。 + 正整数配列 `nums` と目標の正整数 `target` が与えられたとき、要素の和が `target` に等しくなるすべての組合せを見つけてください。**与えられた配列には重複要素が含まれる可能性があり、各要素は 1 回しか選択できません**。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。 -前の問題と比較して、**この問題の入力配列には重複要素が含まれる可能性があり**、新しい問題が導入されます。例えば、配列 $[4, \hat{4}, 5]$ とターゲット要素 $9$ が与えられた場合、既存のコードの出力結果は $[4, 5], [\hat{4}, 5]$ となり、重複する部分集合が生成されます。 +前問と比べると、**この問題の入力配列には重複要素が含まれる可能性があります**。そのため、新たな問題が生じます。例えば、配列 $[4, \hat{4}, 5]$ と目標値 $9$ が与えられると、既存コードの出力は $[4, 5], [\hat{4}, 5]$ となり、重複部分集合が現れます。 -**この重複の理由は、特定のラウンドで等しい要素が複数回選択されることです**。以下の図では、最初のラウンドに3つの選択肢があり、そのうち2つが $4$ であり、2つの重複する検索分岐を生成し、重複する部分集合を出力します。同様に、2番目のラウンドの2つの $4$ も重複する部分集合を生成します。 +**この重複が生じる原因は、同じ値の要素があるラウンドで複数回選ばれてしまうことにあります**。次の図では、1 回目には 3 つの選択肢があり、そのうち 2 つはどちらも $4$ です。これにより 2 本の重複した探索分岐が生じ、重複部分集合が出力されます。同様に、2 回目の 2 つの $4$ も重複部分集合を生みます。 -![等しい要素による重複部分集合](subset_sum_problem.assets/subset_sum_ii_repeat.png){ class="animation-figure" } +![等しい要素によって生じる重複部分集合](subset_sum_problem.assets/subset_sum_ii_repeat.png){ class="animation-figure" } -

図 13-13   等しい要素による重複部分集合

+

図 13-13   等しい要素によって生じる重複部分集合

-### 1.   等値要素の剪定 +### 1.   等しい要素の枝刈り -この問題を解決するために、**等しい要素がラウンドごとに一度だけ選択されるように制限する必要があります**。実装は非常に巧妙です:配列がソートされているため、等しい要素は隣接しています。これは、特定のラウンドの選択で、現在の要素がその左側の要素と等しい場合、それはすでに選択されていることを意味するため、現在の要素を直接スキップします。 +この問題を解決するには、**各ラウンドで等しい要素が 1 回しか選ばれないように制限する必要があります**。実装方法は巧妙です。配列はすでにソートされているため、等しい要素は必ず隣り合っています。したがって、あるラウンドの選択で現在の要素が左隣の要素と等しいなら、それはすでに選ばれたことを意味するので、その要素を直接スキップします。 -同時に、**この問題では各配列要素は一度だけ選択できると規定されています**。幸い、変数 `start` を使用してこの制約も満たすことができます:選択 $x_{i}$ を行った後、次のラウンドをインデックス $i + 1$ から前方に開始するように設定します。これにより、重複する部分集合が除去されるだけでなく、要素の重複選択も回避されます。 +同時に、**この問題では各配列要素を 1 回しか選択できない**という制約もあります。幸い、この制約も変数 `start` を使って満たせます。すなわち、選択 $x_{i}$ を行った後、次のラウンドはインデックス $i + 1$ から後ろへ走査するよう設定します。これにより、重複部分集合を除去できるだけでなく、同じ要素を繰り返し選ぶことも防げます。 ### 2.   コード実装 @@ -473,35 +1122,35 @@ comments: true def backtrack( state: list[int], target: int, choices: list[int], start: int, res: list[list[int]] ): - """バックトラッキングアルゴリズム:部分集合の和 II""" - # 部分集合の和が target と等しいとき、解を記録 + """バックトラッキング:部分和 II""" + # 部分集合の和が target に等しければ、解を記録 if target == 0: res.append(list(state)) return # すべての選択肢を走査 - # 枝刈り二:start から走査を開始して重複する部分集合の生成を避ける - # 枝刈り三:start から走査を開始して同じ要素の重複選択を避ける + # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + # 枝刈り 3: start から走査し、同じ要素の重複選択を避ける for i in range(start, len(choices)): - # 枝刈り一:部分集合の和が target を超える場合、直ちにループを終了 - # これは配列がソートされており、後の要素がより大きいため、部分集合の和は必ず target を超えるため + # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため if target - choices[i] < 0: break - # 枝刈り四:要素が左の要素と等しい場合、検索分岐が重複していることを示すため、スキップ + # 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする if i > start and choices[i] == choices[i - 1]: continue - # 試行:選択を行い、target、start を更新 + # 試す:選択を行い、target と start を更新 state.append(choices[i]) - # 次の選択ラウンドに進む + # 次の選択へ進む backtrack(state, target - choices[i], choices, i + 1, res) - # 撤回:選択を取り消し、前の状態に復元 + # バックトラック:選択を取り消し、前の状態に戻す state.pop() def subset_sum_ii(nums: list[int], target: int) -> list[list[int]]: - """部分集合の和 II を解く""" + """部分和 II を解く""" state = [] # 状態(部分集合) nums.sort() # nums をソート - start = 0 # 走査の開始点 - res = [] # 結果リスト(部分集合リスト) + start = 0 # 開始点を走査 + res = [] # 結果リスト(部分集合のリスト) backtrack(state, target, nums, start, res) return res ``` @@ -509,41 +1158,41 @@ comments: true === "C++" ```cpp title="subset_sum_ii.cpp" - /* バックトラッキングアルゴリズム:部分集合和 II */ + /* バックトラッキング:部分和 II */ void backtrack(vector &state, int target, vector &choices, int start, vector> &res) { - // 部分集合の和がtargetと等しいとき、解を記録 + // 部分集合の和が target に等しければ、解を記録 if (target == 0) { res.push_back(state); return; } // すべての選択肢を走査 - // 剪定二:startから走査を開始し、重複する部分集合の生成を回避 - // 剪定三:startから走査を開始し、同じ要素の繰り返し選択を回避 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける for (int i = start; i < choices.size(); i++) { - // 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了 - // 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため if (target - choices[i] < 0) { break; } - // 剪定四:要素が左の要素と等しい場合、検索ブランチの重複を示すのでスキップ + // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする if (i > start && choices[i] == choices[i - 1]) { continue; } - // 試行:選択を行い、target、startを更新 + // 試す:選択を行い、target と start を更新 state.push_back(choices[i]); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, target - choices[i], choices, i + 1, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す state.pop_back(); } } - /* 部分集合和 II を解く */ - vector> subsetSumII(vector nums, int target) { + /* 部分和 II を解く */ + vector> subsetSumII(vector &nums, int target) { vector state; // 状態(部分集合) sort(nums.begin(), nums.end()); // nums をソート - int start = 0; // 走査の開始点 - vector> res; // 結果リスト(部分集合リスト) + int start = 0; // 開始点を走査 + vector> res; // 結果リスト(部分集合のリスト) backtrack(state, target, nums, start, res); return res; } @@ -552,41 +1201,41 @@ comments: true === "Java" ```java title="subset_sum_ii.java" - /* バックトラッキングアルゴリズム:部分集合和 II */ + /* バックトラッキング:部分和 II */ void backtrack(List state, int target, int[] choices, int start, List> res) { - // 部分集合の和がtargetと等しいとき、解を記録 + // 部分集合の和が target に等しければ、解を記録 if (target == 0) { res.add(new ArrayList<>(state)); return; } // すべての選択肢を走査 - // 剪定二:startから走査を開始し、重複する部分集合の生成を回避 - // 剪定三:startから走査を開始し、同じ要素の繰り返し選択を回避 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける for (int i = start; i < choices.length; i++) { - // 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了 - // 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため if (target - choices[i] < 0) { break; } - // 剪定四:要素が左の要素と等しい場合、検索ブランチの重複を示すのでスキップ + // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする if (i > start && choices[i] == choices[i - 1]) { continue; } - // 試行:選択を行い、target、startを更新 + // 試す:選択を行い、target と start を更新 state.add(choices[i]); - // 次のラウンドの選択に進む + // 次の選択へ進む backtrack(state, target - choices[i], choices, i + 1, res); - // 回退:選択を取り消し、前の状態に復元 + // バックトラック:選択を取り消し、前の状態に戻す state.remove(state.size() - 1); } } - /* 部分集合和 II を解く */ + /* 部分和 II を解く */ List> subsetSumII(int[] nums, int target) { List state = new ArrayList<>(); // 状態(部分集合) Arrays.sort(nums); // nums をソート - int start = 0; // 走査の開始点 - List> res = new ArrayList<>(); // 結果リスト(部分集合リスト) + int start = 0; // 開始点を走査 + List> res = new ArrayList<>(); // 結果リスト(部分集合のリスト) backtrack(state, target, nums, start, res); return res; } @@ -595,85 +1244,463 @@ comments: true === "C#" ```csharp title="subset_sum_ii.cs" - [class]{subset_sum_ii}-[func]{Backtrack} + /* バックトラッキング:部分和 II */ + void Backtrack(List state, int target, int[] choices, int start, List> res) { + // 部分集合の和が target に等しければ、解を記録 + if (target == 0) { + res.Add(new List(state)); + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける + for (int i = start; i < choices.Length; i++) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if (target - choices[i] < 0) { + break; + } + // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // 試す:選択を行い、target と start を更新 + state.Add(choices[i]); + // 次の選択へ進む + Backtrack(state, target - choices[i], choices, i + 1, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.RemoveAt(state.Count - 1); + } + } - [class]{subset_sum_ii}-[func]{SubsetSumII} + /* 部分和 II を解く */ + List> SubsetSumII(int[] nums, int target) { + List state = []; // 状態(部分集合) + Array.Sort(nums); // nums をソート + int start = 0; // 開始点を走査 + List> res = []; // 結果リスト(部分集合のリスト) + Backtrack(state, target, nums, start, res); + return res; + } ``` === "Go" ```go title="subset_sum_ii.go" - [class]{}-[func]{backtrackSubsetSumII} + /* バックトラッキング:部分和 II */ + func backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) { + // 部分集合の和が target に等しければ、解を記録 + if target == 0 { + newState := append([]int{}, *state...) + *res = append(*res, newState) + return + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける + for i := start; i < len(*choices); i++ { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if target-(*choices)[i] < 0 { + break + } + // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする + if i > start && (*choices)[i] == (*choices)[i-1] { + continue + } + // 試す:選択を行い、target と start を更新 + *state = append(*state, (*choices)[i]) + // 次の選択へ進む + backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res) + // バックトラック:選択を取り消し、前の状態に戻す + *state = (*state)[:len(*state)-1] + } + } - [class]{}-[func]{subsetSumII} + /* 部分和 II を解く */ + func subsetSumII(nums []int, target int) [][]int { + state := make([]int, 0) // 状態(部分集合) + sort.Ints(nums) // nums をソート + start := 0 // 開始点を走査 + res := make([][]int, 0) // 結果リスト(部分集合のリスト) + backtrackSubsetSumII(start, target, &state, &nums, &res) + return res + } ``` === "Swift" ```swift title="subset_sum_ii.swift" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 II */ + func backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) { + // 部分集合の和が target に等しければ、解を記録 + if target == 0 { + res.append(state) + return + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける + for i in choices.indices.dropFirst(start) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if target - choices[i] < 0 { + break + } + // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする + if i > start, choices[i] == choices[i - 1] { + continue + } + // 試す:選択を行い、target と start を更新 + state.append(choices[i]) + // 次の選択へ進む + backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res) + // バックトラック:選択を取り消し、前の状態に戻す + state.removeLast() + } + } - [class]{}-[func]{subsetSumII} + /* 部分和 II を解く */ + func subsetSumII(nums: [Int], target: Int) -> [[Int]] { + var state: [Int] = [] // 状態(部分集合) + let nums = nums.sorted() // nums をソート + let start = 0 // 開始点を走査 + var res: [[Int]] = [] // 結果リスト(部分集合のリスト) + backtrack(state: &state, target: target, choices: nums, start: start, res: &res) + return res + } ``` === "JS" ```javascript title="subset_sum_ii.js" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 II */ + function backtrack(state, target, choices, start, res) { + // 部分集合の和が target に等しければ、解を記録 + if (target === 0) { + res.push([...state]); + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける + for (let i = start; i < choices.length; i++) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if (target - choices[i] < 0) { + break; + } + // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする + if (i > start && choices[i] === choices[i - 1]) { + continue; + } + // 試す:選択を行い、target と start を更新 + state.push(choices[i]); + // 次の選択へ進む + backtrack(state, target - choices[i], choices, i + 1, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.pop(); + } + } - [class]{}-[func]{subsetSumII} + /* 部分和 II を解く */ + function subsetSumII(nums, target) { + const state = []; // 状態(部分集合) + nums.sort((a, b) => a - b); // nums をソート + const start = 0; // 開始点を走査 + const res = []; // 結果リスト(部分集合のリスト) + backtrack(state, target, nums, start, res); + return res; + } ``` === "TS" ```typescript title="subset_sum_ii.ts" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 II */ + function backtrack( + state: number[], + target: number, + choices: number[], + start: number, + res: number[][] + ): void { + // 部分集合の和が target に等しければ、解を記録 + if (target === 0) { + res.push([...state]); + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける + for (let i = start; i < choices.length; i++) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if (target - choices[i] < 0) { + break; + } + // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする + if (i > start && choices[i] === choices[i - 1]) { + continue; + } + // 試す:選択を行い、target と start を更新 + state.push(choices[i]); + // 次の選択へ進む + backtrack(state, target - choices[i], choices, i + 1, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.pop(); + } + } - [class]{}-[func]{subsetSumII} + /* 部分和 II を解く */ + function subsetSumII(nums: number[], target: number): number[][] { + const state = []; // 状態(部分集合) + nums.sort((a, b) => a - b); // nums をソート + const start = 0; // 開始点を走査 + const res = []; // 結果リスト(部分集合のリスト) + backtrack(state, target, nums, start, res); + return res; + } ``` === "Dart" ```dart title="subset_sum_ii.dart" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 II */ + void backtrack( + List state, + int target, + List choices, + int start, + List> res, + ) { + // 部分集合の和が target に等しければ、解を記録 + if (target == 0) { + res.add(List.from(state)); + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける + for (int i = start; i < choices.length; i++) { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if (target - choices[i] < 0) { + break; + } + // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // 試す:選択を行い、target と start を更新 + state.add(choices[i]); + // 次の選択へ進む + backtrack(state, target - choices[i], choices, i + 1, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.removeLast(); + } + } - [class]{}-[func]{subsetSumII} + /* 部分和 II を解く */ + List> subsetSumII(List nums, int target) { + List state = []; // 状態(部分集合) + nums.sort(); // nums をソート + int start = 0; // 開始点を走査 + List> res = []; // 結果リスト(部分集合のリスト) + backtrack(state, target, nums, start, res); + return res; + } ``` === "Rust" ```rust title="subset_sum_ii.rs" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 II */ + fn backtrack( + state: &mut Vec, + target: i32, + choices: &[i32], + start: usize, + res: &mut Vec>, + ) { + // 部分集合の和が target に等しければ、解を記録 + if target == 0 { + res.push(state.clone()); + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける + for i in start..choices.len() { + // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + if target - choices[i] < 0 { + break; + } + // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする + if i > start && choices[i] == choices[i - 1] { + continue; + } + // 試す:選択を行い、target と start を更新 + state.push(choices[i]); + // 次の選択へ進む + backtrack(state, target - choices[i], choices, i + 1, res); + // バックトラック:選択を取り消し、前の状態に戻す + state.pop(); + } + } - [class]{}-[func]{subset_sum_ii} + /* 部分和 II を解く */ + fn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec> { + let mut state = Vec::new(); // 状態(部分集合) + nums.sort(); // nums をソート + let start = 0; // 開始点を走査 + let mut res = Vec::new(); // 結果リスト(部分集合のリスト) + backtrack(&mut state, target, nums, start, &mut res); + res + } ``` === "C" ```c title="subset_sum_ii.c" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 II */ + void backtrack(int target, int *choices, int choicesSize, int start) { + // 部分集合の和が target に等しければ、解を記録 + if (target == 0) { + for (int i = 0; i < stateSize; i++) { + res[resSize][i] = state[i]; + } + resColSizes[resSize++] = stateSize; + return; + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける + for (int i = start; i < choicesSize; i++) { + // 枝刈り 1: 部分集合の和が target を超えたら、そのままスキップする + if (target - choices[i] < 0) { + continue; + } + // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // 試す:選択を行い、target と start を更新 + state[stateSize] = choices[i]; + stateSize++; + // 次の選択へ進む + backtrack(target - choices[i], choices, choicesSize, i + 1); + // バックトラック:選択を取り消し、前の状態に戻す + stateSize--; + } + } - [class]{}-[func]{subsetSumII} + /* 部分和 II を解く */ + void subsetSumII(int *nums, int numsSize, int target) { + // nums をソート + qsort(nums, numsSize, sizeof(int), cmp); + // バックトラッキングを開始 + backtrack(target, nums, numsSize, 0); + } ``` === "Kotlin" ```kotlin title="subset_sum_ii.kt" - [class]{}-[func]{backtrack} + /* バックトラッキング:部分和 II */ + fun backtrack( + state: MutableList, + target: Int, + choices: IntArray, + start: Int, + res: MutableList?> + ) { + // 部分集合の和が target に等しければ、解を記録 + if (target == 0) { + res.add(state.toMutableList()) + return + } + // すべての選択肢を走査 + // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける + for (i in start.. start && choices[i] == choices[i - 1]) { + continue + } + // 試す:選択を行い、target と start を更新 + state.add(choices[i]) + // 次の選択へ進む + backtrack(state, target - choices[i], choices, i + 1, res) + // バックトラック:選択を取り消し、前の状態に戻す + state.removeAt(state.size - 1) + } + } - [class]{}-[func]{subsetSumII} + /* 部分和 II を解く */ + fun subsetSumII(nums: IntArray, target: Int): MutableList?> { + val state = mutableListOf() // 状態(部分集合) + nums.sort() // nums をソート + val start = 0 // 開始点を走査 + val res = mutableListOf?>() // 結果リスト(部分集合のリスト) + backtrack(state, target, nums, start, res) + return res + } ``` === "Ruby" ```ruby title="subset_sum_ii.rb" - [class]{}-[func]{backtrack} + ### バックトラッキング法:部分和 II ### + def backtrack(state, target, choices, start, res) + # 部分集合の和が target に等しければ、解を記録 + if target.zero? + res << state.dup + return + end - [class]{}-[func]{subset_sum_ii} + # すべての選択肢を走査 + # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける + # 枝刈り 3: start から走査し、同じ要素の重複選択を避ける + for i in start...choices.length + # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する + # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため + break if target - choices[i] < 0 + # 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする + next if i > start && choices[i] == choices[i - 1] + # 試す:選択を行い、target と start を更新 + state << choices[i] + # 次の選択へ進む + backtrack(state, target - choices[i], choices, i + 1, res) + # バックトラック:選択を取り消し、前の状態に戻す + state.pop + end + end + + ### 部分和 II を解く ### + def subset_sum_ii(nums, target) + state = [] # 状態(部分集合) + nums.sort! # nums をソート + start = 0 # 開始点を走査 + res = [] # 結果リスト(部分集合のリスト) + backtrack(state, target, nums, start, res) + res + end ``` -以下の図は、配列 $[4, 4, 5]$ とターゲット要素 $9$ のバックトラッキングプロセスを示し、4種類の剪定操作が含まれています。図とコードのコメントを組み合わせて、検索プロセス全体と各種類の剪定操作の動作を理解してください。 +??? pythontutor "コードの可視化" -![部分集合和 II のバックトラッキングプロセス](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" } +
+ -

図 13-14   部分集合和 II のバックトラッキングプロセス

+次の図は、配列 $[4, 4, 5]$ と目標値 $9$ に対するバックトラッキング過程を示しており、全部で 4 種類の枝刈り操作が含まれています。図とコードコメントを対応させながら、探索全体の流れと、各枝刈り操作がどのように機能するかを理解してください。 + +![部分和 II のバックトラッキング過程](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" } + +

図 13-14   部分和 II のバックトラッキング過程

diff --git a/ja/docs/chapter_backtracking/summary.md b/ja/docs/chapter_backtracking/summary.md index 12f76b317..01b224791 100644 --- a/ja/docs/chapter_backtracking/summary.md +++ b/ja/docs/chapter_backtracking/summary.md @@ -4,24 +4,24 @@ comments: true # 13.5   まとめ -### 1.   重要な復習 +### 1.   重要なポイントの振り返り -- バックトラッキングアルゴリズムの本質は全数探索です。解空間の深さ優先走査を実行することで条件を満たす解を求めます。検索中に満足のいく解が見つかった場合、それを記録し、すべての解が見つかるか走査が完了するまで続けます。 -- バックトラッキングアルゴリズムの検索プロセスには試行と後退が含まれます。深さ優先探索を使用して様々な選択を探索し、選択が制約を満たさない場合、前の選択を取り消します。そして前の状態に戻って他のオプションを試し続けます。試行と後退は反対方向の操作です。 -- バックトラッキング問題には通常複数の制約が含まれます。これらの制約は剪定操作を実行するために使用できます。剪定は不要な検索分岐を事前に終了し、検索効率を大幅に向上させることができます。 -- バックトラッキングアルゴリズムは主に検索問題と制約満足問題を解決するために使用されます。組み合わせ最適化問題はバックトラッキングを使用して解決できますが、多くの場合、より効率的または効果的な解決方法が利用可能です。 -- 順列問題は、与えられた集合の要素のすべての可能な順列を検索することを目的とします。各要素が選択されたかどうかを記録するために配列を使用し、同じ要素の重複選択を避けます。これにより、各要素が一度だけ選択されることが保証されます。 -- 順列問題では、集合に重複要素が含まれている場合、最終結果に重複順列が含まれます。同一要素が各ラウンドで一度だけ選択できるように制限する必要があり、これは通常ハッシュセットを使用して実装されます。 -- 部分集合和問題は、与えられた集合でターゲット値に合計する全ての部分集合を見つけることを目的とします。集合は要素の順序を区別しませんが、検索プロセスでは重複する部分集合が生成される可能性があります。これは、アルゴリズムが異なる要素順序を独特のパスとして探索するために発生します。バックトラッキングの前に、データをソートし、各ラウンドの走査の開始点を示す変数を設定します。これにより、重複する部分集合を生成する検索分岐を剪定できます。 -- 部分集合和問題では、配列内の等しい要素は重複集合を生成する可能性があります。配列がすでにソートされているという前提条件を使用して、隣接する要素が等しいかどうかを判定することで剪定を行います。これにより、等しい要素がラウンドごとに一度だけ選択されることが保証されます。 -- $n$ クイーン問題は、2つのクイーンが互いに攻撃できないように $n \times n$ のチェスボードに $n$ 個のクイーンを配置する方案を見つけることを目的とします。問題の制約には行制約、列制約、および主対角線と副対角線の制約が含まれます。行制約を満たすために、行ごとに1つのクイーンを配置する戦略を採用し、各行に1つのクイーンが配置されることを保証します。 -- 列制約と対角線制約の処理は似ています。列制約については、各列にクイーンがあるかどうかを記録する配列を使用し、選択されたセルが合法かどうかを示します。対角線制約については、2つの配列を使用して主対角線と副対角線にそれぞれクイーンの存在を記録します。課題は、同じ主対角線または副対角線上のセルの行と列のインデックス間の関係を決定することです。 +- バックトラッキングアルゴリズムの本質は全探索法であり、解空間を深さ優先で走査することで条件を満たす解を探索します。探索の過程で条件を満たす解に出会ったら記録し、すべての解を見つけるか探索が完了するまで続けます。 +- バックトラッキングアルゴリズムの探索過程は、試行と戻るという 2 つの部分から成ります。深さ優先探索によってさまざまな選択を試し、制約条件を満たさない状況に遭遇した場合は直前の選択を取り消して前の状態に戻り、ほかの選択を引き続き試します。試行と戻るは互いに逆方向の操作です。 +- バックトラッキング問題には通常複数の制約条件が含まれており、それらを枝刈りに利用できます。枝刈りによって不要な探索分岐を早期に打ち切り、探索効率を大幅に高められます。 +- バックトラッキングアルゴリズムは主に探索問題と制約充足問題の解決に用いられます。組合せ最適化問題もバックトラッキングで解けますが、より高効率またはより適した解法が存在することが少なくありません。 +- 全順列問題の目的は、与えられた集合要素のすべての可能な並べ方を探索することです。各要素が選択済みかどうかを配列で記録し、同じ要素を重複して選ぶ探索分岐を刈り取ることで、各要素が 1 度だけ選ばれるようにします。 +- 全順列問題では、集合内に重複要素があると最終結果にも重複した順列が現れます。各ラウンドで等しい要素は 1 回しか選べないように制約する必要があり、通常はハッシュ集合を用いて実現します。 +- 部分和問題の目標は、与えられた集合の中から和が目標値となるすべての部分集合を見つけることです。集合では要素順序を区別しませんが、探索過程では順序違いの結果も出力されるため、重複部分集合が生じます。そこで、バックトラッキング前にデータをソートし、各ラウンドの走査開始位置を示す変数を設定することで、重複部分集合を生成する探索分岐を枝刈りします。 +- 部分和問題では、配列中の等しい要素が重複集合を生みます。配列がソート済みであるという前提を利用し、隣接要素が等しいかどうかを判定して枝刈りすることで、等しい要素が各ラウンドで 1 回しか選ばれないようにします。 +- $n$ クイーン問題の目的は、$n \times n$ の盤面に $n$ 個のクイーンを配置する方法を見つけることであり、どの 2 つのクイーンも互いに攻撃できないことが条件です。この問題の制約には行制約、列制約、主対角線制約、副対角線制約があります。行制約を満たすため、行ごとに配置する戦略を採用し、各行に 1 個のクイーンを置くことを保証します。 +- 列制約と対角線制約の扱い方は似ています。列制約については、各列にクイーンが存在するかどうかを配列で記録し、選択したマスが有効かどうかを判定します。対角線制約については、主対角線と副対角線それぞれにクイーンが存在するかを 2 つの配列で記録します。難点は、同じ主対角線または副対角線上にあるマスが満たす行列インデックスの規則を見つけることにあります。 ### 2.   Q & A -**Q**: バックトラッキングと再帰の関係をどのように理解すればよいですか? +**Q**:バックトラッキングと再帰の関係はどのように理解すればよいですか? -全体的に、バックトラッキングは「アルゴリズム戦略」であり、再帰はより「ツール」です。 +全体として見ると、バックトラッキングは「アルゴリズム戦略」の一種であり、再帰はむしろ「道具」に近いものです。 -- バックトラッキングアルゴリズムは通常再帰に基づいています。しかし、バックトラッキングは再帰の応用シナリオの一つであり、特に検索問題においてです。 -- 再帰の構造は「部分問題分解」の問題解決パラダイムを反映します。分割統治、バックトラッキング、動的プログラミング(メモ化再帰)を含む問題の解決でよく使用されます。 +- バックトラッキングアルゴリズムは通常、再帰に基づいて実装されます。ただし、バックトラッキングは再帰の応用場面の 1 つであり、探索問題における再帰の応用です。 +- 再帰の構造は「部分問題への分解」という問題解決パラダイムを表しており、分割統治、バックトラッキング、動的計画法(メモ化再帰)などの問題によく用いられます。 diff --git a/ja/docs/chapter_computational_complexity/index.md b/ja/docs/chapter_computational_complexity/index.md index ce25fccaf..0a0be1948 100644 --- a/ja/docs/chapter_computational_complexity/index.md +++ b/ja/docs/chapter_computational_complexity/index.md @@ -3,19 +3,19 @@ comments: true icon: material/timer-sand --- -# 第 2 章   複雑度解析 +# 第 2 章   計算量解析 -![Complexity analysis](../assets/covers/chapter_complexity_analysis.jpg){ class="cover-image" } +![計算量解析](../assets/covers/chapter_complexity_analysis.jpg){ class="cover-image" } !!! abstract - 複雑度解析は、アルゴリズムの広大な宇宙における時空のナビゲーターのようなものです。 - - 時間と空間の次元をより深く探求し、より優雅な解決策を求めるためのガイドとなります。 + 計算量解析は、広大なアルゴリズム宇宙における時空の案内人のようなものです。 + + それは、時間と空間という二つの次元で私たちをより深く探求へ導き、より洗練された解決策を見つけ出します。 ## 章の内容 -- [2.1   アルゴリズムの効率評価](performance_evaluation.md) +- [2.1   アルゴリズム効率の評価](performance_evaluation.md) - [2.2   反復と再帰](iteration_and_recursion.md) - [2.3   時間計算量](time_complexity.md) - [2.4   空間計算量](space_complexity.md) diff --git a/ja/docs/chapter_computational_complexity/iteration_and_recursion.md b/ja/docs/chapter_computational_complexity/iteration_and_recursion.md index 2792a36d4..4296b14a6 100644 --- a/ja/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/ja/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -4,25 +4,25 @@ comments: true # 2.2   反復と再帰 -アルゴリズムにおいて、タスクの繰り返し実行は非常に一般的であり、複雑度の分析と密接に関係しています。したがって、時間計算量と空間計算量の概念を詳しく学ぶ前に、まずプログラミングで繰り返しタスクを実装する方法を探究しましょう。これには、2つの基本的なプログラミング制御構造である反復と再帰の理解が含まれます。 +アルゴリズムでは、ある処理を繰り返し実行することがよくあり、これは複雑度解析と密接に関係しています。そのため、時間計算量と空間計算量を紹介する前に、まずプログラム内で反復実行を実現する方法、つまり 2 つの基本的な制御構造である反復と再帰について見ていきます。 ## 2.2.1   反復 -反復は、タスクを繰り返し実行するための制御構造です。反復では、プログラムは特定の条件が満たされている限りコードブロックを繰り返し実行し、この条件が満たされなくなるまで続けます。 +反復(iteration)は、ある処理を繰り返し実行するための制御構造です。反復では、プログラムは一定の条件を満たす間、あるコード片を繰り返し実行し、その条件を満たさなくなるまで続けます。 -### 1.   forループ +### 1.   for ループ -`for`ループは反復の最も一般的な形式の1つであり、**反復回数が事前に分かっている場合に特に適しています**。 +`for` ループは最も一般的な反復形式の 1 つで、**反復回数があらかじめ分かっている場合に適しています**。 -以下の関数は`for`ループを使用して$1 + 2 + \dots + n$の合計を実行し、合計を変数`res`に格納します。Pythonでは、`range(a, b)`は`a`を含み`b`を除く区間を作成することに注意してください。つまり、$a$から$b−1$までの範囲で反復します。 +次の関数は `for` ループを用いて $1 + 2 + \dots + n$ の総和を計算しており、その結果は変数 `res` に記録されます。なお、Python の `range(a, b)` に対応する区間は「左閉右開」であり、走査範囲は $a, a + 1, \dots, b-1$ です。 === "Python" ```python title="iteration.py" def for_loop(n: int) -> int: - """forループ""" + """for ループ""" res = 0 - # 1, 2, ..., n-1, n の合計をループ + # 1, 2, ..., n-1, n を順に加算する for i in range(1, n + 1): res += i return res @@ -34,7 +34,7 @@ comments: true /* for ループ */ int forLoop(int n) { int res = 0; - // 1, 2, ..., n-1, n の合計をループ計算 + // 1, 2, ..., n-1, n を順に加算する for (int i = 1; i <= n; ++i) { res += i; } @@ -48,7 +48,7 @@ comments: true /* for ループ */ int forLoop(int n) { int res = 0; - // 1, 2, ..., n-1, n の合計をループ計算 + // 1, 2, ..., n-1, n を順に加算する for (int i = 1; i <= n; i++) { res += i; } @@ -59,88 +59,175 @@ comments: true === "C#" ```csharp title="iteration.cs" - [class]{iteration}-[func]{ForLoop} + /* for ループ */ + int ForLoop(int n) { + int res = 0; + // 1, 2, ..., n-1, n を順に加算する + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "Go" ```go title="iteration.go" - [class]{}-[func]{forLoop} + /* for ループ */ + func forLoop(n int) int { + res := 0 + // 1, 2, ..., n-1, n を順に加算する + for i := 1; i <= n; i++ { + res += i + } + return res + } ``` === "Swift" ```swift title="iteration.swift" - [class]{}-[func]{forLoop} + /* for ループ */ + func forLoop(n: Int) -> Int { + var res = 0 + // 1, 2, ..., n-1, n を順に加算する + for i in 1 ... n { + res += i + } + return res + } ``` === "JS" ```javascript title="iteration.js" - [class]{}-[func]{forLoop} + /* for ループ */ + function forLoop(n) { + let res = 0; + // 1, 2, ..., n-1, n を順に加算する + for (let i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "TS" ```typescript title="iteration.ts" - [class]{}-[func]{forLoop} + /* for ループ */ + function forLoop(n: number): number { + let res = 0; + // 1, 2, ..., n-1, n を順に加算する + for (let i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{forLoop} + /* for ループ */ + int forLoop(int n) { + int res = 0; + // 1, 2, ..., n-1, n を順に加算する + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "Rust" ```rust title="iteration.rs" - [class]{}-[func]{for_loop} + /* for ループ */ + fn for_loop(n: i32) -> i32 { + let mut res = 0; + // 1, 2, ..., n-1, n を順に加算する + for i in 1..=n { + res += i; + } + res + } ``` === "C" ```c title="iteration.c" - [class]{}-[func]{forLoop} + /* for ループ */ + int forLoop(int n) { + int res = 0; + // 1, 2, ..., n-1, n を順に加算する + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "Kotlin" ```kotlin title="iteration.kt" - [class]{}-[func]{forLoop} + /* for ループ */ + fun forLoop(n: Int): Int { + var res = 0 + // 1, 2, ..., n-1, n を順に加算する + for (i in 1..n) { + res += i + } + return res + } ``` === "Ruby" ```ruby title="iteration.rb" - [class]{}-[func]{for_loop} + ### for ループ ### + def for_loop(n) + res = 0 + + # 1, 2, ..., n-1, n を順に加算する + for i in 1..n + res += i + end + + res + end ``` -以下の図はこの合計関数を表しています。 +??? pythontutor "コードの可視化" -![Flowchart of the sum function](iteration_and_recursion.assets/iteration.png){ class="animation-figure" } +
+ -

図 2-1   Flowchart of the sum function

+次の図は、この総和関数のフローチャートです。 -この合計関数での操作数は入力データのサイズ$n$に比例する、つまり線形関係があります。**この「線形関係」こそが時間計算量が記述するものです**。このトピックについては次のセクションで詳しく説明します。 +![総和関数のフローチャート](iteration_and_recursion.assets/iteration.png){ class="animation-figure" } -### 2.   whileループ +

図 2-1   総和関数のフローチャート

-`for`ループと同様に、`while`ループは反復を実装するためのもう1つのアプローチです。`while`ループでは、プログラムは各反復の開始時に条件をチェックし、条件が真の場合は実行を継続し、そうでなければループを終了します。 +この総和関数の操作回数は入力データサイズ $n$ に比例し、言い換えれば「線形関係」にあります。実際、**時間計算量が記述するのはこの「線形関係」そのものです**。関連内容は次節で詳しく説明します。 -以下では`while`ループを使用して合計$1 + 2 + \dots + n$を実装します。 +### 2.   while ループ + +`for` ループと同様に、`while` ループも反復を実現する方法の 1 つです。`while` ループでは、各反復のたびにまず条件を確認し、条件が真であれば実行を続け、そうでなければループを終了します。 + +次に、`while` ループを使って $1 + 2 + \dots + n$ の総和を求めてみましょう。 === "Python" ```python title="iteration.py" def while_loop(n: int) -> int: - """whileループ""" + """while ループ""" res = 0 - i = 1 # 条件変数を初期化 - # 1, 2, ..., n-1, n の合計をループ + i = 1 # 条件変数を初期化する + # 1, 2, ..., n-1, n を順に加算する while i <= n: res += i - i += 1 # 条件変数を更新 + i += 1 # 条件変数を更新する return res ``` @@ -150,11 +237,11 @@ comments: true /* while ループ */ int whileLoop(int n) { int res = 0; - int i = 1; // 条件変数を初期化 - // 1, 2, ..., n-1, n の合計をループ計算 + int i = 1; // 条件変数を初期化する + // 1, 2, ..., n-1, n を順に加算する while (i <= n) { res += i; - i++; // 条件変数を更新 + i++; // 条件変数を更新する } return res; } @@ -166,11 +253,11 @@ comments: true /* while ループ */ int whileLoop(int n) { int res = 0; - int i = 1; // 条件変数を初期化 - // 1, 2, ..., n-1, n の合計をループ計算 + int i = 1; // 条件変数を初期化する + // 1, 2, ..., n-1, n を順に加算する while (i <= n) { res += i; - i++; // 条件変数を更新 + i++; // 条件変数を更新する } return res; } @@ -179,78 +266,188 @@ comments: true === "C#" ```csharp title="iteration.cs" - [class]{iteration}-[func]{WhileLoop} + /* while ループ */ + int WhileLoop(int n) { + int res = 0; + int i = 1; // 条件変数を初期化する + // 1, 2, ..., n-1, n を順に加算する + while (i <= n) { + res += i; + i += 1; // 条件変数を更新する + } + return res; + } ``` === "Go" ```go title="iteration.go" - [class]{}-[func]{whileLoop} + /* while ループ */ + func whileLoop(n int) int { + res := 0 + // 条件変数を初期化する + i := 1 + // 1, 2, ..., n-1, n を順に加算する + for i <= n { + res += i + // 条件変数を更新する + i++ + } + return res + } ``` === "Swift" ```swift title="iteration.swift" - [class]{}-[func]{whileLoop} + /* while ループ */ + func whileLoop(n: Int) -> Int { + var res = 0 + var i = 1 // 条件変数を初期化する + // 1, 2, ..., n-1, n を順に加算する + while i <= n { + res += i + i += 1 // 条件変数を更新する + } + return res + } ``` === "JS" ```javascript title="iteration.js" - [class]{}-[func]{whileLoop} + /* while ループ */ + function whileLoop(n) { + let res = 0; + let i = 1; // 条件変数を初期化する + // 1, 2, ..., n-1, n を順に加算する + while (i <= n) { + res += i; + i++; // 条件変数を更新する + } + return res; + } ``` === "TS" ```typescript title="iteration.ts" - [class]{}-[func]{whileLoop} + /* while ループ */ + function whileLoop(n: number): number { + let res = 0; + let i = 1; // 条件変数を初期化する + // 1, 2, ..., n-1, n を順に加算する + while (i <= n) { + res += i; + i++; // 条件変数を更新する + } + return res; + } ``` === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{whileLoop} + /* while ループ */ + int whileLoop(int n) { + int res = 0; + int i = 1; // 条件変数を初期化する + // 1, 2, ..., n-1, n を順に加算する + while (i <= n) { + res += i; + i++; // 条件変数を更新する + } + return res; + } ``` === "Rust" ```rust title="iteration.rs" - [class]{}-[func]{while_loop} + /* while ループ */ + fn while_loop(n: i32) -> i32 { + let mut res = 0; + let mut i = 1; // 条件変数を初期化する + + // 1, 2, ..., n-1, n を順に加算する + while i <= n { + res += i; + i += 1; // 条件変数を更新する + } + res + } ``` === "C" ```c title="iteration.c" - [class]{}-[func]{whileLoop} + /* while ループ */ + int whileLoop(int n) { + int res = 0; + int i = 1; // 条件変数を初期化する + // 1, 2, ..., n-1, n を順に加算する + while (i <= n) { + res += i; + i++; // 条件変数を更新する + } + return res; + } ``` === "Kotlin" ```kotlin title="iteration.kt" - [class]{}-[func]{whileLoop} + /* while ループ */ + fun whileLoop(n: Int): Int { + var res = 0 + var i = 1 // 条件変数を初期化する + // 1, 2, ..., n-1, n を順に加算する + while (i <= n) { + res += i + i++ // 条件変数を更新する + } + return res + } ``` === "Ruby" ```ruby title="iteration.rb" - [class]{}-[func]{while_loop} + ### while ループ ### + def while_loop(n) + res = 0 + i = 1 # 条件変数を初期化する + + # 1, 2, ..., n-1, n を順に加算する + while i <= n + res += i + i += 1 # 条件変数を更新する + end + + res + end ``` -**`while`ループは`for`ループよりも柔軟性を提供します**。特に、条件変数のカスタム初期化と各ステップでの変更が可能です。 +??? pythontutor "コードの可視化" -例えば、以下のコードでは、条件変数$i$が各ラウンドで2回更新されますが、これは`for`ループでは実装が不便です。 +
+ + +**`while` ループは `for` ループより自由度が高い**です。`while` ループでは、条件変数の初期化や更新手順を柔軟に設計できます。 + +たとえば次のコードでは、条件変数 $i$ が各反復で 2 回更新されており、このようなケースは `for` ループではあまり扱いやすくありません。 === "Python" ```python title="iteration.py" def while_loop_ii(n: int) -> int: - """whileループ(2つの更新)""" + """while ループ(2回更新)""" res = 0 - i = 1 # 条件変数を初期化 - # 1, 4, 10, ... の合計をループ + i = 1 # 条件変数を初期化する + # 1, 4, 10, ... を順に加算する while i <= n: res += i - # 条件変数を更新 + # 条件変数を更新する i += 1 i *= 2 return res @@ -259,14 +456,14 @@ comments: true === "C++" ```cpp title="iteration.cpp" - /* while ループ(2つの更新) */ + /* while ループ(2回更新) */ int whileLoopII(int n) { int res = 0; - int i = 1; // 条件変数を初期化 - // 1, 4, 10, ... の合計をループ計算 + int i = 1; // 条件変数を初期化する + // 1, 4, 10, ... を順に加算する while (i <= n) { res += i; - // 条件変数を更新 + // 条件変数を更新する i++; i *= 2; } @@ -277,14 +474,14 @@ comments: true === "Java" ```java title="iteration.java" - /* while ループ(2つの更新) */ + /* while ループ(2回更新) */ int whileLoopII(int n) { int res = 0; - int i = 1; // 条件変数を初期化 - // 1, 4, 10, ... の合計をループ計算 + int i = 1; // 条件変数を初期化する + // 1, 4, 10, ... を順に加算する while (i <= n) { res += i; - // 条件変数を更新 + // 条件変数を更新する i++; i *= 2; } @@ -295,78 +492,221 @@ comments: true === "C#" ```csharp title="iteration.cs" - [class]{iteration}-[func]{WhileLoopII} + /* while ループ(2回更新) */ + int WhileLoopII(int n) { + int res = 0; + int i = 1; // 条件変数を初期化する + // 1, 4, 10, ... を順に加算する + while (i <= n) { + res += i; + // 条件変数を更新する + i += 1; + i *= 2; + } + return res; + } ``` === "Go" ```go title="iteration.go" - [class]{}-[func]{whileLoopII} + /* while ループ(2回更新) */ + func whileLoopII(n int) int { + res := 0 + // 条件変数を初期化する + i := 1 + // 1, 4, 10, ... を順に加算する + for i <= n { + res += i + // 条件変数を更新する + i++ + i *= 2 + } + return res + } ``` === "Swift" ```swift title="iteration.swift" - [class]{}-[func]{whileLoopII} + /* while ループ(2回更新) */ + func whileLoopII(n: Int) -> Int { + var res = 0 + var i = 1 // 条件変数を初期化する + // 1, 4, 10, ... を順に加算する + while i <= n { + res += i + // 条件変数を更新する + i += 1 + i *= 2 + } + return res + } ``` === "JS" ```javascript title="iteration.js" - [class]{}-[func]{whileLoopII} + /* while ループ(2回更新) */ + function whileLoopII(n) { + let res = 0; + let i = 1; // 条件変数を初期化する + // 1, 4, 10, ... を順に加算する + while (i <= n) { + res += i; + // 条件変数を更新する + i++; + i *= 2; + } + return res; + } ``` === "TS" ```typescript title="iteration.ts" - [class]{}-[func]{whileLoopII} + /* while ループ(2回更新) */ + function whileLoopII(n: number): number { + let res = 0; + let i = 1; // 条件変数を初期化する + // 1, 4, 10, ... を順に加算する + while (i <= n) { + res += i; + // 条件変数を更新する + i++; + i *= 2; + } + return res; + } ``` === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{whileLoopII} + /* while ループ(2回更新) */ + int whileLoopII(int n) { + int res = 0; + int i = 1; // 条件変数を初期化する + // 1, 4, 10, ... を順に加算する + while (i <= n) { + res += i; + // 条件変数を更新する + i++; + i *= 2; + } + return res; + } ``` === "Rust" ```rust title="iteration.rs" - [class]{}-[func]{while_loop_ii} + /* while ループ(2回更新) */ + fn while_loop_ii(n: i32) -> i32 { + let mut res = 0; + let mut i = 1; // 条件変数を初期化する + + // 1, 4, 10, ... を順に加算する + while i <= n { + res += i; + // 条件変数を更新する + i += 1; + i *= 2; + } + res + } ``` === "C" ```c title="iteration.c" - [class]{}-[func]{whileLoopII} + /* while ループ(2回更新) */ + int whileLoopII(int n) { + int res = 0; + int i = 1; // 条件変数を初期化する + // 1, 4, 10, ... を順に加算する + while (i <= n) { + res += i; + // 条件変数を更新する + i++; + i *= 2; + } + return res; + } ``` === "Kotlin" ```kotlin title="iteration.kt" - [class]{}-[func]{whileLoopII} + /* while ループ(2回更新) */ + fun whileLoopII(n: Int): Int { + var res = 0 + var i = 1 // 条件変数を初期化する + // 1, 4, 10, ... を順に加算する + while (i <= n) { + res += i + // 条件変数を更新する + i++ + i *= 2 + } + return res + } ``` === "Ruby" ```ruby title="iteration.rb" - [class]{}-[func]{while_loop_ii} + ### while ループ ### + def while_loop(n) + res = 0 + i = 1 # 条件変数を初期化する + + # 1, 2, ..., n-1, n を順に加算する + while i <= n + res += i + i += 1 # 条件変数を更新する + end + + res + end + + # ## while ループ(2 回更新)### + def while_loop_ii(n) + res = 0 + i = 1 # 条件変数を初期化する + + # 1, 4, 10, ... を順に加算する + while i <= n + res += i + # 条件変数を更新する + i += 1 + i *= 2 + end + + res + end ``` -全体的に、**`for`ループはより簡潔で、`while`ループはより柔軟です**。どちらも反復構造を実装できます。どちらを使用するかは、問題の具体的な要件に基づいて決定する必要があります。 +??? pythontutor "コードの可視化" + +
+ + +総じて、**`for` ループのコードはより簡潔で、`while` ループはより柔軟**です。どちらも反復構造を実現できますが、どちらを使うかは問題ごとの要件に応じて決めるべきです。 ### 3.   ネストしたループ -1つのループ構造を別のループ構造内にネストできます。以下は`for`ループを使用した例です: +1 つのループ構造の中に別のループ構造を入れ子にできます。以下では `for` ループを例にします。 === "Python" ```python title="iteration.py" def nested_for_loop(n: int) -> str: - """二重forループ""" + """二重 for ループ""" res = "" - # i = 1, 2, ..., n-1, n をループ + # i = 1, 2, ..., n-1, n とループする for i in range(1, n + 1): - # j = 1, 2, ..., n-1, n をループ + # j = 1, 2, ..., n-1, n とループする for j in range(1, n + 1): res += f"({i}, {j}), " return res @@ -375,12 +715,12 @@ comments: true === "C++" ```cpp title="iteration.cpp" - /* 2重 for ループ */ + /* 二重 for ループ */ string nestedForLoop(int n) { ostringstream res; - // ループ i = 1, 2, ..., n-1, n + // i = 1, 2, ..., n-1, n とループする for (int i = 1; i <= n; ++i) { - // ループ j = 1, 2, ..., n-1, n + // j = 1, 2, ..., n-1, n とループする for (int j = 1; j <= n; ++j) { res << "(" << i << ", " << j << "), "; } @@ -392,12 +732,12 @@ comments: true === "Java" ```java title="iteration.java" - /* 2重 for ループ */ + /* 二重 for ループ */ String nestedForLoop(int n) { StringBuilder res = new StringBuilder(); - // ループ i = 1, 2, ..., n-1, n + // i = 1, 2, ..., n-1, n とループする for (int i = 1; i <= n; i++) { - // ループ j = 1, 2, ..., n-1, n + // j = 1, 2, ..., n-1, n とループする for (int j = 1; j <= n; j++) { res.append("(" + i + ", " + j + "), "); } @@ -409,87 +749,208 @@ comments: true === "C#" ```csharp title="iteration.cs" - [class]{iteration}-[func]{NestedForLoop} + /* 二重 for ループ */ + string NestedForLoop(int n) { + StringBuilder res = new(); + // i = 1, 2, ..., n-1, n とループする + for (int i = 1; i <= n; i++) { + // j = 1, 2, ..., n-1, n とループする + for (int j = 1; j <= n; j++) { + res.Append($"({i}, {j}), "); + } + } + return res.ToString(); + } ``` === "Go" ```go title="iteration.go" - [class]{}-[func]{nestedForLoop} + /* 二重 for ループ */ + func nestedForLoop(n int) string { + res := "" + // i = 1, 2, ..., n-1, n とループする + for i := 1; i <= n; i++ { + for j := 1; j <= n; j++ { + // j = 1, 2, ..., n-1, n とループする + res += fmt.Sprintf("(%d, %d), ", i, j) + } + } + return res + } ``` === "Swift" ```swift title="iteration.swift" - [class]{}-[func]{nestedForLoop} + /* 二重 for ループ */ + func nestedForLoop(n: Int) -> String { + var res = "" + // i = 1, 2, ..., n-1, n とループする + for i in 1 ... n { + // j = 1, 2, ..., n-1, n とループする + for j in 1 ... n { + res.append("(\(i), \(j)), ") + } + } + return res + } ``` === "JS" ```javascript title="iteration.js" - [class]{}-[func]{nestedForLoop} + /* 二重 for ループ */ + function nestedForLoop(n) { + let res = ''; + // i = 1, 2, ..., n-1, n とループする + for (let i = 1; i <= n; i++) { + // j = 1, 2, ..., n-1, n とループする + for (let j = 1; j <= n; j++) { + res += `(${i}, ${j}), `; + } + } + return res; + } ``` === "TS" ```typescript title="iteration.ts" - [class]{}-[func]{nestedForLoop} + /* 二重 for ループ */ + function nestedForLoop(n: number): string { + let res = ''; + // i = 1, 2, ..., n-1, n とループする + for (let i = 1; i <= n; i++) { + // j = 1, 2, ..., n-1, n とループする + for (let j = 1; j <= n; j++) { + res += `(${i}, ${j}), `; + } + } + return res; + } ``` === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{nestedForLoop} + /* 二重 for ループ */ + String nestedForLoop(int n) { + String res = ""; + // i = 1, 2, ..., n-1, n とループする + for (int i = 1; i <= n; i++) { + // j = 1, 2, ..., n-1, n とループする + for (int j = 1; j <= n; j++) { + res += "($i, $j), "; + } + } + return res; + } ``` === "Rust" ```rust title="iteration.rs" - [class]{}-[func]{nested_for_loop} + /* 二重 for ループ */ + fn nested_for_loop(n: i32) -> String { + let mut res = vec![]; + // i = 1, 2, ..., n-1, n とループする + for i in 1..=n { + // j = 1, 2, ..., n-1, n とループする + for j in 1..=n { + res.push(format!("({}, {}), ", i, j)); + } + } + res.join("") + } ``` === "C" ```c title="iteration.c" - [class]{}-[func]{nestedForLoop} + /* 二重 for ループ */ + char *nestedForLoop(int n) { + // n * n は対応する点の個数であり、"(i, j), " に対応する文字列長の最大は 6+10*2 で、さらに末尾の空文字 \0 のための追加領域が必要 + int size = n * n * 26 + 1; + char *res = malloc(size * sizeof(char)); + // i = 1, 2, ..., n-1, n とループする + for (int i = 1; i <= n; i++) { + // j = 1, 2, ..., n-1, n とループする + for (int j = 1; j <= n; j++) { + char tmp[26]; + snprintf(tmp, sizeof(tmp), "(%d, %d), ", i, j); + strncat(res, tmp, size - strlen(res) - 1); + } + } + return res; + } ``` === "Kotlin" ```kotlin title="iteration.kt" - [class]{}-[func]{nestedForLoop} + /* 二重 for ループ */ + fun nestedForLoop(n: Int): String { + val res = StringBuilder() + // i = 1, 2, ..., n-1, n とループする + for (i in 1..n) { + // j = 1, 2, ..., n-1, n とループする + for (j in 1..n) { + res.append(" ($i, $j), ") + } + } + return res.toString() + } ``` === "Ruby" ```ruby title="iteration.rb" - [class]{}-[func]{nested_for_loop} + ### 二重 for ループ ### + def nested_for_loop(n) + res = "" + + # i = 1, 2, ..., n-1, n とループする + for i in 1..n + # j = 1, 2, ..., n-1, n とループする + for j in 1..n + res += "(#{i}, #{j}), " + end + end + + res + end ``` -以下の図はこのネストしたループを表しています。 +??? pythontutor "コードの可視化" -![Flowchart of the nested loop](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" } +
+ -

図 2-2   Flowchart of the nested loop

+次の図は、このネストしたループのフローチャートです。 -このような場合、関数の操作数は$n^2$に比例します。つまり、アルゴリズムの実行時間と入力データのサイズ$n$には「二次関係」があります。 +![ネストしたループのフローチャート](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" } -さらにネストしたループを追加することで複雑度を高めることができ、各レベルのネストは事実上「次元を増加」させ、時間計算量を「三次」、「四次」などに引き上げます。 +

図 2-2   ネストしたループのフローチャート

+ +この場合、関数の操作回数は $n^2$ に比例し、言い換えればアルゴリズムの実行時間は入力データサイズ $n$ と「二次関係」にあります。 + +さらにネストしたループを追加することもできます。ネストが 1 段増えるたびに「次元が 1 つ上がる」ことになり、時間計算量は「三次関係」「四次関係」へと高くなっていきます。 ## 2.2.2   再帰 -再帰は、関数が自分自身を呼び出すことで問題を解決するアルゴリズム戦略です。主に2つのフェーズが含まれます: + 再帰(recursion)は、関数が自分自身を呼び出すことで問題を解決するアルゴリズム戦略です。主に 2 つの段階から成ります。 -1. **呼び出し**: プログラムが自分自身を繰り返し呼び出し、しばしばより小さいまたはより単純な引数で、「終了条件」に向かって進みます。 -2. **返却**: 「終了条件」がトリガーされると、プログラムは最も深い再帰関数から返り始め、各レイヤーの結果を集約します。 +1. **再帰呼び出し**:プログラムは自分自身をより深く呼び出し続け、通常はより小さい、またはより単純化された引数を渡し、「終了条件」に達するまで進みます。 +2. **復帰**: 「終了条件」が満たされると、プログラムは最も深い再帰関数から 1 層ずつ戻り、各層の結果をまとめていきます。 -実装の観点から、再帰コードは主に3つの要素を含みます。 +実装の観点から見ると、再帰コードは主に 3 つの要素から成ります。 -1. **終了条件**: 「呼び出し」から「返却」にいつ切り替えるかを決定します。 -2. **再帰呼び出し**: 「呼び出し」に対応し、関数が自分自身を呼び出し、通常はより小さいまたはより単純化されたパラメータで行います。 -3. **結果の返却**: 「返却」に対応し、現在の再帰レベルの結果が前のレイヤーに返されます。 +1. **終了条件**:いつ再帰呼び出しから復帰へ切り替わるかを決めます。 +2. **再帰呼び出し**:再帰呼び出しに対応し、関数が自分自身を呼び出します。通常はより小さい、またはより単純化された引数を入力します。 +3. **結果の返却**:復帰に対応し、現在の再帰レベルの結果を 1 つ上の層へ返します。 -以下のコードを観察してください。単純に関数`recur(n)`を呼び出すだけで$1 + 2 + \dots + n$の合計を計算できます: +次のコードを見ると、関数 `recur(n)` を呼び出すだけで $1 + 2 + \dots + n$ を計算できます。 === "Python" @@ -501,7 +962,7 @@ comments: true return 1 # 再帰:再帰呼び出し res = recur(n - 1) - # 復帰:結果を返す + # 帰りがけ:結果を返す return n + res ``` @@ -515,7 +976,7 @@ comments: true return 1; // 再帰:再帰呼び出し int res = recur(n - 1); - // 戻り値:結果を返す + // 帰りがけ:結果を返す return n + res; } ``` @@ -530,7 +991,7 @@ comments: true return 1; // 再帰:再帰呼び出し int res = recur(n - 1); - // 戻り値:結果を返す + // 帰りがけ:結果を返す return n + res; } ``` @@ -538,102 +999,196 @@ comments: true === "C#" ```csharp title="recursion.cs" - [class]{recursion}-[func]{Recur} + /* 再帰 */ + int Recur(int n) { + // 終了条件 + if (n == 1) + return 1; + // 再帰:再帰呼び出し + int res = Recur(n - 1); + // 帰りがけ:結果を返す + return n + res; + } ``` === "Go" ```go title="recursion.go" - [class]{}-[func]{recur} + /* 再帰 */ + func recur(n int) int { + // 終了条件 + if n == 1 { + return 1 + } + // 再帰:再帰呼び出し + res := recur(n - 1) + // 帰りがけ:結果を返す + return n + res + } ``` === "Swift" ```swift title="recursion.swift" - [class]{}-[func]{recur} + /* 再帰 */ + func recur(n: Int) -> Int { + // 終了条件 + if n == 1 { + return 1 + } + // 再帰:再帰呼び出し + let res = recur(n: n - 1) + // 帰りがけ:結果を返す + return n + res + } ``` === "JS" ```javascript title="recursion.js" - [class]{}-[func]{recur} + /* 再帰 */ + function recur(n) { + // 終了条件 + if (n === 1) return 1; + // 再帰:再帰呼び出し + const res = recur(n - 1); + // 帰りがけ:結果を返す + return n + res; + } ``` === "TS" ```typescript title="recursion.ts" - [class]{}-[func]{recur} + /* 再帰 */ + function recur(n: number): number { + // 終了条件 + if (n === 1) return 1; + // 再帰:再帰呼び出し + const res = recur(n - 1); + // 帰りがけ:結果を返す + return n + res; + } ``` === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{recur} + /* 再帰 */ + int recur(int n) { + // 終了条件 + if (n == 1) return 1; + // 再帰:再帰呼び出し + int res = recur(n - 1); + // 帰りがけ:結果を返す + return n + res; + } ``` === "Rust" ```rust title="recursion.rs" - [class]{}-[func]{recur} + /* 再帰 */ + fn recur(n: i32) -> i32 { + // 終了条件 + if n == 1 { + return 1; + } + // 再帰:再帰呼び出し + let res = recur(n - 1); + // 帰りがけ:結果を返す + n + res + } ``` === "C" ```c title="recursion.c" - [class]{}-[func]{recur} + /* 再帰 */ + int recur(int n) { + // 終了条件 + if (n == 1) + return 1; + // 再帰:再帰呼び出し + int res = recur(n - 1); + // 帰りがけ:結果を返す + return n + res; + } ``` === "Kotlin" ```kotlin title="recursion.kt" - [class]{}-[func]{recur} + /* 再帰 */ + fun recur(n: Int): Int { + // 終了条件 + if (n == 1) + return 1 + // 再帰: 再帰呼び出し + val res = recur(n - 1) + // 戻る: 結果を返す + return n + res + } ``` === "Ruby" ```ruby title="recursion.rb" - [class]{}-[func]{recur} + ### 再帰 ### + def recur(n) + # 終了条件 + return 1 if n == 1 + # 再帰:再帰呼び出し + res = recur(n - 1) + # 帰りがけ:結果を返す + n + res + end ``` -以下の図はこの関数の再帰プロセスを示しています。 +??? pythontutor "コードの可視化" -![Recursive process of the sum function](iteration_and_recursion.assets/recursion_sum.png){ class="animation-figure" } +
+ -

図 2-3   Recursive process of the sum function

+次の図は、この関数の再帰過程を示しています。 -反復と再帰は計算の観点から同じ結果を達成できますが、**それらは思考と問題解決の全く異なるパラダイムを表します**。 +![総和関数の再帰過程](iteration_and_recursion.assets/recursion_sum.png){ class="animation-figure" } -- **反復**: 「ボトムアップ」で問題を解決します。最も基本的なステップから始まり、タスクが完了するまでこれらのステップを繰り返し追加または累積します。 -- **再帰**: 「トップダウン」で問題を解決します。元の問題をより小さなサブ問題に分解し、各サブ問題は元の問題と同じ形式を持ちます。これらのサブ問題は、解が分かっているベースケースで停止するまで、さらに小さなサブ問題に分解されます。 +

図 2-3   総和関数の再帰過程

-先ほどの合計関数の例を取ってみましょう。$f(n) = 1 + 2 + \dots + n$として定義されます。 +計算の観点では、反復と再帰は同じ結果を得られますが、**それらは問題を考え解決するためのまったく異なる 2 つのパラダイムを表しています**。 -- **反復**: このアプローチでは、ループ内で合計プロセスをシミュレートします。$1$から始まり$n$まで横断し、各反復で合計操作を実行して最終的に$f(n)$を計算します。 -- **再帰**: ここでは、問題はサブ問題に分解されます:$f(n) = n + f(n-1)$。この分解は、ベースケースの$f(1) = 1$に到達するまで再帰的に続き、そこで再帰が終了します。 +- **反復**:「ボトムアップ」で問題を解決します。最も基本的な手順から始め、それらを繰り返したり積み上げたりして、処理が完了するまで進めます。 +- **再帰**:「トップダウン」で問題を解決します。元の問題をより小さな部分問題に分解し、それらの部分問題は元の問題と同じ形を持ちます。さらに部分問題をより小さな部分問題へと分解し、基本ケースに達したところで停止します(基本ケースの解は既知です)。 + +前述の総和関数を例に、問題を $f(n) = 1 + 2 + \dots + n$ とします。 + +- **反復**:ループ内で総和の過程を模擬し、$1$ から $n$ まで走査して、各反復で加算を行えば $f(n)$ を求められます。 +- **再帰**:問題を部分問題 $f(n) = n + f(n-1)$ に分解し、これを再帰的に分解し続け、基本ケース $f(1) = 1$ に達したところで終了します。 ### 1.   呼び出しスタック -再帰関数が自分自身を呼び出すたびに、システムは新しく開始された関数にメモリを割り当てて、ローカル変数、戻りアドレス、その他の関連情報を格納します。これは2つの主要な結果をもたらします。 +再帰関数が自分自身を呼び出すたびに、システムは新たに開始された関数のためにメモリを割り当て、局所変数、呼び出し先アドレス、その他の情報を保存します。これにより 2 つの結果が生じます。 -- 関数のコンテキストデータは「スタックフレーム空間」と呼ばれるメモリ領域に格納され、関数が返された後にのみ解放されます。したがって、**再帰は一般的に反復よりも多くのメモリ空間を消費します**。 -- 再帰呼び出しは追加のオーバーヘッドを導入します。**したがって、再帰は通常ループよりも時間効率が劣ります。** +- 関数のコンテキストデータは「スタックフレーム領域」と呼ばれるメモリ領域に保存され、関数が戻るまで解放されません。したがって、**再帰は通常、反復より多くのメモリ空間を消費します**。 +- 再帰による関数呼び出しには追加のオーバーヘッドが発生します。**そのため再帰は通常、ループより時間効率が低くなります**。 -以下の図に示されているように、終了条件がトリガーされる前に$n$個の未返却の再帰関数があり、**再帰の深さが$n$であることを示しています**。 +次の図のように、終了条件が発動する前には、まだ戻っていない再帰関数が同時に $n$ 個存在し、**再帰の深さは $n$** になります。 -![Recursion call depth](iteration_and_recursion.assets/recursion_sum_depth.png){ class="animation-figure" } +![再帰呼び出しの深さ](iteration_and_recursion.assets/recursion_sum_depth.png){ class="animation-figure" } -

図 2-4   Recursion call depth

+

図 2-4   再帰呼び出しの深さ

-実際には、プログラミング言語で許可される再帰の深さは通常制限されており、過度に深い再帰はスタックオーバーフローエラーを引き起こす可能性があります。 +実際には、プログラミング言語が許容する再帰の深さには通常上限があり、深すぎる再帰はスタックオーバーフローを引き起こす可能性があります。 ### 2.   末尾再帰 -興味深いことに、**関数が返す直前の最後のステップとして再帰呼び出しを実行する場合**、コンパイラまたはインタープリターによって反復と同じ空間効率になるように最適化できます。このシナリオは末尾再帰として知られています。 +興味深いことに、**関数が返る直前の最後の処理で再帰呼び出しを行う場合**、その関数はコンパイラやインタプリタによって最適化され、空間効率が反復と同程度になることがあります。これを末尾再帰(tail recursion)と呼びます。 -- **通常の再帰**: 標準的な再帰では、関数が前のレベルに戻ったとき、さらにコードを実行し続けるため、システムは前の呼び出しのコンテキストを保存する必要があります。 -- **末尾再帰**: ここでは、再帰呼び出しは関数が返す前の最終操作です。これは、前のレベルに戻った際に、さらなるアクションが必要ないことを意味するため、システムは前のレベルのコンテキストを保存する必要がありません。 +- **通常の再帰**:関数が 1 つ上の階層の関数へ戻った後も、引き続きコードを実行する必要があるため、システムは 1 つ上の呼び出しのコンテキストを保存しておく必要があります。 +- **末尾再帰**:再帰呼び出しが関数の返却前の最後の操作であるため、1 つ上の階層へ戻った後に他の処理を続ける必要がなく、システムは 1 つ上の関数のコンテキストを保存する必要がありません。 -例えば、$1 + 2 + \dots + n$の計算では、結果変数`res`を関数のパラメータにすることで、末尾再帰を実現できます: +$1 + 2 + \dots + n$ の計算を例にすると、結果変数 `res` を関数の引数にすることで、末尾再帰を実現できます。 === "Python" @@ -676,90 +1231,165 @@ comments: true === "C#" ```csharp title="recursion.cs" - [class]{recursion}-[func]{TailRecur} + /* 末尾再帰 */ + int TailRecur(int n, int res) { + // 終了条件 + if (n == 0) + return res; + // 末尾再帰呼び出し + return TailRecur(n - 1, res + n); + } ``` === "Go" ```go title="recursion.go" - [class]{}-[func]{tailRecur} + /* 末尾再帰 */ + func tailRecur(n int, res int) int { + // 終了条件 + if n == 0 { + return res + } + // 末尾再帰呼び出し + return tailRecur(n-1, res+n) + } ``` === "Swift" ```swift title="recursion.swift" - [class]{}-[func]{tailRecur} + /* 末尾再帰 */ + func tailRecur(n: Int, res: Int) -> Int { + // 終了条件 + if n == 0 { + return res + } + // 末尾再帰呼び出し + return tailRecur(n: n - 1, res: res + n) + } ``` === "JS" ```javascript title="recursion.js" - [class]{}-[func]{tailRecur} + /* 末尾再帰 */ + function tailRecur(n, res) { + // 終了条件 + if (n === 0) return res; + // 末尾再帰呼び出し + return tailRecur(n - 1, res + n); + } ``` === "TS" ```typescript title="recursion.ts" - [class]{}-[func]{tailRecur} + /* 末尾再帰 */ + function tailRecur(n: number, res: number): number { + // 終了条件 + if (n === 0) return res; + // 末尾再帰呼び出し + return tailRecur(n - 1, res + n); + } ``` === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{tailRecur} + /* 末尾再帰 */ + int tailRecur(int n, int res) { + // 終了条件 + if (n == 0) return res; + // 末尾再帰呼び出し + return tailRecur(n - 1, res + n); + } ``` === "Rust" ```rust title="recursion.rs" - [class]{}-[func]{tail_recur} + /* 末尾再帰 */ + fn tail_recur(n: i32, res: i32) -> i32 { + // 終了条件 + if n == 0 { + return res; + } + // 末尾再帰呼び出し + tail_recur(n - 1, res + n) + } ``` === "C" ```c title="recursion.c" - [class]{}-[func]{tailRecur} + /* 末尾再帰 */ + int tailRecur(int n, int res) { + // 終了条件 + if (n == 0) + return res; + // 末尾再帰呼び出し + return tailRecur(n - 1, res + n); + } ``` === "Kotlin" ```kotlin title="recursion.kt" - [class]{}-[func]{tailRecur} + /* 末尾再帰 */ + tailrec fun tailRecur(n: Int, res: Int): Int { + // `tailrec` キーワードを追加して末尾再帰最適化を有効にする + // 終了条件 + if (n == 0) + return res + // 末尾再帰呼び出し + return tailRecur(n - 1, res + n) + } ``` === "Ruby" ```ruby title="recursion.rb" - [class]{}-[func]{tail_recur} + ### 末尾再帰 ### + def tail_recur(n, res) + # 終了条件 + return res if n == 0 + # 末尾再帰呼び出し + tail_recur(n - 1, res + n) + end ``` -末尾再帰の実行プロセスは以下の図に示されています。通常の再帰と末尾再帰を比較すると、合計操作のポイントが異なります。 +??? pythontutor "コードの可視化" -- **通常の再帰**: 合計操作は「返却」フェーズで発生し、各レイヤーが返った後にもう一度合計が必要です。 -- **末尾再帰**: 合計操作は「呼び出し」フェーズで発生し、「返却」フェーズは各レイヤーを通じて返すだけです。 +
+ -![Tail recursion process](iteration_and_recursion.assets/tail_recursion_sum.png){ class="animation-figure" } +末尾再帰の実行過程を次の図に示します。通常の再帰と末尾再帰を比べると、加算処理が実行されるタイミングが異なります。 -

図 2-5   Tail recursion process

+- **通常の再帰**:加算処理は復帰の過程で実行され、各層が戻るたびにもう一度加算を行います。 +- **末尾再帰**:加算処理は再帰呼び出しの過程で実行され、復帰の過程では各層が戻るだけで済みます。 + +![末尾再帰の過程](iteration_and_recursion.assets/tail_recursion_sum.png){ class="animation-figure" } + +

図 2-5   末尾再帰の過程

!!! tip - 多くのコンパイラやインタープリターは末尾再帰最適化をサポートしていないことに注意してください。例えば、Pythonはデフォルトで末尾再帰最適化をサポートしていないため、関数が末尾再帰の形式であっても、スタックオーバーフローの問題に遭遇する可能性があります。 + 多くのコンパイラやインタプリタは末尾再帰最適化をサポートしていない点に注意してください。たとえば、Python はデフォルトで末尾再帰最適化をサポートしていないため、関数が末尾再帰の形であっても、スタックオーバーフローが発生する可能性があります。 ### 3.   再帰木 -「分割統治」に関連するアルゴリズムを扱う際、再帰は反復よりもしばしばより直感的なアプローチとより読みやすいコードを提供します。「フィボナッチ数列」を例に取ってみましょう。 +「分割統治」に関連するアルゴリズム問題を扱う際、再帰は反復よりも発想が直感的で、コードも読みやすいことがよくあります。「フィボナッチ数列」を例に見てみましょう。 !!! question - フィボナッチ数列$0, 1, 1, 2, 3, 5, 8, 13, \dots$が与えられた場合、数列の$n$番目の数を求めなさい。 + フィボナッチ数列 $0, 1, 1, 2, 3, 5, 8, 13, \dots$ が与えられたとき、この数列の第 $n$ 項を求めてください。 -フィボナッチ数列の$n$番目の数を$f(n)$とすると、2つの結論を簡単に導き出せます: +フィボナッチ数列の第 $n$ 項を $f(n)$ とすると、次の 2 つが容易に分かります。 -- 数列の最初の2つの数は$f(1) = 0$と$f(2) = 1$です。 -- 数列の各数は前の2つの数の合計です。つまり、$f(n) = f(n - 1) + f(n - 2)$です。 +- 数列の最初の 2 項は $f(1) = 0$ と $f(2) = 1$ です。 +- 数列中の各項は直前の 2 項の和であり、すなわち $f(n) = f(n - 1) + f(n - 2)$ です。 -再帰関係を使用し、最初の2つの数を終了条件として考慮すると、再帰コードを書けます。`fib(n)`を呼び出すとフィボナッチ数列の$n$番目の数が得られます: +漸化式に従って再帰呼び出しを行い、最初の 2 項を終了条件とすれば、再帰コードを書けます。`fib(n)` を呼び出すことでフィボナッチ数列の第 $n$ 項を得られます。 === "Python" @@ -769,7 +1399,7 @@ comments: true # 終了条件 f(1) = 0, f(2) = 1 if n == 1 or n == 2: return n - 1 - # 再帰呼び出し f(n) = f(n-1) + f(n-2) + # f(n) = f(n-1) + f(n-2) を再帰的に呼び出す res = fib(n - 1) + fib(n - 2) # 結果 f(n) を返す return res @@ -783,7 +1413,7 @@ comments: true // 終了条件 f(1) = 0, f(2) = 1 if (n == 1 || n == 2) return n - 1; - // 再帰呼び出し f(n) = f(n-1) + f(n-2) + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す int res = fib(n - 1) + fib(n - 2); // 結果 f(n) を返す return res; @@ -798,7 +1428,7 @@ comments: true // 終了条件 f(1) = 0, f(2) = 1 if (n == 1 || n == 2) return n - 1; - // 再帰呼び出し f(n) = f(n-1) + f(n-2) + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す int res = fib(n - 1) + fib(n - 2); // 結果 f(n) を返す return res; @@ -808,119 +1438,213 @@ comments: true === "C#" ```csharp title="recursion.cs" - [class]{recursion}-[func]{Fib} + /* フィボナッチ数列:再帰 */ + int Fib(int n) { + // 終了条件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1; + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す + int res = Fib(n - 1) + Fib(n - 2); + // 結果 f(n) を返す + return res; + } ``` === "Go" ```go title="recursion.go" - [class]{}-[func]{fib} + /* フィボナッチ数列:再帰 */ + func fib(n int) int { + // 終了条件 f(1) = 0, f(2) = 1 + if n == 1 || n == 2 { + return n - 1 + } + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す + res := fib(n-1) + fib(n-2) + // 結果 f(n) を返す + return res + } ``` === "Swift" ```swift title="recursion.swift" - [class]{}-[func]{fib} + /* フィボナッチ数列:再帰 */ + func fib(n: Int) -> Int { + // 終了条件 f(1) = 0, f(2) = 1 + if n == 1 || n == 2 { + return n - 1 + } + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す + let res = fib(n: n - 1) + fib(n: n - 2) + // 結果 f(n) を返す + return res + } ``` === "JS" ```javascript title="recursion.js" - [class]{}-[func]{fib} + /* フィボナッチ数列:再帰 */ + function fib(n) { + // 終了条件 f(1) = 0, f(2) = 1 + if (n === 1 || n === 2) return n - 1; + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す + const res = fib(n - 1) + fib(n - 2); + // 結果 f(n) を返す + return res; + } ``` === "TS" ```typescript title="recursion.ts" - [class]{}-[func]{fib} + /* フィボナッチ数列:再帰 */ + function fib(n: number): number { + // 終了条件 f(1) = 0, f(2) = 1 + if (n === 1 || n === 2) return n - 1; + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す + const res = fib(n - 1) + fib(n - 2); + // 結果 f(n) を返す + return res; + } ``` === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{fib} + /* フィボナッチ数列:再帰 */ + int fib(int n) { + // 終了条件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) return n - 1; + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す + int res = fib(n - 1) + fib(n - 2); + // 結果 f(n) を返す + return res; + } ``` === "Rust" ```rust title="recursion.rs" - [class]{}-[func]{fib} + /* フィボナッチ数列:再帰 */ + fn fib(n: i32) -> i32 { + // 終了条件 f(1) = 0, f(2) = 1 + if n == 1 || n == 2 { + return n - 1; + } + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す + let res = fib(n - 1) + fib(n - 2); + // 結果を返す + res + } ``` === "C" ```c title="recursion.c" - [class]{}-[func]{fib} + /* フィボナッチ数列:再帰 */ + int fib(int n) { + // 終了条件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1; + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す + int res = fib(n - 1) + fib(n - 2); + // 結果 f(n) を返す + return res; + } ``` === "Kotlin" ```kotlin title="recursion.kt" - [class]{}-[func]{fib} + /* フィボナッチ数列:再帰 */ + fun fib(n: Int): Int { + // 終了条件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1 + // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す + val res = fib(n - 1) + fib(n - 2) + // 結果 f(n) を返す + return res + } ``` === "Ruby" ```ruby title="recursion.rb" - [class]{}-[func]{fib} + ### フィボナッチ数列:再帰 ### + def fib(n) + # 終了条件 f(1) = 0, f(2) = 1 + return n - 1 if n == 1 || n == 2 + # f(n) = f(n-1) + f(n-2) を再帰的に呼び出す + res = fib(n - 1) + fib(n - 2) + # 結果 f(n) を返す + res + end ``` -上記のコードを観察すると、それ自体の中で2つの関数を再帰的に呼び出していることがわかります。**つまり、1回の呼び出しで2つの分岐呼び出しが生成されます**。以下の図に示されているように、この継続的な再帰呼び出しは最終的に深さ$n$の再帰木を作成します。 +??? pythontutor "コードの可視化" -![Fibonacci sequence recursion tree](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" } +
+ -

図 2-6   Fibonacci sequence recursion tree

+上のコードを見ると、関数内で 2 回の再帰呼び出しを行っています。**これは 1 回の呼び出しから 2 つの呼び出し分岐が生じることを意味します**。次の図のように、この再帰呼び出しを繰り返していくと、最終的に深さ $n$ の再帰木(recursion tree)が生成されます。 -基本的に、再帰は「問題をより小さなサブ問題に分解する」パラダイムを体現しています。この分割統治戦略は重要です。 +![フィボナッチ数列の再帰木](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" } -- アルゴリズムの観点から、探索、ソート、バックトラッキング、分割統治、動的プログラミングなどの多くの重要な戦略は、直接的または間接的にこの思考方法を使用しています。 -- データ構造の観点から、再帰は連結リスト、木、グラフを扱うのに自然に適しており、これらは分割統治アプローチを使用した分析に適しているためです。 +

図 2-6   フィボナッチ数列の再帰木

-## 2.2.3   比較 +本質的に見ると、再帰は「問題をより小さな部分問題へ分解する」という思考パラダイムを体現しており、この分割統治の戦略は非常に重要です。 -上記の内容をまとめると、以下の表は実装、性能、適用性の観点から反復と再帰の違いを示しています。 +- アルゴリズムの観点では、探索、ソート、バックトラッキング、分割統治、動的計画法など、多くの重要な戦略が直接または間接にこの考え方を用いています。 +- データ構造の観点では、再帰は連結リスト、木、グラフに関する問題の処理に本質的に適しており、これらは分割統治の考え方で分析しやすいからです。 -

表: 反復と再帰の特性の比較

+## 2.2.3   両者の比較 + +以上をまとめると、次の表のように、反復と再帰は実装、性能、適用性の面で違いがあります。 + +

表 2-1   反復と再帰の特徴の比較

-| | 反復 | 再帰 | -| ----------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------- | -| アプローチ | ループ構造 | 関数が自分自身を呼び出す | -| 時間効率 | 一般的により高い効率、関数呼び出しのオーバーヘッドなし | 各関数呼び出しがオーバーヘッドを生成 | -| メモリ使用量 | 通常は固定サイズのメモリ空間を使用 | 累積的な関数呼び出しが大量のスタックフレーム空間を使用する可能性 | -| 適用可能な問題 | 単純なループタスクに適している、直感的で読みやすいコード | 問題の分解に適している(木、グラフ、分割統治、バックトラッキングなど)、簡潔で明確なコード構造 | +| | 反復 | 再帰 | +| -------- | -------------------------------------- | ------------------------------------------------------------ | +| 実装方法 | ループ構造 | 関数が自分自身を呼び出す | +| 時間効率 | 通常は効率が高く、関数呼び出しの負荷がない | 関数呼び出しのたびにオーバーヘッドが発生する | +| メモリ使用 | 通常は固定サイズのメモリ空間を使う | 関数呼び出しの蓄積により大量のスタックフレーム領域を使う可能性がある | +| 適用対象 | 単純な反復処理に適し、コードが直感的で読みやすい | 木、グラフ、分割統治、バックトラッキングなどの部分問題分解に適し、コード構造が簡潔で明快 |
!!! tip - 以下の内容が理解しにくい場合は、「スタック」の章を読んだ後に再び訪れることを検討してください。 + 以下の内容が難しいと感じる場合は、「スタック」の章を読み終えた後に改めて復習してください。 -それでは、反復と再帰の本質的な関連は何でしょうか?上記の再帰関数を例に取ると、合計操作は再帰の「返却」フェーズで発生します。これは、最初に呼び出された関数が最後に合計操作を完了することを意味し、**スタックの「後入れ先出し」原理を反映しています**。 +では、反復と再帰にはどのような内在的な関係があるのでしょうか。前述の再帰関数を例にすると、加算処理は再帰の復帰段階で行われます。これは、最初に呼び出された関数が実際には最後に加算を完了することを意味しており、**この動作の仕組みはスタックの「後入れ先出し」の原則とよく似ています**。 -「呼び出しスタック」や「スタックフレーム空間」などの再帰用語は、再帰とスタックの密接な関係を示しています。 +実際、「呼び出しスタック」や「スタックフレーム領域」といった再帰の用語自体が、再帰とスタックの密接な関係を示唆しています。 -1. **呼び出し**: 関数が呼び出されると、システムは「呼び出しスタック」上にその関数用の新しいスタックフレームを割り当て、ローカル変数、パラメータ、戻りアドレス、その他のデータを格納します。 -2. **返却**: 関数が実行を完了して返ると、対応するスタックフレームが「呼び出しスタック」から削除され、前の関数の実行環境が復元されます。 +1. **再帰呼び出し**:関数が呼び出されると、システムは「呼び出しスタック」上にその関数のための新しいスタックフレームを割り当て、局所変数、引数、返却先アドレスなどのデータを保存します。 +2. **復帰**:関数の実行が完了して戻ると、対応するスタックフレームは「呼び出しスタック」から取り除かれ、前の関数の実行環境が復元されます。 -したがって、**明示的なスタックを使用して呼び出しスタックの動作をシミュレートできます**。これにより再帰を反復形式に変換できます: +したがって、**明示的なスタックを使って呼び出しスタックの振る舞いを模擬することができ**、その結果として再帰を反復形式へ変換できます。 === "Python" ```python title="recursion.py" def for_loop_recur(n: int) -> int: - """反復で再帰をシミュレート""" - # 明示的なスタックを使用してシステムコールスタックをシミュレート + """反復で再帰を模擬する""" + # 明示的なスタックを使ってシステムコールスタックを模擬する stack = [] res = 0 # 再帰:再帰呼び出し for i in range(n, 0, -1): - # 「スタックへのプッシュ」で「再帰」をシミュレート + # 「スタックへのプッシュ」で「再帰」を模擬する stack.append(i) - # 復帰:結果を返す + # 帰りがけ:結果を返す while stack: - # 「スタックからのポップ」で「復帰」をシミュレート + # 「スタックから取り出す操作」で「帰り」をシミュレート res += stack.pop() # res = 1+2+3+...+n return res @@ -929,19 +1653,19 @@ comments: true === "C++" ```cpp title="recursion.cpp" - /* 反復で再帰をシミュレート */ + /* 反復で再帰を模擬する */ int forLoopRecur(int n) { - // 明示的なスタックを使用してシステムコールスタックをシミュレート + // 明示的なスタックを使ってシステムコールスタックを模擬する stack stack; int res = 0; // 再帰:再帰呼び出し for (int i = n; i > 0; i--) { - // 「スタックへのプッシュ」で「再帰」をシミュレート + // 「スタックへのプッシュ」で「再帰」を模擬する stack.push(i); } - // 戻り値:結果を返す + // 帰りがけ:結果を返す while (!stack.empty()) { - // 「スタックからのポップ」で「戻り値」をシミュレート + // 「スタックから取り出す操作」で「帰り」をシミュレート res += stack.top(); stack.pop(); } @@ -953,19 +1677,19 @@ comments: true === "Java" ```java title="recursion.java" - /* 反復で再帰をシミュレート */ + /* 反復で再帰を模擬する */ int forLoopRecur(int n) { - // 明示的なスタックを使用してシステムコールスタックをシミュレート + // 明示的なスタックを使ってシステムコールスタックを模擬する Stack stack = new Stack<>(); int res = 0; // 再帰:再帰呼び出し for (int i = n; i > 0; i--) { - // 「スタックへのプッシュ」で「再帰」をシミュレート + // 「スタックへのプッシュ」で「再帰」を模擬する stack.push(i); } - // 戻り値:結果を返す + // 帰りがけ:結果を返す while (!stack.isEmpty()) { - // 「スタックからのポップ」で「戻り値」をシミュレート + // 「スタックから取り出す操作」で「帰り」をシミュレート res += stack.pop(); } // res = 1+2+3+...+n @@ -976,66 +1700,243 @@ comments: true === "C#" ```csharp title="recursion.cs" - [class]{recursion}-[func]{ForLoopRecur} + /* 反復で再帰を模擬する */ + int ForLoopRecur(int n) { + // 明示的なスタックを使ってシステムコールスタックを模擬する + Stack stack = new(); + int res = 0; + // 再帰:再帰呼び出し + for (int i = n; i > 0; i--) { + // 「スタックへのプッシュ」で「再帰」を模擬する + stack.Push(i); + } + // 帰りがけ:結果を返す + while (stack.Count > 0) { + // 「スタックから取り出す操作」で「帰り」をシミュレート + res += stack.Pop(); + } + // res = 1+2+3+...+n + return res; + } ``` === "Go" ```go title="recursion.go" - [class]{}-[func]{forLoopRecur} + /* 反復で再帰を模擬する */ + func forLoopRecur(n int) int { + // 明示的なスタックを使ってシステムコールスタックを模擬する + stack := list.New() + res := 0 + // 再帰:再帰呼び出し + for i := n; i > 0; i-- { + // 「スタックへのプッシュ」で「再帰」を模擬する + stack.PushBack(i) + } + // 帰りがけ:結果を返す + for stack.Len() != 0 { + // 「スタックから取り出す操作」で「帰り」をシミュレート + res += stack.Back().Value.(int) + stack.Remove(stack.Back()) + } + // res = 1+2+3+...+n + return res + } ``` === "Swift" ```swift title="recursion.swift" - [class]{}-[func]{forLoopRecur} + /* 反復で再帰を模擬する */ + func forLoopRecur(n: Int) -> Int { + // 明示的なスタックを使ってシステムコールスタックを模擬する + var stack: [Int] = [] + var res = 0 + // 再帰:再帰呼び出し + for i in (1 ... n).reversed() { + // 「スタックへのプッシュ」で「再帰」を模擬する + stack.append(i) + } + // 帰りがけ:結果を返す + while !stack.isEmpty { + // 「スタックから取り出す操作」で「帰り」をシミュレート + res += stack.removeLast() + } + // res = 1+2+3+...+n + return res + } ``` === "JS" ```javascript title="recursion.js" - [class]{}-[func]{forLoopRecur} + /* 反復で再帰を模擬する */ + function forLoopRecur(n) { + // 明示的なスタックを使ってシステムコールスタックを模擬する + const stack = []; + let res = 0; + // 再帰:再帰呼び出し + for (let i = n; i > 0; i--) { + // 「スタックへのプッシュ」で「再帰」を模擬する + stack.push(i); + } + // 帰りがけ:結果を返す + while (stack.length) { + // 「スタックから取り出す操作」で「帰り」をシミュレート + res += stack.pop(); + } + // res = 1+2+3+...+n + return res; + } ``` === "TS" ```typescript title="recursion.ts" - [class]{}-[func]{forLoopRecur} + /* 反復で再帰を模擬する */ + function forLoopRecur(n: number): number { + // 明示的なスタックを使ってシステムコールスタックを模擬する + const stack: number[] = []; + let res: number = 0; + // 再帰:再帰呼び出し + for (let i = n; i > 0; i--) { + // 「スタックへのプッシュ」で「再帰」を模擬する + stack.push(i); + } + // 帰りがけ:結果を返す + while (stack.length) { + // 「スタックから取り出す操作」で「帰り」をシミュレート + res += stack.pop(); + } + // res = 1+2+3+...+n + return res; + } ``` === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{forLoopRecur} + /* 反復で再帰を模擬する */ + int forLoopRecur(int n) { + // 明示的なスタックを使ってシステムコールスタックを模擬する + List stack = []; + int res = 0; + // 再帰:再帰呼び出し + for (int i = n; i > 0; i--) { + // 「スタックへのプッシュ」で「再帰」を模擬する + stack.add(i); + } + // 帰りがけ:結果を返す + while (!stack.isEmpty) { + // 「スタックから取り出す操作」で「帰り」をシミュレート + res += stack.removeLast(); + } + // res = 1+2+3+...+n + return res; + } ``` === "Rust" ```rust title="recursion.rs" - [class]{}-[func]{for_loop_recur} + /* 反復で再帰を模擬する */ + fn for_loop_recur(n: i32) -> i32 { + // 明示的なスタックを使ってシステムコールスタックを模擬する + let mut stack = Vec::new(); + let mut res = 0; + // 再帰:再帰呼び出し + for i in (1..=n).rev() { + // 「スタックへのプッシュ」で「再帰」を模擬する + stack.push(i); + } + // 帰りがけ:結果を返す + while !stack.is_empty() { + // 「スタックから取り出す操作」で「帰り」をシミュレート + res += stack.pop().unwrap(); + } + // res = 1+2+3+...+n + res + } ``` === "C" ```c title="recursion.c" - [class]{}-[func]{forLoopRecur} + /* 反復で再帰を模擬する */ + int forLoopRecur(int n) { + int stack[1000]; // 大きな配列を使ってスタックを実装する + int top = -1; // スタックトップのインデックス + int res = 0; + // 再帰:再帰呼び出し + for (int i = n; i > 0; i--) { + // 「スタックへのプッシュ」で「再帰」を模擬する + stack[1 + top++] = i; + } + // 帰りがけ:結果を返す + while (top >= 0) { + // 「スタックから取り出す操作」で「帰り」をシミュレート + res += stack[top--]; + } + // res = 1+2+3+...+n + return res; + } ``` === "Kotlin" ```kotlin title="recursion.kt" - [class]{}-[func]{forLoopRecur} + /* 反復で再帰を模擬する */ + fun forLoopRecur(n: Int): Int { + // 明示的なスタックを使ってシステムコールスタックを模擬する + val stack = Stack() + var res = 0 + // 再帰: 再帰呼び出し + for (i in n downTo 0) { + // 「スタックへのプッシュ」で「再帰」を模擬する + stack.push(i) + } + // 戻る: 結果を返す + while (stack.isNotEmpty()) { + // 「スタックから取り出す操作」で「帰り」をシミュレート + res += stack.pop() + } + // res = 1+2+3+...+n + return res + } ``` === "Ruby" ```ruby title="recursion.rb" - [class]{}-[func]{for_loop_recur} + ### 反復で再帰をシミュレート ### + def for_loop_recur(n) + # 明示的なスタックを使ってシステムコールスタックを模擬する + stack = [] + res = 0 + + # 再帰:再帰呼び出し + for i in n.downto(0) + # 「スタックへのプッシュ」で「再帰」を模擬する + stack << i + end + # 帰りがけ:結果を返す + while !stack.empty? + res += stack.pop + end + + # res = 1+2+3+...+n + res + end ``` -上記のコードを観察すると、再帰が反復に変換されたとき、コードはより複雑になります。反復と再帰はしばしば相互に変換できますが、2つの理由でそうすることが常に推奨されるわけではありません: +??? pythontutor "コードの可視化" -- 変換されたコードは理解がより困難になり、読みにくくなる可能性があります。 -- 一部の複雑な問題では、システムの呼び出しスタックの動作をシミュレートすることは非常に困難です。 +
+ -結論として、**反復または再帰を選択するかは問題の具体的な性質によります**。プログラミングの実践では、両方の長所と短所を比較検討し、手元の状況に最も適したアプローチを選択することが重要です。 +上のコードを見ると、再帰を反復へ変換すると、コードはより複雑になります。反復と再帰は多くの場合に相互変換できますが、常にそうする価値があるとは限りません。理由は次の 2 点です。 + +- 変換後のコードは理解しにくくなり、可読性が下がる可能性があります。 +- 複雑な問題によっては、システムの呼び出しスタックの振る舞いを模擬すること自体が非常に難しい場合があります。 + +要するに、**反復を選ぶか再帰を選ぶかは、対象となる問題の性質によって決まります**。実際のプログラミングでは、両者の長所と短所を見極め、状況に応じて適切な方法を選ぶことが重要です。 diff --git a/ja/docs/chapter_computational_complexity/performance_evaluation.md b/ja/docs/chapter_computational_complexity/performance_evaluation.md index 65716ad15..0cd095132 100644 --- a/ja/docs/chapter_computational_complexity/performance_evaluation.md +++ b/ja/docs/chapter_computational_complexity/performance_evaluation.md @@ -2,52 +2,52 @@ comments: true --- -# 2.1   アルゴリズムの効率評価 +# 2.1   アルゴリズム効率の評価 -アルゴリズム設計において、私たちは順序に従って以下の2つの目標を追求します。 +アルゴリズム設計では、次の 2 つのレベルの目標を順に追求します。 -1. **問題の解決策を見つける**: アルゴリズムは、指定された入力範囲内で確実に正しい解を見つけることができるべきです。 -2. **最適解を求める**: 同じ問題に対して複数の解決策が存在する場合があり、私たちは可能な限り最も効率的なアルゴリズムを見つけることを目指します。 +1. **問題の解法を見つける**:アルゴリズムは、定められた入力範囲内で問題の正しい解を確実に求められる必要があります。 +2. **最適な解法を追求する**:同じ問題に対して複数の解法が存在する場合があり、私たちはできるだけ効率的なアルゴリズムを見つけたいと考えます。 -つまり、問題を解決できることを前提として、アルゴリズムの効率がアルゴリズムを評価する主要な基準となっており、これには以下の2つの次元が含まれます。 +つまり、問題を解けることを前提として、アルゴリズム効率はその良し悪しを測る主要な評価指標となっており、次の 2 つの観点を含みます。 -- **時間効率**: アルゴリズムが実行される速度。 -- **空間効率**: アルゴリズムが占有するメモリ空間のサイズ。 +- **時間効率**:アルゴリズムの実行時間の長さ。 +- **空間効率**:アルゴリズムが使用するメモリ空間の大きさ。 -要するに、**私たちの目標は、高速でメモリ効率の良いデータ構造とアルゴリズムを設計することです**。アルゴリズムの効率を効果的に評価することは重要です。なぜなら、そうすることで初めて様々なアルゴリズムを比較し、アルゴリズムの設計と最適化プロセスを導くことができるからです。 +簡単に言えば、**私たちの目標は「高速で省メモリ」なデータ構造とアルゴリズムを設計すること**です。そして、アルゴリズム効率を効果的に評価することは非常に重要です。そうすることで初めて、さまざまなアルゴリズムを比較し、さらにアルゴリズム設計と最適化の過程を導けるからです。 -効率評価には主に2つの方法があります:実際のテストと理論的推定です。 +効率の評価方法は主に 2 種類に分けられます。実測と理論的な見積もりです。 -## 2.1.1   実際のテスト +## 2.1.1   実測 -アルゴリズム`A`と`B`があり、どちらも同じ問題を解決でき、それらの効率を比較する必要があるとします。最も直接的な方法は、コンピュータを使用してこれら2つのアルゴリズムを実行し、実行時間とメモリ使用量を監視・記録することです。この評価方法は実際の状況を反映しますが、大きな制限があります。 +いまアルゴリズム `A` とアルゴリズム `B` があり、どちらも同じ問題を解けるとします。この 2 つのアルゴリズムの効率を比較する必要がある場合、最も直接的な方法は 1 台のコンピュータで両者を実行し、その実行時間とメモリ使用量を監視して記録することです。この評価方法は実際の状況を反映できますが、大きな制約もあります。 -一方で、**テスト環境からの干渉を排除することは困難です**。ハードウェア構成はアルゴリズムの性能に影響を与える可能性があります。例えば、並列度の高いアルゴリズムはマルチコアCPUでの実行により適していますし、集約的なメモリ操作を含むアルゴリズムは高性能メモリでより良い性能を発揮します。アルゴリズムのテスト結果は、異なるマシン間で変わる可能性があります。これは、平均効率を計算するために複数のマシンでテストすることが実用的でないことを意味します。 +一方では、**テスト環境による干渉要因を排除しにくい**という問題があります。ハードウェア構成はアルゴリズムの性能に影響します。たとえば、並列度の高いアルゴリズムはマルチコア CPU での実行により適しており、メモリアクセスが集中的なアルゴリズムは高性能メモリ上でより良い性能を示します。つまり、異なるマシンでのテスト結果は一致しない可能性があります。これは、さまざまなマシンでテストして平均効率を統計的に求める必要があることを意味しますが、それは現実的ではありません。 -一方で、**完全なテストを実施することは非常にリソース集約的です**。アルゴリズムの効率は入力データサイズによって変わります。例えば、データ量が少ない場合はアルゴリズム`A`が`B`より速く実行される可能性がありますが、データ量が多い場合はテスト結果が逆になる可能性があります。したがって、説得力のある結論を導くためには、幅広い入力データサイズをテストする必要があり、これには過度な計算リソースが必要になります。 +他方では、**完全なテストを実施するには非常に多くの資源が必要**です。入力データ量が変化すると、アルゴリズムは異なる効率を示します。たとえば、入力データ量が小さいときはアルゴリズム `A` の実行時間がアルゴリズム `B` より短くても、入力データ量が大きいときには結果がちょうど逆になるかもしれません。そのため、説得力のある結論を得るには、さまざまな規模の入力データでテストする必要があり、それには大量の計算資源を要します。 -## 2.1.2   理論的推定 +## 2.1.2   理論的な見積もり -実際のテストの大きな制限により、計算のみでアルゴリズムの効率を評価することを検討できます。この推定方法は漸近的複雑度解析、または単に複雑度解析として知られています。 +実測には大きな制約があるため、いくつかの計算だけによってアルゴリズムの効率を評価することを考えられます。この見積もり方法は漸近計算量解析(asymptotic complexity analysis)と呼ばれ、略して計算量解析といいます。 -複雑度解析は、アルゴリズムの実行に必要な時間と空間リソースと入力データのサイズとの関係を反映します。**これは、入力データのサイズが増加するにつれて、アルゴリズムに必要な時間と空間の増加傾向を記述します**。この定義は複雑に聞こえるかもしれませんが、より良く理解するために3つの重要なポイントに分解できます。 +計算量解析は、アルゴリズムの実行に必要な時間資源と空間資源が入力データ規模とどのような関係にあるかを表します。**これは、入力データ規模が増加するにつれて、アルゴリズムの実行に必要な時間と空間がどのように増加するかという傾向を記述するものです**。この定義はややわかりにくいので、次の 3 つのポイントに分けて理解できます。 -- 「時間と空間リソース」は、それぞれ時間計算量空間計算量に対応します。 -- 「入力データのサイズが増加するにつれて」は、複雑度がアルゴリズムの効率と入力データ量との関係を反映することを意味します。 -- 「時間と空間の増加傾向」は、複雑度解析が実行時間や占有空間の具体的な値ではなく、時間や空間が増加する「率」に焦点を当てることを示します。 +- 「時間資源と空間資源」は、それぞれ時間計算量(time complexity)空間計算量(space complexity)に対応します。 +- 「入力データ規模が増加するにつれて」とは、計算量がアルゴリズムの実行効率と入力データ規模との関係を反映していることを意味します。 +- 「時間と空間の増加傾向」とは、計算量解析が注目するのは実行時間や使用空間の具体的な値ではなく、時間や空間の増加の「速さ」であることを示します。 -**複雑度解析は実際のテスト方法の欠点を克服します**。これは以下の側面で反映されます: +**計算量解析は実測という方法の欠点を克服しています**。その点は次のように表れます。 -- 実際にコードを実行する必要がないため、より環境に優しく、エネルギー効率が良いです。 -- テスト環境に依存せず、すべての動作プラットフォームに適用できます。 -- 異なるデータ量でのアルゴリズムの効率を反映でき、特に大量データでのアルゴリズムの性能を示します。 +- 実際にコードを動かす必要がなく、より環境にやさしく省エネルギーです。 +- テスト環境から独立しており、解析結果はすべての実行プラットフォームに適用できます。 +- 異なるデータ量におけるアルゴリズム効率を表せ、とくに大規模データ量での性能を反映できます。 !!! tip - 複雑度の概念についてまだ混乱している場合でも、心配しないでください。以降の章で詳しく取り上げます。 + それでも計算量の概念がまだわかりにくくても、心配はいりません。後続の章で詳しく説明します。 -複雑度解析は、アルゴリズムの効率を評価する「ものさし」を提供し、実行に必要な時間と空間リソースを測定し、異なるアルゴリズムの効率を比較することを可能にします。 +計算量解析は、アルゴリズム効率を評価するための「物差し」を私たちに与えてくれます。これにより、あるアルゴリズムの実行に必要な時間資源と空間資源を測り、異なるアルゴリズム同士の効率を比較できます。 -複雑度は数学的概念であり、初心者には抽象的で困難かもしれません。この観点から、複雑度解析は最初に紹介するのに最も適したトピックではないかもしれません。しかし、特定のデータ構造やアルゴリズムの特性について議論するとき、その速度と空間使用量を分析することを避けるのは困難です。 +計算量は数学的な概念であり、初学者にとってはやや抽象的で、学習の難度も比較的高いかもしれません。この観点から見ると、計算量解析は最初に紹介する内容としてはあまり適していない可能性があります。しかし、あるデータ構造やアルゴリズムの特徴を議論する際には、その実行速度や空間使用状況の分析を避けることはできません。 -要約すると、データ構造とアルゴリズムに深く入る前に複雑度解析の基本的な理解を身につけることをお勧めします。**これにより、簡単なアルゴリズムで複雑度解析を実行できるようになります**。 +以上を踏まえると、データ構造とアルゴリズムを深く学ぶ前に、**まず計算量解析について初歩的な理解を持ち、簡単なアルゴリズムの計算量解析ができるようにしておくこと**を勧めます。 diff --git a/ja/docs/chapter_computational_complexity/space_complexity.md b/ja/docs/chapter_computational_complexity/space_complexity.md index 1db205645..4fcfa2605 100644 --- a/ja/docs/chapter_computational_complexity/space_complexity.md +++ b/ja/docs/chapter_computational_complexity/space_complexity.md @@ -4,31 +4,31 @@ comments: true # 2.4   空間計算量 -空間計算量は、データ量が増加するにつれてアルゴリズムが占有するメモリ空間の増加傾向を測定するために使用されます。この概念は時間計算量と非常に似ていますが、「実行時間」が「占有メモリ空間」に置き換えられています。 +空間計算量(space complexity)は、アルゴリズムが占有するメモリ空間がデータ量の増加に伴ってどのように増えるかを測る指標です。この概念は時間計算量と非常によく似ており、「実行時間」を「占有メモリ空間」に置き換えるだけです。 ## 2.4.1   アルゴリズムに関連する空間 -アルゴリズムが実行中に使用するメモリ空間には、主に以下の種類があります。 +アルゴリズムが実行中に使用するメモリ空間には、主に次の種類があります。 -- **入力空間**: アルゴリズムの入力データを格納するために使用されます。 -- **一時空間**: アルゴリズムの実行中に変数、オブジェクト、関数コンテキスト、その他のデータを格納するために使用されます。 -- **出力空間**: アルゴリズムの出力データを格納するために使用されます。 +- **入力空間**:アルゴリズムの入力データを格納するための空間。 +- **一時空間**:アルゴリズムの実行中に使用する変数、オブジェクト、関数コンテキストなどのデータを格納するための空間。 +- **出力空間**:アルゴリズムの出力データを格納するための空間。 -一般的に、空間計算量の統計範囲には「一時空間」と「出力空間」の両方が含まれます。 +一般に、空間計算量の集計範囲は「一時空間」と「出力空間」を合わせたものです。 -一時空間はさらに3つの部分に分けることができます。 +一時空間はさらに三つに分けられます。 -- **一時データ**: アルゴリズムの実行中に様々な定数、変数、オブジェクトなどを保存するために使用されます。 -- **スタックフレーム空間**: 呼び出された関数のコンテキストデータを保存するために使用されます。システムは関数が呼び出されるたびにスタックの頂上にスタックフレームを作成し、関数が返された後にスタックフレーム空間を解放します。 -- **命令空間**: コンパイル済みプログラム命令を格納するために使用され、実際の統計では通常無視できます。 +- **一時データ**:アルゴリズム実行中の各種定数、変数、オブジェクトなどを保存するための空間。 +- **スタックフレーム空間**:呼び出された関数のコンテキストデータを保存するための空間。システムは関数を呼び出すたびにスタックの先頭にスタックフレームを作成し、関数が戻るとその空間を解放します。 +- **命令空間**:コンパイル後のプログラム命令を保存するための空間で、実際の集計では通常無視されます。 -プログラムの空間計算量を分析する際、**通常は一時データ、スタックフレーム空間、出力データをカウントします**。以下の図に示されています。 +プログラムの空間計算量を分析する際には、**通常、一時データ、スタックフレーム空間、出力データの三つを数えます**。以下の図に示すとおりです。 -![Space types used in algorithms](space_complexity.assets/space_types.png){ class="animation-figure" } +![アルゴリズムで使用される関連空間](space_complexity.assets/space_types.png){ class="animation-figure" } -

図 2-15   Space types used in algorithms

+

図 2-15   アルゴリズムで使用される関連空間

-関連するコードは以下の通りです: +関連するコードを以下に示します。 === "Python" @@ -36,20 +36,20 @@ comments: true class Node: """クラス""" def __init__(self, x: int): - self.val: int = x # ノード値 - self.next: Node | None = None # 次のノードへの参照 + self.val: int = x # ノードの値 + self.next: Node | None = None # 次のノードへの参照 def function() -> int: """関数""" - # 特定の操作を実行... + # いくつかの処理を実行... return 0 - def algorithm(n) -> int: # 入力データ - A = 0 # 一時データ(定数、通常大文字) - b = 0 # 一時データ(変数) - node = Node(0) # 一時データ(オブジェクト) - c = function() # スタックフレーム空間(関数呼び出し) - return A + b + c # 出力データ + def algorithm(n) -> int: # 入力データ + A = 0 # 一時データ(定数。一般に大文字で表す) + b = 0 # 一時データ(変数) + node = Node(0) # 一時データ(オブジェクト) + c = function() # スタックフレーム空間(関数呼び出し) + return A + b + c # 出力データ ``` === "C++" @@ -64,16 +64,16 @@ comments: true /* 関数 */ int func() { - // 特定の操作を実行... + // いくつかの処理を実行... return 0; } - int algorithm(int n) { // 入力データ - const int a = 0; // 一時データ(定数) - int b = 0; // 一時データ(変数) - Node* node = new Node(0); // 一時データ(オブジェクト) - int c = func(); // スタックフレーム空間(関数呼び出し) - return a + b + c; // 出力データ + int algorithm(int n) { // 入力データ + const int a = 0; // 一時データ(定数) + int b = 0; // 一時データ(変数) + Node* node = new Node(0); // 一時データ(オブジェクト) + int c = func(); // スタックフレーム空間(関数呼び出し) + return a + b + c; // 出力データ } ``` @@ -86,19 +86,19 @@ comments: true Node next; Node(int x) { val = x; } } - + /* 関数 */ int function() { - // 特定の操作を実行... + // いくつかの処理を実行... return 0; } - - int algorithm(int n) { // 入力データ - final int a = 0; // 一時データ(定数) - int b = 0; // 一時データ(変数) - Node node = new Node(0); // 一時データ(オブジェクト) - int c = function(); // スタックフレーム空間(関数呼び出し) - return a + b + c; // 出力データ + + int algorithm(int n) { // 入力データ + final int a = 0; // 一時データ(定数) + int b = 0; // 一時データ(変数) + Node node = new Node(0); // 一時データ(オブジェクト) + int c = function(); // スタックフレーム空間(関数呼び出し) + return a + b + c; // 出力データ } ``` @@ -106,24 +106,23 @@ comments: true ```csharp title="" /* クラス */ - class Node { - int val; + class Node(int x) { + int val = x; Node next; - Node(int x) { val = x; } } /* 関数 */ int Function() { - // 特定の操作を実行... + // いくつかの処理を実行... return 0; } - int Algorithm(int n) { // 入力データ - const int a = 0; // 一時データ(定数) - int b = 0; // 一時データ(変数) - Node node = new(0); // 一時データ(オブジェクト) - int c = Function(); // スタックフレーム空間(関数呼び出し) - return a + b + c; // 出力データ + int Algorithm(int n) { // 入力データ + const int a = 0; // 一時データ(定数) + int b = 0; // 一時データ(変数) + Node node = new(0); // 一時データ(オブジェクト) + int c = Function(); // スタックフレーム空間(関数呼び出し) + return a + b + c; // 出力データ } ``` @@ -136,14 +135,14 @@ comments: true next *node } - /* ノード構造体を作成 */ + /* node 構造体を作成 */ func newNode(val int) *node { return &node{val: val} } - + /* 関数 */ func function() int { - // 特定の操作を実行... + // いくつかの処理を実行... return 0 } @@ -171,16 +170,16 @@ comments: true /* 関数 */ func function() -> Int { - // 特定の操作を実行... + // いくつかの処理を実行... return 0 } func algorithm(n: Int) -> Int { // 入力データ - let a = 0 // 一時データ(定数) - var b = 0 // 一時データ(変数) - let node = Node(x: 0) // 一時データ(オブジェクト) - let c = function() // スタックフレーム空間(関数呼び出し) - return a + b + c // 出力データ + let a = 0 // 一時データ(定数) + var b = 0 // 一時データ(変数) + let node = Node(x: 0) // 一時データ(オブジェクト) + let c = function() // スタックフレーム空間(関数呼び出し) + return a + b + c // 出力データ } ``` @@ -192,23 +191,23 @@ comments: true val; next; constructor(val) { - this.val = val === undefined ? 0 : val; // ノード値 + this.val = val === undefined ? 0 : val; // ノードの値 this.next = null; // 次のノードへの参照 } } /* 関数 */ function constFunc() { - // 特定の操作を実行 + // いくつかの処理を実行 return 0; } - function algorithm(n) { // 入力データ - const a = 0; // 一時データ(定数) - let b = 0; // 一時データ(変数) - const node = new Node(0); // 一時データ(オブジェクト) - const c = constFunc(); // スタックフレーム空間(関数呼び出し) - return a + b + c; // 出力データ + function algorithm(n) { // 入力データ + const a = 0; // 一時データ(定数) + let b = 0; // 一時データ(変数) + const node = new Node(0); // 一時データ(オブジェクト) + const c = constFunc(); // スタックフレーム空間(関数呼び出し) + return a + b + c; // 出力データ } ``` @@ -220,14 +219,14 @@ comments: true val: number; next: Node | null; constructor(val?: number) { - this.val = val === undefined ? 0 : val; // ノード値 + this.val = val === undefined ? 0 : val; // ノードの値 this.next = null; // 次のノードへの参照 } } /* 関数 */ function constFunc(): number { - // 特定の操作を実行 + // いくつかの処理を実行 return 0; } @@ -252,7 +251,7 @@ comments: true /* 関数 */ int function() { - // 特定の操作を実行... + // いくつかの処理を実行... return 0; } @@ -270,14 +269,14 @@ comments: true ```rust title="" use std::rc::Rc; use std::cell::RefCell; - + /* 構造体 */ struct Node { val: i32, next: Option>>, } - /* コンストラクタ */ + /* Node 構造体を作成 */ impl Node { fn new(val: i32) -> Self { Self { val: val, next: None } @@ -285,17 +284,17 @@ comments: true } /* 関数 */ - fn function() -> i32 { - // 特定の操作を実行... + fn function() -> i32 { + // いくつかの処理を実行... return 0; } - fn algorithm(n: i32) -> i32 { // 入力データ - const a: i32 = 0; // 一時データ(定数) - let mut b = 0; // 一時データ(変数) - let node = Node::new(0); // 一時データ(オブジェクト) - let c = function(); // スタックフレーム空間(関数呼び出し) - return a + b + c; // 出力データ + fn algorithm(n: i32) -> i32 { // 入力データ + const a: i32 = 0; // 一時データ(定数) + let mut b = 0; // 一時データ(変数) + let node = Node::new(0); // 一時データ(オブジェクト) + let c = function(); // スタックフレーム空間(関数呼び出し) + return a + b + c; // 出力データ } ``` @@ -304,34 +303,80 @@ comments: true ```c title="" /* 関数 */ int func() { - // 特定の操作を実行... + // いくつかの処理を実行... return 0; } - int algorithm(int n) { // 入力データ - const int a = 0; // 一時データ(定数) - int b = 0; // 一時データ(変数) - int c = func(); // スタックフレーム空間(関数呼び出し) - return a + b + c; // 出力データ + int algorithm(int n) { // 入力データ + const int a = 0; // 一時データ(定数) + int b = 0; // 一時データ(変数) + int c = func(); // スタックフレーム空間(関数呼び出し) + return a + b + c; // 出力データ } ``` === "Kotlin" ```kotlin title="" + /* クラス */ + class Node(var _val: Int) { + var next: Node? = null + } + /* 関数 */ + fun function(): Int { + // いくつかの処理を実行... + return 0 + } + + fun algorithm(n: Int): Int { // 入力データ + val a = 0 // 一時データ(定数) + var b = 0 // 一時データ(変数) + val node = Node(0) // 一時データ(オブジェクト) + val c = function() // スタックフレーム空間(関数呼び出し) + return a + b + c // 出力データ + } ``` -## 2.4.2   計算方法 +=== "Ruby" -空間計算量を計算する方法は時間計算量とほぼ同様で、統計対象を「操作数」から「使用空間のサイズ」に変更するだけです。 + ```ruby title="" + ### クラス ### + class Node + attr_accessor :val # ノードの値 + attr_accessor :next # 次のノードへの参照 -しかし、時間計算量とは異なり、**通常は最悪ケース空間計算量のみに焦点を当てます**。これは、メモリ空間がハード要件であり、すべての入力データの下で十分なメモリ空間が確保されていることを保証する必要があるためです。 + def initialize(x) + @val = x + end + end -以下のコードを考えてみましょう。最悪ケース空間計算量の「最悪ケース」という用語には2つの意味があります。 + ### 関数 ### + def function + # いくつかの処理を実行... + 0 + end -1. **最悪の入力データに基づく**: $n < 10$の場合、空間計算量は$O(1)$ですが、$n > 10$の場合、初期化された配列`nums`が$O(n)$の空間を占有するため、最悪ケース空間計算量は$O(n)$です。 -2. **アルゴリズムの実行中に使用されるピークメモリに基づく**: 例えば、最後の行を実行する前、プログラムは$O(1)$の空間を占有します。配列`nums`を初期化する際、プログラムは$O(n)$の空間を占有するため、最悪ケース空間計算量は$O(n)$です。 + ### アルゴリズム ### + def algorithm(n) # 入力データ + a = 0 # 一時データ(定数) + b = 0 # 一時データ(変数) + node = Node.new(0) # 一時データ(オブジェクト) + c = function # スタックフレーム空間(関数呼び出し) + a + b + c # 出力データ + end + ``` + +## 2.4.2   推定方法 + +空間計算量の推定方法は時間計算量とおおむね同じで、数える対象を「操作回数」から「使用空間の大きさ」に変えるだけです。 + +ただし時間計算量と異なり、**通常は最悪空間計算量だけに注目します**。メモリ空間は厳格な要件であり、どの入力データに対しても十分なメモリを確保できることを保証しなければならないからです。 + +以下のコードを見ると、最悪空間計算量における「最悪」には二つの意味があります。 + +1. **最悪の入力データを基準にする**:$n < 10$ のとき空間計算量は $O(1)$ ですが、$n > 10$ のとき初期化される配列 `nums` が $O(n)$ の空間を占有するため、最悪空間計算量は $O(n)$ です。 +2. **アルゴリズム実行中のメモリ使用量のピークを基準にする**:例えば、プログラムは最後の行を実行する前までは $O(1)$ の空間しか使いませんが、配列 `nums` を初期化するときには $O(n)$ の空間を占有するため、最悪空間計算量は $O(n)$ です。 === "Python" @@ -443,10 +488,10 @@ comments: true ```rust title="" fn algorithm(n: i32) { - let a = 0; // O(1) - let b = [0; 10000]; // O(1) + let a = 0; // O(1) + let b = [0; 10000]; // O(1) if n > 10 { - let nums = vec![0; n as usize]; // O(n) + let nums = vec![0; n as usize]; // O(n) } } ``` @@ -465,25 +510,41 @@ comments: true === "Kotlin" ```kotlin title="" - + fun algorithm(n: Int) { + val a = 0 // O(1) + val b = IntArray(10000) // O(1) + if (n > 10) { + val nums = IntArray(n) // O(n) + } + } ``` -**再帰関数では、スタックフレーム空間を考慮に入れる必要があります**。以下のコードを考えてみましょう: +=== "Ruby" + + ```ruby title="" + def algorithm(n) + a = 0 # O(1) + b = Array.new(10000) # O(1) + nums = Array.new(n) if n > 10 # O(n) + end + ``` + +**再帰関数では、スタックフレーム空間の集計に注意が必要です**。以下のコードを見てみましょう。 === "Python" ```python title="" def function() -> int: - # 特定の操作を実行 + # いくつかの処理を実行 return 0 def loop(n: int): - """ループ O(1)""" + """ループの空間計算量は O(1)""" for _ in range(n): function() def recur(n: int): - """再帰 O(n)""" + """再帰の空間計算量は O(n)""" if n == 1: return return recur(n - 1) @@ -493,16 +554,16 @@ comments: true ```cpp title="" int func() { - // 特定の操作を実行 + // いくつかの処理を実行 return 0; } - /* サイクル O(1) */ + /* ループの空間計算量は O(1) */ void loop(int n) { for (int i = 0; i < n; i++) { func(); } } - /* 再帰 O(n) */ + /* 再帰の空間計算量は O(n) */ void recur(int n) { if (n == 1) return; recur(n - 1); @@ -513,16 +574,16 @@ comments: true ```java title="" int function() { - // 特定の操作を実行 + // いくつかの処理を実行 return 0; } - /* サイクル O(1) */ + /* ループの空間計算量は O(1) */ void loop(int n) { for (int i = 0; i < n; i++) { function(); } } - /* 再帰 O(n) */ + /* 再帰の空間計算量は O(n) */ void recur(int n) { if (n == 1) return; recur(n - 1); @@ -533,16 +594,16 @@ comments: true ```csharp title="" int Function() { - // 特定の操作を実行 + // いくつかの処理を実行 return 0; } - /* サイクル O(1) */ + /* ループの空間計算量は O(1) */ void Loop(int n) { for (int i = 0; i < n; i++) { Function(); } } - /* 再帰 O(n) */ + /* 再帰の空間計算量は O(n) */ int Recur(int n) { if (n == 1) return 1; return Recur(n - 1); @@ -553,18 +614,18 @@ comments: true ```go title="" func function() int { - // 特定の操作を実行 + // いくつかの処理を実行 return 0 } - - /* サイクル O(1) */ + + /* ループの空間計算量は O(1) */ func loop(n int) { for i := 0; i < n; i++ { function() } } - - /* 再帰 O(n) */ + + /* 再帰の空間計算量は O(n) */ func recur(n int) { if n == 1 { return @@ -578,18 +639,18 @@ comments: true ```swift title="" @discardableResult func function() -> Int { - // 特定の操作を実行 + // いくつかの処理を実行 return 0 } - /* サイクル O(1) */ + /* ループの空間計算量は O(1) */ func loop(n: Int) { for _ in 0 ..< n { function() } } - /* 再帰 O(n) */ + /* 再帰の空間計算量は O(n) */ func recur(n: Int) { if n == 1 { return @@ -602,16 +663,16 @@ comments: true ```javascript title="" function constFunc() { - // 特定の操作を実行 + // いくつかの処理を実行 return 0; } - /* サイクル O(1) */ + /* ループの空間計算量は O(1) */ function loop(n) { for (let i = 0; i < n; i++) { constFunc(); } } - /* 再帰 O(n) */ + /* 再帰の空間計算量は O(n) */ function recur(n) { if (n === 1) return; return recur(n - 1); @@ -622,16 +683,16 @@ comments: true ```typescript title="" function constFunc(): number { - // 特定の操作を実行 + // いくつかの処理を実行 return 0; } - /* サイクル O(1) */ + /* ループの空間計算量は O(1) */ function loop(n: number): void { for (let i = 0; i < n; i++) { constFunc(); } } - /* 再帰 O(n) */ + /* 再帰の空間計算量は O(n) */ function recur(n: number): void { if (n === 1) return; return recur(n - 1); @@ -642,16 +703,16 @@ comments: true ```dart title="" int function() { - // 特定の操作を実行 + // いくつかの処理を実行 return 0; } - /* サイクル O(1) */ + /* ループの空間計算量は O(1) */ void loop(int n) { for (int i = 0; i < n; i++) { function(); } } - /* 再帰 O(n) */ + /* 再帰の空間計算量は O(n) */ void recur(int n) { if (n == 1) return; recur(n - 1); @@ -662,17 +723,17 @@ comments: true ```rust title="" fn function() -> i32 { - // 特定の操作を実行 + // いくつかの処理を実行 return 0; } - /* サイクル O(1) */ + /* ループの空間計算量は O(1) */ fn loop(n: i32) { for i in 0..n { function(); } } - /* 再帰 O(n) */ - void recur(n: i32) { + /* 再帰の空間計算量は O(n) */ + fn recur(n: i32) { if n == 1 { return; } @@ -684,16 +745,16 @@ comments: true ```c title="" int func() { - // 特定の操作を実行 + // いくつかの処理を実行 return 0; } - /* サイクル O(1) */ + /* ループの空間計算量は O(1) */ void loop(int n) { for (int i = 0; i < n; i++) { func(); } } - /* 再帰 O(n) */ + /* 再帰の空間計算量は O(n) */ void recur(int n) { if (n == 1) return; recur(n - 1); @@ -703,53 +764,87 @@ comments: true === "Kotlin" ```kotlin title="" - + fun function(): Int { + // いくつかの処理を実行 + return 0 + } + /* ループの空間計算量は O(1) */ + fun loop(n: Int) { + for (i in 0.. 図 2-16   Common types of space complexity

+

図 2-16   よくある空間計算量の型

-### 1.   定数オーダー $O(1)$ {data-toc-label="1.   定数オーダー"} +### 1.   定数階 $O(1)$ {data-toc-label="1.   定数階"} -定数オーダーは、入力データサイズ$n$とは無関係な定数、変数、オブジェクトで一般的です。 +定数階は、個数が入力データサイズ $n$ に依存しない定数、変数、オブジェクトなどによく現れます。 -ループで変数を初期化したり関数を呼び出したりするために占有されるメモリは、次のサイクルに入る際に解放され、空間上で累積されないため、空間計算量は$O(1)$のままです: +注意すべき点として、ループ内で変数を初期化したり関数を呼び出したりして使用されたメモリは、次の反復に入ると解放されるため、空間の占有は累積せず、空間計算量は依然として $O(1)$ です。 === "Python" ```python title="space_complexity.py" def function() -> int: """関数""" - # 何らかの操作を実行 + # 何らかの処理を行う return 0 def constant(n: int): - """定数複雑度""" - # 定数、変数、オブジェクトは O(1) のスペースを占有 + """定数階""" + # 定数、変数、オブジェクトは O(1) の空間を占める a = 0 nums = [0] * 10000 node = ListNode(0) - # ループ内の変数は O(1) のスペースを占有 + # ループ内の変数は O(1) の空間を占める for _ in range(n): c = 0 - # ループ内の関数は O(1) のスペースを占有 + # ループ内の関数は O(1) の空間を占める for _ in range(n): function() ``` @@ -759,22 +854,22 @@ $$ ```cpp title="space_complexity.cpp" /* 関数 */ int func() { - // 何らかの操作を実行 + // 何らかの処理を行う return 0; } - /* 定数計算量 */ + /* 定数階 */ void constant(int n) { - // 定数、変数、オブジェクトは O(1) 空間を占める + // 定数、変数、オブジェクトは O(1) の空間を占める const int a = 0; int b = 0; vector nums(10000); ListNode node(0); - // ループ内の変数は O(1) 空間を占める + // ループ内の変数は O(1) の空間を占める for (int i = 0; i < n; i++) { int c = 0; } - // ループ内の関数は O(1) 空間を占める + // ループ内の関数は O(1) の空間を占める for (int i = 0; i < n; i++) { func(); } @@ -786,22 +881,22 @@ $$ ```java title="space_complexity.java" /* 関数 */ int function() { - // 何らかの操作を実行 + // 何らかの処理を行う return 0; } - /* 定数計算量 */ + /* 定数階 */ void constant(int n) { - // 定数、変数、オブジェクトは O(1) 空間を占める + // 定数、変数、オブジェクトは O(1) の空間を占める final int a = 0; int b = 0; int[] nums = new int[10000]; ListNode node = new ListNode(0); - // ループ内の変数は O(1) 空間を占める + // ループ内の変数は O(1) の空間を占める for (int i = 0; i < n; i++) { int c = 0; } - // ループ内の関数は O(1) 空間を占める + // ループ内の関数は O(1) の空間を占める for (int i = 0; i < n; i++) { function(); } @@ -811,95 +906,294 @@ $$ === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{Function} + /* 関数 */ + int Function() { + // 何らかの処理を行う + return 0; + } - [class]{space_complexity}-[func]{Constant} + /* 定数階 */ + void Constant(int n) { + // 定数、変数、オブジェクトは O(1) の空間を占める + int a = 0; + int b = 0; + int[] nums = new int[10000]; + ListNode node = new(0); + // ループ内の変数は O(1) の空間を占める + for (int i = 0; i < n; i++) { + int c = 0; + } + // ループ内の関数は O(1) の空間を占める + for (int i = 0; i < n; i++) { + Function(); + } + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{function} + /* 関数 */ + func function() int { + // いくつかの操作を実行... + return 0 + } - [class]{}-[func]{spaceConstant} + /* 定数階 */ + func spaceConstant(n int) { + // 定数、変数、オブジェクトは O(1) の空間を占める + const a = 0 + b := 0 + nums := make([]int, 10000) + node := newNode(0) + // ループ内の変数は O(1) の空間を占める + var c int + for i := 0; i < n; i++ { + c = 0 + } + // ループ内の関数は O(1) の空間を占める + for i := 0; i < n; i++ { + function() + } + b += 0 + c += 0 + nums[0] = 0 + node.val = 0 + } ``` === "Swift" ```swift title="space_complexity.swift" - [class]{}-[func]{function} + /* 関数 */ + @discardableResult + func function() -> Int { + // 何らかの処理を行う + return 0 + } - [class]{}-[func]{constant} + /* 定数階 */ + func constant(n: Int) { + // 定数、変数、オブジェクトは O(1) の空間を占める + let a = 0 + var b = 0 + let nums = Array(repeating: 0, count: 10000) + let node = ListNode(x: 0) + // ループ内の変数は O(1) の空間を占める + for _ in 0 ..< n { + let c = 0 + } + // ループ内の関数は O(1) の空間を占める + for _ in 0 ..< n { + function() + } + } ``` === "JS" ```javascript title="space_complexity.js" - [class]{}-[func]{constFunc} + /* 関数 */ + function constFunc() { + // 何らかの処理を行う + return 0; + } - [class]{}-[func]{constant} + /* 定数階 */ + function constant(n) { + // 定数、変数、オブジェクトは O(1) の空間を占める + const a = 0; + const b = 0; + const nums = new Array(10000); + const node = new ListNode(0); + // ループ内の変数は O(1) の空間を占める + for (let i = 0; i < n; i++) { + const c = 0; + } + // ループ内の関数は O(1) の空間を占める + for (let i = 0; i < n; i++) { + constFunc(); + } + } ``` === "TS" ```typescript title="space_complexity.ts" - [class]{}-[func]{constFunc} + /* 関数 */ + function constFunc(): number { + // 何らかの処理を行う + return 0; + } - [class]{}-[func]{constant} + /* 定数階 */ + function constant(n: number): void { + // 定数、変数、オブジェクトは O(1) の空間を占める + const a = 0; + const b = 0; + const nums = new Array(10000); + const node = new ListNode(0); + // ループ内の変数は O(1) の空間を占める + for (let i = 0; i < n; i++) { + const c = 0; + } + // ループ内の関数は O(1) の空間を占める + for (let i = 0; i < n; i++) { + constFunc(); + } + } ``` === "Dart" ```dart title="space_complexity.dart" - [class]{}-[func]{function} + /* 関数 */ + int function() { + // 何らかの処理を行う + return 0; + } - [class]{}-[func]{constant} + /* 定数階 */ + void constant(int n) { + // 定数、変数、オブジェクトは O(1) の空間を占める + final int a = 0; + int b = 0; + List nums = List.filled(10000, 0); + ListNode node = ListNode(0); + // ループ内の変数は O(1) の空間を占める + for (var i = 0; i < n; i++) { + int c = 0; + } + // ループ内の関数は O(1) の空間を占める + for (var i = 0; i < n; i++) { + function(); + } + } ``` === "Rust" ```rust title="space_complexity.rs" - [class]{}-[func]{function} + /* 関数 */ + fn function() -> i32 { + // 何らかの処理を行う + return 0; + } - [class]{}-[func]{constant} + /* 定数階 */ + #[allow(unused)] + fn constant(n: i32) { + // 定数、変数、オブジェクトは O(1) の空間を占める + const A: i32 = 0; + let b = 0; + let nums = vec![0; 10000]; + let node = ListNode::new(0); + // ループ内の変数は O(1) の空間を占める + for i in 0..n { + let c = 0; + } + // ループ内の関数は O(1) の空間を占める + for i in 0..n { + function(); + } + } ``` === "C" ```c title="space_complexity.c" - [class]{}-[func]{func} + /* 関数 */ + int func() { + // 何らかの処理を行う + return 0; + } - [class]{}-[func]{constant} + /* 定数階 */ + void constant(int n) { + // 定数、変数、オブジェクトは O(1) の空間を占める + const int a = 0; + int b = 0; + int nums[1000]; + ListNode *node = newListNode(0); + free(node); + // ループ内の変数は O(1) の空間を占める + for (int i = 0; i < n; i++) { + int c = 0; + } + // ループ内の関数は O(1) の空間を占める + for (int i = 0; i < n; i++) { + func(); + } + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{function} + /* 関数 */ + fun function(): Int { + // 何らかの処理を行う + return 0 + } - [class]{}-[func]{constant} + /* 定数階 */ + fun constant(n: Int) { + // 定数、変数、オブジェクトは O(1) の空間を占める + val a = 0 + var b = 0 + val nums = Array(10000) { 0 } + val node = ListNode(0) + // ループ内の変数は O(1) の空間を占める + for (i in 0.. + + +### 2.   線形階 $O(n)$ {data-toc-label="2.   線形階"} + +線形階は、要素数が $n$ に比例する配列、連結リスト、スタック、キューなどによく現れます。 === "Python" ```python title="space_complexity.py" def linear(n: int): - """線形複雑度""" - # 長さ n のリストは O(n) のスペースを占有 + """線形階""" + # 長さ n のリストは O(n) の空間を使用 nums = [0] * n - # 長さ n のハッシュマップは O(n) のスペースを占有 + # 長さ n のハッシュテーブルは O(n) の空間を使用 hmap = dict[int, str]() for i in range(n): hmap[i] = str(i) @@ -908,16 +1202,16 @@ $$ === "C++" ```cpp title="space_complexity.cpp" - /* 線形計算量 */ + /* 線形階 */ void linear(int n) { - // 長さ n の配列は O(n) 空間を占める + // 長さ n の配列は O(n) の空間を使用 vector nums(n); - // 長さ n のリストは O(n) 空間を占める + // 長さ n のリストは O(n) の空間を使用 vector nodes; for (int i = 0; i < n; i++) { nodes.push_back(ListNode(i)); } - // 長さ n のハッシュテーブルは O(n) 空間を占める + // 長さ n のハッシュテーブルは O(n) の空間を使用 unordered_map map; for (int i = 0; i < n; i++) { map[i] = to_string(i); @@ -928,16 +1222,16 @@ $$ === "Java" ```java title="space_complexity.java" - /* 線形計算量 */ + /* 線形階 */ void linear(int n) { - // 長さ n の配列は O(n) 空間を占める + // 長さ n の配列は O(n) の空間を使用 int[] nums = new int[n]; - // 長さ n のリストは O(n) 空間を占める + // 長さ n のリストは O(n) の空間を使用 List nodes = new ArrayList<>(); for (int i = 0; i < n; i++) { nodes.add(new ListNode(i)); } - // 長さ n のハッシュテーブルは O(n) 空間を占める + // 長さ n のハッシュテーブルは O(n) の空間を使用 Map map = new HashMap<>(); for (int i = 0; i < n; i++) { map.put(i, String.valueOf(i)); @@ -948,72 +1242,231 @@ $$ === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{Linear} + /* 線形階 */ + void Linear(int n) { + // 長さ n の配列は O(n) の空間を使用 + int[] nums = new int[n]; + // 長さ n のリストは O(n) の空間を使用 + List nodes = []; + for (int i = 0; i < n; i++) { + nodes.Add(new ListNode(i)); + } + // 長さ n のハッシュテーブルは O(n) の空間を使用 + Dictionary map = []; + for (int i = 0; i < n; i++) { + map.Add(i, i.ToString()); + } + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{spaceLinear} + /* 線形階 */ + func spaceLinear(n int) { + // 長さ n の配列は O(n) の空間を使用 + _ = make([]int, n) + // 長さ n のリストは O(n) の空間を使用 + var nodes []*node + for i := 0; i < n; i++ { + nodes = append(nodes, newNode(i)) + } + // 長さ n のハッシュテーブルは O(n) の空間を使用 + m := make(map[int]string, n) + for i := 0; i < n; i++ { + m[i] = strconv.Itoa(i) + } + } ``` === "Swift" ```swift title="space_complexity.swift" - [class]{}-[func]{linear} + /* 線形階 */ + func linear(n: Int) { + // 長さ n の配列は O(n) の空間を使用 + let nums = Array(repeating: 0, count: n) + // 長さ n のリストは O(n) の空間を使用 + let nodes = (0 ..< n).map { ListNode(x: $0) } + // 長さ n のハッシュテーブルは O(n) の空間を使用 + let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, "\($0)") }) + } ``` === "JS" ```javascript title="space_complexity.js" - [class]{}-[func]{linear} + /* 線形階 */ + function linear(n) { + // 長さ n の配列は O(n) の空間を使用 + const nums = new Array(n); + // 長さ n のリストは O(n) の空間を使用 + const nodes = []; + for (let i = 0; i < n; i++) { + nodes.push(new ListNode(i)); + } + // 長さ n のハッシュテーブルは O(n) の空間を使用 + const map = new Map(); + for (let i = 0; i < n; i++) { + map.set(i, i.toString()); + } + } ``` === "TS" ```typescript title="space_complexity.ts" - [class]{}-[func]{linear} + /* 線形階 */ + function linear(n: number): void { + // 長さ n の配列は O(n) の空間を使用 + const nums = new Array(n); + // 長さ n のリストは O(n) の空間を使用 + const nodes: ListNode[] = []; + for (let i = 0; i < n; i++) { + nodes.push(new ListNode(i)); + } + // 長さ n のハッシュテーブルは O(n) の空間を使用 + const map = new Map(); + for (let i = 0; i < n; i++) { + map.set(i, i.toString()); + } + } ``` === "Dart" ```dart title="space_complexity.dart" - [class]{}-[func]{linear} + /* 線形階 */ + void linear(int n) { + // 長さ n の配列は O(n) の空間を使用 + List nums = List.filled(n, 0); + // 長さ n のリストは O(n) の空間を使用 + List nodes = []; + for (var i = 0; i < n; i++) { + nodes.add(ListNode(i)); + } + // 長さ n のハッシュテーブルは O(n) の空間を使用 + Map map = HashMap(); + for (var i = 0; i < n; i++) { + map.putIfAbsent(i, () => i.toString()); + } + } ``` === "Rust" ```rust title="space_complexity.rs" - [class]{}-[func]{linear} + /* 線形階 */ + #[allow(unused)] + fn linear(n: i32) { + // 長さ n の配列は O(n) の空間を使用 + let mut nums = vec![0; n as usize]; + // 長さ n のリストは O(n) の空間を使用 + let mut nodes = Vec::new(); + for i in 0..n { + nodes.push(ListNode::new(i)) + } + // 長さ n のハッシュテーブルは O(n) の空間を使用 + let mut map = HashMap::new(); + for i in 0..n { + map.insert(i, i.to_string()); + } + } ``` === "C" ```c title="space_complexity.c" - [class]{HashTable}-[func]{} + /* ハッシュテーブル */ + typedef struct { + int key; + int val; + UT_hash_handle hh; // uthash.h を用いて実装 + } HashTable; - [class]{}-[func]{linear} + /* 線形階 */ + void linear(int n) { + // 長さ n の配列は O(n) の空間を使用 + int *nums = malloc(sizeof(int) * n); + free(nums); + + // 長さ n のリストは O(n) の空間を使用 + ListNode **nodes = malloc(sizeof(ListNode *) * n); + for (int i = 0; i < n; i++) { + nodes[i] = newListNode(i); + } + // メモリを解放する + for (int i = 0; i < n; i++) { + free(nodes[i]); + } + free(nodes); + + // 長さ n のハッシュテーブルは O(n) の空間を使用 + HashTable *h = NULL; + for (int i = 0; i < n; i++) { + HashTable *tmp = malloc(sizeof(HashTable)); + tmp->key = i; + tmp->val = i; + HASH_ADD_INT(h, key, tmp); + } + + // メモリを解放する + HashTable *curr, *tmp; + HASH_ITER(hh, h, curr, tmp) { + HASH_DEL(h, curr); + free(curr); + } + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{linear} + /* 線形階 */ + fun linear(n: Int) { + // 長さ n の配列は O(n) の空間を使用 + val nums = Array(n) { 0 } + // 長さ n のリストは O(n) の空間を使用 + val nodes = mutableListOf() + for (i in 0..() + for (i in 0.. + + +以下の図に示すように、この関数の再帰の深さは $n$ であり、同時に $n$ 個の未返却 `linear_recur()` 関数が存在するため、$O(n)$ のスタックフレーム空間を使用します。 === "Python" ```python title="space_complexity.py" def linear_recur(n: int): - """線形複雑度(再帰実装)""" + """線形時間(再帰実装)""" print("再帰 n =", n) if n == 1: return @@ -1023,7 +1476,7 @@ $$ === "C++" ```cpp title="space_complexity.cpp" - /* 線形計算量(再帰実装) */ + /* 線形時間(再帰実装) */ void linearRecur(int n) { cout << "再帰 n = " << n << endl; if (n == 1) @@ -1035,7 +1488,7 @@ $$ === "Java" ```java title="space_complexity.java" - /* 線形計算量(再帰実装) */ + /* 線形時間(再帰実装) */ void linearRecur(int n) { System.out.println("再帰 n = " + n); if (n == 1) @@ -1047,86 +1500,161 @@ $$ === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{LinearRecur} + /* 線形時間(再帰実装) */ + void LinearRecur(int n) { + Console.WriteLine("再帰 n = " + n); + if (n == 1) return; + LinearRecur(n - 1); + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{spaceLinearRecur} + /* 線形時間(再帰実装) */ + func spaceLinearRecur(n int) { + fmt.Println("再帰 n =", n) + if n == 1 { + return + } + spaceLinearRecur(n - 1) + } ``` === "Swift" ```swift title="space_complexity.swift" - [class]{}-[func]{linearRecur} + /* 線形時間(再帰実装) */ + func linearRecur(n: Int) { + print("再帰 n = \(n)") + if n == 1 { + return + } + linearRecur(n: n - 1) + } ``` === "JS" ```javascript title="space_complexity.js" - [class]{}-[func]{linearRecur} + /* 線形時間(再帰実装) */ + function linearRecur(n) { + console.log(`再帰 n = ${n}`); + if (n === 1) return; + linearRecur(n - 1); + } ``` === "TS" ```typescript title="space_complexity.ts" - [class]{}-[func]{linearRecur} + /* 線形時間(再帰実装) */ + function linearRecur(n: number): void { + console.log(`再帰 n = ${n}`); + if (n === 1) return; + linearRecur(n - 1); + } ``` === "Dart" ```dart title="space_complexity.dart" - [class]{}-[func]{linearRecur} + /* 線形時間(再帰実装) */ + void linearRecur(int n) { + print('再帰 n = $n'); + if (n == 1) return; + linearRecur(n - 1); + } ``` === "Rust" ```rust title="space_complexity.rs" - [class]{}-[func]{linear_recur} + /* 線形時間(再帰実装) */ + fn linear_recur(n: i32) { + println!("再帰 n = {}", n); + if n == 1 { + return; + }; + linear_recur(n - 1); + } ``` === "C" ```c title="space_complexity.c" - [class]{}-[func]{linearRecur} + /* 線形時間(再帰実装) */ + void linearRecur(int n) { + printf("再帰 n = %d\r\n", n); + if (n == 1) + return; + linearRecur(n - 1); + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{linearRecur} + /* 線形時間(再帰実装) */ + fun linearRecur(n: Int) { + println("再帰 n = $n") + if (n == 1) + return + linearRecur(n - 1) + } ``` === "Ruby" ```ruby title="space_complexity.rb" - [class]{}-[func]{linear_recur} + ### 線形階 ### + def linear(n) + # 長さ n のリストは O(n) の空間を使用 + nums = Array.new(n, 0) + + # 長さ n のハッシュテーブルは O(n) の空間を使用 + hmap = {} + for i in 0...n + hmap[i] = i.to_s + end + end + + # ## 線形階(再帰実装)### + def linear_recur(n) + puts "再帰 n = #{n}" + return if n == 1 + linear_recur(n - 1) + end ``` -![Recursive function generating linear order space complexity](space_complexity.assets/space_complexity_recursive_linear.png){ class="animation-figure" } +??? pythontutor "コードの可視化" -

図 2-17   Recursive function generating linear order space complexity

+
+ -### 3.   二次オーダー $O(n^2)$ {data-toc-label="3.   二次オーダー"} +![再帰関数が生み出す線形階の空間計算量](space_complexity.assets/space_complexity_recursive_linear.png){ class="animation-figure" } -二次オーダーは行列やグラフで一般的で、要素数は$n$の二乗に比例します: +

図 2-17   再帰関数が生み出す線形階の空間計算量

+ +### 3.   平方階 $O(n^2)$ {data-toc-label="3.   平方階"} + +平方階は、要素数が $n$ の二乗に比例する行列やグラフによく現れます。 === "Python" ```python title="space_complexity.py" def quadratic(n: int): - """平方複雑度""" - # 二次元リストは O(n^2) のスペースを占有 + """二乗階""" + # 二次元リストは O(n^2) の空間を使用 num_matrix = [[0] * n for _ in range(n)] ``` === "C++" ```cpp title="space_complexity.cpp" - /* 二次計算量 */ + /* 二乗階 */ void quadratic(int n) { - // 二次元リストは O(n^2) 空間を占める + // 二次元リストは O(n^2) の空間を使用 vector> numMatrix; for (int i = 0; i < n; i++) { vector tmp; @@ -1141,11 +1669,11 @@ $$ === "Java" ```java title="space_complexity.java" - /* 二次計算量 */ + /* 二乗階 */ void quadratic(int n) { - // 行列は O(n^2) 空間を占める + // 行列は O(n^2) の空間を使用する int[][] numMatrix = new int[n][n]; - // 二次元リストは O(n^2) 空間を占める + // 二次元リストは O(n^2) の空間を使用 List> numList = new ArrayList<>(); for (int i = 0; i < n; i++) { List tmp = new ArrayList<>(); @@ -1160,86 +1688,206 @@ $$ === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{Quadratic} + /* 二乗階 */ + void Quadratic(int n) { + // 行列は O(n^2) の空間を使用する + int[,] numMatrix = new int[n, n]; + // 二次元リストは O(n^2) の空間を使用 + List> numList = []; + for (int i = 0; i < n; i++) { + List tmp = []; + for (int j = 0; j < n; j++) { + tmp.Add(0); + } + numList.Add(tmp); + } + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{spaceQuadratic} + /* 二乗階 */ + func spaceQuadratic(n int) { + // 行列は O(n^2) の空間を使用する + numMatrix := make([][]int, n) + for i := 0; i < n; i++ { + numMatrix[i] = make([]int, n) + } + } ``` === "Swift" ```swift title="space_complexity.swift" - [class]{}-[func]{quadratic} + /* 二乗階 */ + func quadratic(n: Int) { + // 二次元リストは O(n^2) の空間を使用 + let numList = Array(repeating: Array(repeating: 0, count: n), count: n) + } ``` === "JS" ```javascript title="space_complexity.js" - [class]{}-[func]{quadratic} + /* 二乗階 */ + function quadratic(n) { + // 行列は O(n^2) の空間を使用する + const numMatrix = Array(n) + .fill(null) + .map(() => Array(n).fill(null)); + // 二次元リストは O(n^2) の空間を使用 + const numList = []; + for (let i = 0; i < n; i++) { + const tmp = []; + for (let j = 0; j < n; j++) { + tmp.push(0); + } + numList.push(tmp); + } + } ``` === "TS" ```typescript title="space_complexity.ts" - [class]{}-[func]{quadratic} + /* 二乗階 */ + function quadratic(n: number): void { + // 行列は O(n^2) の空間を使用する + const numMatrix = Array(n) + .fill(null) + .map(() => Array(n).fill(null)); + // 二次元リストは O(n^2) の空間を使用 + const numList = []; + for (let i = 0; i < n; i++) { + const tmp = []; + for (let j = 0; j < n; j++) { + tmp.push(0); + } + numList.push(tmp); + } + } ``` === "Dart" ```dart title="space_complexity.dart" - [class]{}-[func]{quadratic} + /* 二乗階 */ + void quadratic(int n) { + // 行列は O(n^2) の空間を使用する + List> numMatrix = List.generate(n, (_) => List.filled(n, 0)); + // 二次元リストは O(n^2) の空間を使用 + List> numList = []; + for (var i = 0; i < n; i++) { + List tmp = []; + for (int j = 0; j < n; j++) { + tmp.add(0); + } + numList.add(tmp); + } + } ``` === "Rust" ```rust title="space_complexity.rs" - [class]{}-[func]{quadratic} + /* 二乗階 */ + #[allow(unused)] + fn quadratic(n: i32) { + // 行列は O(n^2) の空間を使用する + let num_matrix = vec![vec![0; n as usize]; n as usize]; + // 二次元リストは O(n^2) の空間を使用 + let mut num_list = Vec::new(); + for i in 0..n { + let mut tmp = Vec::new(); + for j in 0..n { + tmp.push(0); + } + num_list.push(tmp); + } + } ``` === "C" ```c title="space_complexity.c" - [class]{}-[func]{quadratic} + /* 二乗階 */ + void quadratic(int n) { + // 二次元リストは O(n^2) の空間を使用 + int **numMatrix = malloc(sizeof(int *) * n); + for (int i = 0; i < n; i++) { + int *tmp = malloc(sizeof(int) * n); + for (int j = 0; j < n; j++) { + tmp[j] = 0; + } + numMatrix[i] = tmp; + } + + // メモリを解放する + for (int i = 0; i < n; i++) { + free(numMatrix[i]); + } + free(numMatrix); + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{quadratic} + /* 二乗階 */ + fun quadratic(n: Int) { + // 行列は O(n^2) の空間を使用する + val numMatrix = arrayOfNulls?>(n) + // 二次元リストは O(n^2) の空間を使用 + val numList = mutableListOf>() + for (i in 0..() + for (j in 0.. + + +以下の図に示すように、この関数の再帰の深さは $n$ であり、各再帰関数の中で長さがそれぞれ $n$、$n-1$、$\dots$、$2$、$1$ の配列を初期化しています。平均長は $n / 2$ なので、全体では $O(n^2)$ の空間を占有します。 === "Python" ```python title="space_complexity.py" def quadratic_recur(n: int) -> int: - """平方複雑度(再帰実装)""" + """二次時間(再帰実装)""" if n <= 0: return 0 + # 配列 nums の長さは n, n-1, ..., 2, 1 nums = [0] * n - print(f"再帰 n = {n} の中で配列の長さ = {len(nums)}") return quadratic_recur(n - 1) ``` === "C++" ```cpp title="space_complexity.cpp" - /* 二次計算量(再帰実装) */ + /* 二次時間(再帰実装) */ int quadraticRecur(int n) { if (n <= 0) return 0; vector nums(n); - cout << "再帰 n = " << n << ", nums の長さ = " << nums.size() << endl; + cout << "再帰 n = " << n << " における nums の長さ = " << nums.size() << endl; return quadraticRecur(n - 1); } ``` @@ -1247,13 +1895,13 @@ $$ === "Java" ```java title="space_complexity.java" - /* 二次計算量(再帰実装) */ + /* 二次時間(再帰実装) */ int quadraticRecur(int n) { if (n <= 0) return 0; - // 配列 nums の長さ = n, n-1, ..., 2, 1 + // 配列 nums の長さは n, n-1, ..., 2, 1 int[] nums = new int[n]; - System.out.println("再帰 n = " + n + " の nums の長さ = " + nums.length); + System.out.println("再帰 n = " + n + " における nums の長さ = " + nums.length); return quadraticRecur(n - 1); } ``` @@ -1261,76 +1909,162 @@ $$ === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{QuadraticRecur} + /* 二次時間(再帰実装) */ + int QuadraticRecur(int n) { + if (n <= 0) return 0; + int[] nums = new int[n]; + Console.WriteLine("再帰 n = " + n + " における nums の長さ = " + nums.Length); + return QuadraticRecur(n - 1); + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{spaceQuadraticRecur} + /* 二次時間(再帰実装) */ + func spaceQuadraticRecur(n int) int { + if n <= 0 { + return 0 + } + nums := make([]int, n) + fmt.Printf("再帰 n = %d における nums の長さ = %d \n", n, len(nums)) + return spaceQuadraticRecur(n - 1) + } ``` === "Swift" ```swift title="space_complexity.swift" - [class]{}-[func]{quadraticRecur} + /* 二次時間(再帰実装) */ + @discardableResult + func quadraticRecur(n: Int) -> Int { + if n <= 0 { + return 0 + } + // 配列 nums の長さは n, n-1, ..., 2, 1 + let nums = Array(repeating: 0, count: n) + print("再帰 n = \(n) における nums の長さ = \(nums.count)") + return quadraticRecur(n: n - 1) + } ``` === "JS" ```javascript title="space_complexity.js" - [class]{}-[func]{quadraticRecur} + /* 二次時間(再帰実装) */ + function quadraticRecur(n) { + if (n <= 0) return 0; + const nums = new Array(n); + console.log(`再帰 n = ${n} における nums の長さ = ${nums.length}`); + return quadraticRecur(n - 1); + } ``` === "TS" ```typescript title="space_complexity.ts" - [class]{}-[func]{quadraticRecur} + /* 二次時間(再帰実装) */ + function quadraticRecur(n: number): number { + if (n <= 0) return 0; + const nums = new Array(n); + console.log(`再帰 n = ${n} における nums の長さ = ${nums.length}`); + return quadraticRecur(n - 1); + } ``` === "Dart" ```dart title="space_complexity.dart" - [class]{}-[func]{quadraticRecur} + /* 二次時間(再帰実装) */ + int quadraticRecur(int n) { + if (n <= 0) return 0; + List nums = List.filled(n, 0); + print('再帰 n = $n における nums の長さ = ${nums.length}'); + return quadraticRecur(n - 1); + } ``` === "Rust" ```rust title="space_complexity.rs" - [class]{}-[func]{quadratic_recur} + /* 二次時間(再帰実装) */ + fn quadratic_recur(n: i32) -> i32 { + if n <= 0 { + return 0; + }; + // 配列 nums の長さは n, n-1, ..., 2, 1 + let nums = vec![0; n as usize]; + println!("再帰 n = {} における nums の長さ = {}", n, nums.len()); + return quadratic_recur(n - 1); + } ``` === "C" ```c title="space_complexity.c" - [class]{}-[func]{quadraticRecur} + /* 二次時間(再帰実装) */ + int quadraticRecur(int n) { + if (n <= 0) + return 0; + int *nums = malloc(sizeof(int) * n); + printf("再帰 n = %d における nums の長さ = %d\r\n", n, n); + int res = quadraticRecur(n - 1); + free(nums); + return res; + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{quadraticRecur} + /* 二次時間(再帰実装) */ + tailrec fun quadraticRecur(n: Int): Int { + if (n <= 0) + return 0 + // 配列 nums の長さは n, n-1, ..., 2, 1 + val nums = Array(n) { 0 } + println("再帰 n = $n における nums の長さ = ${nums.size}") + return quadraticRecur(n - 1) + } ``` === "Ruby" ```ruby title="space_complexity.rb" - [class]{}-[func]{quadratic_recur} + ### 平方階 ### + def quadratic(n) + # 二次元リストは O(n^2) の空間を使用 + Array.new(n) { Array.new(n, 0) } + end + + # ## 平方階(再帰実装)### + def quadratic_recur(n) + return 0 unless n > 0 + + # 配列 nums の長さは n, n-1, ..., 2, 1 + nums = Array.new(n, 0) + quadratic_recur(n - 1) + end ``` -![Recursive function generating quadratic order space complexity](space_complexity.assets/space_complexity_recursive_quadratic.png){ class="animation-figure" } +??? pythontutor "コードの可視化" -

図 2-18   Recursive function generating quadratic order space complexity

+
+ -### 4.   指数オーダー $O(2^n)$ {data-toc-label="4.   指数オーダー"} +![再帰関数が生み出す平方階の空間計算量](space_complexity.assets/space_complexity_recursive_quadratic.png){ class="animation-figure" } -指数オーダーは二分木で一般的です。下図を観察すると、$n$レベルの「完全二分木」は$2^n - 1$個のノードを持ち、$O(2^n)$の空間を占有します: +

図 2-18   再帰関数が生み出す平方階の空間計算量

+ +### 4.   指数階 $O(2^n)$ {data-toc-label="4.   指数階"} + +指数階は二分木によく現れます。以下の図を見ると、高さが $n$ の「満二分木」のノード数は $2^n - 1$ であり、$O(2^n)$ の空間を占有します。 === "Python" ```python title="space_complexity.py" def build_tree(n: int) -> TreeNode | None: - """指数複雑度(完全二分木の構築)""" + """指数時間(完全二分木の構築)""" if n == 0: return None root = TreeNode(0) @@ -1342,7 +2076,7 @@ $$ === "C++" ```cpp title="space_complexity.cpp" - /* 指数計算量(完全二分木の構築) */ + /* 指数時間(完全二分木の構築) */ TreeNode *buildTree(int n) { if (n == 0) return nullptr; @@ -1356,7 +2090,7 @@ $$ === "Java" ```java title="space_complexity.java" - /* 指数計算量(完全二分木の構築) */ + /* 指数時間(完全二分木の構築) */ TreeNode buildTree(int n) { if (n == 0) return null; @@ -1370,77 +2104,177 @@ $$ === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{BuildTree} + /* 指数時間(完全二分木の構築) */ + TreeNode? BuildTree(int n) { + if (n == 0) return null; + TreeNode root = new(0) { + left = BuildTree(n - 1), + right = BuildTree(n - 1) + }; + return root; + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{buildTree} + /* 指数時間(完全二分木の構築) */ + func buildTree(n int) *TreeNode { + if n == 0 { + return nil + } + root := NewTreeNode(0) + root.Left = buildTree(n - 1) + root.Right = buildTree(n - 1) + return root + } ``` === "Swift" ```swift title="space_complexity.swift" - [class]{}-[func]{buildTree} + /* 指数時間(完全二分木の構築) */ + func buildTree(n: Int) -> TreeNode? { + if n == 0 { + return nil + } + let root = TreeNode(x: 0) + root.left = buildTree(n: n - 1) + root.right = buildTree(n: n - 1) + return root + } ``` === "JS" ```javascript title="space_complexity.js" - [class]{}-[func]{buildTree} + /* 指数時間(完全二分木の構築) */ + function buildTree(n) { + if (n === 0) return null; + const root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } ``` === "TS" ```typescript title="space_complexity.ts" - [class]{}-[func]{buildTree} + /* 指数時間(完全二分木の構築) */ + function buildTree(n: number): TreeNode | null { + if (n === 0) return null; + const root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } ``` === "Dart" ```dart title="space_complexity.dart" - [class]{}-[func]{buildTree} + /* 指数時間(完全二分木の構築) */ + TreeNode? buildTree(int n) { + if (n == 0) return null; + TreeNode root = TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } ``` === "Rust" ```rust title="space_complexity.rs" - [class]{}-[func]{build_tree} + /* 指数時間(完全二分木の構築) */ + fn build_tree(n: i32) -> Option>> { + if n == 0 { + return None; + }; + let root = TreeNode::new(0); + root.borrow_mut().left = build_tree(n - 1); + root.borrow_mut().right = build_tree(n - 1); + return Some(root); + } ``` === "C" ```c title="space_complexity.c" - [class]{}-[func]{buildTree} + /* 指数時間(完全二分木の構築) */ + TreeNode *buildTree(int n) { + if (n == 0) + return NULL; + TreeNode *root = newTreeNode(0); + root->left = buildTree(n - 1); + root->right = buildTree(n - 1); + return root; + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{buildTree} + /* 指数時間(完全二分木の構築) */ + fun buildTree(n: Int): TreeNode? { + if (n == 0) + return null + val root = TreeNode(0) + root.left = buildTree(n - 1) + root.right = buildTree(n - 1) + return root + } ``` === "Ruby" ```ruby title="space_complexity.rb" - [class]{}-[func]{build_tree} + ### 平方階 ### + def quadratic(n) + # 二次元リストは O(n^2) の空間を使用 + Array.new(n) { Array.new(n, 0) } + end + + # ## 平方階(再帰実装)### + def quadratic_recur(n) + return 0 unless n > 0 + + # 配列 nums の長さは n, n-1, ..., 2, 1 + nums = Array.new(n, 0) + quadratic_recur(n - 1) + end + + # ## 指数階(満二分木を構築)### + def build_tree(n) + return if n == 0 + + TreeNode.new.tap do |root| + root.left = build_tree(n - 1) + root.right = build_tree(n - 1) + end + end ``` -![Full binary tree generating exponential order space complexity](space_complexity.assets/space_complexity_exponential.png){ class="animation-figure" } +??? pythontutor "コードの可視化" -

図 2-19   Full binary tree generating exponential order space complexity

+
+ -### 5.   対数オーダー $O(\log n)$ {data-toc-label="5.   対数オーダー"} +![満二分木が生み出す指数階の空間計算量](space_complexity.assets/space_complexity_exponential.png){ class="animation-figure" } -対数オーダーは分割統治アルゴリズムで一般的です。例えば、マージソートでは、長さ$n$の配列が各ラウンドで再帰的に半分に分割され、高さ$\log n$の再帰木を形成し、$O(\log n)$のスタックフレーム空間を使用します。 +

図 2-19   満二分木が生み出す指数階の空間計算量

-別の例は、数値を文字列に変換することです。正の整数$n$が与えられた場合、その桁数は$\log_{10} n + 1$で、文字列の長さに対応するため、空間計算量は$O(\log_{10} n + 1) = O(\log n)$です。 +### 5.   対数階 $O(\log n)$ {data-toc-label="5.   対数階"} -## 2.4.4   時間と空間のバランス +対数階は分割統治アルゴリズムによく現れます。例えばマージソートでは、長さ $n$ の配列を入力として、各再帰で配列を中央から二つに分割するため、高さ $\log n$ の再帰木が形成され、$O(\log n)$ のスタックフレーム空間を使用します。 -理想的には、時間計算量と空間計算量の両方が最適であることを目指します。しかし、実際には両方を同時に最適化することはしばしば困難です。 +また、数値を文字列に変換する場合を考えると、正の整数 $n$ の桁数は $\lfloor \log_{10} n \rfloor + 1$ であり、対応する文字列長も $\lfloor \log_{10} n \rfloor + 1$ です。したがって空間計算量は $O(\log_{10} n + 1) = O(\log n)$ となります。 -**時間計算量を下げることは通常、空間計算量の増加を代償とし、その逆も同様です**。アルゴリズムの速度を向上させるためにメモリ空間を犠牲にするアプローチは「時空トレードオフ」として知られ、その逆は「空時トレードオフ」として知られています。 +## 2.4.4   時間と空間のトレードオフ -選択は、どちらの側面をより重視するかに依存します。ほとんどの場合、時間は空間よりも貴重であるため、「時空トレードオフ」がより一般的な戦略です。もちろん、大量のデータを扱う際は空間計算量を制御することも非常に重要です。 +理想的には、アルゴリズムの時間計算量と空間計算量の両方を最適にしたいところです。しかし実際には、この二つを同時に最適化するのは通常きわめて困難です。 + +**時間計算量を下げるには、通常、空間計算量を増やす代償が必要であり、その逆も同様です**。メモリ空間を犠牲にして実行速度を上げる考え方を「空間を時間と引き換えにする」と呼び、その逆を「時間を空間と引き換えにする」と呼びます。 + +どちらの考え方を選ぶかは、何をより重視するかによって決まります。多くの場合、空間より時間のほうが貴重なので、「空間を時間と引き換えにする」戦略のほうが一般的です。もちろん、データ量が非常に大きい場合には、空間計算量を抑えることも同じくらい重要です。 diff --git a/ja/docs/chapter_computational_complexity/summary.md b/ja/docs/chapter_computational_complexity/summary.md index 6b0b99a7d..f96291204 100644 --- a/ja/docs/chapter_computational_complexity/summary.md +++ b/ja/docs/chapter_computational_complexity/summary.md @@ -4,50 +4,56 @@ comments: true # 2.5   まとめ -### 1.   重要なレビュー +### 1.   要点の振り返り -**アルゴリズム効率評価** +**アルゴリズム効率の評価** -- 時間効率と空間効率は、アルゴリズムの優劣を評価する2つの主要な基準です。 -- 実際のテストによってアルゴリズムの効率を評価できますが、テスト環境の影響を排除することは困難で、大量の計算リソースを消費します。 -- 複雑度分析は実際のテストの欠点を克服できます。その結果はすべての動作プラットフォームに適用でき、異なるデータスケールでのアルゴリズムの効率を明らかにできます。 +- 時間効率と空間効率は、アルゴリズムの良し悪しを測る二つの主要な評価指標です。 +- 実測によってアルゴリズム効率を評価できますが、テスト環境の影響を排除しにくく、多くの計算資源も消費します。 +- 複雑度分析は実測の欠点を補い、分析結果はすべての実行プラットフォームに適用でき、データ規模ごとの効率も明らかにできます。 **時間計算量** -- 時間計算量は、データ量の増加に伴うアルゴリズムの実行時間の傾向を測定し、アルゴリズムの効率を効果的に評価します。しかし、入力データ量が少ない場合や時間計算量が同じ場合など、特定のケースでは失敗することがあり、アルゴリズムの効率を正確に比較することが困難になります。 -- 最悪ケース時間計算量はビッグ$O$記法を使用して表記され、漸近上限を表し、$n$が無限大に近づくにつれての操作数$T(n)$の増加レベルを反映します。 -- 時間計算量の計算には2つのステップが含まれます:まず操作数をカウントし、次に漸近上限を決定します。 -- 一般的な時間計算量は、低いものから高いものへと並べると、$O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(2^n)$、$O(n!)$などが含まれます。 -- 一部のアルゴリズムの時間計算量は固定されておらず、入力データの分布に依存します。時間計算量は最悪、最良、平均のケースに分けられます。最良ケースは、入力データが最良ケースを達成するために厳格な条件を満たす必要があるため、ほとんど使用されません。 -- 平均時間計算量は、ランダムデータ入力下でのアルゴリズムの効率を反映し、実際のアプリケーションでのアルゴリズムの性能に密接に類似しています。平均時間計算量の計算には、入力データの分布とその後の数学的期待値を考慮する必要があります。 +- 時間計算量は、アルゴリズムの実行時間がデータ量の増加に伴ってどう変化するかを測るためのものであり、効率評価に有効です。ただし、入力データ量が小さい場合や時間計算量が同じ場合などには、効率の優劣を正確に比較できないことがあります。 +- 最悪時間計算量はビッグオー記法 $O$ で表され、関数の漸近上界に対応し、$n$ が正の無限大に近づくときの操作回数 $T(n)$ の増加の度合いを表します。 +- 時間計算量の推定は二段階に分かれ、まず操作回数を数え、次に漸近上界を判断します。 +- 一般的な時間計算量を低い順から並べると、$O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(2^n)$、$O(n!)$ などがあります。 +- 一部のアルゴリズムの時間計算量は固定ではなく、入力データの分布に関係します。時間計算量には最悪、最良、平均時間計算量がありますが、最良時間計算量は入力データが厳しい条件を満たす必要があるため、ほとんど使われません。 +- 平均時間計算量は、ランダムな入力データに対するアルゴリズムの実行効率を表し、実運用時の性能に最も近い指標です。平均時間計算量を求めるには、入力データの分布と、それを踏まえた数学的期待値を統計する必要があります。 **空間計算量** -- 空間計算量は、時間計算量と同様に、データ量の増加に伴うアルゴリズムが占有するメモリ空間の傾向を測定します。 -- アルゴリズムの実行中に使用される関連メモリ空間は、入力空間、一時空間、出力空間に分けることができます。一般的に、入力空間は空間計算量の計算に含まれません。一時空間は一時データ、スタックフレーム空間、命令空間に分けることができ、スタックフレーム空間は通常、再帰関数でのみ空間計算量に影響します。 -- 通常は最悪ケース空間計算量のみに焦点を当てます。これは、最悪の入力データと操作の最悪の瞬間でのアルゴリズムの空間計算量を計算することを意味します。 -- 一般的な空間計算量は、低いものから高いものへと並べると、$O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$、$O(2^n)$などが含まれます。 +- 空間計算量の役割は時間計算量に似ており、アルゴリズムが使用するメモリ空間がデータ量の増加に伴ってどう変化するかを測ります。 +- アルゴリズム実行中に関係するメモリ空間は、入力空間、一時空間、出力空間に分けられます。通常、入力空間は空間計算量の計算に含めません。一時空間は一時データ、スタックフレーム空間、命令空間に分けられ、このうちスタックフレーム空間は通常、再帰関数でのみ空間計算量に影響します。 +- 私たちは通常、最悪空間計算量のみに注目し、最悪の入力データと最悪の実行時点における空間計算量を数えます。 +- 一般的な空間計算量を低い順から並べると、$O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$、$O(2^n)$ などがあります。 ### 2.   Q & A -**Q**: 末尾再帰の空間計算量は$O(1)$ですか? +**Q**:尾再帰の空間計算量は $O(1)$ ですか? -理論的には、末尾再帰関数の空間計算量は$O(1)$に最適化できます。しかし、ほとんどのプログラミング言語(Java、Python、C++、Go、C#など)は末尾再帰の自動最適化をサポートしていないため、一般的に空間計算量は$O(n)$と考えられています。 +理論上、尾再帰関数の空間計算量は $O(1)$ まで最適化できます。ただし、ほとんどのプログラミング言語(Java、Python、C++、Go、C# など)は尾再帰の自動最適化をサポートしていないため、通常は空間計算量を $O(n)$ と見なします。 -**Q**: 「関数」と「メソッド」という用語の違いは何ですか? +**Q**:関数とメソッドという二つの用語の違いは何ですか? -関数は独立して実行でき、すべてのパラメータが明示的に渡されます。メソッドはオブジェクトに関連付けられ、それを呼び出すオブジェクトに暗黙的に渡され、クラスのインスタンス内に含まれるデータを操作できます。 +関数(function)は独立して実行でき、すべての引数は明示的に渡されます。メソッド(method)はオブジェクトに関連付けられ、それを呼び出すオブジェクトが暗黙的に渡され、クラスのインスタンスに含まれるデータを操作できます。 -一般的なプログラミング言語からの例をいくつか示します: +以下では、いくつかの一般的なプログラミング言語を例に説明します。 -- Cは手続き型プログラミング言語で、オブジェクト指向の概念がないため、関数のみがあります。しかし、構造体(struct)を作成することでオブジェクト指向プログラミングをシミュレートでき、これらの構造体に関連付けられた関数は他のプログラミング言語のメソッドと同等です。 -- JavaとC#はオブジェクト指向プログラミング言語で、コードブロック(メソッド)は通常クラスの一部です。静的メソッドはクラスにバインドされ、特定のインスタンス変数にアクセスできないため、関数のように動作します。 -- C++とPythonは手続き型プログラミング(関数)とオブジェクト指向プログラミング(メソッド)の両方をサポートしています。 +- C 言語は手続き型プログラミング言語であり、オブジェクト指向の概念がないため、関数しかありません。ただし、構造体(struct)を作成してオブジェクト指向プログラミングを模倣でき、構造体に関連付けられた関数は、他のプログラミング言語におけるメソッドに相当します。 +- Java と C# はオブジェクト指向のプログラミング言語であり、コードブロック(メソッド)は通常あるクラスの一部です。静的メソッドの振る舞いは関数に似ており、クラスに束縛され、特定のインスタンス変数にはアクセスできません。 +- C++ と Python は、手続き型プログラミング(関数)にもオブジェクト指向プログラミング(メソッド)にも対応しています。 -**Q**: 「空間計算量の一般的な種類」の図は、占有空間の絶対サイズを反映していますか? +**Q**:「一般的な空間計算量の種類」の図が表しているのは、使用空間の絶対量ですか? -いいえ、図は空間計算量を示しており、これは増加傾向を反映するものであり、占有空間の絶対サイズではありません。 +いいえ。この図が示しているのは空間計算量であり、表しているのは増加傾向であって、使用空間の絶対量ではありません。 -$n = 8$を取ると、各曲線の値がその関数に対応していないことに気づくかもしれません。これは、各曲線に定数項が含まれているためで、値の範囲を視覚的に快適な範囲に圧縮することを意図しています。 +$n = 8$ と仮定すると、各曲線の値が対応する関数と一致していないように見えるかもしれません。これは、各曲線に定数項が含まれており、値の範囲を視覚的に見やすい範囲へ圧縮しているためです。 -実際には、通常は各メソッドの「定数項」複雑度を知らないため、複雑度のみに基づいて$n = 8$の最良ソリューションを選択することは一般的に不可能です。しかし、$n = 8^5$の場合、増加傾向が支配的になるため、選択がはるかに容易になります。 +実際には、各手法の「定数項」の複雑度がどれほどか通常は分からないため、一般に複雑度だけを根拠に $n = 8$ 以下で最適解を選ぶことはできません。ただし、$n = 8^5$ であれば選びやすく、このときは増加傾向がすでに支配的になっています。 + +**Q** 実際の利用場面に応じて、時間(または空間)を犠牲にしてアルゴリズムを設計することはありますか? + +実際の応用では、多くの場合、空間を犠牲にして時間を得る選択をします。たとえばデータベースのインデックスでは、通常 B+ 木やハッシュインデックスを構築し、大量のメモリ空間を使う代わりに、$O(\log n)$ あるいは $O(1)$ の高速な検索を実現します。 + +空間資源が貴重な場面では、時間を犠牲にして空間を得ることもあります。たとえば組み込み開発では、デバイスのメモリが非常に貴重なため、エンジニアはハッシュテーブルの使用をやめ、配列による順次探索を選んでメモリ使用量を節約することがあります。その代償として探索は遅くなります。 diff --git a/ja/docs/chapter_computational_complexity/time_complexity.md b/ja/docs/chapter_computational_complexity/time_complexity.md index 442f4d8de..8d5aac239 100644 --- a/ja/docs/chapter_computational_complexity/time_complexity.md +++ b/ja/docs/chapter_computational_complexity/time_complexity.md @@ -4,23 +4,23 @@ comments: true # 2.3   時間計算量 -実行時間は、アルゴリズムの効率を直感的に評価できます。アルゴリズムの実行時間を正確に推定するにはどうすればよいでしょうか? +実行時間はアルゴリズムの効率を直感的かつ正確に反映します。あるコードの実行時間を正確に見積もりたい場合、どのようにすればよいでしょうか? -1. **実行プラットフォームの決定**: これには、ハードウェア構成、プログラミング言語、システム環境などが含まれ、これらすべてがコードの実行効率に影響する可能性があります。 -2. **様々な計算操作の実行時間の評価**: 例えば、加算操作`+`は1 ns、乗算操作`*`は10 ns、印刷操作`print()`は5 nsなどかかる可能性があります。 -3. **コード内のすべての計算操作をカウント**: これらすべての操作の実行時間を合計すると、総実行時間が得られます。 +1. **実行プラットフォームを特定する**。ハードウェア構成、プログラミング言語、システム環境などが含まれ、これらの要因はいずれもコードの実行効率に影響します。 +2. **各種計算操作に必要な実行時間を評価する**。例えば加算 `+` には 1 ns 、乗算 `*` には 10 ns 、出力 `print()` には 5 ns などが必要です。 +3. **コード中のすべての計算操作を数える**。そして各操作の実行時間を合計することで、実行時間を得ます。 -例えば、入力サイズが$n$の以下のコードを考えてみましょう: +例えば次のコードでは、入力データサイズを $n$ とします: === "Python" ```python title="" - # 特定の操作プラットフォーム下で + # ある実行プラットフォーム上で def algorithm(n: int): a = 2 # 1 ns a = a + 1 # 1 ns a = a * 2 # 10 ns - # n回ループ + # n 回ループ for _ in range(n): # 1 ns print(0) # 5 ns ``` @@ -28,13 +28,13 @@ comments: true === "C++" ```cpp title="" - // 特定の操作プラットフォーム下で + // ある実行プラットフォーム上で void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns - // n回ループ - for (int i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される + // n 回ループ + for (int i = 0; i < n; i++) { // 1 ns cout << 0 << endl; // 5 ns } } @@ -43,13 +43,13 @@ comments: true === "Java" ```java title="" - // 特定の操作プラットフォーム下で + // ある実行プラットフォーム上で void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns - // n回ループ - for (int i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される + // n 回ループ + for (int i = 0; i < n; i++) { // 1 ns System.out.println(0); // 5 ns } } @@ -58,13 +58,13 @@ comments: true === "C#" ```csharp title="" - // 特定の操作プラットフォーム下で + // ある実行プラットフォーム上で void Algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns - // n回ループ - for (int i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される + // n 回ループ + for (int i = 0; i < n; i++) { // 1 ns Console.WriteLine(0); // 5 ns } } @@ -73,12 +73,12 @@ comments: true === "Go" ```go title="" - // 特定の操作プラットフォーム下で + // ある実行プラットフォーム上で func algorithm(n int) { a := 2 // 1 ns a = a + 1 // 1 ns a = a * 2 // 10 ns - // n回ループ + // n 回ループ for i := 0; i < n; i++ { // 1 ns fmt.Println(a) // 5 ns } @@ -88,12 +88,12 @@ comments: true === "Swift" ```swift title="" - // 特定の操作プラットフォーム下で + // ある実行プラットフォーム上で func algorithm(n: Int) { var a = 2 // 1 ns a = a + 1 // 1 ns a = a * 2 // 10 ns - // n回ループ + // n 回ループ for _ in 0 ..< n { // 1 ns print(0) // 5 ns } @@ -103,13 +103,13 @@ comments: true === "JS" ```javascript title="" - // 特定の操作プラットフォーム下で + // ある実行プラットフォーム上で function algorithm(n) { var a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns - // n回ループ - for(let i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される + // n 回ループ + for(let i = 0; i < n; i++) { // 1 ns console.log(0); // 5 ns } } @@ -118,13 +118,13 @@ comments: true === "TS" ```typescript title="" - // 特定の操作プラットフォーム下で + // ある実行プラットフォーム上で function algorithm(n: number): void { var a: number = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns - // n回ループ - for(let i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される + // n 回ループ + for(let i = 0; i < n; i++) { // 1 ns console.log(0); // 5 ns } } @@ -133,13 +133,13 @@ comments: true === "Dart" ```dart title="" - // 特定の操作プラットフォーム下で + // ある実行プラットフォーム上で void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns - // n回ループ - for (int i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される + // n 回ループ + for (int i = 0; i < n; i++) { // 1 ns print(0); // 5 ns } } @@ -148,13 +148,13 @@ comments: true === "Rust" ```rust title="" - // 特定の操作プラットフォーム下で + // ある実行プラットフォーム上で fn algorithm(n: i32) { let mut a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns - // n回ループ - for _ in 0..n { // 毎回i++で1 ns + // n 回ループ + for _ in 0..n { // 1 ns println!("{}", 0); // 5 ns } } @@ -163,13 +163,13 @@ comments: true === "C" ```c title="" - // 特定の操作プラットフォーム下で + // ある実行プラットフォーム上で void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns - // n回ループ - for (int i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される + // n 回ループ + for (int i = 0; i < n; i++) { // 1 ns printf("%d", 0); // 5 ns } } @@ -178,34 +178,58 @@ comments: true === "Kotlin" ```kotlin title="" - + // ある実行プラットフォーム上で + fun algorithm(n: Int) { + var a = 2 // 1 ns + a = a + 1 // 1 ns + a = a * 2 // 10 ns + // n 回ループ + for (i in 0.. 図 2-7   Time growth trend of algorithms a, b, and c

+ # アルゴリズム C の時間計算量:定数階 + def algorithm_C(n) + (0...1_000_000).each { puts 0 } + end + ``` -アルゴリズムの実行時間を直接カウントすることと比較して、時間計算量分析の特徴は何でしょうか? +以下の図は、上記 3 つのアルゴリズム関数の時間計算量を示しています。 -- **時間計算量はアルゴリズムの効率を効果的に評価します**。例えば、アルゴリズム`B`は線形に増加する実行時間を持ち、$n > 1$の時はアルゴリズム`A`より遅く、$n > 1,000,000$の時は`C`より遅くなります。実際、入力データサイズ$n$が十分に大きい限り、「定数オーダー」複雑度アルゴリズムは常に「線形オーダー」よりも優れており、時間増加傾向の本質を示しています。 -- **時間計算量分析はより直感的です**。明らかに、実行プラットフォームと計算操作の種類は実行時間増加の傾向とは無関係です。したがって、時間計算量分析では、すべての計算操作の実行時間を同じ「単位時間」として扱うことができ、「計算操作実行時間カウント」を「計算操作カウント」に単純化できます。これにより推定の複雑さが大幅に軽減されます。 -- **時間計算量には制限があります**。例えば、アルゴリズム`A`と`C`は同じ時間計算量を持ちますが、実際の実行時間は大きく異なる場合があります。同様に、アルゴリズム`B`は`C`よりも高い時間計算量を持ちますが、入力データサイズ$n$が小さい場合は明らかに優れています。これらの場合、時間計算量のみに基づいてアルゴリズムの効率を判断することは困難です。しかし、これらの問題にもかかわらず、複雑度分析はアルゴリズムの効率を評価するための最も効果的で一般的に使用される方法です。 +- アルゴリズム `A` には出力操作が $1$ 回しかなく、実行時間は $n$ が大きくなっても増加しません。このアルゴリズムの時間計算量を「定数階」と呼びます。 +- アルゴリズム `B` の出力操作は $n$ 回ループする必要があり、実行時間は $n$ の増加に対して線形に増加します。このアルゴリズムの時間計算量は「線形階」と呼ばれます。 +- アルゴリズム `C` の出力操作は $1000000$ 回ループする必要があり、実行時間は長いものの、入力データサイズ $n$ とは無関係です。したがって `C` の時間計算量は `A` と同じく、依然として「定数階」です。 -## 2.3.2   漸近上限 +![アルゴリズム A、B、C の時間増加傾向](time_complexity.assets/time_complexity_simple_example.png){ class="animation-figure" } -入力サイズが$n$の関数を考えてみましょう: +

図 2-7   アルゴリズム A、B、C の時間増加傾向

+ +アルゴリズムの実行時間を直接数える方法と比べて、時間計算量分析にはどのような特徴があるでしょうか? + +- **時間計算量はアルゴリズム効率を有効に評価できます**。例えばアルゴリズム `B` の実行時間は線形に増加するため、$n > 1$ ではアルゴリズム `A` より遅く、$n > 1000000$ ではアルゴリズム `C` より遅くなります。実際、入力データサイズ $n$ が十分に大きければ、「定数階」のアルゴリズムは必ず「線形階」のアルゴリズムより優れます。これが実行時間の増加傾向の意味です。 +- **時間計算量の見積もり方法はより簡潔です**。実行プラットフォームや計算操作の種類は、アルゴリズム実行時間の増加傾向とは無関係です。そのため時間計算量分析では、すべての計算操作の実行時間を同じ「単位時間」とみなしてよく、「計算操作の実行時間を数える」作業を「計算操作の個数を数える」作業へ簡略化できます。これにより見積もりの難易度は大きく下がります。 +- **時間計算量には一定の限界もあります**。例えばアルゴリズム `A` と `C` の時間計算量は同じでも、実際の実行時間には大きな差があります。同様に、アルゴリズム `B` の時間計算量は `C` より高いものの、入力データサイズ $n$ が小さい場合にはアルゴリズム `B` のほうが明らかに優れます。このような場合、時間計算量だけでアルゴリズム効率の高低を判断するのは難しいことがあります。もっとも、こうした問題があっても、複雑度分析は依然としてアルゴリズム効率を評価する最も有効で一般的な方法です。 + +## 2.3.2   関数の漸近上界 + +入力サイズが $n$ の次の関数を考えます: === "Python" @@ -457,7 +515,7 @@ $$ a = 1 # +1 a = a + 1 # +1 a = a * 2 # +1 - # n回ループ + # n 回ループ for i in range(n): # +1 print(0) # +1 ``` @@ -469,8 +527,8 @@ $$ int a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 - // n回ループ - for (int i = 0; i < n; i++) { // +1 (毎回i++が実行される) + // n 回ループ + for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行) cout << 0 << endl; // +1 } } @@ -483,8 +541,8 @@ $$ int a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 - // n回ループ - for (int i = 0; i < n; i++) { // +1 (毎回i++が実行される) + // n 回ループ + for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行) System.out.println(0); // +1 } } @@ -497,8 +555,8 @@ $$ int a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 - // n回ループ - for (int i = 0; i < n; i++) { // +1 (毎回i++が実行される) + // n 回ループ + for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行) Console.WriteLine(0); // +1 } } @@ -511,7 +569,7 @@ $$ a := 1 // +1 a = a + 1 // +1 a = a * 2 // +1 - // n回ループ + // n 回ループ for i := 0; i < n; i++ { // +1 fmt.Println(a) // +1 } @@ -525,7 +583,7 @@ $$ var a = 1 // +1 a = a + 1 // +1 a = a * 2 // +1 - // n回ループ + // n 回ループ for _ in 0 ..< n { // +1 print(0) // +1 } @@ -539,8 +597,8 @@ $$ var a = 1; // +1 a += 1; // +1 a *= 2; // +1 - // n回ループ - for(let i = 0; i < n; i++){ // +1 (毎回i++が実行される) + // n 回ループ + for(let i = 0; i < n; i++){ // +1(各反復で i ++ を実行) console.log(0); // +1 } } @@ -553,8 +611,8 @@ $$ var a: number = 1; // +1 a += 1; // +1 a *= 2; // +1 - // n回ループ - for(let i = 0; i < n; i++){ // +1 (毎回i++が実行される) + // n 回ループ + for(let i = 0; i < n; i++){ // +1(各反復で i ++ を実行) console.log(0); // +1 } } @@ -567,8 +625,8 @@ $$ int a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 - // n回ループ - for (int i = 0; i < n; i++) { // +1 (毎回i++が実行される) + // n 回ループ + for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行) print(0); // +1 } } @@ -582,8 +640,8 @@ $$ a = a + 1; // +1 a = a * 2; // +1 - // n回ループ - for _ in 0..n { // +1 (毎回i++が実行される) + // n 回ループ + for _ in 0..n { // +1(各反復で i ++ を実行) println!("{}", 0); // +1 } } @@ -596,8 +654,8 @@ $$ int a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 - // n回ループ - for (int i = 0; i < n; i++) { // +1 (毎回i++が実行される) + // n 回ループ + for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行) printf("%d", 0); // +1 } } @@ -606,55 +664,79 @@ $$ === "Kotlin" ```kotlin title="" - + fun algorithm(n: Int) { + var a = 1 // +1 + a = a + 1 // +1 + a = a * 2 // +1 + // n 回ループ + for (i in 0..ビッグO記法
として知られ、関数$T(n)$の漸近上限を表します。 +$T(n)$ は一次関数であり、実行時間の増加傾向が線形であることを示しています。したがってその時間計算量は線形階です。 -本質的に、時間計算量分析は「操作数$T(n)$」の漸近上限を見つけることです。それには正確な数学的定義があります。 +線形階の時間計算量を $O(n)$ と表します。この数学記号はビッグ $O$ 記法(big-$O$ notation)と呼ばれ、関数 $T(n)$ の漸近上界(asymptotic upper bound)を表します。 -!!! note "漸近上限" +時間計算量の分析は本質的に「操作回数 $T(n)$」の漸近上界を求めることであり、明確な数学的定義があります。 - すべての$n > n_0$に対して$T(n) \leq c \cdot f(n)$となるような正の実数$c$と$n_0$が存在する場合、$f(n)$は$T(n)$の漸近上限とみなされ、$T(n) = O(f(n))$と表記されます。 +!!! note "関数の漸近上界" -下図に示されているように、漸近上限の計算では、$n$が無限大に近づくにつれて、$T(n)$と$f(n)$が同じ増加オーダーを持ち、定数因子$c$のみが異なるような関数$f(n)$を見つけることが含まれます。 + 正の実数 $c$ と実数 $n_0$ が存在し、すべての $n > n_0$ について $T(n) \leq c \cdot f(n)$ が成り立つならば、$f(n)$ は $T(n)$ の漸近上界の 1 つであるとみなせます。これを $T(n) = O(f(n))$ と記します。 -![Asymptotic upper bound of a function](time_complexity.assets/asymptotic_upper_bound.png){ class="animation-figure" } +下図のように、漸近上界を求めるとは関数 $f(n)$ を探すことであり、$n$ が無限大へ近づくときに $T(n)$ と $f(n)$ が同じ増加オーダーにあり、定数係数 $c$ だけが異なる状態を表します。 -

図 2-8   Asymptotic upper bound of a function

+![関数の漸近上界](time_complexity.assets/asymptotic_upper_bound.png){ class="animation-figure" } -## 2.3.3   計算方法 +

図 2-8   関数の漸近上界

-漸近上限の概念は数学的に濃密に見えるかもしれませんが、今すぐ完全に理解する必要はありません。まず計算方法を理解し、時間をかけて練習し理解しましょう。 +## 2.3.3   求め方 -$f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし、漸近上限$f(n)$をどのように決定するのでしょうか?このプロセスには一般的に2つのステップが含まれます:操作数のカウントと漸近上限の決定です。 +漸近上界はやや数学色が強い概念ですが、完全に理解できていなくても心配はいりません。まずは求め方を押さえ、実践を重ねる中で徐々にその数学的意味をつかめば十分です。 -### 1.   ステップ1: 操作数のカウント +定義より、$f(n)$ が定まれば時間計算量 $O(f(n))$ が得られます。では、漸近上界 $f(n)$ をどのように決めればよいのでしょうか。大きく 2 段階あります。まず操作回数を数え、その後で漸近上界を判断します。 -このステップでは、コードを行ごとに確認します。しかし、$c \cdot f(n)$の定数$c$の存在により、**$T(n)$のすべての係数と定数項は無視できます**。この原理により、操作をカウントする際の簡略化技法が可能になります。 +### 1.   第 1 ステップ:操作回数を数える -1. **$T(n)$の定数項を無視します**。これらは$n$とは無関係であるため、時間計算量に影響しません。 -2. **すべての係数を省略します**。例えば、$2n$、$5n + 1$回などのループは、$n$の前の係数が時間計算量に影響しないため、$n$回に簡略化できます。 -3. **ネストしたループには乗算を使用します**。総操作数は各ループの操作数の積であり、ポイント1と2の簡略化技法を各ループレベルに適用します。 +コードについては、上から下へ 1 行ずつ数えれば十分です。しかし、前述の $c \cdot f(n)$ における定数係数 $c$ は任意に大きく取れるため、**操作回数 $T(n)$ に含まれるさまざまな係数や定数項は無視できます**。この原則から、次のような簡略化のコツが得られます。 -関数が与えられた場合、これらの技法を使用して操作をカウントできます: +1. **$T(n)$ 中の定数を無視する**。それらはすべて $n$ と無関係なので、時間計算量には影響しません。 +2. **すべての係数を省略する**。例えば $2n$ 回や $5n + 1$ 回のループは、いずれも $n$ 回と簡略化できます。$n$ の前の係数は時間計算量に影響しないからです。 +3. **ループが入れ子のときは乗算を使う**。総操作回数は外側のループと内側のループの操作回数の積に等しく、各ループ層には引き続き `1.` と `2.` のコツをそれぞれ適用できます。 + +次の関数では、上記のコツを使って操作回数を数えられます: === "Python" ```python title="" def algorithm(n: int): - a = 1 # +0 (技法1) - a = a + n # +0 (技法1) - # +n (技法2) + a = 1 # +0(コツ 1) + a = a + n # +0(コツ 1) + # +n(コツ 2) for i in range(5 * n + 1): print(0) - # +n*n (技法3) + # +n*n(コツ 3) for i in range(2 * n): for j in range(n + 1): print(0) @@ -664,13 +746,13 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ```cpp title="" void algorithm(int n) { - int a = 1; // +0 (技法1) - a = a + n; // +0 (技法1) - // +n (技法2) + int a = 1; // +0(コツ 1) + a = a + n; // +0(コツ 1) + // +n(コツ 2) for (int i = 0; i < 5 * n + 1; i++) { cout << 0 << endl; } - // +n*n (技法3) + // +n*n(コツ 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { cout << 0 << endl; @@ -683,13 +765,13 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ```java title="" void algorithm(int n) { - int a = 1; // +0 (技法1) - a = a + n; // +0 (技法1) - // +n (技法2) + int a = 1; // +0(コツ 1) + a = a + n; // +0(コツ 1) + // +n(コツ 2) for (int i = 0; i < 5 * n + 1; i++) { System.out.println(0); } - // +n*n (技法3) + // +n*n(コツ 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { System.out.println(0); @@ -702,13 +784,13 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ```csharp title="" void Algorithm(int n) { - int a = 1; // +0 (技法1) - a = a + n; // +0 (技法1) - // +n (技法2) + int a = 1; // +0(コツ 1) + a = a + n; // +0(コツ 1) + // +n(コツ 2) for (int i = 0; i < 5 * n + 1; i++) { Console.WriteLine(0); } - // +n*n (技法3) + // +n*n(コツ 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { Console.WriteLine(0); @@ -721,13 +803,13 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ```go title="" func algorithm(n int) { - a := 1 // +0 (技法1) - a = a + n // +0 (技法1) - // +n (技法2) + a := 1 // +0(コツ 1) + a = a + n // +0(コツ 1) + // +n(コツ 2) for i := 0; i < 5 * n + 1; i++ { fmt.Println(0) } - // +n*n (技法3) + // +n*n(コツ 3) for i := 0; i < 2 * n; i++ { for j := 0; j < n + 1; j++ { fmt.Println(0) @@ -740,13 +822,13 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ```swift title="" func algorithm(n: Int) { - var a = 1 // +0 (技法1) - a = a + n // +0 (技法1) - // +n (技法2) + var a = 1 // +0(コツ 1) + a = a + n // +0(コツ 1) + // +n(コツ 2) for _ in 0 ..< (5 * n + 1) { print(0) } - // +n*n (技法3) + // +n*n(コツ 3) for _ in 0 ..< (2 * n) { for _ in 0 ..< (n + 1) { print(0) @@ -759,13 +841,13 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ```javascript title="" function algorithm(n) { - let a = 1; // +0 (技法1) - a = a + n; // +0 (技法1) - // +n (技法2) + let a = 1; // +0(コツ 1) + a = a + n; // +0(コツ 1) + // +n(コツ 2) for (let i = 0; i < 5 * n + 1; i++) { console.log(0); } - // +n*n (技法3) + // +n*n(コツ 3) for (let i = 0; i < 2 * n; i++) { for (let j = 0; j < n + 1; j++) { console.log(0); @@ -778,13 +860,13 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ```typescript title="" function algorithm(n: number): void { - let a = 1; // +0 (技法1) - a = a + n; // +0 (技法1) - // +n (技法2) + let a = 1; // +0(コツ 1) + a = a + n; // +0(コツ 1) + // +n(コツ 2) for (let i = 0; i < 5 * n + 1; i++) { console.log(0); } - // +n*n (技法3) + // +n*n(コツ 3) for (let i = 0; i < 2 * n; i++) { for (let j = 0; j < n + 1; j++) { console.log(0); @@ -797,13 +879,13 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ```dart title="" void algorithm(int n) { - int a = 1; // +0 (技法1) - a = a + n; // +0 (技法1) - // +n (技法2) + int a = 1; // +0(コツ 1) + a = a + n; // +0(コツ 1) + // +n(コツ 2) for (int i = 0; i < 5 * n + 1; i++) { print(0); } - // +n*n (技法3) + // +n*n(コツ 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { print(0); @@ -816,15 +898,15 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ```rust title="" fn algorithm(n: i32) { - let mut a = 1; // +0 (技法1) - a = a + n; // +0 (技法1) + let mut a = 1; // +0(コツ 1) + a = a + n; // +0(コツ 1) - // +n (技法2) + // +n(コツ 2) for i in 0..(5 * n + 1) { println!("{}", 0); } - // +n*n (技法3) + // +n*n(コツ 3) for i in 0..(2 * n) { for j in 0..(n + 1) { println!("{}", 0); @@ -837,13 +919,13 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ```c title="" void algorithm(int n) { - int a = 1; // +0 (技法1) - a = a + n; // +0 (技法1) - // +n (技法2) + int a = 1; // +0(コツ 1) + a = a + n; // +0(コツ 1) + // +n(コツ 2) for (int i = 0; i < 5 * n + 1; i++) { printf("%d", 0); } - // +n*n (技法3) + // +n*n(コツ 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { printf("%d", 0); @@ -855,63 +937,93 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし === "Kotlin" ```kotlin title="" - + fun algorithm(n: Int) { + var a = 1 // +0(コツ 1) + a = a + n // +0(コツ 1) + // +n(コツ 2) + for (i in 0..<5 * n + 1) { + println(0) + } + // +n*n(コツ 3) + for (i in 0..<2 * n) { + for (j in 0.. 表: 異なる操作カウントに対する時間計算量

+

表 2-2   異なる操作回数に対応する時間計算量

-| 操作カウント $T(n)$ | 時間計算量 $O(f(n))$ | -| ---------------------- | ------------------------- | -| $100000$ | $O(1)$ | -| $3n + 2$ | $O(n)$ | -| $2n^2 + 3n + 2$ | $O(n^2)$ | -| $n^3 + 10000n^2$ | $O(n^3)$ | -| $2^n + 10000n^{10000}$ | $O(2^n)$ | +| 操作回数 $T(n)$ | 時間計算量 $O(f(n))$ | +| ---------------------- | -------------------- | +| $100000$ | $O(1)$ | +| $3n + 2$ | $O(n)$ | +| $2n^2 + 3n + 2$ | $O(n^2)$ | +| $n^3 + 10000n^2$ | $O(n^3)$ | +| $2^n + 10000n^{10000}$ | $O(2^n)$ |
-## 2.3.4   一般的な時間計算量の種類 +## 2.3.4   よくある種類 -入力データサイズを$n$としましょう。一般的な時間計算量の種類を下図に示し、低いものから高いものへと並べています: +入力データサイズを $n$ とすると、よくある時間計算量の種類は次図のとおりです(小さい順に並べています)。 $$ \begin{aligned} -& O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline -& \text{定数} < \text{対数} < \text{線形} < \text{線形対数} < \text{二次} < \text{指数} < \text{階乗} +O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline +\text{定数階} < \text{対数階} < \text{線形階} < \text{線形対数階} < \text{平方階} < \text{指数階} < \text{階乗階} \end{aligned} $$ -![Common types of time complexity](time_complexity.assets/time_complexity_common_types.png){ class="animation-figure" } +![よくある時間計算量の種類](time_complexity.assets/time_complexity_common_types.png){ class="animation-figure" } -

図 2-9   Common types of time complexity

+

図 2-9   よくある時間計算量の種類

-### 1.   定数オーダー $O(1)$ {data-toc-label="1.   定数オーダー"} +### 1.   定数階 $O(1)$ {data-toc-label="1.   定数階"} -定数オーダーは、操作数が入力データサイズ$n$とは無関係であることを意味します。以下の関数では、操作数`size`が大きい場合でも、$n$とは無関係であるため、時間計算量は$O(1)$のままです: +定数階の操作回数は入力データサイズ $n$ と無関係であり、$n$ が変化しても増減しません。 + +次の関数では、操作回数 `size` が大きい可能性はありますが、入力データサイズ $n$ とは無関係なので、時間計算量は依然として $O(1)$ です: === "Python" ```python title="time_complexity.py" def constant(n: int) -> int: - """定数複雑度""" + """定数階""" count = 0 size = 100000 for _ in range(size): @@ -922,7 +1034,7 @@ $$ === "C++" ```cpp title="time_complexity.cpp" - /* 定数計算量 */ + /* 定数階 */ int constant(int n) { int count = 0; int size = 100000; @@ -935,7 +1047,7 @@ $$ === "Java" ```java title="time_complexity.java" - /* 定数計算量 */ + /* 定数階 */ int constant(int n) { int count = 0; int size = 100000; @@ -948,72 +1060,153 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{Constant} + /* 定数階 */ + int Constant(int n) { + int count = 0; + int size = 100000; + for (int i = 0; i < size; i++) + count++; + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{constant} + /* 定数階 */ + func constant(n int) int { + count := 0 + size := 100000 + for i := 0; i < size; i++ { + count++ + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{constant} + /* 定数階 */ + func constant(n: Int) -> Int { + var count = 0 + let size = 100_000 + for _ in 0 ..< size { + count += 1 + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{constant} + /* 定数階 */ + function constant(n) { + let count = 0; + const size = 100000; + for (let i = 0; i < size; i++) count++; + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{constant} + /* 定数階 */ + function constant(n: number): number { + let count = 0; + const size = 100000; + for (let i = 0; i < size; i++) count++; + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{constant} + /* 定数階 */ + int constant(int n) { + int count = 0; + int size = 100000; + for (var i = 0; i < size; i++) { + count++; + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{constant} + /* 定数階 */ + fn constant(n: i32) -> i32 { + _ = n; + let mut count = 0; + let size = 100_000; + for _ in 0..size { + count += 1; + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{constant} + /* 定数階 */ + int constant(int n) { + int count = 0; + int size = 100000; + int i = 0; + for (int i = 0; i < size; i++) { + count++; + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{constant} + /* 定数階 */ + fun constant(n: Int): Int { + var count = 0 + val size = 100000 + for (i in 0.. + + +### 2.   線形階 $O(n)$ {data-toc-label="2.   線形階"} + +線形階の操作回数は入力データサイズ $n$ に対して線形に増加します。線形階は通常、単一ループに現れます: === "Python" ```python title="time_complexity.py" def linear(n: int) -> int: - """線形複雑度""" + """線形階""" count = 0 for _ in range(n): count += 1 @@ -1023,7 +1216,7 @@ $$ === "C++" ```cpp title="time_complexity.cpp" - /* 線形計算量 */ + /* 線形階 */ int linear(int n) { int count = 0; for (int i = 0; i < n; i++) @@ -1035,7 +1228,7 @@ $$ === "Java" ```java title="time_complexity.java" - /* 線形計算量 */ + /* 線形階 */ int linear(int n) { int count = 0; for (int i = 0; i < n; i++) @@ -1047,72 +1240,139 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{Linear} + /* 線形階 */ + int Linear(int n) { + int count = 0; + for (int i = 0; i < n; i++) + count++; + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{linear} + /* 線形階 */ + func linear(n int) int { + count := 0 + for i := 0; i < n; i++ { + count++ + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{linear} + /* 線形階 */ + func linear(n: Int) -> Int { + var count = 0 + for _ in 0 ..< n { + count += 1 + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{linear} + /* 線形階 */ + function linear(n) { + let count = 0; + for (let i = 0; i < n; i++) count++; + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{linear} + /* 線形階 */ + function linear(n: number): number { + let count = 0; + for (let i = 0; i < n; i++) count++; + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{linear} + /* 線形階 */ + int linear(int n) { + int count = 0; + for (var i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{linear} + /* 線形階 */ + fn linear(n: i32) -> i32 { + let mut count = 0; + for _ in 0..n { + count += 1; + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{linear} + /* 線形階 */ + int linear(int n) { + int count = 0; + for (int i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{linear} + /* 線形階 */ + fun linear(n: Int): Int { + var count = 0 + for (i in 0.. + + +配列走査や連結リスト走査などの操作の時間計算量はいずれも $O(n)$ であり、ここでの $n$ は配列または連結リストの長さです: === "Python" ```python title="time_complexity.py" def array_traversal(nums: list[int]) -> int: - """線形複雑度(配列の走査)""" + """線形時間(配列を走査)""" count = 0 - # ループ回数は配列の長さに比例する + # ループ回数は配列長に比例する for num in nums: count += 1 return count @@ -1121,10 +1381,10 @@ $$ === "C++" ```cpp title="time_complexity.cpp" - /* 線形計算量(配列の走査) */ + /* 線形時間(配列を走査) */ int arrayTraversal(vector &nums) { int count = 0; - // ループ回数は配列の長さに比例 + // ループ回数は配列長に比例する for (int num : nums) { count++; } @@ -1135,10 +1395,10 @@ $$ === "Java" ```java title="time_complexity.java" - /* 線形計算量(配列の走査) */ + /* 線形時間(配列を走査) */ int arrayTraversal(int[] nums) { int count = 0; - // ループ回数は配列の長さに比例 + // ループ回数は配列長に比例する for (int num : nums) { count++; } @@ -1149,76 +1409,170 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{ArrayTraversal} + /* 線形時間(配列を走査) */ + int ArrayTraversal(int[] nums) { + int count = 0; + // ループ回数は配列長に比例する + foreach (int num in nums) { + count++; + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{arrayTraversal} + /* 線形時間(配列を走査) */ + func arrayTraversal(nums []int) int { + count := 0 + // ループ回数は配列長に比例する + for range nums { + count++ + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{arrayTraversal} + /* 線形時間(配列を走査) */ + func arrayTraversal(nums: [Int]) -> Int { + var count = 0 + // ループ回数は配列長に比例する + for _ in nums { + count += 1 + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{arrayTraversal} + /* 線形時間(配列を走査) */ + function arrayTraversal(nums) { + let count = 0; + // ループ回数は配列長に比例する + for (let i = 0; i < nums.length; i++) { + count++; + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{arrayTraversal} + /* 線形時間(配列を走査) */ + function arrayTraversal(nums: number[]): number { + let count = 0; + // ループ回数は配列長に比例する + for (let i = 0; i < nums.length; i++) { + count++; + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{arrayTraversal} + /* 線形時間(配列を走査) */ + int arrayTraversal(List nums) { + int count = 0; + // ループ回数は配列長に比例する + for (var _num in nums) { + count++; + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{array_traversal} + /* 線形時間(配列を走査) */ + fn array_traversal(nums: &[i32]) -> i32 { + let mut count = 0; + // ループ回数は配列長に比例する + for _ in nums { + count += 1; + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{arrayTraversal} + /* 線形時間(配列を走査) */ + int arrayTraversal(int *nums, int n) { + int count = 0; + // ループ回数は配列長に比例する + for (int i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{arrayTraversal} + /* 線形時間(配列を走査) */ + fun arrayTraversal(nums: IntArray): Int { + var count = 0 + // ループ回数は配列長に比例する + for (num in nums) { + count++ + } + return count + } ``` === "Ruby" ```ruby title="time_complexity.rb" - [class]{}-[func]{array_traversal} + ### 線形階 ### + def linear(n) + count = 0 + (0...n).each { count += 1 } + count + end + + # ## 線形階(配列を走査)### + def array_traversal(nums) + count = 0 + + # ループ回数は配列長に比例する + for num in nums + count += 1 + end + + count + end ``` -**入力データサイズ$n$は入力データの種類に基づいて決定する必要があります**。例えば、最初の例では、$n$は入力データサイズを表し、2番目の例では、配列の長さ$n$がデータサイズです。 +??? pythontutor "コードの可視化" -### 3.   二次オーダー $O(n^2)$ {data-toc-label="3.   二次オーダー"} +
+ -二次オーダーは、操作数が入力データサイズ$n$の二乗に比例して増加することを意味します。二次オーダーは通常ネストしたループで現れ、外側と内側のループの両方が時間計算量$O(n)$を持ち、全体の複雑度は$O(n^2)$になります: +注意すべきなのは、**入力データサイズ $n$ は入力データの型に応じて具体的に定める必要がある**ということです。例えば 1 つ目の例では変数 $n$ が入力データサイズであり、2 つ目の例では配列長 $n$ がデータサイズです。 + +### 3.   平方階 $O(n^2)$ {data-toc-label="3.   平方階"} + +平方階の操作回数は入力データサイズ $n$ に対して二乗のオーダーで増加します。平方階は通常、入れ子ループに現れ、外側のループと内側のループの時間計算量がともに $O(n)$ であるため、全体の時間計算量は $O(n^2)$ になります: === "Python" ```python title="time_complexity.py" def quadratic(n: int) -> int: - """二次複雑度""" + """二乗階""" count = 0 - # ループ回数はデータサイズnの二乗に比例する + # ループ回数はデータサイズ n の二乗に比例する for i in range(n): for j in range(n): count += 1 @@ -1228,10 +1582,10 @@ $$ === "C++" ```cpp title="time_complexity.cpp" - /* 二次計算量 */ + /* 二乗階 */ int quadratic(int n) { int count = 0; - // ループ回数はデータサイズ n の二乗に比例 + // ループ回数はデータサイズ n の二乗に比例する for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; @@ -1244,10 +1598,10 @@ $$ === "Java" ```java title="time_complexity.java" - /* 二次計算量 */ + /* 二乗階 */ int quadratic(int n) { int count = 0; - // ループ回数はデータサイズ n の二乗に比例 + // ループ回数はデータサイズ n の二乗に比例する for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; @@ -1260,106 +1614,213 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{Quadratic} + /* 二乗階 */ + int Quadratic(int n) { + int count = 0; + // ループ回数はデータサイズ n の二乗に比例する + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{quadratic} + /* 二乗階 */ + func quadratic(n int) int { + count := 0 + // ループ回数はデータサイズ n の二乗に比例する + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + count++ + } + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{quadratic} + /* 二乗階 */ + func quadratic(n: Int) -> Int { + var count = 0 + // ループ回数はデータサイズ n の二乗に比例する + for _ in 0 ..< n { + for _ in 0 ..< n { + count += 1 + } + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{quadratic} + /* 二乗階 */ + function quadratic(n) { + let count = 0; + // ループ回数はデータサイズ n の二乗に比例する + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + count++; + } + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{quadratic} + /* 二乗階 */ + function quadratic(n: number): number { + let count = 0; + // ループ回数はデータサイズ n の二乗に比例する + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + count++; + } + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{quadratic} + /* 二乗階 */ + int quadratic(int n) { + int count = 0; + // ループ回数はデータサイズ n の二乗に比例する + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{quadratic} + /* 二乗階 */ + fn quadratic(n: i32) -> i32 { + let mut count = 0; + // ループ回数はデータサイズ n の二乗に比例する + for _ in 0..n { + for _ in 0..n { + count += 1; + } + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{quadratic} + /* 二乗階 */ + int quadratic(int n) { + int count = 0; + // ループ回数はデータサイズ n の二乗に比例する + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{quadratic} + /* 二乗階 */ + fun quadratic(n: Int): Int { + var count = 0 + // ループ回数はデータサイズ n の二乗に比例する + for (i in 0.. + -

図 2-10   Constant, linear, and quadratic order time complexities

+以下の図は、定数階・線形階・平方階の 3 種類の時間計算量を比較したものです。 -例えば、バブルソートでは、外側のループが$n - 1$回実行され、内側のループが$n-1$、$n-2$、...、$2$、$1$回実行され、平均$n / 2$回となり、時間計算量は$O((n - 1) n / 2) = O(n^2)$になります: +![定数階、線形階、平方階の時間計算量](time_complexity.assets/time_complexity_constant_linear_quadratic.png){ class="animation-figure" } + +

図 2-10   定数階、線形階、平方階の時間計算量

+ +バブルソートを例にとると、外側のループは $n - 1$ 回実行され、内側のループは $n-1$、$n-2$、$\dots$、$2$、$1$ 回実行され、平均すると $n / 2$ 回です。したがって時間計算量は $O((n - 1) n / 2) = O(n^2)$ となります: === "Python" ```python title="time_complexity.py" def bubble_sort(nums: list[int]) -> int: - """二次複雑度(バブルソート)""" + """二次時間(バブルソート)""" count = 0 # カウンタ - # 外側のループ: 未ソート範囲は [0, i] + # 外側のループ:未ソート区間は [0, i] for i in range(len(nums) - 1, 0, -1): - # 内側のループ: 未ソート範囲 [0, i] の最大要素を右端にスワップ + # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j in range(i): if nums[j] > nums[j + 1]: - # nums[j] と nums[j + 1] をスワップ + # nums[j] と nums[j + 1] を交換 tmp: int = nums[j] nums[j] = nums[j + 1] nums[j + 1] = tmp - count += 3 # 要素のスワップは3つの個別操作を含む + count += 3 # 要素交換には 3 回の単位操作が含まれる return count ``` === "C++" ```cpp title="time_complexity.cpp" - /* 二次計算量(バブルソート) */ + /* 二次時間(バブルソート) */ int bubbleSort(vector &nums) { - int count = 0; // カウンター - // 外側ループ:未ソート範囲は [0, i] + int count = 0; // カウンタ + // 外側のループ:未ソート区間は [0, i] for (int i = nums.size() - 1; i > 0; i--) { - // 内側ループ:未ソート範囲 [0, i] の最大要素を範囲の右端にスワップ + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { - // nums[j] と nums[j + 1] をスワップ + // nums[j] と nums[j + 1] を交換 int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; - count += 3; // 要素のスワップには3つの個別操作が含まれる + count += 3; // 要素交換には 3 回の単位操作が含まれる } } } @@ -1370,19 +1831,19 @@ $$ === "Java" ```java title="time_complexity.java" - /* 二次計算量(バブルソート) */ + /* 二次時間(バブルソート) */ int bubbleSort(int[] nums) { - int count = 0; // カウンター - // 外側ループ:未ソート範囲は [0, i] + int count = 0; // カウンタ + // 外側のループ:未ソート区間は [0, i] for (int i = nums.length - 1; i > 0; i--) { - // 内側ループ:未ソート範囲 [0, i] の最大要素を範囲の右端にスワップ + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { - // nums[j] と nums[j + 1] をスワップ + // nums[j] と nums[j + 1] を交換 int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; - count += 3; // 要素のスワップには3つの個別操作が含まれる + count += 3; // 要素交換には 3 回の単位操作が含まれる } } } @@ -1393,77 +1854,267 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{BubbleSort} + /* 二次時間(バブルソート) */ + int BubbleSort(int[] nums) { + int count = 0; // カウンタ + // 外側のループ:未ソート区間は [0, i] + for (int i = nums.Length - 1; i > 0; i--) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]); + count += 3; // 要素交換には 3 回の単位操作が含まれる + } + } + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{bubbleSort} + /* 二次時間(バブルソート) */ + func bubbleSort(nums []int) int { + count := 0 // カウンタ + // 外側のループ:未ソート区間は [0, i] + for i := len(nums) - 1; i > 0; i-- { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // nums[j] と nums[j + 1] を交換 + tmp := nums[j] + nums[j] = nums[j+1] + nums[j+1] = tmp + count += 3 // 要素交換には 3 回の単位操作が含まれる + } + } + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{bubbleSort} + /* 二次時間(バブルソート) */ + func bubbleSort(nums: inout [Int]) -> Int { + var count = 0 // カウンタ + // 外側のループ:未ソート区間は [0, i] + for i in nums.indices.dropFirst().reversed() { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // nums[j] と nums[j + 1] を交換 + let tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 // 要素交換には 3 回の単位操作が含まれる + } + } + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{bubbleSort} + /* 二次時間(バブルソート) */ + function bubbleSort(nums) { + let count = 0; // カウンタ + // 外側のループ:未ソート区間は [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 要素交換には 3 回の単位操作が含まれる + } + } + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{bubbleSort} + /* 二次時間(バブルソート) */ + function bubbleSort(nums: number[]): number { + let count = 0; // カウンタ + // 外側のループ:未ソート区間は [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 要素交換には 3 回の単位操作が含まれる + } + } + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{bubbleSort} + /* 二次時間(バブルソート) */ + int bubbleSort(List nums) { + int count = 0; // カウンタ + // 外側のループ:未ソート区間は [0, i] + for (var i = nums.length - 1; i > 0; i--) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (var j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 要素交換には 3 回の単位操作が含まれる + } + } + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{bubble_sort} + /* 二次時間(バブルソート) */ + fn bubble_sort(nums: &mut [i32]) -> i32 { + let mut count = 0; // カウンタ + + // 外側のループ:未ソート区間は [0, i] + for i in (1..nums.len()).rev() { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0..i { + if nums[j] > nums[j + 1] { + // nums[j] と nums[j + 1] を交換 + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 要素交換には 3 回の単位操作が含まれる + } + } + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{bubbleSort} + /* 二次時間(バブルソート) */ + int bubbleSort(int *nums, int n) { + int count = 0; // カウンタ + // 外側のループ:未ソート区間は [0, i] + for (int i = n - 1; i > 0; i--) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 要素交換には 3 回の単位操作が含まれる + } + } + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{bubbleSort} + /* 二次時間(バブルソート) */ + fun bubbleSort(nums: IntArray): Int { + var count = 0 // カウンタ + // 外側のループ:未ソート区間は [0, i] + for (i in nums.size - 1 downTo 1) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (j in 0.. nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + val temp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = temp + count += 3 // 要素交換には 3 回の単位操作が含まれる + } + } + } + return count + } ``` === "Ruby" ```ruby title="time_complexity.rb" - [class]{}-[func]{bubble_sort} + ### 平方階 ### + def quadratic(n) + count = 0 + + # ループ回数はデータサイズ n の二乗に比例する + for i in 0...n + for j in 0...n + count += 1 + end + end + + count + end + + # ## 平方階(バブルソート)### + def bubble_sort(nums) + count = 0 # カウンタ + + # 外側のループ:未ソート区間は [0, i] + for i in (nums.length - 1).downto(0) + # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0...i + if nums[j] > nums[j + 1] + # nums[j] と nums[j + 1] を交換 + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # 要素交換には 3 回の単位操作が含まれる + end + end + end + + count + end ``` -### 4.   指数オーダー $O(2^n)$ {data-toc-label="4.   指数オーダー"} +??? pythontutor "コードの可視化" -生物学的「細胞分裂」は指数オーダー増加の典型例です:1つの細胞から始まり、1回の分裂後に2つ、2回の分裂後に4つとなり、$n$回の分裂後に$2^n$個の細胞になります。 +
+ -下図とコードは細胞分裂プロセスをシミュレートし、時間計算量は$O(2^n)$です: +### 4.   指数階 $O(2^n)$ {data-toc-label="4.   指数階"} + +生物学における「細胞分裂」は指数階増加の典型例です。初期状態では細胞が $1$ 個あり、1 回分裂すると $2$ 個、2 回分裂すると $4$ 個となり、以下同様に、$n$ 回分裂すると $2^n$ 個の細胞になります。 + +以下の図とコードは細胞分裂の過程を模擬したもので、時間計算量は $O(2^n)$ です。なお、入力の $n$ は分裂回数を表し、戻り値 `count` は総分裂回数を表します。 === "Python" ```python title="time_complexity.py" def exponential(n: int) -> int: - """指数複雑度(ループ実装)""" + """指数時間(ループ実装)""" count = 0 base = 1 - # セルは毎回2つに分裂し、1, 2, 4, 8, ..., 2^(n-1) の数列を形成する + # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for _ in range(n): for _ in range(base): count += 1 @@ -1475,10 +2126,10 @@ $$ === "C++" ```cpp title="time_complexity.cpp" - /* 指数計算量(ループ実装) */ + /* 指数時間(ループ実装) */ int exponential(int n) { int count = 0, base = 1; - // セルは毎ラウンド2つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成 + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for (int i = 0; i < n; i++) { for (int j = 0; j < base; j++) { count++; @@ -1493,10 +2144,10 @@ $$ === "Java" ```java title="time_complexity.java" - /* 指数計算量(ループ実装) */ + /* 指数時間(ループ実装) */ int exponential(int n) { int count = 0, base = 1; - // セルは毎ラウンド2つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成 + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for (int i = 0; i < n; i++) { for (int j = 0; j < base; j++) { count++; @@ -1511,74 +2162,240 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{Exponential} + /* 指数時間(ループ実装) */ + int Exponential(int n) { + int count = 0, bas = 1; + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + for (int i = 0; i < n; i++) { + for (int j = 0; j < bas; j++) { + count++; + } + bas *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{exponential} + /* 指数時間(ループ実装) */ + func exponential(n int) int { + count, base := 0, 1 + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + for i := 0; i < n; i++ { + for j := 0; j < base; j++ { + count++ + } + base *= 2 + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{exponential} + /* 指数時間(ループ実装) */ + func exponential(n: Int) -> Int { + var count = 0 + var base = 1 + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + for _ in 0 ..< n { + for _ in 0 ..< base { + count += 1 + } + base *= 2 + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{exponential} + /* 指数時間(ループ実装) */ + function exponential(n) { + let count = 0, + base = 1; + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + for (let i = 0; i < n; i++) { + for (let j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{exponential} + /* 指数時間(ループ実装) */ + function exponential(n: number): number { + let count = 0, + base = 1; + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + for (let i = 0; i < n; i++) { + for (let j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{exponential} + /* 指数時間(ループ実装) */ + int exponential(int n) { + int count = 0, base = 1; + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + for (var i = 0; i < n; i++) { + for (var j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{exponential} + /* 指数時間(ループ実装) */ + fn exponential(n: i32) -> i32 { + let mut count = 0; + let mut base = 1; + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + for _ in 0..n { + for _ in 0..base { + count += 1 + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{exponential} + /* 指数時間(ループ実装) */ + int exponential(int n) { + int count = 0; + int bas = 1; + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + for (int i = 0; i < n; i++) { + for (int j = 0; j < bas; j++) { + count++; + } + bas *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{exponential} + /* 指数時間(ループ実装) */ + fun exponential(n: Int): Int { + var count = 0 + var base = 1 + // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + for (i in 0.. nums[j + 1] + # nums[j] と nums[j + 1] を交換 + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # 要素交換には 3 回の単位操作が含まれる + end + end + end + + count + end + + # ## 指数階(ループ実装)### + def exponential(n) + count, base = 0, 1 + + # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + (0...n).each do + (0...base).each { count += 1 } + base *= 2 + end + + # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + count + end ``` -![Exponential order time complexity](time_complexity.assets/time_complexity_exponential.png){ class="animation-figure" } +??? pythontutor "コードの可視化" -

図 2-11   Exponential order time complexity

+
+ -実際には、指数オーダーは再帰関数でよく現れます。例えば、以下のコードでは、再帰的に2つの半分に分割し、$n$回の分割後に停止します: +![指数階の時間計算量](time_complexity.assets/time_complexity_exponential.png){ class="animation-figure" } + +

図 2-11   指数階の時間計算量

+ +実際のアルゴリズムでも、指数階は再帰関数によく現れます。例えば次のコードでは、再帰的に 2 つへ分岐し、$n$ 回分裂した後に停止します: === "Python" ```python title="time_complexity.py" def exp_recur(n: int) -> int: - """指数複雑度(再帰実装)""" + """指数時間(再帰実装)""" if n == 1: return 1 return exp_recur(n - 1) + exp_recur(n - 1) + 1 @@ -1587,7 +2404,7 @@ $$ === "C++" ```cpp title="time_complexity.cpp" - /* 指数計算量(再帰実装) */ + /* 指数時間(再帰実装) */ int expRecur(int n) { if (n == 1) return 1; @@ -1598,7 +2415,7 @@ $$ === "Java" ```java title="time_complexity.java" - /* 指数計算量(再帰実装) */ + /* 指数時間(再帰実装) */ int expRecur(int n) { if (n == 1) return 1; @@ -1609,76 +2426,179 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{ExpRecur} + /* 指数時間(再帰実装) */ + int ExpRecur(int n) { + if (n == 1) return 1; + return ExpRecur(n - 1) + ExpRecur(n - 1) + 1; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{expRecur} + /* 指数時間(再帰実装) */ + func expRecur(n int) int { + if n == 1 { + return 1 + } + return expRecur(n-1) + expRecur(n-1) + 1 + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{expRecur} + /* 指数時間(再帰実装) */ + func expRecur(n: Int) -> Int { + if n == 1 { + return 1 + } + return expRecur(n: n - 1) + expRecur(n: n - 1) + 1 + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{expRecur} + /* 指数時間(再帰実装) */ + function expRecur(n) { + if (n === 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{expRecur} + /* 指数時間(再帰実装) */ + function expRecur(n: number): number { + if (n === 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{expRecur} + /* 指数時間(再帰実装) */ + int expRecur(int n) { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{exp_recur} + /* 指数時間(再帰実装) */ + fn exp_recur(n: i32) -> i32 { + if n == 1 { + return 1; + } + exp_recur(n - 1) + exp_recur(n - 1) + 1 + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{expRecur} + /* 指数時間(再帰実装) */ + int expRecur(int n) { + if (n == 1) + return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{expRecur} + /* 指数時間(再帰実装) */ + fun expRecur(n: Int): Int { + if (n == 1) { + return 1 + } + return expRecur(n - 1) + expRecur(n - 1) + 1 + } ``` === "Ruby" ```ruby title="time_complexity.rb" - [class]{}-[func]{exp_recur} + ### 平方階 ### + def quadratic(n) + count = 0 + + # ループ回数はデータサイズ n の二乗に比例する + for i in 0...n + for j in 0...n + count += 1 + end + end + + count + end + + # ## 平方階(バブルソート)### + def bubble_sort(nums) + count = 0 # カウンタ + + # 外側のループ:未ソート区間は [0, i] + for i in (nums.length - 1).downto(0) + # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0...i + if nums[j] > nums[j + 1] + # nums[j] と nums[j + 1] を交換 + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # 要素交換には 3 回の単位操作が含まれる + end + end + end + + count + end + + # ## 指数階(ループ実装)### + def exponential(n) + count, base = 0, 1 + + # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + (0...n).each do + (0...base).each { count += 1 } + base *= 2 + end + + # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + count + end + + # ## 指数階(再帰実装)### + def exp_recur(n) + return 1 if n == 1 + exp_recur(n - 1) + exp_recur(n - 1) + 1 + end ``` -指数オーダーの増加は極めて急速で、全数探索法(ブルートフォース、バックトラッキングなど)でよく見られます。大規模問題では、指数オーダーは受け入れられず、しばしば動的プログラミングや貪欲アルゴリズムが解決策として必要になります。 +??? pythontutor "コードの可視化" -### 5.   対数オーダー $O(\log n)$ {data-toc-label="5.   対数オーダー"} +
+ -指数オーダーとは対照的に、対数オーダーは「各ラウンドでサイズが半分になる」状況を反映します。入力データサイズが$n$の場合、各ラウンドでサイズが半分になるため、反復回数は$\log_2 n$で、これは$2^n$の逆関数です。 +指数階の増加は非常に速く、全探索法(ブルートフォース、バックトラッキングなど)によく見られます。データ規模が大きい問題では、指数階は受け入れられず、通常は動的計画法や貪欲法などを使って解く必要があります。 -下図とコードは「各ラウンドで半分にする」プロセスをシミュレートし、時間計算量は$O(\log_2 n)$で、一般的に$O(\log n)$と省略されます: +### 5.   対数階 $O(\log n)$ {data-toc-label="5.   対数階"} + +指数階とは逆に、対数階は「各ラウンドで半分になる」状況を表します。入力データサイズを $n$ とすると、各ラウンドで半減するため、ループ回数は $\log_2 n$、すなわち $2^n$ の逆関数になります。 + +以下の図とコードは、「各ラウンドで半分になる」過程を模擬したもので、時間計算量は $O(\log_2 n)$、簡潔には $O(\log n)$ と書きます: === "Python" ```python title="time_complexity.py" def logarithmic(n: int) -> int: - """対数複雑度(ループ実装)""" + """対数時間(ループ実装)""" count = 0 while n > 1: n = n / 2 @@ -1689,7 +2609,7 @@ $$ === "C++" ```cpp title="time_complexity.cpp" - /* 対数計算量(ループ実装) */ + /* 対数時間(ループ実装) */ int logarithmic(int n) { int count = 0; while (n > 1) { @@ -1703,7 +2623,7 @@ $$ === "Java" ```java title="time_complexity.java" - /* 対数計算量(ループ実装) */ + /* 対数時間(ループ実装) */ int logarithmic(int n) { int count = 0; while (n > 1) { @@ -1717,74 +2637,218 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{Logarithmic} + /* 対数時間(ループ実装) */ + int Logarithmic(int n) { + int count = 0; + while (n > 1) { + n /= 2; + count++; + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{logarithmic} + /* 対数時間(ループ実装) */ + func logarithmic(n int) int { + count := 0 + for n > 1 { + n = n / 2 + count++ + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{logarithmic} + /* 対数時間(ループ実装) */ + func logarithmic(n: Int) -> Int { + var count = 0 + var n = n + while n > 1 { + n = n / 2 + count += 1 + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{logarithmic} + /* 対数時間(ループ実装) */ + function logarithmic(n) { + let count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{logarithmic} + /* 対数時間(ループ実装) */ + function logarithmic(n: number): number { + let count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{logarithmic} + /* 対数時間(ループ実装) */ + int logarithmic(int n) { + int count = 0; + while (n > 1) { + n = n ~/ 2; + count++; + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{logarithmic} + /* 対数時間(ループ実装) */ + fn logarithmic(mut n: i32) -> i32 { + let mut count = 0; + while n > 1 { + n = n / 2; + count += 1; + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{logarithmic} + /* 対数時間(ループ実装) */ + int logarithmic(int n) { + int count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{logarithmic} + /* 対数時間(ループ実装) */ + fun logarithmic(n: Int): Int { + var n1 = n + var count = 0 + while (n1 > 1) { + n1 /= 2 + count++ + } + return count + } ``` === "Ruby" ```ruby title="time_complexity.rb" - [class]{}-[func]{logarithmic} + ### 平方階 ### + def quadratic(n) + count = 0 + + # ループ回数はデータサイズ n の二乗に比例する + for i in 0...n + for j in 0...n + count += 1 + end + end + + count + end + + # ## 平方階(バブルソート)### + def bubble_sort(nums) + count = 0 # カウンタ + + # 外側のループ:未ソート区間は [0, i] + for i in (nums.length - 1).downto(0) + # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0...i + if nums[j] > nums[j + 1] + # nums[j] と nums[j + 1] を交換 + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # 要素交換には 3 回の単位操作が含まれる + end + end + end + + count + end + + # ## 指数階(ループ実装)### + def exponential(n) + count, base = 0, 1 + + # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + (0...n).each do + (0...base).each { count += 1 } + base *= 2 + end + + # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + count + end + + # ## 指数階(再帰実装)### + def exp_recur(n) + return 1 if n == 1 + exp_recur(n - 1) + exp_recur(n - 1) + 1 + end + + # ## 対数階(ループ実装)### + def logarithmic(n) + count = 0 + + while n > 1 + n /= 2 + count += 1 + end + + count + end ``` -![Logarithmic order time complexity](time_complexity.assets/time_complexity_logarithmic.png){ class="animation-figure" } +??? pythontutor "コードの可視化" -

図 2-12   Logarithmic order time complexity

+
+ -指数オーダーと同様に、対数オーダーも再帰関数で頻繁に現れます。以下のコードは高さ$\log_2 n$の再帰木を形成します: +![対数階の時間計算量](time_complexity.assets/time_complexity_logarithmic.png){ class="animation-figure" } + +

図 2-12   対数階の時間計算量

+ +指数階と同様に、対数階も再帰関数によく現れます。次のコードは高さ $\log_2 n$ の再帰木を形成します: === "Python" ```python title="time_complexity.py" def log_recur(n: int) -> int: - """対数複雑度(再帰実装)""" + """対数時間(再帰実装)""" if n <= 1: return 0 return log_recur(n / 2) + 1 @@ -1793,7 +2857,7 @@ $$ === "C++" ```cpp title="time_complexity.cpp" - /* 対数計算量(再帰実装) */ + /* 対数時間(再帰実装) */ int logRecur(int n) { if (n <= 1) return 0; @@ -1804,7 +2868,7 @@ $$ === "Java" ```java title="time_complexity.java" - /* 対数計算量(再帰実装) */ + /* 対数時間(再帰実装) */ int logRecur(int n) { if (n <= 1) return 0; @@ -1815,87 +2879,209 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{LogRecur} + /* 対数時間(再帰実装) */ + int LogRecur(int n) { + if (n <= 1) return 0; + return LogRecur(n / 2) + 1; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{logRecur} + /* 対数時間(再帰実装) */ + func logRecur(n int) int { + if n <= 1 { + return 0 + } + return logRecur(n/2) + 1 + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{logRecur} + /* 対数時間(再帰実装) */ + func logRecur(n: Int) -> Int { + if n <= 1 { + return 0 + } + return logRecur(n: n / 2) + 1 + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{logRecur} + /* 対数時間(再帰実装) */ + function logRecur(n) { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{logRecur} + /* 対数時間(再帰実装) */ + function logRecur(n: number): number { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{logRecur} + /* 対数時間(再帰実装) */ + int logRecur(int n) { + if (n <= 1) return 0; + return logRecur(n ~/ 2) + 1; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{log_recur} + /* 対数時間(再帰実装) */ + fn log_recur(n: i32) -> i32 { + if n <= 1 { + return 0; + } + log_recur(n / 2) + 1 + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{logRecur} + /* 対数時間(再帰実装) */ + int logRecur(int n) { + if (n <= 1) + return 0; + return logRecur(n / 2) + 1; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{logRecur} + /* 対数時間(再帰実装) */ + fun logRecur(n: Int): Int { + if (n <= 1) + return 0 + return logRecur(n / 2) + 1 + } ``` === "Ruby" ```ruby title="time_complexity.rb" - [class]{}-[func]{log_recur} + ### 平方階 ### + def quadratic(n) + count = 0 + + # ループ回数はデータサイズ n の二乗に比例する + for i in 0...n + for j in 0...n + count += 1 + end + end + + count + end + + # ## 平方階(バブルソート)### + def bubble_sort(nums) + count = 0 # カウンタ + + # 外側のループ:未ソート区間は [0, i] + for i in (nums.length - 1).downto(0) + # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0...i + if nums[j] > nums[j + 1] + # nums[j] と nums[j + 1] を交換 + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # 要素交換には 3 回の単位操作が含まれる + end + end + end + + count + end + + # ## 指数階(ループ実装)### + def exponential(n) + count, base = 0, 1 + + # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する + (0...n).each do + (0...base).each { count += 1 } + base *= 2 + end + + # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + count + end + + # ## 指数階(再帰実装)### + def exp_recur(n) + return 1 if n == 1 + exp_recur(n - 1) + exp_recur(n - 1) + 1 + end + + # ## 対数階(ループ実装)### + def logarithmic(n) + count = 0 + + while n > 1 + n /= 2 + count += 1 + end + + count + end + + # ## 対数階(再帰実装)### + def log_recur(n) + return 0 unless n > 1 + log_recur(n / 2) + 1 + end ``` -対数オーダーは分割統治戦略に基づくアルゴリズムの典型で、「多くに分割」と「複雑な問題を単純化」するアプローチを体現しています。増加が遅く、定数オーダーの次に最も理想的な時間計算量です。 +??? pythontutor "コードの可視化" -!!! tip "$O(\log n)$の底は何ですか?" +
+ - 技術的には、「$m$に分割」は時間計算量$O(\log_m n)$に対応します。対数の底変更公式を使用すると、異なる対数複雑度を等価にできます: +対数階は分割統治に基づくアルゴリズムによく現れ、「1 つを複数に分ける」「複雑なものを単純化する」という考え方を体現しています。増加は緩やかで、定数階に次いで理想的な時間計算量です。 + +!!! tip "$O(\log n)$ の底は何か?" + + 正確には、「$m$ 個に分ける」場合に対応する時間計算量は $O(\log_m n)$ です。そして対数の底の変換公式により、底が異なっても同値な時間計算量が得られます: $$ O(\log_m n) = O(\log_k n / \log_k m) = O(\log_k n) $$ - これは、底$m$を複雑度に影響を与えることなく変更できることを意味します。したがって、しばしば底$m$を省略し、単に対数オーダーを$O(\log n)$と表記します。 + つまり、底 $m$ は複雑度に影響を与えずに変換できます。そのため通常は底 $m$ を省略し、対数階を単に $O(\log n)$ と記します。 -### 6.   線形対数オーダー $O(n \log n)$ {data-toc-label="6.   線形対数オーダー"} +### 6.   線形対数階 $O(n \log n)$ {data-toc-label="6.   線形対数階"} -線形対数オーダーはネストしたループでよく現れ、2つのループの複雑度がそれぞれ$O(\log n)$と$O(n)$です。関連するコードは以下の通りです: +線形対数階は入れ子ループによく現れ、2 層のループの時間計算量はそれぞれ $O(\log n)$ と $O(n)$ です。関連するコードは次のとおりです: === "Python" ```python title="time_complexity.py" def linear_log_recur(n: int) -> int: - """線形対数複雑度""" + """線形対数時間""" if n <= 1: return 1 - count: int = linear_log_recur(n // 2) + linear_log_recur(n // 2) + # 二つに分割すると、部分問題の規模は半分になる + count = linear_log_recur(n // 2) + linear_log_recur(n // 2) + # 現在の部分問題には n 個の操作が含まれる for _ in range(n): count += 1 return count @@ -1904,7 +3090,7 @@ $$ === "C++" ```cpp title="time_complexity.cpp" - /* 線形対数計算量 */ + /* 線形対数時間 */ int linearLogRecur(int n) { if (n <= 1) return 1; @@ -1919,7 +3105,7 @@ $$ === "Java" ```java title="time_complexity.java" - /* 線形対数計算量 */ + /* 線形対数時間 */ int linearLogRecur(int n) { if (n <= 1) return 1; @@ -1934,90 +3120,183 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{LinearLogRecur} + /* 線形対数時間 */ + int LinearLogRecur(int n) { + if (n <= 1) return 1; + int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{linearLogRecur} + /* 線形対数時間 */ + func linearLogRecur(n int) int { + if n <= 1 { + return 1 + } + count := linearLogRecur(n/2) + linearLogRecur(n/2) + for i := 0; i < n; i++ { + count++ + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{linearLogRecur} + /* 線形対数時間 */ + func linearLogRecur(n: Int) -> Int { + if n <= 1 { + return 1 + } + var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2) + for _ in stride(from: 0, to: n, by: 1) { + count += 1 + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{linearLogRecur} + /* 線形対数時間 */ + function linearLogRecur(n) { + if (n <= 1) return 1; + let count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (let i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{linearLogRecur} + /* 線形対数時間 */ + function linearLogRecur(n: number): number { + if (n <= 1) return 1; + let count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (let i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{linearLogRecur} + /* 線形対数時間 */ + int linearLogRecur(int n) { + if (n <= 1) return 1; + int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2); + for (var i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{linear_log_recur} + /* 線形対数時間 */ + fn linear_log_recur(n: i32) -> i32 { + if n <= 1 { + return 1; + } + let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2); + for _ in 0..n { + count += 1; + } + return count; + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{linearLogRecur} + /* 線形対数時間 */ + int linearLogRecur(int n) { + if (n <= 1) + return 1; + int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{linearLogRecur} + /* 線形対数時間 */ + fun linearLogRecur(n: Int): Int { + if (n <= 1) + return 1 + var count = linearLogRecur(n / 2) + linearLogRecur(n / 2) + for (i in 0.. 1 + + count = linear_log_recur(n / 2) + linear_log_recur(n / 2) + (0...n).each { count += 1 } + + count + end ``` -下図は線形対数オーダーがどのように生成されるかを示しています。二分木の各レベルには$n$個の操作があり、木には$\log_2 n + 1$レベルがあり、時間計算量は$O(n \log n)$になります。 +??? pythontutor "コードの可視化" -![Linear-logarithmic order time complexity](time_complexity.assets/time_complexity_logarithmic_linear.png){ class="animation-figure" } +
+ -

図 2-13   Linear-logarithmic order time complexity

+下図は線形対数階がどのように生じるかを示しています。二分木の各層の操作総数はすべて $n$ であり、木全体は $\log_2 n + 1$ 層あるため、時間計算量は $O(n \log n)$ です。 -主流のソートアルゴリズムは通常$O(n \log n)$の時間計算量を持ち、クイックソート、マージソート、ヒープソートなどがあります。 +![線形対数階の時間計算量](time_complexity.assets/time_complexity_logarithmic_linear.png){ class="animation-figure" } -### 7.   階乗オーダー $O(n!)$ {data-toc-label="7.   階乗オーダー"} +

図 2-13   線形対数階の時間計算量

-階乗オーダーは「全順列」の数学問題に対応します。$n$個の異なる要素が与えられた場合、可能な順列の総数は: +主なソートアルゴリズムの時間計算量は通常 $O(n \log n)$ であり、例えばクイックソート、マージソート、ヒープソートなどがあります。 + +### 7.   階乗階 $O(n!)$ {data-toc-label="7.   階乗階"} + +階乗階は、数学における「全順列」の問題に対応します。互いに重複しない $n$ 個の要素が与えられたとき、そのすべての並べ方を求めると、通り数は次のようになります: $$ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1 $$ -階乗は通常再帰を使用して実装されます。以下のコードと図に示されているように、第1レベルは$n$個の分岐に分割され、第2レベルは$n - 1$個の分岐に分割され、第$n$レベル後に停止します: +階乗は通常、再帰で実装されます。以下の図とコードのように、第 1 層では $n$ 個に分岐し、第 2 層では $n - 1$ 個に分岐し、以下同様に、第 $n$ 層で分岐が停止します: === "Python" ```python title="time_complexity.py" def factorial_recur(n: int) -> int: - """階乗複雑度(再帰実装)""" + """階乗時間(再帰実装)""" if n == 0: return 1 count = 0 - # 1つからnに分岐 + # 1個から n 個に分裂 for _ in range(n): count += factorial_recur(n - 1) return count @@ -2026,12 +3305,12 @@ $$ === "C++" ```cpp title="time_complexity.cpp" - /* 階乗計算量(再帰実装) */ + /* 階乗時間(再帰実装) */ int factorialRecur(int n) { if (n == 0) return 1; int count = 0; - // 1から n に分裂 + // 1個から n 個に分裂 for (int i = 0; i < n; i++) { count += factorialRecur(n - 1); } @@ -2042,12 +3321,12 @@ $$ === "Java" ```java title="time_complexity.java" - /* 階乗計算量(再帰実装) */ + /* 階乗時間(再帰実装) */ int factorialRecur(int n) { if (n == 0) return 1; int count = 0; - // 1から n に分裂 + // 1個から n 個に分裂 for (int i = 0; i < n; i++) { count += factorialRecur(n - 1); } @@ -2058,94 +3337,206 @@ $$ === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{FactorialRecur} + /* 階乗時間(再帰実装) */ + int FactorialRecur(int n) { + if (n == 0) return 1; + int count = 0; + // 1個から n 個に分裂 + for (int i = 0; i < n; i++) { + count += FactorialRecur(n - 1); + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{factorialRecur} + /* 階乗時間(再帰実装) */ + func factorialRecur(n int) int { + if n == 0 { + return 1 + } + count := 0 + // 1個から n 個に分裂 + for i := 0; i < n; i++ { + count += factorialRecur(n - 1) + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{factorialRecur} + /* 階乗時間(再帰実装) */ + func factorialRecur(n: Int) -> Int { + if n == 0 { + return 1 + } + var count = 0 + // 1個から n 個に分裂 + for _ in 0 ..< n { + count += factorialRecur(n: n - 1) + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{factorialRecur} + /* 階乗時間(再帰実装) */ + function factorialRecur(n) { + if (n === 0) return 1; + let count = 0; + // 1個から n 個に分裂 + for (let i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{factorialRecur} + /* 階乗時間(再帰実装) */ + function factorialRecur(n: number): number { + if (n === 0) return 1; + let count = 0; + // 1個から n 個に分裂 + for (let i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{factorialRecur} + /* 階乗時間(再帰実装) */ + int factorialRecur(int n) { + if (n == 0) return 1; + int count = 0; + // 1個から n 個に分裂 + for (var i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{factorial_recur} + /* 階乗時間(再帰実装) */ + fn factorial_recur(n: i32) -> i32 { + if n == 0 { + return 1; + } + let mut count = 0; + // 1個から n 個に分裂 + for _ in 0..n { + count += factorial_recur(n - 1); + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{factorialRecur} + /* 階乗時間(再帰実装) */ + int factorialRecur(int n) { + if (n == 0) + return 1; + int count = 0; + for (int i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{factorialRecur} + /* 階乗時間(再帰実装) */ + fun factorialRecur(n: Int): Int { + if (n == 0) + return 1 + var count = 0 + // 1個から n 個に分裂 + for (i in 0.. 1 + + count = linear_log_recur(n / 2) + linear_log_recur(n / 2) + (0...n).each { count += 1 } + + count + end + + # ## 階乗階(再帰実装)### + def factorial_recur(n) + return 1 if n == 0 + + count = 0 + # 1個から n 個に分裂 + (0...n).each { count += factorial_recur(n - 1) } + + count + end ``` -![Factorial order time complexity](time_complexity.assets/time_complexity_factorial.png){ class="animation-figure" } +??? pythontutor "コードの可視化" -

図 2-14   Factorial order time complexity

+
+ -階乗オーダーは指数オーダーよりもさらに速く増加することに注意してください。より大きな$n$値では受け入れられません。 +![階乗階の時間計算量](time_complexity.assets/time_complexity_factorial.png){ class="animation-figure" } -## 2.3.5   最悪、最良、平均時間計算量 +

図 2-14   階乗階の時間計算量

-**アルゴリズムの時間効率は固定されていないことが多く、入力データの分布に依存します**。長さ$n$の配列`nums`があり、$1$から$n$までの数で構成され、それぞれが一度だけ現れますが、ランダムにシャッフルされた順序であるとします。タスクは要素$1$のインデックスを返すことです。以下の結論を導けます: +注意すべき点として、$n \geq 4$ なら常に $n! > 2^n$ なので、階乗階は指数階よりもさらに速く増加し、$n$ が大きい場合にはやはり受け入れられません。 -- `nums = [?, ?, ..., 1]`の場合、つまり最後の要素が$1$の場合、配列の完全な走査が必要で、**最悪ケース時間計算量$O(n)$を達成します**。 -- `nums = [1, ?, ?, ...]`の場合、つまり最初の要素が$1$の場合、配列の長さに関係なく、さらなる走査は不要で、**最良ケース時間計算量$\Omega(1)$を達成します**。 +## 2.3.5   最悪・最良・平均時間計算量 -「最悪ケース時間計算量」は漸近上限に対応し、大きな$O$記法で表されます。対応して、「最良ケース時間計算量」は漸近下限に対応し、$\Omega$で表されます: +**アルゴリズムの時間効率は固定ではなく、入力データの分布に左右されることが多いです**。長さ $n$ の配列 `nums` を考えます。`nums` は $1$ から $n$ までの数字で構成され、各数字は 1 回だけ現れます。ただし要素の順序はランダムにシャッフルされており、目標は要素 $1$ のインデックスを返すことです。ここから次の結論が得られます。 + +- `nums = [?, ?, ..., 1]`、つまり末尾の要素が $1$ の場合は、配列全体を最後まで走査する必要があり、**最悪時間計算量 $O(n)$** になります。 +- `nums = [1, ?, ?, ...]`、つまり先頭要素が $1$ の場合は、配列がどれだけ長くてもそれ以上走査する必要がなく、**最良時間計算量 $\Omega(1)$** になります。 + +「最悪時間計算量」は関数の漸近上界に対応し、ビッグ $O$ 記法で表します。同様に、「最良時間計算量」は関数の漸近下界に対応し、$\Omega$ 記法で表します: === "Python" ```python title="worst_best_time_complexity.py" def random_numbers(n: int) -> list[int]: - """要素 1, 2, ..., n を含む配列を生成、順序はシャッフル""" - # 配列 nums = 1, 2, 3, ..., n を生成 + """要素が 1, 2, ..., n で順序がシャッフルされた配列を生成する""" + # 配列 nums =: 1, 2, 3, ..., n を生成する nums = [i for i in range(1, n + 1)] # 配列要素をランダムにシャッフル random.shuffle(nums) return nums def find_one(nums: list[int]) -> int: - """配列 nums で数値 1 のインデックスを検索""" + """配列 nums 内で数値 1 のインデックスを探す""" for i in range(len(nums)): - # 要素 1 が配列の最初にある場合、最良時間計算量 O(1) を達成 - # 要素 1 が配列の最後にある場合、最悪時間計算量 O(n) を達成 + # 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + # 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if nums[i] == 1: return i return -1 @@ -2154,25 +3545,25 @@ $$ === "C++" ```cpp title="worst_best_time_complexity.cpp" - /* 要素 {1, 2, ..., n} をランダムにシャッフルした配列を生成 */ + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ vector randomNumbers(int n) { vector nums(n); // 配列 nums = { 1, 2, 3, ..., n } を生成 for (int i = 0; i < n; i++) { nums[i] = i + 1; } - // システム時刻を使用してランダムシードを生成 + // システム時刻を使って乱数シードを生成する unsigned seed = chrono::system_clock::now().time_since_epoch().count(); // 配列要素をランダムにシャッフル shuffle(nums.begin(), nums.end(), default_random_engine(seed)); return nums; } - /* 配列 nums で数値1のインデックスを見つける */ + /* 配列 nums 内で数値 1 のインデックスを探す */ int findOne(vector &nums) { for (int i = 0; i < nums.size(); i++) { - // 要素1が配列の先頭にある場合、最良時間計算量 O(1) を達成 - // 要素1が配列の末尾にある場合、最悪時間計算量 O(n) を達成 + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if (nums[i] == 1) return i; } @@ -2183,7 +3574,7 @@ $$ === "Java" ```java title="worst_best_time_complexity.java" - /* 要素 {1, 2, ..., n} をランダムにシャッフルした配列を生成 */ + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ int[] randomNumbers(int n) { Integer[] nums = new Integer[n]; // 配列 nums = { 1, 2, 3, ..., n } を生成 @@ -2200,11 +3591,11 @@ $$ return res; } - /* 配列 nums で数値1のインデックスを見つける */ + /* 配列 nums 内で数値 1 のインデックスを探す */ int findOne(int[] nums) { for (int i = 0; i < nums.length; i++) { - // 要素1が配列の先頭にある場合、最良時間計算量 O(1) を達成 - // 要素1が配列の末尾にある場合、最悪時間計算量 O(n) を達成 + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if (nums[i] == 1) return i; } @@ -2215,91 +3606,308 @@ $$ === "C#" ```csharp title="worst_best_time_complexity.cs" - [class]{worst_best_time_complexity}-[func]{RandomNumbers} + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ + int[] RandomNumbers(int n) { + int[] nums = new int[n]; + // 配列 nums = { 1, 2, 3, ..., n } を生成 + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } - [class]{worst_best_time_complexity}-[func]{FindOne} + // 配列要素をランダムにシャッフル + for (int i = 0; i < nums.Length; i++) { + int index = new Random().Next(i, nums.Length); + (nums[i], nums[index]) = (nums[index], nums[i]); + } + return nums; + } + + /* 配列 nums 内で数値 1 のインデックスを探す */ + int FindOne(int[] nums) { + for (int i = 0; i < nums.Length; i++) { + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる + if (nums[i] == 1) + return i; + } + return -1; + } ``` === "Go" ```go title="worst_best_time_complexity.go" - [class]{}-[func]{randomNumbers} + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ + func randomNumbers(n int) []int { + nums := make([]int, n) + // 配列 nums = { 1, 2, 3, ..., n } を生成 + for i := 0; i < n; i++ { + nums[i] = i + 1 + } + // 配列要素をランダムにシャッフル + rand.Shuffle(len(nums), func(i, j int) { + nums[i], nums[j] = nums[j], nums[i] + }) + return nums + } - [class]{}-[func]{findOne} + /* 配列 nums 内で数値 1 のインデックスを探す */ + func findOne(nums []int) int { + for i := 0; i < len(nums); i++ { + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる + if nums[i] == 1 { + return i + } + } + return -1 + } ``` === "Swift" ```swift title="worst_best_time_complexity.swift" - [class]{}-[func]{randomNumbers} + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ + func randomNumbers(n: Int) -> [Int] { + // 配列 nums = { 1, 2, 3, ..., n } を生成 + var nums = Array(1 ... n) + // 配列要素をランダムにシャッフル + nums.shuffle() + return nums + } - [class]{}-[func]{findOne} + /* 配列 nums 内で数値 1 のインデックスを探す */ + func findOne(nums: [Int]) -> Int { + for i in nums.indices { + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる + if nums[i] == 1 { + return i + } + } + return -1 + } ``` === "JS" ```javascript title="worst_best_time_complexity.js" - [class]{}-[func]{randomNumbers} + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ + function randomNumbers(n) { + const nums = Array(n); + // 配列 nums = { 1, 2, 3, ..., n } を生成 + for (let i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 配列要素をランダムにシャッフル + for (let i = 0; i < n; i++) { + const r = Math.floor(Math.random() * (i + 1)); + const temp = nums[i]; + nums[i] = nums[r]; + nums[r] = temp; + } + return nums; + } - [class]{}-[func]{findOne} + /* 配列 nums 内で数値 1 のインデックスを探す */ + function findOne(nums) { + for (let i = 0; i < nums.length; i++) { + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる + if (nums[i] === 1) { + return i; + } + } + return -1; + } ``` === "TS" ```typescript title="worst_best_time_complexity.ts" - [class]{}-[func]{randomNumbers} + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ + function randomNumbers(n: number): number[] { + const nums = Array(n); + // 配列 nums = { 1, 2, 3, ..., n } を生成 + for (let i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 配列要素をランダムにシャッフル + for (let i = 0; i < n; i++) { + const r = Math.floor(Math.random() * (i + 1)); + const temp = nums[i]; + nums[i] = nums[r]; + nums[r] = temp; + } + return nums; + } - [class]{}-[func]{findOne} + /* 配列 nums 内で数値 1 のインデックスを探す */ + function findOne(nums: number[]): number { + for (let i = 0; i < nums.length; i++) { + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる + if (nums[i] === 1) { + return i; + } + } + return -1; + } ``` === "Dart" ```dart title="worst_best_time_complexity.dart" - [class]{}-[func]{randomNumbers} + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ + List randomNumbers(int n) { + final nums = List.filled(n, 0); + // 配列 nums = { 1, 2, 3, ..., n } を生成 + for (var i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 配列要素をランダムにシャッフル + nums.shuffle(); - [class]{}-[func]{findOne} + return nums; + } + + /* 配列 nums 内で数値 1 のインデックスを探す */ + int findOne(List nums) { + for (var i = 0; i < nums.length; i++) { + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる + if (nums[i] == 1) return i; + } + + return -1; + } ``` === "Rust" ```rust title="worst_best_time_complexity.rs" - [class]{}-[func]{random_numbers} + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ + fn random_numbers(n: i32) -> Vec { + // 配列 nums = { 1, 2, 3, ..., n } を生成 + let mut nums = (1..=n).collect::>(); + // 配列要素をランダムにシャッフル + nums.shuffle(&mut thread_rng()); + nums + } - [class]{}-[func]{find_one} + /* 配列 nums 内で数値 1 のインデックスを探す */ + fn find_one(nums: &[i32]) -> Option { + for i in 0..nums.len() { + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる + if nums[i] == 1 { + return Some(i); + } + } + None + } ``` === "C" ```c title="worst_best_time_complexity.c" - [class]{}-[func]{randomNumbers} + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ + int *randomNumbers(int n) { + // ヒープ領域にメモリを確保する(要素数 n、要素型 int の一次元可変長配列を作成) + int *nums = (int *)malloc(n * sizeof(int)); + // 配列 nums = { 1, 2, 3, ..., n } を生成 + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 配列要素をランダムにシャッフル + for (int i = n - 1; i > 0; i--) { + int j = rand() % (i + 1); + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } + return nums; + } - [class]{}-[func]{findOne} + /* 配列 nums 内で数値 1 のインデックスを探す */ + int findOne(int *nums, int n) { + for (int i = 0; i < n; i++) { + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる + if (nums[i] == 1) + return i; + } + return -1; + } ``` === "Kotlin" ```kotlin title="worst_best_time_complexity.kt" - [class]{}-[func]{randomNumbers} + /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ + fun randomNumbers(n: Int): Array { + val nums = IntArray(n) + // 配列 nums = { 1, 2, 3, ..., n } を生成 + for (i in 0..(n) + for (i in 0..): Int { + for (i in nums.indices) { + // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる + if (nums[i] == 1) + return i + } + return -1 + } ``` === "Ruby" ```ruby title="worst_best_time_complexity.rb" - [class]{}-[func]{random_numbers} + ### 1, 2, ..., n を要素とする配列を生成し、順序をシャッフルする ### + def random_numbers(n) + # 配列 nums =: 1, 2, 3, ..., n を生成する + nums = Array.new(n) { |i| i + 1 } + # 配列要素をランダムにシャッフル + nums.shuffle! + end - [class]{}-[func]{find_one} + ### 配列 nums 内の数値 1 のインデックスを探す ### + def find_one(nums) + for i in 0...nums.length + # 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる + # 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる + return i if nums[i] == 1 + end + + -1 + end ``` -最良ケース時間計算量は実際にはほとんど使用されないことに注意してください。通常は非常に低い確率でのみ達成可能で、誤解を招く可能性があるからです。**最悪ケース時間計算量はより実用的で、効率の安全値を提供し**、アルゴリズムを自信を持って使用できるようにします。 +??? pythontutor "コードの可視化" -上記の例から、最悪ケースと最良ケースの時間計算量は両方とも「特殊なデータ分布」下でのみ発生し、発生確率が小さく、アルゴリズムの実行効率を正確に反映しない可能性があることが明らかです。対照的に、**平均時間計算量はランダム入力データ下でのアルゴリズムの効率を反映でき**、$\Theta$記法で表されます。 +
+ -一部のアルゴリズムでは、ランダムデータ分布下での平均ケースを簡単に推定できます。例えば、前述の例では、入力配列がシャッフルされているため、要素$1$が任意のインデックスに現れる確率は等しいです。したがって、アルゴリズムの平均ループ数は配列長さの半分$n / 2$で、平均時間計算量は$\Theta(n / 2) = \Theta(n)$です。 +実際には、最良時間計算量を使うことはあまりありません。通常それが実現する確率はごく低く、誤解を招く可能性があるからです。**一方で最悪時間計算量はより実用的で、効率の安全側の目安を与えてくれる**ため、安心してアルゴリズムを使えます。 -しかし、より複雑なアルゴリズムの平均時間計算量を計算することは非常に困難です。データ分布下での全体的な数学的期待値を分析することが困難だからです。そのような場合、通常はアルゴリズムの効率を判断する基準として最悪ケース時間計算量を使用します。 +上の例から分かるように、最悪時間計算量と最良時間計算量は「特殊なデータ分布」でのみ現れ、その発生確率は低いことが多く、アルゴリズムの実行効率をそのまま正確に反映するわけではありません。それに対して、**平均時間計算量はランダム入力におけるアルゴリズムの実行効率を表せる**ため、$\Theta$ 記法で表します。 -!!! question "$\Theta$記号はなぜほとんど見られないのですか?" +一部のアルゴリズムでは、ランダムなデータ分布における平均的な状況を比較的簡単に求められます。例えば上の例では、入力配列はシャッフルされているため、要素 $1$ が任意のインデックスに現れる確率は等しいです。したがってアルゴリズムの平均ループ回数は配列長の半分 $n / 2$ となり、平均時間計算量は $\Theta(n / 2) = \Theta(n)$ です。 - おそらく$O$記法がより一般的に話されるため、平均時間計算量を表すためによく使用されます。しかし、厳密に言えば、この実践は正確ではありません。この本や他の資料で「平均時間計算量$O(n)$」のような表現に遭遇した場合は、直接$\Theta(n)$として理解してください。 +しかし、より複雑なアルゴリズムでは、平均時間計算量を計算するのはしばしば困難です。データ分布に対する全体の数学的期待値を分析するのが難しいからです。そのような場合、通常は最悪時間計算量をアルゴリズム効率の評価基準として用います。 + +!!! question "なぜ $\Theta$ 記号をあまり見かけないのか?" + + おそらく $O$ 記号のほうが口にしやすいため、平均時間計算量を表すのにもよく使われます。ただし厳密には、この用法は正確ではありません。本書や他の資料で「平均時間計算量 $O(n)$」のような表現を見かけた場合は、そのまま $\Theta(n)$ と理解してください。 diff --git a/ja/docs/chapter_data_structure/basic_data_types.md b/ja/docs/chapter_data_structure/basic_data_types.md index 4d4b9ac32..79f24583e 100644 --- a/ja/docs/chapter_data_structure/basic_data_types.md +++ b/ja/docs/chapter_data_structure/basic_data_types.md @@ -4,71 +4,71 @@ comments: true # 3.2   基本データ型 -コンピュータ内のデータについて考える際、テキスト、画像、動画、音声、3Dモデルなど、様々な形式が思い浮かびます。これらの組織的な形式は異なりますが、すべて様々な基本データ型から構成されています。 +コンピュータ内のデータについて考えるとき、テキスト、画像、動画、音声、3D モデルなど、さまざまな形態を思い浮かべます。これらのデータの構成形式はそれぞれ異なりますが、いずれも各種の基本データ型によって成り立っています。 -**基本データ型とは、CPUが直接操作できるもの**であり、アルゴリズムで直接使用されます。主に以下が含まれます。 +**基本データ型は CPU が直接演算できる型**であり、アルゴリズムの中で直接使われます。主なものは次のとおりです。 -- 整数型:`byte`、`short`、`int`、`long` -- 浮動小数点型:`float`、`double`、小数を表現するために使用 -- 文字型:`char`、様々な言語の文字、句読点、さらには絵文字を表現するために使用 -- ブール型:`bool`、「はい」または「いいえ」の判断を表現するために使用 +- 整数型 `byte`、`short`、`int`、`long` 。 +- 浮動小数点数型 `float`、`double` ,小数を表すために使います。 +- 文字型 `char` ,各言語の文字、句読点、さらには絵文字などを表すために使います。 +- 真偽値型 `bool` ,真か偽かの判定を表すために使います。 -**基本データ型は、コンピュータ内で二進形式で格納されます**。1つの二進桁は1ビットです。ほとんどの現代的なオペレーティングシステムでは、1バイトは8ビットで構成されています。 +**基本データ型はコンピュータ内で 2 進数の形で格納されます**。1 つの二進桁は $1$ ビットです。現代のほとんどのオペレーティングシステムでは、$1$ バイト(byte)は $8$ ビット(bit)で構成されます。 -基本データ型の値の範囲は、それらが占める空間のサイズに依存します。以下では、Javaを例に説明します。 +基本データ型の値域は、その型が占める領域の大きさによって決まります。以下では Java を例に取ります。 -- 整数型`byte`は1バイト = 8ビットを占め、$2^8$個の数値を表現できます。 -- 整数型`int`は4バイト = 32ビットを占め、$2^{32}$個の数値を表現できます。 +- 整数型 `byte` は $1$ バイト = $8$ ビットを占め、$2^{8}$ 個の数を表せます。 +- 整数型 `int` は $4$ バイト = $32$ ビットを占め、$2^{32}$ 個の数を表せます。 -以下の表は、Javaにおける様々な基本データ型が占める空間、値の範囲、デフォルト値を示しています。この表を暗記する必要はありませんが、一般的な理解を持ち、必要時に参照することをお勧めします。 +下表は、Java における各種基本データ型の使用領域、値域、デフォルト値を示したものです。この表を丸暗記する必要はなく、大まかに理解しておけば十分であり、必要になったときに参照すればかまいません。 -

表 3-1   基本データ型が占める空間と値の範囲

+

表 3-1   基本データ型の使用領域と値域

-| 型 | シンボル | 占有空間 | 最小値 | 最大値 | デフォルト値 | -| ------- | -------- | -------- | ------------------------ | ----------------------- | -------------- | -| 整数 | `byte` | 1バイト | $-2^7$ ($-128$) | $2^7 - 1$ ($127$) | 0 | -| | `short` | 2バイト | $-2^{15}$ | $2^{15} - 1$ | 0 | -| | `int` | 4バイト | $-2^{31}$ | $2^{31} - 1$ | 0 | -| | `long` | 8バイト | $-2^{63}$ | $2^{63} - 1$ | 0 | -| 浮動小数点 | `float` | 4バイト | $1.175 \times 10^{-38}$ | $3.403 \times 10^{38}$ | $0.0\text{f}$ | -| | `double` | 8バイト | $2.225 \times 10^{-308}$ | $1.798 \times 10^{308}$ | 0.0 | -| 文字 | `char` | 2バイト | 0 | $2^{16} - 1$ | 0 | -| ブール | `bool` | 1バイト | $\text{false}$ | $\text{true}$ | $\text{false}$ | +| 型 | 記号 | 使用領域 | 最小値 | 最大値 | デフォルト値 | +| ------ | -------- | -------- | ------------------------ | ----------------------- | -------------- | +| 整数 | `byte` | 1 バイト | $-2^7$ ($-128$) | $2^7 - 1$ ($127$) | $0$ | +| | `short` | 2 バイト | $-2^{15}$ | $2^{15} - 1$ | $0$ | +| | `int` | 4 バイト | $-2^{31}$ | $2^{31} - 1$ | $0$ | +| | `long` | 8 バイト | $-2^{63}$ | $2^{63} - 1$ | $0$ | +| 浮動小数点数 | `float` | 4 バイト | $1.175 \times 10^{-38}$ | $3.403 \times 10^{38}$ | $0.0\text{f}$ | +| | `double` | 8 バイト | $2.225 \times 10^{-308}$ | $1.798 \times 10^{308}$ | $0.0$ | +| 文字 | `char` | 2 バイト | $0$ | $2^{16} - 1$ | $0$ | +| 真偽値 | `bool` | 1 バイト | $\text{false}$ | $\text{true}$ | $\text{false}$ |
-上記の表はJavaの基本データ型に特有であることにご注意ください。すべてのプログラミング言語には独自のデータ型定義があり、占有空間、値の範囲、デフォルト値が異なる場合があります。 +注意してください。上表は Java の基本データ型に対するものです。各プログラミング言語にはそれぞれ独自のデータ型定義があり、使用領域、値域、デフォルト値は異なる場合があります。 -- Pythonでは、整数型`int`は任意のサイズになることができ、利用可能なメモリによってのみ制限されます。浮動小数点`float`は倍精度64ビットです。`char`型は存在せず、単一文字は実際には長さ1の文字列`str`です。 -- CおよびC++では基本データ型のサイズが指定されておらず、実装とプラットフォームによって異なります。上記の表はLP64[データモデル](https://en.cppreference.com/w/cpp/language/types#Properties)に従っており、LinuxやmacOSを含むUnix 64ビットオペレーティングシステムで使用されています。 -- CおよびC++における`char`のサイズは1バイトですが、ほとんどのプログラミング言語では、特定の文字エンコーディング方法に依存し、詳細は「文字エンコーディング」の章で説明されています。 -- ブール値の表現には1ビット(0または1)のみが必要ですが、通常はメモリ内に1バイトとして格納されます。これは、現代のコンピュータCPUが通常1バイトを最小のアドレス可能なメモリ単位として使用するためです。 +- Python では、整数型 `int` は利用可能なメモリに制限されるだけで任意の大きさを取れます。浮動小数点数 `float` は倍精度 64 ビットです。`char` 型はなく、1 文字は実際には長さ 1 の文字列 `str` です。 +- C と C++ では基本データ型の大きさは明確に規定されておらず、実装やプラットフォームによって異なります。上表は LP64 [データモデル](https://en.cppreference.com/w/cpp/language/types#Properties) に従っており、Linux や macOS を含む Unix 系 64 ビット OS で用いられています。 +- `char` の大きさは C と C++ では 1 バイトですが、多くのプログラミング言語では採用する文字エンコーディング方式によって決まります。詳しくは「文字エンコーディング」の章を参照してください。 +- 真偽値を表すのに必要なのは 1 ビット($0$ または $1$)だけですが、メモリ上では通常 1 バイトとして格納されます。これは、現代のコンピュータ CPU が通常 1 バイトを最小のアドレス指定可能なメモリ単位としているためです。 -では、基本データ型とデータ構造の関係は何でしょうか?データ構造とは、コンピュータ内でデータを組織化し格納する方法であることを知っています。ここでの焦点は「データ」ではなく「構造」です。 +では、基本データ型とデータ構造の間にはどのような関係があるのでしょうか。データ構造とは、コンピュータ内でデータを組織し格納する方法のことです。この言葉で主役なのは「データ」ではなく「構造」です。 -「数値の列」を表現したい場合、自然に配列の使用を考えます。これは、配列の線形構造が数値の隣接性と順序性を表現できるためですが、格納される内容が整数`int`、小数`float`、文字`char`のいずれであっても、「データ構造」とは無関係です。 +「数字の並び」を表したいなら、自然に配列の使用を思い浮かべるでしょう。これは、配列の線形構造が数字どうしの隣接関係や順序関係を表せるからです。しかし、格納する内容が整数 `int` なのか、小数 `float` なのか、文字 `char` なのかは、「データ構造」とは関係ありません。 -言い換えると、**基本データ型はデータの「内容型」を提供し、データ構造はデータの「組織化方法」を提供します**。例えば、以下のコードでは、同じデータ構造(配列)を使用して、`int`、`float`、`char`、`bool`などの異なる基本データ型を格納し表現しています。 +言い換えると、**基本データ型はデータの「内容の型」を提供し、データ構造はデータの「組織方法」を提供します**。たとえば次のコードでは、同じデータ構造(配列)を使って `int`、`float`、`char`、`bool` など異なる基本データ型を格納・表現しています。 === "Python" ```python title="" - # 様々な基本データ型を使用して配列を初期化 + # さまざまな基本データ型で配列を初期化する numbers: list[int] = [0] * 5 decimals: list[float] = [0.0] * 5 - # Pythonの文字は実際には長さ1の文字列 + # Python の文字は実際には長さ 1 の文字列 characters: list[str] = ['0'] * 5 bools: list[bool] = [False] * 5 - # Pythonのリストは様々な基本データ型とオブジェクト参照を自由に格納可能 + # Python のリストはさまざまな基本データ型とオブジェクト参照を自由に格納できる data = [0, 0.0, 'a', False, ListNode(0)] ``` === "C++" ```cpp title="" - // 様々な基本データ型を使用して配列を初期化 + // さまざまな基本データ型で配列を初期化する int numbers[5]; float decimals[5]; char characters[5]; @@ -78,7 +78,7 @@ comments: true === "Java" ```java title="" - // 様々な基本データ型を使用して配列を初期化 + // さまざまな基本データ型で配列を初期化する int[] numbers = new int[5]; float[] decimals = new float[5]; char[] characters = new char[5]; @@ -88,7 +88,7 @@ comments: true === "C#" ```csharp title="" - // 様々な基本データ型を使用して配列を初期化 + // さまざまな基本データ型で配列を初期化する int[] numbers = new int[5]; float[] decimals = new float[5]; char[] characters = new char[5]; @@ -98,7 +98,7 @@ comments: true === "Go" ```go title="" - // 様々な基本データ型を使用して配列を初期化 + // さまざまな基本データ型で配列を初期化する var numbers = [5]int{} var decimals = [5]float64{} var characters = [5]byte{} @@ -108,7 +108,7 @@ comments: true === "Swift" ```swift title="" - // 様々な基本データ型を使用して配列を初期化 + // さまざまな基本データ型で配列を初期化する let numbers = Array(repeating: 0, count: 5) let decimals = Array(repeating: 0.0, count: 5) let characters: [Character] = Array(repeating: "a", count: 5) @@ -118,14 +118,14 @@ comments: true === "JS" ```javascript title="" - // JavaScriptの配列は様々な基本データ型とオブジェクトを自由に格納可能 + // JavaScript の配列はさまざまな基本データ型とオブジェクトを自由に格納できる const array = [0, 0.0, 'a', false]; ``` === "TS" ```typescript title="" - // 様々な基本データ型を使用して配列を初期化 + // さまざまな基本データ型で配列を初期化する const numbers: number[] = []; const characters: string[] = []; const bools: boolean[] = []; @@ -134,7 +134,7 @@ comments: true === "Dart" ```dart title="" - // 様々な基本データ型を使用して配列を初期化 + // さまざまな基本データ型で配列を初期化する List numbers = List.filled(5, 0); List decimals = List.filled(5, 0.0); List characters = List.filled(5, 'a'); @@ -144,9 +144,9 @@ comments: true === "Rust" ```rust title="" - // 様々な基本データ型を使用して配列を初期化 + // さまざまな基本データ型で配列を初期化する let numbers: Vec = vec![0; 5]; - let decimals: Vec = vec![0.0, 5]; + let decimals: Vec = vec![0.0; 5]; let characters: Vec = vec!['0'; 5]; let bools: Vec = vec![false; 5]; ``` @@ -154,7 +154,7 @@ comments: true === "C" ```c title="" - // 様々な基本データ型を使用して配列を初期化 + // さまざまな基本データ型で配列を初期化する int numbers[10]; float decimals[10]; char characters[10]; @@ -164,6 +164,20 @@ comments: true === "Kotlin" ```kotlin title="" - + // さまざまな基本データ型で配列を初期化する + val numbers = IntArray(5) + val decinals = FloatArray(5) + val characters = CharArray(5) + val bools = BooleanArray(5) ``` +=== "Ruby" + + ```ruby title="" + # Ruby のリストはさまざまな基本データ型とオブジェクト参照を自由に格納できる + data = [0, 0.0, 'a', false, ListNode(0)] + ``` + +??? pythontutor "実行の可視化" + + https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false diff --git a/ja/docs/chapter_data_structure/character_encoding.md b/ja/docs/chapter_data_structure/character_encoding.md index 9d749d85e..dd35e08f8 100644 --- a/ja/docs/chapter_data_structure/character_encoding.md +++ b/ja/docs/chapter_data_structure/character_encoding.md @@ -4,94 +4,94 @@ comments: true # 3.4   文字エンコーディング * -コンピュータシステムでは、すべてのデータが二進形式で格納され、`char`も例外ではありません。文字を表現するために、各文字と二進数の一対一のマッピングを定義する「文字セット」を開発する必要があります。文字セットがあれば、コンピュータは表を参照して二進数を文字に変換できます。 +コンピュータでは、すべてのデータは二進数の形で保存されており、文字 `char` も例外ではありません。文字を表すためには、「文字セット」を定義し、各文字と二進数の間の一対一の対応関係を定める必要があります。文字セットがあれば、コンピュータは対応表を参照して二進数から文字への変換を行えます。 -## 3.4.1   ASCII文字セット +## 3.4.1   ASCII 文字セット -ASCIIコードは最も初期の文字セットの一つで、正式にはAmerican Standard Code for Information Interchangeとして知られています。7つの二進桁(1バイトの下位7ビット)を使用して文字を表現し、最大128種類の異なる文字を表現できます。以下の図に示すように、ASCIIには英語の大文字と小文字、0〜9の数字、様々な句読点、特定の制御文字(改行やタブなど)が含まれています。 +ASCII コードは最も早く登場した文字セットで、その正式名称は American Standard Code for Information Interchange(米国標準情報交換コード)です。これは 7 ビットの二進数(1 バイトの下位 7 ビット)で 1 文字を表し、最大で 128 種類の異なる文字を表現できます。下図のように、ASCII コードには英字の大文字と小文字、数字 0 ~ 9、いくつかの句読点、そしていくつかの制御文字(改行やタブなど)が含まれます。 -![ASCIIコード](character_encoding.assets/ascii_table.png){ class="animation-figure" } +![ASCII コード](character_encoding.assets/ascii_table.png){ class="animation-figure" } -

図 3-6   ASCIIコード

+

図 3-6   ASCII コード

-しかし、**ASCIIは英語の文字のみを表現できます**。コンピュータのグローバル化に伴い、より多くの言語を表現するためにEASCIIと呼ばれる文字セットが開発されました。ASCIIの7ビット構造から8ビットに拡張し、256文字の表現を可能にしました。 +しかし、**ASCII コードで表現できるのは英語だけです**。コンピュータのグローバル化に伴い、より多くの言語を表せる EASCII 文字セットが生まれました。これは ASCII の 7 ビットを 8 ビットへ拡張したもので、256 種類の異なる文字を表現できます。 -世界的に、様々な地域固有のEASCII文字セットが導入されました。これらのセットの最初の128文字はASCIIと一致していますが、残りの128文字は異なる言語の要件に対応するために異なって定義されています。 +世界では、さまざまな地域に適した EASCII 文字セットが次々に登場しました。これらの文字セットでは、前半の 128 文字は ASCII コードで統一され、後半の 128 文字は各言語の要件に合わせて個別に定義されています。 -## 3.4.2   GBK文字セット +## 3.4.2   GBK 文字セット -後に、**EASCIIでも多くの言語の文字要件を満たすことができない**ことが判明しました。例えば、中国語には約10万の漢字があり、そのうち数千が定期的に使用されています。1980年、中国標準化委員会は6763の中国語文字を含むGB2312文字セットを発表し、中国語のコンピュータ処理ニーズを本質的に満たしました。 +その後、人々は**EASCII コードでも多くの言語に必要な文字数を満たせない**ことに気づきました。たとえば漢字は 10 万字近くあり、日常的に使うものだけでも数千字あります。中国国家標準総局は 1980 年に GB2312 文字セットを公開し、6763 字の漢字を収録して、漢字のコンピュータ処理の基本的な需要を満たしました。 -しかし、GB2312は一部の稀少文字や繁体字を処理できませんでした。GBK文字セットはGB2312を拡張し、21886の中国語文字を含んでいます。GBKエンコーディングスキームでは、ASCII文字は1バイトで表現され、中国語文字は2バイトを使用します。 +しかし、GB2312 では一部の珍しい字や繁体字を扱えません。GBK 文字セットは GB2312 を基に拡張されたもので、合計 21886 字の漢字を収録しています。GBK のエンコーディング方式では、ASCII 文字は 1 バイト、漢字は 2 バイトで表されます。 -## 3.4.3   Unicode文字セット +## 3.4.3   Unicode 文字セット -コンピュータ技術の急速な発展と多数の文字セットおよびエンコーディング標準により、数多くの問題が発生しました。一方では、これらの文字セットは一般的に特定の言語の文字のみを定義し、多言語環境では適切に機能できませんでした。他方では、同じ言語に対する複数の文字セット標準の存在により、異なるエンコーディング標準を使用するコンピュータ間で情報交換を行う際に文字化けが発生しました。 +コンピュータ技術が急速に発展するにつれて、文字セットと符号化規格は百花繚乱の状態となり、それに伴って多くの問題も生じました。一方では、これらの文字セットは通常、特定の言語の文字しか定義しておらず、多言語環境では正常に動作できませんでした。もう一方では、同じ言語にも複数の文字セット規格が存在し、2 台のコンピュータが異なる符号化規格を使っていると、情報伝達の際に文字化けが発生しました。 -当時の研究者たちは考えました:**世界のすべての言語と記号を含む包括的な文字セットが開発されれば、言語横断環境と文字化けに関連する問題を解決できるのではないでしょうか?** このアイデアにインスパイアされて、広範囲な文字セットであるUnicodeが誕生しました。 +当時の研究者たちはこう考えました。**十分に完全な文字セットを打ち出して、世界中のあらゆる言語と記号をそこに収録すれば、多言語環境や文字化けの問題を解決できるのではないか**。この発想に後押しされて、大規模で包括的な文字セット Unicode が誕生しました。 -Unicodeは中国語で「统一码」(統一コード)と呼ばれ、理論的に100万文字以上を収容できます。世界中のすべての文字を単一のセットに組み込み、様々な言語の処理と表示のための汎用文字セットを提供し、異なるエンコーディング標準による文字化けの問題を減らすことを目指しています。 +Unicode の中国語名は「統一コード」であり、理論上は 100 万を超える文字を収容できます。Unicode は世界中の文字を 1 つの文字セットに統合することを目指し、さまざまな言語の文字を処理・表示できる汎用文字セットを提供することで、符号化規格の違いによる文字化けを減らそうとしています。 -1991年のリリース以来、Unicodeは新しい言語と文字を含むよう継続的に拡張されています。2022年9月現在、Unicodeには149,186文字が含まれており、様々な言語の文字、記号、さらには絵文字も含まれています。広大なUnicode文字セットでは、一般的に使用される文字は2バイトを占有し、一部の稀少な文字は3バイトまたは4バイトを占有する場合があります。 +1991 年の公開以来、Unicode は新しい言語と文字を継続的に拡充してきました。2022 年 9 月時点で、Unicode にはすでに 149186 文字が含まれており、各種言語の文字、記号、さらには絵文字まで収録されています。巨大な Unicode 文字セットでは、よく使われる文字は 2 バイトを占め、一部の珍しい文字は 3 バイト、さらには 4 バイトを占めます。 -Unicodeは各文字に数値(「コードポイント」と呼ばれる)を割り当てる汎用文字セットですが、**これらの文字コードポイントがコンピュータシステムにどのように格納されるべきかは指定していません**。疑問が生じるかもしれません:システムはテキスト内の異なる長さのUnicodeコードポイントをどのように解釈するのでしょうか?例えば、2バイトのコードが与えられた場合、システムはそれが単一の2バイト文字を表すのか、2つの1バイト文字を表すのかをどのように判断するのでしょうか? +Unicode は汎用文字セットであり、本質的には各文字に番号(「コードポイント」)を割り当てるものですが、**それらのコードポイントをコンピュータ内でどのように保存するかまでは規定していません**。ここで疑問が生じます。長さの異なる Unicode コードポイントが同じテキストに現れたとき、システムはどのように文字を解析するのでしょうか。たとえば長さ 2 バイトの符号が与えられたとき、それが 2 バイトの 1 文字なのか、1 バイトの 2 文字なのかをどう判定するのでしょうか。 -**この問題に対する簡単な解決策は、すべての文字を等長エンコーディングとして格納することです**。以下の図に示すように、「Hello」の各文字は1バイトを占有し、「算法」(アルゴリズム)の各文字は2バイトを占有します。上位ビットをゼロで埋めることで、「Hello 算法」のすべての文字を2バイトとしてエンコードできます。この方法により、システムは2バイトごとに文字を解釈し、フレーズの内容を復元できます。 +この問題に対して、**すべての文字を固定長の符号として保存する**という直接的な解決策があります。下図のように、「Hello」の各文字は 1 バイト、「アルゴリズム」の各文字は 2 バイトを占めます。上位ビットを 0 で埋めることで、「Hello アルゴリズム」のすべての文字を 2 バイト長にエンコードできます。こうすれば、システムは 2 バイトごとに 1 文字を解析して、この語句の内容を復元できます。 -![Unicodeエンコーディング例](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" } +![Unicode エンコーディングの例](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" } -

図 3-7   Unicodeエンコーディング例

+

図 3-7   Unicode エンコーディングの例

-しかし、ASCIIが示したように、英語のエンコーディングには1バイトのみが必要です。上記のアプローチを使用すると、英語テキストが占有する空間がASCIIエンコーディングと比較して2倍になり、メモリ空間の無駄になります。したがって、より効率的なUnicodeエンコーディング方法が必要です。 +しかし ASCII コードはすでに、英語の符号化には 1 バイトで十分であることを示しています。上記の方式を採用すると、英語のテキストが占める空間は ASCII エンコーディング時の 2 倍になり、メモリ空間の浪費が大きくなります。そのため、より効率的な Unicode エンコーディング方式が必要です。 -## 3.4.4   UTF-8エンコーディング +## 3.4.4   UTF-8 エンコーディング -現在、UTF-8は国際的に最も広く使用されているUnicodeエンコーディング方法になっています。**これは可変長エンコーディング**で、文字の複雑さに応じて1〜4バイトを使用して文字を表現します。ASCII文字は1バイトのみが必要で、ラテン文字とギリシャ文字は2バイト、一般的に使用される中国語文字は3バイト、その他の稀少な文字は4バイトが必要です。 +現在、UTF-8 は国際的に最も広く使われている Unicode エンコーディング方式になっています。**これは可変長のエンコーディング**であり、1 文字を 1 〜 4 バイトで表し、文字の複雑さに応じて長さが変わります。ASCII 文字は 1 バイト、ラテン文字とギリシャ文字は 2 バイト、一般的な漢字は 3 バイト、そのほかの一部の珍しい文字は 4 バイト必要です。 -UTF-8のエンコーディング規則は複雑ではなく、2つのケースに分けることができます: +UTF-8 の符号化規則はそれほど複雑ではなく、次の 2 つのケースに分けられます。 -- 1バイト文字の場合、最上位ビットを$0$に設定し、残りの7ビットをUnicodeコードポイントに設定します。注目すべきは、ASCII文字がUnicodeセットの最初の128コードポイントを占有することです。これは**UTF-8エンコーディングがASCIIと後方互換性がある**ことを意味します。これは、UTF-8を使用して古いASCIIテキストを解析できることを意味します。 -- 長さ$n$バイトの文字($n > 1$)の場合、最初のバイトの最上位$n$ビットを$1$に設定し、$(n + 1)^{\text{th}}$ビットを$0$に設定します。2番目のバイトから、各バイトの最上位2ビットを$10$に設定します。残りのビットはUnicodeコードポイントを埋めるために使用されます。 +- 長さ 1 バイトの文字では、最上位ビットを $0$ にし、残りの 7 ビットを Unicode コードポイントに設定します。ここで注意すべきなのは、ASCII 文字が Unicode 文字セットの先頭 128 個のコードポイントを占めていることです。つまり、**UTF-8 エンコーディングは ASCII コードと下位互換性があります**。このため、UTF-8 を使って古い ASCII コードのテキストを解析できます。 +- 長さ $n$ バイトの文字(ただし $n > 1$)では、先頭バイトの上位 $n$ ビットをすべて $1$ にし、第 $n + 1$ ビットを $0$ に設定します。2 バイト目以降では、各バイトの上位 2 ビットをいずれも $10$ にし、残りのすべてのビットで文字の Unicode コードポイントを埋めます。 -以下の図は「Hello算法」のUTF-8エンコーディングを示しています。最上位$n$ビットが$1$に設定されているため、システムは最上位ビットで$1$に設定されたビット数を数えることで文字の長さを$n$として決定できることが観察できます。 +下図は「Helloアルゴリズム」に対応する UTF-8 エンコーディングを示しています。観察すると、上位 $n$ ビットがすべて $1$ に設定されているため、システムは先頭から連続する $1$ の個数を読むことで、その文字の長さが $n$ であると解析できます。 -しかし、なぜ残りのバイトの最上位2ビットを$10$に設定するのでしょうか?実際、この$10$は一種のチェックサムとして機能します。システムが間違ったバイトからテキストの解析を開始した場合、バイトの先頭の$10$によりシステムは異常を迅速に検出できます。 +では、なぜ残りのすべてのバイトの上位 2 ビットを $10$ にするのでしょうか。実は、この $10$ は検査用の印として機能します。システムが誤ったバイト位置からテキストを解析し始めたとしても、バイト先頭の $10$ によって異常を素早く判定できます。 -$10$をチェックサムとして使用する理由は、UTF-8エンコーディング規則の下では、文字の最上位2ビットが$10$になることは不可能だからです。これは矛盾により証明できます:文字の最上位2ビットが$10$の場合、文字の長さが$1$であることを示し、これはASCIIに対応します。しかし、ASCII文字の最上位ビットは$0$であるべきで、これは仮定と矛盾します。 +この $10$ を検査用の印とする理由は、UTF-8 の符号化規則では上位 2 ビットが $10$ になる文字は存在しないからです。この結論は背理法で証明できます。ある文字の上位 2 ビットが $10$ だと仮定すると、その文字の長さは $1$ であり、ASCII コードに対応することになります。しかし ASCII コードの最上位ビットは $0$ であるはずなので、仮定と矛盾します。 -![UTF-8エンコーディング例](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" } +![UTF-8 エンコーディングの例](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" } -

図 3-8   UTF-8エンコーディング例

+

図 3-8   UTF-8 エンコーディングの例

-UTF-8以外にも、他の一般的なエンコーディング方法には以下があります: +UTF-8 以外にも、一般的なエンコーディング方式として次の 2 つがあります。 -- **UTF-16エンコーディング**:2または4バイトを使用して文字を表現します。すべてのASCII文字と一般的に使用される非英語文字は2バイトで表現され、少数の文字は4バイトが必要です。2バイト文字の場合、UTF-16エンコーディングはUnicodeコードポイントと等しくなります。 -- **UTF-32エンコーディング**:すべての文字が4バイトを使用します。これは、UTF-32がUTF-8やUTF-16よりも多くの空間を占有することを意味し、特にASCII文字の割合が高いテキストでは顕著です。 +- **UTF-16 エンコーディング**:1 文字を 2 バイトまたは 4 バイトで表します。すべての ASCII 文字と一般的な非英語文字は 2 バイトで表し、一部の文字だけが 4 バイトを必要とします。2 バイトの文字については、UTF-16 エンコーディングは Unicode コードポイントと等しくなります。 +- **UTF-32 エンコーディング**:各文字を必ず 4 バイトで表します。つまり UTF-32 は UTF-8 や UTF-16 よりも多くの領域を消費し、とくに ASCII 文字の比率が高いテキストでその傾向が顕著です。 -ストレージ空間の観点から、UTF-8を使用して英語文字を表現することは1バイトのみが必要なため非常に効率的です。UTF-16を使用して一部の非英語文字(中国語など)をエンコードすることは、2バイトのみが必要なためより効率的になる場合があります。一方、UTF-8では3バイトが必要になる場合があります。 +記憶領域の使用量という観点では、UTF-8 は英語文字の表現に非常に効率的で、必要なのは 1 バイトだけです。一方、UTF-16 は一部の非英語文字(たとえば中国語の文字)の符号化でより効率的になることがあり、必要なのは 2 バイトだけで、UTF-8 では 3 バイト必要になる場合があります。 -互換性の観点から、UTF-8は最も汎用性があり、多くのツールとライブラリがUTF-8を優先的にサポートしています。 +互換性という観点では、UTF-8 の汎用性が最も高く、多くのツールやライブラリが UTF-8 を優先的にサポートしています。 -## 3.4.5   プログラミング言語における文字エンコーディング +## 3.4.5   プログラミング言語の文字エンコーディング -歴史的に、多くのプログラミング言語はプログラム実行中の文字列処理にUTF-16やUTF-32などの固定長エンコーディングを利用していました。これにより文字列を配列として処理でき、いくつかの利点があります: +従来の多くのプログラミング言語では、実行中の文字列に UTF-16 や UTF-32 のような固定長エンコーディングが使われています。固定長エンコーディングでは、文字列を配列のように扱えるため、次のような利点があります。 -- **ランダムアクセス**:UTF-16でエンコードされた文字列は簡単にランダムアクセスできます。可変長エンコーディングであるUTF-8の場合、$i^{th}$文字の位置を特定するには文字列の開始から$i^{th}$位置まで走査する必要があり、$O(n)$時間がかかります。 -- **文字数カウント**:ランダムアクセスと同様に、UTF-16でエンコードされた文字列の文字数をカウントすることは$O(1)$操作です。しかし、UTF-8でエンコードされた文字列の文字数をカウントするには文字列全体を走査する必要があります。 -- **文字列操作**:分割、連結、挿入、削除などの多くの文字列操作は、UTF-16でエンコードされた文字列で簡単です。これらの操作は一般的に、UTF-8エンコーディングの有効性を確保するためにUTF-8でエンコードされた文字列で追加の計算が必要です。 +- **ランダムアクセス**:UTF-16 で符号化された文字列はランダムアクセスが容易です。UTF-8 は可変長エンコーディングなので、第 $i$ 文字を見つけるには文字列の先頭から第 $i$ 文字まで走査する必要があり、$O(n)$ の時間がかかります。 +- **文字数の計算**:ランダムアクセスと同様に、UTF-16 で符号化された文字列の長さを計算するのも $O(1)$ の操作です。しかし、UTF-8 で符号化された文字列の長さを計算するには、文字列全体を走査する必要があります。 +- **文字列操作**:UTF-16 で符号化された文字列では、多くの文字列操作(分割、連結、挿入、削除など)をより簡単に行えます。UTF-8 で符号化された文字列では、これらの操作を行う際に、無効な UTF-8 エンコーディングを生じさせないための追加計算が通常必要になります。 -プログラミング言語における文字エンコーディングスキームの設計は、様々な要因を含む興味深いトピックです: +実際、プログラミング言語における文字エンコーディング方式の設計は、とても興味深い話題であり、多くの要因が関わっています。 -- Javaの`String`型はUTF-16エンコーディングを使用し、各文字が2バイトを占有します。これは、16ビットがすべての可能な文字を表現するのに十分であるという初期の信念に基づいており、後に間違いであることが証明されました。Unicode標準が16ビットを超えて拡張されると、Javaの文字は「サロゲートペア」として知られる16ビット値のペアで表現される場合があります。 -- JavaScriptとTypeScriptは、Javaと同様の理由でUTF-16エンコーディングを使用します。JavaScriptが1995年にNetscapeによって最初に導入されたとき、Unicodeはまだ初期段階にあり、16ビットエンコーディングはすべてのUnicode文字を表現するのに十分でした。 -- C#はUTF-16エンコーディングを使用し、これは主にMicrosoftによって設計された.NETプラットフォーム、および多くのMicrosoft技術(Windowsオペレーティングシステムを含む)がUTF-16エンコーディングを広範囲に使用しているためです。 +- Java の `String` 型は UTF-16 エンコーディングを使用し、各文字は 2 バイトを占めます。これは Java 言語の設計当初、人々が 16 ビットあればあらゆる文字を表現するのに十分だと考えていたためです。しかし、これは誤った判断でした。その後 Unicode 規格は 16 ビットを超える範囲へ拡張されたため、現在の Java では 1 文字が 16 ビット値の組(「サロゲートペア」)で表されることがあります。 +- JavaScript と TypeScript の文字列が UTF-16 エンコーディングを使う理由も Java と似ています。1995 年に Netscape 社が初めて JavaScript 言語を公開した当時、Unicode はまだ発展初期にあり、16 ビットの符号化で十分すべての Unicode 文字を表せると考えられていました。 +- C# が UTF-16 エンコーディングを使う主な理由は、.NET プラットフォームが Microsoft によって設計され、Microsoft の多くの技術(Windows オペレーティングシステムを含む)で UTF-16 エンコーディングが広く使われているためです。 -文字数の過小評価により、これらの言語は16ビットを超えるUnicode文字を表現するために「サロゲートペア」を使用する必要がありました。このアプローチには欠点があります:サロゲートペアを含む文字列は2バイトまたは4バイトを占有する文字を持つ場合があり、固定長エンコーディングの利点を失います。さらに、サロゲートペアの処理はプログラミングに複雑さとデバッグの困難さを追加します。 +以上のプログラミング言語は文字数を過小評価していたため、16 ビットを超える長さの Unicode 文字を表すために「サロゲートペア」を採用せざるを得ませんでした。これはやむを得ない妥協策です。一方では、サロゲートペアを含む文字列では、1 文字が 2 バイトまたは 4 バイトを占める可能性があり、固定長エンコーディングの利点が失われます。もう一方では、サロゲートペアの処理には追加のコードが必要となり、プログラミングの複雑さとデバッグの難しさが増します。 -これらの課題に対処するため、一部の言語は代替エンコーディング戦略を採用しています: +こうした理由から、一部のプログラミング言語では別のエンコーディング方式が採用されました。 -- Pythonの`str`型は、文字のストレージ長が文字列内の最大のUnicodeコードポイントに依存する柔軟な表現でUnicodeエンコーディングを使用します。すべての文字がASCIIの場合、各文字は1バイトを占有し、基本多言語面(BMP)内の文字は2バイト、BMPを超える文字は4バイトを占有します。 -- Goの`string`型は内部的にUTF-8エンコーディングを使用します。Goは個別のUnicodeコードポイントを表現するための`rune`型も提供します。 -- Rustの`str`と`String`型は内部的にUTF-8エンコーディングを使用します。Rustは個別のUnicodeコードポイント用の`char`型も提供します。 +- Python の `str` は Unicode エンコーディングを使用し、柔軟な文字列表現を採用しています。保存される文字の長さは、その文字列中で最大の Unicode コードポイントに依存します。文字列がすべて ASCII 文字であれば各文字は 1 バイト、ASCII の範囲を超える文字があってもすべてが基本多言語面(BMP)内であれば各文字は 2 バイト、BMP を超える文字があれば各文字は 4 バイトを占めます。 +- Go 言語の `string` 型は内部で UTF-8 エンコーディングを使用します。Go 言語には単一の Unicode コードポイントを表す `rune` 型も用意されています。 +- Rust 言語の `str` と `String` 型は内部で UTF-8 エンコーディングを使用します。Rust にも単一の Unicode コードポイントを表す `char` 型があります。 -上記の議論は、プログラミング言語での文字列の格納方法に関するものであり、**ファイルでの文字列の格納方法やネットワーク上での送信方法とは異なる**ことに注意することが重要です。ファイルストレージやネットワーク送信では、文字列は通常、最適な互換性と空間効率のためにUTF-8形式でエンコードされます。 +注意すべきなのは、ここまでの議論はすべて、プログラミング言語内での文字列の保存方法についてであり、**文字列をファイルに保存したりネットワークで転送したりする方法とは別の問題である**ということです。ファイル保存やネットワーク転送では、通常、互換性と空間効率を最適化するために文字列を UTF-8 形式にエンコードします。 diff --git a/ja/docs/chapter_data_structure/classification_of_data_structure.md b/ja/docs/chapter_data_structure/classification_of_data_structure.md index 9ef7decc4..79a280188 100644 --- a/ja/docs/chapter_data_structure/classification_of_data_structure.md +++ b/ja/docs/chapter_data_structure/classification_of_data_structure.md @@ -4,55 +4,55 @@ comments: true # 3.1   データ構造の分類 -一般的なデータ構造には、配列、連結リスト、スタック、キュー、ハッシュ表、木、ヒープ、グラフがあります。これらは「論理構造」と「物理構造」に分類できます。 +代表的なデータ構造には、配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフがあり、これらは「論理構造」と「物理構造」の 2 つの観点から分類できます。 ## 3.1.1   論理構造:線形と非線形 -**論理構造はデータ要素間の論理的関係を明らかにします**。配列と連結リストでは、データは特定の順序で配置され、データ間の線形関係を示しています。一方、木では、データは上から下へ階層的に配置され、「祖先」と「子孫」間の派生関係を示しています。そして、グラフはノードとエッジから構成され、複雑なネットワーク関係を反映しています。 +**論理構造はデータ要素間の論理的な関係を示します**。配列と連結リストでは、データは一定の順序で並び、データ間の線形関係を表します。一方、木ではデータは上から下へ階層的に並び、「祖先」と「子孫」の派生関係を示します。グラフはノードと辺で構成され、複雑なネットワーク関係を反映します。 -下図に示されているように、論理構造は「線形」と「非線形」の2つの主要カテゴリに分けることができます。線形構造はより直感的で、データが論理関係において線形に配置されていることを示しています。非線形構造は、逆に非線形に配置されています。 +以下の図に示すように、論理構造は「線形」と「非線形」の 2 つに大別できます。線形構造は比較的直感的で、データが論理関係において線形に並ぶことを指します。非線形構造はその逆で、非線形に配置されます。 -- **線形データ構造**:配列、連結リスト、スタック、キュー、ハッシュ表。要素が一対一の順次関係を持ちます。 -- **非線形データ構造**:木、ヒープ、グラフ、ハッシュ表。 +- **線形データ構造**:配列、連結リスト、スタック、キュー、ハッシュテーブルであり、要素間は 1 対 1 の順序関係です。 +- **非線形データ構造**:木、ヒープ、グラフ、ハッシュテーブル。 -非線形データ構造は、さらに木構造とネットワーク構造に分けることができます。 +非線形データ構造は、さらに木構造と網状構造に分けられます。 -- **木構造**:木、ヒープ、ハッシュ表。要素が一対多の関係を持ちます。 -- **ネットワーク構造**:グラフ。要素が多対多の関係を持ちます。 +- **木構造**:木、ヒープ、ハッシュテーブルであり、要素間は 1 対多の関係です。 +- **網状構造**:グラフであり、要素間は多対多の関係です。 -![Linear and non-linear data structures](classification_of_data_structure.assets/classification_logic_structure.png){ class="animation-figure" } +![線形データ構造と非線形データ構造](classification_of_data_structure.assets/classification_logic_structure.png){ class="animation-figure" } -

図 3-1   Linear and non-linear data structures

+

図 3-1   線形データ構造と非線形データ構造

## 3.1.2   物理構造:連続と分散 -**アルゴリズムの実行中、処理されるデータはメモリに格納されます**。下図はコンピュータのメモリスティックを示しており、各黒い正方形は物理メモリ空間です。メモリを巨大なExcelスプレッドシートと考えることができ、各セルは一定量のデータを格納できます。 +**アルゴリズムのプログラムが実行されるとき、処理中のデータは主にメモリに格納されます**。下図はコンピュータのメモリモジュールを示しており、各黒い四角はそれぞれ 1 つのメモリ空間を表しています。メモリは巨大な Excel の表のようなものだと考えることができ、各セルには一定量のデータを格納できます。 -**システムはメモリアドレスによって目標位置のデータにアクセスします**。下図に示されているように、コンピュータは特定のルールに従って表の各セルに一意の識別子を割り当て、各メモリ空間が一意のメモリアドレスを持つことを保証します。これらのアドレスにより、プログラムはメモリに格納されたデータにアクセスできます。 +**システムはメモリアドレスを通じて目的の位置にあるデータへアクセスします**。下図に示すように、コンピュータは特定の規則に従って表内の各セルに番号を割り当て、各メモリ空間が一意のメモリアドレスを持つようにします。これらのアドレスがあれば、プログラムはメモリ内のデータにアクセスできます。 -![Memory stick, memory spaces, memory addresses](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" } +![メモリモジュール、メモリ空間、メモリアドレス](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" } -

図 3-2   Memory stick, memory spaces, memory addresses

+

図 3-2   メモリモジュール、メモリ空間、メモリアドレス

!!! tip - メモリをExcelスプレッドシートに比較することは簡略化された類推であることに注意してください。メモリの実際の動作メカニズムはより複雑で、アドレス空間、メモリ管理、キャッシュメカニズム、仮想メモリ、物理メモリなどの概念が関係しています。 + 補足すると、メモリを Excel の表にたとえるのは単純化した比喩であり、実際のメモリの動作機構はより複雑で、アドレス空間、メモリ管理、キャッシュ機構、仮想メモリ、物理メモリなどの概念が関わります。 -メモリはすべてのプログラムの共有リソースです。あるメモリブロックが1つのプログラムによって占有されると、他のプログラムが同時に使用することはできません。**したがって、メモリリソースはデータ構造とアルゴリズムの設計における重要な考慮事項です**。例えば、アルゴリズムのピークメモリ使用量は、システムの残り空きメモリを超えてはいけません。連続したメモリブロックが不足している場合は、非連続メモリブロックに格納できるデータ構造を選択する必要があります。 +メモリはすべてのプログラムで共有される資源であり、あるメモリ領域が 1 つのプログラムに占有されると、通常は他のプログラムが同時に利用できません。**したがって、データ構造とアルゴリズムの設計では、メモリ資源は重要な考慮要素です**。たとえば、アルゴリズムが使用するメモリ使用量のピークは、システムに残っている空きメモリを超えてはなりません。大きな連続メモリ領域が不足している場合、選択するデータ構造は分散したメモリ空間に格納できる必要があります。 -下図に示されているように、**物理構造はコンピュータメモリにおけるデータの格納方法を反映し**、連続空間格納(配列)と非連続空間格納(連結リスト)に分けることができます。2つのタイプの物理構造は、時間効率と空間効率の観点で補完的な特性を示します。 +下図に示すように、**物理構造はデータがコンピュータメモリ内にどのように格納されるかを表します**。これは連続空間への格納(配列)と分散空間への格納(連結リスト)に分けられます。物理構造は低レベルでデータのアクセス、更新、追加、削除などの操作方法を決定し、2 種類の物理構造は時間効率と空間効率の面で相補的な特徴を持ちます。 -![Contiguous space storage and dispersed space storage](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" } +![連続空間格納と分散空間格納](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" } -

図 3-3   Contiguous space storage and dispersed space storage

+

図 3-3   連続空間格納と分散空間格納

-**すべてのデータ構造は配列、連結リスト、またはその組み合わせに基づいて実装されていることに注意してください**。例えば、スタックとキューは配列または連結リストのどちらでも実装できます。ハッシュ表の実装には配列と連結リストの両方が関係する場合があります。 +補足すると、**すべてのデータ構造は配列、連結リスト、またはその両者の組み合わせに基づいて実装されます**。たとえば、スタックとキューは配列でも連結リストでも実装できます。一方、ハッシュテーブルの実装には配列と連結リストの両方が含まれる場合があります。 -- **配列ベースの実装**:スタック、キュー、ハッシュ表、木、ヒープ、グラフ、行列、テンソル(次元$\geq 3$の配列)。 -- **連結リストベースの実装**:スタック、キュー、ハッシュ表、木、ヒープ、グラフなど。 +- **配列に基づいて実装可能**:スタック、キュー、ハッシュテーブル、木、ヒープ、グラフ、行列、テンソル(次元 $\geq 3$ の配列)など。 +- **連結リストに基づいて実装可能**:スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなど。 -配列に基づいて実装されたデータ構造は「静的データ構造」とも呼ばれ、初期化後に長さを変更できないことを意味します。逆に、連結リストに基づいたものは「動的データ構造」と呼ばれ、プログラム実行中にサイズを調整できます。 +連結リストは初期化後も、プログラムの実行中に長さを調整できるため、「動的データ構造」とも呼ばれます。配列は初期化後に長さを変更できないため、「静的データ構造」とも呼ばれます。なお、配列もメモリを再割り当てすることで長さを変更でき、ある程度の「動的性」を持たせることができます。 !!! tip - 物理構造を理解するのが困難な場合は、次の章「配列と連結リスト」を読んでから、この節に戻ることをお勧めします。 + 物理構造の理解が難しいと感じる場合は、先に次の章を読んでから本節を振り返ることを勧めます。 diff --git a/ja/docs/chapter_data_structure/index.md b/ja/docs/chapter_data_structure/index.md index 37e0bcea7..ea49b8dc8 100644 --- a/ja/docs/chapter_data_structure/index.md +++ b/ja/docs/chapter_data_structure/index.md @@ -5,18 +5,18 @@ icon: material/shape-outline # 第 3 章   データ構造 -![Data structures](../assets/covers/chapter_data_structure.jpg){ class="cover-image" } +![データ構造](../assets/covers/chapter_data_structure.jpg){ class="cover-image" } !!! abstract - データ構造は堅牢で多様なフレームワークとして機能します。 - - データの整然とした組織化のための設計図を提供し、その上でアルゴリズムが生き生きと動き出します。 + データ構造は、堅固で多様な枠組みのようなものである。 + + それはデータを秩序立てて組織するための青写真を示し、アルゴリズムはその上で生き生きと動き出す。 ## 章の内容 - [3.1   データ構造の分類](classification_of_data_structure.md) - [3.2   基本データ型](basic_data_types.md) -- [3.3   数値の符号化 *](number_encoding.md) -- [3.4   文字の符号化 *](character_encoding.md) +- [3.3   数値エンコーディング *](number_encoding.md) +- [3.4   文字エンコーディング *](character_encoding.md) - [3.5   まとめ](summary.md) diff --git a/ja/docs/chapter_data_structure/number_encoding.md b/ja/docs/chapter_data_structure/number_encoding.md index 2391e570b..c9ea939dc 100644 --- a/ja/docs/chapter_data_structure/number_encoding.md +++ b/ja/docs/chapter_data_structure/number_encoding.md @@ -6,25 +6,25 @@ comments: true !!! tip - 本書では、アスタリスク「*」が付いた章は任意読書です。時間が不足している場合や難しいと感じる場合は、最初はこれらをスキップして、必須の章を完了した後に戻ることができます。 + 本書では、タイトルに * 記号が付いている章は選読です。時間が限られている場合や理解が難しいと感じる場合は、いったん読み飛ばし、必読章を終えてから個別に取り組んでください。 -## 3.3.1   整数エンコーディング +## 3.3.1   符号付き絶対値表現、1 の補数、2 の補数 -前の節の表で、すべての整数型は正の数よりも1つ多い負の数を表現できることを観察しました。例えば、`byte`の範囲は$[-128, 127]$です。この現象は直感に反するように見え、その根本的な理由には符号絶対値、1の補数、2の補数エンコーディングの知識が関与しています。 +前節の表を見ると、すべての整数型で表せる負数の個数は正数より 1 つ多く、たとえば `byte` の値域は $[-128, 127]$ です。この現象は直感に反するように見えますが、その背景には符号付き絶対値表現、1 の補数、2 の補数に関する知識があります。 -まず重要なことは、**数値はコンピュータ内で2の補数形式で格納される**ということです。なぜそうなのかを分析する前に、これら3つのエンコーディング方法を定義しましょう: +まず押さえておくべきなのは、**数値はコンピュータ内で「2 の補数」の形で保存される**ということです。その理由を説明する前に、まずはこの 3 つの定義を示します。 -- **符号絶対値**:数値の二進表現の最上位ビットを符号ビットとし、$0$は正の数、$1$は負の数を表します。残りのビットは数値の値を表します。 -- **1の補数**:正の数の1の補数は符号絶対値と同じです。負の数の場合、符号ビット以外のすべてのビットを反転して得られます。 -- **2の補数**:正の数の2の補数は符号絶対値と同じです。負の数の場合、その1の補数に$1$を加えて得られます。 +- **符号付き絶対値表現**:数値の二進表現の最上位ビットを符号ビットとみなし、$0$ は正数、$1$ は負数を表し、残りのビットが数値の値を表します。 +- **1 の補数**:正数の 1 の補数は符号付き絶対値表現と同じで、負数の 1 の補数は符号ビットを除くすべてのビットを反転したものです。 +- **2 の補数**:正数の 2 の補数は符号付き絶対値表現と同じで、負数の 2 の補数は 1 の補数に $1$ を加えたものです。 -以下の図は、符号絶対値、1の補数、2の補数間の変換を示しています: +下図は、符号付き絶対値表現、1 の補数、2 の補数の変換方法を示しています。 -![符号絶対値、1の補数、2の補数間の変換](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" } +![符号付き絶対値表現、1 の補数、2 の補数の相互変換](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" } -

図 3-4   符号絶対値、1の補数、2の補数間の変換

+

図 3-4   符号付き絶対値表現、1 の補数、2 の補数の相互変換

-符号絶対値は最も直感的ですが、制限があります。一つには、**符号絶対値の負の数は計算で直接使用できません**。例えば、符号絶対値で$1 + (-2)$を計算すると$-3$になり、これは正しくありません。 +符号付き絶対値表現(sign-magnitude)は最も直感的ですが、いくつかの制約があります。まず、**負数の符号付き絶対値表現はそのまま演算に使えません**。たとえば符号付き絶対値表現で $1 + (-2)$ を計算すると、結果は $-3$ になってしまい、これは明らかに誤りです。 $$ \begin{aligned} @@ -35,20 +35,20 @@ $$ \end{aligned} $$ -この問題に対処するため、コンピュータは1の補数を導入しました。1の補数に変換して$1 + (-2)$を計算し、結果を符号絶対値に戻すと、正しい結果$-1$が得られます。 +この問題を解決するために、コンピュータには1 の補数(1's complement)が導入されました。まず符号付き絶対値表現を 1 の補数に変換し、1 の補数で $1 + (-2)$ を計算してから、結果を 1 の補数から符号付き絶対値表現へ戻すと、正しい結果 $-1$ が得られます。 $$ \begin{aligned} & 1 + (-2) \newline -& \rightarrow 0000 \; 0001 \; \text{(符号絶対値)} + 1000 \; 0010 \; \text{(符号絶対値)} \newline -& = 0000 \; 0001 \; \text{(1の補数)} + 1111 \; 1101 \; \text{(1の補数)} \newline -& = 1111 \; 1110 \; \text{(1の補数)} \newline -& = 1000 \; 0001 \; \text{(符号絶対値)} \newline +& \rightarrow 0000 \; 0001 \; \text{(符号付き絶対値表現)} + 1000 \; 0010 \; \text{(符号付き絶対値表現)} \newline +& = 0000 \; 0001 \; \text{(1 の補数)} + 1111 \; 1101 \; \text{(1 の補数)} \newline +& = 1111 \; 1110 \; \text{(1 の補数)} \newline +& = 1000 \; 0001 \; \text{(符号付き絶対値表現)} \newline & \rightarrow -1 \end{aligned} $$ -また、**符号絶対値では0に2つの表現があります**:$+0$と$-0$です。これは0に対して2つの異なる二進エンコーディングがあることを意味し、曖昧さを引き起こす可能性があります。例えば、条件チェックで正と負の0を区別しないと、正しくない結果になる可能性があります。この曖昧さに対処するには追加のチェックが必要で、計算効率が低下する可能性があります。 +一方、**数値 0 の符号付き絶対値表現には $+0$ と $-0$ の 2 つの表し方があります**。つまり、数値 0 に対して異なる 2 つの二進コードが対応しており、これは曖昧さの原因になります。たとえば条件判定で正のゼロと負のゼロを区別しないと、誤った判定結果になる可能性があります。また、この曖昧さを解消しようとすると追加の判定処理が必要になり、計算効率が下がるおそれがあります。 $$ \begin{aligned} @@ -57,106 +57,106 @@ $$ \end{aligned} $$ -符号絶対値と同様に、1の補数も正と負の0の曖昧さに悩まされます。そのため、コンピュータはさらに2の補数を導入しました。符号絶対値、1の補数、2の補数における負の0の変換過程を観察してみましょう: +符号付き絶対値表現と同様に、1 の補数にも正負のゼロの曖昧さがあります。そこでコンピュータはさらに2 の補数(2's complement)を導入しました。まずは負のゼロについて、符号付き絶対値表現、1 の補数、2 の補数の変換を見てみましょう。 $$ \begin{aligned} --0 \rightarrow \; & 1000 \; 0000 \; \text{(符号絶対値)} \newline -= \; & 1111 \; 1111 \; \text{(1の補数)} \newline -= 1 \; & 0000 \; 0000 \; \text{(2の補数)} \newline +-0 \rightarrow \; & 1000 \; 0000 \; \text{(符号付き絶対値表現)} \newline += \; & 1111 \; 1111 \; \text{(1 の補数)} \newline += 1 \; & 0000 \; 0000 \; \text{(2 の補数)} \newline \end{aligned} $$ -負の0の1の補数に$1$を加えると桁上がりが発生しますが、`byte`の長さは8ビットのみのため、9番目のビットへの桁上がり$1$は破棄されます。したがって、**負の0の2の補数は$0000 \; 0000$**で、正の0と同じになり、曖昧さが解決されます。 +負のゼロの 1 の補数に $1$ を加えると桁上がりが発生しますが、`byte` 型の長さは 8 ビットしかないため、第 9 ビットへあふれた $1$ は捨てられます。つまり、**負のゼロの 2 の補数は $0000 \; 0000$ であり、正のゼロの 2 の補数と同じです**。そのため、2 の補数表現ではゼロは 1 つしか存在せず、正負のゼロの曖昧さは解消されます。 -最後の謎は、`byte`の$[-128, 127]$の範囲で、追加の負の数$-128$があることです。$[-127, +127]$の区間では、すべての整数に対応する符号絶対値、1の補数、2の補数があり、相互変換が可能であることを観察します。 +最後にもう 1 つ疑問が残ります。`byte` 型の値域は $[-128, 127]$ ですが、余分にある負数 $-128$ はどのように得られるのでしょうか。区間 $[-127, +127]$ にあるすべての整数には、それぞれ対応する符号付き絶対値表現、1 の補数、2 の補数があり、符号付き絶対値表現と 2 の補数の間は相互に変換できます。 -しかし、**2の補数$1000 \; 0000$は対応する符号絶対値を持たない例外です**。変換方法によると、その符号絶対値は$0000 \; 0000$で、0を示します。これは矛盾を示しています。なぜなら、その2の補数は自分自身を表すべきだからです。コンピュータは、この特別な2の補数$1000 \; 0000$を$-128$を表すものとして指定しています。実際、2の補数での$(-1) + (-127)$の計算結果は$-128$になります。 +しかし、**2 の補数 $1000 \; 0000$ だけは例外で、対応する符号付き絶対値表現を持ちません**。変換規則に従うと、この 2 の補数に対応する符号付き絶対値表現は $0000 \; 0000$ になります。これは明らかに矛盾しています。なぜなら、この符号付き絶対値表現は数値 $0$ を表し、その 2 の補数は自分自身であるはずだからです。コンピュータでは、この特別な 2 の補数 $1000 \; 0000$ を $-128$ と定めています。実際、2 の補数での $(-1) + (-127)$ の計算結果はちょうど $-128$ になります。 $$ \begin{aligned} & (-127) + (-1) \newline -& \rightarrow 1111 \; 1111 \; \text{(符号絶対値)} + 1000 \; 0001 \; \text{(符号絶対値)} \newline -& = 1000 \; 0000 \; \text{(1の補数)} + 1111 \; 1110 \; \text{(1の補数)} \newline -& = 1000 \; 0001 \; \text{(2の補数)} + 1111 \; 1111 \; \text{(2の補数)} \newline -& = 1000 \; 0000 \; \text{(2の補数)} \newline +& \rightarrow 1111 \; 1111 \; \text{(符号付き絶対値表現)} + 1000 \; 0001 \; \text{(符号付き絶対値表現)} \newline +& = 1000 \; 0000 \; \text{(1 の補数)} + 1111 \; 1110 \; \text{(1 の補数)} \newline +& = 1000 \; 0001 \; \text{(2 の補数)} + 1111 \; 1111 \; \text{(2 の補数)} \newline +& = 1000 \; 0000 \; \text{(2 の補数)} \newline & \rightarrow -128 \end{aligned} $$ -お気づきかもしれませんが、これらの計算はすべて加算であり、重要な事実を示唆しています:**コンピュータの内部ハードウェア回路は主に加算演算を中心に設計されています**。これは、加算が乗算、除算、減算などの他の演算と比較してハードウェアで実装しやすく、並列化が容易で高速計算が可能だからです。 +すでにお気づきかもしれませんが、上の計算はすべて加算です。これは重要な事実を示しています。**コンピュータ内部のハードウェア回路は、主として加算を基準に設計されている**のです。なぜなら、加算はほかの演算(乗算、除算、減算など)に比べてハードウェアで実装しやすく、並列化もしやすく、演算速度も速いからです。 -これはコンピュータが加算のみを実行できることを意味するものではありません。**加算と基本的な論理演算を組み合わせることで、コンピュータは様々な他の数学演算を実行できます**。例えば、減算$a - b$は$a + (-b)$に変換でき、乗算と除算は複数の加算または減算に変換できます。 +ただし、これはコンピュータが加算しかできないという意味ではありません。**加算といくつかの基本的な論理演算を組み合わせることで、コンピュータはさまざまな数学演算を実現できます**。たとえば減算 $a - b$ は加算 $a + (-b)$ に変換できますし、乗算や除算も繰り返しの加算または減算に変換できます。 -コンピュータで2の補数を使用する理由をまとめることができます:2の補数表現により、コンピュータは同じ回路と演算を使用して正と負の数の加算を処理でき、減算用の特別なハードウェア回路の必要性を排除し、正と負の0の曖昧さを回避できます。これによりハードウェア設計が大幅に簡素化され、計算効率が向上します。 +これで、コンピュータが 2 の補数を使う理由をまとめられます。2 の補数表現に基づけば、コンピュータは同じ回路と操作で正数と負数の加算を扱うことができ、減算専用の特別なハードウェア回路を設計する必要がなく、正負のゼロの曖昧さも特別に処理しなくて済みます。これにより、ハードウェア設計は大幅に簡略化され、演算効率も向上します。 -2の補数の設計は非常に巧妙で、スペースの制約により、ここで停止します。興味のある読者はさらに探求することを奨励します。 +2 の補数の設計は非常に巧妙ですが、紙幅の都合上ここまでにします。興味のある読者は、さらに深く調べてみてください。 -## 3.3.2   浮動小数点数エンコーディング +## 3.3.2   浮動小数点数のエンコーディング -興味深いことに気づいたかもしれません:同じ4バイトの長さにもかかわらず、なぜ`float`は`int`と比較してはるかに大きい値の範囲を持つのでしょうか?これは直感に反するように見えます。`float`は分数を表現する必要があるため、範囲が縮小すると予想されるからです。 +注意深い人なら気づくかもしれません。`int` と `float` はどちらも長さが 4 バイトで同じなのに、なぜ `float` の値域は `int` よりはるかに広いのでしょうか。これはかなり直感に反します。というのも、`float` は小数を表す必要があるので、本来なら値域は狭くなるはずだからです。 -実際、**これは浮動小数点数(`float`)で使用される異なる表現方法によるものです**。32ビットの二進数を次のように考えてみましょう: +実際には、**これは浮動小数点数 `float` が異なる表現方法を採用しているためです**。32 ビット長の二進数を次のように表します。 $$ b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0 $$ -IEEE 754標準によると、32ビットの`float`は次の3つの部分で構成されます: +IEEE 754 標準によれば、32-bit 長の `float` は次の 3 つの部分から構成されます。 -- 符号ビット$\mathrm{S}$:1ビットを占有し、$b_{31}$に対応します。 -- 指数ビット$\mathrm{E}$:8ビットを占有し、$b_{30} b_{29} \ldots b_{23}$に対応します。 -- 仮数ビット$\mathrm{N}$:23ビットを占有し、$b_{22} b_{21} \ldots b_0$に対応します。 +- 符号部 $\mathrm{S}$ :1 ビットを占め、$b_{31}$ に対応します。 +- 指数部 $\mathrm{E}$ :8 ビットを占め、$b_{30} b_{29} \ldots b_{23}$ に対応します。 +- 仮数部 $\mathrm{N}$ :23 ビットを占め、$b_{22} b_{21} \ldots b_0$ に対応します。 -二進`float`数の値は次のように計算されます: +二進数 `float` に対応する値は次式で計算されます。 $$ -\text{val} = (-1)^{b_{31}} \times 2^{\left(b_{30} b_{29} \ldots b_{23}\right)_2 - 127} \times \left(1 . b_{22} b_{21} \ldots b_0\right)_2 +\text {val} = (-1)^{b_{31}} \times 2^{\left(b_{30} b_{29} \ldots b_{23}\right)_2-127} \times\left(1 . b_{22} b_{21} \ldots b_0\right)_2 $$ -十進公式に変換すると、次のようになります: +十進数に直すと、計算式は次のようになります。 $$ -\text{val} = (-1)^{\mathrm{S}} \times 2^{\mathrm{E} - 127} \times (1 + \mathrm{N}) +\text {val}=(-1)^{\mathrm{S}} \times 2^{\mathrm{E} -127} \times (1 + \mathrm{N}) $$ -各成分の範囲は: +各項の取り得る範囲は次のとおりです。 $$ \begin{aligned} \mathrm{S} \in & \{ 0, 1\}, \quad \mathrm{E} \in \{ 1, 2, \dots, 254 \} \newline -(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} \times 2^{-i}) \subset [1, 2 - 2^{-23}] +(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} 2^{-i}) \subset [1, 2 - 2^{-23}] \end{aligned} $$ -![IEEE 754標準での浮動小数点数の計算例](number_encoding.assets/ieee_754_float.png){ class="animation-figure" } +![IEEE 754 標準における float の計算例](number_encoding.assets/ieee_754_float.png){ class="animation-figure" } -

図 3-5   IEEE 754標準での浮動小数点数の計算例

+

図 3-5   IEEE 754 標準における float の計算例

-上の図を観察すると、例のデータ$\mathrm{S} = 0$、$\mathrm{E} = 124$、$\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$が与えられた場合: +上図を見ると、例として $\mathrm{S} = 0$ 、 $\mathrm{E} = 124$ 、$\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$ が与えられた場合、次のようになります。 $$ -\text{val} = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875 +\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875 $$ -これで最初の質問に答えることができます:**`float`の表現には指数ビットが含まれているため、`int`よりもはるかに大きい範囲を持ちます**。上記の計算に基づくと、`float`で表現可能な最大正の数は約$2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$で、最小負の数は符号ビットを切り替えることで得られます。 +これで最初の疑問に答えられます。**`float` の表現方法には指数部が含まれているため、その値域は `int` よりはるかに広い**のです。上の計算より、`float` が表せる最大の正数は $2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$ であり、符号ビットを切り替えれば最小の負数が得られます。 -**しかし、`float`の拡張された範囲のトレードオフは精度の犠牲です**。整数型`int`は32ビットすべてを数値表現に使用し、値は均等に分布していますが、指数ビットのため、`float`の値が大きいほど、隣接する数値間の差が大きくなります。 +**浮動小数点数 `float` は値域を広げる一方で、その代償として精度を犠牲にしています**。整数型 `int` は 32 ビットすべてを数値の表現に使うため、数値は一様に分布します。しかし指数部があるため、浮動小数点数 `float` は値が大きくなるほど、隣り合う 2 つの数の差も大きくなる傾向があります。 -以下の表に示すように、指数ビット$\mathrm{E} = 0$と$\mathrm{E} = 255$は特別な意味を持ち、**0、無限大、$\mathrm{NaN}$などを表現するために使用されます**。 +次の表のとおり、指数部 $\mathrm{E} = 0$ と $\mathrm{E} = 255$ には特別な意味があり、**ゼロ、無限大、$\mathrm{NaN}$ などを表すために使われます**。 -

表 3-2   指数ビットの意味

+

表 3-2   指数部の意味

-| 指数ビットE | 仮数ビット$\mathrm{N} = 0$ | 仮数ビット$\mathrm{N} \ne 0$ | 計算公式 | -| ------------------ | ----------------------------- | ------------------------------- | ---------------------------------------------------------------------- | -| $0$ | $\pm 0$ | 非正規化数 | $(-1)^{\mathrm{S}} \times 2^{-126} \times (0.\mathrm{N})$ | -| $1, 2, \dots, 254$ | 正規化数 | 正規化数 | $(-1)^{\mathrm{S}} \times 2^{(\mathrm{E} -127)} \times (1.\mathrm{N})$ | -| $255$ | $\pm \infty$ | $\mathrm{NaN}$ | | +| 指数部 E | 仮数部 $\mathrm{N} = 0$ | 仮数部 $\mathrm{N} \ne 0$ | 計算式 | +| ------------------ | ----------------------- | ------------------------- | ---------------------------------------------------------------------- | +| $0$ | $\pm 0$ | 非正規化数 | $(-1)^{\mathrm{S}} \times 2^{-126} \times (0.\mathrm{N})$ | +| $1, 2, \dots, 254$ | 正規化数 | 正規化数 | $(-1)^{\mathrm{S}} \times 2^{(\mathrm{E} -127)} \times (1.\mathrm{N})$ | +| $255$ | $\pm \infty$ | $\mathrm{NaN}$ | |
-非正規化数は浮動小数点数の精度を大幅に向上させることは注目に値します。最小の正の正規化数は$2^{-126}$で、最小の正の非正規化数は$2^{-126} \times 2^{-23}$です。 +なお、非正規化数によって浮動小数点数の精度は大きく向上します。最小の正の正規化数は $2^{-126}$ であり、最小の正の非正規化数は $2^{-126} \times 2^{-23}$ です。 -倍精度`double`も`float`と同様の表現方法を使用しますが、簡潔さのためここでは詳述しません。 +倍精度 `double` も `float` と同様の表現方法を採用しているため、ここでは詳述しません。 diff --git a/ja/docs/chapter_data_structure/summary.md b/ja/docs/chapter_data_structure/summary.md index 07c899efc..146637b34 100644 --- a/ja/docs/chapter_data_structure/summary.md +++ b/ja/docs/chapter_data_structure/summary.md @@ -4,67 +4,67 @@ comments: true # 3.5   まとめ -### 1.   重要なポイント +### 1.   重要ポイントの振り返り -- データ構造は論理構造と物理構造の2つの観点から分類できます。論理構造はデータ間の論理的関係を記述し、物理構造はデータがメモリにどのように格納されるかを記述します。 -- よく使用される論理構造には、線形構造、木、ネットワークがあります。通常、論理構造に基づいてデータ構造を線形(配列、連結リスト、スタック、キュー)と非線形(木、グラフ、ヒープ)に分けます。ハッシュ表の実装は線形と非線形の両方のデータ構造を含む場合があります。 -- プログラムが実行中の際、データはメモリに格納されます。各メモリ空間には対応するアドレスがあり、プログラムはこれらのアドレスを通じてデータにアクセスします。 -- 物理構造は連続空間格納(配列)と離散空間格納(連結リスト)に分けることができます。すべてのデータ構造は配列、連結リスト、またはその両方の組み合わせを使用して実装されます。 -- コンピュータの基本データ型には、整数(`byte`、`short`、`int`、`long`)、浮動小数点数(`float`、`double`)、文字(`char`)、ブール値(`bool`)が含まれます。データ型の値の範囲は、そのサイズと表現に依存します。 -- 符号絶対値、1の補数、2の補数は、コンピュータで整数をエンコードする3つの方法であり、相互に変換することができます。符号絶対値の最上位ビットは符号ビットで、残りのビットは数値の値を表します。 -- 整数はコンピュータで2の補数によってエンコードされます。この表現の利点には、(i)コンピュータが正と負の整数の加算を統一できる、(ii)減算用の特別なハードウェア回路を設計する必要がない、(iii)正と負の0の曖昧さがない、があります。 -- 浮動小数点数のエンコーディングは、1つの符号ビット、8つの指数ビット、23の仮数ビットで構成されます。指数ビットのため、浮動小数点数の範囲は整数よりもはるかに大きくなりますが、精度を犠牲にします。 -- ASCIIは最初期の英語文字セットで、1バイトの長さで計127文字です。GBKは人気のある中国語文字セットで、2万文字以上の中国語文字を含みます。Unicodeは世界の様々な言語の文字を含む完全な文字セット標準を提供することを目的とし、文字エンコーディング方法の不一致による文字化け問題を解決します。 -- UTF-8は最も人気があり一般的なUnicodeエンコーディング方法です。これは可変長エンコーディング方法で、優れた拡張性と空間効率を持ちます。UTF-16とUTF-32は固定長エンコーディング方法です。中国語文字をエンコードする際、UTF-16はUTF-8よりも少ない空間を使用します。JavaやC#などのプログラミング言語はデフォルトでUTF-16エンコーディングを使用します。 +- データ構造は、論理構造と物理構造という 2 つの観点から分類できます。論理構造はデータ要素間の論理的関係を記述し、物理構造はデータのコンピュータメモリ上での格納方法を記述します。 +- 代表的な論理構造には、線形、木構造、網状構造などがあります。通常、論理構造に基づいてデータ構造を線形(配列、連結リスト、スタック、キュー)と非線形(木、グラフ、ヒープ)の 2 種類に分類します。ハッシュテーブルの実装には、線形データ構造と非線形データ構造が同時に含まれる場合があります。 +- プログラムの実行時、データはコンピュータメモリに格納されます。各メモリ空間には対応するメモリアドレスがあり、プログラムはそれらのメモリアドレスを通じてデータにアクセスします。 +- 物理構造は主に連続領域への格納(配列)と分散領域への格納(連結リスト)に分けられます。すべてのデータ構造は、配列、連結リスト、またはその両方の組み合わせによって実装されます。 +- コンピュータにおける基本データ型には、整数 `byte`、`short`、`int`、`long`、浮動小数点数 `float`、`double`、文字 `char`、真偽値 `bool` があります。これらの値域は、使用する記憶領域の大きさと表現方式によって決まります。 +- 符号付き絶対値表現、1 の補数、2 の補数は、コンピュータで数値を符号化する 3 つの方法であり、相互に変換できます。整数の符号付き絶対値表現では最上位ビットが符号ビットで、残りのビットが数値の値です。 +- 整数はコンピュータ内では 2 の補数の形式で格納されます。2 の補数表現では、コンピュータは正数と負数の加算を同じように扱うことができ、減算のために特別なハードウェア回路を別途設計する必要がなく、さらに正負のゼロが重複する問題もありません。 +- 浮動小数点数の符号化は、1 ビットの符号部、8 ビットの指数部、23 ビットの仮数部で構成されます。指数部があるため、浮動小数点数の値域は整数よりはるかに広くなりますが、その代償として精度が犠牲になります。 +- ASCII コードは最も早く登場した英字文字集合で、長さは 1 バイト、収録文字数は 127 です。GBK 文字集合はよく使われる中国語文字集合で、2 万字以上の漢字を収録しています。Unicode は完全な文字集合標準を提供することを目指しており、世界中のさまざまな言語の文字を収録することで、文字コード方式の不一致によって生じる文字化けの問題を解決します。 +- UTF-8 は最も広く使われている Unicode の符号化方式で、汎用性が非常に高いです。可変長の符号化方式であり、拡張性に優れ、記憶領域の利用効率を効果的に高めます。UTF-16 と UTF-32 は固定長の符号化方式です。中国語を符号化する場合、UTF-16 は UTF-8 よりも使用領域が小さくなります。Java や C# などのプログラミング言語は、デフォルトで UTF-16 を使用します。 ### 2.   Q & A -**Q**: なぜハッシュ表は線形と非線形の両方のデータ構造を含むのですか? +**Q**:なぜハッシュテーブルには線形データ構造と非線形データ構造が同時に含まれるのですか? -ハッシュ表の基礎構造は配列です。ハッシュ衝突を解決するために、「チェイン法」を使用する場合があります(後の節「ハッシュ衝突」で説明):配列の各バケットは連結リストを指し、その長さが特定の閾値より大きくなると木(通常は赤黒木)に変換される可能性があります。 -格納の観点から、ハッシュ表の基礎構造は配列で、各バケットには値、連結リスト、または木が含まれる場合があります。したがって、ハッシュ表は線形データ構造(配列、連結リスト)と非線形データ構造(木)の両方を含む場合があります。 +ハッシュテーブルの基盤は配列であり、ハッシュ衝突を解決するために「チェイン法」(後続の「ハッシュ衝突」の章で説明します)を使うことがあります。配列内の各バケットは 1 つの連結リストを指し、その連結リストの長さがある閾値を超えると、木(通常は赤黒木)に変換されることもあります。 -**Q**: `char`型の長さは1バイトですか? +格納の観点から見ると、ハッシュテーブルの基盤は配列であり、各バケットスロットには値が入ることもあれば、連結リストや木が入ることもあります。したがって、ハッシュテーブルには線形データ構造(配列、連結リスト)と非線形データ構造(木)が同時に含まれる場合があります。 -`char`型の長さは、プログラミング言語のエンコーディング方法によって決まります。例えば、Java、JavaScript、TypeScript、C#はすべてUTF-16エンコーディング(Unicodeコードポイントを保存するため)を使用するため、`char`型の長さは2バイトです。 +**Q**:`char` 型の長さは 1 バイトですか? -**Q**: 配列ベースのデータ構造を「静的データ構造」と呼ぶことに曖昧さはありませんか?スタックもプッシュやポップなどの「動的」操作を実行できます。 +`char` 型の長さは、プログラミング言語が採用する符号化方式によって決まります。たとえば、Java、JavaScript、TypeScript、C# はいずれも UTF-16 符号化(Unicode コードポイントを保持)を採用しているため、`char` 型の長さは 2 バイトです。 -スタックは動的なデータ操作を実装できますが、データ構造は依然として「静的」です(長さが固定)。配列ベースのデータ構造は動的に要素を追加または削除できますが、その容量は固定されています。スタックサイズが事前に割り当てられたサイズを超える場合、古い配列は新しく作成されたより大きな配列にコピーされます。 +**Q**:配列ベースで実装されたデータ構造を「静的データ構造」と呼ぶのは曖昧ではありませんか? スタックも push や pop などの操作ができ、これらの操作はどれも「動的」です。 -**Q**: スタック(キュー)を構築する際、そのサイズが指定されていないのに、なぜ「静的データ構造」なのですか? +スタックは確かに動的なデータ操作を実現できますが、データ構造自体は依然として「静的」(長さが不変)です。配列ベースのデータ構造でも要素を動的に追加または削除できますが、その容量は固定です。データ量が事前に確保した大きさを超えた場合は、より大きな新しい配列を作成し、古い配列の内容を新しい配列にコピーする必要があります。 -高級プログラミング言語では、スタック(キュー)の初期容量を手動で指定する必要はありません。このタスクはクラス内で自動的に完了されます。例えば、Javaの`ArrayList`の初期容量は通常10です。さらに、拡張操作も自動的に完了されます。詳細については、後続の「リスト」の章を参照してください。 +**Q**:スタック(キュー)を構築するときにサイズを指定していないのに、なぜそれらは「静的データ構造」なのですか? -**Q**: 符号絶対値を2の補数に変換する方法は「最初に否定してから1を加える」ですので、2の補数を符号絶対値に変換することはその逆操作「最初に1を減算してから否定する」であるべきです。 -しかし、2の補数も「最初に否定してから1を加える」を通じて符号絶対値に変換できます。なぜですか? +高水準プログラミング言語では、スタック(キュー)の初期容量を人手で指定する必要はなく、この作業はクラス内部で自動的に行われます。たとえば、Java の `ArrayList` の初期容量は通常 10 です。また、容量拡張も自動的に実装されています。詳しくは後続の「リスト」の章を参照してください。 -**A**: これは、符号絶対値と2の補数間の相互変換が「補数」の計算と等価だからです。まず補数を定義します:$a + b = c$と仮定すると、$a$は$b$の$c$に対する補数と言い、逆に$b$は$a$の$c$に対する補数と言います。 +**Q**:符号付き絶対値表現から 2 の補数への変換方法は「先にビット反転してから 1 を加える」ですが、2 の補数から符号付き絶対値表現への変換は逆演算である「先に 1 を引いてからビット反転する」べきなのに、同じく「先にビット反転してから 1 を加える」でも求められます。これはなぜですか? -長さ$n = 4$の二進数$0010$が与えられた場合、この数が符号絶対値(符号ビットを無視)の場合、その2の補数は「最初に否定してから1を加える」ことで得られます: +これは、符号付き絶対値表現と 2 の補数の相互変換が、実際には「補数」を計算する過程だからです。まず補数の定義を示します。$a + b = c$ とすると、$a$ を $b$ から $c$ への補数と呼び、逆に $b$ も $a$ から $c$ への補数と呼びます。 + +長さ $n = 4$ ビットの 2 進数 $0010$ が与えられたとします。この数を符号付き絶対値表現(符号ビットは考慮しない)とみなすと、その 2 の補数は「先にビット反転してから 1 を加える」ことで得られます。 $$ 0010 \rightarrow 1101 \rightarrow 1110 $$ -符号絶対値と2の補数の和が$0010 + 1110 = 10000$であることを観察します。つまり、2の補数$1110$は符号絶対値$0010$の$10000$に対する「補数」です。**これは、上記の「最初に否定してから1を加える」が$10000$に対する補数の計算と等価であることを意味します**。 +ここで、符号付き絶対値表現と 2 の補数の和は $0010 + 1110 = 10000$ となります。つまり、2 の補数 $1110$ は符号付き絶対値表現 $0010$ から $10000$ への「補数」です。**これは、上記の「先にビット反転してから 1 を加える」が、実際には $10000$ への補数を計算する過程であることを意味します。** -では、$1110$の$10000$に対する「補数」は何でしょうか?「最初に否定してから1を加える」ことで計算できます: +では、2 の補数 $1110$ から $10000$ への「補数」はいくつでしょうか。これもやはり「先にビット反転してから 1 を加える」ことで求められます。 $$ 1110 \rightarrow 0001 \rightarrow 0010 $$ -言い換えると、符号絶対値と2の補数は互いに$10000$に対する「補数」であるため、「符号絶対値から2の補数」と「2の補数から符号絶対値」は同じ操作(最初に否定してから1を加える)で実装できます。 +言い換えると、符号付き絶対値表現と 2 の補数は互いに相手から $10000$ への「補数」なので、「符号付き絶対値表現から 2 の補数への変換」と「2 の補数から符号付き絶対値表現への変換」は同じ操作(先にビット反転してから 1 を加える)で実現できます。 -もちろん、「最初に否定してから1を加える」の逆操作を使用して2の補数$1110$の符号絶対値を求めることもできます。つまり、「最初に1を減算してから否定する」: +もちろん、逆演算を用いて 2 の補数 $1110$ の符号付き絶対値表現を求めることもでき、その場合は「先に 1 を引いてからビット反転する」ことになります。 $$ 1110 \rightarrow 1101 \rightarrow 0010 $$ -要約すると、「最初に否定してから1を加える」と「最初に1を減算してから否定する」は両方とも$10000$に対する補数を計算しており、等価です。 +まとめると、「先にビット反転してから 1 を加える」と「先に 1 を引いてからビット反転する」の 2 つの演算は、どちらも $10000$ への補数を計算しており、等価です。 -本質的に、「否定」操作は実際には$1111$に対する補数を求めることです(`符号絶対値 + 1の補数 = 1111`が常に成り立つため)。そして1の補数に1を加えることは$10000$に対する2の補数と等しくなります。 +本質的には、「ビット反転」という操作は実際には $1111$ への補数を求めています(常に `符号付き絶対値表現 + 1 の補数 = 1111` が成り立つため)。そして、1 の補数にさらに 1 を加えて得られる 2 の補数が、$10000$ への補数です。 -上記では$n = 4$を例に取りましたが、任意の桁数の任意の二進数に一般化できます。 +上記では $n = 4$ を例にしましたが、この考え方は任意のビット長の 2 進数に一般化できます。 diff --git a/ja/docs/chapter_divide_and_conquer/binary_search_recur.md b/ja/docs/chapter_divide_and_conquer/binary_search_recur.md index 9e5ab280a..491d30c3d 100644 --- a/ja/docs/chapter_divide_and_conquer/binary_search_recur.md +++ b/ja/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -2,68 +2,68 @@ comments: true --- -# 12.2   分割統治検索戦略 +# 12.2   分割統治探索戦略 -私たちは検索アルゴリズムが主に2つのカテゴリに分類されることを学びました。 +私たちはすでに学んだように、探索アルゴリズムは大きく二つに分けられる。 -- **総当たり検索**:データ構造を走査することで実装され、時間計算量は $O(n)$ です。 -- **適応検索**:独特なデータ組織形式や事前情報を利用し、時間計算量は $O(\log n)$ または $O(1)$ に達することができます。 +- **力ずく探索**:データ構造を走査することで実現され、時間計算量は $O(n)$ である。 +- **適応的探索**:固有のデータ構造や事前情報を利用し、時間計算量は $O(\log n)$ 、さらには $O(1)$ に達しうる。 -実際、**時間計算量が $O(\log n)$ の検索アルゴリズムは通常分割統治戦略に基づいています**。例えば、二分探索や木などです。 +実際、**時間計算量が $O(\log n)$ の探索アルゴリズムは通常、分割統治戦略に基づいて実装される**。たとえば二分探索や木構造である。 -- 二分探索の各ステップは、問題(配列内でターゲット要素を検索する)をより小さな問題(配列の半分でターゲット要素を検索する)に分割し、配列が空になるかターゲット要素が見つかるまで続けます。 -- 木は分割統治のアイデアを表現し、二分探索木、AVL木、ヒープなどのデータ構造では、様々な操作の時間計算量は $O(\log n)$ です。 +- 二分探索の各ステップでは、問題(配列内で目標要素を探索すること)を小さな問題(配列の半分で目標要素を探索すること)に分解し、この過程は配列が空になるか目標要素が見つかるまで続く。 +- 木構造は分割統治の考え方を代表するものであり、二分探索木、AVL 木、ヒープなどのデータ構造では、さまざまな操作の時間計算量はいずれも $O(\log n)$ である。 -二分探索の分割統治戦略は以下の通りです。 +二分探索の分割統治戦略は以下のとおりである。 -- **問題を分割できる**:二分探索は元の問題(配列内での検索)を部分問題(配列の半分での検索)に再帰的に分割し、中間要素とターゲット要素を比較することで実現されます。 -- **部分問題は独立している**:二分探索では、各ラウンドで一つの部分問題を処理し、他の部分問題に影響されません。 -- **部分問題の解をマージする必要がない**:二分探索は特定の要素を見つけることを目的としているため、部分問題の解をマージする必要がありません。部分問題が解決されると、元の問題も解決されます。 +- **問題は分解できる**:二分探索は、元の問題(配列内で探索すること)を部分問題(配列の半分で探索すること)へ再帰的に分解する。これは中央要素と目標要素を比較することで実現される。 +- **部分問題は独立している**:二分探索では、各ラウンドで一つの部分問題だけを処理し、ほかの部分問題の影響を受けない。 +- **部分問題の解を統合する必要はない**:二分探索は特定の要素を探すことを目的としているため、部分問題の解を統合する必要がない。部分問題が解決されると、元の問題も同時に解決される。 -分割統治は検索効率を向上させることができます。なぜなら、総当たり検索はラウンドごとに1つの選択肢しか除去できませんが、**分割統治は選択肢の半分を除去できるからです**。 +分割統治が探索効率を高められる本質的な理由は、力ずく探索では各ラウンドで一つの候補しか除外できないのに対し、**分割統治による探索では各ラウンドで候補の半分を除外できる**からである。 -### 1.   分割統治に基づく二分探索の実装 +### 1.   分割統治に基づく二分探索 -前の章では、二分探索は反復に基づいて実装されました。今度は、分割統治(再帰)に基づいて実装します。 +前の章では、二分探索を漸化式(反復)に基づいて実装した。ここでは分割統治(再帰)に基づいてこれを実装する。 !!! question - 長さ $n$ の順序付けられた配列 `nums` が与えられ、すべての要素が一意である場合、要素 `target` を見つけてください。 + 長さ $n$ の昇順配列 `nums` が与えられ、そのすべての要素は一意である。要素 `target` を探索せよ。 -分割統治の観点から、検索区間 $[i, j]$ に対応する部分問題を $f(i, j)$ と表します。 +分割統治の観点から、探索区間 $[i, j]$ に対応する部分問題を $f(i, j)$ と記す。 -元の問題 $f(0, n-1)$ から開始して、以下のステップで二分探索を実行します。 +元の問題 $f(0, n-1)$ を出発点として、次の手順で二分探索を行う。 -1. 検索区間 $[i, j]$ の中点 $m$ を計算し、それを使用して検索区間の半分を除去します。 -2. 半分のサイズに縮小された部分問題を再帰的に解決します。これは $f(i, m-1)$ または $f(m+1, j)$ になる可能性があります。 -3. `target` が見つかるか区間が空になってリターンするまで、ステップ `1.` と `2.` を繰り返します。 +1. 探索区間 $[i, j]$ の中点 $m$ を計算し、それに基づいて探索区間の半分を除外する。 +2. 規模が半分に縮小された部分問題を再帰的に解く。候補は $f(i, m-1)$ または $f(m+1, j)$ である。 +3. `1.` と `2.` の手順を繰り返し、`target` が見つかるか区間が空になったら返す。 -以下の図は、配列内で要素 $6$ を探す二分探索の分割統治過程を示しています。 +次の図は、配列内で要素 $6$ を二分探索する分割統治の過程を示している。 -![二分探索の分割統治過程](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" } +![二分探索の分割統治の過程](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" } -

図 12-4   二分探索の分割統治過程

+

図 12-4   二分探索の分割統治の過程

-実装コードでは、問題 $f(i, j)$ を解決するために再帰関数 `dfs()` を宣言します: +実装コードでは、再帰関数 `dfs()` を宣言して問題 $f(i, j)$ を解く。 === "Python" ```python title="binary_search_recur.py" def dfs(nums: list[int], target: int, i: int, j: int) -> int: """二分探索:問題 f(i, j)""" - # 区間が空の場合、対象要素がないことを示すため、-1 を返す + # 区間が空なら対象要素は存在しないので -1 を返す if i > j: return -1 # 中点インデックス m を計算 m = (i + j) // 2 if nums[m] < target: - # 再帰部分問題 f(m+1, j) + # 部分問題 f(m+1, j) を再帰的に解く return dfs(nums, target, m + 1, j) elif nums[m] > target: - # 再帰部分問題 f(i, m-1) + # 部分問題 f(i, m-1) を再帰的に解く return dfs(nums, target, i, m - 1) else: - # 対象要素を発見したため、そのインデックスを返す + # 目標要素が見つかったらそのインデックスを返す return m def binary_search(nums: list[int], target: int) -> int: @@ -78,20 +78,20 @@ comments: true ```cpp title="binary_search_recur.cpp" /* 二分探索:問題 f(i, j) */ int dfs(vector &nums, int target, int i, int j) { - // 区間が空の場合、対象要素が存在しないことを示すため、-1 を返す + // 区間が空なら対象要素は存在しないので -1 を返す if (i > j) { return -1; } // 中点インデックス m を計算 - int m = i + (j - i) / 2; + int m = (i + j) / 2; if (nums[m] < target) { - // 再帰的な部分問題 f(m+1, j) + // 部分問題 f(m+1, j) を再帰的に解く return dfs(nums, target, m + 1, j); } else if (nums[m] > target) { - // 再帰的な部分問題 f(i, m-1) + // 部分問題 f(i, m-1) を再帰的に解く return dfs(nums, target, i, m - 1); } else { - // 対象要素が見つかったため、そのインデックスを返す + // 目標要素が見つかったらそのインデックスを返す return m; } } @@ -109,20 +109,20 @@ comments: true ```java title="binary_search_recur.java" /* 二分探索:問題 f(i, j) */ int dfs(int[] nums, int target, int i, int j) { - // 区間が空の場合、対象要素が存在しないことを示すため、-1 を返す + // 区間が空なら対象要素は存在しないので -1 を返す if (i > j) { return -1; } // 中点インデックス m を計算 - int m = i + (j - i) / 2; + int m = (i + j) / 2; if (nums[m] < target) { - // 再帰的な部分問題 f(m+1, j) + // 部分問題 f(m+1, j) を再帰的に解く return dfs(nums, target, m + 1, j); } else if (nums[m] > target) { - // 再帰的な部分問題 f(i, m-1) + // 部分問題 f(i, m-1) を再帰的に解く return dfs(nums, target, i, m - 1); } else { - // 対象要素が見つかったため、そのインデックスを返す + // 目標要素が見つかったらそのインデックスを返す return m; } } @@ -138,79 +138,319 @@ comments: true === "C#" ```csharp title="binary_search_recur.cs" - [class]{binary_search_recur}-[func]{DFS} + /* 二分探索:問題 f(i, j) */ + int DFS(int[] nums, int target, int i, int j) { + // 区間が空なら対象要素は存在しないので -1 を返す + if (i > j) { + return -1; + } + // 中点インデックス m を計算 + int m = (i + j) / 2; + if (nums[m] < target) { + // 部分問題 f(m+1, j) を再帰的に解く + return DFS(nums, target, m + 1, j); + } else if (nums[m] > target) { + // 部分問題 f(i, m-1) を再帰的に解く + return DFS(nums, target, i, m - 1); + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } - [class]{binary_search_recur}-[func]{BinarySearch} + /* 二分探索 */ + int BinarySearch(int[] nums, int target) { + int n = nums.Length; + // 問題 f(0, n-1) を解く + return DFS(nums, target, 0, n - 1); + } ``` === "Go" ```go title="binary_search_recur.go" - [class]{}-[func]{dfs} + /* 二分探索:問題 f(i, j) */ + func dfs(nums []int, target, i, j int) int { + // 区間が空なら対象要素は存在しないため、-1 を返す + if i > j { + return -1 + } + // 中点インデックスを計算する + m := i + ((j - i) >> 1) + // 中点の要素と目標要素の大小を判定する + if nums[m] < target { + // 小さければ右半分の配列を再帰 + // 部分問題 f(m+1, j) を解く + return dfs(nums, target, m+1, j) + } else if nums[m] > target { + // 大きければ左半分の配列を再帰 + // 部分問題 f(i, m-1) を解く + return dfs(nums, target, i, m-1) + } else { + // 目標要素が見つかったらそのインデックスを返す + return m + } + } - [class]{}-[func]{binarySearch} + /* 二分探索 */ + func binarySearch(nums []int, target int) int { + n := len(nums) + return dfs(nums, target, 0, n-1) + } ``` === "Swift" ```swift title="binary_search_recur.swift" - [class]{}-[func]{dfs} + /* 二分探索:問題 f(i, j) */ + func dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int { + // 区間が空なら対象要素は存在しないので -1 を返す + if i > j { + return -1 + } + // 中点インデックス m を計算 + let m = (i + j) / 2 + if nums[m] < target { + // 部分問題 f(m+1, j) を再帰的に解く + return dfs(nums: nums, target: target, i: m + 1, j: j) + } else if nums[m] > target { + // 部分問題 f(i, m-1) を再帰的に解く + return dfs(nums: nums, target: target, i: i, j: m - 1) + } else { + // 目標要素が見つかったらそのインデックスを返す + return m + } + } - [class]{}-[func]{binarySearch} + /* 二分探索 */ + func binarySearch(nums: [Int], target: Int) -> Int { + // 問題 f(0, n-1) を解く + dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1) + } ``` === "JS" ```javascript title="binary_search_recur.js" - [class]{}-[func]{dfs} + /* 二分探索:問題 f(i, j) */ + function dfs(nums, target, i, j) { + // 区間が空なら対象要素は存在しないので -1 を返す + if (i > j) { + return -1; + } + // 中点インデックス m を計算 + const m = i + ((j - i) >> 1); + if (nums[m] < target) { + // 部分問題 f(m+1, j) を再帰的に解く + return dfs(nums, target, m + 1, j); + } else if (nums[m] > target) { + // 部分問題 f(i, m-1) を再帰的に解く + return dfs(nums, target, i, m - 1); + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } - [class]{}-[func]{binarySearch} + /* 二分探索 */ + function binarySearch(nums, target) { + const n = nums.length; + // 問題 f(0, n-1) を解く + return dfs(nums, target, 0, n - 1); + } ``` === "TS" ```typescript title="binary_search_recur.ts" - [class]{}-[func]{dfs} + /* 二分探索:問題 f(i, j) */ + function dfs(nums: number[], target: number, i: number, j: number): number { + // 区間が空なら対象要素は存在しないので -1 を返す + if (i > j) { + return -1; + } + // 中点インデックス m を計算 + const m = i + ((j - i) >> 1); + if (nums[m] < target) { + // 部分問題 f(m+1, j) を再帰的に解く + return dfs(nums, target, m + 1, j); + } else if (nums[m] > target) { + // 部分問題 f(i, m-1) を再帰的に解く + return dfs(nums, target, i, m - 1); + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } - [class]{}-[func]{binarySearch} + /* 二分探索 */ + function binarySearch(nums: number[], target: number): number { + const n = nums.length; + // 問題 f(0, n-1) を解く + return dfs(nums, target, 0, n - 1); + } ``` === "Dart" ```dart title="binary_search_recur.dart" - [class]{}-[func]{dfs} + /* 二分探索:問題 f(i, j) */ + int dfs(List nums, int target, int i, int j) { + // 区間が空なら対象要素は存在しないので -1 を返す + if (i > j) { + return -1; + } + // 中点インデックス m を計算 + int m = (i + j) ~/ 2; + if (nums[m] < target) { + // 部分問題 f(m+1, j) を再帰的に解く + return dfs(nums, target, m + 1, j); + } else if (nums[m] > target) { + // 部分問題 f(i, m-1) を再帰的に解く + return dfs(nums, target, i, m - 1); + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } - [class]{}-[func]{binarySearch} + /* 二分探索 */ + int binarySearch(List nums, int target) { + int n = nums.length; + // 問題 f(0, n-1) を解く + return dfs(nums, target, 0, n - 1); + } ``` === "Rust" ```rust title="binary_search_recur.rs" - [class]{}-[func]{dfs} + /* 二分探索:問題 f(i, j) */ + fn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 { + // 区間が空なら対象要素は存在しないので -1 を返す + if i > j { + return -1; + } + let m: i32 = i + (j - i) / 2; + if nums[m as usize] < target { + // 部分問題 f(m+1, j) を再帰的に解く + return dfs(nums, target, m + 1, j); + } else if nums[m as usize] > target { + // 部分問題 f(i, m-1) を再帰的に解く + return dfs(nums, target, i, m - 1); + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } - [class]{}-[func]{binary_search} + /* 二分探索 */ + fn binary_search(nums: &[i32], target: i32) -> i32 { + let n = nums.len() as i32; + // 問題 f(0, n-1) を解く + dfs(nums, target, 0, n - 1) + } ``` === "C" ```c title="binary_search_recur.c" - [class]{}-[func]{dfs} + /* 二分探索:問題 f(i, j) */ + int dfs(int nums[], int target, int i, int j) { + // 区間が空なら対象要素は存在しないので -1 を返す + if (i > j) { + return -1; + } + // 中点インデックス m を計算 + int m = (i + j) / 2; + if (nums[m] < target) { + // 部分問題 f(m+1, j) を再帰的に解く + return dfs(nums, target, m + 1, j); + } else if (nums[m] > target) { + // 部分問題 f(i, m-1) を再帰的に解く + return dfs(nums, target, i, m - 1); + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } - [class]{}-[func]{binarySearch} + /* 二分探索 */ + int binarySearch(int nums[], int target, int numsSize) { + int n = numsSize; + // 問題 f(0, n-1) を解く + return dfs(nums, target, 0, n - 1); + } ``` === "Kotlin" ```kotlin title="binary_search_recur.kt" - [class]{}-[func]{dfs} + /* 二分探索:問題 f(i, j) */ + fun dfs( + nums: IntArray, + target: Int, + i: Int, + j: Int + ): Int { + // 区間が空なら対象要素は存在しないので -1 を返す + if (i > j) { + return -1 + } + // 中点インデックス m を計算 + val m = (i + j) / 2 + return if (nums[m] < target) { + // 部分問題 f(m+1, j) を再帰的に解く + dfs(nums, target, m + 1, j) + } else if (nums[m] > target) { + // 部分問題 f(i, m-1) を再帰的に解く + dfs(nums, target, i, m - 1) + } else { + // 目標要素が見つかったらそのインデックスを返す + m + } + } - [class]{}-[func]{binarySearch} + /* 二分探索 */ + fun binarySearch(nums: IntArray, target: Int): Int { + val n = nums.size + // 問題 f(0, n-1) を解く + return dfs(nums, target, 0, n - 1) + } ``` === "Ruby" ```ruby title="binary_search_recur.rb" - [class]{}-[func]{dfs} + ### 二分探索: 問題 f(i, j) ### + def dfs(nums, target, i, j) + # 区間が空なら対象要素は存在しないので -1 を返す + return -1 if i > j + + # 中点インデックス m を計算 + m = (i + j) / 2 - [class]{}-[func]{binary_search} + if nums[m] < target + # 部分問題 f(m+1, j) を再帰的に解く + return dfs(nums, target, m + 1, j) + elsif nums[m] > target + # 部分問題 f(i, m-1) を再帰的に解く + return dfs(nums, target, i, m - 1) + else + # 目標要素が見つかったらそのインデックスを返す + return m + end + end + + ### 二分探索 ### + def binary_search(nums, target) + n = nums.length + # 問題 f(0, n-1) を解く + dfs(nums, target, 0, n - 1) + end ``` + +??? pythontutor "コードの可視化" + +
+ diff --git a/ja/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/ja/docs/chapter_divide_and_conquer/build_binary_tree_problem.md index 4a9768784..2d3a1b520 100644 --- a/ja/docs/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/ja/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -2,74 +2,74 @@ comments: true --- -# 12.3   二分木構築問題 +# 12.3   二分木の構築問題 !!! question - 二分木の前順走査 `preorder` シーケンスと中順走査 `inorder` シーケンスが与えられた場合、二分木を構築してそのルートノードを返してください。二分木に重複するノード値がないと仮定します(以下の図に示すように)。 + 二分木の前順走査 `preorder` と中順走査 `inorder` が与えられたとき、これらから二分木を構築し、その根ノードを返してください。二分木には値が重複するノードが存在しないものとします(下図のとおり)。 -![二分木構築のサンプルデータ](build_binary_tree_problem.assets/build_tree_example.png){ class="animation-figure" } +![二分木を構築する例のデータ](build_binary_tree_problem.assets/build_tree_example.png){ class="animation-figure" } -

図 12-5   二分木構築のサンプルデータ

+

図 12-5   二分木を構築する例のデータ

-### 1.   分割統治問題かどうかの判定 +### 1.   分割統治問題かどうかを判断する -`preorder` と `inorder` シーケンスから二分木を構築する元の問題は、典型的な分割統治問題です。 +元の問題は `preorder` と `inorder` から二分木を構築することであり、典型的な分割統治問題です。 -- **問題を分解できる**:分割統治の観点から、元の問題を2つの部分問題(左の部分木の構築と右の部分木の構築)とルートノードの初期化という1つの操作に分割できます。各部分木(部分問題)について、同じアプローチを継続的に適用し、より小さな部分木(部分問題)に分割し、最小の部分問題(空の部分木)に到達するまで続けます。 -- **部分問題は独立している**:左と右の部分木は重複しません。左の部分木を構築する際、左の部分木に対応する中順走査と前順走査のセグメントのみが必要です。右の部分木にも同じアプローチが適用されます。 -- **部分問題の解を組み合わせることができる**:左と右の部分木(部分問題の解)を構築したら、それらをルートノードに接続して元の問題の解を取得できます。 +- **問題は分解できる**:分割統治の観点から見ると、元の問題は 2 つの部分問題、すなわち左部分木の構築と右部分木の構築に分けられ、さらに根ノードを初期化する 1 ステップが加わります。各部分木(部分問題)に対しても、同じ分割方法を再利用してより小さな部分木(部分問題)へと分けていき、最小の部分問題(空部分木)に達した時点で終了します。 +- **部分問題は独立している**:左部分木と右部分木は互いに独立しており、両者の間に重なりはありません。左部分木を構築するときは、中順走査と前順走査のうち左部分木に対応する部分だけを見れば十分です。右部分木も同様です。 +- **部分問題の解は統合できる**:左部分木と右部分木(部分問題の解)が得られたら、それらを根ノードに接続することで元の問題の解を得られます。 -### 2.   部分木の分割方法 +### 2.   部分木をどのように分割するか -上記の分析に基づいて、この問題は分割統治を使用して解決できます。**しかし、前順走査 `preorder` シーケンスと中順走査 `inorder` シーケンスを使用して左と右の部分木をどのように分割すればよいでしょうか?** +以上の分析より、この問題は分割統治で解けます。**では、前順走査 `preorder` と中順走査 `inorder` を使って左部分木と右部分木をどのように分割すればよいのでしょうか**? -定義により、`preorder` と `inorder` シーケンスの両方を3つの部分に分割できます: +定義に従うと、`preorder` と `inorder` はいずれも 3 つの部分に分けられます。 -- 前順走査:`[ ルート | 左の部分木 | 右の部分木 ]`。例えば、図では、木は `[ 3 | 9 | 2 1 7 ]` に対応します。 -- 中順走査:`[ 左の部分木 | ルート | 右の部分木 ]`。例えば、図では、木は `[ 9 | 3 | 1 2 7 ]` に対応します。 +- 前順走査:`[ 根ノード | 左部分木 | 右部分木 ]` ,例えば上図の木は `[ 3 | 9 | 2 1 7 ]` に対応します。 +- 中順走査:`[ 左部分木 | 根ノード | 右部分木 ]` ,例えば上図の木は `[ 9 | 3 | 1 2 7 ]` に対応します。 -前の図のデータを使用して、次の図に示すステップに従って分割結果を取得できます: +上図のデータを例にすると、下図の手順によって分割結果を得られます。 -1. 前順走査の最初の要素3がルートノードの値です。 -2. `inorder` シーケンス内でルートノード3のインデックスを見つけ、このインデックスを使用して `inorder` を `[ 9 | 3 | 1 2 7 ]` に分割します。 -3. `inorder` シーケンスの分割に従って、左と右の部分木がそれぞれ1個と3個のノードを含むことが簡単に決定できるため、`preorder` シーケンスを `[ 3 | 9 | 2 1 7 ]` に対応して分割できます。 +1. 前順走査の先頭要素 3 が根ノードの値です。 +2. 根ノード 3 の `inorder` におけるインデックスを探すと、そのインデックスを用いて `inorder` を `[ 9 | 3 | 1 2 7 ]` に分割できます。 +3. `inorder` の分割結果から、左部分木と右部分木のノード数はそれぞれ 1 と 3 であることがわかり、したがって `preorder` を `[ 3 | 9 | 2 1 7 ]` に分割できます。 -![前順走査と中順走査での部分木の分割](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png){ class="animation-figure" } +![前順走査と中順走査で部分木を分割する](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png){ class="animation-figure" } -

図 12-6   前順走査と中順走査での部分木の分割

+

図 12-6   前順走査と中順走査で部分木を分割する

-### 3.   変数に基づく部分木範囲の記述 +### 3.   変数を用いて部分木区間を記述する -上記の分割方法に基づいて、**`preorder` と `inorder` シーケンスにおけるルート、左の部分木、右の部分木のインデックス範囲を取得しました**。これらのインデックス範囲を記述するために、いくつかのポインタ変数を使用します。 +以上の分割方法により、**根ノード、左部分木、右部分木が `preorder` と `inorder` の中で占めるインデックス区間**が得られました。これらのインデックス区間を表すために、いくつかのポインタ変数を導入します。 -- 現在の木のルートノードの `preorder` シーケンスでのインデックスを $i$ とします。 -- 現在の木のルートノードの `inorder` シーケンスでのインデックスを $m$ とします。 -- 現在の木の `inorder` シーケンスでのインデックス範囲を $[l, r]$ とします。 +- 現在の木の根ノードが `preorder` に現れるインデックスを $i$ とします。 +- 現在の木の根ノードが `inorder` に現れるインデックスを $m$ とします。 +- 現在の木が `inorder` において占めるインデックス区間を $[l, r]$ とします。 -以下の表に示すように、これらの変数は `preorder` シーケンスでのルートノードのインデックスと `inorder` シーケンスでの部分木のインデックス範囲を表します。 +次の表のように、これらの変数を用いれば根ノードの `preorder` におけるインデックスと、部分木の `inorder` におけるインデックス区間を表せます。 -

表 12-1   前順走査と中順走査でのルートノードと部分木のインデックス

+

表 12-1   根ノードと部分木の前順走査・中順走査におけるインデックス

-| | `preorder` でのルートノードインデックス | `inorder` での部分木インデックス範囲 | -| ------------- | ------------------------------------- | ----------------------------------- | -| 現在の木 | $i$ | $[l, r]$ | -| 左の部分木 | $i + 1$ | $[l, m-1]$ | -| 右の部分木 | $i + 1 + (m - l)$ | $[m+1, r]$ | +| | 根ノードの `preorder` におけるインデックス | 部分木の `inorder` におけるインデックス区間 | +| ------ | ---------------------------- | ----------------------------- | +| 現在の木 | $i$ | $[l, r]$ | +| 左部分木 | $i + 1$ | $[l, m-1]$ | +| 右部分木 | $i + 1 + (m - l)$ | $[m+1, r]$ |
-右の部分木のルートインデックスの $(m-l)$ は「左の部分木のノード数」を表すことに注意してください。より明確な理解のために、以下の図を参照することが役立つ場合があります。 +右部分木の根ノードのインデックスにある $(m-l)$ は「左部分木のノード数」を意味します。下図と合わせて理解することを勧めます。 -![ルートノードと左右の部分木のインデックス](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" } +![根ノードと左右部分木のインデックス区間の表し方](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" } -

図 12-7   ルートノードと左右の部分木のインデックス

+

図 12-7   根ノードと左右部分木のインデックス区間の表し方

-### 4.   コード実装 +### 4.   コードの実装 -$m$ の問い合わせの効率を向上させるために、ハッシュテーブル `hmap` を使用して `inorder` シーケンスの要素からそのインデックスへのマッピングを格納します: +$m$ の検索効率を高めるために、ハッシュテーブル `hmap` を用いて配列 `inorder` の要素からインデックスへの対応を保存します。 === "Python" @@ -81,24 +81,24 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー l: int, r: int, ) -> TreeNode | None: - """二分木の構築:分割統治""" - # 部分木の区間が空のとき終了 + """二分木を構築:分割統治""" + # 部分木区間が空なら終了する if r - l < 0: return None - # ルートノードを初期化 + # ルートノードを初期化する root = TreeNode(preorder[i]) - # m をクエリして左部分木と右部分木を分割 + # m を求めて左右部分木を分割する m = inorder_map[preorder[i]] - # 部分問題:左部分木を構築 + # 部分問題:左部分木を構築する root.left = dfs(preorder, inorder_map, i + 1, l, m - 1) - # 部分問題:右部分木を構築 + # 部分問題:右部分木を構築する root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r) - # ルートノードを返す + # 根ノードを返す return root def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None: """二分木を構築""" - # ハッシュテーブルを初期化、中順走査の要素からインデックスへのマッピングを保存 + # inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する inorder_map = {val: i for i, val in enumerate(inorder)} root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1) return root @@ -107,26 +107,26 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー === "C++" ```cpp title="build_tree.cpp" - /* 二分木の構築:分割統治 */ + /* 二分木を構築:分割統治 */ TreeNode *dfs(vector &preorder, unordered_map &inorderMap, int i, int l, int r) { - // 部分木の区間が空の場合に終了 + // 部分木区間が空なら終了する if (r - l < 0) return NULL; - // ルートノードを初期化 + // ルートノードを初期化する TreeNode *root = new TreeNode(preorder[i]); - // m を問い合わせて左右の部分木を分割 + // m を求めて左右部分木を分割する int m = inorderMap[preorder[i]]; - // 部分問題:左の部分木を構築 + // 部分問題:左部分木を構築する root->left = dfs(preorder, inorderMap, i + 1, l, m - 1); - // 部分問題:右の部分木を構築 + // 部分問題:右部分木を構築する root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r); - // ルートノードを返す + // 根ノードを返す return root; } - /* 二分木の構築 */ + /* 二分木を構築 */ TreeNode *buildTree(vector &preorder, vector &inorder) { - // ハッシュテーブルを初期化し、中間順序の要素からインデックスへのマッピングを格納 + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する unordered_map inorderMap; for (int i = 0; i < inorder.size(); i++) { inorderMap[inorder[i]] = i; @@ -139,26 +139,26 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー === "Java" ```java title="build_tree.java" - /* 二分木の構築:分割統治 */ + /* 二分木を構築:分割統治 */ TreeNode dfs(int[] preorder, Map inorderMap, int i, int l, int r) { - // 部分木の区間が空の場合に終了 + // 部分木区間が空なら終了する if (r - l < 0) return null; - // ルートノードを初期化 + // ルートノードを初期化する TreeNode root = new TreeNode(preorder[i]); - // m を問い合わせて左右の部分木を分割 + // m を求めて左右部分木を分割する int m = inorderMap.get(preorder[i]); - // 部分問題:左の部分木を構築 + // 部分問題:左部分木を構築する root.left = dfs(preorder, inorderMap, i + 1, l, m - 1); - // 部分問題:右の部分木を構築 + // 部分問題:右部分木を構築する root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r); - // ルートノードを返す + // 根ノードを返す return root; } - /* 二分木の構築 */ + /* 二分木を構築 */ TreeNode buildTree(int[] preorder, int[] inorder) { - // ハッシュテーブルを初期化し、中間順序の要素からインデックスへのマッピングを格納 + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する Map inorderMap = new HashMap<>(); for (int i = 0; i < inorder.length; i++) { inorderMap.put(inorder[i], i); @@ -171,87 +171,356 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー === "C#" ```csharp title="build_tree.cs" - [class]{build_tree}-[func]{DFS} + /* 二分木を構築:分割統治 */ + TreeNode? DFS(int[] preorder, Dictionary inorderMap, int i, int l, int r) { + // 部分木区間が空なら終了する + if (r - l < 0) + return null; + // ルートノードを初期化する + TreeNode root = new(preorder[i]); + // m を求めて左右部分木を分割する + int m = inorderMap[preorder[i]]; + // 部分問題:左部分木を構築する + root.left = DFS(preorder, inorderMap, i + 1, l, m - 1); + // 部分問題:右部分木を構築する + root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r); + // 根ノードを返す + return root; + } - [class]{build_tree}-[func]{BuildTree} + /* 二分木を構築 */ + TreeNode? BuildTree(int[] preorder, int[] inorder) { + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する + Dictionary inorderMap = []; + for (int i = 0; i < inorder.Length; i++) { + inorderMap.TryAdd(inorder[i], i); + } + TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1); + return root; + } ``` === "Go" ```go title="build_tree.go" - [class]{}-[func]{dfsBuildTree} + /* 二分木を構築:分割統治 */ + func dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode { + // 部分木区間が空なら終了する + if r-l < 0 { + return nil + } + // ルートノードを初期化する + root := NewTreeNode(preorder[i]) + // m を求めて左右部分木を分割する + m := inorderMap[preorder[i]] + // 部分問題:左部分木を構築する + root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1) + // 部分問題:右部分木を構築する + root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r) + // 根ノードを返す + return root + } - [class]{}-[func]{buildTree} + /* 二分木を構築 */ + func buildTree(preorder, inorder []int) *TreeNode { + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する + inorderMap := make(map[int]int, len(inorder)) + for i := 0; i < len(inorder); i++ { + inorderMap[inorder[i]] = i + } + + root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1) + return root + } ``` === "Swift" ```swift title="build_tree.swift" - [class]{}-[func]{dfs} + /* 二分木を構築:分割統治 */ + func dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? { + // 部分木区間が空なら終了する + if r - l < 0 { + return nil + } + // ルートノードを初期化する + let root = TreeNode(x: preorder[i]) + // m を求めて左右部分木を分割する + let m = inorderMap[preorder[i]]! + // 部分問題:左部分木を構築する + root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1) + // 部分問題:右部分木を構築する + root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r) + // 根ノードを返す + return root + } - [class]{}-[func]{buildTree} + /* 二分木を構築 */ + func buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? { + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する + let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } + return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1) + } ``` === "JS" ```javascript title="build_tree.js" - [class]{}-[func]{dfs} + /* 二分木を構築:分割統治 */ + function dfs(preorder, inorderMap, i, l, r) { + // 部分木区間が空なら終了する + if (r - l < 0) return null; + // ルートノードを初期化する + const root = new TreeNode(preorder[i]); + // m を求めて左右部分木を分割する + const m = inorderMap.get(preorder[i]); + // 部分問題:左部分木を構築する + root.left = dfs(preorder, inorderMap, i + 1, l, m - 1); + // 部分問題:右部分木を構築する + root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r); + // 根ノードを返す + return root; + } - [class]{}-[func]{buildTree} + /* 二分木を構築 */ + function buildTree(preorder, inorder) { + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する + let inorderMap = new Map(); + for (let i = 0; i < inorder.length; i++) { + inorderMap.set(inorder[i], i); + } + const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1); + return root; + } ``` === "TS" ```typescript title="build_tree.ts" - [class]{}-[func]{dfs} + /* 二分木を構築:分割統治 */ + function dfs( + preorder: number[], + inorderMap: Map, + i: number, + l: number, + r: number + ): TreeNode | null { + // 部分木区間が空なら終了する + if (r - l < 0) return null; + // ルートノードを初期化する + const root: TreeNode = new TreeNode(preorder[i]); + // m を求めて左右部分木を分割する + const m = inorderMap.get(preorder[i]); + // 部分問題:左部分木を構築する + root.left = dfs(preorder, inorderMap, i + 1, l, m - 1); + // 部分問題:右部分木を構築する + root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r); + // 根ノードを返す + return root; + } - [class]{}-[func]{buildTree} + /* 二分木を構築 */ + function buildTree(preorder: number[], inorder: number[]): TreeNode | null { + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する + let inorderMap = new Map(); + for (let i = 0; i < inorder.length; i++) { + inorderMap.set(inorder[i], i); + } + const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1); + return root; + } ``` === "Dart" ```dart title="build_tree.dart" - [class]{}-[func]{dfs} + /* 二分木を構築:分割統治 */ + TreeNode? dfs( + List preorder, + Map inorderMap, + int i, + int l, + int r, + ) { + // 部分木区間が空なら終了する + if (r - l < 0) { + return null; + } + // ルートノードを初期化する + TreeNode? root = TreeNode(preorder[i]); + // m を求めて左右部分木を分割する + int m = inorderMap[preorder[i]]!; + // 部分問題:左部分木を構築する + root.left = dfs(preorder, inorderMap, i + 1, l, m - 1); + // 部分問題:右部分木を構築する + root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r); + // 根ノードを返す + return root; + } - [class]{}-[func]{buildTree} + /* 二分木を構築 */ + TreeNode? buildTree(List preorder, List inorder) { + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する + Map inorderMap = {}; + for (int i = 0; i < inorder.length; i++) { + inorderMap[inorder[i]] = i; + } + TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1); + return root; + } ``` === "Rust" ```rust title="build_tree.rs" - [class]{}-[func]{dfs} + /* 二分木を構築:分割統治 */ + fn dfs( + preorder: &[i32], + inorder_map: &HashMap, + i: i32, + l: i32, + r: i32, + ) -> Option>> { + // 部分木区間が空なら終了する + if r - l < 0 { + return None; + } + // ルートノードを初期化する + let root = TreeNode::new(preorder[i as usize]); + // m を求めて左右部分木を分割する + let m = inorder_map.get(&preorder[i as usize]).unwrap(); + // 部分問題:左部分木を構築する + root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1); + // 部分問題:右部分木を構築する + root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r); + // 根ノードを返す + Some(root) + } - [class]{}-[func]{build_tree} + /* 二分木を構築 */ + fn build_tree(preorder: &[i32], inorder: &[i32]) -> Option>> { + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する + let mut inorder_map: HashMap = HashMap::new(); + for i in 0..inorder.len() { + inorder_map.insert(inorder[i], i as i32); + } + let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1); + root + } ``` === "C" ```c title="build_tree.c" - [class]{}-[func]{dfs} + /* 二分木を構築:分割統治 */ + TreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) { + // 部分木区間が空なら終了する + if (r - l < 0) + return NULL; + // ルートノードを初期化する + TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode)); + root->val = preorder[i]; + root->left = NULL; + root->right = NULL; + // m を求めて左右部分木を分割する + int m = inorderMap[preorder[i]]; + // 部分問題:左部分木を構築する + root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size); + // 部分問題:右部分木を構築する + root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size); + // 根ノードを返す + return root; + } - [class]{}-[func]{buildTree} + /* 二分木を構築 */ + TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) { + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する + int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE); + for (int i = 0; i < inorderSize; i++) { + inorderMap[inorder[i]] = i; + } + TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize); + free(inorderMap); + return root; + } ``` === "Kotlin" ```kotlin title="build_tree.kt" - [class]{}-[func]{dfs} + /* 二分木を構築:分割統治 */ + fun dfs( + preorder: IntArray, + inorderMap: Map, + i: Int, + l: Int, + r: Int + ): TreeNode? { + // 部分木区間が空なら終了する + if (r - l < 0) return null + // ルートノードを初期化する + val root = TreeNode(preorder[i]) + // m を求めて左右部分木を分割する + val m = inorderMap[preorder[i]]!! + // 部分問題:左部分木を構築する + root.left = dfs(preorder, inorderMap, i + 1, l, m - 1) + // 部分問題:右部分木を構築する + root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r) + // 根ノードを返す + return root + } - [class]{}-[func]{buildTree} + /* 二分木を構築 */ + fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? { + // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する + val inorderMap = HashMap() + for (i in inorder.indices) { + inorderMap[inorder[i]] = i + } + val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1) + return root + } ``` === "Ruby" ```ruby title="build_tree.rb" - [class]{}-[func]{dfs} + ### 二分木を構築:分割統治 ### + def dfs(preorder, inorder_map, i, l, r) + # 部分木区間が空なら終了する + return if r - l < 0 - [class]{}-[func]{build_tree} + # ルートノードを初期化する + root = TreeNode.new(preorder[i]) + # m を求めて左右部分木を分割する + m = inorder_map[preorder[i]] + # 部分問題:左部分木を構築する + root.left = dfs(preorder, inorder_map, i + 1, l, m - 1) + # 部分問題:右部分木を構築する + root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r) + + # 根ノードを返す + root + end + + ### 二分木を構築 ### + def build_tree(preorder, inorder) + # inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する + inorder_map = {} + inorder.each_with_index { |val, i| inorder_map[val] = i } + dfs(preorder, inorder_map, 0, 0, inorder.length - 1) + end ``` -以下の図は、二分木を構築する再帰過程を示しています。各ノードは再帰の「下降」段階で作成され、各エッジ(参照)は「上昇」段階で形成されます。 +??? pythontutor "コードの可視化" + +
+ + +下図は二分木を構築する再帰過程を示しています。各ノードは下向きに「再帰していく」過程で生成され、各辺(参照)は上向きに「戻る」過程で張られます。 === "<1>" - ![二分木構築の再帰過程](build_binary_tree_problem.assets/built_tree_step1.png){ class="animation-figure" } + ![二分木を構築する再帰過程](build_binary_tree_problem.assets/built_tree_step1.png){ class="animation-figure" } === "<2>" ![built_tree_step2](build_binary_tree_problem.assets/built_tree_step2.png){ class="animation-figure" } @@ -277,14 +546,14 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー === "<9>" ![built_tree_step9](build_binary_tree_problem.assets/built_tree_step9.png){ class="animation-figure" } -

図 12-8   二分木構築の再帰過程

+

図 12-8   二分木を構築する再帰過程

-各再帰関数の `preorder` と `inorder` シーケンスの分割は以下の図に示されています。 +各再帰関数における前順走査 `preorder` と中順走査 `inorder` の分割結果を下図に示します。 -![各再帰関数での分割](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" } +![各再帰関数での分割結果](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" } -

図 12-9   各再帰関数での分割

+

図 12-9   各再帰関数での分割結果

-二分木が $n$ 個のノードを持つと仮定すると、各ノードの初期化(再帰関数 `dfs()` の呼び出し)には $O(1)$ 時間がかかります。**したがって、全体の時間計算量は $O(n)$ です**。 +木のノード数を $n$ とすると、各ノードの初期化(再帰関数 `dfs()` の 1 回の実行)には $O(1)$ 時間かかります。**したがって、全体の時間計算量は $O(n)$** です。 -ハッシュテーブルは `inorder` 要素からそのインデックスへのマッピングを格納するため、$O(n)$ スペースが必要です。最悪の場合、二分木が連結リストに退化すると、再帰の深さは $n$ に達し、$O(n)$ のスタックスペースを消費する可能性があります。**したがって、全体の空間計算量は $O(n)$ です**。 +ハッシュテーブルには `inorder` の要素からインデックスへの対応を保存するため、空間計算量は $O(n)$ です。最悪の場合、すなわち二分木が連結リストに退化すると、再帰の深さは $n$ に達し、$O(n)$ のスタックフレーム空間を使用します。**したがって、全体の空間計算量は $O(n)$** です。 diff --git a/ja/docs/chapter_divide_and_conquer/divide_and_conquer.md b/ja/docs/chapter_divide_and_conquer/divide_and_conquer.md index 3152e09f5..80a6fd005 100644 --- a/ja/docs/chapter_divide_and_conquer/divide_and_conquer.md +++ b/ja/docs/chapter_divide_and_conquer/divide_and_conquer.md @@ -2,45 +2,45 @@ comments: true --- -# 12.1   分割統治アルゴリズム +# 12.1   分割統治法 -分割統治は重要で人気のあるアルゴリズム戦略です。名前が示すように、アルゴリズムは通常再帰的に実装され、「分割」と「統治」の2つのステップから構成されます。 +分割統治法(divide and conquer)は、問題を分けて統べるという意味であり、非常に重要で一般的なアルゴリズム戦略です。分割統治法は通常、再帰に基づいて実装され、「分」と「治」の 2 つのステップから構成されます。 -1. **分割(分割段階)**:元の問題を再帰的に2つ以上の小さな部分問題に分解し、最小の部分問題に到達するまで続けます。 -2. **統治(マージ段階)**:解決方法が既知の最小の部分問題から開始し、部分問題の解をボトムアップ方式でマージして元の問題の解を構築します。 +1. **分(分割段階)**:元の問題を 2 つ以上の部分問題へ再帰的に分解し、最小の部分問題に到達した時点で停止します。 +2. **治(統合段階)**:解が既知である最小の部分問題から始めて、部分問題の解を下から上へ統合し、元の問題の解を構築します。 以下の図に示すように、「マージソート」は分割統治戦略の典型的な応用の一つです。 -1. **分割**:元の配列(元の問題)を再帰的に2つの副配列(部分問題)に分割し、副配列が1つの要素のみになるまで(最小の部分問題)続けます。 -2. **統治**:順序付けられた副配列(部分問題の解)をボトムアップでマージして、順序付けられた元の配列(元の問題の解)を取得します。 +1. **分**:元の配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割し、部分配列に要素が 1 つだけ残るまで続けます。 +2. **治**:整列済みの部分配列(部分問題の解)を下から上へ統合し、整列済みの元の配列(元の問題の解)を得ます。 ![マージソートの分割統治戦略](divide_and_conquer.assets/divide_and_conquer_merge_sort.png){ class="animation-figure" }

図 12-1   マージソートの分割統治戦略

-## 12.1.1   分割統治問題を特定する方法 +## 12.1.1   分割統治法の問題を見極めるには -問題が分割統治解決に適しているかどうかは、通常以下の基準に基づいて決定できます。 +ある問題が分割統治法で解くのに適しているかどうかは、通常、次の判断基準を参考にできます。 -1. **問題をより小さなものに分解できる**:元の問題をより小さく類似した部分問題に分割でき、そのような過程を同じ方法で再帰的に実行できます。 -2. **部分問題は独立している**:部分問題間に重複がなく、独立しており、個別に解決できます。 -3. **部分問題の解をマージできる**:元の問題の解は、部分問題の解を組み合わせることで導出されます。 +1. **問題は分解できる**:元の問題は、より小さく類似した部分問題に分解でき、同じ方法で再帰的に分割できます。 +2. **部分問題は独立している**:部分問題同士に重複がなく、相互依存もないため、独立して解決できます。 +3. **部分問題の解は統合できる**:元の問題の解は、部分問題の解を統合することで得られます。 -明らかに、マージソートはこれら3つの基準を満たしています。 +明らかに、マージソートは以上の 3 つの判断基準を満たしています。 -1. **問題をより小さなものに分解できる**:配列(元の問題)を再帰的に2つの副配列(部分問題)に分割します。 -2. **部分問題は独立している**:各副配列は独立してソートできます(部分問題は独立して解決できます)。 -3. **部分問題の解をマージできる**:2つの順序付けられた副配列(部分問題の解)を1つの順序付けられた配列(元の問題の解)にマージできます。 +1. **問題は分解できる**:配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割します。 +2. **部分問題は独立している**:各部分配列は独立にソートできます(部分問題は独立に解けます)。 +3. **部分問題の解は統合できる**:2 つの整列済み部分配列(部分問題の解)は、1 つの整列済み配列(元の問題の解)に統合できます。 -## 12.1.2   分割統治による効率の向上 +## 12.1.2   分割統治法で効率を高める -**分割統治戦略はアルゴリズム問題を効果的に解決するだけでなく、しばしば効率を向上させます**。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソートは、分割統治戦略を適用しているため、選択ソート、バブルソート、挿入ソートよりも高速です。 +**分割統治法はアルゴリズムの問題を効果的に解けるだけでなく、多くの場合アルゴリズムの効率も高められます**。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソートが選択ソート、バブルソート、挿入ソートより高速ですが、これは分割統治戦略を適用しているためです。 -私たちの心には疑問があるかもしれません:**なぜ分割統治はアルゴリズムの効率を向上させることができ、その根本的な論理は何ですか?** つまり、問題を部分問題に分解し、それらを解決し、それらの解を組み合わせて元の問題に対処することが、元の問題を直接解決するよりも効率的である理由は何ですか?この質問は2つの側面から分析できます:操作数と並列計算。 +ここで次の疑問が生じます。**なぜ分割統治法はアルゴリズム効率を高められるのでしょうか。その根本的な仕組みは何でしょうか**?言い換えると、大きな問題を複数の部分問題に分解し、部分問題を解き、それらの解を統合して元の問題の解にするという手順は、なぜ元の問題を直接解くより効率的なのでしょうか。この問題は、操作回数と並列計算の 2 つの観点から議論できます。 -### 1.   操作数の最適化 +### 1.   操作回数の最適化 -「バブルソート」を例にとると、長さ $n$ の配列を処理するのに $O(n^2)$ 時間が必要です。以下の図に示すように、配列を中点から2つの副配列に分割するとします。そのような分割には $O(n)$ 時間が必要です。各副配列のソートには $O((n / 2)^2)$ 時間が必要です。そして2つの副配列のマージには $O(n)$ 時間が必要です。したがって、全体の時間計算量は: +「バブルソート」を例に取ると、長さ $n$ の配列を処理するのに $O(n^2)$ の時間がかかります。以下の図のように、配列を中央で 2 つの部分配列に分けると仮定すると、分割には $O(n)$ の時間、各部分配列のソートには $O((n / 2)^2)$ の時間、2 つの部分配列の統合には $O(n)$ の時間が必要で、全体の時間計算量は次のようになります: $$ O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n) @@ -50,7 +50,7 @@ $$

図 12-2   配列分割前後のバブルソート

-以下の不等式を計算してみましょう。左側は分割前の総操作数を表し、右側は分割後の総操作数をそれぞれ表します: +次に、以下の不等式を計算します。左辺と右辺はそれぞれ、分割前と分割後の操作総数です: $$ \begin{aligned} @@ -60,42 +60,42 @@ n(n - 4) & > 0 \end{aligned} $$ -**これは $n > 4$ の場合、分割後の操作数が少なく、より良いパフォーマンスにつながることを意味します**。分割後の時間計算量は依然として二次 $O(n^2)$ ですが、計算量の定数係数が減少していることに注意してください。 +**これは、$n > 4$ のときに分割後の操作回数の方が少なくなり、ソート効率が高くなることを意味します**。ただし、分割後の時間計算量は依然として 2 次の $O(n^2)$ であり、計算量の定数項が小さくなっただけです。 -さらに進むことができます。**副配列をその中点からさらに2つの副配列に分割し続けて、副配列が1つの要素のみになるまで続けたらどうでしょうか?** このアイデアは実際には「マージソート」で、時間計算量は $O(n \log n)$ です。 +さらに考えると、**部分配列を中央からさらに 2 つの部分配列へと分割し続け**、部分配列に要素が 1 つだけ残るまで分割を止めないとしたらどうでしょうか。この考え方がまさに「マージソート」であり、時間計算量は $O(n \log n)$ です。 -少し違うことを試してみましょう。**2つではなく、より多くの分割に分割したらどうでしょうか?** 例えば、元の配列を $k$ 個の副配列に均等に分割しますか?このアプローチは「バケットソート」と非常に似ており、大量のデータのソートに非常に適しています。理論的には、時間計算量は $O(n + k)$ に達することができます。 +さらに、**分割点をいくつか増やして**、元の配列を平均的に $k$ 個の部分配列に分けるとしたらどうでしょうか。この状況は「バケットソート」と非常によく似ており、大量データのソートに非常に適しています。理論上の時間計算量は $O(n + k)$ に達します。 -### 2.   並列計算による最適化 +### 2.   並列計算の最適化 -分割統治によって生成される部分問題は互いに独立していることが分かっています。**これは、それらを並列で解決できることを意味します。** その結果、分割統治はアルゴリズムの時間計算量を減らすだけでなく、**現代のオペレーティングシステムによる並列最適化も促進します。** +分割統治法で生成される部分問題は互いに独立しているため、**通常は並列に解くことができます**。つまり、分割統治法はアルゴリズムの時間計算量を下げられるだけでなく、**オペレーティングシステムの並列最適化にも有利です**。 -並列最適化は、複数のコアやプロセッサを持つ環境で特に効果的です。システムが複数の部分問題を同時に処理できるため、計算リソースを完全に活用し、全体的な実行時間が大幅に短縮されます。 +並列最適化は、マルチコアまたはマルチプロセッサ環境で特に有効です。システムが複数の部分問題を同時に処理でき、計算資源をより十分に活用できるため、全体の実行時間を大幅に短縮できます。 -例えば、以下の図に示す「バケットソート」では、大量のデータを様々なバケットに均等に分解します。各バケットのソート作業は、利用可能な計算ユニットに割り当てることができます。すべての作業が完了すると、すべてのソートされたバケットがマージされて最終結果が生成されます。 +たとえば、以下の図に示す「バケットソート」では、大量のデータを各バケットに均等に割り当てることで、すべてのバケットのソート処理を各計算ユニットに分散し、完了後に結果を統合できます。 ![バケットソートの並列計算](divide_and_conquer.assets/divide_and_conquer_parallel_computing.png){ class="animation-figure" }

図 12-3   バケットソートの並列計算

-## 12.1.3   分割統治の一般的な応用 +## 12.1.3   分割統治法の代表的な応用 -分割統治は多くの古典的なアルゴリズム問題を解決するために使用できます。 +一方では、分割統治法は多くの古典的なアルゴリズム問題を解くのに使えます。 -- **最近点対の発見**:このアルゴリズムは点の集合を2つの半分に分割することで動作します。そして各半分で再帰的に最近点対を見つけます。最後に、2つの半分にまたがるペアを考慮して、全体の最近点対を見つけます。 -- **大整数の乗算**:一つのアルゴリズムはKaratsubaと呼ばれます。大整数の乗算をいくつかの小さな整数の乗算と加算に分解します。 -- **行列の乗算**:一例はStrassenアルゴリズムです。大きな行列の乗算を複数の小さな行列の乗算と加算に分解します。 -- **ハノイの塔問題**:ハノイの塔問題は再帰的に解決でき、分割統治戦略の典型的な応用です。 -- **転倒対の解決**:シーケンスで、前の数が後の数より大きい場合、これら2つの数は転倒対を構成します。転倒対問題の解決は、マージソートの助けを借りて、分割統治のアイデアを利用できます。 +- **最近点対探索**:このアルゴリズムは、まず点集合を 2 つに分け、それぞれの部分における最近点対を求め、最後に 2 つの部分をまたぐ最近点対を求めます。 +- **大整数乗算**:たとえば Karatsuba 法では、大整数の乗算を、より小さな整数どうしのいくつかの乗算と加算に分解します。 +- **行列乗算**:たとえば Strassen 法では、大きな行列の乗算を、複数の小さな行列の乗算と加算に分解します。 +- **ハノイの塔問題**:ハノイの塔問題は再帰によって解くことができ、これは典型的な分割統治戦略の応用です。 +- **反転対の計算**:ある数列で前の数が後ろの数より大きい場合、その 2 つの数は反転対を構成します。反転対の問題は、分割統治の考え方を利用し、マージソートを用いて解けます。 -分割統治はアルゴリズムとデータ構造の設計にも広く応用されています。 +他方で、分割統治法はアルゴリズムとデータ構造の設計にも非常に広く応用されています。 -- **二分探索**:二分探索は、ソート済み配列を中点インデックスから2つの半分に分割します。そして、ターゲット値と中間要素値の比較結果に基づいて、一方の半分が破棄されます。同じプロセスで残りの半分で検索が続行され、ターゲットが見つかるか残りの要素がなくなるまで続きます。 -- **マージソート**:この節の冒頭ですでに紹介したため、さらなる詳述は不要です。 -- **クイックソート**:クイックソートはピボット値を選択して配列を2つの副配列に分割し、一方はピボットより小さい要素、もう一方はピボットより大きい要素を持ちます。このプロセスは、これら2つの副配列のそれぞれに対して、1つの要素のみを保持するまで続きます。 -- **バケットソート**:バケットソートの基本的なアイデアは、データを複数のバケットに分散させることです。各バケット内の要素をソートした後、バケットから順序よく要素を取得して順序付けられた配列を取得します。 -- **木**:例えば、二分探索木、AVL木、赤黒木、B木、B+木など。その操作(検索、挿入、削除)はすべて分割統治戦略の応用と見なすことができます。 -- **ヒープ**:ヒープは特別なタイプの完全二分木です。その様々な操作(挿入、削除、ヒープ化)は、実際に分割統治のアイデアを含意しています。 -- **ハッシュテーブル**:ハッシュテーブルは直接分割統治を適用しませんが、一部のハッシュ衝突解決ソリューションは間接的にこの戦略を適用します。例えば、チェイン法の長いリストは、クエリ効率を向上させるために赤黒木に変換される場合があります。 +- **二分探索**:二分探索では、整列済み配列を中央のインデックスで 2 つに分け、目標値と中央要素の比較結果に基づいてどちらの半区間を除外するかを決め、残った区間で同じ二分操作を行います。 +- **マージソート**:本節の冒頭で紹介したため、ここでは繰り返しません。 +- **クイックソート**:クイックソートは基準値を 1 つ選び、配列を、基準値より小さい要素の部分配列と、基準値より大きい要素の部分配列に分け、その後それぞれに対して同じ分割操作を行い、部分配列に要素が 1 つだけ残るまで続けます。 +- **バケットソート**:バケットソートの基本的な考え方は、データを複数のバケットに分散し、各バケット内の要素をソートしたうえで、各バケットの要素を順に取り出して整列済み配列を得ることです。 +- **木構造**:たとえば二分探索木、AVL 木、赤黒木、B 木、B+ 木などでは、探索・挿入・削除などの操作をいずれも分割統治戦略の応用とみなせます。 +- **ヒープ**:ヒープは特殊な完全二分木であり、挿入、削除、ヒープ化などの各種操作には、実際には分割統治の考え方が含まれています。 +- **ハッシュテーブル**:ハッシュテーブル自体は分割統治を直接適用しているわけではありませんが、いくつかのハッシュ衝突解決法では間接的に分割統治戦略が使われています。たとえば、連鎖アドレス法における長い連結リストは、検索効率を高めるために赤黒木へ変換されます。 -**分割統治は巧妙に浸透するアルゴリズムアイデア**であり、様々なアルゴリズムとデータ構造に組み込まれていることが分かります。 +このように、**分割統治法は「静かに物を潤す」ようなアルゴリズム思想**であり、さまざまなアルゴリズムやデータ構造の中に潜んでいます。 diff --git a/ja/docs/chapter_divide_and_conquer/hanota_problem.md b/ja/docs/chapter_divide_and_conquer/hanota_problem.md index 5612ec755..3e3976694 100644 --- a/ja/docs/chapter_divide_and_conquer/hanota_problem.md +++ b/ja/docs/chapter_divide_and_conquer/hanota_problem.md @@ -2,44 +2,44 @@ comments: true --- -# 12.4   ハノイの塔問題 +# 12.4   ハノイの塔の問題 -マージソートと二分木構築の両方で、元の問題を2つの部分問題に分解し、それぞれが元の問題のサイズの半分でした。しかし、ハノイの塔では、異なる分解戦略を採用します。 +マージソートや二分木の構築では、いずれも元の問題を元問題の半分の規模をもつ 2 つの部分問題に分解していました。しかし、ハノイの塔の問題では、異なる分解戦略を採用します。 !!! question - 3つの柱があり、それぞれ `A`、`B`、`C` と表記されます。最初、柱 `A` には $n$ 枚の円盤があり、上から下に向かって昇順のサイズで配置されています。私たちのタスクは、これらの $n$ 枚の円盤を柱 `C` に移動し、元の順序を維持することです(以下の図に示すように)。移動中には以下のルールが適用されます: + 3 本の柱があり、それぞれを `A`、`B`、`C` とします。初期状態では、柱 `A` に $n$ 枚の円盤が通されており、上から下へ小さい順に並んでいます。私たちの課題は、この $n$ 枚の円盤を柱 `C` に移し、元の順序を保つことです(以下の図のとおり)。円盤を移動する際には、次のルールに従う必要があります。 + + 1. 円盤は 1 本の柱の頂上から取り出し、別の柱の頂上に置くことしかできません。 + 2. 1 回に移動できる円盤は 1 枚だけです。 + 3. 小さい円盤は常に大きい円盤の上になければなりません。 - 1. 円盤は柱の上部からのみ取り除くことができ、別の柱の上部に置く必要があります。 - 2. 一度に移動できるのは1枚の円盤のみです。 - 3. 小さい円盤は常に大きい円盤の上にある必要があります。 +![ハノイの塔の問題の例](hanota_problem.assets/hanota_example.png){ class="animation-figure" } -![ハノイの塔の例](hanota_problem.assets/hanota_example.png){ class="animation-figure" } +

図 12-10   ハノイの塔の問題の例

-

図 12-10   ハノイの塔の例

- -**サイズ $i$ のハノイの塔問題を $f(i)$ と表記します**。例えば、$f(3)$ は3枚の円盤を柱 `A` から柱 `C` に移動することを表します。 +**規模が $i$ のハノイの塔の問題を $f(i)$ と表します** 。たとえば $f(3)$ は、$3$ 枚の円盤を `A` から `C` へ移動するハノイの塔の問題を表します。 ### 1.   基本ケースを考える -以下の図に示すように、問題 $f(1)$(円盤が1枚のみ)については、`A` から `C` に直接移動できます。 +以下の図に示すように、問題 $f(1)$ 、すなわち円盤が 1 枚だけの場合は、それを `A` から `C` へ直接移動すれば済みます。 === "<1>" - ![サイズ1の問題の解](hanota_problem.assets/hanota_f1_step1.png){ class="animation-figure" } + ![規模 1 の問題の解](hanota_problem.assets/hanota_f1_step1.png){ class="animation-figure" } === "<2>" ![hanota_f1_step2](hanota_problem.assets/hanota_f1_step2.png){ class="animation-figure" } -

図 12-11   サイズ1の問題の解

+

図 12-11   規模 1 の問題の解

-$f(2)$(円盤が2枚)については、**柱 `B` の助けを借りて小さい円盤を大きい円盤の上に保つ**必要があります。以下の図に示すように: +以下の図に示すように、問題 $f(2)$ 、すなわち円盤が 2 枚ある場合は、**小さい円盤が常に大きい円盤の上にある条件を満たすため、`B` を借りて移動を行う必要があります**。 -1. まず、小さい円盤を `A` から `B` に移動します。 -2. 次に、大きい円盤を `A` から `C` に移動します。 -3. 最後に、小さい円盤を `B` から `C` に移動します。 +1. まず上の小さい円盤を `A` から `B` へ移します。 +2. 次に大きい円盤を `A` から `C` へ移します。 +3. 最後に小さい円盤を `B` から `C` へ移します。 === "<1>" - ![サイズ2の問題の解](hanota_problem.assets/hanota_f2_step1.png){ class="animation-figure" } + ![規模 2 の問題の解](hanota_problem.assets/hanota_f2_step1.png){ class="animation-figure" } === "<2>" ![hanota_f2_step2](hanota_problem.assets/hanota_f2_step2.png){ class="animation-figure" } @@ -50,22 +50,22 @@ $f(2)$(円盤が2枚)については、**柱 `B` の助けを借りて小さ === "<4>" ![hanota_f2_step4](hanota_problem.assets/hanota_f2_step4.png){ class="animation-figure" } -

図 12-12   サイズ2の問題の解

+

図 12-12   規模 2 の問題の解

-$f(2)$ を解決する過程は次のように要約できます:**`B` の助けを借りて2枚の円盤を `A` から `C` に移動する**。ここで、`C` をターゲット柱、`B` をバッファ柱と呼びます。 +問題 $f(2)$ を解く過程は、**2 枚の円盤を `B` を介して `A` から `C` へ移す**と要約できます。このとき、`C` を目標の柱、`B` を補助の柱と呼びます。 -### 2.   部分問題の分解 +### 2.   部分問題への分解 -問題 $f(3)$(つまり、円盤が3枚の場合)については、状況がやや複雑になります。 +問題 $f(3)$ 、すなわち円盤が 3 枚ある場合になると、状況はやや複雑になります。 -すでに $f(1)$ と $f(2)$ の解が分かっているので、分割統治の観点を採用し、**`A` の上の2枚の円盤を1つの単位として扱い**、以下の図に示すステップを実行できます。これにより、3枚の円盤を `A` から `C` に正常に移動できます。 +$f(1)$ と $f(2)$ の解が既知なので、分割統治の観点から、**`A` の上部にある 2 枚の円盤をひとまとまりとみなして**、次の図の手順を実行できます。こうして 3 枚の円盤を `A` から `C` へ順調に移動できます。 -1. `B` をターゲット柱、`C` をバッファ柱として、2枚の円盤を `A` から `B` に移動します。 -2. 残りの円盤を `A` から直接 `C` に移動します。 -3. `C` をターゲット柱、`A` をバッファ柱として、2枚の円盤を `B` から `C` に移動します。 +1. `B` を目標の柱、`C` を補助の柱として、2 枚の円盤を `A` から `B` へ移します。 +2. `A` に残った 1 枚の円盤を `A` から `C` へ直接移動します。 +3. `C` を目標の柱、`A` を補助の柱として、2 枚の円盤を `B` から `C` へ移します。 === "<1>" - ![サイズ3の問題の解](hanota_problem.assets/hanota_f3_step1.png){ class="animation-figure" } + ![規模 3 の問題の解](hanota_problem.assets/hanota_f3_step1.png){ class="animation-figure" } === "<2>" ![hanota_f3_step2](hanota_problem.assets/hanota_f3_step2.png){ class="animation-figure" } @@ -76,87 +76,87 @@ $f(2)$ を解決する過程は次のように要約できます:**`B` の助 === "<4>" ![hanota_f3_step4](hanota_problem.assets/hanota_f3_step4.png){ class="animation-figure" } -

図 12-13   サイズ3の問題の解

+

図 12-13   規模 3 の問題の解

-本質的に、**$f(3)$ を2つの $f(2)$ 部分問題と1つの $f(1)$ 部分問題に分解します**。これら3つの部分問題を順次解決することで、元の問題が解決され、部分問題が独立しており、それらの解をマージできることを示しています。 +本質的には、**問題 $f(3)$ を 2 つの部分問題 $f(2)$ と 1 つの部分問題 $f(1)$ に分けています** 。この 3 つの部分問題を順に解けば、元の問題も解決されます。これは、部分問題が独立しており、解を組み合わせられることを示しています。 -ここから、以下の図に示すハノイの塔の分割統治戦略を要約できます。元の問題 $f(n)$ を2つの部分問題 $f(n-1)$ と1つの部分問題 $f(1)$ に分割し、以下の順序でこれら3つの部分問題を解決します: +ここまでで、次の図に示すハノイの塔の問題を解く分割統治戦略をまとめられます。元の問題 $f(n)$ を 2 つの部分問題 $f(n-1)$ と 1 つの部分問題 $f(1)$ に分け、次の順序でこの 3 つの部分問題を解きます。 -1. `C` をバッファとして使用し、$n-1$ 枚の円盤を `A` から `B` に移動します。 -2. 残りの円盤を `A` から直接 `C` に移動します。 -3. `A` をバッファとして使用し、$n-1$ 枚の円盤を `B` から `C` に移動します。 +1. $n-1$ 枚の円盤を `C` を介して `A` から `B` へ移します。 +2. 残り $1$ 枚の円盤を `A` から `C` へ直接移します。 +3. $n-1$ 枚の円盤を `A` を介して `B` から `C` へ移します。 -各 $f(n-1)$ 部分問題について、**同じ再帰分割を適用でき**、最小の部分問題 $f(1)$ に到達するまで続けます。$f(1)$ は単一の移動のみが必要であることがすでに分かっているため、解決するのは簡単です。 +この 2 つの部分問題 $f(n-1)$ は、**同じ方法で再帰的に分割できます**。最小の部分問題 $f(1)$ に到達するまでこれを続けます。一方、$f(1)$ の解は既知であり、1 回の移動操作だけで済みます。 -![ハノイの塔を解決するための分割統治戦略](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" } +![ハノイの塔の問題を解く分割統治戦略](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" } -

図 12-14   ハノイの塔を解決するための分割統治戦略

+

図 12-14   ハノイの塔の問題を解く分割統治戦略

-### 3.   コード実装 +### 3.   コードの実装 -コードでは、再帰関数 `dfs(i, src, buf, tar)` を定義します。これは柱 `src` から上の $i$ 枚の円盤を柱 `tar` に移動し、柱 `buf` をバッファとして使用します: +コードでは、再帰関数 `dfs(i, src, buf, tar)` を定義します。その役割は、柱 `src` の上部にある $i$ 枚の円盤を、補助の柱 `buf` を使って目標の柱 `tar` へ移動することです: === "Python" ```python title="hanota.py" def move(src: list[int], tar: list[int]): - """円盤を移動""" - # src の上から円盤を取り出す + """円盤を 1 枚移動""" + # src の上から円盤を1枚取り出す pan = src.pop() # 円盤を tar の上に置く tar.append(pan) def dfs(i: int, src: list[int], buf: list[int], tar: list[int]): - """ハノイの塔問題 f(i) を解く""" - # src に円盤が 1 つだけ残っている場合、それを tar に移動 + """ハノイの塔の問題 f(i) を解く""" + # src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す if i == 1: move(src, tar) return - # 部分問題 f(i-1):tar の助けを借りて src の上の i-1 個の円盤を buf に移動 + # 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す dfs(i - 1, src, tar, buf) - # 部分問題 f(1):残りの 1 個の円盤を src から tar に移動 + # 部分問題 f(1):src に残る 1 枚の円盤を tar に移す move(src, tar) - # 部分問題 f(i-1):src の助けを借りて buf の上の i-1 個の円盤を tar に移動 + # 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す dfs(i - 1, buf, src, tar) def solve_hanota(A: list[int], B: list[int], C: list[int]): - """ハノイの塔問題を解く""" + """ハノイの塔を解く""" n = len(A) - # B の助けを借りて A の上の n 個の円盤を C に移動 + # A の上から n 枚の円盤を B を介して C へ移す dfs(n, A, B, C) ``` === "C++" ```cpp title="hanota.cpp" - /* 円盤を移動 */ + /* 円盤を 1 枚移動 */ void move(vector &src, vector &tar) { - // src の最上部から円盤を取り出す + // src の上から円盤を1枚取り出す int pan = src.back(); src.pop_back(); - // 円盤を tar の最上部に配置 + // 円盤を tar の上に置く tar.push_back(pan); } - /* ハノイの塔問題 f(i) を解く */ + /* ハノイの塔の問題 f(i) を解く */ void dfs(int i, vector &src, vector &buf, vector &tar) { - // src に円盤が1つだけ残っている場合、それを tar に移動 + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す if (i == 1) { move(src, tar); return; } - // 部分問題 f(i-1):tar の助けを借りて、上位 i-1 個の円盤を src から buf に移動 + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す dfs(i - 1, src, tar, buf); - // 部分問題 f(1):残りの1つの円盤を src から tar に移動 + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す move(src, tar); - // 部分問題 f(i-1):src の助けを借りて、上位 i-1 個の円盤を buf から tar に移動 + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す dfs(i - 1, buf, src, tar); } - /* ハノイの塔問題を解く */ + /* ハノイの塔を解く */ void solveHanota(vector &A, vector &B, vector &C) { int n = A.size(); - // B の助けを借りて、上位 n 個の円盤を A から C に移動 + // A の上から n 枚の円盤を B を介して C へ移す dfs(n, A, B, C); } ``` @@ -164,33 +164,33 @@ $f(2)$ を解決する過程は次のように要約できます:**`B` の助 === "Java" ```java title="hanota.java" - /* 円盤を移動 */ + /* 円盤を 1 枚移動 */ void move(List src, List tar) { - // src の最上部から円盤を取り出す + // src の上から円盤を1枚取り出す Integer pan = src.remove(src.size() - 1); - // 円盤を tar の最上部に配置 + // 円盤を tar の上に置く tar.add(pan); } - /* ハノイの塔問題 f(i) を解く */ + /* ハノイの塔の問題 f(i) を解く */ void dfs(int i, List src, List buf, List tar) { - // src に円盤が1つだけ残っている場合、それを tar に移動 + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す if (i == 1) { move(src, tar); return; } - // 部分問題 f(i-1):tar の助けを借りて、上位 i-1 個の円盤を src から buf に移動 + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す dfs(i - 1, src, tar, buf); - // 部分問題 f(1):残りの1つの円盤を src から tar に移動 + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す move(src, tar); - // 部分問題 f(i-1):src の助けを借りて、上位 i-1 個の円盤を buf から tar に移動 + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す dfs(i - 1, buf, src, tar); } - /* ハノイの塔問題を解く */ + /* ハノイの塔を解く */ void solveHanota(List A, List B, List C) { int n = A.size(); - // B の助けを借りて、上位 n 個の円盤を A から C に移動 + // A の上から n 枚の円盤を B を介して C へ移す dfs(n, A, B, C); } ``` @@ -198,111 +198,363 @@ $f(2)$ を解決する過程は次のように要約できます:**`B` の助 === "C#" ```csharp title="hanota.cs" - [class]{hanota}-[func]{Move} + /* 円盤を 1 枚移動 */ + void Move(List src, List tar) { + // src の上から円盤を1枚取り出す + int pan = src[^1]; + src.RemoveAt(src.Count - 1); + // 円盤を tar の上に置く + tar.Add(pan); + } - [class]{hanota}-[func]{DFS} + /* ハノイの塔の問題 f(i) を解く */ + void DFS(int i, List src, List buf, List tar) { + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す + if (i == 1) { + Move(src, tar); + return; + } + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す + DFS(i - 1, src, tar, buf); + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す + Move(src, tar); + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す + DFS(i - 1, buf, src, tar); + } - [class]{hanota}-[func]{SolveHanota} + /* ハノイの塔を解く */ + void SolveHanota(List A, List B, List C) { + int n = A.Count; + // A の上から n 枚の円盤を B を介して C へ移す + DFS(n, A, B, C); + } ``` === "Go" ```go title="hanota.go" - [class]{}-[func]{move} + /* 円盤を 1 枚移動 */ + func move(src, tar *list.List) { + // src の上から円盤を1枚取り出す + pan := src.Back() + // 円盤を tar の上に置く + tar.PushBack(pan.Value) + // `src` の最上部の円盤を取り外す + src.Remove(pan) + } - [class]{}-[func]{dfsHanota} + /* ハノイの塔の問題 f(i) を解く */ + func dfsHanota(i int, src, buf, tar *list.List) { + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す + if i == 1 { + move(src, tar) + return + } + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す + dfsHanota(i-1, src, tar, buf) + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す + move(src, tar) + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す + dfsHanota(i-1, buf, src, tar) + } - [class]{}-[func]{solveHanota} + /* ハノイの塔を解く */ + func solveHanota(A, B, C *list.List) { + n := A.Len() + // A の上から n 枚の円盤を B を介して C へ移す + dfsHanota(n, A, B, C) + } ``` === "Swift" ```swift title="hanota.swift" - [class]{}-[func]{move} + /* 円盤を 1 枚移動 */ + func move(src: inout [Int], tar: inout [Int]) { + // src の上から円盤を1枚取り出す + let pan = src.popLast()! + // 円盤を tar の上に置く + tar.append(pan) + } - [class]{}-[func]{dfs} + /* ハノイの塔の問題 f(i) を解く */ + func dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) { + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す + if i == 1 { + move(src: &src, tar: &tar) + return + } + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す + dfs(i: i - 1, src: &src, buf: &tar, tar: &buf) + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す + move(src: &src, tar: &tar) + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す + dfs(i: i - 1, src: &buf, buf: &src, tar: &tar) + } - [class]{}-[func]{solveHanota} + /* ハノイの塔を解く */ + func solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) { + let n = A.count + // リストの末尾が柱の上端に対応する + // src の上から n 個の円盤を、B を介して C に移動する + dfs(i: n, src: &A, buf: &B, tar: &C) + } ``` === "JS" ```javascript title="hanota.js" - [class]{}-[func]{move} + /* 円盤を 1 枚移動 */ + function move(src, tar) { + // src の上から円盤を1枚取り出す + const pan = src.pop(); + // 円盤を tar の上に置く + tar.push(pan); + } - [class]{}-[func]{dfs} + /* ハノイの塔の問題 f(i) を解く */ + function dfs(i, src, buf, tar) { + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す + if (i === 1) { + move(src, tar); + return; + } + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す + dfs(i - 1, src, tar, buf); + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す + move(src, tar); + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す + dfs(i - 1, buf, src, tar); + } - [class]{}-[func]{solveHanota} + /* ハノイの塔を解く */ + function solveHanota(A, B, C) { + const n = A.length; + // A の上から n 枚の円盤を B を介して C へ移す + dfs(n, A, B, C); + } ``` === "TS" ```typescript title="hanota.ts" - [class]{}-[func]{move} + /* 円盤を 1 枚移動 */ + function move(src: number[], tar: number[]): void { + // src の上から円盤を1枚取り出す + const pan = src.pop(); + // 円盤を tar の上に置く + tar.push(pan); + } - [class]{}-[func]{dfs} + /* ハノイの塔の問題 f(i) を解く */ + function dfs(i: number, src: number[], buf: number[], tar: number[]): void { + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す + if (i === 1) { + move(src, tar); + return; + } + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す + dfs(i - 1, src, tar, buf); + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す + move(src, tar); + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す + dfs(i - 1, buf, src, tar); + } - [class]{}-[func]{solveHanota} + /* ハノイの塔を解く */ + function solveHanota(A: number[], B: number[], C: number[]): void { + const n = A.length; + // A の上から n 枚の円盤を B を介して C へ移す + dfs(n, A, B, C); + } ``` === "Dart" ```dart title="hanota.dart" - [class]{}-[func]{move} + /* 円盤を 1 枚移動 */ + void move(List src, List tar) { + // src の上から円盤を1枚取り出す + int pan = src.removeLast(); + // 円盤を tar の上に置く + tar.add(pan); + } - [class]{}-[func]{dfs} + /* ハノイの塔の問題 f(i) を解く */ + void dfs(int i, List src, List buf, List tar) { + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す + if (i == 1) { + move(src, tar); + return; + } + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す + dfs(i - 1, src, tar, buf); + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す + move(src, tar); + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す + dfs(i - 1, buf, src, tar); + } - [class]{}-[func]{solveHanota} + /* ハノイの塔を解く */ + void solveHanota(List A, List B, List C) { + int n = A.length; + // A の上から n 枚の円盤を B を介して C へ移す + dfs(n, A, B, C); + } ``` === "Rust" ```rust title="hanota.rs" - [class]{}-[func]{move_pan} + /* 円盤を 1 枚移動 */ + fn move_pan(src: &mut Vec, tar: &mut Vec) { + // src の上から円盤を1枚取り出す + let pan = src.pop().unwrap(); + // 円盤を tar の上に置く + tar.push(pan); + } - [class]{}-[func]{dfs} + /* ハノイの塔の問題 f(i) を解く */ + fn dfs(i: i32, src: &mut Vec, buf: &mut Vec, tar: &mut Vec) { + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す + if i == 1 { + move_pan(src, tar); + return; + } + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す + dfs(i - 1, src, tar, buf); + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す + move_pan(src, tar); + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す + dfs(i - 1, buf, src, tar); + } - [class]{}-[func]{solve_hanota} + /* ハノイの塔を解く */ + fn solve_hanota(A: &mut Vec, B: &mut Vec, C: &mut Vec) { + let n = A.len() as i32; + // A の上から n 枚の円盤を B を介して C へ移す + dfs(n, A, B, C); + } ``` === "C" ```c title="hanota.c" - [class]{}-[func]{move} + /* 円盤を 1 枚移動 */ + void move(int *src, int *srcSize, int *tar, int *tarSize) { + // src の上から円盤を1枚取り出す + int pan = src[*srcSize - 1]; + src[*srcSize - 1] = 0; + (*srcSize)--; + // 円盤を tar の上に置く + tar[*tarSize] = pan; + (*tarSize)++; + } - [class]{}-[func]{dfs} + /* ハノイの塔の問題 f(i) を解く */ + void dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) { + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す + if (i == 1) { + move(src, srcSize, tar, tarSize); + return; + } + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す + dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize); + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す + move(src, srcSize, tar, tarSize); + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す + dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize); + } - [class]{}-[func]{solveHanota} + /* ハノイの塔を解く */ + void solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) { + // A の上から n 枚の円盤を B を介して C へ移す + dfs(*ASize, A, ASize, B, BSize, C, CSize); + } ``` === "Kotlin" ```kotlin title="hanota.kt" - [class]{}-[func]{move} + /* 円盤を 1 枚移動 */ + fun move(src: MutableList, tar: MutableList) { + // src の上から円盤を1枚取り出す + val pan = src.removeAt(src.size - 1) + // 円盤を tar の上に置く + tar.add(pan) + } - [class]{}-[func]{dfs} + /* ハノイの塔の問題 f(i) を解く */ + fun dfs(i: Int, src: MutableList, buf: MutableList, tar: MutableList) { + // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す + if (i == 1) { + move(src, tar) + return + } + // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す + dfs(i - 1, src, tar, buf) + // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す + move(src, tar) + // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す + dfs(i - 1, buf, src, tar) + } - [class]{}-[func]{solveHanota} + /* ハノイの塔を解く */ + fun solveHanota(A: MutableList, B: MutableList, C: MutableList) { + val n = A.size + // A の上から n 枚の円盤を B を介して C へ移す + dfs(n, A, B, C) + } ``` === "Ruby" ```ruby title="hanota.rb" - [class]{}-[func]{move} + ### 円盤を1枚移動 ### + def move(src, tar) + # src の上から円盤を1枚取り出す + pan = src.pop + # 円盤を tar の上に置く + tar << pan + end - [class]{}-[func]{dfs} + ### ハノイの塔 f(i) を解く ### + def dfs(i, src, buf, tar) + # src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す + if i == 1 + move(src, tar) + return + end - [class]{}-[func]{solve_hanota} + # 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す + dfs(i - 1, src, tar, buf) + # 部分問題 f(1):src に残る 1 枚の円盤を tar に移す + move(src, tar) + # 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す + dfs(i - 1, buf, src, tar) + end + + ### ハノイの塔を解く ### + def solve_hanota(_A, _B, _C) + n = _A.length + # A の上から n 枚の円盤を B を介して C へ移す + dfs(n, _A, _B, _C) + end ``` -以下の図に示すように、ハノイの塔問題は高さ $n$ の再帰木として視覚化できます。各ノードは部分問題を表し、`dfs()` の呼び出しに対応します。**したがって、時間計算量は $O(2^n)$、空間計算量は $O(n)$ です。** +??? pythontutor "コードの可視化" -![ハノイの塔の再帰木](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" } +
+ -

図 12-15   ハノイの塔の再帰木

+以下の図に示すように、ハノイの塔の問題は高さ $n$ の再帰木を形成し、各ノードは 1 つの部分問題、すなわち 1 つ起動された `dfs()` 関数に対応します。**したがって時間計算量は $O(2^n)$ 、空間計算量は $O(n)$** です。 + +![ハノイの塔の問題の再帰木](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" } + +

図 12-15   ハノイの塔の問題の再帰木

!!! quote - ハノイの塔は古代の伝説に由来します。古代インドの寺院で、僧侶たちは3本の高いダイヤモンドの柱と、異なるサイズの $64$ 枚の金の円盤を持っていました。彼らは、最後の円盤が正しく置かれたとき、世界が終わると信じていました。 + ハノイの塔の問題は古い伝説に由来します。古代インドのある寺院で、僧侶たちは 3 本の高いダイヤモンドの柱と、$64$ 枚の大きさの異なる金の円盤を持っていました。僧侶たちは絶えず円盤を動かし、最後の 1 枚が正しく置かれた瞬間に世界が終わると信じていました。 - しかし、僧侶たちが1秒に1枚の円盤を移動したとしても、約 $2^{64} \approx 1.84×10^{19}$ —約5850億年—かかり、宇宙の年齢の現在の推定をはるかに超えています。したがって、この伝説が真実であれば、世界の終わりについて心配する必要はおそらくないでしょう。 + しかし、たとえ僧侶たちが 1 秒に 1 回移動するとしても、合計でおよそ $2^{64} \approx 1.84×10^{19}$ 秒、約 $5850$ 億年が必要で、現在推定されている宇宙の年齢をはるかに上回ります。したがって、この伝説が本当だったとしても、世界の終わりを心配する必要はなさそうです。 diff --git a/ja/docs/chapter_divide_and_conquer/index.md b/ja/docs/chapter_divide_and_conquer/index.md index 63ea0d9c6..5abacb4f6 100644 --- a/ja/docs/chapter_divide_and_conquer/index.md +++ b/ja/docs/chapter_divide_and_conquer/index.md @@ -9,14 +9,14 @@ icon: material/set-split !!! abstract - 困難な問題は層を重ねて分解され、各分解によってより単純になります。 - - 分割統治は深い真理を明らかにします:単純さから始めれば、複雑さは解決される。 + 難題は段階的に分解され、そのたびにより単純になっていく。 + + 分割統治は一つの重要な事実を示している。単純なことから始めれば、すべてはもはや複雑ではない。 ## 章の内容 -- [12.1   分割統治アルゴリズム](divide_and_conquer.md) +- [12.1   分割統治法](divide_and_conquer.md) - [12.2   分割統治探索戦略](binary_search_recur.md) -- [12.3   木の構築問題](build_binary_tree_problem.md) -- [12.4   ハノイの塔問題](hanota_problem.md) +- [12.3   二分木の構築問題](build_binary_tree_problem.md) +- [12.4   ハノイの塔の問題](hanota_problem.md) - [12.5   まとめ](summary.md) diff --git a/ja/docs/chapter_divide_and_conquer/summary.md b/ja/docs/chapter_divide_and_conquer/summary.md index 4a3b3b222..b5f4c3471 100644 --- a/ja/docs/chapter_divide_and_conquer/summary.md +++ b/ja/docs/chapter_divide_and_conquer/summary.md @@ -4,12 +4,14 @@ comments: true # 12.5   まとめ -- 分割統治は一般的なアルゴリズム設計戦略で、分割(分割)と統治(マージ)の2つの段階から構成され、一般的に再帰を使用して実装されます。 -- 問題が分割統治アプローチに適しているかどうかを判断するために、問題が分解可能かどうか、部分問題が独立しているかどうか、部分問題をマージできるかどうかを確認します。 -- マージソートは分割統治戦略の典型的な例です。配列を再帰的に2つの等しい長さの副配列に分割し、1つの要素のみが残るまで続け、次にこれらの副配列を層ごとにマージしてソートを完了します。 -- 分割統治戦略の導入は、しばしばアルゴリズムの効率を向上させます。一方では操作数を減らし、他方では分割後のシステムの並列最適化を促進します。 -- 分割統治は多数のアルゴリズム問題に適用でき、データ構造とアルゴリズム設計で広く使用され、多くのシナリオに現れます。 -- 総当たり検索と比較して、適応検索はより効率的です。時間計算量が $O(\log n)$ の検索アルゴリズムは、通常分割統治戦略に基づいています。 -- 二分探索は分割統治戦略のもう一つの古典的な応用です。部分問題の解のマージを含まず、再帰的な分割統治アプローチで実装できます。 -- 二分木構築問題では、木の構築(元の問題)を左の部分木と右の部分木の構築(部分問題)に分割できます。これは前順走査と中順走査のインデックス範囲を分割することで実現できます。 -- ハノイの塔問題では、サイズ $n$ の問題をサイズ $n-1$ の2つの部分問題とサイズ $1$ の1つの部分問題に分解できます。これら3つの部分問題を順次解決することで、元の問題が解決されます。 +### 1.   要点の振り返り + +- 分割統治法は一般的なアルゴリズム設計戦略であり、分(分割)と治(統合)の 2 つの段階からなり、通常は再帰に基づいて実装されます。 +- それが分割統治法の問題かどうかを判断する基準には、問題を分解できるか、部分問題が独立しているか、部分問題を統合できるかが含まれます。 +- マージソートは分割統治法の典型的な応用であり、配列を再帰的に同じ長さの 2 つの部分配列に分割し、要素が 1 つだけになるまで続け、その後で各層を順に統合してソートを完了します。 +- 分割統治法を導入すると、多くの場合アルゴリズムの効率を高められます。一方では操作回数が減り、他方では分割後にシステムの並列最適化を行いやすくなります。 +- 分割統治法は多くのアルゴリズム問題を解決できるだけでなく、データ構造やアルゴリズム設計にも広く応用されており、至る所でその姿を見ることができます。 +- 総当たり探索と比べて、適応的な探索のほうが効率的です。時間計算量が $O(\log n)$ の探索アルゴリズムは、通常は分割統治法に基づいて実装されます。 +- 二分探索は分割統治法のもう 1 つの典型的な応用であり、部分問題の解を統合する手順を含みません。再帰的な分割統治によって二分探索を実現できます。 +- 二分木を構築する問題では、木の構築(元の問題)を左部分木と右部分木の構築(部分問題)に分けられます。これは、先行順序走査と中間順序走査のインデックス区間を分割することで実現できます。 +- ハノイの塔の問題では、規模 $n$ の問題を、規模 $n-1$ の 2 つの部分問題と規模 $1$ の 1 つの部分問題に分けられます。これら 3 つの部分問題を順に解くと、元の問題も解決されます。 diff --git a/ja/docs/chapter_dynamic_programming/dp_problem_features.md b/ja/docs/chapter_dynamic_programming/dp_problem_features.md index fc7c867f2..781ffaa40 100644 --- a/ja/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/ja/docs/chapter_dynamic_programming/dp_problem_features.md @@ -2,57 +2,57 @@ comments: true --- -# 14.2   動的プログラミング問題の特徴 +# 14.2   動的計画法の問題特性 -前のセクションでは、動的プログラミングが問題を部分問題に分解することで元の問題を解決する方法を学びました。実際、部分問題の分解は一般的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングでは異なる重点があります。 +前節では、動的計画法が部分問題への分解によってどのように元の問題を解くのかを学びました。実際、部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治法、動的計画法、バックトラッキングでは重視点が異なります。 -- 分割統治法アルゴリズムは元の問題を複数の独立した部分問題に再帰的に分割し、最小の部分問題に到達するまで続け、バックトラッキング時に部分問題の解を組み合わせて最終的に元の問題の解を得ます。 -- 動的プログラミングも問題を再帰的に分解しますが、分割統治法アルゴリズムとの主な違いは、動的プログラミングの部分問題が相互依存的であり、分解プロセス中に多くの重複する部分問題が現れることです。 -- バックトラッキングアルゴリズムは試行錯誤によってすべての可能な解を網羅し、枝刈りによって不必要な探索分岐を避けます。元の問題の解は一連の決定ステップから構成され、各決定ステップ前の各部分シーケンスを部分問題として考えることができます。 +- 分割統治法は、元の問題を再帰的に複数の互いに独立した部分問題へ分割し、最小の部分問題に至るまで分解したうえで、バックトラック時に部分問題の解を統合し、最終的に元の問題の解を得ます。 +- 動的計画法も問題を再帰的に分解しますが、分割統治法との主な違いは、動的計画法における部分問題が相互依存しており、分解の過程で多数の重複部分問題が現れることです。 +- バックトラッキング法は、試行と巻き戻しの中ですべての可能な解を列挙し、枝刈りによって不要な探索分岐を避けます。元の問題の解は一連の意思決定ステップから構成されるため、各決定ステップ以前の部分系列を一つの部分問題と見なせます。 -実際、動的プログラミングは最適化問題を解決するためによく使用され、これらは重複する部分問題を含むだけでなく、他に2つの主要な特徴があります:最適部分構造と無記憶性です。 +実際、動的計画法は最適化問題を解くためによく用いられます。これらは重複部分問題を含むだけでなく、さらに二つの大きな特性、すなわち最適部分構造と無後効性を備えています。 ## 14.2.1   最適部分構造 -階段登り問題を少し修正して、最適部分構造の概念を実証するのにより適したものにします。 +階段昇り問題を少し変更し、最適部分構造の概念をより示しやすくします。 -!!! question "階段登りの最小コスト" +!!! question "階段昇りの最小コスト" - 階段があり、一度に1段または2段上ることができ、階段の各段にはその段で支払う必要があるコストを表す非負の整数があります。非負の整数配列 $cost$ が与えられ、$cost[i]$ は $i$ 段目で支払う必要があるコストを表し、$cost[0]$ は地面(開始点)です。頂上に到達するために必要な最小コストは何ですか? + 階段が与えられ、各ステップで $1$ 段または $2$ 段上ることができます。各段には非負整数が貼られており、その段に到達するために支払う必要があるコストを表します。非負整数配列 $cost$ が与えられ、$cost[i]$ は第 $i$ 段で支払うコストを表し、$cost[0]$ は地面(開始地点)です。頂上に到達するために必要な最小コストを求めてください。 -下の図に示すように、1段目、2段目、3段目のコストがそれぞれ $1$、$10$、$1$ の場合、地面から3段目に登る最小コストは $2$ です。 +下図に示すように、第 $1$、$2$、$3$ 段のコストがそれぞれ $1$、$10$、$1$ である場合、地面から第 $3$ 段まで上る最小コストは $2$ です。 -![3段目に登る最小コスト](dp_problem_features.assets/min_cost_cs_example.png){ class="animation-figure" } +![第 3 段まで上る最小コスト](dp_problem_features.assets/min_cost_cs_example.png){ class="animation-figure" } -

図 14-6   3段目に登る最小コスト

+

図 14-6   第 3 段まで上る最小コスト

-$dp[i]$ を $i$ 段目に登る累積コストとします。$i$ 段目は $i-1$ 段目または $i-2$ 段目からのみ来ることができるため、$dp[i]$ は $dp[i-1] + cost[i]$ または $dp[i-2] + cost[i]$ のいずれかしかありえません。コストを最小化するために、2つのうち小さい方を選択すべきです: +$dp[i]$ を第 $i$ 段まで上るのに累積して支払ったコストとします。第 $i$ 段には $i - 1$ 段または $i - 2$ 段からしか到達できないため、$dp[i]$ は $dp[i - 1] + cost[i]$ または $dp[i - 2] + cost[i]$ のいずれかになります。コストをできるだけ小さくするには、この二つのうち小さいほうを選べばよいです。 $$ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i] $$ -これにより最適部分構造の意味がわかります:**元の問題の最適解は部分問題の最適解から構築される**。 +ここから最適部分構造の意味を導けます。**元の問題の最適解は、部分問題の最適解から構築される**ということです。 -この問題は明らかに最適部分構造を持っています:2つの部分問題 $dp[i-1]$ と $dp[i-2]$ の最適解からより良い方を選択し、それを使用して元の問題 $dp[i]$ の最適解を構築します。 +この問題が最適部分構造を持つことは明らかです。二つの部分問題の最適解 $dp[i-1]$ と $dp[i-2]$ からより良いほうを選び、それを用いて元の問題 $dp[i]$ の最適解を構築しています。 -では、前のセクションの階段登り問題は最適部分構造を持っているでしょうか?その目標は解の数を求めることで、これは数え上げ問題のようですが、別の方法で尋ねてみましょう:「解の最大数を求める」。驚くことに、**問題が変わったにもかかわらず、最適部分構造が現れた**ことがわかります:$n$ 段目での解の最大数は、$n-1$ 段目と $n-2$ 段目での解の最大数の和に等しいです。したがって、最適部分構造の解釈は非常に柔軟で、異なる問題では異なる意味を持ちます。 +では、前節の階段昇り問題には最適部分構造があるのでしょうか。その目的は方法数を求めることで、一見すると計数問題です。しかし問い方を変えて「最大の方法数を求める」とすると、意外にも、**問題の変更前後は等価であるにもかかわらず、最適部分構造が現れます**。すなわち、第 $n$ 段の最大方法数は第 $n-1$ 段と第 $n-2$ 段の最大方法数の和に等しいのです。このように、最適部分構造の解釈は比較的柔軟であり、問題によって意味合いが異なります。 -状態遷移方程式と初期状態 $dp[1] = cost[1]$ および $dp[2] = cost[2]$ に従って、動的プログラミングコードを得ることができます: +状態遷移方程式と初期状態 $dp[1] = cost[1]$ および $dp[2] = cost[2]$ に基づいて、次の動的計画法コードが得られます。 === "Python" ```python title="min_cost_climbing_stairs_dp.py" def min_cost_climbing_stairs_dp(cost: list[int]) -> int: - """最小コスト階段登り:動的プログラミング""" + """階段登りの最小コスト:動的計画法""" n = len(cost) - 1 if n == 1 or n == 2: return cost[n] - # dp テーブルを初期化、部分問題の解を格納するために使用 + # 部分問題の解を保存するために dp テーブルを初期化 dp = [0] * (n + 1) - # 初期状態:最小の部分問題の解を事前設定 + # 初期状態:最小部分問題の解をあらかじめ設定 dp[1], dp[2] = cost[1], cost[2] - # 状態遷移:小さい部分問題から大きい部分問題を段階的に解く + # 状態遷移:小さい部分問題から大きい部分問題へ順に解く for i in range(3, n + 1): dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i] return dp[n] @@ -61,17 +61,17 @@ $$ === "C++" ```cpp title="min_cost_climbing_stairs_dp.cpp" - /* 最小コスト階段登り:動的プログラミング */ + /* 階段登りの最小コスト:動的計画法 */ int minCostClimbingStairsDP(vector &cost) { int n = cost.size() - 1; if (n == 1 || n == 2) return cost[n]; - // DPテーブルを初期化し、部分問題の解を格納するために使用 + // 部分問題の解を保存するために dp テーブルを初期化 vector dp(n + 1); - // 初期状態:最小の部分問題の解を事前設定 + // 初期状態:最小部分問題の解をあらかじめ設定 dp[1] = cost[1]; dp[2] = cost[2]; - // 状態遷移:小さな問題から大きな部分問題を段階的に解く + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く for (int i = 3; i <= n; i++) { dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]; } @@ -82,17 +82,17 @@ $$ === "Java" ```java title="min_cost_climbing_stairs_dp.java" - /* 最小コスト階段登り:動的プログラミング */ + /* 階段登りの最小コスト:動的計画法 */ int minCostClimbingStairsDP(int[] cost) { int n = cost.length - 1; if (n == 1 || n == 2) return cost[n]; - // DPテーブルを初期化し、部分問題の解を格納するために使用 + // 部分問題の解を保存するために dp テーブルを初期化 int[] dp = new int[n + 1]; - // 初期状態:最小の部分問題の解を事前設定 + // 初期状態:最小部分問題の解をあらかじめ設定 dp[1] = cost[1]; dp[2] = cost[2]; - // 状態遷移:小さな問題から大きな部分問題を段階的に解く + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く for (int i = 3; i <= n; i++) { dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i]; } @@ -103,76 +103,239 @@ $$ === "C#" ```csharp title="min_cost_climbing_stairs_dp.cs" - [class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDP} + /* 階段登りの最小コスト:動的計画法 */ + int MinCostClimbingStairsDP(int[] cost) { + int n = cost.Length - 1; + if (n == 1 || n == 2) + return cost[n]; + // 部分問題の解を保存するために dp テーブルを初期化 + int[] dp = new int[n + 1]; + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = cost[1]; + dp[2] = cost[2]; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (int i = 3; i <= n; i++) { + dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i]; + } + return dp[n]; + } ``` === "Go" ```go title="min_cost_climbing_stairs_dp.go" - [class]{}-[func]{minCostClimbingStairsDP} + /* 階段登りの最小コスト:動的計画法 */ + func minCostClimbingStairsDP(cost []int) int { + n := len(cost) - 1 + if n == 1 || n == 2 { + return cost[n] + } + min := func(a, b int) int { + if a < b { + return a + } + return b + } + // 部分問題の解を保存するために dp テーブルを初期化 + dp := make([]int, n+1) + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = cost[1] + dp[2] = cost[2] + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i := 3; i <= n; i++ { + dp[i] = min(dp[i-1], dp[i-2]) + cost[i] + } + return dp[n] + } ``` === "Swift" ```swift title="min_cost_climbing_stairs_dp.swift" - [class]{}-[func]{minCostClimbingStairsDP} + /* 階段登りの最小コスト:動的計画法 */ + func minCostClimbingStairsDP(cost: [Int]) -> Int { + let n = cost.count - 1 + if n == 1 || n == 2 { + return cost[n] + } + // 部分問題の解を保存するために dp テーブルを初期化 + var dp = Array(repeating: 0, count: n + 1) + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = cost[1] + dp[2] = cost[2] + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i in 3 ... n { + dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i] + } + return dp[n] + } ``` === "JS" ```javascript title="min_cost_climbing_stairs_dp.js" - [class]{}-[func]{minCostClimbingStairsDP} + /* 階段登りの最小コスト:動的計画法 */ + function minCostClimbingStairsDP(cost) { + const n = cost.length - 1; + if (n === 1 || n === 2) { + return cost[n]; + } + // 部分問題の解を保存するために dp テーブルを初期化 + const dp = new Array(n + 1); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = cost[1]; + dp[2] = cost[2]; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (let i = 3; i <= n; i++) { + dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i]; + } + return dp[n]; + } ``` === "TS" ```typescript title="min_cost_climbing_stairs_dp.ts" - [class]{}-[func]{minCostClimbingStairsDP} + /* 階段登りの最小コスト:動的計画法 */ + function minCostClimbingStairsDP(cost: Array): number { + const n = cost.length - 1; + if (n === 1 || n === 2) { + return cost[n]; + } + // 部分問題の解を保存するために dp テーブルを初期化 + const dp = new Array(n + 1); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = cost[1]; + dp[2] = cost[2]; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (let i = 3; i <= n; i++) { + dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i]; + } + return dp[n]; + } ``` === "Dart" ```dart title="min_cost_climbing_stairs_dp.dart" - [class]{}-[func]{minCostClimbingStairsDP} + /* 階段登りの最小コスト:動的計画法 */ + int minCostClimbingStairsDP(List cost) { + int n = cost.length - 1; + if (n == 1 || n == 2) return cost[n]; + // 部分問題の解を保存するために dp テーブルを初期化 + List dp = List.filled(n + 1, 0); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = cost[1]; + dp[2] = cost[2]; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (int i = 3; i <= n; i++) { + dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]; + } + return dp[n]; + } ``` === "Rust" ```rust title="min_cost_climbing_stairs_dp.rs" - [class]{}-[func]{min_cost_climbing_stairs_dp} + /* 階段登りの最小コスト:動的計画法 */ + fn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 { + let n = cost.len() - 1; + if n == 1 || n == 2 { + return cost[n]; + } + // 部分問題の解を保存するために dp テーブルを初期化 + let mut dp = vec![-1; n + 1]; + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = cost[1]; + dp[2] = cost[2]; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i in 3..=n { + dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i]; + } + dp[n] + } ``` === "C" ```c title="min_cost_climbing_stairs_dp.c" - [class]{}-[func]{minCostClimbingStairsDP} + /* 階段登りの最小コスト:動的計画法 */ + int minCostClimbingStairsDP(int cost[], int costSize) { + int n = costSize - 1; + if (n == 1 || n == 2) + return cost[n]; + // 部分問題の解を保存するために dp テーブルを初期化 + int *dp = calloc(n + 1, sizeof(int)); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = cost[1]; + dp[2] = cost[2]; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (int i = 3; i <= n; i++) { + dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i]; + } + int res = dp[n]; + // メモリを解放する + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="min_cost_climbing_stairs_dp.kt" - [class]{}-[func]{minCostClimbingStairsDP} + /* 階段登りの最小コスト:動的計画法 */ + fun minCostClimbingStairsDP(cost: IntArray): Int { + val n = cost.size - 1 + if (n == 1 || n == 2) return cost[n] + // 部分問題の解を保存するために dp テーブルを初期化 + val dp = IntArray(n + 1) + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = cost[1] + dp[2] = cost[2] + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (i in 3..n) { + dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i] + } + return dp[n] + } ``` === "Ruby" ```ruby title="min_cost_climbing_stairs_dp.rb" - [class]{}-[func]{min_cost_climbing_stairs_dp} + ### 階段登りの最小コスト:動的計画法 ### + def min_cost_climbing_stairs_dp(cost) + n = cost.length - 1 + return cost[n] if n == 1 || n == 2 + # 部分問題の解を保存するために dp テーブルを初期化 + dp = Array.new(n + 1, 0) + # 初期状態:最小部分問題の解をあらかじめ設定 + dp[1], dp[2] = cost[1], cost[2] + # 状態遷移:小さい部分問題から大きい部分問題へ順に解く + (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] } + dp[n] + end ``` -下の図は上記コードの動的プログラミングプロセスを示しています。 +??? pythontutor "コードの可視化" -![階段登りの最小コストの動的プログラミングプロセス](dp_problem_features.assets/min_cost_cs_dp.png){ class="animation-figure" } +
+ -

図 14-7   階段登りの最小コストの動的プログラミングプロセス

+下図は上記コードの動的計画法の過程を示しています。 -この問題も空間最適化が可能で、1次元を0に圧縮し、空間計算量を $O(n)$ から $O(1)$ に削減できます: +![階段昇り最小コストの動的計画法の過程](dp_problem_features.assets/min_cost_cs_dp.png){ class="animation-figure" } + +

図 14-7   階段昇り最小コストの動的計画法の過程

+ +この問題では空間最適化も可能であり、一次元をゼロ次元まで圧縮することで、空間計算量を $O(n)$ から $O(1)$ に削減できます。 === "Python" ```python title="min_cost_climbing_stairs_dp.py" def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int: - """最小コスト階段登り:空間最適化動的プログラミング""" + """階段昇りの最小コスト:空間最適化後の動的計画法""" n = len(cost) - 1 if n == 1 or n == 2: return cost[n] @@ -185,7 +348,7 @@ $$ === "C++" ```cpp title="min_cost_climbing_stairs_dp.cpp" - /* 最小コスト階段登り:空間最適化動的プログラミング */ + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ int minCostClimbingStairsDPComp(vector &cost) { int n = cost.size() - 1; if (n == 1 || n == 2) @@ -203,7 +366,7 @@ $$ === "Java" ```java title="min_cost_climbing_stairs_dp.java" - /* 最小コスト階段登り:空間最適化動的プログラミング */ + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ int minCostClimbingStairsDPComp(int[] cost) { int n = cost.length - 1; if (n == 1 || n == 2) @@ -221,91 +384,236 @@ $$ === "C#" ```csharp title="min_cost_climbing_stairs_dp.cs" - [class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDPComp} + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ + int MinCostClimbingStairsDPComp(int[] cost) { + int n = cost.Length - 1; + if (n == 1 || n == 2) + return cost[n]; + int a = cost[1], b = cost[2]; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = Math.Min(a, tmp) + cost[i]; + a = tmp; + } + return b; + } ``` === "Go" ```go title="min_cost_climbing_stairs_dp.go" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ + func minCostClimbingStairsDPComp(cost []int) int { + n := len(cost) - 1 + if n == 1 || n == 2 { + return cost[n] + } + min := func(a, b int) int { + if a < b { + return a + } + return b + } + // 初期状態:最小部分問題の解をあらかじめ設定 + a, b := cost[1], cost[2] + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i := 3; i <= n; i++ { + tmp := b + b = min(a, tmp) + cost[i] + a = tmp + } + return b + } ``` === "Swift" ```swift title="min_cost_climbing_stairs_dp.swift" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ + func minCostClimbingStairsDPComp(cost: [Int]) -> Int { + let n = cost.count - 1 + if n == 1 || n == 2 { + return cost[n] + } + var (a, b) = (cost[1], cost[2]) + for i in 3 ... n { + (a, b) = (b, min(a, b) + cost[i]) + } + return b + } ``` === "JS" ```javascript title="min_cost_climbing_stairs_dp.js" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ + function minCostClimbingStairsDPComp(cost) { + const n = cost.length - 1; + if (n === 1 || n === 2) { + return cost[n]; + } + let a = cost[1], + b = cost[2]; + for (let i = 3; i <= n; i++) { + const tmp = b; + b = Math.min(a, tmp) + cost[i]; + a = tmp; + } + return b; + } ``` === "TS" ```typescript title="min_cost_climbing_stairs_dp.ts" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ + function minCostClimbingStairsDPComp(cost: Array): number { + const n = cost.length - 1; + if (n === 1 || n === 2) { + return cost[n]; + } + let a = cost[1], + b = cost[2]; + for (let i = 3; i <= n; i++) { + const tmp = b; + b = Math.min(a, tmp) + cost[i]; + a = tmp; + } + return b; + } ``` === "Dart" ```dart title="min_cost_climbing_stairs_dp.dart" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ + int minCostClimbingStairsDPComp(List cost) { + int n = cost.length - 1; + if (n == 1 || n == 2) return cost[n]; + int a = cost[1], b = cost[2]; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = min(a, tmp) + cost[i]; + a = tmp; + } + return b; + } ``` === "Rust" ```rust title="min_cost_climbing_stairs_dp.rs" - [class]{}-[func]{min_cost_climbing_stairs_dp_comp} + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ + fn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 { + let n = cost.len() - 1; + if n == 1 || n == 2 { + return cost[n]; + }; + let (mut a, mut b) = (cost[1], cost[2]); + for i in 3..=n { + let tmp = b; + b = cmp::min(a, tmp) + cost[i]; + a = tmp; + } + b + } ``` === "C" ```c title="min_cost_climbing_stairs_dp.c" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ + int minCostClimbingStairsDPComp(int cost[], int costSize) { + int n = costSize - 1; + if (n == 1 || n == 2) + return cost[n]; + int a = cost[1], b = cost[2]; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = myMin(a, tmp) + cost[i]; + a = tmp; + } + return b; + } ``` === "Kotlin" ```kotlin title="min_cost_climbing_stairs_dp.kt" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* 階段昇りの最小コスト:空間最適化後の動的計画法 */ + fun minCostClimbingStairsDPComp(cost: IntArray): Int { + val n = cost.size - 1 + if (n == 1 || n == 2) return cost[n] + var a = cost[1] + var b = cost[2] + for (i in 3..n) { + val tmp = b + b = min(a, tmp) + cost[i] + a = tmp + } + return b + } ``` === "Ruby" ```ruby title="min_cost_climbing_stairs_dp.rb" - [class]{}-[func]{min_cost_climbing_stairs_dp_comp} + ### 階段登りの最小コスト:動的計画法 ### + def min_cost_climbing_stairs_dp(cost) + n = cost.length - 1 + return cost[n] if n == 1 || n == 2 + # 部分問題の解を保存するために dp テーブルを初期化 + dp = Array.new(n + 1, 0) + # 初期状態:最小部分問題の解をあらかじめ設定 + dp[1], dp[2] = cost[1], cost[2] + # 状態遷移:小さい部分問題から大きい部分問題へ順に解く + (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] } + dp[n] + end + + # 階段昇りの最小コスト:空間最適化後の動的計画法 + def min_cost_climbing_stairs_dp_comp(cost) + n = cost.length - 1 + return cost[n] if n == 1 || n == 2 + a, b = cost[1], cost[2] + (3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] } + b + end ``` -## 14.2.2   無記憶性 +??? pythontutor "コードの可視化" -無記憶性は動的プログラミングが問題解決に効果的であることを可能にする重要な特徴の1つです。その定義は:**特定の状態が与えられたとき、その将来の発展は現在の状態のみに関連し、過去に経験したすべての状態とは無関係である**。 +
+ -階段登り問題を例に取ると、状態 $i$ が与えられたとき、それは状態 $i+1$ と $i+2$ に発展し、それぞれ1段ジャンプと2段ジャンプに対応します。これら2つの選択をするとき、状態 $i$ より前の状態を考慮する必要はありません。なぜなら、それらは状態 $i$ の将来に影響しないからです。 +## 14.2.2   無後効性 -しかし、階段登り問題に制約を追加すると、状況が変わります。 +無後効性は、動的計画法が問題を効率よく解ける重要な特性の一つであり、その定義は次のとおりです。**ある確定した状態が与えられたとき、その後の発展は現在の状態のみに依存し、過去に経たすべての状態には依存しない**。 -!!! question "制約付き階段登り" +階段昇り問題を例にすると、状態 $i$ が与えられたとき、そこから状態 $i+1$ と状態 $i+2$ へ発展し、それぞれ $1$ 段進む場合と $2$ 段進む場合に対応します。この二つの選択を行う際、状態 $i$ より前の状態を考慮する必要はなく、それらは状態 $i$ の将来に影響を与えません。 - $n$ 段の階段があり、毎回1段または2段上ることができますが、**1段を2回連続でジャンプすることはできません**。頂上に登る方法は何通りありますか? +しかし、階段昇り問題に制約を一つ追加すると、状況は変わります。 -下の図に示すように、3段目に登る実行可能な選択肢は2つだけで、1段を3回連続でジャンプする選択肢は制約条件を満たさないため破棄されます。 +!!! question "制約付き階段昇り" -![制約付きで3段目に登る実行可能な選択肢の数](dp_problem_features.assets/climbing_stairs_constraint_example.png){ class="animation-figure" } + 全部で $n$ 段ある階段が与えられ、各ステップで $1$ 段または $2$ 段上ることができます。**ただし、連続する 2 回で $1$ 段ずつ上ることはできません**。頂上まで上る方法は何通りあるでしょうか。 -

図 14-8   制約付きで3段目に登る実行可能な選択肢の数

+下図に示すように、第 $3$ 段まで上る実行可能な方法は $2$ 通りしか残りません。そのうち、$1$ 段ずつ 3 回連続で上る方法は制約を満たさないため除外されます。 -この問題では、前回が1段ジャンプだった場合、次回は必ず2段ジャンプでなければなりません。これは**次のステップの選択が現在の状態(現在の階段段数)だけでは独立して決定できず、前の状態(前回の階段段数)にも依存する**ことを意味します。 +![制約付きで第 3 段まで上る方法数](dp_problem_features.assets/climbing_stairs_constraint_example.png){ class="animation-figure" } -この問題がもはや無記憶性を満たさず、状態遷移方程式 $dp[i] = dp[i-1] + dp[i-2]$ も失敗することは容易にわかります。なぜなら $dp[i-1]$ は今回の1段ジャンプを表しますが、多くの「前回が1段ジャンプだった」選択肢を含んでおり、制約を満たすためにはこれらを直接 $dp[i]$ に含めることはできません。 +

図 14-8   制約付きで第 3 段まで上る方法数

-このため、状態定義を拡張する必要があります:**状態 $[i, j]$ は $i$ 段目にいて、前回が $j$ 段ジャンプだったことを表す**。ここで $j \in \{1, 2\}$ です。この状態定義は前回が1段ジャンプだったか2段ジャンプだったかを効果的に区別し、現在の状態がどこから来たかを適切に判断できます。 +この問題では、前回が $1$ 段上りだった場合、次回は必ず $2$ 段上らなければなりません。これは、**次の一手が現在の状態(現在いる階段の段数)だけでは独立に決まらず、一つ前の状態(前回いた段数)にも関係する**ことを意味します。 -- 前回が1段ジャンプだった場合、前々回は必ず2段ジャンプを選択していたはずです。つまり、$dp[i, 1]$ は $dp[i-1, 2]$ からのみ遷移できます。 -- 前回が2段ジャンプだった場合、前々回は1段ジャンプまたは2段ジャンプを選択できました。つまり、$dp[i, 2]$ は $dp[i-2, 1]$ または $dp[i-2, 2]$ から遷移できます。 +容易に分かるように、この問題はもはや無後効性を満たしておらず、状態遷移方程式 $dp[i] = dp[i-1] + dp[i-2]$ も成立しません。というのも、$dp[i-1]$ は今回 $1$ 段上る場合を表しますが、その中には「前回も $1$ 段上ってきた」方法が多数含まれており、制約を満たすためには $dp[i-1]$ をそのまま $dp[i]$ に加えることができないからです。 -下の図に示すように、$dp[i, j]$ は状態 $[i, j]$ の解の数を表します。この時点で、状態遷移方程式は次のようになります: +このため、状態定義を拡張する必要があります。**状態 $[i, j]$ は、第 $i$ 段にいて前回に $j$ 段上ったことを表す**とし、ここで $j \in \{1, 2\}$ です。この状態定義により、前回が $1$ 段上りか $2$ 段上りかを有効に区別でき、現在の状態がどこから来たのかを判断できます。 + +- 前回に $1$ 段上った場合、その前の回は $2$ 段上りしか選べないため、$dp[i, 1]$ は $dp[i-1, 2]$ からのみ遷移できます。 +- 前回に $2$ 段上った場合、その前の回は $1$ 段上りまたは $2$ 段上りを選べるため、$dp[i, 2]$ は $dp[i-2, 1]$ または $dp[i-2, 2]$ から遷移できます。 + +下図に示すように、この定義のもとでは $dp[i, j]$ は状態 $[i, j]$ に対応する方法数を表します。このとき状態遷移方程式は次のようになります。 $$ \begin{cases} @@ -314,25 +622,25 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \end{cases} $$ -![制約を考慮した再帰関係](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png){ class="animation-figure" } +![制約を考慮した漸化関係](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png){ class="animation-figure" } -

図 14-9   制約を考慮した再帰関係

+

図 14-9   制約を考慮した漸化関係

-最終的に、$dp[n, 1] + dp[n, 2]$ を返せばよく、この2つの合計が $n$ 段目に登る解の総数を表します: +最終的に、$dp[n, 1] + dp[n, 2]$ を返せば十分であり、その和が第 $n$ 段まで上る方法の総数を表します。 === "Python" ```python title="climbing_stairs_constraint_dp.py" def climbing_stairs_constraint_dp(n: int) -> int: - """制約付き階段登り:動的プログラミング""" + """制約付き階段登り:動的計画法""" if n == 1 or n == 2: return 1 - # dp テーブルを初期化、部分問題の解を格納するために使用 + # 部分問題の解を保存するために dp テーブルを初期化 dp = [[0] * 3 for _ in range(n + 1)] - # 初期状態:最小の部分問題の解を事前設定 + # 初期状態:最小部分問題の解をあらかじめ設定 dp[1][1], dp[1][2] = 1, 0 dp[2][1], dp[2][2] = 0, 1 - # 状態遷移:小さい部分問題から大きい部分問題を段階的に解く + # 状態遷移:小さい部分問題から大きい部分問題へ順に解く for i in range(3, n + 1): dp[i][1] = dp[i - 1][2] dp[i][2] = dp[i - 2][1] + dp[i - 2][2] @@ -342,19 +650,19 @@ $$ === "C++" ```cpp title="climbing_stairs_constraint_dp.cpp" - /* 制約付き階段登り:動的プログラミング */ + /* 制約付き階段登り:動的計画法 */ int climbingStairsConstraintDP(int n) { if (n == 1 || n == 2) { return 1; } - // DPテーブルを初期化し、部分問題の解を格納するために使用 + // 部分問題の解を保存するために dp テーブルを初期化 vector> dp(n + 1, vector(3, 0)); - // 初期状態:最小の部分問題の解を事前設定 + // 初期状態:最小部分問題の解をあらかじめ設定 dp[1][1] = 1; dp[1][2] = 0; dp[2][1] = 0; dp[2][2] = 1; - // 状態遷移:小さな問題から大きな部分問題を段階的に解く + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く for (int i = 3; i <= n; i++) { dp[i][1] = dp[i - 1][2]; dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; @@ -366,19 +674,19 @@ $$ === "Java" ```java title="climbing_stairs_constraint_dp.java" - /* 制約付き階段登り:動的プログラミング */ + /* 制約付き階段登り:動的計画法 */ int climbingStairsConstraintDP(int n) { if (n == 1 || n == 2) { return 1; } - // DPテーブルを初期化し、部分問題の解を格納するために使用 + // 部分問題の解を保存するために dp テーブルを初期化 int[][] dp = new int[n + 1][3]; - // 初期状態:最小の部分問題の解を事前設定 + // 初期状態:最小部分問題の解をあらかじめ設定 dp[1][1] = 1; dp[1][2] = 0; dp[2][1] = 0; dp[2][2] = 1; - // 状態遷移:小さな問題から大きな部分問題を段階的に解く + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く for (int i = 3; i <= n; i++) { dp[i][1] = dp[i - 1][2]; dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; @@ -390,69 +698,261 @@ $$ === "C#" ```csharp title="climbing_stairs_constraint_dp.cs" - [class]{climbing_stairs_constraint_dp}-[func]{ClimbingStairsConstraintDP} + /* 制約付き階段登り:動的計画法 */ + int ClimbingStairsConstraintDP(int n) { + if (n == 1 || n == 2) { + return 1; + } + // 部分問題の解を保存するために dp テーブルを初期化 + int[,] dp = new int[n + 1, 3]; + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1, 1] = 1; + dp[1, 2] = 0; + dp[2, 1] = 0; + dp[2, 2] = 1; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (int i = 3; i <= n; i++) { + dp[i, 1] = dp[i - 1, 2]; + dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2]; + } + return dp[n, 1] + dp[n, 2]; + } ``` === "Go" ```go title="climbing_stairs_constraint_dp.go" - [class]{}-[func]{climbingStairsConstraintDP} + /* 制約付き階段登り:動的計画法 */ + func climbingStairsConstraintDP(n int) int { + if n == 1 || n == 2 { + return 1 + } + // 部分問題の解を保存するために dp テーブルを初期化 + dp := make([][3]int, n+1) + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1][1] = 1 + dp[1][2] = 0 + dp[2][1] = 0 + dp[2][2] = 1 + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i := 3; i <= n; i++ { + dp[i][1] = dp[i-1][2] + dp[i][2] = dp[i-2][1] + dp[i-2][2] + } + return dp[n][1] + dp[n][2] + } ``` === "Swift" ```swift title="climbing_stairs_constraint_dp.swift" - [class]{}-[func]{climbingStairsConstraintDP} + /* 制約付き階段登り:動的計画法 */ + func climbingStairsConstraintDP(n: Int) -> Int { + if n == 1 || n == 2 { + return 1 + } + // 部分問題の解を保存するために dp テーブルを初期化 + var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1) + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1][1] = 1 + dp[1][2] = 0 + dp[2][1] = 0 + dp[2][2] = 1 + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i in 3 ... n { + dp[i][1] = dp[i - 1][2] + dp[i][2] = dp[i - 2][1] + dp[i - 2][2] + } + return dp[n][1] + dp[n][2] + } ``` === "JS" ```javascript title="climbing_stairs_constraint_dp.js" - [class]{}-[func]{climbingStairsConstraintDP} + /* 制約付き階段登り:動的計画法 */ + function climbingStairsConstraintDP(n) { + if (n === 1 || n === 2) { + return 1; + } + // 部分問題の解を保存するために dp テーブルを初期化 + const dp = Array.from(new Array(n + 1), () => new Array(3)); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (let i = 3; i <= n; i++) { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + return dp[n][1] + dp[n][2]; + } ``` === "TS" ```typescript title="climbing_stairs_constraint_dp.ts" - [class]{}-[func]{climbingStairsConstraintDP} + /* 制約付き階段登り:動的計画法 */ + function climbingStairsConstraintDP(n: number): number { + if (n === 1 || n === 2) { + return 1; + } + // 部分問題の解を保存するために dp テーブルを初期化 + const dp = Array.from({ length: n + 1 }, () => new Array(3)); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (let i = 3; i <= n; i++) { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + return dp[n][1] + dp[n][2]; + } ``` === "Dart" ```dart title="climbing_stairs_constraint_dp.dart" - [class]{}-[func]{climbingStairsConstraintDP} + /* 制約付き階段登り:動的計画法 */ + int climbingStairsConstraintDP(int n) { + if (n == 1 || n == 2) { + return 1; + } + // 部分問題の解を保存するために dp テーブルを初期化 + List> dp = List.generate(n + 1, (index) => List.filled(3, 0)); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (int i = 3; i <= n; i++) { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + return dp[n][1] + dp[n][2]; + } ``` === "Rust" ```rust title="climbing_stairs_constraint_dp.rs" - [class]{}-[func]{climbing_stairs_constraint_dp} + /* 制約付き階段登り:動的計画法 */ + fn climbing_stairs_constraint_dp(n: usize) -> i32 { + if n == 1 || n == 2 { + return 1; + }; + // 部分問題の解を保存するために dp テーブルを初期化 + let mut dp = vec![vec![-1; 3]; n + 1]; + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i in 3..=n { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + dp[n][1] + dp[n][2] + } ``` === "C" ```c title="climbing_stairs_constraint_dp.c" - [class]{}-[func]{climbingStairsConstraintDP} + /* 制約付き階段登り:動的計画法 */ + int climbingStairsConstraintDP(int n) { + if (n == 1 || n == 2) { + return 1; + } + // 部分問題の解を保存するために dp テーブルを初期化 + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(3, sizeof(int)); + } + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (int i = 3; i <= n; i++) { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + int res = dp[n][1] + dp[n][2]; + // メモリを解放する + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="climbing_stairs_constraint_dp.kt" - [class]{}-[func]{climbingStairsConstraintDP} + /* 制約付き階段登り:動的計画法 */ + fun climbingStairsConstraintDP(n: Int): Int { + if (n == 1 || n == 2) { + return 1 + } + // 部分問題の解を保存するために dp テーブルを初期化 + val dp = Array(n + 1) { IntArray(3) } + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1][1] = 1 + dp[1][2] = 0 + dp[2][1] = 0 + dp[2][2] = 1 + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (i in 3..n) { + dp[i][1] = dp[i - 1][2] + dp[i][2] = dp[i - 2][1] + dp[i - 2][2] + } + return dp[n][1] + dp[n][2] + } ``` === "Ruby" ```ruby title="climbing_stairs_constraint_dp.rb" - [class]{}-[func]{climbing_stairs_constraint_dp} + ### 制約付き階段登り:動的計画法 ### + def climbing_stairs_constraint_dp(n) + return 1 if n == 1 || n == 2 + + # 部分問題の解を保存するために dp テーブルを初期化 + dp = Array.new(n + 1) { Array.new(3, 0) } + # 初期状態:最小部分問題の解をあらかじめ設定 + dp[1][1], dp[1][2] = 1, 0 + dp[2][1], dp[2][2] = 0, 1 + # 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i in 3...(n + 1) + dp[i][1] = dp[i - 1][2] + dp[i][2] = dp[i - 2][1] + dp[i - 2][2] + end + + dp[n][1] + dp[n][2] + end ``` -上記のケースでは、前の状態のみを考慮すればよいため、状態定義を拡張することで依然として無記憶性を満たすことができます。しかし、一部の問題では非常に深刻な「状態効果」があります。 +??? pythontutor "コードの可視化" -!!! question "障害物生成付き階段登り" +
+ - $n$ 段の階段があり、毎回1段または2段上ることができます。**$i$ 段目に登ったとき、システムが自動的に $2i$ 段目に障害物を置き、その後のすべてのラウンドで $2i$ 段目にジャンプすることが禁止される**と規定されています。例えば、最初の2ラウンドで2段目と3段目にジャンプした場合、その後は4段目と6段目にジャンプできません。頂上に登る方法は何通りありますか? +上の例では、追加で考慮すべきなのは一つ前の状態だけであるため、状態定義を拡張することで問題を再び無後効性に適合させることができます。しかし、問題によっては非常に強い「後効性」があります。 -この問題では、次のジャンプはすべての過去の状態に依存します。各ジャンプがより高い段に障害物を置き、将来のジャンプに影響するからです。このような問題では、動的プログラミングはしばしば解決に苦労します。 +!!! question "階段昇りと障害物生成" -実際、多くの複雑な組み合わせ最適化問題(巡回セールスマン問題など)は無記憶性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を選択して、限られた時間内に使用可能な局所最適解を得ます。 + 全部で $n$ 段ある階段が与えられ、各ステップで $1$ 段または $2$ 段上ることができます。**第 $i$ 段に到達すると、システムは自動的に第 $2i$ 段に障害物を置き、それ以降はどの回でも第 $2i$ 段へ跳ぶことができない**とします。例えば、最初の 2 回でそれぞれ第 $2$ 段、第 $3$ 段に到達した場合、その後は第 $4$ 段と第 $6$ 段に跳ぶことはできません。頂上まで上る方法は何通りあるでしょうか。 + +この問題では、次の跳躍が過去のすべての状態に依存します。なぜなら、各跳躍がより高い段に障害物を設置し、将来の跳躍に影響するからです。この種の問題は、動的計画法では解きにくいことが多いです。 + +実際、多くの複雑な組合せ最適化問題(例えば巡回セールスマン問題)は無後効性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を用いて、限られた時間内に実用的な局所最適解を得ます。 diff --git a/ja/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/ja/docs/chapter_dynamic_programming/dp_solution_pipeline.md index c795300a0..85574b6ae 100644 --- a/ja/docs/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/ja/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -2,72 +2,72 @@ comments: true --- -# 14.3   動的プログラミング問題解決アプローチ +# 14.3   動的計画法の問題解決の考え方 -前の2つのセクションでは、動的プログラミング問題の主要な特徴を紹介しました。次に、より実用的な2つの問題を一緒に探索しましょう。 +前の 2 節では動的計画法の問題の主要な特徴を紹介しました。ここからは、さらに実用的な 2 つの問題を一緒に考えていきます。 -1. 問題が動的プログラミング問題かどうかをどのように判断するか? -2. 動的プログラミング問題を解決する完全なステップは何か? +1. ある問題が動的計画法の問題かどうかを、どのように判断すればよいでしょうか? +2. 動的計画法の問題を解くには、どこから着手し、完全な手順はどのようなものでしょうか? ## 14.3.1   問題の判定 -一般的に言えば、問題が重複する部分問題、最適部分構造を含み、無記憶性を示す場合、通常動的プログラミング解法に適しています。しかし、問題の説明から直接これらの特徴を抽出することはしばしば困難です。したがって、通常は条件を緩和し、**まず問題がバックトラッキング(全探索)を使用した解決に適しているかどうかを観察**します。 +一般に、ある問題が重複部分問題と最適部分構造を含み、さらに無後效性を満たしているなら、通常は動的計画法で解くのに適しています。しかし、問題文からこれらの性質を直接読み取るのは簡単ではありません。そのため通常は条件を少し緩めて、**まずその問題がバックトラッキング(全探索)で解くのに適しているか**を観察します。 -**バックトラッキングに適した問題は通常「決定木モデル」に適合**し、これは木構造を使用して記述でき、各ノードは決定を表し、各パスは決定のシーケンスを表します。 +**バックトラッキングで解くのに適した問題は、通常「決定木モデル」を満たします**。この種の問題は木構造で表現でき、各ノードは 1 つの決定を表し、各経路は 1 つの決定列を表します。 -言い換えると、問題が明示的な決定概念を含み、解が一連の決定を通じて生成される場合、それは決定木モデルに適合し、通常バックトラッキングを使用して解決できます。 +言い換えると、問題に明確な決定の概念が含まれており、解が一連の決定によって生成されるなら、その問題は決定木モデルを満たし、通常はバックトラッキングで解くことができます。 -この基礎の上で、動的プログラミング問題を判定するための「ボーナスポイント」があります。 +これに加えて、動的計画法の問題には判定のための「加点要素」もあります。 -- 問題に最大化(最小化)または最も(最も少ない)最適な解を見つけるという記述が含まれている。 -- 問題の状態がリスト、多次元行列、または木を使用して表現でき、状態がその周囲の状態と再帰関係を持っている。 +- 問題文に最大(最小)や最多(最少)などの最適化に関する記述がある。 +- 問題の状態が配列、多次元行列、または木で表現でき、ある状態とその周辺の状態の間に漸化的な関係がある。 -対応して、「ペナルティポイント」もあります。 +反対に、「減点要素」もあります。 -- 問題の目標は最適解だけでなく、すべての可能な解を見つけることである。 -- 問題の説明に順列と組み合わせの明らかな特徴があり、特定の複数の解を返す必要がある。 +- 問題の目的が最適解を求めることではなく、あり得るすべての解を列挙することである。 +- 問題文に明確な順列・組合せの特徴があり、具体的な複数の解を返す必要がある。 -問題が決定木モデルに適合し、比較的明らかな「ボーナスポイント」を持つ場合、それが動的プログラミング問題であると仮定し、解決プロセス中に検証できます。 +ある問題が決定木モデルを満たし、さらに比較的明確な「加点要素」を備えているなら、その問題は動的計画法の問題であると仮定し、解く過程でそれを検証できます。 -## 14.3.2   問題解決ステップ +## 14.3.2   問題を解く手順 -動的プログラミング問題解決プロセスは問題の性質と難易度によって異なりますが、一般的に次のステップに従います:決定の記述、状態の定義、$dp$ テーブルの確立、状態遷移方程式の導出、境界条件の決定など。 +動的計画法の解法の流れは問題の性質や難易度によって異なりますが、通常は次の手順に従います。すなわち、決定を記述し、状態を定義し、$dp$ テーブルを構築し、状態遷移方程式を導出し、境界条件を定めます。 -問題解決ステップをより具体的に説明するために、古典的な問題「最小経路和」を例として使用します。 +解法の手順をより具体的に示すために、ここでは古典的な問題である「最小経路和」を例にします。 !!! question - $n \times m$ の二次元グリッド `grid` が与えられ、グリッドの各セルには負でない整数が含まれ、そのセルのコストを表します。ロボットは左上のセルから始まり、各ステップで下または右にのみ移動でき、右下のセルに到達するまで続けます。左上から右下への最小経路和を返してください。 + $n \times m$ の 2 次元グリッド `grid` が与えられます。グリッドの各セルには非負整数が格納されており、そのセルのコストを表します。ロボットは左上のセルを始点とし、毎回下または右に 1 マスだけ移動して、右下のセルまで進みます。左上から右下までの最小経路和を返してください。 -下の図は例を示しており、与えられたグリッドの最小経路和は $13$ です。 +次の図は 1 つの例を示しており、このグリッドの最小経路和は $13$ です。 -![最小経路和の例データ](dp_solution_pipeline.assets/min_path_sum_example.png){ class="animation-figure" } +![最小経路和のサンプルデータ](dp_solution_pipeline.assets/min_path_sum_example.png){ class="animation-figure" } -

図 14-10   最小経路和の例データ

+

図 14-10   最小経路和のサンプルデータ

-**第1ステップ:各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る** +**ステップ 1:各ラウンドの決定を考え、状態を定義して、$dp$ テーブルを得る** -この問題の各ラウンドの決定は、現在のセルから下または右に1ステップ移動することです。現在のセルの行と列のインデックスが $[i, j]$ であると仮定すると、下または右に移動した後、インデックスは $[i+1, j]$ または $[i, j+1]$ になります。したがって、状態には2つの変数が含まれるべきです:行インデックスと列インデックス、$[i, j]$ と表記されます。 +この問題における各ラウンドの決定は、現在のマスから下または右へ 1 マス進むことです。現在のマスの行・列インデックスを $[i, j]$ とすると、下または右へ 1 マス進んだ後のインデックスは $[i+1, j]$ または $[i, j+1]$ になります。したがって、状態には行インデックスと列インデックスの 2 つの変数を含め、$[i, j]$ と表します。 -状態 $[i, j]$ は部分問題に対応します:開始点 $[0, 0]$ から $[i, j]$ への最小経路和、$dp[i, j]$ と表記されます。 +状態 $[i, j]$ に対応する部分問題は、始点 $[0, 0]$ から $[i, j]$ まで進む最小経路和であり、その解を $dp[i, j]$ と記します。 -このようにして、下の図に示す二次元 $dp$ 行列を得ます。そのサイズは入力グリッド $grid$ と同じです。 +これで、次の図に示す 2 次元の $dp$ 行列が得られます。そのサイズは入力グリッド $grid$ と同じです。 -![状態定義とDPテーブル](dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png){ class="animation-figure" } +![状態の定義と dp テーブル](dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png){ class="animation-figure" } -

図 14-11   状態定義とDPテーブル

+

図 14-11   状態の定義と dp テーブル

!!! note - 動的プログラミングとバックトラッキングは決定のシーケンスとして記述でき、状態はすべての決定変数から構成されます。問題解決の進行を記述するすべての変数を含むべきで、次の状態を導出するのに十分な情報を含んでいる必要があります。 + 動的計画法とバックトラッキングの過程は、いずれも 1 つの決定列として記述できます。そして状態は、すべての決定変数から構成されます。状態には解法の進行状況を表すすべての変数が含まれているべきであり、次の状態を導くのに十分な情報を持っている必要があります。 + + 各状態は 1 つの部分問題に対応しており、すべての部分問題の解を保存するために $dp$ テーブルを定義します。状態の各独立変数は、$dp$ テーブルの 1 つの次元に対応します。本質的に、$dp$ テーブルは状態と部分問題の解との対応関係です。 - 各状態は部分問題に対応し、すべての部分問題の解を保存するための $dp$ テーブルを定義します。状態の各独立変数は $dp$ テーブルの次元です。本質的に、$dp$ テーブルは状態と部分問題の解の間のマッピングです。 +**ステップ 2:最適部分構造を見つけ、状態遷移方程式を導出する** -**第2ステップ:最適部分構造を特定し、状態遷移方程式を導出する** +状態 $[i, j]$ は、上のマス $[i-1, j]$ または左のマス $[i, j-1]$ からしか遷移してきません。したがって最適部分構造は、$[i, j]$ に到達する最小経路和が、$[i, j-1]$ の最小経路和と $[i-1, j]$ の最小経路和のうち小さい方によって決まる、ということです。 -状態 $[i, j]$ について、それは上のセル $[i-1, j]$ または左のセル $[i, j-1]$ からのみ導出できます。したがって、最適部分構造は:$[i, j]$ に到達する最小経路和は、$[i, j-1]$ と $[i-1, j]$ の最小経路和の小さい方によって決定されます。 - -上記の分析に基づいて、下の図に示す状態遷移方程式を導出できます: +以上の分析から、次の図に示す状態遷移方程式を導くことができます。 $$ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j] @@ -79,95 +79,95 @@ $$ !!! note - 定義された $dp$ テーブルに基づいて、元の問題と部分問題の関係を考え、部分問題の最適解から元の問題の最適解をどのように構築するか、つまり最適部分構造を見つけます。 + 定義済みの $dp$ テーブルに基づいて、元の問題と部分問題の関係を考え、部分問題の最適解から元の問題の最適解を構成する方法、すなわち最適部分構造を見つけます。 - 最適部分構造を特定したら、それを使用して状態遷移方程式を構築できます。 + ひとたび最適部分構造が見つかれば、それを使って状態遷移方程式を構築できます。 -**第3ステップ:境界条件と状態遷移順序を決定する** +**ステップ 3:境界条件と状態遷移の順序を決める** -この問題では、最初の行の状態は左の状態からのみ来ることができ、最初の列の状態は上の状態からのみ来ることができるため、最初の行 $i = 0$ と最初の列 $j = 0$ が境界条件です。 +この問題では、先頭行にある状態は左の状態からしか得られず、先頭列にある状態は上の状態からしか得られません。したがって、先頭行 $i = 0$ と先頭列 $j = 0$ が境界条件になります。 -下の図に示すように、各セルは左のセルと上のセルから導出されるため、ループを使用して行列を走査し、外側のループは行を反復し、内側のループは列を反復します。 +次の図に示すように、各マスは左のマスと上のマスから遷移してくるため、ループを用いて行列を走査します。外側のループで各行を、内側のループで各列を走査します。 -![境界条件と状態遷移順序](dp_solution_pipeline.assets/min_path_sum_solution_initial_state.png){ class="animation-figure" } +![境界条件と状態遷移の順序](dp_solution_pipeline.assets/min_path_sum_solution_initial_state.png){ class="animation-figure" } -

図 14-13   境界条件と状態遷移順序

+

図 14-13   境界条件と状態遷移の順序

!!! note - 境界条件は動的プログラミングで $dp$ テーブルを初期化するために使用され、探索では枝刈りに使用されます。 + 境界条件は、動的計画法では $dp$ テーブルの初期化に使われ、探索では枝刈りに使われます。 + + 状態遷移の順序で重要なのは、現在の問題の解を計算するときに、それが依存するより小さな部分問題の解がすべてすでに正しく計算済みであることを保証する点です。 - 状態遷移順序の核心は、現在の問題の解を計算するとき、それが依存するすべての小さな部分問題が既に正しく計算されていることを確保することです。 +以上の分析により、すでに動的計画法のコードを直接書くことができます。しかし、部分問題への分解はトップダウンの考え方であるため、「力任せ探索 $\rightarrow$ メモ化探索 $\rightarrow$ 動的計画法」の順に実装するほうが、思考の流れにはより自然です。 -上記の分析に基づいて、動的プログラミングコードを直接書くことができます。しかし、部分問題の分解はトップダウンアプローチであるため、「力任せ探索 → メモ化探索 → 動的プログラミング」の順序で実装することが習慣的な思考により適合します。 +### 1.   方法 1:力任せ探索 -### 1.   方法1:力任せ探索 +状態 $[i, j]$ から探索を開始し、より小さな状態 $[i-1, j]$ と $[i, j-1]$ へと分解していきます。再帰関数には次の要素が含まれます。 -状態 $[i, j]$ から探索を開始し、それを常により小さな状態 $[i-1, j]$ と $[i, j-1]$ に分解します。再帰関数には以下の要素が含まれます。 - -- **再帰パラメータ**:状態 $[i, j]$。 -- **戻り値**:$[0, 0]$ から $[i, j]$ への最小経路和 $dp[i, j]$。 +- **再帰引数**:状態 $[i, j]$ 。 +- **戻り値**:$[0, 0]$ から $[i, j]$ までの最小経路和 $dp[i, j]$ 。 - **終了条件**:$i = 0$ かつ $j = 0$ のとき、コスト $grid[0, 0]$ を返す。 -- **枝刈り**:$i < 0$ または $j < 0$ でインデックスが範囲外のとき、コスト $+\infty$ を返し、実行不可能性を表す。 +- **枝刈り**:$i < 0$ または $j < 0$ でインデックスが範囲外になった場合、コスト $+\infty$ を返し、実行不可能であることを表す。 -実装コードは以下の通りです: +実装コードは次のとおりです。 === "Python" ```python title="min_path_sum.py" def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int: - """最小パス和:ブルートフォース探索""" - # 左上のセルの場合、探索を終了 + """最小経路和:全探索""" + # 左上のセルなら探索を終了する if i == 0 and j == 0: return grid[0][0] - # 行または列のインデックスが範囲外の場合、+∞ コストを返す + # 行または列のインデックスが範囲外なら、コスト +∞ を返す if i < 0 or j < 0: return inf - # 左上から (i-1, j) と (i, j-1) への最小パスコストを計算 + # 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する up = min_path_sum_dfs(grid, i - 1, j) left = min_path_sum_dfs(grid, i, j - 1) - # 左上から (i, j) への最小パスコストを返す + # 左上隅から (i, j) までの最小経路コストを返す return min(left, up) + grid[i][j] ``` === "C++" ```cpp title="min_path_sum.cpp" - /* 最小パス和:ブルートフォース探索 */ + /* 最小経路和:全探索 */ int minPathSumDFS(vector> &grid, int i, int j) { - // 左上のセルの場合、探索を終了 + // 左上のセルなら探索を終了する if (i == 0 && j == 0) { return grid[0][0]; } - // 行または列のインデックスが範囲外の場合、+∞ のコストを返す + // 行または列のインデックスが範囲外なら、コスト +∞ を返す if (i < 0 || j < 0) { return INT_MAX; } - // 左上から (i-1, j) と (i, j-1) への最小パスコストを計算 + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する int up = minPathSumDFS(grid, i - 1, j); int left = minPathSumDFS(grid, i, j - 1); - // 左上から (i, j) への最小パスコストを返す - return min(left, up) + grid[i][j]; + // 左上隅から (i, j) までの最小経路コストを返す + return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX; } ``` === "Java" ```java title="min_path_sum.java" - /* 最小パス和:ブルートフォース探索 */ + /* 最小経路和:全探索 */ int minPathSumDFS(int[][] grid, int i, int j) { - // 左上のセルの場合、探索を終了 + // 左上のセルなら探索を終了する if (i == 0 && j == 0) { return grid[0][0]; } - // 行または列のインデックスが範囲外の場合、+∞ のコストを返す + // 行または列のインデックスが範囲外なら、コスト +∞ を返す if (i < 0 || j < 0) { return Integer.MAX_VALUE; } - // 左上から (i-1, j) と (i, j-1) への最小パスコストを計算 + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する int up = minPathSumDFS(grid, i - 1, j); int left = minPathSumDFS(grid, i, j - 1); - // 左上から (i, j) への最小パスコストを返す + // 左上隅から (i, j) までの最小経路コストを返す return Math.min(left, up) + grid[i][j]; } ``` @@ -175,76 +175,232 @@ $$ === "C#" ```csharp title="min_path_sum.cs" - [class]{min_path_sum}-[func]{MinPathSumDFS} + /* 最小経路和:全探索 */ + int MinPathSumDFS(int[][] grid, int i, int j) { + // 左上のセルなら探索を終了する + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return int.MaxValue; + } + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する + int up = MinPathSumDFS(grid, i - 1, j); + int left = MinPathSumDFS(grid, i, j - 1); + // 左上隅から (i, j) までの最小経路コストを返す + return Math.Min(left, up) + grid[i][j]; + } ``` === "Go" ```go title="min_path_sum.go" - [class]{}-[func]{minPathSumDFS} + /* 最小経路和:全探索 */ + func minPathSumDFS(grid [][]int, i, j int) int { + // 左上のセルなら探索を終了する + if i == 0 && j == 0 { + return grid[0][0] + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if i < 0 || j < 0 { + return math.MaxInt + } + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する + up := minPathSumDFS(grid, i-1, j) + left := minPathSumDFS(grid, i, j-1) + // 左上隅から (i, j) までの最小経路コストを返す + return int(math.Min(float64(left), float64(up))) + grid[i][j] + } ``` === "Swift" ```swift title="min_path_sum.swift" - [class]{}-[func]{minPathSumDFS} + /* 最小経路和:全探索 */ + func minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int { + // 左上のセルなら探索を終了する + if i == 0, j == 0 { + return grid[0][0] + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if i < 0 || j < 0 { + return .max + } + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する + let up = minPathSumDFS(grid: grid, i: i - 1, j: j) + let left = minPathSumDFS(grid: grid, i: i, j: j - 1) + // 左上隅から (i, j) までの最小経路コストを返す + return min(left, up) + grid[i][j] + } ``` === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDFS} + /* 最小経路和:全探索 */ + function minPathSumDFS(grid, i, j) { + // 左上のセルなら探索を終了する + if (i === 0 && j === 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return Infinity; + } + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する + const up = minPathSumDFS(grid, i - 1, j); + const left = minPathSumDFS(grid, i, j - 1); + // 左上隅から (i, j) までの最小経路コストを返す + return Math.min(left, up) + grid[i][j]; + } ``` === "TS" ```typescript title="min_path_sum.ts" - [class]{}-[func]{minPathSumDFS} + /* 最小経路和:全探索 */ + function minPathSumDFS( + grid: Array>, + i: number, + j: number + ): number { + // 左上のセルなら探索を終了する + if (i === 0 && j == 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return Infinity; + } + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する + const up = minPathSumDFS(grid, i - 1, j); + const left = minPathSumDFS(grid, i, j - 1); + // 左上隅から (i, j) までの最小経路コストを返す + return Math.min(left, up) + grid[i][j]; + } ``` === "Dart" ```dart title="min_path_sum.dart" - [class]{}-[func]{minPathSumDFS} + /* 最小経路和:全探索 */ + int minPathSumDFS(List> grid, int i, int j) { + // 左上のセルなら探索を終了する + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + // Dart では、int 型は固定範囲の整数であり、「無限大」を表す値は存在しない + return BigInt.from(2).pow(31).toInt(); + } + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する + int up = minPathSumDFS(grid, i - 1, j); + int left = minPathSumDFS(grid, i, j - 1); + // 左上隅から (i, j) までの最小経路コストを返す + return min(left, up) + grid[i][j]; + } ``` === "Rust" ```rust title="min_path_sum.rs" - [class]{}-[func]{min_path_sum_dfs} + /* 最小経路和:全探索 */ + fn min_path_sum_dfs(grid: &Vec>, i: i32, j: i32) -> i32 { + // 左上のセルなら探索を終了する + if i == 0 && j == 0 { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if i < 0 || j < 0 { + return i32::MAX; + } + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する + let up = min_path_sum_dfs(grid, i - 1, j); + let left = min_path_sum_dfs(grid, i, j - 1); + // 左上隅から (i, j) までの最小経路コストを返す + std::cmp::min(left, up) + grid[i as usize][j as usize] + } ``` === "C" ```c title="min_path_sum.c" - [class]{}-[func]{minPathSumDFS} + /* 最小経路和:全探索 */ + int minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) { + // 左上のセルなら探索を終了する + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return INT_MAX; + } + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する + int up = minPathSumDFS(grid, i - 1, j); + int left = minPathSumDFS(grid, i, j - 1); + // 左上隅から (i, j) までの最小経路コストを返す + return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX; + } ``` === "Kotlin" ```kotlin title="min_path_sum.kt" - [class]{}-[func]{minPathSumDFS} + /* 最小経路和:全探索 */ + fun minPathSumDFS(grid: Array, i: Int, j: Int): Int { + // 左上のセルなら探索を終了する + if (i == 0 && j == 0) { + return grid[0][0] + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return Int.MAX_VALUE + } + // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する + val up = minPathSumDFS(grid, i - 1, j) + val left = minPathSumDFS(grid, i, j - 1) + // 左上隅から (i, j) までの最小経路コストを返す + return min(left, up) + grid[i][j] + } ``` === "Ruby" ```ruby title="min_path_sum.rb" - [class]{}-[func]{min_path_sum_dfs} + ### 最小経路和:全探索 ### + def min_path_sum_dfs(grid, i, j) + # 左上のセルなら探索を終了する + return grid[i][j] if i == 0 && j == 0 + # 行または列のインデックスが範囲外なら、コスト +∞ を返す + return Float::INFINITY if i < 0 || j < 0 + # 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する + up = min_path_sum_dfs(grid, i - 1, j) + left = min_path_sum_dfs(grid, i, j - 1) + # 左上隅から (i, j) までの最小経路コストを返す + [left, up].min + grid[i][j] + end ``` -下の図は $dp[2, 1]$ を根とする再帰木を示しており、いくつかの重複する部分問題を含み、その数はグリッド `grid` のサイズが増加すると急激に増加します。 +??? pythontutor "コードの可視化" -本質的に、重複する部分問題の理由は:**左上隅から特定のセルに到達する複数のパスが存在する**ことです。 +
+ + +次の図は、$dp[2, 1]$ を根ノードとする再帰木を示しています。この中にはいくつかの重複部分問題が含まれており、その数はグリッド `grid` のサイズが大きくなるにつれて急激に増加します。 + +本質的に、重複部分問題が生じる理由は、**左上からあるセルへ到達する経路が複数存在すること**にあります。 ![力任せ探索の再帰木](dp_solution_pipeline.assets/min_path_sum_dfs.png){ class="animation-figure" }

図 14-14   力任せ探索の再帰木

-各状態には下と右の2つの選択があるため、左上隅から右下隅までの総ステップ数は $m + n - 2$ で、最悪時間計算量は $O(2^{m + n})$ です。この計算方法はグリッドエッジ近くの状況を考慮していないことに注意してください。ネットワークエッジに到達したとき、選択肢が1つしか残らないため、実際のパス数はより少なくなります。 +各状態には下と右の 2 通りの選択肢があり、左上から右下まで進むには合計で $m + n - 2$ 歩必要です。したがって最悪時間計算量は $O(2^{m + n})$ です。ここで、$n$ と $m$ はそれぞれグリッドの行数と列数を表します。なお、この見積もりではグリッド境界付近の状況を考慮していません。境界に達すると選択肢は 1 つだけになるため、実際の経路数はこれより少なくなります。 -### 2.   方法2:メモ化探索 +### 2.   方法 2:メモ化探索 -グリッド `grid` と同じサイズのメモリスト `mem` を導入し、様々な部分問題の解を記録し、重複する部分問題を枝刈りします: +グリッド `grid` と同じサイズのメモ配列 `mem` を導入し、各部分問題の解を記録して、重複部分問題を枝刈りします。 === "Python" @@ -252,20 +408,20 @@ $$ def min_path_sum_dfs_mem( grid: list[list[int]], mem: list[list[int]], i: int, j: int ) -> int: - """最小パス和:記憶化探索""" - # 左上のセルの場合、探索を終了 + """最小経路和:メモ化探索""" + # 左上のセルなら探索を終了する if i == 0 and j == 0: return grid[0][0] - # 行または列のインデックスが範囲外の場合、+∞ コストを返す + # 行または列のインデックスが範囲外なら、コスト +∞ を返す if i < 0 or j < 0: return inf - # 記録がある場合、それを返す + # 既に記録があればそのまま返す if mem[i][j] != -1: return mem[i][j] - # 左と上のセルからの最小パスコスト + # 左と上のセルからの最小経路コスト up = min_path_sum_dfs_mem(grid, mem, i - 1, j) left = min_path_sum_dfs_mem(grid, mem, i, j - 1) - # 左上から (i, j) への最小パスコストを記録して返す + # 左上から (i, j) までの最小経路コストを記録して返す mem[i][j] = min(left, up) + grid[i][j] return mem[i][j] ``` @@ -273,30 +429,50 @@ $$ === "C++" ```cpp title="min_path_sum.cpp" - [class]{}-[func]{minPathSumDFSMem} + /* 最小経路和:メモ化探索 */ + int minPathSumDFSMem(vector> &grid, vector> &mem, int i, int j) { + // 左上のセルなら探索を終了する + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return INT_MAX; + } + // 既に記録があればそのまま返す + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左と上のセルからの最小経路コスト + int up = minPathSumDFSMem(grid, mem, i - 1, j); + int left = minPathSumDFSMem(grid, mem, i, j - 1); + // 左上から (i, j) までの最小経路コストを記録して返す + mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX; + return mem[i][j]; + } ``` === "Java" ```java title="min_path_sum.java" - /* 最小パス和:メモ化探索 */ + /* 最小経路和:メモ化探索 */ int minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) { - // 左上のセルの場合、探索を終了 + // 左上のセルなら探索を終了する if (i == 0 && j == 0) { return grid[0][0]; } - // 行または列のインデックスが範囲外の場合、+∞ のコストを返す + // 行または列のインデックスが範囲外なら、コスト +∞ を返す if (i < 0 || j < 0) { return Integer.MAX_VALUE; } - // 記録がある場合、それを返す + // 既に記録があればそのまま返す if (mem[i][j] != -1) { return mem[i][j]; } - // 左と上のセルからの最小パスコスト + // 左と上のセルからの最小経路コスト int up = minPathSumDFSMem(grid, mem, i - 1, j); int left = minPathSumDFSMem(grid, mem, i, j - 1); - // 左上から (i, j) への最小パスコストを記録して返す + // 左上から (i, j) までの最小経路コストを記録して返す mem[i][j] = Math.min(left, up) + grid[i][j]; return mem[i][j]; } @@ -305,89 +481,298 @@ $$ === "C#" ```csharp title="min_path_sum.cs" - [class]{min_path_sum}-[func]{MinPathSumDFSMem} + /* 最小経路和:メモ化探索 */ + int MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) { + // 左上のセルなら探索を終了する + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return int.MaxValue; + } + // 既に記録があればそのまま返す + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左と上のセルからの最小経路コスト + int up = MinPathSumDFSMem(grid, mem, i - 1, j); + int left = MinPathSumDFSMem(grid, mem, i, j - 1); + // 左上から (i, j) までの最小経路コストを記録して返す + mem[i][j] = Math.Min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "Go" ```go title="min_path_sum.go" - [class]{}-[func]{minPathSumDFSMem} + /* 最小経路和:メモ化探索 */ + func minPathSumDFSMem(grid, mem [][]int, i, j int) int { + // 左上のセルなら探索を終了する + if i == 0 && j == 0 { + return grid[0][0] + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if i < 0 || j < 0 { + return math.MaxInt + } + // 既に記録があればそのまま返す + if mem[i][j] != -1 { + return mem[i][j] + } + // 左と上のセルからの最小経路コスト + up := minPathSumDFSMem(grid, mem, i-1, j) + left := minPathSumDFSMem(grid, mem, i, j-1) + // 左上から (i, j) までの最小経路コストを記録して返す + mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j] + return mem[i][j] + } ``` === "Swift" ```swift title="min_path_sum.swift" - [class]{}-[func]{minPathSumDFSMem} + /* 最小経路和:メモ化探索 */ + func minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int { + // 左上のセルなら探索を終了する + if i == 0, j == 0 { + return grid[0][0] + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if i < 0 || j < 0 { + return .max + } + // 既に記録があればそのまま返す + if mem[i][j] != -1 { + return mem[i][j] + } + // 左と上のセルからの最小経路コスト + let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j) + let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1) + // 左上から (i, j) までの最小経路コストを記録して返す + mem[i][j] = min(left, up) + grid[i][j] + return mem[i][j] + } ``` === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDFSMem} + /* 最小経路和:メモ化探索 */ + function minPathSumDFSMem(grid, mem, i, j) { + // 左上のセルなら探索を終了する + if (i === 0 && j === 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return Infinity; + } + // 既に記録があればそのまま返す + if (mem[i][j] !== -1) { + return mem[i][j]; + } + // 左と上のセルからの最小経路コスト + const up = minPathSumDFSMem(grid, mem, i - 1, j); + const left = minPathSumDFSMem(grid, mem, i, j - 1); + // 左上から (i, j) までの最小経路コストを記録して返す + mem[i][j] = Math.min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "TS" ```typescript title="min_path_sum.ts" - [class]{}-[func]{minPathSumDFSMem} + /* 最小経路和:メモ化探索 */ + function minPathSumDFSMem( + grid: Array>, + mem: Array>, + i: number, + j: number + ): number { + // 左上のセルなら探索を終了する + if (i === 0 && j === 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return Infinity; + } + // 既に記録があればそのまま返す + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左と上のセルからの最小経路コスト + const up = minPathSumDFSMem(grid, mem, i - 1, j); + const left = minPathSumDFSMem(grid, mem, i, j - 1); + // 左上から (i, j) までの最小経路コストを記録して返す + mem[i][j] = Math.min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "Dart" ```dart title="min_path_sum.dart" - [class]{}-[func]{minPathSumDFSMem} + /* 最小経路和:メモ化探索 */ + int minPathSumDFSMem(List> grid, List> mem, int i, int j) { + // 左上のセルなら探索を終了する + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + // Dart では、int 型は固定範囲の整数であり、「無限大」を表す値は存在しない + return BigInt.from(2).pow(31).toInt(); + } + // 既に記録があればそのまま返す + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左と上のセルからの最小経路コスト + int up = minPathSumDFSMem(grid, mem, i - 1, j); + int left = minPathSumDFSMem(grid, mem, i, j - 1); + // 左上から (i, j) までの最小経路コストを記録して返す + mem[i][j] = min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "Rust" ```rust title="min_path_sum.rs" - [class]{}-[func]{min_path_sum_dfs_mem} + /* 最小経路和:メモ化探索 */ + fn min_path_sum_dfs_mem(grid: &Vec>, mem: &mut Vec>, i: i32, j: i32) -> i32 { + // 左上のセルなら探索を終了する + if i == 0 && j == 0 { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if i < 0 || j < 0 { + return i32::MAX; + } + // 既に記録があればそのまま返す + if mem[i as usize][j as usize] != -1 { + return mem[i as usize][j as usize]; + } + // 左と上のセルからの最小経路コスト + let up = min_path_sum_dfs_mem(grid, mem, i - 1, j); + let left = min_path_sum_dfs_mem(grid, mem, i, j - 1); + // 左上から (i, j) までの最小経路コストを記録して返す + mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize]; + mem[i as usize][j as usize] + } ``` === "C" ```c title="min_path_sum.c" - [class]{}-[func]{minPathSumDFSMem} + /* 最小経路和:メモ化探索 */ + int minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) { + // 左上のセルなら探索を終了する + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return INT_MAX; + } + // 既に記録があればそのまま返す + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左と上のセルからの最小経路コスト + int up = minPathSumDFSMem(grid, mem, i - 1, j); + int left = minPathSumDFSMem(grid, mem, i, j - 1); + // 左上から (i, j) までの最小経路コストを記録して返す + mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX; + return mem[i][j]; + } ``` === "Kotlin" ```kotlin title="min_path_sum.kt" - [class]{}-[func]{minPathSumDFSMem} + /* 最小経路和:メモ化探索 */ + fun minPathSumDFSMem( + grid: Array, + mem: Array, + i: Int, + j: Int + ): Int { + // 左上のセルなら探索を終了する + if (i == 0 && j == 0) { + return grid[0][0] + } + // 行または列のインデックスが範囲外なら、コスト +∞ を返す + if (i < 0 || j < 0) { + return Int.MAX_VALUE + } + // 既に記録があればそのまま返す + if (mem[i][j] != -1) { + return mem[i][j] + } + // 左と上のセルからの最小経路コスト + val up = minPathSumDFSMem(grid, mem, i - 1, j) + val left = minPathSumDFSMem(grid, mem, i, j - 1) + // 左上から (i, j) までの最小経路コストを記録して返す + mem[i][j] = min(left, up) + grid[i][j] + return mem[i][j] + } ``` === "Ruby" ```ruby title="min_path_sum.rb" - [class]{}-[func]{min_path_sum_dfs_mem} + ### 最小経路和:メモ化探索 ### + def min_path_sum_dfs_mem(grid, mem, i, j) + # 左上のセルなら探索を終了する + return grid[0][0] if i == 0 && j == 0 + # 行または列のインデックスが範囲外なら、コスト +∞ を返す + return Float::INFINITY if i < 0 || j < 0 + # 既に記録があればそのまま返す + return mem[i][j] if mem[i][j] != -1 + # 左と上のセルからの最小経路コスト + up = min_path_sum_dfs_mem(grid, mem, i - 1, j) + left = min_path_sum_dfs_mem(grid, mem, i, j - 1) + # 左上から (i, j) までの最小経路コストを記録して返す + mem[i][j] = [left, up].min + grid[i][j] + end ``` -下の図に示すように、メモ化を導入した後、すべての部分問題の解は一度だけ計算される必要があるため、時間計算量は状態の総数、つまりグリッドサイズ $O(nm)$ に依存します。 +??? pythontutor "コードの可視化" + +
+ + +次の図に示すように、メモ化を導入すると、すべての部分問題の解は 1 回だけ計算すればよくなります。したがって時間計算量は状態総数、すなわちグリッドサイズの $O(nm)$ に依存します。 ![メモ化探索の再帰木](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png){ class="animation-figure" }

図 14-15   メモ化探索の再帰木

-### 3.   方法3:動的プログラミング +### 3.   方法 3:動的計画法 -動的プログラミング解法を反復的に実装します。コードは以下の通りです: +反復に基づいて動的計画法の解法を実装すると、コードは次のようになります。 === "Python" ```python title="min_path_sum.py" def min_path_sum_dp(grid: list[list[int]]) -> int: - """最小パス和:動的プログラミング""" + """最小経路和:動的計画法""" n, m = len(grid), len(grid[0]) # dp テーブルを初期化 dp = [[0] * m for _ in range(n)] dp[0][0] = grid[0][0] - # 状態遷移:最初の行 + # 状態遷移:先頭行 for j in range(1, m): dp[0][j] = dp[0][j - 1] + grid[0][j] - # 状態遷移:最初の列 + # 状態遷移:先頭列 for i in range(1, n): dp[i][0] = dp[i - 1][0] + grid[i][0] - # 状態遷移:残りの行と列 + # 状態遷移: 残りの行と列 for i in range(1, n): for j in range(1, m): dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j] @@ -397,21 +782,21 @@ $$ === "C++" ```cpp title="min_path_sum.cpp" - /* 最小パス和:動的プログラミング */ + /* 最小経路和:動的計画法 */ int minPathSumDP(vector> &grid) { int n = grid.size(), m = grid[0].size(); - // DPテーブルを初期化 + // dp テーブルを初期化 vector> dp(n, vector(m)); dp[0][0] = grid[0][0]; - // 状態遷移:最初の行 + // 状態遷移:先頭行 for (int j = 1; j < m; j++) { dp[0][j] = dp[0][j - 1] + grid[0][j]; } - // 状態遷移:最初の列 + // 状態遷移:先頭列 for (int i = 1; i < n; i++) { dp[i][0] = dp[i - 1][0] + grid[i][0]; } - // 状態遷移:残りの行と列 + // 状態遷移: 残りの行と列 for (int i = 1; i < n; i++) { for (int j = 1; j < m; j++) { dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; @@ -424,21 +809,21 @@ $$ === "Java" ```java title="min_path_sum.java" - /* 最小パス和:動的プログラミング */ + /* 最小経路和:動的計画法 */ int minPathSumDP(int[][] grid) { int n = grid.length, m = grid[0].length; - // DPテーブルを初期化 + // dp テーブルを初期化 int[][] dp = new int[n][m]; dp[0][0] = grid[0][0]; - // 状態遷移:最初の行 + // 状態遷移:先頭行 for (int j = 1; j < m; j++) { dp[0][j] = dp[0][j - 1] + grid[0][j]; } - // 状態遷移:最初の列 + // 状態遷移:先頭列 for (int i = 1; i < n; i++) { dp[i][0] = dp[i - 1][0] + grid[i][0]; } - // 状態遷移:残りの行と列 + // 状態遷移: 残りの行と列 for (int i = 1; i < n; i++) { for (int j = 1; j < m; j++) { dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; @@ -451,69 +836,298 @@ $$ === "C#" ```csharp title="min_path_sum.cs" - [class]{min_path_sum}-[func]{MinPathSumDP} + /* 最小経路和:動的計画法 */ + int MinPathSumDP(int[][] grid) { + int n = grid.Length, m = grid[0].Length; + // dp テーブルを初期化 + int[,] dp = new int[n, m]; + dp[0, 0] = grid[0][0]; + // 状態遷移:先頭行 + for (int j = 1; j < m; j++) { + dp[0, j] = dp[0, j - 1] + grid[0][j]; + } + // 状態遷移:先頭列 + for (int i = 1; i < n; i++) { + dp[i, 0] = dp[i - 1, 0] + grid[i][0]; + } + // 状態遷移: 残りの行と列 + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j]; + } + } + return dp[n - 1, m - 1]; + } ``` === "Go" ```go title="min_path_sum.go" - [class]{}-[func]{minPathSumDP} + /* 最小経路和:動的計画法 */ + func minPathSumDP(grid [][]int) int { + n, m := len(grid), len(grid[0]) + // dp テーブルを初期化 + dp := make([][]int, n) + for i := 0; i < n; i++ { + dp[i] = make([]int, m) + } + dp[0][0] = grid[0][0] + // 状態遷移:先頭行 + for j := 1; j < m; j++ { + dp[0][j] = dp[0][j-1] + grid[0][j] + } + // 状態遷移:先頭列 + for i := 1; i < n; i++ { + dp[i][0] = dp[i-1][0] + grid[i][0] + } + // 状態遷移: 残りの行と列 + for i := 1; i < n; i++ { + for j := 1; j < m; j++ { + dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j] + } + } + return dp[n-1][m-1] + } ``` === "Swift" ```swift title="min_path_sum.swift" - [class]{}-[func]{minPathSumDP} + /* 最小経路和:動的計画法 */ + func minPathSumDP(grid: [[Int]]) -> Int { + let n = grid.count + let m = grid[0].count + // dp テーブルを初期化 + var dp = Array(repeating: Array(repeating: 0, count: m), count: n) + dp[0][0] = grid[0][0] + // 状態遷移:先頭行 + for j in 1 ..< m { + dp[0][j] = dp[0][j - 1] + grid[0][j] + } + // 状態遷移:先頭列 + for i in 1 ..< n { + dp[i][0] = dp[i - 1][0] + grid[i][0] + } + // 状態遷移: 残りの行と列 + for i in 1 ..< n { + for j in 1 ..< m { + dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j] + } + } + return dp[n - 1][m - 1] + } ``` === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDP} + /* 最小経路和:動的計画法 */ + function minPathSumDP(grid) { + const n = grid.length, + m = grid[0].length; + // dp テーブルを初期化 + const dp = Array.from({ length: n }, () => + Array.from({ length: m }, () => 0) + ); + dp[0][0] = grid[0][0]; + // 状態遷移:先頭行 + for (let j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 状態遷移:先頭列 + for (let i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 状態遷移: 残りの行と列 + for (let i = 1; i < n; i++) { + for (let j = 1; j < m; j++) { + dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } ``` === "TS" ```typescript title="min_path_sum.ts" - [class]{}-[func]{minPathSumDP} + /* 最小経路和:動的計画法 */ + function minPathSumDP(grid: Array>): number { + const n = grid.length, + m = grid[0].length; + // dp テーブルを初期化 + const dp = Array.from({ length: n }, () => + Array.from({ length: m }, () => 0) + ); + dp[0][0] = grid[0][0]; + // 状態遷移:先頭行 + for (let j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 状態遷移:先頭列 + for (let i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 状態遷移: 残りの行と列 + for (let i = 1; i < n; i++) { + for (let j: number = 1; j < m; j++) { + dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } ``` === "Dart" ```dart title="min_path_sum.dart" - [class]{}-[func]{minPathSumDP} + /* 最小経路和:動的計画法 */ + int minPathSumDP(List> grid) { + int n = grid.length, m = grid[0].length; + // dp テーブルを初期化 + List> dp = List.generate(n, (i) => List.filled(m, 0)); + dp[0][0] = grid[0][0]; + // 状態遷移:先頭行 + for (int j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 状態遷移:先頭列 + for (int i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 状態遷移: 残りの行と列 + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } ``` === "Rust" ```rust title="min_path_sum.rs" - [class]{}-[func]{min_path_sum_dp} + /* 最小経路和:動的計画法 */ + fn min_path_sum_dp(grid: &Vec>) -> i32 { + let (n, m) = (grid.len(), grid[0].len()); + // dp テーブルを初期化 + let mut dp = vec![vec![0; m]; n]; + dp[0][0] = grid[0][0]; + // 状態遷移:先頭行 + for j in 1..m { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 状態遷移:先頭列 + for i in 1..n { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 状態遷移: 残りの行と列 + for i in 1..n { + for j in 1..m { + dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + dp[n - 1][m - 1] + } ``` === "C" ```c title="min_path_sum.c" - [class]{}-[func]{minPathSumDP} + /* 最小経路和:動的計画法 */ + int minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) { + // dp テーブルを初期化 + int **dp = malloc(n * sizeof(int *)); + for (int i = 0; i < n; i++) { + dp[i] = calloc(m, sizeof(int)); + } + dp[0][0] = grid[0][0]; + // 状態遷移:先頭行 + for (int j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 状態遷移:先頭列 + for (int i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 状態遷移: 残りの行と列 + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + int res = dp[n - 1][m - 1]; + // メモリを解放する + for (int i = 0; i < n; i++) { + free(dp[i]); + } + return res; + } ``` === "Kotlin" ```kotlin title="min_path_sum.kt" - [class]{}-[func]{minPathSumDP} + /* 最小経路和:動的計画法 */ + fun minPathSumDP(grid: Array): Int { + val n = grid.size + val m = grid[0].size + // dp テーブルを初期化 + val dp = Array(n) { IntArray(m) } + dp[0][0] = grid[0][0] + // 状態遷移:先頭行 + for (j in 1.. + + +次の図は最小経路和の状態遷移の過程を示しています。グリッド全体を走査するため、**時間計算量は $O(nm)$** です。 配列 `dp` のサイズは $n \times m$ であるため、**空間計算量は $O(nm)$** です。 === "<1>" - ![最小経路和の動的プログラミングプロセス](dp_solution_pipeline.assets/min_path_sum_dp_step1.png){ class="animation-figure" } + ![最小経路和の動的計画法の過程](dp_solution_pipeline.assets/min_path_sum_dp_step1.png){ class="animation-figure" } === "<2>" ![min_path_sum_dp_step2](dp_solution_pipeline.assets/min_path_sum_dp_step2.png){ class="animation-figure" } @@ -548,29 +1162,29 @@ $$ === "<12>" ![min_path_sum_dp_step12](dp_solution_pipeline.assets/min_path_sum_dp_step12.png){ class="animation-figure" } -

図 14-16   最小経路和の動的プログラミングプロセス

+

図 14-16   最小経路和の動的計画法の過程

### 4.   空間最適化 -各セルは左と上のセルのみに関連するため、単一行配列を使用して $dp$ テーブルを実装できます。 +各マスは左のマスと上のマスにのみ関係するため、1 行の配列だけを使って $dp$ テーブルを実装できます。 -配列 `dp` は1行の状態のみを表現できるため、最初の列の状態を事前に初期化できず、各行を走査するときに更新することに注意してください: +ただし、配列 `dp` は 1 行分の状態しか表せないため、先頭列の状態を事前に初期化することはできず、各行を走査するときに更新する必要があります。 === "Python" ```python title="min_path_sum.py" def min_path_sum_dp_comp(grid: list[list[int]]) -> int: - """最小パス和:空間最適化動的プログラミング""" + """最小経路和:空間最適化後の動的計画法""" n, m = len(grid), len(grid[0]) # dp テーブルを初期化 dp = [0] * m - # 状態遷移:最初の行 + # 状態遷移:先頭行 dp[0] = grid[0][0] for j in range(1, m): dp[j] = dp[j - 1] + grid[0][j] # 状態遷移:残りの行 for i in range(1, n): - # 状態遷移:最初の列 + # 状態遷移:先頭列 dp[0] = dp[0] + grid[i][0] # 状態遷移:残りの列 for j in range(1, m): @@ -581,25 +1195,45 @@ $$ === "C++" ```cpp title="min_path_sum.cpp" - [class]{}-[func]{minPathSumDPComp} - ``` - -=== "Java" - - ```java title="min_path_sum.java" - /* 最小パス和:空間最適化動的プログラミング */ - int minPathSumDPComp(int[][] grid) { - int n = grid.length, m = grid[0].length; - // DPテーブルを初期化 - int[] dp = new int[m]; - // 状態遷移:最初の行 + /* 最小経路和:空間最適化後の動的計画法 */ + int minPathSumDPComp(vector> &grid) { + int n = grid.size(), m = grid[0].size(); + // dp テーブルを初期化 + vector dp(m); + // 状態遷移:先頭行 dp[0] = grid[0][0]; for (int j = 1; j < m; j++) { dp[j] = dp[j - 1] + grid[0][j]; } // 状態遷移:残りの行 for (int i = 1; i < n; i++) { - // 状態遷移:最初の列 + // 状態遷移:先頭列 + dp[0] = dp[0] + grid[i][0]; + // 状態遷移:残りの列 + for (int j = 1; j < m; j++) { + dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } + ``` + +=== "Java" + + ```java title="min_path_sum.java" + /* 最小経路和:空間最適化後の動的計画法 */ + int minPathSumDPComp(int[][] grid) { + int n = grid.length, m = grid[0].length; + // dp テーブルを初期化 + int[] dp = new int[m]; + // 状態遷移:先頭行 + dp[0] = grid[0][0]; + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状態遷移:残りの行 + for (int i = 1; i < n; i++) { + // 状態遷移:先頭列 dp[0] = dp[0] + grid[i][0]; // 状態遷移:残りの列 for (int j = 1; j < m; j++) { @@ -613,59 +1247,265 @@ $$ === "C#" ```csharp title="min_path_sum.cs" - [class]{min_path_sum}-[func]{MinPathSumDPComp} + /* 最小経路和:空間最適化後の動的計画法 */ + int MinPathSumDPComp(int[][] grid) { + int n = grid.Length, m = grid[0].Length; + // dp テーブルを初期化 + int[] dp = new int[m]; + dp[0] = grid[0][0]; + // 状態遷移:先頭行 + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状態遷移:残りの行 + for (int i = 1; i < n; i++) { + // 状態遷移:先頭列 + dp[0] = dp[0] + grid[i][0]; + // 状態遷移:残りの列 + for (int j = 1; j < m; j++) { + dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } ``` === "Go" ```go title="min_path_sum.go" - [class]{}-[func]{minPathSumDPComp} + /* 最小経路和:空間最適化後の動的計画法 */ + func minPathSumDPComp(grid [][]int) int { + n, m := len(grid), len(grid[0]) + // dp テーブルを初期化 + dp := make([]int, m) + // 状態遷移:先頭行 + dp[0] = grid[0][0] + for j := 1; j < m; j++ { + dp[j] = dp[j-1] + grid[0][j] + } + // 状態遷移: 残りの行と列 + for i := 1; i < n; i++ { + // 状態遷移:先頭列 + dp[0] = dp[0] + grid[i][0] + // 状態遷移:残りの列 + for j := 1; j < m; j++ { + dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j] + } + } + return dp[m-1] + } ``` === "Swift" ```swift title="min_path_sum.swift" - [class]{}-[func]{minPathSumDPComp} + /* 最小経路和:空間最適化後の動的計画法 */ + func minPathSumDPComp(grid: [[Int]]) -> Int { + let n = grid.count + let m = grid[0].count + // dp テーブルを初期化 + var dp = Array(repeating: 0, count: m) + // 状態遷移:先頭行 + dp[0] = grid[0][0] + for j in 1 ..< m { + dp[j] = dp[j - 1] + grid[0][j] + } + // 状態遷移:残りの行 + for i in 1 ..< n { + // 状態遷移:先頭列 + dp[0] = dp[0] + grid[i][0] + // 状態遷移:残りの列 + for j in 1 ..< m { + dp[j] = min(dp[j - 1], dp[j]) + grid[i][j] + } + } + return dp[m - 1] + } ``` === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDPComp} + /* 最小経路和:空間最適化後の動的計画法 */ + function minPathSumDPComp(grid) { + const n = grid.length, + m = grid[0].length; + // dp テーブルを初期化 + const dp = new Array(m); + // 状態遷移:先頭行 + dp[0] = grid[0][0]; + for (let j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状態遷移:残りの行 + for (let i = 1; i < n; i++) { + // 状態遷移:先頭列 + dp[0] = dp[0] + grid[i][0]; + // 状態遷移:残りの列 + for (let j = 1; j < m; j++) { + dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } ``` === "TS" ```typescript title="min_path_sum.ts" - [class]{}-[func]{minPathSumDPComp} + /* 最小経路和:空間最適化後の動的計画法 */ + function minPathSumDPComp(grid: Array>): number { + const n = grid.length, + m = grid[0].length; + // dp テーブルを初期化 + const dp = new Array(m); + // 状態遷移:先頭行 + dp[0] = grid[0][0]; + for (let j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状態遷移:残りの行 + for (let i = 1; i < n; i++) { + // 状態遷移:先頭列 + dp[0] = dp[0] + grid[i][0]; + // 状態遷移:残りの列 + for (let j = 1; j < m; j++) { + dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } ``` === "Dart" ```dart title="min_path_sum.dart" - [class]{}-[func]{minPathSumDPComp} + /* 最小経路和:空間最適化後の動的計画法 */ + int minPathSumDPComp(List> grid) { + int n = grid.length, m = grid[0].length; + // dp テーブルを初期化 + List dp = List.filled(m, 0); + dp[0] = grid[0][0]; + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状態遷移:残りの行 + for (int i = 1; i < n; i++) { + // 状態遷移:先頭列 + dp[0] = dp[0] + grid[i][0]; + // 状態遷移:残りの列 + for (int j = 1; j < m; j++) { + dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } ``` === "Rust" ```rust title="min_path_sum.rs" - [class]{}-[func]{min_path_sum_dp_comp} + /* 最小経路和:空間最適化後の動的計画法 */ + fn min_path_sum_dp_comp(grid: &Vec>) -> i32 { + let (n, m) = (grid.len(), grid[0].len()); + // dp テーブルを初期化 + let mut dp = vec![0; m]; + // 状態遷移:先頭行 + dp[0] = grid[0][0]; + for j in 1..m { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状態遷移:残りの行 + for i in 1..n { + // 状態遷移:先頭列 + dp[0] = dp[0] + grid[i][0]; + // 状態遷移:残りの列 + for j in 1..m { + dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + dp[m - 1] + } ``` === "C" ```c title="min_path_sum.c" - [class]{}-[func]{minPathSumDPComp} + /* 最小経路和:空間最適化後の動的計画法 */ + int minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) { + // dp テーブルを初期化 + int *dp = calloc(m, sizeof(int)); + // 状態遷移:先頭行 + dp[0] = grid[0][0]; + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状態遷移:残りの行 + for (int i = 1; i < n; i++) { + // 状態遷移:先頭列 + dp[0] = dp[0] + grid[i][0]; + // 状態遷移:残りの列 + for (int j = 1; j < m; j++) { + dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j]; + } + } + int res = dp[m - 1]; + // メモリを解放する + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="min_path_sum.kt" - [class]{}-[func]{minPathSumDPComp} + /* 最小経路和:空間最適化後の動的計画法 */ + fun minPathSumDPComp(grid: Array): Int { + val n = grid.size + val m = grid[0].size + // dp テーブルを初期化 + val dp = IntArray(m) + // 状態遷移:先頭行 + dp[0] = grid[0][0] + for (j in 1.. + diff --git a/ja/docs/chapter_dynamic_programming/edit_distance_problem.md b/ja/docs/chapter_dynamic_programming/edit_distance_problem.md index a19e17c67..9d8d2a100 100644 --- a/ja/docs/chapter_dynamic_programming/edit_distance_problem.md +++ b/ja/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -4,99 +4,99 @@ comments: true # 14.6   編集距離問題 -編集距離は、レーベンシュタイン距離とも呼ばれ、一つの文字列を別の文字列に変換するために必要な最小修正回数を指し、情報検索や自然言語処理で2つのシーケンス間の類似度を測定するためによく使用されます。 +編集距離は、Levenshtein 距離とも呼ばれ、2つの文字列の相互変換に必要な最小の編集回数を指し、通常は情報検索や自然言語処理において2つの系列の類似度を測るために用いられます。 !!! question - 2つの文字列 $s$ と $t$ が与えられたとき、$s$ を $t$ に変換するために必要な最小編集回数を返してください。 + 2つの文字列 $s$ と $t$ を入力し、$s$ を $t$ に変換するのに必要な最小編集回数を返してください。 + + 1つの文字列に対して3種類の編集操作を行えます。1文字の挿入、1文字の削除、任意の文字への置換です。 - 文字列に対して3種類の編集を実行できます:文字の挿入、文字の削除、または文字を他の任意の文字に置換。 +下図に示すように、`kitten` を `sitting` に変換するには 3 回の編集が必要で、内訳は 2 回の置換と 1 回の挿入です。`hello` を `algo` に変換する場合も 3 回必要で、内訳は 2 回の置換と 1 回の削除です。 -下の図に示すように、`kitten` を `sitting` に変換するには3回の編集が必要で、2回の置換と1回の挿入を含みます。`hello` を `algo` に変換するには3ステップが必要で、2回の置換と1回の削除を含みます。 +![編集距離のサンプルデータ](edit_distance_problem.assets/edit_distance_example.png){ class="animation-figure" } -![編集距離の例データ](edit_distance_problem.assets/edit_distance_example.png){ class="animation-figure" } +

図 14-27   編集距離のサンプルデータ

-

図 14-27   編集距離の例データ

+**編集距離問題は決定木モデルで自然に説明できます**。文字列が木のノードに対応し、1回の決定(1回の編集操作)が木の1本の辺に対応します。 -**編集距離問題は決定木モデルで自然に説明できます**。文字列は木のノードに対応し、1ラウンドの決定(編集操作)は木のエッジに対応します。 +下図に示すように、操作に制限がない場合、各ノードからは多くの辺を派生でき、それぞれの辺が1種類の操作に対応します。これは `hello` から `algo` への変換に多くの経路があり得ることを意味します。 -下の図に示すように、操作に制限がない場合、各ノードは多くのエッジを導出でき、それぞれが1つの操作に対応するため、`hello` を `algo` に変換する可能な経路は多数あります。 +決定木の観点から見ると、本問の目標はノード `hello` とノード `algo` の間の最短経路を求めることです。 -決定木の観点から、この問題の目標は、ノード `hello` とノード `algo` の間の最短経路を見つけることです。 +![決定木モデルに基づく編集距離問題の表現](edit_distance_problem.assets/edit_distance_decision_tree.png){ class="animation-figure" } -![決定木モデルに基づいて表現された編集距離問題](edit_distance_problem.assets/edit_distance_decision_tree.png){ class="animation-figure" } +

図 14-28   決定木モデルに基づく編集距離問題の表現

-

図 14-28   決定木モデルに基づいて表現された編集距離問題

+### 1.   動的計画法の考え方 -### 1.   動的プログラミングアプローチ +**第1ステップ:各ラウンドの決定を考え、状態を定義して、$dp$ テーブルを得る** -**ステップ1:各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る** +各ラウンドの決定は、文字列 $s$ に対して1回の編集操作を行うことです。 -各ラウンドの決定は、文字列 $s$ に対して1つの編集操作を実行することを含みます。 +編集操作の過程で問題の規模が徐々に小さくなることを期待します。そうして初めて部分問題を構築できます。文字列 $s$ と $t$ の長さをそれぞれ $n$ と $m$ とし、まず両文字列の末尾の文字 $s[n-1]$ と $t[m-1]$ を考えます。 -編集プロセス中に問題のサイズを段階的に縮小することを目指し、これにより部分問題を構築できます。文字列 $s$ と $t$ の長さをそれぞれ $n$ と $m$ とします。まず、両方の文字列の末尾文字 $s[n-1]$ と $t[m-1]$ を考慮します。 +- $s[n-1]$ と $t[m-1]$ が同じなら、それらをスキップして、直接 $s[n-2]$ と $t[m-2]$ を考えます。 +- $s[n-1]$ と $t[m-1]$ が異なるなら、$s$ に対して1回の編集(挿入、削除、置換)を行い、両文字列の末尾の文字を同じにします。そうすることでそれらをスキップし、より小さい問題を考えられます。 -- $s[n-1]$ と $t[m-1]$ が同じ場合、それらをスキップして直接 $s[n-2]$ と $t[m-2]$ を考慮できます。 -- $s[n-1]$ と $t[m-1]$ が異なる場合、$s$ に対して1つの編集(挿入、削除、置換)を実行して、2つの文字列の末尾文字を一致させ、それらをスキップしてより小規模な問題を考慮できるようにする必要があります。 +つまり、文字列 $s$ に対する各ラウンドの決定(編集操作)は、$s$ と $t$ における残りの未一致文字を変化させます。したがって、状態は現在 $s$ と $t$ で考えている第 $i$ と第 $j$ 文字とし、$[i, j]$ と記します。 -したがって、文字列 $s$ での各ラウンドの決定(編集操作)は、$s$ と $t$ でマッチされる残りの文字を変更します。したがって、状態は $s$ と $t$ で現在考慮されている $i$ 番目と $j$ 番目の文字であり、$[i, j]$ と表記されます。 +状態 $[i, j]$ に対応する部分問題は、**$s$ の先頭 $i$ 文字を $t$ の先頭 $j$ 文字に変換するのに必要な最小編集回数**です。 -状態 $[i, j]$ は部分問題に対応します:**$s$ の最初の $i$ 文字を $t$ の最初の $j$ 文字に変更するために必要な最小編集回数**。 +これにより、サイズが $(i+1) \times (j+1)$ の2次元 $dp$ テーブルが得られます。 -これから、サイズ $(i+1) \times (j+1)$ の二次元 $dp$ テーブルを得ます。 +**第2ステップ:最適部分構造を見つけ、状態遷移方程式を導く** -**ステップ2:最適部分構造を特定し、状態遷移方程式を導出する** +部分問題 $dp[i, j]$ を考えます。これに対応する2つの文字列の末尾文字は $s[i-1]$ と $t[j-1]$ であり、編集操作の違いに応じて下図の3つの場合に分けられます。 -部分問題 $dp[i, j]$ を考慮すると、これに対応する2つの文字列の末尾文字は $s[i-1]$ と $t[j-1]$ であり、下の図に示すように3つのシナリオに分けることができます。 - -1. $s[i-1]$ の後に $t[j-1]$ を追加すると、残りの部分問題は $dp[i, j-1]$ です。 -2. $s[i-1]$ を削除すると、残りの部分問題は $dp[i-1, j]$ です。 -3. $s[i-1]$ を $t[j-1]$ に置換すると、残りの部分問題は $dp[i-1, j-1]$ です。 +1. $s[i-1]$ の後ろに $t[j-1]$ を追加する。このとき残る部分問題は $dp[i, j-1]$ です。 +2. $s[i-1]$ を削除する。このとき残る部分問題は $dp[i-1, j]$ です。 +3. $s[i-1]$ を $t[j-1]$ に置き換える。このとき残る部分問題は $dp[i-1, j-1]$ です。 ![編集距離の状態遷移](edit_distance_problem.assets/edit_distance_state_transfer.png){ class="animation-figure" }

図 14-29   編集距離の状態遷移

-上記の分析に基づいて、最適部分構造を決定できます:$dp[i, j]$ の最小編集回数は、$dp[i, j-1]$、$dp[i-1, j]$、$dp[i-1, j-1]$ の中の最小値に編集ステップ $1$ を加えたものです。対応する状態遷移方程式は: +以上の分析から、最適部分構造は次のように得られます。$dp[i, j]$ の最小編集回数は、$dp[i, j-1]$、$dp[i-1, j]$、$dp[i-1, j-1]$ の3つのうち最小の編集回数に、今回の編集回数 $1$ を加えたものです。対応する状態遷移方程式は次のとおりです: $$ dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 $$ -注意してください。**$s[i-1]$ と $t[j-1]$ が同じ場合、現在の文字に対して編集は必要ありません**。この場合、状態遷移方程式は: +注意すべき点として、**$s[i-1]$ と $t[j-1]$ が同じ場合、現在の文字を編集する必要はありません**。この場合の状態遷移方程式は次のとおりです: $$ dp[i, j] = dp[i-1, j-1] $$ -**ステップ3:境界条件と状態遷移の順序を決定する** +**第3ステップ:境界条件と状態遷移の順序を決める** -両方の文字列が空の場合、編集回数は $0$ です。つまり、$dp[0, 0] = 0$ です。$s$ が空で $t$ が空でない場合、最小編集回数は $t$ の長さに等しく、つまり最初の行 $dp[0, j] = j$ です。$s$ が空でなく $t$ が空の場合、最小編集回数は $s$ の長さに等しく、つまり最初の列 $dp[i, 0] = i$ です。 +2つの文字列がともに空のとき、編集回数は $0$、すなわち $dp[0, 0] = 0$ です。$s$ が空で $t$ が空でないとき、最小編集回数は $t$ の長さに等しいため、先頭行は $dp[0, j] = j$ です。$s$ が空でなく $t$ が空のとき、最小編集回数は $s$ の長さに等しいため、先頭列は $dp[i, 0] = i$ です。 -状態遷移方程式を観察すると、$dp[i, j]$ の解決は左、上、左上の解に依存するため、二重ループを使用して正しい順序で $dp$ テーブル全体を走査できます。 +状態遷移方程式を観察すると、$dp[i, j]$ の解は左、上、左上の解に依存します。そのため、2重ループで $dp$ テーブル全体を順方向に走査すれば十分です。 -### 2.   コード実装 +### 2.   コードの実装 === "Python" ```python title="edit_distance.py" def edit_distance_dp(s: str, t: str) -> int: - """編集距離:動的プログラミング""" + """編集距離:動的計画法""" n, m = len(s), len(t) dp = [[0] * (m + 1) for _ in range(n + 1)] - # 状態遷移:最初の行と最初の列 + # 状態遷移:先頭行と先頭列 for i in range(1, n + 1): dp[i][0] = i for j in range(1, m + 1): dp[0][j] = j - # 状態遷移:残りの行と列 + # 状態遷移: 残りの行と列 for i in range(1, n + 1): for j in range(1, m + 1): if s[i - 1] == t[j - 1]: - # 2 つの文字が等しい場合、これら 2 つの文字をスキップ + # 2 つの文字が等しければ、その 2 文字をそのままスキップする dp[i][j] = dp[i - 1][j - 1] else: - # 最小編集数 = 3 つの操作(挿入、削除、置換)からの最小編集数 + 1 + # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1 return dp[n][m] ``` @@ -104,25 +104,25 @@ $$ === "C++" ```cpp title="edit_distance.cpp" - /* 編集距離:動的プログラミング */ + /* 編集距離:動的計画法 */ int editDistanceDP(string s, string t) { int n = s.length(), m = t.length(); vector> dp(n + 1, vector(m + 1, 0)); - // 状態遷移:最初の行と最初の列 + // 状態遷移:先頭行と先頭列 for (int i = 1; i <= n; i++) { dp[i][0] = i; } for (int j = 1; j <= m; j++) { dp[0][j] = j; } - // 状態遷移:残りの行と列 + // 状態遷移: 残りの行と列 for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (s[i - 1] == t[j - 1]) { - // 2つの文字が等しい場合、これら2つの文字をスキップ + // 2 つの文字が等しければ、その 2 文字をそのままスキップする dp[i][j] = dp[i - 1][j - 1]; } else { - // 最小編集数 = 3つの操作(挿入、削除、置換)からの最小編集数 + 1 + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; } } @@ -134,25 +134,25 @@ $$ === "Java" ```java title="edit_distance.java" - /* 編集距離:動的プログラミング */ + /* 編集距離:動的計画法 */ int editDistanceDP(String s, String t) { int n = s.length(), m = t.length(); int[][] dp = new int[n + 1][m + 1]; - // 状態遷移:最初の行と最初の列 + // 状態遷移:先頭行と先頭列 for (int i = 1; i <= n; i++) { dp[i][0] = i; } for (int j = 1; j <= m; j++) { dp[0][j] = j; } - // 状態遷移:残りの行と列 + // 状態遷移: 残りの行と列 for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (s.charAt(i - 1) == t.charAt(j - 1)) { - // 2つの文字が等しい場合、これら2つの文字をスキップ + // 2 つの文字が等しければ、その 2 文字をそのままスキップする dp[i][j] = dp[i - 1][j - 1]; } else { - // 最小編集数 = 3つの操作(挿入、削除、置換)からの最小編集数 + 1 + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; } } @@ -164,67 +164,328 @@ $$ === "C#" ```csharp title="edit_distance.cs" - [class]{edit_distance}-[func]{EditDistanceDP} + /* 編集距離:動的計画法 */ + int EditDistanceDP(string s, string t) { + int n = s.Length, m = t.Length; + int[,] dp = new int[n + 1, m + 1]; + // 状態遷移:先頭行と先頭列 + for (int i = 1; i <= n; i++) { + dp[i, 0] = i; + } + for (int j = 1; j <= m; j++) { + dp[0, j] = j; + } + // 状態遷移: 残りの行と列 + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + if (s[i - 1] == t[j - 1]) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[i, j] = dp[i - 1, j - 1]; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1; + } + } + } + return dp[n, m]; + } ``` === "Go" ```go title="edit_distance.go" - [class]{}-[func]{editDistanceDP} + /* 編集距離:動的計画法 */ + func editDistanceDP(s string, t string) int { + n := len(s) + m := len(t) + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, m+1) + } + // 状態遷移:先頭行と先頭列 + for i := 1; i <= n; i++ { + dp[i][0] = i + } + for j := 1; j <= m; j++ { + dp[0][j] = j + } + // 状態遷移: 残りの行と列 + for i := 1; i <= n; i++ { + for j := 1; j <= m; j++ { + if s[i-1] == t[j-1] { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[i][j] = dp[i-1][j-1] + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1 + } + } + } + return dp[n][m] + } ``` === "Swift" ```swift title="edit_distance.swift" - [class]{}-[func]{editDistanceDP} + /* 編集距離:動的計画法 */ + func editDistanceDP(s: String, t: String) -> Int { + let n = s.utf8CString.count + let m = t.utf8CString.count + var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1) + // 状態遷移:先頭行と先頭列 + for i in 1 ... n { + dp[i][0] = i + } + for j in 1 ... m { + dp[0][j] = j + } + // 状態遷移: 残りの行と列 + for i in 1 ... n { + for j in 1 ... m { + if s.utf8CString[i - 1] == t.utf8CString[j - 1] { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[i][j] = dp[i - 1][j - 1] + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1 + } + } + } + return dp[n][m] + } ``` === "JS" ```javascript title="edit_distance.js" - [class]{}-[func]{editDistanceDP} + /* 編集距離:動的計画法 */ + function editDistanceDP(s, t) { + const n = s.length, + m = t.length; + const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)); + // 状態遷移:先頭行と先頭列 + for (let i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (let j = 1; j <= m; j++) { + dp[0][j] = j; + } + // 状態遷移: 残りの行と列 + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= m; j++) { + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[i][j] = + Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1; + } + } + } + return dp[n][m]; + } ``` === "TS" ```typescript title="edit_distance.ts" - [class]{}-[func]{editDistanceDP} + /* 編集距離:動的計画法 */ + function editDistanceDP(s: string, t: string): number { + const n = s.length, + m = t.length; + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: m + 1 }, () => 0) + ); + // 状態遷移:先頭行と先頭列 + for (let i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (let j = 1; j <= m; j++) { + dp[0][j] = j; + } + // 状態遷移: 残りの行と列 + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= m; j++) { + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[i][j] = + Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1; + } + } + } + return dp[n][m]; + } ``` === "Dart" ```dart title="edit_distance.dart" - [class]{}-[func]{editDistanceDP} + /* 編集距離:動的計画法 */ + int editDistanceDP(String s, String t) { + int n = s.length, m = t.length; + List> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0)); + // 状態遷移:先頭行と先頭列 + for (int i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (int j = 1; j <= m; j++) { + dp[0][j] = j; + } + // 状態遷移: 残りの行と列 + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + if (s[i - 1] == t[j - 1]) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; + } + } + } + return dp[n][m]; + } ``` === "Rust" ```rust title="edit_distance.rs" - [class]{}-[func]{edit_distance_dp} + /* 編集距離:動的計画法 */ + fn edit_distance_dp(s: &str, t: &str) -> i32 { + let (n, m) = (s.len(), t.len()); + let mut dp = vec![vec![0; m + 1]; n + 1]; + // 状態遷移:先頭行と先頭列 + for i in 1..=n { + dp[i][0] = i as i32; + } + for j in 1..m { + dp[0][j] = j as i32; + } + // 状態遷移: 残りの行と列 + for i in 1..=n { + for j in 1..=m { + if s.chars().nth(i - 1) == t.chars().nth(j - 1) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[i][j] = + std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; + } + } + } + dp[n][m] + } ``` === "C" ```c title="edit_distance.c" - [class]{}-[func]{editDistanceDP} + /* 編集距離:動的計画法 */ + int editDistanceDP(char *s, char *t, int n, int m) { + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(m + 1, sizeof(int)); + } + // 状態遷移:先頭行と先頭列 + for (int i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (int j = 1; j <= m; j++) { + dp[0][j] = j; + } + // 状態遷移: 残りの行と列 + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + if (s[i - 1] == t[j - 1]) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; + } + } + } + int res = dp[n][m]; + // メモリを解放する + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + return res; + } ``` === "Kotlin" ```kotlin title="edit_distance.kt" - [class]{}-[func]{editDistanceDP} + /* 編集距離:動的計画法 */ + fun editDistanceDP(s: String, t: String): Int { + val n = s.length + val m = t.length + val dp = Array(n + 1) { IntArray(m + 1) } + // 状態遷移:先頭行と先頭列 + for (i in 1..n) { + dp[i][0] = i + } + for (j in 1..m) { + dp[0][j] = j + } + // 状態遷移: 残りの行と列 + for (i in 1..n) { + for (j in 1..m) { + if (s[i - 1] == t[j - 1]) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[i][j] = dp[i - 1][j - 1] + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1 + } + } + } + return dp[n][m] + } ``` === "Ruby" ```ruby title="edit_distance.rb" - [class]{}-[func]{edit_distance_dp} + ### 編集距離:動的計画法 ### + def edit_distance_dp(s, t) + n, m = s.length, t.length + dp = Array.new(n + 1) { Array.new(m + 1, 0) } + # 状態遷移:先頭行と先頭列 + (1...(n + 1)).each { |i| dp[i][0] = i } + (1...(m + 1)).each { |j| dp[0][j] = j } + # 状態遷移: 残りの行と列 + for i in 1...(n + 1) + for j in 1...(m +1) + if s[i - 1] == t[j - 1] + # 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[i][j] = dp[i - 1][j - 1] + else + # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1 + end + end + end + dp[n][m] + end ``` -下の図に示すように、編集距離問題の状態遷移プロセスはナップサック問題と非常に似ており、二次元グリッドを埋めることと見なすことができます。 +??? pythontutor "コードの可視化" + +
+ + +下図に示すように、編集距離問題の状態遷移の過程はナップサック問題と非常によく似ており、どちらも2次元グリッドを埋めていく過程とみなせます。 === "<1>" - ![編集距離の動的プログラミングプロセス](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" } + ![編集距離の動的計画法の過程](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" } === "<2>" ![edit_distance_dp_step2](edit_distance_problem.assets/edit_distance_dp_step2.png){ class="animation-figure" } @@ -268,75 +529,102 @@ $$ === "<15>" ![edit_distance_dp_step15](edit_distance_problem.assets/edit_distance_dp_step15.png){ class="animation-figure" } -

図 14-30   編集距離の動的プログラミングプロセス

+

図 14-30   編集距離の動的計画法の過程

### 3.   空間最適化 -$dp[i, j]$ は上の $dp[i-1, j]$、左の $dp[i, j-1]$、左上の $dp[i-1, j-1]$ の解から導出され、直接走査では左上の解 $dp[i-1, j-1]$ が失われ、逆走査では事前に $dp[i, j-1]$ を構築できないため、どちらの走査順序も実行可能ではありません。 +$dp[i,j]$ は上の $dp[i-1, j]$、左の $dp[i, j-1]$、左上の $dp[i-1, j-1]$ から遷移されますが、順方向走査では左上の $dp[i-1, j-1]$ を失い、逆方向走査では $dp[i, j-1]$ を事前に構築できません。そのため、どちらの走査順序も適切ではありません。 -この理由で、変数 `leftup` を使用して左上の $dp[i-1, j-1]$ からの解を一時的に保存し、左と上の解のみを考慮すればよくなります。この状況は無制限ナップサック問題と似ており、直接走査が可能です。コードは以下の通りです: +そのため、変数 `leftup` を用いて左上の解 $dp[i-1, j-1]$ を一時保存し、左と上の解だけを考えればよくなります。このときの状況は完全ナップサック問題と同じであり、順方向走査を用いることができます。コードは次のとおりです: === "Python" ```python title="edit_distance.py" def edit_distance_dp_comp(s: str, t: str) -> int: - """編集距離:空間最適化動的プログラミング""" + """編集距離:空間最適化した動的計画法""" n, m = len(s), len(t) dp = [0] * (m + 1) - # 状態遷移:最初の行 + # 状態遷移:先頭行 for j in range(1, m + 1): dp[j] = j # 状態遷移:残りの行 for i in range(1, n + 1): - # 状態遷移:最初の列 - leftup = dp[0] # dp[i-1, j-1] を一時的に保存 + # 状態遷移:先頭列 + leftup = dp[0] # dp[i-1, j-1] を一時保存する dp[0] += 1 # 状態遷移:残りの列 for j in range(1, m + 1): temp = dp[j] if s[i - 1] == t[j - 1]: - # 2 つの文字が等しい場合、これら 2 つの文字をスキップ + # 2 つの文字が等しければ、その 2 文字をそのままスキップする dp[j] = leftup else: - # 最小編集数 = 3 つの操作(挿入、削除、置換)からの最小編集数 + 1 + # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 dp[j] = min(dp[j - 1], dp[j], leftup) + 1 - leftup = temp # 次の dp[i-1, j-1] のために更新 + leftup = temp # 次の反復の dp[i-1, j-1] に更新する return dp[m] ``` === "C++" ```cpp title="edit_distance.cpp" - [class]{}-[func]{editDistanceDPComp} - ``` - -=== "Java" - - ```java title="edit_distance.java" - /* 編集距離:空間最適化動的プログラミング */ - int editDistanceDPComp(String s, String t) { + /* 編集距離:空間最適化した動的計画法 */ + int editDistanceDPComp(string s, string t) { int n = s.length(), m = t.length(); - int[] dp = new int[m + 1]; - // 状態遷移:最初の行 + vector dp(m + 1, 0); + // 状態遷移:先頭行 for (int j = 1; j <= m; j++) { dp[j] = j; } // 状態遷移:残りの行 for (int i = 1; i <= n; i++) { - // 状態遷移:最初の列 - int leftup = dp[0]; // dp[i-1, j-1] を一時的に格納 + // 状態遷移:先頭列 + int leftup = dp[0]; // dp[i-1, j-1] を一時保存する + dp[0] = i; + // 状態遷移:残りの列 + for (int j = 1; j <= m; j++) { + int temp = dp[j]; + if (s[i - 1] == t[j - 1]) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftup; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // 次の反復の dp[i-1, j-1] に更新する + } + } + return dp[m]; + } + ``` + +=== "Java" + + ```java title="edit_distance.java" + /* 編集距離:空間最適化した動的計画法 */ + int editDistanceDPComp(String s, String t) { + int n = s.length(), m = t.length(); + int[] dp = new int[m + 1]; + // 状態遷移:先頭行 + for (int j = 1; j <= m; j++) { + dp[j] = j; + } + // 状態遷移:残りの行 + for (int i = 1; i <= n; i++) { + // 状態遷移:先頭列 + int leftup = dp[0]; // dp[i-1, j-1] を一時保存する dp[0] = i; // 状態遷移:残りの列 for (int j = 1; j <= m; j++) { int temp = dp[j]; if (s.charAt(i - 1) == t.charAt(j - 1)) { - // 2つの文字が等しい場合、これら2つの文字をスキップ + // 2 つの文字が等しければ、その 2 文字をそのままスキップする dp[j] = leftup; } else { - // 最小編集数 = 3つの操作(挿入、削除、置換)からの最小編集数 + 1 + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1; } - leftup = temp; // 次のラウンドの dp[i-1, j-1] のために更新 + leftup = temp; // 次の反復の dp[i-1, j-1] に更新する } } return dp[m]; @@ -346,59 +634,339 @@ $dp[i, j]$ は上の $dp[i-1, j]$、左の $dp[i, j-1]$、左上の $dp[i-1, j-1 === "C#" ```csharp title="edit_distance.cs" - [class]{edit_distance}-[func]{EditDistanceDPComp} + /* 編集距離:空間最適化した動的計画法 */ + int EditDistanceDPComp(string s, string t) { + int n = s.Length, m = t.Length; + int[] dp = new int[m + 1]; + // 状態遷移:先頭行 + for (int j = 1; j <= m; j++) { + dp[j] = j; + } + // 状態遷移:残りの行 + for (int i = 1; i <= n; i++) { + // 状態遷移:先頭列 + int leftup = dp[0]; // dp[i-1, j-1] を一時保存する + dp[0] = i; + // 状態遷移:残りの列 + for (int j = 1; j <= m; j++) { + int temp = dp[j]; + if (s[i - 1] == t[j - 1]) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftup; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // 次の反復の dp[i-1, j-1] に更新する + } + } + return dp[m]; + } ``` === "Go" ```go title="edit_distance.go" - [class]{}-[func]{editDistanceDPComp} + /* 編集距離:空間最適化した動的計画法 */ + func editDistanceDPComp(s string, t string) int { + n := len(s) + m := len(t) + dp := make([]int, m+1) + // 状態遷移:先頭行 + for j := 1; j <= m; j++ { + dp[j] = j + } + // 状態遷移:残りの行 + for i := 1; i <= n; i++ { + // 状態遷移:先頭列 + leftUp := dp[0] // dp[i-1, j-1] を一時保存する + dp[0] = i + // 状態遷移:残りの列 + for j := 1; j <= m; j++ { + temp := dp[j] + if s[i-1] == t[j-1] { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftUp + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1 + } + leftUp = temp // 次の反復の dp[i-1, j-1] に更新する + } + } + return dp[m] + } ``` === "Swift" ```swift title="edit_distance.swift" - [class]{}-[func]{editDistanceDPComp} + /* 編集距離:空間最適化した動的計画法 */ + func editDistanceDPComp(s: String, t: String) -> Int { + let n = s.utf8CString.count + let m = t.utf8CString.count + var dp = Array(repeating: 0, count: m + 1) + // 状態遷移:先頭行 + for j in 1 ... m { + dp[j] = j + } + // 状態遷移:残りの行 + for i in 1 ... n { + // 状態遷移:先頭列 + var leftup = dp[0] // dp[i-1, j-1] を一時保存する + dp[0] = i + // 状態遷移:残りの列 + for j in 1 ... m { + let temp = dp[j] + if s.utf8CString[i - 1] == t.utf8CString[j - 1] { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftup + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1 + } + leftup = temp // 次の反復の dp[i-1, j-1] に更新する + } + } + return dp[m] + } ``` === "JS" ```javascript title="edit_distance.js" - [class]{}-[func]{editDistanceDPComp} + /* 編集距離:空間最適化した動的計画法 */ + function editDistanceDPComp(s, t) { + const n = s.length, + m = t.length; + const dp = new Array(m + 1).fill(0); + // 状態遷移:先頭行 + for (let j = 1; j <= m; j++) { + dp[j] = j; + } + // 状態遷移:残りの行 + for (let i = 1; i <= n; i++) { + // 状態遷移:先頭列 + let leftup = dp[0]; // dp[i-1, j-1] を一時保存する + dp[0] = i; + // 状態遷移:残りの列 + for (let j = 1; j <= m; j++) { + const temp = dp[j]; + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftup; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1; + } + leftup = temp; // 次の反復の dp[i-1, j-1] に更新する + } + } + return dp[m]; + } ``` === "TS" ```typescript title="edit_distance.ts" - [class]{}-[func]{editDistanceDPComp} + /* 編集距離:空間最適化した動的計画法 */ + function editDistanceDPComp(s: string, t: string): number { + const n = s.length, + m = t.length; + const dp = new Array(m + 1).fill(0); + // 状態遷移:先頭行 + for (let j = 1; j <= m; j++) { + dp[j] = j; + } + // 状態遷移:残りの行 + for (let i = 1; i <= n; i++) { + // 状態遷移:先頭列 + let leftup = dp[0]; // dp[i-1, j-1] を一時保存する + dp[0] = i; + // 状態遷移:残りの列 + for (let j = 1; j <= m; j++) { + const temp = dp[j]; + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftup; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1; + } + leftup = temp; // 次の反復の dp[i-1, j-1] に更新する + } + } + return dp[m]; + } ``` === "Dart" ```dart title="edit_distance.dart" - [class]{}-[func]{editDistanceDPComp} + /* 編集距離:空間最適化した動的計画法 */ + int editDistanceDPComp(String s, String t) { + int n = s.length, m = t.length; + List dp = List.filled(m + 1, 0); + // 状態遷移:先頭行 + for (int j = 1; j <= m; j++) { + dp[j] = j; + } + // 状態遷移:残りの行 + for (int i = 1; i <= n; i++) { + // 状態遷移:先頭列 + int leftup = dp[0]; // dp[i-1, j-1] を一時保存する + dp[0] = i; + // 状態遷移:残りの列 + for (int j = 1; j <= m; j++) { + int temp = dp[j]; + if (s[i - 1] == t[j - 1]) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftup; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // 次の反復の dp[i-1, j-1] に更新する + } + } + return dp[m]; + } ``` === "Rust" ```rust title="edit_distance.rs" - [class]{}-[func]{edit_distance_dp_comp} + /* 編集距離:空間最適化した動的計画法 */ + fn edit_distance_dp_comp(s: &str, t: &str) -> i32 { + let (n, m) = (s.len(), t.len()); + let mut dp = vec![0; m + 1]; + // 状態遷移:先頭行 + for j in 1..m { + dp[j] = j as i32; + } + // 状態遷移:残りの行 + for i in 1..=n { + // 状態遷移:先頭列 + let mut leftup = dp[0]; // dp[i-1, j-1] を一時保存する + dp[0] = i as i32; + // 状態遷移:残りの列 + for j in 1..=m { + let temp = dp[j]; + if s.chars().nth(i - 1) == t.chars().nth(j - 1) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftup; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // 次の反復の dp[i-1, j-1] に更新する + } + } + dp[m] + } ``` === "C" ```c title="edit_distance.c" - [class]{}-[func]{editDistanceDPComp} + /* 編集距離:空間最適化した動的計画法 */ + int editDistanceDPComp(char *s, char *t, int n, int m) { + int *dp = calloc(m + 1, sizeof(int)); + // 状態遷移:先頭行 + for (int j = 1; j <= m; j++) { + dp[j] = j; + } + // 状態遷移:残りの行 + for (int i = 1; i <= n; i++) { + // 状態遷移:先頭列 + int leftup = dp[0]; // dp[i-1, j-1] を一時保存する + dp[0] = i; + // 状態遷移:残りの列 + for (int j = 1; j <= m; j++) { + int temp = dp[j]; + if (s[i - 1] == t[j - 1]) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftup; + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // 次の反復の dp[i-1, j-1] に更新する + } + } + int res = dp[m]; + // メモリを解放する + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="edit_distance.kt" - [class]{}-[func]{editDistanceDPComp} + /* 編集距離:空間最適化した動的計画法 */ + fun editDistanceDPComp(s: String, t: String): Int { + val n = s.length + val m = t.length + val dp = IntArray(m + 1) + // 状態遷移:先頭行 + for (j in 1..m) { + dp[j] = j + } + // 状態遷移:残りの行 + for (i in 1..n) { + // 状態遷移:先頭列 + var leftup = dp[0] // dp[i-1, j-1] を一時保存する + dp[0] = i + // 状態遷移:残りの列 + for (j in 1..m) { + val temp = dp[j] + if (s[i - 1] == t[j - 1]) { + // 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftup + } else { + // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1 + } + leftup = temp // 次の反復の dp[i-1, j-1] に更新する + } + } + return dp[m] + } ``` === "Ruby" ```ruby title="edit_distance.rb" - [class]{}-[func]{edit_distance_dp_comp} + ### 編集距離:空間最適化した動的計画法 ### + def edit_distance_dp_comp(s, t) + n, m = s.length, t.length + dp = Array.new(m + 1, 0) + # 状態遷移:先頭行 + (1...(m + 1)).each { |j| dp[j] = j } + # 状態遷移:残りの行 + for i in 1...(n + 1) + # 状態遷移:先頭列 + leftup = dp.first # dp[i-1, j-1] を一時保存する + dp[0] += 1 + # 状態遷移:残りの列 + for j in 1...(m + 1) + temp = dp[j] + if s[i - 1] == t[j - 1] + # 2 つの文字が等しければ、その 2 文字をそのままスキップする + dp[j] = leftup + else + # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1 + dp[j] = [dp[j - 1], dp[j], leftup].min + 1 + end + leftup = temp # 次の反復の dp[i-1, j-1] に更新する + end + end + dp[m] + end ``` + +??? pythontutor "コードの可視化" + +
+ diff --git a/ja/docs/chapter_dynamic_programming/index.md b/ja/docs/chapter_dynamic_programming/index.md index 31bb6d91a..dff7b1047 100644 --- a/ja/docs/chapter_dynamic_programming/index.md +++ b/ja/docs/chapter_dynamic_programming/index.md @@ -3,22 +3,22 @@ comments: true icon: material/table-pivot --- -# 第 14 章   動的プログラミング +# 第 14 章   動的計画法 -![動的プログラミング](../assets/covers/chapter_dynamic_programming.jpg){ class="cover-image" } +![動的計画法](../assets/covers/chapter_dynamic_programming.jpg){ class="cover-image" } !!! abstract - 川が流れて海に注ぐように、 - - 動的プログラミングは小さな問題の解を織り合わせて、より大きな問題の解へと導きます。一歩一歩進んで、最終的な答えが待つ彼岸へと向かいます。 + 小川は川へと注ぎ、河川は大海へと注ぐ。 + + 動的計画法は小さな問題の解を集めて大きな問題の答えとし、一歩ずつ私たちを問題解決の彼岸へと導く。 ## 章の内容 -- [14.1   動的計画法の初歩](intro_to_dynamic_programming.md) -- [14.2   DP 問題の特性](dp_problem_features.md) -- [14.3   DP の解法の考え方](dp_solution_pipeline.md) -- [14.4   0-1ナップサック問題](knapsack_problem.md) +- [14.1   動的計画法入門](intro_to_dynamic_programming.md) +- [14.2   動的計画法の問題特性](dp_problem_features.md) +- [14.3   動的計画法の問題解決の考え方](dp_solution_pipeline.md) +- [14.4   0-1 ナップサック問題](knapsack_problem.md) - [14.5   完全ナップサック問題](unbounded_knapsack_problem.md) - [14.6   編集距離問題](edit_distance_problem.md) - [14.7   まとめ](summary.md) diff --git a/ja/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/ja/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md index cd1e47668..72fbf6d02 100644 --- a/ja/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/ja/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -2,46 +2,46 @@ comments: true --- -# 14.1   動的プログラミングの紹介 +# 14.1   動的計画法入門 -動的プログラミングは重要なアルゴリズムパラダイムであり、問題を一連の小さな部分問題に分解し、これらの部分問題の解を保存することで冗長な計算を避け、時間効率を大幅に向上させます。 +動的計画法(dynamic programming)は重要なアルゴリズムパラダイムであり、問題をより小さな部分問題の列に分解し、それらの解を保存して重複計算を避けることで、時間効率を大幅に向上させます。 -このセクションでは、古典的な問題から始めて、まず力任せの探索法による解法を提示し、重複する部分問題を特定してから、より効率的な動的プログラミング解法を段階的に導出します。 +本節では、古典的な例題から始めて、まずその力任せのバックトラッキング解法を示し、そこに含まれる重複部分問題を観察したうえで、より効率的な動的計画法の解法を段階的に導きます。 -!!! question "階段登り" +!!! question "階段を上る" - $n$ 段の階段があり、一度に $1$ 段または $2$ 段上ることができます。頂上に到達する方法は何通りありますか? + 全体で $n$ 段ある階段が与えられ、各ステップで $1$ 段または $2$ 段上ることができます。頂上まで到達する方法は何通りあるでしょうか? -下の図に示すように、$3$ 段の階段の頂上に到達する方法は $3$ 通りあります。 +次の図に示すように、$3$ 段の階段では、頂上まで到達する方法は全部で $3$ 通りあります。 -![3段目に到達する方法の数](intro_to_dynamic_programming.assets/climbing_stairs_example.png){ class="animation-figure" } +![3 段の階段を上る方法の数](intro_to_dynamic_programming.assets/climbing_stairs_example.png){ class="animation-figure" } -

図 14-1   3段目に到達する方法の数

+

図 14-1   3 段の階段を上る方法の数

-この問題は**バックトラッキングを用いてすべての可能性を網羅**することで方法の数を計算することを目的としています。具体的には、階段登りの問題を多段階選択プロセスとして考えます:地面から始めて、毎回 $1$ 段または $2$ 段上るかを選択し、階段の頂上に到達したら方法の数をカウントし、頂上を超えた場合はプルーニング(枝刈り)を行います。コードは以下の通りです: +この問題の目的は方法の総数を求めることです。**考えられるすべての可能性をバックトラッキングで総当たりすることができます**。具体的には、階段を上ることを複数ラウンドの選択過程とみなし、地面から出発して各ラウンドで $1$ 段または $2$ 段上ります。階段の頂上に到達するたびに方法数を $1$ 増やし、頂上を越えた場合は枝刈りします。コードは次のとおりです: === "Python" ```python title="climbing_stairs_backtrack.py" def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int: """バックトラッキング""" - # n 段目に登ったとき、解の数に 1 を加える + # 第 n 段に到達したら、方法数を 1 増やす if state == n: res[0] += 1 # すべての選択肢を走査 for choice in choices: - # 枝刈り:n 段を超えて登ることを許可しない + # 枝刈り: 第 n 段を超えないようにする if state + choice > n: continue - # 試行:選択を行い、状態を更新 + # 試行: 選択を行い、状態を更新 backtrack(choices, state + choice, n, res) - # 撤回 + # バックトラック def climbing_stairs_backtrack(n: int) -> int: """階段登り:バックトラッキング""" - choices = [1, 2] # 1 段または 2 段登ることを選択可能 - state = 0 # 0 段目から登り始める - res = [0] # res[0] を使用して解の数を記録 + choices = [1, 2] # 1 段または 2 段上ることを選べる + state = 0 # 第 0 段から上り始める + res = [0] # res[0] を使って方法数を記録する backtrack(choices, state, n, res) return res[0] ``` @@ -51,25 +51,25 @@ comments: true ```cpp title="climbing_stairs_backtrack.cpp" /* バックトラッキング */ void backtrack(vector &choices, int state, int n, vector &res) { - // n段目に到達したとき、解の数に1を加える + // 第 n 段に到達したら、方法数を 1 増やす if (state == n) res[0]++; // すべての選択肢を走査 for (auto &choice : choices) { - // 剪定:n段を超えて登ることを許可しない + // 枝刈り: 第 n 段を超えないようにする if (state + choice > n) continue; - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 backtrack(choices, state + choice, n, res); - // 撤回 + // バックトラック } } /* 階段登り:バックトラッキング */ int climbingStairsBacktrack(int n) { - vector choices = {1, 2}; // 1段または2段登ることを選択可能 - int state = 0; // 0段目から登り始める - vector res = {0}; // res[0] を使用して解の数を記録 + vector choices = {1, 2}; // 1 段または 2 段上ることを選べる + int state = 0; // 第 0 段から上り始める + vector res = {0}; // res[0] を使って方法数を記録する backtrack(choices, state, n, res); return res[0]; } @@ -80,26 +80,26 @@ comments: true ```java title="climbing_stairs_backtrack.java" /* バックトラッキング */ void backtrack(List choices, int state, int n, List res) { - // n段目に到達したとき、解の数に1を加える + // 第 n 段に到達したら、方法数を 1 増やす if (state == n) res.set(0, res.get(0) + 1); // すべての選択肢を走査 for (Integer choice : choices) { - // 剪定:n段を超えて登ることを許可しない + // 枝刈り: 第 n 段を超えないようにする if (state + choice > n) continue; - // 試行:選択を行い、状態を更新 + // 試行: 選択を行い、状態を更新 backtrack(choices, state + choice, n, res); - // 撤回 + // バックトラック } } /* 階段登り:バックトラッキング */ int climbingStairsBacktrack(int n) { - List choices = Arrays.asList(1, 2); // 1段または2段登ることを選択可能 - int state = 0; // 0段目から登り始める + List choices = Arrays.asList(1, 2); // 1 段または 2 段上ることを選べる + int state = 0; // 第 0 段から上り始める List res = new ArrayList<>(); - res.add(0); // res[0] を使用して解の数を記録 + res.add(0); // res[0] を使って方法数を記録する backtrack(choices, state, n, res); return res.get(0); } @@ -108,117 +108,357 @@ comments: true === "C#" ```csharp title="climbing_stairs_backtrack.cs" - [class]{climbing_stairs_backtrack}-[func]{Backtrack} + /* バックトラッキング */ + void Backtrack(List choices, int state, int n, List res) { + // 第 n 段に到達したら、方法数を 1 増やす + if (state == n) + res[0]++; + // すべての選択肢を走査 + foreach (int choice in choices) { + // 枝刈り: 第 n 段を超えないようにする + if (state + choice > n) + continue; + // 試行: 選択を行い、状態を更新 + Backtrack(choices, state + choice, n, res); + // バックトラック + } + } - [class]{climbing_stairs_backtrack}-[func]{ClimbingStairsBacktrack} + /* 階段登り:バックトラッキング */ + int ClimbingStairsBacktrack(int n) { + List choices = [1, 2]; // 1 段または 2 段上ることを選べる + int state = 0; // 第 0 段から上り始める + List res = [0]; // res[0] を使って方法数を記録する + Backtrack(choices, state, n, res); + return res[0]; + } ``` === "Go" ```go title="climbing_stairs_backtrack.go" - [class]{}-[func]{backtrack} + /* バックトラッキング */ + func backtrack(choices []int, state, n int, res []int) { + // 第 n 段に到達したら、方法数を 1 増やす + if state == n { + res[0] = res[0] + 1 + } + // すべての選択肢を走査 + for _, choice := range choices { + // 枝刈り: 第 n 段を超えないようにする + if state+choice > n { + continue + } + // 試行: 選択を行い、状態を更新 + backtrack(choices, state+choice, n, res) + // バックトラック + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* 階段登り:バックトラッキング */ + func climbingStairsBacktrack(n int) int { + // 1 段または 2 段上ることを選べる + choices := []int{1, 2} + // 第 0 段から上り始める + state := 0 + res := make([]int, 1) + // res[0] を使って方法数を記録する + res[0] = 0 + backtrack(choices, state, n, res) + return res[0] + } ``` === "Swift" ```swift title="climbing_stairs_backtrack.swift" - [class]{}-[func]{backtrack} + /* バックトラッキング */ + func backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) { + // 第 n 段に到達したら、方法数を 1 増やす + if state == n { + res[0] += 1 + } + // すべての選択肢を走査 + for choice in choices { + // 枝刈り: 第 n 段を超えないようにする + if state + choice > n { + continue + } + // 試行: 選択を行い、状態を更新 + backtrack(choices: choices, state: state + choice, n: n, res: &res) + // バックトラック + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* 階段登り:バックトラッキング */ + func climbingStairsBacktrack(n: Int) -> Int { + let choices = [1, 2] // 1 段または 2 段上ることを選べる + let state = 0 // 第 0 段から上り始める + var res: [Int] = [] + res.append(0) // res[0] を使って方法数を記録する + backtrack(choices: choices, state: state, n: n, res: &res) + return res[0] + } ``` === "JS" ```javascript title="climbing_stairs_backtrack.js" - [class]{}-[func]{backtrack} + /* バックトラッキング */ + function backtrack(choices, state, n, res) { + // 第 n 段に到達したら、方法数を 1 増やす + if (state === n) res.set(0, res.get(0) + 1); + // すべての選択肢を走査 + for (const choice of choices) { + // 枝刈り: 第 n 段を超えないようにする + if (state + choice > n) continue; + // 試行: 選択を行い、状態を更新 + backtrack(choices, state + choice, n, res); + // バックトラック + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* 階段登り:バックトラッキング */ + function climbingStairsBacktrack(n) { + const choices = [1, 2]; // 1 段または 2 段上ることを選べる + const state = 0; // 第 0 段から上り始める + const res = new Map(); + res.set(0, 0); // res[0] を使って方法数を記録する + backtrack(choices, state, n, res); + return res.get(0); + } ``` === "TS" ```typescript title="climbing_stairs_backtrack.ts" - [class]{}-[func]{backtrack} + /* バックトラッキング */ + function backtrack( + choices: number[], + state: number, + n: number, + res: Map<0, any> + ): void { + // 第 n 段に到達したら、方法数を 1 増やす + if (state === n) res.set(0, res.get(0) + 1); + // すべての選択肢を走査 + for (const choice of choices) { + // 枝刈り: 第 n 段を超えないようにする + if (state + choice > n) continue; + // 試行: 選択を行い、状態を更新 + backtrack(choices, state + choice, n, res); + // バックトラック + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* 階段登り:バックトラッキング */ + function climbingStairsBacktrack(n: number): number { + const choices = [1, 2]; // 1 段または 2 段上ることを選べる + const state = 0; // 第 0 段から上り始める + const res = new Map(); + res.set(0, 0); // res[0] を使って方法数を記録する + backtrack(choices, state, n, res); + return res.get(0); + } ``` === "Dart" ```dart title="climbing_stairs_backtrack.dart" - [class]{}-[func]{backtrack} + /* バックトラッキング */ + void backtrack(List choices, int state, int n, List res) { + // 第 n 段に到達したら、方法数を 1 増やす + if (state == n) { + res[0]++; + } + // すべての選択肢を走査 + for (int choice in choices) { + // 枝刈り: 第 n 段を超えないようにする + if (state + choice > n) continue; + // 試行: 選択を行い、状態を更新 + backtrack(choices, state + choice, n, res); + // バックトラック + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* 階段登り:バックトラッキング */ + int climbingStairsBacktrack(int n) { + List choices = [1, 2]; // 1 段または 2 段上ることを選べる + int state = 0; // 第 0 段から上り始める + List res = []; + res.add(0); // res[0] を使って方法数を記録する + backtrack(choices, state, n, res); + return res[0]; + } ``` === "Rust" ```rust title="climbing_stairs_backtrack.rs" - [class]{}-[func]{backtrack} + /* バックトラッキング */ + fn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) { + // 第 n 段に到達したら、方法数を 1 増やす + if state == n { + res[0] = res[0] + 1; + } + // すべての選択肢を走査 + for &choice in choices { + // 枝刈り: 第 n 段を超えないようにする + if state + choice > n { + continue; + } + // 試行: 選択を行い、状態を更新 + backtrack(choices, state + choice, n, res); + // バックトラック + } + } - [class]{}-[func]{climbing_stairs_backtrack} + /* 階段登り:バックトラッキング */ + fn climbing_stairs_backtrack(n: usize) -> i32 { + let choices = vec![1, 2]; // 1 段または 2 段上ることを選べる + let state = 0; // 第 0 段から上り始める + let mut res = Vec::new(); + res.push(0); // res[0] を使って方法数を記録する + backtrack(&choices, state, n as i32, &mut res); + res[0] + } ``` === "C" ```c title="climbing_stairs_backtrack.c" - [class]{}-[func]{backtrack} + /* バックトラッキング */ + void backtrack(int *choices, int state, int n, int *res, int len) { + // 第 n 段に到達したら、方法数を 1 増やす + if (state == n) + res[0]++; + // すべての選択肢を走査 + for (int i = 0; i < len; i++) { + int choice = choices[i]; + // 枝刈り: 第 n 段を超えないようにする + if (state + choice > n) + continue; + // 試行: 選択を行い、状態を更新 + backtrack(choices, state + choice, n, res, len); + // バックトラック + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* 階段登り:バックトラッキング */ + int climbingStairsBacktrack(int n) { + int choices[2] = {1, 2}; // 1 段または 2 段上ることを選べる + int state = 0; // 第 0 段から上り始める + int *res = (int *)malloc(sizeof(int)); + *res = 0; // res[0] を使って方法数を記録する + int len = sizeof(choices) / sizeof(int); + backtrack(choices, state, n, res, len); + int result = *res; + free(res); + return result; + } ``` === "Kotlin" ```kotlin title="climbing_stairs_backtrack.kt" - [class]{}-[func]{backtrack} + /* バックトラッキング */ + fun backtrack( + choices: MutableList, + state: Int, + n: Int, + res: MutableList + ) { + // 第 n 段に到達したら、方法数を 1 増やす + if (state == n) + res[0] = res[0] + 1 + // すべての選択肢を走査 + for (choice in choices) { + // 枝刈り: 第 n 段を超えないようにする + if (state + choice > n) continue + // 試行: 選択を行い、状態を更新 + backtrack(choices, state + choice, n, res) + // バックトラック + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* 階段登り:バックトラッキング */ + fun climbingStairsBacktrack(n: Int): Int { + val choices = mutableListOf(1, 2) // 1 段または 2 段上ることを選べる + val state = 0 // 第 0 段から上り始める + val res = mutableListOf() + res.add(0) // res[0] を使って方法数を記録する + backtrack(choices, state, n, res) + return res[0] + } ``` === "Ruby" ```ruby title="climbing_stairs_backtrack.rb" - [class]{}-[func]{backtrack} + ### バックトラッキング ### + def backtrack(choices, state, n, res) + # 第 n 段に到達したら、方法数を 1 増やす + res[0] += 1 if state == n + # すべての選択肢を走査 + for choice in choices + # 枝刈り: 第 n 段を超えないようにする + next if state + choice > n - [class]{}-[func]{climbing_stairs_backtrack} + # 試行: 選択を行い、状態を更新 + backtrack(choices, state + choice, n, res) + end + # バックトラック + end + + ### 階段登り:バックトラッキング ### + def climbing_stairs_backtrack(n) + choices = [1, 2] # 1 段または 2 段上ることを選べる + state = 0 # 第 0 段から上り始める + res = [0] # res[0] を使って方法数を記録する + backtrack(choices, state, n, res) + res.first + end ``` -## 14.1.1   方法1:力任せ探索 +??? pythontutor "コードの可視化" -バックトラッキングアルゴリズムは問題を明示的に部分問題に分解しません。代わりに、問題を一連の決定ステップとして扱い、試行と枝刈りを通じてすべての可能性を探索します。 +
+ -この問題を分解アプローチを使って分析できます。$dp[i]$ を $i$ 段目に到達する方法の数とします。この場合、$dp[i]$ が元の問題であり、その部分問題は次のようになります: +## 14.1.1   方法 1:総当たり探索 + +バックトラッキング法は通常、問題を明示的に分解するのではなく、問題解決を一連の意思決定ステップとみなし、試行と枝刈りによってあらゆる可能な解を探索します。 + +この問題を問題分解の観点から分析してみましょう。$i$ 段目まで上る方法が全部で $dp[i]$ 通りあるとすると、$dp[i]$ が元の問題であり、その部分問題には次が含まれます: $$ dp[i-1], dp[i-2], \dots, dp[2], dp[1] $$ -各移動は $1$ 段または $2$ 段しか進めないため、$i$ 段目に立っているとき、前のステップは $i-1$ 段目または $i-2$ 段目のいずれかにいたはずです。つまり、$i$ 段目には $i-1$ 段目または $i-2$ 段目からしか到達できません。 +各ラウンドでは $1$ 段または $2$ 段しか上れないため、$i$ 段目の階段に立っているとき、直前のラウンドでは $i - 1$ 段目または $i - 2$ 段目にしか立てません。言い換えると、$i -1$ 段目または $i - 2$ 段目からしか $i$ 段目へ進めません。 -これにより重要な結論が得られます:**$i-1$ 段目に到達する方法の数に $i-2$ 段目に到達する方法の数を加えたものが、$i$ 段目に到達する方法の数に等しい**。式は以下の通りです: +ここから重要な帰結が得られます。**$i - 1$ 段目まで上る方法数と $i - 2$ 段目まで上る方法数の和が、$i$ 段目まで上る方法数に等しい**のです。式は次のとおりです: $$ dp[i] = dp[i-1] + dp[i-2] $$ -これは、階段登り問題において部分問題間に再帰関係があることを意味し、**元の問題の解は部分問題の解から構築できます**。下の図はこの再帰関係を示しています。 +これは、階段を上る問題では各部分問題の間に漸化関係があり、**元の問題の解は部分問題の解から構築できる**ことを意味します。次の図はこの漸化関係を示しています。 -![解の数の再帰関係](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png){ class="animation-figure" } +![方法数の漸化関係](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png){ class="animation-figure" } -

図 14-2   解の数の再帰関係

+

図 14-2   方法数の漸化関係

-再帰式に従って力任せ探索解法を得ることができます。$dp[n]$ から始めて、**より大きな問題を再帰的に2つの小さな部分問題の和に分解**し、解が既知の最小の部分問題 $dp[1]$ と $dp[2]$ に到達するまで続けます。$dp[1] = 1$ と $dp[2] = 2$ で、それぞれ1段目と2段目に登る方法が $1$ 通りと $2$ 通りあることを表します。 +漸化式に基づいて総当たり探索の解法を得ることができます。$dp[n]$ を出発点とし、**より大きな問題を再帰的に 2 つのより小さな問題の和へ分解**していき、最小部分問題 $dp[1]$ と $dp[2]$ に到達したら返します。ここで最小部分問題の解は既知であり、$dp[1] = 1$、$dp[2] = 2$ です。これは、第 $1$ 段目と第 $2$ 段目まで上る方法がそれぞれ $1$ 通り、$2$ 通りであることを表します。 -以下のコードを観察すると、標準的なバックトラッキングコードと同様に深さ優先探索に属しますが、より簡潔です: +次のコードを見ると、標準的なバックトラッキングコードと同じく深さ優先探索に属しますが、より簡潔です: === "Python" ```python title="climbing_stairs_dfs.py" def dfs(i: int) -> int: - """探索""" - # 既知の dp[1] と dp[2] は、それらを返す + """検索""" + # dp[1] と dp[2] は既知なので返す if i == 1 or i == 2: return i # dp[i] = dp[i-1] + dp[i-2] @@ -233,9 +473,9 @@ $$ === "C++" ```cpp title="climbing_stairs_dfs.cpp" - /* 探索 */ + /* 検索 */ int dfs(int i) { - // 既知の dp[1] と dp[2] を返す + // dp[1] と dp[2] は既知なので返す if (i == 1 || i == 2) return i; // dp[i] = dp[i-1] + dp[i-2] @@ -252,9 +492,9 @@ $$ === "Java" ```java title="climbing_stairs_dfs.java" - /* 探索 */ + /* 検索 */ int dfs(int i) { - // 既知の dp[1] と dp[2] を返す + // dp[1] と dp[2] は既知なので返す if (i == 1 || i == 2) return i; // dp[i] = dp[i-1] + dp[i-2] @@ -271,122 +511,234 @@ $$ === "C#" ```csharp title="climbing_stairs_dfs.cs" - [class]{climbing_stairs_dfs}-[func]{DFS} + /* 検索 */ + int DFS(int i) { + // dp[1] と dp[2] は既知なので返す + if (i == 1 || i == 2) + return i; + // dp[i] = dp[i-1] + dp[i-2] + int count = DFS(i - 1) + DFS(i - 2); + return count; + } - [class]{climbing_stairs_dfs}-[func]{ClimbingStairsDFS} + /* 階段登り:探索 */ + int ClimbingStairsDFS(int n) { + return DFS(n); + } ``` === "Go" ```go title="climbing_stairs_dfs.go" - [class]{}-[func]{dfs} + /* 検索 */ + func dfs(i int) int { + // dp[1] と dp[2] は既知なので返す + if i == 1 || i == 2 { + return i + } + // dp[i] = dp[i-1] + dp[i-2] + count := dfs(i-1) + dfs(i-2) + return count + } - [class]{}-[func]{climbingStairsDFS} + /* 階段登り:探索 */ + func climbingStairsDFS(n int) int { + return dfs(n) + } ``` === "Swift" ```swift title="climbing_stairs_dfs.swift" - [class]{}-[func]{dfs} + /* 検索 */ + func dfs(i: Int) -> Int { + // dp[1] と dp[2] は既知なので返す + if i == 1 || i == 2 { + return i + } + // dp[i] = dp[i-1] + dp[i-2] + let count = dfs(i: i - 1) + dfs(i: i - 2) + return count + } - [class]{}-[func]{climbingStairsDFS} + /* 階段登り:探索 */ + func climbingStairsDFS(n: Int) -> Int { + dfs(i: n) + } ``` === "JS" ```javascript title="climbing_stairs_dfs.js" - [class]{}-[func]{dfs} + /* 検索 */ + function dfs(i) { + // dp[1] と dp[2] は既知なので返す + if (i === 1 || i === 2) return i; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1) + dfs(i - 2); + return count; + } - [class]{}-[func]{climbingStairsDFS} + /* 階段登り:探索 */ + function climbingStairsDFS(n) { + return dfs(n); + } ``` === "TS" ```typescript title="climbing_stairs_dfs.ts" - [class]{}-[func]{dfs} + /* 検索 */ + function dfs(i: number): number { + // dp[1] と dp[2] は既知なので返す + if (i === 1 || i === 2) return i; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1) + dfs(i - 2); + return count; + } - [class]{}-[func]{climbingStairsDFS} + /* 階段登り:探索 */ + function climbingStairsDFS(n: number): number { + return dfs(n); + } ``` === "Dart" ```dart title="climbing_stairs_dfs.dart" - [class]{}-[func]{dfs} + /* 検索 */ + int dfs(int i) { + // dp[1] と dp[2] は既知なので返す + if (i == 1 || i == 2) return i; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1) + dfs(i - 2); + return count; + } - [class]{}-[func]{climbingStairsDFS} + /* 階段登り:探索 */ + int climbingStairsDFS(int n) { + return dfs(n); + } ``` === "Rust" ```rust title="climbing_stairs_dfs.rs" - [class]{}-[func]{dfs} + /* 検索 */ + fn dfs(i: usize) -> i32 { + // dp[1] と dp[2] は既知なので返す + if i == 1 || i == 2 { + return i as i32; + } + // dp[i] = dp[i-1] + dp[i-2] + let count = dfs(i - 1) + dfs(i - 2); + count + } - [class]{}-[func]{climbing_stairs_dfs} + /* 階段登り:探索 */ + fn climbing_stairs_dfs(n: usize) -> i32 { + dfs(n) + } ``` === "C" ```c title="climbing_stairs_dfs.c" - [class]{}-[func]{dfs} + /* 検索 */ + int dfs(int i) { + // dp[1] と dp[2] は既知なので返す + if (i == 1 || i == 2) + return i; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1) + dfs(i - 2); + return count; + } - [class]{}-[func]{climbingStairsDFS} + /* 階段登り:探索 */ + int climbingStairsDFS(int n) { + return dfs(n); + } ``` === "Kotlin" ```kotlin title="climbing_stairs_dfs.kt" - [class]{}-[func]{dfs} + /* 検索 */ + fun dfs(i: Int): Int { + // dp[1] と dp[2] は既知なので返す + if (i == 1 || i == 2) return i + // dp[i] = dp[i-1] + dp[i-2] + val count = dfs(i - 1) + dfs(i - 2) + return count + } - [class]{}-[func]{climbingStairsDFS} + /* 階段登り:探索 */ + fun climbingStairsDFS(n: Int): Int { + return dfs(n) + } ``` === "Ruby" ```ruby title="climbing_stairs_dfs.rb" - [class]{}-[func]{dfs} + ### 探索 ### + def dfs(i) + # dp[1] と dp[2] は既知なので返す + return i if i == 1 || i == 2 + # dp[i] = dp[i-1] + dp[i-2] + dfs(i - 1) + dfs(i - 2) + end - [class]{}-[func]{climbing_stairs_dfs} + ### 階段登り:探索 ### + def climbing_stairs_dfs(n) + dfs(n) + end ``` -下の図は力任せ探索によって形成される再帰木を示しています。問題 $dp[n]$ について、その再帰木の深さは $n$ で、時間計算量は $O(2^n)$ です。この指数的増加により、$n$ が大きいとプログラムの実行がはるかに遅くなり、長い待機時間が生じます。 +??? pythontutor "コードの可視化" -![階段登りの再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png){ class="animation-figure" } +
+ -

図 14-3   階段登りの再帰木

+次の図は総当たり探索によって形成される再帰木を示しています。問題 $dp[n]$ に対して、その再帰木の深さは $n$、時間計算量は $O(2^n)$ です。指数オーダーは爆発的に増加するため、比較的大きな $n$ を入力すると長時間待たされることになります。 -上の図を観察すると、**指数時間計算量は「重複する部分問題」によって引き起こされる**ことがわかります。例えば、$dp[9]$ は $dp[8]$ と $dp[7]$ に分解され、$dp[8]$ はさらに $dp[7]$ と $dp[6]$ に分解され、両方とも部分問題 $dp[7]$ を含んでいます。 +![階段上りに対応する再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png){ class="animation-figure" } -このように、部分問題にはさらに小さな重複する部分問題が含まれ、これは無限に続きます。計算リソースの大部分がこれらの重複する部分問題に浪費されています。 +

図 14-3   階段上りに対応する再帰木

-## 14.1.2   方法2:メモ化探索 +上の図を見ると、**指数オーダーの時間計算量は「重複部分問題」によって生じています**。たとえば $dp[9]$ は $dp[8]$ と $dp[7]$ に分解され、$dp[8]$ は $dp[7]$ と $dp[6]$ に分解されるため、どちらにも部分問題 $dp[7]$ が含まれています。 -アルゴリズムの効率を向上させるため、**すべての重複する部分問題を一度だけ計算したい**と考えます。この目的のため、各部分問題の解を記録する配列 `mem` を宣言し、探索プロセス中に重複する部分問題を枝刈りします。 +このように、部分問題の中にはさらに小さな重複部分問題が含まれ、それが際限なく続いていきます。計算資源の大部分は、こうした重複部分問題に浪費されています。 -1. $dp[i]$ が初めて計算されるとき、後で使用するために `mem[i]` に記録します。 -2. $dp[i]$ を再度計算する必要があるとき、`mem[i]` から直接結果を取得でき、その部分問題の冗長な計算を避けられます。 +## 14.1.2   方法 2:メモ化探索 -コードは以下の通りです: +アルゴリズム効率を高めるため、**すべての重複部分問題を 1 回だけ計算したい**と考えます。そのために、各部分問題の解を記録する配列 `mem` を宣言し、探索の過程で重複部分問題を枝刈りします。 + +1. $dp[i]$ を初めて計算したとき、その結果を `mem[i]` に記録して後で使えるようにします。 +2. 再び $dp[i]$ を計算する必要が生じたときは、`mem[i]` から直接結果を取得し、その部分問題の重複計算を避けます。 + +コードは次のとおりです: === "Python" ```python title="climbing_stairs_dfs_mem.py" def dfs(i: int, mem: list[int]) -> int: - """記憶化探索""" - # 既知の dp[1] と dp[2] は、それらを返す + """メモ化探索""" + # dp[1] と dp[2] は既知なので返す if i == 1 or i == 2: return i - # dp[i] の記録がある場合、それを返す + # dp[i] の記録があれば、それをそのまま返す if mem[i] != -1: return mem[i] # dp[i] = dp[i-1] + dp[i-2] count = dfs(i - 1, mem) + dfs(i - 2, mem) - # dp[i] を記録 + # dp[i] を記録する mem[i] = count return count def climbing_stairs_dfs_mem(n: int) -> int: - """階段登り:記憶化探索""" - # mem[i] は i 段目に登る解の総数を記録、-1 は記録なしを意味する + """階段登り:メモ化探索""" + # mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す mem = [-1] * (n + 1) return dfs(n, mem) ``` @@ -396,22 +748,22 @@ $$ ```cpp title="climbing_stairs_dfs_mem.cpp" /* メモ化探索 */ int dfs(int i, vector &mem) { - // 既知の dp[1] と dp[2] を返す + // dp[1] と dp[2] は既知なので返す if (i == 1 || i == 2) return i; - // dp[i] の記録がある場合、それを返す + // dp[i] の記録があれば、それをそのまま返す if (mem[i] != -1) return mem[i]; // dp[i] = dp[i-1] + dp[i-2] int count = dfs(i - 1, mem) + dfs(i - 2, mem); - // dp[i] を記録 + // dp[i] を記録する mem[i] = count; return count; } /* 階段登り:メモ化探索 */ int climbingStairsDFSMem(int n) { - // mem[i] は i 段目に登る総解数を記録、-1 は記録なしを意味する + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す vector mem(n + 1, -1); return dfs(n, mem); } @@ -422,22 +774,22 @@ $$ ```java title="climbing_stairs_dfs_mem.java" /* メモ化探索 */ int dfs(int i, int[] mem) { - // 既知の dp[1] と dp[2] を返す + // dp[1] と dp[2] は既知なので返す if (i == 1 || i == 2) return i; - // dp[i] の記録がある場合、それを返す + // dp[i] の記録があれば、それをそのまま返す if (mem[i] != -1) return mem[i]; // dp[i] = dp[i-1] + dp[i-2] int count = dfs(i - 1, mem) + dfs(i - 2, mem); - // dp[i] を記録 + // dp[i] を記録する mem[i] = count; return count; } /* 階段登り:メモ化探索 */ int climbingStairsDFSMem(int n) { - // mem[i] は i 段目に登る総解数を記録、-1 は記録なしを意味する + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す int[] mem = new int[n + 1]; Arrays.fill(mem, -1); return dfs(n, mem); @@ -447,109 +799,300 @@ $$ === "C#" ```csharp title="climbing_stairs_dfs_mem.cs" - [class]{climbing_stairs_dfs_mem}-[func]{DFS} + /* メモ化探索 */ + int DFS(int i, int[] mem) { + // dp[1] と dp[2] は既知なので返す + if (i == 1 || i == 2) + return i; + // dp[i] の記録があれば、それをそのまま返す + if (mem[i] != -1) + return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + int count = DFS(i - 1, mem) + DFS(i - 2, mem); + // dp[i] を記録する + mem[i] = count; + return count; + } - [class]{climbing_stairs_dfs_mem}-[func]{ClimbingStairsDFSMem} + /* 階段登り:メモ化探索 */ + int ClimbingStairsDFSMem(int n) { + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す + int[] mem = new int[n + 1]; + Array.Fill(mem, -1); + return DFS(n, mem); + } ``` === "Go" ```go title="climbing_stairs_dfs_mem.go" - [class]{}-[func]{dfsMem} + /* メモ化探索 */ + func dfsMem(i int, mem []int) int { + // dp[1] と dp[2] は既知なので返す + if i == 1 || i == 2 { + return i + } + // dp[i] の記録があれば、それをそのまま返す + if mem[i] != -1 { + return mem[i] + } + // dp[i] = dp[i-1] + dp[i-2] + count := dfsMem(i-1, mem) + dfsMem(i-2, mem) + // dp[i] を記録する + mem[i] = count + return count + } - [class]{}-[func]{climbingStairsDFSMem} + /* 階段登り:メモ化探索 */ + func climbingStairsDFSMem(n int) int { + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す + mem := make([]int, n+1) + for i := range mem { + mem[i] = -1 + } + return dfsMem(n, mem) + } ``` === "Swift" ```swift title="climbing_stairs_dfs_mem.swift" - [class]{}-[func]{dfs} + /* メモ化探索 */ + func dfs(i: Int, mem: inout [Int]) -> Int { + // dp[1] と dp[2] は既知なので返す + if i == 1 || i == 2 { + return i + } + // dp[i] の記録があれば、それをそのまま返す + if mem[i] != -1 { + return mem[i] + } + // dp[i] = dp[i-1] + dp[i-2] + let count = dfs(i: i - 1, mem: &mem) + dfs(i: i - 2, mem: &mem) + // dp[i] を記録する + mem[i] = count + return count + } - [class]{}-[func]{climbingStairsDFSMem} + /* 階段登り:メモ化探索 */ + func climbingStairsDFSMem(n: Int) -> Int { + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す + var mem = Array(repeating: -1, count: n + 1) + return dfs(i: n, mem: &mem) + } ``` === "JS" ```javascript title="climbing_stairs_dfs_mem.js" - [class]{}-[func]{dfs} + /* メモ化探索 */ + function dfs(i, mem) { + // dp[1] と dp[2] は既知なので返す + if (i === 1 || i === 2) return i; + // dp[i] の記録があれば、それをそのまま返す + if (mem[i] != -1) return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1, mem) + dfs(i - 2, mem); + // dp[i] を記録する + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* 階段登り:メモ化探索 */ + function climbingStairsDFSMem(n) { + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す + const mem = new Array(n + 1).fill(-1); + return dfs(n, mem); + } ``` === "TS" ```typescript title="climbing_stairs_dfs_mem.ts" - [class]{}-[func]{dfs} + /* メモ化探索 */ + function dfs(i: number, mem: number[]): number { + // dp[1] と dp[2] は既知なので返す + if (i === 1 || i === 2) return i; + // dp[i] の記録があれば、それをそのまま返す + if (mem[i] != -1) return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1, mem) + dfs(i - 2, mem); + // dp[i] を記録する + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* 階段登り:メモ化探索 */ + function climbingStairsDFSMem(n: number): number { + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す + const mem = new Array(n + 1).fill(-1); + return dfs(n, mem); + } ``` === "Dart" ```dart title="climbing_stairs_dfs_mem.dart" - [class]{}-[func]{dfs} + /* メモ化探索 */ + int dfs(int i, List mem) { + // dp[1] と dp[2] は既知なので返す + if (i == 1 || i == 2) return i; + // dp[i] の記録があれば、それをそのまま返す + if (mem[i] != -1) return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1, mem) + dfs(i - 2, mem); + // dp[i] を記録する + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* 階段登り:メモ化探索 */ + int climbingStairsDFSMem(int n) { + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す + List mem = List.filled(n + 1, -1); + return dfs(n, mem); + } ``` === "Rust" ```rust title="climbing_stairs_dfs_mem.rs" - [class]{}-[func]{dfs} + /* メモ化探索 */ + fn dfs(i: usize, mem: &mut [i32]) -> i32 { + // dp[1] と dp[2] は既知なので返す + if i == 1 || i == 2 { + return i as i32; + } + // dp[i] の記録があれば、それをそのまま返す + if mem[i] != -1 { + return mem[i]; + } + // dp[i] = dp[i-1] + dp[i-2] + let count = dfs(i - 1, mem) + dfs(i - 2, mem); + // dp[i] を記録する + mem[i] = count; + count + } - [class]{}-[func]{climbing_stairs_dfs_mem} + /* 階段登り:メモ化探索 */ + fn climbing_stairs_dfs_mem(n: usize) -> i32 { + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す + let mut mem = vec![-1; n + 1]; + dfs(n, &mut mem) + } ``` === "C" ```c title="climbing_stairs_dfs_mem.c" - [class]{}-[func]{dfs} + /* メモ化探索 */ + int dfs(int i, int *mem) { + // dp[1] と dp[2] は既知なので返す + if (i == 1 || i == 2) + return i; + // dp[i] の記録があれば、それをそのまま返す + if (mem[i] != -1) + return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1, mem) + dfs(i - 2, mem); + // dp[i] を記録する + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* 階段登り:メモ化探索 */ + int climbingStairsDFSMem(int n) { + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す + int *mem = (int *)malloc((n + 1) * sizeof(int)); + for (int i = 0; i <= n; i++) { + mem[i] = -1; + } + int result = dfs(n, mem); + free(mem); + return result; + } ``` === "Kotlin" ```kotlin title="climbing_stairs_dfs_mem.kt" - [class]{}-[func]{dfs} + /* メモ化探索 */ + fun dfs(i: Int, mem: IntArray): Int { + // dp[1] と dp[2] は既知なので返す + if (i == 1 || i == 2) return i + // dp[i] の記録があれば、それをそのまま返す + if (mem[i] != -1) return mem[i] + // dp[i] = dp[i-1] + dp[i-2] + val count = dfs(i - 1, mem) + dfs(i - 2, mem) + // dp[i] を記録する + mem[i] = count + return count + } - [class]{}-[func]{climbingStairsDFSMem} + /* 階段登り:メモ化探索 */ + fun climbingStairsDFSMem(n: Int): Int { + // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す + val mem = IntArray(n + 1) + mem.fill(-1) + return dfs(n, mem) + } ``` === "Ruby" ```ruby title="climbing_stairs_dfs_mem.rb" - [class]{}-[func]{dfs} + ### メモ化探索 ### + def dfs(i, mem) + # dp[1] と dp[2] は既知なので返す + return i if i == 1 || i == 2 + # dp[i] の記録があれば、それをそのまま返す + return mem[i] if mem[i] != -1 - [class]{}-[func]{climbing_stairs_dfs_mem} + # dp[i] = dp[i-1] + dp[i-2] + count = dfs(i - 1, mem) + dfs(i - 2, mem) + # dp[i] を記録する + mem[i] = count + end + + ### 階段登り:メモ化探索 ### + def climbing_stairs_dfs_mem(n) + # mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す + mem = Array.new(n + 1, -1) + dfs(n, mem) + end ``` -下の図を観察すると、**メモ化後、すべての重複する部分問題は一度だけ計算される必要があり、時間計算量を $O(n)$ に最適化**します。これは大幅な改善です。 +??? pythontutor "コードの可視化" -![メモ化探索による再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png){ class="animation-figure" } +
+ -

図 14-4   メモ化探索による再帰木

+次の図を見ると、**メモ化を行うことで、すべての重複部分問題は 1 回だけ計算すればよくなり、時間計算量は $O(n)$ まで改善されます**。これは大きな飛躍です。 -## 14.1.3   方法3:動的プログラミング +![メモ化探索に対応する再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png){ class="animation-figure" } -**メモ化探索は「トップダウン」方式**です:元の問題(根ノード)から始めて、より大きな部分問題をより小さなものに再帰的に分解し、最小の既知の部分問題(葉ノード)の解に到達するまで続けます。その後、バックトラッキングにより部分問題の解を収集し、元の問題の解を構築します。 +

図 14-4   メモ化探索に対応する再帰木

-一方、**動的プログラミングは「ボトムアップ」方式**です:最小の部分問題の解から始めて、元の問題が解決されるまで、より大きな部分問題の解を反復的に構築します。 +## 14.1.3   方法 3:動的計画法 -動的プログラミングはバックトラッキングを必要としないため、ループを使った反復のみが必要で、再帰は不要です。以下のコードでは、配列 `dp` を初期化して部分問題の解を保存し、メモ化探索の配列 `mem` と同じ記録機能を果たします: +**メモ化探索は「トップダウン」の方法**です。元の問題(根ノード)から始めて、より大きな部分問題を再帰的により小さな部分問題へ分解し、解が既知である最小部分問題(葉ノード)に至ります。その後、バックトラックしながら各層で部分問題の解を集め、元の問題の解を構築します。 + +これとは対照的に、**動的計画法は「ボトムアップ」の方法**です。最小部分問題の解から始めて、より大きな部分問題の解を反復的に構築し、最終的に元の問題の解を得ます。 + +動的計画法にはバックトラックの過程が含まれないため、再帰を使う必要はなく、ループによる反復だけで実装できます。次のコードでは、部分問題の解を保存する配列 `dp` を初期化しており、これはメモ化探索における配列 `mem` と同じ記録の役割を果たします: === "Python" ```python title="climbing_stairs_dp.py" def climbing_stairs_dp(n: int) -> int: - """階段登り:動的プログラミング""" + """階段登り:動的計画法""" if n == 1 or n == 2: return n - # dp テーブルを初期化、部分問題の解を格納するため使用 + # 部分問題の解を保存するために dp テーブルを初期化 dp = [0] * (n + 1) - # 初期状態:最小の部分問題の解を事前設定 + # 初期状態:最小部分問題の解をあらかじめ設定 dp[1], dp[2] = 1, 2 - # 状態遷移:小さい部分問題から大きい部分問題を段階的に解く + # 状態遷移:小さい部分問題から大きい部分問題へ順に解く for i in range(3, n + 1): dp[i] = dp[i - 1] + dp[i - 2] return dp[n] @@ -558,16 +1101,16 @@ $$ === "C++" ```cpp title="climbing_stairs_dp.cpp" - /* 階段登り:動的プログラミング */ + /* 階段登り:動的計画法 */ int climbingStairsDP(int n) { if (n == 1 || n == 2) return n; - // DPテーブルを初期化し、部分問題の解を格納するために使用 + // 部分問題の解を保存するために dp テーブルを初期化 vector dp(n + 1); - // 初期状態:最小の部分問題の解を事前設定 + // 初期状態:最小部分問題の解をあらかじめ設定 dp[1] = 1; dp[2] = 2; - // 状態遷移:小さな問題から大きな部分問題を段階的に解く + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く for (int i = 3; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } @@ -578,16 +1121,16 @@ $$ === "Java" ```java title="climbing_stairs_dp.java" - /* 階段登り:動的プログラミング */ + /* 階段登り:動的計画法 */ int climbingStairsDP(int n) { if (n == 1 || n == 2) return n; - // DPテーブルを初期化し、部分問題の解を格納するために使用 + // 部分問題の解を保存するために dp テーブルを初期化 int[] dp = new int[n + 1]; - // 初期状態:最小の部分問題の解を事前設定 + // 初期状態:最小部分問題の解をあらかじめ設定 dp[1] = 1; dp[2] = 2; - // 状態遷移:小さな問題から大きな部分問題を段階的に解く + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く for (int i = 3; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } @@ -598,86 +1141,231 @@ $$ === "C#" ```csharp title="climbing_stairs_dp.cs" - [class]{climbing_stairs_dp}-[func]{ClimbingStairsDP} + /* 階段登り:動的計画法 */ + int ClimbingStairsDP(int n) { + if (n == 1 || n == 2) + return n; + // 部分問題の解を保存するために dp テーブルを初期化 + int[] dp = new int[n + 1]; + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = 1; + dp[2] = 2; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } ``` === "Go" ```go title="climbing_stairs_dp.go" - [class]{}-[func]{climbingStairsDP} + /* 階段登り:動的計画法 */ + func climbingStairsDP(n int) int { + if n == 1 || n == 2 { + return n + } + // 部分問題の解を保存するために dp テーブルを初期化 + dp := make([]int, n+1) + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = 1 + dp[2] = 2 + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i := 3; i <= n; i++ { + dp[i] = dp[i-1] + dp[i-2] + } + return dp[n] + } ``` === "Swift" ```swift title="climbing_stairs_dp.swift" - [class]{}-[func]{climbingStairsDP} + /* 階段登り:動的計画法 */ + func climbingStairsDP(n: Int) -> Int { + if n == 1 || n == 2 { + return n + } + // 部分問題の解を保存するために dp テーブルを初期化 + var dp = Array(repeating: 0, count: n + 1) + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = 1 + dp[2] = 2 + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i in 3 ... n { + dp[i] = dp[i - 1] + dp[i - 2] + } + return dp[n] + } ``` === "JS" ```javascript title="climbing_stairs_dp.js" - [class]{}-[func]{climbingStairsDP} + /* 階段登り:動的計画法 */ + function climbingStairsDP(n) { + if (n === 1 || n === 2) return n; + // 部分問題の解を保存するために dp テーブルを初期化 + const dp = new Array(n + 1).fill(-1); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = 1; + dp[2] = 2; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (let i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } ``` === "TS" ```typescript title="climbing_stairs_dp.ts" - [class]{}-[func]{climbingStairsDP} + /* 階段登り:動的計画法 */ + function climbingStairsDP(n: number): number { + if (n === 1 || n === 2) return n; + // 部分問題の解を保存するために dp テーブルを初期化 + const dp = new Array(n + 1).fill(-1); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = 1; + dp[2] = 2; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (let i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } ``` === "Dart" ```dart title="climbing_stairs_dp.dart" - [class]{}-[func]{climbingStairsDP} + /* 階段登り:動的計画法 */ + int climbingStairsDP(int n) { + if (n == 1 || n == 2) return n; + // 部分問題の解を保存するために dp テーブルを初期化 + List dp = List.filled(n + 1, 0); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = 1; + dp[2] = 2; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } ``` === "Rust" ```rust title="climbing_stairs_dp.rs" - [class]{}-[func]{climbing_stairs_dp} + /* 階段登り:動的計画法 */ + fn climbing_stairs_dp(n: usize) -> i32 { + // dp[1] と dp[2] は既知なので返す + if n == 1 || n == 2 { + return n as i32; + } + // 部分問題の解を保存するために dp テーブルを初期化 + let mut dp = vec![-1; n + 1]; + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = 1; + dp[2] = 2; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i in 3..=n { + dp[i] = dp[i - 1] + dp[i - 2]; + } + dp[n] + } ``` === "C" ```c title="climbing_stairs_dp.c" - [class]{}-[func]{climbingStairsDP} + /* 階段登り:動的計画法 */ + int climbingStairsDP(int n) { + if (n == 1 || n == 2) + return n; + // 部分問題の解を保存するために dp テーブルを初期化 + int *dp = (int *)malloc((n + 1) * sizeof(int)); + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = 1; + dp[2] = 2; + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + int result = dp[n]; + free(dp); + return result; + } ``` === "Kotlin" ```kotlin title="climbing_stairs_dp.kt" - [class]{}-[func]{climbingStairsDP} + /* 階段登り:動的計画法 */ + fun climbingStairsDP(n: Int): Int { + if (n == 1 || n == 2) return n + // 部分問題の解を保存するために dp テーブルを初期化 + val dp = IntArray(n + 1) + // 初期状態:最小部分問題の解をあらかじめ設定 + dp[1] = 1 + dp[2] = 2 + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for (i in 3..n) { + dp[i] = dp[i - 1] + dp[i - 2] + } + return dp[n] + } ``` === "Ruby" ```ruby title="climbing_stairs_dp.rb" - [class]{}-[func]{climbing_stairs_dp} + ### 階段登り:動的計画法 ### + def climbing_stairs_dp(n) + return n if n == 1 || n == 2 + + # 部分問題の解を保存するために dp テーブルを初期化 + dp = Array.new(n + 1, 0) + # 初期状態:最小部分問題の解をあらかじめ設定 + dp[1], dp[2] = 1, 2 + # 状態遷移:小さい部分問題から大きい部分問題へ順に解く + (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] } + + dp[n] + end ``` -下の図は上記コードの実行プロセスをシミュレートしています。 +??? pythontutor "コードの可視化" -![階段登りの動的プログラミングプロセス](intro_to_dynamic_programming.assets/climbing_stairs_dp.png){ class="animation-figure" } +
+ -

図 14-5   階段登りの動的プログラミングプロセス

+次の図は、以上のコードの実行過程をシミュレートしたものです。 -バックトラッキングアルゴリズムと同様に、動的プログラミングも「状態」の概念を使用して問題解決の特定の段階を表現し、各状態は部分問題とその局所最適解に対応します。例えば、階段登り問題の状態は現在のステップ番号 $i$ として定義されます。 +![階段上りの動的計画法の過程](intro_to_dynamic_programming.assets/climbing_stairs_dp.png){ class="animation-figure" } -上記の内容に基づいて、動的プログラミングでよく使用される用語をまとめることができます。 +

図 14-5   階段上りの動的計画法の過程

-- 配列 `dp` はDPテーブルと呼ばれ、$dp[i]$ は状態 $i$ に対応する部分問題の解を表します。 -- 最小の部分問題(ステップ $1$ と $2$)に対応する状態は初期状態と呼ばれます。 -- 再帰式 $dp[i] = dp[i-1] + dp[i-2]$ は状態遷移方程式と呼ばれます。 +バックトラッキング法と同様に、動的計画法でも問題解決の特定段階を表すために「状態」という概念を用います。各状態は 1 つの部分問題と、それに対応する局所最適解に対応します。たとえば、階段を上る問題では、状態は現在いる階段の段数 $i$ と定義されます。 + +以上を踏まえると、動的計画法のよく使われる用語を次のようにまとめられます。 + +- 配列 `dp` を dp テーブル と呼び、$dp[i]$ は状態 $i$ に対応する部分問題の解を表します。 +- 最小部分問題に対応する状態(第 $1$ 段目と第 $2$ 段目の階段)を初期状態と呼びます。 +- 漸化式 $dp[i] = dp[i-1] + dp[i-2]$ を状態遷移方程式と呼びます。 ## 14.1.4   空間最適化 -注意深い読者は**$dp[i]$ は $dp[i-1]$ と $dp[i-2]$ のみに関連するため、すべての部分問題の解を保存するために配列 `dp` を使用する必要がない**ことに気づくでしょう。単に2つの変数を使って反復的に進めることができます。コードは以下の通りです: +注意深い読者は気づいたかもしれません。**$dp[i]$ は $dp[i-1]$ と $dp[i-2]$ にしか依存しないため、すべての部分問題の解を保存するために配列 `dp` を使う必要はありません**。2 つの変数を順に更新していくだけで十分です。コードは次のとおりです: === "Python" ```python title="climbing_stairs_dp.py" def climbing_stairs_dp_comp(n: int) -> int: - """階段登り:空間最適化動的プログラミング""" + """階段登り:空間最適化した動的計画法""" if n == 1 or n == 2: return n a, b = 1, 2 @@ -689,7 +1377,7 @@ $$ === "C++" ```cpp title="climbing_stairs_dp.cpp" - /* 階段登り:空間最適化動的プログラミング */ + /* 階段登り:空間最適化した動的計画法 */ int climbingStairsDPComp(int n) { if (n == 1 || n == 2) return n; @@ -706,7 +1394,7 @@ $$ === "Java" ```java title="climbing_stairs_dp.java" - /* 階段登り:空間最適化動的プログラミング */ + /* 階段登り:空間最適化した動的計画法 */ int climbingStairsDPComp(int n) { if (n == 1 || n == 2) return n; @@ -723,63 +1411,175 @@ $$ === "C#" ```csharp title="climbing_stairs_dp.cs" - [class]{climbing_stairs_dp}-[func]{ClimbingStairsDPComp} + /* 階段登り:空間最適化した動的計画法 */ + int ClimbingStairsDPComp(int n) { + if (n == 1 || n == 2) + return n; + int a = 1, b = 2; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = a + b; + a = tmp; + } + return b; + } ``` === "Go" ```go title="climbing_stairs_dp.go" - [class]{}-[func]{climbingStairsDPComp} + /* 階段登り:空間最適化した動的計画法 */ + func climbingStairsDPComp(n int) int { + if n == 1 || n == 2 { + return n + } + a, b := 1, 2 + // 状態遷移:小さい部分問題から大きい部分問題へ順に解く + for i := 3; i <= n; i++ { + a, b = b, a+b + } + return b + } ``` === "Swift" ```swift title="climbing_stairs_dp.swift" - [class]{}-[func]{climbingStairsDPComp} + /* 階段登り:空間最適化した動的計画法 */ + func climbingStairsDPComp(n: Int) -> Int { + if n == 1 || n == 2 { + return n + } + var a = 1 + var b = 2 + for _ in 3 ... n { + (a, b) = (b, a + b) + } + return b + } ``` === "JS" ```javascript title="climbing_stairs_dp.js" - [class]{}-[func]{climbingStairsDPComp} + /* 階段登り:空間最適化した動的計画法 */ + function climbingStairsDPComp(n) { + if (n === 1 || n === 2) return n; + let a = 1, + b = 2; + for (let i = 3; i <= n; i++) { + const tmp = b; + b = a + b; + a = tmp; + } + return b; + } ``` === "TS" ```typescript title="climbing_stairs_dp.ts" - [class]{}-[func]{climbingStairsDPComp} + /* 階段登り:空間最適化した動的計画法 */ + function climbingStairsDPComp(n: number): number { + if (n === 1 || n === 2) return n; + let a = 1, + b = 2; + for (let i = 3; i <= n; i++) { + const tmp = b; + b = a + b; + a = tmp; + } + return b; + } ``` === "Dart" ```dart title="climbing_stairs_dp.dart" - [class]{}-[func]{climbingStairsDPComp} + /* 階段登り:空間最適化した動的計画法 */ + int climbingStairsDPComp(int n) { + if (n == 1 || n == 2) return n; + int a = 1, b = 2; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = a + b; + a = tmp; + } + return b; + } ``` === "Rust" ```rust title="climbing_stairs_dp.rs" - [class]{}-[func]{climbing_stairs_dp_comp} + /* 階段登り:空間最適化した動的計画法 */ + fn climbing_stairs_dp_comp(n: usize) -> i32 { + if n == 1 || n == 2 { + return n as i32; + } + let (mut a, mut b) = (1, 2); + for _ in 3..=n { + let tmp = b; + b = a + b; + a = tmp; + } + b + } ``` === "C" ```c title="climbing_stairs_dp.c" - [class]{}-[func]{climbingStairsDPComp} + /* 階段登り:空間最適化した動的計画法 */ + int climbingStairsDPComp(int n) { + if (n == 1 || n == 2) + return n; + int a = 1, b = 2; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = a + b; + a = tmp; + } + return b; + } ``` === "Kotlin" ```kotlin title="climbing_stairs_dp.kt" - [class]{}-[func]{climbingStairsDPComp} + /* 階段登り:空間最適化した動的計画法 */ + fun climbingStairsDPComp(n: Int): Int { + if (n == 1 || n == 2) return n + var a = 1 + var b = 2 + for (i in 3..n) { + val temp = b + b += a + a = temp + } + return b + } ``` === "Ruby" ```ruby title="climbing_stairs_dp.rb" - [class]{}-[func]{climbing_stairs_dp_comp} + ### 階段登り:空間最適化後の動的計画法 ### + def climbing_stairs_dp_comp(n) + return n if n == 1 || n == 2 + + a, b = 1, 2 + (3...(n + 1)).each { a, b = b, a + b } + + b + end ``` -上記のコードを観察すると、配列 `dp` が占有していた空間が削除されるため、空間計算量は $O(n)$ から $O(1)$ に削減されます。 +??? pythontutor "コードの可視化" -多くの動的プログラミング問題では、現在の状態は限られた数の前の状態のみに依存するため、必要な状態のみを保持し、「次元削減」によってメモリ空間を節約できます。**この空間最適化技術は「ローリング変数」または「ローリング配列」として知られています**。 +
+ + +上のコードを見ると、配列 `dp` が占めていた領域を省けるため、空間計算量は $O(n)$ から $O(1)$ へと下がります。 + +動的計画法の問題では、現在の状態はしばしば直前の限られた個数の状態にしか関係しません。このような場合は、必要な状態だけを保持し、「次元削減」によってメモリ空間を節約できます。**この空間最適化の技巧は「ローリング変数」または「ローリング配列」と呼ばれます**。 diff --git a/ja/docs/chapter_dynamic_programming/knapsack_problem.md b/ja/docs/chapter_dynamic_programming/knapsack_problem.md index 9286f8a59..af1d99184 100644 --- a/ja/docs/chapter_dynamic_programming/knapsack_problem.md +++ b/ja/docs/chapter_dynamic_programming/knapsack_problem.md @@ -2,101 +2,101 @@ comments: true --- -# 14.4   0-1ナップサック問題 +# 14.4   0-1 ナップサック問題 -ナップサック問題は動的プログラミングの優れた入門問題であり、動的プログラミングで最も一般的な問題タイプです。0-1ナップサック問題、無制限ナップサック問題、複数ナップサック問題など、多くの変種があります。 +ナップサック問題は、動的計画法の入門として非常に適した問題であり、動的計画法で最もよく見られる問題形式の1つです。これには 0-1 ナップサック問題、完全ナップサック問題、多重ナップサック問題など、多くの派生があります。 -このセクションでは、まず最も一般的な0-1ナップサック問題を解決します。 +本節では、まず最も一般的な 0-1 ナップサック問題を解いていきます。 !!! question - $n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重量は $wgt[i-1]$ で値は $val[i-1]$ です。容量が $cap$ のナップサックがあります。各アイテムは1回のみ選択できます。容量制限下でナップサックに入れることができるアイテムの最大値は何ですか? + $n$ 個の品物が与えられ、$i$ 番目の品物の重さは $wgt[i-1]$、価値は $val[i-1]$ であり、容量 $cap$ のナップサックがあります。各品物は1回しか選べないとき、ナップサック容量の制約下で入れられる品物の最大価値を求めてください。 -下の図を観察すると、アイテム番号 $i$ は1から数え始め、配列インデックスは0から始まるため、アイテム $i$ の重量は $wgt[i-1]$ に対応し、値は $val[i-1]$ に対応します。 +以下の図を見てみましょう。品物番号 $i$ は $1$ から始まり、配列のインデックスは $0$ から始まるため、品物 $i$ は重さ $wgt[i-1]$、価値 $val[i-1]$ に対応します。 -![0-1ナップサックの例データ](knapsack_problem.assets/knapsack_example.png){ class="animation-figure" } +![0-1 ナップサックのサンプルデータ](knapsack_problem.assets/knapsack_example.png){ class="animation-figure" } -

図 14-17   0-1ナップサックの例データ

+

図 14-17   0-1 ナップサックのサンプルデータ

-0-1ナップサック問題を $n$ ラウンドの決定から構成されるプロセスとして考えることができます。各アイテムについて入れない、または入れるという2つの決定があり、したがって問題は決定木モデルに適合します。 +0-1 ナップサック問題は、$n$ 回の意思決定からなる過程とみなせます。各品物について「入れない」「入れる」という2つの選択肢があるため、この問題は決定木モデルを満たします。 -この問題の目的は「限られた容量の下でナップサックに入れることができるアイテムの値を最大化する」ことであり、動的プログラミング問題である可能性が高いです。 +この問題の目的は「ナップサック容量の制約下で入れられる品物の最大価値」を求めることなので、動的計画法の問題である可能性が高いです。 -**第1ステップ:各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る** +**ステップ1:各ラウンドの選択を考え、状態を定義して、$dp$ テーブルを得る** -各アイテムについて、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。これから状態定義を得ることができます:現在のアイテム番号 $i$ とナップサック容量 $c$、$[i, c]$ と表記されます。 +各品物について、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。ここから状態を、現在の品物番号 $i$ とナップサック容量 $c$ として定義し、$[i, c]$ と表せます。 -状態 $[i, c]$ は部分問題に対応します:**容量 $c$ のナップサックでの最初の $i$ 個のアイテムの最大値**、$dp[i, c]$ と表記されます。 +状態 $[i, c]$ に対応する部分問題は、**先頭 $i$ 個の品物を容量 $c$ のナップサックに入れるときの最大価値** であり、これを $dp[i, c]$ と記します。 -探している解は $dp[n, cap]$ であるため、サイズ $(n+1) \times (cap+1)$ の二次元 $dp$ テーブルが必要です。 +求めるべきものは $dp[n, cap]$ なので、サイズ $(n+1) \times (cap+1)$ の2次元 $dp$ テーブルが必要です。 -**第2ステップ:最適部分構造を特定し、状態遷移方程式を導出する** +**ステップ2:最適部分構造を見つけ、状態遷移方程式を導く** -アイテム $i$ の決定を行った後、残るのは最初の $i-1$ 個のアイテムの決定の部分問題であり、これは2つのケースに分けることができます。 +品物 $i$ に対する選択を行った後に残るのは、先頭 $i-1$ 個の品物に対する部分問題であり、次の2つのケースに分けられます。 -- **アイテム $i$ を入れない**:ナップサック容量は変わらず、状態は $[i-1, c]$ に変わります。 -- **アイテム $i$ を入れる**:ナップサック容量は $wgt[i-1]$ だけ減少し、値は $val[i-1]$ だけ増加し、状態は $[i-1, c-wgt[i-1]]$ に変わります。 +- **品物 $i$ を入れない** :ナップサック容量は変わらず、状態は $[i-1, c]$ に変化します。 +- **品物 $i$ を入れる** :ナップサック容量は $wgt[i-1]$ だけ減少し、価値は $val[i-1]$ だけ増加して、状態は $[i-1, c-wgt[i-1]]$ に変化します。 -上記の分析により、この問題の最適部分構造が明らかになります:**最大値 $dp[i, c]$ は、アイテム $i$ を入れない方案とアイテム $i$ を入れる方案の2つのうち、より大きな値に等しい**。これから状態遷移方程式を導出できます: +以上の分析から、この問題の最適部分構造が分かります。すなわち、**最大価値 $dp[i, c]$ は、品物 $i$ を入れない場合と入れる場合のうち、より価値の大きい方に等しい** ということです。これにより、次の状態遷移方程式を導けます。 $$ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) $$ -現在のアイテムの重量 $wgt[i - 1]$ が残りのナップサック容量 $c$ を超える場合、唯一の選択肢はナップサックに入れないことであることに注意することが重要です。 +注意すべき点として、現在の品物の重さ $wgt[i - 1]$ が残りのナップサック容量 $c$ を超える場合は、入れない選択しかできません。 -**第3ステップ:境界条件と状態遷移の順序を決定する** +**ステップ3:境界条件と状態遷移の順序を決める** -アイテムがない場合またはナップサック容量が $0$ の場合、最大値は $0$ です。つまり、最初の列 $dp[i, 0]$ と最初の行 $dp[0, c]$ はどちらも $0$ に等しいです。 +品物がない場合、またはナップサック容量が $0$ の場合、最大価値は $0$ です。すなわち、先頭列 $dp[i, 0]$ と先頭行 $dp[0, c]$ はいずれも $0$ になります。 -現在の状態 $[i, c]$ は直接上の状態 $[i-1, c]$ と左上の状態 $[i-1, c-wgt[i-1]]$ から遷移するため、2層のループを通じて $dp$ テーブル全体を順序通りに走査します。 +現在の状態 $[i, c]$ は、上側の状態 $[i-1, c]$ と左上の状態 $[i-1, c-wgt[i-1]]$ から遷移してくるため、2重ループで $dp$ テーブル全体を順方向に走査すれば十分です。 -上記の分析に従って、次に力任せ探索、メモ化探索、動的プログラミングの順序で解法を実装します。 +以上の分析に基づき、次に全探索、メモ化探索、動的計画法の順で実装していきます。 -### 1.   方法1:力任せ探索 +### 1.   方法1:全探索 -探索コードには以下の要素が含まれます。 +探索コードには次の要素が含まれます。 -- **再帰パラメータ**:状態 $[i, c]$。 -- **戻り値**:部分問題 $dp[i, c]$ の解。 -- **終了条件**:アイテム番号が範囲外 $i = 0$ またはナップサックの残り容量が $0$ のとき、再帰を終了し値 $0$ を返す。 -- **枝刈り**:現在のアイテムの重量がナップサックの残り容量を超える場合、唯一の選択肢はナップサックに入れないことです。 +- **再帰引数**:状態 $[i, c]$ です。 +- **戻り値**:部分問題の解 $dp[i, c]$ です。 +- **終了条件**:品物番号が範囲外である $i = 0$、またはナップサックの残り容量が $0$ のとき、再帰を終了して価値 $0$ を返します。 +- **枝刈り**:現在の品物の重さがナップサックの残り容量を超える場合、入れない選択しかできません。 === "Python" ```python title="knapsack.py" def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int: - """0-1 ナップサック:ブルートフォース探索""" - # すべてのアイテムが選択されたかナップサックに残り容量がない場合、値 0 を返す + """0-1 ナップサック:総当たり探索""" + # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す if i == 0 or c == 0: return 0 - # ナップサック容量を超える場合、ナップサックに入れないことしか選択できない + # ナップサック容量を超える場合は、入れない選択しかできない if wgt[i - 1] > c: return knapsack_dfs(wgt, val, i - 1, c) - # アイテム i を入れないのと入れるのとの最大値を計算 + # 品物 i を入れない場合と入れる場合の最大価値を計算する no = knapsack_dfs(wgt, val, i - 1, c) yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1] - # 2 つの選択肢のうち大きい値を返す + # 2つの案のうち価値が大きいほうを返す return max(no, yes) ``` === "C++" ```cpp title="knapsack.cpp" - /* 0-1 ナップサック:ブルートフォース探索 */ + /* 0-1 ナップサック:総当たり探索 */ int knapsackDFS(vector &wgt, vector &val, int i, int c) { - // すべてのアイテムが選択されたか、ナップサックに残り容量がない場合、値 0 を返す + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す if (i == 0 || c == 0) { return 0; } - // ナップサックの容量を超える場合、ナップサックに入れないことしか選択できない + // ナップサック容量を超える場合は、入れない選択しかできない if (wgt[i - 1] > c) { return knapsackDFS(wgt, val, i - 1, c); } - // アイテム i を入れない場合と入れる場合の最大値を計算 + // 品物 i を入れない場合と入れる場合の最大価値を計算する int no = knapsackDFS(wgt, val, i - 1, c); int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; - // 2つの選択肢のより大きい値を返す + // 2つの案のうち価値が大きいほうを返す return max(no, yes); } ``` @@ -104,20 +104,20 @@ $$ === "Java" ```java title="knapsack.java" - /* 0-1 ナップサック:ブルートフォース探索 */ + /* 0-1 ナップサック:総当たり探索 */ int knapsackDFS(int[] wgt, int[] val, int i, int c) { - // すべてのアイテムが選択されたか、ナップサックに残り容量がない場合、値 0 を返す + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す if (i == 0 || c == 0) { return 0; } - // ナップサックの容量を超える場合、ナップサックに入れないことしか選択できない + // ナップサック容量を超える場合は、入れない選択しかできない if (wgt[i - 1] > c) { return knapsackDFS(wgt, val, i - 1, c); } - // アイテム i を入れない場合と入れる場合の最大値を計算 + // 品物 i を入れない場合と入れる場合の最大価値を計算する int no = knapsackDFS(wgt, val, i - 1, c); int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; - // 2つの選択肢のより大きい値を返す + // 2つの案のうち価値が大きいほうを返す return Math.max(no, yes); } ``` @@ -125,76 +125,237 @@ $$ === "C#" ```csharp title="knapsack.cs" - [class]{knapsack}-[func]{KnapsackDFS} + /* 0-1 ナップサック:総当たり探索 */ + int KnapsackDFS(int[] weight, int[] val, int i, int c) { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i == 0 || c == 0) { + return 0; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (weight[i - 1] > c) { + return KnapsackDFS(weight, val, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + int no = KnapsackDFS(weight, val, i - 1, c); + int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1]; + // 2つの案のうち価値が大きいほうを返す + return Math.Max(no, yes); + } ``` === "Go" ```go title="knapsack.go" - [class]{}-[func]{knapsackDFS} + /* 0-1 ナップサック:総当たり探索 */ + func knapsackDFS(wgt, val []int, i, c int) int { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if i == 0 || c == 0 { + return 0 + } + // ナップサック容量を超える場合は、入れない選択しかできない + if wgt[i-1] > c { + return knapsackDFS(wgt, val, i-1, c) + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + no := knapsackDFS(wgt, val, i-1, c) + yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1] + // 2つの案のうち価値が大きいほうを返す + return int(math.Max(float64(no), float64(yes))) + } ``` === "Swift" ```swift title="knapsack.swift" - [class]{}-[func]{knapsackDFS} + /* 0-1 ナップサック:総当たり探索 */ + func knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if i == 0 || c == 0 { + return 0 + } + // ナップサック容量を超える場合は、入れない選択しかできない + if wgt[i - 1] > c { + return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c) + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c) + let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1] + // 2つの案のうち価値が大きいほうを返す + return max(no, yes) + } ``` === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDFS} + /* 0-1 ナップサック:総当たり探索 */ + function knapsackDFS(wgt, val, i, c) { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i === 0 || c === 0) { + return 0; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + const no = knapsackDFS(wgt, val, i - 1, c); + const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 2つの案のうち価値が大きいほうを返す + return Math.max(no, yes); + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDFS} + /* 0-1 ナップサック:総当たり探索 */ + function knapsackDFS( + wgt: Array, + val: Array, + i: number, + c: number + ): number { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i === 0 || c === 0) { + return 0; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + const no = knapsackDFS(wgt, val, i - 1, c); + const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 2つの案のうち価値が大きいほうを返す + return Math.max(no, yes); + } ``` === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDFS} + /* 0-1 ナップサック:総当たり探索 */ + int knapsackDFS(List wgt, List val, int i, int c) { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i == 0 || c == 0) { + return 0; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + int no = knapsackDFS(wgt, val, i - 1, c); + int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 2つの案のうち価値が大きいほうを返す + return max(no, yes); + } ``` === "Rust" ```rust title="knapsack.rs" - [class]{}-[func]{knapsack_dfs} + /* 0-1 ナップサック:総当たり探索 */ + fn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if i == 0 || c == 0 { + return 0; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if wgt[i - 1] > c as i32 { + return knapsack_dfs(wgt, val, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + let no = knapsack_dfs(wgt, val, i - 1, c); + let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1]; + // 2つの案のうち価値が大きいほうを返す + std::cmp::max(no, yes) + } ``` === "C" ```c title="knapsack.c" - [class]{}-[func]{knapsackDFS} + /* 0-1 ナップサック:総当たり探索 */ + int knapsackDFS(int wgt[], int val[], int i, int c) { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i == 0 || c == 0) { + return 0; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + int no = knapsackDFS(wgt, val, i - 1, c); + int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 2つの案のうち価値が大きいほうを返す + return myMax(no, yes); + } ``` === "Kotlin" ```kotlin title="knapsack.kt" - [class]{}-[func]{knapsackDFS} + /* 0-1 ナップサック:総当たり探索 */ + fun knapsackDFS( + wgt: IntArray, + _val: IntArray, + i: Int, + c: Int + ): Int { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i == 0 || c == 0) { + return 0 + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, _val, i - 1, c) + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + val no = knapsackDFS(wgt, _val, i - 1, c) + val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1] + // 2つの案のうち価値が大きいほうを返す + return max(no, yes) + } ``` === "Ruby" ```ruby title="knapsack.rb" - [class]{}-[func]{knapsack_dfs} + ### 0-1 ナップサック: 全探索 ### + def knapsack_dfs(wgt, val, i, c) + # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + return 0 if i == 0 || c == 0 + # ナップサック容量を超える場合は、入れない選択しかできない + return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c + # 品物 i を入れない場合と入れる場合の最大価値を計算する + no = knapsack_dfs(wgt, val, i - 1, c) + yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1] + # 2つの案のうち価値が大きいほうを返す + [no, yes].max + end ``` -下の図に示すように、各アイテムは選択しないと選択するという2つの探索分岐を生成するため、時間計算量は $O(2^n)$ です。 +??? pythontutor "コードの可視化" -再帰木を観察すると、$dp[1, 10]$ などの重複する部分問題があることが容易にわかります。アイテムが多く、ナップサック容量が大きい場合、特に同じ重量のアイテムが多い場合、重複する部分問題の数は大幅に増加します。 +
+ -![0-1ナップサック問題の力任せ探索再帰木](knapsack_problem.assets/knapsack_dfs.png){ class="animation-figure" } +以下の図のように、各品物ごとに「選ばない」「選ぶ」の2つの探索分岐が生じるため、時間計算量は $O(2^n)$ です。 -

図 14-18   0-1ナップサック問題の力任せ探索再帰木

+再帰木を観察すると、$dp[1, 10]$ などの重複部分問題が存在することが分かります。品物数が多く、ナップサック容量が大きく、特に同じ重さの品物が多い場合には、重複部分問題の数は大幅に増加します。 + +![0-1 ナップサック問題の全探索の再帰木](knapsack_problem.assets/knapsack_dfs.png){ class="animation-figure" } + +

図 14-18   0-1 ナップサック問題の全探索の再帰木

### 2.   方法2:メモ化探索 -重複する部分問題が一度だけ計算されることを確保するために、部分問題の解を記録するメモ化リスト `mem` を使用します。ここで `mem[i][c]` は $dp[i, c]$ に対応します。 +重複部分問題が一度だけ計算されるようにするため、メモ配列 `mem` を用いて部分問題の解を記録します。ここで `mem[i][c]` は $dp[i, c]$ に対応します。 -メモ化を導入した後、**時間計算量は部分問題の数に依存**し、$O(n \times cap)$ になります。実装コードは以下の通りです: +メモ化を導入すると、**時間計算量は部分問題の数に依存し**、すなわち $O(n \times cap)$ になります。実装コードは次のとおりです。 === "Python" @@ -202,20 +363,20 @@ $$ def knapsack_dfs_mem( wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int ) -> int: - """0-1 ナップサック:記憶化探索""" - # すべてのアイテムが選択されたかナップサックに残り容量がない場合、値 0 を返す + """0-1 ナップサック:メモ化探索""" + # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す if i == 0 or c == 0: return 0 - # 記録がある場合、それを返す + # 既に記録があればそのまま返す if mem[i][c] != -1: return mem[i][c] - # ナップサック容量を超える場合、ナップサックに入れないことしか選択できない + # ナップサック容量を超える場合は、入れない選択しかできない if wgt[i - 1] > c: return knapsack_dfs_mem(wgt, val, mem, i - 1, c) - # アイテム i を入れないのと入れるのとの最大値を計算 + # 品物 i を入れない場合と入れる場合の最大価値を計算する no = knapsack_dfs_mem(wgt, val, mem, i - 1, c) yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1] - # 2 つの選択肢のうち大きい値を記録して返す + # 2 つの案のうち価値が大きい方を記録して返す mem[i][c] = max(no, yes) return mem[i][c] ``` @@ -223,7 +384,27 @@ $$ === "C++" ```cpp title="knapsack.cpp" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 ナップサック:メモ化探索 */ + int knapsackDFSMem(vector &wgt, vector &val, vector> &mem, int i, int c) { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i == 0 || c == 0) { + return 0; + } + // 既に記録があればそのまま返す + if (mem[i][c] != -1) { + return mem[i][c]; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + int no = knapsackDFSMem(wgt, val, mem, i - 1, c); + int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 2 つの案のうち価値が大きい方を記録して返す + mem[i][c] = max(no, yes); + return mem[i][c]; + } ``` === "Java" @@ -231,22 +412,22 @@ $$ ```java title="knapsack.java" /* 0-1 ナップサック:メモ化探索 */ int knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) { - // すべてのアイテムが選択されたか、ナップサックに残り容量がない場合、値 0 を返す + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す if (i == 0 || c == 0) { return 0; } - // 記録がある場合、それを返す + // 既に記録があればそのまま返す if (mem[i][c] != -1) { return mem[i][c]; } - // ナップサックの容量を超える場合、ナップサックに入れないことしか選択できない + // ナップサック容量を超える場合は、入れない選択しかできない if (wgt[i - 1] > c) { return knapsackDFSMem(wgt, val, mem, i - 1, c); } - // アイテム i を入れない場合と入れる場合の最大値を計算 + // 品物 i を入れない場合と入れる場合の最大価値を計算する int no = knapsackDFSMem(wgt, val, mem, i - 1, c); int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; - // 2つの選択肢のより大きい値を記録して返す + // 2 つの案のうち価値が大きい方を記録して返す mem[i][c] = Math.max(no, yes); return mem[i][c]; } @@ -255,78 +436,296 @@ $$ === "C#" ```csharp title="knapsack.cs" - [class]{knapsack}-[func]{KnapsackDFSMem} + /* 0-1 ナップサック:メモ化探索 */ + int KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i == 0 || c == 0) { + return 0; + } + // 既に記録があればそのまま返す + if (mem[i][c] != -1) { + return mem[i][c]; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (weight[i - 1] > c) { + return KnapsackDFSMem(weight, val, mem, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + int no = KnapsackDFSMem(weight, val, mem, i - 1, c); + int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1]; + // 2 つの案のうち価値が大きい方を記録して返す + mem[i][c] = Math.Max(no, yes); + return mem[i][c]; + } ``` === "Go" ```go title="knapsack.go" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 ナップサック:メモ化探索 */ + func knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if i == 0 || c == 0 { + return 0 + } + // 既に記録があればそのまま返す + if mem[i][c] != -1 { + return mem[i][c] + } + // ナップサック容量を超える場合は、入れない選択しかできない + if wgt[i-1] > c { + return knapsackDFSMem(wgt, val, mem, i-1, c) + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + no := knapsackDFSMem(wgt, val, mem, i-1, c) + yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1] + // 2つの案のうち価値が大きいほうを返す + mem[i][c] = int(math.Max(float64(no), float64(yes))) + return mem[i][c] + } ``` === "Swift" ```swift title="knapsack.swift" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 ナップサック:メモ化探索 */ + func knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if i == 0 || c == 0 { + return 0 + } + // 既に記録があればそのまま返す + if mem[i][c] != -1 { + return mem[i][c] + } + // ナップサック容量を超える場合は、入れない選択しかできない + if wgt[i - 1] > c { + return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c) + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + let no = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c) + let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1] + // 2 つの案のうち価値が大きい方を記録して返す + mem[i][c] = max(no, yes) + return mem[i][c] + } ``` === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 ナップサック:メモ化探索 */ + function knapsackDFSMem(wgt, val, mem, i, c) { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i === 0 || c === 0) { + return 0; + } + // 既に記録があればそのまま返す + if (mem[i][c] !== -1) { + return mem[i][c]; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + const no = knapsackDFSMem(wgt, val, mem, i - 1, c); + const yes = + knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 2 つの案のうち価値が大きい方を記録して返す + mem[i][c] = Math.max(no, yes); + return mem[i][c]; + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 ナップサック:メモ化探索 */ + function knapsackDFSMem( + wgt: Array, + val: Array, + mem: Array>, + i: number, + c: number + ): number { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i === 0 || c === 0) { + return 0; + } + // 既に記録があればそのまま返す + if (mem[i][c] !== -1) { + return mem[i][c]; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + const no = knapsackDFSMem(wgt, val, mem, i - 1, c); + const yes = + knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 2 つの案のうち価値が大きい方を記録して返す + mem[i][c] = Math.max(no, yes); + return mem[i][c]; + } ``` === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 ナップサック:メモ化探索 */ + int knapsackDFSMem( + List wgt, + List val, + List> mem, + int i, + int c, + ) { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i == 0 || c == 0) { + return 0; + } + // 既に記録があればそのまま返す + if (mem[i][c] != -1) { + return mem[i][c]; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + int no = knapsackDFSMem(wgt, val, mem, i - 1, c); + int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 2 つの案のうち価値が大きい方を記録して返す + mem[i][c] = max(no, yes); + return mem[i][c]; + } ``` === "Rust" ```rust title="knapsack.rs" - [class]{}-[func]{knapsack_dfs_mem} + /* 0-1 ナップサック:メモ化探索 */ + fn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec>, i: usize, c: usize) -> i32 { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if i == 0 || c == 0 { + return 0; + } + // 既に記録があればそのまま返す + if mem[i][c] != -1 { + return mem[i][c]; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if wgt[i - 1] > c as i32 { + return knapsack_dfs_mem(wgt, val, mem, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c); + let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1]; + // 2 つの案のうち価値が大きい方を記録して返す + mem[i][c] = std::cmp::max(no, yes); + mem[i][c] + } ``` === "C" ```c title="knapsack.c" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 ナップサック:メモ化探索 */ + int knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i == 0 || c == 0) { + return 0; + } + // 既に記録があればそのまま返す + if (mem[i][c] != -1) { + return mem[i][c]; + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c); + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c); + int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 2 つの案のうち価値が大きい方を記録して返す + mem[i][c] = myMax(no, yes); + return mem[i][c]; + } ``` === "Kotlin" ```kotlin title="knapsack.kt" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 ナップサック:メモ化探索 */ + fun knapsackDFSMem( + wgt: IntArray, + _val: IntArray, + mem: Array, + i: Int, + c: Int + ): Int { + // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + if (i == 0 || c == 0) { + return 0 + } + // 既に記録があればそのまま返す + if (mem[i][c] != -1) { + return mem[i][c] + } + // ナップサック容量を超える場合は、入れない選択しかできない + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, _val, mem, i - 1, c) + } + // 品物 i を入れない場合と入れる場合の最大価値を計算する + val no = knapsackDFSMem(wgt, _val, mem, i - 1, c) + val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1] + // 2 つの案のうち価値が大きい方を記録して返す + mem[i][c] = max(no, yes) + return mem[i][c] + } ``` === "Ruby" ```ruby title="knapsack.rb" - [class]{}-[func]{knapsack_dfs_mem} + ### 0-1 ナップサック: メモ化探索 ### + def knapsack_dfs_mem(wgt, val, mem, i, c) + # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す + return 0 if i == 0 || c == 0 + # 既に記録があればそのまま返す + return mem[i][c] if mem[i][c] != -1 + # ナップサック容量を超える場合は、入れない選択しかできない + return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c + # 品物 i を入れない場合と入れる場合の最大価値を計算する + no = knapsack_dfs_mem(wgt, val, mem, i - 1, c) + yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1] + # 2 つの案のうち価値が大きい方を記録して返す + mem[i][c] = [no, yes].max + end ``` -下の図はメモ化探索で枝刈りされる探索分岐を示しています。 +??? pythontutor "コードの可視化" -![0-1ナップサック問題のメモ化探索再帰木](knapsack_problem.assets/knapsack_dfs_mem.png){ class="animation-figure" } +
+ -

図 14-19   0-1ナップサック問題のメモ化探索再帰木

+次の図は、メモ化探索で剪定された探索分岐を示しています。 -### 3.   方法3:動的プログラミング +![0-1 ナップサック問題のメモ化探索の再帰木](knapsack_problem.assets/knapsack_dfs_mem.png){ class="animation-figure" } -動的プログラミングは本質的に状態遷移中に $dp$ テーブルを埋めることを含みます。コードは下の図に示されています: +

図 14-19   0-1 ナップサック問題のメモ化探索の再帰木

+ +### 3.   方法3:動的計画法 + +動的計画法の本質は、状態遷移に従って $dp$ テーブルを埋めていく過程です。コードは次のようになります。 === "Python" ```python title="knapsack.py" def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int: - """0-1 ナップサック:動的プログラミング""" + """0-1 ナップサック:動的計画法""" n = len(wgt) # dp テーブルを初期化 dp = [[0] * (cap + 1) for _ in range(n + 1)] @@ -334,10 +733,10 @@ $$ for i in range(1, n + 1): for c in range(1, cap + 1): if wgt[i - 1] > c: - # ナップサック容量を超える場合、アイテム i を選択しない + # ナップサック容量を超えるなら品物 i は選ばない dp[i][c] = dp[i - 1][c] else: - # アイテム i を選択しないのと選択するのとで大きい値 + # 品物 i を選ばない場合と選ぶ場合の大きい方 dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]) return dp[n][cap] ``` @@ -345,19 +744,19 @@ $$ === "C++" ```cpp title="knapsack.cpp" - /* 0-1 ナップサック:動的プログラミング */ + /* 0-1 ナップサック:動的計画法 */ int knapsackDP(vector &wgt, vector &val, int cap) { int n = wgt.size(); - // DPテーブルを初期化 + // dp テーブルを初期化 vector> dp(n + 1, vector(cap + 1, 0)); // 状態遷移 for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // ナップサックの容量を超える場合、アイテム i を選択しない + // ナップサック容量を超えるなら品物 i は選ばない dp[i][c] = dp[i - 1][c]; } else { - // 選択しない場合とアイテム i を選択する場合のより大きい値 + // 品物 i を選ばない場合と選ぶ場合の大きい方 dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); } } @@ -369,19 +768,19 @@ $$ === "Java" ```java title="knapsack.java" - /* 0-1 ナップサック:動的プログラミング */ + /* 0-1 ナップサック:動的計画法 */ int knapsackDP(int[] wgt, int[] val, int cap) { int n = wgt.length; - // DPテーブルを初期化 + // dp テーブルを初期化 int[][] dp = new int[n + 1][cap + 1]; // 状態遷移 for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // ナップサックの容量を超える場合、アイテム i を選択しない + // ナップサック容量を超えるなら品物 i は選ばない dp[i][c] = dp[i - 1][c]; } else { - // 選択しない場合とアイテム i を選択する場合のより大きい値 + // 品物 i を選ばない場合と選ぶ場合の大きい方 dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); } } @@ -393,67 +792,280 @@ $$ === "C#" ```csharp title="knapsack.cs" - [class]{knapsack}-[func]{KnapsackDP} + /* 0-1 ナップサック:動的計画法 */ + int KnapsackDP(int[] weight, int[] val, int cap) { + int n = weight.Length; + // dp テーブルを初期化 + int[,] dp = new int[n + 1, cap + 1]; + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (weight[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i, c] = dp[i - 1, c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]); + } + } + } + return dp[n, cap]; + } ``` === "Go" ```go title="knapsack.go" - [class]{}-[func]{knapsackDP} + /* 0-1 ナップサック:動的計画法 */ + func knapsackDP(wgt, val []int, cap int) int { + n := len(wgt) + // dp テーブルを初期化 + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, cap+1) + } + // 状態遷移 + for i := 1; i <= n; i++ { + for c := 1; c <= cap; c++ { + if wgt[i-1] > c { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i-1][c] + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1]))) + } + } + } + return dp[n][cap] + } ``` === "Swift" ```swift title="knapsack.swift" - [class]{}-[func]{knapsackDP} + /* 0-1 ナップサック:動的計画法 */ + func knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // dp テーブルを初期化 + var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1) + // 状態遷移 + for i in 1 ... n { + for c in 1 ... cap { + if wgt[i - 1] > c { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c] + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]) + } + } + } + return dp[n][cap] + } ``` === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDP} + /* 0-1 ナップサック:動的計画法 */ + function knapsackDP(wgt, val, cap) { + const n = wgt.length; + // dp テーブルを初期化 + const dp = Array(n + 1) + .fill(0) + .map(() => Array(cap + 1).fill(0)); + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i - 1][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDP} + /* 0-1 ナップサック:動的計画法 */ + function knapsackDP( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // dp テーブルを初期化 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i - 1][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } ``` === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDP} + /* 0-1 ナップサック:動的計画法 */ + int knapsackDP(List wgt, List val, int cap) { + int n = wgt.length; + // dp テーブルを初期化 + List> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0)); + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n][cap]; + } ``` === "Rust" ```rust title="knapsack.rs" - [class]{}-[func]{knapsack_dp} + /* 0-1 ナップサック:動的計画法 */ + fn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // dp テーブルを初期化 + let mut dp = vec![vec![0; cap + 1]; n + 1]; + // 状態遷移 + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = std::cmp::max( + dp[i - 1][c], + dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1], + ); + } + } + } + dp[n][cap] + } ``` === "C" ```c title="knapsack.c" - [class]{}-[func]{knapsackDP} + /* 0-1 ナップサック:動的計画法 */ + int knapsackDP(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // dp テーブルを初期化 + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(cap + 1, sizeof(int)); + } + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[n][cap]; + // メモリを解放する + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + return res; + } ``` === "Kotlin" ```kotlin title="knapsack.kt" - [class]{}-[func]{knapsackDP} + /* 0-1 ナップサック:動的計画法 */ + fun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int { + val n = wgt.size + // dp テーブルを初期化 + val dp = Array(n + 1) { IntArray(cap + 1) } + // 状態遷移 + for (i in 1..n) { + for (c in 1..cap) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c] + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1]) + } + } + } + return dp[n][cap] + } ``` === "Ruby" ```ruby title="knapsack.rb" - [class]{}-[func]{knapsack_dp} + ### 0-1 ナップサック: 動的計画法 ### + def knapsack_dp(wgt, val, cap) + n = wgt.length + # dp テーブルを初期化 + dp = Array.new(n + 1) { Array.new(cap + 1, 0) } + # 状態遷移 + for i in 1...(n + 1) + for c in 1...(cap + 1) + if wgt[i - 1] > c + # ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c] + else + # 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max + end + end + end + dp[n][cap] + end ``` -下の図に示すように、時間計算量と空間計算量の両方が配列 `dp` のサイズ、つまり $O(n \times cap)$ によって決定されます。 +??? pythontutor "コードの可視化" + +
+ + +以下の図のように、時間計算量と空間計算量はいずれも配列 `dp` のサイズによって決まり、$O(n \times cap)$ です。 === "<1>" - ![0-1ナップサック問題の動的プログラミングプロセス](knapsack_problem.assets/knapsack_dp_step1.png){ class="animation-figure" } + ![0-1 ナップサック問題の動的計画法の過程](knapsack_problem.assets/knapsack_dp_step1.png){ class="animation-figure" } === "<2>" ![knapsack_dp_step2](knapsack_problem.assets/knapsack_dp_step2.png){ class="animation-figure" } @@ -494,21 +1106,21 @@ $$ === "<14>" ![knapsack_dp_step14](knapsack_problem.assets/knapsack_dp_step14.png){ class="animation-figure" } -

図 14-20   0-1ナップサック問題の動的プログラミングプロセス

+

図 14-20   0-1 ナップサック問題の動的計画法の過程

### 4.   空間最適化 -各状態は上の行の状態のみに関連するため、2つの配列を使用してローリング前進させ、空間計算量を $O(n^2)$ から $O(n)$ に削減できます。 +各状態は直前の行の状態にしか依存しないため、2つの配列をローテーションして用いることで、空間計算量を $O(n^2)$ から $O(n)$ に削減できます。 -さらに考えてみると、1つの配列だけで空間最適化を達成できるでしょうか?各状態が直接上のセルまたは左上のセルから遷移することが観察できます。配列が1つしかない場合、$i$ 行目の走査を開始するとき、その配列はまだ $i-1$ 行目の状態を保存しています。 +さらに考えると、1つの配列だけで空間最適化を実現できるでしょうか。観察すると、各状態は真上または左上のマスから遷移してきます。配列が1つしかないと仮定すると、$i$ 行目の走査を開始した時点では、その配列にはまだ $i-1$ 行目の状態が格納されています。 -- 通常の順序で走査する場合、$dp[i, j]$ に走査したとき、左上の $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ の値がすでに上書きされている可能性があり、正しい状態遷移結果を得ることができません。 -- 逆順で走査する場合、上書き問題はなく、状態遷移を正しく実行できます。 +- 順方向に走査すると、$dp[i, j]$ に到達した時点で、左上にある $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ の値がすでに上書きされている可能性があり、正しい状態遷移結果を得られません。 +- 逆方向に走査すれば、上書きの問題は発生せず、状態遷移を正しく行えます。 -下の図は単一配列での $i = 1$ 行目から $i = 2$ 行目への遷移プロセスを示しています。通常順序走査と逆順走査の違いについて考えてみてください。 +次の図は、単一配列のもとで $i = 1$ 行目から $i = 2$ 行目へ変換する過程を示しています。順方向走査と逆方向走査の違いを考えてみてください。 === "<1>" - ![0-1ナップサックの空間最適化動的プログラミングプロセス](knapsack_problem.assets/knapsack_dp_comp_step1.png){ class="animation-figure" } + ![0-1 ナップサックの空間最適化後の動的計画法の過程](knapsack_problem.assets/knapsack_dp_comp_step1.png){ class="animation-figure" } === "<2>" ![knapsack_dp_comp_step2](knapsack_problem.assets/knapsack_dp_comp_step2.png){ class="animation-figure" } @@ -525,27 +1137,27 @@ $$ === "<6>" ![knapsack_dp_comp_step6](knapsack_problem.assets/knapsack_dp_comp_step6.png){ class="animation-figure" } -

図 14-21   0-1ナップサックの空間最適化動的プログラミングプロセス

+

図 14-21   0-1 ナップサックの空間最適化後の動的計画法の過程

-コード実装では、配列 `dp` の最初の次元 $i$ を削除し、内側のループを逆走査に変更するだけです: +コード実装では、配列 `dp` の第1次元 $i$ をそのまま削除し、内側のループを逆方向走査に変更するだけで済みます。 === "Python" ```python title="knapsack.py" def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int: - """0-1 ナップサック:空間最適化動的プログラミング""" + """0-1 ナップサック:空間最適化後の動的計画法""" n = len(wgt) # dp テーブルを初期化 dp = [0] * (cap + 1) # 状態遷移 for i in range(1, n + 1): - # 逆順で走査 + # 逆順に走査する for c in range(cap, 0, -1): if wgt[i - 1] > c: - # ナップサック容量を超える場合、アイテム i を選択しない + # ナップサック容量を超えるなら品物 i は選ばない dp[c] = dp[c] else: - # アイテム i を選択しないのと選択するのとで大きい値 + # 品物 i を選ばない場合と選ぶ場合の大きい方 dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) return dp[cap] ``` @@ -553,17 +1165,17 @@ $$ === "C++" ```cpp title="knapsack.cpp" - /* 0-1 ナップサック:空間最適化動的プログラミング */ + /* 0-1 ナップサック:空間最適化後の動的計画法 */ int knapsackDPComp(vector &wgt, vector &val, int cap) { int n = wgt.size(); - // DPテーブルを初期化 + // dp テーブルを初期化 vector dp(cap + 1, 0); // 状態遷移 for (int i = 1; i <= n; i++) { - // 逆順で走査 + // 逆順に走査する for (int c = cap; c >= 1; c--) { if (wgt[i - 1] <= c) { - // 選択しない場合とアイテム i を選択する場合のより大きい値 + // 品物 i を選ばない場合と選ぶ場合の大きい方 dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); } } @@ -575,17 +1187,17 @@ $$ === "Java" ```java title="knapsack.java" - /* 0-1 ナップサック:空間最適化動的プログラミング */ + /* 0-1 ナップサック:空間最適化後の動的計画法 */ int knapsackDPComp(int[] wgt, int[] val, int cap) { int n = wgt.length; - // DPテーブルを初期化 + // dp テーブルを初期化 int[] dp = new int[cap + 1]; // 状態遷移 for (int i = 1; i <= n; i++) { - // 逆順で走査 + // 逆順に走査する for (int c = cap; c >= 1; c--) { if (wgt[i - 1] <= c) { - // 選択しない場合とアイテム i を選択する場合のより大きい値 + // 品物 i を選ばない場合と選ぶ場合の大きい方 dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); } } @@ -597,59 +1209,237 @@ $$ === "C#" ```csharp title="knapsack.cs" - [class]{knapsack}-[func]{KnapsackDPComp} + /* 0-1 ナップサック:空間最適化後の動的計画法 */ + int KnapsackDPComp(int[] weight, int[] val, int cap) { + int n = weight.Length; + // dp テーブルを初期化 + int[] dp = new int[cap + 1]; + // 状態遷移 + for (int i = 1; i <= n; i++) { + // 逆順に走査する + for (int c = cap; c > 0; c--) { + if (weight[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Go" ```go title="knapsack.go" - [class]{}-[func]{knapsackDPComp} + /* 0-1 ナップサック:空間最適化後の動的計画法 */ + func knapsackDPComp(wgt, val []int, cap int) int { + n := len(wgt) + // dp テーブルを初期化 + dp := make([]int, cap+1) + // 状態遷移 + for i := 1; i <= n; i++ { + // 逆順に走査する + for c := cap; c >= 1; c-- { + if wgt[i-1] <= c { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1]))) + } + } + } + return dp[cap] + } ``` === "Swift" ```swift title="knapsack.swift" - [class]{}-[func]{knapsackDPComp} + /* 0-1 ナップサック:空間最適化後の動的計画法 */ + func knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // dp テーブルを初期化 + var dp = Array(repeating: 0, count: cap + 1) + // 状態遷移 + for i in 1 ... n { + // 逆順に走査する + for c in (1 ... cap).reversed() { + if wgt[i - 1] <= c { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) + } + } + } + return dp[cap] + } ``` === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDPComp} + /* 0-1 ナップサック:空間最適化後の動的計画法 */ + function knapsackDPComp(wgt, val, cap) { + const n = wgt.length; + // dp テーブルを初期化 + const dp = Array(cap + 1).fill(0); + // 状態遷移 + for (let i = 1; i <= n; i++) { + // 逆順に走査する + for (let c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDPComp} + /* 0-1 ナップサック:空間最適化後の動的計画法 */ + function knapsackDPComp( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // dp テーブルを初期化 + const dp = Array(cap + 1).fill(0); + // 状態遷移 + for (let i = 1; i <= n; i++) { + // 逆順に走査する + for (let c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDPComp} + /* 0-1 ナップサック:空間最適化後の動的計画法 */ + int knapsackDPComp(List wgt, List val, int cap) { + int n = wgt.length; + // dp テーブルを初期化 + List dp = List.filled(cap + 1, 0); + // 状態遷移 + for (int i = 1; i <= n; i++) { + // 逆順に走査する + for (int c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Rust" ```rust title="knapsack.rs" - [class]{}-[func]{knapsack_dp_comp} + /* 0-1 ナップサック:空間最適化後の動的計画法 */ + fn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // dp テーブルを初期化 + let mut dp = vec![0; cap + 1]; + // 状態遷移 + for i in 1..=n { + // 逆順に走査する + for c in (1..=cap).rev() { + if wgt[i - 1] <= c as i32 { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]); + } + } + } + dp[cap] + } ``` === "C" ```c title="knapsack.c" - [class]{}-[func]{knapsackDPComp} + /* 0-1 ナップサック:空間最適化後の動的計画法 */ + int knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // dp テーブルを初期化 + int *dp = calloc(cap + 1, sizeof(int)); + // 状態遷移 + for (int i = 1; i <= n; i++) { + // 逆順に走査する + for (int c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[cap]; + // メモリを解放する + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="knapsack.kt" - [class]{}-[func]{knapsackDPComp} + /* 0-1 ナップサック:空間最適化後の動的計画法 */ + fun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int { + val n = wgt.size + // dp テーブルを初期化 + val dp = IntArray(cap + 1) + // 状態遷移 + for (i in 1..n) { + // 逆順に走査する + for (c in cap downTo 1) { + if (wgt[i - 1] <= c) { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1]) + } + } + } + return dp[cap] + } ``` === "Ruby" ```ruby title="knapsack.rb" - [class]{}-[func]{knapsack_dp_comp} + ### 0-1 ナップサック: 空間最適化後の動的計画法 ### + def knapsack_dp_comp(wgt, val, cap) + n = wgt.length + # dp テーブルを初期化 + dp = Array.new(cap + 1, 0) + # 状態遷移 + for i in 1...(n + 1) + # 逆順に走査する + for c in cap.downto(1) + if wgt[i - 1] > c + # ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c] + else + # 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max + end + end + end + dp[cap] + end ``` + +??? pythontutor "コードの可視化" + +
+ diff --git a/ja/docs/chapter_dynamic_programming/summary.md b/ja/docs/chapter_dynamic_programming/summary.md index 3ec24eb93..b15cb0fa0 100644 --- a/ja/docs/chapter_dynamic_programming/summary.md +++ b/ja/docs/chapter_dynamic_programming/summary.md @@ -4,24 +4,26 @@ comments: true # 14.7   まとめ -- 動的プログラミングは問題を分解し、部分問題の解を保存することで冗長な計算を避け、計算効率を向上させます。 -- 時間を考慮しなければ、すべての動的プログラミング問題はバックトラッキング(力任せ探索)を使用して解決できますが、再帰木には多くの重複する部分問題があり、効率が非常に低くなります。記憶化リストを導入することで、計算されたすべての部分問題の解を保存し、重複する部分問題が一度だけ計算されることを保証できます。 -- 記憶化探索はトップダウンの再帰解法であり、動的プログラミングはボトムアップの反復アプローチに対応し、「表を埋める」ことに似ています。現在の状態は特定の局所状態のみに依存するため、dpテーブルの1次元を削除して空間計算量を削減できます。 -- 部分問題の分解は汎用的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングで特徴が異なります。 -- 動的プログラミング問題には3つの主要な特徴があります:重複する部分問題、最適部分構造、無記憶性。 -- 元の問題の最適解がその部分問題の最適解から構築できる場合、最適部分構造を持ちます。 -- 無記憶性とは、状態の将来の発展が現在の状態のみに依存し、過去に経験したすべての状態に依存しないことを意味します。多くの組み合わせ最適化問題にはこの特性がなく、動的プログラミングを使用して迅速に解決することはできません。 +### 1.   要点の振り返り + +- 動的計画法は問題を分解し、部分問題の解を保存することで重複計算を避け、計算効率を高めます。 +- 時間を考慮しなければ、すべての動的計画法の問題はバックトラッキング(総当たり探索)で解けますが、再帰木には大量の重複部分問題が存在するため、効率はきわめて低くなります。メモ化配列を導入すると、計算済みのすべての部分問題の解を保存でき、重複部分問題が 1 回だけ計算されることを保証できます。 +- メモ化探索はトップダウンの再帰的解法であり、それに対応する動的計画法はボトムアップの漸化式による解法で、ちょうど「表を埋める」ようなものです。現在の状態は一部の局所状態にのみ依存するため、$dp$ 表の 1 次元を削減して空間計算量を下げることができます。 +- 部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治、動的計画法、バックトラッキングではそれぞれ異なる性質を持ちます。 +- 動的計画法の問題には 3 つの大きな特徴があります。重複部分問題、最適部分構造、無後効性です。 +- 元の問題の最適解が部分問題の最適解から構築できるなら、その問題は最適部分構造を持ちます。 +- 無後効性とは、ある状態の将来の発展がその状態のみに関係し、過去に経たすべての状態とは無関係であることを指します。多くの組合せ最適化問題は無後効性を持たず、動的計画法で高速に解くことはできません。 **ナップサック問題** -- ナップサック問題は最も典型的な動的プログラミング問題の1つで、0-1ナップサック、無制限ナップサック、複数ナップサックなどの変種があります。 -- 0-1ナップサックの状態定義は、最初の $i$ 個のアイテムを含む容量 $c$ のナップサックでの最大値です。アイテムをナップサックに入れないまたは入れるという決定に基づいて、最適部分構造を特定し、状態遷移方程式を構築できます。空間最適化では、各状態が直接上と左上の状態に依存するため、左上の状態の上書きを避けるためにリストを逆順で走査する必要があります。 -- 無制限ナップサック問題では、各種類のアイテムを選択できる数に制限がないため、アイテムを含める状態遷移は0-1ナップサックと異なります。状態が直接上と左の状態に依存するため、空間最適化では前方走査を含める必要があります。 -- コイン交換問題は無制限ナップサック問題の変種で、「最大」値を求めることから「最小」コイン数を求めることに変わり、状態遷移方程式は $\max()$ を $\min()$ に変更する必要があります。ナップサックの容量を「超えない」ことを追求することから、正確に目標金額を求めることに変わり、「目標金額を構成できない」無効解を表すために $amt + 1$ を使用します。 -- コイン交換問題IIは「最小コイン数」を求めることから「コインの組み合わせ数」を求めることに変わり、状態遷移方程式を $\min()$ から和算演算子に変更します。 +- ナップサック問題は最も典型的な動的計画法の問題の 1 つであり、0-1 ナップサック、完全ナップサック、多重ナップサックなどの派生があります。 +- 0-1 ナップサックの状態は、容量 $c$ のナップサックに対して、前 $i$ 個の品物で得られる最大価値として定義されます。ナップサックに入れない場合と入れる場合の 2 つの判断から最適部分構造を得て、状態遷移方程式を構築できます。空間最適化では、各状態が真上と左上の状態に依存するため、左上の状態が上書きされるのを避けるために配列を逆順に走査する必要があります。 +- 完全ナップサック問題では各品物の選択数に制限がないため、品物を入れる場合の状態遷移は 0-1 ナップサック問題とは異なります。状態は真上と真左の状態に依存するので、空間最適化では順方向に走査するべきです。 +- コイン両替問題は完全ナップサック問題の変種です。「最大」価値を求める問題から「最小」の硬貨枚数を求める問題へ変わるため、状態遷移方程式の $\max()$ は $\min()$ に置き換える必要があります。また、ナップサック容量を「超えない」ことを目指すのではなく、目標金額を「ちょうど」作ることを目指すため、$amt + 1$ を「目標金額を作れない」無効解の表現として用います。 +- コイン両替問題 II では、「最少硬貨枚数」を求める問題から「硬貨の組合せ数」を求める問題へ変わるため、状態遷移方程式も $\min()$ から総和演算子へ対応して変わります。 **編集距離問題** -- 編集距離(レーベンシュタイン距離)は2つの文字列間の類似度を測定し、一つの文字列を別の文字列に変更するために必要な最小編集ステップ数として定義され、編集操作には追加、削除、置換が含まれます。 -- 編集距離問題の状態定義は、$s$ の最初の $i$ 文字を $t$ の最初の $j$ 文字に変更するために必要な最小編集ステップ数です。$s[i] \ne t[j]$ の場合、追加、削除、置換の3つの決定があり、それぞれに対応する残余部分問題があります。これから最適部分構造を特定し、状態遷移方程式を構築できます。$s[i] = t[j]$ の場合、現在の文字の編集は必要ありません。 -- 編集距離では、状態が直接上、左、左上の状態に依存します。したがって、空間最適化後、前方走査も逆走査も正しく状態遷移を実行できません。これに対処するため、変数を使用して左上の状態を一時的に保存し、無制限ナップサック問題の状況と同等にし、空間最適化後に前方走査を可能にします。 +- 編集距離(Levenshtein 距離)は 2 つの文字列間の類似度を測るために用いられ、ある文字列を別の文字列へ変換するための最小編集回数として定義されます。編集操作には追加、削除、置換が含まれます。 +- 編集距離問題の状態は、$s$ の前 $i$ 文字を $t$ の前 $j$ 文字へ変更するのに必要な最小編集回数として定義されます。$s[i] \ne t[j]$ のときは、追加、削除、置換の 3 つの判断があり、それぞれに対応する残りの部分問題があります。これにより最適部分構造を見いだし、状態遷移方程式を構築できます。一方、$s[i] = t[j]$ のときは現在の文字を編集する必要はありません。 +- 編集距離では、状態は真上、真左、左上の状態に依存します。そのため、空間最適化後は順方向でも逆方向でも正しく状態遷移できません。そこで、変数を 1 つ用いて左上の状態を一時保存し、完全ナップサック問題と等価な形へ変換することで、空間最適化後に順方向走査を行えるようにします。 diff --git a/ja/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/ja/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md index e930c688d..52ae74f56 100644 --- a/ja/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/ja/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -2,33 +2,33 @@ comments: true --- -# 14.5   無制限ナップサック問題 +# 14.5   完全ナップサック問題 -このセクションでは、まず別の一般的なナップサック問題である無制限ナップサックを解決し、次にその特殊ケースであるコイン交換問題を探索します。 +本節では、まずもう 1 つの代表的なナップサック問題である完全ナップサック問題を解き、その特殊例である硬貨交換問題について見ていきます。 -## 14.5.1   無制限ナップサック問題 +## 14.5.1   完全ナップサック問題 !!! question - $n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重量は $wgt[i-1]$ で値は $val[i-1]$ です。容量が $cap$ のバックパックがあります。**各アイテムは複数回選択できます**。容量を超えることなくバックパックに入れることができるアイテムの最大値は何ですか?以下の例を参照してください。 + $n$ 個の品物が与えられ、$i$ 番目の品物の重さは $wgt[i-1]$、価値は $val[i-1]$ であり、容量 $cap$ のナップサックがあります。**各品物は繰り返し選択できます**。ナップサック容量の制約下で入れられる品物の最大価値を求めてください。例を以下の図に示します。 -![無制限ナップサック問題の例データ](unbounded_knapsack_problem.assets/unbounded_knapsack_example.png){ class="animation-figure" } +![完全ナップサック問題のサンプルデータ](unbounded_knapsack_problem.assets/unbounded_knapsack_example.png){ class="animation-figure" } -

図 14-22   無制限ナップサック問題の例データ

+

図 14-22   完全ナップサック問題のサンプルデータ

-### 1.   動的プログラミングアプローチ +### 1.   動的計画法の考え方 -無制限ナップサック問題は0-1ナップサック問題と非常に似ており、**唯一の違いはアイテムを選択できる回数に制限がないことです**。 +完全ナップサック問題は 0-1 ナップサック問題と非常によく似ています。**違いは、品物の選択回数に制限がない点だけです**。 -- 0-1ナップサック問題では、各アイテムは1つしかないため、アイテム $i$ をバックパックに入れた後は、前の $i-1$ 個のアイテムからのみ選択できます。 -- 無制限ナップサック問題では、各アイテムの数量は無制限であるため、アイテム $i$ をバックパックに入れた後も、**前の $i$ 個のアイテムから引き続き選択できます**。 +- 0-1 ナップサック問題では、各品物は 1 つしかないため、品物 $i$ をナップサックに入れた後は先頭 $i-1$ 個の品物からしか選べません。 +- 完全ナップサック問題では、各品物の数は無限であるため、品物 $i$ をナップサックに入れた後も、**引き続き先頭 $i$ 個の品物から選べます**。 -無制限ナップサック問題のルールの下で、状態 $[i, c]$ は2つの方法で変化できます。 +完全ナップサック問題では、状態 $[i, c]$ の変化は 2 つの場合に分けられます。 -- **アイテム $i$ を入れない**:0-1ナップサック問題と同様に、$[i-1, c]$ に遷移します。 -- **アイテム $i$ を入れる**:0-1ナップサック問題とは異なり、$[i, c-wgt[i-1]]$ に遷移します。 +- **品物 $i$ を入れない** :0-1 ナップサック問題と同様に、$[i-1, c]$ へ遷移します。 +- **品物 $i$ を入れる** :0-1 ナップサック問題とは異なり、$[i, c-wgt[i-1]]$ へ遷移します。 -したがって、状態遷移方程式は次のようになります: +したがって、状態遷移方程式は次のようになります。 $$ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) @@ -36,13 +36,13 @@ $$ ### 2.   コード実装 -2つの問題のコードを比較すると、状態遷移が $i-1$ から $i$ に変わり、残りは完全に同一です: +2 つの問題のコードを比較すると、状態遷移の中で 1 か所だけ $i-1$ が $i$ に変わり、それ以外は完全に同じです。 === "Python" ```python title="unbounded_knapsack.py" def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int: - """完全ナップサック:動的プログラミング""" + """完全ナップサック問題:動的計画法""" n = len(wgt) # dp テーブルを初期化 dp = [[0] * (cap + 1) for _ in range(n + 1)] @@ -50,10 +50,10 @@ $$ for i in range(1, n + 1): for c in range(1, cap + 1): if wgt[i - 1] > c: - # ナップサック容量を超える場合、アイテム i を選択しない + # ナップサック容量を超えるなら品物 i は選ばない dp[i][c] = dp[i - 1][c] else: - # アイテム i を選択しないのと選択するのとで大きい値 + # 品物 i を選ばない場合と選ぶ場合の大きい方 dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]) return dp[n][cap] ``` @@ -61,19 +61,19 @@ $$ === "C++" ```cpp title="unbounded_knapsack.cpp" - /* 完全ナップサック:動的プログラミング */ + /* 完全ナップサック問題:動的計画法 */ int unboundedKnapsackDP(vector &wgt, vector &val, int cap) { int n = wgt.size(); - // DPテーブルを初期化 + // dp テーブルを初期化 vector> dp(n + 1, vector(cap + 1, 0)); // 状態遷移 for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // ナップサックの容量を超える場合、アイテム i を選択しない + // ナップサック容量を超えるなら品物 i は選ばない dp[i][c] = dp[i - 1][c]; } else { - // 選択しない場合とアイテム i を選択する場合のより大きい値 + // 品物 i を選ばない場合と選ぶ場合の大きい方 dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); } } @@ -85,19 +85,19 @@ $$ === "Java" ```java title="unbounded_knapsack.java" - /* 完全ナップサック:動的プログラミング */ + /* 完全ナップサック問題:動的計画法 */ int unboundedKnapsackDP(int[] wgt, int[] val, int cap) { int n = wgt.length; - // DPテーブルを初期化 + // dp テーブルを初期化 int[][] dp = new int[n + 1][cap + 1]; // 状態遷移 for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // ナップサックの容量を超える場合、アイテム i を選択しない + // ナップサック容量を超えるなら品物 i は選ばない dp[i][c] = dp[i - 1][c]; } else { - // 選択しない場合とアイテム i を選択する場合のより大きい値 + // 品物 i を選ばない場合と選ぶ場合の大きい方 dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); } } @@ -109,71 +109,281 @@ $$ === "C#" ```csharp title="unbounded_knapsack.cs" - [class]{unbounded_knapsack}-[func]{UnboundedKnapsackDP} + /* 完全ナップサック問題:動的計画法 */ + int UnboundedKnapsackDP(int[] wgt, int[] val, int cap) { + int n = wgt.Length; + // dp テーブルを初期化 + int[,] dp = new int[n + 1, cap + 1]; + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i, c] = dp[i - 1, c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i, c] = Math.Max(dp[i - 1, c], dp[i, c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n, cap]; + } ``` === "Go" ```go title="unbounded_knapsack.go" - [class]{}-[func]{unboundedKnapsackDP} + /* 完全ナップサック問題:動的計画法 */ + func unboundedKnapsackDP(wgt, val []int, cap int) int { + n := len(wgt) + // dp テーブルを初期化 + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, cap+1) + } + // 状態遷移 + for i := 1; i <= n; i++ { + for c := 1; c <= cap; c++ { + if wgt[i-1] > c { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i-1][c] + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i][c-wgt[i-1]]+val[i-1]))) + } + } + } + return dp[n][cap] + } ``` === "Swift" ```swift title="unbounded_knapsack.swift" - [class]{}-[func]{unboundedKnapsackDP} + /* 完全ナップサック問題:動的計画法 */ + func unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // dp テーブルを初期化 + var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1) + // 状態遷移 + for i in 1 ... n { + for c in 1 ... cap { + if wgt[i - 1] > c { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c] + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]) + } + } + } + return dp[n][cap] + } ``` === "JS" ```javascript title="unbounded_knapsack.js" - [class]{}-[func]{unboundedKnapsackDP} + /* 完全ナップサック問題:動的計画法 */ + function unboundedKnapsackDP(wgt, val, cap) { + const n = wgt.length; + // dp テーブルを初期化 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } ``` === "TS" ```typescript title="unbounded_knapsack.ts" - [class]{}-[func]{unboundedKnapsackDP} + /* 完全ナップサック問題:動的計画法 */ + function unboundedKnapsackDP( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // dp テーブルを初期化 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } ``` === "Dart" ```dart title="unbounded_knapsack.dart" - [class]{}-[func]{unboundedKnapsackDP} + /* 完全ナップサック問題:動的計画法 */ + int unboundedKnapsackDP(List wgt, List val, int cap) { + int n = wgt.length; + // dp テーブルを初期化 + List> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0)); + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n][cap]; + } ``` === "Rust" ```rust title="unbounded_knapsack.rs" - [class]{}-[func]{unbounded_knapsack_dp} + /* 完全ナップサック問題:動的計画法 */ + fn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // dp テーブルを初期化 + let mut dp = vec![vec![0; cap + 1]; n + 1]; + // 状態遷移 + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]); + } + } + } + return dp[n][cap]; + } ``` === "C" ```c title="unbounded_knapsack.c" - [class]{}-[func]{unboundedKnapsackDP} + /* 完全ナップサック問題:動的計画法 */ + int unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // dp テーブルを初期化 + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(cap + 1, sizeof(int)); + } + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[n][cap]; + // メモリを解放する + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + return res; + } ``` === "Kotlin" ```kotlin title="unbounded_knapsack.kt" - [class]{}-[func]{unboundedKnapsackDP} + /* 完全ナップサック問題:動的計画法 */ + fun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int { + val n = wgt.size + // dp テーブルを初期化 + val dp = Array(n + 1) { IntArray(cap + 1) } + // 状態遷移 + for (i in 1..n) { + for (c in 1..cap) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c] + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + _val[i - 1]) + } + } + } + return dp[n][cap] + } ``` === "Ruby" ```ruby title="unbounded_knapsack.rb" - [class]{}-[func]{unbounded_knapsack_dp} + ### 完全ナップサック:動的計画法 ### + def unbounded_knapsack_dp(wgt, val, cap) + n = wgt.length + # dp テーブルを初期化 + dp = Array.new(n + 1) { Array.new(cap + 1, 0) } + # 状態遷移 + for i in 1...(n + 1) + for c in 1...(cap + 1) + if wgt[i - 1] > c + # ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c] + else + # 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max + end + end + end + dp[n][cap] + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 3.   空間最適化 -現在の状態は左と上の状態から来るため、**空間最適化解法は $dp$ テーブルの各行に対して前方走査を実行する必要があります**。 +現在の状態は左側と上側の状態から遷移してくるため、**空間最適化後は $dp$ テーブルの各行を順方向に走査する必要があります**。 -この走査順序は0-1ナップサックの場合とは逆です。違いを理解するために下の図を参照してください。 +この走査順序は 0-1 ナップサックとはちょうど逆です。両者の違いは次の図を用いて理解してください。 === "<1>" - ![空間最適化後の無制限ナップサック問題の動的プログラミングプロセス](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png){ class="animation-figure" } + ![完全ナップサック問題における空間最適化後の動的計画法の過程](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png){ class="animation-figure" } === "<2>" ![unbounded_knapsack_dp_comp_step2](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step2.png){ class="animation-figure" } @@ -190,27 +400,27 @@ $$ === "<6>" ![unbounded_knapsack_dp_comp_step6](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step6.png){ class="animation-figure" } -

図 14-23   空間最適化後の無制限ナップサック問題の動的プログラミングプロセス

+

図 14-23   完全ナップサック問題における空間最適化後の動的計画法の過程

-コード実装は非常に簡単で、配列 `dp` の最初の次元を削除するだけです: +コード実装は比較的簡単で、配列 `dp` の第 1 次元を削除するだけです。 === "Python" ```python title="unbounded_knapsack.py" def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int: - """完全ナップサック:空間最適化動的プログラミング""" + """完全ナップサック問題:空間最適化後の動的計画法""" n = len(wgt) # dp テーブルを初期化 dp = [0] * (cap + 1) # 状態遷移 for i in range(1, n + 1): - # 順序で走査 + # 順方向に走査する for c in range(1, cap + 1): if wgt[i - 1] > c: - # ナップサック容量を超える場合、アイテム i を選択しない + # ナップサック容量を超えるなら品物 i は選ばない dp[c] = dp[c] else: - # アイテム i を選択しないのと選択するのとで大きい値 + # 品物 i を選ばない場合と選ぶ場合の大きい方 dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) return dp[cap] ``` @@ -218,19 +428,19 @@ $$ === "C++" ```cpp title="unbounded_knapsack.cpp" - /* 完全ナップサック:空間最適化動的プログラミング */ + /* 完全ナップサック問題:空間最適化後の動的計画法 */ int unboundedKnapsackDPComp(vector &wgt, vector &val, int cap) { int n = wgt.size(); - // DPテーブルを初期化 + // dp テーブルを初期化 vector dp(cap + 1, 0); // 状態遷移 for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // ナップサックの容量を超える場合、アイテム i を選択しない + // ナップサック容量を超えるなら品物 i は選ばない dp[c] = dp[c]; } else { - // 選択しない場合とアイテム i を選択する場合のより大きい値 + // 品物 i を選ばない場合と選ぶ場合の大きい方 dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); } } @@ -242,19 +452,19 @@ $$ === "Java" ```java title="unbounded_knapsack.java" - /* 完全ナップサック:空間最適化動的プログラミング */ + /* 完全ナップサック問題:空間最適化後の動的計画法 */ int unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) { int n = wgt.length; - // DPテーブルを初期化 + // dp テーブルを初期化 int[] dp = new int[cap + 1]; // 状態遷移 for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // ナップサックの容量を超える場合、アイテム i を選択しない + // ナップサック容量を超えるなら品物 i は選ばない dp[c] = dp[c]; } else { - // 選択しない場合とアイテム i を選択する場合のより大きい値 + // 品物 i を選ばない場合と選ぶ場合の大きい方 dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); } } @@ -266,132 +476,349 @@ $$ === "C#" ```csharp title="unbounded_knapsack.cs" - [class]{unbounded_knapsack}-[func]{UnboundedKnapsackDPComp} + /* 完全ナップサック問題:空間最適化後の動的計画法 */ + int UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) { + int n = wgt.Length; + // dp テーブルを初期化 + int[] dp = new int[cap + 1]; + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Go" ```go title="unbounded_knapsack.go" - [class]{}-[func]{unboundedKnapsackDPComp} + /* 完全ナップサック問題:空間最適化後の動的計画法 */ + func unboundedKnapsackDPComp(wgt, val []int, cap int) int { + n := len(wgt) + // dp テーブルを初期化 + dp := make([]int, cap+1) + // 状態遷移 + for i := 1; i <= n; i++ { + for c := 1; c <= cap; c++ { + if wgt[i-1] > c { + // ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c] + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1]))) + } + } + } + return dp[cap] + } ``` === "Swift" ```swift title="unbounded_knapsack.swift" - [class]{}-[func]{unboundedKnapsackDPComp} + /* 完全ナップサック問題:空間最適化後の動的計画法 */ + func unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // dp テーブルを初期化 + var dp = Array(repeating: 0, count: cap + 1) + // 状態遷移 + for i in 1 ... n { + for c in 1 ... cap { + if wgt[i - 1] > c { + // ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c] + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) + } + } + } + return dp[cap] + } ``` === "JS" ```javascript title="unbounded_knapsack.js" - [class]{}-[func]{unboundedKnapsackDPComp} + /* 完全ナップサック問題:空間最適化後の動的計画法 */ + function unboundedKnapsackDPComp(wgt, val, cap) { + const n = wgt.length; + // dp テーブルを初期化 + const dp = Array.from({ length: cap + 1 }, () => 0); + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "TS" ```typescript title="unbounded_knapsack.ts" - [class]{}-[func]{unboundedKnapsackDPComp} + /* 完全ナップサック問題:空間最適化後の動的計画法 */ + function unboundedKnapsackDPComp( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // dp テーブルを初期化 + const dp = Array.from({ length: cap + 1 }, () => 0); + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Dart" ```dart title="unbounded_knapsack.dart" - [class]{}-[func]{unboundedKnapsackDPComp} + /* 完全ナップサック問題:空間最適化後の動的計画法 */ + int unboundedKnapsackDPComp(List wgt, List val, int cap) { + int n = wgt.length; + // dp テーブルを初期化 + List dp = List.filled(cap + 1, 0); + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Rust" ```rust title="unbounded_knapsack.rs" - [class]{}-[func]{unbounded_knapsack_dp_comp} + /* 完全ナップサック問題:空間最適化後の動的計画法 */ + fn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // dp テーブルを初期化 + let mut dp = vec![0; cap + 1]; + // 状態遷移 + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]); + } + } + } + dp[cap] + } ``` === "C" ```c title="unbounded_knapsack.c" - [class]{}-[func]{unboundedKnapsackDPComp} + /* 完全ナップサック問題:空間最適化後の動的計画法 */ + int unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // dp テーブルを初期化 + int *dp = calloc(cap + 1, sizeof(int)); + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c]; + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[cap]; + // メモリを解放する + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="unbounded_knapsack.kt" - [class]{}-[func]{unboundedKnapsackDPComp} + /* 完全ナップサック問題:空間最適化後の動的計画法 */ + fun unboundedKnapsackDPComp( + wgt: IntArray, + _val: IntArray, + cap: Int + ): Int { + val n = wgt.size + // dp テーブルを初期化 + val dp = IntArray(cap + 1) + // 状態遷移 + for (i in 1..n) { + for (c in 1..cap) { + if (wgt[i - 1] > c) { + // ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c] + } else { + // 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1]) + } + } + } + return dp[cap] + } ``` === "Ruby" ```ruby title="unbounded_knapsack.rb" - [class]{}-[func]{unbounded_knapsack_dp_comp} + ### 完全ナップサック:動的計画法 ### + def unbounded_knapsack_dp(wgt, val, cap) + n = wgt.length + # dp テーブルを初期化 + dp = Array.new(n + 1) { Array.new(cap + 1, 0) } + # 状態遷移 + for i in 1...(n + 1) + for c in 1...(cap + 1) + if wgt[i - 1] > c + # ナップサック容量を超えるなら品物 i は選ばない + dp[i][c] = dp[i - 1][c] + else + # 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max + end + end + end + dp[n][cap] + end + + # ## 完全ナップサック: 空間最適化後の動的計画法 ##3 + def unbounded_knapsack_dp_comp(wgt, val, cap) + n = wgt.length + # dp テーブルを初期化 + dp = Array.new(cap + 1, 0) + # 状態遷移 + for i in 1...(n + 1) + # 順方向に走査する + for c in 1...(cap + 1) + if wgt[i -1] > c + # ナップサック容量を超えるなら品物 i は選ばない + dp[c] = dp[c] + else + # 品物 i を選ばない場合と選ぶ場合の大きい方 + dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max + end + end + end + dp[cap] + end ``` -## 14.5.2   コイン交換問題 +??? pythontutor "コードの可視化" -ナップサック問題は動的プログラミング問題の大きなクラスの代表であり、コイン交換問題など多くの変種があります。 +
+ + +## 14.5.2   硬貨交換問題 + +ナップサック問題は動的計画法の代表的な問題群であり、多くの派生問題があります。硬貨交換問題もその 1 つです。 !!! question - $n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ で、目標金額は $amt$ です。**各種類のコインは複数回選択できます**。目標金額を構成するのに必要な最小コイン数は何ですか?目標金額を構成できない場合は $-1$ を返してください。以下の例を参照してください。 + $n$ 種類の硬貨が与えられ、$i$ 番目の硬貨の額面は $coins[i - 1]$ 、目標金額は $amt$ です。**各硬貨は繰り返し選択できます**。目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は $-1$ を返します。例を以下の図に示します。 -![コイン交換問題の例データ](unbounded_knapsack_problem.assets/coin_change_example.png){ class="animation-figure" } +![硬貨交換問題のサンプルデータ](unbounded_knapsack_problem.assets/coin_change_example.png){ class="animation-figure" } -

図 14-24   コイン交換問題の例データ

+

図 14-24   硬貨交換問題のサンプルデータ

-### 1.   動的プログラミングアプローチ +### 1.   動的計画法の考え方 -**コイン交換は無制限ナップサック問題の特殊ケースと見なすことができ**、以下の類似点と相違点を共有しています。 +**硬貨交換は完全ナップサック問題の特殊なケースとみなせます**。両者には次の対応関係と相違点があります。 -- 2つの問題は互いに変換できます:「アイテム」は「コイン」に対応し、「アイテムの重量」は「コインの額面」に対応し、「バックパックの容量」は「目標金額」に対応します。 -- 最適化目標は逆です:無制限ナップサック問題はアイテムの値を最大化することを目的とし、コイン交換問題はコインの数を最小化することを目的とします。 -- 無制限ナップサック問題はバックパック容量を「超えない」解を求め、コイン交換は目標金額を「正確に」構成する解を求めます。 +- 2 つの問題は相互に変換でき、「品物」は「硬貨」、「品物の重さ」は「硬貨の額面」、「ナップサック容量」は「目標金額」に対応します。 +- 最適化の目標は逆であり、完全ナップサック問題は品物価値の最大化、硬貨交換問題は硬貨枚数の最小化を目指します。 +- 完全ナップサック問題はナップサック容量を「超えない」解を求めますが、硬貨交換は目標金額に「ちょうど」一致する解を求めます。 -**第1ステップ:各ラウンドの意思決定を考え、状態を定義し、それにより $dp$ テーブルを導出する** +**ステップ 1:各ラウンドの選択を考え、状態を定義して、$dp$ テーブルを得る** -状態 $[i, a]$ は部分問題に対応します:**最初の $i$ 種類のコインを使用して金額 $a$ を構成できる最小コイン数**、$dp[i, a]$ と表記されます。 +状態 $[i, a]$ に対応する部分問題は、**先頭 $i$ 種類の硬貨で金額 $a$ を作るための最小硬貨枚数**であり、これを $dp[i, a]$ と表します。 -二次元 $dp$ テーブルのサイズは $(n+1) \times (amt+1)$ です。 +2 次元 $dp$ テーブルのサイズは $(n+1) \times (amt+1)$ です。 -**第2ステップ:最適部分構造を特定し、状態遷移方程式を導出する** +**ステップ 2:最適部分構造を見つけ、状態遷移方程式を導く** -この問題は状態遷移方程式の2つの側面で無制限ナップサック問題と異なります。 +本問の状態遷移方程式は、完全ナップサック問題と比べて次の 2 点が異なります。 -- この問題は最小値を求めるため、演算子 $\max()$ を $\min()$ に変更する必要があります。 -- 最適化はコインの数に焦点を当てているため、コインが選択されたときに単純に $+1$ を追加します。 +- 本問では最小値を求めるため、演算子 $\max()$ を $\min()$ に変更する必要があります。 +- 最適化の対象は品物価値ではなく硬貨枚数であるため、硬貨を選んだときは $+1$ すれば十分です。 $$ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) $$ -**第3ステップ:境界条件と状態遷移順序を定義する** +**ステップ 3:境界条件と状態遷移順序を決める** -目標金額が $0$ の場合、それを構成するのに必要な最小コイン数は $0$ であるため、最初の列のすべての $dp[i, 0]$ は $0$ です。 +目標金額が $0$ のとき、それを作るための最小硬貨枚数は $0$ です。つまり、先頭列のすべての $dp[i, 0]$ は $0$ になります。 -コインがない場合、**任意の金額 >0 を構成することは不可能**であり、これは無効な解です。状態遷移方程式の $\min()$ 関数が無効な解を認識してフィルタリングできるように、$+\infty$ を使用してそれらを表現することを検討し、つまり最初の行のすべての $dp[0, a]$ を $+\infty$ に設定します。 +硬貨が 1 枚もない場合、**任意の $> 0$ の目標金額を作ることはできません**。これは無効解です。状態遷移方程式内の $\min()$ 関数が無効解を識別して除外できるように、それらを $+ \infty$ で表すことを考えます。すなわち、先頭行のすべての $dp[0, a]$ を $+ \infty$ とします。 ### 2.   コード実装 -ほとんどのプログラミング言語は $+\infty$ 変数を提供しておらず、整数 `int` の最大値のみを代替として使用できます。これによりオーバーフローが発生する可能性があります:状態遷移方程式の $+1$ 演算がオーバーフローする可能性があります。 +多くのプログラミング言語には $+ \infty$ を表す変数が用意されていないため、通常は整数型 `int` の最大値で代用します。しかし、その場合は大きな数のオーバーフローが起こり得ます。状態遷移方程式中の $+ 1$ 操作で桁あふれが発生する可能性があるためです。 -この理由で、数値 $amt + 1$ を使用して無効な解を表します。なぜなら、$amt$ を構成するのに必要な最大コイン数は最大でも $amt$ だからです。結果を返す前に、$dp[n, amt]$ が $amt + 1$ に等しいかどうかを確認し、そうであれば $-1$ を返し、目標金額を構成できないことを示します。コードは以下の通りです: +そのため、ここでは数値 $amt + 1$ を無効解の表現として用います。金額 $amt$ を作るための硬貨枚数は最大でも $amt$ 枚だからです。最後に返す前に、$dp[n, amt]$ が $amt + 1$ に等しいかを判定し、等しければ $-1$ を返して目標金額を作れないことを表します。コードは次のとおりです。 === "Python" ```python title="coin_change.py" def coin_change_dp(coins: list[int], amt: int) -> int: - """硬貨交換:動的プログラミング""" + """コイン両替:動的計画法""" n = len(coins) MAX = amt + 1 # dp テーブルを初期化 dp = [[0] * (amt + 1) for _ in range(n + 1)] - # 状態遷移:最初の行と最初の列 + # 状態遷移:先頭行と先頭列 for a in range(1, amt + 1): dp[0][a] = MAX - # 状態遷移:残りの行と列 + # 状態遷移: 残りの行と列 for i in range(1, n + 1): for a in range(1, amt + 1): if coins[i - 1] > a: - # 目標金額を超える場合、硬貨 i を選択しない + # 目標金額を超えるなら硬貨 i は選ばない dp[i][a] = dp[i - 1][a] else: - # 硬貨 i を選択しないのと選択するのとで小さい値 + # 硬貨 i を選ばない場合と選ぶ場合の小さい方 dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1) return dp[n][amt] if dp[n][amt] != MAX else -1 ``` @@ -399,24 +826,24 @@ $$ === "C++" ```cpp title="coin_change.cpp" - /* 硬貨両替:動的プログラミング */ + /* コイン両替:動的計画法 */ int coinChangeDP(vector &coins, int amt) { int n = coins.size(); int MAX = amt + 1; - // DPテーブルを初期化 + // dp テーブルを初期化 vector> dp(n + 1, vector(amt + 1, 0)); - // 状態遷移:最初の行と最初の列 + // 状態遷移:先頭行と先頭列 for (int a = 1; a <= amt; a++) { dp[0][a] = MAX; } - // 状態遷移:残りの行と列 + // 状態遷移: 残りの行と列 for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // 目標金額を超える場合、硬貨 i を選択しない + // 目標金額を超えるなら硬貨 i は選ばない dp[i][a] = dp[i - 1][a]; } else { - // 選択しない場合と硬貨 i を選択する場合のより小さい値 + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); } } @@ -428,24 +855,24 @@ $$ === "Java" ```java title="coin_change.java" - /* 硬貨両替:動的プログラミング */ + /* コイン両替:動的計画法 */ int coinChangeDP(int[] coins, int amt) { int n = coins.length; int MAX = amt + 1; - // DPテーブルを初期化 + // dp テーブルを初期化 int[][] dp = new int[n + 1][amt + 1]; - // 状態遷移:最初の行と最初の列 + // 状態遷移:先頭行と先頭列 for (int a = 1; a <= amt; a++) { dp[0][a] = MAX; } - // 状態遷移:残りの行と列 + // 状態遷移: 残りの行と列 for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // 目標金額を超える場合、硬貨 i を選択しない + // 目標金額を超えるなら硬貨 i は選ばない dp[i][a] = dp[i - 1][a]; } else { - // 選択しない場合と硬貨 i を選択する場合のより小さい値 + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); } } @@ -457,67 +884,323 @@ $$ === "C#" ```csharp title="coin_change.cs" - [class]{coin_change}-[func]{CoinChangeDP} + /* コイン両替:動的計画法 */ + int CoinChangeDP(int[] coins, int amt) { + int n = coins.Length; + int MAX = amt + 1; + // dp テーブルを初期化 + int[,] dp = new int[n + 1, amt + 1]; + // 状態遷移:先頭行と先頭列 + for (int a = 1; a <= amt; a++) { + dp[0, a] = MAX; + } + // 状態遷移: 残りの行と列 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i, a] = dp[i - 1, a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1); + } + } + } + return dp[n, amt] != MAX ? dp[n, amt] : -1; + } ``` === "Go" ```go title="coin_change.go" - [class]{}-[func]{coinChangeDP} + /* コイン両替:動的計画法 */ + func coinChangeDP(coins []int, amt int) int { + n := len(coins) + max := amt + 1 + // dp テーブルを初期化 + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, amt+1) + } + // 状態遷移:先頭行と先頭列 + for a := 1; a <= amt; a++ { + dp[0][a] = max + } + // 状態遷移: 残りの行と列 + for i := 1; i <= n; i++ { + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i-1][a] + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1))) + } + } + } + if dp[n][amt] != max { + return dp[n][amt] + } + return -1 + } ``` === "Swift" ```swift title="coin_change.swift" - [class]{}-[func]{coinChangeDP} + /* コイン両替:動的計画法 */ + func coinChangeDP(coins: [Int], amt: Int) -> Int { + let n = coins.count + let MAX = amt + 1 + // dp テーブルを初期化 + var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1) + // 状態遷移:先頭行と先頭列 + for a in 1 ... amt { + dp[0][a] = MAX + } + // 状態遷移: 残りの行と列 + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a] + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1) + } + } + } + return dp[n][amt] != MAX ? dp[n][amt] : -1 + } ``` === "JS" ```javascript title="coin_change.js" - [class]{}-[func]{coinChangeDP} + /* コイン両替:動的計画法 */ + function coinChangeDP(coins, amt) { + const n = coins.length; + const MAX = amt + 1; + // dp テーブルを初期化 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 状態遷移:先頭行と先頭列 + for (let a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 状態遷移: 残りの行と列 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] !== MAX ? dp[n][amt] : -1; + } ``` === "TS" ```typescript title="coin_change.ts" - [class]{}-[func]{coinChangeDP} + /* コイン両替:動的計画法 */ + function coinChangeDP(coins: Array, amt: number): number { + const n = coins.length; + const MAX = amt + 1; + // dp テーブルを初期化 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 状態遷移:先頭行と先頭列 + for (let a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 状態遷移: 残りの行と列 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] !== MAX ? dp[n][amt] : -1; + } ``` === "Dart" ```dart title="coin_change.dart" - [class]{}-[func]{coinChangeDP} + /* コイン両替:動的計画法 */ + int coinChangeDP(List coins, int amt) { + int n = coins.length; + int MAX = amt + 1; + // dp テーブルを初期化 + List> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0)); + // 状態遷移:先頭行と先頭列 + for (int a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 状態遷移: 残りの行と列 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] != MAX ? dp[n][amt] : -1; + } ``` === "Rust" ```rust title="coin_change.rs" - [class]{}-[func]{coin_change_dp} + /* コイン両替:動的計画法 */ + fn coin_change_dp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + let max = amt + 1; + // dp テーブルを初期化 + let mut dp = vec![vec![0; amt + 1]; n + 1]; + // 状態遷移:先頭行と先頭列 + for a in 1..=amt { + dp[0][a] = max; + } + // 状態遷移: 残りの行と列 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1); + } + } + } + if dp[n][amt] != max { + return dp[n][amt] as i32; + } else { + -1 + } + } ``` === "C" ```c title="coin_change.c" - [class]{}-[func]{coinChangeDP} + /* コイン両替:動的計画法 */ + int coinChangeDP(int coins[], int amt, int coinsSize) { + int n = coinsSize; + int MAX = amt + 1; + // dp テーブルを初期化 + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(amt + 1, sizeof(int)); + } + // 状態遷移:先頭行と先頭列 + for (int a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 状態遷移: 残りの行と列 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + int res = dp[n][amt] != MAX ? dp[n][amt] : -1; + // メモリを解放する + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="coin_change.kt" - [class]{}-[func]{coinChangeDP} + /* コイン両替:動的計画法 */ + fun coinChangeDP(coins: IntArray, amt: Int): Int { + val n = coins.size + val MAX = amt + 1 + // dp テーブルを初期化 + val dp = Array(n + 1) { IntArray(amt + 1) } + // 状態遷移:先頭行と先頭列 + for (a in 1..amt) { + dp[0][a] = MAX + } + // 状態遷移: 残りの行と列 + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a] + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1) + } + } + } + return if (dp[n][amt] != MAX) dp[n][amt] else -1 + } ``` === "Ruby" ```ruby title="coin_change.rb" - [class]{}-[func]{coin_change_dp} + ### コイン両替:動的計画法 ### + def coin_change_dp(coins, amt) + n = coins.length + _MAX = amt + 1 + # dp テーブルを初期化 + dp = Array.new(n + 1) { Array.new(amt + 1, 0) } + # 状態遷移:先頭行と先頭列 + (1...(amt + 1)).each { |a| dp[0][a] = _MAX } + # 状態遷移: 残りの行と列 + for i in 1...(n + 1) + for a in 1...(amt + 1) + if coins[i - 1] > a + # 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a] + else + # 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[i][a] = [dp[i - 1][a], dp[i][a - coins[i - 1]] + 1].min + end + end + end + dp[n][amt] != _MAX ? dp[n][amt] : -1 + end ``` -下の図はコイン交換問題の動的プログラミングプロセスを示しており、無制限ナップサック問題と非常に似ています。 +??? pythontutor "コードの可視化" + +
+ + +次の図は硬貨交換の動的計画法の過程を示しており、完全ナップサック問題と非常によく似ています。 === "<1>" - ![コイン交換問題の動的プログラミングプロセス](unbounded_knapsack_problem.assets/coin_change_dp_step1.png){ class="animation-figure" } + ![硬貨交換問題の動的計画法の過程](unbounded_knapsack_problem.assets/coin_change_dp_step1.png){ class="animation-figure" } === "<2>" ![coin_change_dp_step2](unbounded_knapsack_problem.assets/coin_change_dp_step2.png){ class="animation-figure" } @@ -561,17 +1244,17 @@ $$ === "<15>" ![coin_change_dp_step15](unbounded_knapsack_problem.assets/coin_change_dp_step15.png){ class="animation-figure" } -

図 14-25   コイン交換問題の動的プログラミングプロセス

+

図 14-25   硬貨交換問題の動的計画法の過程

### 3.   空間最適化 -コイン交換問題の空間最適化は無制限ナップサック問題と同じ方法で処理されます: +硬貨交換の空間最適化の方法は、完全ナップサック問題と同じです。 === "Python" ```python title="coin_change.py" def coin_change_dp_comp(coins: list[int], amt: int) -> int: - """硬貨交換:空間最適化動的プログラミング""" + """コイン交換:空間最適化後の動的計画法""" n = len(coins) MAX = amt + 1 # dp テーブルを初期化 @@ -579,13 +1262,13 @@ $$ dp[0] = 0 # 状態遷移 for i in range(1, n + 1): - # 順序で走査 + # 順方向に走査する for a in range(1, amt + 1): if coins[i - 1] > a: - # 目標金額を超える場合、硬貨 i を選択しない + # 目標金額を超えるなら硬貨 i は選ばない dp[a] = dp[a] else: - # 硬貨 i を選択しないのと選択するのとで小さい値 + # 硬貨 i を選ばない場合と選ぶ場合の小さい方 dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1) return dp[amt] if dp[amt] != MAX else -1 ``` @@ -593,21 +1276,21 @@ $$ === "C++" ```cpp title="coin_change.cpp" - /* 硬貨両替:空間最適化動的プログラミング */ + /* コイン交換:空間最適化後の動的計画法 */ int coinChangeDPComp(vector &coins, int amt) { int n = coins.size(); int MAX = amt + 1; - // DPテーブルを初期化 + // dp テーブルを初期化 vector dp(amt + 1, MAX); dp[0] = 0; // 状態遷移 for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // 目標金額を超える場合、硬貨 i を選択しない + // 目標金額を超えるなら硬貨 i は選ばない dp[a] = dp[a]; } else { - // 選択しない場合と硬貨 i を選択する場合のより小さい値 + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1); } } @@ -619,11 +1302,11 @@ $$ === "Java" ```java title="coin_change.java" - /* 硬貨両替:空間最適化動的プログラミング */ + /* コイン交換:空間最適化後の動的計画法 */ int coinChangeDPComp(int[] coins, int amt) { int n = coins.length; int MAX = amt + 1; - // DPテーブルを初期化 + // dp テーブルを初期化 int[] dp = new int[amt + 1]; Arrays.fill(dp, MAX); dp[0] = 0; @@ -631,10 +1314,10 @@ $$ for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // 目標金額を超える場合、硬貨 i を選択しない + // 目標金額を超えるなら硬貨 i は選ばない dp[a] = dp[a]; } else { - // 選択しない場合と硬貨 i を選択する場合のより小さい値 + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1); } } @@ -646,84 +1329,310 @@ $$ === "C#" ```csharp title="coin_change.cs" - [class]{coin_change}-[func]{CoinChangeDPComp} + /* コイン交換:空間最適化後の動的計画法 */ + int CoinChangeDPComp(int[] coins, int amt) { + int n = coins.Length; + int MAX = amt + 1; + // dp テーブルを初期化 + int[] dp = new int[amt + 1]; + Array.Fill(dp, MAX); + dp[0] = 0; + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] != MAX ? dp[amt] : -1; + } ``` === "Go" ```go title="coin_change.go" - [class]{}-[func]{coinChangeDPComp} + /* コイン両替:動的計画法 */ + func coinChangeDPComp(coins []int, amt int) int { + n := len(coins) + max := amt + 1 + // dp テーブルを初期化 + dp := make([]int, amt+1) + for i := 1; i <= amt; i++ { + dp[i] = max + } + // 状態遷移 + for i := 1; i <= n; i++ { + // 順方向に走査する + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a] + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1))) + } + } + } + if dp[amt] != max { + return dp[amt] + } + return -1 + } ``` === "Swift" ```swift title="coin_change.swift" - [class]{}-[func]{coinChangeDPComp} + /* コイン交換:空間最適化後の動的計画法 */ + func coinChangeDPComp(coins: [Int], amt: Int) -> Int { + let n = coins.count + let MAX = amt + 1 + // dp テーブルを初期化 + var dp = Array(repeating: MAX, count: amt + 1) + dp[0] = 0 + // 状態遷移 + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a] + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1) + } + } + } + return dp[amt] != MAX ? dp[amt] : -1 + } ``` === "JS" ```javascript title="coin_change.js" - [class]{}-[func]{coinChangeDPComp} + /* コイン交換:空間最適化後の動的計画法 */ + function coinChangeDPComp(coins, amt) { + const n = coins.length; + const MAX = amt + 1; + // dp テーブルを初期化 + const dp = Array.from({ length: amt + 1 }, () => MAX); + dp[0] = 0; + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] !== MAX ? dp[amt] : -1; + } ``` === "TS" ```typescript title="coin_change.ts" - [class]{}-[func]{coinChangeDPComp} + /* コイン交換:空間最適化後の動的計画法 */ + function coinChangeDPComp(coins: Array, amt: number): number { + const n = coins.length; + const MAX = amt + 1; + // dp テーブルを初期化 + const dp = Array.from({ length: amt + 1 }, () => MAX); + dp[0] = 0; + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] !== MAX ? dp[amt] : -1; + } ``` === "Dart" ```dart title="coin_change.dart" - [class]{}-[func]{coinChangeDPComp} + /* コイン交換:空間最適化後の動的計画法 */ + int coinChangeDPComp(List coins, int amt) { + int n = coins.length; + int MAX = amt + 1; + // dp テーブルを初期化 + List dp = List.filled(amt + 1, MAX); + dp[0] = 0; + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] != MAX ? dp[amt] : -1; + } ``` === "Rust" ```rust title="coin_change.rs" - [class]{}-[func]{coin_change_dp_comp} + /* コイン交換:空間最適化後の動的計画法 */ + fn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + let max = amt + 1; + // dp テーブルを初期化 + let mut dp = vec![0; amt + 1]; + dp.fill(max); + dp[0] = 0; + // 状態遷移 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1); + } + } + } + if dp[amt] != max { + return dp[amt] as i32; + } else { + -1 + } + } ``` === "C" ```c title="coin_change.c" - [class]{}-[func]{coinChangeDPComp} + /* コイン交換:空間最適化後の動的計画法 */ + int coinChangeDPComp(int coins[], int amt, int coinsSize) { + int n = coinsSize; + int MAX = amt + 1; + // dp テーブルを初期化 + int *dp = malloc((amt + 1) * sizeof(int)); + for (int j = 1; j <= amt; j++) { + dp[j] = MAX; + } + dp[0] = 0; + + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + int res = dp[amt] != MAX ? dp[amt] : -1; + // メモリを解放する + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="coin_change.kt" - [class]{}-[func]{coinChangeDPComp} + /* コイン交換:空間最適化後の動的計画法 */ + fun coinChangeDPComp(coins: IntArray, amt: Int): Int { + val n = coins.size + val MAX = amt + 1 + // dp テーブルを初期化 + val dp = IntArray(amt + 1) + dp.fill(MAX) + dp[0] = 0 + // 状態遷移 + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a] + } else { + // 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1) + } + } + } + return if (dp[amt] != MAX) dp[amt] else -1 + } ``` === "Ruby" ```ruby title="coin_change.rb" - [class]{}-[func]{coin_change_dp_comp} + ### コイン両替:空間最適化した動的計画法 ### + def coin_change_dp_comp(coins, amt) + n = coins.length + _MAX = amt + 1 + # dp テーブルを初期化 + dp = Array.new(amt + 1, _MAX) + dp[0] = 0 + # 状態遷移 + for i in 1...(n + 1) + # 順方向に走査する + for a in 1...(amt + 1) + if coins[i - 1] > a + # 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a] + else + # 硬貨 i を選ばない場合と選ぶ場合の小さい方 + dp[a] = [dp[a], dp[a - coins[i - 1]] + 1].min + end + end + end + dp[amt] != _MAX ? dp[amt] : -1 + end ``` -## 14.5.3   コイン交換問題II +??? pythontutor "コードの可視化" + +
+ + +## 14.5.3   硬貨交換問題 II !!! question - $n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ で、目標金額は $amt$ です。各種類のコインは複数回選択でき、**目標金額を構成できるコインの組み合わせは何通りありますか**。以下の例を参照してください。 + $n$ 種類の硬貨が与えられ、$i$ 番目の硬貨の額面は $coins[i - 1]$ 、目標金額は $amt$ です。各硬貨は繰り返し選択できるとして、**目標金額を作る硬貨の組合せ数**を求めてください。例を以下の図に示します。 -![コイン交換問題IIの例データ](unbounded_knapsack_problem.assets/coin_change_ii_example.png){ class="animation-figure" } +![硬貨交換問題 II のサンプルデータ](unbounded_knapsack_problem.assets/coin_change_ii_example.png){ class="animation-figure" } -

図 14-26   コイン交換問題IIの例データ

+

図 14-26   硬貨交換問題 II のサンプルデータ

-### 1.   動的プログラミングアプローチ +### 1.   動的計画法の考え方 -前の問題と比較して、この問題の目標は組み合わせの数を決定することであるため、部分問題は次のようになります:**最初の $i$ 種類のコインを使用して金額 $a$ を構成できる組み合わせの数**。$dp$ テーブルはサイズ $(n+1) \times (amt + 1)$ の二次元行列のまま残ります。 +前問と比べて、本問の目的は組合せ数を求めることです。そのため、部分問題は **先頭 $i$ 種類の硬貨で金額 $a$ を作れる組合せ数** になります。一方、$dp$ テーブルは引き続きサイズ $(n+1) \times (amt + 1)$ の 2 次元行列です。 -現在の状態の組み合わせ数は、現在のコインを選択しない組み合わせと現在のコインを選択する組み合わせの合計です。状態遷移方程式は: +現在の状態における組合せ数は、現在の硬貨を選ばない場合と選ぶ場合の 2 つの選択肢の組合せ数の和に等しくなります。状態遷移方程式は次のとおりです。 $$ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] $$ -目標金額が $0$ の場合、目標金額を構成するのにコインは必要ないため、最初の列のすべての $dp[i, 0]$ は $1$ に初期化されるべきです。コインがない場合、任意の金額 >0 を構成することは不可能であるため、最初の行のすべての $dp[0, a]$ は $0$ に設定されるべきです。 +目標金額が $0$ のときは、どの硬貨も選ばなくても目標金額を作れるため、先頭列のすべての $dp[i, 0]$ を $1$ に初期化します。硬貨がないときは、任意の $>0$ の目標金額を作れないため、先頭行のすべての $dp[0, a]$ は $0$ になります。 ### 2.   コード実装 @@ -731,21 +1640,21 @@ $$ ```python title="coin_change_ii.py" def coin_change_ii_dp(coins: list[int], amt: int) -> int: - """硬貨交換 II:動的プログラミング""" + """コイン両替 II:動的計画法""" n = len(coins) # dp テーブルを初期化 dp = [[0] * (amt + 1) for _ in range(n + 1)] - # 最初の列を初期化 + # 先頭列を初期化する for i in range(n + 1): dp[i][0] = 1 # 状態遷移 for i in range(1, n + 1): for a in range(1, amt + 1): if coins[i - 1] > a: - # 目標金額を超える場合、硬貨 i を選択しない + # 目標金額を超えるなら硬貨 i は選ばない dp[i][a] = dp[i - 1][a] else: - # 硬貨 i を選択しないのと選択するのとの両方の選択肢の和 + # コイン i を選ばない場合と選ぶ場合の和 dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]] return dp[n][amt] ``` @@ -753,12 +1662,12 @@ $$ === "C++" ```cpp title="coin_change_ii.cpp" - /* 硬貨両替 II:動的プログラミング */ + /* コイン両替 II:動的計画法 */ int coinChangeIIDP(vector &coins, int amt) { int n = coins.size(); - // DPテーブルを初期化 + // dp テーブルを初期化 vector> dp(n + 1, vector(amt + 1, 0)); - // 最初の列を初期化 + // 先頭列を初期化する for (int i = 0; i <= n; i++) { dp[i][0] = 1; } @@ -766,10 +1675,10 @@ $$ for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // 目標金額を超える場合、硬貨 i を選択しない + // 目標金額を超えるなら硬貨 i は選ばない dp[i][a] = dp[i - 1][a]; } else { - // 選択しない場合と硬貨 i を選択する場合の2つの選択肢の合計 + // コイン i を選ばない場合と選ぶ場合の和 dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; } } @@ -781,12 +1690,12 @@ $$ === "Java" ```java title="coin_change_ii.java" - /* 硬貨両替 II:動的プログラミング */ + /* コイン両替 II:動的計画法 */ int coinChangeIIDP(int[] coins, int amt) { int n = coins.length; - // DPテーブルを初期化 + // dp テーブルを初期化 int[][] dp = new int[n + 1][amt + 1]; - // 最初の列を初期化 + // 先頭列を初期化する for (int i = 0; i <= n; i++) { dp[i][0] = 1; } @@ -794,10 +1703,10 @@ $$ for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // 目標金額を超える場合、硬貨 i を選択しない + // 目標金額を超えるなら硬貨 i は選ばない dp[i][a] = dp[i - 1][a]; } else { - // 選択しない場合と硬貨 i を選択する場合の2つの選択肢の合計 + // コイン i を選ばない場合と選ぶ場合の和 dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; } } @@ -809,85 +1718,324 @@ $$ === "C#" ```csharp title="coin_change_ii.cs" - [class]{coin_change_ii}-[func]{CoinChangeIIDP} + /* コイン両替 II:動的計画法 */ + int CoinChangeIIDP(int[] coins, int amt) { + int n = coins.Length; + // dp テーブルを初期化 + int[,] dp = new int[n + 1, amt + 1]; + // 先頭列を初期化する + for (int i = 0; i <= n; i++) { + dp[i, 0] = 1; + } + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i, a] = dp[i - 1, a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]]; + } + } + } + return dp[n, amt]; + } ``` === "Go" ```go title="coin_change_ii.go" - [class]{}-[func]{coinChangeIIDP} + /* コイン両替 II:動的計画法 */ + func coinChangeIIDP(coins []int, amt int) int { + n := len(coins) + // dp テーブルを初期化 + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, amt+1) + } + // 先頭列を初期化する + for i := 0; i <= n; i++ { + dp[i][0] = 1 + } + // 状態遷移: 残りの行と列 + for i := 1; i <= n; i++ { + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i-1][a] + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]] + } + } + } + return dp[n][amt] + } ``` === "Swift" ```swift title="coin_change_ii.swift" - [class]{}-[func]{coinChangeIIDP} + /* コイン両替 II:動的計画法 */ + func coinChangeIIDP(coins: [Int], amt: Int) -> Int { + let n = coins.count + // dp テーブルを初期化 + var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1) + // 先頭列を初期化する + for i in 0 ... n { + dp[i][0] = 1 + } + // 状態遷移 + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a] + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]] + } + } + } + return dp[n][amt] + } ``` === "JS" ```javascript title="coin_change_ii.js" - [class]{}-[func]{coinChangeIIDP} + /* コイン両替 II:動的計画法 */ + function coinChangeIIDP(coins, amt) { + const n = coins.length; + // dp テーブルを初期化 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 先頭列を初期化する + for (let i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } ``` === "TS" ```typescript title="coin_change_ii.ts" - [class]{}-[func]{coinChangeIIDP} + /* コイン両替 II:動的計画法 */ + function coinChangeIIDP(coins: Array, amt: number): number { + const n = coins.length; + // dp テーブルを初期化 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 先頭列を初期化する + for (let i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } ``` === "Dart" ```dart title="coin_change_ii.dart" - [class]{}-[func]{coinChangeIIDP} + /* コイン両替 II:動的計画法 */ + int coinChangeIIDP(List coins, int amt) { + int n = coins.length; + // dp テーブルを初期化 + List> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0)); + // 先頭列を初期化する + for (int i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } ``` === "Rust" ```rust title="coin_change_ii.rs" - [class]{}-[func]{coin_change_ii_dp} + /* コイン両替 II:動的計画法 */ + fn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + // dp テーブルを初期化 + let mut dp = vec![vec![0; amt + 1]; n + 1]; + // 先頭列を初期化する + for i in 0..=n { + dp[i][0] = 1; + } + // 状態遷移 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize]; + } + } + } + dp[n][amt] + } ``` === "C" ```c title="coin_change_ii.c" - [class]{}-[func]{coinChangeIIDP} + /* コイン両替 II:動的計画法 */ + int coinChangeIIDP(int coins[], int amt, int coinsSize) { + int n = coinsSize; + // dp テーブルを初期化 + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(amt + 1, sizeof(int)); + } + // 先頭列を初期化する + for (int i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + int res = dp[n][amt]; + // メモリを解放する + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="coin_change_ii.kt" - [class]{}-[func]{coinChangeIIDP} + /* コイン両替 II:動的計画法 */ + fun coinChangeIIDP(coins: IntArray, amt: Int): Int { + val n = coins.size + // dp テーブルを初期化 + val dp = Array(n + 1) { IntArray(amt + 1) } + // 先頭列を初期化する + for (i in 0..n) { + dp[i][0] = 1 + } + // 状態遷移 + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a] + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]] + } + } + } + return dp[n][amt] + } ``` === "Ruby" ```ruby title="coin_change_ii.rb" - [class]{}-[func]{coin_change_ii_dp} + ### コイン両替 II:動的計画法 ### + def coin_change_ii_dp(coins, amt) + n = coins.length + # dp テーブルを初期化 + dp = Array.new(n + 1) { Array.new(amt + 1, 0) } + # 先頭列を初期化する + (0...(n + 1)).each { |i| dp[i][0] = 1 } + # 状態遷移 + for i in 1...(n + 1) + for a in 1...(amt + 1) + if coins[i - 1] > a + # 目標金額を超えるなら硬貨 i は選ばない + dp[i][a] = dp[i - 1][a] + else + # コイン i を選ばない場合と選ぶ場合の和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]] + end + end + end + dp[n][amt] + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 3.   空間最適化 -空間最適化アプローチは同じで、コインの次元を削除するだけです: +空間最適化の方法も同様で、硬貨の次元を削除するだけです。 === "Python" ```python title="coin_change_ii.py" def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int: - """硬貨交換 II:空間最適化動的プログラミング""" + """コイン両替 II:空間最適化した動的計画法""" n = len(coins) # dp テーブルを初期化 dp = [0] * (amt + 1) dp[0] = 1 # 状態遷移 for i in range(1, n + 1): - # 順序で走査 + # 順方向に走査する for a in range(1, amt + 1): if coins[i - 1] > a: - # 目標金額を超える場合、硬貨 i を選択しない + # 目標金額を超えるなら硬貨 i は選ばない dp[a] = dp[a] else: - # 硬貨 i を選択しないのと選択するのとの両方の選択肢の和 + # コイン i を選ばない場合と選ぶ場合の和 dp[a] = dp[a] + dp[a - coins[i - 1]] return dp[amt] ``` @@ -895,20 +2043,20 @@ $$ === "C++" ```cpp title="coin_change_ii.cpp" - /* 硬貨両替 II:空間最適化動的プログラミング */ + /* コイン両替 II:空間最適化した動的計画法 */ int coinChangeIIDPComp(vector &coins, int amt) { int n = coins.size(); - // DPテーブルを初期化 + // dp テーブルを初期化 vector dp(amt + 1, 0); dp[0] = 1; // 状態遷移 for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // 目標金額を超える場合、硬貨 i を選択しない + // 目標金額を超えるなら硬貨 i は選ばない dp[a] = dp[a]; } else { - // 選択しない場合と硬貨 i を選択する場合の2つの選択肢の合計 + // コイン i を選ばない場合と選ぶ場合の和 dp[a] = dp[a] + dp[a - coins[i - 1]]; } } @@ -920,20 +2068,20 @@ $$ === "Java" ```java title="coin_change_ii.java" - /* 硬貨両替 II:空間最適化動的プログラミング */ + /* コイン両替 II:空間最適化した動的計画法 */ int coinChangeIIDPComp(int[] coins, int amt) { int n = coins.length; - // DPテーブルを初期化 + // dp テーブルを初期化 int[] dp = new int[amt + 1]; dp[0] = 1; // 状態遷移 for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // 目標金額を超える場合、硬貨 i を選択しない + // 目標金額を超えるなら硬貨 i は選ばない dp[a] = dp[a]; } else { - // 選択しない場合と硬貨 i を選択する場合の2つの選択肢の合計 + // コイン i を選ばない場合と選ぶ場合の和 dp[a] = dp[a] + dp[a - coins[i - 1]]; } } @@ -945,59 +2093,259 @@ $$ === "C#" ```csharp title="coin_change_ii.cs" - [class]{coin_change_ii}-[func]{CoinChangeIIDPComp} + /* コイン両替 II:空間最適化した動的計画法 */ + int CoinChangeIIDPComp(int[] coins, int amt) { + int n = coins.Length; + // dp テーブルを初期化 + int[] dp = new int[amt + 1]; + dp[0] = 1; + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "Go" ```go title="coin_change_ii.go" - [class]{}-[func]{coinChangeIIDPComp} + /* コイン両替 II:空間最適化した動的計画法 */ + func coinChangeIIDPComp(coins []int, amt int) int { + n := len(coins) + // dp テーブルを初期化 + dp := make([]int, amt+1) + dp[0] = 1 + // 状態遷移 + for i := 1; i <= n; i++ { + // 順方向に走査する + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a] + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[a] = dp[a] + dp[a-coins[i-1]] + } + } + } + return dp[amt] + } ``` === "Swift" ```swift title="coin_change_ii.swift" - [class]{}-[func]{coinChangeIIDPComp} + /* コイン両替 II:空間最適化した動的計画法 */ + func coinChangeIIDPComp(coins: [Int], amt: Int) -> Int { + let n = coins.count + // dp テーブルを初期化 + var dp = Array(repeating: 0, count: amt + 1) + dp[0] = 1 + // 状態遷移 + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a] + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[a] = dp[a] + dp[a - coins[i - 1]] + } + } + } + return dp[amt] + } ``` === "JS" ```javascript title="coin_change_ii.js" - [class]{}-[func]{coinChangeIIDPComp} + /* コイン両替 II:空間最適化した動的計画法 */ + function coinChangeIIDPComp(coins, amt) { + const n = coins.length; + // dp テーブルを初期化 + const dp = Array.from({ length: amt + 1 }, () => 0); + dp[0] = 1; + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "TS" ```typescript title="coin_change_ii.ts" - [class]{}-[func]{coinChangeIIDPComp} + /* コイン両替 II:空間最適化した動的計画法 */ + function coinChangeIIDPComp(coins: Array, amt: number): number { + const n = coins.length; + // dp テーブルを初期化 + const dp = Array.from({ length: amt + 1 }, () => 0); + dp[0] = 1; + // 状態遷移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "Dart" ```dart title="coin_change_ii.dart" - [class]{}-[func]{coinChangeIIDPComp} + /* コイン両替 II:空間最適化した動的計画法 */ + int coinChangeIIDPComp(List coins, int amt) { + int n = coins.length; + // dp テーブルを初期化 + List dp = List.filled(amt + 1, 0); + dp[0] = 1; + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "Rust" ```rust title="coin_change_ii.rs" - [class]{}-[func]{coin_change_ii_dp_comp} + /* コイン両替 II:空間最適化した動的計画法 */ + fn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + // dp テーブルを初期化 + let mut dp = vec![0; amt + 1]; + dp[0] = 1; + // 状態遷移 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[a] = dp[a] + dp[a - coins[i - 1] as usize]; + } + } + } + dp[amt] + } ``` === "C" ```c title="coin_change_ii.c" - [class]{}-[func]{coinChangeIIDPComp} + /* コイン両替 II:空間最適化した動的計画法 */ + int coinChangeIIDPComp(int coins[], int amt, int coinsSize) { + int n = coinsSize; + // dp テーブルを初期化 + int *dp = calloc(amt + 1, sizeof(int)); + dp[0] = 1; + // 状態遷移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a]; + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + int res = dp[amt]; + // メモリを解放する + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="coin_change_ii.kt" - [class]{}-[func]{coinChangeIIDPComp} + /* コイン両替 II:空間最適化した動的計画法 */ + fun coinChangeIIDPComp(coins: IntArray, amt: Int): Int { + val n = coins.size + // dp テーブルを初期化 + val dp = IntArray(amt + 1) + dp[0] = 1 + // 状態遷移 + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a] + } else { + // コイン i を選ばない場合と選ぶ場合の和 + dp[a] = dp[a] + dp[a - coins[i - 1]] + } + } + } + return dp[amt] + } ``` === "Ruby" ```ruby title="coin_change_ii.rb" - [class]{}-[func]{coin_change_ii_dp_comp} + ### コイン両替 II:空間最適化した動的計画法 ### + def coin_change_ii_dp_comp(coins, amt) + n = coins.length + # dp テーブルを初期化 + dp = Array.new(amt + 1, 0) + dp[0] = 1 + # 状態遷移 + for i in 1...(n + 1) + # 順方向に走査する + for a in 1...(amt + 1) + if coins[i - 1] > a + # 目標金額を超えるなら硬貨 i は選ばない + dp[a] = dp[a] + else + # コイン i を選ばない場合と選ぶ場合の和 + dp[a] = dp[a] + dp[a - coins[i - 1]] + end + end + end + dp[amt] + end ``` + +??? pythontutor "コードの可視化" + +
+ diff --git a/ja/docs/chapter_graph/graph.md b/ja/docs/chapter_graph/graph.md index a460f3a12..cfd7eca6b 100644 --- a/ja/docs/chapter_graph/graph.md +++ b/ja/docs/chapter_graph/graph.md @@ -4,7 +4,7 @@ comments: true # 9.1   グラフ -グラフは非線形データ構造の一種で、頂点で構成されます。グラフ$G$は、頂点の集合$V$と辺の集合$E$の組み合わせとして抽象的に表現できます。以下の例は、5つの頂点と7つの辺を含むグラフを示しています。 +グラフ(graph)は、頂点(vertex)辺(edge)から構成される非線形データ構造です。グラフ $G$ は、頂点集合 $V$ と辺集合 $E$ からなる集合として抽象的に表せます。以下の例は、5 個の頂点と 7 本の辺を含むグラフを示しています。 $$ \begin{aligned} @@ -14,7 +14,7 @@ G & = \{ V, E \} \newline \end{aligned} $$ -頂点をノード、辺をノードを接続する参照(ポインタ)と見なすと、グラフは連結リストから拡張されたデータ構造として見ることができます。下図に示すように、**線形関係(連結リスト)や分割統治関係(木)と比較して、ネットワーク関係(グラフ)は自由度が高いため、より複雑です**。 +頂点をノード、辺を各ノードをつなぐ参照(ポインタ)とみなせば、グラフは連結リストを拡張したデータ構造の一種と捉えられます。次の図に示すように、**線形関係(連結リスト)や分治関係(木)と比べて、ネットワーク関係(グラフ)は自由度が高く**、そのぶん複雑です。 ![連結リスト、木、グラフの関係](graph.assets/linkedlist_tree_graph.png){ class="animation-figure" } @@ -22,82 +22,82 @@ $$ ## 9.1.1   グラフの一般的な種類と用語 -グラフは、辺に方向があるかどうかによって無向グラフ有向グラフに分けることができます(下図参照)。 +辺が方向性を持つかどうかに応じて、無向グラフ(undirected graph)有向グラフ(directed graph)に分けられます。次の図のとおりです。 -- 無向グラフでは、辺は2つの頂点間の「双方向」接続を表します。例えば、Facebookの「友達」関係です。 -- 有向グラフでは、辺に方向性があります。つまり、辺$A \rightarrow B$と$A \leftarrow B$は互いに独立しています。例えば、InstagramやTikTokの「フォロー」と「フォロワー」の関係です。 +- 無向グラフでは、辺は 2 つの頂点間の「双方向」の接続関係を表します。例えば WeChat や QQ における「友だち関係」です。 +- 有向グラフでは、辺は方向性を持ち、すなわち $A \rightarrow B$ と $A \leftarrow B$ の 2 方向の辺は互いに独立です。例えば Weibo や Douyin における「フォロー」と「フォロワー」の関係です。 ![有向グラフと無向グラフ](graph.assets/directed_graph.png){ class="animation-figure" }

図 9-2   有向グラフと無向グラフ

-すべての頂点が接続されているかどうかによって、グラフは連結グラフ非連結グラフに分けることができます(下図参照)。 +すべての頂点が連結しているかどうかに応じて、連結グラフ(connected graph)非連結グラフ(disconnected graph)に分けられます。次の図のとおりです。 -- 連結グラフでは、任意の頂点から開始して他の任意の頂点に到達することが可能です。 -- 非連結グラフでは、任意の開始頂点から到達できない頂点が少なくとも1つ存在します。 +- 連結グラフでは、ある頂点から出発すると、ほかの任意の頂点に到達できます。 +- 非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。 ![連結グラフと非連結グラフ](graph.assets/connected_graph.png){ class="animation-figure" }

図 9-3   連結グラフと非連結グラフ

-辺に重み変数を追加することもでき、その結果として重み付きグラフが生まれます(下図参照)。例えば、Instagramでは、システムがあなたと他のユーザーとの間の相互作用レベル(いいね、閲覧、コメントなど)によってフォロワーとフォロー中のリストをソートします。このような相互作用ネットワークは重み付きグラフで表現できます。 +辺に「重み」の変数を追加すると、次の図に示すような重み付きグラフ(weighted graph)が得られます。例えば『Honor of Kings』のようなモバイルゲームでは、システムが共にプレイした時間に基づいてプレイヤー間の「親密度」を計算します。この親密度ネットワークは重み付きグラフで表せます。 ![重み付きグラフと重みなしグラフ](graph.assets/weighted_graph.png){ class="animation-figure" }

図 9-4   重み付きグラフと重みなしグラフ

-グラフデータ構造には、以下のような一般的に使用される用語があります。 +グラフというデータ構造には、次のような基本用語があります。 -- 隣接:2つの頂点を接続する辺がある場合、これら2つの頂点は「隣接」していると言われます。上図では、頂点1の隣接頂点は頂点2、3、5です。 -- パス:頂点Aから頂点Bまでに通過する辺のシーケンスを、AからBへのパスと呼びます。上図では、辺のシーケンス1-5-2-4は頂点1から頂点4へのパスです。 -- 次数:頂点が持つ辺の数です。有向グラフの場合、入次数はその頂点を指す辺の数、出次数はその頂点から出る辺の数を指します。 +- 隣接(adjacency):2 つの頂点の間に辺が存在するとき、この 2 つの頂点は「隣接している」といいます。上図では、頂点 1 に隣接する頂点は 2、3、5 です。 +- 経路(path):頂点 A から頂点 B までに通過する辺で構成された列を、A から B への「経路」と呼びます。上図では、辺の列 1-5-2-4 は頂点 1 から頂点 4 への 1 本の経路です。 +- 次数(degree):ある頂点が持つ辺の本数です。有向グラフでは、入次数(in-degree)はその頂点に向かう辺の本数を表し、出次数(out-degree)はその頂点から出る辺の本数を表します。 ## 9.1.2   グラフの表現 -グラフの一般的な表現には「隣接行列」と「隣接リスト」があります。以下の例では無向グラフを使用します。 +グラフの一般的な表現方法には「隣接行列」と「隣接リスト」があります。以下では無向グラフを例に説明します。 ### 1.   隣接行列 -グラフの頂点数を$n$とすると、隣接行列は$n \times n$の行列を使用してグラフを表現します。各行(列)は頂点を表し、行列要素は辺を表し、2つの頂点間に辺があるかどうかを$1$または$0$で示します。 +グラフの頂点数を $n$ とすると、隣接行列(adjacency matrix)は $n \times n$ の行列を用いてグラフを表します。各行(列)は 1 つの頂点を表し、行列要素は辺を表します。$1$ または $0$ を用いて、2 つの頂点の間に辺があるかどうかを示します。 -下図に示すように、隣接行列を$M$、頂点のリストを$V$とすると、行列要素$M[i, j] = 1$は頂点$V[i]$と頂点$V[j]$の間に辺があることを示し、逆に$M[i, j] = 0$は2つの頂点間に辺がないことを示します。 +次の図のように、隣接行列を $M$、頂点リストを $V$ とすると、行列要素 $M[i, j] = 1$ は頂点 $V[i]$ から頂点 $V[j]$ への辺が存在することを表し、逆に $M[i, j] = 0$ は 2 つの頂点の間に辺がないことを表します。 -![隣接行列によるグラフの表現](graph.assets/adjacency_matrix.png){ class="animation-figure" } +![グラフの隣接行列による表現](graph.assets/adjacency_matrix.png){ class="animation-figure" } -

図 9-5   隣接行列によるグラフの表現

+

図 9-5   グラフの隣接行列による表現

-隣接行列には以下の特性があります。 +隣接行列には次の特徴があります。 -- 頂点は自分自身に接続することはできないため、隣接行列の主対角線上の要素は意味がありません。 -- 無向グラフの場合、両方向の辺は等価であるため、隣接行列は主対角線に関して対称です。 -- 隣接行列の要素を$1$と$0$から重みに置き換えることで、重み付きグラフを表現できます。 +- 単純グラフでは、頂点は自分自身とは接続できないため、このとき隣接行列の主対角線上の要素には意味がありません。 +- 無向グラフでは、2 方向の辺は等価であるため、このとき隣接行列は主対角線に関して対称です。 +- 隣接行列の要素を $1$ と $0$ から重みに置き換えると、重み付きグラフを表せます。 -隣接行列でグラフを表現する場合、行列要素に直接アクセスして辺を取得できるため、追加、削除、検索、変更の操作が効率的で、すべて時間計算量$O(1)$です。ただし、行列の空間計算量は$O(n^2)$で、より多くのメモリを消費します。 +隣接行列でグラフを表す場合、行列要素に直接アクセスして辺を取得できるため、追加・削除・検索・更新の操作効率は高く、時間計算量はいずれも $O(1)$ です。しかし、行列の空間計算量は $O(n^2)$ であり、メモリ使用量は多くなります。 ### 2.   隣接リスト -隣接リストは$n$個の連結リストを使用してグラフを表現し、各連結リストノードは頂点を表します。$i$番目の連結リストは頂点$i$に対応し、すべての隣接頂点(その頂点に接続された頂点)を含みます。下図は隣接リストを使用して格納されたグラフの例を示しています。 +隣接リスト(adjacency list)は、$n$ 本の連結リストを使ってグラフを表します。連結リストのノードは頂点を表します。第 $i$ 本の連結リストは頂点 $i$ に対応し、その頂点に隣接するすべての頂点(その頂点と接続された頂点)を格納します。次の図は、隣接リストで保存したグラフの例です。 -![隣接リストによるグラフの表現](graph.assets/adjacency_list.png){ class="animation-figure" } +![グラフの隣接リストによる表現](graph.assets/adjacency_list.png){ class="animation-figure" } -

図 9-6   隣接リストによるグラフの表現

+

図 9-6   グラフの隣接リストによる表現

-隣接リストは実際の辺のみを格納し、辺の総数は$n^2$よりもはるかに少ないことが多く、より空間効率的です。ただし、隣接リストで辺を見つけるには連結リストを走査する必要があるため、その時間効率は隣接行列ほど良くありません。 +隣接リストは実際に存在する辺だけを格納し、辺の総数は通常 $n^2$ よりはるかに小さいため、より省スペースです。しかし、隣接リストでは辺を見つけるために連結リストを走査する必要があるため、時間効率は隣接行列に及びません。 -上図を観察すると、**隣接リストの構造はハッシュテーブルの「チェイン法」と非常に似ているため、同様の方法を使用して効率を最適化できます**。例えば、連結リストが長い場合、それをAVL木や赤黒木に変換して、時間効率を$O(n)$から$O(\log n)$に最適化できます。連結リストをハッシュテーブルに変換することで、時間計算量を$O(1)$に削減することもできます。 +上図を見ると、**隣接リストの構造はハッシュテーブルにおける「連鎖アドレス法」と非常によく似ているため、同様の方法で効率を最適化できます**。例えば、連結リストが長い場合は AVL 木や赤黒木に変換して時間効率を $O(n)$ から $O(\log n)$ に改善できます。さらに、連結リストをハッシュテーブルに変換すれば、時間計算量を $O(1)$ まで下げられます。 ## 9.1.3   グラフの一般的な応用 -下表に示すように、多くの現実世界のシステムはグラフでモデル化でき、対応する問題はグラフ計算問題に削減できます。 +次の表のように、多くの現実のシステムはグラフでモデル化でき、対応する問題もグラフ計算の問題に帰着できます。 -

表 9-1   現実生活の一般的なグラフ

+

表 9-1   現実世界でよく見られるグラフ

-| | 頂点 | 辺 | グラフ計算問題 | -| -------------- | -------------- | -------------------------------- | --------------------------- | -| ソーシャルネットワーク | ユーザー | フォロー / フォロワー関係 | 潜在的フォロー推薦 | -| 地下鉄路線 | 駅 | 駅間の接続性 | 最短ルート推薦 | -| 太陽系 | 天体 | 天体間の重力 | 惑星軌道計算 | +| | 頂点 | 辺 | グラフ計算問題 | +| -------- | ---- | -------------------- | ------------ | +| ソーシャルネットワーク | ユーザー | 友だち関係 | 潜在的な友だちの推薦 | +| 地下鉄路線 | 駅 | 駅間の接続性 | 最短経路の推薦 | +| 太陽系 | 天体 | 天体間の万有引力作用 | 惑星軌道の計算 |
diff --git a/ja/docs/chapter_graph/graph_operations.md b/ja/docs/chapter_graph/graph_operations.md index 76c14da1e..64abc24f2 100644 --- a/ja/docs/chapter_graph/graph_operations.md +++ b/ja/docs/chapter_graph/graph_operations.md @@ -4,35 +4,35 @@ comments: true # 9.2   グラフの基本操作 -グラフの基本操作は「辺」に対する操作と「頂点」に対する操作に分けることができます。「隣接行列」と「隣接リスト」の2つの表現方法の下では、実装が異なります。 +グラフの基本操作は、「辺」に対する操作と「頂点」に対する操作に分けられます。「隣接行列」と「隣接リスト」の 2 つの表現方法では、実装方法が異なります。 ## 9.2.1   隣接行列に基づく実装 -$n$個の頂点を持つ無向グラフが与えられた場合、さまざまな操作は下図のように実装されます。 +頂点数が $n$ の無向グラフを与えると、各種操作の実装方法は次図のとおりです。 -- **辺の追加または削除**:隣接行列内の指定された辺を直接変更し、$O(1)$時間を使用します。無向グラフであるため、両方向の辺を同時に更新する必要があります。 -- **頂点の追加**:隣接行列の末尾に行と列を追加し、すべて$0$で埋めます。$O(n)$時間を使用します。 -- **頂点の削除**:隣接行列内の行と列を削除します。最悪の場合は最初の行と列が削除されるときで、$(n-1)^2$個の要素を「上と左に移動」する必要があり、$O(n^2)$時間を使用します。 -- **初期化**:$n$個の頂点を渡し、長さ$n$の頂点リスト`vertices`を初期化し、$O(n)$時間を使用します。$n \times n$サイズの隣接行列`adjMat`を初期化し、$O(n^2)$時間を使用します。 +- **辺の追加または削除**:隣接行列で指定した辺を直接変更すればよく、$O(1)$ 時間です。無向グラフであるため、2 方向の辺を同時に更新する必要があります。 +- **頂点の追加**:隣接行列の末尾に 1 行 1 列を追加し、すべてを $0$ で埋めればよく、$O(n)$ 時間です。 +- **頂点の削除**:隣接行列から 1 行 1 列を削除します。先頭行と先頭列を削除する場合が最悪で、$(n-1)^2$ 個の要素を「左上へ移動」させる必要があるため、$O(n^2)$ 時間です。 +- **初期化**:$n$ 個の頂点を受け取り、長さ $n$ の頂点リスト `vertices` を初期化するのに $O(n)$ 時間、サイズ $n \times n$ の隣接行列 `adjMat` を初期化するのに $O(n^2)$ 時間かかります。 -=== "隣接行列の初期化" - ![隣接行列での初期化、辺の追加と削除、頂点の追加と削除](graph_operations.assets/adjacency_matrix_step1_initialization.png){ class="animation-figure" } +=== "<1>" + ![隣接行列の初期化、辺の追加と削除、頂点の追加と削除](graph_operations.assets/adjacency_matrix_step1_initialization.png){ class="animation-figure" } -=== "辺の追加" +=== "<2>" ![adjacency_matrix_add_edge](graph_operations.assets/adjacency_matrix_step2_add_edge.png){ class="animation-figure" } -=== "辺の削除" +=== "<3>" ![adjacency_matrix_remove_edge](graph_operations.assets/adjacency_matrix_step3_remove_edge.png){ class="animation-figure" } -=== "頂点の追加" +=== "<4>" ![adjacency_matrix_add_vertex](graph_operations.assets/adjacency_matrix_step4_add_vertex.png){ class="animation-figure" } -=== "頂点の削除" +=== "<5>" ![adjacency_matrix_remove_vertex](graph_operations.assets/adjacency_matrix_step5_remove_vertex.png){ class="animation-figure" } -

図 9-7   隣接行列での初期化、辺の追加と削除、頂点の追加と削除

+

図 9-7   隣接行列の初期化、辺の追加と削除、頂点の追加と削除

-以下は隣接行列を使用して表現されたグラフの実装コードです: +以下は、隣接行列でグラフを表した実装コードです: === "Python" @@ -42,15 +42,15 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま def __init__(self, vertices: list[int], edges: list[list[int]]): """コンストラクタ""" - # 頂点リスト、要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す + # 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す self.vertices: list[int] = [] - # 隣接行列、行と列のインデックスは「頂点インデックス」に対応 + # 隣接行列。行・列のインデックスは「頂点インデックス」に対応 self.adj_mat: list[list[int]] = [] # 頂点を追加 for val in vertices: self.add_vertex(val) # 辺を追加 - # edges要素は頂点インデックスを表す + # 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する for e in edges: self.add_edge(e[0], e[1]) @@ -61,12 +61,12 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま def add_vertex(self, val: int): """頂点を追加""" n = self.size() - # 頂点リストに新しい頂点値を追加 + # 頂点リストに新しい頂点の値を追加 self.vertices.append(val) - # 隣接行列に行を追加 + # 隣接行列に 1 行追加 new_row = [0] * n self.adj_mat.append(new_row) - # 隣接行列に列を追加 + # 隣接行列に 1 列追加 for row in self.adj_mat: row.append(0) @@ -74,28 +74,28 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま """頂点を削除""" if index >= self.size(): raise IndexError() - # 頂点リストから`index`の頂点を削除 + # 頂点リストから index の頂点を削除する self.vertices.pop(index) - # 隣接行列から`index`の行を削除 + # 隣接行列で index 行を削除する self.adj_mat.pop(index) - # 隣接行列から`index`の列を削除 + # 隣接行列で index 列を削除する for row in self.adj_mat: row.pop(index) def add_edge(self, i: int, j: int): """辺を追加""" - # パラメータi、jは頂点要素のインデックスに対応 - # インデックスの範囲外と等価性を処理 + # パラメータ i, j は vertices の要素インデックスに対応する + # 範囲外と同値の場合の処理 if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j: raise IndexError() - # 無向グラフでは、隣接行列は主対角線について対称、すなわち (i, j) == (j, i) を満たす + # 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす self.adj_mat[i][j] = 1 self.adj_mat[j][i] = 1 def remove_edge(self, i: int, j: int): """辺を削除""" - # パラメータi、jは頂点要素のインデックスに対応 - # インデックスの範囲外と等価性を処理 + # パラメータ i, j は vertices の要素インデックスに対応する + # 範囲外と同値の場合の処理 if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j: raise IndexError() self.adj_mat[i][j] = 0 @@ -113,8 +113,8 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま ```cpp title="graph_adjacency_matrix.cpp" /* 隣接行列に基づく無向グラフクラス */ class GraphAdjMat { - vector vertices; // 頂点リスト、要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す - vector> adjMat; // 隣接行列、行と列のインデックスは「頂点インデックス」に対応 + vector vertices; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す + vector> adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応 public: /* コンストラクタ */ @@ -124,7 +124,7 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま addVertex(val); } // 辺を追加 - // 辺の要素は頂点インデックスを表す + // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する for (const vector &edge : edges) { addEdge(edge[0], edge[1]); } @@ -138,11 +138,11 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま /* 頂点を追加 */ void addVertex(int val) { int n = size(); - // 頂点リストに新しい頂点値を追加 + // 頂点リストに新しい頂点の値を追加 vertices.push_back(val); - // 隣接行列に行を追加 + // 隣接行列に 1 行追加 adjMat.emplace_back(vector(n, 0)); - // 隣接行列に列を追加 + // 隣接行列に 1 列追加 for (vector &row : adjMat) { row.push_back(0); } @@ -151,42 +151,42 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま /* 頂点を削除 */ void removeVertex(int index) { if (index >= size()) { - throw out_of_range("Vertex does not exist"); + throw out_of_range("頂点が存在しません"); } - // 頂点リストから`index`の頂点を削除 + // 頂点リストから index の頂点を削除する vertices.erase(vertices.begin() + index); - // 隣接行列から`index`の行を削除 + // 隣接行列で index 行を削除する adjMat.erase(adjMat.begin() + index); - // 隣接行列から`index`の列を削除 + // 隣接行列で index 列を削除する for (vector &row : adjMat) { row.erase(row.begin() + index); } } /* 辺を追加 */ - // パラメータi、jは頂点要素のインデックスに対応 + // 引数 i, j は vertices の要素インデックスに対応する void addEdge(int i, int j) { - // インデックス範囲外と等価性を処理 + // インデックスの範囲外と等値の処理 if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { - throw out_of_range("Vertex does not exist"); + throw out_of_range("頂点が存在しません"); } - // 無向グラフでは、隣接行列は主対角線について対称、即ち(i, j) == (j, i)を満たす + // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす adjMat[i][j] = 1; adjMat[j][i] = 1; } /* 辺を削除 */ - // パラメータi、jは頂点要素のインデックスに対応 + // 引数 i, j は vertices の要素インデックスに対応する void removeEdge(int i, int j) { - // インデックス範囲外と等価性を処理 + // インデックスの範囲外と等値の処理 if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { - throw out_of_range("Vertex does not exist"); + throw out_of_range("頂点が存在しません"); } adjMat[i][j] = 0; adjMat[j][i] = 0; } - /* 隣接行列を印刷 */ + /* 隣接行列を出力 */ void print() { cout << "頂点リスト = "; printVector(vertices); @@ -201,8 +201,8 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま ```java title="graph_adjacency_matrix.java" /* 隣接行列に基づく無向グラフクラス */ class GraphAdjMat { - List vertices; // 頂点リスト、要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す - List> adjMat; // 隣接行列、行と列のインデックスは「頂点インデックス」に対応 + List vertices; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す + List> adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応 /* コンストラクタ */ public GraphAdjMat(int[] vertices, int[][] edges) { @@ -213,7 +213,7 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま addVertex(val); } // 辺を追加 - // 辺の要素は頂点インデックスを表す + // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する for (int[] e : edges) { addEdge(e[0], e[1]); } @@ -227,15 +227,15 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま /* 頂点を追加 */ public void addVertex(int val) { int n = size(); - // 頂点リストに新しい頂点値を追加 + // 頂点リストに新しい頂点の値を追加 vertices.add(val); - // 隣接行列に行を追加 + // 隣接行列に 1 行追加 List newRow = new ArrayList<>(n); for (int j = 0; j < n; j++) { newRow.add(0); } adjMat.add(newRow); - // 隣接行列に列を追加 + // 隣接行列に 1 列追加 for (List row : adjMat) { row.add(0); } @@ -245,31 +245,31 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま public void removeVertex(int index) { if (index >= size()) throw new IndexOutOfBoundsException(); - // 頂点リストから `index` の頂点を削除 + // 頂点リストから index の頂点を削除する vertices.remove(index); - // 隣接行列から `index` の行を削除 + // 隣接行列で index 行を削除する adjMat.remove(index); - // 隣接行列から `index` の列を削除 + // 隣接行列で index 列を削除する for (List row : adjMat) { row.remove(index); } } /* 辺を追加 */ - // パラメータ i、j は頂点要素のインデックスに対応 + // 引数 i, j は vertices の要素インデックスに対応する public void addEdge(int i, int j) { - // インデックスの範囲外と等価性を処理 + // インデックスの範囲外と等値の処理 if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) throw new IndexOutOfBoundsException(); - // 無向グラフでは、隣接行列は主対角線について対称、すなわち (i, j) == (j, i) を満たす + // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす adjMat.get(i).set(j, 1); adjMat.get(j).set(i, 1); } /* 辺を削除 */ - // パラメータ i、j は頂点要素のインデックスに対応 + // 引数 i, j は vertices の要素インデックスに対応する public void removeEdge(int i, int j) { - // インデックスの範囲外と等価性を処理 + // インデックスの範囲外と等値の処理 if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) throw new IndexOutOfBoundsException(); adjMat.get(i).set(j, 0); @@ -289,96 +289,961 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま === "C#" ```csharp title="graph_adjacency_matrix.cs" - [class]{GraphAdjMat}-[func]{} + /* 隣接行列に基づく無向グラフクラス */ + class GraphAdjMat { + List vertices; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す + List> adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応 + + /* コンストラクタ */ + public GraphAdjMat(int[] vertices, int[][] edges) { + this.vertices = []; + this.adjMat = []; + // 頂点を追加 + foreach (int val in vertices) { + AddVertex(val); + } + // 辺を追加 + // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する + foreach (int[] e in edges) { + AddEdge(e[0], e[1]); + } + } + + /* 頂点数を取得 */ + int Size() { + return vertices.Count; + } + + /* 頂点を追加 */ + public void AddVertex(int val) { + int n = Size(); + // 頂点リストに新しい頂点の値を追加 + vertices.Add(val); + // 隣接行列に 1 行追加 + List newRow = new(n); + for (int j = 0; j < n; j++) { + newRow.Add(0); + } + adjMat.Add(newRow); + // 隣接行列に 1 列追加 + foreach (List row in adjMat) { + row.Add(0); + } + } + + /* 頂点を削除 */ + public void RemoveVertex(int index) { + if (index >= Size()) + throw new IndexOutOfRangeException(); + // 頂点リストから index の頂点を削除する + vertices.RemoveAt(index); + // 隣接行列で index 行を削除する + adjMat.RemoveAt(index); + // 隣接行列で index 列を削除する + foreach (List row in adjMat) { + row.RemoveAt(index); + } + } + + /* 辺を追加 */ + // 引数 i, j は vertices の要素インデックスに対応する + public void AddEdge(int i, int j) { + // インデックスの範囲外と等値の処理 + if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j) + throw new IndexOutOfRangeException(); + // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす + adjMat[i][j] = 1; + adjMat[j][i] = 1; + } + + /* 辺を削除 */ + // 引数 i, j は vertices の要素インデックスに対応する + public void RemoveEdge(int i, int j) { + // インデックスの範囲外と等値の処理 + if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j) + throw new IndexOutOfRangeException(); + adjMat[i][j] = 0; + adjMat[j][i] = 0; + } + + /* 隣接行列を出力 */ + public void Print() { + Console.Write("頂点リスト = "); + PrintUtil.PrintList(vertices); + Console.WriteLine("隣接行列 ="); + PrintUtil.PrintMatrix(adjMat); + } + } ``` === "Go" ```go title="graph_adjacency_matrix.go" - [class]{graphAdjMat}-[func]{} + /* 隣接行列に基づく無向グラフクラス */ + type graphAdjMat struct { + // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す + vertices []int + // 隣接行列。行・列のインデックスは「頂点インデックス」に対応 + adjMat [][]int + } + + /* コンストラクタ */ + func newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat { + // 頂点を追加 + n := len(vertices) + adjMat := make([][]int, n) + for i := range adjMat { + adjMat[i] = make([]int, n) + } + // グラフを初期化する + g := &graphAdjMat{ + vertices: vertices, + adjMat: adjMat, + } + // 辺を追加 + // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する + for i := range edges { + g.addEdge(edges[i][0], edges[i][1]) + } + return g + } + + /* 頂点数を取得 */ + func (g *graphAdjMat) size() int { + return len(g.vertices) + } + + /* 頂点を追加 */ + func (g *graphAdjMat) addVertex(val int) { + n := g.size() + // 頂点リストに新しい頂点の値を追加 + g.vertices = append(g.vertices, val) + // 隣接行列に 1 行追加 + newRow := make([]int, n) + g.adjMat = append(g.adjMat, newRow) + // 隣接行列に 1 列追加 + for i := range g.adjMat { + g.adjMat[i] = append(g.adjMat[i], 0) + } + } + + /* 頂点を削除 */ + func (g *graphAdjMat) removeVertex(index int) { + if index >= g.size() { + return + } + // 頂点リストから index の頂点を削除する + g.vertices = append(g.vertices[:index], g.vertices[index+1:]...) + // 隣接行列で index 行を削除する + g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...) + // 隣接行列で index 列を削除する + for i := range g.adjMat { + g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...) + } + } + + /* 辺を追加 */ + // 引数 i, j は vertices の要素インデックスに対応する + func (g *graphAdjMat) addEdge(i, j int) { + // インデックスの範囲外と等値の処理 + if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j { + fmt.Errorf("%s", "Index Out Of Bounds Exception") + } + // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす + g.adjMat[i][j] = 1 + g.adjMat[j][i] = 1 + } + + /* 辺を削除 */ + // 引数 i, j は vertices の要素インデックスに対応する + func (g *graphAdjMat) removeEdge(i, j int) { + // インデックスの範囲外と等値の処理 + if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j { + fmt.Errorf("%s", "Index Out Of Bounds Exception") + } + g.adjMat[i][j] = 0 + g.adjMat[j][i] = 0 + } + + /* 隣接行列を出力 */ + func (g *graphAdjMat) print() { + fmt.Printf("\t頂点リスト = %v\n", g.vertices) + fmt.Printf("\t隣接行列 = \n") + for i := range g.adjMat { + fmt.Printf("\t\t\t%v\n", g.adjMat[i]) + } + } ``` === "Swift" ```swift title="graph_adjacency_matrix.swift" - [class]{GraphAdjMat}-[func]{} + /* 隣接行列に基づく無向グラフクラス */ + class GraphAdjMat { + private var vertices: [Int] // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す + private var adjMat: [[Int]] // 隣接行列。行・列のインデックスは「頂点インデックス」に対応 + + /* コンストラクタ */ + init(vertices: [Int], edges: [[Int]]) { + self.vertices = [] + adjMat = [] + // 頂点を追加 + for val in vertices { + addVertex(val: val) + } + // 辺を追加 + // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する + for e in edges { + addEdge(i: e[0], j: e[1]) + } + } + + /* 頂点数を取得 */ + func size() -> Int { + vertices.count + } + + /* 頂点を追加 */ + func addVertex(val: Int) { + let n = size() + // 頂点リストに新しい頂点の値を追加 + vertices.append(val) + // 隣接行列に 1 行追加 + let newRow = Array(repeating: 0, count: n) + adjMat.append(newRow) + // 隣接行列に 1 列追加 + for i in adjMat.indices { + adjMat[i].append(0) + } + } + + /* 頂点を削除 */ + func removeVertex(index: Int) { + if index >= size() { + fatalError("範囲外") + } + // 頂点リストから index の頂点を削除する + vertices.remove(at: index) + // 隣接行列で index 行を削除する + adjMat.remove(at: index) + // 隣接行列で index 列を削除する + for i in adjMat.indices { + adjMat[i].remove(at: index) + } + } + + /* 辺を追加 */ + // 引数 i, j は vertices の要素インデックスに対応する + func addEdge(i: Int, j: Int) { + // インデックスの範囲外と等値の処理 + if i < 0 || j < 0 || i >= size() || j >= size() || i == j { + fatalError("範囲外") + } + // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす + adjMat[i][j] = 1 + adjMat[j][i] = 1 + } + + /* 辺を削除 */ + // 引数 i, j は vertices の要素インデックスに対応する + func removeEdge(i: Int, j: Int) { + // インデックスの範囲外と等値の処理 + if i < 0 || j < 0 || i >= size() || j >= size() || i == j { + fatalError("範囲外") + } + adjMat[i][j] = 0 + adjMat[j][i] = 0 + } + + /* 隣接行列を出力 */ + func print() { + Swift.print("頂点リスト = ", terminator: "") + Swift.print(vertices) + Swift.print("隣接行列 =") + PrintUtil.printMatrix(matrix: adjMat) + } + } ``` === "JS" ```javascript title="graph_adjacency_matrix.js" - [class]{GraphAdjMat}-[func]{} + /* 隣接行列に基づく無向グラフクラス */ + class GraphAdjMat { + vertices; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す + adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応 + + /* コンストラクタ */ + constructor(vertices, edges) { + this.vertices = []; + this.adjMat = []; + // 頂点を追加 + for (const val of vertices) { + this.addVertex(val); + } + // 辺を追加 + // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する + for (const e of edges) { + this.addEdge(e[0], e[1]); + } + } + + /* 頂点数を取得 */ + size() { + return this.vertices.length; + } + + /* 頂点を追加 */ + addVertex(val) { + const n = this.size(); + // 頂点リストに新しい頂点の値を追加 + this.vertices.push(val); + // 隣接行列に 1 行追加 + const newRow = []; + for (let j = 0; j < n; j++) { + newRow.push(0); + } + this.adjMat.push(newRow); + // 隣接行列に 1 列追加 + for (const row of this.adjMat) { + row.push(0); + } + } + + /* 頂点を削除 */ + removeVertex(index) { + if (index >= this.size()) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // 頂点リストから index の頂点を削除する + this.vertices.splice(index, 1); + + // 隣接行列で index 行を削除する + this.adjMat.splice(index, 1); + // 隣接行列で index 列を削除する + for (const row of this.adjMat) { + row.splice(index, 1); + } + } + + /* 辺を追加 */ + // 引数 i, j は vertices の要素インデックスに対応する + addEdge(i, j) { + // インデックスの範囲外と等値の処理 + if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // 無向グラフでは、隣接行列は主対角線に関して対称であり、(i, j) === (j, i) を満たす + this.adjMat[i][j] = 1; + this.adjMat[j][i] = 1; + } + + /* 辺を削除 */ + // 引数 i, j は vertices の要素インデックスに対応する + removeEdge(i, j) { + // インデックスの範囲外と等値の処理 + if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) { + throw new RangeError('Index Out Of Bounds Exception'); + } + this.adjMat[i][j] = 0; + this.adjMat[j][i] = 0; + } + + /* 隣接行列を出力 */ + print() { + console.log('頂点リスト = ', this.vertices); + console.log('隣接行列 =', this.adjMat); + } + } ``` === "TS" ```typescript title="graph_adjacency_matrix.ts" - [class]{GraphAdjMat}-[func]{} + /* 隣接行列に基づく無向グラフクラス */ + class GraphAdjMat { + vertices: number[]; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す + adjMat: number[][]; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応 + + /* コンストラクタ */ + constructor(vertices: number[], edges: number[][]) { + this.vertices = []; + this.adjMat = []; + // 頂点を追加 + for (const val of vertices) { + this.addVertex(val); + } + // 辺を追加 + // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する + for (const e of edges) { + this.addEdge(e[0], e[1]); + } + } + + /* 頂点数を取得 */ + size(): number { + return this.vertices.length; + } + + /* 頂点を追加 */ + addVertex(val: number): void { + const n: number = this.size(); + // 頂点リストに新しい頂点の値を追加 + this.vertices.push(val); + // 隣接行列に 1 行追加 + const newRow: number[] = []; + for (let j: number = 0; j < n; j++) { + newRow.push(0); + } + this.adjMat.push(newRow); + // 隣接行列に 1 列追加 + for (const row of this.adjMat) { + row.push(0); + } + } + + /* 頂点を削除 */ + removeVertex(index: number): void { + if (index >= this.size()) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // 頂点リストから index の頂点を削除する + this.vertices.splice(index, 1); + + // 隣接行列で index 行を削除する + this.adjMat.splice(index, 1); + // 隣接行列で index 列を削除する + for (const row of this.adjMat) { + row.splice(index, 1); + } + } + + /* 辺を追加 */ + // 引数 i, j は vertices の要素インデックスに対応する + addEdge(i: number, j: number): void { + // インデックスの範囲外と等値の処理 + if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // 無向グラフでは、隣接行列は主対角線に関して対称であり、(i, j) === (j, i) を満たす + this.adjMat[i][j] = 1; + this.adjMat[j][i] = 1; + } + + /* 辺を削除 */ + // 引数 i, j は vertices の要素インデックスに対応する + removeEdge(i: number, j: number): void { + // インデックスの範囲外と等値の処理 + if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) { + throw new RangeError('Index Out Of Bounds Exception'); + } + this.adjMat[i][j] = 0; + this.adjMat[j][i] = 0; + } + + /* 隣接行列を出力 */ + print(): void { + console.log('頂点リスト = ', this.vertices); + console.log('隣接行列 =', this.adjMat); + } + } ``` === "Dart" ```dart title="graph_adjacency_matrix.dart" - [class]{GraphAdjMat}-[func]{} + /* 隣接行列に基づく無向グラフクラス */ + class GraphAdjMat { + List vertices = []; // 頂点要素。要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す + List> adjMat = []; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応 + + /* コンストラクタ */ + GraphAdjMat(List vertices, List> edges) { + this.vertices = []; + this.adjMat = []; + // 頂点を追加 + for (int val in vertices) { + addVertex(val); + } + // 辺を追加 + // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する + for (List e in edges) { + addEdge(e[0], e[1]); + } + } + + /* 頂点数を取得 */ + int size() { + return vertices.length; + } + + /* 頂点を追加 */ + void addVertex(int val) { + int n = size(); + // 頂点リストに新しい頂点の値を追加 + vertices.add(val); + // 隣接行列に 1 行追加 + List newRow = List.filled(n, 0, growable: true); + adjMat.add(newRow); + // 隣接行列に 1 列追加 + for (List row in adjMat) { + row.add(0); + } + } + + /* 頂点を削除 */ + void removeVertex(int index) { + if (index >= size()) { + throw IndexError; + } + // 頂点リストから index の頂点を削除する + vertices.removeAt(index); + // 隣接行列で index 行を削除する + adjMat.removeAt(index); + // 隣接行列で index 列を削除する + for (List row in adjMat) { + row.removeAt(index); + } + } + + /* 辺を追加 */ + // 引数 i, j は vertices の要素インデックスに対応する + void addEdge(int i, int j) { + // インデックスの範囲外と等値の処理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { + throw IndexError; + } + // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす + adjMat[i][j] = 1; + adjMat[j][i] = 1; + } + + /* 辺を削除 */ + // 引数 i, j は vertices の要素インデックスに対応する + void removeEdge(int i, int j) { + // インデックスの範囲外と等値の処理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { + throw IndexError; + } + adjMat[i][j] = 0; + adjMat[j][i] = 0; + } + + /* 隣接行列を出力 */ + void printAdjMat() { + print("頂点リスト = $vertices"); + print("隣接行列 = "); + printMatrix(adjMat); + } + } ``` === "Rust" ```rust title="graph_adjacency_matrix.rs" - [class]{GraphAdjMat}-[func]{} + /* 隣接行列に基づく無向グラフ型 */ + pub struct GraphAdjMat { + // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す + pub vertices: Vec, + // 隣接行列。行・列のインデックスは「頂点インデックス」に対応 + pub adj_mat: Vec>, + } + + impl GraphAdjMat { + /* コンストラクタ */ + pub fn new(vertices: Vec, edges: Vec<[usize; 2]>) -> Self { + let mut graph = GraphAdjMat { + vertices: vec![], + adj_mat: vec![], + }; + // 頂点を追加 + for val in vertices { + graph.add_vertex(val); + } + // 辺を追加 + // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する + for edge in edges { + graph.add_edge(edge[0], edge[1]) + } + + graph + } + + /* 頂点数を取得 */ + pub fn size(&self) -> usize { + self.vertices.len() + } + + /* 頂点を追加 */ + pub fn add_vertex(&mut self, val: i32) { + let n = self.size(); + // 頂点リストに新しい頂点の値を追加 + self.vertices.push(val); + // 隣接行列に 1 行追加 + self.adj_mat.push(vec![0; n]); + // 隣接行列に 1 列追加 + for row in self.adj_mat.iter_mut() { + row.push(0); + } + } + + /* 頂点を削除 */ + pub fn remove_vertex(&mut self, index: usize) { + if index >= self.size() { + panic!("index error") + } + // 頂点リストから index の頂点を削除する + self.vertices.remove(index); + // 隣接行列で index 行を削除する + self.adj_mat.remove(index); + // 隣接行列で index 列を削除する + for row in self.adj_mat.iter_mut() { + row.remove(index); + } + } + + /* 辺を追加 */ + pub fn add_edge(&mut self, i: usize, j: usize) { + // パラメータ i, j は vertices の要素インデックスに対応する + // 範囲外と同値の場合の処理 + if i >= self.size() || j >= self.size() || i == j { + panic!("index error") + } + // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす + self.adj_mat[i][j] = 1; + self.adj_mat[j][i] = 1; + } + + /* 辺を削除 */ + // 引数 i, j は vertices の要素インデックスに対応する + pub fn remove_edge(&mut self, i: usize, j: usize) { + // パラメータ i, j は vertices の要素インデックスに対応する + // 範囲外と同値の場合の処理 + if i >= self.size() || j >= self.size() || i == j { + panic!("index error") + } + self.adj_mat[i][j] = 0; + self.adj_mat[j][i] = 0; + } + + /* 隣接行列を出力 */ + pub fn print(&self) { + println!("頂点リスト = {:?}", self.vertices); + println!("隣接行列 ="); + println!("["); + for row in &self.adj_mat { + println!(" {:?},", row); + } + println!("]") + } + } ``` === "C" ```c title="graph_adjacency_matrix.c" - [class]{GraphAdjMat}-[func]{} + /* 隣接行列に基づく無向グラフ構造体 */ + typedef struct { + int vertices[MAX_SIZE]; + int adjMat[MAX_SIZE][MAX_SIZE]; + int size; + } GraphAdjMat; + + /* コンストラクタ */ + GraphAdjMat *newGraphAdjMat() { + GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat)); + graph->size = 0; + for (int i = 0; i < MAX_SIZE; i++) { + for (int j = 0; j < MAX_SIZE; j++) { + graph->adjMat[i][j] = 0; + } + } + return graph; + } + + /* デストラクタ */ + void delGraphAdjMat(GraphAdjMat *graph) { + free(graph); + } + + /* 頂点を追加 */ + void addVertex(GraphAdjMat *graph, int val) { + if (graph->size == MAX_SIZE) { + fprintf(stderr, "グラフの頂点数が最大値に達しました\n"); + return; + } + // n 番目の頂点を追加し、n 行目と n 列目を 0 にする + int n = graph->size; + graph->vertices[n] = val; + for (int i = 0; i <= n; i++) { + graph->adjMat[n][i] = graph->adjMat[i][n] = 0; + } + graph->size++; + } + + /* 頂点を削除 */ + void removeVertex(GraphAdjMat *graph, int index) { + if (index < 0 || index >= graph->size) { + fprintf(stderr, "頂点インデックスが範囲外です\n"); + return; + } + // 頂点リストから index の頂点を削除する + for (int i = index; i < graph->size - 1; i++) { + graph->vertices[i] = graph->vertices[i + 1]; + } + // 隣接行列で index 行を削除する + for (int i = index; i < graph->size - 1; i++) { + for (int j = 0; j < graph->size; j++) { + graph->adjMat[i][j] = graph->adjMat[i + 1][j]; + } + } + // 隣接行列で index 列を削除する + for (int i = 0; i < graph->size; i++) { + for (int j = index; j < graph->size - 1; j++) { + graph->adjMat[i][j] = graph->adjMat[i][j + 1]; + } + } + graph->size--; + } + + /* 辺を追加 */ + // 引数 i, j は vertices の要素インデックスに対応する + void addEdge(GraphAdjMat *graph, int i, int j) { + if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) { + fprintf(stderr, "辺インデックスが範囲外であるか、同一です\n"); + return; + } + graph->adjMat[i][j] = 1; + graph->adjMat[j][i] = 1; + } + + /* 辺を削除 */ + // 引数 i, j は vertices の要素インデックスに対応する + void removeEdge(GraphAdjMat *graph, int i, int j) { + if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) { + fprintf(stderr, "辺インデックスが範囲外であるか、同一です\n"); + return; + } + graph->adjMat[i][j] = 0; + graph->adjMat[j][i] = 0; + } + + /* 隣接行列を出力 */ + void printGraphAdjMat(GraphAdjMat *graph) { + printf("頂点リスト = "); + printArray(graph->vertices, graph->size); + printf("隣接行列 =\n"); + for (int i = 0; i < graph->size; i++) { + printArray(graph->adjMat[i], graph->size); + } + } ``` === "Kotlin" ```kotlin title="graph_adjacency_matrix.kt" - [class]{GraphAdjMat}-[func]{} + /* 隣接行列に基づく無向グラフクラス */ + class GraphAdjMat(vertices: IntArray, edges: Array) { + val vertices = mutableListOf() // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す + val adjMat = mutableListOf>() // 隣接行列。行・列のインデックスは「頂点インデックス」に対応 + + /* コンストラクタ */ + init { + // 頂点を追加 + for (vertex in vertices) { + addVertex(vertex) + } + // 辺を追加 + // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する + for (edge in edges) { + addEdge(edge[0], edge[1]) + } + } + + /* 頂点数を取得 */ + fun size(): Int { + return vertices.size + } + + /* 頂点を追加 */ + fun addVertex(_val: Int) { + val n = size() + // 頂点リストに新しい頂点の値を追加 + vertices.add(_val) + // 隣接行列に 1 行追加 + val newRow = mutableListOf() + for (j in 0..= size()) + throw IndexOutOfBoundsException() + // 頂点リストから index の頂点を削除する + vertices.removeAt(index) + // 隣接行列で index 行を削除する + adjMat.removeAt(index) + // 隣接行列で index 列を削除する + for (row in adjMat) { + row.removeAt(index) + } + } + + /* 辺を追加 */ + // 引数 i, j は vertices の要素インデックスに対応する + fun addEdge(i: Int, j: Int) { + // インデックスの範囲外と等値の処理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) + throw IndexOutOfBoundsException() + // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす + adjMat[i][j] = 1 + adjMat[j][i] = 1 + } + + /* 辺を削除 */ + // 引数 i, j は vertices の要素インデックスに対応する + fun removeEdge(i: Int, j: Int) { + // インデックスの範囲外と等値の処理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) + throw IndexOutOfBoundsException() + adjMat[i][j] = 0 + adjMat[j][i] = 0 + } + + /* 隣接行列を出力 */ + fun print() { + print("頂点リスト = ") + println(vertices) + println("隣接行列 =") + printMatrix(adjMat) + } + } ``` === "Ruby" ```ruby title="graph_adjacency_matrix.rb" - [class]{GraphAdjMat}-[func]{} + ### 隣接行列で実装した無向グラフクラス ### + class GraphAdjMat + def initialize(vertices, edges) + ### コンストラクタ ### + # 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す + @vertices = [] + # 隣接行列。行・列のインデックスは「頂点インデックス」に対応 + @adj_mat = [] + # 頂点を追加 + vertices.each { |val| add_vertex(val) } + # 辺を追加 + # 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する + edges.each { |e| add_edge(e[0], e[1]) } + end + + ### 頂点数を取得 ### + def size + @vertices.length + end + + ### 頂点を追加 ### + def add_vertex(val) + n = size + # 頂点リストに新しい頂点の値を追加 + @vertices << val + # 隣接行列に 1 行追加 + new_row = Array.new(n, 0) + @adj_mat << new_row + # 隣接行列に 1 列追加 + @adj_mat.each { |row| row << 0 } + end + + ### 頂点を削除 ### + def remove_vertex(index) + raise IndexError if index >= size + + # 頂点リストから index の頂点を削除する + @vertices.delete_at(index) + # 隣接行列で index 行を削除する + @adj_mat.delete_at(index) + # 隣接行列で index 列を削除する + @adj_mat.each { |row| row.delete_at(index) } + end + + ### 辺を追加 ### + def add_edge(i, j) + # パラメータ i, j は vertices の要素インデックスに対応する + # 範囲外と同値の場合の処理 + if i < 0 || j < 0 || i >= size || j >= size || i == j + raise IndexError + end + # 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす + @adj_mat[i][j] = 1 + @adj_mat[j][i] = 1 + end + + ### 辺を削除 ### + def remove_edge(i, j) + # パラメータ i, j は vertices の要素インデックスに対応する + # 範囲外と同値の場合の処理 + if i < 0 || j < 0 || i >= size || j >= size || i == j + raise IndexError + end + @adj_mat[i][j] = 0 + @adj_mat[j][i] = 0 + end + + ### 隣接行列を出力 ### + def __print__ + puts "頂点リスト = #{@vertices}" + puts '隣接行列 =' + print_matrix(@adj_mat) + end + end ``` +??? pythontutor "コードの可視化" + +
+ + ## 9.2.2   隣接リストに基づく実装 -総計$n$個の頂点と$m$個の辺を持つ無向グラフが与えられた場合、さまざまな操作は下図のように実装できます。 +無向グラフの頂点総数を $n$、辺総数を $m$ とすると、各種操作は次図の方法で実装できます。 -- **辺の追加**:対応する頂点の連結リストの末尾に辺を追加するだけで、$O(1)$時間を使用します。無向グラフであるため、両方向に同時に辺を追加する必要があります。 -- **辺の削除**:対応する頂点の連結リスト内で指定された辺を見つけて削除し、$O(m)$時間を使用します。無向グラフでは、両方向の辺を同時に削除する必要があります。 -- **頂点の追加**:隣接リストに連結リストを追加し、新しい頂点をリストのヘッドノードにし、$O(1)$時間を使用します。 -- **頂点の削除**:隣接リスト全体を走査し、指定された頂点を含むすべての辺を削除する必要があり、$O(n + m)$時間を使用します。 -- **初期化**:隣接リストに$n$個の頂点と$2m$個の辺を作成し、$O(n + m)$時間を使用します。 +- **辺の追加**:頂点に対応する連結リストの末尾に辺を追加すればよく、$O(1)$ 時間です。無向グラフなので、2 方向の辺を同時に追加する必要があります。 +- **辺の削除**:頂点に対応する連結リストから指定した辺を探して削除するため、$O(m)$ 時間です。無向グラフでは、2 方向の辺を同時に削除する必要があります。 +- **頂点の追加**:隣接リストに 1 つの連結リストを追加し、新しい頂点をその連結リストの先頭ノードとするため、$O(1)$ 時間です。 +- **頂点の削除**:隣接リスト全体を走査し、指定した頂点を含むすべての辺を削除する必要があるため、$O(n + m)$ 時間です。 +- **初期化**:隣接リストに $n$ 個の頂点と $2m$ 本の辺を作成するため、$O(n + m)$ 時間です。 -=== "隣接リストの初期化" - ![隣接リストでの初期化、辺の追加と削除、頂点の追加と削除](graph_operations.assets/adjacency_list_step1_initialization.png){ class="animation-figure" } +=== "<1>" + ![隣接リストの初期化、辺の追加と削除、頂点の追加と削除](graph_operations.assets/adjacency_list_step1_initialization.png){ class="animation-figure" } -=== "辺の追加" +=== "<2>" ![adjacency_list_add_edge](graph_operations.assets/adjacency_list_step2_add_edge.png){ class="animation-figure" } -=== "辺の削除" +=== "<3>" ![adjacency_list_remove_edge](graph_operations.assets/adjacency_list_step3_remove_edge.png){ class="animation-figure" } -=== "頂点の追加" +=== "<4>" ![adjacency_list_add_vertex](graph_operations.assets/adjacency_list_step4_add_vertex.png){ class="animation-figure" } -=== "頂点の削除" +=== "<5>" ![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_step5_remove_vertex.png){ class="animation-figure" } -

図 9-8   隣接リストでの初期化、辺の追加と削除、頂点の追加と削除

+

図 9-8   隣接リストの初期化、辺の追加と削除、頂点の追加と削除

-以下は隣接リストのコード実装です。上図と比較して、実際のコードには以下の違いがあります。 +以下は隣接リストのコード実装です。上図と比べると、実際のコードには次の違いがあります。 -- 頂点の追加と削除の便宜、およびコードの簡素化のため、連結リストの代わりにリスト(動的配列)を使用します。 -- ハッシュテーブルを使用して隣接リストを格納し、`key`が頂点インスタンス、`value`がその頂点の隣接頂点のリスト(連結リスト)です。 +- 頂点の追加と削除を容易にし、コードを簡潔にするため、連結リストの代わりにリスト(動的配列)を使用しています。 +- ハッシュテーブルを用いて隣接リストを格納しており、`key` は頂点インスタンス、`value` はその頂点に隣接する頂点のリスト(連結リスト)です。 -さらに、隣接リストで頂点を表現するために`Vertex`クラスを使用します。その理由は:隣接行列のようにリストインデックスを使用して異なる頂点を区別する場合、インデックス$i$の頂点を削除したい場合、隣接リスト全体を走査し、$i$より大きいすべてのインデックスを1つずつ減少させる必要があり、これは非常に非効率的です。しかし、各頂点が一意の`Vertex`インスタンスである場合、頂点を削除しても他の頂点に変更を加える必要がありません。 +また、隣接リストでは頂点を表すために `Vertex` クラスを使用しています。その理由は、もし隣接行列と同様にリストのインデックスで異なる頂点を区別すると、インデックス $i$ の頂点を削除する場合、隣接リスト全体を走査して、$i$ より大きいすべてのインデックスを $1$ 減らす必要があり、効率が非常に低いためです。これに対して、各頂点が一意な `Vertex` インスタンスであれば、ある頂点を削除しても他の頂点を変更する必要はありません。 === "Python" @@ -388,7 +1253,7 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま def __init__(self, edges: list[list[Vertex]]): """コンストラクタ""" - # 隣接リスト、キー: 頂点、値: その頂点の隣接する全頂点 + # 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 self.adj_list = dict[Vertex, list[Vertex]]() # すべての頂点と辺を追加 for edge in edges: @@ -420,16 +1285,16 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま """頂点を追加""" if vet in self.adj_list: return - # 隣接リストに新しい連結リストを追加 + # 隣接リストに新しいリストを追加 self.adj_list[vet] = [] def remove_vertex(self, vet: Vertex): """頂点を削除""" if vet not in self.adj_list: raise ValueError() - # 隣接リストから頂点vetに対応する連結リストを削除 + # 隣接リストから頂点 vet に対応するリストを削除 self.adj_list.pop(vet) - # 他の頂点の連結リストを走査し、vetを含むすべての辺を削除 + # 他の頂点のリストを走査し、vet を含むすべての辺を削除 for vertex in self.adj_list: if vet in self.adj_list[vertex]: self.adj_list[vertex].remove(vet) @@ -448,10 +1313,10 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま /* 隣接リストに基づく無向グラフクラス */ class GraphAdjList { public: - // 隣接リスト、キー:頂点、値:その頂点のすべての隣接頂点 + // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 unordered_map> adjList; - /* ベクターから指定されたノードを削除 */ + /* vector 内の指定ノードを削除 */ void remove(vector &vec, Vertex *vet) { for (int i = 0; i < vec.size(); i++) { if (vec[i] == vet) { @@ -479,7 +1344,7 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま /* 辺を追加 */ void addEdge(Vertex *vet1, Vertex *vet2) { if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2) - throw invalid_argument("Vertex does not exist"); + throw invalid_argument("頂点が存在しません"); // 辺 vet1 - vet2 を追加 adjList[vet1].push_back(vet2); adjList[vet2].push_back(vet1); @@ -488,7 +1353,7 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま /* 辺を削除 */ void removeEdge(Vertex *vet1, Vertex *vet2) { if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2) - throw invalid_argument("Vertex does not exist"); + throw invalid_argument("頂点が存在しません"); // 辺 vet1 - vet2 を削除 remove(adjList[vet1], vet2); remove(adjList[vet2], vet1); @@ -498,23 +1363,23 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま void addVertex(Vertex *vet) { if (adjList.count(vet)) return; - // 隣接リストに新しい連結リストを追加 + // 隣接リストに新しいリストを追加 adjList[vet] = vector(); } /* 頂点を削除 */ void removeVertex(Vertex *vet) { if (!adjList.count(vet)) - throw invalid_argument("Vertex does not exist"); - // 隣接リストから頂点vetに対応する連結リストを削除 + throw invalid_argument("頂点が存在しません"); + // 隣接リストから頂点 vet に対応するリストを削除 adjList.erase(vet); - // 他の頂点の連結リストを走査し、vetを含むすべての辺を削除 + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 for (auto &adj : adjList) { remove(adj.second, vet); } } - /* 隣接リストを印刷 */ + /* 隣接リストを出力 */ void print() { cout << "隣接リスト =" << endl; for (auto &adj : adjList) { @@ -532,7 +1397,7 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま ```java title="graph_adjacency_list.java" /* 隣接リストに基づく無向グラフクラス */ class GraphAdjList { - // 隣接リスト、キー: 頂点、値: その頂点のすべての隣接頂点 + // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 Map> adjList; /* コンストラクタ */ @@ -573,7 +1438,7 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま public void addVertex(Vertex vet) { if (adjList.containsKey(vet)) return; - // 隣接リストに新しい連結リストを追加 + // 隣接リストに新しいリストを追加 adjList.put(vet, new ArrayList<>()); } @@ -581,9 +1446,9 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま public void removeVertex(Vertex vet) { if (!adjList.containsKey(vet)) throw new IllegalArgumentException(); - // 隣接リストから頂点 vet に対応する連結リストを削除 + // 隣接リストから頂点 vet に対応するリストを削除 adjList.remove(vet); - // 他の頂点の連結リストを走査し、vet を含むすべての辺を削除 + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 for (List list : adjList.values()) { list.remove(vet); } @@ -605,82 +1470,922 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま === "C#" ```csharp title="graph_adjacency_list.cs" - [class]{GraphAdjList}-[func]{} + /* 隣接リストに基づく無向グラフクラス */ + class GraphAdjList { + // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 + public Dictionary> adjList; + + /* コンストラクタ */ + public GraphAdjList(Vertex[][] edges) { + adjList = []; + // すべての頂点と辺を追加 + foreach (Vertex[] edge in edges) { + AddVertex(edge[0]); + AddVertex(edge[1]); + AddEdge(edge[0], edge[1]); + } + } + + /* 頂点数を取得 */ + int Size() { + return adjList.Count; + } + + /* 辺を追加 */ + public void AddEdge(Vertex vet1, Vertex vet2) { + if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2) + throw new InvalidOperationException(); + // 辺 vet1 - vet2 を追加 + adjList[vet1].Add(vet2); + adjList[vet2].Add(vet1); + } + + /* 辺を削除 */ + public void RemoveEdge(Vertex vet1, Vertex vet2) { + if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2) + throw new InvalidOperationException(); + // 辺 vet1 - vet2 を削除 + adjList[vet1].Remove(vet2); + adjList[vet2].Remove(vet1); + } + + /* 頂点を追加 */ + public void AddVertex(Vertex vet) { + if (adjList.ContainsKey(vet)) + return; + // 隣接リストに新しいリストを追加 + adjList.Add(vet, []); + } + + /* 頂点を削除 */ + public void RemoveVertex(Vertex vet) { + if (!adjList.ContainsKey(vet)) + throw new InvalidOperationException(); + // 隣接リストから頂点 vet に対応するリストを削除 + adjList.Remove(vet); + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 + foreach (List list in adjList.Values) { + list.Remove(vet); + } + } + + /* 隣接リストを出力 */ + public void Print() { + Console.WriteLine("隣接リスト ="); + foreach (KeyValuePair> pair in adjList) { + List tmp = []; + foreach (Vertex vertex in pair.Value) + tmp.Add(vertex.val); + Console.WriteLine(pair.Key.val + ": [" + string.Join(", ", tmp) + "],"); + } + } + } ``` === "Go" ```go title="graph_adjacency_list.go" - [class]{graphAdjList}-[func]{} + /* 隣接リストに基づく無向グラフクラス */ + type graphAdjList struct { + // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 + adjList map[Vertex][]Vertex + } + + /* コンストラクタ */ + func newGraphAdjList(edges [][]Vertex) *graphAdjList { + g := &graphAdjList{ + adjList: make(map[Vertex][]Vertex), + } + // すべての頂点と辺を追加 + for _, edge := range edges { + g.addVertex(edge[0]) + g.addVertex(edge[1]) + g.addEdge(edge[0], edge[1]) + } + return g + } + + /* 頂点数を取得 */ + func (g *graphAdjList) size() int { + return len(g.adjList) + } + + /* 辺を追加 */ + func (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) { + _, ok1 := g.adjList[vet1] + _, ok2 := g.adjList[vet2] + if !ok1 || !ok2 || vet1 == vet2 { + panic("error") + } + // 辺 `vet1 - vet2` を追加し、無名 `struct{}` を追加する + g.adjList[vet1] = append(g.adjList[vet1], vet2) + g.adjList[vet2] = append(g.adjList[vet2], vet1) + } + + /* 辺を削除 */ + func (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) { + _, ok1 := g.adjList[vet1] + _, ok2 := g.adjList[vet2] + if !ok1 || !ok2 || vet1 == vet2 { + panic("error") + } + // 辺 vet1 - vet2 を削除 + g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2) + g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1) + } + + /* 頂点を追加 */ + func (g *graphAdjList) addVertex(vet Vertex) { + _, ok := g.adjList[vet] + if ok { + return + } + // 隣接リストに新しいリストを追加 + g.adjList[vet] = make([]Vertex, 0) + } + + /* 頂点を削除 */ + func (g *graphAdjList) removeVertex(vet Vertex) { + _, ok := g.adjList[vet] + if !ok { + panic("error") + } + // 隣接リストから頂点 vet に対応するリストを削除 + delete(g.adjList, vet) + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 + for v, list := range g.adjList { + g.adjList[v] = DeleteSliceElms(list, vet) + } + } + + /* 隣接リストを出力 */ + func (g *graphAdjList) print() { + var builder strings.Builder + fmt.Printf("隣接リスト = \n") + for k, v := range g.adjList { + builder.WriteString("\t\t" + strconv.Itoa(k.Val) + ": ") + for _, vet := range v { + builder.WriteString(strconv.Itoa(vet.Val) + " ") + } + fmt.Println(builder.String()) + builder.Reset() + } + } ``` === "Swift" ```swift title="graph_adjacency_list.swift" - [class]{GraphAdjList}-[func]{} + /* 隣接リストに基づく無向グラフクラス */ + class GraphAdjList { + // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 + public private(set) var adjList: [Vertex: [Vertex]] + + /* コンストラクタ */ + public init(edges: [[Vertex]]) { + adjList = [:] + // すべての頂点と辺を追加 + for edge in edges { + addVertex(vet: edge[0]) + addVertex(vet: edge[1]) + addEdge(vet1: edge[0], vet2: edge[1]) + } + } + + /* 頂点数を取得 */ + public func size() -> Int { + adjList.count + } + + /* 辺を追加 */ + public func addEdge(vet1: Vertex, vet2: Vertex) { + if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 { + fatalError("引数エラー") + } + // 辺 vet1 - vet2 を追加 + adjList[vet1]?.append(vet2) + adjList[vet2]?.append(vet1) + } + + /* 辺を削除 */ + public func removeEdge(vet1: Vertex, vet2: Vertex) { + if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 { + fatalError("引数エラー") + } + // 辺 vet1 - vet2 を削除 + adjList[vet1]?.removeAll { $0 == vet2 } + adjList[vet2]?.removeAll { $0 == vet1 } + } + + /* 頂点を追加 */ + public func addVertex(vet: Vertex) { + if adjList[vet] != nil { + return + } + // 隣接リストに新しいリストを追加 + adjList[vet] = [] + } + + /* 頂点を削除 */ + public func removeVertex(vet: Vertex) { + if adjList[vet] == nil { + fatalError("引数エラー") + } + // 隣接リストから頂点 vet に対応するリストを削除 + adjList.removeValue(forKey: vet) + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 + for key in adjList.keys { + adjList[key]?.removeAll { $0 == vet } + } + } + + /* 隣接リストを出力 */ + public func print() { + Swift.print("隣接リスト =") + for (vertex, list) in adjList { + let list = list.map { $0.val } + Swift.print("\(vertex.val): \(list),") + } + } + } ``` === "JS" ```javascript title="graph_adjacency_list.js" - [class]{GraphAdjList}-[func]{} + /* 隣接リストに基づく無向グラフクラス */ + class GraphAdjList { + // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 + adjList; + + /* コンストラクタ */ + constructor(edges) { + this.adjList = new Map(); + // すべての頂点と辺を追加 + for (const edge of edges) { + this.addVertex(edge[0]); + this.addVertex(edge[1]); + this.addEdge(edge[0], edge[1]); + } + } + + /* 頂点数を取得 */ + size() { + return this.adjList.size; + } + + /* 辺を追加 */ + addEdge(vet1, vet2) { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 + ) { + throw new Error('Illegal Argument Exception'); + } + // 辺 vet1 - vet2 を追加 + this.adjList.get(vet1).push(vet2); + this.adjList.get(vet2).push(vet1); + } + + /* 辺を削除 */ + removeEdge(vet1, vet2) { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 || + this.adjList.get(vet1).indexOf(vet2) === -1 + ) { + throw new Error('Illegal Argument Exception'); + } + // 辺 vet1 - vet2 を削除 + this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1); + this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1); + } + + /* 頂点を追加 */ + addVertex(vet) { + if (this.adjList.has(vet)) return; + // 隣接リストに新しいリストを追加 + this.adjList.set(vet, []); + } + + /* 頂点を削除 */ + removeVertex(vet) { + if (!this.adjList.has(vet)) { + throw new Error('Illegal Argument Exception'); + } + // 隣接リストから頂点 vet に対応するリストを削除 + this.adjList.delete(vet); + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 + for (const set of this.adjList.values()) { + const index = set.indexOf(vet); + if (index > -1) { + set.splice(index, 1); + } + } + } + + /* 隣接リストを出力 */ + print() { + console.log('隣接リスト ='); + for (const [key, value] of this.adjList) { + const tmp = []; + for (const vertex of value) { + tmp.push(vertex.val); + } + console.log(key.val + ': ' + tmp.join()); + } + } + } ``` === "TS" ```typescript title="graph_adjacency_list.ts" - [class]{GraphAdjList}-[func]{} + /* 隣接リストに基づく無向グラフクラス */ + class GraphAdjList { + // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 + adjList: Map; + + /* コンストラクタ */ + constructor(edges: Vertex[][]) { + this.adjList = new Map(); + // すべての頂点と辺を追加 + for (const edge of edges) { + this.addVertex(edge[0]); + this.addVertex(edge[1]); + this.addEdge(edge[0], edge[1]); + } + } + + /* 頂点数を取得 */ + size(): number { + return this.adjList.size; + } + + /* 辺を追加 */ + addEdge(vet1: Vertex, vet2: Vertex): void { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 + ) { + throw new Error('Illegal Argument Exception'); + } + // 辺 vet1 - vet2 を追加 + this.adjList.get(vet1).push(vet2); + this.adjList.get(vet2).push(vet1); + } + + /* 辺を削除 */ + removeEdge(vet1: Vertex, vet2: Vertex): void { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 || + this.adjList.get(vet1).indexOf(vet2) === -1 + ) { + throw new Error('Illegal Argument Exception'); + } + // 辺 vet1 - vet2 を削除 + this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1); + this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1); + } + + /* 頂点を追加 */ + addVertex(vet: Vertex): void { + if (this.adjList.has(vet)) return; + // 隣接リストに新しいリストを追加 + this.adjList.set(vet, []); + } + + /* 頂点を削除 */ + removeVertex(vet: Vertex): void { + if (!this.adjList.has(vet)) { + throw new Error('Illegal Argument Exception'); + } + // 隣接リストから頂点 vet に対応するリストを削除 + this.adjList.delete(vet); + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 + for (const set of this.adjList.values()) { + const index: number = set.indexOf(vet); + if (index > -1) { + set.splice(index, 1); + } + } + } + + /* 隣接リストを出力 */ + print(): void { + console.log('隣接リスト ='); + for (const [key, value] of this.adjList.entries()) { + const tmp = []; + for (const vertex of value) { + tmp.push(vertex.val); + } + console.log(key.val + ': ' + tmp.join()); + } + } + } ``` === "Dart" ```dart title="graph_adjacency_list.dart" - [class]{GraphAdjList}-[func]{} + /* 隣接リストに基づく無向グラフクラス */ + class GraphAdjList { + // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 + Map> adjList = {}; + + /* コンストラクタ */ + GraphAdjList(List> edges) { + for (List edge in edges) { + addVertex(edge[0]); + addVertex(edge[1]); + addEdge(edge[0], edge[1]); + } + } + + /* 頂点数を取得 */ + int size() { + return adjList.length; + } + + /* 辺を追加 */ + void addEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || + !adjList.containsKey(vet2) || + vet1 == vet2) { + throw ArgumentError; + } + // 辺 vet1 - vet2 を追加 + adjList[vet1]!.add(vet2); + adjList[vet2]!.add(vet1); + } + + /* 辺を削除 */ + void removeEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || + !adjList.containsKey(vet2) || + vet1 == vet2) { + throw ArgumentError; + } + // 辺 vet1 - vet2 を削除 + adjList[vet1]!.remove(vet2); + adjList[vet2]!.remove(vet1); + } + + /* 頂点を追加 */ + void addVertex(Vertex vet) { + if (adjList.containsKey(vet)) return; + // 隣接リストに新しいリストを追加 + adjList[vet] = []; + } + + /* 頂点を削除 */ + void removeVertex(Vertex vet) { + if (!adjList.containsKey(vet)) { + throw ArgumentError; + } + // 隣接リストから頂点 vet に対応するリストを削除 + adjList.remove(vet); + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 + adjList.forEach((key, value) { + value.remove(vet); + }); + } + + /* 隣接リストを出力 */ + void printAdjList() { + print("隣接リスト ="); + adjList.forEach((key, value) { + List tmp = []; + for (Vertex vertex in value) { + tmp.add(vertex.val); + } + print("${key.val}: $tmp,"); + }); + } + } ``` === "Rust" ```rust title="graph_adjacency_list.rs" - [class]{GraphAdjList}-[func]{} + /* 隣接リストに基づく無向グラフ型 */ + pub struct GraphAdjList { + // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 + pub adj_list: HashMap>, // maybe HashSet for value part is better? + } + + impl GraphAdjList { + /* コンストラクタ */ + pub fn new(edges: Vec<[Vertex; 2]>) -> Self { + let mut graph = GraphAdjList { + adj_list: HashMap::new(), + }; + // すべての頂点と辺を追加 + for edge in edges { + graph.add_vertex(edge[0]); + graph.add_vertex(edge[1]); + graph.add_edge(edge[0], edge[1]); + } + + graph + } + + /* 頂点数を取得 */ + #[allow(unused)] + pub fn size(&self) -> usize { + self.adj_list.len() + } + + /* 辺を追加 */ + pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) { + if vet1 == vet2 { + panic!("value error"); + } + // 辺 vet1 - vet2 を追加 + self.adj_list.entry(vet1).or_default().push(vet2); + self.adj_list.entry(vet2).or_default().push(vet1); + } + + /* 辺を削除 */ + #[allow(unused)] + pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) { + if vet1 == vet2 { + panic!("value error"); + } + // 辺 vet1 - vet2 を削除 + self.adj_list + .entry(vet1) + .and_modify(|v| v.retain(|&e| e != vet2)); + self.adj_list + .entry(vet2) + .and_modify(|v| v.retain(|&e| e != vet1)); + } + + /* 頂点を追加 */ + pub fn add_vertex(&mut self, vet: Vertex) { + if self.adj_list.contains_key(&vet) { + return; + } + // 隣接リストに新しいリストを追加 + self.adj_list.insert(vet, vec![]); + } + + /* 頂点を削除 */ + #[allow(unused)] + pub fn remove_vertex(&mut self, vet: Vertex) { + // 隣接リストから頂点 vet に対応するリストを削除 + self.adj_list.remove(&vet); + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 + for list in self.adj_list.values_mut() { + list.retain(|&v| v != vet); + } + } + + /* 隣接リストを出力 */ + pub fn print(&self) { + println!("隣接リスト ="); + for (vertex, list) in &self.adj_list { + let list = list.iter().map(|vertex| vertex.val).collect::>(); + println!("{}: {:?},", vertex.val, list); + } + } + } ``` === "C" ```c title="graph_adjacency_list.c" - [class]{AdjListNode}-[func]{} + /* ノード構造体 */ + typedef struct AdjListNode { + Vertex *vertex; // 頂点 + struct AdjListNode *next; // 後続ノード + } AdjListNode; - [class]{GraphAdjList}-[func]{} + /* 頂点に対応するノードを検索 */ + AdjListNode *findNode(GraphAdjList *graph, Vertex *vet) { + for (int i = 0; i < graph->size; i++) { + if (graph->heads[i]->vertex == vet) { + return graph->heads[i]; + } + } + return NULL; + } + + /* 辺を追加する補助関数 */ + void addEdgeHelper(AdjListNode *head, Vertex *vet) { + AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode)); + node->vertex = vet; + // 先頭挿入法 + node->next = head->next; + head->next = node; + } + + /* 辺削除の補助関数 */ + void removeEdgeHelper(AdjListNode *head, Vertex *vet) { + AdjListNode *pre = head; + AdjListNode *cur = head->next; + // 連結リスト内で vet に対応するノードを探索 + while (cur != NULL && cur->vertex != vet) { + pre = cur; + cur = cur->next; + } + if (cur == NULL) + return; + // vet に対応するノードを連結リストから削除 + pre->next = cur->next; + // メモリを解放する + free(cur); + } + + /* 隣接リストに基づく無向グラフクラス */ + typedef struct { + AdjListNode *heads[MAX_SIZE]; // ノード配列 + int size; // ノード数 + } GraphAdjList; + + /* コンストラクタ */ + GraphAdjList *newGraphAdjList() { + GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList)); + if (!graph) { + return NULL; + } + graph->size = 0; + for (int i = 0; i < MAX_SIZE; i++) { + graph->heads[i] = NULL; + } + return graph; + } + + /* デストラクタ */ + void delGraphAdjList(GraphAdjList *graph) { + for (int i = 0; i < graph->size; i++) { + AdjListNode *cur = graph->heads[i]; + while (cur != NULL) { + AdjListNode *next = cur->next; + if (cur != graph->heads[i]) { + free(cur); + } + cur = next; + } + free(graph->heads[i]->vertex); + free(graph->heads[i]); + } + free(graph); + } + + /* 頂点に対応するノードを検索 */ + AdjListNode *findNode(GraphAdjList *graph, Vertex *vet) { + for (int i = 0; i < graph->size; i++) { + if (graph->heads[i]->vertex == vet) { + return graph->heads[i]; + } + } + return NULL; + } + + /* 辺を追加 */ + void addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) { + AdjListNode *head1 = findNode(graph, vet1); + AdjListNode *head2 = findNode(graph, vet2); + assert(head1 != NULL && head2 != NULL && head1 != head2); + // 辺 vet1 - vet2 を追加 + addEdgeHelper(head1, vet2); + addEdgeHelper(head2, vet1); + } + + /* 辺を削除 */ + void removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) { + AdjListNode *head1 = findNode(graph, vet1); + AdjListNode *head2 = findNode(graph, vet2); + assert(head1 != NULL && head2 != NULL); + // 辺 vet1 - vet2 を削除 + removeEdgeHelper(head1, head2->vertex); + removeEdgeHelper(head2, head1->vertex); + } + + /* 頂点を追加 */ + void addVertex(GraphAdjList *graph, Vertex *vet) { + assert(graph != NULL && graph->size < MAX_SIZE); + AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode)); + head->vertex = vet; + head->next = NULL; + // 隣接リストに新しいリストを追加 + graph->heads[graph->size++] = head; + } + + /* 頂点を削除 */ + void removeVertex(GraphAdjList *graph, Vertex *vet) { + AdjListNode *node = findNode(graph, vet); + assert(node != NULL); + // 隣接リストから頂点 vet に対応するリストを削除 + AdjListNode *cur = node, *pre = NULL; + while (cur) { + pre = cur; + cur = cur->next; + free(pre); + } + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 + for (int i = 0; i < graph->size; i++) { + cur = graph->heads[i]; + pre = NULL; + while (cur) { + pre = cur; + cur = cur->next; + if (cur && cur->vertex == vet) { + pre->next = cur->next; + free(cur); + break; + } + } + } + // この頂点より後ろの頂点を前に詰めて欠損を埋める + int i; + for (i = 0; i < graph->size; i++) { + if (graph->heads[i] == node) + break; + } + for (int j = i; j < graph->size - 1; j++) { + graph->heads[j] = graph->heads[j + 1]; + } + graph->size--; + free(vet); + } ``` === "Kotlin" ```kotlin title="graph_adjacency_list.kt" - [class]{GraphAdjList}-[func]{} + /* 隣接リストに基づく無向グラフクラス */ + class GraphAdjList(edges: Array>) { + // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 + val adjList = HashMap>() + + /* コンストラクタ */ + init { + // すべての頂点と辺を追加 + for (edge in edges) { + addVertex(edge[0]!!) + addVertex(edge[1]!!) + addEdge(edge[0]!!, edge[1]!!) + } + } + + /* 頂点数を取得 */ + fun size(): Int { + return adjList.size + } + + /* 辺を追加 */ + fun addEdge(vet1: Vertex, vet2: Vertex) { + if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2) + throw IllegalArgumentException() + // 辺 vet1 - vet2 を追加 + adjList[vet1]?.add(vet2) + adjList[vet2]?.add(vet1) + } + + /* 辺を削除 */ + fun removeEdge(vet1: Vertex, vet2: Vertex) { + if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2) + throw IllegalArgumentException() + // 辺 vet1 - vet2 を削除 + adjList[vet1]?.remove(vet2) + adjList[vet2]?.remove(vet1) + } + + /* 頂点を追加 */ + fun addVertex(vet: Vertex) { + if (adjList.containsKey(vet)) + return + // 隣接リストに新しいリストを追加 + adjList[vet] = mutableListOf() + } + + /* 頂点を削除 */ + fun removeVertex(vet: Vertex) { + if (!adjList.containsKey(vet)) + throw IllegalArgumentException() + // 隣接リストから頂点 vet に対応するリストを削除 + adjList.remove(vet) + // 他の頂点のリストを走査し、vet を含むすべての辺を削除 + for (list in adjList.values) { + list.remove(vet) + } + } + + /* 隣接リストを出力 */ + fun print() { + println("隣接リスト =") + for (pair in adjList.entries) { + val tmp = mutableListOf() + for (vertex in pair.value) { + tmp.add(vertex._val) + } + println("${pair.key._val}: $tmp,") + } + } + } ``` === "Ruby" ```ruby title="graph_adjacency_list.rb" - [class]{GraphAdjList}-[func]{} + ### 隣接リストで実装した無向グラフクラス ### + class GraphAdjList + attr_reader :adj_list + + ### コンストラクタ ### + def initialize(edges) + # 隣接リスト。key は頂点、value はその頂点に隣接する全頂点 + @adj_list = {} + # すべての頂点と辺を追加 + for edge in edges + add_vertex(edge[0]) + add_vertex(edge[1]) + add_edge(edge[0], edge[1]) + end + end + + ### 頂点数を取得 ### + def size + @adj_list.length + end + + ### 辺を追加 ### + def add_edge(vet1, vet2) + raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2) + + @adj_list[vet1] << vet2 + @adj_list[vet2] << vet1 + end + + ### 辺を削除 ### + def remove_edge(vet1, vet2) + raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2) + + # 辺 vet1 - vet2 を削除 + @adj_list[vet1].delete(vet2) + @adj_list[vet2].delete(vet1) + end + + ### 頂点を追加 ### + def add_vertex(vet) + return if @adj_list.include?(vet) + + # 隣接リストに新しいリストを追加 + @adj_list[vet] = [] + end + + ### 頂点を削除 ### + def remove_vertex(vet) + raise ArgumentError unless @adj_list.include?(vet) + + # 隣接リストから頂点 vet に対応するリストを削除 + @adj_list.delete(vet) + # 他の頂点のリストを走査し、vet を含むすべての辺を削除 + for vertex in @adj_list + @adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet) + end + end + + ### 隣接リストを出力 ### + def __print__ + puts '隣接リスト =' + for vertex in @adj_list + tmp = @adj_list[vertex.first].map { |v| v.val } + puts "#{vertex.first.val}: #{tmp}," + end + end + end ``` +??? pythontutor "コードの可視化" + +
+ + ## 9.2.3   効率の比較 -グラフに$n$個の頂点と$m$個の辺があると仮定すると、下表は隣接行列と隣接リストの時間効率と空間効率を比較しています。 +グラフに $n$ 個の頂点と $m$ 本の辺があるとすると、次の表は隣接行列と隣接リストの時間効率および空間効率を比較したものです。なお、隣接リスト(連結リスト)は本記事の実装に対応し、隣接リスト(ハッシュテーブル)はすべての連結リストをハッシュテーブルに置き換えた実装を指します。

表 9-2   隣接行列と隣接リストの比較

-| | 隣接行列 | 隣接リスト(連結リスト) | 隣接リスト(ハッシュテーブル) | -| ---------------- | -------------- | ----------------------- | ----------------------------- | -| 隣接性の判定 | $O(1)$ | $O(m)$ | $O(1)$ | -| 辺の追加 | $O(1)$ | $O(1)$ | $O(1)$ | -| 辺の削除 | $O(1)$ | $O(m)$ | $O(1)$ | -| 頂点の追加 | $O(n)$ | $O(1)$ | $O(1)$ | -| 頂点の削除 | $O(n^2)$ | $O(n + m)$ | $O(n)$ | -| メモリ空間使用量 | $O(n^2)$ | $O(n + m)$ | $O(n + m)$ | +| | 隣接行列 | 隣接リスト(連結リスト) | 隣接リスト(ハッシュテーブル) | +| ------------ | -------- | -------------- | ---------------- | +| 隣接判定 | $O(1)$ | $O(n)$ | $O(1)$ | +| 辺の追加 | $O(1)$ | $O(1)$ | $O(1)$ | +| 辺の削除 | $O(1)$ | $O(n)$ | $O(1)$ | +| 頂点の追加 | $O(n)$ | $O(1)$ | $O(1)$ | +| 頂点の削除 | $O(n^2)$ | $O(n + m)$ | $O(n)$ | +| メモリ使用量 | $O(n^2)$ | $O(n + m)$ | $O(n + m)$ |
-上表を観察すると、隣接リスト(ハッシュテーブル)が最高の時間効率と空間効率を持っているように見えます。しかし、実際には、隣接行列での辺に対する操作がより効率的で、単一の配列アクセスまたは代入操作のみが必要です。全体的に、隣接行列は「空間と時間のトレードオフ」の原則を例示し、隣接リストは「時間と空間のトレードオフ」を例示しています。 +上表を見ると、隣接リスト(ハッシュテーブル)の時間効率と空間効率が最も優れているように見えます。しかし実際には、隣接行列のほうが辺の操作効率は高く、必要なのは 1 回の配列アクセスまたは代入だけです。総合的に見ると、隣接行列は「空間を時間と引き換えにする」原則を体現し、隣接リストは「時間を空間と引き換えにする」原則を体現しています。 diff --git a/ja/docs/chapter_graph/graph_traversal.md b/ja/docs/chapter_graph/graph_traversal.md index 845c33a0e..dce1f9610 100644 --- a/ja/docs/chapter_graph/graph_traversal.md +++ b/ja/docs/chapter_graph/graph_traversal.md @@ -2,15 +2,15 @@ comments: true --- -# 9.3   グラフ走査 +# 9.3   グラフの走査 -木は「一対多」の関係を表現し、グラフはより高い自由度を持ち、任意の「多対多」の関係を表現できます。したがって、木をグラフの特別なケースと見なすことができます。明らかに、**木の走査操作もグラフ走査操作の特別なケースです**。 +木は「一対多」の関係を表すのに対し、グラフはより高い自由度を持ち、任意の「多対多」の関係を表現できます。したがって、木はグラフの一種の特殊な場合とみなせます。明らかに、**木の走査操作もグラフの走査操作の一種の特殊な場合です**。 -グラフと木の両方で、走査操作を実装するために探索アルゴリズムの応用が必要です。グラフ走査は2つのタイプに分けることができます:幅優先探索(BFS)深さ優先探索(DFS)です。 +グラフと木はいずれも、走査操作を実現するために探索アルゴリズムを用いる必要があります。グラフの走査方法も、幅優先走査深さ優先走査の 2 種類に分けられます。 -## 9.3.1   幅優先探索 +## 9.3.1   幅優先走査 -**幅優先探索は近くから遠くへの走査方法で、ある頂点から開始し、常に最も近い頂点を優先的に訪問し、層ごとに外側に展開していきます**。下図に示すように、左上の頂点から開始し、まずその頂点のすべての隣接頂点を走査し、次に次の頂点のすべての隣接頂点を走査し、以下同様に、すべての頂点が訪問されるまで続けます。 +**幅優先走査は、近いところから遠いところへ向かう走査方法であり、ある頂点から出発して、常に最も近い頂点を優先して訪問し、層ごとに外側へ広がっていきます**。以下の図に示すように、左上の頂点から出発し、まずその頂点のすべての隣接頂点を走査し、次に次の頂点のすべての隣接頂点を走査し、これを繰り返して、すべての頂点を訪問するまで続けます。 ![グラフの幅優先走査](graph_traversal.assets/graph_bfs.png){ class="animation-figure" } @@ -18,67 +18,71 @@ comments: true ### 1.   アルゴリズムの実装 -BFSは通常キューの助けを借りて実装されます(下記のコード参照)。キューは「先入先出」で、これは「近くから遠くへ」走査するBFSの考え方と一致します。 +BFS は通常キューを用いて実装され、コードは以下のとおりです。キューは「先入れ先出し」という性質を持ち、これは BFS の「近いところから遠いところへ」という考え方と本質的に一致しています。 -1. 開始頂点`startVet`をキューに追加し、ループを開始します。 -2. ループの各反復で、キューの先頭の頂点をポップし、それを訪問済みとして記録し、次にその頂点のすべての隣接頂点をキューの末尾に追加します。 -3. すべての頂点が訪問されるまで手順`2.`を繰り返します。 +1. 走査の開始頂点 `startVet` をキューに追加し、ループを開始します。 +2. ループの各反復で、キュー先頭の頂点を取り出して訪問を記録し、その後その頂点のすべての隣接頂点をキューの末尾に追加します。 +3. 手順 `2.` を繰り返し、すべての頂点が訪問されると終了します。 -頂点の再訪問を防ぐために、ハッシュセット`visited`を使用してどのノードが訪問されたかを記録します。 +頂点の重複走査を防ぐために、どの頂点が訪問済みかを記録するハッシュ集合 `visited` を用います。 + +!!! tip + + ハッシュ集合は、`value` を持たず `key` だけを格納するハッシュテーブルとみなせます。これは $O(1)$ の時間計算量で `key` の追加・削除・検索・更新を行えます。`key` の一意性にもとづき、ハッシュ集合は通常、データの重複排除などの場面で用いられます。 === "Python" ```python title="graph_bfs.py" def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]: - """幅優先走査""" - # 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得 - # 頂点走査シーケンス + """幅優先探索""" + # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する + # 頂点の走査順序 res = [] - # ハッシュセット、訪問済み頂点を記録するために使用 + # 訪問済み頂点を記録するためのハッシュ集合 visited = set[Vertex]([start_vet]) - # BFSを実装するために使用されるキュー + # BFS の実装にキューを用いる que = deque[Vertex]([start_vet]) - # 頂点vetから開始し、すべての頂点が訪問されるまでループ + # 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す while len(que) > 0: - vet = que.popleft() # キューの先頭の頂点をデキュー - res.append(vet) # 訪問済み頂点を記録 - # その頂点のすべての隣接頂点を走査 + vet = que.popleft() # 先頭の頂点をデキュー + res.append(vet) # 訪問した頂点を記録 + # この頂点のすべての隣接頂点を走査 for adj_vet in graph.adj_list[vet]: if adj_vet in visited: - continue # 既に訪問済みの頂点をスキップ - que.append(adj_vet) # 未訪問の頂点のみをエンキュー - visited.add(adj_vet) # 頂点を訪問済みとしてマーク - # 頂点走査シーケンスを返す + continue # 訪問済みの頂点をスキップ + que.append(adj_vet) # 未訪問の頂点のみをキューに追加 + visited.add(adj_vet) # この頂点を訪問済みにする + # 頂点の走査順を返す return res ``` === "C++" ```cpp title="graph_bfs.cpp" - /* 幅優先走査 */ - // 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得 + /* 幅優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする vector graphBFS(GraphAdjList &graph, Vertex *startVet) { - // 頂点走査順序 + // 頂点の走査順序 vector res; - // ハッシュセット、訪問済み頂点を記録するために使用 + // 訪問済み頂点を記録するためのハッシュ集合 unordered_set visited = {startVet}; - // BFSを実装するために使用されるキュー + // BFS の実装にキューを用いる queue que; que.push(startVet); - // 頂点vetから開始し、すべての頂点が訪問されるまでループ + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す while (!que.empty()) { Vertex *vet = que.front(); - que.pop(); // キューの先頭の頂点をデキュー - res.push_back(vet); // 訪問済み頂点を記録 - // その頂点のすべての隣接頂点を走査 + que.pop(); // 先頭の頂点をデキュー + res.push_back(vet); // 訪問した頂点を記録 + // この頂点のすべての隣接頂点を走査 for (auto adjVet : graph.adjList[vet]) { if (visited.count(adjVet)) - continue; // すでに訪問済みの頂点をスキップ - que.push(adjVet); // 未訪問の頂点のみをエンキュー - visited.emplace(adjVet); // 頂点を訪問済みとしてマーク + continue; // 訪問済みの頂点をスキップ + que.push(adjVet); // 未訪問の頂点のみをキューに追加 + visited.emplace(adjVet); // この頂点を訪問済みにする } } - // 頂点走査順序を返す + // 頂点の走査順を返す return res; } ``` @@ -86,30 +90,30 @@ BFSは通常キューの助けを借りて実装されます(下記のコー === "Java" ```java title="graph_bfs.java" - /* 幅優先走査 */ - // 隣接リストを使用してグラフを表現し、指定した頂点のすべての隣接頂点を取得 + /* 幅優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする List graphBFS(GraphAdjList graph, Vertex startVet) { - // 頂点走査順序 + // 頂点の走査順序 List res = new ArrayList<>(); - // ハッシュセット、訪問済みの頂点を記録するために使用 + // 訪問済み頂点を記録するためのハッシュ集合 Set visited = new HashSet<>(); visited.add(startVet); - // BFS を実装するために使用するキュー + // BFS の実装にキューを用いる Queue que = new LinkedList<>(); que.offer(startVet); - // 頂点 vet から開始し、すべての頂点が訪問されるまでループ + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す while (!que.isEmpty()) { - Vertex vet = que.poll(); // キューの先頭の頂点をデキュー + Vertex vet = que.poll(); // 先頭の頂点をデキュー res.add(vet); // 訪問した頂点を記録 - // その頂点のすべての隣接頂点を走査 + // この頂点のすべての隣接頂点を走査 for (Vertex adjVet : graph.adjList.get(vet)) { if (visited.contains(adjVet)) - continue; // すでに訪問済みの頂点をスキップ - que.offer(adjVet); // 未訪問の頂点のみをエンキュー - visited.add(adjVet); // 頂点を訪問済みとしてマーク + continue; // 訪問済みの頂点をスキップ + que.offer(adjVet); // 未訪問の頂点のみをキューに追加 + visited.add(adjVet); // この頂点を訪問済みにする } } - // 頂点走査順序を返す + // 頂点の走査順を返す return res; } ``` @@ -117,71 +121,369 @@ BFSは通常キューの助けを借りて実装されます(下記のコー === "C#" ```csharp title="graph_bfs.cs" - [class]{graph_bfs}-[func]{GraphBFS} + /* 幅優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + List GraphBFS(GraphAdjList graph, Vertex startVet) { + // 頂点の走査順序 + List res = []; + // 訪問済み頂点を記録するためのハッシュ集合 + HashSet visited = [startVet]; + // BFS の実装にキューを用いる + Queue que = new(); + que.Enqueue(startVet); + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す + while (que.Count > 0) { + Vertex vet = que.Dequeue(); // 先頭の頂点をデキュー + res.Add(vet); // 訪問した頂点を記録 + foreach (Vertex adjVet in graph.adjList[vet]) { + if (visited.Contains(adjVet)) { + continue; // 訪問済みの頂点をスキップ + } + que.Enqueue(adjVet); // 未訪問の頂点のみをキューに追加 + visited.Add(adjVet); // この頂点を訪問済みにする + } + } + + // 頂点の走査順を返す + return res; + } ``` === "Go" ```go title="graph_bfs.go" - [class]{}-[func]{graphBFS} + /* 幅優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + func graphBFS(g *graphAdjList, startVet Vertex) []Vertex { + // 頂点の走査順序 + res := make([]Vertex, 0) + // 訪問済み頂点を記録するためのハッシュ集合 + visited := make(map[Vertex]struct{}) + visited[startVet] = struct{}{} + // キューは BFS の実装に用い、スライスでキューをシミュレートする + queue := make([]Vertex, 0) + queue = append(queue, startVet) + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す + for len(queue) > 0 { + // 先頭の頂点をデキュー + vet := queue[0] + queue = queue[1:] + // 訪問した頂点を記録 + res = append(res, vet) + // この頂点のすべての隣接頂点を走査 + for _, adjVet := range g.adjList[vet] { + _, isExist := visited[adjVet] + // 未訪問の頂点のみをキューに追加 + if !isExist { + queue = append(queue, adjVet) + visited[adjVet] = struct{}{} + } + } + } + // 頂点の走査順を返す + return res + } ``` === "Swift" ```swift title="graph_bfs.swift" - [class]{}-[func]{graphBFS} + /* 幅優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + func graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] { + // 頂点の走査順序 + var res: [Vertex] = [] + // 訪問済み頂点を記録するためのハッシュ集合 + var visited: Set = [startVet] + // BFS の実装にキューを用いる + var que: [Vertex] = [startVet] + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す + while !que.isEmpty { + let vet = que.removeFirst() // 先頭の頂点をデキュー + res.append(vet) // 訪問した頂点を記録 + // この頂点のすべての隣接頂点を走査 + for adjVet in graph.adjList[vet] ?? [] { + if visited.contains(adjVet) { + continue // 訪問済みの頂点をスキップ + } + que.append(adjVet) // 未訪問の頂点のみをキューに追加 + visited.insert(adjVet) // この頂点を訪問済みにする + } + } + // 頂点の走査順を返す + return res + } ``` === "JS" ```javascript title="graph_bfs.js" - [class]{}-[func]{graphBFS} + /* 幅優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + function graphBFS(graph, startVet) { + // 頂点の走査順序 + const res = []; + // 訪問済み頂点を記録するためのハッシュ集合 + const visited = new Set(); + visited.add(startVet); + // BFS の実装にキューを用いる + const que = [startVet]; + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す + while (que.length) { + const vet = que.shift(); // 先頭の頂点をデキュー + res.push(vet); // 訪問した頂点を記録 + // この頂点のすべての隣接頂点を走査 + for (const adjVet of graph.adjList.get(vet) ?? []) { + if (visited.has(adjVet)) { + continue; // 訪問済みの頂点をスキップ + } + que.push(adjVet); // 未訪問の頂点のみをキューに追加 + visited.add(adjVet); // この頂点を訪問済みにする + } + } + // 頂点の走査順を返す + return res; + } ``` === "TS" ```typescript title="graph_bfs.ts" - [class]{}-[func]{graphBFS} + /* 幅優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + function graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] { + // 頂点の走査順序 + const res: Vertex[] = []; + // 訪問済み頂点を記録するためのハッシュ集合 + const visited: Set = new Set(); + visited.add(startVet); + // BFS の実装にキューを用いる + const que = [startVet]; + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す + while (que.length) { + const vet = que.shift(); // 先頭の頂点をデキュー + res.push(vet); // 訪問した頂点を記録 + // この頂点のすべての隣接頂点を走査 + for (const adjVet of graph.adjList.get(vet) ?? []) { + if (visited.has(adjVet)) { + continue; // 訪問済みの頂点をスキップ + } + que.push(adjVet); // 未訪問のものだけをキューに入れる + visited.add(adjVet); // この頂点を訪問済みにする + } + } + // 頂点の走査順を返す + return res; + } ``` === "Dart" ```dart title="graph_bfs.dart" - [class]{}-[func]{graphBFS} + /* 幅優先探索 */ + List graphBFS(GraphAdjList graph, Vertex startVet) { + // 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する + // 頂点の走査順序 + List res = []; + // 訪問済み頂点を記録するためのハッシュ集合 + Set visited = {}; + visited.add(startVet); + // BFS の実装にキューを用いる + Queue que = Queue(); + que.add(startVet); + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す + while (que.isNotEmpty) { + Vertex vet = que.removeFirst(); // 先頭の頂点をデキュー + res.add(vet); // 訪問した頂点を記録 + // この頂点のすべての隣接頂点を走査 + for (Vertex adjVet in graph.adjList[vet]!) { + if (visited.contains(adjVet)) { + continue; // 訪問済みの頂点をスキップ + } + que.add(adjVet); // 未訪問の頂点のみをキューに追加 + visited.add(adjVet); // この頂点を訪問済みにする + } + } + // 頂点の走査順を返す + return res; + } ``` === "Rust" ```rust title="graph_bfs.rs" - [class]{}-[func]{graph_bfs} + /* 幅優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + fn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec { + // 頂点の走査順序 + let mut res = vec![]; + // 訪問済み頂点を記録するためのハッシュ集合 + let mut visited = HashSet::new(); + visited.insert(start_vet); + // BFS の実装にキューを用いる + let mut que = VecDeque::new(); + que.push_back(start_vet); + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す + while let Some(vet) = que.pop_front() { + res.push(vet); // 訪問した頂点を記録 + + // この頂点のすべての隣接頂点を走査 + if let Some(adj_vets) = graph.adj_list.get(&vet) { + for &adj_vet in adj_vets { + if visited.contains(&adj_vet) { + continue; // 訪問済みの頂点をスキップ + } + que.push_back(adj_vet); // 未訪問の頂点のみをキューに追加 + visited.insert(adj_vet); // この頂点を訪問済みにする + } + } + } + // 頂点の走査順を返す + res + } ``` === "C" ```c title="graph_bfs.c" - [class]{Queue}-[func]{} + /* ノードキュー構造体 */ + typedef struct { + Vertex *vertices[MAX_SIZE]; + int front, rear, size; + } Queue; - [class]{}-[func]{isVisited} + /* コンストラクタ */ + Queue *newQueue() { + Queue *q = (Queue *)malloc(sizeof(Queue)); + q->front = q->rear = q->size = 0; + return q; + } - [class]{}-[func]{graphBFS} + /* キューが空かどうかを判定 */ + int isEmpty(Queue *q) { + return q->size == 0; + } + + /* エンキュー操作 */ + void enqueue(Queue *q, Vertex *vet) { + q->vertices[q->rear] = vet; + q->rear = (q->rear + 1) % MAX_SIZE; + q->size++; + } + + /* デキュー操作 */ + Vertex *dequeue(Queue *q) { + Vertex *vet = q->vertices[q->front]; + q->front = (q->front + 1) % MAX_SIZE; + q->size--; + return vet; + } + + /* 頂点が訪問済みかを確認 */ + int isVisited(Vertex **visited, int size, Vertex *vet) { + // 走査してノードを探すため、O(n) 時間を要する + for (int i = 0; i < size; i++) { + if (visited[i] == vet) + return 1; + } + return 0; + } + + /* 幅優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + void graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) { + // BFS の実装にキューを用いる + Queue *queue = newQueue(); + enqueue(queue, startVet); + visited[(*visitedSize)++] = startVet; + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す + while (!isEmpty(queue)) { + Vertex *vet = dequeue(queue); // 先頭の頂点をデキュー + res[(*resSize)++] = vet; // 訪問した頂点を記録 + // この頂点のすべての隣接頂点を走査 + AdjListNode *node = findNode(graph, vet); + while (node != NULL) { + // 訪問済みの頂点をスキップ + if (!isVisited(visited, *visitedSize, node->vertex)) { + enqueue(queue, node->vertex); // 未訪問の頂点のみをキューに追加 + visited[(*visitedSize)++] = node->vertex; // この頂点を訪問済みにする + } + node = node->next; + } + } + // メモリを解放する + free(queue); + } ``` === "Kotlin" ```kotlin title="graph_bfs.kt" - [class]{}-[func]{graphBFS} + /* 幅優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + fun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList { + // 頂点の走査順序 + val res = mutableListOf() + // 訪問済み頂点を記録するためのハッシュ集合 + val visited = HashSet() + visited.add(startVet) + // BFS の実装にキューを用いる + val que = LinkedList() + que.offer(startVet) + // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す + while (!que.isEmpty()) { + val vet = que.poll() // 先頭の頂点をデキュー + res.add(vet) // 訪問した頂点を記録 + // この頂点のすべての隣接頂点を走査 + for (adjVet in graph.adjList[vet]!!) { + if (visited.contains(adjVet)) + continue // 訪問済みの頂点をスキップ + que.offer(adjVet) // 未訪問の頂点のみをキューに追加 + visited.add(adjVet) // この頂点を訪問済みにする + } + } + // 頂点の走査順を返す + return res + } ``` === "Ruby" ```ruby title="graph_bfs.rb" - [class]{}-[func]{graph_bfs} + ### 幅優先探索 ### + def graph_bfs(graph, start_vet) + # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する + # 頂点の走査順序 + res = [] + # 訪問済み頂点を記録するためのハッシュ集合 + visited = Set.new([start_vet]) + # BFS の実装にキューを用いる + que = [start_vet] + # 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す + while que.length > 0 + vet = que.shift # 先頭の頂点をデキュー + res << vet # 訪問した頂点を記録 + # この頂点のすべての隣接頂点を走査 + for adj_vet in graph.adj_list[vet] + next if visited.include?(adj_vet) # 訪問済みの頂点をスキップ + que << adj_vet # 未訪問の頂点のみをキューに追加 + visited.add(adj_vet) # この頂点を訪問済みにする + end + end + # 頂点の走査順を返す + res + end ``` -コードは比較的抽象的ですが、下図と比較することでより良く理解できます。 +??? pythontutor "コードの可視化" + +
+ + +コードはやや抽象的なので、以下の図と照らし合わせて理解を深めることを勧めます。 === "<1>" - ![グラフの幅優先探索の手順](graph_traversal.assets/graph_bfs_step1.png){ class="animation-figure" } + ![グラフの幅優先走査の手順](graph_traversal.assets/graph_bfs_step1.png){ class="animation-figure" } === "<2>" ![graph_bfs_step2](graph_traversal.assets/graph_bfs_step2.png){ class="animation-figure" } @@ -213,21 +515,21 @@ BFSは通常キューの助けを借りて実装されます(下記のコー === "<11>" ![graph_bfs_step11](graph_traversal.assets/graph_bfs_step11.png){ class="animation-figure" } -

図 9-10   グラフの幅優先探索の手順

+

図 9-10   グラフの幅優先走査の手順

-!!! question "幅優先走査のシーケンスは一意ですか?" +!!! question "幅優先走査の順序列は一意ですか?" - 一意ではありません。幅優先走査は「近くから遠く」の順序で走査することのみを要求し、**同じ距離の頂点の走査順序は任意にできます**。例えば、上図では、頂点$1$と$3$の訪問順序を交換できますし、頂点$2$、$4$、$6$の順序も同様です。 + 一意ではありません。幅優先走査は「近いところから遠いところへ」の順で走査することだけを要求し、**同じ距離にある複数の頂点の走査順は任意に入れ替えて構いません**。上図を例にすると、頂点 $1$ と $3$ の訪問順は交換でき、頂点 $2$、$4$、$6$ の訪問順も任意に入れ替えられます。 -### 2.   計算量分析 +### 2.   計算量の分析 -**時間計算量**:すべての頂点が一度ずつエンキューおよびデキューされ、$O(|V|)$時間を使用します。隣接頂点を走査する過程で、無向グラフであるため、すべての辺が$2$回訪問され、$O(2|E|)$時間を使用します。全体で$O(|V| + |E|)$時間を使用します。 +**時間計算量**:すべての頂点は 1 回ずつキューに入り、1 回ずつキューから出るため、$O(|V|)$ 時間です。隣接頂点を走査する過程では、無向グラフであるため、すべての辺が $2$ 回訪問され、$O(2|E|)$ 時間です。したがって全体では $O(|V| + |E|)$ 時間です。 -**空間計算量**:リスト`res`、ハッシュセット`visited`、キュー`que`の最大頂点数は$|V|$で、$O(|V|)$空間を使用します。 +**空間計算量**:リスト `res`、ハッシュ集合 `visited`、キュー `que` に含まれる頂点数は最大で $|V|$ であるため、$O(|V|)$ 空間です。 -## 9.3.2   深さ優先探索 +## 9.3.2   深さ優先走査 -**深さ優先探索は可能な限り遠くまで行き、それ以上のパスがない場合にバックトラックする走査方法です**。下図に示すように、左上の頂点から開始し、それ以上のパスがなくなるまで現在の頂点のいずれかの隣接頂点を訪問し、次に戻って続行し、すべての頂点が走査されるまで続けます。 +**深さ優先走査は、まず行けるところまで進み、進めなくなったら戻る走査方法です**。以下の図に示すように、左上の頂点から出発し、現在の頂点のある隣接頂点を訪問して、行き止まりに達するまで進んだら戻り、再び別の方向へ進んで行き止まりまで進んで戻る、ということを繰り返し、すべての頂点の走査が完了するまで続けます。 ![グラフの深さ優先走査](graph_traversal.assets/graph_dfs.png){ class="animation-figure" } @@ -235,28 +537,28 @@ BFSは通常キューの助けを借りて実装されます(下記のコー ### 1.   アルゴリズムの実装 -この「可能な限り遠くまで行ってから戻る」アルゴリズムパラダイムは通常再帰に基づいて実装されます。幅優先探索と同様に、深さ優先探索でも、再訪問を避けるために訪問済み頂点を記録するハッシュセット`visited`の助けが必要です。 +この「行き止まりまで進んでから戻る」アルゴリズムのパターンは、通常再帰にもとづいて実装されます。幅優先走査と同様に、深さ優先走査でも、頂点の重複訪問を避けるために、訪問済みの頂点を記録するハッシュ集合 `visited` を用います。 === "Python" ```python title="graph_dfs.py" def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex): - """深さ優先走査のヘルパー関数""" - res.append(vet) # 訪問済み頂点を記録 - visited.add(vet) # 頂点を訪問済みとしてマーク - # その頂点のすべての隣接頂点を走査 + """深さ優先走査の補助関数""" + res.append(vet) # 訪問した頂点を記録 + visited.add(vet) # この頂点を訪問済みにする + # この頂点のすべての隣接頂点を走査 for adjVet in graph.adj_list[vet]: if adjVet in visited: - continue # 既に訪問済みの頂点をスキップ + continue # 訪問済みの頂点をスキップ # 隣接頂点を再帰的に訪問 dfs(graph, visited, res, adjVet) def graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]: - """深さ優先走査""" - # 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得 - # 頂点走査シーケンス + """深さ優先探索""" + # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する + # 頂点の走査順序 res = [] - # ハッシュセット、訪問済み頂点を記録するために使用 + # 訪問済み頂点を記録するためのハッシュ集合 visited = set[Vertex]() dfs(graph, visited, res, start_vet) return res @@ -265,25 +567,25 @@ BFSは通常キューの助けを借りて実装されます(下記のコー === "C++" ```cpp title="graph_dfs.cpp" - /* 深さ優先走査ヘルパー関数 */ + /* 深さ優先走査の補助関数 */ void dfs(GraphAdjList &graph, unordered_set &visited, vector &res, Vertex *vet) { - res.push_back(vet); // 訪問済み頂点を記録 - visited.emplace(vet); // 頂点を訪問済みとしてマーク - // その頂点のすべての隣接頂点を走査 + res.push_back(vet); // 訪問した頂点を記録 + visited.emplace(vet); // この頂点を訪問済みにする + // この頂点のすべての隣接頂点を走査 for (Vertex *adjVet : graph.adjList[vet]) { if (visited.count(adjVet)) - continue; // すでに訪問済みの頂点をスキップ + continue; // 訪問済みの頂点をスキップ // 隣接頂点を再帰的に訪問 dfs(graph, visited, res, adjVet); } } - /* 深さ優先走査 */ - // 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得 + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする vector graphDFS(GraphAdjList &graph, Vertex *startVet) { - // 頂点走査順序 + // 頂点の走査順序 vector res; - // ハッシュセット、訪問済み頂点を記録するために使用 + // 訪問済み頂点を記録するためのハッシュ集合 unordered_set visited; dfs(graph, visited, res, startVet); return res; @@ -296,22 +598,22 @@ BFSは通常キューの助けを借りて実装されます(下記のコー /* 深さ優先走査の補助関数 */ void dfs(GraphAdjList graph, Set visited, List res, Vertex vet) { res.add(vet); // 訪問した頂点を記録 - visited.add(vet); // 頂点を訪問済みとしてマーク - // その頂点のすべての隣接頂点を走査 + visited.add(vet); // この頂点を訪問済みにする + // この頂点のすべての隣接頂点を走査 for (Vertex adjVet : graph.adjList.get(vet)) { if (visited.contains(adjVet)) - continue; // すでに訪問済みの頂点をスキップ + continue; // 訪問済みの頂点をスキップ // 隣接頂点を再帰的に訪問 dfs(graph, visited, res, adjVet); } } - /* 深さ優先走査 */ - // 隣接リストを使用してグラフを表現し、指定した頂点のすべての隣接頂点を取得 + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする List graphDFS(GraphAdjList graph, Vertex startVet) { - // 頂点走査順序 + // 頂点の走査順序 List res = new ArrayList<>(); - // ハッシュセット、訪問済みの頂点を記録するために使用 + // 訪問済み頂点を記録するためのハッシュ集合 Set visited = new HashSet<>(); dfs(graph, visited, res, startVet); return res; @@ -321,94 +623,332 @@ BFSは通常キューの助けを借りて実装されます(下記のコー === "C#" ```csharp title="graph_dfs.cs" - [class]{graph_dfs}-[func]{DFS} + /* 深さ優先走査の補助関数 */ + void DFS(GraphAdjList graph, HashSet visited, List res, Vertex vet) { + res.Add(vet); // 訪問した頂点を記録 + visited.Add(vet); // この頂点を訪問済みにする + // この頂点のすべての隣接頂点を走査 + foreach (Vertex adjVet in graph.adjList[vet]) { + if (visited.Contains(adjVet)) { + continue; // 訪問済みの頂点をスキップ + } + // 隣接頂点を再帰的に訪問 + DFS(graph, visited, res, adjVet); + } + } - [class]{graph_dfs}-[func]{GraphDFS} + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + List GraphDFS(GraphAdjList graph, Vertex startVet) { + // 頂点の走査順序 + List res = []; + // 訪問済み頂点を記録するためのハッシュ集合 + HashSet visited = []; + DFS(graph, visited, res, startVet); + return res; + } ``` === "Go" ```go title="graph_dfs.go" - [class]{}-[func]{dfs} + /* 深さ優先走査の補助関数 */ + func dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) { + // append 操作は新しい参照を返すため、元の参照を新しい slice の参照で再代入する必要がある + *res = append(*res, vet) + visited[vet] = struct{}{} + // この頂点のすべての隣接頂点を走査 + for _, adjVet := range g.adjList[vet] { + _, isExist := visited[adjVet] + // 隣接頂点を再帰的に訪問 + if !isExist { + dfs(g, visited, res, adjVet) + } + } + } - [class]{}-[func]{graphDFS} + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + func graphDFS(g *graphAdjList, startVet Vertex) []Vertex { + // 頂点の走査順序 + res := make([]Vertex, 0) + // 訪問済み頂点を記録するためのハッシュ集合 + visited := make(map[Vertex]struct{}) + dfs(g, visited, &res, startVet) + // 頂点の走査順を返す + return res + } ``` === "Swift" ```swift title="graph_dfs.swift" - [class]{}-[func]{dfs} + /* 深さ優先走査の補助関数 */ + func dfs(graph: GraphAdjList, visited: inout Set, res: inout [Vertex], vet: Vertex) { + res.append(vet) // 訪問した頂点を記録 + visited.insert(vet) // この頂点を訪問済みにする + // この頂点のすべての隣接頂点を走査 + for adjVet in graph.adjList[vet] ?? [] { + if visited.contains(adjVet) { + continue // 訪問済みの頂点をスキップ + } + // 隣接頂点を再帰的に訪問 + dfs(graph: graph, visited: &visited, res: &res, vet: adjVet) + } + } - [class]{}-[func]{graphDFS} + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + func graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] { + // 頂点の走査順序 + var res: [Vertex] = [] + // 訪問済み頂点を記録するためのハッシュ集合 + var visited: Set = [] + dfs(graph: graph, visited: &visited, res: &res, vet: startVet) + return res + } ``` === "JS" ```javascript title="graph_dfs.js" - [class]{}-[func]{dfs} + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + function dfs(graph, visited, res, vet) { + res.push(vet); // 訪問した頂点を記録 + visited.add(vet); // この頂点を訪問済みにする + // この頂点のすべての隣接頂点を走査 + for (const adjVet of graph.adjList.get(vet)) { + if (visited.has(adjVet)) { + continue; // 訪問済みの頂点をスキップ + } + // 隣接頂点を再帰的に訪問 + dfs(graph, visited, res, adjVet); + } + } - [class]{}-[func]{graphDFS} + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + function graphDFS(graph, startVet) { + // 頂点の走査順序 + const res = []; + // 訪問済み頂点を記録するためのハッシュ集合 + const visited = new Set(); + dfs(graph, visited, res, startVet); + return res; + } ``` === "TS" ```typescript title="graph_dfs.ts" - [class]{}-[func]{dfs} + /* 深さ優先走査の補助関数 */ + function dfs( + graph: GraphAdjList, + visited: Set, + res: Vertex[], + vet: Vertex + ): void { + res.push(vet); // 訪問した頂点を記録 + visited.add(vet); // この頂点を訪問済みにする + // この頂点のすべての隣接頂点を走査 + for (const adjVet of graph.adjList.get(vet)) { + if (visited.has(adjVet)) { + continue; // 訪問済みの頂点をスキップ + } + // 隣接頂点を再帰的に訪問 + dfs(graph, visited, res, adjVet); + } + } - [class]{}-[func]{graphDFS} + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + function graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] { + // 頂点の走査順序 + const res: Vertex[] = []; + // 訪問済み頂点を記録するためのハッシュ集合 + const visited: Set = new Set(); + dfs(graph, visited, res, startVet); + return res; + } ``` === "Dart" ```dart title="graph_dfs.dart" - [class]{}-[func]{dfs} + /* 深さ優先走査の補助関数 */ + void dfs( + GraphAdjList graph, + Set visited, + List res, + Vertex vet, + ) { + res.add(vet); // 訪問した頂点を記録 + visited.add(vet); // この頂点を訪問済みにする + // この頂点のすべての隣接頂点を走査 + for (Vertex adjVet in graph.adjList[vet]!) { + if (visited.contains(adjVet)) { + continue; // 訪問済みの頂点をスキップ + } + // 隣接頂点を再帰的に訪問 + dfs(graph, visited, res, adjVet); + } + } - [class]{}-[func]{graphDFS} + /* 深さ優先探索 */ + List graphDFS(GraphAdjList graph, Vertex startVet) { + // 頂点の走査順序 + List res = []; + // 訪問済み頂点を記録するためのハッシュ集合 + Set visited = {}; + dfs(graph, visited, res, startVet); + return res; + } ``` === "Rust" ```rust title="graph_dfs.rs" - [class]{}-[func]{dfs} + /* 深さ優先走査の補助関数 */ + fn dfs(graph: &GraphAdjList, visited: &mut HashSet, res: &mut Vec, vet: Vertex) { + res.push(vet); // 訪問した頂点を記録 + visited.insert(vet); // この頂点を訪問済みにする + // この頂点のすべての隣接頂点を走査 + if let Some(adj_vets) = graph.adj_list.get(&vet) { + for &adj_vet in adj_vets { + if visited.contains(&adj_vet) { + continue; // 訪問済みの頂点をスキップ + } + // 隣接頂点を再帰的に訪問 + dfs(graph, visited, res, adj_vet); + } + } + } - [class]{}-[func]{graph_dfs} + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + fn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec { + // 頂点の走査順序 + let mut res = vec![]; + // 訪問済み頂点を記録するためのハッシュ集合 + let mut visited = HashSet::new(); + dfs(&graph, &mut visited, &mut res, start_vet); + + res + } ``` === "C" ```c title="graph_dfs.c" - [class]{}-[func]{isVisited} + /* 頂点が訪問済みかを確認 */ + int isVisited(Vertex **res, int size, Vertex *vet) { + // 走査してノードを探すため、O(n) 時間を要する + for (int i = 0; i < size; i++) { + if (res[i] == vet) { + return 1; + } + } + return 0; + } - [class]{}-[func]{dfs} + /* 深さ優先走査の補助関数 */ + void dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) { + // 訪問した頂点を記録 + res[(*resSize)++] = vet; + // この頂点のすべての隣接頂点を走査 + AdjListNode *node = findNode(graph, vet); + while (node != NULL) { + // 訪問済みの頂点をスキップ + if (!isVisited(res, *resSize, node->vertex)) { + // 隣接頂点を再帰的に訪問 + dfs(graph, res, resSize, node->vertex); + } + node = node->next; + } + } - [class]{}-[func]{graphDFS} + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + void graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) { + dfs(graph, res, resSize, startVet); + } ``` === "Kotlin" ```kotlin title="graph_dfs.kt" - [class]{}-[func]{dfs} + /* 深さ優先走査の補助関数 */ + fun dfs( + graph: GraphAdjList, + visited: MutableSet, + res: MutableList, + vet: Vertex? + ) { + res.add(vet) // 訪問した頂点を記録 + visited.add(vet) // この頂点を訪問済みにする + // この頂点のすべての隣接頂点を走査 + for (adjVet in graph.adjList[vet]!!) { + if (visited.contains(adjVet)) + continue // 訪問済みの頂点をスキップ + // 隣接頂点を再帰的に訪問 + dfs(graph, visited, res, adjVet) + } + } - [class]{}-[func]{graphDFS} + /* 深さ優先探索 */ + // グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする + fun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList { + // 頂点の走査順序 + val res = mutableListOf() + // 訪問済み頂点を記録するためのハッシュ集合 + val visited = HashSet() + dfs(graph, visited, res, startVet) + return res + } ``` === "Ruby" ```ruby title="graph_dfs.rb" - [class]{}-[func]{dfs} + ### 深さ優先探索の補助関数 ### + def dfs(graph, visited, res, vet) + res << vet # 訪問した頂点を記録 + visited.add(vet) # この頂点を訪問済みにする + # この頂点のすべての隣接頂点を走査 + for adj_vet in graph.adj_list[vet] + next if visited.include?(adj_vet) # 訪問済みの頂点をスキップ + # 隣接頂点を再帰的に訪問 + dfs(graph, visited, res, adj_vet) + end + end - [class]{}-[func]{graph_dfs} + ### 深さ優先探索 ### + def graph_dfs(graph, start_vet) + # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する + # 頂点の走査順序 + res = [] + # 訪問済み頂点を記録するためのハッシュ集合 + visited = Set.new + dfs(graph, visited, res, start_vet) + res + end ``` -深さ優先探索のアルゴリズムプロセスを下図に示します。 +??? pythontutor "コードの可視化" -- **破線は下向きの再帰を表し**、新しい頂点を訪問するために新しい再帰メソッドが開始されたことを示します。 -- **曲線の破線は上向きのバックトラックを表し**、この再帰メソッドがこのメソッドが開始された位置に戻ったことを示します。 +
+ -理解を深めるため、下図とコードを組み合わせて、DFSプロセス全体を頭の中でシミュレート(または描画)することをお勧めします。各再帰メソッドがいつ開始され、いつ戻るかを含めてです。 +深さ優先走査のアルゴリズムの流れは以下の図のとおりです。 + +- **直線の破線は下向きの再帰呼び出し**を表し、新しい頂点を訪問するために新たな再帰メソッドが開始されたことを意味します。 +- **曲線の破線は上向きのバックトラック**を表し、この再帰メソッドがすでに戻って、呼び出し元の位置までたどり着いたことを意味します。 + +理解を深めるために、以下の図とコードを結びつけて、DFS 全体の過程を頭の中でシミュレーションする(あるいは紙に書き出す)ことを勧めます。各再帰メソッドがいつ開始し、いつ戻るかも含めて追ってみてください。 === "<1>" - ![グラフの深さ優先探索の手順](graph_traversal.assets/graph_dfs_step1.png){ class="animation-figure" } + ![グラフの深さ優先走査の手順](graph_traversal.assets/graph_dfs_step1.png){ class="animation-figure" } === "<2>" ![graph_dfs_step2](graph_traversal.assets/graph_dfs_step2.png){ class="animation-figure" } @@ -440,16 +980,16 @@ BFSは通常キューの助けを借りて実装されます(下記のコー === "<11>" ![graph_dfs_step11](graph_traversal.assets/graph_dfs_step11.png){ class="animation-figure" } -

図 9-12   グラフの深さ優先探索の手順

+

図 9-12   グラフの深さ優先走査の手順

-!!! question "深さ優先走査のシーケンスは一意ですか?" +!!! question "深さ優先走査の順序列は一意ですか?" - 幅優先走査と同様に、深さ優先走査シーケンスの順序も一意ではありません。ある頂点が与えられた場合、どの方向を最初に探索することも可能です。つまり、隣接頂点の順序は任意にシャッフルできますが、すべて深さ優先走査の一部です。 + 幅優先走査と同様に、深さ優先走査の順序列も一意ではありません。ある頂点が与えられたとき、どの方向を先に探索してもよく、つまり隣接頂点の順序は任意に入れ替えられ、それでも深さ優先走査になります。 + + 木の走査を例にすると、「根 $\rightarrow$ 左 $\rightarrow$ 右」「左 $\rightarrow$ 根 $\rightarrow$ 右」「左 $\rightarrow$ 右 $\rightarrow$ 根」は、それぞれ先行順、中間順、後行順走査に対応します。これらは 3 種類の走査優先順位を示していますが、いずれも深さ優先走査に属します。 - 木の走査を例に取ると、「根 $\rightarrow$ 左 $\rightarrow$ 右」、「左 $\rightarrow$ 根 $\rightarrow$ 右」、「左 $\rightarrow$ 右 $\rightarrow$ 根」は、それぞれ前順、中順、後順走査に対応します。これらは3つの異なる走査優先度を示していますが、3つすべてが深さ優先走査と見なされます。 +### 2.   計算量の分析 -### 2.   計算量分析 +**時間計算量**:すべての頂点は $1$ 回ずつ訪問されるため、$O(|V|)$ 時間です。すべての辺は $2$ 回ずつ訪問されるため、$O(2|E|)$ 時間です。したがって全体では $O(|V| + |E|)$ 時間です。 -**時間計算量**:すべての頂点が一度訪問され、$O(|V|)$時間を使用します。すべての辺が2回訪問され、$O(2|E|)$時間を使用します。全体で$O(|V| + |E|)$時間を使用します。 - -**空間計算量**:リスト`res`、ハッシュセット`visited`の最大頂点数は$|V|$で、最大再帰深度は$|V|$です。したがって、$O(|V|)$空間を使用します。 +**空間計算量**:リスト `res` とハッシュ集合 `visited` に含まれる頂点数は最大で $|V|$ であり、再帰の深さも最大で $|V|$ であるため、$O(|V|)$ 空間です。 diff --git a/ja/docs/chapter_graph/index.md b/ja/docs/chapter_graph/index.md index c02e9a3fe..6b5d53eec 100644 --- a/ja/docs/chapter_graph/index.md +++ b/ja/docs/chapter_graph/index.md @@ -9,9 +9,9 @@ icon: material/graphql !!! abstract - 人生の旅路において、私たちの一人一人はノードであり、無数の見えない辺で結ばれています。 - - 一つ一つの出会いと別れが、この広大な人生のグラフに独特の印を残していきます。 + 人生の旅路において、私たちはそれぞれ一つひとつのノードのようなものであり、無数の見えない辺によって結ばれています。 + + 出会いと別れのたびに、この巨大なネットワークグラフの中に固有の足跡が刻まれます。 ## 章の内容 diff --git a/ja/docs/chapter_graph/summary.md b/ja/docs/chapter_graph/summary.md index 2db94fe2e..3c1dc1b6f 100644 --- a/ja/docs/chapter_graph/summary.md +++ b/ja/docs/chapter_graph/summary.md @@ -4,32 +4,32 @@ comments: true # 9.4   まとめ -### 1.   重要な復習 +### 1.   重要なポイントの振り返り -- グラフは頂点と辺で構成されます。頂点の集合と辺の集合として記述できます。 -- 線形関係(連結リストなど)や階層関係(木など)と比較して、ネットワーク関係(グラフ)はより大きな柔軟性を提供し、より複雑になります。 -- 有向グラフでは、辺に方向があります。連結グラフでは、任意の頂点から他の任意の頂点に到達できます。重み付きグラフでは、各辺に関連する重み変数があります。 -- 隣接行列は、行列(2次元配列)を使用してグラフを表現する方法です。行と列は頂点を表します。行列要素の値は、2つの頂点間に辺があるかどうかを示し、辺がある場合は$1$、ない場合は$0$を使用します。隣接行列は辺の追加、削除、チェックなどの操作に非常に効率的ですが、より多くのスペースが必要です。 -- 隣接リストは、連結リストの集合を使用してグラフを表現するもう一つの一般的な方法です。グラフ内の各頂点には、その隣接するすべての頂点を含むリストがあります。$i$番目のリストは頂点$i$を表します。隣接リストは隣接行列と比較してより少ないスペースを使用します。ただし、辺を見つけるためにリストを走査する必要があるため、時間効率は低くなります。 -- 隣接リストの連結リストが十分に長くなったとき、ルックアップ効率を向上させるために赤黒木やハッシュテーブルに変換できます。 -- アルゴリズム設計の観点から、隣接行列は「空間と時間のトレードオフ」の概念を反映し、隣接リストは「時間と空間のトレードオフ」を反映します。 -- グラフは、ソーシャルネットワークや地下鉄路線など、さまざまな現実世界のシステムをモデル化するために使用できます。 -- 木はグラフの特別なケースであり、木の走査もグラフ走査の特別なケースです。 -- グラフの幅優先走査は、近くから遠くへと層ごとに拡張する探索方法で、通常キューを使用します。 -- グラフの深さ優先走査は、それ以上のパスがない場合にバックトラックする前に、まず終端に到達することを優先する探索方法です。しばしば再帰を使用して実装されます。 +- グラフは頂点と辺から構成され、一組の頂点と一組の辺からなる集合として表せます。 +- 線形関係(連結リスト)や分治関係(木)と比べて、ネットワーク関係(グラフ)は自由度が高く、そのぶん複雑です。 +- 有向グラフの辺は方向性を持ち、連結グラフでは任意の頂点に到達でき、重み付きグラフの各辺は重み変数を含みます。 +- 隣接行列は行列を用いてグラフを表し、各行(列)が 1 つの頂点を表し、行列要素が辺を表します。$1$ または $0$ を用いて、2 つの頂点の間に辺があるかないかを示します。隣接行列は追加・削除・検索・更新の操作効率が高い一方で、より多くの空間を消費します。 +- 隣接リストは複数の連結リストを使ってグラフを表し、第 $i$ 個の連結リストが頂点 $i$ に対応し、その頂点に隣接するすべての頂点を格納します。隣接リストは隣接行列よりも省スペースですが、辺を探すために連結リストを走査する必要があるため、時間効率は低くなります。 +- 隣接リスト内の連結リストが長くなりすぎた場合は、赤黒木やハッシュテーブルに変換することで、検索効率を高められます。 +- アルゴリズムの考え方という観点では、隣接行列は「空間を時間と引き換えにする」ことを体現し、隣接リストは「時間を空間と引き換えにする」ことを体現します。 +- グラフは、ソーシャルネットワークや地下鉄路線など、さまざまな現実のシステムをモデル化するために使えます。 +- 木はグラフの特殊な一例であり、木の走査もグラフ走査の特殊な一例です。 +- グラフの幅優先探索は、近いところから遠いところへ、層ごとに広がっていく探索方法であり、通常はキューを使って実装します。 +- グラフの深さ優先探索は、まず行けるところまで進み、進めなくなったらバックトラックする探索方法であり、通常は再帰に基づいて実装します。 ### 2.   Q & A -**Q**: パスは頂点のシーケンスとして定義されますか、それとも辺のシーケンスとして定義されますか? +**Q**:経路の定義は頂点列ですか、それとも辺列ですか? -グラフ理論では、グラフ内のパスは頂点のシーケンスを結ぶ有限または無限の辺のシーケンスです。 +Wikipedia では言語版ごとに定義が一致していません。英語版では「経路は辺の列」であり、中国語版では「経路は頂点の列」です。以下は英語版の原文です:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices. -この文書では、パスは頂点のシーケンスではなく、辺のシーケンスと考えられます。これは、2つの頂点を結ぶ複数の辺がある可能性があり、その場合各辺がパスに対応するためです。 +本書では、経路を頂点列ではなく辺列とみなします。これは、2 つの頂点の間に複数の辺が存在する可能性があり、その場合は各辺がそれぞれ 1 本の経路に対応するためです。 -**Q**: 非連結グラフでは、走査できない点がありますか? +**Q**:非連結グラフには到達できない頂点がありますか? -非連結グラフでは、特定の点から到達できない頂点が少なくとも1つあります。非連結グラフを走査するには、グラフのすべての連結成分を走査するために複数の開始点を設定する必要があります。 +非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。非連結グラフ全体を走査するには、グラフ内のすべての連結成分をたどれるように複数の始点を設定する必要があります。 -**Q**: 隣接リストで、「その頂点に接続されたすべての頂点」の順序は重要ですか? +**Q**:隣接リストにおいて、「その頂点に接続されたすべての頂点」の順序に決まりはありますか? -任意の順序で構いません。ただし、実際のアプリケーションでは、頂点が追加された順序や頂点値の順序など、特定のルールに従ってそれらをソートする必要がある場合があります。これにより、特定の極値を持つ頂点を素早く見つけることができます。 +順序は任意でかまいません。ただし実際の応用では、頂点を追加した順序や頂点値の大小順など、特定の規則に従って並べ替える必要がある場合があります。そうすることで、「ある種の極値を持つ」頂点をすばやく見つけやすくなります。 diff --git a/ja/docs/chapter_greedy/fractional_knapsack_problem.md b/ja/docs/chapter_greedy/fractional_knapsack_problem.md index 27d909940..87f571afe 100644 --- a/ja/docs/chapter_greedy/fractional_knapsack_problem.md +++ b/ja/docs/chapter_greedy/fractional_knapsack_problem.md @@ -6,30 +6,30 @@ comments: true !!! question - $n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重量は $wgt[i-1]$ で値は $val[i-1]$ です。容量が $cap$ のナップサックがあります。各アイテムは1回のみ選択できますが、**アイテムの一部を選択することができ、その値は選択された重量の割合に基づいて計算されます**。限られた容量の下でナップサック内のアイテムの最大値は何ですか?例を下の図に示します。 + $n$ 個の品物が与えられ、第 $i$ 個の品物の重さは $wgt[i-1]$、価値は $val[i-1]$ であり、容量が $cap$ のナップサックがある。各品物は 1 回だけ選択できるが、**品物の一部を選ぶこともでき、価値は選択した重量の割合に応じて計算される**。容量制限の下でナップサック内の品物の最大価値を求めよ。例を以下に示す。 ![分数ナップサック問題の例データ](fractional_knapsack_problem.assets/fractional_knapsack_example.png){ class="animation-figure" }

図 15-3   分数ナップサック問題の例データ

-分数ナップサック問題は全体的に0-1ナップサック問題と非常に似ており、現在のアイテム $i$ と容量 $c$ を含み、ナップサックの限られた容量内で値を最大化することを目的としています。 +分数ナップサック問題は 0-1 ナップサック問題と全体として非常によく似ており、状態には現在の品物 $i$ と容量 $c$ が含まれ、目標は容量制限下での最大価値を求めることである。 -違いは、この問題ではアイテムの一部のみを選択できることです。下の図に示すように、**アイテムを任意に分割し、重量の割合に基づいて対応する値を計算できます**。 +異なる点は、本問では品物の一部だけを選べることである。以下に示すように、**品物は任意に分割でき、対応する価値は重量の割合に応じて計算される**。 -1. アイテム $i$ について、その単位重量あたりの値は $val[i-1] / wgt[i-1]$ で、単位値と呼ばれます。 -2. 重量 $w$ のアイテム $i$ の一部をナップサックに入れるとすると、ナップサックに追加される値は $w \times val[i-1] / wgt[i-1]$ です。 +1. 品物 $i$ について、単位重量あたりの価値は $val[i-1] / wgt[i-1]$ であり、これを単位価値と呼ぶ。 +2. 品物 $i$ の一部を重さ $w$ だけ入れると、ナップサックに増える価値は $w \times val[i-1] / wgt[i-1]$ となる。 -![アイテムの単位重量あたりの値](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" } +![品物の単位重量あたりの価値](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" } -

図 15-4   アイテムの単位重量あたりの値

+

図 15-4   品物の単位重量あたりの価値

### 1.   貪欲戦略の決定 -ナップサック内のアイテムの総値を最大化することは、**本質的に単位重量あたりの値を最大化することを意味します**。これから、下の図に示す貪欲戦略を導出できます。 +ナップサック内の品物の総価値を最大化することは、**本質的には単位重量あたりの品物価値を最大化すること**である。そこから、以下に示す貪欲戦略を導ける。 -1. アイテムを単位値の高い順から低い順にソートします。 -2. すべてのアイテムを反復し、**各ラウンドで最も高い単位値を持つアイテムを貪欲に選択**します。 -3. ナップサックの残り容量が不十分な場合、現在のアイテムの一部を使用してナップサックを満たします。 +1. 品物を単位価値の高い順にソートする。 +2. すべての品物を走査し、**各回で単位価値が最も高い品物を貪欲に選択する**。 +3. 残りのナップサック容量が足りない場合は、現在の品物の一部を使ってナップサックを満たす。 ![分数ナップサック問題の貪欲戦略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png){ class="animation-figure" } @@ -37,35 +37,35 @@ comments: true ### 2.   コード実装 -アイテムを単位値でソートするために `Item` クラスを作成しました。ナップサックが満たされるまでループして貪欲な選択を行い、その後終了して解を返します: +品物を単位価値でソートできるように、`Item` クラスを定義する。貪欲選択を繰り返し、ナップサックが満杯になったら終了して解を返す。 === "Python" ```python title="fractional_knapsack.py" class Item: - """アイテム""" + """品物""" def __init__(self, w: int, v: int): - self.w = w # アイテムの重量 - self.v = v # アイテムの価値 + self.w = w # 品物の重さ + self.v = v # 品物の価値 def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int: """分数ナップサック:貪欲法""" - # アイテムリストを作成、2 つの属性を含む:重量、価値 + # 重さと価値の 2 属性を持つ品物リストを作成 items = [Item(w, v) for w, v in zip(wgt, val)] - # 単位価値 item.v / item.w で高い順にソート + # 単位価値 item.v / item.w の高い順にソートする items.sort(key=lambda item: item.v / item.w, reverse=True) - # 貪欲選択をループ + # 貪欲選択を繰り返す res = 0 for item in items: if item.w <= cap: - # 残り容量が十分な場合、アイテム全体をナップサックに入れる + # 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる res += item.v cap -= item.w else: - # 残り容量が不十分な場合、アイテムの一部をナップサックに入れる + # 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる res += (item.v / item.w) * cap - # 残り容量がなくなったため、ループを中断 + # 残り容量がないため、ループを抜ける break return res ``` @@ -73,11 +73,11 @@ comments: true === "C++" ```cpp title="fractional_knapsack.cpp" - /* アイテム */ + /* 品物 */ class Item { public: - int w; // アイテムの重量 - int v; // アイテムの価値 + int w; // 品物の重さ + int v; // 品物の価値 Item(int w, int v) : w(w), v(v) { } @@ -85,24 +85,24 @@ comments: true /* 分数ナップサック:貪欲法 */ double fractionalKnapsack(vector &wgt, vector &val, int cap) { - // アイテムリストを作成、2つの属性を含む:重量、価値 + // 重さと価値の 2 属性を持つ品物リストを作成 vector items; for (int i = 0; i < wgt.size(); i++) { items.push_back(Item(wgt[i], val[i])); } - // 単位価値 item.v / item.w で高い順にソート + // 単位価値 item.v / item.w の高い順にソートする sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; }); - // 貪欲選択をループ + // 貪欲選択を繰り返す double res = 0; for (auto &item : items) { if (item.w <= cap) { - // 残り容量が十分な場合、アイテム全体をナップサックに入れる + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる res += item.v; cap -= item.w; } else { - // 残り容量が不十分な場合、アイテムの一部をナップサックに入れる + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる res += (double)item.v / item.w * cap; - // 残り容量がなくなったため、ループを中断 + // 残り容量がないため、ループを抜ける break; } } @@ -113,10 +113,10 @@ comments: true === "Java" ```java title="fractional_knapsack.java" - /* アイテム */ + /* 品物 */ class Item { - int w; // アイテムの重量 - int v; // アイテムの価値 + int w; // 品物の重さ + int v; // 品物の価値 public Item(int w, int v) { this.w = w; @@ -126,24 +126,24 @@ comments: true /* 分数ナップサック:貪欲法 */ double fractionalKnapsack(int[] wgt, int[] val, int cap) { - // アイテムリストを作成、2つの属性を含む:重量、価値 + // 重さと価値の 2 属性を持つ品物リストを作成 Item[] items = new Item[wgt.length]; for (int i = 0; i < wgt.length; i++) { items[i] = new Item(wgt[i], val[i]); } - // 単位価値 item.v / item.w で高い順にソート + // 単位価値 item.v / item.w の高い順にソートする Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w))); - // 貪欲選択をループ + // 貪欲選択を繰り返す double res = 0; for (Item item : items) { if (item.w <= cap) { - // 残り容量が十分な場合、アイテム全体をナップサックに入れる + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる res += item.v; cap -= item.w; } else { - // 残り容量が不十分な場合、アイテムの一部をナップサックに入れる + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる res += (double) item.v / item.w * cap; - // 残り容量がなくなったため、ループを中断 + // 残り容量がないため、ループを抜ける break; } } @@ -154,96 +154,403 @@ comments: true === "C#" ```csharp title="fractional_knapsack.cs" - [class]{Item}-[func]{} + /* 品物 */ + class Item(int w, int v) { + public int w = w; // 品物の重さ + public int v = v; // 品物の価値 + } - [class]{fractional_knapsack}-[func]{FractionalKnapsack} + /* 分数ナップサック:貪欲法 */ + double FractionalKnapsack(int[] wgt, int[] val, int cap) { + // 重さと価値の 2 属性を持つ品物リストを作成 + Item[] items = new Item[wgt.Length]; + for (int i = 0; i < wgt.Length; i++) { + items[i] = new Item(wgt[i], val[i]); + } + // 単位価値 item.v / item.w の高い順にソートする + Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w)); + // 貪欲選択を繰り返す + double res = 0; + foreach (Item item in items) { + if (item.w <= cap) { + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる + res += item.v; + cap -= item.w; + } else { + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる + res += (double)item.v / item.w * cap; + // 残り容量がないため、ループを抜ける + break; + } + } + return res; + } ``` === "Go" ```go title="fractional_knapsack.go" - [class]{Item}-[func]{} + /* 品物 */ + type Item struct { + w int // 品物の重さ + v int // 品物の価値 + } - [class]{}-[func]{fractionalKnapsack} + /* 分数ナップサック:貪欲法 */ + func fractionalKnapsack(wgt []int, val []int, cap int) float64 { + // 重さと価値の 2 属性を持つ品物リストを作成 + items := make([]Item, len(wgt)) + for i := 0; i < len(wgt); i++ { + items[i] = Item{wgt[i], val[i]} + } + // 単位価値 item.v / item.w の高い順にソートする + sort.Slice(items, func(i, j int) bool { + return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w) + }) + // 貪欲選択を繰り返す + res := 0.0 + for _, item := range items { + if item.w <= cap { + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる + res += float64(item.v) + cap -= item.w + } else { + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる + res += float64(item.v) / float64(item.w) * float64(cap) + // 残り容量がないため、ループを抜ける + break + } + } + return res + } ``` === "Swift" ```swift title="fractional_knapsack.swift" - [class]{Item}-[func]{} + /* 品物 */ + class Item { + var w: Int // 品物の重さ + var v: Int // 品物の価値 - [class]{}-[func]{fractionalKnapsack} + init(w: Int, v: Int) { + self.w = w + self.v = v + } + } + + /* 分数ナップサック:貪欲法 */ + func fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double { + // 重さと価値の 2 属性を持つ品物リストを作成 + var items = zip(wgt, val).map { Item(w: $0, v: $1) } + // 単位価値 item.v / item.w の高い順にソートする + items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) } + // 貪欲選択を繰り返す + var res = 0.0 + var cap = cap + for item in items { + if item.w <= cap { + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる + res += Double(item.v) + cap -= item.w + } else { + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる + res += Double(item.v) / Double(item.w) * Double(cap) + // 残り容量がないため、ループを抜ける + break + } + } + return res + } ``` === "JS" ```javascript title="fractional_knapsack.js" - [class]{Item}-[func]{} + /* 品物 */ + class Item { + constructor(w, v) { + this.w = w; // 品物の重さ + this.v = v; // 品物の価値 + } + } - [class]{}-[func]{fractionalKnapsack} + /* 分数ナップサック:貪欲法 */ + function fractionalKnapsack(wgt, val, cap) { + // 重さと価値の 2 属性を持つ品物リストを作成 + const items = wgt.map((w, i) => new Item(w, val[i])); + // 単位価値 item.v / item.w の高い順にソートする + items.sort((a, b) => b.v / b.w - a.v / a.w); + // 貪欲選択を繰り返す + let res = 0; + for (const item of items) { + if (item.w <= cap) { + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる + res += item.v; + cap -= item.w; + } else { + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる + res += (item.v / item.w) * cap; + // 残り容量がないため、ループを抜ける + break; + } + } + return res; + } ``` === "TS" ```typescript title="fractional_knapsack.ts" - [class]{Item}-[func]{} + /* 品物 */ + class Item { + w: number; // 品物の重さ + v: number; // 品物の価値 - [class]{}-[func]{fractionalKnapsack} + constructor(w: number, v: number) { + this.w = w; + this.v = v; + } + } + + /* 分数ナップサック:貪欲法 */ + function fractionalKnapsack(wgt: number[], val: number[], cap: number): number { + // 重さと価値の 2 属性を持つ品物リストを作成 + const items: Item[] = wgt.map((w, i) => new Item(w, val[i])); + // 単位価値 item.v / item.w の高い順にソートする + items.sort((a, b) => b.v / b.w - a.v / a.w); + // 貪欲選択を繰り返す + let res = 0; + for (const item of items) { + if (item.w <= cap) { + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる + res += item.v; + cap -= item.w; + } else { + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる + res += (item.v / item.w) * cap; + // 残り容量がないため、ループを抜ける + break; + } + } + return res; + } ``` === "Dart" ```dart title="fractional_knapsack.dart" - [class]{Item}-[func]{} + /* 品物 */ + class Item { + int w; // 品物の重さ + int v; // 品物の価値 - [class]{}-[func]{fractionalKnapsack} + Item(this.w, this.v); + } + + /* 分数ナップサック:貪欲法 */ + double fractionalKnapsack(List wgt, List val, int cap) { + // 重さと価値の 2 属性を持つ品物リストを作成 + List items = List.generate(wgt.length, (i) => Item(wgt[i], val[i])); + // 単位価値 item.v / item.w の高い順にソートする + items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w)); + // 貪欲選択を繰り返す + double res = 0; + for (Item item in items) { + if (item.w <= cap) { + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる + res += item.v; + cap -= item.w; + } else { + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる + res += item.v / item.w * cap; + // 残り容量がないため、ループを抜ける + break; + } + } + return res; + } ``` === "Rust" ```rust title="fractional_knapsack.rs" - [class]{Item}-[func]{} + /* 品物 */ + struct Item { + w: i32, // 品物の重さ + v: i32, // 品物の価値 + } - [class]{}-[func]{fractional_knapsack} + impl Item { + fn new(w: i32, v: i32) -> Self { + Self { w, v } + } + } + + /* 分数ナップサック:貪欲法 */ + fn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 { + // 重さと価値の 2 属性を持つ品物リストを作成 + let mut items = wgt + .iter() + .zip(val.iter()) + .map(|(&w, &v)| Item::new(w, v)) + .collect::>(); + // 単位価値 item.v / item.w の高い順にソートする + items.sort_by(|a, b| { + (b.v as f64 / b.w as f64) + .partial_cmp(&(a.v as f64 / a.w as f64)) + .unwrap() + }); + // 貪欲選択を繰り返す + let mut res = 0.0; + for item in &items { + if item.w <= cap { + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる + res += item.v as f64; + cap -= item.w; + } else { + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる + res += item.v as f64 / item.w as f64 * cap as f64; + // 残り容量がないため、ループを抜ける + break; + } + } + res + } ``` === "C" ```c title="fractional_knapsack.c" - [class]{Item}-[func]{} + /* 品物 */ + typedef struct { + int w; // 品物の重さ + int v; // 品物の価値 + } Item; - [class]{}-[func]{fractionalKnapsack} + /* 分数ナップサック:貪欲法 */ + float fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) { + // 重さと価値の 2 属性を持つ品物リストを作成 + Item *items = malloc(sizeof(Item) * itemCount); + for (int i = 0; i < itemCount; i++) { + items[i] = (Item){.w = wgt[i], .v = val[i]}; + } + // 単位価値 item.v / item.w の高い順にソートする + qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity); + // 貪欲選択を繰り返す + float res = 0.0; + for (int i = 0; i < itemCount; i++) { + if (items[i].w <= cap) { + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる + res += items[i].v; + cap -= items[i].w; + } else { + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる + res += (float)cap / items[i].w * items[i].v; + cap = 0; + break; + } + } + free(items); + return res; + } ``` === "Kotlin" ```kotlin title="fractional_knapsack.kt" - [class]{Item}-[func]{} + /* 品物 */ + class Item( + val w: Int, // 品物 + val v: Int // 品物の価値 + ) - [class]{}-[func]{fractionalKnapsack} + /* 分数ナップサック:貪欲法 */ + fun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double { + // 重さと価値の 2 属性を持つ品物リストを作成 + var cap = c + val items = arrayOfNulls(wgt.size) + for (i in wgt.indices) { + items[i] = Item(wgt[i], _val[i]) + } + // 単位価値 item.v / item.w の高い順にソートする + items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) } + // 貪欲選択を繰り返す + var res = 0.0 + for (item in items) { + if (item!!.w <= cap) { + // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる + res += item.v + cap -= item.w + } else { + // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる + res += item.v.toDouble() / item.w * cap + // 残り容量がないため、ループを抜ける + break + } + } + return res + } ``` === "Ruby" ```ruby title="fractional_knapsack.rb" - [class]{Item}-[func]{} + ### アイテム ### + class Item + attr_accessor :w # 品物の重さ + attr_accessor :v # 品物の価値 - [class]{}-[func]{fractional_knapsack} + def initialize(w, v) + @w = w + @v = v + end + end + + ### 分数ナップサック:貪欲法 ### + def fractional_knapsack(wgt, val, cap) + # 重さと価値の 2 属性を持つ品物リストを作成する + items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) } + # 単位価値 item.v / item.w の高い順にソートする + items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) } + # 貪欲選択を繰り返す + res = 0 + for item in items + if item.w <= cap + # 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる + res += item.v + cap -= item.w + else + # 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる + res += (item.v.to_f / item.w) * cap + # 残り容量がないため、ループを抜ける + break + end + end + res + end ``` -ソート以外に、最悪の場合、アイテムのリスト全体を走査する必要があるため、**時間計算量は $O(n)$** です。ここで $n$ はアイテムの数です。 +??? pythontutor "コードの可視化" -`Item` オブジェクトリストが初期化されるため、**空間計算量は $O(n)$** です。 +
+ + +組み込みのソートアルゴリズムの時間計算量は通常 $O(\log n)$、空間計算量は通常 $O(\log n)$ または $O(n)$ であり、具体的な値はプログラミング言語の実装に依存する。 + +ソートを除けば、最悪の場合は品物リスト全体を走査する必要があるため、**時間計算量は $O(n)$** であり、ここで $n$ は品物数である。 + +`Item` オブジェクトのリストを初期化しているため、**空間計算量は $O(n)$** である。 ### 3.   正しさの証明 -背理法を使用します。アイテム $x$ が最高の単位値を持ち、あるアルゴリズムが最大値 `res` を生成するが、解にアイテム $x$ が含まれていないと仮定します。 +背理法を用いる。品物 $x$ が単位価値最大の品物であり、あるアルゴリズムで得られた最大価値を `res` とするが、その解には品物 $x$ が含まれていないと仮定する。 -今、ナップサックから任意のアイテムの単位重量を取り除き、アイテム $x$ の単位重量で置き換えます。アイテム $x$ の単位値が最高であるため、置き換え後の総値は確実に `res` より大きくなります。**これは `res` が最適解であるという仮定と矛盾し、最適解には必ずアイテム $x$ が含まれることを証明します**。 +ここでナップサックから単位重量の任意の品物を取り出し、単位重量の品物 $x$ に置き換える。品物 $x$ の単位価値が最大であるため、置き換え後の総価値は必ず `res` より大きくなる。**これは `res` が最適解であることに矛盾し、最適解には必ず品物 $x$ が含まれなければならないことを示す**。 -この解の他のアイテムについても、上記の矛盾を構築できます。全体的に、**単位値がより大きいアイテムは常により良い選択**であり、貪欲戦略が効果的であることを証明します。 +この解に含まれる他の品物についても、同様の矛盾を構成できる。要するに、**単位価値がより大きい品物は常により良い選択である**。これは貪欲戦略が有効であることを示している。 -下の図に示すように、アイテムの重量と単位値をそれぞれ二次元チャートの横軸と縦軸と見なすと、分数ナップサック問題は「限られた横軸範囲内で囲まれる最大面積を求める」ことに変換できます。この類推は、幾何学的観点から貪欲戦略の効果を理解するのに役立ちます。 +以下に示すように、品物の重さと品物の単位価値をそれぞれ二次元グラフの横軸と縦軸とみなすと、分数ナップサック問題は「有限な横軸区間で囲まれる最大面積を求める問題」に変換できる。この類比は、幾何学的な観点から貪欲戦略の有効性を理解する助けになる。 ![分数ナップサック問題の幾何学的表現](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png){ class="animation-figure" } diff --git a/ja/docs/chapter_greedy/greedy_algorithm.md b/ja/docs/chapter_greedy/greedy_algorithm.md index 5cbbd7b15..6dc257ab0 100644 --- a/ja/docs/chapter_greedy/greedy_algorithm.md +++ b/ja/docs/chapter_greedy/greedy_algorithm.md @@ -2,68 +2,68 @@ comments: true --- -# 15.1   貪欲アルゴリズム +# 15.1   貪欲法 -貪欲アルゴリズムは最適化問題を解決するための一般的なアルゴリズムで、基本的に問題の各意思決定段階で最も良い選択をすること、つまり局所的に最適な決定を貪欲に行い、グローバルに最適な解を見つけることを望みます。貪欲アルゴリズムは簡潔で効率的であり、多くの実用的な問題で広く使用されています。 +貪欲法(greedy algorithm)は、最適化問題を解くための一般的なアルゴリズムです。その基本的な考え方は、問題の各意思決定段階において、その時点で最善に見える選択を行い、すなわち貪欲に局所最適な決定を下すことで、大域最適解を得ようとするものです。貪欲法は簡潔で効率的であり、多くの実際の問題で広く用いられています。 -貪欲アルゴリズムと動的プログラミングは、どちらも最適化問題を解決するためによく使用されます。両者は最適部分構造の性質に依存するなど、いくつかの類似点を共有していますが、動作方法が異なります。 +貪欲法と動的計画法は、どちらも最適化問題を解く際によく用いられます。両者には、最適部分構造に依存するなどの共通点がありますが、その動作原理は異なります。 -- 動的プログラミングは現在の決定段階ですべての以前の決定を考慮し、過去の部分問題の解を使用して現在の部分問題の解を構築します。 -- 貪欲アルゴリズムは過去の決定を考慮せず、代わりに貪欲な選択を続け、問題が解決されるまで問題の範囲を継続的に狭めます。 +- 動的計画法は、前の段階までのすべての決定に基づいて現在の決定を考え、過去の部分問題の解を用いて現在の部分問題の解を構築します。 +- 貪欲法は過去の決定を考慮せず、ひたすら前に進みながら貪欲な選択を行い、問題の範囲を縮小し続けて、最終的に問題を解決します。 -まず、「完全ナップサック問題」の章で紹介された「コイン交換」の例を通じて貪欲アルゴリズムの動作原理を理解しましょう。すでによく知っていると思います。 +まずは例題「コイン両替」を通して、貪欲法の仕組みを理解しましょう。この問題はすでに「完全ナップサック問題」の節で紹介しているので、見覚えがあるはずです。 !!! question - $n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ で、目標金額は $amt$ です。各種類のコインは無制限に利用可能で、目標金額を構成するのに必要な最小コイン数は何ですか?目標金額を構成できない場合は $-1$ を返してください。 + $n$ 種類の硬貨が与えられ、$i$ 番目の硬貨の額面は $coins[i - 1]$ 、目標金額は $amt$ です。各硬貨は何度でも選べるとき、目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は $-1$ を返します。 -この問題で採用される貪欲戦略を下の図に示します。目標金額が与えられたとき、**それに最も近く、それを超えないコインを貪欲に選択**し、目標金額が満たされるまでこのステップを繰り返します。 +この問題で採用する貪欲戦略は下図のとおりです。目標金額が与えられたら、**それを超えず、かつ最も近い硬貨を貪欲に選択し**、この手順を目標金額を作り切るまで繰り返します。 -![コイン交換の貪欲戦略](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" } +![コイン両替の貪欲戦略](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" } -

図 15-1   コイン交換の貪欲戦略

+

図 15-1   コイン両替の貪欲戦略

-実装コードは以下の通りです: +実装コードは次のとおりです。 === "Python" ```python title="coin_change_greedy.py" def coin_change_greedy(coins: list[int], amt: int) -> int: - """硬貨交換:貪欲法""" - # coins リストがソートされていると仮定 + """コイン交換:貪欲法""" + # coins リストはソート済みと仮定する i = len(coins) - 1 count = 0 - # 残り金額がなくなるまで貪欲選択をループ + # 残額がなくなるまで貪欲選択を繰り返す while amt > 0: - # 残り金額に最も近く、それより小さい硬貨を見つける + # 残額以下で最も近い硬貨を見つける while i > 0 and coins[i] > amt: i -= 1 - # coins[i] を選択 + # coins[i] を選択する amt -= coins[i] count += 1 - # 実行可能な解が見つからない場合、-1 を返す + # 実行可能な解が見つからなければ -1 を返す return count if amt == 0 else -1 ``` === "C++" ```cpp title="coin_change_greedy.cpp" - /* 硬貨両替:貪欲法 */ + /* コイン交換:貪欲法 */ int coinChangeGreedy(vector &coins, int amt) { - // 硬貨リストが順序付けされていると仮定 + // coins リストはソート済みと仮定する int i = coins.size() - 1; int count = 0; - // 残り金額がなくなるまで貪欲選択をループ + // 残額がなくなるまで貪欲選択を繰り返す while (amt > 0) { - // 残り金額に近く、それ以下の最小硬貨を見つける + // 残額以下で最も近い硬貨を見つける while (i > 0 && coins[i] > amt) { i--; } - // coins[i] を選択 + // coins[i] を選択する amt -= coins[i]; count++; } - // 実行可能な解が見つからない場合、-1 を返す + // 実行可能な解が見つからなければ -1 を返す return amt == 0 ? count : -1; } ``` @@ -71,22 +71,22 @@ comments: true === "Java" ```java title="coin_change_greedy.java" - /* 硬貨両替:貪欲法 */ + /* コイン交換:貪欲法 */ int coinChangeGreedy(int[] coins, int amt) { - // 硬貨リストが順序付けされていると仮定 + // coins リストはソート済みと仮定する int i = coins.length - 1; int count = 0; - // 残り金額がなくなるまで貪欲選択をループ + // 残額がなくなるまで貪欲選択を繰り返す while (amt > 0) { - // 残り金額に近く、それ以下の最小硬貨を見つける + // 残額以下で最も近い硬貨を見つける while (i > 0 && coins[i] > amt) { i--; } - // coins[i] を選択 + // coins[i] を選択する amt -= coins[i]; count++; } - // 実行可能な解が見つからない場合、-1 を返す + // 実行可能な解が見つからなければ -1 を返す return amt == 0 ? count : -1; } ``` @@ -94,131 +94,315 @@ comments: true === "C#" ```csharp title="coin_change_greedy.cs" - [class]{coin_change_greedy}-[func]{CoinChangeGreedy} + /* コイン交換:貪欲法 */ + int CoinChangeGreedy(int[] coins, int amt) { + // coins リストはソート済みと仮定する + int i = coins.Length - 1; + int count = 0; + // 残額がなくなるまで貪欲選択を繰り返す + while (amt > 0) { + // 残額以下で最も近い硬貨を見つける + while (i > 0 && coins[i] > amt) { + i--; + } + // coins[i] を選択する + amt -= coins[i]; + count++; + } + // 実行可能な解が見つからなければ -1 を返す + return amt == 0 ? count : -1; + } ``` === "Go" ```go title="coin_change_greedy.go" - [class]{}-[func]{coinChangeGreedy} + /* コイン交換:貪欲法 */ + func coinChangeGreedy(coins []int, amt int) int { + // coins リストはソート済みと仮定する + i := len(coins) - 1 + count := 0 + // 残額がなくなるまで貪欲選択を繰り返す + for amt > 0 { + // 残額以下で最も近い硬貨を見つける + for i > 0 && coins[i] > amt { + i-- + } + // coins[i] を選択する + amt -= coins[i] + count++ + } + // 実行可能な解が見つからなければ -1 を返す + if amt != 0 { + return -1 + } + return count + } ``` === "Swift" ```swift title="coin_change_greedy.swift" - [class]{}-[func]{coinChangeGreedy} + /* コイン交換:貪欲法 */ + func coinChangeGreedy(coins: [Int], amt: Int) -> Int { + // coins リストはソート済みと仮定する + var i = coins.count - 1 + var count = 0 + var amt = amt + // 残額がなくなるまで貪欲選択を繰り返す + while amt > 0 { + // 残額以下で最も近い硬貨を見つける + while i > 0 && coins[i] > amt { + i -= 1 + } + // coins[i] を選択する + amt -= coins[i] + count += 1 + } + // 実行可能な解が見つからなければ -1 を返す + return amt == 0 ? count : -1 + } ``` === "JS" ```javascript title="coin_change_greedy.js" - [class]{}-[func]{coinChangeGreedy} + /* コイン交換:貪欲法 */ + function coinChangeGreedy(coins, amt) { + // coins 配列はソート済みと仮定する + let i = coins.length - 1; + let count = 0; + // 残額がなくなるまで貪欲選択を繰り返す + while (amt > 0) { + // 残額以下で最も近い硬貨を見つける + while (i > 0 && coins[i] > amt) { + i--; + } + // coins[i] を選択する + amt -= coins[i]; + count++; + } + // 実行可能な解が見つからなければ -1 を返す + return amt === 0 ? count : -1; + } ``` === "TS" ```typescript title="coin_change_greedy.ts" - [class]{}-[func]{coinChangeGreedy} + /* コイン交換:貪欲法 */ + function coinChangeGreedy(coins: number[], amt: number): number { + // coins 配列はソート済みと仮定する + let i = coins.length - 1; + let count = 0; + // 残額がなくなるまで貪欲選択を繰り返す + while (amt > 0) { + // 残額以下で最も近い硬貨を見つける + while (i > 0 && coins[i] > amt) { + i--; + } + // coins[i] を選択する + amt -= coins[i]; + count++; + } + // 実行可能な解が見つからなければ -1 を返す + return amt === 0 ? count : -1; + } ``` === "Dart" ```dart title="coin_change_greedy.dart" - [class]{}-[func]{coinChangeGreedy} + /* コイン交換:貪欲法 */ + int coinChangeGreedy(List coins, int amt) { + // coins リストはソート済みと仮定する + int i = coins.length - 1; + int count = 0; + // 残額がなくなるまで貪欲選択を繰り返す + while (amt > 0) { + // 残額以下で最も近い硬貨を見つける + while (i > 0 && coins[i] > amt) { + i--; + } + // coins[i] を選択する + amt -= coins[i]; + count++; + } + // 実行可能な解が見つからなければ -1 を返す + return amt == 0 ? count : -1; + } ``` === "Rust" ```rust title="coin_change_greedy.rs" - [class]{}-[func]{coin_change_greedy} + /* コイン交換:貪欲法 */ + fn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 { + // coins リストはソート済みと仮定する + let mut i = coins.len() - 1; + let mut count = 0; + // 残額がなくなるまで貪欲選択を繰り返す + while amt > 0 { + // 残額以下で最も近い硬貨を見つける + while i > 0 && coins[i] > amt { + i -= 1; + } + // coins[i] を選択する + amt -= coins[i]; + count += 1; + } + // 実行可能な解が見つからなければ -1 を返す + if amt == 0 { + count + } else { + -1 + } + } ``` === "C" ```c title="coin_change_greedy.c" - [class]{}-[func]{coinChangeGreedy} + /* コイン交換:貪欲法 */ + int coinChangeGreedy(int *coins, int size, int amt) { + // coins リストはソート済みと仮定する + int i = size - 1; + int count = 0; + // 残額がなくなるまで貪欲選択を繰り返す + while (amt > 0) { + // 残額以下で最も近い硬貨を見つける + while (i > 0 && coins[i] > amt) { + i--; + } + // coins[i] を選択する + amt -= coins[i]; + count++; + } + // 実行可能な解が見つからなければ -1 を返す + return amt == 0 ? count : -1; + } ``` === "Kotlin" ```kotlin title="coin_change_greedy.kt" - [class]{}-[func]{coinChangeGreedy} + /* コイン交換:貪欲法 */ + fun coinChangeGreedy(coins: IntArray, amt: Int): Int { + // coins リストはソート済みと仮定する + var am = amt + var i = coins.size - 1 + var count = 0 + // 残額がなくなるまで貪欲選択を繰り返す + while (am > 0) { + // 残額以下で最も近い硬貨を見つける + while (i > 0 && coins[i] > am) { + i-- + } + // coins[i] を選択する + am -= coins[i] + count++ + } + // 実行可能な解が見つからなければ -1 を返す + return if (am == 0) count else -1 + } ``` === "Ruby" ```ruby title="coin_change_greedy.rb" - [class]{}-[func]{coin_change_greedy} + ### コイン両替:貪欲法 ### + def coin_change_greedy(coins, amt) + # coins リストはソート済みと仮定する + i = coins.length - 1 + count = 0 + # 残額がなくなるまで貪欲選択を繰り返す + while amt > 0 + # 残額以下で最も近い硬貨を見つける + while i > 0 && coins[i] > amt + i -= 1 + end + # coins[i] を選択する + amt -= coins[i] + count += 1 + end + # 実行可能な解が見つからなければ `-1` を返す + amt == 0 ? count : -1 + end ``` -感嘆するかもしれません:なんて簡潔なんだ!貪欲アルゴリズムは約10行のコードでコイン交換問題を解決します。 +??? pythontutor "コードの可視化" -## 15.1.1   貪欲アルゴリズムの利点と制限 +
+ -**貪欲アルゴリズムは直接的で実装が簡単であるだけでなく、通常非常に効率的でもあります**。上記のコードで、最小のコイン額面を $\min(coins)$ とすると、貪欲な選択ループは最大 $amt / \min(coins)$ 回実行され、時間計算量は $O(amt / \min(coins))$ になります。これは動的プログラミング解法の時間計算量 $O(n \times amt)$ よりも一桁小さいです。 +思わずこう言いたくなるかもしれません。So clean!貪欲法はわずか十行ほどのコードでコイン両替問題を解いてしまいます。 -しかし、**一部のコイン額面の組み合わせでは、貪欲アルゴリズムは最適解を見つけることができません**。下の図は2つの例を示しています。 +## 15.1.1   貪欲法の利点と限界 -- **正の例 $coins = [1, 5, 10, 20, 50, 100]$**:このコインの組み合わせでは、任意の $amt$ に対して、貪欲アルゴリズムは最適解を見つけることができます。 -- **負の例 $coins = [1, 20, 50]$**:$amt = 60$ とすると、貪欲アルゴリズムは組み合わせ $50 + 1 \times 10$ しか見つけられず、合計11枚のコインですが、動的プログラミングは最適解 $20 + 20 + 20$ を見つけることができ、3枚のコインのみが必要です。 -- **負の例 $coins = [1, 49, 50]$**:$amt = 98$ とすると、貪欲アルゴリズムは組み合わせ $50 + 1 \times 48$ しか見つけられず、合計49枚のコインですが、動的プログラミングは最適解 $49 + 49$ を見つけることができ、2枚のコインのみが必要です。 +**貪欲法は操作が直接的で実装が簡単なだけでなく、通常は効率も高い**です。上のコードでは、硬貨の最小額面を $\min(coins)$ とすると、貪欲選択のループ回数は高々 $amt / \min(coins)$ 回であり、時間計算量は $O(amt / \min(coins))$ です。これは動的計画法による解法の時間計算量 $O(n \times amt)$ より 1 桁小さいオーダーです。 -![貪欲アルゴリズムが最適解を見つけられない例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" } +しかし、**硬貨の額面の組み合わせによっては、貪欲法では最適解を見つけられません**。下図に 2 つの例を示します。 -

図 15-2   貪欲アルゴリズムが最適解を見つけられない例

+- **正例 $coins = [1, 5, 10, 20, 50, 100]$**:この硬貨の組み合わせでは、任意の $amt$ に対して貪欲法で最適解を見つけられます。 +- **反例 $coins = [1, 20, 50]$**:$amt = 60$ とすると、貪欲法では $50 + 1 \times 10$ という両替しか見つからず、硬貨は合計 $11$ 枚になります。しかし動的計画法なら最適解 $20 + 20 + 20$ を見つけられ、必要なのはわずか $3$ 枚です。 +- **反例 $coins = [1, 49, 50]$**:$amt = 98$ とすると、貪欲法では $50 + 1 \times 48$ という両替しか見つからず、硬貨は合計 $49$ 枚になります。しかし動的計画法なら最適解 $49 + 49$ を見つけられ、必要なのはわずか $2$ 枚です。 -これは、コイン交換問題において、貪欲アルゴリズムがグローバルに最適な解を見つけることを保証できず、非常に悪い解を見つける可能性があることを意味します。動的プログラミングの方が適しています。 +![貪欲法では最適解を見つけられない例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" } -一般的に、貪欲アルゴリズムの適用性は2つのカテゴリに分類されます。 +

図 15-2   貪欲法では最適解を見つけられない例

-1. **最適解を見つけることが保証される**:これらの場合、貪欲アルゴリズムはしばしば最良の選択で、バックトラッキングや動的プログラミングよりも効率的である傾向があります。 -2. **準最適解を見つけることができる**:貪欲アルゴリズムはここでも適用可能です。多くの複雑な問題では、グローバル最適解を見つけることは非常に困難であり、高効率の準最適解を見つけることも非常に価値があります。 +つまり、コイン両替問題に対して、貪欲法は大域最適解を保証できず、非常に悪い解を見つけてしまうこともあります。この問題は動的計画法で解くほうが適しています。 -## 15.1.2   貪欲アルゴリズムの特徴 +一般に、貪欲法が適用できる状況は次の 2 つに分けられます。 -それでは、どのような問題が貪欲アルゴリズムで解決するのに適しているのでしょうか?言い換えれば、どのような条件下で貪欲アルゴリズムは最適解を見つけることを保証できるのでしょうか? +1. **最適解を保証できる場合**:この場合、貪欲法はしばしば最良の選択です。多くの場合、バックトラッキングや動的計画法より効率的だからです。 +2. **近似最適解を見つけられる場合**:この場合も貪欲法は有効です。多くの複雑な問題では、大域最適解を求めること自体が非常に難しく、より高い効率で準最適解を得られるだけでも十分価値があります。 -動的プログラミングと比較して、貪欲アルゴリズムはより厳しい使用条件を持ち、主に問題の2つの性質に焦点を当てています。 +## 15.1.2   貪欲法の特性 -- **貪欲選択性**:局所的に最適な選択が常にグローバルに最適な解に導くことができる場合のみ、貪欲アルゴリズムは最適解を得ることを保証できます。 -- **最適部分構造**:元の問題の最適解はその部分問題の最適解を含みます。 +では、どのような問題が貪欲法に適しているのでしょうか。言い換えると、貪欲法はどのような場合に最適解を保証できるのでしょうか。 -最適部分構造は「動的プログラミング」の章ですでに紹介されているため、ここではこれ以上議論しません。一部の問題には明らかな最適部分構造がありませんが、それでも貪欲アルゴリズムを使用して解決できることに注意することが重要です。 +動的計画法と比べると、貪欲法の適用条件はより厳しく、主に次の 2 つの性質に注目します。 -主に貪欲選択性を決定する方法を探索します。その記述は単純に見えますが、**実際には、多くの問題の貪欲選択性を証明することは容易ではありません**。 +- **貪欲選択性**:局所最適な選択が常に大域最適解につながる場合にのみ、貪欲法は最適解を保証できます。 +- **最適部分構造**:元の問題の最適解が、部分問題の最適解を含むことです。 -例えば、コイン交換問題では、貪欲選択性を反証するために反例を簡単に挙げることができますが、それを証明することははるかに困難です。**コインの組み合わせが貪欲アルゴリズムを使用して解決できるためには、どのような条件を満たす必要があるか**と尋ねられた場合、厳密な数学的証明を提供することが困難であるため、しばしば直感や例に頼って曖昧な答えを提供しなければなりません。 +最適部分構造については「動的計画法」の節ですでに紹介したので、ここでは繰り返しません。なお、問題によっては最適部分構造が明確でなくても、貪欲法で解ける場合があります。 + +ここでは主に、貪欲選択性をどのように判定するかを考えます。説明だけを見ると単純そうですが、**実際には多くの問題で、貪欲選択性を証明するのは容易ではありません**。 + +たとえばコイン両替問題では、反例を挙げて貪欲選択性が成り立たないことを示すのは簡単ですが、成り立つことを証明するのは難しいです。もし、**どのような条件を満たす硬貨の組み合わせなら貪欲法で解けるのか**と問われると、直感や例示に頼った曖昧な答えしか出せず、厳密な数学的証明を与えるのは困難です。 !!! quote - ある論文では、コインの組み合わせが任意の金額に対して貪欲アルゴリズムを使用して最適解を見つけることができるかどうかを判定するための時間計算量 $O(n^3)$ のアルゴリズムが提示されています。 + ある論文では、ある硬貨の組み合わせについて、任意の金額に対する最適解を貪欲法で求められるかどうかを判定する、時間計算量 $O(n^3)$ のアルゴリズムが示されています。 Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234. -## 15.1.3   貪欲アルゴリズムによる問題解決のステップ +## 15.1.3   貪欲法の問題解決手順 -貪欲問題の問題解決プロセスは、一般的に以下の3つのステップに分けることができます。 +貪欲法による問題解決の流れは、おおむね次の 3 段階に分けられます。 -1. **問題分析**:問題の特徴を整理し理解する。状態定義、最適化目標、制約などを含みます。このステップはバックトラッキングや動的プログラミングでも関与します。 -2. **貪欲戦略の決定**:各ステップで貪欲な選択をする方法を決定する。この戦略は各ステップで問題の規模を縮小し、最終的に問題全体を解決できます。 -3. **正確性の証明**:通常、問題が貪欲選択性と最適部分構造の両方を持つことを証明する必要があります。このステップには、帰納法や背理法などの数学的証明が必要な場合があります。 +1. **問題分析**:状態の定義、最適化目標、制約条件などを整理し、問題の性質を理解します。この段階はバックトラッキングや動的計画法でも共通して現れます。 +2. **貪欲戦略の決定**:各ステップでどのように貪欲選択を行うかを定めます。この戦略により各ステップで問題規模を縮小し、最終的に問題全体を解決します。 +3. **正しさの証明**:通常は、その問題が貪欲選択性と最適部分構造を持つことを示す必要があります。この段階では、帰納法や背理法などの数学的証明が必要になることがあります。 -貪欲戦略の決定は問題解決の核心ステップですが、実装は容易ではない場合があります。主な理由は以下の通りです。 +貪欲戦略を定めることは問題解決の核心ですが、実際には簡単ではないことも多く、主な理由は次のとおりです。 -- **異なる問題間で貪欲戦略は大きく異なる**。多くの問題では、貪欲戦略はかなり直接的で、一般的な思考と試行を通じて思いつくことができます。しかし、一部の複雑な問題では、貪欲戦略は非常に見つけにくく、これは個人の問題解決経験とアルゴリズム能力の真のテストです。 -- **一部の貪欲戦略は非常に誤解を招く**。自信を持って貪欲戦略を設計し、コードを書いてテストに提出したとき、一部のテストケースが通らない可能性が高いです。これは設計された貪欲戦略が「部分的に正しい」だけであるためで、上記のコイン交換の例で説明した通りです。 +- **問題ごとに貪欲戦略の差が大きい**。多くの問題では貪欲戦略は比較的わかりやすく、おおまかな考察や試行だけで見つけられます。しかし複雑な問題では、貪欲戦略が非常に見えにくいことがあり、その場合は解法経験やアルゴリズム力が大きく問われます。 +- **一見もっともらしい貪欲戦略もある**。自信を持って貪欲戦略を設計し、コードを書いて提出しても、一部のテストケースを通過できないことがあります。これは、その貪欲戦略が「部分的にしか正しくない」ためであり、先ほどのコイン両替は典型例です。 -正確性を確保するために、貪欲戦略に対して厳密な数学的証明を提供すべきで、**通常は背理法や数学的帰納法を含みます**。 +正しさを保証するためには、貪欲戦略に対して厳密な数学的証明を行うべきであり、**通常は背理法や数学的帰納法が必要になります**。 -しかし、正確性を証明することは容易な作業ではない場合があります。途方に暮れた場合、通常はテストケースに基づいてコードをデバッグし、貪欲戦略を段階的に修正し検証することを選択します。 +しかし、正しさの証明もまた簡単とは限りません。手がかりがない場合には、テストケースを使ってコードをデバッグしながら、貪欲戦略を少しずつ修正して検証していくことがよくあります。 -## 15.1.4   貪欲アルゴリズムで解決される典型的な問題 +## 15.1.4   貪欲法の典型問題 -貪欲アルゴリズムは、貪欲選択と最適部分構造の性質を満たす最適化問題によく適用されます。以下は典型的な貪欲アルゴリズム問題のいくつかです。 +貪欲法は、貪欲選択性と最適部分構造を満たす最適化問題によく用いられます。以下に典型的な貪欲法の問題をいくつか挙げます。 -- **コイン交換問題**:一部のコインの組み合わせでは、貪欲アルゴリズムは常に最適解を提供します。 -- **区間スケジューリング問題**:いくつかのタスクがあり、それぞれが一定期間にわたって行われるとします。目標はできるだけ多くのタスクを完了することです。常に最も早く終了するタスクを選択すると、貪欲アルゴリズムは最適解を達成できます。 -- **分数ナップサック問題**:アイテムのセットと運搬容量が与えられ、目標は総重量が運搬容量を超えず、総価値が最大化されるようなアイテムのセットを選択することです。常に最高の価値対重量比(価値/重量)のアイテムを選択すると、貪欲アルゴリズムは一部のケースで最適解を達成できます。 -- **株式取引問題**:株価の履歴のセットが与えられ、複数回の取引を行うことができますが、すでに株式を所有している場合は売却後でないと再度購入できません。目標は最大利益を達成することです。 -- **ハフマン符号化**:ハフマン符号化は無損失データ圧縮に使用される貪欲アルゴリズムです。ハフマン木を構築することにより、常に最低頻度の2つのノードを統合し、最小重み付きパス長(符号化長)のハフマン木を生成します。 -- **ダイクストラのアルゴリズム**:これは与えられたソース頂点から他のすべての頂点への最短経路問題を解決するための貪欲アルゴリズムです。 +- **硬貨のお釣り問題**:ある種の硬貨の組み合わせでは、貪欲法で常に最適解が得られます。 +- **区間スケジューリング問題**:いくつかのタスクがあり、それぞれがある時間区間で実行されるとします。できるだけ多くのタスクを完了することが目標で、毎回終了時刻が最も早いタスクを選ぶなら、貪欲法で最適解を得られます。 +- **分数ナップサック問題**:一群の品物と積載容量が与えられたとき、総重量が容量を超えず、かつ総価値が最大になるように品物を選ぶ問題です。毎回、価値対重量比(価値 / 重量)が最も高い品物を選ぶなら、ある条件下で貪欲法は最適解を得られます。 +- **株式売買問題**:株価の履歴が与えられ、複数回の売買が可能ですが、すでに株を保有している場合は売却前に再度購入することはできません。目標は最大利益を得ることです。 +- **ハフマン符号化**:ハフマン符号化は、可逆データ圧縮に用いられる貪欲法です。ハフマン木を構築する際、毎回出現頻度が最も低い 2 つのノードを選んで併合すると、最終的に得られるハフマン木の重み付きパス長(符号長)は最小になります。 +- **Dijkstra アルゴリズム**:与えられた始点から他の各頂点への最短経路問題を解く貪欲法です。 diff --git a/ja/docs/chapter_greedy/index.md b/ja/docs/chapter_greedy/index.md index ff4870a88..71d4ff400 100644 --- a/ja/docs/chapter_greedy/index.md +++ b/ja/docs/chapter_greedy/index.md @@ -9,13 +9,13 @@ icon: material/head-heart-outline !!! abstract - ひまわりは太陽の方を向き、常に自分にとって最大の成長を求めます。 + ヒマワリは太陽に向かって回り、自らが最も大きく成長できる可能性を常に追い求める。 - 貪欲な戦略は、一連の単純な選択を通じて、段階的に最良の答えへと導きます。 + 貪欲戦略は、一回ごとの単純な選択を通じて、徐々に最適な答えへと導く。 ## 章の内容 -- [15.1   貪欲アルゴリズム](greedy_algorithm.md) +- [15.1   貪欲法](greedy_algorithm.md) - [15.2   分数ナップサック問題](fractional_knapsack_problem.md) - [15.3   最大容量問題](max_capacity_problem.md) - [15.4   最大積分割問題](max_product_cutting_problem.md) diff --git a/ja/docs/chapter_greedy/max_capacity_problem.md b/ja/docs/chapter_greedy/max_capacity_problem.md index deeb265ab..a3f067991 100644 --- a/ja/docs/chapter_greedy/max_capacity_problem.md +++ b/ja/docs/chapter_greedy/max_capacity_problem.md @@ -6,59 +6,59 @@ comments: true !!! question - 配列 $ht$ を入力します。各要素は垂直仕切りの高さを表します。配列内の任意の2つの仕切りと、それらの間のスペースによってコンテナを形成できます。 + 配列 $ht$ が与えられ、各要素は垂直な仕切り板の高さを表します。配列内の任意の 2 枚の仕切り板と、その間の空間で容器を構成できます。 + + 容器の容量は高さと幅の積(面積)に等しく、高さは短い方の仕切り板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。 + + 配列から 2 枚の仕切り板を選び、構成される容器の容量が最大となるようにしてください。最大容量を返します。例を以下の図に示します。 - コンテナの容量は高さと幅の積(面積)で、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。 +![最大容量問題のサンプルデータ](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" } - コンテナの容量を最大化する2つの仕切りを配列から選択し、この最大容量を返してください。例を下の図に示します。 +

図 15-7   最大容量問題のサンプルデータ

-![最大容量問題の例データ](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" } +容器は任意の 2 枚の仕切り板で囲まれるため、**本問の状態は 2 枚の仕切り板のインデックスで表され、$[i, j]$ と記します**。 -

図 15-7   最大容量問題の例データ

- -コンテナは任意の2つの仕切りによって形成されるため、**この問題の状態は2つの仕切りのインデックスで表現され、$[i, j]$ と表記されます**。 - -問題の記述によれば、容量は高さと幅の積に等しく、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。容量 $cap[i, j]$ の式は: +問題の条件より、容量は高さと幅の積に等しく、高さは短い板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。容量を $cap[i, j]$ とすると、計算式は次のようになります。 $$ cap[i, j] = \min(ht[i], ht[j]) \times (j - i) $$ -配列の長さを $n$ と仮定すると、2つの仕切りの組み合わせ数(状態の総数)は $C_n^2 = \frac{n(n - 1)}{2}$ です。最も直接的なアプローチは**すべての可能な状態を列挙する**ことで、時間計算量は $O(n^2)$ になります。 +配列の長さを $n$ とすると、2 枚の仕切り板の組合せ数(状態総数)は $C_n^2 = \frac{n(n - 1)}{2}$ 個です。最も直接的には、**すべての状態を総当たりできます**。これにより最大容量を求められ、時間計算量は $O(n^2)$ です。 ### 1.   貪欲戦略の決定 -この問題にはより効率的な解法があります。下の図に示すように、インデックス $i < j$ かつ高さ $ht[i] < ht[j]$ の状態 $[i, j]$ を選択します。つまり、$i$ は短い仕切り、$j$ は高い仕切りです。 +この問題にはさらに効率的な解法があります。以下の図のように、状態 $[i, j]$ を 1 つ選び、インデックスが $i < j$ かつ高さが $ht[i] < ht[j]$ を満たすとします。つまり、$i$ が短い板、$j$ が長い板です。 ![初期状態](max_capacity_problem.assets/max_capacity_initial_state.png){ class="animation-figure" }

図 15-8   初期状態

-下の図に示すように、**高い仕切り $j$ を短い仕切り $i$ に近づけて移動すると、容量は確実に減少します**。 +以下の図のように、**このとき長い板 $j$ を短い板 $i$ に近づけると、容量は必ず小さくなります**。 -これは、高い仕切り $j$ を移動すると、幅 $j-i$ が確実に減少するためです。高さは短い仕切りによって決定されるため、高さは同じまま($i$ が短い仕切りのまま)か減少(移動した $j$ が短い仕切りになる)しかありません。 +これは、長い板 $j$ を動かした後は幅 $j-i$ が必ず小さくなるためです。また、高さは短い板で決まるので、高さは変わらない( $i$ が依然として短い板)か、小さくなる(移動後の $j$ が短い板になる)ことしかありません。 -![高い仕切りを内側に移動した後の状態](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" } +![長い板を内側へ動かした後の状態](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" } -

図 15-9   高い仕切りを内側に移動した後の状態

+

図 15-9   長い板を内側へ動かした後の状態

-逆に、**短い仕切り $i$ を内側に移動することによってのみ容量を増加させることが可能です**。幅は確実に減少しますが、**高さが増加する可能性があります**(移動した短い仕切り $i$ が高くなる場合)。例えば、下の図では、短い仕切りを移動した後に面積が増加しています。 +逆に考えると、**短い板 $i$ を内側へ縮めた場合にのみ、容量が大きくなる可能性があります**。幅は必ず小さくなりますが、**高さは大きくなる可能性がある**からです(移動後の短い板 $i$ がより長くなる可能性があります)。たとえば次の図では、短い板を動かした後に面積が大きくなっています。 -![短い仕切りを内側に移動した後の状態](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" } +![短い板を内側へ動かした後の状態](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" } -

図 15-10   短い仕切りを内側に移動した後の状態

+

図 15-10   短い板を内側へ動かした後の状態

-これにより、この問題の貪欲戦略が導かれます:コンテナの両端に2つのポインタを初期化し、各ラウンドで短い仕切りに対応するポインタを内側に移動し、2つのポインタが出会うまで続けます。 +以上から、本問の貪欲戦略を導けます。2 本のポインタを初期化して容器の両端に置き、各ラウンドで短い板に対応するポインタを内側へ縮め、2 本のポインタが出会うまで続けます。 -下の図は貪欲戦略の実行を示しています。 +以下の図は、貪欲戦略の実行過程を示しています。 -1. 最初に、ポインタ $i$ と $j$ が配列の両端に配置されます。 +1. 初期状態では、ポインタ $i$ と $j$ は配列の両端にあります。 2. 現在の状態の容量 $cap[i, j]$ を計算し、最大容量を更新します。 -3. 仕切り $i$ と $j$ の高さを比較し、短い仕切りを1ステップ内側に移動します。 -4. $i$ と $j$ が出会うまでステップ `2.` と `3.` を繰り返します。 +3. 板 $i$ と板 $j$ の高さを比較し、短い板を内側へ 1 マス移動します。 +4. `2.` と `3.` を繰り返し実行し、$i$ と $j$ が出会ったら終了します。 === "<1>" - ![最大容量問題の貪欲プロセス](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" } + ![最大容量問題の貪欲な過程](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" } === "<2>" ![max_capacity_greedy_step2](max_capacity_problem.assets/max_capacity_greedy_step2.png){ class="animation-figure" } @@ -84,29 +84,29 @@ $$ === "<9>" ![max_capacity_greedy_step9](max_capacity_problem.assets/max_capacity_greedy_step9.png){ class="animation-figure" } -

図 15-11   最大容量問題の貪欲プロセス

+

図 15-11   最大容量問題の貪欲な過程

-### 2.   実装 +### 2.   コード実装 -コードは最大 $n$ 回ループするため、**時間計算量は $O(n)$** です。 +コードのループ回数は最大でも $n$ 回であるため、**時間計算量は $O(n)$** です。 -変数 $i$、$j$、$res$ は一定量の追加スペースを使用するため、**空間計算量は $O(1)$** です。 +変数 $i$、$j$、$res$ が使う追加領域は定数サイズなので、**空間計算量は $O(1)$** です。 === "Python" ```python title="max_capacity.py" def max_capacity(ht: list[int]) -> int: """最大容量:貪欲法""" - # i、j を初期化、配列の両端で分割させる + # i, j を初期化し、それぞれ配列の両端に置く i, j = 0, len(ht) - 1 - # 初期最大容量は 0 + # 初期の最大容量は 0 res = 0 - # 2 つの板が出会うまで貪欲選択をループ + # 2 枚の板が出会うまで貪欲選択を繰り返す while i < j: - # 最大容量を更新 + # 最大容量を更新する cap = min(ht[i], ht[j]) * (j - i) res = max(res, cap) - # 短い板を内側に移動 + # 短い方を内側へ動かす if ht[i] < ht[j]: i += 1 else: @@ -119,16 +119,16 @@ $$ ```cpp title="max_capacity.cpp" /* 最大容量:貪欲法 */ int maxCapacity(vector &ht) { - // i、j を初期化し、配列の両端で分割させる + // i, j を初期化し、それぞれ配列の両端に置く int i = 0, j = ht.size() - 1; - // 初期最大容量は 0 + // 初期の最大容量は 0 int res = 0; - // 2つの板が出会うまで貪欲選択をループ + // 2 枚の板が出会うまで貪欲選択を繰り返す while (i < j) { - // 最大容量を更新 + // 最大容量を更新する int cap = min(ht[i], ht[j]) * (j - i); res = max(res, cap); - // より短い板を内側に移動 + // 短い方を内側へ動かす if (ht[i] < ht[j]) { i++; } else { @@ -144,16 +144,16 @@ $$ ```java title="max_capacity.java" /* 最大容量:貪欲法 */ int maxCapacity(int[] ht) { - // i、j を初期化し、配列の両端で分割させる + // i, j を初期化し、それぞれ配列の両端に置く int i = 0, j = ht.length - 1; - // 初期最大容量は 0 + // 初期の最大容量は 0 int res = 0; - // 2つの板が出会うまで貪欲選択をループ + // 2 枚の板が出会うまで貪欲選択を繰り返す while (i < j) { - // 最大容量を更新 + // 最大容量を更新する int cap = Math.min(ht[i], ht[j]) * (j - i); res = Math.max(res, cap); - // より短い板を内側に移動 + // 短い方を内側へ動かす if (ht[i] < ht[j]) { i++; } else { @@ -167,77 +167,279 @@ $$ === "C#" ```csharp title="max_capacity.cs" - [class]{max_capacity}-[func]{MaxCapacity} + /* 最大容量:貪欲法 */ + int MaxCapacity(int[] ht) { + // i, j を初期化し、それぞれ配列の両端に置く + int i = 0, j = ht.Length - 1; + // 初期の最大容量は 0 + int res = 0; + // 2 枚の板が出会うまで貪欲選択を繰り返す + while (i < j) { + // 最大容量を更新する + int cap = Math.Min(ht[i], ht[j]) * (j - i); + res = Math.Max(res, cap); + // 短い方を内側へ動かす + if (ht[i] < ht[j]) { + i++; + } else { + j--; + } + } + return res; + } ``` === "Go" ```go title="max_capacity.go" - [class]{}-[func]{maxCapacity} + /* 最大容量:貪欲法 */ + func maxCapacity(ht []int) int { + // i, j を初期化し、それぞれ配列の両端に置く + i, j := 0, len(ht)-1 + // 初期の最大容量は 0 + res := 0 + // 2 枚の板が出会うまで貪欲選択を繰り返す + for i < j { + // 最大容量を更新する + capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i) + res = int(math.Max(float64(res), float64(capacity))) + // 短い方を内側へ動かす + if ht[i] < ht[j] { + i++ + } else { + j-- + } + } + return res + } ``` === "Swift" ```swift title="max_capacity.swift" - [class]{}-[func]{maxCapacity} + /* 最大容量:貪欲法 */ + func maxCapacity(ht: [Int]) -> Int { + // i, j を初期化し、それぞれ配列の両端に置く + var i = ht.startIndex, j = ht.endIndex - 1 + // 初期の最大容量は 0 + var res = 0 + // 2 枚の板が出会うまで貪欲選択を繰り返す + while i < j { + // 最大容量を更新する + let cap = min(ht[i], ht[j]) * (j - i) + res = max(res, cap) + // 短い方を内側へ動かす + if ht[i] < ht[j] { + i += 1 + } else { + j -= 1 + } + } + return res + } ``` === "JS" ```javascript title="max_capacity.js" - [class]{}-[func]{maxCapacity} + /* 最大容量:貪欲法 */ + function maxCapacity(ht) { + // i, j を初期化し、それぞれ配列の両端に置く + let i = 0, + j = ht.length - 1; + // 初期の最大容量は 0 + let res = 0; + // 2 枚の板が出会うまで貪欲選択を繰り返す + while (i < j) { + // 最大容量を更新する + const cap = Math.min(ht[i], ht[j]) * (j - i); + res = Math.max(res, cap); + // 短い方を内側へ動かす + if (ht[i] < ht[j]) { + i += 1; + } else { + j -= 1; + } + } + return res; + } ``` === "TS" ```typescript title="max_capacity.ts" - [class]{}-[func]{maxCapacity} + /* 最大容量:貪欲法 */ + function maxCapacity(ht: number[]): number { + // i, j を初期化し、それぞれ配列の両端に置く + let i = 0, + j = ht.length - 1; + // 初期の最大容量は 0 + let res = 0; + // 2 枚の板が出会うまで貪欲選択を繰り返す + while (i < j) { + // 最大容量を更新する + const cap: number = Math.min(ht[i], ht[j]) * (j - i); + res = Math.max(res, cap); + // 短い方を内側へ動かす + if (ht[i] < ht[j]) { + i += 1; + } else { + j -= 1; + } + } + return res; + } ``` === "Dart" ```dart title="max_capacity.dart" - [class]{}-[func]{maxCapacity} + /* 最大容量:貪欲法 */ + int maxCapacity(List ht) { + // i, j を初期化し、それぞれ配列の両端に置く + int i = 0, j = ht.length - 1; + // 初期の最大容量は 0 + int res = 0; + // 2 枚の板が出会うまで貪欲選択を繰り返す + while (i < j) { + // 最大容量を更新する + int cap = min(ht[i], ht[j]) * (j - i); + res = max(res, cap); + // 短い方を内側へ動かす + if (ht[i] < ht[j]) { + i++; + } else { + j--; + } + } + return res; + } ``` === "Rust" ```rust title="max_capacity.rs" - [class]{}-[func]{max_capacity} + /* 最大容量:貪欲法 */ + fn max_capacity(ht: &[i32]) -> i32 { + // i, j を初期化し、それぞれ配列の両端に置く + let mut i = 0; + let mut j = ht.len() - 1; + // 初期の最大容量は 0 + let mut res = 0; + // 2 枚の板が出会うまで貪欲選択を繰り返す + while i < j { + // 最大容量を更新する + let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32; + res = std::cmp::max(res, cap); + // 短い方を内側へ動かす + if ht[i] < ht[j] { + i += 1; + } else { + j -= 1; + } + } + res + } ``` === "C" ```c title="max_capacity.c" - [class]{}-[func]{maxCapacity} + /* 最大容量:貪欲法 */ + int maxCapacity(int ht[], int htLength) { + // i, j を初期化し、それぞれ配列の両端に置く + int i = 0; + int j = htLength - 1; + // 初期の最大容量は 0 + int res = 0; + // 2 枚の板が出会うまで貪欲選択を繰り返す + while (i < j) { + // 最大容量を更新する + int capacity = myMin(ht[i], ht[j]) * (j - i); + res = myMax(res, capacity); + // 短い方を内側へ動かす + if (ht[i] < ht[j]) { + i++; + } else { + j--; + } + } + return res; + } ``` === "Kotlin" ```kotlin title="max_capacity.kt" - [class]{}-[func]{maxCapacity} + /* 最大容量:貪欲法 */ + fun maxCapacity(ht: IntArray): Int { + // i, j を初期化し、それぞれ配列の両端に置く + var i = 0 + var j = ht.size - 1 + // 初期の最大容量は 0 + var res = 0 + // 2 枚の板が出会うまで貪欲選択を繰り返す + while (i < j) { + // 最大容量を更新する + val cap = min(ht[i], ht[j]) * (j - i) + res = max(res, cap) + // 短い方を内側へ動かす + if (ht[i] < ht[j]) { + i++ + } else { + j-- + } + } + return res + } ``` === "Ruby" ```ruby title="max_capacity.rb" - [class]{}-[func]{max_capacity} + ### 最大容量:貪欲法 ### + def max_capacity(ht) + # i, j を初期化し、それぞれ配列の両端に置く + i, j = 0, ht.length - 1 + # 初期の最大容量は 0 + res = 0 + + # 2 枚の板が出会うまで貪欲選択を繰り返す + while i < j + # 最大容量を更新する + cap = [ht[i], ht[j]].min * (j - i) + res = [res, cap].max + # 短い方を内側へ動かす + if ht[i] < ht[j] + i += 1 + else + j -= 1 + end + end + + res + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 3.   正しさの証明 -貪欲法が列挙よりも高速である理由は、各ラウンドの貪欲選択が一部の状態を「スキップ」するからです。 +貪欲法が総当たりより速いのは、各ラウンドの貪欲な選択がいくつかの状態を「スキップ」するためです。 -例えば、$i$ が短い仕切りで $j$ が高い仕切りである状態 $cap[i, j]$ の下で、短い仕切り $i$ を貪欲に1ステップ内側に移動すると、下の図に示す「スキップされた」状態につながります。**これは、これらの状態の容量を後で検証できないことを意味します**。 +たとえば状態 $cap[i, j]$ において、$i$ が短い板、$j$ が長い板だとします。貪欲に短い板 $i$ を内側へ 1 マス動かすと、次の図に示す状態が「スキップ」されます。**これは、その後それらの状態の容量を検証できないことを意味します**。 $$ cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1] $$ -![短い仕切りの移動によってスキップされる状態](max_capacity_problem.assets/max_capacity_skipped_states.png){ class="animation-figure" } +![短い板の移動によってスキップされる状態](max_capacity_problem.assets/max_capacity_skipped_states.png){ class="animation-figure" } -

図 15-12   短い仕切りの移動によってスキップされる状態

+

図 15-12   短い板の移動によってスキップされる状態

-観察すると、**これらのスキップされた状態は実際には高い仕切り $j$ が内側に移動したすべての状態**です。高い仕切りを内側に移動すると容量が確実に減少することをすでに証明しました。したがって、スキップされた状態は最適解である可能性がなく、**それらをスキップしても最適解を見逃すことはありません**。 +観察すると、**これらのスキップされた状態は、実際には長い板 $j$ を内側へ動かしたすべての状態そのものです**。前述のとおり、長い板を内側へ動かすと容量は必ず小さくなります。つまり、スキップされた状態はいずれも最適解にはなりえず、**それらを飛ばしても最適解を逃すことはありません**。 -分析により、短い仕切りを移動する操作は「安全」であり、貪欲戦略が効果的であることが示されます。 +以上の分析から、短い板を動かす操作は「安全」であり、貪欲戦略は有効であると分かります。 diff --git a/ja/docs/chapter_greedy/max_product_cutting_problem.md b/ja/docs/chapter_greedy/max_product_cutting_problem.md index 928e5766b..1201dfad6 100644 --- a/ja/docs/chapter_greedy/max_product_cutting_problem.md +++ b/ja/docs/chapter_greedy/max_product_cutting_problem.md @@ -2,33 +2,33 @@ comments: true --- -# 15.4   最大積切断問題 +# 15.4   最大積分割問題 !!! question - 正の整数 $n$ が与えられたとき、それを合計が $n$ になる少なくとも2つの正の整数に分割し、これらの整数の最大積を求めてください。下の図に示すとおりです。 + 正整数 $n$ が与えられたとき、それを少なくとも 2 つの正整数の和に分割し、分割後のすべての整数の積の最大値を求めよ。下図に示す。 -![最大積切断問題の定義](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" } +![最大積分割問題の定義](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" } -

図 15-13   最大積切断問題の定義

+

図 15-13   最大積分割問題の定義

-$n$ を $m$ 個の整数因子に分割すると仮定し、$i$ 番目の因子を $n_i$ と表記すると、 +仮に $n$ を $m$ 個の整数因子に分割し、そのうち第 $i$ 個の因子を $n_i$ と記すと、 $$ n = \sum_{i=1}^{m}n_i $$ -この問題の目標は、すべての整数因子の最大積を見つけることです。すなわち、 +本問題の目的は、すべての整数因子の積の最大値を求めることであり、すなわち $$ \max(\prod_{i=1}^{m}n_i) $$ -考慮すべき点:分割数 $m$ はどの程度大きくすべきか、各 $n_i$ は何であるべきか? +考えるべきことは、分割数 $m$ をいくつにすべきか、各 $n_i$ をいくつにすべきかである。 ### 1.   貪欲戦略の決定 -経験的に、2つの整数の積は多くの場合その和よりも大きくなります。$n$ から因子 $2$ を分割すると仮定すると、その積は $2(n-2)$ です。この積を $n$ と比較します: +経験的に、2 つの整数の積はその和より大きくなることが多い。$n$ から因子 $2$ を 1 つ切り出すと、それらの積は $2(n-2)$ となる。この積を $n$ と比較すると、 $$ \begin{aligned} @@ -38,82 +38,82 @@ n & \geq 4 \end{aligned} $$ -下の図に示すように、$n \geq 4$ のとき、$2$ を分割すると積が増加します。**これは4以上の整数を分割すべきであることを示しています**。 +下図のように、$n \geq 4$ のとき、$2$ を 1 つ切り出すと積は大きくなる。**これは、$4$ 以上の整数はすべて分割すべきことを意味する**。 -**貪欲戦略1**:分割スキームが $\geq 4$ の因子を含む場合、それらはさらに分割されるべきです。最終的な分割は因子 $1$、$2$、$3$ のみを含むべきです。 +**貪欲戦略一**:分割方法に $\geq 4$ の因子が含まれるなら、それはさらに分割すべきである。最終的な分割方法に現れる因子は $1$、$2$、$3$ の 3 種類だけである。 -![分割による積の増加](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" } +![分割により積が大きくなる](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" } -

図 15-14   分割による積の増加

+

図 15-14   分割により積が大きくなる

-次に、どの因子が最適かを考慮します。因子 $1$、$2$、$3$ の中で、明らかに $1$ が最悪です。$1 \times (n-1) < n$ が常に成り立つため、$1$ を分割すると実際に積が減少します。 +次に、どの因子が最適かを考える。$1$、$2$、$3$ の 3 つの因子のうち、明らかに $1$ が最も悪い。なぜなら $1 \times (n-1) < n$ は常に成り立ち、$1$ を切り出すとかえって積が小さくなるからである。 -下の図に示すように、$n = 6$ のとき、$3 \times 3 > 2 \times 2 \times 2$ です。**これは $3$ を分割する方が $2$ を分割するよりも良いことを意味します**。 +下図のように、$n = 6$ のとき、$3 \times 3 > 2 \times 2 \times 2$ が成り立つ。**これは、$2$ を切り出すより $3$ を切り出すほうが有利であることを意味する**。 -**貪欲戦略2**:分割スキームには最大で2つの $2$ があるべきです。3つの $2$ は常に2つの $3$ に置き換えてより高い積を得ることができるからです。 +**貪欲戦略二**:分割方法の中に存在してよい $2$ は高々 2 つである。なぜなら、3 つの $2$ は常に 2 つの $3$ に置き換えられ、より大きな積を得られるからである。 ![最適な分割因子](max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png){ class="animation-figure" }

図 15-15   最適な分割因子

-上記から、以下の貪欲戦略を導出できます。 +以上より、次の貪欲戦略が導かれる。 -1. 入力整数 $n$ について、余りが $0$、$1$、または $2$ になるまで因子 $3$ を継続的に分割します。 -2. 余りが $0$ の場合、$n$ が $3$ の倍数であることを意味するため、それ以上の行動は取りません。 -3. 余りが $2$ の場合、さらに分割を続けず、そのまま保持します。 -4. 余りが $1$ の場合、$2 \times 2 > 1 \times 3$ であるため、最後の $3$ を $2$ に置き換えるべきです。 +1. 整数 $n$ を入力し、余りが $0$、$1$、$2$ になるまで、そこから因子 $3$ を繰り返し切り出す。 +2. 余りが $0$ のとき、$n$ は $3$ の倍数であることを表すため、何も処理しない。 +3. 余りが $2$ のときは、それ以上分割せず、そのまま残す。 +4. 余りが $1$ のとき、$2 \times 2 > 1 \times 3$ であるため、最後の $3$ を $2$ に置き換えるべきである。 ### 2.   コード実装 -下の図に示すように、整数を分割するためにループを使用する必要はなく、床除算演算を使用して $3$ の数 $a$ を取得し、剰余演算を使用して余り $b$ を取得できます。したがって: +下図のように、ループで整数を分割する必要はなく、切り捨て除算によって $3$ の個数 $a$ を、剰余演算によって余り $b$ を得られる。このとき、 $$ -n = 3a + b +n = 3 a + b $$ -$n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積は $1 \times (n - 1)$ であることに注意してください。 +なお、$n \leq 3$ の境界ケースでは、必ず $1$ を 1 つ分割する必要があり、積は $1 \times (n - 1)$ となる。 === "Python" ```python title="max_product_cutting.py" def max_product_cutting(n: int) -> int: - """切断の最大積:貪欲法""" - # n <= 3 の場合、1 を切り出す必要がある + """最大切断積:貪欲法""" + # n <= 3 のときは、必ず 1 を切り出す if n <= 3: return 1 * (n - 1) - # 貪欲的に 3 を切り出す、a は 3 の個数、b は余り + # 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする a, b = n // 3, n % 3 if b == 1: - # 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換 + # 余りが 1 のときは、1 * 3 を 2 * 2 に変える return int(math.pow(3, a - 1)) * 2 * 2 if b == 2: - # 余りが 2 の場合、何もしない + # 余りが 2 のときは、そのままにする return int(math.pow(3, a)) * 2 - # 余りが 0 の場合、何もしない + # 余りが 0 のときは、そのままにする return int(math.pow(3, a)) ``` === "C++" ```cpp title="max_product_cutting.cpp" - /* 最大積切断:貪欲法 */ + /* 最大切断積:貪欲法 */ int maxProductCutting(int n) { - // n <= 3 の場合、1 を切り出す必要がある + // n <= 3 のときは、必ず 1 を切り出す if (n <= 3) { return 1 * (n - 1); } - // 貪欲に 3 を切り出す。a は 3 の個数、b は余り + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする int a = n / 3; int b = n % 3; if (b == 1) { - // 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換 + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える return (int)pow(3, a - 1) * 2 * 2; } if (b == 2) { - // 余りが 2 の場合、何もしない + // 余りが 2 のときは、そのままにする return (int)pow(3, a) * 2; } - // 余りが 0 の場合、何もしない + // 余りが 0 のときは、そのままにする return (int)pow(3, a); } ``` @@ -121,24 +121,24 @@ $n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積 === "Java" ```java title="max_product_cutting.java" - /* 最大積切断:貪欲法 */ + /* 最大切断積:貪欲法 */ int maxProductCutting(int n) { - // n <= 3 の場合、1 を切り出す必要がある + // n <= 3 のときは、必ず 1 を切り出す if (n <= 3) { return 1 * (n - 1); } - // 貪欲に 3 を切り出す。a は 3 の個数、b は余り + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする int a = n / 3; int b = n % 3; if (b == 1) { - // 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換 + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える return (int) Math.pow(3, a - 1) * 2 * 2; } if (b == 2) { - // 余りが 2 の場合、何もしない + // 余りが 2 のときは、そのままにする return (int) Math.pow(3, a) * 2; } - // 余りが 0 の場合、何もしない + // 余りが 0 のときは、そのままにする return (int) Math.pow(3, a); } ``` @@ -146,78 +146,266 @@ $n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積 === "C#" ```csharp title="max_product_cutting.cs" - [class]{max_product_cutting}-[func]{MaxProductCutting} + /* 最大切断積:貪欲法 */ + int MaxProductCutting(int n) { + // n <= 3 のときは、必ず 1 を切り出す + if (n <= 3) { + return 1 * (n - 1); + } + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする + int a = n / 3; + int b = n % 3; + if (b == 1) { + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える + return (int)Math.Pow(3, a - 1) * 2 * 2; + } + if (b == 2) { + // 余りが 2 のときは、そのままにする + return (int)Math.Pow(3, a) * 2; + } + // 余りが 0 のときは、そのままにする + return (int)Math.Pow(3, a); + } ``` === "Go" ```go title="max_product_cutting.go" - [class]{}-[func]{maxProductCutting} + /* 最大切断積:貪欲法 */ + func maxProductCutting(n int) int { + // n <= 3 のときは、必ず 1 を切り出す + if n <= 3 { + return 1 * (n - 1) + } + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする + a := n / 3 + b := n % 3 + if b == 1 { + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える + return int(math.Pow(3, float64(a-1))) * 2 * 2 + } + if b == 2 { + // 余りが 2 のときは、そのままにする + return int(math.Pow(3, float64(a))) * 2 + } + // 余りが 0 のときは、そのままにする + return int(math.Pow(3, float64(a))) + } ``` === "Swift" ```swift title="max_product_cutting.swift" - [class]{}-[func]{maxProductCutting} + /* 最大切断積:貪欲法 */ + func maxProductCutting(n: Int) -> Int { + // n <= 3 のときは、必ず 1 を切り出す + if n <= 3 { + return 1 * (n - 1) + } + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする + let a = n / 3 + let b = n % 3 + if b == 1 { + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える + return pow(3, a - 1) * 2 * 2 + } + if b == 2 { + // 余りが 2 のときは、そのままにする + return pow(3, a) * 2 + } + // 余りが 0 のときは、そのままにする + return pow(3, a) + } ``` === "JS" ```javascript title="max_product_cutting.js" - [class]{}-[func]{maxProductCutting} + /* 最大切断積:貪欲法 */ + function maxProductCutting(n) { + // n <= 3 のときは、必ず 1 を切り出す + if (n <= 3) { + return 1 * (n - 1); + } + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする + let a = Math.floor(n / 3); + let b = n % 3; + if (b === 1) { + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える + return Math.pow(3, a - 1) * 2 * 2; + } + if (b === 2) { + // 余りが 2 のときは、そのままにする + return Math.pow(3, a) * 2; + } + // 余りが 0 のときは、そのままにする + return Math.pow(3, a); + } ``` === "TS" ```typescript title="max_product_cutting.ts" - [class]{}-[func]{maxProductCutting} + /* 最大切断積:貪欲法 */ + function maxProductCutting(n: number): number { + // n <= 3 のときは、必ず 1 を切り出す + if (n <= 3) { + return 1 * (n - 1); + } + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする + let a: number = Math.floor(n / 3); + let b: number = n % 3; + if (b === 1) { + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える + return Math.pow(3, a - 1) * 2 * 2; + } + if (b === 2) { + // 余りが 2 のときは、そのままにする + return Math.pow(3, a) * 2; + } + // 余りが 0 のときは、そのままにする + return Math.pow(3, a); + } ``` === "Dart" ```dart title="max_product_cutting.dart" - [class]{}-[func]{maxProductCutting} + /* 最大切断積:貪欲法 */ + int maxProductCutting(int n) { + // n <= 3 のときは、必ず 1 を切り出す + if (n <= 3) { + return 1 * (n - 1); + } + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする + int a = n ~/ 3; + int b = n % 3; + if (b == 1) { + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える + return (pow(3, a - 1) * 2 * 2).toInt(); + } + if (b == 2) { + // 余りが 2 のときは、そのままにする + return (pow(3, a) * 2).toInt(); + } + // 余りが 0 のときは、そのままにする + return pow(3, a).toInt(); + } ``` === "Rust" ```rust title="max_product_cutting.rs" - [class]{}-[func]{max_product_cutting} + /* 最大切断積:貪欲法 */ + fn max_product_cutting(n: i32) -> i32 { + // n <= 3 のときは、必ず 1 を切り出す + if n <= 3 { + return 1 * (n - 1); + } + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする + let a = n / 3; + let b = n % 3; + if b == 1 { + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える + 3_i32.pow(a as u32 - 1) * 2 * 2 + } else if b == 2 { + // 余りが 2 のときは、そのままにする + 3_i32.pow(a as u32) * 2 + } else { + // 余りが 0 のときは、そのままにする + 3_i32.pow(a as u32) + } + } ``` === "C" ```c title="max_product_cutting.c" - [class]{}-[func]{maxProductCutting} + /* 最大切断積:貪欲法 */ + int maxProductCutting(int n) { + // n <= 3 のときは、必ず 1 を切り出す + if (n <= 3) { + return 1 * (n - 1); + } + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする + int a = n / 3; + int b = n % 3; + if (b == 1) { + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える + return pow(3, a - 1) * 2 * 2; + } + if (b == 2) { + // 余りが 2 のときは、そのままにする + return pow(3, a) * 2; + } + // 余りが 0 のときは、そのままにする + return pow(3, a); + } ``` === "Kotlin" ```kotlin title="max_product_cutting.kt" - [class]{}-[func]{maxProductCutting} + /* 最大切断積:貪欲法 */ + fun maxProductCutting(n: Int): Int { + // n <= 3 のときは、必ず 1 を切り出す + if (n <= 3) { + return 1 * (n - 1) + } + // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする + val a = n / 3 + val b = n % 3 + if (b == 1) { + // 余りが 1 のときは、1 * 3 を 2 * 2 に変える + return 3.0.pow((a - 1)).toInt() * 2 * 2 + } + if (b == 2) { + // 余りが 2 のときは、そのままにする + return 3.0.pow(a).toInt() * 2 * 2 + } + // 余りが 0 のときは、そのままにする + return 3.0.pow(a).toInt() + } ``` === "Ruby" ```ruby title="max_product_cutting.rb" - [class]{}-[func]{max_product_cutting} + ### 最大分割積:貪欲法 ### + def max_product_cutting(n) + # n <= 3 のときは、必ず 1 を切り出す + return 1 * (n - 1) if n <= 3 + # 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする + a, b = n / 3, n % 3 + # 余りが 1 のときは、1 * 3 を 2 * 2 に変える + return (3.pow(a - 1) * 2 * 2).to_i if b == 1 + # 余りが 2 のときは、そのままにする + return (3.pow(a) * 2).to_i if b == 2 + # 余りが 0 のときは、そのままにする + 3.pow(a).to_i + end ``` -![切断後の最大積の計算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" } +??? pythontutor "コードの可視化" -

図 15-16   切断後の最大積の計算方法

+
+ -**時間計算量はプログラミング言語のべき乗演算の実装に依存します**。Pythonでは、よく使用されるべき乗計算関数は3種類あります: +![最大積分割の計算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" } -- 演算子 `**` と関数 `pow()` の両方の時間計算量は $O(\log⁡ a)$ です。 -- `math.pow()` 関数は内部でC言語ライブラリの `pow()` 関数を呼び出し、浮動小数点べき乗を実行し、時間計算量は $O(1)$ です。 +

図 15-16   最大積分割の計算方法

-変数 $a$ と $b$ は一定サイズの追加スペースを使用するため、**空間計算量は $O(1)$** です。 +**時間計算量は、プログラミング言語におけるべき乗演算の実装方法に依存する**。Python を例に取ると、よく使われるべき乗計算関数は 3 種類ある。 + +- 演算子 `**` と関数 `pow()` の時間計算量はいずれも $O(\log⁡ a)$ である。 +- 関数 `math.pow()` は内部で C 言語ライブラリの `pow()` 関数を呼び出し、浮動小数点のべき乗を実行するため、時間計算量は $O(1)$ である。 + +変数 $a$ と $b$ が使う追加領域は定数サイズであり、**したがって空間計算量は $O(1)$ である**。 ### 3.   正しさの証明 -背理法を使用し、$n \geq 3$ のケースのみを分析します。 +背理法を用い、$n \geq 4$ の場合のみを考える。 -1. **すべての因子 $\leq 3$**:最適分割スキームが因子 $x \geq 4$ を含むと仮定すると、それを確実に $2(x-2)$ にさらに分割でき、より大きな積を得られます。これは仮定と矛盾します。 -2. **分割スキームに $1$ が含まれない**:最適分割スキームが因子 $1$ を含むと仮定すると、それを確実に別の因子と結合してより大きな積を得られます。これは仮定と矛盾します。 -3. **分割スキームには最大で2つの $2$ が含まれる**:最適分割スキームが3つの $2$ を含むと仮定すると、それらを確実に2つの $3$ に置き換えて、より高い積を達成できます。これは仮定と矛盾します。 +1. **すべての因子は $\leq 3$** :最適な分割方法に $\geq 4$ の因子 $x$ が存在すると仮定すると、それは必ずさらに $2(x-2)$ に分割でき、より大きい(または等しい)積が得られる。これは仮定に矛盾する。 +2. **分割方法に $1$ は含まれない** :最適な分割方法に因子 $1$ が 1 つ存在すると仮定すると、それは必ず別の因子に併合でき、より大きい積を得られる。これは仮定に矛盾する。 +3. **分割方法に含まれる $2$ は高々 2 つ** :最適な分割方法に 3 つの $2$ が含まれると仮定すると、それは必ず 2 つの $3$ に置き換えられ、積はより大きくなる。これは仮定に矛盾する。 diff --git a/ja/docs/chapter_greedy/summary.md b/ja/docs/chapter_greedy/summary.md index 5f80e10bf..efbfbcb2a 100644 --- a/ja/docs/chapter_greedy/summary.md +++ b/ja/docs/chapter_greedy/summary.md @@ -4,13 +4,15 @@ comments: true # 15.5   まとめ -- 貪欲アルゴリズムは最適化問題を解決するためによく使用され、原理は各決定段階で局所的に最適な決定を行い、グローバルに最適な解を達成することです。 -- 貪欲アルゴリズムは貪欲な選択を次々と反復的に行い、各ラウンドで問題をより小さな部分問題に変換し、問題が解決されるまで続けます。 -- 貪欲アルゴリズムは実装が簡単なだけでなく、問題解決効率も高いです。動的プログラミングと比較して、貪欲アルゴリズムは一般的により低い時間計算量を持ちます。 -- コイン交換問題において、貪欲アルゴリズムは特定のコインの組み合わせに対して最適解を保証できますが、他の組み合わせでは貪欲アルゴリズムが非常に悪い解を見つける可能性があります。 -- 貪欲アルゴリズム解法に適した問題は2つの主要な性質を持ちます:貪欲選択性と最適部分構造。貪欲選択性は貪欲戦略の効果を表します。 -- 一部の複雑な問題では、貪欲選択性を証明することは簡単ではありません。逆に、無効性を証明することはしばしばより容易で、コイン交換問題などがその例です。 -- 貪欲問題の解決は主に3つのステップから構成されます:問題分析、貪欲戦略の決定、正しさの証明。このうち、貪欲戦略の決定が重要なステップであり、正しさの証明がしばしば挑戦となります。 -- 分数ナップサック問題は0-1ナップサック問題に基づいてアイテムの一部の選択を可能にし、したがって貪欲アルゴリズムを使用して解決できます。貪欲戦略の正しさは背理法によって証明できます。 -- 最大容量問題は全探索法で解決でき、時間計算量は $O(n^2)$ です。貪欲戦略を設計することで、各ラウンドで短い板を内側に移動し、時間計算量を $O(n)$ に最適化します。 -- 切断後の最大積問題において、2つの貪欲戦略を導出します:$\geq 4$ の整数は継続的に切断されるべきで、最適な切断因子は $3$ です。コードにはべき乗演算が含まれ、時間計算量はべき乗演算の実装方法に依存し、一般的に $O(1)$ または $O(\log n)$ です。 +### 1.   重要な振り返り + +- 貪欲法は通常、最適化問題を解くために用いられ、その原理は各意思決定段階で局所最適な決定を行い、全体最適解を得ることを目指すというものである。 +- 貪欲法は反復的に次々と貪欲な選択を行い、各ラウンドで問題をより小さな部分問題へと変換し、最終的に問題を解決する。 +- 貪欲法は実装が簡単であるだけでなく、問題を解く効率も高い。動的計画法と比べると、貪欲法の時間計算量は通常より低い。 +- 硬貨両替問題では、ある種の硬貨の組み合わせに対しては貪欲法で最適解を保証できるが、別の組み合わせではそうではなく、非常に悪い解を見つけてしまう可能性がある。 +- 貪欲法による解法に適した問題は、貪欲選択性と最適部分構造という 2 つの性質を備えている。貪欲選択性は、貪欲戦略の有効性を表している。 +- 一部の複雑な問題では、貪欲選択性を証明するのは容易ではない。相対的には、反例による否定のほうが簡単であり、硬貨両替問題がその一例である。 +- 貪欲法の問題を解く流れは主に 3 段階に分かれる。すなわち、問題分析、貪欲戦略の決定、正しさの証明である。このうち、貪欲戦略の決定が中核であり、正しさの証明はしばしば難所となる。 +- 分数ナップサック問題は 0-1 ナップサックを基に、品物の一部を選ぶことを許しているため、貪欲法で解くことができる。貪欲戦略の正しさは背理法で証明できる。 +- 最大容量問題は全探索で解くことができ、時間計算量は $O(n^2)$ である。貪欲戦略を設計し、各ラウンドで短い板を内側へ動かすことで、時間計算量を $O(n)$ に最適化できる。 +- 最大分割積問題では、2 つの貪欲戦略を順に導いた。すなわち、$\geq 4$ の整数はすべてさらに分割すべきであり、最適な分割因子は $3$ である。コードにはべき乗演算が含まれており、時間計算量はその実装方法に依存し、通常は $O(1)$ または $O(\log n)$ である。 diff --git a/ja/docs/chapter_hashing/hash_algorithm.md b/ja/docs/chapter_hashing/hash_algorithm.md index cb5e1f994..a2399d7cc 100644 --- a/ja/docs/chapter_hashing/hash_algorithm.md +++ b/ja/docs/chapter_hashing/hash_algorithm.md @@ -4,59 +4,59 @@ comments: true # 6.3   ハッシュアルゴリズム -前の2つの節では、ハッシュ表の動作原理とハッシュ衝突を処理する方法を紹介しました。しかし、オープンアドレス法と連鎖法はどちらも**衝突が発生した際にハッシュ表が正常に機能することのみを保証でき、ハッシュ衝突の発生頻度を減らすことはできません**。 +前の 2 節では、ハッシュテーブルの動作原理とハッシュ衝突の処理方法を紹介しました。しかし、オープンアドレス法であれ連鎖方式であれ、**それらが保証できるのは衝突発生時でもハッシュテーブルが正常に動作することだけであり、ハッシュ衝突そのものを減らすことはできません**。 -ハッシュ衝突があまりにも頻繁に発生すると、ハッシュ表の性能は劇的に悪化します。下図に示すように、連鎖法ハッシュ表では、理想的なケースではキー値ペアがバケット間に均等に分散され、最適なクエリ効率を実現します。最悪のケースでは、すべてのキー値ペアが同じバケットに格納され、時間計算量が$O(n)$に悪化します。 +ハッシュ衝突があまりにも頻繁に発生すると、ハッシュテーブルの性能は急激に劣化します。下図のように、連鎖方式のハッシュテーブルでは、理想的な場合にはキーと値のペアが各バケットに均等に分布し、最良の検索効率を達成します。最悪の場合には、すべてのキーと値のペアが同じバケットに格納され、時間計算量は $O(n)$ に劣化します。 -![ハッシュ衝突の理想的および最悪のケース](hash_algorithm.assets/hash_collision_best_worst_condition.png){ class="animation-figure" } +![ハッシュ衝突の最良ケースと最悪ケース](hash_algorithm.assets/hash_collision_best_worst_condition.png){ class="animation-figure" } -

図 6-8   ハッシュ衝突の理想的および最悪のケース

+

図 6-8   ハッシュ衝突の最良ケースと最悪ケース

-**キー値ペアの分布はハッシュ関数によって決定されます**。ハッシュ関数の計算ステップを思い出すと、まずハッシュ値を計算し、次に配列長で剰余を取ります: +**キーと値のペアの分布はハッシュ関数によって決まります**。ハッシュ関数の計算手順を思い出すと、まずハッシュ値を計算し、その後で配列長に対して剰余を取ります。 ```shell index = hash(key) % capacity ``` -上記の式を観察すると、ハッシュ表の容量`capacity`が固定されている場合、**ハッシュアルゴリズム`hash()`が出力値を決定し**、それによってハッシュ表におけるキー値ペアの分布を決定します。 +上の式から分かるように、ハッシュテーブルの容量 `capacity` が固定されているとき、**出力値を決めるのはハッシュアルゴリズム `hash()` です**。したがって、それがキーと値のペアのハッシュテーブル内での分布も決定します。 -これは、ハッシュ衝突の確率を減らすために、ハッシュアルゴリズム`hash()`の設計に焦点を当てるべきであることを意味します。 +これは、ハッシュ衝突の発生確率を下げるには、ハッシュアルゴリズム `hash()` の設計に注目すべきだということを意味します。 ## 6.3.1   ハッシュアルゴリズムの目標 -「高速で安定した」ハッシュ表データ構造を実現するために、ハッシュアルゴリズムは以下の特性を持つべきです: +「高速かつ安定した」ハッシュテーブルというデータ構造を実現するために、ハッシュアルゴリズムは次の特徴を備える必要があります。 -- **決定性**: 同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成するべきです。そうでなければハッシュ表は信頼できません。 -- **高効率**: ハッシュ値を計算するプロセスは十分に高速である必要があります。計算オーバーヘッドが小さいほど、ハッシュ表はより実用的になります。 -- **均等分散**: ハッシュアルゴリズムはキー値ペアがハッシュ表に均等に分散されることを保証するべきです。分散が均等であるほど、ハッシュ衝突の確率は低くなります。 +- **決定性**:同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成しなければなりません。そうして初めて、ハッシュテーブルの信頼性が保たれます。 +- **高効率**:ハッシュ値の計算過程は十分に高速であるべきです。計算コストが小さいほど、ハッシュテーブルの実用性は高くなります。 +- **均一分布**:ハッシュアルゴリズムは、キーと値のペアがハッシュテーブル内に均等に分布するようにすべきです。分布が均一であるほど、ハッシュ衝突の確率は低くなります。 -実際、ハッシュアルゴリズムはハッシュ表の実装だけでなく、他の分野でも広く応用されています。 +実際には、ハッシュアルゴリズムはハッシュテーブルの実装だけでなく、ほかの多くの分野でも広く利用されています。 -- **パスワード保存**: ユーザーパスワードのセキュリティを保護するために、システムは通常平文パスワードを保存せず、パスワードのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力のハッシュ値を計算し、保存されているハッシュ値と比較します。一致すれば、パスワードは正しいと見なされます。 -- **データ整合性チェック**: データ送信者はデータのハッシュ値を計算して一緒に送信できます。受信者は受信したデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。一致すれば、データは完全であると見なされます。 +- **パスワード保存**:ユーザーのパスワードを保護するために、システムは通常、平文パスワードを直接保存せず、そのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力内容のハッシュ値を計算し、保存済みのハッシュ値と比較します。一致すれば、そのパスワードは正しいと見なされます。 +- **データ完全性検査**:送信側はデータのハッシュ値を計算してデータと一緒に送信できます。受信側は受け取ったデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。両者が一致すれば、そのデータは完全だと見なされます。 -暗号化アプリケーションでは、ハッシュ値から元のパスワードを推測するなどの逆行分析を防ぐために、ハッシュアルゴリズムはより高いレベルのセキュリティ機能が必要です。 +暗号分野の応用では、ハッシュ値から元のパスワードを推測するといった逆解析を防ぐために、ハッシュアルゴリズムにはさらに高いレベルの安全性が求められます。 -- **一方向性**: ハッシュ値から入力データに関する情報を推測することは不可能であるべきです。 -- **衝突耐性**: 同じハッシュ値を生成する2つの異なる入力を見つけることは極めて困難であるべきです。 -- **雪崩効果**: 入力の小さな変更は、出力に大きく予測不可能な変化をもたらすべきです。 +- **一方向性**:ハッシュ値から入力データに関するいかなる情報も逆算できないこと。 +- **耐衝突性**:異なる 2 つの入力で同じハッシュ値になるものを見つけることが、極めて困難であること。 +- **アバランシェ効果**:入力のわずかな変化が、出力の大きく予測不能な変化を引き起こすこと。 -**「均等分散」と「衝突耐性」は2つの別々の概念**であることに注意してください。均等分散を満たしても、必ずしも衝突耐性があるとは限りません。例えば、ランダムな入力`key`の下で、ハッシュ関数`key % 100`は均等に分散された出力を生成できます。しかし、このハッシュアルゴリズムは過度にシンプルで、下二桁が同じすべての`key`は同じ出力を持つため、ハッシュ値から使用可能な`key`を簡単に推測でき、パスワードを破ることができます。 +注意してほしいのは、**「均一分布」と「耐衝突性」は独立した 2 つの概念である**という点です。均一分布を満たしていても、耐衝突性を満たすとは限りません。たとえば、入力 `key` がランダムである場合、ハッシュ関数 `key % 100` は均一に分布した出力を生成できます。しかし、このハッシュアルゴリズムはあまりにも単純で、下 2 桁が同じ `key` はすべて同じ出力になります。そのため、ハッシュ値から利用可能な `key` を容易に逆算でき、結果としてパスワードが破られてしまいます。 ## 6.3.2   ハッシュアルゴリズムの設計 -ハッシュアルゴリズムの設計は多くの要因を考慮する必要がある複雑な問題です。しかし、要求が少ない一部のシナリオでは、いくつかの簡単なハッシュアルゴリズムを設計することもできます。 +ハッシュアルゴリズムの設計は、多くの要素を考慮しなければならない複雑な問題です。しかし、要求の高くない場面であれば、いくつかの単純なハッシュアルゴリズムを設計することもできます。 -- **加算ハッシュ**: 入力の各文字のASCIIコードを合計し、合計をハッシュ値として使用します。 -- **乗算ハッシュ**: 乗算の非相関性を利用し、各ラウンドで定数を乗算し、各文字のASCIIコードをハッシュ値に累積します。 -- **XORハッシュ**: 入力データの各要素をXORすることでハッシュ値を累積します。 -- **回転ハッシュ**: 各文字のASCIIコードをハッシュ値に累積し、各累積前にハッシュ値に回転操作を実行します。 +- **加算ハッシュ**:入力の各文字の ASCII コードを足し合わせ、その合計をハッシュ値とします。 +- **乗算ハッシュ**:乗算の非相関性を利用し、各ラウンドで定数を掛けながら、各文字の ASCII コードをハッシュ値に累積します。 +- **XOR ハッシュ**:入力データの各要素を XOR 演算で 1 つのハッシュ値に累積します。 +- **回転ハッシュ**:各文字の ASCII コードを 1 つのハッシュ値に累積し、各回の累積前にハッシュ値を回転させます。 === "Python" ```python title="simple_hash.py" def add_hash(key: str) -> int: - """加法ハッシュ""" + """加算ハッシュ""" hash = 0 modulus = 1000000007 for c in key: @@ -64,7 +64,7 @@ index = hash(key) % capacity return hash % modulus def mul_hash(key: str) -> int: - """乗法ハッシュ""" + """乗算ハッシュ""" hash = 0 modulus = 1000000007 for c in key: @@ -72,7 +72,7 @@ index = hash(key) % capacity return hash % modulus def xor_hash(key: str) -> int: - """XORハッシュ""" + """XOR ハッシュ""" hash = 0 modulus = 1000000007 for c in key: @@ -111,7 +111,7 @@ index = hash(key) % capacity return (int)hash; } - /* XORハッシュ */ + /* XOR ハッシュ */ int xorHash(string key) { int hash = 0; const int MODULUS = 1000000007; @@ -155,7 +155,7 @@ index = hash(key) % capacity return (int) hash; } - /* XORハッシュ */ + /* XOR ハッシュ */ int xorHash(String key) { int hash = 0; final int MODULUS = 1000000007; @@ -179,128 +179,479 @@ index = hash(key) % capacity === "C#" ```csharp title="simple_hash.cs" - [class]{simple_hash}-[func]{AddHash} + /* 加算ハッシュ */ + int AddHash(string key) { + long hash = 0; + const int MODULUS = 1000000007; + foreach (char c in key) { + hash = (hash + c) % MODULUS; + } + return (int)hash; + } - [class]{simple_hash}-[func]{MulHash} + /* 乗算ハッシュ */ + int MulHash(string key) { + long hash = 0; + const int MODULUS = 1000000007; + foreach (char c in key) { + hash = (31 * hash + c) % MODULUS; + } + return (int)hash; + } - [class]{simple_hash}-[func]{XorHash} + /* XOR ハッシュ */ + int XorHash(string key) { + int hash = 0; + const int MODULUS = 1000000007; + foreach (char c in key) { + hash ^= c; + } + return hash & MODULUS; + } - [class]{simple_hash}-[func]{RotHash} + /* 回転ハッシュ */ + int RotHash(string key) { + long hash = 0; + const int MODULUS = 1000000007; + foreach (char c in key) { + hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS; + } + return (int)hash; + } ``` === "Go" ```go title="simple_hash.go" - [class]{}-[func]{addHash} + /* 加算ハッシュ */ + func addHash(key string) int { + var hash int64 + var modulus int64 - [class]{}-[func]{mulHash} + modulus = 1000000007 + for _, b := range []byte(key) { + hash = (hash + int64(b)) % modulus + } + return int(hash) + } - [class]{}-[func]{xorHash} + /* 乗算ハッシュ */ + func mulHash(key string) int { + var hash int64 + var modulus int64 - [class]{}-[func]{rotHash} + modulus = 1000000007 + for _, b := range []byte(key) { + hash = (31*hash + int64(b)) % modulus + } + return int(hash) + } + + /* XOR ハッシュ */ + func xorHash(key string) int { + hash := 0 + modulus := 1000000007 + for _, b := range []byte(key) { + fmt.Println(int(b)) + hash ^= int(b) + hash = (31*hash + int(b)) % modulus + } + return hash & modulus + } + + /* 回転ハッシュ */ + func rotHash(key string) int { + var hash int64 + var modulus int64 + + modulus = 1000000007 + for _, b := range []byte(key) { + hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus + } + return int(hash) + } ``` === "Swift" ```swift title="simple_hash.swift" - [class]{}-[func]{addHash} + /* 加算ハッシュ */ + func addHash(key: String) -> Int { + var hash = 0 + let MODULUS = 1_000_000_007 + for c in key { + for scalar in c.unicodeScalars { + hash = (hash + Int(scalar.value)) % MODULUS + } + } + return hash + } - [class]{}-[func]{mulHash} + /* 乗算ハッシュ */ + func mulHash(key: String) -> Int { + var hash = 0 + let MODULUS = 1_000_000_007 + for c in key { + for scalar in c.unicodeScalars { + hash = (31 * hash + Int(scalar.value)) % MODULUS + } + } + return hash + } - [class]{}-[func]{xorHash} + /* XOR ハッシュ */ + func xorHash(key: String) -> Int { + var hash = 0 + let MODULUS = 1_000_000_007 + for c in key { + for scalar in c.unicodeScalars { + hash ^= Int(scalar.value) + } + } + return hash & MODULUS + } - [class]{}-[func]{rotHash} + /* 回転ハッシュ */ + func rotHash(key: String) -> Int { + var hash = 0 + let MODULUS = 1_000_000_007 + for c in key { + for scalar in c.unicodeScalars { + hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS + } + } + return hash + } ``` === "JS" ```javascript title="simple_hash.js" - [class]{}-[func]{addHash} + /* 加算ハッシュ */ + function addHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{mulHash} + /* 乗算ハッシュ */ + function mulHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (31 * hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{xorHash} + /* XOR ハッシュ */ + function xorHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash ^= c.charCodeAt(0); + } + return hash % MODULUS; + } - [class]{}-[func]{rotHash} + /* 回転ハッシュ */ + function rotHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; + } + return hash; + } ``` === "TS" ```typescript title="simple_hash.ts" - [class]{}-[func]{addHash} + /* 加算ハッシュ */ + function addHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{mulHash} + /* 乗算ハッシュ */ + function mulHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (31 * hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{xorHash} + /* XOR ハッシュ */ + function xorHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash ^= c.charCodeAt(0); + } + return hash % MODULUS; + } - [class]{}-[func]{rotHash} + /* 回転ハッシュ */ + function rotHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; + } + return hash; + } ``` === "Dart" ```dart title="simple_hash.dart" - [class]{}-[func]{addHash} + /* 加算ハッシュ */ + int addHash(String key) { + int hash = 0; + final int MODULUS = 1000000007; + for (int i = 0; i < key.length; i++) { + hash = (hash + key.codeUnitAt(i)) % MODULUS; + } + return hash; + } - [class]{}-[func]{mulHash} + /* 乗算ハッシュ */ + int mulHash(String key) { + int hash = 0; + final int MODULUS = 1000000007; + for (int i = 0; i < key.length; i++) { + hash = (31 * hash + key.codeUnitAt(i)) % MODULUS; + } + return hash; + } - [class]{}-[func]{xorHash} + /* XOR ハッシュ */ + int xorHash(String key) { + int hash = 0; + final int MODULUS = 1000000007; + for (int i = 0; i < key.length; i++) { + hash ^= key.codeUnitAt(i); + } + return hash & MODULUS; + } - [class]{}-[func]{rotHash} + /* 回転ハッシュ */ + int rotHash(String key) { + int hash = 0; + final int MODULUS = 1000000007; + for (int i = 0; i < key.length; i++) { + hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS; + } + return hash; + } ``` === "Rust" ```rust title="simple_hash.rs" - [class]{}-[func]{add_hash} + /* 加算ハッシュ */ + fn add_hash(key: &str) -> i32 { + let mut hash = 0_i64; + const MODULUS: i64 = 1000000007; - [class]{}-[func]{mul_hash} + for c in key.chars() { + hash = (hash + c as i64) % MODULUS; + } - [class]{}-[func]{xor_hash} + hash as i32 + } - [class]{}-[func]{rot_hash} + /* 乗算ハッシュ */ + fn mul_hash(key: &str) -> i32 { + let mut hash = 0_i64; + const MODULUS: i64 = 1000000007; + + for c in key.chars() { + hash = (31 * hash + c as i64) % MODULUS; + } + + hash as i32 + } + + /* XOR ハッシュ */ + fn xor_hash(key: &str) -> i32 { + let mut hash = 0_i64; + const MODULUS: i64 = 1000000007; + + for c in key.chars() { + hash ^= c as i64; + } + + (hash & MODULUS) as i32 + } + + /* 回転ハッシュ */ + fn rot_hash(key: &str) -> i32 { + let mut hash = 0_i64; + const MODULUS: i64 = 1000000007; + + for c in key.chars() { + hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS; + } + + hash as i32 + } ``` === "C" ```c title="simple_hash.c" - [class]{}-[func]{addHash} + /* 加算ハッシュ */ + int addHash(char *key) { + long long hash = 0; + const int MODULUS = 1000000007; + for (int i = 0; i < strlen(key); i++) { + hash = (hash + (unsigned char)key[i]) % MODULUS; + } + return (int)hash; + } - [class]{}-[func]{mulHash} + /* 乗算ハッシュ */ + int mulHash(char *key) { + long long hash = 0; + const int MODULUS = 1000000007; + for (int i = 0; i < strlen(key); i++) { + hash = (31 * hash + (unsigned char)key[i]) % MODULUS; + } + return (int)hash; + } - [class]{}-[func]{xorHash} + /* XOR ハッシュ */ + int xorHash(char *key) { + int hash = 0; + const int MODULUS = 1000000007; - [class]{}-[func]{rotHash} + for (int i = 0; i < strlen(key); i++) { + hash ^= (unsigned char)key[i]; + } + return hash & MODULUS; + } + + /* 回転ハッシュ */ + int rotHash(char *key) { + long long hash = 0; + const int MODULUS = 1000000007; + for (int i = 0; i < strlen(key); i++) { + hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS; + } + + return (int)hash; + } ``` === "Kotlin" ```kotlin title="simple_hash.kt" - [class]{}-[func]{addHash} + /* 加算ハッシュ */ + fun addHash(key: String): Int { + var hash = 0L + val MODULUS = 1000000007 + for (c in key.toCharArray()) { + hash = (hash + c.code) % MODULUS + } + return hash.toInt() + } - [class]{}-[func]{mulHash} + /* 乗算ハッシュ */ + fun mulHash(key: String): Int { + var hash = 0L + val MODULUS = 1000000007 + for (c in key.toCharArray()) { + hash = (31 * hash + c.code) % MODULUS + } + return hash.toInt() + } - [class]{}-[func]{xorHash} + /* XOR ハッシュ */ + fun xorHash(key: String): Int { + var hash = 0 + val MODULUS = 1000000007 + for (c in key.toCharArray()) { + hash = hash xor c.code + } + return hash and MODULUS + } - [class]{}-[func]{rotHash} + /* 回転ハッシュ */ + fun rotHash(key: String): Int { + var hash = 0L + val MODULUS = 1000000007 + for (c in key.toCharArray()) { + hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS + } + return hash.toInt() + } ``` === "Ruby" ```ruby title="simple_hash.rb" - [class]{}-[func]{add_hash} + ### 加算ハッシュ ### + def add_hash(key) + hash = 0 + modulus = 1_000_000_007 - [class]{}-[func]{mul_hash} + key.each_char { |c| hash += c.ord } - [class]{}-[func]{xor_hash} + hash % modulus + end - [class]{}-[func]{rot_hash} + ### 乗算ハッシュ ### + def mul_hash(key) + hash = 0 + modulus = 1_000_000_007 + + key.each_char { |c| hash = 31 * hash + c.ord } + + hash % modulus + end + + ### XOR ハッシュ ### + def xor_hash(key) + hash = 0 + modulus = 1_000_000_007 + + key.each_char { |c| hash ^= c.ord } + + hash % modulus + end + + ### 回転ハッシュ ### + def rot_hash(key) + hash = 0 + modulus = 1_000_000_007 + + key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord } + + hash % modulus + end ``` -各ハッシュアルゴリズムの最後のステップが大きな素数$1000000007$の剰余を取ることで、ハッシュ値が適切な範囲内にあることを保証していることが観察されます。なぜ素数の剰余を取ることが強調されるのか、または合成数の剰余を取ることの欠点は何かを考える価値があります。これは興味深い質問です。 +??? pythontutor "コードの可視化" -結論として:**大きな素数を剰余として使用することで、ハッシュ値の均等分散を最大化できます**。素数は他の数と共通因子を持たないため、剰余演算によって引き起こされる周期的パターンを減らし、ハッシュ衝突を回避できます。 +
+ -例えば、合成数$9$を剰余として選択するとします。これは$3$で割り切れるため、$3$で割り切れるすべての`key`はハッシュ値$0$、$3$、$6$にマッピングされます。 +見て分かるように、各ハッシュアルゴリズムの最後のステップでは、大きな素数 $1000000007$ で剰余を取り、ハッシュ値が適切な範囲に収まるようにしています。ここで考えてみる価値があるのは、なぜ素数での剰余を強調するのか、あるいは合成数で剰余を取ることにどんな欠点があるのか、という点です。これは興味深い問題です。 + +先に結論を述べると、**法として大きな素数を使うと、ハッシュ値が均一に分布することを最大限に保証できます**。素数はほかの数と公約数を持たないため、剰余演算によって生じる周期的なパターンを減らし、ハッシュ衝突を避けやすくなります。 + +たとえば、法として合成数 $9$ を選ぶとします。これは $3$ で割り切れるため、$3$ で割り切れるすべての `key` は、$0$、$3$、$6$ の 3 つのハッシュ値に写像されます。 $$ \begin{aligned} @@ -310,7 +661,7 @@ $$ \end{aligned} $$ -入力`key`がたまたまこの種の等差数列分布を持つ場合、ハッシュ値がクラスターし、ハッシュ衝突を悪化させます。今度は`modulus`を素数$13$に置き換えるとします。`key`と`modulus`の間に共通因子がないため、出力ハッシュ値の均等性が大幅に改善されます。 +入力 `key` がたまたまこのような等差数列の分布をしていると、ハッシュ値に偏りが生じ、ハッシュ衝突がさらに深刻になります。そこで `modulus` を素数 $13$ に置き換えると仮定すると、`key` と `modulus` の間に公約数が存在しないため、出力されるハッシュ値の均一性は明らかに向上します。 $$ \begin{aligned} @@ -320,75 +671,75 @@ $$ \end{aligned} $$ -`key`がランダムで均等に分散されることが保証されている場合、剰余として素数または合成数を選択しても、両方とも均等に分散されたハッシュ値を生成できることは注目に値します。しかし、`key`の分布にある種の周期性がある場合、合成数の剰余はクラスタリングを引き起こしやすくなります。 +補足すると、`key` がランダムかつ均一に分布していると保証できるなら、法に素数を選んでも合成数を選んでも構いません。どちらでも均一に分布したハッシュ値を出力できます。しかし、`key` の分布に何らかの周期性がある場合、合成数で剰余を取るほうが偏りが生じやすくなります。 -要約すると、通常は素数を剰余として選択し、この素数は周期的パターンを可能な限り排除し、ハッシュアルゴリズムの堅牢性を向上させるために十分大きくある必要があります。 +要するに、通常は法として素数を選び、その素数はできるだけ大きいほうが望ましいです。そうすることで周期的なパターンをできる限り取り除き、ハッシュアルゴリズムの堅牢性を高められます。 ## 6.3.3   一般的なハッシュアルゴリズム -上記で言及した簡単なハッシュアルゴリズムはかなり「脆弱」で、ハッシュアルゴリズムの設計目標から程遠いことは難しくありません。例えば、加算とXORは交換法則に従うため、加算ハッシュとXORハッシュは同じ内容だが順序が異なる文字列を区別できず、ハッシュ衝突を悪化させ、セキュリティ問題を引き起こす可能性があります。 +上で紹介した単純なハッシュアルゴリズムは、どれも比較的「脆弱」であり、ハッシュアルゴリズムの設計目標にはほど遠いことが分かります。たとえば、加算と XOR は交換法則を満たすため、加算ハッシュと XOR ハッシュでは、内容が同じで順序だけ異なる文字列を区別できません。これはハッシュ衝突を悪化させ、一部の安全上の問題を引き起こす可能性があります。 -実際には、通常MD5、SHA-1、SHA-2、SHA-3などの標準ハッシュアルゴリズムを使用します。これらは任意の長さの入力データを固定長のハッシュ値にマッピングできます。 +実際には、MD5、SHA-1、SHA-2、SHA-3 などの標準的なハッシュアルゴリズムを用いることが一般的です。これらは任意長の入力データを、固定長のハッシュ値へ写像できます。 -過去1世紀にわたって、ハッシュアルゴリズムは継続的なアップグレードと最適化のプロセスにありました。一部の研究者はハッシュアルゴリズムの性能向上に努め、ハッカーを含む他の人々はハッシュアルゴリズムのセキュリティ問題を見つけることに専念しています。以下の表は、実用的なアプリケーションで一般的に使用されるハッシュアルゴリズムを示しています。 +ここ 1 世紀近くの間、ハッシュアルゴリズムは継続的に改良と最適化が進められてきました。ある研究者たちは性能向上に取り組み、別の研究者やハッカーたちは安全性の弱点を探し続けてきました。次の表は、実際の応用でよく使われるハッシュアルゴリズムを示したものです。 -- MD5とSHA-1は複数回攻撃に成功しており、さまざまなセキュリティアプリケーションで放棄されています。 -- SHA-2シリーズ、特にSHA-256は、現在最も安全なハッシュアルゴリズムの1つで、成功した攻撃は報告されておらず、さまざまなセキュリティアプリケーションとプロトコルで一般的に使用されています。 -- SHA-3はSHA-2と比較して実装コストが低く、計算効率が高いですが、現在の使用範囲はSHA-2シリーズほど広範囲ではありません。 +- MD5 と SHA-1 は何度も攻撃に成功されているため、各種のセキュリティ用途では廃止されています。 +- SHA-2 系列の SHA-256 は最も安全なハッシュアルゴリズムの 1 つであり、いまだに成功した攻撃例がないため、多くのセキュリティ用途やプロトコルで広く使われています。 +- SHA-3 は SHA-2 と比べて実装コストが低く、計算効率も高い一方で、現時点での普及度は SHA-2 系列に及びません。

表 6-2   一般的なハッシュアルゴリズム

-| | MD5 | SHA-1 | SHA-2 | SHA-3 | -| --------------- | ----------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------- | ---------------------------- | -| リリース年 | 1992 | 1995 | 2002 | 2008 | -| 出力長 | 128 bit | 160 bit | 256/512 bit | 224/256/384/512 bit | -| ハッシュ衝突 | 頻繁 | 頻繁 | まれ | まれ | -| セキュリティレベル | 低、攻撃に成功している | 低、攻撃に成功している | 高 | 高 | -| アプリケーション | 放棄、データ整合性チェックにまだ使用 | 放棄 | 暗号通貨取引検証、デジタル署名など | SHA-2の代替として使用可能 | +| | MD5 | SHA-1 | SHA-2 | SHA-3 | +| -------- | ------------------------------ | ---------------- | ---------------------------- | ------------------- | +| 発表年 | 1992 | 1995 | 2002 | 2008 | +| 出力長 | 128 bit | 160 bit | 256/512 bit | 224/256/384/512 bit | +| ハッシュ衝突 | 多い | 多い | 非常に少ない | 非常に少ない | +| セキュリティレベル | 低く、攻撃に成功されている | 低く、攻撃に成功されている | 高い | 高い | +| 用途 | 廃止済みだが、データ完全性検査には使われる | 廃止済み | 暗号資産の取引検証、デジタル署名など | SHA-2 の代替に使える |
-# データ構造におけるハッシュ値 +## 6.3.4   データ構造のハッシュ値 -ハッシュ表のキーは整数、小数、文字列などのさまざまなデータ型にできることを知っています。プログラミング言語は通常、これらのデータ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュ表のバケットインデックスを計算します。Pythonを例に取ると、`hash()`関数を使用してさまざまなデータ型のハッシュ値を計算できます。 +ご存じのように、ハッシュテーブルの `key` には整数、小数、文字列などのデータ型を使えます。プログラミング言語は通常、これらのデータ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュテーブル内のバケットインデックス計算に利用します。Python を例にすると、`hash()` 関数を呼び出して各種データ型のハッシュ値を計算できます。 -- 整数とブール値のハッシュ値は、それら自身の値です。 -- 浮動小数点数と文字列のハッシュ値の計算はより複雑で、興味のある読者は自分で研究することをお勧めします。 -- タプルのハッシュ値は、その各要素のハッシュ値の組み合わせで、単一のハッシュ値になります。 -- オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすることで、内容に基づいてハッシュ値を生成できます。 +- 整数と真理値のハッシュ値は、その値自身です。 +- 浮動小数点数と文字列のハッシュ値の計算はやや複雑なので、興味がある読者は自分で調べてみてください。 +- タプルのハッシュ値は、各要素のハッシュ値を求めてから、それらを組み合わせて 1 つのハッシュ値にしたものです。 +- オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすれば、内容に基づくハッシュ値を実装できます。 !!! tip - 異なるプログラミング言語における組み込みハッシュ値計算関数の定義と方法は異なることに注意してください。 + 注意してください。組み込みのハッシュ値計算関数の定義や方法は、プログラミング言語ごとに異なります。 === "Python" ```python title="built_in_hash.py" num = 3 hash_num = hash(num) - # 整数3のハッシュ値は3 + # 整数 3 のハッシュ値は 3 bol = True hash_bol = hash(bol) - # ブール値Trueのハッシュ値は1 + # 真理値 True のハッシュ値は 1 dec = 3.14159 hash_dec = hash(dec) - # 小数3.14159のハッシュ値は326484311674566659 + # 小数 3.14159 のハッシュ値は 326484311674566659 - str = "Hello 算法" + str = "Hello アルゴリズム" hash_str = hash(str) - # 文字列"Hello 算法"のハッシュ値は4617003410720528961 + # 文字列「Hello アルゴリズム」のハッシュ値は 4617003410720528961 - tup = (12836, "小哈") + tup = (12836, "シャオハ") hash_tup = hash(tup) - # タプル(12836, '小哈')のハッシュ値は1029005403108185979 + # タプル (12836, 'シャオハ') のハッシュ値は 1029005403108185979 obj = ListNode(0) hash_obj = hash(obj) - # ListNodeオブジェクト0x1058fd810のハッシュ値は274267521 + # ノードオブジェクト のハッシュ値は 274267521 ``` === "C++" @@ -396,22 +747,22 @@ $$ ```cpp title="built_in_hash.cpp" int num = 3; size_t hashNum = hash()(num); - // 整数3のハッシュ値は3 + // 整数 3 のハッシュ値は 3 bool bol = true; size_t hashBol = hash()(bol); - // ブール値1のハッシュ値は1 + // 真理値 1 のハッシュ値は 1 double dec = 3.14159; size_t hashDec = hash()(dec); - // 小数3.14159のハッシュ値は4614256650576692846 + // 小数 3.14159 のハッシュ値は 4614256650576692846 - string str = "Hello 算法"; + string str = "Hello アルゴリズム"; size_t hashStr = hash()(str); - // 文字列"Hello 算法"のハッシュ値は15466937326284535026 + // 文字列「Hello アルゴリズム」のハッシュ値は 15466937326284535026 - // C++では、組み込みstd::hash()は基本データ型のハッシュ値のみを提供 - // 配列とオブジェクトのハッシュ値は別途実装が必要 + // C++ では、組み込みの std:hash() は基本データ型のハッシュ値計算のみを提供する + // 配列やオブジェクトのハッシュ値計算は自分で実装する必要がある ``` === "Java" @@ -419,27 +770,27 @@ $$ ```java title="built_in_hash.java" int num = 3; int hashNum = Integer.hashCode(num); - // 整数3のハッシュ値は3 + // 整数 3 のハッシュ値は 3 boolean bol = true; int hashBol = Boolean.hashCode(bol); - // ブール値trueのハッシュ値は1231 + // 真理値 true のハッシュ値は 1231 double dec = 3.14159; int hashDec = Double.hashCode(dec); - // 小数3.14159のハッシュ値は-1340954729 + // 小数 3.14159 のハッシュ値は -1340954729 - String str = "Hello 算法"; + String str = "Hello アルゴリズム"; int hashStr = str.hashCode(); - // 文字列"Hello 算法"のハッシュ値は-727081396 + // 文字列「Hello アルゴリズム」のハッシュ値は -727081396 - Object[] arr = { 12836, "小哈" }; + Object[] arr = { 12836, "シャオハ" }; int hashTup = Arrays.hashCode(arr); - // 配列[12836, 小哈]のハッシュ値は1151158 + // 配列 [12836, シャオハ] のハッシュ値は 1151158 ListNode obj = new ListNode(0); int hashObj = obj.hashCode(); - // ListNodeオブジェクトutils.ListNode@7dc5e7b4のハッシュ値は2110121908 + // ノードオブジェクト utils.ListNode@7dc5e7b4 のハッシュ値は 2110121908 ``` === "C#" @@ -447,33 +798,33 @@ $$ ```csharp title="built_in_hash.cs" int num = 3; int hashNum = num.GetHashCode(); - // 整数3のハッシュ値は3; + // 整数 3 のハッシュ値は 3; bool bol = true; int hashBol = bol.GetHashCode(); - // ブール値trueのハッシュ値は1; + // 真理値 true のハッシュ値は 1; double dec = 3.14159; int hashDec = dec.GetHashCode(); - // 小数3.14159のハッシュ値は-1340954729; + // 小数 3.14159 のハッシュ値は -1340954729; - string str = "Hello 算法"; + string str = "Hello アルゴリズム"; int hashStr = str.GetHashCode(); - // 文字列"Hello 算法"のハッシュ値は-586107568; + // 文字列「Hello アルゴリズム」のハッシュ値は -586107568; - object[] arr = [12836, "小哈"]; + object[] arr = [12836, "シャオハ"]; int hashTup = arr.GetHashCode(); - // 配列[12836, 小哈]のハッシュ値は42931033; + // 配列 [12836, シャオハ] のハッシュ値は 42931033; ListNode obj = new(0); int hashObj = obj.GetHashCode(); - // ListNodeオブジェクト0のハッシュ値は39053774; + // ノードオブジェクト 0 のハッシュ値は 39053774; ``` === "Go" ```go title="built_in_hash.go" - // Goには組み込みのハッシュコード関数が提供されていません + // Go は組み込みの hash code 関数を提供していない ``` === "Swift" @@ -481,39 +832,39 @@ $$ ```swift title="built_in_hash.swift" let num = 3 let hashNum = num.hashValue - // 整数3のハッシュ値は9047044699613009734 + // 整数 3 のハッシュ値は 9047044699613009734 let bol = true let hashBol = bol.hashValue - // ブール値trueのハッシュ値は-4431640247352757451 + // 真理値 true のハッシュ値は -4431640247352757451 let dec = 3.14159 let hashDec = dec.hashValue - // 小数3.14159のハッシュ値は-2465384235396674631 + // 小数 3.14159 のハッシュ値は -2465384235396674631 - let str = "Hello 算法" + let str = "Hello アルゴリズム" let hashStr = str.hashValue - // 文字列"Hello 算法"のハッシュ値は-7850626797806988787 + // 文字列「Hello アルゴリズム」のハッシュ値は -7850626797806988787 - let arr = [AnyHashable(12836), AnyHashable("小哈")] + let arr = [AnyHashable(12836), AnyHashable("シャオハ")] let hashTup = arr.hashValue - // 配列[AnyHashable(12836), AnyHashable("小哈")]のハッシュ値は-2308633508154532996 + // 配列 [AnyHashable(12836), AnyHashable("シャオハ")] のハッシュ値は -2308633508154532996 let obj = ListNode(x: 0) let hashObj = obj.hashValue - // ListNodeオブジェクトutils.ListNodeのハッシュ値は-2434780518035996159 + // ノードオブジェクト utils.ListNode のハッシュ値は -2434780518035996159 ``` === "JS" ```javascript title="built_in_hash.js" - // JavaScriptには組み込みのハッシュコード関数が提供されていません + // JavaScript は組み込みの hash code 関数を提供していない ``` === "TS" ```typescript title="built_in_hash.ts" - // TypeScriptには組み込みのハッシュコード関数が提供されていません + // TypeScript は組み込みの hash code 関数を提供していない ``` === "Dart" @@ -521,27 +872,27 @@ $$ ```dart title="built_in_hash.dart" int num = 3; int hashNum = num.hashCode; - // 整数3のハッシュ値は34803 + // 整数 3 のハッシュ値は 34803 bool bol = true; int hashBol = bol.hashCode; - // ブール値trueのハッシュ値は1231 + // 真理値 true のハッシュ値は 1231 double dec = 3.14159; int hashDec = dec.hashCode; - // 小数3.14159のハッシュ値は2570631074981783 + // 小数 3.14159 のハッシュ値は 2570631074981783 - String str = "Hello 算法"; + String str = "Hello アルゴリズム"; int hashStr = str.hashCode; - // 文字列"Hello 算法"のハッシュ値は468167534 + // 文字列「Hello アルゴリズム」のハッシュ値は 468167534 - List arr = [12836, "小哈"]; + List arr = [12836, "シャオハ"]; int hashArr = arr.hashCode; - // 配列[12836, 小哈]のハッシュ値は976512528 + // 配列 [12836, シャオハ] のハッシュ値は 976512528 ListNode obj = new ListNode(0); int hashObj = obj.hashCode; - // ListNodeオブジェクトInstance of 'ListNode'のハッシュ値は1033450432 + // ノードオブジェクト Instance of 'ListNode' のハッシュ値は 1033450432 ``` === "Rust" @@ -554,53 +905,107 @@ $$ let mut num_hasher = DefaultHasher::new(); num.hash(&mut num_hasher); let hash_num = num_hasher.finish(); - // 整数3のハッシュ値は568126464209439262 + // 整数 3 のハッシュ値は 568126464209439262 let bol = true; let mut bol_hasher = DefaultHasher::new(); bol.hash(&mut bol_hasher); let hash_bol = bol_hasher.finish(); - // ブール値trueのハッシュ値は4952851536318644461 + // 真理値 true のハッシュ値は 4952851536318644461 let dec: f32 = 3.14159; let mut dec_hasher = DefaultHasher::new(); dec.to_bits().hash(&mut dec_hasher); let hash_dec = dec_hasher.finish(); - // 小数3.14159のハッシュ値は2566941990314602357 + // 小数 3.14159 のハッシュ値は 2566941990314602357 - let str = "Hello 算法"; + let str = "Hello アルゴリズム"; let mut str_hasher = DefaultHasher::new(); str.hash(&mut str_hasher); let hash_str = str_hasher.finish(); - // 文字列"Hello 算法"のハッシュ値は16092673739211250988 + // 文字列「Hello アルゴリズム」のハッシュ値は 16092673739211250988 - let arr = (&12836, &"小哈"); + let arr = (&12836, &"シャオハ"); let mut tup_hasher = DefaultHasher::new(); arr.hash(&mut tup_hasher); let hash_tup = tup_hasher.finish(); - // タプル(12836, "小哈")のハッシュ値は1885128010422702749 + // タプル (12836, "シャオハ") のハッシュ値は 1885128010422702749 let node = ListNode::new(42); let mut hasher = DefaultHasher::new(); node.borrow().val.hash(&mut hasher); let hash = hasher.finish(); - // ListNodeオブジェクトRefCell { value: ListNode { val: 42, next: None } }のハッシュ値は15387811073369036852 + // ノードオブジェクト RefCell { value: ListNode { val: 42, next: None } } のハッシュ値は15387811073369036852 ``` === "C" ```c title="built_in_hash.c" - // Cには組み込みのハッシュコード関数が提供されていません + // C は組み込みの hash code 関数を提供していない ``` === "Kotlin" ```kotlin title="built_in_hash.kt" + val num = 3 + val hashNum = num.hashCode() + // 整数 3 のハッシュ値は 3 + val bol = true + val hashBol = bol.hashCode() + // 真理値 true のハッシュ値は 1231 + + val dec = 3.14159 + val hashDec = dec.hashCode() + // 小数 3.14159 のハッシュ値は -1340954729 + + val str = "Hello アルゴリズム" + val hashStr = str.hashCode() + // 文字列「Hello アルゴリズム」のハッシュ値は -727081396 + + val arr = arrayOf(12836, "シャオハ") + val hashTup = arr.hashCode() + // 配列 [12836, シャオハ] のハッシュ値は 189568618 + + val obj = ListNode(0) + val hashObj = obj.hashCode() + // ノードオブジェクト utils.ListNode@1d81eb93 のハッシュ値は 495053715 ``` -多くのプログラミング言語では、**不変オブジェクトのみがハッシュ表の`key`として機能できます**。リスト(動的配列)を`key`として使用する場合、リストの内容が変更されると、そのハッシュ値も変更され、ハッシュ表で元の`value`を見つけることができなくなります。 +=== "Ruby" -カスタムオブジェクト(連結リストノードなど)のメンバー変数は可変ですが、ハッシュ可能です。**これは、オブジェクトのハッシュ値が通常そのメモリアドレスに基づいて生成されるためです**。オブジェクトの内容が変更されても、メモリアドレスは同じままなので、ハッシュ値は変更されません。 + ```ruby title="built_in_hash.rb" + num = 3 + hash_num = num.hash + # 整数 3 のハッシュ値は -4385856518450339636 -異なるコンソールで出力されるハッシュ値が異なることに気づいたかもしれません。**これは、Pythonインタープリターが起動するたびに文字列ハッシュ関数にランダムソルトを追加するためです**。このアプローチはHashDoS攻撃を効果的に防ぎ、ハッシュアルゴリズムのセキュリティを向上させます。 + bol = true + hash_bol = bol.hash + # 真理値 true のハッシュ値は -1617938112149317027 + + dec = 3.14159 + hash_dec = dec.hash + # 小数 3.14159 のハッシュ値は -1479186995943067893 + + str = "Hello アルゴリズム" + hash_str = str.hash + # 文字列「Hello アルゴリズム」のハッシュ値は -4075943250025831763 + + tup = [12836, 'シャオハ'] + hash_tup = tup.hash + # タプル (12836, 'シャオハ') のハッシュ値は 1999544809202288822 + + obj = ListNode.new(0) + hash_obj = obj.hash + # ノードオブジェクト # のハッシュ値は 4302940560806366381 + ``` + +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + +多くのプログラミング言語では、**不変オブジェクトだけがハッシュテーブルの `key` として使えます**。仮にリスト(動的配列)を `key` とすると、その内容が変化したときにハッシュ値も変わってしまうため、もとの `value` をハッシュテーブルから検索できなくなります。 + +カスタムオブジェクト(たとえば連結リストのノード)のメンバ変数は可変ですが、それでもハッシュ可能です。**これは、オブジェクトのハッシュ値が通常はメモリアドレスに基づいて生成されるためです**。オブジェクトの内容が変化しても、メモリアドレスが変わらなければ、ハッシュ値も変わりません。 + +注意深い人なら、異なるコンソールでプログラムを実行したときに、出力されるハッシュ値が異なることに気づくかもしれません。**これは、Python インタプリタが起動のたびに文字列ハッシュ関数へランダムな salt 値を追加しているためです**。この方法によって HashDoS 攻撃を効果的に防ぎ、ハッシュアルゴリズムの安全性を高めています。 diff --git a/ja/docs/chapter_hashing/hash_collision.md b/ja/docs/chapter_hashing/hash_collision.md index 8e0c4fc97..9cd9db470 100644 --- a/ja/docs/chapter_hashing/hash_collision.md +++ b/ja/docs/chapter_hashing/hash_collision.md @@ -4,51 +4,51 @@ comments: true # 6.2   ハッシュ衝突 -前節で述べたように、**ほとんどの場合、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論的にはハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列容量のサイズの場合、複数の整数が必然的に同じバケットインデックスにマッピングされます。 +前節で述べたように、**通常、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論上ハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列の容量サイズである場合、必然的に複数の整数が同じバケットインデックスに写像されます。 -ハッシュ衝突は誤ったクエリ結果につながり、ハッシュ表の使いやすさに深刻な影響を与える可能性があります。この問題に対処するために、ハッシュ衝突が発生するたびに、衝突が消えるまでハッシュ表のリサイズを実行します。このアプローチは非常にシンプルで直接的であり、うまく機能します。しかし、テーブルの拡張には大量のデータ移行とハッシュコードの再計算が含まれ、これらは高コストであるため、非常に非効率的に見えます。効率を向上させるために、以下の戦略を採用できます: +ハッシュ衝突は検索結果の誤りを招き、ハッシュテーブルの利用可能性に深刻な影響を与えます。この問題を解決するために、ハッシュ衝突が発生するたびにハッシュテーブルを拡張し、衝突が消えるまで続けることが考えられます。この方法は単純で効果的ですが、効率が低すぎます。なぜなら、ハッシュテーブルの拡張には大量のデータ移動とハッシュ値の計算が必要だからです。効率を高めるために、次の戦略を採用できます。 -1. **ハッシュ衝突が発生した場合でも、ターゲット要素の検索が適切に機能する**ようにハッシュ表のデータ構造を改善する。 -2. 深刻な衝突が観察され、必要になる前に、拡張は最後の手段とする。 +1. ハッシュテーブルのデータ構造を改良し、**ハッシュ衝突が発生してもハッシュテーブルが正常に動作できるようにする**。 +2. 必要な場合、すなわちハッシュ衝突が比較的深刻なときにのみ、拡張操作を実行する。 -ハッシュ表の構造を改善する主な方法は2つあります:「連鎖法」と「オープンアドレス法」です。 +ハッシュテーブルの構造改善方法には、主に「チェイン法」と「オープンアドレッシング」があります。 -## 6.2.1   連鎖法 +## 6.2.1   チェイン法 -元のハッシュ表では、各バケットは1つのキー値ペアのみを格納できます。連鎖法は単一の要素を連結リストに変換し、キー値ペアをリストノードとして扱い、衝突するすべてのキー値ペアを同じ連結リストに格納します。下図は連鎖法を使用したハッシュ表の例を示しています。 +元のハッシュテーブルでは、各バケットには 1 つのキーと値のペアしか格納できません。チェイン法(separate chaining)では、単一要素を連結リストに置き換え、キーと値のペアを連結リストのノードとして扱い、衝突したすべてのキーと値のペアを同じ連結リストに格納します。下図はチェイン法によるハッシュテーブルの例を示しています。 -![連鎖法ハッシュ表](hash_collision.assets/hash_table_chaining.png){ class="animation-figure" } +![チェイン法ハッシュテーブル](hash_collision.assets/hash_table_chaining.png){ class="animation-figure" } -

図 6-5   連鎖法ハッシュ表

+

図 6-5   チェイン法ハッシュテーブル

-連鎖法で実装されたハッシュ表の操作は以下のように変更されます: +チェイン法で実装されたハッシュテーブルでは、操作方法が次のように変わります。 -- **要素のクエリ**: `key`を入力し、ハッシュ関数を通してバケットインデックスを取得し、連結リストのヘッドノードにアクセスします。連結リストを走査してキーを比較し、ターゲットキー値ペアを見つけます。 -- **要素の追加**: ハッシュ関数を通して連結リストのヘッドノードにアクセスし、ノード(キー値ペア)をリストに追加します。 -- **要素の削除**: ハッシュ関数の結果に基づいて連結リストのヘッドにアクセスし、連結リストを走査してターゲットノードを見つけて削除します。 +- **要素の検索**:入力 `key` をハッシュ関数に通してバケットインデックスを得ると、連結リストの先頭ノードにアクセスできます。その後、連結リストを走査して `key` を比較し、目的のキーと値のペアを探します。 +- **要素の追加**:まずハッシュ関数で連結リストの先頭ノードにアクセスし、その後ノード(キーと値のペア)を連結リストに追加します。 +- **要素の削除**:ハッシュ関数の結果に基づいて連結リストの先頭にアクセスし、続いて連結リストを走査して対象ノードを探し、削除します。 -連鎖法には以下の制限があります: +チェイン法には次の制約があります。 -- **空間使用量の増加**: 連結リストにはノードポインタが含まれており、配列よりも多くのメモリ空間を消費します。 -- **クエリ効率の低下**: 対応する要素を見つけるために連結リストの線形走査が必要になるためです。 +- **使用メモリの増加**:連結リストにはノードポインタが含まれるため、配列よりも多くのメモリを消費します。 +- **検索効率の低下**:対応する要素を見つけるために連結リストを線形走査する必要があるためです。 -以下のコードは連鎖法ハッシュ表の簡単な実装を提供し、注意すべき2つの点があります: +以下のコードはチェイン法ハッシュテーブルの簡単な実装を示しています。注意すべき点は 2 つあります。 -- 簡単にするために、連結リストの代わりにリスト(動的配列)を使用します。この設定では、ハッシュ表(配列)は複数のバケットを含み、各バケットはリストです。 -- この実装にはハッシュ表のリサイズメソッドが含まれています。負荷率が$\frac{2}{3}$を超えると、ハッシュ表を元のサイズの2倍に拡張します。 +- 連結リストの代わりにリスト(動的配列)を使って、コードを簡潔にしています。この設定では、ハッシュテーブル(配列)は複数のバケットを含み、各バケットは 1 つのリストです。 +- 以下の実装にはハッシュテーブルの拡張メソッドが含まれています。負荷率が $\frac{2}{3}$ を超えたとき、ハッシュテーブルを元の $2$ 倍に拡張します。 === "Python" ```python title="hash_map_chaining.py" class HashMapChaining: - """チェーンアドレス法ハッシュテーブル""" + """チェイン法ハッシュテーブル""" def __init__(self): """コンストラクタ""" - self.size = 0 # キー値ペアの数 - self.capacity = 4 # ハッシュテーブルの容量 - self.load_thres = 2.0 / 3.0 # 拡張をトリガーする負荷率の閾値 - self.extend_ratio = 2 # 拡張の倍数 + self.size = 0 # キーと値のペア数 + self.capacity = 4 # ハッシュテーブル容量 + self.load_thres = 2.0 / 3.0 # リサイズを発動する負荷率のしきい値 + self.extend_ratio = 2 # 拡張倍率 self.buckets = [[] for _ in range(self.capacity)] # バケット配列 def hash_func(self, key: int) -> int: @@ -60,29 +60,29 @@ comments: true return self.size / self.capacity def get(self, key: int) -> str | None: - """照会操作""" + """検索操作""" index = self.hash_func(key) bucket = self.buckets[index] - # バケットを走査し、キーが見つかれば対応する val を返す + # バケットを走査し、key が見つかれば対応する val を返す for pair in bucket: if pair.key == key: return pair.val - # キーが見つからない場合、None を返す + # key が見つからない場合は None を返す return None def put(self, key: int, val: str): """追加操作""" - # 負荷率が閾値を超えた場合、拡張を実行 + # 負荷率がしきい値を超えたら、リサイズを実行 if self.load_factor() > self.load_thres: self.extend() index = self.hash_func(key) bucket = self.buckets[index] - # バケットを走査し、指定されたキーに遭遇した場合、対応する val を更新して返す + # バケットを走査し、指定した key が見つかれば対応する val を更新して返す for pair in bucket: if pair.key == key: pair.val = val return - # キーが見つからない場合、キー値ペアを末尾に追加 + # その key が存在しなければ、キーと値のペアを末尾に追加 pair = Pair(key, val) bucket.append(pair) self.size += 1 @@ -91,7 +91,7 @@ comments: true """削除操作""" index = self.hash_func(key) bucket = self.buckets[index] - # バケットを走査し、その中からキー値ペアを削除 + # バケットを走査してキーと値のペアを削除 for pair in bucket: if pair.key == key: bucket.remove(pair) @@ -100,13 +100,13 @@ comments: true def extend(self): """ハッシュテーブルを拡張""" - # 元のハッシュテーブルを一時的に保存 + # 元のハッシュテーブルを一時保存 buckets = self.buckets - # 拡張された新しいハッシュテーブルを初期化 + # リサイズ後の新しいハッシュテーブルを初期化 self.capacity *= self.extend_ratio self.buckets = [[] for _ in range(self.capacity)] self.size = 0 - # 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動 + # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す for bucket in buckets: for pair in bucket: self.put(pair.key, pair.val) @@ -126,9 +126,9 @@ comments: true /* チェイン法ハッシュテーブル */ class HashMapChaining { private: - int size; // キー値ペアの数 - int capacity; // ハッシュテーブルの容量 - double loadThres; // 拡張をトリガーする負荷率の閾値 + int size; // キーと値のペア数 + int capacity; // ハッシュテーブル容量 + double loadThres; // リサイズを発動する負荷率のしきい値 int extendRatio; // 拡張倍率 vector> buckets; // バケット配列 @@ -138,11 +138,11 @@ comments: true buckets.resize(capacity); } - /* デストラクタ */ + /* デストラクタメソッド */ ~HashMapChaining() { for (auto &bucket : buckets) { for (Pair *pair : bucket) { - // メモリを解放 + // メモリを解放する delete pair; } } @@ -158,34 +158,34 @@ comments: true return (double)size / (double)capacity; } - /* クエリ操作 */ + /* 検索操作 */ string get(int key) { int index = hashFunc(key); - // バケットを走査、キーが見つかった場合、対応するvalを返却 + // バケットを走査し、key が見つかれば対応する val を返す for (Pair *pair : buckets[index]) { if (pair->key == key) { return pair->val; } } - // キーが見つからない場合、空文字列を返却 + // key が見つからない場合は空文字列を返す return ""; } /* 追加操作 */ void put(int key, string val) { - // 負荷率が閾値を超えた場合、拡張を実行 + // 負荷率がしきい値を超えたら、リサイズを実行 if (loadFactor() > loadThres) { extend(); } int index = hashFunc(key); - // バケットを走査、指定キーに遭遇した場合、対応するvalを更新して返却 + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す for (Pair *pair : buckets[index]) { if (pair->key == key) { pair->val = val; return; } } - // キーが見つからない場合、キー値ペアを末尾に追加 + // その key が存在しなければ、キーと値のペアを末尾に追加 buckets[index].push_back(new Pair(key, val)); size++; } @@ -194,12 +194,12 @@ comments: true void remove(int key) { int index = hashFunc(key); auto &bucket = buckets[index]; - // バケットを走査、キー値ペアを削除 + // バケットを走査してキーと値のペアを削除 for (int i = 0; i < bucket.size(); i++) { if (bucket[i]->key == key) { Pair *tmp = bucket[i]; - bucket.erase(bucket.begin() + i); // キー値ペアを削除 - delete tmp; // メモリを解放 + bucket.erase(bucket.begin() + i); // そこからキーと値の組を削除する + delete tmp; // メモリを解放する size--; return; } @@ -210,22 +210,22 @@ comments: true void extend() { // 元のハッシュテーブルを一時保存 vector> bucketsTmp = buckets; - // 拡張された新しいハッシュテーブルを初期化 + // リサイズ後の新しいハッシュテーブルを初期化 capacity *= extendRatio; buckets.clear(); buckets.resize(capacity); size = 0; - // 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動 + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す for (auto &bucket : bucketsTmp) { for (Pair *pair : bucket) { put(pair->key, pair->val); - // メモリを解放 + // メモリを解放する delete pair; } } } - /* ハッシュテーブルを印刷 */ + /* ハッシュテーブルを出力 */ void print() { for (auto &bucket : buckets) { cout << "["; @@ -243,9 +243,9 @@ comments: true ```java title="hash_map_chaining.java" /* チェイン法ハッシュテーブル */ class HashMapChaining { - int size; // キー値ペアの数 - int capacity; // ハッシュテーブルの容量 - double loadThres; // 拡張をトリガーする負荷率の閾値 + int size; // キーと値のペア数 + int capacity; // ハッシュテーブル容量 + double loadThres; // リサイズを発動する負荷率のしきい値 int extendRatio; // 拡張倍率 List> buckets; // バケット配列 @@ -271,36 +271,36 @@ comments: true return (double) size / capacity; } - /* クエリ操作 */ + /* 検索操作 */ String get(int key) { int index = hashFunc(key); List bucket = buckets.get(index); - // バケットを走査、キーが見つかった場合対応するvalを返す + // バケットを走査し、key が見つかれば対応する val を返す for (Pair pair : bucket) { if (pair.key == key) { return pair.val; } } - // キーが見つからない場合、nullを返す + // key が見つからない場合は null を返す return null; } /* 追加操作 */ void put(int key, String val) { - // 負荷率が閾値を超えた場合、拡張を実行 + // 負荷率がしきい値を超えたら、リサイズを実行 if (loadFactor() > loadThres) { extend(); } int index = hashFunc(key); List bucket = buckets.get(index); - // バケットを走査、指定したキーに遭遇した場合、対応するvalを更新して戻る + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す for (Pair pair : bucket) { if (pair.key == key) { pair.val = val; return; } } - // キーが見つからない場合、キー値ペアを末尾に追加 + // その key が存在しなければ、キーと値のペアを末尾に追加 Pair pair = new Pair(key, val); bucket.add(pair); size++; @@ -310,7 +310,7 @@ comments: true void remove(int key) { int index = hashFunc(key); List bucket = buckets.get(index); - // バケットを走査、その中からキー値ペアを削除 + // バケットを走査してキーと値のペアを削除 for (Pair pair : bucket) { if (pair.key == key) { bucket.remove(pair); @@ -322,16 +322,16 @@ comments: true /* ハッシュテーブルを拡張 */ void extend() { - // 元のハッシュテーブルを一時的に保存 + // 元のハッシュテーブルを一時保存 List> bucketsTmp = buckets; - // 拡張された新しいハッシュテーブルを初期化 + // リサイズ後の新しいハッシュテーブルを初期化 capacity *= extendRatio; buckets = new ArrayList<>(capacity); for (int i = 0; i < capacity; i++) { buckets.add(new ArrayList<>()); } size = 0; - // 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動 + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す for (List bucket : bucketsTmp) { for (Pair pair : bucket) { put(pair.key, pair.val); @@ -339,7 +339,7 @@ comments: true } } - /* ハッシュテーブルを印刷 */ + /* ハッシュテーブルを出力 */ void print() { for (List bucket : buckets) { List res = new ArrayList<>(); @@ -355,101 +355,1209 @@ comments: true === "C#" ```csharp title="hash_map_chaining.cs" - [class]{HashMapChaining}-[func]{} + /* チェイン法ハッシュテーブル */ + class HashMapChaining { + int size; // キーと値のペア数 + int capacity; // ハッシュテーブル容量 + double loadThres; // リサイズを発動する負荷率のしきい値 + int extendRatio; // 拡張倍率 + List> buckets; // バケット配列 + + /* コンストラクタ */ + public HashMapChaining() { + size = 0; + capacity = 4; + loadThres = 2.0 / 3.0; + extendRatio = 2; + buckets = new List>(capacity); + for (int i = 0; i < capacity; i++) { + buckets.Add([]); + } + } + + /* ハッシュ関数 */ + int HashFunc(int key) { + return key % capacity; + } + + /* 負荷率 */ + double LoadFactor() { + return (double)size / capacity; + } + + /* 検索操作 */ + public string? Get(int key) { + int index = HashFunc(key); + // バケットを走査し、key が見つかれば対応する val を返す + foreach (Pair pair in buckets[index]) { + if (pair.key == key) { + return pair.val; + } + } + // key が見つからない場合は null を返す + return null; + } + + /* 追加操作 */ + public void Put(int key, string val) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (LoadFactor() > loadThres) { + Extend(); + } + int index = HashFunc(key); + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す + foreach (Pair pair in buckets[index]) { + if (pair.key == key) { + pair.val = val; + return; + } + } + // その key が存在しなければ、キーと値のペアを末尾に追加 + buckets[index].Add(new Pair(key, val)); + size++; + } + + /* 削除操作 */ + public void Remove(int key) { + int index = HashFunc(key); + // バケットを走査してキーと値のペアを削除 + foreach (Pair pair in buckets[index].ToList()) { + if (pair.key == key) { + buckets[index].Remove(pair); + size--; + break; + } + } + } + + /* ハッシュテーブルを拡張 */ + void Extend() { + // 元のハッシュテーブルを一時保存 + List> bucketsTmp = buckets; + // リサイズ後の新しいハッシュテーブルを初期化 + capacity *= extendRatio; + buckets = new List>(capacity); + for (int i = 0; i < capacity; i++) { + buckets.Add([]); + } + size = 0; + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + foreach (List bucket in bucketsTmp) { + foreach (Pair pair in bucket) { + Put(pair.key, pair.val); + } + } + } + + /* ハッシュテーブルを出力 */ + public void Print() { + foreach (List bucket in buckets) { + List res = []; + foreach (Pair pair in bucket) { + res.Add(pair.key + " -> " + pair.val); + } + foreach (string kv in res) { + Console.WriteLine(kv); + } + } + } + } ``` === "Go" ```go title="hash_map_chaining.go" - [class]{hashMapChaining}-[func]{} + /* チェイン法ハッシュテーブル */ + type hashMapChaining struct { + size int // キーと値のペア数 + capacity int // ハッシュテーブル容量 + loadThres float64 // リサイズを発動する負荷率のしきい値 + extendRatio int // 拡張倍率 + buckets [][]pair // バケット配列 + } + + /* コンストラクタ */ + func newHashMapChaining() *hashMapChaining { + buckets := make([][]pair, 4) + for i := 0; i < 4; i++ { + buckets[i] = make([]pair, 0) + } + return &hashMapChaining{ + size: 0, + capacity: 4, + loadThres: 2.0 / 3.0, + extendRatio: 2, + buckets: buckets, + } + } + + /* ハッシュ関数 */ + func (m *hashMapChaining) hashFunc(key int) int { + return key % m.capacity + } + + /* 負荷率 */ + func (m *hashMapChaining) loadFactor() float64 { + return float64(m.size) / float64(m.capacity) + } + + /* 検索操作 */ + func (m *hashMapChaining) get(key int) string { + idx := m.hashFunc(key) + bucket := m.buckets[idx] + // バケットを走査し、key が見つかれば対応する val を返す + for _, p := range bucket { + if p.key == key { + return p.val + } + } + // key が見つからない場合は空文字列を返す + return "" + } + + /* 追加操作 */ + func (m *hashMapChaining) put(key int, val string) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if m.loadFactor() > m.loadThres { + m.extend() + } + idx := m.hashFunc(key) + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す + for i := range m.buckets[idx] { + if m.buckets[idx][i].key == key { + m.buckets[idx][i].val = val + return + } + } + // その key が存在しなければ、キーと値のペアを末尾に追加 + p := pair{ + key: key, + val: val, + } + m.buckets[idx] = append(m.buckets[idx], p) + m.size += 1 + } + + /* 削除操作 */ + func (m *hashMapChaining) remove(key int) { + idx := m.hashFunc(key) + // バケットを走査してキーと値のペアを削除 + for i, p := range m.buckets[idx] { + if p.key == key { + // スライスから削除する + m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...) + m.size -= 1 + break + } + } + } + + /* ハッシュテーブルを拡張 */ + func (m *hashMapChaining) extend() { + // 元のハッシュテーブルを一時保存 + tmpBuckets := make([][]pair, len(m.buckets)) + for i := 0; i < len(m.buckets); i++ { + tmpBuckets[i] = make([]pair, len(m.buckets[i])) + copy(tmpBuckets[i], m.buckets[i]) + } + // リサイズ後の新しいハッシュテーブルを初期化 + m.capacity *= m.extendRatio + m.buckets = make([][]pair, m.capacity) + for i := 0; i < m.capacity; i++ { + m.buckets[i] = make([]pair, 0) + } + m.size = 0 + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for _, bucket := range tmpBuckets { + for _, p := range bucket { + m.put(p.key, p.val) + } + } + } + + /* ハッシュテーブルを出力 */ + func (m *hashMapChaining) print() { + var builder strings.Builder + + for _, bucket := range m.buckets { + builder.WriteString("[") + for _, p := range bucket { + builder.WriteString(strconv.Itoa(p.key) + " -> " + p.val + " ") + } + builder.WriteString("]") + fmt.Println(builder.String()) + builder.Reset() + } + } ``` === "Swift" ```swift title="hash_map_chaining.swift" - [class]{HashMapChaining}-[func]{} + /* チェイン法ハッシュテーブル */ + class HashMapChaining { + var size: Int // キーと値のペア数 + var capacity: Int // ハッシュテーブル容量 + var loadThres: Double // リサイズを発動する負荷率のしきい値 + var extendRatio: Int // 拡張倍率 + var buckets: [[Pair]] // バケット配列 + + /* コンストラクタ */ + init() { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = Array(repeating: [], count: capacity) + } + + /* ハッシュ関数 */ + func hashFunc(key: Int) -> Int { + key % capacity + } + + /* 負荷率 */ + func loadFactor() -> Double { + Double(size) / Double(capacity) + } + + /* 検索操作 */ + func get(key: Int) -> String? { + let index = hashFunc(key: key) + let bucket = buckets[index] + // バケットを走査し、key が見つかれば対応する val を返す + for pair in bucket { + if pair.key == key { + return pair.val + } + } + // `key` が見つからなければ `nil` を返す + return nil + } + + /* 追加操作 */ + func put(key: Int, val: String) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if loadFactor() > loadThres { + extend() + } + let index = hashFunc(key: key) + let bucket = buckets[index] + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す + for pair in bucket { + if pair.key == key { + pair.val = val + return + } + } + // その key が存在しなければ、キーと値のペアを末尾に追加 + let pair = Pair(key: key, val: val) + buckets[index].append(pair) + size += 1 + } + + /* 削除操作 */ + func remove(key: Int) { + let index = hashFunc(key: key) + let bucket = buckets[index] + // バケットを走査してキーと値のペアを削除 + for (pairIndex, pair) in bucket.enumerated() { + if pair.key == key { + buckets[index].remove(at: pairIndex) + size -= 1 + break + } + } + } + + /* ハッシュテーブルを拡張 */ + func extend() { + // 元のハッシュテーブルを一時保存 + let bucketsTmp = buckets + // リサイズ後の新しいハッシュテーブルを初期化 + capacity *= extendRatio + buckets = Array(repeating: [], count: capacity) + size = 0 + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for bucket in bucketsTmp { + for pair in bucket { + put(key: pair.key, val: pair.val) + } + } + } + + /* ハッシュテーブルを出力 */ + func print() { + for bucket in buckets { + let res = bucket.map { "\($0.key) -> \($0.val)" } + Swift.print(res) + } + } + } ``` === "JS" ```javascript title="hash_map_chaining.js" - [class]{HashMapChaining}-[func]{} + /* チェイン法ハッシュテーブル */ + class HashMapChaining { + #size; // キーと値のペア数 + #capacity; // ハッシュテーブル容量 + #loadThres; // リサイズを発動する負荷率のしきい値 + #extendRatio; // 拡張倍率 + #buckets; // バケット配列 + + /* コンストラクタ */ + constructor() { + this.#size = 0; + this.#capacity = 4; + this.#loadThres = 2.0 / 3.0; + this.#extendRatio = 2; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + } + + /* ハッシュ関数 */ + #hashFunc(key) { + return key % this.#capacity; + } + + /* 負荷率 */ + #loadFactor() { + return this.#size / this.#capacity; + } + + /* 検索操作 */ + get(key) { + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // バケットを走査し、key が見つかれば対応する val を返す + for (const pair of bucket) { + if (pair.key === key) { + return pair.val; + } + } + // key が見つからない場合は null を返す + return null; + } + + /* 追加操作 */ + put(key, val) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す + for (const pair of bucket) { + if (pair.key === key) { + pair.val = val; + return; + } + } + // その key が存在しなければ、キーと値のペアを末尾に追加 + const pair = new Pair(key, val); + bucket.push(pair); + this.#size++; + } + + /* 削除操作 */ + remove(key) { + const index = this.#hashFunc(key); + let bucket = this.#buckets[index]; + // バケットを走査してキーと値のペアを削除 + for (let i = 0; i < bucket.length; i++) { + if (bucket[i].key === key) { + bucket.splice(i, 1); + this.#size--; + break; + } + } + } + + /* ハッシュテーブルを拡張 */ + #extend() { + // 元のハッシュテーブルを一時保存 + const bucketsTmp = this.#buckets; + // リサイズ後の新しいハッシュテーブルを初期化 + this.#capacity *= this.#extendRatio; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + this.#size = 0; + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for (const bucket of bucketsTmp) { + for (const pair of bucket) { + this.put(pair.key, pair.val); + } + } + } + + /* ハッシュテーブルを出力 */ + print() { + for (const bucket of this.#buckets) { + let res = []; + for (const pair of bucket) { + res.push(pair.key + ' -> ' + pair.val); + } + console.log(res); + } + } + } ``` === "TS" ```typescript title="hash_map_chaining.ts" - [class]{HashMapChaining}-[func]{} + /* チェイン法ハッシュテーブル */ + class HashMapChaining { + #size: number; // キーと値のペア数 + #capacity: number; // ハッシュテーブル容量 + #loadThres: number; // リサイズを発動する負荷率のしきい値 + #extendRatio: number; // 拡張倍率 + #buckets: Pair[][]; // バケット配列 + + /* コンストラクタ */ + constructor() { + this.#size = 0; + this.#capacity = 4; + this.#loadThres = 2.0 / 3.0; + this.#extendRatio = 2; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + } + + /* ハッシュ関数 */ + #hashFunc(key: number): number { + return key % this.#capacity; + } + + /* 負荷率 */ + #loadFactor(): number { + return this.#size / this.#capacity; + } + + /* 検索操作 */ + get(key: number): string | null { + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // バケットを走査し、key が見つかれば対応する val を返す + for (const pair of bucket) { + if (pair.key === key) { + return pair.val; + } + } + // key が見つからない場合は null を返す + return null; + } + + /* 追加操作 */ + put(key: number, val: string): void { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す + for (const pair of bucket) { + if (pair.key === key) { + pair.val = val; + return; + } + } + // その key が存在しなければ、キーと値のペアを末尾に追加 + const pair = new Pair(key, val); + bucket.push(pair); + this.#size++; + } + + /* 削除操作 */ + remove(key: number): void { + const index = this.#hashFunc(key); + let bucket = this.#buckets[index]; + // バケットを走査してキーと値のペアを削除 + for (let i = 0; i < bucket.length; i++) { + if (bucket[i].key === key) { + bucket.splice(i, 1); + this.#size--; + break; + } + } + } + + /* ハッシュテーブルを拡張 */ + #extend(): void { + // 元のハッシュテーブルを一時保存 + const bucketsTmp = this.#buckets; + // リサイズ後の新しいハッシュテーブルを初期化 + this.#capacity *= this.#extendRatio; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + this.#size = 0; + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for (const bucket of bucketsTmp) { + for (const pair of bucket) { + this.put(pair.key, pair.val); + } + } + } + + /* ハッシュテーブルを出力 */ + print(): void { + for (const bucket of this.#buckets) { + let res = []; + for (const pair of bucket) { + res.push(pair.key + ' -> ' + pair.val); + } + console.log(res); + } + } + } ``` === "Dart" ```dart title="hash_map_chaining.dart" - [class]{HashMapChaining}-[func]{} + /* チェイン法ハッシュテーブル */ + class HashMapChaining { + late int size; // キーと値のペア数 + late int capacity; // ハッシュテーブル容量 + late double loadThres; // リサイズを発動する負荷率のしきい値 + late int extendRatio; // 拡張倍率 + late List> buckets; // バケット配列 + + /* コンストラクタ */ + HashMapChaining() { + size = 0; + capacity = 4; + loadThres = 2.0 / 3.0; + extendRatio = 2; + buckets = List.generate(capacity, (_) => []); + } + + /* ハッシュ関数 */ + int hashFunc(int key) { + return key % capacity; + } + + /* 負荷率 */ + double loadFactor() { + return size / capacity; + } + + /* 検索操作 */ + String? get(int key) { + int index = hashFunc(key); + List bucket = buckets[index]; + // バケットを走査し、key が見つかれば対応する val を返す + for (Pair pair in bucket) { + if (pair.key == key) { + return pair.val; + } + } + // key が見つからない場合は null を返す + return null; + } + + /* 追加操作 */ + void put(int key, String val) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (loadFactor() > loadThres) { + extend(); + } + int index = hashFunc(key); + List bucket = buckets[index]; + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す + for (Pair pair in bucket) { + if (pair.key == key) { + pair.val = val; + return; + } + } + // その key が存在しなければ、キーと値のペアを末尾に追加 + Pair pair = Pair(key, val); + bucket.add(pair); + size++; + } + + /* 削除操作 */ + void remove(int key) { + int index = hashFunc(key); + List bucket = buckets[index]; + // バケットを走査してキーと値のペアを削除 + for (Pair pair in bucket) { + if (pair.key == key) { + bucket.remove(pair); + size--; + break; + } + } + } + + /* ハッシュテーブルを拡張 */ + void extend() { + // 元のハッシュテーブルを一時保存 + List> bucketsTmp = buckets; + // リサイズ後の新しいハッシュテーブルを初期化 + capacity *= extendRatio; + buckets = List.generate(capacity, (_) => []); + size = 0; + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for (List bucket in bucketsTmp) { + for (Pair pair in bucket) { + put(pair.key, pair.val); + } + } + } + + /* ハッシュテーブルを出力 */ + void printHashMap() { + for (List bucket in buckets) { + List res = []; + for (Pair pair in bucket) { + res.add("${pair.key} -> ${pair.val}"); + } + print(res); + } + } + } ``` === "Rust" ```rust title="hash_map_chaining.rs" - [class]{HashMapChaining}-[func]{} + /* チェイン法ハッシュテーブル */ + struct HashMapChaining { + size: usize, + capacity: usize, + load_thres: f32, + extend_ratio: usize, + buckets: Vec>, + } + + impl HashMapChaining { + /* コンストラクタ */ + fn new() -> Self { + Self { + size: 0, + capacity: 4, + load_thres: 2.0 / 3.0, + extend_ratio: 2, + buckets: vec![vec![]; 4], + } + } + + /* ハッシュ関数 */ + fn hash_func(&self, key: i32) -> usize { + key as usize % self.capacity + } + + /* 負荷率 */ + fn load_factor(&self) -> f32 { + self.size as f32 / self.capacity as f32 + } + + /* 削除操作 */ + fn remove(&mut self, key: i32) -> Option { + let index = self.hash_func(key); + + // バケットを走査してキーと値のペアを削除 + for (i, p) in self.buckets[index].iter_mut().enumerate() { + if p.key == key { + let pair = self.buckets[index].remove(i); + self.size -= 1; + return Some(pair.val); + } + } + + // key が見つからない場合は None を返す + None + } + + /* ハッシュテーブルを拡張 */ + fn extend(&mut self) { + // 元のハッシュテーブルを一時保存 + let buckets_tmp = std::mem::take(&mut self.buckets); + + // リサイズ後の新しいハッシュテーブルを初期化 + self.capacity *= self.extend_ratio; + self.buckets = vec![Vec::new(); self.capacity as usize]; + self.size = 0; + + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for bucket in buckets_tmp { + for pair in bucket { + self.put(pair.key, pair.val); + } + } + } + + /* ハッシュテーブルを出力 */ + fn print(&self) { + for bucket in &self.buckets { + let mut res = Vec::new(); + for pair in bucket { + res.push(format!("{} -> {}", pair.key, pair.val)); + } + println!("{:?}", res); + } + } + + /* 追加操作 */ + fn put(&mut self, key: i32, val: String) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if self.load_factor() > self.load_thres { + self.extend(); + } + + let index = self.hash_func(key); + + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す + for pair in self.buckets[index].iter_mut() { + if pair.key == key { + pair.val = val; + return; + } + } + + // その key が存在しなければ、キーと値のペアを末尾に追加 + let pair = Pair { key, val }; + self.buckets[index].push(pair); + self.size += 1; + } + + /* 検索操作 */ + fn get(&self, key: i32) -> Option<&str> { + let index = self.hash_func(key); + + // バケットを走査し、key が見つかれば対応する val を返す + for pair in self.buckets[index].iter() { + if pair.key == key { + return Some(&pair.val); + } + } + + // key が見つからない場合は None を返す + None + } + } ``` === "C" ```c title="hash_map_chaining.c" - [class]{Node}-[func]{} + /* 連結リストノード */ + typedef struct Node { + Pair *pair; + struct Node *next; + } Node; - [class]{HashMapChaining}-[func]{} + /* チェイン法ハッシュテーブル */ + typedef struct { + int size; // キーと値のペア数 + int capacity; // ハッシュテーブル容量 + double loadThres; // リサイズを発動する負荷率のしきい値 + int extendRatio; // 拡張倍率 + Node **buckets; // バケット配列 + } HashMapChaining; + + /* コンストラクタ */ + HashMapChaining *newHashMapChaining() { + HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining)); + hashMap->size = 0; + hashMap->capacity = 4; + hashMap->loadThres = 2.0 / 3.0; + hashMap->extendRatio = 2; + hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *)); + for (int i = 0; i < hashMap->capacity; i++) { + hashMap->buckets[i] = NULL; + } + return hashMap; + } + + /* デストラクタ */ + void delHashMapChaining(HashMapChaining *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Node *cur = hashMap->buckets[i]; + while (cur) { + Node *tmp = cur; + cur = cur->next; + free(tmp->pair); + free(tmp); + } + } + free(hashMap->buckets); + free(hashMap); + } + + /* ハッシュ関数 */ + int hashFunc(HashMapChaining *hashMap, int key) { + return key % hashMap->capacity; + } + + /* 負荷率 */ + double loadFactor(HashMapChaining *hashMap) { + return (double)hashMap->size / (double)hashMap->capacity; + } + + /* 検索操作 */ + char *get(HashMapChaining *hashMap, int key) { + int index = hashFunc(hashMap, key); + // バケットを走査し、key が見つかれば対応する val を返す + Node *cur = hashMap->buckets[index]; + while (cur) { + if (cur->pair->key == key) { + return cur->pair->val; + } + cur = cur->next; + } + return ""; // key が見つからない場合は空文字列を返す + } + + /* 追加操作 */ + void put(HashMapChaining *hashMap, int key, const char *val) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (loadFactor(hashMap) > hashMap->loadThres) { + extend(hashMap); + } + int index = hashFunc(hashMap, key); + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す + Node *cur = hashMap->buckets[index]; + while (cur) { + if (cur->pair->key == key) { + strcpy(cur->pair->val, val); // 指定した `key` に遭遇したら、対応する `val` を更新して返す + return; + } + cur = cur->next; + } + // 該当する `key` がなければ、キーと値のペアを連結リストの先頭に追加する + Pair *newPair = (Pair *)malloc(sizeof(Pair)); + newPair->key = key; + strcpy(newPair->val, val); + Node *newNode = (Node *)malloc(sizeof(Node)); + newNode->pair = newPair; + newNode->next = hashMap->buckets[index]; + hashMap->buckets[index] = newNode; + hashMap->size++; + } + + /* ハッシュテーブルを拡張 */ + void extend(HashMapChaining *hashMap) { + // 元のハッシュテーブルを一時保存 + int oldCapacity = hashMap->capacity; + Node **oldBuckets = hashMap->buckets; + // リサイズ後の新しいハッシュテーブルを初期化 + hashMap->capacity *= hashMap->extendRatio; + hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *)); + for (int i = 0; i < hashMap->capacity; i++) { + hashMap->buckets[i] = NULL; + } + hashMap->size = 0; + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for (int i = 0; i < oldCapacity; i++) { + Node *cur = oldBuckets[i]; + while (cur) { + put(hashMap, cur->pair->key, cur->pair->val); + Node *temp = cur; + cur = cur->next; + // メモリを解放する + free(temp->pair); + free(temp); + } + } + + free(oldBuckets); + } + + /* 削除操作 */ + void removeItem(HashMapChaining *hashMap, int key) { + int index = hashFunc(hashMap, key); + Node *cur = hashMap->buckets[index]; + Node *pre = NULL; + while (cur) { + if (cur->pair->key == key) { + // そこからキーと値の組を削除する + if (pre) { + pre->next = cur->next; + } else { + hashMap->buckets[index] = cur->next; + } + // メモリを解放する + free(cur->pair); + free(cur); + hashMap->size--; + return; + } + pre = cur; + cur = cur->next; + } + } + + /* ハッシュテーブルを出力 */ + void print(HashMapChaining *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Node *cur = hashMap->buckets[i]; + printf("["); + while (cur) { + printf("%d -> %s, ", cur->pair->key, cur->pair->val); + cur = cur->next; + } + printf("]\n"); + } + } ``` === "Kotlin" ```kotlin title="hash_map_chaining.kt" - [class]{HashMapChaining}-[func]{} + /* チェイン法ハッシュテーブル */ + class HashMapChaining { + var size: Int // キーと値のペア数 + var capacity: Int // ハッシュテーブル容量 + val loadThres: Double // リサイズを発動する負荷率のしきい値 + val extendRatio: Int // 拡張倍率 + var buckets: MutableList> // バケット配列 + + /* コンストラクタ */ + init { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = mutableListOf() + for (i in 0.. loadThres) { + extend() + } + val index = hashFunc(key) + val bucket = buckets[index] + // バケットを走査し、指定した key が見つかれば対応する val を更新して返す + for (pair in bucket) { + if (pair.key == key) { + pair._val = _val + return + } + } + // その key が存在しなければ、キーと値のペアを末尾に追加 + val pair = Pair(key, _val) + bucket.add(pair) + size++ + } + + /* 削除操作 */ + fun remove(key: Int) { + val index = hashFunc(key) + val bucket = buckets[index] + // バケットを走査してキーと値のペアを削除 + for (pair in bucket) { + if (pair.key == key) { + bucket.remove(pair) + size-- + break + } + } + } + + /* ハッシュテーブルを拡張 */ + fun extend() { + // 元のハッシュテーブルを一時保存 + val bucketsTmp = buckets + // リサイズ後の新しいハッシュテーブルを初期化 + capacity *= extendRatio + // mutablelist には固定サイズがない + buckets = mutableListOf() + for (i in 0..() + for (pair in bucket) { + val k = pair.key + val v = pair._val + res.add("$k -> $v") + } + println(res) + } + } + } ``` === "Ruby" ```ruby title="hash_map_chaining.rb" - [class]{HashMapChaining}-[func]{} + ### キーアドレス法ハッシュテーブル ### + class HashMapChaining + ### コンストラクタ ### + def initialize + @size = 0 # キーと値のペア数 + @capacity = 4 # ハッシュテーブル容量 + @load_thres = 2.0 / 3.0 # リサイズを発動する負荷率のしきい値 + @extend_ratio = 2 # 拡張倍率 + @buckets = Array.new(@capacity) { [] } # バケット配列 + end + + ### ハッシュ関数 ### + def hash_func(key) + key % @capacity + end + + ### 負荷率 ### + def load_factor + @size / @capacity + end + + ### 検索操作 ### + def get(key) + index = hash_func(key) + bucket = @buckets[index] + # バケットを走査し、key が見つかれば対応する val を返す + for pair in bucket + return pair.val if pair.key == key + end + # `key` が見つからなければ `nil` を返す + nil + end + + ### 追加操作 ### + def put(key, val) + # 負荷率がしきい値を超えたら、リサイズを実行 + extend if load_factor > @load_thres + index = hash_func(key) + bucket = @buckets[index] + # バケットを走査し、指定した key が見つかれば対応する val を更新して返す + for pair in bucket + if pair.key == key + pair.val = val + return + end + end + # その key が存在しなければ、キーと値のペアを末尾に追加 + pair = Pair.new(key, val) + bucket << pair + @size += 1 + end + + ### 削除操作 ### + def remove(key) + index = hash_func(key) + bucket = @buckets[index] + # バケットを走査してキーと値のペアを削除 + for pair in bucket + if pair.key == key + bucket.delete(pair) + @size -= 1 + break + end + end + end + + ### ハッシュテーブルを拡張 ### + def extend + # 元のハッシュテーブルを一時保存 + buckets = @buckets + # リサイズ後の新しいハッシュテーブルを初期化 + @capacity *= @extend_ratio + @buckets = Array.new(@capacity) { [] } + @size = 0 + # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for bucket in buckets + for pair in bucket + put(pair.key, pair.val) + end + end + end + + ### ハッシュテーブルを出力 ### + def print + for bucket in @buckets + res = [] + for pair in bucket + res << "#{pair.key} -> #{pair.val}" + end + pp res + end + end + end ``` -連結リストが非常に長い場合、クエリ効率$O(n)$が悪いことは注目に値します。**この場合、リストを「AVL木」または「赤黒木」に変換して**、クエリ操作の時間計算量を$O(\log n)$に最適化できます。 +??? pythontutor "コードの可視化" -## 6.2.2   オープンアドレス法 +
+ -オープンアドレス法は追加のデータ構造を導入せず、代わりに「複数回プローブ」を通してハッシュ衝突を処理します。プローブ方法には主に線形プローブ、二次プローブ、二重ハッシュがあります。 +注意すべきなのは、連結リストが長い場合、検索効率 $O(n)$ は非常に低いことです。**このとき、連結リストを「AVL 木」または「赤黒木」に変換することで**、検索操作の時間計算量を $O(\log n)$ に最適化できます。 -線形プローブを例にして、オープンアドレス法ハッシュ表のメカニズムを紹介しましょう。 +## 6.2.2   オープンアドレッシング -### 1.   線形プローブ +オープンアドレッシング(open addressing)では追加のデータ構造を導入せず、「複数回の探索」によってハッシュ衝突を処理します。探索方法には主に線形探索、二次探索、多重ハッシュなどがあります。 -線形プローブは固定ステップの線形検索をプローブに使用し、通常のハッシュ表とは異なります。 +以下では線形探索を例に、オープンアドレッシングハッシュテーブルの動作の仕組みを説明します。 -- **要素の挿入**: ハッシュ関数を使用してバケットインデックスを計算します。バケットに既に要素が含まれている場合、衝突位置から線形に前方に走査し(通常ステップサイズは$1$)、空のバケットが見つかるまで進み、要素を挿入します。 -- **要素の検索**: ハッシュ衝突に遭遇した場合、同じステップサイズを使用して線形に前方に走査し、対応する要素が見つかったら`value`を返します。空のバケットに遭遇した場合、ターゲット要素がハッシュ表にないことを意味するため、`None`を返します。 +### 1.   線形探索 -下図はオープンアドレス法(線形プローブ)ハッシュ表におけるキー値ペアの分布を示しています。このハッシュ関数によると、下二桁が同じキーは同じバケットにマッピングされます。線形プローブを通して、それらはそのバケットとその下のバケットに順次格納されます。 +線形探索では、固定ステップ長の線形探索によって探索を行います。その操作方法は通常のハッシュテーブルとは異なります。 -![オープンアドレス法(線形プローブ)ハッシュ表におけるキー値ペアの分布](hash_collision.assets/hash_table_linear_probing.png){ class="animation-figure" } +- **要素の挿入**:ハッシュ関数によってバケットインデックスを計算し、バケット内にすでに要素がある場合は、衝突位置から後方へ線形に走査し(ステップ長は通常 $1$ )、空のバケットが見つかるまで進み、その中に要素を挿入します。 +- **要素の検索**:ハッシュ衝突が見つかった場合は、同じステップ長で後方へ線形走査を行い、対応する要素が見つかるまで続け、 `value` を返します。空のバケットに遭遇した場合は、対象要素がハッシュテーブル内に存在しないことを意味するため、 `None` を返します。 -

図 6-6   オープンアドレス法(線形プローブ)ハッシュ表におけるキー値ペアの分布

+下図はオープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布を示しています。このハッシュ関数では、末尾 2 桁が同じ `key` はすべて同じバケットに写像されます。線形探索によって、それらはそのバケットとその後続のバケットに順に格納されます。 -しかし、**線形プローブは「クラスタリング」を作りやすい傾向があります**。具体的には、配列内の連続的に占有された位置が長いほど、これらの連続した位置でハッシュ衝突が発生する確率が高くなり、その位置でのクラスタリングの成長をさらに促進し、悪循環を形成し、最終的に挿入、削除、クエリ、更新操作の効率低下につながります。 +![オープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布](hash_collision.assets/hash_table_linear_probing.png){ class="animation-figure" } -**オープンアドレス法ハッシュ表では要素を直接削除できない**ことに注意することが重要です。要素を削除すると、配列に空のバケット`None`が作成されます。要素を検索する際、線形プローブがこの空のバケットに遭遇すると戻ってしまい、このバケットの下の要素にアクセスできなくなります。プログラムはこれらの要素が存在しないと誤って仮定する可能性があります。下図に示すとおりです。 +

図 6-6   オープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布

-![オープンアドレス法での削除によるクエリ問題](hash_collision.assets/hash_table_open_addressing_deletion.png){ class="animation-figure" } +しかし、**線形探索では「クラスタリング現象」が起こりやすい**です。具体的には、配列内で連続して占有された位置が長いほど、それらの連続位置でハッシュ衝突が発生する可能性が高くなり、さらにその位置の集積成長を促して悪循環を生み、最終的には追加・削除・検索・更新操作の効率低下を招きます。 -

図 6-7   オープンアドレス法での削除によるクエリ問題

+注意すべきなのは、**オープンアドレッシングハッシュテーブルでは要素を直接削除できない**ことです。これは、要素を削除すると配列内に空バケット `None` が生じ、要素を検索するときに線形探索がその空バケットに到達した時点で返ってしまうため、その空バケットより後ろの要素には二度とアクセスできなくなるからです。結果として、プログラムがそれらの要素を存在しないと誤判定する可能性があります。下図のとおりです。 -この問題を解決するために、遅延削除メカニズムを採用できます:ハッシュ表から要素を直接削除する代わりに、**定数`TOMBSTONE`を使用してバケットをマークします**。このメカニズムでは、`None`と`TOMBSTONE`の両方が空のバケットを表し、キー値ペアを保持できます。ただし、線形プローブが`TOMBSTONE`に遭遇した場合、その下にまだキー値ペアがある可能性があるため、走査を続ける必要があります。 +![オープンアドレッシングで要素を削除したことによる検索問題](hash_collision.assets/hash_table_open_addressing_deletion.png){ class="animation-figure" } -しかし、**遅延削除はハッシュ表の性能劣化を加速する可能性があります**。削除操作のたびに削除マークが生成され、`TOMBSTONE`が増加すると、線形プローブがターゲット要素を見つけるために複数の`TOMBSTONE`をスキップする必要がある可能性があるため、検索時間も増加します。 +

図 6-7   オープンアドレッシングで要素を削除したことによる検索問題

-これに対処するために、線形プローブ中に最初に遭遇した`TOMBSTONE`のインデックスを記録し、検索されたターゲット要素とその`TOMBSTONE`の位置を交換することを検討してください。これを行う利点は、要素がクエリまたは追加されるたびに、要素がその理想的な位置(プローブの開始点)により近いバケットに移動され、クエリ効率が最適化されることです。 +この問題を解決するために、遅延削除(lazy deletion)の仕組みを採用できます。これは要素をハッシュテーブルから直接取り除かず、**代わりに定数 `TOMBSTONE` を使ってこのバケットをマークします**。この仕組みでは、`None` と `TOMBSTONE` はどちらも空バケットを表し、どちらにもキーと値のペアを配置できます。ただし異なるのは、線形探索が `TOMBSTONE` に到達した場合は、その先にキーと値のペアが存在する可能性があるため、探索を続けるべきだという点です。 -以下のコードは、遅延削除を使用したオープンアドレス法(線形プローブ)ハッシュ表を実装しています。ハッシュ表の空間をより有効に活用するために、ハッシュ表を「循環配列」として扱います。配列の終わりを超えると、最初に戻って走査を続けます。 +しかし、**遅延削除はハッシュテーブルの性能劣化を加速させる可能性があります**。これは、削除操作のたびに削除マークが 1 つ生成され、`TOMBSTONE` が増えるにつれて探索時間も増加するためです。線形探索では、対象要素を見つけるまでに複数の `TOMBSTONE` を飛び越える必要があるかもしれません。 + +そのため、線形探索では、遭遇した最初の `TOMBSTONE` のインデックスを記録し、見つかった対象要素とその `TOMBSTONE` を交換することを考えます。こうする利点は、要素を検索または追加するたびに、要素が理想位置(探索開始点)により近いバケットへ移動し、検索効率が向上することです。 + +以下のコードは、遅延削除を含むオープンアドレッシング(線形探索)ハッシュテーブルを実装したものです。ハッシュテーブルの空間をより十分に活用するために、ハッシュテーブルを「環状配列」とみなし、配列末尾を越えたら先頭に戻って探索を続けます。 === "Python" @@ -459,12 +1567,12 @@ comments: true def __init__(self): """コンストラクタ""" - self.size = 0 # キー値ペアの数 - self.capacity = 4 # ハッシュテーブルの容量 - self.load_thres = 2.0 / 3.0 # 拡張をトリガーする負荷率の閾値 - self.extend_ratio = 2 # 拡張の倍数 + self.size = 0 # キーと値のペア数 + self.capacity = 4 # ハッシュテーブル容量 + self.load_thres = 2.0 / 3.0 # リサイズを発動する負荷率のしきい値 + self.extend_ratio = 2 # 拡張倍率 self.buckets: list[Pair | None] = [None] * self.capacity # バケット配列 - self.TOMBSTONE = Pair(-1, "-1") # 削除マーク + self.TOMBSTONE = Pair(-1, "-1") # 削除済みマーク def hash_func(self, key: int) -> int: """ハッシュ関数""" @@ -475,70 +1583,70 @@ comments: true return self.size / self.capacity def find_bucket(self, key: int) -> int: - """key に対応するバケットインデックスを検索""" + """key に対応するバケットインデックスを探す""" index = self.hash_func(key) first_tombstone = -1 - # 線形探査、空のバケットに遭遇したらブレーク + # 線形プロービングを行い、空バケットに達したら終了 while self.buckets[index] is not None: - # キーに遭遇した場合、対応するバケットインデックスを返す + # key が見つかったら、対応するバケットのインデックスを返す if self.buckets[index].key == key: - # 削除マークが以前に遭遇していた場合、キー値ペアをそのインデックスに移動 + # 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 if first_tombstone != -1: self.buckets[first_tombstone] = self.buckets[index] self.buckets[index] = self.TOMBSTONE - return first_tombstone # 移動されたバケットインデックスを返す - return index # バケットインデックスを返す - # 最初に遭遇した削除マークを記録 + return first_tombstone # 移動後のバケットインデックスを返す + return index # バケットのインデックスを返す + # 最初に見つかった削除マークを記録 if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE: first_tombstone = index - # バケットインデックスを計算、末尾を超えた場合は先頭に戻る + # バケットのインデックスを計算し、末尾を越えたら先頭に戻る index = (index + 1) % self.capacity - # キーが存在しない場合、挿入ポイントのインデックスを返す + # key が存在しない場合は追加位置のインデックスを返す return index if first_tombstone == -1 else first_tombstone def get(self, key: int) -> str: - """照会操作""" - # key に対応するバケットインデックスを検索 + """検索操作""" + # key に対応するバケットインデックスを探す index = self.find_bucket(key) - # キー値ペアが見つかれば、対応する val を返す + # キーと値の組が見つかったら、対応する val を返す if self.buckets[index] not in [None, self.TOMBSTONE]: return self.buckets[index].val - # キー値ペアが存在しない場合、None を返す + # キーと値のペアが存在しない場合は `None` を返す return None def put(self, key: int, val: str): """追加操作""" - # 負荷率が閾値を超えた場合、拡張を実行 + # 負荷率がしきい値を超えたら、リサイズを実行 if self.load_factor() > self.load_thres: self.extend() - # key に対応するバケットインデックスを検索 + # key に対応するバケットインデックスを探す index = self.find_bucket(key) - # キー値ペアが見つかれば、val を上書きして返す + # キーと値の組が見つかったら、val を上書きして返す if self.buckets[index] not in [None, self.TOMBSTONE]: self.buckets[index].val = val return - # キー値ペアが存在しない場合、キー値ペアを追加 + # キーと値の組が存在しない場合は、その組を追加する self.buckets[index] = Pair(key, val) self.size += 1 def remove(self, key: int): """削除操作""" - # key に対応するバケットインデックスを検索 + # key に対応するバケットインデックスを探す index = self.find_bucket(key) - # キー値ペアが見つかれば、削除マークで覆う + # キーと値の組が見つかったら、削除マーカーで上書きする if self.buckets[index] not in [None, self.TOMBSTONE]: self.buckets[index] = self.TOMBSTONE self.size -= 1 def extend(self): """ハッシュテーブルを拡張""" - # 元のハッシュテーブルを一時的に保存 + # 元のハッシュテーブルを一時保存 buckets_tmp = self.buckets - # 拡張された新しいハッシュテーブルを初期化 + # リサイズ後の新しいハッシュテーブルを初期化 self.capacity *= self.extend_ratio self.buckets = [None] * self.capacity self.size = 0 - # 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動 + # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す for pair in buckets_tmp: if pair not in [None, self.TOMBSTONE]: self.put(pair.key, pair.val) @@ -560,19 +1668,19 @@ comments: true /* オープンアドレス法ハッシュテーブル */ class HashMapOpenAddressing { private: - int size; // キー値ペアの数 - int capacity = 4; // ハッシュテーブルの容量 - const double loadThres = 2.0 / 3.0; // 拡張をトリガーする負荷率の閾値 + int size; // キーと値のペア数 + int capacity = 4; // ハッシュテーブル容量 + const double loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値 const int extendRatio = 2; // 拡張倍率 vector buckets; // バケット配列 - Pair *TOMBSTONE = new Pair(-1, "-1"); // 削除マーク + Pair *TOMBSTONE = new Pair(-1, "-1"); // 削除済みマーク public: /* コンストラクタ */ HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) { } - /* デストラクタ */ + /* デストラクタメソッド */ ~HashMapOpenAddressing() { for (Pair *pair : buckets) { if (pair != nullptr && pair != TOMBSTONE) { @@ -592,68 +1700,68 @@ comments: true return (double)size / capacity; } - /* keyに対応するバケットインデックスを検索 */ + /* key に対応するバケットインデックスを探す */ int findBucket(int key) { int index = hashFunc(key); int firstTombstone = -1; - // 線形探査、空のバケットに遭遇したら中断 + // 線形プロービングを行い、空バケットに達したら終了 while (buckets[index] != nullptr) { - // keyに遭遇した場合、対応するバケットインデックスを返却 + // key が見つかったら、対応するバケットのインデックスを返す if (buckets[index]->key == key) { - // 以前に削除マークに遭遇していた場合、キー値ペアをそのインデックスに移動 + // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 if (firstTombstone != -1) { buckets[firstTombstone] = buckets[index]; buckets[index] = TOMBSTONE; - return firstTombstone; // 移動されたバケットインデックスを返却 + return firstTombstone; // 移動後のバケットインデックスを返す } - return index; // バケットインデックスを返却 + return index; // バケットのインデックスを返す } - // 最初に遭遇した削除マークを記録 + // 最初に見つかった削除マークを記録 if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { firstTombstone = index; } - // バケットインデックスを計算、末尾を超えた場合は先頭に戻る + // バケットのインデックスを計算し、末尾を越えたら先頭に戻る index = (index + 1) % capacity; } - // keyが存在しない場合、挿入ポイントのインデックスを返却 + // key が存在しない場合は追加位置のインデックスを返す return firstTombstone == -1 ? index : firstTombstone; } - /* クエリ操作 */ + /* 検索操作 */ string get(int key) { - // keyに対応するバケットインデックスを検索 + // key に対応するバケットインデックスを探す int index = findBucket(key); - // キー値ペアが見つかった場合、対応するvalを返却 + // キーと値の組が見つかったら、対応する val を返す if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) { return buckets[index]->val; } - // キー値ペアが存在しない場合、空文字列を返却 + // キーと値の組が存在しない場合は空文字列を返す return ""; } /* 追加操作 */ void put(int key, string val) { - // 負荷率が閾値を超えた場合、拡張を実行 + // 負荷率がしきい値を超えたら、リサイズを実行 if (loadFactor() > loadThres) { extend(); } - // keyに対応するバケットインデックスを検索 + // key に対応するバケットインデックスを探す int index = findBucket(key); - // キー値ペアが見つかった場合、valを上書きして返却 + // キーと値の組が見つかったら、val を上書きして返す if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) { buckets[index]->val = val; return; } - // キー値ペアが存在しない場合、キー値ペアを追加 + // キーと値の組が存在しない場合は、その組を追加する buckets[index] = new Pair(key, val); size++; } /* 削除操作 */ void remove(int key) { - // keyに対応するバケットインデックスを検索 + // key に対応するバケットインデックスを探す int index = findBucket(key); - // キー値ペアが見つかった場合、削除マークで覆う + // キーと値の組が見つかったら、削除マーカーで上書きする if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) { delete buckets[index]; buckets[index] = TOMBSTONE; @@ -665,11 +1773,11 @@ comments: true void extend() { // 元のハッシュテーブルを一時保存 vector bucketsTmp = buckets; - // 拡張された新しいハッシュテーブルを初期化 + // リサイズ後の新しいハッシュテーブルを初期化 capacity *= extendRatio; buckets = vector(capacity, nullptr); size = 0; - // 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動 + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す for (Pair *pair : bucketsTmp) { if (pair != nullptr && pair != TOMBSTONE) { put(pair->key, pair->val); @@ -678,7 +1786,7 @@ comments: true } } - /* ハッシュテーブルを印刷 */ + /* ハッシュテーブルを出力 */ void print() { for (Pair *pair : buckets) { if (pair == nullptr) { @@ -698,12 +1806,12 @@ comments: true ```java title="hash_map_open_addressing.java" /* オープンアドレス法ハッシュテーブル */ class HashMapOpenAddressing { - private int size; // キー値ペアの数 - private int capacity = 4; // ハッシュテーブルの容量 - private final double loadThres = 2.0 / 3.0; // 拡張をトリガーする負荷率の閾値 + private int size; // キーと値のペア数 + private int capacity = 4; // ハッシュテーブル容量 + private final double loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値 private final int extendRatio = 2; // 拡張倍率 private Pair[] buckets; // バケット配列 - private final Pair TOMBSTONE = new Pair(-1, "-1"); // 削除マーク + private final Pair TOMBSTONE = new Pair(-1, "-1"); // 削除済みマーク /* コンストラクタ */ public HashMapOpenAddressing() { @@ -721,68 +1829,68 @@ comments: true return (double) size / capacity; } - /* keyに対応するバケットインデックスを検索 */ + /* key に対応するバケットインデックスを探す */ private int findBucket(int key) { int index = hashFunc(key); int firstTombstone = -1; - // 線形探査、空のバケットに遭遇したら終了 + // 線形プロービングを行い、空バケットに達したら終了 while (buckets[index] != null) { - // keyに遭遇した場合、対応するバケットインデックスを返す + // key が見つかったら、対応するバケットのインデックスを返す if (buckets[index].key == key) { - // 以前に削除マークに遭遇していた場合、キー値ペアをそのインデックスに移動 + // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 if (firstTombstone != -1) { buckets[firstTombstone] = buckets[index]; buckets[index] = TOMBSTONE; return firstTombstone; // 移動後のバケットインデックスを返す } - return index; // バケットインデックスを返す + return index; // バケットのインデックスを返す } - // 最初に遭遇した削除マークを記録 + // 最初に見つかった削除マークを記録 if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { firstTombstone = index; } - // バケットインデックスを計算、末尾を超えた場合は先頭に戻る + // バケットのインデックスを計算し、末尾を越えたら先頭に戻る index = (index + 1) % capacity; } - // keyが存在しない場合、挿入ポイントのインデックスを返す + // key が存在しない場合は追加位置のインデックスを返す return firstTombstone == -1 ? index : firstTombstone; } - /* クエリ操作 */ + /* 検索操作 */ public String get(int key) { - // keyに対応するバケットインデックスを検索 + // key に対応するバケットインデックスを探す int index = findBucket(key); - // キー値ペアが見つかった場合、対応するvalを返す + // キーと値の組が見つかったら、対応する val を返す if (buckets[index] != null && buckets[index] != TOMBSTONE) { return buckets[index].val; } - // キー値ペアが存在しない場合、nullを返す + // キーと値の組が存在しなければ null を返す return null; } /* 追加操作 */ public void put(int key, String val) { - // 負荷率が閾値を超えた場合、拡張を実行 + // 負荷率がしきい値を超えたら、リサイズを実行 if (loadFactor() > loadThres) { extend(); } - // keyに対応するバケットインデックスを検索 + // key に対応するバケットインデックスを探す int index = findBucket(key); - // キー値ペアが見つかった場合、valを上書きして戻る + // キーと値の組が見つかったら、val を上書きして返す if (buckets[index] != null && buckets[index] != TOMBSTONE) { buckets[index].val = val; return; } - // キー値ペアが存在しない場合、キー値ペアを追加 + // キーと値の組が存在しない場合は、その組を追加する buckets[index] = new Pair(key, val); size++; } /* 削除操作 */ public void remove(int key) { - // keyに対応するバケットインデックスを検索 + // key に対応するバケットインデックスを探す int index = findBucket(key); - // キー値ペアが見つかった場合、削除マークで覆う + // キーと値の組が見つかったら、削除マーカーで上書きする if (buckets[index] != null && buckets[index] != TOMBSTONE) { buckets[index] = TOMBSTONE; size--; @@ -791,13 +1899,13 @@ comments: true /* ハッシュテーブルを拡張 */ private void extend() { - // 元のハッシュテーブルを一時的に保存 + // 元のハッシュテーブルを一時保存 Pair[] bucketsTmp = buckets; - // 拡張された新しいハッシュテーブルを初期化 + // リサイズ後の新しいハッシュテーブルを初期化 capacity *= extendRatio; buckets = new Pair[capacity]; size = 0; - // 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動 + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す for (Pair pair : bucketsTmp) { if (pair != null && pair != TOMBSTONE) { put(pair.key, pair.val); @@ -805,7 +1913,7 @@ comments: true } } - /* ハッシュテーブルを印刷 */ + /* ハッシュテーブルを出力 */ public void print() { for (Pair pair : buckets) { if (pair == null) { @@ -823,94 +1931,1388 @@ comments: true === "C#" ```csharp title="hash_map_open_addressing.cs" - [class]{HashMapOpenAddressing}-[func]{} + /* オープンアドレス法ハッシュテーブル */ + class HashMapOpenAddressing { + int size; // キーと値のペア数 + int capacity = 4; // ハッシュテーブル容量 + double loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値 + int extendRatio = 2; // 拡張倍率 + Pair[] buckets; // バケット配列 + Pair TOMBSTONE = new(-1, "-1"); // 削除済みマーク + + /* コンストラクタ */ + public HashMapOpenAddressing() { + size = 0; + buckets = new Pair[capacity]; + } + + /* ハッシュ関数 */ + int HashFunc(int key) { + return key % capacity; + } + + /* 負荷率 */ + double LoadFactor() { + return (double)size / capacity; + } + + /* key に対応するバケットインデックスを探す */ + int FindBucket(int key) { + int index = HashFunc(key); + int firstTombstone = -1; + // 線形プロービングを行い、空バケットに達したら終了 + while (buckets[index] != null) { + // key が見つかったら、対応するバケットのインデックスを返す + if (buckets[index].key == key) { + // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 + if (firstTombstone != -1) { + buckets[firstTombstone] = buckets[index]; + buckets[index] = TOMBSTONE; + return firstTombstone; // 移動後のバケットインデックスを返す + } + return index; // バケットのインデックスを返す + } + // 最初に見つかった削除マークを記録 + if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { + firstTombstone = index; + } + // バケットのインデックスを計算し、末尾を越えたら先頭に戻る + index = (index + 1) % capacity; + } + // key が存在しない場合は追加位置のインデックスを返す + return firstTombstone == -1 ? index : firstTombstone; + } + + /* 検索操作 */ + public string? Get(int key) { + // key に対応するバケットインデックスを探す + int index = FindBucket(key); + // キーと値の組が見つかったら、対応する val を返す + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + return buckets[index].val; + } + // キーと値の組が存在しなければ null を返す + return null; + } + + /* 追加操作 */ + public void Put(int key, string val) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (LoadFactor() > loadThres) { + Extend(); + } + // key に対応するバケットインデックスを探す + int index = FindBucket(key); + // キーと値の組が見つかったら、val を上書きして返す + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index].val = val; + return; + } + // キーと値の組が存在しない場合は、その組を追加する + buckets[index] = new Pair(key, val); + size++; + } + + /* 削除操作 */ + public void Remove(int key) { + // key に対応するバケットインデックスを探す + int index = FindBucket(key); + // キーと値の組が見つかったら、削除マーカーで上書きする + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index] = TOMBSTONE; + size--; + } + } + + /* ハッシュテーブルを拡張 */ + void Extend() { + // 元のハッシュテーブルを一時保存 + Pair[] bucketsTmp = buckets; + // リサイズ後の新しいハッシュテーブルを初期化 + capacity *= extendRatio; + buckets = new Pair[capacity]; + size = 0; + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + foreach (Pair pair in bucketsTmp) { + if (pair != null && pair != TOMBSTONE) { + Put(pair.key, pair.val); + } + } + } + + /* ハッシュテーブルを出力 */ + public void Print() { + foreach (Pair pair in buckets) { + if (pair == null) { + Console.WriteLine("null"); + } else if (pair == TOMBSTONE) { + Console.WriteLine("TOMBSTONE"); + } else { + Console.WriteLine(pair.key + " -> " + pair.val); + } + } + } + } ``` === "Go" ```go title="hash_map_open_addressing.go" - [class]{hashMapOpenAddressing}-[func]{} + /* オープンアドレス法ハッシュテーブル */ + type hashMapOpenAddressing struct { + size int // キーと値のペア数 + capacity int // ハッシュテーブル容量 + loadThres float64 // リサイズを発動する負荷率のしきい値 + extendRatio int // 拡張倍率 + buckets []*pair // バケット配列 + TOMBSTONE *pair // 削除済みマーク + } + + /* コンストラクタ */ + func newHashMapOpenAddressing() *hashMapOpenAddressing { + return &hashMapOpenAddressing{ + size: 0, + capacity: 4, + loadThres: 2.0 / 3.0, + extendRatio: 2, + buckets: make([]*pair, 4), + TOMBSTONE: &pair{-1, "-1"}, + } + } + + /* ハッシュ関数 */ + func (h *hashMapOpenAddressing) hashFunc(key int) int { + return key % h.capacity // キーに基づいてハッシュ値を計算 + } + + /* 負荷率 */ + func (h *hashMapOpenAddressing) loadFactor() float64 { + return float64(h.size) / float64(h.capacity) // 現在の負荷率を計算 + } + + /* key に対応するバケットインデックスを探す */ + func (h *hashMapOpenAddressing) findBucket(key int) int { + index := h.hashFunc(key) // 初期インデックスを取得 + firstTombstone := -1 // 最初に遭遇した `TOMBSTONE` の位置を記録する + for h.buckets[index] != nil { + if h.buckets[index].key == key { + if firstTombstone != -1 { + // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 + h.buckets[firstTombstone] = h.buckets[index] + h.buckets[index] = h.TOMBSTONE + return firstTombstone // 移動後のバケットインデックスを返す + } + return index // 見つかったインデックスを返す + } + if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE { + firstTombstone = index // 最初に遭遇した削除マークの位置を記録する + } + index = (index + 1) % h.capacity // 線形探索を行い、末尾を越えたら先頭に戻る + } + // key が存在しない場合は追加位置のインデックスを返す + if firstTombstone != -1 { + return firstTombstone + } + return index + } + + /* 検索操作 */ + func (h *hashMapOpenAddressing) get(key int) string { + index := h.findBucket(key) // key に対応するバケットインデックスを探す + if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE { + return h.buckets[index].val // キーと値の組が見つかったら、対応する val を返す + } + return "" // キーと値のペアが存在しない場合は `""` を返す + } + + /* 追加操作 */ + func (h *hashMapOpenAddressing) put(key int, val string) { + if h.loadFactor() > h.loadThres { + h.extend() // 負荷率がしきい値を超えたら、リサイズを実行 + } + index := h.findBucket(key) // key に対応するバケットインデックスを探す + if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE { + h.buckets[index] = &pair{key, val} // キーと値の組が存在しない場合は、その組を追加する + h.size++ + } else { + h.buckets[index].val = val // キーと値のペアが見つかった場合は、`val` を上書きする + } + } + + /* 削除操作 */ + func (h *hashMapOpenAddressing) remove(key int) { + index := h.findBucket(key) // key に対応するバケットインデックスを探す + if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE { + h.buckets[index] = h.TOMBSTONE // キーと値の組が見つかったら、削除マーカーで上書きする + h.size-- + } + } + + /* ハッシュテーブルを拡張 */ + func (h *hashMapOpenAddressing) extend() { + oldBuckets := h.buckets // 元のハッシュテーブルを一時保存 + h.capacity *= h.extendRatio // 容量を更新 + h.buckets = make([]*pair, h.capacity) // リサイズ後の新しいハッシュテーブルを初期化 + h.size = 0 // サイズをリセットする + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for _, pair := range oldBuckets { + if pair != nil && pair != h.TOMBSTONE { + h.put(pair.key, pair.val) + } + } + } + + /* ハッシュテーブルを出力 */ + func (h *hashMapOpenAddressing) print() { + for _, pair := range h.buckets { + if pair == nil { + fmt.Println("nil") + } else if pair == h.TOMBSTONE { + fmt.Println("TOMBSTONE") + } else { + fmt.Printf("%d -> %s\n", pair.key, pair.val) + } + } + } ``` === "Swift" ```swift title="hash_map_open_addressing.swift" - [class]{HashMapOpenAddressing}-[func]{} + /* オープンアドレス法ハッシュテーブル */ + class HashMapOpenAddressing { + var size: Int // キーと値のペア数 + var capacity: Int // ハッシュテーブル容量 + var loadThres: Double // リサイズを発動する負荷率のしきい値 + var extendRatio: Int // 拡張倍率 + var buckets: [Pair?] // バケット配列 + var TOMBSTONE: Pair // 削除済みマーク + + /* コンストラクタ */ + init() { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = Array(repeating: nil, count: capacity) + TOMBSTONE = Pair(key: -1, val: "-1") + } + + /* ハッシュ関数 */ + func hashFunc(key: Int) -> Int { + key % capacity + } + + /* 負荷率 */ + func loadFactor() -> Double { + Double(size) / Double(capacity) + } + + /* key に対応するバケットインデックスを探す */ + func findBucket(key: Int) -> Int { + var index = hashFunc(key: key) + var firstTombstone = -1 + // 線形プロービングを行い、空バケットに達したら終了 + while buckets[index] != nil { + // key が見つかったら、対応するバケットのインデックスを返す + if buckets[index]!.key == key { + // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 + if firstTombstone != -1 { + buckets[firstTombstone] = buckets[index] + buckets[index] = TOMBSTONE + return firstTombstone // 移動後のバケットインデックスを返す + } + return index // バケットのインデックスを返す + } + // 最初に見つかった削除マークを記録 + if firstTombstone == -1 && buckets[index] == TOMBSTONE { + firstTombstone = index + } + // バケットのインデックスを計算し、末尾を越えたら先頭に戻る + index = (index + 1) % capacity + } + // key が存在しない場合は追加位置のインデックスを返す + return firstTombstone == -1 ? index : firstTombstone + } + + /* 検索操作 */ + func get(key: Int) -> String? { + // key に対応するバケットインデックスを探す + let index = findBucket(key: key) + // キーと値の組が見つかったら、対応する val を返す + if buckets[index] != nil, buckets[index] != TOMBSTONE { + return buckets[index]!.val + } + // キーと値の組が存在しなければ null を返す + return nil + } + + /* 追加操作 */ + func put(key: Int, val: String) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if loadFactor() > loadThres { + extend() + } + // key に対応するバケットインデックスを探す + let index = findBucket(key: key) + // キーと値の組が見つかったら、val を上書きして返す + if buckets[index] != nil, buckets[index] != TOMBSTONE { + buckets[index]!.val = val + return + } + // キーと値の組が存在しない場合は、その組を追加する + buckets[index] = Pair(key: key, val: val) + size += 1 + } + + /* 削除操作 */ + func remove(key: Int) { + // key に対応するバケットインデックスを探す + let index = findBucket(key: key) + // キーと値の組が見つかったら、削除マーカーで上書きする + if buckets[index] != nil, buckets[index] != TOMBSTONE { + buckets[index] = TOMBSTONE + size -= 1 + } + } + + /* ハッシュテーブルを拡張 */ + func extend() { + // 元のハッシュテーブルを一時保存 + let bucketsTmp = buckets + // リサイズ後の新しいハッシュテーブルを初期化 + capacity *= extendRatio + buckets = Array(repeating: nil, count: capacity) + size = 0 + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for pair in bucketsTmp { + if let pair, pair != TOMBSTONE { + put(key: pair.key, val: pair.val) + } + } + } + + /* ハッシュテーブルを出力 */ + func print() { + for pair in buckets { + if pair == nil { + Swift.print("null") + } else if pair == TOMBSTONE { + Swift.print("TOMBSTONE") + } else { + Swift.print("\(pair!.key) -> \(pair!.val)") + } + } + } + } ``` === "JS" ```javascript title="hash_map_open_addressing.js" - [class]{HashMapOpenAddressing}-[func]{} + /* オープンアドレス法ハッシュテーブル */ + class HashMapOpenAddressing { + #size; // キーと値のペア数 + #capacity; // ハッシュテーブル容量 + #loadThres; // リサイズを発動する負荷率のしきい値 + #extendRatio; // 拡張倍率 + #buckets; // バケット配列 + #TOMBSTONE; // 削除済みマーク + + /* コンストラクタ */ + constructor() { + this.#size = 0; // キーと値のペア数 + this.#capacity = 4; // ハッシュテーブル容量 + this.#loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値 + this.#extendRatio = 2; // 拡張倍率 + this.#buckets = Array(this.#capacity).fill(null); // バケット配列 + this.#TOMBSTONE = new Pair(-1, '-1'); // 削除済みマーク + } + + /* ハッシュ関数 */ + #hashFunc(key) { + return key % this.#capacity; + } + + /* 負荷率 */ + #loadFactor() { + return this.#size / this.#capacity; + } + + /* key に対応するバケットインデックスを探す */ + #findBucket(key) { + let index = this.#hashFunc(key); + let firstTombstone = -1; + // 線形プロービングを行い、空バケットに達したら終了 + while (this.#buckets[index] !== null) { + // key が見つかったら、対応するバケットのインデックスを返す + if (this.#buckets[index].key === key) { + // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 + if (firstTombstone !== -1) { + this.#buckets[firstTombstone] = this.#buckets[index]; + this.#buckets[index] = this.#TOMBSTONE; + return firstTombstone; // 移動後のバケットインデックスを返す + } + return index; // バケットのインデックスを返す + } + // 最初に見つかった削除マークを記録 + if ( + firstTombstone === -1 && + this.#buckets[index] === this.#TOMBSTONE + ) { + firstTombstone = index; + } + // バケットのインデックスを計算し、末尾を越えたら先頭に戻る + index = (index + 1) % this.#capacity; + } + // key が存在しない場合は追加位置のインデックスを返す + return firstTombstone === -1 ? index : firstTombstone; + } + + /* 検索操作 */ + get(key) { + // key に対応するバケットインデックスを探す + const index = this.#findBucket(key); + // キーと値の組が見つかったら、対応する val を返す + if ( + this.#buckets[index] !== null && + this.#buckets[index] !== this.#TOMBSTONE + ) { + return this.#buckets[index].val; + } + // キーと値の組が存在しなければ null を返す + return null; + } + + /* 追加操作 */ + put(key, val) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + // key に対応するバケットインデックスを探す + const index = this.#findBucket(key); + // キーと値の組が見つかったら、val を上書きして返す + if ( + this.#buckets[index] !== null && + this.#buckets[index] !== this.#TOMBSTONE + ) { + this.#buckets[index].val = val; + return; + } + // キーと値の組が存在しない場合は、その組を追加する + this.#buckets[index] = new Pair(key, val); + this.#size++; + } + + /* 削除操作 */ + remove(key) { + // key に対応するバケットインデックスを探す + const index = this.#findBucket(key); + // キーと値の組が見つかったら、削除マーカーで上書きする + if ( + this.#buckets[index] !== null && + this.#buckets[index] !== this.#TOMBSTONE + ) { + this.#buckets[index] = this.#TOMBSTONE; + this.#size--; + } + } + + /* ハッシュテーブルを拡張 */ + #extend() { + // 元のハッシュテーブルを一時保存 + const bucketsTmp = this.#buckets; + // リサイズ後の新しいハッシュテーブルを初期化 + this.#capacity *= this.#extendRatio; + this.#buckets = Array(this.#capacity).fill(null); + this.#size = 0; + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for (const pair of bucketsTmp) { + if (pair !== null && pair !== this.#TOMBSTONE) { + this.put(pair.key, pair.val); + } + } + } + + /* ハッシュテーブルを出力 */ + print() { + for (const pair of this.#buckets) { + if (pair === null) { + console.log('null'); + } else if (pair === this.#TOMBSTONE) { + console.log('TOMBSTONE'); + } else { + console.log(pair.key + ' -> ' + pair.val); + } + } + } + } ``` === "TS" ```typescript title="hash_map_open_addressing.ts" - [class]{HashMapOpenAddressing}-[func]{} + /* オープンアドレス法ハッシュテーブル */ + class HashMapOpenAddressing { + private size: number; // キーと値のペア数 + private capacity: number; // ハッシュテーブル容量 + private loadThres: number; // リサイズを発動する負荷率のしきい値 + private extendRatio: number; // 拡張倍率 + private buckets: Array; // バケット配列 + private TOMBSTONE: Pair; // 削除済みマーク + + /* コンストラクタ */ + constructor() { + this.size = 0; // キーと値のペア数 + this.capacity = 4; // ハッシュテーブル容量 + this.loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値 + this.extendRatio = 2; // 拡張倍率 + this.buckets = Array(this.capacity).fill(null); // バケット配列 + this.TOMBSTONE = new Pair(-1, '-1'); // 削除済みマーク + } + + /* ハッシュ関数 */ + private hashFunc(key: number): number { + return key % this.capacity; + } + + /* 負荷率 */ + private loadFactor(): number { + return this.size / this.capacity; + } + + /* key に対応するバケットインデックスを探す */ + private findBucket(key: number): number { + let index = this.hashFunc(key); + let firstTombstone = -1; + // 線形プロービングを行い、空バケットに達したら終了 + while (this.buckets[index] !== null) { + // key が見つかったら、対応するバケットのインデックスを返す + if (this.buckets[index]!.key === key) { + // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 + if (firstTombstone !== -1) { + this.buckets[firstTombstone] = this.buckets[index]; + this.buckets[index] = this.TOMBSTONE; + return firstTombstone; // 移動後のバケットインデックスを返す + } + return index; // バケットのインデックスを返す + } + // 最初に見つかった削除マークを記録 + if ( + firstTombstone === -1 && + this.buckets[index] === this.TOMBSTONE + ) { + firstTombstone = index; + } + // バケットのインデックスを計算し、末尾を越えたら先頭に戻る + index = (index + 1) % this.capacity; + } + // key が存在しない場合は追加位置のインデックスを返す + return firstTombstone === -1 ? index : firstTombstone; + } + + /* 検索操作 */ + get(key: number): string | null { + // key に対応するバケットインデックスを探す + const index = this.findBucket(key); + // キーと値の組が見つかったら、対応する val を返す + if ( + this.buckets[index] !== null && + this.buckets[index] !== this.TOMBSTONE + ) { + return this.buckets[index]!.val; + } + // キーと値の組が存在しなければ null を返す + return null; + } + + /* 追加操作 */ + put(key: number, val: string): void { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (this.loadFactor() > this.loadThres) { + this.extend(); + } + // key に対応するバケットインデックスを探す + const index = this.findBucket(key); + // キーと値の組が見つかったら、val を上書きして返す + if ( + this.buckets[index] !== null && + this.buckets[index] !== this.TOMBSTONE + ) { + this.buckets[index]!.val = val; + return; + } + // キーと値の組が存在しない場合は、その組を追加する + this.buckets[index] = new Pair(key, val); + this.size++; + } + + /* 削除操作 */ + remove(key: number): void { + // key に対応するバケットインデックスを探す + const index = this.findBucket(key); + // キーと値の組が見つかったら、削除マーカーで上書きする + if ( + this.buckets[index] !== null && + this.buckets[index] !== this.TOMBSTONE + ) { + this.buckets[index] = this.TOMBSTONE; + this.size--; + } + } + + /* ハッシュテーブルを拡張 */ + private extend(): void { + // 元のハッシュテーブルを一時保存 + const bucketsTmp = this.buckets; + // リサイズ後の新しいハッシュテーブルを初期化 + this.capacity *= this.extendRatio; + this.buckets = Array(this.capacity).fill(null); + this.size = 0; + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for (const pair of bucketsTmp) { + if (pair !== null && pair !== this.TOMBSTONE) { + this.put(pair.key, pair.val); + } + } + } + + /* ハッシュテーブルを出力 */ + print(): void { + for (const pair of this.buckets) { + if (pair === null) { + console.log('null'); + } else if (pair === this.TOMBSTONE) { + console.log('TOMBSTONE'); + } else { + console.log(pair.key + ' -> ' + pair.val); + } + } + } + } ``` === "Dart" ```dart title="hash_map_open_addressing.dart" - [class]{HashMapOpenAddressing}-[func]{} + /* オープンアドレス法ハッシュテーブル */ + class HashMapOpenAddressing { + late int _size; // キーと値のペア数 + int _capacity = 4; // ハッシュテーブル容量 + double _loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値 + int _extendRatio = 2; // 拡張倍率 + late List _buckets; // バケット配列 + Pair _TOMBSTONE = Pair(-1, "-1"); // 削除済みマーク + + /* コンストラクタ */ + HashMapOpenAddressing() { + _size = 0; + _buckets = List.generate(_capacity, (index) => null); + } + + /* ハッシュ関数 */ + int hashFunc(int key) { + return key % _capacity; + } + + /* 負荷率 */ + double loadFactor() { + return _size / _capacity; + } + + /* key に対応するバケットインデックスを探す */ + int findBucket(int key) { + int index = hashFunc(key); + int firstTombstone = -1; + // 線形プロービングを行い、空バケットに達したら終了 + while (_buckets[index] != null) { + // key が見つかったら、対応するバケットのインデックスを返す + if (_buckets[index]!.key == key) { + // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 + if (firstTombstone != -1) { + _buckets[firstTombstone] = _buckets[index]; + _buckets[index] = _TOMBSTONE; + return firstTombstone; // 移動後のバケットインデックスを返す + } + return index; // バケットのインデックスを返す + } + // 最初に見つかった削除マークを記録 + if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) { + firstTombstone = index; + } + // バケットのインデックスを計算し、末尾を越えたら先頭に戻る + index = (index + 1) % _capacity; + } + // key が存在しない場合は追加位置のインデックスを返す + return firstTombstone == -1 ? index : firstTombstone; + } + + /* 検索操作 */ + String? get(int key) { + // key に対応するバケットインデックスを探す + int index = findBucket(key); + // キーと値の組が見つかったら、対応する val を返す + if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) { + return _buckets[index]!.val; + } + // キーと値の組が存在しなければ null を返す + return null; + } + + /* 追加操作 */ + void put(int key, String val) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (loadFactor() > _loadThres) { + extend(); + } + // key に対応するバケットインデックスを探す + int index = findBucket(key); + // キーと値の組が見つかったら、val を上書きして返す + if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) { + _buckets[index]!.val = val; + return; + } + // キーと値の組が存在しない場合は、その組を追加する + _buckets[index] = new Pair(key, val); + _size++; + } + + /* 削除操作 */ + void remove(int key) { + // key に対応するバケットインデックスを探す + int index = findBucket(key); + // キーと値の組が見つかったら、削除マーカーで上書きする + if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) { + _buckets[index] = _TOMBSTONE; + _size--; + } + } + + /* ハッシュテーブルを拡張 */ + void extend() { + // 元のハッシュテーブルを一時保存 + List bucketsTmp = _buckets; + // リサイズ後の新しいハッシュテーブルを初期化 + _capacity *= _extendRatio; + _buckets = List.generate(_capacity, (index) => null); + _size = 0; + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for (Pair? pair in bucketsTmp) { + if (pair != null && pair != _TOMBSTONE) { + put(pair.key, pair.val); + } + } + } + + /* ハッシュテーブルを出力 */ + void printHashMap() { + for (Pair? pair in _buckets) { + if (pair == null) { + print("null"); + } else if (pair == _TOMBSTONE) { + print("TOMBSTONE"); + } else { + print("${pair.key} -> ${pair.val}"); + } + } + } + } ``` === "Rust" ```rust title="hash_map_open_addressing.rs" - [class]{HashMapOpenAddressing}-[func]{} + /* オープンアドレス法ハッシュテーブル */ + struct HashMapOpenAddressing { + size: usize, // キーと値のペア数 + capacity: usize, // ハッシュテーブル容量 + load_thres: f64, // リサイズを発動する負荷率のしきい値 + extend_ratio: usize, // 拡張倍率 + buckets: Vec>, // バケット配列 + TOMBSTONE: Option, // 削除済みマーク + } + + impl HashMapOpenAddressing { + /* コンストラクタ */ + fn new() -> Self { + Self { + size: 0, + capacity: 4, + load_thres: 2.0 / 3.0, + extend_ratio: 2, + buckets: vec![None; 4], + TOMBSTONE: Some(Pair { + key: -1, + val: "-1".to_string(), + }), + } + } + + /* ハッシュ関数 */ + fn hash_func(&self, key: i32) -> usize { + (key % self.capacity as i32) as usize + } + + /* 負荷率 */ + fn load_factor(&self) -> f64 { + self.size as f64 / self.capacity as f64 + } + + /* key に対応するバケットインデックスを探す */ + fn find_bucket(&mut self, key: i32) -> usize { + let mut index = self.hash_func(key); + let mut first_tombstone = -1; + // 線形プロービングを行い、空バケットに達したら終了 + while self.buckets[index].is_some() { + // `key` に遭遇したら、対応するバケットのインデックスを返す + if self.buckets[index].as_ref().unwrap().key == key { + // 以前に削除マークに遭遇していた場合は、キーと値のペアをそのインデックスへ移動する + if first_tombstone != -1 { + self.buckets[first_tombstone as usize] = self.buckets[index].take(); + self.buckets[index] = self.TOMBSTONE.clone(); + return first_tombstone as usize; // 移動後のバケットインデックスを返す + } + return index; // バケットのインデックスを返す + } + // 最初に見つかった削除マークを記録 + if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE { + first_tombstone = index as i32; + } + // バケットのインデックスを計算し、末尾を越えたら先頭に戻る + index = (index + 1) % self.capacity; + } + // key が存在しない場合は追加位置のインデックスを返す + if first_tombstone == -1 { + index + } else { + first_tombstone as usize + } + } + + /* 検索操作 */ + fn get(&mut self, key: i32) -> Option<&str> { + // key に対応するバケットインデックスを探す + let index = self.find_bucket(key); + // キーと値の組が見つかったら、対応する val を返す + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + return self.buckets[index].as_ref().map(|pair| &pair.val as &str); + } + // キーと値の組が存在しなければ null を返す + None + } + + /* 追加操作 */ + fn put(&mut self, key: i32, val: String) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if self.load_factor() > self.load_thres { + self.extend(); + } + // key に対応するバケットインデックスを探す + let index = self.find_bucket(key); + // キーと値の組が見つかったら、val を上書きして返す + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + self.buckets[index].as_mut().unwrap().val = val; + return; + } + // キーと値の組が存在しない場合は、その組を追加する + self.buckets[index] = Some(Pair { key, val }); + self.size += 1; + } + + /* 削除操作 */ + fn remove(&mut self, key: i32) { + // key に対応するバケットインデックスを探す + let index = self.find_bucket(key); + // キーと値の組が見つかったら、削除マーカーで上書きする + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + self.buckets[index] = self.TOMBSTONE.clone(); + self.size -= 1; + } + } + + /* ハッシュテーブルを拡張 */ + fn extend(&mut self) { + // 元のハッシュテーブルを一時保存 + let buckets_tmp = self.buckets.clone(); + // リサイズ後の新しいハッシュテーブルを初期化 + self.capacity *= self.extend_ratio; + self.buckets = vec![None; self.capacity]; + self.size = 0; + + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for pair in buckets_tmp { + if pair.is_none() || pair == self.TOMBSTONE { + continue; + } + let pair = pair.unwrap(); + + self.put(pair.key, pair.val); + } + } + /* ハッシュテーブルを出力 */ + fn print(&self) { + for pair in &self.buckets { + if pair.is_none() { + println!("null"); + } else if pair == &self.TOMBSTONE { + println!("TOMBSTONE"); + } else { + let pair = pair.as_ref().unwrap(); + println!("{} -> {}", pair.key, pair.val); + } + } + } + } ``` === "C" ```c title="hash_map_open_addressing.c" - [class]{HashMapOpenAddressing}-[func]{} + /* オープンアドレス法ハッシュテーブル */ + typedef struct { + int size; // キーと値のペア数 + int capacity; // ハッシュテーブル容量 + double loadThres; // リサイズを発動する負荷率のしきい値 + int extendRatio; // 拡張倍率 + Pair **buckets; // バケット配列 + Pair *TOMBSTONE; // 削除済みマーク + } HashMapOpenAddressing; + + /* コンストラクタ */ + HashMapOpenAddressing *newHashMapOpenAddressing() { + HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing)); + hashMap->size = 0; + hashMap->capacity = 4; + hashMap->loadThres = 2.0 / 3.0; + hashMap->extendRatio = 2; + hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *)); + hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair)); + hashMap->TOMBSTONE->key = -1; + hashMap->TOMBSTONE->val = "-1"; + + return hashMap; + } + + /* デストラクタ */ + void delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Pair *pair = hashMap->buckets[i]; + if (pair != NULL && pair != hashMap->TOMBSTONE) { + free(pair->val); + free(pair); + } + } + free(hashMap->buckets); + free(hashMap->TOMBSTONE); + free(hashMap); + } + + /* ハッシュ関数 */ + int hashFunc(HashMapOpenAddressing *hashMap, int key) { + return key % hashMap->capacity; + } + + /* 負荷率 */ + double loadFactor(HashMapOpenAddressing *hashMap) { + return (double)hashMap->size / (double)hashMap->capacity; + } + + /* key に対応するバケットインデックスを探す */ + int findBucket(HashMapOpenAddressing *hashMap, int key) { + int index = hashFunc(hashMap, key); + int firstTombstone = -1; + // 線形プロービングを行い、空バケットに達したら終了 + while (hashMap->buckets[index] != NULL) { + // key が見つかったら、対応するバケットのインデックスを返す + if (hashMap->buckets[index]->key == key) { + // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 + if (firstTombstone != -1) { + hashMap->buckets[firstTombstone] = hashMap->buckets[index]; + hashMap->buckets[index] = hashMap->TOMBSTONE; + return firstTombstone; // 移動後のバケットインデックスを返す + } + return index; // バケットのインデックスを返す + } + // 最初に見つかった削除マークを記録 + if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) { + firstTombstone = index; + } + // バケットのインデックスを計算し、末尾を越えたら先頭に戻る + index = (index + 1) % hashMap->capacity; + } + // key が存在しない場合は追加位置のインデックスを返す + return firstTombstone == -1 ? index : firstTombstone; + } + + /* 検索操作 */ + char *get(HashMapOpenAddressing *hashMap, int key) { + // key に対応するバケットインデックスを探す + int index = findBucket(hashMap, key); + // キーと値の組が見つかったら、対応する val を返す + if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) { + return hashMap->buckets[index]->val; + } + // キーと値の組が存在しない場合は空文字列を返す + return ""; + } + + /* 追加操作 */ + void put(HashMapOpenAddressing *hashMap, int key, char *val) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (loadFactor(hashMap) > hashMap->loadThres) { + extend(hashMap); + } + // key に対応するバケットインデックスを探す + int index = findBucket(hashMap, key); + // キーと値の組が見つかったら、val を上書きして返す + if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) { + free(hashMap->buckets[index]->val); + hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val) + 1)); + strcpy(hashMap->buckets[index]->val, val); + hashMap->buckets[index]->val[strlen(val)] = '\0'; + return; + } + // キーと値の組が存在しない場合は、その組を追加する + Pair *pair = (Pair *)malloc(sizeof(Pair)); + pair->key = key; + pair->val = (char *)malloc(sizeof(strlen(val) + 1)); + strcpy(pair->val, val); + pair->val[strlen(val)] = '\0'; + + hashMap->buckets[index] = pair; + hashMap->size++; + } + + /* 削除操作 */ + void removeItem(HashMapOpenAddressing *hashMap, int key) { + // key に対応するバケットインデックスを探す + int index = findBucket(hashMap, key); + // キーと値の組が見つかったら、削除マーカーで上書きする + if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) { + Pair *pair = hashMap->buckets[index]; + free(pair->val); + free(pair); + hashMap->buckets[index] = hashMap->TOMBSTONE; + hashMap->size--; + } + } + + /* ハッシュテーブルを拡張 */ + void extend(HashMapOpenAddressing *hashMap) { + // 元のハッシュテーブルを一時保存 + Pair **bucketsTmp = hashMap->buckets; + int oldCapacity = hashMap->capacity; + // リサイズ後の新しいハッシュテーブルを初期化 + hashMap->capacity *= hashMap->extendRatio; + hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *)); + hashMap->size = 0; + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for (int i = 0; i < oldCapacity; i++) { + Pair *pair = bucketsTmp[i]; + if (pair != NULL && pair != hashMap->TOMBSTONE) { + put(hashMap, pair->key, pair->val); + free(pair->val); + free(pair); + } + } + free(bucketsTmp); + } + + /* ハッシュテーブルを出力 */ + void print(HashMapOpenAddressing *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Pair *pair = hashMap->buckets[i]; + if (pair == NULL) { + printf("NULL\n"); + } else if (pair == hashMap->TOMBSTONE) { + printf("TOMBSTONE\n"); + } else { + printf("%d -> %s\n", pair->key, pair->val); + } + } + } ``` === "Kotlin" ```kotlin title="hash_map_open_addressing.kt" - [class]{HashMapOpenAddressing}-[func]{} + /* オープンアドレス法ハッシュテーブル */ + class HashMapOpenAddressing { + private var size: Int // キーと値のペア数 + private var capacity: Int // ハッシュテーブル容量 + private val loadThres: Double // リサイズを発動する負荷率のしきい値 + private val extendRatio: Int // 拡張倍率 + private var buckets: Array // バケット配列 + private val TOMBSTONE: Pair // 削除済みマーク + + /* コンストラクタ */ + init { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = arrayOfNulls(capacity) + TOMBSTONE = Pair(-1, "-1") + } + + /* ハッシュ関数 */ + fun hashFunc(key: Int): Int { + return key % capacity + } + + /* 負荷率 */ + fun loadFactor(): Double { + return (size / capacity).toDouble() + } + + /* key に対応するバケットインデックスを探す */ + fun findBucket(key: Int): Int { + var index = hashFunc(key) + var firstTombstone = -1 + // 線形プロービングを行い、空バケットに達したら終了 + while (buckets[index] != null) { + // key が見つかったら、対応するバケットのインデックスを返す + if (buckets[index]?.key == key) { + // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 + if (firstTombstone != -1) { + buckets[firstTombstone] = buckets[index] + buckets[index] = TOMBSTONE + return firstTombstone // 移動後のバケットインデックスを返す + } + return index // バケットのインデックスを返す + } + // 最初に見つかった削除マークを記録 + if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { + firstTombstone = index + } + // バケットのインデックスを計算し、末尾を越えたら先頭に戻る + index = (index + 1) % capacity + } + // key が存在しない場合は追加位置のインデックスを返す + return if (firstTombstone == -1) index else firstTombstone + } + + /* 検索操作 */ + fun get(key: Int): String? { + // key に対応するバケットインデックスを探す + val index = findBucket(key) + // キーと値の組が見つかったら、対応する val を返す + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + return buckets[index]?._val + } + // キーと値の組が存在しなければ null を返す + return null + } + + /* 追加操作 */ + fun put(key: Int, _val: String) { + // 負荷率がしきい値を超えたら、リサイズを実行 + if (loadFactor() > loadThres) { + extend() + } + // key に対応するバケットインデックスを探す + val index = findBucket(key) + // キーと値の組が見つかったら、val を上書きして返す + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index]!!._val = _val + return + } + // キーと値の組が存在しない場合は、その組を追加する + buckets[index] = Pair(key, _val) + size++ + } + + /* 削除操作 */ + fun remove(key: Int) { + // key に対応するバケットインデックスを探す + val index = findBucket(key) + // キーと値の組が見つかったら、削除マーカーで上書きする + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index] = TOMBSTONE + size-- + } + } + + /* ハッシュテーブルを拡張 */ + fun extend() { + // 元のハッシュテーブルを一時保存 + val bucketsTmp = buckets + // リサイズ後の新しいハッシュテーブルを初期化 + capacity *= extendRatio + buckets = arrayOfNulls(capacity) + size = 0 + // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for (pair in bucketsTmp) { + if (pair != null && pair != TOMBSTONE) { + put(pair.key, pair._val) + } + } + } + + /* ハッシュテーブルを出力 */ + fun print() { + for (pair in buckets) { + if (pair == null) { + println("null") + } else if (pair == TOMBSTONE) { + println("TOMESTOME") + } else { + println("${pair.key} -> ${pair._val}") + } + } + } + } ``` === "Ruby" ```ruby title="hash_map_open_addressing.rb" - [class]{HashMapOpenAddressing}-[func]{} + ### オープンアドレス法ハッシュテーブル ### + class HashMapOpenAddressing + TOMBSTONE = Pair.new(-1, '-1') # 削除済みマーク + + ### コンストラクタ ### + def initialize + @size = 0 # キーと値のペア数 + @capacity = 4 # ハッシュテーブル容量 + @load_thres = 2.0 / 3.0 # リサイズを発動する負荷率のしきい値 + @extend_ratio = 2 # 拡張倍率 + @buckets = Array.new(@capacity) # バケット配列 + end + + ### ハッシュ関数 ### + def hash_func(key) + key % @capacity + end + + ### 負荷率 ### + def load_factor + @size / @capacity + end + + ### key に対応するバケットインデックスを検索 ### + def find_bucket(key) + index = hash_func(key) + first_tombstone = -1 + # 線形プロービングを行い、空バケットに達したら終了 + while !@buckets[index].nil? + # key が見つかったら、対応するバケットのインデックスを返す + if @buckets[index].key == key + # 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動 + if first_tombstone != -1 + @buckets[first_tombstone] = @buckets[index] + @buckets[index] = TOMBSTONE + return first_tombstone # 移動後のバケットインデックスを返す + end + return index # バケットのインデックスを返す + end + # 最初に見つかった削除マークを記録 + first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE + # バケットのインデックスを計算し、末尾を越えたら先頭に戻る + index = (index + 1) % @capacity + end + # key が存在しない場合は追加位置のインデックスを返す + first_tombstone == -1 ? index : first_tombstone + end + + ### 検索操作 ### + def get(key) + # key に対応するバケットインデックスを探す + index = find_bucket(key) + # キーと値の組が見つかったら、対応する val を返す + return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index]) + # キーと値のペアが存在しない場合は `nil` を返す + nil + end + + ### 追加操作 ### + def put(key, val) + # 負荷率がしきい値を超えたら、リサイズを実行 + extend if load_factor > @load_thres + # key に対応するバケットインデックスを探す + index = find_bucket(key) + # キーと値のペアが見つかった場合は、`val` を上書きして返す + unless [nil, TOMBSTONE].include?(@buckets[index]) + @buckets[index].val = val + return + end + # キーと値の組が存在しない場合は、その組を追加する + @buckets[index] = Pair.new(key, val) + @size += 1 + end + + ### 削除操作 ### + def remove(key) + # key に対応するバケットインデックスを探す + index = find_bucket(key) + # キーと値の組が見つかったら、削除マーカーで上書きする + unless [nil, TOMBSTONE].include?(@buckets[index]) + @buckets[index] = TOMBSTONE + @size -= 1 + end + end + + ### ハッシュテーブルを拡張 ### + def extend + # 元のハッシュテーブルを一時保存 + buckets_tmp = @buckets + # リサイズ後の新しいハッシュテーブルを初期化 + @capacity *= @extend_ratio + @buckets = Array.new(@capacity) + @size = 0 + # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す + for pair in buckets_tmp + put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair) + end + end + + ### ハッシュテーブルを出力 ### + def print + for pair in @buckets + if pair.nil? + puts "Nil" + elsif pair == TOMBSTONE + puts "TOMBSTONE" + else + puts "#{pair.key} -> #{pair.val}" + end + end + end + end ``` -### 2.   二次プローブ +### 2.   二次探索 -二次プローブは線形プローブに似ており、オープンアドレス法の一般的な戦略の1つです。衝突が発生した場合、二次プローブは単純に固定ステップ数をスキップするのではなく、「プローブ回数の二乗」に等しいステップ数、つまり$1, 4, 9, \dots$ステップをスキップします。 +二次探索は線形探索に似ており、オープンアドレッシングの一般的な戦略の 1 つです。衝突が発生したとき、二次探索では単純に固定歩数を飛ばすのではなく、「探索回数の二乗」に相当する歩数、すなわち $1, 4, 9, \dots$ 歩を飛ばします。 -二次プローブには以下の利点があります: +二次探索には主に次の利点があります。 -- 二次プローブは、プローブ回数の二乗の距離をスキップすることで、線形プローブのクラスタリング効果を軽減しようとします。 -- 二次プローブはより大きな距離をスキップして空の位置を見つけ、データをより均等に分散するのに役立ちます。 +- 二次探索は、探索回数の二乗の距離を飛ばすことで、線形探索のクラスタリング効果を緩和しようとします。 +- 二次探索はより大きな距離を飛ばして空き位置を探すため、データ分布がより均一になるのに役立ちます。 -しかし、二次プローブは完璧ではありません: +しかし、二次探索は完璧ではありません。 -- クラスタリングは依然として存在し、つまり一部の位置は他の位置よりも占有される可能性が高いです。 -- 二乗の成長により、二次プローブはハッシュ表全体をプローブできない可能性があり、ハッシュ表に空のバケットがあっても、二次プローブがアクセスできない可能性があります。 +- 依然としてクラスタリング現象は存在し、ある位置が他の位置より占有されやすいことがあります。 +- 二乗の増加により、二次探索はハッシュテーブル全体を探索できない可能性があります。これは、ハッシュテーブルに空バケットがあっても、二次探索ではそこに到達できないことがあることを意味します。 -### 3.   二重ハッシュ +### 3.   多重ハッシュ -名前が示すように、二重ハッシュ法は複数のハッシュ関数$f_1(x)$、$f_2(x)$、$f_3(x)$、$\dots$をプローブに使用します。 +その名のとおり、多重ハッシュ法では複数のハッシュ関数 $f_1(x)$、$f_2(x)$、$f_3(x)$、$\dots$ を使って探索を行います。 -- **要素の挿入**: ハッシュ関数$f_1(x)$が衝突に遭遇した場合、$f_2(x)$を試し、以下同様に、空の位置が見つかって要素が挿入されるまで続けます。 -- **要素の検索**: 同じハッシュ関数の順序で検索し、ターゲット要素が見つかって返されるまで、または空の位置に遭遇するかすべてのハッシュ関数が試されるまで続け、要素がハッシュ表にないことを示し、`None`を返します。 +- **要素の挿入**:ハッシュ関数 $f_1(x)$ で衝突が発生した場合は、$f_2(x)$ を試し、以下同様に、空き位置が見つかるまで続けてから要素を挿入します。 +- **要素の検索**:同じハッシュ関数の順序で探索し、対象要素が見つかった時点で返します。空き位置に遭遇するか、すべてのハッシュ関数を試しても見つからない場合は、ハッシュテーブル内にその要素は存在しないため、 `None` を返します。 -線形プローブと比較して、二重ハッシュ法はクラスタリングが起こりにくいですが、複数のハッシュ関数は追加の計算オーバーヘッドを導入します。 +線形探索と比べると、多重ハッシュ法はクラスタリングを起こしにくい一方で、複数のハッシュ関数により追加の計算量が発生します。 !!! tip - オープンアドレス法(線形プローブ、二次プローブ、二重ハッシュ)ハッシュ表はすべて「要素を直接削除できない」という問題があることに注意してください。 + 注意してください。オープンアドレッシング(線形探索、二次探索、多重ハッシュ)のハッシュテーブルには、いずれも「要素を直接削除できない」という問題があります。 ## 6.2.3   プログラミング言語の選択 -異なるプログラミング言語は異なるハッシュ表実装戦略を採用しています。以下にいくつかの例を示します: +各種プログラミング言語は異なるハッシュテーブル実装戦略を採用しています。以下にいくつか例を挙げます。 -- Pythonはオープンアドレス法を使用します。`dict`辞書はプローブに疑似乱数を使用します。 -- Javaは連鎖法を使用します。JDK 1.8以降、`HashMap`の配列長が64に達し、連結リストの長さが8に達すると、連結リストは検索性能を向上させるために赤黒木に変換されます。 -- Goは連鎖法を使用します。Goは各バケットが最大8つのキー値ペアを格納できることを規定し、容量を超えた場合はオーバーフローバケットが連結されます。オーバーフローバケットが多すぎる場合、性能を確保するために特別な等容量リサイズ操作が実行されます。 +- Python はオープンアドレッシングを採用しています。辞書 `dict` は疑似乱数を用いて探索します。 +- Java はチェイン法を採用しています。JDK 1.8 以降、`HashMap` 内の配列長が 64 に達し、かつ連結リスト長が 8 に達すると、連結リストは検索性能を高めるため赤黒木に変換されます。 +- Go はチェイン法を採用しています。Go では各バケットに最大 8 個のキーと値のペアを格納でき、容量を超えるとオーバーフローバケットを連結します。オーバーフローバケットが多すぎる場合は、性能を確保するために特殊な等量拡張操作を実行します。 diff --git a/ja/docs/chapter_hashing/hash_map.md b/ja/docs/chapter_hashing/hash_map.md index 023b82fb7..08a9d1524 100644 --- a/ja/docs/chapter_hashing/hash_map.md +++ b/ja/docs/chapter_hashing/hash_map.md @@ -2,247 +2,247 @@ comments: true --- -# 6.1   ハッシュ表 +# 6.1   ハッシュテーブル -ハッシュ表ハッシュマップとも呼ばれ、キーと値の間のマッピングを確立し、効率的な要素の取得を可能にするデータ構造です。具体的には、ハッシュ表に`key`を入力すると、$O(1)$の時間計算量で対応する`value`を取得できます。 +ハッシュテーブル(hash table)は、散列表とも呼ばれ、キー `key` と値 `value` の対応関係を構築することで、高効率な要素検索を実現します。具体的には、ハッシュテーブルにキー `key` を入力すると、対応する値 `value` を $O(1)$ 時間で取得できます。 -下図に示すように、$n$人の学生がいて、各学生には「名前」と「学籍番号」の2つのデータフィールドがあるとします。学籍番号を入力として対応する名前を返すクエリ機能を実装したい場合、下図に示すハッシュ表を使用できます。 +以下の図に示すように、$n$ 人の学生がいるとし、各学生は「名前」と「学籍番号」の 2 つの情報を持っています。もし「学籍番号を入力すると対応する名前を返す」という検索機能を実現したいなら、下図のようなハッシュテーブルを用いることができます。 -![ハッシュ表の抽象的な表現](hash_map.assets/hash_table_lookup.png){ class="animation-figure" } +![ハッシュテーブルの抽象表現](hash_map.assets/hash_table_lookup.png){ class="animation-figure" } -

図 6-1   ハッシュ表の抽象的な表現

+

図 6-1   ハッシュテーブルの抽象表現

-ハッシュ表に加えて、配列や連結リストもクエリ機能の実装に使用できますが、時間計算量が異なります。効率は以下の表で比較されています: +ハッシュテーブルのほかに、配列や連結リストでも検索機能を実現できます。それらの効率比較を次の表に示します。 -- **要素の挿入**: 配列(または連結リスト)の末尾に要素を追加するだけです。この操作の時間計算量は$O(1)$です。 -- **要素の検索**: 配列(または連結リスト)がソートされていないため、要素を検索するにはすべての要素を走査する必要があります。この操作の時間計算量は$O(n)$です。 -- **要素の削除**: 要素を削除するには、まずその要素を見つけてから、配列(または連結リスト)から削除します。この操作の時間計算量は$O(n)$です。 +- **要素の追加**:要素を配列(連結リスト)の末尾に追加するだけでよく、$O(1)$ 時間です。 +- **要素の検索**:配列(連結リスト)は無秩序なので、すべての要素を走査する必要があり、$O(n)$ 時間かかります。 +- **要素の削除**:先に要素を検索してから配列(連結リスト)から削除する必要があり、$O(n)$ 時間かかります。 -

表 6-1   一般的な操作の時間効率の比較

+

表 6-1   要素検索効率の比較

-| | 配列 | 連結リスト | ハッシュ表 | -| -------------- | ------ | ----------- | ---------- | -| 要素の検索 | $O(n)$ | $O(n)$ | $O(1)$ | -| 要素の挿入 | $O(1)$ | $O(1)$ | $O(1)$ | -| 要素の削除 | $O(n)$ | $O(n)$ | $O(1)$ | +| | 配列 | 連結リスト | ハッシュテーブル | +| -------- | ------ | ------ | ------ | +| 要素の検索 | $O(n)$ | $O(n)$ | $O(1)$ | +| 要素の追加 | $O(1)$ | $O(1)$ | $O(1)$ | +| 要素の削除 | $O(n)$ | $O(n)$ | $O(1)$ |
-観察されるように、**ハッシュ表における操作(挿入、削除、検索、変更)の時間計算量は$O(1)$**で、非常に効率的です。 +以上から分かるように、**ハッシュテーブルにおける追加・削除・検索・更新の時間計算量はいずれも $O(1)$** であり、非常に高効率です。 -## 6.1.1   ハッシュ表の一般的な操作 +## 6.1.1   ハッシュテーブルの基本操作 -ハッシュ表の一般的な操作には、初期化、クエリ、キー値ペアの追加、キー値ペアの削除があります。以下はコード例です: +ハッシュテーブルの一般的な操作には、初期化、検索、キーと値のペアの追加、キーと値のペアの削除などがあります。コード例は以下のとおりです: === "Python" ```python title="hash_map.py" - # ハッシュ表を初期化 + # ハッシュテーブルを初期化 hmap: dict = {} # 追加操作 - # ハッシュ表にキー値ペア (key, value) を追加 - hmap[12836] = "小哈" - hmap[15937] = "小啰" - hmap[16750] = "小算" - hmap[13276] = "小法" - hmap[10583] = "小鸭" + # ハッシュテーブルにキーと値のペア (key, value) を追加 + hmap[12836] = "シャオハ" + hmap[15937] = "シャオルオ" + hmap[16750] = "シャオスワン" + hmap[13276] = "シャオファ" + hmap[10583] = "シャオヤー" - # クエリ操作 - # ハッシュ表にキーを入力し、値を取得 + # 検索操作 + # ハッシュテーブルにキー key を入力し、値 value を取得 name: str = hmap[15937] # 削除操作 - # ハッシュ表からキー値ペア (key, value) を削除 + # ハッシュテーブルからキーと値のペア (key, value) を削除 hmap.pop(10583) ``` === "C++" ```cpp title="hash_map.cpp" - /* ハッシュ表を初期化 */ + /* ハッシュテーブルを初期化 */ unordered_map map; /* 追加操作 */ - // ハッシュ表にキー値ペア (key, value) を追加 - map[12836] = "小哈"; - map[15937] = "小啰"; - map[16750] = "小算"; - map[13276] = "小法"; - map[10583] = "小鸭"; + // ハッシュテーブルにキーと値のペア (key, value) を追加 + map[12836] = "シャオハ"; + map[15937] = "シャオルオ"; + map[16750] = "シャオスワン"; + map[13276] = "シャオファ"; + map[10583] = "シャオヤー"; - /* クエリ操作 */ - // ハッシュ表にキーを入力し、値を取得 + /* 検索操作 */ + // ハッシュテーブルにキー key を入力し、値 value を取得 string name = map[15937]; /* 削除操作 */ - // ハッシュ表からキー値ペア (key, value) を削除 + // ハッシュテーブルからキーと値のペア (key, value) を削除 map.erase(10583); ``` === "Java" ```java title="hash_map.java" - /* ハッシュ表を初期化 */ + /* ハッシュテーブルを初期化 */ Map map = new HashMap<>(); /* 追加操作 */ - // ハッシュ表にキー値ペア (key, value) を追加 - map.put(12836, "小哈"); - map.put(15937, "小啰"); - map.put(16750, "小算"); - map.put(13276, "小法"); - map.put(10583, "小鸭"); + // ハッシュテーブルにキーと値のペア (key, value) を追加 + map.put(12836, "シャオハ"); + map.put(15937, "シャオルオ"); + map.put(16750, "シャオスワン"); + map.put(13276, "シャオファ"); + map.put(10583, "シャオヤー"); - /* クエリ操作 */ - // ハッシュ表にキーを入力し、値を取得 + /* 検索操作 */ + // ハッシュテーブルにキー key を入力し、値 value を取得 String name = map.get(15937); /* 削除操作 */ - // ハッシュ表からキー値ペア (key, value) を削除 + // ハッシュテーブルからキーと値のペア (key, value) を削除 map.remove(10583); ``` === "C#" ```csharp title="hash_map.cs" - /* ハッシュ表を初期化 */ + /* ハッシュテーブルを初期化 */ Dictionary map = new() { /* 追加操作 */ - // ハッシュ表にキー値ペア (key, value) を追加 - { 12836, "小哈" }, - { 15937, "小啰" }, - { 16750, "小算" }, - { 13276, "小法" }, - { 10583, "小鸭" } + // ハッシュテーブルにキーと値のペア (key, value) を追加 + { 12836, "シャオハ" }, + { 15937, "シャオルオ" }, + { 16750, "シャオスワン" }, + { 13276, "シャオファ" }, + { 10583, "シャオヤー" } }; - /* クエリ操作 */ - // ハッシュ表にキーを入力し、値を取得 + /* 検索操作 */ + // ハッシュテーブルにキー key を入力し、値 value を取得 string name = map[15937]; /* 削除操作 */ - // ハッシュ表からキー値ペア (key, value) を削除 + // ハッシュテーブルからキーと値のペア (key, value) を削除 map.Remove(10583); ``` === "Go" ```go title="hash_map_test.go" - /* ハッシュ表を初期化 */ + /* ハッシュテーブルを初期化 */ hmap := make(map[int]string) /* 追加操作 */ - // ハッシュ表にキー値ペア (key, value) を追加 - hmap[12836] = "小哈" - hmap[15937] = "小啰" - hmap[16750] = "小算" - hmap[13276] = "小法" - hmap[10583] = "小鸭" + // ハッシュテーブルにキーと値のペア (key, value) を追加 + hmap[12836] = "シャオハ" + hmap[15937] = "シャオルオ" + hmap[16750] = "シャオスワン" + hmap[13276] = "シャオファ" + hmap[10583] = "シャオヤー" - /* クエリ操作 */ - // ハッシュ表にキーを入力し、値を取得 + /* 検索操作 */ + // ハッシュテーブルにキー key を入力し、値 value を取得 name := hmap[15937] /* 削除操作 */ - // ハッシュ表からキー値ペア (key, value) を削除 + // ハッシュテーブルからキーと値のペア (key, value) を削除 delete(hmap, 10583) ``` === "Swift" ```swift title="hash_map.swift" - /* ハッシュ表を初期化 */ + /* ハッシュテーブルを初期化 */ var map: [Int: String] = [:] /* 追加操作 */ - // ハッシュ表にキー値ペア (key, value) を追加 - map[12836] = "小哈" - map[15937] = "小啰" - map[16750] = "小算" - map[13276] = "小法" - map[10583] = "小鸭" + // ハッシュテーブルにキーと値のペア (key, value) を追加 + map[12836] = "シャオハ" + map[15937] = "シャオルオ" + map[16750] = "シャオスワン" + map[13276] = "シャオファ" + map[10583] = "シャオヤー" - /* クエリ操作 */ - // ハッシュ表にキーを入力し、値を取得 + /* 検索操作 */ + // ハッシュテーブルにキー key を入力し、値 value を取得 let name = map[15937]! /* 削除操作 */ - // ハッシュ表からキー値ペア (key, value) を削除 + // ハッシュテーブルからキーと値のペア (key, value) を削除 map.removeValue(forKey: 10583) ``` === "JS" ```javascript title="hash_map.js" - /* ハッシュ表を初期化 */ + /* ハッシュテーブルを初期化 */ const map = new Map(); /* 追加操作 */ - // ハッシュ表にキー値ペア (key, value) を追加 - map.set(12836, '小哈'); - map.set(15937, '小啰'); - map.set(16750, '小算'); - map.set(13276, '小法'); - map.set(10583, '小鸭'); + // ハッシュテーブルにキーと値のペア (key, value) を追加 + map.set(12836, 'シャオハ'); + map.set(15937, 'シャオルオ'); + map.set(16750, 'シャオスワン'); + map.set(13276, 'シャオファ'); + map.set(10583, 'シャオヤー'); - /* クエリ操作 */ - // ハッシュ表にキーを入力し、値を取得 + /* 検索操作 */ + // ハッシュテーブルにキー key を入力し、値 value を取得 let name = map.get(15937); /* 削除操作 */ - // ハッシュ表からキー値ペア (key, value) を削除 + // ハッシュテーブルからキーと値のペア (key, value) を削除 map.delete(10583); ``` === "TS" ```typescript title="hash_map.ts" - /* ハッシュ表を初期化 */ + /* ハッシュテーブルを初期化 */ const map = new Map(); /* 追加操作 */ - // ハッシュ表にキー値ペア (key, value) を追加 - map.set(12836, '小哈'); - map.set(15937, '小啰'); - map.set(16750, '小算'); - map.set(13276, '小法'); - map.set(10583, '小鸭'); - console.info('\n追加後、ハッシュ表は\nKey -> Value'); + // ハッシュテーブルにキーと値のペア (key, value) を追加 + map.set(12836, 'シャオハ'); + map.set(15937, 'シャオルオ'); + map.set(16750, 'シャオスワン'); + map.set(13276, 'シャオファ'); + map.set(10583, 'シャオヤー'); + console.info('\n追加後のハッシュテーブルは次のとおりです\nKey -> Value'); console.info(map); - /* クエリ操作 */ - // ハッシュ表にキーを入力し、値を取得 + /* 検索操作 */ + // ハッシュテーブルにキー key を入力し、値 value を取得 let name = map.get(15937); - console.info('\n学籍番号15937を入力、名前を問い合わせ ' + name); + console.info('\n学籍番号 15937 を入力し、名前を検索: ' + name); /* 削除操作 */ - // ハッシュ表からキー値ペア (key, value) を削除 + // ハッシュテーブルからキーと値のペア (key, value) を削除 map.delete(10583); - console.info('\n10583を削除後、ハッシュ表は\nKey -> Value'); + console.info('\n10583 を削除した後のハッシュテーブル\nKey -> Value'); console.info(map); ``` === "Dart" ```dart title="hash_map.dart" - /* ハッシュ表を初期化 */ + /* ハッシュテーブルを初期化 */ Map map = {}; /* 追加操作 */ - // ハッシュ表にキー値ペア (key, value) を追加 - map[12836] = "小哈"; - map[15937] = "小啰"; - map[16750] = "小算"; - map[13276] = "小法"; - map[10583] = "小鸭"; + // ハッシュテーブルにキーと値のペア (key, value) を追加 + map[12836] = "シャオハ"; + map[15937] = "シャオルオ"; + map[16750] = "シャオスワン"; + map[13276] = "シャオファ"; + map[10583] = "シャオヤー"; - /* クエリ操作 */ - // ハッシュ表にキーを入力し、値を取得 + /* 検索操作 */ + // ハッシュテーブルにキー key を入力し、値 value を取得 String name = map[15937]; /* 削除操作 */ - // ハッシュ表からキー値ペア (key, value) を削除 + // ハッシュテーブルからキーと値のペア (key, value) を削除 map.remove(10583); ``` @@ -251,51 +251,95 @@ comments: true ```rust title="hash_map.rs" use std::collections::HashMap; - /* ハッシュ表を初期化 */ + /* ハッシュテーブルを初期化 */ let mut map: HashMap = HashMap::new(); /* 追加操作 */ - // ハッシュ表にキー値ペア (key, value) を追加 - map.insert(12836, "小哈".to_string()); - map.insert(15937, "小啰".to_string()); - map.insert(16750, "小算".to_string()); - map.insert(13279, "小法".to_string()); - map.insert(10583, "小鸭".to_string()); + // ハッシュテーブルにキーと値のペア (key, value) を追加 + map.insert(12836, "シャオハ".to_string()); + map.insert(15937, "シャオルオ".to_string()); + map.insert(16750, "シャオスワン".to_string()); + map.insert(13279, "シャオファ".to_string()); + map.insert(10583, "シャオヤー".to_string()); - /* クエリ操作 */ - // ハッシュ表にキーを入力し、値を取得 + /* 検索操作 */ + // ハッシュテーブルにキー key を入力し、値 value を取得 let _name: Option<&String> = map.get(&15937); /* 削除操作 */ - // ハッシュ表からキー値ペア (key, value) を削除 + // ハッシュテーブルからキーと値のペア (key, value) を削除 let _removed_value: Option = map.remove(&10583); ``` === "C" ```c title="hash_map.c" - // Cには組み込みのハッシュ表が提供されていません + // C には組み込みのハッシュテーブルはありません ``` === "Kotlin" ```kotlin title="hash_map.kt" + /* ハッシュテーブルを初期化 */ + val map = HashMap() + /* 追加操作 */ + // ハッシュテーブルにキーと値のペア (key, value) を追加 + map[12836] = "シャオハ" + map[15937] = "シャオルオ" + map[16750] = "シャオスワン" + map[13276] = "シャオファ" + map[10583] = "シャオヤー" + + /* 検索操作 */ + // ハッシュテーブルにキー key を入力し、値 value を取得 + val name = map[15937] + + /* 削除操作 */ + // ハッシュテーブルからキーと値のペア (key, value) を削除 + map.remove(10583) ``` -ハッシュ表を走査する一般的な方法は3つあります:キー値ペアの走査、キーの走査、値の走査。以下はコード例です: +=== "Ruby" + + ```ruby title="hash_map.rb" + # ハッシュテーブルを初期化 + hmap = {} + + # 追加操作 + # ハッシュテーブルにキーと値のペア (key, value) を追加 + hmap[12836] = "シャオハ" + hmap[15937] = "シャオルオ" + hmap[16750] = "シャオスワン" + hmap[13276] = "シャオファ" + hmap[10583] = "シャオヤー" + + # 検索操作 + # ハッシュテーブルにキー key を入力し、値 value を取得 + name = hmap[15937] + + # 削除操作 + # ハッシュテーブルからキーと値のペア (key, value) を削除 + hmap.delete(10583) + ``` + +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + +ハッシュテーブルには、キーと値のペア、キー、値を走査する 3 つの一般的な方法があります。コード例は以下のとおりです: === "Python" ```python title="hash_map.py" - # ハッシュ表を走査 - # キー値ペア key->value を走査 + # ハッシュテーブルを走査 + # キーと値のペア key->value を走査 for key, value in hmap.items(): print(key, "->", value) - # キーのみを走査 + # キー key のみを走査 for key in hmap.keys(): print(key) - # 値のみを走査 + # 値 value のみを走査 for value in hmap.values(): print(value) ``` @@ -303,12 +347,12 @@ comments: true === "C++" ```cpp title="hash_map.cpp" - /* ハッシュ表を走査 */ - // キー値ペア key->value を走査 + /* ハッシュテーブルを走査 */ + // キーと値のペア key->value を走査 for (auto kv: map) { cout << kv.first << " -> " << kv.second << endl; } - // イテレータを使用してキー値ペア key->value を走査 + // イテレータを使って key->value を走査 for (auto iter = map.begin(); iter != map.end(); iter++) { cout << iter->first << "->" << iter->second << endl; } @@ -317,16 +361,16 @@ comments: true === "Java" ```java title="hash_map.java" - /* ハッシュ表を走査 */ - // キー値ペア key->value を走査 - for (Map.Entry kv: map.entrySet()) { + /* ハッシュテーブルを走査 */ + // キーと値のペア key->value を走査 + for (Map.Entry kv: map.entrySet()) { System.out.println(kv.getKey() + " -> " + kv.getValue()); } - // キーのみを走査 + // キー key のみを走査 for (int key: map.keySet()) { System.out.println(key); } - // 値のみを走査 + // 値 value のみを走査 for (String val: map.values()) { System.out.println(val); } @@ -335,16 +379,16 @@ comments: true === "C#" ```csharp title="hash_map.cs" - /* ハッシュ表を走査 */ - // キー値ペア Key->Value を走査 + /* ハッシュテーブルを走査 */ + // キーと値のペア Key->Value を走査 foreach (var kv in map) { Console.WriteLine(kv.Key + " -> " + kv.Value); } - // キーのみを走査 + // キー key のみを走査 foreach (int key in map.Keys) { Console.WriteLine(key); } - // 値のみを走査 + // 値 value のみを走査 foreach (string val in map.Values) { Console.WriteLine(val); } @@ -353,16 +397,16 @@ comments: true === "Go" ```go title="hash_map_test.go" - /* ハッシュ表を走査 */ - // キー値ペア key->value を走査 + /* ハッシュテーブルを走査 */ + // キーと値のペア key->value を走査 for key, value := range hmap { fmt.Println(key, "->", value) } - // キーのみを走査 + // キー key のみを走査 for key := range hmap { fmt.Println(key) } - // 値のみを走査 + // 値 value のみを走査 for _, value := range hmap { fmt.Println(value) } @@ -371,16 +415,16 @@ comments: true === "Swift" ```swift title="hash_map.swift" - /* ハッシュ表を走査 */ - // キー値ペア Key->Value を走査 + /* ハッシュテーブルを走査 */ + // キーと値のペア Key->Value を走査 for (key, value) in map { print("\(key) -> \(value)") } - // キーのみを走査 + // キー Key のみを走査 for key in map.keys { print(key) } - // 値のみを走査 + // 値 Value のみを走査 for value in map.values { print(value) } @@ -389,16 +433,16 @@ comments: true === "JS" ```javascript title="hash_map.js" - /* ハッシュ表を走査 */ - console.info('\nキー値ペア Key->Value を走査'); + /* ハッシュテーブルを走査 */ + console.info('\nキーと値のペア Key->Value を走査'); for (const [k, v] of map.entries()) { console.info(k + ' -> ' + v); } - console.info('\nキーのみを走査 Key'); + console.info('\nキー Key のみを走査'); for (const k of map.keys()) { console.info(k); } - console.info('\n値のみを走査 Value'); + console.info('\n値 Value のみを走査'); for (const v of map.values()) { console.info(v); } @@ -407,16 +451,16 @@ comments: true === "TS" ```typescript title="hash_map.ts" - /* ハッシュ表を走査 */ - console.info('\nキー値ペア Key->Value を走査'); + /* ハッシュテーブルを走査 */ + console.info('\nキーと値のペア Key->Value を走査'); for (const [k, v] of map.entries()) { console.info(k + ' -> ' + v); } - console.info('\nキーのみを走査 Key'); + console.info('\nキー Key のみを走査'); for (const k of map.keys()) { console.info(k); } - console.info('\n値のみを走査 Value'); + console.info('\n値 Value のみを走査'); for (const v of map.values()) { console.info(v); } @@ -425,38 +469,38 @@ comments: true === "Dart" ```dart title="hash_map.dart" - /* ハッシュ表を走査 */ - // キー値ペア Key->Value を走査 + /* ハッシュテーブルを走査 */ + // キーと値のペア Key->Value を走査 map.forEach((key, value) { - print('$key -> $value'); + print('$key -> $value'); }); - // キーのみを走査 Key + // キー Key のみを走査 map.keys.forEach((key) { - print(key); + print(key); }); - // 値のみを走査 Value + // 値 Value のみを走査 map.values.forEach((value) { - print(value); + print(value); }); ``` === "Rust" ```rust title="hash_map.rs" - /* ハッシュ表を走査 */ - // キー値ペア Key->Value を走査 + /* ハッシュテーブルを走査 */ + // キーと値のペア Key->Value を走査 for (key, value) in &map { println!("{key} -> {value}"); } - // キーのみを走査 Key + // キー Key のみを走査 for key in map.keys() { println!("{key}"); } - // 値のみを走査 Value + // 値 Value のみを走査 for value in map.values() { println!("{value}"); } @@ -465,56 +509,86 @@ comments: true === "C" ```c title="hash_map.c" - // Cには組み込みのハッシュ表が提供されていません + // C には組み込みのハッシュテーブルはありません ``` === "Kotlin" ```kotlin title="hash_map.kt" - + /* ハッシュテーブルを走査 */ + // キーと値のペア key->value を走査 + for ((key, value) in map) { + println("$key -> $value") + } + // キー key のみを走査 + for (key in map.keys) { + println(key) + } + // 値 value のみを走査 + for (_val in map.values) { + println(_val) + } ``` -## 6.1.2   ハッシュ表の簡単な実装 +=== "Ruby" -まず、最も簡単なケースを考えてみましょう:**配列のみを使ってハッシュ表を実装すること**。ハッシュ表において、配列の各空きスロットはバケットと呼ばれ、各バケットはキー値ペアを格納できます。したがって、クエリ操作は`key`に対応するバケットを見つけ、そこから`value`を取得することになります。 + ```ruby title="hash_map.rb" + # ハッシュテーブルを走査 + # キーと値のペア key->value を走査 + hmap.entries.each { |key, value| puts "#{key} -> #{value}" } -では、`key`に基づいて対応するバケットをどのように特定するのでしょうか?これはハッシュ関数によって実現されます。ハッシュ関数の役割は、より大きな入力空間をより小さな出力空間にマッピングすることです。ハッシュ表では、入力空間はすべてのキーで構成され、出力空間はすべてのバケット(配列インデックス)で構成されます。つまり、`key`が与えられた場合、**ハッシュ関数を使用して対応するキー値ペアの配列内の格納位置を決定できます**。 + # キー key のみを走査 + hmap.keys.each { |key| puts key } -与えられた`key`に対して、ハッシュ関数の計算は2つのステップで構成されます: + # 値 value のみを走査 + hmap.values.each { |val| puts val } + ``` -1. 特定のハッシュアルゴリズム`hash()`を使用してハッシュ値を計算します。 -2. ハッシュ値をバケット数(配列長)`capacity`で剰余を取り、キーに対応する配列`index`を取得します。 +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + +## 6.1.2   ハッシュテーブルの簡単な実装 + +まずは最も単純なケースとして、**1 つの配列だけでハッシュテーブルを実装する**ことを考えます。ハッシュテーブルでは、配列中の各空き位置をバケット(bucket)と呼び、各バケットには 1 つのキーと値のペアを格納できます。したがって、検索操作とは `key` に対応するバケットを見つけ、そのバケットから `value` を取得することです。 + +では、`key` に基づいて対応するバケットをどのように特定するのでしょうか。これはハッシュ関数(hash function)によって実現されます。ハッシュ関数の役割は、大きな入力空間をより小さな出力空間に写像することです。ハッシュテーブルでは、入力空間はすべての `key` 、出力空間はすべてのバケット(配列インデックス)です。言い換えると、`key` を入力すると、**ハッシュ関数によってその `key` に対応するキーと値のペアの配列内での格納位置を求められます**。 + +`key` を入力したとき、ハッシュ関数の計算過程は次の 2 段階に分かれます。 + +1. あるハッシュアルゴリズム `hash()` を用いてハッシュ値を計算します。 +2. ハッシュ値をバケット数(配列長)`capacity` で剰余し、その `key` に対応するバケット(配列インデックス)`index` を求めます。 ```shell index = hash(key) % capacity ``` -その後、`index`を使用してハッシュ表内の対応するバケットにアクセスし、`value`を取得できます。 +その後、`index` を使ってハッシュテーブル内の対応するバケットにアクセスし、`value` を取得できます。 -配列長が`capacity = 100`で、ハッシュアルゴリズムが`hash(key) = key`として定義されているとします。したがって、ハッシュ関数は`key % 100`として表現できます。以下の図は、`key`を学籍番号、`value`を名前として、ハッシュ関数の動作原理を示しています。 +配列長を `capacity = 100` 、ハッシュアルゴリズムを `hash(key) = key` とすると、ハッシュ関数は `key % 100` となります。次の図では、`key` を学籍番号、`value` を名前の例として、ハッシュ関数の動作原理を示します。 ![ハッシュ関数の動作原理](hash_map.assets/hash_function.png){ class="animation-figure" }

図 6-2   ハッシュ関数の動作原理

-以下のコードは簡単なハッシュ表を実装しています。ここでは、`key`と`value`を`Pair`クラスにカプセル化してキー値ペアを表現しています。 +以下のコードは、単純なハッシュテーブルを実装したものです。ここでは、キーと値のペアを表すために `key` と `value` をクラス `Pair` にまとめています。 === "Python" ```python title="array_hash_map.py" class Pair: - """キー値ペア""" + """キーと値の組""" def __init__(self, key: int, val: str): self.key = key self.val = val class ArrayHashMap: - """配列実装に基づくハッシュテーブル""" + """配列ベースのハッシュテーブル""" def __init__(self): """コンストラクタ""" - # 100個のバケットを含む配列を初期化 + # 100 個のバケットを含む配列を初期化 self.buckets: list[Pair | None] = [None] * 100 def hash_func(self, key: int) -> int: @@ -522,8 +596,8 @@ index = hash(key) % capacity index = key % 100 return index - def get(self, key: int) -> str: - """照会操作""" + def get(self, key: int) -> str | None: + """検索操作""" index: int = self.hash_func(key) pair: Pair = self.buckets[index] if pair is None: @@ -531,7 +605,7 @@ index = hash(key) % capacity return pair.val def put(self, key: int, val: str): - """追加操作""" + """追加と更新の操作""" pair = Pair(key, val) index: int = self.hash_func(key) self.buckets[index] = pair @@ -539,11 +613,11 @@ index = hash(key) % capacity def remove(self, key: int): """削除操作""" index: int = self.hash_func(key) - # None に設定し、削除を表現 + # None に設定し、削除を表す self.buckets[index] = None def entry_set(self) -> list[Pair]: - """すべてのキー値ペアを取得""" + """すべてのキーと値のペアを取得""" result: list[Pair] = [] for pair in self.buckets: if pair is not None: @@ -576,7 +650,7 @@ index = hash(key) % capacity === "C++" ```cpp title="array_hash_map.cpp" - /* キー値ペア */ + /* キーと値の組 */ struct Pair { public: int key; @@ -587,19 +661,19 @@ index = hash(key) % capacity } }; - /* 配列実装に基づくハッシュテーブル */ + /* 配列ベースのハッシュテーブル */ class ArrayHashMap { private: vector buckets; public: ArrayHashMap() { - // 配列を初期化、100個のバケットを含む + // 100 個のバケットを含む配列を初期化 buckets = vector(100); } ~ArrayHashMap() { - // メモリを解放 + // メモリを解放する for (const auto &bucket : buckets) { delete bucket; } @@ -612,7 +686,7 @@ index = hash(key) % capacity return index; } - /* クエリ操作 */ + /* 検索操作 */ string get(int key) { int index = hashFunc(key); Pair *pair = buckets[index]; @@ -631,12 +705,12 @@ index = hash(key) % capacity /* 削除操作 */ void remove(int key) { int index = hashFunc(key); - // メモリを解放してnullptrに設定 + // メモリを解放して nullptr に設定する delete buckets[index]; buckets[index] = nullptr; } - /* すべてのキー値ペアを取得 */ + /* すべてのキーと値のペアを取得 */ vector pairSet() { vector pairSet; for (Pair *pair : buckets) { @@ -669,7 +743,7 @@ index = hash(key) % capacity return valueSet; } - /* ハッシュテーブルを印刷 */ + /* ハッシュテーブルを出力 */ void print() { for (Pair *kv : pairSet()) { cout << kv->key << " -> " << kv->val << endl; @@ -681,7 +755,7 @@ index = hash(key) % capacity === "Java" ```java title="array_hash_map.java" - /* キー値ペア */ + /* キーと値の組 */ class Pair { public int key; public String val; @@ -692,12 +766,12 @@ index = hash(key) % capacity } } - /* 配列実装に基づくハッシュテーブル */ + /* 配列ベースのハッシュテーブル */ class ArrayHashMap { private List buckets; public ArrayHashMap() { - // 100個のバケットを含む配列を初期化 + // 100 個のバケットを含む配列を初期化 buckets = new ArrayList<>(); for (int i = 0; i < 100; i++) { buckets.add(null); @@ -710,7 +784,7 @@ index = hash(key) % capacity return index; } - /* クエリ操作 */ + /* 検索操作 */ public String get(int key) { int index = hashFunc(key); Pair pair = buckets.get(index); @@ -729,11 +803,11 @@ index = hash(key) % capacity /* 削除操作 */ public void remove(int key) { int index = hashFunc(key); - // nullに設定して削除を示す + // null に設定し、削除を表す buckets.set(index, null); } - /* すべてのキー値ペアを取得 */ + /* すべてのキーと値のペアを取得 */ public List pairSet() { List pairSet = new ArrayList<>(); for (Pair pair : buckets) { @@ -763,7 +837,7 @@ index = hash(key) % capacity return valueSet; } - /* ハッシュテーブルを印刷 */ + /* ハッシュテーブルを出力 */ public void print() { for (Pair kv : pairSet()) { System.out.println(kv.key + " -> " + kv.val); @@ -775,108 +849,938 @@ index = hash(key) % capacity === "C#" ```csharp title="array_hash_map.cs" - [class]{Pair}-[func]{} + /* キーと値の組 int->string */ + class Pair(int key, string val) { + public int key = key; + public string val = val; + } - [class]{ArrayHashMap}-[func]{} + /* 配列ベースのハッシュテーブル */ + class ArrayHashMap { + List buckets; + public ArrayHashMap() { + // 100 個のバケットを含む配列を初期化 + buckets = []; + for (int i = 0; i < 100; i++) { + buckets.Add(null); + } + } + + /* ハッシュ関数 */ + int HashFunc(int key) { + int index = key % 100; + return index; + } + + /* 検索操作 */ + public string? Get(int key) { + int index = HashFunc(key); + Pair? pair = buckets[index]; + if (pair == null) return null; + return pair.val; + } + + /* 追加操作 */ + public void Put(int key, string val) { + Pair pair = new(key, val); + int index = HashFunc(key); + buckets[index] = pair; + } + + /* 削除操作 */ + public void Remove(int key) { + int index = HashFunc(key); + // null に設定し、削除を表す + buckets[index] = null; + } + + /* すべてのキーと値のペアを取得 */ + public List PairSet() { + List pairSet = []; + foreach (Pair? pair in buckets) { + if (pair != null) + pairSet.Add(pair); + } + return pairSet; + } + + /* すべてのキーを取得 */ + public List KeySet() { + List keySet = []; + foreach (Pair? pair in buckets) { + if (pair != null) + keySet.Add(pair.key); + } + return keySet; + } + + /* すべての値を取得 */ + public List ValueSet() { + List valueSet = []; + foreach (Pair? pair in buckets) { + if (pair != null) + valueSet.Add(pair.val); + } + return valueSet; + } + + /* ハッシュテーブルを出力 */ + public void Print() { + foreach (Pair kv in PairSet()) { + Console.WriteLine(kv.key + " -> " + kv.val); + } + } + } ``` === "Go" ```go title="array_hash_map.go" - [class]{pair}-[func]{} + /* キーと値の組 */ + type pair struct { + key int + val string + } - [class]{arrayHashMap}-[func]{} + /* 配列ベースのハッシュテーブル */ + type arrayHashMap struct { + buckets []*pair + } + + /* ハッシュテーブルを初期化 */ + func newArrayHashMap() *arrayHashMap { + // 100 個のバケットを含む配列を初期化 + buckets := make([]*pair, 100) + return &arrayHashMap{buckets: buckets} + } + + /* ハッシュ関数 */ + func (a *arrayHashMap) hashFunc(key int) int { + index := key % 100 + return index + } + + /* 検索操作 */ + func (a *arrayHashMap) get(key int) string { + index := a.hashFunc(key) + pair := a.buckets[index] + if pair == nil { + return "Not Found" + } + return pair.val + } + + /* 追加操作 */ + func (a *arrayHashMap) put(key int, val string) { + pair := &pair{key: key, val: val} + index := a.hashFunc(key) + a.buckets[index] = pair + } + + /* 削除操作 */ + func (a *arrayHashMap) remove(key int) { + index := a.hashFunc(key) + // nil に設定し、削除を表す + a.buckets[index] = nil + } + + /* すべてのキーのペアを取得する */ + func (a *arrayHashMap) pairSet() []*pair { + var pairs []*pair + for _, pair := range a.buckets { + if pair != nil { + pairs = append(pairs, pair) + } + } + return pairs + } + + /* すべてのキーを取得 */ + func (a *arrayHashMap) keySet() []int { + var keys []int + for _, pair := range a.buckets { + if pair != nil { + keys = append(keys, pair.key) + } + } + return keys + } + + /* すべての値を取得 */ + func (a *arrayHashMap) valueSet() []string { + var values []string + for _, pair := range a.buckets { + if pair != nil { + values = append(values, pair.val) + } + } + return values + } + + /* ハッシュテーブルを出力 */ + func (a *arrayHashMap) print() { + for _, pair := range a.buckets { + if pair != nil { + fmt.Println(pair.key, "->", pair.val) + } + } + } ``` === "Swift" ```swift title="array_hash_map.swift" - [file]{utils/pair.swift}-[class]{Pair}-[func]{} + /* キーと値の組 */ + class Pair: Equatable { + public var key: Int + public var val: String - [class]{ArrayHashMap}-[func]{} + public init(key: Int, val: String) { + self.key = key + self.val = val + } + + public static func == (lhs: Pair, rhs: Pair) -> Bool { + lhs.key == rhs.key && lhs.val == rhs.val + } + } + + /* 配列ベースのハッシュテーブル */ + class ArrayHashMap { + private var buckets: [Pair?] + + init() { + // 100 個のバケットを含む配列を初期化 + buckets = Array(repeating: nil, count: 100) + } + + /* ハッシュ関数 */ + private func hashFunc(key: Int) -> Int { + let index = key % 100 + return index + } + + /* 検索操作 */ + func get(key: Int) -> String? { + let index = hashFunc(key: key) + let pair = buckets[index] + return pair?.val + } + + /* 追加操作 */ + func put(key: Int, val: String) { + let pair = Pair(key: key, val: val) + let index = hashFunc(key: key) + buckets[index] = pair + } + + /* 削除操作 */ + func remove(key: Int) { + let index = hashFunc(key: key) + // nil に設定し、削除を表す + buckets[index] = nil + } + + /* すべてのキーと値のペアを取得 */ + func pairSet() -> [Pair] { + buckets.compactMap { $0 } + } + + /* すべてのキーを取得 */ + func keySet() -> [Int] { + buckets.compactMap { $0?.key } + } + + /* すべての値を取得 */ + func valueSet() -> [String] { + buckets.compactMap { $0?.val } + } + + /* ハッシュテーブルを出力 */ + func print() { + for pair in pairSet() { + Swift.print("\(pair.key) -> \(pair.val)") + } + } + } ``` === "JS" ```javascript title="array_hash_map.js" - [class]{Pair}-[func]{} + /* キーと値の組 Number -> String */ + class Pair { + constructor(key, val) { + this.key = key; + this.val = val; + } + } - [class]{ArrayHashMap}-[func]{} + /* 配列ベースのハッシュテーブル */ + class ArrayHashMap { + #buckets; + constructor() { + // 100 個のバケットを含む配列を初期化 + this.#buckets = new Array(100).fill(null); + } + + /* ハッシュ関数 */ + #hashFunc(key) { + return key % 100; + } + + /* 検索操作 */ + get(key) { + let index = this.#hashFunc(key); + let pair = this.#buckets[index]; + if (pair === null) return null; + return pair.val; + } + + /* 追加操作 */ + set(key, val) { + let index = this.#hashFunc(key); + this.#buckets[index] = new Pair(key, val); + } + + /* 削除操作 */ + delete(key) { + let index = this.#hashFunc(key); + // null に設定し、削除を表す + this.#buckets[index] = null; + } + + /* すべてのキーと値のペアを取得 */ + entries() { + let arr = []; + for (let i = 0; i < this.#buckets.length; i++) { + if (this.#buckets[i]) { + arr.push(this.#buckets[i]); + } + } + return arr; + } + + /* すべてのキーを取得 */ + keys() { + let arr = []; + for (let i = 0; i < this.#buckets.length; i++) { + if (this.#buckets[i]) { + arr.push(this.#buckets[i].key); + } + } + return arr; + } + + /* すべての値を取得 */ + values() { + let arr = []; + for (let i = 0; i < this.#buckets.length; i++) { + if (this.#buckets[i]) { + arr.push(this.#buckets[i].val); + } + } + return arr; + } + + /* ハッシュテーブルを出力 */ + print() { + let pairSet = this.entries(); + for (const pair of pairSet) { + console.info(`${pair.key} -> ${pair.val}`); + } + } + } ``` === "TS" ```typescript title="array_hash_map.ts" - [class]{Pair}-[func]{} + /* キーと値の組 Number -> String */ + class Pair { + public key: number; + public val: string; - [class]{ArrayHashMap}-[func]{} + constructor(key: number, val: string) { + this.key = key; + this.val = val; + } + } + + /* 配列ベースのハッシュテーブル */ + class ArrayHashMap { + private readonly buckets: (Pair | null)[]; + + constructor() { + // 100 個のバケットを含む配列を初期化 + this.buckets = new Array(100).fill(null); + } + + /* ハッシュ関数 */ + private hashFunc(key: number): number { + return key % 100; + } + + /* 検索操作 */ + public get(key: number): string | null { + let index = this.hashFunc(key); + let pair = this.buckets[index]; + if (pair === null) return null; + return pair.val; + } + + /* 追加操作 */ + public set(key: number, val: string) { + let index = this.hashFunc(key); + this.buckets[index] = new Pair(key, val); + } + + /* 削除操作 */ + public delete(key: number) { + let index = this.hashFunc(key); + // null に設定し、削除を表す + this.buckets[index] = null; + } + + /* すべてのキーと値のペアを取得 */ + public entries(): (Pair | null)[] { + let arr: (Pair | null)[] = []; + for (let i = 0; i < this.buckets.length; i++) { + if (this.buckets[i]) { + arr.push(this.buckets[i]); + } + } + return arr; + } + + /* すべてのキーを取得 */ + public keys(): (number | undefined)[] { + let arr: (number | undefined)[] = []; + for (let i = 0; i < this.buckets.length; i++) { + if (this.buckets[i]) { + arr.push(this.buckets[i].key); + } + } + return arr; + } + + /* すべての値を取得 */ + public values(): (string | undefined)[] { + let arr: (string | undefined)[] = []; + for (let i = 0; i < this.buckets.length; i++) { + if (this.buckets[i]) { + arr.push(this.buckets[i].val); + } + } + return arr; + } + + /* ハッシュテーブルを出力 */ + public print() { + let pairSet = this.entries(); + for (const pair of pairSet) { + console.info(`${pair.key} -> ${pair.val}`); + } + } + } ``` === "Dart" ```dart title="array_hash_map.dart" - [class]{Pair}-[func]{} + /* キーと値の組 */ + class Pair { + int key; + String val; + Pair(this.key, this.val); + } - [class]{ArrayHashMap}-[func]{} + /* 配列ベースのハッシュテーブル */ + class ArrayHashMap { + late List _buckets; + + ArrayHashMap() { + // 100 個のバケットを含む配列を初期化 + _buckets = List.filled(100, null); + } + + /* ハッシュ関数 */ + int _hashFunc(int key) { + final int index = key % 100; + return index; + } + + /* 検索操作 */ + String? get(int key) { + final int index = _hashFunc(key); + final Pair? pair = _buckets[index]; + if (pair == null) { + return null; + } + return pair.val; + } + + /* 追加操作 */ + void put(int key, String val) { + final Pair pair = Pair(key, val); + final int index = _hashFunc(key); + _buckets[index] = pair; + } + + /* 削除操作 */ + void remove(int key) { + final int index = _hashFunc(key); + _buckets[index] = null; + } + + /* すべてのキーと値のペアを取得 */ + List pairSet() { + List pairSet = []; + for (final Pair? pair in _buckets) { + if (pair != null) { + pairSet.add(pair); + } + } + return pairSet; + } + + /* すべてのキーを取得 */ + List keySet() { + List keySet = []; + for (final Pair? pair in _buckets) { + if (pair != null) { + keySet.add(pair.key); + } + } + return keySet; + } + + /* すべての値を取得 */ + List values() { + List valueSet = []; + for (final Pair? pair in _buckets) { + if (pair != null) { + valueSet.add(pair.val); + } + } + return valueSet; + } + + /* ハッシュテーブルを出力 */ + void printHashMap() { + for (final Pair kv in pairSet()) { + print("${kv.key} -> ${kv.val}"); + } + } + } ``` === "Rust" ```rust title="array_hash_map.rs" - [class]{Pair}-[func]{} + /* キーと値の組 */ + #[derive(Debug, Clone, PartialEq)] + pub struct Pair { + pub key: i32, + pub val: String, + } - [class]{ArrayHashMap}-[func]{} + /* 配列ベースのハッシュテーブル */ + pub struct ArrayHashMap { + buckets: Vec>, + } + + impl ArrayHashMap { + pub fn new() -> ArrayHashMap { + // 100 個のバケットを含む配列を初期化 + Self { + buckets: vec![None; 100], + } + } + + /* ハッシュ関数 */ + fn hash_func(&self, key: i32) -> usize { + key as usize % 100 + } + + /* 検索操作 */ + pub fn get(&self, key: i32) -> Option<&String> { + let index = self.hash_func(key); + self.buckets[index].as_ref().map(|pair| &pair.val) + } + + /* 追加操作 */ + pub fn put(&mut self, key: i32, val: &str) { + let index = self.hash_func(key); + self.buckets[index] = Some(Pair { + key, + val: val.to_string(), + }); + } + + /* 削除操作 */ + pub fn remove(&mut self, key: i32) { + let index = self.hash_func(key); + // None に設定し、削除を表す + self.buckets[index] = None; + } + + /* すべてのキーと値のペアを取得 */ + pub fn entry_set(&self) -> Vec<&Pair> { + self.buckets + .iter() + .filter_map(|pair| pair.as_ref()) + .collect() + } + + /* すべてのキーを取得 */ + pub fn key_set(&self) -> Vec<&i32> { + self.buckets + .iter() + .filter_map(|pair| pair.as_ref().map(|pair| &pair.key)) + .collect() + } + + /* すべての値を取得 */ + pub fn value_set(&self) -> Vec<&String> { + self.buckets + .iter() + .filter_map(|pair| pair.as_ref().map(|pair| &pair.val)) + .collect() + } + + /* ハッシュテーブルを出力 */ + pub fn print(&self) { + for pair in self.entry_set() { + println!("{} -> {}", pair.key, pair.val); + } + } + } ``` === "C" ```c title="array_hash_map.c" - [class]{Pair}-[func]{} + /* キーと値の組 int->string */ + typedef struct { + int key; + char *val; + } Pair; - [class]{ArrayHashMap}-[func]{} + /* 配列ベースのハッシュテーブル */ + typedef struct { + Pair *buckets[MAX_SIZE]; + } ArrayHashMap; + + /* コンストラクタ */ + ArrayHashMap *newArrayHashMap() { + ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap)); + for (int i=0; i < MAX_SIZE; i++) { + hmap->buckets[i] = NULL; + } + return hmap; + } + + /* デストラクタ */ + void delArrayHashMap(ArrayHashMap *hmap) { + for (int i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + free(hmap->buckets[i]->val); + free(hmap->buckets[i]); + } + } + free(hmap); + } + + /* 追加操作 */ + void put(ArrayHashMap *hmap, const int key, const char *val) { + Pair *Pair = malloc(sizeof(Pair)); + Pair->key = key; + Pair->val = malloc(strlen(val) + 1); + strcpy(Pair->val, val); + + int index = hashFunc(key); + hmap->buckets[index] = Pair; + } + + /* 削除操作 */ + void removeItem(ArrayHashMap *hmap, const int key) { + int index = hashFunc(key); + free(hmap->buckets[index]->val); + free(hmap->buckets[index]); + hmap->buckets[index] = NULL; + } + + /* すべてのキーと値のペアを取得 */ + void pairSet(ArrayHashMap *hmap, MapSet *set) { + Pair *entries; + int i = 0, index = 0; + int total = 0; + /* 有効なキーと値のペア数を集計 */ + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + total++; + } + } + entries = malloc(sizeof(Pair) * total); + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + entries[index].key = hmap->buckets[i]->key; + entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1); + strcpy(entries[index].val, hmap->buckets[i]->val); + index++; + } + } + set->set = entries; + set->len = total; + } + + /* すべてのキーを取得 */ + void keySet(ArrayHashMap *hmap, MapSet *set) { + int *keys; + int i = 0, index = 0; + int total = 0; + /* 有効なキーと値のペア数を集計 */ + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + total++; + } + } + keys = malloc(total * sizeof(int)); + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + keys[index] = hmap->buckets[i]->key; + index++; + } + } + set->set = keys; + set->len = total; + } + + /* すべての値を取得 */ + void valueSet(ArrayHashMap *hmap, MapSet *set) { + char **vals; + int i = 0, index = 0; + int total = 0; + /* 有効なキーと値のペア数を集計 */ + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + total++; + } + } + vals = malloc(total * sizeof(char *)); + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + vals[index] = hmap->buckets[i]->val; + index++; + } + } + set->set = vals; + set->len = total; + } + + /* ハッシュテーブルを出力 */ + void print(ArrayHashMap *hmap) { + int i; + MapSet set; + pairSet(hmap, &set); + Pair *entries = (Pair *)set.set; + for (i = 0; i < set.len; i++) { + printf("%d -> %s\n", entries[i].key, entries[i].val); + } + free(set.set); + } ``` === "Kotlin" ```kotlin title="array_hash_map.kt" - [class]{Pair}-[func]{} + /* キーと値の組 */ + class Pair( + var key: Int, + var _val: String + ) - [class]{ArrayHashMap}-[func]{} + /* 配列ベースのハッシュテーブル */ + class ArrayHashMap { + // 100 個のバケットを含む配列を初期化 + private val buckets = arrayOfNulls(100) + + /* ハッシュ関数 */ + fun hashFunc(key: Int): Int { + val index = key % 100 + return index + } + + /* 検索操作 */ + fun get(key: Int): String? { + val index = hashFunc(key) + val pair = buckets[index] ?: return null + return pair._val + } + + /* 追加操作 */ + fun put(key: Int, _val: String) { + val pair = Pair(key, _val) + val index = hashFunc(key) + buckets[index] = pair + } + + /* 削除操作 */ + fun remove(key: Int) { + val index = hashFunc(key) + // null に設定し、削除を表す + buckets[index] = null + } + + /* すべてのキーと値のペアを取得 */ + fun pairSet(): MutableList { + val pairSet = mutableListOf() + for (pair in buckets) { + if (pair != null) + pairSet.add(pair) + } + return pairSet + } + + /* すべてのキーを取得 */ + fun keySet(): MutableList { + val keySet = mutableListOf() + for (pair in buckets) { + if (pair != null) + keySet.add(pair.key) + } + return keySet + } + + /* すべての値を取得 */ + fun valueSet(): MutableList { + val valueSet = mutableListOf() + for (pair in buckets) { + if (pair != null) + valueSet.add(pair._val) + } + return valueSet + } + + /* ハッシュテーブルを出力 */ + fun print() { + for (kv in pairSet()) { + val key = kv.key + val _val = kv._val + println("$key -> $_val") + } + } + } ``` === "Ruby" ```ruby title="array_hash_map.rb" - [class]{Pair}-[func]{} + ### キーと値のペア ### + class Pair + attr_accessor :key, :val - [class]{ArrayHashMap}-[func]{} + def initialize(key, val) + @key = key + @val = val + end + end + + ### 配列で実装したハッシュテーブル ### + class ArrayHashMap + ### コンストラクタ ### + def initialize + # 100 個のバケットを含む配列を初期化 + @buckets = Array.new(100) + end + + ### ハッシュ関数 ### + def hash_func(key) + index = key % 100 + end + + ### 検索操作 ### + def get(key) + index = hash_func(key) + pair = @buckets[index] + + return if pair.nil? + pair.val + end + + ### 追加操作 ### + def put(key, val) + pair = Pair.new(key, val) + index = hash_func(key) + @buckets[index] = pair + end + + ### 削除操作 ### + def remove(key) + index = hash_func(key) + # nil に設定し、削除を表す + @buckets[index] = nil + end + + ### すべてのキーと値のペアを取得 ### + def entry_set + result = [] + @buckets.each { |pair| result << pair unless pair.nil? } + result + end + + ### すべてのキーを取得 ### + def key_set + result = [] + @buckets.each { |pair| result << pair.key unless pair.nil? } + result + end + + ### すべての値を取得 ### + def value_set + result = [] + @buckets.each { |pair| result << pair.val unless pair.nil? } + result + end + + ### ハッシュテーブルを出力 ### + def print + @buckets.each { |pair| puts "#{pair.key} -> #{pair.val}" unless pair.nil? } + end + end ``` -## 6.1.3   ハッシュ衝突とリサイズ +??? pythontutor "コードの可視化" -本質的に、ハッシュ関数の役割は、すべてのキーの入力空間全体を、すべての配列インデックスの出力空間にマッピングすることです。しかし、入力空間は出力空間よりもはるかに大きいことがよくあります。したがって、**理論的には、「複数の入力が同じ出力に対応する」ケースが常に存在します**。 +
+ -上記の例では、与えられたハッシュ関数で、入力`key`の下二桁が同じ場合、ハッシュ関数は同じ出力を生成します。例えば、学籍番号12836と20336の2人の学生をクエリすると、以下のことがわかります: +## 6.1.3   ハッシュ衝突と拡張 + +本質的には、ハッシュ関数の役割は、すべての `key` からなる入力空間を、配列のすべてのインデックスからなる出力空間に写像することです。しかし、入力空間は多くの場合、出力空間よりはるかに大きいため、**理論上は必ず「複数の入力が同じ出力に対応する」状況が存在します**。 + +上の例のハッシュ関数では、入力 `key` の下 2 桁が同じであれば、出力結果も同じになります。たとえば、学籍番号 12836 と 20336 の 2 人の学生を検索すると、次の結果を得ます: ```shell 12836 % 100 = 36 20336 % 100 = 36 ``` -下図に示すように、両方の学籍番号が同じ名前を指しており、これは明らかに間違っています。この複数の入力が同じ出力に対応する状況をハッシュ衝突と呼びます。 +次の図に示すように、2 つの学籍番号が同じ名前を指してしまっており、これは明らかに誤りです。このような、複数の入力が同じ出力に対応する状況をハッシュ衝突(hash collision)と呼びます。 ![ハッシュ衝突の例](hash_map.assets/hash_collision.png){ class="animation-figure" }

図 6-3   ハッシュ衝突の例

-ハッシュ表の容量$n$が増加するにつれて、複数のキーが同じバケットに割り当てられる確率が減少し、衝突が少なくなることは理解しやすいです。したがって、**ハッシュ表をリサイズすることでハッシュ衝突を減らすことができます**。 +容易に分かるように、ハッシュテーブルの容量 $n$ が大きいほど、複数の `key` が同じバケットに割り当てられる確率は低くなり、衝突も少なくなります。したがって、**ハッシュテーブルを拡張することでハッシュ衝突を減らせます**。 -下図に示すように、リサイズ前は、キー値ペア`(136, A)`と`(236, D)`が衝突していました。しかし、リサイズ後は衝突が解決されています。 +次の図に示すように、拡張前はキーと値のペア `(136, A)` と `(236, D)` が衝突していますが、拡張後は衝突が解消されます。 -![ハッシュ表のリサイズ](hash_map.assets/hash_table_reshash.png){ class="animation-figure" } +![ハッシュテーブルの拡張](hash_map.assets/hash_table_reshash.png){ class="animation-figure" } -

図 6-4   ハッシュ表のリサイズ

+

図 6-4   ハッシュテーブルの拡張

-配列の拡張と同様に、ハッシュ表のリサイズにはすべてのキー値ペアを元のハッシュ表から新しいものに移行する必要があり、時間がかかります。さらに、ハッシュ表の`capacity`が変更されるため、ハッシュ関数を使用してすべてのキー値ペアの格納位置を再計算する必要があり、リサイズプロセスの計算オーバーヘッドがさらに増加します。したがって、プログラミング言語は頻繁なリサイズを防ぐために、ハッシュ表に十分大きな容量を割り当てることがよくあります。 +配列の拡張と同様に、ハッシュテーブルの拡張ではすべてのキーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移し替える必要があり、非常に時間がかかります。また、ハッシュテーブルの容量 `capacity` が変わるため、ハッシュ関数を使ってすべてのキーと値のペアの格納位置を再計算しなければならず、これによって拡張過程の計算コストがさらに増加します。そのため、プログラミング言語では通常、頻繁な拡張を防ぐために十分大きなハッシュテーブル容量をあらかじめ確保します。 -負荷率はハッシュ表の重要な概念です。ハッシュ表内の要素数とバケット数の比率として定義されます。ハッシュ衝突の深刻度を測定するために使用され、**しばしばハッシュ表のリサイズのトリガーとしても機能します**。例えば、Javaでは、負荷率が$0.75$を超えると、システムはハッシュ表を元のサイズの2倍にリサイズします。 +負荷率(load factor)はハッシュテーブルにおける重要な概念であり、ハッシュテーブル内の要素数をバケット数で割ったものとして定義され、ハッシュ衝突の深刻さを測るために用いられます。**また、ハッシュテーブル拡張の発動条件としてもよく使われます**。例えば Java では、負荷率が $0.75$ を超えると、システムはハッシュテーブルを元の $2$ 倍に拡張します。 diff --git a/ja/docs/chapter_hashing/index.md b/ja/docs/chapter_hashing/index.md index 79878daf7..aa862e991 100644 --- a/ja/docs/chapter_hashing/index.md +++ b/ja/docs/chapter_hashing/index.md @@ -3,19 +3,19 @@ comments: true icon: material/table-search --- -# 第 6 章   ハッシュ表 +# 第 6 章   ハッシュテーブル -![ハッシュ表](../assets/covers/chapter_hashing.jpg){ class="cover-image" } +![ハッシュテーブル](../assets/covers/chapter_hashing.jpg){ class="cover-image" } !!! abstract - コンピューティングの世界において、ハッシュ表は賢い司書のようなものです。 - - インデックス番号の計算方法を理解し、目的の本を迅速に取得することを可能にします。 + コンピュータの世界では、ハッシュテーブルは聡明な図書館員のような存在です。 + + 彼は請求記号の計算方法を知っており、そのため目的の本を素早く見つけられます。 ## 章の内容 -- [6.1   ハッシュ表](hash_map.md) +- [6.1   ハッシュテーブル](hash_map.md) - [6.2   ハッシュ衝突](hash_collision.md) - [6.3   ハッシュアルゴリズム](hash_algorithm.md) - [6.4   まとめ](summary.md) diff --git a/ja/docs/chapter_hashing/summary.md b/ja/docs/chapter_hashing/summary.md index 0fdcc5049..24b917513 100644 --- a/ja/docs/chapter_hashing/summary.md +++ b/ja/docs/chapter_hashing/summary.md @@ -4,48 +4,52 @@ comments: true # 6.4   まとめ -### 1.   重要なポイント +### 1.   重要ポイントの振り返り -- 入力`key`が与えられると、ハッシュ表は$O(1)$の時間で対応する`value`を取得でき、非常に効率的です。 -- 一般的なハッシュ表の操作には、クエリ、キー値ペアの追加、キー値ペアの削除、ハッシュ表の走査があります。 -- ハッシュ関数は`key`を配列インデックスにマッピングし、対応するバケットにアクセスして`value`を取得できるようにします。 -- 2つの異なるキーがハッシュ化後に同じ配列インデックスになる場合があり、誤ったクエリ結果につながります。この現象はハッシュ衝突として知られています。 -- ハッシュ表の容量が大きいほど、ハッシュ衝突の確率は低くなります。したがって、ハッシュ表のリサイズはハッシュ衝突を緩和できます。配列のリサイズと同様に、ハッシュ表のリサイズはコストが高いです。 -- 要素数をバケット数で割った負荷率は、ハッシュ衝突の深刻度を反映し、しばしばハッシュ表リサイズのトリガー条件として使用されます。 -- 連鎖法は各要素を連結リストに変換し、衝突するすべての要素を同じリストに格納することでハッシュ衝突に対処します。ただし、過度に長いリストはクエリ効率を低下させる可能性があり、リストを赤黒木に変換することで改善できます。 -- オープンアドレス法は複数回のプローブを通してハッシュ衝突を処理します。線形プローブは固定ステップサイズを使用しますが、要素を削除できず、クラスタリングを起こしやすい傾向があります。多重ハッシュはプローブに複数のハッシュ関数を使用し、線形プローブと比較してクラスタリングを減らしますが、計算オーバーヘッドが増加します。 -- 異なるプログラミング言語はさまざまなハッシュ表実装を採用しています。例えば、Javaの`HashMap`は連鎖法を使用し、Pythonの`dict`はオープンアドレス法を採用しています。 -- ハッシュ表では、決定性、高効率、均等分散を持つハッシュアルゴリズムが望まれます。暗号化では、ハッシュアルゴリズムは衝突耐性と雪崩効果も持つべきです。 -- ハッシュアルゴリズムは通常、ハッシュ値の均等分散を保証し、ハッシュ衝突を減らすために、大きな素数を剰余として使用します。 -- 一般的なハッシュアルゴリズムには、MD5、SHA-1、SHA-2、SHA-3があります。MD5はファイル整合性チェックによく使用され、SHA-2は安全なアプリケーションとプロトコルで一般的に使用されます。 -- プログラミング言語は通常、ハッシュ表のバケットインデックスを計算するために、データ型に対して組み込みのハッシュアルゴリズムを提供します。一般的に、不変オブジェクトのみがハッシュ可能です。 +- `key` を入力すると、ハッシュテーブルは $O(1)$ 時間で `value` を検索でき、非常に高効率である。 +- 一般的なハッシュテーブルの操作には、検索、キーと値のペアの追加、キーと値のペアの削除、ハッシュテーブルの走査などがある。 +- ハッシュ関数は `key` を配列インデックスに写像し、それによって対応するバケットにアクセスして `value` を取得する。 +- 異なる 2 つの `key` が、ハッシュ関数を通した後に同じ配列インデックスになることがあり、検索結果の誤りを引き起こす。この現象をハッシュ衝突と呼ぶ。 +- ハッシュテーブルの容量が大きいほど、ハッシュ衝突の確率は低くなる。そのため、ハッシュテーブルを拡張することでハッシュ衝突を緩和できる。配列の拡張と同様に、ハッシュテーブルの拡張操作のコストは大きい。 +- 負荷率は、ハッシュテーブル内の要素数をバケット数で割ったものと定義され、ハッシュ衝突の深刻さを反映する。ハッシュテーブル拡張を発動する条件としてよく用いられる。 +- 連鎖方式では、単一要素を連結リストに変換し、衝突したすべての要素を同じ連結リストに格納する。しかし、連結リストが長すぎると検索効率が低下するため、さらに連結リストを赤黒木に変換して効率を高めることができる。 +- オープンアドレス法は複数回の探索によってハッシュ衝突を処理する。線形探索は固定のステップ幅を用いるが、要素を削除できず、クラスタリングが発生しやすいという欠点がある。二重ハッシュは複数のハッシュ関数を用いて探索するため、線形探索に比べてクラスタリングが起きにくいが、複数のハッシュ関数によって計算量が増える。 +- プログラミング言語ごとに、異なるハッシュテーブル実装が採用されている。たとえば、Java の `HashMap` は連鎖方式を使用し、Python の `Dict` はオープンアドレス法を採用している。 +- ハッシュテーブルでは、ハッシュアルゴリズムに決定性、高効率、均一分布という特徴が求められる。暗号学では、ハッシュアルゴリズムはさらに耐衝突性とアバランシェ効果も備えるべきである。 +- ハッシュアルゴリズムは通常、大きな素数を法として用い、ハッシュ値の均一分布を最大限に保証してハッシュ衝突を減らす。 +- 一般的なハッシュアルゴリズムには MD5、SHA-1、SHA-2、SHA-3 などがある。MD5 はファイル完全性の検証によく用いられ、SHA-2 はセキュリティ用途やプロトコルでよく用いられる。 +- プログラミング言語は通常、データ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュテーブル内のバケットインデックスの計算に用いる。通常、ハッシュ可能なのは不変オブジェクトだけである。 ### 2.   Q & A -**Q**: ハッシュ表の時間計算量が$O(n)$に悪化するのはいつですか? +**Q**:ハッシュテーブルの時間計算量が $O(n)$ になるのはどのような場合ですか? -ハッシュ表の時間計算量は、ハッシュ衝突が深刻な場合に$O(n)$に悪化する可能性があります。ハッシュ関数が適切に設計され、容量が適切に設定され、衝突が均等に分散されている場合、時間計算量は$O(1)$です。プログラミング言語の組み込みハッシュ表を使用する場合、通常は時間計算量を$O(1)$と考えます。 +ハッシュ衝突が深刻な場合、ハッシュテーブルの時間計算量は $O(n)$ に劣化する。ハッシュ関数の設計が適切で、容量設定が合理的で、衝突が比較的均等な場合、時間計算量は $O(1)$ である。プログラミング言語組み込みのハッシュテーブルを使うとき、通常は時間計算量を $O(1)$ とみなす。 -**Q**: なぜハッシュ関数$f(x) = x$を使用しないのですか?これなら衝突を排除できます。 +**Q**:なぜハッシュ関数 $f(x) = x$ を使わないのですか? そうすれば衝突は起きません。 -ハッシュ関数$f(x) = x$では、各要素が一意のバケットインデックスに対応し、これは配列と同等です。しかし、入力空間は通常出力空間(配列長)よりもはるかに大きいため、ハッシュ関数の最後のステップは配列長の剰余を取ることがよくあります。言い換えると、ハッシュ表の目標は、$O(1)$のクエリ効率を提供しながら、より大きな状態空間をより小さなものにマッピングすることです。 +$f(x) = x$ というハッシュ関数では、各要素は一意のバケットインデックスに対応し、これは配列と等価である。しかし、入力空間は通常、出力空間(配列長)よりはるかに大きいため、ハッシュ関数の最後のステップはたいてい配列長での剰余になる。言い換えると、ハッシュテーブルの目的は、大きな状態空間をより小さな空間に写像し、$O(1)$ の検索効率を提供することである。 -**Q**: ハッシュ表がこれらの構造を使って実装されているにもかかわらず、なぜ配列、連結リスト、二分木よりも効率的になれるのですか? +**Q**:ハッシュテーブルの基礎実装は配列、連結リスト、二分木なのに、なぜそれらより高効率になり得るのですか? -まず、ハッシュ表は時間効率が高いですが、空間効率は低いです。ハッシュ表のメモリの大部分は未使用のままです。 +まず、ハッシュテーブルは時間効率が高くなる一方で、空間効率は低くなる。ハッシュテーブルには、かなりの部分で未使用のメモリが存在する。 -次に、ハッシュ表は特定のユースケースでのみ時間効率が高いです。配列や連結リストを使用して同じ時間計算量で機能を実装できる場合、通常はハッシュ表を使用するよりも高速です。これは、ハッシュ関数の計算がオーバーヘッドを発生させ、時間計算量の定数因子が大きくなるためです。 +次に、時間効率が高くなるのは特定の利用場面に限られる。ある機能が同じ時間計算量で配列や連結リストによって実装できるなら、通常はハッシュテーブルより速い。これは、ハッシュ関数の計算にコストがかかり、時間計算量の定数項がより大きいからである。 -最後に、ハッシュ表の時間計算量は悪化する可能性があります。例えば、連鎖法では、連結リストや赤黒木で検索操作を実行し、これは依然として$O(n)$時間に悪化するリスクがあります。 +最後に、ハッシュテーブルの時間計算量は劣化する可能性がある。たとえば連鎖方式では、連結リストや赤黒木で検索操作を行うため、なお $O(n)$ 時間に劣化するリスクがある。 -**Q**: 多重ハッシュにも要素を直接削除できないという欠陥がありますか?削除としてマークされた空間は再利用できますか? +**Q**:二重ハッシュにも要素を直接削除できない欠点がありますか? 削除済みとマークした領域は再利用できますか? -多重ハッシュはオープンアドレス法の一形態であり、すべてのオープンアドレス法には要素を直接削除できないという欠点があります。要素を削除済みとしてマークする必要があります。マークされた空間は再利用できます。ハッシュ表に新しい要素を挿入する際、ハッシュ関数が削除済みとしてマークされた位置を指している場合、その位置は新しい要素によって使用できます。これにより、ハッシュ表のプローブシーケンスを維持しながら、空間の効率的な使用が保証されます。 +二重ハッシュはオープンアドレス法の一種であり、オープンアドレス法はいずれも要素を直接削除できないという欠点があるため、削除のマーク付けが必要になる。削除済みとマークされた領域は再利用できる。新しい要素をハッシュテーブルに挿入し、ハッシュ関数によって削除済みとマークされた位置を見つけた場合、その位置は新しい要素に使用できる。こうすることで、ハッシュテーブルの探索系列を変えずに保ちつつ、空間利用率も確保できる。 -**Q**: なぜ線形プローブの検索プロセス中にハッシュ衝突が発生するのですか? +**Q**:なぜ線形探索では、要素を探すときにハッシュ衝突が発生するのですか? -検索プロセス中、ハッシュ関数は対応するバケットとキー値ペアを指します。`key`が一致しない場合、ハッシュ衝突を示します。したがって、線形プローブは正しいキー値ペアが見つかるか検索が失敗するまで、事前に決められたステップサイズで下方向に検索します。 +探索時には、ハッシュ関数で対応するバケットとキーと値のペアを見つけ、`key` が一致しないことが分かると、それはハッシュ衝突を意味する。そのため、線形探索法では事前に設定したステップ幅に従って順に探索し、正しいキーと値のペアを見つけるか、見つからずに終了するまで続ける。 -**Q**: なぜハッシュ表のリサイズがハッシュ衝突を緩和できるのですか? +**Q**:なぜハッシュテーブルの拡張でハッシュ衝突を緩和できるのですか? -ハッシュ関数の最後のステップは、出力を配列インデックス範囲内に保つために、配列長$n$の剰余を取ることがよくあります。リサイズ時、配列長$n$が変化し、キーに対応するインデックスも変化する可能性があります。以前に同じバケットにマッピングされていたキーが、リサイズ後に複数のバケットに分散される可能性があり、それによってハッシュ衝突が緩和されます。 +ハッシュ関数の最後のステップは、たいてい配列長 $n$ での剰余を取り、出力値を配列インデックスの範囲内に収めることである。拡張後は配列長 $n$ が変化し、`key` に対応するインデックスも変化する可能性がある。もともと同じバケットに入っていた複数の `key` は、拡張後には複数のバケットに割り当てられる可能性があり、それによってハッシュ衝突が緩和される。 + +**Q**:高効率な読み書きのためなら、配列を直接使えばよいのではないですか? + +データの `key` が連続した小範囲の整数であれば、配列を直接使えばよく、単純で高効率である。しかし `key` が別の型(たとえば文字列)の場合は、ハッシュ関数を用いて `key` を配列インデックスに写像し、さらにバケット配列を通じて要素を格納する必要がある。このような構造がハッシュテーブルである。 diff --git a/ja/docs/chapter_heap/build_heap.md b/ja/docs/chapter_heap/build_heap.md index 0ecb2efcf..276c800d3 100644 --- a/ja/docs/chapter_heap/build_heap.md +++ b/ja/docs/chapter_heap/build_heap.md @@ -2,39 +2,39 @@ comments: true --- -# 8.2   ヒープ構築操作 +# 8.2   ヒープ構築 -場合によっては、リストのすべての要素を使用してヒープを構築したいことがあり、このプロセスは「ヒープ構築操作」として知られています。 +場合によっては、リスト内のすべての要素を使ってヒープを構築したいことがあります。この過程を「ヒープ構築」と呼びます。 -## 8.2.1   ヒープ挿入操作による実装 +## 8.2.1   ヒープへの挿入操作による実現 -まず、空のヒープを作成し、次にリストを反復処理して、各要素に対して順番に「ヒープ挿入操作」を実行します。これは、要素をヒープの末尾に追加し、次に下から上に「ヒープ化」することを意味します。 +まず空のヒープを作成し、次にリストを走査して、各要素に対して順に「ヒープへの挿入操作」を実行します。つまり、要素をヒープの末尾に追加してから、その要素に対して「下から上へ」のヒープ化を行います。 -ヒープに要素が追加されるたびに、ヒープの長さは1つずつ増加します。ノードは二分木に上から下に追加されるため、ヒープは「上から下に」構築されます。 +要素が1つヒープに挿入されるたびに、ヒープの長さは1増加します。ノードは上から下へ順に二分木へ追加されるため、ヒープは「上から下へ」構築されます。 -要素数を$n$とすると、各要素の挿入操作は$O(\log{n})$時間かかるため、このヒープ構築方法の時間計算量は$O(n \log n)$です。 +要素数を $n$ とすると、各要素のヒープへの挿入操作には $O(\log{n})$ の時間がかかるため、このヒープ構築法の時間計算量は $O(n \log n)$ です。 -## 8.2.2   走査によるヒープ化の実装 +## 8.2.2   走査によるヒープ化で実現 -実際には、2つのステップでより効率的なヒープ構築方法を実装できます。 +実際には、より効率的なヒープ構築法を実現でき、全体は2つの手順に分かれます。 1. リストのすべての要素をそのままヒープに追加します。この時点では、ヒープの性質はまだ満たされていません。 -2. ヒープを逆順(レベル順走査の逆)で走査し、各非葉ノードに対して「上から下のヒープ化」を実行します。 +2. ヒープを逆順で走査し(レベル順走査の逆順)、各非葉ノードに対して順に「上から下へ」のヒープ化を実行します。 -**ノードをヒープ化した後、そのノードを根とする部分木は有効な部分ヒープになります**。走査が逆順であるため、ヒープは「下から上に」構築されます。 +**あるノードをヒープ化するたびに、そのノードを根とする部分木は合法な部分ヒープになります**。また、逆順で走査するため、ヒープは「下から上へ」構築されます。 -逆走査を選択する理由は、現在のノードの下の部分木がすでに有効な部分ヒープであることを保証し、現在のノードのヒープ化を効果的にするためです。 +逆順走査を選ぶのは、この方法なら現在のノードの下にある部分木がすでに合法な部分ヒープであることを保証でき、そのうえで現在のノードをヒープ化してはじめて有効になるからです。 -言及する価値があるのは、**葉ノードは子を持たないため、自然に有効な部分ヒープを形成し、ヒープ化する必要がない**ということです。以下のコードに示すように、最後の非葉ノードは最後のノードの親です。そこから開始して逆順に走査してヒープ化を実行します: +なお、**葉ノードには子ノードがないため、それ自体が自然に合法な部分ヒープであり、ヒープ化は不要です**。以下のコードが示すように、最後の非葉ノードは最後のノードの親ノードであり、そこから逆順に走査してヒープ化を実行します。 === "Python" ```python title="my_heap.py" def __init__(self, nums: list[int]): - """コンストラクタ、入力リストに基づいてヒープを構築""" - # すべてのリスト要素をヒープに追加 + """コンストラクタ。入力リストに基づいてヒープを構築する""" + # リスト要素をそのままヒープに追加 self.max_heap = nums - # 葉以外のすべてのノードをヒープ化 + # 葉ノード以外のすべてのノードをヒープ化 for i in range(self.parent(self.size() - 1), -1, -1): self.sift_down(i) ``` @@ -42,11 +42,11 @@ comments: true === "C++" ```cpp title="my_heap.cpp" - /* コンストラクタ、入力リストに基づいてヒープを構築 */ + /* コンストラクタ。入力リストに基づいてヒープを構築する */ MaxHeap(vector nums) { - // すべてのリスト要素をヒープに追加 + // リスト要素をそのままヒープに追加 maxHeap = nums; - // 葉以外のすべてのノードをヒープ化 + // 葉ノード以外のすべてのノードをヒープ化 for (int i = parent(size() - 1); i >= 0; i--) { siftDown(i); } @@ -56,11 +56,11 @@ comments: true === "Java" ```java title="my_heap.java" - /* コンストラクタ、入力リストに基づいてヒープを構築 */ + /* コンストラクタ。入力リストに基づいてヒープを構築する */ MaxHeap(List nums) { - // すべてのリスト要素をヒープに追加 + // リスト要素をそのままヒープに追加 maxHeap = new ArrayList<>(nums); - // 葉を除くすべてのノードをヒープ化 + // 葉ノード以外のすべてのノードをヒープ化 for (int i = parent(size() - 1); i >= 0; i--) { siftDown(i); } @@ -70,100 +70,300 @@ comments: true === "C#" ```csharp title="my_heap.cs" - [class]{MaxHeap}-[func]{MaxHeap} + /* コンストラクタ。入力リストに基づいてヒープを構築 */ + MaxHeap(IEnumerable nums) { + // リスト要素をそのままヒープに追加 + maxHeap = new List(nums); + // 葉ノード以外のすべてのノードをヒープ化 + var size = Parent(this.Size() - 1); + for (int i = size; i >= 0; i--) { + SiftDown(i); + } + } ``` === "Go" ```go title="my_heap.go" - [class]{maxHeap}-[func]{newMaxHeap} + /* コンストラクタ。スライスからヒープを構築する */ + func newMaxHeap(nums []any) *maxHeap { + // リスト要素をそのままヒープに追加 + h := &maxHeap{data: nums} + for i := h.parent(len(h.data) - 1); i >= 0; i-- { + // 葉ノード以外のすべてのノードをヒープ化 + h.siftDown(i) + } + return h + } ``` === "Swift" ```swift title="my_heap.swift" - [class]{MaxHeap}-[func]{init} + /* コンストラクタ。入力リストに基づいてヒープを構築する */ + init(nums: [Int]) { + // リスト要素をそのままヒープに追加 + maxHeap = nums + // 葉ノード以外のすべてのノードをヒープ化 + for i in (0 ... parent(i: size() - 1)).reversed() { + siftDown(i: i) + } + } ``` === "JS" ```javascript title="my_heap.js" - [class]{MaxHeap}-[func]{constructor} + /* コンストラクタ。空のヒープを作成するか、入力リストからヒープを構築する */ + constructor(nums) { + // リスト要素をそのままヒープに追加 + this.#maxHeap = nums === undefined ? [] : [...nums]; + // 葉ノード以外のすべてのノードをヒープ化 + for (let i = this.#parent(this.size() - 1); i >= 0; i--) { + this.#siftDown(i); + } + } ``` === "TS" ```typescript title="my_heap.ts" - [class]{MaxHeap}-[func]{constructor} + /* コンストラクタ。空のヒープを作成するか、入力リストからヒープを構築する */ + constructor(nums?: number[]) { + // リスト要素をそのままヒープに追加 + this.maxHeap = nums === undefined ? [] : [...nums]; + // 葉ノード以外のすべてのノードをヒープ化 + for (let i = this.parent(this.size() - 1); i >= 0; i--) { + this.siftDown(i); + } + } ``` === "Dart" ```dart title="my_heap.dart" - [class]{MaxHeap}-[func]{MaxHeap} + /* コンストラクタ。入力リストに基づいてヒープを構築する */ + MaxHeap(List nums) { + // リスト要素をそのままヒープに追加 + _maxHeap = nums; + // 葉ノード以外のすべてのノードをヒープ化 + for (int i = _parent(size() - 1); i >= 0; i--) { + siftDown(i); + } + } ``` === "Rust" ```rust title="my_heap.rs" - [class]{MaxHeap}-[func]{new} + /* コンストラクタ。入力リストに基づいてヒープを構築する */ + fn new(nums: Vec) -> Self { + // リスト要素をそのままヒープに追加 + let mut heap = MaxHeap { max_heap: nums }; + // 葉ノード以外のすべてのノードをヒープ化 + for i in (0..=Self::parent(heap.size() - 1)).rev() { + heap.sift_down(i); + } + heap + } ``` === "C" ```c title="my_heap.c" - [class]{MaxHeap}-[func]{newMaxHeap} + /* コンストラクタ。スライスからヒープを構築する */ + MaxHeap *newMaxHeap(int nums[], int size) { + // すべての要素をヒープに入れる + MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap)); + maxHeap->size = size; + memcpy(maxHeap->data, nums, size * sizeof(int)); + for (int i = parent(maxHeap, size - 1); i >= 0; i--) { + // 葉ノード以外のすべてのノードをヒープ化 + siftDown(maxHeap, i); + } + return maxHeap; + } ``` === "Kotlin" ```kotlin title="my_heap.kt" - [class]{MaxHeap}-[func]{} + /* 最大ヒープ */ + class MaxHeap(nums: MutableList?) { + // 配列ではなくリストを使うことで、拡張を考慮する必要がない + private val maxHeap = mutableListOf() + + /* コンストラクタ。入力リストに基づいてヒープを構築する */ + init { + // リスト要素をそのままヒープに追加 + maxHeap.addAll(nums!!) + // 葉ノード以外のすべてのノードをヒープ化 + for (i in parent(size() - 1) downTo 0) { + siftDown(i) + } + } + + /* 左子ノードのインデックスを取得 */ + private fun left(i: Int): Int { + return 2 * i + 1 + } + + /* 右子ノードのインデックスを取得 */ + private fun right(i: Int): Int { + return 2 * i + 2 + } + + /* 親ノードのインデックスを取得 */ + private fun parent(i: Int): Int { + return (i - 1) / 2 // 切り捨て除算 + } + + /* 要素を交換 */ + private fun swap(i: Int, j: Int) { + val temp = maxHeap[i] + maxHeap[i] = maxHeap[j] + maxHeap[j] = temp + } + + /* ヒープのサイズを取得 */ + fun size(): Int { + return maxHeap.size + } + + /* ヒープが空かどうかを判定 */ + fun isEmpty(): Boolean { + /* ヒープが空かどうかを判定 */ + return size() == 0 + } + + /* ヒープ先頭要素にアクセス */ + fun peek(): Int { + return maxHeap[0] + } + + /* 要素をヒープに追加 */ + fun push(_val: Int) { + // ノードを追加 + maxHeap.add(_val) + // 下から上へヒープ化 + siftUp(size() - 1) + } + + /* ノード i から始めて、下から上へヒープ化 */ + private fun siftUp(it: Int) { + // Kotlin の関数引数は不変のため、一時変数を作成する + var i = it + while (true) { + // ノード i の親ノードを取得 + val p = parent(i) + // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 + if (p < 0 || maxHeap[i] <= maxHeap[p]) break + // 2 つのノードを交換 + swap(i, p) + // ループで下から上へヒープ化 + i = p + } + } + + /* 要素をヒープから取り出す */ + fun pop(): Int { + // 空判定の処理 + if (isEmpty()) throw IndexOutOfBoundsException() + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + swap(0, size() - 1) + // ノードを削除 + val _val = maxHeap.removeAt(size() - 1) + // 上から下へヒープ化 + siftDown(0) + // ヒープ先頭要素を返す + return _val + } + + /* ノード i から始めて、上から下へヒープ化 */ + private fun siftDown(it: Int) { + // Kotlin の関数引数は不変のため、一時変数を作成する + var i = it + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + val l = left(i) + val r = right(i) + var ma = i + if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l + if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma == i) break + // 2 つのノードを交換 + swap(i, ma) + // ループで上から下へヒープ化 + i = ma + } + } + + /* ヒープ(二分木)を出力 */ + fun print() { + val queue = PriorityQueue { a: Int, b: Int -> b - a } + queue.addAll(maxHeap) + printHeap(queue) + } + } ``` === "Ruby" ```ruby title="my_heap.rb" - [class]{MaxHeap}-[func]{initialize} + ### コンストラクタ。入力リストに基づいてヒープを構築 ### + def initialize(nums) + # リスト要素をそのままヒープに追加 + @max_heap = nums + # 葉ノード以外のすべてのノードをヒープ化 + parent(size - 1).downto(0) do |i| + sift_down(i) + end + end ``` -## 8.2.3   計算量分析 +??? pythontutor "コードの可視化" -次に、この第2のヒープ構築方法の時間計算量を計算してみましょう。 +
+ -- 完備二分木のノード数を$n$と仮定すると、葉ノードの数は$(n + 1) / 2$です。ここで$/$ は整数除算です。したがって、ヒープ化が必要なノードの数は$(n - 1) / 2$です。 -- 「上から下のヒープ化」のプロセスでは、各ノードは最大で葉ノードまでヒープ化されるため、最大反復回数は二分木の高さ$\log n$です。 +## 8.2.3   計算量の分析 -この2つを掛け合わせると、ヒープ構築プロセスの時間計算量は$O(n \log n)$となります。**しかし、この推定は正確ではありません。二分木の下位レベルには上位よりもはるかに多くのノードがあるという性質を考慮していないからです。** +以下では、2つ目のヒープ構築法の時間計算量を求めてみましょう。 -より正確な計算を行いましょう。計算を簡素化するため、$n$個のノードと高さ$h$を持つ「完全二分木」を仮定します。この仮定は結果の正確性に影響しません。 +- 完全二分木のノード数を $n$ とすると、葉ノード数は $(n + 1) / 2$ です。ここで $/$ は切り捨て除算を表します。したがって、ヒープ化が必要なノード数は $(n - 1) / 2$ です。 +- 上から下へのヒープ化の過程では、各ノードは最大で葉ノードまでヒープ化されるため、最大反復回数は二分木の高さ $\log n$ です。 -![完全二分木の各レベルのノード数](build_heap.assets/heapify_operations_count.png){ class="animation-figure" } +上の2つを掛け合わせると、ヒープ構築過程の時間計算量は $O(n \log n)$ となります。**しかし、この見積もりは正確ではありません。二分木では下層のノード数が上層よりはるかに多いという性質を考慮していないためです**。 -

図 8-5   完全二分木の各レベルのノード数

+次に、より正確な計算を行います。計算を簡単にするため、ノード数が $n$ 、高さが $h$ の「満二分木」を仮定します。この仮定は計算結果の正しさに影響しません。 -上図に示すように、ノードが「上から下にヒープ化される」最大反復回数は、そのノードから葉ノードまでの距離と等しく、これは正確に「ノードの高さ」です。したがって、各レベルで「ノード数×ノードの高さ」を合計して、**すべてのノードの総ヒープ化反復回数を得る**ことができます。 +![満二分木の各層のノード数](build_heap.assets/heapify_operations_count.png){ class="animation-figure" } + +

図 8-5   満二分木の各層のノード数

+ +上図に示すように、ノードを「上から下へヒープ化」する最大反復回数は、そのノードから葉ノードまでの距離に等しく、この距離こそが「ノードの高さ」です。したがって、各層の「ノード数 $\times$ ノードの高さ」を合計すれば、**すべてのノードのヒープ化反復回数の総和**が得られます。 $$ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1 $$ -上記の方程式を簡素化するために、高校の数列の知識を使用する必要があります。まず$T(h)$に$2$を掛けて以下を得ます: +上式を簡約するには中学の数列の知識を用います。まず $T(h)$ に $2$ を掛けると、次のようになります。 $$ \begin{aligned} T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline -2T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^h\times1 \newline +2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^{h}\times1 \newline \end{aligned} $$ -変位法を使用して$2T(h)$から$T(h)$を減算すると、以下を得ます: +ずらして引く方法を用い、下式の $2 T(h)$ から上式の $T(h)$ を引くと、次が得られます。 $$ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h $$ -方程式を観察すると、$T(h)$は等比数列であり、和の公式を使用して直接計算でき、時間計算量は以下になります: +上式を見ると、$T(h)$ は等比数列であることがわかるため、和の公式を直接用いて、時間計算量は次のように求められます。 $$ \begin{aligned} @@ -173,4 +373,4 @@ T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline \end{aligned} $$ -さらに、高さ$h$の完全二分木は$n = 2^{h+1} - 1$個のノードを持つため、計算量は$O(2^h) = O(n)$です。この計算は、**リストを入力してヒープを構築する時間計算量が$O(n)$であり、非常に効率的である**ことを示しています。 +さらに、高さ $h$ の満二分木のノード数は $n = 2^{h+1} - 1$ であるため、計算量は容易に $O(2^h) = O(n)$ とわかります。以上の導出は、**入力リストからヒープを構築する時間計算量が $O(n)$ であり、非常に効率的である**ことを示しています。 diff --git a/ja/docs/chapter_heap/heap.md b/ja/docs/chapter_heap/heap.md index 78de77039..e00600b70 100644 --- a/ja/docs/chapter_heap/heap.md +++ b/ja/docs/chapter_heap/heap.md @@ -4,71 +4,71 @@ comments: true # 8.1   ヒープ -ヒープは特定の条件を満たす完備二分木で、主に次の2つのタイプに分類されます(下図参照)。 +ヒープ(heap)は、特定の条件を満たす完全二分木であり、主に次の 2 種類に分けられます。 -- 最小ヒープ:任意のノードの値 $\leq$ その子ノードの値。 -- 最大ヒープ:任意のノードの値 $\geq$ その子ノードの値。 +- 最小ヒープ(min heap):任意のノードの値 $\leq$ その子ノードの値。 +- 最大ヒープ(max heap):任意のノードの値 $\geq$ その子ノードの値。 ![最小ヒープと最大ヒープ](heap.assets/min_heap_and_max_heap.png){ class="animation-figure" }

図 8-1   最小ヒープと最大ヒープ

-完備二分木の特別なケースとして、ヒープには以下の特性があります: +ヒープは完全二分木の特殊な例であり、次の性質を持ちます。 -- 最下位層のノードは左から右に埋められ、他の層のノードは完全に埋められています。 -- 二分木の根ノードをヒープの「先頭」と呼び、最も右下のノードをヒープの「末尾」と呼びます。 -- 最大ヒープ(最小ヒープ)の場合、先頭要素(根)の値はすべての要素の中で最大(最小)です。 +- 最下層のノードは左から順に埋められ、ほかの層のノードはすべて埋まっています。 +- 二分木の根ノードを「ヒープ頂点」、最下層で最も右にあるノードを「ヒープ底」と呼びます。 +- 最大ヒープ(最小ヒープ)では、ヒープ頂点の要素(根ノード)の値が最大(最小)です。 -## 8.1.1   ヒープの一般的な操作 +## 8.1.1   ヒープの基本操作 -多くのプログラミング言語が優先度キューを提供していることに注意してください。これは優先度付きソートを持つキューとして定義される抽象データ構造です。 +ここで注意したいのは、多くのプログラミング言語が提供しているのは優先度付きキュー(priority queue)であり、これは優先度順に並ぶキューとして定義される抽象データ構造だということです。 -実際には、**ヒープは優先度キューを実装するためによく使用されます。最大ヒープは、要素が降順でデキューされる優先度キューに対応します**。使用の観点から、「優先度キュー」と「ヒープ」を同等のデータ構造と考えることができます。したがって、この本では両者を特別に区別せず、統一して「ヒープ」と呼びます。 +実際には、**ヒープは通常、優先度付きキューの実装に用いられ、最大ヒープは要素が大きい順に取り出される優先度付きキューに相当します**。利用の観点では、「優先度付きキュー」と「ヒープ」は等価なデータ構造とみなせます。そのため、本書では両者を特に区別せず、まとめて「ヒープ」と呼びます。 -ヒープの一般的な操作を下表に示します。メソッド名はプログラミング言語によって異なる場合があります。 +ヒープの基本操作を以下の表に示します。メソッド名はプログラミング言語によって異なります。 -

表 8-1   ヒープ操作の効率

+

表 8-1   ヒープの操作効率

-| メソッド名 | 説明 | 時間計算量 | -| ----------- | ------------------------------------------------- | ----------- | -| `push()` | ヒープに要素を追加 | $O(\log n)$ | -| `pop()` | ヒープから先頭要素を削除 | $O(\log n)$ | -| `peek()` | 先頭要素にアクセス(最大/最小ヒープの場合、最大/最小値) | $O(1)$ | -| `size()` | ヒープ内の要素数を取得 | $O(1)$ | -| `isEmpty()` | ヒープが空かどうかをチェック | $O(1)$ | +| メソッド名 | 説明 | 時間計算量 | +| ----------- | ------------------------------------------------ | ----------- | +| `push()` | 要素をヒープに追加 | $O(\log n)$ | +| `pop()` | ヒープ頂点の要素を取り出す | $O(\log n)$ | +| `peek()` | ヒープ頂点の要素にアクセス(最大 / 最小ヒープではそれぞれ最大 / 最小値) | $O(1)$ | +| `size()` | ヒープ内の要素数を取得 | $O(1)$ | +| `isEmpty()` | ヒープが空かどうかを判定 | $O(1)$ |
-実際には、プログラミング言語によって提供されるヒープクラス(または優先度キュークラス)を直接使用できます。 +実際の応用では、プログラミング言語が提供するヒープクラス(または優先度付きキュークラス)をそのまま使えます。 -ソートアルゴリズムで「昇順」と「降順」があるように、`flag`を設定するか`Comparator`を変更することで「最小ヒープ」と「最大ヒープ」を切り替えることができます。コードは以下の通りです: +ソートアルゴリズムにおける「昇順」と「降順」と同様に、`flag` を設定したり `Comparator` を変更したりすることで、「最小ヒープ」と「最大ヒープ」を切り替えられます。コードは以下のとおりです: === "Python" ```python title="heap.py" - # 最小ヒープの初期化 + # 最小ヒープを初期化 min_heap, flag = [], 1 - # 最大ヒープの初期化 + # 最大ヒープを初期化 max_heap, flag = [], -1 - # Pythonのheapqモジュールはデフォルトで最小ヒープを実装 - # 要素をヒープにプッシュする前に負の値にすることで、順序を反転させ、最大ヒープを実装 - # この例では、flag = 1は最小ヒープに対応し、flag = -1は最大ヒープに対応 + # Python の heapq モジュールはデフォルトで最小ヒープを実装している + # 「要素を負にして」からヒープに追加すると、大小関係を反転させて最大ヒープを実現できる + # この例では、flag = 1 のときは最小ヒープ、flag = -1 のときは最大ヒープに対応する - # ヒープに要素をプッシュ + # 要素をヒープに追加 heapq.heappush(max_heap, flag * 1) heapq.heappush(max_heap, flag * 3) heapq.heappush(max_heap, flag * 2) heapq.heappush(max_heap, flag * 5) heapq.heappush(max_heap, flag * 4) - # ヒープの先頭要素を取得 + # ヒープ頂点の要素を取得 peek: int = flag * max_heap[0] # 5 - # ヒープの先頭要素をポップ - # ポップされた要素は降順のシーケンスを形成 + # ヒープ頂点の要素を取り出す + # 取り出された要素は大きい順の列になる val = flag * heapq.heappop(max_heap) # 5 val = flag * heapq.heappop(max_heap) # 4 val = flag * heapq.heappop(max_heap) # 3 @@ -78,10 +78,10 @@ comments: true # ヒープのサイズを取得 size: int = len(max_heap) - # ヒープが空かどうかをチェック + # ヒープが空かどうかを判定 is_empty: bool = not max_heap - # リストからヒープを作成 + # 入力リストからヒープを構築 min_heap: list[int] = [1, 3, 2, 5, 4] heapq.heapify(min_heap) ``` @@ -89,24 +89,24 @@ comments: true === "C++" ```cpp title="heap.cpp" - /* ヒープの初期化 */ - // 最小ヒープの初期化 + /* ヒープを初期化 */ + // 最小ヒープを初期化 priority_queue, greater> minHeap; - // 最大ヒープの初期化 + // 最大ヒープを初期化 priority_queue, less> maxHeap; - /* ヒープに要素をプッシュ */ + /* 要素をヒープに追加 */ maxHeap.push(1); maxHeap.push(3); maxHeap.push(2); maxHeap.push(5); maxHeap.push(4); - /* ヒープの先頭要素を取得 */ + /* ヒープ頂点の要素を取得 */ int peek = maxHeap.top(); // 5 - /* ヒープの先頭要素をポップ */ - // ポップされた要素は降順のシーケンスを形成 + /* ヒープ頂点の要素を取り出す */ + // 取り出された要素は大きい順の列になる maxHeap.pop(); // 5 maxHeap.pop(); // 4 maxHeap.pop(); // 3 @@ -116,10 +116,10 @@ comments: true /* ヒープのサイズを取得 */ int size = maxHeap.size(); - /* ヒープが空かどうかをチェック */ + /* ヒープが空かどうかを判定 */ bool isEmpty = maxHeap.empty(); - /* リストからヒープを作成 */ + /* 入力リストからヒープを構築 */ vector input{1, 3, 2, 5, 4}; priority_queue, greater> minHeap(input.begin(), input.end()); ``` @@ -127,24 +127,24 @@ comments: true === "Java" ```java title="heap.java" - /* ヒープの初期化 */ - // 最小ヒープの初期化 + /* ヒープを初期化 */ + // 最小ヒープを初期化 Queue minHeap = new PriorityQueue<>(); - // 最大ヒープの初期化(ラムダ式でComparatorを変更するだけ) + // 最大ヒープを初期化(lambda 式で Comparator を変更すればよい) Queue maxHeap = new PriorityQueue<>((a, b) -> b - a); - /* ヒープに要素をプッシュ */ + /* 要素をヒープに追加 */ maxHeap.offer(1); maxHeap.offer(3); maxHeap.offer(2); maxHeap.offer(5); maxHeap.offer(4); - /* ヒープの先頭要素を取得 */ + /* ヒープ頂点の要素を取得 */ int peek = maxHeap.peek(); // 5 - /* ヒープの先頭要素をポップ */ - // ポップされた要素は降順のシーケンスを形成 + /* ヒープ頂点の要素を取り出す */ + // 取り出された要素は大きい順の列になる peek = maxHeap.poll(); // 5 peek = maxHeap.poll(); // 4 peek = maxHeap.poll(); // 3 @@ -154,34 +154,34 @@ comments: true /* ヒープのサイズを取得 */ int size = maxHeap.size(); - /* ヒープが空かどうかをチェック */ + /* ヒープが空かどうかを判定 */ boolean isEmpty = maxHeap.isEmpty(); - /* リストからヒープを作成 */ + /* 入力リストからヒープを構築 */ minHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4)); ``` === "C#" ```csharp title="heap.cs" - /* ヒープの初期化 */ - // 最小ヒープの初期化 + /* ヒープを初期化 */ + // 最小ヒープを初期化 PriorityQueue minHeap = new(); - // 最大ヒープの初期化(ラムダ式でComparatorを変更するだけ) - PriorityQueue maxHeap = new(Comparer.Create((x, y) => y - x)); + // 最大ヒープを初期化(lambda 式で Comparer を変更すればよい) + PriorityQueue maxHeap = new(Comparer.Create((x, y) => y.CompareTo(x))); - /* ヒープに要素をプッシュ */ + /* 要素をヒープに追加 */ maxHeap.Enqueue(1, 1); maxHeap.Enqueue(3, 3); maxHeap.Enqueue(2, 2); maxHeap.Enqueue(5, 5); maxHeap.Enqueue(4, 4); - /* ヒープの先頭要素を取得 */ + /* ヒープ頂点の要素を取得 */ int peek = maxHeap.Peek();//5 - /* ヒープの先頭要素をポップ */ - // ポップされた要素は降順のシーケンスを形成 + /* ヒープ頂点の要素を取り出す */ + // 取り出された要素は大きい順の列になる peek = maxHeap.Dequeue(); // 5 peek = maxHeap.Dequeue(); // 4 peek = maxHeap.Dequeue(); // 3 @@ -191,76 +191,76 @@ comments: true /* ヒープのサイズを取得 */ int size = maxHeap.Count; - /* ヒープが空かどうかをチェック */ + /* ヒープが空かどうかを判定 */ bool isEmpty = maxHeap.Count == 0; - /* リストからヒープを作成 */ + /* 入力リストからヒープを構築 */ minHeap = new PriorityQueue([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]); ``` === "Go" ```go title="heap.go" - // Goでは、heap.Interfaceを実装することで整数の最大ヒープを構築できます - // heap.Interfaceを実装するには、sort.Interfaceも実装する必要があります + // Go では、heap.Interface を実装することで整数の最大ヒープを構築できる + // heap.Interface を実装するには、同時に sort.Interface も実装する必要がある type intHeap []any - // heap.InterfaceのPushメソッド、要素をヒープにプッシュ + // Push は heap.Interface のメソッドで、要素をヒープに追加する func (h *intHeap) Push(x any) { - // PushとPopの両方でポインタレシーバーを使用 - // スライスの要素を調整するだけでなく、その長さも変更するため + // Push と Pop は pointer receiver を引数に取る + // スライスの内容を調整するだけでなく、スライスの長さも変更するため。 *h = append(*h, x.(int)) } - // heap.InterfaceのPopメソッド、ヒープの先頭要素を削除 + // Pop は heap.Interface のメソッドで、ヒープ頂点の要素を取り出す func (h *intHeap) Pop() any { - // ヒープからポップする要素は末尾に格納 + // 取り出す要素は末尾に格納されている last := (*h)[len(*h)-1] *h = (*h)[:len(*h)-1] return last } - // sort.InterfaceのLenメソッド + // Len は sort.Interface のメソッド func (h *intHeap) Len() int { return len(*h) } - // sort.InterfaceのLessメソッド + // Less は sort.Interface のメソッド func (h *intHeap) Less(i, j int) bool { - // 最小ヒープを実装したい場合は、これを小なり比較に変更 + // 最小ヒープを実装する場合は、不等号を小なりに変更する return (*h)[i].(int) > (*h)[j].(int) } - // sort.InterfaceのSwapメソッド + // Swap は sort.Interface のメソッド func (h *intHeap) Swap(i, j int) { (*h)[i], (*h)[j] = (*h)[j], (*h)[i] } - // Top ヒープの先頭要素を取得 + // Top はヒープ頂点の要素を取得 func (h *intHeap) Top() any { return (*h)[0] } - /* ドライバーコード */ + /* Driver Code */ func TestHeap(t *testing.T) { - /* ヒープの初期化 */ - // 最大ヒープの初期化 + /* ヒープを初期化 */ + // 最大ヒープを初期化 maxHeap := &intHeap{} heap.Init(maxHeap) - /* ヒープに要素をプッシュ */ - // heap.Interfaceのメソッドを呼び出して要素を追加 + /* 要素をヒープに追加 */ + // heap.Interface のメソッドを呼び出して要素を追加する heap.Push(maxHeap, 1) heap.Push(maxHeap, 3) heap.Push(maxHeap, 2) heap.Push(maxHeap, 4) heap.Push(maxHeap, 5) - /* ヒープの先頭要素を取得 */ + /* ヒープ頂点の要素を取得 */ top := maxHeap.Top() - fmt.Printf("ヒープの先頭要素は %d\n", top) + fmt.Printf("ヒープ頂点の要素は %d\n", top) - /* ヒープの先頭要素をポップ */ - // heap.Interfaceのメソッドを呼び出して要素を削除 + /* ヒープ頂点の要素を取り出す */ + // heap.Interface のメソッドを呼び出して要素を削除する heap.Pop(maxHeap) // 5 heap.Pop(maxHeap) // 4 heap.Pop(maxHeap) // 3 @@ -271,30 +271,30 @@ comments: true size := len(*maxHeap) fmt.Printf("ヒープ内の要素数は %d\n", size) - /* ヒープが空かどうかをチェック */ + /* ヒープが空かどうかを判定 */ isEmpty := len(*maxHeap) == 0 - fmt.Printf("ヒープは空ですか? %t\n", isEmpty) + fmt.Printf("ヒープは空か %t\n", isEmpty) } ``` === "Swift" ```swift title="heap.swift" - /* ヒープの初期化 */ - // SwiftのHeap型は最大ヒープと最小ヒープの両方をサポートし、swift-collectionsライブラリが必要 + /* ヒープを初期化 */ + // Swift の Heap 型は最大ヒープと最小ヒープの両方をサポートしており、swift-collections の導入が必要 var heap = Heap() - /* ヒープに要素をプッシュ */ + /* 要素をヒープに追加 */ heap.insert(1) heap.insert(3) heap.insert(2) heap.insert(5) heap.insert(4) - /* ヒープの先頭要素を取得 */ + /* ヒープ頂点の要素を取得 */ var peek = heap.max()! - /* ヒープの先頭要素をポップ */ + /* ヒープ頂点の要素を取り出す */ peek = heap.removeMax() // 5 peek = heap.removeMax() // 4 peek = heap.removeMax() // 3 @@ -304,29 +304,29 @@ comments: true /* ヒープのサイズを取得 */ let size = heap.count - /* ヒープが空かどうかをチェック */ + /* ヒープが空かどうかを判定 */ let isEmpty = heap.isEmpty - /* リストからヒープを作成 */ + /* 入力リストからヒープを構築 */ let heap2 = Heap([1, 3, 2, 5, 4]) ``` === "JS" ```javascript title="heap.js" - // JavaScriptは組み込みのHeapクラスを提供していません + // JavaScript には組み込みの Heap クラスがない ``` === "TS" ```typescript title="heap.ts" - // TypeScriptは組み込みのHeapクラスを提供していません + // TypeScript には組み込みの Heap クラスがない ``` === "Dart" ```dart title="heap.dart" - // Dartは組み込みのHeapクラスを提供していません + // Dart には組み込みの Heap クラスがない ``` === "Rust" @@ -335,24 +335,24 @@ comments: true use std::collections::BinaryHeap; use std::cmp::Reverse; - /* ヒープの初期化 */ - // 最小ヒープの初期化 + /* ヒープを初期化 */ + // 最小ヒープを初期化 let mut min_heap = BinaryHeap::>::new(); - // 最大ヒープの初期化 + // 最大ヒープを初期化 let mut max_heap = BinaryHeap::new(); - /* ヒープに要素をプッシュ */ + /* 要素をヒープに追加 */ max_heap.push(1); max_heap.push(3); max_heap.push(2); max_heap.push(5); max_heap.push(4); - /* ヒープの先頭要素を取得 */ + /* ヒープ頂点の要素を取得 */ let peek = max_heap.peek().unwrap(); // 5 - /* ヒープの先頭要素をポップ */ - // ポップされた要素は降順のシーケンスを形成 + /* ヒープ頂点の要素を取り出す */ + // 取り出された要素は大きい順の列になる let peek = max_heap.pop().unwrap(); // 5 let peek = max_heap.pop().unwrap(); // 4 let peek = max_heap.pop().unwrap(); // 3 @@ -362,40 +362,40 @@ comments: true /* ヒープのサイズを取得 */ let size = max_heap.len(); - /* ヒープが空かどうかをチェック */ + /* ヒープが空かどうかを判定 */ let is_empty = max_heap.is_empty(); - /* リストからヒープを作成 */ + /* 入力リストからヒープを構築 */ let min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]); ``` === "C" ```c title="heap.c" - // Cは組み込みのHeapクラスを提供していません + // C には組み込みの Heap クラスがない ``` === "Kotlin" ```kotlin title="heap.kt" - /* ヒープの初期化 */ - // 最小ヒープの初期化 + /* ヒープを初期化 */ + // 最小ヒープを初期化 var minHeap = PriorityQueue() - // 最大ヒープの初期化(ラムダ式でComparatorを変更するだけ) + // 最大ヒープを初期化(lambda 式で Comparator を変更すればよい) val maxHeap = PriorityQueue { a: Int, b: Int -> b - a } - /* ヒープに要素をプッシュ */ + /* 要素をヒープに追加 */ maxHeap.offer(1) maxHeap.offer(3) maxHeap.offer(2) maxHeap.offer(5) maxHeap.offer(4) - /* ヒープの先頭要素を取得 */ + /* ヒープ頂点の要素を取得 */ var peek = maxHeap.peek() // 5 - /* ヒープの先頭要素をポップ */ - // ポップされた要素は降順のシーケンスを形成 + /* ヒープ頂点の要素を取り出す */ + // 取り出された要素は大きい順の列になる peek = maxHeap.poll() // 5 peek = maxHeap.poll() // 4 peek = maxHeap.poll() // 3 @@ -405,207 +405,302 @@ comments: true /* ヒープのサイズを取得 */ val size = maxHeap.size - /* ヒープが空かどうかをチェック */ + /* ヒープが空かどうかを判定 */ val isEmpty = maxHeap.isEmpty() - /* リストからヒープを作成 */ + /* 入力リストからヒープを構築 */ minHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4)) ``` === "Ruby" ```ruby title="heap.rb" - + # Ruby には組み込みの Heap クラスがない ``` +??? pythontutor "実行を可視化" + + https://pythontutor.com/render.html#code=import%20heapq%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20min_heap,%20flag%20%3D%20%5B%5D,%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap,%20flag%20%3D%20%5B%5D,%20-1%0A%20%20%20%20%0A%20%20%20%20%23%20Python%20%E7%9A%84%20heapq%20%E6%A8%A1%E5%9D%97%E9%BB%98%E8%AE%A4%E5%AE%9E%E7%8E%B0%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E8%80%83%E8%99%91%E5%B0%86%E2%80%9C%E5%85%83%E7%B4%A0%E5%8F%96%E8%B4%9F%E2%80%9D%E5%90%8E%E5%86%8D%E5%85%A5%E5%A0%86%EF%BC%8C%E8%BF%99%E6%A0%B7%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%B0%86%E5%A4%A7%E5%B0%8F%E5%85%B3%E7%B3%BB%E9%A2%A0%E5%80%92%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%AE%9E%E7%8E%B0%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E5%9C%A8%E6%9C%AC%E7%A4%BA%E4%BE%8B%E4%B8%AD%EF%BC%8Cflag%20%3D%201%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%B0%8F%E9%A1%B6%E5%A0%86%EF%BC%8Cflag%20%3D%20-1%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%201%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%203%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%202%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%205%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%204%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20flag%20*%20max_heap%5B0%5D%20%23%205%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%0A%20%20%20%20%23%20%E5%87%BA%E5%A0%86%E5%85%83%E7%B4%A0%E4%BC%9A%E5%BD%A2%E6%88%90%E4%B8%80%E4%B8%AA%E4%BB%8E%E5%A4%A7%E5%88%B0%E5%B0%8F%E7%9A%84%E5%BA%8F%E5%88%97%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%205%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%204%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%203%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%202%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%201%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%0A%20%20%20%20size%20%3D%20len%28max_heap%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%A0%86%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20not%20max_heap%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%B9%B6%E5%BB%BA%E5%A0%86%0A%20%20%20%20min_heap%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20heapq.heapify%28min_heap%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + ## 8.1.2   ヒープの実装 -以下の実装は最大ヒープです。最小ヒープに変換するには、すべてのサイズ論理比較を反転させるだけです(例えば、$\geq$を$\leq$に置き換える)。興味のある読者は自分で実装することをお勧めします。 +以下では最大ヒープを実装します。最小ヒープに変換したい場合は、すべての大小比較ロジックを反転させるだけです(たとえば、$\geq$ を $\leq$ に置き換えます)。興味のある読者は自分で実装してみてください。 ### 1.   ヒープの格納と表現 -「二分木」の節で述べたように、完備二分木は配列表現に非常に適しています。ヒープは完備二分木の一種なので、**配列を使用してヒープを格納します**。 +「二分木」の章で述べたように、完全二分木は配列で表現するのに非常に適しています。ヒープはまさに完全二分木の一種なので、**ここでは配列を使ってヒープを格納します**。 -配列を使用して二分木を表現する場合、要素はノード値を表し、インデックスは二分木内のノード位置を表します。**ノードポインタはインデックスマッピング公式を通じて実装されます**。 +配列で二分木を表す場合、要素はノードの値を表し、インデックスは二分木におけるノードの位置を表します。**ノード間の参照関係はインデックスの対応式によって実現できます**。 -下図に示すように、インデックス$i$が与えられた場合、その左の子のインデックスは$2i + 1$、右の子のインデックスは$2i + 2$、親のインデックスは$(i - 1) / 2$(床除算)です。インデックスが範囲外の場合、nullノードまたはノードが存在しないことを意味します。 +次の図に示すように、インデックス $i$ に対して、左子ノードのインデックスは $2i + 1$ 、右子ノードのインデックスは $2i + 2$ 、親ノードのインデックスは $(i - 1) / 2$(切り捨て除算)です。インデックスが範囲外であれば、空ノードまたはノードが存在しないことを表します。 ![ヒープの表現と格納](heap.assets/representation_of_heap.png){ class="animation-figure" }

図 8-2   ヒープの表現と格納

-後で便利に使用するため、インデックスマッピング公式を関数にカプセル化できます: +インデックスの対応式は関数にまとめておくと、後続で使いやすくなります: === "Python" ```python title="my_heap.py" def left(self, i: int) -> int: - """左の子ノードのインデックスを取得""" + """左子ノードのインデックスを取得""" return 2 * i + 1 def right(self, i: int) -> int: - """右の子ノードのインデックスを取得""" + """右子ノードのインデックスを取得""" return 2 * i + 2 def parent(self, i: int) -> int: """親ノードのインデックスを取得""" - return (i - 1) // 2 # 整数除算で切り下げ + return (i - 1) // 2 # 切り捨て除算 ``` === "C++" ```cpp title="my_heap.cpp" - /* 左の子ノードのインデックスを取得 */ + /* 左子ノードのインデックスを取得 */ int left(int i) { return 2 * i + 1; } - /* 右の子ノードのインデックスを取得 */ + /* 右子ノードのインデックスを取得 */ int right(int i) { return 2 * i + 2; } /* 親ノードのインデックスを取得 */ int parent(int i) { - return (i - 1) / 2; // 整数除算で切り下げ + return (i - 1) / 2; // 切り捨て除算 } ``` === "Java" ```java title="my_heap.java" - /* 左の子ノードのインデックスを取得 */ + /* 左子ノードのインデックスを取得 */ int left(int i) { return 2 * i + 1; } - /* 右の子ノードのインデックスを取得 */ + /* 右子ノードのインデックスを取得 */ int right(int i) { return 2 * i + 2; } /* 親ノードのインデックスを取得 */ int parent(int i) { - return (i - 1) / 2; // 整数除算で切り下げ + return (i - 1) / 2; // 切り捨て除算 } ``` === "C#" ```csharp title="my_heap.cs" - [class]{MaxHeap}-[func]{Left} + /* 左子ノードのインデックスを取得 */ + int Left(int i) { + return 2 * i + 1; + } - [class]{MaxHeap}-[func]{Right} + /* 右子ノードのインデックスを取得 */ + int Right(int i) { + return 2 * i + 2; + } - [class]{MaxHeap}-[func]{Parent} + /* 親ノードのインデックスを取得 */ + int Parent(int i) { + return (i - 1) / 2; // 切り捨て除算 + } ``` === "Go" ```go title="my_heap.go" - [class]{maxHeap}-[func]{left} + /* 左子ノードのインデックスを取得 */ + func (h *maxHeap) left(i int) int { + return 2*i + 1 + } - [class]{maxHeap}-[func]{right} + /* 右子ノードのインデックスを取得 */ + func (h *maxHeap) right(i int) int { + return 2*i + 2 + } - [class]{maxHeap}-[func]{parent} + /* 親ノードのインデックスを取得 */ + func (h *maxHeap) parent(i int) int { + // 切り捨て除算 + return (i - 1) / 2 + } ``` === "Swift" ```swift title="my_heap.swift" - [class]{MaxHeap}-[func]{left} + /* 左子ノードのインデックスを取得 */ + func left(i: Int) -> Int { + 2 * i + 1 + } - [class]{MaxHeap}-[func]{right} + /* 右子ノードのインデックスを取得 */ + func right(i: Int) -> Int { + 2 * i + 2 + } - [class]{MaxHeap}-[func]{parent} + /* 親ノードのインデックスを取得 */ + func parent(i: Int) -> Int { + (i - 1) / 2 // 切り捨て除算 + } ``` === "JS" ```javascript title="my_heap.js" - [class]{MaxHeap}-[func]{left} + /* 左子ノードのインデックスを取得 */ + #left(i) { + return 2 * i + 1; + } - [class]{MaxHeap}-[func]{right} + /* 右子ノードのインデックスを取得 */ + #right(i) { + return 2 * i + 2; + } - [class]{MaxHeap}-[func]{parent} + /* 親ノードのインデックスを取得 */ + #parent(i) { + return Math.floor((i - 1) / 2); // 切り捨て除算 + } ``` === "TS" ```typescript title="my_heap.ts" - [class]{MaxHeap}-[func]{left} + /* 左子ノードのインデックスを取得 */ + left(i: number): number { + return 2 * i + 1; + } - [class]{MaxHeap}-[func]{right} + /* 右子ノードのインデックスを取得 */ + right(i: number): number { + return 2 * i + 2; + } - [class]{MaxHeap}-[func]{parent} + /* 親ノードのインデックスを取得 */ + parent(i: number): number { + return Math.floor((i - 1) / 2); // 切り捨て除算 + } ``` === "Dart" ```dart title="my_heap.dart" - [class]{MaxHeap}-[func]{_left} + /* 左子ノードのインデックスを取得 */ + int _left(int i) { + return 2 * i + 1; + } - [class]{MaxHeap}-[func]{_right} + /* 右子ノードのインデックスを取得 */ + int _right(int i) { + return 2 * i + 2; + } - [class]{MaxHeap}-[func]{_parent} + /* 親ノードのインデックスを取得 */ + int _parent(int i) { + return (i - 1) ~/ 2; // 切り捨て除算 + } ``` === "Rust" ```rust title="my_heap.rs" - [class]{MaxHeap}-[func]{left} + /* 左子ノードのインデックスを取得 */ + fn left(i: usize) -> usize { + 2 * i + 1 + } - [class]{MaxHeap}-[func]{right} + /* 右子ノードのインデックスを取得 */ + fn right(i: usize) -> usize { + 2 * i + 2 + } - [class]{MaxHeap}-[func]{parent} + /* 親ノードのインデックスを取得 */ + fn parent(i: usize) -> usize { + (i - 1) / 2 // 切り捨て除算 + } ``` === "C" ```c title="my_heap.c" - [class]{MaxHeap}-[func]{left} + /* 左子ノードのインデックスを取得 */ + int left(MaxHeap *maxHeap, int i) { + return 2 * i + 1; + } - [class]{MaxHeap}-[func]{right} + /* 右子ノードのインデックスを取得 */ + int right(MaxHeap *maxHeap, int i) { + return 2 * i + 2; + } - [class]{MaxHeap}-[func]{parent} + /* 親ノードのインデックスを取得 */ + int parent(MaxHeap *maxHeap, int i) { + return (i - 1) / 2; // 切り捨て + } ``` === "Kotlin" ```kotlin title="my_heap.kt" - [class]{MaxHeap}-[func]{left} + /* 左子ノードのインデックスを取得 */ + fun left(i: Int): Int { + return 2 * i + 1 + } - [class]{MaxHeap}-[func]{right} + /* 右子ノードのインデックスを取得 */ + fun right(i: Int): Int { + return 2 * i + 2 + } - [class]{MaxHeap}-[func]{parent} + /* 親ノードのインデックスを取得 */ + fun parent(i: Int): Int { + return (i - 1) / 2 // 切り捨て除算 + } ``` === "Ruby" ```ruby title="my_heap.rb" - [class]{MaxHeap}-[func]{left} + ### 左子ノードのインデックスを取得 ### + def left(i) + 2 * i + 1 + end - [class]{MaxHeap}-[func]{right} + ### 右子ノードのインデックスを取得 ### + def right(i) + 2 * i + 2 + end - [class]{MaxHeap}-[func]{parent} + ### 親ノードのインデックスを取得 ### + def parent(i) + (i - 1) / 2 # 切り捨て除算 + end ``` -### 2.   ヒープの先頭要素へのアクセス +### 2.   ヒープ頂点の要素にアクセス -ヒープの先頭要素は二分木の根ノードで、リストの最初の要素でもあります: +ヒープ頂点の要素は二分木の根ノード、すなわちリストの先頭要素です: === "Python" ```python title="my_heap.py" def peek(self) -> int: - """ヒープの先頭要素にアクセス""" + """ヒープ先頭要素にアクセス""" return self.max_heap[0] ``` === "C++" ```cpp title="my_heap.cpp" - /* ヒープの先頭要素にアクセス */ + /* ヒープ先頭要素にアクセス */ int peek() { return maxHeap[0]; } @@ -614,7 +709,7 @@ comments: true === "Java" ```java title="my_heap.java" - /* ヒープの先頭要素にアクセス */ + /* ヒープ先頭要素にアクセス */ int peek() { return maxHeap.get(0); } @@ -623,71 +718,106 @@ comments: true === "C#" ```csharp title="my_heap.cs" - [class]{MaxHeap}-[func]{Peek} + /* ヒープ先頭要素にアクセス */ + int Peek() { + return maxHeap[0]; + } ``` === "Go" ```go title="my_heap.go" - [class]{maxHeap}-[func]{peek} + /* ヒープ先頭要素にアクセス */ + func (h *maxHeap) peek() any { + return h.data[0] + } ``` === "Swift" ```swift title="my_heap.swift" - [class]{MaxHeap}-[func]{peek} + /* ヒープ先頭要素にアクセス */ + func peek() -> Int { + maxHeap[0] + } ``` === "JS" ```javascript title="my_heap.js" - [class]{MaxHeap}-[func]{peek} + /* ヒープ先頭要素にアクセス */ + peek() { + return this.#maxHeap[0]; + } ``` === "TS" ```typescript title="my_heap.ts" - [class]{MaxHeap}-[func]{peek} + /* ヒープ先頭要素にアクセス */ + peek(): number { + return this.maxHeap[0]; + } ``` === "Dart" ```dart title="my_heap.dart" - [class]{MaxHeap}-[func]{peek} + /* ヒープ先頭要素にアクセス */ + int peek() { + return _maxHeap[0]; + } ``` === "Rust" ```rust title="my_heap.rs" - [class]{MaxHeap}-[func]{peek} + /* ヒープ先頭要素にアクセス */ + fn peek(&self) -> Option { + self.max_heap.first().copied() + } ``` === "C" ```c title="my_heap.c" - [class]{MaxHeap}-[func]{peek} + /* ヒープ先頭要素にアクセス */ + int peek(MaxHeap *maxHeap) { + return maxHeap->data[0]; + } ``` === "Kotlin" ```kotlin title="my_heap.kt" - [class]{MaxHeap}-[func]{peek} + /* ヒープ先頭要素にアクセス */ + fun peek(): Int { + return maxHeap[0] + } ``` === "Ruby" ```ruby title="my_heap.rb" - [class]{MaxHeap}-[func]{peek} + ### ヒープ先頭要素を参照 ### + def peek + @max_heap[0] + end ``` -### 3.   ヒープへの要素挿入 +??? pythontutor "コードの可視化" -要素`val`が与えられた場合、まずそれをヒープの底に追加します。追加後、`val`がヒープ内の他の要素より大きい可能性があるため、ヒープの完全性が損なわれる可能性があります。**したがって、挿入されたノードから根ノードまでのパスを修復する必要があります**。この操作はヒープ化と呼ばれます。 +
+ -挿入されたノードから開始して、**下から上にヒープ化を実行します**。下図に示すように、挿入されたノードの値をその親ノードと比較し、挿入されたノードが大きい場合はそれらを交換します。次にこの操作を続行し、根に到達するか、交換が不要なノードに遭遇するまで、下から上にヒープ内の各ノードを修復します。 +### 3.   要素をヒープに追加 + +与えられた要素 `val` を、まずヒープの底に追加します。追加後、`val` がヒープ内のほかの要素より大きい可能性があるため、ヒープ条件が崩れているかもしれません。**そのため、挿入ノードから根ノードまでの経路上にある各ノードを修復する必要があります**。この操作をヒープ化(heapify)と呼びます。 + +ヒープへ追加したノードから始めて、**下から上へヒープ化**を行います。次の図のように、挿入ノードとその親ノードの値を比較し、挿入ノードのほうが大きければそれらを交換します。その後もこの操作を繰り返し、下から上へ各ノードを修復して、根ノードを越えるか交換不要のノードに達した時点で終了します。 === "<1>" - ![ヒープへの要素挿入の手順](heap.assets/heap_push_step1.png){ class="animation-figure" } + ![要素をヒープに追加する手順](heap.assets/heap_push_step1.png){ class="animation-figure" } === "<2>" ![heap_push_step2](heap.assets/heap_push_step2.png){ class="animation-figure" } @@ -713,38 +843,38 @@ comments: true === "<9>" ![heap_push_step9](heap.assets/heap_push_step9.png){ class="animation-figure" } -

図 8-3   ヒープへの要素挿入の手順

+

図 8-3   要素をヒープに追加する手順

-総ノード数を$n$とすると、木の高さは$O(\log n)$です。したがって、ヒープ化操作のループ反復回数は最大$O(\log n)$で、**要素挿入操作の時間計算量は$O(\log n)$になります**。コードは以下の通りです: +ノード総数を $n$ とすると、木の高さは $O(\log n)$ です。したがって、ヒープ化操作のループ回数は高々 $O(\log n)$ であり、**要素をヒープに追加する操作の時間計算量は $O(\log n)$** です。コードは以下のとおりです: === "Python" ```python title="my_heap.py" def push(self, val: int): - """ヒープに要素をプッシュ""" + """要素をヒープに追加""" # ノードを追加 self.max_heap.append(val) # 下から上へヒープ化 self.sift_up(self.size() - 1) def sift_up(self, i: int): - """ノードiから開始して、下から上へヒープ化""" + """ノード i から始めて、下から上へヒープ化""" while True: - # ノードiの親ノードを取得 + # ノード i の親ノードを取得 p = self.parent(i) - # 「ルートノードを越える」または「ノードが修復不要」の場合、ヒープ化を終了 + # 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 if p < 0 or self.max_heap[i] <= self.max_heap[p]: break - # 2つのノードを交換 + # 2 つのノードを交換 self.swap(i, p) - # 上向きのループヒープ化 + # ループで下から上へヒープ化 i = p ``` === "C++" ```cpp title="my_heap.cpp" - /* ヒープに要素をプッシュ */ + /* 要素をヒープに追加 */ void push(int val) { // ノードを追加 maxHeap.push_back(val); @@ -752,17 +882,17 @@ comments: true siftUp(size() - 1); } - /* ノードiから上向きにヒープ化を開始 */ + /* ノード i から始めて、下から上へヒープ化 */ void siftUp(int i) { while (true) { - // ノードiの親ノードを取得 + // ノード i の親ノードを取得 int p = parent(i); - // 「ルートノードを超える」または「ノードが修復不要」の場合、ヒープ化を終了 + // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 if (p < 0 || maxHeap[i] <= maxHeap[p]) break; - // 2つのノードを交換 + // 2 つのノードを交換 swap(maxHeap[i], maxHeap[p]); - // 上向きにループしてヒープ化 + // ループで下から上へヒープ化 i = p; } } @@ -771,7 +901,7 @@ comments: true === "Java" ```java title="my_heap.java" - /* 要素をヒープにプッシュ */ + /* 要素をヒープに追加 */ void push(int val) { // ノードを追加 maxHeap.add(val); @@ -779,17 +909,17 @@ comments: true siftUp(size() - 1); } - /* ノード i から上向きにヒープ化を開始 */ + /* ノード i から始めて、下から上へヒープ化 */ void siftUp(int i) { while (true) { // ノード i の親ノードを取得 int p = parent(i); - // 「根ノードを越える」または「ノードが修復不要」の場合、ヒープ化を終了 + // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 if (p < 0 || maxHeap.get(i) <= maxHeap.get(p)) break; - // 2つのノードを交換 + // 2 つのノードを交換 swap(i, p); - // 上向きにヒープ化をループ + // ループで下から上へヒープ化 i = p; } } @@ -798,95 +928,305 @@ comments: true === "C#" ```csharp title="my_heap.cs" - [class]{MaxHeap}-[func]{Push} + /* 要素をヒープに追加 */ + void Push(int val) { + // ノードを追加 + maxHeap.Add(val); + // 下から上へヒープ化 + SiftUp(Size() - 1); + } - [class]{MaxHeap}-[func]{SiftUp} + /* ノード i から始めて、下から上へヒープ化 */ + void SiftUp(int i) { + while (true) { + // ノード i の親ノードを取得 + int p = Parent(i); + // 「根ノードを越えた」または「ノードの修復が不要」な場合は、ヒープ化を終了する + if (p < 0 || maxHeap[i] <= maxHeap[p]) + break; + // 2 つのノードを交換 + Swap(i, p); + // ループで下から上へヒープ化 + i = p; + } + } ``` === "Go" ```go title="my_heap.go" - [class]{maxHeap}-[func]{push} + /* 要素をヒープに追加 */ + func (h *maxHeap) push(val any) { + // ノードを追加 + h.data = append(h.data, val) + // 下から上へヒープ化 + h.siftUp(len(h.data) - 1) + } - [class]{maxHeap}-[func]{siftUp} + /* ノード i から始めて、下から上へヒープ化 */ + func (h *maxHeap) siftUp(i int) { + for true { + // ノード i の親ノードを取得 + p := h.parent(i) + // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 + if p < 0 || h.data[i].(int) <= h.data[p].(int) { + break + } + // 2 つのノードを交換 + h.swap(i, p) + // ループで下から上へヒープ化 + i = p + } + } ``` === "Swift" ```swift title="my_heap.swift" - [class]{MaxHeap}-[func]{push} + /* 要素をヒープに追加 */ + func push(val: Int) { + // ノードを追加 + maxHeap.append(val) + // 下から上へヒープ化 + siftUp(i: size() - 1) + } - [class]{MaxHeap}-[func]{siftUp} + /* ノード i から始めて、下から上へヒープ化 */ + func siftUp(i: Int) { + var i = i + while true { + // ノード i の親ノードを取得 + let p = parent(i: i) + // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 + if p < 0 || maxHeap[i] <= maxHeap[p] { + break + } + // 2 つのノードを交換 + swap(i: i, j: p) + // ループで下から上へヒープ化 + i = p + } + } ``` === "JS" ```javascript title="my_heap.js" - [class]{MaxHeap}-[func]{push} + /* 要素をヒープに追加 */ + push(val) { + // ノードを追加 + this.#maxHeap.push(val); + // 下から上へヒープ化 + this.#siftUp(this.size() - 1); + } - [class]{MaxHeap}-[func]{siftUp} + /* ノード i から始めて、下から上へヒープ化 */ + #siftUp(i) { + while (true) { + // ノード i の親ノードを取得 + const p = this.#parent(i); + // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 + if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break; + // 2 つのノードを交換 + this.#swap(i, p); + // ループで下から上へヒープ化 + i = p; + } + } ``` === "TS" ```typescript title="my_heap.ts" - [class]{MaxHeap}-[func]{push} + /* 要素をヒープに追加 */ + push(val: number): void { + // ノードを追加 + this.maxHeap.push(val); + // 下から上へヒープ化 + this.siftUp(this.size() - 1); + } - [class]{MaxHeap}-[func]{siftUp} + /* ノード i から始めて、下から上へヒープ化 */ + siftUp(i: number): void { + while (true) { + // ノード i の親ノードを取得 + const p = this.parent(i); + // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 + if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break; + // 2 つのノードを交換 + this.swap(i, p); + // ループで下から上へヒープ化 + i = p; + } + } ``` === "Dart" ```dart title="my_heap.dart" - [class]{MaxHeap}-[func]{push} + /* 要素をヒープに追加 */ + void push(int val) { + // ノードを追加 + _maxHeap.add(val); + // 下から上へヒープ化 + siftUp(size() - 1); + } - [class]{MaxHeap}-[func]{siftUp} + /* ノード i から始めて、下から上へヒープ化 */ + void siftUp(int i) { + while (true) { + // ノード i の親ノードを取得 + int p = _parent(i); + // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 + if (p < 0 || _maxHeap[i] <= _maxHeap[p]) { + break; + } + // 2 つのノードを交換 + _swap(i, p); + // ループで下から上へヒープ化 + i = p; + } + } ``` === "Rust" ```rust title="my_heap.rs" - [class]{MaxHeap}-[func]{push} + /* 要素をヒープに追加 */ + fn push(&mut self, val: i32) { + // ノードを追加 + self.max_heap.push(val); + // 下から上へヒープ化 + self.sift_up(self.size() - 1); + } - [class]{MaxHeap}-[func]{sift_up} + /* ノード i から始めて、下から上へヒープ化 */ + fn sift_up(&mut self, mut i: usize) { + loop { + // ノード i はすでにヒープの先頭ノードなので、ヒープ化を終了する + if i == 0 { + break; + } + // ノード i の親ノードを取得 + let p = Self::parent(i); + // 「ノードの修復が不要」になったら、ヒープ化を終了 + if self.max_heap[i] <= self.max_heap[p] { + break; + } + // 2 つのノードを交換 + self.swap(i, p); + // ループで下から上へヒープ化 + i = p; + } + } ``` === "C" ```c title="my_heap.c" - [class]{MaxHeap}-[func]{push} + /* 要素をヒープに追加 */ + void push(MaxHeap *maxHeap, int val) { + // 通常は、これほど多くのノードを追加すべきではない + if (maxHeap->size == MAX_SIZE) { + printf("heap is full!"); + return; + } + // ノードを追加 + maxHeap->data[maxHeap->size] = val; + maxHeap->size++; - [class]{MaxHeap}-[func]{siftUp} + // 下から上へヒープ化 + siftUp(maxHeap, maxHeap->size - 1); + } + + /* ノード i から始めて、下から上へヒープ化 */ + void siftUp(MaxHeap *maxHeap, int i) { + while (true) { + // ノード i の親ノードを取得 + int p = parent(maxHeap, i); + // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 + if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) { + break; + } + // 2 つのノードを交換 + swap(maxHeap, i, p); + // ループで下から上へヒープ化 + i = p; + } + } ``` === "Kotlin" ```kotlin title="my_heap.kt" - [class]{MaxHeap}-[func]{push} + /* 要素をヒープに追加 */ + fun push(_val: Int) { + // ノードを追加 + maxHeap.add(_val) + // 下から上へヒープ化 + siftUp(size() - 1) + } - [class]{MaxHeap}-[func]{siftUp} + /* ノード i から始めて、下から上へヒープ化 */ + fun siftUp(it: Int) { + // Kotlin の関数引数は不変のため、一時変数を作成する + var i = it + while (true) { + // ノード i の親ノードを取得 + val p = parent(i) + // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 + if (p < 0 || maxHeap[i] <= maxHeap[p]) break + // 2 つのノードを交換 + swap(i, p) + // ループで下から上へヒープ化 + i = p + } + } ``` === "Ruby" ```ruby title="my_heap.rb" - [class]{MaxHeap}-[func]{push} + ### 要素をヒープに挿入 ### + def push(val) + # ノードを追加 + @max_heap << val + # 下から上へヒープ化 + sift_up(size - 1) + end - [class]{MaxHeap}-[func]{sift_up} + ### ノード i から下から上へヒープ化 ### + def sift_up(i) + loop do + # ノード i の親ノードを取得 + p = parent(i) + # 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了 + break if p < 0 || @max_heap[i] <= @max_heap[p] + # 2 つのノードを交換 + swap(i, p) + # ループで下から上へヒープ化 + i = p + end + end ``` -### 4.   ヒープからの先頭要素削除 +??? pythontutor "コードの可視化" -ヒープの先頭要素は二分木の根ノード、つまりリストの最初の要素です。リストから最初の要素を直接削除すると、二分木内のすべてのノードインデックスが変更され、後続の修復にヒープ化を使用することが困難になります。要素インデックスの変更を最小限に抑えるため、次の手順を使用します。 +
+ -1. ヒープの先頭要素と底の要素を交換します(根ノードと最も右の葉ノードを交換)。 -2. 交換後、リストからヒープの底を削除します(交換されているため、実際には元の先頭要素が削除される)。 -3. 根ノードから開始して、**上から下にヒープ化を実行します**。 +### 4.   ヒープ頂点の要素を取り出す -下図に示すように、**「上から下のヒープ化」の方向は「下から上のヒープ化」と反対です**。根ノードの値をその2つの子と比較し、最大の子と交換します。次に、葉ノードに到達するか、交換が不要なノードに遭遇するまで、この操作を繰り返します。 +ヒープ頂点の要素は二分木の根ノード、すなわちリストの先頭要素です。もし先頭要素をそのまま削除すると、二分木内のすべてのノードのインデックスが変化してしまい、その後のヒープ化による修復が困難になります。要素インデックスの変動をできるだけ小さくするため、次の手順を取ります。 + +1. ヒープ頂点の要素とヒープ底の要素を交換する(根ノードと最も右の葉ノードを交換する)。 +2. 交換後、ヒープ底をリストから削除する(すでに交換済みであるため、実際に削除されるのは元のヒープ頂点の要素であることに注意)。 +3. 根ノードから開始し、**上から下へヒープ化**を行う。 + +次の図のように、**「上から下へのヒープ化」の方向は「下から上へのヒープ化」と逆**です。根ノードの値を 2 つの子ノードと比較し、最大の子ノードと根ノードを交換します。その後、この操作を繰り返し、葉ノードを越えるか交換不要のノードに達した時点で終了します。 === "<1>" - ![ヒープからの先頭要素削除の手順](heap.assets/heap_pop_step1.png){ class="animation-figure" } + ![ヒープ頂点の要素を取り出す手順](heap.assets/heap_pop_step1.png){ class="animation-figure" } === "<2>" ![heap_pop_step2](heap.assets/heap_pop_step2.png){ class="animation-figure" } @@ -915,55 +1255,55 @@ comments: true === "<10>" ![heap_pop_step10](heap.assets/heap_pop_step10.png){ class="animation-figure" } -

図 8-4   ヒープからの先頭要素削除の手順

+

図 8-4   ヒープ頂点の要素を取り出す手順

-要素挿入操作と同様に、先頭要素削除操作の時間計算量も$O(\log n)$です。コードは以下の通りです: +要素をヒープに追加する操作と同様に、ヒープ頂点の要素を取り出す操作の時間計算量も $O(\log n)$ です。コードは以下のとおりです: === "Python" ```python title="my_heap.py" def pop(self) -> int: - """要素をヒープから出す""" - # 空の処理 + """要素をヒープから取り出す""" + # 空判定の処理 if self.is_empty(): - raise IndexError("Heap is empty") - # ルートノードと最右端の葉ノードを交換(最初の要素と最後の要素を交換) + raise IndexError("ヒープが空です") + # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) self.swap(0, self.size() - 1) # ノードを削除 val = self.max_heap.pop() # 上から下へヒープ化 self.sift_down(0) - # ヒープの先頭要素を返す + # ヒープ先頭要素を返す return val def sift_down(self, i: int): - """ノードiから開始して、上から下へヒープ化""" + """ノード i から始めて、上から下へヒープ化""" while True: - # i、l、rの中で最大のノードを決定し、maとする + # ノード i, l, r のうち値が最大のノードを ma とする l, r, ma = self.left(i), self.right(i), i if l < self.size() and self.max_heap[l] > self.max_heap[ma]: ma = l if r < self.size() and self.max_heap[r] > self.max_heap[ma]: ma = r - # ノードiが最大またはインデックスl、rが範囲外の場合、さらなるヒープ化は不要、ブレーク + # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける if ma == i: break - # 2つのノードを交換 + # 2 つのノードを交換 self.swap(i, ma) - # 下向きのループヒープ化 + # ループで上から下へヒープ化 i = ma ``` === "C++" ```cpp title="my_heap.cpp" - /* 要素がヒープから退出 */ + /* 要素をヒープから取り出す */ void pop() { - // 空の処理 + // 空判定の処理 if (isEmpty()) { - throw out_of_range("Heap is empty"); + throw out_of_range("ヒープが空です"); } - // ルートノードを最も右の葉ノードと交換(最初の要素と最後の要素を交換) + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) swap(maxHeap[0], maxHeap[size() - 1]); // ノードを削除 maxHeap.pop_back(); @@ -971,20 +1311,20 @@ comments: true siftDown(0); } - /* ノードiから下向きにヒープ化を開始 */ + /* ノード i から始めて、上から下へヒープ化 */ void siftDown(int i) { while (true) { - // i、l、rの中で最大のノードを決定し、maとして記録 + // ノード i, l, r のうち値が最大のノードを ma とする int l = left(i), r = right(i), ma = i; if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l; if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r; - // ノードiが最大、またはインデックスl、rが範囲外の場合、これ以上のヒープ化は不要、ブレーク + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける if (ma == i) break; swap(maxHeap[i], maxHeap[ma]); - // 下向きにループしてヒープ化 + // ループで上から下へヒープ化 i = ma; } } @@ -993,36 +1333,36 @@ comments: true === "Java" ```java title="my_heap.java" - /* 要素がヒープから退出 */ + /* 要素をヒープから取り出す */ int pop() { - // 空の処理 + // 空判定の処理 if (isEmpty()) throw new IndexOutOfBoundsException(); - // 根ノードを最も右の葉ノードと交換(最初の要素を最後の要素と交換) + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) swap(0, size() - 1); // ノードを削除 int val = maxHeap.remove(size() - 1); // 上から下へヒープ化 siftDown(0); - // ヒープの先頭要素を返す + // ヒープ先頭要素を返す return val; } - /* ノード i から下向きにヒープ化を開始 */ + /* ノード i から始めて、上から下へヒープ化 */ void siftDown(int i) { while (true) { - // i、l、r の中で最大のノードを決定し、ma とする + // ノード i, l, r のうち値が最大のノードを ma とする int l = left(i), r = right(i), ma = i; if (l < size() && maxHeap.get(l) > maxHeap.get(ma)) ma = l; if (r < size() && maxHeap.get(r) > maxHeap.get(ma)) ma = r; - // ノード i が最大の場合、またはインデックス l、r が範囲外の場合、さらなるヒープ化は不要、終了 + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける if (ma == i) break; - // 2つのノードを交換 + // 2 つのノードを交換 swap(i, ma); - // 下向きにヒープ化をループ + // ループで上から下へヒープ化 i = ma; } } @@ -1031,85 +1371,409 @@ comments: true === "C#" ```csharp title="my_heap.cs" - [class]{MaxHeap}-[func]{Pop} + /* 要素をヒープから取り出す */ + int Pop() { + // 空判定の処理 + if (IsEmpty()) + throw new IndexOutOfRangeException(); + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + Swap(0, Size() - 1); + // ノードを削除 + int val = maxHeap.Last(); + maxHeap.RemoveAt(Size() - 1); + // 上から下へヒープ化 + SiftDown(0); + // ヒープ先頭要素を返す + return val; + } - [class]{MaxHeap}-[func]{SiftDown} + /* ノード i から始めて、上から下へヒープ化 */ + void SiftDown(int i) { + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + int l = Left(i), r = Right(i), ma = i; + if (l < Size() && maxHeap[l] > maxHeap[ma]) + ma = l; + if (r < Size() && maxHeap[r] > maxHeap[ma]) + ma = r; + // 「ノード i が最大」または「葉ノードを越えた」場合は、ヒープ化を終了する + if (ma == i) break; + // 2 つのノードを交換 + Swap(i, ma); + // ループで上から下へヒープ化 + i = ma; + } + } ``` === "Go" ```go title="my_heap.go" - [class]{maxHeap}-[func]{pop} + /* 要素をヒープから取り出す */ + func (h *maxHeap) pop() any { + // 空判定の処理 + if h.isEmpty() { + fmt.Println("error") + return nil + } + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + h.swap(0, h.size()-1) + // ノードを削除 + val := h.data[len(h.data)-1] + h.data = h.data[:len(h.data)-1] + // 上から下へヒープ化 + h.siftDown(0) - [class]{maxHeap}-[func]{siftDown} + // ヒープ先頭要素を返す + return val + } + + /* ノード i から始めて、上から下へヒープ化 */ + func (h *maxHeap) siftDown(i int) { + for true { + // ノード i, l, r のうち値が最大のノードを max とする + l, r, max := h.left(i), h.right(i), i + if l < h.size() && h.data[l].(int) > h.data[max].(int) { + max = l + } + if r < h.size() && h.data[r].(int) > h.data[max].(int) { + max = r + } + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if max == i { + break + } + // 2 つのノードを交換 + h.swap(i, max) + // ループで上から下へヒープ化 + i = max + } + } ``` === "Swift" ```swift title="my_heap.swift" - [class]{MaxHeap}-[func]{pop} + /* 要素をヒープから取り出す */ + func pop() -> Int { + // 空判定の処理 + if isEmpty() { + fatalError("ヒープが空です") + } + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + swap(i: 0, j: size() - 1) + // ノードを削除 + let val = maxHeap.remove(at: size() - 1) + // 上から下へヒープ化 + siftDown(i: 0) + // ヒープ先頭要素を返す + return val + } - [class]{MaxHeap}-[func]{siftDown} + /* ノード i から始めて、上から下へヒープ化 */ + func siftDown(i: Int) { + var i = i + while true { + // ノード i, l, r のうち値が最大のノードを ma とする + let l = left(i: i) + let r = right(i: i) + var ma = i + if l < size(), maxHeap[l] > maxHeap[ma] { + ma = l + } + if r < size(), maxHeap[r] > maxHeap[ma] { + ma = r + } + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if ma == i { + break + } + // 2 つのノードを交換 + swap(i: i, j: ma) + // ループで上から下へヒープ化 + i = ma + } + } ``` === "JS" ```javascript title="my_heap.js" - [class]{MaxHeap}-[func]{pop} + /* 要素をヒープから取り出す */ + pop() { + // 空判定の処理 + if (this.isEmpty()) throw new Error('ヒープが空です'); + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + this.#swap(0, this.size() - 1); + // ノードを削除 + const val = this.#maxHeap.pop(); + // 上から下へヒープ化 + this.#siftDown(0); + // ヒープ先頭要素を返す + return val; + } - [class]{MaxHeap}-[func]{siftDown} + /* ノード i から始めて、上から下へヒープ化 */ + #siftDown(i) { + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + const l = this.#left(i), + r = this.#right(i); + let ma = i; + if (l < this.size() && this.#maxHeap[l] > this.#maxHeap[ma]) ma = l; + if (r < this.size() && this.#maxHeap[r] > this.#maxHeap[ma]) ma = r; + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma === i) break; + // 2 つのノードを交換 + this.#swap(i, ma); + // ループで上から下へヒープ化 + i = ma; + } + } ``` === "TS" ```typescript title="my_heap.ts" - [class]{MaxHeap}-[func]{pop} + /* 要素をヒープから取り出す */ + pop(): number { + // 空判定の処理 + if (this.isEmpty()) throw new RangeError('Heap is empty.'); + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + this.swap(0, this.size() - 1); + // ノードを削除 + const val = this.maxHeap.pop(); + // 上から下へヒープ化 + this.siftDown(0); + // ヒープ先頭要素を返す + return val; + } - [class]{MaxHeap}-[func]{siftDown} + /* ノード i から始めて、上から下へヒープ化 */ + siftDown(i: number): void { + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + const l = this.left(i), + r = this.right(i); + let ma = i; + if (l < this.size() && this.maxHeap[l] > this.maxHeap[ma]) ma = l; + if (r < this.size() && this.maxHeap[r] > this.maxHeap[ma]) ma = r; + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma === i) break; + // 2 つのノードを交換 + this.swap(i, ma); + // ループで上から下へヒープ化 + i = ma; + } + } ``` === "Dart" ```dart title="my_heap.dart" - [class]{MaxHeap}-[func]{pop} + /* 要素をヒープから取り出す */ + int pop() { + // 空判定の処理 + if (isEmpty()) throw Exception('ヒープが空です'); + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + _swap(0, size() - 1); + // ノードを削除 + int val = _maxHeap.removeLast(); + // 上から下へヒープ化 + siftDown(0); + // ヒープ先頭要素を返す + return val; + } - [class]{MaxHeap}-[func]{siftDown} + /* ノード i から始めて、上から下へヒープ化 */ + void siftDown(int i) { + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + int l = _left(i); + int r = _right(i); + int ma = i; + if (l < size() && _maxHeap[l] > _maxHeap[ma]) ma = l; + if (r < size() && _maxHeap[r] > _maxHeap[ma]) ma = r; + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma == i) break; + // 2 つのノードを交換 + _swap(i, ma); + // ループで上から下へヒープ化 + i = ma; + } + } ``` === "Rust" ```rust title="my_heap.rs" - [class]{MaxHeap}-[func]{pop} + /* 要素をヒープから取り出す */ + fn pop(&mut self) -> i32 { + // 空判定の処理 + if self.is_empty() { + panic!("index out of bounds"); + } + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + self.swap(0, self.size() - 1); + // ノードを削除 + let val = self.max_heap.pop().unwrap(); + // 上から下へヒープ化 + self.sift_down(0); + // ヒープ先頭要素を返す + val + } - [class]{MaxHeap}-[func]{sift_down} + /* ノード i から始めて、上から下へヒープ化 */ + fn sift_down(&mut self, mut i: usize) { + loop { + // ノード i, l, r のうち値が最大のノードを ma とする + let (l, r, mut ma) = (Self::left(i), Self::right(i), i); + if l < self.size() && self.max_heap[l] > self.max_heap[ma] { + ma = l; + } + if r < self.size() && self.max_heap[r] > self.max_heap[ma] { + ma = r; + } + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if ma == i { + break; + } + // 2 つのノードを交換 + self.swap(i, ma); + // ループで上から下へヒープ化 + i = ma; + } + } ``` === "C" ```c title="my_heap.c" - [class]{MaxHeap}-[func]{pop} + /* 要素をヒープから取り出す */ + int pop(MaxHeap *maxHeap) { + // 空判定の処理 + if (isEmpty(maxHeap)) { + printf("heap is empty!"); + return INT_MAX; + } + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + swap(maxHeap, 0, size(maxHeap) - 1); + // ノードを削除 + int val = maxHeap->data[maxHeap->size - 1]; + maxHeap->size--; + // 上から下へヒープ化 + siftDown(maxHeap, 0); - [class]{MaxHeap}-[func]{siftDown} + // ヒープ先頭要素を返す + return val; + } + + /* ノード i から始めて、上から下へヒープ化 */ + void siftDown(MaxHeap *maxHeap, int i) { + while (true) { + // ノード i, l, r のうち値が最大のノードを max とする + int l = left(maxHeap, i); + int r = right(maxHeap, i); + int max = i; + if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) { + max = l; + } + if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) { + max = r; + } + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (max == i) { + break; + } + // 2 つのノードを交換 + swap(maxHeap, i, max); + // ループで上から下へヒープ化 + i = max; + } + } ``` === "Kotlin" ```kotlin title="my_heap.kt" - [class]{MaxHeap}-[func]{pop} + /* 要素をヒープから取り出す */ + fun pop(): Int { + // 空判定の処理 + if (isEmpty()) throw IndexOutOfBoundsException() + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + swap(0, size() - 1) + // ノードを削除 + val _val = maxHeap.removeAt(size() - 1) + // 上から下へヒープ化 + siftDown(0) + // ヒープ先頭要素を返す + return _val + } - [class]{MaxHeap}-[func]{siftDown} + /* ノード i から始めて、上から下へヒープ化 */ + fun siftDown(it: Int) { + // Kotlin の関数引数は不変のため、一時変数を作成する + var i = it + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + val l = left(i) + val r = right(i) + var ma = i + if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l + if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma == i) break + // 2 つのノードを交換 + swap(i, ma) + // ループで上から下へヒープ化 + i = ma + } + } ``` === "Ruby" ```ruby title="my_heap.rb" - [class]{MaxHeap}-[func]{pop} + ### 要素をヒープから取り出す ### + def pop + # 空判定の処理 + raise IndexError, "ヒープが空です" if is_empty? + # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + swap(0, size - 1) + # ノードを削除 + val = @max_heap.pop + # 上から下へヒープ化 + sift_down(0) + # ヒープ先頭要素を返す + val + end - [class]{MaxHeap}-[func]{sift_down} + ### ノード i から上から下へヒープ化 ### + def sift_down(i) + loop do + # ノード i, l, r のうち値が最大のノードを ma とする + l, r, ma = left(i), right(i), i + ma = l if l < size && @max_heap[l] > @max_heap[ma] + ma = r if r < size && @max_heap[r] > @max_heap[ma] + + # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + break if ma == i + + # 2 つのノードを交換 + swap(i, ma) + # ループで上から下へヒープ化 + i = ma + end + end ``` -## 8.1.3   ヒープの一般的な応用 +??? pythontutor "コードの可視化" -- **優先度キュー**:ヒープは優先度キューを実装するための好ましいデータ構造で、エンキュー操作とデキュー操作の両方の時間計算量が$O(\log n)$、キュー構築の時間計算量が$O(n)$で、すべて非常に効率的です。 -- **ヒープソート**:データセットが与えられた場合、それらからヒープを作成し、次に要素削除操作を継続的に実行して順序付けされたデータを取得できます。ただし、ヒープソートを実装するより洗練された方法があり、「ヒープソート」の章で説明されています。 -- **最大$k$個の要素の発見**:これは古典的なアルゴリズム問題であり、一般的な使用例でもあります。Weiboホット検索のトップ10ホットニュースの選択や、トップ10の売れ筋商品の選択などです。 +
+ + +## 8.1.3   ヒープの代表的な応用 + +- **優先度付きキュー**:ヒープは、優先度付きキューを実装するための代表的なデータ構造です。キューへの追加と取り出しの時間計算量はいずれも $O(\log n)$ で、ヒープ構築は $O(n)$ であり、これらの操作はいずれも非常に効率的です。 +- **ヒープソート**:与えられたデータ群からヒープを構築し、要素の取り出しを繰り返すことで整列済みデータを得られます。ただし、通常はより洗練された方法でヒープソートを実装します。詳しくは「ヒープソート」の章を参照してください。 +- **最大の $k$ 個の要素を取得**:これは古典的なアルゴリズム問題であると同時に、典型的な応用でもあります。たとえば、人気上位 10 件のニュースをホットトピックとして選んだり、売上上位 10 件の商品を選んだりする場面です。 diff --git a/ja/docs/chapter_heap/index.md b/ja/docs/chapter_heap/index.md index eba589741..86d5b7ad3 100644 --- a/ja/docs/chapter_heap/index.md +++ b/ja/docs/chapter_heap/index.md @@ -9,13 +9,13 @@ icon: material/family-tree !!! abstract - ヒープは山とその険しい峰のように、層をなして起伏し、それぞれが独特の形を持っています。 - - 各山の頂は散らばった高さで上下しますが、最も高いものが常に最初に注目を集めます。 + ヒープは連なる山々の峰のように、幾重にも重なり、さまざまな形をしている。 + + いくつもの山の高さはまちまちだが、最も高い峰がいつも最初に目に入る。 ## 章の内容 - [8.1   ヒープ](heap.md) -- [8.2   ヒープ構築操作](build_heap.md) +- [8.2   ヒープ構築](build_heap.md) - [8.3   Top-k 問題](top_k.md) - [8.4   まとめ](summary.md) diff --git a/ja/docs/chapter_heap/summary.md b/ja/docs/chapter_heap/summary.md index fe709a8fe..6a7098007 100644 --- a/ja/docs/chapter_heap/summary.md +++ b/ja/docs/chapter_heap/summary.md @@ -4,18 +4,18 @@ comments: true # 8.4   まとめ -### 1.   重要な復習 +### 1.   重要なポイントの振り返り -- ヒープは完備二分木で、その構築性質に基づいて最大ヒープまたは最小ヒープに分類できます。最大ヒープの先頭要素は最大で、最小ヒープの先頭要素は最小です。 -- 優先度キューは、デキューの優先度を持つキューとして定義され、通常ヒープを使用して実装されます。 -- ヒープの一般的な操作とそれに対応する時間計算量には以下があります:ヒープへの要素挿入$O(\log n)$、ヒープからの先頭要素削除$O(\log n)$、ヒープの先頭要素へのアクセス$O(1)$。 -- 完備二分木は配列で表現するのに適しているため、ヒープは一般的に配列を使用して格納されます。 -- ヒープ化操作はヒープの性質を維持するために使用され、ヒープの挿入操作と削除操作の両方で使用されます。 -- $n$個の要素が入力として与えられた場合のヒープ構築の時間計算量は$O(n)$に最適化でき、これは非常に効率的です。 -- Top-kは古典的なアルゴリズム問題で、ヒープデータ構造を使用して効率的に解決でき、時間計算量は$O(n \log k)$です。 +- ヒープは完全二分木であり、条件の違いによって最大ヒープと最小ヒープに分けられる。最大(最小)ヒープの根の要素は最大値(最小値)である。 +- 優先度付きキューとは、取り出し時に優先度が考慮されるキューであり、通常はヒープを用いて実装される。 +- ヒープの代表的な操作とそれに対応する時間計算量には、要素の挿入 $O(\log n)$、根の要素の削除 $O(\log n)$、根の要素へのアクセス $O(1)$ などがある。 +- 完全二分木は配列で表現するのに非常に適しているため、通常は配列を使ってヒープを格納する。 +- ヒープ化操作はヒープの性質を保つために用いられ、挿入操作と削除操作の両方で使用される。 +- $n$ 個の要素を入力してヒープを構築する時間計算量は $O(n)$ まで最適化でき、非常に効率的である。 +- Top-k は古典的なアルゴリズム問題であり、ヒープ構造を用いることで効率的に解くことができ、時間計算量は $O(n \log k)$ である。 ### 2.   Q & A -**Q**: データ構造の「ヒープ」とメモリ管理の「ヒープ」は同じ概念ですか? +**Q**:データ構造の「ヒープ」とメモリ管理の「ヒープ」は同じ概念ですか? -この2つは、どちらも「ヒープ」と呼ばれますが、同じ概念ではありません。コンピュータシステムメモリのヒープは動的メモリ割り当ての一部で、プログラムが実行中にデータを格納するために使用できます。プログラムは、オブジェクトや配列などの複雑な構造を格納するために、一定量のヒープメモリを要求できます。割り当てられたデータが不要になったときは、メモリリークを防ぐためにプログラムがこのメモリを解放する必要があります。スタックメモリと比較して、ヒープメモリの管理と使用にはより多くの注意が必要で、不適切な使用はメモリリークやダングリングポインタにつながる可能性があります。 +両者は同じ概念ではなく、たまたまどちらも「ヒープ」と呼ばれているだけである。コンピュータシステムのメモリにおけるヒープは動的メモリ割り当ての一部であり、プログラムは実行時にこれを使ってデータを格納できる。プログラムは一定量のヒープメモリを要求し、オブジェクトや配列などの複雑な構造を保存できる。これらのデータが不要になったときは、メモリリークを防ぐためにそのメモリを解放する必要がある。スタックメモリと比べると、ヒープメモリの管理と使用にはより慎重さが求められ、不適切に扱うとメモリリークやダングリングポインタなどの問題を引き起こす可能性がある。 diff --git a/ja/docs/chapter_heap/top_k.md b/ja/docs/chapter_heap/top_k.md index 3f3b8de0e..7c4e8d75d 100644 --- a/ja/docs/chapter_heap/top_k.md +++ b/ja/docs/chapter_heap/top_k.md @@ -2,49 +2,49 @@ comments: true --- -# 8.3   Top-k問題 +# 8.3   Top-k 問題 !!! question - 長さ$n$の順序付けられていない配列`nums`が与えられたとき、配列内の最大$k$個の要素を返してください。 + 長さ $n$ の未整列配列 `nums` が与えられたとき、配列内で最大の $k$ 個の要素を返してください。 -この問題について、まず2つの直接的な解法を紹介し、次により効率的なヒープベースの方法を説明します。 +この問題について、まずは発想が比較的直接的な 2 つの解法を紹介し、その後でより効率の高いヒープ解法を紹介します。 -## 8.3.1   方法1:反復選択 +## 8.3.1   方法一:走査による選択 -下図に示すように、$k$回の反復を実行し、各回で$1$番目、$2$番目、$\dots$、$k$番目に大きい要素を抽出できます。時間計算量は$O(nk)$です。 +以下の図に示すように $k$ 回の走査を行い、各ラウンドでそれぞれ第 $1$、$2$、$\dots$、$k$ 位の要素を取り出すことができます。時間計算量は $O(nk)$ です。 -この方法は$k \ll n$の場合にのみ適しています。$k$が$n$に近い場合、時間計算量は$O(n^2)$に近づき、非常に時間がかかります。 +この方法は $k \ll n$ の場合にしか適していません。$k$ が $n$ にかなり近いと、時間計算量は $O(n^2)$ に近づき、非常に時間がかかるためです。 -![最大k個の要素を反復的に見つける](top_k.assets/top_k_traversal.png){ class="animation-figure" } +![走査によって最大の k 個の要素を探す](top_k.assets/top_k_traversal.png){ class="animation-figure" } -

図 8-6   最大k個の要素を反復的に見つける

+

図 8-6   走査によって最大の k 個の要素を探す

!!! tip - $k = n$の場合、完全に順序付けられたシーケンスを得ることができ、これは「選択ソート」アルゴリズムと同等です。 + $k = n$ のとき、完全な昇順列を得ることができ、この場合は「選択ソート」アルゴリズムと等価になります。 -## 8.3.2   方法2:ソート +## 8.3.2   方法二:ソート -下図に示すように、まず配列`nums`をソートし、次に最後の$k$個の要素を返すことができます。時間計算量は$O(n \log n)$です。 +以下の図に示すように、まず配列 `nums` をソートし、その後で右端の $k$ 個の要素を返すことができます。時間計算量は $O(n \log n)$ です。 -明らかに、この方法はタスクを「やりすぎ」ています。最大$k$個の要素を見つけるだけでよく、他の要素をソートする必要はありません。 +明らかに、この方法は必要以上の処理を行っています。なぜなら、必要なのは最大の $k$ 個の要素を見つけることだけであり、他の要素をソートする必要はないからです。 -![ソートによる最大k個の要素の発見](top_k.assets/top_k_sorting.png){ class="animation-figure" } +![ソートによって最大の k 個の要素を探す](top_k.assets/top_k_sorting.png){ class="animation-figure" } -

図 8-7   ソートによる最大k個の要素の発見

+

図 8-7   ソートによって最大の k 個の要素を探す

-## 8.3.3   方法3:ヒープ +## 8.3.3   方法三:ヒープ -以下のプロセスに示すように、ヒープに基づいてTop-k問題をより効率的に解決できます。 +ヒープを用いることで、Top-k 問題をより効率的に解くことができます。手順は以下の図のとおりです。 -1. 最小ヒープを初期化します。先頭要素が最小になります。 -2. まず、配列の最初の$k$個の要素をヒープに挿入します。 -3. $k + 1$番目の要素から開始し、現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに挿入します。 -4. 走査を完了した後、ヒープには最大$k$個の要素が含まれています。 +1. 最小ヒープを初期化し、そのヒープ頂点の要素が最小となるようにします。 +2. まず配列の先頭 $k$ 個の要素を順にヒープへ挿入します。 +3. $k + 1$ 番目の要素から開始し、現在の要素がヒープ頂点の要素より大きければ、ヒープ頂点の要素を取り出し、現在の要素をヒープへ挿入します。 +4. 走査が完了した後、ヒープに保持されているのが最大の $k$ 個の要素です。 === "<1>" - ![ヒープに基づく最大k個の要素の発見](top_k.assets/top_k_heap_step1.png){ class="animation-figure" } + ![ヒープに基づいて最大の k 個の要素を探す](top_k.assets/top_k_heap_step1.png){ class="animation-figure" } === "<2>" ![top_k_heap_step2](top_k.assets/top_k_heap_step2.png){ class="animation-figure" } @@ -70,23 +70,23 @@ comments: true === "<9>" ![top_k_heap_step9](top_k.assets/top_k_heap_step9.png){ class="animation-figure" } -

図 8-8   ヒープに基づく最大k個の要素の発見

+

図 8-8   ヒープに基づいて最大の k 個の要素を探す

-サンプルコードは以下の通りです: +サンプルコードは以下のとおりです。 === "Python" ```python title="top_k.py" def top_k_heap(nums: list[int], k: int) -> list[int]: - """ヒープを使用して配列内の最大k個の要素を見つける""" + """ヒープに基づいて配列中の最大の k 個の要素を探す""" # 最小ヒープを初期化 heap = [] - # 配列の最初のk個の要素をヒープに入力 + # 配列の先頭 k 個の要素をヒープに追加 for i in range(k): heapq.heappush(heap, nums[i]) - # k+1番目の要素から、ヒープの長さをkに保つ + # k+1 番目の要素から開始し、ヒープ長を k に保つ for i in range(k, len(nums)): - # 現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに入力 + # 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する if nums[i] > heap[0]: heapq.heappop(heap) heapq.heappush(heap, nums[i]) @@ -96,17 +96,17 @@ comments: true === "C++" ```cpp title="top_k.cpp" - /* ヒープを使用して配列内の最大k個の要素を見つける */ + /* ヒープに基づいて配列中の最大の k 個の要素を探す */ priority_queue, greater> topKHeap(vector &nums, int k) { // 最小ヒープを初期化 priority_queue, greater> heap; - // 配列の最初のk個の要素をヒープに入力 + // 配列の先頭 k 個の要素をヒープに追加 for (int i = 0; i < k; i++) { heap.push(nums[i]); } - // k+1番目の要素から、ヒープの長さをkに保つ + // k+1 番目の要素から開始し、ヒープ長を k に保つ for (int i = k; i < nums.size(); i++) { - // 現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに入力 + // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する if (nums[i] > heap.top()) { heap.pop(); heap.push(nums[i]); @@ -119,17 +119,17 @@ comments: true === "Java" ```java title="top_k.java" - /* ヒープを使用して配列内の最大 k 個の要素を検索 */ + /* ヒープに基づいて配列中の最大の k 個の要素を探す */ Queue topKHeap(int[] nums, int k) { // 最小ヒープを初期化 Queue heap = new PriorityQueue(); - // 配列の最初の k 個の要素をヒープに入力 + // 配列の先頭 k 個の要素をヒープに追加 for (int i = 0; i < k; i++) { heap.offer(nums[i]); } - // k+1 番目の要素から、ヒープの長さを k に保つ + // k+1 番目の要素から開始し、ヒープ長を k に保つ for (int i = k; i < nums.length; i++) { - // 現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに入力 + // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する if (nums[i] > heap.peek()) { heap.poll(); heap.offer(nums[i]); @@ -142,87 +142,330 @@ comments: true === "C#" ```csharp title="top_k.cs" - [class]{top_k}-[func]{TopKHeap} + /* ヒープに基づいて配列中の最大の k 個の要素を探す */ + PriorityQueue TopKHeap(int[] nums, int k) { + // 最小ヒープを初期化 + PriorityQueue heap = new(); + // 配列の先頭 k 個の要素をヒープに追加 + for (int i = 0; i < k; i++) { + heap.Enqueue(nums[i], nums[i]); + } + // k+1 番目の要素から開始し、ヒープ長を k に保つ + for (int i = k; i < nums.Length; i++) { + // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する + if (nums[i] > heap.Peek()) { + heap.Dequeue(); + heap.Enqueue(nums[i], nums[i]); + } + } + return heap; + } ``` === "Go" ```go title="top_k.go" - [class]{}-[func]{topKHeap} + /* ヒープに基づいて配列中の最大の k 個の要素を探す */ + func topKHeap(nums []int, k int) *minHeap { + // 最小ヒープを初期化 + h := &minHeap{} + heap.Init(h) + // 配列の先頭 k 個の要素をヒープに追加 + for i := 0; i < k; i++ { + heap.Push(h, nums[i]) + } + // k+1 番目の要素から開始し、ヒープ長を k に保つ + for i := k; i < len(nums); i++ { + // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する + if nums[i] > h.Top().(int) { + heap.Pop(h) + heap.Push(h, nums[i]) + } + } + return h + } ``` === "Swift" ```swift title="top_k.swift" - [class]{}-[func]{topKHeap} + /* ヒープに基づいて配列中の最大の k 個の要素を探す */ + func topKHeap(nums: [Int], k: Int) -> [Int] { + // 最小ヒープを初期化し、先頭 k 個の要素でヒープを構築する + var heap = Heap(nums.prefix(k)) + // k+1 番目の要素から開始し、ヒープ長を k に保つ + for i in nums.indices.dropFirst(k) { + // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する + if nums[i] > heap.min()! { + _ = heap.removeMin() + heap.insert(nums[i]) + } + } + return heap.unordered + } ``` === "JS" ```javascript title="top_k.js" - [class]{}-[func]{pushMinHeap} + /* 要素をヒープに追加 */ + function pushMinHeap(maxHeap, val) { + // 要素を反転する + maxHeap.push(-val); + } - [class]{}-[func]{popMinHeap} + /* 要素をヒープから取り出す */ + function popMinHeap(maxHeap) { + // 要素を反転する + return -maxHeap.pop(); + } - [class]{}-[func]{peekMinHeap} + /* ヒープ先頭要素にアクセス */ + function peekMinHeap(maxHeap) { + // 要素を反転する + return -maxHeap.peek(); + } - [class]{}-[func]{getMinHeap} + /* ヒープから要素を取り出す */ + function getMinHeap(maxHeap) { + // 要素を反転する + return maxHeap.getMaxHeap().map((num) => -num); + } - [class]{}-[func]{topKHeap} + /* ヒープに基づいて配列中の最大の k 個の要素を探す */ + function topKHeap(nums, k) { + // 最小ヒープを初期化する + // 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする + const maxHeap = new MaxHeap([]); + // 配列の先頭 k 個の要素をヒープに追加 + for (let i = 0; i < k; i++) { + pushMinHeap(maxHeap, nums[i]); + } + // k+1 番目の要素から開始し、ヒープ長を k に保つ + for (let i = k; i < nums.length; i++) { + // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する + if (nums[i] > peekMinHeap(maxHeap)) { + popMinHeap(maxHeap); + pushMinHeap(maxHeap, nums[i]); + } + } + // ヒープ内の要素を返す + return getMinHeap(maxHeap); + } ``` === "TS" ```typescript title="top_k.ts" - [class]{}-[func]{pushMinHeap} + /* 要素をヒープに追加 */ + function pushMinHeap(maxHeap: MaxHeap, val: number): void { + // 要素を反転する + maxHeap.push(-val); + } - [class]{}-[func]{popMinHeap} + /* 要素をヒープから取り出す */ + function popMinHeap(maxHeap: MaxHeap): number { + // 要素を反転する + return -maxHeap.pop(); + } - [class]{}-[func]{peekMinHeap} + /* ヒープ先頭要素にアクセス */ + function peekMinHeap(maxHeap: MaxHeap): number { + // 要素を反転する + return -maxHeap.peek(); + } - [class]{}-[func]{getMinHeap} + /* ヒープから要素を取り出す */ + function getMinHeap(maxHeap: MaxHeap): number[] { + // 要素を反転する + return maxHeap.getMaxHeap().map((num: number) => -num); + } - [class]{}-[func]{topKHeap} + /* ヒープに基づいて配列中の最大の k 個の要素を探す */ + function topKHeap(nums: number[], k: number): number[] { + // 最小ヒープを初期化する + // 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする + const maxHeap = new MaxHeap([]); + // 配列の先頭 k 個の要素をヒープに追加 + for (let i = 0; i < k; i++) { + pushMinHeap(maxHeap, nums[i]); + } + // k+1 番目の要素から開始し、ヒープ長を k に保つ + for (let i = k; i < nums.length; i++) { + // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する + if (nums[i] > peekMinHeap(maxHeap)) { + popMinHeap(maxHeap); + pushMinHeap(maxHeap, nums[i]); + } + } + // ヒープ内の要素を返す + return getMinHeap(maxHeap); + } ``` === "Dart" ```dart title="top_k.dart" - [class]{}-[func]{topKHeap} + /* ヒープに基づいて配列中の最大の k 個の要素を探す */ + MinHeap topKHeap(List nums, int k) { + // 最小ヒープを初期化し、配列の先頭 k 個の要素をヒープに入れる + MinHeap heap = MinHeap(nums.sublist(0, k)); + // k+1 番目の要素から開始し、ヒープ長を k に保つ + for (int i = k; i < nums.length; i++) { + // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する + if (nums[i] > heap.peek()) { + heap.pop(); + heap.push(nums[i]); + } + } + return heap; + } ``` === "Rust" ```rust title="top_k.rs" - [class]{}-[func]{top_k_heap} + /* ヒープに基づいて配列中の最大の k 個の要素を探す */ + fn top_k_heap(nums: Vec, k: usize) -> BinaryHeap> { + // BinaryHeap は最大ヒープであり、Reverse で要素の順序を反転することで最小ヒープを実現する + let mut heap = BinaryHeap::>::new(); + // 配列の先頭 k 個の要素をヒープに追加 + for &num in nums.iter().take(k) { + heap.push(Reverse(num)); + } + // k+1 番目の要素から開始し、ヒープ長を k に保つ + for &num in nums.iter().skip(k) { + // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する + if num > heap.peek().unwrap().0 { + heap.pop(); + heap.push(Reverse(num)); + } + } + heap + } ``` === "C" ```c title="top_k.c" - [class]{}-[func]{pushMinHeap} + /* 要素をヒープに追加 */ + void pushMinHeap(MaxHeap *maxHeap, int val) { + // 要素を反転する + push(maxHeap, -val); + } - [class]{}-[func]{popMinHeap} + /* 要素をヒープから取り出す */ + int popMinHeap(MaxHeap *maxHeap) { + // 要素を反転する + return -pop(maxHeap); + } - [class]{}-[func]{peekMinHeap} + /* ヒープ先頭要素にアクセス */ + int peekMinHeap(MaxHeap *maxHeap) { + // 要素を反転する + return -peek(maxHeap); + } - [class]{}-[func]{getMinHeap} + /* ヒープから要素を取り出す */ + int *getMinHeap(MaxHeap *maxHeap) { + // ヒープ内のすべての要素を反転して res 配列に格納 + int *res = (int *)malloc(maxHeap->size * sizeof(int)); + for (int i = 0; i < maxHeap->size; i++) { + res[i] = -maxHeap->data[i]; + } + return res; + } - [class]{}-[func]{topKHeap} + /* ヒープから要素を取り出す */ + int *getMinHeap(MaxHeap *maxHeap) { + // ヒープ内のすべての要素を反転して res 配列に格納 + int *res = (int *)malloc(maxHeap->size * sizeof(int)); + for (int i = 0; i < maxHeap->size; i++) { + res[i] = -maxHeap->data[i]; + } + return res; + } + + // ヒープに基づいて配列中の最大の k 個の要素を求める関数 + int *topKHeap(int *nums, int sizeNums, int k) { + // 最小ヒープを初期化する + // 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする + int *empty = (int *)malloc(0); + MaxHeap *maxHeap = newMaxHeap(empty, 0); + // 配列の先頭 k 個の要素をヒープに追加 + for (int i = 0; i < k; i++) { + pushMinHeap(maxHeap, nums[i]); + } + // k+1 番目の要素から開始し、ヒープ長を k に保つ + for (int i = k; i < sizeNums; i++) { + // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する + if (nums[i] > peekMinHeap(maxHeap)) { + popMinHeap(maxHeap); + pushMinHeap(maxHeap, nums[i]); + } + } + int *res = getMinHeap(maxHeap); + // メモリを解放する + delMaxHeap(maxHeap); + return res; + } ``` === "Kotlin" ```kotlin title="top_k.kt" - [class]{}-[func]{topKHeap} + /* ヒープに基づいて配列中の最大の k 個の要素を探す */ + fun topKHeap(nums: IntArray, k: Int): Queue { + // 最小ヒープを初期化 + val heap = PriorityQueue() + // 配列の先頭 k 個の要素をヒープに追加 + for (i in 0.. heap.peek()) { + heap.poll() + heap.offer(nums[i]) + } + } + return heap + } ``` === "Ruby" ```ruby title="top_k.rb" - [class]{}-[func]{top_k_heap} + ### ヒープに基づいて配列中の最大 k 個の要素を探す ### + def top_k_heap(nums, k) + # 最小ヒープを初期化する + # 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする + max_heap = MaxHeap.new([]) + + # 配列の先頭 k 個の要素をヒープに追加 + for i in 0...k + push_min_heap(max_heap, nums[i]) + end + + # k+1 番目の要素から開始し、ヒープ長を k に保つ + for i in k...nums.length + # 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する + if nums[i] > peek_min_heap(max_heap) + pop_min_heap(max_heap) + push_min_heap(max_heap, nums[i]) + end + end + + get_min_heap(max_heap) + end ``` -合計$n$回のヒープ挿入と削除が実行され、最大ヒープサイズが$k$であるため、時間計算量は$O(n \log k)$です。この方法は非常に効率的で、$k$が小さい場合、時間計算量は$O(n)$に近づき、$k$が大きい場合でも、時間計算量は$O(n \log n)$を超えません。 +??? pythontutor "コードの可視化" -さらに、この方法は動的データストリームのシナリオに適しています。データを継続的に追加することで、ヒープ内の要素を維持し、最大$k$個の要素の動的更新を実現できます。 +
+ + +合計で $n$ 回のヒープ挿入と取り出しを行い、ヒープの最大長は $k$ であるため、時間計算量は $O(n \log k)$ です。この方法は非常に効率が高く、$k$ が小さいときは時間計算量が $O(n)$ に近づき、$k$ が大きいときでも $O(n \log n)$ を超えることはありません。 + +さらに、この方法は動的データストリームの利用シーンにも適しています。データが継続的に追加される場合でも、ヒープ内の要素を保ち続けることで、最大の $k$ 個の要素を動的に更新できます。 diff --git a/ja/docs/chapter_hello_algo/index.md b/ja/docs/chapter_hello_algo/index.md index 7f1d01291..089da2962 100644 --- a/ja/docs/chapter_hello_algo/index.md +++ b/ja/docs/chapter_hello_algo/index.md @@ -5,26 +5,26 @@ icon: material/rocket-launch-outline # はじめに -数年前、私はLeetCodeで「剣指Offer」の問題解答を共有し、多くの読者から励ましとサポートを受けました。読者とのやり取りの中で、最もよく聞かれた質問は「アルゴリズムの勉強をどう始めたらよいか」でした。次第に、私はこの質問に強い関心を抱くようになりました。 +数年前、私は LeetCode 中国版で「剣指 Offer」シリーズの解説を共有し、多くの読者から励ましと支持をいただきました。読者との交流の中で、最もよく聞かれた質問の一つが「アルゴリズムをどう学び始めればよいか」でした。次第に、私はこの問題に強い関心を抱くようになりました。 -問題を直接解くことが最も人気のある方法のようです。これはシンプルで直接的で効果的です。しかし、問題解決はマインスイーパーをプレイするようなものです。自学自習の能力が高い人は、地雷を一つずつ回避していくことができますが、しっかりとした基礎がない人は、何度もつまずいて挫折しながら後退することになるかもしれません。教科書を読むことも一般的な方法ですが、就職活動中の人にとって、卒業論文の執筆、履歴書の提出、筆記試験や面接の準備が既にエネルギーの大部分を消費しており、分厚い本を読むことはしばしば困難な挑戦となります。 +手探りでひたすら問題を解くことは、最も人気のある方法のようです。単純で、直接的で、しかも効果的です。しかし問題演習は「マインスイーパー」を遊ぶことに似ており、独学力の高い人は地雷を一つずつうまく取り除ける一方で、基礎が十分でない人は大きな痛手を受け、挫折の中で少しずつ後退してしまいがちです。教材を通読するのもよくある方法ですが、就職を目指す人にとっては、卒業論文、履歴書の提出、筆記試験や面接の準備ですでに大半の力を使い果たしており、分厚い本を読み込むことはしばしば困難な挑戦になってしまいます。 -もしあなたが同様の悩みを抱えているなら、この本があなたを見つけることができて幸運です。この本は、この質問に対する私の答えです。これが最良の解決策ではないかもしれませんが、少なくとも積極的な試みです。この本があなたに直接内定をもたらすことはできませんが、データ構造とアルゴリズムの「知識地図」を探索する手引きとなり、さまざまな「地雷」の形、大きさ、位置を理解し、さまざまな「地雷除去方法」をマスターできるようお手伝いします。これらのスキルがあれば、より快適に問題を解き、文献を読むことができ、徐々に知識体系を構築できると信じています。 +もしあなたも同じような悩みを抱えているなら、この本があなたのもとに“たどり着いた”のは幸運なことです。本書は、この問いに対して私が示す答えです。たとえ最良の解ではなくても、少なくとも前向きな試みではあります。本書だけで直接内定を得られるわけではありませんが、データ構造とアルゴリズムの「知識地図」を探る手助けをし、さまざまな「地雷」の形や大きさ、分布を理解し、いろいろな「地雷除去の方法」を身につけられるよう導きます。こうした力があれば、問題演習や文献読解をより自在に進め、やがて完整な知識体系を築いていけると信じています。 -私は、ファインマン教授の言葉に深く同感します。「知識は無料ではありません。注意を払わなければならないのです。」この意味で、この本は完全に「無料」ではありません。この本に対するあなたの貴重な「注意」に応えるために、私は最善を尽くし、最大の「注意」を払ってこの本を書きます。 +私はファインマン教授の言葉に深く賛同しています。「Knowledge isn't free. You have to pay attention.」この意味において、本書は完全に「無料」ではありません。本書のためにあなたが払ってくれる貴重な「注意」に応えるため、私はできる限りの力を尽くし、最大限の「注意」を注いで本書を書き上げます。 -自分の限界を認識しており、この本の内容が時間をかけて洗練されたにもかかわらず、間違いは確実に残っていることを理解しています。先生方や学生の皆様からの批評と訂正を心から歓迎いたします。 +私は自らの学識と力量の浅さをよく承知しています。本書の内容はある程度磨きをかけてきたものの、なお多くの誤りが残っているはずです。先生方、そして学習者の皆さまからのご批判とご指摘を心よりお願いいたします。 -![Hello Algo](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" } +![Hello アルゴリズム](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" }
-

Hello, Algo!

+

Hello、アルゴリズム!

-コンピュータの出現は世界に大きな変化をもたらしました。高速な計算能力と優れたプログラム可能性により、コンピュータはアルゴリズムを実行しデータを処理するための理想的な媒体となりました。ビデオゲームのリアルなグラフィックス、自動運転の知的な判断、AlphaGoの見事な囲碁ゲーム、ChatGPTの自然な対話など、これらのアプリケーションはすべて、コンピュータ上で動作するアルゴリズムの精巧な実演です。 +コンピュータの登場は世界に大きな変革をもたらしました。高速な計算能力と優れたプログラム可能性によって、アルゴリズムの実行とデータ処理の理想的な媒体となったのです。ビデオゲームのリアルな映像、自動運転の知的な意思決定、AlphaGo の見事な対局、ChatGPT の自然な対話に至るまで、これらの応用はいずれもコンピュータ上でアルゴリズムが巧みに表現されたものです。 -実際、コンピュータの出現以前から、アルゴリズムとデータ構造は世界の至る所に存在していました。初期のアルゴリズムは比較的シンプルで、古代の計数方法や道具作りの手順などがありました。文明が進歩するにつれて、アルゴリズムはより洗練され複雑になりました。職人の精巧な技術から、生産力を解放する工業製品、宇宙を支配する科学法則まで、ほぼすべての平凡または驚異的なことの背後には、アルゴリズムの巧妙な思考があります。 +実際には、コンピュータが誕生する以前から、アルゴリズムとデータ構造は世界のいたるところに存在していました。初期のアルゴリズムは比較的単純で、たとえば古代の数え方や道具作りの手順などがそれに当たります。文明の進歩とともに、アルゴリズムは次第により精緻で複雑なものになっていきました。匠の巧みな技から、生産力を解放する工業製品、さらには宇宙の運行を支配する科学法則に至るまで、ほとんどあらゆる平凡なもの、あるいは驚嘆すべきものの背後には、精妙なアルゴリズムの思想が潜んでいます。 -同様に、データ構造は至る所にあります。ソーシャルネットワークから地下鉄路線まで、多くのシステムは「グラフ」としてモデル化できます。国から家族まで、社会組織の主要な形態は「木」の特徴を示します。冬服は「スタック」のようで、最初に着たものが最後に脱がれます。バドミントンのシャトル筒は「キュー」に似ており、一方の端で挿入し、もう一方の端で取り出します。辞書は「ハッシュテーブル」のようで、目標エントリを素早く検索できます。 +同様に、データ構造も至るところに存在します。社会ネットワークのような大きなものから地下鉄路線のような小さなものまで、多くのシステムは「グラフ」としてモデル化できます。国家のような大きな単位から家庭のような小さな単位まで、社会の主な組織形態には「木」の特徴があります。冬服は「スタック」のように、最初に着たものが最後に脱がれます。バドミントンシャトルの筒は「キュー」のように、一方から入れて他方から取り出します。辞書は「ハッシュテーブル」のようなもので、目的の見出し語を素早く探せます。 -この本は、明確で理解しやすいアニメーション図解と実行可能なコード例を通じて、読者がアルゴリズムとデータ構造の核心概念を理解し、プログラミングを通じてそれらを実装できるようになることを目指しています。この基盤の上で、この本は複雑な世界におけるアルゴリズムの生き生きとした現れを明らかにし、アルゴリズムの美しさを示すことに努めています。この本があなたのお役に立てることを願っています! +本書は、わかりやすいアニメーション図解と実行可能なコード例を通じて、読者がアルゴリズムとデータ構造の核心概念を理解し、さらにプログラミングによってそれらを実装できるようになることを目指しています。そのうえで、本書は複雑な世界の中にあるアルゴリズムの生き生きとした現れを明らかにし、アルゴリズムの美しさを示そうとしています。本書があなたの助けになれば幸いです。 diff --git a/ja/docs/chapter_introduction/algorithms_are_everywhere.md b/ja/docs/chapter_introduction/algorithms_are_everywhere.md index 636454fa2..09897af39 100644 --- a/ja/docs/chapter_introduction/algorithms_are_everywhere.md +++ b/ja/docs/chapter_introduction/algorithms_are_everywhere.md @@ -2,65 +2,65 @@ comments: true --- -# 1.1   アルゴリズムは至る所にある +# 1.1   アルゴリズムは至るところにある -「アルゴリズム」という言葉を聞くと、自然に数学を思い浮かべます。しかし、多くのアルゴリズムは複雑な数学を含まず、基本的な論理により多く依存しており、これは私たちの日常生活の至る所で見ることができます。 +「アルゴリズム」という言葉を聞くと、自然に数学を思い浮かべます。しかし実際には、多くのアルゴリズムは複雑な数学を必要とせず、むしろ基本的な論理に依存しており、その論理は私たちの日常生活のいたるところで見られます。 -アルゴリズムについて正式に議論を始める前に、興味深い事実を共有する価値があります。**あなたは無意識のうちに多くのアルゴリズムを学び、日常生活でそれらを応用することに慣れています**。ここで、この点を証明するためにいくつかの具体的な例を挙げます。 +アルゴリズムを本格的に議論する前に、ひとつ面白い事実を共有しておきます。**あなたはすでに知らず知らずのうちに多くのアルゴリズムを身につけ、それらを日常生活に応用することに慣れているのです**。以下では、いくつかの具体例を挙げてこれを示します。 -**例1:辞書の引き方**。英語の辞書では、単語がアルファベット順に並んでいます。$r$で始まる単語を探していると仮定すると、通常は以下の方法で行います: +**例1:辞書を引く**。辞書では、各漢字に対応するピンインがあり、辞書はピンインのアルファベット順に並んでいます。ピンインの先頭文字が $r$ の字を探すと仮定すると、通常は次の図のような方法で行います。 -1. 辞書を大体半分ぐらいのところで開き、そのページの最初の語彙を確認します。例えば$m$で始まる文字だとしましょう。 -2. $r$はアルファベットで$m$の後に来るので、前半を無視して、探索空間を後半に絞ります。 -3. $r$で始まる単語を見つけるまで、ステップ`1.`と`2.`を繰り返します。 +1. 辞書をおよそ半分のところまで開き、そのページの先頭文字を確認し、先頭文字が $m$ だとします。 +2. ピンインのアルファベット表では $r$ は $m$ の後にあるため、辞書の前半を除外し、探索範囲を後半に絞ります。 +3. ピンインの先頭文字が $r$ のページを見つけるまで、手順 `1.` と手順 `2.` を繰り返します。 === "<1>" - ![辞書を引く過程](algorithms_are_everywhere.assets/binary_search_dictionary_step1.png){ class="animation-figure" } + ![辞書を引く手順](algorithms_are_everywhere.assets/binary_search_dictionary_step1.png){ class="animation-figure" } === "<2>" - ![辞書での二分探索ステップ2](algorithms_are_everywhere.assets/binary_search_dictionary_step2.png){ class="animation-figure" } + ![binary_search_dictionary_step2](algorithms_are_everywhere.assets/binary_search_dictionary_step2.png){ class="animation-figure" } === "<3>" - ![辞書での二分探索ステップ3](algorithms_are_everywhere.assets/binary_search_dictionary_step3.png){ class="animation-figure" } + ![binary_search_dictionary_step3](algorithms_are_everywhere.assets/binary_search_dictionary_step3.png){ class="animation-figure" } === "<4>" - ![辞書での二分探索ステップ4](algorithms_are_everywhere.assets/binary_search_dictionary_step4.png){ class="animation-figure" } + ![binary_search_dictionary_step4](algorithms_are_everywhere.assets/binary_search_dictionary_step4.png){ class="animation-figure" } === "<5>" - ![辞書での二分探索ステップ5](algorithms_are_everywhere.assets/binary_search_dictionary_step5.png){ class="animation-figure" } + ![binary_search_dictionary_step5](algorithms_are_everywhere.assets/binary_search_dictionary_step5.png){ class="animation-figure" } -

図 1-1   辞書を引く過程

+

図 1-1   辞書を引く手順

-辞書を引くことは、小学生にとって必須のスキルですが、実際には有名な「二分探索」アルゴリズムです。データ構造の観点から、辞書をソートされた「配列」と考えることができます。アルゴリズムの観点から、辞書で単語を探すために取られる一連の行動は、「二分探索」アルゴリズムと見なすことができます。 +辞書を引くという小学生の必須スキルは、実は有名な「二分探索」アルゴリズムそのものです。データ構造の観点では、辞書を整列済みの「配列」とみなせます。アルゴリズムの観点では、上記の一連の辞書引きの操作を「二分探索」とみなせます。 -**例2:トランプの整理**。トランプをプレイするとき、手札を昇順に並べる必要があります。以下の過程で示されます。 +**例2:トランプを整理する**。カードゲームをするとき、毎回手札のトランプを小さい順に並べ替える必要があります。その流れは次の図のとおりです。 -1. トランプを「整列済み」と「未整列」のセクションに分けます。最初は一番左のカードが既に整列していると仮定します。 -2. 未整列セクションからカードを1枚取り出し、整列済みセクションの正しい位置に挿入します。この後、左端の2枚のカードが整列します。 -3. すべてのカードが整列するまで、ステップ`2`を繰り返します。 +1. トランプを「整列済み」と「未整列」の2つの部分に分け、初期状態では一番左の1枚がすでに整列済みだとします。 +2. 未整列部分から1枚のトランプを取り出し、整列済み部分の正しい位置に挿入します。完了すると、左端の2枚は整列済みになります。 +3. 手順 `2.` を繰り返し、各ラウンドで未整列部分から1枚を整列済み部分へ挿入し、すべてのトランプが整列済みになるまで続けます。 -![トランプの整理過程](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" } +![トランプを並べ替える手順](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" } -

図 1-2   トランプの整理過程

+

図 1-2   トランプを並べ替える手順

-上記のトランプを整理する方法は、実質的に「挿入ソート」アルゴリズムであり、小さなデータセットに対して非常に効率的です。多くのプログラミング言語のソート関数には挿入ソートが含まれています。 +上記のトランプ整理の方法は、本質的には「挿入ソート」アルゴリズムです。これは小規模なデータ集合を処理する際に非常に効率的で、多くのプログラミング言語のソートライブラリ関数にも挿入ソートが使われています。 -**例3:お釣りの計算**。スーパーマーケットで$69$の買い物をしたと仮定します。レジ係に$100$を渡すと、$31$のお釣りを提供する必要があります。この過程は以下の図で明確に理解できます。 +**例3:お釣りを出す**。スーパーで $69$ 元の商品を購入し、店員に $100$ 元渡したとすると、店員は $31$ 元のお釣りを返す必要があります。店員は自然に次の図のような考え方をします。 -1. 選択肢は$31$以下の価値のある通貨で、$1$、$5$、$10$、$20$が含まれます。 -2. 選択肢から最大の$20$を取り出し、$31 - 20 = 11$が残ります。 -3. 残りの選択肢から最大の$10$を取り出し、$11 - 10 = 1$が残ります。 -4. 残りの選択肢から最大の$1$を取り出し、$1 - 1 = 0$が残ります。 -5. お釣りの計算が完了し、解答は$20 + 10 + 1 = 31$です。 +1. 選択肢は $31$ 元より小さい額面の貨幣で、$1$ 元、$5$ 元、$10$ 元、$20$ 元があります。 +2. 選択肢の中から最大の $20$ 元を取り出すと、残りは $31 - 20 = 11$ 元です。 +3. 残りの選択肢の中から最大の $10$ 元を取り出すと、残りは $11 - 10 = 1$ 元です。 +4. 残りの選択肢の中から最大の $1$ 元を取り出すと、残りは $1 - 1 = 0$ 元です。 +5. お釣りは完了し、内訳は $20 + 10 + 1 = 31$ 元です。 -![お釣りの計算過程](algorithms_are_everywhere.assets/greedy_change.png){ class="animation-figure" } +![お釣りの過程](algorithms_are_everywhere.assets/greedy_change.png){ class="animation-figure" } -

図 1-3   お釣りの計算過程

+

図 1-3   お釣りの過程

-記述されたステップでは、利用可能な最大の額面を使用して各段階で最良の選択肢を選ぶことで、効果的なお釣り計算戦略につながります。データ構造とアルゴリズムの観点から、このアプローチは「貪欲」アルゴリズムとして知られています。 +以上の手順では、各ステップでその時点で最善と思われる選択肢を取っています。つまり、できるだけ額面の大きい貨幣を使い、最終的に実行可能なお釣りの方案を得ています。データ構造とアルゴリズムの観点から見ると、この方法は本質的に「貪欲法」です。 -料理の準備から宇宙旅行まで、ほぼすべての問題解決にはアルゴリズムが関わっています。コンピュータの出現により、メモリにデータ構造を格納し、CPUとGPUを呼び出してアルゴリズムを実行するコードを書くことができるようになりました。このようにして、現実世界の問題をコンピュータに移し、より効率的な方法でさまざまな複雑な問題を解決できます。 +料理を一品作ることから星間航行に至るまで、ほとんどあらゆる問題の解決にアルゴリズムは欠かせません。コンピュータの登場によって、プログラミングを通じてデータ構造をメモリに格納し、さらにコードを書いて CPU や GPU にアルゴリズムを実行させることが可能になりました。こうして、生活の中の問題をコンピュータに移し、より効率的な方法でさまざまな複雑な問題を解決できるのです。 !!! tip - データ構造、アルゴリズム、配列、二分探索などの概念についてまだ混乱している場合は、読み続けることをお勧めします。この本は、データ構造とアルゴリズムの理解の領域へと優しく導いてくれるでしょう。 + データ構造、アルゴリズム、配列、二分探索といった概念がまだ少し曖昧でも、そのまま読み進めてください。本書がデータ構造とアルゴリズムの知識の世界へと案内します。 diff --git a/ja/docs/chapter_introduction/index.md b/ja/docs/chapter_introduction/index.md index 25f751594..c92762750 100644 --- a/ja/docs/chapter_introduction/index.md +++ b/ja/docs/chapter_introduction/index.md @@ -3,18 +3,18 @@ comments: true icon: material/calculator-variant-outline --- -# 第 1 章   アルゴリズムとの出会い +# 第 1 章   アルゴリズム入門 -![アルゴリズムとの出会い](../assets/covers/chapter_introduction.jpg){ class="cover-image" } +![アルゴリズム入門](../assets/covers/chapter_introduction.jpg){ class="cover-image" } !!! abstract - 優雅な乙女が踊ります。データと絡み合い、アルゴリズムのメロディーに合わせてスカートをなびかせながら。 - - 彼女があなたをダンスに誘います。彼女のステップに従って、論理と美に満ちたアルゴリズムの世界に入りましょう。 + 一人の少女が軽やかに舞い、データと織り重なり合いながら、スカートの裾にはアルゴリズムの旋律がたなびいています。 + + 彼女はあなたをこの舞いへと誘います。その足取りに続いて、論理と美しさに満ちたアルゴリズムの世界へ踏み入りましょう。 ## 章の内容 -- [1.1   アルゴリズムはどこにでもある](algorithms_are_everywhere.md) -- [1.2   アルゴリズムとは何か](what_is_dsa.md) +- [1.1   アルゴリズムは至るところにある](algorithms_are_everywhere.md) +- [1.2   アルゴリズムとは](what_is_dsa.md) - [1.3   まとめ](summary.md) diff --git a/ja/docs/chapter_introduction/summary.md b/ja/docs/chapter_introduction/summary.md index 0f1c3e869..22caa5e3d 100644 --- a/ja/docs/chapter_introduction/summary.md +++ b/ja/docs/chapter_introduction/summary.md @@ -4,23 +4,25 @@ comments: true # 1.3   まとめ -- アルゴリズムは日常生活にありふれており、思っているほどアクセスしにくく複雑なものではありません。実際、私たちは既に無意識のうちに多くのアルゴリズムを学び、生活の様々な問題を解決するために使用しています。 -- 辞書で単語を引く原理は二分探索アルゴリズムと一致しています。二分探索アルゴリズムは分割統治という重要なアルゴリズム概念を体現しています。 -- トランプを整理する過程は挿入ソートアルゴリズムと非常に似ています。挿入ソートアルゴリズムは小さなデータセットのソートに適しています。 -- 通貨でお釣りを計算するステップは本質的に貪欲アルゴリズムに従っており、各ステップでその時点での最良の選択をします。 -- アルゴリズムは有限時間内で特定の問題を解決するための段階的な指示のセットですが、データ構造はコンピュータでのデータの組織化と保存方法を定義します。 -- データ構造とアルゴリズムは密接に関連しています。データ構造はアルゴリズムの基礎であり、アルゴリズムはデータ構造の機能を活用するステージです。 -- データ構造とアルゴリズムをブロックの組み立てに例えることができます。ブロックはデータを表し、ブロックの形状と接続方法はデータ構造を表し、ブロックを組み立てるステップはアルゴリズムに対応します。 +### 1.   要点の振り返り -### 1.   Q & A +- アルゴリズムは日常生活の至る所にあり、決して手の届かない難解な知識ではありません。実際、私たちは気づかないうちに多くのアルゴリズムを身につけ、生活のさまざまな問題を解決しています。 +- 辞書を引く原理は二分探索アルゴリズムと一致しています。二分探索アルゴリズムは分割統治という重要なアルゴリズム思想を体現しています。 +- トランプを整理する過程は挿入ソートアルゴリズムと非常によく似ています。挿入ソートアルゴリズムは小規模なデータ集合のソートに適しています。 +- 貨幣の釣り銭を求める手順の本質は貪欲アルゴリズムであり、各ステップでその時点で最善と思われる選択を取ります。 +- アルゴリズムとは、限られた時間内に特定の問題を解決するための一連の命令または操作手順であり、データ構造とは、コンピュータ内でデータを組織し保存する方法です。 +- データ構造とアルゴリズムは密接に結びついています。データ構造はアルゴリズムの土台であり、アルゴリズムはデータ構造に生命を吹き込みます。 +- データ構造とアルゴリズムは積み木の組み立てにたとえることができます。積み木はデータを表し、積み木の形や接続方法などはデータ構造を表し、積み木を組み立てる手順がアルゴリズムに対応します。 -**Q**:プログラマーとして、日常の仕事でアルゴリズムを手動で実装する必要があることはめったにありません。最も一般的に使用されるアルゴリズムは、既にプログラミング言語とライブラリに組み込まれており、すぐに使用できます。これは、私たちが仕事で遭遇する問題が、カスタムアルゴリズム設計を必要とする複雑さのレベルにまだ達していないことを示唆していますか? +### 2.   Q & A -特定の仕事スキルが武術の「技」のようなものだとすれば、基礎科目は「内功」のようなものです。 +**Q**:プログラマーとして、私は日常業務でアルゴリズムを使って問題を解決したことがありません。よく使うアルゴリズムはプログラミング言語にすべてカプセル化されており、そのまま使えばよいです。これは、仕事上の問題がまだアルゴリズムを必要とする段階に達していないことを意味するのでしょうか? -アルゴリズム(およびその他の基礎科目)を学ぶ意義は、必ずしも仕事でそれらを一から実装することではなく、概念の確固たる理解に基づいて、より専門的な意思決定と問題解決を可能にし、それによって仕事の全体的な質を向上させることだと私は信じています。例えば、すべてのプログラミング言語には組み込みのソート関数があります: +具体的な仕事のスキルを武術の「型」にたとえるなら、基礎科目はむしろ「内功」に近いものです。 -- データ構造とアルゴリズムを学んでいない場合、どんなデータが与えられても、このソート関数に渡すだけかもしれません。スムーズに動作し、良いパフォーマンスを示し、問題がないように見えます。 -- しかし、アルゴリズムを学んだことがあれば、組み込みのソート関数の時間複雑度は通常$O(n \log n)$であることを理解しています。さらに、データが固定桁数の整数(学生IDなど)で構成されている場合、基数ソートのようなより効率的なアプローチを適用でき、時間複雑度をO(nk)に削減できます。ここでkは桁数です。大量のデータを処理する際、節約された時間は重要な価値に変わります — コストの削減、ユーザーエクスペリエンスの向上、システムパフォーマンスの向上。 +私は、アルゴリズム(およびその他の基礎科目)を学ぶ意義は、仕事でそれをゼロから実装することではなく、学んだ知識に基づいて問題解決の際に専門的な反応や判断を下せるようになり、その結果として仕事全体の品質を高めることにあると考えています。簡単な例を挙げると、どのプログラミング言語にもソート関数が組み込まれています。 -エンジニアリングでは、多くの問題を最適に解決することは困難です。ほとんどは「準最適」解決策で対処されます。問題の難しさは、その固有の複雑さだけでなく、それに取り組む人の知識と経験にも依存します。専門知識と経験が深いほど、分析がより徹底的になり、問題をより優雅に解決できます。 +- もしデータ構造とアルゴリズムを学んでいなければ、どんなデータが与えられても、そのソート関数に任せてしまうかもしれません。問題なく動き、性能も悪くなく、一見すると特に問題はありません。 +- しかしアルゴリズムを学んでいれば、組み込みのソート関数の時間計算量が $O(n \log n)$ であることを知っています。さらに、与えられたデータが固定桁数の整数(例えば学籍番号)であれば、より効率の高い「基数ソート」を使って、時間計算量を $O(nk)$ に下げることができます。ここで $k$ は桁数です。データ量が非常に大きい場合、節約できた実行時間は大きな価値を生みます(コスト削減、体験向上など)。 + +工学分野では、多くの問題で最適解に到達することは難しく、少なくない問題は「だいたい」解決されているにすぎません。問題の難しさは、一方では問題そのものの性質に依存し、他方ではそれを観測する人の知識の蓄積にも依存します。知識が充実し、経験が豊富であるほど、問題分析はより深くなり、問題はより洗練された形で解決できるようになります。 diff --git a/ja/docs/chapter_introduction/what_is_dsa.md b/ja/docs/chapter_introduction/what_is_dsa.md index c56152eda..f97730252 100644 --- a/ja/docs/chapter_introduction/what_is_dsa.md +++ b/ja/docs/chapter_introduction/what_is_dsa.md @@ -2,64 +2,64 @@ comments: true --- -# 1.2   アルゴリズムとは何か +# 1.2   アルゴリズムとは ## 1.2.1   アルゴリズムの定義 -アルゴリズムは、有限時間内で特定の問題を解決するための一連の指示またはステップです。以下の特徴があります: +アルゴリズム(algorithm)とは、限られた時間内に特定の問題を解決するための一連の命令または操作手順であり、次のような特徴を持ちます。 -- 問題が明確に定義されており、入力と出力の明確な定義が含まれています。 -- アルゴリズムは実行可能で、有限の回数のステップ、時間、メモリ空間内で完了できることを意味します。 -- 各ステップには明確な意味があります。同じ入力と条件の下で出力は一貫して同じです。 +- 問題が明確であり、入力と出力の定義がはっきりしています。 +- 実行可能であり、有限の手順、時間、メモリ空間で完了できます。 +- 各手順の意味が確定しており、同じ入力と実行条件では常に同じ出力になります。 ## 1.2.2   データ構造の定義 -データ構造は、コンピュータ内でデータを組織し保存する方法で、以下の設計目標があります: +データ構造(data structure)とは、データを整理して保存する方式であり、データの内容、データ間の関係、データの操作方法を含み、次のような設計目標があります。 -- コンピュータのメモリを節約するために空間占有を最小化する。 -- データ操作を可能な限り高速にし、データのアクセス、追加、削除、更新などをカバーする。 -- 効率的なアルゴリズム実行を可能にするために、簡潔なデータ表現と論理情報を提供する。 +- 使用する空間をできるだけ少なくし、コンピュータのメモリを節約します。 +- データの操作をできるだけ高速にし、アクセス、追加、削除、更新などを含みます。 +- 簡潔なデータ表現と論理情報を提供し、アルゴリズムが効率よく動作できるようにします。 -**データ構造の設計はバランスを取る行為であり、しばしばトレードオフが必要です**。一つの側面を改善したい場合、しばしば別の側面で妥協する必要があります。以下は2つの例です: +**データ構造の設計はトレードオフに満ちた過程です**。ある面を改善したい場合、別の面で妥協が必要になることがよくあります。以下に 2 つの例を示します。 -- 配列と比較して、連結リストはデータの追加と削除においてより便利ですが、データアクセス速度を犠牲にします。 -- 連結リストと比較して、グラフはより豊富な論理情報を提供しますが、より多くのメモリ空間が必要です。 +- 連結リストは配列に比べてデータの追加や削除がしやすい一方で、データアクセス速度を犠牲にしています。 +- グラフは連結リストに比べてより豊富な論理情報を提供しますが、より大きなメモリ空間を必要とします。 ## 1.2.3   データ構造とアルゴリズムの関係 -以下の図に示すように、データ構造とアルゴリズムは高度に関連し、密接に統合されており、具体的には以下の3つの側面があります: +以下の図のように、データ構造とアルゴリズムは高度に関連し、密接に結び付いており、具体的には次の 3 つの点に表れます。 -- データ構造はアルゴリズムの基礎です。構造化されたデータ保存とアルゴリズムのためのデータ操作方法を提供します。 -- アルゴリズムはデータ構造に活力を注入します。データ構造だけではデータ情報を保存するだけです。アルゴリズムの応用によって、特定の問題を解決できます。 -- アルゴリズムは異なるデータ構造に基づいて実装できることが多いですが、実行効率は大きく異なることがあります。適切なデータ構造を選択することが鍵です。 +- データ構造はアルゴリズムの土台です。データ構造はアルゴリズムに対して、構造化して格納されたデータと、そのデータを操作する方法を提供します。 +- アルゴリズムはデータ構造に命を吹き込みます。データ構造そのものはデータ情報を保存するだけであり、アルゴリズムと組み合わせて初めて特定の問題を解決できます。 +- アルゴリズムは通常、異なるデータ構造に基づいて実装できますが、実行効率が大きく異なる場合があり、適切なデータ構造を選ぶことが重要です。 ![データ構造とアルゴリズムの関係](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png){ class="animation-figure" }

図 1-4   データ構造とアルゴリズムの関係

-データ構造とアルゴリズムは、以下の図に示すように、ブロックのセットに例えることができます。ブロックセットには多数のピースが含まれ、詳細な組み立て説明書が付いています。これらの説明書に段階的に従うことで、複雑なブロックモデルを構築できます。 +データ構造とアルゴリズムは、以下の図に示す組み立てブロックのようなものです。1 セットのブロックには多くの部品が含まれるだけでなく、詳しい組み立て説明書も付いています。説明書に従って一歩ずつ操作すれば、精巧なブロック模型を組み立てられます。 -![ブロックの組み立て](what_is_dsa.assets/assembling_blocks.png){ class="animation-figure" } +![組み立てブロック](what_is_dsa.assets/assembling_blocks.png){ class="animation-figure" } -

図 1-5   ブロックの組み立て

+

図 1-5   組み立てブロック

-両者の詳細な対応関係は以下の表に示されています。 +両者の詳細な対応関係を次の表に示します。 -

表 1-1   データ構造とアルゴリズムをブロックと比較

+

表 1-1   データ構造とアルゴリズムを組み立てブロックにたとえる

-| データ構造とアルゴリズム | ブロック | -| ------------------------ | ----------------------------------------------------- | -| 入力データ | 未組み立てのブロック | -| データ構造 | ブロックの組織、形状、サイズ、接続などを含む | -| アルゴリズム | ブロックを望ましい形状に組み立てる一連のステップ | -| 出力データ | 完成したブロックモデル | +| データ構造とアルゴリズム | 組み立てブロック | +| -------------- | ---------------------------------------- | +| 入力データ | まだ組み立てていないブロック | +| データ構造 | ブロックの構成形式。形状、大きさ、接続方法などを含む | +| アルゴリズム | ブロックを目標の形に組み上げる一連の操作手順 | +| 出力データ | ブロック模型 |
-データ構造とアルゴリズムはプログラミング言語から独立していることは注目に値します。この理由により、この本は複数のプログラミング言語での実装を提供できます。 +特筆すべき点として、データ構造とアルゴリズムはプログラミング言語から独立しています。だからこそ、本書では複数のプログラミング言語に基づく実装を提供できます。 -!!! tip "慣習的な略語" +!!! tip "慣習的な略称" - 実生活の議論では、「データ構造とアルゴリズム」を単純に「アルゴリズム」と呼ぶことがよくあります。例えば、よく知られたLeetCodeアルゴリズム問題は、実際にはデータ構造とアルゴリズムの両方の知識をテストしています。 + 実際の議論では、私たちは通常「データ構造とアルゴリズム」を略して「アルゴリズム」と呼びます。たとえば広く知られている LeetCode のアルゴリズム問題は、実際にはデータ構造とアルゴリズムの両方の知識を同時に問うています。 diff --git a/ja/docs/chapter_paperbook/index.md b/ja/docs/chapter_paperbook/index.md new file mode 100644 index 000000000..efc58cbde --- /dev/null +++ b/ja/docs/chapter_paperbook/index.md @@ -0,0 +1,68 @@ +--- +comments: true +icon: material/book-open-page-variant +status: new +--- + +# 紙の書籍 + +長い時間をかけて磨き上げた『Hello アルゴリズム』の紙の書籍が、ついに発売されました!今の気持ちは、次の一節で表せます: + +

風を追い月を追って立ち止まるな、草原の果てには春の山がある。

+ +![](index.assets/paper_book_overview.jpg){ class="animation-figure" } + +以下の動画では紙の書籍を紹介しており、私の考えもいくつか含まれています: + +- データ構造とアルゴリズムを学ぶ重要性。 +- なぜ紙の書籍で Python を選んだのか。 +- 知識共有に対する理解。 + +> 新人 UP 主ですので、ぜひ応援と高評価・チャンネル登録をお願いします~ありがとうございます! + +
+ +
+ +紙の書籍のスナップショット: + +![](index.assets/paper_book_chapter_heap.jpg){ class="animation-figure" } + +![](index.assets/paper_book_avl_tree.jpg){ class="animation-figure" } + +## 長所と短所 + +紙の書籍ならではの魅力を、簡単にまとめると次のとおりです: + +- フルカラー印刷を採用し、本書の「アニメーション図解」の強みをそのまま活かせます。 +- 紙の素材にもこだわり、色彩を高い精度で再現しつつ、紙の書籍ならではの質感も残しています。 +- 紙の書籍版は Web 版よりも書式が整っており、たとえば図中の数式には斜体を用いています。 +- 価格を上げずに、マインドマップの折り込みページやしおりも付属します。 +- 紙の書籍、Web 版、PDF 版で内容は同期しており、自由に切り替えて読めます。 + +!!! tip + + 紙の書籍と Web 版を同期させるのは難しいため、細かな違いが生じる場合があります。ご了承ください! + +もちろん、購入前に検討しておくべき点もいくつかあります: + +- Python 言語を使用しているため、あなたの主言語と合わない可能性があります(Python は疑似コードと捉え、考え方の理解を重視してください)。 +- フルカラー印刷は図解やコードの読みやすさを大きく高める一方で、白黒印刷より価格はやや高くなります。 + +!!! tip + + 「印刷品質」と「価格」は、アルゴリズムにおける「時間効率」と「空間効率」のようなもので、両立は容易ではありません。そして私は、「印刷品質」は「時間効率」に当たるため、より重視すべきだと考えています。 + +## 購入リンク + +紙の書籍に興味があれば、ぜひ一冊ご検討ください。新刊の 5 割引を用意していただきましたので、[こちらのリンク](https://3.cn/1X-qmTD3)をご覧いただくか、以下の QR コードをスキャンしてください: + +![](index.assets/book_jd_link.jpg){ class="animation-figure" } + +## あとがき + +当初、私は紙の書籍出版に必要な作業量を甘く見ていて、オープンソースプロジェクトをきちんと保守していれば、紙の書籍版も何らかの自動化手段で生成できると思っていました。実際には、紙の書籍の制作フローとオープンソースプロジェクトの更新の仕組みには大きな違いがあり、その間をつなぐには多くの追加作業が必要でした。 + +一冊の本の初稿と出版基準を満たす完成稿との間には、なお大きな隔たりがあります。出版社(企画、編集、デザイン、マーケティングなど)と著者が力を合わせ、長い時間をかけて磨き上げていく必要があります。ここで、図霊の企画編集者である王軍花さん、そして人民郵電出版社と図霊コミュニティで本書の出版工程に携わってくださったすべての皆さまに感謝いたします! + +この本があなたの助けになれば幸いです! diff --git a/ja/docs/chapter_preface/about_the_book.md b/ja/docs/chapter_preface/about_the_book.md index f572b7924..f929e0427 100644 --- a/ja/docs/chapter_preface/about_the_book.md +++ b/ja/docs/chapter_preface/about_the_book.md @@ -2,33 +2,33 @@ comments: true --- -# 0.1   この本について +# 0.1   本書について -このオープンソースプロジェクトは、データ構造とアルゴリズムに関する無料で初心者にやさしいクラッシュコースの作成を目指しています。 +本プロジェクトは、オープンソースで無料、かつ初心者にやさしいデータ構造とアルゴリズムの入門書を作ることを目的としています。 -- アニメーション付きの図解、理解しやすい内容、滑らかな学習曲線により、初心者がデータ構造とアルゴリズムの「知識マップ」を探索するのに役立ちます。 -- ワンクリックでコードを実行できるため、読者のプログラミングスキルの向上と、アルゴリズムの動作原理およびデータ構造の基礎実装の理解に役立ちます。 -- 教えることによる学習を促進し、質問や洞察の共有を自由に行ってください。議論を通じて一緒に成長しましょう。 +- 全編でアニメーション付きの図解を採用し、内容は明快で理解しやすく、学習曲線もなだらかで、初心者がデータ構造とアルゴリズムの知識地図を探求できるよう導きます。 +- ソースコードはワンクリックで実行でき、読者が演習を通じてプログラミング能力を高め、アルゴリズムの動作原理とデータ構造の内部実装を理解する助けとなります。 +- 読者どうしの助け合いによる学習を推奨しており、コメント欄で質問や見解を共有し、対話と議論を通じてともに成長していくことを歓迎します。 ## 0.1.1   対象読者 -もしあなたがアルゴリズムに触れたばかりで経験が限られている場合、またはアルゴリズムである程度の経験を積んでいても、データ構造とアルゴリズムについて曖昧な理解しかなく、常に「分かった」と「うーん」の間を行き来している場合、この本はあなたのためのものです! +もしあなたがアルゴリズム初心者で、これまでアルゴリズムに触れたことがない、あるいはすでに多少の問題演習の経験はあるものの、データ構造とアルゴリズムについてはまだ曖昧な理解にとどまり、できるかできないかの間を行き来しているなら、本書はまさにあなたのために作られています! -すでにある程度の問題解決経験を積んでおり、ほとんどのタイプの問題に精通している場合、この本はアルゴリズムの知識体系を復習し整理するのに役立ちます。リポジトリのソースコードは「問題解決ツールキット」や「アルゴリズムチートシート」として使用できます。 +もしすでに一定量の問題演習を積み、ほとんどの問題パターンに慣れているなら、本書はアルゴリズム知識体系の復習と整理に役立ちます。リポジトリのソースコードは「問題演習ツール集」や「アルゴリズム辞典」として活用できます。 -もしあなたがアルゴリズムの専門家であれば、貴重な提案をいただくか、[参加して協力](https://www.hello-algo.com/chapter_appendix/contribution/)していただければと思います。 +もしあなたがアルゴリズムの「達人」なら、貴重なご提案をいただけることを楽しみにしています。あるいは[一緒に執筆に参加](https://www.hello-algo.com/chapter_appendix/contribution/)してください。 !!! success "前提条件" - 少なくとも一つのプログラミング言語で簡単なコードを書いて読むことができる必要があります。 + 少なくともいずれか一つの言語でのプログラミング基礎があり、簡単なコードを読んだり書いたりできる必要があります。 ## 0.1.2   内容構成 -本書の主な内容を下図に示します。 +本書の主な内容は以下の図のとおりです。 -- **計算量解析**: データ構造とアルゴリズムを評価する側面と方法を探求します。時間計算量と空間計算量を導出する方法、および一般的なタイプと例を扱います。 -- **データ構造**: 基本的なデータ型、分類方法、定義、長所と短所、一般的な操作、タイプ、応用、および配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなどのデータ構造の実装方法に焦点を当てます。 -- **アルゴリズム**: アルゴリズムを定義し、その長所と短所、効率性、応用シナリオ、問題解決ステップについて議論し、検索、ソート、分割統治、バックトラッキング、動的プログラミング、貪欲アルゴリズムなど、さまざまなアルゴリズムのサンプル問題を含みます。 +- **計算量解析**:データ構造とアルゴリズムを評価する観点と方法。時間計算量と空間計算量の求め方、代表的な種類、例など。 +- **データ構造**:基本データ型とデータ構造の分類方法。配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなどのデータ構造の定義、長所と短所、基本操作、代表的な種類、典型的な応用、実装方法など。 +- **アルゴリズム**:探索、ソート、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムの定義、長所と短所、効率、適用場面、問題を解く手順、例題など。 ![本書の主な内容](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" } @@ -36,23 +36,25 @@ comments: true ## 0.1.3   謝辞 -本書は、オープンソースコミュニティの多数の貢献者による共同の努力のもと、継続的に改善されています。時間と労力を注いで執筆に携わってくださったすべての方々に感謝します。貢献者は以下のとおりです(GitHub により自動生成された順序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、IsChristina、xBLACKICEx、guowei-gong、Cathay-Chen、pengchzn、QiLOL、magentaqin、hello-ikun、JoseHung、qualifier1024、thomasq0、sunshinesDL、L-Super、Guanngxu、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、Shyam-Chen、theNefelibatas、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、MolDuM、Nigh、Dr-XYZ、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、beatrix-chan、DullSword、xjr7670、jiaxianhua、qq909244296、iStig、boloboloda、hts0000、gledfish、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、llql1211、fbigm、echo1937、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、GaochaoZhu、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Allen-Scai、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、zhongfq、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Keynman、KeiichiKasai、KawaiiAsh。 +本書は、オープンソースコミュニティの多くの貢献者による共同の努力のもとで、継続的に改善されています。時間と労力を注いでくださったすべての執筆者の皆さんに感謝します。お名前は次のとおりです(GitHub により自動生成された順序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、pengchzn、QiLOL、Cathay-Chen、guowei-gong、xBLACKICEx、IsChristina、JoseHung、qualifier1024、hello-ikun、magentaqin、Guanngxu、thomasq0、sunshinesDL、L-Super、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、theNefelibatas、Shyam-Chen、sangxiaai、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、Nigh、Dr-XYZ、MolDuM、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、xjr7670、beatrix-chan、DullSword、qq909244296、iStig、boloboloda、hts0000、gledfish、fbigm、echo1937、jiaxianhua、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、JTCPOWI、KawaiiAsh、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、llql1211、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Senrian、Allen-Scai、19santosh99、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、codetypess、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Kunchen-Luo、Keynman と KeiichiKasai。 -この本のコードレビュー作業は、coderonion, Gonglja, gvenusleo, hpstory, justin‐tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi(アルファベット順)によって完了されました。彼らの時間と努力に感謝し、様々な言語でのコードの標準化と統一性を確保してくださいました。 +本書のコードレビューは coderonion、curtishd、Gonglja、gvenusleo、hpstory、justin-tse、khoaxuantu、krahets、night-cruise、nuomi1、Reanon と rongyi によって行われました(アルファベット順)。彼らが費やしてくれた時間と労力に感謝します。各言語のコードの規範性と統一性が保たれているのは、まさに彼らのおかげです。 -本書の繁体字中国語版は Shyam-Chen および Dr-XYZ によってレビューされ、英語版は yuelinxin、K3v123、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn、thomasq0、magentaqin によってレビューされ、日本語版は eltociear によってレビューされました。彼らの継続的な貢献のおかげで、本書はより幅広い読者層に提供することができています。ここに深く感謝いたします。 +本書の英語版は yuelinxin、K3v123、magentaqin、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn と thomasq0 がレビューし、日本語版は eltociear がレビューし、ロシア語版は И. А. Шевкун と Yuyan Huang がレビューし、繁体字中国語版は Shyam-Chen と Dr-XYZ がレビューしました。彼らの貢献があってこそ、本書はより幅広い読者に届けられています。感謝いたします。 -この本の制作過程において、多くの方々から貴重な支援をいただきました。これらに限定されませんが: +本書の ePub 電子書籍生成ツールは zhongfq によって開発されました。彼の貢献に感謝します。読者により柔軟な読書方法を提供してくれました。 -- 会社でのメンター、李熙博士に感謝します。ある会話で「早く行動しろ」と励ましていただき、この本を書く決意を固めることができました。 -- ガールフレンドのBubbleに感謝します。この本の最初の読者として、アルゴリズム初心者の視点から多くの貴重な提案をいただき、この本を初心者により適したものにしてくださいました。 -- Tengbao、Qibao、Feibaoに感謝します。この本のクリエイティブな名前を考えてくださり、みんなが初めて「Hello World!」を書いた時の素晴らしい思い出を呼び起こしてくれました。 -- Xiaoquanに感謝します。知的財産に関する専門的な支援を提供してくださり、このオープンソース本の開発において重要な役割を果たしてくださいました。 -- Sutongに感謝します。この本の美しいカバーとロゴをデザインしてくださり、私の要求で何度も修正を辛抱強く行ってくださいました。 -- @squidfunk に感謝します。執筆と組版の提案、および彼が開発したオープンソースドキュメントテーマ [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) を提供してくださいました。 +本書の執筆過程で、私は多くの方々の助けを得ました。 -執筆の過程で、データ構造とアルゴリズムに関する多数の教科書や記事を深く研究しました。これらの作品は模範的なモデルとして機能し、この本の内容の正確性と品質を確保してくださいました。先人の方々の貴重な貢献に感謝いたします! +- 会社での私の指導教員である李汐博士に感謝します。ある対話の中で「すぐに行動しよう」と励ましてくださり、この本を書く決意を固めることができました; +- 私の恋人であり、本書の最初の読者でもある泡泡に感謝します。アルゴリズム初心者の視点から多くの貴重な提案をしてくれたおかげで、本書はより初心者に適したものになりました; +- 腾宝、琦宝、飞宝が本書に創造性あふれる名前を付けてくれたことに感謝します。みんなが最初のコード行「Hello World!」を書いた美しい記憶を呼び起こしてくれました; +- 校铨が知的財産の面で専門的な支援をしてくれたことに感謝します。これは本オープンソース書籍の改善に重要な役割を果たしました; +- 苏潼が本書の美しい表紙と logo をデザインし、私の完璧主義につき合って何度も辛抱強く修正してくれたことに感謝します; +- @squidfunk が組版に関する助言を提供してくれたこと、そして彼が開発したオープンソースのドキュメントテーマ [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material) に感謝します。 -この本は、理論と実践を組み合わせた学習を提唱しており、この点で ["Dive into Deep Learning"](https://github.com/d2l-ai/d2l-en) からインスピレーションを受けています。この優れた本をすべての読者に強くお勧めします。 +執筆の過程で、私はデータ構造とアルゴリズムに関する多くの教材や記事を読みました。これらの作品は本書に優れた手本を与え、本書の内容の正確性と品質を支えてくれました。ここに、すべての先生方と先人たちの卓越した貢献に感謝します! -**継続的な支援と励ましにより、この興味深い仕事をすることを可能にしてくださった両親に心から感謝いたします**。 +本書は手と頭を同時に使う学習方法を提唱しています。この点で私は[『手を動かして学ぶ深層学習』](https://github.com/d2l-ai/d2l-zh)から大きな啓発を受けました。ここで読者の皆さんにこの優れた著作を強くお勧めします。 + +**心から両親に感謝します。いつも支え励ましてくれたからこそ、私はこの興味深いことに取り組む機会を得ることができました**。 diff --git a/ja/docs/chapter_preface/index.md b/ja/docs/chapter_preface/index.md index b9ecef68b..d7292f34c 100644 --- a/ja/docs/chapter_preface/index.md +++ b/ja/docs/chapter_preface/index.md @@ -3,15 +3,15 @@ comments: true icon: material/book-open-outline --- -# 第 0 章   序文 +# 第 0 章   はじめに -![序文](../assets/covers/chapter_preface.jpg){ class="cover-image" } +![はじめに](../assets/covers/chapter_preface.jpg){ class="cover-image" } !!! abstract - アルゴリズムは美しい交響曲のようで、コードの一行一行がリズムのように流れています。 - - この本があなたの心の中で静かに響き、独特で深い旋律を残すことを願っています。 + アルゴリズムは美しい交響曲のようであり、コードの一行一行が旋律のように流れていきます。 + + この本があなたの心の中でそっと響き、独自で深い旋律を残してくれることを願っています。 ## 章の内容 diff --git a/ja/docs/chapter_preface/suggestions.md b/ja/docs/chapter_preface/suggestions.md index c5c10aab0..7deb2d78b 100644 --- a/ja/docs/chapter_preface/suggestions.md +++ b/ja/docs/chapter_preface/suggestions.md @@ -2,28 +2,28 @@ comments: true --- -# 0.2   読み方 +# 0.2   本書の使い方 !!! tip - 最良の読書体験のために、このセクションを通読することをお勧めします。 + 最適な読書体験を得るために、本節の内容を一通り読むことをおすすめします。 -## 0.2.1   記述規則 +## 0.2.1   文章スタイルの約束 -- タイトルの後に「*」が付いた章は任意であり、比較的難易度の高い内容が含まれています。時間に制約がある場合は、これらをスキップすることをお勧めします。 -- 技術用語は太字(印刷版およびPDF版)または下線(Web版)で表示されます。例えば、配列などです。技術文書をより良く理解するために、これらに慣れることをお勧めします。 -- **太字のテキスト**は重要な内容や要約文を示し、特別な注意を払う価値があります。 -- 特定の意味を持つ単語や句は「引用符」で示され、曖昧さを避けます。 -- プログラミング言語間で一致しない用語については、この本はPythonに従います。例えば、`null`を意味するために`None`を使用します。 -- この本は、よりコンパクトなコンテンツレイアウトと引き換えに、プログラミング言語のコメント規約を部分的に無視しています。コメントは主に3つのタイプで構成されています:タイトルコメント、内容コメント、複数行コメント。 +- 見出しの後に `*` が付いているのは選読章で、内容は比較的難しめです。時間が限られている場合は、先に読み飛ばしてもかまいません。 +- 専門用語は太字(紙書籍版と PDF 版)または下線付き(Web 版)で示します。たとえば配列(array)のようなものです。文献を読む際に役立つため、覚えておくことをおすすめします。 +- 重要な内容やまとめの文は **太字** で示します。これらの文章には特に注意してください。 +- 特定の意味を持つ語句には“引用符”を付け、曖昧さを避けます。 +- プログラミング言語ごとに用語が一致しない場合、本書では Python を基準とします。たとえば、“空”を表すのに `None` を使います。 +- 本書では、よりコンパクトなレイアウトのために、言語ごとのコメント規約を一部省略しています。コメントは主に3種類あります。タイトルコメント、内容コメント、複数行コメントです。 === "Python" ```python title="" - """関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント""" - - # 詳細を説明するためのコメント - + """タイトルコメント。関数、クラス、テストケースなどを示すために使います""" + + # 内容コメント。コードを詳しく説明するために使います + """ 複数行 コメント @@ -33,10 +33,10 @@ comments: true === "C++" ```cpp title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ - - // 詳細を説明するためのコメント - + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ + + // 内容コメント。コードを詳しく説明するために使います + /** * 複数行 * コメント @@ -46,10 +46,10 @@ comments: true === "Java" ```java title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ - - // 詳細を説明するためのコメント - + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ + + // 内容コメント。コードを詳しく説明するために使います + /** * 複数行 * コメント @@ -59,10 +59,10 @@ comments: true === "C#" ```csharp title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ - - // 詳細を説明するためのコメント - + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ + + // 内容コメント。コードを詳しく説明するために使います + /** * 複数行 * コメント @@ -72,10 +72,10 @@ comments: true === "Go" ```go title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ - - // 詳細を説明するためのコメント - + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ + + // 内容コメント。コードを詳しく説明するために使います + /** * 複数行 * コメント @@ -85,10 +85,10 @@ comments: true === "Swift" ```swift title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ - - // 詳細を説明するためのコメント - + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ + + // 内容コメント。コードを詳しく説明するために使います + /** * 複数行 * コメント @@ -98,10 +98,10 @@ comments: true === "JS" ```javascript title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ - - // 詳細を説明するためのコメント - + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ + + // 内容コメント。コードを詳しく説明するために使います + /** * 複数行 * コメント @@ -111,10 +111,10 @@ comments: true === "TS" ```typescript title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ - - // 詳細を説明するためのコメント - + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ + + // 内容コメント。コードを詳しく説明するために使います + /** * 複数行 * コメント @@ -124,10 +124,10 @@ comments: true === "Dart" ```dart title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ - - // 詳細を説明するためのコメント - + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ + + // 内容コメント。コードを詳しく説明するために使います + /** * 複数行 * コメント @@ -137,23 +137,21 @@ comments: true === "Rust" ```rust title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ - // 詳細を説明するためのコメント - - /** - * 複数行 - * コメント - */ + // 内容コメント。コードを詳しく説明するために使います + + // 複数行 + // コメント ``` === "C" ```c title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ - - // 詳細を説明するためのコメント - + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ + + // 内容コメント。コードを詳しく説明するために使います + /** * 複数行 * コメント @@ -163,82 +161,97 @@ comments: true === "Kotlin" ```kotlin title="" - /* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */ - - // 詳細を説明するためのコメント - + /* タイトルコメント。関数、クラス、テストケースなどを示すために使います */ + + // 内容コメント。コードを詳しく説明するために使います + /** * 複数行 * コメント */ ``` -## 0.2.2   アニメーション図解による効率的学習 +=== "Ruby" -テキストと比較して、動画や画像は情報密度が高く、より構造化されており、理解しやすくなっています。この本では、**重要で難しい概念は主にアニメーションと図解を通じて提示され**、テキストは説明と補足として機能します。 + ```ruby title="" + ### タイトルコメント。関数、クラス、テストケースなどを示すために使います ### -下図に示すようなアニメーションや図解のある内容に遭遇した場合、**図の理解を優先し、テキストを補足として**、両方を統合して包括的な理解を得てください。 + # 内容コメント。コードを詳しく説明するために使います + + # 複数行 + # コメント + ``` + +## 0.2.2   アニメーション図解で効率よく学ぶ + +文字と比べて、動画や画像は情報密度と構造化の度合いが高く、理解しやすいものです。本書では、**重要かつ難解な知識は主にアニメーションによる図解で示し**、文章は説明と補足を担います。 + +本書を読んでいて、ある内容に以下の図のようなアニメーション図解がある場合は、**図を主、文章を従として**、両方を合わせて理解してください。 ![アニメーション図解の例](../index.assets/animation.gif){ class="animation-figure" }

図 0-2   アニメーション図解の例

-## 0.2.3   コーディング実践による理解の深化 +## 0.2.3   コード実践で理解を深める -この本のソースコードは[GitHubリポジトリ](https://github.com/krahets/hello-algo)でホストされています。下図に示すように、**ソースコードにはテスト例が付属しており、ワンクリックで実行できます**。 +本書のサンプルコードは [GitHub リポジトリ](https://github.com/krahets/hello-algo) で管理されています。以下の図のように、**ソースコードにはテストケースが付いており、ワンクリックで実行できます**。 -時間に余裕がある場合は、**自分でコードをタイプすることをお勧めします**。時間がない場合は、少なくともすべてのコードを読んで実行してください。 +時間に余裕があれば、**コードを見ながら自分で一度書いてみることをおすすめします**。学習時間が限られている場合でも、少なくともすべてのコードに目を通し、実行してください。 -コードを読むだけと比較して、コードを書くことは多くの場合、より多くの学習をもたらします。**実践による学習こそが真の学習方法です。** +コードを読むのに比べて、書く過程のほうが得られるものは多いものです。**手を動かしてこそ、本当に学んだことになります**。 ![コード実行例](../index.assets/running_code.gif){ class="animation-figure" }

図 0-3   コード実行例

-コードを実行するための設定には、主に3つのステップが含まれます。 +コードを実行する前準備は主に3ステップです。 -**ステップ1:ローカルプログラミング環境をインストール**。付録の[チュートリアル](https://www.hello-algo.com/chapter_appendix/installation/)に従ってインストールするか、すでにインストールされている場合はこのステップをスキップしてください。 +**第1ステップ:ローカルのプログラミング環境をインストールする**。付録の[チュートリアル](https://www.hello-algo.com/chapter_appendix/installation/)を参照してインストールしてください。すでにインストール済みであれば、この手順は省略できます。 -**ステップ2:コードリポジトリをクローンまたはダウンロード**。[GitHubリポジトリ](https://github.com/krahets/hello-algo)を訪問してください。 - -[Git](https://git-scm.com/downloads)がインストールされている場合は、次のコマンドを使用してリポジトリをクローンします: +**第2ステップ:コードリポジトリをクローンまたはダウンロードする**。 [GitHub リポジトリ](https://github.com/krahets/hello-algo) にアクセスしてください。すでに [Git](https://git-scm.com/downloads) をインストールしている場合は、次のコマンドでこのリポジトリをクローンできます: ```shell git clone https://github.com/krahets/hello-algo.git ``` -または、下図に示す場所にある「Download ZIP」ボタンをクリックして、コードを圧縮ZIPファイルとして直接ダウンロードすることもできます。その後、ローカルで展開するだけです。 +もちろん、以下の図に示す場所で“Download ZIP”ボタンをクリックし、コードの圧縮ファイルを直接ダウンロードしてローカルで展開することもできます。 ![リポジトリのクローンとコードのダウンロード](suggestions.assets/download_code.png){ class="animation-figure" }

図 0-4   リポジトリのクローンとコードのダウンロード

-**ステップ3:ソースコードを実行**。下図に示すように、上部にファイル名が記載されたコードブロックについては、リポジトリの`codes`フォルダで対応するソースコードファイルを見つけることができます。これらのファイルはワンクリックで実行でき、不要なデバッグ時間を節約し、学習に集中できます。 +**第3ステップ:ソースコードを実行する**。以下の図のように、上部にファイル名が表示されているコードブロックについては、リポジトリの `codes` フォルダ内に対応するソースコードファイルがあります。ソースコードファイルはワンクリックで実行できるため、不要なデバッグ時間を減らし、学習内容に集中できます。 ![コードブロックと対応するソースコードファイル](suggestions.assets/code_md_to_repo.png){ class="animation-figure" }

図 0-5   コードブロックと対応するソースコードファイル

-## 0.2.4   議論による共同学習 +ローカルでコードを実行するだけでなく、**Web 版では Python コードの可視化実行にも対応しています**([pythontutor](https://pythontutor.com/) を利用)。以下の図のように、コードブロックの下にある“可視化実行”をクリックすると表示を展開し、アルゴリズムコードの実行過程を観察できます。また、“全画面表示”をクリックすると、より見やすい閲覧体験が得られます。 -この本を読んでいる間、学べなかった点を飛ばさないでください。**コメントセクションで気軽に質問してください**。喜んでお答えし、通常2日以内に回答できます。 +![Python コードの可視化実行](suggestions.assets/pythontutor_example.png){ class="animation-figure" } -下図に示すように、各章の下部にコメントセクションがあります。これらのコメントに注意を払うことをお勧めします。他の人が遭遇した問題を知ることで、知識のギャップを特定し、より深い思索を促すだけでなく、仲間の読者の質問に答えたり、洞察を共有したり、相互の向上を促進したりすることで寛大に貢献することも招待します。 +

図 0-6   Python コードの可視化実行

-![コメントセクションの例](../index.assets/comment.gif){ class="animation-figure" } +## 0.2.4   質問と議論を通じてともに成長する -

図 0-6   コメントセクションの例

+本書を読んでいて、理解できていない知識点を安易に読み飛ばさないでください。**コメント欄で気軽に質問してください**。私と仲間たちが誠意をもって回答し、通常は 2 日以内に返信します。 -## 0.2.5   アルゴリズム学習パス +以下の図のように、Web 版では各章の下部にコメント欄があります。ぜひコメント欄の内容にも目を通してください。一方では、みんなが直面した問題を知ることで知識の抜けを補い、より深い思考を促せます。もう一方では、ほかの仲間の質問にも積極的に答え、見解を共有し、互いの成長を助けてほしいと思います。 -全体的に、データ構造とアルゴリズムをマスターする旅は3つの段階に分けることができます: +![コメント欄の例](../index.assets/comment.gif){ class="animation-figure" } -1. **段階1:アルゴリズムの入門**。さまざまなデータ構造の特性と使用法に慣れ、異なるアルゴリズムの原理、プロセス、用途、効率について学ぶ必要があります。 -2. **段階2:アルゴリズム問題の練習**。[Sword for Offer](https://leetcode.cn/studyplan/coding-interviews/)や[LeetCode Hot 100](https://leetcode.cn/studyplan/top-100-liked/)などの人気のある問題から始めることをお勧めし、少なくとも100問を蓄積して主流のアルゴリズム問題に慣れることです。練習を始めると忘却が課題になる可能性がありますが、これは正常なことですのでご安心ください。「エビングハウスの忘却曲線」に従って問題を復習することができ、通常3〜5回の反復の後、それらを覚えることができるでしょう。 -3. **段階3:知識体系の構築**。学習の面では、アルゴリズムコラム記事、解法フレームワーク、アルゴリズム教科書を読んで知識体系を継続的に豊かにすることができます。練習の面では、トピック別分類、一つの問題に対する複数の解法、複数の問題に対する一つの解法など、高度な戦略を試すことができます。これらの戦略に関する洞察は、さまざまなコミュニティで見つけることができます。 +

図 0-7   コメント欄の例

-下図に示すように、この本は主に「段階1」をカバーしており、段階2と3により効率的に取り組むのに役立つことを目的としています。 +## 0.2.5   アルゴリズム学習ロードマップ -![アルゴリズム学習パス](suggestions.assets/learning_route.png){ class="animation-figure" } +全体として見ると、データ構造とアルゴリズムの学習過程は 3 つの段階に分けられます。 -

図 0-7   アルゴリズム学習パス

+1. **第 1 段階:アルゴリズム入門**。さまざまなデータ構造の特徴と使い方に慣れ、異なるアルゴリズムの原理、流れ、用途、効率などを学ぶ必要があります。 +2. **第 2 段階:アルゴリズム問題を解く**。まずは人気の高い問題から取り組み、少なくとも 100 問は蓄積して、主流のアルゴリズム問題に慣れることをおすすめします。最初のうちは、“知識の忘却”が課題になるかもしれませんが、心配はいりません。これはごく自然なことです。“エビングハウスの忘却曲線”に沿って問題を復習すれば、通常は 3~5 回繰り返すことでしっかり記憶に定着します。おすすめの問題リストと学習計画は、この [GitHub リポジトリ](https://github.com/krahets/LeetCode-Book) を参照してください。 +3. **第 3 段階:知識体系を構築する**。学習面では、アルゴリズムの連載記事、解法フレームワーク、教材などを読むことで、知識体系を継続的に充実させられます。問題演習の面では、トピック別分類、1 問多解、1 解多題といった発展的な戦略も試せます。関連する学習ノウハウは各コミュニティで見つけられます。 + +以下の図のように、本書の内容は主に“第 1 段階”を扱っており、第 2 段階と第 3 段階の学習をより効率的に進める助けとなることを目的としています。 + +![アルゴリズム学習ロードマップ](suggestions.assets/learning_route.png){ class="animation-figure" } + +

図 0-8   アルゴリズム学習ロードマップ

diff --git a/ja/docs/chapter_preface/summary.md b/ja/docs/chapter_preface/summary.md index 361aaafad..e0dd3c145 100644 --- a/ja/docs/chapter_preface/summary.md +++ b/ja/docs/chapter_preface/summary.md @@ -4,9 +4,11 @@ comments: true # 0.3   まとめ -- この本の主な読者はアルゴリズムの初心者です。すでに基本的な知識をお持ちの場合、この本はアルゴリズムの知識を体系的に復習するのに役立ち、この本のソースコードは「コーディングツールキット」としても使用できます。 -- この本は3つの主要なセクション、計算量解析、データ構造、アルゴリズムで構成されており、この分野のほとんどのトピックをカバーしています。 -- アルゴリズムの初心者にとって、多くの回り道や一般的な落とし穴を避けるために、初期段階で入門書を読むことが重要です。 -- 本書内のアニメーションと図は通常、重要なポイントと難しい知識を紹介するために使用されます。本を読む際にはこれらにより多くの注意を払う必要があります。 -- 実践はプログラミングを学ぶ最良の方法です。ソースコードを実行し、自分でコードをタイプすることを強くお勧めします。 -- この本のWeb版の各章には議論セクションがあり、いつでも質問や洞察を共有することを歓迎します。 +### 1.   重要ポイントの振り返り + +- 本書の主な対象読者はアルゴリズム初学者です。すでにある程度の基礎がある場合でも、本書はアルゴリズム知識を体系的に振り返る助けとなり、書中のソースコードは「問題演習用ツール集」としても利用できます。 +- 本書の内容は主に計算量解析、データ構造、アルゴリズムの三部からなり、この分野の大部分のテーマを網羅しています。 +- アルゴリズム初心者にとって、学習初期の段階で入門書を読むことは非常に重要であり、多くの遠回りを避けられます。 +- 本書のアニメーション図解は通常、重要な知識や難しい知識を紹介するために用いられます。本書を読む際は、これらの内容により多く注意を払うべきです。 +- 実践はプログラミングを学ぶ最良の方法です。ソースコードを実行し、実際に自分でコードを書くことを強く勧めます。 +- 本書のWeb版の各章にはコメント欄が設けられており、疑問や見解をいつでも共有することを歓迎します。 diff --git a/ja/docs/chapter_reference/index.md b/ja/docs/chapter_reference/index.md index d9db6397a..eca1a8866 100644 --- a/ja/docs/chapter_reference/index.md +++ b/ja/docs/chapter_reference/index.md @@ -10,15 +10,15 @@ icon: material/bookshelf [3] Robert Sedgewick, et al. Algorithms (4th Edition). -[4] Yan Weimin. Data Structures (C Language Version). +[4] 严蔚敏. データ構造(C 言語版). -[5] Deng Junhui. Data Structures (C++ Language Version, Third Edition). +[5] 邓俊辉. データ構造(C++ 言語版、第3版). -[6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition). +[6] マーク・アレン・ワイス著,陈越訳. データ構造とアルゴリズム解析:Java言語による記述(第3版). -[7] Cheng Jie. Speaking of Data Structures. +[7] 程杰. データ構造の話. -[8] Wang Zheng. The Beauty of Data Structures and Algorithms. +[8] 王争. データ構造とアルゴリズムの美. [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition). diff --git a/ja/docs/chapter_searching/binary_search.md b/ja/docs/chapter_searching/binary_search.md index 84c906bd2..7cc2f7aa2 100644 --- a/ja/docs/chapter_searching/binary_search.md +++ b/ja/docs/chapter_searching/binary_search.md @@ -4,30 +4,30 @@ comments: true # 10.1   二分探索 -二分探索は分割統治戦略を用いる効率的な探索アルゴリズムです。配列内の要素の整列順序を利用し、各反復で探索区間を半分に減らしながら、目標要素が見つかるか探索区間が空になるまで続行します。 +二分探索(binary search)は分割統治法に基づく効率的な探索アルゴリズムです。データが整列済みである性質を利用し、各ラウンドで探索範囲を半分に縮小し、目標要素を見つけるか探索区間が空になるまで続けます。 !!! question - 長さ$n$の配列`nums`が与えられ、要素は重複なしで昇順に配列されています。この配列内の要素`target`のインデックスを見つけて返してください。配列に要素が含まれていない場合は$-1$を返してください。例を下図に示します。 + 長さ $n$ の配列 `nums` が与えられます。要素は小さい順に並んでおり、重複しません。要素 `target` がこの配列内にある場合はそのインデックスを返し、含まれない場合は $-1$ を返してください。例を次の図に示します。 -![Binary search example data](binary_search.assets/binary_search_example.png){ class="animation-figure" } +![二分探索の例](binary_search.assets/binary_search_example.png){ class="animation-figure" } -

図 10-1   Binary search example data

+

図 10-1   二分探索の例

-下図に示すように、まず$i = 0$と$j = n - 1$でポインタを初期化し、それぞれ配列の最初と最後の要素を指します。これらはまた全体の探索区間$[0, n - 1]$を表します。角括弧は閉区間を示し、境界値自身も含むことに注意してください。 +次の図に示すように、まずポインタ $i = 0$ と $j = n - 1$ を初期化し、それぞれ配列の先頭要素と末尾要素を指すようにして、探索区間 $[0, n - 1]$ を表します。角括弧は閉区間を表し、境界値自体を含むことに注意してください。 -そして、以下の2つのステップをループで実行する可能性があります。 +次に、以下の 2 つの手順を繰り返します。 -1. 中点インデックス$m = \lfloor {(i + j) / 2} \rfloor$を計算します。ここで$\lfloor \: \rfloor$は床関数を表します。 -2. `nums[m]`と`target`の比較に基づいて、以下の3つのケースのうち1つを選択して実行します。 - 1. `nums[m] < target`の場合、`target`は区間$[m + 1, j]$にあることを示すため、$i = m + 1$とします。 - 2. `nums[m] > target`の場合、`target`は区間$[i, m - 1]$にあることを示すため、$j = m - 1$とします。 - 3. `nums[m] = target`の場合、`target`が見つかったことを示すため、インデックス$m$を返します。 +1. 中央のインデックス $m = \lfloor {(i + j) / 2} \rfloor$ を計算します。ここで $\lfloor \: \rfloor$ は切り捨てを表します。 +2. `nums[m]` と `target` の大小関係を判定し、次の 3 つの場合に分かれます。 + 1. `nums[m] < target` のとき、`target` は区間 $[m + 1, j]$ にあるため、$i = m + 1$ を実行します。 + 2. `nums[m] > target` のとき、`target` は区間 $[i, m - 1]$ にあるため、$j = m - 1$ を実行します。 + 3. `nums[m] = target` のとき、`target` が見つかったので、インデックス $m$ を返します。 -配列に目標要素が含まれていない場合、探索区間は最終的に空になり、$-1$を返して終了します。 +配列に目標要素が含まれない場合、探索区間は最終的に空まで縮小されます。このとき $-1$ を返します。 === "<1>" - ![Binary search process](binary_search.assets/binary_search_step1.png){ class="animation-figure" } + ![二分探索の流れ](binary_search.assets/binary_search_step1.png){ class="animation-figure" } === "<2>" ![binary_search_step2](binary_search.assets/binary_search_step2.png){ class="animation-figure" } @@ -47,50 +47,50 @@ comments: true === "<7>" ![binary_search_step7](binary_search.assets/binary_search_step7.png){ class="animation-figure" } -

図 10-2   Binary search process

+

図 10-2   二分探索の流れ

-$i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲を超える可能性がある**ことは注目に値します。大きな数のオーバーフローを避けるため、通常は式$m = \lfloor {i + (j - i) / 2} \rfloor$を使用して中点を計算します。 +注意すべき点として、$i$ と $j$ はどちらも `int` 型であるため、**$i + j$ が `int` 型の範囲を超える可能性があります**。大きな数によるオーバーフローを避けるため、通常は式 $m = \lfloor {i + (j - i) / 2} \rfloor$ を用いて中点を計算します。 -コードは以下の通りです: +コードは次のとおりです。 === "Python" ```python title="binary_search.py" def binary_search(nums: list[int], target: int) -> int: - """二分探索(両端閉区間)""" - # 両端閉区間 [0, n-1] を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素を指す + """二分探索(両閉区間)""" + # 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す i, j = 0, len(nums) - 1 - # 検索区間が空になるまでループ(i > j のとき空) + # ループし、探索区間が空になったら終了する(i > j で空) while i <= j: - # 理論的には、Pythonの数値は無限に大きくなることができる(メモリサイズに依存)ため、大きな数のオーバーフローを考慮する必要はない - m = i + (j - i) // 2 # 中点インデックス m を計算 + # 理論上、Python の数値は無限に大きくできるため(メモリ容量に依存)、大きな数のオーバーフローを考慮する必要はない + m = (i + j) // 2 # 中点インデックス m を計算 if nums[m] < target: - i = m + 1 # この場合、target は区間 [m+1, j] にあることを示す + i = m + 1 # この場合、target は区間 [m+1, j] にある elif nums[m] > target: - j = m - 1 # この場合、target は区間 [i, m-1] にあることを示す + j = m - 1 # この場合、target は区間 [i, m-1] にある else: - return m # ターゲット要素が見つかったため、そのインデックスを返す - return -1 # ターゲット要素が見つからなかったため、-1 を返す + return m # 目標要素が見つかったらそのインデックスを返す + return -1 # 目標要素が見つからなければ -1 を返す ``` === "C++" ```cpp title="binary_search.cpp" - /* 二分探索(両端閉区間) */ + /* 二分探索(両閉区間) */ int binarySearch(vector &nums, int target) { - // 両端閉区間[0, n-1]を初期化、すなわちi、jはそれぞれ配列の最初の要素と最後の要素を指す + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す int i = 0, j = nums.size() - 1; - // 探索区間が空になるまでループ(i > jの時空になる) + // ループし、探索区間が空になったら終了する(i > j で空) while (i <= j) { - int m = i + (j - i) / 2; // 中点インデックスmを計算 - if (nums[m] < target) // この状況はtargetが区間[m+1, j]にあることを示す + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) // この場合、target は区間 [m+1, j] にある i = m + 1; - else if (nums[m] > target) // この状況はtargetが区間[i, m-1]にあることを示す + else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある j = m - 1; - else // ターゲット要素が見つかったため、そのインデックスを返す + else // 目標要素が見つかったらそのインデックスを返す return m; } - // ターゲット要素が見つからなかったため、-1を返す + // 目標要素が見つからなければ -1 を返す return -1; } ``` @@ -98,21 +98,21 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲 === "Java" ```java title="binary_search.java" - /* 二分探索(両端閉区間) */ + /* 二分探索(両閉区間) */ int binarySearch(int[] nums, int target) { - // 両端閉区間 [0, n-1] を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素を指す + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す int i = 0, j = nums.length - 1; - // 探索区間が空になるまでループ(i > j のとき空) + // ループし、探索区間が空になったら終了する(i > j で空) while (i <= j) { int m = i + (j - i) / 2; // 中点インデックス m を計算 - if (nums[m] < target) // この状況は target が区間 [m+1, j] にあることを示す + if (nums[m] < target) // この場合、target は区間 [m+1, j] にある i = m + 1; - else if (nums[m] > target) // この状況は target が区間 [i, m-1] にあることを示す + else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある j = m - 1; - else // 目標要素を見つけたので、そのインデックスを返す + else // 目標要素が見つかったらそのインデックスを返す return m; } - // 目標要素を見つけられなかったので、-1 を返す + // 目標要素が見つからなければ -1 を返す return -1; } ``` @@ -120,90 +120,280 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲 === "C#" ```csharp title="binary_search.cs" - [class]{binary_search}-[func]{BinarySearch} + /* 二分探索(両閉区間) */ + int BinarySearch(int[] nums, int target) { + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す + int i = 0, j = nums.Length - 1; + // ループし、探索区間が空になったら終了する(i > j で空) + while (i <= j) { + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) // この場合、target は区間 [m+1, j] にある + i = m + 1; + else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある + j = m - 1; + else // 目標要素が見つかったらそのインデックスを返す + return m; + } + // 目標要素が見つからなければ -1 を返す + return -1; + } ``` === "Go" ```go title="binary_search.go" - [class]{}-[func]{binarySearch} + /* 二分探索(両閉区間) */ + func binarySearch(nums []int, target int) int { + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す + i, j := 0, len(nums)-1 + // ループし、探索区間が空になったら終了する(i > j で空) + for i <= j { + m := i + (j-i)/2 // 中点インデックス m を計算 + if nums[m] < target { // この場合、target は区間 [m+1, j] にある + i = m + 1 + } else if nums[m] > target { // この場合、target は区間 [i, m-1] にある + j = m - 1 + } else { // 目標要素が見つかったらそのインデックスを返す + return m + } + } + // 目標要素が見つからなければ -1 を返す + return -1 + } ``` === "Swift" ```swift title="binary_search.swift" - [class]{}-[func]{binarySearch} + /* 二分探索(両閉区間) */ + func binarySearch(nums: [Int], target: Int) -> Int { + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す + var i = nums.startIndex + var j = nums.endIndex - 1 + // ループし、探索区間が空になったら終了する(i > j で空) + while i <= j { + let m = i + (j - i) / 2 // 中点インデックス m を計算 + if nums[m] < target { // この場合、target は区間 [m+1, j] にある + i = m + 1 + } else if nums[m] > target { // この場合、target は区間 [i, m-1] にある + j = m - 1 + } else { // 目標要素が見つかったらそのインデックスを返す + return m + } + } + // 目標要素が見つからなければ -1 を返す + return -1 + } ``` === "JS" ```javascript title="binary_search.js" - [class]{}-[func]{binarySearch} + /* 二分探索(両閉区間) */ + function binarySearch(nums, target) { + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す + let i = 0, + j = nums.length - 1; + // ループし、探索区間が空になったら終了する(i > j で空) + while (i <= j) { + // 中点インデックス `m` を計算し、`parseInt()` で切り捨てる + const m = parseInt(i + (j - i) / 2); + if (nums[m] < target) + // この場合、target は区間 [m+1, j] にある + i = m + 1; + else if (nums[m] > target) + // この場合、target は区間 [i, m-1] にある + j = m - 1; + else return m; // 目標要素が見つかったらそのインデックスを返す + } + // 目標要素が見つからなければ -1 を返す + return -1; + } ``` === "TS" ```typescript title="binary_search.ts" - [class]{}-[func]{binarySearch} + /* 二分探索(両閉区間) */ + function binarySearch(nums: number[], target: number): number { + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す + let i = 0, + j = nums.length - 1; + // ループし、探索区間が空になったら終了する(i > j で空) + while (i <= j) { + // 中点インデックス m を計算 + const m = Math.floor(i + (j - i) / 2); + if (nums[m] < target) { + // この場合、target は区間 [m+1, j] にある + i = m + 1; + } else if (nums[m] > target) { + // この場合、target は区間 [i, m-1] にある + j = m - 1; + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } + return -1; // 目標要素が見つからなければ -1 を返す + } ``` === "Dart" ```dart title="binary_search.dart" - [class]{}-[func]{binarySearch} + /* 二分探索(両閉区間) */ + int binarySearch(List nums, int target) { + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す + int i = 0, j = nums.length - 1; + // ループし、探索区間が空になったら終了する(i > j で空) + while (i <= j) { + int m = i + (j - i) ~/ 2; // 中点インデックス m を計算 + if (nums[m] < target) { + // この場合、target は区間 [m+1, j] にある + i = m + 1; + } else if (nums[m] > target) { + // この場合、target は区間 [i, m-1] にある + j = m - 1; + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } + // 目標要素が見つからなければ -1 を返す + return -1; + } ``` === "Rust" ```rust title="binary_search.rs" - [class]{}-[func]{binary_search} + /* 二分探索(両閉区間) */ + fn binary_search(nums: &[i32], target: i32) -> i32 { + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す + let mut i = 0; + let mut j = nums.len() as i32 - 1; + // ループし、探索区間が空になったら終了する(i > j で空) + while i <= j { + let m = i + (j - i) / 2; // 中点インデックス m を計算 + if nums[m as usize] < target { + // この場合、target は区間 [m+1, j] にある + i = m + 1; + } else if nums[m as usize] > target { + // この場合、target は区間 [i, m-1] にある + j = m - 1; + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } + // 目標要素が見つからなければ -1 を返す + return -1; + } ``` === "C" ```c title="binary_search.c" - [class]{}-[func]{binarySearch} + /* 二分探索(両閉区間) */ + int binarySearch(int *nums, int len, int target) { + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す + int i = 0, j = len - 1; + // ループし、探索区間が空になったら終了する(i > j で空) + while (i <= j) { + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) // この場合、target は区間 [m+1, j] にある + i = m + 1; + else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある + j = m - 1; + else // 目標要素が見つかったらそのインデックスを返す + return m; + } + // 目標要素が見つからなければ -1 を返す + return -1; + } ``` === "Kotlin" ```kotlin title="binary_search.kt" - [class]{}-[func]{binarySearch} + /* 二分探索(両閉区間) */ + fun binarySearch(nums: IntArray, target: Int): Int { + // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す + var i = 0 + var j = nums.size - 1 + // ループし、探索区間が空になったら終了する(i > j で空) + while (i <= j) { + val m = i + (j - i) / 2 // 中点インデックス m を計算 + if (nums[m] < target) // この場合、target は区間 [m+1, j] にある + i = m + 1 + else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある + j = m - 1 + else // 目標要素が見つかったらそのインデックスを返す + return m + } + // 目標要素が見つからなければ -1 を返す + return -1 + } ``` === "Ruby" ```ruby title="binary_search.rb" - [class]{}-[func]{binary_search} + ### 二分探索(両閉区間) ### + def binary_search(nums, target) + # 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す + i, j = 0, nums.length - 1 + + # ループし、探索区間が空になったら終了する(i > j で空) + while i <= j + # 理論上、Ruby の数値は無限に大きくできるため(メモリ容量に依存)、大きな数のオーバーフローを考慮する必要はない + m = (i + j) / 2 # 中点インデックス m を計算 + + if nums[m] < target + i = m + 1 # この場合、target は区間 [m+1, j] にある + elsif nums[m] > target + j = m - 1 # この場合、target は区間 [i, m-1] にある + else + return m # 目標要素が見つかったらそのインデックスを返す + end + end + + -1 # 目標要素が見つからなければ -1 を返す + end ``` -**時間計算量は$O(\log n)$です**:二分ループにおいて、区間は各ラウンドで半分に減少するため、反復回数は$\log_2 n$となります。 +??? pythontutor "コードの可視化" -**空間計算量は$O(1)$です**:ポインタ$i$と$j$は定数サイズの空間を占有します。 +
+ -## 10.1.1   区間表現方法 +**時間計算量は $O(\log n)$** :二分探索のループでは各ラウンドで区間が半分になるため、ループ回数は $\log_2 n$ です。 -上記の閉区間の他に、もう一つの一般的な区間表現は「左閉右開」区間で、$[0, n)$として定義され、左境界は自身を含み、右境界は含みません。この表現では、$i = j$のとき区間$[i, j)$は空になります。 +**空間計算量は $O(1)$** :ポインタ $i$ と $j$ に必要なのは定数サイズの空間だけです。 -この表現に基づいて同じ機能を持つ二分探索アルゴリズムを実装できます: +## 10.1.1   区間の表し方 + +上記の両閉区間のほかに、一般的な区間表現として「左閉右開」区間があり、$[0, n)$ と定義されます。つまり左端は含み、右端は含みません。この表現では、区間 $[i, j)$ は $i = j$ のとき空です。 + +この表現に基づいて、同じ機能を持つ二分探索アルゴリズムを実装できます。 === "Python" ```python title="binary_search.py" def binary_search_lcro(nums: list[int], target: int) -> int: """二分探索(左閉右開区間)""" - # 左閉右開区間 [0, n) を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素+1を指す + # 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す i, j = 0, len(nums) - # 検索区間が空になるまでループ(i = j のとき空) + # ループし、探索区間が空になったら終了する(i = j で空) while i < j: - m = i + (j - i) // 2 # 中点インデックス m を計算 + m = (i + j) // 2 # 中点インデックス m を計算 if nums[m] < target: - i = m + 1 # この場合、target は区間 [m+1, j) にあることを示す + i = m + 1 # この場合、target は区間 [m+1, j) にある elif nums[m] > target: - j = m # この場合、target は区間 [i, m) にあることを示す + j = m # この場合、target は区間 [i, m) にある else: - return m # ターゲット要素が見つかったため、そのインデックスを返す - return -1 # ターゲット要素が見つからなかったため、-1 を返す + return m # 目標要素が見つかったらそのインデックスを返す + return -1 # 目標要素が見つからなければ -1 を返す ``` === "C++" @@ -211,19 +401,19 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲 ```cpp title="binary_search.cpp" /* 二分探索(左閉右開区間) */ int binarySearchLCRO(vector &nums, int target) { - // 左閉右開区間[0, n)を初期化、すなわちi、jはそれぞれ配列の最初の要素と最後の要素+1を指す + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す int i = 0, j = nums.size(); - // 探索区間が空になるまでループ(i = jの時空になる) + // ループし、探索区間が空になったら終了する(i = j で空) while (i < j) { - int m = i + (j - i) / 2; // 中点インデックスmを計算 - if (nums[m] < target) // この状況はtargetが区間[m+1, j)にあることを示す + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) // この場合、target は区間 [m+1, j) にある i = m + 1; - else if (nums[m] > target) // この状況はtargetが区間[i, m)にあることを示す + else if (nums[m] > target) // この場合、target は区間 [i, m) にある j = m; - else // ターゲット要素が見つかったため、そのインデックスを返す + else // 目標要素が見つかったらそのインデックスを返す return m; } - // ターゲット要素が見つからなかったため、-1を返す + // 目標要素が見つからなければ -1 を返す return -1; } ``` @@ -233,19 +423,19 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲 ```java title="binary_search.java" /* 二分探索(左閉右開区間) */ int binarySearchLCRO(int[] nums, int target) { - // 左閉右開区間 [0, n) を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素+1を指す + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す int i = 0, j = nums.length; - // 探索区間が空になるまでループ(i = j のとき空) + // ループし、探索区間が空になったら終了する(i = j で空) while (i < j) { int m = i + (j - i) / 2; // 中点インデックス m を計算 - if (nums[m] < target) // この状況は target が区間 [m+1, j) にあることを示す + if (nums[m] < target) // この場合、target は区間 [m+1, j) にある i = m + 1; - else if (nums[m] > target) // この状況は target が区間 [i, m) にあることを示す + else if (nums[m] > target) // この場合、target は区間 [i, m) にある j = m; - else // 目標要素を見つけたので、そのインデックスを返す + else // 目標要素が見つかったらそのインデックスを返す return m; } - // 目標要素を見つけられなかったので、-1 を返す + // 目標要素が見つからなければ -1 を返す return -1; } ``` @@ -253,80 +443,271 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲 === "C#" ```csharp title="binary_search.cs" - [class]{binary_search}-[func]{BinarySearchLCRO} + /* 二分探索(左閉右開区間) */ + int BinarySearchLCRO(int[] nums, int target) { + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す + int i = 0, j = nums.Length; + // ループし、探索区間が空になったら終了する(i = j で空) + while (i < j) { + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) // この場合、target は区間 [m+1, j) にある + i = m + 1; + else if (nums[m] > target) // この場合、target は区間 [i, m) にある + j = m; + else // 目標要素が見つかったらそのインデックスを返す + return m; + } + // 目標要素が見つからなければ -1 を返す + return -1; + } ``` === "Go" ```go title="binary_search.go" - [class]{}-[func]{binarySearchLCRO} + /* 二分探索(左閉右開区間) */ + func binarySearchLCRO(nums []int, target int) int { + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す + i, j := 0, len(nums) + // ループし、探索区間が空になったら終了する(i = j で空) + for i < j { + m := i + (j-i)/2 // 中点インデックス m を計算 + if nums[m] < target { // この場合、target は区間 [m+1, j) にある + i = m + 1 + } else if nums[m] > target { // この場合、target は区間 [i, m) にある + j = m + } else { // 目標要素が見つかったらそのインデックスを返す + return m + } + } + // 目標要素が見つからなければ -1 を返す + return -1 + } ``` === "Swift" ```swift title="binary_search.swift" - [class]{}-[func]{binarySearchLCRO} + /* 二分探索(左閉右開区間) */ + func binarySearchLCRO(nums: [Int], target: Int) -> Int { + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す + var i = nums.startIndex + var j = nums.endIndex + // ループし、探索区間が空になったら終了する(i = j で空) + while i < j { + let m = i + (j - i) / 2 // 中点インデックス m を計算 + if nums[m] < target { // この場合、target は区間 [m+1, j) にある + i = m + 1 + } else if nums[m] > target { // この場合、target は区間 [i, m) にある + j = m + } else { // 目標要素が見つかったらそのインデックスを返す + return m + } + } + // 目標要素が見つからなければ -1 を返す + return -1 + } ``` === "JS" ```javascript title="binary_search.js" - [class]{}-[func]{binarySearchLCRO} + /* 二分探索(左閉右開区間) */ + function binarySearchLCRO(nums, target) { + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す + let i = 0, + j = nums.length; + // ループし、探索区間が空になったら終了する(i = j で空) + while (i < j) { + // 中点インデックス `m` を計算し、`parseInt()` で切り捨てる + const m = parseInt(i + (j - i) / 2); + if (nums[m] < target) + // この場合、target は区間 [m+1, j) にある + i = m + 1; + else if (nums[m] > target) + // この場合、target は区間 [i, m) にある + j = m; + // 目標要素が見つかったらそのインデックスを返す + else return m; + } + // 目標要素が見つからなければ -1 を返す + return -1; + } ``` === "TS" ```typescript title="binary_search.ts" - [class]{}-[func]{binarySearchLCRO} + /* 二分探索(左閉右開区間) */ + function binarySearchLCRO(nums: number[], target: number): number { + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す + let i = 0, + j = nums.length; + // ループし、探索区間が空になったら終了する(i = j で空) + while (i < j) { + // 中点インデックス m を計算 + const m = Math.floor(i + (j - i) / 2); + if (nums[m] < target) { + // この場合、target は区間 [m+1, j) にある + i = m + 1; + } else if (nums[m] > target) { + // この場合、target は区間 [i, m) にある + j = m; + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } + return -1; // 目標要素が見つからなければ -1 を返す + } ``` === "Dart" ```dart title="binary_search.dart" - [class]{}-[func]{binarySearchLCRO} + /* 二分探索(左閉右開区間) */ + int binarySearchLCRO(List nums, int target) { + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す + int i = 0, j = nums.length; + // ループし、探索区間が空になったら終了する(i = j で空) + while (i < j) { + int m = i + (j - i) ~/ 2; // 中点インデックス m を計算 + if (nums[m] < target) { + // この場合、target は区間 [m+1, j) にある + i = m + 1; + } else if (nums[m] > target) { + // この場合、target は区間 [i, m) にある + j = m; + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } + // 目標要素が見つからなければ -1 を返す + return -1; + } ``` === "Rust" ```rust title="binary_search.rs" - [class]{}-[func]{binary_search_lcro} + /* 二分探索(左閉右開区間) */ + fn binary_search_lcro(nums: &[i32], target: i32) -> i32 { + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す + let mut i = 0; + let mut j = nums.len() as i32; + // ループし、探索区間が空になったら終了する(i = j で空) + while i < j { + let m = i + (j - i) / 2; // 中点インデックス m を計算 + if nums[m as usize] < target { + // この場合、target は区間 [m+1, j) にある + i = m + 1; + } else if nums[m as usize] > target { + // この場合、target は区間 [i, m) にある + j = m; + } else { + // 目標要素が見つかったらそのインデックスを返す + return m; + } + } + // 目標要素が見つからなければ -1 を返す + return -1; + } ``` === "C" ```c title="binary_search.c" - [class]{}-[func]{binarySearchLCRO} + /* 二分探索(左閉右開区間) */ + int binarySearchLCRO(int *nums, int len, int target) { + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す + int i = 0, j = len; + // ループし、探索区間が空になったら終了する(i = j で空) + while (i < j) { + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) // この場合、target は区間 [m+1, j) にある + i = m + 1; + else if (nums[m] > target) // この場合、target は区間 [i, m) にある + j = m; + else // 目標要素が見つかったらそのインデックスを返す + return m; + } + // 目標要素が見つからなければ -1 を返す + return -1; + } ``` === "Kotlin" ```kotlin title="binary_search.kt" - [class]{}-[func]{binarySearchLCRO} + /* 二分探索(左閉右開区間) */ + fun binarySearchLCRO(nums: IntArray, target: Int): Int { + // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す + var i = 0 + var j = nums.size + // ループし、探索区間が空になったら終了する(i = j で空) + while (i < j) { + val m = i + (j - i) / 2 // 中点インデックス m を計算 + if (nums[m] < target) // この場合、target は区間 [m+1, j) にある + i = m + 1 + else if (nums[m] > target) // この場合、target は区間 [i, m) にある + j = m + else // 目標要素が見つかったらそのインデックスを返す + return m + } + // 目標要素が見つからなければ -1 を返す + return -1 + } ``` === "Ruby" ```ruby title="binary_search.rb" - [class]{}-[func]{binary_search_lcro} + ### 二分探索(左閉右開区間) ### + def binary_search_lcro(nums, target) + # 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す + i, j = 0, nums.length + + # ループし、探索区間が空になったら終了する(i = j で空) + while i < j + # 中点インデックス m を計算 + m = (i + j) / 2 + + if nums[m] < target + i = m + 1 # この場合、target は区間 [m+1, j) にある + elsif nums[m] > target + j = m - 1 # この場合、target は区間 [i, m) にある + else + return m # 目標要素が見つかったらそのインデックスを返す + end + end + + -1 # 目標要素が見つからなければ -1 を返す + end ``` -下図に示すように、2つの区間表現タイプにおいて、二分探索アルゴリズムの初期化、ループ条件、区間縮小操作が異なります。 +??? pythontutor "コードの可視化" -「閉区間」表現では両方の境界が包含的であるため、ポインタ$i$と$j$による区間縮小操作も対称的です。これによりエラーが発生しにくくなるため、**一般的に「閉区間」アプローチの使用が推奨されます**。 +
+ -![Two types of interval definitions](binary_search.assets/binary_search_ranges.png){ class="animation-figure" } +次の図に示すように、2 種類の区間表現では、二分探索アルゴリズムの初期化、ループ条件、区間の縮小操作がそれぞれ異なります。 -

図 10-3   Two types of interval definitions

+「両閉区間」の表現では左右の境界がどちらも閉区間として定義されるため、ポインタ $i$ とポインタ $j$ による区間縮小の操作も対称になります。このほうがミスをしにくいため、**一般には「両閉区間」の書き方を推奨します**。 -## 10.1.2   利点と制限 +![2 種類の区間定義](binary_search.assets/binary_search_ranges.png){ class="animation-figure" } -二分探索は時間と空間の両方の面で良好な性能を示します。 +

図 10-3   2 種類の区間定義

-- 二分探索は時間効率が良いです。大きなデータセットでは、対数時間計算量が大きな利点を提供します。例えば、サイズ$n = 2^{20}$のデータセットが与えられた場合、線形探索は$2^{20} = 1048576$回の反復が必要ですが、二分探索は$\log_2 2^{20} = 20$回のループのみで済みます。 -- 二分探索には追加の空間が必要ありません。追加の空間に依存する探索アルゴリズム(ハッシュ探索など)と比較して、二分探索はより空間効率的です。 +## 10.1.2   利点と限界 -しかし、二分探索は以下の懸念により、すべてのシナリオに適しているとは限りません。 +二分探索は時間と空間の両面で優れた性能を持ちます。 -- 二分探索はソート済みデータにのみ適用できます。未ソートのデータは二分探索を適用する前にソートする必要があり、ソートアルゴリズムは通常$O(n \log n)$の時間計算量を持つため、これは価値がないかもしれません。このコストは線形探索よりも高く、二分探索自体は言うまでもありません。頻繁な挿入があるシナリオでは、配列を順序に保つコストは非常に高く、特定の位置に新しい要素を挿入する時間計算量は$O(n)$です。 -- 二分探索は配列のみを使用できます。二分探索には非連続(ジャンプ)要素アクセスが必要で、これは連結リストでは非効率的です。そのため、連結リストや連結リストに基づくデータ構造はこのアルゴリズムに適していない可能性があります。 -- 線形探索は小さなデータセットでより良い性能を示します。線形探索では各反復で1つの判定操作のみが必要ですが、二分探索では1つの加算、1つの除算、1つから3つの判定操作、1つの加算(減算)を含み、合計4つから6つの操作が必要です。そのため、データサイズ$n$が小さい場合、線形探索は二分探索よりも高速です。 +- 二分探索は時間効率が高いです。データ量が大きい場合、対数時間計算量は大きな優位性を持ちます。たとえば、データサイズ $n = 2^{20}$ のとき、線形探索では $2^{20} = 1048576$ 回のループが必要ですが、二分探索では $\log_2 2^{20} = 20$ 回で済みます。 +- 二分探索は追加の空間を必要としません。追加領域を要する探索アルゴリズム(たとえばハッシュ探索)と比べて、二分探索はより省メモリです。 + +しかし、二分探索があらゆる状況に適しているわけではなく、主な理由は次のとおりです。 + +- 二分探索は整列済みデータにしか適用できません。入力データが無秩序な場合、二分探索を使うためだけにソートするのは割に合いません。ソートアルゴリズムの時間計算量は通常 $O(n \log n)$ であり、線形探索や二分探索よりも高いからです。要素を頻繁に挿入する場面では、配列の整列性を保つために特定位置へ挿入する必要があり、その時間計算量は $O(n)$ と高コストです。 +- 二分探索は配列にしか適していません。二分探索では要素へ飛び飛びにアクセスする必要がありますが、連結リストでそのようなアクセスを行う効率は低いため、連結リストやそれを基に実装されたデータ構造には向きません。 +- データ量が小さい場合は線形探索のほうが高性能です。線形探索では各ラウンドで 1 回の比較だけで済みますが、二分探索では 1 回の加算、1 回の除算、1 ~ 3 回の比較、1 回の加算(減算)が必要で、合計 4 ~ 6 個の基本操作になります。したがって、データ量 $n$ が小さいときは、線形探索のほうがかえって速くなります。 diff --git a/ja/docs/chapter_searching/binary_search_edge.md b/ja/docs/chapter_searching/binary_search_edge.md index 026b3f08b..21d2f48eb 100644 --- a/ja/docs/chapter_searching/binary_search_edge.md +++ b/ja/docs/chapter_searching/binary_search_edge.md @@ -4,47 +4,47 @@ comments: true # 10.3   二分探索の境界 -## 10.3.1   左境界を見つける +## 10.3.1   左端境界を探す !!! question - 重複要素を含む可能性がある長さ$n$のソート済み配列`nums`が与えられ、最も左の要素`target`のインデックスを返してください。要素が配列に存在しない場合は、$-1$を返してください。 + 長さ $n$ のソート済み配列 `nums` が与えられ、その中には重複要素が含まれる可能性があります。配列内で最も左にある要素 `target` のインデックスを返してください。配列にこの要素が含まれない場合は、$-1$ を返します。 -挿入位置の二分探索方法を思い出すと、探索完了後、インデックス$i$は`target`の最も左の出現を指します。したがって、**挿入位置の探索は本質的に最も左の`target`のインデックスを見つけることと同じです**。 +二分探索で挿入位置を求める方法を思い出すと、探索完了後に $i$ は最も左にある `target` を指します。**したがって、挿入位置を探すことの本質は、最も左にある `target` のインデックスを探すことです**。 -挿入位置を見つける関数を使用して`target`の左境界を見つけることができます。配列に`target`が含まれていない可能性があることに注意してください。これは以下の2つの結果につながる可能性があります: +挿入位置を探す関数を使って左端境界を求めることを考えます。なお、配列に `target` が含まれない場合があり、そのときは次の 2 つの結果が起こりえます。 -- 挿入位置のインデックス$i$が範囲外です。 -- 要素`nums[i]`が`target`と等しくありません。 +- 挿入位置のインデックス $i$ が範囲外になる。 +- 要素 `nums[i]` が `target` と等しくない。 -これらの場合、単に$-1$を返します。コードは以下の通りです: +上の 2 つの状況に当てはまる場合は、直接 $-1$ を返せば十分です。コードは以下のとおりです: === "Python" ```python title="binary_search_edge.py" def binary_search_left_edge(nums: list[int], target: int) -> int: - """最左端のターゲットの二分探索""" - # ターゲットの挿入位置を見つけることと同等 + """最も左の target を二分探索""" + # target の挿入位置を探すのと等価 i = binary_search_insertion(nums, target) - # ターゲットが見つからなかった場合、-1 を返す + # target が見つからなければ、-1 を返す if i == len(nums) or nums[i] != target: return -1 - # ターゲットが見つかった場合、インデックス i を返す + # target が見つかったら、インデックス i を返す return i ``` === "C++" ```cpp title="binary_search_edge.cpp" - /* 最左のターゲットの二分探索 */ + /* 最も左の target を二分探索 */ int binarySearchLeftEdge(vector &nums, int target) { - // targetの挿入ポイントを見つけることと等価 + // target の挿入位置を探すのと等価 int i = binarySearchInsertion(nums, target); - // targetが見つからなかったため、-1を返す + // target が見つからなければ、-1 を返す if (i == nums.size() || nums[i] != target) { return -1; } - // targetが見つかったため、インデックスiを返す + // target が見つかったら、インデックス i を返す return i; } ``` @@ -54,13 +54,13 @@ comments: true ```java title="binary_search_edge.java" /* 最も左の target を二分探索 */ int binarySearchLeftEdge(int[] nums, int target) { - // target の挿入点を見つけることと等価 + // target の挿入位置を探すのと等価 int i = binary_search_insertion.binarySearchInsertion(nums, target); - // target を見つけられなかったので、-1 を返す + // target が見つからなければ、-1 を返す if (i == nums.length || nums[i] != target) { return -1; } - // target を見つけたので、インデックス i を返す + // target が見つかったら、インデックス i を返す return i; } ``` @@ -68,111 +68,215 @@ comments: true === "C#" ```csharp title="binary_search_edge.cs" - [class]{binary_search_edge}-[func]{BinarySearchLeftEdge} + /* 最も左の target を二分探索 */ + int BinarySearchLeftEdge(int[] nums, int target) { + // target の挿入位置を探すのと等価 + int i = binary_search_insertion.BinarySearchInsertion(nums, target); + // target が見つからなければ、-1 を返す + if (i == nums.Length || nums[i] != target) { + return -1; + } + // target が見つかったら、インデックス i を返す + return i; + } ``` === "Go" ```go title="binary_search_edge.go" - [class]{}-[func]{binarySearchLeftEdge} + /* 最も左の target を二分探索 */ + func binarySearchLeftEdge(nums []int, target int) int { + // target の挿入位置を探すのと等価 + i := binarySearchInsertion(nums, target) + // target が見つからなければ、-1 を返す + if i == len(nums) || nums[i] != target { + return -1 + } + // target が見つかったら、インデックス i を返す + return i + } ``` === "Swift" ```swift title="binary_search_edge.swift" - [class]{}-[func]{binarySearchLeftEdge} + /* 最も左の target を二分探索 */ + func binarySearchLeftEdge(nums: [Int], target: Int) -> Int { + // target の挿入位置を探すのと等価 + let i = binarySearchInsertion(nums: nums, target: target) + // target が見つからなければ、-1 を返す + if i == nums.endIndex || nums[i] != target { + return -1 + } + // target が見つかったら、インデックス i を返す + return i + } ``` === "JS" ```javascript title="binary_search_edge.js" - [class]{}-[func]{binarySearchLeftEdge} + /* 最も左の target を二分探索 */ + function binarySearchLeftEdge(nums, target) { + // target の挿入位置を探すのと等価 + const i = binarySearchInsertion(nums, target); + // target が見つからなければ、-1 を返す + if (i === nums.length || nums[i] !== target) { + return -1; + } + // target が見つかったら、インデックス i を返す + return i; + } ``` === "TS" ```typescript title="binary_search_edge.ts" - [class]{}-[func]{binarySearchLeftEdge} + /* 最も左の target を二分探索 */ + function binarySearchLeftEdge(nums: Array, target: number): number { + // target の挿入位置を探すのと等価 + const i = binarySearchInsertion(nums, target); + // target が見つからなければ、-1 を返す + if (i === nums.length || nums[i] !== target) { + return -1; + } + // target が見つかったら、インデックス i を返す + return i; + } ``` === "Dart" ```dart title="binary_search_edge.dart" - [class]{}-[func]{binarySearchLeftEdge} + /* 最も左の target を二分探索 */ + int binarySearchLeftEdge(List nums, int target) { + // target の挿入位置を探すのと等価 + int i = binarySearchInsertion(nums, target); + // target が見つからなければ、-1 を返す + if (i == nums.length || nums[i] != target) { + return -1; + } + // target が見つかったら、インデックス i を返す + return i; + } ``` === "Rust" ```rust title="binary_search_edge.rs" - [class]{}-[func]{binary_search_left_edge} + /* 最も左の target を二分探索 */ + fn binary_search_left_edge(nums: &[i32], target: i32) -> i32 { + // target の挿入位置を探すのと等価 + let i = binary_search_insertion(nums, target); + // target が見つからなければ、-1 を返す + if i == nums.len() as i32 || nums[i as usize] != target { + return -1; + } + // target が見つかったら、インデックス i を返す + i + } ``` === "C" ```c title="binary_search_edge.c" - [class]{}-[func]{binarySearchLeftEdge} + /* 最も左の target を二分探索 */ + int binarySearchLeftEdge(int *nums, int numSize, int target) { + // target の挿入位置を探すのと等価 + int i = binarySearchInsertion(nums, numSize, target); + // target が見つからなければ、-1 を返す + if (i == numSize || nums[i] != target) { + return -1; + } + // target が見つかったら、インデックス i を返す + return i; + } ``` === "Kotlin" ```kotlin title="binary_search_edge.kt" - [class]{}-[func]{binarySearchLeftEdge} + /* 最も左の target を二分探索 */ + fun binarySearchLeftEdge(nums: IntArray, target: Int): Int { + // target の挿入位置を探すのと等価 + val i = binarySearchInsertion(nums, target) + // target が見つからなければ、-1 を返す + if (i == nums.size || nums[i] != target) { + return -1 + } + // target が見つかったら、インデックス i を返す + return i + } ``` === "Ruby" ```ruby title="binary_search_edge.rb" - [class]{}-[func]{binary_search_left_edge} + ### target の最左位置を二分探索 ### + def binary_search_left_edge(nums, target) + # target の挿入位置を探すのと等価 + i = binary_search_insertion(nums, target) + + # target が見つからなければ、-1 を返す + return -1 if i == nums.length || nums[i] != target + + i # target が見つかったら、インデックス i を返す + end ``` -## 10.3.2   右境界を見つける +??? pythontutor "コードの可視化" -`target`の最も右の出現をどのように見つけるでしょうか?最も直接的な方法は、`nums[m] == target`の場合に探索境界を調整する方法を変更して、従来の二分探索ロジックを修正することです。コードはここでは省略されています。興味がある場合は、自分でコードを実装してみてください。 +
+ -以下では、さらに2つの巧妙な方法を紹介します。 +## 10.3.2   右端境界を探す -### 1.   左境界探索を再利用する +では、最も右にある `target` はどのように探せるでしょうか。最も直接的な方法はコードを修正し、`nums[m] == target` の場合のポインタの縮小操作を置き換えることです。ここではコードを省略するので、興味があれば自分で実装してみてください。 -`target`の最も右の出現を見つけるには、最も左の`target`を見つけるために使用された関数を再利用できます。具体的には、最も右のターゲットの探索を最も左のターゲット + 1の探索に変換します。 +ここでは、より巧妙な 2 つの方法を紹介します。 -下図に示すように、探索完了後、ポインタ$i$は最も左の`target + 1`(存在する場合)を指し、ポインタ$j$は`target`の最も右の出現を指します。したがって、$j$を返すことで右境界が得られます。 +### 1.   左端境界探索を再利用する -![Transforming the search for the right boundary into the search for the left boundary](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png){ class="animation-figure" } +実際には、最も左の要素を探す関数を利用して最も右の要素を探せます。具体的には、**最も右にある `target` を探すことを、最も左にある `target + 1` を探すことに変換します**。 -

図 10-7   Transforming the search for the right boundary into the search for the left boundary

+下図のように、探索完了後、ポインタ $i$ は最も左にある `target + 1`(存在する場合)を指し、$j$ は最も右にある `target` を指します。**したがって $j$ を返せばよいです**。 -返される挿入位置は$i$であることに注意してください。したがって、$j$を得るためには1を引く必要があります: +![右端境界の探索を左端境界の探索に変換する](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png){ class="animation-figure" } + +

図 10-7   右端境界の探索を左端境界の探索に変換する

+ +返される挿入位置は $i$ なので、そこから $1$ を引いて $j$ を得る必要があることに注意してください: === "Python" ```python title="binary_search_edge.py" def binary_search_right_edge(nums: list[int], target: int) -> int: - """最右端のターゲットの二分探索""" - # 最左端のターゲット + 1 を見つけることに変換 + """最も右の target を二分探索""" + # 最左の target + 1 を探す問題に変換する i = binary_search_insertion(nums, target + 1) - # j は最右端のターゲットを指し、i はターゲットより大きい最初の要素を指す + # j は最も右の target を指し、i は target より大きい最初の要素を指す j = i - 1 - # ターゲットが見つからなかった場合、-1 を返す + # target が見つからなければ、-1 を返す if j == -1 or nums[j] != target: return -1 - # ターゲットが見つかった場合、インデックス j を返す + # target が見つかったら、インデックス j を返す return j ``` === "C++" ```cpp title="binary_search_edge.cpp" - /* 最右のターゲットの二分探索 */ + /* 最も右の target を二分探索 */ int binarySearchRightEdge(vector &nums, int target) { - // 最左のtarget + 1を見つけることに変換 + // 最左の target + 1 を探す問題に変換する int i = binarySearchInsertion(nums, target + 1); - // jは最右のターゲットを指し、iはtargetより大きい最初の要素を指す + // j は最も右の target を指し、i は target より大きい最初の要素を指す int j = i - 1; - // targetが見つからなかったため、-1を返す + // target が見つからなければ、-1 を返す if (j == -1 || nums[j] != target) { return -1; } - // targetが見つかったため、インデックスjを返す + // target が見つかったら、インデックス j を返す return j; } ``` @@ -182,15 +286,15 @@ comments: true ```java title="binary_search_edge.java" /* 最も右の target を二分探索 */ int binarySearchRightEdge(int[] nums, int target) { - // 最も左の target + 1 を見つけることに変換 + // 最左の target + 1 を探す問題に変換する int i = binary_search_insertion.binarySearchInsertion(nums, target + 1); // j は最も右の target を指し、i は target より大きい最初の要素を指す int j = i - 1; - // target を見つけられなかったので、-1 を返す + // target が見つからなければ、-1 を返す if (j == -1 || nums[j] != target) { return -1; } - // target を見つけたので、インデックス j を返す + // target が見つかったら、インデックス j を返す return j; } ``` @@ -198,77 +302,202 @@ comments: true === "C#" ```csharp title="binary_search_edge.cs" - [class]{binary_search_edge}-[func]{BinarySearchRightEdge} + /* 最も右の target を二分探索 */ + int BinarySearchRightEdge(int[] nums, int target) { + // 最左の target + 1 を探す問題に変換する + int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1); + // j は最も右の target を指し、i は target より大きい最初の要素を指す + int j = i - 1; + // target が見つからなければ、-1 を返す + if (j == -1 || nums[j] != target) { + return -1; + } + // target が見つかったら、インデックス j を返す + return j; + } ``` === "Go" ```go title="binary_search_edge.go" - [class]{}-[func]{binarySearchRightEdge} + /* 最も右の target を二分探索 */ + func binarySearchRightEdge(nums []int, target int) int { + // 最左の target + 1 を探す問題に変換する + i := binarySearchInsertion(nums, target+1) + // j は最も右の target を指し、i は target より大きい最初の要素を指す + j := i - 1 + // target が見つからなければ、-1 を返す + if j == -1 || nums[j] != target { + return -1 + } + // target が見つかったら、インデックス j を返す + return j + } ``` === "Swift" ```swift title="binary_search_edge.swift" - [class]{}-[func]{binarySearchRightEdge} + /* 最も右の target を二分探索 */ + func binarySearchRightEdge(nums: [Int], target: Int) -> Int { + // 最左の target + 1 を探す問題に変換する + let i = binarySearchInsertion(nums: nums, target: target + 1) + // j は最も右の target を指し、i は target より大きい最初の要素を指す + let j = i - 1 + // target が見つからなければ、-1 を返す + if j == -1 || nums[j] != target { + return -1 + } + // target が見つかったら、インデックス j を返す + return j + } ``` === "JS" ```javascript title="binary_search_edge.js" - [class]{}-[func]{binarySearchRightEdge} + /* 最も右の target を二分探索 */ + function binarySearchRightEdge(nums, target) { + // 最左の target + 1 を探す問題に変換する + const i = binarySearchInsertion(nums, target + 1); + // j は最も右の target を指し、i は target より大きい最初の要素を指す + const j = i - 1; + // target が見つからなければ、-1 を返す + if (j === -1 || nums[j] !== target) { + return -1; + } + // target が見つかったら、インデックス j を返す + return j; + } ``` === "TS" ```typescript title="binary_search_edge.ts" - [class]{}-[func]{binarySearchRightEdge} + /* 最も右の target を二分探索 */ + function binarySearchRightEdge(nums: Array, target: number): number { + // 最左の target + 1 を探す問題に変換する + const i = binarySearchInsertion(nums, target + 1); + // j は最も右の target を指し、i は target より大きい最初の要素を指す + const j = i - 1; + // target が見つからなければ、-1 を返す + if (j === -1 || nums[j] !== target) { + return -1; + } + // target が見つかったら、インデックス j を返す + return j; + } ``` === "Dart" ```dart title="binary_search_edge.dart" - [class]{}-[func]{binarySearchRightEdge} + /* 最も右の target を二分探索 */ + int binarySearchRightEdge(List nums, int target) { + // 最左の target + 1 を探す問題に変換する + int i = binarySearchInsertion(nums, target + 1); + // j は最も右の target を指し、i は target より大きい最初の要素を指す + int j = i - 1; + // target が見つからなければ、-1 を返す + if (j == -1 || nums[j] != target) { + return -1; + } + // target が見つかったら、インデックス j を返す + return j; + } ``` === "Rust" ```rust title="binary_search_edge.rs" - [class]{}-[func]{binary_search_right_edge} + /* 最も右の target を二分探索 */ + fn binary_search_right_edge(nums: &[i32], target: i32) -> i32 { + // 最左の target + 1 を探す問題に変換する + let i = binary_search_insertion(nums, target + 1); + // j は最も右の target を指し、i は target より大きい最初の要素を指す + let j = i - 1; + // target が見つからなければ、-1 を返す + if j == -1 || nums[j as usize] != target { + return -1; + } + // target が見つかったら、インデックス j を返す + j + } ``` === "C" ```c title="binary_search_edge.c" - [class]{}-[func]{binarySearchRightEdge} + /* 最も右の target を二分探索 */ + int binarySearchRightEdge(int *nums, int numSize, int target) { + // 最左の target + 1 を探す問題に変換する + int i = binarySearchInsertion(nums, numSize, target + 1); + // j は最も右の target を指し、i は target より大きい最初の要素を指す + int j = i - 1; + // target が見つからなければ、-1 を返す + if (j == -1 || nums[j] != target) { + return -1; + } + // target が見つかったら、インデックス j を返す + return j; + } ``` === "Kotlin" ```kotlin title="binary_search_edge.kt" - [class]{}-[func]{binarySearchRightEdge} + /* 最も右の target を二分探索 */ + fun binarySearchRightEdge(nums: IntArray, target: Int): Int { + // 最左の target + 1 を探す問題に変換する + val i = binarySearchInsertion(nums, target + 1) + // j は最も右の target を指し、i は target より大きい最初の要素を指す + val j = i - 1 + // target が見つからなければ、-1 を返す + if (j == -1 || nums[j] != target) { + return -1 + } + // target が見つかったら、インデックス j を返す + return j + } ``` === "Ruby" ```ruby title="binary_search_edge.rb" - [class]{}-[func]{binary_search_right_edge} + ### target の最右位置を二分探索 ### + def binary_search_right_edge(nums, target) + # 最左の target + 1 を探す問題に変換する + i = binary_search_insertion(nums, target + 1) + + # j は最も右の target を指し、i は target より大きい最初の要素を指す + j = i - 1 + + # target が見つからなければ、-1 を返す + return -1 if j == -1 || nums[j] != target + + j # target が見つかったら、インデックス j を返す + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 2.   要素探索に変換する -配列に`target`が含まれていない場合、$i$と$j$は最終的に`target`より大きい最初の要素と小さい最初の要素をそれぞれ指します。 +配列に `target` が含まれない場合、最終的に $i$ と $j$ はそれぞれ `target` より大きい最初の要素と、`target` より小さい最初の要素を指すことになります。 -したがって、下図に示すように、配列に存在しない要素を構築して、左と右の境界を探索できます。 +したがって、下図のように、配列中に存在しない要素を構成して、それを使って左右の境界を探せます。 -- 最も左の`target`を見つけるには:`target - 0.5`を探索することに変換でき、ポインタ$i$を返します。 -- 最も右の`target`を見つけるには:`target + 0.5`を探索することに変換でき、ポインタ$j$を返します。 +- 最も左にある `target` の探索:`target - 0.5` を探すことに変換でき、ポインタ $i$ を返します。 +- 最も右にある `target` の探索:`target + 0.5` を探すことに変換でき、ポインタ $j$ を返します。 -![Transforming the search for boundaries into the search for an element](binary_search_edge.assets/binary_search_edge_by_element.png){ class="animation-figure" } +![境界の探索を要素の探索に変換する](binary_search_edge.assets/binary_search_edge_by_element.png){ class="animation-figure" } -

図 10-8   Transforming the search for boundaries into the search for an element

+

図 10-8   境界の探索を要素の探索に変換する

-コードはここでは省略されていますが、このアプローチについて注意すべき2つの重要な点があります。 +ここではコードを省略しますが、次の 2 点に注意が必要です。 -- 与えられた配列`nums`には小数が含まれていないため、等しい場合の処理は心配ありません。 -- ただし、このアプローチで小数を導入するには、`target`変数を浮動小数点型に変更する必要があります(Pythonでは変更は不要です)。 +- 与えられた配列には小数が含まれないため、等しい場合をどう処理するかを気にする必要はありません。 +- この方法では小数を導入するため、関数内の変数 `target` を浮動小数点数型に変更する必要があります(Python は変更不要です)。 diff --git a/ja/docs/chapter_searching/binary_search_insertion.md b/ja/docs/chapter_searching/binary_search_insertion.md index d659466b9..d329d2da0 100644 --- a/ja/docs/chapter_searching/binary_search_insertion.md +++ b/ja/docs/chapter_searching/binary_search_insertion.md @@ -2,77 +2,56 @@ comments: true --- -# 10.2   二分探索による挿入 +# 10.2   二分探索の挿入位置 -二分探索は目標要素を探索するだけでなく、目標要素の挿入位置を探索するなど、多くの変種問題を解決するためにも使用されます。 +二分探索は目標要素の検索だけでなく、目標要素の挿入位置を探すなど、多くの派生問題の解決にも利用できます。 ## 10.2.1   重複要素がない場合 !!! question - 一意の要素を持つ長さ$n$のソート済み配列`nums`と要素`target`が与えられ、ソート順を維持しながら`target`を`nums`に挿入します。`target`が配列にすでに存在する場合は、既存の要素の左側に挿入します。挿入後の配列における`target`のインデックスを返してください。下図に示す例を参照してください。 + 長さ $n$ の整列済み配列 `nums` と要素 `target` が与えられます。配列には重複要素は存在しません。ここで `target` を配列 `nums` に挿入し、その順序を保ちます。配列中にすでに要素 `target` が存在する場合は、その左側に挿入します。挿入後の配列における `target` のインデックスを返してください。例を以下の図に示します。 -![Example data for binary search insertion point](binary_search_insertion.assets/binary_search_insertion_example.png){ class="animation-figure" } +![二分探索の挿入位置の例データ](binary_search_insertion.assets/binary_search_insertion_example.png){ class="animation-figure" } -

図 10-4   Example data for binary search insertion point

+

図 10-4   二分探索の挿入位置の例データ

-前のセクションの二分探索コードを再利用したい場合、以下の2つの質問に答える必要があります。 +前節の二分探索コードを再利用したい場合は、次の二つの問題に答える必要があります。 -**質問1**:配列にすでに`target`が含まれている場合、挿入位置は既存要素のインデックスになりますか? +**問題 1**:配列に `target` が含まれる場合、挿入位置のインデックスはその要素のインデックスですか? -`target`を等しい要素の左側に挿入するという要件は、新しく挿入される`target`が元の`target`の位置を置き換えることを意味します。つまり、**配列に`target`が含まれている場合、挿入位置は確かにその`target`のインデックスです**。 +問題では `target` を等しい要素の左側に挿入するよう求めているため、新しく挿入された `target` は元の `target` の位置に入ります。つまり、**配列に `target` が含まれる場合、挿入位置のインデックスはその `target` のインデックスです**。 -**質問2**:配列に`target`が含まれていない場合、どのインデックスに挿入されますか? +**問題 2**:配列に `target` が存在しない場合、挿入位置はどの要素のインデックスですか? -二分探索プロセスをさらに考えてみましょう:`nums[m] < target`のとき、ポインタ$i$が移動します。これは、ポインタ$i$が`target`以上の要素に近づいていることを意味します。同様に、ポインタ$j$は常に`target`以下の要素に近づいています。 +二分探索の過程をさらに考えると、`nums[m] < target` のときは $i$ が移動します。これは、ポインタ $i$ が `target` 以上の要素へ近づいていることを意味します。同様に、ポインタ $j$ は常に `target` 以下の要素へ近づいています。 -したがって、二分の終了時には確実に:$i$は`target`より大きい最初の要素を指し、$j$は`target`より小さい最初の要素を指します。**配列に`target`が含まれていない場合、挿入位置は$i$であることは明らかです**。コードは以下の通りです: +したがって二分探索の終了時には、$i$ は最初の `target` より大きい要素を指し、$j$ は最初の `target` より小さい要素を指します。**よって、配列に `target` が含まれない場合、挿入インデックスは $i$ です**。コードは次のとおりです: === "Python" ```python title="binary_search_insertion.py" def binary_search_insertion_simple(nums: list[int], target: int) -> int: - """挿入位置の二分探索(重複要素なし)""" - i, j = 0, len(nums) - 1 # 両端閉区間 [0, n-1] を初期化 + """二分探索で挿入位置を探す(重複要素なし)""" + i, j = 0, len(nums) - 1 # 両閉区間 [0, n-1] を初期化 while i <= j: - m = i + (j - i) // 2 # 中点インデックス m を計算 + m = (i + j) // 2 # 中点インデックス m を計算 if nums[m] < target: - i = m + 1 # ターゲットは区間 [m+1, j] にある + i = m + 1 # target は区間 [m+1, j] にある elif nums[m] > target: - j = m - 1 # ターゲットは区間 [i, m-1] にある + j = m - 1 # target は区間 [i, m-1] にある else: - return m # ターゲットが見つかった場合、挿入位置 m を返す - # ターゲットが見つからなかった場合、挿入位置 i を返す + return m # target が見つかったら、挿入位置 m を返す + # target が見つからなければ、挿入位置 i を返す return i ``` === "C++" ```cpp title="binary_search_insertion.cpp" - /* 挿入ポイントの二分探索(重複要素なし) */ + /* 二分探索で挿入位置を探す(重複要素なし) */ int binarySearchInsertionSimple(vector &nums, int target) { - int i = 0, j = nums.size() - 1; // 両端閉区間[0, n-1]を初期化 - while (i <= j) { - int m = i + (j - i) / 2; // 中点インデックスmを計算 - if (nums[m] < target) { - i = m + 1; // ターゲットは区間[m+1, j]にある - } else if (nums[m] > target) { - j = m - 1; // ターゲットは区間[i, m-1]にある - } else { - return m; // ターゲットが見つかったため、挿入ポイントmを返す - } - } - // ターゲットが見つからなかったため、挿入ポイントiを返す - return i; - } - ``` - -=== "Java" - - ```java title="binary_search_insertion.java" - /* 挿入点の二分探索(重複要素なし) */ - int binarySearchInsertionSimple(int[] nums, int target) { - int i = 0, j = nums.length - 1; // 両端閉区間 [0, n-1] を初期化 + int i = 0, j = nums.size() - 1; // 両閉区間 [0, n-1] を初期化 while (i <= j) { int m = i + (j - i) / 2; // 中点インデックス m を計算 if (nums[m] < target) { @@ -80,10 +59,31 @@ comments: true } else if (nums[m] > target) { j = m - 1; // target は区間 [i, m-1] にある } else { - return m; // target を見つけたので、挿入点 m を返す + return m; // target が見つかったら、挿入位置 m を返す } } - // target を見つけられなかったので、挿入点 i を返す + // target が見つからなければ、挿入位置 i を返す + return i; + } + ``` + +=== "Java" + + ```java title="binary_search_insertion.java" + /* 二分探索で挿入位置を探す(重複要素なし) */ + int binarySearchInsertionSimple(int[] nums, int target) { + int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + return m; // target が見つかったら、挿入位置 m を返す + } + } + // target が見つからなければ、挿入位置 i を返す return i; } ``` @@ -91,91 +91,263 @@ comments: true === "C#" ```csharp title="binary_search_insertion.cs" - [class]{binary_search_insertion}-[func]{BinarySearchInsertionSimple} + /* 二分探索で挿入位置を探す(重複要素なし) */ + int BinarySearchInsertionSimple(int[] nums, int target) { + int i = 0, j = nums.Length - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + return m; // target が見つかったら、挿入位置 m を返す + } + } + // target が見つからなければ、挿入位置 i を返す + return i; + } ``` === "Go" ```go title="binary_search_insertion.go" - [class]{}-[func]{binarySearchInsertionSimple} + /* 二分探索で挿入位置を探す(重複要素なし) */ + func binarySearchInsertionSimple(nums []int, target int) int { + // 両閉区間 [0, n-1] を初期化 + i, j := 0, len(nums)-1 + for i <= j { + // 中点インデックス m を計算 + m := i + (j-i)/2 + if nums[m] < target { + // target は区間 [m+1, j] にある + i = m + 1 + } else if nums[m] > target { + // target は区間 [i, m-1] にある + j = m - 1 + } else { + // target が見つかったら、挿入位置 m を返す + return m + } + } + // target が見つからなければ、挿入位置 i を返す + return i + } ``` === "Swift" ```swift title="binary_search_insertion.swift" - [class]{}-[func]{binarySearchInsertionSimple} + /* 二分探索で挿入位置を探す(重複要素なし) */ + func binarySearchInsertionSimple(nums: [Int], target: Int) -> Int { + // 両閉区間 [0, n-1] を初期化 + var i = nums.startIndex + var j = nums.endIndex - 1 + while i <= j { + let m = i + (j - i) / 2 // 中点インデックス m を計算 + if nums[m] < target { + i = m + 1 // target は区間 [m+1, j] にある + } else if nums[m] > target { + j = m - 1 // target は区間 [i, m-1] にある + } else { + return m // target が見つかったら、挿入位置 m を返す + } + } + // target が見つからなければ、挿入位置 i を返す + return i + } ``` === "JS" ```javascript title="binary_search_insertion.js" - [class]{}-[func]{binarySearchInsertionSimple} + /* 二分探索で挿入位置を探す(重複要素なし) */ + function binarySearchInsertionSimple(nums, target) { + let i = 0, + j = nums.length - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + return m; // target が見つかったら、挿入位置 m を返す + } + } + // target が見つからなければ、挿入位置 i を返す + return i; + } ``` === "TS" ```typescript title="binary_search_insertion.ts" - [class]{}-[func]{binarySearchInsertionSimple} + /* 二分探索で挿入位置を探す(重複要素なし) */ + function binarySearchInsertionSimple( + nums: Array, + target: number + ): number { + let i = 0, + j = nums.length - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + return m; // target が見つかったら、挿入位置 m を返す + } + } + // target が見つからなければ、挿入位置 i を返す + return i; + } ``` === "Dart" ```dart title="binary_search_insertion.dart" - [class]{}-[func]{binarySearchInsertionSimple} + /* 二分探索で挿入位置を探す(重複要素なし) */ + int binarySearchInsertionSimple(List nums, int target) { + int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + int m = i + (j - i) ~/ 2; // 中点インデックス m を計算 + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + return m; // target が見つかったら、挿入位置 m を返す + } + } + // target が見つからなければ、挿入位置 i を返す + return i; + } ``` === "Rust" ```rust title="binary_search_insertion.rs" - [class]{}-[func]{binary_search_insertion_simple} + /* 二分探索で挿入位置を探す(重複要素なし) */ + fn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 { + let (mut i, mut j) = (0, nums.len() as i32 - 1); // 両閉区間 [0, n-1] を初期化 + while i <= j { + let m = i + (j - i) / 2; // 中点インデックス m を計算 + if nums[m as usize] < target { + i = m + 1; // target は区間 [m+1, j] にある + } else if nums[m as usize] > target { + j = m - 1; // target は区間 [i, m-1] にある + } else { + return m; + } + } + // target が見つからなければ、挿入位置 i を返す + i + } ``` === "C" ```c title="binary_search_insertion.c" - [class]{}-[func]{binarySearchInsertionSimple} + /* 二分探索で挿入位置を探す(重複要素なし) */ + int binarySearchInsertionSimple(int *nums, int numSize, int target) { + int i = 0, j = numSize - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + return m; // target が見つかったら、挿入位置 m を返す + } + } + // target が見つからなければ、挿入位置 i を返す + return i; + } ``` === "Kotlin" ```kotlin title="binary_search_insertion.kt" - [class]{}-[func]{binarySearchInsertionSimple} + /* 二分探索で挿入位置を探す(重複要素なし) */ + fun binarySearchInsertionSimple(nums: IntArray, target: Int): Int { + var i = 0 + var j = nums.size - 1 // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + val m = i + (j - i) / 2 // 中点インデックス m を計算 + if (nums[m] < target) { + i = m + 1 // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1 // target は区間 [i, m-1] にある + } else { + return m // target が見つかったら、挿入位置 m を返す + } + } + // target が見つからなければ、挿入位置 i を返す + return i + } ``` === "Ruby" ```ruby title="binary_search_insertion.rb" - [class]{}-[func]{binary_search_insertion_simple} + ### 二分探索の挿入位置(重複要素なし) ### + def binary_search_insertion_simple(nums, target) + # 両閉区間 [0, n-1] を初期化 + i, j = 0, nums.length - 1 + + while i <= j + # 中点インデックス m を計算 + m = (i + j) / 2 + + if nums[m] < target + i = m + 1 # target は区間 [m+1, j] にある + elsif nums[m] > target + j = m - 1 # target は区間 [i, m-1] にある + else + return m # target が見つかったら、挿入位置 m を返す + end + end + + i # target が見つからなければ、挿入位置 i を返す + end ``` +??? pythontutor "コードの可視化" + +
+ + ## 10.2.2   重複要素がある場合 !!! question - 前の質問に基づいて、配列に重複要素が含まれている可能性があると仮定し、他はすべて同じとします。 + 前問を踏まえ、配列には重複要素が含まれる可能性があるものとし、それ以外の条件は変わりません。 -配列に`target`の複数の出現がある場合、通常の二分探索は`target`の1つの出現のインデックスのみを返すことができ、**その位置の左右に`target`の出現がいくつあるかを特定することはできません**。 +配列中に複数の `target` が存在する場合、通常の二分探索ではそのうち一つの `target` のインデックスしか返せず、**その要素の左側と右側にあといくつ `target` があるかは分かりません**。 -問題では目標要素を最も左の位置に挿入することが要求されているため、**配列内の最も左の`target`のインデックスを見つける必要があります**。最初に下図に示すステップを通してこれを実装することを考えてみましょう。 +問題では目標要素を最も左に挿入する必要があるため、**配列中で最も左にある `target` のインデックスを探す必要があります**。まずは以下の図に示す手順で実現することを考えます。 -1. 二分探索を実行して`target`の任意のインデックス、例えば$k$を見つけます。 -2. インデックス$k$から開始して、最も左の`target`の出現が見つかるまで左に線形探索を行い、このインデックスを返します。 +1. 二分探索を実行し、任意の `target` のインデックスを得て、これを $k$ とします。 +2. インデックス $k$ から始めて左へ線形探索し、最も左の `target` を見つけたら返します。 -![Linear search for the insertion point of duplicate elements](binary_search_insertion.assets/binary_search_insertion_naive.png){ class="animation-figure" } +![線形探索による重複要素の挿入位置](binary_search_insertion.assets/binary_search_insertion_naive.png){ class="animation-figure" } -

図 10-5   Linear search for the insertion point of duplicate elements

+

図 10-5   線形探索による重複要素の挿入位置

-この方法は実現可能ですが、線形探索を含むため、時間計算量は$O(n)$です。この方法は、配列に多くの重複する`target`が含まれている場合に非効率です。 +この方法は使用できますが、線形探索を含むため、時間計算量は $O(n)$ です。配列中に重複した `target` が多い場合、この方法の効率は低くなります。 -今度は二分探索コードを拡張することを考えてみましょう。下図に示すように、全体的なプロセスは同じままです。各ラウンドで、まず中間インデックス$m$を計算し、次に`target`と`nums[m]`の値を比較して、以下のケースになります。 +次に、二分探索のコードを拡張することを考えます。以下の図に示すように、全体の流れは変えず、各反復でまず中点インデックス $m$ を計算し、その後 `target` と `nums[m]` の大小関係を判定して、次のいくつかの状況に分けます。 -- `nums[m] < target`または`nums[m] > target`のとき、これは`target`がまだ見つかっていないことを意味するため、通常の二分探索を使用して探索範囲を狭め、**ポインタ$i$と$j$を`target`に近づけます**。 -- `nums[m] == target`のとき、これは`target`より小さい要素が範囲$[i, m - 1]$にあることを示すため、$j = m - 1$を使用して範囲を狭め、**ポインタ$j$を`target`より小さい要素に近づけます**。 +- `nums[m] < target` または `nums[m] > target` のときは、まだ `target` を見つけていないことを意味するため、通常の二分探索と同じ区間縮小を行い、**ポインタ $i$ と $j$ を `target` に近づけます**。 +- `nums[m] == target` のときは、`target` より小さい要素が区間 $[i, m - 1]$ にあることを意味するため、$j = m - 1$ として区間を縮小し、**ポインタ $j$ を `target` より小さい要素に近づけます**。 -ループ後、$i$は最も左の`target`を指し、$j$は`target`より小さい最初の要素を指すため、**インデックス$i$が挿入位置です**。 +ループ終了後、$i$ は最も左の `target` を指し、$j$ は最初の `target` より小さい要素を指すため、**インデックス $i$ が挿入位置です**。 === "<1>" - ![Steps for binary search insertion point of duplicate elements](binary_search_insertion.assets/binary_search_insertion_step1.png){ class="animation-figure" } + ![重複要素に対する二分探索の挿入位置の手順](binary_search_insertion.assets/binary_search_insertion_step1.png){ class="animation-figure" } === "<2>" ![binary_search_insertion_step2](binary_search_insertion.assets/binary_search_insertion_step2.png){ class="animation-figure" } @@ -198,26 +370,26 @@ comments: true === "<8>" ![binary_search_insertion_step8](binary_search_insertion.assets/binary_search_insertion_step8.png){ class="animation-figure" } -

図 10-6   Steps for binary search insertion point of duplicate elements

+

図 10-6   重複要素に対する二分探索の挿入位置の手順

-以下のコードを観察してください。分岐`nums[m] > target`と`nums[m] == target`の操作は同じであるため、これら2つの分岐をマージできます。 +以下のコードを観察すると、分岐 `nums[m] > target` と `nums[m] == target` の処理は同じであるため、両者はまとめることができます。 -それでも、ロジックがより明確になり、可読性が向上するため、条件を展開したままにしておくことができます。 +それでも、判定条件を分けたままにしておくことは可能であり、そのほうがロジックがより明確で、可読性も高くなります。 === "Python" ```python title="binary_search_insertion.py" def binary_search_insertion(nums: list[int], target: int) -> int: - """挿入位置の二分探索(重複要素あり)""" - i, j = 0, len(nums) - 1 # 両端閉区間 [0, n-1] を初期化 + """二分探索で挿入位置を探す(重複要素あり)""" + i, j = 0, len(nums) - 1 # 両閉区間 [0, n-1] を初期化 while i <= j: - m = i + (j - i) // 2 # 中点インデックス m を計算 + m = (i + j) // 2 # 中点インデックス m を計算 if nums[m] < target: - i = m + 1 # ターゲットは区間 [m+1, j] にある + i = m + 1 # target は区間 [m+1, j] にある elif nums[m] > target: - j = m - 1 # ターゲットは区間 [i, m-1] にある + j = m - 1 # target は区間 [i, m-1] にある else: - j = m - 1 # ターゲット未満の最初の要素は区間 [i, m-1] にある + j = m - 1 # target より小さい最初の要素は区間 [i, m-1] にある # 挿入位置 i を返す return i ``` @@ -225,30 +397,9 @@ comments: true === "C++" ```cpp title="binary_search_insertion.cpp" - /* 挿入ポイントの二分探索(重複要素あり) */ + /* 二分探索で挿入位置を探す(重複要素あり) */ int binarySearchInsertion(vector &nums, int target) { - int i = 0, j = nums.size() - 1; // 両端閉区間[0, n-1]を初期化 - while (i <= j) { - int m = i + (j - i) / 2; // 中点インデックスmを計算 - if (nums[m] < target) { - i = m + 1; // ターゲットは区間[m+1, j]にある - } else if (nums[m] > target) { - j = m - 1; // ターゲットは区間[i, m-1]にある - } else { - j = m - 1; // ターゲット未満の最初の要素は区間[i, m-1]にある - } - } - // 挿入ポイントiを返す - return i; - } - ``` - -=== "Java" - - ```java title="binary_search_insertion.java" - /* 挿入点の二分探索(重複要素あり) */ - int binarySearchInsertion(int[] nums, int target) { - int i = 0, j = nums.length - 1; // 両端閉区間 [0, n-1] を初期化 + int i = 0, j = nums.size() - 1; // 両閉区間 [0, n-1] を初期化 while (i <= j) { int m = i + (j - i) / 2; // 中点インデックス m を計算 if (nums[m] < target) { @@ -259,7 +410,28 @@ comments: true j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある } } - // 挿入点 i を返す + // 挿入位置 i を返す + return i; + } + ``` + +=== "Java" + + ```java title="binary_search_insertion.java" + /* 二分探索で挿入位置を探す(重複要素あり) */ + int binarySearchInsertion(int[] nums, int target) { + int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある + } + } + // 挿入位置 i を返す return i; } ``` @@ -267,67 +439,236 @@ comments: true === "C#" ```csharp title="binary_search_insertion.cs" - [class]{binary_search_insertion}-[func]{BinarySearchInsertion} + /* 二分探索で挿入位置を探す(重複要素あり) */ + int BinarySearchInsertion(int[] nums, int target) { + int i = 0, j = nums.Length - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある + } + } + // 挿入位置 i を返す + return i; + } ``` === "Go" ```go title="binary_search_insertion.go" - [class]{}-[func]{binarySearchInsertion} + /* 二分探索で挿入位置を探す(重複要素あり) */ + func binarySearchInsertion(nums []int, target int) int { + // 両閉区間 [0, n-1] を初期化 + i, j := 0, len(nums)-1 + for i <= j { + // 中点インデックス m を計算 + m := i + (j-i)/2 + if nums[m] < target { + // target は区間 [m+1, j] にある + i = m + 1 + } else if nums[m] > target { + // target は区間 [i, m-1] にある + j = m - 1 + } else { + // target より小さい最初の要素は区間 [i, m-1] にある + j = m - 1 + } + } + // 挿入位置 i を返す + return i + } ``` === "Swift" ```swift title="binary_search_insertion.swift" - [class]{}-[func]{binarySearchInsertion} + /* 二分探索で挿入位置を探す(重複要素あり) */ + func binarySearchInsertion(nums: [Int], target: Int) -> Int { + // 両閉区間 [0, n-1] を初期化 + var i = nums.startIndex + var j = nums.endIndex - 1 + while i <= j { + let m = i + (j - i) / 2 // 中点インデックス m を計算 + if nums[m] < target { + i = m + 1 // target は区間 [m+1, j] にある + } else if nums[m] > target { + j = m - 1 // target は区間 [i, m-1] にある + } else { + j = m - 1 // target より小さい最初の要素は区間 [i, m-1] にある + } + } + // 挿入位置 i を返す + return i + } ``` === "JS" ```javascript title="binary_search_insertion.js" - [class]{}-[func]{binarySearchInsertion} + /* 二分探索で挿入位置を探す(重複要素あり) */ + function binarySearchInsertion(nums, target) { + let i = 0, + j = nums.length - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある + } + } + // 挿入位置 i を返す + return i; + } ``` === "TS" ```typescript title="binary_search_insertion.ts" - [class]{}-[func]{binarySearchInsertion} + /* 二分探索で挿入位置を探す(重複要素あり) */ + function binarySearchInsertion(nums: Array, target: number): number { + let i = 0, + j = nums.length - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある + } + } + // 挿入位置 i を返す + return i; + } ``` === "Dart" ```dart title="binary_search_insertion.dart" - [class]{}-[func]{binarySearchInsertion} + /* 二分探索で挿入位置を探す(重複要素あり) */ + int binarySearchInsertion(List nums, int target) { + int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + int m = i + (j - i) ~/ 2; // 中点インデックス m を計算 + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある + } + } + // 挿入位置 i を返す + return i; + } ``` === "Rust" ```rust title="binary_search_insertion.rs" - [class]{}-[func]{binary_search_insertion} + /* 二分探索で挿入位置を探す(重複要素あり) */ + pub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 { + let (mut i, mut j) = (0, nums.len() as i32 - 1); // 両閉区間 [0, n-1] を初期化 + while i <= j { + let m = i + (j - i) / 2; // 中点インデックス m を計算 + if nums[m as usize] < target { + i = m + 1; // target は区間 [m+1, j] にある + } else if nums[m as usize] > target { + j = m - 1; // target は区間 [i, m-1] にある + } else { + j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある + } + } + // 挿入位置 i を返す + i + } ``` === "C" ```c title="binary_search_insertion.c" - [class]{}-[func]{binarySearchInsertion} + /* 二分探索で挿入位置を探す(重複要素あり) */ + int binarySearchInsertion(int *nums, int numSize, int target) { + int i = 0, j = numSize - 1; // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + int m = i + (j - i) / 2; // 中点インデックス m を計算 + if (nums[m] < target) { + i = m + 1; // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1; // target は区間 [i, m-1] にある + } else { + j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある + } + } + // 挿入位置 i を返す + return i; + } ``` === "Kotlin" ```kotlin title="binary_search_insertion.kt" - [class]{}-[func]{binarySearchInsertion} + /* 二分探索で挿入位置を探す(重複要素あり) */ + fun binarySearchInsertion(nums: IntArray, target: Int): Int { + var i = 0 + var j = nums.size - 1 // 両閉区間 [0, n-1] を初期化 + while (i <= j) { + val m = i + (j - i) / 2 // 中点インデックス m を計算 + if (nums[m] < target) { + i = m + 1 // target は区間 [m+1, j] にある + } else if (nums[m] > target) { + j = m - 1 // target は区間 [i, m-1] にある + } else { + j = m - 1 // target より小さい最初の要素は区間 [i, m-1] にある + } + } + // 挿入位置 i を返す + return i + } ``` === "Ruby" ```ruby title="binary_search_insertion.rb" - [class]{}-[func]{binary_search_insertion} + ### 二分探索の挿入位置(重複要素あり) ### + def binary_search_insertion(nums, target) + # 両閉区間 [0, n-1] を初期化 + i, j = 0, nums.length - 1 + + while i <= j + # 中点インデックス m を計算 + m = (i + j) / 2 + + if nums[m] < target + i = m + 1 # target は区間 [m+1, j] にある + elsif nums[m] > target + j = m - 1 # target は区間 [i, m-1] にある + else + j = m - 1 # target より小さい最初の要素は区間 [i, m-1] にある + end + end + + i # 挿入位置 i を返す + end ``` +??? pythontutor "コードの可視化" + +
+ + !!! tip - このセクションのコードは「閉区間」を使用しています。「左閉右開」に興味がある場合は、自分でコードを実装してみてください。 + 本節のコードはすべて「両閉区間」の書き方です。興味のある読者は「左閉右開」の書き方を自分で実装してみてください。 -要約すると、二分探索は本質的にポインタ$i$と$j$の探索目標を設定することです。これらの目標は特定の要素(`target`など)または要素の範囲(`target`より小さいものなど)である可能性があります。 +要するに、二分探索とはポインタ $i$ と $j$ にそれぞれ探索目標を設定することにほかなりません。目標は具体的な要素(たとえば `target`)である場合もあれば、要素の範囲(たとえば `target` より小さい要素)である場合もあります。 -二分探索の連続ループにおいて、ポインタ$i$と$j$は段階的に事前定義された目標に近づきます。最終的に、それらは答えを見つけるか、境界を越えた後に停止します。 +繰り返される二分のループの中で、ポインタ $i$ と $j$ はどちらも事前に定めた目標へ徐々に近づいていきます。最終的に、それらは答えを見つけるか、境界を越えたところで停止します。 diff --git a/ja/docs/chapter_searching/index.md b/ja/docs/chapter_searching/index.md index db26bd581..d550a9a38 100644 --- a/ja/docs/chapter_searching/index.md +++ b/ja/docs/chapter_searching/index.md @@ -5,19 +5,19 @@ icon: material/text-search # 第 10 章   探索 -![Searching](../assets/covers/chapter_searching.jpg){ class="cover-image" } +![探索](../assets/covers/chapter_searching.jpg){ class="cover-image" } !!! abstract - 探索は未知への冒険です。神秘的な空間の隅々まで巡る必要があるかもしれませんし、あるいはすぐに目標を見つけることができるかもしれません。 - - この発見の旅において、それぞれの探査は予期しない答えで終わるかもしれません。 + 探索は未知の冒険であり、私たちは神秘的な空間の隅々まで歩き回る必要があるかもしれず、あるいは素早く目標を特定できるかもしれません。 + + この探索の旅において、すべての探求が思いもよらなかった答えをもたらすかもしれません。 ## 章の内容 - [10.1   二分探索](binary_search.md) -- [10.2   二分探索の挿入点](binary_search_insertion.md) +- [10.2   二分探索の挿入位置](binary_search_insertion.md) - [10.3   二分探索の境界](binary_search_edge.md) -- [10.4   ハッシュ最適化戦略](replace_linear_by_hashing.md) -- [10.5   探索アルゴリズムの再認識](searching_algorithm_revisited.md) +- [10.4   ハッシュによる最適化戦略](replace_linear_by_hashing.md) +- [10.5   探索アルゴリズム再考](searching_algorithm_revisited.md) - [10.6   まとめ](summary.md) diff --git a/ja/docs/chapter_searching/replace_linear_by_hashing.md b/ja/docs/chapter_searching/replace_linear_by_hashing.md index 7756a3969..19ef07bd6 100644 --- a/ja/docs/chapter_searching/replace_linear_by_hashing.md +++ b/ja/docs/chapter_searching/replace_linear_by_hashing.md @@ -2,30 +2,30 @@ comments: true --- -# 10.4   ハッシュ最適化戦略 +# 10.4   ハッシュによる最適化戦略 -アルゴリズム問題において、**線形探索をハッシュベースの探索に置き換えることで、アルゴリズムの時間計算量を削減することがよくあります**。アルゴリズム問題を使用して理解を深めましょう。 +アルゴリズムの問題では,**線形探索をハッシュ探索に置き換えることでアルゴリズムの時間計算量を下げることがよくあります**。ここでは,あるアルゴリズム問題を通じて理解を深めましょう。 !!! question - 整数配列`nums`と目標要素`target`が与えられ、配列内で「和」が`target`に等しい2つの要素を探索し、それらの配列インデックスを返してください。任意の解が受け入れられます。 + 整数配列 `nums` と目標要素 `target` が与えられたとき,配列内から和が `target` となる 2 つの要素を探索し,それらの配列インデックスを返してください。任意の 1 つの解を返せば十分です。 -## 10.4.1   線形探索:時間を空間と交換 +## 10.4.1   線形探索:時間と引き換えに空間を節約 -すべての可能な組み合わせを直接横断することを考えてみます。下図に示すように、ネストしたループを開始し、各反復で2つの整数の和が`target`に等しいかどうかを判断します。そうであれば、それらのインデックスを返します。 +考えられるすべての組み合わせを直接走査することを考えます。次の図に示すように,2 重ループを開始し,各ラウンドで 2 つの整数の和が `target` であるかを判定します。そうであれば,それらのインデックスを返します。 -![Linear search solution for two-sum problem](replace_linear_by_hashing.assets/two_sum_brute_force.png){ class="animation-figure" } +![線形探索で 2 数の和を求める](replace_linear_by_hashing.assets/two_sum_brute_force.png){ class="animation-figure" } -

図 10-9   Linear search solution for two-sum problem

+

図 10-9   線形探索で 2 数の和を求める

-コードは以下の通りです: +コードは次のとおりです: === "Python" ```python title="two_sum.py" def two_sum_brute_force(nums: list[int], target: int) -> list[int]: - """方法一:ブルートフォース列挙""" - # 二重ループ、時間計算量は O(n^2) + """方法 1:総当たり列挙""" + # 2重ループのため、時間計算量は O(n^2) for i in range(len(nums) - 1): for j in range(i + 1, len(nums)): if nums[i] + nums[j] == target: @@ -36,10 +36,10 @@ comments: true === "C++" ```cpp title="two_sum.cpp" - /* 方法一:ブルートフォース列挙 */ + /* 方法 1:総当たり列挙 */ vector twoSumBruteForce(vector &nums, int target) { int size = nums.size(); - // 二重ループ、時間計算量はO(n^2) + // 2重ループのため、時間計算量は O(n^2) for (int i = 0; i < size - 1; i++) { for (int j = i + 1; j < size; j++) { if (nums[i] + nums[j] == target) @@ -53,10 +53,10 @@ comments: true === "Java" ```java title="two_sum.java" - /* 方法一: 暴力列挙 */ + /* 方法 1:総当たり列挙 */ int[] twoSumBruteForce(int[] nums, int target) { int size = nums.length; - // 二重ループ、時間計算量は O(n^2) + // 2重ループのため、時間計算量は O(n^2) for (int i = 0; i < size - 1; i++) { for (int j = i + 1; j < size; j++) { if (nums[i] + nums[j] == target) @@ -70,74 +70,193 @@ comments: true === "C#" ```csharp title="two_sum.cs" - [class]{two_sum}-[func]{TwoSumBruteForce} + /* 方法 1:総当たり列挙 */ + int[] TwoSumBruteForce(int[] nums, int target) { + int size = nums.Length; + // 2重ループのため、時間計算量は O(n^2) + for (int i = 0; i < size - 1; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) + return [i, j]; + } + } + return []; + } ``` === "Go" ```go title="two_sum.go" - [class]{}-[func]{twoSumBruteForce} + /* 方法 1:総当たり列挙 */ + func twoSumBruteForce(nums []int, target int) []int { + size := len(nums) + // 2重ループのため、時間計算量は O(n^2) + for i := 0; i < size-1; i++ { + for j := i + 1; j < size; j++ { + if nums[i]+nums[j] == target { + return []int{i, j} + } + } + } + return nil + } ``` === "Swift" ```swift title="two_sum.swift" - [class]{}-[func]{twoSumBruteForce} + /* 方法 1:総当たり列挙 */ + func twoSumBruteForce(nums: [Int], target: Int) -> [Int] { + // 2重ループのため、時間計算量は O(n^2) + for i in nums.indices.dropLast() { + for j in nums.indices.dropFirst(i + 1) { + if nums[i] + nums[j] == target { + return [i, j] + } + } + } + return [0] + } ``` === "JS" ```javascript title="two_sum.js" - [class]{}-[func]{twoSumBruteForce} + /* 方法 1:総当たり列挙 */ + function twoSumBruteForce(nums, target) { + const n = nums.length; + // 2重ループのため、時間計算量は O(n^2) + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + if (nums[i] + nums[j] === target) { + return [i, j]; + } + } + } + return []; + } ``` === "TS" ```typescript title="two_sum.ts" - [class]{}-[func]{twoSumBruteForce} + /* 方法 1:総当たり列挙 */ + function twoSumBruteForce(nums: number[], target: number): number[] { + const n = nums.length; + // 2重ループのため、時間計算量は O(n^2) + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + if (nums[i] + nums[j] === target) { + return [i, j]; + } + } + } + return []; + } ``` === "Dart" ```dart title="two_sum.dart" - [class]{}-[func]{twoSumBruteForce} + /* 方法1: 総当たり列挙 */ + List twoSumBruteForce(List nums, int target) { + int size = nums.length; + // 2重ループのため、時間計算量は O(n^2) + for (var i = 0; i < size - 1; i++) { + for (var j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) return [i, j]; + } + } + return [0]; + } ``` === "Rust" ```rust title="two_sum.rs" - [class]{}-[func]{two_sum_brute_force} + /* 方法 1:総当たり列挙 */ + pub fn two_sum_brute_force(nums: &Vec, target: i32) -> Option> { + let size = nums.len(); + // 2重ループのため、時間計算量は O(n^2) + for i in 0..size - 1 { + for j in i + 1..size { + if nums[i] + nums[j] == target { + return Some(vec![i as i32, j as i32]); + } + } + } + None + } ``` === "C" ```c title="two_sum.c" - [class]{}-[func]{twoSumBruteForce} + /* 方法 1:総当たり列挙 */ + int *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) { + for (int i = 0; i < numsSize; ++i) { + for (int j = i + 1; j < numsSize; ++j) { + if (nums[i] + nums[j] == target) { + int *res = malloc(sizeof(int) * 2); + res[0] = i, res[1] = j; + *returnSize = 2; + return res; + } + } + } + *returnSize = 0; + return NULL; + } ``` === "Kotlin" ```kotlin title="two_sum.kt" - [class]{}-[func]{twoSumBruteForce} + /* 方法 1:総当たり列挙 */ + fun twoSumBruteForce(nums: IntArray, target: Int): IntArray { + val size = nums.size + // 2重ループのため、時間計算量は O(n^2) + for (i in 0.. + -ハッシュテーブルの使用を考えてみましょう。キーと値のペアはそれぞれ配列要素とそのインデックスです。配列をループし、各反復中に下図に示すステップを実行します。 +この方法の時間計算量は $O(n^2)$ ,空間計算量は $O(1)$ であり,大規模データでは非常に時間がかかります。 -1. 数値`target - nums[i]`がハッシュテーブルにあるかどうかを確認します。ある場合は、これら2つの要素のインデックスを直接返します。 -2. キーと値のペア`nums[i]`とインデックス`i`をハッシュテーブルに追加します。 +## 10.4.2   ハッシュ探索:空間と引き換えに時間を節約 + +ハッシュテーブルを利用し,キーと値をそれぞれ配列要素と要素のインデックスにします。配列をループで走査し,各ラウンドで次の図に示す手順を実行します。 + +1. 数値 `target - nums[i]` がハッシュテーブル内にあるかを判定します。あれば,この 2 つの要素のインデックスを直接返します。 +2. キーと値の組 `nums[i]` とインデックス `i` をハッシュテーブルに追加します。 === "<1>" - ![Help hash table solve two-sum](replace_linear_by_hashing.assets/two_sum_hashtable_step1.png){ class="animation-figure" } + ![補助ハッシュテーブルで 2 数の和を求める](replace_linear_by_hashing.assets/two_sum_hashtable_step1.png){ class="animation-figure" } === "<2>" ![two_sum_hashtable_step2](replace_linear_by_hashing.assets/two_sum_hashtable_step2.png){ class="animation-figure" } @@ -145,18 +264,18 @@ comments: true === "<3>" ![two_sum_hashtable_step3](replace_linear_by_hashing.assets/two_sum_hashtable_step3.png){ class="animation-figure" } -

図 10-10   Help hash table solve two-sum

+

図 10-10   補助ハッシュテーブルで 2 数の和を求める

-実装コードは以下に示され、単一のループのみが必要です: +実装コードは次のとおりで,単一ループだけで済みます: === "Python" ```python title="two_sum.py" def two_sum_hash_table(nums: list[int], target: int) -> list[int]: - """方法二:補助ハッシュテーブル""" - # 補助ハッシュテーブル、空間計算量は O(n) + """方法 2:補助ハッシュテーブル""" + # 補助ハッシュテーブルを使用し、空間計算量は O(n) dic = {} - # 単一ループ、時間計算量は O(n) + # 単一ループで、時間計算量は O(n) for i in range(len(nums)): if target - nums[i] in dic: return [dic[target - nums[i]], i] @@ -167,12 +286,12 @@ comments: true === "C++" ```cpp title="two_sum.cpp" - /* 方法二:補助ハッシュテーブル */ + /* 方法 2:補助ハッシュテーブル */ vector twoSumHashTable(vector &nums, int target) { int size = nums.size(); - // 補助ハッシュテーブル、空間計算量はO(n) + // 補助ハッシュテーブルを使用し、空間計算量は O(n) unordered_map dic; - // 単層ループ、時間計算量はO(n) + // 単一ループで、時間計算量は O(n) for (int i = 0; i < size; i++) { if (dic.find(target - nums[i]) != dic.end()) { return {dic[target - nums[i]], i}; @@ -186,12 +305,12 @@ comments: true === "Java" ```java title="two_sum.java" - /* 方法二: 補助ハッシュテーブル */ + /* 方法 2:補助ハッシュテーブル */ int[] twoSumHashTable(int[] nums, int target) { int size = nums.length; - // 補助ハッシュテーブル、空間計算量は O(n) + // 補助ハッシュテーブルを使用し、空間計算量は O(n) Map dic = new HashMap<>(); - // 単一層ループ、時間計算量は O(n) + // 単一ループで、時間計算量は O(n) for (int i = 0; i < size; i++) { if (dic.containsKey(target - nums[i])) { return new int[] { dic.get(target - nums[i]), i }; @@ -205,65 +324,223 @@ comments: true === "C#" ```csharp title="two_sum.cs" - [class]{two_sum}-[func]{TwoSumHashTable} + /* 方法 2:補助ハッシュテーブル */ + int[] TwoSumHashTable(int[] nums, int target) { + int size = nums.Length; + // 補助ハッシュテーブルを使用し、空間計算量は O(n) + Dictionary dic = []; + // 単一ループで、時間計算量は O(n) + for (int i = 0; i < size; i++) { + if (dic.ContainsKey(target - nums[i])) { + return [dic[target - nums[i]], i]; + } + dic.Add(nums[i], i); + } + return []; + } ``` === "Go" ```go title="two_sum.go" - [class]{}-[func]{twoSumHashTable} + /* 方法 2:補助ハッシュテーブル */ + func twoSumHashTable(nums []int, target int) []int { + // 補助ハッシュテーブルを使用し、空間計算量は O(n) + hashTable := map[int]int{} + // 単一ループで、時間計算量は O(n) + for idx, val := range nums { + if preIdx, ok := hashTable[target-val]; ok { + return []int{preIdx, idx} + } + hashTable[val] = idx + } + return nil + } ``` === "Swift" ```swift title="two_sum.swift" - [class]{}-[func]{twoSumHashTable} + /* 方法 2:補助ハッシュテーブル */ + func twoSumHashTable(nums: [Int], target: Int) -> [Int] { + // 補助ハッシュテーブルを使用し、空間計算量は O(n) + var dic: [Int: Int] = [:] + // 単一ループで、時間計算量は O(n) + for i in nums.indices { + if let j = dic[target - nums[i]] { + return [j, i] + } + dic[nums[i]] = i + } + return [0] + } ``` === "JS" ```javascript title="two_sum.js" - [class]{}-[func]{twoSumHashTable} + /* 方法 2:補助ハッシュテーブル */ + function twoSumHashTable(nums, target) { + // 補助ハッシュテーブルを使用し、空間計算量は O(n) + let m = {}; + // 単一ループで、時間計算量は O(n) + for (let i = 0; i < nums.length; i++) { + if (m[target - nums[i]] !== undefined) { + return [m[target - nums[i]], i]; + } else { + m[nums[i]] = i; + } + } + return []; + } ``` === "TS" ```typescript title="two_sum.ts" - [class]{}-[func]{twoSumHashTable} + /* 方法 2:補助ハッシュテーブル */ + function twoSumHashTable(nums: number[], target: number): number[] { + // 補助ハッシュテーブルを使用し、空間計算量は O(n) + let m: Map = new Map(); + // 単一ループで、時間計算量は O(n) + for (let i = 0; i < nums.length; i++) { + let index = m.get(target - nums[i]); + if (index !== undefined) { + return [index, i]; + } else { + m.set(nums[i], i); + } + } + return []; + } ``` === "Dart" ```dart title="two_sum.dart" - [class]{}-[func]{twoSumHashTable} + /* 方法2: 補助ハッシュテーブル */ + List twoSumHashTable(List nums, int target) { + int size = nums.length; + // 補助ハッシュテーブルを使用し、空間計算量は O(n) + Map dic = HashMap(); + // 単一ループで、時間計算量は O(n) + for (var i = 0; i < size; i++) { + if (dic.containsKey(target - nums[i])) { + return [dic[target - nums[i]]!, i]; + } + dic.putIfAbsent(nums[i], () => i); + } + return [0]; + } ``` === "Rust" ```rust title="two_sum.rs" - [class]{}-[func]{two_sum_hash_table} + /* 方法 2:補助ハッシュテーブル */ + pub fn two_sum_hash_table(nums: &Vec, target: i32) -> Option> { + // 補助ハッシュテーブルを使用し、空間計算量は O(n) + let mut dic = HashMap::new(); + // 単一ループで、時間計算量は O(n) + for (i, num) in nums.iter().enumerate() { + match dic.get(&(target - num)) { + Some(v) => return Some(vec![*v as i32, i as i32]), + None => dic.insert(num, i as i32), + }; + } + None + } ``` === "C" ```c title="two_sum.c" - [class]{HashTable}-[func]{} + /* ハッシュテーブル */ + typedef struct { + int key; + int val; + UT_hash_handle hh; // uthash.h を用いて実装 + } HashTable; - [class]{}-[func]{twoSumHashTable} + /* ハッシュテーブルを検索する */ + HashTable *find(HashTable *h, int key) { + HashTable *tmp; + HASH_FIND_INT(h, &key, tmp); + return tmp; + } + + /* ハッシュテーブルに要素を挿入する */ + void insert(HashTable **h, int key, int val) { + HashTable *t = find(*h, key); + if (t == NULL) { + HashTable *tmp = malloc(sizeof(HashTable)); + tmp->key = key, tmp->val = val; + HASH_ADD_INT(*h, key, tmp); + } else { + t->val = val; + } + } + + /* 方法 2:補助ハッシュテーブル */ + int *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) { + HashTable *hashtable = NULL; + for (int i = 0; i < numsSize; i++) { + HashTable *t = find(hashtable, target - nums[i]); + if (t != NULL) { + int *res = malloc(sizeof(int) * 2); + res[0] = t->val, res[1] = i; + *returnSize = 2; + return res; + } + insert(&hashtable, nums[i], i); + } + *returnSize = 0; + return NULL; + } ``` === "Kotlin" ```kotlin title="two_sum.kt" - [class]{}-[func]{twoSumHashTable} + /* 方法 2:補助ハッシュテーブル */ + fun twoSumHashTable(nums: IntArray, target: Int): IntArray { + val size = nums.size + // 補助ハッシュテーブルを使用し、空間計算量は O(n) + val dic = HashMap() + // 単一ループで、時間計算量は O(n) + for (i in 0.. + + +この方法ではハッシュ探索によって時間計算量を $O(n^2)$ から $O(n)$ に下げ,実行効率を大幅に向上させます。 + +追加のハッシュテーブルを維持する必要があるため,空間計算量は $O(n)$ です。**それでも,この方法は全体として時間と空間の効率のバランスがより良く,本問の最適解です**。 diff --git a/ja/docs/chapter_searching/searching_algorithm_revisited.md b/ja/docs/chapter_searching/searching_algorithm_revisited.md index 1a61d21be..c8e681086 100644 --- a/ja/docs/chapter_searching/searching_algorithm_revisited.md +++ b/ja/docs/chapter_searching/searching_algorithm_revisited.md @@ -2,93 +2,93 @@ comments: true --- -# 10.5   探索アルゴリズムの再検討 +# 10.5   探索アルゴリズム再考 -探索アルゴリズム(検索アルゴリズム)は、配列、連結リスト、木、グラフなどのデータ構造内で特定の基準を満たす1つ以上の要素を取得するために使用されます。 +探索アルゴリズム(searching algorithm)は、データ構造(配列、連結リスト、木、グラフなど)の中から、特定の条件を満たす 1 つまたは複数の要素を探索するために用いられます。 -探索アルゴリズムは、そのアプローチに基づいて以下の2つのカテゴリに分けることができます。 +探索アルゴリズムは、実装の考え方に応じて次の 2 種類に分けられます。 -- **データ構造を横断することで目標要素を特定する**:配列、連結リスト、木、グラフの横断など。 -- **データの組織構造や既存のデータを使用して効率的な要素探索を実現する**:二分探索、ハッシュ探索、二分探索木探索など。 +- **データ構造を走査して目標要素を特定する方法**。配列、連結リスト、木、グラフの走査などがこれに当たります。 +- **データの構成やデータに含まれる事前情報を利用して、要素を効率よく探す方法**。二分探索、ハッシュ探索、二分探索木による探索などがこれに当たります。 -これらのトピックは前の章で紹介されたため、私たちには馴染みのないものではありません。このセクションでは、より体系的な観点から探索アルゴリズムを再検討します。 +これらのトピックはすでに前の章で扱っているため、探索アルゴリズムは私たちにとって見慣れたものです。本節では、より体系的な視点から探索アルゴリズムをあらためて見直します。 ## 10.5.1   総当たり探索 -総当たり探索は、データ構造のすべての要素を横断することで目標要素を特定します。 +総当たり探索は、データ構造の各要素を順に調べて目標要素を特定します。 -- 「線形探索」は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から開始し、目標要素が見つかるか、目標要素を見つけることなく他端に到達するまで、各要素に一つずつアクセスします。 -- 「幅優先探索」と「深さ優先探索」は、グラフと木の2つの横断戦略です。幅優先探索は初期ノードから開始し、層ごと(左から右へ)に探索し、近くから遠くのノードにアクセスします。深さ優先探索は初期ノードから開始し、パスの終端(上から下へ)まで追跡し、その後バックトラックして他のパスを試し、データ構造全体が横断されるまで続行します。 +- “線形探索”は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から始めて、要素を 1 つずつ調べ、目標要素が見つかるか、もう一方の端に達しても見つからないまで続けます。 +- “幅優先探索”と“深さ優先探索”は、グラフと木における 2 つの走査戦略です。幅優先探索は初期ノードから始めて層ごとに探索し、近いところから遠いところへ各ノードを訪れます。深さ優先探索は初期ノードから始めて 1 本の経路を最後までたどり、その後でバックトラックしてほかの経路を試し、データ構造全体を走査し終えるまで続けます。 -総当たり探索の利点は、その単純さと汎用性であり、**データの前処理や追加のデータ構造の助けが不要**です。 +総当たり探索の利点は、単純で汎用性が高く、**データの前処理や追加のデータ構造を必要としない**ことです。 -ただし、**このタイプのアルゴリズムの時間計算量は$O(n)$**で、$n$は要素数であるため、大規模なデータセットでは性能が悪くなります。 +しかし、**この種のアルゴリズムの時間計算量は $O(n)$ です**。ここで $n$ は要素数であり、そのためデータ量が大きい場合は性能が低くなります。 -## 10.5.2   適応的探索 +## 10.5.2   適応的な探索 -適応的探索は、データの固有の性質(順序など)を使用して探索プロセスを最適化し、それにより目標要素をより効率的に特定します。 +適応的な探索は、データが持つ固有の性質(整列性など)を利用して探索過程を最適化し、目標要素をより効率よく特定します。 -- 「二分探索」はデータの整列性を使用して効率的な探索を実現し、配列にのみ適用可能です。 -- 「ハッシュ探索」はハッシュテーブルを使用して探索データと目標データの間にキーと値のマッピングを確立し、それによりクエリ操作を実装します。 -- 特定の木構造(二分探索木など)での「木探索」は、ノード値の比較に基づいてノードを迅速に除外し、それにより目標要素を特定します。 +- “二分探索”は、データの順序性を利用して効率的な探索を行う方法で、配列にしか適用できません。 +- “ハッシュ探索”は、ハッシュ表を用いて探索対象のデータと目標データをキーと値の対応にし、問い合わせ操作を実現します。 +- “木探索”は、特定の木構造(たとえば二分探索木)の中で、ノード値の比較に基づいて不要なノードをすばやく除外し、目標要素を特定します。 -これらのアルゴリズムの利点は高効率であり、**時間計算量が$O(\log n)$または$O(1)$にまで達します**。 +この種のアルゴリズムの利点は効率が高く、**時間計算量が $O(\log n)$ あるいは $O(1)$ に達する**ことです。 -ただし、**これらのアルゴリズムを使用するには、多くの場合データの前処理が必要です**。例えば、二分探索では事前に配列をソートする必要があり、ハッシュ探索と木探索の両方で追加のデータ構造の助けが必要です。これらの構造を維持することも、時間と空間の面でより多くのオーバーヘッドが必要です。 +しかし、**これらのアルゴリズムを使うには、たいていデータの前処理が必要です**。たとえば、二分探索では事前に配列をソートする必要があり、ハッシュ探索と木探索では追加のデータ構造が必要です。これらのデータ構造を維持するにも、追加の時間と空間のコストがかかります。 !!! tip - 適応的探索アルゴリズムは、多くの場合探索アルゴリズムと呼ばれ、**主に特定のデータ構造内で目標要素を迅速に取得するために使用されます**。 + 適応的な探索アルゴリズムは、しばしば検索アルゴリズムとも呼ばれ、**主に特定のデータ構造の中で目標要素を高速に取得するために用いられます**。 -## 10.5.3   探索方法の選択 +## 10.5.3   探索手法の選択 -サイズ$n$のデータセットが与えられた場合、線形探索、二分探索、木探索、ハッシュ探索、またはその他の方法を使用して目標要素を取得できます。これらの方法の動作原理を下図に示します。 +大きさ $n$ のデータ集合が与えられたとき、線形探索、二分探索、木探索、ハッシュ探索など、さまざまな方法で目標要素を探索できます。各手法の動作原理を下図に示します。 -![Various search strategies](searching_algorithm_revisited.assets/searching_algorithms.png){ class="animation-figure" } +![複数の探索戦略](searching_algorithm_revisited.assets/searching_algorithms.png){ class="animation-figure" } -

図 10-11   Various search strategies

+

図 10-11   複数の探索戦略

-前述の方法の特性と操作効率を以下の表に示します。 +上記のいくつかの手法について、操作効率と特性を次の表に示します。 -

表 10-1   探索アルゴリズム効率の比較

+

表 10-1   探索アルゴリズムの効率比較

-| | 線形探索 | 二分探索 | 木探索 | ハッシュ探索 | -| ------------------ | ------------- | --------------------- | --------------------------- | -------------------------- | -| 要素探索 | $O(n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$ | -| 要素挿入 | $O(1)$ | $O(n)$ | $O(\log n)$ | $O(1)$ | -| 要素削除 | $O(n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ | -| 追加空間 | $O(1)$ | $O(1)$ | $O(n)$ | $O(n)$ | -| データ前処理 | / | ソート $O(n \log n)$ | 木構築 $O(n \log n)$ | ハッシュテーブル構築 $O(n)$ | -| データ順序性 | 無順序 | 順序 | 順序 | 無順序 | +| | 線形探索 | 二分探索 | 木探索 | ハッシュ探索 | +| ------------ | -------- | ------------------ | ------------------ | --------------- | +| 要素探索 | $O(n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$ | +| 要素挿入 | $O(1)$ | $O(n)$ | $O(\log n)$ | $O(1)$ | +| 要素削除 | $O(n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ | +| 追加領域 | $O(1)$ | $O(1)$ | $O(n)$ | $O(n)$ | +| データ前処理 | / | ソート $O(n \log n)$ | 木構築 $O(n \log n)$ | ハッシュ表構築 $O(n)$ | +| データの順序性 | なし | あり | あり | なし |
-探索アルゴリズムの選択は、データ量、探索性能要件、データクエリと更新の頻度などにも依存します。 +探索アルゴリズムの選択は、規模、探索性能の要求、データの問い合わせ頻度や更新頻度などにも左右されます。 **線形探索** -- 汎用性が良く、データ前処理操作が不要です。データを一度だけクエリする必要がある場合、他の3つの方法のデータ前処理時間は線形探索の時間よりも長くなります。 -- 小容量のデータに適しており、時間計算量が効率に与える影響は小さいです。 -- データ更新が非常に頻繁なシナリオに適しています。この方法はデータの追加メンテナンスを必要としないためです。 +- 汎用性が高く、データの前処理をまったく必要としません。データを 1 回だけ問い合わせればよい場合、ほか 3 つの手法では前処理にかかる時間のほうが、線形探索そのものより長くなることがあります。 +- 規模の小さいデータに適しています。この場合、時間計算量が効率に与える影響は比較的小さいです。 +- データ更新頻度が高い場面に適しています。この手法では、データに対する追加の保守が不要だからです。 **二分探索** -- より大きなデータ量に適しており、安定した性能と最悪ケースの時間計算量$O(\log n)$を持ちます。 -- ただし、データ量が大きすぎることはできません。配列の保存には連続したメモリ空間が必要だからです。 -- 頻繁な追加と削除があるシナリオには適していません。順序付き配列の維持に多くのオーバーヘッドが発生するためです。 +- 大規模データに適しており、効率が安定しています。最悪時間計算量は $O(\log n)$ です。 +- データ量が大きすぎる場合には向きません。配列の格納には連続したメモリ領域が必要だからです。 +- 頻繁な挿入・削除がある場面には向きません。整列配列を維持するコストが高いためです。 **ハッシュ探索** -- 高速クエリ性能が不可欠なシナリオに適しており、平均時間計算量は$O(1)$です。 -- 順序付きデータや範囲探索が必要なシナリオには適していません。ハッシュテーブルはデータの順序性を維持できないためです。 +- 問い合わせ性能への要求が高い場面に適しており、平均時間計算量は $O(1)$ です。 +- 順序付きデータや範囲探索が必要な場面には向きません。ハッシュ表ではデータの順序性を維持できないからです。 - ハッシュ関数とハッシュ衝突処理戦略への依存度が高く、性能劣化のリスクが大きいです。 -- 過度に大容量のデータには適していません。ハッシュテーブルは衝突を最小化し、良好なクエリ性能を提供するために追加の空間が必要だからです。 +- データ量が大きすぎる場合には向きません。ハッシュ表は衝突をできるだけ減らして良好な問い合わせ性能を出すために、追加の空間を必要とするからです。 **木探索** -- 大容量データに適しています。木ノードはメモリ内に分散して保存されるためです。 -- 順序付きデータの維持や範囲探索に適しています。 -- ノードの継続的な追加と削除により、二分探索木は偏る可能性があり、時間計算量が$O(n)$に劣化する可能性があります。 -- AVL木や赤黒木を使用する場合、操作は$O(\log n)$効率で安定して実行できますが、木のバランスを維持する操作により追加のオーバーヘッドが追加されます。 +- 巨大データに適しています。木ノードはメモリ上に分散して格納されるためです。 +- 順序付きデータの維持や範囲探索が必要な場面に適しています。 +- ノードの挿入・削除を続ける過程で、二分探索木は偏ることがあり、時間計算量は $O(n)$ まで劣化する可能性があります。 +- AVL 木や赤黒木を使えば、各種操作を $O(\log n)$ の効率で安定して実行できますが、木の平衡を保つ処理による追加コストが発生します。 diff --git a/ja/docs/chapter_searching/summary.md b/ja/docs/chapter_searching/summary.md index 7e394511d..0ec0cf141 100644 --- a/ja/docs/chapter_searching/summary.md +++ b/ja/docs/chapter_searching/summary.md @@ -4,9 +4,11 @@ comments: true # 10.6   まとめ -- 二分探索はデータの順序に依存し、探索区間を反復的に半分にすることで探索を実行します。入力データがソート済みである必要があり、配列または配列ベースのデータ構造にのみ適用可能です。 -- 無順序データセット内のエントリを見つけるには、総当たり探索が必要な場合があります。データ構造に基づいて異なる探索アルゴリズムを適用できます:線形探索は配列と連結リストに適しており、幅優先探索(BFS)と深さ優先探索(DFS)はグラフと木に適しています。これらのアルゴリズムは非常に汎用性が高く、データの前処理が不要ですが、$O(n)$という高い時間計算量を持ちます。 -- ハッシュ探索、木探索、二分探索は効率的な探索方法で、特定のデータ構造内で目標要素を迅速に特定できます。これらのアルゴリズムは非常に効率的で、時間計算量が$O(\log n)$または$O(1)$にまで達しますが、通常は追加のデータ構造を収容するために追加の空間が必要です。 -- 実際には、データ量、探索性能要件、データクエリと更新頻度などの要因を分析して、適切な探索方法を選択する必要があります。 -- 線形探索は小さなデータや頻繁に更新される(変動性の高い)データに理想的です。二分探索は大きくてソート済みのデータに適しています。ハッシュ探索は高いクエリ効率が必要で範囲クエリが不要なデータに適しています。木探索は順序を維持し、範囲クエリをサポートする必要がある大きな動的データに最も適しています。 -- 線形探索をハッシュ探索に置き換えることは、実行時性能を最適化する一般的な戦略で、時間計算量を$O(n)$から$O(1)$に削減します。 +### 1.   要点の振り返り + +- 二分探索はデータの順序性に依存し、ループによって探索区間を半分ずつ縮小しながら探索を行う。入力データがソート済みであることを前提とし、配列または配列ベースで実装されたデータ構造にのみ適用できる。 +- 総当たり探索はデータ構造を走査してデータを特定する。線形探索は配列と連結リストに適しており、幅優先探索と深さ優先探索はグラフと木に適している。この種のアルゴリズムは汎用性が高く、データの前処理を必要としないが、時間計算量 $O(n)$ は高い。 +- ハッシュ探索、木探索、二分探索は高効率な探索手法であり、特定のデータ構造内で目的の要素を高速に特定できる。この種のアルゴリズムは効率が高く、時間計算量は $O(\log n)$ あるいは $O(1)$ に達するが、通常は追加のデータ構造を必要とする。 +- 実際には、データ規模、探索性能の要件、データの問い合わせ頻度や更新頻度などの要因を具体的に分析し、そのうえで適切な探索手法を選択する必要がある。 +- 線形探索は小規模または頻繁に更新されるデータに適している。二分探索は大規模でソート済みのデータに適している。ハッシュ探索は問い合わせ効率への要求が高く、範囲検索を必要としないデータに適している。木探索は順序の維持と範囲検索のサポートが必要な大規模動的データに適している。 +- ハッシュ探索で線形探索を置き換えることは、実行時間を最適化するための一般的な戦略であり、時間計算量を $O(n)$ から $O(1)$ へと下げられる。 diff --git a/ja/docs/chapter_sorting/bubble_sort.md b/ja/docs/chapter_sorting/bubble_sort.md index 75cffe648..b8ca4b6e1 100644 --- a/ja/docs/chapter_sorting/bubble_sort.md +++ b/ja/docs/chapter_sorting/bubble_sort.md @@ -4,12 +4,12 @@ comments: true # 11.3   バブルソート -バブルソートは、隣接する要素を継続的に比較し交換することで動作します。このプロセスは泡が底から上に上昇するようなものなので、「バブルソート」と名付けられました。 +バブルソート(bubble sort)は、隣接する要素を繰り返し比較して交換することで整列を行います。この過程が泡のように下から上へ浮かび上がることから、バブルソートと呼ばれます。 -下図に示すように、バブリングプロセスは要素交換を使用してシミュレートできます:配列の左端から開始して右に移動し、隣接する要素の各ペアを比較します。左の要素が右の要素より大きい場合は、それらを交換します。横断後、最大要素は配列の右端にバブルアップします。 +次の図に示すように、バブル処理は要素の交換操作によってシミュレートできます。配列の最も左の端から右へ走査し、隣接する要素の大小を順に比較して、「左要素 > 右要素」であれば両者を交換します。走査が終わると、最大の要素は配列の最も右端へ移動します。 === "<1>" - ![Simulating bubble process using element swap](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" } + ![要素の交換操作でバブル処理をシミュレート](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" } === "<2>" ![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png){ class="animation-figure" } @@ -29,22 +29,22 @@ comments: true === "<7>" ![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png){ class="animation-figure" } -

図 11-4   Simulating bubble process using element swap

+

図 11-4   要素の交換操作でバブル処理をシミュレート

-## 11.3.1   アルゴリズムプロセス +## 11.3.1   アルゴリズムの流れ -配列の長さを$n$とします。バブルソートのステップは下図に示されます: +配列の長さを $n$ とすると、バブルソートの手順は次の図のとおりです。 -1. まず、$n$個の要素に対して1回の「バブル」パスを実行し、**最大要素を正しい位置に交換します**。 -2. 次に、残りの$n - 1$個の要素に対して「バブル」パスを実行し、**2番目に大きい要素を正しい位置に交換します**。 -3. この方法で続行します;$n - 1$回のパスの後、**最大$n - 1$個の要素が正しい位置に移動されます**。 -4. 残りの唯一の要素は**必ず**最小であるため、**さらなる**ソートは必要ありません。この時点で、配列はソートされます。 +1. まず、$n$ 個の要素に対して「バブル処理」を行い、**配列中の最大要素を正しい位置へ交換します**。 +2. 次に、残りの $n - 1$ 個の要素に対して「バブル処理」を行い、**2 番目に大きい要素を正しい位置へ交換します**。 +3. このようにして、$n - 1$ 回の「バブル処理」を終えると、**大きいほうから $n - 1$ 個の要素がすべて正しい位置へ交換されます**。 +4. 残った 1 つの要素は必ず最小要素なので、並べ替える必要はなく、これで配列のソートが完了します。 -![Bubble sort process](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" } +![バブルソートの流れ](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" } -

図 11-5   Bubble sort process

+

図 11-5   バブルソートの流れ

-コード例は以下の通りです: +コード例は次のとおりです。 === "Python" @@ -52,9 +52,9 @@ comments: true def bubble_sort(nums: list[int]): """バブルソート""" n = len(nums) - # 外側のループ:未ソート範囲は [0, i] + # 外側のループ:未ソート区間は [0, i] for i in range(n - 1, 0, -1): - # 内側のループ:未ソート範囲 [0, i] の最大要素を範囲の右端に移動 + # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j in range(i): if nums[j] > nums[j + 1]: # nums[j] と nums[j + 1] を交換 @@ -66,13 +66,13 @@ comments: true ```cpp title="bubble_sort.cpp" /* バブルソート */ void bubbleSort(vector &nums) { - // 外側ループ:未ソート範囲は[0, i] + // 外側のループ:未ソート区間は [0, i] for (int i = nums.size() - 1; i > 0; i--) { - // 内側ループ:未ソート範囲[0, i]内の最大要素を範囲の右端に交換 + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { - // nums[j]とnums[j + 1]を交換 - // ここではstdのswapを使用 + // nums[j] と nums[j + 1] を交換する + // ここでは std::swap() 関数を使用する swap(nums[j], nums[j + 1]); } } @@ -85,9 +85,9 @@ comments: true ```java title="bubble_sort.java" /* バブルソート */ void bubbleSort(int[] nums) { - // 外側ループ: 未ソート範囲は [0, i] + // 外側のループ:未ソート区間は [0, i] for (int i = nums.length - 1; i > 0; i--) { - // 内側ループ: 未ソート範囲 [0, i] の最大要素を範囲の右端に交換 + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // nums[j] と nums[j + 1] を交換 @@ -103,107 +103,242 @@ comments: true === "C#" ```csharp title="bubble_sort.cs" - [class]{bubble_sort}-[func]{BubbleSort} + /* バブルソート */ + void BubbleSort(int[] nums) { + // 外側のループ:未ソート区間は [0, i] + for (int i = nums.Length - 1; i > 0; i--) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]); + } + } + } + } ``` === "Go" ```go title="bubble_sort.go" - [class]{}-[func]{bubbleSort} + /* バブルソート */ + func bubbleSort(nums []int) { + // 外側のループ:未ソート区間は [0, i] + for i := len(nums) - 1; i > 0; i-- { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // nums[j] と nums[j + 1] を交換 + nums[j], nums[j+1] = nums[j+1], nums[j] + } + } + } + } ``` === "Swift" ```swift title="bubble_sort.swift" - [class]{}-[func]{bubbleSort} + /* バブルソート */ + func bubbleSort(nums: inout [Int]) { + // 外側のループ:未ソート区間は [0, i] + for i in nums.indices.dropFirst().reversed() { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // nums[j] と nums[j + 1] を交換 + nums.swapAt(j, j + 1) + } + } + } + } ``` === "JS" ```javascript title="bubble_sort.js" - [class]{}-[func]{bubbleSort} + /* バブルソート */ + function bubbleSort(nums) { + // 外側のループ:未ソート区間は [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } ``` === "TS" ```typescript title="bubble_sort.ts" - [class]{}-[func]{bubbleSort} + /* バブルソート */ + function bubbleSort(nums: number[]): void { + // 外側のループ:未ソート区間は [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } ``` === "Dart" ```dart title="bubble_sort.dart" - [class]{}-[func]{bubbleSort} + /* バブルソート */ + void bubbleSort(List nums) { + // 外側のループ:未ソート区間は [0, i] + for (int i = nums.length - 1; i > 0; i--) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } ``` === "Rust" ```rust title="bubble_sort.rs" - [class]{}-[func]{bubble_sort} + /* バブルソート */ + fn bubble_sort(nums: &mut [i32]) { + // 外側のループ:未ソート区間は [0, i] + for i in (1..nums.len()).rev() { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0..i { + if nums[j] > nums[j + 1] { + // nums[j] と nums[j + 1] を交換 + nums.swap(j, j + 1); + } + } + } + } ``` === "C" ```c title="bubble_sort.c" - [class]{}-[func]{bubbleSort} + /* バブルソート */ + void bubbleSort(int nums[], int size) { + // 外側のループ:未ソート区間は [0, i] + for (int i = size - 1; i > 0; i--) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + } + } + } + } ``` === "Kotlin" ```kotlin title="bubble_sort.kt" - [class]{}-[func]{bubbleSort} + /* バブルソート */ + fun bubbleSort(nums: IntArray) { + // 外側のループ:未ソート区間は [0, i] + for (i in nums.size - 1 downTo 1) { + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (j in 0.. nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + val temp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = temp + } + } + } + } ``` === "Ruby" ```ruby title="bubble_sort.rb" - [class]{}-[func]{bubble_sort} + ### バブルソート ### + def bubble_sort(nums) + n = nums.length + # 外側のループ:未ソート区間は [0, i] + for i in (n - 1).downto(1) + # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0...i + if nums[j] > nums[j + 1] + # nums[j] と nums[j + 1] を交換 + nums[j], nums[j + 1] = nums[j + 1], nums[j] + end + end + end + end ``` +??? pythontutor "コードの可視化" + +
+ + ## 11.3.2   効率の最適化 -「バブリング」のラウンド中に交換が発生しない場合、配列はすでにソートされているため、すぐに戻ることができます。これを検出するために、`flag`変数を追加できます;パスで交換が行われない場合は、フラグを設定して早期に戻ります。 +ある回の「バブル処理」で交換操作が一度も行われなければ、配列はすでにソート済みであり、結果をそのまま返せることがわかります。したがって、この状況を検出するためのフラグ `flag` を追加し、発生した時点で直ちに返すようにできます。 -この最適化があっても、バブルソートの最悪時間計算量と平均時間計算量は$O(n^2)$のままです。ただし、入力配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$まで低くなる可能性があります。 +最適化後も、バブルソートの最悪時間計算量と平均時間計算量は依然として $O(n^2)$ です。ただし、入力配列が完全に整列済みであれば、最良時間計算量は $O(n)$ に達します。 === "Python" ```python title="bubble_sort.py" def bubble_sort_with_flag(nums: list[int]): - """バブルソート(フラグによる最適化)""" + """バブルソート(フラグ最適化)""" n = len(nums) - # 外側のループ:未ソート範囲は [0, i] + # 外側のループ:未ソート区間は [0, i] for i in range(n - 1, 0, -1): - flag = False # フラグを初期化 - # 内側のループ:未ソート範囲 [0, i] の最大要素を範囲の右端に移動 + flag = False # フラグを初期化する + # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j in range(i): if nums[j] > nums[j + 1]: # nums[j] と nums[j + 1] を交換 nums[j], nums[j + 1] = nums[j + 1], nums[j] - flag = True # 要素を交換したことを記録 + flag = True # 交換する要素を記録 if not flag: - break # この回の「バブリング」で要素が交換されなかった場合、終了 + break # このバブル処理で要素交換が一度もなければそのまま終了 ``` === "C++" ```cpp title="bubble_sort.cpp" - /* バブルソート(フラグ最適化版)*/ + /* バブルソート(フラグ最適化) */ void bubbleSortWithFlag(vector &nums) { - // 外側ループ:未ソート範囲は[0, i] + // 外側のループ:未ソート区間は [0, i] for (int i = nums.size() - 1; i > 0; i--) { - bool flag = false; // フラグを初期化 - // 内側ループ:未ソート範囲[0, i]内の最大要素を範囲の右端に交換 + bool flag = false; // フラグを初期化する + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { - // nums[j]とnums[j + 1]を交換 - // ここではstdのswapを使用 + // nums[j] と nums[j + 1] を交換する + // ここでは std::swap() 関数を使用する swap(nums[j], nums[j + 1]); - flag = true; // 交換された要素を記録 + flag = true; // 交換する要素を記録 } } if (!flag) - break; // この回の「バブリング」で要素が交換されなかった場合、終了 + break; // このバブル処理で要素交換が一度もなければそのまま終了 } } ``` @@ -211,23 +346,23 @@ comments: true === "Java" ```java title="bubble_sort.java" - /* バブルソート(フラグによる最適化) */ + /* バブルソート(フラグ最適化) */ void bubbleSortWithFlag(int[] nums) { - // 外側ループ: 未ソート範囲は [0, i] + // 外側のループ:未ソート区間は [0, i] for (int i = nums.length - 1; i > 0; i--) { - boolean flag = false; // フラグを初期化 - // 内側ループ: 未ソート範囲 [0, i] の最大要素を範囲の右端に交換 + boolean flag = false; // フラグを初期化する + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // nums[j] と nums[j + 1] を交換 int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; - flag = true; // 交換された要素を記録 + flag = true; // 交換する要素を記録 } } if (!flag) - break; // この「バブリング」ラウンドで要素が交換されなかった場合、終了 + break; // このバブル処理で要素交換が一度もなければそのまま終了 } } ``` @@ -235,65 +370,253 @@ comments: true === "C#" ```csharp title="bubble_sort.cs" - [class]{bubble_sort}-[func]{BubbleSortWithFlag} + /* バブルソート(フラグ最適化) */ + void BubbleSortWithFlag(int[] nums) { + // 外側のループ:未ソート区間は [0, i] + for (int i = nums.Length - 1; i > 0; i--) { + bool flag = false; // フラグを初期化する + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]); + flag = true; // 交換する要素を記録 + } + } + if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了 + } + } ``` === "Go" ```go title="bubble_sort.go" - [class]{}-[func]{bubbleSortWithFlag} + /* バブルソート(フラグ最適化) */ + func bubbleSortWithFlag(nums []int) { + // 外側のループ:未ソート区間は [0, i] + for i := len(nums) - 1; i > 0; i-- { + flag := false // フラグを初期化する + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // nums[j] と nums[j + 1] を交換 + nums[j], nums[j+1] = nums[j+1], nums[j] + flag = true // 交換する要素を記録 + } + } + if flag == false { // このバブル処理で要素交換が一度もなければそのまま終了 + break + } + } + } ``` === "Swift" ```swift title="bubble_sort.swift" - [class]{}-[func]{bubbleSortWithFlag} + /* バブルソート(フラグ最適化) */ + func bubbleSortWithFlag(nums: inout [Int]) { + // 外側のループ:未ソート区間は [0, i] + for i in nums.indices.dropFirst().reversed() { + var flag = false // フラグを初期化する + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // nums[j] と nums[j + 1] を交換 + nums.swapAt(j, j + 1) + flag = true // 交換する要素を記録 + } + } + if !flag { // このバブル処理で要素交換が一度もなければそのまま終了 + break + } + } + } ``` === "JS" ```javascript title="bubble_sort.js" - [class]{}-[func]{bubbleSortWithFlag} + /* バブルソート(フラグ最適化) */ + function bubbleSortWithFlag(nums) { + // 外側のループ:未ソート区間は [0, i] + for (let i = nums.length - 1; i > 0; i--) { + let flag = false; // フラグを初期化する + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 交換する要素を記録 + } + } + if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了 + } + } ``` === "TS" ```typescript title="bubble_sort.ts" - [class]{}-[func]{bubbleSortWithFlag} + /* バブルソート(フラグ最適化) */ + function bubbleSortWithFlag(nums: number[]): void { + // 外側のループ:未ソート区間は [0, i] + for (let i = nums.length - 1; i > 0; i--) { + let flag = false; // フラグを初期化する + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 交換する要素を記録 + } + } + if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了 + } + } ``` === "Dart" ```dart title="bubble_sort.dart" - [class]{}-[func]{bubbleSortWithFlag} + /* バブルソート(フラグ最適化) */ + void bubbleSortWithFlag(List nums) { + // 外側のループ:未ソート区間は [0, i] + for (int i = nums.length - 1; i > 0; i--) { + bool flag = false; // フラグを初期化する + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 交換する要素を記録 + } + } + if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了 + } + } ``` === "Rust" ```rust title="bubble_sort.rs" - [class]{}-[func]{bubble_sort_with_flag} + /* バブルソート(フラグ最適化) */ + fn bubble_sort_with_flag(nums: &mut [i32]) { + // 外側のループ:未ソート区間は [0, i] + for i in (1..nums.len()).rev() { + let mut flag = false; // フラグを初期化する + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0..i { + if nums[j] > nums[j + 1] { + // nums[j] と nums[j + 1] を交換 + nums.swap(j, j + 1); + flag = true; // 交換する要素を記録 + } + } + if !flag { + break; // このバブル処理で要素交換が一度もなければそのまま終了 + }; + } + } ``` === "C" ```c title="bubble_sort.c" - [class]{}-[func]{bubbleSortWithFlag} + /* バブルソート(フラグ最適化) */ + void bubbleSortWithFlag(int nums[], int size) { + // 外側のループ:未ソート区間は [0, i] + for (int i = size - 1; i > 0; i--) { + bool flag = false; + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + flag = true; + } + } + if (!flag) + break; + } + } ``` === "Kotlin" ```kotlin title="bubble_sort.kt" - [class]{}-[func]{bubbleSortWithFlag} + /* バブルソート(フラグ最適化) */ + fun bubbleSortWithFlag(nums: IntArray) { + // 外側のループ:未ソート区間は [0, i] + for (i in nums.size - 1 downTo 1) { + var flag = false // フラグを初期化する + // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for (j in 0.. nums[j + 1]) { + // nums[j] と nums[j + 1] を交換 + val temp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = temp + flag = true // 交換する要素を記録 + } + } + if (!flag) break // このバブル処理で要素交換が一度もなければそのまま終了 + } + } ``` === "Ruby" ```ruby title="bubble_sort.rb" - [class]{}-[func]{bubble_sort_with_flag} + ### バブルソート ### + def bubble_sort(nums) + n = nums.length + # 外側のループ:未ソート区間は [0, i] + for i in (n - 1).downto(1) + # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0...i + if nums[j] > nums[j + 1] + # nums[j] と nums[j + 1] を交換 + nums[j], nums[j + 1] = nums[j + 1], nums[j] + end + end + end + end + + # ## バブルソート(フラグ最適化)### + def bubble_sort_with_flag(nums) + n = nums.length + # 外側のループ:未ソート区間は [0, i] + for i in (n - 1).downto(1) + flag = false # フラグを初期化する + + # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 + for j in 0...i + if nums[j] > nums[j + 1] + # nums[j] と nums[j + 1] を交換 + nums[j], nums[j + 1] = nums[j + 1], nums[j] + flag = true # 交換する要素を記録 + end + end + + break unless flag # このバブル処理で要素交換が一度もなければそのまま終了 + end + end ``` -## 11.3.3   アルゴリズムの特性 +??? pythontutor "コードの可視化" -- **$O(n^2)$の時間計算量、適応ソート。** 各「バブリング」ラウンドは長さ$n - 1$、$n - 2$、$\dots$、$2$、$1$の配列セグメントを横断し、合計は$(n - 1) n / 2$となります。`flag`最適化により、配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$に達する可能性があります。 -- **$O(1)$の空間計算量、インプレースソート。** ポインタ$i$と$j$によって定数量の追加空間のみが使用されます。 -- **安定ソート。** 等しい要素は「バブリング」中に交換されないため、元の順序が保持され、これは安定ソートになります。 +
+ + +## 11.3.3   アルゴリズムの特徴 + +- **時間計算量は $O(n^2)$、適応的ソート**:各回の「バブル処理」で走査する配列の長さは順に $n - 1$、$n - 2$、$\dots$、$2$、$1$ であり、その総和は $(n - 1) n / 2$ です。`flag` による最適化を導入すると、最良時間計算量は $O(n)$ に達します。 +- **空間計算量は $O(1)$、インプレースソート**:ポインタ $i$ と $j$ は定数サイズの追加領域しか使用しません。 +- **安定ソート**:「バブル処理」では等しい要素に出会っても交換しないためです。 diff --git a/ja/docs/chapter_sorting/bucket_sort.md b/ja/docs/chapter_sorting/bucket_sort.md index 723b9ad9e..91797e392 100644 --- a/ja/docs/chapter_sorting/bucket_sort.md +++ b/ja/docs/chapter_sorting/bucket_sort.md @@ -4,43 +4,43 @@ comments: true # 11.8   バケットソート -前述のソートアルゴリズムはすべて「比較ベースのソートアルゴリズム」で、値を比較することで要素をソートします。このようなソートアルゴリズムは $O(n \log n)$ より良い時間計算量を持つことはできません。次に、線形時間計算量を達成できるいくつかの「非比較ソートアルゴリズム」について議論します。 +前述のいくつかのソートアルゴリズムは、いずれも「比較ベースのソートアルゴリズム」に属し、要素間の大小を比較することで整列を実現します。この種のソートアルゴリズムの時間計算量は $O(n \log n)$ を超えられません。続いて、時間計算量が線形オーダーに達しうる「非比較ソートアルゴリズム」をいくつか見ていきます。 -バケットソートは分割統治戦略の典型的な応用です。一連の順序付けられたバケットを設定し、各バケットがデータの範囲を含み、入力データをこれらのバケットに均等に分散させることで動作します。そして、各バケット内のデータを個別にソートします。最後に、すべてのバケットからのソート済みデータを順次マージして最終結果を生成します。 +バケットソート(bucket sort)は分割統治戦略の典型的な応用です。大小関係をもつ複数のバケットを用意し、各バケットがあるデータ範囲に対応するようにして、データを各バケットへ均等に分配します。その後、各バケット内でそれぞれソートを行い、最後にバケットの順序に従ってすべてのデータを結合します。 -## 11.8.1   アルゴリズムの過程 +## 11.8.1   アルゴリズムの流れ -長さ $n$ の配列で、$[0, 1)$ の範囲の浮動小数点数を考えてみます。バケットソートの過程は以下の図に示されています。 +長さ $n$ の配列を考え、その要素は範囲 $[0, 1)$ の浮動小数点数であるとします。バケットソートの流れを以下の図に示します。 -1. $k$ 個のバケットを初期化し、$n$ 個の要素をこれらの $k$ 個のバケットに分散させます。 -2. 各バケットを個別にソートします(プログラミング言語の組み込みソート関数を使用)。 -3. 最小から最大のバケットの順序で結果をマージします。 +1. $k$ 個のバケットを初期化し、$n$ 個の要素を $k$ 個のバケットに分配します。 +2. 各バケットに対してそれぞれソートを実行します(ここではプログラミング言語の組み込みソート関数を用います)。 +3. バケットを小さい順にたどって結果を結合します。 -![バケットソートアルゴリズムの過程](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" } +![バケットソートの流れ](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" } -

図 11-13   バケットソートアルゴリズムの過程

+

図 11-13   バケットソートの流れ

-コードは以下の通りです: +コードは以下のとおりです: === "Python" ```python title="bucket_sort.py" def bucket_sort(nums: list[float]): """バケットソート""" - # k = n/2 個のバケットを初期化、各バケットに平均2個の要素を配置することを期待 + # k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする k = len(nums) // 2 buckets = [[] for _ in range(k)] - # 1. 配列要素を各バケットに分散 + # 1. 配列要素を各バケットに振り分ける for num in nums: - # 入力データ範囲は [0, 1)、num * k を使用してインデックス範囲 [0, k-1] にマッピング + # 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する i = int(num * k) # num をバケット i に追加 buckets[i].append(num) - # 2. 各バケットをソート + # 2. 各バケットをソートする for bucket in buckets: - # 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能 + # 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい bucket.sort() - # 3. バケットを走査して結果をマージ + # 3. バケットを走査して結果を結合 i = 0 for bucket in buckets: for num in bucket: @@ -53,22 +53,22 @@ comments: true ```cpp title="bucket_sort.cpp" /* バケットソート */ void bucketSort(vector &nums) { - // k = n/2個のバケットを初期化、各バケットに2つの要素を割り当てることを期待 + // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする int k = nums.size() / 2; vector> buckets(k); - // 1. 配列要素を各バケットに分配 + // 1. 配列要素を各バケットに振り分ける for (float num : nums) { - // 入力データ範囲は[0, 1)、num * kを使用してインデックス範囲[0, k-1]にマップ + // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する int i = num * k; - // bucket_idxバケットに数値を追加 + // num をバケット bucket_idx に追加 buckets[i].push_back(num); } - // 2. 各バケットをソート + // 2. 各バケットをソートする for (vector &bucket : buckets) { - // 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能 + // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい sort(bucket.begin(), bucket.end()); } - // 3. バケットを走査して結果をマージ + // 3. バケットを走査して結果を結合 int i = 0; for (vector &bucket : buckets) { for (float num : bucket) { @@ -83,25 +83,25 @@ comments: true ```java title="bucket_sort.java" /* バケットソート */ void bucketSort(float[] nums) { - // k = n/2 個のバケットを初期化、各バケットに期待される要素数は 2 個 + // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする int k = nums.length / 2; List> buckets = new ArrayList<>(); for (int i = 0; i < k; i++) { buckets.add(new ArrayList<>()); } - // 1. 配列要素を各バケットに分散 + // 1. 配列要素を各バケットに振り分ける for (float num : nums) { - // 入力データ範囲は [0, 1)、num * k を使ってインデックス範囲 [0, k-1] にマッピング + // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する int i = (int) (num * k); // num をバケット i に追加 buckets.get(i).add(num); } - // 2. 各バケットをソート + // 2. 各バケットをソートする for (List bucket : buckets) { - // 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能 + // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい Collections.sort(bucket); } - // 3. バケットを走査して結果をマージ + // 3. バケットを走査して結果を結合 int i = 0; for (List bucket : buckets) { for (float num : bucket) { @@ -114,87 +114,358 @@ comments: true === "C#" ```csharp title="bucket_sort.cs" - [class]{bucket_sort}-[func]{BucketSort} + /* バケットソート */ + void BucketSort(float[] nums) { + // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする + int k = nums.Length / 2; + List> buckets = []; + for (int i = 0; i < k; i++) { + buckets.Add([]); + } + // 1. 配列要素を各バケットに振り分ける + foreach (float num in nums) { + // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する + int i = (int)(num * k); + // num をバケット i に追加 + buckets[i].Add(num); + } + // 2. 各バケットをソートする + foreach (List bucket in buckets) { + // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい + bucket.Sort(); + } + // 3. バケットを走査して結果を結合 + int j = 0; + foreach (List bucket in buckets) { + foreach (float num in bucket) { + nums[j++] = num; + } + } + } ``` === "Go" ```go title="bucket_sort.go" - [class]{}-[func]{bucketSort} + /* バケットソート */ + func bucketSort(nums []float64) { + // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする + k := len(nums) / 2 + buckets := make([][]float64, k) + for i := 0; i < k; i++ { + buckets[i] = make([]float64, 0) + } + // 1. 配列要素を各バケットに振り分ける + for _, num := range nums { + // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する + i := int(num * float64(k)) + // num をバケット i に追加 + buckets[i] = append(buckets[i], num) + } + // 2. 各バケットをソートする + for i := 0; i < k; i++ { + // 組み込みのスライスソート関数を使う。ほかのソートアルゴリズムに置き換えてもよい + sort.Float64s(buckets[i]) + } + // 3. バケットを走査して結果を結合 + i := 0 + for _, bucket := range buckets { + for _, num := range bucket { + nums[i] = num + i++ + } + } + } ``` === "Swift" ```swift title="bucket_sort.swift" - [class]{}-[func]{bucketSort} + /* バケットソート */ + func bucketSort(nums: inout [Double]) { + // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする + let k = nums.count / 2 + var buckets = (0 ..< k).map { _ in [Double]() } + // 1. 配列要素を各バケットに振り分ける + for num in nums { + // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する + let i = Int(num * Double(k)) + // num をバケット i に追加 + buckets[i].append(num) + } + // 2. 各バケットをソートする + for i in buckets.indices { + // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい + buckets[i].sort() + } + // 3. バケットを走査して結果を結合 + var i = nums.startIndex + for bucket in buckets { + for num in bucket { + nums[i] = num + i += 1 + } + } + } ``` === "JS" ```javascript title="bucket_sort.js" - [class]{}-[func]{bucketSort} + /* バケットソート */ + function bucketSort(nums) { + // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする + const k = nums.length / 2; + const buckets = []; + for (let i = 0; i < k; i++) { + buckets.push([]); + } + // 1. 配列要素を各バケットに振り分ける + for (const num of nums) { + // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する + const i = Math.floor(num * k); + // num をバケット i に追加 + buckets[i].push(num); + } + // 2. 各バケットをソートする + for (const bucket of buckets) { + // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい + bucket.sort((a, b) => a - b); + } + // 3. バケットを走査して結果を結合 + let i = 0; + for (const bucket of buckets) { + for (const num of bucket) { + nums[i++] = num; + } + } + } ``` === "TS" ```typescript title="bucket_sort.ts" - [class]{}-[func]{bucketSort} + /* バケットソート */ + function bucketSort(nums: number[]): void { + // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする + const k = nums.length / 2; + const buckets: number[][] = []; + for (let i = 0; i < k; i++) { + buckets.push([]); + } + // 1. 配列要素を各バケットに振り分ける + for (const num of nums) { + // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する + const i = Math.floor(num * k); + // num をバケット i に追加 + buckets[i].push(num); + } + // 2. 各バケットをソートする + for (const bucket of buckets) { + // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい + bucket.sort((a, b) => a - b); + } + // 3. バケットを走査して結果を結合 + let i = 0; + for (const bucket of buckets) { + for (const num of bucket) { + nums[i++] = num; + } + } + } ``` === "Dart" ```dart title="bucket_sort.dart" - [class]{}-[func]{bucketSort} + /* バケットソート */ + void bucketSort(List nums) { + // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする + int k = nums.length ~/ 2; + List> buckets = List.generate(k, (index) => []); + + // 1. 配列要素を各バケットに振り分ける + for (double _num in nums) { + // 入力データの範囲は [0, 1) であり、_num * k を用いてインデックス範囲 [0, k-1] に写像する + int i = (_num * k).toInt(); + // _num をバケット bucket_idx に追加 + buckets[i].add(_num); + } + // 2. 各バケットをソートする + for (List bucket in buckets) { + bucket.sort(); + } + // 3. バケットを走査して結果を結合 + int i = 0; + for (List bucket in buckets) { + for (double _num in bucket) { + nums[i++] = _num; + } + } + } ``` === "Rust" ```rust title="bucket_sort.rs" - [class]{}-[func]{bucket_sort} + /* バケットソート */ + fn bucket_sort(nums: &mut [f64]) { + // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする + let k = nums.len() / 2; + let mut buckets = vec![vec![]; k]; + // 1. 配列要素を各バケットに振り分ける + for &num in nums.iter() { + // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する + let i = (num * k as f64) as usize; + // num をバケット i に追加 + buckets[i].push(num); + } + // 2. 各バケットをソートする + for bucket in &mut buckets { + // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい + bucket.sort_by(|a, b| a.partial_cmp(b).unwrap()); + } + // 3. バケットを走査して結果を結合 + let mut i = 0; + for bucket in buckets.iter() { + for &num in bucket.iter() { + nums[i] = num; + i += 1; + } + } + } ``` === "C" ```c title="bucket_sort.c" - [class]{}-[func]{bucketSort} + /* バケットソート */ + void bucketSort(float nums[], int n) { + int k = n / 2; // k = n/2 個のバケットを初期化する + int *sizes = malloc(k * sizeof(int)); // 各バケットのサイズを記録する + float **buckets = malloc(k * sizeof(float *)); // 動的配列の配列(バケット) + // 各バケットに十分な容量を事前確保する + for (int i = 0; i < k; ++i) { + buckets[i] = (float *)malloc(n * sizeof(float)); + sizes[i] = 0; + } + // 1. 配列要素を各バケットに振り分ける + for (int i = 0; i < n; ++i) { + int idx = (int)(nums[i] * k); + buckets[idx][sizes[idx]++] = nums[i]; + } + // 2. 各バケットをソートする + for (int i = 0; i < k; ++i) { + qsort(buckets[i], sizes[i], sizeof(float), compare); + } + // 3. ソート済みのバケットを結合する + int idx = 0; + for (int i = 0; i < k; ++i) { + for (int j = 0; j < sizes[i]; ++j) { + nums[idx++] = buckets[i][j]; + } + // メモリを解放する + free(buckets[i]); + } + } ``` === "Kotlin" ```kotlin title="bucket_sort.kt" - [class]{}-[func]{bucketSort} + /* バケットソート */ + fun bucketSort(nums: FloatArray) { + // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする + val k = nums.size / 2 + val buckets = mutableListOf>() + for (i in 0.. + -- **時間計算量は $O(n + k)$**:要素がバケット間で均等に分散されていると仮定すると、各バケット内の要素数は $n/k$ です。単一のバケットのソートに $O(n/k \log(n/k))$ 時間がかかると仮定すると、すべてのバケットのソートに $O(n \log(n/k))$ 時間がかかります。**バケット数 $k$ が比較的大きいとき、時間計算量は $O(n)$ に近づきます**。結果のマージには、すべてのバケットと要素を走査する必要があり、$O(n + k)$ 時間がかかります。最悪の場合、すべてのデータが単一のバケットに分散され、そのバケットのソートには $O(n^2)$ 時間がかかります。 -- **空間計算量は $O(n + k)$、非インプレースソート**:$k$ 個のバケットと合計 $n$ 個の要素のための追加スペースが必要です。 -- バケットソートが安定かどうかは、各バケット内で使用されるソートアルゴリズムが安定かどうかに依存します。 +## 11.8.2   アルゴリズムの特性 -## 11.8.3   均等分散を達成する方法 +バケットソートは、非常に大規模なデータの処理に適しています。たとえば、入力データに 100 万個の要素が含まれ、空間の制約によりシステムメモリへすべてのデータを一度に読み込めない場合です。このとき、データを 1000 個のバケットに分け、それぞれのバケットを個別にソートしてから、最後に結果を結合できます。 -バケットソートの理論的時間計算量は $O(n)$ に達することができます。**重要なことは、すべてのバケットに要素を均等に分散させることです**。実世界のデータはしばしば均一に分散されていないからです。例えば、eBayのすべての商品を価格範囲で10個のバケットに均等に分散させたいとします。しかし、商品価格の分散は均等でない可能性があり、100ドル未満の商品が多く、500ドル以上の商品が少ないかもしれません。価格範囲を均等に10分割すると、各バケットの商品数の差が大きくなります。 +- **時間計算量は $O(n + k)$** :要素が各バケット内に平均的に分布していると仮定すると、各バケット内の要素数は $\frac{n}{k}$ です。1 つのバケットをソートするのに $O(\frac{n}{k} \log\frac{n}{k})$ の時間がかかるなら、すべてのバケットのソートには $O(n \log\frac{n}{k})$ の時間がかかります。**バケット数 $k$ が十分大きいとき、時間計算量は $O(n)$ に近づきます**。結果を結合する際には、すべてのバケットと要素を走査する必要があり、$O(n + k)$ の時間を要します。最悪の場合、すべてのデータが 1 つのバケットに割り当てられ、そのバケットのソートに $O(n^2)$ の時間がかかります。 +- **空間計算量は $O(n + k)$、非インプレースソート**:$k$ 個のバケットと合計 $n$ 個の要素ぶんの追加領域が必要です。 +- バケットソートが安定かどうかは、バケット内要素のソートに用いるアルゴリズムが安定かどうかに依存します。 -均等分散を達成するために、最初におおよその境界を設定して、データを3つのバケットに大まかに分割できます。**分散が完了した後、より多くのアイテムを持つバケットをさらに3つのバケットに分割し、すべてのバケットの要素数がほぼ等しくなるまで続けます**。 +## 11.8.3   均等な分配を実現するには -以下の図に示すように、この方法は本質的に再帰木を構築し、葉ノードの要素数ができるだけ均等になることを目指します。もちろん、各ラウンドでデータを3つのバケットに分割する必要はありません - 分割戦略はデータの独特な特性に適応的に調整できます。 +バケットソートの時間計算量は理論上 $O(n)$ に達しますが、**鍵は要素を各バケットへ均等に分配すること** にあります。実際のデータは均一に分布していないことが多いからです。たとえば、Taobao 上のすべての商品を価格帯ごとに 10 個のバケットへ均等に分けたいとしても、商品の価格分布は偏っており、100 元未満は非常に多く、1000 元超は非常に少ないかもしれません。価格区間を単純に 10 等分すると、各バケットの商品数には大きな差が生じます。 -![バケットの再帰的分割](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" } +均等な分配を実現するために、まず大まかな境界線を設定し、データをひとまず 3 個のバケットに粗く振り分けます。**分配後は、商品数の多いバケットをさらに 3 個のバケットに分割し、すべてのバケット内の要素数がおおむね等しくなるまでこれを続けます**。 -

図 11-14   バケットの再帰的分割

+以下の図に示すように、この方法の本質は再帰木を構築することにあり、目標は葉ノードの値をできるだけ均等にすることです。もちろん、毎回データを 3 個のバケットに分割する必要はなく、具体的な分け方はデータの特徴に応じて柔軟に選べます。 -商品価格の確率分布を事前に知っている場合、**データの確率分布に基づいて各バケットの価格境界を設定できます**。データ分布を具体的に計算する必要は必ずしもなく、代わりに確率モデルを使用してデータ特性に基づいて近似できることに注意してください。 +![再帰的にバケットを分割](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" } -以下の図に示すように、商品価格が正規分布に従うと仮定すると、バケット間でアイテムの分散のバランスを取るために合理的な価格区間を定義できます。 +

図 11-14   再帰的にバケットを分割

-![確率分布に基づくバケット分割](bucket_sort.assets/scatter_in_buckets_distribution.png){ class="animation-figure" } +商品価格の確率分布をあらかじめ把握しているなら、**データの確率分布に基づいて各バケットの価格境界を設定できます**。なお、データ分布は必ずしも特別に統計を取る必要はなく、データの特徴に応じて何らかの確率モデルで近似することもできます。 -

図 11-15   確率分布に基づくバケット分割

+以下の図に示すように、商品価格が正規分布に従うと仮定すれば、価格区間を合理的に設定でき、それによって商品を各バケットへ均等に分配できます。 + +![確率分布に基づいてバケットを分割](bucket_sort.assets/scatter_in_buckets_distribution.png){ class="animation-figure" } + +

図 11-15   確率分布に基づいてバケットを分割

diff --git a/ja/docs/chapter_sorting/counting_sort.md b/ja/docs/chapter_sorting/counting_sort.md index 5a19c9376..365fb86d6 100644 --- a/ja/docs/chapter_sorting/counting_sort.md +++ b/ja/docs/chapter_sorting/counting_sort.md @@ -4,38 +4,36 @@ comments: true # 11.9   計数ソート -計数ソートは要素の数をカウントすることでソートを実現し、通常は整数配列に適用されます。 +計数ソート(counting sort)は要素数を集計することでソートを実現し、通常は整数配列に適用されます。 -## 11.9.1   簡単な実装 +## 11.9.1   単純な実装 -簡単な例から始めましょう。長さ $n$ の配列 `nums` が与えられ、すべての要素が「非負整数」である場合、計数ソートの全体的な過程は以下の図に示されています。 +まず簡単な例を見てみましょう。長さ $n$ の配列 `nums` が与えられ、その要素はすべて「非負整数」であるとします。計数ソートの全体的な流れを以下の図に示します。 -1. 配列を走査して最大数を見つけ、それを $m$ とし、長さ $m + 1$ の補助配列 `counter` を作成します。 -2. **`counter` を使用して `nums` 内の各数の出現回数をカウントします**。ここで `counter[num]` は数 `num` の出現回数に対応します。カウント方法は簡単で、`nums` を走査し(現在の数を `num` とする)、各ラウンドで `counter[num]` を $1$ 増やします。 -3. **`counter` のインデックスは自然に順序付けられているため、すべての数は本質的にすでにソートされています**。次に、`counter` を走査し、出現順に `nums` を昇順で埋めます。 +1. 配列を走査し、その中の最大値を見つけて $m$ とし、続いて長さ $m + 1$ の補助配列 `counter` を作成します。 +2. **`counter` を用いて `nums` 内の各数値の出現回数を集計します**。ここで `counter[num]` は数値 `num` の出現回数に対応します。集計方法は非常に簡単で、`nums` を走査し(現在の数値を `num` とする)、各回で `counter[num]` を $1$ 増やせばよいです。 +3. **`counter` の各インデックスは自然に順序づけられているため、すべての数値はすでに整列された状態とみなせます**。続いて `counter` を走査し、各数値の出現回数に応じて小さい順に `nums` へ書き戻せば完了です。 -![計数ソートの過程](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" } +![計数ソートの流れ](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" } -

図 11-16   計数ソートの過程

+

図 11-16   計数ソートの流れ

-コードは以下の通りです: +コードは以下のとおりです: === "Python" ```python title="counting_sort.py" def counting_sort_naive(nums: list[int]): """計数ソート""" - # シンプルな実装、オブジェクトのソートには使用できない - # 1. 配列内の最大要素 m を統計 - m = 0 - for num in nums: - m = max(m, num) - # 2. 各数字の出現回数を統計 + # 簡易版。オブジェクトのソートには使えない + # 1. 配列の最大要素 m を求める + m = max(nums) + # 2. 各数値の出現回数を数える # counter[num] は num の出現回数を表す counter = [0] * (m + 1) for num in nums: counter[num] += 1 - # 3. counter を走査し、各要素を元の配列 nums に埋め戻し + # 3. counter を走査し、各要素を元の配列 nums に書き戻す i = 0 for num in range(m + 1): for _ in range(counter[num]): @@ -46,21 +44,21 @@ comments: true === "C++" ```cpp title="counting_sort.cpp" - /* カウントソート */ - // 簡単な実装、オブジェクトのソートには使用できない + /* 計数ソート */ + // 簡易実装のため、オブジェクトのソートには使えない void countingSortNaive(vector &nums) { - // 1. 配列の最大要素mを統計 + // 1. 配列の最大要素 m を求める int m = 0; for (int num : nums) { m = max(m, num); } - // 2. 各数字の出現回数を統計 - // counter[num]はnumの出現回数を表す + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す vector counter(m + 1, 0); for (int num : nums) { counter[num]++; } - // 3. counterを走査し、各要素を元の配列numsに戻す + // 3. counter を走査し、各要素を元の配列 nums に書き戻す int i = 0; for (int num = 0; num < m + 1; num++) { for (int j = 0; j < counter[num]; j++, i++) { @@ -74,20 +72,20 @@ comments: true ```java title="counting_sort.java" /* 計数ソート */ - // 簡単な実装、オブジェクトのソートには使用できない + // 簡易実装のため、オブジェクトのソートには使えない void countingSortNaive(int[] nums) { - // 1. 配列の最大要素 m を統計 + // 1. 配列の最大要素 m を求める int m = 0; for (int num : nums) { m = Math.max(m, num); } - // 2. 各数字の出現回数を統計 + // 2. 各数値の出現回数を数える // counter[num] は num の出現回数を表す int[] counter = new int[m + 1]; for (int num : nums) { counter[num]++; } - // 3. counter を走査し、各要素を元の配列 nums に戻す + // 3. counter を走査し、各要素を元の配列 nums に書き戻す int i = 0; for (int num = 0; num < m + 1; num++) { for (int j = 0; j < counter[num]; j++, i++) { @@ -100,86 +98,297 @@ comments: true === "C#" ```csharp title="counting_sort.cs" - [class]{counting_sort}-[func]{CountingSortNaive} + /* 計数ソート */ + // 簡易実装のため、オブジェクトのソートには使えない + void CountingSortNaive(int[] nums) { + // 1. 配列の最大要素 m を求める + int m = 0; + foreach (int num in nums) { + m = Math.Max(m, num); + } + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + int[] counter = new int[m + 1]; + foreach (int num in nums) { + counter[num]++; + } + // 3. counter を走査し、各要素を元の配列 nums に書き戻す + int i = 0; + for (int num = 0; num < m + 1; num++) { + for (int j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } ``` === "Go" ```go title="counting_sort.go" - [class]{}-[func]{countingSortNaive} + /* 計数ソート */ + // 簡易実装のため、オブジェクトのソートには使えない + func countingSortNaive(nums []int) { + // 1. 配列の最大要素 m を求める + m := 0 + for _, num := range nums { + if num > m { + m = num + } + } + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + counter := make([]int, m+1) + for _, num := range nums { + counter[num]++ + } + // 3. counter を走査し、各要素を元の配列 nums に書き戻す + for i, num := 0, 0; num < m+1; num++ { + for j := 0; j < counter[num]; j++ { + nums[i] = num + i++ + } + } + } ``` === "Swift" ```swift title="counting_sort.swift" - [class]{}-[func]{countingSortNaive} + /* 計数ソート */ + // 簡易実装のため、オブジェクトのソートには使えない + func countingSortNaive(nums: inout [Int]) { + // 1. 配列の最大要素 m を求める + let m = nums.max()! + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + var counter = Array(repeating: 0, count: m + 1) + for num in nums { + counter[num] += 1 + } + // 3. counter を走査し、各要素を元の配列 nums に書き戻す + var i = 0 + for num in 0 ..< m + 1 { + for _ in 0 ..< counter[num] { + nums[i] = num + i += 1 + } + } + } ``` === "JS" ```javascript title="counting_sort.js" - [class]{}-[func]{countingSortNaive} + /* 計数ソート */ + // 簡易実装のため、オブジェクトのソートには使えない + function countingSortNaive(nums) { + // 1. 配列の最大要素 m を求める + let m = Math.max(...nums); + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + const counter = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. counter を走査し、各要素を元の配列 nums に書き戻す + let i = 0; + for (let num = 0; num < m + 1; num++) { + for (let j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } ``` === "TS" ```typescript title="counting_sort.ts" - [class]{}-[func]{countingSortNaive} + /* 計数ソート */ + // 簡易実装のため、オブジェクトのソートには使えない + function countingSortNaive(nums: number[]): void { + // 1. 配列の最大要素 m を求める + let m: number = Math.max(...nums); + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + const counter: number[] = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. counter を走査し、各要素を元の配列 nums に書き戻す + let i = 0; + for (let num = 0; num < m + 1; num++) { + for (let j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } ``` === "Dart" ```dart title="counting_sort.dart" - [class]{}-[func]{countingSortNaive} + /* 計数ソート */ + // 簡易実装のため、オブジェクトのソートには使えない + void countingSortNaive(List nums) { + // 1. 配列の最大要素 m を求める + int m = 0; + for (int _num in nums) { + m = max(m, _num); + } + // 2. 各数値の出現回数を数える + // counter[_num] は _num の出現回数を表す + List counter = List.filled(m + 1, 0); + for (int _num in nums) { + counter[_num]++; + } + // 3. counter を走査し、各要素を元の配列 nums に書き戻す + int i = 0; + for (int _num = 0; _num < m + 1; _num++) { + for (int j = 0; j < counter[_num]; j++, i++) { + nums[i] = _num; + } + } + } ``` === "Rust" ```rust title="counting_sort.rs" - [class]{}-[func]{counting_sort_naive} + /* 計数ソート */ + // 簡易実装のため、オブジェクトのソートには使えない + fn counting_sort_naive(nums: &mut [i32]) { + // 1. 配列の最大要素 m を求める + let m = *nums.iter().max().unwrap(); + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + let mut counter = vec![0; m as usize + 1]; + for &num in nums.iter() { + counter[num as usize] += 1; + } + // 3. counter を走査し、各要素を元の配列 nums に書き戻す + let mut i = 0; + for num in 0..m + 1 { + for _ in 0..counter[num as usize] { + nums[i] = num; + i += 1; + } + } + } ``` === "C" ```c title="counting_sort.c" - [class]{}-[func]{countingSortNaive} + /* 計数ソート */ + // 簡易実装のため、オブジェクトのソートには使えない + void countingSortNaive(int nums[], int size) { + // 1. 配列の最大要素 m を求める + int m = 0; + for (int i = 0; i < size; i++) { + if (nums[i] > m) { + m = nums[i]; + } + } + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + int *counter = calloc(m + 1, sizeof(int)); + for (int i = 0; i < size; i++) { + counter[nums[i]]++; + } + // 3. counter を走査し、各要素を元の配列 nums に書き戻す + int i = 0; + for (int num = 0; num < m + 1; num++) { + for (int j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + // 4. メモリを解放する + free(counter); + } ``` === "Kotlin" ```kotlin title="counting_sort.kt" - [class]{}-[func]{countingSortNaive} + /* 計数ソート */ + // 簡易実装のため、オブジェクトのソートには使えない + fun countingSortNaive(nums: IntArray) { + // 1. 配列の最大要素 m を求める + var m = 0 + for (num in nums) { + m = max(m, num) + } + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + val counter = IntArray(m + 1) + for (num in nums) { + counter[num]++ + } + // 3. counter を走査し、各要素を元の配列 nums に書き戻す + var i = 0 + for (num in 0.. + + !!! note "計数ソートとバケットソートの関係" - バケットソートの観点から、計数ソートにおける計数配列 `counter` の各インデックスをバケットと考え、カウントの過程を要素を対応するバケットに分散させることと考えることができます。本質的に、計数ソートは整数データのためのバケットソートの特別なケースです。 + バケットソートの観点から見ると、計数ソートにおける計数配列 `counter` の各インデックスを 1 つのバケットとみなし、個数を数える過程を各要素を対応するバケットへ振り分ける操作とみなせます。本質的には、計数ソートは整数データにおけるバケットソートの特殊な一例です。 ## 11.9.2   完全な実装 -注意深い読者は気付くかもしれませんが、**入力データがオブジェクトの場合、上記の手順 `3.` は無効です**。入力データが商品オブジェクトで、価格(クラスメンバ変数)で商品をソートしたいとします。しかし、上記のアルゴリズムは結果としてソート済みの価格のみを提供できます。 +注意深い読者なら、**入力データがオブジェクトである場合、上記の手順 `3.` は機能しない**ことに気づくかもしれません。入力データが商品オブジェクトであり、商品価格(クラスのメンバ変数)に基づいて商品をソートしたいとします。しかし上記のアルゴリズムが返せるのは価格のソート結果だけです。 -では、元のデータのソート結果をどのように取得できるでしょうか?まず、`counter` の「前置和」を計算します。名前が示すように、インデックス `i` での前置和 `prefix[i]` は、配列の最初の `i` 個の要素の和に等しいです: +では、元のデータのソート結果を得るにはどうすればよいのでしょうか。まず `counter` の「累積和」を計算します。名前のとおり、インデックス `i` における累積和 `prefix[i]` は、配列の先頭から `i` 番目までの要素の総和に等しくなります: $$ \text{prefix}[i] = \sum_{j=0}^i \text{counter[j]} $$ -**前置和には明確な意味があります。`prefix[num] - 1` は結果配列 `res` における要素 `num` の最後の出現のインデックスを表します**。この情報は重要で、各要素が結果配列のどこに現れるべきかを教えてくれます。次に、元の配列 `nums` の各要素 `num` を逆順で走査し、各反復で以下の2つの手順を実行します。 +**累積和には明確な意味があり、`prefix[num] - 1` は要素 `num` が結果配列 `res` に最後に現れるインデックスを表します**。この情報は非常に重要で、各要素が結果配列のどの位置に現れるべきかを示してくれます。続いて元の配列 `nums` を逆順に走査し、各要素 `num` に対して各反復で次の 2 つの手順を行います。 -1. インデックス `prefix[num] - 1` で配列 `res` に `num` を埋めます。 -2. 前置和 `prefix[num]` を $1$ 減らして、`num` を配置する次のインデックスを取得します。 +1. `num` を配列 `res` のインデックス `prefix[num] - 1` に格納します。 +2. 累積和 `prefix[num]` を $1$ 減らし、次に `num` を配置するインデックスを得ます。 -走査後、配列 `res` にはソートされた結果が含まれ、最後に `res` が元の配列 `nums` を置き換えます。完全な計数ソートの過程は以下の図に示されています。 +走査が完了すると、配列 `res` にソート済みの結果が格納されます。最後に `res` で元の配列 `nums` を上書きすれば完了です。以下の図は完全な計数ソートの流れを示しています。 === "<1>" - ![計数ソートの過程](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" } + ![計数ソートの手順](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" } === "<2>" ![counting_sort_step2](counting_sort.assets/counting_sort_step2.png){ class="animation-figure" } @@ -202,36 +411,36 @@ $$ === "<8>" ![counting_sort_step8](counting_sort.assets/counting_sort_step8.png){ class="animation-figure" } -

図 11-17   計数ソートの過程

+

図 11-17   計数ソートの手順

-計数ソートの実装コードは以下の通りです: +計数ソートの実装コードは以下のとおりです: === "Python" ```python title="counting_sort.py" def counting_sort(nums: list[int]): """計数ソート""" - # 完全な実装、オブジェクトのソートが可能で、安定ソート - # 1. 配列内の最大要素 m を統計 + # 完全版。オブジェクトをソートでき、かつ安定ソートである + # 1. 配列の最大要素 m を求める m = max(nums) - # 2. 各数字の出現回数を統計 + # 2. 各数値の出現回数を数える # counter[num] は num の出現回数を表す counter = [0] * (m + 1) for num in nums: counter[num] += 1 - # 3. counter の前置和を計算し、「出現回数」を「末尾インデックス」に変換 - # counter[num]-1 は res において num が最後に出現するインデックス + # 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + # つまり counter[num]-1 は、num が res に最後に現れるインデックス for i in range(m): counter[i + 1] += counter[i] - # 4. nums を逆順に走査し、各要素を結果配列 res に配置 + # 4. nums を逆順に走査し、各要素を結果配列 res に格納する # 結果を記録するための配列 res を初期化 n = len(nums) res = [0] * n for i in range(n - 1, -1, -1): num = nums[i] res[counter[num] - 1] = num # num を対応するインデックスに配置 - counter[num] -= 1 # 前置和を1減らし、num を配置する次のインデックスを取得 - # 結果配列 res を使用して元の配列 nums を上書き + counter[num] -= 1 # 累積和を 1 減らして、次に num を配置するインデックスを得る + # 結果配列 res で元の配列 nums を上書きする for i in range(n): nums[i] = res[i] ``` @@ -239,35 +448,35 @@ $$ === "C++" ```cpp title="counting_sort.cpp" - /* カウントソート */ - // 完全な実装、オブジェクトのソートが可能で安定ソート + /* 計数ソート */ + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである void countingSort(vector &nums) { - // 1. 配列の最大要素mを統計 + // 1. 配列の最大要素 m を求める int m = 0; for (int num : nums) { m = max(m, num); } - // 2. 各数字の出現回数を統計 - // counter[num]はnumの出現回数を表す + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す vector counter(m + 1, 0); for (int num : nums) { counter[num]++; } - // 3. counterの前缀和を計算し、「出現回数」を「末尾インデックス」に変換 - // counter[num]-1はnumがresで現れる最後のインデックス + // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + // つまり counter[num]-1 は、num が res に最後に現れるインデックス for (int i = 0; i < m; i++) { counter[i + 1] += counter[i]; } - // 4. numsを逆順で走査し、各要素を結果配列resに配置 - // 結果を記録する配列resを初期化 + // 4. nums を逆順に走査し、各要素を結果配列 res に格納する + // 結果を記録するための配列 res を初期化 int n = nums.size(); vector res(n); for (int i = n - 1; i >= 0; i--) { int num = nums[i]; - res[counter[num] - 1] = num; // numを対応するインデックスに配置 - counter[num]--; // 前缀和を1減らし、numを配置する次のインデックスを取得 + res[counter[num] - 1] = num; // num を対応するインデックスに配置 + counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る } - // 結果配列resで元の配列numsを上書き + // 結果配列 res で元の配列 nums を上書きする nums = res; } ``` @@ -276,34 +485,34 @@ $$ ```java title="counting_sort.java" /* 計数ソート */ - // 完全な実装、オブジェクトをソートでき、安定ソート + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである void countingSort(int[] nums) { - // 1. 配列の最大要素 m を統計 + // 1. 配列の最大要素 m を求める int m = 0; for (int num : nums) { m = Math.max(m, num); } - // 2. 各数字の出現回数を統計 + // 2. 各数値の出現回数を数える // counter[num] は num の出現回数を表す int[] counter = new int[m + 1]; for (int num : nums) { counter[num]++; } - // 3. counter の累積和を計算し、「出現回数」を「尻尾インデックス」に変換 - // counter[num]-1 は res 内で num が出現する最後のインデックス + // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + // つまり counter[num]-1 は、num が res に最後に現れるインデックス for (int i = 0; i < m; i++) { counter[i + 1] += counter[i]; } - // 4. nums を逆順に走査し、各要素を結果配列 res に配置 - // 結果を記録する配列 res を初期化 + // 4. nums を逆順に走査し、各要素を結果配列 res に格納する + // 結果を記録するための配列 res を初期化 int n = nums.length; int[] res = new int[n]; for (int i = n - 1; i >= 0; i--) { int num = nums[i]; res[counter[num] - 1] = num; // num を対応するインデックスに配置 - counter[num]--; // 累積和を 1 減算し、num を配置する次のインデックスを取得 + counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る } - // 結果配列 res を使って元の配列 nums を上書き + // 結果配列 res で元の配列 nums を上書きする for (int i = 0; i < n; i++) { nums[i] = res[i]; } @@ -313,73 +522,376 @@ $$ === "C#" ```csharp title="counting_sort.cs" - [class]{counting_sort}-[func]{CountingSort} + /* 計数ソート */ + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである + void CountingSort(int[] nums) { + // 1. 配列の最大要素 m を求める + int m = 0; + foreach (int num in nums) { + m = Math.Max(m, num); + } + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + int[] counter = new int[m + 1]; + foreach (int num in nums) { + counter[num]++; + } + // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + // つまり counter[num]-1 は、num が res に最後に現れるインデックス + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. nums を逆順に走査し、各要素を結果配列 res に格納する + // 結果を記録するための配列 res を初期化 + int n = nums.Length; + int[] res = new int[n]; + for (int i = n - 1; i >= 0; i--) { + int num = nums[i]; + res[counter[num] - 1] = num; // num を対応するインデックスに配置 + counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る + } + // 結果配列 res で元の配列 nums を上書きする + for (int i = 0; i < n; i++) { + nums[i] = res[i]; + } + } ``` === "Go" ```go title="counting_sort.go" - [class]{}-[func]{countingSort} + /* 計数ソート */ + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである + func countingSort(nums []int) { + // 1. 配列の最大要素 m を求める + m := 0 + for _, num := range nums { + if num > m { + m = num + } + } + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + counter := make([]int, m+1) + for _, num := range nums { + counter[num]++ + } + // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + // つまり counter[num]-1 は、num が res に最後に現れるインデックス + for i := 0; i < m; i++ { + counter[i+1] += counter[i] + } + // 4. nums を逆順に走査し、各要素を結果配列 res に格納する + // 結果を記録するための配列 res を初期化 + n := len(nums) + res := make([]int, n) + for i := n - 1; i >= 0; i-- { + num := nums[i] + // num を対応するインデックスに配置 + res[counter[num]-1] = num + // 累積和を 1 減らして、次に num を配置するインデックスを得る + counter[num]-- + } + // 結果配列 res で元の配列 nums を上書きする + copy(nums, res) + } ``` === "Swift" ```swift title="counting_sort.swift" - [class]{}-[func]{countingSort} + /* 計数ソート */ + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである + func countingSort(nums: inout [Int]) { + // 1. 配列の最大要素 m を求める + let m = nums.max()! + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + var counter = Array(repeating: 0, count: m + 1) + for num in nums { + counter[num] += 1 + } + // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + // つまり counter[num]-1 は、num が res に最後に現れるインデックス + for i in 0 ..< m { + counter[i + 1] += counter[i] + } + // 4. nums を逆順に走査し、各要素を結果配列 res に格納する + // 結果を記録するための配列 res を初期化 + var res = Array(repeating: 0, count: nums.count) + for i in nums.indices.reversed() { + let num = nums[i] + res[counter[num] - 1] = num // num を対応するインデックスに配置 + counter[num] -= 1 // 累積和を 1 減らして、次に num を配置するインデックスを得る + } + // 結果配列 res で元の配列 nums を上書きする + for i in nums.indices { + nums[i] = res[i] + } + } ``` === "JS" ```javascript title="counting_sort.js" - [class]{}-[func]{countingSort} + /* 計数ソート */ + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである + function countingSort(nums) { + // 1. 配列の最大要素 m を求める + let m = Math.max(...nums); + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + const counter = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + // つまり counter[num]-1 は、num が res に最後に現れるインデックス + for (let i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. nums を逆順に走査し、各要素を結果配列 res に格納する + // 結果を記録するための配列 res を初期化 + const n = nums.length; + const res = new Array(n); + for (let i = n - 1; i >= 0; i--) { + const num = nums[i]; + res[counter[num] - 1] = num; // num を対応するインデックスに配置 + counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る + } + // 結果配列 res で元の配列 nums を上書きする + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } ``` === "TS" ```typescript title="counting_sort.ts" - [class]{}-[func]{countingSort} + /* 計数ソート */ + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである + function countingSort(nums: number[]): void { + // 1. 配列の最大要素 m を求める + let m: number = Math.max(...nums); + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + const counter: number[] = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + // つまり counter[num]-1 は、num が res に最後に現れるインデックス + for (let i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. nums を逆順に走査し、各要素を結果配列 res に格納する + // 結果を記録するための配列 res を初期化 + const n = nums.length; + const res: number[] = new Array(n); + for (let i = n - 1; i >= 0; i--) { + const num = nums[i]; + res[counter[num] - 1] = num; // num を対応するインデックスに配置 + counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る + } + // 結果配列 res で元の配列 nums を上書きする + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } ``` === "Dart" ```dart title="counting_sort.dart" - [class]{}-[func]{countingSort} + /* 計数ソート */ + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである + void countingSort(List nums) { + // 1. 配列の最大要素 m を求める + int m = 0; + for (int _num in nums) { + m = max(m, _num); + } + // 2. 各数値の出現回数を数える + // counter[_num] は _num の出現回数を表す + List counter = List.filled(m + 1, 0); + for (int _num in nums) { + counter[_num]++; + } + // 3. counter の累積和を求め、「出現回数」を「末尾インデックス」に変換する + // つまり counter[_num]-1 は、res において _num が最後に出現する位置のインデックスである + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. nums を逆順に走査し、各要素を結果配列 res に格納する + // 結果を記録するための配列 res を初期化 + int n = nums.length; + List res = List.filled(n, 0); + for (int i = n - 1; i >= 0; i--) { + int _num = nums[i]; + res[counter[_num] - 1] = _num; // _num を対応する添字に配置 + counter[_num]--; // 累積和を 1 減らし、次に _num を配置するインデックスを得る + } + // 結果配列 res で元の配列 nums を上書きする + nums.setAll(0, res); + } ``` === "Rust" ```rust title="counting_sort.rs" - [class]{}-[func]{counting_sort} + /* 計数ソート */ + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである + fn counting_sort(nums: &mut [i32]) { + // 1. 配列の最大要素 m を求める + let m = *nums.iter().max().unwrap() as usize; + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + let mut counter = vec![0; m + 1]; + for &num in nums.iter() { + counter[num as usize] += 1; + } + // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + // つまり counter[num]-1 は、num が res に最後に現れるインデックス + for i in 0..m { + counter[i + 1] += counter[i]; + } + // 4. nums を逆順に走査し、各要素を結果配列 res に格納する + // 結果を記録するための配列 res を初期化 + let n = nums.len(); + let mut res = vec![0; n]; + for i in (0..n).rev() { + let num = nums[i]; + res[counter[num as usize] - 1] = num; // num を対応するインデックスに配置 + counter[num as usize] -= 1; // 累積和を 1 減らして、次に num を配置するインデックスを得る + } + // 結果配列 res で元の配列 nums を上書きする + nums.copy_from_slice(&res) + } ``` === "C" ```c title="counting_sort.c" - [class]{}-[func]{countingSort} + /* 計数ソート */ + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである + void countingSort(int nums[], int size) { + // 1. 配列の最大要素 m を求める + int m = 0; + for (int i = 0; i < size; i++) { + if (nums[i] > m) { + m = nums[i]; + } + } + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + int *counter = calloc(m, sizeof(int)); + for (int i = 0; i < size; i++) { + counter[nums[i]]++; + } + // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + // つまり counter[num]-1 は、num が res に最後に現れるインデックス + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. nums を逆順に走査し、各要素を結果配列 res に格納する + // 結果を記録するための配列 res を初期化 + int *res = malloc(sizeof(int) * size); + for (int i = size - 1; i >= 0; i--) { + int num = nums[i]; + res[counter[num] - 1] = num; // num を対応するインデックスに配置 + counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る + } + // 結果配列 res で元の配列 nums を上書きする + memcpy(nums, res, size * sizeof(int)); + // 5. メモリを解放する + free(res); + free(counter); + } ``` === "Kotlin" ```kotlin title="counting_sort.kt" - [class]{}-[func]{countingSort} + /* 計数ソート */ + // 完全な実装で、オブジェクトをソートでき、かつ安定ソートである + fun countingSort(nums: IntArray) { + // 1. 配列の最大要素 m を求める + var m = 0 + for (num in nums) { + m = max(m, num) + } + // 2. 各数値の出現回数を数える + // counter[num] は num の出現回数を表す + val counter = IntArray(m + 1) + for (num in nums) { + counter[num]++ + } + // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する + // つまり counter[num]-1 は、num が res に最後に現れるインデックス + for (i in 0.. + -## 11.9.4   制限事項 +## 11.9.3   アルゴリズムの特性 -今までに、計数ソートは非常に巧妙だと感じるかもしれません。単に量をカウントするだけで効率的なソートを実現できるからです。しかし、計数ソートを使用するための前提条件は比較的厳しいです。 +- **時間計算量は $O(n + m)$、非適応ソート** :`nums` の走査と `counter` の走査が含まれ、いずれも線形時間です。一般には $n \gg m$ であり、時間計算量は $O(n)$ に近づきます。 +- **空間計算量は $O(n + m)$、非インプレースソート**:長さがそれぞれ $n$ と $m$ の配列 `res` と `counter` を利用します。 +- **安定ソート**:`res` に要素を埋める順序が「右から左」であるため、`nums` を逆順に走査することで等しい要素どうしの相対位置が変化するのを防ぎ、安定ソートを実現できます。実際には、`nums` を順方向に走査しても正しいソート結果は得られますが、その結果は安定ではありません。 -**計数ソートは非負整数にのみ適用できます**。他のタイプのデータに適用したい場合、これらのデータが要素の元の順序を変更することなく非負整数に変換できることを保証する必要があります。例えば、負の整数を含む配列の場合、最初にすべての数に定数を加えて、すべてを正の数に変換し、ソート完了後に元に戻すことができます。 +## 11.9.4   制約 -**計数ソートは値の範囲が小さい大きなデータセットに適しています**。例えば、上記の例では、$m$ は大きすぎるべきではありません。そうでなければ、あまりにも多くのスペースを占有してしまいます。そして $n \ll m$ の場合、計数ソートは $O(m)$ 時間を使用し、$O(n \log n)$ ソートアルゴリズムより遅い可能性があります。 +ここまで読むと、計数ソートは非常に巧妙で、個数を数えるだけで効率的なソートを実現できると感じるかもしれません。しかし、計数ソートを利用するための前提条件は比較的厳格です。 + +**計数ソートは非負整数にしか適用できません**。ほかの型のデータに適用したい場合は、それらのデータを非負整数に変換でき、かつ変換の過程で要素間の相対的な大小関係が変わらないことを保証する必要があります。たとえば、負数を含む整数配列に対しては、すべての数値に定数を加えて正数へ変換し、ソート後に元へ戻すことができます。 + +**計数ソートはデータ量が多く、値域が小さい場合に適しています**。たとえば上記の例では $m$ が大きすぎてはならず、そうでないと過剰な空間を消費します。また、$n \ll m$ のとき、計数ソートは $O(m)$ 時間を要するため、$O(n \log n)$ のソートアルゴリズムより遅くなる可能性があります。 diff --git a/ja/docs/chapter_sorting/heap_sort.md b/ja/docs/chapter_sorting/heap_sort.md index f4e7b5ee8..8e14f7f9c 100644 --- a/ja/docs/chapter_sorting/heap_sort.md +++ b/ja/docs/chapter_sorting/heap_sort.md @@ -6,30 +6,30 @@ comments: true !!! tip - この節を読む前に、「ヒープ」の章を必ず完了させてください。 + 本節を読む前に、「ヒープ」の章を学習済みであることを確認してください。 -ヒープソートは、ヒープデータ構造に基づく効率的なソートアルゴリズムです。すでに学習した「ヒープの構築」と「要素の抽出」操作を使用してヒープソートを実装できます。 +ヒープソート(heap sort)は、ヒープデータ構造に基づいて実装される効率的なソートアルゴリズムです。すでに学んだ「ヒープ構築操作」と「要素の取り出し操作」を利用してヒープソートを実現できます。 -1. 配列を入力し、最小ヒープを構築します。ここで、最小要素がヒープの頂上に位置します。 -2. 継続的に抽出操作を実行し、抽出された要素を順次記録して、最小から最大までのソート済みリストを取得します。 +1. 配列を入力して最小ヒープを構築すると、このとき最小要素はヒープの頂点にあります。 +2. 取り出し操作を繰り返し実行し、取り出された要素を順に記録すれば、昇順に並んだ列が得られます。 -上記の方法は実現可能ですが、ポップされた要素を格納するための追加の配列が必要で、やや空間を消費します。実際には、通常、より優雅な実装を使用します。 +以上の方法でも実行できますが、取り出した要素を保存するために追加の配列が必要となり、空間をやや無駄にします。実際には、通常はより洗練された実装方法を用います。 ## 11.7.1   アルゴリズムの流れ -配列の長さを $n$ とすると、ヒープソートの過程は以下の通りです。 +配列の長さを $n$ とすると、ヒープソートの流れは次図のとおりです。 -1. 配列を入力し、最大ヒープを構築します。この手順の後、最大要素がヒープの頂上に位置します。 -2. ヒープの頂上要素(最初の要素)とヒープの底部要素(最後の要素)を交換します。この交換の後、ヒープの長さを $1$ 減らし、ソート済み要素の数を $1$ 増やします。 -3. ヒープの頂上から開始して、上から下へのsift-down操作を実行します。sift-downの後、ヒープの性質が復元されます。 -4. 手順 `2.` と `3.` を繰り返します。$n - 1$ ラウンドループして、配列のソートを完了します。 +1. 配列を入力して最大ヒープを構築します。完了後、最大要素はヒープの頂点にあります。 +2. ヒープ頂点の要素(最初の要素)とヒープ末尾の要素(最後の要素)を交換します。交換後、ヒープの長さは $1$ 減少し、整列済み要素数は $1$ 増加します。 +3. ヒープ頂点の要素から始めて、上から下へヒープ化操作(sift down)を実行します。ヒープ化が完了すると、ヒープの性質が回復します。 +4. 第 `2.` ステップと第 `3.` ステップを繰り返し実行します。これを $n - 1$ 回繰り返すと、配列の整列が完了します。 !!! tip - 実際、要素抽出操作も手順 `2.` と `3.` を含み、抽出された要素をヒープから削除する追加の手順があります。 + 実際には、要素の取り出し操作にも第 `2.` ステップと第 `3.` ステップが含まれており、要素を取り出す処理が 1 つ加わるだけです。 === "<1>" - ![ヒープソートの過程](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" } + ![ヒープソートの手順](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" } === "<2>" ![heap_sort_step2](heap_sort.assets/heap_sort_step2.png){ class="animation-figure" } @@ -64,17 +64,17 @@ comments: true === "<12>" ![heap_sort_step12](heap_sort.assets/heap_sort_step12.png){ class="animation-figure" } -

図 11-12   ヒープソートの過程

+

図 11-12   ヒープソートの手順

-コードの実装では、「ヒープ」の章からのsift-down関数 `sift_down()` を使用しました。最大要素が抽出されるにつれてヒープの長さが減少するため、`sift_down()` 関数に長さパラメータ $n$ を追加して、ヒープの現在の有効長を指定する必要があることに注意することが重要です。コードは以下の通りです: +コード実装では、「ヒープ」の章と同じ上から下へのヒープ化 `sift_down()` 関数を使用します。注意すべき点として、ヒープの長さは最大要素を取り出すたびに短くなるため、`sift_down()` 関数に長さパラメータ $n$ を追加し、ヒープの現在の有効な長さを指定する必要があります。コードは以下のとおりです。 === "Python" ```python title="heap_sort.py" def sift_down(nums: list[int], n: int, i: int): - """ヒープの長さが n、ノード i から上から下へヒープ化を開始""" + """ヒープの長さは n。ノード i から下方向にヒープ化""" while True: - # i、l、r の中で最大のノードを判定し、ma とする + # ノード i, l, r のうち値が最大のノードを ma とする l = 2 * i + 1 r = 2 * i + 2 ma = i @@ -82,34 +82,34 @@ comments: true ma = l if r < n and nums[r] > nums[ma]: ma = r - # ノード i が最大または l、r のインデックスが範囲外の場合、さらなるヒープ化は不要、ループを抜ける + # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける if ma == i: break - # 2つのノードを交換 + # 2 つのノードを交換 nums[i], nums[ma] = nums[ma], nums[i] - # 下向きにヒープ化をループ + # ループで上から下へヒープ化 i = ma def heap_sort(nums: list[int]): """ヒープソート""" - # ヒープ構築操作:葉ノード以外のすべてのノードをヒープ化 + # ヒープ構築:葉ノード以外のすべてのノードをヒープ化する for i in range(len(nums) // 2 - 1, -1, -1): sift_down(nums, len(nums), i) - # ヒープから最大要素を抽出し、n-1 回繰り返す + # ヒープから最大要素を取り出し、n-1 回繰り返す for i in range(len(nums) - 1, 0, -1): - # ルートノードと最も右の葉ノードを交換(最初の要素と最後の要素を交換) + # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) nums[0], nums[i] = nums[i], nums[0] - # ルートノードから上から下へヒープ化を開始 + # 根ノードを起点に、上から下へヒープ化 sift_down(nums, i, 0) ``` === "C++" ```cpp title="heap_sort.cpp" - /* ヒープの長さはn、ノードiから上から下へヒープ化を開始 */ + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ void siftDown(vector &nums, int n, int i) { while (true) { - // i、l、r の中で最大のノードを決定し、maとして記録 + // ノード i, l, r のうち値が最大のノードを ma とする int l = 2 * i + 1; int r = 2 * i + 2; int ma = i; @@ -117,28 +117,28 @@ comments: true ma = l; if (r < n && nums[r] > nums[ma]) ma = r; - // ノードiが最大か、インデックスl、rが境界外の場合、それ以上のヒープ化は不要で終了 + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける if (ma == i) { break; } - // 二つのノードを交換 + // 2 つのノードを交換 swap(nums[i], nums[ma]); - // 下向きにヒープ化をループ + // ループで上から下へヒープ化 i = ma; } } /* ヒープソート */ void heapSort(vector &nums) { - // ヒープ構築操作:葉以外のすべてのノードをヒープ化 + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する for (int i = nums.size() / 2 - 1; i >= 0; --i) { siftDown(nums, nums.size(), i); } - // ヒープから最大要素を抽出し、n-1回繰り返す + // ヒープから最大要素を取り出し、n-1 回繰り返す for (int i = nums.size() - 1; i > 0; --i) { - // ルートノードを最右葉ノードと交換(最初の要素を最後の要素と交換) + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) swap(nums[0], nums[i]); - // ルートノードから上から下へヒープ化を開始 + // 根ノードを起点に、上から下へヒープ化 siftDown(nums, i, 0); } } @@ -147,10 +147,10 @@ comments: true === "Java" ```java title="heap_sort.java" - /* ヒープの長さは n、ノード i から上から下へヒープ化開始 */ + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ void siftDown(int[] nums, int n, int i) { while (true) { - // i, l, r の中で最大のノードを判定し、ma とする + // ノード i, l, r のうち値が最大のノードを ma とする int l = 2 * i + 1; int r = 2 * i + 2; int ma = i; @@ -158,31 +158,31 @@ comments: true ma = l; if (r < n && nums[r] > nums[ma]) ma = r; - // ノード i が最大、またはインデックス l, r が範囲外の場合、さらなるヒープ化は不要、ブレーク + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける if (ma == i) break; - // 2つのノードを交換 + // 2 つのノードを交換 int temp = nums[i]; nums[i] = nums[ma]; nums[ma] = temp; - // 下向きにヒープ化をループ + // ループで上から下へヒープ化 i = ma; } } /* ヒープソート */ void heapSort(int[] nums) { - // ヒープ構築操作: 葉ノード以外のすべてのノードをヒープ化 + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する for (int i = nums.length / 2 - 1; i >= 0; i--) { siftDown(nums, nums.length, i); } - // ヒープから最大要素を抽出し、n-1 回繰り返し + // ヒープから最大要素を取り出し、n-1 回繰り返す for (int i = nums.length - 1; i > 0; i--) { - // ルートノードと最も右の葉ノードを交換(最初の要素と最後の要素を交換) + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) int tmp = nums[0]; nums[0] = nums[i]; nums[i] = tmp; - // ルートノードから上から下へヒープ化開始 + // 根ノードを起点に、上から下へヒープ化 siftDown(nums, i, 0); } } @@ -191,85 +191,434 @@ comments: true === "C#" ```csharp title="heap_sort.cs" - [class]{heap_sort}-[func]{SiftDown} + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ + void SiftDown(int[] nums, int n, int i) { + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + int l = 2 * i + 1; + int r = 2 * i + 2; + int ma = i; + if (l < n && nums[l] > nums[ma]) + ma = l; + if (r < n && nums[r] > nums[ma]) + ma = r; + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma == i) + break; + // 2 つのノードを交換 + (nums[ma], nums[i]) = (nums[i], nums[ma]); + // ループで上から下へヒープ化 + i = ma; + } + } - [class]{heap_sort}-[func]{HeapSort} + /* ヒープソート */ + void HeapSort(int[] nums) { + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する + for (int i = nums.Length / 2 - 1; i >= 0; i--) { + SiftDown(nums, nums.Length, i); + } + // ヒープから最大要素を取り出し、n-1 回繰り返す + for (int i = nums.Length - 1; i > 0; i--) { + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + (nums[i], nums[0]) = (nums[0], nums[i]); + // 根ノードを起点に、上から下へヒープ化 + SiftDown(nums, i, 0); + } + } ``` === "Go" ```go title="heap_sort.go" - [class]{}-[func]{siftDown} + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ + func siftDown(nums *[]int, n, i int) { + for true { + // ノード i, l, r のうち値が最大のノードを ma とする + l := 2*i + 1 + r := 2*i + 2 + ma := i + if l < n && (*nums)[l] > (*nums)[ma] { + ma = l + } + if r < n && (*nums)[r] > (*nums)[ma] { + ma = r + } + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if ma == i { + break + } + // 2 つのノードを交換 + (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i] + // ループで上から下へヒープ化 + i = ma + } + } - [class]{}-[func]{heapSort} + /* ヒープソート */ + func heapSort(nums *[]int) { + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する + for i := len(*nums)/2 - 1; i >= 0; i-- { + siftDown(nums, len(*nums), i) + } + // ヒープから最大要素を取り出し、n-1 回繰り返す + for i := len(*nums) - 1; i > 0; i-- { + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0] + // 根ノードを起点に、上から下へヒープ化 + siftDown(nums, i, 0) + } + } ``` === "Swift" ```swift title="heap_sort.swift" - [class]{}-[func]{siftDown} + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ + func siftDown(nums: inout [Int], n: Int, i: Int) { + var i = i + while true { + // ノード i, l, r のうち値が最大のノードを ma とする + let l = 2 * i + 1 + let r = 2 * i + 2 + var ma = i + if l < n, nums[l] > nums[ma] { + ma = l + } + if r < n, nums[r] > nums[ma] { + ma = r + } + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if ma == i { + break + } + // 2 つのノードを交換 + nums.swapAt(i, ma) + // ループで上から下へヒープ化 + i = ma + } + } - [class]{}-[func]{heapSort} + /* ヒープソート */ + func heapSort(nums: inout [Int]) { + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する + for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) { + siftDown(nums: &nums, n: nums.count, i: i) + } + // ヒープから最大要素を取り出し、n-1 回繰り返す + for i in nums.indices.dropFirst().reversed() { + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + nums.swapAt(0, i) + // 根ノードを起点に、上から下へヒープ化 + siftDown(nums: &nums, n: i, i: 0) + } + } ``` === "JS" ```javascript title="heap_sort.js" - [class]{}-[func]{siftDown} + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ + function siftDown(nums, n, i) { + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + let l = 2 * i + 1; + let r = 2 * i + 2; + let ma = i; + if (l < n && nums[l] > nums[ma]) { + ma = l; + } + if (r < n && nums[r] > nums[ma]) { + ma = r; + } + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma === i) { + break; + } + // 2 つのノードを交換 + [nums[i], nums[ma]] = [nums[ma], nums[i]]; + // ループで上から下へヒープ化 + i = ma; + } + } - [class]{}-[func]{heapSort} + /* ヒープソート */ + function heapSort(nums) { + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する + for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) { + siftDown(nums, nums.length, i); + } + // ヒープから最大要素を取り出し、n-1 回繰り返す + for (let i = nums.length - 1; i > 0; i--) { + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + [nums[0], nums[i]] = [nums[i], nums[0]]; + // 根ノードを起点に、上から下へヒープ化 + siftDown(nums, i, 0); + } + } ``` === "TS" ```typescript title="heap_sort.ts" - [class]{}-[func]{siftDown} + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ + function siftDown(nums: number[], n: number, i: number): void { + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + let l = 2 * i + 1; + let r = 2 * i + 2; + let ma = i; + if (l < n && nums[l] > nums[ma]) { + ma = l; + } + if (r < n && nums[r] > nums[ma]) { + ma = r; + } + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma === i) { + break; + } + // 2 つのノードを交換 + [nums[i], nums[ma]] = [nums[ma], nums[i]]; + // ループで上から下へヒープ化 + i = ma; + } + } - [class]{}-[func]{heapSort} + /* ヒープソート */ + function heapSort(nums: number[]): void { + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する + for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) { + siftDown(nums, nums.length, i); + } + // ヒープから最大要素を取り出し、n-1 回繰り返す + for (let i = nums.length - 1; i > 0; i--) { + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + [nums[0], nums[i]] = [nums[i], nums[0]]; + // 根ノードを起点に、上から下へヒープ化 + siftDown(nums, i, 0); + } + } ``` === "Dart" ```dart title="heap_sort.dart" - [class]{}-[func]{siftDown} + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ + void siftDown(List nums, int n, int i) { + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + int l = 2 * i + 1; + int r = 2 * i + 2; + int ma = i; + if (l < n && nums[l] > nums[ma]) ma = l; + if (r < n && nums[r] > nums[ma]) ma = r; + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma == i) break; + // 2 つのノードを交換 + int temp = nums[i]; + nums[i] = nums[ma]; + nums[ma] = temp; + // ループで上から下へヒープ化 + i = ma; + } + } - [class]{}-[func]{heapSort} + /* ヒープソート */ + void heapSort(List nums) { + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する + for (int i = nums.length ~/ 2 - 1; i >= 0; i--) { + siftDown(nums, nums.length, i); + } + // ヒープから最大要素を取り出し、n-1 回繰り返す + for (int i = nums.length - 1; i > 0; i--) { + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + int tmp = nums[0]; + nums[0] = nums[i]; + nums[i] = tmp; + // 根ノードを起点に、上から下へヒープ化 + siftDown(nums, i, 0); + } + } ``` === "Rust" ```rust title="heap_sort.rs" - [class]{}-[func]{sift_down} + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ + fn sift_down(nums: &mut [i32], n: usize, mut i: usize) { + loop { + // ノード i, l, r のうち値が最大のノードを ma とする + let l = 2 * i + 1; + let r = 2 * i + 2; + let mut ma = i; + if l < n && nums[l] > nums[ma] { + ma = l; + } + if r < n && nums[r] > nums[ma] { + ma = r; + } + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if ma == i { + break; + } + // 2 つのノードを交換 + nums.swap(i, ma); + // ループで上から下へヒープ化 + i = ma; + } + } - [class]{}-[func]{heap_sort} + /* ヒープソート */ + fn heap_sort(nums: &mut [i32]) { + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する + for i in (0..nums.len() / 2).rev() { + sift_down(nums, nums.len(), i); + } + // ヒープから最大要素を取り出し、n-1 回繰り返す + for i in (1..nums.len()).rev() { + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + nums.swap(0, i); + // 根ノードを起点に、上から下へヒープ化 + sift_down(nums, i, 0); + } + } ``` === "C" ```c title="heap_sort.c" - [class]{}-[func]{siftDown} + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ + void siftDown(int nums[], int n, int i) { + while (1) { + // ノード i, l, r のうち値が最大のノードを ma とする + int l = 2 * i + 1; + int r = 2 * i + 2; + int ma = i; + if (l < n && nums[l] > nums[ma]) + ma = l; + if (r < n && nums[r] > nums[ma]) + ma = r; + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma == i) { + break; + } + // 2 つのノードを交換 + int temp = nums[i]; + nums[i] = nums[ma]; + nums[ma] = temp; + // ループで上から下へヒープ化 + i = ma; + } + } - [class]{}-[func]{heapSort} + /* ヒープソート */ + void heapSort(int nums[], int n) { + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する + for (int i = n / 2 - 1; i >= 0; --i) { + siftDown(nums, n, i); + } + // ヒープから最大要素を取り出し、n-1 回繰り返す + for (int i = n - 1; i > 0; --i) { + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + int tmp = nums[0]; + nums[0] = nums[i]; + nums[i] = tmp; + // 根ノードを起点に、上から下へヒープ化 + siftDown(nums, i, 0); + } + } ``` === "Kotlin" ```kotlin title="heap_sort.kt" - [class]{}-[func]{siftDown} + /* ヒープの長さは n。ノード i から下方向にヒープ化 */ + fun siftDown(nums: IntArray, n: Int, li: Int) { + var i = li + while (true) { + // ノード i, l, r のうち値が最大のノードを ma とする + val l = 2 * i + 1 + val r = 2 * i + 2 + var ma = i + if (l < n && nums[l] > nums[ma]) + ma = l + if (r < n && nums[r] > nums[ma]) + ma = r + // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + if (ma == i) + break + // 2 つのノードを交換 + val temp = nums[i] + nums[i] = nums[ma] + nums[ma] = temp + // ループで上から下へヒープ化 + i = ma + } + } - [class]{}-[func]{heapSort} + /* ヒープソート */ + fun heapSort(nums: IntArray) { + // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する + for (i in nums.size / 2 - 1 downTo 0) { + siftDown(nums, nums.size, i) + } + // ヒープから最大要素を取り出し、n-1 回繰り返す + for (i in nums.size - 1 downTo 1) { + // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + val temp = nums[0] + nums[0] = nums[i] + nums[i] = temp + // 根ノードを起点に、上から下へヒープ化 + siftDown(nums, i, 0) + } + } ``` === "Ruby" ```ruby title="heap_sort.rb" - [class]{}-[func]{sift_down} + ### ヒープ長 n で、ノード i から上から下へヒープ化 ### + def sift_down(nums, n, i) + while true + # ノード i, l, r のうち値が最大のノードを ma とする + l = 2 * i + 1 + r = 2 * i + 2 + ma = i + ma = l if l < n && nums[l] > nums[ma] + ma = r if r < n && nums[r] > nums[ma] + # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける + break if ma == i + # 2 つのノードを交換 + nums[i], nums[ma] = nums[ma], nums[i] + # ループで上から下へヒープ化 + i = ma + end + end - [class]{}-[func]{heap_sort} + ### ヒープソート ### + def heap_sort(nums) + # ヒープ構築:葉ノード以外のすべてのノードをヒープ化する + (nums.length / 2 - 1).downto(0) do |i| + sift_down(nums, nums.length, i) + end + # ヒープから最大要素を取り出し、n-1 回繰り返す + (nums.length - 1).downto(1) do |i| + # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換) + nums[0], nums[i] = nums[i], nums[0] + # 根ノードを起点に、上から下へヒープ化 + sift_down(nums, i, 0) + end + end ``` -## 11.7.2   アルゴリズムの特徴 +??? pythontutor "コードの可視化" -- **時間計算量は $O(n \log n)$、非適応ソート**:ヒープの構築は $O(n)$ 時間を使用します。ヒープから最大要素を抽出するには $O(\log n)$ 時間がかかり、$n - 1$ ラウンドループします。 -- **空間計算量は $O(1)$、インプレースソート**:いくつかのポインタ変数が $O(1)$ 空間を使用します。要素の交換とヒープ化操作は元の配列で実行されます。 -- **非安定ソート**:ヒープの頂上と底部要素の交換中に、等しい要素の相対位置が変わる可能性があります。 +
+ + +## 11.7.2   アルゴリズムの特性 + +- **時間計算量は $O(n \log n)$、非適応ソート**:ヒープ構築操作には $O(n)$ の時間がかかります。ヒープから最大要素を取り出す時間計算量は $O(\log n)$ であり、これを合計 $n - 1$ 回繰り返します。 +- **空間計算量は $O(1)$、インプレースソート**:いくつかのポインタ変数が使う空間は $O(1)$ です。要素の交換とヒープ化操作はいずれも元の配列上で行われます。 +- **非安定ソート**:ヒープ頂点の要素とヒープ末尾の要素を交換する際、等しい要素どうしの相対位置が変化する可能性があります。 diff --git a/ja/docs/chapter_sorting/index.md b/ja/docs/chapter_sorting/index.md index e7736403b..96eb8963f 100644 --- a/ja/docs/chapter_sorting/index.md +++ b/ja/docs/chapter_sorting/index.md @@ -5,13 +5,13 @@ icon: material/sort-ascending # 第 11 章   ソート -![Sorting](../assets/covers/chapter_sorting.jpg){ class="cover-image" } +![ソート](../assets/covers/chapter_sorting.jpg){ class="cover-image" } !!! abstract - ソートは混沌を秩序に変える魔法の鍵のようなもので、データをより効率的に理解し処理することを可能にします。 + ソートは、混沌を秩序へと変える魔法の鍵のようなものであり、データをより効率的に理解し処理することを可能にします。 - 単純な昇順であろうと複雑なカテゴリ配列であろうと、ソートはデータの調和美を明らかにします。 + 単純な昇順であれ、複雑な分類配列であれ、ソートはデータの調和のとれた美しさを私たちに示してくれます。 ## 章の内容 diff --git a/ja/docs/chapter_sorting/insertion_sort.md b/ja/docs/chapter_sorting/insertion_sort.md index 854f1d31c..95bcd59bf 100644 --- a/ja/docs/chapter_sorting/insertion_sort.md +++ b/ja/docs/chapter_sorting/insertion_sort.md @@ -4,45 +4,45 @@ comments: true # 11.4   挿入ソート -挿入ソートは、トランプのデッキを手動でソートするプロセスによく似た動作をするシンプルなソートアルゴリズムです。 +挿入ソート(insertion sort)は単純なソートアルゴリズムであり、その動作原理は手作業でトランプの山を整える過程と非常によく似ています。 -具体的には、未ソート区間からベース要素を選択し、その左側のソート済み区間の要素と比較して、要素を正しい位置に挿入します。 +具体的には、未ソート区間から基準要素を 1 つ選び、その要素を左側の整列済み区間の要素と 1 つずつ比較し、正しい位置に挿入します。 -下図は、要素が配列に挿入される方法を示しています。ベース要素を`base`とすると、ターゲットインデックスから`base`までのすべての要素を右に1つずつシフトし、その後`base`をターゲットインデックスに割り当てる必要があります。 +以下の図は、配列に要素を挿入する操作の流れを示しています。基準要素を `base` とすると、目的のインデックスから `base` までのすべての要素を 1 つずつ右に移動し、その後 `base` を目的のインデックスに代入する必要があります。 -![Single insertion operation](insertion_sort.assets/insertion_operation.png){ class="animation-figure" } +![1 回の挿入操作](insertion_sort.assets/insertion_operation.png){ class="animation-figure" } -

図 11-6   Single insertion operation

+

図 11-6   1 回の挿入操作

-## 11.4.1   アルゴリズムプロセス +## 11.4.1   アルゴリズムの流れ -挿入ソートの全体的なプロセスは下図に示されます。 +挿入ソート全体の流れを以下の図に示します。 -1. 配列の最初の要素をソート済みとみなします。 -2. 2番目の要素を`base`として選択し、正しい位置に挿入して、**最初の2つの要素をソート済みにします**。 -3. 3番目の要素を`base`として選択し、正しい位置に挿入して、**最初の3つの要素をソート済みにします**。 -4. この方法で続行し、最後の反復では、最後の要素を`base`として取り、正しい位置に挿入した後、**すべての要素がソートされます**。 +1. 初期状態では、配列の 1 番目の要素はすでに整列済みです。 +2. 配列の 2 番目の要素を `base` として選び、正しい位置に挿入すると、**配列の先頭 2 要素が整列済み**になります。 +3. 3 番目の要素を `base` として選び、正しい位置に挿入すると、**配列の先頭 3 要素が整列済み**になります。 +4. このように繰り返し、最後のラウンドで最後の要素を `base` として選んで正しい位置に挿入すると、**すべての要素が整列済み**になります。 -![Insertion sort process](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" } +![挿入ソートの流れ](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" } -

図 11-7   Insertion sort process

+

図 11-7   挿入ソートの流れ

-コード例は以下の通りです: +コード例は以下のとおりです。 === "Python" ```python title="insertion_sort.py" def insertion_sort(nums: list[int]): """挿入ソート""" - # 外側のループ:ソート済み範囲は [0, i-1] + # 外側ループ:整列済み区間は [0, i-1] for i in range(1, len(nums)): base = nums[i] j = i - 1 - # 内側のループ:base をソート済み範囲 [0, i-1] の正しい位置に挿入 + # 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する while j >= 0 and nums[j] > base: - nums[j + 1] = nums[j] # nums[j] を右に1つ移動 + nums[j + 1] = nums[j] # nums[j] を 1 つ右へ移動する j -= 1 - nums[j + 1] = base # base を正しい位置に代入 + nums[j + 1] = base # base を正しい位置に配置する ``` === "C++" @@ -50,15 +50,15 @@ comments: true ```cpp title="insertion_sort.cpp" /* 挿入ソート */ void insertionSort(vector &nums) { - // 外側ループ:ソート済み範囲は[0, i-1] + // 外側ループ:整列済み区間は [0, i-1] for (int i = 1; i < nums.size(); i++) { int base = nums[i], j = i - 1; - // 内側ループ:baseをソート済み範囲[0, i-1]内の正しい位置に挿入 + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する while (j >= 0 && nums[j] > base) { - nums[j + 1] = nums[j]; // nums[j]を一つ右に移動 + nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する j--; } - nums[j + 1] = base; // baseを正しい位置に代入 + nums[j + 1] = base; // base を正しい位置に配置する } } ``` @@ -68,15 +68,15 @@ comments: true ```java title="insertion_sort.java" /* 挿入ソート */ void insertionSort(int[] nums) { - // 外側ループ: ソート済み範囲は [0, i-1] + // 外側ループ:整列済み区間は [0, i-1] for (int i = 1; i < nums.length; i++) { int base = nums[i], j = i - 1; - // 内側ループ: base をソート済み範囲 [0, i-1] の正しい位置に挿入 + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する while (j >= 0 && nums[j] > base) { - nums[j + 1] = nums[j]; // nums[j] を右に1つ移動 + nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する j--; } - nums[j + 1] = base; // base を正しい位置に代入 + nums[j + 1] = base; // base を正しい位置に配置する } } ``` @@ -84,79 +84,213 @@ comments: true === "C#" ```csharp title="insertion_sort.cs" - [class]{insertion_sort}-[func]{InsertionSort} + /* 挿入ソート */ + void InsertionSort(int[] nums) { + // 外側ループ:整列済み区間は [0, i-1] + for (int i = 1; i < nums.Length; i++) { + int bas = nums[i], j = i - 1; + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する + while (j >= 0 && nums[j] > bas) { + nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する + j--; + } + nums[j + 1] = bas; // base を正しい位置に配置する + } + } ``` === "Go" ```go title="insertion_sort.go" - [class]{}-[func]{insertionSort} + /* 挿入ソート */ + func insertionSort(nums []int) { + // 外側ループ:整列済み区間は [0, i-1] + for i := 1; i < len(nums); i++ { + base := nums[i] + j := i - 1 + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する + for j >= 0 && nums[j] > base { + nums[j+1] = nums[j] // nums[j] を 1 つ右へ移動する + j-- + } + nums[j+1] = base // base を正しい位置に配置する + } + } ``` === "Swift" ```swift title="insertion_sort.swift" - [class]{}-[func]{insertionSort} + /* 挿入ソート */ + func insertionSort(nums: inout [Int]) { + // 外側ループ:整列済み区間は [0, i-1] + for i in nums.indices.dropFirst() { + let base = nums[i] + var j = i - 1 + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する + while j >= 0, nums[j] > base { + nums[j + 1] = nums[j] // nums[j] を 1 つ右へ移動する + j -= 1 + } + nums[j + 1] = base // base を正しい位置に配置する + } + } ``` === "JS" ```javascript title="insertion_sort.js" - [class]{}-[func]{insertionSort} + /* 挿入ソート */ + function insertionSort(nums) { + // 外側ループ:整列済み区間は [0, i-1] + for (let i = 1; i < nums.length; i++) { + let base = nums[i], + j = i - 1; + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する + j--; + } + nums[j + 1] = base; // base を正しい位置に配置する + } + } ``` === "TS" ```typescript title="insertion_sort.ts" - [class]{}-[func]{insertionSort} + /* 挿入ソート */ + function insertionSort(nums: number[]): void { + // 外側ループ:整列済み区間は [0, i-1] + for (let i = 1; i < nums.length; i++) { + const base = nums[i]; + let j = i - 1; + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する + j--; + } + nums[j + 1] = base; // base を正しい位置に配置する + } + } ``` === "Dart" ```dart title="insertion_sort.dart" - [class]{}-[func]{insertionSort} + /* 挿入ソート */ + void insertionSort(List nums) { + // 外側ループ:整列済み区間は [0, i-1] + for (int i = 1; i < nums.length; i++) { + int base = nums[i], j = i - 1; + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する + j--; + } + nums[j + 1] = base; // base を正しい位置に配置する + } + } ``` === "Rust" ```rust title="insertion_sort.rs" - [class]{}-[func]{insertion_sort} + /* 挿入ソート */ + fn insertion_sort(nums: &mut [i32]) { + // 外側ループ:整列済み区間は [0, i-1] + for i in 1..nums.len() { + let (base, mut j) = (nums[i], (i - 1) as i32); + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する + while j >= 0 && nums[j as usize] > base { + nums[(j + 1) as usize] = nums[j as usize]; // nums[j] を 1 つ右へ移動する + j -= 1; + } + nums[(j + 1) as usize] = base; // base を正しい位置に配置する + } + } ``` === "C" ```c title="insertion_sort.c" - [class]{}-[func]{insertionSort} + /* 挿入ソート */ + void insertionSort(int nums[], int size) { + // 外側ループ:整列済み区間は [0, i-1] + for (int i = 1; i < size; i++) { + int base = nums[i], j = i - 1; + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する + while (j >= 0 && nums[j] > base) { + // nums[j] を 1 つ右へ移動する + nums[j + 1] = nums[j]; + j--; + } + // base を正しい位置に配置する + nums[j + 1] = base; + } + } ``` === "Kotlin" ```kotlin title="insertion_sort.kt" - [class]{}-[func]{insertionSort} + /* 挿入ソート */ + fun insertionSort(nums: IntArray) { + // 外側ループ: ソート済み要素は 1, 2, ..., n + for (i in nums.indices) { + val base = nums[i] + var j = i - 1 + // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j] // nums[j] を 1 つ右へ移動する + j-- + } + nums[j + 1] = base // base を正しい位置に配置する + } + } ``` === "Ruby" ```ruby title="insertion_sort.rb" - [class]{}-[func]{insertion_sort} + ### 挿入ソート ### + def insertion_sort(nums) + n = nums.length + # 外側ループ:整列済み区間は [0, i-1] + for i in 1...n + base = nums[i] + j = i - 1 + # 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する + while j >= 0 && nums[j] > base + nums[j + 1] = nums[j] # nums[j] を 1 つ右へ移動する + j -= 1 + end + nums[j + 1] = base # base を正しい位置に配置する + end + end ``` -## 11.4.2   アルゴリズムの特性 +??? pythontutor "コードの可視化" -- **時間計算量は$O(n^2)$、適応ソート**:最悪の場合、各挿入操作には$n - 1$、$n-2$、...、$2$、$1$のループが必要で、合計は$(n - 1) n / 2$となり、時間計算量は$O(n^2)$です。順序付きデータの場合、挿入操作は早期に終了します。入力配列が完全に順序付けられている場合、挿入ソートは最良時間計算量$O(n)$を実現します。 -- **空間計算量は$O(1)$、インプレースソート**:ポインタ$i$と$j$は定数量の追加空間を使用します。 -- **安定ソート**:挿入操作中、等しい要素の右側に要素を挿入し、順序を変更しません。 +
+ + +## 11.4.2   アルゴリズムの特徴 + +- **計算量は $O(n^2)$、適応的ソート**:最悪の場合、各挿入操作ではそれぞれ $n - 1$、$n-2$、$\dots$、$2$、$1$ 回のループが必要であり、合計は $(n - 1) n / 2$ となるため、時間計算量は $O(n^2)$ です。データが整列済みであれば、挿入操作は早期に終了します。入力配列が完全に整列済みである場合、挿入ソートは最良の時間計算量 $O(n)$ に達します。 +- **空間計算量は $O(1)$、インプレースソート**:ポインタ $i$ と $j$ は定数サイズの追加領域しか使用しません。 +- **安定ソート**:挿入操作の過程では、要素を等しい要素の右側に挿入するため、それらの順序は変化しません。 ## 11.4.3   挿入ソートの利点 -挿入ソートの時間計算量は$O(n^2)$で、次に学習するクイックソートの時間計算量は$O(n \log n)$です。挿入ソートはより高い時間計算量を持ちますが、**小さな入力サイズでは通常より高速です**。 +挿入ソートの時間計算量は $O(n^2)$ であり、これから学ぶクイックソートの時間計算量は $O(n \log n)$ です。挿入ソートの時間計算量のほうが大きいにもかかわらず、**データ量が小さい場合には、挿入ソートのほうが通常は高速**です。 -この結論は線形探索と二分探索の結論と似ています。時間計算量が$O(n \log n)$で分割統治戦略に基づくクイックソートなどのアルゴリズムは、多くの場合より多くの単位操作を含みます。小さな入力サイズでは、$n^2$と$n \log n$の数値は近く、計算量が支配的でなく、ラウンドあたりの単位操作数が決定的な役割を果たします。 +この結論は、線形探索と二分探索の適用条件に関する結論と似ています。クイックソートのような $O(n \log n)$ のアルゴリズムは分割統治法に基づくソートアルゴリズムであり、一般により多くの基本演算を含みます。一方、データ量が小さい場合は、$n^2$ と $n \log n$ の値は比較的近く、計算量が支配的ではなくなり、各ラウンドにおける基本演算の回数が決定的な役割を果たします。 -実際、多くのプログラミング言語(Javaなど)は、組み込みソート関数内で挿入ソートを使用しています。一般的なアプローチは:長い配列に対しては、クイックソートなどの分割統治戦略に基づくソートアルゴリズムを使用し、短い配列に対しては挿入ソートを直接使用します。 +実際、多くのプログラミング言語(たとえば Java)の組み込みソート関数では挿入ソートが採用されており、その大まかな考え方は次のとおりです。長い配列にはクイックソートなどの分割統治法に基づくソートアルゴリズムを使い、短い配列には直接挿入ソートを使います。 -バブルソート、選択ソート、挿入ソートはすべて時間計算量$O(n^2)$を持ちますが、実際には、**挿入ソートはバブルソートや選択ソートよりも一般的に使用されます**。主な理由は以下の通りです。 +バブルソート、選択ソート、挿入ソートはいずれも時間計算量が $O(n^2)$ ですが、実際には、**挿入ソートはバブルソートや選択ソートよりもはるかに高い頻度で使われます**。主な理由は次のとおりです。 -- バブルソートは要素交換に基づき、一時変数の使用が必要で、3つの単位操作を含みます;挿入ソートは要素代入に基づき、1つの単位操作のみが必要です。したがって、**バブルソートの計算オーバーヘッドは一般的に挿入ソートよりも高くなります**。 -- 選択ソートの時間計算量は常に$O(n^2)$です。**部分的に順序付けられたデータのセットが与えられた場合、挿入ソートは通常選択ソートよりも効率的です**。 -- 選択ソートは不安定で、マルチレベルソートに適用できません。 +- バブルソートは要素の交換によって実装され、1 つの一時変数を必要とするため、合計で 3 回の基本演算が関わります。これに対して、挿入ソートは要素の代入に基づいており、必要な基本演算は 1 回だけです。したがって、**バブルソートの計算コストは通常、挿入ソートより高くなります**。 +- 選択ソートの時間計算量はどのような場合でも $O(n^2)$ です。**部分的に整列されたデータが与えられた場合、挿入ソートは通常、選択ソートより効率的**です。 +- 選択ソートは安定ではないため、多段ソートには適用できません。 diff --git a/ja/docs/chapter_sorting/merge_sort.md b/ja/docs/chapter_sorting/merge_sort.md index 2b787a3e7..804c45d96 100644 --- a/ja/docs/chapter_sorting/merge_sort.md +++ b/ja/docs/chapter_sorting/merge_sort.md @@ -4,26 +4,26 @@ comments: true # 11.6   マージソート -マージソートは分割統治戦略に基づくソートアルゴリズムで、下図に示す「分割」と「マージ」フェーズを含みます。 +マージソート(merge sort)は分割統治戦略に基づくソートアルゴリズムであり、以下の図に示す「分割」と「マージ」の段階から構成されます。 -1. **分割フェーズ**:中点から配列を再帰的に分割し、長い配列のソート問題をより短い配列に変換します。 -2. **マージフェーズ**:サブ配列の長さが1になったときに分割を停止し、その後マージを開始します。2つの短いソート済み配列を連続的により長いソート済み配列にマージし、プロセスが完了するまで続行します。 +1. **分割段階**:再帰によって配列を中点で繰り返し分割し、長い配列のソート問題を短い配列のソート問題へ変換します。 +2. **マージ段階**:部分配列の長さが 1 になったら分割を終了し、マージを開始して、左右 2 つの短いソート済み配列をより長いソート済み配列へと繰り返しマージしていきます。 -![The divide and merge phases of merge sort](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" } +![マージソートの分割とマージの段階](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" } -

図 11-10   The divide and merge phases of merge sort

+

図 11-10   マージソートの分割とマージの段階

-## 11.6.1   アルゴリズムワークフロー +## 11.6.1   アルゴリズムの流れ -下図に示すように、「分割フェーズ」は中点から配列を上から下に2つのサブ配列に再帰的に分割します。 +以下の図に示すように、「分割段階」では配列を上から下へ再帰的に中点で 2 つの部分配列へ分割します。 -1. 中点`mid`を計算し、左サブ配列(区間`[left, mid]`)と右サブ配列(区間`[mid + 1, right]`)を再帰的に分割します。 -2. サブ配列の長さが1になるまでステップ`1.`を再帰的に続行し、その後停止します。 +1. 配列の中点 `mid` を計算し、左部分配列(区間 `[left, mid]` )と右部分配列(区間 `[mid + 1, right]` )を再帰的に分割します。 +2. 手順 `1.` を再帰的に実行し、部分配列区間の長さが 1 になった時点で終了します。 -「マージフェーズ」は左と右のサブ配列を下から上にソート済み配列に結合します。重要なのは、マージが長さ1のサブ配列から開始され、マージフェーズ中に各サブ配列がソートされることです。 +「マージ段階」では左部分配列と右部分配列を下から上へとマージし、1 つのソート済み配列にします。長さ 1 の部分配列からマージを始めるため、この段階の各部分配列はすでに整列されています。 === "<1>" - ![Merge sort process](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" } + ![マージソートの手順](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" } === "<2>" ![merge_sort_step2](merge_sort.assets/merge_sort_step2.png){ class="animation-figure" } @@ -52,26 +52,26 @@ comments: true === "<10>" ![merge_sort_step10](merge_sort.assets/merge_sort_step10.png){ class="animation-figure" } -

図 11-11   Merge sort process

+

図 11-11   マージソートの手順

-マージソートの再帰順序は二分木の後順横断と一致することが観察できます。 +観察すると、マージソートの再帰順序は二分木の後順走査と一致していることがわかります。 -- **後順横断**:まず左のサブツリーを再帰的に横断し、次に右のサブツリーを横断し、最後にルートノードを処理します。 -- **マージソート**:まず左のサブ配列を再帰的に処理し、次に右のサブ配列を処理し、最後にマージを実行します。 +- **後順走査**:まず左部分木を再帰し、次に右部分木を再帰し、最後に根ノードを処理します。 +- **マージソート**:まず左部分配列を再帰し、次に右部分配列を再帰し、最後にマージを処理します。 -マージソートの実装は以下のコードに示されます。`nums`でマージされる区間は`[left, right]`で、`tmp`の対応する区間は`[0, right - left]`であることに注意してください。 +マージソートの実装を以下のコードに示します。注意として、`nums` のマージ対象区間は `[left, right]` であり、`tmp` の対応区間は `[0, right - left]` です。 === "Python" ```python title="merge_sort.py" def merge(nums: list[int], left: int, mid: int, right: int): - """左サブ配列と右サブ配列をマージ""" - # 左サブ配列区間は [left, mid]、右サブ配列区間は [mid+1, right] - # 一時配列 tmp を作成してマージ結果を格納 + """左部分配列と右部分配列をマージ""" + # 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + # マージ結果を格納する一時配列 tmp を作成 tmp = [0] * (right - left + 1) - # 左右サブ配列の開始インデックスを初期化 + # 左右の部分配列の開始インデックスを初期化する i, j, k = left, mid + 1, 0 - # 両方のサブ配列に要素が残っている間、より小さい要素を一時配列にコピー + # 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする while i <= mid and j <= right: if nums[i] <= nums[j]: tmp[k] = nums[i] @@ -80,7 +80,7 @@ comments: true tmp[k] = nums[j] j += 1 k += 1 - # 残った左右サブ配列の要素を一時配列にコピー + # 左右の部分配列の残り要素を一時配列にコピーする while i <= mid: tmp[k] = nums[i] i += 1 @@ -89,7 +89,7 @@ comments: true tmp[k] = nums[j] j += 1 k += 1 - # 一時配列 tmp の要素を元の配列 nums の対応する区間にコピーバック + # 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする for k in range(0, len(tmp)): nums[left + k] = tmp[k] @@ -97,40 +97,40 @@ comments: true """マージソート""" # 終了条件 if left >= right: - return # サブ配列の長さが1のときに再帰を終了 - # 分割段階 - mid = left + (right - left) // 2 # 中点を計算 - merge_sort(nums, left, mid) # 左サブ配列を再帰的に処理 - merge_sort(nums, mid + 1, right) # 右サブ配列を再帰的に処理 - # マージ段階 + return # 部分配列の長さが 1 になったら再帰を終了 + # 分割フェーズ + mid = (left + right) // 2 # 中点を計算 + merge_sort(nums, left, mid) # 左部分配列を再帰処理 + merge_sort(nums, mid + 1, right) # 右部分配列を再帰処理 + # マージフェーズ merge(nums, left, mid, right) ``` === "C++" ```cpp title="merge_sort.cpp" - /* 左サブ配列と右サブ配列をマージ */ + /* 左部分配列と右部分配列をマージ */ void merge(vector &nums, int left, int mid, int right) { - // 左サブ配列の区間は[left, mid]、右サブ配列の区間は[mid+1, right] - // マージ結果を保存する一時配列tmpを作成 + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 vector tmp(right - left + 1); - // 左右サブ配列の開始インデックスを初期化 + // 左右の部分配列の開始インデックスを初期化する int i = left, j = mid + 1, k = 0; - // 両サブ配列に要素がある間、小さい方の要素を一時配列にコピー + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする while (i <= mid && j <= right) { if (nums[i] <= nums[j]) tmp[k++] = nums[i++]; else tmp[k++] = nums[j++]; } - // 左右サブ配列の残りの要素を一時配列にコピー + // 左右の部分配列の残り要素を一時配列にコピーする while (i <= mid) { tmp[k++] = nums[i++]; } while (j <= right) { tmp[k++] = nums[j++]; } - // 一時配列tmpの要素を元の配列numsの対応する区間にコピー + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする for (k = 0; k < tmp.size(); k++) { nums[left + k] = tmp[k]; } @@ -140,12 +140,12 @@ comments: true void mergeSort(vector &nums, int left, int right) { // 終了条件 if (left >= right) - return; // サブ配列の長さが1の時、再帰を終了 - // 分割段階 + return; // 部分配列の長さが 1 になったら再帰を終了 + // 分割フェーズ int mid = left + (right - left) / 2; // 中点を計算 - mergeSort(nums, left, mid); // 左サブ配列を再帰的に処理 - mergeSort(nums, mid + 1, right); // 右サブ配列を再帰的に処理 - // マージ段階 + mergeSort(nums, left, mid); // 左部分配列を再帰処理 + mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理 + // マージフェーズ merge(nums, left, mid, right); } ``` @@ -155,26 +155,26 @@ comments: true ```java title="merge_sort.java" /* 左部分配列と右部分配列をマージ */ void merge(int[] nums, int left, int mid, int right) { - // 左部分配列区間は [left, mid]、右部分配列区間は [mid+1, right] - // 一時配列 tmp を作成してマージ結果を格納 + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 int[] tmp = new int[right - left + 1]; - // 左右部分配列の開始インデックスを初期化 + // 左右の部分配列の開始インデックスを初期化する int i = left, j = mid + 1, k = 0; - // 両部分配列にまだ要素がある間、比較してより小さい要素を一時配列にコピー + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする while (i <= mid && j <= right) { if (nums[i] <= nums[j]) tmp[k++] = nums[i++]; else tmp[k++] = nums[j++]; } - // 左右部分配列の残りの要素を一時配列にコピー + // 左右の部分配列の残り要素を一時配列にコピーする while (i <= mid) { tmp[k++] = nums[i++]; } while (j <= right) { tmp[k++] = nums[j++]; } - // 一時配列 tmp の要素を元の配列 nums の対応する区間にコピーバック + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする for (k = 0; k < tmp.length; k++) { nums[left + k] = tmp[k]; } @@ -184,12 +184,12 @@ comments: true void mergeSort(int[] nums, int left, int right) { // 終了条件 if (left >= right) - return; // 部分配列の長さが 1 のとき再帰を終了 - // 分割段階 + return; // 部分配列の長さが 1 になったら再帰を終了 + // 分割フェーズ int mid = left + (right - left) / 2; // 中点を計算 - mergeSort(nums, left, mid); // 左部分配列を再帰的に処理 - mergeSort(nums, mid + 1, right); // 右部分配列を再帰的に処理 - // マージ段階 + mergeSort(nums, left, mid); // 左部分配列を再帰処理 + mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理 + // マージフェーズ merge(nums, left, mid, right); } ``` @@ -197,94 +197,504 @@ comments: true === "C#" ```csharp title="merge_sort.cs" - [class]{merge_sort}-[func]{Merge} + /* 左部分配列と右部分配列をマージ */ + void Merge(int[] nums, int left, int mid, int right) { + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 + int[] tmp = new int[right - left + 1]; + // 左右の部分配列の開始インデックスを初期化する + int i = left, j = mid + 1, k = 0; + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) + tmp[k++] = nums[i++]; + else + tmp[k++] = nums[j++]; + } + // 左右の部分配列の残り要素を一時配列にコピーする + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする + for (k = 0; k < tmp.Length; ++k) { + nums[left + k] = tmp[k]; + } + } - [class]{merge_sort}-[func]{MergeSort} + /* マージソート */ + void MergeSort(int[] nums, int left, int right) { + // 終了条件 + if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了 + // 分割フェーズ + int mid = left + (right - left) / 2; // 中点を計算 + MergeSort(nums, left, mid); // 左部分配列を再帰処理 + MergeSort(nums, mid + 1, right); // 右部分配列を再帰処理 + // マージフェーズ + Merge(nums, left, mid, right); + } ``` === "Go" ```go title="merge_sort.go" - [class]{}-[func]{merge} + /* 左部分配列と右部分配列をマージ */ + func merge(nums []int, left, mid, right int) { + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 + tmp := make([]int, right-left+1) + // 左右の部分配列の開始インデックスを初期化する + i, j, k := left, mid+1, 0 + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする + for i <= mid && j <= right { + if nums[i] <= nums[j] { + tmp[k] = nums[i] + i++ + } else { + tmp[k] = nums[j] + j++ + } + k++ + } + // 左右の部分配列の残り要素を一時配列にコピーする + for i <= mid { + tmp[k] = nums[i] + i++ + k++ + } + for j <= right { + tmp[k] = nums[j] + j++ + k++ + } + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする + for k := 0; k < len(tmp); k++ { + nums[left+k] = tmp[k] + } + } - [class]{}-[func]{mergeSort} + /* マージソート */ + func mergeSort(nums []int, left, right int) { + // 終了条件 + if left >= right { + return + } + // 分割フェーズ + mid := left + (right - left) / 2 + mergeSort(nums, left, mid) + mergeSort(nums, mid+1, right) + // マージフェーズ + merge(nums, left, mid, right) + } ``` === "Swift" ```swift title="merge_sort.swift" - [class]{}-[func]{merge} + /* 左部分配列と右部分配列をマージ */ + func merge(nums: inout [Int], left: Int, mid: Int, right: Int) { + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 + var tmp = Array(repeating: 0, count: right - left + 1) + // 左右の部分配列の開始インデックスを初期化する + var i = left, j = mid + 1, k = 0 + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする + while i <= mid, j <= right { + if nums[i] <= nums[j] { + tmp[k] = nums[i] + i += 1 + } else { + tmp[k] = nums[j] + j += 1 + } + k += 1 + } + // 左右の部分配列の残り要素を一時配列にコピーする + while i <= mid { + tmp[k] = nums[i] + i += 1 + k += 1 + } + while j <= right { + tmp[k] = nums[j] + j += 1 + k += 1 + } + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする + for k in tmp.indices { + nums[left + k] = tmp[k] + } + } - [class]{}-[func]{mergeSort} + /* マージソート */ + func mergeSort(nums: inout [Int], left: Int, right: Int) { + // 終了条件 + if left >= right { // 部分配列の長さが 1 になったら再帰を終了 + return + } + // 分割フェーズ + let mid = left + (right - left) / 2 // 中点を計算 + mergeSort(nums: &nums, left: left, right: mid) // 左部分配列を再帰処理 + mergeSort(nums: &nums, left: mid + 1, right: right) // 右部分配列を再帰処理 + // マージフェーズ + merge(nums: &nums, left: left, mid: mid, right: right) + } ``` === "JS" ```javascript title="merge_sort.js" - [class]{}-[func]{merge} + /* 左部分配列と右部分配列をマージ */ + function merge(nums, left, mid, right) { + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 + const tmp = new Array(right - left + 1); + // 左右の部分配列の開始インデックスを初期化する + let i = left, + j = mid + 1, + k = 0; + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) { + tmp[k++] = nums[i++]; + } else { + tmp[k++] = nums[j++]; + } + } + // 左右の部分配列の残り要素を一時配列にコピーする + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする + for (k = 0; k < tmp.length; k++) { + nums[left + k] = tmp[k]; + } + } - [class]{}-[func]{mergeSort} + /* マージソート */ + function mergeSort(nums, left, right) { + // 終了条件 + if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了 + // 分割フェーズ + let mid = Math.floor(left + (right - left) / 2); // 中点を計算 + mergeSort(nums, left, mid); // 左部分配列を再帰処理 + mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理 + // マージフェーズ + merge(nums, left, mid, right); + } ``` === "TS" ```typescript title="merge_sort.ts" - [class]{}-[func]{merge} + /* 左部分配列と右部分配列をマージ */ + function merge(nums: number[], left: number, mid: number, right: number): void { + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 + const tmp = new Array(right - left + 1); + // 左右の部分配列の開始インデックスを初期化する + let i = left, + j = mid + 1, + k = 0; + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) { + tmp[k++] = nums[i++]; + } else { + tmp[k++] = nums[j++]; + } + } + // 左右の部分配列の残り要素を一時配列にコピーする + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする + for (k = 0; k < tmp.length; k++) { + nums[left + k] = tmp[k]; + } + } - [class]{}-[func]{mergeSort} + /* マージソート */ + function mergeSort(nums: number[], left: number, right: number): void { + // 終了条件 + if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了 + // 分割フェーズ + let mid = Math.floor(left + (right - left) / 2); // 中点を計算 + mergeSort(nums, left, mid); // 左部分配列を再帰処理 + mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理 + // マージフェーズ + merge(nums, left, mid, right); + } ``` === "Dart" ```dart title="merge_sort.dart" - [class]{}-[func]{merge} + /* 左部分配列と右部分配列をマージ */ + void merge(List nums, int left, int mid, int right) { + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 + List tmp = List.filled(right - left + 1, 0); + // 左右の部分配列の開始インデックスを初期化する + int i = left, j = mid + 1, k = 0; + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) + tmp[k++] = nums[i++]; + else + tmp[k++] = nums[j++]; + } + // 左右の部分配列の残り要素を一時配列にコピーする + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする + for (k = 0; k < tmp.length; k++) { + nums[left + k] = tmp[k]; + } + } - [class]{}-[func]{mergeSort} + /* マージソート */ + void mergeSort(List nums, int left, int right) { + // 終了条件 + if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了 + // 分割フェーズ + int mid = left + (right - left) ~/ 2; // 中点を計算 + mergeSort(nums, left, mid); // 左部分配列を再帰処理 + mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理 + // マージフェーズ + merge(nums, left, mid, right); + } ``` === "Rust" ```rust title="merge_sort.rs" - [class]{}-[func]{merge} + /* 左部分配列と右部分配列をマージ */ + fn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) { + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 + let tmp_size = right - left + 1; + let mut tmp = vec![0; tmp_size]; + // 左右の部分配列の開始インデックスを初期化する + let (mut i, mut j, mut k) = (left, mid + 1, 0); + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする + while i <= mid && j <= right { + if nums[i] <= nums[j] { + tmp[k] = nums[i]; + i += 1; + } else { + tmp[k] = nums[j]; + j += 1; + } + k += 1; + } + // 左右の部分配列の残り要素を一時配列にコピーする + while i <= mid { + tmp[k] = nums[i]; + k += 1; + i += 1; + } + while j <= right { + tmp[k] = nums[j]; + k += 1; + j += 1; + } + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする + for k in 0..tmp_size { + nums[left + k] = tmp[k]; + } + } - [class]{}-[func]{merge_sort} + /* マージソート */ + fn merge_sort(nums: &mut [i32], left: usize, right: usize) { + // 終了条件 + if left >= right { + return; // 部分配列の長さが 1 になったら再帰を終了 + } + + // 分割フェーズ + let mid = left + (right - left) / 2; // 中点を計算 + merge_sort(nums, left, mid); // 左部分配列を再帰処理 + merge_sort(nums, mid + 1, right); // 右部分配列を再帰処理 + + // マージフェーズ + merge(nums, left, mid, right); + } ``` === "C" ```c title="merge_sort.c" - [class]{}-[func]{merge} + /* 左部分配列と右部分配列をマージ */ + void merge(int *nums, int left, int mid, int right) { + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 + int tmpSize = right - left + 1; + int *tmp = (int *)malloc(tmpSize * sizeof(int)); + // 左右の部分配列の開始インデックスを初期化する + int i = left, j = mid + 1, k = 0; + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) { + tmp[k++] = nums[i++]; + } else { + tmp[k++] = nums[j++]; + } + } + // 左右の部分配列の残り要素を一時配列にコピーする + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする + for (k = 0; k < tmpSize; ++k) { + nums[left + k] = tmp[k]; + } + // メモリを解放する + free(tmp); + } - [class]{}-[func]{mergeSort} + /* マージソート */ + void mergeSort(int *nums, int left, int right) { + // 終了条件 + if (left >= right) + return; // 部分配列の長さが 1 になったら再帰を終了 + // 分割フェーズ + int mid = left + (right - left) / 2; // 中点を計算 + mergeSort(nums, left, mid); // 左部分配列を再帰処理 + mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理 + // マージフェーズ + merge(nums, left, mid, right); + } ``` === "Kotlin" ```kotlin title="merge_sort.kt" - [class]{}-[func]{merge} + /* 左部分配列と右部分配列をマージ */ + fun merge(nums: IntArray, left: Int, mid: Int, right: Int) { + // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + // マージ結果を格納する一時配列 tmp を作成 + val tmp = IntArray(right - left + 1) + // 左右の部分配列の開始インデックスを初期化する + var i = left + var j = mid + 1 + var k = 0 + // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) + tmp[k++] = nums[i++] + else + tmp[k++] = nums[j++] + } + // 左右の部分配列の残り要素を一時配列にコピーする + while (i <= mid) { + tmp[k++] = nums[i++] + } + while (j <= right) { + tmp[k++] = nums[j++] + } + // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする + for (l in tmp.indices) { + nums[left + l] = tmp[l] + } + } - [class]{}-[func]{mergeSort} + /* マージソート */ + fun mergeSort(nums: IntArray, left: Int, right: Int) { + // 終了条件 + if (left >= right) return // 部分配列の長さが 1 になったら再帰を終了 + // 分割フェーズ + val mid = left + (right - left) / 2 // 中点を計算 + mergeSort(nums, left, mid) // 左部分配列を再帰処理 + mergeSort(nums, mid + 1, right) // 右部分配列を再帰処理 + // マージフェーズ + merge(nums, left, mid, right) + } ``` === "Ruby" ```ruby title="merge_sort.rb" - [class]{}-[func]{merge} + ### 左部分配列と右部分配列をマージ ### + def merge(nums, left, mid, right) + # 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right] + # マージ結果を格納する一時配列 tmp を作成 + tmp = Array.new(right - left + 1, 0) + # 左右の部分配列の開始インデックスを初期化する + i, j, k = left, mid + 1, 0 + # 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする + while i <= mid && j <= right + if nums[i] <= nums[j] + tmp[k] = nums[i] + i += 1 + else + tmp[k] = nums[j] + j += 1 + end + k += 1 + end + # 左右の部分配列の残り要素を一時配列にコピーする + while i <= mid + tmp[k] = nums[i] + i += 1 + k += 1 + end + while j <= right + tmp[k] = nums[j] + j += 1 + k += 1 + end + # 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする + (0...tmp.length).each do |k| + nums[left + k] = tmp[k] + end + end - [class]{}-[func]{merge_sort} + ### マージソート ### + def merge_sort(nums, left, right) + # 終了条件 + # 部分配列の長さが 1 になったら再帰を終了する + return if left >= right + # 分割フェーズ + mid = left + (right - left) / 2 # 中点を計算 + merge_sort(nums, left, mid) # 左部分配列を再帰処理 + merge_sort(nums, mid + 1, right) # 右部分配列を再帰処理 + # マージフェーズ + merge(nums, left, mid, right) + end ``` +??? pythontutor "コードの可視化" + +
+ + ## 11.6.2   アルゴリズムの特性 -- **$O(n \log n)$の時間計算量、非適応ソート**:分割により高さ$\log n$の再帰ツリーが作成され、各層で合計$n$回の操作をマージし、全体的な時間計算量は$O(n \log n)$となります。 -- **$O(n)$の空間計算量、非インプレースソート**:再帰深度は$\log n$で、$O(\log n)$のスタックフレーム空間を使用します。マージ操作には補助配列が必要で、追加の$O(n)$空間を使用します。 -- **安定ソート**:マージプロセス中、等しい要素の順序は変更されません。 +- **時間計算量は $O(n \log n)$、非適応型ソート**:分割によって高さ $\log n$ の再帰木が生成され、各層でのマージ操作の総数は $n$ であるため、全体の時間計算量は $O(n \log n)$ です。 +- **空間計算量は $O(n)$、インプレースではないソート**:再帰の深さは $\log n$ であり、サイズ $O(\log n)$ のスタックフレーム領域を使用します。マージ操作は補助配列を用いて実装する必要があり、サイズ $O(n)$ の追加領域を使用します。 +- **安定ソート**:マージの過程では、等しい要素の順序は変化しません。 ## 11.6.3   連結リストのソート -連結リストの場合、マージソートは他のソートアルゴリズムよりも大きな利点があります。**連結リストソートタスクの空間計算量を$O(1)$に最適化できます**。 +連結リストに対しては、マージソートは他のソートアルゴリズムと比べて顕著な利点があり、**連結リストのソート問題の空間計算量を $O(1)$ まで最適化できます** 。 -- **分割フェーズ**:「再帰」の代わりに「反復」を使用して連結リスト分割作業を実行できるため、再帰で使用されるスタックフレーム空間を節約できます。 -- **マージフェーズ**:連結リストでは、ノードの挿入と削除操作は参照(ポインタ)を変更することで実現できるため、マージフェーズ(2つの短い順序付きリストを1つの長い順序付きリストに結合)中に追加のリストを作成する必要がありません。 +- **分割段階**:連結リストの分割は「再帰」の代わりに「反復」で実装できるため、再帰で使用するスタックフレーム領域を省けます。 +- **マージ段階**:連結リストでは、ノードの追加や削除は参照(ポインタ)を変更するだけで実現できるため、マージ段階(2 つの短いソート済み連結リストを 1 つの長いソート済み連結リストにマージすること)では追加の連結リストを作成する必要がありません。 -実装の詳細は比較的複雑で、興味のある読者は関連資料を参照して学習してください。 +具体的な実装の詳細は比較的複雑なので、興味のある読者は関連資料を参照して学習してください。 diff --git a/ja/docs/chapter_sorting/quick_sort.md b/ja/docs/chapter_sorting/quick_sort.md index 2b393a070..b0a9b2ffc 100644 --- a/ja/docs/chapter_sorting/quick_sort.md +++ b/ja/docs/chapter_sorting/quick_sort.md @@ -4,16 +4,16 @@ comments: true # 11.5   クイックソート -クイックソートは分割統治戦略に基づくソートアルゴリズムで、その効率性と幅広い応用で知られています。 +クイックソート(quick sort)は分割統治戦略に基づくソートアルゴリズムであり、実行効率が高く、広く利用されています。 -クイックソートのコア操作は「ピボット分割」で、配列から要素を「ピボット」として選択し、ピボットより小さいすべての要素をその左側に移動し、ピボットより大きいすべての要素をその右側に移動することを目的としています。具体的に、ピボット分割のプロセスは下図に示されます。 +クイックソートの中核操作は「パーティション」であり、その目的は、配列内のある要素を「基準数」として選び、基準数より小さいすべての要素を左側へ、大きい要素を右側へ移動することです。具体的には、パーティションの流れを下図に示します。 -1. 配列の最も左の要素をピボットとして選択し、2つのポインタ`i`と`j`を初期化して配列の両端をそれぞれ指すようにします。 -2. 各ラウンドで`i`(`j`)を使用してピボットより大きい(小さい)最初の要素を探索し、次にこれら2つの要素を交換するループを設定します。 -3. `i`と`j`が出会うまでステップ`2.`を繰り返し、最後にピボットを2つのサブ配列の境界に交換します。 +1. 配列の最左端の要素を基準数として選び、2 つのポインタ `i` と `j` を初期化して、それぞれ配列の両端を指すようにします。 +2. ループを設定し、各ラウンドで `i`(`j`)を使ってそれぞれ基準数より大きい(小さい)最初の要素を探し、その後この 2 つの要素を交換します。 +3. `i` と `j` が出会うまでステップ `2.` を繰り返し、最後に基準数を 2 つの部分配列の境界へ交換します。 === "<1>" - ![Pivot division process](quick_sort.assets/pivot_division_step1.png){ class="animation-figure" } + ![パーティションの手順](quick_sort.assets/pivot_division_step1.png){ class="animation-figure" } === "<2>" ![pivot_division_step2](quick_sort.assets/pivot_division_step2.png){ class="animation-figure" } @@ -39,74 +39,74 @@ comments: true === "<9>" ![pivot_division_step9](quick_sort.assets/pivot_division_step9.png){ class="animation-figure" } -

図 11-8   Pivot division process

+

図 11-8   パーティションの手順

-ピボット分割後、元の配列は3つの部分に分割されます:左サブ配列、ピボット、右サブ配列で、「左サブ配列の任意の要素 $\leq$ ピボット $\leq$ 右サブ配列の任意の要素」を満たします。したがって、これら2つのサブ配列のみをソートすればよいのです。 +パーティションが完了すると、元の配列は左部分配列、基準数、右部分配列の 3 つに分けられ、「左部分配列の任意の要素 $\leq$ 基準数 $\leq$ 右部分配列の任意の要素」を満たします。したがって、次はこの 2 つの部分配列だけをソートすれば済みます。 !!! note "クイックソートの分割統治戦略" - ピボット分割の本質は、より長い配列のソート問題をより短い2つの配列に簡素化することです。 + パーティションの本質は、長い配列のソート問題を 2 つの短い配列のソート問題へ簡略化することです。 === "Python" ```python title="quick_sort.py" def partition(self, nums: list[int], left: int, right: int) -> int: - """分割""" - # nums[left] をピボットとして使用 + """番兵分割""" + # nums[left] を基準値とする i, j = left, right while i < j: while i < j and nums[j] >= nums[left]: - j -= 1 # 右から左へピボットより小さい最初の要素を探す + j -= 1 # 右から左へ基準値未満の最初の要素を探す while i < j and nums[i] <= nums[left]: - i += 1 # 左から右へピボットより大きい最初の要素を探す - # 要素を交換 + i += 1 # 左から右へ基準値より大きい最初の要素を探す + # 要素の交換 nums[i], nums[j] = nums[j], nums[i] - # ピボットを2つのサブ配列の境界に交換 + # 基準値を 2 つの部分配列の境界へ交換する nums[i], nums[left] = nums[left], nums[i] - return i # ピボットのインデックスを返す + return i # 基準値のインデックスを返す ``` === "C++" ```cpp title="quick_sort.cpp" - /* 分割 */ + /* 番兵分割 */ int partition(vector &nums, int left, int right) { - // nums[left]をピボットとして使用 + // nums[left] を基準値とする int i = left, j = right; while (i < j) { while (i < j && nums[j] >= nums[left]) - j--; // 右から左へピボットより小さい最初の要素を検索 + j--; // 右から左へ基準値未満の最初の要素を探す while (i < j && nums[i] <= nums[left]) - i++; // 左から右へピボットより大きい最初の要素を検索 - swap(nums, i, j); // これら二つの要素を交換 + i++; // 左から右へ基準値より大きい最初の要素を探す + swap(nums[i], nums[j]); // この 2 つの要素を交換 } - swap(nums, i, left); // ピボットを二つのサブ配列の境界に交換 - return i; // ピボットのインデックスを返す + swap(nums[i], nums[left]); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す } ``` === "Java" ```java title="quick_sort.java" - /* 要素を交換 */ + /* 要素の交換 */ void swap(int[] nums, int i, int j) { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } - /* 分割 */ + /* 番兵分割 */ int partition(int[] nums, int left, int right) { - // nums[left] を基準値として使用 + // nums[left] を基準値とする int i = left, j = right; while (i < j) { while (i < j && nums[j] >= nums[left]) - j--; // 右から左へ、基準値より小さい最初の要素を検索 + j--; // 右から左へ基準値未満の最初の要素を探す while (i < j && nums[i] <= nums[left]) - i++; // 左から右へ、基準値より大きい最初の要素を検索 - swap(nums, i, j); // これら2つの要素を交換 + i++; // 左から右へ基準値より大きい最初の要素を探す + swap(nums, i, j); // この 2 つの要素を交換 } - swap(nums, i, left); // 基準値を2つの部分配列の境界に交換 + swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する return i; // 基準値のインデックスを返す } ``` @@ -114,98 +114,286 @@ comments: true === "C#" ```csharp title="quick_sort.cs" - [class]{quickSort}-[func]{Swap} + /* 要素の交換 */ + void Swap(int[] nums, int i, int j) { + (nums[j], nums[i]) = (nums[i], nums[j]); + } - [class]{quickSort}-[func]{Partition} + /* 番兵分割 */ + int Partition(int[] nums, int left, int right) { + // nums[left] を基準値とする + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 右から左へ基準値未満の最初の要素を探す + while (i < j && nums[i] <= nums[left]) + i++; // 左から右へ基準値より大きい最初の要素を探す + Swap(nums, i, j); // この 2 つの要素を交換 + } + Swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す + } ``` === "Go" ```go title="quick_sort.go" - [class]{quickSort}-[func]{partition} + /* 番兵分割 */ + func (q *quickSort) partition(nums []int, left, right int) int { + // nums[left] を基準値とする + i, j := left, right + for i < j { + for i < j && nums[j] >= nums[left] { + j-- // 右から左へ基準値未満の最初の要素を探す + } + for i < j && nums[i] <= nums[left] { + i++ // 左から右へ基準値より大きい最初の要素を探す + } + // 要素の交換 + nums[i], nums[j] = nums[j], nums[i] + } + // 基準値を 2 つの部分配列の境界へ交換する + nums[i], nums[left] = nums[left], nums[i] + return i // 基準値のインデックスを返す + } ``` === "Swift" ```swift title="quick_sort.swift" - [class]{}-[func]{partition} + /* 番兵分割 */ + func partition(nums: inout [Int], left: Int, right: Int) -> Int { + // nums[left] を基準値とする + var i = left + var j = right + while i < j { + while i < j, nums[j] >= nums[left] { + j -= 1 // 右から左へ基準値未満の最初の要素を探す + } + while i < j, nums[i] <= nums[left] { + i += 1 // 左から右へ基準値より大きい最初の要素を探す + } + nums.swapAt(i, j) // この 2 つの要素を交換 + } + nums.swapAt(i, left) // 基準値を 2 つの部分配列の境界へ交換する + return i // 基準値のインデックスを返す + } ``` === "JS" ```javascript title="quick_sort.js" - [class]{QuickSort}-[func]{swap} + /* 要素の交換 */ + swap(nums, i, j) { + let tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } - [class]{QuickSort}-[func]{partition} + /* 番兵分割 */ + partition(nums, left, right) { + // nums[left] を基準値とする + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j -= 1; // 右から左へ基準値未満の最初の要素を探す + } + while (i < j && nums[i] <= nums[left]) { + i += 1; // 左から右へ基準値より大きい最初の要素を探す + } + // 要素の交換 + this.swap(nums, i, j); // この 2 つの要素を交換 + } + this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す + } ``` === "TS" ```typescript title="quick_sort.ts" - [class]{QuickSort}-[func]{swap} + /* 要素の交換 */ + swap(nums: number[], i: number, j: number): void { + let tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } - [class]{QuickSort}-[func]{partition} + /* 番兵分割 */ + partition(nums: number[], left: number, right: number): number { + // nums[left] を基準値とする + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j -= 1; // 右から左へ基準値未満の最初の要素を探す + } + while (i < j && nums[i] <= nums[left]) { + i += 1; // 左から右へ基準値より大きい最初の要素を探す + } + // 要素の交換 + this.swap(nums, i, j); // この 2 つの要素を交換 + } + this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す + } ``` === "Dart" ```dart title="quick_sort.dart" - [class]{QuickSort}-[func]{_swap} + /* 要素の交換 */ + void _swap(List nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } - [class]{QuickSort}-[func]{_partition} + /* 番兵分割 */ + int _partition(List nums, int left, int right) { + // nums[left] を基準値とする + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // 右から左へ基準値未満の最初の要素を探す + while (i < j && nums[i] <= nums[left]) i++; // 左から右へ基準値より大きい最初の要素を探す + _swap(nums, i, j); // この 2 つの要素を交換 + } + _swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す + } ``` === "Rust" ```rust title="quick_sort.rs" - [class]{QuickSort}-[func]{partition} + /* 番兵分割 */ + fn partition(nums: &mut [i32], left: usize, right: usize) -> usize { + // nums[left] を基準値とする + let (mut i, mut j) = (left, right); + while i < j { + while i < j && nums[j] >= nums[left] { + j -= 1; // 右から左へ基準値未満の最初の要素を探す + } + while i < j && nums[i] <= nums[left] { + i += 1; // 左から右へ基準値より大きい最初の要素を探す + } + nums.swap(i, j); // この 2 つの要素を交換 + } + nums.swap(i, left); // 基準値を 2 つの部分配列の境界へ交換する + i // 基準値のインデックスを返す + } ``` === "C" ```c title="quick_sort.c" - [class]{}-[func]{swap} + /* 要素の交換 */ + void swap(int nums[], int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } - [class]{}-[func]{partition} + /* 番兵分割 */ + int partition(int nums[], int left, int right) { + // nums[left] を基準値とする + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j--; // 右から左へ基準値未満の最初の要素を探す + } + while (i < j && nums[i] <= nums[left]) { + i++; // 左から右へ基準値より大きい最初の要素を探す + } + // この 2 つの要素を交換 + swap(nums, i, j); + } + // 基準値を 2 つの部分配列の境界へ交換する + swap(nums, i, left); + // 基準値のインデックスを返す + return i; + } ``` === "Kotlin" ```kotlin title="quick_sort.kt" - [class]{}-[func]{swap} + /* 要素の交換 */ + fun swap(nums: IntArray, i: Int, j: Int) { + val temp = nums[i] + nums[i] = nums[j] + nums[j] = temp + } - [class]{}-[func]{partition} + /* 番兵分割 */ + fun partition(nums: IntArray, left: Int, right: Int): Int { + // nums[left] を基準値とする + var i = left + var j = right + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j-- // 右から左へ基準値未満の最初の要素を探す + while (i < j && nums[i] <= nums[left]) + i++ // 左から右へ基準値より大きい最初の要素を探す + swap(nums, i, j) // この 2 つの要素を交換 + } + swap(nums, i, left) // 基準値を 2 つの部分配列の境界へ交換する + return i // 基準値のインデックスを返す + } ``` === "Ruby" ```ruby title="quick_sort.rb" - [class]{QuickSort}-[func]{partition} + ### 番兵分割 ### + def partition(nums, left, right) + # nums[left] を基準値とする + i, j = left, right + while i < j + while i < j && nums[j] >= nums[left] + j -= 1 # 右から左へ基準値未満の最初の要素を探す + end + while i < j && nums[i] <= nums[left] + i += 1 # 左から右へ基準値より大きい最初の要素を探す + end + # 要素の交換 + nums[i], nums[j] = nums[j], nums[i] + end + # 基準値を 2 つの部分配列の境界へ交換する + nums[i], nums[left] = nums[left], nums[i] + i # 基準値のインデックスを返す + end ``` -## 11.5.1   アルゴリズムプロセス +??? pythontutor "コードの可視化" -クイックソートの全体的なプロセスは下図に示されます。 +
+ -1. まず、元の配列に対して「ピボット分割」を実行し、未ソートの左と右のサブ配列を取得します。 -2. 次に、左と右のサブ配列に対してそれぞれ再帰的に「ピボット分割」を実行します。 -3. サブ配列の長さが1になるまで再帰を続け、配列全体のソートを完了します。 +## 11.5.1   アルゴリズムの流れ -![Quick sort process](quick_sort.assets/quick_sort_overview.png){ class="animation-figure" } +クイックソート全体の流れを下図に示します。 -

図 11-9   Quick sort process

+1. まず、元の配列に対して 1 回「パーティション」を実行し、未ソートの左部分配列と右部分配列を得ます。 +2. 次に、左部分配列と右部分配列に対してそれぞれ再帰的に「パーティション」を実行します。 +3. 部分配列の長さが 1 になるまで再帰を続け、配列全体のソートを完了します。 + +![クイックソートの流れ](quick_sort.assets/quick_sort_overview.png){ class="animation-figure" } + +

図 11-9   クイックソートの流れ

=== "Python" ```python title="quick_sort.py" def quick_sort(self, nums: list[int], left: int, right: int): """クイックソート""" - # サブ配列の長さが1のときに再帰を終了 + # 部分配列の長さが 1 なら再帰を終了する if left >= right: return - # 分割 + # 番兵分割 pivot = self.partition(nums, left, right) - # 左サブ配列と右サブ配列を再帰的に処理 + # 左右の部分配列を再帰処理 self.quick_sort(nums, left, pivot - 1) self.quick_sort(nums, pivot + 1, right) ``` @@ -215,12 +403,12 @@ comments: true ```cpp title="quick_sort.cpp" /* クイックソート */ void quickSort(vector &nums, int left, int right) { - // サブ配列の長さが1の時、再帰を終了 + // 部分配列の長さが 1 なら再帰を終了する if (left >= right) return; - // 分割 + // 番兵分割 int pivot = partition(nums, left, right); - // 左サブ配列と右サブ配列を再帰的に処理 + // 左右の部分配列を再帰処理 quickSort(nums, left, pivot - 1); quickSort(nums, pivot + 1, right); } @@ -231,12 +419,12 @@ comments: true ```java title="quick_sort.java" /* クイックソート */ void quickSort(int[] nums, int left, int right) { - // 部分配列の長さが 1 のとき再帰を終了 + // 部分配列の長さが 1 なら再帰を終了する if (left >= right) return; - // 分割 + // 番兵分割 int pivot = partition(nums, left, right); - // 左部分配列と右部分配列を再帰的に処理 + // 左右の部分配列を再帰処理 quickSort(nums, left, pivot - 1); quickSort(nums, pivot + 1, right); } @@ -245,94 +433,202 @@ comments: true === "C#" ```csharp title="quick_sort.cs" - [class]{quickSort}-[func]{QuickSort} + /* クイックソート */ + void QuickSort(int[] nums, int left, int right) { + // 部分配列の長さが 1 なら再帰を終了する + if (left >= right) + return; + // 番兵分割 + int pivot = Partition(nums, left, right); + // 左右の部分配列を再帰処理 + QuickSort(nums, left, pivot - 1); + QuickSort(nums, pivot + 1, right); + } ``` === "Go" ```go title="quick_sort.go" - [class]{quickSort}-[func]{quickSort} + /* クイックソート */ + func (q *quickSort) quickSort(nums []int, left, right int) { + // 部分配列の長さが 1 なら再帰を終了する + if left >= right { + return + } + // 番兵分割 + pivot := q.partition(nums, left, right) + // 左右の部分配列を再帰処理 + q.quickSort(nums, left, pivot-1) + q.quickSort(nums, pivot+1, right) + } ``` === "Swift" ```swift title="quick_sort.swift" - [class]{}-[func]{quickSort} + /* クイックソート */ + func quickSort(nums: inout [Int], left: Int, right: Int) { + // 部分配列の長さが 1 なら再帰を終了する + if left >= right { + return + } + // 番兵分割 + let pivot = partition(nums: &nums, left: left, right: right) + // 左右の部分配列を再帰処理 + quickSort(nums: &nums, left: left, right: pivot - 1) + quickSort(nums: &nums, left: pivot + 1, right: right) + } ``` === "JS" ```javascript title="quick_sort.js" - [class]{QuickSort}-[func]{quickSort} + /* クイックソート */ + quickSort(nums, left, right) { + // 部分配列の長さが 1 なら再帰を終了する + if (left >= right) return; + // 番兵分割 + const pivot = this.partition(nums, left, right); + // 左右の部分配列を再帰処理 + this.quickSort(nums, left, pivot - 1); + this.quickSort(nums, pivot + 1, right); + } ``` === "TS" ```typescript title="quick_sort.ts" - [class]{QuickSort}-[func]{quickSort} + /* クイックソート */ + quickSort(nums: number[], left: number, right: number): void { + // 部分配列の長さが 1 なら再帰を終了する + if (left >= right) { + return; + } + // 番兵分割 + const pivot = this.partition(nums, left, right); + // 左右の部分配列を再帰処理 + this.quickSort(nums, left, pivot - 1); + this.quickSort(nums, pivot + 1, right); + } ``` === "Dart" ```dart title="quick_sort.dart" - [class]{QuickSort}-[func]{quickSort} + /* クイックソート */ + void quickSort(List nums, int left, int right) { + // 部分配列の長さが 1 なら再帰を終了する + if (left >= right) return; + // 番兵分割 + int pivot = _partition(nums, left, right); + // 左右の部分配列を再帰処理 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } ``` === "Rust" ```rust title="quick_sort.rs" - [class]{QuickSort}-[func]{quick_sort} + /* クイックソート */ + pub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) { + // 部分配列の長さが 1 なら再帰を終了する + if left >= right { + return; + } + // 番兵分割 + let pivot = Self::partition(nums, left as usize, right as usize) as i32; + // 左右の部分配列を再帰処理 + Self::quick_sort(left, pivot - 1, nums); + Self::quick_sort(pivot + 1, right, nums); + } ``` === "C" ```c title="quick_sort.c" - [class]{}-[func]{quickSort} + /* クイックソート */ + void quickSort(int nums[], int left, int right) { + // 部分配列の長さが 1 なら再帰を終了する + if (left >= right) { + return; + } + // 番兵分割 + int pivot = partition(nums, left, right); + // 左右の部分配列を再帰処理 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } ``` === "Kotlin" ```kotlin title="quick_sort.kt" - [class]{}-[func]{quickSort} + /* クイックソート */ + fun quickSort(nums: IntArray, left: Int, right: Int) { + // 部分配列の長さが 1 なら再帰を終了する + if (left >= right) return + // 番兵分割 + val pivot = partition(nums, left, right) + // 左右の部分配列を再帰処理 + quickSort(nums, left, pivot - 1) + quickSort(nums, pivot + 1, right) + } ``` === "Ruby" ```ruby title="quick_sort.rb" - [class]{QuickSort}-[func]{quick_sort} + ### クイックソートクラス ### + def quick_sort(nums, left, right) + # 部分配列の長さが 1 でない場合は再帰する + if left < right + # 番兵分割 + pivot = partition(nums, left, right) + # 左右の部分配列を再帰処理 + quick_sort(nums, left, pivot - 1) + quick_sort(nums, pivot + 1, right) + end + nums + end ``` -## 11.5.2   アルゴリズムの特徴 +??? pythontutor "コードの可視化" -- **$O(n \log n)$の時間計算量、非適応ソート**:平均的なケースでは、ピボット分割の再帰レベルは$\log n$で、レベルあたりのループの総数は$n$であり、全体で$O(n \log n)$の時間を使用します。最悪の場合、各ラウンドのピボット分割は長さ$n$の配列を長さ$0$と$n - 1$の2つのサブ配列に分割し、再帰レベル数が$n$に達すると、各レベルのループ数は$n$で、使用される総時間は$O(n^2)$です。 -- **$O(n)$の空間計算量、インプレースソート**:入力配列が完全に逆順の場合、最悪の再帰深度は$n$に達し、$O(n)$のスタックフレーム空間を使用します。ソート操作は追加の配列の助けなしに元の配列で実行されます。 -- **非安定ソート**:ピボット分割の最終ステップで、ピボットは等しい要素の右側に交換される可能性があります。 +
+ -## 11.5.3   なぜクイックソートは高速なのか +## 11.5.2   アルゴリズムの特性 -名前が示すように、クイックソートは効率性の面で一定の利点を持つべきです。クイックソートの平均時間計算量は「マージソート」や「ヒープソート」と同じですが、以下の理由で一般的により効率的です。 +- **時間計算量は $O(n \log n)$、非適応型ソート**:平均的な場合、パーティションの再帰の深さは $\log n$ で、各層の総ループ回数は $n$ のため、全体で $O(n \log n)$ 時間を要します。最悪の場合、各回のパーティション操作で長さ $n$ の配列が長さ $0$ と $n - 1$ の 2 つの部分配列に分割され、このとき再帰の深さは $n$ に達し、各層のループ回数は $n$ となるため、全体で $O(n^2)$ 時間を要します。 +- **空間計算量は $O(n)$、インプレースソート**:入力配列が完全な逆順の場合、最悪の再帰深さ $n$ に達し、$O(n)$ のスタックフレーム空間を使用します。ソート操作は元の配列上で行われ、追加の配列は用いません。 +- **非安定ソート**:パーティションの最後のステップで、基準数が等しい要素の右側へ交換される可能性があります。 -- **最悪ケースシナリオの低い確率**:クイックソートの最悪時間計算量は$O(n^2)$で、マージソートほど安定していませんが、ほとんどの場合、クイックソートは$O(n \log n)$の時間計算量で動作できます。 -- **高いキャッシュ利用率**:ピボット分割操作中、システムはサブ配列全体をキャッシュにロードできるため、要素により効率的にアクセスできます。対照的に、「ヒープソート」などのアルゴリズムは要素にジャンプ方式でアクセスする必要があり、この特徴を欠いています。 -- **計算量の小さな定数係数**:上記3つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作の総数が最も少ないです。これは「挿入ソート」が「バブルソート」よりも高速な理由と似ています。 +## 11.5.3   クイックソートが速い理由 -## 11.5.4   ピボット最適化 +名前からも分かるように、クイックソートは効率面で一定の優位性を持っています。クイックソートの平均時間計算量は「マージソート」や「ヒープソート」と同じですが、通常はクイックソートのほうが高効率であり、主な理由は次のとおりです。 -**クイックソートの時間効率は特定の入力で劣化する可能性があります**。例えば、入力配列が完全に逆順の場合、最も左の要素をピボットとして選択するため、ピボット分割後、ピボットは配列の右端に交換され、左サブ配列の長さが$n - 1$、右サブ配列の長さが$0$になります。この方法を続けると、各ラウンドのピボット分割でサブ配列の長さが$0$になり、分割統治戦略が失敗し、クイックソートは「バブルソート」に似た形に劣化します。 +- **最悪ケースが起こる確率が低い**:クイックソートの最悪時間計算量は $O(n^2)$ で、「マージソート」ほど安定ではありませんが、大半のケースでは $O(n \log n)$ の時間計算量で動作します。 +- **キャッシュ利用効率が高い**:パーティション操作の実行時には、システムが部分配列全体をキャッシュに読み込めるため、要素アクセスの効率が高くなります。一方、「ヒープソート」のようなアルゴリズムは要素へ飛び飛びにアクセスする必要があり、この性質を持ちません。 +- **計算量の定数係数が小さい**:上記 3 つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作総数が最も少なくなります。これは「挿入ソート」が「バブルソート」より速い理由と似ています。 -この状況を避けるため、**ピボット分割でピボット選択戦略を最適化できます**。例えば、要素をランダムに選択してピボットとすることができます。ただし、運が悪く、一貫して最適でないピボットを選択した場合、効率はまだ満足できません。 +## 11.5.4   基準数の最適化 -プログラミング言語は通常「疑似乱数」を生成することに注意することが重要です。疑似乱数シーケンスに対して特定のテストケースを構築すると、クイックソートの効率はまだ劣化する可能性があります。 +**クイックソートは、入力によっては時間効率が低下する可能性があります**。極端な例として、入力配列が完全な逆順であるとします。最左端の要素を基準数として選ぶため、パーティション完了後には基準数が配列の最右端へ交換され、左部分配列の長さが $n - 1$、右部分配列の長さが $0$ になります。この再帰を続けると、各回のパーティション後に必ず一方の部分配列の長さが $0$ となり、分割統治戦略が機能せず、クイックソートは「バブルソート」に近い形へ退化します。 -さらなる改善のため、3つの候補要素(通常は配列の最初、最後、中点の要素)を選択し、**これら3つの候補要素の中央値をピボットとして使用**できます。この方法で、ピボットが「小さすぎず大きすぎない」確率が大幅に増加します。もちろん、さらに多くの候補要素を選択してアルゴリズムの堅牢性をさらに向上させることもできます。この方法により、時間計算量が$O(n^2)$に劣化する確率が大幅に削減されます。 +この状況をできるだけ避けるため、**パーティションにおける基準数の選び方を最適化できます**。たとえば、ランダムに 1 つの要素を選んで基準数にできます。しかし、運が悪く毎回望ましくない基準数を選んでしまうと、効率は依然として十分ではありません。 -サンプルコードは以下の通りです: +注意すべき点として、プログラミング言語が通常生成するのは「疑似乱数」です。疑似乱数列に合わせて特定のテストケースを構築すると、クイックソートの効率はやはり劣化する可能性があります。 + +さらに改善するために、配列から 3 つの候補要素(通常は先頭、末尾、中間の要素)を選び、**その 3 つの候補要素の中央値を基準数とする**ことができます。こうすると、基準数が「小さすぎず大きすぎもしない」確率が大幅に上がります。もちろん、候補要素をさらに増やして、アルゴリズムの頑健性をいっそう高めることも可能です。この方法を採用すると、時間計算量が $O(n^2)$ まで劣化する確率は大きく下がります。 + +コード例を以下に示します。 === "Python" ```python title="quick_sort.py" def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int: - """3つの候補要素の中央値を選択""" + """3つの候補要素の中央値を選ぶ""" l, m, r = nums[left], nums[mid], nums[right] if (l <= m <= r) or (r <= m <= l): return mid # m は l と r の間 @@ -341,62 +637,62 @@ comments: true return right def partition(self, nums: list[int], left: int, right: int) -> int: - """分割(三点中央値)""" - # nums[left] をピボットとして使用 + """番兵による分割処理(3 点中央値)""" + # nums[left] を基準値とする med = self.median_three(nums, left, (left + right) // 2, right) - # 中央値を配列の最左端に交換 + # 中央値を配列の最左端に交換する nums[left], nums[med] = nums[med], nums[left] - # nums[left] をピボットとして使用 + # nums[left] を基準値とする i, j = left, right while i < j: while i < j and nums[j] >= nums[left]: - j -= 1 # 右から左へピボットより小さい最初の要素を探す + j -= 1 # 右から左へ基準値未満の最初の要素を探す while i < j and nums[i] <= nums[left]: - i += 1 # 左から右へピボットより大きい最初の要素を探す - # 要素を交換 + i += 1 # 左から右へ基準値より大きい最初の要素を探す + # 要素の交換 nums[i], nums[j] = nums[j], nums[i] - # ピボットを2つのサブ配列の境界に交換 + # 基準値を 2 つの部分配列の境界へ交換する nums[i], nums[left] = nums[left], nums[i] - return i # ピボットのインデックスを返す + return i # 基準値のインデックスを返す ``` === "C++" ```cpp title="quick_sort.cpp" - /* 三つの候補要素の中央値を選択 */ + /* 3つの候補要素の中央値を選ぶ */ int medianThree(vector &nums, int left, int mid, int right) { int l = nums[left], m = nums[mid], r = nums[right]; if ((l <= m && m <= r) || (r <= m && m <= l)) - return mid; // mはlとrの間 + return mid; // m は l と r の間 if ((m <= l && l <= r) || (r <= l && l <= m)) - return left; // lはmとrの間 + return left; // l は m と r の間 return right; } - /* 分割(三つの中央値) */ + /* 番兵による分割処理(3 点中央値) */ int partition(vector &nums, int left, int right) { - // 三つの候補要素の中央値を選択 + // 3つの候補要素の中央値を選ぶ int med = medianThree(nums, left, (left + right) / 2, right); - // 中央値を配列の最左位置に交換 - swap(nums, left, med); - // nums[left]をピボットとして使用 + // 中央値を配列の最左端に交換する + swap(nums[left], nums[med]); + // nums[left] を基準値とする int i = left, j = right; while (i < j) { while (i < j && nums[j] >= nums[left]) - j--; // 右から左へピボットより小さい最初の要素を検索 + j--; // 右から左へ基準値未満の最初の要素を探す while (i < j && nums[i] <= nums[left]) - i++; // 左から右へピボットより大きい最初の要素を検索 - swap(nums, i, j); // これら二つの要素を交換 + i++; // 左から右へ基準値より大きい最初の要素を探す + swap(nums[i], nums[j]); // この 2 つの要素を交換 } - swap(nums, i, left); // ピボットを二つのサブ配列の境界に交換 - return i; // ピボットのインデックスを返す + swap(nums[i], nums[left]); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す } ``` === "Java" ```java title="quick_sort.java" - /* 3つの候補要素の中央値を選択 */ + /* 3つの候補要素の中央値を選ぶ */ int medianThree(int[] nums, int left, int mid, int right) { int l = nums[left], m = nums[mid], r = nums[right]; if ((l <= m && m <= r) || (r <= m && m <= l)) @@ -406,22 +702,22 @@ comments: true return right; } - /* 分割(3つの中央値) */ + /* 番兵による分割処理(3 点中央値) */ int partition(int[] nums, int left, int right) { - // 3つの候補要素の中央値を選択 + // 3つの候補要素の中央値を選ぶ int med = medianThree(nums, left, (left + right) / 2, right); - // 中央値を配列の最左端の位置に交換 + // 中央値を配列の最左端に交換する swap(nums, left, med); - // nums[left] を基準値として使用 + // nums[left] を基準値とする int i = left, j = right; while (i < j) { while (i < j && nums[j] >= nums[left]) - j--; // 右から左へ、基準値より小さい最初の要素を検索 + j--; // 右から左へ基準値未満の最初の要素を探す while (i < j && nums[i] <= nums[left]) - i++; // 左から右へ、基準値より大きい最初の要素を検索 - swap(nums, i, j); // これら2つの要素を交換 + i++; // 左から右へ基準値より大きい最初の要素を探す + swap(nums, i, j); // この 2 つの要素を交換 } - swap(nums, i, left); // 基準値を2つの部分配列の境界に交換 + swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する return i; // 基準値のインデックスを返す } ``` @@ -429,124 +725,419 @@ comments: true === "C#" ```csharp title="quick_sort.cs" - [class]{QuickSortMedian}-[func]{MedianThree} + /* 3つの候補要素の中央値を選ぶ */ + int MedianThree(int[] nums, int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m は l と r の間 + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l は m と r の間 + return right; + } - [class]{QuickSortMedian}-[func]{Partition} + /* 番兵による分割処理(3 点中央値) */ + int Partition(int[] nums, int left, int right) { + // 3つの候補要素の中央値を選ぶ + int med = MedianThree(nums, left, (left + right) / 2, right); + // 中央値を配列の最左端に交換する + Swap(nums, left, med); + // nums[left] を基準値とする + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 右から左へ基準値未満の最初の要素を探す + while (i < j && nums[i] <= nums[left]) + i++; // 左から右へ基準値より大きい最初の要素を探す + Swap(nums, i, j); // この 2 つの要素を交換 + } + Swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す + } ``` === "Go" ```go title="quick_sort.go" - [class]{quickSortMedian}-[func]{medianThree} + /* 3つの候補要素の中央値を選ぶ */ + func (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int { + l, m, r := nums[left], nums[mid], nums[right] + if (l <= m && m <= r) || (r <= m && m <= l) { + return mid // m は l と r の間 + } + if (m <= l && l <= r) || (r <= l && l <= m) { + return left // l は m と r の間 + } + return right + } - [class]{quickSortMedian}-[func]{partition} + /* 番兵による分割処理(3 点中央値) */ + func (q *quickSortMedian) partition(nums []int, left, right int) int { + // nums[left] を基準値とする + med := q.medianThree(nums, left, (left+right)/2, right) + // 中央値を配列の最左端に交換する + nums[left], nums[med] = nums[med], nums[left] + // nums[left] を基準値とする + i, j := left, right + for i < j { + for i < j && nums[j] >= nums[left] { + j-- // 右から左へ基準値未満の最初の要素を探す + } + for i < j && nums[i] <= nums[left] { + i++ // 左から右へ基準値より大きい最初の要素を探す + } + // 要素の交換 + nums[i], nums[j] = nums[j], nums[i] + } + // 基準値を 2 つの部分配列の境界へ交換する + nums[i], nums[left] = nums[left], nums[i] + return i // 基準値のインデックスを返す + } ``` === "Swift" ```swift title="quick_sort.swift" - [class]{}-[func]{medianThree} + /* 3つの候補要素の中央値を選ぶ */ + func medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int { + let l = nums[left] + let m = nums[mid] + let r = nums[right] + if (l <= m && m <= r) || (r <= m && m <= l) { + return mid // m は l と r の間 + } + if (m <= l && l <= r) || (r <= l && l <= m) { + return left // l は m と r の間 + } + return right + } - [class]{}-[func]{partitionMedian} + /* 番兵による分割処理(3 点中央値) */ + func partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int { + // 3つの候補要素の中央値を選ぶ + let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right) + // 中央値を配列の最左端に交換する + nums.swapAt(left, med) + return partition(nums: &nums, left: left, right: right) + } ``` === "JS" ```javascript title="quick_sort.js" - [class]{QuickSortMedian}-[func]{medianThree} + /* 3つの候補要素の中央値を選ぶ */ + medianThree(nums, left, mid, right) { + let l = nums[left], + m = nums[mid], + r = nums[right]; + // m は l と r の間 + if ((l <= m && m <= r) || (r <= m && m <= l)) return mid; + // l は m と r の間 + if ((m <= l && l <= r) || (r <= l && l <= m)) return left; + return right; + } - [class]{QuickSortMedian}-[func]{partition} + /* 番兵による分割処理(3 点中央値) */ + partition(nums, left, right) { + // 3つの候補要素の中央値を選ぶ + let med = this.medianThree( + nums, + left, + Math.floor((left + right) / 2), + right + ); + // 中央値を配列の最左端に交換する + this.swap(nums, left, med); + // nums[left] を基準値とする + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // 右から左へ基準値未満の最初の要素を探す + while (i < j && nums[i] <= nums[left]) i++; // 左から右へ基準値より大きい最初の要素を探す + this.swap(nums, i, j); // この 2 つの要素を交換 + } + this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す + } ``` === "TS" ```typescript title="quick_sort.ts" - [class]{QuickSortMedian}-[func]{medianThree} + /* 3つの候補要素の中央値を選ぶ */ + medianThree( + nums: number[], + left: number, + mid: number, + right: number + ): number { + let l = nums[left], + m = nums[mid], + r = nums[right]; + // m は l と r の間 + if ((l <= m && m <= r) || (r <= m && m <= l)) return mid; + // l は m と r の間 + if ((m <= l && l <= r) || (r <= l && l <= m)) return left; + return right; + } - [class]{QuickSortMedian}-[func]{partition} + /* 番兵による分割処理(3 点中央値) */ + partition(nums: number[], left: number, right: number): number { + // 3つの候補要素の中央値を選ぶ + let med = this.medianThree( + nums, + left, + Math.floor((left + right) / 2), + right + ); + // 中央値を配列の最左端に交換する + this.swap(nums, left, med); + // nums[left] を基準値とする + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j--; // 右から左へ基準値未満の最初の要素を探す + } + while (i < j && nums[i] <= nums[left]) { + i++; // 左から右へ基準値より大きい最初の要素を探す + } + this.swap(nums, i, j); // この 2 つの要素を交換 + } + this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す + } ``` === "Dart" ```dart title="quick_sort.dart" - [class]{QuickSortMedian}-[func]{_medianThree} + /* 3つの候補要素の中央値を選ぶ */ + int _medianThree(List nums, int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m は l と r の間 + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l は m と r の間 + return right; + } - [class]{QuickSortMedian}-[func]{_partition} + /* 番兵による分割処理(3 点中央値) */ + int _partition(List nums, int left, int right) { + // 3つの候補要素の中央値を選ぶ + int med = _medianThree(nums, left, (left + right) ~/ 2, right); + // 中央値を配列の最左端に交換する + _swap(nums, left, med); + // nums[left] を基準値とする + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // 右から左へ基準値未満の最初の要素を探す + while (i < j && nums[i] <= nums[left]) i++; // 左から右へ基準値より大きい最初の要素を探す + _swap(nums, i, j); // この 2 つの要素を交換 + } + _swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す + } ``` === "Rust" ```rust title="quick_sort.rs" - [class]{QuickSortMedian}-[func]{median_three} + /* 3つの候補要素の中央値を選ぶ */ + fn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize { + let (l, m, r) = (nums[left], nums[mid], nums[right]); + if (l <= m && m <= r) || (r <= m && m <= l) { + return mid; // m は l と r の間 + } + if (m <= l && l <= r) || (r <= l && l <= m) { + return left; // l は m と r の間 + } + right + } - [class]{QuickSortMedian}-[func]{partition} + /* 番兵による分割処理(3 点中央値) */ + fn partition(nums: &mut [i32], left: usize, right: usize) -> usize { + // 3つの候補要素の中央値を選ぶ + let med = Self::median_three(nums, left, (left + right) / 2, right); + // 中央値を配列の最左端に交換する + nums.swap(left, med); + // nums[left] を基準値とする + let (mut i, mut j) = (left, right); + while i < j { + while i < j && nums[j] >= nums[left] { + j -= 1; // 右から左へ基準値未満の最初の要素を探す + } + while i < j && nums[i] <= nums[left] { + i += 1; // 左から右へ基準値より大きい最初の要素を探す + } + nums.swap(i, j); // この 2 つの要素を交換 + } + nums.swap(i, left); // 基準値を 2 つの部分配列の境界へ交換する + i // 基準値のインデックスを返す + } ``` === "C" ```c title="quick_sort.c" - [class]{}-[func]{medianThree} + /* 3つの候補要素の中央値を選ぶ */ + int medianThree(int nums[], int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m は l と r の間 + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l は m と r の間 + return right; + } - [class]{}-[func]{partitionMedian} + /* 番兵による分割処理(3 点中央値) */ + int partitionMedian(int nums[], int left, int right) { + // 3つの候補要素の中央値を選ぶ + int med = medianThree(nums, left, (left + right) / 2, right); + // 中央値を配列の最左端に交換する + swap(nums, left, med); + // nums[left] を基準値とする + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 右から左へ基準値未満の最初の要素を探す + while (i < j && nums[i] <= nums[left]) + i++; // 左から右へ基準値より大きい最初の要素を探す + swap(nums, i, j); // この 2 つの要素を交換 + } + swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する + return i; // 基準値のインデックスを返す + } ``` === "Kotlin" ```kotlin title="quick_sort.kt" - [class]{}-[func]{medianThree} + /* 3つの候補要素の中央値を選ぶ */ + fun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int { + val l = nums[left] + val m = nums[mid] + val r = nums[right] + if ((m in l..r) || (m in r..l)) + return mid // m は l と r の間 + if ((l in m..r) || (l in r..m)) + return left // l は m と r の間 + return right + } - [class]{}-[func]{partitionMedian} + /* 番兵による分割処理(3 点中央値) */ + fun partitionMedian(nums: IntArray, left: Int, right: Int): Int { + // 3つの候補要素の中央値を選ぶ + val med = medianThree(nums, left, (left + right) / 2, right) + // 中央値を配列の最左端に交換する + swap(nums, left, med) + // nums[left] を基準値とする + var i = left + var j = right + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j-- // 右から左へ基準値未満の最初の要素を探す + while (i < j && nums[i] <= nums[left]) + i++ // 左から右へ基準値より大きい最初の要素を探す + swap(nums, i, j) // この 2 つの要素を交換 + } + swap(nums, i, left) // 基準値を 2 つの部分配列の境界へ交換する + return i // 基準値のインデックスを返す + } ``` === "Ruby" ```ruby title="quick_sort.rb" - [class]{QuickSortMedian}-[func]{median_three} + ### 3 つの候補要素の中央値を選ぶ ### + def median_three(nums, left, mid, right) + # 3つの候補要素の中央値を選ぶ + _l, _m, _r = nums[left], nums[mid], nums[right] + # m は l と r の間 + return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l) + # l は m と r の間 + return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m) + return right + end - [class]{QuickSortMedian}-[func]{partition} + ### 3 つの候補要素の中央値を選ぶ ### + def median_three(nums, left, mid, right) + # 3つの候補要素の中央値を選ぶ + _l, _m, _r = nums[left], nums[mid], nums[right] + # m は l と r の間 + return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l) + # l は m と r の間 + return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m) + return right + end + + # ## 番兵分割(三数中央値)### + def partition(nums, left, right) + # ## nums[left] を基準値とする + med = median_three(nums, left, (left + right) / 2, right) + # 中央値を配列の最左端に交換する + nums[left], nums[med] = nums[med], nums[left] + i, j = left, right + while i < j + while i < j && nums[j] >= nums[left] + j -= 1 # 右から左へ基準値未満の最初の要素を探す + end + while i < j && nums[i] <= nums[left] + i += 1 # 左から右へ基準値より大きい最初の要素を探す + end + # 要素の交換 + nums[i], nums[j] = nums[j], nums[i] + end + # 基準値を 2 つの部分配列の境界へ交換する + nums[i], nums[left] = nums[left], nums[i] + i # 基準値のインデックスを返す + end ``` -## 11.5.5   末尾再帰最適化 +??? pythontutor "コードの可視化" -**特定の入力では、クイックソートはより多くの空間を占有する可能性があります**。例えば、完全に順序付けられた入力配列を考えてみましょう。再帰でのサブ配列の長さを$m$とします。各ラウンドのピボット分割で、長さ$0$の左サブ配列と長さ$m - 1$の右サブ配列が生成されます。これは、再帰呼び出しごとに問題サイズが1つの要素のみ減少することを意味し、各レベルの再帰での削減が非常に小さくなります。 -結果として、再帰ツリーの高さは$n − 1$に達する可能性があり、これには$O(n)$のスタックフレーム空間が必要です。 +
+ -スタックフレーム空間の蓄積を防ぐため、各ラウンドのピボットソート後に2つのサブ配列の長さを比較し、**より短いサブ配列のみを再帰的にソート**できます。より短いサブ配列の長さは$n / 2$を超えないため、この方法は再帰深度が$\log n$を超えないことを保証し、最悪空間計算量を$O(\log n)$に最適化します。コードは以下の通りです: +## 11.5.5   再帰の深さの最適化 + +**一部の入力では、クイックソートは多くの空間を消費する可能性があります**。完全に整列済みの入力配列を例にとり、再帰中の部分配列の長さを $m$ とします。各回のパーティション操作では長さ $0$ の左部分配列と長さ $m - 1$ の右部分配列が生成されます。これは、各再帰呼び出しで減る問題サイズが非常に小さいこと(要素が 1 つ減るだけ)を意味し、再帰木の高さは $n - 1$ に達するため、このとき $O(n)$ のスタックフレーム空間を占有します。 + +スタックフレーム空間の蓄積を防ぐために、各回のパーティション完了後に 2 つの部分配列の長さを比較し、**短いほうの部分配列に対してのみ再帰**を行えます。短い部分配列の長さは $n / 2$ を超えないため、この方法なら再帰の深さを $\log n$ 以下に抑えられ、最悪時の空間計算量を $O(\log n)$ まで最適化できます。コードを以下に示します。 === "Python" ```python title="quick_sort.py" def quick_sort(self, nums: list[int], left: int, right: int): - """クイックソート(末尾再帰最適化)""" - # サブ配列の長さが1のときに終了 + """クイックソート(再帰深度最適化)""" + # 部分配列の長さが 1 なら終了 while left < right: - # 分割操作 + # 番兵による分割処理 pivot = self.partition(nums, left, right) - # 2つのサブ配列のうち短い方に対してクイックソートを実行 + # 2 つの部分配列のうち短いほうにクイックソートを適用する if pivot - left < right - pivot: - self.quick_sort(nums, left, pivot - 1) # 左サブ配列を再帰的にソート - left = pivot + 1 # 残りの未ソート区間は [pivot + 1, right] + self.quick_sort(nums, left, pivot - 1) # 左部分配列を再帰的にソート + left = pivot + 1 # 未ソート区間の残りは [pivot + 1, right] else: - self.quick_sort(nums, pivot + 1, right) # 右サブ配列を再帰的にソート - right = pivot - 1 # 残りの未ソート区間は [left, pivot - 1] + self.quick_sort(nums, pivot + 1, right) # 右部分配列を再帰的にソート + right = pivot - 1 # 未ソート区間の残りは [left, pivot - 1] ``` === "C++" ```cpp title="quick_sort.cpp" - /* クイックソート(末尾再帰最適化) */ + /* クイックソート(再帰深度最適化) */ void quickSort(vector &nums, int left, int right) { - // サブ配列の長さが1の時終了 + // 部分配列の長さが 1 なら終了 while (left < right) { - // 分割操作 + // 番兵による分割処理 int pivot = partition(nums, left, right); - // 二つのサブ配列のうち短い方でクイックソートを実行 + // 2 つの部分配列のうち短いほうにクイックソートを適用する if (pivot - left < right - pivot) { - quickSort(nums, left, pivot - 1); // 左サブ配列を再帰的にソート - left = pivot + 1; // 残りの未ソート区間は[pivot + 1, right] + quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート + left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right] } else { - quickSort(nums, pivot + 1, right); // 右サブ配列を再帰的にソート - right = pivot - 1; // 残りの未ソート区間は[left, pivot - 1] + quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート + right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1] } } } @@ -555,19 +1146,19 @@ comments: true === "Java" ```java title="quick_sort.java" - /* クイックソート(末尾再帰最適化) */ + /* クイックソート(再帰深度最適化) */ void quickSort(int[] nums, int left, int right) { - // 部分配列の長さが 1 のとき終了 + // 部分配列の長さが 1 なら終了 while (left < right) { - // 分割操作 + // 番兵による分割処理 int pivot = partition(nums, left, right); - // 2つの部分配列のうち短い方にクイックソートを実行 + // 2 つの部分配列のうち短いほうにクイックソートを適用する if (pivot - left < right - pivot) { quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート - left = pivot + 1; // 残りの未ソート区間は [pivot + 1, right] + left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right] } else { quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート - right = pivot - 1; // 残りの未ソート区間は [left, pivot - 1] + right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1] } } } @@ -576,59 +1167,242 @@ comments: true === "C#" ```csharp title="quick_sort.cs" - [class]{QuickSortTailCall}-[func]{QuickSort} + /* クイックソート(再帰深度最適化) */ + void QuickSort(int[] nums, int left, int right) { + // 部分配列の長さが 1 なら終了 + while (left < right) { + // 番兵による分割処理 + int pivot = Partition(nums, left, right); + // 2 つの部分配列のうち短いほうにクイックソートを適用する + if (pivot - left < right - pivot) { + QuickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート + left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right] + } else { + QuickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート + right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1] + } + } + } ``` === "Go" ```go title="quick_sort.go" - [class]{quickSortTailCall}-[func]{quickSort} + /* クイックソート(再帰深度最適化) */ + func (q *quickSortTailCall) quickSort(nums []int, left, right int) { + // 部分配列の長さが 1 なら終了 + for left < right { + // 番兵による分割処理 + pivot := q.partition(nums, left, right) + // 2 つの部分配列のうち短いほうにクイックソートを適用する + if pivot-left < right-pivot { + q.quickSort(nums, left, pivot-1) // 左部分配列を再帰的にソート + left = pivot + 1 // 未ソート区間の残りは [pivot + 1, right] + } else { + q.quickSort(nums, pivot+1, right) // 右部分配列を再帰的にソート + right = pivot - 1 // 未ソート区間の残りは [left, pivot - 1] + } + } + } ``` === "Swift" ```swift title="quick_sort.swift" - [class]{}-[func]{quickSortTailCall} + /* クイックソート(再帰深度最適化) */ + func quickSortTailCall(nums: inout [Int], left: Int, right: Int) { + var left = left + var right = right + // 部分配列の長さが 1 なら終了 + while left < right { + // 番兵による分割処理 + let pivot = partition(nums: &nums, left: left, right: right) + // 2 つの部分配列のうち短いほうにクイックソートを適用する + if (pivot - left) < (right - pivot) { + quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // 左部分配列を再帰的にソート + left = pivot + 1 // 未ソート区間の残りは [pivot + 1, right] + } else { + quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // 右部分配列を再帰的にソート + right = pivot - 1 // 未ソート区間の残りは [left, pivot - 1] + } + } + } ``` === "JS" ```javascript title="quick_sort.js" - [class]{QuickSortTailCall}-[func]{quickSort} + /* クイックソート(再帰深度最適化) */ + quickSort(nums, left, right) { + // 部分配列の長さが 1 なら終了 + while (left < right) { + // 番兵による分割処理 + let pivot = this.partition(nums, left, right); + // 2 つの部分配列のうち短いほうにクイックソートを適用する + if (pivot - left < right - pivot) { + this.quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート + left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right] + } else { + this.quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート + right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1] + } + } + } ``` === "TS" ```typescript title="quick_sort.ts" - [class]{QuickSortTailCall}-[func]{quickSort} + /* クイックソート(再帰深度最適化) */ + quickSort(nums: number[], left: number, right: number): void { + // 部分配列の長さが 1 なら終了 + while (left < right) { + // 番兵による分割処理 + let pivot = this.partition(nums, left, right); + // 2 つの部分配列のうち短いほうにクイックソートを適用する + if (pivot - left < right - pivot) { + this.quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート + left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right] + } else { + this.quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート + right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1] + } + } + } ``` === "Dart" ```dart title="quick_sort.dart" - [class]{QuickSortTailCall}-[func]{quickSort} + /* クイックソート(再帰深度最適化) */ + void quickSort(List nums, int left, int right) { + // 部分配列の長さが 1 なら終了 + while (left < right) { + // 番兵による分割処理 + int pivot = _partition(nums, left, right); + // 2 つの部分配列のうち短いほうにクイックソートを適用する + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート + left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート + right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1] + } + } + } ``` === "Rust" ```rust title="quick_sort.rs" - [class]{QuickSortTailCall}-[func]{quick_sort} + /* クイックソート(再帰深度最適化) */ + pub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) { + // 部分配列の長さが 1 なら終了 + while left < right { + // 番兵による分割処理 + let pivot = Self::partition(nums, left as usize, right as usize) as i32; + // 2 つの部分配列のうち短いほうにクイックソートを適用する + if pivot - left < right - pivot { + Self::quick_sort(left, pivot - 1, nums); // 左部分配列を再帰的にソート + left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right] + } else { + Self::quick_sort(pivot + 1, right, nums); // 右部分配列を再帰的にソート + right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1] + } + } + } ``` === "C" ```c title="quick_sort.c" - [class]{}-[func]{quickSortTailCall} + /* クイックソート(再帰深度最適化) */ + void quickSortTailCall(int nums[], int left, int right) { + // 部分配列の長さが 1 なら終了 + while (left < right) { + // 番兵による分割処理 + int pivot = partition(nums, left, right); + // 2 つの部分配列のうち短いほうにクイックソートを適用する + if (pivot - left < right - pivot) { + // 左部分配列を再帰的にソート + quickSortTailCall(nums, left, pivot - 1); + // 未ソート区間の残りは [pivot + 1, right] + left = pivot + 1; + } else { + // 右部分配列を再帰的にソート + quickSortTailCall(nums, pivot + 1, right); + // 未ソート区間の残りは [left, pivot - 1] + right = pivot - 1; + } + } + } ``` === "Kotlin" ```kotlin title="quick_sort.kt" - [class]{}-[func]{quickSortTailCall} + /* クイックソート(再帰深度最適化) */ + fun quickSortTailCall(nums: IntArray, left: Int, right: Int) { + // 部分配列の長さが 1 なら終了 + var l = left + var r = right + while (l < r) { + // 番兵による分割処理 + val pivot = partition(nums, l, r) + // 2 つの部分配列のうち短いほうにクイックソートを適用する + if (pivot - l < r - pivot) { + quickSort(nums, l, pivot - 1) // 左部分配列を再帰的にソート + l = pivot + 1 // 未ソート区間の残りは [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, r) // 右部分配列を再帰的にソート + r = pivot - 1 // 未ソート区間の残りは [left, pivot - 1] + } + } + } ``` === "Ruby" ```ruby title="quick_sort.rb" - [class]{QuickSortTailCall}-[func]{quick_sort} + ### 番兵分割 ### + def partition(nums, left, right) + # nums[left] を基準値とする + i = left + j = right + while i < j + while i < j && nums[j] >= nums[left] + j -= 1 # 右から左へ基準値未満の最初の要素を探す + end + while i < j && nums[i] <= nums[left] + i += 1 # 左から右へ基準値より大きい最初の要素を探す + end + # 要素の交換 + nums[i], nums[j] = nums[j], nums[i] + end + # 基準値を 2 つの部分配列の境界へ交換する + nums[i], nums[left] = nums[left], nums[i] + i # 基準値のインデックスを返す + end + + # ## クイックソート(再帰深度最適化)### + def quick_sort(nums, left, right) + # 部分配列の長さが 1 でない場合は再帰する + while left < right + # 番兵分割 + pivot = partition(nums, left, right) + # 2 つの部分配列のうち短いほうにクイックソートを適用する + if pivot - left < right - pivot + quick_sort(nums, left, pivot - 1) + left = pivot + 1 # 未ソート区間の残りは [pivot + 1, right] + else + quick_sort(nums, pivot + 1, right) + right = pivot - 1 # 未ソート区間の残りは [left, pivot - 1] + end + end + end ``` + +??? pythontutor "コードの可視化" + +
+ diff --git a/ja/docs/chapter_sorting/radix_sort.md b/ja/docs/chapter_sorting/radix_sort.md index f397caf78..e63fd358a 100644 --- a/ja/docs/chapter_sorting/radix_sort.md +++ b/ja/docs/chapter_sorting/radix_sort.md @@ -4,74 +4,74 @@ comments: true # 11.10   基数ソート -前の節では計数ソートを紹介しました。これは、データサイズ $n$ が大きいがデータ範囲 $m$ が小さいシナリオに適しています。$n = 10^6$ の学生IDをソートする必要があり、各IDが $8$ 桁の数字であるとします。これは、データ範囲 $m = 10^8$ が非常に大きいことを意味します。この場合、計数ソートを使用すると、大量のメモリスペースが必要になります。基数ソートはこの状況を回避できます。 +前節では計数ソートを紹介しました。これは、データ量 $n$ が大きく、データ範囲 $m$ が小さい場合に適しています。$n = 10^6$ 個の学籍番号をソートすると仮定すると、学籍番号は $8$ 桁の数字なので、データ範囲 $m = 10^8$ は非常に大きくなります。計数ソートでは大量のメモリ空間を確保する必要がありますが、基数ソートではこの問題を回避できます。 -基数ソートは計数ソートと同じ核心概念を共有し、要素の頻度をカウントすることでソートします。同時に、基数ソートは数字の桁間の漸進的関係を利用してこれを基盤としています。桁を一度に一つずつ処理してソートし、最終的なソート順序を達成します。 +基数ソート(radix sort)の基本的な考え方は計数ソートと同じで、個数を数えることによってソートを実現します。そのうえで、基数ソートは各桁の段階的な関係を利用し、各桁を順にソートすることで、最終的なソート結果を得ます。 -## 11.10.1   アルゴリズムの過程 +## 11.10.1   アルゴリズムの流れ -学生IDデータを例として、最下位桁を $1$ 番目、最上位桁を $8$ 番目とすると、基数ソートの過程は以下の図に示されています。 +学籍番号データを例にすると、数字の最下位桁を第 $1$ 位、最上位桁を第 $8$ 位としたとき、基数ソートの流れは次図のようになります。 -1. 桁 $k = 1$ を初期化します。 -2. 学生IDの $k$ 番目の桁に対して「計数ソート」を実行します。完了後、データは $k$ 番目の桁に基づいて最小から最大までソートされます。 -3. $k$ を $1$ 増やし、手順 `2.` に戻って反復を続け、すべての桁がソートされるまで続けます。この時点で過程が終了します。 +1. 桁番号 $k = 1$ を初期化します。 +2. 学籍番号の第 $k$ 位に対して「計数ソート」を実行します。完了すると、データは第 $k$ 位に従って昇順に並びます。 +3. $k$ を $1$ 増やし、手順 `2.` に戻って反復を続けます。すべての桁のソートが完了したら終了します。 -![基数ソートアルゴリズムの過程](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" } +![基数ソートのアルゴリズムの流れ](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" } -

図 11-18   基数ソートアルゴリズムの過程

+

図 11-18   基数ソートのアルゴリズムの流れ

-以下、コード実装を詳しく見てみます。基数 $d$ での数 $x$ に対して、その $k$ 番目の桁 $x_k$ を取得するには、以下の計算式を使用できます: +以下ではコード実装を分解して見ていきます。$d$ 進数の数値 $x$ について、その第 $k$ 位 $x_k$ を取得するには、次の計算式を用います。 $$ x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d $$ -ここで $\lfloor a \rfloor$ は浮動小数点数 $a$ の切り捨てを表し、$\bmod \: d$ は $d$ による剰余を表します。学生IDデータの場合、$d = 10$ で $k \in [1, 8]$ です。 +ここで、$\lfloor a \rfloor$ は浮動小数点数 $a$ の切り捨てを表し、$\bmod \: d$ は $d$ による剰余を表します。学籍番号データでは、$d = 10$ かつ $k \in [1, 8]$ です。 -さらに、$k$ 番目の桁に基づいてソートできるように、計数ソートのコードを少し修正する必要があります: +さらに、数字の第 $k$ 位に基づいてソートできるように、計数ソートのコードを少し変更する必要があります。 === "Python" ```python title="radix_sort.py" def digit(num: int, exp: int) -> int: - """要素 num の k 番目の桁を取得、exp = 10^(k-1)""" - # k の代わりに exp を渡すことで、ここでコストの高い累乗計算を避けることができる + """要素 num の下から k 桁目を取得(exp = 10^(k-1))""" + # ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す return (num // exp) % 10 def counting_sort_digit(nums: list[int], exp: int): - """計数ソート(nums の k 番目の桁に基づく)""" - # 10進数の桁の範囲は 0~9、したがって長さ10のバケット配列が必要 + """計数ソート(nums の k 桁目でソート)""" + # 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 counter = [0] * 10 n = len(nums) - # 数字 0~9 の出現回数を統計 + # 0~9 の各数字の出現回数を集計する for i in range(n): - d = digit(nums[i], exp) # nums[i] の k 番目の桁を取得、d とする - counter[d] += 1 # 数字 d の出現回数を統計 - # 前置和を計算し、「出現回数」を「配列インデックス」に変換 + d = digit(nums[i], exp) # nums[i] の第 k 位を取得し、d とする + counter[d] += 1 # 数字 d の出現回数を数える + # 累積和を求め、「出現回数」を「配列インデックス」に変換する for i in range(1, 10): counter[i] += counter[i - 1] - # 逆順に走査し、バケット統計に基づいて各要素を res に配置 + # 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する res = [0] * n for i in range(n - 1, -1, -1): d = digit(nums[i], exp) - j = counter[d] - 1 # 配列内の d のインデックス j を取得 - res[j] = nums[i] # 現在の要素をインデックス j に配置 - counter[d] -= 1 # d の数を1減らす - # 結果を使用して元の配列 nums を上書き + j = counter[d] - 1 # d の配列内インデックス j を取得する + res[j] = nums[i] # 現在の要素をインデックス j に格納する + counter[d] -= 1 # d の個数を 1 減らす + # 結果で元の配列 nums を上書きする for i in range(n): nums[i] = res[i] def radix_sort(nums: list[int]): """基数ソート""" - # 配列の最大要素を取得し、最大桁数を判定するために使用 + # 最大桁数の判定用に配列の最大要素を取得 m = max(nums) - # 最下位桁から最上位桁まで走査 + # 下位桁から上位桁の順に走査する exp = 1 while exp <= m: - # 配列要素の k 番目の桁に対して計数ソートを実行 + # 配列要素の k 桁目に対して計数ソートを行う # k = 1 -> exp = 1 # k = 2 -> exp = 10 - # つまり、exp = 10^(k-1) + # つまり exp = 10^(k-1) counting_sort_digit(nums, exp) exp *= 10 ``` @@ -79,49 +79,49 @@ $$ === "C++" ```cpp title="radix_sort.cpp" - /* 要素numのk番目の桁を取得、exp = 10^(k-1) */ + /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */ int digit(int num, int exp) { - // kの代わりにexpを渡すことで、ここで繰り返される高価な冪乗計算を避けることができる + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す return (num / exp) % 10; } - /* カウントソート(numsのk番目の桁に基づく) */ + /* 計数ソート(nums の k 桁目でソート) */ void countingSortDigit(vector &nums, int exp) { - // 10進数の桁範囲は0~9なので、長さ10のバケット配列が必要 + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 vector counter(10, 0); int n = nums.size(); - // 数字0~9の出現回数を統計 + // 0~9 の各数字の出現回数を集計する for (int i = 0; i < n; i++) { - int d = digit(nums[i], exp); // nums[i]のk番目の桁を取得、dとして記録 - counter[d]++; // 数字dの出現回数を統計 + int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする + counter[d]++; // 数字 d の出現回数を数える } - // 前缀和を計算し、「出現回数」を「配列インデックス」に変換 + // 累積和を求め、「出現回数」を「配列インデックス」に変換する for (int i = 1; i < 10; i++) { counter[i] += counter[i - 1]; } - // 逆順で走査し、バケット統計に基づいて各要素をresに配置 + // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する vector res(n, 0); for (int i = n - 1; i >= 0; i--) { int d = digit(nums[i], exp); - int j = counter[d] - 1; // dが配列内にあるインデックスjを取得 - res[j] = nums[i]; // 現在の要素をインデックスjに配置 - counter[d]--; // dのカウントを1減らす + int j = counter[d] - 1; // d の配列内インデックス j を取得する + res[j] = nums[i]; // 現在の要素をインデックス j に格納する + counter[d]--; // d の個数を 1 減らす } - // 結果で元の配列numsを上書き + // 結果で元の配列 nums を上書きする for (int i = 0; i < n; i++) nums[i] = res[i]; } /* 基数ソート */ void radixSort(vector &nums) { - // 配列の最大要素を取得、最大桁数を判定するために使用 + // 最大桁数の判定用に配列の最大要素を取得 int m = *max_element(nums.begin(), nums.end()); - // 最下位桁から最上位桁まで走査 + // 下位桁から上位桁の順に走査する for (int exp = 1; exp <= m; exp *= 10) - // 配列要素のk番目の桁でカウントソートを実行 + // 配列要素の k 桁目に対して計数ソートを行う // k = 1 -> exp = 1 // k = 2 -> exp = 10 - // つまり、exp = 10^(k-1) + // つまり exp = 10^(k-1) countingSortDigit(nums, exp); } ``` @@ -129,52 +129,52 @@ $$ === "Java" ```java title="radix_sort.java" - /* 要素 num の k 番目の桁を取得、exp = 10^(k-1) */ + /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */ int digit(int num, int exp) { - // k の代わりに exp を渡すことで、ここでコストの高い累乗計算の繰り返しを避けることができる + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す return (num / exp) % 10; } - /* 計数ソート(nums の k 番目の桁に基づく) */ + /* 計数ソート(nums の k 桁目でソート) */ void countingSortDigit(int[] nums, int exp) { - // 10進数の桁の範囲は 0~9、したがって長さ 10 のバケット配列が必要 + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 int[] counter = new int[10]; int n = nums.length; - // 桁 0~9 の出現回数を統計 + // 0~9 の各数字の出現回数を集計する for (int i = 0; i < n; i++) { - int d = digit(nums[i], exp); // nums[i] の k 番目の桁を取得、d とする - counter[d]++; // 桁 d の出現回数を統計 + int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする + counter[d]++; // 数字 d の出現回数を数える } - // 累積和を計算し、「出現回数」を「配列インデックス」に変換 + // 累積和を求め、「出現回数」を「配列インデックス」に変換する for (int i = 1; i < 10; i++) { counter[i] += counter[i - 1]; } - // 逆順に走査し、バケット統計に基づいて各要素を res に配置 + // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する int[] res = new int[n]; for (int i = n - 1; i >= 0; i--) { int d = digit(nums[i], exp); - int j = counter[d] - 1; // 配列内での d のインデックス j を取得 - res[j] = nums[i]; // 現在の要素をインデックス j に配置 - counter[d]--; // d のカウントを 1 減らす + int j = counter[d] - 1; // d の配列内インデックス j を取得する + res[j] = nums[i]; // 現在の要素をインデックス j に格納する + counter[d]--; // d の個数を 1 減らす } - // 結果で元の配列 nums を上書き + // 結果で元の配列 nums を上書きする for (int i = 0; i < n; i++) nums[i] = res[i]; } /* 基数ソート */ void radixSort(int[] nums) { - // 配列の最大要素を取得し、最大桁数を判定するために使用 + // 最大桁数の判定用に配列の最大要素を取得 int m = Integer.MIN_VALUE; for (int num : nums) if (num > m) m = num; - // 最下位桁から最上位桁まで走査 + // 下位桁から上位桁の順に走査する for (int exp = 1; exp <= m; exp *= 10) { - // 配列要素の k 番目の桁に対して計数ソートを実行 + // 配列要素の k 桁目に対して計数ソートを行う // k = 1 -> exp = 1 // k = 2 -> exp = 10 - // すなわち exp = 10^(k-1) + // つまり exp = 10^(k-1) countingSortDigit(nums, exp); } } @@ -183,111 +183,558 @@ $$ === "C#" ```csharp title="radix_sort.cs" - [class]{radix_sort}-[func]{Digit} + /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */ + int Digit(int num, int exp) { + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す + return (num / exp) % 10; + } - [class]{radix_sort}-[func]{CountingSortDigit} + /* 計数ソート(nums の k 桁目でソート) */ + void CountingSortDigit(int[] nums, int exp) { + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 + int[] counter = new int[10]; + int n = nums.Length; + // 0~9 の各数字の出現回数を集計する + for (int i = 0; i < n; i++) { + int d = Digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする + counter[d]++; // 数字 d の出現回数を数える + } + // 累積和を求め、「出現回数」を「配列インデックス」に変換する + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する + int[] res = new int[n]; + for (int i = n - 1; i >= 0; i--) { + int d = Digit(nums[i], exp); + int j = counter[d] - 1; // d の配列内インデックス j を取得する + res[j] = nums[i]; // 現在の要素をインデックス j に格納する + counter[d]--; // d の個数を 1 減らす + } + // 結果で元の配列 nums を上書きする + for (int i = 0; i < n; i++) { + nums[i] = res[i]; + } + } - [class]{radix_sort}-[func]{RadixSort} + /* 基数ソート */ + void RadixSort(int[] nums) { + // 最大桁数の判定用に配列の最大要素を取得 + int m = int.MinValue; + foreach (int num in nums) { + if (num > m) m = num; + } + // 下位桁から上位桁の順に走査する + for (int exp = 1; exp <= m; exp *= 10) { + // 配列要素の k 桁目に対して計数ソートを行う + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // つまり exp = 10^(k-1) + CountingSortDigit(nums, exp); + } + } ``` === "Go" ```go title="radix_sort.go" - [class]{}-[func]{digit} + /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */ + func digit(num, exp int) int { + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す + return (num / exp) % 10 + } - [class]{}-[func]{countingSortDigit} + /* 計数ソート(nums の k 桁目でソート) */ + func countingSortDigit(nums []int, exp int) { + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 + counter := make([]int, 10) + n := len(nums) + // 0~9 の各数字の出現回数を集計する + for i := 0; i < n; i++ { + d := digit(nums[i], exp) // nums[i] の第 k 位を取得し、d とする + counter[d]++ // 数字 d の出現回数を数える + } + // 累積和を求め、「出現回数」を「配列インデックス」に変換する + for i := 1; i < 10; i++ { + counter[i] += counter[i-1] + } + // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する + res := make([]int, n) + for i := n - 1; i >= 0; i-- { + d := digit(nums[i], exp) + j := counter[d] - 1 // d の配列内インデックス j を取得する + res[j] = nums[i] // 現在の要素をインデックス j に格納する + counter[d]-- // d の個数を 1 減らす + } + // 結果で元の配列 nums を上書きする + for i := 0; i < n; i++ { + nums[i] = res[i] + } + } - [class]{}-[func]{radixSort} + /* 基数ソート */ + func radixSort(nums []int) { + // 最大桁数の判定用に配列の最大要素を取得 + max := math.MinInt + for _, num := range nums { + if num > max { + max = num + } + } + // 下位桁から上位桁の順に走査する + for exp := 1; max >= exp; exp *= 10 { + // 配列要素の k 桁目に対して計数ソートを行う + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // つまり exp = 10^(k-1) + countingSortDigit(nums, exp) + } + } ``` === "Swift" ```swift title="radix_sort.swift" - [class]{}-[func]{digit} + /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */ + func digit(num: Int, exp: Int) -> Int { + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す + (num / exp) % 10 + } - [class]{}-[func]{countingSortDigit} + /* 計数ソート(nums の k 桁目でソート) */ + func countingSortDigit(nums: inout [Int], exp: Int) { + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 + var counter = Array(repeating: 0, count: 10) + // 0~9 の各数字の出現回数を集計する + for i in nums.indices { + let d = digit(num: nums[i], exp: exp) // nums[i] の第 k 位を取得し、d とする + counter[d] += 1 // 数字 d の出現回数を数える + } + // 累積和を求め、「出現回数」を「配列インデックス」に変換する + for i in 1 ..< 10 { + counter[i] += counter[i - 1] + } + // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する + var res = Array(repeating: 0, count: nums.count) + for i in nums.indices.reversed() { + let d = digit(num: nums[i], exp: exp) + let j = counter[d] - 1 // d の配列内インデックス j を取得する + res[j] = nums[i] // 現在の要素をインデックス j に格納する + counter[d] -= 1 // d の個数を 1 減らす + } + // 結果で元の配列 nums を上書きする + for i in nums.indices { + nums[i] = res[i] + } + } - [class]{}-[func]{radixSort} + /* 基数ソート */ + func radixSort(nums: inout [Int]) { + // 最大桁数の判定用に配列の最大要素を取得 + var m = Int.min + for num in nums { + if num > m { + m = num + } + } + // 下位桁から上位桁の順に走査する + for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) { + // 配列要素の k 桁目に対して計数ソートを行う + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // つまり exp = 10^(k-1) + countingSortDigit(nums: &nums, exp: exp) + } + } ``` === "JS" ```javascript title="radix_sort.js" - [class]{}-[func]{digit} + /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */ + function digit(num, exp) { + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す + return Math.floor(num / exp) % 10; + } - [class]{}-[func]{countingSortDigit} + /* 計数ソート(nums の k 桁目でソート) */ + function countingSortDigit(nums, exp) { + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 + const counter = new Array(10).fill(0); + const n = nums.length; + // 0~9 の各数字の出現回数を集計する + for (let i = 0; i < n; i++) { + const d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする + counter[d]++; // 数字 d の出現回数を数える + } + // 累積和を求め、「出現回数」を「配列インデックス」に変換する + for (let i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する + const res = new Array(n).fill(0); + for (let i = n - 1; i >= 0; i--) { + const d = digit(nums[i], exp); + const j = counter[d] - 1; // d の配列内インデックス j を取得する + res[j] = nums[i]; // 現在の要素をインデックス j に格納する + counter[d]--; // d の個数を 1 減らす + } + // 結果で元の配列 nums を上書きする + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } - [class]{}-[func]{radixSort} + /* 基数ソート */ + function radixSort(nums) { + // 最大桁数の判定用に配列の最大要素を取得 + let m = Math.max(... nums); + // 下位桁から上位桁の順に走査する + for (let exp = 1; exp <= m; exp *= 10) { + // 配列要素の k 桁目に対して計数ソートを行う + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // つまり exp = 10^(k-1) + countingSortDigit(nums, exp); + } + } ``` === "TS" ```typescript title="radix_sort.ts" - [class]{}-[func]{digit} + /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */ + function digit(num: number, exp: number): number { + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す + return Math.floor(num / exp) % 10; + } - [class]{}-[func]{countingSortDigit} + /* 計数ソート(nums の k 桁目でソート) */ + function countingSortDigit(nums: number[], exp: number): void { + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 + const counter = new Array(10).fill(0); + const n = nums.length; + // 0~9 の各数字の出現回数を集計する + for (let i = 0; i < n; i++) { + const d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする + counter[d]++; // 数字 d の出現回数を数える + } + // 累積和を求め、「出現回数」を「配列インデックス」に変換する + for (let i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する + const res = new Array(n).fill(0); + for (let i = n - 1; i >= 0; i--) { + const d = digit(nums[i], exp); + const j = counter[d] - 1; // d の配列内インデックス j を取得する + res[j] = nums[i]; // 現在の要素をインデックス j に格納する + counter[d]--; // d の個数を 1 減らす + } + // 結果で元の配列 nums を上書きする + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } - [class]{}-[func]{radixSort} + /* 基数ソート */ + function radixSort(nums: number[]): void { + // 最大桁数の判定用に配列の最大要素を取得 + let m: number = Math.max(... nums); + // 下位桁から上位桁の順に走査する + for (let exp = 1; exp <= m; exp *= 10) { + // 配列要素の k 桁目に対して計数ソートを行う + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // つまり exp = 10^(k-1) + countingSortDigit(nums, exp); + } + } ``` === "Dart" ```dart title="radix_sort.dart" - [class]{}-[func]{digit} + /* 要素 `_num` の第 k 桁を取得する。ここで `exp = 10^(k-1)` */ + int digit(int _num, int exp) { + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す + return (_num ~/ exp) % 10; + } - [class]{}-[func]{countingSortDigit} + /* 計数ソート(nums の k 桁目でソート) */ + void countingSortDigit(List nums, int exp) { + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 + List counter = List.filled(10, 0); + int n = nums.length; + // 0~9 の各数字の出現回数を集計する + for (int i = 0; i < n; i++) { + int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする + counter[d]++; // 数字 d の出現回数を数える + } + // 累積和を求め、「出現回数」を「配列インデックス」に変換する + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する + List res = List.filled(n, 0); + for (int i = n - 1; i >= 0; i--) { + int d = digit(nums[i], exp); + int j = counter[d] - 1; // d の配列内インデックス j を取得する + res[j] = nums[i]; // 現在の要素をインデックス j に格納する + counter[d]--; // d の個数を 1 減らす + } + // 結果で元の配列 nums を上書きする + for (int i = 0; i < n; i++) nums[i] = res[i]; + } - [class]{}-[func]{radixSort} + /* 基数ソート */ + void radixSort(List nums) { + // 最大桁数の判定用に配列の最大要素を取得する + // dart の `int` の長さは 64 ビット + int m = -1 << 63; + for (int _num in nums) if (_num > m) m = _num; + // 下位桁から上位桁の順に走査する + for (int exp = 1; exp <= m; exp *= 10) + // 配列要素の k 桁目に対して計数ソートを行う + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // つまり exp = 10^(k-1) + countingSortDigit(nums, exp); + } ``` === "Rust" ```rust title="radix_sort.rs" - [class]{}-[func]{digit} + /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */ + fn digit(num: i32, exp: i32) -> usize { + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す + return ((num / exp) % 10) as usize; + } - [class]{}-[func]{counting_sort_digit} + /* 計数ソート(nums の k 桁目でソート) */ + fn counting_sort_digit(nums: &mut [i32], exp: i32) { + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 + let mut counter = [0; 10]; + let n = nums.len(); + // 0~9 の各数字の出現回数を集計する + for i in 0..n { + let d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする + counter[d] += 1; // 数字 d の出現回数を数える + } + // 累積和を求め、「出現回数」を「配列インデックス」に変換する + for i in 1..10 { + counter[i] += counter[i - 1]; + } + // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する + let mut res = vec![0; n]; + for i in (0..n).rev() { + let d = digit(nums[i], exp); + let j = counter[d] - 1; // d の配列内インデックス j を取得する + res[j] = nums[i]; // 現在の要素をインデックス j に格納する + counter[d] -= 1; // d の個数を 1 減らす + } + // 結果で元の配列 nums を上書きする + nums.copy_from_slice(&res); + } - [class]{}-[func]{radix_sort} + /* 基数ソート */ + fn radix_sort(nums: &mut [i32]) { + // 最大桁数の判定用に配列の最大要素を取得 + let m = *nums.into_iter().max().unwrap(); + // 下位桁から上位桁の順に走査する + let mut exp = 1; + while exp <= m { + counting_sort_digit(nums, exp); + exp *= 10; + } + } ``` === "C" ```c title="radix_sort.c" - [class]{}-[func]{digit} + /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */ + int digit(int num, int exp) { + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す + return (num / exp) % 10; + } - [class]{}-[func]{countingSortDigit} + /* 計数ソート(nums の k 桁目でソート) */ + void countingSortDigit(int nums[], int size, int exp) { + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 + int *counter = (int *)malloc((sizeof(int) * 10)); + memset(counter, 0, sizeof(int) * 10); // 後続のメモリ解放に備えて 0 で初期化する + // 0~9 の各数字の出現回数を集計する + for (int i = 0; i < size; i++) { + // nums[i] の第 k 位を取得し、d とする + int d = digit(nums[i], exp); + // 数字 d の出現回数を数える + counter[d]++; + } + // 累積和を求め、「出現回数」を「配列インデックス」に変換する + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する + int *res = (int *)malloc(sizeof(int) * size); + for (int i = size - 1; i >= 0; i--) { + int d = digit(nums[i], exp); + int j = counter[d] - 1; // d の配列内インデックス j を取得する + res[j] = nums[i]; // 現在の要素をインデックス j に格納する + counter[d]--; // d の個数を 1 減らす + } + // 結果で元の配列 nums を上書きする + for (int i = 0; i < size; i++) { + nums[i] = res[i]; + } + // メモリを解放する + free(res); + free(counter); + } - [class]{}-[func]{radixSort} + /* 基数ソート */ + void radixSort(int nums[], int size) { + // 最大桁数の判定用に配列の最大要素を取得 + int max = INT32_MIN; + for (int i = 0; i < size; i++) { + if (nums[i] > max) { + max = nums[i]; + } + } + // 下位桁から上位桁の順に走査する + for (int exp = 1; max >= exp; exp *= 10) + // 配列要素の k 桁目に対して計数ソートを行う + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // つまり exp = 10^(k-1) + countingSortDigit(nums, size, exp); + } ``` === "Kotlin" ```kotlin title="radix_sort.kt" - [class]{}-[func]{digit} + /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */ + fun digit(num: Int, exp: Int): Int { + // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す + return (num / exp) % 10 + } - [class]{}-[func]{countingSortDigit} + /* 計数ソート(nums の k 桁目でソート) */ + fun countingSortDigit(nums: IntArray, exp: Int) { + // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 + val counter = IntArray(10) + val n = nums.size + // 0~9 の各数字の出現回数を集計する + for (i in 0.. m) m = num + var exp = 1 + // 下位桁から上位桁の順に走査する + while (exp <= m) { + // 配列要素の k 桁目に対して計数ソートを行う + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // つまり exp = 10^(k-1) + countingSortDigit(nums, exp) + exp *= 10 + } + } ``` === "Ruby" ```ruby title="radix_sort.rb" - [class]{}-[func]{digit} + ### num の第 k 桁を取得する。ここで exp = 10^(k-1) ### + def digit(num, exp) + # k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる + (num / exp) % 10 + end - [class]{}-[func]{counting_sort_digit} + ### num の第 k 桁を取得する。ここで exp = 10^(k-1) ### + def digit(num, exp) + # k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる + (num / exp) % 10 + end - [class]{}-[func]{radix_sort} + # ## 計数ソート(nums の k 桁目でソート)### + def counting_sort_digit(nums, exp) + # 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要 + counter = Array.new(10, 0) + n = nums.length + # 0~9 の各数字の出現回数を集計する + for i in 0...n + d = digit(nums[i], exp) # nums[i] の第 k 位を取得し、d とする + counter[d] += 1 # 数字 d の出現回数を数える + end + # 累積和を求め、「出現回数」を「配列インデックス」に変換する + (1...10).each { |i| counter[i] += counter[i - 1] } + # 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する + res = Array.new(n, 0) + for i in (n - 1).downto(0) + d = digit(nums[i], exp) + j = counter[d] - 1 # d の配列内インデックス j を取得する + res[j] = nums[i] # 現在の要素をインデックス j に格納する + counter[d] -= 1 # d の個数を 1 減らす + end + # 結果で元の配列 nums を上書きする + (0...n).each { |i| nums[i] = res[i] } + end + + ### 基数ソート ### + def radix_sort(nums) + # 最大桁数の判定用に配列の最大要素を取得 + m = nums.max + # 下位桁から上位桁の順に走査する + exp = 1 + while exp <= m + # 配列要素の k 桁目に対して計数ソートを行う + # k = 1 -> exp = 1 + # k = 2 -> exp = 10 + # つまり exp = 10^(k-1) + counting_sort_digit(nums, exp) + exp *= 10 + end + end ``` -!!! question "なぜ最下位桁から開始するのか?" +??? pythontutor "コードの可視化" - 連続するソートラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。例えば、最初のラウンドの結果が $a < b$ で、2番目のラウンドが $a > b$ の場合、2番目のラウンドの結果が最初のラウンドの結果を置き換えます。上位桁は下位桁より優先されるため、上位桁の前に下位桁をソートすることが理にかなっています。 +
+ + +!!! question "なぜ最下位桁からソートするのですか?" + + 連続するソートの各ラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。たとえば、第1ラウンドで $a < b$ となっていても、第2ラウンドで $a > b$ となれば、第2ラウンドの結果が優先されます。数字では高位の優先度が低位より高いため、先に低位をソートし、その後で高位をソートする必要があります。 ## 11.10.2   アルゴリズムの特徴 -計数ソートと比較して、基数ソートはより大きな数値範囲に適していますが、**データが固定桁数で表現でき、桁数があまり大きくないことを前提としています**。例えば、浮動小数点数は桁数 $k$ が大きい可能性があり、時間計算量 $O(nk) \gg O(n^2)$ につながる可能性があるため、基数ソートには適していません。 +計数ソートと比べると、基数ソートは値の範囲が大きい場合に適しています。**ただし、データが固定桁数の形式で表せること、かつ桁数が大きすぎないことが前提です**。たとえば、浮動小数点数は基数ソートに適していません。桁数 $k$ が大きすぎて、時間計算量が $O(nk) \gg O(n^2)$ になる可能性があるためです。 -- **時間計算量は $O(nk)$、非適応ソート**:データサイズを $n$、データが基数 $d$、最大桁数を $k$ とすると、単一桁のソートには $O(n + d)$ 時間がかかり、すべての $k$ 桁のソートには $O((n + d)k)$ 時間がかかります。一般的に、$d$ と $k$ はどちらも比較的小さく、時間計算量は $O(n)$ に近づきます。 -- **空間計算量は $O(n + d)$、非インプレースソート**:計数ソートと同様に、基数ソートは長さ $n$ と $d$ の配列 `res` と `counter` にそれぞれ依存します。 -- **安定ソート**:計数ソートが安定な場合、基数ソートも安定です。計数ソートが不安定な場合、基数ソートは正しいソート順序を保証できません。 +- **時間計算量は $O(nk)$、非適応ソート**:データ量を $n$、データが $d$ 進数、最大桁数を $k$ とすると、ある1桁に対して計数ソートを実行する時間は $O(n + d)$ であり、全 $k$ 桁をソートする時間は $O((n + d)k)$ です。通常、$d$ と $k$ はどちらも比較的小さいため、時間計算量は $O(n)$ に近づきます。 +- **空間計算量は $O(n + d)$、非原地ソート**:計数ソートと同様に、基数ソートでは長さ $n$ と $d$ の配列 `res` と `counter` を補助的に用います。 +- **安定ソート**:計数ソートが安定であれば基数ソートも安定です。計数ソートが不安定な場合、基数ソートでは正しいソート結果を保証できません。 diff --git a/ja/docs/chapter_sorting/selection_sort.md b/ja/docs/chapter_sorting/selection_sort.md index 198a42e95..c3b89d346 100644 --- a/ja/docs/chapter_sorting/selection_sort.md +++ b/ja/docs/chapter_sorting/selection_sort.md @@ -4,18 +4,18 @@ comments: true # 11.2   選択ソート -選択ソートは非常にシンプルな原理で動作します:各反復で未ソート区間から最小要素を選択し、ソート済みセクションの末尾に移動するループを使用します。 +選択ソート(selection sort)の仕組みは非常に単純です。ループを開始し、各ラウンドで未ソート区間から最小の要素を選び、整列済み区間の末尾に配置します。 -配列の長さを$n$とすると、選択ソートのステップは下図に示されます。 +配列の長さを $n$ とすると、選択ソートの手順は次の図のようになります。 -1. 最初に、すべての要素は未ソートで、つまり未ソート(インデックス)区間は$[0, n-1]$です。 -2. 区間$[0, n-1]$の最小要素を選択し、インデックス$0$の要素と交換します。この後、配列の最初の要素がソートされます。 -3. 区間$[1, n-1]$の最小要素を選択し、インデックス$1$の要素と交換します。この後、配列の最初の2つの要素がソートされます。 -4. この方法で続行します。$n - 1$ラウンドの選択と交換の後、最初の$n - 1$個の要素がソートされます。 -5. 残りの唯一の要素は結果的に最大要素であり、ソートする必要がないため、配列はソートされます。 +1. 初期状態では、すべての要素が未ソートであり、未ソートな(インデックス)区間は $[0, n-1]$ です。 +2. 区間 $[0, n-1]$ 内の最小要素を選び、インデックス $0$ の要素と交換します。これにより、配列の先頭 1 要素が整列済みになります。 +3. 区間 $[1, n-1]$ 内の最小要素を選び、インデックス $1$ の要素と交換します。これにより、配列の先頭 2 要素が整列済みになります。 +4. これを繰り返します。$n - 1$ 回の選択と交換を経ると、配列の先頭 $n - 1$ 要素が整列済みになります。 +5. 残った 1 つの要素は必ず最大要素なので、ソートは不要です。これで配列のソートは完了します。 === "<1>" - ![Selection sort process](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" } + ![選択ソートの手順](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" } === "<2>" ![selection_sort_step2](selection_sort.assets/selection_sort_step2.png){ class="animation-figure" } @@ -47,9 +47,9 @@ comments: true === "<11>" ![selection_sort_step11](selection_sort.assets/selection_sort_step11.png){ class="animation-figure" } -

図 11-2   Selection sort process

+

図 11-2   選択ソートの手順

-コードでは、$k$を使用して未ソート区間内の最小要素を記録します: +コードでは、$k$ を用いて未ソート区間内の最小要素を記録します。 === "Python" @@ -57,14 +57,14 @@ comments: true def selection_sort(nums: list[int]): """選択ソート""" n = len(nums) - # 外側のループ:未ソート範囲は [i, n-1] + # 外側ループ:未整列区間は [i, n-1] for i in range(n - 1): - # 内側のループ:未ソート範囲内で最小要素を見つける + # 内側のループ:未ソート区間の最小要素を見つける k = i for j in range(i + 1, n): if nums[j] < nums[k]: k = j # 最小要素のインデックスを記録 - # 最小要素を未ソート範囲の先頭要素と交換 + # その最小要素を未整列区間の先頭要素と交換する nums[i], nums[k] = nums[k], nums[i] ``` @@ -74,15 +74,15 @@ comments: true /* 選択ソート */ void selectionSort(vector &nums) { int n = nums.size(); - // 外側ループ:未ソート範囲は[i, n-1] + // 外側ループ:未整列区間は [i, n-1] for (int i = 0; i < n - 1; i++) { - // 内側ループ:未ソート範囲内で最小要素を見つける + // 内側のループ:未ソート区間の最小要素を見つける int k = i; for (int j = i + 1; j < n; j++) { if (nums[j] < nums[k]) k = j; // 最小要素のインデックスを記録 } - // 最小要素を未ソート範囲の最初の要素と交換 + // その最小要素を未整列区間の先頭要素と交換する swap(nums[i], nums[k]); } } @@ -94,15 +94,15 @@ comments: true /* 選択ソート */ void selectionSort(int[] nums) { int n = nums.length; - // 外側ループ: 未ソート範囲は [i, n-1] + // 外側ループ:未整列区間は [i, n-1] for (int i = 0; i < n - 1; i++) { - // 内側ループ: 未ソート範囲内で最小要素を見つける + // 内側のループ:未ソート区間の最小要素を見つける int k = i; for (int j = i + 1; j < n; j++) { if (nums[j] < nums[k]) k = j; // 最小要素のインデックスを記録 } - // 最小要素と未ソート範囲の最初の要素を交換 + // その最小要素を未整列区間の先頭要素と交換する int temp = nums[i]; nums[i] = nums[k]; nums[k] = temp; @@ -113,69 +113,228 @@ comments: true === "C#" ```csharp title="selection_sort.cs" - [class]{selection_sort}-[func]{SelectionSort} + /* 選択ソート */ + void SelectionSort(int[] nums) { + int n = nums.Length; + // 外側ループ:未整列区間は [i, n-1] + for (int i = 0; i < n - 1; i++) { + // 内側のループ:未ソート区間の最小要素を見つける + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) + k = j; // 最小要素のインデックスを記録 + } + // その最小要素を未整列区間の先頭要素と交換する + (nums[k], nums[i]) = (nums[i], nums[k]); + } + } ``` === "Go" ```go title="selection_sort.go" - [class]{}-[func]{selectionSort} + /* 選択ソート */ + func selectionSort(nums []int) { + n := len(nums) + // 外側ループ:未整列区間は [i, n-1] + for i := 0; i < n-1; i++ { + // 内側のループ:未ソート区間の最小要素を見つける + k := i + for j := i + 1; j < n; j++ { + if nums[j] < nums[k] { + // 最小要素のインデックスを記録 + k = j + } + } + // その最小要素を未整列区間の先頭要素と交換する + nums[i], nums[k] = nums[k], nums[i] + + } + } ``` === "Swift" ```swift title="selection_sort.swift" - [class]{}-[func]{selectionSort} + /* 選択ソート */ + func selectionSort(nums: inout [Int]) { + // 外側ループ:未整列区間は [i, n-1] + for i in nums.indices.dropLast() { + // 内側のループ:未ソート区間の最小要素を見つける + var k = i + for j in nums.indices.dropFirst(i + 1) { + if nums[j] < nums[k] { + k = j // 最小要素のインデックスを記録 + } + } + // その最小要素を未整列区間の先頭要素と交換する + nums.swapAt(i, k) + } + } ``` === "JS" ```javascript title="selection_sort.js" - [class]{}-[func]{selectionSort} + /* 選択ソート */ + function selectionSort(nums) { + let n = nums.length; + // 外側ループ:未整列区間は [i, n-1] + for (let i = 0; i < n - 1; i++) { + // 内側のループ:未ソート区間の最小要素を見つける + let k = i; + for (let j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) { + k = j; // 最小要素のインデックスを記録 + } + } + // その最小要素を未整列区間の先頭要素と交換する + [nums[i], nums[k]] = [nums[k], nums[i]]; + } + } ``` === "TS" ```typescript title="selection_sort.ts" - [class]{}-[func]{selectionSort} + /* 選択ソート */ + function selectionSort(nums: number[]): void { + let n = nums.length; + // 外側ループ:未整列区間は [i, n-1] + for (let i = 0; i < n - 1; i++) { + // 内側のループ:未ソート区間の最小要素を見つける + let k = i; + for (let j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) { + k = j; // 最小要素のインデックスを記録 + } + } + // その最小要素を未整列区間の先頭要素と交換する + [nums[i], nums[k]] = [nums[k], nums[i]]; + } + } ``` === "Dart" ```dart title="selection_sort.dart" - [class]{}-[func]{selectionSort} + /* 選択ソート */ + void selectionSort(List nums) { + int n = nums.length; + // 外側ループ:未整列区間は [i, n-1] + for (int i = 0; i < n - 1; i++) { + // 内側のループ:未ソート区間の最小要素を見つける + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) k = j; // 最小要素のインデックスを記録 + } + // その最小要素を未整列区間の先頭要素と交換する + int temp = nums[i]; + nums[i] = nums[k]; + nums[k] = temp; + } + } ``` === "Rust" ```rust title="selection_sort.rs" - [class]{}-[func]{selection_sort} + /* 選択ソート */ + fn selection_sort(nums: &mut [i32]) { + if nums.is_empty() { + return; + } + let n = nums.len(); + // 外側ループ:未整列区間は [i, n-1] + for i in 0..n - 1 { + // 内側のループ:未ソート区間の最小要素を見つける + let mut k = i; + for j in i + 1..n { + if nums[j] < nums[k] { + k = j; // 最小要素のインデックスを記録 + } + } + // その最小要素を未整列区間の先頭要素と交換する + nums.swap(i, k); + } + } ``` === "C" ```c title="selection_sort.c" - [class]{}-[func]{selectionSort} + /* 選択ソート */ + void selectionSort(int nums[], int n) { + // 外側ループ:未整列区間は [i, n-1] + for (int i = 0; i < n - 1; i++) { + // 内側のループ:未ソート区間の最小要素を見つける + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) + k = j; // 最小要素のインデックスを記録 + } + // その最小要素を未整列区間の先頭要素と交換する + int temp = nums[i]; + nums[i] = nums[k]; + nums[k] = temp; + } + } ``` === "Kotlin" ```kotlin title="selection_sort.kt" - [class]{}-[func]{selectionSort} + /* 選択ソート */ + fun selectionSort(nums: IntArray) { + val n = nums.size + // 外側ループ:未整列区間は [i, n-1] + for (i in 0.. + -![Selection sort instability example](selection_sort.assets/selection_sort_instability.png){ class="animation-figure" } +## 11.2.1   アルゴリズムの特徴 -

図 11-3   Selection sort instability example

+- **時間計算量は $O(n^2)$、非適応ソート**:外側のループは合計 $n - 1$ 回です。最初のラウンドの未ソート区間の長さは $n$、最後のラウンドでは $2$ であり、各ラウンドの内側のループ回数はそれぞれ $n$、$n - 1$、$\dots$、$3$、$2$ となります。総和は $\frac{(n - 1)(n + 2)}{2}$ です。 +- **空間計算量は $O(1)$、インプレースソート**:ポインタ $i$ と $j$ は定数サイズの追加領域しか使用しません。 +- **不安定ソート**:次の図のように、要素 `nums[i]` がそれと等しい要素の右側へ交換され、両者の相対的な順序が変わる可能性があります。 + +![選択ソートの不安定な例](selection_sort.assets/selection_sort_instability.png){ class="animation-figure" } + +

図 11-3   選択ソートの不安定な例

diff --git a/ja/docs/chapter_sorting/sorting_algorithm.md b/ja/docs/chapter_sorting/sorting_algorithm.md index ee10312a5..eeb0ff6e1 100644 --- a/ja/docs/chapter_sorting/sorting_algorithm.md +++ b/ja/docs/chapter_sorting/sorting_algorithm.md @@ -4,36 +4,36 @@ comments: true # 11.1   ソートアルゴリズム -ソートアルゴリズムは、データセットを特定の順序で配列するために使用されます。ソートアルゴリズムは、順序付けられたデータは通常、より効率的に探索、分析、処理できるため、幅広い応用があります。 +ソートアルゴリズム(sorting algorithm)は、データの集合を特定の順序に従って並べ替えるために用いられます。ソートアルゴリズムは幅広く応用されており、整列済みデータは通常、より効率的に検索、分析、処理できるためです。 -下図に示すように、ソートアルゴリズムのデータ型は整数、浮動小数点数、文字、文字列などです。ソート基準は、数値サイズ、文字ASCII順序、またはカスタム基準など、必要に応じて設定できます。 +下図に示すように、ソートアルゴリズムにおけるデータ型は整数、浮動小数点数、文字、文字列などです。ソートの判定規則は、数値の大小、文字の ASCII コード順、またはカスタムルールなど、要件に応じて設定できます。 -![Data types and comparator examples](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" } +![データ型と判定規則の例](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" } -

図 11-1   Data types and comparator examples

+

図 11-1   データ型と判定規則の例

-## 11.1.1   評価次元 +## 11.1.1   評価軸 -**実行効率**:ソートアルゴリズムの時間計算量ができるだけ低いことを期待し、全体的な操作数も少ないこと(時間計算量の定数項を下げる)を望みます。大容量データでは、実行効率が特に重要です。 +**実行効率**:ソートアルゴリズムの時間計算量はできるだけ低く、かつ全体の操作回数も少ないこと(時間計算量における定数項が小さいこと)が望まれます。大量データの場合、実行効率はとりわけ重要です。 -**インプレース性**:名前が示すとおり、インプレースソートは元の配列を直接操作することで実現され、追加のヘルパー配列が不要であるため、メモリを節約します。一般的に、インプレースソートはデータ移動操作が少なく、高速です。 +**インプレース性**:その名のとおり、インプレースソートは元の配列を直接操作して並べ替えを行うため、追加の補助配列を必要とせず、メモリを節約できます。通常、インプレースソートはデータの移動操作が少なく、実行速度もより高速です。 -**安定性**:安定ソートは、ソート後に配列内の等しい要素の相対順序が変わらないことを保証します。 +**安定性**:安定ソートは、並べ替え完了後も、等しい要素の配列内での相対順序が変化しません。 -安定ソートは、マルチキーソートシナリオにおいて必要条件です。学生情報を格納するテーブルがあり、第1列と第2列がそれぞれ名前と年齢であるとします。この場合、不安定ソートは入力データの順序を失う可能性があります: +安定ソートは多段ソートの場面で必要条件となります。学生情報を保存した表があり、第 1 列と第 2 列がそれぞれ氏名と年齢であると仮定します。この場合、不安定ソートによって入力データの順序性が失われる可能性があります。 ```shell -# 入力データは名前でソート済み -# (名前, 年齢) +# 入力データは氏名順にソートされている +# (name, age) ('A', 19) ('B', 18) ('C', 21) ('D', 19) ('E', 23) -# 不安定ソートアルゴリズムを使用してリストを年齢でソートすると仮定すると、 -# 結果は('D', 19)と('A', 19)の相対位置を変更し、 -# 入力データが名前でソート済みであるという性質が失われる +# 不安定ソートアルゴリズムで年齢順にリストを並べ替えると仮定すると、 +# 結果では ('D', 19) と ('A', 19) の相対位置が変わり、 +# 入力データが氏名順である性質が失われる ('B', 18) ('D', 19) ('A', 19) @@ -41,12 +41,12 @@ comments: true ('E', 23) ``` -**適応性**:適応ソートは入力データ内の既存の順序情報を活用して計算負荷を削減し、より最適な時間効率を実現します。適応ソートアルゴリズムの最良ケース時間計算量は、通常平均ケース時間計算量よりも優れています。 +**適応性**:適応的ソートは、入力データに既に存在する順序情報を利用して計算量を減らし、より優れた時間効率を実現できます。適応的ソートアルゴリズムの最良時間計算量は、通常、平均時間計算量より優れています。 -**比較ベースまたは非比較ベース**:比較ベースソートは比較演算子($<$、$=$、$>$)に依存して要素の相対順序を決定し、配列全体をソートします。理論的最適時間計算量は$O(n \log n)$です。一方、非比較ソートは比較演算子を使用せず、$O(n)$の時間計算量を実現できますが、汎用性は比較的劣ります。 +**比較ベースかどうか**:比較ベースのソートは、比較演算子($<$、$=$、$>$)に依存して要素の相対順序を判定し、それによって配列全体をソートします。理論上の最良時間計算量は $O(n \log n)$ です。一方、非比較ソートは比較演算子を使用せず、時間計算量は $O(n)$ に達しますが、汎用性は相対的に低くなります。 ## 11.1.2   理想的なソートアルゴリズム -**高速実行、インプレース、安定、適応、汎用**。明らかに、これらのすべての特徴を組み合わせたソートアルゴリズムは今日まで見つかっていません。したがって、ソートアルゴリズムを選択する際は、データの特定の特徴と問題の要件に基づいて決定する必要があります。 +**高速、インプレース、安定、適応的、高い汎用性**。明らかに、これまでのところ、以上のすべての特性を兼ね備えたソートアルゴリズムはまだ見つかっていません。そのため、ソートアルゴリズムを選択する際には、具体的なデータの特徴と問題の要件に応じて判断する必要があります。 -次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価次元に基づいてそれぞれの利点と欠点を分析します。 +次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価軸に基づいて各ソートアルゴリズムの長所と短所を分析していきます。 diff --git a/ja/docs/chapter_sorting/summary.md b/ja/docs/chapter_sorting/summary.md index afaf7a5aa..0cfb832f2 100644 --- a/ja/docs/chapter_sorting/summary.md +++ b/ja/docs/chapter_sorting/summary.md @@ -4,17 +4,17 @@ comments: true # 11.11   まとめ -### 1.   重要な復習 +### 1.   重要なポイントの振り返り -- バブルソートは隣接する要素を交換することで動作します。フラグを追加して早期リターンを可能にすることで、バブルソートの最良ケースの時間計算量を $O(n)$ に最適化できます。 -- 挿入ソートは、未ソート区間から要素を取り出してソート済み区間の正しい位置に挿入することで各ラウンドをソートします。挿入ソートの時間計算量は $O(n^2)$ ですが、単位あたりの操作が比較的少ないため、少量のデータのソートでは非常に人気があります。 -- クイックソートは歩哨分割操作に基づいています。歩哨分割では、常に最悪のピボットを選ぶ可能性があり、時間計算量が $O(n^2)$ に劣化する可能性があります。中央値やランダムピボットを導入することで、そのような劣化の確率を減らすことができます。末尾再帰は再帰の深さを効果的に減らし、空間計算量を $O(\log n)$ に最適化します。 -- マージソートには分割とマージの2つの段階があり、通常分割統治戦略を体現しています。マージソートでは、配列のソートには補助配列の作成が必要で、空間計算量は $O(n)$ になります。しかし、リストのソートの空間計算量は $O(1)$ に最適化できます。 -- バケットソートは3つの手順から構成されます:データをバケットに分散、各バケット内でのソート、バケット順での結果のマージ。これも分割統治戦略を体現し、非常に大きなデータセットに適しています。バケットソートの鍵はデータの均等分散です。 -- 計数ソートはバケットソートの変形で、各データポイントの出現回数をカウントすることでソートします。計数ソートは限られた範囲のデータを持つ大きなデータセットに適しており、データを正の整数に変換する必要があります。 -- 基数ソートは桁ごとにソートすることでデータを処理し、データが固定長の数値として表現される必要があります。 -- 全体的に、私たちは高効率、安定性、インプレース操作、適応性を持つソートアルゴリズムを求めています。しかし、他のデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たすソートアルゴリズムは存在しません。実際の応用では、データの特性に基づいて適切なソートアルゴリズムを選択する必要があります。 -- 以下の図は、効率性、安定性、インプレース性、適応性の観点から主流のソートアルゴリズムを比較しています。 +- バブルソートは隣接する要素を交換することで整列を行います。フラグを追加して早期リターンを可能にすると、バブルソートの最良時間計算量を $O(n)$ に最適化できます。 +- 挿入ソートは各ラウンドで未整列区間の要素を整列済み区間の正しい位置に挿入することで整列を完了します。挿入ソートの時間計算量は $O(n^2)$ ですが、基本操作が比較的少ないため、小規模データのソート処理で非常に人気があります。 +- クイックソートは番兵分割操作に基づいて整列を行います。番兵分割では毎回最悪の基準値を選んでしまう可能性があり、その結果、時間計算量は $O(n^2)$ まで劣化することがあります。中央値の基準値やランダムな基準値を導入すると、この劣化の確率を下げられます。短い部分配列を優先して再帰すれば、再帰の深さを効果的に抑え、空間計算量を $O(\log n)$ に最適化できます。 +- マージソートは分割とマージという 2 つの段階からなり、分割統治戦略を典型的に体現しています。マージソートでは配列を整列する際に補助配列の作成が必要で、空間計算量は $O(n)$ です。一方、連結リストを整列する場合の空間計算量は $O(1)$ まで最適化できます。 +- バケットソートはデータのバケット分配、バケット内ソート、結果の結合という 3 つの手順を含みます。これも分割統治戦略を体現しており、データ量が非常に大きい場合に適しています。バケットソートの鍵は、データを平均的に分配することにあります。 +- カウントソートはバケットソートの特例であり、データの出現回数を数えることで整列を行います。カウントソートはデータ量が大きく、かつデータ範囲が限られている場合に適しており、データを正の整数に変換できることが前提です。 +- 基数ソートは各桁ごとの整列によってデータを整列し、データが固定桁数の数値として表せることを前提とします。 +- 総じて言えば、私たちは高効率で、安定で、インプレースで、さらに適応的であるといった利点を備えたソートアルゴリズムを見つけたいと考えます。しかし、ほかのデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たせるソートアルゴリズムは存在しません。実際の応用では、データの特性に応じて適切なソートアルゴリズムを選ぶ必要があります。 +- 下図では、主流のソートアルゴリズムについて、効率、安定性、インプレース性、適応性などを比較しています。 ![ソートアルゴリズムの比較](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" } @@ -22,32 +22,32 @@ comments: true ### 2.   Q & A -**Q**: ソートアルゴリズムの安定性はいつ必要ですか? +**Q**:ソートアルゴリズムの安定性は、どのような場合に必須ですか? -実際には、オブジェクトの一つの属性に基づいてソートする場合があります。例えば、学生は名前と身長の属性を持ち、多段階ソートを実装することを目指します:最初に名前で `(A, 180) (B, 185) (C, 170) (D, 170)` を取得し、次に身長で。ソートアルゴリズムが不安定なため、`(D, 170) (C, 170) (A, 180) (B, 185)` になってしまう可能性があります。 +現実には、オブジェクトのある属性に基づいて整列することがあります。たとえば、学生には氏名と身長という 2 つの属性があり、多段階のソートを行いたいとします。まず氏名で整列して `(A, 180) (B, 185) (C, 170) (D, 170)` を得て、その後に身長で整列します。ソートアルゴリズムが不安定である場合、結果は `(D, 170) (C, 170) (A, 180) (B, 185)` になる可能性があります。 -学生DとCの位置が交換され、名前の順序性が破られているのが分かります。これは望ましくありません。 +このように、学生 D と C の位置が入れ替わり、氏名に関する順序性が壊れてしまいます。これは望ましくありません。 -**Q**: 歩哨分割での「右から左への検索」と「左から右への検索」の順序を交換できますか? +**Q**:番兵分割において、「右から左へ探索する」順序と「左から右へ探索する」順序は入れ替えられますか? -いいえ、最左要素をピボットとして使用する場合、最初に「右から左への検索」を行い、次に「左から右への検索」を行う必要があります。この結論はやや直観に反するので、理由を分析してみましょう。 +できません。最も左端の要素を基準値とする場合は、必ず先に「右から左へ探索する」を行い、その後に「左から右へ探索する」を行う必要があります。この結論はやや直感に反するので、理由を分析してみましょう。 -歩哨分割 `partition()` の最後のステップは `nums[left]` と `nums[i]` を交換することです。交換後、ピボットの左側の要素はすべてピボット以下になります。**これには最後の交換前に `nums[left] >= nums[i]` が成り立つ必要があります**。「左から右への検索」を最初に行い、ピボットより大きい要素が見つからない場合、**`i == j` でループを終了し、`nums[j] == nums[i] > nums[left]` となる可能性があります**。つまり、最終交換操作はピボットより大きい要素を配列の左端に交換し、歩哨分割を失敗させます。 +番兵分割 `partition()` の最後の手順は、`nums[left]` と `nums[i]` を交換することです。交換が終わると、基準値の左側にある要素はすべて基準値 `<=` になります。**したがって、最後の交換の前に `nums[left] >= nums[i]` が必ず成り立っていなければなりません**。仮に先に「左から右へ探索する」を行うと、基準値より大きい要素が見つからない場合、**`i == j` の時点でループを抜け、このとき `nums[j] == nums[i] > nums[left]` となる可能性があります**。つまり、この最後の交換によって、基準値より大きい要素が配列の最左端へ移されてしまい、番兵分割は失敗します。 -例えば、配列 `[0, 0, 0, 0, 1]` が与えられた場合、最初に「左から右への検索」を行うと、歩哨分割後の配列は `[1, 0, 0, 0, 0]` となり、これは正しくありません。 +たとえば、配列 `[0, 0, 0, 0, 1]` が与えられたとき、先に「左から右へ探索する」を行うと、番兵分割後の配列は `[1, 0, 0, 0, 0]` になります。これは誤った結果です。 -さらに考えると、`nums[right]` をピボットとして選択する場合、まったく逆で、最初に「左から右への検索」を行う必要があります。 +さらに考えると、`nums[right]` を基準値に選ぶ場合はちょうど逆になり、必ず先に「左から右へ探索する」を行う必要があります。 -**Q**: 末尾再帰最適化について、短い配列を選択することで再帰の深さが $\log n$ を超えないことを保証するのはなぜですか? +**Q**:クイックソートの再帰深度最適化について、短い配列を選ぶとなぜ再帰深度が $\log n$ を超えないと保証できるのですか? -再帰の深さは現在リターンしていない再帰メソッドの数です。歩哨分割の各ラウンドは元の配列を2つの副配列に分割します。末尾再帰最適化により、再帰的に続行する副配列の長さは最大でも元の配列長の半分です。最悪の場合常に長さを半分にすると仮定すると、最終的な再帰の深さは $\log n$ になります。 +再帰深度とは、現在まだ戻っていない再帰呼び出しの数のことです。各ラウンドの番兵分割では、元の配列を 2 つの部分配列に分けます。再帰深度の最適化後は、下方向に再帰する部分配列の長さは最大でも元の配列長の半分です。最悪の場合でも毎回半分の長さになると仮定すれば、最終的な再帰深度は $\log n$ になります。 -元のクイックソートを見直すと、より大きな配列を継続的に再帰処理する可能性があり、最悪の場合 $n$、$n - 1$、...、$2$、$1$ で、再帰の深さは $n$ になります。末尾再帰最適化はこのシナリオを回避できます。 +元のクイックソートを振り返ると、長いほうの配列に対して連続して再帰してしまう可能性があり、最悪の場合は $n$、$n - 1$、$\dots$、$2$、$1$ と続き、再帰深度は $n$ になります。再帰深度の最適化により、このような状況を避けられます。 -**Q**: 配列のすべての要素が等しい場合、クイックソートの時間計算量は $O(n^2)$ ですか?この劣化ケースをどう処理すべきですか? +**Q**:配列内のすべての要素が等しい場合、クイックソートの時間計算量は $O(n^2)$ になりますか?このような退化はどう処理すべきですか? -はい。この状況については、歩哨分割を使用して配列をピボットより小さい、等しい、大きいの3つの部分に分割することを検討してください。小さい部分と大きい部分のみを再帰的に進めます。この方法では、すべての入力要素が等しい配列を1ラウンドの歩哨分割だけでソートできます。 +はい。この場合は、番兵分割によって配列を「基準値より小さい」「基準値に等しい」「基準値より大きい」の 3 つの部分に分ける方法を検討できます。下方向に再帰するのは、小さい部分と大きい部分だけです。この方法では、入力要素がすべて等しい配列は、1 回の番兵分割だけで整列を完了できます。 -**Q**: なぜバケットソートの最悪ケース時間計算量は $O(n^2)$ ですか? +**Q**:バケットソートの最悪時間計算量が $O(n^2)$ なのはなぜですか? -最悪の場合、すべての要素が同じバケットに配置されます。これらの要素をソートするために $O(n^2)$ アルゴリズムを使用する場合、時間計算量は $O(n^2)$ になります。 +最悪の場合、すべての要素が同じバケットに振り分けられます。その要素群を整列するのに $O(n^2)$ のアルゴリズムを使えば、時間計算量は $O(n^2)$ になります。 diff --git a/ja/docs/chapter_stack_and_queue/deque.md b/ja/docs/chapter_stack_and_queue/deque.md index c7919f1b0..190be6c2e 100644 --- a/ja/docs/chapter_stack_and_queue/deque.md +++ b/ja/docs/chapter_stack_and_queue/deque.md @@ -4,32 +4,32 @@ comments: true # 5.3   両端キュー -キューでは、先頭からの要素の削除や末尾への要素の追加のみが可能です。下図に示すように、両端キュー(deque)はより柔軟性を提供し、先頭と末尾の両方で要素の追加や削除を可能にします。 +キューでは、先頭要素を削除するか末尾に要素を追加することしかできません。次の図に示すように、両端キュー(double-ended queue)はより高い柔軟性を備えており、先頭と末尾の両方で要素の追加や削除を行えます。 ![両端キューの操作](deque.assets/deque_operations.png){ class="animation-figure" }

図 5-7   両端キューの操作

-## 5.3.1   両端キューの一般的な操作 +## 5.3.1   両端キューの基本操作 -両端キューの一般的な操作は以下の通りです。具体的なメソッド名は使用するプログラミング言語によって異なります。 +両端キューの基本操作を次の表に示します。具体的なメソッド名は、使用するプログラミング言語によって異なります。

表 5-3   両端キューの操作効率

-| メソッド名 | 説明 | 時間計算量 | -| ------------- | ------------------ | ------------- | -| `pushFirst()` | 先頭に要素を追加 | $O(1)$ | -| `pushLast()` | 末尾に要素を追加 | $O(1)$ | -| `popFirst()` | 先頭要素を削除 | $O(1)$ | -| `popLast()` | 末尾要素を削除 | $O(1)$ | -| `peekFirst()` | 先頭要素にアクセス | $O(1)$ | -| `peekLast()` | 末尾要素にアクセス | $O(1)$ | +| メソッド名 | 説明 | 時間計算量 | +| -------------- | ---------------- | ---------- | +| `push_first()` | 先頭に要素を追加 | $O(1)$ | +| `push_last()` | 末尾に要素を追加 | $O(1)$ | +| `pop_first()` | 先頭要素を削除 | $O(1)$ | +| `pop_last()` | 末尾要素を削除 | $O(1)$ | +| `peek_first()` | 先頭要素にアクセス | $O(1)$ | +| `peek_last()` | 末尾要素にアクセス | $O(1)$ |
-同様に、プログラミング言語で実装された両端キュークラスを直接使用することができます: +同様に、プログラミング言語に組み込み実装されている両端キューのクラスを直接使うこともできます: === "Python" @@ -57,7 +57,7 @@ comments: true # 両端キューの長さを取得 size: int = len(deq) - # 両端キューが空かどうかを確認 + # 両端キューが空かどうかを判定 is_empty: bool = len(deq) == 0 ``` @@ -85,7 +85,7 @@ comments: true /* 両端キューの長さを取得 */ int size = deque.size(); - /* 両端キューが空かどうかを確認 */ + /* 両端キューが空かどうかを判定 */ bool empty = deque.empty(); ``` @@ -113,7 +113,7 @@ comments: true /* 両端キューの長さを取得 */ int size = deque.size(); - /* 両端キューが空かどうかを確認 */ + /* 両端キューが空かどうかを判定 */ boolean isEmpty = deque.isEmpty(); ``` @@ -121,7 +121,7 @@ comments: true ```csharp title="deque.cs" /* 両端キューを初期化 */ - // C#では、LinkedListを両端キューとして使用 + // C# では、連結リスト LinkedList を両端キューとして使用する LinkedList deque = new(); /* 要素をエンキュー */ @@ -142,7 +142,7 @@ comments: true /* 両端キューの長さを取得 */ int size = deque.Count; - /* 両端キューが空かどうかを確認 */ + /* 両端キューが空かどうかを判定 */ bool isEmpty = deque.Count == 0; ``` @@ -150,7 +150,7 @@ comments: true ```go title="deque_test.go" /* 両端キューを初期化 */ - // Goでは、listを両端キューとして使用 + // Go では、list を両端キューとして使用する deque := list.New() /* 要素をエンキュー */ @@ -171,7 +171,7 @@ comments: true /* 両端キューの長さを取得 */ size := deque.Len() - /* 両端キューが空かどうかを確認 */ + /* 両端キューが空かどうかを判定 */ isEmpty := deque.Len() == 0 ``` @@ -179,7 +179,7 @@ comments: true ```swift title="deque.swift" /* 両端キューを初期化 */ - // Swiftには組み込みの両端キュークラスがないため、Arrayを両端キューとして使用 + // Swift には組み込みの両端キュークラスがないため、Array を両端キューとして使用する var deque: [Int] = [] /* 要素をエンキュー */ @@ -191,17 +191,17 @@ comments: true /* 要素にアクセス */ let peekFirst = deque.first! // 先頭要素 - let peekLast = deque.last! // 末尾要素 + let peekLast = deque.last! // 末尾要素 /* 要素をデキュー */ - // Arrayを使用する場合、popFirstの計算量はO(n) + // Array で模擬する場合、popFirst の計算量は O(n) let popFirst = deque.removeFirst() // 先頭要素をデキュー - let popLast = deque.removeLast() // 末尾要素をデキュー + let popLast = deque.removeLast() // 末尾要素をデキュー /* 両端キューの長さを取得 */ let size = deque.count - /* 両端キューが空かどうかを確認 */ + /* 両端キューが空かどうかを判定 */ let isEmpty = deque.isEmpty ``` @@ -209,30 +209,30 @@ comments: true ```javascript title="deque.js" /* 両端キューを初期化 */ - // JavaScriptには組み込みの両端キューがないため、Arrayを両端キューとして使用 + // JavaScript には組み込みの両端キューがないため、Array を両端キューとして使用するしかない const deque = []; /* 要素をエンキュー */ deque.push(2); deque.push(5); deque.push(4); - // 注意:unshift()は配列のため時間計算量がO(n) + // 配列であるため、unshift() メソッドの時間計算量は O(n) です deque.unshift(3); deque.unshift(1); /* 要素にアクセス */ - const peekFirst = deque[0]; // 先頭要素 - const peekLast = deque[deque.length - 1]; // 末尾要素 + const peekFirst = deque[0]; + const peekLast = deque[deque.length - 1]; /* 要素をデキュー */ - // 注意:shift()は配列のため時間計算量がO(n) - const popFront = deque.shift(); // 先頭要素をデキュー - const popBack = deque.pop(); // 末尾要素をデキュー + // 配列であるため、shift() メソッドの時間計算量は O(n) です + const popFront = deque.shift(); + const popBack = deque.pop(); /* 両端キューの長さを取得 */ const size = deque.length; - /* 両端キューが空かどうかを確認 */ + /* 両端キューが空かどうかを判定 */ const isEmpty = size === 0; ``` @@ -240,30 +240,30 @@ comments: true ```typescript title="deque.ts" /* 両端キューを初期化 */ - // TypeScriptには組み込みの両端キューがないため、Arrayを両端キューとして使用 + // TypeScript には組み込みの両端キューがないため、Array を両端キューとして使用するしかない const deque: number[] = []; /* 要素をエンキュー */ deque.push(2); deque.push(5); deque.push(4); - // 注意:unshift()は配列のため時間計算量がO(n) + // 配列であるため、unshift() メソッドの時間計算量は O(n) です deque.unshift(3); deque.unshift(1); /* 要素にアクセス */ - const peekFirst: number = deque[0]; // 先頭要素 - const peekLast: number = deque[deque.length - 1]; // 末尾要素 + const peekFirst: number = deque[0]; + const peekLast: number = deque[deque.length - 1]; /* 要素をデキュー */ - // 注意:shift()は配列のため時間計算量がO(n) - const popFront: number = deque.shift() as number; // 先頭要素をデキュー - const popBack: number = deque.pop() as number; // 末尾要素をデキュー + // 配列であるため、shift() メソッドの時間計算量は O(n) です + const popFront: number = deque.shift() as number; + const popBack: number = deque.pop() as number; /* 両端キューの長さを取得 */ const size: number = deque.length; - /* 両端キューが空かどうかを確認 */ + /* 両端キューが空かどうかを判定 */ const isEmpty: boolean = size === 0; ``` @@ -271,7 +271,7 @@ comments: true ```dart title="deque.dart" /* 両端キューを初期化 */ - // Dartでは、Queueが両端キューとして定義される + // Dart では、Queue は両端キューとして定義されています Queue deque = Queue(); /* 要素をエンキュー */ @@ -292,7 +292,7 @@ comments: true /* 両端キューの長さを取得 */ int size = deque.length; - /* 両端キューが空かどうかを確認 */ + /* 両端キューが空かどうかを判定 */ bool isEmpty = deque.isEmpty; ``` @@ -324,52 +324,109 @@ comments: true /* 両端キューの長さを取得 */ let size = deque.len(); - /* 両端キューが空かどうかを確認 */ + /* 両端キューが空かどうかを判定 */ let is_empty = deque.is_empty(); ``` === "C" ```c title="deque.c" - // Cには組み込みの両端キューが提供されていません + // C には組み込みの両端キューがありません ``` === "Kotlin" ```kotlin title="deque.kt" + /* 両端キューを初期化 */ + val deque = LinkedList() + /* 要素をエンキュー */ + deque.offerLast(2) // 末尾に追加 + deque.offerLast(5) + deque.offerLast(4) + deque.offerFirst(3) // 先頭に追加 + deque.offerFirst(1) + + /* 要素にアクセス */ + val peekFirst = deque.peekFirst() // 先頭要素 + val peekLast = deque.peekLast() // 末尾要素 + + /* 要素をデキュー */ + val popFirst = deque.pollFirst() // 先頭要素をデキュー + val popLast = deque.pollLast() // 末尾要素をデキュー + + /* 両端キューの長さを取得 */ + val size = deque.size + + /* 両端キューが空かどうかを判定 */ + val isEmpty = deque.isEmpty() ``` +=== "Ruby" + + ```ruby title="deque.rb" + # 両端キューを初期化 + # Ruby には組み込みの両端キューがないため、Array を両端キューとして使用するしかありません + deque = [] + + # 要素をエンキュー + deque << 2 + deque << 5 + deque << 4 + # 配列であるため、Array#unshift メソッドの時間計算量は O(n) です + deque.unshift(3) + deque.unshift(1) + + # 要素にアクセス + peek_first = deque.first + peek_last = deque.last + + # 要素をデキュー + # 配列であるため、 Array#shift メソッドの時間計算量は O(n) です + pop_front = deque.shift + pop_back = deque.pop + + # 両端キューの長さを取得 + size = deque.length + + # 両端キューが空かどうかを判定 + is_empty = size.zero? + ``` + +??? pythontutor "実行の可視化" + + https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%0A%20%20%20%20deq%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20deq.append%282%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E5%B0%BE%0A%20%20%20%20deq.append%285%29%0A%20%20%20%20deq.append%284%29%0A%20%20%20%20deq.appendleft%283%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E9%A6%96%0A%20%20%20%20deq.appendleft%281%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20deq%5B0%5D%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%20%20%20%20rear%20%3D%20deq%5B-1%5D%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%20rear%20%3D%22,%20rear%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop_front%20%3D%20deq.popleft%28%29%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_front%20%3D%22,%20pop_front%29%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%20%20%20%20pop_rear%20%3D%20deq.pop%28%29%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_rear%20%3D%22,%20pop_rear%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28deq%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28deq%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + ## 5.3.2   両端キューの実装 * -両端キューの実装は通常のキューの実装と似ており、連結リストまたは配列を基盤となるデータ構造として使用できます。 +両端キューの実装はキューと似ており、連結リストまたは配列を基盤となるデータ構造として選べます。 ### 1.   双方向連結リストに基づく実装 -前節で、通常の単一連結リストを使ってキューを実装したことを思い出してください。これは先頭からの削除(デキュー操作に対応)と末尾への新しい要素の追加(エンキュー操作に対応)を便利に行えるためでした。 +前節を振り返ると、通常の単方向連結リストを使ってキューを実装しました。これは、先頭ノードの削除(デキューに対応)と末尾ノードの後ろへの新規ノード追加(エンキューに対応)を容易に行えるためです。 -両端キューでは、先頭と末尾の両方でエンキューとデキュー操作を実行できます。つまり、両端キューは逆方向の操作も実装する必要があります。このため、両端キューの基盤となるデータ構造として「双方向連結リスト」を使用します。 +両端キューでは、先頭と末尾のどちらでもエンキューとデキューを行えます。言い換えると、両端キューではもう一方の対称方向の操作も実装する必要があります。そのため、両端キューの基盤データ構造として「双方向連結リスト」を採用します。 -下図に示すように、双方向連結リストの先頭ノードと末尾ノードをそれぞれ両端キューの前端と後端として扱い、両端でのノードの追加と削除機能を実装します。 +次の図に示すように、双方向連結リストの先頭ノードと末尾ノードを両端キューの先頭と末尾と見なし、両端でノードを追加および削除する機能を実現します。 -=== "LinkedListDeque" - ![双方向連結リストによる両端キューのエンキューとデキュー操作の実装](deque.assets/linkedlist_deque_step1.png){ class="animation-figure" } +=== "<1>" + ![連結リストによる両端キューのエンキューとデキュー](deque.assets/linkedlist_deque_step1.png){ class="animation-figure" } -=== "pushLast()" +=== "<2>" ![linkedlist_deque_push_last](deque.assets/linkedlist_deque_step2_push_last.png){ class="animation-figure" } -=== "pushFirst()" +=== "<3>" ![linkedlist_deque_push_first](deque.assets/linkedlist_deque_step3_push_first.png){ class="animation-figure" } -=== "popLast()" +=== "<4>" ![linkedlist_deque_pop_last](deque.assets/linkedlist_deque_step4_pop_last.png){ class="animation-figure" } -=== "popFirst()" +=== "<5>" ![linkedlist_deque_pop_first](deque.assets/linkedlist_deque_step5_pop_first.png){ class="animation-figure" } -

図 5-8   双方向連結リストによる両端キューのエンキューとデキュー操作の実装

+

図 5-8   連結リストによる両端キューのエンキューとデキュー

-実装コードは以下の通りです: +実装コードは次のとおりです: === "Python" @@ -384,93 +441,93 @@ comments: true self.prev: ListNode | None = None # 前駆ノードへの参照 class LinkedListDeque: - """双方向連結リストベースの双端キュークラス""" + """双方向連結リストベースの両端キュー""" def __init__(self): """コンストラクタ""" - self._front: ListNode | None = None # ヘッドノード front - self._rear: ListNode | None = None # テールノード rear - self._size: int = 0 # 双端キューの長さ + self._front: ListNode | None = None # 先頭ノード front + self._rear: ListNode | None = None # 末尾ノード rear + self._size: int = 0 # 両端キューの長さ def size(self) -> int: - """双端キューの長さを取得""" + """両端キューの長さを取得""" return self._size def is_empty(self) -> bool: - """双端キューが空かどうかを判定""" + """両端キューが空かどうかを判定""" return self._size == 0 def push(self, num: int, is_front: bool): """エンキュー操作""" node = ListNode(num) - # リストが空の場合、front と rear の両方を node に向ける + # 連結リストが空なら、front と rear の両方を node に向ける if self.is_empty(): self._front = self._rear = node - # 前端エンキュー操作 + # 先頭へのエンキュー操作 elif is_front: - # ノードをリストの先頭に追加 + # node を連結リストの先頭に追加 self._front.prev = node node.next = self._front - self._front = node # ヘッドノードを更新 - # 後端エンキュー操作 + self._front = node # 先頭ノードを更新する + # 末尾へのエンキュー操作 else: - # ノードをリストの末尾に追加 + # node を連結リストの末尾に追加 self._rear.next = node node.prev = self._rear - self._rear = node # テールノードを更新 + self._rear = node # 末尾ノードを更新する self._size += 1 # キューの長さを更新 def push_first(self, num: int): - """前端エンキュー""" + """キュー先頭にエンキュー""" self.push(num, True) def push_last(self, num: int): - """後端エンキュー""" + """キュー末尾にエンキュー""" self.push(num, False) def pop(self, is_front: bool) -> int: """デキュー操作""" if self.is_empty(): - raise IndexError("Double-ended queue is empty") - # 前端デキュー操作 + raise IndexError("両端キューが空です") + # キュー先頭からの取り出し if is_front: - val: int = self._front.val # ヘッドノードの値を一時的に保存 - # ヘッドノードを削除 + val: int = self._front.val # 先頭ノードの値を一時保存 + # 先頭ノードを削除 fnext: ListNode | None = self._front.next if fnext is not None: fnext.prev = None self._front.next = None - self._front = fnext # ヘッドノードを更新 - # 後端デキュー操作 + self._front = fnext # 先頭ノードを更新する + # キュー末尾からの取り出し else: - val: int = self._rear.val # テールノードの値を一時的に保存 - # テールノードを削除 + val: int = self._rear.val # 末尾ノードの値を一時保存 + # 末尾ノードを削除 rprev: ListNode | None = self._rear.prev if rprev is not None: rprev.next = None self._rear.prev = None - self._rear = rprev # テールノードを更新 + self._rear = rprev # 末尾ノードを更新する self._size -= 1 # キューの長さを更新 return val def pop_first(self) -> int: - """前端デキュー""" + """キュー先頭からデキュー""" return self.pop(True) def pop_last(self) -> int: - """後端デキュー""" + """キュー末尾からデキュー""" return self.pop(False) def peek_first(self) -> int: - """前端要素にアクセス""" + """キュー先頭の要素にアクセス""" if self.is_empty(): - raise IndexError("Double-ended queue is empty") + raise IndexError("両端キューが空です") return self._front.val def peek_last(self) -> int: - """後端要素にアクセス""" + """キュー末尾の要素にアクセス""" if self.is_empty(): - raise IndexError("Double-ended queue is empty") + raise IndexError("両端キューが空です") return self._rear.val def to_array(self) -> list[int]: @@ -488,17 +545,17 @@ comments: true ```cpp title="linkedlist_deque.cpp" /* 双方向連結リストノード */ struct DoublyListNode { - int val; // ノードの値 - DoublyListNode *next; // 後続ノードへのポインタ - DoublyListNode *prev; // 前続ノードへのポインタ + int val; // ノード値 + DoublyListNode *next; // 後継ノードへのポインタ + DoublyListNode *prev; // 前駆ノードへのポインタ DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) { } }; - /* 双方向連結リストに基づく両端キュークラス */ + /* 双方向連結リストベースの両端キュー */ class LinkedListDeque { private: - DoublyListNode *front, *rear; // 先頭ノードfront、末尾ノードrear + DoublyListNode *front, *rear; // 先頭ノード front、末尾ノード rear int queSize = 0; // 両端キューの長さ public: @@ -506,9 +563,9 @@ comments: true LinkedListDeque() : front(nullptr), rear(nullptr) { } - /* デストラクタ */ + /* デストラクタメソッド */ ~LinkedListDeque() { - // 連結リストを走査、ノードを削除、メモリを解放 + // 連結リストを走査してノードを削除し、メモリを解放する DoublyListNode *pre, *cur = front; while (cur != nullptr) { pre = cur; @@ -530,31 +587,31 @@ comments: true /* エンキュー操作 */ void push(int num, bool isFront) { DoublyListNode *node = new DoublyListNode(num); - // リストが空の場合、frontとrearの両方をnodeに向ける + // 連結リストが空なら、front と rear の両方を node に向ける if (isEmpty()) front = rear = node; - // 先頭エンキュー操作 + // 先頭へのエンキュー操作 else if (isFront) { - // ノードをリストの先頭に追加 + // node を連結リストの先頭に追加 front->prev = node; node->next = front; - front = node; // 先頭ノードを更新 - // 末尾エンキュー操作 + front = node; // 先頭ノードを更新する + // 末尾へのエンキュー操作 } else { - // ノードをリストの末尾に追加 + // node を連結リストの末尾に追加 rear->next = node; node->prev = rear; - rear = node; // 末尾ノードを更新 + rear = node; // 末尾ノードを更新する } - queSize++; // キュー長を更新 + queSize++; // キューの長さを更新 } - /* 先頭エンキュー */ + /* キュー先頭にエンキュー */ void pushFirst(int num) { push(num, true); } - /* 末尾エンキュー */ + /* キュー末尾にエンキュー */ void pushLast(int num) { push(num, false); } @@ -562,9 +619,9 @@ comments: true /* デキュー操作 */ int pop(bool isFront) { if (isEmpty()) - throw out_of_range("Queue is empty"); + throw out_of_range("キューが空です"); int val; - // 先頭デキュー操作 + // キュー先頭からの取り出し if (isFront) { val = front->val; // 先頭ノードの値を一時保存 // 先頭ノードを削除 @@ -574,8 +631,8 @@ comments: true front->next = nullptr; } delete front; - front = fNext; // 先頭ノードを更新 - // 末尾デキュー操作 + front = fNext; // 先頭ノードを更新する + // キュー末尾からの取り出し } else { val = rear->val; // 末尾ノードの値を一時保存 // 末尾ノードを削除 @@ -585,37 +642,37 @@ comments: true rear->prev = nullptr; } delete rear; - rear = rPrev; // 末尾ノードを更新 + rear = rPrev; // 末尾ノードを更新する } - queSize--; // キュー長を更新 + queSize--; // キューの長さを更新 return val; } - /* 先頭デキュー */ + /* キュー先頭からデキュー */ int popFirst() { return pop(true); } - /* 末尾デキュー */ + /* キュー末尾からデキュー */ int popLast() { return pop(false); } - /* 先頭要素にアクセス */ + /* キュー先頭の要素にアクセス */ int peekFirst() { if (isEmpty()) - throw out_of_range("Double-ended queue is empty"); + throw out_of_range("両端キューが空です"); return front->val; } - /* 末尾要素にアクセス */ + /* キュー末尾の要素にアクセス */ int peekLast() { if (isEmpty()) - throw out_of_range("Double-ended queue is empty"); + throw out_of_range("両端キューが空です"); return rear->val; } - /* 印刷用に配列を返却 */ + /* 出力用の配列を返す */ vector toVector() { DoublyListNode *node = front; vector res(size()); @@ -635,7 +692,7 @@ comments: true class ListNode { int val; // ノード値 ListNode next; // 後続ノードへの参照 - ListNode prev; // 前任ノードへの参照 + ListNode prev; // 前駆ノードへの参照 ListNode(int val) { this.val = val; @@ -643,7 +700,7 @@ comments: true } } - /* 双方向連結リストに基づく両端キュークラス */ + /* 双方向連結リストベースの両端キュー */ class LinkedListDeque { private ListNode front, rear; // 先頭ノード front、末尾ノード rear private int queSize = 0; // 両端キューの長さ @@ -665,31 +722,31 @@ comments: true /* エンキュー操作 */ private void push(int num, boolean isFront) { ListNode node = new ListNode(num); - // リストが空の場合、front と rear の両方を node に指す + // 連結リストが空なら、front と rear の両方を node に向ける if (isEmpty()) front = rear = node; - // 先頭エンキュー操作 + // 先頭へのエンキュー操作 else if (isFront) { - // node をリストの先頭に追加 + // node を連結リストの先頭に追加 front.prev = node; node.next = front; - front = node; // front を更新 - // 末尾エンキュー操作 + front = node; // 先頭ノードを更新する + // 末尾へのエンキュー操作 } else { - // node をリストの末尾に追加 + // node を連結リストの末尾に追加 rear.next = node; node.prev = rear; - rear = node; // rear を更新 + rear = node; // 末尾ノードを更新する } - queSize++; // 長さを更新 + queSize++; // キューの長さを更新 } - /* 先頭エンキュー */ + /* キュー先頭にエンキュー */ public void pushFirst(int num) { push(num, true); } - /* 末尾エンキュー */ + /* キュー末尾にエンキュー */ public void pushLast(int num) { push(num, false); } @@ -699,56 +756,56 @@ comments: true if (isEmpty()) throw new IndexOutOfBoundsException(); int val; - // 先頭デキュー操作 + // キュー先頭からの取り出し if (isFront) { - val = front.val; // 一時的に先頭ノード値を保存 - // 次のノードを削除 + val = front.val; // 先頭ノードの値を一時保存 + // 先頭ノードを削除 ListNode fNext = front.next; if (fNext != null) { fNext.prev = null; front.next = null; } - front = fNext; // front を更新 - // 末尾デキュー操作 + front = fNext; // 先頭ノードを更新する + // キュー末尾からの取り出し } else { - val = rear.val; // 一時的に末尾ノード値を保存 - // 前のノードを削除 + val = rear.val; // 末尾ノードの値を一時保存 + // 末尾ノードを削除 ListNode rPrev = rear.prev; if (rPrev != null) { rPrev.next = null; rear.prev = null; } - rear = rPrev; // rear を更新 + rear = rPrev; // 末尾ノードを更新する } - queSize--; // 長さを更新 + queSize--; // キューの長さを更新 return val; } - /* 先頭デキュー */ + /* キュー先頭からデキュー */ public int popFirst() { return pop(true); } - /* 末尾デキュー */ + /* キュー末尾からデキュー */ public int popLast() { return pop(false); } - /* 先頭要素にアクセス */ + /* キュー先頭の要素にアクセス */ public int peekFirst() { if (isEmpty()) throw new IndexOutOfBoundsException(); return front.val; } - /* 末尾要素にアクセス */ + /* キュー末尾の要素にアクセス */ public int peekLast() { if (isEmpty()) throw new IndexOutOfBoundsException(); return rear.val; } - /* 配列を返す */ + /* 出力用の配列を返す */ public int[] toArray() { ListNode node = front; int[] res = new int[size()]; @@ -764,109 +821,1366 @@ comments: true === "C#" ```csharp title="linkedlist_deque.cs" - [class]{ListNode}-[func]{} + /* 双方向連結リストノード */ + class ListNode(int val) { + public int val = val; // ノード値 + public ListNode? next = null; // 後続ノードへの参照 + public ListNode? prev = null; // 前駆ノードへの参照 + } - [class]{LinkedListDeque}-[func]{} + /* 双方向連結リストベースの両端キュー */ + class LinkedListDeque { + ListNode? front, rear; // 先頭ノード front、末尾ノード rear + int queSize = 0; // 両端キューの長さ + + public LinkedListDeque() { + front = null; + rear = null; + } + + /* 両端キューの長さを取得 */ + public int Size() { + return queSize; + } + + /* 両端キューが空かどうかを判定 */ + public bool IsEmpty() { + return Size() == 0; + } + + /* エンキュー操作 */ + void Push(int num, bool isFront) { + ListNode node = new(num); + // 連結リストが空なら、front と rear の両方を node に向ける + if (IsEmpty()) { + front = node; + rear = node; + } + // 先頭へのエンキュー操作 + else if (isFront) { + // node を連結リストの先頭に追加 + front!.prev = node; + node.next = front; + front = node; // 先頭ノードを更新する + } + // 末尾へのエンキュー操作 + else { + // node を連結リストの末尾に追加 + rear!.next = node; + node.prev = rear; + rear = node; // 末尾ノードを更新する + } + + queSize++; // キューの長さを更新 + } + + /* キュー先頭にエンキュー */ + public void PushFirst(int num) { + Push(num, true); + } + + /* キュー末尾にエンキュー */ + public void PushLast(int num) { + Push(num, false); + } + + /* デキュー操作 */ + int? Pop(bool isFront) { + if (IsEmpty()) + throw new Exception(); + int? val; + // キュー先頭からの取り出し + if (isFront) { + val = front?.val; // 先頭ノードの値を一時保存 + // 先頭ノードを削除 + ListNode? fNext = front?.next; + if (fNext != null) { + fNext.prev = null; + front!.next = null; + } + front = fNext; // 先頭ノードを更新する + } + // キュー末尾からの取り出し + else { + val = rear?.val; // 末尾ノードの値を一時保存 + // 末尾ノードを削除 + ListNode? rPrev = rear?.prev; + if (rPrev != null) { + rPrev.next = null; + rear!.prev = null; + } + rear = rPrev; // 末尾ノードを更新する + } + + queSize--; // キューの長さを更新 + return val; + } + + /* キュー先頭からデキュー */ + public int? PopFirst() { + return Pop(true); + } + + /* キュー末尾からデキュー */ + public int? PopLast() { + return Pop(false); + } + + /* キュー先頭の要素にアクセス */ + public int? PeekFirst() { + if (IsEmpty()) + throw new Exception(); + return front?.val; + } + + /* キュー末尾の要素にアクセス */ + public int? PeekLast() { + if (IsEmpty()) + throw new Exception(); + return rear?.val; + } + + /* 出力用の配列を返す */ + public int?[] ToArray() { + ListNode? node = front; + int?[] res = new int?[Size()]; + for (int i = 0; i < res.Length; i++) { + res[i] = node?.val; + node = node?.next; + } + + return res; + } + } ``` === "Go" ```go title="linkedlist_deque.go" - [class]{linkedListDeque}-[func]{} + /* 双方向連結リストベースの両端キュー */ + type linkedListDeque struct { + // 組み込みパッケージ list を使う + data *list.List + } + + /* 両端キューを初期化する */ + func newLinkedListDeque() *linkedListDeque { + return &linkedListDeque{ + data: list.New(), + } + } + + /* キュー先頭に要素を追加する */ + func (s *linkedListDeque) pushFirst(value any) { + s.data.PushFront(value) + } + + /* キュー末尾に要素を追加する */ + func (s *linkedListDeque) pushLast(value any) { + s.data.PushBack(value) + } + + /* 先頭要素を取り出す */ + func (s *linkedListDeque) popFirst() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + s.data.Remove(e) + return e.Value + } + + /* 末尾要素を取り出す */ + func (s *linkedListDeque) popLast() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + s.data.Remove(e) + return e.Value + } + + /* キュー先頭の要素にアクセス */ + func (s *linkedListDeque) peekFirst() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + return e.Value + } + + /* キュー末尾の要素にアクセス */ + func (s *linkedListDeque) peekLast() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + return e.Value + } + + /* キューの長さを取得 */ + func (s *linkedListDeque) size() int { + return s.data.Len() + } + + /* キューが空かどうかを判定 */ + func (s *linkedListDeque) isEmpty() bool { + return s.data.Len() == 0 + } + + /* 表示用に List を取得 */ + func (s *linkedListDeque) toList() *list.List { + return s.data + } ``` === "Swift" ```swift title="linkedlist_deque.swift" - [class]{ListNode}-[func]{} + /* 双方向連結リストノード */ + class ListNode { + var val: Int // ノード値 + var next: ListNode? // 後続ノードへの参照 + weak var prev: ListNode? // 前駆ノードへの参照 - [class]{LinkedListDeque}-[func]{} + init(val: Int) { + self.val = val + } + } + + /* 双方向連結リストベースの両端キュー */ + class LinkedListDeque { + private var front: ListNode? // 先頭ノード front + private var rear: ListNode? // 末尾ノード rear + private var _size: Int // 両端キューの長さ + + init() { + _size = 0 + } + + /* 両端キューの長さを取得 */ + func size() -> Int { + _size + } + + /* 両端キューが空かどうかを判定 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* エンキュー操作 */ + private func push(num: Int, isFront: Bool) { + let node = ListNode(val: num) + // 連結リストが空なら、front と rear の両方を node に向ける + if isEmpty() { + front = node + rear = node + } + // 先頭へのエンキュー操作 + else if isFront { + // node を連結リストの先頭に追加 + front?.prev = node + node.next = front + front = node // 先頭ノードを更新する + } + // 末尾へのエンキュー操作 + else { + // node を連結リストの末尾に追加 + rear?.next = node + node.prev = rear + rear = node // 末尾ノードを更新する + } + _size += 1 // キューの長さを更新 + } + + /* キュー先頭にエンキュー */ + func pushFirst(num: Int) { + push(num: num, isFront: true) + } + + /* キュー末尾にエンキュー */ + func pushLast(num: Int) { + push(num: num, isFront: false) + } + + /* デキュー操作 */ + private func pop(isFront: Bool) -> Int { + if isEmpty() { + fatalError("両端キューが空です") + } + let val: Int + // キュー先頭からの取り出し + if isFront { + val = front!.val // 先頭ノードの値を一時保存 + // 先頭ノードを削除 + let fNext = front?.next + if fNext != nil { + fNext?.prev = nil + front?.next = nil + } + front = fNext // 先頭ノードを更新する + } + // キュー末尾からの取り出し + else { + val = rear!.val // 末尾ノードの値を一時保存 + // 末尾ノードを削除 + let rPrev = rear?.prev + if rPrev != nil { + rPrev?.next = nil + rear?.prev = nil + } + rear = rPrev // 末尾ノードを更新する + } + _size -= 1 // キューの長さを更新 + return val + } + + /* キュー先頭からデキュー */ + func popFirst() -> Int { + pop(isFront: true) + } + + /* キュー末尾からデキュー */ + func popLast() -> Int { + pop(isFront: false) + } + + /* キュー先頭の要素にアクセス */ + func peekFirst() -> Int { + if isEmpty() { + fatalError("両端キューが空です") + } + return front!.val + } + + /* キュー末尾の要素にアクセス */ + func peekLast() -> Int { + if isEmpty() { + fatalError("両端キューが空です") + } + return rear!.val + } + + /* 出力用の配列を返す */ + func toArray() -> [Int] { + var node = front + var res = Array(repeating: 0, count: size()) + for i in res.indices { + res[i] = node!.val + node = node?.next + } + return res + } + } ``` === "JS" ```javascript title="linkedlist_deque.js" - [class]{ListNode}-[func]{} + /* 双方向連結リストノード */ + class ListNode { + prev; // 前駆ノードへの参照(ポインタ) + next; // 後継ノードへの参照(ポインタ) + val; // ノード値 - [class]{LinkedListDeque}-[func]{} + constructor(val) { + this.val = val; + this.next = null; + this.prev = null; + } + } + + /* 双方向連結リストベースの両端キュー */ + class LinkedListDeque { + #front; // 先頭ノード front + #rear; // 末尾ノード rear + #queSize; // 両端キューの長さ + + constructor() { + this.#front = null; + this.#rear = null; + this.#queSize = 0; + } + + /* 末尾へのエンキュー操作 */ + pushLast(val) { + const node = new ListNode(val); + // 連結リストが空なら、front と rear の両方を node に向ける + if (this.#queSize === 0) { + this.#front = node; + this.#rear = node; + } else { + // node を連結リストの末尾に追加 + this.#rear.next = node; + node.prev = this.#rear; + this.#rear = node; // 末尾ノードを更新する + } + this.#queSize++; + } + + /* 先頭へのエンキュー操作 */ + pushFirst(val) { + const node = new ListNode(val); + // 連結リストが空なら、front と rear の両方を node に向ける + if (this.#queSize === 0) { + this.#front = node; + this.#rear = node; + } else { + // node を連結リストの先頭に追加 + this.#front.prev = node; + node.next = this.#front; + this.#front = node; // 先頭ノードを更新する + } + this.#queSize++; + } + + /* キュー末尾からの取り出し */ + popLast() { + if (this.#queSize === 0) { + return null; + } + const value = this.#rear.val; // 末尾ノードの値を保存する + // 末尾ノードを削除 + let temp = this.#rear.prev; + if (temp !== null) { + temp.next = null; + this.#rear.prev = null; + } + this.#rear = temp; // 末尾ノードを更新する + this.#queSize--; + return value; + } + + /* キュー先頭からの取り出し */ + popFirst() { + if (this.#queSize === 0) { + return null; + } + const value = this.#front.val; // 末尾ノードの値を保存する + // 先頭ノードを削除 + let temp = this.#front.next; + if (temp !== null) { + temp.prev = null; + this.#front.next = null; + } + this.#front = temp; // 先頭ノードを更新する + this.#queSize--; + return value; + } + + /* キュー末尾の要素にアクセス */ + peekLast() { + return this.#queSize === 0 ? null : this.#rear.val; + } + + /* キュー先頭の要素にアクセス */ + peekFirst() { + return this.#queSize === 0 ? null : this.#front.val; + } + + /* 両端キューの長さを取得 */ + size() { + return this.#queSize; + } + + /* 両端キューが空かどうかを判定 */ + isEmpty() { + return this.#queSize === 0; + } + + /* 両端キューを出力する */ + print() { + const arr = []; + let temp = this.#front; + while (temp !== null) { + arr.push(temp.val); + temp = temp.next; + } + console.log('[' + arr.join(', ') + ']'); + } + } ``` === "TS" ```typescript title="linkedlist_deque.ts" - [class]{ListNode}-[func]{} + /* 双方向連結リストノード */ + class ListNode { + prev: ListNode; // 前駆ノードへの参照(ポインタ) + next: ListNode; // 後継ノードへの参照(ポインタ) + val: number; // ノード値 - [class]{LinkedListDeque}-[func]{} + constructor(val: number) { + this.val = val; + this.next = null; + this.prev = null; + } + } + + /* 双方向連結リストベースの両端キュー */ + class LinkedListDeque { + private front: ListNode; // 先頭ノード front + private rear: ListNode; // 末尾ノード rear + private queSize: number; // 両端キューの長さ + + constructor() { + this.front = null; + this.rear = null; + this.queSize = 0; + } + + /* 末尾へのエンキュー操作 */ + pushLast(val: number): void { + const node: ListNode = new ListNode(val); + // 連結リストが空なら、front と rear の両方を node に向ける + if (this.queSize === 0) { + this.front = node; + this.rear = node; + } else { + // node を連結リストの末尾に追加 + this.rear.next = node; + node.prev = this.rear; + this.rear = node; // 末尾ノードを更新する + } + this.queSize++; + } + + /* 先頭へのエンキュー操作 */ + pushFirst(val: number): void { + const node: ListNode = new ListNode(val); + // 連結リストが空なら、front と rear の両方を node に向ける + if (this.queSize === 0) { + this.front = node; + this.rear = node; + } else { + // node を連結リストの先頭に追加 + this.front.prev = node; + node.next = this.front; + this.front = node; // 先頭ノードを更新する + } + this.queSize++; + } + + /* キュー末尾からの取り出し */ + popLast(): number { + if (this.queSize === 0) { + return null; + } + const value: number = this.rear.val; // 末尾ノードの値を保存する + // 末尾ノードを削除 + let temp: ListNode = this.rear.prev; + if (temp !== null) { + temp.next = null; + this.rear.prev = null; + } + this.rear = temp; // 末尾ノードを更新する + this.queSize--; + return value; + } + + /* キュー先頭からの取り出し */ + popFirst(): number { + if (this.queSize === 0) { + return null; + } + const value: number = this.front.val; // 末尾ノードの値を保存する + // 先頭ノードを削除 + let temp: ListNode = this.front.next; + if (temp !== null) { + temp.prev = null; + this.front.next = null; + } + this.front = temp; // 先頭ノードを更新する + this.queSize--; + return value; + } + + /* キュー末尾の要素にアクセス */ + peekLast(): number { + return this.queSize === 0 ? null : this.rear.val; + } + + /* キュー先頭の要素にアクセス */ + peekFirst(): number { + return this.queSize === 0 ? null : this.front.val; + } + + /* 両端キューの長さを取得 */ + size(): number { + return this.queSize; + } + + /* 両端キューが空かどうかを判定 */ + isEmpty(): boolean { + return this.queSize === 0; + } + + /* 両端キューを出力する */ + print(): void { + const arr: number[] = []; + let temp: ListNode = this.front; + while (temp !== null) { + arr.push(temp.val); + temp = temp.next; + } + console.log('[' + arr.join(', ') + ']'); + } + } ``` === "Dart" ```dart title="linkedlist_deque.dart" - [class]{ListNode}-[func]{} + /* 双方向連結リストノード */ + class ListNode { + int val; // ノード値 + ListNode? next; // 後続ノードへの参照 + ListNode? prev; // 前駆ノードへの参照 - [class]{LinkedListDeque}-[func]{} + ListNode(this.val, {this.next, this.prev}); + } + + /* 双方向連結リストに基づく双方向キュー */ + class LinkedListDeque { + late ListNode? _front; // 先頭ノード _front + late ListNode? _rear; // 末尾ノード _rear + int _queSize = 0; // 両端キューの長さ + + LinkedListDeque() { + this._front = null; + this._rear = null; + } + + /* 両端キューの長さを取得 */ + int size() { + return this._queSize; + } + + /* 両端キューが空かどうかを判定 */ + bool isEmpty() { + return size() == 0; + } + + /* エンキュー操作 */ + void push(int _num, bool isFront) { + final ListNode node = ListNode(_num); + if (isEmpty()) { + // 連結リストが空なら、`_front` と `_rear` の両方を `node` に向ける + _front = _rear = node; + } else if (isFront) { + // 先頭へのエンキュー操作 + // node を連結リストの先頭に追加する + _front!.prev = node; + node.next = _front; + _front = node; // 先頭ノードを更新する + } else { + // 末尾へのエンキュー操作 + // node を連結リストの末尾に追加する + _rear!.next = node; + node.prev = _rear; + _rear = node; // 末尾ノードを更新する + } + _queSize++; // キューの長さを更新 + } + + /* キュー先頭にエンキュー */ + void pushFirst(int _num) { + push(_num, true); + } + + /* キュー末尾にエンキュー */ + void pushLast(int _num) { + push(_num, false); + } + + /* デキュー操作 */ + int? pop(bool isFront) { + // キューが空なら、そのまま `null` を返す + if (isEmpty()) { + return null; + } + final int val; + if (isFront) { + // キュー先頭からの取り出し + val = _front!.val; // 先頭ノードの値を一時保存 + // 先頭ノードを削除 + ListNode? fNext = _front!.next; + if (fNext != null) { + fNext.prev = null; + _front!.next = null; + } + _front = fNext; // 先頭ノードを更新する + } else { + // キュー末尾からの取り出し + val = _rear!.val; // 末尾ノードの値を一時保存 + // 末尾ノードを削除 + ListNode? rPrev = _rear!.prev; + if (rPrev != null) { + rPrev.next = null; + _rear!.prev = null; + } + _rear = rPrev; // 末尾ノードを更新する + } + _queSize--; // キューの長さを更新 + return val; + } + + /* キュー先頭からデキュー */ + int? popFirst() { + return pop(true); + } + + /* キュー末尾からデキュー */ + int? popLast() { + return pop(false); + } + + /* キュー先頭の要素にアクセス */ + int? peekFirst() { + return _front?.val; + } + + /* キュー末尾の要素にアクセス */ + int? peekLast() { + return _rear?.val; + } + + /* 出力用の配列を返す */ + List toArray() { + ListNode? node = _front; + final List res = []; + for (int i = 0; i < _queSize; i++) { + res.add(node!.val); + node = node.next; + } + return res; + } + } ``` === "Rust" ```rust title="linkedlist_deque.rs" - [class]{ListNode}-[func]{} + /* 双方向連結リストノード */ + pub struct ListNode { + pub val: T, // ノード値 + pub next: Option>>>, // 後継ノードへのポインタ + pub prev: Option>>>, // 前駆ノードへのポインタ + } - [class]{LinkedListDeque}-[func]{} + impl ListNode { + pub fn new(val: T) -> Rc>> { + Rc::new(RefCell::new(ListNode { + val, + next: None, + prev: None, + })) + } + } + + /* 双方向連結リストベースの両端キュー */ + #[allow(dead_code)] + pub struct LinkedListDeque { + front: Option>>>, // 先頭ノード front + rear: Option>>>, // 末尾ノード rear + que_size: usize, // 両端キューの長さ + } + + impl LinkedListDeque { + pub fn new() -> Self { + Self { + front: None, + rear: None, + que_size: 0, + } + } + + /* 両端キューの長さを取得 */ + pub fn size(&self) -> usize { + return self.que_size; + } + + /* 両端キューが空かどうかを判定 */ + pub fn is_empty(&self) -> bool { + return self.que_size == 0; + } + + /* エンキュー操作 */ + fn push(&mut self, num: T, is_front: bool) { + let node = ListNode::new(num); + // 先頭へのエンキュー操作 + if is_front { + match self.front.take() { + // 連結リストが空なら、front と rear の両方を node に向ける + None => { + self.rear = Some(node.clone()); + self.front = Some(node); + } + // node を連結リストの先頭に追加 + Some(old_front) => { + old_front.borrow_mut().prev = Some(node.clone()); + node.borrow_mut().next = Some(old_front); + self.front = Some(node); // 先頭ノードを更新する + } + } + } + // 末尾へのエンキュー操作 + else { + match self.rear.take() { + // 連結リストが空なら、front と rear の両方を node に向ける + None => { + self.front = Some(node.clone()); + self.rear = Some(node); + } + // node を連結リストの末尾に追加 + Some(old_rear) => { + old_rear.borrow_mut().next = Some(node.clone()); + node.borrow_mut().prev = Some(old_rear); + self.rear = Some(node); // 末尾ノードを更新する + } + } + } + self.que_size += 1; // キューの長さを更新 + } + + /* キュー先頭にエンキュー */ + pub fn push_first(&mut self, num: T) { + self.push(num, true); + } + + /* キュー末尾にエンキュー */ + pub fn push_last(&mut self, num: T) { + self.push(num, false); + } + + /* デキュー操作 */ + fn pop(&mut self, is_front: bool) -> Option { + // キューが空なら、そのまま `None` を返す + if self.is_empty() { + return None; + }; + // キュー先頭からの取り出し + if is_front { + self.front.take().map(|old_front| { + match old_front.borrow_mut().next.take() { + Some(new_front) => { + new_front.borrow_mut().prev.take(); + self.front = Some(new_front); // 先頭ノードを更新する + } + None => { + self.rear.take(); + } + } + self.que_size -= 1; // キューの長さを更新 + old_front.borrow().val + }) + } + // キュー末尾からの取り出し + else { + self.rear.take().map(|old_rear| { + match old_rear.borrow_mut().prev.take() { + Some(new_rear) => { + new_rear.borrow_mut().next.take(); + self.rear = Some(new_rear); // 末尾ノードを更新する + } + None => { + self.front.take(); + } + } + self.que_size -= 1; // キューの長さを更新 + old_rear.borrow().val + }) + } + } + + /* キュー先頭からデキュー */ + pub fn pop_first(&mut self) -> Option { + return self.pop(true); + } + + /* キュー末尾からデキュー */ + pub fn pop_last(&mut self) -> Option { + return self.pop(false); + } + + /* キュー先頭の要素にアクセス */ + pub fn peek_first(&self) -> Option<&Rc>>> { + self.front.as_ref() + } + + /* キュー末尾の要素にアクセス */ + pub fn peek_last(&self) -> Option<&Rc>>> { + self.rear.as_ref() + } + + /* 出力用の配列を返す */ + pub fn to_array(&self, head: Option<&Rc>>>) -> Vec { + let mut res: Vec = Vec::new(); + fn recur(cur: Option<&Rc>>>, res: &mut Vec) { + if let Some(cur) = cur { + res.push(cur.borrow().val); + recur(cur.borrow().next.as_ref(), res); + } + } + + recur(head, &mut res); + res + } + } ``` === "C" ```c title="linkedlist_deque.c" - [class]{DoublyListNode}-[func]{} + /* 双方向連結リストノード */ + typedef struct DoublyListNode { + int val; // ノード値 + struct DoublyListNode *next; // 後続ノード + struct DoublyListNode *prev; // 前駆ノード + } DoublyListNode; - [class]{LinkedListDeque}-[func]{} + /* コンストラクタ */ + DoublyListNode *newDoublyListNode(int num) { + DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode)); + new->val = num; + new->next = NULL; + new->prev = NULL; + return new; + } + + /* デストラクタ */ + void delDoublyListNode(DoublyListNode *node) { + free(node); + } + + /* 双方向連結リストベースの両端キュー */ + typedef struct { + DoublyListNode *front, *rear; // 先頭ノード front、末尾ノード rear + int queSize; // 両端キューの長さ + } LinkedListDeque; + + /* コンストラクタ */ + LinkedListDeque *newLinkedListDeque() { + LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque)); + deque->front = NULL; + deque->rear = NULL; + deque->queSize = 0; + return deque; + } + + /* デストラクタ */ + void delLinkedListdeque(LinkedListDeque *deque) { + // すべてのノードを解放 + for (int i = 0; i < deque->queSize && deque->front != NULL; i++) { + DoublyListNode *tmp = deque->front; + deque->front = deque->front->next; + free(tmp); + } + // deque 構造体を解放する + free(deque); + } + + /* キューの長さを取得 */ + int size(LinkedListDeque *deque) { + return deque->queSize; + } + + /* キューが空かどうかを判定 */ + bool empty(LinkedListDeque *deque) { + return (size(deque) == 0); + } + + /* エンキュー */ + void push(LinkedListDeque *deque, int num, bool isFront) { + DoublyListNode *node = newDoublyListNode(num); + // 連結リストが空なら、`front` と `rear` の両方を `node` に向ける + if (empty(deque)) { + deque->front = deque->rear = node; + } + // 先頭へのエンキュー操作 + else if (isFront) { + // node を連結リストの先頭に追加 + deque->front->prev = node; + node->next = deque->front; + deque->front = node; // 先頭ノードを更新する + } + // 末尾へのエンキュー操作 + else { + // node を連結リストの末尾に追加 + deque->rear->next = node; + node->prev = deque->rear; + deque->rear = node; + } + deque->queSize++; // キューの長さを更新 + } + + /* キュー先頭にエンキュー */ + void pushFirst(LinkedListDeque *deque, int num) { + push(deque, num, true); + } + + /* キュー末尾にエンキュー */ + void pushLast(LinkedListDeque *deque, int num) { + push(deque, num, false); + } + + /* キュー先頭の要素にアクセス */ + int peekFirst(LinkedListDeque *deque) { + assert(size(deque) && deque->front); + return deque->front->val; + } + + /* キュー末尾の要素にアクセス */ + int peekLast(LinkedListDeque *deque) { + assert(size(deque) && deque->rear); + return deque->rear->val; + } + + /* デキュー */ + int pop(LinkedListDeque *deque, bool isFront) { + if (empty(deque)) + return -1; + int val; + // キュー先頭からの取り出し + if (isFront) { + val = peekFirst(deque); // 先頭ノードの値を一時保存 + DoublyListNode *fNext = deque->front->next; + if (fNext) { + fNext->prev = NULL; + deque->front->next = NULL; + } + delDoublyListNode(deque->front); + deque->front = fNext; // 先頭ノードを更新する + } + // キュー末尾からの取り出し + else { + val = peekLast(deque); // 末尾ノードの値を一時保存 + DoublyListNode *rPrev = deque->rear->prev; + if (rPrev) { + rPrev->next = NULL; + deque->rear->prev = NULL; + } + delDoublyListNode(deque->rear); + deque->rear = rPrev; // 末尾ノードを更新する + } + deque->queSize--; // キューの長さを更新 + return val; + } + + /* キュー先頭からデキュー */ + int popFirst(LinkedListDeque *deque) { + return pop(deque, true); + } + + /* キュー末尾からデキュー */ + int popLast(LinkedListDeque *deque) { + return pop(deque, false); + } + + /* キューを出力する */ + void printLinkedListDeque(LinkedListDeque *deque) { + int *arr = malloc(sizeof(int) * deque->queSize); + // 連結リスト内のデータを配列にコピー + int i; + DoublyListNode *node; + for (i = 0, node = deque->front; i < deque->queSize; i++) { + arr[i] = node->val; + node = node->next; + } + printArray(arr, deque->queSize); + free(arr); + } ``` === "Kotlin" ```kotlin title="linkedlist_deque.kt" - [class]{ListNode}-[func]{} + /* 双方向連結リストノード */ + class ListNode(var _val: Int) { + // ノード値 + var next: ListNode? = null // 後続ノードへの参照 + var prev: ListNode? = null // 前駆ノードへの参照 + } - [class]{LinkedListDeque}-[func]{} + /* 双方向連結リストベースの両端キュー */ + class LinkedListDeque { + private var front: ListNode? = null // 先頭ノード front + private var rear: ListNode? = null // 末尾ノード rear + private var queSize: Int = 0 // 両端キューの長さ + + /* 両端キューの長さを取得 */ + fun size(): Int { + return queSize + } + + /* 両端キューが空かどうかを判定 */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* エンキュー操作 */ + fun push(num: Int, isFront: Boolean) { + val node = ListNode(num) + // 連結リストが空なら、front と rear の両方を node に向ける + if (isEmpty()) { + rear = node + front = rear + // 先頭へのエンキュー操作 + } else if (isFront) { + // node を連結リストの先頭に追加 + front?.prev = node + node.next = front + front = node // 先頭ノードを更新する + // 末尾へのエンキュー操作 + } else { + // node を連結リストの末尾に追加 + rear?.next = node + node.prev = rear + rear = node // 末尾ノードを更新する + } + queSize++ // キューの長さを更新 + } + + /* キュー先頭にエンキュー */ + fun pushFirst(num: Int) { + push(num, true) + } + + /* キュー末尾にエンキュー */ + fun pushLast(num: Int) { + push(num, false) + } + + /* デキュー操作 */ + fun pop(isFront: Boolean): Int { + if (isEmpty()) + throw IndexOutOfBoundsException() + val _val: Int + // キュー先頭からの取り出し + if (isFront) { + _val = front!!._val // 先頭ノードの値を一時保存 + // 先頭ノードを削除 + val fNext = front!!.next + if (fNext != null) { + fNext.prev = null + front!!.next = null + } + front = fNext // 先頭ノードを更新する + // キュー末尾からの取り出し + } else { + _val = rear!!._val // 末尾ノードの値を一時保存 + // 末尾ノードを削除 + val rPrev = rear!!.prev + if (rPrev != null) { + rPrev.next = null + rear!!.prev = null + } + rear = rPrev // 末尾ノードを更新する + } + queSize-- // キューの長さを更新 + return _val + } + + /* キュー先頭からデキュー */ + fun popFirst(): Int { + return pop(true) + } + + /* キュー末尾からデキュー */ + fun popLast(): Int { + return pop(false) + } + + /* キュー先頭の要素にアクセス */ + fun peekFirst(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return front!!._val + } + + /* キュー末尾の要素にアクセス */ + fun peekLast(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return rear!!._val + } + + /* 出力用の配列を返す */ + fun toArray(): IntArray { + var node = front + val res = IntArray(size()) + for (i in res.indices) { + res[i] = node!!._val + node = node.next + } + return res + } + } ``` === "Ruby" ```ruby title="linkedlist_deque.rb" - [class]{ListNode}-[func]{} + =begin + File: linkedlist_deque.rb + Created Time: 2024-04-06 + Author: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com) + =end - [class]{LinkedListDeque}-[func]{} + # ## 双方向連結リストノード + class ListNode + attr_accessor :val + attr_accessor :next # 後続ノードへの参照 + attr_accessor :prev # 前ノードへの参照 + + ### コンストラクタ ### + def initialize(val) + @val = val + end + end + + ### 双方向連結リストで実装した両端キュー ### + class LinkedListDeque + ### 両端キューの長さを取得 ### + attr_reader :size + + ### コンストラクタ ### + def initialize + @front = nil # 先頭ノード front + @rear = nil # 末尾ノード rear + @size = 0 # 両端キューの長さ + end + + ### 両端キューが空か判定 ### + def is_empty? + size.zero? + end + + ### エンキュー操作 ### + def push(num, is_front) + node = ListNode.new(num) + # 連結リストが空なら、`front` と `rear` の両方を `node` に向ける + if is_empty? + @front = @rear = node + # 先頭へのエンキュー操作 + elsif is_front + # node を連結リストの先頭に追加 + @front.prev = node + node.next = @front + @front = node # 先頭ノードを更新する + # 末尾へのエンキュー操作 + else + # node を連結リストの末尾に追加 + @rear.next = node + node.prev = @rear + @rear = node # 末尾ノードを更新する + end + @size += 1 # キューの長さを更新 + end + + ### キュー先頭に追加 ### + def push_first(num) + push(num, true) + end + + ### キュー末尾に追加 ### + def push_last(num) + push(num, false) + end + + ### デキュー操作 ### + def pop(is_front) + raise IndexError, '両端キューは空です' if is_empty? + + # キュー先頭からの取り出し + if is_front + val = @front.val # 先頭ノードの値を一時保存 + # 先頭ノードを削除 + fnext = @front.next + unless fnext.nil? + fnext.prev = nil + @front.next = nil + end + @front = fnext # 先頭ノードを更新する + # キュー末尾からの取り出し + else + val = @rear.val # 末尾ノードの値を一時保存 + # 末尾ノードを削除 + rprev = @rear.prev + unless rprev.nil? + rprev.next = nil + @rear.prev = nil + end + @rear = rprev # 末尾ノードを更新する + end + @size -= 1 # キューの長さを更新 + + val + end + + ### キュー先頭から取り出す ### + def pop_first + pop(true) + end + + ### キュー先頭から取り出す ### + def pop_last + pop(false) + end + + ### 先頭要素にアクセス ### + def peek_first + raise IndexError, '両端キューは空です' if is_empty? + + @front.val + end + + ### キュー末尾要素を参照 ### + def peek_last + raise IndexError, '両端キューは空です' if is_empty? + + @rear.val + end + + ### 表示用の配列を返す ### + def to_array + node = @front + res = Array.new(size, 0) + for i in 0...size + res[i] = node.val + node = node.next + end + res + end + end ``` ### 2.   配列に基づく実装 -下図に示すように、配列でキューを実装するのと同様に、循環配列を使って両端キューを実装することもできます。 +次の図に示すように、配列によるキュー実装と同様に、循環配列を使って両端キューを実装することもできます。 -=== "ArrayDeque" - ![配列による両端キューのエンキューとデキュー操作の実装](deque.assets/array_deque_step1.png){ class="animation-figure" } +=== "<1>" + ![配列による両端キューのエンキューとデキュー](deque.assets/array_deque_step1.png){ class="animation-figure" } -=== "pushLast()" +=== "<2>" ![array_deque_push_last](deque.assets/array_deque_step2_push_last.png){ class="animation-figure" } -=== "pushFirst()" +=== "<3>" ![array_deque_push_first](deque.assets/array_deque_step3_push_first.png){ class="animation-figure" } -=== "popLast()" +=== "<4>" ![array_deque_pop_last](deque.assets/array_deque_step4_pop_last.png){ class="animation-figure" } -=== "popFirst()" +=== "<5>" ![array_deque_pop_first](deque.assets/array_deque_step5_pop_first.png){ class="animation-figure" } -

図 5-9   配列による両端キューのエンキューとデキュー操作の実装

+

図 5-9   配列による両端キューのエンキューとデキュー

-実装では「前端エンキュー」と「後端デキュー」のメソッドを追加するだけです: +キュー実装を土台として、「先頭へのエンキュー」と「末尾からのデキュー」のメソッドを追加するだけで済みます: === "Python" ```python title="array_deque.py" class ArrayDeque: - """循環配列ベースの双端キュークラス""" + """循環配列ベースの両端キュー""" def __init__(self, capacity: int): """コンストラクタ""" @@ -875,78 +2189,78 @@ comments: true self._size: int = 0 def capacity(self) -> int: - """双端キューの容量を取得""" + """両端キューの容量を取得""" return len(self._nums) def size(self) -> int: - """双端キューの長さを取得""" + """両端キューの長さを取得""" return self._size def is_empty(self) -> bool: - """双端キューが空かどうかを判定""" + """両端キューが空かどうかを判定""" return self._size == 0 def index(self, i: int) -> int: """循環配列のインデックスを計算""" - # モジュロ演算によって循環配列を実装 - # i が配列の末尾を超えた場合、先頭に戻る - # i が配列の先頭を超えた場合、末尾に戻る + # 剰余演算により配列の先頭と末尾をつなげる + # i が配列の末尾を越えたら先頭に戻る + # i が配列の先頭を越えて前に出たら末尾に戻る return (i + self.capacity()) % self.capacity() def push_first(self, num: int): - """前端エンキュー""" + """キュー先頭にエンキュー""" if self._size == self.capacity(): - print("双端キューが満杯です") + print("両端キューがいっぱいです") return - # フロントポインタを左に1つ移動 - # モジュロ演算によってフロントが配列の先頭を超えて末尾に戻ることを実装 + # 先頭ポインタを左に 1 つ移動する + # 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする self._front = self.index(self._front - 1) - # num を前端に追加 + # num をキュー先頭に追加 self._nums[self._front] = num self._size += 1 def push_last(self, num: int): - """後端エンキュー""" + """キュー末尾にエンキュー""" if self._size == self.capacity(): - print("双端キューが満杯です") + print("両端キューがいっぱいです") return - # リアポインタを計算、リアインデックス + 1 を指す + # キュー末尾ポインタを計算し、末尾インデックス + 1 を指す rear = self.index(self._front + self._size) - # num を後端に追加 + # num をキュー末尾に追加 self._nums[rear] = num self._size += 1 def pop_first(self) -> int: - """前端デキュー""" + """キュー先頭からデキュー""" num = self.peek_first() - # フロントポインタを1つ後ろに移動 + # 先頭ポインタを 1 つ後ろへ進める self._front = self.index(self._front + 1) self._size -= 1 return num def pop_last(self) -> int: - """後端デキュー""" + """キュー末尾からデキュー""" num = self.peek_last() self._size -= 1 return num def peek_first(self) -> int: - """前端要素にアクセス""" + """キュー先頭の要素にアクセス""" if self.is_empty(): - raise IndexError("Double-ended queue is empty") + raise IndexError("両端キューが空です") return self._nums[self._front] def peek_last(self) -> int: - """後端要素にアクセス""" + """キュー末尾の要素にアクセス""" if self.is_empty(): - raise IndexError("Double-ended queue is empty") - # 後端要素のインデックスを計算 + raise IndexError("両端キューが空です") + # 末尾要素のインデックスを計算 last = self.index(self._front + self._size - 1) return self._nums[last] def to_array(self) -> list[int]: """出力用の配列を返す""" - # 有効な長さ範囲内の要素のみを変換 + # 有効長の範囲内のリスト要素のみを変換 res = [] for i in range(self._size): res.append(self._nums[self.index(self._front + i)]) @@ -956,11 +2270,11 @@ comments: true === "C++" ```cpp title="array_deque.cpp" - /* 循環配列に基づく両端キュークラス */ + /* 循環配列ベースの両端キュー */ class ArrayDeque { private: vector nums; // 両端キューの要素を格納する配列 - int front; // 先頭ポインタ、先頭要素を指す + int front; // 先頭ポインタ。先頭要素を指す int queSize; // 両端キューの長さ public: @@ -987,74 +2301,74 @@ comments: true /* 循環配列のインデックスを計算 */ int index(int i) { - // 剰余演算で循環配列を実現 - // iが配列の末尾を超えた場合、先頭に戻る - // iが配列の先頭を超えた場合、末尾に戻る + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えて前に出たら末尾に戻る return (i + capacity()) % capacity(); } - /* 先頭エンキュー */ + /* キュー先頭にエンキュー */ void pushFirst(int num) { if (queSize == capacity()) { - cout << "Double-ended queue is full" << endl; + cout << "両端キューがいっぱいです" << endl; return; } - // 先頭ポインタを1つ左に移動 - // 剰余演算でfrontが配列の先頭を越えて末尾に戻ることを実現 + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする front = index(front - 1); - // numを先頭に追加 + // num をキュー先頭に追加 nums[front] = num; queSize++; } - /* 末尾エンキュー */ + /* キュー末尾にエンキュー */ void pushLast(int num) { if (queSize == capacity()) { - cout << "Double-ended queue is full" << endl; + cout << "両端キューがいっぱいです" << endl; return; } - // 末尾ポインタを計算、末尾インデックス + 1を指す + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す int rear = index(front + queSize); - // numを末尾に追加 + // num をキュー末尾に追加 nums[rear] = num; queSize++; } - /* 先頭デキュー */ + /* キュー先頭からデキュー */ int popFirst() { int num = peekFirst(); - // 先頭ポインタを1つ後ろに移動 + // 先頭ポインタを 1 つ後ろへ進める front = index(front + 1); queSize--; return num; } - /* 末尾デキュー */ + /* キュー末尾からデキュー */ int popLast() { int num = peekLast(); queSize--; return num; } - /* 先頭要素にアクセス */ + /* キュー先頭の要素にアクセス */ int peekFirst() { if (isEmpty()) - throw out_of_range("Double-ended queue is empty"); + throw out_of_range("両端キューが空です"); return nums[front]; } - /* 末尾要素にアクセス */ + /* キュー末尾の要素にアクセス */ int peekLast() { if (isEmpty()) - throw out_of_range("Double-ended queue is empty"); + throw out_of_range("両端キューが空です"); // 末尾要素のインデックスを計算 int last = index(front + queSize - 1); return nums[last]; } - /* 印刷用に配列を返却 */ + /* 出力用の配列を返す */ vector toVector() { - // 有効な長さ範囲内の要素のみを変換 + // 有効長の範囲内のリスト要素のみを変換 vector res(queSize); for (int i = 0, j = front; i < queSize; i++, j++) { res[i] = nums[index(j)]; @@ -1067,10 +2381,10 @@ comments: true === "Java" ```java title="array_deque.java" - /* 循環配列に基づく両端キュークラス */ + /* 循環配列ベースの両端キュー */ class ArrayDeque { private int[] nums; // 両端キューの要素を格納する配列 - private int front; // 先頭ポインタ、先頭要素を指す + private int front; // 先頭ポインタ。先頭要素を指す private int queSize; // 両端キューの長さ /* コンストラクタ */ @@ -1094,63 +2408,65 @@ comments: true return queSize == 0; } - /* 循環配列インデックスを計算 */ + /* 循環配列のインデックスを計算 */ private int index(int i) { - // モジュロ演算により循環配列を実装 - // i が配列の末尾を超える場合、先頭に戻る - // i が配列の先頭を超える場合、末尾に戻る + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えて前に出たら末尾に戻る return (i + capacity()) % capacity(); } - /* 先頭エンキュー */ + /* キュー先頭にエンキュー */ public void pushFirst(int num) { if (queSize == capacity()) { - System.out.println("両端キューが満杯です"); + System.out.println("双方向キューは満杯です"); return; } - // 先頭ポインタを左に移動し、境界を越える場合は配列の末尾に回る + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする front = index(front - 1); - // 先頭に num を追加 + // num をキュー先頭に追加 nums[front] = num; queSize++; } - /* 末尾エンキュー */ + /* キュー末尾にエンキュー */ public void pushLast(int num) { if (queSize == capacity()) { - System.out.println("両端キューが満杯です"); + System.out.println("双方向キューは満杯です"); return; } - // 末尾ポインタを計算し、末尾に要素を追加 + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す int rear = index(front + queSize); + // num をキュー末尾に追加 nums[rear] = num; queSize++; } - /* 先頭デキュー */ + /* キュー先頭からデキュー */ public int popFirst() { int num = peekFirst(); - // 先頭ポインタを右に移動 + // 先頭ポインタを 1 つ後ろへ進める front = index(front + 1); queSize--; return num; } - /* 末尾デキュー */ + /* キュー末尾からデキュー */ public int popLast() { int num = peekLast(); queSize--; return num; } - /* 先頭要素にアクセス */ + /* キュー先頭の要素にアクセス */ public int peekFirst() { if (isEmpty()) throw new IndexOutOfBoundsException(); return nums[front]; } - /* 末尾要素にアクセス */ + /* キュー末尾の要素にアクセス */ public int peekLast() { if (isEmpty()) throw new IndexOutOfBoundsException(); @@ -1159,9 +2475,9 @@ comments: true return nums[last]; } - /* 配列を返す */ + /* 出力用の配列を返す */ public int[] toArray() { - // front から開始して queSize 個の要素のみをコピー + // 有効長の範囲内のリスト要素のみを変換 int[] res = new int[queSize]; for (int i = 0, j = front; i < queSize; i++, j++) { res[i] = nums[index(j)]; @@ -1174,65 +2490,1119 @@ comments: true === "C#" ```csharp title="array_deque.cs" - [class]{ArrayDeque}-[func]{} + /* 循環配列ベースの両端キュー */ + class ArrayDeque { + int[] nums; // 両端キューの要素を格納する配列 + int front; // 先頭ポインタ。先頭要素を指す + int queSize; // 両端キューの長さ + + /* コンストラクタ */ + public ArrayDeque(int capacity) { + nums = new int[capacity]; + front = queSize = 0; + } + + /* 両端キューの容量を取得 */ + int Capacity() { + return nums.Length; + } + + /* 両端キューの長さを取得 */ + public int Size() { + return queSize; + } + + /* 両端キューが空かどうかを判定 */ + public bool IsEmpty() { + return queSize == 0; + } + + /* 循環配列のインデックスを計算 */ + int Index(int i) { + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えて前に出たら末尾に戻る + return (i + Capacity()) % Capacity(); + } + + /* キュー先頭にエンキュー */ + public void PushFirst(int num) { + if (queSize == Capacity()) { + Console.WriteLine("両端キューは満杯です"); + return; + } + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする + front = Index(front - 1); + // num をキュー先頭に追加 + nums[front] = num; + queSize++; + } + + /* キュー末尾にエンキュー */ + public void PushLast(int num) { + if (queSize == Capacity()) { + Console.WriteLine("両端キューは満杯です"); + return; + } + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す + int rear = Index(front + queSize); + // num をキュー末尾に追加 + nums[rear] = num; + queSize++; + } + + /* キュー先頭からデキュー */ + public int PopFirst() { + int num = PeekFirst(); + // 先頭ポインタを 1 つ後ろへ進める + front = Index(front + 1); + queSize--; + return num; + } + + /* キュー末尾からデキュー */ + public int PopLast() { + int num = PeekLast(); + queSize--; + return num; + } + + /* キュー先頭の要素にアクセス */ + public int PeekFirst() { + if (IsEmpty()) { + throw new InvalidOperationException(); + } + return nums[front]; + } + + /* キュー末尾の要素にアクセス */ + public int PeekLast() { + if (IsEmpty()) { + throw new InvalidOperationException(); + } + // 末尾要素のインデックスを計算 + int last = Index(front + queSize - 1); + return nums[last]; + } + + /* 出力用の配列を返す */ + public int[] ToArray() { + // 有効長の範囲内のリスト要素のみを変換 + int[] res = new int[queSize]; + for (int i = 0, j = front; i < queSize; i++, j++) { + res[i] = nums[Index(j)]; + } + return res; + } + } ``` === "Go" ```go title="array_deque.go" - [class]{arrayDeque}-[func]{} + /* 循環配列ベースの両端キュー */ + type arrayDeque struct { + nums []int // 両端キューの要素を格納する配列 + front int // 先頭ポインタ。先頭要素を指す + queSize int // 両端キューの長さ + queCapacity int // キュー容量(格納できる要素数の上限) + } + + /* キューを初期化 */ + func newArrayDeque(queCapacity int) *arrayDeque { + return &arrayDeque{ + nums: make([]int, queCapacity), + queCapacity: queCapacity, + front: 0, + queSize: 0, + } + } + + /* 両端キューの長さを取得 */ + func (q *arrayDeque) size() int { + return q.queSize + } + + /* 両端キューが空かどうかを判定 */ + func (q *arrayDeque) isEmpty() bool { + return q.queSize == 0 + } + + /* 循環配列のインデックスを計算 */ + func (q *arrayDeque) index(i int) int { + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えて前に出たら末尾に戻る + return (i + q.queCapacity) % q.queCapacity + } + + /* キュー先頭にエンキュー */ + func (q *arrayDeque) pushFirst(num int) { + if q.queSize == q.queCapacity { + fmt.Println("両端キューは満杯です") + return + } + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする + q.front = q.index(q.front - 1) + // num をキュー先頭に追加 + q.nums[q.front] = num + q.queSize++ + } + + /* キュー末尾にエンキュー */ + func (q *arrayDeque) pushLast(num int) { + if q.queSize == q.queCapacity { + fmt.Println("両端キューは満杯です") + return + } + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す + rear := q.index(q.front + q.queSize) + // num をキュー末尾に追加 + q.nums[rear] = num + q.queSize++ + } + + /* キュー先頭からデキュー */ + func (q *arrayDeque) popFirst() any { + num := q.peekFirst() + if num == nil { + return nil + } + // 先頭ポインタを 1 つ後ろへ進める + q.front = q.index(q.front + 1) + q.queSize-- + return num + } + + /* キュー末尾からデキュー */ + func (q *arrayDeque) popLast() any { + num := q.peekLast() + if num == nil { + return nil + } + q.queSize-- + return num + } + + /* キュー先頭の要素にアクセス */ + func (q *arrayDeque) peekFirst() any { + if q.isEmpty() { + return nil + } + return q.nums[q.front] + } + + /* キュー末尾の要素にアクセス */ + func (q *arrayDeque) peekLast() any { + if q.isEmpty() { + return nil + } + // 末尾要素のインデックスを計算 + last := q.index(q.front + q.queSize - 1) + return q.nums[last] + } + + /* 表示用に Slice を取得 */ + func (q *arrayDeque) toSlice() []int { + // 有効長の範囲内のリスト要素のみを変換 + res := make([]int, q.queSize) + for i, j := 0, q.front; i < q.queSize; i++ { + res[i] = q.nums[q.index(j)] + j++ + } + return res + } ``` === "Swift" ```swift title="array_deque.swift" - [class]{ArrayDeque}-[func]{} + /* 循環配列ベースの両端キュー */ + class ArrayDeque { + private var nums: [Int] // 両端キューの要素を格納する配列 + private var front: Int // 先頭ポインタ。先頭要素を指す + private var _size: Int // 両端キューの長さ + + /* コンストラクタ */ + init(capacity: Int) { + nums = Array(repeating: 0, count: capacity) + front = 0 + _size = 0 + } + + /* 両端キューの容量を取得 */ + func capacity() -> Int { + nums.count + } + + /* 両端キューの長さを取得 */ + func size() -> Int { + _size + } + + /* 両端キューが空かどうかを判定 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* 循環配列のインデックスを計算 */ + private func index(i: Int) -> Int { + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えて前に出たら末尾に戻る + (i + capacity()) % capacity() + } + + /* キュー先頭にエンキュー */ + func pushFirst(num: Int) { + if size() == capacity() { + print("両端キューがいっぱいです") + return + } + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする + front = index(i: front - 1) + // num をキュー先頭に追加 + nums[front] = num + _size += 1 + } + + /* キュー末尾にエンキュー */ + func pushLast(num: Int) { + if size() == capacity() { + print("両端キューがいっぱいです") + return + } + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す + let rear = index(i: front + size()) + // num をキュー末尾に追加 + nums[rear] = num + _size += 1 + } + + /* キュー先頭からデキュー */ + func popFirst() -> Int { + let num = peekFirst() + // 先頭ポインタを 1 つ後ろへ進める + front = index(i: front + 1) + _size -= 1 + return num + } + + /* キュー末尾からデキュー */ + func popLast() -> Int { + let num = peekLast() + _size -= 1 + return num + } + + /* キュー先頭の要素にアクセス */ + func peekFirst() -> Int { + if isEmpty() { + fatalError("両端キューが空です") + } + return nums[front] + } + + /* キュー末尾の要素にアクセス */ + func peekLast() -> Int { + if isEmpty() { + fatalError("両端キューが空です") + } + // 末尾要素のインデックスを計算 + let last = index(i: front + size() - 1) + return nums[last] + } + + /* 出力用の配列を返す */ + func toArray() -> [Int] { + // 有効長の範囲内のリスト要素のみを変換 + (front ..< front + size()).map { nums[index(i: $0)] } + } + } ``` === "JS" ```javascript title="array_deque.js" - [class]{ArrayDeque}-[func]{} + /* 循環配列ベースの両端キュー */ + class ArrayDeque { + #nums; // 両端キューの要素を格納する配列 + #front; // 先頭ポインタ。先頭要素を指す + #queSize; // 両端キューの長さ + + /* コンストラクタ */ + constructor(capacity) { + this.#nums = new Array(capacity); + this.#front = 0; + this.#queSize = 0; + } + + /* 両端キューの容量を取得 */ + capacity() { + return this.#nums.length; + } + + /* 両端キューの長さを取得 */ + size() { + return this.#queSize; + } + + /* 両端キューが空かどうかを判定 */ + isEmpty() { + return this.#queSize === 0; + } + + /* 循環配列のインデックスを計算 */ + index(i) { + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えて前に出たら末尾に戻る + return (i + this.capacity()) % this.capacity(); + } + + /* キュー先頭にエンキュー */ + pushFirst(num) { + if (this.#queSize === this.capacity()) { + console.log('両端キューがいっぱいです'); + return; + } + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする + this.#front = this.index(this.#front - 1); + // num をキュー先頭に追加 + this.#nums[this.#front] = num; + this.#queSize++; + } + + /* キュー末尾にエンキュー */ + pushLast(num) { + if (this.#queSize === this.capacity()) { + console.log('両端キューがいっぱいです'); + return; + } + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す + const rear = this.index(this.#front + this.#queSize); + // num をキュー末尾に追加 + this.#nums[rear] = num; + this.#queSize++; + } + + /* キュー先頭からデキュー */ + popFirst() { + const num = this.peekFirst(); + // 先頭ポインタを 1 つ後ろへ進める + this.#front = this.index(this.#front + 1); + this.#queSize--; + return num; + } + + /* キュー末尾からデキュー */ + popLast() { + const num = this.peekLast(); + this.#queSize--; + return num; + } + + /* キュー先頭の要素にアクセス */ + peekFirst() { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + return this.#nums[this.#front]; + } + + /* キュー末尾の要素にアクセス */ + peekLast() { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + // 末尾要素のインデックスを計算 + const last = this.index(this.#front + this.#queSize - 1); + return this.#nums[last]; + } + + /* 出力用の配列を返す */ + toArray() { + // 有効長の範囲内のリスト要素のみを変換 + const res = []; + for (let i = 0, j = this.#front; i < this.#queSize; i++, j++) { + res[i] = this.#nums[this.index(j)]; + } + return res; + } + } ``` === "TS" ```typescript title="array_deque.ts" - [class]{ArrayDeque}-[func]{} + /* 循環配列ベースの両端キュー */ + class ArrayDeque { + private nums: number[]; // 両端キューの要素を格納する配列 + private front: number; // 先頭ポインタ。先頭要素を指す + private queSize: number; // 両端キューの長さ + + /* コンストラクタ */ + constructor(capacity: number) { + this.nums = new Array(capacity); + this.front = 0; + this.queSize = 0; + } + + /* 両端キューの容量を取得 */ + capacity(): number { + return this.nums.length; + } + + /* 両端キューの長さを取得 */ + size(): number { + return this.queSize; + } + + /* 両端キューが空かどうかを判定 */ + isEmpty(): boolean { + return this.queSize === 0; + } + + /* 循環配列のインデックスを計算 */ + index(i: number): number { + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えて前に出たら末尾に戻る + return (i + this.capacity()) % this.capacity(); + } + + /* キュー先頭にエンキュー */ + pushFirst(num: number): void { + if (this.queSize === this.capacity()) { + console.log('両端キューは満杯です'); + return; + } + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする + this.front = this.index(this.front - 1); + // num をキュー先頭に追加 + this.nums[this.front] = num; + this.queSize++; + } + + /* キュー末尾にエンキュー */ + pushLast(num: number): void { + if (this.queSize === this.capacity()) { + console.log('両端キューは満杯です'); + return; + } + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す + const rear: number = this.index(this.front + this.queSize); + // num をキュー末尾に追加 + this.nums[rear] = num; + this.queSize++; + } + + /* キュー先頭からデキュー */ + popFirst(): number { + const num: number = this.peekFirst(); + // 先頭ポインタを 1 つ後ろへ進める + this.front = this.index(this.front + 1); + this.queSize--; + return num; + } + + /* キュー末尾からデキュー */ + popLast(): number { + const num: number = this.peekLast(); + this.queSize--; + return num; + } + + /* キュー先頭の要素にアクセス */ + peekFirst(): number { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + return this.nums[this.front]; + } + + /* キュー末尾の要素にアクセス */ + peekLast(): number { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + // 末尾要素のインデックスを計算 + const last = this.index(this.front + this.queSize - 1); + return this.nums[last]; + } + + /* 出力用の配列を返す */ + toArray(): number[] { + // 有効長の範囲内のリスト要素のみを変換 + const res: number[] = []; + for (let i = 0, j = this.front; i < this.queSize; i++, j++) { + res[i] = this.nums[this.index(j)]; + } + return res; + } + } ``` === "Dart" ```dart title="array_deque.dart" - [class]{ArrayDeque}-[func]{} + /* 循環配列ベースの両端キュー */ + class ArrayDeque { + late List _nums; // 両端キューの要素を格納する配列 + late int _front; // 先頭ポインタ。先頭要素を指す + late int _queSize; // 両端キューの長さ + + /* コンストラクタ */ + ArrayDeque(int capacity) { + this._nums = List.filled(capacity, 0); + this._front = this._queSize = 0; + } + + /* 両端キューの容量を取得 */ + int capacity() { + return _nums.length; + } + + /* 両端キューの長さを取得 */ + int size() { + return _queSize; + } + + /* 両端キューが空かどうかを判定 */ + bool isEmpty() { + return _queSize == 0; + } + + /* 循環配列のインデックスを計算 */ + int index(int i) { + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えて前に出たら末尾に戻る + return (i + capacity()) % capacity(); + } + + /* キュー先頭にエンキュー */ + void pushFirst(int _num) { + if (_queSize == capacity()) { + throw Exception("両端キューがいっぱいです"); + } + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により _front が配列の先頭を越えたあと末尾に戻るようにする + _front = index(_front - 1); + // _num をキューの先頭に追加 + _nums[_front] = _num; + _queSize++; + } + + /* キュー末尾にエンキュー */ + void pushLast(int _num) { + if (_queSize == capacity()) { + throw Exception("両端キューがいっぱいです"); + } + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す + int rear = index(_front + _queSize); + // _num をキュー末尾に追加 + _nums[rear] = _num; + _queSize++; + } + + /* キュー先頭からデキュー */ + int popFirst() { + int _num = peekFirst(); + // 先頭ポインタを右に 1 つ移動する + _front = index(_front + 1); + _queSize--; + return _num; + } + + /* キュー末尾からデキュー */ + int popLast() { + int _num = peekLast(); + _queSize--; + return _num; + } + + /* キュー先頭の要素にアクセス */ + int peekFirst() { + if (isEmpty()) { + throw Exception("両端キューが空です"); + } + return _nums[_front]; + } + + /* キュー末尾の要素にアクセス */ + int peekLast() { + if (isEmpty()) { + throw Exception("両端キューが空です"); + } + // 末尾要素のインデックスを計算 + int last = index(_front + _queSize - 1); + return _nums[last]; + } + + /* 出力用の配列を返す */ + List toArray() { + // 有効長の範囲内のリスト要素のみを変換 + List res = List.filled(_queSize, 0); + for (int i = 0, j = _front; i < _queSize; i++, j++) { + res[i] = _nums[index(j)]; + } + return res; + } + } ``` === "Rust" ```rust title="array_deque.rs" - [class]{ArrayDeque}-[func]{} + /* 循環配列ベースの両端キュー */ + struct ArrayDeque { + nums: Vec, // 両端キューの要素を格納する配列 + front: usize, // 先頭ポインタ。先頭要素を指す + que_size: usize, // 両端キューの長さ + } + + impl ArrayDeque { + /* コンストラクタ */ + pub fn new(capacity: usize) -> Self { + Self { + nums: vec![T::default(); capacity], + front: 0, + que_size: 0, + } + } + + /* 両端キューの容量を取得 */ + pub fn capacity(&self) -> usize { + self.nums.len() + } + + /* 両端キューの長さを取得 */ + pub fn size(&self) -> usize { + self.que_size + } + + /* 両端キューが空かどうかを判定 */ + pub fn is_empty(&self) -> bool { + self.que_size == 0 + } + + /* 循環配列のインデックスを計算 */ + fn index(&self, i: i32) -> usize { + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えて前に出たら末尾に戻る + ((i + self.capacity() as i32) % self.capacity() as i32) as usize + } + + /* キュー先頭にエンキュー */ + pub fn push_first(&mut self, num: T) { + if self.que_size == self.capacity() { + println!("両端キューがいっぱいです"); + return; + } + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする + self.front = self.index(self.front as i32 - 1); + // num をキュー先頭に追加 + self.nums[self.front] = num; + self.que_size += 1; + } + + /* キュー末尾にエンキュー */ + pub fn push_last(&mut self, num: T) { + if self.que_size == self.capacity() { + println!("両端キューがいっぱいです"); + return; + } + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す + let rear = self.index(self.front as i32 + self.que_size as i32); + // num をキュー末尾に追加 + self.nums[rear] = num; + self.que_size += 1; + } + + /* キュー先頭からデキュー */ + fn pop_first(&mut self) -> T { + let num = self.peek_first(); + // 先頭ポインタを 1 つ後ろへ進める + self.front = self.index(self.front as i32 + 1); + self.que_size -= 1; + num + } + + /* キュー末尾からデキュー */ + fn pop_last(&mut self) -> T { + let num = self.peek_last(); + self.que_size -= 1; + num + } + + /* キュー先頭の要素にアクセス */ + fn peek_first(&self) -> T { + if self.is_empty() { + panic!("両端キューが空です") + }; + self.nums[self.front] + } + + /* キュー末尾の要素にアクセス */ + fn peek_last(&self) -> T { + if self.is_empty() { + panic!("両端キューが空です") + }; + // 末尾要素のインデックスを計算 + let last = self.index(self.front as i32 + self.que_size as i32 - 1); + self.nums[last] + } + + /* 出力用の配列を返す */ + fn to_array(&self) -> Vec { + // 有効長の範囲内のリスト要素のみを変換 + let mut res = vec![T::default(); self.que_size]; + let mut j = self.front; + for i in 0..self.que_size { + res[i] = self.nums[self.index(j as i32)]; + j += 1; + } + res + } + } ``` === "C" ```c title="array_deque.c" - [class]{ArrayDeque}-[func]{} + /* 循環配列ベースの両端キュー */ + typedef struct { + int *nums; // キュー要素を格納する配列 + int front; // 先頭ポインタ。先頭要素を指す + int queSize; // 末尾ポインタ。キューの末尾 + 1 を指す + int queCapacity; // キューの容量 + } ArrayDeque; + + /* コンストラクタ */ + ArrayDeque *newArrayDeque(int capacity) { + ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque)); + // 配列を初期化 + deque->queCapacity = capacity; + deque->nums = (int *)malloc(sizeof(int) * deque->queCapacity); + deque->front = deque->queSize = 0; + return deque; + } + + /* デストラクタ */ + void delArrayDeque(ArrayDeque *deque) { + free(deque->nums); + free(deque); + } + + /* 両端キューの容量を取得 */ + int capacity(ArrayDeque *deque) { + return deque->queCapacity; + } + + /* 両端キューの長さを取得 */ + int size(ArrayDeque *deque) { + return deque->queSize; + } + + /* 両端キューが空かどうかを判定 */ + bool empty(ArrayDeque *deque) { + return deque->queSize == 0; + } + + /* 循環配列のインデックスを計算 */ + int dequeIndex(ArrayDeque *deque, int i) { + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えたら末尾に戻る + return ((i + capacity(deque)) % capacity(deque)); + } + + /* キュー先頭にエンキュー */ + void pushFirst(ArrayDeque *deque, int num) { + if (deque->queSize == capacity(deque)) { + printf("両端キューがいっぱいです\r\n"); + return; + } + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により front が配列の先頭を越えたあと末尾に戻るようにする + deque->front = dequeIndex(deque, deque->front - 1); + // num をキューの先頭に追加 + deque->nums[deque->front] = num; + deque->queSize++; + } + + /* キュー末尾にエンキュー */ + void pushLast(ArrayDeque *deque, int num) { + if (deque->queSize == capacity(deque)) { + printf("両端キューがいっぱいです\r\n"); + return; + } + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す + int rear = dequeIndex(deque, deque->front + deque->queSize); + // num をキュー末尾に追加 + deque->nums[rear] = num; + deque->queSize++; + } + + /* キュー先頭の要素にアクセス */ + int peekFirst(ArrayDeque *deque) { + // アクセス例外:双方向キューが空です + assert(empty(deque) == 0); + return deque->nums[deque->front]; + } + + /* キュー末尾の要素にアクセス */ + int peekLast(ArrayDeque *deque) { + // アクセス例外:双方向キューが空です + assert(empty(deque) == 0); + int last = dequeIndex(deque, deque->front + deque->queSize - 1); + return deque->nums[last]; + } + + /* キュー先頭からデキュー */ + int popFirst(ArrayDeque *deque) { + int num = peekFirst(deque); + // 先頭ポインタを 1 つ後ろへ進める + deque->front = dequeIndex(deque, deque->front + 1); + deque->queSize--; + return num; + } + + /* キュー末尾からデキュー */ + int popLast(ArrayDeque *deque) { + int num = peekLast(deque); + deque->queSize--; + return num; + } + + /* 出力用の配列を返す */ + int *toArray(ArrayDeque *deque, int *queSize) { + *queSize = deque->queSize; + int *res = (int *)calloc(deque->queSize, sizeof(int)); + int j = deque->front; + for (int i = 0; i < deque->queSize; i++) { + res[i] = deque->nums[j % deque->queCapacity]; + j++; + } + return res; + } ``` === "Kotlin" ```kotlin title="array_deque.kt" - [class]{ArrayDeque}-[func]{} + /* コンストラクタ */ + class ArrayDeque(capacity: Int) { + private var nums: IntArray = IntArray(capacity) // 両端キューの要素を格納する配列 + private var front: Int = 0 // 先頭ポインタ。先頭要素を指す + private var queSize: Int = 0 // 両端キューの長さ + + /* 両端キューの容量を取得 */ + fun capacity(): Int { + return nums.size + } + + /* 両端キューの長さを取得 */ + fun size(): Int { + return queSize + } + + /* 両端キューが空かどうかを判定 */ + fun isEmpty(): Boolean { + return queSize == 0 + } + + /* 循環配列のインデックスを計算 */ + private fun index(i: Int): Int { + // 剰余演算により配列の先頭と末尾をつなげる + // i が配列の末尾を越えたら先頭に戻る + // i が配列の先頭を越えて前に出たら末尾に戻る + return (i + capacity()) % capacity() + } + + /* キュー先頭にエンキュー */ + fun pushFirst(num: Int) { + if (queSize == capacity()) { + println("双方向キューは満杯です") + return + } + // 先頭ポインタを左に 1 つ移動する + // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする + front = index(front - 1) + // num をキュー先頭に追加 + nums[front] = num + queSize++ + } + + /* キュー末尾にエンキュー */ + fun pushLast(num: Int) { + if (queSize == capacity()) { + println("双方向キューは満杯です") + return + } + // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す + val rear = index(front + queSize) + // num をキュー末尾に追加 + nums[rear] = num + queSize++ + } + + /* キュー先頭からデキュー */ + fun popFirst(): Int { + val num = peekFirst() + // 先頭ポインタを 1 つ後ろへ進める + front = index(front + 1) + queSize-- + return num + } + + /* キュー末尾からデキュー */ + fun popLast(): Int { + val num = peekLast() + queSize-- + return num + } + + /* キュー先頭の要素にアクセス */ + fun peekFirst(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return nums[front] + } + + /* キュー末尾の要素にアクセス */ + fun peekLast(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + // 末尾要素のインデックスを計算 + val last = index(front + queSize - 1) + return nums[last] + } + + /* 出力用の配列を返す */ + fun toArray(): IntArray { + // 有効長の範囲内のリスト要素のみを変換 + val res = IntArray(queSize) + var i = 0 + var j = front + while (i < queSize) { + res[i] = nums[index(j)] + i++ + j++ + } + return res + } + } ``` === "Ruby" ```ruby title="array_deque.rb" - [class]{ArrayDeque}-[func]{} + ### 循環配列で実装した両端キュー ### + class ArrayDeque + ### 両端キューの長さを取得 ### + attr_reader :size + + ### コンストラクタ ### + def initialize(capacity) + @nums = Array.new(capacity, 0) + @front = 0 + @size = 0 + end + + ### 両端キューの容量を取得 ### + def capacity + @nums.length + end + + ### 両端キューが空か判定 ### + def is_empty? + size.zero? + end + + ### キュー先頭に追加 ### + def push_first(num) + if size == capacity + puts '両端キューがいっぱいです' + return + end + + # 先頭ポインタを左に 1 つ移動する + # 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする + @front = index(@front - 1) + # num をキュー先頭に追加 + @nums[@front] = num + @size += 1 + end + + ### キュー末尾に追加 ### + def push_last(num) + if size == capacity + puts '両端キューがいっぱいです' + return + end + + # キュー末尾ポインタを計算し、末尾インデックス + 1 を指す + rear = index(@front + size) + # num をキュー末尾に追加 + @nums[rear] = num + @size += 1 + end + + ### キュー先頭から取り出す ### + def pop_first + num = peek_first + # 先頭ポインタを 1 つ後ろへ進める + @front = index(@front + 1) + @size -= 1 + num + end + + ### キューの末尾から取り出す ### + def pop_last + num = peek_last + @size -= 1 + num + end + + ### 先頭要素にアクセス ### + def peek_first + raise IndexError, '両端キューは空です' if is_empty? + + @nums[@front] + end + + ### キュー末尾要素を参照 ### + def peek_last + raise IndexError, '両端キューは空です' if is_empty? + + # 末尾要素のインデックスを計算 + last = index(@front + size - 1) + @nums[last] + end + + ### 表示用の配列を返す ### + def to_array + # 有効長の範囲内のリスト要素のみを変換 + res = [] + for i in 0...size + res << @nums[index(@front + i)] + end + res + end + + private + + ### 循環配列のインデックスを計算 ### + def index(i) + # 剰余演算により配列の先頭と末尾をつなげる + # i が配列の末尾を越えたら先頭に戻る + # i が配列の先頭を越えて前に出たら末尾に戻る + (i + capacity) % capacity + end + end ``` ## 5.3.3   両端キューの応用 -両端キューはスタックとキューの両方のロジックを組み合わせているため、**それぞれのすべてのユースケースを実装でき、より大きな柔軟性を提供します**。 +両端キューはスタックとキューの両方の論理を備えているため、**これら 2 つのすべての応用場面を実現でき、さらに高い自由度を提供します**。 -ソフトウェアの「元に戻す」機能は通常スタックを使って実装されることを知っています:システムは各変更操作をスタックに`push`し、次に`pop`して元に戻すことを実装します。しかし、システムリソースの制限を考慮して、ソフトウェアは元に戻すステップの数を制限することがよくあります(例えば、最後の50ステップのみを許可)。スタックの長さが50を超えた場合、ソフトウェアはスタックの底部(キューの前端)で削除操作を実行する必要があります。**しかし、通常のスタックではこの機能を実行できないため、両端キューが必要になります**。「元に戻す」のコアロジックは依然としてスタックの後入れ先出し原則に従いますが、両端キューはより柔軟にいくつかの追加ロジックを実装できることに注意してください。 +私たちが知っているように、ソフトウェアの「元に戻す」機能は通常スタックを使って実装されます。システムは変更操作を毎回スタックに `push` し、その後 `pop` によって取り消しを実現します。しかし、システム資源の制約を考慮すると、通常ソフトウェアは取り消し可能な手数を制限します(たとえば $50$ 手まで保存可能)。スタックの長さが $50$ を超えると、ソフトウェアはスタックの底部(先頭)で削除操作を行う必要があります。**しかしスタックではこの機能を実現できないため、この場合はスタックの代わりに両端キューを使用する必要があります**。なお、「元に戻す」の中核ロジック自体は依然としてスタックの後入れ先出し原則に従っており、両端キューは追加のロジックをより柔軟に実装できるだけです。 diff --git a/ja/docs/chapter_stack_and_queue/index.md b/ja/docs/chapter_stack_and_queue/index.md index aa966a5ba..d057e9475 100644 --- a/ja/docs/chapter_stack_and_queue/index.md +++ b/ja/docs/chapter_stack_and_queue/index.md @@ -9,9 +9,9 @@ icon: material/stack-overflow !!! abstract - スタックは積み重ねられた猫のようなもので、キューは一列に並んだ猫のようなものです。 - - それらはそれぞれ、後入先出(LIFO)と先入先出(FIFO)の論理関係を表しています。 + スタックは猫を積み重ねるようなもので、キューは猫が列に並ぶようなものです。 + + 両者はそれぞれ、後入れ先出しと先入れ先出しの論理関係を表します。 ## 章の内容 diff --git a/ja/docs/chapter_stack_and_queue/queue.md b/ja/docs/chapter_stack_and_queue/queue.md index 3f8def737..9be036258 100644 --- a/ja/docs/chapter_stack_and_queue/queue.md +++ b/ja/docs/chapter_stack_and_queue/queue.md @@ -4,31 +4,31 @@ comments: true # 5.2   キュー -キューは、先入先出(FIFO)ルールに従う線形データ構造です。名前が示すように、キューは行列の現象をシミュレートし、新参者は列の後ろに並び、前の人が最初に列を離れます。 +キュー(queue)は、先入れ先出しの規則に従う線形データ構造です。名前のとおり、キューは順番待ちの現象を模したもので、新しく来た人は絶えずキュー末尾に加わり、キュー先頭にいる人から順に離れていきます。 -下図に示すように、キューの前面を「ヘッド」、後面を「テール」と呼びます。キューの後ろに要素を追加する操作を「エンキュー」、前から要素を削除する操作を「デキュー」と呼びます。 +下図のように、キューの先頭を「キュー先頭」、末尾を「キュー末尾」と呼びます。要素をキュー末尾に加える操作を「エンキュー」、キュー先頭の要素を削除する操作を「デキュー」と呼びます。 -![キューの先入先出ルール](queue.assets/queue_operations.png){ class="animation-figure" } +![キューの先入れ先出し規則](queue.assets/queue_operations.png){ class="animation-figure" } -

図 5-4   キューの先入先出ルール

+

図 5-4   キューの先入れ先出し規則

-## 5.2.1   キューの一般的な操作 +## 5.2.1   キューの基本操作 -キューの一般的な操作を下表に示します。メソッド名はプログラミング言語によって異なる場合があることに注意してください。ここでは、スタックで使用したのと同じ命名規則を使用します。 +キューの基本操作を以下の表に示します。なお、メソッド名はプログラミング言語によって異なる場合があります。ここではスタックと同じ命名を採用します。

表 5-2   キュー操作の効率

-| メソッド名 | 説明 | 時間計算量 | -| ----------- | -------------------------------------- | --------------- | -| `push()` | 要素をエンキュー、テールに追加 | $O(1)$ | -| `pop()` | ヘッド要素をデキュー | $O(1)$ | -| `peek()` | ヘッド要素にアクセス | $O(1)$ | +| メソッド名 | 説明 | 時間計算量 | +| -------- | ---------------------------- | ---------- | +| `push()` | 要素をエンキューし、キュー末尾に追加する | $O(1)$ | +| `pop()` | キュー先頭の要素をデキューする | $O(1)$ | +| `peek()` | キュー先頭の要素にアクセスする | $O(1)$ |
-プログラミング言語で用意されているキュークラスを直接使用できます: +プログラミング言語に用意された既存のキュークラスをそのまま利用できます: === "Python" @@ -36,8 +36,8 @@ comments: true from collections import deque # キューを初期化 - # Pythonでは、一般的にdequeクラスをキューとして使用します - # queue.Queue()は純粋なキュークラスですが、使いにくいため推奨されません + # Python では、通常は双方向キュークラス deque をキューとして使用する + # queue.Queue() は純粋なキュークラスだが、やや使いにくいため非推奨 que: deque[int] = deque() # 要素をエンキュー @@ -47,7 +47,7 @@ comments: true que.append(5) que.append(4) - # 最初の要素にアクセス + # キュー先頭の要素にアクセス front: int = que[0] # 要素をデキュー @@ -56,7 +56,7 @@ comments: true # キューの長さを取得 size: int = len(que) - # キューが空かどうかチェック + # キューが空かどうかを判定 is_empty: bool = len(que) == 0 ``` @@ -73,7 +73,7 @@ comments: true queue.push(5); queue.push(4); - /* 最初の要素にアクセス */ + /* キュー先頭の要素にアクセス */ int front = queue.front(); /* 要素をデキュー */ @@ -82,7 +82,7 @@ comments: true /* キューの長さを取得 */ int size = queue.size(); - /* キューが空かどうかチェック */ + /* キューが空かどうかを判定 */ bool empty = queue.empty(); ``` @@ -99,7 +99,7 @@ comments: true queue.offer(5); queue.offer(4); - /* 最初の要素にアクセス */ + /* キュー先頭の要素にアクセス */ int peek = queue.peek(); /* 要素をデキュー */ @@ -108,7 +108,7 @@ comments: true /* キューの長さを取得 */ int size = queue.size(); - /* キューが空かどうかチェック */ + /* キューが空かどうかを判定 */ boolean isEmpty = queue.isEmpty(); ``` @@ -125,7 +125,7 @@ comments: true queue.Enqueue(5); queue.Enqueue(4); - /* 最初の要素にアクセス */ + /* キュー先頭の要素にアクセス */ int peek = queue.Peek(); /* 要素をデキュー */ @@ -134,7 +134,7 @@ comments: true /* キューの長さを取得 */ int size = queue.Count; - /* キューが空かどうかチェック */ + /* キューが空かどうかを判定 */ bool isEmpty = queue.Count == 0; ``` @@ -142,7 +142,7 @@ comments: true ```go title="queue_test.go" /* キューを初期化 */ - // Goでは、listをキューとして使用 + // Go では、list をキューとして使用する queue := list.New() /* 要素をエンキュー */ @@ -152,7 +152,7 @@ comments: true queue.PushBack(5) queue.PushBack(4) - /* 最初の要素にアクセス */ + /* キュー先頭の要素にアクセス */ peek := queue.Front() /* 要素をデキュー */ @@ -162,7 +162,7 @@ comments: true /* キューの長さを取得 */ size := queue.Len() - /* キューが空かどうかチェック */ + /* キューが空かどうかを判定 */ isEmpty := queue.Len() == 0 ``` @@ -170,7 +170,7 @@ comments: true ```swift title="queue.swift" /* キューを初期化 */ - // Swiftには組み込みのキュークラスがないため、Arrayをキューとして使用 + // Swift には組み込みのキュークラスがないため、Array をキューとして使える var queue: [Int] = [] /* 要素をエンキュー */ @@ -180,17 +180,17 @@ comments: true queue.append(5) queue.append(4) - /* 最初の要素にアクセス */ + /* キュー先頭の要素にアクセス */ let peek = queue.first! /* 要素をデキュー */ - // 配列なので、removeFirstの計算量はO(n) + // 配列であるため、removeFirst の計算量は O(n) let pool = queue.removeFirst() /* キューの長さを取得 */ let size = queue.count - /* キューが空かどうかチェック */ + /* キューが空かどうかを判定 */ let isEmpty = queue.isEmpty ``` @@ -198,7 +198,7 @@ comments: true ```javascript title="queue.js" /* キューを初期化 */ - // JavaScriptには組み込みのキューがないため、Arrayをキューとして使用 + // JavaScript には組み込みのキューがないため、Array をキューとして使える const queue = []; /* 要素をエンキュー */ @@ -208,17 +208,17 @@ comments: true queue.push(5); queue.push(4); - /* 最初の要素にアクセス */ + /* キュー先頭の要素にアクセス */ const peek = queue[0]; /* 要素をデキュー */ - // 基礎構造が配列なので、shift()メソッドの時間計算量はO(n) + // 基盤は配列であるため、shift() メソッドの時間計算量は O(n) const pop = queue.shift(); /* キューの長さを取得 */ const size = queue.length; - /* キューが空かどうかチェック */ + /* キューが空かどうかを判定 */ const empty = queue.length === 0; ``` @@ -226,7 +226,7 @@ comments: true ```typescript title="queue.ts" /* キューを初期化 */ - // TypeScriptには組み込みのキューがないため、Arrayをキューとして使用 + // TypeScript には組み込みのキューがないため、Array をキューとして使える const queue: number[] = []; /* 要素をエンキュー */ @@ -236,17 +236,17 @@ comments: true queue.push(5); queue.push(4); - /* 最初の要素にアクセス */ + /* キュー先頭の要素にアクセス */ const peek = queue[0]; /* 要素をデキュー */ - // 基礎構造が配列なので、shift()メソッドの時間計算量はO(n) + // 基盤は配列であるため、shift() メソッドの時間計算量は O(n) const pop = queue.shift(); /* キューの長さを取得 */ const size = queue.length; - /* キューが空かどうかチェック */ + /* キューが空かどうかを判定 */ const empty = queue.length === 0; ``` @@ -254,7 +254,7 @@ comments: true ```dart title="queue.dart" /* キューを初期化 */ - // DartのQueueクラスは双方向キューですが、キューとして使用できます + // Dart では、キュークラス Qeque は双方向キューであり、キューとしても使用できる Queue queue = Queue(); /* 要素をエンキュー */ @@ -264,7 +264,7 @@ comments: true queue.add(5); queue.add(4); - /* 最初の要素にアクセス */ + /* キュー先頭の要素にアクセス */ int peek = queue.first; /* 要素をデキュー */ @@ -273,7 +273,7 @@ comments: true /* キューの長さを取得 */ int size = queue.length; - /* キューが空かどうかチェック */ + /* キューが空かどうかを判定 */ bool isEmpty = queue.isEmpty; ``` @@ -281,7 +281,7 @@ comments: true ```rust title="queue.rs" /* 双方向キューを初期化 */ - // Rustでは、双方向キューを通常のキューとして使用 + // Rust では双方向キューを通常のキューとして使う let mut deque: VecDeque = VecDeque::new(); /* 要素をエンキュー */ @@ -291,7 +291,7 @@ comments: true deque.push_back(5); deque.push_back(4); - /* 最初の要素にアクセス */ + /* キュー先頭の要素にアクセス */ if let Some(front) = deque.front() { } @@ -302,53 +302,105 @@ comments: true /* キューの長さを取得 */ let size = deque.len(); - /* キューが空かどうかチェック */ + /* キューが空かどうかを判定 */ let is_empty = deque.is_empty(); ``` === "C" ```c title="queue.c" - // Cは組み込みのキューを提供していません + // C には組み込みのキューがない ``` === "Kotlin" ```kotlin title="queue.kt" + /* キューを初期化 */ + val queue = LinkedList() + /* 要素をエンキュー */ + queue.offer(1) + queue.offer(3) + queue.offer(2) + queue.offer(5) + queue.offer(4) + + /* キュー先頭の要素にアクセス */ + val peek = queue.peek() + + /* 要素をデキュー */ + val pop = queue.poll() + + /* キューの長さを取得 */ + val size = queue.size + + /* キューが空かどうかを判定 */ + val isEmpty = queue.isEmpty() ``` +=== "Ruby" + + ```ruby title="queue.rb" + # キューを初期化 + # Ruby 組み込みのキュー(Thread::Queue) には peek と走査メソッドがないため、Array をキューとして使える + queue = [] + + # 要素をエンキュー + queue.push(1) + queue.push(3) + queue.push(2) + queue.push(5) + queue.push(4) + + # キュー先頭の要素にアクセス + peek = queue.first + + # 要素をデキュー + # 注意:配列であるため、Array#shift メソッドの時間計算量は O(n) + pop = queue.shift + + # キューの長さを取得 + size = queue.length + + # キューが空かどうかを判定 + is_empty = queue.empty? + ``` + +??? pythontutor "可視化実行" + + https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%98%9F%E5%88%97%0A%20%20%20%20%23%20%E5%9C%A8%20Python%20%E4%B8%AD%EF%BC%8C%E6%88%91%E4%BB%AC%E4%B8%80%E8%88%AC%E5%B0%86%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%B1%BB%20deque%20%E7%9C%8B%E4%BD%9C%E9%98%9F%E5%88%97%E4%BD%BF%E7%94%A8%0A%20%20%20%20%23%20%E8%99%BD%E7%84%B6%20queue.Queue%28%29%20%E6%98%AF%E7%BA%AF%E6%AD%A3%E7%9A%84%E9%98%9F%E5%88%97%E7%B1%BB%EF%BC%8C%E4%BD%86%E4%B8%8D%E5%A4%AA%E5%A5%BD%E7%94%A8%0A%20%20%20%20que%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20que.append%281%29%0A%20%20%20%20que.append%283%29%0A%20%20%20%20que.append%282%29%0A%20%20%20%20que.append%285%29%0A%20%20%20%20que.append%284%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20que%5B0%5D%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%90%8E%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + ## 5.2.2   キューの実装 -キューを実装するには、一方の端で要素を追加し、もう一方の端で要素を削除できるデータ構造が必要です。連結リストと配列の両方がこの要件を満たします。 +キューを実装するには、一方の端で要素を追加し、もう一方の端で要素を削除できるデータ構造が必要です。連結リストと配列はいずれもこの条件を満たします。 -### 1.   連結リストベースの実装 +### 1.   連結リストに基づく実装 -下図に示すように、連結リストの「ヘッドノード」と「テールノード」をそれぞれキューの「フロント」と「リア」と考えることができます。ノードは後ろでのみ追加でき、前でのみ削除できるように規定されています。 +下図のように、連結リストの「先頭ノード」と「末尾ノード」をそれぞれ「キュー先頭」と「キュー末尾」とみなし、キュー末尾ではノードの追加のみ、キュー先頭ではノードの削除のみを行うようにします。 -=== "LinkedListQueue" - ![連結リストによるキュー実装のエンキューとデキュー操作](queue.assets/linkedlist_queue_step1.png){ class="animation-figure" } +=== "<1>" + ![連結リストでキューを実装したエンキューとデキュー操作](queue.assets/linkedlist_queue_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![linkedlist_queue_push](queue.assets/linkedlist_queue_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![linkedlist_queue_pop](queue.assets/linkedlist_queue_step3_pop.png){ class="animation-figure" } -

図 5-5   連結リストによるキュー実装のエンキューとデキュー操作

+

図 5-5   連結リストでキューを実装したエンキューとデキュー操作

-以下は、連結リストを使用してキューを実装するコードです: +以下は連結リストでキューを実装するコードです: === "Python" ```python title="linkedlist_queue.py" class LinkedListQueue: - """連結リストベースのキュークラス""" + """連結リストベースのキュー""" def __init__(self): """コンストラクタ""" - self._front: ListNode | None = None # ヘッドノード front - self._rear: ListNode | None = None # テールノード rear + self._front: ListNode | None = None # 先頭ノード front + self._rear: ListNode | None = None # 末尾ノード rear self._size: int = 0 def size(self) -> int: @@ -361,13 +413,13 @@ comments: true def push(self, num: int): """エンキュー""" - # テールノードの後ろに num を追加 + # 末尾ノードの後ろに num を追加 node = ListNode(num) - # キューが空の場合、ヘッドとテールノードの両方をそのノードに向ける + # キューが空なら、先頭・末尾ノードをともにそのノードに設定 if self._front is None: self._front = node self._rear = node - # キューが空でない場合、そのノードをテールノードの後ろに追加 + # キューが空でなければ、そのノードを末尾ノードの後ろに追加 else: self._rear.next = node self._rear = node @@ -376,19 +428,19 @@ comments: true def pop(self) -> int: """デキュー""" num = self.peek() - # ヘッドノードを削除 + # 先頭ノードを削除 self._front = self._front.next self._size -= 1 return num def peek(self) -> int: - """フロント要素にアクセス""" + """キュー先頭の要素にアクセス""" if self.is_empty(): - raise IndexError("Queue is empty") + raise IndexError("キューが空です") return self._front.val def to_list(self) -> list[int]: - """出力用のリストに変換""" + """表示用にリストへ変換""" queue = [] temp = self._front while temp: @@ -400,10 +452,10 @@ comments: true === "C++" ```cpp title="linkedlist_queue.cpp" - /* 連結リストに基づくキュークラス */ + /* 連結リストベースのキュー */ class LinkedListQueue { private: - ListNode *front, *rear; // 先頭ノードfront、末尾ノードrear + ListNode *front, *rear; // 先頭ノード front、末尾ノード rear int queSize; public: @@ -414,7 +466,7 @@ comments: true } ~LinkedListQueue() { - // 連結リストを走査、ノードを削除、メモリを解放 + // 連結リストを走査してノードを削除し、メモリを解放する freeMemoryLinkedList(front); } @@ -430,14 +482,14 @@ comments: true /* エンキュー */ void push(int num) { - // 末尾ノードの後ろにnumを追加 + // 末尾ノードの後ろに num を追加 ListNode *node = new ListNode(num); - // キューが空の場合、先頭と末尾ノードの両方をそのノードに向ける + // キューが空なら、先頭・末尾ノードをともにそのノードに設定 if (front == nullptr) { front = node; rear = node; } - // キューが空でない場合、そのノードを末尾ノードの後ろに追加 + // キューが空でなければ、そのノードを末尾ノードの後ろに追加 else { rear->next = node; rear = node; @@ -451,20 +503,20 @@ comments: true // 先頭ノードを削除 ListNode *tmp = front; front = front->next; - // メモリを解放 + // メモリを解放する delete tmp; queSize--; return num; } - /* 先頭要素にアクセス */ + /* キュー先頭の要素にアクセス */ int peek() { if (size() == 0) - throw out_of_range("Queue is empty"); + throw out_of_range("キューが空です"); return front->val; } - /* 連結リストをVectorに変換して返却 */ + /* 連結リストを Vector に変換して返す */ vector toVector() { ListNode *node = front; vector res(size()); @@ -480,7 +532,7 @@ comments: true === "Java" ```java title="linkedlist_queue.java" - /* 連結リストに基づくキュークラス */ + /* 連結リストベースのキュー */ class LinkedListQueue { private ListNode front, rear; // 先頭ノード front、末尾ノード rear private int queSize = 0; @@ -504,11 +556,11 @@ comments: true public void push(int num) { // 末尾ノードの後ろに num を追加 ListNode node = new ListNode(num); - // キューが空の場合、先頭と末尾ノードの両方をそのノードにポイント + // キューが空なら、先頭・末尾ノードをともにそのノードに設定 if (front == null) { front = node; rear = node; - // キューが空でない場合、そのノードを末尾ノードの後ろに追加 + // キューが空でなければ、そのノードを末尾ノードの後ろに追加 } else { rear.next = node; rear = node; @@ -525,14 +577,14 @@ comments: true return num; } - /* 先頭要素にアクセス */ + /* キュー先頭の要素にアクセス */ public int peek() { if (isEmpty()) throw new IndexOutOfBoundsException(); return front.val; } - /* 連結リストを配列に変換して返す */ + /* 連結リストを Array に変換して返す */ public int[] toArray() { ListNode node = front; int[] res = new int[size()]; @@ -548,101 +600,759 @@ comments: true === "C#" ```csharp title="linkedlist_queue.cs" - [class]{LinkedListQueue}-[func]{} + /* 連結リストベースのキュー */ + class LinkedListQueue { + ListNode? front, rear; // 先頭ノード front、末尾ノード rear + int queSize = 0; + + public LinkedListQueue() { + front = null; + rear = null; + } + + /* キューの長さを取得 */ + public int Size() { + return queSize; + } + + /* キューが空かどうかを判定 */ + public bool IsEmpty() { + return Size() == 0; + } + + /* エンキュー */ + public void Push(int num) { + // 末尾ノードの後ろに num を追加 + ListNode node = new(num); + // キューが空なら、先頭・末尾ノードをともにそのノードに設定 + if (front == null) { + front = node; + rear = node; + // キューが空でなければ、そのノードを末尾ノードの後ろに追加 + } else if (rear != null) { + rear.next = node; + rear = node; + } + queSize++; + } + + /* デキュー */ + public int Pop() { + int num = Peek(); + // 先頭ノードを削除 + front = front?.next; + queSize--; + return num; + } + + /* キュー先頭の要素にアクセス */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return front!.val; + } + + /* 連結リストを Array に変換して返す */ + public int[] ToArray() { + if (front == null) + return []; + + ListNode? node = front; + int[] res = new int[Size()]; + for (int i = 0; i < res.Length; i++) { + res[i] = node!.val; + node = node.next; + } + return res; + } + } ``` === "Go" ```go title="linkedlist_queue.go" - [class]{linkedListQueue}-[func]{} + /* 連結リストベースのキュー */ + type linkedListQueue struct { + // 組み込みパッケージ list でキューを実装する + data *list.List + } + + /* キューを初期化 */ + func newLinkedListQueue() *linkedListQueue { + return &linkedListQueue{ + data: list.New(), + } + } + + /* エンキュー */ + func (s *linkedListQueue) push(value any) { + s.data.PushBack(value) + } + + /* デキュー */ + func (s *linkedListQueue) pop() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + s.data.Remove(e) + return e.Value + } + + /* キュー先頭の要素にアクセス */ + func (s *linkedListQueue) peek() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + return e.Value + } + + /* キューの長さを取得 */ + func (s *linkedListQueue) size() int { + return s.data.Len() + } + + /* キューが空かどうかを判定 */ + func (s *linkedListQueue) isEmpty() bool { + return s.data.Len() == 0 + } + + /* 表示用に List を取得 */ + func (s *linkedListQueue) toList() *list.List { + return s.data + } ``` === "Swift" ```swift title="linkedlist_queue.swift" - [class]{LinkedListQueue}-[func]{} + /* 連結リストベースのキュー */ + class LinkedListQueue { + private var front: ListNode? // 先頭ノード + private var rear: ListNode? // 末尾ノード + private var _size: Int + + init() { + _size = 0 + } + + /* キューの長さを取得 */ + func size() -> Int { + _size + } + + /* キューが空かどうかを判定 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* エンキュー */ + func push(num: Int) { + // 末尾ノードの後ろに num を追加 + let node = ListNode(x: num) + // キューが空なら、先頭・末尾ノードをともにそのノードに設定 + if front == nil { + front = node + rear = node + } + // キューが空でなければ、そのノードを末尾ノードの後ろに追加 + else { + rear?.next = node + rear = node + } + _size += 1 + } + + /* デキュー */ + @discardableResult + func pop() -> Int { + let num = peek() + // 先頭ノードを削除 + front = front?.next + _size -= 1 + return num + } + + /* キュー先頭の要素にアクセス */ + func peek() -> Int { + if isEmpty() { + fatalError("キューが空です") + } + return front!.val + } + + /* 連結リストを Array に変換して返す */ + func toArray() -> [Int] { + var node = front + var res = Array(repeating: 0, count: size()) + for i in res.indices { + res[i] = node!.val + node = node?.next + } + return res + } + } ``` === "JS" ```javascript title="linkedlist_queue.js" - [class]{LinkedListQueue}-[func]{} + /* 連結リストベースのキュー */ + class LinkedListQueue { + #front; // 先頭ノード #front + #rear; // 末尾ノード #rear + #queSize = 0; + + constructor() { + this.#front = null; + this.#rear = null; + } + + /* キューの長さを取得 */ + get size() { + return this.#queSize; + } + + /* キューが空かどうかを判定 */ + isEmpty() { + return this.size === 0; + } + + /* エンキュー */ + push(num) { + // 末尾ノードの後ろに num を追加 + const node = new ListNode(num); + // キューが空なら、先頭・末尾ノードをともにそのノードに設定 + if (!this.#front) { + this.#front = node; + this.#rear = node; + // キューが空でなければ、そのノードを末尾ノードの後ろに追加 + } else { + this.#rear.next = node; + this.#rear = node; + } + this.#queSize++; + } + + /* デキュー */ + pop() { + const num = this.peek(); + // 先頭ノードを削除 + this.#front = this.#front.next; + this.#queSize--; + return num; + } + + /* キュー先頭の要素にアクセス */ + peek() { + if (this.size === 0) throw new Error('キューが空'); + return this.#front.val; + } + + /* 連結リストを Array に変換して返す */ + toArray() { + let node = this.#front; + const res = new Array(this.size); + for (let i = 0; i < res.length; i++) { + res[i] = node.val; + node = node.next; + } + return res; + } + } ``` === "TS" ```typescript title="linkedlist_queue.ts" - [class]{LinkedListQueue}-[func]{} + /* 連結リストベースのキュー */ + class LinkedListQueue { + private front: ListNode | null; // 先頭ノード front + private rear: ListNode | null; // 末尾ノード rear + private queSize: number = 0; + + constructor() { + this.front = null; + this.rear = null; + } + + /* キューの長さを取得 */ + get size(): number { + return this.queSize; + } + + /* キューが空かどうかを判定 */ + isEmpty(): boolean { + return this.size === 0; + } + + /* エンキュー */ + push(num: number): void { + // 末尾ノードの後ろに num を追加 + const node = new ListNode(num); + // キューが空なら、先頭・末尾ノードをともにそのノードに設定 + if (!this.front) { + this.front = node; + this.rear = node; + // キューが空でなければ、そのノードを末尾ノードの後ろに追加 + } else { + this.rear!.next = node; + this.rear = node; + } + this.queSize++; + } + + /* デキュー */ + pop(): number { + const num = this.peek(); + if (!this.front) throw new Error('キューが空です'); + // 先頭ノードを削除 + this.front = this.front.next; + this.queSize--; + return num; + } + + /* キュー先頭の要素にアクセス */ + peek(): number { + if (this.size === 0) throw new Error('キューが空です'); + return this.front!.val; + } + + /* 連結リストを Array に変換して返す */ + toArray(): number[] { + let node = this.front; + const res = new Array(this.size); + for (let i = 0; i < res.length; i++) { + res[i] = node!.val; + node = node!.next; + } + return res; + } + } ``` === "Dart" ```dart title="linkedlist_queue.dart" - [class]{LinkedListQueue}-[func]{} + /* 連結リストベースのキュー */ + class LinkedListQueue { + ListNode? _front; // 先頭ノード _front + ListNode? _rear; // 末尾ノード _rear + int _queSize = 0; // キューの長さ + + LinkedListQueue() { + _front = null; + _rear = null; + } + + /* キューの長さを取得 */ + int size() { + return _queSize; + } + + /* キューが空かどうかを判定 */ + bool isEmpty() { + return _queSize == 0; + } + + /* エンキュー */ + void push(int _num) { + // 末尾ノードの後ろに _num を追加 + final node = ListNode(_num); + // キューが空なら、先頭・末尾ノードをともにそのノードに設定 + if (_front == null) { + _front = node; + _rear = node; + } else { + // キューが空でなければ、そのノードを末尾ノードの後ろに追加 + _rear!.next = node; + _rear = node; + } + _queSize++; + } + + /* デキュー */ + int pop() { + final int _num = peek(); + // 先頭ノードを削除 + _front = _front!.next; + _queSize--; + return _num; + } + + /* キュー先頭の要素にアクセス */ + int peek() { + if (_queSize == 0) { + throw Exception('キューが空です'); + } + return _front!.val; + } + + /* 連結リストを Array に変換して返す */ + List toArray() { + ListNode? node = _front; + final List queue = []; + while (node != null) { + queue.add(node.val); + node = node.next; + } + return queue; + } + } ``` === "Rust" ```rust title="linkedlist_queue.rs" - [class]{LinkedListQueue}-[func]{} + /* 連結リストベースのキュー */ + #[allow(dead_code)] + pub struct LinkedListQueue { + front: Option>>>, // 先頭ノード front + rear: Option>>>, // 末尾ノード rear + que_size: usize, // キューの長さ + } + + impl LinkedListQueue { + pub fn new() -> Self { + Self { + front: None, + rear: None, + que_size: 0, + } + } + + /* キューの長さを取得 */ + pub fn size(&self) -> usize { + return self.que_size; + } + + /* キューが空かどうかを判定 */ + pub fn is_empty(&self) -> bool { + return self.que_size == 0; + } + + /* エンキュー */ + pub fn push(&mut self, num: T) { + // 末尾ノードの後ろに num を追加 + let new_rear = ListNode::new(num); + match self.rear.take() { + // キューが空でなければ、そのノードを末尾ノードの後ろに追加 + Some(old_rear) => { + old_rear.borrow_mut().next = Some(new_rear.clone()); + self.rear = Some(new_rear); + } + // キューが空なら、先頭・末尾ノードをともにそのノードに設定 + None => { + self.front = Some(new_rear.clone()); + self.rear = Some(new_rear); + } + } + self.que_size += 1; + } + + /* デキュー */ + pub fn pop(&mut self) -> Option { + self.front.take().map(|old_front| { + match old_front.borrow_mut().next.take() { + Some(new_front) => { + self.front = Some(new_front); + } + None => { + self.rear.take(); + } + } + self.que_size -= 1; + old_front.borrow().val + }) + } + + /* キュー先頭の要素にアクセス */ + pub fn peek(&self) -> Option<&Rc>>> { + self.front.as_ref() + } + + /* 連結リストを Array に変換して返す */ + pub fn to_array(&self, head: Option<&Rc>>>) -> Vec { + let mut res: Vec = Vec::new(); + + fn recur(cur: Option<&Rc>>>, res: &mut Vec) { + if let Some(cur) = cur { + res.push(cur.borrow().val); + recur(cur.borrow().next.as_ref(), res); + } + } + + recur(head, &mut res); + + res + } + } ``` === "C" ```c title="linkedlist_queue.c" - [class]{LinkedListQueue}-[func]{} + /* 連結リストベースのキュー */ + typedef struct { + ListNode *front, *rear; + int queSize; + } LinkedListQueue; + + /* コンストラクタ */ + LinkedListQueue *newLinkedListQueue() { + LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue)); + queue->front = NULL; + queue->rear = NULL; + queue->queSize = 0; + return queue; + } + + /* デストラクタ */ + void delLinkedListQueue(LinkedListQueue *queue) { + // すべてのノードを解放 + while (queue->front != NULL) { + ListNode *tmp = queue->front; + queue->front = queue->front->next; + free(tmp); + } + // queue 構造体を解放する + free(queue); + } + + /* キューの長さを取得 */ + int size(LinkedListQueue *queue) { + return queue->queSize; + } + + /* キューが空かどうかを判定 */ + bool empty(LinkedListQueue *queue) { + return (size(queue) == 0); + } + + /* エンキュー */ + void push(LinkedListQueue *queue, int num) { + // 末尾ノードに node を追加 + ListNode *node = newListNode(num); + // キューが空なら、先頭・末尾ノードをともにそのノードに設定 + if (queue->front == NULL) { + queue->front = node; + queue->rear = node; + } + // キューが空でなければ、そのノードを末尾ノードの後ろに追加 + else { + queue->rear->next = node; + queue->rear = node; + } + queue->queSize++; + } + + /* キュー先頭の要素にアクセス */ + int peek(LinkedListQueue *queue) { + assert(size(queue) && queue->front); + return queue->front->val; + } + + /* デキュー */ + int pop(LinkedListQueue *queue) { + int num = peek(queue); + ListNode *tmp = queue->front; + queue->front = queue->front->next; + free(tmp); + queue->queSize--; + return num; + } + + /* キューを出力する */ + void printLinkedListQueue(LinkedListQueue *queue) { + int *arr = malloc(sizeof(int) * queue->queSize); + // 連結リスト内のデータを配列にコピー + int i; + ListNode *node; + for (i = 0, node = queue->front; i < queue->queSize; i++) { + arr[i] = node->val; + node = node->next; + } + printArray(arr, queue->queSize); + free(arr); + } ``` === "Kotlin" ```kotlin title="linkedlist_queue.kt" - [class]{LinkedListQueue}-[func]{} + /* 連結リストベースのキュー */ + class LinkedListQueue( + // 先頭ノード front、末尾ノード rear + private var front: ListNode? = null, + private var rear: ListNode? = null, + private var queSize: Int = 0 + ) { + + /* キューの長さを取得 */ + fun size(): Int { + return queSize + } + + /* キューが空かどうかを判定 */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* エンキュー */ + fun push(num: Int) { + // 末尾ノードの後ろに num を追加 + val node = ListNode(num) + // キューが空なら、先頭・末尾ノードをともにそのノードに設定 + if (front == null) { + front = node + rear = node + // キューが空でなければ、そのノードを末尾ノードの後ろに追加 + } else { + rear?.next = node + rear = node + } + queSize++ + } + + /* デキュー */ + fun pop(): Int { + val num = peek() + // 先頭ノードを削除 + front = front?.next + queSize-- + return num + } + + /* キュー先頭の要素にアクセス */ + fun peek(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return front!!._val + } + + /* 連結リストを Array に変換して返す */ + fun toArray(): IntArray { + var node = front + val res = IntArray(size()) + for (i in res.indices) { + res[i] = node!!._val + node = node.next + } + return res + } + } ``` === "Ruby" ```ruby title="linkedlist_queue.rb" - [class]{LinkedListQueue}-[func]{} + ### 連結リストで実装したキュー ### + class LinkedListQueue + ### キューの長さを取得 ### + attr_reader :size + + ### コンストラクタ ### + def initialize + @front = nil # 先頭ノード front + @rear = nil # 末尾ノード rear + @size = 0 + end + + ### キューが空か判定 ### + def is_empty? + @front.nil? + end + + ### エンキュー ### + def push(num) + # 末尾ノードの後ろに num を追加 + node = ListNode.new(num) + + # キューが空なら、先頭ノードと末尾ノードの両方をそのノードに向ける + if @front.nil? + @front = node + @rear = node + # キューが空でなければ、そのノードを末尾ノードの後ろに追加する + else + @rear.next = node + @rear = node + end + + @size += 1 + end + + ### デキュー ### + def pop + num = peek + # 先頭ノードを削除 + @front = @front.next + @size -= 1 + num + end + + ### 先頭要素にアクセス ### + def peek + raise IndexError, 'キューは空です' if is_empty? + + @front.val + end + + ### 連結リストを Array に変換して返す ### + def to_array + queue = [] + temp = @front + while temp + queue << temp.val + temp = temp.next + end + queue + end + end ``` -### 2.   配列ベースの実装 +??? pythontutor "コードの可視化" -配列の最初の要素を削除する時間計算量は$O(n)$で、デキュー操作が非効率になります。しかし、この問題は以下のように巧妙に回避できます。 +
+ -変数`front`を使用してフロント要素のインデックスを示し、変数`size`を維持してキューの長さを記録します。`rear = front + size`を定義し、これはテール要素の直後の位置を指します。 +### 2.   配列に基づく実装 -この設計により、**配列内の要素の有効な間隔は`[front, rear - 1]`です**。各操作の実装方法を下図に示します。 +配列で先頭要素を削除する時間計算量は $O(n)$ であり、そのままではデキュー操作の効率が低くなります。しかし、次の巧妙な方法によってこの問題を回避できます。 -- エンキュー操作:入力要素を`rear`インデックスに割り当て、`size`を1増加させます。 -- デキュー操作:単に`front`を1増加させ、`size`を1減少させます。 +変数 `front` を用いてキュー先頭要素のインデックスを指し、さらに変数 `size` でキューの長さを記録できます。`rear = front + size` と定義すると、この式で得られる `rear` はキュー末尾要素の次の位置を指します。 -エンキューとデキュー操作は両方とも単一の操作のみを必要とし、それぞれの時間計算量は$O(1)$です。 +この設計に基づくと、**配列内で要素を含む有効区間は `[front, rear - 1]`** となります。各種操作の実装方法を下図に示します。 -=== "ArrayQueue" - ![配列によるキュー実装のエンキューとデキュー操作](queue.assets/array_queue_step1.png){ class="animation-figure" } +- エンキュー操作:入力要素を `rear` の位置に代入し、`size` を 1 増やします。 +- デキュー操作:`front` を 1 増やし、`size` を 1 減らすだけです。 -=== "push()" +このように、エンキューとデキューはいずれも 1 回の操作だけで済み、時間計算量はともに $O(1)$ です。 + +=== "<1>" + ![配列でキューを実装したエンキューとデキュー操作](queue.assets/array_queue_step1.png){ class="animation-figure" } + +=== "<2>" ![array_queue_push](queue.assets/array_queue_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![array_queue_pop](queue.assets/array_queue_step3_pop.png){ class="animation-figure" } -

図 5-6   配列によるキュー実装のエンキューとデキュー操作

+

図 5-6   配列でキューを実装したエンキューとデキュー操作

-問題に気づくかもしれません:エンキューとデキュー操作が継続的に実行されると、`front`と`rear`の両方が右に移動し、**最終的に配列の末尾に到達してそれ以上移動できなくなります**。これを解決するために、配列を「循環配列」として扱い、配列の末尾を先頭に接続します。 +ここで 1 つ問題があります。エンキューとデキューを繰り返すと、`front` と `rear` はどちらも右へ移動し続け、**配列の末尾に達するとそれ以上進めなくなります**。この問題を解決するために、配列を先頭と末尾がつながった「環状配列」とみなします。 -循環配列では、`front`または`rear`が末尾に到達すると、配列の先頭にループバックする必要があります。この循環パターンは、以下のコードに示すように「剰余演算」で実現できます: +環状配列では、`front` または `rear` が配列末尾を越えたときに、直ちに配列先頭へ戻って走査を続けられるようにする必要があります。この周期的な規則は「剰余演算」によって実現できます。コードは次のとおりです: === "Python" ```python title="array_queue.py" class ArrayQueue: - """循環配列ベースのキュークラス""" + """循環配列ベースのキュー""" def __init__(self, size: int): """コンストラクタ""" self._nums: list[int] = [0] * size # キュー要素を格納する配列 - self._front: int = 0 # フロントポインタ、フロント要素を指す + self._front: int = 0 # 先頭ポインタ。先頭要素を指す self._size: int = 0 # キューの長さ def capacity(self) -> int: @@ -660,30 +1370,30 @@ comments: true def push(self, num: int): """エンキュー""" if self._size == self.capacity(): - raise IndexError("Queue is full") - # リアポインタを計算、リアインデックス + 1 を指す - # モジュロ演算を使用してリアポインタを配列の末尾から先頭に戻す + raise IndexError("キューがいっぱいです") + # 末尾ポインタを計算し、末尾インデックス + 1 を指す + # 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする rear: int = (self._front + self._size) % self.capacity() - # num をリアに追加 + # num をキュー末尾に追加 self._nums[rear] = num self._size += 1 def pop(self) -> int: """デキュー""" num: int = self.peek() - # フロントポインタを1つ後ろに移動、末尾を超えた場合は配列の先頭に戻る + # 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す self._front = (self._front + 1) % self.capacity() self._size -= 1 return num def peek(self) -> int: - """フロント要素にアクセス""" + """キュー先頭の要素にアクセス""" if self.is_empty(): - raise IndexError("Queue is empty") + raise IndexError("キューが空です") return self._nums[self._front] def to_list(self) -> list[int]: - """出力用の配列を返す""" + """表示用のリストを返す""" res = [0] * self.size() j: int = self._front for i in range(self.size()): @@ -695,11 +1405,11 @@ comments: true === "C++" ```cpp title="array_queue.cpp" - /* 循環配列に基づくキュークラス */ + /* 循環配列ベースのキュー */ class ArrayQueue { private: int *nums; // キュー要素を格納する配列 - int front; // 先頭ポインタ、先頭要素を指す + int front; // 先頭ポインタ。先頭要素を指す int queSize; // キューの長さ int queCapacity; // キューの容量 @@ -733,13 +1443,13 @@ comments: true /* エンキュー */ void push(int num) { if (queSize == queCapacity) { - cout << "Queue is full" << endl; + cout << "キューがいっぱいです" << endl; return; } - // 末尾ポインタを計算、末尾インデックス + 1を指す - // 剰余演算を使用して末尾ポインタが配列の末尾から先頭に戻るようにラップ + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする int rear = (front + queSize) % queCapacity; - // numを末尾に追加 + // num をキュー末尾に追加 nums[rear] = num; queSize++; } @@ -747,22 +1457,22 @@ comments: true /* デキュー */ int pop() { int num = peek(); - // 先頭ポインタを1つ後ろに移動、末尾を超えた場合は配列の先頭に戻る + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す front = (front + 1) % queCapacity; queSize--; return num; } - /* 先頭要素にアクセス */ + /* キュー先頭の要素にアクセス */ int peek() { if (isEmpty()) - throw out_of_range("Queue is empty"); + throw out_of_range("キューが空です"); return nums[front]; } - /* 配列をVectorに変換して返却 */ + /* 配列を Vector に変換して返す */ vector toVector() { - // 有効な長さ範囲内の要素のみを変換 + // 有効長の範囲内のリスト要素のみを変換 vector arr(queSize); for (int i = 0, j = front; i < queSize; i++, j++) { arr[i] = nums[j % queCapacity]; @@ -775,10 +1485,10 @@ comments: true === "Java" ```java title="array_queue.java" - /* 配列に基づくキュークラス */ + /* 循環配列ベースのキュー */ class ArrayQueue { - private int[] nums; // 要素を格納する配列 - private int front; // キューヘッドポインタ、最初の要素を指す + private int[] nums; // キュー要素を格納する配列 + private int front; // 先頭ポインタ。先頭要素を指す private int queSize; // キューの長さ public ArrayQueue(int capacity) { @@ -804,13 +1514,13 @@ comments: true /* エンキュー */ public void push(int num) { if (queSize == capacity()) { - System.out.println("キューが満杯です"); + System.out.println("キューは満杯です"); return; } - // リアポインタを計算:front + queSize - // モジュロ操作により rear が配列の長さを超えることを回避 + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする int rear = (front + queSize) % capacity(); - // 要素をキューリアに追加 + // num をキュー末尾に追加 nums[rear] = num; queSize++; } @@ -818,13 +1528,13 @@ comments: true /* デキュー */ public int pop() { int num = peek(); - // キューヘッドポインタを後ろに1つ移動、モジュロ操作により範囲を超えることを回避 + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す front = (front + 1) % capacity(); queSize--; return num; } - /* キューヘッド要素にアクセス */ + /* キュー先頭の要素にアクセス */ public int peek() { if (isEmpty()) throw new IndexOutOfBoundsException(); @@ -833,7 +1543,7 @@ comments: true /* 配列を返す */ public int[] toArray() { - // front から開始して queSize 個の要素のみをコピー + // 有効長の範囲内のリスト要素のみを変換 int[] res = new int[queSize]; for (int i = 0, j = front; i < queSize; i++, j++) { res[i] = nums[j % capacity()]; @@ -846,68 +1556,745 @@ comments: true === "C#" ```csharp title="array_queue.cs" - [class]{ArrayQueue}-[func]{} + /* 循環配列ベースのキュー */ + class ArrayQueue { + int[] nums; // キュー要素を格納する配列 + int front; // 先頭ポインタ。先頭要素を指す + int queSize; // キューの長さ + + public ArrayQueue(int capacity) { + nums = new int[capacity]; + front = queSize = 0; + } + + /* キューの容量を取得 */ + int Capacity() { + return nums.Length; + } + + /* キューの長さを取得 */ + public int Size() { + return queSize; + } + + /* キューが空かどうかを判定 */ + public bool IsEmpty() { + return queSize == 0; + } + + /* エンキュー */ + public void Push(int num) { + if (queSize == Capacity()) { + Console.WriteLine("キューは満杯です"); + return; + } + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする + int rear = (front + queSize) % Capacity(); + // num をキュー末尾に追加 + nums[rear] = num; + queSize++; + } + + /* デキュー */ + public int Pop() { + int num = Peek(); + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す + front = (front + 1) % Capacity(); + queSize--; + return num; + } + + /* キュー先頭の要素にアクセス */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return nums[front]; + } + + /* 配列を返す */ + public int[] ToArray() { + // 有効長の範囲内のリスト要素のみを変換 + int[] res = new int[queSize]; + for (int i = 0, j = front; i < queSize; i++, j++) { + res[i] = nums[j % this.Capacity()]; + } + return res; + } + } ``` === "Go" ```go title="array_queue.go" - [class]{arrayQueue}-[func]{} + /* 循環配列ベースのキュー */ + type arrayQueue struct { + nums []int // キュー要素を格納する配列 + front int // 先頭ポインタ。先頭要素を指す + queSize int // キューの長さ + queCapacity int // キュー容量(格納できる要素数の上限) + } + + /* キューを初期化 */ + func newArrayQueue(queCapacity int) *arrayQueue { + return &arrayQueue{ + nums: make([]int, queCapacity), + queCapacity: queCapacity, + front: 0, + queSize: 0, + } + } + + /* キューの長さを取得 */ + func (q *arrayQueue) size() int { + return q.queSize + } + + /* キューが空かどうかを判定 */ + func (q *arrayQueue) isEmpty() bool { + return q.queSize == 0 + } + + /* エンキュー */ + func (q *arrayQueue) push(num int) { + // rear == queCapacity のときキューは満杯 + if q.queSize == q.queCapacity { + return + } + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする + rear := (q.front + q.queSize) % q.queCapacity + // num をキュー末尾に追加 + q.nums[rear] = num + q.queSize++ + } + + /* デキュー */ + func (q *arrayQueue) pop() any { + num := q.peek() + if num == nil { + return nil + } + + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す + q.front = (q.front + 1) % q.queCapacity + q.queSize-- + return num + } + + /* キュー先頭の要素にアクセス */ + func (q *arrayQueue) peek() any { + if q.isEmpty() { + return nil + } + return q.nums[q.front] + } + + /* 表示用に Slice を取得 */ + func (q *arrayQueue) toSlice() []int { + rear := (q.front + q.queSize) + if rear >= q.queCapacity { + rear %= q.queCapacity + return append(q.nums[q.front:], q.nums[:rear]...) + } + return q.nums[q.front:rear] + } ``` === "Swift" ```swift title="array_queue.swift" - [class]{ArrayQueue}-[func]{} + /* 循環配列ベースのキュー */ + class ArrayQueue { + private var nums: [Int] // キュー要素を格納する配列 + private var front: Int // 先頭ポインタ。先頭要素を指す + private var _size: Int // キューの長さ + + init(capacity: Int) { + // 配列を初期化 + nums = Array(repeating: 0, count: capacity) + front = 0 + _size = 0 + } + + /* キューの容量を取得 */ + func capacity() -> Int { + nums.count + } + + /* キューの長さを取得 */ + func size() -> Int { + _size + } + + /* キューが空かどうかを判定 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* エンキュー */ + func push(num: Int) { + if size() == capacity() { + print("キューがいっぱいです") + return + } + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする + let rear = (front + size()) % capacity() + // num をキュー末尾に追加 + nums[rear] = num + _size += 1 + } + + /* デキュー */ + @discardableResult + func pop() -> Int { + let num = peek() + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す + front = (front + 1) % capacity() + _size -= 1 + return num + } + + /* キュー先頭の要素にアクセス */ + func peek() -> Int { + if isEmpty() { + fatalError("キューが空です") + } + return nums[front] + } + + /* 配列を返す */ + func toArray() -> [Int] { + // 有効長の範囲内のリスト要素のみを変換 + (front ..< front + size()).map { nums[$0 % capacity()] } + } + } ``` === "JS" ```javascript title="array_queue.js" - [class]{ArrayQueue}-[func]{} + /* 循環配列ベースのキュー */ + class ArrayQueue { + #nums; // キュー要素を格納する配列 + #front = 0; // 先頭ポインタ。先頭要素を指す + #queSize = 0; // キューの長さ + + constructor(capacity) { + this.#nums = new Array(capacity); + } + + /* キューの容量を取得 */ + get capacity() { + return this.#nums.length; + } + + /* キューの長さを取得 */ + get size() { + return this.#queSize; + } + + /* キューが空かどうかを判定 */ + isEmpty() { + return this.#queSize === 0; + } + + /* エンキュー */ + push(num) { + if (this.size === this.capacity) { + console.log('キューがいっぱいです'); + return; + } + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする + const rear = (this.#front + this.size) % this.capacity; + // num をキュー末尾に追加 + this.#nums[rear] = num; + this.#queSize++; + } + + /* デキュー */ + pop() { + const num = this.peek(); + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す + this.#front = (this.#front + 1) % this.capacity; + this.#queSize--; + return num; + } + + /* キュー先頭の要素にアクセス */ + peek() { + if (this.isEmpty()) throw new Error('キューが空です'); + return this.#nums[this.#front]; + } + + /* Array を返す */ + toArray() { + // 有効長の範囲内のリスト要素のみを変換 + const arr = new Array(this.size); + for (let i = 0, j = this.#front; i < this.size; i++, j++) { + arr[i] = this.#nums[j % this.capacity]; + } + return arr; + } + } ``` === "TS" ```typescript title="array_queue.ts" - [class]{ArrayQueue}-[func]{} + /* 循環配列ベースのキュー */ + class ArrayQueue { + private nums: number[]; // キュー要素を格納する配列 + private front: number; // 先頭ポインタ。先頭要素を指す + private queSize: number; // キューの長さ + + constructor(capacity: number) { + this.nums = new Array(capacity); + this.front = this.queSize = 0; + } + + /* キューの容量を取得 */ + get capacity(): number { + return this.nums.length; + } + + /* キューの長さを取得 */ + get size(): number { + return this.queSize; + } + + /* キューが空かどうかを判定 */ + isEmpty(): boolean { + return this.queSize === 0; + } + + /* エンキュー */ + push(num: number): void { + if (this.size === this.capacity) { + console.log('キューは満杯です'); + return; + } + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする + const rear = (this.front + this.queSize) % this.capacity; + // num をキュー末尾に追加 + this.nums[rear] = num; + this.queSize++; + } + + /* デキュー */ + pop(): number { + const num = this.peek(); + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す + this.front = (this.front + 1) % this.capacity; + this.queSize--; + return num; + } + + /* キュー先頭の要素にアクセス */ + peek(): number { + if (this.isEmpty()) throw new Error('キューが空です'); + return this.nums[this.front]; + } + + /* Array を返す */ + toArray(): number[] { + // 有効長の範囲内のリスト要素のみを変換 + const arr = new Array(this.size); + for (let i = 0, j = this.front; i < this.size; i++, j++) { + arr[i] = this.nums[j % this.capacity]; + } + return arr; + } + } ``` === "Dart" ```dart title="array_queue.dart" - [class]{ArrayQueue}-[func]{} + /* 循環配列ベースのキュー */ + class ArrayQueue { + late List _nums; // キュー要素を格納する配列 + late int _front; // 先頭ポインタ。先頭要素を指す + late int _queSize; // キューの長さ + + ArrayQueue(int capacity) { + _nums = List.filled(capacity, 0); + _front = _queSize = 0; + } + + /* キューの容量を取得 */ + int capaCity() { + return _nums.length; + } + + /* キューの長さを取得 */ + int size() { + return _queSize; + } + + /* キューが空かどうかを判定 */ + bool isEmpty() { + return _queSize == 0; + } + + /* エンキュー */ + void push(int _num) { + if (_queSize == capaCity()) { + throw Exception("キューは満杯です"); + } + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする + int rear = (_front + _queSize) % capaCity(); + // _num をキュー末尾に追加 + _nums[rear] = _num; + _queSize++; + } + + /* デキュー */ + int pop() { + int _num = peek(); + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す + _front = (_front + 1) % capaCity(); + _queSize--; + return _num; + } + + /* キュー先頭の要素にアクセス */ + int peek() { + if (isEmpty()) { + throw Exception("キューが空です"); + } + return _nums[_front]; + } + + /* Array を返す */ + List toArray() { + // 有効長の範囲内のリスト要素のみを変換 + final List res = List.filled(_queSize, 0); + for (int i = 0, j = _front; i < _queSize; i++, j++) { + res[i] = _nums[j % capaCity()]; + } + return res; + } + } ``` === "Rust" ```rust title="array_queue.rs" - [class]{ArrayQueue}-[func]{} + /* 循環配列ベースのキュー */ + struct ArrayQueue { + nums: Vec, // キュー要素を格納する配列 + front: i32, // 先頭ポインタ。先頭要素を指す + que_size: i32, // キューの長さ + que_capacity: i32, // キューの容量 + } + + impl ArrayQueue { + /* コンストラクタ */ + fn new(capacity: i32) -> ArrayQueue { + ArrayQueue { + nums: vec![T::default(); capacity as usize], + front: 0, + que_size: 0, + que_capacity: capacity, + } + } + + /* キューの容量を取得 */ + fn capacity(&self) -> i32 { + self.que_capacity + } + + /* キューの長さを取得 */ + fn size(&self) -> i32 { + self.que_size + } + + /* キューが空かどうかを判定 */ + fn is_empty(&self) -> bool { + self.que_size == 0 + } + + /* エンキュー */ + fn push(&mut self, num: T) { + if self.que_size == self.capacity() { + println!("キューがいっぱいです"); + return; + } + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする + let rear = (self.front + self.que_size) % self.que_capacity; + // num をキュー末尾に追加 + self.nums[rear as usize] = num; + self.que_size += 1; + } + + /* デキュー */ + fn pop(&mut self) -> T { + let num = self.peek(); + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す + self.front = (self.front + 1) % self.que_capacity; + self.que_size -= 1; + num + } + + /* キュー先頭の要素にアクセス */ + fn peek(&self) -> T { + if self.is_empty() { + panic!("index out of bounds"); + } + self.nums[self.front as usize] + } + + /* 配列を返す */ + fn to_vector(&self) -> Vec { + let cap = self.que_capacity; + let mut j = self.front; + let mut arr = vec![T::default(); cap as usize]; + for i in 0..self.que_size { + arr[i as usize] = self.nums[(j % cap) as usize]; + j += 1; + } + arr + } + } ``` === "C" ```c title="array_queue.c" - [class]{ArrayQueue}-[func]{} + /* 循環配列ベースのキュー */ + typedef struct { + int *nums; // キュー要素を格納する配列 + int front; // 先頭ポインタ。先頭要素を指す + int queSize; // 現在のキュー内の要素数 + int queCapacity; // キューの容量 + } ArrayQueue; + + /* コンストラクタ */ + ArrayQueue *newArrayQueue(int capacity) { + ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue)); + // 配列を初期化 + queue->queCapacity = capacity; + queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity); + queue->front = queue->queSize = 0; + return queue; + } + + /* デストラクタ */ + void delArrayQueue(ArrayQueue *queue) { + free(queue->nums); + free(queue); + } + + /* キューの容量を取得 */ + int capacity(ArrayQueue *queue) { + return queue->queCapacity; + } + + /* キューの長さを取得 */ + int size(ArrayQueue *queue) { + return queue->queSize; + } + + /* キューが空かどうかを判定 */ + bool empty(ArrayQueue *queue) { + return queue->queSize == 0; + } + + /* キュー先頭の要素にアクセス */ + int peek(ArrayQueue *queue) { + assert(size(queue) != 0); + return queue->nums[queue->front]; + } + + /* エンキュー */ + void push(ArrayQueue *queue, int num) { + if (size(queue) == capacity(queue)) { + printf("キューは満杯です\r\n"); + return; + } + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする + int rear = (queue->front + queue->queSize) % queue->queCapacity; + // num をキュー末尾に追加 + queue->nums[rear] = num; + queue->queSize++; + } + + /* デキュー */ + int pop(ArrayQueue *queue) { + int num = peek(queue); + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す + queue->front = (queue->front + 1) % queue->queCapacity; + queue->queSize--; + return num; + } + + /* 出力用の配列を返す */ + int *toArray(ArrayQueue *queue, int *queSize) { + *queSize = queue->queSize; + int *res = (int *)calloc(queue->queSize, sizeof(int)); + int j = queue->front; + for (int i = 0; i < queue->queSize; i++) { + res[i] = queue->nums[j % queue->queCapacity]; + j++; + } + return res; + } ``` === "Kotlin" ```kotlin title="array_queue.kt" - [class]{ArrayQueue}-[func]{} + /* 循環配列ベースのキュー */ + class ArrayQueue(capacity: Int) { + private val nums: IntArray = IntArray(capacity) // キュー要素を格納する配列 + private var front: Int = 0 // 先頭ポインタ。先頭要素を指す + private var queSize: Int = 0 // キューの長さ + + /* キューの容量を取得 */ + fun capacity(): Int { + return nums.size + } + + /* キューの長さを取得 */ + fun size(): Int { + return queSize + } + + /* キューが空かどうかを判定 */ + fun isEmpty(): Boolean { + return queSize == 0 + } + + /* エンキュー */ + fun push(num: Int) { + if (queSize == capacity()) { + println("キューは満杯です") + return + } + // 末尾ポインタを計算し、末尾インデックス + 1 を指す + // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする + val rear = (front + queSize) % capacity() + // num をキュー末尾に追加 + nums[rear] = num + queSize++ + } + + /* デキュー */ + fun pop(): Int { + val num = peek() + // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す + front = (front + 1) % capacity() + queSize-- + return num + } + + /* キュー先頭の要素にアクセス */ + fun peek(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return nums[front] + } + + /* 配列を返す */ + fun toArray(): IntArray { + // 有効長の範囲内のリスト要素のみを変換 + val res = IntArray(queSize) + var i = 0 + var j = front + while (i < queSize) { + res[i] = nums[j % capacity()] + i++ + j++ + } + return res + } + } ``` === "Ruby" ```ruby title="array_queue.rb" - [class]{ArrayQueue}-[func]{} + ### 循環配列で実装したキュー ### + class ArrayQueue + ### キューの長さを取得 ### + attr_reader :size + + ### コンストラクタ ### + def initialize(size) + @nums = Array.new(size, 0) # キュー要素を格納する配列 + @front = 0 # 先頭ポインタ。先頭要素を指す + @size = 0 # キューの長さ + end + + ### キューの容量を取得 ### + def capacity + @nums.length + end + + ### キューが空か判定 ### + def is_empty? + size.zero? + end + + ### エンキュー ### + def push(num) + raise IndexError, 'キューがいっぱいです' if size == capacity + + # 末尾ポインタを計算し、末尾インデックス + 1 を指す + # 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする + rear = (@front + size) % capacity + # num をキュー末尾に追加 + @nums[rear] = num + @size += 1 + end + + ### デキュー ### + def pop + num = peek + # 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す + @front = (@front + 1) % capacity + @size -= 1 + num + end + + ### 先頭要素にアクセス ### + def peek + raise IndexError, 'キューは空です' if is_empty? + + @nums[@front] + end + + ### 表示用のリストを返す ### + def to_array + res = Array.new(size, 0) + j = @front + + for i in 0...size + res[i] = @nums[j % capacity] + j += 1 + end + + res + end + end ``` -上記のキュー実装にはまだ制限があります:長さが固定されています。しかし、この問題は解決が困難ではありません。配列を必要に応じて自動拡張できる動的配列に置き換えることができます。興味のある読者は自分で実装してみてください。 +??? pythontutor "コードの可視化" -2つの実装の比較はスタックの場合と一貫しており、ここでは繰り返しません。 +
+ + +上記の実装によるキューにも制約があり、長さを可変にできません。しかし、この問題の解決は難しくなく、配列を動的配列に置き換えれば容量拡張の仕組みを導入できます。興味があれば自分で実装してみてください。 + +2 つの実装の比較に関する結論はスタックの場合と同じなので、ここでは繰り返しません。 ## 5.2.3   キューの典型的な応用 -- **Amazonの注文**:買い物客が注文を行った後、これらの注文はキューに参加し、システムは順番に処理します。独身の日などのイベント中は、短時間で大量の注文が生成され、高い同時実行性がエンジニアにとって重要な課題となります。 -- **様々なToDoリスト**:「先着順」機能が必要なシナリオ、例えばプリンターのタスクキューやレストランの配達キューなど、キューで処理順序を効果的に維持できます。 +- **淘宝の注文**。購入者が注文すると、その注文はキューに追加され、システムは順番に従って注文を処理します。ダブルイレブンの期間には短時間で膨大な注文が発生するため、高並行性がエンジニアにとって重点的に解決すべき課題になります。 +- **各種の待機事項**。先着順の機能を実現する必要があるあらゆる場面、たとえばプリンターのジョブキューや飲食店の配膳キューなどでは、キューによって処理順序を効果的に維持できます。 diff --git a/ja/docs/chapter_stack_and_queue/stack.md b/ja/docs/chapter_stack_and_queue/stack.md index ba10a32bc..70fe85267 100644 --- a/ja/docs/chapter_stack_and_queue/stack.md +++ b/ja/docs/chapter_stack_and_queue/stack.md @@ -4,58 +4,58 @@ comments: true # 5.1   スタック -スタックは、後入先出(LIFO)の原則に従う線形データ構造です。 +スタック(stack)は、後入れ先出しの論理に従う線形データ構造です。 -スタックをテーブル上の皿の山に例えることができます。底の皿にアクセスするには、まず上の皿を取り除く必要があります。皿を様々な種類の要素(整数、文字、オブジェクトなど)に置き換えることで、スタックと呼ばれるデータ構造を得ることができます。 +スタックは机の上に積まれた皿の山にたとえられます。1回に1枚の皿しか動かせないとすると、いちばん下の皿を取り出すには、上にある皿を順番にどかす必要があります。この皿をさまざまな型の要素(整数、文字、オブジェクトなど)に置き換えたものが、スタックというデータ構造です。 -下図に示すように、要素の山の上部を「スタックのトップ」、下部を「スタックのボトム」と呼びます。スタックのトップに要素を追加する操作を「プッシュ」、トップ要素を削除する操作を「ポップ」と呼びます。 +下図のように、積み重なった要素の上端を「スタックトップ」、下端を「スタックボトム」と呼びます。要素をスタックトップに追加する操作を「プッシュ」、スタックトップの要素を削除する操作を「ポップ」と呼びます。 -![スタックの後入先出ルール](stack.assets/stack_operations.png){ class="animation-figure" } +![スタックの後入れ先出しの規則](stack.assets/stack_operations.png){ class="animation-figure" } -

図 5-1   スタックの後入先出ルール

+

図 5-1   スタックの後入れ先出しの規則

-## 5.1.1   スタックの一般的な操作 +## 5.1.1   スタックの基本操作 -スタックの一般的な操作を下表に示します。具体的なメソッド名は使用するプログラミング言語によって異なります。ここでは、例として`push()`、`pop()`、`peek()`を使用します。 +スタックの基本操作を以下の表に示します。具体的なメソッド名は使用するプログラミング言語によって異なります。ここでは、一般的な `push()`、`pop()`、`peek()` を例に挙げます。 -

表 5-1   スタック操作の効率

+

表 5-1   スタックの操作効率

-| メソッド | 説明 | 時間計算量 | -| -------- | ----------------------------------------------- | --------------- | -| `push()` | 要素をスタックにプッシュ(トップに追加) | $O(1)$ | -| `pop()` | スタックからトップ要素をポップ | $O(1)$ | -| `peek()` | スタックのトップ要素にアクセス | $O(1)$ | +| メソッド | 説明 | 時間計算量 | +| -------- | ---------------------- | ---------- | +| `push()` | 要素をプッシュする(スタックトップに追加) | $O(1)$ | +| `pop()` | スタックトップの要素をポップする | $O(1)$ | +| `peek()` | スタックトップの要素にアクセスする | $O(1)$ |
-通常、プログラミング言語に組み込まれているスタッククラスを直接使用できます。ただし、一部の言語では具体的にスタッククラスを提供していない場合があります。これらの場合、言語の「配列」または「連結リスト」をスタックとして使用し、プログラムでスタックロジックに関連しない操作を無視できます。 +通常は、プログラミング言語に組み込まれているスタッククラスをそのまま利用できます。ただし、専用のスタッククラスが用意されていない言語もあります。その場合は、その言語の「配列」や「連結リスト」をスタックとして用い、プログラムのロジック上でスタックに無関係な操作を無視します。 === "Python" ```python title="stack.py" # スタックを初期化 - # Pythonには組み込みのスタッククラスがないため、listをスタックとして使用 + # Python には組み込みのスタッククラスがないため、list をスタックとして使用できる stack: list[int] = [] - # 要素をスタックにプッシュ + # 要素をプッシュ stack.append(1) stack.append(3) stack.append(2) stack.append(5) stack.append(4) - # スタックのトップ要素にアクセス + # スタックトップの要素にアクセス peek: int = stack[-1] - # スタックから要素をポップ + # 要素をポップ pop: int = stack.pop() # スタックの長さを取得 size: int = len(stack) - # スタックが空かどうかチェック + # 空かどうかを判定 is_empty: bool = len(stack) == 0 ``` @@ -65,23 +65,23 @@ comments: true /* スタックを初期化 */ stack stack; - /* 要素をスタックにプッシュ */ + /* 要素をプッシュ */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); - /* スタックのトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ int top = stack.top(); - /* スタックから要素をポップ */ + /* 要素をポップ */ stack.pop(); // 戻り値なし /* スタックの長さを取得 */ int size = stack.size(); - /* スタックが空かどうかチェック */ + /* 空かどうかを判定 */ bool empty = stack.empty(); ``` @@ -91,23 +91,23 @@ comments: true /* スタックを初期化 */ Stack stack = new Stack<>(); - /* 要素をスタックにプッシュ */ + /* 要素をプッシュ */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); - /* スタックのトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ int peek = stack.peek(); - /* スタックから要素をポップ */ + /* 要素をポップ */ int pop = stack.pop(); /* スタックの長さを取得 */ int size = stack.size(); - /* スタックが空かどうかチェック */ + /* 空かどうかを判定 */ boolean isEmpty = stack.isEmpty(); ``` @@ -117,23 +117,23 @@ comments: true /* スタックを初期化 */ Stack stack = new(); - /* 要素をスタックにプッシュ */ + /* 要素をプッシュ */ stack.Push(1); stack.Push(3); stack.Push(2); stack.Push(5); stack.Push(4); - /* スタックのトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ int peek = stack.Peek(); - /* スタックから要素をポップ */ + /* 要素をポップ */ int pop = stack.Pop(); /* スタックの長さを取得 */ int size = stack.Count; - /* スタックが空かどうかチェック */ + /* 空かどうかを判定 */ bool isEmpty = stack.Count == 0; ``` @@ -141,27 +141,27 @@ comments: true ```go title="stack_test.go" /* スタックを初期化 */ - // Goでは、Sliceをスタックとして使用することが推奨されます + // Go では、Slice をスタックとして使うのが一般的 var stack []int - /* 要素をスタックにプッシュ */ + /* 要素をプッシュ */ stack = append(stack, 1) stack = append(stack, 3) stack = append(stack, 2) stack = append(stack, 5) stack = append(stack, 4) - /* スタックのトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ peek := stack[len(stack)-1] - /* スタックから要素をポップ */ + /* 要素をポップ */ pop := stack[len(stack)-1] stack = stack[:len(stack)-1] /* スタックの長さを取得 */ size := len(stack) - /* スタックが空かどうかチェック */ + /* 空かどうかを判定 */ isEmpty := len(stack) == 0 ``` @@ -169,26 +169,26 @@ comments: true ```swift title="stack.swift" /* スタックを初期化 */ - // Swiftには組み込みのスタッククラスがないため、Arrayをスタックとして使用 + // Swift には組み込みのスタッククラスがないため、Array をスタックとして使用できる var stack: [Int] = [] - /* 要素をスタックにプッシュ */ + /* 要素をプッシュ */ stack.append(1) stack.append(3) stack.append(2) stack.append(5) stack.append(4) - /* スタックのトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ let peek = stack.last! - /* スタックから要素をポップ */ + /* 要素をポップ */ let pop = stack.removeLast() /* スタックの長さを取得 */ let size = stack.count - /* スタックが空かどうかチェック */ + /* 空かどうかを判定 */ let isEmpty = stack.isEmpty ``` @@ -196,26 +196,26 @@ comments: true ```javascript title="stack.js" /* スタックを初期化 */ - // JavaScriptには組み込みのスタッククラスがないため、Arrayをスタックとして使用 + // JavaScript には組み込みのスタッククラスがないため、Array をスタックとして使用できる const stack = []; - /* 要素をスタックにプッシュ */ + /* 要素をプッシュ */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); - /* スタックのトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ const peek = stack[stack.length-1]; - /* スタックから要素をポップ */ + /* 要素をポップ */ const pop = stack.pop(); /* スタックの長さを取得 */ const size = stack.length; - /* スタックが空かどうかチェック */ + /* 空かどうかを判定 */ const is_empty = stack.length === 0; ``` @@ -223,26 +223,26 @@ comments: true ```typescript title="stack.ts" /* スタックを初期化 */ - // TypeScriptには組み込みのスタッククラスがないため、Arrayをスタックとして使用 + // TypeScript には組み込みのスタッククラスがないため、Array をスタックとして使用できる const stack: number[] = []; - /* 要素をスタックにプッシュ */ + /* 要素をプッシュ */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); - /* スタックのトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ const peek = stack[stack.length - 1]; - /* スタックから要素をポップ */ + /* 要素をポップ */ const pop = stack.pop(); /* スタックの長さを取得 */ const size = stack.length; - /* スタックが空かどうかチェック */ + /* 空かどうかを判定 */ const is_empty = stack.length === 0; ``` @@ -250,26 +250,26 @@ comments: true ```dart title="stack.dart" /* スタックを初期化 */ - // Dartには組み込みのスタッククラスがないため、Listをスタックとして使用 + // Dart には組み込みのスタッククラスがないため、List をスタックとして使用できる List stack = []; - /* 要素をスタックにプッシュ */ + /* 要素をプッシュ */ stack.add(1); stack.add(3); stack.add(2); stack.add(5); stack.add(4); - /* スタックのトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ int peek = stack.last; - /* スタックから要素をポップ */ + /* 要素をポップ */ int pop = stack.removeLast(); /* スタックの長さを取得 */ int size = stack.length; - /* スタックが空かどうかチェック */ + /* 空かどうかを判定 */ bool isEmpty = stack.isEmpty; ``` @@ -277,71 +277,122 @@ comments: true ```rust title="stack.rs" /* スタックを初期化 */ - // Vecをスタックとして使用 + // Vec をスタックとして使用する let mut stack: Vec = Vec::new(); - /* 要素をスタックにプッシュ */ + /* 要素をプッシュ */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); - /* スタックのトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ let top = stack.last().unwrap(); - /* スタックから要素をポップ */ + /* 要素をポップ */ let pop = stack.pop().unwrap(); /* スタックの長さを取得 */ let size = stack.len(); - /* スタックが空かどうかチェック */ + /* 空かどうかを判定 */ let is_empty = stack.is_empty(); ``` === "C" ```c title="stack.c" - // Cは組み込みのスタックを提供していません + // C には組み込みのスタックがない ``` === "Kotlin" ```kotlin title="stack.kt" + /* スタックを初期化 */ + val stack = Stack() + /* 要素をプッシュ */ + stack.push(1) + stack.push(3) + stack.push(2) + stack.push(5) + stack.push(4) + + /* スタックトップの要素にアクセス */ + val peek = stack.peek() + + /* 要素をポップ */ + val pop = stack.pop() + + /* スタックの長さを取得 */ + val size = stack.size + + /* 空かどうかを判定 */ + val isEmpty = stack.isEmpty() ``` +=== "Ruby" + + ```ruby title="stack.rb" + # スタックを初期化 + # Ruby には組み込みのスタッククラスがないため、Array をスタックとして使用できる + stack = [] + + # 要素をプッシュ + stack << 1 + stack << 3 + stack << 2 + stack << 5 + stack << 4 + + # スタックトップの要素にアクセス + peek = stack.last + + # 要素をポップ + pop = stack.pop + + # スタックの長さを取得 + size = stack.length + + # 空かどうかを判定 + is_empty = stack.empty? + ``` + +??? pythontutor "実行の可視化" + + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + ## 5.1.2   スタックの実装 -スタックがどのように動作するかをより深く理解するために、自分でスタッククラスを実装してみましょう。 +スタックの動作の仕組みをより深く理解するために、自分でスタッククラスを実装してみましょう。 -スタックは後入先出の原則に従うため、スタックのトップでのみ要素を追加または削除できます。しかし、配列と連結リストの両方は任意の位置で要素を追加・削除できるため、**スタックは制限された配列または連結リストと見なすことができます**。言い換えれば、配列や連結リストの特定の無関係な操作を「遮蔽」して、外部の動作をスタックの特性に合わせることができます。 +スタックは後入れ先出しの原則に従うため、要素の追加や削除はスタックトップでしか行えません。一方、配列や連結リストでは任意の位置で要素を追加・削除できます。**つまり、スタックは制限付きの配列または連結リストとみなせます。** 言い換えると、配列や連結リストのうち無関係な操作を「隠蔽」することで、外から見た振る舞いをスタックの特性に合わせられます。 -### 1.   連結リストベースの実装 +### 1.   連結リストによる実装 -連結リストを使用してスタックを実装する場合、リストのヘッドノードをスタックのトップ、テールノードをスタックのボトムと考えることができます。 +連結リストでスタックを実装する場合、連結リストの先頭ノードをスタックトップ、末尾ノードをスタックボトムとみなせます。 -下図に示すように、プッシュ操作では、単に連結リストのヘッドに要素を挿入します。このノード挿入方法は「ヘッド挿入」として知られています。ポップ操作では、リストからヘッドノードを削除するだけです。 +下図のように、プッシュ操作では要素を連結リストの先頭に挿入するだけでよく、このノード挿入方法は「頭部挿入法」と呼ばれます。ポップ操作では、先頭ノードを連結リストから削除するだけです。 -=== "LinkedListStack" - ![連結リストによるスタック実装のプッシュとポップ操作](stack.assets/linkedlist_stack_step1.png){ class="animation-figure" } +=== "<1>" + ![連結リストによるスタック実装のプッシュ・ポップ操作](stack.assets/linkedlist_stack_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![linkedlist_stack_push](stack.assets/linkedlist_stack_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![linkedlist_stack_pop](stack.assets/linkedlist_stack_step3_pop.png){ class="animation-figure" } -

図 5-2   連結リストによるスタック実装のプッシュとポップ操作

+

図 5-2   連結リストによるスタック実装のプッシュ・ポップ操作

-以下は、連結リストに基づくスタック実装のサンプルコードです: +以下は、連結リストによってスタックを実装したコード例です: === "Python" ```python title="linkedlist_stack.py" class LinkedListStack: - """連結リストベースのスタッククラス""" + """連結リストベースのスタック""" def __init__(self): """コンストラクタ""" @@ -371,13 +422,13 @@ comments: true return num def peek(self) -> int: - """スタックトップ要素にアクセス""" + """スタックトップの要素にアクセス""" if self.is_empty(): - raise IndexError("Stack is empty") + raise IndexError("スタックが空です") return self._peek.val def to_list(self) -> list[int]: - """出力用のリストに変換""" + """表示用にリストへ変換""" arr = [] node = self._peek while node: @@ -390,10 +441,10 @@ comments: true === "C++" ```cpp title="linkedlist_stack.cpp" - /* 連結リストに基づくスタッククラス */ + /* 連結リストベースのスタック */ class LinkedListStack { private: - ListNode *stackTop; // 先頭ノードをスタックトップとして使用 + ListNode *stackTop; // 先頭ノードをスタックトップとする int stkSize; // スタックの長さ public: @@ -403,7 +454,7 @@ comments: true } ~LinkedListStack() { - // 連結リストを走査、ノードを削除、メモリを解放 + // 連結リストを走査してノードを削除し、メモリを解放する freeMemoryLinkedList(stackTop); } @@ -430,20 +481,20 @@ comments: true int num = top(); ListNode *tmp = stackTop; stackTop = stackTop->next; - // メモリを解放 + // メモリを解放する delete tmp; stkSize--; return num; } - /* スタックトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ int top() { if (isEmpty()) - throw out_of_range("Stack is empty"); + throw out_of_range("スタックが空です"); return stackTop->val; } - /* リストを配列に変換して返却 */ + /* List を Array に変換して返す */ vector toVector() { ListNode *node = stackTop; vector res(size()); @@ -459,9 +510,9 @@ comments: true === "Java" ```java title="linkedlist_stack.java" - /* 連結リストに基づくスタッククラス */ + /* 連結リストベースのスタック */ class LinkedListStack { - private ListNode stackPeek; // ヘッドノードをスタックトップとして使用 + private ListNode stackPeek; // 先頭ノードをスタックトップとする private int stkSize = 0; // スタックの長さ public LinkedListStack() { @@ -494,7 +545,7 @@ comments: true return num; } - /* スタックトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ public int peek() { if (isEmpty()) throw new IndexOutOfBoundsException(); @@ -517,85 +568,624 @@ comments: true === "C#" ```csharp title="linkedlist_stack.cs" - [class]{LinkedListStack}-[func]{} + /* 連結リストベースのスタック */ + class LinkedListStack { + ListNode? stackPeek; // 先頭ノードをスタックトップとする + int stkSize = 0; // スタックの長さ + + public LinkedListStack() { + stackPeek = null; + } + + /* スタックの長さを取得 */ + public int Size() { + return stkSize; + } + + /* スタックが空かどうかを判定 */ + public bool IsEmpty() { + return Size() == 0; + } + + /* プッシュ */ + public void Push(int num) { + ListNode node = new(num) { + next = stackPeek + }; + stackPeek = node; + stkSize++; + } + + /* ポップ */ + public int Pop() { + int num = Peek(); + stackPeek = stackPeek!.next; + stkSize--; + return num; + } + + /* スタックトップの要素にアクセス */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return stackPeek!.val; + } + + /* List を Array に変換して返す */ + public int[] ToArray() { + if (stackPeek == null) + return []; + + ListNode? node = stackPeek; + int[] res = new int[Size()]; + for (int i = res.Length - 1; i >= 0; i--) { + res[i] = node!.val; + node = node.next; + } + return res; + } + } ``` === "Go" ```go title="linkedlist_stack.go" - [class]{linkedListStack}-[func]{} + /* 連結リストベースのスタック */ + type linkedListStack struct { + // 組み込みパッケージ list でスタックを実装する + data *list.List + } + + /* スタックを初期化 */ + func newLinkedListStack() *linkedListStack { + return &linkedListStack{ + data: list.New(), + } + } + + /* プッシュ */ + func (s *linkedListStack) push(value int) { + s.data.PushBack(value) + } + + /* ポップ */ + func (s *linkedListStack) pop() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + s.data.Remove(e) + return e.Value + } + + /* スタックトップの要素にアクセス */ + func (s *linkedListStack) peek() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + return e.Value + } + + /* スタックの長さを取得 */ + func (s *linkedListStack) size() int { + return s.data.Len() + } + + /* スタックが空かどうかを判定 */ + func (s *linkedListStack) isEmpty() bool { + return s.data.Len() == 0 + } + + /* 表示用に List を取得 */ + func (s *linkedListStack) toList() *list.List { + return s.data + } ``` === "Swift" ```swift title="linkedlist_stack.swift" - [class]{LinkedListStack}-[func]{} + /* 連結リストベースのスタック */ + class LinkedListStack { + private var _peek: ListNode? // 先頭ノードをスタックトップとする + private var _size: Int // スタックの長さ + + init() { + _size = 0 + } + + /* スタックの長さを取得 */ + func size() -> Int { + _size + } + + /* スタックが空かどうかを判定 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* プッシュ */ + func push(num: Int) { + let node = ListNode(x: num) + node.next = _peek + _peek = node + _size += 1 + } + + /* ポップ */ + @discardableResult + func pop() -> Int { + let num = peek() + _peek = _peek?.next + _size -= 1 + return num + } + + /* スタックトップの要素にアクセス */ + func peek() -> Int { + if isEmpty() { + fatalError("スタックが空です") + } + return _peek!.val + } + + /* List を Array に変換して返す */ + func toArray() -> [Int] { + var node = _peek + var res = Array(repeating: 0, count: size()) + for i in res.indices.reversed() { + res[i] = node!.val + node = node?.next + } + return res + } + } ``` === "JS" ```javascript title="linkedlist_stack.js" - [class]{LinkedListStack}-[func]{} + /* 連結リストベースのスタック */ + class LinkedListStack { + #stackPeek; // 先頭ノードをスタックトップとする + #stkSize = 0; // スタックの長さ + + constructor() { + this.#stackPeek = null; + } + + /* スタックの長さを取得 */ + get size() { + return this.#stkSize; + } + + /* スタックが空かどうかを判定 */ + isEmpty() { + return this.size === 0; + } + + /* プッシュ */ + push(num) { + const node = new ListNode(num); + node.next = this.#stackPeek; + this.#stackPeek = node; + this.#stkSize++; + } + + /* ポップ */ + pop() { + const num = this.peek(); + this.#stackPeek = this.#stackPeek.next; + this.#stkSize--; + return num; + } + + /* スタックトップの要素にアクセス */ + peek() { + if (!this.#stackPeek) throw new Error('スタックが空'); + return this.#stackPeek.val; + } + + /* 連結リストを Array に変換して返す */ + toArray() { + let node = this.#stackPeek; + const res = new Array(this.size); + for (let i = res.length - 1; i >= 0; i--) { + res[i] = node.val; + node = node.next; + } + return res; + } + } ``` === "TS" ```typescript title="linkedlist_stack.ts" - [class]{LinkedListStack}-[func]{} + /* 連結リストベースのスタック */ + class LinkedListStack { + private stackPeek: ListNode | null; // 先頭ノードをスタックトップとする + private stkSize: number = 0; // スタックの長さ + + constructor() { + this.stackPeek = null; + } + + /* スタックの長さを取得 */ + get size(): number { + return this.stkSize; + } + + /* スタックが空かどうかを判定 */ + isEmpty(): boolean { + return this.size === 0; + } + + /* プッシュ */ + push(num: number): void { + const node = new ListNode(num); + node.next = this.stackPeek; + this.stackPeek = node; + this.stkSize++; + } + + /* ポップ */ + pop(): number { + const num = this.peek(); + if (!this.stackPeek) throw new Error('スタックが空です'); + this.stackPeek = this.stackPeek.next; + this.stkSize--; + return num; + } + + /* スタックトップの要素にアクセス */ + peek(): number { + if (!this.stackPeek) throw new Error('スタックが空です'); + return this.stackPeek.val; + } + + /* 連結リストを Array に変換して返す */ + toArray(): number[] { + let node = this.stackPeek; + const res = new Array(this.size); + for (let i = res.length - 1; i >= 0; i--) { + res[i] = node!.val; + node = node!.next; + } + return res; + } + } ``` === "Dart" ```dart title="linkedlist_stack.dart" - [class]{LinkedListStack}-[func]{} + /* 連結リストクラスに基づくスタック */ + class LinkedListStack { + ListNode? _stackPeek; // 先頭ノードをスタックトップとする + int _stkSize = 0; // スタックの長さ + + LinkedListStack() { + _stackPeek = null; + } + + /* スタックの長さを取得 */ + int size() { + return _stkSize; + } + + /* スタックが空かどうかを判定 */ + bool isEmpty() { + return _stkSize == 0; + } + + /* プッシュ */ + void push(int _num) { + final ListNode node = ListNode(_num); + node.next = _stackPeek; + _stackPeek = node; + _stkSize++; + } + + /* ポップ */ + int pop() { + final int _num = peek(); + _stackPeek = _stackPeek!.next; + _stkSize--; + return _num; + } + + /* スタックトップの要素にアクセス */ + int peek() { + if (_stackPeek == null) { + throw Exception("スタックが空です"); + } + return _stackPeek!.val; + } + + /* 連結リストを List に変換して返す */ + List toList() { + ListNode? node = _stackPeek; + List list = []; + while (node != null) { + list.add(node.val); + node = node.next; + } + list = list.reversed.toList(); + return list; + } + } ``` === "Rust" ```rust title="linkedlist_stack.rs" - [class]{LinkedListStack}-[func]{} + /* 連結リストベースのスタック */ + #[allow(dead_code)] + pub struct LinkedListStack { + stack_peek: Option>>>, // 先頭ノードをスタックトップとする + stk_size: usize, // スタックの長さ + } + + impl LinkedListStack { + pub fn new() -> Self { + Self { + stack_peek: None, + stk_size: 0, + } + } + + /* スタックの長さを取得 */ + pub fn size(&self) -> usize { + return self.stk_size; + } + + /* スタックが空かどうかを判定 */ + pub fn is_empty(&self) -> bool { + return self.size() == 0; + } + + /* プッシュ */ + pub fn push(&mut self, num: T) { + let node = ListNode::new(num); + node.borrow_mut().next = self.stack_peek.take(); + self.stack_peek = Some(node); + self.stk_size += 1; + } + + /* ポップ */ + pub fn pop(&mut self) -> Option { + self.stack_peek.take().map(|old_head| { + self.stack_peek = old_head.borrow_mut().next.take(); + self.stk_size -= 1; + + old_head.borrow().val + }) + } + + /* スタックトップの要素にアクセス */ + pub fn peek(&self) -> Option<&Rc>>> { + self.stack_peek.as_ref() + } + + /* List を Array に変換して返す */ + pub fn to_array(&self) -> Vec { + fn _to_array(head: Option<&Rc>>>) -> Vec { + if let Some(node) = head { + let mut nums = _to_array(node.borrow().next.as_ref()); + nums.push(node.borrow().val); + return nums; + } + return Vec::new(); + } + + _to_array(self.peek()) + } + } ``` === "C" ```c title="linkedlist_stack.c" - [class]{LinkedListStack}-[func]{} + /* 連結リストベースのスタック */ + typedef struct { + ListNode *top; // 先頭ノードをスタックトップとする + int size; // スタックの長さ + } LinkedListStack; + + /* コンストラクタ */ + LinkedListStack *newLinkedListStack() { + LinkedListStack *s = malloc(sizeof(LinkedListStack)); + s->top = NULL; + s->size = 0; + return s; + } + + /* デストラクタ */ + void delLinkedListStack(LinkedListStack *s) { + while (s->top) { + ListNode *n = s->top->next; + free(s->top); + s->top = n; + } + free(s); + } + + /* スタックの長さを取得 */ + int size(LinkedListStack *s) { + return s->size; + } + + /* スタックが空かどうかを判定 */ + bool isEmpty(LinkedListStack *s) { + return size(s) == 0; + } + + /* プッシュ */ + void push(LinkedListStack *s, int num) { + ListNode *node = (ListNode *)malloc(sizeof(ListNode)); + node->next = s->top; // 新しく追加したノードのポインタフィールドを更新 + node->val = num; // 新しく追加したノードのデータフィールドを更新 + s->top = node; // スタックトップを更新 + s->size++; // スタックサイズを更新 + } + + /* スタックトップの要素にアクセス */ + int peek(LinkedListStack *s) { + if (s->size == 0) { + printf("スタックは空です\n"); + return INT_MAX; + } + return s->top->val; + } + + /* ポップ */ + int pop(LinkedListStack *s) { + int val = peek(s); + ListNode *tmp = s->top; + s->top = s->top->next; + // メモリを解放する + free(tmp); + s->size--; + return val; + } ``` === "Kotlin" ```kotlin title="linkedlist_stack.kt" - [class]{LinkedListStack}-[func]{} + /* 連結リストベースのスタック */ + class LinkedListStack( + private var stackPeek: ListNode? = null, // 先頭ノードをスタックトップとする + private var stkSize: Int = 0 // スタックの長さ + ) { + + /* スタックの長さを取得 */ + fun size(): Int { + return stkSize + } + + /* スタックが空かどうかを判定 */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* プッシュ */ + fun push(num: Int) { + val node = ListNode(num) + node.next = stackPeek + stackPeek = node + stkSize++ + } + + /* ポップ */ + fun pop(): Int? { + val num = peek() + stackPeek = stackPeek?.next + stkSize-- + return num + } + + /* スタックトップの要素にアクセス */ + fun peek(): Int? { + if (isEmpty()) throw IndexOutOfBoundsException() + return stackPeek?._val + } + + /* List を Array に変換して返す */ + fun toArray(): IntArray { + var node = stackPeek + val res = IntArray(size()) + for (i in res.size - 1 downTo 0) { + res[i] = node?._val!! + node = node.next + } + return res + } + } ``` === "Ruby" ```ruby title="linkedlist_stack.rb" - [class]{LinkedListStack}-[func]{} + ### 連結リストで実装したスタック ### + class LinkedListStack + attr_reader :size + + ### コンストラクタ ### + def initialize + @size = 0 + end + + ### スタックが空か判定 ### + def is_empty? + @peek.nil? + end + + ### プッシュ ### + def push(val) + node = ListNode.new(val) + node.next = @peek + @peek = node + @size += 1 + end + + ### ポップ ### + def pop + num = peek + @peek = @peek.next + @size -= 1 + num + end + + ### スタックトップ要素を参照 ### + def peek + raise IndexError, 'スタックは空です' if is_empty? + + @peek.val + end + + ### 連結リストを Array に変換して返す ### + def to_array + arr = [] + node = @peek + while node + arr << node.val + node = node.next + end + arr.reverse + end + end ``` -### 2.   配列ベースの実装 +??? pythontutor "コードの可視化" -配列を使用してスタックを実装する場合、配列の末尾をスタックのトップと考えることができます。下図に示すように、プッシュとポップ操作は、それぞれ配列の末尾での要素の追加と削除に対応し、どちらも時間計算量$O(1)$です。 +
+ -=== "ArrayStack" - ![配列によるスタック実装のプッシュとポップ操作](stack.assets/array_stack_step1.png){ class="animation-figure" } +### 2.   配列による実装 -=== "push()" +配列でスタックを実装する場合、配列の末尾をスタックトップとして扱えます。下図のように、プッシュとポップはそれぞれ配列末尾への要素追加と削除に対応し、どちらの時間計算量も $O(1)$ です。 + +=== "<1>" + ![配列によるスタック実装のプッシュ・ポップ操作](stack.assets/array_stack_step1.png){ class="animation-figure" } + +=== "<2>" ![array_stack_push](stack.assets/array_stack_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![array_stack_pop](stack.assets/array_stack_step3_pop.png){ class="animation-figure" } -

図 5-3   配列によるスタック実装のプッシュとポップ操作

+

図 5-3   配列によるスタック実装のプッシュ・ポップ操作

-スタックにプッシュされる要素が継続的に増加する可能性があるため、動的配列を使用でき、配列拡張を自分で処理する必要がありません。以下はサンプルコードです: +プッシュされる要素は際限なく増える可能性があるため、動的配列を使えば、配列の拡張を自前で処理する必要がありません。以下にコード例を示します: === "Python" ```python title="array_stack.py" class ArrayStack: - """配列ベースのスタッククラス""" + """配列ベースのスタック""" def __init__(self): """コンストラクタ""" @@ -616,24 +1206,24 @@ comments: true def pop(self) -> int: """ポップ""" if self.is_empty(): - raise IndexError("Stack is empty") + raise IndexError("スタックが空です") return self._stack.pop() def peek(self) -> int: - """スタックトップ要素にアクセス""" + """スタックトップの要素にアクセス""" if self.is_empty(): - raise IndexError("Stack is empty") + raise IndexError("スタックが空です") return self._stack[-1] def to_list(self) -> list[int]: - """出力用の配列を返す""" + """表示用のリストを返す""" return self._stack ``` === "C++" ```cpp title="array_stack.cpp" - /* 配列に基づくスタッククラス */ + /* 配列ベースのスタック */ class ArrayStack { private: vector stack; @@ -661,14 +1251,14 @@ comments: true return num; } - /* スタックトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ int top() { if (isEmpty()) - throw out_of_range("Stack is empty"); + throw out_of_range("スタックが空です"); return stack.back(); } - /* Vectorを返却 */ + /* Vector を返す */ vector toVector() { return stack; } @@ -678,12 +1268,12 @@ comments: true === "Java" ```java title="array_stack.java" - /* 配列に基づくスタッククラス */ + /* 配列ベースのスタック */ class ArrayStack { private ArrayList stack; public ArrayStack() { - // リスト(動的配列)を初期化 + // リスト(動的配列)を初期化する stack = new ArrayList<>(); } @@ -709,7 +1299,7 @@ comments: true return stack.remove(size() - 1); } - /* スタックトップ要素にアクセス */ + /* スタックトップの要素にアクセス */ public int peek() { if (isEmpty()) throw new IndexOutOfBoundsException(); @@ -726,89 +1316,522 @@ comments: true === "C#" ```csharp title="array_stack.cs" - [class]{ArrayStack}-[func]{} + /* 配列ベースのスタック */ + class ArrayStack { + List stack; + public ArrayStack() { + // リスト(動的配列)を初期化する + stack = []; + } + + /* スタックの長さを取得 */ + public int Size() { + return stack.Count; + } + + /* スタックが空かどうかを判定 */ + public bool IsEmpty() { + return Size() == 0; + } + + /* プッシュ */ + public void Push(int num) { + stack.Add(num); + } + + /* ポップ */ + public int Pop() { + if (IsEmpty()) + throw new Exception(); + var val = Peek(); + stack.RemoveAt(Size() - 1); + return val; + } + + /* スタックトップの要素にアクセス */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return stack[Size() - 1]; + } + + /* List を Array に変換して返す */ + public int[] ToArray() { + return [.. stack]; + } + } ``` === "Go" ```go title="array_stack.go" - [class]{arrayStack}-[func]{} + /* 配列ベースのスタック */ + type arrayStack struct { + data []int // データ + } + + /* スタックを初期化 */ + func newArrayStack() *arrayStack { + return &arrayStack{ + // スタックの長さを 0、容量を 16 に設定 + data: make([]int, 0, 16), + } + } + + /* スタックの長さ */ + func (s *arrayStack) size() int { + return len(s.data) + } + + /* スタックが空かどうか */ + func (s *arrayStack) isEmpty() bool { + return s.size() == 0 + } + + /* プッシュ */ + func (s *arrayStack) push(v int) { + // スライスは自動で拡張される + s.data = append(s.data, v) + } + + /* ポップ */ + func (s *arrayStack) pop() any { + val := s.peek() + s.data = s.data[:len(s.data)-1] + return val + } + + /* スタックトップ要素を取得する */ + func (s *arrayStack) peek() any { + if s.isEmpty() { + return nil + } + val := s.data[len(s.data)-1] + return val + } + + /* 表示用に Slice を取得 */ + func (s *arrayStack) toSlice() []int { + return s.data + } ``` === "Swift" ```swift title="array_stack.swift" - [class]{ArrayStack}-[func]{} + /* 配列ベースのスタック */ + class ArrayStack { + private var stack: [Int] + + init() { + // リスト(動的配列)を初期化する + stack = [] + } + + /* スタックの長さを取得 */ + func size() -> Int { + stack.count + } + + /* スタックが空かどうかを判定 */ + func isEmpty() -> Bool { + stack.isEmpty + } + + /* プッシュ */ + func push(num: Int) { + stack.append(num) + } + + /* ポップ */ + @discardableResult + func pop() -> Int { + if isEmpty() { + fatalError("スタックが空です") + } + return stack.removeLast() + } + + /* スタックトップの要素にアクセス */ + func peek() -> Int { + if isEmpty() { + fatalError("スタックが空です") + } + return stack.last! + } + + /* List を Array に変換して返す */ + func toArray() -> [Int] { + stack + } + } ``` === "JS" ```javascript title="array_stack.js" - [class]{ArrayStack}-[func]{} + /* 配列ベースのスタック */ + class ArrayStack { + #stack; + constructor() { + this.#stack = []; + } + + /* スタックの長さを取得 */ + get size() { + return this.#stack.length; + } + + /* スタックが空かどうかを判定 */ + isEmpty() { + return this.#stack.length === 0; + } + + /* プッシュ */ + push(num) { + this.#stack.push(num); + } + + /* ポップ */ + pop() { + if (this.isEmpty()) throw new Error('スタックが空'); + return this.#stack.pop(); + } + + /* スタックトップの要素にアクセス */ + top() { + if (this.isEmpty()) throw new Error('スタックが空'); + return this.#stack[this.#stack.length - 1]; + } + + /* Array を返す */ + toArray() { + return this.#stack; + } + } ``` === "TS" ```typescript title="array_stack.ts" - [class]{ArrayStack}-[func]{} + /* 配列ベースのスタック */ + class ArrayStack { + private stack: number[]; + constructor() { + this.stack = []; + } + + /* スタックの長さを取得 */ + get size(): number { + return this.stack.length; + } + + /* スタックが空かどうかを判定 */ + isEmpty(): boolean { + return this.stack.length === 0; + } + + /* プッシュ */ + push(num: number): void { + this.stack.push(num); + } + + /* ポップ */ + pop(): number | undefined { + if (this.isEmpty()) throw new Error('スタックが空です'); + return this.stack.pop(); + } + + /* スタックトップの要素にアクセス */ + top(): number | undefined { + if (this.isEmpty()) throw new Error('スタックが空です'); + return this.stack[this.stack.length - 1]; + } + + /* Array を返す */ + toArray() { + return this.stack; + } + } ``` === "Dart" ```dart title="array_stack.dart" - [class]{ArrayStack}-[func]{} + /* 配列ベースのスタック */ + class ArrayStack { + late List _stack; + ArrayStack() { + _stack = []; + } + + /* スタックの長さを取得 */ + int size() { + return _stack.length; + } + + /* スタックが空かどうかを判定 */ + bool isEmpty() { + return _stack.isEmpty; + } + + /* プッシュ */ + void push(int _num) { + _stack.add(_num); + } + + /* ポップ */ + int pop() { + if (isEmpty()) { + throw Exception("スタックが空です"); + } + return _stack.removeLast(); + } + + /* スタックトップの要素にアクセス */ + int peek() { + if (isEmpty()) { + throw Exception("スタックが空です"); + } + return _stack.last; + } + + /* スタックを Array に変換して返す */ + List toArray() => _stack; + } ``` === "Rust" ```rust title="array_stack.rs" - [class]{ArrayStack}-[func]{} + /* 配列ベースのスタック */ + struct ArrayStack { + stack: Vec, + } + + impl ArrayStack { + /* スタックを初期化 */ + fn new() -> ArrayStack { + ArrayStack:: { + stack: Vec::::new(), + } + } + + /* スタックの長さを取得 */ + fn size(&self) -> usize { + self.stack.len() + } + + /* スタックが空かどうかを判定 */ + fn is_empty(&self) -> bool { + self.size() == 0 + } + + /* プッシュ */ + fn push(&mut self, num: T) { + self.stack.push(num); + } + + /* ポップ */ + fn pop(&mut self) -> Option { + self.stack.pop() + } + + /* スタックトップの要素にアクセス */ + fn peek(&self) -> Option<&T> { + if self.is_empty() { + panic!("スタックが空です") + }; + self.stack.last() + } + + /* &Vec を返す */ + fn to_array(&self) -> &Vec { + &self.stack + } + } ``` === "C" ```c title="array_stack.c" - [class]{ArrayStack}-[func]{} + /* 配列ベースのスタック */ + typedef struct { + int *data; + int size; + } ArrayStack; + + /* コンストラクタ */ + ArrayStack *newArrayStack() { + ArrayStack *stack = malloc(sizeof(ArrayStack)); + // 大きめの容量で初期化し、拡張を避ける + stack->data = malloc(sizeof(int) * MAX_SIZE); + stack->size = 0; + return stack; + } + + /* デストラクタ */ + void delArrayStack(ArrayStack *stack) { + free(stack->data); + free(stack); + } + + /* スタックの長さを取得 */ + int size(ArrayStack *stack) { + return stack->size; + } + + /* スタックが空かどうかを判定 */ + bool isEmpty(ArrayStack *stack) { + return stack->size == 0; + } + + /* プッシュ */ + void push(ArrayStack *stack, int num) { + if (stack->size == MAX_SIZE) { + printf("スタックは満杯です\n"); + return; + } + stack->data[stack->size] = num; + stack->size++; + } + + /* スタックトップの要素にアクセス */ + int peek(ArrayStack *stack) { + if (stack->size == 0) { + printf("スタックは空です\n"); + return INT_MAX; + } + return stack->data[stack->size - 1]; + } + + /* ポップ */ + int pop(ArrayStack *stack) { + int val = peek(stack); + stack->size--; + return val; + } ``` === "Kotlin" ```kotlin title="array_stack.kt" - [class]{ArrayStack}-[func]{} + /* 配列ベースのスタック */ + class ArrayStack { + // リスト(動的配列)を初期化する + private val stack = mutableListOf() + + /* スタックの長さを取得 */ + fun size(): Int { + return stack.size + } + + /* スタックが空かどうかを判定 */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* プッシュ */ + fun push(num: Int) { + stack.add(num) + } + + /* ポップ */ + fun pop(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return stack.removeAt(size() - 1) + } + + /* スタックトップの要素にアクセス */ + fun peek(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return stack[size() - 1] + } + + /* List を Array に変換して返す */ + fun toArray(): Array { + return stack.toTypedArray() + } + } ``` === "Ruby" ```ruby title="array_stack.rb" - [class]{ArrayStack}-[func]{} + ### 配列で実装したスタック ### + class ArrayStack + ### コンストラクタ ### + def initialize + @stack = [] + end + + ### スタックの長さを取得 ### + def size + @stack.length + end + + ### スタックが空か判定 ### + def is_empty? + @stack.empty? + end + + ### プッシュ ### + def push(item) + @stack << item + end + + ### ポップ ### + def pop + raise IndexError, 'スタックは空です' if is_empty? + + @stack.pop + end + + ### スタックトップ要素を参照 ### + def peek + raise IndexError, 'スタックは空です' if is_empty? + + @stack.last + end + + ### 表示用のリストを返す ### + def to_array + @stack + end + end ``` +??? pythontutor "コードの可視化" + +
+ + ## 5.1.3   2つの実装の比較 -**サポートされる操作** +**対応する操作** -両方の実装は、スタックで定義されたすべての操作をサポートします。配列実装はさらにランダムアクセスをサポートしますが、これはスタック定義の範囲を超えており、一般的には使用されません。 +どちらの実装も、スタックの定義に含まれる各種操作をサポートします。配列ベースの実装はランダムアクセスも可能ですが、これはスタックの定義範囲を超えているため、通常は利用しません。 **時間効率** -配列ベースの実装では、プッシュとポップ操作の両方が事前に割り当てられた連続メモリで発生し、良好なキャッシュ局所性があるため効率が高くなります。しかし、プッシュ操作が配列容量を超える場合、リサイズメカニズムがトリガーされ、そのプッシュ操作の時間計算量は$O(n)$になります。 +配列ベースの実装では、プッシュとポップの両方があらかじめ確保された連続メモリ上で行われるため、キャッシュ局所性が高く、効率に優れます。ただし、プッシュ時に配列容量を超えると拡張処理が発生し、その1回のプッシュの時間計算量は $O(n)$ になります。 -連結リスト実装では、リスト拡張は非常に柔軟で、配列拡張のような効率低下の問題はありません。しかし、プッシュ操作にはノードオブジェクトの初期化とポインタの変更が必要なため、効率は比較的低くなります。プッシュされる要素がすでにノードオブジェクトの場合、初期化ステップをスキップでき、効率が向上します。 +連結リストベースの実装では、サイズ拡張が非常に柔軟であり、前述のような配列拡張による効率低下はありません。ただし、プッシュ時にはノードオブジェクトの初期化とポインタの更新が必要になるため、効率は相対的に低くなります。もっとも、プッシュする要素自体がノードオブジェクトであれば、初期化の手間を省けるため、効率を高められます。 -したがって、プッシュとポップ操作の要素が`int`や`double`などの基本データ型の場合、以下の結論を導くことができます: +以上を踏まえると、プッシュおよびポップの対象が `int` や `double` のような基本データ型である場合、次の結論が得られます。 -- 配列ベースのスタック実装は拡張時に効率が低下しますが、拡張は低頻度操作であるため、平均効率は高くなります。 -- 連結リストベースのスタック実装はより安定した効率パフォーマンスを提供します。 +- 配列ベースのスタックは拡張時に効率が低下しますが、拡張は低頻度の操作であるため、平均効率はより高くなります。 +- 連結リストベースのスタックは、より安定した効率を提供できます。 **空間効率** -リストを初期化する際、システムは「初期容量」を割り当てますが、これは実際の必要量を超える可能性があります。さらに、拡張メカニズムは通常、特定の係数(2倍など)で容量を増加させ、これも実際の必要量を超える可能性があります。したがって、**配列ベースのスタックは一部の空間を無駄にする可能性があります**。 +リストを初期化するとき、システムは「初期容量」を割り当てますが、この容量は実際の必要量を上回ることがあります。また、拡張は通常、一定の倍率(たとえば2倍)で行われるため、拡張後の容量も実際の必要量を超える可能性があります。したがって、**配列ベースのスタックは一定のメモリ浪費を招く可能性があります。** -しかし、連結リストノードはポインタを格納するための追加空間が必要なため、**連結リストノードが占有する空間は比較的大きくなります**。 +一方で、連結リストのノードはポインタを追加で保持する必要があるため、**連結リストノードは相対的に大きな領域を占有します。** -まとめると、どちらの実装がよりメモリ効率的かを単純に判断することはできません。特定の状況に基づく分析が必要です。 +以上より、どちらの実装がより省メモリかを単純に断定することはできず、具体的な状況に応じて分析する必要があります。 ## 5.1.4   スタックの典型的な応用 -- **ブラウザの戻ると進む、ソフトウェアの元に戻すとやり直し**。新しいWebページを開くたびに、ブラウザは前のページをスタックにプッシュし、戻る操作(本質的にはポップ操作)を通じて前のページに戻ることができます。戻ると進むの両方をサポートするには、2つのスタックが連携して動作する必要があります。 -- **プログラムのメモリ管理**。関数が呼び出されるたびに、システムはスタックのトップにスタックフレームを追加して関数のコンテキスト情報を記録します。再帰関数では、下方向の再帰フェーズはスタックへのプッシュを続け、上方向のバックトラッキングフェーズはスタックからのポップを続けます。 +- **ブラウザにおける戻ると進む、ソフトウェアにおける取り消しとやり直し**。新しいWebページを開くたびに、ブラウザは直前のページをスタックにプッシュするため、戻る操作によって前のページに戻れます。戻る操作は実際にはポップに相当します。戻ると進むを同時にサポートするには、2つのスタックを組み合わせて実現する必要があります。 +- **プログラムのメモリ管理**。関数を呼び出すたびに、システムはスタックトップにスタックフレームを追加し、関数のコンテキスト情報を記録します。再帰関数では、下向きに再帰していく段階でプッシュが繰り返され、上向きにバックトラックする段階でポップが繰り返されます。 diff --git a/ja/docs/chapter_stack_and_queue/summary.md b/ja/docs/chapter_stack_and_queue/summary.md index 605c25d69..050618432 100644 --- a/ja/docs/chapter_stack_and_queue/summary.md +++ b/ja/docs/chapter_stack_and_queue/summary.md @@ -4,32 +4,32 @@ comments: true # 5.4   まとめ -### 1.   重要なポイント +### 1.   要点の振り返り -- スタックは後入れ先出し(LIFO)の原則に従うデータ構造で、配列または連結リストを使って実装できます。 -- 時間効率の観点では、スタックの配列実装の方が平均的な効率が高いです。ただし、拡張時には単一のプッシュ操作の時間計算量が$O(n)$に悪化する可能性があります。対照的に、スタックの連結リスト実装はより安定した効率を提供します。 -- 空間効率に関しては、スタックの配列実装は一定程度の空間の無駄につながる可能性があります。ただし、連結リストのノードが占有するメモリ空間は一般的に配列の要素よりも大きいことに注意することが重要です。 -- キューは先入れ先出し(FIFO)の原則に従うデータ構造で、同様に配列または連結リストを使って実装できます。キューの時間と空間効率に関する結論は、スタックと似ています。 -- 両端キュー(deque)はより柔軟なキューの種類で、両端での要素の追加と削除を可能にします。 +- スタックは後入れ先出しの原則に従うデータ構造であり、配列または連結リストで実装できます。 +- 時間効率の面では、スタックの配列実装は平均効率が高い一方、拡張時には 1 回のプッシュ操作の時間計算量が $O(n)$ まで悪化します。これに対して、スタックの連結リスト実装はより安定した効率を示します。 +- 空間効率の面では、スタックの配列実装はある程度の領域の無駄を生む可能性があります。ただし、連結リストのノードが占有するメモリは配列要素よりも大きい点に注意が必要です。 +- キューは先入れ先出しの原則に従うデータ構造であり、同様に配列または連結リストで実装できます。時間効率と空間効率の比較における結論は、前述のスタックの場合と似ています。 +- 両端キューはより高い自由度を持つキューであり、両端で要素の追加と削除を行えます。 ### 2.   Q & A -**Q**: ブラウザの進む・戻る機能は双方向連結リストで実装されているのですか? +**Q**:ブラウザの進む・戻るは双方向連結リストで実装されているのですか? -ブラウザの進む・戻るナビゲーションは本質的に「スタック」概念の現れです。ユーザーが新しいページを訪問すると、そのページがスタックの先頭に追加されます。戻るボタンをクリックすると、ページがスタックの先頭からポップされます。両端キュー(deque)は、「両端キュー」の章で述べたように、いくつかの追加操作を便利に実装できます。 +ブラウザの進む・戻る機能の本質は「スタック」の表れです。ユーザーが新しいページにアクセスすると、そのページはスタックの先頭に追加されます。ユーザーが戻るボタンをクリックすると、そのページはスタックの先頭から取り出されます。両端キューを使うといくつかの追加操作を簡単に実装でき、この点は「両端キュー」の章で触れています。 -**Q**: スタックからポップした後、ポップされたノードのメモリを解放する必要がありますか? +**Q**:ポップした後、そのノードのメモリを解放する必要はありますか? -ポップされたノードが後で使用される場合は、そのメモリを解放する必要はありません。自動ガベージコレクションを持つJavaやPythonなどの言語では、手動のメモリ解放は必要ありません。CやC++では、手動のメモリ解放が必要です。 +後で取り出したノードを引き続き使うのであれば、メモリを解放する必要はありません。以降そのノードを使わない場合でも、`Java` や `Python` などの言語には自動ガベージコレクション機構があるため、手動でメモリを解放する必要はありません。一方、`C` と `C++` では手動でメモリを解放する必要があります。 -**Q**: 両端キューは2つのスタックを結合したもののように見えます。その用途は何ですか? +**Q**:両端キューは 2 つのスタックをつなげたように見えますが、用途は何ですか? -両端キューは、スタックとキューの組み合わせまたは2つのスタックを結合したもので、スタックとキューの両方のロジックを示します。したがって、スタックとキューのすべてのアプリケーションを実装でき、より大きな柔軟性を提供します。 +両端キューは、スタックとキューの組み合わせ、あるいは 2 つのスタックをつなげたもののような構造です。表しているのはスタック + キューのロジックなので、スタックとキューのすべての応用を実現でき、しかもより柔軟です。 -**Q**: 元に戻すとやり直しは具体的にどのように実装されるのですか? +**Q**:取り消し(undo)とやり直し(redo)は具体的にどのように実装されますか? -元に戻すとやり直しの操作は2つのスタックを使って実装されます:元に戻す用のスタック`A`とやり直し用のスタック`B`です。 +2 つのスタックを使い、スタック `A` を取り消し用、スタック `B` をやり直し用に使います。 -1. ユーザーが操作を実行するたびに、それがスタック`A`にプッシュされ、スタック`B`がクリアされます。 -2. ユーザーが「元に戻す」を実行すると、最新の操作がスタック`A`からポップされ、スタック`B`にプッシュされます。 -3. ユーザーが「やり直し」を実行すると、最新の操作がスタック`B`からポップされ、スタック`A`に戻されます。 +1. ユーザーが操作を 1 つ実行するたびに、その操作をスタック `A` にプッシュし、スタック `B` を空にします。 +2. ユーザーが「取り消し」を実行したときは、スタック `A` から直近の操作をポップし、それをスタック `B` にプッシュします。 +3. ユーザーが「やり直し」を実行したときは、スタック `B` から直近の操作をポップし、それをスタック `A` にプッシュします。 diff --git a/ja/docs/chapter_tree/array_representation_of_tree.md b/ja/docs/chapter_tree/array_representation_of_tree.md index 2a4b005d8..0282a156c 100644 --- a/ja/docs/chapter_tree/array_representation_of_tree.md +++ b/ja/docs/chapter_tree/array_representation_of_tree.md @@ -4,39 +4,39 @@ comments: true # 7.3   二分木の配列表現 -連結リスト表現では、二分木の格納単位はノード`TreeNode`であり、ノードはポインタによって接続されます。連結リスト表現での二分木の基本操作については前の節で紹介しました。 +連結リスト表現では、二分木の記憶単位はノード `TreeNode` であり、ノード同士はポインタによって接続されます。前節では、連結リスト表現における二分木の各種基本操作を紹介しました。 -では、配列を使って二分木を表現することはできるでしょうか?答えはイエスです。 +では、配列で二分木を表現できるでしょうか?答えはもちろん可能です。 -## 7.3.1   完全二分木の表現 +## 7.3.1   充足二分木を表現する -まず簡単なケースから分析してみましょう。完全二分木が与えられたとき、レベル順探索の順序に従ってすべてのノードを配列に格納し、各ノードは一意の配列インデックスに対応します。 +まずは簡単な例を考えます。与えられた 1 本の充足二分木について、すべてのノードをレベル順走査の順に配列へ格納すると、各ノードは一意な配列インデックスに対応します。 -レベル順探索の特性に基づいて、親ノードのインデックスとその子ノードの間の「マッピング公式」を導き出すことができます:**ノードのインデックスが$i$の場合、その左の子のインデックスは$2i + 1$、右の子のインデックスは$2i + 2$です**。下図は、さまざまなノードのインデックス間のマッピング関係を示しています。 +レベル順走査の性質に基づくと、親ノードのインデックスと子ノードのインデックスの間にある「対応式」を導けます。**あるノードのインデックスが $i$ なら、その左子ノードのインデックスは $2i + 1$ 、右子ノードのインデックスは $2i + 2$ です**。以下の図は、各ノードインデックス間の対応関係を示しています。 -![完全二分木の配列表現](array_representation_of_tree.assets/array_representation_binary_tree.png){ class="animation-figure" } +![充足二分木の配列表現](array_representation_of_tree.assets/array_representation_binary_tree.png){ class="animation-figure" } -

図 7-12   完全二分木の配列表現

+

図 7-12   充足二分木の配列表現

-**マッピング公式は、連結リストのノード参照(ポインタ)と同様の役割を果たします**。配列内の任意のノードが与えられたとき、マッピング公式を使用してその左(右)の子ノードにアクセスできます。 +**対応式は、連結リストにおけるノード参照(ポインタ)と同じ役割を果たします**。与えられた配列内の任意のノードについて、この対応式を使えばその左(右)子ノードにアクセスできます。 -## 7.3.2   任意の二分木の表現 +## 7.3.2   任意の二分木を表現する -完全二分木は特別なケースです。二分木の中間レベルには多くの`None`値が存在することがよくあります。レベル順探索のシーケンスにはこれらの`None`値が含まれないため、このシーケンスだけに依存して`None`値の数と分布を推測することはできません。**つまり、複数の二分木構造が同じレベル順探索シーケンスと一致する可能性があります**。 +充足二分木は特殊なケースであり、一般の二分木では中間層に多数の `None` が存在することがよくあります。レベル順走査の列にはこれらの `None` が含まれないため、その列だけから `None` の数や分布位置を推定することはできません。**つまり、このレベル順走査列に一致する二分木構造は複数存在し得ます**。 -下図に示すように、完全でない二分木が与えられた場合、上記の配列表現方法は失敗します。 +次の図のように、非充足二分木が与えられると、上記の配列表現はすでに成り立ちません。 -![レベル順探索シーケンスが複数の二分木の可能性に対応](array_representation_of_tree.assets/array_representation_without_empty.png){ class="animation-figure" } +![レベル順走査列に対応する複数の二分木の可能性](array_representation_of_tree.assets/array_representation_without_empty.png){ class="animation-figure" } -

図 7-13   レベル順探索シーケンスが複数の二分木の可能性に対応

+

図 7-13   レベル順走査列に対応する複数の二分木の可能性

-この問題を解決するために、**レベル順探索シーケンスですべての`None`値を明示的に書き出すことを検討できます**。下図に示すように、この処理後、レベル順探索シーケンスは二分木を一意に表現できます。サンプルコードは以下の通りです: +この問題を解決するために、**レベル順走査列にすべての `None` を明示的に書き込む**ことを考えられます。次の図のように、このように処理すればレベル順走査列で二分木を一意に表現できます。コード例は以下のとおりです: === "Python" ```python title="" # 二分木の配列表現 - # Noneを使用して空のスロットを表現 + # 空き位置を表すために None を使う tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15] ``` @@ -44,7 +44,7 @@ comments: true ```cpp title="" /* 二分木の配列表現 */ - // 最大整数値INT_MAXを使用して空のスロットをマーク + // int の最大値 INT_MAX を使って空き位置を示す vector tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15}; ``` @@ -52,7 +52,7 @@ comments: true ```java title="" /* 二分木の配列表現 */ - // Integerラッパークラスを使用してnullで空のスロットをマーク + // int のラッパークラス Integer を使えば、null で空き位置を示せる Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 }; ``` @@ -60,7 +60,7 @@ comments: true ```csharp title="" /* 二分木の配列表現 */ - // nullable int (int?)を使用してnullで空のスロットをマーク + // nullable な int? 型を使えば、null で空き位置を示せる int?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; ``` @@ -68,7 +68,7 @@ comments: true ```go title="" /* 二分木の配列表現 */ - // any型スライスを使用してnilで空のスロットをマーク + // any 型のスライスを使えば、nil で空き位置を示せる tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15} ``` @@ -76,7 +76,7 @@ comments: true ```swift title="" /* 二分木の配列表現 */ - // optional Int (Int?)を使用してnilで空のスロットをマーク + // nullable な Int? 型を使えば、nil で空き位置を示せる let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] ``` @@ -84,7 +84,7 @@ comments: true ```javascript title="" /* 二分木の配列表現 */ - // nullを使用して空のスロットを表現 + // null を使って空き位置を表す let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; ``` @@ -92,7 +92,7 @@ comments: true ```typescript title="" /* 二分木の配列表現 */ - // nullを使用して空のスロットを表現 + // null を使って空き位置を表す let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; ``` @@ -100,7 +100,7 @@ comments: true ```dart title="" /* 二分木の配列表現 */ - // nullable int (int?)を使用してnullで空のスロットをマーク + // nullable な int? 型を使えば、null で空き位置を示せる List tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; ``` @@ -108,7 +108,7 @@ comments: true ```rust title="" /* 二分木の配列表現 */ - // Noneを使用して空のスロットをマーク + // None を使って空き位置を示す let tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)]; ``` @@ -116,7 +116,7 @@ comments: true ```c title="" /* 二分木の配列表現 */ - // 最大int値を使用して空のスロットをマーク、したがってノード値はINT_MAXであってはならない + // int の最大値で空き位置を示すため、ノード値は INT_MAX であってはならない int tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15}; ``` @@ -124,80 +124,82 @@ comments: true ```kotlin title="" /* 二分木の配列表現 */ - // nullを使用して空のスロットを表現 - val tree = mutableListOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 ) + // null を使って空き位置を表す + val tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 ) ``` === "Ruby" ```ruby title="" - + ### 二分木の配列表現 ### + # nil を使って空き位置を表す + tree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] ``` -![任意の種類の二分木の配列表現](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" } +![任意の二分木の配列表現](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" } -

図 7-14   任意の種類の二分木の配列表現

+

図 7-14   任意の二分木の配列表現

-注目すべきは、**完備二分木は配列表現に非常に適している**ということです。完備二分木の定義を思い出すと、`None`は最下位レベルでのみ、かつ右側に向かって現れます。**つまり、すべての`None`値は確実にレベル順探索シーケンスの最後に現れます**。 +補足すると、**完全二分木は配列による表現に非常に適しています**。完全二分木の定義を振り返ると、`None` は最下層の右側にしか現れないため、**すべての `None` は必ずレベル順走査列の末尾に現れます**。 -これは、配列を使用して完備二分木を表現する際、すべての`None`値の格納を省略できることを意味し、非常に便利です。下図に例を示します。 +つまり、完全二分木を配列で表す場合は、すべての `None` の格納を省略できるため、非常に便利です。次の図に例を示します。 -![完備二分木の配列表現](array_representation_of_tree.assets/array_representation_complete_binary_tree.png){ class="animation-figure" } +![完全二分木の配列表現](array_representation_of_tree.assets/array_representation_complete_binary_tree.png){ class="animation-figure" } -

図 7-15   完備二分木の配列表現

+

図 7-15   完全二分木の配列表現

-以下のコードは、配列表現に基づく二分木を実装し、次の操作を含みます: +以下のコードでは、配列ベースで表現した二分木を実装しており、次の操作を含みます。 -- ノードが与えられたとき、その値、左(右)の子ノード、および親ノードを取得する。 -- 前順、中順、後順、およびレベル順探索シーケンスを取得する。 +- あるノードが与えられたとき、その値、左(右)子ノード、親ノードを取得する。 +- 前順走査、中順走査、後順走査、レベル順走査の列を取得する。 === "Python" ```python title="array_binary_tree.py" class ArrayBinaryTree: - """配列ベースの二分木クラス""" + """配列表現による二分木クラス""" def __init__(self, arr: list[int | None]): """コンストラクタ""" self._tree = list(arr) def size(self): - """リストの容量""" + """リスト容量""" return len(self._tree) def val(self, i: int) -> int | None: - """インデックスiのノードの値を取得""" - # インデックスが範囲外の場合、Noneを返し、空席を表す + """インデックス i のノードの値を取得""" + # インデックスが範囲外なら、空きを表す None を返す if i < 0 or i >= self.size(): return None return self._tree[i] def left(self, i: int) -> int | None: - """インデックスiのノードの左の子のインデックスを取得""" + """インデックス i のノードの左子ノードのインデックスを取得""" return 2 * i + 1 def right(self, i: int) -> int | None: - """インデックスiのノードの右の子のインデックスを取得""" + """インデックス i のノードの右子ノードのインデックスを取得""" return 2 * i + 2 def parent(self, i: int) -> int | None: - """インデックスiのノードの親のインデックスを取得""" + """インデックス i のノードの親ノードのインデックスを取得""" return (i - 1) // 2 def level_order(self) -> list[int]: """レベル順走査""" self.res = [] - # 配列を走査 + # 配列を直接走査する for i in range(self.size()): if self.val(i) is not None: self.res.append(self.val(i)) return self.res def dfs(self, i: int, order: str): - """深さ優先走査""" + """深さ優先探索""" if self.val(i) is None: return - # 前順走査 + # 先行順走査 if order == "pre": self.res.append(self.val(i)) self.dfs(self.left(i), order) @@ -210,7 +212,7 @@ comments: true self.res.append(self.val(i)) def pre_order(self) -> list[int]: - """前順走査""" + """先行順走査""" self.res = [] self.dfs(0, order="pre") return self.res @@ -231,7 +233,7 @@ comments: true === "C++" ```cpp title="array_binary_tree.cpp" - /* 配列ベースの二分木クラス */ + /* 配列表現による二分木クラス */ class ArrayBinaryTree { public: /* コンストラクタ */ @@ -239,30 +241,30 @@ comments: true tree = arr; } - /* リストの容量 */ + /* リスト容量 */ int size() { return tree.size(); } /* インデックス i のノードの値を取得 */ int val(int i) { - // インデックスが範囲外の場合、INT_MAX を返す(null を表す) + // インデックスが範囲外なら、空きを表す INT_MAX を返す if (i < 0 || i >= size()) return INT_MAX; return tree[i]; } - /* インデックス i のノードの左の子のインデックスを取得 */ + /* インデックス i のノードの左子ノードのインデックスを取得 */ int left(int i) { return 2 * i + 1; } - /* インデックス i のノードの右の子のインデックスを取得 */ + /* インデックス i のノードの右子ノードのインデックスを取得 */ int right(int i) { return 2 * i + 2; } - /* インデックス i のノードの親のインデックスを取得 */ + /* インデックス i のノードの親ノードのインデックスを取得 */ int parent(int i) { return (i - 1) / 2; } @@ -270,7 +272,7 @@ comments: true /* レベル順走査 */ vector levelOrder() { vector res; - // 配列を走査 + // 配列を直接走査する for (int i = 0; i < size(); i++) { if (val(i) != INT_MAX) res.push_back(val(i)); @@ -278,7 +280,7 @@ comments: true return res; } - /* 前順走査 */ + /* 先行順走査 */ vector preOrder() { vector res; dfs(0, "pre", res); @@ -302,12 +304,12 @@ comments: true private: vector tree; - /* 深さ優先走査 */ + /* 深さ優先探索 */ void dfs(int i, string order, vector &res) { - // 空の位置の場合、戻る + // 空きスロットなら返す if (val(i) == INT_MAX) return; - // 前順走査 + // 先行順走査 if (order == "pre") res.push_back(val(i)); dfs(left(i), order, res); @@ -325,7 +327,7 @@ comments: true === "Java" ```java title="array_binary_tree.java" - /* 配列ベースの二分木クラス */ + /* 配列表現による二分木クラス */ class ArrayBinaryTree { private List tree; @@ -334,30 +336,30 @@ comments: true tree = new ArrayList<>(arr); } - /* リストの容量 */ + /* リスト容量 */ public int size() { return tree.size(); } /* インデックス i のノードの値を取得 */ public Integer val(int i) { - // インデックスが範囲外の場合、null を返す(空の位置を表す) + // インデックスが範囲外なら、空きを表す null を返す if (i < 0 || i >= size()) return null; return tree.get(i); } - /* インデックス i のノードの左の子のインデックスを取得 */ + /* インデックス i のノードの左子ノードのインデックスを取得 */ public Integer left(int i) { return 2 * i + 1; } - /* インデックス i のノードの右の子のインデックスを取得 */ + /* インデックス i のノードの右子ノードのインデックスを取得 */ public Integer right(int i) { return 2 * i + 2; } - /* インデックス i のノードの親のインデックスを取得 */ + /* インデックス i のノードの親ノードのインデックスを取得 */ public Integer parent(int i) { return (i - 1) / 2; } @@ -365,7 +367,7 @@ comments: true /* レベル順走査 */ public List levelOrder() { List res = new ArrayList<>(); - // 配列を走査 + // 配列を直接走査する for (int i = 0; i < size(); i++) { if (val(i) != null) res.add(val(i)); @@ -373,12 +375,12 @@ comments: true return res; } - /* 深さ優先走査 */ + /* 深さ優先探索 */ private void dfs(Integer i, String order, List res) { - // 空の位置の場合、戻る + // 空きスロットなら返す if (val(i) == null) return; - // 前順走査 + // 先行順走査 if ("pre".equals(order)) res.add(val(i)); dfs(left(i), order, res); @@ -391,7 +393,7 @@ comments: true res.add(val(i)); } - /* 前順走査 */ + /* 先行順走査 */ public List preOrder() { List res = new ArrayList<>(); dfs(0, "pre", res); @@ -417,73 +419,930 @@ comments: true === "C#" ```csharp title="array_binary_tree.cs" - [class]{ArrayBinaryTree}-[func]{} + /* 配列表現による二分木クラス */ + class ArrayBinaryTree(List arr) { + List tree = new(arr); + + /* リスト容量 */ + public int Size() { + return tree.Count; + } + + /* インデックス i のノードの値を取得 */ + public int? Val(int i) { + // インデックスが範囲外なら、空きを表す null を返す + if (i < 0 || i >= Size()) + return null; + return tree[i]; + } + + /* インデックス i のノードの左子ノードのインデックスを取得 */ + public int Left(int i) { + return 2 * i + 1; + } + + /* インデックス i のノードの右子ノードのインデックスを取得 */ + public int Right(int i) { + return 2 * i + 2; + } + + /* インデックス i のノードの親ノードのインデックスを取得 */ + public int Parent(int i) { + return (i - 1) / 2; + } + + /* レベル順走査 */ + public List LevelOrder() { + List res = []; + // 配列を直接走査する + for (int i = 0; i < Size(); i++) { + if (Val(i).HasValue) + res.Add(Val(i)!.Value); + } + return res; + } + + /* 深さ優先探索 */ + void DFS(int i, string order, List res) { + // 空きスロットなら返す + if (!Val(i).HasValue) + return; + // 先行順走査 + if (order == "pre") + res.Add(Val(i)!.Value); + DFS(Left(i), order, res); + // 中順走査 + if (order == "in") + res.Add(Val(i)!.Value); + DFS(Right(i), order, res); + // 後順走査 + if (order == "post") + res.Add(Val(i)!.Value); + } + + /* 先行順走査 */ + public List PreOrder() { + List res = []; + DFS(0, "pre", res); + return res; + } + + /* 中順走査 */ + public List InOrder() { + List res = []; + DFS(0, "in", res); + return res; + } + + /* 後順走査 */ + public List PostOrder() { + List res = []; + DFS(0, "post", res); + return res; + } + } ``` === "Go" ```go title="array_binary_tree.go" - [class]{arrayBinaryTree}-[func]{} + /* 配列表現による二分木クラス */ + type arrayBinaryTree struct { + tree []any + } + + /* コンストラクタ */ + func newArrayBinaryTree(arr []any) *arrayBinaryTree { + return &arrayBinaryTree{ + tree: arr, + } + } + + /* リスト容量 */ + func (abt *arrayBinaryTree) size() int { + return len(abt.tree) + } + + /* インデックス i のノードの値を取得 */ + func (abt *arrayBinaryTree) val(i int) any { + // インデックスが範囲外なら、空きを表す null を返す + if i < 0 || i >= abt.size() { + return nil + } + return abt.tree[i] + } + + /* インデックス i のノードの左子ノードのインデックスを取得 */ + func (abt *arrayBinaryTree) left(i int) int { + return 2*i + 1 + } + + /* インデックス i のノードの右子ノードのインデックスを取得 */ + func (abt *arrayBinaryTree) right(i int) int { + return 2*i + 2 + } + + /* インデックス i のノードの親ノードのインデックスを取得 */ + func (abt *arrayBinaryTree) parent(i int) int { + return (i - 1) / 2 + } + + /* レベル順走査 */ + func (abt *arrayBinaryTree) levelOrder() []any { + var res []any + // 配列を直接走査する + for i := 0; i < abt.size(); i++ { + if abt.val(i) != nil { + res = append(res, abt.val(i)) + } + } + return res + } + + /* 深さ優先探索 */ + func (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) { + // 空きスロットなら返す + if abt.val(i) == nil { + return + } + // 先行順走査 + if order == "pre" { + *res = append(*res, abt.val(i)) + } + abt.dfs(abt.left(i), order, res) + // 中順走査 + if order == "in" { + *res = append(*res, abt.val(i)) + } + abt.dfs(abt.right(i), order, res) + // 後順走査 + if order == "post" { + *res = append(*res, abt.val(i)) + } + } + + /* 先行順走査 */ + func (abt *arrayBinaryTree) preOrder() []any { + var res []any + abt.dfs(0, "pre", &res) + return res + } + + /* 中順走査 */ + func (abt *arrayBinaryTree) inOrder() []any { + var res []any + abt.dfs(0, "in", &res) + return res + } + + /* 後順走査 */ + func (abt *arrayBinaryTree) postOrder() []any { + var res []any + abt.dfs(0, "post", &res) + return res + } ``` === "Swift" ```swift title="array_binary_tree.swift" - [class]{ArrayBinaryTree}-[func]{} + /* 配列表現による二分木クラス */ + class ArrayBinaryTree { + private var tree: [Int?] + + /* コンストラクタ */ + init(arr: [Int?]) { + tree = arr + } + + /* リスト容量 */ + func size() -> Int { + tree.count + } + + /* インデックス i のノードの値を取得 */ + func val(i: Int) -> Int? { + // インデックスが範囲外なら、空きを表す null を返す + if i < 0 || i >= size() { + return nil + } + return tree[i] + } + + /* インデックス i のノードの左子ノードのインデックスを取得 */ + func left(i: Int) -> Int { + 2 * i + 1 + } + + /* インデックス i のノードの右子ノードのインデックスを取得 */ + func right(i: Int) -> Int { + 2 * i + 2 + } + + /* インデックス i のノードの親ノードのインデックスを取得 */ + func parent(i: Int) -> Int { + (i - 1) / 2 + } + + /* レベル順走査 */ + func levelOrder() -> [Int] { + var res: [Int] = [] + // 配列を直接走査する + for i in 0 ..< size() { + if let val = val(i: i) { + res.append(val) + } + } + return res + } + + /* 深さ優先探索 */ + private func dfs(i: Int, order: String, res: inout [Int]) { + // 空きスロットなら返す + guard let val = val(i: i) else { + return + } + // 先行順走査 + if order == "pre" { + res.append(val) + } + dfs(i: left(i: i), order: order, res: &res) + // 中順走査 + if order == "in" { + res.append(val) + } + dfs(i: right(i: i), order: order, res: &res) + // 後順走査 + if order == "post" { + res.append(val) + } + } + + /* 先行順走査 */ + func preOrder() -> [Int] { + var res: [Int] = [] + dfs(i: 0, order: "pre", res: &res) + return res + } + + /* 中順走査 */ + func inOrder() -> [Int] { + var res: [Int] = [] + dfs(i: 0, order: "in", res: &res) + return res + } + + /* 後順走査 */ + func postOrder() -> [Int] { + var res: [Int] = [] + dfs(i: 0, order: "post", res: &res) + return res + } + } ``` === "JS" ```javascript title="array_binary_tree.js" - [class]{ArrayBinaryTree}-[func]{} + /* 配列表現による二分木クラス */ + class ArrayBinaryTree { + #tree; + + /* コンストラクタ */ + constructor(arr) { + this.#tree = arr; + } + + /* リスト容量 */ + size() { + return this.#tree.length; + } + + /* インデックス i のノードの値を取得 */ + val(i) { + // インデックスが範囲外なら、空きを表す null を返す + if (i < 0 || i >= this.size()) return null; + return this.#tree[i]; + } + + /* インデックス i のノードの左子ノードのインデックスを取得 */ + left(i) { + return 2 * i + 1; + } + + /* インデックス i のノードの右子ノードのインデックスを取得 */ + right(i) { + return 2 * i + 2; + } + + /* インデックス i のノードの親ノードのインデックスを取得 */ + parent(i) { + return Math.floor((i - 1) / 2); // 切り捨て除算 + } + + /* レベル順走査 */ + levelOrder() { + let res = []; + // 配列を直接走査する + for (let i = 0; i < this.size(); i++) { + if (this.val(i) !== null) res.push(this.val(i)); + } + return res; + } + + /* 深さ優先探索 */ + #dfs(i, order, res) { + // 空きスロットなら返す + if (this.val(i) === null) return; + // 先行順走査 + if (order === 'pre') res.push(this.val(i)); + this.#dfs(this.left(i), order, res); + // 中順走査 + if (order === 'in') res.push(this.val(i)); + this.#dfs(this.right(i), order, res); + // 後順走査 + if (order === 'post') res.push(this.val(i)); + } + + /* 先行順走査 */ + preOrder() { + const res = []; + this.#dfs(0, 'pre', res); + return res; + } + + /* 中順走査 */ + inOrder() { + const res = []; + this.#dfs(0, 'in', res); + return res; + } + + /* 後順走査 */ + postOrder() { + const res = []; + this.#dfs(0, 'post', res); + return res; + } + } ``` === "TS" ```typescript title="array_binary_tree.ts" - [class]{ArrayBinaryTree}-[func]{} + /* 配列表現による二分木クラス */ + class ArrayBinaryTree { + #tree: (number | null)[]; + + /* コンストラクタ */ + constructor(arr: (number | null)[]) { + this.#tree = arr; + } + + /* リスト容量 */ + size(): number { + return this.#tree.length; + } + + /* インデックス i のノードの値を取得 */ + val(i: number): number | null { + // インデックスが範囲外なら、空きを表す null を返す + if (i < 0 || i >= this.size()) return null; + return this.#tree[i]; + } + + /* インデックス i のノードの左子ノードのインデックスを取得 */ + left(i: number): number { + return 2 * i + 1; + } + + /* インデックス i のノードの右子ノードのインデックスを取得 */ + right(i: number): number { + return 2 * i + 2; + } + + /* インデックス i のノードの親ノードのインデックスを取得 */ + parent(i: number): number { + return Math.floor((i - 1) / 2); // 切り捨て除算 + } + + /* レベル順走査 */ + levelOrder(): number[] { + let res = []; + // 配列を直接走査する + for (let i = 0; i < this.size(); i++) { + if (this.val(i) !== null) res.push(this.val(i)); + } + return res; + } + + /* 深さ優先探索 */ + #dfs(i: number, order: Order, res: (number | null)[]): void { + // 空きスロットなら返す + if (this.val(i) === null) return; + // 先行順走査 + if (order === 'pre') res.push(this.val(i)); + this.#dfs(this.left(i), order, res); + // 中順走査 + if (order === 'in') res.push(this.val(i)); + this.#dfs(this.right(i), order, res); + // 後順走査 + if (order === 'post') res.push(this.val(i)); + } + + /* 先行順走査 */ + preOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'pre', res); + return res; + } + + /* 中順走査 */ + inOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'in', res); + return res; + } + + /* 後順走査 */ + postOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'post', res); + return res; + } + } ``` === "Dart" ```dart title="array_binary_tree.dart" - [class]{ArrayBinaryTree}-[func]{} + /* 配列表現による二分木クラス */ + class ArrayBinaryTree { + late List _tree; + + /* コンストラクタ */ + ArrayBinaryTree(this._tree); + + /* リスト容量 */ + int size() { + return _tree.length; + } + + /* インデックス i のノードの値を取得 */ + int? val(int i) { + // インデックスが範囲外なら、空きを表す null を返す + if (i < 0 || i >= size()) { + return null; + } + return _tree[i]; + } + + /* インデックス i のノードの左子ノードのインデックスを取得 */ + int? left(int i) { + return 2 * i + 1; + } + + /* インデックス i のノードの右子ノードのインデックスを取得 */ + int? right(int i) { + return 2 * i + 2; + } + + /* インデックス i のノードの親ノードのインデックスを取得 */ + int? parent(int i) { + return (i - 1) ~/ 2; + } + + /* レベル順走査 */ + List levelOrder() { + List res = []; + for (int i = 0; i < size(); i++) { + if (val(i) != null) { + res.add(val(i)!); + } + } + return res; + } + + /* 深さ優先探索 */ + void dfs(int i, String order, List res) { + // 空きスロットなら返す + if (val(i) == null) { + return; + } + // 先行順走査 + if (order == 'pre') { + res.add(val(i)); + } + dfs(left(i)!, order, res); + // 中順走査 + if (order == 'in') { + res.add(val(i)); + } + dfs(right(i)!, order, res); + // 後順走査 + if (order == 'post') { + res.add(val(i)); + } + } + + /* 先行順走査 */ + List preOrder() { + List res = []; + dfs(0, 'pre', res); + return res; + } + + /* 中順走査 */ + List inOrder() { + List res = []; + dfs(0, 'in', res); + return res; + } + + /* 後順走査 */ + List postOrder() { + List res = []; + dfs(0, 'post', res); + return res; + } + } ``` === "Rust" ```rust title="array_binary_tree.rs" - [class]{ArrayBinaryTree}-[func]{} + /* 配列表現による二分木クラス */ + struct ArrayBinaryTree { + tree: Vec>, + } + + impl ArrayBinaryTree { + /* コンストラクタ */ + fn new(arr: Vec>) -> Self { + Self { tree: arr } + } + + /* リスト容量 */ + fn size(&self) -> i32 { + self.tree.len() as i32 + } + + /* インデックス i のノードの値を取得 */ + fn val(&self, i: i32) -> Option { + // インデックスが範囲外なら、空きを表す None を返す + if i < 0 || i >= self.size() { + None + } else { + self.tree[i as usize] + } + } + + /* インデックス i のノードの左子ノードのインデックスを取得 */ + fn left(&self, i: i32) -> i32 { + 2 * i + 1 + } + + /* インデックス i のノードの右子ノードのインデックスを取得 */ + fn right(&self, i: i32) -> i32 { + 2 * i + 2 + } + + /* インデックス i のノードの親ノードのインデックスを取得 */ + fn parent(&self, i: i32) -> i32 { + (i - 1) / 2 + } + + /* レベル順走査 */ + fn level_order(&self) -> Vec { + self.tree.iter().filter_map(|&x| x).collect() + } + + /* 深さ優先探索 */ + fn dfs(&self, i: i32, order: &'static str, res: &mut Vec) { + if self.val(i).is_none() { + return; + } + let val = self.val(i).unwrap(); + // 先行順走査 + if order == "pre" { + res.push(val); + } + self.dfs(self.left(i), order, res); + // 中順走査 + if order == "in" { + res.push(val); + } + self.dfs(self.right(i), order, res); + // 後順走査 + if order == "post" { + res.push(val); + } + } + + /* 先行順走査 */ + fn pre_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "pre", &mut res); + res + } + + /* 中順走査 */ + fn in_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "in", &mut res); + res + } + + /* 後順走査 */ + fn post_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "post", &mut res); + res + } + } ``` === "C" ```c title="array_binary_tree.c" - [class]{ArrayBinaryTree}-[func]{} + /* 配列表現による二分木の構造体 */ + typedef struct { + int *tree; + int size; + } ArrayBinaryTree; + + /* コンストラクタ */ + ArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) { + ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree)); + abt->tree = malloc(sizeof(int) * arrSize); + memcpy(abt->tree, arr, sizeof(int) * arrSize); + abt->size = arrSize; + return abt; + } + + /* デストラクタ */ + void delArrayBinaryTree(ArrayBinaryTree *abt) { + free(abt->tree); + free(abt); + } + + /* リスト容量 */ + int size(ArrayBinaryTree *abt) { + return abt->size; + } + + /* インデックス i のノードの値を取得 */ + int val(ArrayBinaryTree *abt, int i) { + // インデックスが範囲外なら、空きを表す INT_MAX を返す + if (i < 0 || i >= size(abt)) + return INT_MAX; + return abt->tree[i]; + } + + /* レベル順走査 */ + int *levelOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + // 配列を直接走査する + for (int i = 0; i < size(abt); i++) { + if (val(abt, i) != INT_MAX) + res[index++] = val(abt, i); + } + *returnSize = index; + return res; + } + + /* 深さ優先探索 */ + void dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) { + // 空きスロットなら返す + if (val(abt, i) == INT_MAX) + return; + // 先行順走査 + if (strcmp(order, "pre") == 0) + res[(*index)++] = val(abt, i); + dfs(abt, left(i), order, res, index); + // 中順走査 + if (strcmp(order, "in") == 0) + res[(*index)++] = val(abt, i); + dfs(abt, right(i), order, res, index); + // 後順走査 + if (strcmp(order, "post") == 0) + res[(*index)++] = val(abt, i); + } + + /* 先行順走査 */ + int *preOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + dfs(abt, 0, "pre", res, &index); + *returnSize = index; + return res; + } + + /* 中順走査 */ + int *inOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + dfs(abt, 0, "in", res, &index); + *returnSize = index; + return res; + } + + /* 後順走査 */ + int *postOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + dfs(abt, 0, "post", res, &index); + *returnSize = index; + return res; + } ``` === "Kotlin" ```kotlin title="array_binary_tree.kt" - [class]{ArrayBinaryTree}-[func]{} + /* 配列表現による二分木クラス */ + class ArrayBinaryTree(val tree: MutableList) { + /* リスト容量 */ + fun size(): Int { + return tree.size + } + + /* インデックス i のノードの値を取得 */ + fun _val(i: Int): Int? { + // インデックスが範囲外なら、空きを表す null を返す + if (i < 0 || i >= size()) return null + return tree[i] + } + + /* インデックス i のノードの左子ノードのインデックスを取得 */ + fun left(i: Int): Int { + return 2 * i + 1 + } + + /* インデックス i のノードの右子ノードのインデックスを取得 */ + fun right(i: Int): Int { + return 2 * i + 2 + } + + /* インデックス i のノードの親ノードのインデックスを取得 */ + fun parent(i: Int): Int { + return (i - 1) / 2 + } + + /* レベル順走査 */ + fun levelOrder(): MutableList { + val res = mutableListOf() + // 配列を直接走査する + for (i in 0..) { + // 空きスロットなら返す + if (_val(i) == null) + return + // 先行順走査 + if ("pre" == order) + res.add(_val(i)) + dfs(left(i), order, res) + // 中順走査 + if ("in" == order) + res.add(_val(i)) + dfs(right(i), order, res) + // 後順走査 + if ("post" == order) + res.add(_val(i)) + } + + /* 先行順走査 */ + fun preOrder(): MutableList { + val res = mutableListOf() + dfs(0, "pre", res) + return res + } + + /* 中順走査 */ + fun inOrder(): MutableList { + val res = mutableListOf() + dfs(0, "in", res) + return res + } + + /* 後順走査 */ + fun postOrder(): MutableList { + val res = mutableListOf() + dfs(0, "post", res) + return res + } + } ``` === "Ruby" ```ruby title="array_binary_tree.rb" - [class]{ArrayBinaryTree}-[func]{} + ### 配列表現の二分木クラス ### + class ArrayBinaryTree + ### コンストラクタ ### + def initialize(arr) + @tree = arr.to_a + end + + ### リスト容量 ### + def size + @tree.length + end + + ### インデックス i のノードの値を取得 ### + def val(i) + # インデックスが範囲外なら `nil` を返し、空きスロットを表す + return if i < 0 || i >= size + + @tree[i] + end + + ### インデックス i のノードの左子ノードのインデックスを取得 ### + def left(i) + 2 * i + 1 + end + + ### インデックス i のノードの右子ノードのインデックスを取得 ### + def right(i) + 2 * i + 2 + end + + ### インデックス i のノードの親ノードのインデックスを取得 ### + def parent(i) + (i - 1) / 2 + end + + ### レベル順走査 ### + def level_order + @res = [] + + # 配列を直接走査する + for i in 0...size + @res << val(i) unless val(i).nil? + end + + @res + end + + ### 深さ優先探索 ### + def dfs(i, order) + return if val(i).nil? + # 先行順走査 + @res << val(i) if order == :pre + dfs(left(i), order) + # 中順走査 + @res << val(i) if order == :in + dfs(right(i), order) + # 後順走査 + @res << val(i) if order == :post + end + + ### 前順走査 ### + def pre_order + @res = [] + dfs(0, :pre) + @res + end + + ### 中順走査 ### + def in_order + @res = [] + dfs(0, :in) + @res + end + + ### 後順走査 ### + def post_order + @res = [] + dfs(0, :post) + @res + end + end ``` -## 7.3.3   利点と制限 +??? pythontutor "コードの可視化" -二分木の配列表現には以下の利点があります: +
+ -- 配列は連続したメモリ空間に格納されるため、キャッシュフレンドリーで、より高速なアクセスと探索が可能です。 -- ポインタを格納する必要がないため、スペースを節約できます。 -- ノードへのランダムアクセスが可能です。 +## 7.3.3   利点と制約 -しかし、配列表現にはいくつかの制限もあります: +二分木の配列表現には主に次の利点があります。 -- 配列格納には連続したメモリ空間が必要なため、大量のデータを持つ木の格納には適していません。 -- ノードの追加や削除には配列の挿入や削除操作が必要で、効率が低くなります。 -- 二分木に多くの`None`値がある場合、配列に含まれるノードデータの割合が低くなり、空間利用率が低下します。 +- 配列は連続したメモリ空間に格納されるため、キャッシュ効率が高く、アクセスと走査が速い。 +- ポインタを格納する必要がなく、比較的省スペースである。 +- ノードへのランダムアクセスが可能である。 + +ただし、配列表現にはいくつかの制約もあります。 + +- 配列による格納には連続したメモリ空間が必要なため、データ量が大きすぎる木の格納には向かない。 +- ノードの追加と削除は配列の挿入・削除操作で実現する必要があり、効率は低い。 +- 二分木に大量の `None` が存在すると、配列に占める実ノードデータの比率が低くなり、空間利用率も低下する。 diff --git a/ja/docs/chapter_tree/avl_tree.md b/ja/docs/chapter_tree/avl_tree.md index 56ba2766f..dbb376758 100644 --- a/ja/docs/chapter_tree/avl_tree.md +++ b/ja/docs/chapter_tree/avl_tree.md @@ -2,53 +2,53 @@ comments: true --- -# 7.5   AVL木 * +# 7.5   AVL 木 * -「二分探索木」の節では、複数の挿入と削除の後、二分探索木が連結リストに退化する可能性があることを述べました。このような場合、すべての操作の時間計算量が$O(\log n)$から$O(n)$に悪化します。 +「二分探索木」章で述べたように、挿入と削除を何度も繰り返すと、二分探索木は連結リストへ退化する可能性があります。この場合、すべての操作の時間計算量は $O(\log n)$ から $O(n)$ へ劣化します。 -下図に示すように、2つのノード削除操作の後、この二分探索木は連結リストに退化します。 +以下の図に示すように、ノード削除を 2 回行うと、この二分探索木は連結リストへ退化します。 -![ノード削除後のAVL木の退化](avl_tree.assets/avltree_degradation_from_removing_node.png){ class="animation-figure" } +![AVL 木がノード削除後に退化する](avl_tree.assets/avltree_degradation_from_removing_node.png){ class="animation-figure" } -

図 7-24   ノード削除後のAVL木の退化

+

図 7-24   AVL 木がノード削除後に退化する

-例えば、下図に示す完全二分木では、2つのノードを挿入した後、木が左に大きく傾き、検索操作の時間計算量も悪化します。 +別の例として、以下の図に示す完全二分木に 2 つのノードを挿入すると、木は大きく左に傾き、探索操作の時間計算量もそれに伴って劣化します。 -![ノード挿入後のAVL木の退化](avl_tree.assets/avltree_degradation_from_inserting_node.png){ class="animation-figure" } +![AVL 木がノード挿入後に退化する](avl_tree.assets/avltree_degradation_from_inserting_node.png){ class="animation-figure" } -

図 7-25   ノード挿入後のAVL木の退化

+

図 7-25   AVL 木がノード挿入後に退化する

-1962年、G. M. Adelson-VelskyとE. M. Landisが論文「An algorithm for the organization of information」でAVL木を提案しました。この論文では、ノードの継続的な追加と削除の後もAVL木が退化しないことを保証する一連の操作について詳述し、さまざまな操作の時間計算量を$O(\log n)$レベルに維持しました。つまり、頻繁な追加、削除、検索、変更が必要なシナリオで、AVL木は常に効率的なデータ操作性能を維持でき、大きな応用価値があります。 +1962 年、G. M. Adelson-Velsky と E. M. Landis は論文“An algorithm for the organization of information”の中で AVL 木 を提案しました。論文では一連の操作が詳しく説明されており、ノードの追加と削除を続けても AVL 木が退化しないようにして、各種操作の時間計算量を $O(\log n)$ の水準に保ちます。言い換えると、追加・削除・探索・更新を頻繁に行う場面でも、AVL 木は常に高いデータ操作性能を維持でき、実用価値の高い構造です。 -## 7.5.1   AVL木の一般的な用語 +## 7.5.1   AVL 木の基本用語 -AVL木は二分探索木でありかつ平衡二分木でもあり、これら2つの種類の二分木のすべての性質を満たしているため、平衡二分探索木です。 +AVL 木は二分探索木であると同時に平衡二分木でもあり、これら 2 種類の二分木の性質をすべて満たします。したがって、平衡二分探索木(balanced binary search tree)の一種です。 ### 1.   ノードの高さ -AVL木に関連する操作ではノードの高さを取得する必要があるため、ノードクラスに`height`変数を追加する必要があります: +AVL 木の操作ではノードの高さを取得する必要があるため、ノードクラスに `height` 変数を追加します: === "Python" ```python title="" class TreeNode: - """AVL木ノード""" + """AVL 木ノードクラス""" def __init__(self, val: int): self.val: int = val # ノード値 self.height: int = 0 # ノードの高さ - self.left: TreeNode | None = None # 左の子への参照 - self.right: TreeNode | None = None # 右の子への参照 + self.left: TreeNode | None = None # 左の子ノード参照 + self.right: TreeNode | None = None # 右の子ノード参照 ``` === "C++" ```cpp title="" - /* AVL木ノード */ + /* AVL 木ノードクラス */ struct TreeNode { int val{}; // ノード値 int height = 0; // ノードの高さ - TreeNode *left{}; // 左の子 - TreeNode *right{}; // 右の子 + TreeNode *left{}; // 左の子ノード + TreeNode *right{}; // 右の子ノード TreeNode() = default; explicit TreeNode(int x) : val(x){} }; @@ -57,12 +57,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ === "Java" ```java title="" - /* AVL木ノード */ + /* AVL 木ノードクラス */ class TreeNode { public int val; // ノード値 public int height; // ノードの高さ - public TreeNode left; // 左の子 - public TreeNode right; // 右の子 + public TreeNode left; // 左の子ノード + public TreeNode right; // 右の子ノード public TreeNode(int x) { val = x; } } ``` @@ -70,36 +70,36 @@ AVL木に関連する操作ではノードの高さを取得する必要があ === "C#" ```csharp title="" - /* AVL木ノード */ + /* AVL 木ノードクラス */ class TreeNode(int? x) { public int? val = x; // ノード値 public int height; // ノードの高さ - public TreeNode? left; // 左の子への参照 - public TreeNode? right; // 右の子への参照 + public TreeNode? left; // 左の子ノード参照 + public TreeNode? right; // 右の子ノード参照 } ``` === "Go" ```go title="" - /* AVL木ノード */ + /* AVL 木ノード構造体 */ type TreeNode struct { Val int // ノード値 Height int // ノードの高さ - Left *TreeNode // 左の子への参照 - Right *TreeNode // 右の子への参照 + Left *TreeNode // 左の子ノード参照 + Right *TreeNode // 右の子ノード参照 } ``` === "Swift" ```swift title="" - /* AVL木ノード */ + /* AVL 木ノードクラス */ class TreeNode { var val: Int // ノード値 var height: Int // ノードの高さ - var left: TreeNode? // 左の子 - var right: TreeNode? // 右の子 + var left: TreeNode? // 左の子ノード + var right: TreeNode? // 右の子ノード init(x: Int) { val = x @@ -111,12 +111,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ === "JS" ```javascript title="" - /* AVL木ノード */ + /* AVL 木ノードクラス */ class TreeNode { val; // ノード値 - height; // ノードの高さ - left; // 左の子ポインタ - right; // 右の子ポインタ + height; //ノードの高さ + left; // 左の子ノードポインタ + right; // 右の子ノードポインタ constructor(val, left, right, height) { this.val = val === undefined ? 0 : val; this.height = height === undefined ? 0 : height; @@ -129,12 +129,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ === "TS" ```typescript title="" - /* AVL木ノード */ + /* AVL 木ノードクラス */ class TreeNode { val: number; // ノード値 height: number; // ノードの高さ - left: TreeNode | null; // 左の子ポインタ - right: TreeNode | null; // 右の子ポインタ + left: TreeNode | null; // 左の子ノードポインタ + right: TreeNode | null; // 右の子ノードポインタ constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) { this.val = val === undefined ? 0 : val; this.height = height === undefined ? 0 : height; @@ -147,12 +147,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ === "Dart" ```dart title="" - /* AVL木ノード */ + /* AVL 木ノードクラス */ class TreeNode { int val; // ノード値 int height; // ノードの高さ - TreeNode? left; // 左の子 - TreeNode? right; // 右の子 + TreeNode? left; // 左の子ノード + TreeNode? right; // 右の子ノード TreeNode(this.val, [this.height = 0, this.left, this.right]); } ``` @@ -163,12 +163,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ use std::rc::Rc; use std::cell::RefCell; - /* AVL木ノード */ + /* AVL 木ノード構造体 */ struct TreeNode { val: i32, // ノード値 height: i32, // ノードの高さ - left: Option>>, // 左の子 - right: Option>>, // 右の子 + left: Option>>, // 左の子ノード + right: Option>>, // 右の子ノード } impl TreeNode { @@ -187,8 +187,8 @@ AVL木に関連する操作ではノードの高さを取得する必要があ === "C" ```c title="" - /* AVL木ノード */ - TreeNode struct TreeNode { + /* AVL 木ノード構造体 */ + typedef struct TreeNode { int val; int height; struct TreeNode *left; @@ -211,35 +211,46 @@ AVL木に関連する操作ではノードの高さを取得する必要があ === "Kotlin" ```kotlin title="" - /* AVL木ノード */ + /* AVL 木ノードクラス */ class TreeNode(val _val: Int) { // ノード値 val height: Int = 0 // ノードの高さ - val left: TreeNode? = null // 左の子 - val right: TreeNode? = null // 右の子 + val left: TreeNode? = null // 左の子ノード + val right: TreeNode? = null // 右の子ノード } ``` === "Ruby" ```ruby title="" + ### AVL 木ノードクラス ### + class TreeNode + attr_accessor :val # ノード値 + attr_accessor :height # ノードの高さ + attr_accessor :left # 左の子ノード参照 + attr_accessor :right # 右の子ノード参照 + def initialize(val) + @val = val + @height = 0 + end + end ``` -「ノードの高さ」とは、そのノードから最も遠い葉ノードまでの距離、つまり通過する「辺」の数を指します。重要なのは、葉ノードの高さは$0$で、nullノードの高さは$-1$であることです。ノードの高さを取得し、更新するための2つのユーティリティ関数を作成します: +「ノードの高さ」とは、そのノードから最も遠い葉ノードまでの距離、すなわち通過する「辺」の本数を指します。特に、葉ノードの高さは $0$、空ノードの高さは $-1$ です。ここでは、ノードの高さを取得・更新するための 2 つの補助関数を用意します: === "Python" ```python title="avl_tree.py" def height(self, node: TreeNode | None) -> int: """ノードの高さを取得""" - # 空ノードの高さは-1、葉ノードの高さは0 + # 空ノードの高さは -1、葉ノードの高さは 0 if node is not None: return node.height return -1 def update_height(self, node: TreeNode | None): - """ノードの高さを更新""" - # ノードの高さ = 最も高い部分木の高さ + 1 + """ノードの高さを更新する""" + # ノードの高さは最も高い部分木の高さ + 1 に等しい node.height = max([self.height(node.left), self.height(node.right)]) + 1 ``` @@ -248,13 +259,13 @@ AVL木に関連する操作ではノードの高さを取得する必要があ ```cpp title="avl_tree.cpp" /* ノードの高さを取得 */ int height(TreeNode *node) { - // 空ノードの高さは-1、葉ノードの高さは0 + // 空ノードの高さは -1、葉ノードの高さは 0 return node == nullptr ? -1 : node->height; } - /* ノードの高さを更新 */ + /* ノードの高さを更新する */ void updateHeight(TreeNode *node) { - // ノードの高さ = 最も高い部分木の高さ + 1 + // ノードの高さは最も高い部分木の高さ + 1 に等しい node->height = max(height(node->left), height(node->right)) + 1; } ``` @@ -268,9 +279,9 @@ AVL木に関連する操作ではノードの高さを取得する必要があ return node == null ? -1 : node.height; } - /* ノードの高さを更新 */ + /* ノードの高さを更新する */ void updateHeight(TreeNode node) { - // ノードの高さは最も高い部分木の高さ + 1 + // ノードの高さは最も高い部分木の高さ + 1 に等しい node.height = Math.max(height(node.left), height(node.right)) + 1; } ``` @@ -278,108 +289,217 @@ AVL木に関連する操作ではノードの高さを取得する必要があ === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{Height} + /* ノードの高さを取得 */ + int Height(TreeNode? node) { + // 空ノードの高さは -1、葉ノードの高さは 0 + return node == null ? -1 : node.height; + } - [class]{AVLTree}-[func]{UpdateHeight} + /* ノードの高さを更新する */ + void UpdateHeight(TreeNode node) { + // ノードの高さは最も高い部分木の高さ + 1 に等しい + node.height = Math.Max(Height(node.left), Height(node.right)) + 1; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{height} + /* ノードの高さを取得 */ + func (t *aVLTree) height(node *TreeNode) int { + // 空ノードの高さは -1、葉ノードの高さは 0 + if node != nil { + return node.Height + } + return -1 + } - [class]{aVLTree}-[func]{updateHeight} + /* ノードの高さを更新する */ + func (t *aVLTree) updateHeight(node *TreeNode) { + lh := t.height(node.Left) + rh := t.height(node.Right) + // ノードの高さは最も高い部分木の高さ + 1 に等しい + if lh > rh { + node.Height = lh + 1 + } else { + node.Height = rh + 1 + } + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{height} + /* ノードの高さを取得 */ + func height(node: TreeNode?) -> Int { + // 空ノードの高さは -1、葉ノードの高さは 0 + node?.height ?? -1 + } - [class]{AVLTree}-[func]{updateHeight} + /* ノードの高さを更新する */ + func updateHeight(node: TreeNode?) { + // ノードの高さは最も高い部分木の高さ + 1 に等しい + node?.height = max(height(node: node?.left), height(node: node?.right)) + 1 + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{height} + /* ノードの高さを取得 */ + height(node) { + // 空ノードの高さは -1、葉ノードの高さは 0 + return node === null ? -1 : node.height; + } - [class]{AVLTree}-[func]{updateHeight} + /* ノードの高さを更新する */ + #updateHeight(node) { + // ノードの高さは最も高い部分木の高さ + 1 に等しい + node.height = + Math.max(this.height(node.left), this.height(node.right)) + 1; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{height} + /* ノードの高さを取得 */ + height(node: TreeNode): number { + // 空ノードの高さは -1、葉ノードの高さは 0 + return node === null ? -1 : node.height; + } - [class]{AVLTree}-[func]{updateHeight} + /* ノードの高さを更新する */ + updateHeight(node: TreeNode): void { + // ノードの高さは最も高い部分木の高さ + 1 に等しい + node.height = + Math.max(this.height(node.left), this.height(node.right)) + 1; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{height} + /* ノードの高さを取得 */ + int height(TreeNode? node) { + // 空ノードの高さは -1、葉ノードの高さは 0 + return node == null ? -1 : node.height; + } - [class]{AVLTree}-[func]{updateHeight} + /* ノードの高さを更新する */ + void updateHeight(TreeNode? node) { + // ノードの高さは最も高い部分木の高さ + 1 に等しい + node!.height = max(height(node.left), height(node.right)) + 1; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{height} + /* ノードの高さを取得 */ + fn height(node: OptionTreeNodeRc) -> i32 { + // 空ノードの高さは -1、葉ノードの高さは 0 + match node { + Some(node) => node.borrow().height, + None => -1, + } + } - [class]{AVLTree}-[func]{update_height} + /* ノードの高さを更新する */ + fn update_height(node: OptionTreeNodeRc) { + if let Some(node) = node { + let left = node.borrow().left.clone(); + let right = node.borrow().right.clone(); + // ノードの高さは最も高い部分木の高さ + 1 に等しい + node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1; + } + } ``` === "C" ```c title="avl_tree.c" - [class]{}-[func]{height} + /* ノードの高さを取得 */ + int height(TreeNode *node) { + // 空ノードの高さは -1、葉ノードの高さは 0 + if (node != NULL) { + return node->height; + } + return -1; + } - [class]{}-[func]{updateHeight} + /* ノードの高さを更新する */ + void updateHeight(TreeNode *node) { + int lh = height(node->left); + int rh = height(node->right); + // ノードの高さは最も高い部分木の高さ + 1 に等しい + if (lh > rh) { + node->height = lh + 1; + } else { + node->height = rh + 1; + } + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{height} + /* ノードの高さを取得 */ + fun height(node: TreeNode?): Int { + // 空ノードの高さは -1、葉ノードの高さは 0 + return node?.height ?: -1 + } - [class]{AVLTree}-[func]{updateHeight} + /* ノードの高さを更新する */ + fun updateHeight(node: TreeNode?) { + // ノードの高さは最も高い部分木の高さ + 1 に等しい + node?.height = max(height(node?.left), height(node?.right)) + 1 + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{height} + ### ノードの高さを取得 ### + def height(node) + # 空ノードの高さは -1、葉ノードの高さは 0 + return node.height unless node.nil? - [class]{AVLTree}-[func]{update_height} + -1 + end + + ### ノードの高さを更新 ### + def update_height(node) + # ノードの高さは最も高い部分木の高さ + 1 に等しい + node.height = [height(node.left), height(node.right)].max + 1 + end ``` -### 2.   ノードの平衡因子 +### 2.   ノードの平衡係数 -ノードの平衡因子は、そのノードの左部分木の高さから右部分木の高さを引いた値として定義され、nullノードの平衡因子は$0$として定義されます。後で使いやすくするため、ノードの平衡因子を取得する機能も関数にカプセル化します: +ノードの平衡係数(balance factor)は、左部分木の高さから右部分木の高さを引いた値と定義し、空ノードの平衡係数は $0$ とします。同様に、ノードの平衡係数を取得する機能も関数にカプセル化して、後続で使いやすくします: === "Python" ```python title="avl_tree.py" def balance_factor(self, node: TreeNode | None) -> int: - """バランス因子を取得""" - # 空ノードのバランス因子は0 + """平衡係数を取得""" + # 空ノードの平衡係数は 0 if node is None: return 0 - # ノードのバランス因子 = 左部分木の高さ - 右部分木の高さ + # ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ return self.height(node.left) - self.height(node.right) ``` === "C++" ```cpp title="avl_tree.cpp" - /* 平衡因子を取得 */ + /* 平衡係数を取得 */ int balanceFactor(TreeNode *node) { - // 空ノードの平衡因子は0 + // 空ノードの平衡係数は 0 if (node == nullptr) return 0; - // ノードの平衡因子 = 左部分木の高さ - 右部分木の高さ + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ return height(node->left) - height(node->right); } ``` @@ -387,12 +507,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ === "Java" ```java title="avl_tree.java" - /* 平衡因子を取得 */ + /* 平衡係数を取得 */ int balanceFactor(TreeNode node) { - // 空ノードの平衡因子は 0 + // 空ノードの平衡係数は 0 if (node == null) return 0; - // ノードの平衡因子 = 左部分木の高さ - 右部分木の高さ + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ return height(node.left) - height(node.right); } ``` @@ -400,76 +520,145 @@ AVL木に関連する操作ではノードの高さを取得する必要があ === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{BalanceFactor} + /* 平衡係数を取得 */ + int BalanceFactor(TreeNode? node) { + // 空ノードの平衡係数は 0 + if (node == null) return 0; + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ + return Height(node.left) - Height(node.right); + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{balanceFactor} + /* 平衡係数を取得 */ + func (t *aVLTree) balanceFactor(node *TreeNode) int { + // 空ノードの平衡係数は 0 + if node == nil { + return 0 + } + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ + return t.height(node.Left) - t.height(node.Right) + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{balanceFactor} + /* 平衡係数を取得 */ + func balanceFactor(node: TreeNode?) -> Int { + // 空ノードの平衡係数は 0 + guard let node = node else { return 0 } + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ + return height(node: node.left) - height(node: node.right) + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{balanceFactor} + /* 平衡係数を取得 */ + balanceFactor(node) { + // 空ノードの平衡係数は 0 + if (node === null) return 0; + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ + return this.height(node.left) - this.height(node.right); + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{balanceFactor} + /* 平衡係数を取得 */ + balanceFactor(node: TreeNode): number { + // 空ノードの平衡係数は 0 + if (node === null) return 0; + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ + return this.height(node.left) - this.height(node.right); + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{balanceFactor} + /* 平衡係数を取得 */ + int balanceFactor(TreeNode? node) { + // 空ノードの平衡係数は 0 + if (node == null) return 0; + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ + return height(node.left) - height(node.right); + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{balance_factor} + /* 平衡係数を取得 */ + fn balance_factor(node: OptionTreeNodeRc) -> i32 { + match node { + // 空ノードの平衡係数は 0 + None => 0, + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ + Some(node) => { + Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone()) + } + } + } ``` === "C" ```c title="avl_tree.c" - [class]{}-[func]{balanceFactor} + /* 平衡係数を取得 */ + int balanceFactor(TreeNode *node) { + // 空ノードの平衡係数は 0 + if (node == NULL) { + return 0; + } + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ + return height(node->left) - height(node->right); + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{balanceFactor} + /* 平衡係数を取得 */ + fun balanceFactor(node: TreeNode?): Int { + // 空ノードの平衡係数は 0 + if (node == null) return 0 + // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ + return height(node.left) - height(node.right) + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{balance_factor} + ### 平衡係数を取得 ### + def balance_factor(node) + # 空ノードの平衡係数は 0 + return 0 if node.nil? + + # ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ + height(node.left) - height(node.right) + end ``` !!! tip - 平衡因子を$f$とすると、AVL木の任意のノードの平衡因子は$-1 \le f \le 1$を満たします。 + 平衡係数を $f$ とすると、AVL 木の任意のノードの平衡係数は常に $-1 \le f \le 1$ を満たします。 -## 7.5.2   AVL木の回転 +## 7.5.2   AVL 木の回転 -AVL木の特徴的な機能は「回転」操作で、これは二分木の中順探索シーケンスに影響を与えることなく、不平衡なノードのバランスを回復できます。つまり、**回転操作は「二分探索木」の性質を維持しながら、木を「平衡二分木」に戻すことができます**。 +AVL 木の特徴は「回転」操作にあり、二分木の中順走査列を変えずに、不平衡ノードを再び平衡に戻せます。言い換えると、**回転操作は「二分探索木」の性質を保ちながら、木を再び「平衡二分木」に戻すことができます**。 -絶対平衡因子が$> 1$のノードを「不平衡ノード」と呼びます。不平衡のタイプに応じて、4種類の回転があります:右回転、左回転、右左回転、左右回転です。以下、これらの回転操作について詳しく説明します。 +平衡係数の絶対値が $> 1$ のノードを「不平衡ノード」と呼びます。ノードの不平衡の形に応じて、回転操作は 4 種類に分かれます。右回転、左回転、右回転してから左回転、左回転してから右回転です。以下でこれらを順に説明します。 ### 1.   右回転 -下図に示すように、二分木で下から上への最初の不平衡ノードは「ノード3」です。この不平衡ノードを根とする部分木に焦点を当て、これを`node`とし、その左の子を`child`として、「右回転」を実行します。右回転後、部分木は再びバランスが取れ、同時に二分探索木の性質も維持されます。 +以下の図では、ノードの下に平衡係数を示しています。下から上へ見ると、二分木で最初に不平衡になるのは「ノード 3」です。この不平衡ノードを根とする部分木に注目し、そのノードを `node`、左の子ノードを `child` として、「右回転」を行います。右回転後、部分木は平衡を回復し、なおかつ二分探索木の性質も保たれます。 === "<1>" ![右回転の手順](avl_tree.assets/avltree_right_rotate_step1.png){ class="animation-figure" } @@ -485,45 +674,45 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中

図 7-26   右回転の手順

-下図に示すように、`child`ノードに右の子(`grand_child`と表記)がある場合、右回転で手順を追加する必要があります:`grand_child`を`node`の左の子に設定します。 +以下の図に示すように、ノード `child` に右の子ノード(`grand_child` と記す)がある場合、右回転には 1 ステップ追加する必要があります。すなわち、`grand_child` を `node` の左の子ノードにします。 -![grand_childがある右回転](avl_tree.assets/avltree_right_rotate_with_grandchild.png){ class="animation-figure" } +![grand_child を持つ右回転](avl_tree.assets/avltree_right_rotate_with_grandchild.png){ class="animation-figure" } -

図 7-27   grand_childがある右回転

+

図 7-27   grand_child を持つ右回転

-「右回転」は比喩的な用語で、実際にはノードポインタを変更することで実現されます。以下のコードで示されます: +「右に回転する」というのはあくまでイメージしやすい表現であり、実際にはノードポインタを変更して実現します。コードは次のとおりです: === "Python" ```python title="avl_tree.py" def right_rotate(self, node: TreeNode | None) -> TreeNode | None: - """右回転操作""" + """右回転""" child = node.left grand_child = child.right - # childを中心にnodeを右に回転 + # child を支点として node を右回転させる child.right = node node.left = grand_child - # ノードの高さを更新 + # ノードの高さを更新する self.update_height(node) self.update_height(child) - # 回転後の部分木のルートを返す + # 回転後の部分木の根ノードを返す return child ``` === "C++" ```cpp title="avl_tree.cpp" - /* 右回転操作 */ + /* 右回転 */ TreeNode *rightRotate(TreeNode *node) { TreeNode *child = node->left; TreeNode *grandChild = child->right; - // childを中心にnodeを右に回転 + // child を支点として node を右回転させる child->right = node; node->left = grandChild; - // ノードの高さを更新 + // ノードの高さを更新する updateHeight(node); updateHeight(child); - // 回転後の部分木のルートを返す + // 回転後の部分木の根ノードを返す return child; } ``` @@ -531,17 +720,17 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 === "Java" ```java title="avl_tree.java" - /* 右回転操作 */ + /* 右回転 */ TreeNode rightRotate(TreeNode node) { TreeNode child = node.left; TreeNode grandChild = child.right; - // child を軸として node を右に回転 + // child を支点として node を右回転させる child.right = node; node.left = grandChild; - // ノードの高さを更新 + // ノードの高さを更新する updateHeight(node); updateHeight(child); - // 回転後の部分木の根を返す + // 回転後の部分木の根ノードを返す return child; } ``` @@ -549,110 +738,236 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{RightRotate} + /* 右回転 */ + TreeNode? RightRotate(TreeNode? node) { + TreeNode? child = node?.left; + TreeNode? grandChild = child?.right; + // child を支点として node を右回転させる + child.right = node; + node.left = grandChild; + // ノードの高さを更新する + UpdateHeight(node); + UpdateHeight(child); + // 回転後の部分木の根ノードを返す + return child; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{rightRotate} + /* 右回転 */ + func (t *aVLTree) rightRotate(node *TreeNode) *TreeNode { + child := node.Left + grandChild := child.Right + // child を支点として node を右回転させる + child.Right = node + node.Left = grandChild + // ノードの高さを更新する + t.updateHeight(node) + t.updateHeight(child) + // 回転後の部分木の根ノードを返す + return child + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{rightRotate} + /* 右回転 */ + func rightRotate(node: TreeNode?) -> TreeNode? { + let child = node?.left + let grandChild = child?.right + // child を支点として node を右回転させる + child?.right = node + node?.left = grandChild + // ノードの高さを更新する + updateHeight(node: node) + updateHeight(node: child) + // 回転後の部分木の根ノードを返す + return child + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{rightRotate} + /* 右回転 */ + #rightRotate(node) { + const child = node.left; + const grandChild = child.right; + // child を支点として node を右回転させる + child.right = node; + node.left = grandChild; + // ノードの高さを更新する + this.#updateHeight(node); + this.#updateHeight(child); + // 回転後の部分木の根ノードを返す + return child; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{rightRotate} + /* 右回転 */ + rightRotate(node: TreeNode): TreeNode { + const child = node.left; + const grandChild = child.right; + // child を支点として node を右回転させる + child.right = node; + node.left = grandChild; + // ノードの高さを更新する + this.updateHeight(node); + this.updateHeight(child); + // 回転後の部分木の根ノードを返す + return child; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{rightRotate} + /* 右回転 */ + TreeNode? rightRotate(TreeNode? node) { + TreeNode? child = node!.left; + TreeNode? grandChild = child!.right; + // child を支点として node を右回転させる + child.right = node; + node.left = grandChild; + // ノードの高さを更新する + updateHeight(node); + updateHeight(child); + // 回転後の部分木の根ノードを返す + return child; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{right_rotate} + /* 右回転 */ + fn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + match node { + Some(node) => { + let child = node.borrow().left.clone().unwrap(); + let grand_child = child.borrow().right.clone(); + // child を支点として node を右回転させる + child.borrow_mut().right = Some(node.clone()); + node.borrow_mut().left = grand_child; + // ノードの高さを更新する + Self::update_height(Some(node)); + Self::update_height(Some(child.clone())); + // 回転後の部分木の根ノードを返す + Some(child) + } + None => None, + } + } ``` === "C" ```c title="avl_tree.c" - [class]{}-[func]{rightRotate} + /* 右回転 */ + TreeNode *rightRotate(TreeNode *node) { + TreeNode *child, *grandChild; + child = node->left; + grandChild = child->right; + // child を支点として node を右回転させる + child->right = node; + node->left = grandChild; + // ノードの高さを更新する + updateHeight(node); + updateHeight(child); + // 回転後の部分木の根ノードを返す + return child; + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{rightRotate} + /* 右回転 */ + fun rightRotate(node: TreeNode?): TreeNode { + val child = node!!.left + val grandChild = child!!.right + // child を支点として node を右回転させる + child.right = node + node.left = grandChild + // ノードの高さを更新する + updateHeight(node) + updateHeight(child) + // 回転後の部分木の根ノードを返す + return child + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{right_rotate} + ### 右回転操作 ### + def right_rotate(node) + child = node.left + grand_child = child.right + # child を支点として node を右回転させる + child.right = node + node.left = grand_child + # ノードの高さを更新する + update_height(node) + update_height(child) + # 回転後の部分木の根ノードを返す + child + end ``` ### 2.   左回転 -対応して、上記の不平衡二分木の「鏡像」を考慮すると、下図に示す「左回転」操作を実行する必要があります。 +対応する鏡像として、上記の不平衡二分木を左右反転して考えると、以下の図に示す「左回転」が必要になります。 -![左回転操作](avl_tree.assets/avltree_left_rotate.png){ class="animation-figure" } +![左回転](avl_tree.assets/avltree_left_rotate.png){ class="animation-figure" } -

図 7-28   左回転操作

+

図 7-28   左回転

-同様に、下図に示すように、`child`ノードに左の子(`grand_child`と表記)がある場合、左回転で手順を追加する必要があります:`grand_child`を`node`の右の子に設定します。 +同様に、以下の図に示すように、ノード `child` に左の子ノード(`grand_child` と記す)がある場合、左回転にも 1 ステップ追加する必要があります。すなわち、`grand_child` を `node` の右の子ノードにします。 -![grand_childがある左回転](avl_tree.assets/avltree_left_rotate_with_grandchild.png){ class="animation-figure" } +![grand_child を持つ左回転](avl_tree.assets/avltree_left_rotate_with_grandchild.png){ class="animation-figure" } -

図 7-29   grand_childがある左回転

+

図 7-29   grand_child を持つ左回転

-**右回転と左回転の操作は論理的に対称であり、2つの対称的な不平衡タイプを解決します**ことが観察できます。対称性に基づいて、右回転の実装コードですべての`left`を`right`に、すべての`right`を`left`に置き換えることで、左回転の実装コードを得ることができます: +分かるように、**右回転と左回転は論理的に鏡像対称であり、それぞれが解決する 2 種類の不平衡も対称です**。この対称性に基づけば、右回転の実装コードにあるすべての `left` を `right` に、すべての `right` を `left` に置き換えるだけで、左回転の実装コードが得られます: === "Python" ```python title="avl_tree.py" def left_rotate(self, node: TreeNode | None) -> TreeNode | None: - """左回転操作""" + """左回転""" child = node.right grand_child = child.left - # childを中心にnodeを左に回転 + # child を支点として node を左回転させる child.left = node node.right = grand_child - # ノードの高さを更新 + # ノードの高さを更新する self.update_height(node) self.update_height(child) - # 回転後の部分木のルートを返す + # 回転後の部分木の根ノードを返す return child ``` === "C++" ```cpp title="avl_tree.cpp" - /* 左回転操作 */ + /* 左回転 */ TreeNode *leftRotate(TreeNode *node) { TreeNode *child = node->right; TreeNode *grandChild = child->left; - // childを中心にnodeを左に回転 + // child を支点として node を左回転させる child->left = node; node->right = grandChild; - // ノードの高さを更新 + // ノードの高さを更新する updateHeight(node); updateHeight(child); - // 回転後の部分木のルートを返す + // 回転後の部分木の根ノードを返す return child; } ``` @@ -660,17 +975,17 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 === "Java" ```java title="avl_tree.java" - /* 左回転操作 */ + /* 左回転 */ TreeNode leftRotate(TreeNode node) { TreeNode child = node.right; TreeNode grandChild = child.left; - // child を軸として node を左に回転 + // child を支点として node を左回転させる child.left = node; node.right = grandChild; - // ノードの高さを更新 + // ノードの高さを更新する updateHeight(node); updateHeight(child); - // 回転後の部分木の根を返す + // 回転後の部分木の根ノードを返す return child; } ``` @@ -678,112 +993,238 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{LeftRotate} + /* 左回転 */ + TreeNode? LeftRotate(TreeNode? node) { + TreeNode? child = node?.right; + TreeNode? grandChild = child?.left; + // child を支点として node を左回転させる + child.left = node; + node.right = grandChild; + // ノードの高さを更新する + UpdateHeight(node); + UpdateHeight(child); + // 回転後の部分木の根ノードを返す + return child; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{leftRotate} + /* 左回転 */ + func (t *aVLTree) leftRotate(node *TreeNode) *TreeNode { + child := node.Right + grandChild := child.Left + // child を支点として node を左回転させる + child.Left = node + node.Right = grandChild + // ノードの高さを更新する + t.updateHeight(node) + t.updateHeight(child) + // 回転後の部分木の根ノードを返す + return child + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{leftRotate} + /* 左回転 */ + func leftRotate(node: TreeNode?) -> TreeNode? { + let child = node?.right + let grandChild = child?.left + // child を支点として node を左回転させる + child?.left = node + node?.right = grandChild + // ノードの高さを更新する + updateHeight(node: node) + updateHeight(node: child) + // 回転後の部分木の根ノードを返す + return child + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{leftRotate} + /* 左回転 */ + #leftRotate(node) { + const child = node.right; + const grandChild = child.left; + // child を支点として node を左回転させる + child.left = node; + node.right = grandChild; + // ノードの高さを更新する + this.#updateHeight(node); + this.#updateHeight(child); + // 回転後の部分木の根ノードを返す + return child; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{leftRotate} + /* 左回転 */ + leftRotate(node: TreeNode): TreeNode { + const child = node.right; + const grandChild = child.left; + // child を支点として node を左回転させる + child.left = node; + node.right = grandChild; + // ノードの高さを更新する + this.updateHeight(node); + this.updateHeight(child); + // 回転後の部分木の根ノードを返す + return child; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{leftRotate} + /* 左回転 */ + TreeNode? leftRotate(TreeNode? node) { + TreeNode? child = node!.right; + TreeNode? grandChild = child!.left; + // child を支点として node を左回転させる + child.left = node; + node.right = grandChild; + // ノードの高さを更新する + updateHeight(node); + updateHeight(child); + // 回転後の部分木の根ノードを返す + return child; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{left_rotate} + /* 左回転 */ + fn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + match node { + Some(node) => { + let child = node.borrow().right.clone().unwrap(); + let grand_child = child.borrow().left.clone(); + // child を支点として node を左回転させる + child.borrow_mut().left = Some(node.clone()); + node.borrow_mut().right = grand_child; + // ノードの高さを更新する + Self::update_height(Some(node)); + Self::update_height(Some(child.clone())); + // 回転後の部分木の根ノードを返す + Some(child) + } + None => None, + } + } ``` === "C" ```c title="avl_tree.c" - [class]{}-[func]{leftRotate} + /* 左回転 */ + TreeNode *leftRotate(TreeNode *node) { + TreeNode *child, *grandChild; + child = node->right; + grandChild = child->left; + // child を支点として node を左回転させる + child->left = node; + node->right = grandChild; + // ノードの高さを更新する + updateHeight(node); + updateHeight(child); + // 回転後の部分木の根ノードを返す + return child; + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{leftRotate} + /* 左回転 */ + fun leftRotate(node: TreeNode?): TreeNode { + val child = node!!.right + val grandChild = child!!.left + // child を支点として node を左回転させる + child.left = node + node.right = grandChild + // ノードの高さを更新する + updateHeight(node) + updateHeight(child) + // 回転後の部分木の根ノードを返す + return child + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{left_rotate} + ### 左回転操作 ### + def left_rotate(node) + child = node.right + grand_child = child.left + # child を支点として node を左回転させる + child.left = node + node.right = grand_child + # ノードの高さを更新する + update_height(node) + update_height(child) + # 回転後の部分木の根ノードを返す + child + end ``` -### 3.   左右回転 +### 3.   左回転してから右回転 -下図に示す不平衡ノード3の場合、左回転または右回転のいずれかだけでは部分木のバランスを回復できません。この場合、まず`child`に対して「左回転」を実行し、次に`node`に対して「右回転」を実行する必要があります。 +以下の図の不平衡ノード 3 では、左回転だけでも右回転だけでも部分木を平衡に戻せません。この場合は、まず `child` に「左回転」を行い、次に `node` に「右回転」を行います。 -![左右回転](avl_tree.assets/avltree_left_right_rotate.png){ class="animation-figure" } +![左回転してから右回転](avl_tree.assets/avltree_left_right_rotate.png){ class="animation-figure" } -

図 7-30   左右回転

+

図 7-30   左回転してから右回転

-### 4.   右左回転 +### 4.   右回転してから左回転 -下図に示すように、上記の不平衡二分木の鏡像ケースでは、まず`child`に対して「右回転」を実行し、次に`node`に対して「左回転」を実行する必要があります。 +以下の図に示すように、上記の不平衡二分木の鏡像のケースでは、まず `child` に「右回転」を行い、次に `node` に「左回転」を行います。 -![右左回転](avl_tree.assets/avltree_right_left_rotate.png){ class="animation-figure" } +![右回転してから左回転](avl_tree.assets/avltree_right_left_rotate.png){ class="animation-figure" } -

図 7-31   右左回転

+

図 7-31   右回転してから左回転

### 5.   回転の選択 -下図に示す4種類の不平衡は、それぞれ上記で説明したケースに対応し、右回転、左右回転、右左回転、左回転が必要です。 +以下の図に示す 4 種類の不平衡は、上の各ケースにそれぞれ対応しており、必要な操作は順に右回転、左回転してから右回転、右回転してから左回転、左回転です。 -![AVL木の4つの回転ケース](avl_tree.assets/avltree_rotation_cases.png){ class="animation-figure" } +![AVL 木の 4 つの回転ケース](avl_tree.assets/avltree_rotation_cases.png){ class="animation-figure" } -

図 7-32   AVL木の4つの回転ケース

+

図 7-32   AVL 木の 4 つの回転ケース

-下表に示すように、不平衡ノードの平衡因子とその高い側の子の平衡因子の符号を判断することで、不平衡ノードが上記のどのケースに属するかを決定します。 +以下の表に示すように、不平衡ノードの平衡係数と、高い側の子ノードの平衡係数の符号を判定することで、その不平衡ノードが上図のどのケースに属するかを判断できます。 -

表 7-3   4つの回転ケースの選択条件

+

表 7-3   4 種類の回転ケースの選択条件

-| 不平衡ノードの平衡因子 | 子ノードの平衡因子 | 使用する回転方法 | -| --------------------- | ----------------- | --------------------------- | -| $> 1$(左に傾いた木) | $\geq 0$ | 右回転 | -| $> 1$(左に傾いた木) | $<0$ | 左回転してから右回転 | -| $< -1$(右に傾いた木) | $\leq 0$ | 左回転 | -| $< -1$(右に傾いた木) | $>0$ | 右回転してから左回転 | +| 不平衡ノードの平衡係数 | 子ノードの平衡係数 | 採用すべき回転方法 | +| ------------------ | ---------------- | ---------------- | +| $> 1$ (左に偏った木) | $\geq 0$ | 右回転 | +| $> 1$ (左に偏った木) | $<0$ | 左回転してから右回転 | +| $< -1$ (右に偏った木) | $\leq 0$ | 左回転 | +| $< -1$ (右に偏った木) | $>0$ | 右回転してから左回転 |
-便宜上、回転操作を関数にカプセル化します。**この関数により、さまざまな種類の不平衡に対して回転を実行し、不平衡ノードのバランスを回復できます**。コードは以下の通りです: +使いやすくするために、回転操作を 1 つの関数にカプセル化します。**この関数があれば、さまざまな不平衡ケースに対して回転を行い、不平衡ノードを再び平衡に戻せます**。コードは次のとおりです: === "Python" ```python title="avl_tree.py" def rotate(self, node: TreeNode | None) -> TreeNode | None: - """回転操作を実行して部分木のバランスを復元""" - # nodeのバランス因子を取得 + """回転操作を行い、この部分木の平衡を回復する""" + # ノード node の平衡係数を取得 balance_factor = self.balance_factor(node) - # 左偏り木 + # 左に偏った木 if balance_factor > 1: if self.balance_factor(node.left) >= 0: # 右回転 @@ -792,7 +1233,7 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 # 左回転してから右回転 node.left = self.left_rotate(node.left) return self.right_rotate(node) - # 右偏り木 + # 右に偏った木 elif balance_factor < -1: if self.balance_factor(node.right) <= 0: # 左回転 @@ -801,40 +1242,40 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 # 右回転してから左回転 node.right = self.right_rotate(node.right) return self.left_rotate(node) - # バランスの取れた木、回転不要、戻る + # 平衡木なので回転不要、そのまま返す return node ``` === "C++" ```cpp title="avl_tree.cpp" - /* 回転操作を実行して部分木の平衡を回復 */ + /* 回転操作を行い、この部分木の平衡を回復する */ TreeNode *rotate(TreeNode *node) { - // nodeの平衡因子を取得 + // ノード node の平衡係数を取得 int _balanceFactor = balanceFactor(node); - // 左に傾いた木 + // 左に偏った木 if (_balanceFactor > 1) { if (balanceFactor(node->left) >= 0) { // 右回転 return rightRotate(node); } else { - // 先に左回転、その後右回転 + // 左回転してから右回転 node->left = leftRotate(node->left); return rightRotate(node); } } - // 右に傾いた木 + // 右に偏った木 if (_balanceFactor < -1) { if (balanceFactor(node->right) <= 0) { // 左回転 return leftRotate(node); } else { - // 先に右回転、その後左回転 + // 右回転してから左回転 node->right = rightRotate(node->right); return leftRotate(node); } } - // 平衡な木、回転不要、そのまま戻る + // 平衡木なので回転不要、そのまま返す return node; } ``` @@ -842,33 +1283,33 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 === "Java" ```java title="avl_tree.java" - /* 回転操作を実行して部分木の平衡を回復 */ + /* 回転操作を行い、この部分木の平衡を回復する */ TreeNode rotate(TreeNode node) { - // node の平衡因子を取得 + // ノード node の平衡係数を取得 int balanceFactor = balanceFactor(node); - // 左傾斜の木 + // 左に偏った木 if (balanceFactor > 1) { if (balanceFactor(node.left) >= 0) { // 右回転 return rightRotate(node); } else { - // 先に左回転、その後右回転 + // 左回転してから右回転 node.left = leftRotate(node.left); return rightRotate(node); } } - // 右傾斜の木 + // 右に偏った木 if (balanceFactor < -1) { if (balanceFactor(node.right) <= 0) { // 左回転 return leftRotate(node); } else { - // 先に右回転、その後左回転 + // 右回転してから左回転 node.right = rightRotate(node.right); return leftRotate(node); } } - // 平衡木、回転は不要、戻る + // 平衡木なので回転不要、そのまま返す return node; } ``` @@ -876,68 +1317,353 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{Rotate} + /* 回転操作を行い、この部分木の平衡を回復する */ + TreeNode? Rotate(TreeNode? node) { + // ノード node の平衡係数を取得 + int balanceFactorInt = BalanceFactor(node); + // 左に偏った木 + if (balanceFactorInt > 1) { + if (BalanceFactor(node?.left) >= 0) { + // 右回転 + return RightRotate(node); + } else { + // 左回転してから右回転 + node!.left = LeftRotate(node!.left); + return RightRotate(node); + } + } + // 右に偏った木 + if (balanceFactorInt < -1) { + if (BalanceFactor(node?.right) <= 0) { + // 左回転 + return LeftRotate(node); + } else { + // 右回転してから左回転 + node!.right = RightRotate(node!.right); + return LeftRotate(node); + } + } + // 平衡木なので回転不要、そのまま返す + return node; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{rotate} + /* 回転操作を行い、この部分木の平衡を回復する */ + func (t *aVLTree) rotate(node *TreeNode) *TreeNode { + // ノード `node` の平衡係数を取得する + // Go では短い変数名が推奨されるため、ここで `bf` は `t.balanceFactor` を表す + bf := t.balanceFactor(node) + // 左に偏った木 + if bf > 1 { + if t.balanceFactor(node.Left) >= 0 { + // 右回転 + return t.rightRotate(node) + } else { + // 左回転してから右回転 + node.Left = t.leftRotate(node.Left) + return t.rightRotate(node) + } + } + // 右に偏った木 + if bf < -1 { + if t.balanceFactor(node.Right) <= 0 { + // 左回転 + return t.leftRotate(node) + } else { + // 右回転してから左回転 + node.Right = t.rightRotate(node.Right) + return t.leftRotate(node) + } + } + // 平衡木なので回転不要、そのまま返す + return node + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{rotate} + /* 回転操作を行い、この部分木の平衡を回復する */ + func rotate(node: TreeNode?) -> TreeNode? { + // ノード node の平衡係数を取得 + let balanceFactor = balanceFactor(node: node) + // 左に偏った木 + if balanceFactor > 1 { + if self.balanceFactor(node: node?.left) >= 0 { + // 右回転 + return rightRotate(node: node) + } else { + // 左回転してから右回転 + node?.left = leftRotate(node: node?.left) + return rightRotate(node: node) + } + } + // 右に偏った木 + if balanceFactor < -1 { + if self.balanceFactor(node: node?.right) <= 0 { + // 左回転 + return leftRotate(node: node) + } else { + // 右回転してから左回転 + node?.right = rightRotate(node: node?.right) + return leftRotate(node: node) + } + } + // 平衡木なので回転不要、そのまま返す + return node + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{rotate} + /* 回転操作を行い、この部分木の平衡を回復する */ + #rotate(node) { + // ノード node の平衡係数を取得 + const balanceFactor = this.balanceFactor(node); + // 左に偏った木 + if (balanceFactor > 1) { + if (this.balanceFactor(node.left) >= 0) { + // 右回転 + return this.#rightRotate(node); + } else { + // 左回転してから右回転 + node.left = this.#leftRotate(node.left); + return this.#rightRotate(node); + } + } + // 右に偏った木 + if (balanceFactor < -1) { + if (this.balanceFactor(node.right) <= 0) { + // 左回転 + return this.#leftRotate(node); + } else { + // 右回転してから左回転 + node.right = this.#rightRotate(node.right); + return this.#leftRotate(node); + } + } + // 平衡木なので回転不要、そのまま返す + return node; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{rotate} + /* 回転操作を行い、この部分木の平衡を回復する */ + rotate(node: TreeNode): TreeNode { + // ノード node の平衡係数を取得 + const balanceFactor = this.balanceFactor(node); + // 左に偏った木 + if (balanceFactor > 1) { + if (this.balanceFactor(node.left) >= 0) { + // 右回転 + return this.rightRotate(node); + } else { + // 左回転してから右回転 + node.left = this.leftRotate(node.left); + return this.rightRotate(node); + } + } + // 右に偏った木 + if (balanceFactor < -1) { + if (this.balanceFactor(node.right) <= 0) { + // 左回転 + return this.leftRotate(node); + } else { + // 右回転してから左回転 + node.right = this.rightRotate(node.right); + return this.leftRotate(node); + } + } + // 平衡木なので回転不要、そのまま返す + return node; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{rotate} + /* 回転操作を行い、この部分木の平衡を回復する */ + TreeNode? rotate(TreeNode? node) { + // ノード node の平衡係数を取得 + int factor = balanceFactor(node); + // 左に偏った木 + if (factor > 1) { + if (balanceFactor(node!.left) >= 0) { + // 右回転 + return rightRotate(node); + } else { + // 左回転してから右回転 + node.left = leftRotate(node.left); + return rightRotate(node); + } + } + // 右に偏った木 + if (factor < -1) { + if (balanceFactor(node!.right) <= 0) { + // 左回転 + return leftRotate(node); + } else { + // 右回転してから左回転 + node.right = rightRotate(node.right); + return leftRotate(node); + } + } + // 平衡木なので回転不要、そのまま返す + return node; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{rotate} + /* 回転操作を行い、この部分木の平衡を回復する */ + fn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + // ノード node の平衡係数を取得 + let balance_factor = Self::balance_factor(node.clone()); + // 左に偏った木 + if balance_factor > 1 { + let node = node.unwrap(); + if Self::balance_factor(node.borrow().left.clone()) >= 0 { + // 右回転 + Self::right_rotate(Some(node)) + } else { + // 左回転してから右回転 + let left = node.borrow().left.clone(); + node.borrow_mut().left = Self::left_rotate(left); + Self::right_rotate(Some(node)) + } + } + // 右に偏った木 + else if balance_factor < -1 { + let node = node.unwrap(); + if Self::balance_factor(node.borrow().right.clone()) <= 0 { + // 左回転 + Self::left_rotate(Some(node)) + } else { + // 右回転してから左回転 + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::right_rotate(right); + Self::left_rotate(Some(node)) + } + } else { + // 平衡木なので回転不要、そのまま返す + node + } + } ``` === "C" ```c title="avl_tree.c" - [class]{}-[func]{rotate} + /* 回転操作を行い、この部分木の平衡を回復する */ + TreeNode *rotate(TreeNode *node) { + // ノード node の平衡係数を取得 + int bf = balanceFactor(node); + // 左に偏った木 + if (bf > 1) { + if (balanceFactor(node->left) >= 0) { + // 右回転 + return rightRotate(node); + } else { + // 左回転してから右回転 + node->left = leftRotate(node->left); + return rightRotate(node); + } + } + // 右に偏った木 + if (bf < -1) { + if (balanceFactor(node->right) <= 0) { + // 左回転 + return leftRotate(node); + } else { + // 右回転してから左回転 + node->right = rightRotate(node->right); + return leftRotate(node); + } + } + // 平衡木なので回転不要、そのまま返す + return node; + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{rotate} + /* 回転操作を行い、この部分木の平衡を回復する */ + fun rotate(node: TreeNode): TreeNode { + // ノード node の平衡係数を取得 + val balanceFactor = balanceFactor(node) + // 左に偏った木 + if (balanceFactor > 1) { + if (balanceFactor(node.left) >= 0) { + // 右回転 + return rightRotate(node) + } else { + // 左回転してから右回転 + node.left = leftRotate(node.left) + return rightRotate(node) + } + } + // 右に偏った木 + if (balanceFactor < -1) { + if (balanceFactor(node.right) <= 0) { + // 左回転 + return leftRotate(node) + } else { + // 右回転してから左回転 + node.right = rightRotate(node.right) + return leftRotate(node) + } + } + // 平衡木なので回転不要、そのまま返す + return node + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{rotate} + ### 回転操作を行い、この部分木の平衡を回復する ### + def rotate(node) + # ノード node の平衡係数を取得 + balance_factor = balance_factor(node) + # 左部分木をたどる + if balance_factor > 1 + if balance_factor(node.left) >= 0 + # 右回転 + return right_rotate(node) + else + # 左回転してから右回転 + node.left = left_rotate(node.left) + return right_rotate(node) + end + # 右に偏った木 + elsif balance_factor < -1 + if balance_factor(node.right) <= 0 + # 左回転 + return left_rotate(node) + else + # 右回転してから左回転 + node.right = right_rotate(node.right) + return left_rotate(node) + end + end + # 平衡木なので回転不要、そのまま返す + node + end ``` -## 7.5.3   AVL木の一般的な操作 +## 7.5.3   AVL 木の基本操作 ### 1.   ノードの挿入 -AVL木のノード挿入操作は二分探索木のそれと似ています。唯一の違いは、AVL木でノードを挿入した後、そのノードから根ノードまでのパス上に一連の不平衡ノードが現れる可能性があることです。したがって、**このノードから始めて上向きに回転操作を実行し、すべての不平衡ノードのバランスを回復する必要があります**。コードは以下の通りです: +AVL 木のノード挿入は、基本的には二分探索木と同じです。唯一の違いは、AVL 木ではノード挿入後に、そのノードから根ノードまでの経路上に複数の不平衡ノードが現れる可能性があることです。したがって、**このノードから開始して、下から上へ回転操作を行い、すべての不平衡ノードを平衡に戻す必要があります**。コードは次のとおりです: === "Python" @@ -947,20 +1673,20 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 self._root = self.insert_helper(self._root, val) def insert_helper(self, node: TreeNode | None, val: int) -> TreeNode: - """再帰的にノードを挿入(ヘルパーメソッド)""" + """ノードを再帰的に挿入する(補助メソッド)""" if node is None: return TreeNode(val) - # 1. 挿入位置を見つけてノードを挿入 + # 1. 挿入位置を探索してノードを挿入 if val < node.val: node.left = self.insert_helper(node.left, val) elif val > node.val: node.right = self.insert_helper(node.right, val) else: - # 重複ノードは挿入しない、戻る + # 重複ノードは挿入せず、そのまま返す return node - # ノードの高さを更新 + # ノードの高さを更新する self.update_height(node) - # 2. 回転操作を実行して部分木のバランスを復元 + # 2. 回転操作を行い、部分木の平衡を回復する return self.rotate(node) ``` @@ -972,21 +1698,21 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 root = insertHelper(root, val); } - /* ノードを再帰的に挿入(ヘルパーメソッド) */ + /* ノードを再帰的に挿入する(補助メソッド) */ TreeNode *insertHelper(TreeNode *node, int val) { if (node == nullptr) return new TreeNode(val); - /* 1. 挿入位置を見つけてノードを挿入 */ + /* 1. 挿入位置を探索してノードを挿入 */ if (val < node->val) node->left = insertHelper(node->left, val); else if (val > node->val) node->right = insertHelper(node->right, val); else - return node; // 重複ノードは挿入しない、そのまま戻る - updateHeight(node); // ノードの高さを更新 - /* 2. 回転操作を実行して部分木の平衡を回復 */ + return node; // 重複ノードは挿入せず、そのまま返す + updateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ node = rotate(node); - // 部分木のルートノードを返す + // 部分木の根ノードを返す return node; } ``` @@ -999,19 +1725,19 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 root = insertHelper(root, val); } - /* 再帰的にノードを挿入(補助メソッド) */ + /* ノードを再帰的に挿入する(補助メソッド) */ TreeNode insertHelper(TreeNode node, int val) { if (node == null) return new TreeNode(val); - /* 1. 挿入位置を見つけてノードを挿入 */ + /* 1. 挿入位置を探索してノードを挿入 */ if (val < node.val) node.left = insertHelper(node.left, val); else if (val > node.val) node.right = insertHelper(node.right, val); else - return node; // 重複ノードは挿入しない、戻る - updateHeight(node); // ノードの高さを更新 - /* 2. 回転操作を実行して部分木の平衡を回復 */ + return node; // 重複ノードは挿入せず、そのまま返す + updateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ node = rotate(node); // 部分木の根ノードを返す return node; @@ -1021,86 +1747,304 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{Insert} + /* ノードを挿入 */ + void Insert(int val) { + root = InsertHelper(root, val); + } - [class]{AVLTree}-[func]{InsertHelper} + /* ノードを再帰的に挿入する(補助メソッド) */ + TreeNode? InsertHelper(TreeNode? node, int val) { + if (node == null) return new TreeNode(val); + /* 1. 挿入位置を探索してノードを挿入 */ + if (val < node.val) + node.left = InsertHelper(node.left, val); + else if (val > node.val) + node.right = InsertHelper(node.right, val); + else + return node; // 重複ノードは挿入せず、そのまま返す + UpdateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = Rotate(node); + // 部分木の根ノードを返す + return node; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{insert} + /* ノードを挿入 */ + func (t *aVLTree) insert(val int) { + t.root = t.insertHelper(t.root, val) + } - [class]{aVLTree}-[func]{insertHelper} + /* ノードを再帰的に挿入する(補助関数) */ + func (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode { + if node == nil { + return NewTreeNode(val) + } + /* 1. 挿入位置を探索してノードを挿入 */ + if val < node.Val.(int) { + node.Left = t.insertHelper(node.Left, val) + } else if val > node.Val.(int) { + node.Right = t.insertHelper(node.Right, val) + } else { + // 重複ノードは挿入せず、そのまま返す + return node + } + // ノードの高さを更新する + t.updateHeight(node) + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = t.rotate(node) + // 部分木の根ノードを返す + return node + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{insert} + /* ノードを挿入 */ + func insert(val: Int) { + root = insertHelper(node: root, val: val) + } - [class]{AVLTree}-[func]{insertHelper} + /* ノードを再帰的に挿入する(補助メソッド) */ + func insertHelper(node: TreeNode?, val: Int) -> TreeNode? { + var node = node + if node == nil { + return TreeNode(x: val) + } + /* 1. 挿入位置を探索してノードを挿入 */ + if val < node!.val { + node?.left = insertHelper(node: node?.left, val: val) + } else if val > node!.val { + node?.right = insertHelper(node: node?.right, val: val) + } else { + return node // 重複ノードは挿入せず、そのまま返す + } + updateHeight(node: node) // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = rotate(node: node) + // 部分木の根ノードを返す + return node + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{insert} + /* ノードを挿入 */ + insert(val) { + this.root = this.#insertHelper(this.root, val); + } - [class]{AVLTree}-[func]{insertHelper} + /* ノードを再帰的に挿入する(補助メソッド) */ + #insertHelper(node, val) { + if (node === null) return new TreeNode(val); + /* 1. 挿入位置を探索してノードを挿入 */ + if (val < node.val) node.left = this.#insertHelper(node.left, val); + else if (val > node.val) + node.right = this.#insertHelper(node.right, val); + else return node; // 重複ノードは挿入せず、そのまま返す + this.#updateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = this.#rotate(node); + // 部分木の根ノードを返す + return node; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{insert} + /* ノードを挿入 */ + insert(val: number): void { + this.root = this.insertHelper(this.root, val); + } - [class]{AVLTree}-[func]{insertHelper} + /* ノードを再帰的に挿入する(補助メソッド) */ + insertHelper(node: TreeNode, val: number): TreeNode { + if (node === null) return new TreeNode(val); + /* 1. 挿入位置を探索してノードを挿入 */ + if (val < node.val) { + node.left = this.insertHelper(node.left, val); + } else if (val > node.val) { + node.right = this.insertHelper(node.right, val); + } else { + return node; // 重複ノードは挿入せず、そのまま返す + } + this.updateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = this.rotate(node); + // 部分木の根ノードを返す + return node; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{insert} + /* ノードを挿入 */ + void insert(int val) { + root = insertHelper(root, val); + } - [class]{AVLTree}-[func]{insertHelper} + /* ノードを再帰的に挿入する(補助メソッド) */ + TreeNode? insertHelper(TreeNode? node, int val) { + if (node == null) return TreeNode(val); + /* 1. 挿入位置を探索してノードを挿入 */ + if (val < node.val) + node.left = insertHelper(node.left, val); + else if (val > node.val) + node.right = insertHelper(node.right, val); + else + return node; // 重複ノードは挿入せず、そのまま返す + updateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = rotate(node); + // 部分木の根ノードを返す + return node; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{insert} + /* ノードを挿入 */ + fn insert(&mut self, val: i32) { + self.root = Self::insert_helper(self.root.clone(), val); + } - [class]{AVLTree}-[func]{insert_helper} + /* ノードを再帰的に挿入する(補助メソッド) */ + fn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc { + match node { + Some(mut node) => { + /* 1. 挿入位置を探索してノードを挿入 */ + match { + let node_val = node.borrow().val; + node_val + } + .cmp(&val) + { + Ordering::Greater => { + let left = node.borrow().left.clone(); + node.borrow_mut().left = Self::insert_helper(left, val); + } + Ordering::Less => { + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::insert_helper(right, val); + } + Ordering::Equal => { + return Some(node); // 重複ノードは挿入せず、そのまま返す + } + } + Self::update_height(Some(node.clone())); // ノードの高さを更新する + + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = Self::rotate(Some(node)).unwrap(); + // 部分木の根ノードを返す + Some(node) + } + None => Some(TreeNode::new(val)), + } + } ``` === "C" ```c title="avl_tree.c" - [class]{AVLTree}-[func]{insert} + /* ノードを挿入 */ + void insert(AVLTree *tree, int val) { + tree->root = insertHelper(tree->root, val); + } - [class]{}-[func]{insertHelper} + /* ノードを再帰的に挿入する(補助関数) */ + TreeNode *insertHelper(TreeNode *node, int val) { + if (node == NULL) { + return newTreeNode(val); + } + /* 1. 挿入位置を探索してノードを挿入 */ + if (val < node->val) { + node->left = insertHelper(node->left, val); + } else if (val > node->val) { + node->right = insertHelper(node->right, val); + } else { + // 重複ノードは挿入せず、そのまま返す + return node; + } + // ノードの高さを更新する + updateHeight(node); + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = rotate(node); + // 部分木の根ノードを返す + return node; + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{insert} + /* ノードを挿入 */ + fun insert(_val: Int) { + root = insertHelper(root, _val) + } - [class]{AVLTree}-[func]{insertHelper} + /* ノードを再帰的に挿入する(補助メソッド) */ + fun insertHelper(n: TreeNode?, _val: Int): TreeNode { + if (n == null) + return TreeNode(_val) + var node = n + /* 1. 挿入位置を探索してノードを挿入 */ + if (_val < node._val) + node.left = insertHelper(node.left, _val) + else if (_val > node._val) + node.right = insertHelper(node.right, _val) + else + return node // 重複ノードは挿入せず、そのまま返す + updateHeight(node) // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = rotate(node) + // 部分木の根ノードを返す + return node + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{insert} + ### ノードを挿入 ### + def insert(val) + @root = insert_helper(@root, val) + end - [class]{AVLTree}-[func]{insert_helper} + ### ノードを挿入 ### + def insert(val) + @root = insert_helper(@root, val) + end + + # ## ノードを再帰的に挿入(補助メソッド)### + def insert_helper(node, val) + return TreeNode.new(val) if node.nil? + # 1. 挿入位置を探索してノードを挿入 + if val < node.val + node.left = insert_helper(node.left, val) + elsif val > node.val + node.right = insert_helper(node.right, val) + else + # 重複ノードは挿入せず、そのまま返す + return node + end + # ノードの高さを更新する + update_height(node) + # 2. 回転操作を行い、部分木の平衡を回復する + rotate(node) + end ``` ### 2.   ノードの削除 -同様に、二分探索木でのノード削除方法に基づいて、下から上へ回転操作を実行してすべての不平衡ノードのバランスを回復する必要があります。コードは以下の通りです: +同様に、二分探索木のノード削除メソッドを土台として、下から上へ回転操作を行い、すべての不平衡ノードを平衡に戻す必要があります。コードは次のとおりです: === "Python" @@ -1110,10 +2054,10 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 self._root = self.remove_helper(self._root, val) def remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None: - """再帰的にノードを削除(ヘルパーメソッド)""" + """ノードを再帰的に削除する(補助メソッド)""" if node is None: return None - # 1. ノードを見つけて削除 + # 1. ノードを探索して削除 if val < node.val: node.left = self.remove_helper(node.left, val) elif val > node.val: @@ -1121,22 +2065,22 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 else: if node.left is None or node.right is None: child = node.left or node.right - # 子ノード数 = 0、ノードを削除して戻る + # 子ノード数 = 0 の場合、node をそのまま削除して返す if child is None: return None - # 子ノード数 = 1、ノードを削除 + # 子ノード数 = 1 の場合、node をそのまま削除する else: node = child else: - # 子ノード数 = 2、中順走査の次のノードを削除し、それで現在のノードを置き換え + # 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える temp = node.right while temp.left is not None: temp = temp.left node.right = self.remove_helper(node.right, temp.val) node.val = temp.val - # ノードの高さを更新 + # ノードの高さを更新する self.update_height(node) - # 2. 回転操作を実行して部分木のバランスを復元 + # 2. 回転操作を行い、部分木の平衡を回復する return self.rotate(node) ``` @@ -1148,11 +2092,11 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 root = removeHelper(root, val); } - /* ノードを再帰的に削除(ヘルパーメソッド) */ + /* ノードを再帰的に削除する(補助メソッド) */ TreeNode *removeHelper(TreeNode *node, int val) { if (node == nullptr) return nullptr; - /* 1. ノードを見つけて削除 */ + /* 1. ノードを探索して削除 */ if (val < node->val) node->left = removeHelper(node->left, val); else if (val > node->val) @@ -1160,18 +2104,18 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 else { if (node->left == nullptr || node->right == nullptr) { TreeNode *child = node->left != nullptr ? node->left : node->right; - // 子ノード数 = 0、ノードを削除して戻る + // 子ノード数 = 0 の場合、node をそのまま削除して返す if (child == nullptr) { delete node; return nullptr; } - // 子ノード数 = 1、ノードを削除 + // 子ノード数 = 1 の場合、node をそのまま削除する else { delete node; node = child; } } else { - // 子ノード数 = 2、中順走査の次のノードを削除し、現在のノードと置き換える + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える TreeNode *temp = node->right; while (temp->left != nullptr) { temp = temp->left; @@ -1181,10 +2125,10 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 node->val = tempVal; } } - updateHeight(node); // ノードの高さを更新 - /* 2. 回転操作を実行して部分木の平衡を回復 */ + updateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ node = rotate(node); - // 部分木のルートノードを返す + // 部分木の根ノードを返す return node; } ``` @@ -1197,11 +2141,11 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 root = removeHelper(root, val); } - /* 再帰的にノードを削除(補助メソッド) */ + /* ノードを再帰的に削除する(補助メソッド) */ TreeNode removeHelper(TreeNode node, int val) { if (node == null) return null; - /* 1. ノードを見つけて削除 */ + /* 1. ノードを探索して削除 */ if (val < node.val) node.left = removeHelper(node.left, val); else if (val > node.val) @@ -1209,14 +2153,14 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 else { if (node.left == null || node.right == null) { TreeNode child = node.left != null ? node.left : node.right; - // 子ノード数 = 0、ノードを削除して戻る + // 子ノード数 = 0 の場合、node をそのまま削除して返す if (child == null) return null; - // 子ノード数 = 1、ノードを削除 + // 子ノード数 = 1 の場合、node をそのまま削除する else node = child; } else { - // 子ノード数 = 2、中順走査の次のノードを削除し、現在のノードをそれで置き換える + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える TreeNode temp = node.right; while (temp.left != null) { temp = temp.left; @@ -1225,8 +2169,8 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 node.val = temp.val; } } - updateHeight(node); // ノードの高さを更新 - /* 2. 回転操作を実行して部分木の平衡を回復 */ + updateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ node = rotate(node); // 部分木の根ノードを返す return node; @@ -1236,89 +2180,480 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{Remove} + /* ノードを削除 */ + void Remove(int val) { + root = RemoveHelper(root, val); + } - [class]{AVLTree}-[func]{RemoveHelper} + /* ノードを再帰的に削除する(補助メソッド) */ + TreeNode? RemoveHelper(TreeNode? node, int val) { + if (node == null) return null; + /* 1. ノードを探索して削除 */ + if (val < node.val) + node.left = RemoveHelper(node.left, val); + else if (val > node.val) + node.right = RemoveHelper(node.right, val); + else { + if (node.left == null || node.right == null) { + TreeNode? child = node.left ?? node.right; + // 子ノード数 = 0 の場合、node をそのまま削除して返す + if (child == null) + return null; + // 子ノード数 = 1 の場合、node をそのまま削除する + else + node = child; + } else { + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える + TreeNode? temp = node.right; + while (temp.left != null) { + temp = temp.left; + } + node.right = RemoveHelper(node.right, temp.val!.Value); + node.val = temp.val; + } + } + UpdateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = Rotate(node); + // 部分木の根ノードを返す + return node; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{remove} + /* ノードを削除 */ + func (t *aVLTree) remove(val int) { + t.root = t.removeHelper(t.root, val) + } - [class]{aVLTree}-[func]{removeHelper} + /* ノードを再帰的に削除する(補助関数) */ + func (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode { + if node == nil { + return nil + } + /* 1. ノードを探索して削除 */ + if val < node.Val.(int) { + node.Left = t.removeHelper(node.Left, val) + } else if val > node.Val.(int) { + node.Right = t.removeHelper(node.Right, val) + } else { + if node.Left == nil || node.Right == nil { + child := node.Left + if node.Right != nil { + child = node.Right + } + if child == nil { + // 子ノード数 = 0 の場合、node をそのまま削除して返す + return nil + } else { + // 子ノード数 = 1 の場合、node をそのまま削除する + node = child + } + } else { + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える + temp := node.Right + for temp.Left != nil { + temp = temp.Left + } + node.Right = t.removeHelper(node.Right, temp.Val.(int)) + node.Val = temp.Val + } + } + // ノードの高さを更新する + t.updateHeight(node) + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = t.rotate(node) + // 部分木の根ノードを返す + return node + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{remove} + /* ノードを削除 */ + func remove(val: Int) { + root = removeHelper(node: root, val: val) + } - [class]{AVLTree}-[func]{removeHelper} + /* ノードを再帰的に削除する(補助メソッド) */ + func removeHelper(node: TreeNode?, val: Int) -> TreeNode? { + var node = node + if node == nil { + return nil + } + /* 1. ノードを探索して削除 */ + if val < node!.val { + node?.left = removeHelper(node: node?.left, val: val) + } else if val > node!.val { + node?.right = removeHelper(node: node?.right, val: val) + } else { + if node?.left == nil || node?.right == nil { + let child = node?.left ?? node?.right + // 子ノード数 = 0 の場合、node をそのまま削除して返す + if child == nil { + return nil + } + // 子ノード数 = 1 の場合、node をそのまま削除する + else { + node = child + } + } else { + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える + var temp = node?.right + while temp?.left != nil { + temp = temp?.left + } + node?.right = removeHelper(node: node?.right, val: temp!.val) + node?.val = temp!.val + } + } + updateHeight(node: node) // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = rotate(node: node) + // 部分木の根ノードを返す + return node + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{remove} + /* ノードを削除 */ + remove(val) { + this.root = this.#removeHelper(this.root, val); + } - [class]{AVLTree}-[func]{removeHelper} + /* ノードを再帰的に削除する(補助メソッド) */ + #removeHelper(node, val) { + if (node === null) return null; + /* 1. ノードを探索して削除 */ + if (val < node.val) node.left = this.#removeHelper(node.left, val); + else if (val > node.val) + node.right = this.#removeHelper(node.right, val); + else { + if (node.left === null || node.right === null) { + const child = node.left !== null ? node.left : node.right; + // 子ノード数 = 0 の場合、node をそのまま削除して返す + if (child === null) return null; + // 子ノード数 = 1 の場合、node をそのまま削除する + else node = child; + } else { + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える + let temp = node.right; + while (temp.left !== null) { + temp = temp.left; + } + node.right = this.#removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + this.#updateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = this.#rotate(node); + // 部分木の根ノードを返す + return node; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{remove} + /* ノードを削除 */ + remove(val: number): void { + this.root = this.removeHelper(this.root, val); + } - [class]{AVLTree}-[func]{removeHelper} + /* ノードを再帰的に削除する(補助メソッド) */ + removeHelper(node: TreeNode, val: number): TreeNode { + if (node === null) return null; + /* 1. ノードを探索して削除 */ + if (val < node.val) { + node.left = this.removeHelper(node.left, val); + } else if (val > node.val) { + node.right = this.removeHelper(node.right, val); + } else { + if (node.left === null || node.right === null) { + const child = node.left !== null ? node.left : node.right; + // 子ノード数 = 0 の場合、node をそのまま削除して返す + if (child === null) { + return null; + } else { + // 子ノード数 = 1 の場合、node をそのまま削除する + node = child; + } + } else { + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える + let temp = node.right; + while (temp.left !== null) { + temp = temp.left; + } + node.right = this.removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + this.updateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = this.rotate(node); + // 部分木の根ノードを返す + return node; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{remove} + /* ノードを削除 */ + void remove(int val) { + root = removeHelper(root, val); + } - [class]{AVLTree}-[func]{removeHelper} + /* ノードを再帰的に削除する(補助メソッド) */ + TreeNode? removeHelper(TreeNode? node, int val) { + if (node == null) return null; + /* 1. ノードを探索して削除 */ + if (val < node.val) + node.left = removeHelper(node.left, val); + else if (val > node.val) + node.right = removeHelper(node.right, val); + else { + if (node.left == null || node.right == null) { + TreeNode? child = node.left ?? node.right; + // 子ノード数 = 0 の場合、node をそのまま削除して返す + if (child == null) + return null; + // 子ノード数 = 1 の場合、node をそのまま削除する + else + node = child; + } else { + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える + TreeNode? temp = node.right; + while (temp!.left != null) { + temp = temp.left; + } + node.right = removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + updateHeight(node); // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = rotate(node); + // 部分木の根ノードを返す + return node; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{remove} + /* ノードを削除 */ + fn remove(&self, val: i32) { + Self::remove_helper(self.root.clone(), val); + } - [class]{AVLTree}-[func]{remove_helper} + /* ノードを再帰的に削除する(補助メソッド) */ + fn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc { + match node { + Some(mut node) => { + /* 1. ノードを探索して削除 */ + if val < node.borrow().val { + let left = node.borrow().left.clone(); + node.borrow_mut().left = Self::remove_helper(left, val); + } else if val > node.borrow().val { + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::remove_helper(right, val); + } else if node.borrow().left.is_none() || node.borrow().right.is_none() { + let child = if node.borrow().left.is_some() { + node.borrow().left.clone() + } else { + node.borrow().right.clone() + }; + match child { + // 子ノード数 = 0 の場合、node をそのまま削除して返す + None => { + return None; + } + // 子ノード数 = 1 の場合、node をそのまま削除する + Some(child) => node = child, + } + } else { + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える + let mut temp = node.borrow().right.clone().unwrap(); + loop { + let temp_left = temp.borrow().left.clone(); + if temp_left.is_none() { + break; + } + temp = temp_left.unwrap(); + } + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val); + node.borrow_mut().val = temp.borrow().val; + } + Self::update_height(Some(node.clone())); // ノードの高さを更新する + + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = Self::rotate(Some(node)).unwrap(); + // 部分木の根ノードを返す + Some(node) + } + None => None, + } + } ``` === "C" ```c title="avl_tree.c" - [class]{AVLTree}-[func]{removeItem} + /* ノードを削除 */ + // stdio.h を導入しているため、ここでは remove 識別子を使えない + void removeItem(AVLTree *tree, int val) { + TreeNode *root = removeHelper(tree->root, val); + } - [class]{}-[func]{removeHelper} + /* ノードを再帰的に削除する(補助関数) */ + TreeNode *removeHelper(TreeNode *node, int val) { + TreeNode *child, *grandChild; + if (node == NULL) { + return NULL; + } + /* 1. ノードを探索して削除 */ + if (val < node->val) { + node->left = removeHelper(node->left, val); + } else if (val > node->val) { + node->right = removeHelper(node->right, val); + } else { + if (node->left == NULL || node->right == NULL) { + child = node->left; + if (node->right != NULL) { + child = node->right; + } + // 子ノード数 = 0 の場合、node をそのまま削除して返す + if (child == NULL) { + return NULL; + } else { + // 子ノード数 = 1 の場合、node をそのまま削除する + node = child; + } + } else { + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える + TreeNode *temp = node->right; + while (temp->left != NULL) { + temp = temp->left; + } + int tempVal = temp->val; + node->right = removeHelper(node->right, temp->val); + node->val = tempVal; + } + } + // ノードの高さを更新する + updateHeight(node); + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = rotate(node); + // 部分木の根ノードを返す + return node; + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{remove} + /* ノードを削除 */ + fun remove(_val: Int) { + root = removeHelper(root, _val) + } - [class]{AVLTree}-[func]{removeHelper} + /* ノードを再帰的に削除する(補助メソッド) */ + fun removeHelper(n: TreeNode?, _val: Int): TreeNode? { + var node = n ?: return null + /* 1. ノードを探索して削除 */ + if (_val < node._val) + node.left = removeHelper(node.left, _val) + else if (_val > node._val) + node.right = removeHelper(node.right, _val) + else { + if (node.left == null || node.right == null) { + val child = if (node.left != null) + node.left + else + node.right + // 子ノード数 = 0 の場合、node をそのまま削除して返す + if (child == null) + return null + // 子ノード数 = 1 の場合、node をそのまま削除する + else + node = child + } else { + // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える + var temp = node.right + while (temp!!.left != null) { + temp = temp.left + } + node.right = removeHelper(node.right, temp._val) + node._val = temp._val + } + } + updateHeight(node) // ノードの高さを更新する + /* 2. 回転操作を行い、部分木の平衡を回復する */ + node = rotate(node) + // 部分木の根ノードを返す + return node + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{remove} + ### ノードを削除 ### + def remove(val) + @root = remove_helper(@root, val) + end - [class]{AVLTree}-[func]{remove_helper} + ### ノードを削除 ### + def remove(val) + @root = remove_helper(@root, val) + end + + # ## ノードを再帰的に削除(補助メソッド)### + def remove_helper(node, val) + return if node.nil? + # 1. ノードを探索して削除 + if val < node.val + node.left = remove_helper(node.left, val) + elsif val > node.val + node.right = remove_helper(node.right, val) + else + if node.left.nil? || node.right.nil? + child = node.left || node.right + # 子ノード数 = 0 の場合、node をそのまま削除して返す + return if child.nil? + # 子ノード数 = 1 の場合、node をそのまま削除する + node = child + else + # 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える + temp = node.right + while !temp.left.nil? + temp = temp.left + end + node.right = remove_helper(node.right, temp.val) + node.val = temp.val + end + end + # ノードの高さを更新する + update_height(node) + # 2. 回転操作を行い、部分木の平衡を回復する + rotate(node) + end ``` -### 3.   ノードの検索 +### 3.   ノードの探索 -AVL木でのノード検索操作は二分探索木のそれと一致しており、ここでは詳述しません。 +AVL 木のノード探索操作は二分探索木と同じなので、ここでは繰り返しません。 -## 7.5.4   AVL木の典型的な応用 +## 7.5.4   AVL 木の代表的な応用 -- 大量のデータの整理と格納に使用され、検索頻度が高く、挿入と削除の頻度が低いシナリオに適しています。 -- データベースのインデックスシステムの構築に使用されます。 -- 赤黒木も一般的な平衡二分探索木の一種です。AVL木と比較して、赤黒木はより緩い平衡条件を持ち、ノードの挿入と削除にかかる回転数が少なく、ノードの追加と削除操作の平均効率が高くなります。 +- 大規模データの整理・格納に用いられ、高頻度の探索と低頻度の追加・削除に適しています。 +- データベースのインデックスシステムの構築に使われます。 +- 赤黒木も代表的な平衡二分探索木の一つです。AVL 木と比べると、赤黒木は平衡条件がより緩く、ノードの挿入・削除に必要な回転操作が少ないため、平均的な更新効率はより高くなります。 diff --git a/ja/docs/chapter_tree/binary_search_tree.md b/ja/docs/chapter_tree/binary_search_tree.md index 5e46323fe..506eef1d5 100644 --- a/ja/docs/chapter_tree/binary_search_tree.md +++ b/ja/docs/chapter_tree/binary_search_tree.md @@ -4,10 +4,10 @@ comments: true # 7.4   二分探索木 -下図に示すように、二分探索木は以下の条件を満たします。 +以下の図に示すように、二分探索木(binary search tree)は次の条件を満たします。 1. 根ノードについて、左部分木のすべてのノードの値 $<$ 根ノードの値 $<$ 右部分木のすべてのノードの値。 -2. 任意のノードの左と右の部分木も二分探索木です。つまり、条件`1.`も満たします。 +2. 任意のノードの左部分木と右部分木も二分探索木であり、すなわち条件 `1.` も満たします。 ![二分探索木](binary_search_tree.assets/binary_search_tree.png){ class="animation-figure" } @@ -15,18 +15,18 @@ comments: true ## 7.4.1   二分探索木の操作 -二分探索木をクラス`BinarySearchTree`としてカプセル化し、木の根ノードを指すメンバー変数`root`を宣言します。 +二分探索木をクラス `BinarySearchTree` としてカプセル化し、木の根ノードを指すメンバ変数 `root` を宣言します。 -### 1.   ノードの検索 +### 1.   ノードの探索 -ターゲットノード値`num`が与えられた場合、二分探索木の性質に従って検索できます。下図に示すように、ノード`cur`を宣言し、二分木の根ノード`root`から開始し、ノード値`cur.val`と`num`のサイズを比較するループを行います。 +目標ノードの値 `num` が与えられたら、二分探索木の性質に基づいて探索できます。以下の図に示すように、ノード `cur` を宣言し、二分木の根ノード `root` から出発して、ノード値 `cur.val` と `num` の大小関係を繰り返し比較します。 -- `cur.val < num`の場合、ターゲットノードは`cur`の右部分木にあることを意味するため、`cur = cur.right`を実行します。 -- `cur.val > num`の場合、ターゲットノードは`cur`の左部分木にあることを意味するため、`cur = cur.left`を実行します。 -- `cur.val = num`の場合、ターゲットノードが見つかったことを意味するため、ループを終了してノードを返します。 +- `cur.val < num` の場合、目標ノードは `cur` の右部分木にあるため、`cur = cur.right` を実行します。 +- `cur.val > num` の場合、目標ノードは `cur` の左部分木にあるため、`cur = cur.left` を実行します。 +- `cur.val = num` の場合、目標ノードが見つかったことを表し、ループを抜けてそのノードを返します。 === "<1>" - ![二分探索木でのノード検索例](binary_search_tree.assets/bst_search_step1.png){ class="animation-figure" } + ![二分探索木のノード探索例](binary_search_tree.assets/bst_search_step1.png){ class="animation-figure" } === "<2>" ![bst_search_step2](binary_search_tree.assets/bst_search_step2.png){ class="animation-figure" } @@ -37,9 +37,9 @@ comments: true === "<4>" ![bst_search_step4](binary_search_tree.assets/bst_search_step4.png){ class="animation-figure" } -

図 7-17   二分探索木でのノード検索例

+

図 7-17   二分探索木のノード探索例

-二分探索木での検索操作は二分探索アルゴリズムと同じ原理で動作し、各ラウンドでケースの半分を排除します。ループ数は最大で二分木の高さです。二分木が平衡している場合、$O(\log n)$の時間を使用します。コード例は以下の通りです: +二分探索木の探索操作は二分探索アルゴリズムと同じ原理で動作し、各ラウンドで半分の候補を除外します。ループ回数の上限は二分木の高さであり、二分木が平衡であれば $O(\log n)$ 時間です。コード例は次のとおりです。 === "Python" @@ -47,15 +47,15 @@ comments: true def search(self, num: int) -> TreeNode | None: """ノードを探索""" cur = self._root - # ループで探索、葉ノードを通過した後にブレーク + # ループで探索し、葉ノードを越えたら抜ける while cur is not None: - # ターゲットノードはcurの右部分木にある + # 目標ノードは cur の右部分木にある if cur.val < num: cur = cur.right - # ターゲットノードはcurの左部分木にある + # 目標ノードは cur の左部分木にある elif cur.val > num: cur = cur.left - # ターゲットノードを発見、ループをブレーク + # 目標ノードが見つかったらループを抜ける else: break return cur @@ -64,18 +64,18 @@ comments: true === "C++" ```cpp title="binary_search_tree.cpp" - /* ノードを検索 */ + /* ノードを探索 */ TreeNode *search(int num) { TreeNode *cur = root; - // ループで検索、葉ノードを通り過ぎたら終了 + // ループで探索し、葉ノードを越えたら抜ける while (cur != nullptr) { - // 目標ノードはcurの右部分木にある + // 目標ノードは cur の右部分木にある if (cur->val < num) cur = cur->right; - // 目標ノードはcurの左部分木にある + // 目標ノードは cur の左部分木にある else if (cur->val > num) cur = cur->left; - // 目標ノードを見つけた、ループを抜ける + // 目標ノードが見つかったらループを抜ける else break; } @@ -87,22 +87,22 @@ comments: true === "Java" ```java title="binary_search_tree.java" - /* ノードを検索 */ + /* ノードを探索 */ TreeNode search(int num) { TreeNode cur = root; - // ループで検索、葉ノードを通過後に終了 + // ループで探索し、葉ノードを越えたら抜ける while (cur != null) { - // 対象ノードは cur の右部分木にある + // 目標ノードは cur の右部分木にある if (cur.val < num) cur = cur.right; - // 対象ノードは cur の左部分木にある + // 目標ノードは cur の左部分木にある else if (cur.val > num) cur = cur.left; - // 対象ノードを見つけた、ループを終了 + // 目標ノードが見つかったらループを抜ける else break; } - // 対象ノードを返す + // 目標ノードを返す return cur; } ``` @@ -110,99 +110,275 @@ comments: true === "C#" ```csharp title="binary_search_tree.cs" - [class]{BinarySearchTree}-[func]{Search} + /* ノードを探索 */ + TreeNode? Search(int num) { + TreeNode? cur = root; + // ループで探索し、葉ノードを越えたら抜ける + while (cur != null) { + // 目標ノードは cur の右部分木にある + if (cur.val < num) cur = + cur.right; + // 目標ノードは cur の左部分木にある + else if (cur.val > num) + cur = cur.left; + // 目標ノードが見つかったらループを抜ける + else + break; + } + // 目標ノードを返す + return cur; + } ``` === "Go" ```go title="binary_search_tree.go" - [class]{binarySearchTree}-[func]{search} + /* ノードを探索 */ + func (bst *binarySearchTree) search(num int) *TreeNode { + node := bst.root + // ループで探索し、葉ノードを越えたら抜ける + for node != nil { + if node.Val.(int) < num { + // 目標ノードは cur の右部分木にある + node = node.Right + } else if node.Val.(int) > num { + // 目標ノードは cur の左部分木にある + node = node.Left + } else { + // 目標ノードが見つかったらループを抜ける + break + } + } + // 目標ノードを返す + return node + } ``` === "Swift" ```swift title="binary_search_tree.swift" - [class]{BinarySearchTree}-[func]{search} + /* ノードを探索 */ + func search(num: Int) -> TreeNode? { + var cur = root + // ループで探索し、葉ノードを越えたら抜ける + while cur != nil { + // 目標ノードは cur の右部分木にある + if cur!.val < num { + cur = cur?.right + } + // 目標ノードは cur の左部分木にある + else if cur!.val > num { + cur = cur?.left + } + // 目標ノードが見つかったらループを抜ける + else { + break + } + } + // 目標ノードを返す + return cur + } ``` === "JS" ```javascript title="binary_search_tree.js" - [class]{BinarySearchTree}-[func]{search} + /* ノードを探索 */ + search(num) { + let cur = this.root; + // ループで探索し、葉ノードを越えたら抜ける + while (cur !== null) { + // 目標ノードは cur の右部分木にある + if (cur.val < num) cur = cur.right; + // 目標ノードは cur の左部分木にある + else if (cur.val > num) cur = cur.left; + // 目標ノードが見つかったらループを抜ける + else break; + } + // 目標ノードを返す + return cur; + } ``` === "TS" ```typescript title="binary_search_tree.ts" - [class]{BinarySearchTree}-[func]{search} + /* ノードを探索 */ + search(num: number): TreeNode | null { + let cur = this.root; + // ループで探索し、葉ノードを越えたら抜ける + while (cur !== null) { + // 目標ノードは cur の右部分木にある + if (cur.val < num) cur = cur.right; + // 目標ノードは cur の左部分木にある + else if (cur.val > num) cur = cur.left; + // 目標ノードが見つかったらループを抜ける + else break; + } + // 目標ノードを返す + return cur; + } ``` === "Dart" ```dart title="binary_search_tree.dart" - [class]{BinarySearchTree}-[func]{search} + /* ノードを探索 */ + TreeNode? search(int _num) { + TreeNode? cur = _root; + // ループで探索し、葉ノードを越えたら抜ける + while (cur != null) { + // 目標ノードは cur の右部分木にある + if (cur.val < _num) + cur = cur.right; + // 目標ノードは cur の左部分木にある + else if (cur.val > _num) + cur = cur.left; + // 目標ノードが見つかったらループを抜ける + else + break; + } + // 目標ノードを返す + return cur; + } ``` === "Rust" ```rust title="binary_search_tree.rs" - [class]{BinarySearchTree}-[func]{search} + /* ノードを探索 */ + pub fn search(&self, num: i32) -> OptionTreeNodeRc { + let mut cur = self.root.clone(); + // ループで探索し、葉ノードを越えたら抜ける + while let Some(node) = cur.clone() { + match num.cmp(&node.borrow().val) { + // 目標ノードは cur の右部分木にある + Ordering::Greater => cur = node.borrow().right.clone(), + // 目標ノードは cur の左部分木にある + Ordering::Less => cur = node.borrow().left.clone(), + // 目標ノードが見つかったらループを抜ける + Ordering::Equal => break, + } + } + + // 目標ノードを返す + cur + } ``` === "C" ```c title="binary_search_tree.c" - [class]{BinarySearchTree}-[func]{search} + /* ノードを探索 */ + TreeNode *search(BinarySearchTree *bst, int num) { + TreeNode *cur = bst->root; + // ループで探索し、葉ノードを越えたら抜ける + while (cur != NULL) { + if (cur->val < num) { + // 目標ノードは cur の右部分木にある + cur = cur->right; + } else if (cur->val > num) { + // 目標ノードは cur の左部分木にある + cur = cur->left; + } else { + // 目標ノードが見つかったらループを抜ける + break; + } + } + // 目標ノードを返す + return cur; + } ``` === "Kotlin" ```kotlin title="binary_search_tree.kt" - [class]{BinarySearchTree}-[func]{search} + /* ノードを探索 */ + fun search(num: Int): TreeNode? { + var cur = root + // ループで探索し、葉ノードを越えたら抜ける + while (cur != null) { + // 目標ノードは cur の右部分木にある + cur = if (cur._val < num) + cur.right + // 目標ノードは cur の左部分木にある + else if (cur._val > num) + cur.left + // 目標ノードが見つかったらループを抜ける + else + break + } + // 目標ノードを返す + return cur + } ``` === "Ruby" ```ruby title="binary_search_tree.rb" - [class]{BinarySearchTree}-[func]{search} + ### ノードを検索 ### + def search(num) + cur = @root + + # ループで探索し、葉ノードを越えたら抜ける + while !cur.nil? + # 目標ノードは cur の右部分木にある + if cur.val < num + cur = cur.right + # 目標ノードは cur の左部分木にある + elsif cur.val > num + cur = cur.left + # 目標ノードが見つかったらループを抜ける + else + break + end + end + + cur + end ``` +??? pythontutor "コードの可視化" + +
+ + ### 2.   ノードの挿入 -挿入する要素`num`が与えられた場合、二分探索木の性質「左部分木 < 根ノード < 右部分木」を維持するため、挿入操作は下図に示すように進行します。 +挿入する要素 `num` が与えられたとき、二分探索木の「左部分木 < 根ノード < 右部分木」という性質を保つため、挿入操作の流れは以下の図のようになります。 -1. **挿入位置を見つける**: 検索操作と同様に、根ノードから開始し、現在のノード値と`num`のサイズ関係に従って下向きにループし、葉ノードを通過(`None`に走査)するまで、ループを終了します。 -2. **この位置にノードを挿入**: ノード`num`を初期化し、`None`があった場所に配置します。 +1. **挿入位置を探索する**:探索操作と同様に、根ノードから出発し、現在のノード値と `num` の大小関係に基づいて下方向へ探索を繰り返し、葉ノードを越えて(`None` まで到達して)ループを抜けます。 +2. **その位置にノードを挿入する**:ノード `num` を初期化し、そのノードを `None` の位置に置きます。 -![二分探索木へのノード挿入](binary_search_tree.assets/bst_insert.png){ class="animation-figure" } +![二分探索木にノードを挿入する](binary_search_tree.assets/bst_insert.png){ class="animation-figure" } -

図 7-18   二分探索木へのノード挿入

+

図 7-18   二分探索木にノードを挿入する

-コード実装では、以下の2点に注意してください。 +コード実装では、次の 2 点に注意が必要です。 -- 二分探索木は重複ノードの存在を許可しません。そうでなければ、その定義に違反します。したがって、挿入するノードが既に木に存在する場合、挿入は実行されず、ノードは直接戻ります。 -- 挿入操作を実行するには、前のループからのノードを保存するためにノード`pre`を使用する必要があります。このようにして、`None`に走査したときに、その親ノードを取得でき、ノード挿入操作を完了できます。 +- 二分探索木では重複ノードを許可しません。そうでないと定義に反するためです。したがって、挿入対象のノードが木内にすでに存在する場合は、挿入を行わずそのまま返します。 +- ノード挿入を実現するために、ノード `pre` を用いて前回のループのノードを保持する必要があります。これにより、`None` までたどり着いたときにその親ノードを取得でき、ノード挿入を完了できます。 === "Python" ```python title="binary_search_tree.py" def insert(self, num: int): """ノードを挿入""" - # 木が空の場合、ルートノードを初期化 + # 木が空なら、根ノードを初期化する if self._root is None: self._root = TreeNode(num) return - # ループで探索、葉ノードを通過した後にブレーク + # ループで探索し、葉ノードを越えたら抜ける cur, pre = self._root, None while cur is not None: - # 重複ノードを発見したため、戻る + # 重複ノードが見つかったら、直ちに返す if cur.val == num: return pre = cur - # 挿入位置はcurの右部分木にある + # 挿入位置は cur の右部分木にある if cur.val < num: cur = cur.right - # 挿入位置はcurの左部分木にある + # 挿入位置は cur の左部分木にある else: cur = cur.left # ノードを挿入 @@ -218,22 +394,22 @@ comments: true ```cpp title="binary_search_tree.cpp" /* ノードを挿入 */ void insert(int num) { - // 木が空の場合、ルートノードを初期化 + // 木が空なら、根ノードを初期化する if (root == nullptr) { root = new TreeNode(num); return; } TreeNode *cur = root, *pre = nullptr; - // ループで検索、葉ノードを通り過ぎたら終了 + // ループで探索し、葉ノードを越えたら抜ける while (cur != nullptr) { - // 重複ノードを見つけた場合、戻る + // 重複ノードが見つかったら、直ちに返す if (cur->val == num) return; pre = cur; - // 挿入位置はcurの右部分木にある + // 挿入位置は cur の右部分木にある if (cur->val < num) cur = cur->right; - // 挿入位置はcurの左部分木にある + // 挿入位置は cur の左部分木にある else cur = cur->left; } @@ -251,15 +427,15 @@ comments: true ```java title="binary_search_tree.java" /* ノードを挿入 */ void insert(int num) { - // 木が空の場合、根ノードを初期化 + // 木が空なら、根ノードを初期化する if (root == null) { root = new TreeNode(num); return; } TreeNode cur = root, pre = null; - // ループで検索、葉ノードを通過後に終了 + // ループで探索し、葉ノードを越えたら抜ける while (cur != null) { - // 重複ノードを見つけた場合、戻る + // 重複ノードが見つかったら、直ちに返す if (cur.val == num) return; pre = cur; @@ -282,90 +458,382 @@ comments: true === "C#" ```csharp title="binary_search_tree.cs" - [class]{BinarySearchTree}-[func]{Insert} + /* ノードを挿入 */ + void Insert(int num) { + // 木が空なら、根ノードを初期化する + if (root == null) { + root = new TreeNode(num); + return; + } + TreeNode? cur = root, pre = null; + // ループで探索し、葉ノードを越えたら抜ける + while (cur != null) { + // 重複ノードが見つかったら、直ちに返す + if (cur.val == num) + return; + pre = cur; + // 挿入位置は cur の右部分木にある + if (cur.val < num) + cur = cur.right; + // 挿入位置は cur の左部分木にある + else + cur = cur.left; + } + + // ノードを挿入 + TreeNode node = new(num); + if (pre != null) { + if (pre.val < num) + pre.right = node; + else + pre.left = node; + } + } ``` === "Go" ```go title="binary_search_tree.go" - [class]{binarySearchTree}-[func]{insert} + /* ノードを挿入 */ + func (bst *binarySearchTree) insert(num int) { + cur := bst.root + // 木が空なら、根ノードを初期化する + if cur == nil { + bst.root = NewTreeNode(num) + return + } + // 挿入対象ノードの直前のノード位置 + var pre *TreeNode = nil + // ループで探索し、葉ノードを越えたら抜ける + for cur != nil { + if cur.Val == num { + return + } + pre = cur + if cur.Val.(int) < num { + cur = cur.Right + } else { + cur = cur.Left + } + } + // ノードを挿入 + node := NewTreeNode(num) + if pre.Val.(int) < num { + pre.Right = node + } else { + pre.Left = node + } + } ``` === "Swift" ```swift title="binary_search_tree.swift" - [class]{BinarySearchTree}-[func]{insert} + /* ノードを挿入 */ + func insert(num: Int) { + // 木が空なら、根ノードを初期化する + if root == nil { + root = TreeNode(x: num) + return + } + var cur = root + var pre: TreeNode? + // ループで探索し、葉ノードを越えたら抜ける + while cur != nil { + // 重複ノードが見つかったら、直ちに返す + if cur!.val == num { + return + } + pre = cur + // 挿入位置は cur の右部分木にある + if cur!.val < num { + cur = cur?.right + } + // 挿入位置は cur の左部分木にある + else { + cur = cur?.left + } + } + // ノードを挿入 + let node = TreeNode(x: num) + if pre!.val < num { + pre?.right = node + } else { + pre?.left = node + } + } ``` === "JS" ```javascript title="binary_search_tree.js" - [class]{BinarySearchTree}-[func]{insert} + /* ノードを挿入 */ + insert(num) { + // 木が空なら、根ノードを初期化する + if (this.root === null) { + this.root = new TreeNode(num); + return; + } + let cur = this.root, + pre = null; + // ループで探索し、葉ノードを越えたら抜ける + while (cur !== null) { + // 重複ノードが見つかったら、直ちに返す + if (cur.val === num) return; + pre = cur; + // 挿入位置は cur の右部分木にある + if (cur.val < num) cur = cur.right; + // 挿入位置は cur の左部分木にある + else cur = cur.left; + } + // ノードを挿入 + const node = new TreeNode(num); + if (pre.val < num) pre.right = node; + else pre.left = node; + } ``` === "TS" ```typescript title="binary_search_tree.ts" - [class]{BinarySearchTree}-[func]{insert} + /* ノードを挿入 */ + insert(num: number): void { + // 木が空なら、根ノードを初期化する + if (this.root === null) { + this.root = new TreeNode(num); + return; + } + let cur: TreeNode | null = this.root, + pre: TreeNode | null = null; + // ループで探索し、葉ノードを越えたら抜ける + while (cur !== null) { + // 重複ノードが見つかったら、直ちに返す + if (cur.val === num) return; + pre = cur; + // 挿入位置は cur の右部分木にある + if (cur.val < num) cur = cur.right; + // 挿入位置は cur の左部分木にある + else cur = cur.left; + } + // ノードを挿入 + const node = new TreeNode(num); + if (pre!.val < num) pre!.right = node; + else pre!.left = node; + } ``` === "Dart" ```dart title="binary_search_tree.dart" - [class]{BinarySearchTree}-[func]{insert} + /* ノードを挿入 */ + void insert(int _num) { + // 木が空なら、根ノードを初期化する + if (_root == null) { + _root = TreeNode(_num); + return; + } + TreeNode? cur = _root; + TreeNode? pre = null; + // ループで探索し、葉ノードを越えたら抜ける + while (cur != null) { + // 重複ノードが見つかったら、直ちに返す + if (cur.val == _num) return; + pre = cur; + // 挿入位置は cur の右部分木にある + if (cur.val < _num) + cur = cur.right; + // 挿入位置は cur の左部分木にある + else + cur = cur.left; + } + // ノードを挿入 + TreeNode? node = TreeNode(_num); + if (pre!.val < _num) + pre.right = node; + else + pre.left = node; + } ``` === "Rust" ```rust title="binary_search_tree.rs" - [class]{BinarySearchTree}-[func]{insert} + /* ノードを挿入 */ + pub fn insert(&mut self, num: i32) { + // 木が空なら、根ノードを初期化する + if self.root.is_none() { + self.root = Some(TreeNode::new(num)); + return; + } + let mut cur = self.root.clone(); + let mut pre = None; + // ループで探索し、葉ノードを越えたら抜ける + while let Some(node) = cur.clone() { + match num.cmp(&node.borrow().val) { + // 重複ノードが見つかったら、直ちに返す + Ordering::Equal => return, + // 挿入位置は cur の右部分木にある + Ordering::Greater => { + pre = cur.clone(); + cur = node.borrow().right.clone(); + } + // 挿入位置は cur の左部分木にある + Ordering::Less => { + pre = cur.clone(); + cur = node.borrow().left.clone(); + } + } + } + // ノードを挿入 + let pre = pre.unwrap(); + let node = Some(TreeNode::new(num)); + if num > pre.borrow().val { + pre.borrow_mut().right = node; + } else { + pre.borrow_mut().left = node; + } + } ``` === "C" ```c title="binary_search_tree.c" - [class]{BinarySearchTree}-[func]{insert} + /* ノードを挿入 */ + void insert(BinarySearchTree *bst, int num) { + // 木が空なら、根ノードを初期化する + if (bst->root == NULL) { + bst->root = newTreeNode(num); + return; + } + TreeNode *cur = bst->root, *pre = NULL; + // ループで探索し、葉ノードを越えたら抜ける + while (cur != NULL) { + // 重複ノードが見つかったら、直ちに返す + if (cur->val == num) { + return; + } + pre = cur; + if (cur->val < num) { + // 挿入位置は cur の右部分木にある + cur = cur->right; + } else { + // 挿入位置は cur の左部分木にある + cur = cur->left; + } + } + // ノードを挿入 + TreeNode *node = newTreeNode(num); + if (pre->val < num) { + pre->right = node; + } else { + pre->left = node; + } + } ``` === "Kotlin" ```kotlin title="binary_search_tree.kt" - [class]{BinarySearchTree}-[func]{insert} + /* ノードを挿入 */ + fun insert(num: Int) { + // 木が空なら、根ノードを初期化する + if (root == null) { + root = TreeNode(num) + return + } + var cur = root + var pre: TreeNode? = null + // ループで探索し、葉ノードを越えたら抜ける + while (cur != null) { + // 重複ノードが見つかったら、直ちに返す + if (cur._val == num) + return + pre = cur + // 挿入位置は cur の右部分木にある + cur = if (cur._val < num) + cur.right + // 挿入位置は cur の左部分木にある + else + cur.left + } + // ノードを挿入 + val node = TreeNode(num) + if (pre?._val!! < num) + pre.right = node + else + pre.left = node + } ``` === "Ruby" ```ruby title="binary_search_tree.rb" - [class]{BinarySearchTree}-[func]{insert} + ### ノードを挿入 ### + def insert(num) + # 木が空なら、根ノードを初期化する + if @root.nil? + @root = TreeNode.new(num) + return + end + + # ループで探索し、葉ノードを越えたら抜ける + cur, pre = @root, nil + while !cur.nil? + # 重複ノードが見つかったら、直ちに返す + return if cur.val == num + + pre = cur + # 挿入位置は cur の右部分木にある + if cur.val < num + cur = cur.right + # 挿入位置は cur の左部分木にある + else + cur = cur.left + end + end + + # ノードを挿入 + node = TreeNode.new(num) + if pre.val < num + pre.right = node + else + pre.left = node + end + end ``` -ノードの検索と同様に、ノードの挿入には$O(\log n)$の時間を使用します。 +??? pythontutor "コードの可視化" + +
+ + +ノード探索と同様に、ノード挿入には $O(\log n)$ 時間を要します。 ### 3.   ノードの削除 -まず、二分木でターゲットノードを見つけ、それを削除します。ノードの挿入と同様に、削除操作が完了した後も、二分探索木の性質「左部分木 < 根ノード < 右部分木」が満たされることを保証する必要があります。したがって、ターゲットノードの子ノード数に基づいて、0、1、2の3つのケースに分け、対応するノード削除操作を実行します。 +まず二分木内で目標ノードを見つけ、その後で削除します。ノード挿入と同様に、削除操作の完了後も二分探索木の「左部分木 < 根ノード < 右部分木」という性質が保たれる必要があります。そのため、目標ノードの子ノード数に応じて、0、1、2 の 3 つのケースに分けて対応する削除操作を行います。 -下図に示すように、削除するノードの次数が$0$の場合、そのノードは葉ノードであることを意味し、直接削除できます。 +以下の図に示すように、削除対象ノードの次数が $0$ のとき、そのノードは葉ノードであり、直接削除できます。 -![二分探索木でのノード削除(次数0)](binary_search_tree.assets/bst_remove_case1.png){ class="animation-figure" } +![二分探索木でノードを削除する(次数 0 )](binary_search_tree.assets/bst_remove_case1.png){ class="animation-figure" } -

図 7-19   二分探索木でのノード削除(次数0)

+

図 7-19   二分探索木でノードを削除する(次数 0 )

-下図に示すように、削除するノードの次数が$1$の場合、削除するノードをその子ノードで置き換えるだけで十分です。 +以下の図に示すように、削除対象ノードの次数が $1$ のとき、削除対象ノードをその子ノードで置き換えれば十分です。 -![二分探索木でのノード削除(次数1)](binary_search_tree.assets/bst_remove_case2.png){ class="animation-figure" } +![二分探索木でノードを削除する(次数 1 )](binary_search_tree.assets/bst_remove_case2.png){ class="animation-figure" } -

図 7-20   二分探索木でのノード削除(次数1)

+

図 7-20   二分探索木でノードを削除する(次数 1 )

-削除するノードの次数が$2$の場合、直接削除することはできませんが、ノードを使用して置き換える必要があります。二分探索木の性質「左部分木 $<$ 根ノード $<$ 右部分木」を維持するため、**このノードは右部分木の最小ノードまたは左部分木の最大ノードのいずれかです**。 +削除対象ノードの次数が $2$ のときは、直接削除できず、別のノードでそのノードを置き換える必要があります。二分探索木の「左部分木 $<$ 根ノード $<$ 右部分木」という性質を保つ必要があるため、**このノードには右部分木の最小ノードまたは左部分木の最大ノードを使えます**。 -右部分木の最小ノード(中順走査での次のノード)を選択すると仮定すると、削除操作は下図に示すように進行します。 +右部分木の最小ノード(中順走査で次のノード)を選ぶと仮定すると、削除操作の流れは以下の図のようになります。 -1. 削除するノードの「中順走査シーケンス」での次のノードを見つけ、`tmp`として示します。 -2. 削除するノードの値を`tmp`の値で置き換え、木内でノード`tmp`を再帰的に削除します。 +1. 削除対象ノードの「中順走査列」における次のノードを見つけ、`tmp` と記します。 +2. `tmp` の値で削除対象ノードの値を上書きし、木の中でノード `tmp` を再帰的に削除します。 === "<1>" - ![二分探索木でのノード削除(次数2)](binary_search_tree.assets/bst_remove_case3_step1.png){ class="animation-figure" } + ![二分探索木でノードを削除する(次数 2 )](binary_search_tree.assets/bst_remove_case3_step1.png){ class="animation-figure" } === "<2>" ![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png){ class="animation-figure" } @@ -376,57 +844,57 @@ comments: true === "<4>" ![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png){ class="animation-figure" } -

図 7-21   二分探索木でのノード削除(次数2)

+

図 7-21   二分探索木でノードを削除する(次数 2 )

-ノードを削除する操作も$O(\log n)$の時間を使用します。削除するノードを見つけるのに$O(\log n)$の時間が必要で、中順走査の後継ノードを取得するのに$O(\log n)$の時間が必要です。コード例は以下の通りです: +ノード削除操作も同様に $O(\log n)$ 時間を要します。削除対象ノードの探索に $O(\log n)$ 時間、中順走査の後続ノードの取得に $O(\log n)$ 時間が必要です。コード例は次のとおりです。 === "Python" ```python title="binary_search_tree.py" def remove(self, num: int): """ノードを削除""" - # 木が空の場合、戻る + # 木が空なら、そのまま早期リターンする if self._root is None: return - # ループで探索、葉ノードを通過した後にブレーク + # ループで探索し、葉ノードを越えたら抜ける cur, pre = self._root, None while cur is not None: - # 削除するノードを発見、ループをブレーク + # 削除対象のノードが見つかったら、ループを抜ける if cur.val == num: break pre = cur - # 削除するノードはcurの右部分木にある + # 削除対象ノードは cur の右部分木にある if cur.val < num: cur = cur.right - # 削除するノードはcurの左部分木にある + # 削除対象ノードは cur の左部分木にある else: cur = cur.left - # 削除するノードが存在しない場合、戻る + # 削除対象ノードがなければそのまま返す if cur is None: return - # 子ノード数 = 0 または 1 + # 子ノード数 = 0 or 1 if cur.left is None or cur.right is None: - # 子ノード数 = 0/1の場合、child = null/その子ノード + # 子ノード数が 0 / 1 のとき、child = null / その子ノード child = cur.left or cur.right - # ノードcurを削除 + # ノード cur を削除する if cur != self._root: if pre.left == cur: pre.left = child else: pre.right = child else: - # 削除されるノードがルートの場合、ルートを再割り当て + # 削除ノードが根ノードなら、根ノードを再設定 self._root = child # 子ノード数 = 2 else: - # curの中順走査の次のノードを取得 + # 中順走査における cur の次ノードを取得 tmp: TreeNode = cur.right while tmp.left is not None: tmp = tmp.left - # 再帰的にノードtmpを削除 + # ノード tmp を再帰的に削除 self.remove(tmp.val) - # curをtmpで置き換え + # tmp で cur を上書きする cur.val = tmp.val ``` @@ -435,54 +903,54 @@ comments: true ```cpp title="binary_search_tree.cpp" /* ノードを削除 */ void remove(int num) { - // 木が空の場合、戻る + // 木が空なら、そのまま早期リターンする if (root == nullptr) return; TreeNode *cur = root, *pre = nullptr; - // ループで検索、葉ノードを通り過ぎたら終了 + // ループで探索し、葉ノードを越えたら抜ける while (cur != nullptr) { - // 削除するノードを見つけた、ループを抜ける + // 削除対象のノードが見つかったら、ループを抜ける if (cur->val == num) break; pre = cur; - // 削除するノードはcurの右部分木にある + // 削除対象ノードは cur の右部分木にある if (cur->val < num) cur = cur->right; - // 削除するノードはcurの左部分木にある + // 削除対象ノードは cur の左部分木にある else cur = cur->left; } - // 削除するノードがない場合、戻る + // 削除対象ノードがなければそのまま返す if (cur == nullptr) return; - // 子ノード数 = 0 または 1 + // 子ノード数 = 0 or 1 if (cur->left == nullptr || cur->right == nullptr) { - // 子ノード数 = 0 / 1の場合、child = nullptr / その子ノード + // 子ノード数 = 0 / 1 のとき、child = nullptr / その子ノード TreeNode *child = cur->left != nullptr ? cur->left : cur->right; - // ノードcurを削除 + // ノード cur を削除する if (cur != root) { if (pre->left == cur) pre->left = child; else pre->right = child; } else { - // 削除されるノードがルートの場合、ルートを再割り当て + // 削除ノードが根ノードなら、根ノードを再設定 root = child; } - // メモリを解放 + // メモリを解放する delete cur; } // 子ノード数 = 2 else { - // curの中順走査の次のノードを取得 + // 中順走査における cur の次ノードを取得 TreeNode *tmp = cur->right; while (tmp->left != nullptr) { tmp = tmp->left; } int tmpVal = tmp->val; - // ノードtmpを再帰的に削除 + // ノード tmp を再帰的に削除 remove(tmp->val); - // curをtmpで置き換え + // tmp で cur を上書きする cur->val = tmpVal; } } @@ -493,51 +961,51 @@ comments: true ```java title="binary_search_tree.java" /* ノードを削除 */ void remove(int num) { - // 木が空の場合、戻る + // 木が空なら、そのまま早期リターンする if (root == null) return; TreeNode cur = root, pre = null; - // ループで検索、葉ノードを通過後に終了 + // ループで探索し、葉ノードを越えたら抜ける while (cur != null) { - // 削除するノードを見つけた、ループを終了 + // 削除対象のノードが見つかったら、ループを抜ける if (cur.val == num) break; pre = cur; - // 削除するノードは cur の右部分木にある + // 削除対象ノードは cur の右部分木にある if (cur.val < num) cur = cur.right; - // 削除するノードは cur の左部分木にある + // 削除対象ノードは cur の左部分木にある else cur = cur.left; } - // 削除するノードがない場合、戻る + // 削除対象ノードがなければそのまま返す if (cur == null) return; - // 子ノード数 = 0 または 1 + // 子ノード数 = 0 or 1 if (cur.left == null || cur.right == null) { - // 子ノード数 = 0/1 の場合、child = null/その子ノード + // 子ノード数が 0 / 1 のとき、child = null / その子ノード TreeNode child = cur.left != null ? cur.left : cur.right; - // ノード cur を削除 + // ノード cur を削除する if (cur != root) { if (pre.left == cur) pre.left = child; else pre.right = child; } else { - // 削除されるノードが根の場合、根を再割り当て + // 削除ノードが根ノードなら、根ノードを再設定 root = child; } } // 子ノード数 = 2 else { - // cur の中順走査の次のノードを取得 + // 中順走査における cur の次ノードを取得 TreeNode tmp = cur.right; while (tmp.left != null) { tmp = tmp.left; } - // 再帰的にノード tmp を削除 + // ノード tmp を再帰的に削除 remove(tmp.val); - // cur を tmp で置き換える + // tmp で cur を上書きする cur.val = tmp.val; } } @@ -546,101 +1014,623 @@ comments: true === "C#" ```csharp title="binary_search_tree.cs" - [class]{BinarySearchTree}-[func]{Remove} + /* ノードを削除 */ + void Remove(int num) { + // 木が空なら、そのまま早期リターンする + if (root == null) + return; + TreeNode? cur = root, pre = null; + // ループで探索し、葉ノードを越えたら抜ける + while (cur != null) { + // 削除対象のノードが見つかったら、ループを抜ける + if (cur.val == num) + break; + pre = cur; + // 削除対象ノードは cur の右部分木にある + if (cur.val < num) + cur = cur.right; + // 削除対象ノードは cur の左部分木にある + else + cur = cur.left; + } + // 削除対象ノードがなければそのまま返す + if (cur == null) + return; + // 子ノード数 = 0 or 1 + if (cur.left == null || cur.right == null) { + // 子ノード数が 0 / 1 のとき、child = null / その子ノード + TreeNode? child = cur.left ?? cur.right; + // ノード cur を削除する + if (cur != root) { + if (pre!.left == cur) + pre.left = child; + else + pre.right = child; + } else { + // 削除ノードが根ノードなら、根ノードを再設定 + root = child; + } + } + // 子ノード数 = 2 + else { + // 中順走査における cur の次ノードを取得 + TreeNode? tmp = cur.right; + while (tmp.left != null) { + tmp = tmp.left; + } + // ノード tmp を再帰的に削除 + Remove(tmp.val!.Value); + // tmp で cur を上書きする + cur.val = tmp.val; + } + } ``` === "Go" ```go title="binary_search_tree.go" - [class]{binarySearchTree}-[func]{remove} + /* ノードを削除 */ + func (bst *binarySearchTree) remove(num int) { + cur := bst.root + // 木が空なら、そのまま早期リターンする + if cur == nil { + return + } + // 削除対象ノードの直前のノード位置 + var pre *TreeNode = nil + // ループで探索し、葉ノードを越えたら抜ける + for cur != nil { + if cur.Val == num { + break + } + pre = cur + if cur.Val.(int) < num { + // 削除対象ノードは右部分木にある + cur = cur.Right + } else { + // 削除対象ノードは左部分木にある + cur = cur.Left + } + } + // 削除対象ノードがなければそのまま返す + if cur == nil { + return + } + // 子ノード数は 0 または 1 + if cur.Left == nil || cur.Right == nil { + var child *TreeNode = nil + // 削除対象ノードの子ノードを取り出す + if cur.Left != nil { + child = cur.Left + } else { + child = cur.Right + } + // ノード cur を削除する + if cur != bst.root { + if pre.Left == cur { + pre.Left = child + } else { + pre.Right = child + } + } else { + // 削除ノードが根ノードなら、根ノードを再設定 + bst.root = child + } + // 子ノード数は 2 + } else { + // 中順走査で削除対象ノード `cur` の次のノードを取得する + tmp := cur.Right + for tmp.Left != nil { + tmp = tmp.Left + } + // ノード tmp を再帰的に削除 + bst.remove(tmp.Val.(int)) + // tmp で cur を上書きする + cur.Val = tmp.Val + } + } ``` === "Swift" ```swift title="binary_search_tree.swift" - [class]{BinarySearchTree}-[func]{remove} + /* ノードを削除 */ + func remove(num: Int) { + // 木が空なら、そのまま早期リターンする + if root == nil { + return + } + var cur = root + var pre: TreeNode? + // ループで探索し、葉ノードを越えたら抜ける + while cur != nil { + // 削除対象のノードが見つかったら、ループを抜ける + if cur!.val == num { + break + } + pre = cur + // 削除対象ノードは cur の右部分木にある + if cur!.val < num { + cur = cur?.right + } + // 削除対象ノードは cur の左部分木にある + else { + cur = cur?.left + } + } + // 削除対象ノードがなければそのまま返す + if cur == nil { + return + } + // 子ノード数 = 0 or 1 + if cur?.left == nil || cur?.right == nil { + // 子ノード数が 0 / 1 のとき、child = null / その子ノード + let child = cur?.left ?? cur?.right + // ノード cur を削除する + if cur !== root { + if pre?.left === cur { + pre?.left = child + } else { + pre?.right = child + } + } else { + // 削除ノードが根ノードなら、根ノードを再設定 + root = child + } + } + // 子ノード数 = 2 + else { + // 中順走査における cur の次ノードを取得 + var tmp = cur?.right + while tmp?.left != nil { + tmp = tmp?.left + } + // ノード tmp を再帰的に削除 + remove(num: tmp!.val) + // tmp で cur を上書きする + cur?.val = tmp!.val + } + } ``` === "JS" ```javascript title="binary_search_tree.js" - [class]{BinarySearchTree}-[func]{remove} + /* ノードを削除 */ + remove(num) { + // 木が空なら、そのまま早期リターンする + if (this.root === null) return; + let cur = this.root, + pre = null; + // ループで探索し、葉ノードを越えたら抜ける + while (cur !== null) { + // 削除対象のノードが見つかったら、ループを抜ける + if (cur.val === num) break; + pre = cur; + // 削除対象ノードは cur の右部分木にある + if (cur.val < num) cur = cur.right; + // 削除対象ノードは cur の左部分木にある + else cur = cur.left; + } + // 削除対象ノードがなければそのまま返す + if (cur === null) return; + // 子ノード数 = 0 or 1 + if (cur.left === null || cur.right === null) { + // 子ノード数が 0 / 1 のとき、child = null / その子ノード + const child = cur.left !== null ? cur.left : cur.right; + // ノード cur を削除する + if (cur !== this.root) { + if (pre.left === cur) pre.left = child; + else pre.right = child; + } else { + // 削除ノードが根ノードなら、根ノードを再設定 + this.root = child; + } + } + // 子ノード数 = 2 + else { + // 中順走査における cur の次ノードを取得 + let tmp = cur.right; + while (tmp.left !== null) { + tmp = tmp.left; + } + // ノード tmp を再帰的に削除 + this.remove(tmp.val); + // tmp で cur を上書きする + cur.val = tmp.val; + } + } ``` === "TS" ```typescript title="binary_search_tree.ts" - [class]{BinarySearchTree}-[func]{remove} + /* ノードを削除 */ + remove(num: number): void { + // 木が空なら、そのまま早期リターンする + if (this.root === null) return; + let cur: TreeNode | null = this.root, + pre: TreeNode | null = null; + // ループで探索し、葉ノードを越えたら抜ける + while (cur !== null) { + // 削除対象のノードが見つかったら、ループを抜ける + if (cur.val === num) break; + pre = cur; + // 削除対象ノードは cur の右部分木にある + if (cur.val < num) cur = cur.right; + // 削除対象ノードは cur の左部分木にある + else cur = cur.left; + } + // 削除対象ノードがなければそのまま返す + if (cur === null) return; + // 子ノード数 = 0 or 1 + if (cur.left === null || cur.right === null) { + // 子ノード数が 0 / 1 のとき、child = null / その子ノード + const child: TreeNode | null = + cur.left !== null ? cur.left : cur.right; + // ノード cur を削除する + if (cur !== this.root) { + if (pre!.left === cur) pre!.left = child; + else pre!.right = child; + } else { + // 削除ノードが根ノードなら、根ノードを再設定 + this.root = child; + } + } + // 子ノード数 = 2 + else { + // 中順走査における cur の次ノードを取得 + let tmp: TreeNode | null = cur.right; + while (tmp!.left !== null) { + tmp = tmp!.left; + } + // ノード tmp を再帰的に削除 + this.remove(tmp!.val); + // tmp で cur を上書きする + cur.val = tmp!.val; + } + } ``` === "Dart" ```dart title="binary_search_tree.dart" - [class]{BinarySearchTree}-[func]{remove} + /* ノードを削除 */ + void remove(int _num) { + // 木が空なら、そのまま早期リターンする + if (_root == null) return; + TreeNode? cur = _root; + TreeNode? pre = null; + // ループで探索し、葉ノードを越えたら抜ける + while (cur != null) { + // 削除対象のノードが見つかったら、ループを抜ける + if (cur.val == _num) break; + pre = cur; + // 削除対象ノードは cur の右部分木にある + if (cur.val < _num) + cur = cur.right; + // 削除対象ノードは cur の左部分木にある + else + cur = cur.left; + } + // 削除対象ノードがない場合は、そのまま返す + if (cur == null) return; + // 子ノード数 = 0 or 1 + if (cur.left == null || cur.right == null) { + // 子ノード数が 0 / 1 のとき、child = null / その子ノード + TreeNode? child = cur.left ?? cur.right; + // ノード cur を削除する + if (cur != _root) { + if (pre!.left == cur) + pre.left = child; + else + pre.right = child; + } else { + // 削除ノードが根ノードなら、根ノードを再設定 + _root = child; + } + } else { + // 子ノード数 = 2 + // 中順走査における cur の次のノードを取得 + TreeNode? tmp = cur.right; + while (tmp!.left != null) { + tmp = tmp.left; + } + // ノード tmp を再帰的に削除 + remove(tmp.val); + // tmp で cur を上書きする + cur.val = tmp.val; + } + } ``` === "Rust" ```rust title="binary_search_tree.rs" - [class]{BinarySearchTree}-[func]{remove} + /* ノードを削除 */ + pub fn remove(&mut self, num: i32) { + // 木が空なら、そのまま早期リターンする + if self.root.is_none() { + return; + } + let mut cur = self.root.clone(); + let mut pre = None; + // ループで探索し、葉ノードを越えたら抜ける + while let Some(node) = cur.clone() { + match num.cmp(&node.borrow().val) { + // 削除対象のノードが見つかったら、ループを抜ける + Ordering::Equal => break, + // 削除対象ノードは cur の右部分木にある + Ordering::Greater => { + pre = cur.clone(); + cur = node.borrow().right.clone(); + } + // 削除対象ノードは cur の左部分木にある + Ordering::Less => { + pre = cur.clone(); + cur = node.borrow().left.clone(); + } + } + } + // 削除対象ノードがなければそのまま返す + if cur.is_none() { + return; + } + let cur = cur.unwrap(); + let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone()); + match (left_child.clone(), right_child.clone()) { + // 子ノード数 = 0 or 1 + (None, None) | (Some(_), None) | (None, Some(_)) => { + // 子ノード数 = 0 / 1 のとき、child = nullptr / その子ノード + let child = left_child.or(right_child); + let pre = pre.unwrap(); + // ノード cur を削除する + if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) { + let left = pre.borrow().left.clone(); + if left.is_some() && Rc::ptr_eq(left.as_ref().unwrap(), &cur) { + pre.borrow_mut().left = child; + } else { + pre.borrow_mut().right = child; + } + } else { + // 削除ノードが根ノードなら、根ノードを再設定 + self.root = child; + } + } + // 子ノード数 = 2 + (Some(_), Some(_)) => { + // 中順走査における cur の次ノードを取得 + let mut tmp = cur.borrow().right.clone(); + while let Some(node) = tmp.clone() { + if node.borrow().left.is_some() { + tmp = node.borrow().left.clone(); + } else { + break; + } + } + let tmp_val = tmp.unwrap().borrow().val; + // ノード tmp を再帰的に削除 + self.remove(tmp_val); + // tmp で cur を上書きする + cur.borrow_mut().val = tmp_val; + } + } + } ``` === "C" ```c title="binary_search_tree.c" - [class]{BinarySearchTree}-[func]{removeItem} + /* ノードを削除 */ + // stdio.h を導入しているため、ここでは remove 識別子を使えない + void removeItem(BinarySearchTree *bst, int num) { + // 木が空なら、そのまま早期リターンする + if (bst->root == NULL) + return; + TreeNode *cur = bst->root, *pre = NULL; + // ループで探索し、葉ノードを越えたら抜ける + while (cur != NULL) { + // 削除対象のノードが見つかったら、ループを抜ける + if (cur->val == num) + break; + pre = cur; + if (cur->val < num) { + // 削除対象ノードは root の右部分木にある + cur = cur->right; + } else { + // 削除対象ノードは root の左部分木にある + cur = cur->left; + } + } + // 削除対象ノードがなければそのまま返す + if (cur == NULL) + return; + // 削除対象ノードに子ノードがあるかを判定する + if (cur->left == NULL || cur->right == NULL) { + /* 子ノード数 = 0 or 1 */ + // 子ノード数 = 0 / 1 のとき、child = nullptr / その子ノード + TreeNode *child = cur->left != NULL ? cur->left : cur->right; + // ノード cur を削除する + if (pre->left == cur) { + pre->left = child; + } else { + pre->right = child; + } + // メモリを解放する + free(cur); + } else { + /* 子ノード数 = 2 */ + // 中順走査における cur の次ノードを取得 + TreeNode *tmp = cur->right; + while (tmp->left != NULL) { + tmp = tmp->left; + } + int tmpVal = tmp->val; + // ノード tmp を再帰的に削除 + removeItem(bst, tmp->val); + // tmp で cur を上書きする + cur->val = tmpVal; + } + } ``` === "Kotlin" ```kotlin title="binary_search_tree.kt" - [class]{BinarySearchTree}-[func]{remove} + /* ノードを削除 */ + fun remove(num: Int) { + // 木が空なら、そのまま早期リターンする + if (root == null) + return + var cur = root + var pre: TreeNode? = null + // ループで探索し、葉ノードを越えたら抜ける + while (cur != null) { + // 削除対象のノードが見つかったら、ループを抜ける + if (cur._val == num) + break + pre = cur + // 削除対象ノードは cur の右部分木にある + cur = if (cur._val < num) + cur.right + // 削除対象ノードは cur の左部分木にある + else + cur.left + } + // 削除対象ノードがなければそのまま返す + if (cur == null) + return + // 子ノード数 = 0 or 1 + if (cur.left == null || cur.right == null) { + // 子ノード数が 0 / 1 のとき、child = null / その子ノード + val child = if (cur.left != null) + cur.left + else + cur.right + // ノード cur を削除する + if (cur != root) { + if (pre!!.left == cur) + pre.left = child + else + pre.right = child + } else { + // 削除ノードが根ノードなら、根ノードを再設定 + root = child + } + // 子ノード数 = 2 + } else { + // 中順走査における cur の次ノードを取得 + var tmp = cur.right + while (tmp!!.left != null) { + tmp = tmp.left + } + // ノード tmp を再帰的に削除 + remove(tmp._val) + // tmp で cur を上書きする + cur._val = tmp._val + } + } ``` === "Ruby" ```ruby title="binary_search_tree.rb" - [class]{BinarySearchTree}-[func]{remove} + ### ノードを削除 ### + def remove(num) + # 木が空なら、そのまま早期リターンする + return if @root.nil? + + # ループで探索し、葉ノードを越えたら抜ける + cur, pre = @root, nil + while !cur.nil? + # 削除対象のノードが見つかったら、ループを抜ける + break if cur.val == num + + pre = cur + # 削除対象ノードは cur の右部分木にある + if cur.val < num + cur = cur.right + # 削除対象ノードは cur の左部分木にある + else + cur = cur.left + end + end + # 削除対象ノードがなければそのまま返す + return if cur.nil? + + # 子ノード数 = 0 or 1 + if cur.left.nil? || cur.right.nil? + # 子ノード数が 0 / 1 のとき、child = null / その子ノード + child = cur.left || cur.right + # ノード cur を削除する + if cur != @root + if pre.left == cur + pre.left = child + else + pre.right = child + end + else + # 削除ノードが根ノードなら、根ノードを再設定 + @root = child + end + # 子ノード数 = 2 + else + # 中順走査における cur の次ノードを取得 + tmp = cur.right + while !tmp.left.nil? + tmp = tmp.left + end + # ノード tmp を再帰的に削除 + remove(tmp.val) + # tmp で cur を上書きする + cur.val = tmp.val + end + end ``` -### 4.   中順走査は順序付けされている +??? pythontutor "コードの可視化" -下図に示すように、二分木の中順走査は「左 $\rightarrow$ 根 $\rightarrow$ 右」の走査順序に従い、二分探索木は「左子ノード $<$ 根ノード $<$ 右子ノード」のサイズ関係を満たします。 +
+ -これは、二分探索木で中順走査を実行するときに、常に次に小さいノードが最初に走査されることを意味し、重要な性質につながります:**二分探索木の中順走査のシーケンスは昇順です**。 +### 4.   中順走査は昇順 -中順走査の昇順性質を使用して、二分探索木で順序付けされたデータを取得するには$O(n)$の時間のみが必要で、追加のソート操作は不要であり、非常に効率的です。 +以下の図に示すように、二分木の中順走査は「左 $\rightarrow$ 根 $\rightarrow$ 右」という順序に従い、二分探索木は「左子ノード $<$ 根ノード $<$ 右子ノード」という大小関係を満たします。 -![二分探索木の中順走査シーケンス](binary_search_tree.assets/bst_inorder_traversal.png){ class="animation-figure" } +これは、二分探索木で中順走査を行うと常に次の最小ノードが優先して走査されることを意味し、そこから重要な性質が導かれます。**二分探索木の中順走査列は昇順です**。 -

図 7-22   二分探索木の中順走査シーケンス

+中順走査が昇順になる性質を利用すれば、二分探索木から整列済みデータを取得するのに必要な時間は $O(n)$ のみで、追加のソート操作は不要です。非常に効率的です。 + +![二分探索木の中順走査列](binary_search_tree.assets/bst_inorder_traversal.png){ class="animation-figure" } + +

図 7-22   二分探索木の中順走査列

## 7.4.2   二分探索木の効率 -データのセットが与えられた場合、配列または二分探索木を使用して格納することを検討します。下の表を観察すると、二分探索木のすべての操作は対数時間計算量を持ち、安定して効率的です。配列は、頻繁な追加と検索や削除の頻度が少ないシナリオでのみ、二分探索木よりも効率的です。 +あるデータ集合が与えられたとき、配列または二分探索木で格納する場合を考えます。次の表を見ると、二分探索木の各操作の時間計算量はいずれも対数オーダーであり、安定して高効率です。高頻度の追加と低頻度の探索・削除という場面でのみ、配列のほうが二分探索木より効率的です。

表 7-2   配列と探索木の効率比較

-| | 未ソート配列 | 二分探索木 | -| -------------- | -------------- | ------------------ | -| 要素の検索 | $O(n)$ | $O(\log n)$ | -| 要素の挿入 | $O(1)$ | $O(\log n)$ | -| 要素の削除 | $O(n)$ | $O(\log n)$ | +| | 無秩序配列 | 二分探索木 | +| -------- | -------- | ----------- | +| 要素の探索 | $O(n)$ | $O(\log n)$ | +| 要素の挿入 | $O(1)$ | $O(\log n)$ | +| 要素の削除 | $O(n)$ | $O(\log n)$ |
-理想的には、二分探索木は「平衡」しており、任意のノードを$\log n$ループ内で見つけることができます。 +理想的な状況では、二分探索木は「平衡」しており、その場合は $\log n$ 回のループ内で任意のノードを探索できます。 -しかし、二分探索木で継続的にノードを挿入および削除すると、下図に示すように連結リストに退化する可能性があり、さまざまな操作の時間計算量も$O(n)$に悪化します。 +しかし、二分探索木でノードの挿入と削除を繰り返すと、二分木が以下の図のような連結リストへ退化する可能性があり、このとき各操作の時間計算量も $O(n)$ に退化します。 ![二分探索木の退化](binary_search_tree.assets/bst_degradation.png){ class="animation-figure" }

図 7-23   二分探索木の退化

-## 7.4.3   二分探索木の一般的な応用 +## 7.4.3   二分探索木の代表的な応用 -- システムでの多レベルインデックスとして使用され、効率的な検索、挿入、削除操作を実装します。 -- 特定の検索アルゴリズムの基盤となるデータ構造として機能します。 -- データストリームを格納して、その順序付けされた状態を維持するために使用されます。 +- システム内の多段インデックスとして用いられ、効率的な探索、挿入、削除操作を実現します。 +- 一部の探索アルゴリズムの基盤データ構造として使われます。 +- データストリームを格納し、その順序状態を保つために使われます。 diff --git a/ja/docs/chapter_tree/binary_tree.md b/ja/docs/chapter_tree/binary_tree.md index 0eb6777da..e3711b890 100644 --- a/ja/docs/chapter_tree/binary_tree.md +++ b/ja/docs/chapter_tree/binary_tree.md @@ -4,27 +4,27 @@ comments: true # 7.1   二分木 -二分木は、祖先と子孫の間の階層関係を表現し、「二つに分割する」分割統治法の論理を体現する非線形データ構造です。連結リストと同様に、二分木の基本単位はノードであり、各ノードは値、左の子ノードへの参照、右の子ノードへの参照を含みます。 +二分木(binary tree)は非線形データ構造の一種であり、「祖先」と「子孫」の派生関係を表し、「一つを二つに分ける」分割統治の考え方を体現しています。連結リストと同様に、二分木の基本単位はノードであり、各ノードは値、左子ノードへの参照、右子ノードへの参照を含みます。 === "Python" ```python title="" class TreeNode: - """二分木ノード""" + """二分木ノードクラス""" def __init__(self, val: int): self.val: int = val # ノード値 - self.left: TreeNode | None = None # 左の子ノードへの参照 - self.right: TreeNode | None = None # 右の子ノードへの参照 + self.left: TreeNode | None = None # 左子ノード参照 + self.right: TreeNode | None = None # 右子ノード参照 ``` === "C++" ```cpp title="" - /* 二分木ノード */ + /* 二分木ノード構造体 */ struct TreeNode { int val; // ノード値 - TreeNode *left; // 左の子ノードへのポインタ - TreeNode *right; // 右の子ノードへのポインタ + TreeNode *left; // 左子ノードポインタ + TreeNode *right; // 右子ノードポインタ TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; ``` @@ -32,11 +32,11 @@ comments: true === "Java" ```java title="" - /* 二分木ノード */ + /* 二分木ノードクラス */ class TreeNode { int val; // ノード値 - TreeNode left; // 左の子ノードへの参照 - TreeNode right; // 右の子ノードへの参照 + TreeNode left; // 左子ノード参照 + TreeNode right; // 右子ノード参照 TreeNode(int x) { val = x; } } ``` @@ -44,18 +44,18 @@ comments: true === "C#" ```csharp title="" - /* 二分木ノード */ + /* 二分木ノードクラス */ class TreeNode(int? x) { public int? val = x; // ノード値 - public TreeNode? left; // 左の子ノードへの参照 - public TreeNode? right; // 右の子ノードへの参照 + public TreeNode? left; // 左子ノード参照 + public TreeNode? right; // 右子ノード参照 } ``` === "Go" ```go title="" - /* 二分木ノード */ + /* 二分木ノード構造体 */ type TreeNode struct { Val int Left *TreeNode @@ -64,8 +64,8 @@ comments: true /* コンストラクタ */ func NewTreeNode(v int) *TreeNode { return &TreeNode{ - Left: nil, // 左の子ノードへのポインタ - Right: nil, // 右の子ノードへのポインタ + Left: nil, // 左子ノードポインタ + Right: nil, // 右子ノードポインタ Val: v, // ノード値 } } @@ -74,11 +74,11 @@ comments: true === "Swift" ```swift title="" - /* 二分木ノード */ + /* 二分木ノードクラス */ class TreeNode { var val: Int // ノード値 - var left: TreeNode? // 左の子ノードへの参照 - var right: TreeNode? // 右の子ノードへの参照 + var left: TreeNode? // 左子ノード参照 + var right: TreeNode? // 右子ノード参照 init(x: Int) { val = x @@ -89,11 +89,11 @@ comments: true === "JS" ```javascript title="" - /* 二分木ノード */ + /* 二分木ノードクラス */ class TreeNode { val; // ノード値 - left; // 左の子ノードへのポインタ - right; // 右の子ノードへのポインタ + left; // 左子ノードポインタ + right; // 右子ノードポインタ constructor(val, left, right) { this.val = val === undefined ? 0 : val; this.left = left === undefined ? null : left; @@ -105,7 +105,7 @@ comments: true === "TS" ```typescript title="" - /* 二分木ノード */ + /* 二分木ノードクラス */ class TreeNode { val: number; left: TreeNode | null; @@ -113,8 +113,8 @@ comments: true constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) { this.val = val === undefined ? 0 : val; // ノード値 - this.left = left === undefined ? null : left; // 左の子ノードへの参照 - this.right = right === undefined ? null : right; // 右の子ノードへの参照 + this.left = left === undefined ? null : left; // 左子ノード参照 + this.right = right === undefined ? null : right; // 右子ノード参照 } } ``` @@ -122,11 +122,11 @@ comments: true === "Dart" ```dart title="" - /* 二分木ノード */ + /* 二分木ノードクラス */ class TreeNode { int val; // ノード値 - TreeNode? left; // 左の子ノードへの参照 - TreeNode? right; // 右の子ノードへの参照 + TreeNode? left; // 左子ノード参照 + TreeNode? right; // 右子ノード参照 TreeNode(this.val, [this.left, this.right]); } ``` @@ -137,11 +137,11 @@ comments: true use std::rc::Rc; use std::cell::RefCell; - /* 二分木ノード */ + /* 二分木ノード構造体 */ struct TreeNode { val: i32, // ノード値 - left: Option>>, // 左の子ノードへの参照 - right: Option>>, // 右の子ノードへの参照 + left: Option>>, // 左子ノード参照 + right: Option>>, // 右子ノード参照 } impl TreeNode { @@ -159,12 +159,12 @@ comments: true === "C" ```c title="" - /* 二分木ノード */ + /* 二分木ノード構造体 */ typedef struct TreeNode { int val; // ノード値 int height; // ノードの高さ - struct TreeNode *left; // 左の子ノードへのポインタ - struct TreeNode *right; // 右の子ノードへのポインタ + struct TreeNode *left; // 左子ノードポインタ + struct TreeNode *right; // 右子ノードポインタ } TreeNode; /* コンストラクタ */ @@ -183,65 +183,74 @@ comments: true === "Kotlin" ```kotlin title="" - /* 二分木ノード */ + /* 二分木ノードクラス */ class TreeNode(val _val: Int) { // ノード値 - val left: TreeNode? = null // 左の子ノードへの参照 - val right: TreeNode? = null // 右の子ノードへの参照 + val left: TreeNode? = null // 左子ノード参照 + val right: TreeNode? = null // 右子ノード参照 } ``` === "Ruby" ```ruby title="" + ### 二分木ノードクラス ### + class TreeNode + attr_accessor :val # ノード値 + attr_accessor :left # 左子ノード参照 + attr_accessor :right # 右子ノード参照 + def initialize(val) + @val = val + end + end ``` -各ノードは2つの参照(ポインタ)を持ち、それぞれ左の子ノード右の子ノードを指しています。このノードは、これら2つの子ノードの親ノードと呼ばれます。二分木のノードが与えられたとき、このノードの左の子とその下にあるすべてのノードで形成される木を、このノードの左部分木と呼びます。同様に、右部分木も定義できます。 +各ノードは 2 つの参照(ポインタ)を持ち、それぞれ左子ノード(left-child node)右子ノード(right-child node)を指します。このノードはこれら 2 つの子ノードの親ノード(parent node)と呼ばれます。二分木のあるノードが与えられたとき、そのノードの左子ノードとその配下のノードからなる木をそのノードの左部分木(left subtree)と呼び、同様に右部分木(right subtree)が定義されます。 -**二分木では、葉ノードを除いて、他のすべてのノードは子ノードと空でない部分木を含みます。** 下図に示すように、「ノード2」を親ノードとして見ると、その左と右の子ノードはそれぞれ「ノード4」と「ノード5」です。左部分木は「ノード4」とその下にあるすべてのノードで形成され、右部分木は「ノード5」とその下にあるすべてのノードで形成されます。 +**二分木では、葉ノードを除くすべてのノードが子ノードと空でない部分木を持ちます**。以下の図に示すように、「ノード 2」を親ノードとみなすと、その左子ノードと右子ノードはそれぞれ「ノード 4」と「ノード 5」であり、左部分木は「ノード 4 とその配下のノードからなる木」、右部分木は「ノード 5 とその配下のノードからなる木」です。 ![親ノード、子ノード、部分木](binary_tree.assets/binary_tree_definition.png){ class="animation-figure" }

図 7-1   親ノード、子ノード、部分木

-## 7.1.1   二分木の一般的な用語 +## 7.1.1   二分木のよく使われる用語 -二分木でよく使用される用語を下図に示します。 +二分木でよく使われる用語を以下の図に示します。 -- 根ノード:二分木の最上位レベルにあるノードで、親ノードを持ちません。 -- 葉ノード:子ノードを持たないノードで、両方のポインタが`None`を指しています。 -- :2つのノードを結ぶ線分で、ノード間の参照(ポインタ)を表現します。 -- ノードのレベル:上から下に向かって増加し、根ノードがレベル1です。 -- ノードの次数:ノードが持つ子ノードの数です。二分木では、次数は0、1、または2になります。 -- 二分木の高さ:根ノードから最も遠い葉ノードまでの辺の数です。 -- ノードの深さ:根ノードからそのノードまでの辺の数です。 -- ノードの高さ:最も遠い葉ノードからそのノードまでの辺の数です。 +- 根ノード(root node):二分木の最上位にあるノードで、親ノードを持ちません。 +- 葉ノード(leaf node):子ノードを持たないノードで、2 本のポインタはいずれも `None` を指します。 +- 辺(edge):2 つのノードを結ぶ線分、すなわちノード参照(ポインタ)です。 +- ノードが属するレベル(level):上から下へ向かって増加し、根ノードのレベルは 1 です。 +- ノードの次数(degree):ノードの子ノードの数。二分木では次数の取り得る値は 0、1、2 です。 +- 二分木の高さ(height):根ノードから最も遠い葉ノードまでに通る辺の数。 +- ノードの深さ(depth):根ノードからそのノードまでに通る辺の数。 +- ノードの高さ(height):そのノードから最も遠い葉ノードまでに通る辺の数。 -![二分木の一般的な用語](binary_tree.assets/binary_tree_terminology.png){ class="animation-figure" } +![二分木のよく使われる用語](binary_tree.assets/binary_tree_terminology.png){ class="animation-figure" } -

図 7-2   二分木の一般的な用語

+

図 7-2   二分木のよく使われる用語

!!! tip - 「高さ」と「深さ」は通常「通過する辺の数」として定義しますが、一部の問題や教科書では「通過するノードの数」として定義されることがあります。この場合、高さと深さの両方を1だけ増やす必要があります。 + なお、通常「高さ」と「深さ」は「通過した辺の数」と定義しますが、問題や教材によっては「通過したノードの数」と定義する場合もあります。その場合、高さと深さはいずれも 1 を加える必要があります。 ## 7.1.2   二分木の基本操作 -### 1.   二分木の初期化 +### 1.   二分木を初期化する -連結リストと同様に、二分木の初期化では、まずノードを作成し、次にそれらの間の参照(ポインタ)を確立します。 +連結リストと同様に、まずノードを初期化し、その後で参照(ポインタ)を構築します。 === "Python" ```python title="binary_tree.py" - # 二分木の初期化 - # ノードの初期化 + # 二分木を初期化する + # ノードを初期化する n1 = TreeNode(val=1) n2 = TreeNode(val=2) n3 = TreeNode(val=3) n4 = TreeNode(val=4) n5 = TreeNode(val=5) - # ノード間の参照(ポインタ)を結ぶ + # ノード間の参照(ポインタ)を構築する n1.left = n2 n1.right = n3 n2.left = n4 @@ -251,14 +260,14 @@ comments: true === "C++" ```cpp title="binary_tree.cpp" - /* 二分木の初期化 */ - // ノードの初期化 + /* 二分木を初期化する */ + // ノードを初期化する TreeNode* n1 = new TreeNode(1); TreeNode* n2 = new TreeNode(2); TreeNode* n3 = new TreeNode(3); TreeNode* n4 = new TreeNode(4); TreeNode* n5 = new TreeNode(5); - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1->left = n2; n1->right = n3; n2->left = n4; @@ -268,13 +277,13 @@ comments: true === "Java" ```java title="binary_tree.java" - // ノードの初期化 + // ノードを初期化する TreeNode n1 = new TreeNode(1); TreeNode n2 = new TreeNode(2); TreeNode n3 = new TreeNode(3); TreeNode n4 = new TreeNode(4); TreeNode n5 = new TreeNode(5); - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1.left = n2; n1.right = n3; n2.left = n4; @@ -284,14 +293,14 @@ comments: true === "C#" ```csharp title="binary_tree.cs" - /* 二分木の初期化 */ - // ノードの初期化 + /* 二分木を初期化する */ + // ノードを初期化する TreeNode n1 = new(1); TreeNode n2 = new(2); TreeNode n3 = new(3); TreeNode n4 = new(4); TreeNode n5 = new(5); - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1.left = n2; n1.right = n3; n2.left = n4; @@ -301,14 +310,14 @@ comments: true === "Go" ```go title="binary_tree.go" - /* 二分木の初期化 */ - // ノードの初期化 + /* 二分木を初期化する */ + // ノードを初期化する n1 := NewTreeNode(1) n2 := NewTreeNode(2) n3 := NewTreeNode(3) n4 := NewTreeNode(4) n5 := NewTreeNode(5) - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1.Left = n2 n1.Right = n3 n2.Left = n4 @@ -318,13 +327,13 @@ comments: true === "Swift" ```swift title="binary_tree.swift" - // ノードの初期化 + // ノードを初期化する let n1 = TreeNode(x: 1) let n2 = TreeNode(x: 2) let n3 = TreeNode(x: 3) let n4 = TreeNode(x: 4) let n5 = TreeNode(x: 5) - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1.left = n2 n1.right = n3 n2.left = n4 @@ -334,14 +343,14 @@ comments: true === "JS" ```javascript title="binary_tree.js" - /* 二分木の初期化 */ - // ノードの初期化 + /* 二分木を初期化する */ + // ノードを初期化する let n1 = new TreeNode(1), n2 = new TreeNode(2), n3 = new TreeNode(3), n4 = new TreeNode(4), n5 = new TreeNode(5); - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1.left = n2; n1.right = n3; n2.left = n4; @@ -351,14 +360,14 @@ comments: true === "TS" ```typescript title="binary_tree.ts" - /* 二分木の初期化 */ - // ノードの初期化 + /* 二分木を初期化する */ + // ノードを初期化する let n1 = new TreeNode(1), n2 = new TreeNode(2), n3 = new TreeNode(3), n4 = new TreeNode(4), n5 = new TreeNode(5); - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1.left = n2; n1.right = n3; n2.left = n4; @@ -368,14 +377,14 @@ comments: true === "Dart" ```dart title="binary_tree.dart" - /* 二分木の初期化 */ - // ノードの初期化 + /* 二分木を初期化する */ + // ノードを初期化する TreeNode n1 = new TreeNode(1); TreeNode n2 = new TreeNode(2); TreeNode n3 = new TreeNode(3); TreeNode n4 = new TreeNode(4); TreeNode n5 = new TreeNode(5); - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1.left = n2; n1.right = n3; n2.left = n4; @@ -385,13 +394,13 @@ comments: true === "Rust" ```rust title="binary_tree.rs" - // ノードの初期化 + // ノードを初期化する let n1 = TreeNode::new(1); let n2 = TreeNode::new(2); let n3 = TreeNode::new(3); let n4 = TreeNode::new(4); let n5 = TreeNode::new(5); - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1.borrow_mut().left = Some(n2.clone()); n1.borrow_mut().right = Some(n3); n2.borrow_mut().left = Some(n4); @@ -401,14 +410,14 @@ comments: true === "C" ```c title="binary_tree.c" - /* 二分木の初期化 */ - // ノードの初期化 + /* 二分木を初期化する */ + // ノードを初期化する TreeNode *n1 = newTreeNode(1); TreeNode *n2 = newTreeNode(2); TreeNode *n3 = newTreeNode(3); TreeNode *n4 = newTreeNode(4); TreeNode *n5 = newTreeNode(5); - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1->left = n2; n1->right = n3; n2->left = n4; @@ -418,13 +427,13 @@ comments: true === "Kotlin" ```kotlin title="binary_tree.kt" - // ノードの初期化 + // ノードを初期化する val n1 = TreeNode(1) val n2 = TreeNode(2) val n3 = TreeNode(3) val n4 = TreeNode(4) val n5 = TreeNode(5) - // ノード間の参照(ポインタ)を結ぶ + // ノード間の参照(ポインタ)を構築する n1.left = n2 n1.right = n3 n2.left = n4 @@ -434,26 +443,41 @@ comments: true === "Ruby" ```ruby title="binary_tree.rb" - + # 二分木を初期化する + # ノードを初期化する + n1 = TreeNode.new(1) + n2 = TreeNode.new(2) + n3 = TreeNode.new(3) + n4 = TreeNode.new(4) + n5 = TreeNode.new(5) + # ノード間の参照(ポインタ)を構築する + n1.left = n2 + n1.right = n3 + n2.left = n4 + n2.right = n5 ``` +??? pythontutor "実行の可視化" + + https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + ### 2.   ノードの挿入と削除 -連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。下図に例を示します。 +連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。以下の図に 1 つの例を示します。 -![二分木でのノードの挿入と削除](binary_tree.assets/binary_tree_add_remove.png){ class="animation-figure" } +![二分木でノードを挿入・削除する](binary_tree.assets/binary_tree_add_remove.png){ class="animation-figure" } -

図 7-3   二分木でのノードの挿入と削除

+

図 7-3   二分木でノードを挿入・削除する

=== "Python" ```python title="binary_tree.py" # ノードの挿入と削除 p = TreeNode(0) - # n1 -> n2の間にノードPを挿入 + # n1 -> n2 の間にノード P を挿入する n1.left = p p.left = n2 - # ノードPを削除 + # ノード P を削除する n1.left = n2 ``` @@ -462,21 +486,23 @@ comments: true ```cpp title="binary_tree.cpp" /* ノードの挿入と削除 */ TreeNode* P = new TreeNode(0); - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する n1->left = P; P->left = n2; - // ノードPを削除 + // ノード P を削除する n1->left = n2; + // メモリを解放する + delete P; ``` === "Java" ```java title="binary_tree.java" TreeNode P = new TreeNode(0); - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する n1.left = P; P.left = n2; - // ノードPを削除 + // ノード P を削除する n1.left = n2; ``` @@ -485,10 +511,10 @@ comments: true ```csharp title="binary_tree.cs" /* ノードの挿入と削除 */ TreeNode P = new(0); - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する n1.left = P; P.left = n2; - // ノードPを削除 + // ノード P を削除する n1.left = n2; ``` @@ -496,11 +522,11 @@ comments: true ```go title="binary_tree.go" /* ノードの挿入と削除 */ - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する p := NewTreeNode(0) n1.Left = p p.Left = n2 - // ノードPを削除 + // ノード P を削除する n1.Left = n2 ``` @@ -508,10 +534,10 @@ comments: true ```swift title="binary_tree.swift" let P = TreeNode(x: 0) - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する n1.left = P P.left = n2 - // ノードPを削除 + // ノード P を削除する n1.left = n2 ``` @@ -520,10 +546,10 @@ comments: true ```javascript title="binary_tree.js" /* ノードの挿入と削除 */ let P = new TreeNode(0); - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する n1.left = P; P.left = n2; - // ノードPを削除 + // ノード P を削除する n1.left = n2; ``` @@ -532,10 +558,10 @@ comments: true ```typescript title="binary_tree.ts" /* ノードの挿入と削除 */ const P = new TreeNode(0); - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する n1.left = P; P.left = n2; - // ノードPを削除 + // ノード P を削除する n1.left = n2; ``` @@ -544,10 +570,10 @@ comments: true ```dart title="binary_tree.dart" /* ノードの挿入と削除 */ TreeNode P = new TreeNode(0); - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する n1.left = P; P.left = n2; - // ノードPを削除 + // ノード P を削除する n1.left = n2; ``` @@ -555,10 +581,10 @@ comments: true ```rust title="binary_tree.rs" let p = TreeNode::new(0); - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する n1.borrow_mut().left = Some(p.clone()); p.borrow_mut().left = Some(n2.clone()); - // ノードPを削除 + // ノード p を削除する n1.borrow_mut().left = Some(n2); ``` @@ -567,67 +593,79 @@ comments: true ```c title="binary_tree.c" /* ノードの挿入と削除 */ TreeNode *P = newTreeNode(0); - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する n1->left = P; P->left = n2; - // ノードPを削除 + // ノード P を削除する n1->left = n2; + // メモリを解放する + free(P); ``` === "Kotlin" ```kotlin title="binary_tree.kt" val P = TreeNode(0) - // n1とn2の間にノードPを挿入 + // n1 -> n2 の間にノード P を挿入する n1.left = P P.left = n2 - // ノードPを削除 + // ノード P を削除する n1.left = n2 ``` === "Ruby" ```ruby title="binary_tree.rb" - + # ノードの挿入と削除 + _p = TreeNode.new(0) + # n1 -> n2 の間にノード _p を挿入する + n1.left = _p + _p.left = n2 + # ノードを削除する + n1.left = n2 ``` -!!! tip +??? pythontutor "実行の可視化" - ノードの挿入は二分木の元の論理構造を変更する可能性があり、ノードの削除は通常そのノードとそのすべての部分木を削除することになることに注意してください。したがって、二分木では、挿入と削除は通常一連の操作を通じて実行され、意味のある結果を得ます。 - -## 7.1.3   二分木の一般的な種類 - -### 1.   完全二分木 - -下図に示すように、完全二分木では、すべてのレベルがノードで完全に埋められています。完全二分木では、葉ノードの次数は$0$で、他のすべてのノードの次数は$2$です。ノードの総数は$2^{h+1} - 1$として計算でき、ここで$h$は木の高さです。これは標準的な指数関係を示し、自然界の細胞分裂の一般的な現象を反映しています。 + https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false !!! tip - 中国語圏では、完全二分木はしばしば満二分木と呼ばれることに注意してください。 + 注意すべき点として、ノードの挿入は二分木の元の論理構造を変える可能性があり、ノードの削除は通常、そのノードと配下のすべての部分木の削除を意味します。そのため、二分木における挿入と削除は、実際に意味のある操作を実現するために、通常は一連の操作を組み合わせて行います。 -![完全二分木](binary_tree.assets/perfect_binary_tree.png){ class="animation-figure" } +## 7.1.3   一般的な二分木の種類 -

図 7-4   完全二分木

+### 1.   充足二分木 -### 2.   完備二分木 +以下の図に示すように、充足二分木(perfect binary tree)ではすべてのレベルのノードが完全に埋まっています。充足二分木では、葉ノードの次数は $0$ で、それ以外のすべてのノードの次数は $2$ です。木の高さを $h$ とすると、ノード総数は $2^{h+1} - 1$ となり、標準的な指数関係を示して、自然界でよく見られる細胞分裂の現象を反映しています。 -下図に示すように、完備二分木は、最下位レベルのみが完全に埋められていない可能性がある二分木で、最下位レベルのノードは左から右に連続して埋められる必要があります。完全二分木は完備二分木でもあることに注意してください。 +!!! tip -![完備二分木](binary_tree.assets/complete_binary_tree.png){ class="animation-figure" } + なお、中国語圏では充足二分木は満二分木と呼ばれることもあります。 -

図 7-5   完備二分木

+![充足二分木](binary_tree.assets/perfect_binary_tree.png){ class="animation-figure" } -### 3.   満二分木 +

図 7-4   充足二分木

-下図に示すように、満二分木では、葉ノードを除いて、他のすべてのノードが2つの子ノードを持ちます。 +### 2.   完全二分木 -![満二分木](binary_tree.assets/full_binary_tree.png){ class="animation-figure" } +以下の図に示すように、完全二分木(complete binary tree)では最下層のノードだけが完全に埋まっていなくてもよく、しかも最下層のノードは左から右へ連続して詰められていなければなりません。なお、充足二分木も完全二分木の一種です。 -

図 7-6   満二分木

+![完全二分木](binary_tree.assets/complete_binary_tree.png){ class="animation-figure" } + +

図 7-5   完全二分木

+ +### 3.   充満二分木 + +以下の図に示すように、充満二分木(full binary tree)では、葉ノードを除くすべてのノードが 2 つの子ノードを持ちます。 + +![充満二分木](binary_tree.assets/full_binary_tree.png){ class="animation-figure" } + +

図 7-6   充満二分木

### 4.   平衡二分木 -下図に示すように、平衡二分木では、任意のノードの左と右の部分木の高さの絶対差が1を超えません。 +以下の図に示すように、平衡二分木(balanced binary tree)では、任意のノードについて左部分木と右部分木の高さの差の絶対値が 1 を超えません。 ![平衡二分木](binary_tree.assets/balanced_binary_tree.png){ class="animation-figure" } @@ -635,26 +673,26 @@ comments: true ## 7.1.4   二分木の退化 -下図は、二分木の理想的な構造と退化した構造を示しています。二分木は、すべてのレベルが埋められているときに「完全二分木」になり、すべてのノードが一方に偏っているときに「連結リスト」に退化します。 +以下の図は、二分木の理想的な構造と退化した構造を示しています。二分木の各レベルのノードがすべて埋まっていると「充足二分木」となり、すべてのノードが片側に偏ると二分木は「連結リスト」へ退化します。 -- 完全二分木は、二分木の「分割統治法」の利点を十分に活用できる理想的なシナリオです。 -- 一方、連結リストは別の極端を表し、すべての操作が線形になり、時間計算量が$O(n)$になります。 +- 充足二分木は理想的なケースであり、二分木の「分割統治」の利点を十分に発揮できます。 +- 連結リストはその対極にあり、各種操作はすべて線形操作となり、時間計算量は $O(n)$ まで退化します。 -![二分木の最良と最悪の構造](binary_tree.assets/binary_tree_best_worst_cases.png){ class="animation-figure" } +![二分木の最良構造と最悪構造](binary_tree.assets/binary_tree_best_worst_cases.png){ class="animation-figure" } -

図 7-8   二分木の最良と最悪の構造

+

図 7-8   二分木の最良構造と最悪構造

-下表に示すように、最良と最悪の構造では、二分木は葉ノード数、総ノード数、高さの最大値または最小値を達成します。 +以下の表に示すように、最良構造と最悪構造では、二分木の葉ノード数、ノード総数、高さなどが極大または極小になります。 -

表 7-1   二分木の最良と最悪の構造

+

表 7-1   二分木の最良構造と最悪構造

-| | 完全二分木 | 連結リスト | -| ----------------------------------------------- | ------------------ | ----------- | -| レベル$i$のノード数 | $2^{i-1}$ | $1$ | -| 高さ$h$の木の葉ノード数 | $2^h$ | $1$ | -| 高さ$h$の木の総ノード数 | $2^{h+1} - 1$ | $h + 1$ | -| 総ノード数$n$の木の高さ | $\log_2 (n+1) - 1$ | $n - 1$ | +| | 充足二分木 | 連結リスト | +| --------------------------- | ------------------ | ------- | +| 第 $i$ レベルのノード数 | $2^{i-1}$ | $1$ | +| 高さ $h$ の木の葉ノード数 | $2^h$ | $1$ | +| 高さ $h$ の木のノード総数 | $2^{h+1} - 1$ | $h + 1$ | +| ノード総数 $n$ の木の高さ | $\log_2 (n+1) - 1$ | $n - 1$ |
diff --git a/ja/docs/chapter_tree/binary_tree_traversal.md b/ja/docs/chapter_tree/binary_tree_traversal.md index f35084e85..20c652233 100644 --- a/ja/docs/chapter_tree/binary_tree_traversal.md +++ b/ja/docs/chapter_tree/binary_tree_traversal.md @@ -4,41 +4,41 @@ comments: true # 7.2   二分木の走査 -物理的構造の観点から見ると、木は連結リストに基づくデータ構造です。したがって、その走査方法はポインタを通してノードに一つずつアクセスすることを含みます。しかし、木は非線形データ構造であるため、木の走査は連結リストの走査よりも複雑で、検索アルゴリズムの支援が必要です。 +物理構造の観点から見ると、木は連結リストを基盤としたデータ構造であり、その走査はポインタを通じてノードへ順にアクセスすることで行われます。しかし、木は非線形データ構造であるため、木の走査は連結リストの走査よりも複雑であり、検索アルゴリズムを用いて実現する必要があります。 -二分木の一般的な走査方法には、レベル順走査、前順走査、中順走査、後順走査があります。 +二分木の一般的な走査方法には、レベル順走査、先行順走査、中間順走査、後行順走査などがあります。 ## 7.2.1   レベル順走査 -下図に示すように、レベル順走査は二分木を上から下へ、層ごとに走査します。各レベル内では、左から右へノードを訪問します。 +次の図に示すように、レベル順走査(level-order traversal)では、二分木を上から下へ層ごとに走査し、各層では左から右の順にノードへアクセスします。 -レベル順走査は本質的に幅優先走査の一種で、幅優先探索(BFS)とも呼ばれ、「周囲に向かって外向きに拡張する」層ごとの走査方法を体現しています。 +レベル順走査は本質的に幅優先走査(breadth-first traversal)に属し、幅優先探索(breadth-first search, BFS)とも呼ばれます。これは「同心円状に外側へ広がる」ような、層ごとの走査方法を表しています。 ![二分木のレベル順走査](binary_tree_traversal.assets/binary_tree_bfs.png){ class="animation-figure" }

図 7-9   二分木のレベル順走査

-### 1.   コード実装 +### 1.   コードの実装 -幅優先走査は通常「キュー」の助けを借りて実装されます。キューは「先入れ先出し」の規則に従い、幅優先走査は「層ごとの進行」規則に従います。両者の基本的な考え方は一致しています。実装コードは以下の通りです: +幅優先走査は通常「キュー」を用いて実装します。キューは「先入れ先出し」の規則に従い、幅優先走査は「層ごとに進む」という規則に従います。両者の背後にある考え方は一致しています。実装コードは次のとおりです: === "Python" ```python title="binary_tree_bfs.py" def level_order(root: TreeNode | None) -> list[int]: """レベル順走査""" - # キューを初期化し、ルートノードを追加 + # キューを初期化し、ルートノードを追加する queue: deque[TreeNode] = deque() queue.append(root) - # 走査シーケンスを格納するリストを初期化 + # 走査順序を保存するためのリストを初期化する res = [] while queue: - node: TreeNode = queue.popleft() # キューからデキュー - res.append(node.val) # ノードの値を保存 + node: TreeNode = queue.popleft() # デキュー + res.append(node.val) # ノードの値を保存する if node.left is not None: - queue.append(node.left) # 左の子ノードをエンキュー + queue.append(node.left) # 左子ノードをキューに追加 if node.right is not None: - queue.append(node.right) # 右の子ノードをエンキュー + queue.append(node.right) # 右子ノードをキューに追加 return res ``` @@ -47,19 +47,19 @@ comments: true ```cpp title="binary_tree_bfs.cpp" /* レベル順走査 */ vector levelOrder(TreeNode *root) { - // キューを初期化、ルートノードを追加 + // キューを初期化し、ルートノードを追加する queue queue; queue.push(root); - // 走査順序を保存するリストを初期化 + // 走査順序を保存するためのリストを初期化する vector vec; while (!queue.empty()) { TreeNode *node = queue.front(); - queue.pop(); // キューからデキュー - vec.push_back(node->val); // ノード値を保存 + queue.pop(); // デキュー + vec.push_back(node->val); // ノードの値を保存する if (node->left != nullptr) - queue.push(node->left); // 左の子ノードをエンキュー + queue.push(node->left); // 左子ノードをキューに追加 if (node->right != nullptr) - queue.push(node->right); // 右の子ノードをエンキュー + queue.push(node->right); // 右子ノードをキューに追加 } return vec; } @@ -70,18 +70,18 @@ comments: true ```java title="binary_tree_bfs.java" /* レベル順走査 */ List levelOrder(TreeNode root) { - // キューを初期化し、根ノードを追加 + // キューを初期化し、ルートノードを追加する Queue queue = new LinkedList<>(); queue.add(root); - // 走査順序を格納するリストを初期化 + // 走査順序を保存するためのリストを初期化する List list = new ArrayList<>(); while (!queue.isEmpty()) { - TreeNode node = queue.poll(); // キューのデキュー - list.add(node.val); // ノードの値を保存 + TreeNode node = queue.poll(); // デキュー + list.add(node.val); // ノードの値を保存する if (node.left != null) - queue.offer(node.left); // 左の子ノードをエンキュー + queue.offer(node.left); // 左子ノードをキューに追加 if (node.right != null) - queue.offer(node.right); // 右の子ノードをエンキュー + queue.offer(node.right); // 右子ノードをキューに追加 } return list; } @@ -90,90 +90,282 @@ comments: true === "C#" ```csharp title="binary_tree_bfs.cs" - [class]{binary_tree_bfs}-[func]{LevelOrder} + /* レベル順走査 */ + List LevelOrder(TreeNode root) { + // キューを初期化し、ルートノードを追加する + Queue queue = new(); + queue.Enqueue(root); + // 走査順序を保存するためのリストを初期化する + List list = []; + while (queue.Count != 0) { + TreeNode node = queue.Dequeue(); // デキュー + list.Add(node.val!.Value); // ノードの値を保存する + if (node.left != null) + queue.Enqueue(node.left); // 左子ノードをキューに追加 + if (node.right != null) + queue.Enqueue(node.right); // 右子ノードをキューに追加 + } + return list; + } ``` === "Go" ```go title="binary_tree_bfs.go" - [class]{}-[func]{levelOrder} + /* レベル順走査 */ + func levelOrder(root *TreeNode) []any { + // キューを初期化し、ルートノードを追加する + queue := list.New() + queue.PushBack(root) + // 走査順を保存するためのスライスを初期化する + nums := make([]any, 0) + for queue.Len() > 0 { + // デキュー + node := queue.Remove(queue.Front()).(*TreeNode) + // ノードの値を保存する + nums = append(nums, node.Val) + if node.Left != nil { + // 左子ノードをキューに追加 + queue.PushBack(node.Left) + } + if node.Right != nil { + // 右子ノードをキューに追加 + queue.PushBack(node.Right) + } + } + return nums + } ``` === "Swift" ```swift title="binary_tree_bfs.swift" - [class]{}-[func]{levelOrder} + /* レベル順走査 */ + func levelOrder(root: TreeNode) -> [Int] { + // キューを初期化し、ルートノードを追加する + var queue: [TreeNode] = [root] + // 走査順序を保存するためのリストを初期化する + var list: [Int] = [] + while !queue.isEmpty { + let node = queue.removeFirst() // デキュー + list.append(node.val) // ノードの値を保存する + if let left = node.left { + queue.append(left) // 左子ノードをキューに追加 + } + if let right = node.right { + queue.append(right) // 右子ノードをキューに追加 + } + } + return list + } ``` === "JS" ```javascript title="binary_tree_bfs.js" - [class]{}-[func]{levelOrder} + /* レベル順走査 */ + function levelOrder(root) { + // キューを初期化し、ルートノードを追加する + const queue = [root]; + // 走査順序を保存するためのリストを初期化する + const list = []; + while (queue.length) { + let node = queue.shift(); // デキュー + list.push(node.val); // ノードの値を保存する + if (node.left) queue.push(node.left); // 左子ノードをキューに追加 + if (node.right) queue.push(node.right); // 右子ノードをキューに追加 + } + return list; + } ``` === "TS" ```typescript title="binary_tree_bfs.ts" - [class]{}-[func]{levelOrder} + /* レベル順走査 */ + function levelOrder(root: TreeNode | null): number[] { + // キューを初期化し、ルートノードを追加する + const queue = [root]; + // 走査順序を保存するためのリストを初期化する + const list: number[] = []; + while (queue.length) { + let node = queue.shift() as TreeNode; // デキュー + list.push(node.val); // ノードの値を保存する + if (node.left) { + queue.push(node.left); // 左子ノードをキューに追加 + } + if (node.right) { + queue.push(node.right); // 右子ノードをキューに追加 + } + } + return list; + } ``` === "Dart" ```dart title="binary_tree_bfs.dart" - [class]{}-[func]{levelOrder} + /* レベル順走査 */ + List levelOrder(TreeNode? root) { + // キューを初期化し、ルートノードを追加する + Queue queue = Queue(); + queue.add(root); + // 走査順序を保存するためのリストを初期化する + List res = []; + while (queue.isNotEmpty) { + TreeNode? node = queue.removeFirst(); // デキュー + res.add(node!.val); // ノードの値を保存する + if (node.left != null) queue.add(node.left); // 左子ノードをキューに追加 + if (node.right != null) queue.add(node.right); // 右子ノードをキューに追加 + } + return res; + } ``` === "Rust" ```rust title="binary_tree_bfs.rs" - [class]{}-[func]{level_order} + /* レベル順走査 */ + fn level_order(root: &Rc>) -> Vec { + // キューを初期化し、ルートノードを追加する + let mut que = VecDeque::new(); + que.push_back(root.clone()); + // 走査順序を保存するためのリストを初期化する + let mut vec = Vec::new(); + + while let Some(node) = que.pop_front() { + // デキュー + vec.push(node.borrow().val); // ノードの値を保存する + if let Some(left) = node.borrow().left.as_ref() { + que.push_back(left.clone()); // 左子ノードをキューに追加 + } + if let Some(right) = node.borrow().right.as_ref() { + que.push_back(right.clone()); // 右子ノードをキューに追加 + }; + } + vec + } ``` === "C" ```c title="binary_tree_bfs.c" - [class]{}-[func]{levelOrder} + /* レベル順走査 */ + int *levelOrder(TreeNode *root, int *size) { + /* 補助キュー */ + int front, rear; + int index, *arr; + TreeNode *node; + TreeNode **queue; + + /* 補助キュー */ + queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE); + // キューへのポインタ + front = 0, rear = 0; + // 根ノードを追加する + queue[rear++] = root; + // 走査順序を保存するためのリストを初期化する + /* 補助配列 */ + arr = (int *)malloc(sizeof(int) * MAX_SIZE); + // 配列ポインタ + index = 0; + while (front < rear) { + // デキュー + node = queue[front++]; + // ノードの値を保存する + arr[index++] = node->val; + if (node->left != NULL) { + // 左子ノードをキューに追加 + queue[rear++] = node->left; + } + if (node->right != NULL) { + // 右子ノードをキューに追加 + queue[rear++] = node->right; + } + } + // 配列長の値を更新 + *size = index; + arr = realloc(arr, sizeof(int) * (*size)); + + // 補助配列の領域を解放する + free(queue); + return arr; + } ``` === "Kotlin" ```kotlin title="binary_tree_bfs.kt" - [class]{}-[func]{levelOrder} + /* レベル順走査 */ + fun levelOrder(root: TreeNode?): MutableList { + // キューを初期化し、ルートノードを追加する + val queue = LinkedList() + queue.add(root) + // 走査順序を保存するためのリストを初期化する + val list = mutableListOf() + while (queue.isNotEmpty()) { + val node = queue.poll() // デキュー + list.add(node?._val!!) // ノードの値を保存する + if (node.left != null) + queue.offer(node.left) // 左子ノードをキューに追加 + if (node.right != null) + queue.offer(node.right) // 右子ノードをキューに追加 + } + return list + } ``` === "Ruby" ```ruby title="binary_tree_bfs.rb" - [class]{}-[func]{level_order} + ### レベル順走査 ### + def level_order(root) + # キューを初期化し、ルートノードを追加する + queue = [root] + # 走査順序を保存するためのリストを初期化する + res = [] + while !queue.empty? + node = queue.shift # デキュー + res << node.val # ノードの値を保存する + queue << node.left unless node.left.nil? # 左子ノードをキューに追加 + queue << node.right unless node.right.nil? # 右子ノードをキューに追加 + end + res + end ``` -### 2.   計算量分析 +??? pythontutor "コードの可視化" -- **時間計算量は$O(n)$**: すべてのノードが一度ずつ訪問され、$O(n)$の時間がかかります。ここで$n$はノード数です。 -- **空間計算量は$O(n)$**: 最悪の場合、つまり完全二分木の場合、最下位レベルに走査する前に、キューは最大$(n + 1) / 2$個のノードを同時に含むことができ、$O(n)$の空間を占有します。 +
+ -## 7.2.2   前順、中順、後順走査 +### 2.   計算量 -対応して、前順、中順、後順走査はすべて深度優先走査に属し、深度優先探索(DFS)とも呼ばれ、「まず最後まで進み、その後バックトラックして続行する」走査方法を体現しています。 +- **時間計算量は $O(n)$** :すべてのノードを1回ずつ訪問するため、計算量は $O(n)$ です。ここで、$n$ はノード数です。 +- **空間計算量は $O(n)$** :最悪の場合、すなわち完全二分木では、最下層に到達する前に、キュー内には最大で同時に $(n + 1) / 2$ 個のノードが存在し、$O(n)$ の空間を使用します。 -下図は二分木に対して深度優先走査を実行する動作原理を示しています。**深度優先走査は二分木全体を「歩き回る」ようなもので**、各ノードで3つの位置に遭遇し、それらは前順、中順、後順走査に対応しています。 +## 7.2.2   先行順・中間順・後行順走査 -![二分探索木の前順、中順、後順走査](binary_tree_traversal.assets/binary_tree_dfs.png){ class="animation-figure" } +同様に、先行順・中間順・後行順走査はいずれも深度優先走査(depth-first traversal)に属し、深度優先探索(depth-first search, DFS)とも呼ばれます。これは「まず行き止まりまで進み、その後で戻って続ける」という走査方法を表しています。 -

図 7-10   二分探索木の前順、中順、後順走査

+次の図は、二分木に対して深度優先走査を行う仕組みを示しています。**深度優先走査は、二分木全体の外周をぐるりと「一周する」ようなものです**。各ノードでは3つの位置に出会い、それぞれが先行順走査・中間順走査・後行順走査に対応します。 -### 1.   コード実装 +![二分探索木の先行順・中間順・後行順走査](binary_tree_traversal.assets/binary_tree_dfs.png){ class="animation-figure" } -深度優先探索は通常再帰に基づいて実装されます: +

図 7-10   二分探索木の先行順・中間順・後行順走査

+ +### 1.   コードの実装 + +深度優先探索は通常、再帰に基づいて実装されます: === "Python" ```python title="binary_tree_dfs.py" def pre_order(root: TreeNode | None): - """前順走査""" + """先行順走査""" if root is None: return - # 訪問順序: ルートノード -> 左部分木 -> 右部分木 + # 訪問順序:根ノード -> 左部分木 -> 右部分木 res.append(root.val) pre_order(root=root.left) pre_order(root=root.right) @@ -182,7 +374,7 @@ comments: true """中順走査""" if root is None: return - # 訪問順序: 左部分木 -> ルートノード -> 右部分木 + # 訪問優先順: 左部分木 -> 根ノード -> 右部分木 in_order(root=root.left) res.append(root.val) in_order(root=root.right) @@ -191,7 +383,7 @@ comments: true """後順走査""" if root is None: return - # 訪問順序: 左部分木 -> 右部分木 -> ルートノード + # 訪問優先順: 左部分木 -> 右部分木 -> 根ノード post_order(root=root.left) post_order(root=root.right) res.append(root.val) @@ -200,11 +392,11 @@ comments: true === "C++" ```cpp title="binary_tree_dfs.cpp" - /* 前順走査 */ + /* 先行順走査 */ void preOrder(TreeNode *root) { if (root == nullptr) return; - // 訪問優先度:ルートノード -> 左部分木 -> 右部分木 + // 訪問順序:根ノード -> 左部分木 -> 右部分木 vec.push_back(root->val); preOrder(root->left); preOrder(root->right); @@ -214,7 +406,7 @@ comments: true void inOrder(TreeNode *root) { if (root == nullptr) return; - // 訪問優先度:左部分木 -> ルートノード -> 右部分木 + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 inOrder(root->left); vec.push_back(root->val); inOrder(root->right); @@ -224,7 +416,7 @@ comments: true void postOrder(TreeNode *root) { if (root == nullptr) return; - // 訪問優先度:左部分木 -> 右部分木 -> ルートノード + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード postOrder(root->left); postOrder(root->right); vec.push_back(root->val); @@ -234,11 +426,11 @@ comments: true === "Java" ```java title="binary_tree_dfs.java" - /* 前順走査 */ + /* 先行順走査 */ void preOrder(TreeNode root) { if (root == null) return; - // 訪問優先度: 根ノード -> 左部分木 -> 右部分木 + // 訪問順序:根ノード -> 左部分木 -> 右部分木 list.add(root.val); preOrder(root.left); preOrder(root.right); @@ -248,7 +440,7 @@ comments: true void inOrder(TreeNode root) { if (root == null) return; - // 訪問優先度: 左部分木 -> 根ノード -> 右部分木 + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 inOrder(root.left); list.add(root.val); inOrder(root.right); @@ -258,7 +450,7 @@ comments: true void postOrder(TreeNode root) { if (root == null) return; - // 訪問優先度: 左部分木 -> 右部分木 -> 根ノード + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード postOrder(root.left); postOrder(root.right); list.add(root.val); @@ -268,114 +460,381 @@ comments: true === "C#" ```csharp title="binary_tree_dfs.cs" - [class]{binary_tree_dfs}-[func]{PreOrder} + /* 先行順走査 */ + void PreOrder(TreeNode? root) { + if (root == null) return; + // 訪問順序:根ノード -> 左部分木 -> 右部分木 + list.Add(root.val!.Value); + PreOrder(root.left); + PreOrder(root.right); + } - [class]{binary_tree_dfs}-[func]{InOrder} + /* 中順走査 */ + void InOrder(TreeNode? root) { + if (root == null) return; + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 + InOrder(root.left); + list.Add(root.val!.Value); + InOrder(root.right); + } - [class]{binary_tree_dfs}-[func]{PostOrder} + /* 後順走査 */ + void PostOrder(TreeNode? root) { + if (root == null) return; + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード + PostOrder(root.left); + PostOrder(root.right); + list.Add(root.val!.Value); + } ``` === "Go" ```go title="binary_tree_dfs.go" - [class]{}-[func]{preOrder} + /* 先行順走査 */ + func preOrder(node *TreeNode) { + if node == nil { + return + } + // 訪問順序:根ノード -> 左部分木 -> 右部分木 + nums = append(nums, node.Val) + preOrder(node.Left) + preOrder(node.Right) + } - [class]{}-[func]{inOrder} + /* 中順走査 */ + func inOrder(node *TreeNode) { + if node == nil { + return + } + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 + inOrder(node.Left) + nums = append(nums, node.Val) + inOrder(node.Right) + } - [class]{}-[func]{postOrder} + /* 後順走査 */ + func postOrder(node *TreeNode) { + if node == nil { + return + } + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード + postOrder(node.Left) + postOrder(node.Right) + nums = append(nums, node.Val) + } ``` === "Swift" ```swift title="binary_tree_dfs.swift" - [class]{}-[func]{preOrder} + /* 先行順走査 */ + func preOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 訪問順序:根ノード -> 左部分木 -> 右部分木 + list.append(root.val) + preOrder(root: root.left) + preOrder(root: root.right) + } - [class]{}-[func]{inOrder} + /* 中順走査 */ + func inOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 + inOrder(root: root.left) + list.append(root.val) + inOrder(root: root.right) + } - [class]{}-[func]{postOrder} + /* 後順走査 */ + func postOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード + postOrder(root: root.left) + postOrder(root: root.right) + list.append(root.val) + } ``` === "JS" ```javascript title="binary_tree_dfs.js" - [class]{}-[func]{preOrder} + /* 先行順走査 */ + function preOrder(root) { + if (root === null) return; + // 訪問順序:根ノード -> 左部分木 -> 右部分木 + list.push(root.val); + preOrder(root.left); + preOrder(root.right); + } - [class]{}-[func]{inOrder} + /* 中順走査 */ + function inOrder(root) { + if (root === null) return; + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 + inOrder(root.left); + list.push(root.val); + inOrder(root.right); + } - [class]{}-[func]{postOrder} + /* 後順走査 */ + function postOrder(root) { + if (root === null) return; + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード + postOrder(root.left); + postOrder(root.right); + list.push(root.val); + } ``` === "TS" ```typescript title="binary_tree_dfs.ts" - [class]{}-[func]{preOrder} + /* 先行順走査 */ + function preOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 訪問順序:根ノード -> 左部分木 -> 右部分木 + list.push(root.val); + preOrder(root.left); + preOrder(root.right); + } - [class]{}-[func]{inOrder} + /* 中順走査 */ + function inOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 + inOrder(root.left); + list.push(root.val); + inOrder(root.right); + } - [class]{}-[func]{postOrder} + /* 後順走査 */ + function postOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード + postOrder(root.left); + postOrder(root.right); + list.push(root.val); + } ``` === "Dart" ```dart title="binary_tree_dfs.dart" - [class]{}-[func]{preOrder} + /* 先行順走査 */ + void preOrder(TreeNode? node) { + if (node == null) return; + // 訪問順序:根ノード -> 左部分木 -> 右部分木 + list.add(node.val); + preOrder(node.left); + preOrder(node.right); + } - [class]{}-[func]{inOrder} + /* 中順走査 */ + void inOrder(TreeNode? node) { + if (node == null) return; + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 + inOrder(node.left); + list.add(node.val); + inOrder(node.right); + } - [class]{}-[func]{postOrder} + /* 後順走査 */ + void postOrder(TreeNode? node) { + if (node == null) return; + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード + postOrder(node.left); + postOrder(node.right); + list.add(node.val); + } ``` === "Rust" ```rust title="binary_tree_dfs.rs" - [class]{}-[func]{pre_order} + /* 先行順走査 */ + fn pre_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; - [class]{}-[func]{in_order} + fn dfs(root: Option<&Rc>>, res: &mut Vec) { + if let Some(node) = root { + // 訪問順序:根ノード -> 左部分木 -> 右部分木 + let node = node.borrow(); + res.push(node.val); + dfs(node.left.as_ref(), res); + dfs(node.right.as_ref(), res); + } + } + dfs(root, &mut result); - [class]{}-[func]{post_order} + result + } + + /* 中順走査 */ + fn in_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; + + fn dfs(root: Option<&Rc>>, res: &mut Vec) { + if let Some(node) = root { + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 + let node = node.borrow(); + dfs(node.left.as_ref(), res); + res.push(node.val); + dfs(node.right.as_ref(), res); + } + } + dfs(root, &mut result); + + result + } + + /* 後順走査 */ + fn post_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; + + fn dfs(root: Option<&Rc>>, res: &mut Vec) { + if let Some(node) = root { + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード + let node = node.borrow(); + dfs(node.left.as_ref(), res); + dfs(node.right.as_ref(), res); + res.push(node.val); + } + } + + dfs(root, &mut result); + + result + } ``` === "C" ```c title="binary_tree_dfs.c" - [class]{}-[func]{preOrder} + /* 先行順走査 */ + void preOrder(TreeNode *root, int *size) { + if (root == NULL) + return; + // 訪問順序:根ノード -> 左部分木 -> 右部分木 + arr[(*size)++] = root->val; + preOrder(root->left, size); + preOrder(root->right, size); + } - [class]{}-[func]{inOrder} + /* 中順走査 */ + void inOrder(TreeNode *root, int *size) { + if (root == NULL) + return; + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 + inOrder(root->left, size); + arr[(*size)++] = root->val; + inOrder(root->right, size); + } - [class]{}-[func]{postOrder} + /* 後順走査 */ + void postOrder(TreeNode *root, int *size) { + if (root == NULL) + return; + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード + postOrder(root->left, size); + postOrder(root->right, size); + arr[(*size)++] = root->val; + } ``` === "Kotlin" ```kotlin title="binary_tree_dfs.kt" - [class]{}-[func]{preOrder} + /* 先行順走査 */ + fun preOrder(root: TreeNode?) { + if (root == null) return + // 訪問順序:根ノード -> 左部分木 -> 右部分木 + list.add(root._val) + preOrder(root.left) + preOrder(root.right) + } - [class]{}-[func]{inOrder} + /* 中順走査 */ + fun inOrder(root: TreeNode?) { + if (root == null) return + // 訪問優先順: 左部分木 -> 根ノード -> 右部分木 + inOrder(root.left) + list.add(root._val) + inOrder(root.right) + } - [class]{}-[func]{postOrder} + /* 後順走査 */ + fun postOrder(root: TreeNode?) { + if (root == null) return + // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード + postOrder(root.left) + postOrder(root.right) + list.add(root._val) + } ``` === "Ruby" ```ruby title="binary_tree_dfs.rb" - [class]{}-[func]{pre_order} + ### 前順走査 ### + def pre_order(root) + return if root.nil? - [class]{}-[func]{in_order} + # 訪問順序:根ノード -> 左部分木 -> 右部分木 + $res << root.val + pre_order(root.left) + pre_order(root.right) + end - [class]{}-[func]{post_order} + ### 中順走査 ### + def in_order(root) + return if root.nil? + + # 訪問優先順: 左部分木 -> 根ノード -> 右部分木 + in_order(root.left) + $res << root.val + in_order(root.right) + end + + ### 後順走査 ### + def post_order(root) + return if root.nil? + + # 訪問優先順: 左部分木 -> 右部分木 -> 根ノード + post_order(root.left) + post_order(root.right) + $res << root.val + end ``` +??? pythontutor "コードの可視化" + +
+ + !!! tip - 深度優先探索は反復に基づいても実装できます。興味のある読者は自分で学習してください。 + 深度優先探索は反復によって実装することもできます。興味のある読者は自身で調べてみてください。 -下図は二分木の前順走査の再帰プロセスを示しており、これは「再帰」と「復帰」という2つの反対の部分に分けることができます。 +次の図は、二分木の先行順走査における再帰の過程を示しており、「行き」と「帰り」という2つの逆向きの部分に分けられます。 -1. 「再帰」は新しいメソッドを開始することを意味し、プログラムはこのプロセスで次のノードにアクセスします。 -2. 「復帰」は関数が戻ることを意味し、現在のノードが完全にアクセスされたことを示します。 +1. 「行き」は新しいメソッドの開始を表し、この過程でプログラムは次のノードにアクセスします。 +2. 「帰り」は関数の復帰を表し、現在のノードへのアクセスが完了したことを意味します。 === "<1>" - ![前順走査の再帰プロセス](binary_tree_traversal.assets/preorder_step1.png){ class="animation-figure" } + ![先行順走査の再帰過程](binary_tree_traversal.assets/preorder_step1.png){ class="animation-figure" } === "<2>" ![preorder_step2](binary_tree_traversal.assets/preorder_step2.png){ class="animation-figure" } @@ -407,9 +866,9 @@ comments: true === "<11>" ![preorder_step11](binary_tree_traversal.assets/preorder_step11.png){ class="animation-figure" } -

図 7-11   前順走査の再帰プロセス

+

図 7-11   先行順走査の再帰過程

-### 2.   計算量分析 +### 2.   計算量 -- **時間計算量は$O(n)$**: すべてのノードが一度ずつ訪問され、$O(n)$の時間を使用します。 -- **空間計算量は$O(n)$**: 最悪の場合、つまり木が連結リストに退化した場合、再帰の深さは$n$に達し、システムは$O(n)$のスタックフレーム空間を占有します。 +- **時間計算量は $O(n)$** :すべてのノードを1回ずつ訪問するため、計算量は $O(n)$ です。 +- **空間計算量は $O(n)$** :最悪の場合、すなわち木が連結リストに退化したとき、再帰の深さは $n$ に達し、システムは $O(n)$ のスタックフレーム空間を使用します。 diff --git a/ja/docs/chapter_tree/index.md b/ja/docs/chapter_tree/index.md index 4772a7fe3..625017de6 100644 --- a/ja/docs/chapter_tree/index.md +++ b/ja/docs/chapter_tree/index.md @@ -9,9 +9,9 @@ icon: material/graph-outline !!! abstract - そびえ立つ木は活力に満ちた本質を放ち、深い根と豊かな葉を誇りながらも、その枝は疎らに散らばり、幽玄な雰囲気を醸し出しています。 - - それはデータにおける分割統治の鮮やかな形を私たちに示しています。 + 大樹は生命力に満ち、根は深く葉は生い茂り、枝は豊かに広がる。 + + それはデータ分割統治の生き生きとした姿を私たちに示してくれる。 ## 章の内容 @@ -19,5 +19,5 @@ icon: material/graph-outline - [7.2   二分木の走査](binary_tree_traversal.md) - [7.3   二分木の配列表現](array_representation_of_tree.md) - [7.4   二分探索木](binary_search_tree.md) -- [7.5   AVL木 *](avl_tree.md) +- [7.5   AVL 木 *](avl_tree.md) - [7.6   まとめ](summary.md) diff --git a/ja/docs/chapter_tree/summary.md b/ja/docs/chapter_tree/summary.md index 9b2007570..c261ca41d 100644 --- a/ja/docs/chapter_tree/summary.md +++ b/ja/docs/chapter_tree/summary.md @@ -4,55 +4,55 @@ comments: true # 7.6   まとめ -### 1.   重要なポイント +### 1.   要点の振り返り -- 二分木は非線形データ構造で、「一つを二つに分ける」分割統治のロジックを反映しています。各二分木ノードには値と2つのポインタが含まれ、それぞれ左と右の子ノードを指します。 -- 二分木のノードについて、その左(右)子ノードとその下に形成される木は、まとめてそのノードの左(右)部分木と呼ばれます。 -- 二分木に関連する用語には、根ノード、葉ノード、レベル、次数、エッジ、高さ、深さがあります。 -- 二分木の初期化、ノードの挿入、ノードの削除の操作は、連結リストの操作と似ています。 -- 一般的な二分木の種類には、完全二分木、完備二分木、満二分木、平衡二分木があります。完全二分木は理想的な状態を表し、連結リストは退化後の最悪の状態です。 -- 二分木は、ノード値と空きスロットをレベル順走査シーケンスで配置し、親ノードと子ノード間のインデックスマッピング関係に基づいてポインタを実装することで、配列を使用して表現できます。 -- 二分木のレベル順走査は幅優先探索手法で、「円を拡大しながら」の層ごとの走査方式を反映しています。通常はキューを使用して実装されます。 -- 前順、中順、後順走査はすべて深度優先探索手法で、「まず最後まで行き、その後バックトラックして続行する」走査方式を反映しています。通常は再帰を使用して実装されます。 -- 二分探索木は要素検索のための効率的なデータ構造で、検索、挿入、削除操作の時間計算量はすべて$O(\log n)$です。二分探索木が連結リストに退化すると、これらの時間計算量は$O(n)$に悪化します。 -- AVL木は平衡二分探索木とも呼ばれ、回転操作を通して継続的なノード挿入と削除後も木が平衡を保つことを保証します。 -- AVL木の回転操作には、右回転、左回転、右左回転、左右回転があります。ノードの挿入または削除後、AVL木はボトムアップ方式でこれらの回転を実行して自己平衡を取ります。 +- 二分木は非線形データ構造の一種であり、「二分する」分割統治の考え方を体現している。各二分木ノードは 1 つの値と 2 本のポインタを持ち、それぞれ左子ノードと右子ノードを指す。 +- 二分木のあるノードについて、その左(右)子ノードおよびその配下から構成される木を、そのノードの左(右)部分木と呼ぶ。 +- 二分木に関する用語には、根ノード、葉ノード、レベル、次数、辺、高さ、深さなどがある。 +- 二分木の初期化、ノードの挿入、ノードの削除は、連結リストの操作方法と似ている。 +- 一般的な二分木の種類には、perfect 二分木、complete 二分木、full 二分木、平衡二分木がある。perfect 二分木が最も理想的な状態であり、連結リストは退化後の最悪の状態である。 +- 二分木は配列で表現できる。方法としては、ノード値と空き位置をレベル順走査の順に並べ、親ノードと子ノードのインデックス対応関係に基づいてポインタを実現する。 +- 二分木のレベル順走査は幅優先探索の一種であり、「同心円状に外へ広がる」ような逐次的な走査方式を表しており、通常はキューによって実装される。 +- 前順、中順、後順走査はいずれも深さ優先探索に属し、「まず末端まで進み、その後バックトラックして続ける」という走査方式を体現しており、通常は再帰で実装される。 +- 二分探索木は効率的な要素探索データ構造であり、探索、挿入、削除の時間計算量はいずれも $O(\log n)$ である。二分探索木が連結リストへ退化すると、各操作の時間計算量は $O(n)$ まで悪化する。 +- AVL 木は平衡二分探索木とも呼ばれ、回転操作によって、ノードの挿入と削除を繰り返した後も木が平衡を保つようにしている。 +- AVL 木の回転操作には、右回転、左回転、右回転してから左回転、左回転してから右回転がある。ノードの挿入または削除の後、AVL 木は下から上へ回転操作を行い、木を再び平衡状態に戻す。 ### 2.   Q & A -**Q**: 一つのノードのみを持つ二分木について、木の高さと根ノードの深さの両方が$0$ですか? +**Q**:ノードが 1 つしかない二分木では、木の高さと根ノードの深さはどちらも $0$ ですか? -はい、高さと深さは通常「通過したエッジの数」として定義されるためです。 +はい。高さと深さは通常「通過した辺の本数」として定義されるからです。 -**Q**: 二分木における挿入と削除は一般的に一連の操作によって達成されます。ここでの「一連の操作」とは何を指しますか?子ノードのリソースを解放することを意味しますか? +**Q**:二分木における挿入と削除は通常一連の操作を組み合わせて完了しますが、ここでいう「一連の操作」とは何を指すのでしょうか?リソースの子ノードに対するリソース解放と理解できますか? -二分探索木を例に取ると、ノードを削除する操作は3つの異なるシナリオで処理する必要があり、それぞれ複数ステップのノード操作が必要です。 +二分探索木を例にすると、ノード削除は 3 つのケースに分けて処理する必要があり、各ケースで複数段階のノード操作が必要になります。 -**Q**: 二分木のDFS走査で前順、中順、後順の3つのシーケンスがあるのはなぜですか?その用途は何ですか? +**Q**:なぜ DFS による二分木走査には前順・中順・後順の 3 種類があり、それぞれどのような用途があるのですか? -配列の順次および逆順走査と同様に、前順、中順、後順走査は二分木を走査する3つの方法であり、特定の順序で走査結果を取得できます。例えば、二分探索木では、ノードサイズが「左子ノード値 < 根ノード値 < 右子ノード値」を満たすため、「左 $\rightarrow$ 根 $\rightarrow$ 右」の優先順位で木を走査することで、順序付けられたノードシーケンスを取得できます。 +配列の順方向走査と逆方向走査に似て、前順・中順・後順走査は二分木の 3 つの走査方法であり、特定の順序で走査結果を得るために使えます。たとえば二分探索木では、ノードの大小関係が `左子ノードの値 < 根ノードの値 < 右子ノードの値` を満たすため、「左 $\rightarrow$ 根 $\rightarrow$ 右」の優先順で木を走査すれば、整列済みのノード列を得られます。 -**Q**: 不平衡ノード`node`、`child`、`grand_child`間の関係を処理する右回転操作において、右回転後に`node`とその親ノード間の接続と`node`の元のリンクが失われるのではありませんか? +**Q**:右回転操作は不平衡ノード `node`、`child`、`grand_child` の関係を処理するものですが、`node` の親ノードと `node` の元の接続は維持しなくてよいのですか?右回転後に切れてしまいませんか? -この問題を再帰的な観点から見る必要があります。`right_rotate(root)`操作は部分木の根ノードを渡し、最終的に`return child`で回転された部分木の根ノードを返します。部分木の根ノードとその親ノード間の接続は、この関数が戻った後に確立され、これは右回転操作の保守範囲外です。 +この問題は再帰の視点から考える必要があります。右回転操作 `right_rotate(root)` に渡されるのは部分木の根ノードであり、最終的に `return child` によって回転後の部分木の根ノードを返します。部分木の根ノードとその親ノードの接続は、この関数の返却後に行われるため、右回転操作自身が管理する範囲には含まれません。 -**Q**: C++では、関数は`private`と`public`セクションに分かれています。これにはどのような考慮事項がありますか?なぜ`height()`関数と`updateHeight()`関数がそれぞれ`public`と`private`に配置されているのですか? +**Q**:C++ では関数を `private` と `public` に分けますが、この設計にはどのような考えがありますか?なぜ `height()` 関数と `updateHeight()` 関数をそれぞれ `public` と `private` に置くのですか? -これはメソッドの使用範囲によります。メソッドがクラス内でのみ使用される場合、`private`に設計されます。例えば、ユーザーが独自に`updateHeight()`を呼び出すことは意味がありません。これは挿入または削除操作の一ステップに過ぎないからです。しかし、`height()`はノードの高さにアクセスするためのもので、`vector.size()`と同様であるため、使用のために`public`に設定されています。 +主に、そのメソッドの利用範囲を見て決めます。メソッドがクラス内部でしか使われないなら、`private` に設計します。たとえば、利用者が `updateHeight()` を単独で呼び出しても意味はなく、これは挿入や削除の途中の 1 ステップにすぎません。一方で `height()` はノードの高さにアクセスするためのもので、`vector.size()` に似た役割を持つため、使いやすいように `public` に設定します。 -**Q**: 入力データのセットから二分探索木をどのように構築しますか?根ノードの選択は非常に重要ですか? +**Q**:入力データの集合から二分探索木をどのように構築しますか?根ノードの選び方は重要ですか? -はい、木を構築する方法は二分探索木コードの`build_tree()`メソッドで提供されています。根ノードの選択については、通常入力データをソートし、中央の要素を根ノードとして選択し、再帰的に左と右の部分木を構築します。このアプローチは木の平衡を最大化します。 +はい。木の構築方法は、二分探索木のコード中の `build_tree()` メソッドですでに示されています。根ノードの選択については、通常は入力データをソートし、その中央の要素を根ノードにしてから、左右の部分木を再帰的に構築します。こうすることで、木の平衡性を最大限に保てます。 -**Q**: Javaでは、文字列比較に常に`equals()`メソッドを使用する必要がありますか? +**Q**:Java では、文字列比較には必ず `equals()` メソッドを使うべきですか? -Javaでは、プリミティブデータ型の場合、`==`は2つの変数の値が等しいかどうかを比較するために使用されます。参照型の場合、2つのシンボルの動作原理は異なります。 +Java では、基本データ型については `==` を使って 2 つの変数の値が等しいかどうかを比較します。参照型については、この 2 つの記法の働き方は異なります。 -- `==`: 2つの変数が同じオブジェクトを指しているかどうか、つまりメモリ内の位置が同じかどうかを比較するために使用されます。 -- `equals()`: 2つのオブジェクトの値が等しいかどうかを比較するために使用されます。 +- `==` :2 つの変数が同じオブジェクトを指しているか、つまりメモリ上の位置が同じかどうかを比較するために使います。 +- `equals()`:2 つのオブジェクトの値が等しいかどうかを比較するために使います。 -したがって、値を比較するには`equals()`を使用すべきです。ただし、`String a = "hi"; String b = "hi";`で初期化された文字列は文字列定数プールに格納され、同じオブジェクトを指すため、`a == b`も2つの文字列の内容を比較するために使用できます。 +したがって、値を比較したい場合は `equals()` を使うべきです。ただし、`String a = "hi"; String b = "hi";` によって初期化された文字列は文字列定数プールに格納され、同じオブジェクトを指すため、`a == b` でも 2 つの文字列の内容を比較できます。 -**Q**: 最下位レベルに到達する前に、幅優先走査でキュー内のノード数は$2^h$ですか? +**Q**:幅優先走査で最下層に到達する前、キュー内のノード数は $2^h$ ですか? -はい、例えば高さ$h = 2$の満二分木は合計$n = 7$個のノードを持ち、最下位レベルには$4 = 2^h = (n + 1) / 2$個のノードがあります。 +はい。たとえば高さ $h = 2$ の充足二分木では、ノード総数は $n = 7$ であり、最下層のノード数は $4 = 2^h = (n + 1) / 2$ です。 diff --git a/ja/docs/index.assets/animation.mp4 b/ja/docs/index.assets/animation.mp4 new file mode 100644 index 000000000..cf3838e79 Binary files /dev/null and b/ja/docs/index.assets/animation.mp4 differ diff --git a/ja/docs/index.assets/comment.mp4 b/ja/docs/index.assets/comment.mp4 new file mode 100644 index 000000000..3f9ea340c Binary files /dev/null and b/ja/docs/index.assets/comment.mp4 differ diff --git a/ja/docs/index.assets/running_code.mp4 b/ja/docs/index.assets/running_code.mp4 new file mode 100644 index 000000000..c946fbd68 Binary files /dev/null and b/ja/docs/index.assets/running_code.mp4 differ diff --git a/ja/docs/index.html b/ja/docs/index.html index 66f476de9..da2524efc 100644 --- a/ja/docs/index.html +++ b/ja/docs/index.html @@ -71,7 +71,7 @@

- アニメーション解説とワンクリック実行ができるデータ構造とアルゴリズムのチュートリアル + アニメーション図解とワンクリック実行で学ぶデータ構造とアルゴリズムのチュートリアル

@@ -84,7 +84,7 @@ - コードリポジトリ + GitHub
@@ -96,32 +96,8 @@
- -
-
- Preview -
- - - - - - - - - - - - - - -
-

500 枚のアニメーション図解、14 類のプログラミング言語のコード、3000 件のコミュニティQ&Aで,データ構造とアルゴリズムをすばやく習得しよう

-
-
- -
+
+

500枚のアニメーション図解、14種類のプログラミング言語コード、3000件のコミュニティQ&Aで、データ構造とアルゴリズムの学習をすばやく始めよう

-
+

推薦文

-

「わかりやすいデータ構造とアルゴリズムの入門書で、読者が手と頭を使って学べるよう導いてくれます。アルゴリズム初心者に強くおすすめします。」

-

—— 邓俊辉,清華大学コンピュータ系教授

+

「わかりやすいデータ構造とアルゴリズムの入門書で、読者を手と頭の両方を使う学びへと導いてくれます。アルゴリズムを初めて学ぶ人に強くおすすめします。」

+

—— Junhui Deng、清華大学コンピュータ科学科教授

-

「もし私が当時データ構造とアルゴリズムを学んでいたときに『Hello アルゴリズム』があったなら、学習は10倍も簡単だったはずだ!」

-

—— 李沐,アマゾンのシニアプリンシパルサイエンティスト

+

「もし私がデータ構造とアルゴリズムを学んでいた当時に『Hello アルゴリズム』があったなら、学ぶのは10倍は簡単だったはずです!」

+

—— Mu Li、Amazon シニアプリンシパルサイエンティスト

-
+
@@ -198,15 +163,15 @@

アニメーション図解

-

内容は明快で理解しやすく、学習曲線は緩やか

+

内容は明快で理解しやすく、学習曲線もなだらかです

"A picture is worth a thousand words."
「一枚の絵は千の言葉に値する」

- Animation example +
- Running code example +
@@ -215,7 +180,7 @@

ワンクリック実行

-

10種類以上のプログラミング言語で、コードを可視化して実行

+

10種類以上のプログラミング言語に対応し、コードを可視化して実行できます

"Talk is cheap. Show me the code."
「口で言うよりコードを見せよ」

@@ -230,16 +195,32 @@

相互学習

-

議論や質問を歓迎し、読者同士でともに前進

+

議論や質問を歓迎し、読者同士が支え合いながら学べます

"Learning by teaching."
「教えることで学ぶ」

- Comments example +
+ +
+ +
+
@@ -256,9 +237,24 @@
- +
-

コードレビュー担当者

+

翻訳者

+

本書の日本語版は、以下の翻訳者によってレビューされました。ご貢献に感謝します!

+ +
+ + +
+

コードレビュアー

+

本書の多言語コード版は、以下の開発者の協力によって実現しました。ご尽力に感謝します!

- - -

貢献者

-

本書はオープンソースコミュニティの200名を超える貢献者の皆さんの共同の努力によって継続的に改善されています。時間と労力に感謝します!

+

本書は、オープンソースコミュニティの200名を超える貢献者の共同作業によって継続的に改善されています。皆さんの時間と労力に感謝します!

Contributors
-
\ No newline at end of file +
diff --git a/ja/docs/index.md b/ja/docs/index.md index bcabc4880..818a368dd 100644 --- a/ja/docs/index.md +++ b/ja/docs/index.md @@ -1,6 +1,5 @@ --- comments: false -glightbox: false search: exclude: true hide: diff --git a/overrides/javascripts/animation_player.js b/overrides/javascripts/animation_player.js new file mode 100644 index 000000000..9bb22fbfb --- /dev/null +++ b/overrides/javascripts/animation_player.js @@ -0,0 +1,253 @@ +(() => { + const ANIMATION_LABEL_PATTERN = /^<\d+>$/; + const AUTO_SLIDE_INITIAL_DELAY_MS = 1000; + const AUTO_SLIDE_INTERVAL_MS = 1500; + const PLAY_LABEL = "播放幻灯片"; + const PAUSE_LABEL = "暂停幻灯片"; + const SVG_NS = "http://www.w3.org/2000/svg"; + const FA_PLAY_PATH = + "M91.2 36.9c-12.4-6.8-27.4-6.5-39.6.7S32 57.9 32 72v368c0 14.1 7.5 27.2 19.6 34.4s27.2 7.5 39.6.7l336-184c12.8-7 20.8-20.5 20.8-35.1s-8-28.1-20.8-35.1z"; + const FA_PAUSE_PATH = + "M48 32C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h64c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm224 0c-26.5 0-48 21.5-48 48v352c0 26.5 21.5 48 48 48h64c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z"; + const FA_ARROW_LEFT_PATH = + "M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 288H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H109.3l105.4-105.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"; + const FA_ARROW_RIGHT_PATH = + "M502.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L402.7 224H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h370.7L297.3 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"; + const initializedSets = new WeakSet(); + const controllers = new Set(); + + const getCheckedIndex = (inputs) => { + const checkedIndex = inputs.findIndex((input) => input.checked); + return checkedIndex === -1 ? 0 : checkedIndex; + }; + + const setCheckedIndex = (inputs, index) => { + const normalizedIndex = ((index % inputs.length) + inputs.length) % inputs.length; + inputs[normalizedIndex].checked = true; + return normalizedIndex; + }; + + const isAnimationSet = (tabbedSet) => { + const labels = Array.from(tabbedSet.querySelectorAll(":scope > .tabbed-labels > label")); + if (labels.length < 2) { + return false; + } + return labels.every((label) => ANIMATION_LABEL_PATTERN.test(label.textContent.trim())); + }; + + const createSvgIcon = ({ className, path, viewBox, width, height }) => { + const icon = document.createElementNS(SVG_NS, "svg"); + const iconPath = document.createElementNS(SVG_NS, "path"); + + icon.setAttribute("class", className); + icon.setAttribute("aria-hidden", "true"); + icon.setAttribute("viewBox", viewBox); + icon.setAttribute("focusable", "false"); + if (width) { + icon.setAttribute("width", width); + } + if (height) { + icon.setAttribute("height", height); + } + iconPath.setAttribute("d", path); + icon.append(iconPath); + return { icon, iconPath }; + }; + + const createIconButton = ({ className, ariaLabel, path }) => { + const button = document.createElement("button"); + const { icon } = createSvgIcon({ + className: "animation-controls__nav-icon", + path, + viewBox: "0 0 512 512", + }); + + button.type = "button"; + button.className = `animation-controls__button ${className}`; + button.setAttribute("aria-label", ariaLabel); + button.append(icon); + return button; + }; + + const createPlayButton = () => { + const button = document.createElement("button"); + const glyph = document.createElement("span"); + const { icon, iconPath } = createSvgIcon({ + className: "animation-controls__play-icon", + path: FA_PLAY_PATH, + viewBox: "0 0 448 512", + width: "12", + height: "12", + }); + const srOnly = document.createElement("span"); + + button.type = "button"; + button.className = "animation-controls__button animation-controls__play"; + button.setAttribute("aria-label", PLAY_LABEL); + + glyph.className = "animation-controls__play-glyph"; + + icon.setAttribute("preserveAspectRatio", "xMidYMid meet"); + glyph.append(icon); + + srOnly.className = "animation-controls__sr-only"; + srOnly.textContent = PLAY_LABEL; + + button.append(glyph, srOnly); + return { button, srOnly, icon, iconPath }; + }; + + const initAnimationSet = (tabbedSet) => { + if (initializedSets.has(tabbedSet) || !isAnimationSet(tabbedSet)) { + return; + } + + const inputs = Array.from(tabbedSet.querySelectorAll(':scope > input[type="radio"]')); + const tabbedContent = tabbedSet.querySelector(":scope > .tabbed-content"); + if (inputs.length < 2 || !tabbedContent) { + return; + } + + initializedSets.add(tabbedSet); + tabbedSet.dataset.autoSlide = "true"; + + const controls = document.createElement("div"); + controls.className = "animation-controls"; + + const playControl = createPlayButton(); + const playButton = playControl.button; + const nav = document.createElement("div"); + nav.className = "animation-controls__nav"; + + const prevButton = createIconButton({ + className: "animation-controls__prev", + ariaLabel: "上一页", + path: FA_ARROW_LEFT_PATH, + }); + const pageIndicator = document.createElement("span"); + pageIndicator.className = "animation-controls__page"; + pageIndicator.setAttribute("aria-live", "polite"); + + const nextButton = createIconButton({ + className: "animation-controls__next", + ariaLabel: "下一页", + path: FA_ARROW_RIGHT_PATH, + }); + + nav.append(prevButton, pageIndicator, nextButton); + controls.append(playButton, nav); + tabbedContent.insertAdjacentElement("afterend", controls); + + const state = { + inputs, + currentIndex: getCheckedIndex(inputs), + intervalId: null, + timeoutId: null, + isPlaying: false, + }; + + const updatePlayButton = () => { + const label = state.isPlaying ? PAUSE_LABEL : PLAY_LABEL; + playButton.setAttribute("aria-label", label); + playButton.classList.toggle("is-playing", state.isPlaying); + playControl.srOnly.textContent = label; + playControl.icon.setAttribute("viewBox", state.isPlaying ? "0 0 384 512" : "0 0 448 512"); + playControl.icon.setAttribute("width", state.isPlaying ? "10" : "12"); + playControl.iconPath.setAttribute("d", state.isPlaying ? FA_PAUSE_PATH : FA_PLAY_PATH); + }; + + const updatePageIndicator = () => { + pageIndicator.textContent = `${state.currentIndex + 1} / ${inputs.length}`; + }; + + const stop = () => { + if (state.timeoutId !== null) { + window.clearTimeout(state.timeoutId); + state.timeoutId = null; + } + if (state.intervalId !== null) { + window.clearInterval(state.intervalId); + state.intervalId = null; + } + state.isPlaying = false; + updatePlayButton(); + }; + + const syncCurrentIndex = () => { + state.currentIndex = getCheckedIndex(inputs); + }; + + const moveTo = (index) => { + state.currentIndex = setCheckedIndex(inputs, index); + updatePageIndicator(); + }; + + const step = (delta) => { + moveTo(state.currentIndex + delta); + }; + + const start = () => { + if (state.isPlaying) { + return; + } + state.isPlaying = true; + updatePlayButton(); + state.timeoutId = window.setTimeout(() => { + step(1); + state.timeoutId = null; + state.intervalId = window.setInterval(() => { + step(1); + }, AUTO_SLIDE_INTERVAL_MS); + }, AUTO_SLIDE_INITIAL_DELAY_MS); + }; + + playButton.addEventListener("click", () => { + if (state.isPlaying) { + stop(); + return; + } + syncCurrentIndex(); + start(); + }); + + prevButton.addEventListener("click", () => { + syncCurrentIndex(); + step(-1); + }); + + nextButton.addEventListener("click", () => { + syncCurrentIndex(); + step(1); + }); + + inputs.forEach((input, index) => { + input.addEventListener("change", () => { + if (input.checked) { + state.currentIndex = index; + updatePageIndicator(); + } + }); + }); + + controllers.add(stop); + updatePlayButton(); + updatePageIndicator(); + }; + + const initAutoSlide = () => { + document.querySelectorAll(".tabbed-set").forEach(initAnimationSet); + }; + + document.addEventListener("visibilitychange", () => { + if (!document.hidden) { + return; + } + controllers.forEach((stop) => stop()); + }); + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initAutoSlide, { once: true }); + } else { + initAutoSlide(); + } +})(); diff --git a/overrides/main.html b/overrides/main.html index d67c25706..0a4550c8a 100644 --- a/overrides/main.html +++ b/overrides/main.html @@ -8,7 +8,7 @@ {% elif config.theme.language == 'en' %} {% set announcements = 'Welcome to contribute to Chinese-to-English translation! For more details, please refer to CONTRIBUTING.md.' %} {% elif config.theme.language == 'ja' %} - {% set announcements = '日本語版審閱者を募集しています!詳細は CONTRIBUTING.md を参照してください。' %} + {% set announcements = '日本語版のレビュアーを募集しています。詳しくは こちら をご覧ください。' %} {% elif config.theme.language == 'ru' %} {% set announcements = 'Приглашаем вас участвовать в развитии русской версии! Подробнее здесь.' %} {% endif %} diff --git a/overrides/partials/comments.html b/overrides/partials/comments.html index 4a2f570c2..e56e60213 100644 --- a/overrides/partials/comments.html +++ b/overrides/partials/comments.html @@ -43,28 +43,35 @@ diff --git a/overrides/stylesheets/animation_player.css b/overrides/stylesheets/animation_player.css new file mode 100644 index 000000000..e311a3f50 --- /dev/null +++ b/overrides/stylesheets/animation_player.css @@ -0,0 +1,178 @@ +.md-typeset .tabbed-set[data-auto-slide="true"] { + margin-bottom: 1.5em; + overflow: hidden; + border-radius: 0.45rem; + background: var(--md-default-bg-color); + box-shadow: 0 0.12rem 0.45rem var(--md-default-fg-color--lightest); +} + +.md-typeset .tabbed-set[data-auto-slide="true"] > .tabbed-labels { + display: none; +} + +.md-typeset .tabbed-set[data-auto-slide="true"] > .tabbed-content { + margin-top: 0; + margin-bottom: 0; + background: var(--md-default-bg-color); +} + +.md-typeset .tabbed-set[data-auto-slide="true"] > .tabbed-content > .tabbed-block > p { + margin: 0; +} + +.md-typeset .tabbed-set[data-auto-slide="true"] .animation-figure { + border-radius: 0; + box-shadow: none; +} + +.animation-controls { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.55rem; + width: 100%; + margin: 0 auto; + padding: 0.14rem 0.3rem; + border-radius: 0; + background: color-mix(in srgb, var(--md-code-bg-color) 97.5%, var(--md-default-fg-color) 2.5%); + box-sizing: border-box; +} + +.animation-controls__nav { + display: flex; + align-items: center; + gap: 0.03rem; + margin-left: auto; + padding-left: 0.36rem; +} + +.animation-controls__button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 1.64rem; + min-width: 1.64rem; + padding: 0.1rem; + border: 0; + background: transparent; + border-radius: 999px; + color: var(--md-default-fg-color); + cursor: pointer; + transition: + color 0.15s ease, + background-color 0.15s ease; +} + +.animation-controls__button:hover, +.animation-controls__button:focus-visible { + background: color-mix(in srgb, var(--md-code-bg-color) 92%, var(--md-default-fg-color) 8%); + color: var(--md-accent-fg-color); +} + +.animation-controls__play { + flex: 0 0 auto; + line-height: 0; +} + +.animation-controls__play-glyph { + display: inline-flex; + align-items: center; + justify-content: center; + width: 0.72rem; + height: 0.72rem; + flex: 0 0 auto; + overflow: hidden; +} + +.animation-controls__play-icon { + display: block; + width: 100%; + height: 100%; + fill: currentColor; + flex: 0 0 auto; +} + +.animation-controls__play-glyph .animation-controls__play-icon { + transform: translateX(0.04rem); +} + +.animation-controls__play.is-playing .animation-controls__play-glyph { + width: 0.6rem; +} + +.animation-controls__play.is-playing .animation-controls__play-glyph .animation-controls__play-icon { + transform: none; +} + +.animation-controls__nav-icon { + width: 0.72rem; + height: 0.72rem; + fill: currentColor; + flex: 0 0 auto; +} + +.animation-controls__page { + min-width: 2.25rem; + padding: 0 0.01rem; + color: var(--md-typeset-color); + font-size: 0.75rem; + font-variant-numeric: tabular-nums; + line-height: 1; + text-align: center; +} + +.animation-controls__sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + +[data-md-color-scheme="slate"] .animation-controls { + background: color-mix(in srgb, var(--md-code-bg-color) 91%, black 9%); +} + +[data-md-color-scheme="slate"] .animation-controls__button:hover, +[data-md-color-scheme="slate"] .animation-controls__button:focus-visible { + background: color-mix(in srgb, var(--md-code-bg-color) 92%, white 8%); +} + +[data-md-color-scheme="slate"] .md-typeset .tabbed-set[data-auto-slide="true"] { + background: var(--md-code-bg-color); + box-shadow: 0 0.2rem 0.75rem rgb(0 0 0 / 0.18); +} + +[data-md-color-scheme="slate"] .md-typeset .tabbed-set[data-auto-slide="true"] > .tabbed-content { + background: var(--md-code-bg-color); +} + +@media screen and (max-width: 44.9375em) { + .animation-controls { + flex-wrap: nowrap; + justify-content: space-between; + gap: 0.32rem; + padding: 0.11rem 0.22rem; + } + + .animation-controls__nav { + gap: 0.02rem; + padding-left: 0.16rem; + } + + .animation-controls__button { + min-width: 1.48rem; + min-height: 1.48rem; + padding: 0.08rem; + } + + .animation-controls__page { + min-width: 2rem; + padding: 0; + font-size: 0.7rem; + } +} diff --git a/overrides/stylesheets/extra.css b/overrides/stylesheets/extra.css index cb2027bf0..4cc92b465 100644 --- a/overrides/stylesheets/extra.css +++ b/overrides/stylesheets/extra.css @@ -1,4 +1,4 @@ -/* Color Settings */ +/* Theme tokens */ /* https://github.com/squidfunk/mkdocs-material/blob/6b5035f5580f97532d664e3d1babf5f320e88ee9/src/assets/stylesheets/main/_colors.scss */ /* https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#custom-colors */ :root>* { @@ -14,23 +14,23 @@ --md-code-fg-color: #1d1d20; --md-code-bg-color: #f5f5f5; - --md-accent-fg-color: #999; - - --md-admonition-fg-color: #1d1d20; - --md-typeset-color: #1d1d20; --md-typeset-a-color: #349890; + --md-accent-fg-color: var(--md-typeset-a-color); + --md-typeset-btn-color: #55aea6; --md-typeset-btn-hover-color: #52bbb1; --md-admonition-icon--pythontutor: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-pythontutor-color: #eee; + --md-admonition-pythontutor-color: var(--md-code-bg-color); + + --hello-algo-sidebar-width: 13rem; } [data-md-color-scheme="slate"] { - --theme-dark-base: #1E1E1E; - --theme-dark-mantle: #1A1A1A; + --theme-dark-base: #1e1e1e; + --theme-dark-mantle: #1a1a1a; --theme-dark-crust: #171717; --hero-starfield-bg-color: var(--theme-dark-base); @@ -39,27 +39,25 @@ --md-default-fg-color: #adbac7; --md-default-bg-color: var(--theme-dark-base); + --md-default-bg-color--light: rgb(30 30 30 / 0.8); - --md-body-bg-color: var(--theme-dark-mantle); + --md-body-bg-color: var(--md-default-bg-color); --md-header-bg-color: rgba(26, 26, 26, 0.8); --md-code-fg-color: #adbac7; --md-code-bg-color: var(--theme-dark-crust); - --md-accent-fg-color: #aaa; - - --md-admonition-fg-color: #adbac7; - - --md-footer-fg-color: #adbac7; - --md-footer-bg-color: var(--theme-dark-mantle); - --md-typeset-color: #adbac7; --md-typeset-a-color: #52bbb1; + --md-accent-fg-color: var(--md-typeset-a-color); --md-typeset-btn-color: #52bbb1; --md-typeset-btn-hover-color: #55aea6; - --md-admonition-pythontutor-color: var(--theme-dark-crust); + --md-footer-fg-color: #adbac7; + --md-footer-bg-color: var(--theme-dark-mantle); + + --md-admonition-pythontutor-color: var(--md-code-bg-color); } [data-md-color-scheme="slate"][data-md-color-primary="black"], @@ -67,39 +65,82 @@ --md-typeset-a-color: #52bbb1; } -[data-md-color-primary="black"] .md-header { - background-color: var(--md-header-bg-color); +/* Base layout */ +body { + background-color: var(--md-default-bg-color); + --md-text-font-family: -apple-system, BlinkMacSystemFont, + var(--md-text-font, _), Helvetica, Arial, sans-serif; + --md-code-font-family: var(--md-code-font, _), SFMono-Regular, Consolas, Menlo, + -apple-system, BlinkMacSystemFont, var(--md-text-font, _), monospace; } -.md-header--shadow, -.md-header--landing { - box-shadow: none; - transition: none; - backdrop-filter: saturate(180%) blur(20px); - /* Gaussian blur */ - -webkit-backdrop-filter: saturate(180%) blur(20px); - /* Safari */ - background-color: var(--md-header-bg-color); +html:has(body[data-md-color-scheme="slate"]) { + background-color: #1e1e1e; +} + +html:has(body[data-md-color-scheme="default"]) { + background-color: #ffffff; +} + +@media screen and (min-width: 76.25em) { + .md-grid { + max-width: calc(61rem + 2 * (var(--hello-algo-sidebar-width) - 12.1rem)); + } + + .md-sidebar--primary, + .md-sidebar--secondary { + width: var(--hello-algo-sidebar-width); + } + + [dir="ltr"] .md-sidebar__inner { + padding-right: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); + } + + [dir="rtl"] .md-sidebar__inner { + padding-left: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); + } +} + +.md-sidebar--primary .md-sidebar__scrollwrap { + scrollbar-color: var(--md-default-fg-color--lighter) transparent; +} + +.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb, +.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: var(--md-default-fg-color--lighter); +} + +/* Banner and footer */ +.md-banner { + background-color: var(--md-default-bg-color); + color: var(--md-default-fg-color); + font-size: 0.75rem; +} + +.md-banner .banner-svg svg { + margin-right: 0.3rem; + height: 0.63rem; + fill: var(--md-default-fg-color); +} + +.md-footer, +.md-footer__inner, +.md-footer-meta, +.md-footer__link, +.md-footer__link:hover { + background-color: var(--md-default-bg-color); +} + +.md-footer { + border-top: 0.05rem solid var(--md-default-fg-color--lightest); } [data-md-color-scheme="slate"] .md-footer, -[data-md-color-scheme="slate"] .md-footer__inner { - background-color: var(--theme-dark-mantle); - color: var(--md-footer-fg-color); -} - -[data-md-color-scheme="slate"] .md-footer-meta { - background-color: var(--theme-dark-crust); - color: var(--md-footer-fg-color); -} - -[data-md-color-scheme="slate"] .md-footer__link { - background-color: var(--theme-dark-crust); - color: var(--md-footer-fg-color); -} - +[data-md-color-scheme="slate"] .md-footer__inner, +[data-md-color-scheme="slate"] .md-footer-meta, +[data-md-color-scheme="slate"] .md-footer__link, [data-md-color-scheme="slate"] .md-footer__link:hover { - background-color: var(--theme-dark-base); + color: var(--md-footer-fg-color); } [data-md-color-scheme="slate"] .md-footer__title, @@ -112,40 +153,31 @@ color: var(--md-footer-fg-color); } -/* https://github.com/squidfunk/mkdocs-material/issues/4832#issuecomment-1374891676 */ -.md-nav__link[for] { - color: var(--md-default-fg-color) !important; -} - -/* Figure class */ +/* Shared content elements */ .animation-figure { border-radius: 0.3rem; display: block; margin: 0 auto; - box-shadow: var(--md-shadow-z2); + box-shadow: 0 0.03rem 0.16rem rgb(0 0 0 / 0.07); } -/* Cover image class */ .cover-image { width: 28rem; height: auto; border-radius: 0.3rem; display: block; margin: 0 auto; - box-shadow: var(--md-shadow-z2); + box-shadow: 0 0.03rem 0.16rem rgb(0 0 0 / 0.07); } -/* Center Markdown Tables (requires md_in_html extension) */ .center-table { text-align: center; } -/* Reset alignment for table cells */ .md-typeset .center-table :is(td, th):not([align]) { text-align: initial; } -/* Font size */ .md-typeset { font-size: 0.75rem; line-height: 1.5; @@ -155,7 +187,6 @@ font-size: 0.95em; } -/* Markdown Header */ /* https://github.com/squidfunk/mkdocs-material/blob/dcab57dd1cced4b77875c1aa1b53467c62709d31/src/assets/stylesheets/main/_typeset.scss */ .md-typeset h1 { font-weight: 400; @@ -174,11 +205,6 @@ text-transform: none; } -.md-typeset a:hover { - color: var(--md-typeset-a-color); - text-decoration: underline; -} - .md-typeset code { border-radius: 0.2rem; } @@ -187,48 +213,61 @@ font-weight: normal; } -/* font-family setting for Win10 */ -body { - --md-text-font-family: -apple-system, BlinkMacSystemFont, - var(--md-text-font, _), Helvetica, Arial, sans-serif; - --md-code-font-family: var(--md-code-font, _), SFMono-Regular, Consolas, Menlo, - -apple-system, BlinkMacSystemFont, var(--md-text-font, _), monospace; -} - -/* max height of code block */ /* https://github.com/squidfunk/mkdocs-material/issues/3444 */ .md-typeset pre>code { max-height: 25rem; } -/* Make the picture not glare in dark theme */ +.md-typeset pre>code:hover { + scrollbar-color: var(--md-default-fg-color--lighter) transparent; +} + +.md-typeset pre>code::-webkit-scrollbar-thumb:hover { + background-color: var(--md-default-fg-color--lighter); +} + [data-md-color-scheme="slate"] .md-typeset img, [data-md-color-scheme="slate"] .md-typeset svg, [data-md-color-scheme="slate"] .md-typeset video { filter: brightness(0.85) invert(0.05); } -/* landing page */ -.header-img-div { - display: flex; - align-items: center; - justify-content: center; - margin: 0 auto; - width: 100%; - /* Default to full width */ +.md-typeset a:not(.md-button) { + text-decoration: none; +} + +.md-typeset a:not(.md-button):hover, +.md-typeset a:not(.md-button):focus-visible { + color: var(--md-typeset-a-color); + text-decoration: underline; +} + +/* Admonitions and tabs */ +.md-typeset .admonition-title:before, +.md-typeset summary:before, +.md-typeset summary:after { + top: 50%; +} + +.md-typeset .admonition-title:before, +.md-typeset summary:before { + transform: translateY(-50%); +} + +.md-typeset summary:after { + transform: translateY(-50%) rotate(0deg); +} + +.md-typeset details[open]>summary:after { + transform: translateY(-50%) rotate(90deg); } -/* Admonition for python tutor */ .md-typeset .admonition.pythontutor, .md-typeset details.pythontutor { border-color: var(--md-default-fg-color--lightest); margin-top: 0; margin-bottom: 1.5625em; -} - -.md-typeset .admonition:focus-within, -.md-typeset details:focus-within { - box-shadow: var(--md-shadow-z1); + background-color: var(--md-code-bg-color); } .md-typeset .pythontutor>.admonition-title, @@ -243,31 +282,18 @@ body { mask-image: var(--md-admonition-icon--pythontutor); } -.md-typeset .admonition-title:before, -.md-typeset summary:before { - width: 1.25em; +[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary), +[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary) :is(p, li, strong, em, sub, sup, code, a) { + background-color: #f5f5f5; + color: #1d1d20; } -/* code block tabs */ .md-typeset .tabbed-labels>label { font-size: 0.61rem; } .md-typeset .tabbed-labels--linked>label>a { - padding: .78125em 1.0em .625em; -} - -/* header banner */ -.md-banner { - background-color: var(--md-code-bg-color); - color: var(--md-default-fg-color); - font-size: 0.75rem; -} - -.md-banner .banner-svg svg { - margin-right: 0.3rem; - height: 0.63rem; - fill: var(--md-default-fg-color); + padding: 0.78125em 1em 0.625em; } .pythontutor-iframe { @@ -280,129 +306,80 @@ body { border: none; } -/* landing page container */ +/* Landing page layout */ +.header-img-div { + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; + width: 100%; +} + .home-div { width: 100%; height: auto; display: flex; justify-content: center; align-items: center; + padding: 3em 2em; background-color: var(--md-default-bg-color); color: var(--md-default-fg-color); font-size: 0.9rem; - padding: 3em 2em; text-align: center; } +.home-div--black { + background-color: #101010; +} + +.home-div[data-md-color-scheme="default"], +.home-div[data-md-color-scheme="default"] h1, +.home-div[data-md-color-scheme="default"] h2, +.home-div[data-md-color-scheme="default"] h3, +.home-div[data-md-color-scheme="default"] h4, +.home-div[data-md-color-scheme="default"] h5, +.home-div[data-md-color-scheme="default"] h6, +.home-div[data-md-color-scheme="slate"], +.home-div[data-md-color-scheme="slate"] h1, +.home-div[data-md-color-scheme="slate"] h2, +.home-div[data-md-color-scheme="slate"] h3, +.home-div[data-md-color-scheme="slate"] h4, +.home-div[data-md-color-scheme="slate"] h5, +.home-div[data-md-color-scheme="slate"] h6 { + color: var(--md-default-fg-color); +} + .section-content { width: 100%; height: auto; max-width: 70vw; } -/* rounded button */ -.rounded-button { - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 10em; - margin: 0 0.1em; - padding: 0.6em 1.3em; - border: none; - background-color: var(--md-typeset-btn-color); - color: var(--md-primary-fg-color) !important; - text-align: center; - text-decoration: none; - cursor: pointer; -} - -.rounded-button:hover { - background-color: var(--md-typeset-btn-hover-color); -} - -.rounded-button span { - margin: 0; - margin-bottom: 0.07em; - white-space: nowrap; -} - -.rounded-button svg { - fill: var(--md-primary-fg-color); - width: auto; - height: 1.2em; - margin-right: 0.5em; -} - -/* device image */ -.device-on-hover { - width: auto; - transition: transform 0.3s ease-in-out, filter 0.3s ease-in-out; -} - -a:hover .device-on-hover { - filter: drop-shadow(0 0 0.2rem rgba(0, 0, 0, 0.15)); - transform: scale(1.01); -} - -/* text button */ -.reading-media { - display: flex; - justify-content: center; - align-items: flex-end; - height: 32vw; -} - -.media-block { - height: 100%; - margin: 0 0.2em; -} - -.text-button { - width: auto; - color: var(--md-typeset-btn-color); - text-decoration: none; - text-align: center; - margin: 2.7em auto; -} - -.text-button span { - white-space: nowrap; -} - -.text-button svg { - display: inline-block; - fill: var(--md-typeset-btn-color); - width: auto; - height: 0.9em; - background-size: cover; - padding-top: 0.17em; - margin-left: 0.15em; -} - -a:hover .text-button span { - text-decoration: underline; -} - -/* hero image */ .hero-div { height: min(84vh, 75vw); width: min(112vh, 100vw); - margin: 0 auto; - margin-top: -2.4rem; + margin: -2.4rem auto 0; padding: 0; position: relative; font-size: min(1.8vh, 2.5vw); font-weight: normal; } +.hero-div--en { + font-size: min(1.65vh, 2.3vw); +} + +.hero-div--ru { + font-size: min(1.45vh, 2vw); +} + .hero-bg { - height: 100%; width: 100%; + height: 100%; object-fit: cover; position: absolute; } -/* hover on the planets */ .hero-div>a>img { width: auto; position: absolute; @@ -414,7 +391,6 @@ a:hover .text-button span { position: absolute; transform: translateX(-50%) translateY(-50%); white-space: nowrap; - /* prevent line breaks */ color: white; } @@ -424,21 +400,136 @@ a:hover .text-button span { } .hero-div>a:hover>span { - text-decoration: underline; color: var(--md-typeset-btn-color); + text-decoration: underline; } .heading-div { width: 100%; position: absolute; - transform: translateX(-50%); left: 50%; bottom: min(2vh, 3vw); + transform: translateX(-50%); pointer-events: none; color: #fff; } -/* code badge */ +/* Landing page CTAs */ +.rounded-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.38em; + margin: 0 0.1em; + padding: 0.72em 1.18em; + border: 1px solid rgb(255 255 255 / 0.24); + border-radius: 10em; + background-color: rgb(24 24 24 / 0.2); + color: rgb(232 241 240) !important; + font-weight: 400; + letter-spacing: 0.01em; + line-height: 1.2; + min-width: 7.2em; + text-align: center; + text-decoration: none; + cursor: pointer; + box-shadow: + 0 0.3rem 1rem rgb(0 0 0 / 0.16); + transition: + color 0.15s ease-out, + background-color 0.15s ease-out, + border-color 0.15s ease-out, + box-shadow 0.15s ease-out; + backdrop-filter: saturate(115%) blur(0.18rem); + -webkit-backdrop-filter: saturate(115%) blur(0.18rem); +} + +.rounded-button:hover { + background-color: rgb(255 255 255 / 0.2); + border-color: rgb(255 255 255 / 0.34); + color: rgb(244 249 248) !important; + box-shadow: + 0 0.4rem 1.2rem rgb(0 0 0 / 0.18); + text-decoration: none; +} + +.heading-div .rounded-button:first-of-type { + border-color: rgb(160 223 217 / 0.42); + background-color: rgb(42 104 99 / 0.2); + color: rgb(233 245 243) !important; +} + +.heading-div .rounded-button:first-of-type:hover { + background-color: rgb(82 187 177 / 0.28); + border-color: rgb(186 228 223 / 0.38); + color: rgb(242 249 248) !important; +} + +.rounded-button span { + margin: 0 0 0.07em; + white-space: nowrap; +} + +.rounded-button svg { + width: auto; + height: 1.2em; + margin-right: 0.5em; + fill: currentColor; +} + +.reading-media { + display: flex; + justify-content: center; + align-items: flex-end; + height: 32vw; +} + +.reading-media+p { + margin-top: 1em !important; +} + +.media-block { + height: 100%; + margin: 0 0.2em; +} + +.text-button { + width: auto; + margin: 2.7em auto; + color: var(--md-typeset-btn-color); + text-align: center; + text-decoration: none; +} + +.text-button span { + white-space: nowrap; +} + +.text-button svg { + display: inline-block; + width: auto; + height: 0.9em; + margin-left: 0.15em; + padding-top: 0.17em; + fill: var(--md-typeset-btn-color); + background-size: cover; +} + +a:hover .text-button span { + text-decoration: underline; +} + +.device-on-hover { + width: auto; + transition: transform 0.3s ease-in-out, filter 0.3s ease-in-out; +} + +a:hover .device-on-hover { + filter: drop-shadow(0 0 0.2rem rgba(0, 0, 0, 0.15)); + transform: scale(1.01); +} + +/* Landing page content blocks */ .code-badge { width: 100%; height: auto; @@ -446,11 +537,10 @@ a:hover .text-button span { } .code-badge img { - height: 1.07em; width: auto; + height: 1.07em; } -/* brief intro */ .intro-container { display: flex; align-items: center; @@ -467,14 +557,13 @@ a:hover .text-button span { .intro-text { flex-grow: 1; - /* fill the space */ display: flex; flex-direction: column; justify-content: center; - text-align: left; align-items: flex-start; width: fit-content; margin: 2em; + text-align: left; } .intro-text>div { @@ -483,6 +572,10 @@ a:hover .text-button span { margin: 0 auto; } +.intro-text svg path { + fill: var(--md-primary-bg-color); +} + .endor-text { width: 50%; } @@ -492,7 +585,10 @@ a:hover .text-button span { font-weight: bold; } -/* contributors table */ +.home-div .intro-quote { + color: var(--md-default-fg-color--light) !important; +} + .profile-div { display: flex; flex-wrap: wrap; @@ -507,6 +603,11 @@ a:hover .text-button span { text-align: center; } +.profile-cell a:hover b, +.profile-cell a:focus-visible b { + text-decoration: underline; +} + .profile-img { width: 5em; border-radius: 50%; @@ -517,6 +618,14 @@ a:hover .text-button span { gap: 0.5em; } +.en-translator-profile-div { + max-width: 40em; +} + +.en-translator-profile-div .profile-cell { + flex: 0 0 20%; +} + .translator-profile-cell { flex: 0 0 auto; margin: 0.5em 0; @@ -529,7 +638,37 @@ a:hover .text-button span { margin: 0 auto; } -/* Hide navigation */ +/* Embedded media */ +.video-container { + position: relative; + padding-bottom: 56.25%; + height: 0; +} + +.video-container iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.starfield { + position: absolute; + width: 100%; + height: 100%; + z-index: 0; + background-color: var(--hero-starfield-bg-color, transparent); +} + +.starfield-origin { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +/* Responsive adjustments */ @media screen and (max-width: 76.25em) { .section-content { max-width: 95vw; @@ -544,7 +683,6 @@ a:hover .text-button span { } } -/* mobile devices */ @media screen and (max-width: 60em) { .home-div { font-size: 0.75rem; @@ -582,35 +720,8 @@ a:hover .text-button span { .profile-cell { flex: 1 1 30%; } -} -.video-container { - position: relative; - padding-bottom: 56.25%; - /* 16:9 */ - height: 0; -} - -.video-container iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -/* starfield */ -.starfield { - position: absolute; - width: 100%; - height: 100%; - z-index: 0; - background-color: var(--hero-starfield-bg-color, transparent); -} - -.starfield-origin { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + .en-translator-profile-div .profile-cell { + flex: 1 1 30%; + } } diff --git a/overrides/stylesheets/giscus-dark.css b/overrides/stylesheets/giscus-dark.css new file mode 100644 index 000000000..a977589eb --- /dev/null +++ b/overrides/stylesheets/giscus-dark.css @@ -0,0 +1,29 @@ +@import url("https://giscus.app/themes/noborder_dark.css"); + +.gsc-comment-box-buttons .btn-primary { + border-radius: 999px !important; +} + +/* Limit long giscus comments so they don't dominate the page. */ +.gsc-comment-content, +.gsc-reply-content { + max-height: 32rem; + overflow-y: auto; + padding-inline-end: 0.5rem; +} + +.gsc-comment-content::-webkit-scrollbar, +.gsc-reply-content::-webkit-scrollbar { + width: 0.35rem; +} + +.gsc-comment-content::-webkit-scrollbar-thumb, +.gsc-reply-content::-webkit-scrollbar-thumb { + background: rgba(230, 237, 243, 0.24); + border-radius: 999px; +} + +.gsc-comment-content::-webkit-scrollbar-track, +.gsc-reply-content::-webkit-scrollbar-track { + background: transparent; +} diff --git a/overrides/stylesheets/giscus-light.css b/overrides/stylesheets/giscus-light.css new file mode 100644 index 000000000..72dd0cf1e --- /dev/null +++ b/overrides/stylesheets/giscus-light.css @@ -0,0 +1,57 @@ +@import url("https://giscus.app/themes/light.css"); + +main { + --color-border-default: rgba(29, 29, 32, 0.08); + --color-border-muted: rgba(29, 29, 32, 0.05); + --color-accent-fg: #349890; + --color-btn-primary-text: #ffffff; + --color-btn-primary-bg: #52bbb1; + --color-btn-primary-hover-bg: #7acfc7; + --color-btn-primary-border: rgba(52, 152, 144, 0.2); + --color-btn-primary-hover-border: rgba(52, 152, 144, 0.24); +} + +.border, +.color-border-primary { + border-color: rgba(29, 29, 32, 0.07) !important; +} + +.gsc-comment-box-buttons .btn-primary { + border-radius: 999px !important; + border-color: transparent !important; + background-color: #52bbb1 !important; + color: #ffffff !important; + box-shadow: none !important; +} + +.gsc-comment-box-buttons .btn-primary:hover:not(:disabled) { + background-color: #7acfc7 !important; +} + +.gsc-comment-box-buttons .btn-primary:disabled { + opacity: 0.62; +} + +/* Limit long giscus comments so they don't dominate the page. */ +.gsc-comment-content, +.gsc-reply-content { + max-height: 32rem; + overflow-y: auto; + padding-inline-end: 0.5rem; +} + +.gsc-comment-content::-webkit-scrollbar, +.gsc-reply-content::-webkit-scrollbar { + width: 0.35rem; +} + +.gsc-comment-content::-webkit-scrollbar-thumb, +.gsc-reply-content::-webkit-scrollbar-thumb { + background: rgba(31, 35, 40, 0.22); + border-radius: 999px; +} + +.gsc-comment-content::-webkit-scrollbar-track, +.gsc-reply-content::-webkit-scrollbar-track { + background: transparent; +} diff --git a/overrides/zensical/stylesheets/extra.css b/overrides/zensical/stylesheets/extra.css deleted file mode 100644 index d52c7432b..000000000 --- a/overrides/zensical/stylesheets/extra.css +++ /dev/null @@ -1,770 +0,0 @@ -/* Color Settings */ -/* https://github.com/squidfunk/mkdocs-material/blob/6b5035f5580f97532d664e3d1babf5f320e88ee9/src/assets/stylesheets/main/_colors.scss */ -/* https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#custom-colors */ -:root>* { - --md-primary-fg-color: #ffffff; - --md-primary-bg-color: #1d1d20; - - --md-default-fg-color: #1d1d20; - --md-default-bg-color: #ffffff; - - --md-body-bg-color: #22272e; - --md-header-bg-color: rgba(255, 255, 255, 0.6); - - --md-code-fg-color: #1d1d20; - --md-code-bg-color: #f5f5f5; - - --md-accent-fg-color: #999; - - --md-typeset-color: #1d1d20; - --md-typeset-a-color: #349890; - - --md-typeset-btn-color: #55aea6; - --md-typeset-btn-hover-color: #52bbb1; - - --md-admonition-icon--pythontutor: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-pythontutor-color: #eee; -} - -[data-md-color-scheme="slate"] { - --theme-dark-base: #1E1E1E; - --theme-dark-mantle: #1A1A1A; - --theme-dark-crust: #171717; - --hero-starfield-bg-color: var(--theme-dark-base); - - --md-primary-fg-color: var(--theme-dark-base); - --md-primary-bg-color: #adbac7; - - --md-default-fg-color: #adbac7; - --md-default-bg-color: var(--theme-dark-base); - - --md-body-bg-color: var(--theme-dark-mantle); - --md-header-bg-color: rgba(26, 26, 26, 0.8); - - --md-code-fg-color: #adbac7; - --md-code-bg-color: var(--theme-dark-crust); - - --md-accent-fg-color: #aaa; - - --md-footer-fg-color: #adbac7; - --md-footer-bg-color: var(--theme-dark-mantle); - - --md-typeset-color: #adbac7; - --md-typeset-a-color: #52bbb1; - - --md-typeset-btn-color: #52bbb1; - --md-typeset-btn-hover-color: #55aea6; - - --md-admonition-pythontutor-color: var(--theme-dark-crust); -} - -[data-md-color-scheme="slate"][data-md-color-primary="black"], -[data-md-color-scheme="slate"][data-md-color-primary="white"] { - --md-typeset-a-color: #52bbb1; -} - -[data-md-color-scheme="slate"] .md-footer, -[data-md-color-scheme="slate"] .md-footer__inner { - background-color: var(--theme-dark-mantle); - color: var(--md-footer-fg-color); -} - -[data-md-color-scheme="slate"] .md-footer-meta { - background-color: var(--theme-dark-crust); - color: var(--md-footer-fg-color); -} - -[data-md-color-scheme="slate"] .md-footer__link { - background-color: var(--theme-dark-crust); - color: var(--md-footer-fg-color); -} - -[data-md-color-scheme="slate"] .md-footer__link:hover { - background-color: var(--theme-dark-base); -} - -[data-md-color-scheme="slate"] .md-footer__title, -[data-md-color-scheme="slate"] .md-footer__direction, -[data-md-color-scheme="slate"] .md-footer__button, -[data-md-color-scheme="slate"] .md-copyright, -[data-md-color-scheme="slate"] .md-copyright a, -[data-md-color-scheme="slate"] .md-social, -[data-md-color-scheme="slate"] .md-social__link { - color: var(--md-footer-fg-color); -} - -/* https://github.com/squidfunk/mkdocs-material/issues/4832#issuecomment-1374891676 */ -.md-nav__link[for] { - color: var(--md-default-fg-color) !important; -} - -/* Figure class */ -.animation-figure { - border-radius: 0.3rem; - display: block; - margin: 0 auto; - box-shadow: var(--md-shadow-z2); -} - -/* Cover image class */ -.cover-image { - width: 28rem; - height: auto; - border-radius: 0.3rem; - display: block; - margin: 0 auto; - box-shadow: var(--md-shadow-z2); -} - -/* Center Markdown Tables (requires md_in_html extension) */ -.center-table { - text-align: center; -} - -/* Reset alignment for table cells */ -.md-typeset .center-table :is(td, th):not([align]) { - text-align: initial; -} - -/* Font size */ -.md-typeset { - font-size: 0.75rem; - line-height: 1.5; -} - -.md-typeset pre { - font-size: 0.95em; -} - -/* Markdown Header */ -/* https://github.com/squidfunk/mkdocs-material/blob/dcab57dd1cced4b77875c1aa1b53467c62709d31/src/assets/stylesheets/main/_typeset.scss */ -.md-typeset h1 { - font-weight: 400; - color: var(--md-default-fg-color); -} - -.md-typeset h2 { - font-weight: 400; -} - -.md-typeset h3 { - font-weight: 500; -} - -.md-typeset h5 { - text-transform: none; -} - -.md-typeset a:hover { - color: var(--md-typeset-a-color); - text-decoration: underline; -} - -.md-typeset code { - border-radius: 0.2rem; -} - -.highlight span.filename { - font-weight: normal; -} - -/* font-family setting for Win10 */ -body { - --md-text-font-family: -apple-system, BlinkMacSystemFont, - var(--md-text-font, _), Helvetica, Arial, sans-serif; - --md-code-font-family: var(--md-code-font, _), SFMono-Regular, Consolas, Menlo, - -apple-system, BlinkMacSystemFont, var(--md-text-font, _), monospace; -} - -/* max height of code block */ -/* https://github.com/squidfunk/mkdocs-material/issues/3444 */ -.md-typeset pre>code { - max-height: 25rem; -} - -/* Keep code block scrollbar hover neutral instead of accent-colored */ -.md-typeset pre>code:hover { - scrollbar-color: var(--md-default-fg-color--lighter) transparent; -} - -.md-typeset pre>code::-webkit-scrollbar-thumb:hover { - background-color: var(--md-default-fg-color--lighter); -} - -/* Make the picture not glare in dark theme */ -[data-md-color-scheme="slate"] .md-typeset img, -[data-md-color-scheme="slate"] .md-typeset svg, -[data-md-color-scheme="slate"] .md-typeset video { - filter: brightness(0.85) invert(0.05); -} - -/* landing page */ -.header-img-div { - display: flex; - align-items: center; - justify-content: center; - margin: 0 auto; - width: 100%; - /* Default to full width */ -} - -/* Admonition for python tutor */ -.md-typeset .admonition.pythontutor, -.md-typeset details.pythontutor { - border-color: var(--md-default-fg-color--lightest); - margin-top: 0; - margin-bottom: 1.5625em; -} - -.md-typeset .pythontutor>.admonition-title, -.md-typeset .pythontutor>summary { - background-color: var(--md-code-bg-color); -} - -.md-typeset .pythontutor>.admonition-title::before, -.md-typeset .pythontutor>summary::before { - background-color: rgb(55, 118, 171); - -webkit-mask-image: var(--md-admonition-icon--pythontutor); - mask-image: var(--md-admonition-icon--pythontutor); -} - -/* code block tabs */ -.md-typeset .tabbed-labels>label { - font-size: 0.61rem; -} - -.md-typeset .tabbed-labels--linked>label>a { - padding: .78125em 1.0em .625em; -} - -/* header banner */ -.md-banner { - background-color: var(--md-code-bg-color); - color: var(--md-default-fg-color); - font-size: 0.75rem; -} - -.md-banner .banner-svg svg { - margin-right: 0.3rem; - height: 0.63rem; - fill: var(--md-default-fg-color); -} - -.pythontutor-iframe { - width: 125%; - height: 125%; - max-width: 125% !important; - max-height: 125% !important; - transform: scale(0.8); - transform-origin: top left; - border: none; -} - -/* landing page container */ -.home-div { - width: 100%; - height: auto; - display: flex; - justify-content: center; - align-items: center; - background-color: var(--md-default-bg-color); - color: var(--md-default-fg-color); - font-size: 0.9rem; - padding: 3em 2em; - text-align: center; -} - -.section-content { - width: 100%; - height: auto; - max-width: 70vw; -} - -/* rounded button */ -.rounded-button { - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 10em; - margin: 0 0.1em; - padding: 0.6em 1.3em; - border: none; - background-color: var(--md-typeset-btn-color); - color: var(--md-primary-fg-color) !important; - text-align: center; - text-decoration: none; - cursor: pointer; -} - -.rounded-button:hover { - background-color: var(--md-typeset-btn-hover-color); -} - -.rounded-button span { - margin: 0; - margin-bottom: 0.07em; - white-space: nowrap; -} - -.rounded-button svg { - fill: var(--md-primary-fg-color); - width: auto; - height: 1.2em; - margin-right: 0.5em; -} - -/* device image */ -.device-on-hover { - width: auto; - transition: transform 0.3s ease-in-out, filter 0.3s ease-in-out; -} - -a:hover .device-on-hover { - filter: drop-shadow(0 0 0.2rem rgba(0, 0, 0, 0.15)); - transform: scale(1.01); -} - -/* text button */ -.reading-media { - display: flex; - justify-content: center; - align-items: flex-end; - height: 32vw; -} - -.media-block { - height: 100%; - margin: 0 0.2em; -} - -.text-button { - width: auto; - color: var(--md-typeset-btn-color); - text-decoration: none; - text-align: center; - margin: 2.7em auto; -} - -.text-button span { - white-space: nowrap; -} - -.text-button svg { - display: inline-block; - fill: var(--md-typeset-btn-color); - width: auto; - height: 0.9em; - background-size: cover; - padding-top: 0.17em; - margin-left: 0.15em; -} - -a:hover .text-button span { - text-decoration: underline; -} - -/* hero image */ -.hero-div { - height: min(84vh, 75vw); - width: min(112vh, 100vw); - margin: 0 auto; - margin-top: -2.4rem; - padding: 0; - position: relative; - font-size: min(1.8vh, 2.5vw); - font-weight: normal; -} - -.hero-bg { - height: 100%; - width: 100%; - object-fit: cover; - position: absolute; -} - -/* hover on the planets */ -.hero-div>a>img { - width: auto; - position: absolute; - transition: transform 0.3s ease-in-out, filter 0.3s ease-in-out; -} - -.hero-div>a>span { - margin: 0; - position: absolute; - transform: translateX(-50%) translateY(-50%); - white-space: nowrap; - /* prevent line breaks */ - color: white; -} - -.hero-div>a:hover>img { - filter: brightness(1.15) saturate(1.1) drop-shadow(0 0 0.5rem rgba(255, 255, 255, 0.2)); - transform: scale(1.03); -} - -.hero-div>a:hover>span { - text-decoration: underline; - color: var(--md-typeset-btn-color); -} - -.heading-div { - width: 100%; - position: absolute; - transform: translateX(-50%); - left: 50%; - bottom: min(2vh, 3vw); - pointer-events: none; - color: #fff; -} - -/* code badge */ -.code-badge { - width: 100%; - height: auto; - margin: 1em auto; -} - -.code-badge img { - height: 1.07em; - width: auto; -} - -/* brief intro */ -.intro-container { - display: flex; - align-items: center; - margin: 2em auto; -} - -.intro-image { - flex-shrink: 0; - flex-grow: 0; - width: 50%; - border-radius: 0.5em; - box-shadow: var(--md-shadow-z2); -} - -.intro-text { - flex-grow: 1; - /* fill the space */ - display: flex; - flex-direction: column; - justify-content: center; - text-align: left; - align-items: flex-start; - width: fit-content; - margin: 2em; -} - -.intro-text>div { - align-self: flex-start; - width: auto; - margin: 0 auto; -} - -.endor-text { - width: 50%; -} - -.intro-quote { - color: var(--md-accent-fg-color); - font-weight: bold; -} - -/* contributors table */ -.profile-div { - display: flex; - flex-wrap: wrap; - justify-content: center; - max-width: 40em; - margin: 1em auto; -} - -.profile-cell { - flex: 1 1 15%; - margin: 1em 0; - text-align: center; -} - -.profile-img { - width: 5em; - border-radius: 50%; - margin-bottom: 0.5em; -} - -.translator-profile-div { - gap: 0.5em; -} - -.translator-profile-cell { - flex: 0 0 auto; - margin: 0.5em 0; - min-width: 8.5em; -} - -.giscus-container { - width: 40em; - max-width: 100%; - margin: 0 auto; -} - -/* Hide navigation */ -@media screen and (max-width: 76.25em) { - .section-content { - max-width: 95vw; - } - - .reading-media { - height: 33vw; - } - - .contrib-image { - width: 100%; - } -} - -/* mobile devices */ -@media screen and (max-width: 60em) { - .home-div { - font-size: 0.75rem; - } - - .hero-div { - margin-top: -4rem; - } - - .intro-container { - flex-direction: column; - } - - .intro-text { - width: auto; - order: 2; - margin: 0 auto; - } - - .endor-text { - width: auto; - margin: 0 auto; - } - - .intro-image { - width: 100%; - order: 1; - margin-bottom: 1em; - } - - .text-button { - margin: 0.7em auto; - } - - .profile-cell { - flex: 1 1 30%; - } -} - -.video-container { - position: relative; - padding-bottom: 56.25%; - /* 16:9 */ - height: 0; -} - -.video-container iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -/* starfield */ -.starfield { - position: absolute; - width: 100%; - height: 100%; - z-index: 0; - background-color: var(--hero-starfield-bg-color, transparent); -} - -.starfield-origin { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -/* Zensical-specific adjustments merged into the main stylesheet. */ -:root>* { - --md-accent-fg-color: var(--md-typeset-a-color); - --md-admonition-pythontutor-color: var(--md-code-bg-color); - --hello-algo-sidebar-width: 13rem; -} - -[data-md-color-scheme="slate"] { - --md-accent-fg-color: var(--md-typeset-a-color); - --md-admonition-pythontutor-color: var(--md-code-bg-color); - --md-body-bg-color: var(--md-default-bg-color); - --md-default-bg-color--light: rgb(30 30 30 / 0.8); -} - -[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary), -[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary) :is(p, li, strong, em, sub, sup, code, a) { - background-color: #f5f5f5; - color: #1d1d20; -} - -body { - background-color: var(--md-default-bg-color); -} - -html:has(body[data-md-color-scheme="slate"]) { - background-color: #1e1e1e; -} - -html:has(body[data-md-color-scheme="default"]) { - background-color: #ffffff; -} - -.home-div[data-md-color-scheme="default"], -.home-div[data-md-color-scheme="default"] h1, -.home-div[data-md-color-scheme="default"] h2, -.home-div[data-md-color-scheme="default"] h3, -.home-div[data-md-color-scheme="default"] h4, -.home-div[data-md-color-scheme="default"] h5, -.home-div[data-md-color-scheme="default"] h6 { - color: var(--md-default-fg-color); -} - -.home-div[data-md-color-scheme="slate"], -.home-div[data-md-color-scheme="slate"] h1, -.home-div[data-md-color-scheme="slate"] h2, -.home-div[data-md-color-scheme="slate"] h3, -.home-div[data-md-color-scheme="slate"] h4, -.home-div[data-md-color-scheme="slate"] h5, -.home-div[data-md-color-scheme="slate"] h6 { - color: var(--md-default-fg-color); -} - -.home-div .intro-quote { - color: var(--md-default-fg-color--light) !important; -} - -.reading-media+p { - margin-top: 1em !important; -} - -.md-typeset .admonition-title:before, -.md-typeset summary:before, -.md-typeset summary:after { - top: 50%; -} - -.md-typeset .admonition-title:before, -.md-typeset summary:before { - transform: translateY(-50%); -} - -.md-typeset summary:after { - transform: translateY(-50%) rotate(0deg); -} - -.md-typeset details[open]>summary:after { - transform: translateY(-50%) rotate(90deg); -} - -.md-nav__link[for] { - color: inherit !important; -} - -.md-nav__link[for].md-nav__link--active { - color: var(--md-accent-fg-color) !important; -} - -@media screen and (min-width: 76.25em) { - .md-grid { - max-width: calc(61rem + 2 * (var(--hello-algo-sidebar-width) - 12.1rem)); - } - - .md-sidebar--primary, - .md-sidebar--secondary { - width: var(--hello-algo-sidebar-width); - } - - [dir="ltr"] .md-sidebar__inner { - padding-right: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); - } - - [dir="rtl"] .md-sidebar__inner { - padding-left: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); - } -} - -.md-sidebar--primary .md-sidebar__scrollwrap { - scrollbar-color: var(--md-default-fg-color--lighter) transparent; -} - -.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb, -.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { - background-color: var(--md-default-fg-color--lighter); -} - -.md-footer, -.md-footer__inner, -.md-footer-meta, -.md-footer__link, -.md-footer__link:hover { - background-color: var(--md-default-bg-color); -} - -.md-footer { - border-top: 0.05rem solid var(--md-default-fg-color--lightest); -} - -[data-md-color-scheme="slate"] .md-footer, -[data-md-color-scheme="slate"] .md-footer__inner, -[data-md-color-scheme="slate"] .md-footer-meta, -[data-md-color-scheme="slate"] .md-footer__link, -[data-md-color-scheme="slate"] .md-footer__link:hover { - background-color: var(--md-default-bg-color); -} - -.md-banner { - background-color: var(--md-default-bg-color); -} - -.md-typeset .admonition.pythontutor, -.md-typeset details.pythontutor, -.md-typeset .pythontutor>.admonition-title, -.md-typeset .pythontutor>summary { - background-color: var(--md-code-bg-color); -} - -.md-typeset .pythontutor>.admonition-title::before, -.md-typeset .pythontutor>summary::before, -.md-typeset .pythontutor>summary::after { - top: 50%; -} - -.md-typeset .pythontutor>.admonition-title::before, -.md-typeset .pythontutor>summary::before { - transform: translateY(-50%); -} - -.md-typeset .pythontutor>summary::after { - transform: translateY(-50%) rotate(0deg); -} - -.md-typeset details[open].pythontutor>summary::after { - transform: translateY(-50%) rotate(90deg); -} - -.md-typeset a:not(.md-button) { - text-decoration: none; -} - -.md-typeset a:not(.md-button):hover, -.md-typeset a:not(.md-button):focus-visible { - text-decoration: underline; -} \ No newline at end of file diff --git a/overrides/zensical/zensical.toml b/overrides/zensical/zensical.toml deleted file mode 100644 index d4e36734f..000000000 --- a/overrides/zensical/zensical.toml +++ /dev/null @@ -1,20 +0,0 @@ -[project] -extra_css = [ - "stylesheets/extra.css", -] - -[project.theme] -features = [ - "content.action.edit", - "content.code.annotate", - "content.code.copy", - "content.tabs.link", - "content.tooltips", - "navigation.indexes", - "navigation.top", - "navigation.tracking", - "search.highlight", - "search.share", - "search.suggest", - "toc.follow", -] diff --git a/ru/docs/chapter_appendix/contribution.md b/ru/docs/chapter_appendix/contribution.md index 61cfb82ee..c289ece37 100644 --- a/ru/docs/chapter_appendix/contribution.md +++ b/ru/docs/chapter_appendix/contribution.md @@ -6,7 +6,7 @@ comments: true Возможности автора ограничены, поэтому в книге неизбежно могут встречаться упущения и ошибки. Просим отнестись к этому с пониманием. Если вы заметите опечатки, неработающие ссылки, пропуски в содержании, двусмысленные формулировки, неясные объяснения или неудачную структуру изложения, пожалуйста, помогите нам это исправить, чтобы читатели получили более качественный учебный ресурс. -GitHub ID всех [участников](https://github.com/krahets/hello-algo/graphs/contributors) будут указаны на главных страницах репозитория книги, веб-версии и PDF-версии в знак благодарности за их бескорыстный вклад в сообщество открытого исходного кода. +Все GitHub ID [авторов](https://github.com/krahets/hello-algo/graphs/contributors) будут указаны на главных страницах репозитория книги, веб-версии и PDF-версии в знак благодарности за их бескорыстный вклад в сообщество открытого исходного кода. !!! success "Сила открытого исходного кода" @@ -16,31 +16,31 @@ GitHub ID всех [участников](https://github.com/krahets/hello-algo/ ### 1.   Небольшие правки содержания -Как показано на рисунке 16-3, в правом верхнем углу каждой страницы есть "значок редактирования". Вы можете изменить текст или код следующим образом. +Как показано на рисунке 16-3, в правом верхнем углу каждой страницы есть "значок редактирования". Текст или код можно изменить следующим образом. 1. Нажмите на "значок редактирования". Если появится сообщение "You need to fork this repository", согласитесь с этим действием. 2. Измените содержимое исходного Markdown-файла, проверьте корректность правок и постарайтесь сохранить единый стиль оформления. -3. Внизу страницы заполните описание изменений, затем нажмите кнопку "Propose file change". После перехода на следующую страницу нажмите кнопку "Create pull request", чтобы создать pull request. +3. Внизу страницы заполните описание изменений, затем нажмите кнопку "Propose file change". После перехода на следующую страницу нажмите кнопку "Create pull request", чтобы отправить pull request. ![Кнопка редактирования страницы](contribution.assets/edit_markdown.png){ class="animation-figure" }

Рисунок 16-3   Кнопка редактирования страницы

-Изображения нельзя изменить напрямую, поэтому проблему с ними нужно описывать через новый [Issue](https://github.com/krahets/hello-algo/issues) или комментарий. Мы постараемся как можно быстрее перерисовать и заменить изображение. +Изображения нельзя изменить напрямую, поэтому проблему с ними нужно описывать через новый [Issue](https://github.com/krahets/hello-algo/issues) или комментарий. Мы постараемся как можно быстрее исправить и обновить изображение. ### 2.   Создание содержания -Если вам интересно участвовать в этом проекте с открытым исходным кодом, например переводить код на другие языки программирования или расширять содержание статей, то следует придерживаться следующего рабочего процесса Pull Request. +Если вам интересно участвовать в этом проекте с открытым исходным кодом, например переводить код на другие языки программирования или расширять содержание статей, то следует придерживаться следующего процесса Pull Request. 1. Войдите в GitHub и сделайте Fork [репозитория книги](https://github.com/krahets/hello-algo) в свой личный аккаунт. 2. Перейдите на страницу своего Fork-репозитория и с помощью команды `git clone` клонируйте репозиторий локально. 3. Создавайте и редактируйте содержание локально, затем проведите полное тестирование и проверьте корректность кода. -4. Сделайте Commit для локальных изменений, после чего выполните Push в удаленный репозиторий. -5. Обновите страницу репозитория и нажмите кнопку "Create pull request", чтобы отправить pull request. +4. Зафиксируйте локальные изменения, после чего выполните Push в удаленный репозиторий. +5. Обновите страницу репозитория и нажмите кнопку "Create pull request", чтобы инициировать pull request. ### 3.   Развертывание Docker -В корневом каталоге `hello-algo` выполните следующий Docker-скрипт, после чего проект будет доступен по адресу `http://localhost:8000`: +В корневом каталоге `hello-algo` выполните следующий Docker-скрипт, после чего проект станет доступен по адресу `http://localhost:8000`: ```shell docker-compose up -d diff --git a/ru/docs/chapter_appendix/installation.md b/ru/docs/chapter_appendix/installation.md index eb19743b6..c3c40fb59 100644 --- a/ru/docs/chapter_appendix/installation.md +++ b/ru/docs/chapter_appendix/installation.md @@ -6,13 +6,13 @@ comments: true ## 16.1.1   Установка IDE -В качестве локальной интегрированной среды разработки (IDE) рекомендуется использовать открытую и легковесную VS Code. Перейдите на [официальный сайт VS Code](https://code.visualstudio.com/), выберите версию для своей операционной системы и установите ее. +В качестве локальной интегрированной среды разработки (IDE) рекомендуется использовать открытую и быструю VS Code. Перейдите на [официальный сайт VS Code](https://code.visualstudio.com/), выберите версию для своей операционной системы и установите ее. ![Загрузка VS Code с официального сайта](installation.assets/vscode_installation.png){ class="animation-figure" }

Рисунок 16-1   Загрузка VS Code с официального сайта

-VS Code обладает мощной экосистемой расширений и поддерживает запуск и отладку большинства языков программирования. Например, после установки расширения "Python Extension Pack" можно отлаживать код на Python. Процесс установки показан на рисунке 16-2. +VS Code обладает мощной экосистемой расширений и поддерживает выполнение и отладку большинства языков программирования. Например, после установки расширения "Python Extension Pack" можно отлаживать код на Python. Процесс установки показан на рисунке 16-2. ![Установка расширений VS Code](installation.assets/vscode_extension_installation.png){ class="animation-figure" } @@ -22,13 +22,13 @@ VS Code обладает мощной экосистемой расширени ### 1.   Среда Python -1. Загрузите и установите [Miniconda3](https://docs.conda.io/en/latest/miniconda.html), требуется Python 3.10 или более новая версия. +1. Загрузите и установите [Miniconda3](https://docs.conda.io/en/latest/miniconda.html), требуется Python 3.10 или более поздняя версия. 2. В магазине расширений VS Code найдите `python` и установите Python Extension Pack. 3. (Необязательно) Введите в командной строке `pip install black`, чтобы установить инструмент форматирования кода. ### 2.   Среда C/C++ -1. В Windows требуется установить [MinGW](https://sourceforge.net/projects/mingw-w64/files/) ([руководство по настройке](https://blog.csdn.net/qq_33698226/article/details/129031241)); в macOS Clang уже установлен по умолчанию. +1. В Windows требуется установить [MinGW](https://sourceforge.net/projects/mingw-w64/files/) ([руководство по настройке](https://blog.csdn.net/qq_33698226/article/details/129031241)); в macOS компилятор Clang уже установлен по умолчанию. 2. В магазине расширений VS Code найдите `c++` и установите C/C++ Extension Pack. 3. (Необязательно) Откройте страницу Settings, найдите параметр форматирования `Clang_format_fallback Style` и задайте значение `{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }`. diff --git a/ru/docs/chapter_appendix/terminology.md b/ru/docs/chapter_appendix/terminology.md index afb6fec24..c29d642cc 100644 --- a/ru/docs/chapter_appendix/terminology.md +++ b/ru/docs/chapter_appendix/terminology.md @@ -4,10 +4,10 @@ comments: true # 16.3   Глоссарий -В таблице 16-1 перечислены важные термины, встречающиеся в книге. Обратите внимание на следующие моменты. +В таблице 16-1 приведены важные термины, встречающиеся в книге. Обратите внимание на следующие моменты. -- Рекомендуем запомнить английские названия терминов, чтобы легче читать англоязычную литературу. -- В русской версии приводится единый рекомендуемый перевод каждого термина. +- Рекомендуем запомнить английские названия терминов, чтобы легче читать англоязычные материалы. +- В русской версии для каждого термина приводится единый рекомендуемый перевод.

Таблица 16-1   Важные термины по структурам данных и алгоритмам

@@ -57,7 +57,7 @@ comments: true | rear of the queue | хвост очереди | | hash table | хеш-таблица | | hash set | хеш-набор | -| bucket | корзина | +| bucket | бакет | | hash function | хеш-функция | | hash collision | хеш-коллизия | | load factor | коэффициент заполнения | @@ -80,8 +80,8 @@ comments: true | height | высота | | depth | глубина | | perfect binary tree | идеальное двоичное дерево | -| complete binary tree | совершенное двоичное дерево | -| full binary tree | полное двоичное дерево | +| complete binary tree | полное двоичное дерево | +| full binary tree | строгое двоичное дерево | | balanced binary tree | сбалансированное двоичное дерево | | binary search tree | двоичное дерево поиска | | AVL tree | АВЛ-дерево | @@ -89,7 +89,9 @@ comments: true | level-order traversal | обход по уровням | | breadth-first traversal | обход в ширину | | depth-first traversal | обход в глубину | -| binary search tree | двоичное дерево поиска | +| pre-order traversal | прямой обход | +| in-order traversal | симметричный обход | +| post-order traversal | обратный обход | | balanced binary search tree | сбалансированное двоичное дерево поиска | | balance factor | фактор баланса | | heap | куча | diff --git a/ru/docs/chapter_array_and_linkedlist/array.md b/ru/docs/chapter_array_and_linkedlist/array.md index 43915894d..abcd80af5 100644 --- a/ru/docs/chapter_array_and_linkedlist/array.md +++ b/ru/docs/chapter_array_and_linkedlist/array.md @@ -4,7 +4,7 @@ comments: true # 4.1   Массив -Массив (array) - это линейная структура данных, которая хранит элементы одного типа в непрерывной области памяти. Положение элемента в массиве называется его индексом (index). На рисунке 4-1 показаны основные понятия, связанные с массивом, и способ его хранения. +Массив (array) - это линейная структура данных, в которой элементы одного типа хранятся в непрерывной области памяти. Положение элемента в массиве называется его индексом (index). На рисунке 4-1 показаны основные понятия, связанные с массивом, и способ его хранения. ![Определение массива и способ хранения](array.assets/array_definition.png){ class="animation-figure" } @@ -14,7 +14,7 @@ comments: true ### 1.   Инициализация массива -В зависимости от задачи мы можем выбрать один из двух способов инициализации массива: без начальных значений или с заданными начальными значениями. Если начальные значения не указаны, большинство языков программирования инициализируют элементы массива значением $0$ : +Существует два способа инициализации массива: без начальных значений и с заданными начальными значениями. Если начальные значения не указаны, большинство языков программирования инициализируют элементы массива нулями: === "Python" @@ -138,13 +138,13 @@ comments: true ### 2.   Доступ к элементам -Элементы массива хранятся в непрерывной области памяти, а это означает, что вычислить адрес любого элемента очень просто. Зная адрес массива в памяти (то есть адрес первого элемента) и индекс некоторого элемента, мы можем по формуле с рисунка ниже вычислить адрес этого элемента и напрямую обратиться к нему. +Элементы массива хранятся в непрерывной области памяти, что упрощает вычисление их адресов. Зная адрес массива в памяти (то есть адрес первого элемента) и индекс некоторого элемента, мы можем по формуле с рисунка ниже вычислить адрес этого элемента и напрямую обратиться к нему. ![Вычисление адреса элемента массива](array.assets/array_memory_location_calculation.png){ class="animation-figure" }

Рисунок 4-2   Вычисление адреса элемента массива

-Если посмотреть на рисунок 4-2, можно заметить, что индекс первого элемента массива равен $0$ , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с $1$ . Однако с точки зрения формулы адресации **индекс по сути является смещением относительно адреса памяти**. Смещение первого элемента равно $0$ , поэтому индекс $0$ вполне логичен. +Если посмотреть на рисунок 4-2, можно заметить, что индекс первого элемента массива равен $0$ , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с $1$ . Однако с точки зрения формулы адресации **индекс по сути является смещением относительно адреса памяти**. Смещение первого элемента равно $0$ , поэтому индекс $0$ полностью логичен. Доступ к элементам массива очень эффективен: любой элемент массива можно получить за $O(1)$ времени. @@ -324,13 +324,13 @@ comments: true ### 3.   Вставка элемента -Элементы массива в памяти расположены "вплотную" друг к другу, и между ними нет места для размещения новых данных. Как показано на рисунке 4-3, если мы хотим вставить элемент в середину массива, то все элементы после этой позиции нужно сдвинуть на одну позицию вправо, а затем записать новое значение в освободившийся индекс. +Элементы массива в памяти расположены вплотную друг к другу, и между ними нет места для размещения новых данных. Как показано на рисунке 4-3, если мы хотим вставить элемент в середину массива, то все элементы после этой позиции нужно сдвинуть на одну позицию вправо, а затем записать новое значение в освободившийся индекс. ![Пример вставки элемента в массив](array.assets/array_insert_element.png){ class="animation-figure" }

Рисунок 4-3   Пример вставки элемента в массив

-Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к "потере" элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о "списках". +Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к потере элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о "списках". === "Python" @@ -526,7 +526,7 @@ comments: true

Рисунок 4-4   Пример удаления элемента из массива

-Обрати внимание: после удаления исходный последний элемент становится "бессмысленным", поэтому специально изменять его не требуется. +Обрати внимание: после удаления исходный последний элемент становится бессмысленным, поэтому специально изменять его не требуется. === "Python" @@ -692,7 +692,7 @@ comments: true - **Высокая временная сложность**: средняя временная сложность и вставки, и удаления равна $O(n)$ , где $n$ - длина массива. - **Потеря элементов**: поскольку длина массива неизменяема, после вставки элементы, выходящие за пределы длины массива, будут потеряны. -- **Потери памяти**: можно заранее инициализировать более длинный массив и использовать только его переднюю часть; тогда "теряемые" при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти. +- **Потери памяти**: можно заранее инициализировать более длинный массив и использовать только его переднюю часть; тогда теряемые при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти. ### 5.   Обход массива @@ -939,7 +939,7 @@ comments: true Чтобы найти заданный элемент в массиве, нужно пройти по массиву и на каждой итерации проверять, совпадает ли значение; если совпадает, вернуть соответствующий индекс. -Поскольку массив - это линейная структура данных, такая операция поиска называется "линейным поиском". +Поскольку массив - это линейная структура данных, такая операция поиска называется линейным поиском. === "Python" @@ -1326,8 +1326,8 @@ comments: true ```ruby title="array.rb" ### Увеличить длину массива ### - # Обратите внимание: Array в Ruby является динамическим массивом и может быть расширен напрямую - # Для удобства обучения эта функция рассматривает Array как массив неизменяемой длины + # Обратите внимание: Array в Ruby является динамическим массивом и может расширяться напрямую + # Для удобства обучения в этой функции Array рассматривается как массив неизменяемой длины def extend(nums, enlarge) # Инициализировать массив увеличенной длины res = Array.new(nums.length + enlarge, 0) @@ -1349,13 +1349,13 @@ comments: true ## 4.1.2   Преимущества и ограничения массива -Массив хранится в непрерывной области памяти, и все его элементы имеют один и тот же тип. Такой подход содержит много априорной информации, которую система может использовать для оптимизации эффективности операций со структурой данных. +Массив хранится в непрерывной области памяти, и все его элементы имеют один и тот же тип. Такой подход содержит богатую априорную информацию, которую система может использовать для оптимизации эффективности операций с этой структурой данных. - **Высокая пространственная эффективность**: массив выделяет для данных непрерывный блок памяти без дополнительного структурного накладного расхода. - **Поддержка произвольного доступа**: массив позволяет обращаться к любому элементу за $O(1)$ времени. - **Локальность кэша**: при обращении к элементу массива компьютер загружает не только сам элемент, но и соседние данные, что позволяет использовать кэш для ускорения последующих операций. -Хранение в непрерывной области памяти - палка о двух концах, и у него есть следующие ограничения. +Непрерывное хранение данных - это палка о двух концах, и у него есть следующие ограничения. - **Низкая эффективность вставки и удаления**: когда элементов в массиве много, вставка и удаление требуют сдвига большого количества элементов. - **Неизменяемая длина**: после инициализации длина массива фиксирована; расширение массива требует копирования всех данных в новый массив, что стоит дорого. @@ -1366,7 +1366,7 @@ comments: true Массив - это базовая и очень распространенная структура данных. Он часто используется как в различных алгоритмах, так и при реализации более сложных структур данных. - **Произвольный доступ**: если мы хотим случайным образом выбирать некоторые образцы, можно сохранить их в массиве и сгенерировать случайную последовательность индексов для выборки. -- **Сортировка и поиск**: массив - самая распространенная структура данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, бинарный поиск и многие другие алгоритмы в основном работают именно с массивами. -- **Таблица поиска**: когда нужно быстро находить элемент или его соответствие, массив можно использовать как lookup table. Например, если мы хотим реализовать отображение символов в коды ASCII, можно использовать значение ASCII как индекс, а соответствующий элемент хранить по этой позиции массива. +- **Сортировка и поиск**: массив - самая распространенная структура данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, двоичный поиск и многие другие алгоритмы в основном работают именно с массивами. +- **Таблица поиска**: когда нужно быстро находить элемент или его соответствие, массив можно использовать как таблицу поиска. Например, если мы хотим реализовать отображение символов в коды ASCII, можно использовать значение ASCII как индекс, а соответствующий элемент хранить по этой позиции массива. - **Машинное обучение**: в нейронных сетях широко используются операции линейной алгебры над векторами, матрицами и тензорами, и все эти данные строятся в форме массивов. Массив - самая часто используемая структура данных в программировании нейросетей. - **Реализация структур данных**: массивы можно использовать для реализации стеков, очередей, хеш-таблиц, куч, графов и других структур данных. Например, матрица смежности графа по сути является двумерным массивом. diff --git a/ru/docs/chapter_array_and_linkedlist/index.md b/ru/docs/chapter_array_and_linkedlist/index.md index ec097e21e..37209ed93 100644 --- a/ru/docs/chapter_array_and_linkedlist/index.md +++ b/ru/docs/chapter_array_and_linkedlist/index.md @@ -11,12 +11,12 @@ icon: material/view-list-outline Мир структур данных напоминает прочную кирпичную стену. - Кирпичи массива уложены ровно и плотно прилегают друг к другу. Кирпичи связного списка разбросаны в разных местах, а соединяющие их лозы свободно тянутся между щелями. + Кирпичи массива уложены ровно и плотно прилегают друг к другу. Узлы связного списка, напротив, разбросаны в разных местах, а соединяющие их связи свободно тянутся между промежутками. ## Содержание главы - [4.1   Массив](array.md) - [4.2   Связный список](linked_list.md) - [4.3   Список](list.md) -- [4.4   Память и кеш *](ram_and_cache.md) +- [4.4   Оперативная память и кэш *](ram_and_cache.md) - [4.5   Резюме](summary.md) diff --git a/ru/docs/chapter_array_and_linkedlist/linked_list.md b/ru/docs/chapter_array_and_linkedlist/linked_list.md index 10355d5d5..3c453b57d 100644 --- a/ru/docs/chapter_array_and_linkedlist/linked_list.md +++ b/ru/docs/chapter_array_and_linkedlist/linked_list.md @@ -4,21 +4,21 @@ comments: true # 4.2   Связный список -Память является общим ресурсом для всех программ, и в сложной среде выполнения свободные участки памяти могут быть разбросаны по всему адресу памяти. Мы знаем, что память для хранения массива должна быть непрерывной, а если массив очень велик, память может не суметь предоставить столь большой непрерывный блок. Именно здесь проявляется преимущество гибкости связного списка. +Память - общий ресурс для всех программ, и в сложной среде выполнения свободные участки памяти могут быть разбросаны по всему адресному пространству. Мы знаем, что память для хранения массива должна быть непрерывной, а если массив очень велик, в памяти может не оказаться столь большого непрерывного блока. Именно здесь и проявляется преимущество гибкости связного списка. -Связный список (linked list) - это линейная структура данных, в которой каждый элемент представляет собой объект-узел, а сами узлы соединены между собой через "ссылки". Ссылка хранит адрес памяти следующего узла, благодаря чему из текущего узла можно получить доступ к следующему. +Связный список (linked list) - это линейная структура данных, в которой каждый элемент представляет собой объект-узел, а сами узлы соединены между собой с помощью ссылок. Ссылка хранит адрес памяти следующего узла, благодаря чему из текущего узла можно перейти к следующему. -Конструкция связного списка позволяет хранить отдельные узлы в разных местах памяти, и их адреса вовсе не обязаны быть непрерывными. +Конструкция связного списка позволяет хранить отдельные узлы в разных местах памяти, и их адреса вовсе не обязаны быть последовательными. ![Определение связного списка и способ хранения](linked_list.assets/linkedlist_definition.png){ class="animation-figure" }

Рисунок 4-5   Определение связного списка и способ хранения

-Если посмотреть на рисунок 4-5, можно заметить, что базовой единицей связного списка является объект узел (node). Каждый узел содержит две части данных: "значение" узла и "ссылку" на следующий узел. +Как видно на рисунке 4-5, базовой единицей связного списка является объект узел (node). Каждый узел содержит две части данных: значение узла и ссылку на следующий узел. -- Первый узел связного списка называется "головным узлом", а последний - "хвостовым узлом". -- Хвостовой узел указывает на "пусто", что в Java, C++ и Python обозначается как `null` , `nullptr` и `None` соответственно. -- В языках, поддерживающих указатели, таких как C, C++, Go и Rust, упомянутую выше "ссылку" следует заменить на "указатель". +- Первый узел связного списка называется головным узлом, а последний - хвостовым узлом. +- Хвостовой узел указывает на пустое значение, что в Java, C++ и Python обозначается как `null` , `nullptr` и `None` соответственно. +- В языках, поддерживающих указатели, таких как C, C++, Go и Rust, упомянутую выше ссылку следует заменить на указатель. Как показано в коде ниже, узел связного списка `ListNode` хранит не только значение, но и дополнительную ссылку (указатель). Поэтому **при одинаковом объеме данных связный список занимает больше памяти, чем массив**. @@ -73,7 +73,7 @@ comments: true Next *ListNode // Указатель на следующий узел } - // NewListNode Конструктор, создает новый связный список + // NewListNode Конструктор, создает новый узел func NewListNode(val int) *ListNode { return &ListNode{ Val: val, @@ -195,7 +195,7 @@ comments: true ### 1.   Инициализация связного списка -Построение связного списка состоит из двух шагов: сначала нужно инициализировать объекты всех узлов, затем установить связи-ссылки между ними. После завершения инициализации мы можем, начиная с головы списка, последовательно проходить все узлы по ссылке `next`. +Построение связного списка состоит из двух шагов: сначала нужно инициализировать объекты всех узлов, затем установить ссылочные связи между ними. После завершения инициализации мы можем, начиная с головы списка, последовательно проходить все узлы по ссылке `next`. === "Python" @@ -423,13 +423,13 @@ comments: true https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BA%D0%B0%D0%B6%D0%B4%D1%8B%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false -Массив в целом - это одна переменная: например, массив `nums` содержит элементы `nums[0]` , `nums[1]` и т.д. Связный список же состоит из множества независимых объектов-узлов. **Обычно в качестве обозначения всего связного списка используют головной узел**; например, в приведенном выше коде связный список можно обозначить как список `n0` . +Массив в целом - это одна переменная: например, массив `nums` содержит элементы `nums[0]` , `nums[1]` и т.д. Связный список же состоит из множества независимых объектов-узлов. **Обычно в качестве обозначения всего связного списка используют головной узел**; например, в приведенном выше коде связный список можно обозначить как `n0` . ### 2.   Вставка узла Вставить узел в связный список очень легко. Как показано на рисунке 4-6, предположим, что мы хотим вставить новый узел `P` между двумя соседними узлами `n0` и `n1` ; **для этого нужно изменить всего две ссылки (указателя)**, а временная сложность будет равна $O(1)$ . -Для сравнения: временная сложность вставки элемента в массив составляет $O(n)$ , и при большом объеме данных это неэффективно. +Для сравнения: временная сложность вставки элемента в массив составляет $O(n)$ , и при большом объеме данных это менее эффективно. ![Пример вставки узла в связный список](linked_list.assets/linkedlist_insert_node.png){ class="animation-figure" } @@ -571,7 +571,7 @@ comments: true ```ruby title="linked_list.rb" ### Вставка узла _p после узла n0 в связном списке ### - # В Ruby `p` — встроенная функция, а `P` — константа, поэтому вместо этого можно использовать `_p` + # В Ruby `p` является встроенной функцией, а `P` — константой, поэтому вместо них можно использовать `_p` def insert(n0, _p) n1 = n0.next _p.next = n1 @@ -586,9 +586,9 @@ comments: true ### 3.   Удаление узла -Как показано на рисунке 4-7, удалить узел из связного списка тоже очень удобно: **нужно изменить всего одну ссылку (указатель)**. +Как показано на рисунке 4-7, удалить узел из связного списка тоже очень просто: **нужно изменить всего одну ссылку (указатель)**. -Обрати внимание: хотя после завершения операции удаления узел `P` все еще указывает на `n1` , при обходе связного списка до `P` уже нельзя добраться, а значит `P` больше не принадлежит данному списку. +Стоит отметить, что хотя после завершения операции удаления узел `P` все еще указывает на `n1` , при обходе связного списка до `P` уже нельзя добраться. Это означает, что `P` фактически больше не принадлежит данному списку. ![Удаление узла из связного списка](linked_list.assets/linkedlist_remove_node.png){ class="animation-figure" } @@ -789,7 +789,7 @@ comments: true ### 4.   Доступ к узлу -**Доступ к узлам в связном списке менее эффективен**. Как уже обсуждалось в предыдущем разделе, к любому элементу массива можно обратиться за $O(1)$ времени. Со связным списком это не так: программе нужно стартовать от головного узла и последовательно двигаться дальше, пока не будет найден целевой узел. То есть для доступа к $i$ -му узлу списка нужно выполнить $i - 1$ итераций, а временная сложность составляет $O(n)$ . +**Доступ к узлам в связном списке менее эффективен**. Как уже обсуждалось в предыдущем разделе, к любому элементу массива можно обратиться за $O(1)$ времени. Со связным списком это не так: программе нужно начать с головного узла и последовательно двигаться дальше, пока не будет найден целевой узел. То есть для доступа к $i$ -му узлу списка нужно выполнить $i - 1$ итераций, а временная сложность составляет $O(n)$ . === "Python" @@ -993,7 +993,7 @@ comments: true ### 5.   Поиск узла -Выполни обход связного списка, найди в нем узел со значением `target` и верни индекс этого узла в списке. Этот процесс тоже относится к линейному поиску. Код выглядит следующим образом: +Поиск узла заключается в обходе связного списка, нахождении узла со значением `target` и возврате его индекса в списке. Этот процесс тоже относится к линейному поиску. Код выглядит следующим образом: === "Python" @@ -1240,9 +1240,9 @@ comments: true Как показано на рисунке 4-8, существует три распространенных типа связных списков. -- **Односвязный список**: это обычный связный список, рассмотренный выше. Узел односвязного списка содержит значение и ссылку на следующий узел. Первый узел называется головным, последний - хвостовым, и хвост указывает на пусто `None` . -- **Циклический список**: если заставить хвостовой узел односвязного списка указывать на головной (то есть соединить хвост с головой), получится циклический список. В циклическом списке любой узел можно рассматривать как головной. -- **Двусвязный список**: по сравнению с односвязным списком двусвязный хранит ссылки в двух направлениях. Определение узла двусвязного списка включает как ссылку на следующий узел, так и ссылку на предыдущий узел. По сравнению с односвязным списком двусвязный более гибок и позволяет проходить список в обе стороны, но за это приходится платить дополнительной памятью. +- **Односвязный список**: это обычный связный список, рассмотренный выше. Узел односвязного списка содержит значение и ссылку на следующий узел. Первый узел называется головным, последний - хвостовым, и хвост указывает на `None` . +- **Циклический список**: если заставить хвостовой узел односвязного списка указывать на головной, то есть соединить хвост с головой, получится циклический список. В циклическом списке любой узел можно рассматривать как головной. +- **Двусвязный список**: по сравнению с односвязным списком двусвязный хранит ссылки в двух направлениях. Определение узла двусвязного списка включает как ссылку на следующий узел, так и ссылку на предыдущий узел. По сравнению с односвязным списком двусвязный более гибок и позволяет обходить список в обе стороны, но за это приходится платить дополнительной памятью. === "Python" @@ -1457,10 +1457,10 @@ comments: true Двусвязные списки обычно используются там, где нужен быстрый доступ как к предыдущему, так и к следующему элементу. - **Продвинутые структуры данных**: например, в красно-черных деревьях и B-деревьях нам нужен доступ к родительскому узлу; этого можно добиться, сохранив в узле ссылку на родителя, по аналогии с двусвязным списком. -- **История браузера**: когда пользователь в браузере нажимает кнопки "вперед" или "назад", браузеру нужно знать предыдущую и следующую веб-страницы, которые он посещал. Свойства двусвязного списка делают такую операцию простой. +- **История браузера**: когда пользователь в браузере нажимает кнопки "вперед" или "назад", браузеру нужно знать предыдущую и следующую посещенные страницы. Свойства двусвязного списка делают такую операцию простой. - **Алгоритм LRU**: в алгоритмах вытеснения из кэша (LRU) нужно быстро находить наименее недавно использованные данные, а также быстро добавлять и удалять узлы. Для этого двусвязный список подходит очень хорошо. -Циклические списки часто применяются в сценариях, требующих периодических операций, например при планировании ресурсов в операционной системе. +Циклические списки часто применяются в сценариях, требующих циклических операций, например при планировании ресурсов в операционной системе. - **Алгоритм циклического распределения кванта времени**: в операционных системах round-robin scheduling - это распространенный алгоритм планирования CPU, который циклически обходит набор процессов. Каждому процессу выделяется квант времени, и когда он исчерпан, CPU переключается на следующий процесс. Такую циклическую операцию удобно реализовать с помощью кольцевого списка. - **Буферы данных**: в некоторых реализациях буферов данных также могут использоваться циклические списки. Например, в аудио- и видеоплеерах поток данных может делиться на несколько буферных блоков и помещаться в кольцевой список для обеспечения непрерывного воспроизведения. diff --git a/ru/docs/chapter_array_and_linkedlist/list.md b/ru/docs/chapter_array_and_linkedlist/list.md index c0fd0f5ad..acbcac60a 100644 --- a/ru/docs/chapter_array_and_linkedlist/list.md +++ b/ru/docs/chapter_array_and_linkedlist/list.md @@ -19,7 +19,7 @@ comments: true ### 1.   Инициализация списка -Обычно мы используем два способа инициализации: "без начальных значений" и "с начальными значениями": +Обычно используются два способа инициализации: без начальных значений и с начальными значениями: === "Python" @@ -157,7 +157,7 @@ comments: true ### 2.   Доступ к элементам -Список по своей сути является массивом, поэтому доступ к элементам и их обновление можно выполнять за $O(1)$ времени, что очень эффективно. +Поскольку в этом разделе список рассматривается как структура на основе динамического массива, доступ к элементам и их обновление можно выполнять за $O(1)$ времени, что очень эффективно. === "Python" @@ -288,7 +288,7 @@ comments: true ### 3.   Вставка и удаление элементов -По сравнению с массивами список позволяет свободно добавлять и удалять элементы. Добавление элемента в конец списка имеет временную сложность $O(1)$ , но операции вставки и удаления по-прежнему имеют ту же эффективность, что и у массива, то есть $O(n)$ . +В отличие от массива список позволяет свободно добавлять и удалять элементы. Добавление элемента в конец списка имеет временную сложность $O(1)$ , но операции вставки и удаления по-прежнему имеют ту же эффективность, что и у массива, то есть $O(n)$ . === "Python" @@ -743,7 +743,7 @@ comments: true ### 5.   Конкатенация списков -Если дан новый список `nums1` , мы можем присоединить его к хвосту исходного списка. +Создав новый список `nums1` , мы можем присоединить его к хвосту исходного списка. === "Python" @@ -854,7 +854,7 @@ comments: true ### 6.   Сортировка списка -После сортировки списка мы сможем применять алгоритмы "бинарный поиск" и "два указателя", которые очень часто встречаются в задачах по массивам. +После сортировки списка мы сможем применять алгоритмы "двоичный поиск" и "два указателя", которые очень часто встречаются в задачах по массивам. === "Python" @@ -952,12 +952,12 @@ comments: true ## 4.3.2   Реализация списка -Во многих языках программирования списки встроены в стандартную библиотеку, например в Java, C++ и Python. Их реализация довольно сложна, а настройки параметров тщательно продуманы: начальная емкость, коэффициент расширения и так далее. Если тебе интересно, стоит заглянуть в исходный код. +Во многих языках программирования списки встроены в стандартную библиотеку, например в Java, C++, Python и других языках. Их реализация довольно сложна, а настройки параметров тщательно продуманы: начальная емкость, коэффициент расширения и так далее. Если это интересно, стоит заглянуть в исходный код. Чтобы лучше понять принцип работы списка, попробуем реализовать его упрощенную версию, в которой есть три ключевых аспекта проектирования. - **Начальная емкость**: выбрать разумную начальную емкость внутреннего массива. В этом примере мы берем 10. -- **Учет количества элементов**: объявить переменную `size` , которая будет хранить текущее число элементов в списке и обновляться в реальном времени при вставке и удалении элементов. С помощью этой переменной можно находить конец списка и понимать, требуется ли расширение. +- **Учет количества элементов**: объявить переменную `size` , которая будет хранить текущее число элементов в списке и обновляться в реальном времени при вставке и удалении элементов. С помощью этой переменной можно определять конец списка и понимать, требуется ли расширение. - **Механизм расширения**: если при вставке элементов емкость списка исчерпана, нужно выполнить расширение. Для этого сначала создается больший массив с учетом коэффициента расширения, а затем все элементы текущего массива по порядку переносятся в новый. В этом примере мы считаем, что каждый раз массив расширяется в 2 раза. === "Python" diff --git a/ru/docs/chapter_array_and_linkedlist/ram_and_cache.md b/ru/docs/chapter_array_and_linkedlist/ram_and_cache.md index bc2913e15..eb7fee50b 100644 --- a/ru/docs/chapter_array_and_linkedlist/ram_and_cache.md +++ b/ru/docs/chapter_array_and_linkedlist/ram_and_cache.md @@ -4,13 +4,13 @@ comments: true # 4.4   Оперативная память и кэш * -В первых двух разделах этой главы мы разобрали массивы и связные списки - две фундаментальные и важные структуры данных, которые соответственно представляют две физические структуры хранения: "непрерывное хранение" и "разрозненное хранение". +В первых двух разделах этой главы мы разобрали массивы и связные списки - две базовые и важные структуры данных, которые представляют соответственно непрерывное хранение и разрозненное хранение. -На практике **физическая структура во многом определяет, насколько эффективно программа использует память и кэш**, а это, в свою очередь, влияет на общую производительность алгоритмической программы. +На практике **физическая структура во многом определяет, насколько эффективно программа использует память и кэш**, а это, в свою очередь, влияет на общую производительность алгоритма. ## 4.4.1   Устройства хранения данных в компьютере -В компьютере есть три типа устройств хранения данных: жесткий диск (hard disk) , оперативная память (random-access memory, RAM) и кэш-память (cache memory) . В таблице 4-2 показаны их различные роли и характеристики производительности в компьютерной системе. +В компьютере есть три типа устройств хранения данных: жесткий диск (hard disk) , оперативная память (random-access memory, RAM) и кэш-память (cache memory) . В таблице 4-2 показаны их различные роли и характеристики в компьютерной системе.

Таблица 4-2   Устройства хранения данных в компьютере

@@ -22,13 +22,13 @@ comments: true | Энергозависимость | Данные не теряются после отключения питания | Данные теряются после отключения питания | Данные теряются после отключения питания | | Емкость | Большая, уровень TB | Меньшая, уровень GB | Очень малая, уровень MB | | Скорость | Низкая, от сотен до тысяч MB/s | Высокая, десятки GB/s | Очень высокая, десятки и сотни GB/s | -| Цена (юани) | Дешевый, от долей юаня до нескольких юаней за GB | Дорогая, десятки и сотни юаней за GB | Очень дорогой, входит в стоимость упаковки CPU | +| Цена | Низкая, единицы валюты за GB | Высокая, десятки и сотни валютных единиц за GB | Очень высокая, входит в стоимость CPU | Компьютерную систему хранения можно представить в виде пирамиды, показанной на рисунке 4-9. Чем ближе устройство хранения к вершине пирамиды, тем оно быстрее, тем меньше его емкость и тем выше его стоимость. Такая многоуровневая конструкция возникла не случайно, а стала результатом тщательных инженерных компромиссов. -- **Жесткий диск трудно заменить оперативной памятью**. Во-первых, данные в оперативной памяти исчезают после отключения питания, поэтому она не подходит для долговременного хранения. Во-вторых, память стоит в десятки раз дороже жесткого диска, что мешает ее широкому применению в потребительском сегменте. +- **Жесткий диск трудно заменить оперативной памятью**. Во-первых, данные в оперативной памяти исчезают после отключения питания, поэтому она не подходит для долговременного хранения. Во-вторых, память стоит в разы дороже жесткого диска, что мешает ее широкому применению. - **Кэш не может одновременно быть и очень большим, и очень быстрым**. По мере роста емкости кэшей L1, L2 и L3 их физический размер увеличивается, расстояние до ядра CPU становится больше, время передачи данных растет, а задержка доступа к элементам увеличивается. При текущем уровне технологий многоуровневая структура кэша является лучшим балансом между емкостью, скоростью и стоимостью. ![Система хранения данных компьютера](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" } @@ -37,11 +37,11 @@ comments: true !!! tip - Иерархия памяти компьютера отражает тонкий баланс между скоростью, емкостью и стоимостью. На самом деле подобные компромиссы встречаются почти во всех отраслях инженерии: приходится искать оптимальный баланс между преимуществами и ограничениями. + Иерархия памяти компьютера отражает тонкий баланс между скоростью, емкостью и стоимостью. Подобные компромиссы встречаются почти во всех областях инженерии: приходится искать оптимальный баланс между преимуществами и ограничениями. В итоге **жесткий диск используется для долговременного хранения больших объемов данных, оперативная память - для временного хранения данных, с которыми программа работает прямо сейчас, а кэш - для хранения часто используемых данных и инструкций**, чтобы ускорять выполнение программ. Все три уровня работают совместно и обеспечивают эффективную работу компьютерной системы. -Как показано на рисунке 4-10, во время выполнения программы данные читаются с жесткого диска в оперативную память, а затем используются CPU в вычислениях. Кэш можно рассматривать как часть CPU: **он интеллектуально подгружает данные из оперативной памяти**, обеспечивая CPU высокоскоростной доступ и тем самым значительно ускоряя выполнение программы и уменьшая зависимость от более медленной RAM. +Как показано на рисунке 4-10, во время выполнения программы данные читаются с жесткого диска в оперативную память, а затем используются CPU в вычислениях. Кэш можно рассматривать как часть CPU: **он подгружает данные из оперативной памяти**, обеспечивая CPU высокоскоростной доступ и тем самым значительно ускоряя выполнение программы и уменьшая зависимость от более медленной RAM. ![Поток данных между жестким диском, RAM и кэшем](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } @@ -51,7 +51,7 @@ comments: true С точки зрения использования пространства памяти массивы и связные списки имеют свои преимущества и ограничения. -С одной стороны, **память ограничена, и один и тот же участок памяти не может совместно использоваться несколькими программами**, поэтому нам хочется, чтобы структуры данных использовали пространство как можно эффективнее. Элементы массива расположены плотно и не требуют дополнительного места для хранения ссылок (указателей) между узлами списка, поэтому массивы эффективнее по памяти. Однако массиву нужно сразу выделить достаточно большой непрерывный участок памяти, что может приводить к потерям пространства, а его расширение требует дополнительных затрат времени и памяти. Напротив, связные списки выполняют динамическое выделение и освобождение памяти "по узлам", что дает большую гибкость. +С одной стороны, **память ограничена, и один и тот же участок памяти не может совместно использоваться несколькими программами**, поэтому нам хочется, чтобы структуры данных использовали пространство как можно эффективнее. Элементы массива расположены плотно и не требуют дополнительного места для хранения ссылок (указателей) между узлами списка, поэтому массивы эффективнее по памяти. Однако массиву нужно сразу выделить достаточно большой непрерывный участок памяти, что может приводить к потерям пространства, а его расширение требует дополнительных затрат времени и памяти. Напротив, связные списки выделяют и освобождают память на уровне узлов, что дает большую гибкость. С другой стороны, во время выполнения программы **при многократном выделении и освобождении памяти фрагментация свободной памяти становится все более серьезной**, что снижает эффективность ее использования. Массивы из-за непрерывного хранения относительно менее подвержены фрагментации. Напротив, элементы связного списка распределены по памяти, и частые операции вставки и удаления легче приводят к фрагментации. @@ -59,7 +59,7 @@ comments: true Хотя по объему кэш намного меньше оперативной памяти, он значительно быстрее и играет критически важную роль в скорости выполнения программ. Поскольку объем кэша ограничен и в нем можно хранить только небольшую долю часто используемых данных, когда CPU пытается обратиться к данным, которых в кэше нет, происходит промах кэша (cache miss) , и CPU вынужден загружать нужные данные из более медленной памяти. -Очевидно, что **чем меньше "промахов кэша", тем выше эффективность чтения и записи данных CPU**, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют коэффициентом попадания в кэш (cache hit rate) ; этот показатель обычно используют для оценки эффективности кэша. +Очевидно, что **чем меньше промахов кэша, тем выше эффективность чтения и записи данных CPU**, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют коэффициентом попадания в кэш (cache hit rate) ; этот показатель обычно используют для оценки эффективности кэша. Чтобы добиться как можно большей эффективности, кэш использует следующие механизмы загрузки данных. @@ -71,8 +71,8 @@ comments: true На практике **массивы и связные списки по-разному используют кэш**, и это проявляется в нескольких аспектах. - **Занимаемое пространство**: элементы связного списка занимают больше места, чем элементы массива, поэтому в кэше помещается меньше полезных данных. -- **Строки кэша**: данные списка разбросаны по памяти, а кэш загружает данные "строками", поэтому доля бесполезно загружаемых данных оказывается выше. -- **Механизм предвыборки**: шаблон доступа к данным у массивов более "предсказуем", чем у списков, то есть системе легче угадать, какие данные понадобятся следующими. +- **Строки кэша**: данные списка разбросаны по памяти, а кэш загружает данные строками, поэтому доля бесполезно загружаемых данных оказывается выше. +- **Механизм предвыборки**: шаблон доступа к данным у массивов более предсказуем, чем у списков, то есть системе легче угадать, какие данные понадобятся следующими. - **Пространственная локальность**: массив хранится в компактной области памяти, поэтому данные рядом с уже загруженными с большей вероятностью скоро будут использованы. В целом **массивы имеют более высокий коэффициент попадания в кэш, поэтому по эффективности операций они обычно превосходят связные списки**. Именно поэтому при решении алгоритмических задач структуры данных на основе массивов часто оказываются предпочтительнее. diff --git a/ru/docs/chapter_array_and_linkedlist/summary.md b/ru/docs/chapter_array_and_linkedlist/summary.md index c46c36973..1cc5f2265 100644 --- a/ru/docs/chapter_array_and_linkedlist/summary.md +++ b/ru/docs/chapter_array_and_linkedlist/summary.md @@ -6,11 +6,11 @@ comments: true ### 1.   Ключевые выводы -- Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывной области и хранение в разрозненных областях. Их свойства во многом взаимно дополняют друг друга. -- Массив поддерживает произвольный доступ и занимает меньше памяти; однако вставка и удаление элементов в нем неэффективны, а длина после инициализации неизменяема. -- Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину; однако доступ к узлам неэффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки. +- Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывном пространстве и хранение в разрозненном пространстве. Их свойства во многом взаимно дополняют друг друга. +- Массив поддерживает произвольный доступ и занимает меньше памяти; однако вставка и удаление элементов в нем неэффективны, а длина после инициализации фиксирована. +- Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину; однако доступ к узлам менее эффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки. - Список - это упорядоченная коллекция элементов, поддерживающая добавление, удаление, поиск и изменение, и обычно реализуемая на основе динамического массива. Он сохраняет преимущества массива и при этом может гибко менять длину. -- Появление списка значительно повысило практическую полезность массива, хотя это и может приводить к потерям части памяти. +- Появление списка значительно повысило практическую ценность массива, хотя это и может приводить к потере части памяти. - Во время работы программы данные в основном хранятся в оперативной памяти. Массив обеспечивает более высокую эффективность использования пространства памяти, а связный список дает большую гибкость в использовании памяти. - Кэш, используя строки кэша, механизм предвыборки, а также пространственную и временную локальность, предоставляет CPU быстрый доступ к данным и заметно повышает эффективность выполнения программ. - Поскольку массивы обычно имеют более высокий коэффициент попадания в кэш, они в большинстве случаев работают эффективнее списков. При выборе структуры данных нужно исходить из конкретных требований и сценариев. diff --git a/ru/docs/chapter_backtracking/backtracking_algorithm.md b/ru/docs/chapter_backtracking/backtracking_algorithm.md index 61e56658f..fcaca6a69 100644 --- a/ru/docs/chapter_backtracking/backtracking_algorithm.md +++ b/ru/docs/chapter_backtracking/backtracking_algorithm.md @@ -6,7 +6,7 @@ comments: true Алгоритм поиска с возвратом (backtracking algorithm) - это метод решения задач путем полного перебора. Его основная идея состоит в том, чтобы, начиная с некоторого исходного состояния, грубо перебрать все возможные решения, записывать корректные решения и продолжать поиск до тех пор, пока решение не будет найдено или пока не будут исчерпаны все возможные варианты. -Обычно алгоритмы поиска с возвратом используют "поиск в глубину" для обхода пространства решений. В главе "Бинарные деревья" мы уже упоминали, что прямой, симметричный и обратный обходы относятся к поиску в глубину. Теперь мы на основе прямого обхода построим задачу backtracking и постепенно разберем принцип работы этого алгоритма. +Обычно алгоритмы поиска с возвратом используют обход в глубину для обхода пространства решений. В главе "Бинарные деревья" мы уже упоминали, что прямой, симметричный и обратный обходы относятся к обходу в глубину. Теперь мы на основе прямого обхода построим задачу поиска с возвратом и постепенно разберем принцип работы этого алгоритма. !!! question "Пример 1" @@ -243,7 +243,7 @@ comments: true ## 13.1.1   Попытка и откат -**Алгоритм называется backtracking, потому что при поиске в пространстве решений он использует стратегию "попытка" и "откат"**. Когда в процессе поиска алгоритм приходит в состояние, из которого нельзя двигаться дальше или нельзя получить удовлетворяющее условиям решение, он отменяет предыдущий выбор, возвращается к более раннему состоянию и пробует другие возможные варианты. +**Алгоритм называется поиском с возвратом, потому что при поиске в пространстве решений он использует стратегию "попытка" и "откат"**. Когда в процессе поиска алгоритм приходит в состояние, из которого нельзя двигаться дальше или нельзя получить удовлетворяющее условиям решение, он отменяет предыдущий выбор, возвращается к более раннему состоянию и пробует другие возможные варианты. Для примера 1 посещение каждого узла представляет собой "попытку", а прохождение листового узла или возврат к родителю через `return` означает "откат". @@ -589,7 +589,7 @@ comments: true ## 13.1.2   Обрезка -Сложные задачи backtracking обычно содержат одно или несколько ограничений, **которые часто можно использовать для "обрезки"**. +Сложные задачи поиска с возвратом обычно содержат одно или несколько ограничений, **которые часто можно использовать для "обрезки"**. !!! question "Пример 3" @@ -902,7 +902,7 @@ comments: true
-Термин "обрезка" очень нагляден. Как показано на рисунке 13-3, во время поиска **мы "срезаем" ветви поиска, не удовлетворяющие ограничениям** , тем самым избегая множества бессмысленных попыток и повышая эффективность поиска. +Термин "обрезка" очень нагляден. Как показано на рисунке 13-3, во время поиска **мы отсекаем ветви, не удовлетворяющие ограничениям** , тем самым избегая множества бессмысленных попыток и повышая эффективность поиска. ![Обрезка по условиям задачи](backtracking_algorithm.assets/preorder_find_constrained_paths.png){ class="animation-figure" } @@ -1935,13 +1935,13 @@ comments: true

Рисунок 13-4   Сравнение поиска при сохранении и удалении return

-По сравнению с реализацией на основе прямого обхода, версия на основе общего каркаса backtracking выглядит более громоздкой, но при этом обладает лучшей универсальностью. На практике **многие задачи backtracking можно решать в рамках этого каркаса**. Для этого нужно лишь определить `state` и `choices` под конкретную задачу и реализовать соответствующие методы каркаса. +По сравнению с реализацией на основе прямого обхода, версия на основе общего каркаса поиска с возвратом выглядит более громоздкой, но при этом обладает лучшей универсальностью. На практике **многие задачи поиска с возвратом можно решать в рамках этого каркаса**. Для этого нужно лишь определить `state` и `choices` под конкретную задачу и реализовать соответствующие методы каркаса. ## 13.1.4   Часто используемые термины -Чтобы яснее анализировать алгоритмические задачи, подытожим значения часто используемых терминов backtracking и сопоставим их с примером 3, как показано в таблице 13-1. +Чтобы яснее анализировать алгоритмические задачи, подытожим значения часто используемых терминов поиска с возвратом и сопоставим их с примером 3, как показано в таблице 13-1. -

Таблица 13-1   Часто используемые термины алгоритма backtracking

+

Таблица 13-1   Часто используемые термины алгоритма поиска с возвратом

@@ -1958,23 +1958,23 @@ comments: true !!! tip - Такие понятия, как задача, решение и состояние, являются общими и встречаются не только в backtracking, но и в divide and conquer, динамическом программировании, жадных алгоритмах и других темах. + Такие понятия, как задача, решение и состояние, являются общими и встречаются не только в поиске с возвратом, но и в "разделяй и властвуй", динамическом программировании, жадных алгоритмах и других темах. ## 13.1.5   Преимущества и ограничения -Алгоритм поиска с возвратом по своей сути является алгоритмом поиска в глубину, который перебирает все возможные решения, пока не найдет удовлетворяющее условиям. Преимущество этого подхода в том, что он позволяет находить все возможные решения и при разумной обрезке может работать весьма эффективно. +Алгоритм поиска с возвратом по своей сути представляет собой алгоритм обхода в глубину, который перебирает все возможные решения, пока не найдет удовлетворяющее условиям. Преимущество этого подхода в том, что он позволяет находить все возможные решения и при разумной обрезке может работать весьма эффективно. -Однако при работе с большими или сложными задачами **эффективность backtracking может оказаться неприемлемой**. +Однако при работе с большими или сложными задачами **эффективность поиска с возвратом может оказаться неприемлемой**. -- **Время**: backtracking обычно требует обхода всех возможных состояний пространства состояний, и его временная сложность может достигать экспоненциального или факториального порядка. +- **Время**: поиск с возвратом обычно требует обхода всех возможных состояний пространства состояний, и его временная сложность может достигать экспоненциального или факториального порядка. - **Память**: при рекурсивных вызовах нужно хранить текущее состояние (например, путь, вспомогательные переменные для обрезки и т.д.), поэтому при большой глубине рекурсии потребность в памяти может стать значительной. -Тем не менее **backtracking по-прежнему остается лучшим решением для некоторых поисковых задач и задач удовлетворения ограничений**. В таких задачах заранее невозможно предсказать, какие выборы приведут к эффективному решению, поэтому приходится перебирать все возможные варианты. В этой ситуации **ключевым становится вопрос оптимизации эффективности** , и для этого обычно используют две стратегии. +Тем не менее **поиск с возвратом по-прежнему остается лучшим решением для некоторых поисковых задач и задач удовлетворения ограничений**. В таких задачах заранее невозможно предсказать, какие выборы приведут к эффективному решению, поэтому приходится перебирать все возможные варианты. В этой ситуации **ключевым становится вопрос оптимизации эффективности** , и для этого обычно используют две стратегии. - **Обрезка**: избегать поиска по тем путям, которые заведомо не приведут к решению, тем самым экономя время и память. - **Эвристический поиск**: вводить во время поиска дополнительные стратегии или оценки, чтобы в первую очередь исследовать пути, наиболее вероятно ведущие к эффективному решению. -## 13.1.6   Типичные задачи backtracking +## 13.1.6   Типичные задачи поиска с возвратом Алгоритм поиска с возвратом можно использовать для решения множества поисковых задач, задач удовлетворения ограничений и задач комбинаторной оптимизации. @@ -1996,7 +1996,7 @@ comments: true - Задача коммивояжера: начиная из некоторой вершины графа, требуется посетить все остальные вершины ровно по одному разу и вернуться в исходную вершину, найдя при этом кратчайший путь. - Задача о максимальной клике: дан неориентированный граф; требуется найти в нем максимальный полный подграф, то есть подграф, в котором любая пара вершин соединена ребром. -Обратите внимание: для многих задач комбинаторной оптимизации backtracking не является оптимальным способом решения. +Стоит отметить: для многих задач комбинаторной оптимизации поиск с возвратом не является оптимальным способом решения. - Задача о рюкзаке 0-1 обычно решается с помощью динамического программирования, что дает более высокую временную эффективность. - Задача коммивояжера является известной NP-Hard задачей; для ее решения часто используют генетические алгоритмы, муравьиные алгоритмы и другие методы. diff --git a/ru/docs/chapter_backtracking/index.md b/ru/docs/chapter_backtracking/index.md index 5a390f801..187371e62 100644 --- a/ru/docs/chapter_backtracking/index.md +++ b/ru/docs/chapter_backtracking/index.md @@ -18,5 +18,5 @@ icon: material/map-marker-path - [13.1   Алгоритм поиска с возвратом](backtracking_algorithm.md) - [13.2   Задача о перестановках](permutations_problem.md) - [13.3   Задача о сумме подмножеств](subset_sum_problem.md) -- [13.4   Задача о $n$ ферзях](n_queens_problem.md) +- [13.4   Задача о n ферзях](n_queens_problem.md) - [13.5   Резюме](summary.md) diff --git a/ru/docs/chapter_backtracking/n_queens_problem.md b/ru/docs/chapter_backtracking/n_queens_problem.md index 422a45f5a..4c8da05a5 100644 --- a/ru/docs/chapter_backtracking/n_queens_problem.md +++ b/ru/docs/chapter_backtracking/n_queens_problem.md @@ -8,7 +8,7 @@ comments: true Согласно правилам шахмат ферзь может атаковать фигуры, находящиеся с ним на одной строке, в одном столбце или на одной диагонали. Даны $n$ ферзей и шахматная доска размера $n \times n$ ; требуется найти такие расстановки, при которых ни одна пара ферзей не может атаковать друг друга. -Как показано на рисунке 13-15, при $n = 4$ существует два решения. С точки зрения backtracking доска размера $n \times n$ содержит $n^2$ клеток, которые образуют все возможные выборы `choices` . По мере поочередного размещения ферзей состояние доски непрерывно меняется, и текущее содержимое доски образует состояние `state` . +Как показано на рисунке 13-15, при $n = 4$ существует два решения. С точки зрения поиска с возвратом доска размера $n \times n$ содержит $n^2$ клеток, которые образуют все возможные выборы `choices` . По мере поочередного размещения ферзей состояние доски непрерывно меняется, и текущее содержимое доски образует состояние `state` . ![Решения задачи о 4 ферзях](n_queens_problem.assets/solution_4_queens.png){ class="animation-figure" } diff --git a/ru/docs/chapter_backtracking/permutations_problem.md b/ru/docs/chapter_backtracking/permutations_problem.md index e49eaced7..ca3bce57b 100644 --- a/ru/docs/chapter_backtracking/permutations_problem.md +++ b/ru/docs/chapter_backtracking/permutations_problem.md @@ -26,9 +26,9 @@ comments: true Дан массив целых чисел, в котором нет повторяющихся элементов. Верните все возможные перестановки. -С точки зрения backtracking **процесс построения перестановок можно представить как результат последовательности выборов**. Пусть входной массив равен $[1, 2, 3]$ ; если мы сначала выберем $1$ , затем $3$ , а потом $2$ , то получим перестановку $[1, 3, 2]$ . Откат означает отмену одного из выборов с последующей попыткой других вариантов. +С точки зрения поиска с возвратом **процесс построения перестановок можно представить как результат последовательности выборов**. Пусть входной массив равен $[1, 2, 3]$ ; если мы сначала выберем $1$ , затем $3$ , а потом $2$ , то получим перестановку $[1, 3, 2]$ . Откат здесь означает отмену одного из выборов с последующей попыткой других вариантов. -С точки зрения кода backtracking множество кандидатов `choices` состоит из всех элементов входного массива, а состояние `state` - из элементов, уже выбранных к текущему моменту. Обратите внимание, что каждый элемент разрешено выбирать только один раз, **поэтому все элементы в `state` должны быть уникальны**. +С точки зрения кода поиска с возвратом множество кандидатов `choices` состоит из всех элементов входного массива, а состояние `state` - из элементов, уже выбранных к текущему моменту. Поскольку каждый элемент разрешено выбирать только один раз, **все элементы в `state` должны быть уникальны**. Как показано на рисунке 13-5, процесс поиска можно развернуть в дерево рекурсии, где каждый узел представляет текущее состояние `state` . Начиная от корня, после трех раундов выбора мы попадаем в листья, и каждый лист соответствует одной перестановке. @@ -53,7 +53,7 @@ comments: true ### 2.   Реализация кода -После прояснения всей логики можно просто "заполнить пропуски" в шаблоне backtracking. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри `backtrack()` : +После прояснения всей логики можно просто "заполнить пропуски" в шаблоне поиска с возвратом. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри `backtrack()` : === "Python" @@ -565,7 +565,7 @@ comments: true Точно так же, если в первом раунде выбрать $2$ , то во втором раунде выборы $1$ и $\hat{1}$ снова создадут дублирующиеся ветви, поэтому и в этом случае ветвь $\hat{1}$ нужно отсечь. -По своей сути **наша цель заключается в том, чтобы на каждом раунде выбора каждый из нескольких равных элементов выбирался только один раз**. +Иначе говоря, **наша цель заключается в том, чтобы на каждом раунде выбора каждый из нескольких равных элементов выбирался только один раз**. ![Обрезка повторяющихся перестановок](permutations_problem.assets/permutations_ii_pruning.png){ class="animation-figure" } diff --git a/ru/docs/chapter_backtracking/subset_sum_problem.md b/ru/docs/chapter_backtracking/subset_sum_problem.md index 10c822f41..ab09f9729 100644 --- a/ru/docs/chapter_backtracking/subset_sum_problem.md +++ b/ru/docs/chapter_backtracking/subset_sum_problem.md @@ -10,7 +10,7 @@ comments: true Дан массив положительных целых чисел `nums` и целое положительное значение `target` . Найдите все возможные комбинации, сумма элементов которых равна `target` . Во входном массиве нет повторяющихся элементов, и каждый элемент можно выбирать неограниченное число раз. Верните эти комбинации в виде списка; в результате не должно быть повторяющихся комбинаций. -Например, для входного множества $\{3, 4, 5\}$ и целевого значения $9$ решениями будут $\{3, 3, 3\}$ и $\{4, 5\}$ . При этом нужно обратить внимание на два обстоятельства. +Например, для входного множества $\{3, 4, 5\}$ и целевого значения $9$ решениями будут $\{3, 3, 3\}$ и $\{4, 5\}$ . При этом важно учитывать два обстоятельства. - Элементы входного множества можно выбирать повторно неограниченное число раз. - Подмножество не различает порядок элементов, поэтому $\{4, 5\}$ и $\{5, 4\}$ считаются одним и тем же подмножеством. @@ -491,7 +491,28 @@ comments: true end end - ### Решить задачу суммы подмножеств I (с повторяющимися подмножествами) ### + ### Алгоритм бэктрекинга: сумма подмножеств I ### + def backtrack(state, target, total, choices, res) + # Если сумма подмножества равна target, записать решение + if total == target + res << state.dup + return + end + + # Перебор всех вариантов выбора + for i in 0...choices.length + # Отсечение: если сумма подмножества превышает target, пропустить этот выбор + next if total + choices[i] > target + # Попытка: сделать выбор и обновить элемент и total + state << choices[i] + # Перейти к следующему выбору + backtrack(state, target, total + choices[i], choices, res) + # Откат: отменить выбор и восстановить предыдущее состояние + state.pop + end + end + + # ## Решить задачу суммы подмножеств I (с повторяющимися подмножествами) ### def subset_sum_i_naive(nums, target) state = [] # Состояние (подмножество) total = 0 # Сумма подмножеств @@ -526,7 +547,7 @@ comments: true 1. Если в первом и втором раундах выбрать соответственно $3$ и $4$ , то будут сгенерированы все подмножества, содержащие эти два элемента, и их можно обозначить как $[3, 4, \dots]$ . 2. После этого, если в первом раунде выбрать $4$ , **то во втором раунде нужно пропустить $3$** , потому что подмножества $[4, 3, \dots]$ полностью дублируют подмножества, уже построенные на шаге `1.` . -Во время поиска выборы на каждом уровне пробуются по одному слева направо, поэтому чем правее ветвь, тем больше ветвей оказывается отсечено. +Во время поиска варианты на каждом уровне пробуются по одному слева направо, поэтому чем правее ветвь, тем больше ветвей оказывается отсечено. 1. В первых двух раундах выбираются $3$ и $5$ , что дает подмножества $[3, 5, \dots]$ . 2. В первых двух раундах выбираются $4$ и $5$ , что дает подмножества $[4, 5, \dots]$ . @@ -1067,11 +1088,11 @@ comments: true
-На рисунке 13-12 показан полный процесс backtracking для массива $[3, 4, 5]$ и целевого значения $9$ . +На рисунке 13-12 показан полный процесс поиска с возвратом для массива $[3, 4, 5]$ и целевого значения $9$ . -![Процесс backtracking для задачи о сумме подмножеств I](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" } +![Процесс поиска с возвратом для задачи о сумме подмножеств I](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" } -

Рисунок 13-12   Процесс backtracking для задачи о сумме подмножеств I

+

Рисунок 13-12   Процесс поиска с возвратом для задачи о сумме подмножеств I

## 13.3.2   Учет повторяющихся элементов @@ -1089,7 +1110,7 @@ comments: true ### 1.   Обрезка равных элементов -Чтобы решить эту проблему, **нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз**. Реализуется это довольно изящно: поскольку массив отсортирован, равные элементы стоят рядом. Значит, если в текущем раунде текущий элемент равен соседнему слева, то этот вариант уже был рассмотрен, и текущий элемент нужно пропустить. +Чтобы решить эту проблему, **нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз**. Реализуется это довольно естественно: поскольку массив отсортирован, равные элементы стоят рядом. Значит, если в текущем раунде текущий элемент равен соседнему слева, то этот вариант уже был рассмотрен, и текущий элемент нужно пропустить. Одновременно **по условию этой задачи каждый элемент массива можно выбрать только один раз**. К счастью, это ограничение тоже можно реализовать через переменную `start` : после выбора элемента $x_i$ следующий раунд начинается с индекса $i + 1$ . Так мы одновременно убираем повторяющиеся подмножества и исключаем повторный выбор одного и того же элемента. @@ -1678,8 +1699,8 @@ comments: true
-На рисунке 13-14 показан процесс backtracking для массива $[4, 4, 5]$ и целевого значения $9$ . В нем используются четыре вида обрезки. Попробуйте сопоставить рисунок с комментариями в коде, чтобы понять полный процесс поиска и то, как работает каждый тип обрезки. +На рисунке 13-14 показан процесс поиска с возвратом для массива $[4, 4, 5]$ и целевого значения $9$ . В нем используются четыре вида обрезки. Попробуйте сопоставить рисунок с комментариями в коде, чтобы понять полный процесс поиска и то, как работает каждый тип обрезки. -![Процесс backtracking для задачи о сумме подмножеств II](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" } +![Процесс поиска с возвратом для задачи о сумме подмножеств II](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" } -

Рисунок 13-14   Процесс backtracking для задачи о сумме подмножеств II

+

Рисунок 13-14   Процесс поиска с возвратом для задачи о сумме подмножеств II

diff --git a/ru/docs/chapter_backtracking/summary.md b/ru/docs/chapter_backtracking/summary.md index a6305f9bd..cca2e4eef 100644 --- a/ru/docs/chapter_backtracking/summary.md +++ b/ru/docs/chapter_backtracking/summary.md @@ -7,21 +7,21 @@ comments: true ### 1.   Ключевые выводы - Алгоритм поиска с возвратом по своей сути является методом полного перебора: он ищет решения путем обхода пространства решений в глубину. Во время поиска он фиксирует решения, удовлетворяющие условиям, пока не найдет все такие решения или пока обход не завершится. -- Процесс backtracking состоит из двух частей: попытки и отката. Он с помощью поиска в глубину пробует разные варианты выбора; когда встречается состояние, не удовлетворяющее ограничениям, алгоритм отменяет предыдущий выбор, возвращается к прошлому состоянию и продолжает пробовать другие варианты. Попытка и откат являются двумя противоположными по направлению действиями. -- Задачи backtracking обычно содержат несколько ограничений, которые можно использовать для обрезки. Обрезка позволяет заранее завершать ненужные ветви поиска и тем самым значительно повышать эффективность. -- Алгоритм backtracking в первую очередь применяется для решения поисковых задач и задач с ограничениями. Задачи комбинаторной оптимизации тоже можно решать с его помощью, но для них часто существуют более эффективные или более подходящие методы. +- Процесс поиска с возвратом состоит из двух частей: попытки и отката. Он с помощью поиска в глубину пробует разные варианты выбора; когда встречается состояние, не удовлетворяющее ограничениям, алгоритм отменяет предыдущий выбор, возвращается к прошлому состоянию и продолжает пробовать другие варианты. Попытка и откат являются двумя противоположными по направлению действиями. +- Задачи поиска с возвратом обычно содержат несколько ограничений, которые можно использовать для обрезки. Обрезка позволяет заранее завершать ненужные ветви поиска и тем самым значительно повышать эффективность. +- Алгоритм поиска с возвратом в первую очередь применяется для решения поисковых задач и задач с ограничениями. Задачи комбинаторной оптимизации тоже можно решать с его помощью, но для них часто существуют более эффективные или более подходящие методы. - Задача о перестановках нацелена на поиск всех возможных перестановок элементов данного множества. Мы используем массив для записи того, был ли выбран каждый элемент, и отсекаем ветви, где один и тот же элемент выбирается повторно, чтобы гарантировать однократный выбор каждого элемента. - В задаче о перестановках, если во множестве присутствуют повторяющиеся элементы, в итоговом результате возникнут повторяющиеся перестановки. Поэтому нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз; обычно это реализуется с помощью хеш-множества. -- Цель задачи о сумме подмножеств - найти все подмножества данного множества, сумма которых равна целевому значению. В множестве порядок элементов не важен, однако процесс поиска порождает результаты во всех возможных порядках, из-за чего появляются повторяющиеся подмножества. Поэтому перед запуском backtracking мы сортируем данные и вводим переменную, указывающую начальную точку обхода в каждом раунде, чтобы отсечь ветви, создающие дубликаты. +- Цель задачи о сумме подмножеств - найти все подмножества данного множества, сумма которых равна целевому значению. В множестве порядок элементов не важен, однако процесс поиска порождает результаты во всех возможных порядках, из-за чего появляются повторяющиеся подмножества. Поэтому перед запуском поиска с возвратом мы сортируем данные и вводим переменную, указывающую начальную точку обхода в каждом раунде, чтобы отсечь ветви, создающие дубликаты. - В задаче о сумме подмножеств равные элементы массива также порождают повторяющиеся множества. При наличии предварительной сортировки их можно отсекать, проверяя равенство соседних элементов, и тем самым гарантировать, что в каждом раунде равные элементы будут выбираться только один раз. - Задача о $n$ ферзях состоит в поиске способов разместить $n$ ферзей на доске размера $n \times n$ так, чтобы никакие два ферзя не атаковали друг друга. Ограничения этой задачи включают строки, столбцы, главные диагонали и побочные диагонали. Чтобы выполнить ограничение по строкам, используется построчная стратегия размещения, гарантирующая по одному ферзю в каждой строке. - Обработка ограничений по столбцам и диагоналям устроена похожим образом. Для ограничения по столбцам используется массив, фиксирующий наличие ферзя в каждом столбце. Для диагоналей используются два массива, записывающие наличие ферзей на главных и побочных диагоналях. Основная сложность здесь состоит в том, чтобы найти закономерность индексов строк и столбцов клеток, лежащих на одной и той же главной или побочной диагонали. -### 2.   Q & A +### 2.   Вопросы и ответы **Q**: Как понять связь между поиском с возвратом и рекурсией? -В целом backtracking - это скорее "алгоритмическая стратегия", а рекурсия больше похожа на "инструмент". +В целом поиск с возвратом - это скорее "алгоритмическая стратегия", а рекурсия больше похожа на "инструмент". -- Алгоритмы поиска с возвратом обычно реализуются на основе рекурсии. Однако backtracking - это лишь один из вариантов применения рекурсии, а именно ее использование в поисковых задачах. -- Структура рекурсии отражает парадигму разбиения на подзадачи и часто применяется для решения задач divide and conquer, backtracking, динамического программирования (мемоизированной рекурсии) и других подобных задач. +- Алгоритмы поиска с возвратом обычно реализуются на основе рекурсии. Однако поиск с возвратом - это лишь один из вариантов применения рекурсии, а именно ее использование в поисковых задачах. +- Структура рекурсии отражает парадигму разбиения на подзадачи и часто применяется для решения задач "разделяй и властвуй", поиска с возвратом, динамического программирования (мемоизированной рекурсии) и других подобных задач. diff --git a/ru/docs/chapter_computational_complexity/index.md b/ru/docs/chapter_computational_complexity/index.md index fe06f236c..b1e4155d4 100644 --- a/ru/docs/chapter_computational_complexity/index.md +++ b/ru/docs/chapter_computational_complexity/index.md @@ -9,9 +9,9 @@ icon: material/timer-sand !!! abstract - Анализ сложности подобен пространственно-временному проводнику в огромной вселенной алгоритмов. + Анализ сложности подобен пространственно-временному проводнику в необъятной вселенной алгоритмов. - Он ведет нас вглубь двух измерений - времени и пространства, помогая искать более изящные решения. + Он ведет нас в глубину двух измерений - времени и пространства, помогая искать более изящные решения. ## Содержание главы diff --git a/ru/docs/chapter_computational_complexity/iteration_and_recursion.md b/ru/docs/chapter_computational_complexity/iteration_and_recursion.md index 5b9a0747f..9b1206ae2 100644 --- a/ru/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/ru/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -4,17 +4,17 @@ comments: true # 2.2   Итерация и рекурсия -В алгоритмах очень часто приходится многократно выполнять одну и ту же задачу, и это тесно связано с анализом сложности. Поэтому, прежде чем переходить к временной и пространственной сложности, давай сначала разберемся, как в программах организуется повторяющееся выполнение задач, то есть с двумя базовыми управляющими структурами: итерацией и рекурсией. +В алгоритмах часто требуется повторное выполнение определенной задачи, что тесно связано с анализом сложности. Поэтому, прежде чем перейти к обсуждению временной и пространственной сложности, рассмотрим, как реализовать повторное выполнение задач в программе, а именно две основные структуры управления программой: итерацию и рекурсию. ## 2.2.1   Итерация -Итерация (iteration) - это управляющая структура, предназначенная для многократного выполнения некоторой задачи. При итерации программа повторно выполняет определенный фрагмент кода при соблюдении некоторого условия, пока это условие не перестанет выполняться. +Итерация (iteration) - это структура управления, которая позволяет повторно выполнять определенную задачу. В итерации программа повторяет выполнение определенного участка кода, пока выполняется определенное условие. ### 1.   Цикл for -Цикл `for` - одна из самых распространенных форм итерации, **она хорошо подходит в тех случаях, когда число повторений известно заранее**. +Цикл `for` - одна из наиболее распространенных форм итерации, **которая подходит для использования, когда количество итераций известно заранее**. -Следующая функция реализует вычисление суммы $1 + 2 + \dots + n$ на основе цикла `for` , а результат сохраняется в переменной `res` . Обрати внимание, что в Python `range(a, b)` соответствует "лево-замкнутому, право-открытому" интервалу, то есть перебираются значения $a, a + 1, \dots, b-1$ : +Следующая функция реализует суммирование $1 + 2 + \dots + n$ с использованием цикла `for` , а результат суммирования сохраняется в переменной `res` . Следует отметить, что в Python диапазон `range(a, b)` соответствует левому закрытому, правому открытому интервалу, то есть перебираются значения $a, a + 1, \dots, b-1$ : === "Python" @@ -203,19 +203,19 @@ comments: true
-На рисунке 2-1 показана блок-схема этой функции суммирования. +Ниже представлена блок-схема этой функции суммирования. ![Блок-схема функции суммирования](iteration_and_recursion.assets/iteration.png){ class="animation-figure" }

Рисунок 2-1   Блок-схема функции суммирования

-Число операций в этой функции суммирования пропорционально размеру входных данных $n$ , то есть между ними существует "линейная зависимость". На самом деле **временная сложность как раз и описывает такую "линейную зависимость"**. Соответствующий материал будет подробно разобран в следующем разделе. +Количество операций этой функции суммирования пропорционально размеру входных данных $n$ , или, другими словами, линейно зависит от него. **На самом деле временная сложность описывает именно эту линейную зависимость**. Соответствующий материал будет подробно рассмотрен в следующем разделе. ### 2.   Цикл while -Подобно циклу `for` , цикл `while` тоже является способом реализации итерации. В цикле `while` программа в каждом раунде сначала проверяет условие: если условие истинно, выполнение продолжается, иначе цикл завершается. +Подобно циклу `for` , цикл `while` также представляет собой метод реализации итерации. В цикле `while` программа перед каждой итерацией проверяет условие: если условие истинно, то выполнение продолжается, иначе цикл завершается. -Ниже мы используем цикл `while` для реализации суммы $1 + 2 + \dots + n$ : +Ниже приведен пример реализации суммирования $1 + 2 + \dots + n$ с использованием цикла `while` : === "Python" @@ -433,9 +433,9 @@ comments: true
-**Цикл `while` обладает большей свободой, чем цикл `for` **. В цикле `while` мы можем свободно задавать шаги инициализации и обновления условной переменной. +**Цикл `while` обладает большей степенью свободы по сравнению с циклом `for` **. В цикле `while` можно свободно управлять инициализацией и обновлением условной переменной. -Например, в следующем коде условная переменная $i$ обновляется два раза за один проход, и такой случай уже не слишком удобно выражать через цикл `for` : +Например, в следующем коде условная переменная $i$ обновляется дважды на каждой итерации, что затруднительно сделать с использованием цикла `for` : === "Python" @@ -656,7 +656,21 @@ comments: true === "Ruby" ```ruby title="iteration.rb" - ### Цикл while (двойное обновление) ### + ### Цикл while ### + def while_loop(n) + res = 0 + i = 1 # Инициализация условной переменной + + # Циклическое суммирование 1, 2, ..., n-1, n + while i <= n + res += i + i += 1 # Обновить условную переменную + end + + res + end + + # ## Цикл while (двойное обновление) ### def while_loop_ii(n) res = 0 i = 1 # Инициализация условной переменной @@ -678,11 +692,11 @@ comments: true
-В целом **код с `for` обычно компактнее, а `while` более гибок**; обе конструкции позволяют реализовывать итерационные структуры. Выбор между ними должен определяться требованиями конкретной задачи. +В целом **код с использованием цикла `for` более компактный, а цикл `while` более гибкий**. Но они оба могут реализовать итерационную структуру. Выбор между ними определяется требованиями конкретной задачи. ### 3.   Вложенные циклы -Мы можем вкладывать одну циклическую структуру в другую; ниже показан пример на основе цикла `for` : +Внутрь одной циклической структуры можно вложить другую, например используя два цикла `for` : === "Python" @@ -913,30 +927,30 @@ comments: true
-На рисунке 2-2 показана блок-схема такого вложенного цикла. +Ниже приведена блок-схема такого вложенного цикла. ![Блок-схема вложенного цикла](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" }

Рисунок 2-2   Блок-схема вложенного цикла

-В этом случае число операций функции пропорционально $n^2$ , то есть время работы алгоритма и размер входных данных $n$ находятся в "квадратичной зависимости". +В этом случае количество выполненных действий пропорционально $n^2$ , или, другими словами, время выполнения алгоритма и размер входных данных $n$ находятся в квадратичной зависимости. -Мы можем продолжать добавлять вложенные циклы, и каждое новое вложение будет означать очередное "повышение размерности", увеличивая временную сложность до "кубической зависимости", "зависимости четвертой степени" и так далее. +Можно и дальше добавлять вложенные циклы, тогда каждое вложение будет повышать размерность, увеличивая временную сложность до кубической зависимости, зависимости четвертой степени и так далее. ## 2.2.2   Рекурсия - Рекурсия (recursion) - это алгоритмическая стратегия, в которой функция решает задачу, вызывая саму себя. В основном она включает две фазы. + Рекурсия (recursion) - это стратегия алгоритма, при которой функция вызывает саму себя для решения задачи. Она включает два основных этапа. -1. **Спуск**: программа все глубже вызывает саму себя, обычно передавая меньшие или более упрощенные параметры, пока не достигнет "условия завершения". -2. **Подъем**: после срабатывания "условия завершения" программа начинает возвращаться от самой глубокой рекурсивной функции вверх, собирая результаты с каждого уровня. +1. **Вызов**: программа постоянно вызывает саму себя, обычно передавая меньшие или более упрощенные параметры, пока не будет достигнуто условие завершения. +2. **Возврат**: после срабатывания условия завершения программа начинает возвращаться из самой глубокой рекурсивной функции, объединяя результаты каждого уровня. -С точки зрения реализации рекурсивный код в основном состоит из трех элементов. +С точки зрения реализации рекурсивный код включает три основных элемента. -1. **Условие завершения**: определяет момент перехода от "спуска" к "подъему". -2. **Рекурсивный вызов**: соответствует "спуску", когда функция вызывает саму себя, обычно с меньшими или более упрощенными параметрами. -3. **Возврат результата**: соответствует "подъему", когда результат текущего уровня рекурсии передается предыдущему. +1. **Условие завершения**: используется для определения момента перехода от вызова к возврату. +2. **Рекурсивный вызов**: соответствует вызову, функция вызывает саму себя, обычно с меньшими или упрощенными параметрами. +3. **Возврат результата**: соответствует возврату, возвращает результат текущего уровня рекурсии на предыдущий уровень. -Посмотри на следующий код: нам достаточно вызвать функцию `recur(n)` , чтобы вычислить $1 + 2 + \dots + n$ : +Рассмотрим следующий код: вызов функции `recur(n)` позволяет вычислить сумму $1 + 2 + \dots + n$ : === "Python" @@ -1136,45 +1150,45 @@ comments: true
-На рисунке 2-3 показан рекурсивный процесс этой функции. +Ниже представлен рекурсивный процесс этой функции. ![Рекурсивный процесс функции суммирования](iteration_and_recursion.assets/recursion_sum.png){ class="animation-figure" }

Рисунок 2-3   Рекурсивный процесс функции суммирования

-Хотя с вычислительной точки зрения итерация и рекурсия могут давать один и тот же результат, **они представляют собой две совершенно разные парадигмы мышления и решения задач**. +Хотя с точки зрения вычислений итерация и рекурсия могут давать одинаковый результат, **они представляют собой совершенно разные парадигмы мышления и решения задач**. -- **Итерация**: решает задачу "снизу вверх". Мы начинаем с самых базовых шагов, а затем многократно повторяем или накапливаем их, пока задача не будет завершена. -- **Рекурсия**: решает задачу "сверху вниз". Исходная задача разбивается на более мелкие подзадачи той же формы. Затем эти подзадачи продолжают разбиваться еще дальше, пока не будет достигнут базовый случай (для которого решение уже известно). +- **Итерация**: решение задачи снизу вверх. Начинаем с самых базовых шагов, которые затем повторяются или накапливаются до завершения задачи. +- **Рекурсия**: решение задачи сверху вниз. Исходная задача разбивается на более мелкие подзадачи, которые имеют ту же форму, что и исходная задача. Далее подзадачи продолжают делиться на еще более мелкие, пока не достигается базовый случай, решение которого известно. -Возьмем в качестве примера указанную выше функцию суммирования и обозначим задачу как $f(n) = 1 + 2 + \dots + n$ . +Рассмотрим в качестве примера вышеупомянутую функцию суммирования, где решается задача $f(n) = 1 + 2 + \dots + n$ . -- **Итерация**: в цикле моделируется процесс суммирования от $1$ до $n$ , и на каждом шаге выполняется операция сложения, в результате чего получается $f(n)$ . -- **Рекурсия**: задача раскладывается на подзадачу $f(n) = n + f(n-1)$ , а затем продолжает раскладываться (рекурсивно) до базового случая $f(1) = 1$ . +- **Итерация**: моделирование процесса суммирования в цикле проходит от $1$ до $n$ , выполняя операцию суммирования на каждом шаге, чтобы получить итоговое значение $f(n)$ . +- **Рекурсия**: последовательное разбиение задачи на подзадачи вида $f(n) = n + f(n - 1)$ до достижения базового случая $f(1) = 1$ . ### 1.   Стек вызовов -Каждый раз, когда рекурсивная функция вызывает сама себя, система выделяет память для нового экземпляра функции, чтобы хранить локальные переменные, адрес возврата и другую информацию. Это приводит к двум последствиям. +Каждый раз, когда рекурсивная функция вызывает саму себя, система выделяет память для нового вызова функции, чтобы хранить локальные переменные, адрес вызова и другую информацию. Это поведение имеет два последствия. -- Контекстные данные функции хранятся в области памяти, называемой "пространством кадра стека", и освобождаются только после возврата функции. Поэтому **рекурсия обычно требует больше памяти, чем итерация**. -- Вызов рекурсивной функции создает дополнительный накладной расход. **Поэтому рекурсия обычно уступает циклам по временной эффективности**. +- Контекстные данные функции хранятся в области памяти, называемой пространством стекового кадра, и освобождаются только после возврата функции. **Поэтому рекурсия обычно требует больше памяти, чем итерация**. +- Рекурсивный вызов функции создает дополнительные накладные расходы. **Поэтому рекурсия обычно менее эффективна по времени, чем цикл**. -Как показано на рисунке 2-4, до срабатывания условия завершения одновременно существует $n$ еще не завершившихся рекурсивных вызовов, а **глубина рекурсии равна $n$** . +Как показано на рисунке 2-4, до срабатывания условия завершения одновременно существует $n$ невозвращенных рекурсивных функций, а **глубина рекурсии равна $n$** . ![Глубина рекурсивного вызова](iteration_and_recursion.assets/recursion_sum_depth.png){ class="animation-figure" }

Рисунок 2-4   Глубина рекурсивного вызова

-На практике разрешенная языком программирования глубина рекурсии обычно ограничена, и слишком глубокая рекурсия может привести к ошибке переполнения стека. +На практике глубина рекурсии, разрешенная языком программирования, обычно ограничена, и слишком глубокая рекурсия может привести к ошибке переполнения стека. ### 2.   Хвостовая рекурсия -Интересно, что **если функция выполняет рекурсивный вызов в самом последнем действии перед возвратом** , то компилятор или интерпретатор может оптимизировать такую функцию так, чтобы по использованию памяти она была сопоставима с итерацией. Такой случай называется хвостовой рекурсией (tail recursion). +Интересно, что **если рекурсивный вызов происходит на последнем шаге перед возвратом функции** , то компилятор или интерпретатор может оптимизировать этот вызов, сделав его по эффективности использования памяти сопоставимым с итерацией. Это называется хвостовой рекурсией (tail recursion). -- **Обычная рекурсия**: когда функция возвращается на предыдущий уровень, ей все еще нужно продолжать выполнять код, поэтому системе приходится сохранять контекст вызова предыдущего уровня. -- **Хвостовая рекурсия**: рекурсивный вызов - это последняя операция перед возвратом, а значит, после возвращения на предыдущий уровень не требуется выполнять дополнительных действий, и системе не нужно сохранять контекст предыдущей функции. +- **Обычная рекурсия**: когда функция возвращается на предыдущий уровень, необходимо продолжить выполнение кода, поэтому системе нужно сохранить контекст предыдущего вызова. +- **Хвостовая рекурсия**: рекурсивный вызов является последней операцией перед возвратом функции, что означает, что после возврата на предыдущий уровень не требуется выполнять другие операции, поэтому системе не нужно сохранять контекст предыдущей функции. -На примере вычисления $1 + 2 + \dots + n$ можно сделать переменную результата `res` параметром функции и тем самым реализовать хвостовую рекурсию: +В качестве примера вычисления суммы $1 + 2 + \dots + n$ можно установить переменную результата `res` в качестве параметра функции, чтобы реализовать хвостовую рекурсию: === "Python" @@ -1349,10 +1363,10 @@ comments: true
-Процесс выполнения хвостовой рекурсии показан на рисунке 2-5. Если сравнить обычную рекурсию и хвостовую рекурсию, то видно, что точка выполнения операции суммирования у них различается. +Процесс выполнения хвостовой рекурсии показан на рисунке 2-5. Сравнивая обычную и хвостовую рекурсии, можно заметить, что точка выполнения операции суммирования у них различается. -- **Обычная рекурсия**: операция суммирования выполняется в процессе "подъема", то есть после возврата с каждого уровня еще нужно выполнить очередное сложение. -- **Хвостовая рекурсия**: операция суммирования выполняется в процессе "спуска", а сам "подъем" сводится лишь к последовательному возврату. +- **Обычная рекурсия**: операция суммирования выполняется в процессе возврата, после каждого возврата необходимо снова выполнить операцию суммирования. +- **Хвостовая рекурсия**: операция суммирования выполняется в процессе вызова, а процесс возврата требует только последовательного возврата. ![Процесс хвостовой рекурсии](iteration_and_recursion.assets/tail_recursion_sum.png){ class="animation-figure" } @@ -1360,22 +1374,22 @@ comments: true !!! tip - Обрати внимание: многие компиляторы и интерпретаторы не поддерживают оптимизацию хвостовой рекурсии. Например, Python по умолчанию такую оптимизацию не выполняет, поэтому даже функция в хвостово-рекурсивной форме все равно может привести к переполнению стека. + Обратите внимание: многие компиляторы и интерпретаторы не поддерживают оптимизацию хвостовой рекурсии. Например, Python по умолчанию такую оптимизацию не выполняет, поэтому даже функция в хвостово-рекурсивной форме все равно может привести к переполнению стека. ### 3.   Дерево рекурсии -При решении алгоритмических задач, связанных с "разделяй и властвуй", рекурсия часто дает более интуитивный способ рассуждения и более читаемый код, чем итерация. Возьмем в качестве примера "последовательность Фибоначчи". +При решении задач, связанных с алгоритмами типа "разделяй и властвуй", рекурсия часто оказывается более интуитивной и читабельной, чем итерация. Рассмотрим в качестве примера последовательность Фибоначчи. !!! question Дана последовательность Фибоначчи $0, 1, 1, 2, 3, 5, 8, 13, \dots$ ; найди $n$-й элемент этой последовательности. -Обозначим $n$-й элемент последовательности Фибоначчи как $f(n)$ . Тогда нетрудно получить два вывода. +Обозначив $n$-й член последовательности Фибоначчи как $f(n)$ , можно сформулировать два утверждения. -- Первые два числа последовательности равны $f(1) = 0$ и $f(2) = 1$ . -- Каждое последующее число равно сумме двух предыдущих, то есть $f(n) = f(n - 1) + f(n - 2)$ . +- Первые два числа последовательности: $f(1) = 0$ и $f(2) = 1$ . +- Каждое число последовательности является суммой двух предыдущих чисел, то есть $f(n) = f(n - 1) + f(n - 2)$ . -Следуя рекуррентному соотношению и используя первые два числа как условия завершения, мы можем написать рекурсивный код. Вызов `fib(n)` даст нам $n$-й элемент последовательности Фибоначчи: +Используя рекурсивные вызовы в соответствии с рекуррентным соотношением и принимая первые два числа за условия остановки, можно написать рекурсивный код. Вызов `fib(n)` позволит получить $n$-й член последовательности Фибоначчи: === "Python" @@ -1575,46 +1589,46 @@ comments: true
-Если посмотреть на приведенный код, внутри функции выполняются два рекурсивных вызова, **а это означает, что один вызов рождает две ветви вызова**. Как показано на рисунке 2-6, при таком продолжении рекурсивных вызовов в итоге получается дерево рекурсии (recursion tree) глубиной $n$ . +Проанализировав приведенный код, можно заметить, что внутри функции осуществляется рекурсивный вызов двух функций, **то есть из одного вызова образуются два ветвления**. Как показано на рисунке 2-6, при последующем выполнении рекурсивных вызовов в итоге образуется дерево рекурсии (recursion tree) глубиной $n$ . ![Дерево рекурсии последовательности Фибоначчи](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" }

Рисунок 2-6   Дерево рекурсии последовательности Фибоначчи

-По своей сути рекурсия воплощает парадигму "разбиения задачи на более мелкие подзадачи", и именно поэтому стратегия разделяй-и-властвуй столь важна. +По своей сути рекурсия отражает парадигму мышления "разбиение задачи на более мелкие подзадачи", что делает стратегию "разделяй и властвуй" крайне важной. -- С точки зрения алгоритмов многие важнейшие стратегии, такие как поиск, сортировка, бэктрекинг, разделяй-и-властвуй и динамическое программирование, прямо или косвенно используют такой образ мышления. -- С точки зрения структур данных рекурсия естественным образом подходит для решения задач, связанных со связными списками, деревьями и графами, потому что они хорошо поддаются анализу через идеи разделения задачи. +- С точки зрения **алгоритмов** многие важные алгоритмические стратегии, такие как поиск, сортировка, возврат, "разделяй и властвуй" и динамическое программирование, прямо или косвенно используют этот подход. +- С точки зрения **структур данных** рекурсия естественно подходит для решения задач, связанных со списками, деревьями и графами, поскольку они очень хорошо поддаются анализу с использованием идеи "разделяй и властвуй". -## 2.2.3   Сравнение двух подходов +## 2.2.3   Сравнение -Обобщая все сказанное выше, можно представить различия между итерацией и рекурсией с точки зрения реализации, производительности и применимости в следующей таблице. +Подводя итог, можно сказать, что итерация и рекурсия различаются по реализации, производительности и применимости, как показано в таблице 2-1. -

Таблица 2-1   Сравнение характеристик итерации и рекурсии

+

Таблица 2-1   Сравнение итерации и рекурсии

| | Итерация | Рекурсия | | -------- | -------------------------------------- | ------------------------------------------------------------ | -| Реализация | Циклическая структура | Функция вызывает сама себя | -| Временная эффективность | Обычно выше, так как нет накладных расходов на вызовы функций | Каждый вызов функции создает накладные расходы | -| Использование памяти | Обычно требуется фиксированный объем памяти | Накопление вызовов функции может занимать много места в кадрах стека | -| Подходящие задачи | Хорошо подходит для простых циклических задач, код интуитивен и легко читается | Хорошо подходит для разложения на подзадачи, например для деревьев, графов, разделяй-и-властвуй, бэктрекинга и т. д.; код при этом получается компактным и ясным | +| Способ реализации | Циклическая структура | Функция вызывает саму себя | +| Временная эффективность | Обычно высокая эффективность, нет затрат на вызов функции | Каждый вызов функции создает затраты | +| Использование памяти | Обычно используется фиксированный объем памяти | Накопление вызовов функции может использовать значительное количество пространства стека | +| Сфера использования | Подходит для простых циклических задач, код интуитивно понятен и хорошо читаем | Подходит для разбиения на подзадачи, для структур деревья и графы, алгоритмов "разделяй и властвуй", возврата и т. д.; структура кода проста и ясна |
!!! tip - Если тебе сложно понять дальнейшее содержание, можешь вернуться к нему после чтения главы о "стеке". + Если дальнейшее содержание кажется сложным, можно вернуться к нему после чтения главы о "стеке". -Какова же внутренняя связь между итерацией и рекурсией? Если снова взять рекурсивную функцию выше, операция суммирования выполняется в фазе "подъема" рекурсии. Это означает, что функция, вызванная первой, на самом деле завершает сложение последней, **и такой механизм очень похож на принцип стека "последним пришел - первым ушел"**. +Какова же внутренняя связь между итерацией и рекурсией? В рассмотренном примере рекурсивной функции операция сложения выполняется на этапе возврата рекурсии. Это означает, что функция, вызванная первой, фактически завершает операцию сложения последней, **что соответствует принципу стека "первым пришел - последним вышел"**. -На самом деле такие термины рекурсии, как "стек вызовов" и "пространство кадра стека", уже прямо намекают на тесную связь между рекурсией и стеком. +На самом деле такие термины рекурсии, как "стек вызовов" и "пространство стекового кадра", уже намекают на тесную связь между рекурсией и стеком. -1. **Спуск**: когда вызывается функция, система выделяет для нее новый кадр стека в "стеке вызовов", чтобы хранить локальные переменные, параметры, адрес возврата и другие данные. -2. **Подъем**: когда функция завершает выполнение и возвращается, соответствующий кадр стека удаляется из "стека вызовов", а среда выполнения предыдущей функции восстанавливается. +1. **Вызов**: когда вызывается функция, система выделяет для нее новый стековый кадр в "стеке вызовов" для хранения локальных переменных функции, параметров, адреса возврата и других данных. +2. **Возврат**: когда функция завершает выполнение и возвращает результат, соответствующий стековый кадр удаляется из "стека вызовов", восстанавливая среду выполнения предыдущей функции. -Поэтому **мы можем использовать явный стек для имитации поведения стека вызовов** и тем самым преобразовать рекурсию в итеративную форму: +Таким образом, **можно использовать явный стек для моделирования поведения стека вызовов**, чтобы преобразовать рекурсию в итеративную форму: === "Python" @@ -1920,9 +1934,9 @@ comments: true
-Если посмотреть на приведенный выше код, видно, что после преобразования рекурсии в итерацию код становится сложнее. Хотя во многих случаях итерация и рекурсия действительно могут быть преобразованы друг в друга, это не всегда стоит делать по двум причинам. +Наблюдая за приведенным выше кодом, можно заметить, что после преобразования рекурсии в итерацию код становится более сложным. Хотя во многих случаях итерация и рекурсия действительно могут быть преобразованы друг в друга, это не всегда стоит делать по двум причинам. - Преобразованный код может стать труднее для понимания и менее читаемым. -- Для некоторых сложных задач имитация поведения системного стека вызовов может оказаться очень трудной. +- Для некоторых сложных задач моделирование поведения системного стека вызовов может оказаться очень трудным. -Итак, **выбор между итерацией и рекурсией зависит от природы конкретной задачи**. В практическом программировании крайне важно взвешивать плюсы и минусы обоих подходов и выбирать подходящий метод с учетом контекста. +Итак, **выбор между итерацией и рекурсией зависит от природы конкретной задачи**. В практическом программировании крайне важно взвешивать преимущества и недостатки обоих подходов и выбирать подходящий метод с учетом контекста. diff --git a/ru/docs/chapter_computational_complexity/performance_evaluation.md b/ru/docs/chapter_computational_complexity/performance_evaluation.md index 6a6aec05f..e15e294c3 100644 --- a/ru/docs/chapter_computational_complexity/performance_evaluation.md +++ b/ru/docs/chapter_computational_complexity/performance_evaluation.md @@ -4,50 +4,50 @@ comments: true # 2.1   Оценка эффективности алгоритмов -При проектировании алгоритмов мы последовательно стремимся к двум уровням целей. +В процессе разработки алгоритмов мы стремимся к достижению следующих целей. -1. **Найти решение задачи**: алгоритм должен надежно получать правильный ответ в заданном диапазоне входных данных. -2. **Найти оптимальное решение**: для одной и той же задачи может существовать несколько решений, и нам хочется выбрать максимально эффективный алгоритм. +1. **Найти решение задачи**: алгоритм должен надежно находить правильное решение задачи в заданных пределах входных данных. +2. **Найти оптимальное решение**: для одной и той же задачи может существовать несколько решений, и мы стремимся найти максимально эффективный алгоритм. -Иными словами, если задача в принципе решается, эффективность алгоритма становится главным критерием оценки его качества. Она включает два следующих измерения. +Таким образом, при условии возможности решения задачи эффективность алгоритма становится основным критерием его оценки, который включает два аспекта. -- **Временная эффективность**: сколько времени работает алгоритм. -- **Пространственная эффективность**: сколько памяти занимает алгоритм. +- **Временная эффективность**: продолжительность выполнения алгоритма. +- **Пространственная эффективность**: объем памяти, занимаемой алгоритмом. -Короче говоря, **наша цель - проектировать структуры данных и алгоритмы, которые "и быстры, и экономны по памяти"**. Эффективная оценка алгоритмов крайне важна, потому что только так можно сравнивать разные алгоритмы и направлять процесс их проектирования и оптимизации. +В двух словах, **наша цель - разработка быстрых и экономных структур данных и алгоритмов**. Эффективная оценка алгоритмов крайне важна, так как только так можно сравнивать различные алгоритмы и управлять процессом их разработки и оптимизации. -Методы оценки эффективности в основном делятся на два типа: практическое тестирование и теоретическая оценка. +Методы оценки эффективности делятся на два типа: практическое тестирование и теоретическую оценку. ## 2.1.1   Практическое тестирование -Предположим, у нас есть алгоритм `A` и алгоритм `B`, оба решают одну и ту же задачу, и нам нужно сравнить их эффективность. Самый прямой способ - взять компьютер, запустить оба алгоритма и зафиксировать время работы и объем используемой памяти. Такой способ оценки отражает реальную ситуацию, но имеет и серьезные ограничения. +Предположим, у нас есть алгоритмы `A` и `B`, которые решают одну и ту же задачу, и необходимо сравнить их эффективность. Самый прямой метод - это запустить оба алгоритма на компьютере и зафиксировать время их выполнения и объем используемой памяти. Этот метод отражает реальную ситуацию, но имеет значительные ограничения. -С одной стороны, **трудно исключить влияние факторов тестовой среды**. Аппаратная конфигурация влияет на производительность алгоритма. Например, если алгоритм имеет высокий уровень параллелизма, он лучше подходит для многоядерных CPU; если алгоритм интенсивно работает с памятью, он покажет себя лучше на быстрой памяти. Иными словами, результаты тестирования одного и того же алгоритма на разных машинах могут различаться. Это означает, что пришлось бы тестировать на самых разных машинах и усреднять результаты, а на практике это нереалистично. +С одной стороны, **сложно исключить влияние факторов тестовой среды**. Аппаратная конфигурация влияет на производительность алгоритма. Например, если алгоритм обладает высокой степенью параллелизма, он будет лучше работать на многоядерных CPU; если алгоритм интенсивно использует память, его производительность будет выше на высокопроизводительной памяти. Это означает, что результаты тестирования на разных машинах могут значительно отличаться, а для получения средней эффективности пришлось бы тестировать на различных платформах, что крайне затруднительно. -С другой стороны, **полное тестирование требует больших ресурсов**. По мере изменения объема входных данных алгоритм может вести себя по-разному. Например, при небольшом объеме входных данных время работы алгоритма `A` может быть меньше, чем у алгоритма `B`; но при большом объеме результаты могут оказаться прямо противоположными. Поэтому для убедительных выводов пришлось бы тестировать входные данные множества разных масштабов, а это требует значительных вычислительных ресурсов. +С другой стороны, **проведение полного тестирования требует значительных ресурсов**. С изменением объема входных данных алгоритмы демонстрируют разную эффективность. Например, при небольшом объеме данных алгоритм `A` может работать быстрее, чем алгоритм `B`, но при большом объеме данных результат может быть противоположным. Следовательно, для получения убедительных выводов необходимо тестировать различные масштабы входных данных, что требует значительных вычислительных ресурсов. ## 2.1.2   Теоретическая оценка -Поскольку практическое тестирование имеет серьезные ограничения, можно попытаться оценить эффективность алгоритма только с помощью вычислений. Такой метод называется асимптотическим анализом сложности (asymptotic complexity analysis), или сокращенно анализом сложности. +Из-за значительных ограничений практического тестирования можно рассмотреть возможность оценки эффективности алгоритмов только с помощью вычислений. Такой метод называется анализом асимптотической сложности (asymptotic complexity analysis), или сокращенно анализом сложности. -Анализ сложности показывает зависимость между временем и пространственными ресурсами, требуемыми алгоритму, и масштабом входных данных. **Он описывает тенденцию роста времени и памяти, необходимых алгоритму, по мере увеличения размера входных данных**. Это определение звучит немного тяжеловесно, поэтому полезно разложить его на три ключевые идеи. +Анализ сложности позволяет отразить зависимость между ресурсами времени и пространства, необходимыми для выполнения алгоритма, и размером входных данных. **Он описывает тенденцию роста времени и пространства, необходимых для выполнения алгоритма, по мере увеличения размера входных данных**. Это определение может показаться сложным, но его можно разбить на три ключевых момента. -- "Временные и пространственные ресурсы" соответствуют временной сложности (time complexity) и пространственной сложности (space complexity) соответственно. -- "По мере увеличения размера входных данных" означает, что сложность отражает связь между эффективностью алгоритма и масштабом входа. -- "Тенденция роста времени и пространства" означает, что анализ сложности интересуется не конкретными значениями времени или памяти, а тем, насколько быстро они растут. +- "Ресурсы времени и пространства" соответствуют временной сложности (time complexity) и пространственной сложности (space complexity). +- "По мере увеличения размера входных данных" означает, что сложность отражает зависимость эффективности алгоритма от объема входных данных. +- "Тенденция роста времени и пространства" указывает, что анализ сложности фокусируется не на конкретных значениях времени выполнения или объема занимаемой памяти, а на скорости их роста. -**Анализ сложности устраняет недостатки практического тестирования**, что проявляется в следующих аспектах. +**Анализ сложности преодолевает недостатки метода практического тестирования**, что выражается в следующих аспектах. -- Для него не нужно реально запускать код, а значит, он экологичнее и экономит ресурсы. -- Он не зависит от тестовой среды, поэтому результаты анализа применимы ко всем платформам выполнения. -- Он позволяет увидеть эффективность алгоритма при разных объемах данных, особенно на больших данных. +- Он не требует фактического выполнения кода, что делает его более экологичным и энергосберегающим. +- Он независим от тестовой среды, а результаты анализа применимы ко всем платформам выполнения. +- Он может продемонстрировать эффективность алгоритма при различных объемах данных, особенно при больших объемах. !!! tip - Если понятие сложности пока все еще кажется тебе запутанным, не переживай: мы подробно разберем его в следующих разделах. + Если понятие сложности пока все еще кажется вам запутанным, не переживайте: мы подробно разберем его в следующих разделах. -Анализ сложности дает нам "линейку" для оценки эффективности алгоритмов, позволяя измерять, сколько времени и памяти требуется для выполнения конкретного алгоритма, и сравнивать эффективность разных алгоритмов между собой. +Анализ сложности предоставляет нам мерило оценки эффективности алгоритмов, позволяя измерять время и ресурсы, необходимые для выполнения конкретного алгоритма, а также сравнивать эффективность различных алгоритмов. -Сложность - это математическое понятие, поэтому для начинающих оно может показаться довольно абстрактным и сравнительно трудным. С этой точки зрения анализ сложности, возможно, не лучший самый первый материал для знакомства. Однако, когда мы обсуждаем особенности конкретной структуры данных или алгоритма, почти невозможно не затронуть скорость его работы и использование памяти. +Сложность - это математическое понятие, которое новичкам может показаться абстрактным и сложным для изучения. С этой точки зрения анализ сложности не то, с чего стоит начинать изучение алгоритмов. Однако, обсуждая особенности той или иной структуры данных или алгоритма, невозможно избежать анализа их скорости выполнения и использования памяти. -В итоге рекомендуется еще до глубокого погружения в структуры данных и алгоритмы **сформировать хотя бы первичное понимание анализа сложности, чтобы уметь выполнять анализ сложности простых алгоритмов**. +Таким образом, перед погружением в изучение структур данных и алгоритмов рекомендуется получить базовое представление об анализе сложности, чтобы иметь возможность выполнять хотя бы базовую оценку их эффективности. diff --git a/ru/docs/chapter_computational_complexity/space_complexity.md b/ru/docs/chapter_computational_complexity/space_complexity.md index c99ba724c..d71d1ea94 100644 --- a/ru/docs/chapter_computational_complexity/space_complexity.md +++ b/ru/docs/chapter_computational_complexity/space_complexity.md @@ -4,31 +4,31 @@ comments: true # 2.4   Пространственная сложность -Пространственная сложность (space complexity) используется для оценки того, как меняется объем памяти, занимаемой алгоритмом, по мере роста объема данных. Это понятие очень похоже на временную сложность, только вместо "времени выполнения" мы рассматриваем "объем используемой памяти". +Пространственная сложность (space complexity) служит для оценки того, как меняется объем памяти, требуемой алгоритму, по мере роста объема данных. Это понятие очень похоже на временную сложность, только вместо времени выполнения рассматривается объем используемой памяти. ## 2.4.1   Пространство, связанное с алгоритмом -Память, которую использует алгоритм во время работы, в основном включает несколько следующих частей. +Память, которую использует алгоритм во время работы, в основном делится на следующие части. - **Входное пространство**: используется для хранения входных данных алгоритма. - **Временное пространство**: используется для хранения переменных, объектов, контекста функций и других данных, возникающих во время выполнения алгоритма. - **Выходное пространство**: используется для хранения выходных данных алгоритма. -В общем случае при анализе пространственной сложности в расчет включают "временное пространство" и "выходное пространство". +Как правило, при анализе пространственной сложности в расчет включают временное пространство и выходное пространство. Временное пространство можно дополнительно разделить на три части. - **Временные данные**: используются для хранения различных констант, переменных, объектов и т.д., возникающих во время выполнения алгоритма. -- **Пространство кадров стека**: используется для хранения контекстных данных вызываемых функций. Система при каждом вызове функции создает на вершине стека новый кадр; после возврата функции пространство этого кадра освобождается. +- **Пространство кадров стека**: используется для хранения контекстных данных вызываемых функций. При каждом вызове функции система создает на вершине стека новый кадр; после возврата функции пространство этого кадра освобождается. - **Пространство инструкций**: используется для хранения скомпилированных инструкций программы и в реальном подсчете обычно не учитывается. -При анализе пространственной сложности программы **мы обычно учитываем три части: временные данные, пространство кадров стека и выходные данные**, как показано на рисунке 2-15. +При анализе пространственной сложности программы **обычно учитываются временные данные, пространство стека и выходные данные**, как показано на рисунке 2-15. ![Пространство, используемое алгоритмом](space_complexity.assets/space_types.png){ class="animation-figure" }

Рисунок 2-15   Пространство, используемое алгоритмом

-Соответствующий код выглядит следующим образом: +Ниже приведен соответствующий код: === "Python" @@ -369,14 +369,14 @@ comments: true ## 2.4.2   Метод вывода -Метод вывода пространственной сложности в целом аналогичен временному анализу: меняется только объект подсчета, с "количества операций" на "размер используемого пространства". +Метод вывода пространственной сложности в целом аналогичен выводу временной сложности: меняется только объект подсчета, с количества операций на размер используемого пространства. -В отличие от временной сложности, **обычно мы рассматриваем только худшую пространственную сложность**. Это связано с тем, что память является жестким ограничением: нам нужно гарантировать, что для любых входных данных у программы будет достаточно памяти. +В отличие от временной сложности, **обычно рассматривается только худшая пространственная сложность**. Это связано с тем, что память является жестким ограничением: необходимо гарантировать, что для любых входных данных у программы будет достаточно памяти. -Рассмотрим следующий код. Слово "худшая" в "худшей пространственной сложности" имеет два значения. +Рассмотрим следующий код. Понятие худшей пространственной сложности здесь имеет два значения. 1. **Ориентир на худшие входные данные**: когда $n < 10$ , пространственная сложность равна $O(1)$ ; но когда $n > 10$ , инициализированный массив `nums` занимает $O(n)$ пространства, поэтому худшая пространственная сложность равна $O(n)$ . -2. **Ориентир на пиковое потребление памяти во время выполнения алгоритма**: например, до выполнения последней строки программа занимает $O(1)$ пространства; при инициализации массива `nums` она занимает $O(n)$ пространства, поэтому худшая пространственная сложность равна $O(n)$ . +2. **Ориентир на пиковое использование памяти во время выполнения**: например, до выполнения последней строки программа занимает $O(1)$ пространства; при инициализации массива `nums` она занимает $O(n)$ пространства, поэтому худшая пространственная сложность также равна $O(n)$ . === "Python" @@ -808,7 +808,7 @@ comments: true ## 2.4.3   Распространенные типы -Пусть размер входных данных равен $n$ . На рисунке 2-16 показаны распространенные типы пространственной сложности (в порядке от меньшей к большей). +Пусть размер входных данных равен $n$ . На рисунке 2-16 показаны распространенные типы пространственной сложности в порядке от меньшей к большей. $$ \begin{aligned} @@ -823,7 +823,7 @@ $$ ### 1.   Постоянная сложность $O(1)$ {data-toc-label="1.   Постоянная сложность"} -Постоянная сложность часто встречается у констант, переменных и объектов, количество которых не зависит от размера входных данных $n$ . +Постоянная сложность обычно встречается у констант, переменных и объектов, количество которых не зависит от размера входных данных $n$ . Следует заметить, что память, занятая инициализацией переменных или вызовом функций внутри цикла, освобождается при переходе к следующей итерации, поэтому она не накапливается, и пространственная сложность по-прежнему остается $O(1)$ : @@ -1184,7 +1184,7 @@ $$ ### 2.   Линейная сложность $O(n)$ {data-toc-label="2.   Линейная сложность"} -Линейная сложность часто встречается у массивов, связных списков, стеков, очередей и других структур, число элементов в которых пропорционально $n$ : +Линейная сложность часто встречается у массивов, списков, стеков, очередей и других структур, число элементов в которых пропорционально $n$ : === "Python" @@ -1607,7 +1607,19 @@ $$ === "Ruby" ```ruby title="space_complexity.rb" - ### Линейная сложность (рекурсивная реализация) ### + ### Линейная сложность ### + def linear(n) + # Список длины n занимает O(n) памяти + nums = Array.new(n, 0) + + # Хеш-таблица длины n занимает O(n) памяти + hmap = {} + for i in 0...n + hmap[i] = i.to_s + end + end + + # ## Линейная сложность (рекурсивная реализация) ### def linear_recur(n) puts "Рекурсия n = #{n}" return if n == 1 @@ -2019,7 +2031,13 @@ $$ === "Ruby" ```ruby title="space_complexity.rb" - ### Квадратичная сложность (рекурсивная реализация) ### + ### Квадратичная сложность ### + def quadratic(n) + # Двумерный список занимает O(n^2) памяти + Array.new(n) { Array.new(n, 0) } + end + + # ## Квадратичная сложность (рекурсивная реализация) ### def quadratic_recur(n) return 0 unless n > 0 @@ -2040,7 +2058,7 @@ $$ ### 4.   Экспоненциальная сложность $O(2^n)$ {data-toc-label="4.   Экспоненциальная сложность"} -Экспоненциальная сложность часто встречается у бинарных деревьев. Обрати внимание на рисунок 2-19: "полное бинарное дерево" с $n$ уровнями содержит $2^n - 1$ узлов и занимает $O(2^n)$ пространства: +Экспоненциальная сложность часто встречается у бинарных деревьев. Полное бинарное дерево с $n$ уровнями содержит $2^n - 1$ узлов и занимает $O(2^n)$ пространства: === "Python" @@ -2212,7 +2230,22 @@ $$ === "Ruby" ```ruby title="space_complexity.rb" - ### Экспоненциальная сложность (построение полного двоичного дерева) ### + ### Квадратичная сложность ### + def quadratic(n) + # Двумерный список занимает O(n^2) памяти + Array.new(n) { Array.new(n, 0) } + end + + # ## Квадратичная сложность (рекурсивная реализация) ### + def quadratic_recur(n) + return 0 unless n > 0 + + # Длина массива nums равна n, n-1, ..., 2, 1 + nums = Array.new(n, 0) + quadratic_recur(n - 1) + end + + # ## Экспоненциальная сложность (построение полного двоичного дерева) ### def build_tree(n) return if n == 0 @@ -2234,14 +2267,14 @@ $$ ### 5.   Логарифмическая сложность $O(\log n)$ {data-toc-label="5.   Логарифмическая сложность"} -Логарифмическая сложность часто встречается в алгоритмах "разделяй и властвуй". Например, при сортировке слиянием входной массив длины $n$ на каждом шаге рекурсии делится пополам по середине, образуя рекурсивное дерево высоты $\log n$ и используя $O(\log n)$ пространства кадров стека. +Логарифмическая сложность часто встречается в алгоритмах "разделяй и властвуй". Например, при сортировке слиянием входной массив длины $n$ на каждом шаге рекурсии делится пополам, образуя рекурсивное дерево высоты $\log n$ и используя $O(\log n)$ пространства кадров стека. Еще один пример - преобразование числа в строку. Если задано положительное целое число $n$ , то количество его цифр равно $\lfloor \log_{10} n \rfloor + 1$ , то есть длина соответствующей строки тоже равна $\lfloor \log_{10} n \rfloor + 1$ , следовательно, пространственная сложность составляет $O(\log_{10} n + 1) = O(\log n)$ . ## 2.4.4   Компромисс между временем и пространством -В идеале нам хотелось бы, чтобы и временная, и пространственная сложность алгоритма были оптимальными. Однако на практике одновременно оптимизировать и время, и память обычно очень трудно. +В идеальных условиях хотелось бы, чтобы и временная, и пространственная сложность алгоритма были оптимальными. Однако на практике одновременно оптимизировать и время, и память обычно очень трудно. -**Снижение временной сложности обычно достигается ценой увеличения пространственной сложности, и наоборот**. Подход, при котором мы жертвуем памятью ради ускорения работы алгоритма, называется "обмен пространства на время"; обратный подход называется "обмен времени на пространство". +**Снижение временной сложности обычно достигается ценой увеличения пространственной сложности, и наоборот**. Подход, при котором жертвуют памятью ради ускорения работы алгоритма, называется обменом пространства на время; обратный подход называется обменом времени на пространство. -Выбор между этими двумя идеями зависит от того, что для нас важнее. В большинстве случаев время ценнее памяти, поэтому стратегия "обмена пространства на время" используется чаще. Но при очень больших объемах данных контроль пространственной сложности тоже становится крайне важным. +Выбор между этими двумя идеями зависит от того, что важнее в конкретной задаче. В большинстве случаев время ценнее памяти, поэтому стратегия обмена пространства на время используется чаще. Но при очень больших объемах данных контроль пространственной сложности тоже становится крайне важным. diff --git a/ru/docs/chapter_computational_complexity/summary.md b/ru/docs/chapter_computational_complexity/summary.md index ac8c949df..35b08d935 100644 --- a/ru/docs/chapter_computational_complexity/summary.md +++ b/ru/docs/chapter_computational_complexity/summary.md @@ -8,25 +8,25 @@ comments: true **Оценка эффективности алгоритмов** -- Временная эффективность и пространственная эффективность - два главных показателя, по которым оценивают качество алгоритма. -- Мы можем оценивать эффективность алгоритма с помощью практического тестирования, но при этом трудно устранить влияние тестовой среды, а само тестирование потребляет много вычислительных ресурсов. -- Анализ сложности устраняет недостатки практического тестирования, дает результаты, применимые ко всем платформам выполнения, и позволяет увидеть эффективность алгоритма при разных масштабах данных. +- Временная и пространственная эффективность являются двумя основными критериями для оценки качества алгоритмов. +- Эффективность алгоритмов можно оценивать с помощью практических тестов, однако это сложно из-за влияния тестовой среды и значительных затрат вычислительных ресурсов. +- Анализ сложности позволяет устранить недостатки практических тестов, а результаты анализа применимы ко всем платформам и могут выявить эффективность алгоритма при различных объемах данных. **Временная сложность** -- Временная сложность используется для оценки того, как меняется время работы алгоритма с ростом объема данных. Она хорошо подходит для оценки эффективности, но в некоторых случаях может давать недостаточно точное сравнение, например когда входные данные малы или когда временные сложности совпадают. -- Худшая временная сложность обозначается с помощью нотации Big $O$ и соответствует асимптотической верхней границе функции, отражая уровень роста числа операций $T(n)$ при стремлении $n$ к положительной бесконечности. -- Вывод временной сложности включает два шага: сначала подсчитывается число операций, затем определяется асимптотическая верхняя граница. -- Распространенные временные сложности в порядке роста: $O(1)$, $O(\log n)$, $O(n)$, $O(n \log n)$, $O(n^2)$, $O(2^n)$ и $O(n!)$. -- Временная сложность некоторых алгоритмов не фиксирована, а зависит от распределения входных данных. Различают худшую, лучшую и среднюю временную сложность; лучшая временная сложность используется редко, потому что для ее достижения вход обычно должен удовлетворять строгим условиям. -- Средняя временная сложность отражает эффективность алгоритма на случайных входных данных и ближе всего к его поведению в практических сценариях. Для ее вычисления нужно знать распределение входных данных и рассчитать соответствующее математическое ожидание. +- Временная сложность используется для оценки тенденции изменения времени выполнения алгоритма с увеличением объема данных, что позволяет оценивать его эффективность. Однако в некоторых случаях она может работать не так хорошо, например когда объем входных данных мал или временная сложность одинакова, что не позволяет точно сравнить эффективность алгоритмов. +- Худшая временная сложность обозначается символом Big $O$ и соответствует асимптотической верхней границе, отражая уровень роста количества операций $T(n)$ при стремлении $n$ к бесконечности. +- Определение временной сложности включает два этапа: сначала подсчитывается количество операций, затем определяется асимптотическая верхняя граница. +- Наиболее распространенные временные сложности в порядке возрастания: $O(1)$, $O(\log n)$, $O(n)$, $O(n \log n)$, $O(n^2)$, $O(2^n)$ и $O(n!)$. +- Временная сложность некоторых алгоритмов не является фиксированной и зависит от распределения входных данных. Временная сложность делится на худшую, лучшую и среднюю. Лучшая временная сложность почти не используется, так как для достижения лучшего случая входные данные должны соответствовать строгим критериям. +- Средняя временная сложность отражает эффективность алгоритма при случайных входных данных и наиболее близка к реальной производительности алгоритма. Для расчета средней временной сложности необходимо учитывать распределение входных данных и математическое ожидание. **Пространственная сложность** -- Пространственная сложность играет роль, аналогичную временной: она показывает тенденцию роста потребления памяти по мере увеличения объема данных. -- Память, связанная с выполнением алгоритма, можно разделить на входное пространство, временное пространство и выходное пространство. Обычно входное пространство не включается в расчет пространственной сложности. Временное пространство можно разбить на временные данные, пространство кадров стека и пространство инструкций; при этом пространство кадров стека обычно влияет на сложность только в рекурсивных функциях. -- Обычно нас интересует только худшая пространственная сложность, то есть пространственная сложность алгоритма при худшем наборе входных данных и в худший момент времени выполнения. -- Распространенные пространственные сложности в порядке роста: $O(1)$, $O(\log n)$, $O(n)$, $O(n^2)$ и $O(2^n)$. +- Пространственная сложность аналогична временной сложности и используется для оценки тенденции изменения объема памяти, занимаемой алгоритмом, с увеличением объема данных. +- Память, используемую в процессе выполнения алгоритма, можно разделить на входное пространство, временное пространство и выходное пространство. Обычно при расчете пространственной сложности входное пространство не учитывается. Временное пространство делится на временные данные, пространство стека и пространство инструкций, причем пространство стека обычно влияет на сложность только в рекурсивных функциях. +- Обычно рассматривается только худшая пространственная сложность, то есть пространственная сложность алгоритма при худших входных данных и в худший момент выполнения. +- Наиболее распространенные пространственные сложности в порядке возрастания: $O(1)$, $O(\log n)$, $O(n)$, $O(n^2)$ и $O(2^n)$. ### 2.   Q & A diff --git a/ru/docs/chapter_computational_complexity/time_complexity.md b/ru/docs/chapter_computational_complexity/time_complexity.md index 683106360..fd4b7fac1 100644 --- a/ru/docs/chapter_computational_complexity/time_complexity.md +++ b/ru/docs/chapter_computational_complexity/time_complexity.md @@ -4,7 +4,7 @@ comments: true # 2.3   Временная сложность -Время выполнения может наглядно и точно отражать эффективность алгоритма. Если мы хотим точно оценить время работы некоторого фрагмента кода, как это сделать? +Время выполнения действительно может наглядно и точно отражать эффективность алгоритма. Но если мы захотим точно оценить время работы некоторого фрагмента кода, то столкнемся со следующими шагами. 1. **Определить платформу выполнения**, включая конфигурацию оборудования, язык программирования, системную среду и т.д., поскольку все эти факторы влияют на эффективность выполнения кода. 2. **Оценить время выполнения различных вычислительных операций**, например операция сложения `+` требует 1 нс , операция умножения `*` требует 10 нс , операция вывода `print()` требует 5 нс и т.д. @@ -211,13 +211,13 @@ $$ 1 + 1 + 10 + (1 + 5) \times n = 6n + 12 $$ -Но на практике **подсчитывать реальное время выполнения алгоритма и неразумно, и нереалистично**. Во-первых, мы не хотим привязывать оценку времени к конкретной платформе, потому что алгоритм должен запускаться на самых разных платформах. Во-вторых, нам трудно узнать время выполнения каждого типа операций, а это сильно усложняет оценку. +Но на практике **подсчитывать реальное время выполнения алгоритма и неразумно, и нереалистично**. Во-первых, мы не хотим привязывать оценку времени к конкретной платформе, потому что алгоритм должен запускаться на самых разных платформах. Во-вторых, нам трудно определить время выполнения каждого типа операций, а это делает точную оценку крайне затруднительной. ## 2.3.1   Подсчет тенденции роста времени Анализ временной сложности оценивает не само время выполнения алгоритма, **а тенденцию роста этого времени по мере увеличения объема данных**. -Понятие "тенденции роста времени" довольно абстрактно, поэтому разберем его на примере. Предположим, размер входных данных равен $n$ , и даны три алгоритма `A` , `B` и `C` : +Понятие "тенденции роста времени" выглядит довольно абстрактным, поэтому разберем его на примере. Предположим, размер входных данных равен $n$ , и даны три алгоритма `A` , `B` и `C` : === "Python" @@ -488,11 +488,11 @@ $$ end ``` -На рисунке 2-7 показана временная сложность трех функций алгоритмов выше. +Ниже показаны временные сложности трех приведенных выше функций. -- У алгоритма `A` есть только 1 операция вывода, и время его работы не растет с увеличением $n$ . Мы называем такую временную сложность "постоянной". -- В алгоритме `B` операция вывода выполняется в цикле $n$ раз, поэтому время работы растет линейно по мере увеличения $n$ . Такая временная сложность называется "линейной". -- В алгоритме `C` операция вывода выполняется $1000000$ раз; хотя время работы велико, оно не зависит от размера входных данных $n$ . Поэтому временная сложность `C` такая же, как у `A` , и тоже является "постоянной". +- У алгоритма `A` есть только одна операция вывода, и время его работы не растет с увеличением $n$ . Такую временную сложность называют постоянной. +- В алгоритме `B` операция вывода выполняется в цикле $n$ раз, поэтому время работы растет линейно по мере увеличения $n$ . Такая временная сложность называется линейной. +- В алгоритме `C` операция вывода выполняется $1000000$ раз; хотя время работы велико, оно не зависит от размера входных данных $n$ . Поэтому временная сложность `C` такая же, как у `A` , и тоже является постоянной. ![Тенденции роста времени для алгоритмов A, B и C](time_complexity.assets/time_complexity_simple_example.png){ class="animation-figure" } @@ -500,9 +500,9 @@ $$ Какие особенности имеет анализ временной сложности по сравнению с непосредственным измерением времени работы алгоритма? -- **Временная сложность позволяет эффективно оценивать эффективность алгоритма**. Например, время работы алгоритма `B` растет линейно: при $n > 1$ он медленнее алгоритма `A` , а при $n > 1000000$ медленнее алгоритма `C` . На самом деле, если размер входных данных $n$ достаточно велик, алгоритм с "постоянной" сложностью обязательно лучше алгоритма с "линейной" сложностью. В этом и состоит смысл тенденции роста времени. -- **Метод вывода временной сложности проще**. Очевидно, что платформа выполнения и тип вычислительных операций не влияют на тенденцию роста времени работы алгоритма. Поэтому в анализе временной сложности мы можем считать время выполнения всех вычислительных операций одинаковым "единичным временем" и тем самым упростить "подсчет времени выполнения операций" до "подсчета количества операций", что существенно снижает сложность оценки. -- **У временной сложности есть и определенные ограничения**. Например, хотя временная сложность алгоритмов `A` и `C` одинакова, их реальное время выполнения сильно различается. Точно так же, хотя временная сложность `B` выше, чем у `C` , при малых $n$ алгоритм `B` явно лучше `C` . В таких случаях нам часто трудно судить об эффективности алгоритма, опираясь только на временную сложность. Тем не менее, несмотря на эти ограничения, анализ сложности все равно остается самым эффективным и самым распространенным способом оценки алгоритмов. +- **Временная сложность позволяет эффективно оценивать эффективность алгоритма**. Например, время работы алгоритма `B` растет линейно: при $n > 1$ он медленнее алгоритма `A` , а при $n > 1000000$ медленнее алгоритма `C` . Если размер входных данных достаточно велик, алгоритм с постоянной сложностью обязательно лучше алгоритма с линейной сложностью. В этом и состоит смысл тенденции роста времени. +- **Метод вывода временной сложности проще**. Платформа выполнения и тип вычислительных операций не влияют на тенденцию роста времени работы алгоритма. Поэтому в анализе временной сложности можно считать время выполнения всех вычислительных операций одинаковым единичным временем и тем самым упростить подсчет времени выполнения до подсчета количества операций. +- **У временной сложности есть и определенные ограничения**. Например, хотя временная сложность алгоритмов `A` и `C` одинакова, их реальное время выполнения сильно различается. Точно так же, хотя временная сложность `B` выше, чем у `C` , при малых $n$ алгоритм `B` очевидно лучше `C` . Несмотря на эти ограничения, анализ сложности все равно остается самым эффективным и самым распространенным способом оценки алгоритмов. ## 2.3.2   Асимптотическая верхняя граница функции @@ -695,11 +695,11 @@ $$ T(n) = 3 + 2n $$ -$T(n)$ - линейная функция, а это означает, что тенденция роста времени работы линейна, следовательно, ее временная сложность является линейной. +$T(n)$ - линейная функция, а это означает, что тенденция роста времени работы линейна, следовательно, временная сложность здесь тоже линейна. -Линейную временную сложность мы записываем как $O(n)$ ; этот математический символ называется нотацией Big $O$ (big-$O$ notation) и обозначает асимптотическую верхнюю границу (asymptotic upper bound) функции $T(n)$ . +Линейную временную сложность записывают как $O(n)$ ; этот математический символ называется нотацией Big $O$ (big-$O$ notation) и обозначает асимптотическую верхнюю границу (asymptotic upper bound) функции $T(n)$ . -По сути анализ временной сложности - это вычисление асимптотической верхней границы "количества операций $T(n)$", и у него есть строгое математическое определение. +Иными словами, анализ временной сложности сводится к определению асимптотической верхней границы числа операций $T(n)$, и у этого понятия есть строгое математическое определение. !!! note "Асимптотическая верхняя граница функции" @@ -713,13 +713,13 @@ $T(n)$ - линейная функция, а это означает, что т ## 2.3.3   Метод вывода -Математическое определение асимптотической верхней границы выглядит довольно формально, и если ты понял его не до конца, переживать не стоит. Сначала можно освоить сам метод вывода, а в процессе дальнейшей практики постепенно почувствовать его математический смысл. +Математическое определение асимптотической верхней границы выглядит довольно формально, и если оно пока не до конца понятно, переживать не стоит. Сначала можно освоить сам метод вывода, а в процессе дальнейшей практики постепенно почувствовать его математический смысл. -Согласно определению, после того как мы определили $f(n)$ , мы можем получить временную сложность $O(f(n))$ . Но как определить саму асимптотическую верхнюю границу $f(n)$ ? В целом процесс состоит из двух шагов: сначала подсчитать количество операций, затем определить асимптотическую верхнюю границу. +Согласно определению, после того как мы определили $f(n)$ , можно получить временную сложность $O(f(n))$ . Но как определить саму асимптотическую верхнюю границу $f(n)$ ? В целом процесс состоит из двух шагов: сначала подсчитать количество операций, затем определить асимптотическую верхнюю границу. ### 1.   Шаг 1: подсчет количества операций -Для кода это можно делать построчно сверху вниз. Однако, поскольку в выражении $c \cdot f(n)$ выше постоянный коэффициент $c$ может быть сколь угодно большим, **различные коэффициенты и постоянные члены в числе операций $T(n)$ можно игнорировать**. Исходя из этого принципа, можно сформулировать следующие упрощающие приемы подсчета. +Для кода это можно делать построчно сверху вниз. Однако, поскольку в выражении $c \cdot f(n)$ постоянный коэффициент $c$ может быть сколь угодно большим, **различные коэффициенты и постоянные члены в числе операций $T(n)$ можно игнорировать**. Исходя из этого принципа, можно сформулировать следующие упрощающие приемы подсчета. 1. **Игнорировать константы в $T(n)$**. Они не зависят от $n$ , а значит не влияют на временную сложность. 2. **Опускать все коэффициенты**. Например, циклы на $2n$ раз или $5n + 1$ раз можно упростить до $n$ раз, потому что коэффициент перед $n$ не влияет на временную сложность. @@ -982,7 +982,7 @@ $$ **Временная сложность определяется старшим по степени членом в $T(n)$ **. Это связано с тем, что при стремлении $n$ к бесконечности именно старший член начинает доминировать, а влиянием остальных членов можно пренебречь. -В таблице 2-2 приведены несколько примеров. Некоторые значения специально сделаны преувеличенными, чтобы подчеркнуть вывод: "коэффициент не способен изменить порядок". Когда $n$ стремится к бесконечности, эти константы становятся несущественными. +В таблице 2-2 приведены несколько примеров. Некоторые значения специально сделаны преувеличенными, чтобы подчеркнуть вывод: коэффициент не способен изменить порядок. Когда $n$ стремится к бесконечности, эти константы становятся несущественными.

Таблица 2-2   Временная сложность, соответствующая разному количеству операций

@@ -1000,7 +1000,7 @@ $$ ## 2.3.4   Распространенные типы -Пусть размер входных данных равен $n$ ; распространенные типы временной сложности показаны на рисунке 2-9 (в порядке от меньшей к большей). +Пусть размер входных данных равен $n$ ; распространенные типы временной сложности показаны на рисунке 2-9 в порядке от меньшей к большей. $$ \begin{aligned} @@ -1200,7 +1200,7 @@ $$ ### 2.   Линейная сложность $O(n)$ {data-toc-label="2.   Линейная сложность"} -Число операций при линейной сложности растет линейно относительно размера входных данных $n$ . Линейная сложность обычно встречается в одноуровневых циклах: +Линейная сложность характеризуется тем, что число операций растет линейно относительно размера входных данных $n$ . Линейная сложность обычно встречается в одноуровневых циклах: === "Python" @@ -1535,7 +1535,14 @@ $$ === "Ruby" ```ruby title="time_complexity.rb" - ### Линейная сложность (обход массива) ### + ### Линейная сложность ### + def linear(n) + count = 0 + (0...n).each { count += 1 } + count + end + + # ## Линейная сложность (обход массива) ### def array_traversal(nums) count = 0 @@ -1553,11 +1560,11 @@ $$
-Стоит отметить, что **размер входных данных $n$ нужно определять конкретно в зависимости от типа входа**. Например, в первом примере переменная $n$ сама является размером входных данных; во втором примере размером данных служит длина массива $n$ . +Стоит отметить, что **размер входных данных $n$ нужно определять конкретно в зависимости от типа входа**. Например, в первом примере переменная $n$ сама является размером входных данных; во втором примере размером данных служит длина массива. ### 3.   Квадратичная сложность $O(n^2)$ {data-toc-label="3.   Квадратичная сложность"} -Число операций при квадратичной сложности растет квадратично относительно размера входных данных $n$ . Квадратичная сложность обычно встречается во вложенных циклах: временная сложность внешнего и внутреннего циклов равна $O(n)$ , поэтому общая временная сложность составляет $O(n^2)$ : +Квадратичная сложность характеризуется тем, что число операций растет квадратично относительно размера входных данных $n$ . Квадратичная сложность обычно встречается во вложенных циклах: временная сложность внешнего и внутреннего циклов равна $O(n)$ , поэтому общая временная сложность составляет $O(n^2)$ : === "Python" @@ -1777,7 +1784,7 @@ $$

Рисунок 2-10   Постоянная, линейная и квадратичная временная сложность

-Возьмем в качестве примера пузырьковую сортировку: внешний цикл выполняется $n - 1$ раз, внутренний цикл выполняется $n-1$ , $n-2$ , $\dots$ , $2$ , $1$ раз, в среднем это $n / 2$ раз, поэтому временная сложность равна $O((n - 1) n / 2) = O(n^2)$ : +Возьмем в качестве примера пузырьковую сортировку: внешний цикл выполняется $n - 1$ раз, внутренний цикл выполняется $n-1$ , $n-2$ , $\dots$ , $2$ , $1$ раз, в среднем это $n / 2$ раз, поэтому временная сложность равна $O((n - 1)n / 2) = O(n^2)$ : === "Python" @@ -2053,7 +2060,21 @@ $$ === "Ruby" ```ruby title="time_complexity.rb" - ### Квадратичная сложность (пузырьковая сортировка) ### + ### Квадратичная сложность ### + def quadratic(n) + count = 0 + + # Число итераций квадратично зависит от размера данных n + for i in 0...n + for j in 0...n + count += 1 + end + end + + count + end + + # ## Квадратичная сложность (пузырьковая сортировка) ### def bubble_sort(nums) count = 0 # Счетчик @@ -2082,9 +2103,9 @@ $$ ### 4.   Экспоненциальная сложность $O(2^n)$ {data-toc-label="4.   Экспоненциальная сложность"} -Типичный пример экспоненциального роста в биологии - "деление клеток": в начальном состоянии есть 1 клетка, после одного деления их становится 2, после двух делений - 4 и так далее; после $n$ раундов деления клеток становится $2^n$ . +Типичный пример экспоненциального роста в биологии - деление клеток: в начальном состоянии есть одна клетка, после одного деления их становится 2, после двух делений - 4 и так далее; после $n$ раундов деления клеток становится $2^n$ . -На рисунке 2-11 и в следующем коде моделируется процесс деления клеток; временная сложность равна $O(2^n)$ . Обрати внимание, что входное значение $n$ обозначает число раундов деления, а возвращаемое значение `count` обозначает общее число делений. +На рисунке 2-11 и в следующем коде моделируется процесс деления клеток; временная сложность равна $O(2^n)$ . Здесь входное значение $n$ обозначает число раундов деления, а возвращаемое значение `count` обозначает общее число делений. === "Python" @@ -2309,7 +2330,42 @@ $$ === "Ruby" ```ruby title="time_complexity.rb" - ### Экспоненциальная сложность (итеративная реализация) ### + ### Квадратичная сложность ### + def quadratic(n) + count = 0 + + # Число итераций квадратично зависит от размера данных n + for i in 0...n + for j in 0...n + count += 1 + end + end + + count + end + + # ## Квадратичная сложность (пузырьковая сортировка) ### + def bubble_sort(nums) + count = 0 # Счетчик + + # Внешний цикл: неотсортированный диапазон [0, i] + for i in (nums.length - 1).downto(0) + # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец + for j in 0...i + if nums[j] > nums[j + 1] + # Поменять местами nums[j] и nums[j + 1] + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # Обмен элементов включает 3 элементарные операции + end + end + end + + count + end + + # ## Экспоненциальная сложность (итеративная реализация) ### def exponential(n) count, base = 0, 1 @@ -2469,7 +2525,56 @@ $$ === "Ruby" ```ruby title="time_complexity.rb" - ### Экспоненциальная сложность (рекурсивная реализация) ### + ### Квадратичная сложность ### + def quadratic(n) + count = 0 + + # Число итераций квадратично зависит от размера данных n + for i in 0...n + for j in 0...n + count += 1 + end + end + + count + end + + # ## Квадратичная сложность (пузырьковая сортировка) ### + def bubble_sort(nums) + count = 0 # Счетчик + + # Внешний цикл: неотсортированный диапазон [0, i] + for i in (nums.length - 1).downto(0) + # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец + for j in 0...i + if nums[j] > nums[j + 1] + # Поменять местами nums[j] и nums[j + 1] + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # Обмен элементов включает 3 элементарные операции + end + end + end + + count + end + + # ## Экспоненциальная сложность (итеративная реализация) ### + def exponential(n) + count, base = 0, 1 + + # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1) + (0...n).each do + (0...base).each { count += 1 } + base *= 2 + end + + # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + count + end + + # ## Экспоненциальная сложность (рекурсивная реализация) ### def exp_recur(n) return 1 if n == 1 exp_recur(n - 1) + exp_recur(n - 1) + 1 @@ -2481,13 +2586,13 @@ $$
-Экспоненциальный рост происходит очень быстро и часто встречается в переборных методах (грубая сила, backtracking и т.д.). Для задач большого масштаба экспоненциальная сложность неприемлема, и обычно приходится применять динамическое программирование, жадные алгоритмы и другие подходы. +Экспоненциальный рост происходит очень быстро и часто встречается в переборных методах, грубой силе, поиске с возвратом и тому подобных подходах. Для задач большого масштаба экспоненциальная сложность неприемлема, и обычно приходится применять динамическое программирование, жадные алгоритмы и другие стратегии. ### 5.   Логарифмическая сложность $O(\log n)$ {data-toc-label="5.   Логарифмическая сложность"} -В противоположность экспоненциальной, логарифмическая сложность описывает ситуацию "каждый раунд уменьшение вдвое". Пусть размер входных данных равен $n$ ; так как на каждом шаге размер уменьшается вдвое, число итераций равно $\log_2 n$ , то есть является обратной функцией к $2^n$ . +В противоположность экспоненциальной, логарифмическая сложность описывает ситуацию, когда **в каждом раунде размер задачи уменьшается вдвое**. Пусть размер входных данных равен $n$ ; так как на каждом шаге размер уменьшается вдвое, число итераций равно $\log_2 n$ , то есть является обратной функцией к $2^n$ . -На рисунке 2-12 и в следующем коде моделируется процесс "каждый раунд уменьшение вдвое"; временная сложность равна $O(\log_2 n)$ и кратко записывается как $O(\log n)$ : +На рисунке 2-12 и в следующем коде моделируется процесс, в котором **в каждом раунде размер задачи уменьшается вдвое**; временная сложность равна $O(\log_2 n)$ и кратко записывается как $O(\log n)$ : === "Python" @@ -2660,7 +2765,62 @@ $$ === "Ruby" ```ruby title="time_complexity.rb" - ### Логарифмическая сложность (итеративная реализация) ### + ### Квадратичная сложность ### + def quadratic(n) + count = 0 + + # Число итераций квадратично зависит от размера данных n + for i in 0...n + for j in 0...n + count += 1 + end + end + + count + end + + # ## Квадратичная сложность (пузырьковая сортировка) ### + def bubble_sort(nums) + count = 0 # Счетчик + + # Внешний цикл: неотсортированный диапазон [0, i] + for i in (nums.length - 1).downto(0) + # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец + for j in 0...i + if nums[j] > nums[j + 1] + # Поменять местами nums[j] и nums[j + 1] + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # Обмен элементов включает 3 элементарные операции + end + end + end + + count + end + + # ## Экспоненциальная сложность (итеративная реализация) ### + def exponential(n) + count, base = 0, 1 + + # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1) + (0...n).each do + (0...base).each { count += 1 } + base *= 2 + end + + # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + count + end + + # ## Экспоненциальная сложность (рекурсивная реализация) ### + def exp_recur(n) + return 1 if n == 1 + exp_recur(n - 1) + exp_recur(n - 1) + 1 + end + + # ## Логарифмическая сложность (итеративная реализация) ### def logarithmic(n) count = 0 @@ -2817,7 +2977,74 @@ $$ === "Ruby" ```ruby title="time_complexity.rb" - ### Логарифмическая сложность (рекурсивная реализация) ### + ### Квадратичная сложность ### + def quadratic(n) + count = 0 + + # Число итераций квадратично зависит от размера данных n + for i in 0...n + for j in 0...n + count += 1 + end + end + + count + end + + # ## Квадратичная сложность (пузырьковая сортировка) ### + def bubble_sort(nums) + count = 0 # Счетчик + + # Внешний цикл: неотсортированный диапазон [0, i] + for i in (nums.length - 1).downto(0) + # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец + for j in 0...i + if nums[j] > nums[j + 1] + # Поменять местами nums[j] и nums[j + 1] + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # Обмен элементов включает 3 элементарные операции + end + end + end + + count + end + + # ## Экспоненциальная сложность (итеративная реализация) ### + def exponential(n) + count, base = 0, 1 + + # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1) + (0...n).each do + (0...base).each { count += 1 } + base *= 2 + end + + # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + count + end + + # ## Экспоненциальная сложность (рекурсивная реализация) ### + def exp_recur(n) + return 1 if n == 1 + exp_recur(n - 1) + exp_recur(n - 1) + 1 + end + + # ## Логарифмическая сложность (итеративная реализация) ### + def logarithmic(n) + count = 0 + + while n > 1 + n /= 2 + count += 1 + end + + count + end + + # ## Логарифмическая сложность (рекурсивная реализация) ### def log_recur(n) return 0 unless n > 1 log_recur(n / 2) + 1 @@ -2829,7 +3056,7 @@ $$
-Логарифмическая сложность часто встречается в алгоритмах, основанных на стратегии "разделяй и властвуй", и отражает идеи "разделить одно на много" и "упростить сложное". Она растет медленно и является идеальной временной сложностью, уступающей только постоянной. +Логарифмическая сложность часто встречается в алгоритмах, основанных на стратегии "разделяй и властвуй", и отражает идеи разбиения на части и упрощения сложной задачи. Она растет медленно и считается одной из самых желательных временных сложностей после константной. !!! tip "Каково основание у $O(\log n)$ ?" @@ -2843,7 +3070,7 @@ $$ ### 6.   Линейно-логарифмическая сложность $O(n \log n)$ {data-toc-label="6.   Линейно-логарифмическая сложность"} -Линейно-логарифмическая сложность часто встречается во вложенных циклах, когда временная сложность двух уровней соответственно равна $O(\log n)$ и $O(n)$ . Соответствующий код выглядит следующим образом: +Линейно-логарифмическая сложность часто встречается в рекурсивных разбиениях, где временная сложность одного измерения равна $O(\log n)$ , а другого - $O(n)$ . Соответствующий код выглядит следующим образом: === "Python" @@ -3053,13 +3280,13 @@ $$ ### 7.   Факториальная сложность $O(n!)$ {data-toc-label="7.   Факториальная сложность"} -Факториальная сложность соответствует математической задаче "все перестановки". Если даны $n$ попарно различных элементов, то число всех возможных перестановок равно: +Факториальная сложность соответствует математической задаче полной перестановки. Если даны $n$ попарно различных элементов, то число всех возможных перестановок равно: $$ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1 $$ -Факториал обычно реализуют через рекурсию. Как показано на рисунке 2-14 и в следующем коде, на первом уровне происходит ветвление на $n$ подзадач, на втором - на $n - 1$ и так далее, пока на $n$ -м уровне ветвление не прекращается: +Факториал обычно реализуют через рекурсию. Как показано на рисунке 2-14 и в следующем коде, на первом уровне происходит ветвление на $n$ подзадач, на втором - на $n - 1$ и так далее, пока на $n$-м уровне ветвление не прекращается: === "Python" @@ -3252,7 +3479,17 @@ $$ === "Ruby" ```ruby title="time_complexity.rb" - ### Факториальная сложность (рекурсивная реализация) ### + ### Линейно-логарифмическая сложность ### + def linear_log_recur(n) + return 1 unless n > 1 + + count = linear_log_recur(n / 2) + linear_log_recur(n / 2) + (0...n).each { count += 1 } + + count + end + + # ## Факториальная сложность (рекурсивная реализация) ### def factorial_recur(n) return 1 if n == 0 @@ -3273,7 +3510,7 @@ $$

Рисунок 2-14   Факториальная временная сложность

-Обрати внимание: поскольку при $n \geq 4$ всегда выполняется $n! > 2^n$ , факториальная сложность растет еще быстрее, чем экспоненциальная, и при больших $n$ также неприемлема. +Следует отметить, что поскольку при $n \geq 4$ всегда выполняется $n! > 2^n$ , факториальная сложность растет еще быстрее, чем экспоненциальная, и при больших $n$ становится неприемлемой. ## 2.3.5   Худшая, лучшая и средняя временная сложность @@ -3282,7 +3519,7 @@ $$ - Когда `nums = [?, ?, ..., 1]` , то есть когда последний элемент равен $1$ , нужно полностью пройти по массиву, **что дает худшую временную сложность $O(n)$** . - Когда `nums = [1, ?, ?, ...]` , то есть когда первый элемент равен $1$ , независимо от длины массива продолжать обход не нужно, **что дает лучшую временную сложность $\Omega(1)$** . -"Худшая временная сложность" соответствует асимптотической верхней границе функции и обозначается нотацией Big $O$ . Соответственно, "лучшая временная сложность" соответствует асимптотической нижней границе функции и обозначается символом $\Omega$ : +Худшая временная сложность соответствует асимптотической верхней границе функции и обозначается нотацией Big $O$ . Соответственно, лучшая временная сложность соответствует асимптотической нижней границе функции и обозначается символом $\Omega$ : === "Python" @@ -3663,13 +3900,13 @@ $$
-Стоит отметить, что на практике мы редко используем лучшую временную сложность, поскольку обычно она достигается лишь с очень малой вероятностью и может вводить в заблуждение. **Худшая временная сложность гораздо практичнее, потому что задает безопасную оценку эффективности** и позволяет уверенно использовать алгоритм. +Стоит отметить, что на практике лучшая временная сложность используется редко, поскольку обычно она достигается лишь с очень малой вероятностью и может вводить в заблуждение. **Худшая временная сложность гораздо практичнее, потому что задает безопасную оценку эффективности** и позволяет уверенно использовать алгоритм. -Из приведенного выше примера видно, что худшая и лучшая временные сложности возникают только при "особых распределениях данных"; вероятность таких случаев может быть низкой, и они не всегда реально отражают эффективность алгоритма. Напротив, **средняя временная сложность способна показать эффективность алгоритма на случайных входных данных** и обозначается символом $\Theta$ . +Из приведенного выше примера видно, что худшая и лучшая временные сложности возникают только при особых распределениях данных; вероятность таких случаев может быть низкой, и они не всегда реально отражают эффективность алгоритма. Напротив, **средняя временная сложность способна показать эффективность алгоритма на случайных входных данных** и обозначается символом $\Theta$ . -Для некоторых алгоритмов мы можем относительно просто вывести средний случай при случайном распределении данных. Например, в приведенном выше примере входной массив перемешан, а значит вероятность появления элемента $1$ на любом индексе одинакова; следовательно, среднее число итераций алгоритма равно половине длины массива, то есть $n / 2$ , а средняя временная сложность равна $\Theta(n / 2) = \Theta(n)$ . +Для некоторых алгоритмов можно относительно просто вывести средний случай при случайном распределении данных. Например, в приведенном выше примере входной массив перемешан, а вероятность появления элемента $1$ на любом индексе одинакова; следовательно, среднее число итераций алгоритма равно половине длины массива, то есть $n / 2$ , а средняя временная сложность равна $\Theta(n / 2) = \Theta(n)$ . -Но для более сложных алгоритмов вычислить среднюю временную сложность часто непросто, потому что трудно проанализировать полное математическое ожидание на заданном распределении данных. В таких случаях мы обычно используем худшую временную сложность как критерий оценки эффективности алгоритма. +Однако для более сложных алгоритмов вычислить среднюю временную сложность часто непросто, потому что трудно проанализировать полное математическое ожидание на заданном распределении данных. В таких случаях обычно используют худшую временную сложность как критерий оценки эффективности алгоритма. !!! question "Почему символ $\Theta$ встречается так редко?" diff --git a/ru/docs/chapter_data_structure/basic_data_types.md b/ru/docs/chapter_data_structure/basic_data_types.md index 862b33ada..4a04905da 100644 --- a/ru/docs/chapter_data_structure/basic_data_types.md +++ b/ru/docs/chapter_data_structure/basic_data_types.md @@ -4,23 +4,23 @@ comments: true # 3.2   Базовые типы данных -Когда мы говорим о данных в компьютере, нам приходят на ум текст, изображения, видео, звук, 3D-модели и многие другие формы. Хотя эти данные организованы по-разному, все они состоят из различных базовых типов данных. +Когда речь заходит о данных в компьютере, мы в первую очередь вспоминаем текст, изображения, видео, звук, 3D-модели и многие другие формы представления информации. Хотя способы организации этих данных различаются, все они строятся из базовых типов данных. -**Базовые типы данных - это типы, с которыми CPU может работать напрямую**; в алгоритмах они используются непосредственно и в основном включают следующее. +**Базовые типы данных - это типы, которые процессор может обрабатывать непосредственно**. В алгоритмах они используются напрямую и в основном включают следующее. - Целочисленные типы `byte` , `short` , `int` , `long` . - Типы с плавающей точкой `float` , `double` , используемые для представления дробных чисел. - Символьный тип `char` , используемый для представления букв, знаков препинания и даже эмодзи в разных языках. - Логический тип `bool` , используемый для представления суждений "да" и "нет". -**Базовые типы данных хранятся в компьютере в двоичной форме**. Один двоичный разряд равен $1$ биту. В подавляющем большинстве современных операционных систем $1$ байт (byte) состоит из $8$ битов (bit). +**Базовые типы данных хранятся в компьютере в двоичной форме**. Один двоичный разряд равен $1$ биту. В большинстве современных операционных систем $1$ байт (byte) состоит из $8$ битов (bit). Диапазон значений базовых типов данных зависит от объема занимаемого ими пространства. Ниже в качестве примера используется Java. - Целочисленный тип `byte` занимает $1$ байт = $8$ бит и может представлять $2^{8}$ чисел. - Целочисленный тип `int` занимает $4$ байта = $32$ бита и может представлять $2^{32}$ чисел. -В таблице 3-1 перечислены объем памяти, диапазон значений и значения по умолчанию для различных базовых типов данных в Java. Заучивать эту таблицу наизусть не нужно; достаточно иметь общее представление и при необходимости обращаться к ней. +В таблице 3-1 перечислены объем памяти, диапазон значений и значения по умолчанию для различных базовых типов данных в Java. Эту таблицу не нужно заучивать наизусть; достаточно иметь общее представление и при необходимости обращаться к ней.

Таблица 3-1   Объем памяти и диапазоны значений базовых типов данных

@@ -39,36 +39,36 @@ comments: true
-Обрати внимание: приведенная выше таблица относится именно к базовым типам данных Java. В каждом языке программирования определения типов свои, поэтому объем памяти, диапазон значений и значения по умолчанию могут различаться. +Обрати внимание: приведенная выше таблица относится именно к базовым типам данных Java. В каждом языке программирования свои определения типов, поэтому объем памяти, диапазон значений и значения по умолчанию могут различаться. -- В Python целочисленный тип `int` может иметь произвольный размер, ограниченный только доступной памятью; тип `float` использует двойную точность 64 бита; типа `char` нет, а одиночный символ на деле является строкой `str` длины 1. -- В C и C++ размер базовых типов данных явно не зафиксирован и зависит от реализации и платформы. таблица 3-1 соответствует модели данных LP64 [data model](https://en.cppreference.com/w/cpp/language/types#Properties), применяемой в 64-битных Unix-системах, включая Linux и macOS. +- В Python целочисленный тип `int` может иметь произвольный размер, ограниченный только доступной памятью; тип `float` является 64-битным числом двойной точности; типа `char` нет, а отдельный символ на деле является строкой `str` длины 1. +- В C и C++ размер базовых типов данных явно не зафиксирован и зависит от реализации и платформы. таблица 3-1 соответствует модели данных LP64 [data model](https://en.cppreference.com/w/cpp/language/types#Properties), используемой в 64-битных Unix-системах, включая Linux и macOS. - Размер символа `char` в C и C++ составляет 1 байт, а в большинстве других языков программирования зависит от конкретного способа кодирования символов; подробнее это рассматривается в разделе "Кодирование символов". - Хотя для представления логического значения достаточно 1 бита ( $0$ или $1$ ), в памяти оно обычно хранится как 1 байт. Это связано с тем, что современные CPU обычно используют 1 байт как минимальную адресуемую единицу памяти. -Какова же связь между базовыми типами данных и структурами данных? Мы знаем, что структуры данных - это способы организации и хранения данных в компьютере. Подлежащее в этой фразе - "структура", а не "данные". +Какова же связь между базовыми типами данных и структурами данных? Мы знаем, что структура данных - это способ организации и хранения данных в компьютере. Подлежащее в этой фразе - "структура", а не "данные". -Если мы хотим представить "ряд чисел", то естественно подумаем об использовании массива. Это связано с тем, что линейная структура массива может выразить отношения соседства и порядка между числами, а вот то, что именно хранится внутри - целые `int` , вещественные `float` или символы `char` , - к "структуре данных" отношения не имеет. +Если мы хотим представить "ряд чисел", то естественно подумаем об использовании массива. Это связано с тем, что линейная структура массива может выразить отношения соседства и порядка между числами, а то, что именно хранится внутри - целые `int` , вещественные `float` или символы `char` , - к "структуре данных" отношения не имеет. Иными словами, **базовые типы данных задают "тип содержимого" данных, а структуры данных задают "способ организации" данных**. Например, в следующем коде мы используем одну и ту же структуру данных (массив) для хранения и представления различных базовых типов данных, включая `int` , `float` , `char` , `bool` и т.д. === "Python" ```python title="" - # Инициализируем массивы с использованием разных базовых типов данных + # Инициализируем массивы с использованием различных базовых типов данных numbers: list[int] = [0] * 5 decimals: list[float] = [0.0] * 5 - # В Python символы на деле являются строками длины 1 + # В Python символы фактически являются строками длины 1 characters: list[str] = ['0'] * 5 bools: list[bool] = [False] * 5 - # Списки Python могут свободно хранить разные базовые типы данных и ссылки на объекты + # Списки Python могут свободно хранить различные базовые типы данных и ссылки на объекты data = [0, 0.0, 'a', False, ListNode(0)] ``` === "C++" ```cpp title="" - // Инициализируем массивы с использованием разных базовых типов данных + // Инициализируем массивы с использованием различных базовых типов данных int numbers[5]; float decimals[5]; char characters[5]; @@ -78,7 +78,7 @@ comments: true === "Java" ```java title="" - // Инициализируем массивы с использованием разных базовых типов данных + // Инициализируем массивы с использованием различных базовых типов данных int[] numbers = new int[5]; float[] decimals = new float[5]; char[] characters = new char[5]; @@ -88,7 +88,7 @@ comments: true === "C#" ```csharp title="" - // Инициализируем массивы с использованием разных базовых типов данных + // Инициализируем массивы с использованием различных базовых типов данных int[] numbers = new int[5]; float[] decimals = new float[5]; char[] characters = new char[5]; @@ -98,7 +98,7 @@ comments: true === "Go" ```go title="" - // Инициализируем массивы с использованием разных базовых типов данных + // Инициализируем массивы с использованием различных базовых типов данных var numbers = [5]int{} var decimals = [5]float64{} var characters = [5]byte{} @@ -108,7 +108,7 @@ comments: true === "Swift" ```swift title="" - // Инициализируем массивы с использованием разных базовых типов данных + // Инициализируем массивы с использованием различных базовых типов данных let numbers = Array(repeating: 0, count: 5) let decimals = Array(repeating: 0.0, count: 5) let characters: [Character] = Array(repeating: "a", count: 5) @@ -118,14 +118,14 @@ comments: true === "JS" ```javascript title="" - // Массивы JavaScript могут свободно хранить разные базовые типы данных и объекты + // Массивы JavaScript могут свободно хранить различные базовые типы данных и объекты const array = [0, 0.0, 'a', false]; ``` === "TS" ```typescript title="" - // Инициализируем массивы с использованием разных базовых типов данных + // Инициализируем массивы с использованием различных базовых типов данных const numbers: number[] = []; const characters: string[] = []; const bools: boolean[] = []; @@ -134,7 +134,7 @@ comments: true === "Dart" ```dart title="" - // Инициализируем массивы с использованием разных базовых типов данных + // Инициализируем массивы с использованием различных базовых типов данных List numbers = List.filled(5, 0); List decimals = List.filled(5, 0.0); List characters = List.filled(5, 'a'); @@ -144,7 +144,7 @@ comments: true === "Rust" ```rust title="" - // Инициализируем массивы с использованием разных базовых типов данных + // Инициализируем массивы с использованием различных базовых типов данных let numbers: Vec = vec![0; 5]; let decimals: Vec = vec![0.0; 5]; let characters: Vec = vec!['0'; 5]; @@ -154,7 +154,7 @@ comments: true === "C" ```c title="" - // Инициализируем массивы с использованием разных базовых типов данных + // Инициализируем массивы с использованием различных базовых типов данных int numbers[10]; float decimals[10]; char characters[10]; @@ -164,7 +164,7 @@ comments: true === "Kotlin" ```kotlin title="" - // Инициализируем массивы с использованием разных базовых типов данных + // Инициализируем массивы с использованием различных базовых типов данных val numbers = IntArray(5) val decinals = FloatArray(5) val characters = CharArray(5) @@ -174,7 +174,7 @@ comments: true === "Ruby" ```ruby title="" - # Списки Ruby могут свободно хранить разные базовые типы данных и ссылки на объекты + # Списки Ruby могут свободно хранить различные базовые типы данных и ссылки на объекты data = [0, 0.0, 'a', false, ListNode(0)] ``` diff --git a/ru/docs/chapter_data_structure/character_encoding.md b/ru/docs/chapter_data_structure/character_encoding.md index 9ab40e59e..bdf52add8 100644 --- a/ru/docs/chapter_data_structure/character_encoding.md +++ b/ru/docs/chapter_data_structure/character_encoding.md @@ -4,35 +4,35 @@ comments: true # 3.4   Кодирование символов * -В компьютере все данные хранятся в двоичной форме, и символ `char` не является исключением. Чтобы представлять символы, нам нужно определить "набор символов", задающий взаимно-однозначное соответствие между каждым символом и двоичным числом. Имея такой набор, компьютер может преобразовывать двоичные числа в символы простым поиском по таблице. +В компьютере все данные хранятся в виде двоичных чисел, и символьный тип данных `char` не является исключением. Для представления символов необходимо задать "таблицу символов", которая устанавливает взаимно-однозначное соответствие между каждым символом и двоичным числом. С помощью этой таблицы компьютер может преобразовывать двоичные числа в символы. -## 3.4.1   Набор символов ASCII +## 3.4.1   Таблица символов ASCII -Код ASCII - это самый ранний набор символов; его полное название - American Standard Code for Information Interchange (американский стандартный код обмена информацией). Он использует 7 двоичных битов (нижние 7 битов одного байта) для представления одного символа и способен представлять не более 128 различных символов. Как показано на рисунке 3-6, ASCII включает заглавные и строчные английские буквы, цифры 0 ~ 9, некоторые знаки препинания и некоторые управляющие символы (например перевод строки и табуляцию). +Код ASCII - это самая ранняя таблица символов; ее полное название - American Standard Code for Information Interchange (американский стандартный код обмена информацией). Для представления символов в ней используются 7 двоичных битов (нижние 7 битов одного байта), что позволяет закодировать до 128 различных символов. Как показано на рисунке 3-6, ASCII включает заглавные и строчные буквы английского алфавита, цифры 0 ~ 9, некоторые знаки препинания, а также некоторые управляющие символы (например перевод строки и табуляцию). ![Таблица ASCII](character_encoding.assets/ascii_table.png){ class="animation-figure" }

Рисунок 3-6   Таблица ASCII

-Однако **код ASCII может представлять только английский язык**. С глобализацией компьютерных технологий появился набор символов EASCII, способный покрывать больше языков. Он расширяет 7-битную основу ASCII до 8 битов и может представлять 256 различных символов. +Однако **код ASCII может представлять только английский язык**. С развитием компьютерных технологий появилась таблица символов EASCII, способная охватывать больше языков. Она расширяет 7-битную основу ASCII до 8 битов и может представлять 256 различных символов. -Во всем мире постепенно появились разные наборы EASCII, подходящие для разных регионов. Первые 128 символов в этих наборах одинаковы и соответствуют ASCII, а последние 128 символов определяются по-разному, чтобы удовлетворять потребностям разных языков. +Во всем мире постепенно появились разные таблицы EASCII, подходящие для разных регионов. Первые 128 символов в этих таблицах одинаковы и соответствуют ASCII, а последние 128 символов определяются по-разному, чтобы удовлетворять потребностям разных языков. -## 3.4.2   Набор символов GBK +## 3.4.2   Таблица символов GBK -Позже люди обнаружили, что **кода EASCII все равно недостаточно для количества символов во многих языках**. Например, китайских иероглифов существует почти сто тысяч, а в повседневном использовании нужны тысячи. В 1980 году Государственное управление стандартов Китая выпустило набор символов GB2312, включающий 6763 иероглифа, что в основном удовлетворило потребности компьютерной обработки китайского текста. +Позже люди обнаружили, что **кодов EASCII все равно недостаточно для количества символов во многих языках**. Например, китайских иероглифов существует почти сто тысяч, а в повседневном употреблении нужны тысячи. В 1980 году Государственное управление стандартов Китая выпустило таблицу символов GB2312, включающую 6763 иероглифа, что в основном удовлетворило потребности компьютерной обработки китайского текста. -Однако GB2312 не умеет работать с некоторыми редкими иероглифами и традиционными формами письма. Набор символов GBK - это расширение GB2312, содержащее в общей сложности 21886 иероглифов. В схеме кодирования GBK символы ASCII представляются одним байтом, а китайские иероглифы - двумя байтами. +Однако GB2312 не умеет работать с некоторыми редкими иероглифами и традиционными формами письма. Таблица символов GBK представляет собой расширение GB2312 и в общей сложности содержит 21886 иероглифов. В схеме кодирования GBK символы ASCII представляются одним байтом, а китайские иероглифы - двумя байтами. -## 3.4.3   Набор символов Unicode +## 3.4.3   Таблица символов Unicode -С бурным развитием компьютерной техники наборы символов и стандарты кодирования начали стремительно множиться, и это породило множество проблем. С одной стороны, такие наборы обычно определяли символы только для конкретных языков и не могли нормально работать в многоязычной среде. С другой стороны, для одного и того же языка существовало несколько стандартов кодирования; если две машины использовали разные стандарты, при обмене информацией возникали кракозябры. +С бурным развитием компьютерной техники таблицы символов и стандарты кодирования начали стремительно множиться, и это породило множество проблем. С одной стороны, такие таблицы обычно определяли символы только для конкретных языков и не могли нормально работать в многоязычной среде. С другой стороны, для одного и того же языка существовало несколько стандартов кодирования; если две машины использовали разные стандарты, при обмене информацией возникали искажения текста. -Исследователи той эпохи задумались: **если создать достаточно полный набор символов, который включит все языки и знаки мира, разве это не решит проблемы межъязыковой среды и искаженного текста**? Под влиянием этой идеи и появился большой и всеобъемлющий набор символов Unicode. +Исследователи той эпохи задумались: **если создать достаточно полную таблицу символов, которая включит все языки и знаки мира, разве это не решит проблемы многоязычной среды и искаженного текста**? Под влиянием этой идеи и появилась большая и всеобъемлющая таблица символов Unicode. -Unicode по-китайски называется "единый код" и теоретически способен вместить более миллиона символов. Его цель - собрать символы со всего мира в единый набор символов, предоставить универсальный стандарт для обработки и отображения текстов на разных языках и уменьшить количество проблем с искажением текста, вызванных различиями стандартов кодирования. +Unicode по-китайски называется "единый код" и теоретически способен вместить более миллиона символов. Его цель - собрать символы со всего мира в единую таблицу символов, предоставить универсальный стандарт для обработки и отображения текстов на разных языках и уменьшить количество проблем с искажением текста, вызванных различиями стандартов кодирования. -С момента публикации в 1991 году Unicode непрерывно расширялся, добавляя новые языки и символы. По состоянию на сентябрь 2022 года Unicode уже включал 149186 символов, в том числе буквы разных языков, знаки, а также эмодзи. В огромном наборе символов Unicode часто используемые символы занимают 2 байта, а некоторые редкие символы - 3 байта и даже 4 байта. +С момента публикации в 1991 году Unicode непрерывно расширялся, добавляя новые языки и символы. По состоянию на сентябрь 2022 года Unicode уже включал 149186 символов, в том числе буквы разных языков, знаки, а также эмодзи. В огромной таблице символов Unicode часто используемые символы занимают 2 байта, а некоторые редкие символы - 3 байта и даже 4 байта. Unicode - это универсальный набор символов, который по сути просто присваивает каждому символу номер (так называемую "кодовую точку"), **но не определяет, как именно хранить эти кодовые точки в компьютере**. Тут неизбежно возникает вопрос: если в одном тексте одновременно встречаются кодовые точки Unicode разной длины, как система должна разбирать символы? Например, если дан код длиной 2 байта, как понять, является ли это одним 2-байтовым символом или двумя 1-байтовыми? diff --git a/ru/docs/chapter_data_structure/classification_of_data_structure.md b/ru/docs/chapter_data_structure/classification_of_data_structure.md index 6d2bb4842..be23a9685 100644 --- a/ru/docs/chapter_data_structure/classification_of_data_structure.md +++ b/ru/docs/chapter_data_structure/classification_of_data_structure.md @@ -4,21 +4,21 @@ comments: true # 3.1   Классификация структур данных -К распространенным структурам данных относятся массивы, связные списки, стеки, очереди, хеш-таблицы, деревья, кучи и графы; их можно классифицировать по двум измерениям: "логическая структура" и "физическая структура". +К распространенным структурам данных относятся массивы, связные списки, стеки, очереди, хеш-таблицы, деревья, кучи и графы. Их можно классифицировать по двум измерениям: логической структуре и физической структуре. ## 3.1.1   Логическая структура: линейная и нелинейная -**Логическая структура раскрывает логические связи между элементами данных**. В массивах и связных списках данные располагаются в определенном порядке, отражая линейные отношения между элементами; в деревьях данные иерархически располагаются сверху вниз, проявляя производные отношения между "предками" и "потомками"; графы состоят из вершин и ребер и отражают сложные сетевые связи. +**Логическая структура раскрывает логические отношения между элементами данных**. В массивах и связных списках данные расположены в определенном порядке, что отражает линейные отношения между элементами. В деревьях данные расположены по уровням сверху вниз, что демонстрирует отношения "предок" и "потомок". Графы состоят из вершин и ребер, отражая сложные сетевые отношения. -Как показано на рисунке 3-1, логические структуры можно разделить на два больших класса: "линейные" и "нелинейные". Линейные структуры более интуитивны и означают, что данные логически выстроены в линию; нелинейные структуры, напротив, располагаются нелинейно. +Как показано на рисунке 3-1, логические структуры делятся на две большие категории: линейные и нелинейные. Линейные структуры более интуитивны, поскольку в них данные расположены линейно и логически связаны. Нелинейные структуры, напротив, представляют собой нелинейное расположение элементов данных. -- **Линейные структуры данных**: массивы, связные списки, стеки, очереди, хеш-таблицы; между элементами существует отношение "один к одному". +- **Линейные структуры данных**: массивы, связные списки, стеки, очереди, хеш-таблицы, в которых элементы связаны отношением "один к одному". - **Нелинейные структуры данных**: деревья, кучи, графы, хеш-таблицы. Нелинейные структуры данных можно дополнительно разделить на древовидные и сетевые. -- **Древовидные структуры**: деревья, кучи, хеш-таблицы; между элементами существует отношение "один ко многим". -- **Сетевые структуры**: графы; между элементами существует отношение "многие ко многим". +- **Древовидные структуры**: деревья, кучи, хеш-таблицы, в которых элементы связаны отношением "один ко многим". +- **Сетевые структуры**: графы, в которых элементы связаны отношением "многие ко многим". ![Линейные и нелинейные структуры данных](classification_of_data_structure.assets/classification_logic_structure.png){ class="animation-figure" } @@ -26,9 +26,9 @@ comments: true ## 3.1.2   Физическая структура: непрерывная и разрозненная -**Во время выполнения алгоритма обрабатываемые данные в основном хранятся в памяти**. На рисунке 3-2 показана планка памяти компьютера, где каждый черный блок содержит некоторый участок памяти. Мы можем представить память как огромную таблицу Excel, в которой каждая ячейка способна хранить данные определенного размера. +**Во время выполнения программы обрабатываемые данные в основном хранятся в памяти**. На рисунке 3-2 показан модуль оперативной памяти компьютера, где каждый черный блок содержит определенный участок памяти. Память можно представить как огромную таблицу Excel, в которой каждая ячейка способна хранить данные определенного размера. -**Система обращается к данным по адресу памяти соответствующей позиции**. Как показано на рисунке 3-2, компьютер по определенному правилу присваивает каждой ячейке в этой таблице номер, чтобы у каждого участка памяти был уникальный адрес. Имея эти адреса, программа может получать доступ к данным, находящимся в памяти. +**Система обращается к данным по адресам памяти соответствующих позиций**. Как показано на рисунке 3-2, компьютер по определенным правилам присваивает каждой ячейке в этой таблице номер, чтобы каждый участок памяти имел уникальный адрес. Благодаря этим адресам программа получает доступ к данным, находящимся в памяти. ![Планка памяти, участок памяти и адрес памяти](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" } @@ -38,20 +38,20 @@ comments: true Стоит отметить, что сравнение памяти с таблицей Excel - это упрощенная аналогия; реальный механизм работы памяти гораздо сложнее и включает такие понятия, как адресное пространство, управление памятью, кэш-механизмы, виртуальная и физическая память. -Память - общий ресурс для всех программ. Когда некоторый участок памяти занят одной программой, другие программы обычно не могут использовать его одновременно. **Поэтому при проектировании структур данных и алгоритмов память является важным фактором**. Например, пиковое потребление памяти алгоритмом не должно превышать доступную свободную память системы; если непрерывного крупного блока памяти недостаточно, выбранная структура данных должна уметь храниться в разрозненных областях памяти. +Память - общий ресурс для всех программ. Когда некоторый участок памяти занят одной программой, другие программы обычно не могут использовать его одновременно. **Поэтому при проектировании структур данных и алгоритмов память занимает важное место**. Например, пиковое потребление памяти алгоритмом не должно превышать объем доступной свободной памяти системы; если не хватает непрерывных крупных участков памяти, выбранная структура данных должна уметь размещаться в разрозненных областях памяти. -Как показано на рисунке 3-3, **физическая структура отражает способ хранения данных в памяти компьютера**; ее можно разделить на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Физическая структура на нижнем уровне определяет способы доступа к данным, их обновления, вставки и удаления; эти два типа физических структур взаимно дополняют друг друга по временной и пространственной эффективности. +Как показано на рисунке 3-3, **физическая структура отражает способ хранения данных в памяти компьютера**. Ее можно разделить на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Физическая структура на низком уровне определяет способы доступа к данным, их обновления, вставки и удаления. Эти два типа физических структур взаимно дополняют друг друга по временной и пространственной эффективности. ![Хранение в непрерывном и разрозненном пространстве](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" }

Рисунок 3-3   Хранение в непрерывном и разрозненном пространстве

-Стоит отметить, что **все структуры данных реализуются на основе массивов, связных списков или их комбинации**. Например, стеки и очереди можно реализовать как с помощью массивов, так и с помощью связных списков; а реализация хеш-таблицы может одновременно содержать массивы и связные списки. +Стоит отметить, что **все структуры данных реализуются на основе массивов, связных списков или их комбинации**. Например, стек и очередь можно реализовать как с помощью массивов, так и с помощью связных списков; реализация хеш-таблицы также может одновременно включать массивы и связные списки. - **Можно реализовать на основе массивов**: стеки, очереди, хеш-таблицы, деревья, кучи, графы, матрицы, тензоры (массивы размерности $\geq 3$ ) и т.д. - **Можно реализовать на основе связных списков**: стеки, очереди, хеш-таблицы, деревья, кучи, графы и т.д. -После инициализации длину связного списка все еще можно изменять во время выполнения программы, поэтому его также называют "динамической структурой данных". Длина массива после инициализации неизменна, поэтому его также называют "статической структурой данных". Стоит заметить, что массив может менять длину за счет повторного выделения памяти, тем самым приобретая определенную "динамичность". +После инициализации длину связного списка все еще можно изменять во время выполнения программы, поэтому его также называют "динамической структурой данных". Длина массива после инициализации неизменна, поэтому его также называют "статической структурой данных". Стоит отметить, что массив может изменять длину за счет повторного выделения памяти, тем самым приобретая определенную "динамичность". !!! tip diff --git a/ru/docs/chapter_data_structure/index.md b/ru/docs/chapter_data_structure/index.md index ec050a655..26afd4420 100644 --- a/ru/docs/chapter_data_structure/index.md +++ b/ru/docs/chapter_data_structure/index.md @@ -9,9 +9,9 @@ icon: material/shape-outline !!! abstract - Структуры данных подобны прочному и разнообразному каркасу. + Структуры данных подобны прочному и многообразному каркасу. - Они задают план упорядоченной организации данных, а алгоритмы на этой основе обретают жизнь. + Они задают схему упорядоченной организации данных, на основе которой оживают алгоритмы. ## Содержание главы diff --git a/ru/docs/chapter_data_structure/number_encoding.md b/ru/docs/chapter_data_structure/number_encoding.md index 25fe321d8..60f009025 100644 --- a/ru/docs/chapter_data_structure/number_encoding.md +++ b/ru/docs/chapter_data_structure/number_encoding.md @@ -10,9 +10,9 @@ comments: true ## 3.3.1   Прямой, обратный и дополнительный коды -В таблице из предыдущего раздела мы заметили, что все целочисленные типы могут представлять на одно отрицательное число больше, чем положительных. Например, диапазон `byte` равен $[-128, 127]$ . Это явление выглядит не слишком интуитивно, и его внутренняя причина связана с прямым, обратным и дополнительным кодами. +В таблице из предыдущего раздела можно заметить, что все целочисленные типы могут представлять на одно отрицательное число больше, чем положительных. Например, диапазон `byte` равен $[-128, 127]$ . Это выглядит не слишком интуитивно, и внутренняя причина связана с прямым, обратным и дополнительным кодами. -Прежде всего нужно отметить, что **числа хранятся в компьютере в форме "дополнительного кода"**. Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления. +Прежде всего нужно отметить, что **числа хранятся в компьютере в виде "дополнительного кода"**. Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления. - **Прямой код**: старший бит двоичного представления числа рассматривается как знаковый, где $0$ означает положительное число, а $1$ - отрицательное; остальные биты представляют значение числа. - **Обратный код**: для положительного числа обратный код совпадает с прямым; для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита. @@ -71,7 +71,7 @@ $$ Остается последний вопрос: диапазон типа `byte` равен $[-128, 127]$ , откуда берется лишнее отрицательное число $-128$ ? Мы замечаем, что у всех целых чисел из интервала $[-127, +127]$ есть соответствующие прямой, обратный и дополнительный коды, а прямой и дополнительный коды можно преобразовывать друг в друга. -Однако **дополнительный код $1000 \; 0000$ является исключением: у него нет соответствующего прямого кода**. Согласно правилу преобразования, прямой код для этого дополнительного кода должен быть равен $0000 \; 0000$ . Это, очевидно, противоречие, потому что такой прямой код обозначает число $0$ , а его дополнительный код должен совпадать с ним самим. Компьютер просто определяет, что этот особый дополнительный код $1000 \; 0000$ представляет число $-128$ . На самом деле результат вычисления $(-1) + (-127)$ в дополнительном коде как раз и равен $-128$ . +Однако **дополнительный код $1000 \; 0000$ является исключением: у него нет соответствующего прямого кода**. Согласно правилу преобразования, прямой код для этого дополнительного кода должен быть равен $0000 \; 0000$ . Это очевидное противоречие, потому что такой прямой код обозначает число $0$ , а его дополнительный код должен совпадать с ним самим. Компьютер просто определяет, что этот особый дополнительный код $1000 \; 0000$ представляет число $-128$ . На самом деле результат вычисления $(-1) + (-127)$ в дополнительном коде как раз и равен $-128$ . $$ \begin{aligned} @@ -84,11 +84,11 @@ $$ \end{aligned} $$ -Ты, вероятно, уже заметил, что все приведенные выше вычисления были операциями сложения. Это намекает на важный факт: **аппаратные схемы внутри компьютера в основном проектируются на основе операций сложения**. Причина в том, что сложение по сравнению с другими операциями (например умножением, делением и вычитанием) проще реализуется на аппаратном уровне, легче распараллеливается и выполняется быстрее. +Ты, вероятно, уже заметил, что все приведенные выше вычисления были операциями сложения. Это указывает на важный факт: **аппаратные схемы внутри компьютера в основном проектируются на основе операций сложения**. Причина в том, что сложение по сравнению с другими операциями (например умножением, делением и вычитанием) проще реализуется на аппаратном уровне, легче распараллеливается и выполняется быстрее. Обрати внимание: это не означает, что компьютер умеет только складывать. **Комбинируя сложение с некоторыми базовыми логическими операциями, компьютер может реализовать и другие математические операции**. Например, вычитание $a - b$ можно преобразовать в сложение $a + (-b)$ ; умножение и деление можно свести к многократному сложению или вычитанию. -Теперь можно подвести итог, почему компьютеры используют дополнительный код: с представлением в дополнительном коде компьютер может использовать одни и те же схемы и операции для сложения положительных и отрицательных чисел, без необходимости проектировать специальные аппаратные схемы для вычитания, и без особой обработки неоднозначности положительного и отрицательного нуля. Это значительно упрощает аппаратную архитектуру и повышает эффективность вычислений. +Теперь можно подвести итог, почему компьютеры используют дополнительный код: с представлением в дополнительном коде компьютер может использовать одни и те же схемы и операции для сложения положительных и отрицательных чисел, без необходимости проектировать специальные аппаратные схемы для вычитания и без особой обработки неоднозначности положительного и отрицательного нуля. Это значительно упрощает аппаратную архитектуру и повышает эффективность вычислений. Идея дополнительного кода очень изящна; из-за ограничений по объему мы на этом остановимся. Если тебе интересно, стоит изучить эту тему глубже. diff --git a/ru/docs/chapter_data_structure/summary.md b/ru/docs/chapter_data_structure/summary.md index 70cadc5e3..b99a97ce8 100644 --- a/ru/docs/chapter_data_structure/summary.md +++ b/ru/docs/chapter_data_structure/summary.md @@ -6,14 +6,14 @@ comments: true ### 1.   Ключевые выводы -- Структуры данных можно классифицировать с двух точек зрения: логической структуры и физической структуры. Логическая структура описывает логические связи между элементами данных, а физическая структура описывает способ хранения данных в памяти компьютера. -- К распространенным логическим структурам относятся линейные, древовидные и сетевые. Обычно мы делим структуры данных по логической структуре на линейные (массивы, связные списки, стеки, очереди) и нелинейные (деревья, графы, кучи). Реализация хеш-таблицы может одновременно включать линейные и нелинейные структуры данных. -- Во время работы программы данные хранятся в памяти компьютера. У каждого участка памяти есть собственный адрес, и программа обращается к данным именно по этим адресам. -- Физическая структура в основном делится на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Все структуры данных реализуются на основе массивов, связных списков или их комбинации. -- К базовым типам данных в компьютере относятся целые `byte` , `short` , `int` , `long` , числа с плавающей точкой `float` , `double` , символы `char` и логический тип `bool` . Их диапазон значений определяется объемом занимаемого пространства и способом представления. +- Структуры данных можно классифицировать с точки зрения логической и физической структуры. Логическая структура описывает логические отношения между элементами данных, а физическая структура описывает способ хранения данных в памяти компьютера. +- К распространенным логическим структурам относятся линейные, древовидные и сетевые. Обычно структуры данных делятся на линейные (массивы, связные списки, стеки, очереди) и нелинейные (деревья, графы, кучи). Реализация хеш-таблицы может включать как линейные, так и нелинейные структуры данных. +- При выполнении программы данные хранятся в памяти компьютера. Каждый участок памяти имеет соответствующий адрес, с помощью которого программа получает доступ к данным. +- Физическая структура делится на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Все структуры данных реализуются на основе массивов, связных списков или их комбинации. +- Базовые типы данных в компьютере включают целые `byte` , `short` , `int` , `long` , числа с плавающей точкой `float` , `double` , символы `char` и логический тип `bool` . Их диапазон значений зависит от объема занимаемого пространства и способа представления. - Прямой код, обратный код и дополнительный код - это три способа кодирования чисел в компьютере, между которыми можно выполнять взаимные преобразования. В прямом коде старший бит целого числа является знаковым, а остальные биты представляют значение числа. -- Целые числа в компьютере хранятся в виде дополнительного кода. В таком представлении компьютер может одинаково обрабатывать сложение положительных и отрицательных чисел, не проектируя специальную аппаратную схему отдельно для вычитания, и при этом не возникает неоднозначности положительного и отрицательного нуля. -- Кодирование числа с плавающей точкой состоит из 1 бита знака, 8 битов экспоненты и 23 битов мантиссы. Благодаря наличию экспоненты диапазон значений у чисел с плавающей точкой намного больше, чем у целых, но расплачиваться за это приходится точностью. +- Целые числа в компьютере хранятся в виде дополнительного кода. В таком представлении компьютер может одинаково обрабатывать сложение положительных и отрицательных чисел без специальной аппаратной схемы для вычитания, и при этом исчезает неоднозначность положительного и отрицательного нуля. +- Кодирование числа с плавающей точкой состоит из 1 бита знака, 8 битов экспоненты и 23 битов мантиссы. Благодаря наличию экспоненты диапазон значений у чисел с плавающей точкой намного больше, чем у целых, но это достигается ценой потери точности. - ASCII - это самый ранний набор английских символов длиной 1 байт, включающий в общей сложности 127 символов. Набор GBK - распространенный китайский набор символов, включающий более двадцати тысяч иероглифов. Unicode стремится предоставить единый полный стандарт набора символов, включающий символы всех языков мира, чтобы решить проблемы искаженного текста, вызванные несовместимыми способами кодирования. - UTF-8 - самый популярный способ кодирования Unicode, обладающий очень хорошей универсальностью. Это кодировка переменной длины, хорошо расширяемая и эффективно использующая память. UTF-16 и UTF-32 относятся к кодировкам фиксированной длины. При кодировании китайского текста UTF-16 занимает меньше места, чем UTF-8. Такие языки программирования, как Java и C#, по умолчанию используют UTF-16. diff --git a/ru/docs/chapter_divide_and_conquer/binary_search_recur.md b/ru/docs/chapter_divide_and_conquer/binary_search_recur.md index 93a795660..abb848cd2 100644 --- a/ru/docs/chapter_divide_and_conquer/binary_search_recur.md +++ b/ru/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -2,35 +2,35 @@ comments: true --- -# 12.2   Поисковая стратегия divide and conquer +# 12.2   Поисковая стратегия разделяй и властвуй Мы уже знаем, что алгоритмы поиска делятся на две большие категории. - **Полный перебор**: реализуется через обход структуры данных, временная сложность равна $O(n)$ . - **Адаптивный поиск**: использует особую организацию данных или априорную информацию, временная сложность может достигать $O(\log n)$ и даже $O(1)$ . -На практике **алгоритмы поиска с временной сложностью $O(\log n)$ обычно реализуются на основе стратегии divide and conquer**, например двоичный поиск и деревья. +На практике **алгоритмы поиска с временной сложностью $O(\log n)$ обычно реализуются на основе стратегии "разделяй и властвуй"**, например двоичный поиск и деревья. - На каждом шаге двоичный поиск раскладывает задачу (поиск целевого элемента в массиве) на более мелкую задачу (поиск целевого элемента в одной половине массива), и этот процесс продолжается, пока массив не станет пустым или пока не будет найден целевой элемент. -- Деревья являются типичными представителями идей divide and conquer; в таких структурах данных, как двоичное дерево поиска, AVL-дерево и куча, временная сложность различных операций равна $O(\log n)$ . +- Деревья являются типичными представителями идей "разделяй и властвуй"; в таких структурах данных, как двоичное дерево поиска, AVL-дерево и куча, временная сложность различных операций равна $O(\log n)$ . -Стратегия divide and conquer для двоичного поиска выглядит следующим образом. +Стратегия "разделяй и властвуй" для двоичного поиска выглядит следующим образом. - **Задача раскладывается на части**: двоичный поиск рекурсивно разбивает исходную задачу (поиск в массиве) на подзадачу (поиск в одной половине массива), и это достигается сравнением среднего элемента с целевым значением. - **Подзадачи независимы**: в двоичном поиске на каждом шаге обрабатывается только одна подзадача, и она не зависит от других подзадач. - **Решения подзадач не нужно объединять**: двоичный поиск нацелен на поиск конкретного элемента, поэтому объединять решения подзадач не требуется. Как только подзадача решена, одновременно считается решенной и исходная задача. -По сути divide and conquer повышает эффективность поиска потому, что при полном переборе за один шаг удается исключить только один вариант, **тогда как при поиске на основе divide and conquer за один шаг можно исключить половину вариантов**. +Иными словами, стратегия "разделяй и властвуй" повышает эффективность поиска потому, что при полном переборе за один шаг удается исключить только один вариант, **тогда как при поиске на основе "разделяй и властвуй" за один шаг можно исключить половину вариантов**. -### 1.   Реализация двоичного поиска на основе divide and conquer +### 1.   Реализация двоичного поиска на основе "разделяй и властвуй" -В предыдущих главах двоичный поиск реализовывался через итерацию. Теперь реализуем его с помощью divide and conquer, то есть через рекурсию. +В предыдущих главах двоичный поиск реализовывался через итерацию. Теперь реализуем его с помощью стратегии "разделяй и властвуй", то есть через рекурсию. !!! question Дан отсортированный массив `nums` длины $n$ , в котором все элементы уникальны. Найдите элемент `target` . -С точки зрения divide and conquer обозначим подзадачу, соответствующую интервалу поиска $[i, j]$ , через $f(i, j)$ . +С точки зрения стратегии "разделяй и властвуй" обозначим подзадачу, соответствующую интервалу поиска $[i, j]$ , через $f(i, j)$ . Начиная с исходной задачи $f(0, n-1)$ , выполняем двоичный поиск по следующим шагам. @@ -38,11 +38,11 @@ comments: true 2. Рекурсивно решить подзадачу вдвое меньшего размера; это может быть либо $f(i, m-1)$ , либо $f(m+1, j)$ . 3. Повторять шаг `1.` и шаг `2.` , пока не будет найден `target` или пока интервал не станет пустым. -На рисунке 12-4 показан процесс применения divide and conquer для поиска элемента $6$ в массиве. +На рисунке 12-4 показан процесс применения стратегии "разделяй и властвуй" для поиска элемента $6$ в массиве. -![Процесс двоичного поиска в стиле divide and conquer](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" } +![Процесс двоичного поиска в стиле разделяй и властвуй](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" } -

Рисунок 12-4   Процесс двоичного поиска в стиле divide and conquer

+

Рисунок 12-4   Процесс двоичного поиска в стиле разделяй и властвуй

В реализации кода мы объявляем рекурсивную функцию `dfs()` для решения задачи $f(i, j)$ : diff --git a/ru/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/ru/docs/chapter_divide_and_conquer/build_binary_tree_problem.md index 441e80df9..15b38f816 100644 --- a/ru/docs/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/ru/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -12,17 +12,17 @@ comments: true

Рисунок 12-5   Пример данных для построения двоичного дерева

-### 1.   Проверка, является ли это задачей divide and conquer +### 1.   Проверка, является ли это задачей "разделяй и властвуй" -Исходная задача - построить двоичное дерево по `preorder` и `inorder` - является типичной задачей divide and conquer. +Исходная задача - построить двоичное дерево по `preorder` и `inorder` - является типичной задачей для стратегии "разделяй и властвуй". -- **Задача раскладывается на части**: если смотреть с точки зрения divide and conquer, исходную задачу можно разбить на две подзадачи: построение левого поддерева и построение правого поддерева, плюс одно действие: инициализация корневого узла. Для каждого поддерева (подзадачи) можно использовать тот же способ разбиения, пока не будет достигнута наименьшая подзадача (пустое поддерево). +- **Задача раскладывается на части**: если смотреть с точки зрения стратегии "разделяй и властвуй", исходную задачу можно разбить на две подзадачи: построение левого поддерева и построение правого поддерева, плюс одно действие: инициализация корневого узла. Для каждого поддерева (подзадачи) можно использовать тот же способ разбиения, пока не будет достигнута наименьшая подзадача (пустое поддерево). - **Подзадачи независимы**: левое и правое поддеревья независимы друг от друга и не пересекаются. При построении левого поддерева нам нужно смотреть только на ту часть прямого и симметричного обходов, которая соответствует левому поддереву. Для правого поддерева рассуждение аналогично. - **Решения подзадач можно объединить**: когда левое и правое поддеревья (решения подзадач) уже построены, их можно присоединить к корневому узлу и тем самым получить решение исходной задачи. ### 2.   Как разделить поддеревья -Из анализа выше видно, что эта задача действительно решается через divide and conquer, **но как именно, имея прямой обход `preorder` и симметричный обход `inorder`, разделить левое и правое поддеревья**? +Из анализа выше видно, что эта задача действительно решается через "разделяй и властвуй", **но как именно, имея прямой обход `preorder` и симметричный обход `inorder`, отделить левое и правое поддеревья**? По определению и `preorder` , и `inorder` можно разбить на три части. @@ -61,7 +61,7 @@ comments: true -Обратите внимание, что $(m-l)$ в индексе корневого узла правого поддерева означает "число узлов в левом поддереве"; лучше всего понимать это выражение вместе с рисунком ниже. +Стоит отметить, что $(m-l)$ в индексе корневого узла правого поддерева означает число узлов в левом поддереве; лучше всего понимать это выражение вместе с рисунком ниже. ![Представление индексных интервалов корня и поддеревьев](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" } diff --git a/ru/docs/chapter_divide_and_conquer/divide_and_conquer.md b/ru/docs/chapter_divide_and_conquer/divide_and_conquer.md index 706e7524c..71e230ee8 100644 --- a/ru/docs/chapter_divide_and_conquer/divide_and_conquer.md +++ b/ru/docs/chapter_divide_and_conquer/divide_and_conquer.md @@ -2,25 +2,25 @@ comments: true --- -# 12.1   Алгоритмы "разделяй и властвуй" +# 12.1   Стратегия разделяй и властвуй -Разделяй и властвуй (divide and conquer) - это очень важная и широко используемая стратегия построения алгоритмов. Обычно она реализуется через рекурсию и включает два этапа: "разделение" и "решение". +Разделяй и властвуй (divide and conquer) - это очень важная и широко используемая стратегия построения алгоритмов. Обычно она реализуется через рекурсию и включает два этапа: "разделение" и "объединение". 1. **Разделение (этап декомпозиции)**: рекурсивно разбить исходную задачу на две или более подзадачи, пока не будет достигнута наименьшая подзадача. -2. **Решение (этап объединения)**: начиная с уже известных решений наименьших подзадач, снизу вверх объединять решения подзадач и тем самым получать решение исходной задачи. +2. **Объединение (этап синтеза)**: начиная с уже известных решений наименьших подзадач, снизу вверх объединять решения подзадач и тем самым получать решение исходной задачи. Как показано на рисунке 12-1, "сортировка слиянием" является одним из типичных примеров применения стратегии "разделяй и властвуй". 1. **Разделение**: рекурсивно разделить исходный массив (исходную задачу) на два подмассива (подзадачи), пока в подмассиве не останется только один элемент (наименьшая подзадача). -2. **Решение**: снизу вверх объединять упорядоченные подмассивы (решения подзадач), чтобы получить упорядоченный исходный массив (решение исходной задачи). +2. **Объединение**: снизу вверх объединять упорядоченные подмассивы (решения подзадач), чтобы получить упорядоченный исходный массив (решение исходной задачи). -![Стратегия divide and conquer в сортировке слиянием](divide_and_conquer.assets/divide_and_conquer_merge_sort.png){ class="animation-figure" } +![Стратегия разделяй и властвуй в сортировке слиянием](divide_and_conquer.assets/divide_and_conquer_merge_sort.png){ class="animation-figure" } -

Рисунок 12-1   Стратегия divide and conquer в сортировке слиянием

+

Рисунок 12-1   Стратегия разделяй и властвуй в сортировке слиянием

-## 12.1.1   Как определить задачу divide and conquer +## 12.1.1   Как определить задачу "разделяй и властвуй" -Чтобы понять, подходит ли задача для решения методом divide and conquer, обычно можно ориентироваться на следующие критерии. +Чтобы понять, подходит ли задача для решения методом "разделяй и властвуй", обычно можно ориентироваться на следующие критерии. 1. **Задача раскладывается на части**: исходную задачу можно разбить на более мелкие и похожие подзадачи, причем такое разбиение можно применять рекурсивно. 2. **Подзадачи независимы**: подзадачи не пересекаются, не зависят друг от друга и могут решаться независимо. @@ -32,11 +32,11 @@ comments: true 2. **Подзадачи независимы**: каждый подмассив можно сортировать отдельно (то есть каждую подзадачу можно решать независимо). 3. **Решения подзадач можно объединить**: два упорядоченных подмассива (решения подзадач) можно объединить в один упорядоченный массив (решение исходной задачи). -## 12.1.2   Повышение эффективности с помощью divide and conquer +## 12.1.2   Повышение эффективности с помощью "разделяй и властвуй" -**Стратегия divide and conquer не только позволяет эффективно решать алгоритмические задачи, но и часто повышает эффективность самих алгоритмов**. Именно поэтому быстрая сортировка, сортировка слиянием и пирамидальная сортировка обычно работают быстрее, чем сортировка выбором, пузырьком и вставками. +**Стратегия "разделяй и властвуй" не только позволяет эффективно решать алгоритмические задачи, но и часто повышает эффективность самих алгоритмов**. Именно поэтому быстрая сортировка, сортировка слиянием и пирамидальная сортировка обычно работают быстрее, чем сортировка выбором, пузырьком и вставками. -Тогда возникает естественный вопрос: **почему divide and conquer повышает эффективность алгоритма и какова логика этого на более глубоком уровне**? Иными словами, почему разбиение большой задачи на несколько подзадач, решение этих подзадач и последующее объединение их решений оказывается эффективнее, чем прямое решение исходной задачи? Этот вопрос можно рассмотреть с двух сторон: через число операций и через параллельные вычисления. +Тогда возникает естественный вопрос: **почему стратегия "разделяй и властвуй" повышает эффективность алгоритма и какова внутренняя логика этого подхода**? Иными словами, почему разбиение большой задачи на несколько подзадач, решение этих подзадач и последующее объединение их решений оказывается эффективнее, чем прямое решение исходной задачи? Этот вопрос можно рассмотреть с двух сторон: через число операций и через параллельные вычисления. ### 1.   Оптимизация числа операций @@ -64,11 +64,11 @@ $$ Если пойти дальше и **продолжать делить каждый подмассив пополам**, пока в нем не останется только один элемент, то мы фактически получим "сортировку слиянием", чья временная сложность равна $O(n \log n)$ . -Можно пойти еще дальше и спросить: **что если задать несколько точек разделения** и равномерно разбить исходный массив на $k$ подмассивов? Такая ситуация очень похожа на "блочную сортировку", которая особенно хорошо подходит для сортировки очень больших объемов данных и теоретически может достигать временной сложности $O(n + k)$ . +Можно пойти еще дальше и спросить: **что если задать несколько точек разделения** и равномерно разбить исходный массив на $k$ подмассивов? Такая ситуация очень похожа на блочную сортировку, которая особенно хорошо подходит для сортировки очень больших объемов данных и теоретически может достигать временной сложности $O(n + k)$ . ### 2.   Оптимизация параллельных вычислений -Мы знаем, что подзадачи, порождаемые divide and conquer, являются независимыми, **а значит, их обычно можно решать параллельно**. Иначе говоря, divide and conquer не только может уменьшить временную сложность алгоритма, **но и хорошо сочетается с параллельной оптимизацией на уровне системы**. +Мы знаем, что подзадачи, порождаемые стратегией "разделяй и властвуй", являются независимыми, **а значит, их обычно можно решать параллельно**. Иначе говоря, "разделяй и властвуй" не только может уменьшить временную сложность алгоритма, **но и хорошо сочетается с параллельной оптимизацией на уровне системы**. Параллельная оптимизация особенно эффективна в среде с несколькими ядрами или несколькими процессорами, потому что система может одновременно обрабатывать разные подзадачи, лучше загружая вычислительные ресурсы и тем самым заметно сокращая общее время работы. @@ -78,24 +78,24 @@ $$

Рисунок 12-3   Параллельные вычисления в блочной сортировке

-## 12.1.3   Типичные применения divide and conquer +## 12.1.3   Типичные применения стратегии "разделяй и властвуй" -С одной стороны, divide and conquer можно использовать для решения многих классических алгоритмических задач. +С одной стороны, стратегию "разделяй и властвуй" можно использовать для решения многих классических алгоритмических задач. - **Поиск ближайшей пары точек**: сначала множество точек делится на две части, затем ищется ближайшая пара в каждой части, а затем ближайшая пара, пересекающая границу между двумя частями. - **Умножение больших чисел**: например, алгоритм Карацубы, который раскладывает умножение больших чисел на несколько умножений и сложений меньших чисел. - **Умножение матриц**: например, алгоритм Штрассена, который раскладывает умножение больших матриц на несколько умножений и сложений матриц меньшего размера. -- **Задача о Ханойской башне**: задача о Ханойской башне решается рекурсивно и является типичным примером применения divide and conquer. -- **Подсчет инверсий**: если в последовательности предыдущее число больше следующего, то такая пара образует инверсию. Эту задачу можно решить с помощью идей divide and conquer, опираясь на сортировку слиянием. +- **Задача о Ханойской башне**: задача о Ханойской башне решается рекурсивно и является типичным примером применения стратегии "разделяй и властвуй". +- **Подсчет инверсий**: если в последовательности предыдущее число больше следующего, то такая пара образует инверсию. Эту задачу можно решить с помощью идей "разделяй и властвуй", опираясь на сортировку слиянием. -С другой стороны, divide and conquer очень широко применяется при проектировании алгоритмов и структур данных. +С другой стороны, стратегия "разделяй и властвуй" очень широко применяется при проектировании алгоритмов и структур данных. - **Двоичный поиск**: двоичный поиск делит отсортированный массив на две части по индексу середины, а затем, в зависимости от результата сравнения целевого значения со средним элементом, исключает одну из половин и повторяет ту же операцию на оставшемся интервале. - **Сортировка слиянием**: она уже была рассмотрена в начале этого раздела, поэтому не будем повторяться. - **Быстрая сортировка**: в ней выбирается опорное значение, после чего массив делится на два подмассива: один содержит элементы меньше опорного, а другой - больше. Затем такая же операция повторяется для обеих частей, пока в подмассиве не останется один элемент. - **Блочная сортировка**: ее основная идея заключается в распределении данных по нескольким блокам, сортировке элементов внутри каждого блока и последующем последовательном извлечении элементов из блоков для построения отсортированного массива. -- **Деревья**: например, двоичные деревья поиска, AVL-деревья, красно-черные деревья, B-деревья, B+ деревья и т.д. Их операции поиска, вставки и удаления можно рассматривать как применение divide and conquer. -- **Кучи**: куча является особым видом полного бинарного дерева, а такие операции, как вставка, удаление и упорядочивание, по сути содержат идеи divide and conquer. -- **Хеш-таблицы**: хотя хеш-таблицы напрямую не используют divide and conquer, некоторые способы разрешения коллизий косвенно опираются на эту стратегию. Например, длинные цепочки в методе цепочек могут преобразовываться в красно-черные деревья для повышения эффективности поиска. +- **Деревья**: например, двоичные деревья поиска, AVL-деревья, красно-черные деревья, B-деревья, B+ деревья и т.д. Их операции поиска, вставки и удаления можно рассматривать как применение стратегии "разделяй и властвуй". +- **Кучи**: куча является особым видом полного двоичного дерева, а такие операции, как вставка, удаление и упорядочивание, по сути содержат идеи "разделяй и властвуй". +- **Хеш-таблицы**: хотя хеш-таблицы напрямую не используют стратегию "разделяй и властвуй", некоторые способы разрешения коллизий косвенно опираются на эту идею. Например, длинные цепочки в методе цепочек могут преобразовываться в красно-черные деревья для повышения эффективности поиска. -Нетрудно заметить, что **divide and conquer - это "тихая" алгоритмическая идея**, скрыто присутствующая внутри самых разных алгоритмов и структур данных. +Нетрудно заметить, что **"разделяй и властвуй" - это "тихая" алгоритмическая идея**, скрыто присутствующая внутри самых разных алгоритмов и структур данных. diff --git a/ru/docs/chapter_divide_and_conquer/hanota_problem.md b/ru/docs/chapter_divide_and_conquer/hanota_problem.md index 0d21e6b1e..411f18b0e 100644 --- a/ru/docs/chapter_divide_and_conquer/hanota_problem.md +++ b/ru/docs/chapter_divide_and_conquer/hanota_problem.md @@ -56,9 +56,9 @@ comments: true ### 2.   Разбиение на подзадачи -Для задачи $f(3)$ , то есть когда имеется три диска, ситуация становится немного сложнее. +Для задачи $f(3)$ , то есть когда имеется три диска, ситуация становится сложнее. -Поскольку решения $f(1)$ и $f(2)$ уже известны, можно подойти к задаче с точки зрения divide and conquer и **рассматривать два верхних диска на `A` как единое целое**, выполняя шаги, показанные на рисунке 12-13. Так три диска успешно перемещаются с `A` на `C` . +Поскольку решения $f(1)$ и $f(2)$ уже известны, можно подойти к задаче с точки зрения стратегии "разделяй и властвуй" и **рассматривать два верхних диска на `A` как единое целое**, выполняя шаги, показанные на рисунке 12-13. Так три диска успешно перемещаются с `A` на `C` . 1. Сделать `B` целевым стержнем, а `C` буферным, и переместить два диска с `A` на `B` . 2. Переместить оставшийся один диск с `A` напрямую на `C` . @@ -78,9 +78,9 @@ comments: true

Рисунок 12-13   Решение задачи размера 3

-По своей сути **мы разбиваем задачу $f(3)$ на две подзадачи $f(2)$ и одну подзадачу $f(1)$** . Если последовательно решить эти три подзадачи, исходная задача тоже будет решена. Это показывает, что подзадачи независимы и что их решения можно объединить. +Иначе говоря, **мы разбиваем задачу $f(3)$ на две подзадачи $f(2)$ и одну подзадачу $f(1)$** . Если последовательно решить эти три подзадачи, исходная задача тоже будет решена. Это показывает, что подзадачи независимы и что их решения можно объединить. -Таким образом, можно сформулировать показанную на рисунке 12-14 стратегию divide and conquer для задачи о Ханойской башне: исходная задача $f(n)$ разбивается на две подзадачи $f(n-1)$ и одну подзадачу $f(1)$ , которые затем решаются в следующем порядке. +Таким образом, можно сформулировать показанную на рисунке 12-14 стратегию "разделяй и властвуй" для задачи о Ханойской башне: исходная задача $f(n)$ разбивается на две подзадачи $f(n-1)$ и одну подзадачу $f(1)$ , которые затем решаются в следующем порядке. 1. Переместить $n-1$ дисков с `A` на `B` с помощью `C` . 2. Переместить оставшийся $1$ диск напрямую с `A` на `C` . @@ -88,9 +88,9 @@ comments: true Для двух подзадач $f(n-1)$ **можно применять тот же способ рекурсивного разбиения**, пока не будет достигнута наименьшая подзадача $f(1)$ . А решение для $f(1)$ уже известно и требует всего одного перемещения. -![Стратегия divide and conquer для решения задачи о Ханойской башне](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" } +![Стратегия разделяй и властвуй для решения задачи о Ханойской башне](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" } -

Рисунок 12-14   Стратегия divide and conquer для решения задачи о Ханойской башне

+

Рисунок 12-14   Стратегия разделяй и властвуй для решения задачи о Ханойской башне

### 3.   Реализация кода diff --git a/ru/docs/chapter_divide_and_conquer/index.md b/ru/docs/chapter_divide_and_conquer/index.md index 6b1696064..0138c795b 100644 --- a/ru/docs/chapter_divide_and_conquer/index.md +++ b/ru/docs/chapter_divide_and_conquer/index.md @@ -15,8 +15,8 @@ icon: material/set-split ## Содержание главы -- [12.1   Алгоритмы разделяй и властвуй](divide_and_conquer.md) -- [12.2   Стратегия поиска разделяй и властвуй](binary_search_recur.md) +- [12.1   Стратегия разделяй и властвуй](divide_and_conquer.md) +- [12.2   Поисковая стратегия разделяй и властвуй](binary_search_recur.md) - [12.3   Задача построения двоичного дерева](build_binary_tree_problem.md) - [12.4   Задача о Ханойской башне](hanota_problem.md) - [12.5   Резюме](summary.md) diff --git a/ru/docs/chapter_divide_and_conquer/summary.md b/ru/docs/chapter_divide_and_conquer/summary.md index f66791086..14be5fe30 100644 --- a/ru/docs/chapter_divide_and_conquer/summary.md +++ b/ru/docs/chapter_divide_and_conquer/summary.md @@ -6,12 +6,12 @@ comments: true ### 1.   Ключевые выводы -- Divide and conquer - это распространенная стратегия проектирования алгоритмов, которая включает два этапа: разделение (декомпозицию) и решение (объединение), и обычно реализуется с помощью рекурсии. +- "Разделяй и властвуй" - это распространенная стратегия проектирования алгоритмов, которая включает два этапа: разделение (декомпозицию) и объединение (синтез), и обычно реализуется с помощью рекурсии. - Критерии применимости этой стратегии к задаче включают: возможность разложения задачи, независимость подзадач и возможность объединения их решений. -- Сортировка слиянием является типичным применением divide and conquer: она рекурсивно делит массив на два равных по длине подмассива, пока не останется массив из одного элемента, после чего начинает поэтапное объединение. -- Введение стратегии divide and conquer часто позволяет повысить эффективность алгоритма. С одной стороны, стратегия уменьшает число операций; с другой - после разбиения она способствует параллельной оптимизации на уровне системы. -- Divide and conquer не только помогает решать многие алгоритмические задачи, но и широко используется при проектировании структур данных и алгоритмов, поэтому его можно встретить буквально повсюду. -- По сравнению с полным перебором адаптивный поиск работает эффективнее. Алгоритмы поиска со сложностью $O(\log n)$ обычно реализуются на основе стратегии divide and conquer. -- Двоичный поиск - еще одно типичное применение divide and conquer, в котором отсутствует шаг объединения решений подзадач. Мы можем реализовать двоичный поиск рекурсивно, через divide and conquer. +- Сортировка слиянием является типичным применением стратегии "разделяй и властвуй": она рекурсивно делит массив на два равных по длине подмассива, пока не останется массив из одного элемента, после чего начинает поэтапное объединение. +- Использование стратегии "разделяй и властвуй" часто позволяет повысить эффективность алгоритма. С одной стороны, она уменьшает число операций; с другой - после разбиения способствует параллельной оптимизации на уровне системы. +- "Разделяй и властвуй" не только помогает решать многие алгоритмические задачи, но и широко используется при проектировании структур данных и алгоритмов, поэтому его можно встретить буквально повсюду. +- По сравнению с полным перебором адаптивный поиск работает эффективнее. Алгоритмы поиска со сложностью $O(\log n)$ обычно реализуются на основе стратегии "разделяй и властвуй". +- Двоичный поиск - еще одно типичное применение стратегии "разделяй и властвуй", в котором отсутствует шаг объединения решений подзадач. Его можно реализовать рекурсивно, опираясь на эту стратегию. - В задаче построения двоичного дерева исходная задача построения дерева может быть разбита на две подзадачи: построение левого и правого поддеревьев, а реализуется это через разбиение индексных интервалов прямого и симметричного обходов. - В задаче о Ханойской башне задача размера $n$ разбивается на две подзадачи размера $n-1$ и одну подзадачу размера $1$ . После последовательного решения этих трех подзадач исходная задача также оказывается решенной. diff --git a/ru/docs/chapter_dynamic_programming/dp_problem_features.md b/ru/docs/chapter_dynamic_programming/dp_problem_features.md index 58d1206cf..ae64b075d 100644 --- a/ru/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/ru/docs/chapter_dynamic_programming/dp_problem_features.md @@ -4,11 +4,11 @@ comments: true # 14.2   Свойства задач динамического программирования -В предыдущем разделе мы увидели, как динамическое программирование решает исходную задачу через разложение на подзадачи. На самом деле разложение на подзадачи - это общий алгоритмический подход, но в divide and conquer, динамическом программировании и backtracking акценты расставлены по-разному. +В предыдущем разделе мы увидели, как динамическое программирование решает исходную задачу через разложение на подзадачи. На самом деле разложение на подзадачи - это общий алгоритмический подход, но в методе "разделяй и властвуй", динамическом программировании и поиске с возвратом акценты расставлены по-разному. -- Алгоритмы divide and conquer рекурсивно раскладывают исходную задачу на несколько независимых подзадач, пока не будет достигнута наименьшая подзадача, а затем в процессе возврата объединяют решения подзадач в решение исходной задачи. -- Динамическое программирование тоже раскладывает задачу рекурсивно, но его главное отличие от divide and conquer в том, что подзадачи здесь зависят друг от друга и в процессе разложения возникает много перекрывающихся подзадач. -- Алгоритм backtracking перебирает все возможные решения через попытки и откат и с помощью обрезки избегает ненужных ветвей поиска. Решение исходной задачи состоит из последовательности решений, и подзадачей можно считать префикс этой последовательности решений. +- Алгоритмы "разделяй и властвуй" рекурсивно раскладывают исходную задачу на несколько независимых подзадач, пока не будет достигнута наименьшая подзадача, а затем в процессе возврата объединяют решения подзадач в решение исходной задачи. +- Динамическое программирование тоже раскладывает задачу рекурсивно, но его главное отличие от метода "разделяй и властвуй" в том, что подзадачи здесь зависят друг от друга и в процессе разложения возникает много перекрывающихся подзадач. +- Алгоритм поиска с возвратом перебирает все возможные решения через попытки и откат и с помощью обрезки избегает ненужных ветвей поиска. Решение исходной задачи состоит из последовательности решений, и подзадачей можно считать префикс этой последовательности решений. На практике динамическое программирование часто применяется для задач оптимизации. Такие задачи не только содержат перекрывающиеся подзадачи, но и обладают еще двумя важными свойствами: оптимальной подструктурой и отсутствием последствий. diff --git a/ru/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/ru/docs/chapter_dynamic_programming/dp_solution_pipeline.md index 6973429f4..8c3f217e9 100644 --- a/ru/docs/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/ru/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -11,11 +11,11 @@ comments: true ## 14.3.1   Определение задачи -В целом, если задача содержит перекрывающиеся подзадачи, оптимальную подструктуру и удовлетворяет свойству отсутствия последствий, то она обычно подходит для решения с помощью динамического программирования. Однако извлечь все эти свойства напрямую из формулировки задачи бывает трудно. Поэтому на практике мы обычно ослабляем требования и **сначала смотрим, подходит ли задача для решения методом backtracking (полного перебора)**. +В целом, если задача содержит перекрывающиеся подзадачи, оптимальную подструктуру и удовлетворяет свойству отсутствия последствий, то она обычно подходит для решения с помощью динамического программирования. Однако извлечь все эти свойства напрямую из формулировки задачи бывает трудно. Поэтому на практике мы обычно ослабляем требования и **сначала смотрим, подходит ли задача для решения методом поиска с возвратом (полного перебора)**. -**Задачи, подходящие для backtracking, обычно удовлетворяют "модели дерева решений"**. Такие задачи можно описать деревом, где каждый узел представляет одно решение, а каждый путь представляет последовательность решений. +**Задачи, подходящие для поиска с возвратом, обычно удовлетворяют "модели дерева решений"**. Такие задачи можно описать деревом, где каждый узел представляет одно решение, а каждый путь представляет последовательность решений. -Иначе говоря, если в задаче есть четко выраженные решения и ответ порождается последовательностью таких решений, то она удовлетворяет модели дерева решений и обычно допускает решение через backtracking. +Иначе говоря, если в задаче есть четко выраженные решения и ответ порождается последовательностью таких решений, то она удовлетворяет модели дерева решений и обычно допускает решение через поиск с возвратом. Поверх этого у задач динамического программирования есть и некоторые дополнительные "плюсы". @@ -59,7 +59,7 @@ comments: true !!! note - Как в динамическом программировании, так и в backtracking, решение задачи можно описать как последовательность решений, а состояние образуется всеми переменными решений. Оно должно содержать всю информацию, достаточную для вывода следующего состояния. + Как в динамическом программировании, так и в поиске с возвратом, решение задачи можно описать как последовательность решений, а состояние образуется всеми переменными решений. Оно должно содержать всю информацию, достаточную для вывода следующего состояния. Каждому состоянию соответствует некоторая подзадача, и для хранения решений всех подзадач мы определяем таблицу $dp$ ; каждая независимая переменная состояния становится одним измерением таблицы $dp$ . По сути таблица $dp$ - это отображение от состояния к решению соответствующей подзадачи. diff --git a/ru/docs/chapter_dynamic_programming/edit_distance_problem.md b/ru/docs/chapter_dynamic_programming/edit_distance_problem.md index 6b6070f40..3d0dc5464 100644 --- a/ru/docs/chapter_dynamic_programming/edit_distance_problem.md +++ b/ru/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -4,7 +4,7 @@ comments: true # 14.6   Задача о расстоянии редактирования -Расстояние редактирования, также называемое расстоянием Левенштейна, обозначает минимальное число правок, необходимых для взаимного преобразования двух строк. Обычно оно используется для измерения сходства двух последовательностей в информационном поиске и обработке естественного языка. +Расстояние редактирования, также называемое расстоянием Левенштейна, - это минимальное количество изменений, необходимых для преобразования одной строки в другую. Обычно оно используется для измерения сходства двух последовательностей в информационном поиске и обработке естественного языка. !!! question @@ -18,7 +18,7 @@ comments: true

Рисунок 14-27   Пример данных для задачи о расстоянии редактирования

-**Задачу о расстоянии редактирования можно очень естественно описать через модель дерева решений**. Строки соответствуют узлам дерева, а один раунд решения (одна операция редактирования) соответствует одному ребру дерева. +**Задачу о расстоянии редактирования можно естественным образом объяснить с помощью модели дерева решений**. Строки соответствуют узлам дерева, а один шаг решения, то есть одна операция редактирования, соответствует одному ребру дерева. Как показано на рисунке 14-28, если не ограничивать число операций, то каждый узел может порождать множество ребер, и каждое из них соответствует одному из вариантов преобразования. Это означает, что преобразовать `hello` в `algo` можно множеством разных путей. @@ -39,7 +39,7 @@ comments: true - Если $s[n-1]$ и $t[m-1]$ совпадают, их можно просто пропустить и сразу перейти к сравнению $s[n-2]$ и $t[m-2]$ . - Если $s[n-1]$ и $t[m-1]$ различны, нужно выполнить над $s$ одну операцию редактирования (вставку, удаление или замену), чтобы последние символы стали одинаковыми, после чего можно перейти к задаче меньшего размера. -Иначе говоря, каждое решение (операция редактирования), которое мы выполняем над строкой $s$ , меняет те символы, которые еще остаются несопоставленными в строках $s$ и $t$ . Поэтому состояние определяется текущими позициями рассматриваемых символов в $s$ и $t$ , то есть состоянием $[i, j]$ . +Иначе говоря, каждый шаг решения, то есть операция редактирования над строкой $s$ , меняет те символы, которые еще необходимо сопоставить в строках $s$ и $t$ . Поэтому состояние определяется текущими позициями рассматриваемых символов в $s$ и $t$ , то есть состоянием $[i, j]$ . Подзадача, соответствующая состоянию $[i, j]$ , такова: **минимальное число операций редактирования, необходимое для преобразования первых $i$ символов строки $s$ в первые $j$ символов строки $t$**. @@ -57,7 +57,7 @@ comments: true

Рисунок 14-29   Переходы состояния в задаче о расстоянии редактирования

-Согласно этому анализу оптимальная подструктура такова: минимальное число шагов редактирования для $dp[i, j]$ равно минимуму из трех значений - $dp[i, j-1]$ , $dp[i-1, j]$ и $dp[i-1, j-1]$ - плюс цена текущей операции редактирования $1$ . Значит, уравнение перехода состояния имеет вид: +Согласно этому анализу оптимальная подструктура такова: минимальное число шагов редактирования для $dp[i, j]$ равно минимуму из трех значений - $dp[i, j-1]$ , $dp[i-1, j]$ и $dp[i-1, j-1]$ - плюс $1$ шаг за текущее редактирование. Значит, уравнение перехода состояния имеет вид: $$ dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 @@ -482,7 +482,7 @@ $$
-Как показано на рисунке 14-30, процесс переходов состояния в задаче о расстоянии редактирования очень похож на процесс в задачах о рюкзаке: в обоих случаях это заполнение двумерной сетки. +Как показано на рисунке 14-30, процесс переходов состояния в задаче о расстоянии редактирования очень похож на задачи о рюкзаке: и там и здесь его можно рассматривать как заполнение двумерной сетки. === "<1>" ![Процесс динамического программирования для расстояния редактирования](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" } @@ -535,7 +535,7 @@ $$ Поскольку $dp[i,j]$ зависит от значения сверху $dp[i-1, j]$ , слева $dp[i, j-1]$ и слева сверху $dp[i-1, j-1]$ , прямой обход после оптимизации памяти теряет значение слева сверху, а обратный обход не позволяет заранее построить значение слева $dp[i, j-1]$ . Значит, оба наивных варианта обхода здесь непригодны. -Чтобы решить эту проблему, можно использовать переменную `leftup` для временного сохранения значения слева сверху $dp[i-1, j-1]$ ; после этого остается учитывать только верхнее и левое значения. Тогда ситуация становится эквивалентной задаче о полном рюкзаке, и можно выполнять прямой обход. Код приведен ниже: +Чтобы решить эту проблему, можно использовать переменную `leftup` для временного сохранения значения слева сверху $dp[i-1, j-1]$ ; после этого остается учитывать только верхнее и левое значения. Тогда ситуация становится аналогичной задаче о полном рюкзаке, и можно выполнять прямой обход. Код приведен ниже: === "Python" diff --git a/ru/docs/chapter_dynamic_programming/index.md b/ru/docs/chapter_dynamic_programming/index.md index a80c22f6c..25dc6a868 100644 --- a/ru/docs/chapter_dynamic_programming/index.md +++ b/ru/docs/chapter_dynamic_programming/index.md @@ -15,10 +15,10 @@ icon: material/table-pivot ## Содержание главы -- [14.1   Введение в динамическое программирование](intro_to_dynamic_programming.md) +- [14.1   Первое знакомство с динамическим программированием](intro_to_dynamic_programming.md) - [14.2   Свойства задач динамического программирования](dp_problem_features.md) - [14.3   Подход к решению задач динамического программирования](dp_solution_pipeline.md) - [14.4   Задача о рюкзаке 0-1](knapsack_problem.md) -- [14.5   Задача о неограниченном рюкзаке](unbounded_knapsack_problem.md) +- [14.5   Задача о полном рюкзаке](unbounded_knapsack_problem.md) - [14.6   Задача о расстоянии редактирования](edit_distance_problem.md) - [14.7   Резюме](summary.md) diff --git a/ru/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/ru/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md index 0be32977a..565ed765d 100644 --- a/ru/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/ru/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -6,7 +6,7 @@ comments: true Динамическое программирование (dynamic programming) - это важная алгоритмическая парадигма, которая разбивает задачу на последовательность более мелких подзадач и за счет хранения их решений избегает повторных вычислений, тем самым резко повышая эффективность по времени. -В этом разделе мы начнем с классического примера: сначала запишем его грубое решение через backtracking, увидим в нем перекрывающиеся подзадачи, а затем постепенно выведем более эффективное решение на основе динамического программирования. +В этом разделе мы начнем с классического примера: сначала представим его грубое решение методом поиска с возвратом, увидим в нем перекрывающиеся подзадачи, а затем постепенно выведем более эффективное решение на основе динамического программирования. !!! question "Подъем по лестнице" @@ -18,7 +18,7 @@ comments: true

Рисунок 14-1   Число способов подняться на 3-ю ступень

-Цель этой задачи - вычислить количество способов. **Поэтому можно попробовать грубо перебрать все варианты с помощью backtracking**. Если представить подъем по лестнице как последовательность решений, то мы начинаем от земли и на каждом раунде выбираем прыжок на $1$ или на $2$ ступени; всякий раз, когда достигаем вершины, увеличиваем число способов на $1$ , а если перескакиваем вершину, обрезаем эту ветвь. Код выглядит так: +Цель этой задачи - вычислить количество способов. **Поэтому можно попробовать использовать для ее решения метод поиска с возвратом**. Если представить подъем по лестнице как последовательность решений, то мы начинаем от земли и на каждом раунде выбираем прыжок на $1$ или на $2$ ступени; всякий раз, когда достигаем вершины, увеличиваем число способов на $1$ , а если перескакиваем вершину, обрезаем эту ветвь. Код выглядит так: === "Python" @@ -427,7 +427,7 @@ comments: true ## 14.1.1   Метод 1: полный перебор -Backtracking обычно не раскладывает задачу явно на подзадачи; вместо этого он рассматривает решение как последовательность решений, используя попытки и обрезку для поиска всех возможных ответов. +Алгоритм поиска с возвратом обычно не раскладывает задачу явно на подзадачи; вместо этого он рассматривает решение как последовательность решений, используя попытки и обрезку для поиска всех возможных ответов. Попробуем посмотреть на задачу именно как на разложение подзадач. Пусть число способов добраться до ступени $i$ равно $dp[i]$ ; тогда $dp[i]$ - это исходная задача, а ее подзадачи включают: @@ -451,7 +451,7 @@ $$ По рекуррентной формуле можно получить решение полного перебора. Начиная с $dp[n]$ , **мы рекурсивно разлагаем большую задачу в сумму двух меньших задач** , пока не дойдем до наименьших подзадач $dp[1]$ и $dp[2]$ . Их решения уже известны: $dp[1] = 1$ и $dp[2] = 2$ , что означает $1$ и $2$ способа подняться соответственно на $1$-ю и $2$-ю ступени. -Посмотрите на следующий код: как и стандартный backtracking, он относится к поиску в глубину, но выглядит более компактно: +Посмотрите на следующий код: как и стандартный поиск с возвратом, он относится к поиску в глубину, но выглядит более компактно: === "Python" @@ -1349,7 +1349,7 @@ $$

Рисунок 14-5   Процесс динамического программирования для подъема по лестнице

-Как и в backtracking, в динамическом программировании используется понятие "состояние" для обозначения некоторого этапа решения задачи; каждое состояние соответствует одной подзадаче и ее локально оптимальному решению. Например, в задаче о лестнице состояние определяется текущим номером ступени $i$ . +Как и в поиске с возвратом, в динамическом программировании используется понятие "состояние" для обозначения некоторого этапа решения задачи; каждое состояние соответствует одной подзадаче и ее локально оптимальному решению. Например, в задаче о лестнице состояние определяется текущим номером ступени $i$ . На основе сказанного можно подвести несколько часто используемых терминов динамического программирования. diff --git a/ru/docs/chapter_dynamic_programming/knapsack_problem.md b/ru/docs/chapter_dynamic_programming/knapsack_problem.md index 191f01d4a..6ca946f39 100644 --- a/ru/docs/chapter_dynamic_programming/knapsack_problem.md +++ b/ru/docs/chapter_dynamic_programming/knapsack_problem.md @@ -4,7 +4,7 @@ comments: true # 14.4   Задача о рюкзаке 0-1 -Задача о рюкзаке - это очень хороший вводный пример для динамического программирования и одна из самых типичных форм задач этого класса. У нее существует множество вариантов, например задача о рюкзаке 0-1, задача о полном рюкзаке, задача о многократном рюкзаке и т.д. +Задача о рюкзаке является отличным примером для начала изучения динамического программирования и представляет собой одну из наиболее распространенных форм этой задачи. У нее существует множество вариантов, например задача о рюкзаке 0-1, задача о полном рюкзаке, задача о многократном рюкзаке и т.д. В этом разделе сначала разберем самый распространенный вариант - задачу о рюкзаке 0-1. @@ -18,9 +18,9 @@ comments: true

Рисунок 14-17   Пример данных для задачи о рюкзаке 0-1

-На задачу о рюкзаке 0-1 можно смотреть как на процесс из $n$ раундов решений: для каждого предмета есть два решения - не класть его в рюкзак или положить в рюкзак. Поэтому задача удовлетворяет модели дерева решений. +Задачу о рюкзаке 0-1 можно рассматривать как процесс из $n$ раундов принятия решений: для каждого предмета есть два решения - не класть его в рюкзак или положить в рюкзак. Поэтому задача удовлетворяет модели дерева решений. -Цель задачи - найти "максимальную суммарную стоимость при ограниченной вместимости рюкзака", значит, с большой вероятностью это задача динамического программирования. +Цель задачи - найти "максимальную суммарную стоимость при ограниченной вместимости рюкзака", а это с большой вероятностью указывает на задачу динамического программирования. **Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу $dp$** @@ -351,7 +351,7 @@ $$

Рисунок 14-18   Дерево полного перебора для задачи о рюкзаке 0-1

-### 2.   Метод 2: поиск с мемоизацией +### 2.   Метод 2: мемоизация Чтобы каждая перекрывающаяся подзадача вычислялась только один раз, используем таблицу памяти `mem` для хранения решений подзадач, где `mem[i][c]` соответствует $dp[i, c]$ . @@ -1110,14 +1110,14 @@ $$ ### 4.   Оптимизация пространства -Поскольку каждое состояние зависит только от состояния в предыдущей строке, можно использовать два массива, которые будут "перекатываться" вперед, и тем самым уменьшить пространственную сложность с $O(n^2)$ до $O(n)$ . +Поскольку каждое состояние зависит только от состояния в предыдущей строке, можно использовать два массива, которые будут продвигаться вперед по очереди, и тем самым уменьшить пространственную сложность с $O(n^2)$ до $O(n)$ . Если пойти дальше, можно спросить: можно ли оптимизировать память так, чтобы использовать только один массив? Наблюдение показывает, что каждое состояние зависит от клетки прямо сверху и клетки слева сверху. Предположим, что у нас есть только один массив, и в момент начала обхода строки $i$ он еще хранит состояния строки $i-1$ . - Если обходить массив слева направо, то к моменту вычисления $dp[i, j]$ значения слева сверху $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ могут уже быть перезаписаны, и правильный результат перехода состояния получить не удастся. - Если же обходить массив справа налево, проблема перезаписи не возникает, и переход состояния вычисляется корректно. -На рисунке 14-21 показан процесс перехода от строки $i = 1$ к строке $i = 2$ при использовании одного массива. Попробуйте сопоставить его с разницей между прямым и обратным обходом. +На рисунке 14-21 показан процесс перехода от строки $i = 1$ к строке $i = 2$ при использовании одного массива. С его помощью удобно понять различие между прямым и обратным обходом. === "<1>" ![Процесс динамического программирования после оптимизации памяти для рюкзака 0-1](knapsack_problem.assets/knapsack_dp_comp_step1.png){ class="animation-figure" } diff --git a/ru/docs/chapter_dynamic_programming/summary.md b/ru/docs/chapter_dynamic_programming/summary.md index bd91833d0..d17a90532 100644 --- a/ru/docs/chapter_dynamic_programming/summary.md +++ b/ru/docs/chapter_dynamic_programming/summary.md @@ -7,9 +7,9 @@ comments: true ### 1.   Ключевые выводы - Динамическое программирование раскладывает задачу на подзадачи и повышает вычислительную эффективность за счет хранения решений этих подзадач и устранения повторных вычислений. -- Если не учитывать затраты времени, то любую задачу динамического программирования можно решить с помощью backtracking (полного перебора), однако в дереве рекурсии возникает множество перекрывающихся подзадач, из-за чего эффективность крайне низка. После введения таблицы памяти можно хранить решения всех уже вычисленных подзадач и гарантировать, что каждая перекрывающаяся подзадача будет вычисляться только один раз. +- Если не учитывать затраты времени, то любую задачу динамического программирования можно решить с помощью поиска с возвратом (полного перебора), однако в дереве рекурсии возникает множество перекрывающихся подзадач, из-за чего эффективность крайне низка. После введения таблицы памяти можно хранить решения всех уже вычисленных подзадач и гарантировать, что каждая перекрывающаяся подзадача будет вычисляться только один раз. - Поиск с мемоизацией - это рекурсивный метод "сверху вниз", а соответствующее ему динамическое программирование - это итеративный метод "снизу вверх", похожий на заполнение таблицы. Поскольку текущее состояние обычно зависит только от части локальных состояний, можно убрать одно измерение таблицы $dp$ и тем самым снизить пространственную сложность. -- Разложение на подзадачи - это общий алгоритмический подход, но в divide and conquer, динамическом программировании и backtracking он имеет разные свойства. +- Разложение на подзадачи - это общий алгоритмический подход, но в методе "разделяй и властвуй", динамическом программировании и поиске с возвратом он имеет разные свойства. - Для задач динамического программирования характерны три главных свойства: перекрывающиеся подзадачи, оптимальная подструктура и отсутствие последствий. - Если оптимальное решение исходной задачи можно построить из оптимальных решений подзадач, то задача обладает оптимальной подструктурой. - Отсутствие последствий означает, что для данного состояния его дальнейшее развитие определяется только этим состоянием и не зависит от всех прошлых состояний. Многие задачи комбинаторной оптимизации этим свойством не обладают и потому не могут эффективно решаться с помощью динамического программирования. diff --git a/ru/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/ru/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md index b53fe1a4b..c2b9f3769 100644 --- a/ru/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/ru/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -4,7 +4,7 @@ comments: true # 14.5   Задача о полном рюкзаке -В этом разделе сначала решим еще один распространенный вариант задачи о рюкзаке - полный рюкзак, а затем рассмотрим одну из его типичных специальных форм: задачу о размене монет. +В этом разделе сначала решим еще одну распространенную задачу о рюкзаке - задачу о полном рюкзаке, а затем рассмотрим один из ее типичных частных случаев: задачу о размене монет. ## 14.5.1   Задача о полном рюкзаке @@ -18,10 +18,10 @@ comments: true ### 1.   Идея динамического программирования -Задача о полном рюкзаке очень похожа на задачу о рюкзаке 0-1; **разница состоит только в том, что число выборов каждого предмета не ограничено**. +Задача о полном рюкзаке очень похожа на задачу о рюкзаке 0-1; **разница состоит только в том, что количество выборов каждого предмета не ограничено**. - В задаче о рюкзаке 0-1 каждого предмета существует только один экземпляр, поэтому после того как предмет $i$ помещен в рюкзак, выбирать можно только из первых $i-1$ предметов. -- В задаче о полном рюкзаке число экземпляров каждого предмета бесконечно, поэтому после того как предмет $i$ помещен в рюкзак, **выбирать все еще можно из первых $i$ предметов**. +- В задаче о полном рюкзаке количество предметов не ограничено, поэтому после того как предмет $i$ помещен в рюкзак, **можно продолжать выбирать из первых $i$ предметов**. При этом состояние $[i, c]$ в задаче о полном рюкзаке может изменяться двумя способами. @@ -723,7 +723,7 @@ $$ dp[n][cap] end - ### Полный рюкзак: динамическое программирование с оптимизацией памяти ##3 + # ## Полный рюкзак: динамическое программирование с оптимизацией памяти ##3 def unbounded_knapsack_dp_comp(wgt, val, cap) n = wgt.length # Инициализация таблицы dp @@ -764,9 +764,9 @@ $$ ### 1.   Идея динамического программирования -**Задачу о размене монет можно рассматривать как частный случай задачи о полном рюкзаке** ; между ними существует следующая связь и следующие различия. +**Задачу о размене монет можно рассматривать как частный случай задачи о полном рюкзаке** ; между ними существуют следующие соответствия и различия. -- Эти две задачи можно взаимно переводить друг в друга: "предмет" соответствует "монете", "вес предмета" соответствует "номиналу монеты", а "вместимость рюкзака" соответствует "целевой сумме". +- Эти две задачи можно взаимно преобразовать: "предмет" соответствует "монете", "вес предмета" соответствует "номиналу монеты", а "вместимость рюкзака" соответствует "целевой сумме". - Цель оптимизации противоположна: в задаче о полном рюкзаке нужно максимизировать стоимость предметов, а в задаче о размене монет - минимизировать число монет. - В задаче о полном рюкзаке ищется решение, не превышающее вместимость, а в задаче о размене монет требуется **ровно** набрать целевую сумму. @@ -795,7 +795,7 @@ $$ ### 2.   Реализация кода -Большинство языков программирования не предоставляет готовую переменную $+ \infty$ для целых чисел, поэтому обычно приходится заменять ее на максимальное значение типа `int` . Но тогда возникает риск переполнения: операция $+ 1$ в уравнении перехода может переполнить большое число. +Большинство языков программирования не предоставляет представление для $+ \infty$ в целочисленном виде, поэтому обычно приходится заменять его на максимальное значение типа `int` . Но тогда возникает риск переполнения: операция $+ 1$ в уравнении перехода может переполнить большое число. Поэтому здесь мы используем число $amt + 1$ как обозначение недопустимого решения, потому что для набора суммы $amt$ максимум нужно не больше чем $amt$ монет. Перед возвратом результата проверяем, равно ли $dp[n, amt]$ значению $amt + 1$ ; если да, то возвращаем $-1$ , что означает невозможность набрать целевую сумму. Код приведен ниже: @@ -1624,7 +1624,7 @@ $$ ### 1.   Идея динамического программирования -По сравнению с предыдущей задачей теперь целью является число комбинаций. Поэтому подзадача меняется на следующую: **число комбинаций из первых $i$ видов монет, которыми можно набрать сумму $a$**. При этом таблица $dp$ по-прежнему остается двумерной матрицей размера $(n+1) \times (amt + 1)$ . +По сравнению с предыдущей задачей здесь целью является число комбинаций. Поэтому подзадача меняется на следующую: **число комбинаций из первых $i$ видов монет, которыми можно набрать сумму $a$**. При этом таблица $dp$ по-прежнему остается двумерной матрицей размера $(n+1) \times (amt + 1)$ . Число комбинаций для текущего состояния равно сумме числа комбинаций для двух решений: не брать текущую монету и брать текущую монету. Поэтому уравнение перехода состояния принимает вид: diff --git a/ru/docs/chapter_graph/graph.md b/ru/docs/chapter_graph/graph.md index 38350c84e..7bb482c95 100644 --- a/ru/docs/chapter_graph/graph.md +++ b/ru/docs/chapter_graph/graph.md @@ -4,7 +4,7 @@ comments: true # 9.1   Граф -Граф (graph) - это нелинейная структура данных, состоящая из вершин (vertex) и ребер (edge). Мы можем абстрактно представить граф $G$ как множество вершин $V$ и множество ребер $E$ . В примере ниже показан граф, содержащий 5 вершин и 7 ребер. +Граф (graph) - это нелинейная структура данных, состоящая из вершин (vertex) и ребер (edge). Граф $G$ можно абстрактно представить как множество вершин $V$ и множество ребер $E$ . Ниже приведен пример графа, содержащего 5 вершин и 7 ребер. $$ \begin{aligned} @@ -14,7 +14,7 @@ G & = \{ V, E \} \newline \end{aligned} $$ -Если рассматривать вершины как узлы, а ребра как ссылки (указатели), соединяющие эти узлы, то граф можно считать структурой данных, выросшей из связного списка. Как показано на рисунке 9-1, **по сравнению с линейными отношениями (связный список) и отношениями разделения (дерево), сетевые отношения (граф) обладают большей свободой** , а потому и сложнее. +Если рассматривать вершины как узлы, а ребра как ссылки, соединяющие узлы, граф можно считать структурой данных, расширяющей связный список. Как показано на рисунке 9-1, **по сравнению с линейными отношениями (связный список) и отношениями разделения (дерево), сетевые отношения (граф) обладают большей свободой** и потому являются более сложными. ![Связь между связным списком, деревом и графом](graph.assets/linkedlist_tree_graph.png){ class="animation-figure" } @@ -22,45 +22,45 @@ $$ ## 9.1.1   Распространенные типы и термины графов -В зависимости от того, имеют ли ребра направление, графы делятся на неориентированные графы (undirected graph) и ориентированные графы (directed graph) , как показано на рисунке 9-2. +В зависимости от наличия направления у ребер графы делятся на неориентированные графы (undirected graph) и ориентированные графы (directed graph) , как показано на рисунке 9-2. -- В неориентированном графе ребро означает "двустороннюю" связь между двумя вершинами, например отношение "друзья" в WeChat или QQ. -- В ориентированном графе ребро имеет направление, то есть ребра $A \rightarrow B$ и $A \leftarrow B$ независимы друг от друга, как, например, отношения "подписка" и "подписчик" в Weibo или Douyin. +- В неориентированном графе ребро представляет двустороннюю связь между двумя вершинами, например дружеские отношения в социальных сетях. +- В ориентированном графе ребро имеет направление, то есть ребра $A \rightarrow B$ и $A \leftarrow B$ независимы друг от друга, например отношения подписки и подписчиков. ![Ориентированный и неориентированный графы](graph.assets/directed_graph.png){ class="animation-figure" }

Рисунок 9-2   Ориентированный и неориентированный графы

-В зависимости от того, достижимы ли все вершины друг из друга, граф делится на связный граф (connected graph) и несвязный граф (disconnected graph) , как показано на рисунке 9-3. +В зависимости от того, связаны ли все вершины между собой, граф делится на связный граф (connected graph) и несвязный граф (disconnected graph) , как показано на рисунке 9-3. -- В связном графе, начиная из некоторой вершины, можно добраться до любой другой вершины. -- В несвязном графе, начиная из некоторой вершины, по крайней мере одна вершина оказывается недостижимой. +- В связном графе из любой вершины можно достичь любой другой вершины. +- В несвязном графе существует по крайней мере одна вершина, недостижимая из текущей. ![Связный и несвязный графы](graph.assets/connected_graph.png){ class="animation-figure" }

Рисунок 9-3   Связный и несвязный графы

-Мы также можем добавить к ребрам переменную "вес" и тем самым получить взвешенный граф (weighted graph) , показанный на рисунке 9-4. Например, в мобильных играх вроде Honor of Kings система может вычислять "степень близости" между игроками по времени, проведенному в совместных играх; такую сеть близости можно описать взвешенным графом. +Мы также можем добавить к ребрам переменную "вес" и получить показанный ниже взвешенный граф (weighted graph). Например, в мобильных играх вроде Honor of Kings система рассчитывает "близость" между игроками по времени совместной игры, и такую сеть близости можно представить взвешенным графом. ![Взвешенный и невзвешенный графы](graph.assets/weighted_graph.png){ class="animation-figure" }

Рисунок 9-4   Взвешенный и невзвешенный графы

-Для структуры данных "граф" используются следующие распространенные термины. +Со структурой данных "граф" связаны следующие основные термины. -- Смежность (adjacency): если между двумя вершинами существует ребро, то эти вершины называются "смежными". На рисунке 9-4 вершинам 2, 3, 5 смежна вершина 1. -- Путь (path): последовательность ребер, ведущая из вершины A в вершину B, называется "путем" от A до B. На рисунке 9-4 последовательность ребер 1-5-2-4 представляет один из путей от вершины 1 к вершине 4. -- Степень (degree): число ребер, принадлежащих вершине. Для ориентированного графа входящая степень (in-degree) показывает число ребер, ведущих в вершину, а исходящая степень (out-degree) показывает число ребер, исходящих из вершины. +- Смежность (adjacency): если между двумя вершинами существует ребро, то такие вершины называются смежными. На рисунке 9-4 с вершиной 1 смежны вершины 2, 3 и 5. +- Путь (path): последовательность ребер от вершины A до вершины B называется путем из A в B. На рисунке 9-4 последовательность ребер 1-5-2-4 является одним из путей от вершины 1 к вершине 4. +- Степень (degree): количество ребер, принадлежащих вершине. Для ориентированного графа входящая степень (in-degree) показывает, сколько ребер входит в вершину, а исходящая степень (out-degree) показывает, сколько ребер из нее выходит. ## 9.1.2   Представление графа -Распространенные способы представления графа включают "матрицу смежности" и "список смежности". Ниже в качестве примера используется неориентированный граф. +Распространенные способы представления графа включают "матрицу смежности" и "список смежности". Ниже для примера рассматривается неориентированный граф. ### 1.   Матрица смежности -Пусть число вершин графа равно $n$ ; тогда матрица смежности (adjacency matrix) использует матрицу размера $n \times n$ для представления графа: каждая строка и каждый столбец соответствуют вершине, а элементы матрицы отражают наличие ребра, то есть показывают, существует между двумя вершинами связь или нет. +Пусть число вершин графа равно $n$ ; тогда матрица смежности (adjacency matrix) использует матрицу размера $n \times n$ для представления графа, где каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают наличие или отсутствие ребра. -Как показано на рисунке 9-5, пусть матрица смежности обозначается как $M$ , а список вершин - как $V$ ; тогда элемент матрицы $M[i, j] = 1$ означает, что между вершинами $V[i]$ и $V[j]$ существует ребро, а элемент $M[i, j] = 0$ означает, что ребра между ними нет. +Как показано на рисунке 9-5, обозначим матрицу смежности через $M$ , а список вершин через $V$ ; тогда элемент матрицы $M[i, j] = 1$ означает наличие ребра между вершинами $V[i]$ и $V[j]$ , а элемент $M[i, j] = 0$ означает отсутствие ребра. ![Представление графа матрицей смежности](graph.assets/adjacency_matrix.png){ class="animation-figure" } @@ -68,27 +68,27 @@ $$ Матрица смежности обладает следующими особенностями. -- В простом графе вершина не может соединяться сама с собой, поэтому элементы на главной диагонали матрицы смежности не имеют смысла. -- Для неориентированного графа ребра в двух направлениях эквивалентны, поэтому матрица смежности симметрична относительно главной диагонали. -- Если заменить в матрице смежности значения $1$ и $0$ на веса, то можно представить и взвешенный граф. +- В простом графе вершина не может соединяться сама с собой, поэтому элементы на главной диагонали матрицы смежности не имеют значения. +- Для неориентированного графа ребра в обоих направлениях эквивалентны, поэтому матрица смежности симметрична относительно главной диагонали. +- Если заменить в матрице смежности значения $1$ и $0$ на веса, то можно представить взвешенный граф. -При представлении графа матрицей смежности мы можем напрямую обращаться к элементам матрицы, чтобы получить информацию о ребрах, поэтому операции добавления, удаления, поиска и изменения обладают высокой эффективностью, равной $O(1)$ . Однако пространственная сложность матрицы равна $O(n^2)$ , поэтому она занимает заметный объем памяти. +При представлении графа матрицей смежности можно напрямую обращаться к элементам матрицы и получать сведения о ребрах, поэтому операции добавления, удаления, поиска и изменения обладают высокой эффективностью и выполняются за $O(1)$ . Однако пространственная сложность матрицы составляет $O(n^2)$ , поэтому она требует значительных затрат памяти. ### 2.   Список смежности -Список смежности (adjacency list) использует $n$ связанных списков для представления графа, где узлы списка обозначают вершины. $i$-й список соответствует вершине $i$ и хранит все вершины, смежные с ней, то есть все вершины, соединенные с этой вершиной. На рисунке 9-6 показан пример графа, представленного списком смежности. +Список смежности (adjacency list) использует $n$ списков для представления графа, где узлы списка обозначают вершины. $i$-й список соответствует вершине $i$ и хранит все смежные с ней вершины, то есть все вершины, соединенные с данной вершиной. На рисунке 9-6 показан пример графа, представленного списком смежности. ![Представление графа списком смежности](graph.assets/adjacency_list.png){ class="animation-figure" }

Рисунок 9-6   Представление графа списком смежности

-Список смежности хранит только реально существующие ребра, а общее число ребер обычно значительно меньше $n^2$ , поэтому этот способ существенно экономит пространство. Однако для поиска ребра в списке смежности нужно проходить по списку, поэтому по времени он уступает матрице смежности. +Список смежности хранит только реально существующие ребра, а общее число ребер обычно значительно меньше $n^2$ , поэтому он лучше экономит память. Однако для поиска ребра в списке смежности требуется обходить список, поэтому по времени он уступает матрице смежности. -Если посмотреть на рисунок 9-6, можно заметить, что **структура списка смежности очень похожа на "метод цепочек" в хеш-таблице, поэтому для оптимизации эффективности здесь можно использовать сходные идеи**. Например, когда список становится слишком длинным, его можно преобразовать в AVL-дерево или красно-черное дерево, чтобы улучшить временную сложность с $O(n)$ до $O(\log n)$ ; можно также превратить его в хеш-таблицу и снизить сложность до $O(1)$ . +Если посмотреть на рисунок 9-6, можно заметить, что **структура списка смежности очень похожа на цепную адресацию в хеш-таблицах, поэтому здесь можно использовать похожие методы оптимизации эффективности**. Например, если список слишком длинный, его можно преобразовать в AVL-дерево или красно-черное дерево, чтобы снизить временную сложность с $O(n)$ до $O(\log n)$ ; также список можно преобразовать в хеш-таблицу, чтобы довести временную сложность до $O(1)$ . ## 9.1.3   Типичные применения графов -Как показано в таблице 9-1, многие реальные системы можно моделировать графами, а соответствующие задачи затем сводить к задачам вычислений на графах. +Как показано в таблице 9-1, многие реальные системы можно моделировать с помощью графов, а соответствующие задачи затем сводить к задачам вычислений на графах.

Таблица 9-1   Распространенные графы в реальной жизни

@@ -97,7 +97,7 @@ $$ | | Вершина | Ребро | Задача вычислений на графе | | -------- | ------- | -------------------- | -------------------------- | | Социальные сети | Пользователь | Дружеская связь | Рекомендация потенциальных друзей | -| Линии метро | Станция | Связность между станциями | Рекомендация кратчайшего маршрута | +| Линии метро | Станция | Связь между станциями | Рекомендация кратчайшего маршрута | | Солнечная система | Небесное тело | Гравитационное взаимодействие между телами | Вычисление орбит планет | diff --git a/ru/docs/chapter_graph/graph_operations.md b/ru/docs/chapter_graph/graph_operations.md index ffaadaaf7..08855f800 100644 --- a/ru/docs/chapter_graph/graph_operations.md +++ b/ru/docs/chapter_graph/graph_operations.md @@ -4,30 +4,30 @@ comments: true # 9.2   Базовые операции графа -Базовые операции графа можно разделить на операции над "ребрами" и операции над "вершинами". В двух способах представления - "матрица смежности" и "список смежности" - реализация будет различаться. +Базовые операции графа можно разделить на операции над "ребрами" и операции над "вершинами". При двух способах представления, "матрице смежности" и "списке смежности", реализация этих операций различается. ## 9.2.1   Реализация на основе матрицы смежности Пусть дан неориентированный граф с числом вершин $n$ . Тогда способы реализации различных операций показаны на рисунках ниже. -- **Добавление или удаление ребра**: достаточно изменить соответствующее ребро в матрице смежности, это требует $O(1)$ времени. Поскольку граф неориентированный, нужно одновременно обновлять ребра в обоих направлениях. -- **Добавление вершины**: в конец матрицы смежности добавляется одна строка и один столбец, которые полностью заполняются нулями; это требует $O(n)$ времени. -- **Удаление вершины**: из матрицы смежности удаляется одна строка и один столбец. В худшем случае, когда удаляются первая строка и первый столбец, приходится "сдвигать вверх-влево" $(n-1)^2$ элементов, поэтому требуется $O(n^2)$ времени. +- **Добавление или удаление ребра**: достаточно изменить соответствующее ребро в матрице смежности, что требует $O(1)$ времени. Поскольку граф неориентированный, необходимо одновременно обновить ребра в обоих направлениях. +- **Добавление вершины**: в конец матрицы смежности добавляется строка и столбец, полностью заполненные нулями; это требует $O(n)$ времени. +- **Удаление вершины**: из матрицы смежности удаляется строка и столбец. При удалении первой строки и первого столбца достигается худший случай, когда требуется "сдвинуть влево вверх" $(n-1)^2$ элементов, поэтому используется $O(n^2)$ времени. - **Инициализация**: передаются $n$ вершин, затем инициализируется список вершин `vertices` длины $n$ , что требует $O(n)$ времени; после этого инициализируется матрица смежности `adjMat` размера $n \times n$ , что требует $O(n^2)$ времени. -=== "Инициализация матрицы смежности" +=== "<1>" ![Инициализация матрицы смежности, добавление и удаление ребер и вершин](graph_operations.assets/adjacency_matrix_step1_initialization.png){ class="animation-figure" } -=== "Добавление ребра" +=== "<2>" ![adjacency_matrix_add_edge](graph_operations.assets/adjacency_matrix_step2_add_edge.png){ class="animation-figure" } -=== "Удаление ребра" +=== "<3>" ![adjacency_matrix_remove_edge](graph_operations.assets/adjacency_matrix_step3_remove_edge.png){ class="animation-figure" } -=== "Добавление вершины" +=== "<4>" ![adjacency_matrix_add_vertex](graph_operations.assets/adjacency_matrix_step4_add_vertex.png){ class="animation-figure" } -=== "Удаление вершины" +=== "<5>" ![adjacency_matrix_remove_vertex](graph_operations.assets/adjacency_matrix_step5_remove_vertex.png){ class="animation-figure" }

Рисунок 9-7   Инициализация матрицы смежности, добавление и удаление ребер и вершин

@@ -1215,35 +1215,35 @@ comments: true Пусть неориентированный граф содержит в сумме $n$ вершин и $m$ ребер. Тогда различные операции можно реализовать способом, показанным на рисунках ниже. -- **Добавление ребра**: достаточно добавить ребро в конец списка, соответствующего вершине; это требует $O(1)$ времени. Поскольку граф неориентированный, нужно одновременно добавлять ребра в обоих направлениях. -- **Удаление ребра**: нужно найти и удалить указанное ребро в списке, соответствующем вершине; это требует $O(m)$ времени. В неориентированном графе нужно удалять ребра в обоих направлениях. +- **Добавление ребра**: достаточно добавить ребро в конец списка, соответствующего вершине; это требует $O(1)$ времени. Поскольку граф неориентированный, необходимо одновременно добавить ребра в обоих направлениях. +- **Удаление ребра**: нужно найти и удалить указанное ребро в списке, соответствующем вершине; это требует $O(m)$ времени. В неориентированном графе необходимо удалить ребра в обоих направлениях. - **Добавление вершины**: в список смежности добавляется еще один список, а новая вершина становится его головным узлом; это требует $O(1)$ времени. - **Удаление вершины**: требуется пройти по всему списку смежности и удалить все ребра, содержащие указанную вершину; это требует $O(n + m)$ времени. - **Инициализация**: в списке смежности создаются $n$ вершин и $2m$ ребер; это требует $O(n + m)$ времени. -=== "Инициализация списка смежности" +=== "<1>" ![Инициализация списка смежности, добавление и удаление ребер и вершин](graph_operations.assets/adjacency_list_step1_initialization.png){ class="animation-figure" } -=== "Добавление ребра" +=== "<2>" ![adjacency_list_add_edge](graph_operations.assets/adjacency_list_step2_add_edge.png){ class="animation-figure" } -=== "Удаление ребра" +=== "<3>" ![adjacency_list_remove_edge](graph_operations.assets/adjacency_list_step3_remove_edge.png){ class="animation-figure" } -=== "Добавление вершины" +=== "<4>" ![adjacency_list_add_vertex](graph_operations.assets/adjacency_list_step4_add_vertex.png){ class="animation-figure" } -=== "Удаление вершины" +=== "<5>" ![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_step5_remove_vertex.png){ class="animation-figure" }

Рисунок 9-8   Инициализация списка смежности, добавление и удаление ребер и вершин

Ниже приведен код списка смежности. По сравнению с рисунками выше, реальная реализация имеет следующие отличия. -- Чтобы упростить добавление и удаление вершин, а также упростить код, мы используем список, то есть динамический массив, вместо связного списка. +- Чтобы упростить добавление и удаление вершин, а также сделать код проще, мы используем список, то есть динамический массив, вместо связного списка. - Для хранения списка смежности используется хеш-таблица, где `key` - это экземпляр вершины, а `value` - список смежных вершин данной вершины. -Кроме того, в списке смежности мы используем класс `Vertex` для представления вершины. Причина в следующем: если, как и в матрице смежности, различать вершины по индексам списка, то при удалении вершины с индексом $i$ пришлось бы обходить весь список смежности и уменьшать на $1$ все индексы, большие $i$ , что крайне неэффективно. Если же каждая вершина является уникальным экземпляром `Vertex` , то после удаления одной вершины остальные вершины менять уже не требуется. +Кроме того, в списке смежности используется класс `Vertex` для представления вершины. Причина в том, что если, как и в матрице смежности, различать вершины по индексам списка, то при удалении вершины с индексом $i$ пришлось бы обходить весь список смежности и уменьшать на $1$ все индексы, большие $i$ , что крайне неэффективно. Если же каждая вершина является уникальным экземпляром `Vertex` , то после удаления одной вершины остальные вершины менять уже не требуется. === "Python" @@ -2371,7 +2371,7 @@ comments: true ## 9.2.3   Сравнение эффективности -Пусть в графе имеется $n$ вершин и $m$ ребер. В таблице 9-2 сравниваются временная и пространственная эффективность матрицы смежности и списка смежности. Обрати внимание: список смежности (связный список) соответствует реализации из этой статьи, а список смежности (хеш-таблица) означает вариант, где все списки заменены хеш-таблицами. +Пусть в графе имеется $n$ вершин и $m$ ребер. В таблице 9-2 сравниваются временная и пространственная эффективность матрицы смежности и списка смежности. Обратите внимание: список смежности (связный список) соответствует реализации из этой статьи, а список смежности (хеш-таблица) означает вариант, в котором все списки заменены хеш-таблицами.

Таблица 9-2   Сравнение матрицы смежности и списка смежности

@@ -2388,4 +2388,4 @@ comments: true -Если смотреть только на таблицу, может показаться, что список смежности на основе хеш-таблицы является лучшим и по времени, и по памяти. Но на практике операции над ребрами в матрице смежности часто выполняются быстрее, потому что там нужен лишь один доступ к массиву или одно присваивание. В целом матрица смежности воплощает принцип "обмен пространства на время", а список смежности - принцип "обмен времени на пространство". +Если смотреть только на таблицу, может показаться, что список смежности на основе хеш-таблицы является лучшим и по времени, и по памяти. Но на практике операции над ребрами в матрице смежности обычно выполняются быстрее, потому что там нужен лишь один доступ к массиву или одно присваивание. В целом матрица смежности воплощает принцип "обмена пространства на время", а список смежности - принцип "обмена времени на пространство". diff --git a/ru/docs/chapter_graph/graph_traversal.md b/ru/docs/chapter_graph/graph_traversal.md index d28a36c8b..ca4d12209 100644 --- a/ru/docs/chapter_graph/graph_traversal.md +++ b/ru/docs/chapter_graph/graph_traversal.md @@ -4,13 +4,13 @@ comments: true # 9.3   Обход графа -Дерево представляет отношение "один ко многим", а граф имеет более высокую степень свободы и может выражать произвольные отношения "многие ко многим". Поэтому мы можем рассматривать дерево как частный случай графа. Очевидно, что **операции обхода дерева также являются частным случаем операций обхода графа**. +Дерево представляет отношение "один ко многим", тогда как граф обладает большей свободой и может выражать произвольные отношения "многие ко многим". Поэтому дерево можно рассматривать как частный случай графа. Очевидно, что **операции обхода дерева также являются частным случаем операций обхода графа**. -И графы, и деревья требуют использования поисковых алгоритмов для реализации обхода. Способы обхода графа также делятся на два типа: обход в ширину и обход в глубину. +И графы, и деревья требуют применения алгоритмов обхода. Способы обхода графа также делятся на два типа: обход в ширину и обход в глубину. ## 9.3.1   Обход в ширину -**Обход в ширину - это способ обхода "от близкого к далекому": начиная с некоторого узла, мы всегда в первую очередь посещаем ближайшие вершины и слой за слоем расширяемся наружу**. Как показано на рисунке 9-9, начиная с вершины в левом верхнем углу, мы сначала обходим все смежные вершины этой вершины, затем все смежные вершины следующей вершины и так далее, пока не будут посещены все вершины. +**Обход в ширину - это способ обхода от ближнего к дальнему, при котором начиная с некоторого узла сначала посещают ближайшие вершины, а затем слой за слоем расширяются наружу**. Как показано на рисунке 9-9, начиная с вершины в левом верхнем углу, мы сначала обходим все смежные вершины этой вершины, затем все смежные вершины следующей вершины и так далее, пока не будут посещены все вершины. ![Обход графа в ширину](graph_traversal.assets/graph_bfs.png){ class="animation-figure" } @@ -18,17 +18,17 @@ comments: true ### 1.   Реализация алгоритма -BFS обычно реализуется с помощью очереди, код приведен ниже. Очередь обладает свойством "первым пришел - первым вышел", что хорошо соответствует идее BFS "от близкого к далекому". +BFS обычно реализуется с помощью очереди, код приведен ниже. Очередь обладает свойством "первым пришел - первым вышел", что хорошо соответствует идее BFS "от ближнего к дальнему". 1. Поместить стартовую вершину обхода `startVet` в очередь и запустить цикл. -2. На каждой итерации цикла извлекать вершину из головы очереди и записывать факт ее посещения, после чего добавлять все смежные вершины этой вершины в хвост очереди. +2. На каждой итерации цикла извлекать вершину из головы очереди и записывать ее посещение, после чего добавлять все смежные вершины этой вершины в хвост очереди. 3. Повторять шаг `2.` до тех пор, пока не будут посещены все вершины. -Чтобы предотвратить повторный обход вершин, нам нужен хеш-набор `visited` , в котором будет записываться, какие узлы уже посещены. +Чтобы предотвратить повторный обход вершин, нам нужно хеш-множество `visited` , в котором записывается, какие вершины уже посещены. !!! tip - Хеш-набор можно рассматривать как хеш-таблицу, которая хранит только `key` и не хранит `value` . Он позволяет выполнять добавление, удаление, поиск и изменение `key` за $O(1)$ времени. Благодаря уникальности `key` хеш-набор обычно используется, например, для устранения повторов. + Хеш-множество можно рассматривать как хеш-таблицу, которая хранит только `key` и не хранит `value` . Оно позволяет выполнять добавление, удаление и проверку наличия `key` за $O(1)$ времени. Благодаря уникальности `key` хеш-множество обычно используется, например, для устранения повторов. === "Python" @@ -519,17 +519,17 @@ BFS обычно реализуется с помощью очереди, код !!! question "Является ли последовательность обхода в ширину единственной?" - Нет. Обход в ширину требует только соблюдения порядка "от близкого к далекому", **а порядок обхода нескольких вершин на одинаковом расстоянии может произвольно меняться**. Например, на рисунке 9-10 можно поменять местами порядок посещения вершин $1$ и $3$ , а также в произвольном порядке переставить вершины $2$, $4$, $6$ . + Нет. Обход в ширину требует только соблюдения порядка "от ближнего к дальнему", **а порядок обхода нескольких вершин на одинаковом расстоянии может произвольно меняться**. Например, на рисунке 9-10 можно поменять местами порядок посещения вершин $1$ и $3$ , а вершины $2$, $4$, $6$ также можно переставлять произвольно. ### 2.   Анализ сложности **Временная сложность**: все вершины по одному разу помещаются в очередь и извлекаются из нее, что требует $O(|V|)$ времени; при обходе смежных вершин, поскольку граф неориентированный, все ребра будут посещены по $2$ раза, что требует $O(2|E|)$ времени; в сумме получается $O(|V| + |E|)$ . -**Пространственная сложность**: список `res` , хеш-набор `visited` и очередь `que` в худшем случае могут содержать до $|V|$ вершин, поэтому требуется $O(|V|)$ памяти. +**Пространственная сложность**: список `res` , хеш-множество `visited` и очередь `que` в худшем случае могут содержать до $|V|$ вершин, поэтому требуется $O(|V|)$ памяти. ## 9.3.2   Обход в глубину -**Обход в глубину - это способ обхода, при котором сначала идут до самого конца, а когда дальше идти нельзя, откатываются назад**. Как показано на рисунке 9-11, начиная с вершины в левом верхнем углу, мы выбираем некоторую смежную вершину текущей вершины, идем до упора, затем возвращаемся назад, снова идем до упора и так далее, пока не будут посещены все вершины. +**Обход в глубину - это способ обхода, при котором сначала идут до самого конца, а когда дальше идти нельзя, возвращаются назад**. Как показано на рисунке 9-11, начиная с вершины в левом верхнем углу, мы выбираем некоторую смежную вершину текущей вершины, идем до упора, затем возвращаемся назад, снова идем до упора и так далее, пока не будут посещены все вершины. ![Обход графа в глубину](graph_traversal.assets/graph_dfs.png){ class="animation-figure" } @@ -537,7 +537,7 @@ BFS обычно реализуется с помощью очереди, код ### 1.   Реализация алгоритма -Такой алгоритмический шаблон "дойти до конца и вернуться" обычно реализуется через рекурсию. Подобно обходу в ширину, в обходе в глубину мы также используем хеш-набор `visited` для записи уже посещенных вершин и тем самым избегаем повторного посещения. +Такой алгоритмический шаблон "дойти до конца и вернуться" обычно реализуется через рекурсию. Подобно обходу в ширину, в обходе в глубину мы также используем хеш-множество `visited` для записи уже посещенных вершин и тем самым избегаем повторного посещения. === "Python" @@ -942,8 +942,8 @@ BFS обычно реализуется с помощью очереди, код Алгоритмический процесс обхода в глубину показан на рисунках ниже. -- **Прямая пунктирная линия обозначает нисходящее рекурсивное развертывание** , то есть запуск нового рекурсивного метода для посещения новой вершины. -- **Изогнутая пунктирная линия обозначает обратный возврат по рекурсии** , то есть данный рекурсивный метод завершился и управление вернулось туда, откуда он был вызван. +- **Прямая пунктирная линия обозначает нисходящую рекурсию** , то есть запуск нового рекурсивного метода для посещения новой вершины. +- **Изогнутая пунктирная линия обозначает восходящую рекурсию** , то есть данный рекурсивный метод завершился и управление вернулось туда, откуда он был вызван. Чтобы лучше понять алгоритм, рекомендуется совместить рисунки ниже с кодом и мысленно проследить весь процесс DFS, включая моменты запуска и возврата каждого рекурсивного вызова. @@ -992,4 +992,4 @@ BFS обычно реализуется с помощью очереди, код **Временная сложность**: все вершины будут посещены по $1$ разу, что требует $O(|V|)$ времени; все ребра будут посещены по $2$ раза, что требует $O(2|E|)$ времени; суммарно получается $O(|V| + |E|)$ . -**Пространственная сложность**: число вершин в списке `res` и хеш-наборе `visited` в худшем случае достигает $|V|$ , максимальная глубина рекурсии тоже равна $|V|$ , поэтому требуется $O(|V|)$ памяти. +**Пространственная сложность**: число вершин в списке `res` и хеш-множестве `visited` в худшем случае достигает $|V|$ , максимальная глубина рекурсии тоже равна $|V|$ , поэтому требуется $O(|V|)$ памяти. diff --git a/ru/docs/chapter_graph/index.md b/ru/docs/chapter_graph/index.md index 4358b22e7..012b2a03b 100644 --- a/ru/docs/chapter_graph/index.md +++ b/ru/docs/chapter_graph/index.md @@ -9,13 +9,13 @@ icon: material/graphql !!! abstract - На жизненном пути мы подобны узлам, соединенным бесчисленными невидимыми ребрами. + В жизни мы похожи на вершины, соединенные множеством невидимых ребер. - Каждая встреча и каждое расставание оставляют в этой огромной сети свой особый след. + Каждая встреча и каждое расставание оставляют в этой огромной сети свой след. ## Содержание главы - [9.1   Граф](graph.md) -- [9.2   Базовые операции над графами](graph_operations.md) +- [9.2   Базовые операции графа](graph_operations.md) - [9.3   Обход графа](graph_traversal.md) -- [9.4   Резюме](summary.md) +- [9.4   Краткие итоги](summary.md) diff --git a/ru/docs/chapter_graph/summary.md b/ru/docs/chapter_graph/summary.md index 6838b73f9..fd338b926 100644 --- a/ru/docs/chapter_graph/summary.md +++ b/ru/docs/chapter_graph/summary.md @@ -8,11 +8,11 @@ comments: true - Граф состоит из вершин и ребер и может быть задан как множество вершин и множество ребер. - По сравнению с линейными отношениями (связный список) и отношениями разделения (дерево), сетевые отношения (граф) обладают большей свободой и потому более сложны. -- Ребра ориентированного графа имеют направление, в связном графе любые вершины достижимы друг из друга, а в взвешенном графе каждое ребро несет переменную веса. -- Матрица смежности использует матрицу для представления графа: каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают, есть между двумя вершинами ребро или нет. Матрица смежности очень эффективна для операций добавления, удаления, поиска и изменения, но расходует больше памяти. +- Ребра ориентированного графа имеют направление, в связном графе любые вершины достижимы, а во взвешенном графе каждое ребро содержит переменную веса. +- Матрица смежности использует матрицу для представления графа: каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают, есть между двумя вершинами ребро или нет. Матрица смежности эффективна в операциях добавления, удаления, поиска и изменения, но расходует больше памяти. - Список смежности использует несколько списков для представления графа; $i$-й список соответствует вершине $i$ и хранит все ее смежные вершины. По сравнению с матрицей смежности список смежности экономит пространство, но для поиска ребра в нем приходится обходить список, поэтому по времени он уступает. -- Когда списки в списке смежности становятся слишком длинными, их можно преобразовать в красно-черное дерево или хеш-таблицу, чтобы ускорить поиск. -- С точки зрения алгоритмической идеи матрица смежности отражает принцип "обмен пространства на время", а список смежности - принцип "обмена времени на пространство". +- Когда списки в списке смежности становятся слишком длинными, их можно преобразовать в красно-черное дерево или хеш-таблицу, чтобы повысить эффективность поиска. +- С точки зрения алгоритмической идеи матрица смежности отражает принцип "обмена пространства на время", а список смежности - принцип "обмена времени на пространство". - Графы можно использовать для моделирования различных реальных систем, таких как социальные сети, линии метро и так далее. - Дерево является частным случаем графа, а обход дерева - частным случаем обхода графа. - Обход графа в ширину представляет собой способ поиска, который расширяется от ближнего к дальнему и обычно реализуется с помощью очереди. @@ -22,7 +22,7 @@ comments: true **Q**: Что считается путем: последовательность вершин или последовательность ребер? -Определение в разных языковых версиях Википедии различается: в английской версии путь определяется как "последовательность ребер", а в китайской версии - как "последовательность вершин". В английской версии исходная формулировка выглядит так: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices. +Определение в разных языковых версиях Википедии различается: в английской версии путь определяется как "последовательность ребер", а в русской версии - как "последовательность вершин". В английской версии исходная формулировка выглядит так: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices. В этой книге путь рассматривается как последовательность ребер, а не как последовательность вершин. Причина в том, что между двумя вершинами может существовать несколько ребер, и в таком случае каждому ребру соответствует свой путь. diff --git a/ru/docs/chapter_greedy/fractional_knapsack_problem.md b/ru/docs/chapter_greedy/fractional_knapsack_problem.md index 525633d8c..9e56e8222 100644 --- a/ru/docs/chapter_greedy/fractional_knapsack_problem.md +++ b/ru/docs/chapter_greedy/fractional_knapsack_problem.md @@ -536,9 +536,9 @@ comments: true
-Встроенный алгоритм сортировки обычно имеет временную сложность $O(\log n)$, а пространственная сложность обычно равна $O(\log n)$ или $O(n)$, в зависимости от конкретной реализации в языке программирования. +Встроенный алгоритм сортировки обычно имеет временную сложность $O(n \log n)$, а пространственная сложность обычно равна $O(\log n)$ или $O(n)$, в зависимости от конкретной реализации в языке программирования. -Помимо сортировки, в худшем случае потребуется пройти весь список предметов, **поэтому временная сложность равна $O(n)$**, где $n$ - число предметов. +Помимо сортировки, в худшем случае потребуется пройти весь список предметов, но это не меняет асимптотику, **поэтому итоговая временная сложность равна $O(n \log n)$**, где $n$ - число предметов. Поскольку инициализируется список объектов `Item`, **пространственная сложность равна $O(n)$**. diff --git a/ru/docs/chapter_greedy/greedy_algorithm.md b/ru/docs/chapter_greedy/greedy_algorithm.md index 1e578d50e..6b9ca7c6e 100644 --- a/ru/docs/chapter_greedy/greedy_algorithm.md +++ b/ru/docs/chapter_greedy/greedy_algorithm.md @@ -4,14 +4,14 @@ comments: true # 15.1   Жадный алгоритм -Жадный алгоритм (greedy algorithm) - это распространенный подход к решению задач оптимизации. Его основная идея состоит в том, чтобы на каждом этапе принятия решения выбирать вариант, который выглядит наилучшим прямо сейчас, то есть жадно принимать локально оптимальные решения в надежде получить глобально оптимальный результат. Жадные алгоритмы лаконичны и эффективны, поэтому широко применяются во многих практических задачах. +Жадный алгоритм (greedy algorithm) - это распространенный метод решения задач оптимизации. Его основная идея состоит в том, чтобы на каждом этапе принятия решения выбирать вариант, который выглядит наилучшим прямо сейчас, то есть жадно принимать локально оптимальные решения в надежде получить глобально оптимальный результат. Жадные алгоритмы просты и эффективны, поэтому широко применяются во многих практических задачах. -Жадные алгоритмы и динамическое программирование часто используются для решения задач оптимизации. У них есть некоторое сходство, например оба опираются на свойство оптимальной подструктуры, но принципы работы различаются. +Жадные алгоритмы и динамическое программирование часто используются для решения задач оптимизации. У них есть некоторое сходство, например оба метода опираются на свойство оптимальной подструктуры, но принципы работы различаются. -- Динамическое программирование учитывает все решения предыдущих этапов при выборе текущего решения и использует ответы для прошлых подзадач, чтобы построить ответ для текущей подзадачи. -- Жадный алгоритм не учитывает прошлые решения, а просто движется вперед, каждый раз делая жадный выбор, постепенно сужая область задачи, пока она не будет решена. +- Динамическое программирование при получении текущего решения учитывает все предыдущие решения и использует ответы для прошлых подзадач, чтобы построить ответ для текущей подзадачи. +- Жадный алгоритм не учитывает предыдущие решения, а просто движется вперед, каждый раз делая жадный выбор и постепенно сужая область задачи, пока она не будет решена. -Сначала разберем принцип работы жадного алгоритма на примере задачи «размен монет». Эта задача уже встречалась в разделе «задача о полном рюкзаке», поэтому она наверняка вам знакома. +Чтобы лучше понять принцип работы жадного алгоритма, разберем его на примере задачи «размен монет». Эта задача уже встречалась в разделе «задача о полном рюкзаке», поэтому она наверняка вам знакома. !!! question @@ -23,7 +23,7 @@ comments: true

Рисунок 15-1   Жадная стратегия для задачи о размене монет

-Код реализации выглядит следующим образом: +Ниже приведен код реализации. === "Python" @@ -335,11 +335,11 @@ comments: true
-У вас может невольно вырваться: So clean! Жадный алгоритм решает задачу размена монет всего примерно десятью строками кода. +У вас может невольно вырваться: «Эврика!» Жадный алгоритм решает задачу размена монет всего примерно десятью строками кода. ## 15.1.1   Преимущества и ограничения жадного алгоритма -**Жадный алгоритм не только прост в действиях и реализации, но и обычно очень эффективен**. В приведенном выше коде обозначим минимальный номинал монеты через $\min(coins)$, тогда жадный выбор выполняется не более чем $amt / \min(coins)$ раз, а временная сложность равна $O(amt / \min(coins))$. Это на порядок меньше, чем временная сложность решения через динамическое программирование $O(n \times amt)$. +**Жадный алгоритм не только прост в реализации, но и обычно очень эффективен**. В приведенном выше коде обозначим минимальный номинал монеты через $\min(coins)$, тогда жадный выбор выполняется не более чем $amt / \min(coins)$ раз, а временная сложность равна $O(amt / \min(coins))$. Это на порядок меньше, чем временная сложность решения через динамическое программирование $O(n \times amt)$. Однако **для некоторых наборов номиналов монет жадный алгоритм не может найти оптимальный ответ**. Ниже показаны два примера. @@ -362,7 +362,7 @@ comments: true Тогда возникает вопрос: какие задачи подходят для решения жадным алгоритмом? Или, другими словами, в каких случаях жадный алгоритм может гарантировать оптимальный ответ? -По сравнению с динамическим программированием условия применения жадного алгоритма строже. В основном нас интересуют два свойства задачи. +По сравнению с динамическим программированием условия применения жадного алгоритма более строгие. В основном нас интересуют два свойства задачи. - **Свойство жадного выбора**: только когда локально оптимальный выбор всегда может привести к глобально оптимальному решению, жадный алгоритм способен гарантировать оптимум. - **Оптимальная подструктура**: оптимальное решение исходной задачи содержит оптимальные решения подзадач. @@ -381,7 +381,7 @@ comments: true ## 15.1.3   Этапы решения задач жадным алгоритмом -В общем виде процесс решения жадной задачи можно разбить на три шага. +Процесс решения жадной задачи в общем виде можно разбить на три шага. 1. **Анализ задачи**: разобраться в свойствах задачи, включая определение состояний, целевой функции и ограничений. Этот этап присутствует и в поиске с возвратом, и в динамическом программировании. 2. **Определение жадной стратегии**: определить, какой жадный выбор следует делать на каждом шаге. Эта стратегия должна уменьшать размер задачи на каждом этапе и в итоге привести к решению всей задачи. @@ -398,7 +398,7 @@ comments: true ## 15.1.4   Типичные задачи для жадного алгоритма -Жадные алгоритмы часто применяются в задачах оптимизации, которые обладают свойством жадного выбора и оптимальной подструктурой. Ниже приведены некоторые типичные задачи, решаемые жадным подходом. +Жадные алгоритмы часто применяются в задачах оптимизации, обладающих свойством жадного выбора и оптимальной подструктурой. Ниже приведены некоторые типичные задачи, решаемые жадным подходом. - **Задача о размене монет**: при некоторых системах монет жадный алгоритм всегда дает оптимальный ответ. - **Задача о расписании интервалов**: пусть есть несколько задач, каждая выполняется в некотором временном интервале, и требуется завершить как можно больше задач. Если каждый раз выбирать задачу с самым ранним временем окончания, то жадный алгоритм дает оптимальный ответ. diff --git a/ru/docs/chapter_greedy/max_capacity_problem.md b/ru/docs/chapter_greedy/max_capacity_problem.md index db27f34e5..a309b8f55 100644 --- a/ru/docs/chapter_greedy/max_capacity_problem.md +++ b/ru/docs/chapter_greedy/max_capacity_problem.md @@ -442,4 +442,4 @@ $$ Нетрудно заметить, что **эти пропущенные состояния на самом деле и есть все состояния, в которых длинная перегородка $j$ сдвигается внутрь**. Ранее мы уже доказали, что перемещение длинной перегородки внутрь обязательно уменьшает вместимость. Иными словами, пропущенные состояния не могут быть оптимальным решением, **поэтому их пропуск не приводит к потере оптимума**. -Приведенный анализ показывает, что операция перемещения короткой перегородки является «безопасной», а жадная стратегия действительно эффективна. +Приведенный анализ показывает, что операция перемещения короткой перегородки является «безопасной», а жадная стратегия действительно корректна. diff --git a/ru/docs/chapter_greedy/max_product_cutting_problem.md b/ru/docs/chapter_greedy/max_product_cutting_problem.md index 5c98ad8b8..45ecb1d3b 100644 --- a/ru/docs/chapter_greedy/max_product_cutting_problem.md +++ b/ru/docs/chapter_greedy/max_product_cutting_problem.md @@ -65,7 +65,7 @@ $$ ### 2.   Код реализации -Как показано на рисунке 15-16, нам не нужен цикл, чтобы выполнять разбиение числа. Можно использовать целочисленное деление вниз, чтобы получить число троек $a$, и операцию взятия остатка, чтобы получить остаток $b$. Тогда имеем: +Как показано на рисунке 15-16, нам не нужен цикл, чтобы выполнять разбиение числа. Можно использовать целочисленное деление, чтобы получить число троек $a$, и операцию взятия остатка, чтобы получить остаток $b$. Тогда имеем: $$ n = 3 a + b diff --git a/ru/docs/chapter_hashing/hash_algorithm.md b/ru/docs/chapter_hashing/hash_algorithm.md index 43d3d5f15..30f8a69f9 100644 --- a/ru/docs/chapter_hashing/hash_algorithm.md +++ b/ru/docs/chapter_hashing/hash_algorithm.md @@ -24,7 +24,7 @@ index = hash(key) % capacity ## 6.3.1   Цели хеш-алгоритма -Чтобы получить структуру данных хеш-таблицы, которая будет одновременно "быстрой и надежной", хеш-алгоритм должен обладать следующими свойствами. +Чтобы получить структуру данных хеш-таблицы, которая будет одновременно быстрой и надежной, хеш-алгоритм должен обладать следующими свойствами. - **Детерминированность**: для одинакового входа хеш-алгоритм всегда должен выдавать одинаковый результат. Только так хеш-таблица остается надежной. - **Высокая эффективность**: вычисление хеш-значения должно быть достаточно быстрым. Чем меньше вычислительные затраты, тем выше практическая ценность хеш-таблицы. @@ -677,7 +677,7 @@ $$ ## 6.3.3   Распространенные хеш-алгоритмы -Нетрудно заметить, что описанные выше простые хеш-алгоритмы довольно "хрупкие" и далеки от поставленных целей. Например, сложение и XOR подчиняются коммутативному закону, поэтому аддитивный хеш и XOR-хеш не различают строки, состоящие из одних и тех же символов, но в разном порядке. Это может усиливать хеш-коллизии и даже создавать некоторые проблемы безопасности. +Нетрудно заметить, что описанные выше простые хеш-алгоритмы довольно хрупкие и далеки от поставленных целей. Например, сложение и XOR подчиняются коммутативному закону, поэтому аддитивный хеш и XOR-хеш не различают строки, состоящие из одних и тех же символов, но в разном порядке. Это может усиливать хеш-коллизии и даже создавать некоторые проблемы безопасности. На практике мы обычно используем стандартные хеш-алгоритмы, такие как MD5, SHA-1, SHA-2 и SHA-3. Они могут отображать входные данные произвольной длины в хеш-значения фиксированной длины. diff --git a/ru/docs/chapter_hashing/hash_collision.md b/ru/docs/chapter_hashing/hash_collision.md index 3dd11d349..15c58e667 100644 --- a/ru/docs/chapter_hashing/hash_collision.md +++ b/ru/docs/chapter_hashing/hash_collision.md @@ -6,12 +6,12 @@ comments: true Как уже говорилось в предыдущем разделе, **в обычных условиях входное пространство хеш-функции намного больше выходного пространства** , поэтому теоретически хеш-коллизии неизбежны. Например, если входное пространство состоит из всех целых чисел, а выходное пространство ограничено размером массива, то неизбежно несколько целых чисел будут отображаться в один и тот же индекс бакета. -Хеш-коллизии приводят к ошибочным результатам поиска и серьезно влияют на пригодность хеш-таблицы к использованию. Чтобы решить эту проблему, можно при каждом конфликте выполнять расширение хеш-таблицы, пока конфликт не исчезнет. Этот метод прост и груб, но слишком неэффективен, потому что расширение хеш-таблицы требует большого объема переноса данных и вычислений хеш-значений. Чтобы повысить эффективность, можно использовать следующие стратегии. +Хеш-коллизии могут приводить к ошибочным результатам поиска и серьезно влиять на работоспособность хеш-таблицы. Чтобы решить эту проблему, можно при каждом конфликте выполнять расширение хеш-таблицы, пока конфликт не исчезнет. Этот метод понятен и прост, но слишком неэффективен, потому что расширение хеш-таблицы требует большого объема переноса данных и вычислений хеш-значений. Чтобы повысить эффективность, можно использовать следующие стратегии. 1. Улучшить структуру данных хеш-таблицы, **чтобы она могла корректно работать даже при возникновении хеш-коллизий**. 2. Выполнять расширение только тогда, когда это действительно необходимо, то есть когда хеш-коллизии становятся достаточно серьезными. -Основные способы улучшения структуры хеш-таблицы включают "метод цепочек" и "открытую адресацию". +Основные способы улучшения структуры хеш-таблицы включают метод цепочек и открытую адресацию. ## 6.2.1   Метод цепочек @@ -1522,11 +1522,11 @@ comments: true
-Следует отметить, что когда связный список становится очень длинным, эффективность поиска $O(n)$ оказывается низкой. **В этом случае список можно преобразовать в "AVL-дерево" или "красно-черное дерево"** , чтобы оптимизировать временную сложность поиска до $O(\log n)$ . +Следует отметить, что когда связный список становится очень длинным, эффективность поиска $O(n)$ оказывается низкой. **В этом случае список можно преобразовать в AVL-дерево или красно-черное дерево** , чтобы оптимизировать временную сложность поиска до $O(\log n)$ . ## 6.2.2   Открытая адресация -Открытая адресация (open addressing) не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью "многократного пробирования"; основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование. +Открытая адресация (open addressing) не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью многократного пробирования; основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование. Ниже на примере линейного пробирования рассмотрим механизм работы хеш-таблицы с открытой адресацией. @@ -1543,7 +1543,7 @@ comments: true

Рисунок 6-6   Распределение пар ключ-значение в хеш-таблице с открытой адресацией (линейное пробирование)

-Однако **линейное пробирование легко приводит к "кластеризации"**. Иначе говоря, чем длиннее непрерывная занятая область в массиве, тем выше вероятность новых коллизий в этой области, что еще сильнее способствует росту этой группы и в итоге ухудшает эффективность операций добавления, удаления, поиска и обновления. +Однако **линейное пробирование легко приводит к кластеризации**. Иначе говоря, чем длиннее непрерывная занятая область в массиве, тем выше вероятность новых коллизий в этой области, что еще сильнее способствует росту этой группы и в итоге ухудшает эффективность операций добавления, удаления, поиска и обновления. Стоит заметить, что **мы не можем напрямую удалять элементы из хеш-таблицы с открытой адресацией**. Причина в том, что удаление создаст внутри массива пустой бакет `None` , а при поиске элемента линейное пробирование остановится на этом пустом бакете и вернет результат, из-за чего элементы ниже этого бакета уже не смогут быть найдены, и программа может ошибочно посчитать, что их не существует, как показано на рисунке 6-7. @@ -1557,7 +1557,7 @@ comments: true Поэтому имеет смысл при линейном пробировании запоминать индекс первого встреченного `TOMBSTONE` и затем менять найденный целевой элемент местами с этим `TOMBSTONE` . Преимущество такого подхода в том, что при каждом поиске или добавлении элемент будет перемещаться в бакет, расположенный ближе к его идеальной позиции (начальной точке пробирования), а значит, эффективность поиска улучшится. -Ниже приведена реализация хеш-таблицы с открытой адресацией (линейное пробирование), включающая ленивое удаление. Чтобы пространство хеш-таблицы использовалось более полно, мы рассматриваем ее как "кольцевой массив": когда обход выходит за конец массива, он возвращается к началу и продолжается. +Ниже приведена реализация хеш-таблицы с открытой адресацией, то есть с линейным пробированием, включающая ленивое удаление. Чтобы пространство хеш-таблицы использовалось более полно, мы рассматриваем ее как кольцевой массив: когда обход выходит за конец массива, он возвращается к началу и продолжается. === "Python" @@ -3315,4 +3315,4 @@ comments: true - Python использует открытую адресацию. В словаре `dict` для пробирования применяются псевдослучайные числа. - Java использует метод цепочек. Начиная с JDK 1.8, когда длина массива внутри `HashMap` достигает 64, а длина списка достигает 8, этот список преобразуется в красно-черное дерево для повышения производительности поиска. -- Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение; при переполнении подключается overflow-bucket, а когда таких bucket становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность. +- Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение; при переполнении подключается overflow-бакет, а когда таких бакетов становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность. diff --git a/ru/docs/chapter_hashing/hash_map.md b/ru/docs/chapter_hashing/hash_map.md index 3b71915f1..d59ed19fa 100644 --- a/ru/docs/chapter_hashing/hash_map.md +++ b/ru/docs/chapter_hashing/hash_map.md @@ -4,15 +4,15 @@ comments: true # 6.1   Хеш-таблица -Хеш-таблица (hash table), также называемая таблицей рассеяния, обеспечивает эффективный поиск элементов за счет отображения между ключом `key` и значением `value` . Иначе говоря, если передать в хеш-таблицу ключ `key` , то можно за $O(1)$ времени получить соответствующее значение `value` . +Хеш-таблица (hash table), также называемая таблицей рассеяния, реализует эффективный поиск элементов за счет установления соответствия между ключом `key` и значением `value` . Иначе говоря, если передать в хеш-таблицу ключ `key` , то можно за $O(1)$ времени получить соответствующее значение `value` . -Как показано на рисунке 6-1, пусть есть $n$ студентов, и у каждого из них есть два поля данных: "имя" и "номер студенческого билета". Если мы хотим реализовать запрос вида "ввести номер студенческого билета и вернуть соответствующее имя", то для этого можно использовать показанную ниже хеш-таблицу. +Как показано на рисунке 6-1, пусть есть $n$ студентов, и у каждого из них есть два поля данных: имя и номер студенческого билета. Если мы хотим реализовать запрос вида "ввести номер студенческого билета и вернуть соответствующее имя", то для этого можно использовать показанную ниже хеш-таблицу. ![Абстрактное представление хеш-таблицы](hash_map.assets/hash_table_lookup.png){ class="animation-figure" }

Рисунок 6-1   Абстрактное представление хеш-таблицы

-Помимо хеш-таблицы, функции поиска можно реализовать и через массив, и через связный список. Сравнение их эффективности приведено в таблице 6-1. +Помимо хеш-таблицы, функцией поиска также обладают массив и связный список. Сравнение их эффективности приведено в таблице 6-1. - **Добавление элемента**: нужно лишь добавить элемент в конец массива (или списка), что занимает $O(1)$ времени. - **Поиск элемента**: так как массив (или список) неупорядочен, приходится обходить все элементы, что занимает $O(n)$ времени. @@ -30,7 +30,7 @@ comments: true -Нетрудно заметить, что **операции чтения, добавления, удаления и обновления в хеш-таблице имеют временную сложность $O(1)$** , то есть выполняются очень эффективно. +Нетрудно заметить, что **операции поиска, добавления и удаления в хеш-таблице имеют временную сложность $O(1)$** , то есть выполняются очень эффективно. ## 6.1.1   Основные операции с хеш-таблицей @@ -327,7 +327,7 @@ comments: true https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%B4%D0%BE%D0%B1%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%9B%D0%BE%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A1%D1%83%D0%B0%D0%BD%D1%8C%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A4%D0%B0%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%D0%A3%D1%82%D0%B5%D0%BD%D0%BE%D0%BA%22%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B4%D0%B0%D1%82%D1%8C%20%D0%BA%D0%BB%D1%8E%D1%87%20key%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%B8%20%D0%BF%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D0%B8%D0%B7%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%8B%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false -У хеш-таблицы есть три распространенных способа обхода: обход пар ключ-значение, обход ключей и обход значений. Примеры кода приведены ниже: +Существует три распространенных способа обхода хеш-таблицы: обход пар ключ-значение, обход ключей и обход значений. Примеры кода приведены ниже: === "Python" @@ -552,7 +552,7 @@ comments: true Сначала рассмотрим самый простой случай: **реализуем хеш-таблицу только с помощью одного массива**. В хеш-таблице каждую пустую ячейку массива мы называем бакетом (bucket), и каждый бакет может хранить одну пару ключ-значение. Следовательно, операция поиска сводится к тому, чтобы найти бакет, соответствующий `key` , и получить из него `value` . -Но как определить бакет, соответствующий заданному `key` ? Это делается с помощью хеш-функции (hash function). Назначение хеш-функции - отображать большое входное пространство в меньшее выходное пространство. В хеш-таблице входным пространством являются все `key` , а выходным - все бакеты (индексы массива). Иначе говоря, передав `key` на вход, **мы можем через хеш-функцию получить позицию хранения соответствующей пары ключ-значение в массиве**. +Но как определить бакет, соответствующий заданному `key` ? Это делается с помощью хеш-функции (hash function). Назначение хеш-функции - отображать большое входное пространство в меньшее выходное пространство. В хеш-таблице входным пространством являются все `key` , а выходным - все бакеты, то есть индексы массива. Иначе говоря, передав `key` на вход, **мы можем с помощью хеш-функции получить позицию хранения соответствующей пары ключ-значение в массиве**. Процесс вычисления хеш-функции для одного `key` включает два шага. diff --git a/ru/docs/chapter_hashing/index.md b/ru/docs/chapter_hashing/index.md index 4b117fb35..5a608fd55 100644 --- a/ru/docs/chapter_hashing/index.md +++ b/ru/docs/chapter_hashing/index.md @@ -9,13 +9,13 @@ icon: material/table-search !!! abstract - В мире компьютеров хеш-таблица похожа на сообразительного библиотекаря. + Хеш-таблица устанавливает соответствие между ключом и значением. - Он умеет вычислять шифр хранения и потому быстро находит нужную книгу. + Благодаря этому она позволяет получать нужное значение по ключу за очень короткое время. ## Содержание главы - [6.1   Хеш-таблица](hash_map.md) - [6.2   Хеш-коллизии](hash_collision.md) -- [6.3   Хеш-алгоритмы](hash_algorithm.md) +- [6.3   Алгоритмы хеширования](hash_algorithm.md) - [6.4   Резюме](summary.md) diff --git a/ru/docs/chapter_hashing/summary.md b/ru/docs/chapter_hashing/summary.md index 3e00d9a0c..c483d1bc5 100644 --- a/ru/docs/chapter_hashing/summary.md +++ b/ru/docs/chapter_hashing/summary.md @@ -2,9 +2,9 @@ comments: true --- -# 6.4   Краткие итоги +# 6.4   Резюме -### 1.   Основные моменты +### 1.   Ключевые выводы - Передав `key` , мы можем получить `value` из хеш-таблицы за $O(1)$ времени, поэтому она очень эффективна. - К типичным операциям хеш-таблицы относятся поиск, добавление пары ключ-значение, удаление пары ключ-значение и обход хеш-таблицы. diff --git a/ru/docs/chapter_heap/build_heap.md b/ru/docs/chapter_heap/build_heap.md index 6213b4886..a2581da5f 100644 --- a/ru/docs/chapter_heap/build_heap.md +++ b/ru/docs/chapter_heap/build_heap.md @@ -4,13 +4,13 @@ comments: true # 8.2   Построение кучи -В некоторых случаях мы хотим построить кучу, используя сразу все элементы списка. Этот процесс называется "построением кучи". +В некоторых случаях требуется построить кучу, используя сразу все элементы списка. Этот процесс называется построением кучи. ## 8.2.1   Реализация через операцию добавления в кучу -Сначала мы создаем пустую кучу, затем обходим список и для каждого элемента по очереди выполняем "операцию добавления в кучу": сначала помещаем элемент в хвост кучи, а затем выполняем для него упорядочивание "снизу вверх". +Сначала мы создаем пустую кучу, затем обходим список и для каждого элемента по очереди выполняем операцию добавления в кучу: сначала помещаем элемент в хвост кучи, а затем выполняем для него упорядочивание снизу вверх. -Каждый раз, когда элемент добавляется в кучу, ее длина увеличивается на единицу. Поскольку узлы последовательно добавляются в двоичное дерево сверху вниз, куча строится "сверху вниз". +Каждый раз, когда элемент добавляется в кучу, ее длина увеличивается на единицу. Поскольку узлы последовательно добавляются в двоичное дерево сверху вниз, куча строится сверху вниз. Пусть число элементов равно $n$ ; так как каждая операция добавления требует $O(\log{n})$ времени, временная сложность такого построения кучи составляет $O(n \log n)$ . @@ -19,9 +19,9 @@ comments: true На самом деле можно реализовать и более эффективный способ построения кучи, который состоит из двух шагов. 1. Без изменений добавить все элементы списка в кучу; в этот момент свойства кучи еще не выполняются. -2. Обойти кучу в обратном порядке, то есть в порядке, обратном обходу по уровням, и по очереди выполнить упорядочивание "сверху вниз" для каждого нелистового узла. +2. Обойти кучу в обратном порядке, то есть в порядке, обратном обходу по уровням, и по очереди выполнить упорядочивание сверху вниз для каждого нелистового узла. -**После того как некоторый узел был упорядочен, поддерево с этим узлом в качестве корня становится корректной подкучей**. А поскольку обход выполняется в обратном порядке, куча строится "снизу вверх". +**После того как некоторый узел был упорядочен, поддерево с этим узлом в качестве корня становится корректной подкучей**. А поскольку обход выполняется в обратном порядке, куча строится снизу вверх. Причина выбора обратного обхода в том, что он гарантирует: поддеревья ниже текущего узла уже являются корректными подкучами, а значит, упорядочивание текущего узла действительно будет эффективным. @@ -342,7 +342,7 @@ comments: true

Рисунок 8-5   Число узлов на каждом уровне идеального двоичного дерева

-Как показано на рисунке 8-5, максимальное число итераций упорядочивания "сверху вниз" для некоторого узла равно расстоянию от этого узла до листового узла, а это расстояние как раз и есть "высота узла". Поэтому мы можем просуммировать для каждого уровня выражение "число узлов $\times$ высота узла" и **получить суммарное число итераций упорядочивания для всех узлов**. +Как показано на рисунке 8-5, максимальное число итераций упорядочивания сверху вниз для некоторого узла равно расстоянию от этого узла до листового узла, а это расстояние как раз и есть высота узла. Поэтому мы можем просуммировать для каждого уровня выражение "число узлов $\times$ высота узла" и **получить суммарное число итераций упорядочивания для всех узлов**. $$ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1 @@ -373,4 +373,4 @@ T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline \end{aligned} $$ -Далее, число узлов идеального двоичного дерева высоты $h$ равно $n = 2^{h+1} - 1$ , поэтому несложно получить сложность $O(2^h) = O(n)$ . Из этого вывода следует, что **построение кучи из входного списка имеет временную сложность $O(n)$ , что очень эффективно**. +Далее, число узлов идеального двоичного дерева высоты $h$ равно $n = 2^{h+1} - 1$ , поэтому несложно получить сложность $O(2^h) = O(n)$ . Из этого вывода следует, что **построение кучи из входного списка имеет временную сложность $O(n)$ , то есть выполняется очень эффективно**. diff --git a/ru/docs/chapter_heap/heap.md b/ru/docs/chapter_heap/heap.md index 6d6ccd66a..c0a9d6760 100644 --- a/ru/docs/chapter_heap/heap.md +++ b/ru/docs/chapter_heap/heap.md @@ -16,14 +16,14 @@ comments: true Куча, являясь частным случаем полного двоичного дерева, обладает следующими свойствами. - Узлы самого нижнего уровня заполняются слева, а все остальные уровни заполнены полностью. -- Корневой узел двоичного дерева мы называем "вершиной кучи", а самый правый узел нижнего уровня - "основанием кучи". +- Корневой узел двоичного дерева мы называем вершиной кучи, а самый правый узел нижнего уровня - основанием кучи. - Для максимальной (минимальной) кучи значение элемента на вершине, то есть у корневого узла, является максимальным (минимальным). ## 8.1.1   Распространенные операции с кучей Нужно отметить, что многие языки программирования предоставляют не саму кучу, а очередь с приоритетом (priority queue) - абстрактную структуру данных, определяемую как очередь, в которой элементы извлекаются в соответствии с приоритетом. -На практике **куча обычно используется для реализации очереди с приоритетом, а максимальная куча эквивалентна очереди с приоритетом, в которой элементы извлекаются по убыванию**. С точки зрения использования "очередь с приоритетом" и "куча" можно считать эквивалентными структурами данных. Поэтому в этой книге мы не будем специально различать их и в дальнейшем будем единообразно называть "кучей". +На практике **куча обычно используется для реализации очереди с приоритетом, а максимальная куча эквивалентна очереди с приоритетом, в которой элементы извлекаются по убыванию**. С точки зрения использования очередь с приоритетом и куча могут считаться эквивалентными структурами данных. Поэтому в этой книге мы не будем специально различать их и в дальнейшем будем единообразно называть кучей. Распространенные операции с кучей приведены в таблице 8-1. Конкретные имена методов зависят от языка программирования. @@ -812,7 +812,7 @@ comments: true ### 3.   Добавление элемента в кучу -Пусть дан элемент `val` . Сначала мы помещаем его в основание кучи. После добавления свойства кучи могут нарушиться, потому что `val` может оказаться больше, чем другие элементы в куче. **Поэтому необходимо восстановить порядок на пути от вставленного узла к корню** ; эта операция называется heapify, то есть упорядочиванием кучи. +Пусть дан элемент `val` . Сначала мы помещаем его в основание кучи. После добавления свойства кучи могут нарушиться, потому что `val` может оказаться больше, чем другие элементы в куче. **Поэтому необходимо восстановить порядок на пути от вставленного узла к корню** ; эта операция называется упорядочиванием кучи. Рассмотрим ситуацию, когда упорядочивание выполняется **снизу вверх**, начиная от только что вставленного узла. Как показано на рисунках ниже, мы сравниваем значение вставленного узла со значением его родителя; если вставленный узел больше, то меняем их местами. Затем продолжаем выполнять ту же операцию и последовательно восстанавливать корректность всех узлов по пути снизу вверх, пока не выйдем за корень или не встретим узел, для которого обмен не требуется. @@ -845,7 +845,7 @@ comments: true

Рисунок 8-3   Шаги добавления элемента в кучу

-Пусть общее число узлов равно $n$ , тогда высота дерева равна $O(\log n)$ . Следовательно, максимальное число итераций операции heapify тоже не превышает $O(\log n)$ . Отсюда **временная сложность добавления элемента в кучу равна $O(\log n)$** . Код приведен ниже: +Пусть общее число узлов равно $n$ , тогда высота дерева равна $O(\log n)$ . Следовательно, максимальное число итераций операции упорядочивания кучи тоже не превышает $O(\log n)$ . Отсюда **временная сложность добавления элемента в кучу равна $O(\log n)$** . Код приведен ниже: === "Python" @@ -1217,13 +1217,13 @@ comments: true ### 4.   Извлечение элемента с вершины кучи -Элемент на вершине кучи - это корневой узел двоичного дерева, то есть первый элемент списка. Если просто удалить первый элемент списка, то индексы всех узлов двоичного дерева изменятся, и это сильно затруднит последующее восстановление структуры при помощи heapify. Чтобы по возможности минимизировать изменение индексов элементов, мы используем следующий порядок действий. +Элемент на вершине кучи - это корневой узел двоичного дерева, то есть первый элемент списка. Если просто удалить первый элемент списка, то индексы всех узлов двоичного дерева изменятся, и это сильно затруднит последующее восстановление структуры при помощи упорядочивания кучи. Чтобы по возможности минимизировать изменение индексов элементов, мы используем следующий порядок действий. 1. Поменять местами элемент на вершине кучи и элемент у основания кучи, то есть поменять корневой узел с самым правым листовым узлом. -2. После обмена удалить основание кучи из списка. Обрати внимание: поскольку обмен уже выполнен, фактически удаляется исходный элемент вершины кучи. -3. Начиная от корневого узла, **выполнить heapify сверху вниз**. +2. После обмена удалить основание кучи из списка. Стоит отметить, что, поскольку обмен уже выполнен, фактически удаляется исходный элемент вершины кучи. +3. Начиная от корневого узла, **выполнить упорядочивание кучи сверху вниз**. -Как показано на рисунках ниже, **направление операции "heapify сверху вниз" противоположно операции "heapify снизу вверх"**. Мы сравниваем значение корневого узла со значениями двух дочерних узлов, выбираем больший дочерний узел и меняем его местами с корневым узлом. Затем циклически повторяем ту же операцию, пока не выйдем за листовой узел или не встретим узел, который уже не требует обмена. +Как показано на рисунках ниже, **направление операции упорядочивания кучи сверху вниз противоположно операции упорядочивания кучи снизу вверх**. Мы сравниваем значение корневого узла со значениями двух дочерних узлов, выбираем больший дочерний узел и меняем его местами с корневым узлом. Затем циклически повторяем ту же операцию, пока не выйдем за листовой узел или не встретим узел, который уже не требует обмена. === "<1>" ![Шаги извлечения элемента с вершины кучи](heap.assets/heap_pop_step1.png){ class="animation-figure" } diff --git a/ru/docs/chapter_heap/index.md b/ru/docs/chapter_heap/index.md index 85a2fb53b..aa37224ef 100644 --- a/ru/docs/chapter_heap/index.md +++ b/ru/docs/chapter_heap/index.md @@ -9,13 +9,13 @@ icon: material/family-tree !!! abstract - Куча похожа на горные вершины: ярусные, волнистые и самые разные по форме. + Куча - это полное двоичное дерево, удовлетворяющее определенным условиям. - Каждая вершина имеет свою высоту, но самая высокая всегда бросается в глаза первой. + В максимальной и минимальной куче элемент на вершине всегда обладает самым выраженным приоритетом. ## Содержание главы - [8.1   Куча](heap.md) - [8.2   Построение кучи](build_heap.md) -- [8.3   Задача Top-K](top_k.md) +- [8.3   Задача Top-k](top_k.md) - [8.4   Резюме](summary.md) diff --git a/ru/docs/chapter_heap/summary.md b/ru/docs/chapter_heap/summary.md index 0fbacbe13..0683ad3ba 100644 --- a/ru/docs/chapter_heap/summary.md +++ b/ru/docs/chapter_heap/summary.md @@ -2,9 +2,9 @@ comments: true --- -# 8.4   Краткие итоги +# 8.4   Резюме -### 1.   Основные моменты +### 1.   Ключевые выводы - Куча представляет собой полное двоичное дерево и делится на максимальную кучу и минимальную кучу. Элемент на вершине максимальной (минимальной) кучи является наибольшим (наименьшим). - Очередь с приоритетом определяется как очередь, элементы которой извлекаются в соответствии с приоритетом; обычно ее реализуют с помощью кучи. @@ -18,4 +18,4 @@ comments: true **Q**: Является ли "куча" как структура данных тем же самым понятием, что и "куча" в управлении памятью? -Это не одно и то же, просто у них случайно совпало название. Куча в памяти компьютерной системы является частью динамического распределения памяти: во время выполнения программы она используется для хранения данных. Программа может запросить определенный объем памяти в куче для хранения сложных структур, таких как объекты и массивы. Когда эти данные больше не нужны, память нужно освободить, чтобы не допустить утечек. По сравнению со стековой памятью управление памятью в куче требует большей осторожности, а неправильное использование может привести к утечкам памяти, висячим указателям и другим проблемам. +Это не одно и то же, просто у них случайно совпало название. Куча в памяти компьютерной системы является частью динамического распределения памяти: во время выполнения программы она используется для хранения данных. Программа может запросить определенный объем памяти в куче для хранения сложных структур, таких как объекты и массивы. Когда эти данные больше не нужны, память нужно освободить, чтобы не допустить утечек. По сравнению со стековой памятью управление памятью в куче требует большей осторожности, а неправильное использование может привести к утечкам памяти и проблемам с указателями. diff --git a/ru/docs/chapter_heap/top_k.md b/ru/docs/chapter_heap/top_k.md index c25073323..b1843d547 100644 --- a/ru/docs/chapter_heap/top_k.md +++ b/ru/docs/chapter_heap/top_k.md @@ -28,7 +28,7 @@ comments: true Как показано на рисунке 8-7, можно сначала отсортировать массив `nums` , а затем вернуть его крайние правые $k$ элементов; временная сложность такого метода равна $O(n \log n)$ . -Очевидно, что этот способ "делает слишком много", потому что нам нужно только найти наибольшие $k$ элементов, а сортировать остальные элементы совсем не обязательно. +Очевидно, что этот способ делает слишком много, потому что нам нужно только найти наибольшие $k$ элементов, а сортировать остальные элементы совсем не обязательно. ![Поиск наибольших k элементов через сортировку](top_k.assets/top_k_sorting.png){ class="animation-figure" } diff --git a/ru/docs/chapter_hello_algo/index.md b/ru/docs/chapter_hello_algo/index.md index a2d34022d..d9077832e 100644 --- a/ru/docs/chapter_hello_algo/index.md +++ b/ru/docs/chapter_hello_algo/index.md @@ -5,23 +5,23 @@ icon: material/rocket-launch-outline # Перед началом -Несколько лет назад я публиковал на LeetCode разборы серии задач "Sword for Offer" и получил поддержку и ободрение от многих читателей. Во время общения с ними чаще всего мне задавали один и тот же вопрос: "как начать изучать алгоритмы". Постепенно этот вопрос начал меня по-настоящему занимать. +Несколько лет назад я публиковал на LeetCode разборы серии задач "Sword for Offer" и получил поддержку и ободрение от многих читателей. Во время общения с ними мне чаще всего задавали один и тот же вопрос: "как начать изучать алгоритмы?" Постепенно этот вопрос начал меня по-настоящему занимать. Слепо бросаться в решение задач кажется самым популярным способом: он прост, прямолинеен и действительно работает. Но решение задач похоже на игру в "Сапера": люди с сильными навыками самообучения способны обезвредить мины одну за другой, а тем, у кого не хватает базы, легко набить себе шишки и шаг за шагом отступить под давлением неудач. Полностью проходить учебники тоже принято часто, но для тех, кто готовится к поиску работы, диплом, резюме, письменные тесты и собеседования уже отнимают большую часть сил, и потому толстые книги нередко превращаются в тяжелое испытание. -Если ты тоже сталкиваешься с такими трудностями, то можно сказать, что эта книга сама "нашла" тебя. Она стала моим ответом на этот вопрос: пусть не идеальным, но как минимум честной и активной попыткой. Эта книга сама по себе не гарантирует оффер, но поможет тебе увидеть "карту знаний" по структурам данных и алгоритмам, понять форму, размер и расположение разных "мин" и освоить разные "способы разминирования". Освоив это, ты сможешь увереннее решать задачи и читать технические материалы, шаг за шагом выстраивая целостную систему знаний. +Если ты тоже сталкиваешься с такими трудностями, то можно сказать, что эта книга сама "нашла" тебя. Она стала моим ответом на этот вопрос: пусть и не идеальным, но как минимум честной и активной попыткой. Эта книга сама по себе не гарантирует предложения о работе, но поможет тебе увидеть "карту знаний" по структурам данных и алгоритмам, понять форму, размер и расположение разных "мин" и освоить разные "способы разминирования". Освоив это, ты сможешь увереннее решать задачи и читать технические материалы, шаг за шагом выстраивая целостную систему знаний. Я глубоко согласен со словами профессора Фейнмана: "Knowledge isn't free. You have to pay attention." В этом смысле книга не совсем "бесплатна". Чтобы не подвести то драгоценное "внимание", которое ты ей уделишь, я постараюсь вложить в ее создание максимум собственного "внимания". -Я хорошо понимаю ограниченность собственных знаний. Хотя материал этой книги уже довольно долго шлифовался, в нем наверняка все еще осталось немало ошибок, поэтому я искренне прошу преподавателей и читателей указывать на неточности и недоработки. +Я хорошо понимаю пределы собственных знаний. Хотя материал этой книги уже довольно долго шлифовался, в нем наверняка все еще осталось немало ошибок, поэтому я искренне прошу преподавателей и читателей указывать на неточности и недоработки. ![Hello Algo](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" }
-

Hello, Алго!

+

Hello, алгоритмы!

-Появление компьютеров радикально изменило мир. Благодаря высокой вычислительной скорости и отличной программируемости они стали идеальной средой для исполнения алгоритмов и обработки данных. Реалистичная графика в играх, интеллектуальные решения в автономном вождении, впечатляющие партии AlphaGo и естественное взаимодействие ChatGPT: все это изящные проявления алгоритмов на компьютере. +Появление компьютеров радикально изменило мир. Благодаря высокой скорости вычислений и отличной программируемости они стали идеальной средой для исполнения алгоритмов и обработки данных. Реалистичная графика в играх, интеллектуальные решения в автономном вождении, впечатляющие партии AlphaGo и естественное взаимодействие ChatGPT: все это изящные проявления алгоритмов на компьютере. На самом деле еще до появления компьютеров алгоритмы и структуры данных уже существовали во всех уголках мира. Ранние алгоритмы были сравнительно простыми: например, древние способы счета или последовательности действий при изготовлении инструментов. По мере развития цивилизации алгоритмы становились тоньше и сложнее. За мастерством ремесленников, промышленными продуктами, освобождающими производительные силы, и даже за научными законами движения Вселенной почти всегда стоит изобретательная алгоритмическая мысль. diff --git a/ru/docs/chapter_introduction/algorithms_are_everywhere.md b/ru/docs/chapter_introduction/algorithms_are_everywhere.md index 283195298..8d131e21b 100644 --- a/ru/docs/chapter_introduction/algorithms_are_everywhere.md +++ b/ru/docs/chapter_introduction/algorithms_are_everywhere.md @@ -4,63 +4,63 @@ comments: true # 1.1   Алгоритмы повсюду -Когда мы слышим слово "алгоритм", мы естественным образом думаем о математике. Однако на деле многие алгоритмы не связаны со сложной математикой, а в гораздо большей степени опираются на базовую логику, которую можно увидеть повсюду в повседневной жизни. +Говоря об алгоритмах, естественно вспомнить о математике. Однако на самом деле многие алгоритмы не связаны со сложной математикой, а больше полагаются на базовую логику, которая повсеместно встречается в нашей повседневной жизни. -Прежде чем официально перейти к разговору об алгоритмах, стоит поделиться одним любопытным фактом: **ты уже незаметно для себя освоил множество алгоритмов и привык применять их в повседневной жизни**. Ниже я приведу несколько конкретных примеров, чтобы это показать. +Прежде чем углубиться в обсуждение алгоритмов, стоит упомянуть интересный факт: **вы уже точно освоили множество алгоритмов и привыкли применять их в повседневной жизни**. Далее приведем несколько конкретных примеров, чтобы подтвердить этот факт. -**Пример 1: поиск в словаре**. В английском словаре слова расположены в алфавитном порядке. Предположим, нам нужно найти слово, начинающееся на букву $r$; обычно это делается так, как показано ниже. +**Пример 1: поиск в словаре**. В словаре все слова упорядочены по алфавиту. Предположим, нам нужно найти слово, начинающееся на букву $r$; обычно для этого нужно выполнить следующие действия. -1. Открой словарь примерно посередине и посмотри, с какой буквы начинается страница; предположим, это буква $m$. -2. Поскольку в алфавите $r$ идет после $m$, первую половину словаря можно отбросить, и область поиска сузится до второй половины. -3. Повторяй шаги `1.` и `2.` до тех пор, пока не найдешь страницу, на которой слово начинается с буквы $r$. +1. Откройте словарь примерно на половине страниц и посмотрите, какая буква является первой на этой странице; предположим, это буква $m$. +2. Поскольку в алфавите буква $r$ идет после $m$, исключаем первую половину словаря, и область поиска сужается до второй половины. +3. Продолжайте повторять шаги `1.` и `2.` , пока не найдете страницу, где первой буквой слов будет $r$. === "<1>" - ![Процесс поиска в словаре](algorithms_are_everywhere.assets/binary_search_dictionary_step1.png){ class="animation-figure" } + ![Этапы поиска в словаре. Шаг 1](algorithms_are_everywhere.assets/binary_search_dictionary_step1.png){ class="animation-figure" } === "<2>" - ![Бинарный поиск в словаре, шаг 2](algorithms_are_everywhere.assets/binary_search_dictionary_step2.png){ class="animation-figure" } + ![Этапы поиска в словаре. Шаг 2](algorithms_are_everywhere.assets/binary_search_dictionary_step2.png){ class="animation-figure" } === "<3>" - ![Бинарный поиск в словаре, шаг 3](algorithms_are_everywhere.assets/binary_search_dictionary_step3.png){ class="animation-figure" } + ![Этапы поиска в словаре. Шаг 3](algorithms_are_everywhere.assets/binary_search_dictionary_step3.png){ class="animation-figure" } === "<4>" - ![Бинарный поиск в словаре, шаг 4](algorithms_are_everywhere.assets/binary_search_dictionary_step4.png){ class="animation-figure" } + ![Этапы поиска в словаре. Шаг 4](algorithms_are_everywhere.assets/binary_search_dictionary_step4.png){ class="animation-figure" } === "<5>" - ![Бинарный поиск в словаре, шаг 5](algorithms_are_everywhere.assets/binary_search_dictionary_step5.png){ class="animation-figure" } + ![Этапы поиска в словаре. Шаг 5](algorithms_are_everywhere.assets/binary_search_dictionary_step5.png){ class="animation-figure" } -

Рисунок 1-1   Процесс поиска в словаре

+

Рисунок 1-1   Этапы поиска в словаре. Шаг 1

-Поиск в словаре, обязательный навык для школьников, на самом деле и есть знаменитый алгоритм "двоичного поиска". С точки зрения структур данных словарь можно рассматривать как отсортированный "массив"; с точки зрения алгоритмов последовательность действий при поиске слова в словаре можно считать алгоритмом "двоичного поиска". +Навык поиска в словаре, которым владеет каждый школьник, на самом деле является известным алгоритмом двоичного поиска. С точки зрения структуры данных словарь можно рассматривать как отсортированный массив; с точки зрения алгоритма последовательность операций по поиску в словаре можно считать двоичным поиском. -**Пример 2: упорядочивание карт**. Во время игры в карты нам нужно раскладывать карты в руке по возрастанию; процесс выглядит так, как показано ниже. +**Пример 2: упорядочивание карт**. Во время игры в карты необходимо каждый раз упорядочивать карты в руке от меньшего к большему. Для этого нужно выполнить следующие действия. -1. Раздели карты на "упорядоченную" и "неупорядоченную" части и предположи, что в начальный момент самая левая карта уже стоит на правильном месте. -2. Возьми одну карту из неупорядоченной части и вставь ее в правильную позицию внутри упорядоченной части; после этого две самые левые карты уже будут упорядочены. -3. Повторяй шаг `2.` , каждый раз перенося одну карту из неупорядоченной части в упорядоченную, пока все карты не окажутся в порядке. +1. Разделите карты на упорядоченную и неупорядоченную части, предполагая, что изначально самая левая карта уже упорядочена. +2. Из неупорядоченной части извлеките одну карту и вставьте ее в правильное место в упорядоченной части; после этого две самые левые карты станут упорядоченными. +3. Повторяйте шаг `2.` , каждый раз перемещая одну карту из неупорядоченной части в упорядоченную, пока все карты не станут упорядоченными. -![Процесс сортировки колоды карт](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" } +![Этапы упорядочивания карт](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" } -

Рисунок 1-2   Процесс сортировки колоды карт

+

Рисунок 1-2   Этапы упорядочивания карт

-Описанный выше способ раскладывать карты по сути является алгоритмом "сортировки вставками", который очень эффективен на небольших наборах данных. Во многих языках программирования во встроенных функциях сортировки тоже можно увидеть этот алгоритм. +Метод упорядочивания карт по своей сути является алгоритмом сортировки вставками, который весьма эффективен при обработке небольших наборов данных. Многие функции сортировки в библиотеках программирования используют именно этот алгоритм. -**Пример 3: выдача сдачи**. Предположим, в супермаркете мы купили товар на $69$ и дали кассиру $100$, поэтому он должен вернуть нам $31$ сдачи. Этот процесс можно наглядно представить так, как показано на рисунке 1-3. +**Пример 3: сдача**. Предположим, что в супермаркете мы купили товар стоимостью $69$ руб. и дали кассиру купюру в $100$ руб. Кассир должен вернуть нам $31$ руб. Для этого ему нужно выполнить следующие действия. -1. Доступные варианты - это купюры достоинством меньше $31$, например $1$, $5$, $10$ и $20$. -2. Возьми самую большую купюру из доступных, то есть $20$, тогда останется $31 - 20 = 11$. -3. Возьми самую большую купюру из оставшихся, то есть $10$, тогда останется $11 - 10 = 1$. -4. Возьми самую большую купюру из оставшихся, то есть $1$, тогда останется $1 - 1 = 0$. -5. Выдача сдачи завершена, итоговая комбинация: $20 + 10 + 1 = 31$. +1. Варианты выбора - это купюры номиналом меньше $31$ руб. Пусть у нас имеются номиналы $1$ , $5$ , $10$ и $20$ руб. +2. Возьмем самую крупную доступную купюру в $20$ руб. Остаток сдачи составит $31 - 20 = 11$ руб. +3. Возьмем самую крупную из оставшихся купюр в $10$ руб. Остаток составит $11 - 10 = 1$ руб. +4. Возьмем самую крупную из оставшихся купюр в $1$ руб. Остаток составит $1 - 1 = 0$ руб. +5. Завершим выдачу сдачи, схема: $20 + 10 + 1 = 31$ руб. -![Процесс выдачи сдачи](algorithms_are_everywhere.assets/greedy_change.png){ class="animation-figure" } +![Этапы выдачи сдачи](algorithms_are_everywhere.assets/greedy_change.png){ class="animation-figure" } -

Рисунок 1-3   Процесс выдачи сдачи

+

Рисунок 1-3   Этапы выдачи сдачи

-В описанных шагах на каждом этапе выбирается наилучший вариант из доступных в текущий момент, то есть используется купюра наибольшего номинала; в результате получается рабочая схема выдачи сдачи. С точки зрения структур данных и алгоритмов такой подход называется "жадным" алгоритмом. +В этих шагах мы на каждом этапе выбираем наилучший вариант, используя купюры наибольшего номинала, и в итоге получаем рабочую схему сдачи. С точки зрения структуры данных и алгоритмов этот метод по своей сути является жадным алгоритмом. -От приготовления еды до межзвездных путешествий почти любое решение задачи связано с алгоритмами. Появление компьютеров позволило нам хранить структуры данных в памяти и писать код, который вызывает CPU и GPU для выполнения алгоритмов. Благодаря этому мы можем переносить реальные задачи в компьютер и решать самые разные сложные проблемы более эффективно. +От приготовления блюда до межзвездных путешествий решение практически любой задачи неразрывно связано с алгоритмами. Появление компьютеров позволило нам с помощью программирования хранить структуры данных в памяти, а также писать код для вызовов к CPU и GPU для выполнения алгоритмов. Таким образом, мы можем переносить задачи из реальной жизни в компьютер и решать различные сложные проблемы более эффективно. !!! tip - Если ты все еще смутно представляешь себе такие понятия, как структуры данных, алгоритмы, массивы и двоичный поиск, просто продолжай читать. Эта книга постепенно введет тебя в мир понимания структур данных и алгоритмов. + Если представление о структурах данных, алгоритмах, массивах и двоичном поиске пока остается расплывчатым, просто продолжайте читать. Эта книга постепенно введет вас в мир структур данных и алгоритмов. diff --git a/ru/docs/chapter_introduction/index.md b/ru/docs/chapter_introduction/index.md index 43ab55684..2c54231eb 100644 --- a/ru/docs/chapter_introduction/index.md +++ b/ru/docs/chapter_introduction/index.md @@ -9,12 +9,12 @@ icon: material/calculator-variant-outline !!! abstract - Юная девушка легко кружится в танце среди данных, а подол ее платья струится мелодией алгоритмов. + Юная девушка кружится в танце, переплетаясь с данными, а по подолу ее платья струится мелодия алгоритмов. - Она приглашает тебя присоединиться к танцу: следуй за ее шагами и войди в мир алгоритмов, полный логики и красоты. + Она приглашает вас присоединиться к танцу: следуйте за ее шагами и войдите в мир алгоритмов, полный логики и красоты. ## Содержание главы - [1.1   Алгоритмы повсюду](algorithms_are_everywhere.md) -- [1.2   Что такое структуры данных и алгоритмы](what_is_dsa.md) +- [1.2   Что такое алгоритм](what_is_dsa.md) - [1.3   Резюме](summary.md) diff --git a/ru/docs/chapter_introduction/summary.md b/ru/docs/chapter_introduction/summary.md index ae7cff9c7..fec670ac0 100644 --- a/ru/docs/chapter_introduction/summary.md +++ b/ru/docs/chapter_introduction/summary.md @@ -6,23 +6,23 @@ comments: true ### 1.   Ключевые выводы -- Алгоритмы повсюду встречаются в повседневной жизни и вовсе не являются чем-то далеким и эзотерическим. На деле мы уже давно незаметно для себя освоили множество алгоритмов и используем их для решения самых разных жизненных задач. -- Принцип поиска в словаре соответствует алгоритму двоичного поиска. Двоичный поиск воплощает важную алгоритмическую идею "разделяй и властвуй". -- Процесс раскладывания карт очень похож на алгоритм сортировки вставками. Сортировка вставками подходит для упорядочивания небольших наборов данных. -- Выдача сдачи по шагам по своей сути является жадным алгоритмом, в котором на каждом этапе выбирается лучшее решение в текущей ситуации. -- Алгоритм - это набор инструкций или шагов, который решает конкретную задачу за конечное время, а структура данных - это способ, которым компьютер организует и хранит данные. -- Структуры данных и алгоритмы тесно связаны. Структуры данных являются основой алгоритмов, а алгоритмы оживляют структуры данных. -- Структуры данных и алгоритмы можно сравнить со сборкой конструктора: детали представляют данные, форма деталей и способ их соединения представляют структуру данных, а шаги сборки соответствуют алгоритму. +- Алгоритмы повсеместно присутствуют в нашей повседневной жизни и не являются недосягаемыми сложными знаниями. На самом деле мы уже освоили множество алгоритмов, которые помогают решать различные жизненные задачи. +- Принцип поиска в словаре соответствует алгоритму двоичного поиска. Двоичный поиск иллюстрирует важную идею алгоритмов "разделяй и властвуй". +- Процесс сортировки карт в колоде очень похож на алгоритм сортировки вставками, который хорошо подходит для сортировки небольших наборов данных. +- Процесс размена по своей сути является жадным алгоритмом, в котором на каждом этапе принимается наилучшее на данный момент решение. +- Алгоритм представляет собой набор инструкций или шагов, предназначенных для решения конкретной задачи в ограниченное время, а структура данных - это способ организации и хранения данных в компьютере. +- Структуры данных и алгоритмы тесно связаны. Структуры данных являются основой для алгоритмов, а алгоритмы оживляют структуры данных. +- Структуры данных и алгоритмы можно сравнить с конструктором: детали конструктора представляют данные, их форма и способы соединения - структуры данных, а этапы сборки конструктора соответствуют алгоритмам. ### 2.   Q & A -**Q**: Я программист и в повседневной работе никогда не решал задачи "алгоритмами": распространенные алгоритмы уже инкапсулированы в языках программирования, и ими можно пользоваться напрямую. Значит ли это, что рабочие задачи еще не дошли до уровня, где действительно нужны алгоритмы? +**Q**: Я программист и в повседневной работе никогда не использовал алгоритмы для решения задач, поскольку часто используемые алгоритмы уже встроены в языки программирования и ими можно пользоваться напрямую. Значит ли это, что рабочие задачи еще не требуют применения алгоритмов? -Если сравнить конкретные рабочие навыки с "приемами" в боевых искусствах, то фундаментальные дисциплины скорее напоминают "внутреннюю силу". +Если сравнить конкретные профессиональные навыки с приемами в боевых искусствах, то базовые дисциплины скорее напоминают "внутреннюю силу". -Я считаю, что смысл изучения алгоритмов (и других фундаментальных дисциплин) не в том, чтобы каждый раз реализовывать их с нуля в работе, а в том, чтобы, опираясь на полученные знания, уметь профессионально реагировать и принимать решения при решении задач, тем самым повышая общее качество работы. Вот простой пример: в каждом языке программирования есть встроенная функция сортировки. +Я считаю, что изучение алгоритмов и других базовых дисциплин важно не для того, чтобы реализовывать их с нуля в работе, а для того, чтобы на основе полученных знаний принимать профессиональные решения и оценки при решении задач, тем самым повышая общее качество работы. Простой пример: каждый язык программирования имеет встроенные функции сортировки. -- Если мы не изучали структуры данных и алгоритмы, то для любых данных, скорее всего, просто отдали бы их этой функции сортировки. Все работает гладко, производительность хорошая, и на первый взгляд никаких проблем нет. -- Но если мы изучали алгоритмы, то знаем, что временная сложность встроенной сортировки равна $O(n \log n)$ ; однако если данные состоят из целых чисел фиксированной разрядности (например, номеров студентов), можно воспользоваться более эффективной "поразрядной сортировкой", снизив сложность до $O(nk)$ , где $k$ - число разрядов. Когда объем данных очень велик, сэкономленное время выполнения может принести заметную пользу, например снизить издержки и улучшить пользовательский опыт. +- Если бы мы не изучали структуры данных и алгоритмы, то, получив любые данные, возможно, просто передали бы их этой функции сортировки. Все работает гладко, производительность хорошая, и на первый взгляд проблем нет. +- Однако если мы изучили алгоритмы, то знаем, что временная сложность встроенной функции сортировки составляет $O(n \log n)$ ; если же данные представлены целыми числами фиксированной разрядности, например номерами студентов, то можно использовать более эффективный метод поразрядной сортировки, снизив временную сложность до $O(nk)$ , где $k$ - это количество разрядов, а при больших объемах данных выиграть во времени, затратах и пользовательском опыте. -В инженерной практике огромное количество задач трудно довести до оптимального решения, и многие из них решаются лишь "примерно". Сложность задачи зависит, с одной стороны, от ее собственной природы, а с другой - от запаса знаний человека, который на нее смотрит. Чем полнее знания и чем больше опыт, тем глубже получается анализ задачи и тем изящнее ее можно решить. +В инженерной практике множество задач трудно решить оптимальным образом, и многие из них решаются "как-то". Сложность задачи зависит как от ее природы, так и от уровня знаний и опыта человека, который ее анализирует. Чем более полными знаниями и большим опытом обладает человек, тем глубже он может проанализировать проблему и тем изящнее может быть ее решение. diff --git a/ru/docs/chapter_introduction/what_is_dsa.md b/ru/docs/chapter_introduction/what_is_dsa.md index b6cf7834d..f78be447e 100644 --- a/ru/docs/chapter_introduction/what_is_dsa.md +++ b/ru/docs/chapter_introduction/what_is_dsa.md @@ -6,60 +6,60 @@ comments: true ## 1.2.1   Определение алгоритма -Алгоритм (algorithm) - это набор инструкций или шагов, который решает конкретную задачу за конечное время. Он обладает следующими свойствами. +Алгоритм (algorithm) - это набор инструкций или шагов, предназначенных для решения конкретной задачи за ограниченное время. Он обладает следующими свойствами. -- Задача четко определена и имеет ясные определения входных и выходных данных. -- Алгоритм осуществим и может быть выполнен за конечное число шагов, времени и памяти. -- Каждый шаг имеет однозначный смысл, и при одинаковых входных данных и условиях выполнения результат всегда будет одинаковым. +- Задача четко определена и включает ясные определения входных и выходных данных. +- Обладает осуществимостью и может быть выполнен за ограниченное количество шагов, времени и памяти. +- Каждый шаг имеет определенное значение, и при одинаковых входных данных и условиях выполнения результат всегда будет одинаковым. ## 1.2.2   Определение структуры данных -Структура данных (data structure) - это способ организации и хранения данных, охватывающий само содержимое данных, связи между данными и методы работы с ними. У нее есть следующие цели проектирования. +Структура данных (data structure) - это способ организации и хранения данных, включающий содержимое данных, их взаимосвязи и методы операций с ними. Структура данных преследует следующие цели. -- Занимать как можно меньше места, чтобы экономить память компьютера. -- Выполнять операции над данными как можно быстрее, включая доступ, добавление, удаление, обновление и т. д. -- Предоставлять компактное представление данных и логическую информацию, чтобы алгоритмы могли работать эффективно. +- Минимизировать занимаемое пространство для экономии памяти компьютера. +- Обеспечивать максимально быструю обработку данных, включая доступ, добавление, удаление и обновление данных. +- Обеспечивать простое представление данных и логическую информацию для эффективного выполнения алгоритмов. -**Проектирование структур данных - это процесс, полный компромиссов**. Если мы хотим улучшить что-то одно, то часто вынуждены уступить в чем-то другом. Ниже приведены два примера. +**Проектирование структуры данных - это процесс, полный компромиссов**. Если вы хотите улучшить один аспект, часто приходится идти на уступки в другом. Приведем два примера. -- По сравнению с массивами связные списки удобнее для добавления и удаления данных, но жертвуют скоростью доступа к ним. -- По сравнению со связными списками графы предоставляют более богатую логическую информацию, но требуют большего объема памяти. +- Связный список, по сравнению с массивом, более удобен для добавления и удаления данных, но имеет проблемы со скоростью доступа к данным. +- Граф, по сравнению со связным списком, предоставляет более богатую логическую информацию, но требует большего объема памяти. ## 1.2.3   Связь между структурами данных и алгоритмами -Как показано на рисунке 1-4, структуры данных и алгоритмы тесно связаны и сильно зависят друг от друга; это проявляется в следующих трех аспектах. +Как показано на рисунке 1-4, структуры данных и алгоритмы тесно взаимосвязаны, что проявляется в следующих трех аспектах. -- Структуры данных служат фундаментом алгоритмов. Они дают алгоритмам структурированный способ хранения данных и методы работы с ними. -- Алгоритмы оживляют структуры данных. Сами по себе структуры данных лишь хранят информацию, а в сочетании с алгоритмами позволяют решать конкретные задачи. -- Алгоритмы обычно можно реализовать на основе разных структур данных, но эффективность выполнения может сильно различаться, поэтому выбор подходящей структуры данных является ключевым. +- Структуры данных являются основой алгоритмов. Они обеспечивают структурированное хранение данных и методы их обработки. +- Алгоритмы оживляют структуры данных. Сами по себе структуры данных лишь хранят информацию, но в сочетании с алгоритмами они позволяют решать конкретные задачи. +- Алгоритмы можно реализовать на основе различных структур данных, однако эффективность их выполнения может значительно различаться, поэтому выбор подходящей структуры данных является ключевым фактором. ![Связь между структурами данных и алгоритмами](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png){ class="animation-figure" }

Рисунок 1-4   Связь между структурами данных и алгоритмами

-Структуры данных и алгоритмы похожи на сборку конструктора, показанную на рисунке 1-5. В набор конструктора, помимо множества деталей, входит и подробная инструкция по сборке. Если шаг за шагом следовать этой инструкции, можно собрать красивую модель. +Структуры данных и алгоритмы подобны конструктору, как показано на рисунке 1-5. Комплект конструктора, помимо множества деталей, содержит также подробную инструкцию по сборке. Следуя этой инструкции шаг за шагом, можно собрать красивую модель. ![Сборка конструктора](what_is_dsa.assets/assembling_blocks.png){ class="animation-figure" }

Рисунок 1-5   Сборка конструктора

-Подробное соответствие между ними показано в таблице 1-1. +Подробное описание аналогии с конструктором представлено в таблице 1-1. -

Таблица 1-1   Сравнение структур данных и алгоритмов со сборкой конструктора

+

Таблица 1-1   Сравнение структур данных и алгоритмов с конструктором

-| Структуры данных и алгоритмы | Сборка конструктора | -| ---------------------------- | ------------------------------------------ | -| Входные данные | Несобранные детали конструктора | -| Структура данных | Организация деталей: форма, размер, способ соединения и т. д. | -| Алгоритм | Последовательность шагов по сборке деталей в целевую форму | -| Выходные данные | Собранная модель конструктора | +| Структуры данных и алгоритмы | Конструктор | +| ---------------------------- | ----------- | +| Входные данные | Несобранные детали конструктора | +| Структура данных | Организация деталей конструктора, включая форму, размер, способы соединения и т. д. | +| Алгоритм | Последовательность действий по сборке деталей в целевую модель | +| Выходные данные | Собранная модель конструктора |
-Стоит отметить, что структуры данных и алгоритмы не зависят от конкретного языка программирования. Именно поэтому эта книга может давать реализации на разных языках программирования. +Стоит отметить, что структуры данных и алгоритмы не зависят от языка программирования. Именно поэтому данная книга предлагает их реализации на различных языках. !!! tip "Принятое сокращение" - В реальных обсуждениях мы обычно сокращаем выражение "структуры данных и алгоритмы" до просто "алгоритмы". Например, хорошо известные алгоритмические задачи LeetCode на деле одновременно проверяют знания и по структурам данных, и по алгоритмам. + В реальных обсуждениях выражение "структуры данных и алгоритмы" обычно сокращают до просто "алгоритмы". Например, хорошо известные задачи LeetCode на деле одновременно проверяют знания и по структурам данных, и по алгоритмам. diff --git a/ru/docs/chapter_preface/about_the_book.md b/ru/docs/chapter_preface/about_the_book.md index d98a07129..442227429 100644 --- a/ru/docs/chapter_preface/about_the_book.md +++ b/ru/docs/chapter_preface/about_the_book.md @@ -4,31 +4,31 @@ comments: true # 0.1   Об этой книге -Этот проект направлен на создание открытого, бесплатного и дружелюбного к новичкам вводного пособия по структурам данных и алгоритмам. +Этот проект задуман как открытое, бесплатное и дружелюбное к новичкам введение в структуры данных и алгоритмы. -- Вся книга построена на анимированных иллюстрациях: материал изложен ясно и последовательно, а кривая обучения остается плавной, помогая начинающим постепенно увидеть карту знаний по структурам данных и алгоритмам. -- Исходный код можно запускать одним нажатием, что помогает читателю через практику развивать навыки программирования и понимать, как работают алгоритмы и как устроены структуры данных на базовом уровне. -- Мы призываем читателей учиться друг у друга: задавайте вопросы и делитесь своими наблюдениями в комментариях, чтобы вместе продвигаться вперед через обсуждение и обмен идеями. +- В книге используются анимированные иллюстрации: материал изложен ясно и последовательно, что облегчает освоение и помогает начинающим выстроить карту знаний по структурам данных и алгоритмам. +- Исходный код можно запустить одним нажатием, что позволяет тренироваться, развивать навыки программирования и понимать принципы работы алгоритмов и реализации структур данных на фундаментальном уровне. +- Мы призываем читателей к взаимопомощи: задавайте вопросы и делитесь идеями в комментариях. Обсуждения помогают двигаться вперед всем вместе. ## 0.1.1   Целевая аудитория -Если ты только начинаешь изучать алгоритмы, никогда раньше с ними не сталкивался или уже решал некоторые задачи, но все еще смутно представляешь себе структуры данных и алгоритмы и постоянно колеблешься между "понимаю" и "не понимаю", то эта книга создана именно для тебя! +Если вы новичок в алгоритмах, никогда с ними не сталкивались или уже имеете некоторый опыт решения задач, но еще не обладаете четким пониманием структур данных и алгоритмов, эта книга создана специально для вас! -Если у тебя уже накопился определенный опыт решения задач и ты знаком с большинством типовых вопросов, книга поможет тебе системно повторить и упорядочить знания об алгоритмах, а исходный код из репозитория можно использовать как "инструментарий для решения задач" или как "алгоритмический словарь". +Если у вас уже есть определенный опыт решения задач и вы знакомы с большинством типов задач, эта книга поможет вам освежить и систематизировать знания об алгоритмах, а исходный код может служить набором инструментов для решения задач или алгоритмическим словарем. -Если же ты уже настоящий "гуру" алгоритмов, мы будем рады получить твои ценные замечания или [создавать книгу вместе](https://www.hello-algo.com/chapter_appendix/contribution/). +Если вы владеете алгоритмами на экспертном уровне, мы будем рады вашим ценным советам или [совместному участию в создании книги](https://www.hello-algo.com/chapter_appendix/contribution/). !!! success "Предварительные требования" - Тебе нужна хотя бы базовая подготовка в одном из языков программирования, чтобы читать и писать простой код. + Необходимо иметь хотя бы базовую подготовку в одном из языков программирования, чтобы читать и писать простой код. ## 0.1.2   Структура содержания -Основное содержание книги показано на рисунке 0-1. +Основное содержание книги представлено на рисунке 0-1. -- **Анализ сложности**: измерения и методы оценки структур данных и алгоритмов. Способы вычисления временной и пространственной сложности, распространенные типы, примеры и т. д. -- **Структуры данных**: способы классификации базовых типов данных и структур данных. Определения, достоинства и недостатки, основные операции, распространенные разновидности, типичные применения и методы реализации массивов, связных списков, стеков, очередей, хеш-таблиц, деревьев, куч, графов и других структур. -- **Алгоритмы**: определения, достоинства и недостатки, эффективность, области применения, этапы решения и примеры задач для поиска, сортировки, разделяй-и-властвуй, поиска с возвратом, динамического программирования и жадных алгоритмов. +- **Анализ сложности**: критерии и методы оценки структур данных и алгоритмов. Методы расчета временной и пространственной сложности, распространенные типы, примеры и т. д. +- **Структуры данных**: классификация основных типов данных и структур данных. Определение, преимущества и недостатки, основные операции, распространенные типы, типичные приложения и методы реализации массивов, списков, стеков, очередей, хеш-таблиц, деревьев, куч и графов. +- **Алгоритмы**: определение, преимущества и недостатки, эффективность, области применения, этапы решения и примеры задач для поиска, сортировки, алгоритма "разделяй и властвуй", поиска с возвратом, динамического программирования и жадных алгоритмов. ![Основное содержание книги](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" } @@ -36,25 +36,25 @@ comments: true ## 0.1.3   Благодарности -Эта книга непрерывно совершенствуется благодаря совместным усилиям множества участников сообщества open source. Спасибо каждому автору, который вложил свое время и силы; их имена перечислены в порядке, автоматически сгенерированном GitHub: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, IsChristina, xBLACKICEx, guowei-gong, Cathay-Chen, pengchzn, QiLOL, magentaqin, hello-ikun, JoseHung, qualifier1024, thomasq0, sunshinesDL, L-Super, Guanngxu, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, Shyam-Chen, theNefelibatas, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, MolDuM, Nigh, Dr-XYZ, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, beatrix-chan, DullSword, xjr7670, jiaxianhua, qq909244296, iStig, boloboloda, hts0000, gledfish, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, llql1211, fbigm, echo1937, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, GaochaoZhu, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Allen-Scai, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, zhongfq, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Keynman, KeiichiKasai и KawaiiAsh. +Эта книга постоянно совершенствуется благодаря совместным усилиям множества участников открытого сообщества. Благодарим каждого автора, вложившего свое время и силы; их имена перечислены в порядке, автоматически сгенерированном GitHub: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, pengchzn, QiLOL, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, hello-ikun, magentaqin, Guanngxu, thomasq0, sunshinesDL, L-Super, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, Shyam-Chen, sangxiaai, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, Nigh, Dr-XYZ, MolDuM, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, xjr7670, beatrix-chan, DullSword, qq909244296, iStig, boloboloda, hts0000, gledfish, fbigm, echo1937, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, JTCPOWI, KawaiiAsh, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, llql1211, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Senrian, Allen-Scai, 19santosh99, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, codetypess, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Kunchen-Luo, Keynman и KeiichiKasai. -Рецензирование кода для этой книги выполнили coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon и rongyi (в алфавитном порядке). Спасибо им за время и силы: именно они обеспечили единообразие и стандартизацию кода на разных языках. +Рецензирование кода книги выполнили coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon и rongyi (в алфавитном порядке). Благодарим их за потраченное время и силы, которые обеспечили стандартизацию и единообразие кода на различных языках. -Традиционную китайскую версию книги вычитали Shyam-Chen и Dr-XYZ, английскую версию - yuelinxin, K3v123, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn, thomasq0 и magentaqin, а японскую версию - eltociear. Именно благодаря их постоянному вкладу эта книга может быть полезна более широкому кругу читателей, и мы искренне благодарим их. +Английскую версию книги вычитали yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn и thomasq0; японскую версию - eltociear; русскую версию - И. А. Шевкун и Yuyan Huang; традиционную китайскую версию - Shyam-Chen и Dr-XYZ. Именно благодаря их вкладу эта книга может служить более широкому кругу читателей, и мы искренне благодарим их. Инструмент генерации ePub-версии этой книги разработал zhongfq. Благодарим его за вклад, который дал читателям более гибкий способ чтения. -Во время работы над этой книгой мне помогало очень много людей. +В процессе создания этой книги мне помогало много людей. -- Спасибо моему наставнику в компании, доктору Ли Си: в одной из бесед ты подтолкнул меня "быстрее начать", и это укрепило мою решимость написать эту книгу; -- Спасибо моей девушке Bubble, первому читателю этой книги: с позиции новичка в алгоритмах ты дала много ценных замечаний, благодаря которым книга стала понятнее для начинающих; -- Спасибо Tengbao, Qibao и Feibao за придуманное ими креативное название книги, которое возвращает нас к теплому воспоминанию о первой строке кода "Hello World!"; -- Спасибо Xiaoquan за профессиональную помощь по вопросам интеллектуальной собственности: она сыграла важную роль в совершенствовании этой открытой книги; -- Спасибо Sutong за прекрасный дизайн обложки и логотипа, а также за терпеливые многочисленные правки, на которые тебя вдохновлял мой перфекционизм; -- Спасибо @squidfunk за советы по верстке и за созданную им открытую тему документации [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master). +- Благодарю моего наставника в компании, доктора Ли Си: в одной из бесед вы вдохновили меня быстрее начать, что укрепило мою решимость написать эту книгу; +- Благодарю мою девушку Bubble, первого читателя этой книги: с позиции новичка в алгоритмах она дала много ценных советов, благодаря которым книга стала более понятной и доступной; +- Благодарю Tengbao, Qibao и Feibao за креативное название книги, которое навевает приятные воспоминания о первой строке кода "Hello World!"; +- Благодарю Xiaoquan за профессиональную помощь в вопросах интеллектуальной собственности, что сыграло важную роль в совершенствовании этой открытой книги; +- Благодарю Sutong за дизайн обложки и логотипа книги, а также за терпение при многочисленных исправлениях по моим просьбам; +- Благодарю @squidfunk за советы по оформлению и за разработку открытой темы документации [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material). -Во время написания книги я прочитал множество учебников и статей по структурам данных и алгоритмам. Эти работы стали для книги прекрасными образцами и помогли обеспечить точность и качество материала. Я искренне благодарю всех преподавателей и предшественников за их выдающийся вклад! +В процессе написания книги я ознакомился с множеством учебников и статей по структурам данных и алгоритмам. Эти работы послужили отличным образцом для этой книги, обеспечив ее точность и качество. Я искренне благодарю всех преподавателей и предшественников за их выдающийся вклад! -Эта книга пропагандирует способ обучения, в котором работают и руки, и голова; в этом отношении на меня сильно повлияла [Dive into Deep Learning](https://github.com/d2l-ai/d2l-zh). Я горячо рекомендую эту замечательную работу всем читателям. +Эта книга пропагандирует метод обучения, сочетающий умственную и практическую деятельность; в этом отношении на меня сильно повлияла [Dive into Deep Learning](https://github.com/d2l-ai/d2l-zh). Я настоятельно рекомендую эту замечательную работу всем читателям. -**От всего сердца благодарю моих родителей: именно ваша постоянная поддержка и ободрение дали мне возможность заниматься этим интересным делом**. +**Сердечно благодарю моих родителей: именно ваша постоянная поддержка и ободрение дали мне возможность заняться этим увлекательным делом**. diff --git a/ru/docs/chapter_preface/index.md b/ru/docs/chapter_preface/index.md index d26cce461..5dcff9223 100644 --- a/ru/docs/chapter_preface/index.md +++ b/ru/docs/chapter_preface/index.md @@ -9,9 +9,9 @@ icon: material/book-open-outline !!! abstract - Алгоритмы подобны прекрасной симфонии, а каждая строка кода течет, как мелодия. + Алгоритмы подобны прекрасной симфонии, а каждая строка кода льется подобно мелодии. - Пусть эта книга мягко зазвучит в твоем сознании и оставит после себя особую и глубокую мелодию. + Пусть эта книга тихо зазвучит в вашем сознании и оставит после себя особую и глубокую мелодию. ## Содержание главы diff --git a/ru/docs/chapter_preface/suggestions.md b/ru/docs/chapter_preface/suggestions.md index e1fd82e05..21b5cd008 100644 --- a/ru/docs/chapter_preface/suggestions.md +++ b/ru/docs/chapter_preface/suggestions.md @@ -6,16 +6,16 @@ comments: true !!! tip - Чтобы получить наилучший опыт чтения, рекомендуется полностью прочитать этот раздел. + Для получения наилучшего опыта чтения рекомендуется полностью прочитать этот раздел. ## 0.2.1   Соглашения о стиле изложения -- Разделы, помеченные `*` в заголовке, являются дополнительными и сравнительно более сложными. Если времени мало, их можно пока пропустить. -- Технические термины будут выделяться полужирным шрифтом (в бумажной и PDF-версиях) или подчеркиванием (в веб-версии), например массив (array). Рекомендуется запоминать их, чтобы легче читать техническую литературу. -- Ключевое содержание и итоговые формулировки будут **выделяться полужирным**, и на такие фрагменты стоит обращать особое внимание. +- Главы, помеченные `*` в заголовке, являются дополнительными и содержат более сложный материал. Если времени мало, их можно пропустить. +- Профессиональные термины выделяются полужирным шрифтом в печатной и PDF-версии или подчеркиванием в веб-версии, например массив (array). Рекомендуется запоминать их для удобства чтения литературы. +- Важные моменты и обобщающие фразы будут **выделяться полужирным шрифтом**, и на такие тексты следует обращать особое внимание. - Слова и выражения со специальным смыслом будут отмечаться "кавычками", чтобы избежать неоднозначности. -- Когда названия различаются между языками программирования, эта книга ориентируется на Python; например, для обозначения "пустого" значения используется `None`. -- В книге частично отказались от строгих правил оформления комментариев в языках программирования ради более компактной верстки. Комментарии в основном делятся на три типа: комментарии-заголовки, содержательные комментарии и многострочные комментарии. +- Когда термины различаются между языками программирования, в качестве стандарта используется Python; например, `None` применяется для обозначения "пустого" значения. +- В некоторых местах книга отходит от стандартов комментирования программного кода ради более компактного оформления. Комментарии в основном делятся на три типа: заголовочные, содержательные и многострочные. === "Python" @@ -141,10 +141,8 @@ comments: true // Содержательный комментарий: подробно поясняет код - /** - * Многострочный - * комментарий - */ + // Многострочный + // комментарий ``` === "C" @@ -186,9 +184,9 @@ comments: true ## 0.2.2   Эффективное обучение с помощью анимированных иллюстраций -По сравнению с текстом видео и изображения обладают большей информационной плотностью и более четкой структурой, поэтому их легче воспринимать. В этой книге **ключевые и сложные идеи в основном будут показываться в виде анимированных иллюстраций**, а текст будет играть роль пояснения и дополнения. +По сравнению с текстом видео и изображения обладают более высокой плотностью информации и более четкой структурой, поэтому их легче воспринимать. В этой книге **ключевые и сложные моменты в основном представлены в виде анимированных иллюстраций**, а текст служит пояснением и дополнением. -Если во время чтения ты встречаешь фрагмент с анимированной иллюстрацией, как на рисунке 0-2, **в первую очередь ориентируйся на изображение, а текст используй как дополнение**, соединяя оба источника для понимания материала. +Если во время чтения вы встречаете фрагмент с анимированной иллюстрацией, как на рисунке 0-2, **используйте иллюстрацию в качестве основного источника информации, а текст - в качестве вспомогательного**, объединяя оба источника для понимания материала. ![Пример анимированной иллюстрации](../index.assets/animation.gif){ class="animation-figure" } @@ -196,39 +194,39 @@ comments: true ## 0.2.3   Углубление понимания через практику кода -Сопроводительный код этой книги размещен в [репозитории GitHub](https://github.com/krahets/hello-algo). Как показано ниже, **исходный код снабжен тестовыми примерами и может запускаться одним нажатием**. +Сопроводительный код этой книги размещен в [репозитории GitHub](https://github.com/krahets/hello-algo). Как показано ниже, **исходный код содержит тестовые примеры и может быть запущен одним нажатием кнопки**. -Если позволяет время, **рекомендуется самостоятельно перепечатать код**. Если времени на обучение мало, то хотя бы полностью прочитай и запусти весь код. +Если позволяет время, **рекомендуется самостоятельно набирать код**. Если времени на обучение мало, по крайней мере **просмотрите и выполните весь код**. -По сравнению с простым чтением кода сам процесс его написания обычно дает больше пользы. **Учиться на практике - значит учиться по-настоящему**. +Процесс написания кода приносит больше пользы, чем его чтение. **Настоящее обучение - это обучение на практике**. ![Пример запуска кода](../index.assets/running_code.gif){ class="animation-figure" }

Рисунок 0-3   Пример запуска кода

-Подготовка к запуску кода в основном состоит из трех шагов. +Подготовка к запуску кода в основном состоит из трех этапов. -**Шаг 1: установить локальную среду программирования**. Воспользуйся [руководством](https://www.hello-algo.com/chapter_appendix/installation/) из приложения. Если среда уже установлена, этот шаг можно пропустить. +**Шаг 1: установка локальной среды программирования**. Воспользуйтесь [руководством](https://www.hello-algo.com/chapter_appendix/installation/) из приложения. Если среда уже установлена, этот шаг можно пропустить. -**Шаг 2: клонировать или скачать репозиторий с кодом**. Перейди в [репозиторий GitHub](https://github.com/krahets/hello-algo). Если у тебя уже установлен [Git](https://git-scm.com/downloads), репозиторий можно клонировать следующей командой: +**Шаг 2: клонирование или загрузка репозитория кода**. Перейдите в [репозиторий GitHub](https://github.com/krahets/hello-algo). Если у вас уже установлен [Git](https://git-scm.com/downloads), репозиторий можно клонировать следующей командой: ```shell git clone https://github.com/krahets/hello-algo.git ``` -Конечно, можно также нажать кнопку "Download ZIP" в месте, показанном на рисунке 0-4, напрямую скачать архив с кодом и затем распаковать его локально. +Также можно нажать кнопку "Download ZIP" в месте, показанном на рисунке 0-4, напрямую скачать архив с кодом и затем распаковать его локально. ![Клонирование репозитория и загрузка кода](suggestions.assets/download_code.png){ class="animation-figure" }

Рисунок 0-4   Клонирование репозитория и загрузка кода

-**Шаг 3: запустить исходный код**. Как показано на рисунке 0-5, для блоков кода, у которых сверху указано имя файла, соответствующий исходный файл можно найти в папке `codes` репозитория. Эти файлы запускаются одним нажатием, что помогает не тратить лишнее время на отладку и сосредоточиться на изучении материала. +**Шаг 3: запуск исходного кода**. Как показано на рисунке 0-5, для блоков кода, у которых сверху указано имя файла, соответствующий исходный файл можно найти в папке `codes` репозитория. Исходные файлы запускаются одним нажатием, что помогает не тратить лишнее время на отладку и сосредоточиться на изучении материала. ![Блоки кода и соответствующие исходные файлы](suggestions.assets/code_md_to_repo.png){ class="animation-figure" }

Рисунок 0-5   Блоки кода и соответствующие исходные файлы

-Помимо локального запуска, **веб-версия также поддерживает визуальный запуск Python-кода** (на базе [pythontutor](https://pythontutor.com/)). Как показано ниже, можно нажать "Визуализировать выполнение" под блоком кода, чтобы раскрыть окно и наблюдать за выполнением алгоритма; также можно нажать "Полноэкранный режим", чтобы получить более удобный просмотр. +Помимо локального запуска, **веб-версия также поддерживает визуальное выполнение Python-кода** (на базе [pythontutor](https://pythontutor.com/)). Как показано ниже, можно нажать "Визуализировать выполнение" под блоком кода, чтобы раскрыть окно и наблюдать за выполнением алгоритма; также можно нажать "Полноэкранный режим" для более удобного просмотра. ![Визуальный запуск Python-кода](suggestions.assets/pythontutor_example.png){ class="animation-figure" } @@ -236,9 +234,9 @@ git clone https://github.com/krahets/hello-algo.git ## 0.2.4   Совместный рост через вопросы и обсуждения -Во время чтения книги не стоит легко пропускать те места, которые остались непонятными. **Смело задавай свои вопросы в разделе комментариев**: я и мои друзья постараемся ответить тебе как можно тщательнее, обычно в течение двух дней. +Во время чтения книги не стоит пропускать те места, которые остались непонятными. **Мы призываем вас задавать вопросы в разделе комментариев**: я и мои коллеги постараемся ответить вам как можно тщательнее, обычно в течение двух дней. -Как показано на рисунке 0-7, в веб-версии у каждой главы внизу есть раздел комментариев. Надеюсь, ты будешь чаще обращать внимание на его содержание. С одной стороны, это поможет увидеть, с какими трудностями сталкиваются другие читатели, восполнить пробелы и подтолкнуть себя к более глубоким размышлениям. С другой стороны, буду рад, если ты щедро ответишь на вопросы других участников, поделишься своими наблюдениями и поможешь им продвинуться вперед. +Как показано на рисунке 0-7, в веб-версии у каждой главы внизу есть раздел комментариев. Рекомендуется уделять внимание его содержанию. С одной стороны, это поможет увидеть, с какими трудностями сталкиваются другие читатели, восполнить пробелы и подтолкнуть себя к более глубокому пониманию. С другой стороны, мы надеемся, что вы будете отвечать на вопросы других участников и делиться своими мнениями. ![Пример раздела комментариев](../index.assets/comment.gif){ class="animation-figure" } @@ -248,11 +246,11 @@ git clone https://github.com/krahets/hello-algo.git В целом процесс изучения структур данных и алгоритмов можно разделить на три этапа. -1. **Этап 1: введение в алгоритмы**. Нужно познакомиться с особенностями и способами применения разных структур данных, а также изучить принципы, ход работы, назначение и эффективность различных алгоритмов. -2. **Этап 2: решение алгоритмических задач**. Рекомендуется начинать с популярных задач и сначала накопить не менее 100 решенных примеров, чтобы познакомиться с основными типами алгоритмических проблем. На первых порах "забывание знаний" может стать испытанием, но это нормально. Мы можем повторять задачи по "кривой забывания Эббингауза", и обычно после 3-5 циклов повторения материал прочно закрепляется. Рекомендуемые списки задач и планы практики см. в этом [репозитории GitHub](https://github.com/krahets/LeetCode-Book). -3. **Этап 3: построение системы знаний**. В учебной части можно читать статьи по алгоритмам, разбирать каркасы решений и учебники, чтобы постоянно обогащать свою систему знаний. В практической части можно пробовать более продвинутые стратегии, например классификацию по темам, несколько решений одной задачи или одно решение для нескольких задач; соответствующий опыт можно найти в разных сообществах. +1. **Этап 1: введение в алгоритмы**. Необходимо познакомиться с особенностями и применением различных структур данных, изучить принципы, процессы, назначение и эффективность различных алгоритмов. +2. **Этап 2: решение алгоритмических задач**. Рекомендуется начинать с популярных задач и решить не менее 100 из них, чтобы познакомиться с основными алгоритмическими проблемами. При первых попытках "забывание знаний" может стать испытанием, но это нормально. Следуйте при повторении задач "кривой забывания Эббингауза", и обычно после 3-5 циклов повторения материал хорошо запоминается. Рекомендуемые списки задач и планы практики см. в этом [репозитории GitHub](https://github.com/krahets/LeetCode-Book). +3. **Этап 3: построение системы знаний**. В процессе обучения можно читать статьи по алгоритмам, изучать каркасы решений и учебники, чтобы постоянно обогащать свою систему знаний. В решении задач можно применять продвинутые стратегии, например классификацию по темам, несколько решений одной задачи или одно решение для нескольких задач; соответствующий опыт можно найти в различных сообществах. -Как показано на рисунке 0-8, содержание этой книги в основном покрывает "этап 1" и призвано помочь тебе более эффективно перейти к обучению на этапах 2 и 3. +Как показано на рисунке 0-8, содержание этой книги в основном охватывает "этап 1" и призвано помочь вам более эффективно перейти к обучению на этапах 2 и 3. ![Дорожная карта изучения алгоритмов](suggestions.assets/learning_route.png){ class="animation-figure" } diff --git a/ru/docs/chapter_preface/summary.md b/ru/docs/chapter_preface/summary.md index 69b07983d..cf0fd3cd9 100644 --- a/ru/docs/chapter_preface/summary.md +++ b/ru/docs/chapter_preface/summary.md @@ -6,9 +6,9 @@ comments: true ### 1.   Ключевые выводы -- Основная аудитория этой книги - новички в изучении алгоритмов. Если у тебя уже есть определенная база, книга поможет системно повторить знания, а исходный код можно использовать как "инструментарий для решения задач". -- Основное содержание книги состоит из трех частей: анализ сложности, структуры данных и алгоритмы; вместе они охватывают большую часть тем этой области. -- Для начинающих особенно важно на старте прочитать хорошее вводное пособие: это помогает избежать множества лишних обходных путей. -- Анимированные иллюстрации в книге обычно используются для объяснения ключевых и сложных идей. При чтении книги этим материалам стоит уделять больше внимания. -- Практика - лучший способ изучать программирование. Настоятельно рекомендуется запускать исходный код и набирать его самостоятельно. -- В веб-версии книги у каждой главы есть раздел комментариев, где можно в любой момент делиться вопросами и своими мыслями. +- Основная аудитория этой книги - новички в изучении алгоритмов. Если у вас уже есть определенная база, книга поможет систематизировать знания, а исходный код послужит инструментальной библиотекой для решения задач. +- Содержание книги включает три основные части - анализ сложности, структуры данных и алгоритмы - и охватывает большинство тем в этой области. +- Для новичков в алгоритмах крайне важно изучить начальные разделы книги, чтобы избежать множества ошибок в будущем. +- Анимированные иллюстрации в книге обычно используются для представления ключевых и сложных аспектов. При чтении книги следует уделять этим материалам больше внимания. +- Практика - лучший способ изучения программирования. Настоятельно рекомендуется запускать исходный код и самостоятельно писать программы. +- В веб-версии книги каждая глава имеет область комментариев, где можно задавать вопросы и делиться своими мыслями. diff --git a/ru/docs/chapter_reference/index.md b/ru/docs/chapter_reference/index.md index 1ab64b41e..991cee48a 100644 --- a/ru/docs/chapter_reference/index.md +++ b/ru/docs/chapter_reference/index.md @@ -4,17 +4,17 @@ icon: material/bookshelf # Список литературы -[1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition). +[1] Thomas H. Cormen и др. Introduction to Algorithms (3rd Edition). [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition). -[3] Robert Sedgewick, et al. Algorithms (4th Edition). +[3] Robert Sedgewick и др. Algorithms (4th Edition). [4] Yan Weimin. Data Structures (C Language Edition). [5] Deng Junhui. Data Structures (C++ Language Edition, 3rd Edition). -[6] Mark Allen Weiss; translated by Chen Yue. Data Structures and Algorithm Analysis: Java Description (3rd Edition). +[6] Mark Allen Weiss; пер. Chen Yue. Data Structures and Algorithm Analysis: Java Description (3rd Edition). [7] Cheng Jie. A Plainspoken Guide to Data Structures. @@ -22,4 +22,4 @@ icon: material/bookshelf [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition). -[10] Aston Zhang, et al. Dive into Deep Learning. +[10] Aston Zhang и др. Dive into Deep Learning. diff --git a/ru/docs/chapter_searching/binary_search.md b/ru/docs/chapter_searching/binary_search.md index 0fa403275..8616c9493 100644 --- a/ru/docs/chapter_searching/binary_search.md +++ b/ru/docs/chapter_searching/binary_search.md @@ -4,7 +4,7 @@ comments: true # 10.1   Двоичный поиск -Двоичный поиск (binary search) - это эффективный алгоритм поиска, основанный на стратегии "разделяй и властвуй". Он использует упорядоченность данных и на каждом шаге сокращает область поиска вдвое, пока не найдет целевой элемент или пока интервал поиска не опустеет. +Двоичный поиск (binary search) - это эффективный алгоритм поиска, основанный на стратегии "разделяй и властвуй". Он использует упорядоченность данных, сокращая на каждом шаге область поиска вдвое, пока не будет найден целевой элемент или пока интервал поиска не опустеет. !!! question @@ -373,7 +373,7 @@ comments: true ## 10.1.1   Методы представления интервалов -Помимо описанного выше двойного замкнутого интервала, часто используется и интервал "слева закрыт, справа открыт", который задается как $[0, n)$ , то есть левая граница включается, а правая - нет. В этом представлении интервал $[i, j)$ пуст, когда $i = j$ . +Помимо описанного выше двойного замкнутого интервала, часто используется и левозамкнутый правооткрытый интервал, который задается как $[0, n)$ , то есть левая граница включается, а правая - нет. В этом представлении интервал $[i, j)$ пуст, когда $i = j$ . На основе этого представления можно реализовать двоичный поиск с той же функциональностью: diff --git a/ru/docs/chapter_searching/binary_search_edge.md b/ru/docs/chapter_searching/binary_search_edge.md index e7cd5c01a..a49bc1b21 100644 --- a/ru/docs/chapter_searching/binary_search_edge.md +++ b/ru/docs/chapter_searching/binary_search_edge.md @@ -10,7 +10,7 @@ comments: true Дан упорядоченный массив `nums` длины $n$, который может содержать повторяющиеся элементы. Верните индекс самого левого элемента `target` в массиве. Если массив не содержит этот элемент, верните $-1$ . -Вспомним метод поиска точки вставки при двоичном поиске: после завершения поиска указатель $i$ указывает на самый левый `target` , **поэтому поиск точки вставки по сути и есть поиск индекса самого левого `target`**. +Вспомним метод поиска точки вставки при двоичном поиске: после завершения поиска указатель $i$ указывает на самый левый `target` , **поэтому поиск точки вставки по сути является поиском индекса самого левого `target`**. Рассмотрим реализацию поиска левой границы через функцию поиска точки вставки. Обратите внимание: массив может не содержать `target` , и тогда возможны две ситуации. diff --git a/ru/docs/chapter_searching/binary_search_insertion.md b/ru/docs/chapter_searching/binary_search_insertion.md index c1e5fad85..361952380 100644 --- a/ru/docs/chapter_searching/binary_search_insertion.md +++ b/ru/docs/chapter_searching/binary_search_insertion.md @@ -2,7 +2,7 @@ comments: true --- -# 10.2   Точка вставки при двоичном поиске +# 10.2   Двоичный поиск точки вставки Двоичный поиск можно использовать не только для поиска целевого элемента, но и для решения многих вариаций задачи, например для поиска позиции вставки целевого элемента. @@ -24,7 +24,7 @@ comments: true **Вопрос 2**: если массив не содержит `target` , индекс какого элемента будет точкой вставки? -Рассмотрим процесс двоичного поиска подробнее: когда `nums[m] < target` , указатель $i$ сдвигается, а значит, приближается к элементу, который больше либо равен `target` . Аналогично указатель $j$ все время приближается к элементу, который меньше либо равен `target` . +Рассмотрим процесс двоичного поиска подробнее: когда `nums[m] < target` , указатель $i$ сдвигается вправо и тем самым приближается к элементу, который больше либо равен `target` . Аналогично указатель $j$ постепенно приближается к элементу, который меньше либо равен `target` . Следовательно, после завершения двоичного поиска обязательно выполняется следующее: указатель $i$ указывает на первый элемент, больший `target` , а указатель $j$ указывает на первый элемент, меньший `target` . **Нетрудно сделать вывод, что если массив не содержит `target` , то индекс вставки равен $i$** . Код приведен ниже: @@ -372,7 +372,7 @@ comments: true

Рисунок 10-6   Шаги поиска точки вставки для повторяющихся элементов

-Если посмотреть на следующий код, то видно, что операции в ветвях `nums[m] > target` и `nums[m] == target` совпадают, поэтому эти две ветви можно объединить. +Если посмотреть на следующий код, то видно, что действия в ветвях `nums[m] > target` и `nums[m] == target` совпадают, поэтому эти две ветви можно объединить. Даже в этом случае можно оставить условия развернутыми, потому что так логика выглядит более ясной и код легче читать. @@ -669,6 +669,6 @@ comments: true Код в этом разделе записан в стиле "двойного замкнутого интервала". При желании можно самостоятельно реализовать вариант "слева закрыт, справа открыт". -Если смотреть в целом, суть двоичного поиска сводится к тому, что для указателей $i$ и $j$ заранее задаются цели поиска; целью может быть конкретный элемент (например, `target` ), а может быть и диапазон элементов (например, элементы, меньшие `target` ). +Если смотреть в целом, суть двоичного поиска сводится к тому, что для указателей $i$ и $j$ заранее задаются ориентиры поиска; целью может быть конкретный элемент, например `target` , а может быть и диапазон элементов, например все элементы, меньшие `target` . В ходе непрерывного двоичного деления указатели $i$ и $j$ постепенно приближаются к заранее заданной цели. В конце они либо успешно находят ответ, либо останавливаются после выхода за границы. diff --git a/ru/docs/chapter_searching/index.md b/ru/docs/chapter_searching/index.md index ba7c04620..d86db4149 100644 --- a/ru/docs/chapter_searching/index.md +++ b/ru/docs/chapter_searching/index.md @@ -9,15 +9,15 @@ icon: material/text-search !!! abstract - Поиск - это приключение в неизвестность: иногда приходится пройти каждый уголок загадочного пространства, а иногда удается быстро зафиксировать цель. + Поиск - это движение в неизвестность: иногда приходится пройти каждый уголок пространства, а иногда удается быстро найти цель. - В этом путешествии каждый новый шаг может привести к ответу, которого мы не ожидали. + В этом пути каждый новый шаг может привести к ответу, которого мы не ожидали. ## Содержание главы - [10.1   Двоичный поиск](binary_search.md) -- [10.2   Точка вставки двоичного поиска](binary_search_insertion.md) -- [10.3   Граничные случаи двоичного поиска](binary_search_edge.md) -- [10.4   Стратегия оптимизации через хеширование](replace_linear_by_hashing.md) -- [10.5   Алгоритмы поиска: новый взгляд](searching_algorithm_revisited.md) +- [10.2   Двоичный поиск точки вставки](binary_search_insertion.md) +- [10.3   Двоичный поиск границ](binary_search_edge.md) +- [10.4   Стратегии оптимизации хеширования](replace_linear_by_hashing.md) +- [10.5   Переосмысление алгоритмов поиска](searching_algorithm_revisited.md) - [10.6   Резюме](summary.md) diff --git a/ru/docs/chapter_searching/replace_linear_by_hashing.md b/ru/docs/chapter_searching/replace_linear_by_hashing.md index a52ce9f7c..9b5ea4d66 100644 --- a/ru/docs/chapter_searching/replace_linear_by_hashing.md +++ b/ru/docs/chapter_searching/replace_linear_by_hashing.md @@ -2,7 +2,7 @@ comments: true --- -# 10.4   Стратегии хеш-оптимизации +# 10.4   Стратегии оптимизации хеширования В алгоритмических задачах **мы часто заменяем линейный поиск на хеш-поиск, чтобы уменьшить временную сложность алгоритма**. Разберем одну задачу, чтобы лучше понять этот прием. diff --git a/ru/docs/chapter_searching/searching_algorithm_revisited.md b/ru/docs/chapter_searching/searching_algorithm_revisited.md index 2509921a9..c5cbe19e5 100644 --- a/ru/docs/chapter_searching/searching_algorithm_revisited.md +++ b/ru/docs/chapter_searching/searching_algorithm_revisited.md @@ -11,14 +11,14 @@ comments: true - **Поиск целевого элемента путем обхода структуры данных**, например обход массива, списка, дерева или графа. - **Эффективный поиск элементов с использованием структуры организации данных или априорной информации**, например двоичный поиск, хеш-поиск и поиск в двоичном дереве поиска. -Нетрудно заметить, что эти темы уже рассматривались в предыдущих главах, поэтому алгоритмы поиска нам уже знакомы. В этом разделе мы еще раз посмотрим на них, но уже более системно. +Нетрудно заметить, что эти темы уже рассматривались в предыдущих главах, поэтому алгоритмы поиска нам уже знакомы. В этом разделе мы систематизируем полученные ранее знания и еще раз посмотрим на них как на единую группу методов. ## 10.5.1   Полный перебор Полный перебор заключается в том, что мы обходим каждый элемент структуры данных, чтобы найти целевой элемент. - "Линейный поиск" применяется к линейным структурам данных, таким как массивы и списки. Он начинается с одного конца структуры данных и последовательно проверяет элементы, пока не найдет целевой элемент или пока не достигнет другого конца структуры данных. -- "Поиск в ширину" и "поиск в глубину" - это две стратегии обхода графов и деревьев. Поиск в ширину стартует из начального узла и исследует все узлы текущего уровня, прежде чем переходить к следующему. Поиск в глубину стартует из начального узла, проходит один путь до конца, затем возвращается назад и пробует другие пути, пока не будет полностью пройдена вся структура данных. +- "Обход в ширину" и "обход в глубину" - это две стратегии обхода графов и деревьев. Обход в ширину стартует из начального узла и исследует все узлы текущего уровня, прежде чем переходить к следующему. Обход в глубину стартует из начального узла, проходит один путь до конца, затем возвращается назад и пробует другие пути, пока не будет полностью пройдена вся структура данных. Преимущество полного перебора состоит в его простоте и универсальности, **поскольку он не требует предварительной обработки данных и использования дополнительных структур данных**. @@ -32,13 +32,13 @@ comments: true - "Хеш-поиск" использует хеш-таблицу для построения отображения между поисковыми данными и целевыми данными, благодаря чему запросы выполняются эффективно. - "Поиск в дереве" ведется в конкретной древовидной структуре (например, в двоичном дереве поиска) и позволяет быстро отсекать узлы на основе сравнения значений, чтобы найти цель. -Преимущество этих алгоритмов в высокой эффективности, **их временная сложность может достигать $O(\log n)$ и даже $O(1)$** . +Преимущество этих алгоритмов заключается в высокой эффективности: **их временная сложность может достигать $O(\log n)$ и даже $O(1)$** . Однако **для использования таких алгоритмов обычно требуется предварительная обработка данных**. Например, для двоичного поиска нужно заранее отсортировать массив, а хеш-поиск и поиск в дереве требуют дополнительных структур данных, поддержание которых тоже отнимает время и память. !!! tip - Адаптивные алгоритмы поиска часто называют алгоритмами поиска в узком смысле, **поскольку они в основном предназначены для быстрого поиска целевого элемента в конкретной структуре данных**. + Адаптивные алгоритмы поиска часто называют алгоритмами поиска в узком смысле, **поскольку они в основном предназначены для быстрого нахождения целевого элемента в конкретной структуре данных**. ## 10.5.3   Выбор метода поиска diff --git a/ru/docs/chapter_searching/summary.md b/ru/docs/chapter_searching/summary.md index cc59644d0..9fb47d5e5 100644 --- a/ru/docs/chapter_searching/summary.md +++ b/ru/docs/chapter_searching/summary.md @@ -7,7 +7,7 @@ comments: true ### 1.   Ключевые выводы - Двоичный поиск опирается на упорядоченность данных и выполняет поиск путем циклического сокращения интервала вдвое. Он требует упорядоченных входных данных и подходит только для массивов или структур данных, реализованных на их основе. -- Полный перебор находит данные путем обхода структуры данных. Линейный поиск подходит для массивов и списков, а поиск в ширину и поиск в глубину подходят для графов и деревьев. Эти алгоритмы универсальны и не требуют предварительной обработки данных, но их временная сложность $O(n)$ сравнительно велика. +- Полный перебор находит данные путем обхода структуры данных. Линейный поиск подходит для массивов и списков, а обход в ширину и обход в глубину подходят для графов и деревьев. Эти алгоритмы универсальны и не требуют предварительной обработки данных, но их временная сложность $O(n)$ сравнительно велика. - Хеш-поиск, поиск в дереве и двоичный поиск относятся к эффективным методам поиска и позволяют быстро находить целевой элемент в конкретных структурах данных. Такие алгоритмы обладают высокой эффективностью, их временная сложность может достигать $O(\log n)$ и даже $O(1)$ , но обычно им нужны дополнительные структуры данных. - На практике нужно анализировать размер данных, требования к производительности поиска, а также частоту запросов и обновлений данных, чтобы выбрать подходящий метод поиска. - Линейный поиск подходит для небольших или часто обновляемых наборов данных; двоичный поиск - для больших отсортированных данных; хеш-поиск - для сценариев с высокими требованиями к скорости запросов и без необходимости поиска по диапазону; поиск в дереве - для больших динамических данных, где нужно поддерживать порядок и выполнять диапазонные запросы. diff --git a/ru/docs/chapter_sorting/bubble_sort.md b/ru/docs/chapter_sorting/bubble_sort.md index b594b129d..466a3c6b7 100644 --- a/ru/docs/chapter_sorting/bubble_sort.md +++ b/ru/docs/chapter_sorting/bubble_sort.md @@ -4,7 +4,7 @@ comments: true # 11.3   Сортировка пузырьком -Сортировка пузырьком (bubble sort) сортирует массив за счет непрерывного сравнения и обмена соседних элементов. Этот процесс напоминает всплытие пузырьков снизу вверх, откуда и произошло название алгоритма. +Сортировка пузырьком (bubble sort) реализует сортировку путем последовательного сравнения и обмена соседних элементов. Этот процесс напоминает всплытие пузырьков снизу вверх, откуда и произошло название алгоритма. Как показано на рисунке 11-4, процесс "всплытия" можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если "левый элемент > правый элемент", меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива. @@ -297,7 +297,7 @@ comments: true ## 11.3.2   Оптимизация эффективности -Мы замечаем, что если в каком-либо раунде "всплытия" не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг `flag` для отслеживания этой ситуации и немедленного выхода. +Если в каком-либо раунде "всплытия" не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг `flag` для отслеживания этой ситуации и немедленного выхода. После такой оптимизации худшая и средняя временные сложности сортировки пузырьком по-прежнему равны $O(n^2)$ ; однако если входной массив уже полностью упорядочен, достигается лучшая временная сложность $O(n)$ . @@ -574,7 +574,22 @@ comments: true === "Ruby" ```ruby title="bubble_sort.rb" - ### Пузырьковая сортировка (оптимизация флагом) ### + ### Пузырьковая сортировка ### + def bubble_sort(nums) + n = nums.length + # Внешний цикл: неотсортированный диапазон [0, i] + for i in (n - 1).downto(1) + # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец + for j in 0...i + if nums[j] > nums[j + 1] + # Поменять местами nums[j] и nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + end + end + end + end + + # ## Пузырьковая сортировка (оптимизация флагом) ### def bubble_sort_with_flag(nums) n = nums.length # Внешний цикл: неотсортированный диапазон [0, i] diff --git a/ru/docs/chapter_sorting/bucket_sort.md b/ru/docs/chapter_sorting/bucket_sort.md index b19b15615..63b3b7b89 100644 --- a/ru/docs/chapter_sorting/bucket_sort.md +++ b/ru/docs/chapter_sorting/bucket_sort.md @@ -6,7 +6,7 @@ comments: true Рассмотренные выше алгоритмы сортировки относятся к "сортировкам на основе сравнений": они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше $O(n \log n)$ . Далее мы рассмотрим несколько "сортировок без сравнений", чья временная сложность может достигать линейного порядка. -Блочная сортировка (bucket sort) является типичным применением стратегии "разделяй и властвуй". Она задает несколько упорядоченных по диапазонам блоков, каждый блок соответствует некоторому диапазону значений; затем данные равномерно распределяются по блокам, внутри каждого блока выполняется сортировка, а в конце результаты блоков объединяются по порядку. +Блочная сортировка (bucket sort) является типичным применением стратегии "разделяй и властвуй". Она создает набор упорядоченных по величине блоков, где каждый блок соответствует определенному диапазону данных; затем элементы равномерно распределяются по этим блокам, внутри каждого блока отдельно выполняется сортировка, а в конце результаты объединяются в порядке блоков. ## 11.8.1   Алгоритм @@ -452,7 +452,7 @@ comments: true ## 11.8.3   Как добиться равномерного распределения -Теоретически временная сложность блочной сортировки может достигать $O(n)$ ; **ключ к этому - как можно более равномерно распределить элементы по блокам**. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на Taobao по 10 блокам цен, количество товаров дешевле 100 юаней может быть очень большим, а товаров дороже 1000 юаней - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться. +Теоретически временная сложность блочной сортировки может достигать $O(n)$ ; **ключ к этому - как можно более равномерно распределить элементы по блокам**. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на маркетплейсе по 10 ценовым блокам, количество товаров дешевле 100 рублей может быть очень большим, а товаров дороже 1000 рублей - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться. Чтобы добиться более равномерного распределения, можно сначала задать грубую линию раздела и приблизительно распределить данные по 3 блокам. **После этого блоки с большим числом товаров можно снова делить на 3 блока и продолжать процесс до тех пор, пока число элементов в каждом блоке не станет примерно одинаковым**. @@ -462,7 +462,7 @@ comments: true

Рисунок 11-14   Рекурсивное разбиение по блокам

-Если нам заранее известна вероятностная модель распределения цен товаров, **то границы каждого блока можно задавать в соответствии с этим распределением**. Важно отметить, что фактическое распределение данных не обязательно специально измерять - его можно приблизить некоторой вероятностной моделью исходя из свойств данных. +Если нам заранее известна вероятностная модель распределения цен товаров, **то границы цен для каждого блока можно задавать в соответствии с этим распределением**. Важно отметить, что фактическое распределение данных не обязательно специально измерять - его можно приблизить некоторой вероятностной моделью исходя из свойств данных. Как показано на рисунке 11-15, если предположить, что цены товаров подчиняются нормальному распределению, то можно разумно задать интервалы цен и тем самым распределить товары по блокам достаточно равномерно. diff --git a/ru/docs/chapter_sorting/counting_sort.md b/ru/docs/chapter_sorting/counting_sort.md index 1b99321c0..ca5bfce07 100644 --- a/ru/docs/chapter_sorting/counting_sort.md +++ b/ru/docs/chapter_sorting/counting_sort.md @@ -4,7 +4,7 @@ comments: true # 11.9   Сортировка подсчетом -Сортировка подсчетом (counting sort) реализует сортировку за счет подсчета количества элементов и обычно используется для массивов целых чисел. +Сортировка подсчетом (counting sort) реализует сортировку за счет подсчета количества вхождений элементов и обычно используется для массивов целых чисел. ## 11.9.1   Простая реализация @@ -368,7 +368,7 @@ comments: true !!! note "Связь между сортировкой подсчетом и блочной сортировкой" - Если посмотреть на сортировку подсчетом с точки зрения блочной сортировки, то каждый индекс массива `counter` можно рассматривать как отдельный блок, а процесс подсчета - как распределение элементов по соответствующим блокам. По сути, сортировка подсчетом является частным случаем блочной сортировки для целочисленных данных. + Если посмотреть на сортировку подсчетом с точки зрения блочной сортировки, то каждый индекс массива `counter` можно рассматривать как отдельный блок, а процесс подсчета - как распределение элементов по соответствующим блокам. Иными словами, сортировка подсчетом является частным случаем блочной сортировки для целочисленных данных. ## 11.9.2   Полная реализация diff --git a/ru/docs/chapter_sorting/heap_sort.md b/ru/docs/chapter_sorting/heap_sort.md index 1f2d360a9..2303cc683 100644 --- a/ru/docs/chapter_sorting/heap_sort.md +++ b/ru/docs/chapter_sorting/heap_sort.md @@ -13,7 +13,7 @@ comments: true 1. Подать на вход массив и построить из него мин-кучу; в этот момент минимальный элемент будет находиться в вершине кучи. 2. Непрерывно выполнять извлечение из кучи и по порядку записывать извлеченные элементы - так получится последовательность, отсортированная по возрастанию. -Хотя этот метод работоспособен, он требует дополнительного массива для хранения извлеченных элементов и потому расходует лишнюю память. На практике обычно используют более изящную реализацию. +Хотя этот метод и работоспособен, он требует дополнительного массива для хранения извлеченных элементов и потому расходует лишнюю память. На практике обычно используют более изящную реализацию. ## 11.7.1   Алгоритм @@ -21,7 +21,7 @@ comments: true 1. Подать на вход массив и построить из него макс-кучу. После этого максимальный элемент окажется в вершине кучи. 2. Обменять элемент в вершине кучи (первый элемент) с элементом внизу кучи (последний элемент). После обмена длина кучи уменьшается на $1$ , а число уже отсортированных элементов увеличивается на $1$ . -3. Начиная с вершины, выполнить просеивание вниз (sift down) сверху вниз. После этого свойство кучи будет восстановлено. +3. Начиная с вершины, выполнить операцию просеивания сверху вниз. После этого свойство кучи будет восстановлено. 4. Циклически повторять шаг `2.` и шаг `3.` . После $n - 1$ раундов массив будет полностью отсортирован. !!! tip @@ -66,7 +66,7 @@ comments: true

Рисунок 11-12   Шаги пирамидальной сортировки

-В реализации кода используется та же функция просеивания сверху вниз `sift_down()`, что и в главе "Куча". Важно помнить, что длина кучи уменьшается по мере извлечения максимального элемента, поэтому функции `sift_down()` нужно передавать параметр длины $n$ , чтобы указать текущую эффективную длину кучи. Код приведен ниже: +В коде используется та же функция просеивания сверху вниз `sift_down()`, что и в главе "Куча". Важно помнить, что длина кучи уменьшается по мере извлечения максимального элемента, поэтому функции `sift_down()` нужно передавать параметр длины $n$ , чтобы указать текущую действительную длину кучи. Код приведен ниже: === "Python" diff --git a/ru/docs/chapter_sorting/index.md b/ru/docs/chapter_sorting/index.md index 6c66675d5..4752f36b3 100644 --- a/ru/docs/chapter_sorting/index.md +++ b/ru/docs/chapter_sorting/index.md @@ -17,8 +17,8 @@ icon: material/sort-ascending - [11.1   Алгоритмы сортировки](sorting_algorithm.md) - [11.2   Сортировка выбором](selection_sort.md) -- [11.3   Пузырьковая сортировка](bubble_sort.md) -- [11.4   Сортировка вставкой](insertion_sort.md) +- [11.3   Сортировка пузырьком](bubble_sort.md) +- [11.4   Сортировка вставками](insertion_sort.md) - [11.5   Быстрая сортировка](quick_sort.md) - [11.6   Сортировка слиянием](merge_sort.md) - [11.7   Пирамидальная сортировка](heap_sort.md) diff --git a/ru/docs/chapter_sorting/insertion_sort.md b/ru/docs/chapter_sorting/insertion_sort.md index 3978d06bc..1d20c73ec 100644 --- a/ru/docs/chapter_sorting/insertion_sort.md +++ b/ru/docs/chapter_sorting/insertion_sort.md @@ -4,11 +4,11 @@ comments: true # 11.4   Сортировка вставками -Сортировка вставками (insertion sort) - это простой алгоритм сортировки, принцип которого очень похож на ручное упорядочивание колоды карт. +Сортировка вставками (insertion sort) - это простой алгоритм сортировки, принцип которого очень похож на ручную сортировку карт в колоде. -Точнее говоря, в неотсортированном диапазоне выбирается опорный элемент, после чего он поочередно сравнивается с элементами слева в уже отсортированном диапазоне и вставляется в правильную позицию. +Точнее говоря, в неотсортированном диапазоне выбирается опорный элемент, после чего он сравнивается с элементами слева в уже отсортированном диапазоне и вставляется в правильную позицию. -На рисунке 11-6 показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как `base` ; нам нужно сдвинуть все элементы от целевого индекса до `base` на одну позицию вправо, а затем присвоить `base` значение в целевом индексе. +На рисунке 11-6 показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как `base` ; нам нужно сдвинуть все элементы от целевого индекса до `base` на одну позицию вправо, а затем записать `base` в целевой индекс. ![Одна операция вставки](insertion_sort.assets/insertion_operation.png){ class="animation-figure" } @@ -19,8 +19,8 @@ comments: true Общий процесс сортировки вставками показан на рисунке 11-7. 1. В начальном состоянии отсортирован только первый элемент массива. -2. Выбрать второй элемент массива как `base` ; после вставки в правильную позицию **первые 2 элемента массива окажутся отсортированными**. -3. Выбрать третий элемент как `base` ; после вставки в правильную позицию **первые 3 элемента массива окажутся отсортированными**. +2. Выбрать второй элемент массива как `base` ; после вставки в правильную позицию **первые два элемента массива окажутся отсортированными**. +3. Выбрать третий элемент как `base` ; после вставки в правильную позицию **первые три элемента массива окажутся отсортированными**. 4. Продолжать по аналогии; в последнем раунде в качестве `base` берется последний элемент, и после его вставки **все элементы массива будут отсортированы**. ![Процесс сортировки вставками](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" } diff --git a/ru/docs/chapter_sorting/merge_sort.md b/ru/docs/chapter_sorting/merge_sort.md index bf1011559..bdc2f68d6 100644 --- a/ru/docs/chapter_sorting/merge_sort.md +++ b/ru/docs/chapter_sorting/merge_sort.md @@ -4,10 +4,10 @@ comments: true # 11.6   Сортировка слиянием -Сортировка слиянием (merge sort) - это алгоритм сортировки на основе стратегии "разделяй и властвуй", который включает этапы "разделения" и "слияния", показанные на рисунке 11-10. +Сортировка слиянием (merge sort) - это алгоритм сортировки, основанный на стратегии "разделяй и властвуй", который включает этапы "разделения" и "слияния", показанные на рисунке 11-10. -1. **Этап разделения**: массив рекурсивно разбивается от середины, и задача сортировки длинного массива превращается в задачи сортировки более коротких массивов. -2. **Этап слияния**: когда длина подмассива становится равной 1, разделение завершается и начинается слияние; левые и правые короткие упорядоченные массивы непрерывно объединяются в более длинный упорядоченный массив, пока процесс не завершится. +1. **Этап разделения**: массив рекурсивно делится пополам, и задача сортировки длинного массива превращается в задачи сортировки более коротких массивов. +2. **Этап слияния**: когда длина подмассива становится равной 1, разделение завершается и начинается слияние; два коротких упорядоченных массива непрерывно объединяются в один более длинный упорядоченный массив, пока процесс не завершится. ![Этапы разделения и слияния в сортировке слиянием](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" } @@ -54,10 +54,10 @@ comments: true

Рисунок 11-11   Шаги сортировки слиянием

-Нетрудно заметить, что порядок рекурсии в сортировке слиянием совпадает с порядком рекурсии при постфиксном обходе бинарного дерева. +Нетрудно заметить, что порядок рекурсии в сортировке слиянием совпадает с порядком обхода в глубину двоичного дерева. -- **Постфиксный обход**: сначала рекурсивно обходится левое поддерево, затем правое поддерево, а в конце обрабатывается корневой узел. -- **Сортировка слиянием**: сначала рекурсивно обрабатывается левый подмассив, затем правый подмассив, а в конце выполняется слияние. +- **Обход в глубину**: сначала рекурсивно обходится левое поддерево, затем правое поддерево, а в конце обрабатывается корневой узел. +- **Сортировка слиянием**: сначала рекурсивно разделяется левый подмассив, затем правый подмассив, а в конце выполняется слияние. Реализация сортировки слиянием показана в коде ниже. Обратите внимание: в `nums` объединяемый интервал равен `[left, right]` , а соответствующий интервал в `tmp` равен `[0, right - left]` . @@ -697,4 +697,4 @@ comments: true - **Этап разделения**: работу по разбиению списка можно реализовать с помощью "итерации" вместо "рекурсии", тем самым устранив расход памяти на стек вызовов. - **Этап слияния**: в связном списке добавление и удаление узлов требует только изменения ссылок (указателей), поэтому при слиянии двух коротких упорядоченных списков в один длинный упорядоченный список не нужно создавать дополнительный список. -Детали реализации достаточно сложны; заинтересованные читатели могут изучить соответствующие материалы самостоятельно. +Детали реализации достаточно сложны; заинтересованные читатели могут обратиться к соответствующим материалам самостоятельно. diff --git a/ru/docs/chapter_sorting/quick_sort.md b/ru/docs/chapter_sorting/quick_sort.md index 4d32633e0..0bbadd439 100644 --- a/ru/docs/chapter_sorting/quick_sort.md +++ b/ru/docs/chapter_sorting/quick_sort.md @@ -43,9 +43,9 @@ comments: true После завершения разделения исходный массив разбивается на три части: левый подмассив, опорный элемент и правый подмассив; при этом выполняется условие "любой элемент левого подмассива $\leq$ опорный элемент $\leq$ любой элемент правого подмассива". Следовательно, далее нам нужно лишь отсортировать эти два подмассива. -!!! note "Стратегия divide and conquer в быстрой сортировке" +!!! note "Стратегия разделяй и властвуй в быстрой сортировке" - По сути, разделение с опорным элементом сводит задачу сортировки длинного массива к двум задачам сортировки более коротких массивов. + Иными словами, разделение с опорным элементом сводит задачу сортировки длинного массива к двум задачам сортировки более коротких массивов. === "Python" @@ -614,11 +614,11 @@ comments: true ## 11.5.4   Оптимизация выбора опорного элемента -**На некоторых входных данных временная эффективность быстрой сортировки может ухудшаться**. Рассмотрим крайний случай: входной массив полностью отсортирован в обратном порядке. Поскольку в качестве опорного мы выбираем самый левый элемент, после разделения он будет обменян в самый правый конец массива, из-за чего длина левого подмассива станет $n - 1$ , а длина правого - $0$ . Если рекурсия будет продолжаться таким образом, то после каждого разделения один из подмассивов будет иметь длину $0$ , стратегия divide and conquer потеряет смысл, а быстрая сортировка выродится в нечто близкое к "сортировке пузырьком". +**На некоторых входных данных временная эффективность быстрой сортировки может ухудшаться**. Рассмотрим крайний случай: входной массив полностью отсортирован в обратном порядке. Поскольку в качестве опорного мы выбираем самый левый элемент, после разделения он будет обменян в самый правый конец массива, из-за чего длина левого подмассива станет $n - 1$ , а длина правого - $0$ . Если рекурсия будет продолжаться таким образом, то после каждого разделения один из подмассивов будет иметь длину $0$ , стратегия "разделяй и властвуй" потеряет смысл, а быстрая сортировка выродится в нечто близкое к "сортировке пузырьком". -Чтобы по возможности избежать такого сценария, **мы можем улучшить стратегию выбора опорного элемента в процедуре разделения**. Например, можно выбирать случайный элемент массива как опорный. Однако если не повезет и каждый раз будет выбираться неудачный опорный элемент, производительность все равно останется неудовлетворительной. +Чтобы по возможности избежать такого сценария, **можно улучшить стратегию выбора опорного элемента в процедуре разделения**. Например, можно выбирать случайный элемент массива как опорный. Однако если не повезет и каждый раз будет выбираться неудачный опорный элемент, производительность все равно останется неудовлетворительной. -Нужно учитывать, что языки программирования обычно генерируют "псевдослучайные числа". Если специально построить тестовый пример под такую последовательность, эффективность быстрой сортировки все равно может деградировать. +Стоит учитывать, что языки программирования обычно генерируют псевдослучайные числа. Если специально построить тестовый пример под такую последовательность, эффективность быстрой сортировки все равно может деградировать. Чтобы улучшить ситуацию, можно взять три кандидата (обычно первый, последний и средний элементы массива) и **использовать медиану этих трех значений как опорный элемент**. Благодаря этому вероятность того, что опорный элемент окажется "не слишком маленьким и не слишком большим", заметно возрастает. Конечно, можно брать и большее число кандидатов, чтобы еще сильнее повысить устойчивость алгоритма. После этого вероятность деградации временной сложности до $O(n^2)$ существенно уменьшается. @@ -1059,9 +1059,20 @@ comments: true return right end - ### Разбиение с опорными указателями (медиана трех) ### + ### Выбрать медиану из трех кандидатов ### + def median_three(nums, left, mid, right) + # Выбрать медиану из трех кандидатов + _l, _m, _r = nums[left], nums[mid], nums[right] + # m находится между l и r + return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l) + # l находится между m и r + return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m) + return right + end + + # ## Разбиение с опорными указателями (медиана трех) ### def partition(nums, left, right) - ### Использовать nums[left] как опорный элемент + # ## Использовать nums[left] как опорный элемент med = median_three(nums, left, (left + right) / 2, right) # Переместить медиану в крайний левый элемент массива nums[left], nums[med] = nums[med], nums[left] @@ -1353,7 +1364,27 @@ comments: true === "Ruby" ```ruby title="quick_sort.rb" - ### Быстрая сортировка (оптимизация глубины рекурсии) ### + ### Разбиение с опорными указателями ### + def partition(nums, left, right) + # Использовать nums[left] как опорный элемент + i = left + j = right + while i < j + while i < j && nums[j] >= nums[left] + j -= 1 # Идти справа налево в поисках первого элемента меньше опорного + end + while i < j && nums[i] <= nums[left] + i += 1 # Идти слева направо в поисках первого элемента больше опорного + end + # Обмен элементов + nums[i], nums[j] = nums[j], nums[i] + end + # Переместить опорный элемент на границу двух подмассивов + nums[i], nums[left] = nums[left], nums[i] + i # Вернуть индекс опорного элемента + end + + # ## Быстрая сортировка (оптимизация глубины рекурсии) ### def quick_sort(nums, left, right) # Рекурсивно обрабатывать, пока длина подмассива не станет равной 1 while left < right diff --git a/ru/docs/chapter_sorting/radix_sort.md b/ru/docs/chapter_sorting/radix_sort.md index f7ad78691..0ffcc3fd8 100644 --- a/ru/docs/chapter_sorting/radix_sort.md +++ b/ru/docs/chapter_sorting/radix_sort.md @@ -4,9 +4,9 @@ comments: true # 11.10   Поразрядная сортировка -В предыдущем разделе мы познакомились с сортировкой подсчетом: она подходит для случаев, когда объем данных $n$ велик, а диапазон значений $m$ сравнительно мал. Предположим теперь, что нужно отсортировать $n = 10^6$ студенческих идентификаторов, причем каждый идентификатор является $8$-значным числом. Тогда диапазон данных $m = 10^8$ оказывается очень большим; сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать. +В предыдущем разделе была рассмотрена сортировка подсчетом: она хорошо подходит для случаев, когда объем данных $n$ велик, а диапазон значений $m$ сравнительно мал. Предположим теперь, что нужно отсортировать $n = 10^6$ номеров студентов, причем каждый номер представляет собой $8$-значное число. Тогда диапазон данных $m = 10^8$ оказывается очень большим; сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать. -Поразрядная сортировка (radix sort) по своей основной идее совпадает с сортировкой подсчетом и тоже реализует сортировку через подсчет количества. Поверх этого поразрядная сортировка использует иерархию разрядов числа и последовательно сортирует данные по каждому разряду, получая итоговый упорядоченный результат. +Поразрядная сортировка (radix sort) по своей основной идее совпадает с сортировкой подсчетом и тоже реализует сортировку через подсчет количества. При этом поразрядная сортировка использует соотношение между разрядами числа и последовательно сортирует данные по каждому разряду, получая итоговый упорядоченный результат. ## 11.10.1   Алгоритм @@ -26,7 +26,7 @@ $$ x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d $$ -где $\lfloor a \rfloor$ обозначает округление числа $a$ вниз, а $\bmod \: d$ означает взятие остатка по модулю $d$ . Для студенческих идентификаторов выполняется $d = 10$ и $k \in [1, 8]$ . +где $\lfloor a \rfloor$ обозначает округление числа $a$ вниз, а $\bmod \: d$ означает взятие остатка по модулю $d$ . Для студенческих номеров выполняется $d = 10$ и $k \in [1, 8]$ . Кроме того, нам нужно слегка изменить код сортировки подсчетом, чтобы он мог сортировать числа по их $k$-му разряду: @@ -675,7 +675,13 @@ $$ (num / exp) % 10 end - ### Сортировка подсчетом (сортировка по k-му разряду nums) ### + ### Получить k-й разряд элемента num, где exp = 10^(k-1) ### + def digit(num, exp) + # Передача exp вместо k позволяет избежать повторного выполнения дорогостоящих вычислений степени + (num / exp) % 10 + end + + # ## Сортировка подсчетом (сортировка по k-му разряду nums) ### def counting_sort_digit(nums, exp) # Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10 counter = Array.new(10, 0) diff --git a/ru/docs/chapter_sorting/selection_sort.md b/ru/docs/chapter_sorting/selection_sort.md index 1414656b7..8a92e4906 100644 --- a/ru/docs/chapter_sorting/selection_sort.md +++ b/ru/docs/chapter_sorting/selection_sort.md @@ -9,8 +9,8 @@ comments: true Пусть длина массива равна $n$ ; тогда процесс сортировки выбором выглядит так, как показано на рисунке 11-2. 1. В начальном состоянии все элементы не отсортированы, то есть неотсортированный диапазон индексов равен $[0, n-1]$ . -2. Выбрать минимальный элемент из диапазона $[0, n-1]$ и поменять его местами с элементом в позиции $0$ . После этого первые 1 элементов массива отсортированы. -3. Выбрать минимальный элемент из диапазона $[1, n-1]$ и поменять его местами с элементом в позиции $1$ . После этого первые 2 элементов массива отсортированы. +2. Выбрать минимальный элемент из диапазона $[0, n-1]$ и поменять его местами с элементом в позиции $0$ . После этого первый элемент массива отсортирован. +3. Выбрать минимальный элемент из диапазона $[1, n-1]$ и поменять его местами с элементом в позиции $1$ . После этого первые два элемента массива отсортированы. 4. Продолжать по аналогии. После $n - 1$ раундов выбора и обмена первые $n - 1$ элементов массива будут отсортированы. 5. Оставшийся элемент обязательно является максимальным, сортировать его не нужно, поэтому массив считается отсортированным. diff --git a/ru/docs/chapter_sorting/sorting_algorithm.md b/ru/docs/chapter_sorting/sorting_algorithm.md index f414c415b..9084598c2 100644 --- a/ru/docs/chapter_sorting/sorting_algorithm.md +++ b/ru/docs/chapter_sorting/sorting_algorithm.md @@ -4,7 +4,7 @@ comments: true # 11.1   Алгоритмы сортировки -Алгоритмы сортировки (sorting algorithm) используются для упорядочивания набора данных по определенному правилу. Они применяются очень широко, потому что упорядоченные данные обычно проще и быстрее искать, анализировать и обрабатывать. +Алгоритмы сортировки (sorting algorithm) используются для упорядочивания набора данных по определенному правилу. Они применяются очень широко, потому что упорядоченные данные обычно проще анализировать, обрабатывать и искать в них нужные элементы. Как показано на рисунке 11-1, данными в алгоритмах сортировки могут быть целые числа, числа с плавающей запятой, символы, строки и другие типы. Критерий сравнения тоже можно задать по-разному, например по величине чисел, по порядку ASCII-кодов символов или по пользовательскому правилу. diff --git a/ru/docs/chapter_sorting/summary.md b/ru/docs/chapter_sorting/summary.md index 277f33dba..e9ecee5f0 100644 --- a/ru/docs/chapter_sorting/summary.md +++ b/ru/docs/chapter_sorting/summary.md @@ -13,7 +13,7 @@ comments: true - Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию "разделяй и властвуй" и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных. - Сортировка подсчетом является частным случаем блочной сортировки; она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа. - Поразрядная сортировка выполняет сортировку данных путем последовательной сортировки по каждому разряду и требует, чтобы данные можно было представить в виде чисел фиксированной разрядности. -- В общем случае нам хотелось бы найти алгоритм сортировки, который одновременно обладал бы высокой эффективностью, стабильностью, свойством выполнения на месте и адаптивностью. Но, как и в других разделах алгоритмов и структур данных, не существует одного алгоритма сортировки, способного удовлетворить всем этим требованиям одновременно. На практике приходится выбирать подходящий алгоритм в зависимости от свойств данных. +- В общем случае нам хотелось бы найти алгоритм сортировки, который одновременно обладал бы высокой эффективностью, стабильностью, выполнением на месте и адаптивностью. Но, как и в других разделах алгоритмов и структур данных, не существует одного алгоритма сортировки, способного удовлетворить всем этим требованиям одновременно. На практике приходится выбирать подходящий алгоритм в зависимости от свойств данных. - На рисунке 11-19 сравниваются эффективность, стабильность, выполнение на месте и адаптивность основных алгоритмов сортировки. ![Сравнение алгоритмов сортировки](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" } diff --git a/ru/docs/chapter_stack_and_queue/deque.md b/ru/docs/chapter_stack_and_queue/deque.md index 8ae262cfe..c5d178d15 100644 --- a/ru/docs/chapter_stack_and_queue/deque.md +++ b/ru/docs/chapter_stack_and_queue/deque.md @@ -4,7 +4,7 @@ comments: true # 5.3   Двусторонняя очередь -В очереди мы можем удалять элементы только из головы или добавлять их только в хвост. Как показано на рисунке 5-7, двусторонняя очередь (double-ended queue) обеспечивает более высокую гибкость и позволяет выполнять добавление и удаление элементов как с головы, так и с хвоста. +В обычной очереди мы можем удалять элементы только из головы и добавлять их только в хвост. Как показано на рисунке 5-7, двусторонняя очередь (double-ended queue) обеспечивает большую гибкость и позволяет выполнять добавление и удаление элементов как с головы, так и с хвоста. ![Операции двусторонней очереди](deque.assets/deque_operations.png){ class="animation-figure" } @@ -403,25 +403,25 @@ comments: true ### 1.   Реализация на основе двусвязного списка -Вспомним предыдущий раздел: там мы использовали обычный односвязный список для реализации очереди, потому что он позволяет удобно удалять головной узел (это соответствует операции dequeue) и добавлять новый узел после хвостового узла (это соответствует операции enqueue). +Вспомним предыдущий раздел: там мы использовали обычный односвязный список для реализации очереди, потому что он позволяет удобно удалять головной узел, что соответствует операции `dequeue` , и добавлять новый узел после хвостового узла, что соответствует операции `enqueue` . -Для двусторонней очереди и голова, и хвост допускают операции добавления и удаления элементов. Иначе говоря, двусторонняя очередь требует реализации еще одного симметричного направления операций. Поэтому в качестве базовой структуры данных двусторонней очереди мы используем "двусвязный список". +Для двусторонней очереди и голова, и хвост допускают операции добавления и удаления элементов. Иначе говоря, двусторонняя очередь требует реализации еще одного симметричного направления операций. Поэтому в качестве базовой структуры данных двусторонней очереди удобно использовать двусвязный список. Как показано на рисунках ниже, мы рассматриваем головной и хвостовой узлы двусвязного списка как голову и хвост двусторонней очереди и одновременно реализуем функции добавления и удаления узлов с обеих сторон. -=== "LinkedListDeque" +=== "<1>" ![Операции enqueue и dequeue для двусторонней очереди на связном списке](deque.assets/linkedlist_deque_step1.png){ class="animation-figure" } -=== "push_last()" +=== "<2>" ![linkedlist_deque_push_last](deque.assets/linkedlist_deque_step2_push_last.png){ class="animation-figure" } -=== "push_first()" +=== "<3>" ![linkedlist_deque_push_first](deque.assets/linkedlist_deque_step3_push_first.png){ class="animation-figure" } -=== "pop_last()" +=== "<4>" ![linkedlist_deque_pop_last](deque.assets/linkedlist_deque_step4_pop_last.png){ class="animation-figure" } -=== "pop_first()" +=== "<5>" ![linkedlist_deque_pop_first](deque.assets/linkedlist_deque_step5_pop_first.png){ class="animation-figure" }

Рисунок 5-8   Операции enqueue и dequeue для двусторонней очереди на связном списке

@@ -2025,7 +2025,7 @@ comments: true Author: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com) =end - ### Узел двусвязного списка + # ## Узел двусвязного списка class ListNode attr_accessor :val attr_accessor :next # Ссылка на узел-преемник @@ -2155,26 +2155,26 @@ comments: true ### 2.   Реализация на основе массива -Как показано на рисунках ниже, аналогично реализации очереди на массиве мы также можем использовать кольцевой массив для реализации двусторонней очереди. +Как показано на рисунках ниже, аналогично реализации обычной очереди на массиве мы также можем использовать кольцевой массив для реализации двусторонней очереди. -=== "ArrayDeque" +=== "<1>" ![Операции enqueue и dequeue для двусторонней очереди на массиве](deque.assets/array_deque_step1.png){ class="animation-figure" } -=== "push_last()" +=== "<2>" ![array_deque_push_last](deque.assets/array_deque_step2_push_last.png){ class="animation-figure" } -=== "push_first()" +=== "<3>" ![array_deque_push_first](deque.assets/array_deque_step3_push_first.png){ class="animation-figure" } -=== "pop_last()" +=== "<4>" ![array_deque_pop_last](deque.assets/array_deque_step4_pop_last.png){ class="animation-figure" } -=== "pop_first()" +=== "<5>" ![array_deque_pop_first](deque.assets/array_deque_step5_pop_first.png){ class="animation-figure" }

Рисунок 5-9   Операции enqueue и dequeue для двусторонней очереди на массиве

-На основе реализации обычной очереди нужно лишь добавить методы "enqueue в голову" и "dequeue из хвоста": +На основе реализации обычной очереди нужно лишь добавить методы добавления в голову очереди и удаления из хвоста: === "Python" @@ -3605,4 +3605,4 @@ comments: true Двусторонняя очередь сочетает в себе логику стека и очереди, **поэтому она может покрыть все сценарии применения обеих структур и при этом предоставляет более высокую степень свободы**. -Мы знаем, что функция "undo" в программном обеспечении обычно реализуется с помощью стека: система `push`-ит каждое изменение в стек, а затем использует `pop` для отмены. Однако, учитывая ограниченность системных ресурсов, программы обычно ограничивают число шагов отмены (например, разрешают хранить только $50$ шагов). Когда длина стека превышает $50$, программе нужно удалить элемент с дна стека (то есть с головы очереди). **Но стек не может реализовать такую операцию, и в этом случае его приходится заменять двусторонней очередью**. Обрати внимание: основная логика "undo" по-прежнему следует стековому правилу LIFO, просто двусторонняя очередь позволяет более гибко реализовать некоторые дополнительные механизмы. +Мы знаем, что функция "undo" в программном обеспечении обычно реализуется с помощью стека: система помещает каждое изменение в стек с помощью `push` , а затем использует `pop` для отмены. Однако, учитывая ограниченность системных ресурсов, программы обычно ограничивают число шагов отмены, например разрешают хранить только $50$ шагов. Когда длина стека превышает этот предел, программе нужно удалить элемент с дна стека, то есть с головы очереди. **Но стек не может реализовать такую операцию, и в этом случае его приходится заменять двусторонней очередью**. Обрати внимание: основная логика "undo" по-прежнему следует стековому правилу LIFO, просто двусторонняя очередь позволяет более гибко реализовать некоторые дополнительные механизмы. diff --git a/ru/docs/chapter_stack_and_queue/index.md b/ru/docs/chapter_stack_and_queue/index.md index 0f2812da1..ce0d62140 100644 --- a/ru/docs/chapter_stack_and_queue/index.md +++ b/ru/docs/chapter_stack_and_queue/index.md @@ -9,9 +9,9 @@ icon: material/stack-overflow !!! abstract - Стек похож на стопку кошек, а очередь - на очередь из кошек. + Стек и очередь - две базовые линейные структуры данных. - Эти две структуры соответственно представляют отношения "последним пришел - первым вышел" и "первым пришел - первым вышел". + Они соответственно воплощают принципы "последним пришел - первым вышел" и "первым пришел - первым вышел". ## Содержание главы diff --git a/ru/docs/chapter_stack_and_queue/queue.md b/ru/docs/chapter_stack_and_queue/queue.md index 257529838..92245b7bb 100644 --- a/ru/docs/chapter_stack_and_queue/queue.md +++ b/ru/docs/chapter_stack_and_queue/queue.md @@ -6,7 +6,7 @@ comments: true Очередь (queue) - это линейная структура данных, подчиняющаяся правилу "первым пришел - первым вышел". Как видно из названия, очередь моделирует обычную ситуацию ожидания: новые люди непрерывно присоединяются к хвосту очереди, а стоящие в начале по одному уходят. -Как показано на рисунке 5-4, начало очереди называется "головой очереди", а конец - "хвостом очереди"; операцию добавления элемента в хвост называют "enqueue", а операцию удаления элемента из головы - "dequeue". +Как показано на рисунке 5-4, начало очереди называется головой очереди, а конец - хвостом очереди; операцию добавления элемента в хвост называют `enqueue`, а операцию удаления элемента из головы - `dequeue`. ![Правило FIFO для очереди](queue.assets/queue_operations.png){ class="animation-figure" } @@ -28,7 +28,7 @@ comments: true -Мы можем напрямую использовать готовые классы очереди, предоставляемые языками программирования: +Обычно достаточно использовать готовые классы очереди, предоставляемые языками программирования: === "Python" @@ -376,15 +376,15 @@ comments: true ### 1.   Реализация на основе связного списка -Как показано на рисунке 5-5, мы можем рассматривать "головной узел" и "хвостовой узел" связного списка как "голову очереди" и "хвост очереди" соответственно, договорившись, что добавлять узлы можно только в хвост, а удалять - только из головы. +Как показано на рисунке 5-5, мы можем рассматривать головной узел и хвостовой узел связного списка как голову очереди и хвост очереди соответственно, договорившись, что добавлять узлы можно только в хвост, а удалять - только из головы. -=== "LinkedListQueue" +=== "<1>" ![Операции enqueue и dequeue в реализации очереди на связном списке](queue.assets/linkedlist_queue_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![linkedlist_queue_push](queue.assets/linkedlist_queue_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![linkedlist_queue_pop](queue.assets/linkedlist_queue_step3_pop.png){ class="animation-figure" }

Рисунок 5-5   Операции enqueue и dequeue в реализации очереди на связном списке

@@ -1317,29 +1317,29 @@ comments: true ### 2.   Реализация на основе массива -Удаление первого элемента из массива имеет временную сложность $O(n)$ , из-за чего операция dequeue оказывается неэффективной. Однако этого можно избежать с помощью следующего приема. +Удаление первого элемента из массива имеет временную сложность $O(n)$ , из-за чего операция `dequeue` оказывается неэффективной. Однако этого можно избежать с помощью следующего приема. Мы можем использовать переменную `front` , указывающую на индекс элемента в голове очереди, и поддерживать переменную `size` , которая хранит длину очереди. Определим `rear = front + size` ; эта формула дает позицию `rear`, указывающую на ячейку сразу после хвоста очереди. Исходя из этого, **эффективный диапазон элементов массива равен `[front, rear - 1]`**, а различные операции реализуются, как показано на рисунке 5-6. -- Операция enqueue: записать входной элемент по индексу `rear` и увеличить `size` на 1. -- Операция dequeue: просто увеличить `front` на 1 и уменьшить `size` на 1. +- Операция `enqueue`: записать входной элемент по индексу `rear` и увеличить `size` на 1. +- Операция `dequeue`: просто увеличить `front` на 1 и уменьшить `size` на 1. -Можно увидеть, что и enqueue, и dequeue требуют всего одной операции, а значит обе имеют временную сложность $O(1)$ . +Можно увидеть, что и `enqueue` , и `dequeue` требуют всего одной операции, а значит обе имеют временную сложность $O(1)$ . -=== "ArrayQueue" +=== "<1>" ![Операции enqueue и dequeue в реализации очереди на массиве](queue.assets/array_queue_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![array_queue_push](queue.assets/array_queue_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![array_queue_pop](queue.assets/array_queue_step3_pop.png){ class="animation-figure" }

Рисунок 5-6   Операции enqueue и dequeue в реализации очереди на массиве

-Ты можешь заметить еще одну проблему: при непрерывных операциях enqueue и dequeue значения `front` и `rear` оба движутся вправо, и **когда они доходят до конца массива, дальше сдвигаться уже нельзя**. Чтобы решить эту проблему, можно рассматривать массив как "кольцевой массив", у которого начало и конец соединены. +Ты можешь заметить еще одну проблему: при непрерывных операциях `enqueue` и `dequeue` значения `front` и `rear` оба движутся вправо, и **когда они доходят до конца массива, дальше сдвигаться уже нельзя**. Чтобы решить эту проблему, можно рассматривать массив как кольцевой массив, у которого начало и конец соединены. Для кольцевого массива нужно сделать так, чтобы `front` или `rear`, перешагнув конец массива, сразу возвращались к его началу и продолжали движение. Такую периодичность удобно реализовать с помощью операции взятия остатка, как показано в коде ниже: @@ -2071,7 +2071,7 @@ comments: true typedef struct { int *nums; // Массив для хранения элементов очереди int front; // Указатель head, указывающий на первый элемент очереди - int queSize; // Указатель хвоста, указывающий на позицию после хвоста + int queSize; // Текущее количество элементов в очереди int queCapacity; // Вместимость очереди } ArrayQueue; @@ -2296,5 +2296,5 @@ comments: true ## 5.2.3   Типичные применения очереди -- **Заказы на Taobao**. После оформления заказа покупателем заказ попадает в очередь, а затем система обрабатывает заказы по порядку. Во время крупных распродаж, таких как Double 11, за короткое время возникает огромный поток заказов, и высокая конкурентная нагрузка становится ключевой инженерной проблемой. +- **Очереди заказов**. После оформления заказа покупателем заказ попадает в очередь, а затем система обрабатывает заказы по порядку. Во время крупных распродаж за короткое время возникает огромный поток заказов, и высокая конкурентная нагрузка становится ключевой инженерной проблемой. - **Различные отложенные задачи**. Любой сценарий, где нужно реализовать принцип "кто раньше пришел, тот раньше обслуживается", например очередь заданий принтера или очередь блюд на кухне ресторана, хорошо моделируется очередью, которая эффективно поддерживает нужный порядок обработки. diff --git a/ru/docs/chapter_stack_and_queue/stack.md b/ru/docs/chapter_stack_and_queue/stack.md index 038581520..fe8bf74c8 100644 --- a/ru/docs/chapter_stack_and_queue/stack.md +++ b/ru/docs/chapter_stack_and_queue/stack.md @@ -6,9 +6,9 @@ comments: true Стек (stack) - это линейная структура данных, подчиняющаяся логике "последним пришел - первым вышел". -Стек можно сравнить со стопкой тарелок на столе. Если разрешено перемещать только одну тарелку за раз, то, чтобы достать тарелку снизу, сначала придется по одной убрать все тарелки сверху. Если заменить тарелки различными элементами (например целыми числами, символами, объектами и т.д.), получится структура данных "стек". +Стек можно сравнить со стопкой тарелок на столе. Если разрешено перемещать только одну тарелку за раз, то, чтобы достать тарелку снизу, сначала придется по одной убрать все тарелки сверху. Если заменить тарелки различными элементами, например целыми числами, символами, объектами и т.д., получится структура данных "стек". -Как показано на рисунке 5-1, верхнюю часть стопки элементов мы называем "вершиной стека", а нижнюю - "основанием стека". Операция добавления элемента на вершину называется "push", а операция удаления верхнего элемента - "pop". +Как показано на рисунке 5-1, верхнюю часть стопки элементов мы называем вершиной стека, а нижнюю - основанием стека. Операция добавления элемента на вершину называется `push`, а операция удаления верхнего элемента - `pop`. ![Правило LIFO для стека](stack.assets/stack_operations.png){ class="animation-figure" } @@ -30,7 +30,7 @@ comments: true -Обычно мы можем просто использовать встроенный стек, предоставляемый языком программирования. Однако в некоторых языках специальный класс стека может отсутствовать. В таком случае можно использовать "массив" или "связный список" этого языка как стек и в логике программы игнорировать операции, не относящиеся к стеку. +Обычно достаточно использовать встроенный стек, предоставляемый языком программирования. Однако в некоторых языках специальный класс стека может отсутствовать. В таком случае можно использовать массив или связный список как стек и в логике программы игнорировать операции, не относящиеся к стеку. === "Python" @@ -367,21 +367,21 @@ comments: true Чтобы глубже понять механизм работы стека, попробуем самостоятельно реализовать класс стека. -Стек подчиняется принципу LIFO, поэтому мы можем добавлять и удалять элементы только на вершине. Однако и массив, и связный список позволяют добавлять и удалять элементы в произвольном месте. **Следовательно, стек можно рассматривать как ограниченный массив или связный список**. Иными словами, мы можем "скрыть" часть нерелевантных операций массива или списка, так чтобы внешняя логика соответствовала свойствам стека. +Стек подчиняется принципу LIFO, поэтому мы можем добавлять и удалять элементы только на вершине. Однако и массив, и связный список позволяют добавлять и удалять элементы в произвольном месте. **Следовательно, стек можно рассматривать как ограниченный массив или связный список**. Иными словами, мы можем скрыть часть нерелевантных операций массива или списка, так чтобы внешняя логика соответствовала свойствам стека. ### 1.   Реализация на основе связного списка Если реализовывать стек на основе связного списка, то головной узел списка можно рассматривать как вершину стека, а хвостовой - как основание. -Как показано на рисунке 5-2, для операции push достаточно вставить элемент в голову связного списка. Такой способ вставки называется "вставкой в голову". Для операции pop достаточно удалить головной узел из списка. +Как показано на рисунке 5-2, для операции `push` достаточно вставить элемент в голову связного списка. Такой способ вставки называется вставкой в голову. Для операции `pop` достаточно удалить головной узел из списка. -=== "LinkedListStack" +=== "<1>" ![Операции push и pop в реализации стека на связном списке](stack.assets/linkedlist_stack_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![linkedlist_stack_push](stack.assets/linkedlist_stack_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![linkedlist_stack_pop](stack.assets/linkedlist_stack_step3_pop.png){ class="animation-figure" }

Рисунок 5-2   Операции push и pop в реализации стека на связном списке

@@ -1166,15 +1166,15 @@ comments: true ### 2.   Реализация на основе массива -Если реализовывать стек на основе массива, то хвост массива можно рассматривать как вершину стека. Как показано на рисунке 5-3, операции push и pop соответствуют добавлению элемента в конец массива и удалению элемента из конца, обе имеют временную сложность $O(1)$ . +Если реализовывать стек на основе массива, то хвост массива можно рассматривать как вершину стека. Как показано на рисунке 5-3, операции `push` и `pop` соответствуют добавлению элемента в конец массива и удалению элемента из конца, обе имеют временную сложность $O(1)$ . -=== "ArrayStack" +=== "<1>" ![Операции push и pop в реализации стека на массиве](stack.assets/array_stack_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![array_stack_push](stack.assets/array_stack_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![array_stack_pop](stack.assets/array_stack_step3_pop.png){ class="animation-figure" }

Рисунок 5-3   Операции push и pop в реализации стека на массиве

@@ -1814,9 +1814,9 @@ comments: true **Временная эффективность** -В реализации на массиве и push, и pop выполняются в заранее выделенной непрерывной памяти, которая хорошо использует локальность кэша, поэтому такие операции обычно эффективнее. Однако если при push емкость массива оказывается превышена, включается механизм расширения, и временная сложность конкретно этой операции push становится $O(n)$ . +В реализации на массиве и `push` , и `pop` выполняются в заранее выделенной непрерывной памяти, которая хорошо использует локальность кэша, поэтому такие операции обычно эффективнее. Однако если при `push` емкость массива оказывается превышена, включается механизм расширения, и временная сложность именно этой операции становится $O(n)$ . -В реализации на связном списке расширение выполняется очень гибко, и проблемы падения эффективности из-за расширения массива здесь нет. Но сама операция push требует инициализации объекта-узла и изменения указателей, поэтому в среднем она немного менее эффективна. Впрочем, если помещаемые в стек элементы уже сами являются объектами-узлами, шаг инициализации можно пропустить и тем самым повысить эффективность. +В реализации на связном списке расширение выполняется очень гибко, и проблемы падения эффективности из-за расширения массива здесь нет. Но сама операция `push` требует инициализации объекта-узла и изменения указателей, поэтому в среднем она немного менее эффективна. Впрочем, если помещаемые в стек элементы уже сами являются объектами-узлами, шаг инициализации можно пропустить и тем самым повысить эффективность. Итак, когда элементами, помещаемыми и извлекаемыми из стека, являются базовые типы данных, например `int` или `double` , можно сделать следующие выводы. @@ -1825,7 +1825,7 @@ comments: true **Пространственная эффективность** -При инициализации списка система выделяет "начальную емкость", которая может превышать реальную потребность. Кроме того, механизм расширения обычно увеличивает емкость по некоторому коэффициенту (например в 2 раза), и расширенная емкость тоже может оказаться больше фактически необходимой. Поэтому **реализация стека на основе массива может приводить к некоторым потерям памяти**. +При инициализации массива система выделяет начальную емкость, которая может превышать реальную потребность. Кроме того, механизм расширения обычно увеличивает емкость по некоторому коэффициенту, например в 2 раза, и расширенная емкость тоже может оказаться больше фактически необходимой. Поэтому **реализация стека на основе массива может приводить к некоторым потерям памяти**. Однако, поскольку узлы связного списка должны дополнительно хранить указатели, **узлы списка сами по себе занимают больше пространства**. @@ -1833,5 +1833,5 @@ comments: true ## 5.1.4   Типичные применения стека -- **Кнопки "назад" и "вперед" в браузере, undo и redo в программах**. Каждый раз, когда мы открываем новую страницу, браузер помещает предыдущую страницу в стек, чтобы по операции "назад" можно было вернуться к ней. Операция "назад" по сути является pop. Если нужно одновременно поддерживать и "назад", и "вперед", то обычно используются два стека. -- **Управление памятью программы**. Каждый раз при вызове функции система помещает на вершину стека стековый кадр, в котором хранится контекст функции. В рекурсивной функции на этапе углубления рекурсии непрерывно выполняются push-операции, а на этапе возврата - pop-операции. +- **Кнопки "назад" и "вперед" в браузере, undo и redo в программах**. Каждый раз, когда мы открываем новую страницу, браузер помещает предыдущую страницу в стек, чтобы по операции "назад" можно было вернуться к ней. Операция "назад" по сути является `pop` . Если нужно одновременно поддерживать и "назад", и "вперед", то обычно используются два стека. +- **Управление памятью программы**. Каждый раз при вызове функции система помещает на вершину стека стековый кадр, в котором хранится контекст функции. В рекурсивной функции на этапе углубления рекурсии непрерывно выполняются операции `push` , а на этапе возврата - операции `pop` . diff --git a/ru/docs/chapter_stack_and_queue/summary.md b/ru/docs/chapter_stack_and_queue/summary.md index 2e1c9cafd..13a81f750 100644 --- a/ru/docs/chapter_stack_and_queue/summary.md +++ b/ru/docs/chapter_stack_and_queue/summary.md @@ -2,21 +2,21 @@ comments: true --- -# 5.4   Краткие итоги +# 5.4   Резюме -### 1.   Основные моменты +### 1.   Основные выводы - Стек - это структура данных, следующая правилу "последним пришел - первым вышел", и его можно реализовать с помощью массива или связного списка. -- С точки зрения временной эффективности реализация стека на массиве обычно работает быстрее в среднем, но во время расширения емкости временная сложность отдельной операции push может ухудшаться до $O(n)$ . Напротив, реализация стека на связном списке дает более стабильные характеристики. +- С точки зрения временной эффективности реализация стека на массиве обычно работает быстрее в среднем, но во время расширения емкости временная сложность отдельной операции `push` может ухудшаться до $O(n)$ . Напротив, реализация стека на связном списке дает более стабильные характеристики. - С точки зрения использования памяти реализация стека на массиве может приводить к некоторой потере пространства. Однако следует учитывать, что узлы связного списка занимают больше памяти, чем элементы массива. - Очередь - это структура данных, следующая правилу "первым пришел - первым вышел", и ее также можно реализовать с помощью массива или связного списка. Сравнение временной и пространственной эффективности для очереди в целом приводит к тем же выводам, что и для стека. -- Двусторонняя очередь - это очередь с более высокой степенью свободы, которая позволяет добавлять и удалять элементы с обеих сторон. +- Двусторонняя очередь - это очередь с более высокой степенью свободы, которая позволяет добавлять и удалять элементы с обоих концов. ### 2.   Q & A **Q**: Реализованы ли кнопки "вперед" и "назад" в браузере с помощью двусвязного списка? -По сути, функция переходов "вперед/назад" в браузере отражает логику "стека". Когда пользователь открывает новую страницу, она помещается на вершину стека; когда пользователь нажимает кнопку "назад", эта страница снимается с вершины стека. Двусторонняя очередь позволяет удобно реализовать некоторые дополнительные операции, об этом уже упоминалось в разделе "Двусторонняя очередь". +По сути, функция переходов "вперед/назад" в браузере отражает логику стека. Когда пользователь открывает новую страницу, она помещается на вершину стека; когда пользователь нажимает кнопку "назад", эта страница снимается с вершины стека. Двусторонняя очередь позволяет удобно реализовать некоторые дополнительные операции, об этом уже упоминалось в разделе "Двусторонняя очередь". **Q**: Нужно ли освобождать память узла после извлечения его из стека? @@ -24,7 +24,7 @@ comments: true **Q**: Двусторонняя очередь выглядит как два соединенных стека. Для чего она нужна? -Двусторонняя очередь похожа на комбинацию стека и очереди или на два соединенных стека. Она выражает логику "стек + очередь", поэтому может покрыть все применения стека и очереди и при этом остается более гибкой. +Двусторонняя очередь похожа на комбинацию стека и очереди или на два соединенных стека. Она объединяет логику обеих структур, поэтому может покрыть все их применения и при этом остается более гибкой. **Q**: Как именно реализуются отмена (undo) и повтор (redo)? diff --git a/ru/docs/chapter_tree/array_representation_of_tree.md b/ru/docs/chapter_tree/array_representation_of_tree.md index e09bd4464..119506fbb 100644 --- a/ru/docs/chapter_tree/array_representation_of_tree.md +++ b/ru/docs/chapter_tree/array_representation_of_tree.md @@ -12,13 +12,13 @@ comments: true Сначала разберем простой случай. Если дана идеальная двоичная структура и все ее узлы хранятся в массиве в порядке обхода по уровням, то каждому узлу будет соответствовать единственный индекс массива. -Из свойств обхода по уровням можно вывести "формулу соответствия" между индексом родителя и индексами дочерних узлов: **если индекс некоторого узла равен $i$ , то индекс его левого дочернего узла равен $2i + 1$ , а правого - $2i + 2$** . На рисунке 7-12 показано соответствие между индексами разных узлов. +Из свойств обхода по уровням можно вывести формулу соответствия между индексом родителя и индексами дочерних узлов: **если индекс некоторого узла равен $i$ , то индекс его левого дочернего узла равен $2i + 1$ , а правого - $2i + 2$** . На рисунке 7-12 показано соответствие между индексами разных узлов. ![Представление идеального двоичного дерева массивом](array_representation_of_tree.assets/array_representation_binary_tree.png){ class="animation-figure" }

Рисунок 7-12   Представление идеального двоичного дерева массивом

-**Эта формула соответствия играет ту же роль, что и ссылки на узлы в связной структуре** . Имея любой узел в массиве, мы можем по формуле получить доступ к его левому и правому дочерним узлам. +**Эта формула соответствия играет ту же роль, что и ссылки на узлы в связной структуре** . Имея любой узел в массиве, мы можем с ее помощью получить доступ к его левому и правому дочерним узлам. ## 7.3.2   Представление произвольного двоичного дерева diff --git a/ru/docs/chapter_tree/avl_tree.md b/ru/docs/chapter_tree/avl_tree.md index e6390212c..2e200ad3f 100644 --- a/ru/docs/chapter_tree/avl_tree.md +++ b/ru/docs/chapter_tree/avl_tree.md @@ -2018,7 +2018,12 @@ AVL-дерево одновременно является и двоичным @root = insert_helper(@root, val) end - ### Рекурсивная вставка узла (вспомогательный метод) ### + ### Вставка узла ### + def insert(val) + @root = insert_helper(@root, val) + end + + # ## Рекурсивная вставка узла (вспомогательный метод) ### def insert_helper(node, val) return TreeNode.new(val) if node.nil? # 1. Найти позицию вставки и вставить узел @@ -2606,7 +2611,12 @@ AVL-дерево одновременно является и двоичным @root = remove_helper(@root, val) end - ### Рекурсивное удаление узла (вспомогательный метод) ### + ### Удаление узла ### + def remove(val) + @root = remove_helper(@root, val) + end + + # ## Рекурсивное удаление узла (вспомогательный метод) ### def remove_helper(node, val) return if node.nil? # 1. Найти узел и удалить его diff --git a/ru/docs/chapter_tree/binary_search_tree.md b/ru/docs/chapter_tree/binary_search_tree.md index 2fc6cd52d..5221e69ec 100644 --- a/ru/docs/chapter_tree/binary_search_tree.md +++ b/ru/docs/chapter_tree/binary_search_tree.md @@ -39,7 +39,7 @@ comments: true

Рисунок 7-17   Пример поиска узла в двоичном дереве поиска

-Операция поиска в двоичном дереве поиска работает по тому же принципу, что и бинарный поиск: на каждом шаге она отбрасывает половину вариантов. Число итераций не превосходит высоты двоичного дерева, а когда дерево сбалансировано, требуется $O(\log n)$ времени. Пример кода приведен ниже: +Операция поиска в двоичном дереве поиска работает по тому же принципу, что и двоичный поиск: на каждом шаге она отбрасывает половину вариантов. Число итераций не превосходит высоты двоичного дерева, а когда дерево сбалансировано, требуется $O(\log n)$ времени. Пример кода приведен ниже: === "Python" @@ -1607,7 +1607,7 @@ comments: true ## 7.4.2   Эффективность двоичного дерева поиска -Для заданного набора данных можно рассмотреть хранение либо в массиве, либо в двоичном дереве поиска. Из таблицы ниже видно, что временная сложность операций двоичного дерева поиска имеет логарифмический порядок, поэтому его производительность стабильна и высока. Только в сценариях с очень частыми вставками и редкими поисками и удалениями массив может быть эффективнее, чем двоичное дерево поиска. +Для заданного набора данных можно рассмотреть хранение либо в массиве, либо в двоичном дереве поиска. Из таблицы ниже видно, что временная сложность операций двоичного дерева поиска имеет логарифмический порядок и обеспечивает стабильную высокую производительность. Только в сценариях с очень частыми вставками и редкими поисками и удалениями массив может быть эффективнее, чем двоичное дерево поиска.

Таблица 7-2   Сравнение эффективности массива и дерева поиска

diff --git a/ru/docs/chapter_tree/binary_tree.md b/ru/docs/chapter_tree/binary_tree.md index 18e932184..9e35997b2 100644 --- a/ru/docs/chapter_tree/binary_tree.md +++ b/ru/docs/chapter_tree/binary_tree.md @@ -4,7 +4,7 @@ comments: true # 7.1   Двоичное дерево -Двоичное дерево (binary tree) - это нелинейная структура данных, представляющая отношения порождения между "предками" и "потомками" и отражающая логику "разделения надвое". Подобно связному списку, базовой единицей двоичного дерева является узел; каждый узел содержит значение, ссылку на левого дочернего узла и ссылку на правого дочернего узла. +Двоичное дерево (binary tree) - это нелинейная структура данных, представляющая отношения между "предками" и "потомками" и отражающая логику "разделяй и властвуй". Подобно связному списку, базовой единицей двоичного дерева является узел; каждый узел содержит значение, ссылку на левого дочернего узла и ссылку на правого дочернего узла. === "Python" @@ -207,7 +207,7 @@ comments: true Каждый узел имеет две ссылки (указателя), которые соответственно указывают на левого дочернего узла (left-child node) и правого дочернего узла (right-child node); данный узел называется родительским узлом (parent node) для этих двух дочерних узлов. Если задан некоторый узел двоичного дерева, то дерево, образованное его левым дочерним узлом и всеми узлами ниже него, называется левым поддеревом (left subtree) этого узла; аналогично определяется правое поддерево (right subtree). -**В двоичном дереве, кроме листовых узлов, все остальные узлы содержат дочерние узлы и непустые поддеревья**. Как показано на рисунке 7-1, если рассматривать "узел 2" как родительский, то его левым и правым дочерними узлами будут "узел 4" и "узел 5"; левое поддерево - это "узел 4 и дерево ниже него", а правое поддерево - это "узел 5 и дерево ниже него". +**Узлы, не имеющие дочерних узлов, называют листьями, а все остальные узлы содержат дочерние узлы и непустые поддеревья**. Как показано на рисунке 7-1, если рассматривать "узел 2" как родительский, то его левым и правым дочерними узлами будут "узел 4" и "узел 5"; левое поддерево - это "узел 4 и дерево ниже него", а правое поддерево - это "узел 5 и дерево ниже него". ![Родительский узел, дочерние узлы и поддеревья](binary_tree.assets/binary_tree_definition.png){ class="animation-figure" } @@ -232,7 +232,7 @@ comments: true !!! tip - Обрати внимание: обычно под "высотой" и "глубиной" понимают "число пройденных ребер", но в некоторых задачах или учебниках их могут определять как "число пройденных узлов". В таком случае и высоту, и глубину нужно увеличить на 1 . + Обычно под "высотой" и "глубиной" понимают "число пройденных ребер", но в некоторых задачах или учебниках их могут определять как "число пройденных узлов". В таком случае и высоту, и глубину нужно увеличить на 1 . ## 7.1.2   Базовые операции двоичного дерева @@ -631,7 +631,7 @@ comments: true !!! tip - Обрати внимание: вставка узла может изменить исходную логическую структуру двоичного дерева, а удаление узла обычно означает удаление этого узла вместе со всеми его поддеревьями. Поэтому в двоичном дереве операции вставки и удаления обычно являются частью более крупного набора операций, который и реализует осмысленное действие. + Стоит помнить, что вставка узла может изменить исходную логическую структуру двоичного дерева, а удаление узла обычно означает удаление этого узла вместе со всеми его поддеревьями. Поэтому в двоичном дереве операции вставки и удаления обычно являются частью более крупного набора операций, который и реализует осмысленное действие. ## 7.1.3   Распространенные типы двоичных деревьев @@ -641,7 +641,7 @@ comments: true !!! tip - Обрати внимание: в китайскоязычном сообществе идеальное двоичное дерево часто называют полностью заполненным двоичным деревом. + В китайскоязычном сообществе идеальное двоичное дерево часто называют полностью заполненным двоичным деревом. ![Идеальное двоичное дерево](binary_tree.assets/perfect_binary_tree.png){ class="animation-figure" } @@ -649,7 +649,7 @@ comments: true ### 2.   Полное двоичное дерево -Как показано на рисунке 7-5, полное двоичное дерево (complete binary tree) допускает неполное заполнение только на самом нижнем уровне, причем узлы этого уровня должны непрерывно заполняться слева направо. Обрати внимание: идеальное двоичное дерево тоже является полным двоичным деревом. +Как показано на рисунке 7-5, полное двоичное дерево (complete binary tree) допускает неполное заполнение только на самом нижнем уровне, причем узлы этого уровня должны непрерывно заполняться слева направо. Стоит отметить, что идеальное двоичное дерево тоже является полным двоичным деревом. ![Полное двоичное дерево](binary_tree.assets/complete_binary_tree.png){ class="animation-figure" } @@ -675,7 +675,7 @@ comments: true На рисунке 7-8 показаны идеальная структура двоичного дерева и вырожденная структура. Когда каждый уровень двоичного дерева полностью заполнен узлами, мы получаем "идеальное двоичное дерево"; когда же все узлы смещаются к одной стороне, двоичное дерево вырождается в "связный список". -- Идеальное двоичное дерево соответствует лучшему случаю и позволяет полностью раскрыть преимущества двоичного дерева с точки зрения "разделяй и властвуй". +- Идеальное двоичное дерево соответствует лучшему случаю и позволяет в полной мере раскрыть преимущества подхода "разделяй и властвуй". - Связный список представляет противоположную крайность: все операции становятся линейными, а временная сложность деградирует до $O(n)$ . ![Лучший и худший случаи структуры двоичного дерева](binary_tree.assets/binary_tree_best_worst_cases.png){ class="animation-figure" } diff --git a/ru/docs/chapter_tree/binary_tree_traversal.md b/ru/docs/chapter_tree/binary_tree_traversal.md index 4b4d4e882..c88d9c98f 100644 --- a/ru/docs/chapter_tree/binary_tree_traversal.md +++ b/ru/docs/chapter_tree/binary_tree_traversal.md @@ -12,7 +12,7 @@ comments: true Как показано на рисунке 7-9, обход по уровням (level-order traversal) проходит двоичное дерево сверху вниз по уровням и на каждом уровне посещает узлы слева направо. -По своей сути обход по уровням относится к обходу в ширину (breadth-first traversal), также называемому поиском в ширину (breadth-first search, BFS); он отражает идею "расширяться слой за слоем наружу". +По своей сути обход по уровням относится к обходу в ширину (breadth-first traversal), также называемому поиском в ширину (breadth-first search, BFS); он отражает идею "расширяться от центра к периферии слой за слоем". ![Обход двоичного дерева по уровням](binary_tree_traversal.assets/binary_tree_bfs.png){ class="animation-figure" } @@ -346,9 +346,9 @@ comments: true ## 7.2.2   Прямой, симметричный и обратный обходы -Соответственно, прямой, симметричный и обратный обходы относятся к обходу в глубину (depth-first traversal), также называемому поиском в глубину (depth-first search, DFS); он отражает идею "сначала идти до конца, затем откатываться и продолжать". +Соответственно, прямой, симметричный и обратный обходы относятся к обходу в глубину (depth-first traversal), также называемому поиском в глубину (depth-first search, DFS); он отражает идею "сначала идти до конца, затем возвращаться и продолжать". -На рисунке 7-10 показан принцип работы обхода двоичного дерева в глубину. **Обход в глубину похож на то, как будто мы обходим всю двоичную структуру по внешнему контуру** , и у каждого узла встречаем три позиции, соответствующие прямому, симметричному и обратному обходам. +На рисунке 7-10 показан принцип работы обхода двоичного дерева в глубину. **Обход в глубину можно представить как обход всей двоичной структуры по внешнему контуру** , и у каждого узла встречаются три позиции, соответствующие прямому, симметричному и обратному обходам. ![Прямой, симметричный и обратный обходы двоичного дерева поиска](binary_tree_traversal.assets/binary_tree_dfs.png){ class="animation-figure" } diff --git a/ru/docs/chapter_tree/index.md b/ru/docs/chapter_tree/index.md index dd05f2b1b..13105d0dc 100644 --- a/ru/docs/chapter_tree/index.md +++ b/ru/docs/chapter_tree/index.md @@ -9,15 +9,15 @@ icon: material/graph-outline !!! abstract - Высокое дерево полно жизни: мощные корни, густая крона и раскидистые ветви. + Высокое дерево полно жизни: мощные корни, густая листва и раскидистые ветви. - Оно наглядно показывает нам форму данных, построенную на принципе "разделяй и властвуй". + Оно наглядно показывает нам живую форму данных, построенную на принципе "разделяй и властвуй". ## Содержание главы - [7.1   Двоичное дерево](binary_tree.md) - [7.2   Обход двоичного дерева](binary_tree_traversal.md) -- [7.3   Представление дерева массивом](array_representation_of_tree.md) +- [7.3   Представление двоичного дерева массивом](array_representation_of_tree.md) - [7.4   Двоичное дерево поиска](binary_search_tree.md) - [7.5   AVL-дерево *](avl_tree.md) -- [7.6   Резюме](summary.md) +- [7.6   Краткие итоги](summary.md) diff --git a/ru/docs/chapter_tree/summary.md b/ru/docs/chapter_tree/summary.md index 8e3900d8a..d74f59ba3 100644 --- a/ru/docs/chapter_tree/summary.md +++ b/ru/docs/chapter_tree/summary.md @@ -6,14 +6,14 @@ comments: true ### 1.   Основные моменты -- Двоичное дерево - это нелинейная структура данных, отражающая логику "разделения надвое". Каждый узел двоичного дерева содержит значение и два указателя, которые соответственно ведут к левому и правому дочерним узлам. +- Двоичное дерево - это нелинейная структура данных, отражающая логику "разделяй и властвуй". Каждый узел двоичного дерева содержит значение и два указателя, которые соответственно ведут к левому и правому дочерним узлам. - Для любого узла двоичного дерева дерево, образованное его левым (правым) дочерним узлом и всеми нижележащими узлами, называется левым (правым) поддеревом этого узла. - К связанным с двоичным деревом терминам относятся корневой узел, листовой узел, уровень, степень, ребро, высота, глубина и так далее. -- Инициализация двоичного дерева, вставка узлов и удаление узлов похожи по способу реализации на операции со связным списком. +- Инициализация двоичного дерева, вставка узлов и удаление узлов аналогичны операциям со связным списком. - К распространенным видам двоичного дерева относятся идеальное двоичное дерево, полное двоичное дерево, строгое двоичное дерево и сбалансированное двоичное дерево. Идеальное двоичное дерево - наиболее желательное состояние, а связный список - худший случай после вырождения. -- Двоичное дерево можно представить массивом: значения узлов и пустые позиции располагаются в порядке обхода по уровням, а связи между родителем и детьми реализуются через отображение индексов. -- Обход двоичного дерева по уровням является методом поиска в ширину; он отражает идею "расширяться слой за слоем наружу" и обычно реализуется через очередь. -- Прямой, симметричный и обратный обходы относятся к поиску в глубину; они отражают идею "сначала дойти до конца, затем откатиться и продолжить" и обычно реализуются рекурсивно. +- Двоичное дерево можно представить массивом: значения узлов и пустые позиции располагаются в порядке обхода по уровням, а связи между родителем и детьми реализуются через индексацию. +- Обход двоичного дерева по уровням является методом поиска в ширину; он отражает идею "расширяться от центра к периферии слой за слоем" и обычно реализуется через очередь. +- Прямой, симметричный и обратный обходы относятся к поиску в глубину; они отражают идею "сначала дойти до конца, затем вернуться и продолжить" и обычно реализуются рекурсивно. - Двоичное дерево поиска - это эффективная структура данных для поиска элементов; его поиск, вставка и удаление имеют временную сложность $O(\log n)$ . Когда двоичное дерево поиска вырождается в связный список, все эти сложности деградируют до $O(n)$ . - AVL-дерево, также называемое сбалансированным двоичным деревом поиска, с помощью вращений гарантирует, что после постоянных вставок и удалений узлов дерево остается сбалансированным. - Вращения AVL-дерева включают правое вращение, левое вращение, сначала правое затем левое и сначала левое затем правое. После вставки или удаления узла AVL-дерево выполняет вращения снизу вверх, чтобы снова восстановить баланс. diff --git a/ru/docs/index.assets/animation.mp4 b/ru/docs/index.assets/animation.mp4 new file mode 100644 index 000000000..cf3838e79 Binary files /dev/null and b/ru/docs/index.assets/animation.mp4 differ diff --git a/ru/docs/index.assets/comment.mp4 b/ru/docs/index.assets/comment.mp4 new file mode 100644 index 000000000..3f9ea340c Binary files /dev/null and b/ru/docs/index.assets/comment.mp4 differ diff --git a/ru/docs/index.assets/running_code.mp4 b/ru/docs/index.assets/running_code.mp4 new file mode 100644 index 000000000..c946fbd68 Binary files /dev/null and b/ru/docs/index.assets/running_code.mp4 differ diff --git a/ru/docs/index.html b/ru/docs/index.html index 229242b84..cbe077bb0 100644 --- a/ru/docs/index.html +++ b/ru/docs/index.html @@ -5,7 +5,7 @@
-
-
+
@@ -135,7 +135,7 @@
-
+

Рекомендации

@@ -152,7 +152,7 @@
-
+
@@ -167,11 +167,11 @@

"A picture is worth a thousand words."
«Одна схема стоит тысячи слов»

- Animation example +
- Running code example +
@@ -199,14 +199,14 @@

"Learning by teaching."
«Обучая других, учишься сам»

- Comments example +
-
+

Благодарности @@ -222,7 +222,7 @@

-
+
@@ -243,20 +243,22 @@

Русская печатная версия была переведена И. А. Шевкун, а русская онлайн-версия была вычитана Yuyan Huang. Благодарим их за вклад!

- Translator: shevkun + Translator: shevkun
И. А. Шевкун
- Translator: yuyanhuang -
Yuyan Huang + + Translator: yuyanhuang +
Yuyan Huang +
- +
-

Переводчики кода

-

Многоязычные версии кода в этой книге были подготовлены при участии следующих переводчиков. Благодарим их за время и вклад!

+

Ревьюеры кода

+

Многоязычные версии кода в этой книге были подготовлены при участии следующих разработчиков. Благодарим их за время и вклад!

diff --git a/ru/docs/index.md b/ru/docs/index.md index bcabc4880..818a368dd 100644 --- a/ru/docs/index.md +++ b/ru/docs/index.md @@ -1,6 +1,5 @@ --- comments: false -glightbox: false search: exclude: true hide: diff --git a/zh-Hant/docs/chapter_graph/graph_operations.md b/zh-Hant/docs/chapter_graph/graph_operations.md index 6c2f1270a..3aba7064f 100644 --- a/zh-Hant/docs/chapter_graph/graph_operations.md +++ b/zh-Hant/docs/chapter_graph/graph_operations.md @@ -15,19 +15,19 @@ comments: true - **刪除頂點**:在鄰接矩陣中刪除一行一列。當刪除首行首列時達到最差情況,需要將 $(n-1)^2$ 個元素“向左上移動”,從而使用 $O(n^2)$ 時間。 - **初始化**:傳入 $n$ 個頂點,初始化長度為 $n$ 的頂點串列 `vertices` ,使用 $O(n)$ 時間;初始化 $n \times n$ 大小的鄰接矩陣 `adjMat` ,使用 $O(n^2)$ 時間。 -=== "初始化鄰接矩陣" +=== "<1>" ![鄰接矩陣的初始化、增刪邊、增刪頂點](graph_operations.assets/adjacency_matrix_step1_initialization.png){ class="animation-figure" } -=== "新增邊" +=== "<2>" ![adjacency_matrix_add_edge](graph_operations.assets/adjacency_matrix_step2_add_edge.png){ class="animation-figure" } -=== "刪除邊" +=== "<3>" ![adjacency_matrix_remove_edge](graph_operations.assets/adjacency_matrix_step3_remove_edge.png){ class="animation-figure" } -=== "新增頂點" +=== "<4>" ![adjacency_matrix_add_vertex](graph_operations.assets/adjacency_matrix_step4_add_vertex.png){ class="animation-figure" } -=== "刪除頂點" +=== "<5>" ![adjacency_matrix_remove_vertex](graph_operations.assets/adjacency_matrix_step5_remove_vertex.png){ class="animation-figure" }

圖 9-7   鄰接矩陣的初始化、增刪邊、增刪頂點

@@ -1221,19 +1221,19 @@ comments: true - **刪除頂點**:需走訪整個鄰接表,刪除包含指定頂點的所有邊,使用 $O(n + m)$ 時間。 - **初始化**:在鄰接表中建立 $n$ 個頂點和 $2m$ 條邊,使用 $O(n + m)$ 時間。 -=== "初始化鄰接表" +=== "<1>" ![鄰接表的初始化、增刪邊、增刪頂點](graph_operations.assets/adjacency_list_step1_initialization.png){ class="animation-figure" } -=== "新增邊" +=== "<2>" ![adjacency_list_add_edge](graph_operations.assets/adjacency_list_step2_add_edge.png){ class="animation-figure" } -=== "刪除邊" +=== "<3>" ![adjacency_list_remove_edge](graph_operations.assets/adjacency_list_step3_remove_edge.png){ class="animation-figure" } -=== "新增頂點" +=== "<4>" ![adjacency_list_add_vertex](graph_operations.assets/adjacency_list_step4_add_vertex.png){ class="animation-figure" } -=== "刪除頂點" +=== "<5>" ![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_step5_remove_vertex.png){ class="animation-figure" }

圖 9-8   鄰接表的初始化、增刪邊、增刪頂點

diff --git a/zh-Hant/docs/chapter_preface/about_the_book.md b/zh-Hant/docs/chapter_preface/about_the_book.md index 272fb1dff..d478d9916 100644 --- a/zh-Hant/docs/chapter_preface/about_the_book.md +++ b/zh-Hant/docs/chapter_preface/about_the_book.md @@ -36,13 +36,13 @@ comments: true ## 0.1.3   致謝 -本書在開源社群眾多貢獻者的共同努力下不斷完善。感謝每一位投入時間與精力的撰稿人,他們是(按照 GitHub 自動生成的順序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、IsChristina、xBLACKICEx、guowei-gong、Cathay-Chen、pengchzn、QiLOL、magentaqin、hello-ikun、JoseHung、qualifier1024、thomasq0、sunshinesDL、L-Super、Guanngxu、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、Shyam-Chen、theNefelibatas、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、MolDuM、Nigh、Dr-XYZ、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、beatrix-chan、DullSword、xjr7670、jiaxianhua、qq909244296、iStig、boloboloda、hts0000、gledfish、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、llql1211、fbigm、echo1937、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、GaochaoZhu、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Allen-Scai、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、zhongfq、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Keynman、KeiichiKasai 和 KawaiiAsh。 +本書在開源社群眾多貢獻者的共同努力下不斷完善。感謝每一位投入時間與精力的撰稿人,他們是(按照 GitHub 自動生成的順序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、pengchzn、QiLOL、Cathay-Chen、guowei-gong、xBLACKICEx、IsChristina、JoseHung、qualifier1024、hello-ikun、magentaqin、Guanngxu、thomasq0、sunshinesDL、L-Super、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、theNefelibatas、Shyam-Chen、sangxiaai、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、Nigh、Dr-XYZ、MolDuM、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、xjr7670、beatrix-chan、DullSword、qq909244296、iStig、boloboloda、hts0000、gledfish、fbigm、echo1937、jiaxianhua、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、JTCPOWI、KawaiiAsh、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、llql1211、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Senrian、Allen-Scai、19santosh99、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、codetypess、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Kunchen-Luo、Keynman 和 KeiichiKasai。 本書的程式碼審閱工作由 coderonion、curtishd、Gonglja、gvenusleo、hpstory、justin-tse、khoaxuantu、krahets、night-cruise、nuomi1、Reanon 和 rongyi 完成(按照首字母順序排列)。感謝他們付出的時間與精力,正是他們確保了各語言程式碼的規範與統一。 -本書的繁體中文版由 Shyam-Chen 和 Dr-XYZ 審閱,英文版由 yuelinxin、K3v123、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn、thomasq0 和 magentaqin 審閱,日文版由 eltociear 審閱。正是因為他們的持續貢獻,這本書才能夠服務於更廣泛的讀者群體,感謝他們。 +本書的英文版由 yuelinxin、K3v123、magentaqin、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn 和 thomasq0 審閱;日文版由 eltociear 審閱;俄文版由 И. А. Шевкун 和 Yuyan Huang 審閱;繁體中文版由 Shyam-Chen 和 Dr-XYZ 審閱。正是因為他們的貢獻,這本書才能夠服務於更廣泛的讀者群體,感謝他們。 -本書的 ePub 電子書生成工具由 zhongfq 開發。感謝他的貢獻,為讀者提供了更加自由的閱讀方式。 +本書的 ePub 電子書生成工具由 zhongfq 開發。感謝他的貢獻,為讀者提供了更靈活的閱讀方式。 在本書的創作過程中,我得到了許多人的幫助。 @@ -51,7 +51,7 @@ comments: true - 感謝騰寶、琦寶、飛寶為本書起了一個富有創意的名字,喚起大家寫下第一行程式碼“Hello World!”的美好回憶; - 感謝校銓在智慧財產權方面提供的專業幫助,這對本開源書的完善起到了重要作用; - 感謝蘇潼為本書設計了精美的封面和 logo ,並在我的強迫症的驅使下多次耐心修改; -- 感謝 @squidfunk 提供的排版建議,以及他開發的開源文件主題 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 。 +- 感謝 @squidfunk 提供的排版建議,以及他開發的開源文件主題 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material) 。 在寫作過程中,我閱讀了許多關於資料結構與演算法的教材和文章。這些作品為本書提供了優秀的範本,確保了本書內容的準確性與品質。在此感謝所有老師和前輩的傑出貢獻! diff --git a/zh-Hant/docs/chapter_preface/suggestions.md b/zh-Hant/docs/chapter_preface/suggestions.md index 5940a37be..9c9f3fead 100644 --- a/zh-Hant/docs/chapter_preface/suggestions.md +++ b/zh-Hant/docs/chapter_preface/suggestions.md @@ -141,10 +141,8 @@ comments: true // 內容註釋,用於詳解程式碼 - /** - * 多行 - * 註釋 - */ + // 多行 + // 註釋 ``` === "C" diff --git a/zh-Hant/docs/chapter_stack_and_queue/deque.md b/zh-Hant/docs/chapter_stack_and_queue/deque.md index 4bdef68b1..5e667adb1 100644 --- a/zh-Hant/docs/chapter_stack_and_queue/deque.md +++ b/zh-Hant/docs/chapter_stack_and_queue/deque.md @@ -410,19 +410,19 @@ comments: true 如圖 5-8 所示,我們將雙向鏈結串列的頭節點和尾節點視為雙向佇列的佇列首和佇列尾,同時實現在兩端新增和刪除節點的功能。 -=== "LinkedListDeque" +=== "<1>" ![基於鏈結串列實現雙向佇列的入列出列操作](deque.assets/linkedlist_deque_step1.png){ class="animation-figure" } -=== "push_last()" +=== "<2>" ![linkedlist_deque_push_last](deque.assets/linkedlist_deque_step2_push_last.png){ class="animation-figure" } -=== "push_first()" +=== "<3>" ![linkedlist_deque_push_first](deque.assets/linkedlist_deque_step3_push_first.png){ class="animation-figure" } -=== "pop_last()" +=== "<4>" ![linkedlist_deque_pop_last](deque.assets/linkedlist_deque_step4_pop_last.png){ class="animation-figure" } -=== "pop_first()" +=== "<5>" ![linkedlist_deque_pop_first](deque.assets/linkedlist_deque_step5_pop_first.png){ class="animation-figure" }

圖 5-8   基於鏈結串列實現雙向佇列的入列出列操作

@@ -2158,19 +2158,19 @@ comments: true 如圖 5-9 所示,與基於陣列實現佇列類似,我們也可以使用環形陣列來實現雙向佇列。 -=== "ArrayDeque" +=== "<1>" ![基於陣列實現雙向佇列的入列出列操作](deque.assets/array_deque_step1.png){ class="animation-figure" } -=== "push_last()" +=== "<2>" ![array_deque_push_last](deque.assets/array_deque_step2_push_last.png){ class="animation-figure" } -=== "push_first()" +=== "<3>" ![array_deque_push_first](deque.assets/array_deque_step3_push_first.png){ class="animation-figure" } -=== "pop_last()" +=== "<4>" ![array_deque_pop_last](deque.assets/array_deque_step4_pop_last.png){ class="animation-figure" } -=== "pop_first()" +=== "<5>" ![array_deque_pop_first](deque.assets/array_deque_step5_pop_first.png){ class="animation-figure" }

圖 5-9   基於陣列實現雙向佇列的入列出列操作

diff --git a/zh-Hant/docs/chapter_stack_and_queue/queue.md b/zh-Hant/docs/chapter_stack_and_queue/queue.md index b42217782..459f2f206 100755 --- a/zh-Hant/docs/chapter_stack_and_queue/queue.md +++ b/zh-Hant/docs/chapter_stack_and_queue/queue.md @@ -379,13 +379,13 @@ comments: true 如圖 5-5 所示,我們可以將鏈結串列的“頭節點”和“尾節點”分別視為“佇列首”和“佇列尾”,規定佇列尾僅可新增節點,佇列首僅可刪除節點。 -=== "LinkedListQueue" +=== "<1>" ![基於鏈結串列實現佇列的入列出列操作](queue.assets/linkedlist_queue_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![linkedlist_queue_push](queue.assets/linkedlist_queue_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![linkedlist_queue_pop](queue.assets/linkedlist_queue_step3_pop.png){ class="animation-figure" }

圖 5-5   基於鏈結串列實現佇列的入列出列操作

@@ -1329,13 +1329,13 @@ comments: true 可以看到,入列和出列操作都只需進行一次操作,時間複雜度均為 $O(1)$ 。 -=== "ArrayQueue" +=== "<1>" ![基於陣列實現佇列的入列出列操作](queue.assets/array_queue_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![array_queue_push](queue.assets/array_queue_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![array_queue_pop](queue.assets/array_queue_step3_pop.png){ class="animation-figure" }

圖 5-6   基於陣列實現佇列的入列出列操作

@@ -2072,7 +2072,7 @@ comments: true typedef struct { int *nums; // 用於儲存佇列元素的陣列 int front; // 佇列首指標,指向佇列首元素 - int queSize; // 尾指標,指向佇列尾 + 1 + int queSize; // 當前佇列的元素數量 int queCapacity; // 佇列容量 } ArrayQueue; diff --git a/zh-Hant/docs/chapter_stack_and_queue/stack.md b/zh-Hant/docs/chapter_stack_and_queue/stack.md index b49ecf82d..2642e5689 100755 --- a/zh-Hant/docs/chapter_stack_and_queue/stack.md +++ b/zh-Hant/docs/chapter_stack_and_queue/stack.md @@ -376,13 +376,13 @@ comments: true 如圖 5-2 所示,對於入堆疊操作,我們只需將元素插入鏈結串列頭部,這種節點插入方法被稱為“頭插法”。而對於出堆疊操作,只需將頭節點從鏈結串列中刪除即可。 -=== "LinkedListStack" +=== "<1>" ![基於鏈結串列實現堆疊的入堆疊出堆疊操作](stack.assets/linkedlist_stack_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![linkedlist_stack_push](stack.assets/linkedlist_stack_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![linkedlist_stack_pop](stack.assets/linkedlist_stack_step3_pop.png){ class="animation-figure" }

圖 5-2   基於鏈結串列實現堆疊的入堆疊出堆疊操作

@@ -1169,13 +1169,13 @@ comments: true 使用陣列實現堆疊時,我們可以將陣列的尾部作為堆疊頂。如圖 5-3 所示,入堆疊與出堆疊操作分別對應在陣列尾部新增元素與刪除元素,時間複雜度都為 $O(1)$ 。 -=== "ArrayStack" +=== "<1>" ![基於陣列實現堆疊的入堆疊出堆疊操作](stack.assets/array_stack_step1.png){ class="animation-figure" } -=== "push()" +=== "<2>" ![array_stack_push](stack.assets/array_stack_step2_push.png){ class="animation-figure" } -=== "pop()" +=== "<3>" ![array_stack_pop](stack.assets/array_stack_step3_pop.png){ class="animation-figure" }

圖 5-3   基於陣列實現堆疊的入堆疊出堆疊操作

diff --git a/zh-Hant/docs/index.assets/animation.mp4 b/zh-Hant/docs/index.assets/animation.mp4 new file mode 100644 index 000000000..cf3838e79 Binary files /dev/null and b/zh-Hant/docs/index.assets/animation.mp4 differ diff --git a/zh-Hant/docs/index.assets/comment.mp4 b/zh-Hant/docs/index.assets/comment.mp4 new file mode 100644 index 000000000..3f9ea340c Binary files /dev/null and b/zh-Hant/docs/index.assets/comment.mp4 differ diff --git a/zh-Hant/docs/index.assets/running_code.mp4 b/zh-Hant/docs/index.assets/running_code.mp4 new file mode 100644 index 000000000..c946fbd68 Binary files /dev/null and b/zh-Hant/docs/index.assets/running_code.mp4 differ diff --git a/zh-Hant/docs/index.html b/zh-Hant/docs/index.html index 65ec1ab16..56a079c12 100644 --- a/zh-Hant/docs/index.html +++ b/zh-Hant/docs/index.html @@ -84,7 +84,7 @@ - 程式碼倉庫 + GitHub
@@ -96,32 +96,8 @@
- -
-
- Preview -
- - - - - - - - - - - - - - -
-

500 幅動畫圖解、14 種程式語言程式碼、3000 條社群問答,助你快速入門資料結構與演算法

-
-
- -
+
@@ -166,11 +142,12 @@
+

500 幅動畫圖解、14 種程式語言程式碼、3000 條社群問答,助你快速入門資料結構與演算法

-
+

推薦語

@@ -187,7 +164,7 @@
-
+
@@ -202,11 +179,11 @@

"A picture is worth a thousand words."
“一圖勝千言”

- Animation example +
- Running code example +
@@ -234,14 +211,14 @@

"Learning by teaching."
“教學相長”

- Comments example +
-
+

鳴謝 @@ -257,7 +234,7 @@

-
+
@@ -272,9 +249,30 @@
- + +
+

譯者

+

本書繁體中文版由以下譯者審閱,感謝他們的貢獻!

+ +
+ +

程式碼審閱者

+

本書多語言程式碼版本由以下開發者協力完成,感謝他們的投入與貢獻!

- - -

貢獻者

@@ -391,4 +370,4 @@
-
\ No newline at end of file +
diff --git a/zh-Hant/docs/index.md b/zh-Hant/docs/index.md index bcabc4880..818a368dd 100644 --- a/zh-Hant/docs/index.md +++ b/zh-Hant/docs/index.md @@ -1,6 +1,5 @@ --- comments: false -glightbox: false search: exclude: true hide: