├── test └── src │ ├── fixtures │ ├── selfLoop.txt │ ├── tinyFN2.txt │ ├── tinyDG2.txt │ ├── sample.txt │ ├── tinyEWDGnw.txt │ ├── tinyDG3.txt │ ├── tinyFN.txt │ ├── tinyG.txt │ ├── tinyFN3.txt │ ├── tinyEWDG.txt │ ├── tinyDG.txt │ ├── tinyEWG.txt │ ├── tinyEWDGnc.txt │ └── fixtures.dart │ ├── quick_select_test.dart │ ├── binary_search_test.dart │ ├── string_search_test.dart │ ├── union_find_test.dart │ ├── utils.dart │ ├── priority_queue_test.dart │ ├── sorting_test.dart │ └── graph_test.dart ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── ci.md │ ├── build.md │ ├── chore.md │ ├── documentation.md │ ├── style.md │ ├── test.md │ ├── refactor.md │ ├── performance.md │ ├── revert.md │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yaml ├── workflows │ ├── main.yaml │ ├── dart_package.yml │ └── dart_package.yaml └── PULL_REQUEST_TEMPLATE.md ├── .idea ├── dictionaries ├── vcs.xml ├── misc.xml ├── .gitignore ├── modules.xml ├── dart_algorithms.iml └── libraries │ ├── Dart_SDK.xml │ └── Dart_Packages.xml ├── .gitignore ├── lib ├── src │ ├── sorting │ │ ├── sorting.dart │ │ ├── selection_sort.dart │ │ ├── insertion_sort.dart │ │ ├── bubble_sort.dart │ │ ├── shell_sort.dart │ │ ├── heap_sort.dart │ │ ├── merge_sort.dart │ │ └── quick_sort.dart │ ├── graph │ │ ├── graphs.dart │ │ ├── dfs.dart │ │ ├── bfs.dart │ │ ├── topological_sort.dart │ │ ├── cycle_detection.dart │ │ ├── minimum_spanning_tree.dart │ │ ├── connected_components.dart │ │ ├── shortest_path.dart │ │ ├── max_flow.dart │ │ └── graph.dart │ ├── utils.dart │ ├── binary_search.dart │ ├── quick_select.dart │ ├── kmp.dart │ ├── union_find.dart │ └── priority_queue.dart └── dart_algorithms.dart ├── analysis_options.yaml ├── pubspec.yaml ├── challanges.txt ├── coverage_badge.svg └── README.md /test/src/fixtures/selfLoop.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 1 3 | 0 0 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /test/src/fixtures/tinyFN2.txt: -------------------------------------------------------------------------------- 1 | 4 2 | 5 3 | 0 1 100 4 | 0 2 100 5 | 1 2 1 6 | 1 3 100 7 | 2 3 100 -------------------------------------------------------------------------------- /test/src/fixtures/tinyDG2.txt: -------------------------------------------------------------------------------- 1 | 6 2 | 8 3 | 5 0 4 | 2 4 5 | 3 2 6 | 1 2 7 | 0 1 8 | 4 3 9 | 3 5 10 | 0 2 -------------------------------------------------------------------------------- /test/src/fixtures/sample.txt: -------------------------------------------------------------------------------- 1 | 10 2 | 9 3 | 0 1 4 | 0 4 5 | 0 8 6 | 1 2 7 | 2 3 8 | 4 5 9 | 5 6 10 | 4 7 11 | 0 8 12 | 8 9 -------------------------------------------------------------------------------- /test/src/fixtures/tinyEWDGnw.txt: -------------------------------------------------------------------------------- 1 | 5 2 | 8 3 | 0 1 -1 4 | 0 2 4 5 | 1 2 3 6 | 1 3 2 7 | 1 4 2 8 | 3 1 1 9 | 3 2 5 10 | 4 3 -3 -------------------------------------------------------------------------------- /test/src/fixtures/tinyDG3.txt: -------------------------------------------------------------------------------- 1 | 7 2 | 11 3 | 0 1 4 | 1 4 5 | 0 5 6 | 0 2 7 | 5 2 8 | 3 2 9 | 3 5 10 | 3 4 11 | 3 6 12 | 6 0 13 | 6 4 -------------------------------------------------------------------------------- /test/src/fixtures/tinyFN.txt: -------------------------------------------------------------------------------- 1 | 6 2 | 8 3 | 0 1 2.0 4 | 0 2 3.0 5 | 1 3 3.0 6 | 1 4 1.0 7 | 2 3 1.0 8 | 2 4 1.0 9 | 3 5 2.0 10 | 4 5 3.0 -------------------------------------------------------------------------------- /test/src/fixtures/tinyG.txt: -------------------------------------------------------------------------------- 1 | 13 2 | 13 3 | 0 5 4 | 4 3 5 | 0 1 6 | 9 12 7 | 6 4 8 | 5 4 9 | 0 2 10 | 11 12 11 | 9 10 12 | 0 6 13 | 7 8 14 | 9 11 15 | 5 3 -------------------------------------------------------------------------------- /test/src/fixtures/tinyFN3.txt: -------------------------------------------------------------------------------- 1 | 8 2 | 13 3 | 0 1 1 4 | 0 2 4 5 | 0 3 6 6 | 1 4 3 7 | 2 1 8 8 | 2 4 1 9 | 2 5 3 10 | 3 5 7 11 | 3 6 4 12 | 4 7 5 13 | 5 6 2 14 | 6 4 10 15 | 6 7 6 -------------------------------------------------------------------------------- /.idea/dictionaries: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | build/ 6 | .idea/* 7 | coverage/* 8 | .packages 9 | pubspec.lock -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /lib/src/sorting/sorting.dart: -------------------------------------------------------------------------------- 1 | export "bubble_sort.dart"; 2 | export "heap_sort.dart"; 3 | export "insertion_sort.dart"; 4 | export "merge_sort.dart"; 5 | export "quick_sort.dart"; 6 | export "selection_sort.dart"; 7 | export "shell_sort.dart"; 8 | -------------------------------------------------------------------------------- /test/src/fixtures/tinyEWDG.txt: -------------------------------------------------------------------------------- 1 | 8 2 | 15 3 | 4 5 0.35 4 | 5 4 0.35 5 | 4 7 0.37 6 | 5 7 0.28 7 | 7 5 0.28 8 | 5 1 0.32 9 | 0 4 0.38 10 | 0 2 0.26 11 | 7 3 0.39 12 | 1 3 0.29 13 | 2 7 0.34 14 | 6 2 0.40 15 | 3 6 0.52 16 | 6 0 0.58 17 | 6 4 0.93 -------------------------------------------------------------------------------- /test/src/fixtures/tinyDG.txt: -------------------------------------------------------------------------------- 1 | 13 2 | 22 3 | 4 2 4 | 2 3 5 | 3 2 6 | 6 0 7 | 0 1 8 | 2 0 9 | 11 12 10 | 12 9 11 | 9 10 12 | 9 11 13 | 7 9 14 | 10 12 15 | 11 4 16 | 4 3 17 | 3 5 18 | 6 8 19 | 8 6 20 | 5 4 21 | 0 5 22 | 6 4 23 | 6 9 24 | 7 6 -------------------------------------------------------------------------------- /test/src/fixtures/tinyEWG.txt: -------------------------------------------------------------------------------- 1 | 8 2 | 16 3 | 4 5 0.35 4 | 4 7 0.37 5 | 5 7 0.28 6 | 0 7 0.16 7 | 1 5 0.32 8 | 0 4 0.38 9 | 2 3 0.17 10 | 1 7 0.19 11 | 0 2 0.26 12 | 1 2 0.36 13 | 1 3 0.29 14 | 2 7 0.34 15 | 6 2 0.40 16 | 3 6 0.52 17 | 6 0 0.58 18 | 6 4 0.93 -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.3.1.0.yaml 2 | 3 | linter: 4 | rules: 5 | prefer_double_quotes: true 6 | prefer_single_quotes: false 7 | sort_constructors_first: false 8 | lines_longer_than_80_chars: false -------------------------------------------------------------------------------- /test/src/fixtures/tinyEWDGnc.txt: -------------------------------------------------------------------------------- 1 | 8 2 | 15 3 | 4 5 0.35 4 | 5 4 -0.66 5 | 4 7 0.37 6 | 5 7 0.28 7 | 7 5 0.28 8 | 5 1 0.32 9 | 0 4 0.38 10 | 0 2 0.26 11 | 7 3 0.39 12 | 1 3 0.29 13 | 2 7 0.34 14 | 6 2 0.40 15 | 3 6 0.52 16 | 6 0 0.58 17 | 6 4 0.93 -------------------------------------------------------------------------------- /lib/dart_algorithms.dart: -------------------------------------------------------------------------------- 1 | library dart_algorithms; 2 | 3 | export "src/binary_search.dart"; 4 | export "src/graph/graphs.dart"; 5 | export "src/kmp.dart"; 6 | export "src/priority_queue.dart"; 7 | export "src/sorting/sorting.dart"; 8 | export "src/union_find.dart"; 9 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "pub" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /lib/src/graph/graphs.dart: -------------------------------------------------------------------------------- 1 | export "bfs.dart"; 2 | export "connected_components.dart"; 3 | export "cycle_detection.dart"; 4 | export "dfs.dart"; 5 | export "graph.dart"; 6 | export "max_flow.dart"; 7 | export "minimum_spanning_tree.dart"; 8 | export "shortest_path.dart"; 9 | export "topological_sort.dart"; 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/src/fixtures/fixtures.dart: -------------------------------------------------------------------------------- 1 | import "dart:io"; 2 | 3 | import "package:path/path.dart" as p; 4 | 5 | const _fixturesDir = "./test/src/fixtures"; 6 | 7 | String _filePath(String fileName) => p.join(_fixturesDir, fileName); 8 | 9 | List loadTestFile(String fileName) => File(_filePath(fileName)).readAsLinesSync(); 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Continuous Integration 3 | about: Changes to the CI configuration files and scripts 4 | title: "ci: " 5 | labels: ci 6 | --- 7 | 8 | **Description** 9 | 10 | Describe what changes need to be done to the ci/cd system and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] The ci system is passing 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/build.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build System 3 | about: Changes that affect the build system or external dependencies 4 | title: "build: " 5 | labels: build 6 | --- 7 | 8 | **Description** 9 | 10 | Describe what changes need to be done to the build system and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] The build system is passing 15 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_algorithms 2 | description: A Very Good Project created by Very Good CLI. 3 | version: 0.1.0+1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: ">=2.18.0 <3.0.0" 8 | dependencies: 9 | collection: ^1.17.1 10 | 11 | dev_dependencies: 12 | mocktail: ^1.0.0 13 | path: ^1.8.3 14 | test: ^1.19.2 15 | very_good_analysis: ^4.0.0+1 16 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Chore 3 | about: Other changes that don't modify src or test files 4 | title: "chore: " 5 | labels: chore 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what change is needed and why. If this changes code then please use another issue type. 11 | 12 | **Requirements** 13 | 14 | - [ ] No functional changes to the code 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Improve the documentation so all collaborators have a common understanding 4 | title: "docs: " 5 | labels: documentation 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what documentation you are looking to add or improve. 11 | 12 | **Requirements** 13 | 14 | - [ ] Requirements go here 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/style.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Style Changes 3 | about: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 4 | title: "style: " 5 | labels: style 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what you are looking to change and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | about: Adding missing tests or correcting existing tests 4 | title: "test: " 5 | labels: test 6 | --- 7 | 8 | **Description** 9 | 10 | List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Refactor 3 | about: A code change that neither fixes a bug nor adds a feature 4 | title: "refactor: " 5 | labels: refactor 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Performance Update 3 | about: A code change that improves performance 4 | title: "perf: " 5 | labels: performance 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/revert.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Revert Commit 3 | about: Reverts a previous commit 4 | title: "revert: " 5 | labels: revert 6 | --- 7 | 8 | **Description** 9 | 10 | Provide a link to a PR/Commit that you are looking to revert and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] Change has been reverted 15 | - [ ] No change in test coverage has happened 16 | - [ ] A new ticket is created for any follow on work that needs to happen 17 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | workflow_dispatch: 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | semantic_pull_request: 15 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 16 | 17 | build: 18 | uses: ./.github/workflows/dart_package.yml -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: A new feature to be added to the project 4 | title: "feat: " 5 | labels: feature 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what you are looking to add. The more context the better. 11 | 12 | **Requirements** 13 | 14 | - [ ] Checklist of requirements to be fulfilled 15 | 16 | **Additional Context** 17 | 18 | Add any other context or screenshots about the feature request go here. 19 | -------------------------------------------------------------------------------- /test/src/quick_select_test.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/dart_algorithms.dart"; 2 | import "package:test/test.dart"; 3 | 4 | void main() { 5 | setUp(() {}); 6 | group("Check ", () { 7 | test("", () { 8 | expect(quickSelect([3, 2, 1, 5, 6, 4], 2), 5); 9 | expect(quickSelect([3, 2, 3, 1, 2, 4, 5, 5, 6], 4), 4); 10 | expect( 11 | quickSelect( 12 | [3, 2, 1, 5, 6, 4, 0], 13 | compare: (a, b) => b.compareTo(a), 14 | 2, 15 | ), 16 | 1, 17 | ); 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/src/binary_search_test.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/binary_search.dart"; 2 | import "package:test/test.dart"; 3 | 4 | void main() { 5 | test("Binary Search", () { 6 | final arr = [1, 2, 3, 4, 5, 5, 5, 99, 101]; 7 | 8 | expect(binarySearch(arr, 5), 4); 9 | expect(binarySearch(arr, 99), 7); 10 | expect(binarySearch(arr, 101), 8); 11 | expect(binarySearch(arr, 1), 0); 12 | expect(binarySearch(arr, 0), -1); 13 | expect(binarySearch([], 42), -1); 14 | 15 | expect( 16 | () => binarySearch([1, 3, 9, 0], 42), 17 | throwsA(isA()), 18 | ); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: "fix: " 5 | labels: bug 6 | --- 7 | 8 | **Description** 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | **Steps To Reproduce** 13 | 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected Behavior** 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Additional Context** 28 | 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | int defaultCompare(Object? value1, Object? value2) => (value1 as Comparable).compareTo(value2); 2 | 3 | extension IterableSwapX on List { 4 | /// Swaps two items in a list given its indices 5 | void swap(int indexA, int indexB) { 6 | if (isEmpty || indexA > length || indexB > length || indexA == indexB) { 7 | return; 8 | } 9 | 10 | final temp = this[indexA]; 11 | this[indexA] = this[indexB]; 12 | this[indexB] = temp; 13 | } 14 | } 15 | 16 | /// Greatest common divisor 17 | int gcd(int a, int b) => b == 0 ? a : gcd(b, a % b); 18 | 19 | /// Least common multiple 20 | int lcm(int a, int b) => a * b ~/ gcd(a, b); 21 | -------------------------------------------------------------------------------- /.idea/dart_algorithms.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Status 10 | 11 | **READY/IN DEVELOPMENT/HOLD** 12 | 13 | ## Description 14 | 15 | 16 | 17 | ## Type of Change 18 | 19 | 20 | 21 | - [ ] ✨ New feature (non-breaking change which adds functionality) 22 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 23 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 24 | - [ ] 🧹 Code refactor 25 | - [ ] ✅ Build configuration change 26 | - [ ] 📝 Documentation 27 | - [ ] 🗑️ Chore 28 | -------------------------------------------------------------------------------- /challanges.txt: -------------------------------------------------------------------------------- 1 | 2 | Flood fill 3 | 4 | Is a graph bipartite 5 | Eulerian tour 6 | k clustering using kruskal's algorithm 7 | ++ hard 8 | Tarjan algorithm (lay out graph without crossing edges) 9 | Hamiltonian cycle 10 | ++ Fucking hard 11 | Isomorphism 12 | 13 | 14 | Path. Is there a path between s and t ? 15 | Shortest path. What is the shortest path between s and t ? 16 | Euler tour. Is there a cycle that uses each edge exactly once? 17 | Hamilton tour. Is there a cycle that uses each vertex exactly once. 18 | Connectivity. Is there a way to connect all the vertices? 19 | MST. What is the best way to connect all the vertices? 20 | Bi-connectivity. Is there a vertex whose removal disconnects the graph? 21 | Planarity. Can you draw the graph in the plane with no crossing edges 22 | Graph isomorphism: Do two adjacency lists represent the same graph? -------------------------------------------------------------------------------- /lib/src/graph/dfs.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/graph/graph.dart"; 2 | 3 | /// Graph traversal algorithm that explores the longest path from [start] possible. 4 | /// before going back and visit other unvisited nodes. 5 | /// It is useful for: 6 | /// Discovering connected components. 7 | /// Topological sort 8 | /// Path between edges 9 | 10 | Set dfs(Graph graph, T start, {Map? visited, void Function(T node)? onVisited}) { 11 | final visited = Map.fromIterable(graph.vertices, value: (_) => false); 12 | final output = {}; 13 | void _dfs(T vertex) { 14 | visited[vertex] = true; 15 | onVisited?.call(vertex); 16 | output.add(vertex); 17 | for (final neighbour in graph.neighbours(vertex)) { 18 | if (!visited[neighbour]!) { 19 | _dfs(neighbour); 20 | } 21 | } 22 | } 23 | 24 | _dfs(start); 25 | return output; 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/graph/bfs.dart: -------------------------------------------------------------------------------- 1 | import "dart:collection"; 2 | 3 | import "package:dart_algorithms/src/graph/graph.dart"; 4 | 5 | /// Graph traversal algorithm that explores all nodes at the present depth 6 | /// before to the next level of depth. 7 | /// It is useful for: 8 | /// Discovering connected components. 9 | /// Shortest path on unweighted graphs. 10 | /// Testing if a graph is bipartite. 11 | //https://upload.wikimedia.org/wikipedia/commons/4/46/Animated_BFS.gif 12 | 13 | Set bfs(Graph graph, T start) { 14 | final q = Queue(); 15 | final visited = Map.fromIterable(graph.vertices, value: (_) => false); 16 | final output = {}; 17 | 18 | q.add(start); 19 | 20 | while (q.isNotEmpty) { 21 | final node = q.removeFirst(); 22 | visited[node] = true; 23 | output.add(node); 24 | q.addAll(graph.neighbours(node).where((element) => !(visited[element] ?? true))); 25 | } 26 | return output; 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/sorting/selection_sort.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/utils.dart"; 2 | 3 | /// Sorts a list of [elements] using the selection Sort algorithm 4 | /// and if specified a custom [compare] function. 5 | /// 6 | /// It's a unstable sorting algorithm with a worst and best 7 | /// time complexity of O(n²). The space complexity is O(1). 8 | /// 9 | /// It performs (N²/2) compares and N exchanges to sort an array of length N. 10 | void selectionSort( 11 | List elements, { 12 | int Function(E, E)? compare, 13 | }) { 14 | compare ??= defaultCompare; 15 | 16 | for (var i = 0; i < elements.length; i++) { 17 | var smallestIndex = i; 18 | 19 | for (var j = i + 1; j < elements.length; j++) { 20 | final smallest = elements[smallestIndex]; 21 | final current = elements[j]; 22 | if (compare(current, smallest) < 0) { 23 | smallestIndex = j; 24 | } 25 | } 26 | elements.swap(i, smallestIndex); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/graph/topological_sort.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/graph/cycle_detection.dart"; 2 | import "package:dart_algorithms/src/graph/graph.dart"; 3 | 4 | /// Given a directed acyclic graph (DAG) it returns an ordered sequence of vertices 5 | /// such that every edge dependencies are listed before it. 6 | Iterable topologicalSort(Graph graph, {List? stack}) { 7 | assert(!hasCycle(graph), "Topological sorting can only be executed on acyclic graphs"); 8 | final visited = Map.fromIterable(graph.vertices, value: (_) => false); 9 | stack ??= []; 10 | final output = []; 11 | 12 | void _dfs(T vertex) { 13 | visited[vertex] = true; 14 | for (final neighbour in graph.neighbours(vertex)) { 15 | if (!visited[neighbour]!) { 16 | _dfs(neighbour); 17 | } 18 | } 19 | output.add(vertex); 20 | } 21 | 22 | for (final vertex in graph.vertices) { 23 | if (!visited[vertex]!) _dfs(vertex); 24 | } 25 | 26 | return output.reversed; 27 | } 28 | -------------------------------------------------------------------------------- /test/src/string_search_test.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/kmp.dart"; 2 | import "package:test/test.dart"; 3 | 4 | void main() { 5 | group("String processing", () { 6 | test("Knuth Morris Pratt", () { 7 | for (final entry in solutions) { 8 | expect(knuthMorrisPratt(entry.first, entry.second), entry.third); 9 | } 10 | }); 11 | }); 12 | } 13 | 14 | // We store our test results in tuples, by convention we store the needle, haystack and correct result 15 | const solutions = [ 16 | //needle, haystack, answer(needle starting index) 17 | _Triple("aaaaabc", "aaaaaabcb", 1), 18 | _Triple("aabdcaaabcdb", "aabdcaaabdcaaabcdba", 6), 19 | _Triple("needle", "inahaystackneedleina", 11), 20 | _Triple("longer than the haystack", "banana", -1), 21 | _Triple("shortcircuit", "shortcircuit", 0), 22 | _Triple("", "shortcircuit", 0), 23 | ]; 24 | 25 | class _Triple { 26 | final T first; 27 | final E second; 28 | final S third; 29 | 30 | const _Triple(this.first, this.second, this.third); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/binary_search.dart: -------------------------------------------------------------------------------- 1 | /// Performs a binary search of [item] in [elements]. 2 | /// Returns the index of the [item] or -1 if not found. 3 | /// [elements] is required to be sorted. 4 | /// The complexity is O(log(n)) 5 | 6 | int binarySearch>(Iterable elements, E item) { 7 | assert(_isSortedAscending(elements), "Elements MUST be in sorted order"); 8 | var lo = 0; 9 | var hi = elements.length - 1; 10 | 11 | while (lo <= hi) { 12 | final mid = lo + (hi - lo) ~/ 2; 13 | final compare = item.compareTo(elements.elementAt(mid)); 14 | 15 | if (compare < 0) { 16 | hi = mid - 1; 17 | } else if (compare > 0) { 18 | lo = mid + 1; 19 | } else { 20 | return mid; 21 | } 22 | } 23 | return -1; 24 | } 25 | 26 | bool _isSortedAscending>(Iterable elements) { 27 | for (var i = 1; i < elements.length; i++) { 28 | if (elements.elementAt(i - 1).compareTo(elements.elementAt(i)) > 0) { 29 | return false; 30 | } 31 | } 32 | return true; 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/sorting/insertion_sort.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/utils.dart"; 2 | 3 | /// Sorts a list of [elements] using the insertion Sort algorithm 4 | /// and if specified a custom [compare] function. 5 | /// 6 | /// It's a stable sorting algorithm with a worst time complexity of O(n²) 7 | /// and a best of O(n). The space complexity is O(1) 8 | /// 9 | /// Insertion sort is VERY efficient if the amount of [elements] is small 10 | /// or if the array is sorted or partially sorted. 11 | // Because of the above you might see more advanced sorting algorithms switch 12 | // to insertion sort for small amount of elements or for partially sorted lists 13 | void insertionSort( 14 | List elements, { 15 | int Function(E, E)? compare, 16 | int lo = 0, 17 | int? hi, 18 | }) { 19 | compare ??= defaultCompare; 20 | hi ??= elements.length; 21 | 22 | for (var i = lo; i < hi; i++) { 23 | var j = i; 24 | while (j > 0 && compare(elements[j], elements[j - 1]) < 0) { 25 | elements.swap(j - 1, j); 26 | j--; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/quick_select.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: require_trailing_commas 2 | 3 | part of "sorting/quick_sort.dart"; 4 | 5 | /// Selection Algorithm that finds the [k]th element on an unsorted [arr]. 6 | /// With average o(n) complexity. 7 | // Quick select works like quicksort as it takes advantage of partitioning 8 | // the input thus, reducing a portion of the array after each partitioning. 9 | E? quickSelect( 10 | List arr, 11 | int k, { 12 | int lo = 0, 13 | int? hi, 14 | int Function(E, E)? compare, 15 | Random? random, 16 | }) { 17 | if (k > arr.length) return null; 18 | hi ??= arr.length - 1; 19 | 20 | random ??= Random(); 21 | 22 | final partitionIndex = _partition(arr, lo, hi, compare: compare); 23 | 24 | final target = arr.length - k; 25 | if (partitionIndex == target) { 26 | return arr[target]; 27 | } else if (partitionIndex > target) { 28 | return quickSelect(arr, k, lo: lo, hi: partitionIndex - 1, compare: compare); 29 | } else if (partitionIndex < target) { 30 | return quickSelect(arr, k, lo: partitionIndex + 1, hi: hi, compare: compare); 31 | } 32 | return null; 33 | } 34 | -------------------------------------------------------------------------------- /coverage_badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | coverage 16 | coverage 17 | 100% 18 | 100% 19 | 20 | 21 | -------------------------------------------------------------------------------- /lib/src/sorting/bubble_sort.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/utils.dart"; 2 | 3 | /// Sorts a list of [elements] using the bubble Sort algorithm 4 | /// and if specified a custom [compare] function. 5 | /// 6 | /// It's a stable sorting algorithm with a worst time complexity of O(n²) 7 | /// and a best of O(n). The space complexity is O(1) 8 | void bubbleSort( 9 | List elements, { 10 | int Function(E, E)? compare, 11 | }) { 12 | compare ??= defaultCompare; 13 | 14 | for (var i = 0; i < elements.length; i++) { 15 | // Little optimization, so if the list is sorted before we finishes all 16 | // the comparisons the function returns early 17 | // We assume the list is already sorted unless 18 | // there is a swap in the inner loop 19 | var isListSorted = true; 20 | 21 | for (var j = 0; j < elements.length - i - 1; j++) { 22 | final curr = elements[j]; 23 | final next = elements[j + 1]; 24 | if (compare(next, curr) < 0) { 25 | elements.swap(j, j + 1); 26 | // If there is a swap that means the list is not sorted 27 | isListSorted = false; 28 | } 29 | } 30 | if (isListSorted) break; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/src/union_find_test.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/union_find.dart"; 2 | import "package:test/test.dart"; 3 | 4 | void main() { 5 | final components = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]; 6 | 7 | test("Union find", () { 8 | final uf = UnionFind(components); 9 | expect(uf.components, components.length); 10 | 11 | uf.union("A", "B"); 12 | expect(uf.components, components.length - 1); 13 | expect(uf.find("A"), uf.find("B")); 14 | 15 | uf.union("C", "D"); 16 | expect(uf.components, components.length - 2); 17 | uf.union("E", "F"); 18 | expect(uf.components, components.length - 3); 19 | 20 | uf.union("G", "H"); 21 | expect(uf.components, components.length - 4); 22 | 23 | uf.union("I", "J"); 24 | expect(uf.components, components.length - 5); 25 | 26 | uf.union("J", "G"); 27 | expect(uf.components, components.length - 6); 28 | expect(uf.find("J"), uf.find("H")); 29 | expect(uf.connected("H", "J"), true); 30 | uf.union("H", "F"); 31 | expect(uf.components, components.length - 7); 32 | uf 33 | ..union("A", "C") 34 | ..union("D", "E"); 35 | expect(uf.components, 1); 36 | 37 | uf 38 | ..union("G", "B") 39 | ..union("I", "J"); 40 | expect(uf.components, 1); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/kmp.dart: -------------------------------------------------------------------------------- 1 | /// String matching algorithm which is used to find a pattern in a text with O(n) time complexity 2 | int knuthMorrisPratt(String needle, String haystack) { 3 | final n = haystack.length, m = needle.length; 4 | if (m > n) return -1; 5 | if (m == n) return needle == haystack ? 0 : -1; 6 | if (needle.isEmpty) return 0; 7 | 8 | final lps = _constructLongestProperPrefix(needle); 9 | var i = 0, j = 0; 10 | while (i < n && j < m) { 11 | if (haystack[i] == needle[j]) { 12 | i++; 13 | j++; 14 | } else if (j > 0) { 15 | j = lps[j - 1]; 16 | } else { 17 | i++; 18 | } 19 | } 20 | if (j < m) return -1; 21 | return i - m; 22 | } 23 | 24 | // What: 25 | // Constructs an array of indices that point if a sub pattern of [s] next character does not match 26 | // how far back in the pattern we can continue matching. Eg: 27 | // Given the string "aabdcaaabcdb" the computed pattern is 28 | // [a, a, b, d, c, a, a, a, b, c, d, b] 29 | // [0, 1, 0, 0, 0, 1, 2, 2, 3, 0, 0, 0] 30 | // 31 | List _constructLongestProperPrefix(String s) { 32 | final lps = List.generate(s.length, (_) => 0); 33 | for (var j = 0, i = 1; i < s.length;) { 34 | if (s[i] == s[j]) { 35 | j++; 36 | lps[i] = j; 37 | i++; 38 | } else if (j > 0) { 39 | j = lps[j - 1]; 40 | } else { 41 | lps[i] = 0; 42 | i++; 43 | } 44 | } 45 | return lps; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/graph/cycle_detection.dart: -------------------------------------------------------------------------------- 1 | import "dart:collection"; 2 | 3 | import "package:dart_algorithms/src/graph/graph.dart"; 4 | 5 | /// Returns true if a graph has any cycle 6 | bool hasCycle(Graph graph) => findCycle(graph).isNotEmpty; 7 | 8 | /// Finds a cycle in a digraph if any 9 | /// It returns an empty list if no cycle is found 10 | /// It returns the path to the first cycle found 11 | List findCycle(Graph graph) { 12 | if (!graph.directed) throw Exception("Can't find cycle in undirected graphs"); 13 | final visited = Map.fromIterable(graph.vertices, value: (_) => false); 14 | final onStack = HashSet(); 15 | List? dfs(T vertex) { 16 | onStack.add(vertex); 17 | visited[vertex] = true; 18 | for (final neighbour in graph.neighbours(vertex)) { 19 | if (onStack.contains(neighbour)) { 20 | final cycle = [...onStack, neighbour]; 21 | while (cycle.first != neighbour) { 22 | // Remove vertices in the path that precede the cycle found 23 | cycle.removeAt(0); 24 | } 25 | return cycle; 26 | } 27 | if (!visited[neighbour]!) { 28 | return dfs(neighbour); 29 | } 30 | onStack.remove(neighbour); 31 | } 32 | return null; 33 | } 34 | 35 | for (final vertex in graph.vertices) { 36 | if (!visited[vertex]!) { 37 | final hasCycle = dfs(vertex); 38 | if (hasCycle != null) return hasCycle; 39 | } 40 | // Clear stack when moving to another connected component 41 | onStack.clear(); 42 | } 43 | return []; 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/sorting/shell_sort.dart: -------------------------------------------------------------------------------- 1 | import "dart:math"; 2 | 3 | import "package:dart_algorithms/src/utils.dart"; 4 | 5 | /// Sorts a list of [elements] using the shell Sort algorithm 6 | /// and if specified a custom [compare] function. 7 | /// 8 | /// It's a unstable sorting algorithm with a worst time complexity of O(n*3/2) 9 | /// and a best of O(n*log(n)). The space complexity is O(1) 10 | /// 11 | // Its an improvement over insertion sort on large inputs 12 | // 13 | // Shellsort performs more operations than quicksort. 14 | // However, since it can be implemented using little code 15 | // It can be a good alternative for programmers in embedded environments 16 | // or on a rush. 17 | void shellSort( 18 | List elements, { 19 | int Function(E, E)? compare, 20 | }) { 21 | compare ??= defaultCompare; 22 | 23 | final sequence = _tokudaSequence(elements.length); 24 | 25 | while (sequence.isNotEmpty) { 26 | final h = sequence.removeLast(); 27 | 28 | for (var i = h; i < elements.length; i++) { 29 | for (var j = i; 30 | j >= h && 31 | compare( 32 | elements[j], 33 | elements[j - 1], 34 | ) < 35 | 0; 36 | j -= h) { 37 | elements.swap(j, j - h); 38 | } 39 | } 40 | } 41 | } 42 | 43 | // Given [len] which represents the length of a list, it generates 44 | // the gap sequence according to Tokuda's algorithm 45 | // For more sequences refer to https://en.wikipedia.org/wiki/Shellsort#Gap_sequences 46 | @pragma("vm:prefer-inline") 47 | List _tokudaSequence(int len) { 48 | var h = 1; 49 | final sequence = [h]; 50 | for (var i = 1; h < len; i++) { 51 | h = ((9 * pow(9 / 4, i) - 4) / 5).ceil(); 52 | sequence.add(h); 53 | } 54 | return sequence; 55 | } 56 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/graph/minimum_spanning_tree.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/dart_algorithms.dart"; 2 | import "package:dart_algorithms/src/graph/graph.dart"; 3 | import "package:dart_algorithms/src/union_find.dart"; 4 | 5 | ///{@template mst} 6 | /// Finds a minimum spanning forest of an undirected edge-weighted graph 7 | /// It has a complexity of (E log E) with E being the amount of edges in [graph] 8 | ///{@endtemplate} 9 | List> kruskal(WeightedGraph graph) { 10 | // Sort edges in ascending order of weight 11 | // Iterate edges and add to MST if the edge does not create a cycle 12 | // until MST is V-1 in length or we run out of edges(in which case we will have a minimum spanning forest) 13 | final sortedEdges = graph.allEdges.toList()..sort(); 14 | 15 | final uf = UnionFind(graph.vertices); 16 | final mst = >[]; 17 | while (sortedEdges.isNotEmpty && mst.length < graph.vertices.length - 1) { 18 | final lightestEdge = sortedEdges.removeAt(0); 19 | if (!uf.connected(lightestEdge.a, lightestEdge.b)) { 20 | uf.union(lightestEdge.a, lightestEdge.b); 21 | mst.add(lightestEdge); 22 | } 23 | } 24 | return mst; 25 | } 26 | 27 | /// {@macro mst} 28 | List> prim(WeightedGraph graph) { 29 | final pq = PriorityQueue>(); 30 | final mst = >[]; 31 | final visited = Map.fromIterable(graph.vertices, value: (_) => false); 32 | 33 | void visit(T vertex) { 34 | visited[vertex] = true; 35 | for (final edge in graph.edges(vertex)) { 36 | if (!visited[edge.other(vertex)]!) { 37 | pq.insert(edge); 38 | } 39 | } 40 | } 41 | 42 | visit(graph.vertices.first); 43 | 44 | while (!pq.isEmpty) { 45 | final lightestEdge = pq.remove(); 46 | if (visited[lightestEdge.a]! && visited[lightestEdge.b]!) continue; 47 | 48 | mst.add(lightestEdge); 49 | if (!visited[lightestEdge.a]!) visit(lightestEdge.a); 50 | if (!visited[lightestEdge.b]!) visit(lightestEdge.b); 51 | } 52 | return mst; 53 | } 54 | -------------------------------------------------------------------------------- /test/src/utils.dart: -------------------------------------------------------------------------------- 1 | import "dart:convert"; 2 | 3 | import "dart:io"; 4 | 5 | import "package:test/test.dart"; 6 | 7 | import "fixtures/fixtures.dart"; 8 | 9 | /// Transforms graphs stored at /test/fixtures from "Algorithms 4th edition" format to "graphviz" 10 | String transformGraphFormat(List lines, {required bool directed, String filename = ""}) { 11 | final edgeOp = directed ? " -> " : " -- "; 12 | final type = directed ? "digraph" : "graph"; 13 | 14 | //Remove unneeded lines 15 | lines 16 | ..removeAt(0) // Number of vertices 17 | ..removeAt(0); // Number of edges 18 | 19 | final dotFormat = """ $type $filename { ${lines.map((e) => e.split(RegExp(r"\s+")).join(edgeOp)).join("\n")} }"""; 20 | 21 | return dotFormat; 22 | } 23 | 24 | // Generates a svg of a graph using the "graphviz" package. So its required to have this package installed. 25 | // Given a file name it matches it to the fixtures inside ./test/src/fixtures and generates an svg at ./test/[fileName] 26 | Future plotTestFile(String fileName) async { 27 | try { 28 | final fileNameWithoutExtension = fileName.split(".").first; 29 | 30 | final dotFormat = transformGraphFormat(loadTestFile(fileName), 31 | directed: fileNameWithoutExtension.contains("DG"), filename: fileNameWithoutExtension); 32 | 33 | final process = await Process.start("dot", ["-Tsvg", "-o", "./test/$fileNameWithoutExtension.svg"]); 34 | final resultStdoutFuture = process.stdout.transform(const Utf8Decoder()).transform(const LineSplitter()).toList(); 35 | final resultStderrFuture = process.stderr.transform(const Utf8Decoder()).transform(const LineSplitter()).toList(); 36 | 37 | await Stream.value(const Utf8Codec().encode(dotFormat)).pipe(process.stdin); 38 | 39 | await process.stdin.close(); 40 | 41 | print("Process stopped with exit code: ${await process.exitCode}"); 42 | print("Returned stderr:"); 43 | (await resultStderrFuture).forEach((logLine) => print("\t$logLine")); 44 | print("Returned stdout:"); 45 | (await resultStdoutFuture).forEach((logLine) => print("\t$logLine")); 46 | 47 | return await process.exitCode; 48 | } catch (e, st) { 49 | print("Error converting $fileName to svg $e $st"); 50 | } 51 | return -1; 52 | } 53 | 54 | void main() { 55 | test("", () async { 56 | await plotTestFile("tinyEWDG.txt"); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/src/priority_queue_test.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/priority_queue.dart"; 2 | import "package:test/test.dart"; 3 | 4 | void main() { 5 | setUp(() {}); 6 | group("Check PriorityQueue", () { 7 | test("Min PriorityQueue inserts and removes", () { 8 | final heap = PriorityQueue(); 9 | 10 | expect(heap.size, 0); 11 | 12 | heap 13 | ..insert(5) 14 | ..insert(3) 15 | ..insert(69) 16 | ..insert(420) 17 | ..insert(4) 18 | ..insert(1) 19 | ..insert(8) 20 | ..insert(7); 21 | 22 | expect(heap.size, 8); 23 | expect(heap.remove(), 1); 24 | expect(heap.remove(), 3); 25 | expect(heap.size, 6); 26 | expect(heap.remove(), 4); 27 | expect(heap.remove(), 5); 28 | expect(heap.size, 4); 29 | expect(heap.remove(), 7); 30 | expect(heap.remove(), 8); 31 | expect(heap.remove(), 69); 32 | expect(heap.remove(), 420); 33 | expect(heap.size, 0); 34 | }); 35 | test("Max PriorityQueue inserts and removes", () { 36 | int maxCompare(int a, int b) => b.compareTo(a); 37 | final heap = PriorityQueue(maxCompare); 38 | 39 | expect(heap.size, 0); 40 | 41 | heap 42 | ..insert(5) 43 | ..insert(3) 44 | ..insert(69) 45 | ..insert(420) 46 | ..insert(4) 47 | ..insert(1) 48 | ..insert(8) 49 | ..insert(7); 50 | 51 | expect(heap.size, 8); 52 | expect(heap.remove(), 420); 53 | expect(heap.remove(), 69); 54 | expect(heap.size, 6); 55 | expect(heap.remove(), 8); 56 | expect(heap.remove(), 7); 57 | expect(heap.size, 4); 58 | 59 | expect(heap.remove(), 5); 60 | expect(heap.remove(), 4); 61 | expect(heap.remove(), 3); 62 | expect(heap.remove(), 1); 63 | expect(heap.size, 0); 64 | }); 65 | test("Throws when peeked when empty", () { 66 | final heap = PriorityQueue(); 67 | 68 | expect(heap.size, 0); 69 | 70 | heap 71 | ..insert(69) 72 | ..insert(5) 73 | ..insert(3); 74 | 75 | expect(heap.peek, 3); 76 | heap 77 | ..remove() 78 | ..remove(); 79 | expect(heap.peek, 69); 80 | heap.remove(); 81 | 82 | expect(() => heap.peek, throwsA(isA())); 83 | expect(heap.remove, throwsA(isA())); 84 | }); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /lib/src/union_find.dart: -------------------------------------------------------------------------------- 1 | // Also called Disjoint Set 2 | /// Union Find Data structure 3 | /// Performs union and find in O(α(n)) 4 | class UnionFind { 5 | final Map _components; 6 | int _numberOfComponents = 0; 7 | 8 | /// Creates a UnionFind object given [nodes]. 9 | /// Initially there are as many components as [nodes]. 10 | UnionFind(Iterable nodes) 11 | : _components = Map.fromIterable(nodes), 12 | _numberOfComponents = nodes.length; 13 | 14 | /// Join [first] and [second] in the same component 15 | // to unify two elements, first we find which are the 16 | // root nodes of each component. 17 | // and if the root nodes are different make one of the 18 | // root nodes be the parent of the other 19 | void union(T first, T second) { 20 | final firstRoot = find(first); 21 | final secondRoot = find(second); 22 | 23 | // If both elements belong to the same component we return early 24 | if (firstRoot == secondRoot) return; 25 | 26 | // Merge the smaller group into the larger one 27 | if (componentSize(firstRoot) < componentSize(secondRoot)) { 28 | _components[firstRoot] = secondRoot; 29 | } else { 30 | _components[secondRoot] = firstRoot; 31 | } 32 | _numberOfComponents--; 33 | } 34 | 35 | /// Finds to which component a particular element belongs 36 | // to find the root of that component by following the parent nodes 37 | // until a self loop is reached to node who's parent is itself) 38 | T find(T node) { 39 | var root = node; 40 | final transient = []; 41 | 42 | // While a node is not its own parent, travel upwards 43 | while (root != _components[root]) { 44 | transient.add(root); 45 | root = _components[root] as T; 46 | } 47 | // Now we compress the paths by making each node a direct child 48 | // of the root node. We iterate from 0 to length-1 because the last node 49 | // is already a direct child of the root node 50 | while (transient.isNotEmpty) { 51 | _components[transient.removeAt(0)] = root; 52 | } 53 | 54 | return root; 55 | } 56 | 57 | /// Number of components associated with [node] found in O(n) 58 | int componentSize(T node) { 59 | return _components.entries.where((element) => element.value == node).length; 60 | } 61 | 62 | /// Whether [first] and [second] are connected 63 | bool connected(T first, T second) { 64 | return find(first) == find(second); 65 | } 66 | 67 | /// The number of components is also equal to the number of roots remaining 68 | int get components => _numberOfComponents; 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/dart_package.yml: -------------------------------------------------------------------------------- 1 | name: Dart Package Workflow 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | analyze_directories: 7 | required: false 8 | type: string 9 | default: "lib test" 10 | concurrency: 11 | required: false 12 | type: number 13 | default: 4 14 | coverage_excludes: 15 | required: false 16 | type: string 17 | default: "" 18 | dart_sdk: 19 | required: false 20 | type: string 21 | default: "stable" 22 | min_coverage: 23 | required: false 24 | type: number 25 | default: 80 26 | platform: 27 | required: false 28 | type: string 29 | default: "vm" 30 | report_on: 31 | required: false 32 | type: string 33 | default: "lib" 34 | runs_on: 35 | required: false 36 | type: string 37 | default: "ubuntu-latest" 38 | setup: 39 | required: false 40 | type: string 41 | default: "" 42 | working_directory: 43 | required: false 44 | type: string 45 | default: "." 46 | 47 | jobs: 48 | build: 49 | defaults: 50 | run: 51 | working-directory: ${{inputs.working_directory}} 52 | 53 | runs-on: ${{inputs.runs_on}} 54 | 55 | steps: 56 | - name: 📚 Git Checkout 57 | uses: actions/checkout@v3 58 | 59 | - name: 🎯 Setup Dart 60 | uses: dart-lang/setup-dart@v1 61 | with: 62 | sdk: ${{inputs.dart_sdk}} 63 | 64 | - name: 📦 Install Dependencies 65 | run: dart pub get 66 | 67 | - name: ⚙️ Run Setup 68 | if: "${{inputs.setup != ''}}" 69 | run: ${{inputs.setup}} 70 | 71 | - name: ✨ Check Formatting 72 | run: dart format -l 120 --set-exit-if-changed . 73 | 74 | - name: 🕵️ Analyze 75 | run: dart analyze --fatal-warnings ${{inputs.analyze_directories}} 76 | 77 | - name: 🧪 Run Tests 78 | run: | 79 | dart pub global activate coverage 1.2.0 80 | dart test -j ${{inputs.concurrency}} --coverage=coverage --platform=${{inputs.platform}} && dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on=${{inputs.report_on}} 81 | 82 | - name: 📊 Check Code Coverage 83 | uses: VeryGoodOpenSource/very_good_coverage@v2 84 | with: 85 | path: ${{inputs.working_directory}}/coverage/lcov.info 86 | exclude: ${{inputs.coverage_excludes}} 87 | min_coverage: ${{inputs.min_coverage}} -------------------------------------------------------------------------------- /.github/workflows/dart_package.yaml: -------------------------------------------------------------------------------- 1 | name: Dart Package Workflow 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | analyze_directories: 7 | required: false 8 | type: string 9 | default: "lib test" 10 | concurrency: 11 | required: false 12 | type: number 13 | default: 4 14 | coverage_excludes: 15 | required: false 16 | type: string 17 | default: "" 18 | dart_sdk: 19 | required: false 20 | type: string 21 | default: "stable" 22 | min_coverage: 23 | required: false 24 | type: number 25 | default: 100 26 | platform: 27 | required: false 28 | type: string 29 | default: "vm" 30 | report_on: 31 | required: false 32 | type: string 33 | default: "lib" 34 | runs_on: 35 | required: false 36 | type: string 37 | default: "ubuntu-latest" 38 | setup: 39 | required: false 40 | type: string 41 | default: "" 42 | working_directory: 43 | required: false 44 | type: string 45 | default: "." 46 | 47 | jobs: 48 | build: 49 | defaults: 50 | run: 51 | working-directory: ${{inputs.working_directory}} 52 | 53 | runs-on: ${{inputs.runs_on}} 54 | 55 | steps: 56 | - name: 📚 Git Checkout 57 | uses: actions/checkout@v3 58 | 59 | - name: 🎯 Setup Dart 60 | uses: dart-lang/setup-dart@v1 61 | with: 62 | sdk: ${{inputs.dart_sdk}} 63 | 64 | - name: 📦 Install Dependencies 65 | run: dart pub get 66 | 67 | - name: ⚙️ Run Setup 68 | if: "${{inputs.setup != ''}}" 69 | run: ${{inputs.setup}} 70 | 71 | - name: ✨ Check Formatting 72 | run: dart format -l 120 --set-exit-if-changed . 73 | 74 | - name: 🕵️ Analyze 75 | run: dart analyze --fatal-infos --fatal-warnings ${{inputs.analyze_directories}} 76 | 77 | - name: 🧪 Run Tests 78 | run: | 79 | dart pub global activate coverage 1.2.0 80 | dart test -j ${{inputs.concurrency}} --coverage=coverage --platform=${{inputs.platform}} && dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on=${{inputs.report_on}} 81 | 82 | - name: 📊 Check Code Coverage 83 | uses: VeryGoodOpenSource/very_good_coverage@v2 84 | with: 85 | path: ${{inputs.working_directory}}/coverage/lcov.info 86 | exclude: ${{inputs.coverage_excludes}} 87 | min_coverage: ${{inputs.min_coverage}} -------------------------------------------------------------------------------- /lib/src/sorting/heap_sort.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/utils.dart"; 2 | 3 | /// {@template heap_sort} 4 | /// Sorts a list of [elements] using the Heap Sort algorithm 5 | /// and if specified a custom [compare] function. 6 | /// 7 | /// Heap Sort is one of the most performant sort algorithms. 8 | /// It's unstable with a worst/best/average time complexity of O(n*log(n)) 9 | /// The space complexity is O(1). 10 | /// {@endtemplate} 11 | // It might be useful for embedded devices as its very compact to implement 12 | // Although is not very used in modern systems because poor cache performance. 13 | 14 | void heapSort( 15 | List elements, { 16 | int Function(E, E)? compare, 17 | }) { 18 | if (elements.isEmpty) return; 19 | 20 | final len = elements.length; 21 | // We construct the heap, as we are bubbling down/sink we need to start from the last parent 22 | // at index (len ~/ 2) - 1 up to the root, this way we ensure we are constructing a max heap. 23 | for (var i = (len ~/ 2) - 1; i >= 0; i--) { 24 | _sink(elements, i, len - 1); 25 | } 26 | // We swap the maximum element which is at the current root to the last position 27 | // of the array. And then we "shrink" the array by 1 by reducing [end]. 28 | // This way we effectively move the larger elements to the end of the array. 29 | // And keep calling sink to maintain the heap invariant. 30 | // See https://algs4.cs.princeton.edu/25applications/ for a more graphical explanation. 31 | for (var end = len - 1; end > 0; end--) { 32 | elements.swap(0, end); 33 | _sink(elements, 0, end, compare: compare); 34 | } 35 | } 36 | 37 | void _sink( 38 | List arr, 39 | int index, 40 | int len, { 41 | int Function(E, E)? compare, 42 | }) { 43 | compare ??= defaultCompare; 44 | 45 | E elementAt(int index) => arr[index] ?? (null as E); 46 | 47 | // Helper method to check if the index is not out of bounds. 48 | // Note that we don't check with the length of the array but the len 49 | // variable passed int the parameters. 50 | 51 | bool indexExists(int n) => n < len; 52 | 53 | // For a parent index n, the left child will be found at index 2*i+1 54 | int getLeftChildIndex(int i) => 2 * i + 1; 55 | 56 | // The right child will be found at index 2*i+2 57 | int getRightChildIndex(int i) => 2 * i + 2; 58 | 59 | final li = getLeftChildIndex(index); 60 | final ri = getRightChildIndex(index); 61 | 62 | var swapIndex = index; 63 | 64 | if (indexExists(li) && compare(elementAt(li), elementAt(swapIndex)) > 0) { 65 | swapIndex = li; 66 | } 67 | 68 | if (indexExists(ri) && compare(elementAt(ri), elementAt(swapIndex)) > 0) { 69 | swapIndex = ri; 70 | } 71 | 72 | if (swapIndex != index) { 73 | arr.swap(index, swapIndex); 74 | _sink(arr, swapIndex, len, compare: compare); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/sorting/merge_sort.dart: -------------------------------------------------------------------------------- 1 | import "dart:math"; 2 | import "package:dart_algorithms/src/sorting/insertion_sort.dart"; 3 | import "package:dart_algorithms/src/utils.dart"; 4 | 5 | /// {@template merge_sort} 6 | /// Sorts a list of [elements] using the MergeSort algorithm 7 | /// and if specified a custom [compare] function. 8 | /// 9 | /// Merge sort is one of the most performant sort algorithms. 10 | /// It's stable with a worst/best/average time complexity of O(n*log(n)) 11 | /// The space complexity is O(n). 12 | /// {@endtemplate} 13 | void mergeSort( 14 | List elements, { 15 | int Function(E, E)? compare, 16 | int lo = 0, 17 | int? hi, 18 | }) { 19 | hi ??= elements.length - 1; 20 | if (hi <= lo + 7) { 21 | // We fallback to insertion sort for small arrays, because: 22 | // 1. Insertion sort is more efficient for small arrays. 23 | // 2. Insertion sort is stable. 24 | insertionSort(elements, compare: compare, lo: lo, hi: hi + 1); 25 | return; 26 | } else { 27 | final mid = lo + (hi - lo) ~/ 2; 28 | 29 | mergeSort(elements, lo: lo, hi: mid, compare: compare); 30 | mergeSort(elements, lo: mid + 1, hi: hi, compare: compare); 31 | _merge(elements, lo, mid, hi, compare: compare); 32 | } 33 | } 34 | 35 | /// Iterative implementation of the Merge sort Algorithm 36 | /// {@macro merge_sort} 37 | void mergeSortIterative( 38 | List elements, { 39 | int Function(E, E)? compare, 40 | int lo = 0, 41 | int? hi, 42 | }) { 43 | compare ??= defaultCompare; 44 | 45 | final n = elements.length; 46 | 47 | for (var len = 1; len < n; len = len + len) { 48 | // len:subarray size 49 | for (var lo = 0; lo < n - len; lo += len + len) { 50 | // lo: subarray index 51 | _merge( 52 | elements, 53 | lo, 54 | lo + len - 1, 55 | min(lo + len + len - 1, n - 1), 56 | compare: compare, 57 | ); 58 | } 59 | } 60 | } 61 | 62 | void _merge( 63 | List arr, 64 | int lo, 65 | int mid, 66 | int hi, { 67 | int Function(E, E)? compare, 68 | }) { 69 | compare ??= defaultCompare; 70 | // If the last element of the left array is smaller than the first element of 71 | // the right array then the two sub-arrays are sorted. 72 | 73 | if (compare(arr[mid], arr[mid + 1]) < 0) return; 74 | 75 | final leftSize = mid - lo + 1; 76 | final rightSize = hi - mid; 77 | 78 | final leftArray = List.generate(leftSize, (i) => arr[i + lo]); 79 | final rightArray = List.generate(rightSize, (i) => arr[i + mid + 1]); 80 | 81 | for (var i = 0, j = 0, k = lo; k <= hi; k++) { 82 | if ((i < leftSize) && (j >= rightSize || compare(leftArray[i], rightArray[j]) <= 0)) { 83 | arr[k] = leftArray[i]; 84 | i++; 85 | } else { 86 | arr[k] = rightArray[j]; 87 | j++; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/src/sorting/quick_sort.dart: -------------------------------------------------------------------------------- 1 | import "dart:math"; 2 | 3 | import "package:dart_algorithms/src/sorting/insertion_sort.dart"; 4 | import "package:dart_algorithms/src/utils.dart"; 5 | 6 | part "../quick_select.dart"; 7 | 8 | //Naive and easier to implement version of quicksort, with no optimizations applied for the sake of simplicity 9 | /// {@macro quick_sort} 10 | void quickSortSimple( 11 | List elements, { 12 | int lo = 0, 13 | int? hi, 14 | int Function(E, E)? compare, 15 | }) { 16 | hi ??= elements.length - 1; 17 | if (lo >= hi) return; 18 | 19 | compare ??= defaultCompare; 20 | 21 | final pivot = _partition(elements, lo, hi, compare: compare); 22 | quickSortSimple(elements, lo: lo, hi: pivot - 1, compare: compare); 23 | quickSortSimple(elements, lo: pivot + 1, hi: hi, compare: compare); 24 | } 25 | 26 | int _partition( 27 | List elements, 28 | int lo, 29 | int hi, { 30 | int Function(E, E)? compare, 31 | Random? random, 32 | }) { 33 | compare ??= defaultCompare; 34 | var p = random != null ? random.nextInt(hi - lo) + lo : hi; 35 | elements.swap(p, hi); 36 | p = hi; 37 | final pValue = elements[p]; 38 | 39 | for (var i = lo; i < p; i++) { 40 | if (compare(elements[i], pValue) >= 0) { 41 | elements 42 | ..swap(i, p - 1) 43 | ..swap(p, p - 1); 44 | i--; 45 | p--; 46 | } 47 | } 48 | return p; 49 | } 50 | 51 | /// {@template quick_sort} 52 | /// Sorts a list of [elements] using the QuickSort algorithm 53 | /// and if specified a custom [compare] function. 54 | /// 55 | /// QuickSort is one of the most performant sort algorithms. 56 | /// It's unstable with a best/average time complexity of O(n*log(n)) 57 | /// and a worst case of O(n²) (although highly unlikely). 58 | /// The space complexity is O(1). 59 | /// {@endtemplate} 60 | 61 | // This particular implementation of quicksort has 3 common optimizations. 62 | // It uses insertion sort for small arrays. It uses a random pivot. and 63 | // it uses 3 way merge so it performs better with repeated data. 64 | // 65 | // See _quickSortSimple for an easier to remember implementation. 66 | // If curious about further optimizations to quicksort see head to: 67 | // https://cs.fit.edu/~pkc/classes/writing/samples/bentley93engineering.pdf 68 | void quickSort( 69 | List elements, { 70 | int lo = 0, 71 | int? hi, 72 | int Function(E, E)? compare, 73 | Random? random, 74 | }) { 75 | hi ??= elements.length - 1; 76 | // Cutoff to insertion sort for smaller arrays 77 | if (hi <= lo + 7) { 78 | insertionSort(elements, lo: lo, hi: hi + 1, compare: compare); 79 | return; 80 | } 81 | compare ??= defaultCompare; 82 | random ??= Random(); 83 | 84 | var lt = lo; 85 | var i = lo + 1; 86 | var gt = hi; 87 | 88 | final p = random.nextInt(hi - lo) + lo; 89 | elements.swap(p, lo); 90 | 91 | final pValue = elements[lo]; 92 | 93 | while (i <= gt) { 94 | final cmp = compare(elements[i], pValue); 95 | if (cmp < 0) { 96 | elements.swap(lt++, i++); 97 | } else if (cmp > 0) { 98 | elements.swap(i, gt--); 99 | } else { 100 | i++; 101 | } 102 | } 103 | quickSort(elements, lo: lo, hi: lt - 1, compare: compare, random: random); 104 | quickSort(elements, lo: gt + 1, hi: hi, compare: compare, random: random); 105 | } 106 | -------------------------------------------------------------------------------- /lib/src/priority_queue.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/utils.dart"; 2 | 3 | /// A Heap based priority queue. 4 | /// 5 | /// It efficiently retrieves the smallest element of a collection (Min heap). 6 | /// You can supply a custom [comparator] to change that behavior 7 | /// and make it retrieve tha largest value. 8 | // 9 | // This Priority Queue implementation is based on a Binary Heap. 10 | // Certain invariants must be maintained: 11 | // - The key in each node is larger/smaller than or equal to the keys in that node’s two children (if any) 12 | // - The largest/smallest key in a heap-ordered binary tree is found at the root. 13 | // A heap is a complete binary tree: 14 | // This means all levels are totally filled (except maybe the last level), 15 | // 16 | // If we limit the size of the heap to N, we could ob 17 | class PriorityQueue { 18 | /// Creates a Priority Queue with a custom [comparator] function 19 | PriorityQueue([this.comparator = defaultCompare]); 20 | 21 | /// Function used to compare objects in the priority queue 22 | final Comparator comparator; 23 | final List _data = []; 24 | 25 | /// Is the priority Queue empty 26 | bool get isEmpty => _data.isEmpty; 27 | 28 | /// Number of elements in the priority Queue 29 | int get size => _data.length; 30 | 31 | E _elementAt(int index) => _data[index] ?? (null as E); 32 | 33 | /// Inserts an item into the priority Queue in O(log*n). 34 | /// It makes sure the invariants on the data structure are maintained. 35 | void insert(E item) { 36 | _data.add(item); 37 | if (size > 1) _bubbleUp(size - 1); 38 | } 39 | 40 | /// Returns the smallest/largest element without removing it in O(1) 41 | E get peek { 42 | if (_data.isEmpty) throw StateError("No element"); 43 | return _data[0]!; 44 | } 45 | 46 | /// Removes the largest/smallest key off the top of the priority Queue in O(log*n). 47 | /// It makes sure the invariants on the data structure are maintained. 48 | E remove() { 49 | if (!isEmpty) { 50 | // Swap first and last item 51 | _data.swap(0, size - 1); 52 | final item = _data.removeLast() as E; 53 | if (!isEmpty) _bubbleDown(0); 54 | return item; 55 | } else { 56 | throw StateError("No element"); 57 | } 58 | } 59 | 60 | // Also known as heapify/bottomUp/swim 61 | void _bubbleUp(int index) { 62 | var currentIndex = index; 63 | while (currentIndex > 0 && 64 | comparator( 65 | _elementAt(currentIndex), 66 | _elementAt(_getParentIndex(currentIndex)), 67 | ) < 68 | 0) { 69 | _data.swap(currentIndex, _getParentIndex(currentIndex)); 70 | currentIndex = _getParentIndex(currentIndex); 71 | } 72 | } 73 | 74 | // Also known as topDown/sink 75 | void _bubbleDown(int index) { 76 | final li = _getLeftChildIndex(index); 77 | final ri = _getRightChildIndex(index); 78 | 79 | var swap = index; 80 | 81 | if (_indexExists(li) && comparator(_elementAt(li), _elementAt(swap)) < 0) { 82 | swap = li; 83 | } 84 | 85 | if (_indexExists(ri) && comparator(_elementAt(ri), _elementAt(swap)) < 0) { 86 | swap = ri; 87 | } 88 | 89 | if (swap != index) { 90 | _data.swap(index, swap); 91 | _bubbleDown(swap); 92 | } 93 | } 94 | 95 | // Helper method to check if the index is not out of bounds. 96 | bool _indexExists(int n) => n < size; 97 | 98 | //For a child at index i, its parent can be found at index (i-1)/2. 99 | int _getParentIndex(int i) => (i - 1) ~/ 2; 100 | 101 | // For a parent index n, the left child will be found at index 2*i+1 102 | int _getLeftChildIndex(int i) => 2 * i + 1; 103 | 104 | // The right child will be found at index 2*i+2 105 | int _getRightChildIndex(int i) => 2 * i + 2; 106 | } 107 | -------------------------------------------------------------------------------- /test/src/sorting_test.dart: -------------------------------------------------------------------------------- 1 | import "dart:math"; 2 | 3 | import "package:collection/collection.dart" show ListExtensions; 4 | import "package:dart_algorithms/dart_algorithms.dart"; 5 | import "package:test/test.dart"; 6 | 7 | int intCompareFn(int a, int b) => a.compareTo(b); 8 | 9 | typedef SortFunction = void Function( 10 | List elements, { 11 | int Function(E, E)? compare, 12 | }); 13 | 14 | void main() { 15 | final datasets = [ 16 | sortedList, 17 | mostlySorted, 18 | randomList, 19 | reverseOrderedList, 20 | ]; 21 | final sortingAlgorithms = { 22 | "Bubble Sort": bubbleSort, 23 | "Insertion Sort": insertionSort, 24 | "Selection Sort": selectionSort, 25 | "Shell Sort": shellSort, 26 | "Merge Sort": mergeSort, 27 | "Merge Sort Iterative": mergeSortIterative, 28 | "Quick Sort": quickSort, 29 | "Quick Sort Simple implementation": quickSortSimple, 30 | "Heap Sort": heapSort 31 | }; 32 | test("isSorted helper function", () { 33 | final list = randomList(); 34 | expect(isSorted(list, intCompareFn), false); 35 | list.sort(); 36 | expect(isSorted(list, intCompareFn), true); 37 | }); 38 | 39 | group("Sort Algorithms are correct", () { 40 | for (final sort in sortingAlgorithms.entries) { 41 | test(sort.key, () { 42 | for (final dataset in datasets) { 43 | final list = dataset(); 44 | sort.value(list, compare: intCompareFn); 45 | expect(isSorted(list, intCompareFn), true); 46 | } 47 | }); 48 | } 49 | }); 50 | test("Insertion Sort is stable", () { 51 | for (final dataset in datasets) { 52 | final list = box(dataset()).toList(); 53 | insertionSort(list); 54 | expect(isStable(list), true); 55 | } 56 | }); 57 | test("Merge Sort is stable", () { 58 | for (final dataset in datasets) { 59 | final list = box(dataset()).toList(); 60 | mergeSort(list); 61 | expect(isStable(list), true); 62 | } 63 | }); 64 | } 65 | 66 | Iterable box(List ints) => ints.mapIndexed((index, element) => BoxedInt(element, id: index)); 67 | 68 | class BoxedInt implements Comparable { 69 | final int id; 70 | final int value; 71 | 72 | BoxedInt(this.value, {required this.id}); 73 | 74 | static List intToBoxed(List ints) { 75 | return List.generate(ints.length, (index) => BoxedInt(ints[index], id: index)); 76 | } 77 | 78 | @override 79 | bool operator ==(Object other) => 80 | identical(this, other) || other is BoxedInt && runtimeType == other.runtimeType && value == other.value; 81 | 82 | @override 83 | int get hashCode => value.hashCode; 84 | 85 | @override 86 | int compareTo(BoxedInt other) { 87 | return value - other.value; 88 | } 89 | 90 | @override 91 | String toString() { 92 | return "{id: $id, value: $value}"; 93 | } 94 | } 95 | 96 | List randomList([int size = 1000]) { 97 | final random = Random(); 98 | return [for (var i = 0; i < size; i++) random.nextInt(42)]; 99 | } 100 | 101 | List mostlySorted([int size = 1000]) { 102 | final random = Random(); 103 | final list = sortedList(size); 104 | for (var i = 0; i < size ~/ 10; i++) { 105 | list.insert((i * 10) % size, random.nextInt(42)); 106 | } 107 | return randomList(size)..sort(); 108 | } 109 | 110 | List sortedList([int size = 1000]) => randomList(size)..sort(); 111 | 112 | List reverseOrderedList([int size = 1000]) => sortedList(size).reversed.toList(); 113 | 114 | bool isSorted(Iterable elements, int Function(T a, T b) compareFn) { 115 | for (var i = 1; i < elements.length; i++) { 116 | final prev = elements.elementAt(i - 1); 117 | final curr = elements.elementAt(i); 118 | if (compareFn(prev, curr) > 0) return false; 119 | } 120 | return true; 121 | } 122 | 123 | bool isStable(Iterable elements) { 124 | for (var i = 1; i < elements.length; i++) { 125 | final prev = elements.elementAt(i - 1); 126 | final curr = elements.elementAt(i); 127 | if (prev == curr && prev.id > curr.id) { 128 | return false; 129 | } 130 | } 131 | return true; 132 | } 133 | -------------------------------------------------------------------------------- /lib/src/graph/connected_components.dart: -------------------------------------------------------------------------------- 1 | import "dart:collection" show HashMap, HashSet, Queue; 2 | import "dart:math"; 3 | 4 | import "package:dart_algorithms/src/graph/dfs.dart"; 5 | import "package:dart_algorithms/src/graph/graph.dart"; 6 | import "package:dart_algorithms/src/graph/topological_sort.dart"; 7 | 8 | /// {@template connected_component} 9 | /// Pre processes a graph to answer connectivity questions in o(1) 10 | /// Vertices v and w are strongly connected if there is both a directed path 11 | /// from v to w and a directed path from w to v 12 | /// {@endtemplate} 13 | class ConnectedComponents { 14 | final Graph _graph; 15 | int _count = -1; 16 | Map _componentId = {}; 17 | 18 | /// {@macro connected_component} 19 | ConnectedComponents(this._graph) { 20 | _compute(); 21 | } 22 | 23 | void _compute() { 24 | _count = 0; 25 | _componentId = {}; 26 | if (_graph.vertices.isEmpty) return; 27 | if (!_graph.directed) { 28 | _connectedComponent(_graph.vertices); 29 | } else { 30 | _tarjanStronglyConnectedComponents(); 31 | } 32 | } 33 | 34 | void _connectedComponent(Iterable vertices) { 35 | final visited = Map.fromIterable(vertices, value: (_) => false); 36 | for (final vertex in vertices) { 37 | if (!(visited[vertex] ?? true)) { 38 | dfs( 39 | _graph, 40 | vertex, 41 | visited: visited, 42 | onVisited: (n) { 43 | _componentId[n] = _count; 44 | visited[n] = true; 45 | }, 46 | ); 47 | _count++; 48 | } 49 | } 50 | } 51 | 52 | /// O(V+E) it performs 2 DFS, one in the reversed graph. 53 | /// And a second one on the topologically sorted vertices. 54 | void _kosarajuStronglyConnectedComponents() { 55 | final reversed = _graph.reversed(); 56 | final reversedTopological = topologicalSort(reversed); 57 | _connectedComponent(reversedTopological); 58 | } 59 | 60 | /// Finds connected components in a digraph in O(V+E) 61 | void _tarjanStronglyConnectedComponents() { 62 | final lowLinks = HashMap(); 63 | final indexes = HashMap(); 64 | final onStack = HashSet(); 65 | 66 | var index = 0; 67 | final lastVisited = Queue(); 68 | 69 | void strongConnect(T vertex) { 70 | indexes[vertex] = index; 71 | var lowLink = lowLinks[vertex] = index; 72 | index++; 73 | lastVisited.addLast(vertex); 74 | onStack.add(vertex); 75 | for (final next in _graph.neighbours(vertex)) { 76 | if (!indexes.containsKey(next)) { 77 | strongConnect(next); 78 | lowLink = lowLinks[vertex] = min(lowLink, lowLinks[next]!); 79 | } else if (onStack.contains(next)) { 80 | lowLink = lowLinks[vertex] = min(lowLink, indexes[next]!); 81 | } 82 | } 83 | if (lowLinks[vertex] == indexes[vertex]) { 84 | T next; 85 | do { 86 | next = lastVisited.removeLast(); 87 | onStack.remove(next); 88 | _componentId[next] = count; 89 | } while (!_defaultEquals(next, vertex)); 90 | _count++; 91 | } 92 | } 93 | 94 | for (final vertex in _graph.vertices) { 95 | if (!indexes.containsKey(vertex)) strongConnect(vertex); 96 | } 97 | } 98 | 99 | /// The amount of connected components the graph has 100 | int get count => _count; 101 | 102 | /// Given a [vertex] it returns the identifier (int) of the connected component it belongs to. 103 | int componentId(T vertex) { 104 | if (_componentId[vertex] != null) { 105 | return _componentId[vertex]!; 106 | } 107 | throw StateError("Vertex $vertex does not exists in graph"); 108 | } 109 | 110 | ///Given a [vertex] it returns all the reachable vertices in its connected component. 111 | ///If the graph is connected, then it displays the complete graph. 112 | List component(T vertex) { 113 | final id = componentId(vertex); 114 | return _componentId.entries.where((element) => element.value == id).map((e) => e.key).toList(); 115 | } 116 | 117 | /// Whether there is a path between vertices [a] and [b]. 118 | /// Which implies they both belong to the same connected component. 119 | bool connected(T a, T b) => componentId(a) == componentId(b); 120 | 121 | @override 122 | String toString() { 123 | final sb = StringBuffer(); 124 | for (final entry in _componentId.entries) { 125 | sb.writeln("[${entry.key}] => ${entry.value}"); 126 | } 127 | return sb.toString(); 128 | } 129 | } 130 | 131 | bool _defaultEquals(Object a, Object b) => a == b; 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dart Algorithms 2 | 3 | [![License: MIT][license_badge]][license_link] 4 | 5 | A collection of Algorithms and Data structures implemented in dart. 6 | 7 | This repository is intended for educational purposes, you could probably find more optimized implementations of the data 8 | structures and algorithms here described in the standard library or the [collections package][collections_package]. 9 | 10 | Most of the implementations in this repository can be further studied in the 11 | book [Algorithms 4th edition][algorithms_book] . 12 | 13 | ### Sorting 14 | 15 | - [x] Bubble Sort 16 | - [x] Selection Sort 17 | - [x] Insertion Sort 18 | - [x] Shell Sort 19 | - [x] Merge Sort 20 | - [x] Quick Sort 21 | - [x] Heap Sort 22 | 23 | | Algorithm | Stable | In Place | Running time | Extra space | Notes | 24 | |------------------|--------|----------|-------------------|-------------|----------------------------------| 25 | | Selection Sort | no | yes | n² | 1 | | 26 | | Insertion sort | yes | yes | n - n² | 1 | Performs well if input is sorted | 27 | | Shell Sort | no | yes | n*log(n)- n^(3/2) | 1 | | 28 | | Quick Sort | no | yes | n*log(n) - n² | log(n) | Unlikely to hit worst case | 29 | | 3 Way Quick sort | no | yes | n - n*log(n) | log(n) | Depends on distribution of keys | 30 | | Merge Sort | yes | no | n*log(n) | n | | 31 | | Heap Sort | no | yes | n*log(n) | 1 | | 32 | 33 | ### Data Structures 34 | 35 | - [x] Priority Queue 36 | - [x] Union Find 37 | 38 | ### Algorithms 39 | 40 | - [x] Binary Search 41 | - [x] Quick Select 42 | - [x] Knuth-Morris-Pratt 43 | 44 | ### Graphs 45 | 46 | #### Representations 47 | 48 | We represent graphs most commonly either with adjacency matrices or adjacency lists. 49 | 50 | For any graph given V vertices and E edges: 51 | 52 | | Operation/Representation | Adjacency Matrix | Adjacency List | 53 | |--------------------------|------------------|----------------| 54 | | Edge between v and w | O(1) | O(deg(v)) | 55 | | Neighbours | O(v) | O(deg(v)) | 56 | | Space required | O(v²) | O(v+e) | 57 | 58 | > Degree(deg): The number of adjacent vertices of a given vertex. 59 | 60 | > Note 61 | > Real-world graphs tend to be sparse so an adjacency list is most commonly used. 62 | 63 | #### Algorithms 64 | 65 | - [x] Depth First Search (DFS) 66 | - [x] Breadth First Search (BFS) 67 | - [x] Topological Sort 68 | - [x] Cycle detection 69 | - [x] Strongly connected components (SCC) 70 | - [x] Dijkstra 71 | - [x] Bellman-Ford 72 | - [x] Edmonds-Karp 73 | - [ ] A* 74 | 75 | | Algorithm | Running time | Extra space | Notes | 76 | |------------------|--------------|-------------|-------------------------------------------------| 77 | | DFS | O(v+e) | O(v) | | 78 | | BFS | O(v+e) | O(v) | | 79 | | Topological sort | O(v+e) | O(v) | Requires no directed cycles | 80 | | SCC | O(v+e) | O(v) | | 81 | | Cycle detection | O(v+e) | O(v) | | 82 | | Dijkstra | O(E*logV) | O(V) | Requires no negative weights or negative cycles | 83 | | Bellman-Ford | O(E*V) | O(V) | Requires no negative cycles | 84 | | Edmonds-Karp | O(V*E²) | O(V) | | 85 | 86 | --- 87 | 88 | ## Running Tests 🧪 89 | 90 | To run all unit tests: 91 | 92 | ```sh 93 | dart pub global activate very_good_cli 94 | very_good test --coverage 95 | ``` 96 | 97 | To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov). 98 | 99 | ```sh 100 | # Generate Coverage Report 101 | genhtml coverage/lcov.info -o coverage/ 102 | 103 | # Open Coverage Report 104 | open coverage/index.html 105 | ``` 106 | 107 | [algorithms_book]: https://algs4.cs.princeton.edu/home/ 108 | [collections_package]: https://pub.dev/packages/collection 109 | [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg 110 | [license_link]: https://opensource.org/licenses/MIT 111 | -------------------------------------------------------------------------------- /lib/src/graph/shortest_path.dart: -------------------------------------------------------------------------------- 1 | import "package:collection/collection.dart" show PriorityQueue; 2 | import "package:dart_algorithms/src/graph/graph.dart"; 3 | 4 | /// Represents the shortest paths from [source] to all other vertices on a graph. 5 | /// You can query it to retrieve an iterable with the shortest path to any vertex. 6 | /// Or the minimum cost. 7 | /// ``` 8 | /// final result=dijkstra(graph,source:0); 9 | /// 10 | /// result.shortestPath(7);//[0,2,7] 11 | /// result.minimumCost(7);// 0.6 12 | /// 13 | /// ``` 14 | class ShortestPathResult { 15 | final Map _cost; 16 | final Map _previous; 17 | 18 | /// The starting vertex from which the shortest path was computed. 19 | final T source; 20 | 21 | /// Creates an object representing the result of running a "Single [source] shortest path" algorithm. 22 | /// [_cost] is a map between a vertex and the minimum cost to reach it. 23 | /// [_previous] is a map in which the key is the target vertex and the value the previous vertex in the shortest path 24 | /// to the key. 25 | ShortestPathResult(this.source, this._cost, this._previous); 26 | 27 | /// Returns an iterable containing the vertices with the shortest path from [source] to [sink] 28 | Iterable shortestPath(T sink) { 29 | final output = [sink]; 30 | var current = sink; 31 | while (current != source) { 32 | final previous = _previous[current] as T; 33 | output.insert(0, previous); 34 | current = previous; 35 | } 36 | return output; 37 | } 38 | 39 | /// Minimum cost of traversing from [source] to [vertex] 40 | num cost(T vertex) { 41 | if (_cost.containsKey(vertex)) return _cost[vertex]!; 42 | throw Exception("Vertex $vertex is not found "); 43 | } 44 | } 45 | 46 | /// Dijkstra’s algorithm is a Greedy algorithm 47 | /// Returns the shortest path from [source] to all other edges of a given [graph] with non negative weights. 48 | /// It does so in O(E*log V) time and O(V) space. 49 | ShortestPathResult dijkstra( 50 | WeightedGraph graph, { 51 | required T source, 52 | }) { 53 | // pre condition 54 | if (graph.hasNegativeWeight) throw Exception("Can't calculate shortest path on a graph with negative weight"); 55 | 56 | // Book keeping 57 | final cost = Map.fromIterable(graph.vertices, value: (_) => double.infinity); 58 | int compareByCost(T a, T b) => (cost[a] ?? double.infinity).compareTo(cost[b] ?? double.infinity); 59 | final pq = PriorityQueue(compareByCost); 60 | final prev = {}; 61 | 62 | cost[source] = 0; 63 | pq.add(source); 64 | 65 | while (pq.isNotEmpty) { 66 | final vertex = pq.removeFirst(); 67 | 68 | for (final edge in graph.edges(vertex)) { 69 | final previousVertex = edge.a; 70 | final current = edge.b; 71 | final newWeight = cost[previousVertex]! + edge.weight; 72 | if (newWeight < (cost[current] ?? double.infinity)) { 73 | cost[current] = newWeight; 74 | prev[current] = previousVertex; 75 | pq.add(current); 76 | } 77 | } 78 | } 79 | 80 | return ShortestPathResult(source, cost, prev); 81 | } 82 | 83 | /// Performs the Bellman Ford algorithm to get the shortest paths from [source] to 84 | /// all vertices. The [graph] is required to not have negative cycles and will raise an exception if one is found. 85 | ShortestPathResult bellmanFord(WeightedGraph graph, {required T source}) { 86 | // Book keeping 87 | final cost = Map.fromIterable(graph.vertices, value: (_) => double.infinity); 88 | int compareByCost(T a, T b) => (cost[a] ?? double.infinity).compareTo(cost[b] ?? double.infinity); 89 | final pq = PriorityQueue(compareByCost); 90 | 91 | final onQ = Map.fromIterable(graph.vertices, value: (_) => false); 92 | final prev = {}; 93 | 94 | cost[source] = 0; 95 | pq.add(source); 96 | onQ[source] = true; 97 | 98 | bool hasNegativeCycle() { 99 | for (final edge in graph.allEdges) { 100 | final previousVertex = edge.a; 101 | final current = edge.b; 102 | final newWeight = cost[previousVertex]! + edge.weight; 103 | 104 | if (newWeight < (cost[current] ?? double.infinity)) { 105 | return true; 106 | } 107 | } 108 | return false; 109 | } 110 | 111 | final allEdges = graph.allEdges; 112 | for (var i = 1; i < graph.vertices.length; i++) { 113 | for (final edge in allEdges) { 114 | final previousVertex = edge.a; 115 | final current = edge.b; 116 | final newWeight = cost[previousVertex]! + edge.weight; 117 | 118 | if (newWeight < (cost[current] ?? double.infinity)) { 119 | cost[current] = newWeight; 120 | prev[current] = previousVertex; 121 | } 122 | } 123 | } 124 | 125 | if (hasNegativeCycle()) throw Exception("Negative cycle detected"); 126 | return ShortestPathResult(source, cost, prev); 127 | } 128 | -------------------------------------------------------------------------------- /lib/src/graph/max_flow.dart: -------------------------------------------------------------------------------- 1 | import "dart:collection"; 2 | import "dart:math"; 3 | 4 | import "package:dart_algorithms/src/graph/graph.dart"; 5 | 6 | /// Graph that represents a flow network, in which edges have a capacity/weight and a flow. 7 | typedef FlowWeightedGraph = EdgeWeightedGraph>; 8 | 9 | /// Specific type of weighted edge used in flow networks,apart from having a weight/capacity 10 | /// they also have a flow value associated with them 11 | class FlowEdge extends WeightedEdge { 12 | /// The current flow passing through this edge. 13 | /// As an invariant its value MUST always be =< than its [capacity] 14 | num flow; 15 | 16 | /// The maximum amount of throughput the edge has 17 | num get capacity => weight; 18 | 19 | FlowEdge(super.a, super.b, {this.flow = 0, num capacity = 0}) : super(weight: capacity); 20 | 21 | /// Residual capacity relative to [vertex]. If [vertex] is the source the residual capacity is the current [flow]. 22 | /// Else the residual capacity is the difference between the edges maximum [capacity] and its current flow 23 | /// (capacity-flow) also known as the back edge. 24 | num residualCapacityTo(T vertex) { 25 | if (vertex == a) { 26 | return flow; 27 | } else if (vertex == b) { 28 | return capacity - flow; 29 | } else { 30 | throw Exception("Vertex $vertex does not exists in edge $this"); 31 | } 32 | } 33 | 34 | /// Increases the flow by [delta] relative to [vertex]. If [vertex] is the source of the edge the flow is reduced. 35 | /// Else the flow is increased by [delta] 36 | void addResidualFlow(T vertex, num delta) { 37 | if (vertex == a) { 38 | flow -= delta; 39 | } else if (vertex == b) { 40 | flow += delta; 41 | } else { 42 | throw Exception("Vertex $vertex does not exists in edge $this"); 43 | } 44 | } 45 | 46 | @override 47 | bool operator ==(Object other) => 48 | identical(this, other) || 49 | other is FlowEdge && 50 | runtimeType == other.runtimeType && 51 | flow == other.flow && 52 | a == other.a && 53 | b == other.b && 54 | weight == other.weight; 55 | 56 | @override 57 | int get hashCode => flow.hashCode ^ a.hashCode ^ b.hashCode ^ weight.hashCode; 58 | } 59 | 60 | /// Represents the max flow result in a graph started at [source] and finishing at [sink] 61 | class MaxFlowMinCutResult { 62 | /// The maximum flow in the network 63 | final num maxFlow; 64 | final Map> _previous; 65 | 66 | /// Vertex from which the maximum flow started 67 | final T source; 68 | 69 | /// Vertex to which the maximum flow is directed 70 | final T sink; 71 | 72 | MaxFlowMinCutResult(this.maxFlow, this._previous, this.source, this.sink); 73 | 74 | List> edges() => _previous.values.toList(); 75 | } 76 | 77 | ///{@macro maxflow} 78 | /// Implementation of the Ford–Fulkerson method with O(V*E²) worst time complexity 79 | // Other more performant implementations exist like push relabel, goldberg rao or more recently electrical flow 80 | //https://www.youtube.com/watch?v=RppuJYwlcI8 81 | MaxFlowMinCutResult edmondsKarp(FlowWeightedGraph graph, {required T source, required T sink}) { 82 | bool hasAugmentingPathBFS( 83 | FlowWeightedGraph graph, 84 | Map> prev, { 85 | required T source, 86 | required T sink, 87 | }) { 88 | final visited = Map.fromIterable(graph.vertices, value: (_) => false); 89 | prev.clear(); 90 | 91 | final queue = Queue()..add(source); 92 | visited[source] = true; 93 | 94 | while (queue.isNotEmpty && !visited[sink]!) { 95 | final v = queue.removeFirst(); 96 | for (final e in graph.edges(v)) { 97 | final w = e.other(v); 98 | // Explore only nodes that have remaining capacity and have not been visited yet 99 | if (e.residualCapacityTo(w) > 0 && !(visited[w] ?? true)) { 100 | prev[w] = e; 101 | visited[w] = true; 102 | queue.addFirst(w); 103 | } 104 | } 105 | } 106 | // If any node made it to the sink we still have an augmenting path 107 | // In the prev map we can retrace the augmenting path from source to sink 108 | // By using BFS we guarantee we have a shortest length augmented path 109 | return visited[sink]!; 110 | } 111 | 112 | return _fordFulkerson(graph, source: source, sink: sink, hasAugmentingPath: hasAugmentingPathBFS); 113 | } 114 | 115 | /// Function that computes whether a [graph] has an augmenting path. 116 | typedef AugmentingPathFunction = bool Function( 117 | FlowWeightedGraph graph, 118 | Map> prev, { 119 | required T source, 120 | required T sink, 121 | }); 122 | 123 | /// {@template maxflow} 124 | /// Greedy algorithm that computes the maximum flow/minimum cut in a flow network 125 | /// {@endtemplate} 126 | /// It is sometimes called a "method" instead of an "algorithm" as the approach to finding augmenting paths 127 | /// is not fully specified. In this implementation it is provided via [hasAugmentingPath] parameter. 128 | MaxFlowMinCutResult _fordFulkerson(FlowWeightedGraph graph, 129 | {required T source, required T sink, required AugmentingPathFunction hasAugmentingPath}) { 130 | final prev = >{}; 131 | 132 | num value = 0; 133 | // flow at every vertex is >=0 && <= edge capacity 134 | // every vertex has local equilibrium: inflow = outflow 135 | while (hasAugmentingPath(graph, prev, source: source, sink: sink)) { 136 | num bottleneck = double.infinity; 137 | // Reconstruct path and find the edge with the lowest capacity aka the bottleneck 138 | for (var v = sink; v != source; v = prev[v]!.other(v)) { 139 | bottleneck = min(bottleneck, prev[v]!.residualCapacityTo(v)); 140 | } 141 | // Augment the values along the path by the bottleneck 142 | for (var v = sink; v != source; v = prev[v]!.other(v)) { 143 | prev[v]!.addResidualFlow(v, bottleneck); 144 | } 145 | // We have effectively augmented the flow in the graph by bottleneck 146 | value += bottleneck; 147 | } 148 | return MaxFlowMinCutResult(value, prev, source, sink); 149 | } 150 | -------------------------------------------------------------------------------- /lib/src/graph/graph.dart: -------------------------------------------------------------------------------- 1 | import "dart:collection"; 2 | 3 | /// Represents a graph data structure 4 | abstract class Graph { 5 | /// If true the graph edges are directed 6 | bool get directed; 7 | 8 | Graph(Iterable vertices); 9 | 10 | /// A list of all the neighbours of [vertex] 11 | Set neighbours(T vertex); 12 | 13 | /// Returns all the vertices of a given Graph 14 | Set get vertices; 15 | 16 | /// Whether the graph contains [vertex] 17 | bool contains(T vertex); 18 | 19 | ///Connects vertices [a] and [b] 20 | /// If the graph is directed only [a] will be connected to [b]. 21 | /// If the graph is not directed [a]->[b] and [b]->[a] 22 | void addEdge(T a, T b); 23 | 24 | /// Adds a new vertex to the graph 25 | void addVertex(T a); 26 | 27 | /// The degree of a vertex [vertex]. The degree is the amount of edges a vertex has 28 | /// If the vertex does not exists it returns -1 29 | int degree(T vertex); 30 | 31 | /// Returns a reversed representation of a graph. 32 | /// If the graph is undirected the reverse and original representations are the same. 33 | /// If it's a digraph then all wdges between a and b are reversed 34 | Graph reversed(); 35 | } 36 | 37 | ///{@template adj_list} 38 | /// Graph representation backed by an adjacency list. It requires O(v+e) space and is better suited for sparse graphs 39 | ///{@endtemplate} 40 | // Most graphs in practice are sparse 41 | class AdjacencyListGraph implements Graph { 42 | final HashMap> _list = HashMap(); 43 | @override 44 | final bool directed; 45 | 46 | /// Directed defines if the graph is a Digraph. 47 | 48 | AdjacencyListGraph(Iterable vertices, {this.directed = false}) { 49 | for (final vertex in vertices) { 50 | _list[vertex] = {}; 51 | } 52 | } 53 | 54 | @override 55 | void addEdge(T a, T b) { 56 | if (_list[a] == null || _list[b] == null) { 57 | throw StateError("Can't add edge between two vertices that do not exist"); 58 | } 59 | _list[a]!.add(b); 60 | if (!directed) { 61 | _list[b]!.add(a); 62 | } 63 | } 64 | 65 | @override 66 | void addVertex(T a) { 67 | _list[a] = {}; 68 | } 69 | 70 | @override 71 | bool contains(T vertex) => _list[vertex] != null; 72 | 73 | @override 74 | int degree(T vertex) => _list[vertex]?.length ?? -1; 75 | 76 | @override 77 | Set neighbours(T vertex) { 78 | if (contains(vertex)) return _list[vertex]!; 79 | throw StateError("Error retrieving neighbours of vertex $vertex does not exist"); 80 | } 81 | 82 | @override 83 | Set get vertices => _list.keys.toSet(); 84 | 85 | @override 86 | String toString() { 87 | final sb = StringBuffer(); 88 | for (final entry in _list.entries) { 89 | sb.writeln("[${entry.key}] => ${entry.value}"); 90 | } 91 | return sb.toString(); 92 | } 93 | 94 | @override 95 | Graph reversed() { 96 | assert(directed, "Graph must be directed in order to be reversed"); 97 | if (!directed) return this; 98 | 99 | final R = AdjacencyListGraph(vertices, directed: directed); 100 | 101 | for (final vertex in vertices) { 102 | for (final w in neighbours(vertex)) { 103 | R.addEdge(w, vertex); 104 | } 105 | } 106 | return R; 107 | } 108 | } 109 | 110 | /// {@template ewg} 111 | /// Graph in which its edges have a weight. 112 | /// {@endtemplate ewg} 113 | abstract class EdgeWeightedGraph> { 114 | /// If true the graph is directed 115 | bool get directed; 116 | 117 | /// Whether any edge has negative weight. 118 | bool get hasNegativeWeight; 119 | 120 | /// Returns a set of all edges in the graph 121 | Set> get allEdges; 122 | 123 | /// Set of all vertices in the graph 124 | Set get vertices; 125 | 126 | /// Given a [vertex] it returns all the its edges to other vertices 127 | Set edges(V vertex); 128 | 129 | /// Adds an [edge] to the graph. May throw an exception if any of its vertices do not exist. 130 | void addEdge(E edge); 131 | } 132 | 133 | ///{@macro ewg} 134 | typedef WeightedGraph = EdgeWeightedGraph>; 135 | 136 | /// Represents an edge between vertices of a graph that is weighted connecting vertices [a] and [b] 137 | class WeightedEdge implements Comparable> { 138 | /// First vertex connected by the edge. If the graph is directed this is the source vertex. 139 | final T a; 140 | 141 | /// Second vertex connected by the edge. If the graph is directed this is the target vertex. 142 | final T b; 143 | 144 | /// Weight of the edge 145 | final num weight; 146 | 147 | /// Constructs an edge between vertex [a] and [b] with weight [weight](zero if omited) 148 | WeightedEdge(this.a, this.b, {this.weight = 0}); 149 | 150 | @override 151 | int compareTo(WeightedEdge other) => weight.compareTo(other.weight); 152 | 153 | @override 154 | String toString() => "$a--[$weight]--$b"; 155 | 156 | /// Given a vertex [x] return the other vertex in the edge. If [x] is not part of the edge an exception is thrown 157 | T other(T x) { 158 | if (x == a) return b; 159 | if (x == b) return a; 160 | throw Exception("Can't return other node in the edge as $x is not $a or $b"); 161 | } 162 | } 163 | 164 | ///{@macro adj_list} 165 | class AdjacencyListWeightedGraph> implements EdgeWeightedGraph { 166 | final HashMap> _list = HashMap(); 167 | @override 168 | final bool directed; 169 | @override 170 | bool hasNegativeWeight = false; 171 | 172 | /// Directed defines if the graph is a Digraph. 173 | 174 | AdjacencyListWeightedGraph(Iterable vertices, {this.directed = false}) { 175 | for (final vertex in vertices) { 176 | _list[vertex] = {}; 177 | } 178 | } 179 | 180 | @override 181 | void addEdge(E edge) { 182 | if (_list[edge.a] == null || _list[edge.b] == null) { 183 | throw StateError("Can't add edge between two vertices that do not exist"); 184 | } 185 | if (edge.weight <= 0) { 186 | hasNegativeWeight = true; 187 | } 188 | _list[edge.a]!.add(edge); 189 | if (!directed) { 190 | _list[edge.b]!.add(edge); 191 | } 192 | } 193 | 194 | @override 195 | void addVertex(T a) { 196 | _list[a] = {}; 197 | } 198 | 199 | bool contains(T vertex) => _list[vertex] != null; 200 | 201 | int degree(T vertex) => _list[vertex]?.length ?? -1; 202 | 203 | @override 204 | Set edges(T vertex) { 205 | if (contains(vertex)) return _list[vertex]!; 206 | throw StateError("Error retrieving neighbours of vertex $vertex does not exist"); 207 | } 208 | 209 | @override 210 | Set get allEdges => _list.values.fold({}, (prev, curr) => prev.union(curr)); 211 | 212 | @override 213 | Set get vertices => _list.keys.toSet(); 214 | 215 | @override 216 | String toString() { 217 | final sb = StringBuffer(); 218 | for (final entry in _list.entries) { 219 | sb.writeln("[${entry.key}] => ${entry.value}"); 220 | } 221 | return sb.toString(); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /test/src/graph_test.dart: -------------------------------------------------------------------------------- 1 | import "package:dart_algorithms/src/graph/bfs.dart"; 2 | import "package:dart_algorithms/src/graph/connected_components.dart"; 3 | import "package:dart_algorithms/src/graph/cycle_detection.dart"; 4 | import "package:dart_algorithms/src/graph/dfs.dart"; 5 | import "package:dart_algorithms/src/graph/graph.dart"; 6 | import "package:dart_algorithms/src/graph/max_flow.dart"; 7 | import "package:dart_algorithms/src/graph/minimum_spanning_tree.dart"; 8 | import "package:dart_algorithms/src/graph/shortest_path.dart"; 9 | import "package:dart_algorithms/src/graph/topological_sort.dart"; 10 | import "package:test/test.dart"; 11 | import "fixtures/fixtures.dart"; 12 | 13 | void main() { 14 | late final tinyG = fromInput(loadTestFile("tinyG.txt")); 15 | late final sample = fromInput(loadTestFile("sample.txt")); 16 | late final tinyDirectedG = fromInput(loadTestFile("tinyDG.txt"), directed: true); 17 | late final tinyDirectedG2 = fromInput(loadTestFile("tinyDG2.txt"), directed: true); 18 | late final tinyDirectedG3 = fromInput(loadTestFile("tinyDG3.txt"), directed: true); 19 | late final tinyEWG = fromInputWeighted(loadTestFile("tinyEWG.txt")); 20 | 21 | group("Graph ", () { 22 | test(" is correctly constructed when directed", () { 23 | expect(tinyDirectedG.degree(7), 2); 24 | 25 | expect(tinyDirectedG2.toString().isNotEmpty, true); 26 | 27 | expect(tinyDirectedG.neighbours(7).contains(9), true); 28 | expect(tinyDirectedG.neighbours(9).contains(7), false); 29 | expect(dfs(tinyDirectedG, 7), unorderedEquals(tinyDirectedG.vertices)); 30 | expect(dfs(tinyDirectedG, 0), unorderedEquals([0, 1, 2, 3, 4, 5])); 31 | }); 32 | 33 | group("Cycle ", () { 34 | test("is correctly detected in case of existence", () { 35 | expect(hasCycle(tinyDirectedG), true); 36 | expect(hasCycle(tinyDirectedG2), true); 37 | expect(hasCycle(tinyDirectedG3), false); 38 | 39 | final tinyGD = fromInput(loadTestFile("tinyG.txt"), directed: true); 40 | final sampleD = fromInput(loadTestFile("sample.txt"), directed: true); 41 | final selfLoop = fromInput(loadTestFile("selfLoop.txt"), directed: true); 42 | 43 | expect(hasCycle(tinyGD), false); 44 | expect(hasCycle(sampleD), false); 45 | expect(hasCycle(selfLoop), true); 46 | }); 47 | 48 | test("first cycle found is correctly listed", () { 49 | final selfLoop = fromInput(loadTestFile("selfLoop.txt"), directed: true); 50 | expect(findCycle(selfLoop), [0, 0]); 51 | expect( 52 | findCycle(tinyDirectedG), 53 | anyOf([ 54 | [2, 3, 2], 55 | [5, 4, 2, 3, 5], 56 | [0, 5, 4, 2, 0], 57 | [6, 8, 6], 58 | [9, 10, 12, 9], 59 | [9, 11, 12, 9], 60 | ]), 61 | ); 62 | }); 63 | }); 64 | 65 | group("Traversal ", () { 66 | test("Depth First Search", () { 67 | expect(dfs(sample, 0), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 68 | expect(dfs(tinyG, 0), [0, 5, 4, 3, 6, 1, 2]); 69 | }); 70 | test("Breadth First Search", () { 71 | expect(bfs(sample, 0), [0, 1, 4, 8, 2, 5, 7, 9, 3, 6]); 72 | expect(bfs(tinyG, 0), [0, 5, 1, 2, 6, 4, 3]); 73 | expect(bfs(tinyDirectedG2, 0), [0, 1, 2, 4, 3, 5]); 74 | }); 75 | test("Topological sort", () { 76 | expect(topologicalSort(tinyDirectedG3), [3, 6, 0, 5, 2, 1, 4]); 77 | }); 78 | }); 79 | 80 | group("Connected components", () { 81 | test("given a graph with 3 connected components they get correctly identified", () { 82 | final cc = ConnectedComponents(tinyG); 83 | 84 | expect(cc.count, 3); 85 | 86 | expect(cc.connected(0, 6), true); 87 | expect(cc.componentId(5), 0); 88 | expect(cc.component(0), unorderedEquals([0, 1, 2, 3, 4, 5, 6])); 89 | expect(cc.component(7), unorderedEquals([7, 8])); 90 | expect(cc.component(12), unorderedEquals([12, 11, 10, 9])); 91 | 92 | expect(cc.connected(0, 8), false); 93 | expect(cc.connected(7, 11), false); 94 | }); 95 | test( 96 | "given a fully connected graph only one connected component gets identified " 97 | "and all vertices belong to that unique connected component", () { 98 | final cc = ConnectedComponents(sample); 99 | expect(cc.count, 1); 100 | for (final vertex in sample.vertices) { 101 | expect(sample.vertices, unorderedEquals(cc.component(vertex))); 102 | } 103 | }); 104 | test("given a disconnected graph each connected component gets identified", () { 105 | final cc = ConnectedComponents(tinyDirectedG); 106 | expect(tinyDirectedG.directed, true); 107 | expect(cc.count, 5); 108 | 109 | final groups = [ 110 | [1], 111 | [0, 2, 3, 4, 5], 112 | [9, 10, 11, 12], 113 | [6, 8], 114 | [7] 115 | ]; 116 | for (final group in groups) { 117 | final componentId = cc.componentId(group.first); 118 | for (final vertex in group) { 119 | expect(cc.componentId(vertex), componentId); 120 | } 121 | } 122 | }); 123 | }); 124 | 125 | group("Minimum Spanning Trees", () { 126 | test("Kruskal", () { 127 | // ignore: prefer_int_literals 128 | expect(kruskal(tinyEWG).fold(0.0, (a, b) => a + b.weight), 1.81); 129 | }); 130 | test("Prim", () { 131 | // ignore: prefer_int_literals 132 | expect(prim(tinyEWG).fold(0.0, (a, b) => a + b.weight), 1.81); 133 | }); 134 | }); 135 | 136 | group("Single source shortest Path", () { 137 | final tinyEWDG = fromInputWeighted(loadTestFile("tinyEWDG.txt")); 138 | final tinyEWDGnc = fromInputWeighted(loadTestFile("tinyEWDGnc.txt")); 139 | final tinyEWDGnw = fromInputWeighted(loadTestFile("tinyEWDGnw.txt")); 140 | 141 | final pathWeights = [0, 1.05, 0.26, 0.99, 0.38, 0.73, 1.51, 0.60]; 142 | final shortestPaths = >{ 143 | 0: [0], 144 | 1: [0, 4, 5, 1], 145 | 2: [0, 2], 146 | 3: [0, 2, 7, 3], 147 | 4: [0, 4], 148 | 5: [0, 4, 5], 149 | 6: [0, 2, 7, 3, 6], 150 | 7: [0, 2, 7], 151 | }; 152 | test("Dijkstra calculates the single source shortest path correctly", () { 153 | final sp = dijkstra(tinyEWDG, source: 0); 154 | for (final vertex in tinyEWDG.vertices) { 155 | expect(sp.cost(vertex), closeTo(pathWeights[vertex], 0.01)); 156 | expect(sp.shortestPath(vertex), shortestPaths[vertex]); 157 | } 158 | }); 159 | 160 | test("Dijkstra throws exception when graph has negative weight", () { 161 | final tinyEWDG = fromInputWeighted(loadTestFile("tinyEWDG.txt"))..addEdge(WeightedEdge(0, 1, weight: -3)); 162 | 163 | expect(() => dijkstra(tinyEWDG, source: 0), throwsException); 164 | expect(() => dijkstra(tinyEWDGnc, source: 0), throwsException); 165 | }); 166 | test("Bellman-Ford calculates the single source shortest path correctly", () { 167 | final sp = bellmanFord(tinyEWDG, source: 0); 168 | for (final vertex in tinyEWDG.vertices) { 169 | expect(sp.cost(vertex), closeTo(pathWeights[vertex], 0.01)); 170 | expect(sp.shortestPath(vertex), shortestPaths[vertex]); 171 | } 172 | }); 173 | 174 | test("Bellman-Ford calculates the shortest path when a graph has negative weights", () { 175 | final pathWeights = [0, -1, 2, -2, 1]; 176 | final sp = bellmanFord(tinyEWDGnw, source: 0); 177 | for (final vertex in tinyEWDGnw.vertices) { 178 | expect(sp.cost(vertex), pathWeights[vertex]); 179 | } 180 | }); 181 | test("Bellman-Ford throws exception when graph has negative cycle", () { 182 | expect(() => bellmanFord(tinyEWDGnc, source: 0), throwsException); 183 | }); 184 | }); 185 | test("Maximum Flow : Edmonds-Karp correctly calculates the maximum flow for different flow networks", () { 186 | final tinyFN = fromInputFlow(loadTestFile("tinyFN.txt")); 187 | final tinyFN2 = fromInputFlow(loadTestFile("tinyFN2.txt")); 188 | final tinyFN3 = fromInputFlow(loadTestFile("tinyFN3.txt")); 189 | final flowEdges = >[ 190 | FlowEdge(0, 2, capacity: 3, flow: 2), 191 | FlowEdge(0, 1, capacity: 2, flow: 2), 192 | FlowEdge(1, 4, capacity: 1, flow: 1), 193 | FlowEdge(1, 3, capacity: 3, flow: 1), 194 | FlowEdge(2, 3, capacity: 1, flow: 1), 195 | FlowEdge(2, 4, capacity: 1, flow: 1), 196 | FlowEdge(3, 5, capacity: 2, flow: 2), 197 | FlowEdge(4, 5, capacity: 3, flow: 2), 198 | ]; 199 | final result = edmondsKarp(tinyFN, source: 0, sink: 5); 200 | 201 | expect(result.maxFlow, 4); 202 | expect(tinyFN.allEdges, unorderedEquals(flowEdges)); 203 | 204 | expect(edmondsKarp(tinyFN2, source: 0, sink: 3).maxFlow, 200); 205 | expect(edmondsKarp(tinyFN3, source: 0, sink: 7).maxFlow, 10); 206 | }); 207 | }); 208 | } 209 | 210 | WeightedGraph fromInputWeighted(List input, {bool directed = false}) { 211 | final vertices = int.parse(input.removeAt(0)); 212 | final edges = input.removeAt(0); 213 | final graph = 214 | AdjacencyListWeightedGraph>(List.generate(vertices, (i) => i), directed: directed); 215 | 216 | for (final edge in input) { 217 | final s = edge.split(" "); 218 | final a = int.parse(s.first); 219 | final b = int.parse(s[1]); 220 | final weight = num.parse(s.last); 221 | graph.addEdge(WeightedEdge(a, b, weight: weight)); 222 | } 223 | return graph; 224 | } 225 | 226 | FlowWeightedGraph fromInputFlow(List input, {bool directed = false}) { 227 | final vertices = int.parse(input.removeAt(0)); 228 | final edges = input.removeAt(0); 229 | final graph = AdjacencyListWeightedGraph>(List.generate(vertices, (i) => i), directed: directed); 230 | 231 | for (final edge in input) { 232 | final s = edge.split(" "); 233 | final a = int.parse(s.first); 234 | final b = int.parse(s[1]); 235 | final weight = num.parse(s.last); 236 | graph.addEdge(FlowEdge(a, b, capacity: weight)); 237 | } 238 | return graph; 239 | } 240 | 241 | Graph fromInput(List input, {bool directed = false}) { 242 | final vertices = int.parse(input.removeAt(0)); 243 | final edges = input.removeAt(0); 244 | final graph = AdjacencyListGraph(List.generate(vertices, (i) => i), directed: directed); 245 | 246 | for (final edge in input) { 247 | final s = edge.split(" "); 248 | final a = int.parse(s.first); 249 | final b = int.parse(s.last); 250 | graph.addEdge(a, b); 251 | } 252 | return graph; 253 | } 254 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_Packages.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | --------------------------------------------------------------------------------