Skip to content

111. Minimum Depth of Binary Tree#21

Open
hroc135 wants to merge 2 commits intomainfrom
111MinimumDepthOfBinaryTree
Open

111. Minimum Depth of Binary Tree#21
hroc135 wants to merge 2 commits intomainfrom
111MinimumDepthOfBinaryTree

Conversation

@hroc135
Copy link
Copy Markdown
Owner

@hroc135 hroc135 commented Sep 28, 2024

問題:https://leetcode.com/problems/minimum-depth-of-binary-tree/description/

Go: スライスのキャパシティ

2dでスライスのキャパシティについて考察したら、キャパシティの増え方について知りたくなった。
前提として、Goのスライスは基底配列(underlying array)へのポインタ、要素数、キャパシティの3つのプロパティを保持する。
初期のキャパシティは、指定されていればその値、指定されていなければ要素数に等しい。appendがあると、キャパシティを拡張する必要ができる。このとき、要素数1024までは大体2倍に拡張され、それ以上になると少し拡張率が下がって1.25~1.5倍くらいになる。拡張率に関しては、スライスの型によるばらつきが多少ある。
キャパシティを拡大するときに内部では、1. 新しいキャパシティを決める、2. 新しい基底配列のメモリ領域を確保、3. 元のスライスの要素を新しい基底配列にコピー。以下のコードで新しいメモリ領域に基底配列が割り当てられている様子がわかる(これを観察したかったことが当初の動機)

func main() {
	s := []int{}
	fmt.Printf("s: %v, len: %d, cap: %d, address of the underlying array: %p\n", s, len(s), cap(s), (*[0]int)(s))

	s = append(s, 1)
	fmt.Printf("s: %v, len: %d, cap: %d, address of the underlying array: %p\n", s, len(s), cap(s), (*[1]int)(s))

	s = append(s, 2)
	fmt.Printf("s: %v, len: %d, cap: %d, address of the underlying array: %p\n", s, len(s), cap(s), (*[2]int)(s))

	s = append(s, 3)
	fmt.Printf("s: %v, len: %d, cap: %d, address of the underlying array: %p\n", s, len(s), cap(s), (*[2]int)(s))
}
s: [1], len: 1, cap: 1, address of the underlying array: 0x14000120030
s: [1 2], len: 2, cap: 2, address of the underlying array: 0x14000120040
s: [1 2 3], len: 3, cap: 4, address of the underlying array: 0x1400013c020

結論

スライスのメモリ領域の再割り当て(再ハッシュみたいな言い方ないのかな?)があるとその分の時間がかかるし、ガベージコレクションが起きるので、パフォーマンスが落ちる。なので、最大要素数がわかるならキャパシティは初期化時に決めておこう

参考:Sliceのcapacityはどのように増加していくか

- キャパシティを設定することにより、スライスがキャパシティオーバーでリロケートされるのを防ぐことができ、特に綺麗な形をした二分木に対して効果を発揮できる
- 実験:step1のコードとローカルで実行時間の差を計測
- 1. 深さ17、ノード数2^17の綺麗な二分木(常に子が2ついる)
- 見積もり実行時間:2^17 / 10e8 ≒ 10e5 / 10e8 = 10e-3 = 1ms
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

log_10 2 ~ 0.301 を覚えておくと速いです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

$2^{17} = (10^{\log_{10}2})^{17} \fallingdotseq 10^{0.3 \times 17} \fallingdotseq 10^5$ ということですね。ありがとうございます

@oda
Copy link
Copy Markdown

oda commented Oct 5, 2024

拡張率のところは、償却計算量の概念などを聞かれることがあります。

  1. 償却計算量とはなにか。
  2. 足りなくなったときに、定数個ずつ拡張すると append の償却計算量はどうなるか。
  3. 足りなくなったときに、定数倍ずつ拡張すると append の償却計算量はどうなるか。

@hroc135
Copy link
Copy Markdown
Owner Author

hroc135 commented Oct 5, 2024

#21 (comment) を受けて

  1. 償却計算量とは?
    均し計算量ともいう。英語だとamortized time complexity。似た操作を何回か行うとき、毎回の操作にかかる時間を均して1回の操作にかかる時間を求める。つまり、n回の操作をf(n)時間で抑えることができるとき、償却計算量はO(f(n) / n)になる。基本的にはxx時間でできるが、時々xx時間より重い計算になる、というケースで求めることが多い
  2. 足りなくなったときに、定数個ずつ拡張すると append の償却計算量はどうなるか?
    拡張するときの操作はO(n)時間で、拡張しないときはO(1)。k個ずつ拡張するとすると、 $f(n) = n + \sum_{i=1}^{n/k} ik < n + n^2$ (等差級数の和なので)。なので、1回の操作にかかる償却計算量は、O(f(n) / n) = O(n)
  3. 足りなくなったときに、定数倍ずつ拡張すると append の償却計算量はどうなるか?
    拡張がk倍だとする。 $f(n) = n + \sum_{i=1}^{\log_{k}n} k^i < n + kn = (k+1)n$ (等比級数の和なので)。よって、1回の操作にかかる償却計算量は、O(f(n) / n) = O(1)

こんな感じ?

@oda
Copy link
Copy Markdown

oda commented Oct 5, 2024

はい、それで OK です。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants