Skip to content
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
124 changes: 124 additions & 0 deletions 703. Kth Largest Element in a Stream/KthLargestElementInAStream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# step1 何も見ずに解く
- PriorityQueueのカテゴリなので一旦それを使う前提で考える
- PriorityQueueを使用したことがないのでリファレンスを参照する
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.

- メソッド名に馴染みが薄い
- `offer`: 要素を追加
- `poll`: 最優先要素を取り出して削除
- 内部的には配列を使用した完全二分木で実装されている

Choose a reason for hiding this comment

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

  • 内部的には配列を使用した完全二分木で実装されている

こちらですが、少し不正確に感じました。

正しくは「配列で表現されたheapというデータ構造を用いて実装されている」となると思います。

優先度付きキュー - Wikipedia

java.util.PriorityQueue が標準クラスライブラリにあり、二分ヒープで実装されている。

heapの知識はSWEの常識に含まれると思うので、もしご存知でなければご確認いただくことをお勧めします。

Copy link
Owner Author

Choose a reason for hiding this comment

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

恥ずかしながら、各タームの定義が曖昧なまま書いていました。
完全二分木だと、優先順位に従って木構造の親子関係が決まっているというPriorityQueueに必要な要素が説明できていないですね。
仰る通りです。ご指摘ありがとうございます!

完全二分木: 各レベルが左から順に埋まっている二分木
二分ヒープ: 完全二分木の形を持ち、親ノードと子ノードの間に大小関係があるデータ構造

- Priorityがn番目に高い要素を取り出そうと思ったらそこまで`poll()`しないと取り出せなさそう
- ということはこの場合は、取り出したい値が常に最優先になるように実装するのが良さそう
- 入力(nums)がk個未満の場合は何を返すのがいいのだろう
- 一番低い点数が返ってくるのが自然に感じる
Comment on lines +10 to +11

Choose a reason for hiding this comment

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

こちら、一案として良いと思いました。

## 解答
- PriorityQueueを利用した実装
```java
class KthLargest {
// k番目に大きい要素までだけを保持
private Queue<Integer> kthLargestScores = new PriorityQueue<>();
Copy link

Choose a reason for hiding this comment

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

PriorityQueue のほうがよいように思います。PriorityQueue としての性質を使っているので。

Copy link
Owner Author

Choose a reason for hiding this comment

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

Interfaceに定義されていないクラス固有の性質に依存している場合は具体的なクラスで型宣言するということですね。
ご指摘ありがとうございます!

private int k;

public KthLargest(int k, int[] nums) {
this.k = k;
for (int num : nums) {
add(num);
}
}

public int add(int val) {
kthLargestScores.offer(val);
if (kthLargestScores.size() > k) {
kthLargestScores.poll();
}
return kthLargestScores.peek();
}
}
```
- ソートされたArrayListを用いる方法
- ArrayListで全ての要素を保持して、k番目の要素を返す
- 毎回ソートすると、コストが大きいので常にソートされた状態にして二分探索で挿入位置を求めるようにしておく
- 最初書いた時comparatorを指定せず、List昇順のままにしていて意図した動作せず
- 後から気づいたが、わざわざ降順にしなくても配列の末尾からk番目の要素を取得すればよかった
- add()の返り値を取得する際に以下のようにする
- `return kthLargestScores.get(kthLargestScores.size() - k);`
```java
class KthLargest {
private List<Integer> kthLargestScores = new ArrayList<>();
private int k;
private Comparator<Integer> desc = Comparator.reverseOrder();

public KthLargest(int k, int[] nums) {
this.k = k;
kthLargestScores.addAll(
Arrays.stream(nums).boxed().sorted(desc).toList()
);
}

public int add(int val) {
int insertIndex = Collections.binarySearch(kthLargestScores, val, desc);
if (insertIndex < 0) {
insertIndex = -(insertIndex + 1);
}
kthLargestScores.add(insertIndex, val);
// indexは0basedのため-1する
return kthLargestScores.get(k - 1);
}
}
```

# step2 他の方の解答を見る
- クラスのプロパティとして`topKScores`という命名が自然だと感じた
- コンストラクタに渡されるkが1以上であることをチェックしている実装もある
- 確かにチェックしてある方が親切かも
## 解答
- 変数名を少し修正、コンストラクで引数チェックを追加
```java
class KthLargest {
private Queue<Integer> topKScores = new PriorityQueue<>();
private int k;

public KthLargest(int k, int[] nums) {
if (k <= 0) {
throw new IllegalArgumentException("k must be larger than 0. k : " + k);
}
this.k = k;
for (int num : nums) {
add(num);
}
}

public int add(int val) {
topKScores.offer(val);
if (topKScores.size() > k) {
topKScores.poll();
}
return topKScores.peek();
}
Comment on lines +89 to +95

Choose a reason for hiding this comment

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

現在は毎回offerした後にsize > kならpollをしていますが、先に.peek()と比較して不要な挿入を避けるとヒープ操作回数を減らせて有利になる気がします。

    public int add(int val) {
        if (minHeap.size() < k) {
            minHeap.offer(val);
        } else if (val > minHeap.peek()) {
            minHeap.poll();
            minHeap.offer(val);
        }
        return minHeap.peek();
    }

Copy link
Owner Author

Choose a reason for hiding this comment

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

複雑性とのトレードオフがどうかなとも思いますが、二分ヒープの内部構造を考えると不要なsiftUp()を撲滅できてとても効果的な提案だと思います!
ありがとうございます!

}
```

# step3 3回ミスなく書く
## 解答
- PriorityQueueを使って実装
- Step2と同様の感じ
- 今回の入力の制限に沿って、入力値チェックは省略
```java
class KthLargest {
private Queue<Integer> topKScores = new PriorityQueue<>();
private int k;

public KthLargest(int k, int[] nums) {
this.k = k;
for (int num : nums) {
add(num);
}
}

public int add(int val) {
topKScores.offer(val);
if (topKScores.size() > k) {
topKScores.poll();
}
return topKScores.peek();
}
}
```

Choose a reason for hiding this comment

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

今回のケースでは、kは最初に与えられたら不変なので、
PriorityQueue初期化時にサイズを指定すると良いかもしれません。
※kが大きいとヒープサイズが成長していく過程で再割当てが繰り返される可能性があるので。

import java.util.PriorityQueue;
import java.util.Queue;

class KthLargest {
    private final Queue<Integer> topKScores;
    private final int k;

    public KthLargest(int k, int[] nums) {
        this.k = k;
        this.topKScores = new PriorityQueue<>(k);  // 初期容量指定
        for (int num : nums) {
            add(num);
        }
    }

    public int add(int val) {
        topKScores.offer(val);
        if (topKScores.size() > k) {
            topKScores.poll();
        }
        return topKScores.peek();
    }
}

Copy link
Owner Author

Choose a reason for hiding this comment

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

Ryotaro25/leetcode_first60#66 (comment)

要するに、reserve を足すと、概ね3マイクロ秒で動くコードを 50 ナノ秒コードを改善したということです。このためにこの一行足す価値ありますか。

以前このやりとりを目にしていたので、capacityの指定は見送りました。
仰る通り「kは最初に与えられたら不変」なので指定しても問題が起きることは少なそうですが。
個人的にはギリギリのパフォーマンスチューニングが求められてから指定すればいいかなという考えです。

Choose a reason for hiding this comment

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

コメントありがとうございます。リンク先の内容、とても勉強になりました。
この問題を「解く」ことよりも、より複雑な業務での運用を見据えて、あえて指定を見送ったということですね。
理解しました。パフォーマンスが特に求められない限り、多人数が関わり、条件も日々変わるような実務では、保守性を重視して個別設定を極力避けた方が、トラブルのリスクを減らせそうです。