diff --git a/heap-priorityQueue/347.md b/heap-priorityQueue/347.md new file mode 100644 index 0000000..2bcec36 --- /dev/null +++ b/heap-priorityQueue/347.md @@ -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]; + HashMap frequency = new HashMap<>(); + + for (int num : nums) { + frequency.put(num, frequency.getOrDefault(num, 0) + 1); + } + + List[] bucket = new List[nums.length + 1]; + for (int key : frequency.keySet()) { + int value = frequency.get(key); + if (bucket[value] == null) { + bucket[value] = new ArrayList(); + } + bucket[value].add(key); + } + + int idx = 0; + for (int i = bucket.length - 1; i >= 0 && idx < k; i--) { + List 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 frequency = new HashMap<>(); + for (int num : nums) { + frequency.put(num, frequency.getOrDefault(num, 0) + 1); + } + + PriorityQueue> minHeap = new PriorityQueue<>((a, b) -> a.getValue() - b.getValue()); + + for (Map.Entry 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; + } +} +``` \ No newline at end of file