diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ea8641..9c6b96c 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.23.11' ] + go: [ '1.24.0' ] steps: - uses: actions/checkout@v3 diff --git a/.golangci.yml b/.golangci.yml index 09bb23f..60f1183 100755 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,7 @@ version: "2" run: - go: "1.23" + go: "1.24" timeout: 5m tests: false issues-exit-code: 1 diff --git a/LICENSE b/LICENSE index cc163da..9fbfd54 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2019-2025, Mikhail Knyazhev +Copyright (c) 2019-2026, Mikhail Knyazhev Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/control/semaphore.go b/control/semaphore.go index a4fe145..ea46b93 100644 --- a/control/semaphore.go +++ b/control/semaphore.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/encoding/base62/base62.go b/encoding/base62/base62.go index 08051ac..9c2b2e8 100644 --- a/encoding/base62/base62.go +++ b/encoding/base62/base62.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/encoding/base62/base62_test.go b/encoding/base62/base62_test.go index 0e4dfaa..2a781ed 100644 --- a/encoding/base62/base62_test.go +++ b/encoding/base62/base62_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/encoding/otp/options.go b/encoding/otp/options.go index bbd7529..9ead544 100644 --- a/encoding/otp/options.go +++ b/encoding/otp/options.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/encoding/otp/totp.go b/encoding/otp/totp.go index c7d99eb..5cc64aa 100644 --- a/encoding/otp/totp.go +++ b/encoding/otp/totp.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/encoding/otp/totp_test.go b/encoding/otp/totp_test.go index 66a00bd..2b32855 100644 --- a/encoding/otp/totp_test.go +++ b/encoding/otp/totp_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/go.mod b/go.mod index 5e7ac0e..d0f5182 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module go.osspkg.com/algorithms -go 1.23.11 +go 1.24.0 require ( github.com/cespare/xxhash/v2 v2.3.0 diff --git a/graph/kahn/type.go b/graph/kahn/type.go index a65dc19..87d7674 100644 --- a/graph/kahn/type.go +++ b/graph/kahn/type.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -9,118 +9,137 @@ package kahn import ( "errors" - "fmt" "sort" ) var ( - ErrBuildKahn = errors.New("can't do topographical sorting") - ErrBreakPointKahn = errors.New("don`t found topographical break point") + ErrBuild = errors.New("can't do topographical sorting") + ErrBreakPoint = errors.New("don`t found topographical break point") ) -const empty = "" - type Graph struct { - graph map[string]map[string]int - all map[string]struct{} - result []string + from map[string][]string + to map[string][]string + + nodes map[string]struct{} + breakPoint string + result []string } func New() *Graph { return &Graph{ - graph: make(map[string]map[string]int), - all: make(map[string]struct{}), + from: make(map[string][]string), + to: make(map[string][]string), + nodes: make(map[string]struct{}), result: make([]string, 0), } } -// Add - Adding a graph edge -func (k *Graph) Add(from, to string) { - if _, ok := k.graph[from]; !ok { - k.graph[from] = make(map[string]int) - } - k.graph[from][to]++ +func (g *Graph) Add(from, to string) { + g.from[from] = append(g.from[from], to) + g.to[to] = append(g.to[to], from) + g.nodes[from] = struct{}{} + g.nodes[to] = struct{}{} } -func (k *Graph) BreakPoint(point string) { - k.breakPoint = point +func (g *Graph) BreakPoint(point string) { + g.breakPoint = point } -// To update the temporary map -func (k *Graph) updateTemp() (int, []string) { - for i, sub := range k.graph { - for j := range sub { - k.all[j] = struct{}{} +func (g *Graph) Build() error { + g.result = g.result[:0] + + var active map[string]struct{} + + if len(g.breakPoint) == 0 { + active = g.copyAllNodes() + } else { + active = g.copyNodesByBreakPoint() + if len(active) == 0 { + return ErrBreakPoint } - k.all[i] = struct{}{} - } - temp := make([]string, 0, len(k.all)) - for s := range k.all { - temp = append(temp, s) } - sort.Strings(temp) - return len(k.all), temp -} - -// Build - Perform sorting -func (k *Graph) Build() error { - k.result = k.result[:0] - length, temp := k.updateTemp() - if len(k.breakPoint) > 0 { - j := -1 - for i, name := range temp { - if k.breakPoint == name { - j = i + inDegree := make(map[string]int) + for u := range active { + for _, v := range g.from[u] { + if _, ok := active[v]; ok { + inDegree[v]++ } } - if j < 0 { - return fmt.Errorf("%w: %s", ErrBreakPointKahn, k.breakPoint) + } + + queue := make([]string, 0, len(active)) + + for _, key := range getKeys(active) { + if inDegree[key] == 0 { + queue = append(queue, key) } - temp[0], temp[j] = temp[j], temp[0] } - for len(k.result) < length { - found := "" - i := 0 - for j, item := range temp { - if item == empty { - continue - } - if k.find(item) { - found = item - i = j - break + for len(queue) > 0 { + key := queue[0] + queue = queue[1:] + + g.result = append(g.result, key) + + for _, v := range g.from[key] { + if _, ok := active[v]; ok { + inDegree[v]-- + if inDegree[v] == 0 { + queue = append(queue, v) + } } } - if len(found) > 0 { - k.result = append(k.result, found) - delete(k.all, found) - temp[i] = empty - } else { - return ErrBuildKahn - } - if len(k.breakPoint) > 0 && found == k.breakPoint { - break - } } + + if len(g.result) != len(active) { + return ErrBuild + } + return nil } -// Finding the next edge -func (k *Graph) find(item string) bool { - for i, j := range k.graph { - if _, jok := j[item]; jok { - if _, iok := k.all[i]; iok { - return false - } +func (g *Graph) Result() []string { + return append(make([]string, 0, len(g.result)), g.result...) +} + +func (g *Graph) copyAllNodes() map[string]struct{} { + tmp := make(map[string]struct{}, len(g.nodes)) + for k := range g.nodes { + tmp[k] = struct{}{} + } + return tmp +} + +func (g *Graph) copyNodesByBreakPoint() map[string]struct{} { + if _, ok := g.nodes[g.breakPoint]; !ok { + return nil + } + + queue := make([]string, 0, len(g.nodes)) + tmp := make(map[string]struct{}, len(g.nodes)) + + queue = append(queue, g.breakPoint) + for len(queue) > 0 { + key := queue[0] + queue = queue[1:] + + if _, ok := tmp[key]; ok { + continue } + tmp[key] = struct{}{} + queue = append(queue, g.to[key]...) } - return true + return tmp } -// Result - Getting a sorted slice -func (k *Graph) Result() []string { - return append(make([]string, 0, len(k.result)), k.result...) +func getKeys(in map[string]struct{}) []string { + result := make([]string, 0, len(in)) + for k := range in { + result = append(result, k) + } + sort.Strings(result) + return result } diff --git a/graph/kahn/type_test.go b/graph/kahn/type_test.go index 26b513e..5f20dab 100644 --- a/graph/kahn/type_test.go +++ b/graph/kahn/type_test.go @@ -1,72 +1,23 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package kahn +package kahn_test import ( - "strings" + "errors" + "reflect" "testing" - "go.osspkg.com/casecheck" + "go.osspkg.com/algorithms/graph/kahn" ) -func TestUnit_KahnCoherent(t *testing.T) { - graph := New() - graph.Add("a", "b") - graph.Add("a", "c") - graph.Add("a", "d") - graph.Add("a", "e") - graph.Add("b", "d") - graph.Add("c", "d") - graph.Add("c", "e") - graph.Add("d", "e") - casecheck.NoError(t, graph.Build()) - result := graph.Result() - casecheck.True(t, len(result) == 5) - casecheck.Equal(t, "a,b,c,d,e", strings.Join(result, ",")) -} - -func TestUnit_KahnCoherentBreakPoint(t *testing.T) { - graph := New() - graph.Add("a", "b") - graph.Add("a", "c") - graph.Add("a", "d") - graph.Add("a", "e") - graph.Add("b", "d") - graph.Add("c", "d") - graph.Add("c", "e") - graph.Add("d", "e") - graph.BreakPoint("d") - casecheck.NoError(t, graph.Build()) - result := graph.Result() - casecheck.True(t, len(result) == 4) - casecheck.Contains(t, "a,b,c,d", strings.Join(result, ",")) -} - -func TestUnit_KahnCoherentBreakPoint2(t *testing.T) { - graph := New() - graph.Add("a", "b") - graph.Add("a", "c") - graph.Add("a", "d") - graph.BreakPoint("w") - casecheck.Error(t, graph.Build()) -} - -func TestUnit_KahnCyclical(t *testing.T) { - graph := New() - graph.Add("1", "2") - graph.Add("2", "3") - graph.Add("3", "2") - casecheck.Error(t, graph.Build()) -} - func Benchmark_Kahn1(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { func() { - graph := New() + graph := kahn.New() graph.Add("1", "2") graph.Add("1", "3") graph.Add("3", "4") @@ -81,7 +32,7 @@ func Benchmark_Kahn2(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { func() { - graph := New() + graph := kahn.New() graph.Add("1", "2") graph.Add("1", "3") graph.Add("3", "4") @@ -92,3 +43,120 @@ func Benchmark_Kahn2(b *testing.B) { }() } } + +func TestUnit_Graph_Build(t *testing.T) { + tests := []struct { + name string + setup func(g *kahn.Graph) + breakPoint string + want []string + wantErr error + }{ + { + name: "Happy path: simple linear dependencies", + setup: func(g *kahn.Graph) { + g.Add("A", "B") // A -> B + g.Add("B", "C") // B -> C + }, + want: []string{"A", "B", "C"}, + }, + { + name: "Happy path: multiple roots", + setup: func(g *kahn.Graph) { + g.Add("A", "C") + g.Add("B", "C") + }, + // Сортировка по ключам в getKeys гарантирует порядок A, B + want: []string{"A", "B", "C"}, + }, + { + name: "Cycle detection", + setup: func(g *kahn.Graph) { + g.Add("A", "B") + g.Add("B", "C") + g.Add("C", "A") // Cycle + }, + wantErr: kahn.ErrBuild, + }, + { + name: "BreakPoint: subset of graph", + setup: func(g *kahn.Graph) { + g.Add("Base", "Lib") + g.Add("Lib", "App") + g.Add("Other", "Unused") + }, + breakPoint: "App", + want: []string{"Base", "Lib", "App"}, + }, + { + name: "BreakPoint: complex dependencies", + setup: func(g *kahn.Graph) { + g.Add("Common", "Auth") + g.Add("Common", "DB") + g.Add("Auth", "API") + g.Add("DB", "API") + g.Add("Utils", "Extra") // Должно быть проигнорировано + }, + breakPoint: "API", + want: []string{"Common", "Auth", "DB", "API"}, + }, + { + name: "BreakPoint: not found", + setup: func(g *kahn.Graph) { + g.Add("A", "B") + }, + breakPoint: "Z", + wantErr: kahn.ErrBreakPoint, + }, + { + name: "Cycle outside of BreakPoint scope", + setup: func(g *kahn.Graph) { + g.Add("A", "B") // Path to BreakPoint + g.Add("C", "D") // Cycle + g.Add("D", "C") // Cycle + }, + breakPoint: "B", + want: []string{"A", "B"}, // Должно работать, так как цикл не в активных узлах + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := kahn.New() + tt.setup(g) + if tt.breakPoint != "" { + g.BreakPoint(tt.breakPoint) + } + + err := g.Build() + if tt.wantErr != nil { + if !errors.Is(err, tt.wantErr) { + t.Errorf("Build() error = %v, wantErr %v", err, tt.wantErr) + } + return + } + + if err != nil { + t.Fatalf("Build() unexpected error: %v", err) + } + + if !reflect.DeepEqual(g.Result(), tt.want) { + t.Errorf("Result() = %v, want %v", g.Result(), tt.want) + } + }) + } +} + +func TestUnit_Graph_ResultIsCopy(t *testing.T) { + g := kahn.New() + g.Add("A", "B") + _ = g.Build() + + res1 := g.Result() + res1[0] = "MUTATED" + + res2 := g.Result() + if res2[0] == "MUTATED" { + t.Error("Result() returned a pointer to the internal slice, not a copy") + } +} diff --git a/sorts/bubble.go b/sorts/bubble.go index 535919e..33cc61a 100644 --- a/sorts/bubble.go +++ b/sorts/bubble.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/bubble_test.go b/sorts/bubble_test.go index 973aef0..9670e75 100644 --- a/sorts/bubble_test.go +++ b/sorts/bubble_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/cocktail.go b/sorts/cocktail.go index d850295..6df6511 100644 --- a/sorts/cocktail.go +++ b/sorts/cocktail.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/cocktail_test.go b/sorts/cocktail_test.go index 602d830..4bc4a1a 100644 --- a/sorts/cocktail_test.go +++ b/sorts/cocktail_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/common_test.go b/sorts/common_test.go index 1144a95..74a9c7b 100644 --- a/sorts/common_test.go +++ b/sorts/common_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/heapsort.go b/sorts/heapsort.go index e284f68..1883e6b 100644 --- a/sorts/heapsort.go +++ b/sorts/heapsort.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/heapsort_test.go b/sorts/heapsort_test.go index e9ae439..99ed1d9 100644 --- a/sorts/heapsort_test.go +++ b/sorts/heapsort_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/insertion.go b/sorts/insertion.go index 229e545..a88c158 100644 --- a/sorts/insertion.go +++ b/sorts/insertion.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/insertion_test.go b/sorts/insertion_test.go index a06fec6..79a7ff1 100644 --- a/sorts/insertion_test.go +++ b/sorts/insertion_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/merge.go b/sorts/merge.go index 1a8377a..0ea4bba 100644 --- a/sorts/merge.go +++ b/sorts/merge.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/merge_test.go b/sorts/merge_test.go index 1c35a30..69a7577 100644 --- a/sorts/merge_test.go +++ b/sorts/merge_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/reverse.go b/sorts/reverse.go index 59752da..12036a6 100644 --- a/sorts/reverse.go +++ b/sorts/reverse.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/reverse_test.go b/sorts/reverse_test.go index cfd209b..51f322a 100644 --- a/sorts/reverse_test.go +++ b/sorts/reverse_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/selection.go b/sorts/selection.go index b8f6858..a692249 100644 --- a/sorts/selection.go +++ b/sorts/selection.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/sorts/selection_test.go b/sorts/selection_test.go index 63195d6..6cf72dd 100644 --- a/sorts/selection_test.go +++ b/sorts/selection_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/structs/bitmap/bitmap.go b/structs/bitmap/bitmap.go index f9cf262..16a929b 100644 --- a/structs/bitmap/bitmap.go +++ b/structs/bitmap/bitmap.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/structs/bitmap/bitmap_test.go b/structs/bitmap/bitmap_test.go index 0747cbe..bc9c762 100644 --- a/structs/bitmap/bitmap_test.go +++ b/structs/bitmap/bitmap_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/structs/bloom/bloom.go b/structs/bloom/bloom.go index 93a4e16..b7788e1 100644 --- a/structs/bloom/bloom.go +++ b/structs/bloom/bloom.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/structs/bloom/bloom_test.go b/structs/bloom/bloom_test.go index 07d9e67..ab10009 100644 --- a/structs/bloom/bloom_test.go +++ b/structs/bloom/bloom_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2019-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */