~telemachus/algorithms

bb3d5e2ec6fa23056f1f2dff59fb71dcb6c37f66 — Peter Aronoff 10 months ago
Initial commit
A  => README.md +11 -0
@@ 1,11 @@
# Algorithms in Go

This repository implements several basic or well-known algorithms in Go. 

Nothing here is really mine. In most cases, I’ve simply followed pseudocode from [Introduction to Algorithms][ita]. [In one case][ybg], I’ve copied code from [Stefan Nilsson][sn].

In any case, I wrote this to learn a little more about algorithms and to get more comfortable with Go. You shouldn’t use it for anything that matters.

[ita]: https://mitpress.mit.edu/books/introduction-algorithms-third-edition
[ybg]: https://yourbasic.org/golang/quicksort-optimizations
[sn]: https://yourbasic.org/about

A  => algorithm-notes.md +27 -0
@@ 1,27 @@
# Notes on Algorithms

## Loop Invariants

Loop invariants help us to see that an algorithm is correct. Three things should be true about loop invariants. (See *Introduction to Algorithms*[^1], pages 18-20.)

1. Initialization: the loop invariant is true before the first iteration of the loop.
1. Maintenance: the loop invariant is true after each iteration of the loop. (Note that the loop invariant may become temporarily false during the loop, but by the end of the loop, it must be true.)
1. Termination: when the loop ends, the invariant gives us a useful property that helps show that the algorithm is correct.

## Loop Invariants and Insertion Sort

As an example, let’s consider the loop invariants for insertion sort. The loop invariant for insertion sort are the following: first, for each iteration through the outer loop, where `j` is an index of the loop from `1` to `len(slice)-1`, the subslice `slice[0:j-1]` contains the elements from `slice[0:j-1]`, but in sorted order; second, the inner loop only adds items to `xs[i:j]` if those elements are less than or equal to `key`. (I think that there should be a better way to describe the second invariant.)

### Loop Invariant 1

1. Initialization: initially the subslice `xs[0:1]` contains one item; therefore, it is trivially in order.
1. Maintenance: informally, the point of the inner loop is to move `key` down as far as possible; therefore, at the end of the inner loop, everything in `xs[i:j]` is greater than or equal to key. More formally, we explicitly test whether `xs[i] > key` before doing anything. If `xs[i]` is not greater than `key`, then `i` remains unchanged and when we run `xs[i+1] = key`, we are placing `key` back where it started since `i` was defined as `j-1`. Therefore, `i+1` *is* j whenever i has not changed. On the other hand, if `xs[i]` is greater than `key`, then we do two things. First, we slide `xs[i]` rightward. Second, we decrease `i`. Thus, when we assign key to `xs[i+1]`, `xs[i+1]` has become `j-1`. Since the key was previously `xs[j]`, we have thus moved `key` down one and the larger number up one. Thus, `xs[0:j-1]` maintains the first invariant.
1. Termination: when the outer loop finishes, `j` is `len(xs)`. Thus, we can say that the entire slice (`xs[0:len(xs)]`) is in order when the outer loop finishes.

### Loop Invariant 2

1. Initialization: before the loop begins, nothing has been added to `xs[i:j]`, so the invariant is true in a trivial way.
1. Maintenance: the inner loop has a test that insures that anything larger than `key` is moved upwards. Thus, anything larger than key will end up somewhere in `xs[j:]`, and the invariant remains true.
1. Termination: the inner loop ends in one of two ways. First, the loop ends if reach a value of `i` where `xs[i]` is less than or equal to `key`. (Any such items are kept in `xs[i:j]`.) Thus, if the loop ends in this way, the invariant survives termination. Second, the loop explicitly tests for when `i` becomes less than zero. (This test protects us from attempting to index outside the bounds of a slice.) The lower bound of `xs[i:j]` is therefore zero, and based on initialization and maintenance, we know that `xs[0:j]` will have nothing added to it that isn’t smaller than or equal to `key`.

[^1]: *Introduction to Algorithms*, 3rd edition, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. The MIT Press (Cambridge, MA), 2009.

A  => benchmark_quickselect_test.go +103 -0
@@ 1,103 @@
package algorithms_test

import (
	"testing"

	"git.sr.ht/~telemachus/algorithms"
)

func BenchmarkLomutoQuickselectShuffledSlice(b *testing.B) {
	xs := shuffledSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectL(xs, 0, len(xs)-1, 3567)
	}
}

func BenchmarkHoareQuickselectShuffledSlice(b *testing.B) {
	xs := shuffledSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectH(xs, 0, len(xs)-1, 3567)
	}
}

func BenchmarkYourBasicQuickselectShuffledSlice(b *testing.B) {
	xs := shuffledSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectYB(xs, 3567)
	}
}

func BenchmarkLomutoQuickselectEqualSlice(b *testing.B) {
	xs := equalSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectL(xs, 0, len(xs)-1, 3567)
	}
}

func BenchmarkHoareQuickselectEqualSlice(b *testing.B) {
	xs := equalSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectH(xs, 0, len(xs)-1, 3567)
	}
}

func BenchmarkYourBasicQuickselectEqualSlice(b *testing.B) {
	xs := equalSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectYB(xs, 3567)
	}
}

func BenchmarkLomutoQuickselectInOrderSlice(b *testing.B) {
	xs := inOrderSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectL(xs, 0, len(xs)-1, 3567)
	}
}

func BenchmarkHoareQuickselectInOrderSlice(b *testing.B) {
	xs := inOrderSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectH(xs, 0, len(xs)-1, 3567)
	}
}

func BenchmarkYourBasicQuickselectInOrderSlice(b *testing.B) {
	xs := inOrderSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectYB(xs, 3567)
	}
}

func BenchmarkLomutoQuickselectReversedSlice(b *testing.B) {
	xs := reversedSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectL(xs, 0, len(xs)-1, 3567)
	}
}

func BenchmarkHoareQuickselectReversedSlice(b *testing.B) {
	xs := reversedSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectH(xs, 0, len(xs)-1, 3567)
	}
}

func BenchmarkYourBasicQuickselectReversedSlice(b *testing.B) {
	xs := reversedSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuickselectYB(xs, 3567)
	}
}

A  => benchmark_quicksort_test.go +181 -0
@@ 1,181 @@
package algorithms_test

import (
	"math/rand"
	"sort"
	"testing"

	"git.sr.ht/~telemachus/algorithms"
)

func shuffledSlice() sort.IntSlice {
	var xs sort.IntSlice
	xs = make([]int, 10000)
	rand.Shuffle(len(xs), func(i, j int) {
		xs[i], xs[j] = xs[j], xs[i]
	})

	return xs
}

func equalSlice() sort.IntSlice {
	var xs sort.IntSlice
	xs = make([]int, 10000)
	for i := 0; i < 10000; i++ {
		xs[i] = 1
	}

	return xs
}

func inOrderSlice() sort.IntSlice {
	var xs sort.IntSlice
	xs = make([]int, 10000)
	for i := 0; i < 10000; i++ {
		xs[i] = i
	}

	return xs
}

func reversedSlice() sort.IntSlice {
	var xs sort.IntSlice
	xs = make([]int, 10000)
	for i, j := 0, 10000; i < 10000; i, j = i+1, j-1 {
		xs[i] = j
	}

	return xs
}

func BenchmarkLomutoQuicksortShuffledSlice(b *testing.B) {
	xs := shuffledSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortL(xs, 0, len(xs)-1)
	}
}

func BenchmarkHoareQuicksortShuffledSlice(b *testing.B) {
	xs := shuffledSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortH(xs, 0, len(xs)-1)
	}
}

func BenchmarkStdlibSortShuffledSlice(b *testing.B) {
	xs := shuffledSlice()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		xs.Sort()
	}
}

func BenchmarkYourBasicQuicksortShuffledSlice(b *testing.B) {
	xs := shuffledSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortYB(xs)
	}
}

func BenchmarkLomutoQuicksortEqualSlice(b *testing.B) {
	xs := equalSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortL(xs, 0, len(xs)-1)
	}
}

func BenchmarkHoareQuicksortEqualSlice(b *testing.B) {
	xs := equalSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortH(xs, 0, len(xs)-1)
	}
}

func BenchmarkStdlibSortEqualSlice(b *testing.B) {
	xs := equalSlice()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		xs.Sort()
	}
}

func BenchmarkYourBasicQuicksortEqualSlice(b *testing.B) {
	xs := equalSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortYB(xs)
	}
}

func BenchmarkLomutoQuicksortInOrderSlice(b *testing.B) {
	xs := inOrderSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortL(xs, 0, len(xs)-1)
	}
}

func BenchmarkHoareQuicksortInOrderSlice(b *testing.B) {
	xs := inOrderSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortH(xs, 0, len(xs)-1)
	}
}

func BenchmarkStdlibSortInOrderSlice(b *testing.B) {
	xs := inOrderSlice()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		xs.Sort()
	}
}

func BenchmarkYourBasicQuicksortInOrderSlice(b *testing.B) {
	xs := inOrderSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortYB(xs)
	}
}

func BenchmarkLomutoQuicksortReversedSlice(b *testing.B) {
	xs := reversedSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortL(xs, 0, len(xs)-1)
	}
}

func BenchmarkHoareQuicksortReversedSlice(b *testing.B) {
	xs := reversedSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortH(xs, 0, len(xs)-1)
	}
}

func BenchmarkStdlibSortReversedSlice(b *testing.B) {
	xs := reversedSlice()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		xs.Sort()
	}
}

func BenchmarkYourBasicQuicksortReversedSlice(b *testing.B) {
	xs := reversedSlice()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		algorithms.QuicksortYB(xs)
	}
}

A  => binarysearch.go +20 -0
@@ 1,20 @@
package algorithms

func BinarySearch(xs []int, wanted int) int {
	min := 0
	max := len(xs) - 1
	var guess int

	for min <= max {
		guess = (min + max) / 2
		switch {
		case xs[guess] < wanted:
			min = guess + 1
		case xs[guess] > wanted:
			max = guess - 1
		default:
			return guess
		}
	}
	return -1
}

A  => binarysearch_test.go +31 -0
@@ 1,31 @@
package algorithms_test

import (
	"testing"

	"git.sr.ht/~telemachus/algorithms"
)

func TestBinarySearch(t *testing.T) {
	tests := map[string]struct {
		slice    []int
		wanted   int
		expected int
	}{
		"empty slice":                 {[]int{}, 3, -1},
		"wanted not in slice":         {[]int{1, 2, 3}, 4, -1},
		"wanted is a negative number": {[]int{1, 2, 3}, -4, -1},
		"wanted at start of slice":    {[]int{1, 2, 3}, 1, 0},
		"wanted at end of slice":      {[]int{1, 2, 3, 4, 5, 6, 7}, 7, 6},
		"wanted in middle of slice":   {[]int{1, 2, 3, 4, 5, 6, 7}, 4, 3},
	}

	for msg, tc := range tests {
		t.Run(msg, func(t *testing.T) {
			actual := algorithms.BinarySearch(tc.slice, tc.wanted)
			if tc.expected != actual {
				t.Errorf("expected %d; actual %d; given %#v", tc.expected, actual, tc.slice)
			}
		})
	}
}

A  => go.mod +3 -0
@@ 1,3 @@
module git.sr.ht/~telemachus/algorithms

go 1.17

A  => hoarepartition.go +36 -0
@@ 1,36 @@
package algorithms

func hoarePartition(xs []int, low, high int) int {
	pivotValue := xs[(high+low)/2]
	i := low - 1
	j := high + 1

	for {
		for i = i + 1; xs[i] < pivotValue; i++ {
		}
		for j = j - 1; xs[j] > pivotValue; j-- {
		}

		// The loops below are a more literal version of Hoare’s partition,
		// but they are far less idiomatic Go.
		//
		// for {
		// 	i++
		// 	if xs[i] >= pivotValue {
		// 		break
		// 	}
		// }
		// for {
		// 	j--
		// 	if xs[j] <= pivotValue {
		// 		break
		// 	}
		// }

		if i < j {
			xs[i], xs[j] = xs[j], xs[i]
		} else {
			return j
		}
	}
}

A  => insertionsort.go +18 -0
@@ 1,18 @@
package algorithms

// InsertionSort sorts a slice in place.
func InsertionSort(xs []int) {
	// Loop invariant 1: xs[:j-1] contains the items originally in that
	// subslice but in sorted order.
	for j := 1; j < len(xs); j++ {
		key := xs[j]
		i := j - 1

		// Loop invariant 2: xs[i:j] accepts items only <= key.
		for i >= 0 && xs[i] > key {
			xs[i+1] = xs[i]
			i--
		}
		xs[i+1] = key
	}
}

A  => insertionsort_test.go +38 -0
@@ 1,38 @@
package algorithms_test

import (
	"reflect"
	"testing"

	"git.sr.ht/~telemachus/algorithms"
)

func TestInsertionSort(t *testing.T) {
	tests := map[string]struct {
		slice    []int
		expected []int
	}{
		"empty slice":                 {[]int{}, []int{}},
		"one-item slice":              {[]int{14}, []int{14}},
		"three-item slice":            {[]int{15, 16, 14}, []int{14, 15, 16}},
		"four-item slice":             {[]int{15, 16, 17, 14}, []int{14, 15, 16, 17}},
		"five-item slice":             {[]int{5, 6, 7, 4, 8}, []int{4, 5, 6, 7, 8}},
		"slice containing duplicates": {[]int{2, 4, 2, 3, 1, 2}, []int{1, 2, 2, 2, 3, 4}},
		"ten-item slice": {[]int{16, 14, 15, 8, 1, 3, 5, 9, 2, 4},
			[]int{1, 2, 3, 4, 5, 8, 9, 14, 15, 16}},
		"ten-item slice, reversed": {[]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
			[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
	}

	for msg, tc := range tests {
		t.Run(msg, func(t *testing.T) {
			original := make([]int, len(tc.slice))
			copy(original, tc.slice)
			algorithms.InsertionSort(tc.slice)

			if !reflect.DeepEqual(tc.expected, tc.slice) {
				t.Errorf("expected %#v, actual %#v, given %#v", tc.expected, tc.slice, original)
			}
		})
	}
}

A  => lomutopartition.go +17 -0
@@ 1,17 @@
package algorithms

func lomutoPartition(xs []int, low, high int) int {
	pivotValue := xs[high]
	i := low - 1

	for j := low; j < high; j++ {
		if xs[j] <= pivotValue {
			i++
			xs[i], xs[j] = xs[j], xs[i]
		}
	}

	i++
	xs[i], xs[high] = xs[high], xs[i]
	return i
}

A  => quickselect.go +50 -0
@@ 1,50 @@
package algorithms

// QuickselectL implements quickselect using Lomuto’s partition.
func QuickselectL(xs []int, low, high, nthLowest int) int {
	if len(xs) <= 0 || nthLowest > len(xs) {
		return -1
	}

	switch pivotIndex := lomutoPartition(xs, low, high); {
	case nthLowest > pivotIndex:
		return QuickselectL(xs, pivotIndex+1, high, nthLowest)
	case nthLowest < pivotIndex:
		return QuickselectL(xs, low, pivotIndex-1, nthLowest)
	default:
		return xs[nthLowest]
	}
}

// QuickselectH implements quickselect using Hoare’s partition.
func QuickselectH(xs []int, low, high, nthLowest int) int {
	if len(xs) <= 0 || nthLowest > len(xs) {
		return -1
	}

	switch pivotIndex := hoarePartition(xs, low, high); {
	case nthLowest > pivotIndex:
		return QuickselectH(xs, pivotIndex+1, high, nthLowest)
	case nthLowest < pivotIndex:
		return QuickselectH(xs, low, pivotIndex-1, nthLowest)
	default:
		return xs[nthLowest]
	}
}

// QuickselectYB implements quickselect using Stefan Nilsson’s partition.
func QuickselectYB(xs []int, nthLowest int) int {
	if len(xs) <= 0 || nthLowest > len(xs) {
		return -1
	}

	pivotValue := yourBasicPivot(xs)
	switch low, high := yourBasicPartition(xs, pivotValue); {
	case nthLowest <= low:
		QuicksortYB(xs[:low])
		return xs[nthLowest]
	default:
		QuicksortYB(xs[high:])
		return xs[nthLowest]
	}
}

A  => quickselect_test.go +57 -0
@@ 1,57 @@
package algorithms_test

import (
	"testing"

	"git.sr.ht/~telemachus/algorithms"
)

var selectTests = map[string]struct {
	given     []int
	nthLowest int
	expected  int
}{
	"empty slice":                           {[]int{}, 0, -1},
	"nthLowest > len(xs)":                   {[]int{1}, 4, -1},
	"one-item slice":                        {[]int{14}, 0, 14},
	"three-item slice; nthLowest at start":  {[]int{15, 16, 14}, 0, 14},
	"three-item slice; nthLowest in middle": {[]int{15, 16, 14}, 1, 15},
	"three-item slice; nthLowest at end":    {[]int{15, 16, 14}, 2, 16},
	"ten-item slice, nthLowest in middle":   {[]int{16, 14, 15, 8, 1, 3, 5, 9, 2, 4}, 4, 5},
}

func TestLomutoQuickselect(t *testing.T) {
	for msg, tc := range selectTests {
		t.Run(msg, func(t *testing.T) {
			actual := algorithms.QuickselectL(tc.given, 0, len(tc.given)-1, tc.nthLowest)

			if tc.expected != actual {
				t.Errorf("expected %d; actual %d", tc.expected, actual)
			}
		})
	}
}

func TestHoareQuickselect(t *testing.T) {
	for msg, tc := range selectTests {
		t.Run(msg, func(t *testing.T) {
			actual := algorithms.QuickselectH(tc.given, 0, len(tc.given)-1, tc.nthLowest)

			if tc.expected != actual {
				t.Errorf("expected %d; actual %d", tc.expected, actual)
			}
		})
	}
}

func TestYourBasicQuickselect(t *testing.T) {
	for msg, tc := range selectTests {
		t.Run(msg, func(t *testing.T) {
			actual := algorithms.QuickselectYB(tc.given, tc.nthLowest)

			if tc.expected != actual {
				t.Errorf("expected %d; actual %d", tc.expected, actual)
			}
		})
	}
}

A  => quicksort.go +39 -0
@@ 1,39 @@
package algorithms

// QuicksortL implements quicksort using Lomuto’s partition.
func QuicksortL(xs []int, low, high int) {
	if high-low <= 0 {
		return
	}

	pivotIndex := lomutoPartition(xs, low, high)
	QuicksortL(xs, low, pivotIndex-1)
	QuicksortL(xs, pivotIndex+1, high)
}

// QuicksortH implements quicksort using Hoare’s partition.
func QuicksortH(xs []int, low, high int) {
	if high-low <= 0 {
		return
	}

	pivotIndex := hoarePartition(xs, low, high)
	QuicksortH(xs, low, pivotIndex)
	QuicksortH(xs, pivotIndex+1, high)
}

// QuicksortYB implements quicksort using Stefan Nilsson’s partition.
// See the following links:
// - https://yourbasic.org/golang/quicksort-optimizations
// - https://yourbasic.org/about
func QuicksortYB(xs []int) {
	if len(xs) < 20 {
		InsertionSort(xs)
		return
	}

	pivotIndex := yourBasicPivot(xs)
	low, high := yourBasicPartition(xs, pivotIndex)
	QuicksortYB(xs[:low])
	QuicksortYB(xs[high:])
}

A  => quicksort_test.go +60 -0
@@ 1,60 @@
package algorithms_test

import (
	"reflect"
	"testing"

	"git.sr.ht/~telemachus/algorithms"
)

var tests = map[string]struct {
	given    []int
	expected []int
}{
	"empty slice":                 {[]int{}, []int{}},
	"one-item slice":              {[]int{14}, []int{14}},
	"three-item slice":            {[]int{15, 16, 14}, []int{14, 15, 16}},
	"four-item slice":             {[]int{15, 16, 17, 14}, []int{14, 15, 16, 17}},
	"five-item slice":             {[]int{5, 6, 7, 4, 8}, []int{4, 5, 6, 7, 8}},
	"slice containing duplicates": {[]int{2, 4, 2, 3, 1, 2}, []int{1, 2, 2, 2, 3, 4}},
	"ten-item slice": {[]int{16, 14, 15, 8, 1, 3, 5, 9, 2, 4},
		[]int{1, 2, 3, 4, 5, 8, 9, 14, 15, 16}},
	"ten-item slice, reversed": {[]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
		[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
}

func TestQuicksortL(t *testing.T) {
	for msg, tc := range tests {
		t.Run(msg, func(t *testing.T) {
			algorithms.QuicksortL(tc.given, 0, len(tc.given)-1)

			if !reflect.DeepEqual(tc.expected, tc.given) {
				t.Errorf("expected %#v; actual %#v", tc.expected, tc.given)
			}
		})
	}
}

func TestQuicksortH(t *testing.T) {
	for msg, tc := range tests {
		t.Run(msg, func(t *testing.T) {
			algorithms.QuicksortH(tc.given, 0, len(tc.given)-1)

			if !reflect.DeepEqual(tc.expected, tc.given) {
				t.Errorf("expected %#v; actual %#v", tc.expected, tc.given)
			}
		})
	}
}

func TestQuicksortYB(t *testing.T) {
	for msg, tc := range tests {
		t.Run(msg, func(t *testing.T) {
			algorithms.QuicksortYB(tc.given)

			if !reflect.DeepEqual(tc.expected, tc.given) {
				t.Errorf("expected %#v; actual %#v", tc.expected, tc.given)
			}
		})
	}
}

A  => selectionsort.go +18 -0
@@ 1,18 @@
package algorithms

func SelectionSort(xs []int) {
	for i := 0; i < len(xs)-1; i++ {
		minIndex := findMinIndexFrom(xs, i)
		xs[i], xs[minIndex] = xs[minIndex], xs[i]
	}
}

func findMinIndexFrom(xs []int, n int) int {
	minIndex := n
	for i := n + 1; i < len(xs); i++ {
		if xs[i] < xs[minIndex] {
			minIndex = i
		}
	}
	return minIndex
}

A  => selectionsort_test.go +38 -0
@@ 1,38 @@
package algorithms_test

import (
	"reflect"
	"testing"

	"git.sr.ht/~telemachus/algorithms"
)

func TestSelectionSort(t *testing.T) {
	tests := map[string]struct {
		given    []int
		expected []int
	}{
		"empty slice":                 {[]int{}, []int{}},
		"one-item slice":              {[]int{14}, []int{14}},
		"three-item slice":            {[]int{15, 16, 14}, []int{14, 15, 16}},
		"four-item slice":             {[]int{15, 16, 17, 14}, []int{14, 15, 16, 17}},
		"five-item slice":             {[]int{5, 6, 7, 4, 8}, []int{4, 5, 6, 7, 8}},
		"slice containing duplicates": {[]int{2, 4, 2, 3, 1, 2}, []int{1, 2, 2, 2, 3, 4}},
		"ten-item slice": {[]int{16, 14, 15, 8, 1, 3, 5, 9, 2, 4},
			[]int{1, 2, 3, 4, 5, 8, 9, 14, 15, 16}},
		"ten-item slice, reversed": {[]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
			[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
	}

	for msg, tc := range tests {
		t.Run(msg, func(t *testing.T) {
			original := make([]int, len(tc.given))
			copy(original, tc.given)
			algorithms.SelectionSort(tc.given)

			if !reflect.DeepEqual(tc.expected, tc.given) {
				t.Errorf("expected %#v, actual %#v, given %#v", tc.expected, tc.given, original)
			}
		})
	}
}

A  => yourbasicpartition.go +69 -0
@@ 1,69 @@
package algorithms

import (
	"math/rand"
)

func yourBasicPivot(xs []int) int {
	n := len(xs)
	return yourBasicMedian(xs[rand.Intn(n)],
		xs[rand.Intn(n)],
		xs[rand.Intn(n)])
}

func yourBasicMedian(a, b, c int) int {
	if a < b {
		switch {
		case b < c:
			return b
		case a < c:
			return c
		default:
			return a
		}
	}
	switch {
	case a < c:
		return a
	case b < c:
		return c
	default:
		return b
	}
}

// Partition reorders the elements of xs so that:
// - all elements in xs[:low] are less than pivotValue,
// - all elements in xs[low:high] are equal to pivotValue,
// - all elements in xs[high:] are greater than pivotValue.
func yourBasicPartition(xs []int, pivotValue int) (low, high int) {
	low, high = 0, len(xs)
	for mid := 0; mid < high; {
		// Invariant:
		//  - xs[:low] < pivotValue
		//  - xs[low:mid] = pivotValue
		//  - xs[mid:high] are unknown
		//  - xs[high:] > pivotValue
		//
		//     < pivotValue  = pivotValue  unknown       > pivotValue
		//     ----------------------------------------------------
		// xs: |            |            |a            |           |
		//     ----------------------------------------------------
		//                  ^            ^             ^
		//                  low          mid           high
		switch x := xs[mid]; {
		case x < pivotValue:
			xs[mid] = xs[low]
			xs[low] = x
			low++
			mid++
		case x == pivotValue:
			mid++
		default: // x > pivotValue
			xs[mid] = xs[high-1]
			xs[high-1] = x
			high--
		}
	}
	return low, high
}