-
Notifications
You must be signed in to change notification settings - Fork 0
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
Top K Frequent Elements #20
base: main
Are you sure you want to change the base?
Conversation
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.
コードを見てコメントしました。GoでHeapを使う場合はInterfaceの実装が必要なんですね。
} | ||
topK := make([]int, 0, k) | ||
for i := len(countToNum) - 1; i >= 0 && len(topK) < k; i-- { | ||
topK = append(topK, countToNum[i]...) |
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.
特に問題はないですが、この解法だけ一般にはkを超える配列を返却する可能性があるな、と思いました。
(Leetcodeの制約では It is guaranteed that the answer is unique. なので、解として間違っているという事ではありません)
※Goのmakeの3つ目の引数のキャパシティは、要素数が増えたら勝手にスケールするんですね。
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.
GoでHeapを使う場合はInterfaceの実装が必要なんですね。
はい、おっしゃる通りです。なので、特に使わないメソッドについてもインターフェースを満たすように実装しています。
※Goのmakeの3つ目の引数のキャパシティは、要素数が増えたら勝手にスケールするんですね。
その分、新しい領域の確保と要素のコピーのオーバーヘッドが発生するので、kを超える配列が発生する場合にそれを避けたい場合は、ユニークな要素数(len(countToNum)
)分のキャパシティを確保する必要があるのかと思いますが、topKの要素数よりもユニークな要素数がはるかに大きいことが予想できる場合はメモリが無駄になるので考えものですね、、
https://github.com/orlp/pdqsort?tab=readme-ov-file#the-best-case
の部分で、 slices.Sort(nums)
nums = append(nums, 1)
slices.Sort(nums) のようなコードだと、1回目のソートはO(nlogn)で、2回目のソートはO(n)でできるのが時々役に立つと思います |
type Element struct { | ||
num int | ||
count int | ||
} |
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.
実際に開発をしていると、独自定義の構造体には適切な Go Doc Comment が欲しいなと思うことが多いです。面接時にコードに反映させるかどうかは状況次第だと思いますが、次のように書いておくとより親切になるだろうな、ぐらいのことが考えられているとより良いように思います: https://tip.golang.org/doc/comment#type
ちなみにちゃんと書いておくとこのような見た目で出力することが出来、便利です: https://blog.lufia.org/entry/2018/05/14/150400
確認おくれました!いいとおもいます! |
Top K Frequent Elementsを解きました。レビューをお願い致します。
問題:https://leetcode.com/problems/top-k-frequent-elements/
言語:Go
すでに解いている方々:
hayashi-ay/leetcode#3
hayashi-ay/leetcode#60
cheeseNA/leetcode#13
Mike0121/LeetCode#3
TORUS0818/leetcode#11
fhiyo/leetcode#12
Ryotaro25/leetcode_first60#10
kazukiii/leetcode#10
wf9a5m75/leetcode3#3
Yoshiki-Iwasa/Arai60#8
kagetora0924/leetcode-grind#11
seal-azarashi/leetcode#9
ryoooooory/LeetCode#16
hroc135/leetcode#10
パフォーマンス比較
最小ヒープを使う場合の時間計算量は$O({n} log {k})$ ( $O(n + {n} log {k})$ )になる。まず、マップを作成するのに全ての要素をカウントする必要があるので $O(n)$ かかるのと、最小ヒープは最大で $k$ 個の要素のみを保持しておけば良いため、挿入と最小要素の削除にはどちらも $O(log {k})$ かかる。それを全ての要素に対して行うので全体では $O({n} log {k})$ になる。
最大ヒープの場合は、$n$ 個の要素を持つヒープから最大値を取り出すのに再構築を含めて $O(\log {n})$ かかり、 $k$ 個取り出すので $O({k} \log {n})$ になる。
バケットソート
時間計算量は$O(n)$ だが、標準ライブラリのソートは最適化されている分、速い可能性がある(参照)。
クイックセレクト
空間計算量
ただしGoでは末尾呼び出し最適化が行われない。
時間計算量
クイックソートと同様に、クイックセレクトは平均パフォーマンスは良好だが、これは選択されたピボットに左右される。適切なピボットが選択された場合、検索セットのサイズは指数関数的に減少し、線形時間で実行できる。ただし、毎回1つの要素だけ減少するなど、不適切なピボットが一貫して選択されている場合、$O(n^2)$ となる。
ランダムにピボットを選ぶ
3要素の中央値をピボットに選ぶ
中央値の中央値をピボットに選ぶ
全体の配列を小さな部分に分け、それぞれの部分から中央値を計算し、その中央値を再び集めたものから更に中央値を求めることで求められる。
PDQソート
PDQソートは、クイックソートとヒープソートを組み合わせたイントロソートを拡張・改良したPattern-Defeating Quicksort (PDQsort)の論文をベースに Go で実装したもので、クイックソートが苦手な同じ値を持つシーケンスに対してより効率よくソートできる。
Goのsort.Stringsのコードを見るとGo1.22以降は内部では単にslices.Sortを呼び出すと書いてあり、slices.Sortの内部ではPDQソートというものを使っているようで、PDQソートは$k$ 個の異なる要素を持つ入力に対して $O(nk)$ の最悪ケースに収まるとのこと。基本的にはクイックソートなので、平均的には $O({n} log {n})$ で、最悪計算量は $O(n^2)$ にならずに $O(nk)$ になる。
参考:https://www.m3tech.blog/entry/pdqsort
プライオリティキュー
最小ヒープを用いて、サイズがkを超えたら一番頻度の低い要素をPopすることを繰り返すことで最終的にk個の最も頻度の高い要素を得ることができる。最大ヒープを用いて全ての要素を頻度の高い順に保持して先頭からk個を取り出すということも可能だが、その分最小ヒープに比べてメモリ消費量が大きくなる。
その他
ryoooooory/LeetCode#16 (comment)
乱数生成
math/rand
とcrypto/rand
Goには
math/rand
とcrypto/rand
という2つの乱数生成用のパッケージがある。crypto/rand
は暗号学的に安全な乱数生成器を提供し、セキュアなトークンやキーの生成などに使用される。対してmath/rand
は擬似乱数を生成するため、指定されたシード値に基づいて乱数が生成され、同じシード値を使うと常に同じ乱数を得られる。crypto/rand
に比べて高速に実行でき、シード値が同じであれば、プログラムを実行するたびに同じ結果が得られるため、再現性が重要なシミュレーションやテストに使える。Go Playground
math/rand
Go1.20からグローバルなシードを設定する
rand.Seed
を使用するのではなく、rand.New(rand.NewSource(seed))
を使って新しいrand.Rand
インスタンスを作成し、シードを設定する方法が推奨されるようになった。これによって複数の乱数生成器を独立して動かすことができるようになり、意図しないグローバルな状態の共有を避けられる。math/rand/v2
Go1.22で追加される新しい乱数パッケージmath/rand/v2の紹介
Go 1.22 の math/rand/v2 を使ってみる