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
353 changes: 353 additions & 0 deletions heap-priorityQueue/347.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
# 1st
- 問題: [347. Top K Frequent Elements](https://leetcode.com/problems/top-k-frequent-elements/)
- コメント集: [Top k frequent elements](https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/mobilebasic#h.dkkbub5o1tvz)
- 過去に解いたことがあるため、1stのみです
- 方針2つ
- bucketソート
- 時間計算量: 定数落としてO(N), 空間計算量: O(N)
- hashmap で nums にて与えられる数字の出現頻度を数える
- map の key,value を逆にして、List の value 番目に key を入れる方針
- List の value 番目には ArrayList を入れる
- 最後に List を末尾から走査して、k個目まで result に格納したら、result を返す
- PriorityQueue(minHeap)
- 時間計算量: O(n log k). 内訳: add() -> bubble up, poll() -> heapify() が動くため O(log k). これが frequency の n 個に対して動作するため
- 空間計算量: O(N)
- bucketソートと同様
- map で出現頻度を数える
- key, value のうち、value を使って minHeap を作成する
- 比較にラムダ式を用いて、minHeap or maxHeap を決めないと比較できないところが直感的に理解できなかった

## 実装-bucketソート
```java
class Solution {
public int[] topKFrequent(int[] nums, int k) {
int[] result = new int[k];
Copy link

Choose a reason for hiding this comment

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

変数を使用箇所よりだいぶ前で定義をすると、読み手は使用箇所まで変数の定義を覚えていなければならず、短期記憶を圧迫してしまいます。変数は使用箇所の直前で定義することをお勧めいたします。

HashMap<Integer, Integer> frequency = new HashMap<>();
Copy link

Choose a reason for hiding this comment

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

型を Map<Integer, Integer> とするのをよく見かけます。このあたりは所属するチームの平均的な書き方に合わせることをお勧めいたします。


for (int num : nums) {
frequency.put(num, frequency.getOrDefault(num, 0) + 1);
}

List<Integer>[] bucket = new List[nums.length + 1];
for (int key : frequency.keySet()) {
int value = frequency.get(key);
Comment on lines +32 to +33

Choose a reason for hiding this comment

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

keyはnum, valueはfrequencyで良いかもです。
かぶってしまうので、元のfrequencyはnumTo Frequencyとするのはどうでしょう。
map系で、keyToValueという命名の仕方をしている人は多い印象です。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。
そっちの方がいいですね、取り込みます。

if (bucket[value] == null) {
bucket[value] = new ArrayList<Integer>();
}
bucket[value].add(key);
}

int idx = 0;
Copy link

Choose a reason for hiding this comment

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

こちらのコメントをご参照ください。
hemispherium/LeetCode_Arai60#10 (comment)

for (int i = bucket.length - 1; i >= 0 && idx < k; i--) {

Choose a reason for hiding this comment

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

idxがkになったら、breakされるように書かれているので、idx < kはなくても良いですかね。

List<Integer> mostFrequent = bucket[i];
if (mostFrequent != null) {
for (int key : mostFrequent) {
result[idx] = key;
idx++;
if (idx == k) {
break;
}
}
}
}
return result;
}
}
```

## minHeap
```java
class Solution {
public int[] topKFrequent(int[] nums, int k) {
int[] result = new int[k];

HashMap<Integer, Integer> frequency = new HashMap<>();
for (int num : nums) {
frequency.put(num, frequency.getOrDefault(num, 0) + 1);
}

PriorityQueue<Map.Entry<Integer, Integer>> minHeap = new PriorityQueue<>((a, b) -> a.getValue() - b.getValue());

Choose a reason for hiding this comment

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

データ構造ではなく、実際に入っている中身で変数名を決めると良いかもです。

私ならk_topsと名付けます。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。
そうですね、取り込みます。


for (Map.Entry<Integer, Integer> map : frequency.entrySet()) {
minHeap.add(map);
if (minHeap.size() > k) {
minHeap.poll();
}
}

for (int i = 0; i < k; i++) {
result[i] = minHeap.poll().getKey();
}

return result;
}
}
```

## minHeap を何も見ずに実装してみる

### 1回目
```java
class MinHeap {
// heap は int[] 配列とする
int[] heap;
// insert, pop, 時にheapの数を管理する
int size = 0;
// 初期容量を覚えておく
int maxSize;

public MinHeap(int maxSize) {
this.heap = new int[maxSize + 1];
this.heap[0] = Integer.MIN_VALUE;
this.maxSize = maxSize;
size++;
}

public int pop() {
if (heap[1] != null) {
int result = heap[1];
heap[1] = heap[size - 1];
heap = heap[size - 1];
heapify(1);
} else {
throw new ArrayIndexOutOfBoundsException("minheap don't have root");
}
}

public int peek() {
// heap[1]を返すだけ, 一応nullチェック
if (heap[1] != null) {
return heap[1];
} else {
throw new ArrayIndexOutOfBoundsException("minheap don't have root");
}
}

public void add(int element) {
// size == maxValue となった時、heapのサイズは heap[size++] として拡張する
if (isFull()) {
// size が maxSize+1 と同値になった時 heap[] が格納できるサイズ上限に達したのでサイズを増やす
heap = heap[size + 1];
}
heap[size] = element;
// minHeap なので、末尾に加える。親と比較して小さいならswapするを、再帰的に行う
// この書き方はちょっと冗長かな。。別関数に切り出した方が良いかも。。
int index = size;
int parentIndex = parentIndex(index);
while (heap[index] < heap[parentIndex]) {
swap(index, parentIndex);
index = parentIndex;
parentIndex = parentIndex(index);
}
size++;
}

private boolean isFull() {
return size == maxSize + 1;
}

private void heapify(int index) {
if (!hasLeftChild(index)) {
return;
}

int smallestIndex = leftChildIndex(index);
if (heap[rightChildIndex(index)] < heap[leftChildIndex(index)]) {
smallestIndex = rightChildIndex(index);
}
if (heap[index] < heap[smallestIndex]) {
swap(index, smallestIndex);
heapify(smallestIndex);
}
}

private int parentIndex(int index) {
return index / 2;
}

private int leftChildIndex(int index) {
return index * 2;
}

private int rightChildIndex(int index) {
return index * 2 + 1;
}

private boolean hasLeftChild(int index) {
return heap[leftChildIndex(index)] != null;
}

private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
}
```

### Claude に以下レビューを受けたので2回目で修正
① size++ をコンストラクタに入れている
番兵は要素ではないので size は 0 のままでいいです。

```java
public MinHeap(int maxSize) {
this.heap = new int[maxSize + 1];
this.heap[0] = Integer.MIN_VALUE;
this.maxSize = maxSize;
// size++ は不要
}
```

② int に null チェックはできない
先週も出てきましたね。int はプリミティブなので != null はコンパイルエラーです。空チェックは size で行います。
```java// ❌
if (heap[1] != null)

// ✅
if (size > 0)

③ pop と heap の代入がおかしい
java// ❌ heap に heap[size-1] の値を代入している
heap = heap[size - 1];

// ✅ sizeを減らすだけでいい
heap[1] = heap[size--];
```

④ 動的拡張の書き方がおかしい
```java// ❌ heap に heap[size+1] は代入できない
heap = heap[size + 1];

// ✅ Arrays.copyOf で新しい配列を作る
heap = Arrays.copyOf(heap, heap.length * 2);
```
⑤ heapify の比較が逆
```java// ❌ 親が小さい時にswapしてしまう
if (heap[index] < heap[smallestIndex])

// ✅ 親が大きい時にswapする
if (heap[index] > heap[smallestIndex])
```

⑥ hasLeftChild が null チェックになっている
```java// ❌
return heap[leftChildIndex(index)] != null;


// ✅
return leftChildIndex(index) <= size;
```

⑦ heapify で hasRightChild チェックが抜けている
```java// 右子が存在する場合だけ比較する
if (hasRightChild(index) && heap[rightChildIndex(index)] < heap[leftChildIndex(index)]) {
smallestIndex = rightChildIndex(index);
}

修正点は多いですが、設計の流れ(add の Bubble Up、heapify の構造)は正しく理解できています!修正して貼ってみてください。
```

### 2回目
```java
class MinHeap {
// heap は int[] 配列とする
int[] heap;
// insert, pop, 時にheapの数を管理する
int size = 0;
// 初期容量を覚えておく
int maxSize;

public MinHeap(int maxSize) {
this.heap = new int[maxSize + 1];
this.heap[0] = Integer.MIN_VALUE;
this.maxSize = maxSize;
// size++; 番兵は要素ではないのでカウントアップしない
}

public int pop() {
// if (heap[1] != null) { int にnullチェックはできない
if (size > 0) {
int result = heap[1];
heap[1] = heap[size];
size--;
heapify(1);
return result;
} else {
throw new ArrayIndexOutOfBoundsException("minheap don't have root");
}
}

public int peek() {
// heap[1]を返すだけ, 一応nullチェック
if (size > 0) {
return heap[1];
} else {
throw new ArrayIndexOutOfBoundsException("minheap don't have root");
}
}

public void add(int element) {
// size == maxValue となった時、heapのサイズは heap[size++] として拡張する
if (isFull()) {
// size が maxSize+1 と同値になった時 heap[] が格納できるサイズ上限に達したのでサイズを増やす
heap = Arrays.copyOf(heap, heap.length * 2);
maxSize = heap.length - 1;
}
heap[++size] = element;
// minHeap なので、末尾に加える。親と比較して小さいならswapするを、再帰的に行う
// この書き方はちょっと冗長かな。。別関数に切り出した方が良いかも。。
int index = size;
int parentIndex = parentIndex(index);
while (heap[index] < heap[parentIndex]) {
swap(index, parentIndex);
index = parentIndex;
parentIndex = parentIndex(index);
}
}

private boolean isFull() {
return size == maxSize + 1;
}

private void heapify(int index) {
if (!hasLeftChild(index)) {
return;
}

int smallestIndex = leftChildIndex(index);
if (hasRightChild(index) && heap[rightChildIndex(index)] < heap[leftChildIndex(index)]) {
smallestIndex = rightChildIndex(index);
}
if (heap[index] > heap[smallestIndex]) {
swap(index, smallestIndex);
heapify(smallestIndex);
}
}

private int parentIndex(int index) {
return index / 2;
}

private int leftChildIndex(int index) {
return index * 2;
}

private int rightChildIndex(int index) {
return index * 2 + 1;
}

private boolean hasLeftChild(int index) {
return leftChildIndex(index) <= size;
}

private boolean hasRightChild(int index) {
return rightChildIndex(index) <= size;
}

private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
}
```