diff --git a/347. Top K Frequent Elements.md b/347. Top K Frequent Elements.md new file mode 100644 index 0000000..1453a15 --- /dev/null +++ b/347. Top K Frequent Elements.md @@ -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)`にする必要があり、複雑に感じたので使わなかった。 + +`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)) + + 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)] = {} + 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 + ``` +- dictを用いない場合では、Counterを使い、`most_common`を呼び出す。 + https://docs.python.org/3/library/collections.html#collections.Counter + + +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( + 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] +``` \ No newline at end of file