Skip to content

Add 347. Top K Frequent Elements.md #9

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
213 changes: 213 additions & 0 deletions 347. Top K Frequent Elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# step 1
frequencyを数える->frequencyを降順にソートする->値の大きいものから取り出してリストを作成が基本的な流れだと思った。

frequencyを数える作業をどうするか迷った。とりあえず思いついたのは、与えられた`nums`をソートして
それに対してループで走査してfrequencyを数える方法。`nums`が与えられているので、`sorted_numbers`ではなく`sorted_nums`と略しても許容範囲かと判断した。与えられた`nums`からfrequencyを数える時にも、frequencyの大きいものを順に取り出す時にもheapは使えるが、計算量が変わらない上にpythonのデフォルトはmin-heapなので`(-1 * number_of_appears, number)`にする必要があり、複雑に感じたので使わなかった。

Choose a reason for hiding this comment

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

計算量が変わらない上にpythonのデフォルトはmin-heapなので(-1 * number_of_appears, number)にする必要があり、複雑に感じたので使わなかった。

min-heap を max-heap として無理やり使うのが複雑であるというのは僕も同意です。(今回は使用しないという判断なので関係ないですが)仮に使う場合はコメントを残すことは必須かなと個人的には思っています。


`n = nums.length`として、
- time complexity: O(n log(n))
- space complexity: O(n)
```python
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
sorted_nums: List[int] = sorted(nums)
frequency: List[Tuple(int, int)] = []
index: int = 0
while index < len(sorted_nums):
number: int = sorted_nums[index]
number_of_appears: int = 0
while index < len(sorted_nums) and sorted_nums[index] == number:
index += 1
number_of_appears += 1
frequency.append((number_of_appears, number))

Choose a reason for hiding this comment

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

個人的な好みのお話で、ここの while - while について他の書き方もしてみたくなりました。
下記、ループの外側で最後の追加をしていて微妙ですが、こんな感じにも書けそうです。

        if len(sorted_nums) == 0:
            return []
        number: int = sorted_nums[0]
        number_of_appears: int = 1
        for i in range(1, len(sorted_nums)):
            if sorted_nums[i] == number:
                number_of_appears += 1
                continue
            frequency.append((number_of_appears, number))
            number = sorted_nums[i]
            number_of_appears = 0
        frequency.append((number_of_appears, number))

Copy link
Owner Author

Choose a reason for hiding this comment

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

コメントありがとうございます。

こちらの方はネストが少ない分の読みやすさがありますね。


frequency.sort(reverse=True)
kth_most_frequent_elements: List[int] = []
for i in range(k):
kth_most_frequent_elements.append(frequency[i][1])
return kth_most_frequent_elements
```

上を解き終わったあとでdictを使う方法も思いついた。

`occurences`と`frequency`という似た様な意味の変数が並んでいる。
`sorted_frequency`とかにしようかと思ったが、常にsortされているわけでもないので適さないと考えた。

`n = nums.length`、`m`を`nums`内のunique elementsの数として、
- time complexity: O(n + m log(m))
- space complexity: O(n)
```python
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
occurrences: dict[Tuple(int, int)] = {}

Choose a reason for hiding this comment

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

Type Hintが違うような気がします。 dict[int, int]かなと。
https://docs.python.org/3/library/typing.html#generics

Copy link
Owner Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。間違ってますね。

自分の方でも確認しました。https://docs.python.org/3/library/stdtypes.html#types-genericalias:~:text=Another%20example%20for,their%20second%20argument%3A

for num in nums:
if num in occurrences:
occurrences[num] += 1
else:
occurrences[num] = 1

frequency: List[Tuple(int, int)] = []
for number, number_of_appears in occurrences.items():
frequency.append((number_of_appears, number))

frequency.sort(reverse=True)
kth_most_frequent_elements: List[int] = []
for i in range(k):
kth_most_frequent_elements.append(frequency[i][1])
return kth_most_frequent_elements
```
# step 2
- https://github.com/katataku/leetcode/pull/9
- 短期記憶限界は4-7くらい。それくらいで抑えられる様に書く
- https://docs.python.org/3/library/stdtypes.html#dict.setdefault
- 問題設定に依存した書き方をしない。今回で言うとkの値
https://github.com/tarinaihitori/leetcode/pull/9/files#r1816996368
> 問題設定は必ず k 種類あることになっていますが、なかった場合にどうするかは考えておいてください。
特別な値を返すか、Exception を投げるか、短いものでも返すか、プログラムを止めるか、そのあたりです。
- `num_to_count`の様に変数にどの様な値が含まれるか想像できる様な変数名にすべき
- https://github.com/tarinaihitori/leetcode/pull/9
- ループないで使わない変数は変数名をつけずに`_`としてしまう。
- quickselectを使っている解法もあった。k番目の要素を見つける過程で、partitionにてk番目より大きいものと小さいものに分けている。
- https://github.com/thonda28/leetcode/pull/17/files
- ちゃんとkの値が適切でない場合の処理がされていた。
`raise RuntimeError(f"The number of answers is fewer than the required {k}.")`

- dictを用いる場合の初期値設定
- [dictのmethodを用いる](https://docs.python.org/3/library/stdtypes.html#dict.setdefault)
- [defaultdict](https://docs.python.org/3/library/collections.html#collections.defaultdict)
- 書き方を工夫する
```python
if num not in occurrences:
occurrences[num] = 1
else:
occurrences[num] += 1
Comment on lines +80 to +83

Choose a reason for hiding this comment

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

以下書き方のほうが個人的には好きです。

Suggested change
if num not in occurrences:
occurrences[num] = 1
else:
occurrences[num] += 1
if num not in occurrences:
occurrences[num] = 0
occurrences[num] += 1

```
- dictを用いない場合では、Counterを使い、`most_common`を呼び出す。
https://docs.python.org/3/library/collections.html#collections.Counter
Copy link

Choose a reason for hiding this comment

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

いいですね。
せっかくなので実装も見ておきましょう。何を使っていて自分のと比べてどう思いますか。
https://github.com/python/cpython/blob/main/Lib/collections/__init__.py

Copy link
Owner Author

Choose a reason for hiding this comment

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

共通点

相違点

思ったこと
今回は数字の出現頻度を数える問題だったので、それに特化したCounterの方が余計にheapを用意する必要もなく、見やすい様に感じました。
ただ、もともと用意されているCounterのmethodを用いた実装と、defaultdictで足りない出現頻度の高い値を求める部分を自分でheapを使って調べた実装とでやっていることがそう大きくは変わらない印象も受けました。



defaultdictを使う解法
```python
from collections import defaultdict
import heapq


class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
num_to_count = defaultdict(int)
for num in nums:
num_to_count[num] += 1

if not 0 < k <= len(num_to_count):
raise ValueError(
Copy link

Choose a reason for hiding this comment

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

https://docs.python.org/3/library/exceptions.html#ValueError
時々、どのような Exception が定義されているか眺めてみましょう。

f"The number of unique elements in nums is fewer than k: "
f"nums = {nums}, k = {k}"
)

top_k_frequent = []
for number, count in num_to_count.items():
heapq.heappush(top_k_frequent, (count, number))
if len(top_k_frequent) > k:
heapq.heappop(top_k_frequent)
return [num for _, num in top_k_frequent]
```

```python
from collections import Counter


class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
num_to_count = Counter()
for num in nums:
num_to_count[num] += 1

if not 0 < k <= len(num_to_count):
raise ValueError(
f"The number of unique elements in nums is fewer than k: "
f"nums = {nums}, k = {k}"
)

top_k_frequent = num_to_count.most_common(k)
return [num for num, _ in top_k_frequent]
```

# step 3
step 2の例外処理のエラーメッセージだと、k < 1の時に適切でなかった。

```python
from collections import defaultdict
import heapq


class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
num_to_count = defaultdict(int)
for num in nums:
num_to_count[num] += 1

if not 1 <= k <= len(nums):
raise ValueError(f"Invalid number k = {k}")

top_k_frequent = []
for number, count in num_to_count.items():
heapq.heappush(top_k_frequent, (count, number))
if len(top_k_frequent) > k:
heapq.heappop(top_k_frequent)
return [num for _, num in top_k_frequent]
```

# step 4
コメントまとめ
- dictのtype annotation: https://docs.python.org/3/library/typing.html#generics
- defaultdictとCounterの比較など: https://github.com/t0hsumi/leetcode/pull/9#discussion_r1881199623


counterの構築は、`Counter(nums)`で済む。
```python
from collections import Counter


class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
num_to_count = Counter(nums)

if not 1 <= k <= len(num_to_count):
raise ValueError(
f"topKFrequent(): k is out of range: "
f"k = {k}, nums = {nums}"
)

top_k_frequent = num_to_count.most_common(k)
return [num for num, _ in top_k_frequent]
```

必要な時だけpushを行うheappushpopによる最適化を利用する場合、以下の様に書ける(https://github.com/python/cpython/blob/3.13/Lib/heapq.py#L163-L168).
最悪のシナリオ(`num_to_count`が昇順にiterateする場合)でのパフォーマンスはかわらない。
個人的にはstep 3とさして読みやすさも変わらないので、害も益もない最適化に思った。

```python
from collections import defaultdict
import heapq


class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
num_to_count = defaultdict(int)
for num in nums:
num_to_count[num] += 1

if not 1 <= k <= len(num_to_count):
raise ValueError(
"topKFrequent(): k is out of range: "
f"nums = {nums}, k = {k}"
)

top_k_frequent = []
for num, count in num_to_count.items():
if len(top_k_frequent) < k:
heapq.heappush(top_k_frequent, (count, num))
else:
heapq.heappushpop(top_k_frequent, (count, num))
return [num for _, num in top_k_frequent]
```