diff --git a/0238.Product-of-Array-Except-Self/memo.md b/0238.Product-of-Array-Except-Self/memo.md new file mode 100644 index 0000000..f0ce75d --- /dev/null +++ b/0238.Product-of-Array-Except-Self/memo.md @@ -0,0 +1,116 @@ +# 238. Product of Array Except Self + +## step1 + +prefix product (?)(累積和の積バージョン)を前向き、逆向きで計算すればO(n)で計算できる。 +自力で解けた。推敲前の回答を step1_first_sol.pyに残しておく。 + +累積和のときにはitertools.accumulateを使えたので、今回の場合に使えないか調べたところ、予想通り演算子を指定できるようだった。これを利用する: step1.py。 + +https://docs.python.org/ja/3/library/itertools.html#itertools.accumulate + +さらにlistの無駄な再生成を避けるようにすると、実行速度が上がった。(64ms->23ms) + +## step2 + +> Follow up: Can you solve the problem in O(1) extra space complexity? (The output array does not count as extra space for space complexity analysis.) + +同じ配列を使いまわすようにすればこれも可能だ。ただし、itertools.accumulateは使えなくなる(と思う)。 + +二巡で行うのと一巡で行う解法が考えられるが、二巡の解法の方が速かった。 + +この原因を考えると、おそらく次のような理由であろう。 + +1. キャッシュ効率: 二巡する方は配列を順に辿るため効率が良い + +2. 負のindexの計算コスト + + +## Pythonコードが実行される仕組み(内部メカニズム) + +以下 LLM に聞いたこと: + +Pythonは一般的に「インタプリタ言語」と呼ばれますが、ソースコードを直接1行ずつ読み込んで実行しているわけではありません。内部では**「コンパイル(バイトコードへの変換)」**と**「インタプリタによる実行」**の2つのステップを踏んでいます。 + +--- + +### 全体の実行フロー + +Pythonコード(`.py`)が実行され、結果が出力されるまでの大まかな流れは以下の通りです。 + +```text +[ソースコード (.py)] + │ + ▼ (1. 構文解析 & コンパイル) +[バイトコード (.pyc)] + │ + ▼ (2. 仮想マシンへの読み込み) +[Python仮想マシン (PVM)] ── (ランタイム / C言語レイヤー) + │ + ▼ (3. 機械語への翻訳・実行) +[CPU / OS] + +``` + +### 各ステップの詳細 +1. コンパイル・コンポーネント(ソースコードからバイトコードへ) +人間が書いた .py ファイルは、まずPythonのコンパイラによって「バイトコード(Bytecode)」と呼ばれる中間コードに翻訳されます。 + +バイトコードとは: +人間には読めない、Python仮想マシン(PVM)だけが理解できるプラットフォーム(OS)に依存しない命令セットです。 + +.pyc ファイルの生成: +コンパイルされたバイトコードは、2回目以降の実行を高速化するために __pycache__ ディレクトリ内に .pyc ファイルとしてキャッシュされます。 + +2. Python仮想マシン(PVM)の駆動 +生成されたバイトコードは、PVM(Python Virtual Machine) に送り込まれます。 +一般的に使われているPython(CPython)の実体はC言語で書かれたプログラムであり、このPVMが中心となって動作します。 + +3. インタプリタ・コンポーネント(評価ループ) +PVM内部の「評価ループ(Evaluation Loop)」と呼ばれる仕組みが、バイトコードの命令を上から1つずつ取り出します。 + +#### 動的型付けの解決(オーバーヘッドの発生源): +Pythonは実行時まで変数の型が分かりません。そのため、命令を1つ実行するたびに「このデータは整数か?文字列か?」という型チェックや、メモリの割り当て、参照カウントの増減などを毎回愚直に行います。 + +#### 機械語への翻訳: +型チェックなどを経て、最終的にOSやCPUが理解できる「機械語(ネイティブコード)」にその場で翻訳され、実行されます。 + +#### なぜPythonはコンパイル言語(C等)より遅いのか? +実行時のオーバーヘッドが重い +C言語などは事前にすべて機械語に翻訳し、型も確定した状態で実行するため一瞬で終わります。一方のPythonは、実行中に「型チェック」「インデックスの境界チェック」「メモリ管理」をバイトコード1行ごとに毎回行うため、どうしても処理が遅くなります。 + +#### JIT(Just-In-Time)コンパイルが標準ではない +標準のPython(CPython)は純粋なインタプリタベースです。実行頻度の高い部分を自動的に機械語へコンパイルして保持する「JITコンパイラ(PyPyなどで採用)」が標準では搭載されていないため、ループ処理などが比較的苦手という特徴があります。 + +## 他の人のコード + +- https://hayapenguin.com/notes/LeetCode/238/ProductOfArrayExceptSelf +- https://github.com/TaisukeFujise/leetcode_tafujise/pull/7/ +- https://github.com/huyfififi/coding-challenges/pull/37/changes + + +(違う問題の話) + +https://discord.com/channels/1084280443945353267/1200089668901937312/1200403156073455646 + +> IEEE754 の定義と挙動を確認しました。IEEEのドキュメントは有料なんですね。 + +> IEEE754にはQuiet NaNとSignaling NaNの2つのNaNが定義されているがpythonでは両者を区別しないことも初めて知りました + +- Quiet NaN: 例外(エラー)を発生させず、静かに計算結果を NaN として伝播させる。指数部がすべて 1、仮数部の最上位ビットが 1。 +- Signaling NaN: 遭遇した瞬間に、CPUに無効な操作の例外(シグナル)を発生させる。指数部がすべて 1 仮数部の最上位ビットが 0(かつ、それ以外に 1 がある)。 + +### Pythonでは両者を区別しない + +Pythonで float('nan') を生成すると、基本的にはすべて Quiet NaN として扱われ、sNaNを直接生成したり、sNaNによってPythonの例外(ValueError など)を発生させたりすることは通常できません。これには主に2つの理由があります。 + +#### 理由①:Pythonの「エラーハンドリング」の設計思想 +Pythonは、C言語レイヤーやハードウェア(CPU)が発生させるシグナルやトラップに、直接依存したくないという設計思想を持っています。 + +C言語などでは、sNaNに触れると「浮動小数点例外(SIGFPE)」というOSレベルのシグナルが発生し、最悪の場合プログラムが強制終了します。Pythonはこのような低レイヤーのクラッシュを嫌います。Pythonでゼロ除算(1 / 0)をしたときにプログラムが強制終了せず、綺麗に ZeroDivisionError というPythonの例外として捕まえられるのも、Pythonが低レイヤーの挙動をあらかじめ包み込んで隠蔽してくれているからです。 + +#### 理由②:CPython(標準Python)の実装による制限 +Pythonの float 型は、裏側ではC言語の double 型(64bit浮動小数点数)をそのまま使っています。 +PythonがC言語の機能を使って str から float に変換したり計算を行ったりする際、C言語の標準ライブラリやコンパイラ、CPUの挙動によって sNaN が自動的に qNaN に変換(消去)されてしまう ケースがほとんどです。 + +そのため、Pythonのレイヤー(私たちが書くコードのレベル)に届く頃には、すべて「静かな」Quiet NaNになってしまっているのです。 diff --git a/0238.Product-of-Array-Except-Self/step1.py b/0238.Product-of-Array-Except-Self/step1.py new file mode 100644 index 0000000..8c4b6b3 --- /dev/null +++ b/0238.Product-of-Array-Except-Self/step1.py @@ -0,0 +1,21 @@ +import itertools +import operator + + +# listの再生成を伴う +class Solution: + def productExceptSelf(self, nums: list[int]) -> list[int]: + prefix_poduct = itertools.accumulate([1] + nums[:-1], operator.mul) + suffix_poduct = list(itertools.accumulate([1] + nums[:0:-1], operator.mul))[ + ::-1 + ] + + return [p * s for p, s in zip(prefix_poduct, suffix_poduct)] + + +class Solution: + def productExceptSelf(self, nums: list[int]) -> list[int]: + prefix_poduct = itertools.accumulate([1] + nums[:-1], operator.mul) + suffix_poduct = itertools.accumulate([1] + nums[:0:-1], operator.mul) + + return [p * s for p, s in zip(prefix_poduct, reversed(tuple(suffix_poduct)))] diff --git a/0238.Product-of-Array-Except-Self/step1_first_sol.py b/0238.Product-of-Array-Except-Self/step1_first_sol.py new file mode 100644 index 0000000..58c2fc1 --- /dev/null +++ b/0238.Product-of-Array-Except-Self/step1_first_sol.py @@ -0,0 +1,20 @@ +class Solution: + def productExceptSelf(self, nums: list[int]) -> list[int]: + prefix_products = [] + prefix_product = 1 + for n in nums: + prefix_products.append(prefix_product) + prefix_product *= n + reversed_prefix_products = [] + reversed_prefix_product = 1 + for n in nums[::-1]: + reversed_prefix_products.append(reversed_prefix_product) + reversed_prefix_product *= n + + product_except_self = [] + for i in range(len(nums)): + product_except_self.append( + prefix_products[i] * reversed_prefix_products[len(nums) - 1 - i] + ) + + return product_except_self diff --git a/0238.Product-of-Array-Except-Self/step2_no_extra_space.py b/0238.Product-of-Array-Except-Self/step2_no_extra_space.py new file mode 100644 index 0000000..99dd41d --- /dev/null +++ b/0238.Product-of-Array-Except-Self/step2_no_extra_space.py @@ -0,0 +1,30 @@ +class Solution: + def productExceptSelf(self, nums: list[int]) -> list[int]: + product_except_self = [1] * len(nums) + + prefix_product = 1 + for i in range(len(nums)): + product_except_self[i] *= prefix_product + prefix_product *= nums[i] + + suffix_product = 1 + for i in range(len(nums) - 1, -1, -1): + product_except_self[i] *= suffix_product + suffix_product *= nums[i] + + return product_except_self + + +class Solution: + def productExceptSelf(self, nums: list[int]) -> list[int]: + product_except_self = [1] * len(nums) + + prefix_product = 1 + suffix_product = 1 + for i in range(len(nums)): + product_except_self[i] *= prefix_product + product_except_self[-1 - i] *= suffix_product + prefix_product *= nums[i] + suffix_product *= nums[-1 - i] + + return product_except_self