Skip to content

206. Reverse Linked List #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
May 23, 2025
Merged
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
235 changes: 235 additions & 0 deletions leetcode/206/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# Step 1

Linked List系の問題は、referenceの操作が正しくできるかどうかが測られているので、(dummy以外)新しいNodeを作らない解法が求められているのだろう、と予想する。

# Step 2

変数名がなかなか悩みどころ。

`prev`, `curr`, `next`だと、reverseしたlistにおける"前"や"次"なのか、元のlistにおける"前"や"次"なのか少し混乱する。

Choose a reason for hiding this comment

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

STEP3では改善されていたと思いますが、意味と形式のどちらから名前を付けるか、混乱する場合は両方試して比較してみると良いと思いました。
t0hsumi/leetcode#7 (comment)

Copy link
Owner Author

Choose a reason for hiding this comment

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

参考リンクありがとうございます!


また、処理をどうグループ化するかも悩ましい。while文の中に4つの短い処理をスペース無しで並べると、それらに順序的な関係がないような印象を覚えてしまうので (偏った感覚?)、スペースを入れたいのだが

Choose a reason for hiding this comment

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

個人的には、全体で数行の規模なので空行はそこまで読みやすさに寄与しないかなと思いました。


```python
while node:
# 次に処理するnodeを待避
next_node_to_reverse = node.next

# 繋ぎかえて reversed listを伸ばす
node.next = reversed_head
reversed_head = node

# 処理するnodeを進める
node = next_node_to_reverse
```

と見るか

```python
while node:
# 次に処理するnodeを待避
next_node_to_reverse = node.next

# 処理中のnodeのreferenceをひっくり返す
node.next = reversed_head

# 次に進むためのreferenceの更新
reversed_head = node
node = next_node_to_reverse
```

と見るかで少し悩んだ。

## 他の方々のPRを見る

[h1rosakaさんのPR](https://github.com/h1rosaka/arai60/pull/10)
- helper関数に二つNodeを渡してしまうやり方は考えつかなかったな。この問題に関しては、様々な書き方がありそう。
[potrueさんのPR](https://github.com/potrue/leetcode/pull/7)
- 残されたコメントを見る限り、再帰的な解法では、せっかく問題を分解しているのだから、reverseしたlistの先頭と末尾を返して1つのステップを簡潔にする方法が自然...なのだろうか。
[5ky7さんのPR](https://github.com/5ky7/arai60/pull/8)
[irohafternoonさんのPR](https://github.com/irohafternoon/LeetCode/pull/9)
[garunituleさんのPR](https://github.com/garunitule/coding_practice/pull/7)

## 再帰的な解法

どの解法も納得感があるが、現時点で言語化できない個人的な好みにより、step3はstep1と同様にペアを返す方法で実装することにする。
- 最近、再帰関数でペアを返すことで解ける問題の幅が広いことに気づいた。
- "If the only tool you have is a hammer, it is tempting to treat everything as if it were a nail."

### 別解1

```python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None:
return None
if head.next is None:
return head

reversed_head = self.reverseList(head.next)
head.next.next = head
head.next = None # for reversed tail
return reversed_head
```

### 別解2

```python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
def reverse_list_helper(
Copy link

Choose a reason for hiding this comment

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

この関数が何をするつもりなのか、少し日本語で表現してもらえませんか。
「XXXX と YYYY を受け取って、ZZZZ にしたものを返す」だとすると、「YYYY をひっくり返して、後ろに XXXX をつけたもの」というのであっていますか? 読み取るのが結構大変に思います。

さらに curr_node がひっくり返すと後ろになるということを利用しているなどの事情もあるでしょう。

Copy link
Owner Author

@huyfififi huyfififi May 17, 2025

Choose a reason for hiding this comment

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

ありがとうございます。
はい、「XとYを受け取って、Y以降を反転させ、その末尾にXを繋げたリストの先頭を返す」という理解です。

ひっくり返す操作によって、元のリストのcurrent基準から見たpreviousは、逆順にしたリストの視点からcurrent基準で見るとnextになってしまい、(curr_nodeも同様に)ある行においてどちらの視点で見るべきなのか、自分でも混乱しがちでした。prev_nodeがこの時点ではひっくり返らないのも、一見腑に落ちにくい気がします。

Copy link

Choose a reason for hiding this comment

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

いや、素直に呼ぶと、「続きの部分」と「ひっくり返す部分」を受け取って、「ひっくり返す部分をひっくり返して、続きの部分をくっつける」ではないですか。これを「前」と「今」とか読んでいるんですよ。で、さらに「ヘルパーする」と名前をつけていますね。

「ヘルパー」するとは、次のことである。
「前」と「今」を受取り、「今」がなければ「前」を返す。「今の先頭」と「今の続き」で「ヘルパー」したものを「ひっくり返った頭」とする。今の次を「前」にする。「ひっくり返った頭」を返す。

これは正気ではないでしょう。

「続きの部分」と「ひっくり返す部分」を受取り、「ひっくり返す部分」がなければ「続きの部分を」返す。
「「ひっくり返す部分の続き」をひっくり返して、後ろに「ひっくり返す部分の先頭」をくっつける」をして、「ひっくり返った頭」を手に入れる。「ひっくり返す部分」の後ろに「続きの部分」をつける。「ひっくり返った頭」を返す。

まだ分かります。

Copy link
Owner Author

Choose a reason for hiding this comment

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

少し混乱していました。元のリストの末尾側からひっくり返していくので、この関数で(X, Y)を受け取ると、Xが未処理・続きの部分、Yがひっくり返す部分になり、そのXとYに「前」、「今」と名付けるのはあまりに不適当ですね。
一つ目の引数をrestnext_to_reverse、二つ目の引数をreversing_nodeと名付けるべきだったと思いました。
ヘルパー関数の命名については、今すぐにはうまい代替案が思いつきませんが、少し考えてみます。ありがとうございます!

Copy link

Choose a reason for hiding this comment

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

問題として
reversed_head = reverse_list_helper(curr_node, curr_node.next)
が分からないのです。後ろに足すのが第一引数なのもややこしいです。

いいかはともかく逆方向に極端なことをすると

def reverse_the_first_and_append_the_second(reversing, appending):
    if reversing is None:
        return appending
    reversing_tails = reversing.next
    reversing.next = None
    combined = reverse_the_first_and_append_the_second(reversing_tails, reversing)
    reversing.next = appending
    return combined

読めますか。また、これさらに変形したくなりませんか。

Copy link

Choose a reason for hiding this comment

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

「前」「今」という関数の引数の命名方法、関数の機能とは関係がなくて、自分が典型的にどうやって出会うかですね。そういう名前を引数につけても、あまり読む助けになりません。

料理も手続きなので、どのようにそういった場面でどう説明するかを考えるといいかもしれません。
ナス、トマトのような一般名詞、あるいは、サラダ用の野菜、カレー用の野菜などの用途あたりではないですか。入手場所や方法で呼びませんね。デパートとか通販とか。

Copy link
Owner Author

Choose a reason for hiding this comment

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

料理のアナロジー、ありがとうございます。前の場面のコンテクストを引きずった命名をしてしまったことを反省しています。

いただいたコードですが、前の部分がひっくり返す操作、後ろの部分がくっつける操作になっていて、一つの関数内に二つの責務があることが強調されていると感じました。反転・連結の操作によって頭を揺らされる感覚があったので、関数を分けて操作を分離・またはシグネチャを変えて関数内の操作を単純化したいなと思いました。
違和を感じる能力にまだ課題があり、質問の意図を誤解している気がします。お手数ですが、よろしければもう少しヒントをいただければ嬉しいです。

Copy link

Choose a reason for hiding this comment

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

上のコードはこうしませんか。代入を前に持っていきます。そうするとさらに末尾再帰の形なのでループにできますね。

def reverse_the_first_and_append_the_second(reversing, appending):
    if reversing is None:
        return appending
    reversing_tails = reversing.next
    reversing.next = appending
    return reverse_the_first_and_append_the_second(reversing_tails, reversing)

Copy link
Owner Author

@huyfififi huyfififi May 21, 2025

Choose a reason for hiding this comment

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

1: 末尾再帰の形にできる、2: 末尾再帰はループにできる という思考ステップの回路が繋がっていませんでした。確かに、いただいたコードをループにすることができました。

def reverse_the_first_and_append_the_second(reversing, appending):
    while reversing is not None:
        reversing_tails = reversing.next
        reversing.next = appending
        reversing, appending = reversing_tails, reversing
    return appending

末尾再帰と末尾呼出し最適化について調べる中で、OCamlのList Reversalの実装を確認したところ、いただいた末尾再帰の形のコードとほぼ一致していることに気がつきました。

let rec rev_append l1 l2 =
  match l1 with
    [] -> l2
  | a :: l -> rev_append l (a :: l2)

let rev l = rev_append l []

この問題の裏テーマとして末尾再帰があるように思いました。再帰 -> 末尾再帰 -> ループを意識していこうと思います。辛抱強く付き合っていただいてありがとうございます!

Copy link

Choose a reason for hiding this comment

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

はい。そのコード step1_iterative.py とほぼ同じですね。こういう風に色々な変形が念頭にあって書いてます。

prev_node: Optional[ListNode], curr_node: Optional[ListNode]
) -> Optional[ListNode]:
if curr_node is None:
return prev_node

reversed_head = reverse_list_helper(curr_node, curr_node.next)
curr_node.next = prev_node
return reversed_head

return reverse_list_helper(None, head)
```

# Step 3

Step 3までやってようやくしっくりきたのだが、私のiterativeな解法は元のリストを先頭→末尾に進みながら繋ぎかえが発生するが、recursiveだと元のリストの末尾→先頭の順で繋ぎかえが発生している(Stackを用いているため逆順になるのは当たり前だが)。

# Step 4

## Tail Recursion

末尾再帰 (tail recursion)とは、再帰のパターンの一種で、再帰関数において再起呼び出しが処理の末尾にあり、その戻り値をそのまま関数全体の戻り値として使用しているパターンを指す。
末尾再帰の利点は、通常の再帰と異なり、再起呼び出しの後に追加の計算がないため、呼び出し側のスタックフレームを保持する必要がなくなる点にある。
そうすると、ループ構造に機械的に書き換えられる、または末尾呼び出し最適化(tail call optimization)によってスタックフレームを積まずに済む。

```python
def reverse_the_first_and_append_the_second(reversing, appending):
if reversing is None:
return appending
reversing_tails = reversing.next
reversing.next = appending
return reverse_the_first_and_append_the_second(reversing_tails, reversing)
```

```python
def reverse_the_first_and_append_the_second(reversing, appending):
while reversing is not None:
reversing_tails = reversing.next
reversing.next = appending
reversing, appending = reversing_tails, reversing
return appending
```

### Resources

[Tail recursion - HaskellWiki](https://wiki.haskell.org/index.php?title=Tail_recursion)

> A recursive function is tail recursive if the final result of the recursive call is the final result of the function itself. If the result of the recursive call must be further processed (say, by adding 1 to it, or consing another element onto the beginning of it), it is not tail recursive.
> In many programming languages, calling a function uses stack space, so a function that is tail recursive can build up a large stack of calls to itself, which wastes memory. Since in a tail call, the containing function is about to return, its environment can actually be discarded and the recursive call can be entered without creating a new stack frame. This trick is called tail call elimination or tail call optimisation and allows tail-recursive functions to recur indefinitely.

[3.1.1.5. Tail Recursion · Functional Programming in OCaml](https://courses.cs.cornell.edu/cs3110/2021sp/textbook/data/tail_recursion.html?q=)

[github.com/ocaml - ocaml/stdlib/list.ml](https://github.com/ocaml/ocaml/blob/d325f299896417c5f1d477171135acfdf402e770/stdlib/list.ml#L57)

```ocaml
let rec rev_append l1 l2 =
match l1 with
[] -> l2
| a :: l -> rev_append l (a :: l2)

let rev l = rev_append l []
```

これはOdaさんにいただいた末尾再帰のReverse Linked Listのコードとほぼ一致する。

```python
def reverse_the_first_and_append_the_second(reversing, appending):
if reversing is None:
return appending
reversing_tails = reversing.next
reversing.next = appending
return reverse_the_first_and_append_the_second(reversing_tails, reversing)
```

### Examples

階乗を求めるプログラム(末尾再帰ではない形)。この方法では、再起呼び出しの結果にnをかけている。

```python
def factorial(n: int) -> int:
if n == 0:
return 1
return n * factorial(n - 1)
```

末尾再帰で書くと

```python
def factorial(n: int, accumulated: int = 1) -> int:
if n == 0:
return accumulated
return factorial(n - 1, accumulated * n)
```

この方法では、関数内で最後にやることが再起呼び出しの結果を返すことになっている。

## Feedback

- **再帰 <-> ループ間変形**
- 末尾再帰
- 変数の末尾にコメントを書くとフォーマットが大変なので、素直にdocstringに書いた方がいいかも
- 意味ではなく操作の順番からの命名 (previous) に気を付ける、パズルを作らないように
- 四行くらいしかないなら空行を入れても読みやすさは変わらないかも
- どうせ空行を入れるなら、コメントを積極的に入れてもいいかも
- もう一度書いてみると、確かに空行なくてもいいかな、と思った。仕事だと、長いscript的なものを書くと空行でブロックを区切るが、そこまでの量でもない。
- 私の脳内回路がまだ組み変わっていないのか、書き下すとまだ少し違和感が残るが、少し時間をおいてみよう。
- `reversing`, `appending`という命名
- PEP8の (sparingly) というニュアンスに気づかなかった "Extra blank lines may be used (sparingly) to separate groups of related functions."
- `rest*`, `*_tails`

```python
# Step 4 tail recursion
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
def reverse_append(reversing, appending):
if reversing is None:
return appending
reversing_tails = reversing.next
reversing.next = appending
return reverse_append(reversing_tails, reversing)

return reverse_append(head, None)
```

```python
# Step 4 tail recursion -> iterative
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
appending = None
reversing = head
while reversing:
reversing_tails = reversing.next
reversing.next = appending
reversing, appending = reversing_tails, reversing
return appending
```
19 changes: 19 additions & 0 deletions leetcode/206/step1_iterative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
prev = None
curr = head

while curr:
next_ = curr.next

curr.next = prev
prev = curr

curr = next_

return prev
25 changes: 25 additions & 0 deletions leetcode/206/step1_recursive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
def reverse_list_helper(

Choose a reason for hiding this comment

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

L12 のコメントのせいで見にくくなっていると思いました。以下のように別の行に書くのもありでしょうか。

        def reverse_list_helper(
            node: Optional[ListNode],
        ) -> tuple[Optional[ListNode], Optional[ListNode]]:
            # Returns: reversed_tail, reversed_head

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かに、どうせ返り値の説明を書くなら素直にdocstringを書いた方がいい気がしてきました。ありがとうございます!

node: Optional[ListNode],
) -> tuple[
Optional[ListNode], Optional[ListNode]
]: # reversed_tail, reversed_head
if node is None:
return None, None

if node.next is None:
return node, node

reversed_tail, reversed_head = reverse_list_helper(node.next)
reversed_tail.next = node
node.next = None # to avoid cycle at the end of the list

return node, reversed_head

return reverse_list_helper(head)[1]
19 changes: 19 additions & 0 deletions leetcode/206/step2_iterative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
reversed_head = None
node = head

while node:
next_node_to_reverse = node.next

node.next = reversed_head
reversed_head = node

node = next_node_to_reverse

return reversed_head
24 changes: 24 additions & 0 deletions leetcode/206/step2_recursive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
def reverse_list_helper(
node: Optional[ListNode],
) -> tuple[
Optional[ListNode], Optional[ListNode]
]: # (reversed_tail, reversed_head)
if node is None:
return None, None
if node.next is None:
return node, node

reversed_tail, reversed_head = reverse_list_helper(node.next)

Choose a reason for hiding this comment

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

(私のPR見てくださったようなのでご存知かもですが、)実はtailを受け取らずに解く方法もあります。
https://github.com/h1rosaka/arai60/pull/10/files#diff-f1530fc1072ee1f0b7de99a2e5236992c72355da69982c8ca516fcfba7c57927R122

reversed_tail.next = node
node.next = None

return node, reversed_head

return reverse_list_helper(head)[1]
19 changes: 19 additions & 0 deletions leetcode/206/step3_iterative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
reversed_head = None
node = head

Choose a reason for hiding this comment

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

今注目しているのも、という意味でのnodeという変数名の他にも、これから作業しなければならない列の先頭と言った意味で、restrest_headなどというのも候補になりそうです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます!確かにrest*ならnodeよりも具体的に何を指しているのか情報が載せられていますね。あまり使用例を見たことがないのですが、後で調べてみます!


while node:
next_node_to_reverse = node.next

Copy link

@h1rosaka h1rosaka May 17, 2025

Choose a reason for hiding this comment

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

個人的にはもう少し空行がない方が好みです。参考↓

https://pep8-ja.readthedocs.io/ja/latest/#id11

関数の中では、ロジックの境目を示すために、空行を控えめに使うようにします。

https://google.github.io/styleguide/pyguide.html#35-blank-lines

Copy link
Owner Author

Choose a reason for hiding this comment

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

気になって原文だとどういう表現か見てみたら、確かに

Extra blank lines may be used (sparingly) to separate groups of related functions.

"(sparingly)"という文が入っていました。PEP8の著者たちがどれくらいのニュアンスで入れたのか測りかねますが、意味をあまり持たない空行は無くすべきとのご指摘、その通りだと思います。

Googleのスタイルガイドもありがとうございます!

Use single blank lines as you judge appropriate within functions or methods.

このあたりの感度を高めていけるといいなと思いました。

node.next = reversed_head
reversed_head = node

node = next_node_to_reverse

return reversed_head
Copy link

Choose a reason for hiding this comment

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

個人的な感覚ですが、最近はせっかく空行を入れるならそこにコメントを積極的に入れてもいいような気がしてきました。何かしらの区切りであることはわかるのですが、空行だけだとどういう区切りかが上から読んでいったときにわからないので。(関数名や構文などから明らかにどういう区切りかわかる場合は別ですが!)

Copy link
Owner Author

@huyfififi huyfififi May 17, 2025

Choose a reason for hiding this comment

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

確かに、解いている間はいいのですが、記憶を消してコードを眺めてみるとどういった区切りなのかわかりにくいですね。ハッとさせられる指摘でした、ありがとうございます!

24 changes: 24 additions & 0 deletions leetcode/206/step3_recursive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
def reverse_list_helper(
node: Optional[ListNode],
Copy link

Choose a reason for hiding this comment

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

個人的には、このぐらい複雑な処理になるとnode以上の名前をつけてもいい気がしました。(reversing_nodeなど)

Copy link
Owner Author

Choose a reason for hiding this comment

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

reversing_nodeという変数名いいですね!参考になります

) -> tuple[
Optional[ListNode], Optional[ListNode]
]: # (reversed_tail, reversed_head)
if node is None:
return None, None
if node.next is None:
return node, node

reversed_tail, reversed_head = reverse_list_helper(node.next)
reversed_tail.next = node
node.next = None

return node, reversed_head

return reverse_list_helper(head)[1]