From dc373fecb527fa9d77f2733609a92d97303bc8b6 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 23 May 2026 16:12:24 -0400 Subject: [PATCH] feat: progress --- go.mod | 2 +- pkg/questions/design_twitter/main.go | 106 +++++++++++ .../kth_largest_element_in_stream/main.go | 51 ++++++ pkg/questions/maximum_sliding_window/main.go | 9 +- .../maximum_sliding_window/main_test.go | 65 +++++++ pkg/questions/multiply_strings/main.go | 115 ++++++++++++ pkg/questions/n_queens/main.go | 167 ++++++++++++++++++ pkg/questions/word_ladder/main.go | 60 +++++++ pkg/tools/heap/main.go | 96 +++++++++- 9 files changed, 660 insertions(+), 11 deletions(-) create mode 100644 pkg/questions/design_twitter/main.go create mode 100644 pkg/questions/kth_largest_element_in_stream/main.go create mode 100644 pkg/questions/maximum_sliding_window/main_test.go create mode 100644 pkg/questions/multiply_strings/main.go create mode 100644 pkg/questions/n_queens/main.go create mode 100644 pkg/questions/word_ladder/main.go diff --git a/go.mod b/go.mod index 2c4fb50..4f29ee7 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module git.maximhutz.com/practice -go 1.24.10 +go 1.26 require github.com/stretchr/testify v1.11.1 diff --git a/pkg/questions/design_twitter/main.go b/pkg/questions/design_twitter/main.go new file mode 100644 index 0000000..ffa2a29 --- /dev/null +++ b/pkg/questions/design_twitter/main.go @@ -0,0 +1,106 @@ +package design_twitter + +import ( + "container/heap" + "maps" + "slices" +) + +type TweetHeap [][]Tweet + +func (h TweetHeap) Len() int { return len(h) } +func (h TweetHeap) Less(i, j int) bool { + timeI, timeJ := -1, -1 + + if len(h[i]) > 0 { + timeI = h[i][len(h[i])-1].Time + } + if len(h[j]) > 0 { + timeJ = h[j][len(h[i])-1].Time + } + + return timeI > timeJ +} +func (h TweetHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *TweetHeap) Push(x any) { + *h = append(*h, x.([]Tweet)) +} + +func (h *TweetHeap) Pop() any { + old := *h + x := old[len(old)-1] + *h = old[:len(old)-1] + return x +} + +// ---------------------------------- + +type Tweet struct { + ID int + Time int +} + +type Twitter struct { + tweets map[int][]Tweet + follows map[int]map[int]bool + counter int +} + +func Constructor() Twitter { + return Twitter{ + tweets: map[int][]Tweet{}, + follows: map[int]map[int]bool{}, + counter: 0, + } +} + +func (t *Twitter) PostTweet(userId int, tweetId int) { + t.counter++ + + t.tweets[userId] = append(t.tweets[userId], Tweet{ + ID: tweetId, + Time: t.counter, + }) +} + +func (t *Twitter) GetNewsFeed(userId int) []int { + users := slices.AppendSeq([]int{userId}, maps.Keys(t.follows[userId])) + + hp := TweetHeap{} + for _, user := range users { + hp = append(hp, t.tweets[user]) + } + heap.Init(&hp) + + feed := []int{} + for range 10 { + if len(hp[0]) == 0 { + break + } + + top, rest := hp[0][len(hp[0])-1], hp[0][:len(hp[0])-1] + feed = append(feed, top.ID) + hp[0] = rest + + heap.Fix(&hp, 0) + } + + return feed +} + +func (t *Twitter) Follow(followerId int, followeeId int) { + if _, ok := t.follows[followerId]; !ok { + t.follows[followerId] = map[int]bool{} + } + + t.follows[followerId][followeeId] = true +} + +func (t *Twitter) Unfollow(followerId int, followeeId int) { + if _, ok := t.follows[followerId]; !ok { + return + } + + delete(t.follows[followerId], followeeId) +} diff --git a/pkg/questions/kth_largest_element_in_stream/main.go b/pkg/questions/kth_largest_element_in_stream/main.go new file mode 100644 index 0000000..2592c76 --- /dev/null +++ b/pkg/questions/kth_largest_element_in_stream/main.go @@ -0,0 +1,51 @@ +package kthlargestelementinstream + +import ( + "container/heap" +) + +type IntHeap []int + +func (h IntHeap) Len() int { return len(h) } +func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } +func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *IntHeap) Push(x any) { + *h = append(*h, x.(int)) +} + +func (h IntHeap) Peek() int { + return h[0] +} + +func (h *IntHeap) Pop() any { + old := *h + x := old[len(old)-1] + *h = old[:len(old)-1] + return x +} + +type KthLargest struct { + k int + largest IntHeap +} + +func Constructor(k int, nums []int) KthLargest { + result := KthLargest{k, IntHeap{}} + heap.Init(&result.largest) + + for _, n := range nums { + result.Add(n) + } + + return result +} + +func (this *KthLargest) Add(val int) int { + heap.Push(&this.largest, val) + if this.largest.Len() > this.k { + heap.Pop(&this.largest) + } + + return this.largest.Peek() +} diff --git a/pkg/questions/maximum_sliding_window/main.go b/pkg/questions/maximum_sliding_window/main.go index 16568a1..87f3710 100644 --- a/pkg/questions/maximum_sliding_window/main.go +++ b/pkg/questions/maximum_sliding_window/main.go @@ -6,6 +6,13 @@ import ( func MaxSlidingWindow(nums []int, k int) []int { h := heap.NewBy(heap.More, nums[:k]...) - result := []int{h.Data[0]} + result := []int{h.Top()} + + for i := range nums[k:] { + h.Drop(nums[i]) + h.Put(nums[i+k]) + result = append(result, h.Top()) + } + return result } diff --git a/pkg/questions/maximum_sliding_window/main_test.go b/pkg/questions/maximum_sliding_window/main_test.go new file mode 100644 index 0000000..215e2a1 --- /dev/null +++ b/pkg/questions/maximum_sliding_window/main_test.go @@ -0,0 +1,65 @@ +package maximum_sliding_window + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMaxSlidingWindow(t *testing.T) { + tests := []struct { + name string + nums []int + k int + want []int + }{ + { + name: "example 1", + nums: []int{1, 3, -1, -3, 5, 3, 6, 7}, + k: 3, + want: []int{3, 3, 5, 5, 6, 7}, + }, + { + name: "example 2", + nums: []int{1}, + k: 1, + want: []int{1}, + }, + { + name: "k equals length", + nums: []int{4, 2, 7, 1}, + k: 4, + want: []int{7}, + }, + { + name: "all same", + nums: []int{3, 3, 3, 3}, + k: 2, + want: []int{3, 3, 3}, + }, + { + name: "descending", + nums: []int{5, 4, 3, 2, 1}, + k: 3, + want: []int{5, 4, 3}, + }, + { + name: "ascending", + nums: []int{1, 2, 3, 4, 5}, + k: 3, + want: []int{3, 4, 5}, + }, + { + name: "negatives", + nums: []int{-5, -3, -1, -2, -4}, + k: 2, + want: []int{-3, -1, -1, -2}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, MaxSlidingWindow(tt.nums, tt.k)) + }) + } +} diff --git a/pkg/questions/multiply_strings/main.go b/pkg/questions/multiply_strings/main.go new file mode 100644 index 0000000..f22f1d8 --- /dev/null +++ b/pkg/questions/multiply_strings/main.go @@ -0,0 +1,115 @@ +package multiplystrings + +func addDigits(a, b byte) (byte, byte) { + abInt := int(a-'0') + int(b-'0') + return byte(abInt/10) + '0', byte(abInt%10) + '0' +} + +func addByDigit(dst *[]byte, src byte) { + for i, b := range *dst { + if src == '0' { + return + } + + src, (*dst)[i] = addDigits(b, src) + } + + if src != '0' { + *dst = append(*dst, src) + } +} + +func add(dst *[]byte, src []byte) { + carry := byte('0') + + for i := range max(len(*dst), len(src)) { + d, s := byte('0'), byte('0') + if len(*dst) > i { + d = (*dst)[i] + } + if len(src) > i { + s = src[i] + } + + hi, lo := addDigits(d, s) + total := []byte{lo, hi} + addByDigit(&total, carry) + (*dst)[i], carry = total[0], total[1] + } + + if carry != '0' { + *dst = append(*dst, carry) + } +} + +func multiplyDigits(a, b byte) (byte, byte) { + abInt := int(a-'0') * int(b-'0') + return byte(abInt/10) + '0', byte(abInt%10) + '0' +} + +func multiplyByDigit(and []byte, ier byte) []byte { + result, carry := []byte{}, byte('0') + for _, b := range and { + hi, lo := multiplyDigits(b, ier) + + // Fold the incoming carry into the low digit; any overflow rolls + // into hi, which is small enough that it never overflows again. + c, lo := addDigits(lo, carry) + _, carry = addDigits(hi, c) + + result = append(result, lo) + } + + if carry != '0' { + result = append(result, carry) + } + + return result +} + +func reverse(s string) []byte { + digits := make([]byte, len(s)) + for i := range s { + digits[len(s)-1-i] = s[i] + } + + return digits +} + +func multiply(a, b string) string { + if a == "0" || b == "0" { + return "0" + } + + and := reverse(a) + result := make([]byte, len(a)+len(b)) + for i := range result { + result[i] = '0' + } + + for i := range len(b) { + ier := b[len(b)-1-i] + partial := multiplyByDigit(and, ier) + + // Shift left by i places (little-endian: i leading zeros). + shifted := make([]byte, i+len(partial)) + for j := range i { + shifted[j] = '0' + } + copy(shifted[i:], partial) + + add(&result, shifted) + } + + end := len(result) + for end > 1 && result[end-1] == '0' { + end-- + } + + out := make([]byte, end) + for i := range end { + out[i] = result[end-1-i] + } + + return string(out) +} diff --git a/pkg/questions/n_queens/main.go b/pkg/questions/n_queens/main.go new file mode 100644 index 0000000..625fefa --- /dev/null +++ b/pkg/questions/n_queens/main.go @@ -0,0 +1,167 @@ +package n_queens + +import ( + "bytes" +) + +type board [][]bool + +func newBoard(size int) board { + b := make([][]bool, size) + for i := range b { + b[i] = make([]bool, size) + } + + return board(b) +} + +func (b board) size() (int, int) { + return len(b[0]), len(b) +} + +func (b board) get(x, y int) bool { + return b[y][x] +} + +func (b board) has(x, y int) bool { + width, height := b.size() + return x >= 0 && y >= 0 && x < width && y < height +} + +func (b board) set(x, y int, value bool) { + b[y][x] = value +} + +type position struct{ x, y int } + +type step struct { + position int + blocked []position +} + +type state struct { + board board + steps []step + n int +} + +func newState(n int) *state { + return &state{ + board: newBoard(n), + n: n, + steps: make([]step, 0, n), + } +} + +func (s *state) options() []int { + y := len(s.steps) + if y >= s.n { + return []int{} + } + + results := []int{} + for x := range s.n { + if s.board.get(x, y) { + continue + } + + results = append(results, x) + } + + return results +} + +func (s *state) forward(x int) { + blocked := []position{} + y := len(s.steps) + + // Vertical + for i := range s.n { + if !s.board.get(x, i) { + s.board.set(x, i, true) + blocked = append(blocked, position{x, i}) + } + } + + // Horizontal + for i := range s.n { + if !s.board.get(i, y) { + s.board.set(i, y, true) + blocked = append(blocked, position{i, y}) + } + } + + // Forward vertical + for offset := range s.n { + p := position{offset, y + x - offset} + + if s.board.has(p.x, p.y) && !s.board.get(p.x, p.y) { + s.board.set(p.x, p.y, true) + blocked = append(blocked, p) + } + } + + // Backward vertical + for offset := range s.n { + p := position{offset, y - x + offset} + + if s.board.has(p.x, p.y) && !s.board.get(p.x, p.y) { + s.board.set(p.x, p.y, true) + blocked = append(blocked, p) + } + } + + s.steps = append(s.steps, step{ + position: x, + blocked: blocked, + }) +} + +func (s *state) backward() { + top, rest := s.steps[len(s.steps)-1], s.steps[:len(s.steps)-1] + s.steps = rest + + for _, p := range top.blocked { + s.board.set(p.x, p.y, false) + } +} + +func (s *state) solution() ([]string, bool) { + if len(s.steps) != s.n { + return nil, false + } + + blank := bytes.Repeat([]byte{'.'}, s.n) + + solution := make([]string, s.n) + for i := range solution { + row := bytes.Clone(blank) + row[s.steps[i].position] = 'Q' + solution[i] = string(row) + } + + return solution, true +} + +func SolveNQueens(n int) [][]string { + s := newState(n) + solutions := [][]string{} + + var dfs func() + dfs = func() { + if soln, ok := s.solution(); ok { + solutions = append(solutions, soln) + return + } + + for _, opt := range s.options() { + s.forward(opt) + dfs() + s.backward() + } + } + + dfs() + + return solutions +} diff --git a/pkg/questions/word_ladder/main.go b/pkg/questions/word_ladder/main.go new file mode 100644 index 0000000..2f0468b --- /dev/null +++ b/pkg/questions/word_ladder/main.go @@ -0,0 +1,60 @@ +package wordladder + +import "iter" + +type wordSet map[string][]string + +func (w wordSet) add(word string) { + buffer := []byte(word) + for i := range word { + buffer[i] = '*' + w[string(buffer)] = append(w[string(buffer)], word) + buffer[i] = word[i] + } +} + +func (w wordSet) neighborsOf(word string) iter.Seq[string] { + return func(yield func(string) bool) { + buffer := []byte(word) + for i := range word { + buffer[i] = '*' + for _, neighbor := range w[string(buffer)] { + yield(neighbor) + } + buffer[i] = word[i] + } + } +} + +func ladderLength(beginWord string, endWord string, wordList []string) int { + ws := wordSet{} + for _, word := range wordList { + ws.add(word) + } + + idx, iteration, visited := 0, []string{beginWord}, map[string]bool{} + visited[beginWord] = true + + for { + nextIteration := []string{} + for _, it := range iteration { + if it == endWord { + return idx + } + + for neighbor := range ws.neighborsOf(it) { + if visited[neighbor] { + continue + } + + visited[neighbor] = true + nextIteration = append(nextIteration, neighbor) + } + } + + iteration = nextIteration + idx++ + } + + return 0 +} diff --git a/pkg/tools/heap/main.go b/pkg/tools/heap/main.go index ba1cf96..b621979 100644 --- a/pkg/tools/heap/main.go +++ b/pkg/tools/heap/main.go @@ -3,10 +3,12 @@ package heap import ( "cmp" "container/heap" + "slices" ) -type Heap[T any] struct { +type Heap[T comparable] struct { Data []T + Index map[T]map[int]bool lessFunc func(a, b T) bool } @@ -14,22 +16,98 @@ func More[T cmp.Ordered](x, y T) bool { return !cmp.Less(x, y) && x != y } -func NewBy[T any](lessFunc func(a, b T) bool, items ...T) *Heap[T] { - h := &Heap[T]{items, lessFunc} +func NewBy[T comparable](lessFunc func(a, b T) bool, items ...T) *Heap[T] { + h := &Heap[T]{slices.Clone(items), map[T]map[int]bool{}, lessFunc} heap.Init(h) + + for i, v := range h.Data { + if h.Index[v] == nil { + h.Index[v] = map[int]bool{} + } + + h.Index[v][i] = true + } + return h } func New[T cmp.Ordered](items ...T) *Heap[T] { - return NewBy[T](func(a, b T) bool { return a < b }, items...) + return NewBy(func(a, b T) bool { return a < b }, items...) +} + +func (h *Heap[T]) putIndex(item T, index int) { + if h.Index == nil { + h.Index = map[T]map[int]bool{} + } + + if h.Index[item] == nil { + h.Index[item] = map[int]bool{} + } + + h.Index[item][index] = true +} + +func (h *Heap[T]) dropIndex(item T, index int) { + if h.Index == nil { + h.Index = map[T]map[int]bool{} + } + + if h.Index[item] == nil { + h.Index[item] = map[int]bool{} + } + + delete(h.Index[item], index) } func (h Heap[T]) Len() int { return len(h.Data) } func (h Heap[T]) Less(i, j int) bool { return h.lessFunc(h.Data[i], h.Data[j]) } -func (h Heap[T]) Swap(i, j int) { h.Data[i], h.Data[j] = h.Data[j], h.Data[i] } +func (h Heap[T]) Swap(i, j int) { + h.dropIndex(h.Data[i], i) + h.dropIndex(h.Data[j], j) -func (h *Heap[T]) Push(x any) { h.Data = append(h.Data, x.(T)) } -func (h *Heap[T]) Pop() any { - defer func() { h.Data = h.Data[:len(h.Data)-1] }() - return h.Data[len(h.Data)-1] + h.Data[i], h.Data[j] = h.Data[j], h.Data[i] + + h.putIndex(h.Data[i], i) + h.putIndex(h.Data[j], j) +} + +func (h *Heap[T]) Push(x any) { + datum := x.(T) + h.Data = append(h.Data, datum) + h.putIndex(datum, len(h.Data)-1) +} + +func (h *Heap[T]) Pop() any { + index := len(h.Data) - 1 + datum := h.Data[index] + h.dropIndex(datum, index) + + h.Data = h.Data[:index] + return datum +} + +func (h *Heap[T]) GetIndex(item T) int { + if h.Index[item] == nil { + return -1 + } + + var key int + for k := range h.Index[item] { + key = k + break + } + + return key +} + +func (h *Heap[T]) Drop(item T) { + heap.Remove(h, h.GetIndex(item)) +} + +func (h *Heap[T]) Put(item T) { + heap.Push(h, item) +} + +func (h *Heap[T]) Top() T { + return h.Data[0] }