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
73 changes: 73 additions & 0 deletions 206_reverse_linked_list/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 206. Reverse Linked List

https://leetcode.com/problems/reverse-linked-list/

## Comments

### step1

* まあ典型的な問題なのでさらっと。
* 久々に再開したので `ListNode*` (ポインタ) を書くべきところを `ListNode` にしていた

### step2

* `nullptr` との比較 (while の条件) は暗黙評価でよかった。
* 前にも聞いたことあったけど Gemini に聞いてみた

> 3 の `!= nullptr` についての指摘ですが、たとえば Python などでは、暗黙に bool に変換するよりも `is None` / `is not None` など明示的に `None` と比較するほうがよいとされています (Google Python Style Guide など)。なぜ C++ では暗黙に評価するほうがよいのですか。

これは非常に的を射た指摘です。この違いは、まさに Python と C++ の設計思想(Zen)の違いを象徴しています。

あなたが言う通り、Python の "Zen" (PEP 20) には「Explicit is better than implicit.(暗黙的より明示的が良い)」という有名な一文があります。is not None は、この哲学を完璧に体現しています。

ではなぜ C++ では while (node) のような暗黙的な評価が好まれるのか。理由は主に2つあります。

1. C 言語から受け継いだ「慣習(イディオム)」

C++ のこの文化は、その前身である C 言語から直接受け継がれています。

C 言語の「NULL」は「0」だった:

C 言語には bool 型がなく、0 を「偽」、それ以外(非ゼロ)を「真」として扱いました。NULL マクロは、一般的に 0(または (void*)0)として定義されていました。

「ポインタ != 0」がイディオムになった:

そのため、while (ptr != 0)(つまり while (ptr != NULL))と書くよりも、単に while (ptr) と書く方が「ptr が 0 (NULL) でない間」という意味で簡潔かつ直接的でした。

C++ への継承:

C++ はこの慣習をそのまま引き継ぎました。C++11 で型安全な nullptr が導入されましたが、nullptr も bool に変換される際は false として評価されます。したがって、while (node) という書き方は、C/C++ プログラマにとって数十年にわたる「ポインタが null でないことを確認する」ための標準的なイディオム(慣用句)であり続けています。

2. Google Style Guide の「例外」

あなたの指摘は、実は Google のスタイルガイド内部の比較においても非常に興味深い点です。

Python (Explicit):

Google Python Style Guide は if x: ではなく if x is not None: を推奨します。これは「空のリスト []」と None を区別するなど、明示性を重んじるためです。

C++ (Implicit):

一方、Google C++ Style Guide は、ポインタに関しては Python とは逆の方針を推奨しています。

Null, 0, and ''

...

Use if (ptr) and if (!ptr) for nullptr checks.

(訳: nullptr のチェックには if (ptr) と if (!ptr) を使いなさい。)

Google という同じ組織が、言語ごとに異なる(一見、矛盾するような)ガイドラインを設けているのです。これは、while (node) が C++ のコミュニティにおいていかに「ネイティブな」表現であるかを強く裏付けています。C++ においては、while (node != nullptr) と書くほうが、むしろ冗長で「C++ らしくない」と見なされる傾向があります。

まとめ

Python: 「明示的 is 善」。is not None

C++: 「C からの伝統的なイディオムと簡潔性 is 善」。while (node)

私のレビューは、この C++ の強い慣習に基づいています。!= nullptr と書くことは、コードの正しさには何の影響もありませんが、C++ の「流儀」からは少し外れている、と見なされることが多いのです。

### step3

* 典型問題すぎるので省略
24 changes: 24 additions & 0 deletions 206_reverse_linked_list/step1.cpp
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini に投げてみたらまあまあ怒られた (nullptr の件はこれで気づいて修正した)

このコードで改善すべきところはありますか。否定的にレビューしてください。


このコードは、アルゴリズムとしては標準的で効率的ですが、コードの品質という観点からは多くの問題点を指摘できます。

  1. 変数名が不適切かつ一貫性に欠ける

    • node という変数名は、情報量がゼロに等しい最悪の命名です。リンクリストを扱うコードでは、ほとんどの変数がノード(またはノードへのポインタ)を指します。node という名前は、それが「どの」ノードなのか全く示していません。
    • ループで「現在処理中のノード」を指すのであれば、currentcurr といった、その役割を明確に示す名前を使うべきです。
    • previous_nodenext_node では _node というサフィックスを使っているのに、中心となるはずの node にはそれがないため、命名規則に一貫性がありません。prev, curr, next のように簡潔に統一するか、previousNode, currentNode, nextNode のように(C++ではキャメルケースやスネークケースが好まれますが)スタイルを統一すべきです。
  2. 説明責任の放棄(コメントの欠如)

    • このコードは、リンクリストの反転操作における核心部分(ポインタの付け替え)について、一切の説明を放棄しています。
    • ループ内の以下の4行は、特定の順序で実行される必要があります。
      ListNode* next_node = node->next; // (1)
      node->next = previous_node;       // (2)
      previous_node = node;             // (3)
      node = next_node;                 // (4)
    • なぜ (1) が最初に来る必要があるのか((2) で node->next を上書きすると、次のノードへの参照が失われるため)、 のようなポインタの移動がどのように行われるのか、コメントが一切ありません。
    • ロジックが自明であると仮定するのは傲慢であり、特にポインタ操作は間違いやすいため、各ステップの意図を明記すべきです。
  3. C++の慣習に対する無頓着

    • while (node != nullptr) という条件は冗長です。C++ではポインタ型が暗黙的に bool に変換されるため(nullptr であれば false、それ以外は true)、while (node) と書く方が簡潔であり、より C++ らしい書き方です。!= nullptr と明記するのは、時として可読性を助ける場合もありますが、この文脈では冗長に映ります。
  4. エッジケースの考慮がコードから読み取れない

    • このコードは結果的に headnullptr(空のリスト)の場合や、要素が1つの場合でも正しく動作します。
    • しかし、headnullptr の場合に nodenullptr になり、即座にループが終了して previous_nodenullptr)が返る、という流れがコードから直感的に読み取れません。
    • 関数の冒頭で if (head == nullptr || head->next == nullptr) のようなガード節(早期リターン)を追加する方が、コードの意図が明確になり、読み手も安心して主要なロジック(ループ部分)を読むことができます。

総じて、このコードは「ただ動くだけ」のレベルであり、他者(あるいは未来の自分)が容易に理解し、保守できるような「良いコード」とは言えません。

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* previous_node = nullptr;
ListNode* node = head;
while (node != nullptr) {
ListNode* next_node = node->next;
node->next = previous_node;
previous_node = node;
node = next_node;
}
return previous_node;
}
};
24 changes: 24 additions & 0 deletions 206_reverse_linked_list/step2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* previous_node = nullptr;
ListNode* node = head;
while (node) {
ListNode* next_node = node->next;
node->next = previous_node;
previous_node = node;
node = next_node;
}
return previous_node;
}
};
Empty file.