周期式による行別直接抽出 - O(n) 時間 / O(1) 空間実装
文字列を指定された行数のジグザグパターンで配置し、各行を左から右へ読み出して連結した文字列を返す問題です。
入力: s = "PAYPALISHIRING", numRows = 3
P A H N
A P L S I I G
Y I R
出力: "PAHNAPLSIIGYIR"
cycle = 2 * (numRows - 1)
時間計算量: O(n) - 各文字を1回ずつ走査
空間計算量: O(1) - 出力バッファを除く補助領域は定数個
最適化手法: ローカル変数束縛、固定長リスト、ビットシフト演算
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
pass
class Solution:
"""
LeetCode 6. Zigzag Conversion
- 競技向け: convert() は高速版を直接呼び出し
- 実務向け: 必要に応じて _validate_inputs() を有効化
"""
def convert(self, s: str, numRows: int) -> str:
"""
文字列を numRows 行のジグザグ配置で並べ替え、行ごとに読み出す。
Args:
s: 変換対象の文字列(長さ 1..1000)
numRows: 行数(1..1000)
Returns:
変換後の文字列
Time: O(n), Space: O(1) (出力バッファ除く)
"""
n: int = len(s)
# 基底条件: ジグザグ不要
if numRows == 1 or numRows >= n:
return s
# 周期: ジグザグの1サイクル = 2*(numRows-1)
cycle: int = (numRows - 1) * 2
out: list[str] = [""] * n
k: int = 0 # 出力リストの書き込み位置
# ローカル束縛で属性アクセスを削減
_s = s
_n = n
_out = out
_cycle = cycle
last_row = numRows - 1
for row in range(numRows):
i = row # 現在行の縦成分の開始インデックス
if row == 0 or row == last_row:
# 端行: 斜め成分なし、周期刻みで縦成分のみ
while i < _n:
_out[k] = _s[i]
k += 1
i += _cycle
else:
# 中間行: 縦成分 + 斜め成分
step_diag = _cycle - (row << 1) # cycle - 2*row
while i < _n:
# 縦成分
_out[k] = _s[i]
k += 1
# 斜め成分(範囲内なら追加)
diag_idx = i + step_diag
if diag_idx < _n:
_out[k] = _s[diag_idx]
k += 1
i += _cycle
return "".join(_out)
フローの説明:
1. 基底条件: numRows が 1 または n
以上の場合、ジグザグ不要なので元の文字列を返却
2. 周期計算: cycle = 2*(numRows-1) でジグザグの周期を計算
3.
行別走査:
各行について、端行(0行目、最終行)は縦成分のみ、中間行は縦+斜め成分を追加
4. 結合: 出力リストを文字列に結合して返却
| 項目 | 値 | 備考 |
|---|---|---|
| 時間計算量 | O(n) | 各文字を1回ずつ走査 |
| 空間計算量 | O(1) | 出力バッファを除く補助領域は定数個 |
| 最適化ポイント | ローカル変数束縛、固定長リスト、ビットシフト演算 | |
| アプローチ | 時間 | 空間 | 評価 |
|---|---|---|---|
| 周期式・直接抽出(本実装) | O(n) | O(1) | ✓ 最適 |
| 行番号タグ付けソート | O(n log n) | O(n) | 不要に重い |
| 2D配置して読み出し | O(n²) | O(n²) | 無駄セル多数で非現実的 |