feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код'

This commit is contained in:
2026-06-30 17:00:41 +03:00
parent 898f43bf73
commit 7832ec4f07
4 changed files with 193 additions and 120 deletions
+39 -48
View File
@@ -1,60 +1,51 @@
# Graph with Reflection on Code Refactored Implementation # Graph with Reflection
This repository contains a minimal, selfcontained Python implementation of an undirected graph that uses a single, consistent approach: an adjacency list represented by a dictionary of sets. This project implements an undirected graph using an adjacency list and provides reflection capabilities that record all operations performed on the graph.
The original assignment required that the solution use only one approach; this refactor removes any mixedstrategy code and provides a clean, welldocumented API.
## Features ## Features
- **Add / remove nodes** Nodes are any hashable Python objects. - **Adjacency List**: Efficient storage and traversal of graph nodes and edges.
- **Add / remove edges** Undirected edges; selfloops (reflexive edges) are allowed. - **Reflection**: Every mutating operation (`addNode`, `addEdge`, `removeNode`, `removeEdge`) is logged.
- **Query adjacency** Retrieve neighbors, check for an edge, list all nodes or edges. - **API**:
- **Automatic node creation** Adding an edge automatically creates missing nodes. - `addNode(node)`
- **Readable representation** `__repr__` and `__str__` give a quick overview of the graph. - `addEdge(u, v)`
- `removeNode(node)`
- `removeEdge(u, v)`
- `getNeighbors(node)`
- `hasEdge(u, v)`
- `getNodes()`
- `getLog()` returns a copy of the operation log.
## Installation
```bash
npm install
```
## Running Tests
```bash
npm test
```
## Usage ## Usage
```python ```js
from src.index import Graph const { createGraph } = require('./src/index');
# Create an empty graph const graph = createGraph();
g = Graph()
# Add edges (nodes are created automatically) graph.addNode('a');
g.add_edge("A", "B") graph.addNode('b');
g.add_edge("B", "C") graph.addEdge('a', 'b');
g.add_edge("C", "A") # triangle
g.add_edge("D", "D") # reflexive edge
print(g) # Pretty print console.log(graph.getNeighbors('a')); // ['b']
console.log(graph.getLog());
# Query // [
print("Neighbors of B:", g.neighbors("B")) // { method: 'addNode', args: ['a'] },
print("Has edge (A, D)?", g.has_edge("A", "D")) // { method: 'addNode', args: ['b'] },
// { method: 'addEdge', args: ['a', 'b'] }
# Modify // ]
g.remove_edge("A", "B")
g.remove_node("C")
print("After modifications:")
print(g)
``` ```
## Running the Example The `getLog()` method returns a snapshot of all recorded operations, allowing you to inspect the history of changes made to the graph.
```bash
python -m src.index
```
The script will output the graph state after each operation.
## Project Structure
```
src/
└── index.py # Graph implementation
README.md # Documentation
```
## License
This project is released under the MIT License.
+5 -9
View File
@@ -1,19 +1,15 @@
{ {
"name": "graph-reflexive", "name": "graph-reflection",
"version": "1.0.0", "version": "1.0.0",
"description": "Graph implementation with reflexive property using adjacency list", "description": "Graph implementation with reflection using adjacency list",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"test": "jest" "test": "jest"
}, },
"keywords": [ "keywords": [],
"graph", "author": "",
"reflexive",
"adjacency-list"
],
"author": "Auto-generated",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"jest": "^29.7.0" "jest": "^29.6.1"
} }
} }
+82 -4
View File
@@ -1,5 +1,83 @@
const Graph = require('./graph'); class Graph {
constructor() {
this.adj = new Map();
this[Graph._logSymbol] = [];
}
module.exports = { addNode(node) {
Graph if (!this.adj.has(node)) {
}; this.adj.set(node, new Set());
}
}
addEdge(u, v) {
if (!this.adj.has(u)) this.adj.set(u, new Set());
if (!this.adj.has(v)) this.adj.set(v, new Set());
const uSet = this.adj.get(u);
const vSet = this.adj.get(v);
if (!uSet.has(v)) {
uSet.add(v);
vSet.add(u);
}
}
removeNode(node) {
if (!this.adj.has(node)) return;
for (const neighbor of this.adj.get(node)) {
this.adj.get(neighbor).delete(node);
}
this.adj.delete(node);
}
removeEdge(u, v) {
if (this.adj.has(u)) this.adj.get(u).delete(v);
if (this.adj.has(v)) this.adj.get(v).delete(u);
}
getNeighbors(node) {
return this.adj.has(node) ? Array.from(this.adj.get(node)) : [];
}
hasEdge(u, v) {
return this.adj.has(u) && this.adj.get(u).has(v);
}
getNodes() {
return Array.from(this.adj.keys());
}
getLog() {
return this[Graph._logSymbol].slice();
}
}
Graph._logSymbol = Symbol('log');
function createGraph() {
const graph = new Graph();
const handler = {
get(target, prop, receiver) {
const value = target[prop];
if (typeof value === 'function') {
// Mutating methods that should be logged
if (['addNode', 'addEdge', 'removeNode', 'removeEdge'].includes(prop)) {
return function (...args) {
const result = value.apply(target, args);
target[Graph._logSymbol].push({ method: prop, args });
return result;
};
}
// Non-mutating methods (including getLog)
return value.bind(target);
}
return value;
},
};
return new Proxy(graph, handler);
}
module.exports = { Graph, createGraph };
+67 -59
View File
@@ -1,79 +1,87 @@
const { Graph } = require('../src'); const { createGraph } = require('../src/index');
describe('Graph', () => {
let g;
describe('Graph with reflection', () => {
let graph;
beforeEach(() => { beforeEach(() => {
g = new Graph(); graph = createGraph();
}); });
test('initially empty', () => { test('initial log is empty', () => {
expect(g.size()).toBe(0); expect(graph.getLog()).toEqual([]);
expect(g.edgesCount()).toBe(0);
}); });
test('addVertex increases size and adds reflexive edge', () => { test('addNode records operation', () => {
g.addVertex('a'); graph.addNode('a');
expect(g.size()).toBe(1); expect(graph.getLog()).toEqual([{ method: 'addNode', args: ['a'] }]);
expect(g.isReflexive()).toBe(true); expect(graph.getNodes()).toEqual(['a']);
expect(g.hasEdge('a', 'a')).toBe(true);
}); });
test('addEdge connects vertices and updates adjacency', () => { test('addEdge records operation and creates nodes', () => {
g.addEdge('a', 'b'); graph.addEdge('a', 'b');
expect(g.size()).toBe(2); expect(graph.getLog()).toEqual([{ method: 'addEdge', args: ['a', 'b'] }]);
expect(g.hasEdge('a', 'b')).toBe(true); expect(graph.getNodes().sort()).toEqual(['a', 'b']);
expect(g.hasEdge('b', 'a')).toBe(true); expect(graph.getNeighbors('a')).toEqual(['b']);
expect(g.getNeighbors('a')).toEqual(expect.arrayContaining(['a', 'b'])); expect(graph.getNeighbors('b')).toEqual(['a']);
expect(g.getNeighbors('b')).toEqual(expect.arrayContaining(['a', 'b'])); expect(graph.hasEdge('a', 'b')).toBe(true);
}); });
test('removeEdge removes connection', () => { test('removeEdge records operation', () => {
g.addEdge('a', 'b'); graph.addEdge('a', 'b');
g.removeEdge('a', 'b'); graph.removeEdge('a', 'b');
expect(g.hasEdge('a', 'b')).toBe(false); expect(graph.getLog()).toEqual([
expect(g.hasEdge('b', 'a')).toBe(false); { method: 'addEdge', args: ['a', 'b'] },
// self-loops remain { method: 'removeEdge', args: ['a', 'b'] }
expect(g.hasEdge('a', 'a')).toBe(true); ]);
expect(g.hasEdge('b', 'b')).toBe(true); expect(graph.hasEdge('a', 'b')).toBe(false);
}); });
test('removeVertex removes vertex and incident edges', () => { test('removeNode records operation and removes edges', () => {
g.addEdge('a', 'b'); graph.addEdge('a', 'b');
g.addEdge('a', 'c'); graph.addEdge('a', 'c');
g.removeVertex('a'); graph.removeNode('a');
expect(g.size()).toBe(2); expect(graph.getLog()).toEqual([
expect(g.hasEdge('b', 'a')).toBe(false); { method: 'addEdge', args: ['a', 'b'] },
expect(g.hasEdge('c', 'a')).toBe(false); { method: 'addEdge', args: ['a', 'c'] },
expect(g.hasEdge('b', 'c')).toBe(false); { method: 'removeNode', args: ['a'] }
]);
expect(graph.getNodes().sort()).toEqual(['b', 'c']);
expect(graph.getNeighbors('b')).toEqual([]);
expect(graph.getNeighbors('c')).toEqual([]);
}); });
test('edges and edgesCount work correctly', () => { test('getNeighbors does not record operation', () => {
g.addEdge('a', 'b'); graph.addNode('a');
g.addEdge('b', 'c'); graph.getNeighbors('a');
g.addEdge('c', 'a'); expect(graph.getLog()).toEqual([{ method: 'addNode', args: ['a'] }]);
expect(g.edgesCount()).toBe(3);
const edges = g.edges();
expect(edges).toEqual(expect.arrayContaining([
['a', 'b'],
['b', 'c'],
['c', 'a']
]));
}); });
test('self-loop handling', () => { test('log is a copy and not affected by external mutation', () => {
g.addVertex('x'); graph.addNode('a');
expect(g.hasEdge('x', 'x')).toBe(true); const log = graph.getLog();
g.removeEdge('x', 'x'); log.push({ method: 'fake', args: [] });
expect(g.hasEdge('x', 'x')).toBe(false); expect(graph.getLog()).toEqual([{ method: 'addNode', args: ['a'] }]);
}); });
test('reflexivity can be toggled', () => { test('graph uses adjacency list internally', () => {
g.addVertex('p'); graph.addNode('a');
g.addVertex('q'); expect(graph.adj instanceof Map).toBe(true);
g.removeReflexive(); expect(graph.adj.get('a') instanceof Set).toBe(true);
expect(g.isReflexive()).toBe(false); });
g.makeReflexive();
expect(g.isReflexive()).toBe(true); test('handles duplicate nodes and edges gracefully', () => {
graph.addNode('a');
graph.addNode('a');
expect(graph.getNodes()).toEqual(['a']);
graph.addEdge('a', 'a');
expect(graph.hasEdge('a', 'a')).toBe(true);
graph.addEdge('a', 'a');
expect(graph.getNeighbors('a')).toEqual(['a']);
});
test('handles non-existing nodes and edges', () => {
expect(graph.getNeighbors('x')).toEqual([]);
expect(graph.hasEdge('x', 'y')).toBe(false);
graph.removeEdge('x', 'y'); // should not throw
graph.removeNode('x'); // should not throw
}); });
}); });