-
Notifications
You must be signed in to change notification settings - Fork 0
31. Next Permutation #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
Open
hroc135
wants to merge
1
commit into
main
Choose a base branch
from
31NextPermutation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| 問題: https://leetcode.com/problems/next-permutation/description/ | ||
|
|
||
| ### Step 1 | ||
| - 尻から見ていって一番最初に隣り合う要素が昇順になっているところ以降をいじれば next permutation になる。 | ||
| それより前はいじる必要なし | ||
| - nums=[1,4,0,5,3,2] で一番最初に隣り合う要素が昇順になっているところは、0 と 3。 | ||
| - 0 より右にある 0 より大きい要素のうち、最も小さいものを見つける(ここでは 2)。 | ||
| - 0 の位置に 2 を置く(0 を失いたくはないので、swap する)。 | ||
| - 2 より右をソートする | ||
| - nums=[1,4,2,0,3,5] | ||
| - 上記をやろうとすると、nums が降順の場合になにも起きずに終わってしまうので、関数の頭で別途処理する | ||
| - nums を昇順にしたものを返せばいいのだが、降順を昇順にするには単に反転すればいいので、slices.Reverse を使った | ||
| - t_wada さんがコメントには why not を書こうとつぶやいていたのを思い出し、コメントに書くことに | ||
| - https://x.com/t_wada/status/904916106153828352 | ||
| - 時間計算量: O(n logn) | ||
| - 最悪の場合は、先頭が最小要素でその他は降順にソートされているとき | ||
| - 空間計算量: O(1) | ||
| - テストケース | ||
| - nums=[] -> [] | ||
| - nums=[0] -> [0] | ||
| - nums=[0,1] -> [1,0] | ||
| - nums=[0,0,0] -> [0,0,0] | ||
| - nums=[0,1,2] -> [0,2,1] | ||
| - nums=[1,2,0] -> [2,0,1] | ||
| - nums=[2,1,0] -> [0,1,2] | ||
| - nums=[1,4,0,5,3,2] -> [1,4,2,0,3,5] | ||
| - nums=[0,1,2,1] -> [0,2,1,1] | ||
| - nums=[0,0,5,0,0] -> [0,5,0,0,0] | ||
|
|
||
| ```Go | ||
| func nextPermutation(nums []int) { | ||
| // slices.IsSortedFunc returns true when nums is an empty slice. | ||
| if slices.IsSortedFunc(nums, func(a, b int) int { return b - a }) { | ||
| // Reversing sorts nums in ascending order because nums was in descending order. | ||
| // slices.Reverse takes O(n) time while pdqsort in slices.Sort takes O(n logn) time. | ||
| // pdqsort would terminate in O(n) time when its in strictly descending order. | ||
| slices.Reverse(nums) | ||
| return | ||
| } | ||
| for i := len(nums) - 2; i >= 0; i-- { | ||
| if nums[i] < nums[i+1] { | ||
| // a is the smallest element of nums[i+1:] that is larger than nums[i]. | ||
| a := math.MaxInt | ||
| // ai is the index of a in nums. | ||
| var ai int | ||
| for j := i + 1; j < len(nums); j++ { | ||
| if nums[j] > nums[i] && nums[j] < a { | ||
| a = nums[j] | ||
| ai = j | ||
| } | ||
| } | ||
| nums[i], nums[ai] = nums[ai], nums[i] | ||
| slices.Sort(nums[i+1:]) | ||
| return | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 2 | ||
| #### 2a | ||
| - step1 の改善 | ||
| - ライブラリ関数を使って簡潔に書ける気がする | ||
| - と思ったら、そもそもアルゴリズムをもっと簡単にできた | ||
| - https://github.com/olsen-blue/Arai60/pull/59/files#diff-ee8de8f68c4ae03917be0fa643c8defc8592e248688aed43937777d663dbfccdR168 | ||
| - 「一番最初に隣り合う要素が昇順になっているところ」より右は降順であることを利用する | ||
| - i: 「一番最初に隣り合う要素が昇順になっているところ」とする | ||
| - nums[i+1:] は降順 | ||
| - nums を尻から見て、一番最初に nums[i] より大きい要素になるところを j とする | ||
| - nums[j] は step1 で探した「nums[i] より右にある nums[i] より大きい要素のうち、最も小さいもの」である | ||
| - なぜなら、nums[j-1] >= nums[j] >= nums[j+1] >= ... >= 末尾 だから。 | ||
| - nums[j] > nums[i] >= nums[j+1] なので、nums[i] と nums[j] を swap しても、nums[i+1:] は降順 | ||
| - nums[i+1:] を reverse すればよい | ||
|
|
||
| ```Go | ||
| func nextPermutation(nums []int) { | ||
| for i := len(nums) - 2; i >= 0; i-- { | ||
| if nums[i] < nums[i+1] { | ||
| for j := len(nums) - 1; j > i; j-- { | ||
| if nums[j] > nums[i] { | ||
| nums[i], nums[j] = nums[j], nums[i] | ||
| slices.Reverse(nums[i+1:]) | ||
| return | ||
| } | ||
| } | ||
| } | ||
| } | ||
| slices.Reverse(nums) | ||
| } | ||
| ``` | ||
|
|
||
| #### 2b | ||
| - 2a はさすがにネストが深いと思うので、改善 | ||
|
|
||
| ```Go | ||
| func nextPermutation(nums []int) { | ||
| // left is the first index from the end such that nums[left] < nums[left+1] | ||
| left := len(nums) - 2 | ||
| for left >= -1 && nums[left] >= nums[left+1] { | ||
| left-- | ||
| } | ||
| if left < 0 { | ||
| slices.Reverse(nums) | ||
| return | ||
| } | ||
|
|
||
| // right is the smallest number > nums[left] from right to left | ||
| right := len(nums) - 1 | ||
| for nums[right] <= nums[left] { | ||
| right-- | ||
| } | ||
|
|
||
| nums[left], nums[right] = nums[right], nums[left] | ||
| slices.Reverse(nums[left+1:]) | ||
| } | ||
| ``` | ||
|
|
||
| #### 2c | ||
| - 2b を関数に切り出して改善 | ||
| - https://github.com/hayashi-ay/leetcode/pull/67/files#diff-ee8de8f68c4ae03917be0fa643c8defc8592e248688aed43937777d663dbfccdR119 | ||
| - 関数名の参考に | ||
| - 時間計算量: O(3n) = O(n) | ||
|
|
||
| ```Go | ||
| func nextPermutation(nums []int) { | ||
| findFirstDecreasingIndexFromTail := func() int { | ||
| for i := len(nums) - 2; i >= 0; i-- { | ||
| if nums[i] < nums[i+1] { | ||
| return i | ||
| } | ||
| } | ||
| return -1 | ||
| } | ||
|
|
||
| findNextGreaterNumIndex := func(pivot int) int { | ||
| for i := len(nums) - 1; i > pivot; i-- { | ||
| if nums[i] > nums[pivot] { | ||
| return i | ||
| } | ||
| } | ||
| // This line is unreachable because nums[pivot] < nums[pivot+1] | ||
| panic("unreachable") | ||
| } | ||
|
|
||
| pivot := findFirstDecreasingIndexFromTail() | ||
| if pivot < 0 { | ||
| slices.Reverse(nums) | ||
| return | ||
| } | ||
| nextNumIndex := findNextGreaterNumIndex(pivot) | ||
| nums[pivot], nums[nextNumIndex] = nums[nextNumIndex], nums[pivot] | ||
| slices.Reverse(nums[pivot+1:]) | ||
| } | ||
| ``` | ||
|
|
||
| #### 2d | ||
| - https://github.com/usatie/leetcode/pull/2/files#diff-6df63844477a509737ffe87b2dcf17e6b85aaae820bbffe395d625625549a4ddR50 | ||
| - nums[pivot] より大きい最小のインデックスを nums[pivot+1:] から探す部分は二分探索でできるじゃんと気が付いた | ||
| - successor という命名もいいと思った | ||
| - 時間計算量: O(2n + logn) | ||
| - O(3n) より若干改善 | ||
|
|
||
| ```Go | ||
| func nextPermutation(nums []int) { | ||
| pivot := len(nums) - 2 | ||
| for ; pivot >= 0; pivot-- { | ||
| if nums[pivot] < nums[pivot+1] { | ||
| break | ||
| } | ||
| } | ||
| if pivot < 0 { | ||
| slices.Reverse(nums) | ||
| return | ||
| } | ||
|
|
||
| successorIndex := UpperBoundRight(nums[pivot+1:], nums[pivot]) + pivot + 1 | ||
| nums[pivot], nums[successorIndex] = nums[successorIndex], nums[pivot] | ||
| slices.Reverse(nums[pivot+1:]) | ||
| } | ||
|
|
||
| // UpperBoundRight finds the largest index i such that nums[i] > target. | ||
| // nums is sorted in descending order. | ||
| func UpperBoundRight(nums []int, target int) int { | ||
| left := -1 | ||
| right := len(nums) - 1 | ||
| for left < right { | ||
| middle := left + (right-left+1)/2 | ||
| if nums[middle] > target { | ||
| left = middle | ||
| } else { | ||
| right = middle - 1 | ||
| } | ||
| } | ||
| return right | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 3 | ||
| - パフォーマンスが微妙に一番いい方法を採用 | ||
|
|
||
| ```Go | ||
| func nextPermutation(nums []int) { | ||
| pivotIndex := len(nums) - 2 | ||
| for ; pivotIndex >= 0; pivotIndex-- { | ||
| if nums[pivotIndex] < nums[pivotIndex+1] { | ||
| break | ||
| } | ||
| } | ||
| if pivotIndex < 0 { | ||
| slices.Reverse(nums) | ||
| return | ||
| } | ||
|
|
||
| successorIndex := UpperBoundRightDesc(nums[pivotIndex+1:], nums[pivotIndex]) + pivotIndex + 1 | ||
| nums[pivotIndex], nums[successorIndex] = nums[successorIndex], nums[pivotIndex] | ||
| slices.Reverse(nums[pivotIndex+1:]) | ||
| } | ||
|
|
||
| // UpperBoundRightDesc returns the largest index such that nums[index] > target. | ||
| // nums is in descending order. | ||
| // Returns -1 when target is larger than s[0]. | ||
| func UpperBoundRightDesc[S ~[]E, E cmp.Ordered](s S, target E) int { | ||
| left := -1 | ||
| right := len(s) - 1 | ||
| for left < right { | ||
| middle := int(uint(left+right+1) >> 1) | ||
| if cmp.Less(target, s[middle]) { | ||
| left = middle | ||
| } else { | ||
| right = middle - 1 | ||
| } | ||
| } | ||
| return right | ||
| } | ||
| ``` | ||
|
|
||
| - Reverse を自作 | ||
| - https://cs.opensource.google/go/go/+/master:src/slices/slices.go;l=472?q=func%20Reverse&ss=go%2Fgo | ||
| - 可読性について突っ込まれそうだが、実際のソースコードとほぼ同じ | ||
|
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. 私はこれくらいならば一文字の変数で構わないと思います。 |
||
| - i, j -> l, r に変更しただけ | ||
|
|
||
| ```Go | ||
| func Reverse[S ~[]E, E any](s S) { | ||
| for l, r := 0, len(s)-1; l < r; l, r = l+1, r-1 { | ||
| s[l], s[r] = s[r], s[l] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### CS | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
これ、数が10-20くらいだとおそらく速度はそう変わりません。分岐予測が効くので線形に舐めるのは思っているよりも速いのです。
すでに pivot を探すのに線形時間かかっていることもあり、私はこれはコードの単純さを取る場面だと判断すると思います。