news 2026/9/8 23:00:48

Hello Algo探索章「二分探索演習」完全攻略──区間縮小のトレースから重複要素の境界、挿入位置の探索まで

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Hello Algo探索章「二分探索演習」完全攻略──区間縮小のトレースから重複要素の境界、挿入位置の探索まで

Hello Algo探索章「二分探索演習」完全攻略──区間縮小のトレースから重複要素の境界、挿入位置の探索まで

【免费下载链接】hello-algo《Hello 算法》:动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語,提供 Python, Java, C++, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo

本記事は、オープンソースのデータ構造・アルゴリズム教材『Hello Algo』日本語版の探索章に収録された演習問題(確認問題3問+プログラミング演習2問)を、正解・解説・リポジトリ内の実装コード付きで体系的に解説する技術ガイドです。二分探索の区間の狭め方を手で追跡し、重複要素の左右境界と挿入位置の求め方、そして線形探索・二分探索・ハッシュテーブルの使い分け判断力を、実データを使って身に付けられます。

演習の全体像と前提知識

探索(サーチ)は「データ構造の中から条件を満たす要素を特定する」操作であり、探索アルゴリズム再考の章では、実装思想の違いによって次の2系統に整理されます。

  • 総当たり探索:線形探索、幅優先探索(BFS)、深さ優先探索(DFS)など、データ構造を走査して目標を特定する系統。
  • 適応的探索:二分探索・ハッシュ探索・木探索など、データの「整列済み」といった事前情報や追加構造を利用して高速に特定する系統。

本章の演習は、このうち特に二分探索の仕組み理解を問う「確認問題」と、実装力を問う「プログラミング演習」の2部構成です。取り組む前に、以下の各章ページで基本を復習しておくと効果的です。

関連する章ページ学べる内容
二分探索両閉区間・左閉右開区間の基本アルゴリズムと $O(\log n)$ の導出
二分探索の挿入位置挿入位置の探索と重複要素への拡張
二分探索の境界重複要素の左端・右端の探索
探索アルゴリズム再考探索手法の全体俯瞰と効率比較表
まとめ探索章全体の要点の振り返り

確認問題1:二分探索による区間の狭め方をトレースする

問題

ソート済み配列[2, 5, 8, 12, 16, 23, 38]から値 16 を二分探索で探します。両閉区間 $[i, j]$ を使い、中点を $m = i + (j-i)/2$(小数点以下切り捨て)と定義したとき、目的の値が見つかるまでの各回の(i, j, m)、中点の要素、次の区間の狭め方を書き出してください。

解答と解説

各回の探索過程は下表のとおりです。

(i, j, m)中点の要素次の操作
1(0, 6, 3)1212 < 16なのでi = 4とする
2(4, 6, 5)2323 > 16なのでj = 4とする
3(4, 4, 4)16目的の値を発見し、インデックス 4 を返す

配列はソート済みであるため、中点の値が目的の値より小さければ中点とその左側を除外でき(i = m + 1)、大きければ中点とその右側を除外できます(j = m - 1)。3回目の判定で区間が(4, 4)のただ1要素にまで縮小し、nums[4] == 16を確認して探索が終了します。

中点計算式の注意点

本問で与えられている中点式は $m = i + (j-i)/2$ です。これはよくある $m = (i+j)/2$ と等価ですが、C や Java など固定精度の整数型ではi + jint型の最大値を超えてオーバーフローするリスクがあるため、実際のコードでは引き算ベースの式が使われます。リポジトリの C 実装 binary_search.c でも、この点を踏まえて次のように書かれています。

int binarySearch(int *nums, int len, int target) { // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 int i = 0, j = len - 1; 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; // 找到目标元素,返回其索引 } return -1; // 未找到目标元素,返回 -1 }

一方、Python は整数が任意精度のため、binary_search.py ではm = (i + j) // 2と直接計算できます。どの言語でもループ1回につき区間が半分になるため、反復回数は $\log_2 n$ 回に収まり、時間計算量 $O(\log n)$・空間計算量 $O(1)$ というのが二分探索の核心的な性質です。

確認問題2:重複要素の左右の境界を探る

問題

配列[1, 2, 2, 2, 4, 6]から数値 2 を探索します。ある生徒は二分探索でまずインデックス 2 に 2 を見つけて即座に返し、「インデックス 2 が数値 2 の左端境界である」と主張しました。

  1. この生徒の説明は正しいですか?数値 2 の左端境界と右端境界はそれぞれどこですか?理由も説明してください。
  2. 左端境界を探索するとき、中点の要素が目的の値と等しい場合、次はどちら側を探索すべきですか?
  3. 右端境界を探索するときはどちら側を探索すべきですか(方向のみでよい)?

解答と解説

1. 説明は正しくありません。配列中に 2 が 3 個あり、インデックス 2 はそのうちの中央です。1 個の 2 を見つけて即座に返す方法では「いずれかの 2 を見つけた」ことしか保証できず、「最も左」や「最も右」の 2 であるとは限りません。この配列では左端境界はインデックス 1、右端境界はインデックス 3です。

2. 左端境界を探索するときは、中点の要素が 2 と等しくても引き続き左側を探索します。両閉区間を使う場合は、等しいときにj = m - 1として右端を詰めます。これにより、ポインタ $i$ は最終的に「最も左の 2」を指し、ポインタ $j$ は「2 より小さい最も右の要素」を指します。

3. 右端境界を探索するときは、中点の要素が 2 と等しければ引き続き右側を探索し、i = m + 1とします。対称的な操作で、最終的に $j$ が「最も右の 2」を指します。

実装の裏付け:挿入位置探索への帰着

この「等しい場合にも区間を詰め続ける」という発想は、リポジトリの binary_search_insertion.py に明快に実装されています。重複要素がある配列では、nums[m] == targetのときもj = m - 1に縮小することで、ループ終了後の $i$ が最左の target の挿入位置を指すのです。

def binary_search_insertion(nums: list[int], target: int) -> int: """二分查找插入点(存在重复元素)""" i, j = 0, len(nums) - 1 # 初始化双闭区间 [0, n-1] while i <= j: m = (i + j) // 2 # 计算中点索引 m if nums[m] < target: i = m + 1 # target 在区间 [m+1, j] 中 elif nums[m] > target: j = m - 1 # target 在区间 [i, m-1] 中 else: j = m - 1 # 最右一个小于 target 的元素在区间 [i, m-1] 中 return i # 返回插入点 i

さらに binary_search_edge.py は、この関数を左端・右端の探索に再利用しています。

def binary_search_left_edge(nums: list[int], target: int) -> int: """二分查找最左一个 target""" i = binary_search_insertion(nums, target) if i == len(nums) or nums[i] != target: return -1 # 未找到 target return i def binary_search_right_edge(nums: list[int], target: int) -> int: """二分查找最右一个 target(转化为查找最左一个 target + 1)""" i = binary_search_insertion(nums, target + 1) j = i - 1 if j == -1 or nums[j] != target: return -1 # 未找到 target return j

ここで重要なのは「探索区間の縮小 = ポインタ $i$, $j$ に探索目標を設定すること」という視点です。目標は「特定の要素」である場合も、「target より小さい要素」のような要素の範囲である場合もあります。等しいときの分岐先を変えるだけで、同じ二分探索の骨格から挿入位置・左端・右端のすべてが導けることを、この演習は問いかけています。詳細は二分探索の挿入位置と二分探索の境界の章を参照してください。

確認問題3:データ特性に応じた探索手法の選び方

問題

「線形探索・二分探索・ハッシュテーブル」から、次の3つの場面に適した方法を選び、理由を説明してください。

  1. ソート済みで今後変更されない $10^7$ 個の整数を繰り返し探索する。他のデータ構造は追加で作らない。
  2. 挿入と削除が頻繁に行われるデータ集合で、あるキーが存在するかを繰り返し判定する。順序を保つ必要も範囲探索の必要もない。
  3. ソートされていない配列から、ある値を1回だけ探索する。

解答と解説

1. 二分探索を選びます。データがソート済みで今後変更されないため、追加の空間を一切使わずに各探索を $O(\log n)$ で行えます。$10^7 \approx 2^{23.25}$ なので、1回あたり約24回の比較で探索が完了します。追加のデータ構造を作れないという制約も、追加領域 $O(1)$ の二分探索なら問題になりません。

2. ハッシュテーブルを選びます。ハッシュ関数によってキーが各バケットへほぼ均等に分散される場合、挿入・削除・キーによる存在判定の平均時間計算量はいずれも $O(1)$になります。順序を保つ必要や範囲探索の必要がないため、データの順序性を維持できないというハッシュテーブルの弱点が問題になりません。

3. 先頭から末尾まで直接走査する線形探索を選びます。1回しか探索しない場合、二分探索のためのソート($O(n \log n)$)やハッシュテーブルの構築($O(n)$)でも、最初に配列全体を処理する必要があります。この1回の処理に必要な作業の総量は、直接走査の $O(n)$ よりもむしろ増えてしまいます。

判断の観点

どの方法を選ぶかは、次の要素の掛け合わせで決まります。

  • データがソート済みかどうか
  • 追加のデータ構造を作れるかどうか
  • 探索回数(1回だけか、繰り返しか)
  • 必要な操作の種類(存在判定だけか、挿入・削除・範囲探索も必要か)

探索アルゴリズム再考の章では、この判断を一般化した効率比較表が掲載されており、線形探索(要素探索 $O(n)$・前処理不要)、二分探索(要素探索 $O(\log n)$・ソート前処理 $O(n\log n)$・追加領域 $O(1)$)、木探索・ハッシュ探索($O(\log n)$/$O(1)$・追加構造の維持コストあり)のトレードオフを一覧できます。ハッシュで線形探索を置き換える $O(n) \to O(1)$ の高速化戦略については、ハッシュによる線形探索の置き換えの章で具体例(two_sum.py など)とともに解説されています。

プログラミング演習1:ソート済み配列の二分探索

問題

重複のない昇順整数配列numsと目的の値targetが与えられます。二分探索を使ってtargetを探し、存在すればその配列インデックスを、存在しなければ -1 を返す関数を実装してください(LeetCode の Binary Search 系問題としても出題されている定番内容です)。

解法のヒント(演習より)

  1. 最初の区間はleft = 0right = n - 1とし、区間が空でない条件はleft <= right
  2. 中点はmid = left + (right - left) // 2で計算する。
  3. nums[mid] < targetなら左端をmid + 1へ、nums[mid] > targetなら右端をmid - 1へ移す。等しければ即座に返す。

解答コード

リポジトリの binary_search.py にある両閉区間版の実装は、そのまま本問の模範解答になります。

def binary_search(nums: list[int], target: int) -> int: """二分查找(双闭区间)""" i, j = 0, len(nums) - 1 # 初始化双闭区间 [0, n-1] while i <= j: # 当搜索区间为空(i > j)时跳出 m = (i + j) // 2 # 计算中点索引 m if nums[m] < target: i = m + 1 # target 在区间 [m+1, j] 中 elif nums[m] > target: j = m - 1 # target 在区间 [i, m-1] 中 else: return m # 找到目标元素,返回其索引 return -1 # 未找到目标元素,返回 -1

動作確認用のドライバコードも同ファイルにあり、nums = [1, 3, 6, 8, 12, 15, 23, 26, 31, 35]target = 6に対してインデックス2が返ることを確認できます。

区間の表し方による違い

同じ機能は「左閉右開区間 $[0, n)$」でも実装でき、同ファイルのbinary_search_lcro()がその例です。両者の違いは下表の3点に集約され、コードの初期化・ループ条件・区間の縮小操作がそれぞれ異なります。

項目両閉区間 $[0, n-1]$左閉右開区間 $[0, n)$
初期化i, j = 0, n - 1i, j = 0, n
ループ継続条件i <= ji < j
nums[m] > target時の縮小j = m - 1j = m
区間が空になる条件i > ji == j

両閉区間は左右のポインタ操作が対称でミスを犯しにくいため、二分探索の章では両閉区間の書き方が推奨されています。C 実装では両パターンが binary_search.c のbinarySearchbinarySearchLCROとして対照的に確認できます。

プログラミング演習2:ソート済み配列への挿入位置

問題

重複のない昇順整数配列numsと目的の値targetが与えられます。

  • targetがすでに配列にあれば、そのインデックスを返す。
  • なければ、targetを挿入しても重複のない昇順を保てる位置(挿入位置)を返す。

答えが0になる場合も、配列の長さnに等しくなる場合もあります。二分探索を使って求めてください(LeetCode の Search Insert Position 系問題としても出題されている内容です)。

解法のヒント(演習より)

  1. 答えは 0 の場合も、配列の長さ n の場合もある(先頭挿入と末尾挿入を忘れない)。
  2. 両閉区間を使う場合、nums[mid] >= targetならright = mid - 1としてさらに左の位置を調べる。そうでなければleft = mid + 1
  3. ループ終了時点でleftが挿入位置になっている。

解答コード

ヒント2の「等しいときも右端を詰める」流儀に従うと、次のように書けます。このループでは、nums[m] >= targetのときに $j$ を左へ詰め続けるため、終了時のiは「target 以上の最初の要素」=挿入位置を指します。重複がない配列なら、target が存在する場合はそのインデックスと一致します。

def search_insert(nums: list[int], target: int) -> int: i, j = 0, len(nums) - 1 while i <= j: m = (i + j) // 2 if nums[m] >= target: # target は [i, m-1] 側にある j = m - 1 else: # nums[m] < target なら [m+1, j] 側 i = m + 1 return i # ループ終了時、i が挿入位置

挿入位置探索が「左端探索」の土台になる

なお、リポジトリの binary_search_insertion.py は、重複がない場合をbinary_search_insertion_simple()、重複がある場合をbinary_search_insertion()の2関数で提供しており、ドライバコードで次のように検証できます。

# 无重复元素的数组 nums = [1, 3, 6, 8, 12, 15, 23, 26, 31, 35] # target = 6 → 插入点索引 2 / target = 9 → 插入点索引 4 # 包含重复元素的数组 nums = [1, 3, 6, 6, 6, 6, 6, 10, 12, 15] # target = 2 → 插入点 1 / target = 6 → 插入点 2(最左の 6)/ target = 20 → 插入点 10(末尾)

確認問題2でも触れたとおり、重複要素を持つ配列の挿入位置は「最左の target」そのものであり、二分探索の境界の章では、この性質を利用して左端境界binary_search_left_edge()が実装されています。つまり本演習2は、二分探索を「要素の探索」から「挿入位置の探索」へ一般化する、探索章の要となる練習問題なのです。

演習の復習と次のステップ

本記事で扱った5問の要点を整理します。

演習要点
確認問題1(区間のトレース)中点の大小判定で区間を半分に縮小。$m = i + (j-i)/2$ でオーバーフロー回避。 $O(\log n)$ 回で終了
確認問題2(左右境界)等しい要素を1つ見つけても左右の境界は保証されない。左端はj = m - 1、右端はi = m + 1で詰める
確認問題3(手法の選択)ソート済み静的データ=二分探索、頻繁更新=ハッシュ、1回だけ=線形走査
演習1(二分探索)両閉区間[0, n-1]・ループ条件i <= j・縮小i=m+1 / j=m-1
演習2(挿入位置)nums[m] >= targetで右端を詰め、終了時のiが挿入位置。左端探索の土台

ここまで解き終えたら、ぜひ実際のコードを動かして確認してみてください。『Hello Algo』リポジトリの探索章コードは Python のほか、Java、C、C++、Go、Rust など複数言語で収録されており、言語ごとの型の扱い(整数オーバーフローの有無など)を比較しながら読むと理解がさらに深まります。コードを実行したあとは、左閉右開区間での挿入位置探索への書き換えや、二分探索の境界で紹介されている「target + 1の最左を探して右端を求める」変換テクニックにも挑戦し、探索アルゴリズム再考の比較表を頭に入れた上で、実データに対して「どの探索を使うべきか」を判断できる状態を目指しましょう。

【免费下载链接】hello-algo《Hello 算法》:动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語,提供 Python, Java, C++, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 22:59:30

AI与硬件结合的物理实现:从硅基电路到端侧部署

1. 为什么“AI与硬件结合”不是一句空话&#xff0c;而是正在发生的物理现实最近在帮一家做智能农业监测设备的团队做技术复盘&#xff0c;他们年初上线的土壤墒情分析终端&#xff0c;原本用传统阈值告警逻辑&#xff0c;误报率高达37%。接入轻量级CNN模型跑在STM32H7上之后&a…

作者头像 李华
网站建设 2026/9/8 22:57:31

串口1中断控制LED灯:51单片机串口通信与中断机制详解

简介&#xff1a;面向STM32嵌入式开发者的串口1中断控制LED示例工程&#xff0c;聚焦USART1接收中断与GPIO操作&#xff0c;演示通过接收123字符命令切换LED熄灭、点亮与均匀闪烁&#xff0c;可学习中断机制、串口通信、定时器及GPIO的综合运用。压缩包共172个文件&#xff0c;…

作者头像 李华
网站建设 2026/9/8 22:56:55

腾讯开悟“重返秘境”实战:强化学习导航模型训练全复盘

简介&#xff1a;腾讯开悟-重返秘境模型&#xff08;仅到终点&#xff09;是一份面向强化学习与深度强化学习研究者、参赛者及 AI 算法工程师的项目资源&#xff0c;聚焦 DQN 算法在“重返秘境”场景中的模型设计与推理实现&#xff0c;可帮助读者理解目标网络、检查点保存与配…

作者头像 李华
网站建设 2026/9/8 22:56:54

ResNet-152植物识别模型包:解压、推理与EOCD报错排查

简介&#xff1a;这是一份基于ResNet152的植物病害识别迁移学习资源包&#xff0c;面向深度学习初学者及农业AI应用开发者&#xff0c;解决叶片图像分类与病害诊断问题。压缩包内含约2000个文件&#xff0c;其中以5400余张jpg叶片图像为主&#xff0c;覆盖健康与多种病害状态&a…

作者头像 李华
网站建设 2026/9/8 22:55:57

硬件工程师招新全流程:从需求定义到实操考察的实战复盘

招人这件事&#xff0c;真的比画板子、调电路难多了。我这段时间一直在忙“硬件工程师岗位招新”&#xff0c;前前后后筛了几十份简历、面试了十几个人&#xff0c;越招越觉得这个岗位的门道比很多人想象中要深。硬件工程师不是“会画原理图就行”&#xff0c;也不是“用过两个…

作者头像 李华