-
Notifications
You must be signed in to change notification settings - Fork 0
703. Kth Largest Element in a Stream #53
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
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,45 @@ | ||
| # step1 何も見ずに解く | ||
|
|
||
| Nをスコアの長さとする。Mをadd操作の回数とする。 | ||
| 最初のスコアをソートしておくと、k番目のスコアはO(1)で取り出せる。 | ||
| 新しいスコアを追加する時は二分探索を使うとO(logN)で入れる場所を探せる。 | ||
| ただ、配列の途中に挿入するときにO(N + M)の計算量がかかるので、最終的な時間計算量は | ||
| M * (log(M + N) + (M + N))となり、O(M * (M + N))となり、M,Nの最大値が10^4なので間に合わない気がする。 | ||
|
|
||
| ```ruby | ||
| class KthLargest | ||
|
|
||
| =begin | ||
| :type k: Integer | ||
| :type nums: Integer[] | ||
| =end | ||
| def initialize(k, nums) | ||
| raise ArgumentError, "k must be positive integer" if k <= 0 | ||
| @k = k | ||
| @sorted_nums = nums.sort.reverse | ||
| end | ||
|
|
||
|
|
||
| =begin | ||
| :type val: Integer | ||
| :rtype: Integer | ||
| =end | ||
| def add(val) | ||
| return @sorted_nums[@k - 1] if @sorted_nums.size >= @k && @sorted_nums[@k - 1] >= val | ||
|
|
||
| index = @sorted_nums.bsearch_index { |num| num <= val } | ||
| if index.nil? | ||
| @sorted_nums << val | ||
| else | ||
| @sorted_nums.insert(index, val) | ||
| end | ||
| @sorted_nums[@k - 1] | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| なぜか間に合った。テストケースに厳しいものが少なかったのかも。 | ||
|
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. native で走る部分は速いということかと思います。
Owner
Author
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. コメントありがとうございます。 試しにinsertのような処理をRubyレベルで実行して比較したところ require "benchmark"
def native_insert(ary, num)
ary.insert(50, num)
end
def ruby_insert(ary, num)
ary << 0
(ary.size - 1).downto(50) do |i|
ary[i + 1] = ary[i]
end
ary[50] = num
end
BASE = Array.new(10_000, 0)
N = 10 ** 3
Benchmark.bmbm(10) do |x|
x.report("insert (native Array#insert)") do
arr = BASE.dup
N.times do
native_insert(arr, 99)
end
end
x.report("ruby_array_inset") do
arr = BASE.dup
N.times do
ruby_insert(arr, 99)
end
end
end |
||
| 一応Rubyのinsertメソッドの実装を見たが、やはりO(N)かかるっぽい。まあそりゃそうか。 | ||
|
|
||
| 今のコードでは、scoreは降順に並んでいて、@sorted_nums[@k - 1]より小さい値、つまり配列の後にくる値の挿入をスキップしている。 | ||
| 配列の挿入は後ろであればあるほどコストが小さいはずなので昇順にして、前の方に挿入する時にスキップした方がマシだったかもしれない。 | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,130 @@ | ||||||||||||||||||
| # step2 他の方の解答を見る | ||||||||||||||||||
|
|
||||||||||||||||||
| - https://github.com/Ryotaro25/leetcode_first60/pull/9 | ||||||||||||||||||
|
|
||||||||||||||||||
| MinHeapで実装する。 | ||||||||||||||||||
| 小さい順に上位k個数の値を保持しておけば良い。 | ||||||||||||||||||
| Heapに値を追加する時の時間計算量はO(logN) | ||||||||||||||||||
| 一番上の値を取り出した後にHeapの順を直す時の時間計算量もO(logN) | ||||||||||||||||||
| なのでnumsの長さをN, addが呼ばれる回数をMとすると、 | ||||||||||||||||||
| 最初にnumを入れるのにO(N*logN)でaddのクエリは合計で(M * log(k))になる。 | ||||||||||||||||||
| なのでO(NlogN) | ||||||||||||||||||
| 空間計算量はO(M + N) | ||||||||||||||||||
|
Comment on lines
+9
to
+12
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.
Suggested change
Owner
Author
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. コメントありがとうございます。 |
||||||||||||||||||
|
|
||||||||||||||||||
| ```ruby | ||||||||||||||||||
| class MinHeap | ||||||||||||||||||
| def initialize | ||||||||||||||||||
| @heap = [] | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| def peek | ||||||||||||||||||
| @heap.first | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| def push(val) | ||||||||||||||||||
| @heap << val | ||||||||||||||||||
| sift_up(@heap.size - 1) | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| def pop | ||||||||||||||||||
| return nil if @heap.empty? | ||||||||||||||||||
|
|
||||||||||||||||||
| swap(0, @heap.size - 1) | ||||||||||||||||||
| min = @heap.pop | ||||||||||||||||||
| sift_down(0) | ||||||||||||||||||
| min | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| def size | ||||||||||||||||||
| @heap.size | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| private | ||||||||||||||||||
|
|
||||||||||||||||||
| def parent_index(index) | ||||||||||||||||||
| (index - 1) / 2 | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| def left_child_index(index) | ||||||||||||||||||
| 2 * index + 1 | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| def right_child_index(index) | ||||||||||||||||||
| 2 * index + 2 | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| def sift_up(child) | ||||||||||||||||||
| parent = parent_index(child) | ||||||||||||||||||
| return if parent < 0 || @heap[parent] <= @heap[child] | ||||||||||||||||||
|
|
||||||||||||||||||
| swap(child, parent) | ||||||||||||||||||
| sift_up(parent) | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| def sift_down(parent) | ||||||||||||||||||
| left = left_child_index(parent) | ||||||||||||||||||
| right = right_child_index(parent) | ||||||||||||||||||
| smallest = parent | ||||||||||||||||||
| smallest = left if left < @heap.size && @heap[smallest] > @heap[left] | ||||||||||||||||||
| smallest = right if right < @heap.size && @heap[smallest] > @heap[right] | ||||||||||||||||||
|
Comment on lines
+65
to
+69
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. 微かにトリッキーさを感じました。 smallest = parent
left = left_child_index(parent)
smallest = left if left < @heap.size && @heap[smallest] > @heap[left]
right = right_child_index(parent)
smallest = right if right < @heap.size && @heap[smallest] > @heap[right]
Owner
Author
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. 確かに、smallestを更新する直前にleft/rightをそれぞれ変数に入れる方が少しわかりやすいかもしれないです。 ちょっと冗長かもしれませんが if has_left_child?(smallest)
left = left_child_index(smallest)
smallest = left if @heap[left] < @heap[smallest]
endThere 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. https://discord.com/channels/1084280443945353267/1192736784354918470/1194613857046503444
Owner
Author
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. ありがとうございます。 class MinHeap
def initialize
@heap = []
end
def peek
@heap.first
end
def push(val)
@heap << val
sift_up(@heap.size - 1)
end
def pop
return nil if @heap.empty?
swap(0, @heap.size - 1)
min = @heap.pop
sift_down(0)
min
end
def size
@heap.size
end
private
def parent_index(index)
(index - 1) / 2
end
def left_child_index(index)
2 * index + 1
end
def right_child_index(index)
2 * index + 2
end
def has_left_child?(index)
left_child_index(index) < @heap.size
end
def has_right_child?(index)
right_child_index(index) < @heap.size
end
def has_child?(index)
has_left_child?(index)
end
def smaller_child_index(index)
smaller_child_index = left_child_index(index)
if has_right_child?(index) && @heap[left_child_index(index)] > @heap[right_child_index(index)]
smaller_child_index = right_child_index(index)
end
smaller_child_index
end
def sift_up(child)
parent = parent_index(child)
return if parent < 0 || @heap[parent] <= @heap[child]
swap(child, parent)
sift_up(parent)
end
def sift_down(parent)
return unless has_child?(parent)
smaller_child = smaller_child_index(parent)
return if @heap[parent] <= @heap[smaller_child]
swap(parent, smaller_child)
sift_down(smaller_child)
end
def swap(index1, index2)
@heap[index1], @heap[index2] = @heap[index2], @heap[index1]
end
end |
||||||||||||||||||
| return if smallest == parent | ||||||||||||||||||
|
|
||||||||||||||||||
| swap(parent, smallest) | ||||||||||||||||||
| sift_down(smallest) | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| def swap(index1, index2) | ||||||||||||||||||
| @heap[index1], @heap[index2] = @heap[index2], @heap[index1] | ||||||||||||||||||
| end | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class KthLargest | ||||||||||||||||||
|
|
||||||||||||||||||
| =begin | ||||||||||||||||||
| :type k: Integer | ||||||||||||||||||
| :type nums: Integer[] | ||||||||||||||||||
| =end | ||||||||||||||||||
| def initialize(k, nums) | ||||||||||||||||||
| raise ArgumentError, "k must be positive integer" if k <= 0 | ||||||||||||||||||
| raise ArgumentError, "Initial array size is too small to determine the top #{k}" if k > nums.size + 1 | ||||||||||||||||||
|
Comment on lines
+90
to
+91
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. 確かにエラーメッセージには実際の値を埋め込むと、デバッグ時に原因を特定しやすくなると思います。
Suggested change
また、2個目のエラーは不要だと思いました。 初期配列がkより小さくてもaddメソッドがガードして正常に機能するためです。
Owner
Author
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.
機能はしますが、以下のようにtop 3が欲しいのにそうではない結果を返すことが良いかは要件次第かなと思います。 k = KthLargest.new(3, [])
k.add(3) #=> 3を返す |
||||||||||||||||||
| @k = k | ||||||||||||||||||
| @top_k = MinHeap.new | ||||||||||||||||||
| nums.each { |num| add(num) } | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| =begin | ||||||||||||||||||
| :type val: Integer | ||||||||||||||||||
| :rtype: Integer | ||||||||||||||||||
| =end | ||||||||||||||||||
| def add(val) | ||||||||||||||||||
| if @top_k.size < @k | ||||||||||||||||||
| @top_k.push(val) | ||||||||||||||||||
| elsif @top_k.peek < val | ||||||||||||||||||
| @top_k.pop | ||||||||||||||||||
| @top_k.push(val) | ||||||||||||||||||
| end | ||||||||||||||||||
| @top_k.peek | ||||||||||||||||||
| end | ||||||||||||||||||
| end | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| https://github.com/shintaroyoshida20/leetcode/pull/13/files/db18f87a811ced86f26a72e4ee842f1b99d71b15#r2052161956 | ||||||||||||||||||
|
|
||||||||||||||||||
| LeetCode環境でRubyは`Module: Algorithms`が使えるらしい。初めて知った。 | ||||||||||||||||||
| https://www.rubydoc.info/github/kanwei/algorithms/Algorithms | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| https://github.com/fhiyo/leetcode/pull/10/files/0ee4b594d9657627d07ef9810b8f695611e366ac#r1605950261 | ||||||||||||||||||
|
|
||||||||||||||||||
| メソッド単位でmutexを取る前提だと、topで値を返さないと | ||||||||||||||||||
|
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. マルチスレッド前提だとこのような問題が起こる可能性があるのですね。勉強になりました。
Owner
Author
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. スレッドセーフではない外部ライブラリを使う時などは、呼び出し側がmutexをとって競合が起きないようにする、という方法もあり得ると思います。 |
||||||||||||||||||
| - スレッドA | ||||||||||||||||||
| - 1. top | ||||||||||||||||||
| - 3. pop | ||||||||||||||||||
| スレッドB | ||||||||||||||||||
| - 2. pop | ||||||||||||||||||
|
|
||||||||||||||||||
| 上の順番で処理が走ると不整合が起きる。 | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| # step3 3回続けて10分以内に書いてエラーを出さなければOKとする | ||
|
|
||
| ```ruby | ||
| class MinHeap | ||
| def initialize | ||
| @heap = [] | ||
| end | ||
|
|
||
| def size | ||
| @heap.size | ||
| end | ||
|
|
||
| def push(val) | ||
| @heap << val | ||
| sift_up(@heap.size - 1) | ||
| end | ||
|
|
||
| def pop | ||
| swap(0, @heap.size - 1) | ||
| min_val = @heap.pop | ||
|
Comment on lines
+19
to
+20
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. 「先頭の要素を末尾に持っていき、それをpopすると同時にmin_valとして格納する」 一行長くなっても
となっているほうが個人的には違和感がない形かなと思います。
Owner
Author
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. 確かに、そっちの方がわかりやすいですね。 |
||
| sift_down(0) | ||
| min_val | ||
|
Comment on lines
+18
to
+22
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. step2 -> step3でreturn nil if @heap.empty?を削ったと思うですが、賛成です。 addメソッドでチェックがあるので、低レベルにもチェックがあると冗長になりますね。 もしpopメソッドでチェックを含める設計にする場合は、設計の一貫性から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. 個人的にはMinHeapを実装するのであればKthLargestなど他のクラスに依存せずMinHeap単体でも使えるようになっていてほしいので、emptyチェックがあるほうが安心ではあるなと思いました。
こちらに関しては賛成です。
Owner
Author
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では単純にempty?のチェックが漏れていました。 |
||
| end | ||
|
|
||
| def peek | ||
| @heap.first | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def parent_index(index) | ||
| (index - 1) / 2 | ||
| end | ||
|
|
||
| def left_child_index(index) | ||
| index * 2 + 1 | ||
| end | ||
|
|
||
| def right_child_index(index) | ||
| index * 2 + 2 | ||
| end | ||
|
|
||
| def swap(index1, index2) | ||
| @heap[index1], @heap[index2] = @heap[index2], @heap[index1] | ||
| end | ||
|
|
||
| def sift_up(child) | ||
| parent = parent_index(child) | ||
| return if parent < 0 || @heap[parent] <= @heap[child] | ||
|
|
||
| swap(child, parent) | ||
| sift_up(parent) | ||
| end | ||
|
|
||
| def sift_down(parent) | ||
| left = left_child_index(parent) | ||
| right = right_child_index(parent) | ||
| smallest = parent | ||
| smallest = left if left < @heap.size && @heap[left] < @heap[smallest] | ||
| smallest = right if right < @heap.size && @heap[right] < @heap[smallest] | ||
| return if smallest == parent | ||
|
|
||
| swap(parent, smallest) | ||
| sift_down(smallest) | ||
| end | ||
| end | ||
|
|
||
|
|
||
|
|
||
| class KthLargest | ||
|
|
||
| =begin | ||
| :type k: Integer | ||
| :type nums: Integer[] | ||
| =end | ||
| def initialize(k, nums) | ||
| raise ArgumentError, "k must be positive integer" if k <= 0 | ||
| raise ArgumentError, "Initial array size is too small to determine the top #{k}" if k > nums.size + 1 | ||
| @k = k | ||
| @top_k = MinHeap.new | ||
| nums.each { |num| add(num) } | ||
| end | ||
|
|
||
|
|
||
| =begin | ||
| :type val: Integer | ||
| :rtype: Integer | ||
| =end | ||
| def add(val) | ||
| if @top_k.size < @k | ||
| @top_k.push(val) | ||
| elsif @top_k.peek < val | ||
| @top_k.pop | ||
| @top_k.push(val) | ||
| end | ||
| @top_k.peek | ||
| end | ||
| end | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ## step4 レビューを受けて解答を修正 |
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より小さい場合範囲外参照になりnilが返りますかね。