Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions 0238.Product-of-Array-Except-Self/memo.md
Original file line number Diff line number Diff line change
@@ -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になってしまっているのです。
21 changes: 21 additions & 0 deletions 0238.Product-of-Array-Except-Self/step1.py
Original file line number Diff line number Diff line change
@@ -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)))]
20 changes: 20 additions & 0 deletions 0238.Product-of-Array-Except-Self/step1_first_sol.py
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions 0238.Product-of-Array-Except-Self/step2_no_extra_space.py
Original file line number Diff line number Diff line change
@@ -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