From f8718bb0d01f743db55a483114e3d9c101cf35ad Mon Sep 17 00:00:00 2001 From: mike <59136831+Mike0121@users.noreply.github.com> Date: Thu, 1 Aug 2024 20:30:05 +0900 Subject: [PATCH 1/2] Create 35. Search Insert Position.md https://leetcode.com/problems/search-insert-position/description/ --- .../35. Search Insert Position.md" | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 "\347\253\266\346\212\200\343\203\227\343\203\255\345\260\261\346\264\273\351\203\250PR\347\224\250/35. Search Insert Position.md" diff --git "a/\347\253\266\346\212\200\343\203\227\343\203\255\345\260\261\346\264\273\351\203\250PR\347\224\250/35. Search Insert Position.md" "b/\347\253\266\346\212\200\343\203\227\343\203\255\345\260\261\346\264\273\351\203\250PR\347\224\250/35. Search Insert Position.md" new file mode 100644 index 0000000..d8d66cb --- /dev/null +++ "b/\347\253\266\346\212\200\343\203\227\343\203\255\345\260\261\346\264\273\351\203\250PR\347\224\250/35. Search Insert Position.md" @@ -0,0 +1,233 @@ +``` +範囲を絞っていく、というお題なわけですよね。 +left = 0 +right = len(nums) - 1 +と書いたら、left と right の両端を含む、この範囲にある、ということを考えていますね。 +言い換えると、「target はあるとすると、left 以上、right 以下に必ずある」ということです。 + +「target はあるとすると、10以上10以下にあるんだよねー。」といわれたら、10確認しろよ、ってなりますよね。 +だから、両端を含む場合は、同じ値でも確認しないといけません。 + +また、middle として、100を選んで、そこになかった場合。そこよりも左か、そこよりも右にあるかが、値次第で分かるわけです。 + +left, right は両端を含むわけですから、99以下にあることか101以上にあることかが分かるわけですね。だから1を足し引きします。 + + middle の選び方は、left <= middle <= right であれば、この議論だとどこでも大丈夫なはずです。区間は、最低1減っていきますから。 + +これが頭にあれば、それほど抵抗なく書けませんか? + +ちなみに、現実的には、たぶん、個数が50個くらい以下ならば、ループで頭から探したほうが速いでしょう。(少なくとも C++ では。) +分岐予測との兼ね合いです + +left = 0 +right = len(nums) + +とすることもできて、そうすると、左は含むが右は含まないつもりで書いているわけですね。left 以上 right 未満。 +「target はあるとすると、10以上10未満にあるんだよねー。」といったらそんな数はありません。 + +また、middle として、100を選んで、そこになかった場合。そこよりも左か、そこよりも右にあるかが、値次第で分かるわけです。 + +要は、100未満にあることか101以上にあることかが分かるわけですね。 + +middle の選び方は、left <= middle < right に変わります。 + +というわけで、注目ポイント次第ですね。開区間、半開区間、閉区間とかいったりします。 +個数が50個くらい以下ならば、ループで頭から探したほうが速いでしょう。 +``` + +``` +コードはいいと思います。 + +これ、どう考えるといいかなあと思っています。 + +n 個要素があると、植木算で n + 1 個の切れ目がありますね。 + +そのうち、どこで切ると、 +右はすべて、target <= nums[i] で、 +左はすべて、nums[i] < target となるか、ということですね。 +これを answer とでもしましょう。 + +だから、left と right は実は閉区間で、一致するまで回さないといけませんね。 + +mid = (left + right) // 2 + +とすると、切り捨てられるので、 +left <= mid < right +になります。 + +nums[mid] < target +が判明すると、 +mid < answer が分かります。 +一方、 +target <= nums[mid] +が判明すると、 +answer <= mid +が分かりますね。 + +この辺、どう考えていますか? +``` + +1. 閉区間 ↔︎ a ≤ x ≤ b ↔︎ left = 0, right = len(nums) - 1 + 以上、以下の範囲で探索を行うため右端を調査対象に含むべき ↔︎ while left <= right +2. 半開区間 ↔︎ a ≤ x < b ↔︎ left = 0, right = len(nums) + 以上、より小さいの範囲で探索を行うため右端を調査対象に含まない ↔︎ while left < right + +* かなり悩んでしまった。bisect_leftの値に重複がないパターンを実装すべき問題ということには気がついた。 +* 手で解いてみて、調整方法を検討して一応通せた。 +* 二分探索を理解できていないことに気がついたので、Discord内の議論を参考に復習した。 +* 半開区間のleftは、left = rightかつ、重複がなければ挿入されるべき位置に収束する。 +* まだ、"境界"の感覚がわかっていないので、この後の問題を通して理解したい。 + + +``` +[0, 1, 3, 5, 6], target = 2 +nums = [0, 1, 3, 5, 6] (left, right) = (0, 4) +nums = [0, 1] (left, right) = (0, 2) +nums = [1, 3] (left, right) = (1, 2) +``` + + +### 全探索による解法 (1m52s) +* 時間計算量: O(N) +* 空間計算量: O(1) + +```python +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + for i in range(len(nums)): + if nums[i] >= target: + return i + + return len(nums) +``` + + +### 半開区間による解法 +### 1回目 (14m13s) +* 時間計算量: O(logN) +* 空間計算量: O(1) + +```python +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + + left, right = 0, len(nums) - 1 + + while left <= right: + mid = (left + right) // 2 + value = nums[mid] + + if value == target: + return mid + + if value > target: + right = mid - 1 + + if value < target: + left = mid + 1 + + return left +``` + + +### 2回目 +* ifの分岐が対称性があるため、if-elif-elseに修正 +* valueという変数名がわかりづらいこともあるが、nums[mid]のままの方が読みやすいと感じたため修正 + +```python +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + left, right = 0, len(nums) + + while left < right: + mid = (left + right) // 2 + + if nums[mid] == target: + return mid + elif nums[mid] < target: + left = mid + 1 + else: + right = mid + + return left +``` + + +### 3回目 +* 2回目から変化なし +* midが略記の認識がある場合を考慮し、一応middleに。 +```python +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + left, right = 0, len(nums) + + while left < right: + middle = (left + right) // 2 + + if nums[middle] == target: + return middle + elif nums[middle] < target: + left = middle + 1 + else: + right = middle + + return left +``` + + + +## bisect_leftによる解法 +* ドキュメント: https://docs.python.org/3/library/bisect.html +* 実装: https://github.com/python/cpython/blob/cfbdce72083fca791947cbb18114115c90738d99/Lib/bisect.py#L74 + +### 半開区間 +```python +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + def _is_sorted(arr: List[int]) -> bool: + for i in range(1, len(arr)): + if arr[i - 1] > arr[i]: + return False + return True + + def bisect_left(arr: List[int], value: int) -> int: + if _is_sorted(arr) == False: + raise ValueError("Input array is not sorted!") + + left, right = 0, len(arr) + while left < right: + mid = (left + right) // 2 + if arr[mid] >= value: + right = mid + else: + left = mid + 1 + return left + + return bisect_left(nums, target) +``` + +### 閉区間 +```python +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + def _is_sorted(arr: List[int]) -> bool: + for i in range(1, len(arr)): + if arr[i - 1] > arr[i]: + return False + return True + + def bisect_left(arr: List[int], value: int) -> int: + if not _is_sorted(arr): + raise ValueError("Input array is not sorted!") + + left, right = 0, len(arr) - 1 + while left <= right: + mid = (left + right) // 2 + if arr[mid] >= target: + right = mid - 1 + else: + left = mid + 1 + return left + + return bisect_left(nums, target) +``` From f062747058f617718ccc8613cbe4d06d44f72b55 Mon Sep 17 00:00:00 2001 From: mike <59136831+Mike0121@users.noreply.github.com> Date: Sat, 24 Aug 2024 16:32:45 +0900 Subject: [PATCH 2/2] Update 35. Search Insert Position.md --- .../35. Search Insert Position.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/\347\253\266\346\212\200\343\203\227\343\203\255\345\260\261\346\264\273\351\203\250PR\347\224\250/35. Search Insert Position.md" "b/\347\253\266\346\212\200\343\203\227\343\203\255\345\260\261\346\264\273\351\203\250PR\347\224\250/35. Search Insert Position.md" index d8d66cb..a074ff7 100644 --- "a/\347\253\266\346\212\200\343\203\227\343\203\255\345\260\261\346\264\273\351\203\250PR\347\224\250/35. Search Insert Position.md" +++ "b/\347\253\266\346\212\200\343\203\227\343\203\255\345\260\261\346\264\273\351\203\250PR\347\224\250/35. Search Insert Position.md" @@ -197,7 +197,7 @@ class Solution: left, right = 0, len(arr) while left < right: mid = (left + right) // 2 - if arr[mid] >= value: + if arr[mid] >= value: # 等号を外した場合(else側に等号を持っていった場合)、bisect_rightになる。 right = mid else: left = mid + 1