Skip to content
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

Add 703. Kth Largest Element in a Stream.md #8

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
199 changes: 199 additions & 0 deletions 703. Kth Largest Element in a Stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# step 1

思いついた解法(n = これまで入力された要素数とする)
- sortされたリスト (要素数k分だけ昇順で記録し、list[0]でアクセス)
- init
- time complexity: O(n log(n))
- space complexity: O(n) (Aux: O(1))
- add
- time complexity: O(k)
- space complexity: O(k) (Aux: O(1))
- sortされていないリスト (全ての要素を記録し、quickselect)
- init
- time complexity: O(1)
- space complexity: O(n) (Aux: O(1))
- add
- time complexity: O(n^2) (average: O(n))
- space complexity: O(n) (Aux: O(1))
- heap (要素数k分だけ記録し、heap[0]でアクセス)
- init
- time complexity: O(n log(k)) (heappushをn回繰り返す場合)
- space complexity: O(k)
- add
- time complexity: O(log(k))
- space complexity: O(k) (Aux: O(1))

heapを使った解法
```python
import heapq


class KthLargest:

Copy link

Choose a reason for hiding this comment

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

class の次の行は空けないことが多い気がしますが趣味の範囲でしょう。

def __init__(self, k: int, nums: List[int]):
self.heap: List[int] = []
self.k = k
for num in nums:
if len(self.heap) < self.k:
Copy link

Choose a reason for hiding this comment

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

len(self.heap) == self.k のときに push されないように見えます。

Copy link
Owner Author

Choose a reason for hiding this comment

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

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

とりあえずpushをして、kを超える要素数があればpopする方がみよいですね。

heapq.heappush(self.heap, num)
continue
heapq.heappushpop(self.heap, num)

def add(self, val: int) -> int:
heapq.heappush(self.heap, val)
if len(self.heap) == self.k + 1:
heapq.heappop(self.heap)
return self.heap[0]
```

$1 <= k <= nums.length$という問題設定に依存している回答になった。`add()`において、KthLargestが存在しない場合、これまでの最小値を返す様にしている。

`__init__()`にて、heapを作るのに、以下の手法を思いついた。
- heappushをn回繰り返す(O(n log(k)))
- sortして、長さk分だけ取り出す(O(n log(n)))
- heapfyして、n-kだけheappopする(O(n + (n-k)log(n)))


quickselectを使った解法
```python
import random


class KthLargest:

def __init__(self, k: int, nums: List[int]):
self.array = nums
self.k = k

def add(self, val: int) -> int:
def quickselect(
array: List[int],
left: int,
right: int,
index: int
) -> int:
pivot_index = random.randint(left, right)
array[pivot_index], array[right] = array[right], array[pivot_index]

pivot, less_index = array[right], left
for i in range(left, right):
if array[i] <= pivot:
array[i], array[less_index] = array[less_index], array[i]
less_index += 1
array[less_index], array[right] = array[right], array[less_index]
if less_index == index:
return array[less_index]
elif less_index < index:
return quickselect(array, less_index + 1, right, index)
else:
return quickselect(array, left, less_index - 1, index)

self.array.append(val)
length = len(self.array)
return quickselect(self.array, 0, length - 1, length - self.k)
```
partitionを左右から狭めていくタイプのやり方を実装しようとして、10分ほど考えて、うまく動かなかったので、https://www.youtube.com/watch?v=XEmy13g1Qxc&ab_channel=NeetCode を見た。
定数倍遅い解法だがquickselectの参考となった。

addを呼ぶ回数の多いテストケースにてTLEした。

# step 2
- [quicksortのpartiton](https://www.geeksforgeeks.org/hoares-vs-lomuto-partition-scheme-quicksort/)
- pythonのlistのlength:
- https://github.com/python/cpython/blob/cef0a90d8f3a94aa534593f39b4abf98165675b9/Include/cpython/listobject.h#L30-L35
- https://github.com/python/cpython/blob/main/Objects/listobject.c#L299-L309
- 構造体に長さを記録しているので、よばれる度にループを回して要素数を数えているわけではない
- https://github.com/rinost081/LeetCode/pull/9/files
- 関数を呼び出す側としては、ある関数を呼び出したときに、渡した引数の中身が変更されることは、あまり想定しないように思います。関数内では原則引数の中身を変更しないことをお勧めいたします。また、特別な理由があって変更する場合は、関数コメント等に明示的にその旨を書くことをお勧めいたします。
- python3.11のsortアルゴリズム (Timsort -> Powersort)
https://www.i-programmer.info/news/216-python/15954-python-now-uses-powersort.html
- https://github.com/BumbuShoji/Leetcode/pull/9
- (heapの解法にて):heapの要素数がself.kよりも長くならないことはクラス全体を見ないと分からない実装になっている
- https://github.com/konnysh/arai60/pull/8/files

思ったこと
- 変数名について、heapを用いる解法では`kth_largest`とかそういう名前が多かった。確かに、`heapq.heappush`とかやっているので`heap`だけでは不十分だと感じた。
- `__init__()`にて`add()`を呼ぶ解法も多かった。コードの重複がないので、その解法の方が、コンパクトに収まる様に感じた。

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/heapq.html#heapq.heappushpop

heapq の標準ドキュメントを見たことがなければざっと目を通しておいてください。たとえば、heappushpop などがあったりします。dict を使うたびに見る、みたいなのはやりすぎだとは思いますが、時々目を通すといいでしょう。

参考: https://note.com/nir29/n/n025ddc315a2e

Copy link

Choose a reason for hiding this comment

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

(おっと、heappushpop 使っていましたね。)

partitionにてHoaresをやろうとして、off-by-one errorが取れなかった。
引数が何で、何をするということの言語化がちゃんとできていないことが原因だと思う。日常に即した具体例とか作れると良さそう(思いつかない)。

```python
import heapq


class KthLargest:

def __init__(self, k: int, nums: List[int]):
if k <= 0 or k > len(nums) + 1:
Copy link

Choose a reason for hiding this comment

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

if not 0 < k <= len(nums):
のほうが分かりやすいと思います。

raise ValueError(f'Invalid input value k = {k}')
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
そうですね。これは ValueError が適切と思います。

self.kth_largest_scores: List[int] = []
self.k: int = k
for num in nums:
self.add(num)

def add(self, val: int) -> int:
heapq.heappush(self.kth_largest_scores, val)
while len(self.kth_largest_scores) > self.k:
heapq.heappop(self.kth_largest_scores)
return self.kth_largest_scores[0]
```

# step 3
```python
import heapq


class KthLargest:

def __init__(self, k: int, nums: List[int]):
if k < 1 or k > len(nums) + 1:
raise ValueError(f'Invalid argument: '
f'KthLargest class constructor value k = {k}')
self.k: int = k
self.kth_largest_scores: List[int] = []
for num in nums:
self.add(num)

def add(self, val: int) -> int:
heapq.heappush(self.kth_largest_scores, val)
while len(self.kth_largest_scores) > self.k:
heapq.heappop(self.kth_largest_scores)
return self.kth_largest_scores[0]
```

# step 4
コメントまとめ
- クラスの次の行は開けないことが多い気がする
- `if k < 1 or k > len(nums) + 1:`よりは、`if not 1 <= k <= len(nums) + 1:`のほうがわかりやすい。
- [builtin exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)

メモ
- [timsort](https://hal.science/hal-01212839)
- [powersort](https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.ESA.2018.63)
- https://github.com/python/cpython/blob/48c70b8f7dfd00a018abbac50ea987f54fa4db51/Objects/listsort.txt


```python
import heapq


class KthLargest:

def __init__(self, k: int, nums: List[int]):
if not 1 <= k <= len(nums) + 1:
raise ValueError(
"KthLargest constructor: k is a invalid size for nums: "
f"k = {k}, nums = {nums}"
)
self.k: int = k
self.kth_largest_heap: List[int] = []
for num in nums:
self.add(num)

def add(self, val: int) -> int:
heapq.heappush(self.kth_largest_heap, val)
while len(self.kth_largest_heap) > self.k:
heapq.heappop(self.kth_largest_heap)
return self.kth_largest_heap[0]
```