-
Notifications
You must be signed in to change notification settings - Fork 0
703. Kth Largest Element in a Stream #23
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
base: main
Are you sure you want to change the base?
Changes from all commits
aa879b6
82e8b70
bb152d3
76d83a6
0446b49
05a11a7
ca4d756
e13b7a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
## Problem | ||
|
||
https://leetcode.com/problems/kth-largest-element-in-a-stream/ | ||
|
||
## Step 1 | ||
|
||
5 分程度答えを見ずに考えて、手が止まるまでやってみる。 | ||
何も思いつかなければ、答えを見て解く。ただし、コードを書くときは答えを見ないこと。 | ||
動かないコードも記録する。 | ||
正解したら一旦 OK。思考過程もメモする。 | ||
|
||
### Approach 1. Heap を使った方法 | ||
|
||
- わからなかったので回答を見て作成。ヒープはこれまで慣れてなかったのでこの問題でしっかりめにインプットした。時間があれば自分で実装したいところだが一旦後回し | ||
- PriorityQueue | ||
- https://docs.oracle.com/javase/8/docs/api/java/util/PriorityQueue.html | ||
- k 番目に大きい値だけを求めるために、サイズ k の最小ヒープを作り「常に上位 k 個の中で最小の値(= k 番目に大きい値)を根に持つ」よう管理するという方法。 | ||
|
||
時間計算量: 各 add が O(log k) → 合計 O(n log k) | ||
空間計算量: O(k) | ||
|
||
```java | ||
class KthLargest { | ||
private final PriorityQueue<Integer> minHeap; | ||
private final int k; | ||
|
||
public KthLargest(int k, int[] nums) { | ||
this.k = k; | ||
minHeap = new PriorityQueue<>(k); | ||
|
||
for (int num : nums) { | ||
add(num); | ||
} | ||
} | ||
|
||
public int add(int val) { | ||
if (minHeap.size() < k) { | ||
minHeap.offer(val); // add | ||
} else if (minHeap.peek() < val) { | ||
minHeap.poll(); // remove root | ||
minHeap.offer(val); | ||
} | ||
|
||
return minHeap.peek(); | ||
} | ||
} | ||
``` | ||
|
||
上記に対していただいたコメント | ||
|
||
- https://github.com/katsukii/leetcode/pull/23/files#r2068473153 | ||
- > この if-else if 文を読んでいて、else のケースが大丈夫なのかなというのを考えるのに少し時間が取られたのでもう少し素直に書ける余地があるかなと思います。 | ||
- > とりあえず queu に突っ込んでしまって、要素がサイズを超えていれば、減らしてあげるみたいな感じのほうがシンプルかなと個人的には思います。 | ||
- ```java | ||
public int add(int val) { | ||
scores.offer(val); | ||
if (scores.size() > k) { | ||
scores.poll(); | ||
} | ||
return scores.peek(); | ||
} | ||
``` | ||
- たしかにこちらの方がわかりやすい | ||
|
||
## Step 2 | ||
|
||
他の方が描いたコードを見て、参考にしてコードを書き直してみる。 | ||
参考にしたコードのリンクは貼っておく。 | ||
読みやすいことを意識する。 | ||
他の解法も考えみる。 | ||
|
||
### Approach 2. TreeMap を使った方法 | ||
|
||
- https://github.com/Ryotaro25/leetcode_first60/pull/9/files#r1619710596 | ||
- > この問題で priority_queue にいきなりいくのは、私は実は違和感があります。 | ||
- > 平衡二分木が C++ だったら map があり、これは順番に並んでいます。 | ||
- 平衡二分探索木 | ||
- https://ja.wikipedia.org/wiki/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E6%9C%A8 | ||
- Java だと TreeMap がそれに当たるっぽい(赤黒木) | ||
|
||
- TreeMap にスコアを Key、当該スコア個数を Value として保存 | ||
|
||
```java | ||
class KthLargest { | ||
private final int k; | ||
private TreeMap<Integer, Integer> scores; | ||
|
||
public KthLargest(int k, int[] nums) { | ||
this.k = k; | ||
this.scores = new TreeMap<>(); | ||
|
||
for (int num : nums) { | ||
scores.put(num, scores.getOrDefault(num, 0) + 1); | ||
} | ||
} | ||
|
||
public int add(int val) { | ||
|
||
scores.put(val, scores.getOrDefault(val, 0) + 1); | ||
int rank = 0; | ||
for (int key : scores.descendingKeySet()) { | ||
rank += scores.get(key); | ||
if (rank >= k) { | ||
return key; | ||
} | ||
} | ||
return -1; // dummy | ||
} | ||
} | ||
``` | ||
|
||
- 上記の方法で試したところ、大量の add が走るテストケースで Time Limit Exceeded エラーとなった。要素削除がないため add のたびに scors が増え続けること原因 | ||
- 以下は 常に Top k に相当する要素だけを残すようにサイズを k に保つ | ||
- k 番目に大きい要素は TreeMap の最小 Key に該当する | ||
|
||
時間計算量: コンストラクタが O(n log k)、add 単発が O(log k)なので O(n log k) | ||
空間計算量: O(k) | ||
|
||
```java | ||
class KthLargest { | ||
private final int k; | ||
private TreeMap<Integer, Integer> scores; | ||
private int scoreCount; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 自分なら numScores と名付けると思います。チームの平均的な書き方に合わせることをお勧めいたします。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ありがとうございます。たしかにそうですね。意識してみます。 |
||
|
||
public KthLargest(int k, int[] nums) { | ||
this.k = k; | ||
this.scores = new TreeMap<>(); | ||
this.scoreCount = 0; | ||
for (int num : nums) { | ||
add(num); | ||
} | ||
} | ||
|
||
public int add(int val) { | ||
if (scoreCount < k) { | ||
scores.put(val, scores.getOrDefault(val, 0) + 1); | ||
scoreCount++; | ||
} else { | ||
int kthScore = scores.firstKey(); | ||
if (val > kthScore) { | ||
scores.put(val, scores.getOrDefault(val, 0) + 1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 私はこの put と getOrDefault を一行に書くのは好みではないです。 val を2回書かないならば compute を使うようなのもありますが、素直に2行にするのも一つです。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. たしかに素直に2行にした方が断然わかりやすいですね。今後そうします。 |
||
updateScoreCount(kthScore); | ||
} | ||
} | ||
return scores.firstKey(); | ||
} | ||
|
||
private void updateScoreCount(int key) { | ||
int count = scores.get(key); | ||
if (count == 1) { | ||
scores.remove(key); | ||
} else { | ||
scores.put(key, count - 1); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
上記に対しいただいたコメント | ||
|
||
- https://github.com/katsukii/leetcode/pull/23/files#r2065049233 | ||
|
||
- > 自分なら numScores と名付けると思います。チームの平均的な書き方に合わせることをお勧めいたします。 | ||
- たしかに個数を表すなら num◯◯ の方が共通認識としてわかりやすいのはあるかもしれない | ||
|
||
- https://github.com/katsukii/leetcode/pull/23/files#r2067722954 | ||
- > `scores.put(val, scores.getOrDefault(val, 0) + 1);` | ||
- > 私はこの put と getOrDefault を一行に書くのは好みではないです。 | ||
- > val を 2 回書かないならば compute を使うようなのもありますが、素直に 2 行にするのも一つです。 | ||
- たしかに 2 行にした方が見やすい。今後気をつける | ||
- ```java | ||
int count = scores.getOrDefault(val, 0) + 1; | ||
scores.put(val, count); | ||
``` | ||
|
||
## Step 3 | ||
|
||
今度は、時間を測りながら、もう一回書く。 | ||
アクセプトされたら消すを 3 回連続できたら問題は OK。 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. step3 が一番読みやすかったです。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ありがとうございます。 |
||
|
||
```java | ||
class KthLargest { | ||
private PriorityQueue<Integer> scores; | ||
private final int k; | ||
|
||
public KthLargest(int k, int[] nums) { | ||
this.k = k; | ||
this.scores = new PriorityQueue<>(); | ||
for (int num : nums) { | ||
this.add(num); | ||
} | ||
} | ||
|
||
public int add(int val) { | ||
if (scores.size() < k) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. このif-else if文を読んでいて、elseのケースが大丈夫なのかなというのを考えるのに少し時間が取られたのでもう少し素直に書ける余地があるかなと思います。 とりあえずqueuに突っ込んでしまって、要素がサイズを超えていれば、減らしてあげるみたいな感じのほうがシンプルかなと個人的には思います。 public int add(int val) {
scores.offer(val);
if (scores.size() > k) {
scores.poll();
}
return scores.peek();
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. たしかにこちらの方が分かりやすいです。ありがとうございます。 |
||
scores.offer(val); | ||
} else if (scores.peek() < val) { | ||
scores.poll(); | ||
scores.offer(val); | ||
} | ||
return scores.peek(); | ||
} | ||
} | ||
``` | ||
|
||
## Step 4 | ||
|
||
コメントいただいて実装した | ||
|
||
### Approach 3.ソート配列 | ||
|
||
時間計算量: O(n log n) | ||
空間計算量: O(k) | ||
|
||
- https://github.com/katsukii/leetcode/pull/23/files#r2065369258 | ||
- > 最初はソートで k 番目のスコアを求める、それを保持しつつ新しいスコアと比べて更新するとかでもこの問題は問題ないのでしょうか | ||
- たしかこれでも解法としてありえそう | ||
|
||
- コンストラクタ: 空の配列をメンバ変数として用意し、for 文で nums の要素数分 add を呼び出す | ||
- add: 引数の val を配列の適切な位置に挿入したあと要素数が k 個になるように調整し index 0 を返す | ||
- `Collections.binarySearch()` でソート済配列のどの位置に挿入されるか特定可能 | ||
|
||
```java | ||
class KthLargest { | ||
private final int k; | ||
private List<Integer> sortedList; | ||
public KthLargest(int k, int[] nums) { | ||
this.k = k; | ||
this.sortedList = new ArrayList<>(); | ||
|
||
for (int num : nums) { | ||
add(num); | ||
} | ||
} | ||
|
||
public int add(int val) { | ||
int insertPosition = Collections.binarySearch(sortedList, val); | ||
if (insertPosition < 0) { // if val isn't in sortedList | ||
insertPosition = -(insertPosition + 1); | ||
} | ||
|
||
sortedList.add(insertPosition, val); | ||
|
||
if (sortedList.size() > k) { | ||
sortedList.remove(0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. L237でbinarySearchを使っているのは、この配列がソート済みなことを利用して https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ジャストアイデアですが、降順にソートしてあげて削除は末尾要素にすれば解決するかなと思いました。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ご指摘ありがとうございます。おっしゃるとおりO(k)ですね。
たしかに、削除の部分の時間計算量は削除がO(1)になるため平均時間計算量はマシになりますね。 |
||
} | ||
return sortedList.get(0); | ||
} | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
この問題を解いたことないところからの質問失礼します。
上記priority_queueとmapの議論も読ませてもらったのですが、問題の要求的に
1.どうやってk番目の順位のスコアを知るか
2.新しいスコアが入ってきたときの適切な更新
の2つの要求があって、1と2どちらも解決できるデータ構造としてpriority_queueとmapどっちがいいか(まずよく使われるMapを思い出してもいいのでは)という議論だと認識してます。
しかしもっと単純にもしそれらのライブラリを知らなかったら、1を求めるために最初はソートでk番目のスコアを求める、それを保持しつつ新しいスコアと比べて更新するとかでもこの問題は問題ないのでしょうか
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
コメントありがとうございます。仰るとおりコメントいただいた解法でも問題ないと思います。
以下に追加実装してみました。
https://github.com/katsukii/leetcode/pull/23/files#diff-2cbd596e05763f14077e25b7a93421a6edb70d86913362e022332db1f0073c34R179