Skip to content

Commit 7b334a1

Browse files
elibenianlancetaylor
authored andcommitted
slices: initial implementation of sorting functions
Implements golang/go#47619 in the exp/slices package as a testing ground prior to inclusion in the standard library. Relies on the modified sorting function code generator proposed in https://go-review.googlesource.com/c/go/+/353069 to automatically generate the code of the sorting functions. Benchmark comparing sort.Ints with the generic Sort function added in this CL to sort a slice of int: name old time/op new time/op delta Sort-8 12.0ms ± 1% 6.5ms ± 1% -46.02% (p=0.000 n=9+10) Benchmark comparing sort.Sort with SortFunc to sort a slice of struct pointers based on one field in the struct: name old time/op new time/op delta SortStructs-8 18.6ms ± 2% 15.9ms ± 3% -14.43% (p=0.000 n=10+10) Change-Id: Ic301aae7e5b8f99144e39b8a77fde897779588ed Reviewed-on: https://go-review.googlesource.com/c/exp/+/378134 Reviewed-by: Ian Lance Taylor <[email protected]> Trust: Cody Oss <[email protected]> Trust: Jeremy Faller <[email protected]>
1 parent 2c358f7 commit 7b334a1

File tree

5 files changed

+1079
-0
lines changed

5 files changed

+1079
-0
lines changed

slices/sort.go

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package slices
6+
7+
import "constraints"
8+
9+
// Sort sorts a slice of any ordered type in ascending order.
10+
func Sort[Elem constraints.Ordered](x []Elem) {
11+
n := len(x)
12+
quickSortOrdered(x, 0, n, maxDepth(n))
13+
}
14+
15+
// Sort sorts the slice x in ascending order as determined by the less function.
16+
// This sort is not guaranteed to be stable.
17+
func SortFunc[Elem any](x []Elem, less func(a, b Elem) bool) {
18+
n := len(x)
19+
quickSortLessFunc(x, 0, n, maxDepth(n), less)
20+
}
21+
22+
// SortStable sorts the slice x while keeping the original order of equal
23+
// elements, using less to compare elements.
24+
func SortStableFunc[Elem any](x []Elem, less func(a, b Elem) bool) {
25+
stableLessFunc(x, len(x), less)
26+
}
27+
28+
// IsSorted reports whether x is sorted in ascending order.
29+
func IsSorted[Elem constraints.Ordered](x []Elem) bool {
30+
for i := len(x) - 1; i > 0; i-- {
31+
if x[i] < x[i-1] {
32+
return false
33+
}
34+
}
35+
return true
36+
}
37+
38+
// IsSortedFunc reports whether x is sorted in ascending order, with less as the
39+
// comparison function.
40+
func IsSortedFunc[Elem any](x []Elem, less func(a, b Elem) bool) bool {
41+
for i := len(x) - 1; i > 0; i-- {
42+
if less(x[i], x[i-1]) {
43+
return false
44+
}
45+
}
46+
return true
47+
}
48+
49+
// BinarySearch searches for target in a sorted slice and returns the smallest
50+
// index at which target is found. If the target is not found, the index at
51+
// which it could be inserted into the slice is returned; therefore, if the
52+
// intention is to find target itself a separate check for equality with the
53+
// element at the returned index is required.
54+
func BinarySearch[Elem constraints.Ordered](x []Elem, target Elem) int {
55+
return search(len(x), func(i int) bool { return x[i] >= target })
56+
}
57+
58+
// BinarySearchFunc uses binary search to find and return the smallest index i
59+
// in [0, n) at which ok(i) is true, assuming that on the range [0, n),
60+
// ok(i) == true implies ok(i+1) == true. That is, BinarySearchFunc requires
61+
// that ok is false for some (possibly empty) prefix of the input range [0, n)
62+
// and then true for the (possibly empty) remainder; BinarySearchFunc returns
63+
// the first true index. If there is no such index, BinarySearchFunc returns n.
64+
// (Note that the "not found" return value is not -1 as in, for instance,
65+
// strings.Index.) Search calls ok(i) only for i in the range [0, n).
66+
func BinarySearchFunc[Elem any](x []Elem, ok func(Elem) bool) int {
67+
return search(len(x), func(i int) bool { return ok(x[i]) })
68+
}
69+
70+
// maxDepth returns a threshold at which quicksort should switch
71+
// to heapsort. It returns 2*ceil(lg(n+1)).
72+
func maxDepth(n int) int {
73+
var depth int
74+
for i := n; i > 0; i >>= 1 {
75+
depth++
76+
}
77+
return depth * 2
78+
}
79+
80+
func search(n int, f func(int) bool) int {
81+
// Define f(-1) == false and f(n) == true.
82+
// Invariant: f(i-1) == false, f(j) == true.
83+
i, j := 0, n
84+
for i < j {
85+
h := int(uint(i+j) >> 1) // avoid overflow when computing h
86+
// i ≤ h < j
87+
if !f(h) {
88+
i = h + 1 // preserves f(i-1) == false
89+
} else {
90+
j = h // preserves f(j) == true
91+
}
92+
}
93+
// i == j, f(i-1) == false, and f(j) (= f(i)) == true => answer is i.
94+
return i
95+
}

slices/sort_benchmark_test.go

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package slices
6+
7+
import (
8+
"math/rand"
9+
"sort"
10+
"testing"
11+
)
12+
13+
// These benchmarks compare sorting a large slice of int with sort.Ints vs.
14+
// slices.Sort
15+
func makeRandomInts(n int) []int {
16+
rand.Seed(42)
17+
ints := make([]int, n)
18+
for i := 0; i < n; i++ {
19+
ints[i] = rand.Intn(n)
20+
}
21+
return ints
22+
}
23+
24+
const N = 100_000
25+
26+
func BenchmarkSortInts(b *testing.B) {
27+
for i := 0; i < b.N; i++ {
28+
b.StopTimer()
29+
ints := makeRandomInts(N)
30+
b.StartTimer()
31+
sort.Ints(ints)
32+
}
33+
}
34+
35+
func BenchmarkSlicesSort(b *testing.B) {
36+
for i := 0; i < b.N; i++ {
37+
b.StopTimer()
38+
ints := makeRandomInts(N)
39+
b.StartTimer()
40+
Sort(ints)
41+
}
42+
}
43+
44+
// Since we're benchmarking these sorts against each other, make sure that they
45+
// generate similar results.
46+
func TestIntSorts(t *testing.T) {
47+
ints := makeRandomInts(200)
48+
ints2 := Clone(ints)
49+
50+
sort.Ints(ints)
51+
Sort(ints2)
52+
53+
for i := range ints {
54+
if ints[i] != ints2[i] {
55+
t.Fatalf("ints2 mismatch at %d; %d != %d", i, ints[i], ints2[i])
56+
}
57+
}
58+
}
59+
60+
// These benchmarks compare sorting a slice of structs with sort.Sort vs.
61+
// slices.SortFunc.
62+
type myStruct struct {
63+
a, b, c, d string
64+
n int
65+
}
66+
67+
type myStructs []*myStruct
68+
69+
func (s myStructs) Len() int { return len(s) }
70+
func (s myStructs) Less(i, j int) bool { return s[i].n < s[j].n }
71+
func (s myStructs) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
72+
73+
func makeRandomStructs(n int) myStructs {
74+
rand.Seed(42)
75+
structs := make([]*myStruct, n)
76+
for i := 0; i < n; i++ {
77+
structs[i] = &myStruct{n: rand.Intn(n)}
78+
}
79+
return structs
80+
}
81+
82+
func TestStructSorts(t *testing.T) {
83+
ss := makeRandomStructs(200)
84+
ss2 := make([]*myStruct, len(ss))
85+
for i := range ss {
86+
ss2[i] = &myStruct{n: ss[i].n}
87+
}
88+
89+
sort.Sort(ss)
90+
SortFunc(ss2, func(a, b *myStruct) bool { return a.n < b.n })
91+
92+
for i := range ss {
93+
if *ss[i] != *ss2[i] {
94+
t.Fatalf("ints2 mismatch at %d; %v != %v", i, *ss[i], *ss2[i])
95+
}
96+
}
97+
}
98+
99+
func BenchmarkSortStructs(b *testing.B) {
100+
for i := 0; i < b.N; i++ {
101+
b.StopTimer()
102+
ss := makeRandomStructs(N)
103+
b.StartTimer()
104+
sort.Sort(ss)
105+
}
106+
}
107+
108+
func BenchmarkSortFuncStructs(b *testing.B) {
109+
lessFunc := func(a, b *myStruct) bool { return a.n < b.n }
110+
for i := 0; i < b.N; i++ {
111+
b.StopTimer()
112+
ss := makeRandomStructs(N)
113+
b.StartTimer()
114+
SortFunc(ss, lessFunc)
115+
}
116+
}

slices/sort_test.go

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package slices
6+
7+
import (
8+
"math"
9+
"math/rand"
10+
"testing"
11+
)
12+
13+
var ints = [...]int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
14+
var float64s = [...]float64{74.3, 59.0, math.Inf(1), 238.2, -784.0, 2.3, math.NaN(), math.NaN(), math.Inf(-1), 9845.768, -959.7485, 905, 7.8, 7.8}
15+
var strs = [...]string{"", "Hello", "foo", "bar", "foo", "f00", "%*&^*&^&", "***"}
16+
17+
func TestSortIntSlice(t *testing.T) {
18+
data := ints[:]
19+
Sort(data)
20+
if !IsSorted(data) {
21+
t.Errorf("sorted %v", ints)
22+
t.Errorf(" got %v", data)
23+
}
24+
}
25+
26+
func TestSortFuncIntSlice(t *testing.T) {
27+
data := ints[:]
28+
SortFunc(data, func(a, b int) bool { return a < b })
29+
if !IsSorted(data) {
30+
t.Errorf("sorted %v", ints)
31+
t.Errorf(" got %v", data)
32+
}
33+
}
34+
35+
func TestSortFloat64Slice(t *testing.T) {
36+
data := float64s[:]
37+
Sort(data)
38+
if !IsSorted(data) {
39+
t.Errorf("sorted %v", float64s)
40+
t.Errorf(" got %v", data)
41+
}
42+
}
43+
44+
func TestSortStringSlice(t *testing.T) {
45+
data := strs[:]
46+
Sort(data)
47+
if !IsSorted(data) {
48+
t.Errorf("sorted %v", strs)
49+
t.Errorf(" got %v", data)
50+
}
51+
}
52+
53+
func TestSortLarge_Random(t *testing.T) {
54+
n := 1000000
55+
if testing.Short() {
56+
n /= 100
57+
}
58+
data := make([]int, n)
59+
for i := 0; i < len(data); i++ {
60+
data[i] = rand.Intn(100)
61+
}
62+
if IsSorted(data) {
63+
t.Fatalf("terrible rand.rand")
64+
}
65+
Sort(data)
66+
if !IsSorted(data) {
67+
t.Errorf("sort didn't sort - 1M ints")
68+
}
69+
}
70+
71+
type intPair struct {
72+
a, b int
73+
}
74+
75+
type intPairs []intPair
76+
77+
// Pairs compare on a only.
78+
func intPairLess(x, y intPair) bool {
79+
return x.a < y.a
80+
}
81+
82+
// Record initial order in B.
83+
func (d intPairs) initB() {
84+
for i := range d {
85+
d[i].b = i
86+
}
87+
}
88+
89+
// InOrder checks if a-equal elements were not reordered.
90+
func (d intPairs) inOrder() bool {
91+
lastA, lastB := -1, 0
92+
for i := 0; i < len(d); i++ {
93+
if lastA != d[i].a {
94+
lastA = d[i].a
95+
lastB = d[i].b
96+
continue
97+
}
98+
if d[i].b <= lastB {
99+
return false
100+
}
101+
lastB = d[i].b
102+
}
103+
return true
104+
}
105+
106+
func TestStability(t *testing.T) {
107+
n, m := 100000, 1000
108+
if testing.Short() {
109+
n, m = 1000, 100
110+
}
111+
data := make(intPairs, n)
112+
113+
// random distribution
114+
for i := 0; i < len(data); i++ {
115+
data[i].a = rand.Intn(m)
116+
}
117+
if IsSortedFunc(data, intPairLess) {
118+
t.Fatalf("terrible rand.rand")
119+
}
120+
data.initB()
121+
SortStableFunc(data, intPairLess)
122+
if !IsSortedFunc(data, intPairLess) {
123+
t.Errorf("Stable didn't sort %d ints", n)
124+
}
125+
if !data.inOrder() {
126+
t.Errorf("Stable wasn't stable on %d ints", n)
127+
}
128+
129+
// already sorted
130+
data.initB()
131+
SortStableFunc(data, intPairLess)
132+
if !IsSortedFunc(data, intPairLess) {
133+
t.Errorf("Stable shuffled sorted %d ints (order)", n)
134+
}
135+
if !data.inOrder() {
136+
t.Errorf("Stable shuffled sorted %d ints (stability)", n)
137+
}
138+
139+
// sorted reversed
140+
for i := 0; i < len(data); i++ {
141+
data[i].a = len(data) - i
142+
}
143+
data.initB()
144+
SortStableFunc(data, intPairLess)
145+
if !IsSortedFunc(data, intPairLess) {
146+
t.Errorf("Stable didn't sort %d ints", n)
147+
}
148+
if !data.inOrder() {
149+
t.Errorf("Stable wasn't stable on %d ints", n)
150+
}
151+
}
152+
153+
func TestBinarySearch(t *testing.T) {
154+
data := []string{"aa", "ad", "ca", "xy"}
155+
tests := []struct {
156+
target string
157+
want int
158+
}{
159+
{"aa", 0},
160+
{"ab", 1},
161+
{"ad", 1},
162+
{"ax", 2},
163+
{"ca", 2},
164+
{"cc", 3},
165+
{"dd", 3},
166+
{"xy", 3},
167+
{"zz", 4},
168+
}
169+
for _, tt := range tests {
170+
t.Run(tt.target, func(t *testing.T) {
171+
i := BinarySearch(data, tt.target)
172+
if i != tt.want {
173+
t.Errorf("BinarySearch want %d, got %d", tt.want, i)
174+
}
175+
176+
j := BinarySearchFunc(data, func(s string) bool { return s >= tt.target })
177+
if j != tt.want {
178+
t.Errorf("BinarySearchFunc want %d, got %d", tt.want, j)
179+
}
180+
})
181+
}
182+
}

0 commit comments

Comments
 (0)