Exercise: Slice Operations Master
Difficulty - Beginner
📖 Background: This exercise builds upon the comprehensive slice coverage in Slices and Arrays. You should be familiar with basic slice concepts, capacity, and length before starting this exercise.
Learning Objectives
- Master basic slice operations in Go
- Understand slice capacity and length
- Practice appending, slicing, and copying
- Learn common slice manipulation patterns
Problem Statement
Create a program that implements various slice operations. You'll build a SliceUtils package with the following functions:
- RemoveDuplicates: Remove duplicate elements from a slice
- Reverse: Reverse a slice in place
- Chunk: Split a slice into smaller chunks of a given size
- Flatten: Flatten a 2D slice into a 1D slice
- Filter: Keep only elements that satisfy a condition
Function Signatures
1package sliceutils
2
3// RemoveDuplicates returns a new slice with duplicates removed
4func RemoveDuplicates(slice []int) []int
5
6// Reverse reverses the slice in place
7func Reverse(slice []int)
8
9// Chunk splits a slice into chunks of size n
10func Chunk(slice []int, size int) [][]int
11
12// Flatten converts a 2D slice into a 1D slice
13func Flatten(slice [][]int) []int
14
15// Filter returns elements that satisfy the predicate function
16func Filter(slice []int, predicate func(int) bool) []int
Example Usage
1package main
2
3import (
4 "fmt"
5 "sliceutils"
6)
7
8func main() {
9 // RemoveDuplicates
10 nums := []int{1, 2, 2, 3, 4, 4, 5}
11 unique := sliceutils.RemoveDuplicates(nums)
12 fmt.Println(unique) // [1 2 3 4 5]
13
14 // Reverse
15 values := []int{1, 2, 3, 4, 5}
16 sliceutils.Reverse(values)
17 fmt.Println(values) // [5 4 3 2 1]
18
19 // Chunk
20 data := []int{1, 2, 3, 4, 5, 6, 7, 8}
21 chunks := sliceutils.Chunk(data, 3)
22 fmt.Println(chunks) // [[1 2 3] [4 5 6] [7 8]]
23
24 // Flatten
25 matrix := [][]int{{1, 2}, {3, 4}, {5, 6}}
26 flat := sliceutils.Flatten(matrix)
27 fmt.Println(flat) // [1 2 3 4 5 6]
28
29 // Filter
30 numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
31 evens := sliceutils.Filter(numbers, func(n int) bool {
32 return n%2 == 0
33 })
34 fmt.Println(evens) // [2 4 6 8 10]
35}
Requirements
- All functions should handle empty slices gracefully
- RemoveDuplicates should preserve the order of first occurrence
- Reverse should modify the slice in place
- Chunk should handle cases where the slice size isn't evenly divisible
- Filter should not modify the original slice
Test Cases
Your implementation should pass these test cases:
1package sliceutils
2
3import (
4 "reflect"
5 "testing"
6)
7
8func TestRemoveDuplicates(t *testing.T) {
9 tests := []struct {
10 input []int
11 expected []int
12 }{
13 {[]int{1, 2, 2, 3, 4, 4, 5}, []int{1, 2, 3, 4, 5}},
14 {[]int{1, 1, 1, 1}, []int{1}},
15 {[]int{}, []int{}},
16 {[]int{1, 2, 3}, []int{1, 2, 3}},
17 }
18
19 for _, tt := range tests {
20 result := RemoveDuplicates(tt.input)
21 if !reflect.DeepEqual(result, tt.expected) {
22 t.Errorf("RemoveDuplicates(%v) = %v; want %v", tt.input, result, tt.expected)
23 }
24 }
25}
26
27func TestReverse(t *testing.T) {
28 tests := []struct {
29 input []int
30 expected []int
31 }{
32 {[]int{1, 2, 3, 4, 5}, []int{5, 4, 3, 2, 1}},
33 {[]int{1}, []int{1}},
34 {[]int{}, []int{}},
35 {[]int{1, 2}, []int{2, 1}},
36 }
37
38 for _, tt := range tests {
39 Reverse(tt.input)
40 if !reflect.DeepEqual(tt.input, tt.expected) {
41 t.Errorf("After Reverse, got %v; want %v", tt.input, tt.expected)
42 }
43 }
44}
45
46func TestChunk(t *testing.T) {
47 tests := []struct {
48 input []int
49 size int
50 expected [][]int
51 }{
52 {[]int{1, 2, 3, 4, 5, 6}, 2, [][]int{{1, 2}, {3, 4}, {5, 6}}},
53 {[]int{1, 2, 3, 4, 5}, 2, [][]int{{1, 2}, {3, 4}, {5}}},
54 {[]int{}, 2, [][]int{}},
55 {[]int{1, 2, 3}, 5, [][]int{{1, 2, 3}}},
56 }
57
58 for _, tt := range tests {
59 result := Chunk(tt.input, tt.size)
60 if !reflect.DeepEqual(result, tt.expected) {
61 t.Errorf("Chunk(%v, %d) = %v; want %v", tt.input, tt.size, result, tt.expected)
62 }
63 }
64}
65
66func TestFlatten(t *testing.T) {
67 tests := []struct {
68 input [][]int
69 expected []int
70 }{
71 {[][]int{{1, 2}, {3, 4}, {5, 6}}, []int{1, 2, 3, 4, 5, 6}},
72 {[][]int{{1}}, []int{1}},
73 {[][]int{}, []int{}},
74 {[][]int{{}, {1, 2}, {}}, []int{1, 2}},
75 }
76
77 for _, tt := range tests {
78 result := Flatten(tt.input)
79 if !reflect.DeepEqual(result, tt.expected) {
80 t.Errorf("Flatten(%v) = %v; want %v", tt.input, result, tt.expected)
81 }
82 }
83}
84
85func TestFilter(t *testing.T) {
86 isEven := func(n int) bool { return n%2 == 0 }
87 isPositive := func(n int) bool { return n > 0 }
88
89 tests := []struct {
90 input []int
91 predicate func(int) bool
92 expected []int
93 }{
94 {[]int{1, 2, 3, 4, 5, 6}, isEven, []int{2, 4, 6}},
95 {[]int{-2, -1, 0, 1, 2}, isPositive, []int{1, 2}},
96 {[]int{}, isEven, []int{}},
97 {[]int{1, 3, 5}, isEven, []int{}},
98 }
99
100 for _, tt := range tests {
101 result := Filter(tt.input, tt.predicate)
102 if !reflect.DeepEqual(result, tt.expected) {
103 t.Errorf("Filter(%v) = %v; want %v", tt.input, result, tt.expected)
104 }
105 }
106}
Hints
Hint 1: RemoveDuplicates
Use a map to track which elements you've already seen. Iterate through the slice and only add elements to the result if they haven't been seen before.
1seen := make(map[int]bool)
Hint 2: Reverse
Use two pointers, one at the start and one at the end. Swap elements and move the pointers toward each other until they meet.
1for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 {
2 // swap slice[i] and slice[j]
3}
Hint 3: Chunk
Iterate through the slice in steps of size. For each chunk, use slicing to extract the elements. Handle the last chunk carefully if it's smaller than size.
Hint 4: Flatten
Pre-calculate the total size of the flattened slice to avoid multiple allocations. Then iterate through the 2D slice and append all elements.
Hint 5: Filter
Create a new slice with capacity 0 and append only elements that satisfy the predicate function.
Solution
Click to see the complete solution
1package sliceutils
2
3// RemoveDuplicates returns a new slice with duplicates removed
4func RemoveDuplicates(slice []int) []int {
5 if len(slice) == 0 {
6 return []int{}
7 }
8
9 seen := make(map[int]bool)
10 result := make([]int, 0, len(slice))
11
12 for _, value := range slice {
13 if !seen[value] {
14 seen[value] = true
15 result = append(result, value)
16 }
17 }
18
19 return result
20}
21
22// Reverse reverses the slice in place
23func Reverse(slice []int) {
24 for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 {
25 slice[i], slice[j] = slice[j], slice[i]
26 }
27}
28
29// Chunk splits a slice into chunks of size n
30func Chunk(slice []int, size int) [][]int {
31 if len(slice) == 0 {
32 return [][]int{}
33 }
34
35 if size <= 0 {
36 return [][]int{slice}
37 }
38
39 numChunks := + size - 1) / size // Ceiling division
40 result := make([][]int, 0, numChunks)
41
42 for i := 0; i < len(slice); i += size {
43 end := i + size
44 if end > len(slice) {
45 end = len(slice)
46 }
47 result = append(result, slice[i:end])
48 }
49
50 return result
51}
52
53// Flatten converts a 2D slice into a 1D slice
54func Flatten(slice [][]int) []int {
55 if len(slice) == 0 {
56 return []int{}
57 }
58
59 // Calculate total size to avoid multiple allocations
60 totalSize := 0
61 for _, inner := range slice {
62 totalSize += len(inner)
63 }
64
65 result := make([]int, 0, totalSize)
66 for _, inner := range slice {
67 result = append(result, inner...)
68 }
69
70 return result
71}
72
73// Filter returns elements that satisfy the predicate function
74func Filter(slice []int, predicate func(int) bool) []int {
75 result := make([]int, 0)
76
77 for _, value := range slice {
78 if predicate(value) {
79 result = append(result, value)
80 }
81 }
82
83 return result
84}
Explanation
RemoveDuplicates:
- Uses a map to track seen elements in O(n) time
- Maintains insertion order by appending to result slice
- Pre-allocates result slice with capacity equal to input length
Reverse:
- Uses two-pointer technique for in-place reversal
- Swaps elements from both ends moving toward center
- Time complexity: O(n/2) = O(n), Space complexity: O(1)
Chunk:
- Calculates number of chunks needed upfront
- Uses slice expressions to create sub-slices
- Handles remainder correctly when size doesn't divide evenly
Flatten:
- Pre-calculates total size to allocate result slice once
- Uses
append(result, inner...)to efficiently append entire slices - Avoids unnecessary allocations
Filter:
- Creates new slice without modifying original
- Uses predicate function for flexible filtering
- Could be optimized by pre-allocating with estimated capacity
Alternative Implementations
RemoveDuplicates:
1func RemoveDuplicates(slice []int) []int {
2 result := make([]int, 0, len(slice))
3
4 for _, value := range slice {
5 found := false
6 for _, existing := range result {
7 if existing == value {
8 found = true
9 break
10 }
11 }
12 if !found {
13 result = append(result, value)
14 }
15 }
16
17 return result
18}
Filter:
1func Filter(slice []int, predicate func(int) bool) []int {
2 result := make([]int, 0, len(slice)/2) // Estimate half will match
3
4 for _, value := range slice {
5 if predicate(value) {
6 result = append(result, value)
7 }
8 }
9
10 return result
11}
Bonus Challenges
Once you've completed the basic implementation, try these extensions:
- Generic Version: Implement these functions using Go generics to work with any slice type
1func RemoveDuplicates[T comparable](slice []T) []T
2func Reverse[T any](slice []T)
3func Chunk[T any](slice []T, size int) [][]T
-
Additional Operations: Implement these related functions:
Unique: Check if all elements are uniqueContains: Check if a slice contains a valueIndexOf: Find the index of a valueMap: Transform each element using a function
-
Performance Optimization: Benchmark your implementations and optimize them
1func BenchmarkRemoveDuplicates(b *testing.B) {
2 data := make([]int, 1000)
3 for i := range data {
4 data[i] = i % 100 // Create duplicates
5 }
6
7 b.ResetTimer()
8 for i := 0; i < b.N; i++ {
9 RemoveDuplicates(data)
10 }
11}
Key Takeaways
- Maps are useful for tracking unique values with O(1) lookup time
- In-place algorithms save memory but modify the original data
- Pre-allocating slices with appropriate capacity reduces allocations
- Slicing operations create new slice headers but share underlying arrays
- The
...operator efficiently appends entire slices
Understanding these slice operations is fundamental to writing efficient Go code. Slices are used everywhere in Go, and mastering their manipulation patterns will make you a more effective Go programmer.