Slice Operations Master

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:

  1. RemoveDuplicates: Remove duplicate elements from a slice
  2. Reverse: Reverse a slice in place
  3. Chunk: Split a slice into smaller chunks of a given size
  4. Flatten: Flatten a 2D slice into a 1D slice
  5. 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

  1. All functions should handle empty slices gracefully
  2. RemoveDuplicates should preserve the order of first occurrence
  3. Reverse should modify the slice in place
  4. Chunk should handle cases where the slice size isn't evenly divisible
  5. 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:

  1. 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
  1. Additional Operations: Implement these related functions:

    • Unique: Check if all elements are unique
    • Contains: Check if a slice contains a value
    • IndexOf: Find the index of a value
    • Map: Transform each element using a function
  2. 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.