feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код'
This commit is contained in:
@@ -1,60 +1,51 @@
|
|||||||
# Graph with Reflection on Code – Refactored Implementation
|
# Graph with Reflection
|
||||||
|
|
||||||
This repository contains a minimal, self‑contained 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 mixed‑strategy code and provides a clean, well‑documented 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; self‑loops (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
@@ -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
@@ -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
@@ -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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user